@salemcobalt/ui 0.2.0-rc.15 → 0.2.0-rc.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -23,42 +23,42 @@ import {
23
23
 
24
24
  Styles ship as one stylesheet inside `@layer sc`, so an app's unlayered CSS always wins. Every value in it is a token custom property; theme with `data-theme="light|dark"` on `<html>` and density with `data-density="compact"` on any ancestor. Interactive behaviour (Dialog, Tooltip and Menu focus and keyboard handling, `render` composition) comes from Base UI; see `docs/adr/0001-headless-primitives.md`.
25
25
 
26
- | Component | Notes |
27
- | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
28
- | `Button` | `variant` primary · secondary · ghost · danger; `size` sm · md · lg; `iconStart`/`iconEnd`; `loading` (keeps width, `aria-busy`); `fullWidth`. `shortcut={<Kbd>N</Kbd>}` shows a key hint after the label, tinted to the variant and hidden from the name (add `aria-keyshortcuts="n"`); `iconOnly` collapses to the icon in a square button without remounting (label kept as the name, shortcut hidden). The hint is not shown where there is no fine pointer (`not all and (any-pointer: fine)`: phones, tablets without a trackpad). |
29
- | `IconButton` | Square Button for one icon. Required `label` (the accessible name); `variant` ghost (default) · secondary · primary · danger; `size`; `tooltip` (`true` shows `label`, or a string); `tooltipSide`; `round`; `loading`; `render={<a href />}` or a Next.js `Link` makes it a link. `dot` neutral · accent · success · warning · danger · info puts a status dot top-left with a ring in `--sc-icon-button-dot-ring` (default `surface-default`; set it to the header background); decorative, so say the status in `label`. |
30
- | `Icon`, `*Icon` | 49 outline icons on a 16px grid in `currentColor`, one component each (`PlayIcon`, `ChevronDownIcon` …, listed in `iconNames`); `size` xs 12 · sm 14 · md 16 · lg 20 · xl 24 · em; decorative unless `title` is given. `Icon` wraps your own paths. |
31
- | `Badge` | `tone` neutral · accent · success · warning · danger · info; `variant` soft · solid · outline; `size` sm · md; `dot`. Text only. |
32
- | `Card` | `variant` outlined · elevated · filled · plain (a quiet row: no surface, a hover wash without border or shadow, selected is `accent.wash` with a 1px `accent.border`); `padding` none · sm · md · lg; `interactive`; `selected` (visual only: pair with `aria-pressed`/`aria-selected`/`aria-current`; also turns a Score inside solid); `as` or `render` to make it an `a`/`button`/`li`. |
33
- | `Stack`, `Inline` | `gap` from the spacing scale (`none` … `4xl`); `align`; `justify`; `as`. `Inline` wraps unless `wrap={false}`. |
34
- | `Kbd` | One key per `Kbd`; `Kbd.Group label="Command K"` groups a shortcut with a `+` (or `separator="space"`). |
35
- | `Dialog` | `Root` (`open`, `defaultOpen`, `onOpenChange`, `dismissible`, `onRequestClose(reason)`: Esc, backdrop and every `Close` ask instead of closing; close by setting `open`), `Trigger`, `Content` (`size` sm · md · lg, every size clamps to the viewport minus a 16px gutter; `variant="sheet"` becomes a bottom sheet below 640px with a scrolling `Body`; `variant="fullscreen"` covers the viewport, inset 16px from 900px, with a scrolling `Body`), `Title`, `Description`, `Body`, `Actions`, `Close`. Menus, selects and tooltips opened inside render above it. |
36
- | `FullScreenFlow` | A multi-step takeover on `Dialog variant="fullscreen"` (docs/adr/0006). `Root` (`open` default `true`, required `onRequestClose(reason)` for Esc and the ×, `onNext`/`onBack` for Enter and the arrow keys outside controls, `decoration`), `Header` (`title` names the dialog, `logo`, `compactTitle` below 760px, `status`, `closeLabel`; children: `Progress`), `Progress` (`chapters` `{ label, steps }[]`, `step` counted from 1, `label`; current chapter `aria-current="step"`), `Body`, `Rail` (from 1000px, `footer`) › `RailGroup` (`label`) › `RailItem` (`current`, `done`, `disabled`, `adornment`), `Viewport` (`direction` forward · back; one keyed `Step`), `Step` (`title` an `<h2>` that takes focus, `eyebrow`, `description`, `announcement` for the live region), `Footer` (`back`, `hint`, children; 44px buttons). The app owns the step state; the flow owns motion, focus and announcements. |
37
- | `Toolbar` | `Root` (required `aria-label`, `wrap`, `gap`), `Group` (`aria-label`, `gap`), `Separator`, `Spacer`, `Button` (`render={<IconButton … />}` or `render={<Menu.Trigger render={…} />}` so the control joins the roving focus: Tab enters, arrows move, Tab leaves), `Link` (an anchor in the same navigation; `render={<IconButton label render={<a href />} />}` for an icon link). |
38
- | `Banner` | `tone` info (default) · success · warning · danger · neutral; `icon` (or `false`); `action` slot; `dismissible` + `onDismiss` (`open` to control); warning and danger are `role="alert"`, the rest `role="status"`. |
39
- | `Tabs` | `Root` (`value`, `defaultValue`, `onValueChange`), `List` (required `aria-label`, `fill`), `Tab` (`value`, `icon`, `count`, `disabled`), `Panel` (`value`, `keepMounted`). Arrow keys move, Enter/Space activates. `List variant="segmented"`: a quiet segmented control (sunken track, the active tab raised on `surface-default`, no accent, plain muted counts); `line` stays the default. |
40
- | `Input`, `Textarea` | Labelled native controls on Base UI's Field: required `label` (`hideLabel` keeps it for assistive tech), `description`, `error` (sets invalid and announces), `invalid`, `required`, `disabled`, `name`, `size` sm · md · lg (Input), `iconStart`/`iconEnd` (Input), `rows`/`resize`/`variant="code"` (Textarea; monospace, tab-size 2, no spellcheck, add native `wrap="off"` for sideways scroll), `onValueChange(string)` beside native `onChange`. Every native attribute passes through: `type="date" \| "time" \| "number"` keep the same height, `list` + a `<datalist>` gives suggestions. |
41
- | `Select` | Same field props; `options` as `{ value, label, disabled? }` or `{ label, options }` groups; `value`/`defaultValue`/`onValueChange` (`null` for none, shows `placeholder`; no sentinel option needed); `clearable` adds an × and Backspace/Delete on the trigger that call `onValueChange(null)`; `clearLabel`; `fullWidth`. Submits through `name` like a native select. |
42
- | `FileInput` | Same field props (`label`, `hideLabel`, `description`, `error`, `invalid`, `required`, `disabled`, `name`, `size`) on a drop zone built on FilePond (docs/adr/0004): `accept` (`".pdf,.docx"`, MIME types, `image/*`, as the native attribute), `acceptLabel`, `maxFileSize` (bytes or `"10MB"`), `multiple` + `maxFiles`, `files`/`defaultFiles`/`onFilesChange(File[])`, `idleLabel`, `browseLabel`. No upload server: plain `File`s go to the callback, and with `name` to the form's `FormData` (Server Actions included). Rejected files never join the list; the reason shows in the error slot and is announced. Without `multiple`, a new file replaces the current one only once it passes. FilePond loads on mount, in its own chunk. `preview` (default `true`): the name of a PDF, plain text, Markdown or image file is a button that opens a `FilePreviewDialog` before upload; DOCX and other types keep a plain name. |
43
- | `FilePreviewDialog` | Shows a local `File` before upload in a `lg` Dialog (`file`, `open`, `onOpenChange`, `finalFocus`): a PDF in the browser's viewer (an object URL re-typed `application/pdf`; at 48rem and below a link opens it in a new tab), plain text and Markdown as text, images in an `<img>`; anything else gets a note and is never framed. The object URL lives only while it is open. |
44
- | `Checkbox`, `Switch` | Required `label`, `description`, `checked`/`defaultChecked`/`onCheckedChange(boolean)`, `name`/`value`, `disabled`, `required`; Checkbox adds `indeterminate`; Switch adds `labelPosition` start · end. |
45
- | `Tooltip` | `content`; `children` is the trigger (an `IconButton`, `Button` or focusable `Badge`); `side` top · bottom · left · right; `align`; `delay` (300ms); `open`/`onOpenChange`; `disabled`. `Tooltip.Provider` around a row skips the delay between siblings. |
46
- | `Chip` | A pill: `tone` neutral · accent · success · warning · danger · info; `size` sm · md; `icon`; `onClick` + `selected` make it a toggle button (`aria-pressed`); `onRemove` adds a labelled × (Backspace/Delete on the chip removes too); `removeLabel`; `disabled`. |
47
- | `Disclosure` | A native `<details>` collapse: required `summary`, `summaryOpen`, `open`/`defaultOpen`/`onOpenChange`, `previewLines` (fades the first N lines while closed, summary moves below), `summaryPosition`; share `name` for an exclusive group. |
48
- | `EmptyState` | Centred placeholder: required `title`, `description`, `icon` (24px in a muted circle), children are the actions, `size` sm · md, `as` div · section · li. |
49
- | `Skeleton` | Loading placeholder, `aria-hidden`: `variant` text (default, one line) · rect · circle, `width`, `height`, `lines` (text, last line shorter), `still`. Put `aria-busy` on the region or a labelled `Spinner` beside it. |
50
- | `Menu` | `Root` (`open`, `onOpenChange`, `modal`), `Trigger` (secondary Button with a chevron, or `render={<IconButton label="More" />}`), `Content` (`side`, `align`), `Item` (`icon`, `trailing`, `tone="danger"`, `disabled`), `LinkItem` (`href`), `CheckboxItem` (`checked`, `onCheckedChange`), `RadioGroup` (`value`, `onValueChange`, `label`) + `RadioItem`, `Group` (`label`), `Separator`. Arrow keys, Home/End, type-ahead, Esc and focus return come from Base UI. |
51
- | `Table` selection | `useRowSelection(ids)` (ids in visible order; ids that disappear leave the selection): `selected`, `selectedIds`, `count`, `anchor`, `headerState` none · some · all, `isSelected`, `toggle`, `toggleRange`, `selectAll`, `clear`; a pure reducer underneath (`selection.*` events). `Table.Root selection` + `Table.SelectionHeader` (`label`, mixed when some) + `Table.SelectionCell label="Select …"` in a `Table.Row selectionId`. Shift+click (checkbox or row) and Shift+Space extend from the anchor; Cmd/Ctrl+click on a row toggles without reaching its `onClick`; Cmd/Ctrl+A in the table selects all, Esc clears. |
52
- | `SelectionBar` | Floating bulk-action bar on Toolbar, centred at the bottom of the viewport (content width up to 48rem, gutters and the safe area respected, full width between the gutters under 640px, fade and slide-up, none under reduced motion): `count` (hidden at 0), `onClear`, children are `Toolbar.Button`s, `aria-label` ("Bulk actions"), `countLabel`, `clearLabel`. Mount it at the end of the scrolling content: while open it leaves a spacer as tall as itself so the last rows scroll clear. A polite status region announces the count; focus returns to where it came from when the bar leaves. |
53
- | `Toast` | `Toast.Provider` (`timeout` 6000, `limit` 3) near the root, `useToast().add({ title, description, tone, action: { label, onClick }, timeout, priority })` below it; Base UI Toast: polite region, paused on hover and focus, F6 to reach it, bottom-end at `--sc-z-toast`. |
54
- | `PriorityNav` | Priority+ primary navigation (docs/adr/0005): required `aria-label`; `items` as `{ href, label, id?, current? }` in priority order; links that fit stay inline and the rest go into a "More" disclosure (APG disclosure navigation: a `button` with `aria-expanded`, not a menu; Esc, click outside and focus leaving close it). The `current` link is never collapsed. `moreLabel`, `moreCountLabel(n)` (announced only: "More, 3 links"), `renderLink={(props) => <Link {...props} />}` for a router. Server-renders every link on one clipped line (scrolls sideways without JS), measures widths once (again after web fonts load or when keys/string labels change) and re-renders only when the inline count changes. Takes the free space in a flex row (`flex: 1 1 0%`); in a grid use a `minmax(0, 1fr)` track. The open panel sits in the top layer (Popover API), anchored to the toggle with CSS anchor positioning (a JS fallback elsewhere) and capped to the viewport. `data-overflowing` + `onOverflowChange(boolean)` let a header make room, with `overflowHysteresis` (px) so it cannot oscillate. Under 400px wide (container query) More shows only an ellipsis. |
55
- | `Popover` | Non-modal panel on Base UI Popover: `Root` (`open`, `defaultOpen`, `onOpenChange`, `modal` default false), `Trigger` (secondary Button, or `render={<IconButton label dot />}`), `Content` (`side`, `align`, `sideOffset`, `arrow`; Menu's surface, `--sc-z-dropdown`), `Title` (names the dialog), `Description`, `Close` (the × with no children, or wraps a Button). Esc and focus return come from Base UI. |
56
- | `BroomIcon` | Animated 24px-grid icon for "a sweep is running": `sweeping`, `size` (Icon sizes, default `lg`, or `display` 72px with a 1.75px stroke), `title`. Inline SVG; strokes stay 1.25px at any size; holds still with the dust shown under `prefers-reduced-motion`. Inside an IconButton it takes the next icon size up. |
57
- | `Score` | A number on a tinted tile or in a ring: required `value`, `max` (100), `tone` neutral · accent (default) · success · warning · danger (no thresholds inside: map value to tone in the app), `variant` tile · ring (64px, decorative SVG, arc animates only on change, none under reduced motion), `size` sm 40 · md 48 (tile), `caption`, `label` ("Fit score"), `valueText`, `selected`. Assistive tech reads one visually hidden sentence, "Fit score 94 out of 100"; the visible number is `aria-hidden`, no `aria-label`. A tile turns solid when `selected` or inside `.sc-card[data-selected]`, `[aria-pressed="true"]` or `[aria-selected="true"]`. |
58
- | `ReasonList` | `ReasonList` (`ul`) + `ReasonList.Item` (`li`): required `tone` for (filled square) · against (bar) · gap (hollow circle), so the marker shape carries the meaning as well as the colour; `size` sm · md; `divider` (dashed rule above). Name it with a heading via `aria-labelledby`; `<strong>` inside picks out key words. |
59
- | `WordDiff` | Prose word diff: required `parts` (`{ kind: 'same' \| 'added' \| 'removed', text }[]`), `as` p · div · span, `size` sm · md, `addedLabel`/`removedLabel` ("added"/"removed"). Real `<del>`/`<ins>` with visually hidden "[removed: …]"/"[added: …]"; strike-through and underline carry the change (3:1 on the `diff.*` tints); whitespace kept exactly (`pre-wrap`), edge whitespace left outside the mark. |
60
- | `Quote` | Someone's words echoed back with who said them: required `label` ("You") and `children`. A `figure` with the label as `figcaption` and the words in a `blockquote`, on `accent.wash` with an `accent.border`. |
61
- | `Heading`, `Text` | `Heading size="overline"`: 12px uppercase mono with `tracking-wider`, muted unless `tone` is set; keep a real `level`. `Text family="mono"` for data next to prose (row meta, dates). |
26
+ | Component | Notes |
27
+ | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
28
+ | `Button` | `variant` primary · secondary · ghost · danger; `size` sm · md · lg; `iconStart`/`iconEnd`; `loading` (keeps width, `aria-busy`); `fullWidth`. `shortcut={<Kbd>N</Kbd>}` shows a key hint after the label, tinted to the variant and hidden from the name (add `aria-keyshortcuts="n"`); `iconOnly` collapses to the icon in a square button without remounting (label kept as the name, shortcut hidden). The hint is not shown where there is no fine pointer (`not all and (any-pointer: fine)`: phones, tablets without a trackpad). |
29
+ | `IconButton` | Square Button for one icon. Required `label` (the accessible name); `variant` ghost (default) · secondary · primary · danger; `size`; `tooltip` (`true` shows `label`, or a string); `tooltipSide`; `round`; `loading`; `render={<a href />}` or a Next.js `Link` makes it a link. `dot` neutral · accent · success · warning · danger · info puts a status dot top-left with a ring in `--sc-icon-button-dot-ring` (default `surface-default`; set it to the header background); decorative, so say the status in `label`. |
30
+ | `Icon`, `*Icon` | 49 outline icons on a 16px grid in `currentColor`, one component each (`PlayIcon`, `ChevronDownIcon` …, listed in `iconNames`); `size` xs 12 · sm 14 · md 16 · lg 20 · xl 24 · em; decorative unless `title` is given. `Icon` wraps your own paths. |
31
+ | `Badge` | `tone` neutral · accent · success · warning · danger · info; `variant` soft · solid · outline; `size` sm · md; `dot`. Text only. |
32
+ | `Card` | `variant` outlined · elevated · filled · plain (a quiet row: no surface, a hover wash without border or shadow, selected is `accent.wash` with a 1px `accent.border`); `padding` none · sm · md · lg; `interactive`; `selected` (visual only: pair with `aria-pressed`/`aria-selected`/`aria-current`; also turns a Score inside solid); `as` or `render` to make it an `a`/`button`/`li`. |
33
+ | `Stack`, `Inline` | `gap` from the spacing scale (`none` … `4xl`); `align`; `justify`; `as`. `Inline` wraps unless `wrap={false}`. |
34
+ | `Kbd` | One key per `Kbd`; `Kbd.Group label="Command K"` groups a shortcut with a `+` (or `separator="space"`). |
35
+ | `Dialog` | `Root` (`open`, `defaultOpen`, `onOpenChange`, `dismissible`, `onRequestClose(reason)`: Esc, backdrop and every `Close` ask instead of closing; close by setting `open`), `Trigger`, `Content` (`size` sm · md · lg, every size clamps to the viewport minus a 16px gutter; `variant="sheet"` becomes a bottom sheet below 640px with a scrolling `Body`; `variant="fullscreen"` covers the viewport, inset 16px from 900px, with a scrolling `Body`), `Title`, `Description`, `Body`, `Actions`, `Close`. Menus, selects and tooltips opened inside render above it. |
36
+ | `FullScreenFlow` | A multi-step takeover on `Dialog variant="fullscreen"` (docs/adr/0006). `Root` (`open` default `true`, required `onRequestClose(reason)` for Esc and the ×, `onNext`/`onBack` for Enter and the arrow keys outside controls, `decoration`), `Header` (`title` names the dialog, `logo`, `compactTitle` below 760px, `status`, `closeLabel`; children: `Progress`), `Progress` (`chapters` `{ label, steps }[]`, `step` counted from 1, `label`; current chapter `aria-current="step"`), `Body`, `Rail` (from 1000px, `footer`) › `RailGroup` (`label`) › `RailItem` (`current`, `done`, `disabled`, `adornment`), `Viewport` (`direction` forward · back; one keyed `Step`), `Step` (`title` an `<h2>` that takes focus, `eyebrow`, `description`, `announcement` for the live region), `Footer` (`back`, `hint`, children; 44px buttons). The app owns the step state; the flow owns motion, focus and announcements. |
37
+ | `Toolbar` | `Root` (required `aria-label`, `wrap`, `gap`), `Group` (`aria-label`, `gap`), `Separator`, `Spacer`, `Button` (`render={<IconButton … />}` or `render={<Menu.Trigger render={…} />}` so the control joins the roving focus: Tab enters, arrows move, Tab leaves), `Link` (an anchor in the same navigation; `render={<IconButton label render={<a href />} />}` for an icon link). |
38
+ | `Banner` | `tone` info (default) · success · warning · danger · neutral; `icon` (or `false`); `action` slot; `dismissible` + `onDismiss` (`open` to control); warning and danger are `role="alert"`, the rest `role="status"`. |
39
+ | `Tabs` | `Root` (`value`, `defaultValue`, `onValueChange`), `List` (required `aria-label`, `fill`), `Tab` (`value`, `icon`, `count`, `disabled`), `Panel` (`value`, `keepMounted`). Arrow keys move, Enter/Space activates. `List variant="segmented"`: a quiet segmented control (sunken track, the active tab raised on `surface-default`, no accent, plain muted counts); `line` stays the default. |
40
+ | `Input`, `Textarea` | Labelled native controls on Base UI's Field: required `label` (`hideLabel` keeps it for assistive tech), `description`, `error` (sets invalid and announces), `invalid`, `required`, `disabled`, `name`, `size` sm · md · lg (Input), `iconStart`/`iconEnd` (Input), `rows`/`resize`/`variant="code"` (Textarea; monospace, tab-size 2, no spellcheck, add native `wrap="off"` for sideways scroll), `onValueChange(string)` beside native `onChange`. Every native attribute passes through: `type="date" \| "time" \| "number"` keep the same height, `list` + a `<datalist>` gives suggestions. |
41
+ | `Select` | Same field props; `options` as `{ value, label, disabled? }` or `{ label, options }` groups; `value`/`defaultValue`/`onValueChange` (`null` for none, shows `placeholder`; no sentinel option needed); `clearable` adds an × and Backspace/Delete on the trigger that call `onValueChange(null)`; `clearLabel`; `fullWidth`. Submits through `name` like a native select. |
42
+ | `FileInput` | Same field props (`label`, `hideLabel`, `description`, `error`, `invalid`, `required`, `disabled`, `name`, `size`) on a drop zone built on FilePond (docs/adr/0004): `accept` (`".pdf,.docx"`, MIME types, `image/*`, as the native attribute), `acceptLabel`, `maxFileSize` (bytes or `"10MB"`), `multiple` + `maxFiles`, `files`/`defaultFiles`/`onFilesChange(File[])`, `idleLabel`, `browseLabel`. No upload server: plain `File`s go to the callback, and with `name` to the form's `FormData` (Server Actions included). Rejected files never join the list; the reason shows in the error slot and is announced. Without `multiple`, a new file replaces the current one only once it passes. FilePond loads on mount, in its own chunk. `preview` (default `true`): the name of a PDF, plain text, Markdown or image file is a button that opens a `FilePreviewDialog` before upload; DOCX and other types keep a plain name. |
43
+ | `FilePreviewDialog` | Shows a local `File` before upload in a `lg` Dialog (`file`, `open`, `onOpenChange`, `finalFocus`): a PDF in the browser's viewer (an object URL re-typed `application/pdf`; at 48rem and below a link opens it in a new tab), plain text and Markdown as text, images in an `<img>`; anything else gets a note and is never framed. The object URL lives only while it is open. |
44
+ | `Checkbox`, `Switch` | Required `label`, `description`, `checked`/`defaultChecked`/`onCheckedChange(boolean)`, `name`/`value`, `disabled`, `required`; Checkbox adds `indeterminate`; Switch adds `labelPosition` start · end. |
45
+ | `Tooltip` | `content`; `children` is the trigger (an `IconButton`, `Button` or focusable `Badge`); `side` top · bottom · left · right; `align`; `delay` (300ms); `open`/`onOpenChange`; `disabled`. `Tooltip.Provider` around a row skips the delay between siblings. |
46
+ | `Chip` | A pill: `tone` neutral · accent · success · warning · danger · info; `size` sm · md; `icon`; `onClick` + `selected` make it a toggle button (`aria-pressed`); `onRemove` adds a labelled × (Backspace/Delete on the chip removes too); `removeLabel`; `disabled`. |
47
+ | `Disclosure` | A native `<details>` collapse: required `summary`, `summaryOpen`, `open`/`defaultOpen`/`onOpenChange`, `previewLines` (fades the first N lines while closed, summary moves below), `summaryPosition`; share `name` for an exclusive group. |
48
+ | `EmptyState` | Centred placeholder: required `title`, `description`, `icon` (24px in a muted circle), children are the actions, `size` sm · md, `as` div · section · li. |
49
+ | `Skeleton` | Loading placeholder, `aria-hidden`: `variant` text (default, one line) · rect · circle, `width`, `height`, `lines` (text, last line shorter), `still`. Put `aria-busy` on the region or a labelled `Spinner` beside it. |
50
+ | `Menu` | `Root` (`open`, `onOpenChange`, `modal`), `Trigger` (secondary Button with a chevron, or `render={<IconButton label="More" />}`), `Content` (`side`, `align`), `Item` (`icon`, `trailing`, `tone="danger"`, `disabled`), `LinkItem` (`href`), `CheckboxItem` (`checked`, `onCheckedChange`), `RadioGroup` (`value`, `onValueChange`, `label`) + `RadioItem`, `Group` (`label`), `Separator`. Arrow keys, Home/End, type-ahead, Esc and focus return come from Base UI. |
51
+ | `Table` selection | `useRowSelection(ids)` (ids in visible order; ids that disappear leave the selection): `selected`, `selectedIds`, `count`, `anchor`, `headerState` none · some · all, `isSelected`, `toggle`, `toggleRange`, `selectAll`, `clear`; a pure reducer underneath (`selection.*` events). `Table.Root selection` + `Table.SelectionHeader` (`label`, mixed when some) + `Table.SelectionCell label="Select …"` in a `Table.Row selectionId`. Shift+click (checkbox or row) and Shift+Space extend from the anchor; Cmd/Ctrl+click on a row toggles without reaching its `onClick`; Cmd/Ctrl+A in the table selects all, Esc clears. |
52
+ | `SelectionBar` | Floating bulk-action bar on Toolbar, centred at the bottom of the viewport (content width up to 48rem, gutters and the safe area respected, full width between the gutters under 640px, fade and slide-up, none under reduced motion): `count` (hidden at 0), `onClear`, children are `Toolbar.Button`s, `aria-label` ("Bulk actions"), `countLabel`, `clearLabel`. Mount it at the end of the scrolling content: while open it leaves a spacer as tall as itself so the last rows scroll clear. A polite status region announces the count; focus returns to where it came from when the bar leaves. |
53
+ | `Toast` | `Toast.Provider` (`timeout` 6000, `limit` 3) near the root, `useToast().add({ title, description, tone, action: { label, onClick }, timeout, priority })` below it; Base UI Toast: polite region, paused on hover and focus, F6 to reach it, bottom-end at `--sc-z-toast`. |
54
+ | `PriorityNav` | Priority+ primary navigation (docs/adr/0005): required `aria-label`; `items` as `{ href, label, id?, current?, badge?, badgeLabel? }` in priority order; links that fit stay inline and the rest go into a "More" disclosure (APG disclosure navigation: a `button` with `aria-expanded`, not a menu; Esc, click outside and focus leaving close it). The `current` link is never collapsed. `moreLabel`, `moreCountLabel(n)` (announced only: "More, 3 links"), `renderLink={(props) => <Link {...props} />}` for a router. Server-renders every link on one clipped line (scrolls sideways without JS), measures widths once (again after web fonts load or when keys/string labels change) and re-renders only when the inline count changes. Takes the free space in a flex row (`flex: 1 1 0%`); in a grid use a `minmax(0, 1fr)` track. The open panel sits in the top layer (Popover API), anchored to the toggle with CSS anchor positioning (a JS fallback elsewhere) and capped to the viewport. `data-overflowing` + `onOverflowChange(boolean)` let a header make room, with `overflowHysteresis` (px) so it cannot oscillate. Under 400px wide (container query) More shows only an ellipsis. `badge` (a count, or `'dot'`; `0` shows nothing) draws an accent pill or dot after the label, decorative, with `badgeLabel` ("2 suggestions to review") appended to the link's name; while the item is collapsed, More shows the collapsed items' summed count (a dot when they only have dots) and adds their `badgeLabel`s to its name ("More, 3 links, 2 suggestions to review"). Icon-only, the badge sits on More's corner. No motion. |
55
+ | `Popover` | Non-modal panel on Base UI Popover: `Root` (`open`, `defaultOpen`, `onOpenChange`, `modal` default false), `Trigger` (secondary Button, or `render={<IconButton label dot />}`), `Content` (`side`, `align`, `sideOffset`, `arrow`; Menu's surface, `--sc-z-dropdown`), `Title` (names the dialog), `Description`, `Close` (the × with no children, or wraps a Button). Esc and focus return come from Base UI. |
56
+ | `BroomIcon` | Animated 24px-grid icon for "a sweep is running": `sweeping`, `size` (Icon sizes, default `lg`, or `display` 72px with a 1.75px stroke), `title`. Inline SVG; strokes stay 1.25px at any size; holds still with the dust shown under `prefers-reduced-motion`. Inside an IconButton it takes the next icon size up. |
57
+ | `Score` | A number on a tinted tile or in a ring: required `value` (a number, or `null` for not scored yet: a muted dash on the neutral tile or in an empty ring, no tone, no arc, read as "Fit score: not scored"), `max` (100), `tone` neutral · accent (default) · success · warning · danger (no thresholds inside: map value to tone in the app), `variant` tile · ring (64px, decorative SVG, arc animates only on change, none under reduced motion), `size` sm 40 · md 48 (tile), `caption`, `label` ("Fit score"), `valueText`, `selected`. Assistive tech reads one visually hidden sentence, "Fit score 94 out of 100"; the visible number is `aria-hidden`, no `aria-label`. A tile turns solid when `selected` or inside `.sc-card[data-selected]`, `[aria-pressed="true"]` or `[aria-selected="true"]`. |
58
+ | `ReasonList` | `ReasonList` (`ul`) + `ReasonList.Item` (`li`): required `tone` for (filled square) · against (bar) · gap (hollow circle), so the marker shape carries the meaning as well as the colour; `size` sm · md; `divider` (dashed rule above). Name it with a heading via `aria-labelledby`; `<strong>` inside picks out key words. |
59
+ | `WordDiff` | Prose word diff: required `parts` (`{ kind: 'same' \| 'added' \| 'removed', text }[]`), `as` p · div · span, `size` sm · md, `addedLabel`/`removedLabel` ("added"/"removed"). Real `<del>`/`<ins>` with visually hidden "[removed: …]"/"[added: …]"; strike-through and underline carry the change (3:1 on the `diff.*` tints); whitespace kept exactly (`pre-wrap`), edge whitespace left outside the mark. |
60
+ | `Quote` | Someone's words echoed back with who said them: required `label` ("You") and `children`. A `figure` with the label as `figcaption` and the words in a `blockquote`, on `accent.wash` with an `accent.border`. |
61
+ | `Heading`, `Text` | `Heading size="overline"`: 12px uppercase mono with `tracking-wider`, muted unless `tone` is set; keep a real `level`. `Text family="mono"` for data next to prose (row meta, dates). |
62
62
 
63
63
  ```tsx
64
64
  <Dialog.Root>
package/dist/index.d.ts CHANGED
@@ -2645,6 +2645,14 @@ interface PriorityNavItem {
2645
2645
  id?: string | undefined;
2646
2646
  /** The page being shown: `aria-current="page"`, and the link never collapses into "More". */
2647
2647
  current?: boolean | undefined;
2648
+ /**
2649
+ * A small count (`2`) or `'dot'` after the label: something waiting there. `0` shows nothing. Visual only; say
2650
+ * what it means in `badgeLabel`. While the item is collapsed, "More" shows the summed count (or a dot) and adds
2651
+ * the label to its accessible name.
2652
+ */
2653
+ badge?: number | 'dot' | undefined;
2654
+ /** What the badge means, appended to the link's (and, while collapsed, More's) accessible name: "2 suggestions to review". A count without it is read as the bare number; a dot without it is not announced. */
2655
+ badgeLabel?: string | undefined;
2648
2656
  }
2649
2657
  /** What `renderLink` receives: spread it onto your link component. */
2650
2658
  interface PriorityNavLinkProps {
@@ -2908,8 +2916,11 @@ type ScoreTone = 'neutral' | 'accent' | 'success' | 'warning' | 'danger';
2908
2916
  type ScoreSize = 'sm' | 'md';
2909
2917
  type ScoreVariant = 'tile' | 'ring';
2910
2918
  interface ScoreProps extends Omit<ComponentPropsWithRef<'span'>, 'children'> {
2911
- /** The number shown, e.g. a fit score. Rendered as given; round it first. */
2912
- value: number;
2919
+ /**
2920
+ * The number shown, e.g. a fit score. Rendered as given; round it first. `null` means not scored yet: a neutral
2921
+ * tile (or an empty ring) with a dash, read as "{label}: not scored", and `tone` is ignored.
2922
+ */
2923
+ value: number | null;
2913
2924
  /** The top of the scale. Default 100. Fills the ring and ends the default accessible text. */
2914
2925
  max?: number | undefined;
2915
2926
  /** Colour. The system holds no thresholds: map the value to a tone in the app. Default `accent`. */
@@ -2922,7 +2933,7 @@ interface ScoreProps extends Omit<ComponentPropsWithRef<'span'>, 'children'> {
2922
2933
  caption?: ReactNode | undefined;
2923
2934
  /** What the number measures, read before it: "Fit score". Default "Score". */
2924
2935
  label?: string | undefined;
2925
- /** The whole text assistive tech reads instead of the default "{label} {value} out of {max}". */
2936
+ /** The whole text assistive tech reads instead of the default "{label} {value} out of {max}" ("{label}: not scored" without a value). */
2926
2937
  valueText?: string | undefined;
2927
2938
  /** Shows the selected look (solid fill). Also on automatically inside a selected Card or an element with `aria-pressed="true"` or `aria-selected="true"`. */
2928
2939
  selected?: boolean | undefined;
@@ -2930,7 +2941,7 @@ interface ScoreProps extends Omit<ComponentPropsWithRef<'span'>, 'children'> {
2930
2941
  /**
2931
2942
  * A score as a number on a tinted tile or inside a ring. The number and caption are visual; assistive tech reads one
2932
2943
  * visually hidden sentence ("Fit score 94 out of 100"), so the score is named without `aria-label` on a non-interactive
2933
- * element and reads whole inside a row button.
2944
+ * element and reads whole inside a row button. A `null` value is "not scored": a dash, no tone, no arc.
2934
2945
  */
2935
2946
  declare function Score({ value, max, tone, variant, size, caption, label, valueText, selected, className, style, ...rest }: ScoreProps): react.JSX.Element;
2936
2947
 
package/dist/index.js CHANGED
@@ -2807,16 +2807,25 @@ function placePanel({ anchor, panel, viewport, gutter, offset, rtl = false }) {
2807
2807
  }
2808
2808
 
2809
2809
  // src/components/PriorityNav/PriorityNav.tsx
2810
- import { jsx as jsx31, jsxs as jsxs26 } from "react/jsx-runtime";
2810
+ import { Fragment as Fragment9, jsx as jsx31, jsxs as jsxs26 } from "react/jsx-runtime";
2811
2811
  var defaultCountLabel2 = (n) => n === 1 ? "1 link" : `${n} links`;
2812
2812
  var defaultLink = ({ children, ...props }) => /* @__PURE__ */ jsx31("a", { ...props, children });
2813
2813
  var keyOf = (item) => item.id ?? item.href;
2814
+ var badgeOf = (item) => item.badge === "dot" ? "dot" : typeof item.badge === "number" && item.badge > 0 && Number.isFinite(item.badge) ? item.badge : null;
2815
+ var badgeTextOf = (item) => {
2816
+ const badge = badgeOf(item);
2817
+ if (badge === null) return null;
2818
+ return item.badgeLabel ?? (badge === "dot" ? null : String(badge));
2819
+ };
2820
+ function Badge2({ value }) {
2821
+ return /* @__PURE__ */ jsx31("span", { className: "sc-priority-nav__badge", "data-kind": value === "dot" ? "dot" : "count", "aria-hidden": "true", children: value === "dot" ? null : value });
2822
+ }
2814
2823
  var supportsAnchor = () => typeof CSS !== "undefined" && typeof CSS.supports === "function" && CSS.supports("anchor-name: --a");
2815
2824
  var supportsPopover = (el) => typeof el.showPopover === "function";
2816
2825
  function signatureOf(items) {
2817
2826
  return items.map((item) => {
2818
2827
  const text = typeof item.label === "string" || typeof item.label === "number" ? item.label : "";
2819
- return `${keyOf(item)}${text}`;
2828
+ return `${keyOf(item)}${text}${badgeOf(item) ?? ""}`;
2820
2829
  }).join("");
2821
2830
  }
2822
2831
  function PriorityNav({
@@ -2919,6 +2928,10 @@ function PriorityNav({
2919
2928
  const flags = measuring ? null : inlineFlags(items.length, current, fit.count);
2920
2929
  const overflow = flags ? items.filter((_, i) => !flags[i]) : [];
2921
2930
  const expanded = open && overflow.length > 0;
2931
+ const overflowBadges = overflow.map(badgeOf).filter((b) => b !== null);
2932
+ const overflowCount = overflowBadges.reduce((sum, b) => sum + (b === "dot" ? 0 : b), 0);
2933
+ const moreBadge = overflowCount > 0 ? overflowCount : overflowBadges.length > 0 ? "dot" : null;
2934
+ const moreBadgeText = overflow.map(badgeTextOf).filter((text) => text !== null).map((text) => `, ${text}`).join("");
2922
2935
  useLayoutEffect2(() => {
2923
2936
  const panel = panelRef.current;
2924
2937
  if (!panel || !supportsPopover(panel)) return;
@@ -2978,16 +2991,27 @@ function PriorityNav({
2978
2991
  const onBlur = (event) => {
2979
2992
  if (!event.currentTarget.contains(event.relatedTarget)) setOpen(false);
2980
2993
  };
2981
- const link = (item, inPanel) => renderLink(
2982
- {
2983
- href: item.href,
2984
- className: "sc-priority-nav__link",
2985
- "aria-current": item.current ? "page" : void 0,
2986
- onClick: inPanel ? () => setOpen(false) : void 0,
2987
- children: /* @__PURE__ */ jsx31("span", { className: "sc-priority-nav__label", children: item.label })
2988
- },
2989
- item
2990
- );
2994
+ const link = (item, inPanel) => {
2995
+ const badge = badgeOf(item);
2996
+ const badgeText = badgeTextOf(item);
2997
+ return renderLink(
2998
+ {
2999
+ href: item.href,
3000
+ className: "sc-priority-nav__link",
3001
+ "aria-current": item.current ? "page" : void 0,
3002
+ onClick: inPanel ? () => setOpen(false) : void 0,
3003
+ children: /* @__PURE__ */ jsxs26(Fragment9, { children: [
3004
+ /* @__PURE__ */ jsx31("span", { className: "sc-priority-nav__label", children: item.label }),
3005
+ badge !== null ? /* @__PURE__ */ jsx31(Badge2, { value: badge }) : null,
3006
+ badgeText !== null ? /* @__PURE__ */ jsxs26("span", { className: "sc-visually-hidden", children: [
3007
+ ", ",
3008
+ badgeText
3009
+ ] }) : null
3010
+ ] })
3011
+ },
3012
+ item
3013
+ );
3014
+ };
2991
3015
  return /* @__PURE__ */ jsx31(
2992
3016
  "nav",
2993
3017
  {
@@ -3022,8 +3046,10 @@ function PriorityNav({
3022
3046
  /* @__PURE__ */ jsx31("span", { className: "sc-priority-nav__toggle-label", children: moreLabel }),
3023
3047
  /* @__PURE__ */ jsxs26("span", { className: "sc-visually-hidden", children: [
3024
3048
  ", ",
3025
- moreCountLabel(overflow.length)
3049
+ moreCountLabel(overflow.length),
3050
+ moreBadgeText
3026
3051
  ] }),
3052
+ moreBadge !== null ? /* @__PURE__ */ jsx31(Badge2, { value: moreBadge }) : null,
3027
3053
  /* @__PURE__ */ jsx31(ChevronDownIcon, { size: "sm", className: "sc-priority-nav__chevron" })
3028
3054
  ]
3029
3055
  }
@@ -3486,25 +3512,27 @@ function Score({
3486
3512
  style,
3487
3513
  ...rest
3488
3514
  }) {
3489
- const fraction = max > 0 ? Math.min(1, Math.max(0, value / max)) : 0;
3515
+ const scored = value !== null;
3516
+ const fraction = scored && max > 0 ? Math.min(1, Math.max(0, value / max)) : 0;
3490
3517
  return /* @__PURE__ */ jsxs30(
3491
3518
  "span",
3492
3519
  {
3493
3520
  className: cx("sc-score", className),
3494
3521
  "data-variant": variant,
3495
3522
  "data-size": variant === "tile" ? size : void 0,
3496
- "data-tone": tone,
3523
+ "data-tone": scored ? tone : void 0,
3524
+ "data-unscored": scored ? void 0 : true,
3497
3525
  "data-selected": selected || void 0,
3498
3526
  style: variant === "ring" ? { ...style, "--sc-score-fill": (fraction * 100).toFixed(2) } : style,
3499
3527
  ...rest,
3500
3528
  children: [
3501
- /* @__PURE__ */ jsx35("span", { className: "sc-visually-hidden", children: valueText ?? `${label} ${value} out of ${max}` }),
3529
+ /* @__PURE__ */ jsx35("span", { className: "sc-visually-hidden", children: valueText ?? (scored ? `${label} ${value} out of ${max}` : `${label}: not scored`) }),
3502
3530
  variant === "ring" ? /* @__PURE__ */ jsxs30("svg", { className: "sc-score__ring", viewBox: "0 0 64 64", "aria-hidden": "true", focusable: "false", children: [
3503
3531
  /* @__PURE__ */ jsx35("circle", { className: "sc-score__track", cx: "32", cy: "32", r: "29", pathLength: 100 }),
3504
3532
  fraction > 0 ? /* @__PURE__ */ jsx35("circle", { className: "sc-score__arc", cx: "32", cy: "32", r: "29", pathLength: 100 }) : null
3505
3533
  ] }) : null,
3506
3534
  /* @__PURE__ */ jsxs30("span", { className: "sc-score__body", "aria-hidden": "true", children: [
3507
- /* @__PURE__ */ jsx35("span", { className: "sc-score__value", children: value }),
3535
+ /* @__PURE__ */ jsx35("span", { className: "sc-score__value", children: scored ? value : "\u2013" }),
3508
3536
  caption != null ? /* @__PURE__ */ jsx35("span", { className: "sc-score__caption", children: caption }) : null
3509
3537
  ] })
3510
3538
  ]
@@ -3532,14 +3560,14 @@ function ReasonListItem({ className, ...rest }) {
3532
3560
  var ReasonList = Object.assign(ReasonListRoot, { Item: ReasonListItem });
3533
3561
 
3534
3562
  // src/components/WordDiff/WordDiff.tsx
3535
- import { createElement as createElement7, Fragment as Fragment9 } from "react";
3536
- import { Fragment as Fragment10, jsx as jsx37, jsxs as jsxs31 } from "react/jsx-runtime";
3563
+ import { createElement as createElement7, Fragment as Fragment10 } from "react";
3564
+ import { Fragment as Fragment11, jsx as jsx37, jsxs as jsxs31 } from "react/jsx-runtime";
3537
3565
  var edges = /^(\s*)([\s\S]*?)(\s*)$/;
3538
3566
  function Change({ kind, text, label }) {
3539
3567
  const [, lead = "", core = "", trail = ""] = edges.exec(text) ?? [];
3540
- if (!core) return /* @__PURE__ */ jsx37(Fragment10, { children: text });
3568
+ if (!core) return /* @__PURE__ */ jsx37(Fragment11, { children: text });
3541
3569
  const Tag = kind === "added" ? "ins" : "del";
3542
- return /* @__PURE__ */ jsxs31(Fragment10, { children: [
3570
+ return /* @__PURE__ */ jsxs31(Fragment11, { children: [
3543
3571
  lead,
3544
3572
  /* @__PURE__ */ jsxs31(Tag, { className: `sc-word-diff__${kind}`, children: [
3545
3573
  /* @__PURE__ */ jsxs31("span", { className: "sc-visually-hidden", children: [
@@ -3563,7 +3591,7 @@ function WordDiff({
3563
3591
  ...rest
3564
3592
  }) {
3565
3593
  const children = parts.map(
3566
- (part, i) => part.kind === "same" ? /* @__PURE__ */ jsx37(Fragment9, { children: part.text }, i) : /* @__PURE__ */ jsx37(
3594
+ (part, i) => part.kind === "same" ? /* @__PURE__ */ jsx37(Fragment10, { children: part.text }, i) : /* @__PURE__ */ jsx37(
3567
3595
  Change,
3568
3596
  {
3569
3597
  kind: part.kind,