Spaces:
Running
Running
File size: 2,853 Bytes
5f3e9f5 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 | import type { ReactNode } from 'react'
import { AlertCircle, AlertTriangle, CheckCircle2, Clock, Info } from 'lucide-react'
export type BannerTone = 'info' | 'success' | 'warning' | 'danger' | 'neutral'
interface Props {
tone?: BannerTone
title?: ReactNode
children?: ReactNode
/** Optional leading icon override. Falls back to a tone-appropriate icon. */
icon?: ReactNode
/** Optional trailing actions (buttons, links). */
actions?: ReactNode
/** ARIA role: `alert` for important conditions, `status` for soft notices. */
role?: 'alert' | 'status'
/** Extra class names appended to the outer container. */
className?: string
}
const TONE_CLASS: Record<BannerTone, string> = {
info:
'border-indigo-200 bg-indigo-50 text-indigo-900 dark:border-indigo-500/30 dark:bg-indigo-500/10 dark:text-indigo-100',
success:
'border-emerald-200 bg-emerald-50 text-emerald-900 dark:border-emerald-500/30 dark:bg-emerald-500/10 dark:text-emerald-100',
warning:
'border-amber-300/70 bg-amber-50 text-amber-900 dark:border-amber-500/30 dark:bg-amber-500/10 dark:text-amber-100',
danger:
'border-rose-200 bg-rose-50 text-rose-900 dark:border-rose-500/30 dark:bg-rose-500/10 dark:text-rose-100',
neutral:
'border-[rgb(var(--line))] bg-[rgb(var(--bg-muted))] text-[rgb(var(--text-strong))]',
}
function defaultIcon(tone: BannerTone): ReactNode {
switch (tone) {
case 'info':
return <Info size={16} />
case 'success':
return <CheckCircle2 size={16} />
case 'warning':
return <Clock size={16} />
case 'danger':
return <AlertTriangle size={16} />
default:
return <AlertCircle size={16} />
}
}
/**
* Inline banner — single primitive for soft alerts, info notices,
* warnings, and rejected-action states. Replaces ad-hoc colored cards
* scattered across pages so tone, spacing, and accessibility are
* defined in one place.
*/
export default function Banner({
tone = 'info',
title,
children,
icon,
actions,
role,
className,
}: Props) {
const resolvedRole = role ?? (tone === 'danger' || tone === 'warning' ? 'alert' : 'status')
return (
<div
role={resolvedRole}
className={
'flex flex-wrap items-start gap-3 rounded-md border px-3 py-2.5 text-sm ' +
TONE_CLASS[tone] +
(className ? ' ' + className : '')
}
>
<span className="mt-0.5 shrink-0">{icon ?? defaultIcon(tone)}</span>
<div className="min-w-0 flex-1">
{title && <div className="font-medium leading-snug">{title}</div>}
{children && (
<div className={'text-[12.5px] leading-snug opacity-90 ' + (title ? 'mt-0.5' : '')}>
{children}
</div>
)}
</div>
{actions && <div className="flex shrink-0 flex-wrap items-center gap-1.5">{actions}</div>}
</div>
)
}
|