@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.
Files changed (36) hide show
  1. package/data/catalog/rules/frontend.md +5 -5
  2. package/data/catalog/rules/pnpm.md +2 -2
  3. package/data/harness-index.json +1 -1
  4. package/data/harnesses/code/harness.json +2 -2
  5. package/data/harnesses/code/hooks/approved-packages.md +1 -2
  6. package/data/harnesses/code/rules/frontend.md +5 -5
  7. package/data/harnesses/code/rules/pnpm.md +2 -2
  8. package/data/harnesses/code/skills/boundary/references/packs/build-config-runtime.md +1 -1
  9. package/data/harnesses/code/skills/design/components/charts.md +121 -43
  10. package/data/harnesses/code/skills/design/components/collections.md +209 -41
  11. package/data/harnesses/code/skills/design/components/feedback.md +86 -25
  12. package/data/harnesses/code/skills/design/components/hooks.md +198 -12
  13. package/data/harnesses/code/skills/design/components/index.md +93 -71
  14. package/data/harnesses/code/skills/design/components/inputs.md +249 -157
  15. package/data/harnesses/code/skills/design/components/layout.md +98 -38
  16. package/data/harnesses/code/skills/design/components/media.md +46 -25
  17. package/data/harnesses/code/skills/design/components/motion.md +56 -46
  18. package/data/harnesses/code/skills/design/components/navigation.md +54 -15
  19. package/data/harnesses/code/skills/design/components/overlays.md +51 -41
  20. package/data/harnesses/code/skills/design/components/setup.md +84 -5
  21. package/data/harnesses/code/skills/design/components/typography.md +72 -22
  22. package/data/harnesses/code/skills/design/components/utils.md +47 -13
  23. package/data/harnesses/code/skills/design/data/chrome/board-column.md +12 -12
  24. package/data/harnesses/code/skills/design/data/chrome/data-table.md +6 -6
  25. package/data/harnesses/code/skills/design/data/chrome/side-panel.md +15 -15
  26. package/data/harnesses/code/skills/design/data/chrome/sidebar.md +25 -25
  27. package/data/harnesses/code/skills/design/data/chrome/stat-cards.md +22 -22
  28. package/data/harnesses/code/skills/design/data/chrome/topbar.md +12 -12
  29. package/data/harnesses/code/skills/design/references/emission.md +6 -6
  30. package/data/harnesses/code/skills/design/scripts/README-validator.md +20 -8
  31. package/data/harnesses/code/skills/design/scripts/validate-design-md.mjs +15 -10
  32. package/data/harnesses/delivery/harness.json +1 -1
  33. package/data/harnesses/etna/harness.json +2 -2
  34. package/data/harnesses/etna/skills/boundary/references/packs/build-config-runtime.md +1 -1
  35. package/data/harnesses/hr/harness.json +1 -1
  36. package/package.json +2 -2
@@ -1,8 +1,8 @@
1
1
  ---
2
2
  id: side-panel
3
3
  class: d2
4
- sourced: 2026-07-12
5
- source: "@nurix/components Sheet (shadcn sheet on @radix-ui/react-dialog, extended with size variants + SheetBody) — ~/dev/components"
4
+ sourced: 2026-10-07
5
+ source: "@nurix/components Sheet (Base UI Dialog with a logical side, a size ladder and a header/body/footer skeleton) — ~/dev/components"
6
6
  license: open
7
7
  ---
8
8
 
@@ -14,23 +14,23 @@ The slide-in detail/edit panel: a modal surface anchored to a screen edge with a
14
14
 
15
15
  | Slot | Library part | Spec |
16
16
  | --- | --- | --- |
17
- | Root | `Sheet` | Open-state owner; controlled (`open`/`onOpenChange`) or uncontrolled via trigger. |
18
- | Trigger | `SheetTrigger` | Any element (`asChild`) that opens the panel — a table row's action, a card, a button. |
19
- | Overlay | `SheetOverlay` | Full-viewport scrim behind the panel; fades with open state; click closes. |
20
- | Panel | `SheetContent { side: "right" (default) \| "left" \| "top" \| "bottom", size: "sm" 320px \| "md" 384px \| "default" 480px \| "lg" 640px \| "xl" 800px \| "full", showCloseButton?, showOverlay? }` | Fixed to the chosen edge, full-height (left/right), hairline border on the attached edge, elevated shadow, vertical flex column. Below the small breakpoint it takes ~full width regardless of size. |
21
- | Header | `SheetHeader { showDivider? }` | Shrink-to-content top region: `SheetTitle` (the accessible name — a visually hidden fallback title exists, but a real one is the spec) + `SheetDescription` in muted text. |
22
- | Body | `SheetBody { noPadding? }` | `flex-1` scroll region — the only part that scrolls; long content never pushes the footer off-screen. |
23
- | Footer | `SheetFooter { showDivider? }` | Bottom-anchored action row (`mt-auto`): primary commit right, secondary/cancel beside it. |
24
- | Close | `SheetClose` + built-in ✕ | Corner close button (top-right, 70% opacity at rest) plus any element wrapped in `SheetClose`. |
17
+ | Root | `Sheet { open?, defaultOpen?, onOpenChange?, modal?, dismissible?, intent?, labels? }` | Open-state owner; controlled (`open`/`onOpenChange`) or uncontrolled via trigger. `dismissible={false}` refuses Escape and the scrim; `intent="danger"` makes it an alert dialog. |
18
+ | Trigger | `Sheet.Trigger` | Any element (`render`) that opens the panel — a table row's action, a card, a button. |
19
+ | Overlay | drawn by `Sheet.Popup` (styled through `overlayClassName`) | Full-viewport scrim behind the panel; fades with open state; click closes. `<Sheet modal={false}>` draws none. |
20
+ | Panel | `Sheet.Popup { side: "end" (default) \| "start" \| "top" \| "bottom", size: "xs" 288px \| "sm" 320px \| "default" 480px \| "lg" 640px \| "full", hasCloseButton? }` | Fixed to the chosen edge — `start`/`end` follow the writing direction — full-height (start/end), hairline border on the attached edge, elevated shadow, vertical flex column. `size` is the width of a start/end panel and the height cap of a top/bottom one. Below the small breakpoint a start/end panel takes three quarters of the width regardless of size. |
21
+ | Header | `Sheet.Header { hasDivider? }` | Shrink-to-content top region: `Sheet.Title` (the accessible name — an untitled panel is named from `labels.title`, "Sheet" by default, so a real title is the spec) + `Sheet.Description` in muted text. |
22
+ | Body | `Sheet.Body` | `flex-1` scroll region — the only part that scrolls; long content never pushes the footer off-screen. Its `className` styles the padded content box; `className="p-0"` drops the inset. |
23
+ | Footer | `Sheet.Footer { hasDivider? }` | Bottom-anchored action row (`mt-auto`): primary commit right, secondary/cancel beside it. |
24
+ | Close | `Sheet.Close` + built-in ✕ | Corner close button (ghost, icon-only, `xs`, top end corner, muted at rest), drawn while the panel is dismissible, plus any element rendered as `Sheet.Close`. |
25
25
 
26
26
  ## The eight states
27
27
 
28
28
  | State | Spec | Token roles |
29
29
  | --- | --- | --- |
30
30
  | default | Open at rest: panel on the base surface over the scrim; header/body/footer stacked; hairline dividers where opted in. | `background`, `border`, `foreground`, `muted-foreground` (description) |
31
- | hover | Corner close sharpens from 70% to full opacity; interior controls follow their own recipes. | control-level roles |
31
+ | hover | Corner close takes the ghost button's hover wash; interior controls follow their own recipes. | control-level roles |
32
32
  | focus | Focus is trapped inside the open panel; the close button and every control show a focus-visible ring. | `ring` |
33
- | active | Open/closed is expressed as `data-state` driving the slide + fade transition. The library ships 500ms in / 300ms out; the A6 panel budget (200–280ms, interruptible) governs — the package motion base overrides the stock duration, and a non-interruptible slide is a bug, not a style. | — |
33
+ | active | Open/closed is expressed as `data-open` / `data-closed`; the slide + fade is a tween on the slow motion step (300ms at the neutral pace) with the travel ease, and reduced motion drops the travel. The A6 panel budget (200–280ms, interruptible) governs — the package's `pace` brings the slow step inside it, and a non-interruptible slide is a bug, not a style. | — |
34
34
  | disabled | The commit action disables (~50% opacity, no pointer events) while the form is invalid or a save is in flight; the panel itself never disables — Escape and close always work. | reduced-alpha on controls |
35
35
  | loading | Record still resolving: header renders immediately with the known title, body shows skeleton blocks, footer actions disabled. The panel never opens empty-then-jumps. | `muted` (skeleton) |
36
36
  | error / empty | Save failure: inline error summary at the top of the body plus per-field errors — the panel stays open, input preserved. A missing record renders an empty state in the body with a close affordance. | `destructive`, `muted-foreground` |
@@ -49,12 +49,12 @@ The slide-in detail/edit panel: a modal surface anchored to a screen edge with a
49
49
  - `background` — the panel surface; `border` — the attached-edge hairline and the header/footer dividers.
50
50
  - `foreground` / `muted-foreground` — title / description and secondary text.
51
51
  - `ring` — the focus trap's visible rings, including the corner close.
52
- - Scrim: the library hardcodes `black/50` — as a specification the scrim derives from a token (an overlay role, or `foreground` at low alpha) so dark themes don't double-darken; the token-reach report flags a literal scrim.
52
+ - Scrim: the library paints `--scrim` (black at 50% in both modes) — as a specification the scrim derives from that token, restated per mode by the package so dark themes don't double-darken; the token-reach report flags a literal scrim.
53
53
  - Footer actions consume the button recipe's roles (`primary`/`primary-foreground`, `secondary`, `destructive`); inline errors consume `destructive`; skeletons consume `muted`.
54
54
 
55
55
  ## When to use / composition notes
56
56
 
57
- The side panel is edit-in-context: opening a record's detail or a scoped form beside a table, board, or list without losing the operator's position — the list stays visible under the scrim and scroll position survives the round trip. It is the wrong container for destructive confirmation (centered dialog), for navigation (sidebar), and for content that deserves its own URL (a full page — if operators share links to it, it's a page). Pick `size` by content: `sm`/`md` for quick property edits, `default`/`lg` for record detail, `xl` for side-by-side comparison; nested panels are a smell — a second level of depth means the flow wants a page. Footer actions: one primary commit, right-aligned; cancel is quiet, never a second primary.
57
+ The side panel is edit-in-context: opening a record's detail or a scoped form beside a table, board, or list without losing the operator's position — the list stays visible under the scrim and scroll position survives the round trip. It is the wrong container for destructive confirmation (centered dialog), for navigation (sidebar), and for content that deserves its own URL (a full page — if operators share links to it, it's a page). Pick `size` by content: `xs`/`sm` for quick property edits, `default`/`lg` for record detail, `lg` or a width class for side-by-side comparison; nested panels are a smell — a second level of depth means the flow wants a page. Footer actions: one primary commit, right-aligned; cancel is quiet, never a second primary.
58
58
 
59
- - **`primitives: nurix`** — `import { Sheet, SheetTrigger, SheetContent, SheetHeader, SheetTitle, SheetDescription, SheetBody, SheetFooter, SheetClose } from "@nurix/components/sheet";`
59
+ - **`primitives: nurix`** — `import { Sheet } from "@nurix/components/block/sheet";` — parts hang off the namespace: `Sheet.Trigger`, `Sheet.Popup`, `Sheet.Header`, `Sheet.Title`, `Sheet.Description`, `Sheet.Body`, `Sheet.Footer`, `Sheet.Close`.
60
60
  - **`primitives: native`** — this recipe is the build spec: a portal-rendered fixed aside on the chosen edge with the size ramp above, dialog semantics (`role="dialog"`, `aria-modal`, labeled by the title), a real focus trap with focus restore, Escape + overlay close, a fixed-header / scrollable-body / pinned-footer column, and an interruptible slide within the panel motion budget — implemented with the incumbent stack's own dialog/drawer primitive.
@@ -1,14 +1,14 @@
1
1
  ---
2
2
  id: sidebar
3
3
  class: d2
4
- sourced: 2026-07-12
5
- source: "@nurix/components Sidebar + opinionated layer (Brand, Switcher, ItemGroup, UserMenu, Callout, FileTree, SidebarCommandTrigger) — ~/dev/components"
4
+ sourced: 2026-10-07
5
+ source: "@nurix/components Sidebar + opinionated layer (Brand, Switcher, ItemGroup, UserMenu, CommandTrigger) and the sidebar-callout-card composition — ~/dev/components"
6
6
  license: open
7
7
  ---
8
8
 
9
9
  # Chrome recipe — sidebar
10
10
 
11
- The complete app-nav sidebar: brand block, searchable sections, badged items, promo slot, user footer, icon-collapse rail. One import path (`@nurix/components/sidebar`) carries both the unstyled shell primitives and the opinionated content layer.
11
+ The complete app-nav sidebar: brand block, searchable sections, badged items, promo slot, user footer, icon-collapse rail. One import path (`@nurix/components/block/sidebar`) carries both the unstyled shell primitives and the opinionated content layer, every part a member of the `Sidebar` namespace.
12
12
 
13
13
  ## Anatomy
14
14
 
@@ -16,24 +16,24 @@ The complete app-nav sidebar: brand block, searchable sections, badged items, pr
16
16
 
17
17
  | Slot | Library part | Spec |
18
18
  | --- | --- | --- |
19
- | Provider | `SidebarProvider` | Owns open/collapsed state; persists it in a cookie (`sidebar_state`, 7 days); registers the global toggle shortcut. |
20
- | Frame | `Sidebar` | `collapsible: "icon" \| "offcanvas" \| "none"`. Widths: 16rem expanded, 3rem icon rail, 18rem as a mobile drawer below the `md` breakpoint (mobile renders as an overlay sheet, not an inline column). |
21
- | Header | `SidebarHeader` | Pinned top region — hosts the brand block or switcher, and the search trigger. |
22
- | Content | `SidebarContent` | The scrollable middle — one or more item groups. |
23
- | Footer | `SidebarFooter` | Pinned bottom region — promo slot and/or user footer. |
24
- | Rail | `SidebarRail` | Thin edge hit-area that toggles collapse on click. |
25
- | Inset | `SidebarInset` | The main-content wrapper beside the sidebar. |
26
- | Trigger | `SidebarTrigger` | Icon button (lives in the topbar) toggling the same state. |
19
+ | Provider | `Sidebar.Provider` | Owns open/collapsed state (`open` / `defaultOpen` / `onOpenChange`, plus the mobile drawer's `openMobile` trio) and registers the global toggle shortcut (`keyboardShortcut`, default `b`). It stores nothing: persistence is the caller's — read the store into `defaultOpen`, write it from `onOpenChange`. |
20
+ | Frame | `Sidebar` | `collapsible: "offcanvas" (default) \| "icon" \| "none"`, `side: "start" \| "end"`. Widths: 16rem expanded, a 3rem icon rail at the regular density (the item height plus its inset, so it follows `density`), 18rem as a mobile drawer below the `md` breakpoint (mobile renders as an overlay sheet, not an inline column). |
21
+ | Header | `Sidebar.Header` | Pinned top region — hosts the brand block or switcher, and the search trigger. |
22
+ | Content | `Sidebar.Content` | The scrollable middle — one or more item groups. |
23
+ | Footer | `Sidebar.Footer` | Pinned bottom region — promo slot and/or user footer. |
24
+ | Rail | `Sidebar.Rail` | Thin edge hit-area that toggles collapse on click. |
25
+ | Inset | `Sidebar.Inset` | The main-content wrapper beside the sidebar. |
26
+ | Trigger | `Sidebar.Trigger` | Icon button (lives in the topbar) toggling the same state. |
27
27
 
28
28
  **Content layer** (each grounded in the shipped props):
29
29
 
30
- - **Brand block** — `Brand { logo, name, caption?, href? }`: a 32px logo tile filled with `sidebar-primary` / `sidebar-primary-foreground`, name (medium weight) over an optional caption. Collapses to logo-only in icon mode.
31
- - **Switcher** — `Switcher { render: "simple" | "rich", drop: "bottom" | "right", options, activeId, onSelect, addAction?, menuLabel? }`: header dropdown for team/workspace/version. Options carry `{ id, label, sublabel?, icon?, shortcut? }`; an optional add-action row sits under a separator.
32
- - **Search trigger** — `SidebarCommandTrigger { placeholder, shortcut }`: a ⌘K search chip pinned to 32px height; in icon mode it swaps to a matched-height icon button with a tooltip, so the rail never shifts on collapse.
33
- - **Sections** — `ItemGroup { label?, render: "list" | "tree" | "rich", collapsible?, defaultOpen?, showMore?, items, itemsCollapsible?, compact?, collapsedLabel: "hide" | "divider" }`. `list` = flat rows with optional inline collapsible submenu; `tree` = recursive chevron tree; `rich` = two-line rows (label + sublabel + trailing hint). `collapsedLabel: "divider"` keeps a 32px hairline footprint in icon mode so groups stay separated without rail shift. `compact` halves group padding and tightens item gap.
34
- - **Item** — `{ id, label, icon?, badge?, state?, emoji?, avatar?, sublabel?, trailing?, isActive?, href?, onClick?, children? }`: one leading decoration (icon, emoji, or avatar), the label, and a trailing badge/state pill rendered outside the button (`SidebarMenuBadge`). `children` makes it a branch.
35
- - **Promo slot** — `Callout { title, description?, action?, input?: { kind: "email" | "text", placeholder?, onSubmit? } }`: a bordered card on a low-alpha `sidebar-accent` wash with title, description, optional single input + full-width action button. Hidden entirely in icon mode.
36
- - **User footer** — `UserMenu { user: { name, email, avatar? }, menuItems? }`: avatar (initials fallback) + name/email + up-down chevron; opens a dropdown (right of the rail on desktop, below on mobile) with an identity header and action items (`{ id, label, icon?, shortcut?, destructive? }`).
30
+ - **Brand block** — `Sidebar.Brand { logo, name, caption?, href? }`: a 32px logo tile filled with `sidebar-primary` / `sidebar-primary-foreground`, name (medium weight) over an optional caption. Collapses to logo-only in icon mode.
31
+ - **Switcher** — `Sidebar.Switcher { variant: "simple" | "rich", side: "bottom" | "inline-end", options, value / defaultValue, onValueChange(id, details), addAction?, menuLabel? }`: header dropdown for team/workspace/version. Options carry `{ id, label, sublabel?, icon?, shortcut? }`; an optional add-action row sits under a separator.
32
+ - **Search trigger** — `Sidebar.CommandTrigger { children, shortcut?, collapsedTooltip? }`: a ⌘K search chip, its text as children, pinned to the sidebar item height (32px at the regular density); in icon mode it swaps to a matched-height icon button with a tooltip, so the rail never shifts on collapse.
33
+ - **Sections** — `Sidebar.ItemGroup { label?, variant: "list" | "tree" | "rich", isCollapsible?, open / defaultOpen / onOpenChange, onShowMore?, items, hasCollapsibleItems?, defaultExpandedIds?, collapsedLabel: "hide" | "divider", labels?, onItemClick? }`. `list` = flat rows with optional inline collapsible submenu; `tree` = recursive chevron tree (a file tree is this variant with folder and file icons); `rich` = two-line rows (label + sublabel + trailing hint). `collapsedLabel: "divider"` keeps a hairline footprint in icon mode so groups stay separated without rail shift. Group spacing follows the `density` trait; there is no per-group compact switch.
34
+ - **Item** — `{ id, label, icon?, badge?, state?, emoji?, avatar?, sublabel?, trailing?, isActive?, disabled?, href?, onClick?, children? }`: one leading decoration (icon, emoji, or avatar), the label, and a trailing badge/state pill rendered outside the button (`Sidebar.MenuBadge`). `children` makes it a branch.
35
+ - **Promo slot** — the `sidebar-callout-card` composition (no library member): a `Sidebar.Group` in the footer holding a bordered card with title, description, optional single `Input` + full-width `Button`. Hidden entirely in icon mode (`group-data-[collapsible=icon]:hidden`).
36
+ - **User footer** — `Sidebar.UserMenu { user: { name, email, avatar? }, menuItems }`: avatar (initials fallback) + name/email + up-down chevron; opens a dropdown (beside the rail on desktop, below on mobile) with an identity header and the caller's action items (`{ id, label, icon?, shortcut?, onSelect?, intent?: "neutral" | "danger" }`) — `menuItems` is required; nothing is defaulted.
37
37
 
38
38
  ## The eight states
39
39
 
@@ -42,17 +42,17 @@ The complete app-nav sidebar: brand block, searchable sections, badged items, pr
42
42
  | default | Rows at rest: transparent fill, quiet text, 16px icons. | `sidebar` (surface), `sidebar-foreground` |
43
43
  | hover | Row fills; text sharpens. Group labels with collapse affordance show a pointer + chevron. | `sidebar-accent`, `sidebar-accent-foreground` |
44
44
  | focus | Focus-visible ring on the focused row/control; same fill as hover. Never suppressed. | `sidebar-ring` |
45
- | active | The current destination: persistent fill + medium weight, exposed via `data-active` / `aria-current`. Open branch parents show a rotated chevron; an open dropdown trigger holds the accent fill (`data-state="open"`). | `sidebar-accent`, `sidebar-accent-foreground` |
45
+ | active | The current destination: persistent fill + medium weight, exposed via `data-active` / `aria-current`. Open branch parents show a rotated chevron; an open dropdown trigger holds the accent fill (`data-popup-open`). | `sidebar-accent`, `sidebar-accent-foreground` |
46
46
  | disabled | ~50% opacity, no pointer events, removed from tab order; label still legible. | `sidebar-foreground` at reduced alpha |
47
- | loading | `SidebarMenuSkeleton` rows — icon square + text bar shimmer — while nav data resolves; header and footer regions render immediately so the frame never jumps. | `muted` (skeleton), `sidebar-border` |
47
+ | loading | `Sidebar.MenuSkeleton` rows — icon square + text bar shimmer — while nav data resolves; header and footer regions render immediately so the frame never jumps. | `muted` (skeleton), `sidebar-border` |
48
48
  | error / empty | A group with nothing to show renders a one-line hint in muted text or is omitted; the sidebar itself carries no error surface — failures belong to content. Attention counts ride item badges. | `muted-foreground`; `destructive` on alert badges |
49
49
  | success | No persistent success state; confirmation is expressed as badge-count change or a status-role pill on the relevant item. | status roles via badge |
50
50
 
51
51
  ## Keyboard behavior
52
52
 
53
- - **Cmd/Ctrl+B** toggles collapse globally (registered by `SidebarProvider`); `SidebarTrigger` and `SidebarRail` are the pointer equivalents.
53
+ - **Cmd/Ctrl+B** toggles collapse globally (registered by `Sidebar.Provider`; `keyboardShortcut` rebinds it, `null` unbinds); `Sidebar.Trigger` and `Sidebar.Rail` are the pointer equivalents.
54
54
  - **Tab order**: header (brand/switcher → search trigger) → groups top-to-bottom, items in visual order → footer user menu. Items are real links or buttons — **Enter** activates, **Space** activates buttons.
55
- - **Branches**: Enter/Space on a collapsible parent toggles it; the chevron rotation mirrors `data-state`.
55
+ - **Branches**: Enter/Space on a collapsible parent toggles it; the chevron rotation mirrors its open state.
56
56
  - **Dropdowns** (Switcher, UserMenu): Enter/Space opens; arrow keys move through options; Enter selects; **Escape** closes and returns focus to the trigger.
57
57
  - **cmdk path**: the search chip opens the command palette (⌘K); deep or infrequent destinations live in the palette — the sidebar carries the trigger, not a second search UI.
58
58
  - **Icon mode**: focus order is unchanged; each item surfaces its label as a tooltip on focus/hover so keyboard users are never navigating unlabeled glyphs.
@@ -68,11 +68,11 @@ The sidebar consumes the eight dedicated sidebar roles — the active package's
68
68
  - `sidebar-border` — hairlines: the panel edge, icon-mode group dividers.
69
69
  - `sidebar-ring` — focus rings.
70
70
 
71
- Secondary reach: the Callout uses `sidebar-border` + a low-alpha `sidebar-accent` wash + `muted-foreground`; dropdown menus render on `popover` / `popover-foreground`; destructive menu items use `destructive`; skeletons use `muted`. No color literal appears anywhere in the recipe — badge and state-pill colors resolve to status roles.
71
+ Secondary reach: the footer callout card draws a `sidebar-border` frame with `muted-foreground` copy; dropdown menus render on `popover` / `popover-foreground`; danger menu items (`intent: "danger"`) use `destructive`; skeletons use `muted`. No color literal appears anywhere in the recipe — badge and state-pill colors resolve to status roles.
72
72
 
73
73
  ## When to use / composition notes
74
74
 
75
- The sidebar is the primary navigation for any operator shell with more than a handful of destinations; icon-collapse earns its place on dense screens where horizontal space is the scarce resource, and the cookie-persisted state means the user's choice survives reloads. Canonical composition: `Brand` **or** `Switcher` in the header (both only when the product truly has workspace switching), the search trigger under it, one to three `ItemGroup`s in the content (label them once there are two), and the footer stacking `Callout` (optional, promotional, dismissible by omission) above `UserMenu`. Icons on every item are a positive obligation — a bare, icon-less nav is a known judge complaint. Density: `compact` on data-heavy operator tools, default elsewhere.
75
+ The sidebar is the primary navigation for any operator shell with more than a handful of destinations; icon-collapse earns its place on dense screens where horizontal space is the scarce resource, and persisting the open state (read into `defaultOpen`, written from `onOpenChange`) lets the user's choice survive reloads. Canonical composition: `Sidebar.Brand` **or** `Sidebar.Switcher` in the header (both only when the product truly has workspace switching), the search trigger under it, one to three `Sidebar.ItemGroup`s in the content (label them once there are two), and the footer stacking the callout card (optional, promotional, dismissible by omission) above `Sidebar.UserMenu`. Icons on every item are a positive obligation — a bare, icon-less nav is a known judge complaint. Density: the compact `density` trait on data-heavy operator tools, regular elsewhere.
76
76
 
77
- - **`primitives: nurix`** — `import { SidebarProvider, Sidebar, SidebarHeader, SidebarContent, SidebarFooter, SidebarRail, SidebarInset, SidebarTrigger, Brand, Switcher, ItemGroup, UserMenu, Callout, SidebarCommandTrigger } from "@nurix/components/sidebar";`
77
+ - **`primitives: nurix`** — `import { Sidebar } from "@nurix/components/block/sidebar";` — every part hangs off the namespace: `Sidebar.Provider`, `Sidebar.Header`, `Sidebar.Content`, `Sidebar.Footer`, `Sidebar.Rail`, `Sidebar.Inset`, `Sidebar.Trigger`, `Sidebar.Brand`, `Sidebar.Switcher`, `Sidebar.ItemGroup`, `Sidebar.UserMenu`, `Sidebar.CommandTrigger`; the callout card composes `Sidebar.Group` with `Input` (`@nurix/components/element/input`) and `Button` (`@nurix/components/element/button`).
78
78
  - **`primitives: native`** — this recipe is the build spec: a fixed aside (256px expanded / 48px icon rail) with pinned header/footer and an independently scrolling middle, collapse state persisted (cookie or localStorage), a global Cmd/Ctrl+B toggle, an overlay drawer below 768px, tooltips on rail items, and the state/keyboard/token contracts above implemented with the incumbent stack's own nav primitives.
@@ -1,42 +1,42 @@
1
1
  ---
2
2
  id: stat-cards
3
3
  class: d2
4
- sourced: 2026-07-12
5
- source: "@nurix/components Statistics compound family (+ AnimatedNumber, useStatHoverBridge; visuals host Gauge/Spark/ProgressBar/ChartLegend) — ~/dev/components"
4
+ sourced: 2026-10-07
5
+ source: "@nurix/components — composed pattern: Card + Stat (+ AnimatedNumber, Chip trend; visuals Spark / Progress / Chart) — ~/dev/components"
6
6
  license: open
7
7
  ---
8
8
 
9
9
  # Chrome recipe — stat cards
10
10
 
11
- The stat-card family: one compound grammar for every KPI tile, gauge card, sparkline card, legend card, and list card. The root is a structural flex column providing shared context (`size`, `comparisonLabel`); ~two dozen dotted parts compose the regions. The card draws no chart geometry itself — the visual slot hosts a chart element. The dashboard-tile obligation applies to every instance: **number + comparison + trend + state + door**.
11
+ The stat-card family: one composition grammar for every KPI tile, gauge card, sparkline card, legend card, and list card. In `@nurix/components` this is a **documented composition, not a single export** — a `Card` frame whose regions hold a `Stat` (or an `AnimatedNumber`), a trend `Chip`, a visual, and a footer. The card draws no chart geometry itself — the visual slot hosts a chart element. The dashboard-tile obligation applies to every instance: **number + comparison + trend + state + door**.
12
12
 
13
13
  ## Anatomy
14
14
 
15
15
  | Slot | Library part | Spec |
16
16
  | --- | --- | --- |
17
- | Root | `Statistics { size: "sm" \| "md" \| "lg", variant: "bare" \| "card" \| "elevated" \| "outline" \| "soft" \| "ghost", accent: "none" \| "primary" \| "success" \| "warning" \| "danger" \| "info", gap, justify, comparisonLabel }` | The surface. `bare` draws nothing (consumer owns the wrapper); the rest share `overflow-hidden rounded p-6` with their own background/border/shadow. `accent` draws a 4px left stripe. `size` scales the headline value and title through context. |
18
- | Section | `StatisticsSection` | Vertical region group for top/bottom cascades (`justify: "between"` pins one to each end). |
19
- | Header | `StatisticsHeader` → `StatisticsIcon { variant, tone: "soft" \| "solid" \| "outline" \| "plain", size (incl. "oversized"), shape }` + `StatisticsHeading` (`StatisticsTitle`, `StatisticsDescription`) + `StatisticsActions` | Icon/title block left, trailing actions (menu, info, tabs, date range) right. `oversized` icons bleed out the clipped top-right corner as a decorative watermark. |
20
- | Metric row | `StatisticsMetric` → `StatisticsValue { animated?, value, format, prefix, suffix }` + `StatisticsUnit` + `StatisticsTrend { value, direction?, invert?, pill?, format? }` + `StatisticsComparison` | Baseline-aligned: headline value (static, or digit-animated via NumberFlow with a formatted static first paint — SSR-safe), unit ("/ 5,000"), signed trend badge, comparison label (falls back to the card's `comparisonLabel`). |
21
- | Visual | `StatisticsVisual` (layout via `StatisticsGroup` horizontal/vertical) | Generic centered slot hosting `<Spark>`, `<Gauge>`, `<ProgressBar>`, `<ChartLegend>`, or any chart element. (Legacy `StatisticsGauge`/`Progress`/`SegmentedBar`/`Sparkline`/`Legend*` parts are deprecated thin wrappers over those same components.) |
22
- | Content | `StatisticsContent` | Escape hatch for list / table / paragraph cards. |
23
- | Footer | `StatisticsFooter` → `StatisticsCaption` + `StatisticsLink` | Freshness caption left ("Updated 2m ago" — the tile's *state*), link right (the tile's *door*). |
24
- | Hover bridge | `useStatHoverBridge()` | Headless hook lifting a hovered chart point into the header: hand its `onHoverChange` to the embedded graph, read `hover.value`/`hover.label` in the value/description with a resting fallback. |
17
+ | Root | `Card` | The surface: ring edge, scaled radius, insets from the `--density-pad-*` tokens. No variant, size or accent props — depth rides `className` as `shadow-(--elevation-N)` (or `shadow-none`), and an accent stripe is a 4px inline-start border on a `--intent-*` or `primary` role, set through `className`. |
18
+ | Section | flex column | Vertical region group for top/bottom cascades (`justify-between` pins one to each end). |
19
+ | Header | `Card.Header` → `Card.Title` (`render={<h3 />}` picks the level) + `Card.Description` + `Card.Header.Action` | Icon/title block at the start, trailing actions (menu, info, tabs, date range) pinned to the header's end column. The icon is a child glyph tinted from a role. |
20
+ | Metric row | `Stat { label, value, sub?, intent? }` or `AnimatedNumber { value, format?, trend?, fallback? }` + trend `Chip { intent }` | Baseline-aligned: headline value (a pre-formatted `Stat` string in the display face with tabular numerals, or a digit-rolling `AnimatedNumber` that paints formatted static text first — SSR-safe), unit ("/ 5,000") formatted into the string, signed trend chip, comparison line as `Stat`'s `sub`. |
21
+ | Visual | `Card.Content` slot | Generic centered slot hosting `<Spark>` (an axis-less trend: `family` line, area, bar or pie), `<Progress>` (one current value: bar or `variant="circular"` ring), or `<Chart>` once a gauge, legend, axes or a tooltip enter (`family: "gauge"`, `"ring"`, `"progress"` and the series families). |
22
+ | Content | `Card.Content` | Escape hatch for list / table / paragraph cards. |
23
+ | Footer | `Card.Footer` → `Timestamp` caption + `Link` | Freshness caption at the start ("Updated 2m ago" — the tile's *state*, staleness from `useFreshness`), link at the end (the tile's *door*). A whole-tile door is `Stat`'s `href` / `onClick`. |
24
+ | Hover bridge | caller state | No library hook: where the embedded `Chart` reports hover (`onHoverChange`, emitted by some renderers only), hold the hovered point in state and show it in the value/description with a resting fallback. `Spark` is decorative and reports nothing. |
25
25
 
26
- Every part emits `data-slot="statistics-…"` for styling and testing hooks.
26
+ The parts emit `data-slot="card-…"` and `data-slot="stat-…"` for styling and testing hooks; `Stat` and `Chip` add `data-intent`.
27
27
 
28
28
  ## The eight states
29
29
 
30
30
  | State | Spec | Token roles |
31
31
  | --- | --- | --- |
32
- | default | Card variant on the card surface with a hairline border; value at display scale with tabular numerals; title strong, description muted. | `card` + `card-foreground`, `border`, `foreground`, `muted-foreground` |
33
- | hover | Elevated variants lift per the package's elevation ramp; with the hover bridge wired, hovering the embedded chart live-rewrites the value and description to the hovered point, reverting on leave. | elevation system; value stays `foreground` |
32
+ | default | The card on the card surface with its hairline ring edge (`--edge-hairline`); value at display scale with tabular numerals; title strong, description muted. | `card` + `card-foreground`, `border`, `foreground`, `muted-foreground` |
33
+ | hover | A clickable card raises one rung on the package's elevation ramp (`hover:shadow-(--elevation-3)`); a clickable `Stat` takes its hover wash; with the hover bridge wired, hovering the embedded chart live-rewrites the value and description to the hovered point, reverting on leave. | elevation system; value stays `foreground` |
34
34
  | focus | The door (footer link or whole-card link) and any header action are focus stops with visible rings. | `ring` |
35
35
  | active | A clickable card shows a pressed treatment; the accent stripe marks category or alert affiliation. | `accent` stripe via `primary`/status roles |
36
36
  | disabled | No permission for the metric: value masked (—), lock caption, non-interactive, ~50% presence — the tile keeps its footprint. | reduced-alpha, `muted-foreground` |
37
37
  | loading | Skeleton title bar + value block + visual area at the card's fixed height — the grid never reflows when data lands. | `muted` |
38
38
  | error / empty | Degraded: em-dash value + status-role caption ("stale as of 09:12") — never a fabricated number. Empty series: the visual slot shows a quiet hint, never a zeroed fake chart. | `warning` / `destructive` caption, `muted-foreground` |
39
- | success | Trend intent is the success surface: up = good on the success role, down = bad on `destructive`; `invert` flips the mapping for lower-is-better metrics (error rate, latency, churn); zero/no-direction = neutral muted. `pill` wraps it in a tinted badge. Exposed as `data-direction`. | `success` / `destructive` status roles |
39
+ | success | Trend intent is the success surface: up = good on the success role, down = bad on `destructive`; the mapping inverts for lower-is-better metrics (error rate, latency, churn); zero/no-direction = neutral muted. The caller maps the sign to the trend `Chip`'s `intent` (`success`, `danger`, `neutral`), exposed as `data-intent`. | `success` / `destructive` status roles |
40
40
 
41
41
  ## Keyboard behavior
42
42
 
@@ -48,15 +48,15 @@ Every part emits `data-slot="statistics-…"` for styling and testing hooks.
48
48
 
49
49
  ## Token reach
50
50
 
51
- - `card` / `card-foreground` — surface and text; `border` — hairline (outline/card variants); `muted` / `muted-foreground` — description, captions, skeletons.
52
- - Accent stripe and `StatisticsIcon` variants — `primary` plus the status roles (`success`, `warning`, `destructive`/danger, `info`) at soft/solid/outline tones.
53
- - `StatisticsTrend` — **status roles only**. Known library deviation: the shipped component pins success intent to Tailwind `emerald-500` literals and has no success token; under `nurix` the trend/icon success tints are restyled in place to the package's `success` role (the no-jails law permits this; the token-reach report flags the literal), and under `native` the implementation uses status roles from the start.
54
- - Embedded visuals — `chart-1..5` for series color, chart chrome roles (`chart-grid`, `chart-tooltip-background/foreground`) for scaffolding; a spark/gauge/progress fill is never a hex literal. The default spark stroke (`var(--primary)`) already token-reaches.
55
- - The headline value inherits the type ramp's display size via the card `size`; mono/tabular figures come from the package's type roles, never a hardcoded family.
51
+ - `card` / `card-foreground` — surface and text; `border` — the card's hairline ring edge; `muted` / `muted-foreground` — description, captions, skeletons.
52
+ - Accent stripe and the header icon tint — `primary` plus the status roles (`success`, `warning`, `destructive`/danger, `info`) at soft/solid/outline strengths.
53
+ - Trend chip and `Stat` value — **status roles only**: both paint from the `--intent-*` tokens through `intent`, which a package's `success` / `warning` / `info` roles feed; the token-reach report flags a literal trend colour.
54
+ - Embedded visuals — `chart-1..5` for series color, chart chrome roles (`chart-grid`, `chart-tooltip-background/foreground`) for scaffolding; a spark/gauge/progress fill is never a hex literal. `Spark` paints in `currentColor` by default and takes `color="var(--chart-1)"` for a series role.
55
+ - The headline value sits in the display face (`--font-display`) with tabular numerals; a hero size comes from the type ramp through `className`, and mono/tabular figures come from the package's type roles, never a hardcoded family.
56
56
 
57
57
  ## When to use / composition notes
58
58
 
59
- Stat cards are the dashboard's KPI strip (4–6 tiles full-width per the blueprint shapes) and the record header's vital signs; the same grammar scales from a bare number-plus-trend to a gauge card with a legend, so one card system covers the whole surface. Every tile carries all five obligations — a number without a comparison is decoration, a tile without a door is a dead end. Use `size` to build hierarchy (one `lg` hero metric, `md` support tiles) instead of ad-hoc font sizes; use `justify="between"` with two sections for cards whose visual anchors the bottom edge. Wire the hover bridge whenever a chart lives in the card — it is the cheapest "designed, not templated" signal in the set.
59
+ Stat cards are the dashboard's KPI strip (4–6 tiles full-width per the blueprint shapes) and the record header's vital signs; the same grammar scales from a bare number-plus-trend to a gauge card with a legend, so one card system covers the whole surface. Every tile carries all five obligations — a number without a comparison is decoration, a tile without a door is a dead end. Build hierarchy from the type ramp (one hero metric a ramp step up, support tiles at the base) instead of ad-hoc font sizes; use `justify-between` with two sections for cards whose visual anchors the bottom edge. Wire the hover bridge whenever a hover-reporting chart lives in the card — it is the cheapest "designed, not templated" signal in the set. A bare row of KPIs with no card frame is `Stat`s on a `Row`.
60
60
 
61
- - **`primitives: nurix`** — `import { Statistics, StatisticsHeader, StatisticsIcon, StatisticsHeading, StatisticsTitle, StatisticsDescription, StatisticsActions, StatisticsMetric, StatisticsValue, StatisticsUnit, StatisticsTrend, StatisticsComparison, StatisticsVisual, StatisticsFooter, StatisticsCaption, StatisticsLink, useStatHoverBridge } from "@nurix/components/statistics";` (visuals: `@nurix/components/spark`, `/gauge`, `/progress-bar`; the animated number: `@nurix/components/animated-number`).
61
+ - **`primitives: nurix`** — composed, e.g.: `import { Card } from "@nurix/components/block/card"; import { Stat } from "@nurix/components/element/stat"; import { Chip } from "@nurix/components/element/chip"; import { Timestamp } from "@nurix/components/element/timestamp"; import { Link } from "@nurix/components/element/link";` (visuals: `Spark` from `@nurix/components/element/spark`, `Progress` from `@nurix/components/element/progress`, `Chart` from `@nurix/components/block/chart`; the animated number: `AnimatedNumber` from `@nurix/components/element/animated-number`; staleness: `useFreshness` from `@nurix/components/hooks/use-freshness`). `element/spark`, `block/chart` and `element/animated-number` reach optional peer families (`spark`, `charts`, `animated-number`) the consumer installs.
62
62
  - **`primitives: native`** — this recipe is the build spec: a fixed-height card with header / baseline-aligned metric row / visual slot / footer regions, a display-scale tabular headline that is correct on first paint (animation optional and additive), a sign-driven trend badge with an invert switch on status roles, skeletons that hold the footprint, degraded states that never fabricate numbers, and all series color drawn from the token artifact's `chart-1..5`.
@@ -1,26 +1,26 @@
1
1
  ---
2
2
  id: topbar
3
3
  class: d2
4
- sourced: 2026-07-12
5
- source: "@nurix/components — composed pattern: SidebarTrigger + Separator + Breadcrumb family + CommandTrigger + Button (the library's site-header pattern; no single Topbar export) — ~/dev/components"
4
+ sourced: 2026-10-07
5
+ source: "@nurix/components — composed pattern: Sidebar.Trigger + Separator + Breadcrumb + Command.Trigger + Button (the library's site-header pattern; no single Topbar export) — ~/dev/components"
6
6
  license: open
7
7
  ---
8
8
 
9
9
  # Chrome recipe — topbar
10
10
 
11
- The app-shell top bar: the sticky row above the content pane carrying the sidebar toggle, the breadcrumb trail, global search, and screen-level actions. In `@nurix/components` this is a **documented composition, not a single export** — every shipped app-shell demo assembles it from `SidebarTrigger`, `Separator`, the `Breadcrumb` family, `CommandTrigger`, and `Button`. Page-level mastheads below it are separate components (`Header` at `@nurix/components/header` for config-driven record headers; `PageHeader` for list/detail mastheads with a stat ledger) and are not part of this recipe.
11
+ The app-shell top bar: the sticky row above the content pane carrying the sidebar toggle, the breadcrumb trail, global search, and screen-level actions. In `@nurix/components` this is a **documented composition, not a single export** — every shipped app-shell demo assembles it from `Sidebar.Trigger`, `Separator`, `Breadcrumb`, `Command.Trigger`, and `Button`. Page-level headers below it are separate compositions (the library's masthead and record-card header recipes over `Block`, `Row`, `Breadcrumb`, `Stat` and `FieldGrid`) and are not part of this recipe.
12
12
 
13
13
  ## Anatomy
14
14
 
15
15
  | Slot | Library part | Spec |
16
16
  | --- | --- | --- |
17
17
  | Bar | flex row | Sticky (`top-0`), 48–56px tall, full width of the content pane, hairline bottom border. Optional glass treatment: `background` at ~95% alpha + backdrop blur. |
18
- | Sidebar toggle | `SidebarTrigger` | Leading icon button toggling the sidebar; reflects open state via `data-state`. |
18
+ | Sidebar toggle | `Sidebar.Trigger` | Leading ghost icon-only button toggling the sidebar; reflects open state via `aria-expanded` (the sidebar frame carries `data-state="expanded" \| "collapsed"`). |
19
19
  | Divider | `Separator orientation="vertical"` | 16px vertical hairline between toggle and breadcrumbs. |
20
- | Breadcrumbs | `Breadcrumb` → `BreadcrumbList` / `BreadcrumbItem` / `BreadcrumbLink` / `BreadcrumbSeparator` / `BreadcrumbPage` | The location trail. Ancestors are links; the last segment is `BreadcrumbPage` — plain text, `aria-current="page"`, never a link. Deep trails collapse middle segments into an ellipsis dropdown. |
20
+ | Breadcrumbs | `Breadcrumb` → `Breadcrumb.Item` | The location trail. Ancestors are links (`href`, or a router link through `render`); the last `Breadcrumb.Item` is the current page — plain text, `aria-current="page"` and `data-active`, never a link. Deep trails fold the middle segments behind an ellipsis menu (`maxItems`). |
21
21
  | Spring | flex spacer | Pushes the trailing cluster right. |
22
- | Search slot | `CommandTrigger { placeholder, shortcut }` | A bordered search chip with placeholder text and a right-aligned `⌘K` kbd hint; opens the command palette. |
23
- | Actions | `Button` (ghost/icon), overflow `DropdownMenu` | At most 3 visible screen-level actions; further actions collapse into an overflow menu. Theme toggle sits here. |
22
+ | Search slot | `Command.Trigger { children, shortcut? }` inside `Command.Dialog` | A bordered search chip with its text as children (default "Search") and a right-aligned `⌘K` / `Ctrl+K` kbd hint; opens the palette's `Command.Popup`. |
23
+ | Actions | `Button variant="ghost" isIconOnly`, overflow `DropdownMenu` | At most 3 visible screen-level actions; further actions collapse into an overflow menu. Theme toggle sits here. |
24
24
  | Identity | `Avatar` (optional) | Only when the shell has no sidebar user footer — one identity home per shell, never two. |
25
25
 
26
26
  ## The eight states
@@ -32,9 +32,9 @@ State is mostly per-control; the bar itself is stable chrome.
32
32
  | default | Bar on the content canvas with a hairline base; breadcrumb ancestors quiet, current page at full strength. | `background`, `border`, `muted-foreground` (ancestors), `foreground` (current) |
33
33
  | hover | Breadcrumb links sharpen to full foreground; icon buttons gain an accent fill. | `foreground`, `accent` + `accent-foreground` |
34
34
  | focus | Focus-visible ring on every control — toggle, links, chip, buttons. | `ring` |
35
- | active | Sidebar toggle mirrors the open sidebar (`data-state`); the current crumb is the persistent active marker; a pressed action holds its pressed fill. | `foreground`, `accent` |
35
+ | active | Sidebar toggle mirrors the open sidebar (`aria-expanded`); the current crumb is the persistent active marker; a pressed action holds its pressed fill. | `foreground`, `accent` |
36
36
  | disabled | Actions at ~50% opacity, no pointer events; breadcrumbs are never disabled. | reduced-alpha foreground |
37
- | loading | An in-flight screen action shows an inline spinner inside its own button. The bar never skeletons: breadcrumbs derive from the route and render immediately. | `muted-foreground` (spinner) |
37
+ | loading | An in-flight screen action shows its spinner inside its own button (`Button loading`). The bar never skeletons: breadcrumbs derive from the route and render immediately. | `muted-foreground` (spinner) |
38
38
  | error / empty | An empty trail renders the page title as the single `BreadcrumbPage`. Errors surface as toasts or content-level states — the bar itself hosts none; a degraded-environment banner may attach *beneath* the bar on a status-role tint. | `warning` / `destructive` (banner only) |
39
39
  | success | Transient: an action button briefly swaps to a check, or a toast confirms; nothing persistent lives in the bar. | `success` status role (transient) |
40
40
 
@@ -42,7 +42,7 @@ State is mostly per-control; the bar itself is stable chrome.
42
42
 
43
43
  - **Tab order**: the bar follows the skip link and precedes the content pane — toggle → breadcrumb links → search chip → actions → identity. Plain sequential tabbing; no roving focus is needed at this control count.
44
44
  - **Enter** activates crumbs and buttons; **Space** activates buttons.
45
- - **Cmd/Ctrl+B** toggles the sidebar from anywhere (provider-owned); the `SidebarTrigger` is its visible affordance.
45
+ - **Cmd/Ctrl+B** toggles the sidebar from anywhere (owned by `Sidebar.Provider`, rebound through `keyboardShortcut`); the `Sidebar.Trigger` is its visible affordance.
46
46
  - **cmdk path**: **⌘K / Ctrl+K** opens the command palette from anywhere; the search chip is the visible, clickable affordance and displays the shortcut as a kbd hint. Inside the palette: type to filter, arrows to move, Enter to run, Escape to close. Global navigation and infrequent actions belong in the palette, not as more topbar buttons.
47
47
  - Overflow menu: Enter/Space opens, arrows move, Escape closes and returns focus to the trigger.
48
48
 
@@ -59,7 +59,7 @@ State is mostly per-control; the bar itself is stable chrome.
59
59
 
60
60
  ## When to use / composition notes
61
61
 
62
- Every screen inside the shell gets the topbar; it is the constant that makes the sidebar collapsible (the toggle must survive collapse) and the command palette discoverable. Keep it thin — the topbar locates and searches; identity-weight content (record title, status, metadata, stats) belongs to the page masthead below it (`Header` / `PageHeader` under `nurix`), and datasets' own toolbars belong to their tables. The three-zone rhythm — locate left, search center-right, act right — is the invariant; what varies per product is only which trailing actions earn a permanent slot.
62
+ Every screen inside the shell gets the topbar; it is the constant that makes the sidebar collapsible (the toggle must survive collapse) and the command palette discoverable. Keep it thin — the topbar locates and searches; identity-weight content (record title, status, metadata, stats) belongs to the page header below it (the masthead or record-card recipe under `nurix`), and datasets' own toolbars belong to their tables. The three-zone rhythm — locate left, search center-right, act right — is the invariant; what varies per product is only which trailing actions earn a permanent slot.
63
63
 
64
- - **`primitives: nurix`** — composed, e.g.: `import { SidebarTrigger } from "@nurix/components/sidebar"; import { Breadcrumb, BreadcrumbList, BreadcrumbItem, BreadcrumbLink, BreadcrumbPage, BreadcrumbSeparator } from "@nurix/components/breadcrumb"; import { CommandTrigger } from "@nurix/components/command"; import { Separator } from "@nurix/components/separator"; import { Button } from "@nurix/components/button";`
64
+ - **`primitives: nurix`** — composed, e.g.: `import { Sidebar } from "@nurix/components/block/sidebar"; import { Breadcrumb } from "@nurix/components/compound/breadcrumb"; import { Command } from "@nurix/components/compound/command"; import { Separator } from "@nurix/components/element/separator"; import { Button } from "@nurix/components/element/button"; import { DropdownMenu } from "@nurix/components/compound/dropdown-menu";` — parts hang off each namespace (`Sidebar.Trigger`, `Breadcrumb.Item`, `Command.Dialog` / `Command.Trigger` / `Command.Popup`, `DropdownMenu.Trigger` / `DropdownMenu.Popup`).
65
65
  - **`primitives: native`** — this recipe is the build spec: a sticky 48–56px flex row with the slot order above, a real `<nav aria-label="Breadcrumb">` trail with `aria-current="page"` on the last segment, a search affordance wired to the stack's palette (or a plain search route) showing its shortcut, and the state/keyboard/token contracts implemented with the incumbent stack's own primitives.
@@ -5,7 +5,7 @@ One source, two derivations: the project's `design.md` is canonical; the artifac
5
5
  | Artifact | Emit with | Carries | Compiled by |
6
6
  | --- | --- | --- | --- |
7
7
  | `design.tokens.css` | `node scripts/validate-design-md.mjs --project . --emit --write` | the color roles in both themes, the type ramp, elevation, the motion base, layout | the validator |
8
- | `personality.css` — `primitives: nurix` only | `node scripts/validate-design-md.mjs --project . --emit-personality --write` | the six traits compiled into the token families every `@nurix/components` component reads: `--density-*`, `--motion-duration-*`, `--radius-scale`, `--elevation-*`, `--texture-*`, `--accent`/`--ring` | `compileToCss` from `@nurix/components/lib/personality`, reached through the validator from the project's own install |
8
+ | `personality.css` — `primitives: nurix` only | `node scripts/validate-design-md.mjs --project . --emit-personality --write` | the six traits compiled into the token families every `@nurix/components` component reads: `--density-*`, `--motion-duration-*` / `--motion-ease-*` / `--hold-dwell`, `--radius-scale` / `--radius-pill` / `--ring-width` / `--ring-offset` / `--edge-hairline` / `--edge-stroke`, `--elevation-*`, `--texture-*`, `--accent` / `--ring` | `compileToCss` from `@nurix/components/lib/personality`, reached through the validator from the project's own install |
9
9
 
10
10
  ## Coverage
11
11
 
@@ -19,15 +19,15 @@ One source, two derivations: the project's `design.md` is canonical; the artifac
19
19
  personality:
20
20
  density: compact # compact | regular | airy — control height and padding, container padding, gaps
21
21
  pace: 0.3 # 0 still · 0.5 the library's neutral timing · 1 lively
22
- edge: sharp # sharp | regular | soft — the radius scale
22
+ edge: sharp # sharp | regular | soft — the radius scale; sharp also thins ring, hairline and stroke, and squares pills
23
23
  depth: flat # flat | layered | elevated — the elevation ladder
24
24
  texture: clean # clean | grainy — grain opacity on effect surfaces
25
- tone: neutral # neutral | expressive — whether the accent re-anchors on the primary
25
+ accent: neutral # neutral | expressive — whether the accent re-anchors on the primary
26
26
  ```
27
27
 
28
- - **§4–§7 are read into the traits, never into pixels.** A §6 of "flat, hairline-separated" is `depth: flat`; a §7 of "utilitarian, nothing animates twice a day" is `pace` at 0.2–0.35; an operator §4 is `density: compact`. Declare only what differs from neutral (regular · 0.5 · regular · layered · grainy · neutral). An absent block ships the library's defaults and the validator warns (`dmw-05`) — a project that never declared its feel is how these families go unused.
28
+ - **§4–§7 are read into the traits, never into pixels.** A §6 of "flat, hairline-separated" is `depth: flat`; a §7 of "utilitarian, nothing animates twice a day" is `pace` at 0.2–0.35; an operator §4 is `density: compact`. Declare only what differs from neutral (regular · 0.5 · regular · layered · grainy · neutral). An absent block ships the library's defaults and the validator warns (`dmw-09`) — a project that never declared its feel is how these families go unused.
29
29
  - **`motion.duration` and `motion.easing` are the app's own vocabulary** (`--duration-panel`, `--ease-standard`, read by app-owned animation); the library's components never read them. `pace` is what retimes the library — and, through the library's default transition duration, every plain Tailwind `transition-*` utility in the app. State both from the same §7 reading.
30
- - **The block is closed-vocabulary and fails closed** (`dmv-12`): an unknown trait, an out-of-range pace, or a block under `primitives: native` (nothing there reads it) fails validation.
30
+ - **The block is closed-vocabulary and fails closed** (`dmv-12`): an unknown trait (a `tone` key among them — the accent axis is `accent`, and the library's compiler rejects `tone`), an out-of-range pace, or a block under `primitives: native` (nothing there reads it) fails validation.
31
31
  - **The compiler is the project's own install.** `--emit-personality` resolves `@nurix/components` by walking up from the design.md, then one and two levels into the workspace (`packages/<app>`), because a pnpm root hoists nothing. Several installed versions is an ambiguity the app package resolves: `--project . --emit-personality --write --lib packages/<app>`. The artifact's header names the library version that compiled it, so a library upgrade re-emits by drift, never by hand.
32
32
  - **The artifact is committed, never injected.** `personality.css` is the product's artifact — inspectable, diffable, drift-checked at the ship gate — imported after the library's `styles.css` so its `:root` wins on order. `<PersonalityStyle>` from the same subpath serves a runtime switch (a density toggle, a preview) and never replaces the artifact.
33
33
 
@@ -38,7 +38,7 @@ Each prevents a known silent-failure class:
38
38
  - **Specificity armor.** Theme blocks are `:root:not(.dark)` and `:root.dark` — both specificity (0,2,0) — keyed on the `.dark` class the library compiles into its own stylesheet. A `data-theme` attribute keys none of the library's `dark:` variants, its theme toggle, or its toaster: an app on the attribute inverts its tokens and renders mixed library chrome.
39
39
  - **Theme-invariant tokens sit in a bare `:root`.** Only the colour roles are themed. Font stacks, the type ramp, radii, shadows, motion and layout emit into a bare `:root` block ahead of the two theme blocks. Behind `:root:not(.dark)` they match nothing once `.dark` is on the root, and every `font-size: var(--text-…)` collapses to an inherited 16px in dark mode while the colours still look right.
40
40
  - **The ramp emits Tailwind's composite sub-keys.** `--text-<step>` with `--line-height`, `--font-weight` and `--letter-spacing` sub-keys are exactly what the `text-<step>` utility applies. A `-size`/`-line`/`-weight` suffix form generates junk utilities and leaves `text-<step>` undefined. The `--font-feature-settings` and `--font-family` sub-keys are inert for the utility and are read through `var()`. The dense operator tier's steps are optional: absent steps emit nothing and warn once per package (`dmw-08`).
41
- - **The tone trio.** `success`, `warning`, and `info` reach the library as `--tone-success|warning|info` — what a status dot (`<Chip prefix="dot" color="var(--tone-success)">`) and the copy button's check read, falling back to the library's own hue when a role is absent. Declare all three, in both themes — a missing `info` (`dmw-06`) puts the library's blue beside the package's green and amber.
41
+ - **The intent trio.** `success`, `warning`, and `info` reach the library as `--intent-success|warning|info` (and `destructive` as `--intent-danger`) — what a status dot (`<Chip prefix="dot" intent="success">`) and the copy button's check read, falling back to the library's own hue when a role is absent. Declare all three, in both themes — a missing `info` (`dmw-10`) puts the library's blue beside the package's green and amber.
42
42
  - **Font binding by derivation.** Font stacks bind to installed packages under **registered family names** (`"Inter Variable"`, not `"Inter"`), each with its declared fallback stack — otherwise the flagship font silently never renders. Install the font packages the design.md declares (e.g. Fontsource) and verify the name matches what the package registers. The library's `font-sans` / `font-mono` / `font-display` utilities compile to `inherit` on purpose (a ramp, never a face): the app declares its own theme keys in a plain `@theme` pointing at the emitted slots — `--font-body: var(--font-sans)`, `--font-heading: var(--font-display)`, `--font-code: var(--font-mono)` — and sets the ambient face on `body` in `@layer base`.
43
43
  - **Every authored token group reaches a named consumer.** An authored ramp with no emitter vanishes without a trace — wire each group (colors → theme classes/utilities, ramp → text styles, elevation → radius/shadow utilities, motion → duration/easing vars) and let the ship gate's reach check prove it. The personality families reach the library by construction; the app's `--duration-*` / `--ease-*` reach app-owned animation code, or they are unused.
44
44
  - **Compile the emitted CSS once per change** — a stray utility string inside a CSS comment can defeat a fully green byte-level check.
@@ -1,27 +1,39 @@
1
1
  # validate-design-md.mjs — the design.md system's validator + token emitter
2
2
 
3
- **CLI:** `node validate-design-md.mjs <file|folder> [--mode admission|build|ship] [--json] [--emit]`
3
+ **CLI:** `node validate-design-md.mjs (<file|folder> | --project [dir]) [--mode admission|build|ship] [--json] [--emit] [--emit-personality] [--write] [--lib <folder>]`
4
4
  File mode validates one design.md. Folder mode validates every `.md` in the tree (skipping `SCHEMA.md`) plus `index.csv` integrity.
5
- `admission` and `build` run the same checks; `ship` adds the dmv-11 drift check against a `design.tokens.css` beside the input file.
5
+ `--project [dir]` validates the project's own fork, located by the repo's shape: `packages/common/design.md` in a multi-package
6
+ workspace, `src/common/design.md` in a single-package repo (`dir` defaults to the cwd; an absent file fails closed, naming the path).
7
+ `admission` and `build` run the same checks; `ship` adds the dmv-11 drift check against a `design.tokens.css` beside the input file,
8
+ and dmv-12's drift check against `personality.css`.
6
9
  `--json` prints `{file, fails:[{id,detail}], warns:[{id,detail}]}` (array in folder mode). Exit 0 on pass (warns allowed), 1 on any FAIL.
7
10
 
8
11
  **Emit:** `--emit` (single file) compiles the token artifact from the `yaml design-values` block to stdout — deterministic, byte-stable;
9
- redirect to `design.tokens.css`. Shape: `:root, :root[data-theme="light"]` + `:root[data-theme="dark"]` blocks (specificity armor, both
10
- 0,2,0) carrying the 36 color roles as `--<role>`; the light block also carries `--font-display/sans/mono` (registered names quoted, with
11
- fallback stacks), `--text-<step>-size/-line/-weight` (+ `-family/-tracking/-features`), `--radius-*`, `--shadow-*`, `--duration-*`,
12
- `--ease-*`, and layout vars (`--space-base`, `--sidebar-width[-collapsed]`, `--topbar-height`, `--side-panel-width[-wide]`, `--content-max-width`).
12
+ redirect to `design.tokens.css`, or add `--write` to write it beside the design.md. Shape: three blocks. A bare `:root` carries every
13
+ theme-invariant token — `--font-display/sans/mono` (registered names quoted, with fallback stacks), the ramp as `--text-<step>` with
14
+ `--line-height`/`--font-weight`/`--letter-spacing` sub-keys, `--radius-*`, `--shadow-*`, `--duration-*`, `--ease-*`, and layout vars
15
+ (`--space-base`, `--sidebar-width[-collapsed]`, `--topbar-height`, `--side-panel-width[-wide]`, `--content-max-width`). Then
16
+ `:root:not(.dark)` and `:root.dark` (specificity armor, both 0,2,0) carry the color roles as `--<role>`.
17
+ `--emit-personality` (`primitives: nurix`) hands the design.md's `personality` traits to `@nurix/components`' own compiler
18
+ (`compileToCss` from `@nurix/components/lib/personality`) and prints `personality.css`, or writes it with `--write`; `--lib <folder>`
19
+ names where the library is installed when the resolver cannot tell. Any FAIL suppresses either artifact.
13
20
 
14
21
  **Fails (close the gate):** dmv-01 frontmatter strict, per-tier keys, id = filename · dmv-02 the 12 sections in order, non-empty,
15
22
  plus trailing design-values block · dmv-03 value block parses as YAML · dmv-04 coverage: 36 roles × both themes, ramp, register-scope,
16
23
  elevation, motion base, iconography, charts, layout · dmv-05 contrast on every declared pairing (body ≥ 4.5:1; primary/accent/
17
24
  destructive/sidebar-primary fills ≥ 3:1, warn under 4.5) · dmv-06 no image reference anywhere · dmv-07 index.csv ↔ files, both
18
25
  directions, no duplicates · dmv-08 registered font names + fallback stacks · dmv-09 no claude.md (any case) · dmv-10 project tier:
19
- `primitives: nurix|native` + `source-package` · dmv-11 (ship) emitted artifact byte-matches recompilation.
26
+ `primitives: nurix|native` + `source-package` · dmv-11 (ship) emitted artifact byte-matches recompilation · dmv-12 the
27
+ `personality` block (`primitives: nurix` only) uses the six traits `density|pace|edge|depth|texture|accent` with their closed values
28
+ (`pace` a number 0..1; a `tone` key fails naming `accent`, as the library's compiler rejects it), and under `--mode ship`
29
+ `personality.css` byte-matches the recompiled artifact.
20
30
 
21
31
  **Warns (recorded, never close):** dmw-01 background in the anti-cream band (L 0.84–0.97, C < 0.06, hue 40–100) · dmw-02 neutral
22
32
  surfaces pure gray (C < 0.005) or over-tinted (C > 0.015) · dmw-03 untinted black shadow above 0.15 alpha · dmw-04 ease-in entrance
23
33
  or duration over its element-class budget · dmw-05 display face granted a data register · dmw-06 dark ladder steps < ~0.04 L or hue
24
- drift · dmw-07 accent chroma delta between themes > 0.06.
34
+ drift · dmw-07 accent chroma delta between themes > 0.06 · dmw-08 the dense operator tier unadopted (`typography.ramp` omits the
35
+ optional steps) · dmw-09 (`primitives: nurix`) no personality block, so the library's neutral traits ship · dmw-10 (`primitives: nurix`)
36
+ the `info` role undeclared, so `--intent-info` falls back to the library's own hue.
25
37
 
26
38
  Stdlib-only: vendors its own YAML-subset parser and oklch → sRGB → WCAG-contrast math. The exact contrast pair table, thresholds,
27
39
  and emission order live in the script's header comment.
@@ -62,13 +62,14 @@
62
62
  // dmv-11 (--mode ship) drift: the token artifact recompiled from the design.md byte-matches the
63
63
  // design.tokens.css beside it.
64
64
  // dmv-12 the personality block (primitives: nurix): a mapping over the six closed-vocabulary traits
65
- // density|pace|edge|depth|texture|tone (pace a number 0..1); absent under native. In --mode
65
+ // density|pace|edge|depth|texture|accent (pace a number 0..1; a `tone` key fails naming
66
+ // `accent`, as the library's compiler rejects it); absent under native. In --mode
66
67
  // ship, personality.css beside the design.md byte-matches the recompiled artifact, and no
67
68
  // personality.css exists without a block (a hand-authored layer).
68
69
  //
69
70
  // WARN checks (never close): dmw-01 anti-cream backgrounds · dmw-02 neutral tint · dmw-03 untinted shadows ·
70
- // dmw-04 motion budgets · dmw-05 personality block absent on a nurix project (the library's neutral
71
- // traits ship) · dmw-06 `info` role undeclared on a nurix project (--tone-info falls back to the
71
+ // dmw-04 motion budgets · dmw-09 personality block absent on a nurix project (the library's neutral
72
+ // traits ship) · dmw-10 `info` role undeclared on a nurix project (--intent-info falls back to the
72
73
  // library's own hue).
73
74
  //
74
75
  // dmv-05 — the exact contrast pair table (both themes; WCAG 2.1 relative-luminance contrast; colors
@@ -106,6 +107,8 @@
106
107
  // dmw-07 accent chroma delta between themes > 0.06 (identity likely not held across themes).
107
108
  //
108
109
  // dmw-08 the dense operator tier is unadopted: typography.ramp omits optional steps (one warn per package).
110
+ // dmw-09 (primitives: nurix) no personality block — the library's neutral traits ship.
111
+ // dmw-10 (primitives: nurix) the `info` role undeclared — --intent-info falls back to the library's own hue.
109
112
  //
110
113
  // Emitted artifact shape (deterministic, byte-stable): THREE blocks. A bare `:root` carries every
111
114
  // theme-invariant group — --font-display/sans/mono (registered names quoted, fallback stacks appended),
@@ -148,18 +151,18 @@ const RAMP_STEPS_OPTIONAL = ["title", "h3", "prose", "control-lg", "dense", "con
148
151
  const RAMP_STEPS = [...RAMP_STEPS_REQUIRED, ...RAMP_STEPS_OPTIONAL];
149
152
  const FONT_SLOTS = ["display", "sans", "mono"];
150
153
  // Optional color roles: emitted only when declared (both themes or neither). `info` completes the
151
- // success/warning/info trio @nurix/components' status dots read through --tone-*.
154
+ // success/warning/info trio @nurix/components' status dots read through --intent-*.
152
155
  const OPTIONAL_COLOR_ROLES = ["info"];
153
156
  // The personality block — six closed-vocabulary traits @nurix/components compiles into the token
154
157
  // families its components read (lib/personality). The library owns the vocabulary; this mirrors it.
155
- const PERSONALITY_ORDER = ["density", "pace", "edge", "depth", "texture", "tone"];
158
+ const PERSONALITY_ORDER = ["density", "pace", "edge", "depth", "texture", "accent"];
156
159
  const PERSONALITY_TRAITS = {
157
160
  density: ["compact", "regular", "airy"],
158
161
  pace: null, // the one scalar: 0 still · 0.5 the library's neutral timing · 1 lively
159
162
  edge: ["sharp", "regular", "soft"],
160
163
  depth: ["flat", "layered", "elevated"],
161
164
  texture: ["clean", "grainy"],
162
- tone: ["neutral", "expressive"],
165
+ accent: ["neutral", "expressive"],
163
166
  };
164
167
  const THEMES = ["light", "dark"];
165
168
  const TIERS = ["default", "variant", "brand", "project"];
@@ -750,13 +753,13 @@ function checkCoverage(values, report) {
750
753
  else if (layout.base == null) missing("layout.base");
751
754
  }
752
755
 
753
- // dmv-12 — the personality block, and the two nurix-project warns (dmw-05 block absent, dmw-06 info absent).
756
+ // dmv-12 — the personality block, and the two nurix-project warns (dmw-09 block absent, dmw-10 info absent).
754
757
  function checkPersonality(front, values, report) {
755
758
  const isNurixProject = !!front && front.tier === "project" && front.primitives === "nurix";
756
759
  const p = values.personality;
757
760
  if (p === undefined) {
758
761
  if (isNurixProject) {
759
- warn(report, "dmw-05", "personality block absent — @nurix/components ships its neutral traits (regular · pace 0.5 · regular · layered · grainy · neutral); declare the traits §4–§7 imply so density, motion, radius and depth reach the components");
762
+ warn(report, "dmw-09", "personality block absent — @nurix/components ships its neutral traits (regular · pace 0.5 · regular · layered · grainy · neutral); declare the traits §4–§7 imply so density, motion, radius and depth reach the components");
760
763
  }
761
764
  } else if (!isObj(p)) {
762
765
  fail(report, "dmv-12", "personality must be a mapping of traits");
@@ -765,7 +768,9 @@ function checkPersonality(front, values, report) {
765
768
  fail(report, "dmv-12", "personality is compiled by @nurix/components — under primitives: native nothing reads it; drop the block or switch primitives");
766
769
  }
767
770
  for (const [k, v] of Object.entries(p)) {
768
- if (!(k in PERSONALITY_TRAITS)) {
771
+ if (k === "tone") {
772
+ fail(report, "dmv-12", `personality.tone is not a trait — the accent trait carries that axis (accent: ${PERSONALITY_TRAITS.accent.join("|")}); the library's compiler rejects tone`);
773
+ } else if (!(k in PERSONALITY_TRAITS)) {
769
774
  fail(report, "dmv-12", `personality.${k} is not a trait (${PERSONALITY_ORDER.join("|")})`);
770
775
  } else if (k === "pace") {
771
776
  const n = typeof v === "number" ? v : Number(v);
@@ -787,7 +792,7 @@ function checkPersonality(front, values, report) {
787
792
  const bad = THEMES.filter((t) => !parseColor(color[t][role]));
788
793
  if (bad.length) fail(report, "dmv-04", `color.${role} unparseable in ${bad.join(", ")}`);
789
794
  } else if (isNurixProject) {
790
- warn(report, "dmw-06", `color.${role} undeclared — @nurix/components' --tone-${role} falls back to the library's own hue beside your success/warning; declare it in both themes`);
795
+ warn(report, "dmw-10", `color.${role} undeclared — @nurix/components' --intent-${role} falls back to the library's own hue beside your success/warning; declare it in both themes`);
791
796
  }
792
797
  }
793
798
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "harness": "delivery",
3
3
  "name": "delivery",
4
- "version": "0.2.42",
4
+ "version": "0.2.43",
5
5
  "files": [
6
6
  ".prettierignore",
7
7
  ".prettierrc.json",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "harness": "etna",
3
3
  "name": "etna",
4
- "version": "1.5.32",
4
+ "version": "1.5.33",
5
5
  "files": [
6
6
  ".prettierignore",
7
7
  ".prettierrc.json",
@@ -14,5 +14,5 @@
14
14
  "origin": "https://cdn.nustack.io",
15
15
  "track": "latest"
16
16
  },
17
- "digest": "2e85809b94e3"
17
+ "digest": "8de8cea88938"
18
18
  }