From bf28f01655d793c2e21ae35a339da0f608147c93 Mon Sep 17 00:00:00 2001 From: DCCA Date: Sat, 20 Jun 2026 18:54:27 -0300 Subject: [PATCH] refactor(ui): build semantic design-token system and harden primitives Resolves the full component audit. The token layer was previously half-built (an unused --ring token, no semantic colors) and bypassed by every primitive; there was also no shared motion language. Tokens & theming: - Add semantic HSL tokens (primary/secondary/muted/accent/destructive + hover, border, input, ring) in globals.css, mapped in tailwind.config.js. Components now reference tokens instead of hardcoded Tailwind literals, giving a single source of truth. - Add a .dark token block (opt-in via class="dark"; light stays default). Kept outside @layer base so Tailwind does not tree-shake the selector. - Add a shared `ease-swift` cubic-bezier and enable darkMode: "class". Primitives: - Button: token colors, transform+shadow motion on ease-swift, active:scale-[0.98] press feedback, offset focus ring (ring-ring + ring-offset), icon slot (gap-2 + [&_svg]), inner top highlight on filled variants, disabled:cursor-not-allowed. - Card: token surface with machined depth (inner highlight + soft tinted shadow), concentric rounded-lg, CardTitle gains an `as` prop (no longer hardcodes

), refined font-bold weight. - Input/Select/Textarea: unified --input border, offset focus ring, aria-[invalid=true] error state, shadow/transition parity. Select now renders a custom chevron (appearance-none) instead of the native arrow. - Badge: tokens + shape variant (pill default for counts, square option). - Separator: role/aria-orientation semantics + end-faded gradient. Usage sites: - Editor: dedicated color-input treatment; page title -> h1, sections -> h2. - Popup: square "New" badge; title -> h1. - Viewer: title -> h1, "Annotated Image" -> h2. Verified: npm run check green; confirmed token utilities and the .dark block compile into the built CSS. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/components/ui/badge.tsx | 19 ++++++---- src/components/ui/button.tsx | 17 +++++---- src/components/ui/card.tsx | 12 ++++--- src/components/ui/input.tsx | 2 +- src/components/ui/select.tsx | 30 +++++++++++----- src/components/ui/separator.tsx | 10 ++++-- src/components/ui/textarea.tsx | 2 +- src/editor/main.tsx | 14 +++++--- src/popup/main.tsx | 6 ++-- src/styles/globals.css | 63 +++++++++++++++++++++++++++++++-- src/viewer/main.tsx | 6 ++-- tailwind.config.js | 34 ++++++++++++++++-- 12 files changed, 171 insertions(+), 44 deletions(-) diff --git a/src/components/ui/badge.tsx b/src/components/ui/badge.tsx index 65e9e7d..ed2a948 100644 --- a/src/components/ui/badge.tsx +++ b/src/components/ui/badge.tsx @@ -3,17 +3,22 @@ import { cva, type VariantProps } from "class-variance-authority"; import { cn } from "@/lib/utils"; const badgeVariants = cva( - "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors", + "inline-flex items-center border px-2.5 py-0.5 text-xs font-semibold", { variants: { variant: { - default: "border-slate-200 bg-slate-100 text-slate-700", - accent: "border-emerald-200 bg-emerald-100 text-emerald-800", - danger: "border-red-200 bg-red-100 text-red-700" + default: "border-transparent bg-muted text-muted-foreground", + accent: "border-transparent bg-accent text-accent-foreground", + danger: "border-transparent bg-destructive/10 text-destructive" + }, + shape: { + pill: "rounded-full", + square: "rounded-md" } }, defaultVariants: { - variant: "default" + variant: "default", + shape: "pill" } } ); @@ -21,8 +26,8 @@ const badgeVariants = cva( export interface BadgeProps extends React.HTMLAttributes, VariantProps {} -function Badge({ className, variant, ...props }: BadgeProps): JSX.Element { - return
; +function Badge({ className, variant, shape, ...props }: BadgeProps): JSX.Element { + return
; } export { Badge, badgeVariants }; diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx index 7270d2e..eed42fb 100644 --- a/src/components/ui/button.tsx +++ b/src/components/ui/button.tsx @@ -3,19 +3,22 @@ import { cva, type VariantProps } from "class-variance-authority"; import { cn } from "@/lib/utils"; const buttonVariants = cva( - "inline-flex items-center justify-center rounded-md text-sm font-semibold transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-emerald-600/50 disabled:pointer-events-none disabled:opacity-50", + "inline-flex items-center justify-center gap-2 rounded-md text-sm font-semibold transition-[transform,background-color,box-shadow] duration-200 ease-swift focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background active:scale-[0.98] disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 [&_svg]:size-4 [&_svg]:shrink-0", { variants: { variant: { - default: "bg-emerald-700 text-emerald-50 hover:bg-emerald-800", - secondary: "bg-slate-100 text-slate-900 hover:bg-slate-200 border border-slate-200", - ghost: "text-slate-700 hover:bg-slate-100", - destructive: "bg-red-600 text-white hover:bg-red-700" + default: + "bg-primary text-primary-foreground shadow-[inset_0_1px_0_0_hsl(0_0%_100%/0.12)] hover:bg-primary-hover", + secondary: + "border border-border bg-secondary text-secondary-foreground hover:bg-secondary-hover", + ghost: "text-secondary-foreground hover:bg-secondary", + destructive: + "bg-destructive text-destructive-foreground shadow-[inset_0_1px_0_0_hsl(0_0%_100%/0.15)] hover:bg-destructive-hover" }, size: { default: "h-10 px-4 py-2", - sm: "h-9 px-3", - lg: "h-11 px-6" + sm: "h-9 px-3 text-[13px]", + lg: "h-11 px-6 text-base" } }, defaultVariants: { diff --git a/src/components/ui/card.tsx b/src/components/ui/card.tsx index 0a588e3..cfb3932 100644 --- a/src/components/ui/card.tsx +++ b/src/components/ui/card.tsx @@ -5,7 +5,7 @@ function Card({ className, ...props }: React.HTMLAttributes): JS return (
; } -function CardTitle({ className, ...props }: React.HTMLAttributes): JSX.Element { - return

; +type CardTitleProps = React.HTMLAttributes & { + as?: "h1" | "h2" | "h3" | "h4"; +}; + +function CardTitle({ className, as: Tag = "h3", ...props }: CardTitleProps): JSX.Element { + return ; } function CardDescription({ className, ...props }: React.HTMLAttributes): JSX.Element { - return

; + return

; } function CardContent({ className, ...props }: React.HTMLAttributes): JSX.Element { diff --git a/src/components/ui/input.tsx b/src/components/ui/input.tsx index 476af94..a0cc470 100644 --- a/src/components/ui/input.tsx +++ b/src/components/ui/input.tsx @@ -9,7 +9,7 @@ const Input = React.forwardRef( ; const Select = React.forwardRef(({ className, ...props }, ref) => { return ( - + +

); }); Select.displayName = "Select"; diff --git a/src/components/ui/separator.tsx b/src/components/ui/separator.tsx index 4b7191f..1659718 100644 --- a/src/components/ui/separator.tsx +++ b/src/components/ui/separator.tsx @@ -3,18 +3,24 @@ import { cn } from "@/lib/utils"; interface SeparatorProps extends React.HTMLAttributes { orientation?: "horizontal" | "vertical"; + decorative?: boolean; } function Separator({ className, orientation = "horizontal", + decorative = true, ...props }: SeparatorProps): JSX.Element { return (
( return (