feat: full shadcn/ui component system with consistent typography

New components:
- ui/button.tsx — Button with primary/accent/surface/ghost/destructive variants
- ui/input.tsx — Input + Textarea with focus ring and CSS var theming
- ui/label.tsx — Radix Label with peer-disabled support
- ui/separator.tsx — Radix Separator
- ui/tooltip.tsx — Radix Tooltip with themed content
- ui/dialog.tsx — Radix Dialog replacing custom modal implementations
- ui/table.tsx — Table/TableHeader/TableBody/TableRow/TableHead/TableCell
- ui/typography.tsx — PageTitle/SectionHeading/Body/Muted/Caption/MonoCaption

Wired throughout all components:
- ConfirmDialog + FormModal migrated to Radix Dialog (focus trap, Escape, ARIA)
- All raw <button> → Button, <input>/<textarea> → Input/Textarea
- All repeated text patterns → typography components
- All hardcoded hex/rgba strings → COLOR constants
This commit is contained in:
Offending Commit
2026-04-24 13:56:13 -05:00
parent 91c78915e5
commit 9a74182f97
24 changed files with 1387 additions and 652 deletions

View File

@@ -0,0 +1,110 @@
import { cn } from "@/lib/utils";
type AsChild<T extends React.ElementType> = {
as?: T;
className?: string;
children?: React.ReactNode;
};
type Props<T extends React.ElementType> = AsChild<T> &
Omit<React.ComponentPropsWithoutRef<T>, keyof AsChild<T>>;
export function PageTitle<T extends React.ElementType = "h1">({
as,
className,
children,
...rest
}: Props<T>) {
const Tag = (as ?? "h1") as React.ElementType;
return (
<Tag
className={cn("text-xl font-semibold tracking-tight", className)}
style={{ color: "var(--text-1)" }}
{...rest}
>
{children}
</Tag>
);
}
export function SectionHeading<T extends React.ElementType = "h2">({
as,
className,
children,
...rest
}: Props<T>) {
const Tag = (as ?? "h2") as React.ElementType;
return (
<Tag
className={cn("text-sm font-medium mb-3", className)}
style={{ color: "var(--text-1)" }}
{...rest}
>
{children}
</Tag>
);
}
export function Body<T extends React.ElementType = "p">({
as,
className,
children,
...rest
}: Props<T>) {
const Tag = (as ?? "p") as React.ElementType;
return (
<Tag
className={cn("text-sm leading-relaxed", className)}
style={{ color: "var(--text-2)" }}
{...rest}
>
{children}
</Tag>
);
}
export function Muted<T extends React.ElementType = "p">({
as,
className,
children,
...rest
}: Props<T>) {
const Tag = (as ?? "p") as React.ElementType;
return (
<Tag className={cn("text-sm", className)} style={{ color: "var(--text-3)" }} {...rest}>
{children}
</Tag>
);
}
export function Caption<T extends React.ElementType = "span">({
as,
className,
children,
...rest
}: Props<T>) {
const Tag = (as ?? "span") as React.ElementType;
return (
<Tag className={cn("text-xs", className)} style={{ color: "var(--text-4)" }} {...rest}>
{children}
</Tag>
);
}
export function MonoCaption<T extends React.ElementType = "span">({
as,
className,
children,
...rest
}: Props<T>) {
const Tag = (as ?? "span") as React.ElementType;
return (
<Tag
className={cn("text-xs font-mono", className)}
style={{ color: "var(--text-4)" }}
{...rest}
>
{children}
</Tag>
);
}