UI Components & Storybook #
BonsAI uses a shared component library @bonsai/ui (35 shadcn/radix components) consumed by the webapp as a JIT source package.
Storybook #
All components in @bonsai/ui have Storybook stories. Use Storybook to browse, test, and develop UI components in isolation.
Dev workspace #
Storybook does not start automatically in the dev workspace. Run it manually:
mise storybook
Then open the Storybook app from your Coder workspace sidebar (port 6006).
Preview environments #
Storybook starts automatically alongside the webapp in all preview environments — no manual step needed.
Story tests (UI v2) #
@bonsai/ui-v2 stories double as component tests: @storybook/addon-vitest runs each one in headless Chromium (Vitest browser mode), including its play function and an accessibility check. @bonsai/ui stories are not under test — libs/typescript/ui/vitest.config.ts excludes their glob.
mise run storybook-test-init # once: installs Playwright's Chromium
mise run storybook-test
They are not part of PR CI: the Chromium download is too costly per push. Like the integration tests, the Story tests job in .github/workflows/dev-mng-pr-build-deploy.yaml runs on each merge to main and gates the dev deploy, so a failing story blocks the deploy rather than the merge. Run them locally before pushing UI v2 changes. The job runs only when the merge changes libs/typescript/ui-v2, the shared .storybook setup, libs/typescript/ui/package.json, the Vite/Vitest config, or what the stories load from outside ui-v2: apps/webapp/public/onboarding (served at /onboarding), ui-v2’s workspace dependencies (extraction-export, tofu-validator, utils) and pnpm-lock.yaml (the storybook filter in .github/workflows/detect-changes.yml). An accessibility violation fails the story (a11y: { test: 'error' } in .storybook/preview.tsx). In Storybook, Run tests at the bottom of the sidebar runs the same tests and marks each UI v2 story with its result.
Run the whole suite from the CLI, not from Storybook. With Accessibility ticked in the test widget, a full run exhausts the Storybook dev server’s heap: @storybook/addon-vitest 10.6.1 keeps every story’s axe report and resends all of them on each update. Running one component or folder from the widget is fine. If the server does crash, its test runner can outlive it and hold the coverage folder; stop that process before the next run.
Story tests also measure coverage (pnpm test:storybook passes --coverage; reports land in libs/typescript/ui/coverage/). Every ui-v2 component directory must reach 80% branches, functions, lines and statements from its stories alone, or the run fails; story fixtures and tests are not counted. The only exceptions are the components listed in UNGATED_COMPONENTS in libs/typescript/ui/vitest.config.ts, which were ungated before the gate existed. The gate fails closed (libs/typescript/ui/coverage-gate.ts): a stale exemption, or a gated component with no measured source, fails the run too. New components are gated by default.
The run is capped at the machine’s cores, and at most 4 workers (maxWorkers in the same config). Every worker is an iframe in one Chromium; past a few at once, animations and software WebGL slow down enough that stories assert mid-transition.
The story project pins process.env.NODE_ENV to test. Vitest only sets it when unset, and an inherited production makes @storybook/react render outside act(), so effects run after a story’s play starts.
A few Biome rules are suppressed per line in ui-v2, each with its reason. These are deliberate exceptions, not gate workarounds. noNoninteractiveTabindex on scroll regions conflicts with axe’s scrollable-region-focusable, which the story tests enforce, so the axe rule wins. noArrayIndexKey is suppressed only for fixed lists that never reorder. useKeyWithClickEvents on the onboarding slide covers a click on a disabled button that keyboard users cannot reach anyway.
UI v2 stories sit in two sidebar folders. Components holds single components (title: 'Components/<Name>'). Blocks holds compositions of several components that together make one piece of a screen, such as Upload or Plans (title: 'Blocks/<Name>'). Both live under libs/typescript/ui-v2/src/components/<dir>/; only the title differs.
Adding new components #
New shadcn components go into @bonsai/ui:
cd libs/typescript/ui
pnpm dlx shadcn@latest add <component>
Every component must have a co-located .stories.tsx file — CI enforces this via mise run ui-stories-check.
Structure #
libs/typescript/ui/
src/components/ ← 35 pure shadcn components (+ stories)
src/lib/utils.ts ← cn() helper
src/styles/theme.css ← BonsAI design tokens + Tailwind v4 theme (canonical)
src/styles/globals.css ← standalone Tailwind entry (used by Storybook)
Theming is CSS-first (Tailwind v4): theme.css holds the :root/.dark token
values, the @theme inline mappings that turn them into utilities, the shared
animations, and the base layer. There is no tailwind.config.js — each app owns
a single CSS entry that does @import 'tailwindcss', imports
@bonsai/ui/styles/theme.css, and declares @source for this library’s source
(webapp: src/shared/styles/globals.css, nigari: src/shared/styles/base.css).