feat: add component catalog for components/ui/*
From the PR description
Summary
Adds a browsable component catalog for the primitives in
frontend/src/app/components/ui/, using Ladle. 48 stories cover all 17
primitives currently in that directory. The catalog is linked from
docs/design-system.md and smoke-built in CI.
Closes #323 Closes #408
Why Ladle and not Storybook
The issue asked for the choice to be made on actual setup and maintenance cost for this repo, not on which tool is more popular. Four things decided it:
- Nothing in
components/ui/imports fromnext/*. I grepped bothcomponents/ui/andshared/ui/and there are zero Next.js imports - the primitives are plain React pluslucide-react,clsxandtailwind-merge. That removes the single biggest reason to reach for Storybook here, since@storybook/nextjsexists mainly to shimnext/image,next/fontand the router. We would be paying for a compatibility layer with nothing to compatibility-layer. - One devDependency instead of a package set. Ladle is a single
@ladle/reactentry. A Storybook install for this stack is thestorybookcore plus a framework package plus the addons you actually want, each on its own release cadence, plus periodicstorybook upgrademigrations. For a small OSS frontend that is recurring maintenance nobody is assigned to. - The repo already runs Vite. Vitest and
@vitejs/plugin-reactare installed, so Ladle reuses the transform pipeline and@/alias resolution the tests already use. One build system, one mental model. - The exit is cheap. Stories are CSF-compatible -
export default { title }plus named exports - so if the project outgrows Ladle the stories port to Storybook without a rewrite. Choosing the light tool first is reversible; choosing the heavy one is not.
Trade-off accepted: Ladle has a smaller addon ecosystem and no Chromatic-style visual-regression story. Neither is in scope for #323, and #322 established the design system as documentation-first.
What changed
frontend/.ladle/config.mjs- story glob scoped tosrc/app/components/ui/*.stories.tsx, per the issue's "don't catalog all 144 components" constraint. a11y addon on, rtl and msw off.frontend/.ladle/components.tsx- global Provider. Mike's dark mode is class-based (@custom-variant dark (&:is(.dark *))), so the Provider drives the same.darkclass on the document root that Settings > Appearance sets. Without this the theme toggle would flip Ladle's own chrome and leave every app token on its light value.frontend/.ladle/ladle.css- imports the app's realglobals.cssso the catalog renders on the actual tokens and materials rather than a copy, and supplies the two font variables thatnext/fontnormally injects on<body>.- 17
*.stories.tsxfiles colocated with their primitives, matching the existing*.test.tsxconvention. docs/design-system.md- new "Component catalog" section; removed the "tracked in issue #323, not set up yet" line..github/workflows/ci.yml-npm run catalog:build --if-presentafter the frontend build.frontend/package.json-@ladle/reactdevDependency pluscatalogandcatalog:buildscripts.
Coverage
All 17 primitives in components/ui/: button, check-square, cite-button,
dropdown-menu, empty-state, form-field, glass-card,
glass-icon-button, input, liquid-dropdown, liquid-surface,
option-pill, pill-button, popover, search-bar, tab-pill-button,
toggle-switch.
The issue listed the primitives as of filing; the directory has grown since, so
I enumerated it rather than working from that list. liquid-surface.ts is
class-name constants rather than a component, but it gets stories anyway - the
elevation ladder is the part of the design system hardest to hold in your head
from prose, and swatches of each material and interactive state are the clearest
way to show it.
Stories lean on documenting the sharp edges the design system already calls
out: Button has no type default so it submits inside a <form>;
CiteButton only sets aria-label when its text is hidden (WCAG 2.5.3);
CheckSquare is decorative and the row owns the ARIA state.
One non-obvious thing worth flagging in review
Tailwind v4's automatic source detection skips dot-directories, so nothing
in frontend/.ladle/ is scanned for class names, and an @source override
does not lift that exclusion. I found this by probing with a class used nowhere
else and confirming it was absent from the built CSS.
The first version of the Provider happened to work only because every utility
it used also appears somewhere in src/. That is a trap: the next person to
add a novel Tailwind class to the Provider would get silently unstyled output.
Catalog chrome is therefore styled with plain CSS in .ladle/ladle.css, and
both that file and docs/design-system.md say why. Story files live under
src/ and are scanned normally, so Tailwind works as expected in a
*.stories.tsx.
Testing performed
Config/tooling change, so the regression check is a build gate rather than
component unit tests - the primitives already have their own *.test.tsx
suites.
npm run catalog:build --prefix frontend- passes, 48 stories across 17 primitives inmeta.json.Verified the built CSS actually resolves the design system rather than just compiling:
liquid-glass-flat,liquid-glass-modal-row-selected,theme-dropdown-item, and both the light (#f9fafb) and dark (#0b0f14)--app-backgroundvalues are present.Ran the built catalog and loaded it in a browser: stories render, dark mode toggles the real dark tokens, the Radix dropdown opens and portals onto the float glass surface, no console errors.
npm run build --prefix frontend- passes. Confirmed stories are inside the TS program by injecting a deliberate bad prop (tone="not-a-real-tone") and checkingtsccaught it, then reverting; this is what backs the claim that CI catches story/prop drift.npm run lint --prefix frontendwith the ESLint cache cleared - 0 errors, 34 warnings, which is exactly the pre-existing baseline. My first pass added 17no-anonymous-default-exportwarnings, so the default exports are now named consts.npm run test:coverage --prefix frontend- 637 passed, 4 failed, all 4 insrc/app/lib/mikeApi.test.ts(blob.text is not a function).Those 4 failures are pre-existing and not caused by this PR. I verified rather than assumed: they reproduce identically after checking out
origin/main'spackage.jsonandpackage-lock.jsonand reinstalling, with none of this branch's dependencies present. A lockfile diff also shows zero version changes to any pre-existing package - the 166 lockfile additions are new Ladle dependencies and the 27 removals are all deduped nested esbuild platform binaries. The failures are a local Node 24 vs CI Node 22 jsdom/Blob difference; CI onmainat6c77ddais green, so per the repo's guidance on toolchain mismatches I have not filed an issue for them.
Lockfile fix for #408
CI's npm ci was failing in both the frontend and playwright jobs with
Missing: @swc/helpers@0.5.23 and Missing: esbuild@0.28.2 (plus platform
binaries). Root cause: the lockfile above was generated locally with npm
11.16.0 (Node 24), and that npm silently accepted a shape that CI's npm
10.9.8 (bundled with the pinned Node 22) rejects as out of sync with
package.json.
Fixed by installing an actual Node 22 (v22.23.2 / npm 10.9.8, matching what
CI's actions/setup-node@v4 with node-version: 22 provisions) and running
npm install against the existing lockfile in place, rather than deleting
and regenerating it from scratch - a full wipe re-resolves every
caret-ranged dependency to today's registry latest and would have dragged in
hundreds of unrelated version bumps. The in-place repair only touches what
was actually inconsistent:
- 167 additions, all
@ladle/reacttransitive deps (including@vitejs/plugin-react-swc's nested@swc/helpers@0.5.23, one of the two packages CI's error named). - 0 removals.
- 27 version changes, all of them the
esbuildinstance nested undervitestmoving from0.28.1to0.28.2- the second package CI's error named, and the version the rest of the tree already needed.
Verified: npm ci (fresh node_modules wipe) exits 0 with no
missing/invalid/extraneous warnings, npm run catalog:build still produces
48 stories, npx vitest run / npm run test:coverage pass all 641 tests
(also confirming the 4 mikeApi.test.ts failures noted above are Node
24-only - they pass under Node 22), npx tsc --noEmit is clean, npm run lint is unchanged at 0 errors / 34 warnings, and npm run build (next build) succeeds. All run under the same Node 22 toolchain CI uses.
Our analysis
Add Ladle component catalog — read the full analysis →
Think the analysis missed something the PR description covers?
Capture this PR into my fork
Download a Markdown prompt that tells Claude how to port every
commit in this PR into your working tree. Run it via
claude -p < capture-pull-407.md from
inside the repo you want the changes in.