import { CheckRegularIcon } from "alouette-icons/phosphor-icons/Check"; import { InfoRegularIcon } from "alouette-icons/phosphor-icons/Info"; import { WarningDuotoneIcon, WarningRegularIcon, } from "alouette-icons/phosphor-icons/Warning"; import { XRegularIcon } from "alouette-icons/phosphor-icons/X"; import type { ReactNode } from "react"; import type { VariantProps } from "tailwind-variants"; import type { Except } from "type-fest"; import type { Accent } from "../../core/AlouetteConfig"; import { tv } from "../../core/tv"; import { IconButton } from "../actions/IconButton"; import { AccentScope } from "../containers/AccentScope"; import { Box } from "../containers/Box"; import { Icon, type SVGIconElement } from "../primitives/Icon"; import { Text } from "../primitives/Text"; const messageFrameVariants = tv({ base: "flex-row items-center overflow-hidden bg-highlight-accent", variants: { size: { sm: "gap-xs p-sm rounded-xs", md: "gap-m p-m rounded-sm", lg: "gap-l p-l rounded-md", }, variant: { // Raised: the banner is its own layer above the screen background. surface: "shadow-m", // Flush: for a banner already inside a raised surface, where a second // elevation would read as a card stacked on a card. flat: "shadow-none border-border-muted border", }, }, defaultVariants: { size: "md", variant: "surface" }, }); type MessageVariantProps = VariantProps; type MessageSize = NonNullable; export type MessageVariant = NonNullable; const ICON_SIZE: Record = { sm: 20, md: 24, lg: 28 }; const DISMISS_BUTTON_SIZE: Record = { sm: 24, md: 40, lg: 40, }; interface MessageBaseProps { accent: Accent; size?: MessageSize; /** * "surface" (default) is a raised banner. Use "flat" only when the message * already sits inside a raised surface (a Modal footer, a `surface` card). */ variant?: MessageVariant; icon: SVGIconElement; children?: ReactNode; } interface MessagePropsWithDismiss extends MessageBaseProps { onDismiss: () => void; dismissIconAriaLabel: string; } interface MessagePropsWithoutDismiss extends MessageBaseProps { onDismiss?: undefined; dismissIconAriaLabel?: undefined; } export type MessageProps = MessagePropsWithDismiss | MessagePropsWithoutDismiss; export function Message({ icon, size = "md", variant, accent, children, onDismiss, dismissIconAriaLabel, }: MessageProps): ReactNode { const dismissDiameter = DISMISS_BUTTON_SIZE[size]; return ( {/* React Native defaults flexShrink to 0: without `shrink` the text keeps its content width and pushes the dismiss button out of the frame instead of wrapping. */} {children} {onDismiss ? ( } iconSize={size === "sm" ? "fill" : undefined} size={dismissDiameter} variant="soft" // The frame is overflow-hidden: an outer ring would be clipped. withFocusVisibleOutline="inset" aria-label={dismissIconAriaLabel} onPress={onDismiss} /> ) : null} ); } type AccentMessageProps = Except; export function InfoMessage(props: AccentMessageProps): ReactNode { return } />; } export function ConfirmationMessage(props: AccentMessageProps): ReactNode { return } />; } export function WarningMessage(props: AccentMessageProps): ReactNode { return } />; } export function ErrorMessage(props: AccentMessageProps): ReactNode { return } />; }