# F1 component kit: the @rj/admin-kit contract

**Client:** brand-006-rj-laravel-admin (RJ Laravel Admin) · **Phase:** 1, the F1 component kit (HANDOFF §4 Phase 1) · **Written:** 2026-10-18 · design only, no code.
**Direction:** 4b·R, HQ Lockup with the labelled rail (Frank, 2026-10-18). **Stack (locked):** Next.js App Router + React 19 + TypeScript + shadcn/ui on Radix + Tailwind on CSS-variable tokens (`phase-0/STACK-AND-PACKAGES.md`).
**Live:** https://kit.pina-brand-006-rj-laravel-admin.pages.dev/families/ (phone: `/families/m/`).

These specimens **are the component API of the shared package `@rj/admin-kit`** (STACK §5.6), consumed by both the DTC admin (kit v4, Midnight green `#08120E`, `data-theme="dtc"`, comfortable by default) and the Laravel admin (navy `#0E1729` pinned, compact by default). One component, two themes, no per-app forks.

## 1. File layout (copied from brand-005 phase-2/kit)

```
phase-1/kit/
  tokens.css      kit v4 tokens (verbatim) + phase-0 tokens-delta (verbatim) + B6 kit layer + [data-theme="dtc"]
  base.css        Part A: brand-005 kit v4 base.css (verbatim) · Part B: the F1 layer (.lv-* shell + composites, flagship atoms)
  kit.js          page toolbar (density / theme), [data-k-pop] toggle, [data-kx-copy]; nothing else
  CONTRACT.md     this file          KIT-CHANGELOG.md   what this kit added to brand-005 v4
  families/index.html (+ m/)        docs-style hub: left nav by group, every family with its bucket and status, the as-is token-swap strip
  families/<slug>/index.html + m/index.html   one family = desktop (1440) + a dedicated phone composition (390)
  src/            generator (build.py + fam_*.py), vendored upstream files, menu + icon snapshots
```

Every family page has the same sections: **Anatomy · (variants) · States · Density (compact | comfortable) · English · 中文 · Long string | Permission denied · Both apps (Laravel | DTC) · Contract**. The phone page drops the density compare (phone is always comfortable, POWERUPS #15) and the theme compare.

## 2. Rules every component follows

- **Density is an attribute, not a restyle.** `data-density="compact|comfortable"` swaps `--row`, `--row-th`, `--control-h`, `--control-h-sm`, `--text-body`, `--text-small`, `--tab-h`, `--btn-px`, `--label-col`, `--cell-w`. Laravel defaults to compact (2b ultra: 30px rows, 12.5px body); DTC to comfortable; the phone shell is always comfortable.
- **Theme is an attribute.** `data-theme="dtc"` swaps the colour ramp to kit v4 Midnight green. The Laravel theme is `:root`; its navy is pinned literally (tokens-delta §3), so a DTC recolour never repaints the parent app.
- **Permission-shaped.** Every component takes what the server allows (`GET /admin/me` permissions, `GET /admin/me/menu`). Controls a role cannot use are **absent**; `NotAvailable` ("Not available to your role") is used only where hiding would confuse (T003). Each specimen shows the denied case.
- **en / 中文.** Copy comes from `next-intl` messages; menu labels arrive localised from `/me/menu`. A missing cn label falls back to English (cn.json covers 29 of 119 menu labels). CJK glyphs fall back to `PingFang SC`, `Noto Sans SC`, `Microsoft YaHei` (`--font-cjk`). Never letter-space CJK.
- **Long strings.** Single-line cells truncate with an ellipsis and a `title`; multi-line slots wrap (`overflow-wrap:anywhere`). Rail labels wrap to two lines, never truncate. Status chips never truncate below their 176px column.
- **Times.** The API sends UTC; the client formats in the staff zone (`DateTime`, M08). Relative up to 7 days, then absolute; UTC in the tooltip.
- **Real data only.** Order ids are bare 4-digit integers (`4521`, pieces `4521-1`, groups `4521-1-2`); only the 11 real order statuses and the 6 real ticket statuses; factories as codes (FX, FRR, F3.5, FPS); people are anonymised (initial + surname); `dotted underline` = example value.
- **Job states are designed, not polled.** Queued · running % · partial failure · failed + retry · done + download, polled every few seconds with `@tanstack/react-query` `refetchInterval` (T001 Q2).
- **Header dropdowns and sheets size to the data (kit v2).** Width tokens: `--dropdown-w-sm` 360px · `--dropdown-w-md` 520px (notification centre) · `--dropdown-w-lg` 600px (Jobs) · `--sheet-w-lg` 760px · `--sheet-w-xl` 960px; `--dropdown-max-h` `min(78vh, 720px)`. A dropdown is `min(token, 100vw − 32px)` wide, its right edge on its own button's right edge, header + filter strip + footer fixed and the body scrolling with sticky group heads. Phone: the same content as a full-screen sheet. Fields: `phase-2/truth/TRUTH-JOBS.md`.
- **Bulk = batches of 100** (T003 #13): the frontend splits a selection, shows `batch n of m`, and offers *Stop after this batch*.

## 3. Components (F1 variants and missing M01–M17)

### 01 · Page frame and shell (`families/shell/`)

Bucket: **F1 variant** · Covers: page-frame primitive, sidebar designer, menu-item designer, top-bar designer, tab-bar designer

The permission-shaped shell built from GET /admin/me/menu: the white RJ HQ corner, a 76px navy rail with the server's label under every icon (wraps to two lines, never shortened), T-a accordion when expanded, T-b / T-d flyouts when collapsed, T-f breadcrumb dropdown, and the header right side (Find order · Jobs · bell · globe · clock).

| Export | Props | Variants | States | shadcn / Radix | Package underneath | Accessibility | API operations | Phone |
|---|---|---|---|---|---|---|---|---|
| `AppShell` | me (GET /admin/me) · menu (GET /admin/me/menu: sections[].items[] key, label, path, type, badge, disabled, children) · collapsed (⌘B, persisted) · crumbs · headerRight | collapsed (76px labelled rail) · expanded (232px) | loading · ready · signed-out · session-expired · menu-error | Collapsible · Tooltip · DropdownMenu (crumbs) · Sheet (phone drawer) | @clerk/nextjs · @tanstack/react-query (useMe) · next-intl · lucide-react | nav landmarks; rail items are links with the full label as accessible name; ⌘B toggles; focus returns to the toggle | admin.me.show · admin.me.menu | PhoneShell: app bar (menu · title · search · jobs · bell) + bottom bar of 3–5 permitted tabs + drawer accordion |
| `RailItem` | item (menu node) · active · badge | — | default · hover · active · disabled (menu disabled) · badge | Tooltip (collapsed) | lucide-react (stroke 1.5) | label always visible; aria-current=page | — | — |
| `Flyout` | node · depth 2\|3 · openDelay 140ms | T-b (one column) · T-d mega (level-2 headings over level-3 links, 1–3 columns) | — | HoverCard-like Popover with click + keyboard twins | — | → opens, ← / Esc closes, ↑↓ moves | — | — |
| `CrumbSwitcher` | siblings from the parent menu node | T-f | — | DropdownMenu | — | the crumb is a button with aria-haspopup=menu | — | — |

### 02 · Jobs list and notification centre (`families/job-tray/`)

Bucket: **missing (new)** · Covers: M01

Two header controls, never merged (FEEDBACK-B-r1). Jobs opens a 600px list (right edge on the Jobs button) grouped Running · Queued · Needs attention · Done today, each row three lines of the real fields: title + type tag, target · initiator or source · id, then progress with counts, the dispatch / print / send line or the error. Open all jobs is a 960px side sheet with the full error and the failed queue payload. The notification centre is its own 520px bell menu (2-day window, grouped by day, bodies clamp to two lines) with a 760px sheet. Kit v2 after Frank 2026-10-18; fields: phase-2/truth/TRUTH-JOBS.md.

| Export | Props | Variants | States | shadcn / Radix | Package underneath | Accessibility | API operations | Phone |
|---|---|---|---|---|---|---|---|---|
| `JobsButton` | counts {running, failed} from GET /admin/nav-badges (running_job_batches own · failed_jobs if manage failed jobs) + print / agent in-flight · pollMs 3000 while open, 15000 closed | — | idle · running · failed · stale | Popover (align end, sideOffset 6) | @tanstack/react-query refetchInterval | button names the counts (“3 running, 3 need attention”); Esc returns focus | — | app-bar icon → full-screen sheet |
| `JobsDropdown` | width var(--dropdown-w-lg) 600px, clamped min(600px, 100vw − 32px), right edge = button right edge · maxHeight var(--dropdown-max-h) min(78vh, 720px), body scrolls, header + tabs + footer fixed · groupBy state (Running · Queued · Needs attention · Done today) · filter state tab + type select (Queue batch · Cloud print · Cloud agent type · Fabric photos) · onOpenAll | — | default · many (internal scroll, sticky group heads) · attention · partial · stale · empty · own-only (role) | Popover + ScrollArea + Tabs + Select | — | — | admin.notifications.batches (own) · admin.dashboard.job-batches?shape=v2 · admin.orders.print-jobs.index?status=in_flight\|failed · admin.orders.fabric-jobs.index · admin.pricing.exports.show · print-studio photo_batch_status | — |
| `JobRow` | title · typeTag (batch name label · print job_type · CloudJob type_label) · target (order + retailer · supplier · WeChat target · file) · initiator (batches: user) or source (cloud · manual · reprint) · shortId · progress {processed, total, failed} · statusLine (dispatch · print / send) · elapsed · eta · error (2-line clamp, More expands in place) · messagePreview · actions | 3 lines: title + type tag + time · one meta line · progress / status / error | queued · running % · partial (green + red bar) · failed · done | — | — | row is a listitem; error is a native details/summary; actions are buttons with labels | — | same row, full width |
| `JobActions` | only real ones: Download (pricing export file · orders export link) · Open PDF (signed, 15 min) · Reprint label (can_dhl_reprint) · Page-range reprint (can_kanban_reprint) · Retry send (WeChat send, admin) · Results CSV (photo batch) · Details | — | no Cancel and no generic Retry: no REST route cancels a batch or retries a print / Cloud Order row (TRUTH-JOBS §1–§3) | — | — | — | — | — |
| `JobsSheet` | width var(--sheet-w-xl) 960px, clamped to 100vw − 48px · list (360px) + detail · jobId | detail = every field of the row + full last_error + the matching failed queue job (exception + payload in JsonViewer, raw for super-admin, redacted otherwise) | — | Sheet (side right) | — | — | admin.failed-jobs.show · admin.orders.print-jobs.pdf-url · admin.orders.print-jobs.index?highlight | full-screen page |
| `NotificationCentre` | width var(--dropdown-w-md) 520px, clamped, right edge = bell right edge · maxHeight var(--dropdown-max-h) · items (id, type, message, link, links[], read, created_at) · groupBy day (2-day window) · filter All · Unread + type | — | unread · read · hover (mark read · delete) · long body (clamp 2 lines, More) · empty · error | Popover + ScrollArea | — | bell names the unread count | admin.notifications.index · .unread-count · .read · .unread · .toggle · .read-all · .unread-all · .destroy · .destroy-all · .destroy-old | full-screen sheet |
| `NotificationsSheet` | width var(--sheet-w-lg) 760px · type chips (All · Failures · Tickets · Orders · Exports · Shipping) · overflow: mark all unread · delete read · delete older than 2 days | — | — | Sheet (side right) | — | — | — | — |
| `JobToast` | job | — | success with Download · failure with Details | Sonner | sonner | — | — | — |

### 03 · Language globe, clock and timezone (`families/locale-tz/`)

Bucket: **missing (new)** · Covers: M09

Per-person preferences stored by PUT /admin/me/preferences {locale, timezone}. The globe is the whole control: one click toggles EN ↔ 中文. The clock shows the time in the staff zone and opens the timezone choice. Settings that carry their own zone (shipping cutoff, supplier) use the IANA timezone field.

| Export | Props | Variants | States | shadcn / Radix | Package underneath | Accessibility | API operations | Phone |
|---|---|---|---|---|---|---|---|---|
| `LocaleToggle` | locale en\|cn · onChange | — | en · cn · saving · error | Toggle (icon button) | next-intl | aria-label “Language: English. Switch to 中文” | admin.me.preferences.update {locale} | — |
| `ClockButton` | timezone · now | — | — | Popover + Command (search) | @date-fns/tz · cmdk | time is a live region off; button names the zone | admin.me.preferences.update {timezone} | — |
| `TimezoneSelect` | value IANA · disabled | — | — | Combobox (F1) over Intl.supportedValuesOf('timeZone') | cmdk | — | — | in the profile drawer |

### 04 · Date-time display and date range (`families/datetime/`)

Bucket: **F1 variant + missing** · Covers: M08, date designer

The API returns UTC; the browser formats in the staff member's zone (T001 Q25). Relative up to 7 days, then absolute; the tooltip shows the staff zone, UTC and ISO. The F1 DateRange takes day boundaries in the staff zone and sends UTC, with Today / 7 days / 30 days presets and a month / year mode for the gantt grid.

| Export | Props | Variants | States | shadcn / Radix | Package underneath | Accessibility | API operations | Phone |
|---|---|---|---|---|---|---|---|---|
| `DateTime` | value ISO UTC · mode auto\|relative\|absolute · zone (default: me.timezone) · ownZone | — | relative · absolute · dual-zone · null · withheld · loading | Tooltip | date-fns + @date-fns/tz (TZDate) | ; tooltip on focus too | — | tap flips relative ↔ exact |
| `DateRange` | from/to · presets · mode day\|month | range + presets · month / year | — | Popover + Calendar | react-day-picker · @date-fns/tz | — | from / to on admin.replays.index · month / year on admin.orders.gantt-grid | — |

### 05 · Data table (`families/data-table/`)

Bucket: **F1 variant** · Covers: data-table primitive, data-table designer, table-sort designer, sticky-header designer, row-actions designer, column-types designer, table-states designer, density designer

The A01 index (109 pages). TanStack Table with server paging and totals, compact 30px rows, sticky header and sticky first column, column chooser, keyboard walk, inline edit with saving / saved / rejected, expandable and grouped rows, real column types (4-digit order id, retailer pill, piece pills by phase, real status chips, factory codes + HF) and the shared table states. Drawn from flagship O01.

| Export | Props | Variants | States | shadcn / Radix | Package underneath | Accessibility | API operations | Phone |
|---|---|---|---|---|---|---|---|---|
| `DataTable` | columns · data · manualPagination · rowCount · sorting (only when serverSort) · selection · onBulkAction · density · stickyFirstColumn · columnVisibility · expand · groupBy · state | compact · comfortable · framed | loading · ready · refetching (dim) · empty · filtered-empty · zero · error · stale · offline · partial · denied (read-only) · long-string · CJK | Table (shadcn) · Checkbox · DropdownMenu (row ⋯) · Popover (column chooser) | @tanstack/react-table 8 · @tanstack/react-virtual · nuqs (page, sort, filters in the URL) · @tanstack/react-query | role=grid with roving focus; ↑↓ rows, Space peek, ↵ open, x select, / search; header checkbox is tri-state; sticky cells keep their reading order | admin.orders.index · admin.orders.status-catalogue · admin.tickets.index · admin.fabrics.index · admin.default-measurements.update (inline) | DataCardList: cards in a list, status chips scroll sideways, long-press to select |
| `ColumnTypes` | orderId · retailer · pieces · orderStatus · ticketStatus · factory · money · datetime · bilingual · mono + copy · thumb · chips (+N) · bool | — | — | — | — | truncated cells carry the full value in title + accessible name | — | — |
| `InlineEditCell` | value · onSave (optimistic) · validate | — | idle · editing · saving · saved (Undo) · rejected 422 (typed value kept) | — | react-query useMutation (rollback) · sonner (Undo) · zod | — | — | — |
| `ColumnChooser` | columns · visible · order | — | on · off · unavailable with reason (Meas. dropped) | Popover + Checkbox + dnd-kit | — | — | — | — |

### 06 · Pagination (`families/pagination/`)

Bucket: **F1 variant** · Covers: pagination primitive, pagination designer

Server pagination with totals: “51–100 of 1,284”, a 25 / 50 / 100 page size, page and size in the URL, jump to page, and “Load more” for append-only logs.

| Export | Props | Variants | States | shadcn / Radix | Package underneath | Accessibility | API operations | Phone |
|---|---|---|---|---|---|---|---|---|
| `Pagination` | page · perPage 25\|50\|100 · total · lastPage · mode pages\|loadMore | — | first · middle · last · loading · empty | Select (page size) · Button | nuqs (page, per_page) | nav aria-label=Pagination; disabled arrows are aria-disabled, not removed | any list with page / per_page (69 GETs), e.g. admin.api-request-logs.index | “Load 25 more” button under the cards |

### 07 · Filter bar and saved views (`families/filter-bar/`)

Bucket: **F1 variant** · Covers: filter-bar primitive, filter-bar designer, saved-views designer

Facets at scale with every filter in the URL (paste a link into WeChat and the factory sees the same list). Async facet values over hundreds of retailers, the archived toggle, and the real search rule: search pauses the tabs because the API refuses status + customer together. Saved views live in the URL and this browser; shared team views need backend (P2).

| Export | Props | Variants | States | shadcn / Radix | Package underneath | Accessibility | API operations | Phone |
|---|---|---|---|---|---|---|---|---|
| `FilterBar` | facets (key, label, kind: multi\|bool\|range\|async) · value (URL) · onChange · searchPausesTabs | — | empty · set · searching · facet-loading · facet-error · archived | Popover · Command (facet search) · Checkbox · ToggleGroup | nuqs (parseAsArrayOf) · cmdk · react-query (async facets) · use-debounce | each chip is a button naming facet + value; ✕ has its own label | admin.orders.index (status, brand, returning, customer) · admin.tickets.index (status, severity, category, owner, has_order) · with_trashed / only_trashed | chip row + bottom-sheet facet picker |
| `SavedViews` | views · current · dirty · onSave · onUpdate | — | clean · dirty · default | DropdownMenu | zustand persist (this browser) | — | none: shared, server-stored views are proposal P2 (needs backend) | — |

### 08 · Bulk action bar · batches of 100 (`families/bulk-action-bar/`)

Bucket: **F1 variant** · Covers: action-bar primitive, action-bar designer, bulk-bar designer

Select a page, then “select all 2,131 matching”. The frontend splits the run into batches of 100 (T003 #13): batch progress, Stop after this batch, partial failure with the failed ids and a retry, and only the bulk verbs the role holds.

| Export | Props | Variants | States | shadcn / Radix | Package underneath | Accessibility | API operations | Phone |
|---|---|---|---|---|---|---|---|---|
| `BulkBar` | selected · matching · actions (filtered by permission) · batchSize 100 · run | — | idle · page-selected · all-selected · running · stopping · stopped · partial · failed · done | Progress · DropdownMenu (Print ▾) | TanStack row selection · zustand (selection across pages) · sequential useMutation runner | progress is a live region (polite); Stop is reachable by keyboard while running | admin.accounts.bulk-permissions (dry_run) · admin.api-keys.bulk-revoke · admin.accessory-codes.bulk · orders Complete = POST orders/completed in batches (proposal) | long-press to select; bar docks at the bottom |

### 09 · Resizable split pane (`families/split-pane/`)

Bucket: **missing (new)** · Covers: M06

List and record side by side (tickets inbox, orders peek pinned to split). Drag the handle or use the keyboard; the width is remembered per person; collapse the list with ⌘\.

| Export | Props | Variants | States | shadcn / Radix | Package underneath | Accessibility | API operations | Phone |
|---|---|---|---|---|---|---|---|---|
| `SplitPane` | left · right · defaultSize · minSize 320px · collapsible · storageKey | — | default · dragging · collapsed · empty · loading | Resizable (shadcn) | react-resizable-panels · zustand persist (width) | handle is role=separator with aria-valuenow; ←→ resize by 16px, Home/End collapse/expand | admin.tickets.index + admin.tickets.show · admin.orders.index + admin.orders.show | none: list → record |

### 10 · Sortable list (`families/sortable-list/`)

Bucket: **missing (new)** · Covers: M14

Drag to reorder with a keyboard twin (Space to lift, ↑↓ to move, Space to drop), then a saved state with Undo or an error that restores the old order. 23 reorder operations need it (Q13).

| Export | Props | Variants | States | shadcn / Radix | Package underneath | Accessibility | API operations | Phone |
|---|---|---|---|---|---|---|---|---|
| `SortableList` | items · onReorder (ids in order) · disabled | — | idle · dragging · keyboard-moving · saving · saved · error | none (list-row) | @dnd-kit/core + @dnd-kit/sortable (pointer, keyboard, touch sensors) | dnd-kit announcements: “Picked up Chest. Position 3 of 6.” | admin.default-measurements.reorder · admin.fabrics.reorder · admin.product-quality-checks.reorder · admin.photo-market.photos.reorder · admin.designs.sort (23 ops) | long-press to drag, or ↑ / ↓ buttons |

### 11 · Tree view (`families/tree-view/`)

Bucket: **missing (new)** · Covers: M15

Folders and hierarchies: the media library tree and the vision-check failure reasons (module → checkpoint → category) with their toggles.

| Export | Props | Variants | States | shadcn / Radix | Package underneath | Accessibility | API operations | Phone |
|---|---|---|---|---|---|---|---|---|
| `Tree` | nodes · loadChildren · selected · onToggle (optional switch per node) | — | collapsed · expanded · loading · selected · off · empty · error | Collapsible (recursive) | @tanstack/react-virtual if large | role=tree / treeitem with aria-level, aria-expanded; ←→ collapse/expand, ↑↓ move | admin.media-library.tree · admin.vision-checks.failure-reasons.children · …toggle-module / …toggle-checkpoint / …toggle-category | same tree, 44px rows |

### 12 · Combobox, cloth picker and ⌘K (`families/combobox/`)

Bucket: **F1 variant** · Covers: combobox primitive, combobox designer, cloth-picker designer, command-palette designer

Async search over thousands of rows: debounced server search + per_page, “20 of 186”, loading / empty / error, multi-select, creatable, recent picks, and matching on the en or cn name. The cloth picker re-points to admin.fabrics.index (~3,600 cloths); ⌘K adds server quick search plus pages from /me/menu.

| Export | Props | Variants | States | shadcn / Radix | Package underneath | Accessibility | API operations | Phone |
|---|---|---|---|---|---|---|---|---|
| `Combobox` | query (async fn: search, page) · value · multiple · creatable · recentKey · renderOption · minChars | — | closed · typing · loading · results · empty · error · selected · multi · creatable · read-only | Popover + Command (cmdk) | cmdk · @tanstack/react-query · use-debounce · @tanstack/react-virtual | combobox / listbox roles, aria-activedescendant; result count announced | admin.fabrics.index (search, per_page) · admin.orders.lookup · admin.pickers.account-options | full-screen picker sheet |
| `ClothPicker` | supplier facet · status · multiple | — | as Combobox + stock chip | — | Combobox + cloth row | — | admin.fabrics.index | — |
| `CommandPalette` | menu (from /me/menu) · quickSearch | — | — | Dialog + Command | cmdk · react-hotkeys-hook | ⌘K / Ctrl K; Esc closes and returns focus | admin.dashboard.quick-search · admin.me.menu | search icon in the app bar, full screen |

### 13 · Money input · wallet before → after (`families/money-input/`)

Bucket: **F1 variant** · Covers: money-input primitive, money-input designer

Signed money with its currency: Debit (the default, as on the ticket wallet box) or Credit, the settlement currency, and the wallet balance before → after. A debit can never take a wallet below zero.

| Export | Props | Variants | States | shadcn / Radix | Package underneath | Accessibility | API operations | Phone |
|---|---|---|---|---|---|---|---|---|
| `MoneyInput` | amount (minor units) · currency · direction debit\|credit · balance · min/max | — | empty · focus · valid · over-balance · loading · disabled · read-only | Input + Select (currency) + ToggleGroup | react-hook-form · zod · Intl.NumberFormat | label states the currency; the resulting balance is a polite live region | admin.billing.wallets.show · admin.billing.wallets.transactions.store · admin.tickets.wallet-transactions.store · admin.billing.surcharges.update | read-only preview; writes stay desktop |

### 14 · Price group (design pricing) (`families/price-group/`)

Bucket: **F1 variant** · Covers: price-group primitive, price-group designer

The real Design Pricing page: “{retailer} - {main product} - Design Pricing”, rows grouped by design category, columns Name · Production Days · Wholesale (settlement currency) · Retail (consumer currency), one Submit. English only (super-admin).

| Export | Props | Variants | States | shadcn / Radix | Package underneath | Accessibility | API operations | Phone |
|---|---|---|---|---|---|---|---|---|
| `PriceGroup` | retailer · mainProduct · rows (category → options: production_days, wholesale, retail) · currencies {wholesale, retail} | — | clean · cell-focus · dirty (before → after) · saving · saved · rejected · loading · denied | Table + Input cells | react-hook-form field array · zod | grid with row headers; the currency badge names each column | admin.pricing.designs.index · admin.pricing.designs.store (not admin.pricing.master.*: retired, 403) | read-only list |

### 15 · Matrix / grid editor (`families/matrix-editor/`)

Bucket: **missing (new)** · Covers: M02

The spreadsheet surface for A12: fabric codes × combinations, frozen first column and header, group rows by collection, a cell cursor, dirty cells with the old value, paste from Excel / Sheets, per-cell save (PATCH) with saved / rejected states, 10,000-row windows.

| Export | Props | Variants | States | shadcn / Radix | Package underneath | Accessibility | API operations | Phone |
|---|---|---|---|---|---|---|---|---|
| `MatrixEditor` | rows (grouped) · columns · value · onCellSave · frozen 1 · window 10,000 | — | cursor · editing · dirty · pasted · saving · saved · rejected (422, text kept) · loading window · denied | none (grid) — Table skin | react-datasheet-grid (spike first; fallback TanStack Table 8 + react-virtual + own cell focus) | role=grid, arrow keys move the cursor, Enter edits, Esc cancels; paste announces the count | admin.pricing.retailer.index / .update · admin.factory-matrix.show / .update (Q11) · admin.shipping.bands.bulk · admin.fitq.config | read-only card per row |

### 16 · Uploader and import flow (`families/import-flow/`)

Bucket: **F1 variant** · Covers: file-uploader primitive, uploader designer, review-step designer

CSV / JSON / ZIP into the import flow: upload → map → preview (added / updated / unchanged / linked / errors, with the failing rows) → confirm → progress (polled) → result. Limits are stated up front; a failed run says exactly what was written.

| Export | Props | Variants | States | shadcn / Radix | Package underneath | Accessibility | API operations | Phone |
|---|---|---|---|---|---|---|---|---|
| `FileUploader` | accept · maxBytes · multiple · onUpload | — | idle · drag-over · invalid · uploading % · uploaded · error | none (drop zone) + Progress | react-dropzone | the drop zone is a button; errors are announced | — | — |
| `ImportFlow` | steps · preview (counts + row errors) · onConfirm · onCancel · job | — | upload · map · preview · confirming · progress · done · failed · cancelled | — | react-query polling | — | admin.fabric-codes.import → .import.preview.status → .confirm / .cancel · admin.pt-colors.import.preview · admin.colourway-families.import.preview / .confirm · admin.fitq.import · admin.media-library.archives.extract | not on phone (fallback page); the job shows in the phone tray |
| `ReviewStep` | counts {added, updated, unchanged, linked, errors} · errors[] {row, column, message} | — | clean · with errors · skip-and-continue | — | — | — | — | — |

### 17 · Stepper and step rail (`families/stepper/`)

Bucket: **F1 variant** · Covers: stepper primitive, stepper designer, step-rail primitive, step-rail designer

The six import steps with done / current / error / locked, and the step rail's second job: the long-form section nav for the 130 A03 pages (scroll-spy, an error count per section, jump to the first error).

| Export | Props | Variants | States | shadcn / Radix | Package underneath | Accessibility | API operations | Phone |
|---|---|---|---|---|---|---|---|---|
| `Stepper` | steps · current · errorAt · locked | — | done · current · error · locked | none | — | ol with aria-current=step | — | compact “Step 3 of 6” title + bar |
| `SectionNav` | sections {id, label, errors} · active | — | active · done · errors | — | IntersectionObserver (scroll-spy) | nav of in-page links; ⌥↓ jumps to the first error | — | — |

### 18 · Form section, save bar and unsaved guard (`families/form-section/`)

Bucket: **F1 variant** · Covers: form-section primitive, form-section designer, save-bar primitive, save-bar designer, unsaved-guard designer

Long sectioned forms: a numbered section with the 180px label column at compact (stacked on phone), the dark pending-changes bar (dirty · saving · saved · validation summary that jumps to fields · conflict “someone else changed this” with Compare / Reload / Keep mine), and the unsaved-changes guard on shell nav and tab close.

| Export | Props | Variants | States | shadcn / Radix | Package underneath | Accessibility | API operations | Phone |
|---|---|---|---|---|---|---|---|---|
| `FormSection` | id · n · title · errors · layout inline\|stack · repeatable | — | clean · error | none (fieldset) | react-hook-form | fieldset + legend; error count in the legend | — | — |
| `SaveBar` | dirtyFields · onSave · onDiscard · conflict {by, at, version} | — | hidden · dirty · saving · saved · error (summary) · conflict | — | react-hook-form formState · motion | status region; Save is ⌘S | any update; conflict from 409 version_conflict (If-Match / expected_version) e.g. admin.orders.measurements.update | full-width bar at the bottom |
| `UnsavedGuard` | when (isDirty) | — | closed · open | AlertDialog | beforeunload + Next router guard | focus lands on Stay | — | — |

### 19 · Bilingual en / 中文 field pair (four labels) (`families/bilingual-field/`)

Bucket: **missing (new)** · Covers: M07

The designs truth: every name is stored four times — System (English), System (Chinese), Printer (English), Printer (Chinese) — each required. Side by side on desktop, a “Chinese missing” flag (the factory bill would print English), tabs on phone.

| Export | Props | Variants | States | shadcn / Radix | Package underneath | Accessibility | API operations | Phone |
|---|---|---|---|---|---|---|---|---|
| `BilingualField` | value {en, cn} · printer {en, cn} (four-label mode) · required · suggest (optional op) | — | filled · focus · cn-missing · required-error · read-only | Label + Input ×2 (×4) · Tabs on phone | react-hook-form · zod | each input is labelled with its language; lang=zh-Hans on the cn input | admin.designs.update (name, printer_name) · admin.design-components.* · admin.suppliers.* · admin.product-quality-checks.* · admin.vision-checks.failure-reasons.translate-suggest | tabs, one language at a time |

### 20 · Rich-text editor (`families/rich-text/`)

Bucket: **missing (new)** · Covers: M16

Tiptap in the kit skin: full toolbar for articles and drip steps, minimal for ticket replies, image upload with progress, the link popover, and a failed upload that keeps the text.

| Export | Props | Variants | States | shadcn / Radix | Package underneath | Accessibility | API operations | Phone |
|---|---|---|---|---|---|---|---|---|
| `RichText` | value (HTML) · toolbar full\|minimal · onImageUpload · maxBytes | — | empty · focus · link · uploading · error · read-only | Popover (link) + Toggle (toolbar) | @tiptap/react + starter-kit + extension-link / image / placeholder (MIT only) | toolbar is role=toolbar with arrow-key focus; ⌘B / ⌘I / ⌘K | admin.articles.store / .update / .upload-image · admin.drip-sequences.steps.store | minimal toolbar |

### 21 · Cron / schedule builder (`families/cron-builder/`)

Bucket: **missing (new)** · Covers: M12

WeChat scheduled messages: presets plus the five cron fields, a human preview in Asia/Shanghai and the next three runs. The server validates; the client only previews.

| Export | Props | Variants | States | shadcn / Radix | Package underneath | Accessibility | API operations | Phone |
|---|---|---|---|---|---|---|---|---|
| `CronBuilder` | value (5-field) · timezone Asia/Shanghai · presets | — | preset · custom · invalid · paused · read-only | ToggleGroup + Input (mono) | cronstrue · cron-parser | each field labelled; the human preview is a live region | admin.wechat.messages.store / .update / .toggle · admin.schedule-monitor.index | read + pause only |

### 22 · Swatch picker (PT colours) (`families/swatch-picker/`)

Bucket: **F1 variant** · Covers: swatch-picker primitive

PT colour tiles from the one backend matrix (Q23): code search, the selected tile, retired colours hidden by default (struck when shown).

| Export | Props | Variants | States | shadcn / Radix | Package underneath | Accessibility | API operations | Phone |
|---|---|---|---|---|---|---|---|---|
| `SwatchPicker` | colours · value · excludeRetired · search | — | selected · retired · empty · loading · read-only | RadioGroup (tiles) | — | radiogroup; each tile names code + colour | admin.pt-colors.matrix (exclude_retired) · admin.pt-colors.index | 3-column grid |

### 23 · Measurement grid (`families/measurement-grid/`)

Bucket: **missing (new)** · Covers: M17

Pieces side by side in cm with the source label (Recalled / this order / Manual), gold where a value changed from the carried-over one (with the old value), and the Q22 version check: “someone else changed these” with Compare / Use theirs / Keep mine.

| Export | Props | Variants | States | shadcn / Radix | Package underneath | Accessibility | API operations | Phone |
|---|---|---|---|---|---|---|---|---|
| `MeasurementGrid` | pieces · rows (labels in the real upper / lower order) · carried · version · onSave | — | clean · changed (gold) · editing · rejected · conflict · loading · denied | Table + Input cells + Tooltip | TanStack Table 8 compact · react-hook-form · react-query (409) | row headers are measurement names; changed cells announce “was 45” | admin.orders.measurements.update (If-Match, expected_version → 409) · admin.orders.measurement-check | read only |

### 24 · Record header and action model (`families/record-header/`)

Bucket: **F1 variant** · Covers: record-header primitive, record-header designer

Order detail's action model (flagship D01): one dark next-step button by status (Assembly ▾ for Ready For Fulfilment …), a Cloud print ▾ split (Cloud: Order bill / Shirt code · Open PDF: Mark-Up / Order / Shirt Tag / QC Tag), Edit, Raise issue (holds live here), and ⋯ grouped by intent. 4–5 controls instead of 8–10; no Hold button. Front matter, ten tabs, a sticky compact header on scroll.

| Export | Props | Variants | States | shadcn / Radix | Package underneath | Accessibility | API operations | Phone |
|---|---|---|---|---|---|---|---|---|
| `RecordHeader` | title · status · badges · sub · facts[] · actions {next, split[], secondary[], overflow: {group, items[]}[]} · tabs · sticky | full · sticky compact | loading · ready · action-busy · denied (actions absent) · long title | DropdownMenu (split + ⋯) · Tabs · Tooltip | nuqs (active tab) | h1 is the record id; split button = two buttons (action + menu) with distinct labels; destructive items last | admin.orders.show · admin.orders.cloud-print · admin.orders.print-url → admin.signed.orders.print · admin.orders.void (draft / awaiting payment / RFF) | status + facts + scrollable tab chips + Cloud print / Raise issue in thumb reach |
| `SplitButton` | primary · items (grouped) | — | — | Button + DropdownMenu | — | aria-haspopup=menu on the chevron | — | — |

### 25 · Confirmation ladder (`families/confirm-ladder/`)

Bucket: **F1 variant** · Covers: alert-dialog primitive, confirm designer

Three rungs: plain confirm → consequence summary (count, amounts with currency, wallet before → after; 409 idempotent retry) → typed confirmation for the irreversible (revoke all sessions, Q14). Money and revoke stay desktop-first.

| Export | Props | Variants | States | shadcn / Radix | Package underneath | Accessibility | API operations | Phone |
|---|---|---|---|---|---|---|---|---|
| `ConfirmDialog` | level plain\|summary\|typed · title · consequences[] · money {currency, before, after} · phrase · idempotencyKey · onConfirm | — | open · mismatch · ready · busy · conflict (409) · error | AlertDialog | react-hook-form + zod (typed phrase) · crypto.randomUUID() (Idempotency-Key) | focus starts on Cancel; the confirm button names the action and amount; typed input is labelled with the phrase | admin.sessions.revoke-all (confirmation, expected_revision) · admin.orders.void (Idempotency-Key, 409) · admin.billing.wallets.transactions.store · admin.tickets.wallet-transactions.store · admin.api-keys.bulk-revoke | rung 1 only; 2 and 3 open on desktop |

### 26 · State vocabulary (`families/states/`)

Bucket: **F1 variant** · Covers: alert primitive, alert designer, error-state primitive, error-states designer, not-wired primitive

One vocabulary for every page (gate-checked like brand-005, PL-066): empty, filtered-empty, loading, not available to your role, the API error taxonomy (403 · 404 · 409 · 422 · 429 · 5xx with a request id), offline, stale, partial data, needs-backend. Hide what a role can't do; use the denied state only where hiding would confuse.

| Export | Props | Variants | States | shadcn / Radix | Package underneath | Accessibility | API operations | Phone |
|---|---|---|---|---|---|---|---|---|
| `EmptyState` | icon · title · body · action | first-use · filtered · zero | — | none | — | h2-level title | — | — |
| `ErrorState` | status 403\|404\|409\|422\|429\|5xx\|offline · requestId · retry | — | per status | — | react-error-boundary (per panel) | role=alert for blocking errors | — | — |
| `Banner` | kind info\|warn\|bad\|ok\|lock · title · body · actions · thin | stale · offline · partial · conflict · needs-backend | — | — | react-query onlineManager + dataUpdatedAt (stale / offline) | — | — | — |
| `NotAvailable` | permissions[] | — | page · inline · chip | — | — | names the missing permission; never a dead button | admin.me.show (permissions[]) | — |

### 27 · Timeline and audit rows (`families/timeline-audit/`)

Bucket: **F1 variant** · Covers: timeline primitive, timeline designer, audit-row designer

Scoped histories that exist (payload audits, fitting validation log, print jobs, WeChat sends), each row with staff attribution, the time in your zone, a failed + reason + fix row, and a before → after field diff when opened. A record-wide audit trail needs backend (P1) and says so.

| Export | Props | Variants | States | shadcn / Radix | Package underneath | Accessibility | API operations | Phone |
|---|---|---|---|---|---|---|---|---|
| `Timeline` | events[] {kind, title, by, at, diff?, failure?} · loadOlder | — | closed · open (diff) · failed + fix · loading · empty · denied | Collapsible | date-fns + @date-fns/tz · react-virtual for long histories | ordered list; time is  | admin.orders.payload-audits.index / .decoded · admin.orders.fitting-validation-logs.index · admin.wechat.messages.sends-history · admin.orders.print-jobs.index | a record tab, not a side panel |
| `AuditRow` | field · before · after · by · at | — | — | — | — | — | record-wide history = proposal P1 (needs backend) | — |

### 28 · Diff viewer (`families/diff-viewer/`)

Bucket: **missing (new)** · Covers: M05

Before → after at field level (conflict compare, bulk-hub preview) and line level (email template versions, with Revert). Side by side or inline; inline only on phone.

| Export | Props | Variants | States | shadcn / Radix | Package underneath | Accessibility | API operations | Phone |
|---|---|---|---|---|---|---|---|---|
| `DiffViewer` | mode fields\|lines · before · after · labels · layout side\|inline | — | diff · identical · loading · denied | Tabs (layout) | react-diff-viewer-continued (lines) · diff (fields) | removed / added are announced (“removed 104, added 106”), not colour only | admin.email-templates.versions.show / .revert · admin.designs.bulk-hub.preview · admin.pt-colors.import.preview · admin.accounts.bulk-permissions (dry_run) | inline only |

### 29 · Comment thread (tickets) (`families/comment-thread/`)

Bucket: **F1 variant** · Covers: comment-thread primitive, thread designer

The real ticket page: the Original Message (tickets.body) with its attachments, then the thread. No internal notes exist — every staff reply emails the retailer. The composer has Saved response, voice, Cite article and an explicit Get AI Suggestion button (never automatic; it costs a RAG query and an LLM call).

| Export | Props | Variants | States | shadcn / Radix | Package underneath | Accessibility | API operations | Phone |
|---|---|---|---|---|---|---|---|---|
| `Thread` | original {title, body, attachments, subProducts} · comments[] · onReply | — | loading · body-only · thread · sending · failed + retry · denied (read only) | none · Avatar | Tiptap minimal (composer) · react-dropzone | each message is an article with author and time | admin.tickets.show · admin.tickets.comments.store · admin.tickets.attachments.store / .download · admin.tickets.templates.show · admin.tickets.ai.voice-transcribe · admin.tickets.citations.search | thread full screen, composer docked |
| `AiSuggest` | ticketId | — | idle · searching · result (confidence ≥80 green, ≥60 amber, else red) · no-match (no LLM call) · error | — | — | result panel is a region; Use This Response replaces the composer | admin.tickets.ai.suggest (click only) | — |

### 30 · KPI tile and chart frame (`families/kpi-chart/`)

Bucket: **F1 variant** · Covers: kpi-tile primitive, kpi-tile designer, chart-frame primitive, chart-frame designer

Dashboard and report tiles with a sparkline, the stale and denied states, and a chart frame with a time axis in your zone, CN labels and legend. Categorical colours are the validated kit set; status colours never double as categories.

| Export | Props | Variants | States | shadcn / Radix | Package underneath | Accessibility | API operations | Phone |
|---|---|---|---|---|---|---|---|---|
| `KpiTile` | label · value · delta · spark · state | — | ready · loading · stale · error · denied | Card | recharts (spark) or inline SVG | value and delta read as one sentence | — | — |
| `ChartFrame` | title · range 7d\|30d · series · zone | — | ready · loading · empty · stale · denied | ToggleGroup (range) | recharts | svg role=img with a text summary; a table toggle for screen readers | admin.dashboard.index · admin.vision-checks.dashboard.index · admin.reports.forecast-accuracy · admin.api-request-logs.stats | tiles stack, chart full width |

### 31 · Prepare → open PDF and cloud print status (`families/document-print/`)

Bucket: **missing (new)** · Covers: M04

Printed documents stay server PDFs opened in a new tab through a short-lived signed link (T001 Q26): prepare → ready (expires) → open, and expired → prepare again. Cloud print to the Shanghai agent: queued → sent → printed, or failed + reprint.

| Export | Props | Variants | States | shadcn / Radix | Package underneath | Accessibility | API operations | Phone |
|---|---|---|---|---|---|---|---|---|
| `DocumentLink` | prepare (op returning {url, expires_at}) · label | — | idle · preparing · ready (countdown) · expired · error | Button + Badge | useMutation + window.open (no PDF library) | announces “PDF opened in a new tab” | admin.orders.print-url → admin.signed.orders.print · admin.orders.markup-pdf-url · admin.orders.bulk-print-url · admin.orders.print-jobs.pdf-url · admin.shipping.containers.ticket-url · admin.pricing.export-download-url | — |
| `PrintJobStatus` | job · onReprint | — | queued · sent · printed · failed + reprint | — | react-query polling | — | admin.orders.cloud-print · admin.orders.print-jobs.index / .reprint · admin.print-studio.jobs.status | same; reprint is a phone job |

### 32 · Gallery picker (media library) (`families/gallery-picker/`)

Bucket: **F1 variant** · Covers: gallery-picker primitive, picker designer

The server-paged media library as a picker: folder tree, search, upload in place (with per-file progress and type errors), multi-select with a running count, and the media-library grants (Q7) deciding who can upload.

| Export | Props | Variants | States | shadcn / Radix | Package underneath | Accessibility | API operations | Phone |
|---|---|---|---|---|---|---|---|---|
| `GalleryPicker` | folder · multiple · accept · onPick · upload | — | loading · ready · selected · uploading · upload-error · empty · error · browse-only | Dialog (picker) + ScrollArea + Checkbox | react-query (paged) · react-dropzone · react-virtual | grid of checkbox tiles; the count is announced | admin.media-library.list · admin.media-library.tree · admin.media-library.file | 2-column grid, upload from camera roll |

### 33 · Photo-review lightbox and verdict buttons (`families/photo-review/`)

Bucket: **missing (new)** · Covers: M10

Vision checks and QC photos: the photo on a dark stage with prev / next and zoom, the AI verdict and rule, the Pass / Fail buttons with P / F (the fixed version), a failure-reason picker, and a throttled re-run (Q3). Phone: one photo per screen, big buttons, swipe.

| Export | Props | Variants | States | shadcn / Radix | Package underneath | Accessibility | API operations | Phone |
|---|---|---|---|---|---|---|---|---|
| `PhotoReview` | items · index · onVerdict (pass\|fail, reason) · onRerun · reasons | — | loading · ready · pass · fail · reason · throttled · resolved · image-error · denied | Dialog (full) + ToggleGroup (verdict) + Command (reasons) | react-hotkeys-hook (P / F / ← → / Z) · react-zoom-pan-pinch · react-swipeable (phone) | verdict is a toggle group; keys are listed, not required | admin.vision-checks.results.index / .resolve / .rerun (throttled, Q3) · admin.orders.qc-photos · admin.orders.photos.index · admin.fabric-photos.batches.show | one photo per screen, big Pass / Fail, swipe |
| `VerdictButton` | kind pass\|fail · pressed · size md\|big · hotkey | — | default · focus · pressed · disabled | — | — | aria-pressed; the key hint is aria-hidden (the shortcut is in aria-keyshortcuts) | — | — |

### 34 · Gantt (`families/gantt/`)

Bucket: **missing (new)** · Covers: M03

Built in the kit on CSS grid (no library): order ID · customer name · factory (codes, no location — FEEDBACK-B-r1 A07), bars in the real phase colours, red due markers, a today line in steel, weekends shaded, hover detail. Fed by the computed gantt-grid.

| Export | Props | Variants | States | shadcn / Radix | Package underneath | Accessibility | API operations | Phone |
|---|---|---|---|---|---|---|---|---|
| `Gantt` | month · year · rows {orderId, customer, factories, segments[{phase, from, to}], due} · legend · onRow | — | loading · ready · hover · filtered · empty · denied | Tooltip + Sheet (drill) | in-kit CSS grid + @tanstack/react-virtual (fallback @svar-ui/react-gantt, MIT core) | each row is a button naming order, customer, phase and due date; the grid has a table alternative | admin.orders.gantt · admin.orders.gantt-grid (month, year) · admin.orders.gantt.status-legend · admin.orders.gantt-detail | due-date list |

### 35 · JSON / payload viewer (`families/json-viewer/`)

Bucket: **missing (new)** · Covers: M11

Failed jobs, payload audits and replays: pretty JSON with collapse, wrap and copy, UTC times annotated in your zone, the raw unredacted payload for those allowed (Q9c), and the show-once secret (API key) with reveal and copy.

| Export | Props | Variants | States | shadcn / Radix | Package underneath | Accessibility | API operations | Phone |
|---|---|---|---|---|---|---|---|---|
| `JsonViewer` | value · collapsedDepth · wrap · maskKeys · annotateTimes | — | expanded · collapsed · wrapped · masked · loading · error | Collapsible + Button (copy) | react-json-view-lite (CodeMirror only if search / edit is needed) | tree with aria-expanded per node; Copy announces | admin.failed-jobs.show · admin.orders.payload-audits.decoded · admin.replays.show · admin.api-request-logs.index · admin.api-keys.store (show once) | always wrapped |

### 36 · QR display (`families/qr-display/`)

Bucket: **missing (new)** · Covers: M13

An accessory code's durable QR link: the code rendered client-side from the short URL (this specimen's code is real and scans), the target, scan count, copy, download and re-point (printed tags keep working).

| Export | Props | Variants | States | shadcn / Radix | Package underneath | Accessibility | API operations | Phone |
|---|---|---|---|---|---|---|---|---|
| `QrDisplay` | shortUrl · shortCode · target · scans · onRepoint | — | default · repointing · none · loading · error · read-only | Card + Button | qrcode.react | the image has the URL as its accessible name | admin.accessory-qr.show · admin.accessory-qr.repoint | same, stacked |

## 4. As-is: 86 components inherited from brand-005 by name

No specimen in this kit. They change only through the tokens delta (density, ground, steel focus, CJK stack, icon stroke 1.5). Specimens: brand-005 `phase-2/kit/families/` and https://ladder.pina-brand-005-rj-dtc-admin.pages.dev/kit/. The hub shows them in a token-swap strip (Laravel | DTC).

| # | Kind | Component | Why it scales as-is (COMPONENT-INVENTORY) |
|---|---|---|---|
| 1 | primitive | `accordion` | Collapsing help and advanced sections; nothing F1-specific. |
| 2 | primitive | `avatar` | Staff initials in audit rows, threads and the shell. |
| 3 | primitive | `badge` | Counts and plain labels; new domains are data only. |
| 4 | primitive | `button` | All variants exist; the compact height comes from the **D** token. The split button lives in record-header. |
| 5 | primitive | `calendar` | The day grid itself is unchanged; zone handling sits in the `date` composite and `datetime` (M08). |
| 6 | primitive | `card-gallery` | The grid for A08 photo review (2–6 columns, selected state). The verdict sits in the lightbox (M10). |
| 7 | primitive | `card` | Plain container. |
| 8 | primitive | `checkbox` | Tri-state (`indeterminate`) already exists for the select-all header. |
| 9 | primitive | `cloth-card` | The row and tile used by the async cloth picker and the fabrics gallery. |
| 10 | primitive | `collapsible` | Expandable rows and sections reuse it. |
| 11 | primitive | `command` | The cmdk wrapper is unchanged; the F1 behaviour lives in the `command-palette` composite. |
| 12 | primitive | `dialog` | Forms in a modal; nothing F1-specific. |
| 13 | primitive | `drawer` | The phone bottom sheet. |
| 14 | primitive | `dropdown-menu` | Row menus and split-button menus; the `danger` item already exists. |
| 15 | primitive | `empty-state` | first-use and no-results; denied and error cases go to not-wired and error-state. |
| 16 | primitive | `field` | Label, hint and error layout; **CN** expansion is covered by the tokens. The bilingual pair is new (M07). |
| 17 | primitive | `input` | Text and number; tabular numbers come from the type tokens. |
| 18 | primitive | `kbd` | Keyboard hints for table navigation and the verdict shortcuts. |
| 19 | primitive | `key-value` | Record facts; `mono` rows exist. |
| 20 | primitive | `label` | — |
| 21 | primitive | `list-row` | Phone rows for A01 and A11. |
| 22 | primitive | `popover` | Filter value pickers and the column chooser. |
| 23 | primitive | `product-card` | Design and product picker tiles (`admin.designs.picker`). |
| 24 | primitive | `progress` | Determinate and indeterminate already exist; M01 and action-bar reuse it. |
| 25 | primitive | `radio-group` | — |
| 26 | primitive | `scroll-area` | — |
| 27 | primitive | `select` | Fifteen options or fewer; anything longer uses the combobox. |
| 28 | primitive | `separator` | It may carry the tailoring micro-motif through the tokens delta, which is still a token-only change. |
| 29 | primitive | `sheet` | Side sheets for quick edits and row preview. The drag-resizable split is new (M06). |
| 30 | primitive | `skeleton` | — |
| 31 | primitive | `sonner` | Toasts; a job finishing hands off to the tray (M01). |
| 32 | primitive | `spinner` | — |
| 33 | primitive | `status-chip` | Laravel domains (order status from `admin.orders.status-catalogue`, vision verdicts, ticket severity) are **data** added to the dictionary, not new anatomy. |
| 34 | primitive | `step-mark` | — |
| 35 | primitive | `switch` | — |
| 36 | primitive | `table` | The plain table already has `density` and `framed`; F1 behaviour lives in data-table. |
| 37 | primitive | `tabs` | Record tabs with counts. |
| 38 | primitive | `tag-input` | — |
| 39 | primitive | `textarea` | The counter already exists. |
| 40 | primitive | `thumb` | — |
| 41 | primitive | `toggle-group` | The segmented control (Grouped/Flat, density toggle). |
| 42 | primitive | `tooltip` | — |
| 43 | designer | `button` | Same as the primitive. |
| 44 | designer | `icon-button` | — |
| 45 | designer | `link-button` | — |
| 46 | designer | `status-chip` | New domains are data. |
| 47 | designer | `status-filter-chip` | Facet chips with counts; filter-bar carries the F1 work. |
| 48 | designer | `environment-badge` | The staging banner for `stg-admin.rjmtm.com`. |
| 49 | designer | `toast` | — |
| 50 | designer | `empty-state` | — |
| 51 | designer | `skeleton` | — |
| 52 | designer | `spinner-progress` | — |
| 53 | designer | `field` | — |
| 54 | designer | `input` | — |
| 55 | designer | `textarea` | — |
| 56 | designer | `select` | — |
| 57 | designer | `checkbox` | — |
| 58 | designer | `radio` | — |
| 59 | designer | `switch` | Instant-save toggles (`*/toggle-status`, `*/active`). |
| 60 | designer | `segmented` | — |
| 61 | designer | `tag-input` | — |
| 62 | designer | `sheet` | — |
| 63 | designer | `dialog` | — |
| 64 | designer | `dropdown-menu` | — |
| 65 | designer | `popover` | — |
| 66 | designer | `tooltip` | — |
| 67 | designer | `overlay-layer` | The z-index stack is unchanged. |
| 68 | designer | `settings-subnav` | The A09 sub-nav; its entries also come from the menu. |
| 69 | designer | `breadcrumbs` | — |
| 70 | designer | `tabs` | — |
| 71 | designer | `page-header` | Title, count and primary action; saved-view pills come from saved-views. |
| 72 | designer | `list-row` | — |
| 73 | designer | `product-card` | — |
| 74 | designer | `cloth-card` | — |
| 75 | designer | `description-list` | — |
| 76 | designer | `key-value` | — |
| 77 | designer | `step-header` | — |
| 78 | designer | `wizard-action-bar` | Back / Next / Cancel; the cancel import op is wired by the stepper variant. |
| 79 | designer | `step-validation` | — |
| 80 | designer | `step-transition` | A motion pattern; the timing may move in the tokens delta. |
| 81 | designer | `help-trigger` | — |
| 82 | designer | `help-drawer` | — |
| 83 | designer | `icon` | The Lucide stroke is an identity lever, set as a token in the delta. |
| 84 | designer | `avatar` | — |
| 85 | designer | `kbd` | — |
| 86 | designer | `thumb` | — |

**Not needed in F1 (9, DTC-only):** `confirm-unverified-currency` (primitive), `kanban` (primitive), `confirm-unverified-currency` (designer), `hover-card` (designer), `place-switcher` (designer), `payload-frame` (designer), `gallery` (designer), `kanban` (designer), `guided-editor` (designer).

## 5. Proposals carried by the kit (not built as if they exist)

- **P1 record-wide audit viewer** (needs backend: no generic audit op; `GET /admin/audit-logs` → 404). `Timeline` shows only scoped histories that exist.
- **P2 server-stored shared saved views** (needs backend: `/me/preferences` holds only `{locale, timezone}`). `SavedViews` ships on URL state + this browser.
- **Select-all-across-pages on orders** (proposal: orders have no bulk endpoint; `Complete` rides on `POST orders/completed` in batches of 100).
- **Version check on measurement saves** (T001 Q22 ruled; the mock PUT returns no version yet). `SaveBar` conflict and `MeasurementGrid` conflict are designed against the ruling.

## 6. Changing the kit

Edit `src/` and run `python3 src/build.py` (it re-asserts the 63 + 17 coverage against `phase-0/COMPONENT-INVENTORY.md`). Do not hand-edit `tokens.css` / `base.css`: section 1 and Part A are verbatim brand-005 v4, section 2 is verbatim phase-0 tokens-delta. A token the kit needs goes into `src/kit_tokens.css` and KIT-CHANGELOG.md.
