diff --git a/client/components/ui/app-primitives.tsx b/client/components/ui/app-primitives.tsx new file mode 100644 index 0000000..6bcc4d4 --- /dev/null +++ b/client/components/ui/app-primitives.tsx @@ -0,0 +1,159 @@ +import type { ComponentType, ReactNode } from 'react'; +import { Inbox } from 'lucide-react'; +import { cn } from '@/lib/utils'; +import { BrandGlyph } from '@/components/brand/Brand'; +import type { BrandGlyphName } from '@/lib/brand'; + +type Tone = 'neutral' | 'good' | 'warn' | 'danger' | 'info'; + +const toneClasses: Record = { + neutral: 'border-border/75 bg-card/95 text-muted-foreground', + good: 'border-emerald-500/25 bg-emerald-500/[0.07] text-emerald-700 dark:text-emerald-300', + warn: 'border-amber-500/30 bg-amber-500/[0.08] text-amber-700 dark:text-amber-300', + danger: 'border-rose-500/30 bg-rose-500/[0.08] text-rose-700 dark:text-rose-300', + info: 'border-sky-500/25 bg-sky-500/[0.07] text-sky-700 dark:text-sky-300', +}; + +const dotClasses: Record = { + neutral: 'bg-muted-foreground/55', + good: 'bg-emerald-500', + warn: 'bg-amber-500', + danger: 'bg-rose-500', + info: 'bg-sky-500', +}; + +interface PageHeaderProps { + eyebrow?: ReactNode; + title: ReactNode; + description?: ReactNode; + glyph?: BrandGlyphName; + icon?: ComponentType<{ className?: string }>; + actions?: ReactNode; + meta?: ReactNode; + className?: string; +} + +export function PageHeader({ + eyebrow, + title, + description, + glyph, + icon: Icon, + actions, + meta, + className, +}: PageHeaderProps) { + return ( +
+
+ {glyph && } + {Icon && !glyph && ( + + + + )} +
+ {eyebrow && ( +

+ {eyebrow} +

+ )} +

+ {title} +

+ {description && ( +

+ {description} +

+ )} + {meta &&
{meta}
} +
+
+ {actions &&
{actions}
} +
+ ); +} + +export function SectionSurface({ + children, + className, + tone = 'neutral', +}: { + children: ReactNode; + className?: string; + tone?: Tone; +}) { + return ( +
+ {children} +
+ ); +} + +export function StatusDot({ tone = 'neutral', pulse = false, className }: { tone?: Tone; pulse?: boolean; className?: string }) { + return ( +