Skip to content
Nizivo UI

Deviations from Figma

Where the code differs from the Figma file, and why. Generated from the files that own each difference.

Nizivo follows the Figma library, except where a value fails WCAG 2.2 AA, where Figma has no value yet, or where a browser on the support floor cannot do what the design asks. Every difference is listed here, read at build time from the file that owns it; none of it is typed by hand.

Contrast overrides

Figma variables that the code points somewhere else, mostly to reach 4.5:1 for text and 3:1 for non-text. The contrast matrix in packages/tokens/CONTRAST.md checks every pair with any of these in place.

None. Every Figma variable is used as Figma defines it: the 14 contrast overrides the code carried were adopted in Figma with the same values, and removed from the code on 2026-10-18.

Proposals

None. Every token is a Figma variable or code-owned by nature: Figma adopted 18 of the 19 proposals on 2026-10-18, and the last one, --nz-text-accent, was removed before 0.2.0 (use --nz-text-brand on page surfaces and --nz-text-inverse-accent on bg/inverse).

Builder-chosen values

D-i: Progress fills for tones Figma does not draw (§15.3)

Adopted 2026-10-14 (builder default; the maintainer can override). ProgressBar, Meter and ProgressRing paint tone="neutral" with icon/secondary and tone="success" with icon/success until design supplies the fills

Why: chart/neutral is 1.36:1 (light) and 1.41:1 (dark) on chart/track, under 1.4.11's 3:1, and the neutral example shows no value text, so the fill is the only cue. icon/secondary reaches 3.75 / 5.32 and icon/success 4.06 / 5.87 on the track. Figma has no chart/success

Evidence: progress-bar.css, meter.css, progress-ring.css; packages/tokens/CONTRAST.md. Source: docs/ARCHITECTURE.md §23.4

Values Figma leaves to the builder

Where the week 2 Figma facts give no token for a value (off-scale or unspecified), the builder chose one. The Figma column is what the facts file records, where ! marks a value typed into the Figma file rather than bound to a variable; the Chosen column is what the stylesheet ships. Where a CSS hook sets the value, the component name links to its hooks.

ComponentPropertyFigmaChosen
KbdPadding

1 ! / 6 ! / 1 ! / 6 !

inline 6 = --nz-space-1-5; block 1 is off-scale, builder decides (the 24 px height comes from 20 px text + 1 + 1 + 1 px strokes inside)

Block 1px, a literal (no space token is 1 px), which keeps the 24 px box; inline --nz-space-1-5 (6 px).
BadgePadding

3 ! / 10 ! / 3 ! / 10 !

off-scale, no snap in figma/02; builder decides (26 px height = 20 px label line + 3 + 3)

Snapped to --nz-space-0-5 (2 px) block and --nz-space-2 (8 px) inline, so the badge is 24 px tall, not 26.
Hook: --nz-badge-padding-inline
Card–

Difference from the §10.5 sketch: the sketch draws --nz-border-subtle on every tone; Figma's Subtle and Inverse have no stroke. Builder decides; list the choice on the Deviations page.

Every tone keeps the 1 px border box, as Surface does; Subtle and Inverse paint it transparent, so they draw no stroke and no tone changes size.
Hook: --nz-card-border
DeltaPadding

top 4 !, end 10 !, bottom 4 !, start 8 ! (icon side 8, text side 10)

4 → --nz-space-1, 8 → --nz-space-2; 10 is off-scale ("space has 8 and 12", figma/04 §4.1), builder decides

Both sides --nz-space-2 (8 px), since 10 is off the scale; block --nz-space-1 (4 px).
Hook: --nz-delta-padding-inline
ProgressBarSuccess, Neutral

no Figma spec

builder decides from existing tokens; list on the Deviations page

Settled by Figma on 2026-10-18: success fills with --nz-chart-success and neutral with --nz-chart-neutral, the chart variables Figma added (both reach 3:1 on the track in both themes). Until then the code used --nz-icon-success and --nz-icon-secondary, the same colours. Meter and ProgressRing use the same fills.
Hook: --nz-progress-bar-fill
Meter–

Shared tokens: --nz-chart-track, --nz-chart-primary, --nz-font-label-m, --nz-font-numeric-m, gap --nz-space-2. Track height is 8 in the component and 6 in the compositions; neither has a token, so builder decides.

--nz-space-1-5 (6 px), the compositions' track; ProgressBar's track is --nz-space-2 (8 px), the component's.
Hook: --nz-meter-height
Skeleton–

Radii: 8 → --nz-radius-sm; 999 → --nz-radius-full; 6 is off-scale (figma/07 §8 suggests "radius/2xs 6, or snap blocks to 4/8"; builder decides). Gaps: 12 → --nz-space-3, 8 → --nz-space-2; KPI padding 20 → --nz-space-5.

Radius 6 snapped to --nz-radius-xs (4 px); 8 and 999 map to --nz-radius-sm and --nz-radius-full.
Hook: --nz-skeleton-radius
InlineError–

Glyph, its size and the icon-to-text gap: no Figma spec, so builder decides. Evidence: Alert Critical uses Icon/circle-x 738:141 in icon/critical; 14 px text pairs with 16 px icons elsewhere (Delta, Input leading icon). Moving the message to 12 px is a one-line hook change listed on the Deviations page (ARCHITECTURE §15.1).

The glyph defaults to the internal circle-x (Lucide, Feather-derived; icon replaces it). It is drawn at --nz-size-icon-sm (16 px) in --nz-icon-critical, one line box tall, --nz-space-1-5 (6 px) from the message.
Hook: --nz-inline-error-gap

Source: docs/figma/11-w2-server-safe-components.md

Week 3: forms, IconButton and Tooltip

The week 3 Figma facts list 29 values that are off the scale, have no token or are not drawn. The Chosen column is what ships, checked against the file it cites.

#ComponentFigmaChosen
1Checkbox, Radio, Switchrow gap 10--nz-space-2 (8 px) between the control and its label on all three. Rows inside a Fieldset or RadioGroup sit --nz-space-3 (12 px) apart.
Hook: --nz-checkbox-gap
2Checkbox, Radiobox 20 × 20A private 1.25rem (20 px) for the box and the circle.
Hook: --nz-checkbox-size
3Checkboxglyph 14 px (off the icon scale)Kept at 14 px (0.875rem), as drawn: 16 px crowds the 20 px box.
4Radiodot 8 × 8; disabled-selected dot white on bg/muted (1.15:1)The 8 px dot is the input background inside a 5 px inset ring of the fill; the disabled-selected dot is --nz-text-disabled on --nz-bg-muted.
Hook: --nz-radio-accent
5Switchtrack 36 × 20, knob 16, pad 2A private 1rem thumb and a --nz-space-0-5 pad; the track (36 × 20) and the travel (16 px) derive from them.
6Switchknob {neutral/0} primitive--nz-bg-surface, which passes 3:1 on both tracks in both themes.
Hook: --nz-switch-thumb-bg
7Switchknob shadow raw #000 18 %--nz-elevation-1.
Hook: --nz-switch-thumb-shadow
8Switchon-disabled track {green/200} primitive--nz-bg-brand-muted.
9Switchcolour cross-fade under reduced motionThe track colour fades on --nz-duration-fade, so a 120 ms fade survives reduced motion; the thumb moves on the spring pair.
10SegmentedControltrack padding 3, radius 10The track is a control height (32 / 40 / 48) with a --nz-space-1 (4 px) inset (--nz-space-0-5 at sm) and --nz-radius-md, so md items are 32 px with concentric 8 px radii.
Hook: --nz-segmented-control-radius
11SegmentedControlitem inline padding 14--nz-space-3 (12 px); --nz-space-2 at sm and --nz-space-4 at lg.
12SegmentedControlbold label width jitter (56 → 57)Reserved: a hidden copy of each label in the strong weight sizes the item, so selecting never moves its neighbours.
13SegmentedControlfill-width track with hugging items (mobile)Opt-in fullWidth: equal shares of the width, never narrower than an item's text.
14SegmentedControlsizes, icon-only items, disabledSizes sm, md and lg on the control scale; icon-only items named by aria-label; disabled on the group or one item, a selected disabled item keeping a --nz-border-subtle ring; invalid, a 1 px --nz-border-critical ring on the track. No sliding thumb: the selection changes with a colour fade.
15Input, Textareainvalid at rest (only drawn with the halo)1 px --nz-border-critical, no halo at rest (the halo is the focus decoration).
16Inputfocus border 1.5 border/focus + haloThe 2 px outline from nz.base (on the box, focusTarget within) plus the --nz-focus-halo decoration, --nz-focus-halo-critical while invalid; the border colour does not change. Textarea and NativeSelect draw the same halo.
17Inputsizes (40 only)sm, md and lg on the control scale, as Select; sm also takes --nz-radius-sm and a --nz-space-1-5 gap.
18Inputleading-icon colour (not read)--nz-icon-secondary; text affixes are --nz-text-secondary.
Hook: --nz-input-icon-fg
19Inputdisabled border border/subtle vs Select's border/default--nz-border-subtle on Input, Textarea, NativeSelect and (since its week 6 visual pass) Select, a disabled Fieldset included.
20Fielddisabled help text stays text/tertiaryKept --nz-text-tertiary; only the label dims when the Field is disabled.
Hook: --nz-field-description-fg
21Fieldrequired marker (Q30), error glyph size and gap, legend styleRequired mark * in --nz-text-critical, hidden from assistive technology (the control is required; handoff Q30 still open). The error glyph is InlineError's (16 px, 6 px gap). The legend is Label/M, --nz-space-2 above the rows.
Hook: --nz-field-required-fg
22Textareablock padding, min rows, resize, counter lookSettled by Figma's Textarea set (7367:316) on 2026-10-18: --nz-space-3 (12 px) padding on every side and a 112 px minimum height (--nz-textarea-min-block-size lowers it); 3 rows, vertical resize. It grows with its content where field-sizing is supported (never below the larger of its rows and the minimum); Firefox 129 and Safari 17.5 keep that height. No counter yet. Until then the block padding was --nz-space-2 with no minimum.
Hook: --nz-textarea-padding-block
23NativeSelectFigma end padding 10, Body/M Strong value, icon/secondaryMatches the shipped Select: --nz-space-3 both sides, --nz-font-label-m value (--nz-font-label-l, 16 px, under a coarse pointer, M25), --nz-text-secondary chevron.
Hook: --nz-native-select-font
24NativeSelectleading icon (Figma default on)A leadingIcon prop, off by default, as on Select.
25IconButtonPrimary without --nz-elevation-buttonNo elevation on Primary, as Figma draws it.
26IconButtonsm and lg icon size16 px at sm, 20 px at md, 24 px (--nz-size-icon-lg) at lg.
27IconButtonSecondary border--nz-border-default, as Button.
Hook: --nz-icon-button-border
28Tooltiparrow 12 × 7, offset, max width, surface under the inverse schemeNo arrow; at most 20rem wide; --nz-bg-surface-raised under the inverse scheme; the shortcut is plain Mono text in --nz-text-tertiary.
Hook: --nz-tooltip-bg
29All160 ms hover, 220 ms toggleBoth map to --nz-duration-fast; a colour change that must survive reduced motion uses --nz-duration-fade (item 9).

Source: docs/figma/12-w3-forms.md

Week 4: Menu, Tabs, TabNav and the overlays

The week 4 Figma facts list 22 values that are off the scale, have no token or are not drawn. The Chosen column is what ships, checked against the file it cites.

#ComponentFigmaChosen
1Menu, Selectrow height 36Menu rows have a 2.25rem (36 px) minimum height around --nz-space-1-5 (6 px) block padding, so a one-line row is 36 px. Select rows shared the recipe from the week 6 select.css pass until 2026-10-18, when they took Figma's Select Option (item 11).
2Menu, Selectrow inline padding and gap 10--nz-space-3 (12 px) inline padding and --nz-space-2 (8 px) between icon, label, shortcut and check, on Menu and Select rows alike; the group label starts at --nz-space-3 too.
3Menuchecked + highlighted, critical + highlighted, disabled (not drawn)A checked row is --nz-bg-brand-subtle with --nz-text-brand; under the pointer it swaps to the --nz-bg-hover fill and keeps --nz-text-brand (7.91:1 light, 5.87:1 dark), so a hovered checked row still answers the pointer. A critical item keeps --nz-text-critical on the --nz-bg-hover highlight. Disabled is --nz-text-disabled, its icon and shortcut included; in forced colours it is GrayText.
4Menuradio item mark (not drawn)The same trailing check as a checkbox item; no dot. A menu that mixes plain items with checkable ones reserves the check column on the plain items too, so every shortcut lines up.
5Menucheck at the end, replacing the shortcutThe check sits at the end, after the shortcut, and the shortcut stays visible (--nz-text-tertiary, 5.71:1 light and 6.67:1 dark on --nz-bg-brand-subtle). Figma hides the shortcut on a selected row.
6Menukeyboard focus (fill is 1.08 / 1.17)The generated 2 px outline, drawn inside the row (data-focus-inset, offset −2 px) because rows sit 2 px apart in a scrolling list. It follows only the keyboard report from React Aria (focusTarget: "engine"), so the pointer never draws it, though the engine moves DOM focus to the hovered row. The --nz-bg-hover fill (1.07:1 light, 1.16:1 dark) is a pointer hint only.
7Menupopup minimum width (hug in Figma)At least 12rem, and never narrower than the trigger.
8Menu, Selectdark double edge (border + elevation ring)Both kept on Menu: the 1 px --nz-border-subtle border and --nz-elevation-3, so dark mode shows the double edge Figma draws; --nz-menu-border and --nz-menu-shadow change either. The Select popup has the same border and elevation since week 6, with no public hook yet.
Hook: --nz-menu-shadow
9Menuplacement and offset (not drawn)side="bottom", align="start" and sideOffset={8}, as Select; each is a prop.
10Menu160 ms highlight fadeInstant: the highlight follows the keyboard and the pointer with no fade, so arrowing through a list never lags.
11Selectpopup radius, elevation, gaps, selected look, group label, separator, entry motionWeek 4 changed the entry motion: the popup enters on the spring pair with --nz-scale-enter and fades out, like Menu and Dialog (§12.2), so reduced motion keeps its fade. Since 2026-10-18 (M36) the popup follows Figma's Select Menu (7356:406) and its parts (7356:401, 7356:402, 7356:404) instead of repeating Menu's recipe: --nz-radius-md, a 1 px --nz-border-subtle border and --nz-elevation-4, 4 px below the trigger; 36 px Label/M rows with 8 / 12 px padding and no gap, the inset ring; the selected row Label/M Strong in --nz-text-primary with a trailing --nz-icon-brand check (the check and the weight, not the colour, carry the state; indicator replaces it); Label/S --nz-text-secondary group labels; dividers with 4 px above and below. One value Figma does not draw is kept: the selected row's --nz-bg-brand-subtle tint (the maintainer's choice, M36). The trigger gained NativeSelect's --nz-radius-sm at sm, invalid border and halo, and disabled border.
12Tabs, TabNavindicator 2 px, no token--nz-border-width-focus (2 px); no new token.
Hook: --nz-tabs-indicator
13Tabs, TabNavindicator above vs over the hairlineFigma's stacking: the indicator sits on the tab's bottom edge, above the strip's 1 px --nz-border-subtle hairline, so a 40 px tab makes a 41 px strip. TabNav does the same.
Hook: --nz-tabs-border
14Tabs, TabNavbold width jump 67 → 69Reserved, as SegmentedControl: a hidden copy of the label at the strong weight sizes the tab, so selecting never moves its neighbours.
15Tabs, TabNavinline padding 2 (22 px "All" target)Figma's 24 px gap becomes padding: 14 px a side (--nz-space-3 + --nz-space-0-5) and 2 px at the strip's ends, so labels sit where Figma puts them and the space between two labels is target, not dead space. Tabs are 40 px tall; no nz-hit-target.
16Tabs, TabNavfocus (not drawn)The generated 2 px outline, drawn inside the tab (data-focus-inset), because the scrolling strip would clip a 2 px offset. TabNav links do the same.
17Tabsgap 10 label → indicatorThe indicator is pinned to the bottom edge, and --nz-space-1 (4 px) of bottom padding centres the 20 px label 8 px from the top, as in Figma; no token needed.
18Tabs, TabNavdisabled, icon, count, overflow, vertical, sizes (not drawn)Disabled (--nz-text-disabled), a 16 px icon and a count pill (Label/S on --nz-bg-subtle) ship. An overflowing strip scrolls sideways, with no wrap, no fades and no other overflow cue: only the clipped last tab says more tabs exist (the fade techniques are outside the Safari 17.5 floor; a cue is a follow-up). Where scrollbars are classic (Windows, Linux), the scrollbar shows between the indicator and the hairline, because scrollbar-width is outside the floor too. No vertical orientation and no sizes: neither is drawn.
Hook: --nz-tabs-count-bg
19TabNavforced-colours mark for the current linkA stateIndicator fill: the current link's indicator paints Highlight in forced colours, as well as the strong weight. tab-nav.css repeats the generated rule, which loses to nz.components (§8.5).
20Tooltiparrow 12 × 7, offsetNo arrow, as shipped in week 3 (week 3 item 28); the popup sits sideOffset={8} from its trigger.
21IconButtonTop Bar's round 40 px button with an unread dotNot built. IconButton keeps Figma's Icon Button set (radius/md, no badge); the Top Bar's round bell with its unread dot stays a composition, rounded through the --nz-icon-button-radius hook.
Hook: --nz-icon-button-radius
22All160 ms hover, 220 ms clickHover colour changes use --nz-duration-fast (Tabs and TabNav labels and indicators); the Menu highlight is instant (item 10). Nothing animates on click this week, so --nz-duration-base has no user yet.

Source: docs/figma/13-w4-menu-tabs.md

Two week 4 behaviours differ from the APG patterns, and neither is in the Figma file:

  • Enter on a Menu checkbox item toggles it and keeps the menu open, as Space does. The APG menu button closes the menu on Enter. Staying open lets a reader set several columns in one visit (D-ag).
  • Tabs arrow keys follow the direction of the locale set on NizivoConfig, or the dir prop on Tabs.Root, which overrides it for that strip and the strips nested in its panels (week 7, the maintainer's decision). A dir="rtl" attribute set elsewhere on the page alone does not turn them, because nothing reads the DOM: without the locale or the prop, ArrowRight moves to the tab on the left in a right-to-left strip.

Week 5

Week 5 added no component and no Figma facts file, so it has no new value to choose. It removed two places where the shipped CSS leaned on a page reset, which the components must not do: Menu rows are 36 px as Figma draws them with or without a border-box reset, and a Button rendered as a link has no underline.

Week 7

Two values that are not in the Figma file, both the maintainer's decisions of 2026-10-16:

  • 16 px text in fields under a coarse pointer. Input, Textarea, NativeSelect and the Select trigger use Body/L or Label/L (16 px) instead of Figma's Body/M or Label/M (14 px) when the primary pointer is coarse, because iOS Safari zooms the page into a focused field whose text is under 16 px. The weights are Figma's. Heights stay on the 32/40/48 px scale; a Textarea row is Body/L's 25.6 px line there instead of 21 px. The Select list's rows stay at 14 px. A page that sets --nz-input-font, --nz-textarea-font or --nz-native-select-font replaces the font on every pointer, so it also owns the zoom.
  • A minimum size for bare triggers. A Dialog.Trigger or Tooltip.Trigger with no render (its own unstyled <button>) is at least 24 by 24 px (1.5rem), the WCAG 2.2 2.5.8 minimum, and carries nz-hit-target for the 44 px coarse-pointer hit area. Figma draws every trigger as a Button, which is still the recommended composition.

Week 8

One value that is not in the Figma file, the maintainer's decision of 2026-10-17:

  • Alert's dismiss is a small IconButton. Figma draws the close as a bare 16 px glyph with no states. In code it is a tertiary sm IconButton: a 32 px button with IconButton's hover and pressed fills and focus ring, and in forced colours its ButtonText border. The glyph stays 16 px in icon/secondary, 16 px from the inline-end edge, and the button reaches into the padding, so the row is no taller than the title's line. It replaces week 2's 24 px button, whose hover changed only the glyph colour.

Week 9

Figma's file of 2026-10-18 settled several week 3 to week 6 choices (the Select list, the Textarea's padding and height, the chart fills, one border width, the spinner's speed), so the tables above now record Figma's values where they apply. What still differs, with the maintainer's decisions of 2026-10-18:

  • The selected Select row keeps a tint. Figma's Select Option draws the selected row with only Label/M Strong and a trailing check. The code also keeps the --nz-bg-brand-subtle fill it had (M36: "keep tint, for label, follow figma").
  • The Select check's column is always reserved. Figma hides the check on unselected rows and the label takes its place; the code keeps the 16 px column on every row, so selecting never moves the text.
  • The Select list is as wide as its trigger. Figma's Select Menu says "matches its width (min 240)"; the code keeps the trigger's width as the minimum, with no 240 px floor.
  • A pending Button replaces its label under Reduced motion. Figma's Button says "keep the label"; under Reduced motion the code shows "Loading…" in the label's place instead, so the button keeps its width when the wait starts. The label stays in the accessible name ("Loading… Publishing"). Under default motion the label stays, with the spinner, as in Figma (M37).
  • An icon-only pending button shows no text under Reduced motion. Figma's Accessibility page says "Spinner and skeleton loops stop; a text label shows". Button does that. IconButton has no room for text: its spinner stops and aria-disabled stays, and nothing visible says it is loading. Show the progress in words next to it (a status line), or use a Button where a label fits (M37: accepted).
  • A Textarea's minimum height can be lowered. Figma's 112 px minimum is the default; --nz-textarea-min-block-size changes it (for example to 0 for a one-line composer).

Week 12

The dashboard parts (Figma 04), with the maintainer's decisions of 2026-10-20 (M42):

  • The Delta pill is bg/surface on a tinted surface. Figma draws the Up pill in bg/success-subtle, the same green-50 as bg/brand-subtle (and in dark mode all three tints are green-950), so on a tinted KPI tile the pill shows no fill (Figma 04 gap 4). Inside a Card, KpiTile or TrendCard with tone="brand" or "accent", every Delta pill is bg/surface instead, including the Down pill on an accent tile, which Figma draws in bg/critical-subtle and which does show there: the pills in a tinted row match. Its text keeps Figma's colour: text/success, text/critical and text/secondary on bg/surface are at least 5.68:1 in light and 5.96:1 in dark.
  • A chart's halo follows the surface it sits on. Figma rings the Sparkline's end dot in bg/surface, which shows as a white ring on a tinted tile. The halo reads the surface a Card declares (--nz-surface), so on a tinted tile it is the tint; outside a Card it is bg/surface as drawn.
  • brand and accent look the same in dark mode (both green-950). That is Figma's design, and the code follows it for now (Figma 04 issue 5); it is listed here because it reads like a defect.

Builder defaults

The plan's decision log has 116 builder defaults (D-a to D-dl): questions the plan did not settle, where the builder adopted an answer that the maintainer can override. Most are engineering choices rather than Figma values; they are listed here so that every choice the builder made is in one place.

IDDecisionWhat ships
D-a
2026-10-14
Composition precedence (§8.2, §10.4)With a render element, the part's presentational attributes win: data-slot, data-variant, data-size, data-tone, data-full-width (PART_OWNED). Every other prop merges with the element winning; className tokens are de-duplicated. data-size follows the part (Select.Root size="lg" with a Button trigger gives data-size="lg"). Maintainer, 2026-10-14: "okay for now" (confirmed for now). Week 2 additions (builder defaults): PART_OWNED has 13 names (D-j); children follow the same rule, so Button renders no children when it has nothing to show
D-b
2026-10-14
render scope (S-1, §8.2, §10.3)Every DOM-rendering part except the engine-owned positioned popups (Select.Popup, Dialog.Popup, Tooltip.Popup) and Select.Root; S-1 now says so. Maintainer, 2026-10-14: explained as having no visual difference; no change requested. Week 2 addition (builder default): only exported parts take render; a data-slot on an inner element of a single-root component (alert-title, progress-bar-track, …) is for CSS only, and ProgressBar's and Meter's native element takes none
D-c
2026-10-14
data-full-widthIn the vocabulary's presentational group. This row decides data-full-width only; another presentational attribute (for example Card's data-padding) that joins the group also becomes part-owned under D-a, because a unit test keeps PART_OWNED equal to the group. Week 2 added eight (D-j)
D-d
2026-10-14
Tooltip API (§12.3)The compound Tooltip.Root / Trigger / Popup is canonical. A content convenience prop and shortcut (needs Kbd) come in week 3 with IconButton's tooltip wiring. Maintainer, 2026-10-14: "works" (confirmed)
D-e
2026-10-14
Inverse inside inverse (§7.4)A nested inverse surface stays inverse; it does not flip back. Maintainer, 2026-10-14: "if current design does not use it let's not worry about it right now" (set aside while the design has no nested case). The builder checked 6 pages of the Figma file: no inverse surface sits inside another; the only nesting is Card Inverse's slot, which uses bg/inverse-elevated
D-f
2026-10-14
Engine data-* (§8.3 rule 3 vs §9.3 rule 6)Rule 3 wins. Adapters strip every engine data-* except an allowlist of what React Aria reads back from the DOM, for example data-key and data-collection (focus, scroll-into-view and typeahead), data-react-aria-top-layer, data-live-announcer, and the data-href family for Menu links (dom-allowlist.json is the full list: 14 entries in W1). An open trigger that React Aria reports as pressed is not mapped to data-pressed
D-g
2026-10-14
CSS-only exit motion (§13.2 rule 4)Entry with @starting-style on the whole floor. Exit to display: none is instant in every Firefox (129 to 157; bug 1834877), so a CSS-only component hides with visibility plus allow-discrete when its exit animation matters; otherwise the instant exit in Firefox is an accepted deviation (Deviations page entry D2, §18.2)
D-h
2026-10-14
Component subpaths (§5.2, §6)Maintainer, 2026-10-15: "we can keep import path per components" (confirmed; now a maintainer decision, M13). One subpath and one stylesheet per component: ./text, ./heading, ./stack, ./inline, ./grid (Grid, GridItem), ./container, ./separator, ./progress-bar, ./progress-ring and ./meter, each with its css/<name>.css, instead of §5.2's family subpaths text (Text, Heading), layout (Stack, Inline, Grid, GridItem, Container, Separator) and progress (ProgressBar, ProgressRing, Meter)
D-i
2026-10-14
Progress fills for tones Figma does not draw (§15.3)ProgressBar, Meter and ProgressRing paint tone="neutral" with icon/secondary and tone="success" with icon/success until design supplies the fills
D-j
2026-10-14
Eight more presentational attributes (§8.2, §8.3 rule 5)data-padding, data-elevation, data-shape, data-gap, data-span, data-span-sm, data-span-lg and data-ratio join the vocabulary's presentational group with "values": null, so each value set comes from the manifest's variants (Grid gained spanSm and spanLg). They are part-owned under D-a, which extends D-c; PART_OWNED has 13 names
D-k
2026-10-14
Override metadata (§15.4)Three kinds: contrast, ladder and pairing. issue is required, as an https URL or null until an issue is filed (all 14 are null). approvedBy is ["maintainer"] until a second owner and a CODEOWNERS file exist (2026-10-15: the second owner is Dayitava, M15; their GitHub username is pending; the tracker, GitHub Issues or Linear, is open)
D-l
2026-10-14
Uncached a11y gate (§15.4 rule 4)The token a11y gate runs again outside Turbo's cache as the root pnpm check:a11y (node packages/tokens/scripts/a11y.ts --check), after pnpm build in pnpm check
D-m
2026-10-14
Flat elevations take no dark-mode ring (§7.1, §7.5)An elevation.<n> whose every layer has alpha 0 builds as Figma's layers alone (0 0 0 0 transparent for Elevation/0 Flat, and the same no-op layer when there are none): no --nz-elevation-ring, no --nz-elevation-highlight. Equal light and dark layer colours print once, as lightDark() does for colours
D-n
2026-10-14
Docs component routes and example imports (§18.2)apps/docs/scripts/generate.ts writes one route per component from the exports map (app/docs/(generated)/components/<name>/page.tsx, gitignored), and each imports only its own example module. The examples are copied to apps/docs/.generated/examples/ with their relative imports rewritten to the public subpaths (./index → @nizivo/react/<name>); an import it cannot map fails the build
D-o
2026-10-14
Where the API records come from (§18.1, §18.2)nz api reads props and types from the built dist/*.d.ts with ts-morph 28, each export's client flag from the built .js, slots and data-* attributes from the component source and the vocabulary, and hooks from the built dist/css, re-printed flat by Lightning CSS. It stops with "rebuild @nizivo/react" when a built entry's export names differ from its source barrel. A hook's description comes only from a /** … */ doc comment in the source CSS. The root pnpm gen:check builds @nizivo/react (a cache hit in pnpm check) before nz api --check
D-p
2026-10-14
Install-script deny and the fumadocs-ui pin (§19.1, §19.3)allowBuilds is { 'esbuild@0.28.2': false }: esbuild's postinstall is denied at that exact version, and no script runs. fumadocs-ui is pinned at 16.17.2 with fumadocs-core 16.17.2, and both are in minimumReleaseAgeExclude (published 2026-10-13T09:46Z), to be deleted once they are 3 days old (from 2026-10-17)
D-q
2026-10-14
Field's server → client wiring (§10.6, §16.1)Field (no directive) calls only useId and hands serialisable props (ids, flags, label, description, error, messages, validateOn, root attributes) to one 'use client' leaf, FieldScope, which provides the context, owns the native validation message and renders the whole anatomy (root, label, description, error) through fieldRootProps() from field.tsx. Controls read it with useFieldControl(); their own props win (an explicit false included) and aria-describedby is joined. A function child is the zero-JS path: the shell renders the same anatomy and passes ready-made control props. Native validation: onInvalid cancels the bubble, shows the message inline and focuses the form's first invalid control once the message is rendered (a layout effect, so aria-invalid and the error's id are on the control when focus lands); typing updates it silently. A submit from inside the already-focused first invalid control moves no focus, so nothing read the new error. Settled 2026-10-17 by M29 ("A + yes"; built in week 8): FieldScope stays as it is (A), and its announcer, §16.5's one exception, speaks that error (yes). A NativeSelect placed directly in a Field is recognised by an internal mark and wired as a function child would be (D-x)
D-r
2026-10-14
Field and Fieldset subpaths (§5.2)Two subpaths: ./field (Field, useFieldControl) and ./fieldset (Fieldset), each with its own stylesheet. Fieldset is a native <fieldset>/<legend> with no client leaf; its rows sit in a .nz-fieldset-content box
D-s
2026-10-14
Labelling group controls in a Field (§16.1)Field takes group: its label renders with no for, and the group control (SegmentedControl; later ChipGroup and third-party pickers) names its role="radiogroup" root with aria-labelledby={field.labelledBy}
D-t
2026-10-14
SegmentedControl semantics (§8.5, §9.5)Native radios in <label>s under a role="radiogroup" root, one client leaf, no engine: arrows move and select, Tab enters at the checked item, the form submits and resets it, and it works before hydration. Inside a Field it follows D-s. No sliding thumb: selection changes with a colour fade, and a hidden bold copy of each label reserves its width. WebKit matches no :focus-visible on a radio reached by an arrow key, so the item sets data-focus-visible after a key, and the generated nested-input rule matches it. The attribute is written on the DOM, not through state: re-rendering a focused radio makes React re-write its name, after which Chromium gave every radio of an unchecked group its own Tab stop (a parent re-render while such a radio has focus still can). onValueChange reports reason: 'input' as RadioGroup and Checkbox do, and value takes null for a controlled empty group. Invalid paints a 1 px --nz-border-critical ring on the track
D-u
2026-10-14
Checkbox, RadioGroup and Switch API (§16.1)Native <input>s with appearance: none, so :checked and :indeterminate repaint with no JavaScript; no React Aria. Checkbox checked takes true, false or 'indeterminate' (a DOM property, so the server HTML shows it unchecked until hydration). RadioGroup is a server <Fieldset role="radiogroup"> around one client part that passes the group's name and value. No CheckboxGroup: a Fieldset holds checkboxes. readOnly is not supported on the three. Callbacks report reason: 'input'
D-v
2026-10-14
IconButton defaults (§9.5)label is required and is the aria-label and the tooltip text; the tooltip is on by default (tooltip={false} renders the server path, { side, align, delay } tunes it). The button is not described by its tooltip, which only repeats the name; a shortcut is announced through aria-keyshortcuts instead (mod+b → Meta+B Control+B: the server does not know the platform, so both alternatives are listed). pressed makes it a toggle (aria-pressed; secondary and tertiary take the brand tint, border and glyph). Manifest tier server (split): its engine part is Tooltip's
D-w
2026-10-14
Textarea auto-grow (§16.1, §4.1 L3 guard)field-sizing: content inside @supports (field-sizing: content), with a minimum height rebuilt from rows ([rows='1'] … [rows='10'] set a private variable; no inline style, so CSP Profile B holds). The Lightning CSS guard accepts field-sizing only inside such a block (SUPPORTS_GATED_PROPERTIES, not under not); everywhere else it still fails the build
D-x
2026-10-14
NativeSelect tier and skin (§9.5)server, no directive: inside a Field it is wired as plain attributes, either placed directly (Field recognises it by an internal mark on the component and wires it as a function child; its own props win, its own id takes the label) or through the zero-JS function child (<Field>{(c) => <NativeSelect {...c} />}</Field>). Wrapped in a consumer's own component it is not recognised and needs the function child. It copies the shipped Select trigger's metrics (measured equal at sm, md and lg in Chromium and WebKit), takes placeholder and leadingIcon, accepts and drops readOnly so a Field function child spreads in, and omits multiple and native size
D-y
2026-10-14
Input DOM (§8.2, §8.5)The root is the bordered box, span.nz-input[data-slot=input] (focusTarget: "within"; className, style and render apply there), around input.nz-input-control, which takes every other prop and the ref. A press anywhere on the box focuses the input. type is one of text, email, search, password, number, tel, url
D-z
2026-10-14
ThemeScript, platform half (§7.4)@nizivo/react/theme-script exports ThemeScript (nonce) and the themeScript string (257 bytes). It sets data-nz-platform to apple when navigator.userAgentData.platform, else navigator.platform, matches /mac|iphone|ipad|ipod/i, else other; it never reads the user-agent string, keeps a value already on <html> and never throws. The theme half came in week 6 (D-al); the script, both halves, is now 379 bytes
D-aa
2026-10-15
What the client-module gates assert (§9.5, §16.1, §18.2)The docs smoke (CLIENT_PAGES) and the Next fixture (PageSpec.imports) allow, and the smoke requires, every client module a page's server graph imports, not only those its examples render: Turbopack and next dev ship a leaf a server shell imports even when no example renders it (FieldScope behind a function child or a NativeSelect, about 2.7 KB min+gz bundled; the IconButton tooltip chain behind tooltip={false}, about 17 KB). Production webpack drops them. The maintainer, 2026-10-16: "If 'Add client-import-free entries now' would not cause any issues, and not a bad DX, then sure"; the builder tried to get the saving with no new import path first. Week 7 outcome: no design without a new import path met that condition, so nothing changed and the gates stay as above (packages/react/W7-RESULTS.md, "D-aa"). Measured on the Next fixture: Turbopack production ships 20,805 B gzip more on an IconButton page without a tooltip than on a Button-only page and 3,035 B more on a zero-JS Field page (next dev 62,124 B and 7,800 B; production webpack 0). A server shell that loads the leaf with lazy(() => import()) saves nothing: Turbopack ships every client module the server graph reaches, static or dynamic. A thin client leaf that lazy-loads the tooltip chain inside its own <Suspense> cuts Turbopack's cost to 2,191 B, but renderToString then emits a client-render fallback boundary, a client navigation replaces the button's DOM node (detaching a ref, losing focus), and handlers attach 12–15 ms later on localhost (60–70 ms at 150 ms latency) on every page that has a tooltip; Field cannot use it at all (its controls read FieldScope's context on first render). What works is a second export in a module that imports no client leaf (IconButtonStatic, FieldStatic; 0 B extra in all three modes, through the subpath and the barrel): new public API, against M13's one export per subpath, or two new subpaths. ~~Still the maintainer's call, with that trade-off; both need the shells' helpers moved into leaf-free modules first~~ Settled 2026-10-17 by M31 ("add the static exports"); built in week 9: IconButtonStatic and FieldStatic in the existing subpaths, the helpers moved into leaf-free modules, and the Next fixture records /icon-button-static and /field-static at 0 B over /button-only with no client marker in every mode. The gates keep asserting imports, not renders: IconButton and Field still ship their leaf under Turbopack and next dev
D-ab
2026-10-15
SegmentedControl over the own-leaf cap (§17.2)SegmentedControl's client leaf is 1.18 KB min+gz of own code, over §17.2's 1 KB cap for an own input leaf; its budget is recorded at measured + 10% (1.3 KB)
D-ac
2026-10-15
Tabs and TabNav (§5.2, §6, §9.5)Two subpaths, ./tabs (Tabs, tier engine, React Aria Tabs) and ./tab-nav (TabNav, tier server: a <nav> of <a>s with aria-current="page", a router link through render), each with its own stylesheet, so component.json needed no schema change. Tabs activates automatically by default (the APG default when panels show without delay, React Aria's default, and Base UI's activateOnFocus equivalent), with activationMode="manual" for slow panels; only the selected panel renders, on the server too, and keepMounted keeps a panel mounted and hidden. An overflowing strip scrolls sideways (no wrap, no fades); no vertical orientation and no sizes (neither is drawn)
D-ad
2026-10-15
@nizivo/icons API and build (§14.2, §5.2, §19.1)Default size '1em'; accessible name label (role img + aria-label, no <title>); absoluteStrokeWidth through vector-effect: non-scaling-stroke; exports ".", "./*" → dist/icons/* and "./brands/*" → dist/brands/* (scoped wildcards; the shared base in dist/_internal/ is unreachable); brand glyphs only under ./brands/; generated sources built, not committed (manifest.json and THIRD_PARTY_NOTICES are); aliases.json empty (no Lucide deprecated names); SVGO 4.1.0 with two plugins and five convertPathData options off, the only lossless configuration found
D-ae
2026-10-15
tokens.json and the typed . entry of @nizivo/tokens (§5.2, §7.1, §15.4)tokens.json is flat, keyed by the DTCG token id that contract.json uses, with a literal value and provenance for the default context, aliasOf, and modes holding only the contexts that differ (format 1, documented in packages/tokens/TOKENS-JSON.md). The . entry is one named export per token group of var() references (color.bgSurface, space[0.5]), plus typography parts, breakpoints and springs, with types only for CssVariable, TokenId and the JSON shape. Both are computed from the declarations tokens.css holds, so the three outputs agree by construction
D-af
2026-10-15
Where React Aria 1.22.1 differs from §12.2 (§12.1, §12.2)The react-aria adapters fill four gaps the overlay conformance suite found: Tab in a Menu or Select popup closes it and moves focus past the trigger, inside an enclosing dialog (useTabOut); Dialog focus on open goes to the first tabbable element in Dialog.Body (an alertdialog's first Dialog.Close; autoFocus wins) through an internal initialFocus on DialogEngine.Popup; nested overlays portal into a box-less container last in the dialog's positioner that carries data-react-aria-top-layer, so a Tooltip inside a Dialog stays hoverable; focus return goes to the trigger when React Aria would leave it on <body>, as after a defaultOpen popup (ReturnFocus). The helpers that read React Aria's contexts live in popup.client.tsx, not in the shared overlay.client.tsx, which Tooltip imports. The four are listed in OverlayEngine.completes, and delegated names what React Aria still does alone (§12.2)
D-ag
2026-10-15
Menu's item model (§9.5, §12.3)Menu.CheckboxItem is a React Aria MenuItem whose role="menuitemcheckbox" and aria-checked the adapter sets, with its own state, instead of React Aria's selection scope; Menu.RadioGroup is a MenuSection with single selection. Uncontrolled checked state is kept by Menu.Root across reopen (checkbox items keyed by text, radio groups by their values). Checkbox and radio items have their own root slots (menu-checkbox-item, menu-radio-item); the item's inner spans (icon, label, shortcut, check), rendered by one function shared by the three item kinds, carry classes only (nz-menu-item-icon …), so CSS targets them by class and they have no data-slot. Enter on a checkbox item toggles it and keeps the menu open, as Space does (the APG menu button closes on Enter; staying open sets several columns in one visit; on the Deviations page). SubmenuTrigger is not built: React Aria's is @version alpha, Figma draws no submenu, and §12.5 stages O-02 at v0.2; whether it returns to v0.1 is the maintainer's call. The maintainer, 2026-10-16 (M19): "we can build it later in 0.2": no submenu in v0.1
D-ah
2026-10-15
Size budgets and where they live (§17, §6)One size-limit 14.2.0 config, packages/react/.size-limit.ts, built by packages/tooling/size/checks.ts from the exports of @nizivo/react, @nizivo/tokens and @nizivo/icons and from every src/**/*.client.tsx: 118 checks (every JS subpath and the root barrel, every stylesheet and JSON entry, the dashboard set, { Button } and { Select } through the barrel, 1 and 10 icons, the own code of each of the 23 client modules with every import external, and the K6 and K7 helpers). r3's method through @size-limit/esbuild (ESM, production condition, minify, all locales) and gzip. Budgets live in packages/tooling/size/budgets.json as measured + 10% of Nizivo's own subpaths, rounded up to 0.1 KiB below 10 KiB and to 1 KiB above, written in KiB (size-limit reads KB as 1,000 bytes), and never above an entry's §17.1 limit (Tooltip, Dialog, Tabs, Menu, Select and the dashboard set; the week 5 review). measure.ts --write never raises a limit; --rebase does, in a labelled change (§17.3). The gate fails on an entry over budget, an entry with no budget and a budget with no entry. Measured tables: packages/react/SIZES.md
D-ai
2026-10-15
Visual regression runner (§18.1)A plain Node Playwright runner, packages/react/scripts/visual/run.ts, over a harness that tsdown bundles from every *.example.tsx against the built dist (74 examples, 37 components), on the built CSS and the Geist fonts from the docs app's installed geist, offline. Chromium at 960×720, 1×, reduced motion, sRGB. Every example in light, dark, forced colours and forced colours with a dark OS, in the states rest, hover (each interactive slot), press (each enabled control that opens no popup and is not a text field, released off the control), focus (each Tab stop, by keyboard) and open, open-hover and open-key for Dialog, Menu and Select: 1,496 baselines, lossless 8-bit RGB PNGs (4.7 MB). The harness keeps the root at 16 px, so every rem renders at the size Nizivo ships. A shot differs when more than 3 pixels move by more than ±2 in a channel (the pinned environment repeats to ±1, with an occasional ±5 pixel on a focus halo's edge); --update re-records exactly what a compare would fail. pixelmatch's YIQ threshold and anti-aliasing tolerance are opt-in flags (the week 5 review showed that YIQ 0.1 let a one-step brand or border token change through). baselines/manifest.json pins the environment (darwin-arm64, Chromium 156.0.8078.4, Playwright 1.64.0, geist 1.7.2) and a compare elsewhere refuses to run. Not in pnpm check (D-ak)
D-aj
2026-10-15
The local release script (§19.2, M14)packages/tooling/release/release.ts, run as pnpm release (a dry run, the default) or pnpm release --publish (needs a terminal). Order: preflight (on main, clean tree, HEAD equal to origin/main after git fetch, Node and pnpm pins, npm whoami is neevtechnologies, 2FA auth-and-writes); the version step (pending changesets: a real run runs pnpm changeset version, shows the diff, commits it on confirmation and stops for the maintainer to push main and run again); the fixed group's version; pnpm check with TURBO_FORCE=true; the release lanes (RELEASE_LANES, the pnpm check:full set); pack and check:release on those tarballs plus the §19.3 licence gate; npm view (a version already on the registry is skipped, so a re-run carries on; one whose registry bytes differ from the checked tarball stops a real run before anything is published or tagged); a tree the gate or a lane changed is a blocker; npm publish <tarball> --access public --tag next in the order tokens, icons, react, asking for a one-time password each time (passed as npm_config_otp in the child's environment only) and re-checking each tarball's sha512; then local annotated tags <name>@<version> and the push command. npm always runs from a stage folder outside the repo, because the root devEngines makes npm 11 refuse to run inside it (EBADDEVENGINES). It never pushes and never logs in
D-ak
2026-10-15
What runs in pnpm check (§18.1)pnpm check (the per-change gate) gains pnpm size and pnpm floor:check (gen-w5.ts --check), both after pnpm build. pnpm check:full is pnpm check and then packages/tooling/release/lanes.ts, which runs RELEASE_LANES: the browser lane, overlay conformance in Chromium and WebKit, the Next, Vite and Astro fixtures on the packed packages, and visual regression; the release script runs the same list. The floor lanes (pnpm floor) stay manual: they need a Firefox 129 binary and the iOS 17.5 Simulator. Overlay conformance in Firefox is in neither, because Playwright's Firefox does not launch on the release machine. Week 6: it launches (D-ar), and RELEASE_LANES gains the browser lane and the overlay suite in Firefox
D-al
2026-10-15
ThemeScript, theme half (§7.4)localStorage key nz-theme, fixed. Values light, dark or system (an exact match); anything else is ignored. A stored value replaces the server-rendered data-nz-theme on <html> (the server value is the app's default, the stored value the reader's choice); nothing stored or blocked storage leaves it. system is set literally and resolved by CSS (no matchMedia, no listener). The theme and platform parts each have their own try, so blocked storage never stops data-nz-platform and the script never throws. No prop beyond nonce, so one published hash covers every app; an app that needs another key writes its own script. 379 bytes, sha256-nA1HUQWDdDUFqqoxWbjzCzcYSjzWLEe0pwDdRCMGqOw=
D-am
2026-10-15
What styles.css holds (§5.2, §8.1)dist/styles.css is the canonical layer statement, @nizivo/tokens' tokens.css byte for byte (its header included), base.css, then every stable component's sheet sorted by name, each as built; reset.css and src/unstable/** are not in it. Each sheet keeps its own line-1 layer statement, so the file is an exact concatenation that a dist test checks (39 repeated statements: 3.3 KB raw, 49 B gzip). A missing or wrong ./styles.css export fails the CSS build
D-an
2026-10-15
The default Dialog.Close (§12.3, §14.2)With no render and no children, a tertiary md IconButton with core's close glyph, named by labels.close (default "Close") or aria-label. As a direct child of Dialog.Popup it sits in the top-end corner, 20 px in (Figma's header glyph), positioned against the popup, which never clips. children stay text content (fixtures and docs use <Dialog.Close>Done</Dialog.Close>); another glyph goes through render={<IconButton …/>}. Its markup is IconButton's, written out (importing the builder cost about 280 B gzip against the Dialog cap; a browser case asserts the two match). An alertdialog focuses the Dialog.Close in its footer before the corner X. Figma's overflow: clip on the content stays; a control placed directly in the content draws its outline inset
D-ao
2026-10-15
The focus-return chain (§12.2, O-11, O-02)One family helper, ReturnFocus in _engines/react-aria/popup.client.tsx, rendered in every modal popup (Dialog, Menu, Select): returnFocus (a ref or function target is applied by cancelling React Aria's react-aria-focus-scope-restore event; false leaves focus alone) → the trigger → the element that opened the popup → the parent overlay's trigger, from a module-level registry of open popups → <body> with a console.warn behind process.env.NODE_ENV !== 'production'; <body> itself is never a step, and a copy rendered in React Aria's hidden collection pass (no popup) does nothing (week 6 review). The prop is on Dialog.Root and reaches the Popup through context; the engine interface omits it from Root. Menu and Select run the chain without the prop
D-ap
2026-10-15
Tooltip stuck in data-exiting (§12.2 Motion)React Aria 1.22.1's Tooltip is keyed on its instant-close state (key={instantlyClosed(state) ? 'instant' : 'live'}), which resets the exit state an instant close during warm-up leaves stuck; nothing visible remounts (+34 B gzip on the Tooltip entry, size-limit)
D-aq
2026-10-15
Select in a Field, and its skin (§10.3, §16.1)FieldControlOptions.labelBehavior?: 'focus': FieldScope puts an onClick on its <label> after mount that cancels the click and calls label.control.focus({ focusVisible: true }). Select's own props win over the Field's. The selected row carries a trailing check (span.nz-select-item-indicator, added in the item's render function so the trigger value never shows it; indicator replaces it, null removes it), the non-colour cue once Figma's weight 600 is gone (WCAG 1.4.1). Rows, popup, group label and separator copy Menu's values; the popup has no public hooks yet
D-ar
2026-10-15
Firefox in the browser and overlay lanes (§18.1)NZ_BROWSER picks the engine for both lanes (packages/react/test/browser-engine.ts). On macOS, Playwright's Firefox gets CFFIXED_USER_HOME pointing at a fixed $TMPDIR/nizivo-firefox-home, because it exits with "Could not find profile folder." where macOS denies ~/Library/Application Support/Firefox. RELEASE_LANES gains browser-firefox and overlay-firefox; the WebKit browser lane stays out (WebKit on macOS does not Tab to buttons, links or radios)
D-as
2026-10-15
Fixture axe in forced colours and reduced motion; the Vite dev check (§18.1)Every fixture page's first render runs axe again in Chromium's emulated forced colours and with reduced motion, both blocking. For the forced-colours run only, each element without an inline text fill gets -webkit-text-fill-color equal to its computed colour, then the fills are removed (Chromium reports the unforced author colour for the fill, and axe reads it). The Vite fixture also runs vite (dev): frames painted before any stylesheet are reported, not failed; every styled frame must be right
D-at
2026-10-15
The focus ring on fill state indicators in forced colours (§8.5)The generated fill paint (a checked Checkbox, Radio or Switch, an indeterminate Checkbox, progress and meter fills, the current TabNav and Tabs indicators) adds outline-color: CanvasText to background: Highlight and forced-color-adjust: none, as the highlight paint already did, so a focused checked control's ring is a system colour, not the token green
D-au
2026-10-19
BarChart geometry (§14.1)Each datum is one column of an HTML grid (ChartFrame's band x tracks, so labels stay centred and xDirection="rtl" mirrors both). Each column holds an <svg> with no viewBox (one user unit = 1 CSS px), capped by CSS at min(22 px, 52 % of the column); a nested <svg> clips at the zero line and the bar is a <rect> with percentage attributes, 200 % of the clip's height, rx="4" (radius/xs), so only its data end is rounded. Hover dims the other bars to opacity 0.35 and shows a CSS tooltip beside the bar: ~~no client JS, interaction="none"~~ Changed 2026-10-20 by M40 ("BarChart keyboard access: raise budget"); built in week 11: BarChart defaults to interaction="nizivo", the ChartInteraction island's column mode (one tab stop; arrow keys, Home and End over the columns; data-highlighted on the active band, which bar-chart.css dims as before; the ChartTooltip card), and the per-band CSS tooltip renders only under interaction="none", the zero-JS path. ./bar-chart is 5,238 B with the island graph (§17.2: 5.7 KiB)
D-av
2026-10-19
Paths without d3's generators (§10.8, §14.1)src/_core/path.ts drives any d3 CurveFactory (default curveMonotoneX) through a small string context, so only the curve is bundled, not line/area/d3-path; its output equals ~~line().curve(c).digits(3)~~ line().curve(c).digits(2) (path.test.ts). Changed 2026-10-20 by M40 ("Long series - whichever results in optimized sizing"); built in week 11: paths, end dots, AreaChart's grid and baseline and the island's y are written at 2 decimals (PATH_DIGITS = 2; 0.01 of the plot is 0.07 px on Figma's 720 px plot), and the d3 parity tests compare at that precision; bar geometry keeps num()'s 3. Donut segments are hand-written paths (outer radius 100, inner 74, 3-unit parallel gaps, flat ends, from 12 o'clock clockwise), not d3 arc/pie. End dots are zero-length round-capped paths, round in percent space
D-aw
2026-10-19
The data table's default (§14.1, §16)Visually hidden by default, inside a div.nz-chart-visually-hidden wrapper (a <table> ignores inline-size: 1px and kept a 223 × 240 box that could add horizontal scroll; the charts-only fixture now requires the 1 × 1 box); tableView="details" gives a zero-JS native disclosure that renders in every status so the box never moves. Changed for BarChart 2026-10-20 by M40 ("BarChart keyboard access: raise budget"); built in week 11: the hidden table stays the screen-reader path of every chart; BarChart's sighted keyboard path is now the island's column mode, and tableView="details" remains it for DonutChart (its keyboard is still open) and for any chart under interaction="none". Changed for DonutChart 2026-10-20 by M41 item 2; built in week 12: DonutChart's sighted keyboard path is the island's item mode, so tableView="details" is that path only under interaction="none"; the hidden table stays the screen-reader path
D-ax
2026-10-19
Forced colours for chart marks (§15)SVG fill and stroke are forced too, so the chart svgs set forced-color-adjust: none and take system colours: current series, bars, baseline and end dots CanvasText, the dashed comparison and grid GrayText, donut categories Highlight, hollow Canvas, CanvasText, GrayText (outlined), legend marks matching (the legend's old CanvasText rule lost to its own series rules). Only Chromium's emulation was rendered (visual baselines, forced and forced-dark) Week 12 (M41 item 2): DonutChart's active segment takes a 3 px CanvasText outline and its focus ring is round; only Chromium's forced-colours emulation was rendered, so this stays with the accessibility owner
D-ay
2026-10-19
The ChartInteraction island (§14.1, §16)role="application" with aria-roledescription, an aria-describedby hint rendered by the server and an empty polite region in the server HTML; keyboard moves speak, a pointer only shows the card; one tab stop over the plot. The server pre-builds every point's card rows and announce string, and the island positions through CSSOM after hydration (allowed under style-src-attr 'none', verified in all three engines). ~~About 330 B of RSC JSON per point~~ Settled 2026-10-20 by M40 ("The island's role - do recommended"): role="application" stays until the manual screen-reader pass; no code change. Built in week 11 around it (M40 items 2, 5, 8, 10): the compact payload (series names once, points as tuples, the spoken text joined on the client from the server's Intl.ListFormat separators, about 64 B of island JSON a point), Escape for a pointer-shown card wherever focus is, the column mode, and the plot direction read after hydration; the island's client graph is 1,439 B (§17.2: 1.6 KiB)
D-az
2026-10-19
Charts CSS hook names (§8.4)Each charts part folder is a component for the stylelint contract (--nz-<part>-*): --nz-chart-frame-aspect (18 / 7), --nz-chart-frame-gutter (2.75rem), --nz-bar-chart-aspect (32 / 13), --nz-bar-chart-bar-size, --nz-donut-chart-size, --nz-sparkline-width/height/halo, --nz-area-chart-halo, --nz-chart-tooltip-width/halo
D-ba
2026-10-19
Donut fold rule (§14.1)Values that are not finite or not above zero are dropped; largest first with ties in input order (sort="none" keeps input order); the first maxCategories (clamped to 1–3) keep cat-1..3; the rest add up to "Other" (chart/neutral), which keeps its own name when it is a single category. The donut legend follows Figma: a name column of at most 120 px (minmax(0, 7.5rem), so a 300 px chart does not overflow), space/3 rows, 10 px dots
D-bb
2026-10-19
Chart vocabulary (§8.3)data-series = current, previous, cat-1, cat-2, cat-3, other; data-trend = up, down, flat (in contracts/vocabulary.json); they replace classes-as-variants for marks, since charts parts have no component.json
D-bc
2026-10-19
./area-chart held out of the exports map (§17.2)The ./area-chart subpath measures 6,179 B against §17.2's 3.6 KiB; no measuring scope reaches 3.6 (island external 5,447 B; ChartFrame, Legend, ChartTooltip, _shared and the island external, r6's scope, 3,793 B). ~~The integrator did not raise the figure: AreaChart ships through the barrel (8,452 B, under "all charts together ≤ 12"), the Next pages import the barrel, and the docs Import line follows the exports map. Its CHART_TARGETS_KIB entry waits for the export.~~ Review, 2026-10-19: holding the subpath back had also removed AreaChart's size check, so pnpm size now gates @nizivo/charts { AreaChart } from the barrel (6,209 B, 6.7 KiB) and @nizivo/charts island with its imports (1,080 B, 1.2 KiB; the island's own code stays at §17.2's 0.8) as interim ceilings, not §17.2 figures. Settled 2026-10-20 by M40 ("AreaChart's size: increase the size, other options does not sound good"); built in week 11: ./area-chart is in the exports map, and §17.2's AreaChart figure is the framed subpath at its week 11 measurement + 10 % (6,556 B, 7.1 KiB, the island graph included), gated by CHART_TARGETS_KIB again; { AreaChart } from the barrel stays as a tree-shaking check under the same figure, and the island's interim ceiling became its §17.2 graph figure (1.6 KiB, M40 item 8)
D-bdScales are not chart props (§10.8, §14.1, K9)Adopted 2026-10-19 (week 10 review; builder default; the maintainer can override). No chart takes xScale/yScale: each places its points itself (AreaChart and Sparkline evenly, BarChart on grid columns). ScaleLike, LinearScaleLike, BandScaleLike, PointScaleLike and the linear, band and point scales are public for a custom renderer inside ChartFrame, and d3-scale's objects satisfy the interfaces (checked with @types/d3-scale 4.0.9 in the review). L6 and K9 say so
D-beCharts lanes gate every release (§18.1, §21.2)Adopted 2026-10-19 (week 10 review; builder default; the maintainer can override). charts-chromium, charts-webkit, charts-firefox, fixture-charts and visual-charts are in RELEASE_LANES, so a flake in the private @nizivo/charts can block a react, tokens or icons release (0.1.1 included). Settled 2026-10-20 by M40 ("whatever is efficient fpr long term"): kept blocking. Charts joins the fixed group at 0.2, so advisory lanes until then and blocking ones after would be churn; no code change
D-bfAvatar, Breadcrumb and Pagination in week 12 (§9.5, §21.2)Adopted 2026-10-20 (week 12; builder default under the maintainer's "full speed", M41; the maintainer can override; confirmed 2026-10-20 by M42 item 9: the five Pagination defaults and Breadcrumb's no-collapse rule). The server-only parts of §21.2's "Data and navigation" row are built beside the dashboard parts: Avatar (<img> over initials, sizes 24/32/40/56, role="img" named by name, or decorative) with AvatarGroup (8 px overlap, a 2 px surface ring, "+N", named through Intl.ListFormat); Breadcrumb (<nav><ol>, items take render for a router link, the current page is text with aria-current="page", nothing collapses in 0.2, the trail wraps); Pagination (link mode only through getHref and renderLink, 40 px squares at md and 32 at sm instead of Figma's off-scale 36, siblingCount 1 and boundaryCount 1, so always seven page slots once there are more than seven pages, the current page a link with aria-current, the disabled ends a <span role="link" aria-disabled>). No 'use client'; a broken Avatar src shows the initials in Chromium and Firefox, and WebKit draws its own mark over the middle (documented by M42 item 10, in Avatar's JSDoc and docs page: no script-free fix; pass src only when it is known to load, or leave it out and the initials show)
D-bgCountUp's mount, curve and width lock (§13.3, §17.2)Adopted 2026-10-20 (week 12; builder default; the maintainer can override; recorded as a row by the week 12 review, R3-6; confirmed 2026-10-20 by M42 item 7: no count on load). An element already in view when the server HTML hydrates keeps its number and does not count (Figma 04 §5 (a)): its reader has seen it; a client mount in view, or an element off screen at hydration, counts from from on first view; a new value counts from the number on screen when in view and shows at once when not. The curve is cubic-bezier(0, 0, 0, 1), the same as --nz-easing-enter (04 §5's proposal; Figma defines none for count-up), written into the code in closed form: the token is not read. While it counts, the digits keep the final string's used width, read unscaled from the computed style (+0.005 px against Chromium's six-figure serialisation; week 12 review A11Y-W12-5), so a transformed ancestor moves nothing either
D-bhDonutChart's item mode (§14.1)Adopted 2026-10-20 (week 12; builder default; the maintainer can override; recorded as a row by the week 12 review, R3-6; confirmed 2026-10-20 by M42 item 8, with the island figure of 1.7 KiB settled). The ChartTooltip card shows for the donut although Figma 03 §4.6 draws none ("the centre label is the tooltip"): M41 item 2 asked for the card, and the centre swap stays beside it. The pointer picks a slice by element (elementsFromPoint against the segments), and over the hole or a gap the last card stays; a legend row's hover dims and swaps the centre in CSS but shows no card; keys start at the first slice, or at the last one a key or the pointer showed (week 12 review A11Y-W12-6), where AreaChart and BarChart start at the last point; only ArrowLeft and ArrowRight move, as in the other modes
D-biThe dashboard parts' composition (§9.5, §10.7)Adopted 2026-10-20 (week 12; builder default; the maintainer can override; recorded as a row by the week 12 review, R3-6). KpiTile, TrendCard and RankedList take Card's surface by its class and attributes (nz-card, data-tone, data-padding), not by rendering Card.Root, because data-slot is the part's own; KpiTile and TrendCard emit data-padding (sm, md) with no padding prop, so their manifests declare it as a fixed variant and the vocabulary says so. NumberValue is its own subpath (./number-value; the inventory proposed ./metric). Status labels are serialisable strings with a {label} placeholder. comparison is visible text and is not passed to Delta, so it is read once. Card's brand and accent tones are as Figma draws them: the Delta pill does not show on brand (bg/success-subtle equals the tint) and the two tints are the same green-950 in dark (Figma 04 issue 5): known deviations, open for design (week 12 review R2-7). Changed 2026-10-20 by M42 item 4: the alternative "a surface-coloured Delta pill on tints" is adopted (every Delta inside a brand or accent Card is a bg/surface pill, a deviation from Figma 04 gap 4 the maintainer chose); item 5 keeps the two tints equal in dark, as Figma draws them
D-bjKeeping the §16.5 box in a dashboard row (§16.5)Adopted 2026-10-20 (week 12 review A11Y-W12-2, A11Y-W12-3, A11Y-W12-4; builder default; the maintainer can override; confirmed 2026-10-20 by M42 item 16: TrendCard fills its container). KpiTile, TrendCard and RankedList set min-inline-size: 0, so a one-line error or an enlarged number cannot widen their column in one status; an enlarged number that does not fit overflows the tile instead (the docs suggest a row minimum in rem). TrendCard fills its container (inline-size: 100%) and its header is a size container: under 30rem the action goes below the headline and the change below the number, and TrendCard always reserves the change row, so the comparison keeps its room at phone width and the box still matches across statuses. A RankedList label is one line, ending in an ellipsis, because the reserved placeholder rows are one line (the whole label stays in the DOM). The browser test renders each status alone in its own cell at 100 % and 200 % text
D-bkPopover's API and surface (§12.3 Popover row)Adopted 2026-10-22 (week 13; builder default; the maintainer can override). Parts Root, Trigger, Popup, Title, Description, Close, each with render and data-slot except the engine-owned Popup (as Dialog.Popup, §8.2); no Body or Footer. side bottom and align center by default, sideOffset 8. No arrow: Figma draws no Popover, and its popups (Dropdown Menu 746:204, Date Range Picker 746:267) have none. The surface is theirs: bg/surface-raised, a 1 px border/subtle, radius/lg, Elevation/3, with 16 px padding, a 22.5rem (360 px) maximum width (--nz-popover-width sets a width), Heading/H4 titles and the spring entry of every popup. The bare Popover.Close is a 32 px tertiary IconButton in the top end corner, its markup written out as Dialog.Close's. Focus on open: the first tabbable control, passing over that corner close, else the content element. Modality is the engine's default (§12.3: "engine default modal"): the page is inert and scroll-locked while it is open, and Tab stays inside, as with Menu and Select
D-blDrawer's sides, parts and motion (§12.3 Drawer / Sheet row)Adopted 2026-10-22 (week 13; builder default; the maintainer can override). side="start" | "end" | "bottom" (default end, Figma's right edge in a left-to-right page); no top, no size, no modal={false} yet. The leaf resolves side to the physical edge with the engine's direction (DrawerEngine.useDirection) and writes it as data-side on the positioner and the popup, as §8.3 defines data-side; the CSS places the popup with the keyword values justify-self: left | right and the border-radius shorthand, because a logical inset keyed on a physical side lands on the wrong edge in right-to-left and the CSS contract allows logical properties only. Parts: Dialog's plus Header (Figma's Header frame: title, description or badges, the rule below); Body is the only scroller; Footer actions share its width. 440 px wide (Figma), less a 3rem strip of scrim on a narrow viewport; the sheet is the full width and as tall as its content up to that strip. Motion: the whole width (or height) on duration/slow + easing/enter, out on duration/base + easing/exit, the opacity with it, not the spring pair (its 3 % overshoot would open a gap at the edge). Reduced motion drops the slide and keeps the fade through a component rule that reads prefers-reduced-motion and [data-nz-motion="reduce"], the second such rule after Button's pending label (§13.2 rule 3; the markup is unchanged), because no token carries a whole width. The adapter re-exports the Dialog adapter (React Aria 1.22's Sheet is a preview), and Dialog and Drawer share one nesting context, so a modal opened from either adds no second scrim
D-bmThe Popover's nested-portal container (§12.1 L2)Adopted 2026-10-22 (week 13; builder default; the maintainer can override). Overlays opened inside a Popover portal into a box-less element (display: contents) the adapter places right after the popup, in the container the popup portaled into, with the family's top-layer exemption, as the Dialog's nested container has; it is not inside the popup, which scrolls (a clip) and scales while it enters. A hidden anchor <span> inside the popup creates it with a stable callback ref, in the commit that inserts the popup, so a Tooltip shown by the initial focus already finds it
D-bnOutside press inside the nested containers (§12.2 Dismiss stack)Adopted 2026-10-22 (week 13; builder default; the maintainer can override). Found while building O-12: React Aria's useInteractOutside ignores every press inside an element marked top-layer, and the family's nested containers carry that mark (D-af), so a press outside a Select, Menu, Popover or nested Dialog opened inside a Dialog never closed it (weeks 4 to 12; it stayed open until Escape). A fifth family completion, outside-press (_engines/react-aria/outside-press.client.tsx, in every modal popup of the family): a press that starts and ends outside the popup, inside a top-layer element that is not Nizivo's own top layer (data-nz-top-layer, the toast region and announcer, which dismiss nothing) and not on a tooltip, closes the topmost popup of the family (an alertdialog never). Every other press stays with the engine
D-boBudgets for ./popover and ./drawer (§17.1)Settled 2026-10-22 by M43 ("the first one, yes, raise the size. For the second one, yes, raise the size"): §17.1 Popover 39 and Sheet 35 KiB, and the dashboard set, now with Popover, 85 KiB. The original reading follows (week 13; not a raise). §17.1 holds r3's React-Aria-only figures for "Popover" (29.4, limit 32) and "Sheet" (26.3, limit 29). Nizivo's subpaths measure 35,821 B (35.0 KiB) and 32,498 B (31.7 KiB), over both, as Dialog's did before M17 raised it. They are gated at measured + 10 % (39 and 35 KiB) as new entries, PLAN_LIMITS_KIB is unchanged, and the dashboard set keeps its W5 definition without Popover (with it, about 1.5 KB more, over its 76 KiB). The family's popup.client would have gone over its 1.3 KiB own-code budget with the outside-press completion inside it; that completion is its own module with its own new budget
D-bpToast's shape (§12.6, §12.1 L3)Adopted 2026-10-22 (week 14; builder default; the maintainer can override). Tier own behind a ToastEngine (a v0.2 nz gen schema change: engine allowed on own). The queue is Nizivo's (src/toast/store.ts, no React, a module-level store read through useSyncExternalStore) and so are the clocks: React Aria's queue and timers are not used, its hooks get toasts with no timer. useToastRegion gives the landmark, pausing on hover and focus and where focus goes as a focused toast leaves; useToast the non-modal alertdialog role; its contentProps (role="alert") is dropped, so only the announcer speaks. Pausing is a set of reasons (region, hidden), and a clock runs only for a shown toast with a Toaster mounted. limit (3) shows the newest; older ones wait paused. The region is always on the page once hydrated, hidden and out of the top layer between toasts, so the landmark registers once. The announcer and region are portalled to <body> after hydration, so they are not in the server HTML (§12.6 asked for the announcer there; the Toaster is a client component and announcements only happen after hydration). Not built: the "peek" (scroll-padding already keeps focus above the stack), swipe, a promise toast, the native-dialog outlet (O-08)
D-bqBanner's tone name and dismissAdopted 2026-10-22 (week 14; builder default; the maintainer can override). tone="brand" for Figma's "Promo", as Badge and Card name it (handoff Q25 asks design); BannerDismiss is a tertiary md IconButton (40 px, the action's height; a 20 px glyph for Figma's bare 18 px icon). Below about 36rem the action wraps under the text and the dismiss keeps to the row's end
D-brLeaving the toast stackAdopted 2026-10-22 (week 14; builder default; the maintainer can override). §12.6 says "leaving returns focus to where it was" without a key: F8 pressed while focus is in the stack, or Escape on the stack itself (not on a toast), returns focus to the element focused before F8; Escape on a toast dismisses that toast and stops there, so a Dialog under the stack stays open (O-06). React Aria moves focus to the next toast, or back, as the focused one leaves
D-bsToast colours across schemesAdopted 2026-10-22 (week 14; builder default; the maintainer can override). Figma's bg/inverse, text/inverse and text/inverse-accent (M38) are page-scheme tokens, while the Button and IconButton inside need the inverse scheme for hover and focus. Each toast carries data-nz-scheme="inverse"; the three page-scheme colours are registered <color> custom properties (@property, Safari 16.4+, Firefox 128+) set on the region, so light-dark() resolves in the page's scheme there and the toasts inherit the colour. Tone icons resolve in the inverse scheme
D-btToast examples on the docs siteAdopted 2026-10-22 (week 14; builder default; the maintainer can override). An example that calls toast( is shown as source only (it runs in a client component of the app; the docs routes are Server Components), and a component route imports its example module only when one example is live, so the Toast page imports none (its examples use hooks)
D-buThe React matrix (§9.6, M45)Adopted 2026-10-23 (week 15; builder default; the maintainer can override). The workspace develops on React 19.3; the catalog adds react-19.2 and react-dom-19.2 (npm aliases of 19.2.9, published 2026-10-14). NZ_REACT=19.2 aliases every react and react-dom import to them in the node, browser and overlay vitest configs (packages/react/test/react-matrix.ts), with its own Vite cache and module cache; in the node lane a Node resolve hook does the same inside node_modules, because pnpm links react-dom 19.2.9's react peer to the workspace's 19.3.0 (a scoped pnpm override did not change that). A check in each lane fails a run whose react or react-dom is not the line it names, and hydrates a React Aria button on it. RELEASE_LANES gains test-react-19.2, browser-react-19.2 and overlay-react-19.2, Chromium only: §9.6's whole matrix (three engines × two lines) waits for v1.0, as §12.5 plans. Every other lane, the fixtures included, runs 19.3
D-bvThe family's open state under Activity (§12.2, O-15)Adopted 2026-10-23 (week 15; builder default; the maintainer can override). One hook, useOpenState (_engines/react-aria/open-state.client.tsx), in the Roots of Menu, Select, Popover, Tooltip, Dialog and Drawer: React Aria always gets a controlled isOpen (defaultOpen seeds the hook, never the engine). A popup opened after a committed closed render closes in a layout-effect cleanup: an uncontrolled one closes itself, and either way onOpenChange(false, { reason: 'hidden' }) fires; one open at mount (defaultOpen, or open from the server) never closes this way, so Strict Mode's mount, cleanup, mount changes nothing. React cannot tell a hidden Activity from an unmount, so a popup opened after mount that unmounts while open also reports 'hidden'. The close remounts the engine's root through a key (Select remounts only its popup, as its value lives in the engine's root)
D-bwA Toaster inside a hidden route (§12.6)Adopted 2026-10-23 (week 15; builder default; the maintainer can override). The plan says to mount the Toaster outside route Activity; one inside it hides its stack (hidden, out of the top layer) when the route is hidden, since React 19.2 leaves the portal on screen, and shows it again on return. Its toasts wait, their clocks stopped (the Toaster detaches)
D-bxDev-tool advisories (§19.3)Adopted 2026-10-23 (week 16; the maintainer asked "yes update the dev tools"). Vitest and @vitest/browser-playwright 5.0.4 → 5.0.5 (GHSA-p458-7748-mcp3, moderate: the browser-mode server misapplied server.fs.deny); the 5.0.4 minimumReleaseAgeExclude entries go. A scoped pnpm override, 'figma-api@2.0.1-beta>axios': 0.34.0: tokens-bruecke 3.18.0 (and its newest, 3.20.0) pins figma-api 2.0.1-beta, which pins axios 0.27.2 with 24 advisories (10 high); 0.34.0, on axios's maintained 0.x line, fixes all of them without an API change. Delete the override when a tokens-bruecke release drops axios 0.x. Not fixable: braces 3.0.3 (GHSA-vfj7-8cjw-p6xm, high), the newest release, used by the stylelint and size tooling with the repo's own glob patterns. Unverified: figma-api's network path under axios 0.34.0 (it needs a Figma token; the token export the repo runs is offline)
D-byPageHeader (§9.5, figma/08 §4)Adopted 2026-10-23 (week 16; builder default; the maintainer can override). @nizivo/react/page-header, tier server: a <header> with title (the page's one <h1>; level={2} keeps the look), meta beside it, description under it, actions at the row's end. Below --nz-sm the title takes Heading/H2's size (08's mobile frame swaps H1 for H2) and the actions start a row of their own: a viewport query, because a size container has no intrinsic width and collapsed the header in a shrink-to-fit parent (found in the visual lane). No breadcrumb slot: Pattern 2 draws the Breadcrumb above the header, as a sibling
D-bzTable's server parts (§16.2)Adopted 2026-10-23 (week 16; builder default; the maintainer can override). Eleven parts, no client module, no grid role. Figma's two rows disagree (Order Row 14 / 20 px and 57 tall, Product Row 12 / 20 and 65): cells take 12 / 20, on the scale; the container radius 12 (radius/md), header Label/M on bg/subtle. Cells keep to one line, so a wide table scrolls inside its container rather than wrapping. A stretched link: Table.Row interactive with a Table.RowLink, other controls above it (checked by hit tests in three engines). Table.SortButton takes href (an <a>) or render; the arrow follows the <th>'s sort through CSS, so no context is needed in a server tree. A new local vocabulary attribute, data-numeric (end-aligned, tabular figures), on Table.Head and Table.Cell: additive, so not breaking. A name for every table: Table.Caption (with visuallyHidden) or aria-labelledby
D-caThe overview in the Next fixture (§21.2 exit)Adopted 2026-10-23 (week 16; builder default; the maintainer can override). /dashboard gains 08's PageHeader (the greeting, the range as a SegmentedControl, as Figma draws it, and Export), the Banner with a dismiss that is a Server Action (a cookie; the page stops rendering it), Goals (a Card with a ProgressBar) and Recent orders (a Card with a Table of four linked rows). Its Nizivo client modules become three: the charts island, the SegmentedControl and BannerDismiss (week 12 recorded the island alone). /dashboard-kpi is unchanged and ships none
D-cbThe Vite SSR and React Router 8 fixtures (§18.1, §21.2)Adopted 2026-10-23 (week 17; builder default; the maintainer can override). Two new fixtures, each its own pnpm workspace root on the packed packages with the monorepo's supply-chain policy, and two release lanes (fixture-vite-ssr, fixture-react-router; 19 lanes in all). fixtures/vite-ssr: fixtures/vite's pages, vite build plus vite build --ssr, renderToString behind a node:http server, no ssr.noExternal; the pages share one client bundle (renderToString does not wait for lazy chunks), so its per-page client-module checks are off. fixtures/react-router: React Router 8.5.0 framework mode on Vite 8.3.4, CSS through the root route's links with ?url, served by @react-router/node's request listener; routes are code-split, so the per-route client-module checks run. It writes its own entry.server.jsx and entry.client.jsx, because with the defaults react-router build ran npm install isbot by itself. One supply-chain exception, in that fixture only: trustPolicyExclude: semver@6.3.1 (@react-router/dev > @babel/core), a provenance downgrade on semver's 6.x maintenance line by an npm CLI maintainer, like undici-types in the root. Both fixtures have a negative control (a hydration mismatch fails the page)
D-ccThe second Tooltip adapter (§9.4, §9.6)Adopted 2026-10-23 (week 18; builder default, chosen at the maintainer's word, M47 (5): "do what you think best"; the maintainer can override). Layout: "adapter": "hooks" in an engine-tier component.json names src/_engines/<engine>/<name>.hooks.client.tsx in the same family; nz gen checks that it exports every <Pascal>Engine member, as the default adapter does, and engine.ts keeps re-exporting the default. Tests: NZ_ADAPTER=hooks makes packages/react/test/adapters.ts resolve the component's engine.ts to the second adapter in the node, browser and overlay vitest configs, and each lane's adapter spec fails if the swap did not happen; five release lanes run it (test-tooltip-hooks, browser-tooltip-hooks, overlay-tooltip-hooks-{chromium,webkit,firefox}), the node one with its own module cache. Not shipped yet: the size checks and the release directive guard leave a second adapter out (packages/tooling/gen/adapter.ts), and its one new import, react-stately (the ^3.51.0 react-aria itself depends on), is a devDependency of @nizivo/react until the adapter becomes the default after two green weeks (§9.4); then it moves to dependencies, a maintainer decision. The adapter: useTooltipTriggerState, useTooltipTrigger, useTooltip, useOverlayPosition and useFocusable, a plain portal on the family's PortalProvider (not react-aria's Overlay, whose FocusScope cost about 9 KB minified), and its own data-entering/data-exiting hold. Measured (esbuild, minified, gzip, React external): ./tooltip is 15,052 B on the hooks adapter and 16,775 B on the components adapter, 10 % less, not §9.4's 7.2 vs 14.3 KB (React Aria's Tooltip component is a thin layer over the same hooks; most of the bytes are the hooks themselves)
D-cdThe React Compiler lane (§19, D14-hooks)Adopted 2026-10-23 (week 18, M47 (4); builder default for the shape; the maintainer can override). packages/tooling/react-hooks-lint: ESLint 10.13.0 with only eslint-plugin-react-hooks 7.1.1 recommended-latest, on packages/react/src/**/*.client.*, src/_engines/** and packages/charts/src/**/*.client.*; typescript-eslint 8.72.0's parser on the typescript6 named catalog (typescript: npm:@typescript/typescript6@6.0.2), so typescript is TS 6 in that package only. lint.ts runs ESLint's Node API from the repo root (ESLint 10 ignores files outside a config file's folder). Release lane react-hooks with blocking: false: a failure is reported by lanes.ts and the release script and stops nothing; deleting that line makes it blocking. The root takes the React Router fixture's trustPolicyExclude: semver@6.3.1 (M47 (3)). One eslint-disable-next-line react-hooks/<rule> -- reason comment silences both ESLint and oxlint (oxlint reads the react-hooks names), so the two react/refs exceptions moved to it. Adding @babel/core to the tree resolves next's and styled-jsx's optional @babel/core peer in the lockfile; nothing else changed
D-ceDocs search entries for slotsAdopted 2026-10-23 (week 18; builder default). The generated pages index each slot as its own search entry, not one entry per component: Table's six table-select-* slots in one entry outranked the Select page for "Select"
D-cfRow selection's subpath (§16.2), M48Adopted 2026-10-23 (week 18; the subpath is the maintainer's choice, M48; the shape is a builder default). TableSelection, TableSelectAll and TableSelectRow are flat exports of @nizivo/react/table-selection (component table-selection, tier server with one client export, as Alert's AlertDismiss); Table's namespace keeps its 11 server parts. A namespace object references every part, so a client part inside a server namespace ships to every page that uses it; nz gen now rejects that. table.css paints a row with a checked TableSelectRow (:has) and narrows the selection column, so --nz-table-row-bg-selected stays Table's one hook; table-selection.css holds only the island's display: contents. Measured: ./table 1,661 B (limit 1.8 KiB, as in week 16), ./table-selection 1,743 B (1.9 KiB), its island alone 783 B. The Next fixture's /dashboard (Table, no selection) ships no Table module in Turbopack, webpack or webpack with optimizePackageImports
D-cgFileUpload (§9.5, §16.1)Adopted 2026-10-23 (week 19; builder default; the maintainer can override). @nizivo/react/file-upload, tier own behind FileUploadEngine (useDropTarget). The adapter is written on the DOM's drag events, not React Aria's useDrop: useDrop brought React Aria's drag-and-drop strings for every locale, 27,659 B on ./file-upload and +21.5 KB on the root barrel; on the DOM it is 5,608 B (a depth counter for enter and leave, types.includes('Files'), dataTransfer.files). The native input is the form's source of truth: the component checks each pick or drop against accept and maxSize, writes the list back into input.files, and never uploads; the app sends files keyed by the ids onFilesChange gives and passes each one's state back as items (status, progress, error). Before hydration the input covers the zone, so a plain form submits chosen files. New local vocabulary attribute data-drag-over. Hint in text/secondary (Figma's text/tertiary is 4.16:1 on bg/subtle); decimal size units
D-chForm, SubmitButton, FormMessage, PasswordInput (§16.1)Adopted 2026-10-23 (week 19; builder default; the maintainer can override). Form (client) provides errors by the Field's name through a FormScope context that FieldScope reads, the second channel of §16.1's one error channel; editing a control hides its server error until a new errors object arrives; focus moves to the first invalid control (or to the summary, with summary) on each new errors, never on the first render; reset events are ignored. SubmitButton is a primary Button with pending from useFormStatus. FormMessage (server) is a live container (role="alert" for critical) that renders an Alert only with content. PasswordInput (client, uncontrolled) is Input's box with an aria-pressed toggle under a constant label, reverting to type="password" in a capture-phase submit listener; internal eye and eye-off glyphs. Edge's own reveal button is not hidden (::-ms-reveal fails the browser-floor CSS check)
D-ciNavItem, SearchField, ChipGroup, ChoiceCard (M49)Adopted 2026-10-23 (week 19; builder default; the maintainer can override). NavItem (server): href or render, current → aria-current="page", icon, count (0 shows, absent hides), disabled as a role="link" span; 40 px; the future Sidebar's [data-collapsed] makes it a 40 × 40 square whose label stays in the name and whose count becomes a dot; hover bg/muted (Figma's bg/hover equals bg/canvas). SearchField (client leaf): <search> around type="search", sizes 32/40/48, shortcut (Kbd + aria-keyshortcuts, not bound), pending, a clear button outside the Tab order with Escape as the keyboard route; rest edge border/control (Figma's border/subtle is 1.07:1). ChipGroup (client): radios under role="radiogroup", or checkboxes under role="group" with multiple; unavailable chips stay choosable with a strike; a 24 px swatch leading; an inset ring so selection never changes size. ChoiceCardGroup + ChoiceCard (client, a Field group control): a real 20 px radio, leading, description, trailing (the "Default" Badge); 76 px with 16 px padding (Figma's 73/74 uses an off-scale 14)
D-cjSplit and StickyBarAdopted 2026-10-23 (week 19; builder default; the maintainer can override). Split (server): Split.Root / Main / Aside, a wrapping flex row; the aside asks for 360 px (aside="wide") or 232 px (narrow), the main column keeps at least 60 % of the row or the two stack in source order; no media or container query. New vocabulary values data-aside. StickyBar (server): summary and actions, position: sticky at --nz-layout-bottom-inset in the --nz-z-sticky band, bottom padding plus env(safe-area-inset-bottom) where Figma drew 28 px; it reads the shell's bottom inset and writes none (the shell sets it, §12.6)
D-ckThe shell: TopBar, MobileTopBar, AppShell (M49)Adopted 2026-10-23 (week 20; builder default; the maintainer can override). TopBar and MobileTopBar (server): <header>s with slots (title or start, search, actions; mark or leading, title, actions); the title is text in Heading/H3 or H4's look, not a heading, because the page's PageHeader holds its one <h1>; neither is sticky (Figma has no scrolled state, and a sticky bar would oblige the shell to write --nz-layout-top-inset); MobileTopBar's search and bell are the app's IconButtons, not Figma's bare 20 px icons. AppShell (server, no client code of its own): places sidebar, topBar, mobileTopBar and tabBar; the server renders both sets of bars and CSS shows one at each width (the hidden one is display: none, out of the accessibility tree); it owns the page's <main tabindex="-1"> (mainId, or a generated id) and a skip link to it, the first Tab stop; <main> is capped at layout/content-max plus the grid's margin and, below sm, pads for the tab bar through --nz-layout-bottom-inset
D-clSidebar and the rail (M49)Adopted 2026-10-23 (week 20; builder default; the maintainer can override). Server: Sidebar.Root (a <nav> named "Primary"), Header (mark, title, description; Figma's Store Switcher as a card, render for a link or a switcher trigger), Content, Footer (an auto margin, Figma's Spacer). The width is layout/sidebar (0 / 72 / 264 px at sm / lg), so the viewport switch needs no script; collapsed (true keeps the rail, false the full width, from sm) for an app's own toggle, as data-collapsed / data-expanded. The root is a size container nz-sidebar: NavItem folds to its 40 px square under @container nz-sidebar (inline-size < 10rem), so the viewport's rail and collapsed draw the same thing, and its week 19 rule on any data-collapsed ancestor is gone (the NavItem collapsed example is now a collapsed Sidebar). Changed by M52: the rail's labels were first CSS beside the item (no Escape to hide one, WCAG 1.4.13); the maintainer chose the Tooltip. SidebarItem (a client export of ./sidebar) renders NavItem's link as the trigger of a controlled Tooltip that opens only while the item's label is out of the layout (the rail), at the end side, without aria-describedby; the Tooltip is portalled, so the rail scrolls like the expanded Sidebar
D-cmMobileTabBar and the More sheet (M49)Adopted 2026-10-23 (week 20; builder default; the maintainer can override). Split: MobileTabBar.Root (<nav> named "Primary" around a <ul>) and MobileTabBar.Item (an <li> around a link: current → aria-current="page", icon in Figma's 52 × 28 indicator pill, count on its corner as part of the link's text) are server parts; MobileTabBarMore is a client export (nz gen forbids a client part in a server namespace) that owns its own bottom Drawer, so the trigger sits in the list, instead of §12's "the More Drawer rendered by AppShell": a button with aria-haspopup and aria-expanded, the sheet titled "More", closed by Escape, an outside press or a followed link; current paints its pill (data-current, a button has no aria-current). The root renders a second client part, MobileTabBarInset: a layout effect and a ResizeObserver write the bar's height to --nz-layout-bottom-inset and set data-nz-bottom-bar on <html> while it is laid out, and clear both at height 0 (the shell hides it from sm) or on unmount (§12.6). The bar's padding holds the safe area, so StickyBar and the toast region no longer add env(safe-area-inset-bottom) again under data-nz-bottom-bar. The internal glyph ellipsis (Lucide's, Feather's more-horizontal; THIRD_PARTY_NOTICES lists thirteen). The bar is not fixed on its own: AppShell pins it below sm
D-cnThe patterns workspace (decision 5, D3-dist)Adopted 2026-10-23 (week 21; builder default; the maintainer can override). patterns/ is the private workspace package @nizivo/patterns, MIT (patterns/LICENSE). A pattern is a folder of patterns/src/: <name>.example.tsx and, when it needs layout the components do not give, <name>.css. It imports only public entries (@nizivo/react/<subpath>, @nizivo/charts or @nizivo/charts/<part>, @nizivo/icons or @nizivo/icons/brands/<name>, React); it has no 'use client' file of its own, so every interaction is a package's client component. Its stylesheet keeps the packages' CSS contract with its own namespace and layer (patterns/stylelint.config.js): classes nzp-<pattern>[-<part>], every rule in nz.patterns, tokens only, logical properties, and no custom media, because a copied file has no build step that knows --nz-sm. A node test renders every example from the built packages and holds the folder shape and the import list. The docs give each pattern its own route, as the charts parts have, so a page loads only its own client modules, and import the pattern sheets after the packages' (.generated/patterns.css). Images are neutral placeholder blocks, so the docs load no remote image. The 08 templates render the content of AppShell's <main> only (PageHeader at level={2}, since the docs page holds the <h1>); each page shows the wrapping as source. Patterns are not in the react visual lane: the docs smoke covers them (axe light and dark, the 375 px reflow, the client-module allowlist)
D-coThe v0.2 patterns (§21.2)Adopted 2026-10-23 (week 21; builder default; the maintainer can override). 23 patterns. 05 Commerce (10): order summary, product card, variant picker, product table, order table, discount card, payment method, shipment tracker, plan card, customer card. 10 Instagram Storefront (5): collection circles, shoppable post (with the Post Badge), product tag, tagged product, storefront bar. 08 Overview Dashboard (8): the Products, Product detail, Orders (with the order in a Drawer), Customers, Discounts, Analytics, Plan and billing, and Onboarding pages. Not built: the storefront's shader hero (@nizivo/lab, after 1.0) and HotspotEditor (a heavy client editor); the Analytics funnel and heatmap (no component); a copy button for discount codes (no CopyButton, and a pattern adds no client code); card-brand logos (@nizivo/icons has only Instagram); a Steps component (the shipment tracker is an <ol> in its own stylesheet); a bulk-action bar (Figma draws none). Where they differ from Figma, each page's Notes says why: text/secondary wherever Figma's text/tertiary fails AA; an expired discount keeps full contrast (no opacity: 0.70); sold-out variant chips stay choosable and are described "Sold out"; the tracker starts at Ordered and draws the upcoming node with a dashed ring (Figma's fill is 1.04:1); new and seen collection circles differ by more than a 1.2:1 ring (a 2 px brand ring, a bold name and spoken text) and keep one size; text on a photo sits on bg/code with text/on-dark, the only token pair that stays dark and white in both modes (17.36:1 light, 18.69:1 dark), until design adds an on-media family (figma/10 §8.3)
D-cpStrings: ./i18n and ./locales/en-US (§16.4, K10)Adopted 2026-10-23 (week 21; builder default; the maintainer can override). @nizivo/react/i18n: formatLabel(template, values = {}, locale = 'en-US'), with the types PluralLabel ({ other } plus any of zero, one, two, few, many), PluralCategory and LabelValues; values.count selects the plural category through Intl.PluralRules, {name} placeholders are filled from values (numbers through Intl.NumberFormat(locale)), an unknown placeholder stays as written; server-safe, 208 B. @nizivo/react/locales/en-US: enUS, every built-in string keyed by component (24 keys), typed by each part's <Name>Labels with type-only imports, 664 B; the root barrel does not re-export it. Left out: Menu (its only string, dismiss, is engine-owned), Select (its placeholder default is React Aria's) and Tooltip (no strings); Dialog, Drawer and Popover carry only close, because dismiss cannot be set through labels; KpiTile and TrendCard repeat Metric's strings, which they pass on. Only labels with a {count} placeholder take plural objects: AvatarGroup's more and overflow (string | PluralLabel); English defaults stay strings. Tests: formatLabel in en-US, pl and ar; a drift test that reads every Labels member's @default and every fallback in code and requires both to equal the catalogue (it fails on a missing key too); a plural lint over every catalogue in src/locales/, against Intl.PluralRules(tag).resolvedOptions().pluralCategories (en-US has no plural object, so bad fixtures prove it). A global NizivoConfig.labels route is not built
D-cqThe chart-draw specimen (§13.3)Adopted 2026-10-23 (week 21; builder default; the maintainer can override). The specimen is the shipped recipe, tested, not a new one: AreaChart's plot reveal (nz-chart-reveal, clip-path), BarChart's grow (nz-bar-chart-grow) and DonutChart's settle (nz-donut-chart-reveal: opacity and a 0.92 to 1 scale; it does not sweep) run 700 ms on --nz-duration-chart-draw with easing/standard; under Reduced motion (the media query or data-nz-motion="reduce") the duration is 0 s and the first frame equals the drawn end state. test/browser/chart-draw.spec.tsx checks both in Chromium, Firefox and WebKit, through a Playwright emulateMedia command in the charts browser config. Figma 09's stroke-dash recipe (§4.2) is not built. The motion foundations page gets a Chart draw section and links to the three chart pages
D-crGlobal labels, as built (M55, M58)Adopted 2026-10-24 (week 22; builder default; the maintainer can override). NizivoConfig labels is NizivoLabels, derived from the enUS catalogue's type ({ [K in keyof typeof enUS]?: Partial<…> }), exported from @nizivo/react/config. An internal useLabels(key, own) resolves each string: the part's own labels, key by key, then the nearest provider, then the English default, which stays where it was so the drift test (D-cp) is unchanged. It reaches 13 client parts: AlertDismiss, BannerDismiss, ChipGroup, Dialog.Close, Drawer.Close, FileUpload (all 14 strings), Form, MobileTabBarMore (its sheet's Close takes mobileTabBarMore.close), PasswordInput, Popover.Close, SearchField, SubmitButton (button.pending, passed to Button) and the Toaster (which must sit inside the provider). Server parts read no context and keep per-instance labels; the Strings guide gives a wrap-once file for them. The module-graph test helper follows value imports only (type imports on request), since a type derived from the catalogue would otherwise reach every component
D-csThe locale-determinism spec (M56)Adopted 2026-10-24 (week 22; builder default; the maintainer can override). test/browser/locale/locale-determinism.spec.tsx runs in a second instance of the browser lane, <engine>-de-DE, whose Playwright context has locale: 'de-DE' (so navigator.language and Intl's default are German). Every exported example (143) is rendered with renderToString, hydrated with hydrateRoot, and its text and aria-label, aria-valuetext, title, placeholder and value must not change, with no recoverable error and no console error. Because the server render runs in the same German page, three more checks catch a part formatting with the default locale: no German number or date format and no React Aria de-DE string anywhere in the page (popups included), and no Intl call without a locale while an example renders or hydrates. Examples that set locale="de-DE" themselves are left out of the German check, which must still find German text there. It runs wherever the browser lane runs (Chromium and Firefox in the lanes; WebKit passes too). The default instance takes only test/browser/*.spec.tsx, and the config refuses to start if a spec sits in any other subfolder
D-ctCharts at narrow widthsAdopted 2026-10-24 (week 22; builder default; the maintainer can override). The stacked DonutChart's hidden card was approved by Dayitava on 2026-10-26 (M63). .nz-chart-frame is a named size container (nz-chart). Under 31rem a frame with axes keeps a 12rem body (about a 10rem plot), with max-inline-size: 100% because Firefox and WebKit turn a minimum block size into a minimum width through aspect-ratio; under 25rem every other x label hides (visibility: hidden), the first and last always shown, for AreaChart and BarChart; every value stays in the data table. DonutChart under 24rem: the legend goes under the ring (rows auto minmax(0, 1fr) auto auto, names wrap, values never break), the ring is centred and sized by the frame (min(--nz-donut-chart-size, 100cqi)), the centre total steps down to Numeric/M over Body/S under a 10rem ring and is clipped (still read) under 6rem, and the island's hover and focus card is hidden: stacked, it had about 86 px and covered the ring; the centre still shows the slice's share and name, the legend row its value, and the announcement is unchanged. Showing a card there needs an island change (exposing the point's x to CSS), not built: for the accessibility owner. The visual lane adds each charts part's basic example in a 20rem stage (28 baselines)
D-cuThe Arabic font in the visual baselinesAdopted 2026-10-24 (after the 0.2.0 release; builder default; the maintainer can override). Geist has no Arabic, and Chromium on macOS fell back to SF Arabic in some runs and modes and Geeza Pro in others, so the committed area-chart/rtl baselines held both (the CLAUDE.md flake). The visual harness (packages/react/scripts/visual/run.ts) adds Geeza Pro's regular and bold faces to the Geist family by unicode-range (local() can name Geeza Pro; SF Arabic is a hidden system font it cannot), and fontsReady fails if they do not load. It changes only the harness page, not what Nizivo ships: an app's Arabic text still falls back to the reader's system fonts. 7 of the 8 RTL baselines were re-recorded to the pinned render (the Arabic glyphs only); with the faces disabled, 5 of them fail. SF Arabic cannot be named from CSS; a bundled OFL Arabic font (Noto Sans Arabic, say) would work on any machine but adds a dependency
D-cvQuantityStepper (§16.1)Adopted 2026-10-27 (v0.3 week 1; builder default; the maintainer can override). A native <input type="number"> in Input's box between a minus and a plus <button type="button" tabIndex={-1}>. Tab reaches only the input and its arrow keys step it; a press keeps focus where it was (mousedown is cancelled), steps with stepUp() / stepDown() (an empty input starts at min) and fires the native input and change events. Each button is disabled at its end of the range, and both while read-only or disabled. min 0 and step 1 by default, no max; onValueChange(number | null, { reason: 'increment' | 'decrement' | 'input' }). Since 2026-10-27 it follows Figma's Number Stepper 7367:377 (the week 1 build had missed it): the buttons are named "Decrease quantity" and "Increase quantity" (labels, catalogue key quantityStepper) with aria-controls on the input; each is a full-height square as wide as the control is tall (40 x 40 at md; Figma draws md only), with no padding in the box; the number is centred in Label/M (Input's larger line under a coarse pointer, for iOS zoom) and the input is 56 px wide (--nz-quantity-stepper-input-width), so md is Figma's 136 x 40. The storefront's own Quantity frame 7275:20465 (120 x 48, Numeric/M) is not followed: the component wins. No press-and-hold repeat; Figma draws no hover, and code keeps the bg/hover pointer hint. The plus glyph joins the internal glyphs (Lucide, Feather-derived)
D-cwNumberField (§16.1)Adopted 2026-10-27 (v0.3 week 1; builder default; the maintainer can override). Parts NumberField.Root (Input's box), Input, and optional Decrement and Increment on React Aria's NumberField; formatOptions takes Intl.NumberFormat options in NizivoConfig's locale, the server HTML is formatted, and a hidden input carries the plain number. The step buttons have QuantityStepper's shape since 2026-10-27 (the maintainer: "make numberfield buttons match the stepper"): with buttons the box has no padding and each is a full-height square as wide as the control is tall (40 x 40 at md); they keep React Aria's localized names, which end with the field's label ("Increase Price"): with a Field the button names itself and then the label through aria-labelledby, as React Aria does for its own label; labels replaces them per field, and they are engine-owned, not in the catalogue. The input's aria-describedby keeps only the Field's ids: React Aria's own description and error slot ids point at nothing here. React Aria's "Number field" role description and its title="" after hydration are kept. The text is centred only when step buttons are present
D-cxCombobox (§12.3, §16.1)Adopted 2026-10-27 (v0.3 week 1; builder default; the maintainer can override). Parts Combobox.Root, Input, Trigger (optional), Popup, Item, Group, GroupLabel, Separator on React Aria's ComboBox, in the overlay family. Root renders the box (React Aria's Group, combobox-control) around its children, so the popup lines up with the whole box; the popup and rows are Select's. The list filters with React Aria's "contains" in the locale's collation and closes when nothing matches; no custom filter, async loading or empty-state row. React Aria keeps the open state, so there is defaultOpen (opened once after mount) and onOpenChange but no controlled open; a list opened after mount closes under a hidden Activity with reason 'hidden' (O-15). With menuTrigger="focus" a press on the input opens it too. The input carries data-open. The overlay conformance suite gains a ring slot (the element that draws a trigger's focus ring, here the box) and lets a pressed trigger keep focus
D-cyAccordion (§8.3 rule 2, §21.3)Adopted 2026-10-27 (v0.3 week 2; builder default; the maintainer can override). Server parts Accordion.Root, Item (a native <details>), Trigger (its <summary>, with a 16 px chevron that points down while closed and up while open) and Panel, with no client JavaScript. Since 2026-10-27 the look follows Figma's Accordion Item 7370:88 (the week 2 build had missed it): a border/subtle hairline below each item and none above the first, a 52 px trigger (16 px above and below, no inline padding, gap 12) in Label/M Strong with an icon/secondary chevron, the panel's text from the item's edge with 16 px below, and no hover fill (Figma draws none). Figma's description asks for a <button> in a heading with aria-expanded and a duration/base height animation; this row keeps <summary> (it exposes the expanded state itself; a heading can go inside the summary if wanted) and no animation. The maintainer, 2026-10-27: "keep details". Accordion.Root name gives every item that sets none the same <details name>, so they open one at a time with no script (Firefox 129 at the floor does not group them; several can be open there); defaultOpen is the first render's open. No height animation: ::details-content and interpolate-size are past the floor. The stylelint vocabulary rule and the emitted-CSS check allow [open] on .nz-accordion-item alone, the element's own state under §8.3 rule 1 (rule 2 now names the exception); .nz-button[open] and .nz-dialog-popup[open] still fail
D-czPageIndicator (§21.3)Adopted 2026-10-27 (v0.3 week 2; builder default; the maintainer can override). One server part: variant dots (6 px dots 6 px apart, the current one 16 px long and bg/brand, the others border/strong) or fraction ("2 / 9" in a Label/M pill on bg/subtle); tone="media" over a photo uses text/on-dark dots and a bg/code chip, the stand-in M57 set until design adds on-media tokens. The visual is aria-hidden; a visually hidden "2 of 9" (labels.position, catalogue key pageIndicator) says the position; current is clamped to 1…count; no dot windowing for long sets (the fraction is for those)
D-daBulkActionBar (§16.2)Adopted 2026-10-27 (v0.3 week 2; builder default; the maintainer can override). A server part of its own subpath, @nizivo/react/bulk-action-bar: a count, then the consumer's actions, on bg/subtle. defaultCount (the rows checked when the page renders) lets the server write the count, so hydration changes nothing; inside TableSelection, the island then keeps it in step from a hidden template beside it (labels.selected, '{count} selected', catalogue key bulkActionBar), in the bar's locale, aria-live="polite". Without defaultCount the count shows once the island runs; the actions work before hydration through the rows' form. The bar stays in place with nothing selected (no layout shift); it is not sticky (StickyBar can hold it)
D-dbDataView (§16.2)Adopted 2026-10-27 (v0.3 week 3; builder default; the maintainer can override). Server parts DataView.Root (a size container, nz-data-view), Table (a <div> for a Table.Container), List (a <ul role="list">), Item (an <li>) and ItemLink. The table shows from 40rem (sm) of the root's own width, the list below; the other is display: none, out of the accessibility tree and the tab order; each carries data-presentation. Rows follow Figma's Order List Item 764:3483 (14 / 16 px padding, 12 px apart, 44 px minimum, a border/subtle hairline below); interactive makes ItemLink a stretched link with the hover fill, as Table.Row. Both presentations are in the HTML, so the docs warn against repeated ids and names and keep a selection in the table only. No OrderListItem component: the row's content is the consumer's
D-dcSurface, material="glass" (§13, §21.3)Adopted 2026-10-27 (v0.3 week 3; builder default; the maintainer can override). One server part at @nizivo/react/surface: bg/surface, a border/subtle hairline, radius/xl, render for any element. glass is Figma's Nizivo/Surface/Glass: bg/surface at 72 % (the floating Site Navbar 7392:80's fill; the Lab card's white at 42 % does not follow dark) over a 24 px backdrop-filter blur (a literal: no blur token exists, §7), with Elevation/3's shadow (Figma's is 0 16 40 -8 at 10 %, Elevation/3's 0 16 32 -8 at 8 % with the dark ring). Opaque without backdrop-filter (Safari before 18, the 17.5 floor), under prefers-reduced-transparency: reduce (an opt-out, since Firefox and Safari do not know the feature) and in forced colours (Canvas). No public hooks: --nz-surface-* is a token family (--nz-surface-inner-highlight); it never declares the shared --nz-surface (M42). Figma's Liquid Glass effect stays out of core (@nizivo/lab)
D-ddPointerFx and usePointerFx (§13, r6 §3)Adopted 2026-10-27 (v0.3 week 3; builder default; the maintainer can override). At @nizivo/react/pointer-fx, its own subpath (M69), so a CountUp page never ships it. The hook returns { ref, 'data-pointer-fx' } to spread; PointerFx is the same as a component (render={<Card />}) so a server page can use it. A mouse over the element writes --nz-pointer-x and --nz-pointer-y (-1 to 1; two new runtime variables) once per animation frame, and 0 on leave; effect="tilt" turns it up to 6 degrees (a literal: no tilt token exists, r6's sketch) and lifts it by --nz-distance-lift while hovered, effect="magnetic" moves it by up to --nz-magnetic-pull. Only pointerType mouse under (hover: hover) and (pointer: fine); under reduced motion (--nz-duration-fast 0 ms) it writes nothing. The server renders only the attribute
D-deFunnelChart (§14.1)Adopted 2026-10-27 (v0.3 week 4; builder default; the maintainer can override). @nizivo/charts/funnel-chart, server only, no island. Figma 03's Funnel Chart 751:319: per step the name (Label/M, text/secondary, at most 150 px), a track (chart/track, 36 px, radius 8) with the bar (chart/primary) as long as the step's share of the first, the count (Numeric/M) and the share of the previous step (Mono/M, text/brand; "—" in text/tertiary on the first), 16 px between columns, 12 between rows. Bars are SVG rect widths in percent (no inline style, CSP Profile B). funnelSteps is the exported rule: input order, non-finite and negative as 0, bars clamped to 0–1, no share on the first step or after a step of 0. The rows are aria-hidden; the data table is step, count, % of previous (Figma's description). shareFormat defaults to a percent with one fraction digit. The body sizes to its rows; loading, empty and error keep Figma's four-step height (11.25rem). Makes no keyboard claim (conformance 'none')
D-dfHeatmap (§14.1)Adopted 2026-10-27 (v0.3 week 4; builder default; the maintainer can override). @nizivo/charts/heatmap, server only, no island. Figma 03's Heatmap 751:393: row labels (Label/M, text/tertiary, 44 px), square cells (at most 32 px, 6 px apart, radius 6 as --nz-radius-xs × 1.5) that shrink together, column labels (Mono/M; xTicks, default at most 6) and the "Fewer … More" scale inside the plot, under the grid, as Figma places it (ready only). The ramp is Figma's alphas of chart/primary, 8 / 25 / 45 / 70 / 100 %, as color-mix over transparent from data-step 1–5 (a new vocabulary attribute); heatmapStep is the exported rule: five equal widths over 0 to the largest value, 0 and negatives step 1, a non-finite value no step (a hairline cell, labels.missing in the table). Rows and columns in first-seen order (heatmapGrid). The grid is aria-hidden; the data table has one row per grid row. Forced colours: Highlight mixed into Canvas at the same strengths, every cell outlined in CanvasText. Solid ramp colours for dark stay a design question (M65)
D-dgDateRangePicker (§16.3)Adopted 2026-10-27 (v0.3 week 5; builder default; the maintainer can override). @nizivo/react/unstable/date-range-picker, the first component under src/unstable/ (tier own with the react-aria engine: one DateRangePicker export, no compound parts). Figma's Date Range Picker 746:267: a trigger in Select's look (nz-select-trigger on the same element) with a calendar glyph (Lucide, Feather-derived) opens the Popover (its focus, Escape, outside press and return); inside, the presets (Today, Yesterday, Last 7 days, Last 30 days, This quarter, Year to date; presets orders or empties them; a chosen one is aria-pressed), one month of React Aria's RangeCalendar (_engines/react-aria/date-range-picker.client.tsx, the only module importing @internationalized/date, now a direct dependency) and a footer: the chosen range, Cancel and Apply. Nothing applies until Apply (Figma answers §16.3's Apply-or-instant question; one month, its other); Cancel, Escape and an outside press keep the old range. Values { start, end } ISO plain dates; timeZone required, "today" from Intl in it (presets and the today mark, client only); min/max; name gives one hidden input with the ISO interval start/end. The range reads labels.range ('{start} – {end}') with each end formatted alone (never formatRange, whose spacing differs between ICU versions), the start without its year when both share it. Day cells: data-range-start/-end, data-selected, data-outside-month, data-today (already in the vocabulary). Inside a Field the trigger is named by its value and the label, and a label press focuses it without opening. The popup is 33rem (Figma 525 px), as wide as the month without presets; under 30rem of its own width the presets become a row above the month. React Aria moves the keyboard on a day after a range's first Enter (documented). Tooling: the docs catalog, example generator and routes, the API loader and the visual lane read unstable/<name> (page at the bare name); the visual lane shoots a Popover-composing component open, and its harness fixes Date at 8 October 2026 (Figma's today) only while the picker's examples are mounted (a page-wide Playwright clock unsettled Toast's and Combobox's shots); the five app fixtures' lockfiles take the new dependency
D-dhHotkeys (§17 G18; gap report A §5.4)Adopted 2026-10-27 (v0.3 week 6; builder default; the maintainer can override). @nizivo/react/hotkeys (M72): registerHotkey(hotkey) returns its remover, useHotkey(hotkey) registers while mounted (a layout effect, so an Activity-hidden component unregisters; run through useEffectEvent), useHotkeys() lists { id, keys, description } (useSyncExternalStore, empty on the server). One module-level registry, no provider; one window listener rebuilt per change over tinykeys 4.0.1 (MIT, zero dependencies, 1.0 KB), a new runtime dependency through the catalog. Keys are written as Kbd and IconButton take them ("mod+k"), translated by toTinykeys ("$mod+k"). Policy enforced in our code, not tinykeys' defaults: nothing during an IME composition (isComposing, key code 229); nothing while the target is a field or editable unless allowInInputs; a single printable key warns in development (WCAG 2.1.4); a repeated id replaces the first
D-diCommandMenu (§17 G18; gap report A §5.3)Adopted 2026-10-27 (v0.3 week 6; builder default; the maintainer can override). @nizivo/react/unstable/command-menu, tier own with the react-aria engine, parts Root, Trigger, Popup, Input, List, Group, Item, Footer. It composes the Dialog (scrim, focus trap, Escape, return focus) as the DateRangePicker composes the Popover, so no overlay: true manifest; inside, React Aria's Autocomplete joins a SearchField (focused on open) to a Menu: filter 'contains' or 'startsWith' with useFilter({ sensitivity: 'base' }), the arrow keys move a virtual active row, Enter runs it, Escape clears the field and then closes. Root registers hotkey ('mod+k', null for none, allowInInputs) through ./hotkeys; Trigger names it in aria-keyshortcuts. Item: href or onAction (then closes), icon, kind, shortcut (a Kbd and aria-keyshortcuts). Figma's 755:889: 560 px, radius/xl, bg/surface, the search row, rows radius 10 (radius/sm × 1.25) with bg/hover active, Select's group label, the footer on bg/subtle. Not yet (recorded, unstable): async sources, a per-group cap, virtualisation, the polite result count, the lazy chunk (M72), a field-looking trigger (consumers pass render)
D-djThe copy-paste registry (§19.3, decision 5, K13)Adopted 2026-10-27 (v0.3 week 7; builder default; the maintainer can override). apps/docs/scripts/registry.ts, run by the docs build: /r/v<version>/<name>.json, index.json and manifest.json (the SHA-256 of every other file), /r/latest/ a copy of the newest version, and _headers (versions max-age=31536000, immutable, latest five minutes, CORS open). 99 items at 0.2.1: init (@nizivo/react and @nizivo/icons, and nizivo.css, which imports styles.css), the 23 patterns (the example module, which imports its own stylesheet and, in a pattern that draws charts, @nizivo/charts/styles.css, and the stylesheet) and 75 <name>-css re-skin items (the readable source sheet of every stable component; base and unstable/ stay on npm). Every file targets components/nizivo/; an item's dependencies are exactly the @nizivo packages its files import, at ~x.y.0; no registryDependencies (the CLI resolves bare names against shadcn's registry); every file carries the MIT notice, a stylesheet at its top and a module after its imports, because the shadcn CLI 4.22.1 drops every comment above a module's first statement. Released versions are frozen into git (node apps/docs/scripts/registry.ts --freeze at release, apps/docs/registry/v<version>/) and served as frozen. Not built (later): the data-table block (TanStack Table v9) and the -source fork items; the linter rejects a -source name
D-dkThe Base UI canary (§10.4, M65, R4, D1-proof)Adopted 2026-10-27 (v0.3 week 8; builder default; the maintainer can override). packages/react/src/_engines/base-ui/{dom.ts,dom-allowlist.json,overlay.client.tsx,dialog.client.tsx,select.client.tsx}: the DialogEngine and SelectEngine on Base UI 1.9.0 (exact, a devDependency of @nizivo/react like react-stately for the hooks Tooltip, not of testkit: the adapters typecheck with the package), with its own OverlayEngine (a context for the nested-portal chain, since Base UI has no container provider; markTopLayer a no-op, since Base UI exempts only aria-live regions). NZ_ADAPTER=base-ui swaps Dialog and Select together (test/adapters.ts), the overlay config then runs only the dialog, select, select-field and nesting specs, and nesting.spec skips its mixed-family cases (§4.1 L4). gen/adapter.ts treats the folder as unshipped (size and directive checks skip it); the lint and module-graph engine fences include @base-ui/. The harness reads each family's own functional data-* allowlist and accepts a scroll lock on <html> or <body>. Base UI reports why a popup opened or closed, so details.reason is real on this family. Lane overlay-base-ui-chromium, non-blocking: its failures are recorded, not forced green
D-dlThe CSP and Trusted Types fixture (§19.3, R13)Adopted 2026-10-27 (v0.3 week 9; builder default; the maintainer can override). fixtures/runner/csp.ts, lane fixture-csp, blocking: it builds fixtures/vite-ssr and serves its server HTML itself, stamping a fresh nonce per response on the header, every <script>, every modulepreload and <meta property="csp-nonce">; ?csp= picks a (Profile A), hash (the theme script by its SHA-256), nometa (the control: React Aria's press <style> must be blocked without the meta), b (Profile B) or tt (Profile A plus require-trusted-types-for 'script' report-only, Chromium). Blocking: no violation or console message under a and hash, the Dialog and Select open, the theme script runs, the control blocks; no violation and no server style= attribute on the server-safe pages under b. Recorded, not failed: Profile B's blocked attributes on engine pages (R13's list), WebKit's style-src-attr reports on every <select> (no attribute in the HTML), Trusted Types reports. Results in fixtures/vite-ssr/results/csp.json

Source: docs/ARCHITECTURE.md §23.4

Browser degradations

Found on the browser floor (Chrome/Edge 123, Firefox 129, Safari/iOS 17.5).

D1
Firefox 129–131: light-dark() with literal colours written directly on a non-inherited property can take the wrong scheme in nested scopes (Firefox bug 1914735, fixed in 132). Nizivo's tokens are unaffected; write theme colours as custom properties.
D2
Firefox (all versions tested, 129 to 157): an exit transition that ends in display: none is instant, because Firefox cannot animate display (bug 1834877). Entry animations work. Engine-managed overlays are unaffected; a CSS-only component that needs an exit animation hides with visibility instead.
D3
Firefox 129–157, WebKit 27.2 and Mobile Safari 17.5 have no overlay property, so a native popover or <dialog> exit is instant there. Nizivo's overlays are engine-managed and stay mounted through data-exiting, so they are unaffected; this is why no overlay swaps to the native element yet (§12.4 gate 1).
D4
Mobile Safari 17.5 (iOS 17.5 Simulator): an exit transition that ends in display: none is instant, as in Firefox (D2): transition-behavior: allow-discrete parses, but display does not transition. Entry animations work, and WebKit 27.2 fades. Engine-managed overlays are unaffected; a CSS-only component that needs an exit animation hides with visibility instead.

Source: docs/ARCHITECTURE.md

Mobile Safari 17.5 (iOS 17.5 Simulator) passes every check, on the §7.5 sample, on the built tokens.css and on the week 3 controls: 0 failures: 3,240 passing checks and 188 informational cells over both OS schemes, painted pixels included. User agent: Mozilla/5.0 (iPhone; CPU iPhone OS 17_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Mobile/15E148 Safari/604.1.

macOS Safari 17.5 is still not tested. The Simulator runs iOS Safari; Playwright WebKit 27.2 stays a comparison only. Routes for the macOS half are listed below.

Source: fixtures/floor-lane/RESULTS.md

On this page