@nurix/etna 0.20.37 → 0.20.38
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/data/catalog/rules/frontend.md +5 -5
- package/data/catalog/rules/pnpm.md +2 -2
- package/data/harness-index.json +1 -1
- package/data/harnesses/code/harness.json +2 -2
- package/data/harnesses/code/hooks/approved-packages.md +1 -2
- package/data/harnesses/code/rules/frontend.md +5 -5
- package/data/harnesses/code/rules/pnpm.md +2 -2
- package/data/harnesses/code/skills/boundary/references/packs/build-config-runtime.md +1 -1
- package/data/harnesses/code/skills/design/components/charts.md +121 -43
- package/data/harnesses/code/skills/design/components/collections.md +209 -41
- package/data/harnesses/code/skills/design/components/feedback.md +86 -25
- package/data/harnesses/code/skills/design/components/hooks.md +198 -12
- package/data/harnesses/code/skills/design/components/index.md +93 -71
- package/data/harnesses/code/skills/design/components/inputs.md +249 -157
- package/data/harnesses/code/skills/design/components/layout.md +98 -38
- package/data/harnesses/code/skills/design/components/media.md +46 -25
- package/data/harnesses/code/skills/design/components/motion.md +56 -46
- package/data/harnesses/code/skills/design/components/navigation.md +54 -15
- package/data/harnesses/code/skills/design/components/overlays.md +51 -41
- package/data/harnesses/code/skills/design/components/setup.md +84 -5
- package/data/harnesses/code/skills/design/components/typography.md +72 -22
- package/data/harnesses/code/skills/design/components/utils.md +47 -13
- package/data/harnesses/code/skills/design/data/chrome/board-column.md +12 -12
- package/data/harnesses/code/skills/design/data/chrome/data-table.md +6 -6
- package/data/harnesses/code/skills/design/data/chrome/side-panel.md +15 -15
- package/data/harnesses/code/skills/design/data/chrome/sidebar.md +25 -25
- package/data/harnesses/code/skills/design/data/chrome/stat-cards.md +22 -22
- package/data/harnesses/code/skills/design/data/chrome/topbar.md +12 -12
- package/data/harnesses/code/skills/design/references/emission.md +6 -6
- package/data/harnesses/code/skills/design/scripts/README-validator.md +20 -8
- package/data/harnesses/code/skills/design/scripts/validate-design-md.mjs +15 -10
- package/data/harnesses/delivery/harness.json +1 -1
- package/data/harnesses/etna/harness.json +2 -2
- package/data/harnesses/etna/skills/boundary/references/packs/build-config-runtime.md +1 -1
- package/data/harnesses/hr/harness.json +1 -1
- package/package.json +2 -2
|
@@ -4,69 +4,79 @@
|
|
|
4
4
|
|
|
5
5
|
## Dialog
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Centered modal panel (`@nurix/components/compound/dialog`); Base UI owns focus, scroll lock and dismissal.
|
|
8
8
|
|
|
9
|
-
**Sub-components:** `
|
|
9
|
+
**Sub-components:** `Dialog.Trigger`, `Dialog.MorphTrigger`, `Dialog.Popup`, `Dialog.Header`, `Dialog.Body`, `Dialog.Footer`, `Dialog.Title`, `Dialog.Description`, `Dialog.Close`; also `dialogPopupVariants`
|
|
10
10
|
|
|
11
|
-
**
|
|
11
|
+
**Root props:** `intent` [neutral|info|success|warning|danger] (`danger` renders an alert dialog), `dismissible` (default true; false when `intent="danger"`; governs Escape, outside press and focus-out only, `Dialog.Close` always closes), `labels` `{ close, title }`, `modal`
|
|
12
12
|
|
|
13
|
-
**
|
|
13
|
+
**Popup props:** `size` [xs|sm|default|lg|full] (`full` fills the viewport with a thin gap), `title`, `description`, `icon` (render a built-in header), `hasCloseButton` (defaults to the resolved dismissibility, so a danger dialog draws no ✕), `hasDivider`
|
|
14
|
+
|
|
15
|
+
**Header/Footer props:** `hasDivider` **Body:** the one scroll region; the popup caps at the viewport, so the footer stays in view
|
|
14
16
|
|
|
15
17
|
```tsx
|
|
16
18
|
<Dialog>
|
|
17
|
-
<
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
<DialogFooter showDivider>
|
|
19
|
+
<Dialog.Trigger render={<Button />}>Edit profile</Dialog.Trigger>
|
|
20
|
+
<Dialog.Popup size="lg">
|
|
21
|
+
<Dialog.Header hasDivider>
|
|
22
|
+
<Dialog.Title>Edit Profile</Dialog.Title>
|
|
23
|
+
</Dialog.Header>
|
|
24
|
+
<Dialog.Body>
|
|
25
|
+
<Input value={name} onValueChange={setName} />
|
|
26
|
+
</Dialog.Body>
|
|
27
|
+
<Dialog.Footer hasDivider>
|
|
28
|
+
<Dialog.Close render={<Button variant="outline" />}>Cancel</Dialog.Close>
|
|
28
29
|
<Button onClick={handleSave}>Save</Button>
|
|
29
|
-
</
|
|
30
|
-
</
|
|
30
|
+
</Dialog.Footer>
|
|
31
|
+
</Dialog.Popup>
|
|
31
32
|
</Dialog>
|
|
32
33
|
```
|
|
33
34
|
|
|
35
|
+
Destructive confirmation is `<Dialog intent="danger">` with no separate AlertDialog. An untitled popup is named from `labels.title` ("Dialog") unless it carries `aria-label`; pass `title` or `aria-label`, never a second `Dialog.Title` beside the placeholder.
|
|
36
|
+
|
|
34
37
|
## Sheet
|
|
35
38
|
|
|
36
|
-
|
|
39
|
+
Edge-pinned modal panel (`@nurix/components/block/sheet`) with a header/body/footer layout.
|
|
40
|
+
|
|
41
|
+
**Sub-components:** `Sheet.Trigger`, `Sheet.Popup`, `Sheet.Header`, `Sheet.Body`, `Sheet.Footer`, `Sheet.Title`, `Sheet.Description`, `Sheet.Close`; also `sheetSizeVariants`
|
|
37
42
|
|
|
38
|
-
**
|
|
43
|
+
**Root props:** `intent` [neutral|info|success|warning|danger] (`danger` renders an alert dialog), `dismissible` (default true; false when `intent="danger"`), `labels` `{ close, title }`, `modal` (`false` or `"trap-focus"` draws no scrim)
|
|
39
44
|
|
|
40
|
-
**
|
|
45
|
+
**Popup props:** `side` [start|end|top|bottom] (default `end`; logical, so `end` opens from the left in RTL), `size` [xs|sm|default|lg|full] (width of a start/end sheet, height cap of a top/bottom one), `hasCloseButton` (defaults to the resolved dismissibility), `overlayClassName`
|
|
41
46
|
|
|
42
|
-
**Header/Footer props:** `
|
|
47
|
+
**Header/Footer props:** `hasDivider` **Body props:** `className` styles the padded content box; `className="p-0"` drops the inset
|
|
43
48
|
|
|
44
49
|
```tsx
|
|
45
50
|
<Sheet>
|
|
46
|
-
<
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
</SheetBody>
|
|
56
|
-
<SheetFooter showDivider>
|
|
51
|
+
<Sheet.Trigger render={<Button />}>Open</Sheet.Trigger>
|
|
52
|
+
<Sheet.Popup side="end" size="lg">
|
|
53
|
+
<Sheet.Header hasDivider>
|
|
54
|
+
<Sheet.Title>Settings</Sheet.Title>
|
|
55
|
+
</Sheet.Header>
|
|
56
|
+
<Sheet.Body>
|
|
57
|
+
<Input value={name} onValueChange={setName} />
|
|
58
|
+
</Sheet.Body>
|
|
59
|
+
<Sheet.Footer hasDivider>
|
|
57
60
|
<Button onClick={handleSave}>Save</Button>
|
|
58
|
-
</
|
|
59
|
-
</
|
|
61
|
+
</Sheet.Footer>
|
|
62
|
+
</Sheet.Popup>
|
|
60
63
|
</Sheet>
|
|
61
64
|
```
|
|
62
65
|
|
|
66
|
+
A bottom drawer is `side="bottom"`. Omitting `Sheet.Title` names every sheet "Sheet" (`labels.title`); give each one a title. An in-flow docked region that reflows its sibling instead of overlaying it is `SidePanel` (`block/side-panel`, see [layout](layout.md)).
|
|
67
|
+
|
|
68
|
+
## Morphing overlay
|
|
69
|
+
|
|
70
|
+
`Dialog.MorphTrigger` and `Popover.MorphTrigger` grow the popup out of the trigger's own box (shared layout on the slow motion step) instead of the plain open transition. Use one in place of the family's `Trigger`; it takes no `render` (style it with `className`), and under reduced motion the popup opens on the plain transition.
|
|
71
|
+
|
|
63
72
|
## Other Overlays
|
|
64
73
|
|
|
65
|
-
|
|
74
|
+
Namespaced compounds over Base UI: each `Trigger` composes your element through `render` (no `asChild`), each floating part is a `Popup` that portals itself, and there is no `Portal` part.
|
|
66
75
|
|
|
67
|
-
- `Command` (`command`)
|
|
68
|
-
- `ContextMenu` (`context-menu`)
|
|
69
|
-
- `DropdownMenu` (`dropdown-menu`)
|
|
70
|
-
- `
|
|
71
|
-
- `
|
|
72
|
-
- `
|
|
76
|
+
- `Command` (`compound/command`) — `.Input`, `.List`, `.Empty`, `.Loading`, `.Error`, `.Group`, `.Item`, `.Separator`, `.Shortcut`, `.Results` (`data`, `renderItem`), `.Footer`; a dialog palette is `.Dialog` + `.Trigger` (children are its text; `shortcut`, `size` [default|sm]) + `.Popup`; the query lives on the root (`searchValue`/`onSearchValueChange`), plus `loading`, `error`, `filter` (`null` turns it off); items fire `onClick`, never `onSelect`; fetch with `useSearchSource` (`hooks/use-search-source`)
|
|
77
|
+
- `ContextMenu` (`compound/context-menu`) — right-click or long-press on `.Trigger`; `.Popup` requires `aria-label` or `aria-labelledby`; the same item parts as `DropdownMenu`
|
|
78
|
+
- `DropdownMenu` (`compound/dropdown-menu`) — `.Trigger`, `.Popup`, `.Item`, `.LinkItem` (`href`), `.CheckboxItem`, `.RadioGroup`/`.RadioItem`, `.Label`, `.Separator`, `.Group`, `.Shortcut`, `.Sub`/`.SubTrigger`/`.SubPopup`; items fire `onClick` (`closeOnClick={false}` keeps it open), `intent="danger"`, `isInset`; popups take `side`, `align`, `sideOffset`, `alignOffset`, `collisionPadding`, `anchor`
|
|
79
|
+
- `Popover` (`element/popover`) — `.Trigger`, `.MorphTrigger`, `.Popup` (default `side="bottom"`, `align="center"`, `sideOffset={4}`), `.Arrow`, `.Title`, `.Description`, `.Close`; a hover card is `<Popover.Trigger openOnHover delay closeDelay>`
|
|
80
|
+
- `Tooltip` (`element/tooltip`) — `.Trigger`, `.Popup` (`side`, `sideOffset` default 4, `align`), `.Arrow` (none by default), `.Provider` (one per toolbar, shared `delay`); `intent` and `disabled` on the root; an icon-only trigger still needs its own `aria-label`
|
|
81
|
+
- `ActionBar` (`compound/action-bar`) — hover-revealed icon toolbar anchored to the root's other children: `.Bar` (a direct child of the root; `variant` [floating|bare]), `.Action` (`label`, `pressed`); root `side` [top|bottom|start|end] (default bottom), `align` (default end), `offset`, `orientation`, `reveal` [hover|always|hidden]; also `actionBarVariants`
|
|
82
|
+
- `SpeedDial` (`compound/speed-dial`) — `.Trigger` + `.Action` (`label` is its name and tooltip; `onClick`); `side` [start|end] (default start); renders inline, so the host positions it; `event.preventDefault()` in an action's `onClick` keeps it open
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
## Package
|
|
6
6
|
|
|
7
|
-
`@nurix/components` — published to the public npm registry (`registry.npmjs.org`) under the restricted `@nurix` scope.
|
|
7
|
+
`@nurix/components` — published to the public npm registry (`registry.npmjs.org`) under the restricted `@nurix` scope. Ships `dist/` and `capabilities.json`. Required peers: `react ^19`, `react-dom ^19`; `@types/react` / `@types/react-dom` are optional peers for TypeScript consumers. Core dependencies install with the package: `@base-ui/react`, `class-variance-authority`, `cn`, `date-fns`, `lucide-react`, `motion`, `nanoid`, `react-day-picker`, `zod`. Source of truth: `github.com/nurixlabs/components`.
|
|
8
8
|
|
|
9
9
|
## Installing
|
|
10
10
|
|
|
@@ -16,22 +16,101 @@ The scope is restricted, so the consumer project needs an npm token with read ac
|
|
|
16
16
|
|
|
17
17
|
Then install with the project's package manager (detect from the lockfile): `npm install @nurix/components`.
|
|
18
18
|
|
|
19
|
+
## Optional peer families
|
|
20
|
+
|
|
21
|
+
Heavy third-party packages are **optional peers**, grouped into families — install a family only when an imported subpath reaches it. A missing family fails at the bundler with the package named (`Cannot resolve "@visx/scale"`), never silently at runtime. Copy ranges from `node_modules/@nurix/components/package.json` `peerDependencies`.
|
|
22
|
+
|
|
23
|
+
| Family | Subpaths that reach it | Install |
|
|
24
|
+
| --- | --- | --- |
|
|
25
|
+
| `charts` | `block/chart` | `@visx/curve @visx/event @visx/grid @visx/group @visx/heatmap @visx/pattern @visx/responsive @visx/scale @visx/shape d3-array` |
|
|
26
|
+
| `spark` | `element/spark`, `block/table` | `@mantine/charts @mantine/core recharts` (plus `@mantine/hooks`, which `@mantine/core` peers on) |
|
|
27
|
+
| `table` | `block/table` | `@tanstack/react-table` |
|
|
28
|
+
| `code` | `compound/code-block` | `shiki` |
|
|
29
|
+
| `editor` | `block/tiptap-editor` | `@tiptap/core @tiptap/extension-image @tiptap/extension-table @tiptap/extension-task-item @tiptap/extension-task-list @tiptap/pm @tiptap/react @tiptap/starter-kit tiptap-markdown` |
|
|
30
|
+
| `markdown` | `element/markdown` | `react-markdown remark-gfm` |
|
|
31
|
+
| `sortable` | `compound/sortable` | `@dnd-kit/core @dnd-kit/modifiers @dnd-kit/sortable @dnd-kit/utilities` |
|
|
32
|
+
| `carousel` | `compound/carousel` | `embla-carousel-react embla-carousel-autoplay` |
|
|
33
|
+
| `icon-picker` | `compound/icon-picker` | `@tanstack/react-virtual fuse.js` |
|
|
34
|
+
| `resizable` | `block/resizable` | `react-resizable-panels` |
|
|
35
|
+
| `mic-input` | `compound/mic-input` | `react-voice-visualizer` |
|
|
36
|
+
| `issue-reporter` | `block/issue-reporter` | `html2canvas-pro` |
|
|
37
|
+
| `animated-number` | `element/animated-number`, `block/chart` | `@number-flow/react` |
|
|
38
|
+
| `anchored-scroller` | `block/anchored-scroller` | `@shadcn/react` |
|
|
39
|
+
| `file-picker` | `lib/uppy-adapter` | `@uppy/core` |
|
|
40
|
+
|
|
41
|
+
## capabilities.json
|
|
42
|
+
|
|
43
|
+
`@nurix/components/capabilities.json` is the machine-readable manifest — one record per published subpath: `subpath`, `grain` (`element` · `compound` · `block` · `effect` · `hooks` · `lib`), `category`, `tier`, `contractLevel`, `taxonomy`, `symbols` (`{ name, typeOnly? }`), and `peers: [{ family, packages }]` where the subpath needs an optional family (no `peers` field = core only). Read it to resolve which families a project's imports need, or to confirm a symbol exists before importing it.
|
|
44
|
+
|
|
19
45
|
## Styles (required)
|
|
20
46
|
|
|
21
|
-
In the consumer's CSS entry,
|
|
47
|
+
In the consumer's CSS entry, in this order:
|
|
22
48
|
|
|
23
49
|
```css
|
|
50
|
+
@import "./fonts.css"; /* @fontsource registrations */
|
|
51
|
+
@import "./design.tokens.css"; /* the product's brand layer (design skill emission) */
|
|
24
52
|
@import "tailwindcss";
|
|
25
53
|
@import "@nurix/components/styles.css";
|
|
54
|
+
@import "./personality.css"; /* the product's compiled personality (design skill emission) */
|
|
26
55
|
```
|
|
27
56
|
|
|
28
|
-
|
|
57
|
+
`personality.css` follows `styles.css` because its bare `:root` / `.dark` blocks only tie the library's own (0,1,0) personality defaults and win on import order.
|
|
58
|
+
|
|
59
|
+
The shipped `styles.css` is **not** precompiled — it carries the design tokens, the personality token layer at neutral defaults, the base layer, the custom variants and utilities, the library's keyframes, and an embedded `@source "./**/*.js"` directive, so the consumer's Tailwind v4 build scans the package's compiled JS and generates every utility class the components use. The project must have Tailwind v4 set up; it does **not** add its own `@source` for this package.
|
|
60
|
+
|
|
61
|
+
- **Dark mode is the `.dark` class** on an ancestor (`@custom-variant dark (&:is(.dark *))`), never a `data-theme` attribute — the library's dark values compile as `.dark { … }`, so an attribute switch inverts the product's tokens while every library surface stays light.
|
|
62
|
+
- **The cascade has three layers:** brand (`:root` — the shadcn colour vocabulary, `--radius`, the type ramp), personality (`--motion-*`, `--density-*`, `--radius-scale`, edge geometry, `--elevation-*`, `--texture-grain-opacity`), and mode (`.dark`, restating values only). The design skill's `design.tokens.css` armors its theme blocks as `:root:not(.dark)` / `:root.dark` (0,2,0), so the library's (0,1,0) blocks never defeat them whatever the import order.
|
|
63
|
+
- **Roles a product may claim:** `--success`, `--warning`, `--info` retint `--intent-success|warning|info` (the colours the `intent` prop and `data-intent` select); `--intent-danger` aliases `--destructive`. Declaring nothing ships theme-correct fallbacks. The switch is global — a subtree wanting its own status colours redeclares `--intent-*`, never `--success`.
|
|
64
|
+
- **Brand-layer tokens beyond shadcn's set:** `--destructive-foreground`, `--input-surface`, `--scrim` / `--scrim-foreground` (a mode-invariant pair for backdrops and captions over imagery), `--radius-xs` … `--radius-xl` (each scaled by `--radius-scale`), and the type-ramp roles `text-display`, `text-title`, `text-h1`–`text-h3`, `text-prose`, `text-body`, `text-control-lg`, `text-dense`, `text-control`, `text-meta`, `text-micro`, `text-code`, `text-ident`, plus `--text-inline` (0.875em, for text-flow pieces).
|
|
65
|
+
- **No font family ships.** `--font-display`, `--font-sans`, `--font-mono` are declared slots falling back to `inherit`; the product's `design.tokens.css` supplies the faces.
|
|
66
|
+
|
|
67
|
+
## Shipped utilities
|
|
68
|
+
|
|
69
|
+
Set per element at the call site; each reads the personality tokens, so a trait change moves them with no class change.
|
|
70
|
+
|
|
71
|
+
- `focus-ring` — the one focus ring (`--ring-width`, `--ring-offset`, `ring-ring/50`) on `:focus-visible`.
|
|
72
|
+
- `focus-ring-within` — the same ring on a host frame, lit only while its descendant carrying `data-focus-target` shows keyboard focus.
|
|
73
|
+
- `hover-wash` — a `currentColor` wash: 6% on hover (under `hover: hover`), `[data-highlighted]` and `[data-popup-open]`; 12% on `:active` and `[data-dragging]`; never on disabled or read-only parts, never fighting a `bg-*` fill.
|
|
74
|
+
- `interactive:` variant — matches a part that is not `:disabled`, `[data-disabled]`, `[aria-disabled="true"]`, `[data-readonly]` or `[aria-readonly="true"]` (`interactive:cursor-pointer`).
|
|
75
|
+
- `hit-area`, `hit-area-*`, `hit-area-{l,r,t,b,x,y}-*`, `hit-area-debug` — a pointer target larger than the drawn box.
|
|
76
|
+
- `clip`, `clip-*`, `clip-lines`, `clip-fade` — line clamps and fade-outs whose line count is set at runtime.
|
|
29
77
|
|
|
30
|
-
|
|
78
|
+
## Editor stylesheet
|
|
79
|
+
|
|
80
|
+
`block/tiptap-editor` needs its document stylesheet, imported once beside the component or in the CSS entry: `import "@nurix/components/tiptap-editor.css";`. It carries the `--doc-*` tokens at zero specificity (`:where(.nx-tiptap, .nx-doc)`) and the `.nx-doc` rules, deliberately unlayered so a host's layered preflight cannot strip the document's typography; override with `.nx-tiptap { --doc-accent: … }` from anywhere.
|
|
81
|
+
|
|
82
|
+
## Personality
|
|
83
|
+
|
|
84
|
+
`@nurix/components/lib/personality` compiles a **PersonalitySpec** — six orthogonal traits — into the token layer every component reads. The product declares the feel in its `design.md` (`personality:`); the design skill's validator (`validate-design-md.mjs --emit-personality --write`) runs `compileToCss` from the project's own install and writes the committed `personality.css`, imported after `styles.css` so its `:root` wins on order.
|
|
85
|
+
|
|
86
|
+
| Trait | Values | Default | Token family |
|
|
87
|
+
| --- | --- | --- | --- |
|
|
88
|
+
| `density` | `compact` · `regular` · `airy` | `regular` | `--density-control-height`, `--density-control-padding`, `--density-pad-{xs,sm,md,lg}`, `--density-gap-{sm,md,lg}` |
|
|
89
|
+
| `pace` | number `0`–`1` (0 = still, 0.5 = neutral, 1 = lively) | `0.5` | `--motion-duration-{fast,base,slow}`; `--motion-ease-standard` turns lively at ≥ 0.8 |
|
|
90
|
+
| `edge` | `sharp` · `regular` · `soft` | `regular` | `--radius-scale`; `sharp` also narrows `--ring-width`, `--edge-hairline` and `--edge-stroke`, and squares `--radius-pill` |
|
|
91
|
+
| `depth` | `flat` · `layered` · `elevated` | `layered` | `--elevation-1..3` |
|
|
92
|
+
| `texture` | `clean` · `grainy` | `grainy` | `--texture-grain-opacity` |
|
|
93
|
+
| `accent` | `neutral` · `expressive` | `neutral` | `--accent`, `--accent-foreground`, `--ring` (expressive re-anchors them on `--primary`) |
|
|
94
|
+
|
|
95
|
+
- **Spec shape:** `{ version: 1, ...traits }`, strict — any key outside the table is rejected. Declare only the traits that differ from neutral; omitted traits resolve to `PERSONALITY_DEFAULTS`, and the empty spec compiles to the base feel.
|
|
96
|
+
- **Validate at the edge — `compileToCss` does not validate input.** `validatePersonality(input)` → `{ ok: true, spec } | { ok: false, issues: [{ path, message }] }` (JSON-pointer paths). `diagnosePersonality(spec)` → `{ warnings }` for legal but self-defeating combinations (`still-expressive`, `flat-grain`, `compact-soft`). `explainPersonality(spec)` → each trait's value with its source (`spec` | `default`) plus the tokens it emits. `personalityJsonSchema` / `PersonalitySpecSchema` (zod) are the tool-definition and parse surfaces; `resolvePersonality(partial)` fills defaults without validating.
|
|
97
|
+
- **Emitters:** `compileToCss(spec)` → the full stylesheet (`:root`, a paired `.dark` block, the reduced-motion block last); it throws on a contrast-floor or mode-pairing invariant violation. `compileToVars(spec)` → the `:root` values as a style object (no mode pairing, no media queries). `deriveTokens(resolved)` → the raw `TokenEmission[]`.
|
|
98
|
+
- **`<PersonalityStyle spec={spec} />` is a runtime switch only** — it installs one shared `<style id="nurix-personality-style">` (newest mount wins, removed on last unmount) for a density toggle or a preview, never in place of the committed `personality.css`.
|
|
99
|
+
- **App code stays inside the trait's reach by reading tokens, not literals:** `p-(--density-pad-md)`, `gap-(--density-gap-sm)`, `duration-(--motion-duration-base)`, `shadow-(--elevation-2)`. A plain `transition-*` inherits `--motion-duration-fast`; a `duration-*` literal opts that element out. `data-mode="inline"` on a collection container zeroes the motion durations beneath it.
|
|
100
|
+
|
|
101
|
+
```tsx
|
|
102
|
+
import { PersonalityStyle, validatePersonality } from "@nurix/components/lib/personality";
|
|
103
|
+
|
|
104
|
+
const result = validatePersonality({ version: 1, density: "compact", pace: 0.3, edge: "sharp" });
|
|
105
|
+
|
|
106
|
+
export function DensityPreview() {
|
|
107
|
+
return result.ok ? <PersonalityStyle spec={result.spec} /> : null;
|
|
108
|
+
}
|
|
109
|
+
```
|
|
31
110
|
|
|
32
111
|
## Import pattern
|
|
33
112
|
|
|
34
|
-
Use subpath imports —
|
|
113
|
+
Use subpath imports — every published entry is one `<layer>/<name>` subpath (`element/`, `compound/`, `block/`, `effect/`, `hooks/`, `lib/`), plus the `styles.css`, `tiptap-editor.css` and `capabilities.json` assets. There is no root barrel:
|
|
35
114
|
|
|
36
115
|
```tsx
|
|
37
116
|
import { Button } from "@nurix/components/element/button";
|
|
@@ -2,45 +2,95 @@
|
|
|
2
2
|
|
|
3
3
|
> Generated reference (regen-owned; see [index.md](index.md)). Binds only under `primitives: nurix`.
|
|
4
4
|
|
|
5
|
-
Import each from its
|
|
5
|
+
Import each from its layered subpath, e.g. `import { Chip } from "@nurix/components/element/chip"`.
|
|
6
6
|
|
|
7
|
-
##
|
|
7
|
+
## Chip
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
THE small label: tag, filter token, enum value, status pill. A Chip with no `onRemove` renders no interactive descendant, so it is a safe pure-display label (`@nurix/components/element/chip`).
|
|
10
10
|
|
|
11
|
-
**Props:** `
|
|
11
|
+
**Props:** `variant` [default|secondary|outline|muted|soft|ghost|bare|status|colored], `intent` [neutral|info|success|warning|danger] (exclusive with `color`), `color` (a stored data colour), `size` [sm|default|lg], `rounded` [low|medium|high], `prefix` ["dot"|node], `isPulsing`, `onRemove`, `disabled`, `isTruncated` (default true), `isInteractive`, `labels` {remove}, `render`
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
Status meaning is `intent`, never `color="var(--intent-success)"`. `color` repaints the chrome only on `variant="colored"` (the resolved variant when `color` is set without one); on other variants it reaches only a `prefix="dot"`. `isPulsing` animates nothing without `prefix="dot"`. Drop `onRemove` when `render` makes the chip itself a link or button. A toggling chip is `compound/reaction`, not this.
|
|
14
14
|
|
|
15
15
|
```tsx
|
|
16
|
-
<
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
16
|
+
<Chip variant="outline" size="sm" onRemove={() => remove(id)}>React</Chip>
|
|
17
|
+
<Chip variant="status" prefix="dot" isPulsing intent="info">Connecting</Chip>
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Stat
|
|
21
|
+
|
|
22
|
+
One KPI unit (`element/stat`): value and label on a line, an optional sub line beneath. It owns the unit, never the ledger row — lay several on a `Row` at the call site.
|
|
23
|
+
|
|
24
|
+
**Props:** `label:string`, `value:string` (format the number and its unit first), `sub?`, `intent` [neutral|info|success|warning|danger] (paints the value from the `--intent-*` tokens; default `neutral` inherits), `href` (renders an anchor), `onClick` (renders a button; with `href` it runs before navigation), `action` (inline affordance after the label — plain form only, the type rejects it beside `href`/`onClick`), `render` (a router link: `render={<RouterLink to="/breached" />}`)
|
|
25
|
+
|
|
26
|
+
- The anchor and button forms take no margin: their hover wash paints on an outset pseudo-element, and a clipping parent cuts it.
|
|
27
|
+
- A dashboard stat tile with trend, comparison or a visual is the `stat` spec of `Chart` (`block/chart`), documented in [charts](charts.md).
|
|
28
|
+
|
|
29
|
+
```tsx
|
|
30
|
+
<Row align="start" gap="lg" className="justify-end border-t pt-4">
|
|
31
|
+
<Stat label="Open" value="128" sub="across 4 queues" />
|
|
32
|
+
<Stat label="Breached" value="7" intent="danger" href="/breached" />
|
|
33
|
+
</Row>
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## CodeBlock
|
|
37
|
+
|
|
38
|
+
Highlighted block code through shiki: line numbers, diff rows, emphasis, collapse, wrap, folding, line selection and streaming (`@nurix/components/compound/code-block`). Requires the optional peer `shiki` (the `code` family) installed before the subpath is imported at all, even for `hasHighlighting={false}`.
|
|
39
|
+
|
|
40
|
+
**Sub-components:** `CodeBlock.Header`, `CodeBlock.Title`, `CodeBlock.Language`, `CodeBlock.CopyButton`, `CodeBlock.DownloadButton`, `CodeBlock.WrapToggle`, `CodeBlock.ExpandButton`, `CodeBlock.LineActions`, `CodeBlock.Content`
|
|
41
|
+
|
|
42
|
+
**Props:** `value` or `lines` (pre-tokenized, exclusive), `language`, `hasHighlighting` (default true), `hasLineNumbers`, `startLine`, `diff` {added, removed}, `emphasizedLines`, `emphasizedWords`, `focusedLines`, `lineIntents` {danger, warning, info}, `maxLines` + `open`/`defaultOpen`, `wrap`/`defaultWrap`, `isFoldable`, `isSelectable`, `isStreaming`, `variant` [default|bare], `themes`, `label`, `labels`
|
|
43
|
+
|
|
44
|
+
**Helpers:** `highlightCode(value, options)` (a server component awaits it and passes `lines`), `parseUnifiedDiff(patch)` (per-file `lines` for a `git diff`), `ansiToLines(text)` (terminal output), `markdownFences` / `markdownCodeProps` (split a markdown string by hand)
|
|
45
|
+
|
|
46
|
+
Line specs are `"2-4,7"` or `[2, 3, 4, 7]` and need a resolved `language`; an unknown `language` paints plain lines. Highlighting is asynchronous, so plain lines paint first. The header is `flex`, so give the trailing part `ml-auto`. Inline code in a sentence is `element/code`; a markdown string with fenced code is `element/markdown`.
|
|
47
|
+
|
|
48
|
+
```tsx
|
|
49
|
+
<CodeBlock value={snippet} language="tsx" hasLineNumbers diff={{ removed: [2], added: [3] }}>
|
|
50
|
+
<CodeBlock.Header>
|
|
51
|
+
<CodeBlock.Title>app.tsx</CodeBlock.Title>
|
|
52
|
+
<CodeBlock.Language className="ml-auto" />
|
|
53
|
+
<CodeBlock.CopyButton />
|
|
54
|
+
</CodeBlock.Header>
|
|
55
|
+
</CodeBlock>
|
|
22
56
|
```
|
|
23
57
|
|
|
24
58
|
## SecureText
|
|
25
59
|
|
|
26
|
-
Masked display for
|
|
60
|
+
Masked display for a secret (API key, card number) with an optional reveal toggle (`@nurix/components/element/secure-text`). A secret being typed is `element/input` with `type="password"`, not this.
|
|
61
|
+
|
|
62
|
+
**Props:** `value`, `hasRevealButton` (default true), `defaultRevealed`, `revealed`/`onRevealedChange` (controlled), `maskOptions` (`{ maskChar, unmaskedStart, unmaskedEnd, preserveSpacing, grouping, showLength }`), `labels` {reveal, hide, masked}, `fallback` (empty value, no toggle)
|
|
27
63
|
|
|
28
|
-
|
|
64
|
+
It takes no `size` and no `variant`: it inherits the surrounding text, so wrap it in `<Code variant="bare">` for monospaced digits. `maskText(value, options)` masks a value in a cell, tooltip or log line without the toggle. The full `value` reaches the client, so masking is presentation, never protection.
|
|
29
65
|
|
|
30
66
|
```tsx
|
|
31
67
|
<SecureText
|
|
32
68
|
value={apiKey}
|
|
33
|
-
|
|
34
|
-
|
|
69
|
+
maskOptions={{ unmaskedStart: 3, unmaskedEnd: 4 }}
|
|
70
|
+
labels={{ reveal: "Reveal API key", hide: "Hide API key" }}
|
|
35
71
|
/>
|
|
36
72
|
```
|
|
37
73
|
|
|
74
|
+
## Timestamp
|
|
75
|
+
|
|
76
|
+
A living instant in a `<time>` element: relative near now, absolute beyond a threshold, with the exact time on `title` (`@nurix/components/element/timestamp`).
|
|
77
|
+
|
|
78
|
+
**Props:** `value` (Date | ms), `relativeThreshold` (default `DEFAULT_RELATIVE_THRESHOLD`, 30 days; `0` always absolute, `Infinity` always relative), `format` (an `Intl` field-option patch over month/day/year; name a field `undefined` to drop it), `locale`, `isLive` (default true), `title` (`""` writes none), `titleFormat`, `fallback`, `render` (function form for custom copy; no `children`)
|
|
79
|
+
|
|
80
|
+
The label reads absolute until hydration, so server and client markup agree. A date being picked is `compound/date-picker`; a never-live, never-relative string is a plain `date-fns` `format` call.
|
|
81
|
+
|
|
82
|
+
```tsx
|
|
83
|
+
<Timestamp value={event.createdAt} fallback="—" />
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Text and headings
|
|
87
|
+
|
|
88
|
+
No text or heading component exists: write the semantic element (`<h2>`, `<p>`, `<span>`) and pick a step of the shipped type ramp through its utility — `text-display`, `text-title`, `text-h1`, `text-h2`, `text-h3`, `text-prose`, `text-body`, `text-dense`, `text-meta`, `text-micro`, `text-code` (each carries size, line height and weight). The library ships no font family, so text inherits the product's face; line up digits with `tabular-nums`, never `font-mono`.
|
|
89
|
+
|
|
38
90
|
## Other Components
|
|
39
91
|
|
|
40
|
-
- `
|
|
41
|
-
- `
|
|
42
|
-
- `
|
|
43
|
-
- `
|
|
44
|
-
- `
|
|
45
|
-
- `link` — `Link`: `variant` [default|muted|plain|underline], `size` [sm|default|lg], `asChild`, `external` (forces `target="_blank"` + a trailing icon; also auto-detects `target="_blank"`)
|
|
46
|
-
- `markdown` — `Markdown`: GFM `react-markdown` + `remark-gfm` render with a full element map (headings, lists, element/code, blockquote, tables) styled on the library's own tokens — no `@tailwindcss/typography` plugin needed; safe to re-render incrementally while streaming. **Props:** `children` (markdown string), `className`
|
|
92
|
+
- `element/code` — `Code`, `codeVariants`: inline `<code>` sized at the surrounding text (no `size`), no highlighting; `variant` [default|bare] (`bare` inside a host unless explicit), `render`
|
|
93
|
+
- `element/kbd` — `Kbd`, `Kbd.Group`: em-sized keycap (no `size`); `variant` [default|outline] (`outline` on a muted surface such as a search trigger), `label` (spoken name for a glyph key like `⌘`; never `aria-label`); `Kbd.Group` adds spacing, no `+` separator
|
|
94
|
+
- `element/label` — `Label`: native `<label>` bound by `htmlFor`; `disabled` dims it via `data-disabled`, `render` swaps the element (inside a `Cell`, use `Cell.Label`)
|
|
95
|
+
- `element/link` — `Link`, `linkVariants`: navigational anchor that inherits the type around it; `variant` [default|muted|plain|underline], `isExternal` (or `target="_blank"`: hardened `rel`, trailing icon, announced "(opens in a new tab)"), `isActive` (`aria-current="page"`), `disabled`, `labels` {newTab}, `render` (a router link element); an action is Button `variant="link"`
|
|
96
|
+
- `element/markdown` — `Markdown`: read-only GFM render of a string (`children`) with raw HTML escaped; fenced code is an unhighlighted muted block; restyle through `--doc-*` tokens on the root, never `[&_p]:` utilities; `labels` {footnotes, footnoteBackReference}, `render`. Requires the optional peers `react-markdown` + `remark-gfm` (the `markdown` family)
|
|
@@ -2,36 +2,70 @@
|
|
|
2
2
|
|
|
3
3
|
> Generated reference (regen-owned; see [index.md](index.md)). Binds only under `primitives: nurix`.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Non-visual helpers, each published as its own subpath under `@nurix/components/lib/<name>`. Class merging on an ordinary element is the standalone `cn` package (`import { cn } from "cn"`, installed with the library), not a subpath here.
|
|
6
6
|
|
|
7
|
-
##
|
|
7
|
+
## mergeBaseClassName
|
|
8
8
|
|
|
9
|
-
`
|
|
9
|
+
`mergeBaseClassName(ours, theirs)` (from `lib/utils`) — merges a wrapper's own classes with a caller's `className` on a Base UI part, which types `className` as `string | ((state) => string | undefined)`. The result is always the callback form, so a caller styling off part state keeps working; bare `cn` accepts the callback, then silently drops it. The result is a function even for two strings, so never pass it where a `string` is expected. The same subpath exports `isMobileDevice()`, a userAgent regex with no reactivity; branch on `useMediaQuery` (`hooks/use-media-query`) instead.
|
|
10
10
|
|
|
11
11
|
```tsx
|
|
12
|
-
<
|
|
12
|
+
<BasePopover.Popup className={mergeBaseClassName("rounded-md p-3", className)} />
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
## formatDate
|
|
16
16
|
|
|
17
|
-
`formatDate(date, opts?)` (from `format`) —
|
|
17
|
+
`formatDate(date, opts?, locale?)` (from `lib/format`) — an absolute stamp through `Intl.DateTimeFormat`. `opts` is a patch against a seeded `{ month: "long", day: "numeric", year: "numeric" }`, not a verbatim `Intl` bag: remove a field by naming it `undefined`, because omitting it leaves the seed in place. A `dateStyle`/`timeStyle` bag passes unseeded. Omitting `locale` resolves it from the runtime. A missing or unformattable value returns `""` rather than throwing.
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
```tsx
|
|
20
|
+
formatDate(value, { month: "short", day: "numeric", year: undefined }); // "Aug 15"
|
|
21
|
+
```
|
|
20
22
|
|
|
21
|
-
|
|
23
|
+
## formatRelativeTime
|
|
22
24
|
|
|
23
|
-
|
|
25
|
+
`formatRelativeTime(ts, now?, locale?)` and `useNow(tickMs = DEFAULT_TICK_MS, active = true)` (from `lib/time`) — a relative label ("5 minutes ago", "in 2 hours") on `Intl.RelativeTimeFormat`, plus the clock that re-renders it. `DEFAULT_TICK_MS` is `30_000`. Every `useNow` subscriber at the same `tickMs` shares one interval, so call it per row; `active: false` subscribes to nothing and freezes the clock at its last tick. A bare `Date.now()` in place of `useNow()` never updates. `element/timestamp` already wires both.
|
|
24
26
|
|
|
25
|
-
|
|
27
|
+
```tsx
|
|
28
|
+
<span>{formatRelativeTime(updatedAt, useNow())}</span>
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## generateId
|
|
32
|
+
|
|
33
|
+
`generateId({ length = 12 }?)` (from `lib/id`) — a random 62-alphanumeric id for handles that never leave the browser: a React key for a staged file, a client row id minted before the server answers. `length` is the only option; there is no prefix or separator. Ids are neither sortable nor UUIDs, so a persisted row takes a UUID. An id referenced by `aria-*` or `htmlFor` is React's `useId`, since a call-time id breaks hydration. About one id in six starts with a digit, so `querySelector("#" + id)` needs `CSS.escape`.
|
|
34
|
+
|
|
35
|
+
```tsx
|
|
36
|
+
const stagedFile = { key: generateId(), file };
|
|
37
|
+
```
|
|
26
38
|
|
|
27
39
|
## getStrictContext
|
|
28
40
|
|
|
29
|
-
`getStrictContext<T>(name?)` (from `get-strict-context`) — returns a `[Provider, useSafeContext]` pair
|
|
41
|
+
`getStrictContext<T>(name?)` (from `lib/get-strict-context`) — returns a `[Provider, useSafeContext]` pair whose hook throws `useContext must be used within <name>` outside its `Provider`, so a namespaced family's misplaced part fails loudly instead of rendering empty. Call it once at module scope, since each call mints a fresh Provider identity, and pass a memoised `value`. A legitimately optional value wants a plain `React.createContext` with a default.
|
|
42
|
+
|
|
43
|
+
```tsx
|
|
44
|
+
const [TabsProvider, useTabs] = getStrictContext<TabsState>("Tabs");
|
|
45
|
+
```
|
|
30
46
|
|
|
31
47
|
## composeRefs
|
|
32
48
|
|
|
33
|
-
`composeRefs(...refs)` / `useComposedRefs(...refs)` (from `compose-refs`) — merges
|
|
49
|
+
`composeRefs(...refs)` / `useComposedRefs(...refs)` (from `lib/compose-refs`) — merges callback refs and `RefObject`s into one callback ref, honouring React 19 ref cleanups. Use the hook when a component keeps its own ref and accepts the caller's; the measuring hooks' callback refs compose here. `composeRefs` is the non-hook form for event handlers and imperative helpers. The refs are the memo's dependency list: hoist inline callback refs, and keep the arity fixed by passing `undefined` for an absent ref.
|
|
50
|
+
|
|
51
|
+
```tsx
|
|
52
|
+
<div ref={useComposedRefs(forwardedRef, localRef)} />
|
|
53
|
+
```
|
|
34
54
|
|
|
35
|
-
|
|
55
|
+
The motion-aware `asChild` slot, `Slot` (`lib/slot`), is a motion primitive — [motion.md](motion.md).
|
|
36
56
|
|
|
37
|
-
|
|
57
|
+
## readableOn
|
|
58
|
+
|
|
59
|
+
`readableOn(color)` (from `lib/color`) — white or near-black ink (`"#ffffff"` / `"#1a1a1a"`) for a fill by WCAG relative luminance, split at 0.55. Reach for it only when the fill comes from outside the token layer, such as an initials avatar on a brand seed; a themed surface takes `text-foreground`. Input must be a 6-digit hex with or without `#`; a CSS var, an `oklch()` or a 3-digit hex silently answers `"#ffffff"`. Set fill and ink on the same element so they never drift.
|
|
60
|
+
|
|
61
|
+
```tsx
|
|
62
|
+
<span style={{ backgroundColor: seed, color: readableOn(seed) }}>{initials}</span>
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## upgradeConfig
|
|
66
|
+
|
|
67
|
+
`upgradeConfig(input, currentVersion, migrations?)` (from `lib/config-version`) — runs once wherever a stored dashboard or chart config is read back. Versionless input reads as version 1, and `migrations[n]` lifts a config from `n` to `n + 1`. A version with no registered migration passes through unchanged, and unknown fields survive only when each migration spreads its input. The result is always stamped `version: currentVersion`, so a missing migration is invisible afterwards; `undefined` input yields `{ version: currentVersion }`. A config that never leaves memory owes no version.
|
|
68
|
+
|
|
69
|
+
```tsx
|
|
70
|
+
const config = upgradeConfig(stored, 2, { 1: (c) => ({ ...c, density: "comfortable" }) });
|
|
71
|
+
```
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
---
|
|
2
2
|
id: board-column
|
|
3
3
|
class: d2
|
|
4
|
-
sourced: 2026-07
|
|
5
|
-
source: "spec authored from scratch — @nurix/components ships no board/kanban primitive;
|
|
4
|
+
sourced: 2026-10-07
|
|
5
|
+
source: "spec authored from scratch — @nurix/components ships no board/kanban primitive; its kanban-board recommendation (@dnd-kit/core + @dnd-kit/sortable composed with Card) plus Chip, ScrollArea, Avatar, Empty — ~/dev/components"
|
|
6
6
|
license: open
|
|
7
7
|
---
|
|
8
8
|
|
|
9
9
|
# Chrome recipe — board column
|
|
10
10
|
|
|
11
|
-
> **Library gap.** `@nurix/components` has no board/kanban component
|
|
11
|
+
> **Library gap.** `@nurix/components` has no board/kanban component — no kanban/board export; its `kanban-board` recommendation record vouches for `@dnd-kit/core` + `@dnd-kit/sortable` composed with `Card`, and `compound/sortable` covers reordering one flat list only, never moves across columns. This recipe is therefore a from-scratch specification in both switch modes; under `nurix` it follows that recommendation with `Card`/`Chip`/`ScrollArea`. Flagged as a curation candidate for library admission.
|
|
12
12
|
|
|
13
13
|
One column of a kanban/work board: a workflow state rendered as a vertical lane of draggable cards with a counted header, an add affordance, and drop semantics. The board is a view-type projection of a queue — columns are states, cards are work items.
|
|
14
14
|
|
|
@@ -17,15 +17,15 @@ One column of a kanban/work board: a workflow state rendered as a vertical lane
|
|
|
17
17
|
| Slot | Spec | nurix building block |
|
|
18
18
|
| --- | --- | --- |
|
|
19
19
|
| Column shell | Fixed width (280–320px), full board height, vertical flex; a quiet wash distinguishing the lane from the canvas and from its cards; rounded per the package radius scale. | `div` + tokens |
|
|
20
|
-
| Column header | Pinned: state dot or icon (the column's identity color), title, count badge (live: cards currently in the lane), optional WIP limit rendered as `count / limit`, trailing overflow menu (rename, set limit, collapse), optional collapse toggle. | `
|
|
21
|
-
| Card list | The only scrolling region — each column scrolls independently; consistent card gap from the spacing scale. | `ScrollArea` +
|
|
22
|
-
| Card | The unit of drag: title (2-line clamp), meta row (assignee `Avatar`, due date, priority pill), optional label chips and a progress/checklist hint. Whole-card drag (`
|
|
20
|
+
| Column header | Pinned: state dot or icon (the column's identity color), title, count badge (live: cards currently in the lane), optional WIP limit rendered as `count / limit`, trailing overflow menu (rename, set limit, collapse), optional collapse toggle. | `Chip`, `DropdownMenu` |
|
|
21
|
+
| Card list | The only scrolling region — each column scrolls independently; consistent card gap from the spacing scale. | `ScrollArea` + one dnd-kit `SortableContext` per column (`verticalListSortingStrategy`) |
|
|
22
|
+
| Card | The unit of drag: title (2-line clamp), meta row (assignee `Avatar`, due date, priority pill), optional label chips and a progress/checklist hint. Whole-card drag (the `useSortable` listeners on the card) or an explicit grip (the listeners on a handle button) — with an explicit handle, the card body remains a plain click target for opening the record. | `Card` rendered as a `useSortable` node |
|
|
23
23
|
| Drop indicator | An insertion line (or ghost slot) at the projected index while a drag is over the column. | dnd-kit collision feedback |
|
|
24
|
-
| Drag overlay | The dragged card's floating preview, portaled above the board at overlay elevation, slightly scaled/tilted per the package's motion vocabulary; the origin card stays in place at reduced opacity. | `
|
|
24
|
+
| Drag overlay | The dragged card's floating preview, portaled above the board at overlay elevation, slightly scaled/tilted per the package's motion vocabulary; the origin card stays in place at reduced opacity. | dnd-kit `DragOverlay` |
|
|
25
25
|
| Footer | "+ Add card" ghost affordance pinned under the list; activating it opens an inline title input at the column tail. | `Button variant="ghost"` |
|
|
26
|
-
| Empty slot | An empty column keeps a dashed placeholder slot so it remains an obvious drop target. | `
|
|
26
|
+
| Empty slot | An empty column keeps a dashed placeholder slot so it remains an obvious drop target. | `Empty` (quiet) |
|
|
27
27
|
|
|
28
|
-
Board-level contract the column participates in: `
|
|
28
|
+
Board-level contract the column participates in: one `DndContext` (`PointerSensor` + `KeyboardSensor`, `closestCorners`) around all columns; `onDragEnd` reorders within a column (`arrayMove`) or filters-and-inserts across columns, and handles a drop on an empty column, whose `over` is the column id rather than a card. Card ids are unique across the whole board. State changes commit through the consumer's data layer — the column never owns item data.
|
|
29
29
|
|
|
30
30
|
## The eight states
|
|
31
31
|
|
|
@@ -42,9 +42,9 @@ Board-level contract the column participates in: `Sortable` root (`DndContext`)
|
|
|
42
42
|
|
|
43
43
|
## Keyboard behavior
|
|
44
44
|
|
|
45
|
-
Grounded in
|
|
45
|
+
Grounded in dnd-kit's keyboard sensor and screen-reader announcements, the same substrate `compound/sortable` ships (grab / move / drop / cancel are announced):
|
|
46
46
|
|
|
47
|
-
- **Tab** reaches each card (or its handle) in column order, then the column's add affordance and menu. Every card is focusable — the
|
|
47
|
+
- **Tab** reaches each card (or its handle) in column order, then the column's add affordance and menu. Every card is focusable — the `useSortable` attributes (which carry `tabIndex`) go on the card itself or on an explicit handle button; a pointer-only board is a defect.
|
|
48
48
|
- **Enter / Space** on a focused card picks it up; **arrow up/down** moves it within the column; **arrow left/right** moves it to the adjacent column (same relative index, clamped); **Enter / Space** drops; **Escape** cancels and returns the card to its origin.
|
|
49
49
|
- Each move is announced to assistive tech ("moved to Review, position 2 of 5"); drop and cancel are announced likewise.
|
|
50
50
|
- **Enter** on the card body (when a separate handle exists) opens the record — typically in the side-panel recipe.
|
|
@@ -64,5 +64,5 @@ Grounded in the shipped `Sortable`'s keyboard sensor and screen-reader announcem
|
|
|
64
64
|
|
|
65
65
|
A board is the right projection when workflow state is the primary axis the operator thinks in and moving items between states is a frequent, deliberate act — triage, pipeline stages, content review. It is a projection of the queue/browse archetype, never an archetype itself: the same items must remain reachable as a table, and the switch between projections belongs to the page, not the component. Keep visible cards per column modest (order of tens; virtualize beyond) and card content to what the move decision needs — title, owner, urgency; everything else lives one click away in the side panel. Columns are few and stable (a board with twelve columns is a table wearing a costume); WIP limits are the one piece of process the chrome itself should enforce visibly.
|
|
66
66
|
|
|
67
|
-
- **`primitives: nurix`** — composed (no single import; library gap flagged above): `import {
|
|
67
|
+
- **`primitives: nurix`** — composed (no single import; library gap flagged above): `import { DndContext, DragOverlay, PointerSensor, KeyboardSensor, closestCorners } from "@dnd-kit/core"; import { SortableContext, useSortable, verticalListSortingStrategy, arrayMove } from "@dnd-kit/sortable"; import { CSS } from "@dnd-kit/utilities"; import { Card } from "@nurix/components/block/card"; import { Chip } from "@nurix/components/element/chip"; import { ScrollArea } from "@nurix/components/element/scroll-area"; import { Avatar } from "@nurix/components/element/avatar"; import { Empty } from "@nurix/components/compound/empty-state";` — card transforms go through `CSS.Transform.toString(transform)`, never hand-rolled. A single column reordered in isolation is `Sortable` from `@nurix/components/compound/sortable` (`value` / `onValueChange(next, { activeIndex, overIndex })`, `Sortable.Item isHandle`, `Sortable.ItemHandle`, `Sortable.Overlay`).
|
|
68
68
|
- **`primitives: native`** — this recipe is the full build spec: fixed-width independently-scrolling lanes, drag-and-drop with the complete keyboard path and live announcements above (the stack's own dnd library or an existing design-system board), optimistic moves with revert-on-failure, WIP-limit rejection, and all column/priority color resolved from the token artifact.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
id: data-table
|
|
3
3
|
class: d2
|
|
4
|
-
sourced: 2026-07
|
|
4
|
+
sourced: 2026-10-07
|
|
5
5
|
source: "@nurix/components Table + defineColumn (on the UiTable primitive layer, @tanstack/react-table engine) — ~/dev/components"
|
|
6
6
|
license: open
|
|
7
7
|
---
|
|
@@ -17,7 +17,7 @@ The operator data table: column-definition-driven, read-only by default, with op
|
|
|
17
17
|
| Shells | `LoadingShell` → `FooterShell` → `AutoHeightShell` | Opt-in wrappers composed around the bare table, gated on `tableConfig` flags (`loading`, `pagination`, `autoHeight`). |
|
|
18
18
|
| Header row | `ColumnHeader` | One cell per column; sortable columns (`display.sortable`, defaulted from type) render a sort dropdown when `tableConfig.sort` is on. `header: false` removes the row entirely. |
|
|
19
19
|
| Selection column | id `__select__` | Prepended when `tableConfig.rowSelection`: tri-state header checkbox (page-scoped, indeterminate) + per-row checkboxes; selection accumulates across pages and emits `onSelect(selectedIds)`. |
|
|
20
|
-
| Body rows | `
|
|
20
|
+
| Body rows | `UiTable.Row` | Keyed by `row.id` (`TData extends { id: string }`); selected rows carry `data-selected`. |
|
|
21
21
|
| Cells | 13 renderers | Dispatched on the column `component`: `text`, `number`, `boolean`, `date`, `date-range`, `select`, `multiselect`, `slider`, `rating`, `status`, `priority`, `assignee`, `sparkline`. Each has a **display** variant (default — static, no popovers) and an **editable** variant (`editable: true`); a custom `cell(row)` function wins over everything. |
|
|
22
22
|
| Column contract | `defineColumn({ slug, label, type, component?, editable?, hidden?, metadata?, display?, cell? })` | `type: "text" \| "number" \| "boolean" \| "date" \| "enum" \| "user"` drives the default component and sortability; enum-family options are `metadata.options: [{ value, label, color }]`; explicit `display.width` is honored independent of resize. |
|
|
23
23
|
| Pagination footer | `TablePagination` | Controlled `{ current, size, total, onChange }`, 1-indexed; page numbers collapse with ellipses past seven pages. Sorting and pagination are always external — the table never owns that state. |
|
|
@@ -31,10 +31,10 @@ The operator data table: column-definition-driven, read-only by default, with op
|
|
|
31
31
|
| default | Rows on the canvas with hairline row separators; header optionally on a low-alpha muted wash (classic preset); numerals tabular. | `background`/`card`, `border`, `muted` at low alpha, `foreground` |
|
|
32
32
|
| hover | Row takes a quiet muted wash; sort triggers and checkboxes reveal on hover where hidden at rest. | `muted` |
|
|
33
33
|
| focus | Focus-visible ring on each interactive control — checkboxes, sort triggers, editable widgets, pagination buttons. | `ring` |
|
|
34
|
-
| active | Selected rows fill via `data-
|
|
34
|
+
| active | Selected rows fill via `data-selected`; checked boxes fill with the primary role; an open editable cell shows its live widget with ring emphasis; the sorted column's header shows direction. | `muted` (row), `primary` (checkbox), `ring` |
|
|
35
35
|
| disabled | The default **is** read-only — display cells are static, not "disabled". Truly disabled controls (a locked toggle, a non-permitted action) render at ~50% opacity with no pointer events. | reduced-alpha foreground |
|
|
36
36
|
| loading | `loading: true` overlays a centered spinner while retaining rendered rows beneath (no layout collapse); a first load with no data renders skeleton rows at the configured density. | `muted` (skeleton), `muted-foreground` (spinner) |
|
|
37
|
-
| error / empty | Zero rows render an empty-state
|
|
37
|
+
| error / empty | Zero rows render an empty-state row in the body ("No results." by default; the spec is message + optional action) — never a bare void. A failed load (`error={{ message, onRetry? }}`) renders an error state with retry in place of the rows and outranks `loading`. Row-level degradation is expressed through status pills (e.g. a failed sync status) on status roles, and stale data through a caption, not through reddened rows. | `muted-foreground`; `destructive`/`warning` status roles |
|
|
38
38
|
| success | A committed edit re-renders through parent state immediately (single source of truth); positive statuses render as status-role pills. Transient save confirmation belongs to a toast, not the grid. | `success` status role |
|
|
39
39
|
|
|
40
40
|
## Keyboard behavior
|
|
@@ -42,7 +42,7 @@ The operator data table: column-definition-driven, read-only by default, with op
|
|
|
42
42
|
- **Tab** moves through interactive elements in DOM order: header checkbox → sort triggers → row checkboxes → editable widgets → pagination. Display cells are not focus stops.
|
|
43
43
|
- **Enter/Space** toggles checkboxes and opens a sort dropdown; inside the dropdown, arrows move, Enter applies, **Escape** closes to the trigger. Up to 3 simultaneously sorted columns share one direction, FIFO-evicted.
|
|
44
44
|
- **Editable commit semantics** (what fires `onCellChange`): text/number commit on **Enter or blur**, **Escape** reverts the uncommitted value; select/status/priority/assignee commit on selection; multiselect on popover close after a change; boolean on toggle; slider on release; rating on star activation; date on selection; date-range on full-range selection.
|
|
45
|
-
- **Enum-family cells** open a searchable listbox (the
|
|
45
|
+
- **Enum-family cells** open a searchable listbox (the `Combobox`): type to filter, arrows to move, Enter to select, Escape to close — the cmdk interaction grammar applied per-cell.
|
|
46
46
|
- Pagination controls are plain buttons — Enter/Space activate; no custom arrow-key handling.
|
|
47
47
|
|
|
48
48
|
## Token reach
|
|
@@ -59,5 +59,5 @@ The operator data table: column-definition-driven, read-only by default, with op
|
|
|
59
59
|
|
|
60
60
|
The data table is the default projection for browse and queue archetypes — reach for it whenever rows are the unit of work and columns are attributes the operator scans, sorts, or edits in place. Real pagination and status chips are positive obligations for any believable table. Start read-only (the default) and make columns editable one at a time only where in-place editing is the actual workflow; keep sorting/pagination state in the page (the table is deliberately stateless about both) so URLs and refreshes preserve the operator's position. For bespoke markup the recipe's DOM contract is the semantic-table layer itself (`UiTable` under `nurix`), not a div grid.
|
|
61
61
|
|
|
62
|
-
- **`primitives: nurix`** — `import { Table, defineColumn } from "@nurix/components/table";` (escape hatches: cell renderers like `
|
|
62
|
+
- **`primitives: nurix`** — `import { Table, defineColumn } from "@nurix/components/block/table";` (escape hatches: cell renderers like `StatusCell` are exported from the same subpath; the bare primitive layer is `UiTable` from `@nurix/components/block/ui-table`). The subpath reaches two optional peer families — `table` (`@tanstack/react-table`) and `spark` (`@mantine/charts @mantine/core recharts`) — installed by the consumer.
|
|
63
63
|
- **`primitives: native`** — this recipe is the build spec: a semantic `<table>` (never a div grid) with a sticky header, tabular numerals, externally controlled sort (max 3 columns) and 1-indexed pagination with ellipsis collapse, an opt-in checkbox selection column with a tri-state header, read-only-by-default cells with the commit/revert keyboard semantics above, and every status/spark color resolved from the token artifact.
|