nuvox 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,544 @@
1
+ # Building Components on the Nuvox Foundation
2
+
3
+ This is the how-to that sits next to `CONTRACT.md`. CONTRACT.md is the
4
+ rulebook (why each rule exists). This document is the *procedure* —
5
+ what order to build things in, what file goes where, and what each
6
+ category of component specifically needs, so you're not re-deriving
7
+ the process for every one of the ~44 components you'll eventually
8
+ build.
9
+
10
+ Every component in the old Nuvox fell into one of six shapes. Each
11
+ shape has different requirements beyond the universal ones. Find your
12
+ component's category below, but read the **Universal build order**
13
+ first — it applies regardless of category.
14
+
15
+ ---
16
+
17
+ ## Universal build order (every component, no exceptions)
18
+
19
+ Build in this order. Don't skip ahead to the React file before the
20
+ core file exists — that ordering is itself part of the contract
21
+ (Rule: architecture before features, applied at the single-component
22
+ scale).
23
+
24
+ ```
25
+ 1. src/core/<name>.ts — pure logic, no JSX, no DOM
26
+ 2. src/react/<Name>.tsx — thin wrapper consuming the core logic
27
+ 3. src/react/<Name>.css — CSS variables only, no hardcoded values
28
+ 4. tests/<name>.test.tsx — written and RUN before you call it done
29
+ ```
30
+
31
+ ### 1. Core file (`src/core/<name>.ts`)
32
+ Pure functions only. No `import React`, no JSX, ever — this is what
33
+ makes the component portable to a future Vue/Angular adapter for free.
34
+
35
+ ```ts
36
+ import type { Size, Variant } from "./types";
37
+
38
+ export interface <Name>Props {
39
+ size?: Size;
40
+ variant?: Variant;
41
+ disabled?: boolean;
42
+ // ...component-specific props
43
+ }
44
+
45
+ export interface <Name>State {
46
+ size: Size;
47
+ variant: Variant;
48
+ isDisabled: boolean;
49
+ // ...resolved values
50
+ }
51
+
52
+ export function resolve<Name>State(props: <Name>Props): <Name>State {
53
+ return {
54
+ size: props.size ?? "md", // always ??, never || (Rule 3)
55
+ variant: props.variant ?? "solid",
56
+ isDisabled: props.disabled ?? false,
57
+ };
58
+ }
59
+
60
+ export function get<Name>Classes(state: <Name>State): string {
61
+ return ["nx-<name>", `nx-<name>--${state.size}`, `nx-<name>--${state.variant}`]
62
+ .filter(Boolean)
63
+ .join(" ");
64
+ }
65
+ ```
66
+
67
+ ### 2. React file (`src/react/<Name>.tsx`)
68
+ ```tsx
69
+ import { forwardRef } from "react"; // Rule 4
70
+ import { resolve<Name>State, get<Name>Classes } from "../core/<name>";
71
+ import type { BaseComponentProps } from "../core/types"; // Rule 2
72
+ import { withThemeOverride, withAccentOverride } from "../theme-engine/NuvoxProvider"; // Rule 5 / 5b
73
+
74
+ interface <Name>Props extends BaseComponentProps, /* core props */ {}
75
+
76
+ export const <Name> = forwardRef<HTMLElement, <Name>Props>(function <Name>(
77
+ { className, style, theme, color, ...rest }, ref
78
+ ) {
79
+ const state = resolve<Name>State(rest);
80
+ const classes = [get<Name>Classes(state), className].filter(Boolean).join(" ");
81
+ const el = <div ref={ref} className={classes} style={style}>{/* ... */}</div>;
82
+ // theme = shape, color = accent — independent, apply both (Rule 5b).
83
+ return withThemeOverride(theme, withAccentOverride(color, el));
84
+ });
85
+ ```
86
+
87
+ ### 3. CSS file (`src/react/<Name>.css`)
88
+ Every value is a `var(--x, fallback)`. Never a literal color, radius,
89
+ shadow, or duration.
90
+ ```css
91
+ .nx-<name> {
92
+ border-radius: var(--radius-md);
93
+ padding: var(--spacing-sm) var(--spacing-md);
94
+ background: var(--color-surface);
95
+ transition: all var(--motion-durationFast) var(--motion-easeStandard);
96
+ }
97
+ ```
98
+
99
+ **Import it in the `.tsx` file** — `import "./<Name>.css";` near the
100
+ top of `<Name>.tsx`. This step is easy to silently skip: the class
101
+ names in `get<Name>Classes()` will apply to the DOM either way with
102
+ no error from TypeScript, ESLint, or the test suite, so a component
103
+ can look completely correct in code review while rendering as an
104
+ unstyled, un-themed native element. This exact bug happened to
105
+ Button in this project's own foundation — caught only by looking at
106
+ the docs site's live `<Preview>` and noticing every variant/theme
107
+ rendered identically. Add a `<Preview>` example to the component's
108
+ `.mdx` page and actually look at it (`npm run docs:dev`) before
109
+ considering a component done — the automated tests don't currently
110
+ catch a missing CSS import, since none of them assert on computed
111
+ styles.
112
+
113
+ ### 4. Tests (`tests/<name>.test.tsx`)
114
+ Minimum four, per CONTRACT.md Rule 8 — more if the component is
115
+ complex:
116
+ ```tsx
117
+ it("renders with default props", () => { /* ... */ });
118
+ it("scopes a theme/color override to just this component", () => { /* ... */ });
119
+ it("forwards its ref", () => { /* ... */ }); // if applicable
120
+ it("propagates theme correctly through its portal", () => { /* ... */ }); // if applicable
121
+ ```
122
+
123
+ Run `npm test` and `npm run typecheck` before moving to the next
124
+ component. Not after five components — after each one.
125
+
126
+ ---
127
+
128
+ ## Category A — Static / display primitives
129
+ **Examples:** Badge, Avatar, Divider, Skeleton, Spinner, Progress,
130
+ Card, Alert, Tag
131
+
132
+ **What makes this category different:** no interactivity, often no
133
+ portal, sometimes no ref target at all (Divider).
134
+
135
+ **What to keep:**
136
+ - Core file still exists even for something this simple — resist the
137
+ urge to inline the logic directly in the `.tsx` file. It costs
138
+ nothing today and is what makes a future Vue port of Card free.
139
+ - `color` is no longer category-specific — it's on `BaseComponentProps`
140
+ for every component now (the accent override, see CONTRACT.md Rule
141
+ 5b), so every component gets it whether it's in this category or
142
+ not. What IS still a legitimate category-A-specific choice is
143
+ `radius` as an extra override prop (this was the old codebase's
144
+ pattern, on 2/41 components, both in this category) — display
145
+ primitives are the case where overriding a single shape property
146
+ without a full theme override makes sense. Keep it consistent: if
147
+ `Card` gets a `radius` prop, `Skeleton` should offer it the same
148
+ way, not a differently-shaped prop.
149
+ - **Ref exemption check (Rule 4):** Divider is a legitimate exemption
150
+ (no focusable/interactive element). Badge, Avatar, Card are NOT —
151
+ they render a real DOM node and should forward.
152
+ - No portal, no overlay tests needed.
153
+
154
+ **Test minimum:** default render + theme/color override + ref forwarding
155
+ (skip ref only for genuine layout-only elements like Divider).
156
+
157
+ ---
158
+
159
+ ## Category B — Form / input primitives
160
+ **Examples:** Input, Textarea, Checkbox, Radio, Toggle, NumberInput,
161
+ Slider, OTPInput, PhoneInput, FileUpload
162
+
163
+ **What makes this category different:** controlled/uncontrolled state,
164
+ a value the consumer cares about, validation/error states, and — this
165
+ is the category form libraries actually need refs for.
166
+
167
+ **What to keep:**
168
+ - **Ref forwarding is non-negotiable here** — this is exactly the
169
+ category (Input, Checkbox, etc.) where the old codebase's 1-of-41
170
+ problem actually costs real integration pain (react-hook-form,
171
+ Formik, imperative `.focus()` on validation error).
172
+ - Support both controlled (`value` + `onChange`) and uncontrolled
173
+ (`defaultValue`) the same way React's native `<input>` does — don't
174
+ invent a third pattern.
175
+ - Standard props every component in this category should share:
176
+ `value`/`defaultValue`, `onChange`, `disabled`, `error` (or
177
+ `invalid`), `placeholder` where applicable. Decide this shape once,
178
+ here, rather than letting Input and NumberInput drift (the old
179
+ color/radius drift, one category up).
180
+ - `aria-invalid` and `aria-describedby` (for an error message) are
181
+ part of this category's baseline, the same way Rule 11 sets a
182
+ baseline for overlays — worth stating explicitly even though
183
+ CONTRACT.md's Rule 11 is scoped to overlays specifically.
184
+
185
+ **Test minimum:** default render + ref forwarding (mandatory) +
186
+ theme/color override + a controlled-value test (typing/changing updates
187
+ what the consumer expects) + an error/invalid-state test.
188
+
189
+ ---
190
+
191
+ ## Category C — Overlay / portal components (non-composite)
192
+ **Examples:** Modal, Drawer, Tooltip, Toast, ContextMenu, Tour
193
+
194
+ **What makes this category different:** this is exactly the category
195
+ that broke in the old codebase (3 of 15 portal components missing the
196
+ theme fix). Treat every rule here as load-bearing, not optional.
197
+
198
+ **What to keep:**
199
+ - **Always render through `ThemedPortal`, never `createPortal`
200
+ directly (Rule 6).** No exceptions, no "this one's simple enough to
201
+ skip it" — that reasoning is exactly how Toast and Tooltip broke
202
+ last time.
203
+ - **Always implement the Rule 11 accessibility baseline**: Escape to
204
+ close, focus trap, return focus to trigger, correct ARIA role
205
+ (`dialog` for Modal, `alert`/`status` for Toast, etc.). Build this
206
+ once as a shared `useOverlayBehavior()` hook and have every
207
+ component in this category use it — don't hand-roll focus trapping
208
+ five separate times the way the portal-theme fix got hand-rolled
209
+ twelve times.
210
+ - Toast is a special case within this category: it's a *queue*, not a
211
+ single open/closed instance. Its state (the list of active toasts)
212
+ lives outside any individual themed subtree by nature — make sure
213
+ its `ThemedPortal` anchor is something stable (e.g. the toast
214
+ trigger call site, or fall back to whatever element is currently
215
+ focused) rather than assuming one obvious anchor the way Popover
216
+ can.
217
+ - `Tooltip` needs hover AND focus triggers (not just `onMouseEnter`) —
218
+ a keyboard user tabbing to a button needs to see its tooltip too.
219
+
220
+ **Test minimum:** default render + `ThemedPortal` theme propagation
221
+ (mandatory) + Escape-closes + focus-returns-to-trigger + ARIA role
222
+ present. This category needs the full Rule 8 floor plus the
223
+ accessibility-specific tests — don't stop at four.
224
+
225
+ ---
226
+
227
+ ## Category D — Composite overlay + input components
228
+ **Examples:** Select, Combobox, DatePicker, ColorPicker, Command
229
+
230
+ **What makes this category different:** this is exactly what Rule 12
231
+ (composition) is for. This category is where the old codebase's
232
+ "components know too much" problem would show up hardest if repeated.
233
+
234
+ **What to keep:**
235
+ - **Compose, don't reimplement (Rule 12).** A `Select` is: an `Input`
236
+ or trigger button (Category B/A) + a `ThemedPortal`-based panel
237
+ (Category C's pattern) + a shared keyboard-navigable list primitive.
238
+ Build that list primitive (`ListBox` — arrow-key navigation, typeahead,
239
+ active-descendant ARIA wiring) **once**, and have Select, Combobox,
240
+ and Command all use it instead of each implementing arrow-key
241
+ handling separately.
242
+ - `DatePicker` composes: an `Input` (for the text value) + `Popover`
243
+ (for the calendar panel) + a calendar-grid primitive. The calendar
244
+ grid itself is a good candidate for a shared internal primitive if
245
+ you ever add a date-range picker later — don't decide that now, just
246
+ don't make the calendar grid *only* reachable from inside DatePicker
247
+ in a way that'd make reuse painful.
248
+ - These inherit Category C's overlay requirements in full (ThemedPortal,
249
+ `useOverlayBehavior`) *and* Category B's input requirements in full
250
+ (ref forwarding on the visible input/trigger, controlled/uncontrolled
251
+ value). Don't drop either set of requirements because the component
252
+ is "mostly a Select, not really an Input."
253
+
254
+ **Test minimum:** everything from both Category B and Category C's
255
+ minimums, plus one test proving keyboard navigation through the list
256
+ (arrow down moves the active option, Enter selects it).
257
+
258
+ ---
259
+
260
+ ## Category E — Navigation / structural components
261
+ **Examples:** Tabs, Accordion, Breadcrumb, Pagination
262
+
263
+ **What makes this category different:** state is about *which item is
264
+ active*, not a portal, not a value being typed. Usually no portal at
265
+ all.
266
+
267
+ **What to keep:**
268
+ - Roving `tabIndex` pattern for keyboard nav within the group (Tabs,
269
+ Accordion) — only the active tab is in the natural tab order; arrow
270
+ keys move between siblings. This is the same class of "accessibility
271
+ baseline" as Rule 11, just for a different interaction shape — worth
272
+ treating with the same seriousness even though it's not a portal
273
+ component.
274
+ - `aria-selected`/`aria-expanded` and matching `role="tablist"` /
275
+ `role="tab"` / `role="tabpanel"` etc. — get the ARIA pattern right
276
+ once (Tabs) and Accordion's is nearly identical, so build the second
277
+ one by extending the first's approach, not from scratch.
278
+ - Ref forwarding still applies to the interactive elements (each tab
279
+ button, each pagination button) even though the outer container
280
+ might not need one.
281
+
282
+ **Test minimum:** default render + ref forwarding on interactive
283
+ children + keyboard navigation between items + ARIA state updates on
284
+ selection change.
285
+
286
+ ---
287
+
288
+ ## Category F — Complex / stateful components
289
+ **Examples:** Table, Treeview, Kanban, Carousel, Timeline, ChatBubble
290
+
291
+ **What makes this category different:** these are the components most
292
+ likely to become monoliths again if you're not deliberate, because
293
+ each one genuinely has a lot of internal state (Kanban was 1,372 lines
294
+ in the old codebase — by far the largest).
295
+
296
+ **What to keep:**
297
+ - **Decompose on paper before writing code (this is Phase 1/Phase 2
298
+ from your own original retro, applied per-component now instead of
299
+ project-wide).** Before writing `Kanban.tsx`, write down its
300
+ sub-pieces: `KanbanBoard`, `KanbanColumn`, `KanbanCard`,
301
+ a drag-context hook. Build and test the sub-pieces individually
302
+ where possible, the same way `Select` composes `Input` + `Popover` +
303
+ `ListBox` in Category D.
304
+ - If a sub-piece needs its own portal (a Kanban card being dragged
305
+ needs to render above everything, which is effectively an overlay),
306
+ it follows Category C's rules too — this is likely where "Kanban
307
+ missing the theme fix" happened last time, because the drag layer
308
+ wasn't recognized as "a portal component" the way Modal obviously is.
309
+ - These components justify the most tests, per Rule 8's "floor, not
310
+ ceiling" — expect 15–30 meaningful tests for something like Kanban,
311
+ covering drag-and-drop state transitions, keyboard-accessible
312
+ reordering (don't ship drag-and-drop as the *only* way to reorder —
313
+ that's an accessibility gap, not just a nice-to-have), and the
314
+ virtualized-rendering edge cases if the list can get long.
315
+ - Table specifically: sorting/filtering state should be lifted to
316
+ props the consumer can control (controlled sort state), not trapped
317
+ entirely inside the component, the same way Input supports
318
+ controlled `value`.
319
+
320
+ **Test minimum:** default render + ref forwarding on the outer
321
+ container + theme/color override + portal theming for any internal overlay
322
+ (drag layer, cell editor, etc.) + a dedicated test per major state
323
+ transition specific to the component (drag-drop for Kanban, sort-order
324
+ for Table, expand/collapse for Treeview).
325
+
326
+ ---
327
+
328
+ ## Quick-reference: which categories need what
329
+
330
+ | Requirement | A: Static | B: Form | C: Overlay | D: Composite | E: Nav | F: Complex |
331
+ |---|:---:|:---:|:---:|:---:|:---:|:---:|
332
+ | Core/React split | Yes | Yes | Yes | Yes | Yes | Yes |
333
+ | Ref forwarding | Usually | **Always** | Yes | Yes | Yes (children) | Yes |
334
+ | `ThemedPortal` | No | No | **Always** | **Always** | No | Sometimes (drag layer, cell editor) |
335
+ | `useOverlayBehavior` (Rule 11) | No | No | **Always** | **Always** | No | If it has an overlay |
336
+ | Composition over reimplementation (Rule 12) | N/A | N/A | N/A | **Core requirement** | Some | **Core requirement** |
337
+ | Controlled/uncontrolled value | No | **Always** | Yes (selection) | Yes | Sometimes (active tab) | Yes |
338
+ | Test minimum (beyond Rule 8's 4) | none extra | +controlled-value, +error-state | +Escape, +focus-return, +ARIA role | Category B + C combined | +keyboard-nav, +ARIA state | +per-state-transition |
339
+
340
+ ---
341
+
342
+ ## Component build checklist (copy this per component)
343
+
344
+ ```
345
+ [ ] src/core/<name>.ts written — pure logic, ?? not ||, canonical Size/Variant types
346
+ [ ] src/react/<Name>.tsx written — forwardRef (unless genuinely exempt),
347
+ BaseComponentProps extended, BOTH withThemeOverride AND withAccentOverride
348
+ wired (accepting theme/color without applying them is worse than not
349
+ accepting them at all)
350
+ [ ] src/react/<Name>.css written — every value is var(--x, fallback)
351
+ [ ] If it portals: uses ThemedPortal, not createPortal
352
+ [ ] If it's an overlay: Escape/focus-trap/ARIA via useOverlayBehavior
353
+ [ ] If it composes other components: uses the real component, not a reimplementation
354
+ [ ] tests/<name>.test.tsx — Rule 8 floor + category-specific tests, written and RUN
355
+ [ ] npm run typecheck — clean
356
+ [ ] npm test — full suite still green, not just the new file
357
+ [ ] docs-site/content/docs/components/<name>.mdx — import interactive
358
+ components from @/components/nuvox-client (not nuvox directly),
359
+ use <Preview> for a real live-rendered example, added to that
360
+ folder's meta.json, verified via npm run docs:build + grepping the
361
+ prerendered HTML for the component's actual class names (a
362
+ successful build alone doesn't prove the live preview rendered —
363
+ see NUVOX-COMPLETE-REFERENCE.md Part 12)
364
+ ```
365
+
366
+ ---
367
+
368
+ ## Full component list — what each one needs
369
+
370
+ All 42 components from the original Nuvox, categorized, with the
371
+ requirements from the quick-reference table above applied per
372
+ component instead of per category. Use this as the actual build
373
+ checklist source — pick a row, build it, check the boxes.
374
+
375
+ **Legend:** ✅ required &nbsp;·&nbsp; ❌ not required &nbsp;·&nbsp;
376
+ ➖ not applicable &nbsp;·&nbsp; 🟡 conditional / partial &nbsp;·&nbsp;
377
+ 🛠️ already built &nbsp;·&nbsp; ⬜ not built yet
378
+
379
+ **`theme` and `color` overrides aren't columns here** — unlike the
380
+ other requirements, they're universal via `BaseComponentProps` (Rule
381
+ 2/5/5b), so every single component below needs both wired, not just
382
+ some categories. Missing columns for them isn't an oversight; a
383
+ column that's ✅ on all 42 rows wouldn't tell you anything a table is
384
+ good at telling you.
385
+
386
+ | # | Component | Category | Ref forward | ThemedPortal | Overlay a11y (Rule 11) | Composes (Rule 12) | Controlled value | Status |
387
+ |---|---|:---:|:---:|:---:|:---:|:---:|:---:|:---:|
388
+ | 1 | Button | B | ✅ | ❌ | ❌ | ➖ | ❌ | 🛠️ Built |
389
+ | 2 | Popover | C | ✅ | ✅ | ✅ | ➖ | ❌ | 🛠️ Built |
390
+ | 3 | Avatar | A | ✅ | ❌ | ❌ | ➖ | ❌ | 🛠️ Built |
391
+ | 4 | Badge | A | ✅ | ❌ | ❌ | ➖ | ❌ | 🛠️ Built |
392
+ | 5 | Card | A | ✅ | ❌ | ❌ | ➖ | ❌ | 🛠️ Built |
393
+ | 6 | Divider | A | ✅ *(exempt per Rule 4, forwarded anyway)* | ❌ | ❌ | ➖ | ❌ | 🛠️ Built |
394
+ | 7 | Progress | A | ✅ | ❌ | ❌ | ➖ | ❌ | 🛠️ Built |
395
+ | 8 | Skeleton | A | ✅ | ❌ | ❌ | ➖ | ❌ | 🛠️ Built |
396
+ | 9 | Spinner | A | ✅ | ❌ | ❌ | ➖ | ❌ | 🛠️ Built |
397
+ | 10 | Alert | A | ✅ | ❌ | ❌ | ➖ | ❌ | 🛠️ Built |
398
+ | 11 | ChatBubble | A | ✅ | ❌ | ❌ | ➖ | ❌ | ⬜ |
399
+ | 12 | Timeline | A | ✅ | ❌ | ❌ | ➖ | ❌ | 🛠️ Built |
400
+ | 13 | Input | B | ✅ | ❌ | ❌ | ➖ | ✅ | 🛠️ Built |
401
+ | 14 | Textarea | B | ✅ | ❌ | ❌ | ➖ | ✅ | 🛠️ Built |
402
+ | 15 | Checkbox | B | ✅ | ❌ | ❌ | ➖ | ✅ | 🛠️ Built |
403
+ | 16 | Radio | B | ✅ | ❌ | ❌ | ➖ | ✅ | 🛠️ Built |
404
+ | 17 | Toggle | B | ✅ | ❌ | ❌ | ➖ | ✅ | 🛠️ Built |
405
+ | 18 | NumberInput | B | ✅ | ❌ | ❌ | ➖ | ✅ | 🛠️ Built |
406
+ | 19 | Slider | B | ✅ | ❌ | ❌ | ➖ | ✅ | 🛠️ Built |
407
+ | 20 | OTPInput | B | ✅ | ❌ | ❌ | ➖ | ✅ | 🛠️ Built |
408
+ | 21 | PhoneInput | B | ✅ | ❌ | ❌ | ➖ | ✅ | 🛠️ Built |
409
+ | 22 | FileUpload | B | ✅ | ❌ | ❌ | 🟡 *(internal trigger button)* | ✅ | 🛠️ Built |
410
+ | 23 | Modal | C | ✅ | ✅ | ✅ | ➖ | ❌ | 🛠️ Built |
411
+ | 24 | Drawer | C | ✅ | ✅ | ✅ | ➖ | ❌ | 🛠️ Built |
412
+ | 25 | Tooltip | C | ✅ | ✅ | ✅ | ➖ | ❌ | 🛠️ Built |
413
+ | 26 | Toast | C | 🟡 *(queue, not a single instance)* | ✅ | ✅ | ➖ | ❌ | 🛠️ Built |
414
+ | 27 | ContextMenu | C | ✅ | ✅ | ✅ | ➖ | ❌ | 🛠️ Built |
415
+ | 28 | Tour | C | ✅ | ✅ | ✅ | ➖ | ❌ | ⬜ |
416
+ | 29 | Dropdown | C | ✅ | ✅ | ✅ | 🟡 *(reuses `core/collection` from Category D)* | ❌ | 🛠️ Built *(as `DropdownMenu`)* |
417
+ | 30 | Select | D | ✅ | ✅ | ✅ | ✅ | ✅ | 🛠️ Built |
418
+ | 31 | Combobox | D | ✅ | ✅ | ✅ | ✅ | ✅ | 🛠️ Built |
419
+ | 32 | DatePicker | D | ✅ | ✅ | ✅ | ✅ | ✅ | 🛠️ Built *(+ standalone `Calendar`, `core/calendar.ts`)* |
420
+ | 33 | ColorPicker | D | ✅ | ✅ | ✅ | ✅ | ✅ | 🛠️ Built |
421
+ | 34 | Command | D | ✅ | ✅ | ✅ | ✅ | ✅ | 🛠️ Built *(as `CommandPalette`)* |
422
+ | 35 | Accordion | E | ✅ *(items)* | ❌ | ❌ *(own roving-tabindex baseline instead)* | ➖ | ✅ *(expanded index)* | 🛠️ Built |
423
+ | 36 | Tabs | E | ✅ *(tabs)* | ❌ | ❌ *(own roving-tabindex baseline instead)* | ➖ | ✅ *(active tab)* | 🛠️ Built |
424
+ | 37 | Breadcrumb | E | ✅ *(links)* | ❌ | ❌ | ➖ | ❌ | 🛠️ Built |
425
+ | 38 | Pagination | E | ✅ *(buttons)* | ❌ | ❌ | ➖ | ✅ *(current page)* | 🛠️ Built |
426
+ | 39 | Table | F | ✅ | 🟡 *(only if cell editor overlays)* | 🟡 *(only if it has an overlay)* | ✅ | ✅ *(sort/filter state)* | 🛠️ Built |
427
+ | 40 | Treeview | F | ✅ | ❌ | ❌ | ✅ | ✅ *(expanded/selected)* | ⬜ |
428
+ | 41 | Kanban | F | ✅ | 🟡 *(drag layer)* | 🟡 *(drag layer)* | ✅ | ✅ *(board state)* | ⬜ |
429
+ | 42 | Carousel | F | ✅ | ❌ | ❌ *(own baseline: arrow keys, pause-on-hover, not Rule 11)* | ✅ | ✅ *(active slide)* | 🛠️ Built |
430
+ | 43 | Rating | B | ✅ | ❌ | ❌ | ➖ | ✅ *(selected value)* | 🛠️ Built *(added outside the original plan — a single-value control, same shape as Slider)* |
431
+ | 44 | StatCard | A | ✅ | ❌ | ❌ | ➖ | ❌ | 🛠️ Built *(added outside the original plan — composes `Card` for chrome)* |
432
+
433
+ A few things worth noting from the table itself, not just the rows:
434
+
435
+ - **Category C has 8 components and all 8 need `ThemedPortal` +
436
+ the Rule 11 baseline with no exceptions** — this is the exact
437
+ category that produced the 3-missing-out-of-15 bug last time.
438
+ If you ever mark a ❌ in either of those columns for a Category C
439
+ component, stop and double check it actually belongs in Category A
440
+ or E instead.
441
+ - **Toast and Dropdown are the two 🟡 rows in Category C** — both
442
+ for real structural reasons (Toast is a queue; Dropdown reuses
443
+ `core/collection.ts`, Category D's list primitive), not because the
444
+ rule is optional for them. Don't let "it's a bit different" quietly
445
+ become "it's exempt."
446
+ - **Table, Kanban's conditional portal/a11y columns are the most likely
447
+ place to repeat the old bug** — a drag layer or cell editor is an
448
+ overlay in every way that matters, even though the component's name
449
+ doesn't obviously say "overlay" the way Modal's does. Flag it
450
+ explicitly in that component's own file comments once you decide
451
+ whether it applies.
452
+
453
+
454
+ Build roughly in this category order, not alphabetically and not by
455
+ whichever component sounds fun that day:
456
+
457
+ 1. **Button is settled; Categories A, B, C, D, and E are all proven
458
+ out now.** Input, Textarea, Checkbox, Radio, Toggle, and Slider
459
+ (Category B) share `core/field.ts`'s disabled/required/invalid/
460
+ size shape. Badge, Avatar, Spinner, Divider, Card, Alert, and
461
+ Progress (Category A) consistently derive booleans like
462
+ "dismissible" or "clickable" from whether a handler was actually
463
+ passed (`onDismiss`, `onClick`) rather than adding a redundant flag
464
+ prop. Modal, Tooltip, Toast, Dropdown, Popover, and CommandPalette
465
+ (Category C) share `useOverlayBehavior`
466
+ (Escape/focus-trap/return-focus) and `core/floating.ts` (anchor
467
+ positioning). Select, Combobox, and DatePicker (Category D) all
468
+ compose across categories rather than picking one — Select and
469
+ Combobox both reuse Input's field shape AND Category C's overlay
470
+ stack; DatePicker goes one step further and composes Popover
471
+ itself (not just Popover's underlying hooks) plus a standalone
472
+ Calendar component. Tabs and Accordion (Category E) share
473
+ `useRovingFocus`. `core/collection.ts` (arrow-key nav + typeahead)
474
+ is the one primitive proven across three different categories at
475
+ once — Dropdown (C), Select and Combobox (D), and CommandPalette
476
+ (D) all call the exact same functions.
477
+ 2. **Category A is down to one: ChatBubble.** Timeline is done — same
478
+ proven conventions, no portal/overlay complexity. Skeleton is done;
479
+ it also settled a real-world edge case worth carrying forward: a
480
+ full-width block built from divs (not a native form element) needs
481
+ its own `inline-size: 100%` — nothing in the platform gives it one
482
+ for free the way `<input>` gets a UA-stylesheet default width. Any
483
+ future component in this shape should declare it explicitly rather
484
+ than rely on an ancestor happening to be definite-width.
485
+ 3. **Category B is fully built.** OTPInput and PhoneInput both landed
486
+ — the ref-forwarding and controlled-value conventions were already
487
+ established by Input/Checkbox/Slider/NumberInput/etc. FileUpload is
488
+ the reference for Rule 12 inside Category B specifically: its
489
+ "Browse" trigger composes the real `Button` rather than hand-styling
490
+ a second one, exactly what the `🟡 (internal trigger button)` note
491
+ in the table called for.
492
+ 4. **Category D is fully built.** ColorPicker landed following the
493
+ same "compose Input's field shape + Popover + a new pure-logic
494
+ grid/wheel module" shape DatePicker established — Select, Combobox,
495
+ and DatePicker had already proven Rule 12 held here.
496
+ 5. **The rest of Category C** (Tour) — Drawer and ContextMenu are
497
+ done and confirm the prediction: both really were mostly a new
498
+ visual shape on `useOverlayBehavior`/`core/floating.ts`'s existing,
499
+ tested infrastructure. Drawer is a close copy of Modal.tsx with one
500
+ axis added (which edge, and "size" meaning width vs. height
501
+ depending on it). ContextMenu is DropdownMenu's sibling — same
502
+ portal/keyboard-nav machinery, reusing DropdownMenu's own item
503
+ classes and pure logic (Rule 12) rather than duplicating them — with
504
+ the one real new piece being a "virtual anchor" (a zero-size element
505
+ moved to the click point) that lets `useFloatingPosition` position
506
+ relative to a cursor location it was never written to expect,
507
+ completely unmodified. Tour is the one left, and will need actual
508
+ new choreography (sequencing highlighted-element steps) that neither
509
+ Drawer nor ContextMenu needed.
510
+ 6. **The rest of Category E** — Breadcrumb and Pagination are done,
511
+ and turned out *not* to need `useRovingFocus`: that hook is for a
512
+ single widget where only one item belongs in the tab order at a
513
+ time (Tabs, Accordion). A breadcrumb trail and a pager are both
514
+ just several independently-useful links/buttons in normal tab
515
+ order, closer to a browser's own navigation than to a listbox —
516
+ worth checking which model actually fits before reaching for
517
+ `useRovingFocus` on a future Category E component.
518
+ 7. **Category F** — Table and Carousel are both done. Table is the
519
+ reference for "compose more than any other category": seven small
520
+ pieces on paper first (Table, TableHeader, TableBody, TableRow,
521
+ TableHead, TableCell, TableCaption) rather than one component
522
+ holding sorting, selection, and layout all at once. Sort state is
523
+ controlled by the consumer, the same shape as every value-holding
524
+ component in this library; row selection isn't modeled by Table at
525
+ all — a "select all"/row checkbox is just a real `Checkbox` (Rule
526
+ 12) placed in a cell. Carousel followed the same decompose-first
527
+ approach (root, viewport, track, slide, prev/next, indicators) and
528
+ owns its own keyboard/pause-on-hover baseline rather than reusing
529
+ `useOverlayBehavior`, since it isn't an overlay. Treeview, Kanban,
530
+ and ChatBubble remain — each will need its own genuinely new
531
+ interaction model the way Table's sort/selection split did, rather
532
+ than being a rearrangement of an existing component's pieces.
533
+ 8. **Two components shipped outside this table's original plan:**
534
+ Rating (Category B — a single-value control, same conventions as
535
+ Slider) and StatCard (Category A — composes `Card` for its chrome).
536
+ Both follow every rule in `CONTRACT.md` the same as anything on
537
+ the original list; they're just proof this table is a starting
538
+ roadmap, not a hard ceiling on what Nuvox can become.
539
+
540
+ If building a Category D or F component forces a change to the
541
+ foundation (`ThemedPortal`, `useOverlayBehavior`, `core/types.ts`),
542
+ that's a signal the foundation still has a gap — fix it there, not
543
+ with a one-off workaround in the component. That's the whole point of
544
+ having gone through this exercise once already.