@sableui/react 0.15.0 → 0.16.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 (134) hide show
  1. package/CHANGELOG.md +73 -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/wallet-block.js +2 -2
  5. package/dist/components/app-shell/app-shell-frame.js +16 -0
  6. package/dist/components/app-shell/app-shell-main.js +12 -0
  7. package/dist/components/app-shell/app-shell.js +28 -4
  8. package/dist/components/app-shell/app-shell.meta.js +88 -4
  9. package/dist/components/area-chart/area-chart.meta.js +6 -1
  10. package/dist/components/attachment/attachment.meta.js +2 -1
  11. package/dist/components/attachment/attachment.variants.js +1 -1
  12. package/dist/components/badge/badge.meta.js +8 -1
  13. package/dist/components/badge/badge.variants.js +7 -7
  14. package/dist/components/banner/banner.meta.js +6 -1
  15. package/dist/components/banner/banner.variants.js +5 -5
  16. package/dist/components/bar-chart/bar-chart.meta.js +6 -1
  17. package/dist/components/caret-menu/caret-menu.js +5 -6
  18. package/dist/components/chat-composer/chat-composer.meta.js +2 -2
  19. package/dist/components/chat-composer/chat-composer.variants.js +1 -1
  20. package/dist/components/color-field/color-field.js +6 -8
  21. package/dist/components/combobox/combobox.js +27 -5
  22. package/dist/components/combobox/combobox.meta.js +10 -2
  23. package/dist/components/command-menu/command-menu.js +14 -3
  24. package/dist/components/command-menu/command-menu.meta.js +9 -1
  25. package/dist/components/date-picker/date-picker.meta.js +2 -2
  26. package/dist/components/feedback/feedback.js +1 -1
  27. package/dist/components/feedback/feedback.meta.js +2 -1
  28. package/dist/components/gradient-art/gradient-art.js +8 -5
  29. package/dist/components/gradient-art/gradient-art.meta.js +35 -2
  30. package/dist/components/input/input.meta.js +11 -3
  31. package/dist/components/input-group/input-group.js +1 -1
  32. package/dist/components/input-group/input-group.meta.js +1 -1
  33. package/dist/components/line-chart/line-chart.meta.js +6 -1
  34. package/dist/components/menu/menu.js +1 -1
  35. package/dist/components/meter/meter.js +53 -7
  36. package/dist/components/meter/meter.meta.js +51 -4
  37. package/dist/components/meter/meter.variants.js +43 -7
  38. package/dist/components/note/note.meta.js +5 -1
  39. package/dist/components/note/note.variants.js +4 -4
  40. package/dist/components/number-field/number-field.meta.js +2 -2
  41. package/dist/components/otp-field/otp-field.meta.js +2 -2
  42. package/dist/components/panel-group/panel-group.js +5 -8
  43. package/dist/components/pie-chart/pie-chart.meta.js +6 -1
  44. package/dist/components/radar-chart/radar-chart.meta.js +6 -1
  45. package/dist/components/radial-chart/radial-chart.meta.js +6 -1
  46. package/dist/components/search-input/search-input.meta.js +11 -3
  47. package/dist/components/segmented-bar/segmented-bar.meta.js +8 -1
  48. package/dist/components/segmented-bar/segmented-bar.variants.js +7 -7
  49. package/dist/components/select/select.js +1 -1
  50. package/dist/components/select/select.meta.js +2 -2
  51. package/dist/components/sheet/sheet.js +5 -4
  52. package/dist/components/sidebar/sidebar.js +16 -1
  53. package/dist/components/sidebar/sidebar.meta.js +7 -2
  54. package/dist/components/sparkline/sparkline.meta.js +8 -1
  55. package/dist/components/status-dot/status-dot.meta.js +8 -1
  56. package/dist/components/status-dot/status-dot.variants.js +7 -7
  57. package/dist/components/stepper/stepper.meta.js +2 -1
  58. package/dist/components/stepper/stepper.variants.js +1 -1
  59. package/dist/components/table/table.meta.js +4 -4
  60. package/dist/components/textarea/textarea.js +5 -0
  61. package/dist/components/textarea/textarea.meta.js +3 -3
  62. package/dist/file-hashes.json +91 -88
  63. package/dist/foundations/accessibility.js +5 -0
  64. package/dist/index.d.ts +132 -24
  65. package/dist/index.js +9 -1
  66. package/dist/lib/chart/types.js +8 -8
  67. package/dist/lib/chart/use-arrival.js +5 -4
  68. package/dist/lib/field-shell.js +1 -1
  69. package/dist/lib/highlight-glide.js +6 -6
  70. package/dist/lib/scrub-gesture.js +6 -6
  71. package/dist/lib/use-caret-trigger.js +11 -18
  72. package/dist/lib/use-match-highlight.js +69 -0
  73. package/dist/lib/use-smooth-wheel.js +10 -8
  74. package/dist/presets/index.d.ts +13 -2
  75. package/dist/presets/index.js +17 -1
  76. package/dist/styles.css +100 -0
  77. package/dist/{theme-provider-CdSicAad.d.ts → theme-provider-BydfM5Z0.d.ts} +1 -1
  78. package/dist/themes/_contract.js +14 -0
  79. package/dist/themes/atelier/atelier.css +18 -0
  80. package/dist/themes/atelier/index.d.ts +1 -1
  81. package/dist/themes/contrast.js +48 -1
  82. package/dist/themes/ember/ember.css +18 -0
  83. package/dist/themes/ember/index.d.ts +1 -1
  84. package/dist/themes/index.d.ts +2 -2
  85. package/dist/themes/meridian/index.d.ts +1 -1
  86. package/dist/themes/meridian/meridian.css +18 -0
  87. package/dist/themes/poppy/index.d.ts +1 -1
  88. package/dist/themes/poppy/poppy.css +18 -0
  89. package/dist/themes/prism/index.d.ts +1 -1
  90. package/dist/themes/prism/prism.css +18 -0
  91. package/dist/themes/sable/index.d.ts +1 -1
  92. package/dist/themes/sable/sable.css +18 -0
  93. package/dist/themes/verdant/index.d.ts +1 -1
  94. package/dist/themes/verdant/verdant.css +18 -0
  95. package/llms/blocks/wallet-block.md +5 -4
  96. package/llms/components/app-shell.md +72 -3
  97. package/llms/components/area-chart.md +1 -1
  98. package/llms/components/attachment.md +1 -1
  99. package/llms/components/badge.md +1 -1
  100. package/llms/components/banner.md +1 -1
  101. package/llms/components/bar-chart.md +1 -1
  102. package/llms/components/chat-composer.md +1 -1
  103. package/llms/components/combobox.md +2 -1
  104. package/llms/components/command-menu.md +3 -2
  105. package/llms/components/date-picker.md +1 -1
  106. package/llms/components/feedback.md +1 -1
  107. package/llms/components/gradient-art.md +28 -1
  108. package/llms/components/input-group.md +1 -1
  109. package/llms/components/input.md +4 -2
  110. package/llms/components/line-chart.md +1 -1
  111. package/llms/components/meter.md +33 -4
  112. package/llms/components/note.md +1 -1
  113. package/llms/components/number-field.md +1 -1
  114. package/llms/components/otp-field.md +1 -1
  115. package/llms/components/pie-chart.md +1 -1
  116. package/llms/components/radar-chart.md +1 -1
  117. package/llms/components/radial-chart.md +1 -1
  118. package/llms/components/search-input.md +20 -2
  119. package/llms/components/segmented-bar.md +1 -1
  120. package/llms/components/select.md +1 -1
  121. package/llms/components/sidebar.md +3 -2
  122. package/llms/components/sparkline.md +1 -1
  123. package/llms/components/status-dot.md +1 -1
  124. package/llms/components/stepper.md +1 -1
  125. package/llms/components/table.md +2 -2
  126. package/llms/components/textarea.md +2 -2
  127. package/llms/foundations/accessibility.md +1 -0
  128. package/llms/reference/token-intent.md +22 -2
  129. package/llms/reference/utilities.md +5 -2
  130. package/llms-full.txt +222 -49
  131. package/llms.txt +2 -2
  132. package/package.json +1 -1
  133. package/registry.json +396 -57
  134. package/scripts/lint-rules.mjs +6 -3
@@ -1,4 +1,4 @@
1
- import { T as ThemeManifest } from '../../_contract-CkqXDVww.js';
1
+ import { T as ThemeManifest } from '../../_contract-CDa9-NdB.js';
2
2
 
3
3
  /**
4
4
  * Sable — the default theme. Concrete token values live in
@@ -280,6 +280,15 @@
280
280
  --tone-2-foreground: var(--zinc-50);
281
281
  --tone-1-foreground: var(--zinc-50);
282
282
  --tone-warning-foreground: var(--zinc-50);
283
+ /* Graphic strength (MARK_TOKENS): meters, bars, dots, chart series and soft tints.
284
+ The same hues lifted and saturated; held to 2:1 on the page, not a text ratio. */
285
+ --theme-destructive-mark: oklch(62.0% 0.244 22.0);
286
+ --theme-success-mark: oklch(70.0% 0.131 173.0);
287
+ --theme-tone-warning-mark: oklch(71.0% 0.169 52.0);
288
+ --theme-tone-info-mark: oklch(63.0% 0.178 250.0);
289
+ --theme-tone-1-mark: oklch(62.0% 0.269 309.0);
290
+ --theme-tone-2-mark: oklch(60.0% 0.227 285.0);
291
+ --theme-tone-3-mark: oklch(70.0% 0.131 173.0);
283
292
  --tone-info-foreground: var(--zinc-50);
284
293
  --highlight-foreground: var(--zinc-50);
285
294
 
@@ -463,6 +472,15 @@
463
472
  --tone-2-foreground: var(--zinc-950);
464
473
  --tone-1-foreground: var(--zinc-950);
465
474
  --tone-warning-foreground: var(--zinc-950);
475
+ /* Graphic strength (MARK_TOKENS): meters, bars, dots, chart series and soft tints.
476
+ The same hues lifted and saturated; held to 2:1 on the page, not a text ratio. */
477
+ --theme-destructive-mark: oklch(65.0% 0.231 25.0);
478
+ --theme-success-mark: oklch(78.0% 0.157 168.0);
479
+ --theme-tone-warning-mark: oklch(82.0% 0.132 65.0);
480
+ --theme-tone-info-mark: oklch(76.0% 0.144 235.0);
481
+ --theme-tone-1-mark: oklch(62.0% 0.295 318.0);
482
+ --theme-tone-2-mark: oklch(72.0% 0.151 285.0);
483
+ --theme-tone-3-mark: oklch(78.0% 0.157 168.0);
466
484
  --tone-info-foreground: var(--zinc-950);
467
485
  --highlight-foreground: var(--zinc-50);
468
486
  --overlay: var(--zinc-950);
@@ -1,4 +1,4 @@
1
- import { T as ThemeManifest } from '../../_contract-CkqXDVww.js';
1
+ import { T as ThemeManifest } from '../../_contract-CDa9-NdB.js';
2
2
 
3
3
  /**
4
4
  * Verdant — a Sable theme. Concrete token values live in
@@ -128,6 +128,15 @@
128
128
  --tone-warning: oklch(53% 0.12 65);
129
129
  --tone-warning-text: oklch(51% 0.12 65); /* a step under the fill: 53% is 4.44:1 as text on muted */
130
130
  --tone-warning-foreground: oklch(98.6% 0.004 106);
131
+ /* Graphic strength (MARK_TOKENS): meters, bars, dots, chart series and soft tints.
132
+ The same hues lifted and saturated; held to 2:1 on the page, not a text ratio. */
133
+ --theme-destructive-mark: oklch(62.0% 0.244 22.0);
134
+ --theme-success-mark: oklch(70.0% 0.131 173.0);
135
+ --theme-tone-warning-mark: oklch(71.0% 0.156 65.0);
136
+ --theme-tone-info-mark: oklch(63.0% 0.156 250.0);
137
+ --theme-tone-1-mark: oklch(70.0% 0.131 173.0);
138
+ --theme-tone-2-mark: oklch(70.0% 0.091 175.0);
139
+ --theme-tone-3-mark: oklch(60.0% 0.018 175.0);
131
140
  --highlight: oklch(90% 0.05 100);
132
141
  --highlight-foreground: oklch(24% 0.035 175);
133
142
  --overlay: oklch(16% 0.03 175 / 0.6);
@@ -209,6 +218,15 @@
209
218
  --tone-warning: oklch(80% 0.13 75);
210
219
  --tone-warning-text: oklch(80% 0.13 75);
211
220
  --tone-warning-foreground: oklch(16% 0.03 175);
221
+ /* Graphic strength (MARK_TOKENS): meters, bars, dots, chart series and soft tints.
222
+ The same hues lifted and saturated; held to 2:1 on the page, not a text ratio. */
223
+ --theme-destructive-mark: oklch(65.0% 0.218 22.0);
224
+ --theme-success-mark: oklch(74.0% 0.139 173.0);
225
+ --theme-tone-warning-mark: oklch(80.0% 0.149 75.0);
226
+ --theme-tone-info-mark: oklch(76.0% 0.125 250.0);
227
+ --theme-tone-1-mark: oklch(76.0% 0.137 173.0);
228
+ --theme-tone-2-mark: oklch(74.0% 0.103 175.0);
229
+ --theme-tone-3-mark: oklch(72.0% 0.015 175.0);
212
230
  --highlight: oklch(40% 0.06 90);
213
231
  --highlight-foreground: oklch(98.6% 0.004 106);
214
232
  --overlay: oklch(8% 0.015 175 / 0.7);
@@ -28,7 +28,10 @@ const activity = [
28
28
  { title: "Subscription", detail: "Team plan · 2h ago", amount: "+49.00", tone: "1" },
29
29
  { title: "Refund", detail: "Order 1042 · Yesterday", amount: "-24.00", tone: "2" },
30
30
  { title: "Payout", detail: "To checking · Mon", amount: "-850.00", tone: "3" },
31
- ]
31
+ ] as const
32
+
33
+ /** Each row's mark: its tone at graphic strength, which every theme tunes. */
34
+ const TONE_MARK = { "1": "bg-tone-1-mark", "2": "bg-tone-2-mark", "3": "bg-tone-3-mark" } as const
32
35
 
33
36
  export interface WalletBlockProps {
34
37
  className?: string
@@ -62,9 +65,7 @@ export function WalletBlock({ className, framed = true, onSend, onReceive }: Wal
62
65
  leading={
63
66
  <span
64
67
  aria-hidden
65
- className="block size-8 rounded-full"
66
- // design-lint-disable-next-line inline-color -- relative colour derived from the theme's own --tone token, so it follows every look
67
- style={{ background: `oklch(from var(--tone-${item.tone}) clamp(0.5, calc(l + 0.16), 0.9) max(calc(c * 1.25), 0.1) h)` }}
68
+ className={cn("block size-8 rounded-full", TONE_MARK[item.tone])}
68
69
  />
69
70
  }
70
71
  title={item.title}
@@ -4,7 +4,7 @@
4
4
 
5
5
  The application frame: slots for status banners, a mobile header, a desktop sidebar, main content, a mobile bottom nav, a persistent dock (a now-playing or call bar), and floating overlays — plus a built-in skip link and #main-content target.
6
6
 
7
- **Keywords:** app shell, layout, frame, sidebar, header, bottom nav, mobile, banners, main, dock, player bar, now playing, persistent
7
+ **Keywords:** app shell, layout, frame, sidebar, header, bottom nav, mobile, banners, main, dock, player bar, now playing, persistent, inset, inset frame, floating panel, rounded content, scroll container
8
8
 
9
9
  ## Usage
10
10
 
@@ -16,7 +16,10 @@ Use AppShell as the outer product frame for authenticated or app-like experience
16
16
  - Do: Pass app-wide outage or status messages through banners so they remain above the frame.
17
17
  - Do: Use Region inside the main content to choose each route's reading width.
18
18
  - Do: Wrap EVERY return of a route in its own Page or Region — including early returns for loading, empty and error states. AppShell's <main> deliberately carries no padding (it is a frame, and padding belongs to the region that chose the width), so a route that returns a bare spinner or error renders flush against the chrome. This is the most common conversion bug: the happy path gets a Page, the three early returns do not.
19
+ - Do: Use frame="inset" for dense, multi-pane product apps — a CRM, an issue tracker, an AI workspace — where framing the content as one raised panel on the rail's ground helps it read as the workspace and the rail as chrome. Keep the Sidebar on surface="rail" so its tint is the ground. Inset moves the page scroll from the window to <main id="main-content"> on desktop: point scroll listeners, scroll restoration and scroll-to-top at that element, not window.
19
20
  - Don't: Nest AppShell inside route cards or panels. It is page chrome, not local content.
21
+ - Don't: Wrap a route in its own h-full overflow-y-auto box inside an inset shell. main is already the page's scroll container; a second one traps the scrollbar inside a smaller box and breaks sticky headers and the skip link's scroll target. Scroll a genuinely separate pane (a list beside a detail view) with Scroller. → use **Scroller**
22
+ - Don't: Combine frame="inset" with a full-width header bar above the rail. The inset ground is the rail running the full height of the window; a bar across the top cuts it off. Use the flush frame with Sidebar surface="page" for that layout.
20
23
  - Don't: Put desktop-only navigation in the mobile header slot or mobile-only controls in the sidebar slot.
21
24
 
22
25
  ### Anatomy
@@ -26,7 +29,7 @@ Use AppShell as the outer product frame for authenticated or app-like experience
26
29
  | Banners | no | Full-width app status messages rendered above the shell. |
27
30
  | Header | no | Mobile top chrome, usually AppHeader. |
28
31
  | Sidebar | no | Desktop navigation chrome, usually Sidebar. |
29
- | Main | yes | The route content landmark with id main-content. |
32
+ | Main | yes | The route content landmark with id main-content. With frame="inset", from lg it is a rounded, bordered panel that scrolls inside itself. |
30
33
  | Bottom nav | no | Mobile fixed navigation with reserved bottom space. |
31
34
  | Dock | no | Persistent bottom chrome that outlives routes — a now-playing bar, a call bar. Sticky above the bottom nav; the desktop Sidebar shortens to clear it. |
32
35
 
@@ -46,10 +49,11 @@ Use AppShell as the outer product frame for authenticated or app-like experience
46
49
  | bottomNav | ReactNode | — | Mobile bottom navigation (e.g. BottomNav); fixed to the bottom edge. main reserves space for it. |
47
50
  | dock | ReactNode | — | Persistent chrome kept on screen across routes (a now-playing bar, an active-call bar, an upload tray). Full width, sticky to the bottom edge and above bottomNav on mobile; its height is published as --dock-height and the app Sidebar subtracts it. In flow, so main needs no reserved space. The contents paint their own surface and top border, and name themselves (e.g. <section aria-label="Player">). Mount AppShell in a layout that survives navigation, or the playback restarts. |
48
51
  | overlays | ReactNode | — | Floating overlays — a message launcher, FAB, or modal mount points. |
52
+ | frame | "flush" \| "inset" | "flush" | How the desktop frame (lg and up) holds the content; below lg both are the same mobile layout with page scroll. "flush": main sits on the page beside the rail and the document scrolls. "inset": the shell fills the viewport and paints the shell-ground token (one step off the rail's surface, tunable with --theme-shell-ground) behind the rail and around the panel; main becomes a rounded-lg panel with a hairline border and shadow-sm, inset by a spacing step from the window edges, that scrolls inside itself (scrollbar-quiet) while the rail stays put. The Sidebar in the sidebar slot drops its right border and fills the row. main stays the #main-content target (tabIndex -1, so the skip link lands focus in the scroller) and the inline-size query container; sticky children stick to the panel's top; banners and the dock stay in flow, so the panel's bottom gap sits above a dock. With Sidebar surface="page" the ground is the page colour and the panel's border alone lifts it. |
49
53
 
50
54
  ## Tokens used
51
55
 
52
- `background`, `foreground`, `primary`, `primary-foreground`
56
+ `background`, `foreground`, `primary`, `primary-foreground`, `sidebar`, `shell-ground`, `border`
53
57
 
54
58
  ## Accessibility
55
59
 
@@ -57,6 +61,7 @@ Use AppShell as the outer product frame for authenticated or app-like experience
57
61
  - Wraps content in a <main id="main-content"> landmark.
58
62
  - Slot components own their own ARIA (nav landmarks, labels).
59
63
  - The dock follows <main> in DOM order, so keyboard and screen-reader users reach the player after the page content, not before it.
64
+ - With frame="inset", <main> is the desktop scroll container and takes tabIndex -1: the skip link moves focus into it, so PageDown and the arrow keys scroll the content, without adding a tab stop.
60
65
 
61
66
  ## Related
62
67
 
@@ -96,6 +101,70 @@ import { AppHeader, AppShell, Banner, BottomNav, BottomNavItem, Icon, PageHeader
96
101
  </AppShell>
97
102
  ```
98
103
 
104
+ ### Inset frame
105
+
106
+ ```tsx
107
+ import { AppHeader, AppShell, BottomNav, BottomNavItem, Button, Card, CardContent, Icon, Page, PageHeader, Sidebar, SidebarBrand, SidebarHeader, SidebarItem, SidebarNav, Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@sableui/react"
108
+ // <Link> = your router's link (react-router, next/link, @tanstack/router, …)
109
+
110
+ <AppShell
111
+ frame="inset"
112
+ header={<AppHeader title="Pipeline" />}
113
+ sidebar={
114
+ <Sidebar>
115
+ <SidebarHeader>
116
+ <SidebarBrand mark={<Icon name="gem" variant="solid" />} label="Relay" render={<Link to="/" />} />
117
+ </SidebarHeader>
118
+ <SidebarNav>
119
+ <SidebarItem icon={<Icon name="inbox" />} label="Inbox" render={<Link to="/inbox" />} />
120
+ <SidebarItem icon={<Icon name="user" />} label="People" active render={<Link to="/people" />} />
121
+ <SidebarItem icon={<Icon name="folder" />} label="Companies" render={<Link to="/companies" />} />
122
+ </SidebarNav>
123
+ </Sidebar>
124
+ }
125
+ bottomNav={
126
+ <BottomNav>
127
+ <BottomNavItem icon={<Icon name="inbox" />} label="Inbox" render={<Link to="/inbox" />} />
128
+ <BottomNavItem icon={<Icon name="user" />} label="People" active render={<Link to="/people" />} />
129
+ <BottomNavItem icon={<Icon name="folder" />} label="Companies" render={<Link to="/companies" />} />
130
+ </BottomNav>
131
+ }
132
+ >
133
+ <Page max="wide" gap={4} className="py-6">
134
+ <PageHeader
135
+ title="People"
136
+ description="Everyone in your pipeline, newest first."
137
+ actions={<Button><Icon name="plus" />Add person</Button>}
138
+ />
139
+ <Card>
140
+ <CardContent flush>
141
+ <Table>
142
+ <TableHeader>
143
+ <TableRow>
144
+ <TableHead>Name</TableHead>
145
+ <TableHead>Company</TableHead>
146
+ <TableHead numeric>Deals</TableHead>
147
+ </TableRow>
148
+ </TableHeader>
149
+ <TableBody>
150
+ <TableRow>
151
+ <TableCell>Ada Lovelace</TableCell>
152
+ <TableCell>Northwind</TableCell>
153
+ <TableCell numeric>4</TableCell>
154
+ </TableRow>
155
+ <TableRow>
156
+ <TableCell>Grace Hopper</TableCell>
157
+ <TableCell>Harbor Labs</TableCell>
158
+ <TableCell numeric>2</TableCell>
159
+ </TableRow>
160
+ </TableBody>
161
+ </Table>
162
+ </CardContent>
163
+ </Card>
164
+ </Page>
165
+ </AppShell>
166
+ ```
167
+
99
168
  ### SkipLink in a layout of your own
100
169
 
101
170
  ```tsx
@@ -45,7 +45,7 @@ Use AreaChart for volume over time — visitors, revenue, usage — where the fi
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
 
@@ -52,7 +52,7 @@ The card a file takes in a conversation — in the composer while it uploads, an
52
52
 
53
53
  ## Tokens used
54
54
 
55
- `card`, `border`, `border-strong`, `muted`, `muted-foreground`, `foreground`, `destructive`, `overlay`, `overlay-foreground`, `ring`
55
+ `card`, `border`, `border-strong`, `muted`, `muted-foreground`, `foreground`, `destructive`, `overlay`, `overlay-foreground`, `ring`, `destructive-mark`
56
56
 
57
57
  ## Motion
58
58
 
@@ -46,7 +46,7 @@ Use Badge for any short static label: metadata, categories, tone-coded labels, a
46
46
 
47
47
  ## Tokens used
48
48
 
49
- `palette-wash`, `palette-wash-strong`, `palette-edge`, `palette-dot`, `palette-solid`, `palette-text`, `palette-solid-ink`, `secondary`, `secondary-foreground`, `border`, `foreground`, `background`, `muted`, `muted-foreground`, `tone-1`, `tone-2`, `tone-3`, `tone-info`, `success`, `tone-warning`, `destructive`, `overlay`, `overlay-foreground`, `tone-1-foreground`, `tone-2-foreground`, `tone-3-foreground`, `tone-info-foreground`, `success-foreground`, `tone-warning-foreground`, `destructive-foreground`
49
+ `palette-wash`, `palette-wash-strong`, `palette-edge`, `palette-dot`, `palette-solid`, `palette-text`, `palette-solid-ink`, `secondary`, `secondary-foreground`, `border`, `foreground`, `background`, `muted`, `muted-foreground`, `tone-1`, `tone-2`, `tone-3`, `tone-info`, `success`, `tone-warning`, `destructive`, `overlay`, `overlay-foreground`, `tone-1-foreground`, `tone-2-foreground`, `tone-3-foreground`, `tone-info-foreground`, `success-foreground`, `tone-warning-foreground`, `destructive-foreground`, `destructive-mark`, `success-mark`, `tone-warning-mark`, `tone-info-mark`, `tone-1-mark`, `tone-2-mark`, `tone-3-mark`
50
50
 
51
51
  ## Accessibility
52
52
 
@@ -47,7 +47,7 @@ Use Banner for page-level or app-level messages that affect the current experien
47
47
 
48
48
  ## Tokens used
49
49
 
50
- `secondary`, `secondary-foreground`, `foreground`, `background`, `tone-info`, `tone-info-foreground`, `success`, `success-foreground`, `tone-warning`, `tone-warning-foreground`, `tone-1`, `tone-1-foreground`, `destructive`, `destructive-foreground`
50
+ `secondary`, `secondary-foreground`, `foreground`, `background`, `tone-info`, `tone-info-foreground`, `success`, `success-foreground`, `tone-warning`, `tone-warning-foreground`, `tone-1`, `tone-1-foreground`, `destructive`, `destructive-foreground`, `destructive-mark`, `success-mark`, `tone-warning-mark`, `tone-info-mark`, `tone-1-mark`
51
51
 
52
52
  ## Accessibility
53
53
 
@@ -49,7 +49,7 @@ Use BarChart to compare amounts across categories — the default chart for "how
49
49
 
50
50
  ## Tokens used
51
51
 
52
- `tone-1`, `tone-2`, `tone-3`, `tone-info`, `tone-warning`, `primary`, `muted-foreground`, `border`, `popover`, `popover-foreground`, `foreground`, `background`
52
+ `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`
53
53
 
54
54
  ## Motion
55
55
 
@@ -67,7 +67,7 @@ Pairs with MessageScroller to make a working chat. A single surface rather than
67
67
 
68
68
  ## Tokens used
69
69
 
70
- `card`, `input`, `ring`, `destructive`, `foreground`, `muted-foreground`, `primary`, `primary-foreground`, `secondary`, `secondary-foreground`, `accent`, `accent-foreground`
70
+ `field`, `field-border`, `ring`, `destructive`, `foreground`, `muted-foreground`, `primary`, `primary-foreground`, `secondary`, `secondary-foreground`, `accent`, `accent-foreground`
71
71
 
72
72
  ## Motion
73
73
 
@@ -56,13 +56,14 @@ Use Combobox when users choose from a list that benefits from typing, filtering,
56
56
  | items | readonly any[] \| readonly ComboboxItemGroup[] | — | The pool of options, flat or grouped ({ value, items }). Drives type-to-filter and is required for ComboboxEmpty to render its fallback. |
57
57
  | multiple | boolean | false | Select several items instead of one. `value` / `defaultValue` / `onValueChange` become arrays, choosing an item toggles its membership, and the popup stays open between picks. Pair it with ComboboxChips in place of ComboboxInput; every other part is unchanged. |
58
58
  | filter | null \| ((item, query, itemToString?) => boolean) | Base UI's locale-aware contains | How items match the query. `null` turns matching off, for items a server already filtered: pair it with `onInputValueChange` to fetch. |
59
+ | highlightMatches | boolean | true | Paints the typed text wherever it appears in a row's label, on the `highlight` / `highlight-foreground` pair, through the CSS Custom Highlight API — no `<mark>` in the row, and the accessible name is unchanged. Only typed text counts: a field still holding the selected item's label lists everything and lights nothing. A literal, case-insensitive match within the label; a `filter` that matches differently leaves rows it kept unpainted. `false` turns it off. |
59
60
  | onInputValueChange | (inputValue: string, eventDetails) => void | — | Fires as the reader types. The place to fetch results for a server-backed combobox. |
60
61
  | itemToStringLabel | (item) => string | item.label for { value, label } items | The text an object item shows in the input once chosen, and is matched by. Needed for object items without a `label` field. |
61
62
  | virtualized | boolean | false | Tells Base UI the DOM holds only part of the list. Required when using ComboboxVirtualList. |
62
63
 
63
64
  ## Tokens used
64
65
 
65
- `input`, `card`, `foreground`, `muted-foreground`, `ring`, `destructive`, `popover`, `popover-foreground`, `border`, `accent`, `accent-foreground`, `primary`, `secondary`, `secondary-foreground`
66
+ `highlight`, `highlight-foreground`, `field-border`, `field`, `foreground`, `muted-foreground`, `ring`, `destructive`, `popover`, `popover-foreground`, `border`, `accent`, `accent-foreground`, `primary`, `secondary`, `secondary-foreground`
66
67
 
67
68
  ## Motion
68
69
 
@@ -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