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.
- package/BUILDING-COMPONENTS.md +544 -0
- package/CONTRACT.md +255 -0
- package/LICENSE +28 -0
- package/README.md +99 -0
- package/THIRD-PARTY-NOTICES.md +52 -0
- package/dist/chunk-3F7OVF4J.js +1126 -0
- package/dist/chunk-3F7OVF4J.js.map +1 -0
- package/dist/context-menu-CBdFZq5L.d.cts +627 -0
- package/dist/context-menu-CBdFZq5L.d.ts +627 -0
- package/dist/index.cjs +896 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +58 -0
- package/dist/index.d.ts +58 -0
- package/dist/index.js +71 -0
- package/dist/index.js.map +1 -0
- package/dist/react/index.cjs +23038 -0
- package/dist/react/index.cjs.map +1 -0
- package/dist/react/index.css +5731 -0
- package/dist/react/index.css.map +1 -0
- package/dist/react/index.d.cts +1937 -0
- package/dist/react/index.d.ts +1937 -0
- package/dist/react/index.js +21500 -0
- package/dist/react/index.js.map +1 -0
- package/dist/themes/generated.css +1155 -0
- package/package.json +72 -0
|
@@ -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 · ❌ not required ·
|
|
376
|
+
➖ not applicable · 🟡 conditional / partial ·
|
|
377
|
+
🛠️ already built · ⬜ 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.
|