"use client" /** * Banner — two types of banner for system-level and local-level messaging. * * 1. SystemBanner: full-width strip at the very top of the app (above the * utility bar). Use `flush` for shell chrome (no radius / outer shadow). * Used for maintenance notices, feature promotions, global alerts. * * 2. LocalBanner: inline alert within a page section. * Used for page-specific errors, warnings, and informational messages. * * Both support: info, warning, error, success, and promo variants. * Both are dismissible and accessible (role="alert" / role="status"). */ import * as React from "react" import { cva, type VariantProps } from "class-variance-authority" import { cn } from "../../lib/utils" import { Button } from "./button" import { Tip } from "./tip" // ───────────────────────────────────────────────────────────────────────────── // Variant definitions // ───────────────────────────────────────────────────────────────────────────── const VARIANT_CONFIG = { info: { icon: "fa-circle-info", prefix: "fa-light", role: "status" as const, live: "polite" as const, }, warning: { icon: "fa-triangle-exclamation", prefix: "fa-light", role: "alert" as const, live: "assertive" as const, }, error: { icon: "fa-circle-exclamation", prefix: "fa-light", role: "alert" as const, live: "assertive" as const, }, success: { icon: "fa-circle-check", prefix: "fa-light", role: "status" as const, live: "polite" as const, }, promo: { icon: "fa-star-christmas text-brand", prefix: "fa-duotone fa-solid", role: "status" as const, live: "polite" as const, }, } // ───────────────────────────────────────────────────────────────────────────── // SystemBanner — inline at the top of the main content area // ───────────────────────────────────────────────────────────────────────────── const systemBannerActionClassName = cn( "inline-flex shrink-0 items-center gap-1 text-inherit text-xs font-semibold", "underline underline-offset-2 hover:no-underline", "rounded-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-current/50", ) const systemBannerVariants = cva( // No `overflow-hidden` on root — it clips promo `boxShadow` (glow). Clip `decorativeOverlay` in a nested layer instead. "relative isolate flex rounded-lg border text-sm transition-[color,background-color,border-color,box-shadow] duration-150 ease-out", { variants: { variant: { /* Prominent defaults — chip fill + inverted `background` ink. Chip tokens are defined to clear 4.5:1 against `--background` in light, dark, and HC. */ info: "bg-chip-1 text-background border-chip-1", warning: "bg-chip-4 text-background border-chip-4", error: "bg-destructive text-destructive-foreground border-destructive", success: "bg-chip-2 text-background border-chip-2", promo: "bg-gradient-to-r from-brand/14 via-brand/8 to-brand/5 text-foreground border-brand/22 dark:from-brand/20 dark:via-brand/12 dark:to-brand/7 dark:border-brand/26", }, /** Emphasis — "prominent" uses dark solid fills; "subtle" uses soft tinted bg. */ emphasis: { prominent: "", subtle: "", }, /** Action placement: "inline" puts the action to the right; "bottom" puts it below the text */ actionPosition: { // flex-wrap so the action button drops to a second line at 200%+ zoom instead of overflowing inline: "flex-wrap items-center gap-3 px-4 py-2.5", bottom: "flex-col gap-2 px-4 py-3", }, }, compoundVariants: [ { emphasis: "subtle", variant: "info", class: "bg-chip-1/5 text-foreground border-chip-1/30 dark:bg-chip-1/10 dark:border-chip-1/20" }, { emphasis: "subtle", variant: "warning", class: "bg-chip-4/5 text-foreground border-chip-4/30 dark:bg-chip-4/10 dark:border-chip-4/20" }, { emphasis: "subtle", variant: "error", class: "bg-destructive/5 text-foreground border-destructive/30 dark:bg-destructive/10 dark:border-destructive/20" }, { emphasis: "subtle", variant: "success", class: "bg-chip-2/5 text-foreground border-chip-2/30 dark:bg-chip-2/10 dark:border-chip-2/20" }, { emphasis: "subtle", variant: "promo", class: "bg-brand/5 text-foreground border-brand/30 dark:bg-brand/10 dark:border-brand/20" }, ], defaultVariants: { variant: "info", emphasis: "prominent", actionPosition: "inline" }, } ) export interface SystemBannerProps extends React.HTMLAttributes, VariantProps { /** Banner title (optional — adds a bold heading) */ title?: string /** The banner message */ children: React.ReactNode /** Whether the banner can be dismissed */ dismissible?: boolean /** Callback when dismissed */ onDismiss?: () => void /** Optional action — renders as a link-style button. Use href for server components, onClick for client. */ action?: { label: string href?: string onClick?: () => void } /** Where to place the action: "inline" (right side) or "bottom" (below text) */ actionPosition?: "inline" | "bottom" /** Override the default icon */ icon?: string /** Optional absolutely-positioned decorative layer (e.g. ``). */ decorativeOverlay?: React.ReactNode /** * Full-bleed shell strip (above the utility bar). Drops radius, outer * shadow, and side borders so the banner reads as chrome, not a card. */ flush?: boolean } export function SystemBanner({ children, title, variant = "info", emphasis = "prominent", dismissible = true, onDismiss, action, actionPosition = "inline", icon, decorativeOverlay, flush = false, className, style, ...props }: SystemBannerProps) { const [dismissed, setDismissed] = React.useState(false) const config = VARIANT_CONFIG[variant ?? "info"] function handleDismiss() { setDismissed(true) onDismiss?.() } if (dismissed) return null const actionEl = action && ( action.href ? ( {action.label}