import { CircleAlertIcon, CircleCheckIcon, InfoIcon, TriangleAlertIcon } from "lucide-react"; import type { ReactNode } from "react"; import { cn } from "../lib/cn"; /* ---------------------------------------------------------------------------- Notice — the one inline panel for states that need a quiet word (doctrine D7). Replaces every hand-rolled amber/red/emerald box. Tone budget follows the chip doctrine's spirit: color marks the exception, so `muted` is the default and tinted tones are reserved for states that genuinely differ (a failure, an action waiting on the user, a one-time success confirmation). -------------------------------------------------------------------------- */ export type NoticeTone = "muted" | "info" | "success" | "waiting" | "failed"; // Read live icon imports at render time: production chunk cycles can initialize // this table before the icon modules, so eagerly captured values may be undefined. const TONE: Record typeof InfoIcon }> = { muted: { box: "border-border bg-surface/40 text-fg-muted", icon: "text-fg-subtle", glyph: () => InfoIcon, }, info: { box: "border-brand/30 bg-brand/[0.06] text-fg", icon: "text-brand", glyph: () => InfoIcon, }, success: { box: "border-status-idle/30 bg-status-idle/[0.06] text-fg", icon: "text-status-idle", glyph: () => CircleCheckIcon, }, waiting: { box: "border-status-waiting/30 bg-status-waiting/[0.06] text-fg", icon: "text-status-waiting", glyph: () => CircleAlertIcon, }, failed: { box: "border-status-failed/30 bg-status-failed/[0.06] text-fg", icon: "text-status-failed", glyph: () => TriangleAlertIcon, }, }; export function Notice({ tone = "muted", title, children, action, icon, onDismiss, dismissLabel = "Dismiss", layout, actionLayout, live, className, }: { tone?: NoticeTone | undefined; /** Short bolded lead. Omit for a single-sentence notice. */ title?: ReactNode | undefined; children?: ReactNode | undefined; /** Right-aligned action (a small Button or link). */ action?: ReactNode | undefined; /** Override the tone glyph; pass null to render no icon. */ icon?: ReactNode | null | undefined; /** Adds a close button. Only for notices the person may safely hide. */ onDismiss?: (() => void) | undefined; /** Accessible name of the close button. */ dismissLabel?: string | undefined; /** * "banner" spans a whole page or workspace: square corners, a bottom rule * only, one 32px line that keeps the icon and action on the first line * when the text wraps. Omit for the inline panel. */ layout?: "banner" | undefined; /** * "responsive" moves the action under the text when the notice is * narrower than 448px, so phone widths don't squeeze the message. */ actionLayout?: "responsive" | undefined; /** * Announce it when it appears: "polite" for results of an action, * "assertive" only for failures that block what the person is doing. */ live?: "polite" | "assertive" | undefined; className?: string | undefined; }) { const meta = TONE[tone]; const Glyph = meta.glyph(); return (
{icon === null ? null : ( {icon ?? } )}
{/* A title is always full-strength text, whatever the tone; the body under it is the muted description. */} {title ?
{title}
: null} {children ? (
{children}
) : null}
{action ? (
{action}
) : null} {onDismiss ? ( ) : null}
); }