packages/ui is the shared component library consumed by apps/web (and any future app in this monorepo) via the @workspace/ui/* import alias. This guide covers adding, testing, documenting, and exporting a component here — see the design-system PR checklist for the pull-request requirements specific to token and shared-component changes.
Ask: would a second app in this monorepo (or a second unrelated feature in apps/web) plausibly reuse this exactly as-is?
- Yes →
packages/ui. Generic primitives with no knowledge of SO4's domain: buttons, inputs, dialogs, tooltips, tabs. They take data and callbacks as props; they never import fromapps/web/src/features/*, call the indexer, or know what a "stream," "pool," or "position" is. - No → a feature directory (
apps/web/src/features/<feature>/components/). Anything that renders domain data, calls a hook that fetches it, or is meaningfully specific to one page (e.g.StreamProgressBar,PoolTransactionDialog).
If you're unsure, default to the feature directory — it's a much smaller change to promote a component to packages/ui later (once a second real consumer exists) than to over-generalize one prematurely.
- File naming:
kebab-case.tsxinpackages/ui/src/components/(e.g.date-picker.tsx), matching every existing file in that directory. One component (plus its tightly-coupled sub-parts, e.g.Tabs/TabsList/TabsTrigger/TabsContentin one file) per file. - API:
- Accept
classNameand forward it intocn(yourVariants({ ...variantProps }), className)(seelib/utils.ts'scn) so consumers can extend, never fight, your styles. - Use
class-variance-authority(cva) for variant props, matchingbutton.tsxandbadge.tsx— avariantsobject plusdefaultVariants, exported alongside the component (export { Button, buttonVariants }) so consumers can compose the class list elsewhere if they need to. - Spread
...propsonto the root element last, after your own computed props, so a consumer can still override anything you didn't explicitly design as a variant. - Only reach for design values from the token system in
DESIGN.md(colors, radius, the micro-typography scale) —bun run check:tokens(from the repo root) enforces this in CI; see that doc for the escape hatch (ds-allowcomments) for the rare genuinely-structural exception.
- Accept
- Accessibility: this library is built on Base UI primitives (
@base-ui/react/*), which already handle most ARIA/keyboard-interaction concerns — prefer wrapping a Base UI primitive over building interactive behavior (focus trapping, roving tabindex, etc.) from scratch. If you do add custom interactive behavior, it needs to be operable by keyboard alone and expose the correct role/state via ARIA attributes, not just visually. - Testing: there's no unit-test runner wired up in this package yet — component behavior today is exercised indirectly through
apps/web's test suite (which renders these components) and through the visual regression suite below. If you're adding non-trivial logic (not just markup + variants — e.g. a hook, a reducer, keyboard-navigation logic), add a proper test using the shared config:then add a// packages/ui/vitest.config.ts (create if it doesn't exist yet) import { defineConfig, mergeConfig } from "vitest/config" import { reactConfig } from "@repo/vitest-config/react" export default mergeConfig(reactConfig, defineConfig({}))
"test": "vitest run"script to this package'spackage.jsonand acomponent-name.test.tsxalongside the component, following the render/assert patterns already used inapps/web/src/**/*.test.tsx. - Export: add both the component and its prop-types export to whichever consumer imports it via
@workspace/ui/components/<file>— no central barrel file exists (each component is its own subpath export perpackage.json'sexportsmap), so there's no index to update. - Document in the gallery: add your component to
apps/web/src/features/gallery/components/gallery-page.tsx, rendering every variant/size it supports. This is how reviewers and future contributors discover what exists and see every state at a glance — seeDESIGN.mdfor why it exists. - Log in the changelog: if the change is user-observable (new component, API change, deprecation, accessibility fix, token addition), add an entry to
CHANGELOG.mdunder the correct section — see the file's header for the expected sections. Breaking changes require a### Migration notesubsection.
Run from the repo root (or --filter=@workspace/ui via turbo to scope to just this package):
bun run typecheck --filter=@workspace/ui # tsc --noEmit
bun run lint --filter=@workspace/ui # eslint
bun run check:tokens # design-token usage check (whole repo, not package-scoped)
bun run test:e2e -- design-system-visual # visual regression, once your component is in the galleryAll four should be clean before opening a PR that touches this package.
Before opening or requesting review on a PR that touches packages/ui or the design tokens, run through the design-system PR checklist. It covers theme verification, keyboard accessibility, mobile layout, visual regression snapshots, API compatibility, the token check, and changelog entry requirements.
DESIGN.md— the token system (color, radius, typography scale), the design-token check, and the visual regression suite in full.apps/web/src/features/gallery/components/gallery-page.tsx(/galleryroute) — see every component's current variants rendered together.