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.
| Component | What changed |
|---|---|
| Circle icon buttons in dark | Bare again. The 0.6.0 hairline landed on every quiet circle, such as topbar and menu buttons. |
PageTitle | The icon is centred against the title and subtitle again, and the block no longer sits lower on the page. |
ModalTitle | The space under the title block is back to its 0.5 size. |
| Disabled quiet buttons | A disabled text, link or ghost button uses a muted ink, so an empty form's button no longer looks clickable. |
StatTile | On a narrow tile the icon sits above the label, so the label wraps between words. |
Table in a card or dialog | Warns 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
Tabsitem needs anicon. Without one it is a type error, and a development build names the tabs that lack it. RowMenuAction.onSelectcan beundefined, because a menu item can be a link (href) instead. Call it asitem.onSelect?.().- A table cell
widthno longer takes grid syntax such asminmax(). px, rem and percentages still work;shapeandsizereplace 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#
| Component | What changed | Keep the old behaviour with |
|---|---|---|
Button default variant | A bare Button is a quiet text button. Write variant="solid" on the one primary action of a screen. | variant="solid" |
Button shade | Now off by default. When on, the button rests on a wash of its colour instead of only tinting on hover. | add shade |
Button presets | new 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 onClick | A 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 icon | Warns once in development, naming the label. | Icon={null} |
| Cursors | Every enabled control shows the pointer; a button with disabledReason shows not-allowed. | a cursor-* utility |
| Button inks in dark | Quiet circle buttons get a hairline border; neutral outline, success and quiet secondary buttons take new inks so their labels pass AA. | none |
IdentityRow initials disc | Base ink on the primary tint in both themes. | avatarClassName |
CodeBlock, ListItem, IdentityRow, Dropzone, Table text | Shows the full value on hover when cut. | none |
ListItem with onClick | The row is a keyboard control: focusable, Enter and Space activate it. | none |
Dropzone drop area | No pointer on the drop area; the Browse button is the control. | none |
AppShell | Sidebar 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 |
Stepper | The 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 trigger | Shows the pointer, not a text cursor. | none |
Markdown inline code | Plain code on a faint wash that wraps with its line. | none |
DataTable toolbar | One row: filters on the left, search and a circle Refresh on the right, the count in the footer. | your own toolbar |
DataTable pager | Drawn only when there is more than one page. | pager="show" |
DataTable empty after a filter | Shows FilteredEmpty, quoting the query, with a Clear button. | empty |
| Table columns | A 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 Modal | Meets the dialog edges with a hairline above it, as in a Card. | bleed={false} |
| Empty cell | Reads N/A, lighter, where it was an em dash or a blank. | a custom render |
| Table copy buttons | Status and action cells no longer get one under copyCells. | none |
| Table scroll floor | The scroll box keeps three rows in view however a parent squeezes it. | minRows={0} |
Tabs | Icons and counts share the label's line. | none |
Modal title | ModalTitle draws a divider under itself. | divider={false} |
ConfirmModal delete preset | A solid error button named for the action ("Delete job"), plus "This action cannot be undone." | irreversible={false}, confirmLabel |
PageTitle | The 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 Select | Placeholders lose their ellipsis (Search, Select). fullWidth fills the control inside the frame. | placeholder |
StatusPill | dot 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 |
Breadcrumbs | A segment with no label rule shows as written, where it was capitalised. A long id loses its middle. | a labels rule |
Tooltip in a modal | Paints above the dialog. | none |
StatTile | On a narrow tile the figure steps down and the label wraps to two lines. | none |
SchemaForm | A 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.ButtondisabledReason: disables the button and shows the reason as its tooltip. Newaddandrerunpresets.RowActions: an action markedprimaryshows its label as a text button.useAction(fn): runs an action with a pending toast, then success or error, with an optional Retry.ThemeToggleandthemeBootScript: 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.InputendAction;Selecttop,requiredandfilterOption.- Table columns take
shapeandsizefor their width, andpriorityto drop out when the table is narrow. Tables also takereorderable,bulkActionsfor selected rows, andcol.entityfor an owner, team or file cell.Cardtakesfooter. defineStatusMap: one declaration of a state for its pill, filter options and cell.Tag: a borderless label with an icon.RefreshControl,FilteredEmptyandTruncatedText.PageTitletakesmeta.- 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.
reorderabledoes 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.0deprecated are gone, one data-record field is renamed, andButtonis now a discriminated union that rejects at compile time the prop combinations it used to ignore. A consumer on the primary API of0.4.0compiles and renders unchanged; a consumer still calling a deprecated alias, or setting an uppercaseIconfield on a nav item or row action, gets a type error until it migrates. The three apps are pinned at0.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 at71cdce3...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 breaks | the error a consumer sees | migrate to |
|---|---|---|
StatTile tone | tone does not exist on StatTileProps | toneClassName for a raw text-colour class, or color for a palette tone |
Stepper step warn | warn does not exist on StepperStep | warning, the same count-that-needs-eyes line |
InlineEdit ariaLabel | ariaLabel does not exist on InlineEditProps | the native aria-label |
Dropdown setOpen | setOpen does not exist on DropdownProps | onOpenChange(next), which receives the resolved next boolean rather than an updater function |
the LucideIcon type | LucideIcon is not exported | RegistryIcon, the icon-key union, unchanged in meaning |
an uppercase Icon field on a NavItem, NavGroup, or RowAction | Icon does not exist on the record; icon is expected | rename 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
Buttona button-only prop and it is now a compile error rather than a silently-ignored prop.ButtonPropsisButtonAsButton | ButtonAsLink, discriminated onhref: aButtonwithhreftypesdropdown,badge,badgeCorner,promisify, andIconOnClickasnever, so the split-button, badge, and async-pending machinery, which never applied to a link, can no longer be set on one.button.tsxstill branches onhrefat 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 atbb139f2...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 flipped | what a reader sees | keep the old behaviour with |
|---|---|---|
Table and DataTable bleed to the card's edge | rows meet the card's border; the toolbar above and pager below keep the card's padding | bleed={false} |
ModalTitle and ModalFooter draw no divider | the rule above a footer and below a title is gone | divider on that element |
| a captioned field reserves one message line | a field with a top caption gains a small minimum height under it while quiet, so a warning landing never shifts the layout | reserveMessage={false} |
a bricked control refuses its click | a bricked Button no longer runs its onClick, and a bricked link no longer navigates; it stays hoverable so its tooltip can still explain the refusal | nothing, 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-groupfires when two or more buttons in one control group carryshade: 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-mismatchfires when a control in a flex row differs from that row's most common height by more than 4px. An appliedChipis exempt where the row centres its items, because canon sizes it as thelgbadge 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/:
| doc | what 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.md | principle 11, the one-shade rule the new check enforces |
05-cards-and-surfaces.md | the kit bleeds by default; bleed={false} keeps the inset |
06-tables.md | the same rule, stated in the browsable-list row |
07-toolbars-and-filters.md | every control in a toolbar row is 32px tall, with the applied Chip excepted |
12-typography.md | 13px 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.md | a sign-in form reports a wrong credential as a warning, never an error |
What you can delete once you take this#
| app | what goes | replaced by |
|---|---|---|
| versable-forge-v6 | src/components/row-actions.tsx | RowActions |
| versable-forge-v6 | fifteen hand-written aria-label call sites | a ReactNode top naming its own control |
| speedway | app/root.tsx NavProgressBar | TopLoadingBar |
| speedway | the edge-cancelling half of its hand-rolled table bleed | the new default |
| walmart-mvp | eight 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#
RowActionsis a new component: a row's common actions as visible icon buttons, the rest folded into the kit's ownRowMenuor into a caller's card. Five action kinds, exactly one per action:onSelect,href,modal(the sharedopenModal, mounting no dialog of its own),panel(a cached popover, one open across the page at a time) andrender.DataTable rowActionsandcol.rowActionsbuild the trailing column.SchemaForm's own rows table adopts it and drops from five open buttons to two plus a kebab.FacetOptiontakesswatch, a colour dot for a module or stage, andtrailing, a short right-edge slot. Both reach theSelectlistbox row throughtoSelectOptions, and the applied chip shows the swatch as its leading dot when the option carries no icon.
Forms#
- A
ReactNodetoponInputnow names its own control, pointing at the rendered caption throughuseIdso the browser computes the accessible name from the caption's text. A stringtopalready worked, and a caller's ownaria-labeloraria-labelledbystill wins. This closes the caveat 0.3.0 left open. InputandInputContainertakewarning, carrying the same edge treatmenterroralready draws, witherrorwinning when both are set.SchemaForm'sFielddoes the same in edit and cell mode.SchemaFormtakesnoPortaland reaches everySelectit renders, plus anyDropdowna widget opens, so a choice field's listbox opens inside aModal's top layer instead of behind it. PromotesIN-20260905-03.
Modals#
ModalBodyandModalSectiongive a body its padding, its gap and its labelled sections without hand-rolling a<div>.ModalFooter'sbackandnextrender through the kitButtonrather than a rawbtnclass, so each carriescolor,variantandshade.nextdefaults tocolor="primary".
App shell#
SidebarandAppShellgainresizable,width,minWidth,maxWidthandstorageKey, spelledsidebarResizableand so on when passed toAppShell. Drag the rail's right edge between 200 and 400px atlgand up; dragging under 120px collapses to the rail rather than clamping, and the width persists under one key.SidePanelnow 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.TopLoadingBaris 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-schemabin runs the samelintSchemawalk from a shell, so a subprocess can refuse a bad schema before it ever reachesSchemaForm. 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. Seedocs/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:
StatTiletakes acolor(aSemanticColor, winning over the renamedtoneClassName);Stepperreadswarningforwarn;InlineEditreads the nativearia-labelforariaLabel;ChipColoris now an alias ofSemanticColor.DropdowngainsdefaultOpenandonOpenChange(the oldsetOpenstill fires).Buttontakes arefacross its anchor and button forms, andSelectacontrolReffor the focusable control. - Every icon extender now forwards the whole
IconImportPropsgroup by name, soiconPropsreaches the rendered icon onButton,Alert,Chip,StatTile,Toastand the rest, where before it was silently dropped. - A link-mode
Buttonruns the caller'sonClickinstead of discarding it. - Runtime colours route through the accent seam (
accentStyle/accentClass) rather than inline styles, soSelect's swatch and active row, andSchemaForm's colour buttons, read from one place.ToastusescurrentColorand 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,SelectandDropzonesetaria-invalidfrom a truthyerrorand pointaria-describedbyat their message, which carriesrole="alert". Select's closed control reportsaria-expanded, and its clear affordance moved out of the control'srole="button"subtree so the trigger has no interactive descendant.Modalacceptsaria-labelandaria-labelledby, and a stringModalTitlenow labels its dialog automatically. ATabspanel names itself from the active tab,Dropzoneis a drag region whose only button is the browse control, and a closedSidePanelisinertand labelled from its title.AlertandToastpick their live-region role by severity: an error announces asalert, everything else asstatus.
Known gaps#
- Three items landed narrower than they were specified. Modal footer buttons
carry
color,variantandshadebut noticonortooltip. The reserved message line covers captioned fields rather than every field.RowActionsleaves 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
fixseverity 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.
-
CodeBlockand 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-ringintheme/fields.css) instead of a border colour change alone. Every facet trigger in aFacetBarand 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 aDataTablefilter chip hadoutline-nonewith nothing in its place and take the same ring. The kit standard gainsfocus-by-border-only, which found the second. Pinned intest/select-focus-ring.test.tsx. From the notes: the console's keyboard walk, 2026-09-08. -
A bricked
Buttoncarriesaria-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 intest/button-bricked-aria.test.tsx. From the notes: the console's New job walk, 2026-09-08. -
RowMenuis a menu to assistive tech: its trigger declaresaria-haspopup="menu", its panel carriesrole="menu"with the row's label, and each action is amenuitem. The console drove the file-table row menu and found zero menuitems behind a trigger announcing a popup (gcp row #657).Dropdownitself is unchanged: it floats caller JSX and carries no menu semantics of its own. Pinned intest/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
/65floor 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.formatRelativeTimegains a future branch ("in 5m", "in 2h", "in 3d", then the short date), so an expiry or due date never reads "just now".ConfirmModaltakes anIconprop 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 inallValue(required;allLabellabels 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: anymulti: falsefacet fails typecheck until it namesallValue, a one-line addition; a facet that already listed an "all" option first names that value and renders exactly as before. Retires: the console'sabsoluteworkaround on its Invitations time columns, and its explicit{ value: "all", label: "Any time" }first option on the History facet (keep the option, addallValue: "all"). From the notes: gcp.claude/output/20260901-0730-kit-defects/kit-defects.md, 2026-09-01. -
The kit standard:
docs/KIT-STANDARD.mdstates 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-testplants 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 (onChangeorselectedset, not selected, not removable) now defaults tovariant="soft"instead ofsolid, 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 selectableChipthat relied on the solid default at rest gets the soft look; passvariant="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.mdis 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 byscripts/sync-method-docs.mjs;docs/KIT-INVENTORY.mdanddocs/llms.txtare 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; andprompts/claims.mdis 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.mdstops 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) andcards(stacked cards at measured px heights), composingCardandSkeleton. Takes the page's OWN container class rather than shipping a container map, so one map decides the box on both sides;layoutis required with no default because a default is a guess about a page nobody looked at.headeraccepts the page's real header element so the top does not redraw when data lands,headroomholds open chrome the placeholder does not draw. Retires, once an app takes this release: nothing outright. Speedway'sWorkspaceSkeletonbody is replaceable by this, but itsPANELmap is speedway's own layout classes and stays local (per the Retires-line rule: name only what fully dies). -
Input: a stringtopnow becomes the field's accessible name.toprenders outside the<label>that wraps the control, so until now every field built withtop="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 anhtmlFor/idpairing, because the file stays hook-free and server-renderable. A caller's ownaria-labeloraria-labelledbystill wins. Screens change only for assistive tech; nothing moves visually. Still yours to name: a placeholder-only field, and a ReactNodetop. -
Alert: theneutraltone gets its own--neutral-softsurface token instead ofbg-base-200. On any page orAppShellframe, both of which arebase-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 neutralAlertin 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.Modalneeds 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 theitemsprop 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 fromlocation.pathname, router-free. Distilled from speedway'sShellCrumbs, whoseSEGMENT_LABEL/NO_PAGE/TAB_CRUMBS/HIDDENtables were the hand-roll this replaces; at the pin move that file shrinks to its rules feedingBreadcrumbs presets="trail"(the?tab=crumb stays caller-appended). One deliberate divergence from the hand-roll: the last derived crumb never carries anhref, 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'sShellCrumbs.tsx; its rule tables stay local by design. -
InputContainer:disabledandreadOnlynow 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-disabledand.field-readonlyfor 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 toInputContainer; a plainInputis 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. Passingdropdownrenders 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, plusaria-haspopup="menu". Screens change only for assistive tech; nothing moves visually. -
canon-checknow actually ships.package.jsondeclaredbin.canon-checkwhilefilesomittedbin, so every tarball through 0.2.2 shipped nobin/directory andnpx canon-checkresolved 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 fromversable-builder@mainrather than from the package. -
Stepper: the horizontal pipeline strip promoted under D7 (doc 44) from three hand-builds (speedwayWorkflowStageChain's band, walmartPipelineStepperand 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, optionalonStepbuttons withdisabledReasonas the polite refusal,aria-labelrequired, sizes sm/md. Retires, once an app takes this release: walmart'sPipelineStepper.tsxand the Jobs-row chain's strip markup; speedway'sWorkflowStageChainstepper 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:tokens245 to 15 warnings; the 15 are named deliberate one-offs in.claude/output/20260820-tint-audit/report.mdawaiting either a rung or an owner ruling). Verified in both themes on the app-shell, card and admin surfaces. -
canon-checkhardened 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 packagebin(bin/canon-check.mjswith the inspectors inbin/canon-classes.mjs): the canon feedback loop's fail-closed seam. A change that touches UI paths must add a line to the repo'sCANON-NOTES.mdor carry a humancanon-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-monoputs Menlo ahead ofui-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/AppShelltakenavGrouping:flat(default, nothing changes) orrailed, 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. -
CardtakesiconVariant(disc, default, darkens a rung under the pointer;plain, bare at rest, the disc arrives on hover) andiconTone(a theme tone for icon and disc), every wash a rung of the tint ladder (canon 01 §A8). -
Cardtakes the standard icon prop group:Iconrenders left of the title in PageTitle's disc one size down, standard header meta for a section card;iconWrapClassNameslot.NavGrouptakesIcon, 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/AppShellnav: aNavGrouptakesgroups(sub-groups under its rows, each with a smaller kicker and caret, one level) anddefaultCollapsed(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 throughx-option-meta(or straight fromresolveOptions), a checklist can stack its descriptions withx-option-layout: stacked;Inputmd 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. -
Selectoptions taketrailing(a right-side helper) andswatch(a colour dot on the row and beside the selected value, and the row's own hover tint);warningprop 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). -
SchemaFormandlintSchema: 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-whenandx-tier, options from context or a resolver, tables and lists with their row lifecycle, mode choosers over a discriminatedoneOf, 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.Selectgained anidprop. The canon ismethod/design-language/13-forms-and-fields.md, the recipemethod/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. -
Timestampand 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;localeprop to change it deliberately.formatCompactDateTimeandDEFAULT_LOCALEare exported from the barrel. Retires: speedway'sapp/lib/datetime.tslocale 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.