@sableui/react 0.15.0 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (145) hide show
  1. package/CHANGELOG.md +99 -0
  2. package/bin/sable-lint.mjs +5 -1
  3. package/dist/{_contract-CkqXDVww.d.ts → _contract-CDa9-NdB.d.ts} +47 -1
  4. package/dist/blocks/approval-block.js +30 -48
  5. package/dist/blocks/approval-block.meta.js +7 -8
  6. package/dist/blocks/index.d.ts +1 -1
  7. package/dist/blocks/wallet-block.js +2 -2
  8. package/dist/components/app-shell/app-shell-frame.js +16 -0
  9. package/dist/components/app-shell/app-shell-main.js +12 -0
  10. package/dist/components/app-shell/app-shell.js +28 -4
  11. package/dist/components/app-shell/app-shell.meta.js +88 -4
  12. package/dist/components/area-chart/area-chart.meta.js +6 -1
  13. package/dist/components/attachment/attachment.meta.js +2 -1
  14. package/dist/components/attachment/attachment.variants.js +1 -1
  15. package/dist/components/badge/badge.meta.js +8 -1
  16. package/dist/components/badge/badge.variants.js +7 -7
  17. package/dist/components/banner/banner.meta.js +6 -1
  18. package/dist/components/banner/banner.variants.js +5 -5
  19. package/dist/components/bar-chart/bar-chart.meta.js +6 -1
  20. package/dist/components/caret-menu/caret-menu.js +5 -6
  21. package/dist/components/chat-composer/chat-composer.meta.js +2 -2
  22. package/dist/components/chat-composer/chat-composer.variants.js +1 -1
  23. package/dist/components/color-field/color-field.js +6 -8
  24. package/dist/components/combobox/combobox.js +27 -5
  25. package/dist/components/combobox/combobox.meta.js +10 -2
  26. package/dist/components/command-menu/command-menu.js +14 -3
  27. package/dist/components/command-menu/command-menu.meta.js +9 -1
  28. package/dist/components/date-picker/date-picker.meta.js +2 -2
  29. package/dist/components/feedback/feedback.js +1 -1
  30. package/dist/components/feedback/feedback.meta.js +2 -1
  31. package/dist/components/gradient-art/gradient-art.js +8 -5
  32. package/dist/components/gradient-art/gradient-art.meta.js +35 -2
  33. package/dist/components/input/input.meta.js +11 -3
  34. package/dist/components/input-group/input-group.js +1 -1
  35. package/dist/components/input-group/input-group.meta.js +1 -1
  36. package/dist/components/line-chart/line-chart.meta.js +6 -1
  37. package/dist/components/menu/menu.js +1 -1
  38. package/dist/components/meter/meter.js +53 -7
  39. package/dist/components/meter/meter.meta.js +51 -4
  40. package/dist/components/meter/meter.variants.js +43 -7
  41. package/dist/components/note/note.meta.js +5 -1
  42. package/dist/components/note/note.variants.js +4 -4
  43. package/dist/components/number-field/number-field.meta.js +2 -2
  44. package/dist/components/otp-field/otp-field.meta.js +2 -2
  45. package/dist/components/panel-group/panel-group.js +5 -8
  46. package/dist/components/pie-chart/pie-chart.meta.js +6 -1
  47. package/dist/components/radar-chart/radar-chart.meta.js +6 -1
  48. package/dist/components/radial-chart/radial-chart.meta.js +6 -1
  49. package/dist/components/search-input/search-input.meta.js +11 -3
  50. package/dist/components/section-rail/index.js +7 -0
  51. package/dist/components/section-rail/section-rail.js +153 -0
  52. package/dist/components/section-rail/section-rail.meta.js +189 -0
  53. package/dist/components/segmented-bar/segmented-bar.meta.js +8 -1
  54. package/dist/components/segmented-bar/segmented-bar.variants.js +7 -7
  55. package/dist/components/select/select.js +1 -1
  56. package/dist/components/select/select.meta.js +2 -2
  57. package/dist/components/sheet/sheet.js +5 -4
  58. package/dist/components/sidebar/sidebar.js +16 -1
  59. package/dist/components/sidebar/sidebar.meta.js +7 -2
  60. package/dist/components/sparkline/sparkline.meta.js +8 -1
  61. package/dist/components/status-dot/status-dot.meta.js +8 -1
  62. package/dist/components/status-dot/status-dot.variants.js +7 -7
  63. package/dist/components/stepper/stepper.meta.js +2 -1
  64. package/dist/components/stepper/stepper.variants.js +1 -1
  65. package/dist/components/table/table.meta.js +4 -4
  66. package/dist/components/textarea/textarea.js +5 -0
  67. package/dist/components/textarea/textarea.meta.js +3 -3
  68. package/dist/file-hashes.json +99 -92
  69. package/dist/foundations/accessibility.js +5 -0
  70. package/dist/index.d.ts +227 -25
  71. package/dist/index.js +12 -1
  72. package/dist/lib/chart/types.js +8 -8
  73. package/dist/lib/chart/use-arrival.js +5 -4
  74. package/dist/lib/field-shell.js +1 -1
  75. package/dist/lib/highlight-glide.js +6 -6
  76. package/dist/lib/scrub-gesture.js +6 -6
  77. package/dist/lib/taxonomy.js +1 -0
  78. package/dist/lib/use-active-section.js +59 -0
  79. package/dist/lib/use-caret-trigger.js +11 -18
  80. package/dist/lib/use-match-highlight.js +69 -0
  81. package/dist/lib/use-smooth-wheel.js +10 -8
  82. package/dist/presets/index.d.ts +13 -2
  83. package/dist/presets/index.js +17 -1
  84. package/dist/styles.css +100 -0
  85. package/dist/{theme-provider-CdSicAad.d.ts → theme-provider-BydfM5Z0.d.ts} +1 -1
  86. package/dist/themes/_contract.js +14 -0
  87. package/dist/themes/atelier/atelier.css +18 -0
  88. package/dist/themes/atelier/index.d.ts +1 -1
  89. package/dist/themes/contrast.js +48 -1
  90. package/dist/themes/ember/ember.css +18 -0
  91. package/dist/themes/ember/index.d.ts +1 -1
  92. package/dist/themes/index.d.ts +2 -2
  93. package/dist/themes/meridian/index.d.ts +1 -1
  94. package/dist/themes/meridian/meridian.css +18 -0
  95. package/dist/themes/poppy/index.d.ts +1 -1
  96. package/dist/themes/poppy/poppy.css +18 -0
  97. package/dist/themes/prism/index.d.ts +1 -1
  98. package/dist/themes/prism/prism.css +18 -0
  99. package/dist/themes/sable/index.d.ts +1 -1
  100. package/dist/themes/sable/sable.css +18 -0
  101. package/dist/themes/verdant/index.d.ts +1 -1
  102. package/dist/themes/verdant/verdant.css +18 -0
  103. package/llms/blocks/approval-block.md +40 -55
  104. package/llms/blocks/wallet-block.md +5 -4
  105. package/llms/components/app-shell.md +72 -3
  106. package/llms/components/area-chart.md +1 -1
  107. package/llms/components/attachment.md +1 -1
  108. package/llms/components/badge.md +1 -1
  109. package/llms/components/banner.md +1 -1
  110. package/llms/components/bar-chart.md +1 -1
  111. package/llms/components/chat-composer.md +1 -1
  112. package/llms/components/combobox.md +2 -1
  113. package/llms/components/command-menu.md +3 -2
  114. package/llms/components/date-picker.md +1 -1
  115. package/llms/components/feedback.md +1 -1
  116. package/llms/components/gradient-art.md +28 -1
  117. package/llms/components/input-group.md +1 -1
  118. package/llms/components/input.md +4 -2
  119. package/llms/components/line-chart.md +1 -1
  120. package/llms/components/meter.md +33 -4
  121. package/llms/components/note.md +1 -1
  122. package/llms/components/number-field.md +1 -1
  123. package/llms/components/otp-field.md +1 -1
  124. package/llms/components/pie-chart.md +1 -1
  125. package/llms/components/radar-chart.md +1 -1
  126. package/llms/components/radial-chart.md +1 -1
  127. package/llms/components/search-input.md +20 -2
  128. package/llms/components/section-rail.md +101 -0
  129. package/llms/components/segmented-bar.md +1 -1
  130. package/llms/components/select.md +1 -1
  131. package/llms/components/sidebar.md +3 -2
  132. package/llms/components/sparkline.md +1 -1
  133. package/llms/components/status-dot.md +1 -1
  134. package/llms/components/stepper.md +1 -1
  135. package/llms/components/table.md +2 -2
  136. package/llms/components/textarea.md +2 -2
  137. package/llms/foundations/accessibility.md +1 -0
  138. package/llms/reference/token-intent.md +22 -2
  139. package/llms/reference/utilities.md +5 -2
  140. package/llms-full.txt +367 -105
  141. package/llms.txt +6 -5
  142. package/package.json +1 -1
  143. package/registry.json +609 -67
  144. package/scripts/lint-rules.mjs +6 -3
  145. package/targets/ios.json +9 -0
@@ -40,7 +40,7 @@ Use CommandMenu for global or contextual keyboard-driven actions: navigation, cr
40
40
 
41
41
  | Name | Type | Description |
42
42
  | --- | --- | --- |
43
- | CommandMenuTrigger | { onOpen?: () => void; placeholder?: string; shortcut?: string \| false; size?: "sm" \| "md"; disabled?: boolean } | The search field in a header that is really a button. Wears SearchInput's shell (border-input, card surface, leading magnifier, muted placeholder) on the control ladder, with a trailing Kbd: ⌘K on macOS, Ctrl K elsewhere. onOpen runs on click and on the shortcut, which it listens for page-wide (useCommandMenuShortcut). shortcut is the key (default "k"); false drops both the hint and the listener. size is sm in a header beside small buttons, md (default) beside Input and SearchInput. It sits outside CommandMenu, so the Dialog returns focus to it on close. |
43
+ | CommandMenuTrigger | { onOpen?: () => void; placeholder?: string; shortcut?: string \| false; size?: "sm" \| "md"; disabled?: boolean } | The search field in a header that is really a button. Wears SearchInput's shell (the field fill and outline, leading magnifier, muted placeholder) on the control ladder, with a trailing Kbd: ⌘K on macOS, Ctrl K elsewhere. onOpen runs on click and on the shortcut, which it listens for page-wide (useCommandMenuShortcut). shortcut is the key (default "k"); false drops both the hint and the listener. size is sm in a header beside small buttons, md (default) beside Input and SearchInput. It sits outside CommandMenu, so the Dialog returns focus to it on close. |
44
44
  | useCommandMenuShortcut | (onShortcut: (event: KeyboardEvent) => void, key?: string \| false) => void | The page-wide ⌘K / Ctrl+K listener CommandMenuTrigger installs, for a palette with no visible trigger. Ignored while focus is in a text field, textarea, select or contenteditable (Ctrl+K belongs to the field there) except the palette's own input, so the callback may toggle. It calls preventDefault() on a match and skips an event already prevented, so two listeners on one page fire once. key defaults to "k"; false turns it off. Removed on unmount. |
45
45
  | CommandMenuPanel | { items?: CommandItem[]; query?: string; onQueryChange?: (query: string) => void; filter?: CommandFilter; listClassName?: string; placeholder?: string; emptyMessage?: string; title?: string; scope?: ReactNode; inputProps?: ComponentProps<'input'>; className?: string; children?: ReactNode } | The palette rendered in place: the same search header, filtered and grouped listbox, keyboard model and result-count live region, on a static card (border-border, bg-card, no shadow) instead of in a Dialog. Takes every CommandMenu prop except open, onOpenChange, finalFocus and description; className lands on the card, title names the listbox. Focus is not trapped and the input does not take focus on mount (inputProps.autoFocus opts in). Esc clears the query, and passes through once it is empty, so a Popover around the panel still closes. A chosen row runs its onSelect and the panel stays as it was. The page-wide shortcut does not fire from its input. |
46
46
  | CommandMenuGroup | { label: string; children } | A labelled group of command rows. |
@@ -58,6 +58,7 @@ Use CommandMenu for global or contextual keyboard-driven actions: navigation, cr
58
58
  | query | string | — | Controlled query. Leave undefined to let the palette own it; either way it resets on close (a controlled caller gets onQueryChange('')). |
59
59
  | onQueryChange | (query: string) => void | — | Called on every query change. Required to control query, optional otherwise. |
60
60
  | filter | ((items: CommandItem[], query: string) => CommandItem[]) \| null | case-insensitive substring match on label | Replaces or disables the built-in match. A function ranks and filters items itself and its order is kept verbatim; null renders items exactly as given. Either also stops declarative CommandMenuItem children self-filtering. The query arrives raw — trimming and case are policy, and this is the prop that hands policy over. |
61
+ | highlightMatches | boolean | true | Paints the query wherever it appears in a row's label (not its detail or hint), on the `highlight` / `highlight-foreground` pair, through the CSS Custom Highlight API — no `<mark>` in the row, and the accessible name is unchanged. A literal, case-insensitive match, so under a fuzzy `filter` a row that matched as a subsequence keeps an unpainted label. `false` turns it off. Applies to CommandMenuPanel too. |
61
62
  | listClassName | string | — | Classes for the scrolling list. className lands on the dialog surface, so this is the seam for the list's own height cap — listClassName="max-h-[60vh]". The list is a Scroller, so the cap lands on the element that scrolls and the edge fades come free. |
62
63
  | placeholder | string | "Type a command or search…" | Placeholder for the search input. |
63
64
  | emptyMessage | string | "No results found." | Copy shown when the query matches nothing. |
@@ -68,7 +69,7 @@ Use CommandMenu for global or contextual keyboard-driven actions: navigation, cr
68
69
 
69
70
  ## Tokens used
70
71
 
71
- `card`, `card-foreground`, `border`, `foreground`, `muted-foreground`, `accent`, `accent-foreground`
72
+ `highlight`, `highlight-foreground`, `card`, `card-foreground`, `border`, `foreground`, `muted-foreground`, `accent`, `accent-foreground`
72
73
 
73
74
  ## Motion
74
75
 
@@ -38,7 +38,7 @@ Use DatePicker when users need to choose one date — or, with granularity="minu
38
38
 
39
39
  ## Tokens used
40
40
 
41
- `primary`, `primary-foreground`, `accent`, `accent-foreground`, `foreground`, `muted-foreground`, `popover`, `popover-foreground`, `border`, `input`, `card`, `ring`
41
+ `primary`, `primary-foreground`, `accent`, `accent-foreground`, `foreground`, `muted-foreground`, `popover`, `popover-foreground`, `border`, `field-border`, `field`, `ring`
42
42
 
43
43
  ## Accessibility
44
44
 
@@ -32,7 +32,7 @@ Use Feedback to collect lightweight sentiment, usefulness, or quality signals fr
32
32
 
33
33
  ## Tokens used
34
34
 
35
- `popover`, `popover-foreground`, `border`, `input`, `card`, `foreground`, `muted-foreground`, `accent`, `accent-foreground`, `ring`, `success`
35
+ `popover`, `popover-foreground`, `border`, `input`, `card`, `foreground`, `muted-foreground`, `accent`, `accent-foreground`, `ring`, `success`, `success-mark`
36
36
 
37
37
  ## Motion
38
38
 
@@ -8,13 +8,15 @@ Placeholder artwork painted by a static WebGL shader from the preset's own art d
8
8
 
9
9
  ## Usage
10
10
 
11
- Use GradientArt where a real image belongs but none exists yet, or where the image is decoration: a product cover before upload, a profile banner, an album tile, an avatar with no photo. It reads the theme's own tokens, so it follows every preset, mode and live edit; `seed` fixes each item's composition so a list of covers or avatars stays individually recognisable.
11
+ Use GradientArt where a real image belongs but none exists yet, or where the image is decoration: a product cover before upload, a profile banner, an album tile, an avatar with no photo. It reads the theme's own tokens, so it follows every preset, mode and live edit; `seed` fixes each item's composition so a list of covers or avatars stays individually recognisable. `intensity="soft"` paints the same art as a pastel wash — the iridescent aura AI products use as a card or modal header band, and as a stage for floating chips and logo tiles.
12
12
 
13
13
  ### Best practices
14
14
 
15
15
  - Do: Pass a stable `seed` per item — the name for an avatar, the title for a cover. Each item then keeps the same art across renders, reloads and themes, which is what lets someone recognise it.
16
16
  - Do: Size it with layout classes (`aspect-[4/3]`, `h-20`, `size-10 rounded-full`) and put any glyph or initials inside as children; they render above the art.
17
17
  - Do: Keep the default `mesh` for covers, tiles and avatars. Reach for `grain` where a printed, tactile surface suits — a banner, a hero card — and keep one variant per surface type so art reads as a system.
18
+ - Do: Use `intensity="soft"` as a stage: a card or modal header band, the art on an integration or pricing card, a field behind floating prompt chips or rounded logo tiles. Make the tiles and chips floating surfaces (`bg-popover` with `border-border-floating` and a shadow, or a `floating` Button) so they lift off the wash; soft stays light in dark mode too, so the same composition holds in both.
19
+ - Don't: Do not put text straight on soft art with `text-overlay-foreground` ink. Soft is a light ground in every mode, so overlay ink washes out on it; put the content on a Card or a chip over the art, or use the default vivid intensity for a short overlay label.
18
20
  - Don't: Do not put body text over it. The art spans the palette's lightest and most saturated tones, so no single ink is guaranteed to clear contrast across it; reserve overlays for a short label on a scrim or an avatar's initials.
19
21
  - Don't: Do not pass colours, and reach for `tones` rarely. The art follows the preset's --art-* tokens on purpose; to change how a product's art looks, set the --theme-art-* hooks in its theme or preset, so every cover, banner and avatar changes together.
20
22
  - Don't: Do not use it as a page or hero background. It paints at the element's pixel size, so a full-viewport instance costs a large frame on every theme change; it is sized for covers, banners and tiles.
@@ -26,12 +28,14 @@ Use GradientArt where a real image belongs but none exists yet, or where the ima
26
28
  | variant | "mesh" \| "grain" | "mesh" | mesh: soft spots of colour pulled together by gentle waves. grain: glowing light over deep corners with a printed texture. |
27
29
  | tones | ("info" \| "3" \| "2" \| "1" \| "warning")[] | — | Paint these `--tone-*` colours instead of the preset's art palette. Leave unset to follow the preset's art direction. |
28
30
  | seed | string \| number | — | Fixes the composition: the same seed paints the same art in every theme. Pass a stable id. |
31
+ | intensity | "vivid" \| "soft" | "vivid" | vivid: the preset's art at full strength. soft: every colour and the ground lifted to a light pastel of itself (high lightness, half chroma) — a wash for header bands and for staging chips and logo tiles, light in both modes. |
29
32
  | children | ReactNode | — | Rendered above the art — initials, a glyph, a short label. |
30
33
  | className | string | — | Size and shape: aspect, height, rounding. |
31
34
 
32
35
  ## Variants
33
36
 
34
37
  - **variant**: mesh, grain
38
+ - **intensity**: vivid, soft
35
39
 
36
40
  ## Tokens used
37
41
 
@@ -78,3 +82,26 @@ import { GradientArt } from "@sableui/react"
78
82
 
79
83
  <GradientArt seed="Mara Okafor" className="grid size-10 place-items-center rounded-full text-label-md text-overlay-foreground">MO</GradientArt>
80
84
  ```
85
+
86
+ ### Soft header band with logo tiles
87
+
88
+ ```tsx
89
+ import { Button, Card, CardDescription, CardFooter, CardHeader, CardTitle, GradientArt, Icon } from "@sableui/react"
90
+
91
+ <Card className="w-full max-w-sm">
92
+ <GradientArt intensity="soft" seed="Integrations" className="flex h-32 items-center justify-center gap-3 rounded-t-lg">
93
+ {["mail", "calendar", "chart"].map((name) => (
94
+ <span key={name} className="grid size-12 place-items-center rounded-lg border border-border-floating bg-popover text-popover-foreground shadow-md">
95
+ <Icon name={name} className="text-xl" />
96
+ </span>
97
+ ))}
98
+ </GradientArt>
99
+ <CardHeader>
100
+ <CardTitle>Connect your tools</CardTitle>
101
+ <CardDescription>Bring mail, calendar and analytics into one place.</CardDescription>
102
+ </CardHeader>
103
+ <CardFooter>
104
+ <Button>Connect</Button>
105
+ </CardFooter>
106
+ </Card>
107
+ ```
@@ -30,7 +30,7 @@ Use InputGroup when something belongs inside the field with the text: a unit, a
30
30
 
31
31
  ## Tokens used
32
32
 
33
- `input`, `card`, `ring`, `destructive`, `muted-foreground`, `foreground`
33
+ `field-border`, `field`, `ring`, `destructive`, `muted-foreground`, `foreground`
34
34
 
35
35
  ## Motion
36
36
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  **Status:** stable
4
4
 
5
- Styled native text input. Soft-square (rounded-sm), card surface, 2px focus ring; aria-invalid raises the destructive error treatment.
5
+ Styled native text input. Soft-square (rounded-sm), the theme's field surface (outlined or filled), 2px focus ring; aria-invalid raises the destructive error treatment.
6
6
 
7
7
  **Keywords:** input, text input, field, form, email, search, file, invalid, validation
8
8
 
@@ -16,6 +16,8 @@ Use Input for short freeform text, search, email, password, file, and native inp
16
16
  - Do: Use type, inputMode, autoComplete, and placeholder as progressive hints, not as the only label.
17
17
  - Do: Set aria-invalid or use Field error copy when validation fails.
18
18
  - Do: Keep a focusable field at 16px or larger on mobile. iOS Safari zooms the whole page when a focused input computes under 16px and never zooms back out. Input handles this itself — below the sm breakpoint it renders text-body-lg (17px) and returns to text-body-md at sm and up. Build a custom field the same way: a second paired tier under a max-sm: variant, never a hand-set font-size.
19
+ - Do: Choose outlined or filled fields once, in the theme: `--theme-field: var(--muted)` with `--theme-field-border: transparent` turns every Input, Textarea, Select, Combobox, SearchInput, InputGroup and ChatComposer into a borderless filled well; unset, they are outlined on the card. Every field reads the `field` / `field-border` pair, so they change together.
20
+ - Don't: Restyle one Input as a filled chip with `bg-muted border-transparent`. It drifts from every other field on the page, and the theme can no longer move them together; set the field tokens instead.
19
21
  - Don't: Override an Input's type with a page tier below 16px (text-body-md, text-body-sm, text-label-*) without a >=16px mobile arm — that reintroduces the iOS focus zoom.
20
22
  - Don't: Use placeholder text as the label; it disappears once the user types and is not enough context.
21
23
  - Don't: Use Input for long body copy. Use Textarea for multi-line content.
@@ -45,7 +47,7 @@ Use Input for short freeform text, search, email, password, file, and native inp
45
47
 
46
48
  ## Tokens used
47
49
 
48
- `input`, `card`, `foreground`, `muted-foreground`, `ring`, `destructive`
50
+ `field-border`, `field`, `foreground`, `muted-foreground`, `ring`, `destructive`
49
51
 
50
52
  ## Accessibility
51
53
 
@@ -46,7 +46,7 @@ Use LineChart to compare how two or more measures change over the same sequence,
46
46
 
47
47
  ## Tokens used
48
48
 
49
- `tone-1`, `tone-2`, `tone-3`, `tone-info`, `tone-warning`, `primary`, `muted-foreground`, `border`, `popover`, `popover-foreground`, `foreground`, `background`
49
+ `tone-1`, `tone-2`, `tone-3`, `tone-info`, `tone-warning`, `primary`, `muted-foreground`, `border`, `popover`, `popover-foreground`, `foreground`, `background`, `tone-warning-mark`, `tone-info-mark`, `tone-1-mark`, `tone-2-mark`, `tone-3-mark`
50
50
 
51
51
  ## Motion
52
52
 
@@ -4,7 +4,7 @@
4
4
 
5
5
  A role=meter reading over Base UI Meter, drawn as a bar or a ring — how full something is within a known range (storage, quota, strength), with a tone for good/caution/bad. Not task progress.
6
6
 
7
- **Keywords:** meter, usage bar, storage, quota, capacity, limit, password strength, level
7
+ **Keywords:** meter, usage bar, storage, quota, capacity, limit, password strength, level, segmented, tick bar, score, credits, spend limit
8
8
 
9
9
  ## Usage
10
10
 
@@ -15,15 +15,17 @@ Use Meter to show a static measurement against a known maximum — storage used,
15
15
  - Do: Label it and show the reading in units people use ("7.2 of 10 GB"), with MeterLabel and MeterValue. The bar shows proportion; the number is what gets acted on.
16
16
  - Do: Pick `tone` from thresholds you own and apply them consistently across the page — warning near the limit, destructive at or over it. The component does not guess where good becomes bad.
17
17
  - Do: Use shape="ring" when the reading is a score that belongs in a small, glanceable spot on a card. It is the same measurement drawn as a circular gauge, not a different component.
18
+ - Do: Use `segments` (a tick bar, ▮▮▮▮▮▮▯▯▯▯) for a discrete budget, score or quota read at a glance — a spend limit, credits left, an ICP lead score, months of data coverage. Pick a count that matches the unit when there is one (10 for a score out of 10, 12 for months) and otherwise size it to the track: a tick reads as a tick when it is taller than it is wide, about one per 8px of track (40 across a card row, 20 in a narrow table cell). Any reading above min shows at least one tick.
18
19
  - Don't: Do not use Meter for a task in flight — an upload, an install, a step count. That is Progress; Meter tells assistive tech nothing is happening. → use **Progress**
19
20
  - Don't: Do not use it for a value without a meaningful maximum (followers, total spend). A bar implies a full scale; an unbounded count is a stat.
21
+ - Don't: Do not segment a fine-grained continuous value where the exact proportion matters (disk at 73.4%) — ticks round it to the nearest mark; use the continuous bar. And do not use a tick bar for a task in flight or steps completed: that is Progress, or a Stepper for named steps. → use **Progress**
20
22
 
21
23
  ### Anatomy
22
24
 
23
25
  | Part | Required | Description |
24
26
  | --- | --- | --- |
25
27
  | MeterTrack | yes | The rail that represents the full range. |
26
- | MeterIndicator | yes | The fill, sized from value and coloured by tone. |
28
+ | MeterIndicator | yes | The fill, sized from value and coloured by tone. Not used with `segments`: the track draws its ticks instead. |
27
29
  | MeterLabel | no | The visible name; also names the meter for assistive tech. |
28
30
  | MeterValue | no | The formatted reading. |
29
31
 
@@ -31,7 +33,7 @@ Use Meter to show a static measurement against a known maximum — storage used,
31
33
 
32
34
  | Name | Type | Description |
33
35
  | --- | --- | --- |
34
- | MeterTrack | Base UI Track props + className + size | The rail. Rendered automatically when Meter has no children; renders a MeterIndicator by default. |
36
+ | MeterTrack | Base UI Track props + className + size | The rail. Rendered automatically when Meter has no children; renders a MeterIndicator by default — or, when the Meter has `segments`, its row of ticks (children are not rendered then). |
35
37
  | MeterIndicator | Base UI Indicator props + className + tone | The fill. Inherits the Meter's tone unless given its own. |
36
38
  | MeterLabel | Base UI Label props + className | Visible label; wires aria-labelledby on the root. |
37
39
  | MeterValue | Base UI Value props + className | Formatted reading. Accepts a `(formatted, value) => ReactNode` child. |
@@ -47,6 +49,7 @@ Use Meter to show a static measurement against a known maximum — storage used,
47
49
  | shape | "bar" \| "ring" | "bar" | bar is a horizontal track; ring is a circular gauge for a small, glanceable reading — the same value, min, max and tones. |
48
50
  | size | "sm" \| "md" \| "lg" | "md" | Ring diameter (40 / 64 / 96px at the default spacing; the centre value steps with it), or bar thickness on the same sm / md / lg rails as Progress. |
49
51
  | showValue | boolean | — | Print the value in the ring's centre (formatted like MeterValue). |
52
+ | segments | number | — | Draw the bar as this many separate ticks instead of a continuous fill (a quota, score or spend limit). Filled ticks = round(share × N), at least one for any reading above min, none at min. Ticks take the tone; empty ones sit on the rail colour. The segmented track has its own, taller sm / md / lg ladder. Bar only: ignored for shape="ring". |
50
53
  | strokeWidth | number | 6 | Ring stroke width, in viewBox units of 100. |
51
54
  | format | Intl.NumberFormatOptions | — | Formatting for MeterValue and aria-valuetext (percent, unit, currency). |
52
55
  | getAriaValueText | (formatted: string, value: number) => string | — | Human-readable reading for assistive tech ("7.2 of 10 gigabytes used"). |
@@ -60,13 +63,14 @@ Use Meter to show a static measurement against a known maximum — storage used,
60
63
 
61
64
  ## Tokens used
62
65
 
63
- `secondary`, `primary`, `success`, `tone-warning`, `destructive`, `foreground`, `muted-foreground`
66
+ `secondary`, `primary`, `tone-3`, `success`, `tone-warning`, `destructive`, `foreground`, `muted-foreground`, `destructive-mark`, `success-mark`, `tone-warning-mark`, `tone-3-mark`
64
67
 
65
68
  ## Motion
66
69
 
67
70
  - **arrival** · Meter · _no motion_ — A meter is a reading, present with the page that reports it. Animating the fill up from zero on load would act out a change that did not happen.
68
71
  - **response** · Meter · _no motion_ — It reports and accepts no input. A bar that reacted to the pointer would read as a Slider the user could drag.
69
72
  - **transition** · MeterIndicator · _custom (outside the recipe layer)_ — Width and colour ease on the motion tokens (no recipe animates width) when the reading changes — freeing space after a delete visibly shrinks the bar, so the user sees the cause and effect rather than a different bar.
73
+ - **transition** · MeterTrack (segments) · _custom (outside the recipe layer)_ — With segments, the fill runs through the ticks from the edge it is leaving — left to right as it grows, right to left as it shrinks — each tick easing its colour on the fast token after a stagger that is a share of the slow token (a full sweep is one slow duration) × --motion-expressive. Every tick switching at once would read as a different bar, not the same budget moving; `subtle` lands them together and off is instant.
70
74
  - **activity** · Meter · _no motion_ — A meter has no indeterminate state; a reading is either known or not shown. Unknown-but-working is Progress with no value.
71
75
 
72
76
  ## Accessibility
@@ -111,6 +115,31 @@ import { Meter, MeterLabel, MeterTrack, MeterValue } from "@sableui/react"
111
115
  </Meter>
112
116
  ```
113
117
 
118
+ ### Segmented spend limit
119
+
120
+ ```tsx
121
+ import { Meter, MeterLabel, MeterTrack, MeterValue } from "@sableui/react"
122
+
123
+ <Meter value={1240} max={2000} segments={40} getAriaValueText={() => "€1,240 of €2,000 spent this month"}>
124
+ <div className="mb-2 flex flex-wrap items-center justify-between gap-x-3 gap-y-1">
125
+ <MeterLabel>Monthly spend limit</MeterLabel>
126
+ <MeterValue>{() => "€1,240 / €2,000"}</MeterValue>
127
+ </div>
128
+ <MeterTrack />
129
+ </Meter>
130
+ ```
131
+
132
+ ### Segmented score cell
133
+
134
+ ```tsx
135
+ import { Meter } from "@sableui/react"
136
+
137
+ <div className="flex items-center gap-3">
138
+ <Meter value={8} max={10} segments={10} size="sm" tone="success" className="w-24" aria-label="ICP score" getAriaValueText={(_, v) => `${v} out of 10`} />
139
+ <span className="text-mono-sm text-foreground">8/10</span>
140
+ </div>
141
+ ```
142
+
114
143
  ### Tones
115
144
 
116
145
  ```tsx
@@ -35,7 +35,7 @@ Use Note for helpful supporting information that should sit near the relevant co
35
35
 
36
36
  ## Tokens used
37
37
 
38
- `foreground`, `border`, `muted`, `muted-foreground`, `tone-info`, `tone-warning`, `success`, `destructive`
38
+ `foreground`, `border`, `muted`, `muted-foreground`, `tone-info`, `tone-warning`, `success`, `destructive`, `destructive-mark`, `success-mark`, `tone-warning-mark`, `tone-info-mark`
39
39
 
40
40
  ## Motion
41
41
 
@@ -32,7 +32,7 @@ Use NumberField for numeric values that benefit from validation, min/max bounds,
32
32
 
33
33
  ## Tokens used
34
34
 
35
- `input`, `card`, `foreground`, `muted-foreground`, `accent`, `accent-foreground`, `ring`
35
+ `field-border`, `field`, `foreground`, `muted-foreground`, `accent`, `accent-foreground`, `ring`
36
36
 
37
37
  ## Accessibility
38
38
 
@@ -48,7 +48,7 @@ Use OtpField for a short, fixed-length code the user copies from somewhere else
48
48
 
49
49
  ## Tokens used
50
50
 
51
- `input`, `card`, `foreground`, `muted-foreground`, `border-strong`, `primary`, `ring`, `destructive`
51
+ `field-border`, `field`, `foreground`, `muted-foreground`, `border-strong`, `primary`, `ring`, `destructive`
52
52
 
53
53
  ## Motion
54
54
 
@@ -44,7 +44,7 @@ Use PieChart for a share of one total across two to six categories — traffic s
44
44
 
45
45
  ## Tokens used
46
46
 
47
- `tone-1`, `tone-2`, `tone-3`, `tone-info`, `tone-warning`, `primary`, `muted-foreground`, `border`, `popover`, `popover-foreground`, `foreground`, `background`
47
+ `tone-1`, `tone-2`, `tone-3`, `tone-info`, `tone-warning`, `primary`, `muted-foreground`, `border`, `popover`, `popover-foreground`, `foreground`, `background`, `tone-warning-mark`, `tone-info-mark`, `tone-1-mark`, `tone-2-mark`, `tone-3-mark`
48
48
 
49
49
  ## Motion
50
50
 
@@ -43,7 +43,7 @@ Use RadarChart to compare the shape of a profile across five to eight dimensions
43
43
 
44
44
  ## Tokens used
45
45
 
46
- `tone-1`, `tone-2`, `tone-3`, `tone-info`, `tone-warning`, `primary`, `muted-foreground`, `border`, `popover`, `popover-foreground`, `foreground`, `background`
46
+ `tone-1`, `tone-2`, `tone-3`, `tone-info`, `tone-warning`, `primary`, `muted-foreground`, `border`, `popover`, `popover-foreground`, `foreground`, `background`, `tone-warning-mark`, `tone-info-mark`, `tone-1-mark`, `tone-2-mark`, `tone-3-mark`
47
47
 
48
48
  ## Motion
49
49
 
@@ -45,7 +45,7 @@ Use RadialChart for progress toward goals (several rings), a single score or usa
45
45
 
46
46
  ## Tokens used
47
47
 
48
- `tone-1`, `tone-2`, `tone-3`, `tone-info`, `tone-warning`, `primary`, `muted-foreground`, `border`, `popover`, `popover-foreground`, `foreground`, `background`
48
+ `tone-1`, `tone-2`, `tone-3`, `tone-info`, `tone-warning`, `primary`, `muted-foreground`, `border`, `popover`, `popover-foreground`, `foreground`, `background`, `tone-warning-mark`, `tone-info-mark`, `tone-1-mark`, `tone-2-mark`, `tone-3-mark`
49
49
 
50
50
  ## Motion
51
51
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  **Status:** stable
4
4
 
5
- Styled search field with a leading magnifier and a clearable X. Soft-square (rounded-sm), card surface, 2px focus ring; the clear button appears only when there is a value.
5
+ Styled search field with a leading magnifier and a clearable X. Soft-square (rounded-sm), the theme's field surface, 2px focus ring; the clear button appears only when there is a value.
6
6
 
7
7
  **Keywords:** search, input, filter, query
8
8
 
@@ -15,6 +15,7 @@ Use SearchInput for text queries that filter or search a known content set.
15
15
  - Do: Use a clear placeholder that names the content being searched.
16
16
  - Do: Provide a visible clear affordance when query state persists.
17
17
  - Do: When a button stands in for search (a header trigger that opens CommandMenu with ⌘K), use CommandMenuTrigger. It wears this field's shell from the same source, so the two cannot drift; a hand-styled button already did once, into a borderless chip that matched neither the field nor a button.
18
+ - Do: Light the query in the results it filters with `useMatchHighlight(query)` — a callback ref for the results container, with `selector` to keep it to titles. It paints on the `highlight` pair without wrapping matches in `<mark>`, the same paint Combobox and CommandMenu use for their rows.
18
19
  - Don't: Do not use SearchInput as a command palette; use CommandMenu for global commands, opened from the header by CommandMenuTrigger. A SearchInput that opens a dialog on focus is a text box you cannot type in.
19
20
  - Don't: Do not search on every keystroke against an expensive backend without debouncing. The user sees results flicker and reorder under them, and you send a request per character.
20
21
 
@@ -34,7 +35,7 @@ Use SearchInput for text queries that filter or search a known content set.
34
35
 
35
36
  ## Tokens used
36
37
 
37
- `input`, `card`, `foreground`, `muted-foreground`, `ring`, `destructive`
38
+ `field-border`, `field`, `foreground`, `muted-foreground`, `ring`, `destructive`
38
39
 
39
40
  ## Motion
40
41
 
@@ -82,6 +83,23 @@ import { Field, SearchInput } from "@sableui/react"
82
83
  <Field label="Search posts"><SearchInput defaultValue="roadmap" className="max-w-80" /></Field>
83
84
  ```
84
85
 
86
+ ### Highlighting matches in the results
87
+
88
+ ```tsx
89
+ import { SearchInput, useMatchHighlight } from "@sableui/react"
90
+
91
+ const [q, setQ] = useState("")
92
+ const results = useMatchHighlight<HTMLUListElement>(q, { selector: "[data-title]" })
93
+ const shown = posts.filter((post) => post.title.toLowerCase().includes(q.trim().toLowerCase()))
94
+
95
+ <SearchInput value={q} onValueChange={setQ} placeholder="Search posts" />
96
+ <ul ref={results}>
97
+ {shown.map((post) => (
98
+ <li key={post.id}><span data-title>{post.title}</span></li>
99
+ ))}
100
+ </ul>
101
+ ```
102
+
85
103
  ### Disabled
86
104
 
87
105
  ```tsx
@@ -0,0 +1,101 @@
1
+ # SectionRail
2
+
3
+ **Status:** stable
4
+
5
+ A slim vertical rail of dots, one per section of a long page, that shows where the reader is and jumps to any section. The current section's bar glides as the page scrolls; a shared tooltip names each dot.
6
+
7
+ **Keywords:** section rail, scroll spy, scrollspy, dot navigation, page navigation, table of contents, on this page, chapter nav, scroll progress, case study nav
8
+
9
+ ## Usage
10
+
11
+ Use SectionRail beside a long, single-column read — a case study, an essay, a long landing page — so the reader can see which section they are in and jump to another without leaving the text. It follows the scroll by itself (useActiveSection); pass value when the parent already knows the current section.
12
+
13
+ ### Best practices
14
+
15
+ - Do: Give every section an id that matches its entry, and keep the list to the page's top-level sections — about ten at most, so the rail stays shorter than the viewport.
16
+ - Do: Place it with layout classes — usually `fixed top-1/2 left-6 z-(--z-nav) -translate-y-1/2` — and hide it below the width where it would overlap the text (`max-lg:hidden`). Offer the same links another way on narrow screens, such as a Collapsible list at the top of the page.
17
+ - Do: Pass `root` when the content scrolls inside an element (a Scroller, an inset AppShell main) rather than the window, and give sections `scroll-mt-*` when a sticky header would cover their tops after a jump.
18
+ - Do: Set `side="right"` when the rail sits on the right edge, so labels open toward the content.
19
+ - Don't: Use it for app navigation between pages; that is SideNav or Sidebar. A SectionRail only moves within one page. → use **SideNav**, **Sidebar**
20
+ - Don't: Use it to show the steps of a task or a flow; that is Stepper, which shows progress through steps rather than position in a read. → use **Stepper**
21
+ - Don't: Use it for a handful of switchable views in one place; that is Tabs. → use **Tabs**
22
+
23
+ ### Anatomy
24
+
25
+ | Part | Required | Description |
26
+ | --- | --- | --- |
27
+ | Rail | yes | The `<nav>` landmark: a rounded muted track holding one link per section. |
28
+ | Dot | yes | A link to `#id`, drawn as a small dot that grows under the pointer or focus. |
29
+ | Marker | yes | One primary bar that sits on the current section's dot and glides to the next. |
30
+ | Label | yes | A shared tooltip that names the hovered or focused dot and travels between dots. |
31
+
32
+ ## Props
33
+
34
+ | Name | Type | Default | Description |
35
+ | --- | --- | --- | --- |
36
+ | sections | readonly { id: string; label: string }[] | — | The sections, in document order. `id` is the section element's id (the link goes to `#id`); `label` is the tooltip and the link's accessible name. |
37
+ | value | string | — | The current section's id, when the parent decides it. Leave it unset and the rail follows the scroll itself. |
38
+ | onValueChange | (id: string) => void | — | Called with the new id when the reader scrolls into a section or picks one. |
39
+ | offset | number | 0.35 | Where the reading line sits, as a fraction of the viewport from its top. A section becomes current once its top crosses it. |
40
+ | root | RefObject<HTMLElement \| null> | — | The element that scrolls, when it is not the window. |
41
+ | side | "left" \| "right" | "left" | Which edge of the page the rail sits on. The labels open on the other side, toward the content. |
42
+ | motion | "full" \| "subtle" \| "off" | "full" | How much motion this component uses: `full` inherits the app's policy, `subtle` keeps feedback and drops the marker's glide, `off` stills it. It can only turn motion down — it never overrides a reader's reduce-motion setting. |
43
+ | aria-label | string | "Sections" | The nav landmark's name. Name it for the page ("Case study sections") when it has other navs. |
44
+
45
+ ## Tokens used
46
+
47
+ `muted`, `muted-foreground`, `foreground`, `primary`, `ring`, `popover`, `popover-foreground`, `border-floating`
48
+
49
+ ## Motion
50
+
51
+ - **arrival** · Root · _no motion_ — Page chrome arrives with the page; the marker lands on the current section with no transition, because a bar sliding in on load narrates a move nobody made.
52
+ - **response** · Dot · `motion-interactive` — The dot darkens and grows under the pointer or focus, and the label travels between dots as one tooltip (TooltipGroup) rather than closing and reopening at each.
53
+ - **transition** · Marker · `motion-glide` — One bar travels to the new section as the page scrolls, so the change reads as movement down the page. A picked section holds the bar until the reader scrolls by hand, so it goes straight there instead of stepping through every section in between, and stays on the pick even when the page ends before that section can reach the reading line. Expressive: `subtle` moves it at once.
54
+ - **activity** · Root · _no motion_ — Navigation chrome has no working state.
55
+
56
+ ## Accessibility
57
+
58
+ - Renders a <nav> landmark (default name "Sections") of real links to `#id`, so it works with no script, middle-click and copy-link.
59
+ - Each link is named by its section's label; the current one carries aria-current="location".
60
+ - Activating a link from the keyboard moves focus into the section, as following an anchor does, so the next Tab continues from there.
61
+ - The tooltip opens on focus as well as hover; the dots are 44×32 targets.
62
+ - Under reduced motion the jump is instant and the marker moves without travel.
63
+
64
+ ## Related
65
+
66
+ - Components: SideNav, Tooltip, Stepper, Tabs, GlidingHighlight
67
+
68
+ ## Examples
69
+
70
+ ### Fixed beside a long read
71
+
72
+ ```tsx
73
+ import { SectionRail } from "@sableui/react"
74
+
75
+ <SectionRail
76
+ aria-label="Case study sections"
77
+ sections={[
78
+ { id: "overview", label: "Overview" },
79
+ { id: "problem", label: "The problem" },
80
+ { id: "process", label: "Process" },
81
+ { id: "outcome", label: "Outcome" },
82
+ ]}
83
+ className="fixed top-1/2 left-6 z-(--z-nav) -translate-y-1/2 max-lg:hidden"
84
+ />
85
+ ```
86
+
87
+ ### On the right edge
88
+
89
+ ```tsx
90
+ import { SectionRail } from "@sableui/react"
91
+
92
+ <SectionRail
93
+ side="right"
94
+ sections={[
95
+ { id: "intro", label: "Introduction" },
96
+ { id: "usage", label: "Usage" },
97
+ { id: "api", label: "API" },
98
+ ]}
99
+ className="fixed top-1/2 right-6 z-(--z-nav) -translate-y-1/2 max-lg:hidden"
100
+ />
101
+ ```
@@ -40,7 +40,7 @@ Use SegmentedBar when a stat card or row needs to show how a total divides into
40
40
 
41
41
  ## Tokens used
42
42
 
43
- `tone-1`, `tone-2`, `tone-3`, `tone-info`, `primary`, `muted-foreground`, `success`, `tone-warning`, `destructive`
43
+ `tone-1`, `tone-2`, `tone-3`, `tone-info`, `primary`, `muted-foreground`, `success`, `tone-warning`, `destructive`, `destructive-mark`, `success-mark`, `tone-warning-mark`, `tone-info-mark`, `tone-1-mark`, `tone-2-mark`, `tone-3-mark`
44
44
 
45
45
  ## Motion
46
46
 
@@ -50,7 +50,7 @@ Use Select when users choose one value from a known, finite set. Use Combobox fo
50
50
 
51
51
  ## Tokens used
52
52
 
53
- `input`, `card`, `foreground`, `ring`, `border`, `popover`, `popover-foreground`, `accent`, `accent-foreground`, `muted-foreground`
53
+ `field-border`, `field`, `foreground`, `ring`, `border`, `popover`, `popover-foreground`, `accent`, `accent-foreground`, `muted-foreground`
54
54
 
55
55
  ## Motion
56
56
 
@@ -15,6 +15,7 @@ Use Sidebar for persistent desktop navigation or application chrome outside the
15
15
  - Do: Use it for stable app destinations that should remain visible while content changes.
16
16
  - Do: Keep item labels predictable and active state obvious.
17
17
  - Do: Set surface="page" when a full-width top bar sits above the rail. A theme's tinted or dark rail is for a full-height rail that frames the app.
18
+ - Do: Keep surface="rail" inside AppShell frame="inset": the rail's tint becomes the ground the main panel floats on. Do not add a border-r class back — the gap is the divider.
18
19
  - Don't: Do not use Sidebar for page-local settings, filters, or temporary action groups.
19
20
  - Don't: Do not put page content in it. It is fixed-width chrome OUTSIDE the content grid, so anything in it is exempt from the page's measure and rhythm.
20
21
 
@@ -40,7 +41,7 @@ Use Sidebar for persistent desktop navigation or application chrome outside the
40
41
  | collapsed | boolean | false | Collapse the rail to an icon-only width (--sidebar-width-collapsed). Items hide their label + badge and reveal the label in a hover tooltip; the content area reflows. Controlled — wire a toggle from app state. Read the state in custom header/footer content via useSidebar(). |
41
42
  | icons | boolean | true | Render the leading icon on each SidebarItem. Set false for a text-only rail. Ignored while collapsed (icon-only by definition). |
42
43
  | variant | "app" \| "inline" | "app" | Where the rail lives. "app" is the page's chrome column — sticky, full viewport height, hidden below lg where AppHeader + BottomNav take over. "inline" is always visible and fills its parent's height, for a rail inside a bounded frame (a nested shell, an embedded preview). Use it instead of overriding the layout with !important classes. |
43
- | surface | "rail" \| "page" | "rail" | Which surface the rail is painted on. "rail" is its own --sidebar-* surface with a border — right for a full-height rail that frames the app, which a theme may tint or paint dark. "page" uses the page's surface and ink with no border — use it when a full-width header sits above the rail, where a tinted column starting under a page-coloured bar reads as pasted on. Use it instead of overriding bg-/text- classes, which the rail's re-pointed tokens defeat. |
44
+ | surface | "rail" \| "page" | "rail" | Which surface the rail is painted on. "rail" is its own --sidebar-* surface with a border — right for a full-height rail that frames the app, which a theme may tint or paint dark. "page" uses the page's surface and ink with no border — use it when a full-width header sits above the rail, where a tinted column starting under a page-coloured bar reads as pasted on. Use it instead of overriding bg-/text- classes, which the rail's re-pointed tokens defeat. In AppShell's sidebar slot with frame="inset", the rail's surface is also the shell's ground: from lg a "rail" rail drops its border (the gap around the main panel divides them) and either surface stretches to the shell's body row instead of sizing to the viewport. AppShell signals this to its sidebar slot only, so a Sidebar elsewhere in the page is unaffected. |
44
45
  | motion | "full" \| "subtle" \| "off" | "full" | How much motion this component uses: `full` inherits the app's policy, `subtle` keeps feedback and drops the collapse ease and the current-page glide, `off` stills it. It can only turn motion down — it never overrides a reader's reduce-motion setting. |
45
46
 
46
47
  ## Variants
@@ -49,7 +50,7 @@ Use Sidebar for persistent desktop navigation or application chrome outside the
49
50
 
50
51
  ## Tokens used
51
52
 
52
- `sidebar`, `sidebar-foreground`, `sidebar-border`, `sidebar-accent`, `sidebar-accent-foreground`, `sidebar-ring`
53
+ `sidebar`, `sidebar-foreground`, `sidebar-border`, `sidebar-accent`, `sidebar-accent-foreground`, `sidebar-ring`, `shell-ground`
53
54
 
54
55
  ## Motion
55
56
 
@@ -36,7 +36,7 @@ Use Sparkline next to a number to show where it has been — a KPI's last 30 day
36
36
 
37
37
  ## Tokens used
38
38
 
39
- `primary`, `tone-1`, `tone-2`, `tone-3`, `tone-info`, `tone-warning`, `muted-foreground`, `success`, `destructive`
39
+ `primary`, `tone-1`, `tone-2`, `tone-3`, `tone-info`, `tone-warning`, `muted-foreground`, `success`, `destructive`, `destructive-mark`, `success-mark`, `tone-warning-mark`, `tone-info-mark`, `tone-1-mark`, `tone-2-mark`, `tone-3-mark`
40
40
 
41
41
  ## Motion
42
42
 
@@ -39,7 +39,7 @@ Use StatusDot as a compact visual state cue next to text, avatars, rows, or labe
39
39
 
40
40
  ## Tokens used
41
41
 
42
- `palette-dot`, `muted-foreground`, `tone-3`, `success`, `tone-warning`, `tone-info`, `tone-1`, `tone-2`, `destructive`
42
+ `palette-dot`, `muted-foreground`, `tone-3`, `success`, `tone-warning`, `tone-info`, `tone-1`, `tone-2`, `destructive`, `destructive-mark`, `success-mark`, `tone-warning-mark`, `tone-info-mark`, `tone-1-mark`, `tone-2-mark`, `tone-3-mark`
43
43
 
44
44
  ## Accessibility
45
45
 
@@ -51,7 +51,7 @@ Use Stepper above or beside a flow that is split into a known, small number of n
51
51
 
52
52
  ## Tokens used
53
53
 
54
- `primary`, `primary-foreground`, `background`, `foreground`, `muted-foreground`, `border-strong`, `destructive`, `destructive-foreground`, `ring`
54
+ `primary`, `primary-foreground`, `background`, `foreground`, `muted-foreground`, `border-strong`, `destructive`, `destructive-foreground`, `ring`, `destructive-mark`
55
55
 
56
56
  ## Motion
57
57
 
@@ -36,8 +36,8 @@ Use Table for structured data with consistent columns and rows. Use DataTable wh
36
36
  | TableBody | ComponentPropsWithRef<"tbody"> | The data row group (tbody). |
37
37
  | TableFooter | ComponentPropsWithRef<"tfoot"> | Summary row group (tfoot) with a muted fill. |
38
38
  | TableRow | ComponentPropsWithRef<"tr"> | A row (tr) — hairline bottom border, hover lift, and a selected state via the data-selected attribute. |
39
- | TableHead | ComponentPropsWithRef<"th"> | A column header cell (th) in muted label styling. |
40
- | TableCell | ComponentPropsWithRef<"td"> | A data cell (td). |
39
+ | TableHead | ComponentPropsWithRef<"th"> & { numeric?: boolean } | A column header cell (th) in muted label styling. `numeric` marks a number column: end-aligned, tabular figures — set it on the column's cells too. |
40
+ | TableCell | ComponentPropsWithRef<"td"> & { numeric?: boolean } | A data cell (td). `numeric` end-aligns it with tabular figures, matching its column's header. |
41
41
  | TableCaption | ComponentPropsWithRef<"caption"> | Accessible description of the table (caption), muted. |
42
42
 
43
43
  ## Props
@@ -2,7 +2,7 @@
2
2
 
3
3
  **Status:** stable
4
4
 
5
- Styled native multi-line text control. Soft-square, card surface, --ring focus, and an aria-invalid destructive error state.
5
+ Styled native multi-line text control. Soft-square, the theme's field surface, --ring focus, and an aria-invalid destructive error state.
6
6
 
7
7
  **Keywords:** textarea, multi line input, form, text
8
8
 
@@ -31,7 +31,7 @@ Use Textarea for multi-line freeform text entry such as notes, descriptions, or
31
31
 
32
32
  ## Tokens used
33
33
 
34
- `input`, `card`, `foreground`, `muted-foreground`, `ring`, `destructive`
34
+ `field-border`, `field`, `foreground`, `muted-foreground`, `ring`, `destructive`
35
35
 
36
36
  ## Accessibility
37
37
 
@@ -30,6 +30,7 @@ Sable's components ship accessible behavior from Base UI. These rules cover what
30
30
  - **Never:** Color as the only signal (a red border with no message). **Instead:** Color plus text or an icon. Roughly 1 in 12 men cannot tell red from green.
31
31
  - **Never:** Opacity on the focus ring. **Instead:** ring-ring at full opacity; tune the token in the theme. A faded ring loses the contrast the theme tested. *Enforced by sable-lint: faded-focus-ring.*
32
32
  - **Never:** A brand or status fill retuned under its old label — dark ink on a bright red that passes 4.5:1 at APCA Lc 42. **Instead:** A label that clears 4.5:1 and APCA Lc 60 in both modes. Move the fill's lightness, not its hue, keeping the ink its colour takes. A status colour on a surface is its own token (`--destructive-text`), so the fill can move freely. The ratio alone passes labels that read muddy. A deliberate brand pair is allowed with a recorded reason (lint disable or preset exceptions). *Enforced by sable-lint: label-contrast.*
33
+ - **Never:** A label fill painted on a mark that carries no text — a meter, a segmented bar, a status dot, a chart series, a soft tint. **Instead:** The colour's `-mark` token (`bg-success-mark`, `bg-tone-warning-mark/15`): the same hue lifted and saturated, held to 2:1 on the page. Text contrast is for text; a graphic owes visibility, and 3:1 (WCAG 1.4.11) only when it is the sole carrier of its meaning. A fill is pinned dark enough for a white label. On a bar or dot with nothing to label, that darkness only reads as mud: a brown warning, a forest-green success.
33
34
  - **Never:** Blocking paste, or type="number" for codes and card numbers. **Instead:** type="text" inputMode="numeric" with the right autocomplete. Blocking paste breaks password managers; number inputs scroll-change identifiers.
34
35
  - **Never:** forced-color-adjust: none to 'fix' high-contrast mode. **Instead:** Let the system colors apply; verify the result. It removes the user's chosen colors exactly where they need them.
35
36
  - **Never:** outline: none without a replacement. **Instead:** focus-visible:ring-2 ring-ring. Keyboard users lose their place.