Agent docs

Install and upgrade the kit

The kit's front door: prerequisites, the registry line and token, install and pin, the theme import, the bundler contract, the upgrade checklist, where the design comes from.
@versable-git/ui

The shared React component kit behind every Versable app
So a second app does not re-decide what a table, a modal, or an empty state should be, and a correction made once stops being a thing anyone remembers.

React TypeScript Tailwind
registry



./docs · ../../docs/design-language · ../../docs/app-patterns · ../../docs/SCRIPTURE.md


What is this?#

The kit
StackReact 19, TypeScript, Tailwind + daisyUI, jotai, sonner, floating-ui
Shipsthe components on the live gallery, the table feature hooks, standalone hooks; src/index.ts is the inventory
Consumed bywalmart-mvp (pins ^0.0.24), speedway (declares ^0.0.14), apps/playground
PublishedGitHub Packages, by the publish-kit workflow, never by hand
Docsdocs/, one contract doc per component
Entry pointOne. Everything imports from the package root

Architecture#

Three layers, and the middle one is the reason the kit stays framework-free.

your app @versable-git/ui the canon
┌──────────────┐ ┌───────────────────────┐ ┌────────────────┐
│ page / route│ │ Layer A primitives │ │ design-language│
│ │ ────────▶│ Table · Card · Modal │◀─ ─ ─│ 13 traits │
│ useDataTable│ ├───────────────────────┤ law │ §A principles │
│ │ │ │ Layer B feature │ └────────────────┘
│ ▼ │ │ hooks: sort, search, │
│ DataTable │ ────────▶│ filter, select, page │ ┌────────────────┐
│ │ ├───────────────────────┤ │ app-patterns │
│ │ │ Layer C useFeature │◀─ ─ ─│ recipes AP-10 │
└──────┬───────┘ │ State + sync seam │ what │ AP-11 │
│ └───────────┬───────────┘ a └────────────────┘
│ router lives here │ request
▼ ▼ brings
┌──────────────┐ adapter ┌───────────────┐
│ react-router │ ─ ─ ─ ─ ─ ─ ▶│ TableSync │ the kit NEVER imports a
│ or next │ (to build) │ Adapter │ router; the seam is why
└──────────────┘ └───────────────┘

The dashed edges are the deliberate ones. The kit never imports a router, so URL sync plugs in through TableSyncAdapter rather than being built in, and the canon governs the kit without the kit depending on it.


Install#

Everything an app, or an agent starting one, needs to get the kit running. Each step is taken from what speedway and walmart actually do, with the file that proves it; if a step here disagrees with an app, the app is the reference and this page is wrong, say so on the board.

Prerequisites#

NeedValueWhy
Node24 or newer (package.json engines at the repo root)the workspace and the playground run on it; an app on 22 works for consuming the kit, the console proves it
Package managerpnpm inside this repo; pnpm or npm in yoursthe kit publishes source, not a build, so any installer that resolves GitHub Packages works
React19 (peerDependencies: react, react-dom ^19)the kit imports nothing below it
Tailwind4, with @tailwindcss/postcss (or Vite's plugin)the theme file is a Tailwind 4 entry; the kit brings its own daisyUI 5
A tokena GitHub personal access token with read:packagesthe registry is GitHub Packages, not npmjs; without it every install of the scope returns 401

The kit brings daisyUI, floating-ui, jotai, react-icons, react-markdown and sonner itself; do not add them to the app.

1. Point the scope at GitHub Packages#

Two lines in the app's .npmrc:

@versable-git:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken=${GITHUB_TOKEN}

GITHUB_TOKEN is the read:packages token, held in the shell environment or a local .env that is not committed. The 401 you get without it is the first thing every fresh adopter has hit (docs/GUIDEBOOK.md section 7, row 24).

2. Install, and pin on purpose#

pnpm add @versable-git/ui@<version> # or npm install @versable-git/ui@<version>

The current version is the version field of package.json here. Pin the exact number: a caret on a 0.x version admits no minor bump, so ^0.2.0 behaves like a pin anyway, and the plain number says what you mean. An app upgrades with its next customer-facing piece of work, never as a ride-along (owner ruling 2026-08-17, docs/GUIDEBOOK.md section 4).

The framework-free helpers live in a second package on the same registry, @versable-git/toolkit (dates, text, numbers, urls, results, sampling; no React). Add it the same way when a real call site needs one of its modules and import by subpath (@versable-git/toolkit/date); packages/toolkit/README.md is the module map. Do not add it speculatively.

3. Import the theme once#

The theme file is the Tailwind entry for the app: it pulls Tailwind, the two daisyUI themes (versable-light, the default, and versable-dark), and it scans the kit's own source so kit-only classes generate in the app's build. One line at the top of the app's root stylesheet, before anything else:

@import "@versable-git/ui/theme/index.css";

Speedway: app/styles/app.css:9. Walmart: frontend/src/index.css:2 (walmart imports tailwindcss first because it keeps its own @theme static block; that block aliases every app token onto the kit's ramp, which is the shape to copy if the app has tokens of its own).

Set the theme on the document element and let a pre-paint script switch it: <html data-theme="versable-light"> (speedway app/root.tsx:466, the constant in app/components/theme.tsx:14; the Next template's src/lib/theme-boot.ts).

4. The bundler contract#

The kit publishes source, not a build: exports maps . to ./src/index.ts and its files carry "use client". Every consumer must transpile it.

Next: one line in next.config.ts (the playground and the Next template):

transpilePackages: ["@versable-git/ui"],

Vite and React Router: two blocks, copied from speedway vite.config.ts:33-47:

optimizeDeps: { exclude: ["@versable-git/ui"] },
ssr: { noExternal: ["@versable-git/ui", "sonner", "jotai", /^@floating-ui\//, "react-icons"] },

Without optimizeDeps.exclude, the pre-bundle throws "React is not defined" at the first kit component. Without ssr.noExternal, the SSR build externalizes the package and breaks outright.

If the app overrides Tailwind's type scale in its own @theme, kit components inherit it: walmart's override inflated PageTitle to 52px until it was scoped (frontend/src/index.css:44-51). Keep app-level type overrides off the bare utility names the kit uses.

5. Check the install#

import { Button, DataTable, useDataTable } from "@versable-git/ui";

If DataTable resolves, the barrel is reachable; every runtime export is named in src/index.ts, and if a name is not there it does not exist. Then open one screen in both themes. The definition of done for any screen on the kit is docs/app-patterns/14-validating-a-ui-change.md.

5b. Installing in CI and Docker (the traps a green laptop hides)#

Three failures the first console deploy hit, each invisible locally (app-forge-v6, 2026-08-20):

  • A file: pin on a vendored tarball cannot work in CI. vendor/ is gitignored, and source uploads (Cloud Build included) honour .gitignore, so the tarball never reaches the builder. A deployed build installs from the registry or not at all; keep file: pins for local iteration only.
  • npm ci --omit=dev in a deps stage starves next build. Tailwind and its postcss plugin are devDependencies and the build needs them; install full deps in the build stage and let standalone output carry the runtime set.
  • --mount=type=secret,id=X,env=Y fails at PARSE time on older builders. Cloud Build's stock docker ships a dockerfile frontend without it; pin a newer frontend (# syntax=docker/dockerfile:1.10) or pass the registry token another sanctioned way.

The GCP-side half (a fresh project's default service account has zero roles, so the first build 403s on its own uploaded source) is docs/app-patterns/03-deploying-on-gcp.md.

6. Then read, in this order (the kit-consumer cut of AGENTS.md's governing order)#

AGENTS.md (the route), docs/design-language/README.md, the contract doc for the first component you touch (docs/<component>.md here), the recipe for the screen shape (docs/app-patterns/10-recipe-browsable-list.md or 11-recipe-record-detail.md), and docs/DO-NOTS.md before you ship. Starting a whole app: docs/app-patterns/05-starting-a-new-app.md picks up from here, and apps/_templates/ holds what you copy.

Known gaps, so nobody rediscovers them#

Releases since 0.2.1 ship docs/ and CHANGELOG.md in the package; 0.2.0 and older carry neither, so on an old pin both live only in this repo and on the live site. No published release ships bin/ yet (every tarball through 0.2.2 omitted it, so npx canon-check resolves to nothing in a consumer); the fix is on main and rides the next bump. Speedway develops against a symlink to this repo's working tree, so its node_modules runs kit HEAD while its manifest says 0.0.14; a clean install of speedway gets 0.0.14. That one is on the board.


The canon check#

The package ships canon-check as a bin (from the next bump; every tarball through 0.2.2 omitted bin/, see Known gaps). Run it yourself, mid-task, when a change touches UI paths; it is the author's instrument for catching a deviation while it is still a keystroke, not a gate someone else holds:

npx canon-check # or: npm run canon-check, once package.json names it

A change touching UI paths must add a dated line to the repo's CANON-NOTES.md (what the kit could not give you and what you did instead) or carry a human canon-note: none because <reason> line in the PR body; the added lines are also read for the deviation classes the canon retrospective names. Repos reviewed by pr-claude get the same check on every PR with no setup; the notes are read by a curator and may land in the kit. Plan: docs/plan/71-canon-feedback-loop-plan.md in versable-builder.

Upgrading an app's pin#

The consumer-side half of a release. The kit publishes on the owner's bump (Releasing, below); this is what an app does to take it. Both apps hold their pin until customer-facing work arrives (owner ruling 2026-08-17, docs/GUIDEBOOK.md section 4), so this runs at that moment, on a branch, and it is a checklist rather than a research project.

Before you touch package.json:

  1. Know how far behind you are, then read CHANGELOG.md from your pin to the target. npm view @versable-git/ui version names the latest; npm outdated @versable-git/ui shows current, wanted and latest in one line. Every release lists what is added, what changes on screens you already have, and what you can delete once you take it (the Retires line). Write the two lists for your app: screens to look at, files to delete.
  2. List what you import and read those contract docs first, their Banned combinations and "Before you adopt this" sections: rg -o "import \{[^}]*\} from \"@versable-git/ui\"" src app | sort -u, then docs/<component>.md for each name (live under /docs/kit/<component>).
  3. Know what your dev tree really runs. Speedway's node_modules/@versable-git/ui is a symlink into the sibling checkout, so its screens already render kit HEAD; a clean install gets the pinned version. Walmart holds a real copy of its pin. Test the bump from the resolved package, not the link.

The bump:

  1. On a branch: set the exact version in package.json ("@versable-git/ui": "0.2.0", no caret), then install. Confirm the lockfile resolves the number, not link:.
  2. Typecheck (the kit ships raw TypeScript, so a removed or renamed prop fails here first).
  3. Run the app's Playwright suite: speedway has 11 specs, walmart 9. They are the kit's real integration tests on your screens; a red spec is a finding, not noise.
  4. Look, in both themes, at every screen the changelog's "Changed on existing screens" line touches, plus the jobs list, the review or import table, and one modal. Sidebar groups collapsing, warning ink, the neutral tint and the entrance animations are the ones that surprised the first readers.
  5. Delete what the Retires lines name, in the same branch, so the upgrade and the retirement land together and nothing hand-rolled outlives the fix that replaced it (docs/SCRIPTURE.md F9).
  6. Record what it cost: two lines in docs/GUIDEBOOK.md section 4 (the version jump, the surprises, the files retired), so the next bump starts from a number instead of a guess.

When something breaks: a kit component that cannot do what your screen needs is a kit item, not a local workaround. File it as walmart did for Select multiple (docs/GUIDEBOOK.md section 7, row 20), keep the local copy only until the release that carries the fix, and put its name on that release's Retires line when it lands.


Use#

import { Button, DataTable, useDataTable, pushAlert } from "@versable-git/ui";
const model = useDataTable<Job>({
rows,
sort: { default: { key: "created", dir: "desc" } },
search: {},
});
<DataTable model={model} columns={columns} showSearch showPager />

Import everything from the package root. There is one entry point plus @versable-git/ui/theme/* for the theme files.


What is in it#

GroupExports
Layout and shellAppShell, Sidebar, Topbar, Card, SidePanel, PageTitle, Tabs
TableTable, DataTable, useDataTable, col, Pager, FilterBar, and six feature hooks for sort, search, filters, selection, pagination and columns
OverlaysModal, ModalTitle, ModalFooter, ConfirmModal, useModal, Dropdown, Tooltip
StatesPageInfo, EmptyState, PageLoading, PageError, Spinner, Skeleton, SkeletonGroup, Progress
Data displayStatusPill, StatusDot, StatTile, Timestamp, CodeBlock, FieldChip, Chip, ListItem, IdentityRow, WorkspaceSwitcher
InputInput, Select, Dropzone, InlineEdit, CopyButton, Button
FeedbackToasts, pushAlert, useAlertToast, Alert
IconsIconFor, RenderIcon, IconKeyList
HooksuseDebounce, useKeyPress, useDisabledReason, useNavHijack, useLocalStorage, useSessionStorage

Some components carry a decision, not just markup#

Worth knowing before you hand-roll something adjacent, because these exist to make a specific mistake unbuildable:

  • useDisabledReason turns a priority-ordered list of [condition, reason] into button props, so a disabled control always explains itself on hover. There is no way to use it and produce a mute disabled button.
  • pushAlert is a module-level export rather than component state, so a toast pushed from a modal outlives the modal by construction.
  • useModal plus useModalMountWarning make modal identity store-driven and warn at runtime when a modal's declared id and mount state disagree.
  • ConfirmModal replaces a raw confirm(, which is drift rather than an alternative.
  • col builds table columns, so column shapes stay consistent across surfaces.

Each component doc carries a "What this saves you" section naming the canon rules that component retires.


Developing#

This repo is a pnpm workspace. Run pnpm, not npm, inside it.

pnpm typecheck # here, or from the root for every package

The playground app is the fastest way to see a change render. It exercises the kit directly and is, for several primitives, the only place they have ever run:

pnpm dev # from the repo root, serves the playground on 5104

Verify UI changes in the running app rather than by building. A green pnpm build says the types line up, not that the component looks right.

Adding or changing a component, and contributing it back, is a fixed network of edits: the contract doc in docs/, the playground page and its registry entry, tests, the do-nots regeneration, the citation check. The runbook is the repo's CONTRIBUTING.md, section 4.

Releasing#

Bump the version in package.json, move the Unreleased entries in CHANGELOG.md under the new number, commit, then run scripts/ship-kit.sh from the repo root: it pushes main, watches the publish-kit workflow and confirms the version on the registry (--check rehearses without pushing). The workflow publishes any version the registry does not already have.

Do not publish from a laptop. A hand publish races the workflow, which then finds the version present and skips it. Put [skip kit] in the commit message to suppress publishing deliberately.

Consumers pin independently, and the pins differ. Walmart is on ^0.0.24, and speedway declares ^0.0.14, which under npm semver is a patch pin resolving to exactly 0.0.14. So publishing a version does not put it in either app; someone has to bump the consumer, with the checklist above.


Documentation#

Per-component reference lives in ./docs, one file per component: what the props are, what the component does with them, and where it is actually used in a shipped app.

The most common way to waste time here is reading the wrong layer.

You are askingRead
what props does DataTable takepackages/ui/docs/, this kit's per-component reference
what should a table look like, and whydocs/design-language/, the cross-app visual canon
I was asked to build a list page, what does that involvedocs/app-patterns/, the request-shaped recipes
which of those rules does a component already handle for medocs/app-patterns/12-primitives-and-rules.md
how do I bring an existing app onto thisdocs/app-patterns/04-migrating-an-app.md
how do I start a new app on thisdocs/app-patterns/05-starting-a-new-app.md

The canon is law, the component docs are contract, and the recipes are what a request brings along. A component doc never restates a canon rule; it points at the section number.


Where the design comes from#

The platform underneath is bought, not built: Tailwind 4 supplies the utility layer and daisyUI 5 the controls and the theme mechanism, and an owner rule says never rebuild a control daisyUI provides, extend it through theme variables and small CSS (docs/SCRIPTURE.md S16, AGENTS.md). What the kit adds is the part nothing off the shelf carries: the Versable vocabulary for tables, status, toolbars, empty and loading states, forms from a schema, and the corrections two shipping apps have already paid for. Those corrections are the design system's actual source: 73 owner corrections mined from speedway and walmart, every one a rule that had never been written down, became the thirteen traits of the canon (docs/design-language/), and the kit implements the canon once. A component doc never restates a canon rule; it cites the section. So the lineage is Tailwind and daisyUI for the material, the two apps for the vocabulary, and the canon as the written law between them.


Repo Structure#

packages/ui/
├── src/
│ ├── table/ → the largest surface: Table, DataTable, feature hooks
│ │ └── hooks/ · sort, search, filters, selection, pagination,
│ │ columns, and sync.ts, the adapter seam (no-op today)
│ ├── modal/ → Modal, ConfirmModal, and the jotai-backed store
│ ├── internal/ → shared machinery, not exported as components
│ ├── hooks/ → standalone hooks, useDisabledReason and friends
│ └── index.ts · the single entry point
├── theme/ → theme files, imported as @versable-git/ui/theme/*
└── docs/ · one contract doc per component
@versable-git/ui · reference, canon, and method, read in place