You are in versable-builder: a private pnpm monorepo housing the Versable
UI kit (@versable-git/ui), a frontend toolkit, the cross-app design canon,
and a playground app that is both the component gallery and the docs browser
(live at https://versable-builder-playground.vercel.app). Three apps consume
the kit; the list with their pins is the first paragraph of CLAUDE.md and is
kept true there, never restated here. None is upgraded until customer-facing
work arrives (owner ruling 2026-08-17). This file is the map; explore from
here. A person reading this repo starts at README.md instead; an agent
putting the kit into an app starts at packages/ui/USAGE.md; this file is
for an agent working inside this repo.
Ground rules (non-negotiable)#
- Run everything through fnm:
fnm exec --using 24 -- pnpm <cmd>from the repo root. Plainpnpm/noderesolves Homebrew Node 26 and breaks. - Never kill or restart the server on :5104 (pm2 process
playground-dev, hot-reloading, orplayground-prod, a production build for review sessions;pm2 listsays which is up). Never runpnpm buildinapps/playgroundwhileplayground-devruns (build clobbers.nextand kills it). If both are down, the cold-start lines are inCONTRIBUTING.mdsection 3. Switching one for the other is the owner's call. - Semantic tokens only.
pnpm lint:tokens(eslint) FAILS raw palette classes and bracket-arbitrary colors. Colors come from daisy semantic classes, the status vars (--ok-soft/--ok-lineetc.), shadows from--shadow-*, and runtime data colors ONLY viaaccentStyle(color)+accent-*classes (packages/ui/src/internal/accent.ts). - Don't fight daisyUI (owner rule). Extend it via theme vars and small
CSS in
packages/ui/theme/*.css; never rebuild a control it provides. Custom daisy themes REQUIRE the scale vars (--border,--size-selector,--size-field); they are set, do not remove them. - Verify by running, not by reading. A change is done when the CI list
passes locally (the six commands, verbatim in
CONTRIBUTING.mdsection 3, "The bar for done") and the page renders and the interaction works: curl the route, drive one interaction (headless browser or fiber-snatcherfsfromapps/playground/:fs navigate <path>,fs click "<text>",fs shoot). Screenshot claims require having looked at the screenshot. - Commit per logical unit with descriptive messages. While parallel
agents write, use PATH-SCOPED
git add, nevergit add -A. - Numbers in demo data are fictional; anything lifted from speedway reality gets scaled ×π (owner rule).
- Presets are shorthands, never gates (owner ruling 2026-08-14). A
component preset bundles values its plain props can already express, and
explicit props always win over it. A feature reachable ONLY through a
preset is a design defect: build the prop first, then the preset over it.
The pattern lives in
Button(PresetProps+mergePresets) andChip. A bundled field binds only where the component reads it (= preset.x, or a merge path like className's); TypeScript flags a misspelled bundle key but never an unwired one, so wire both ends when adding a preset field.
Repo layout#
| Path | What |
|---|---|
packages/ui/src/ | The kit. One dir per component; index.ts is the barrel = the inventory. internal/ holds shared machinery (ClassNameMap, render-node, markdown-lite, accent, variants). |
packages/ui/theme/ | The styling system: index.css (tokens, two daisy themes, REQUIRED scale vars), components.css (button rules incl. "shade" hovers), fields.css (input surfaces + field dials), accent.css. |
packages/ui/docs/ | The contract doc per component: what it takes, sanctioned and banned combinations, "Before you adopt this", travels with, snippet. Read the one for any component you touch. |
packages/ui/USAGE.md | The consumer's door, human or agent: what you came for in one hop, the six steps to adopt the kit, the scope rule. Ships in the package. |
apps/_templates/ | What you copy to start a new app, one folder per shape (Next, React Router SSR, Vite SPA), each with a TEMPLATE.md. |
contract/ | A tenant, not a kit layer: the foundry contract (charter, system classes, wire contracts, guides, its own do-nots and ADRs) for everything below the screen. Served in the docs browser under Foundry contract; moves to versable-foundry in the org split (row #63). Nothing in CI reads it. |
packages/toolkit/ | @versable-git/toolkit 0.1.0, published, framework-free utilities with tests; no app consumes it yet. PROPOSALS.md is where a cross-app helper is filed. |
packages/qsync/ | @versable-git/qsync 0.1.0, published; URL-state sync the playground consumes. |
.github/workflows/ | ci.yml (typecheck, lint:tokens, test, build, the citation ratchet, the do-nots diff), publish-kit.yml (publishes any of the three packages whose version the registry lacks), docs-publish.yml (mkdocs to tech.versable.ai). |
docs/design-language/ | The visual law: 13 docs, each with principles (A), presets (B) and worked cases cited to file:line (C). Start at its README.md. |
docs/app-patterns/ | How a screen is built and written up: data loading, the surface-doc template, the recipes (10-recipe-browsable-list.md, 11-recipe-record-detail.md), starting a new app, validating a UI change. |
docs/DO-NOTS.md | Every Banned rule in one generated page. Check it before you ship; regenerate with node scripts/build-do-nots.mjs after editing a contract doc. |
contract/DO-NOTS.md | The OTHER one, and a different subject. This repo now holds two generated DO-NOTS pages: docs/ is the kit and the screens, contract/ is what a module must do to be one. Different generators, different sources, neither reads the other. Regenerate the contract one with python3 contract/scripts/build-do-nots.py. |
docs/GUIDEBOOK.md | How the layers relate, the pin and upgrade rules, and the retrospective of lapses in the apps. |
docs/SCRIPTURE.md | Why the kit is shaped the way it is: intent, goals, non-goals, self-graded failure modes, success criteria. |
docs/breakdowns/ | Six real screens taken apart layer by layer. |
apps/playground/ | Next app. /components/* gallery (one page per component), /patterns/*, /docs/* the docs browser, /demo/* demo screens including /demo/admin. src/app/showcase-registry.ts is the one route registry; src/lib/docs-registry.ts is the docs allowlist. |
docs/plan/ | Planning docs and the board (TRACKING.md). Owner rulings live in the numbered docs; older plans are under archive/. |
docs/reference/ | The original design mocks. History, not law: the law is docs/design-language/. |
docs/adr/ | Architecture decision records. |
.claude/output/ | Generated studies and reports, gitignored. Never a source of truth; anything the repo must remember goes into a tracked doc. |
Docs: read in this order for any kit or screen work (the governing copy; other docs carry scoped cuts and point here)#
packages/ui/src/index.ts, the barrel. Every runtime export is named there; if a name is not on it, it does not exist.docs/design-language/README.md, then the doc for the trait you are touching (buttons and actions, status language, overlays, typography, and so on).- The component's contract doc in
packages/ui/docs/<component>.md, its Banned combinations and "Before you adopt this" first. - The app-patterns recipe for the screen shape (
10-recipe-browsable-list.mdfor lists,11-recipe-record-detail.mdfor records,05-starting-a-new-app.mdfor a new app). docs/DO-NOTS.mdbefore you call anything done.
For orientation rather than a task: docs/GUIDEBOOK.md (how the layers relate)
and docs/SCRIPTURE.md (why). Section 4 of the guidebook carries the pin and
upgrade rules; read it against docs/plan/74-guidebook-conformance-audit.md,
which records that its pin numbers are stale and that three apps consume the
kit, not two.
Component conventions (match exactly when adding/emitting kit code)#
- Props:
classNamestyles the component's main element;_classNamethe outer wrapper; extra slots via<slot>ClassName(ClassNameMap). - Callbacks are value-first:
onChange(value, item). - Variants/colors/sizes via
Recordlookups of STATIC class strings. - Icons via the registry:
Icon="Search"(keys inicon/icon-for.ts; add new ones there from react-icons, never inline SVGs). - Every component:
skeletonprop; most:loading,tooltip,noAnimate. - DataTable feature config:
features={{ sort:{}, search:{fields}, filters:{registry}, selection:{}, pagination:{pageSize} }}; row menus{label, icon, onSelect, destructive?}.
When this file and CLAUDE.md disagree#
CLAUDE.md governs: it carries the owner's dated rulings and the bar for done,
and is edited first. This file is the layout and the ground rules, and is
brought into line with it.
When stuck / escalation#
Leave a dated line under "Open questions for the owner" in
docs/plan/TRACKING.md describing what blocked you, commit what is green,
never commit red typecheck or lint. Do not invent exceptions to the ground
rules; stop and flag instead. A defect, a proposal or a question about the kit
that came from an app (yours or a sibling's) is filed in docs/plan/INBOX.md,
one entry, the shape is in the file; the review run reads it, you do not
build from it.