feat(ui): in-tree component primitives + /dev/components showcase (#139) #148
No reviewers
Labels
No labels
area:auth
area:ci
area:db
area:infra
area:native
area:pwa
area:service
epic
feature
foundation
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
james/carol!148
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "139-ui-primitives"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #139. Composes with #138's token foundation (now on
main).What lands
The nine day-one DS primitives under
app/components/ui/:ButtonleadingIcon/trailingIconslots,fullWidthIconButtonaria-label, square heights ride--control-*Card--surface+--border+--radius-lg,elevation="raised"|"hover", optional padding scaleBadgeAvatarsrcrequiresalt), 4 sizes (sm/md/lg/xl),circle/squareshapesFieldid+aria-describedby+aria-invalidonto a single control child viacloneElementInput/Textarea/Select--ringfocus, 3 sizes that ride--control-*so they sit flush with same-size ButtonsEach primitive ships its
.tsxnext to a colocated.module.css. CSS modules are the only path to:hover,:focus-visible,:disabledpseudo-states — inline styles (the existing surface idiom) can't express them. Library alternatives (Radix, headless-ui, shadcn-copy) considered and rejected in ADR-0024; the gist is that at nine focused primitives the library overhead — install-script review per-package, additional dependency surface, the "another way to read" cost for future contributors — outweighs the per-component savings. The decision flips when heavier primitives (Dialog,Combobox,Menu,Tooltip) land and the accessibility complexity grows past what hand-rolled handles cleanly.Adjacent changes worth flagging
lucide-reactadded as a runtime dep. Pure data + JSX, no install scripts (verified —npx allow-scriptsreports no new missing config; the lavamoat allowlist is unchanged per ADR-0010). Ticket said "devDep" but Next.js standalone bundles need runtime React-component deps independenciesor the build artifact fails to resolve them.next/font/google— self-hosted at build time, weights matching the DS (display 600/700, body 400/500, mono 400/500/600).layout.tsxattaches the next/font CSS variables on<html>; theme CSS aliases--font-sans/--font-display/--font-monoto them with the system fallback chain preserved.light.css/dark.cssthat #138's scope didn't cover:--text-*size scale (xs through 3xl),--radius-*(sm/md/lg/xl/full),--control-*heights,--space-*4px grid,--shadow-xs(theme-tuned for dark),--font-display/--font-mono. Theme-invariant tokens (sizes / radii / spacing) live on:rootso they cascade under both themes; color/shadow tokens stay in the per-theme blocks. The primitives literally couldn't ship without these, and they're an obvious oversight in #138's color-only scope./dev/components showcase
Lives at
app/(app)/dev/components/page.tsx— auth-gated by the(app)group,notFound()s itself whenNODE_ENV === "production". Static-rendered (no per-request work). Two side-by-side panels withdata-theme="light"anddata-theme="dark"so a reviewer scans both surfaces in one tab without flipping the app-wide theme switch.Test plan
npm run typecheck— clean.npm run lint— clean (0 warnings, 0 errors).npm run build— clean./dev/componentsappears in the route table as a static (○) prerender.npm test— 421 passed, 91 skipped (no regression — existing screens use inline styles and were not touched)./dev/componentslocally (npm run dev), confirm both themes render correctly, hover/focus/disabled states behave on every interactive primitive.What does NOT change
--color-*) still in place; nothing removed.Dialog,Toast,Tooltip,Progress,Tabs) are explicit follow-ups when the first screen that needs one lands.Adds the nine day-one DS primitives under app/components/ui/: - Button (3 variants x 3 sizes + leadingIcon/trailingIcon/fullWidth) - IconButton (same variants/sizes, required aria-label) - Card (surface + border + radius, elevation raised/hover) - Badge (6 tones x 2 sizes) - Avatar (initials or image, 4 sizes, circle/square) - Field (label + hint + error, auto-wires id/aria-describedby onto a single control child) - Input + Textarea + Select (controlled, --ring focus, 3 sizes that ride --control-*) Each primitive ships its TSX next to a colocated .module.css. CSS modules are the only path to :hover / :focus-visible / :disabled pseudo-states, which inline styles can't express; the rationale + alternatives rejected (Radix, headless-ui, shadcn-style copy) live in ADR-0024. Lucide-react added as a runtime dep — pure data + JSX, no install scripts (verified, no new lavamoat.allowScripts entry needed per ADR-0010). Onest + JetBrains Mono load via next/font/google (self- hosted at build, weights matching the DS: display 600/700, body 400/500, mono 400/500/600). Layout.tsx attaches the next/font CSS variables on <html>; theme CSS aliases --font-sans / --font-display / --font-mono to them with a system fallback chain. Shape/font tokens added to themes that #138 didn't cover: --text-* size scale (xs through 3xl), --radius-* (sm/md/lg/xl/full), --control-* heights (sm 30 / md 36 / lg 44), --space-* 4px grid, --shadow-xs (theme-tuned for dark), --font-display / --font-mono. Theme-invariant tokens (sizes, radii, spacing) live on :root; color/shadow tokens live in the per-theme blocks. /dev/components is a static-rendered showcase route under (app) (auth-gated by virtue of the group) that notFound()'s itself when NODE_ENV === "production". Renders every primitive variant in two side-by-side panels — one data-theme="light", one data-theme="dark" — so a reviewer scans both surfaces without flipping the app-wide theme switch. No existing screen is rewritten; existing inline-styled surfaces keep working unchanged. Per-screen rebuild tickets adopt the primitives. Closes #139. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>📊 Test coverage
Patch coverage: no testable lines changed.
Overall (
app/,lib/,db/, excluding UI per ADR-0019):Soft thresholds per ADR-0019. Coverage is informational and does not block merge.