Agent docs

Kit changelog

What each release adds, what changes on screens an app already has, and what an app can delete once it takes it.

What each release adds, what changes on the screens an app already has, and what an app can delete once it takes the release (the Retires line). Written for the person or agent moving a pin; the upgrade procedure is the "Upgrading an app's pin" section of README.md beside this file. Older releases are reconstructed from the kit's commit log; a release with no Retires line retires nothing an app is known to carry. A change that came out of a consumer's CANON-NOTES.md entry cites it as From the notes: <repo> <date>, which is the loop's closure (docs/canon/NOTEBOOK.md).

Unreleased (next: 0.6.1)#

No code changes needed in an app. This undoes five 0.6.0 regressions and adds one development warning.

ComponentWhat changed
Circle icon buttons in darkBare again. The 0.6.0 hairline landed on every quiet circle, such as topbar and menu buttons.
PageTitleThe icon is centred against the title and subtitle again, and the block no longer sits lower on the page.
ModalTitleThe space under the title block is back to its 0.5 size.
Disabled quiet buttonsA disabled text, link or ghost button uses a muted ink, so an empty form's button no longer looks clickable.
StatTileOn a narrow tile the icon sits above the label, so the label wraps between words.
Table in a card or dialogWarns once in development when a scroller around the table clips its edge padding.

kit-standard gains two notes, row-controls-misaligned and quiet-primary, and a --shots <dir> option that saves a screenshot per route, theme and width. The new bin/shot-diff.mjs compares a before and an after folder and keeps only the regions that changed.

0.6.0 (2026-10-24)#

Three changes need code edits, listed under Breaking changes. Everything else changes how screens look, and the table below lists each default that moved.

Breaking changes#

  • Every Tabs item needs an icon. Without one it is a type error, and a development build names the tabs that lack it.
  • RowMenuAction.onSelect can be undefined, because a menu item can be a link (href) instead. Call it as item.onSelect?.().
  • A table cell width no longer takes grid syntax such as minmax(). px, rem and percentages still work; shape and size replace the rest.

Tests that match rendered text will need updating: empty cells read N/A, placeholders lose their ellipsis, Timestamp is terser, and the delete confirm names its action.

What changes on screens you already have#

ComponentWhat changedKeep the old behaviour with
Button default variantA bare Button is a quiet text button. Write variant="solid" on the one primary action of a screen.variant="solid"
Button shadeNow off by default. When on, the button rests on a wash of its colour instead of only tinting on hover.add shade
Button presetsnew is a toned text button with a plus, refresh an icon-only circle, back no longer an outline.content for a labelled refresh, variant="outline" for back
Button onClickA handler that returns a promise shows pending (spinner, disabled, aria-busy), and a rejection is rethrown.promisify to swallow the rejection
Button variant="link"Renders as text and warns once in development.variant="text"
Button with no iconWarns once in development, naming the label.Icon={null}
CursorsEvery enabled control shows the pointer; a button with disabledReason shows not-allowed.a cursor-* utility
Button inks in darkQuiet circle buttons get a hairline border; neutral outline, success and quiet secondary buttons take new inks so their labels pass AA.none
IdentityRow initials discBase ink on the primary tint in both themes.avatarClassName
CodeBlock, ListItem, IdentityRow, Dropzone, Table textShows the full value on hover when cut.none
ListItem with onClickThe row is a keyboard control: focusable, Enter and Space activate it.none
Dropzone drop areaNo pointer on the drop area; the Browse button is the control.none
AppShellSidebar group headers are sentence case, not uppercase. In the collapsed rail, an item with no icon and no color shows two initials. A string topbar title is a div.topbar.titleAs="h1" for the title
StepperThe active number sits on a darker primary. The md strip adds "Stage N of M" and scrolls sideways when stages do not fit.progress={false}
InlineEdit triggerShows the pointer, not a text cursor.none
Markdown inline codePlain code on a faint wash that wraps with its line.none
DataTable toolbarOne row: filters on the left, search and a circle Refresh on the right, the count in the footer.your own toolbar
DataTable pagerDrawn only when there is more than one page.pager="show"
DataTable empty after a filterShows FilteredEmpty, quoting the query, with a Clear button.empty
Table columnsA plain string cell truncates at 48 characters. A table wider than its box scrolls sideways with an edge shade.shape: "none", or defaultShape="none"
Table in a ModalMeets the dialog edges with a hairline above it, as in a Card.bleed={false}
Empty cellReads N/A, lighter, where it was an em dash or a blank.a custom render
Table copy buttonsStatus and action cells no longer get one under copyCells.none
Table scroll floorThe scroll box keeps three rows in view however a parent squeezes it.minRows={0}
TabsIcons and counts share the label's line.none
Modal titleModalTitle draws a divider under itself.divider={false}
ConfirmModal delete presetA solid error button named for the action ("Delete job"), plus "This action cannot be undone."irreversible={false}, confirmLabel
PageTitleThe icon disc sits on the title line. A string subtitle clamps to two lines. Actions that do not fit wrap onto their own row.none
Input and SelectPlaceholders lose their ellipsis (Search, Select). fullWidth fills the control inside the frame.placeholder
StatusPilldot and Icon together draw the icon only. A long label truncates with a tooltip. A zero count fades.none
Timestamp"just now", "5m ago", "3h ago", then a date such as "Oct 22" from 12 hours on.none
BreadcrumbsA segment with no label rule shows as written, where it was capitalised. A long id loses its middle.a labels rule
Tooltip in a modalPaints above the dialog.none
StatTileOn a narrow tile the figure steps down and the label wraps to two lines.none
SchemaFormA rows table in a section card bleeds to the card edge. A stray x-labels or x-option-meta key warns instead of refusing the schema.none

New#

  • MenuButton: a button with a caret that opens a menu of links or actions.
  • Button disabledReason: disables the button and shows the reason as its tooltip. New add and rerun presets.
  • RowActions: an action marked primary shows its label as a text button.
  • useAction(fn): runs an action with a pending toast, then success or error, with an optional Retry.
  • ThemeToggle and themeBootScript: light and dark with no flash on load.
  • FormModal: fields with a Cancel and submit footer that explains a refused submit. OptionCards: a radio group drawn as cards.
  • Input endAction; Select top, required and filterOption.
  • Table columns take shape and size for their width, and priority to drop out when the table is narrow. Tables also take reorderable, bulkActions for selected rows, and col.entity for an owner, team or file cell. Card takes footer.
  • defineStatusMap: one declaration of a state for its pill, filter options and cell. Tag: a borderless label with an icon.
  • RefreshControl, FilteredEmpty and TruncatedText. PageTitle takes meta.
  • Icons: Server, Laptop, Flask, Rocket.

kit-standard#

After upgrading, run npx kit-standard against your app. It grades 18 new checks, so a screen you did not touch can gain a finding. The checks are in docs/KIT-STANDARD.md.

Known gaps#

  • On phones the sidebar is an icon rail with no menu button, and the topbar does not fold its actions.
  • reorderable does not drag on touch screens; Alt plus the arrow keys works everywhere.

0.5.0 (2026-09-13)#

Breaking, despite the minor version number. This release removes API. The five props 0.4.0 deprecated are gone, one data-record field is renamed, and Button is now a discriminated union that rejects at compile time the prop combinations it used to ignore. A consumer on the primary API of 0.4.0 compiles and renders unchanged; a consumer still calling a deprecated alias, or setting an uppercase Icon field on a nav item or row action, gets a type error until it migrates. The three apps are pinned at 0.2.2, so none takes this unprepared, and that pin is the deprecation window the icon rename skipped.

4 commits, 56 files, +202/-203 since v0.4.0. Areas: StatTile, Stepper, InlineEdit, Dropdown, the Button type, the icon registry, and the nav and row-action data records. Grounded in the diff at 71cdce3...HEAD, not commit subjects.

What changes on screens you already have#

No runtime behaviour changes, and nothing to keep with a flag. Every item here is a compile-time break for a consumer that used a removed name; a screen built on the primary API renders identically. The table is the migration: the left column is the name that is gone, the right is what replaces it.

what breaksthe error a consumer seesmigrate to
StatTile tonetone does not exist on StatTilePropstoneClassName for a raw text-colour class, or color for a palette tone
Stepper step warnwarn does not exist on StepperStepwarning, the same count-that-needs-eyes line
InlineEdit ariaLabelariaLabel does not exist on InlineEditPropsthe native aria-label
Dropdown setOpensetOpen does not exist on DropdownPropsonOpenChange(next), which receives the resolved next boolean rather than an updater function
the LucideIcon typeLucideIcon is not exportedRegistryIcon, the icon-key union, unchanged in meaning
an uppercase Icon field on a NavItem, NavGroup, or RowActionIcon does not exist on the record; icon is expectedrename the record field to lowercase icon. Uppercase Icon now always means a direct component prop (StatTile, Button, EmptyState); lowercase icon always means an icon inside a data record. activeIcon on a RowAction is unchanged

What you can now do#

  • Give a link Button a button-only prop and it is now a compile error rather than a silently-ignored prop. ButtonProps is ButtonAsButton | ButtonAsLink, discriminated on href: a Button with href types dropdown, badge, badgeCorner, promisify, and IconOnClick as never, so the split-button, badge, and async-pending machinery, which never applied to a link, can no longer be set on one. button.tsx still branches on href at runtime as before; the change is entirely in the type.

Known gaps#

  • No consuming app has taken this release yet; all three are pinned at 0.2.2. The migrations above are verified by the kit's own typecheck and its 461 tests, and by rendering the changed surfaces in the playground in both themes, not by a real app upgrade.
  • The icon rename is mechanical for a consuming app: rename the field on nav items, nav groups, and row actions. It was done as a rename, not a bare removal, so a migrating app has a one-to-one replacement rather than a gap.

0.4.0 (2026-09-13)#

26 commits, 123 files, +4269/-434 since v0.3.0. Areas: tables, modals, forms, the app shell, theme tokens, a schema CLI, and the release machinery itself. Grounded in the diff at bb139f2...9fd473b, not commit subjects.

What changes on screens you already have#

Read this section before the rest. Three defaults flipped, and each one reaches a caller who did not ask for it.

what flippedwhat a reader seeskeep the old behaviour with
Table and DataTable bleed to the card's edgerows meet the card's border; the toolbar above and pager below keep the card's paddingbleed={false}
ModalTitle and ModalFooter draw no dividerthe rule above a footer and below a title is gonedivider on that element
a captioned field reserves one message linea field with a top caption gains a small minimum height under it while quiet, so a warning landing never shifts the layoutreserveMessage={false}
a bricked control refuses its clicka bricked Button no longer runs its onClick, and a bricked link no longer navigates; it stays hoverable so its tooltip can still explain the refusalnothing, and that is deliberate: bricked documents itself as off

One appearance change with no opt-out: comments and punctuation inside a code block are darker in the light theme. --code-quiet was the single code token set to a 500 step while every sibling used a 700, and it measured 4.37:1 against a 4.5 floor on every snippet in the product. It takes --p-slate-700 now, so quiet reads through having almost no colour rather than through being pale. The dark theme is unchanged; its 400 step already cleared.

hideBorder still works as the inverse of divider for one release. The two console call sites that pass it (invite-modal.tsx, auth-overview.tsx) already ask for no border, so both render unchanged.

Rules that changed#

The standard grades two things it did not before, so a screen you have not touched can gain a finding on the first run after this upgrade.

  • shade-count-in-group fires when two or more buttons in one control group carry shade: a note at two, a fix at three. One button carries the shade, and a second needs a stated reason. Cites canon 02 §49.
  • toolbar-height-mismatch fires when a control in a flex row differs from that row's most common height by more than 4px. An applied Chip is exempt where the row centres its items, because canon sizes it as the lg badge on purpose. Cites canon 07 §16.

The token lint's arbitrary-value and palette rules had been silently disabled for every file, because an ESLint flat-config block replaced rather than merged the no-restricted-syntax rule. They are live again, one rule per file, and off-ladder-opacity is now an error rather than a warning. No off-ladder usage exists today, so nothing in the apps breaks, but a new arbitrary colour or a white/black utility in kit source is now caught.

Seven canon docs moved, and every one of them ships to you inside method/:

docwhat it now says
01-foundations.md/12, /40 and /60 are named tint rungs by role: the link fill, the unchecked edge, and that edge on hover. /80 is recorded as ruled but unused, and deliberately absent from the lint list
02-buttons-and-actions.mdprinciple 11, the one-shade rule the new check enforces
05-cards-and-surfaces.mdthe kit bleeds by default; bleed={false} keeps the inset
06-tables.mdthe same rule, stated in the browsable-list row
07-toolbars-and-filters.mdevery control in a toolbar row is 32px tall, with the applied Chip excepted
12-typography.md13px is the density rung, the size the kit itself types; a page reaches it through those components rather than as a class
13-forms-and-fields.mda sign-in form reports a wrong credential as a warning, never an error

What you can delete once you take this#

appwhat goesreplaced by
versable-forge-v6src/components/row-actions.tsxRowActions
versable-forge-v6fifteen hand-written aria-label call sitesa ReactNode top naming its own control
speedwayapp/root.tsx NavProgressBarTopLoadingBar
speedwaythe edge-cancelling half of its hand-rolled table bleedthe new default
walmart-mvpeight explicit bleed passes across five pages (ActiveParts.tsx, Jobs.tsx, UnlistedParts.tsx, ErrorManagement.tsx, SpecViewer.tsx)the new default; they are redundant, not broken

What you can now do#

Tables#

  • RowActions is a new component: a row's common actions as visible icon buttons, the rest folded into the kit's own RowMenu or into a caller's card. Five action kinds, exactly one per action: onSelect, href, modal (the shared openModal, mounting no dialog of its own), panel (a cached popover, one open across the page at a time) and render. DataTable rowActions and col.rowActions build the trailing column. SchemaForm's own rows table adopts it and drops from five open buttons to two plus a kebab.
  • FacetOption takes swatch, a colour dot for a module or stage, and trailing, a short right-edge slot. Both reach the Select listbox row through toSelectOptions, and the applied chip shows the swatch as its leading dot when the option carries no icon.

Forms#

  • A ReactNode top on Input now names its own control, pointing at the rendered caption through useId so the browser computes the accessible name from the caption's text. A string top already worked, and a caller's own aria-label or aria-labelledby still wins. This closes the caveat 0.3.0 left open.
  • Input and InputContainer take warning, carrying the same edge treatment error already draws, with error winning when both are set. SchemaForm's Field does the same in edit and cell mode.
  • SchemaForm takes noPortal and reaches every Select it renders, plus any Dropdown a widget opens, so a choice field's listbox opens inside a Modal's top layer instead of behind it. Promotes IN-20260905-03.

Modals#

  • ModalBody and ModalSection give a body its padding, its gap and its labelled sections without hand-rolling a <div>.
  • ModalFooter's back and next render through the kit Button rather than a raw btn class, so each carries color, variant and shade. next defaults to color="primary".

App shell#

  • Sidebar and AppShell gain resizable, width, minWidth, maxWidth and storageKey, spelled sidebarResizable and so on when passed to AppShell. Drag the rail's right edge between 200 and 400px at lg and up; dragging under 120px collapses to the rail rather than clamping, and the width persists under one key. SidePanel now runs on the same engine (internal/use-drag-resize.ts) and gained the keyboard control it lacked. Answers the console's #304, where users had asked to widen a fixed rail.
  • TopLoadingBar is a page-transition progress bar: a controlled 2px bar with a grace window, a trickle toward 88% and a landing jump to 100%, the formula speedway's hand-rolled bar already used.

Working outside the browser#

  • A lint-schema bin runs the same lintSchema walk from a shell, so a subprocess can refuse a bad schema before it ever reaches SchemaForm. It loads the real TypeScript source through a resolve hook rather than duplicating the lint, so the two cannot drift apart. Exit 0 is clean, 1 prints one line per refusal, 2 means a file could not be read or parsed. See docs/schema-form.md, "Run the lint from a shell". This answers the foundry conformance suite's H7 row, advisory until now for want of a CLI to shell out to.

Accessibility and consistency (the GPT-5.6 audit, stage 1)#

A 31-finding consistency audit landed as additive-only changes: every new prop is a superset of the old surface, and the previous API still compiles unchanged. Nothing is removed here; the removals wait for a later release.

  • New props, each with the old name kept as a deprecated alias: StatTile takes a color (a SemanticColor, winning over the renamed toneClassName); Stepper reads warning for warn; InlineEdit reads the native aria-label for ariaLabel; ChipColor is now an alias of SemanticColor. Dropdown gains defaultOpen and onOpenChange (the old setOpen still fires). Button takes a ref across its anchor and button forms, and Select a controlRef for the focusable control.
  • Every icon extender now forwards the whole IconImportProps group by name, so iconProps reaches the rendered icon on Button, Alert, Chip, StatTile, Toast and the rest, where before it was silently dropped.
  • A link-mode Button runs the caller's onClick instead of discarding it.
  • Runtime colours route through the accent seam (accentStyle/accentClass) rather than inline styles, so Select's swatch and active row, and SchemaForm's colour buttons, read from one place. Toast uses currentColor and markdown inline code uses a token wash.
  • Assistive technology gains a run of announcements a sighted user does not see, all derived automatically with no new consumer prop. Input, Select and Dropzone set aria-invalid from a truthy error and point aria-describedby at their message, which carries role="alert".
  • Select's closed control reports aria-expanded, and its clear affordance moved out of the control's role="button" subtree so the trigger has no interactive descendant. Modal accepts aria-label and aria-labelledby, and a string ModalTitle now labels its dialog automatically. A Tabs panel names itself from the active tab, Dropzone is a drag region whose only button is the browse control, and a closed SidePanel is inert and labelled from its title.
  • Alert and Toast pick their live-region role by severity: an error announces as alert, everything else as status.

Known gaps#

  • Three items landed narrower than they were specified. Modal footer buttons carry color, variant and shade but not icon or tooltip. The reserved message line covers captioned fields rather than every field. RowActions leaves per-row caching to the caller rather than building it in. All three are tracked and none is shipped in this release.
  • Canon 12 §A1 says a page reaches 13px through the kit's components and never as a class, while kit-standard's ramp now accepts 13 everywhere. Nothing currently detects a page typing that size directly.
  • The kit standard reports 40 findings at fix severity across the pattern pages and galleries this release touched. None is a regression from it, and the largest single cause, a code token failing contrast, is fixed here.

0.3.0 (2026-09-08)#

The first release to ship bin/ (canon-check, kit-standard) and method/ in the tarball; every tarball through 0.2.2 omitted both.

  • CodeBlock and every colorizer read their token colours through six --code-* theme tokens (theme/colors.css) instead of the 700-step palette names. Light keeps the 700s; dark now gets the 300s, because the kit standard measured the light steps at 2.2 to 3.0:1 on the dark surface across every code, JSON, markdown and schema snapshot view. An app that themes the kit can retune code colour in one place.

  • Select's control shows keyboard focus as the field ring (the same 2px outline the Input frame draws on :focus-within, now .field-ring in theme/fields.css) instead of a border colour change alone. Every facet trigger in a FacetBar and every Select in a form gains the ring; a warned or errored Select rings in its tone. The console's keyboard walk found the facet triggers were the one kit control a probe could not see focus on (gcp row #659, this repo's #113). The bare controls inside a DataTable filter chip had outline-none with nothing in its place and take the same ring. The kit standard gains focus-by-border-only, which found the second. Pinned in test/select-focus-ring.test.tsx. From the notes: the console's keyboard walk, 2026-09-08.

  • A bricked Button carries aria-disabled="true" on both its button and link forms. It stays interactive so its tooltip can explain the refusal (canon 02 A4), and the DOM used to say enabled: the console's New job walk read the Create button as clickable one line before the click did nothing. Pinned in test/button-bricked-aria.test.tsx. From the notes: the console's New job walk, 2026-09-08.

  • RowMenu is a menu to assistive tech: its trigger declares aria-haspopup="menu", its panel carries role="menu" with the row's label, and each action is a menuitem. The console drove the file-table row menu and found zero menuitems behind a trigger announcing a popup (gcp row #657). Dropdown itself is unchanged: it floats caller JSX and carries no menu semantics of its own. Pinned in test/row-menu-roles.test.tsx. From the notes: the console's overlay walk, 2026-09-08.

  • Four small-text inks the kit standard measured under AA in the light theme move to the /65 floor canon 12 §A9 names: the FacetBar count line, the Select's "nothing to choose" row, and the SchemaForm review's "Not set", "None" and "Not chosen" placeholders. Changed on existing screens: those runs read one rung darker. From the notes: the console's round 1 against the standard, 2026-09-08.

  • Three defects the console confirmed in the 0.2.2 source (gcp row #492), each pinned by a test in test/kit-defects-492.test.tsx. formatRelativeTime gains a future branch ("in 5m", "in 2h", "in 3d", then the short date), so an expiry or due date never reads "just now". ConfirmModal takes an Icon prop for its title, defaulting to the trash on the delete preset and to none otherwise, so an icon-led non-delete confirm has a compliant route. A single facet (multi: false) now names its all-sentinel in allValue (required; allLabel labels the option FacetBar prepends when absent), so a facet can no longer display its first real option as an applied filter. Changed on existing screens: any multi: false facet fails typecheck until it names allValue, a one-line addition; a facet that already listed an "all" option first names that value and renders exactly as before. Retires: the console's absolute workaround on its Invitations time columns, and its explicit { value: "all", label: "Any time" } first option on the History facet (keep the option, add allValue: "all"). From the notes: gcp .claude/output/20260901-0730-kit-defects/kit-defects.md, 2026-09-01.

  • The kit standard: docs/KIT-STANDARD.md states the checks a consumer's screens are graded on for reskin, layout, visual mess and the hierarchy of information and controls, and a third bin, kit-standard, runs the measured half against a live app in both themes at two widths (--self-test plants a defect of every class and proves each check fires). Every check cites the canon line it enforces and the kit source it holds to; the standard's own table names the kit defects under which a check is excused rather than required. Nothing changes on an app's screens; this is an instrument. From the notes: the console's reskin row, 2026-09-08.

  • Chip: a selectable chip at rest (onChange or selected set, not selected, not removable) now defaults to variant="soft" instead of solid, so an unselected option is quiet in both themes; solid neutral was near-black on the light theme (owner ruling 2026-09-03, from the audit's theme comparison on /components/chip). Changed on existing screens: any selectable Chip that relied on the solid default at rest gets the soft look; pass variant="solid" to keep it. Selected chips, plain chips and removable chips are unchanged.

  • Consumer enablement: the package now carries its method and answers its own questions. USAGE.md is the six-step front door (agent path first, fetchable raw from the repo); method/ holds verbatim copies of the design canon, the app-patterns recipes and the do-nots aggregate, synced by scripts/sync-method-docs.mjs; docs/KIT-INVENTORY.md and docs/llms.txt are the derived inventory of every component and pattern page, with the admission rules that place a new one; a second bin, kit-inventory, answers --find <term> from inside a consumer against exactly what that consumer's tree resolves, stamping the installed version and saying pin drift and symlink trees out loud; and prompts/claims.md is a brief a consumer's own agent runs over their app for claims that stopped being true. Nothing here runs in anyone's CI or blocks any build outside this repo. docs/patterns.md stops hand-keeping its catalog and points at the inventory.

  • PageSkeleton: the Suspense fallback promoted under D7 (doc 44), which ruled that "Skeleton + panel can be a wrapper around the unitary components". Two page shapes, list (toolbar over table rows) and cards (stacked cards at measured px heights), composing Card and Skeleton. Takes the page's OWN container class rather than shipping a container map, so one map decides the box on both sides; layout is required with no default because a default is a guess about a page nobody looked at. header accepts the page's real header element so the top does not redraw when data lands, headroom holds open chrome the placeholder does not draw. Retires, once an app takes this release: nothing outright. Speedway's WorkspaceSkeleton body is replaceable by this, but its PANEL map is speedway's own layout classes and stays local (per the Retires-line rule: name only what fully dies).

  • Input: a string top now becomes the field's accessible name. top renders outside the <label> that wraps the control, so until now every field built with top="Email" reached a screen reader or a password manager unnamed; the contract doc carried this as a known defect. Reported from the forge console, which measured it in the DOM. Not an htmlFor/id pairing, because the file stays hook-free and server-renderable. A caller's own aria-label or aria-labelledby still wins. Screens change only for assistive tech; nothing moves visually. Still yours to name: a placeholder-only field, and a ReactNode top.

  • Alert: the neutral tone gets its own --neutral-soft surface token instead of bg-base-200. On any page or AppShell frame, both of which are base-200, a neutral alert was the same colour as what it sat on and only its border separated it. The new token sits at the tinted softs' elevation in each theme, above both the page and the card in dark. This visibly changes every neutral Alert in every app that takes the release, which is the point, but look at yours.

  • SidePanel: closing returns focus to whatever opened the panel, but only when focus is still inside the panel at close time. A keyboard user closing via the X or ESC lands back on their trigger instead of losing focus to a hidden element; a pointer user whose focus already moved on is never yanked back, because the panel is a non-modal inspector rather than an overlay. Modal needs none of this: a native <dialog> restores focus itself. Screens change only for keyboard and assistive-tech users; nothing moves visually.

  • Breadcrumbs: deriveCrumbs, the path-to-crumbs seam the items prop always assumed and nobody shipped. An app declares its rules once (static labels, page-less segments, a dropped pair, namers for dynamic values, a hide predicate) and the walk is derived from location.pathname, router-free. Distilled from speedway's ShellCrumbs, whose SEGMENT_LABEL/NO_PAGE/TAB_CRUMBS/HIDDEN tables were the hand-roll this replaces; at the pin move that file shrinks to its rules feeding Breadcrumbs presets="trail" (the ?tab= crumb stays caller-appended). One deliberate divergence from the hand-roll: the last derived crumb never carries an href, per this kit's own Banned rule that the current page does not link to itself. Retires, once an app takes this release: the walking half of speedway's ShellCrumbs.tsx; its rule tables stay local by design.

  • InputContainer: disabled and readOnly now reach the frame. Both props were declared and documented as driving the muted surface, and neither was ever read, so a container wrapping a caller's own control stayed in its resting state no matter what the caller passed. The theme has carried .field-disabled and .field-readonly for exactly this case all along: the :has(input:read-only) selectors beside them only match a native input or textarea, so a select, a combobox, or any custom control fell through both paths. Screens change where an app passes either prop to InputContainer; a plain Input is unaffected, since its state reaches the DOM element and the :has() rules already saw it.

  • Button: the split button's caret gets an accessible name. Passing dropdown renders a second button holding nothing but a chevron, and it sat outside the name ladder the file already runs for icon-only buttons, so a screen reader announced it as "button" with no indication of what menu it opens. It now borrows the main action's words when they are a plain string ("More Export options") and falls back to "More actions" otherwise, plus aria-haspopup="menu". Screens change only for assistive tech; nothing moves visually.

  • canon-check now actually ships. package.json declared bin.canon-check while files omitted bin, so every tarball through 0.2.2 shipped no bin/ directory and npx canon-check resolved to nothing. Five places told consumers to use it, including the app template's own npm script. It went unnoticed because pr-claude fetches the raw file from versable-builder@main rather than from the package.

  • Stepper: the horizontal pipeline strip promoted under D7 (doc 44) from three hand-builds (speedway WorkflowStageChain's band, walmart PipelineStepper and the Jobs-row chain). Status circles (pending number, active ping honouring reduced motion, complete check, blocked pause), completion-keyed connectors, Kicker labels, a quiet detail or amber warn line, optional onStep buttons with disabledReason as the polite refusal, aria-label required, sizes sm/md. Retires, once an app takes this release: walmart's PipelineStepper.tsx and the Jobs-row chain's strip markup; speedway's WorkflowStageChain stepper BAND only, the file's banner and stage list stay local (per the Retires-line rule: name what fully dies).

  • Tint-ladder audit: 230 off-ladder opacity suffixes across the kit and playground moved onto their ladder rungs by role (pnpm lint:tokens 245 to 15 warnings; the 15 are named deliberate one-offs in .claude/output/20260820-tint-audit/report.md awaiting either a rung or an owner ruling). Verified in both themes on the app-shell, card and admin surfaces.

  • canon-check hardened against its own adversarial gate (14 dodges found, .claude/output/20260820-canon-gate/report.md): the seam now requires an ADDED dated note line, not a touched file; non-ASCII and spaced paths are inspected instead of silently skipped; theme modules, style preprocessor files, .mdx and the tailwind/postcss/next config files owe notes; a deleted or unreadable UI file is a finding, never silence; the overrule reason must read as a sentence and a fenced one does not count; inspectors match a two-line window (Prettier-formatted JSX), em-dash entities, and re-exported kit names.

0.2.2 (2026-08-20)#

  • canon-check, a package bin (bin/canon-check.mjs with the inspectors in bin/canon-classes.mjs): the canon feedback loop's fail-closed seam. A change that touches UI paths must add a line to the repo's CANON-NOTES.md or carry a human canon-note: none because <reason> line in the PR body; the lines it added are read for the deviation classes nobody writes a note about (raw controls beside a kit import, react-icons at a call site, arbitrary pixel sizes, colour and font literals, a kit component restyled through className, a local export shadowing a kit export, em dashes, loading text), each finding citing the canon paragraph. Marked deviations (// canon: <reason>) are notes. Plan: docs/plan/71-canon-feedback-loop-plan.md.

0.2.1 (2026-08-20)#

  • Theme: --font-mono puts Menlo ahead of ui-monospace, so an app that loads JetBrains Mono as a Google subset (no box-drawing glyphs) still draws ASCII diagrams with lines that meet; SF Mono's box characters are wider than the face's letters.

  • Sidebar/AppShell take navGrouping: flat (default, nothing changes) or railed, the group kicker at the idiom's quiet weight with rows one step in behind a hairline rail, for a long column (canon 08 §A10); the group holding the current page opens on arrival and can be closed again.

  • Markdown: links read in the primary ink and underline on hover; a link whose text is inline code keeps the chip, underlined at rest, and the chip takes the primary tint on hover.

  • Card takes iconVariant (disc, default, darkens a rung under the pointer; plain, bare at rest, the disc arrives on hover) and iconTone (a theme tone for icon and disc), every wash a rung of the tint ladder (canon 01 §A8).

  • Card takes the standard icon prop group: Icon renders left of the title in PageTitle's disc one size down, standard header meta for a section card; iconWrapClassName slot. NavGroup takes Icon, a mark beside the kicker, so a docs column and its index cards share one icon per section (owner, 2026-08-19, doc 69 D4, D7).

  • Sidebar/AppShell nav: a NavGroup takes groups (sub-groups under its rows, each with a smaller kicker and caret, one level) and defaultCollapsed (start closed; a long sidebar opens only the group the reader is in). Asked for by the playground's patterns and docs columns (owner, 2026-08-19); docs/app-shell.md.

  • SchemaForm polish (owner round R10, doc 67): the kit's RequiredMark (a red asterisk with a "Required" tooltip, now exported) replaces the required dot at every label site, labelTone="soft" for a quieter label; a warned control carries the warning edge and every coloured edge darkens a step on hover; segmented and radio groups use borders instead of outlines, an unselected segment showing the light primary edge; options read as sentences and take icons, swatches, trailing text and descriptions through x-option-meta (or straight from resolveOptions), a checklist can stack its descriptions with x-option-layout: stacked; Input md reads 14px at 40px like Select and Button (D3a). Spec 65 sections 6.2 and 7.1, canon 13 and canon 01 §A8 (the tint ladder) follow.

  • Select options take trailing (a right-side helper) and swatch (a colour dot on the row and beside the selected value, and the row's own hover tint); warning prop for the warning edge.

  • CodePill: a status pill for a module's own codes, reading label and description from the map its manifest declares (error_types, review_reasons); an undeclared code shows as typed in mono. Asked for by the forge console (gcp-opus).

  • SchemaForm and lintSchema: a form rendered from a JSON Schema in the kit's subset (docs/schema-form.md), with one field anatomy, type-derived widths, sections, x-when and x-tier, options from context or a resolver, tables and lists with their row lifecycle, mode choosers over a discriminated oneOf, colour, file and prompt widgets, plain-sentence validation, review and snapshot modes, presets with dirty tracking, a shape-matched loading skeleton, and refusal by name for anything outside the subset. Select gained an id prop. The canon is method/design-language/13-forms-and-fields.md, the recipe method/app-patterns/16-recipe-module-settings-form.md. Retires: every hand-built module settings form in the console once it adopts the schemas; walmart's and speedway's per-module parameter panels are the evidence, not consumers, until their own bump.

  • Timestamp and its formatters pin the locale (DEFAULT_LOCALE, en-US) instead of reading the runtime's, so a server-rendered app no longer risks a hydration mismatch when server and browser locales differ; locale prop to change it deliberately. formatCompactDateTime and DEFAULT_LOCALE are exported from the barrel. Retires: speedway's app/lib/datetime.ts locale pin, once it upgrades.

Added: Breadcrumbs (presets trail for a page trail, topbar for a topbar crumb, promoted from speedway's ShellCrumbs and walmart's topbar shape) · Markdown (a stored doc rendered with kit parts, raw HTML sanitized) · Kicker (default and bricked) · Input presets (email, password, search, url, numeric) and a unit adornment · WorkspaceSwitcher items with disabled, disabledReason and loading · CodeBlock colours bash command lines and gains a size prop · icon keys InputSearch, InputSpark, InputForm · the rich Tooltip (title, body, footer) as the standard for icon-only actions · Progress animates value changes · Chip click feedback on every type · PageTitle icon stagger and a phone-size title floor.

Changed on existing screens: sidebar groups collapse by default with an index row above them (opt out with collapsible: false, docs/app-shell.md) · Alert centres its icon and flows its body inline, border at /18 · Dropzone shows its error below the row · CodeBlock collapses its header on untitled fences · kicker and stat-hint ink at /65 · the topbar search is centred.

Kit-internal, no API change: the table module is exported by name from src/index.ts (the barrel is the inventory again); a render smoke suite and pnpm test exist.

Retires, once an app takes this release: speedway app/components/ShellCrumbs.tsx (use Breadcrumbs preset trail); any hand-rolled workspace picker in either app (speedway's local WorkspaceSwitcher.tsx already has no importers).

0.2.0 (2026-08-16)#

Added: Select gains a multiple mode (chips, the property word on the closed control) · FacetBar composes it and retires its private multi control · PageTitle, StatTile and Alert enter with fadeInUp (noAnimate opts out) · CodeBlock colours bash, python, yaml, html, csv, jsonc plus the ts and jsx aliases · Modal header and footer borders as independent options.

Changed on existing screens: the neutral shade tint is lighter kit-wide (8/14 from 12/20) · solid warning ink is white on amber-600, soft warning keeps amber ink · a Select multiple panel sizes to content, so a FacetBar menu loses its fixed w-60 box · the three components above animate in unless told not to.

Retires: walmart's local Radix multi-select and the hand-rolled MultiFacetControl in FacetToolbar.tsx (the kit's Select multiple and FacetBar cover both; docs/GUIDEBOOK.md section 7, row 20).

0.1.0 (2026-08-16)#

The batch-1 supersets as the first minor. Added: FacetBar (the union of both product toolbars) · col.link (the navigating cell that keeps its tooltip) · ListState (the four-way list branch) · TableSkeleton preset promoted from walmart's four-page shape · Chip presets · EmptyState forwards tone · CodeBlock learns tsx · useDebounce defaults to the canon's 300ms.

Changed on existing screens: dark neutral becomes a raised dark gray with ink remapped · Modal loses the phantom right gutter and trims footer spacing · InlineEdit keeps its content-sized box in column-flex parents · PageLoading drops trailing ellipses · toolbar coherence (one control skin, one menu anatomy) · FacetBar chips take the lg frame and the property prefix.

Retires: walmart's local TableSkeleton (adopted 2026-08); any hand-rolled EmptyState tone wrapper; walmart's .modal-box { scrollbar-gutter: auto } override (frontend/src/index.css:112-114), because the kit stopped reserving the gutter in 6ff8ac3.

0.0.14 to 0.0.25 (2026-07-16 to 2026-08-11)#

Twelve patch releases while both apps were under active development; each app moved its pin the same day a release landed. Not reconstructed here. To see one: git log <previous bump>..<bump> -- packages/ui/src, where the bump commits are git log --format='%h %s' -L '/"version"/,+1:packages/ui/package.json'. 0.0.25 (2026-08-11) carried "the six fixes" walmart's tech-debt review asked for and is the version walmart's README once told adopters to wait for.

@versable-git/ui · reference, canon, and method, read in place