From 4898987a18546506f510687b8889105504198ec9 Mon Sep 17 00:00:00 2001 From: null Date: Sun, 5 Jul 2026 17:18:23 -0500 Subject: [PATCH] feat(brand): shared app-primitives UI kit Adds the cross-cutting UI primitives the rest of the brand refresh consumes. The 159-line file bundles the small, repeated bits every page/dialog needs so the per-page commits can stay minimal: - TonedCard: bordered card with neutral / good / warn / danger / info variants (border + bg + text per tone), consumed by the overdue/summary/bank-sync cards. - ToneDot: small status dot for the new badges. - EmptyState: lucide Inbox + heading + body used for filtered-empty and zero-data states. - SectionHeading / FieldRow: small typographic primitives for the Settings / Profile / Admin pages. - BrandGlyph (re-export of the Brand module) so consumers only need to import from '@/components/ui/app-primitives'. This is commit 2 of 9 in the v0.42.0 brand refresh. Depends on dfee7f9 (brand module); consumed by the layout/UI/consumer commits. --- client/components/ui/app-primitives.tsx | 159 ++++++++++++++++++++++++ 1 file changed, 159 insertions(+) create mode 100644 client/components/ui/app-primitives.tsx 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 ( +