@sableui/react 0.14.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 (163) hide show
  1. package/CHANGELOG.md +131 -0
  2. package/DESIGN.md +3 -1
  3. package/bin/sable-lint.mjs +5 -1
  4. package/dist/{_contract-CkqXDVww.d.ts → _contract-CDa9-NdB.d.ts} +47 -1
  5. package/dist/blocks/index.d.ts +12 -1
  6. package/dist/blocks/index.js +4 -0
  7. package/dist/blocks/message-reactions-block.js +87 -0
  8. package/dist/blocks/message-reactions-block.meta.js +23 -0
  9. package/dist/blocks/wallet-block.js +2 -2
  10. package/dist/components/app-shell/app-shell-frame.js +16 -0
  11. package/dist/components/app-shell/app-shell-main.js +12 -0
  12. package/dist/components/app-shell/app-shell.js +28 -4
  13. package/dist/components/app-shell/app-shell.meta.js +88 -4
  14. package/dist/components/area-chart/area-chart.meta.js +6 -1
  15. package/dist/components/attachment/attachment.meta.js +2 -1
  16. package/dist/components/attachment/attachment.variants.js +1 -1
  17. package/dist/components/badge/badge.meta.js +8 -1
  18. package/dist/components/badge/badge.variants.js +7 -7
  19. package/dist/components/banner/banner.meta.js +6 -1
  20. package/dist/components/banner/banner.variants.js +5 -5
  21. package/dist/components/bar-chart/bar-chart.meta.js +6 -1
  22. package/dist/components/bubble/bubble.js +105 -5
  23. package/dist/components/bubble/bubble.meta.js +42 -6
  24. package/dist/components/bubble/bubble.variants.js +1 -1
  25. package/dist/components/bubble/index.js +2 -0
  26. package/dist/components/caret-menu/caret-menu.js +5 -6
  27. package/dist/components/chat-composer/chat-composer.meta.js +2 -2
  28. package/dist/components/chat-composer/chat-composer.variants.js +1 -1
  29. package/dist/components/color-field/color-field.js +6 -8
  30. package/dist/components/combobox/combobox.js +27 -5
  31. package/dist/components/combobox/combobox.meta.js +10 -2
  32. package/dist/components/command-menu/command-menu.js +14 -3
  33. package/dist/components/command-menu/command-menu.meta.js +9 -1
  34. package/dist/components/date-picker/date-picker.meta.js +2 -2
  35. package/dist/components/feedback/feedback.js +9 -2
  36. package/dist/components/feedback/feedback.meta.js +8 -1
  37. package/dist/components/gradient-art/gradient-art.js +8 -5
  38. package/dist/components/gradient-art/gradient-art.meta.js +35 -2
  39. package/dist/components/input/input.meta.js +11 -3
  40. package/dist/components/input-group/input-group.js +1 -1
  41. package/dist/components/input-group/input-group.meta.js +1 -1
  42. package/dist/components/like-button/index.js +7 -0
  43. package/dist/components/like-button/like-button.js +89 -0
  44. package/dist/components/like-button/like-button.meta.js +132 -0
  45. package/dist/components/line-chart/line-chart.meta.js +6 -1
  46. package/dist/components/menu/menu.js +1 -1
  47. package/dist/components/meter/meter.js +53 -7
  48. package/dist/components/meter/meter.meta.js +51 -4
  49. package/dist/components/meter/meter.variants.js +43 -7
  50. package/dist/components/note/note.meta.js +5 -1
  51. package/dist/components/note/note.variants.js +4 -4
  52. package/dist/components/number-field/number-field.meta.js +2 -2
  53. package/dist/components/otp-field/otp-field.meta.js +2 -2
  54. package/dist/components/panel-group/panel-group.js +5 -8
  55. package/dist/components/pie-chart/pie-chart.meta.js +6 -1
  56. package/dist/components/radar-chart/radar-chart.meta.js +6 -1
  57. package/dist/components/radial-chart/radial-chart.meta.js +6 -1
  58. package/dist/components/reaction-picker/index.js +13 -0
  59. package/dist/components/reaction-picker/reaction-picker.js +104 -0
  60. package/dist/components/reaction-picker/reaction-picker.meta.js +156 -0
  61. package/dist/components/search-input/search-input.meta.js +11 -3
  62. package/dist/components/segmented-bar/segmented-bar.meta.js +8 -1
  63. package/dist/components/segmented-bar/segmented-bar.variants.js +7 -7
  64. package/dist/components/select/select.js +1 -1
  65. package/dist/components/select/select.meta.js +2 -2
  66. package/dist/components/sheet/sheet.js +5 -4
  67. package/dist/components/sidebar/sidebar.js +16 -1
  68. package/dist/components/sidebar/sidebar.meta.js +7 -2
  69. package/dist/components/sparkline/sparkline.meta.js +8 -1
  70. package/dist/components/status-dot/status-dot.meta.js +8 -1
  71. package/dist/components/status-dot/status-dot.variants.js +7 -7
  72. package/dist/components/stepper/stepper.meta.js +2 -1
  73. package/dist/components/stepper/stepper.variants.js +1 -1
  74. package/dist/components/table/table.meta.js +4 -4
  75. package/dist/components/textarea/textarea.js +5 -0
  76. package/dist/components/textarea/textarea.meta.js +3 -3
  77. package/dist/components/toast/toast.js +25 -13
  78. package/dist/components/toast/toast.meta.js +7 -1
  79. package/dist/file-hashes.json +112 -100
  80. package/dist/foundations/accessibility.js +5 -0
  81. package/dist/foundations/motion.js +21 -1
  82. package/dist/icon-sets/reicon.js +11 -1
  83. package/dist/index.d.ts +425 -84
  84. package/dist/index.js +11 -1
  85. package/dist/lib/chart/types.js +8 -8
  86. package/dist/lib/chart/use-arrival.js +5 -4
  87. package/dist/lib/field-shell.js +1 -1
  88. package/dist/lib/highlight-glide.js +6 -6
  89. package/dist/lib/scrub-gesture.js +6 -6
  90. package/dist/lib/taxonomy.js +2 -0
  91. package/dist/lib/use-caret-trigger.js +11 -18
  92. package/dist/lib/use-match-highlight.js +69 -0
  93. package/dist/lib/use-smooth-wheel.js +10 -8
  94. package/dist/motion/Burst.js +79 -0
  95. package/dist/motion/index.js +2 -0
  96. package/dist/presets/index.d.ts +13 -2
  97. package/dist/presets/index.js +17 -1
  98. package/dist/styles.css +241 -0
  99. package/dist/{theme-provider-CdSicAad.d.ts → theme-provider-BydfM5Z0.d.ts} +1 -1
  100. package/dist/themes/_contract.js +14 -0
  101. package/dist/themes/atelier/atelier.css +18 -0
  102. package/dist/themes/atelier/index.d.ts +1 -1
  103. package/dist/themes/contrast.js +48 -1
  104. package/dist/themes/ember/ember.css +18 -0
  105. package/dist/themes/ember/index.d.ts +1 -1
  106. package/dist/themes/index.d.ts +2 -2
  107. package/dist/themes/meridian/index.d.ts +1 -1
  108. package/dist/themes/meridian/meridian.css +18 -0
  109. package/dist/themes/poppy/index.d.ts +1 -1
  110. package/dist/themes/poppy/poppy.css +18 -0
  111. package/dist/themes/prism/index.d.ts +1 -1
  112. package/dist/themes/prism/prism.css +18 -0
  113. package/dist/themes/sable/index.d.ts +1 -1
  114. package/dist/themes/sable/sable.css +18 -0
  115. package/dist/themes/verdant/index.d.ts +1 -1
  116. package/dist/themes/verdant/verdant.css +18 -0
  117. package/llms/blocks/message-reactions-block.md +116 -0
  118. package/llms/blocks/wallet-block.md +5 -4
  119. package/llms/components/app-shell.md +72 -3
  120. package/llms/components/area-chart.md +1 -1
  121. package/llms/components/attachment.md +1 -1
  122. package/llms/components/badge.md +1 -1
  123. package/llms/components/banner.md +1 -1
  124. package/llms/components/bar-chart.md +1 -1
  125. package/llms/components/bubble.md +17 -7
  126. package/llms/components/chat-composer.md +1 -1
  127. package/llms/components/combobox.md +2 -1
  128. package/llms/components/command-menu.md +3 -2
  129. package/llms/components/date-picker.md +1 -1
  130. package/llms/components/feedback.md +2 -1
  131. package/llms/components/gradient-art.md +28 -1
  132. package/llms/components/input-group.md +1 -1
  133. package/llms/components/input.md +4 -2
  134. package/llms/components/like-button.md +91 -0
  135. package/llms/components/line-chart.md +1 -1
  136. package/llms/components/meter.md +33 -4
  137. package/llms/components/note.md +1 -1
  138. package/llms/components/number-field.md +1 -1
  139. package/llms/components/otp-field.md +1 -1
  140. package/llms/components/pie-chart.md +1 -1
  141. package/llms/components/radar-chart.md +1 -1
  142. package/llms/components/radial-chart.md +1 -1
  143. package/llms/components/reaction-picker.md +115 -0
  144. package/llms/components/search-input.md +20 -2
  145. package/llms/components/segmented-bar.md +1 -1
  146. package/llms/components/select.md +1 -1
  147. package/llms/components/sidebar.md +3 -2
  148. package/llms/components/sparkline.md +1 -1
  149. package/llms/components/status-dot.md +1 -1
  150. package/llms/components/stepper.md +1 -1
  151. package/llms/components/table.md +2 -2
  152. package/llms/components/textarea.md +2 -2
  153. package/llms/components/toast.md +2 -1
  154. package/llms/foundations/accessibility.md +1 -0
  155. package/llms/foundations/motion.md +18 -2
  156. package/llms/reference/token-intent.md +22 -2
  157. package/llms/reference/utilities.md +9 -3
  158. package/llms-full.txt +596 -61
  159. package/llms.txt +10 -7
  160. package/package.json +1 -1
  161. package/registry.json +968 -68
  162. package/scripts/lint-rules.mjs +6 -3
  163. package/targets/ios.json +18 -0
package/llms-full.txt CHANGED
@@ -1,6 +1,6 @@
1
1
  # Sable (@sableui/react)
2
2
 
3
- > An owned, themeable React component system. Default theme: **Sable** (dark-by-default, content-first, fluid product UI). Behavior from Base UI; styling via Tailwind v4 + semantic CSS-variable tokens; Reicon icons via a pluggable adapter. 119 components, 13 layout primitives, and a **patterns** tier of canonical page shells. The full, per-component contract is the generated `registry.json`.
3
+ > An owned, themeable React component system. Default theme: **Sable** (dark-by-default, content-first, fluid product UI). Behavior from Base UI; styling via Tailwind v4 + semantic CSS-variable tokens; Reicon icons via a pluggable adapter. 121 components, 13 layout primitives, and a **patterns** tier of canonical page shells. The full, per-component contract is the generated `registry.json`.
4
4
 
5
5
  ## Start here — brief, composition, evidence
6
6
 
@@ -3882,6 +3882,8 @@ Reach for one named class before writing a transition.
3882
3882
  | `motion-appear` | fast fade + base scale | A small control mounting in place: a clear button, a send arrow, a badge. @starting-style, no JS. |
3883
3883
  | `motion-arrive` | base · decelerate · staggered · expressive | Items a user action brings into a list: a folder's children on expand, a row just added. Never on every render. |
3884
3884
  | `motion-size` | base · emphasized · expressive | An element easing between intrinsic sizes when its label changes: a two-step confirm, Copy → Copied. |
3885
+ | `motion-bloom` | slower · 0 → 1.2 → 1 · expressive | A small glyph landing in its on state: a liked heart, a picked rating, a toast's status icon. Remount it (key) to replay; never on the resting state. |
3886
+ | `<Burst>` | slower ring + thrown dots · expressive | The follow-through behind a bloom, in the theme's palette families. Only on a choice a person made: a like, a star, a finished task. |
3885
3887
  | `motion-draw` | slower · decelerate | SVG line art drawing on when a <Reveal> scrolls in. The path needs pathLength="1"; stagger siblings with --motion-draw-delay. |
3886
3888
  | `motion-spin · pulse · shimmer · dot-bounce · ping` | loops | Activity. Scale-aware, so reduced motion freezes them. |
3887
3889
 
@@ -3902,7 +3904,7 @@ Essential motion answers the user; expressive motion narrates continuity. Expres
3902
3904
  | Name | Value | Use |
3903
3905
  |---|---|---|
3904
3906
  | `essential` | always, unless off | Colour and focus feedback, press, overlay arrival, disclosure height, loaders, chart hover. |
3905
- | `expressive` | full only | Glides, rolling numbers (NumberTicker), drawn ticks, shakes, staggers, chart arrival and morph, scroll reveals, spring overshoot. |
3907
+ | `expressive` | full only | Glides, rolling numbers (NumberTicker), drawn ticks, blooms and bursts, shakes, staggers, chart arrival and morph, scroll reveals, spring overshoot. |
3906
3908
  | `full` | default | Both levels play. The crafted default. |
3907
3909
  | `subtle` | data-motion="subtle" | Essential only; nothing overshoots. For dense tools, embedded widgets, readers who want calm without stillness. |
3908
3910
  | `off` | data-motion="off" · reduce-motion | Nothing moves; every state still changes. |
@@ -3931,7 +3933,10 @@ Every interactive component answers all four, by filling the slot or declining i
3931
3933
  8. **One thing travels; two things do not blink. When a selection, a value or a mark changes in place, move the one element that represents it.** A fill switching off here and on there is two events the eye has to connect. A glide, a roll or a draw is one, and it is most of what makes an interface feel made rather than assembled.
3932
3934
  9. **Flourishes are expressive: multiply their durations by --motion-expressive and gate JS ones on useMotion().expressive.** `subtle` promises the answers without the choreography. A glide on a plain token keeps playing after someone asked for calm.
3933
3935
  10. **A state change finishes its story: refused shakes and says why, success confirms in place, armed asks again.** Motion that starts a change and leaves the outcome to a snap reads as unfinished. ConfirmButton, OtpField's invalid shake and the copy-to-check swap are the reference cases.
3934
- 11. **Component motion props and MotionScope only turn motion down.** `full` on a component means inherit. Nothing below the provider can switch motion back on for a reader who asked for it off.
3936
+ 11. **Amplitude scales with smallness. A 16px glyph that turns on grows from nothing past its size; a surface travels from 0.96 and a press settles to 0.97.** The same 4% that is right for a dialog is invisible on an icon, which is why a state change on a small control used to read as a colour swap with no life in it. A dialog that overshot 20% would lurch. Size, not importance, sets how far a thing moves.
3937
+ 12. **Stage a moment: the main object lands first, the follow-through after it.** A ring that opens, a glyph that blooms 90ms later, dots thrown 60ms after that read as one event with a cause and an effect. Everything starting on the same frame reads as a flash. LikeButton and the toast's status glyph are the reference cases.
3938
+ 13. **Celebrate only what a person chose, and only at full motion.** A burst is a reply. On a like it is delight; on a system event, on load, or on every row of a list it is a notification wearing confetti. Under subtle the fill alone answers.
3939
+ 14. **Component motion props and MotionScope only turn motion down.** `full` on a component means inherit. Nothing below the provider can switch motion back on for a reader who asked for it off.
3935
3940
 
3936
3941
  ## Never
3937
3942
 
@@ -3976,6 +3981,17 @@ The underline used to vanish under one tab and appear under the next. Now it sli
3976
3981
 
3977
3982
  The digits that changed roll; the ones that did not stay still, so the eye goes straight to the difference.
3978
3983
 
3984
+ ### A glyph that lands
3985
+
3986
+ ```tsx
3987
+ // before
3988
+ <Icon name="heart" variant={liked ? "solid" : "regular"} />
3989
+ // after
3990
+ <LikeButton count={likes} defaultPressed={liked} />
3991
+ ```
3992
+
3993
+ The heart used to swap fill in one frame. Now it blooms past its size and settles while a ring and dots burst from it, and the count rolls: the same state, told as an event.
3994
+
3979
3995
  ### Faster exits
3980
3996
 
3981
3997
  ```tsx
@@ -4067,6 +4083,7 @@ Sable's components ship accessible behavior from Base UI. These rules cover what
4067
4083
  - **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.
4068
4084
  - **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.*
4069
4085
  - **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.*
4086
+ - **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.
4070
4087
  - **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.
4071
4088
  - **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.
4072
4089
  - **Never:** outline: none without a replacement. **Instead:** focus-visible:ring-2 ring-ring. Keyboard users lose their place.
@@ -8289,6 +8306,125 @@ export function MediaPlayerBlock({ className, framed = true, onPlayingChange, on
8289
8306
 
8290
8307
  ---
8291
8308
 
8309
+ # MessageReactionsBlock
8310
+
8311
+ A short thread you can react to: right-click or long-press a bubble for a menu with a row of quick reactions above Reply and Copy, or use the add-reaction pill under a message. A new reaction blooms in and sprays copies of its emoji.
8312
+
8313
+ **Intent:** Reach for this when a conversation excerpt should be reactable — a team thread, a comment card, a support transcript. A read-only excerpt is ChatThreadBlock; a working chat with a composer and history is the ChatPanel page shell.
8314
+
8315
+ **Block:** Social & messaging · card · designed for 18rem–24rem wide (fills its container up to the max)
8316
+
8317
+ ## Notes
8318
+
8319
+ - Two ways in, on purpose: the right-click / long-press menu is fast once you know it is there, and the add-reaction pill is how a first-time reader finds reactions at all.
8320
+ - The quick row in the menu is MenuReactions (checkbox menu items) and the one in the pill's popover is ReactionPicker (a toolbar), so each is reachable by the keyboard model of the surface it sits in.
8321
+ - Only the reader's own reaction celebrates. Counts from other people roll quietly, and the reactions a page loads with never play.
8322
+ - The add-reaction pill appears only on messages that already have reactions; a message with none is reacted to from its menu, which keeps quiet rows quiet.
8323
+
8324
+ ## Accessibility
8325
+
8326
+ - Each bubble is focusable, and its menu opens on right click, long press, Shift+F10 or the Menu key; its reactions are checkbox items reporting whether the reader has used each one.
8327
+ - Each reaction pill is a toggle named by its meaning and count ("Party, 3") with `aria-pressed`; the emoji and the burst are hidden from assistive tech.
8328
+ - Under reduced motion the reaction appears and the count changes at once; nothing else is lost.
8329
+
8330
+ ## Source
8331
+
8332
+ ```tsx
8333
+ import { Bubble, BubbleReaction, BubbleReactionAdd, BubbleReactions, Card, CardContent, CardFooter, cn, ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuSeparator, ContextMenuTrigger, DEFAULT_REACTIONS, MenuReactions, Message } from "@sableui/react"
8334
+
8335
+ const messages: { id: string; text: string; mine: boolean; reactions: Record<string, number> }[] = [
8336
+ { id: "m1", text: "The new onboarding flow is live on staging 🚀", mine: false, reactions: { "🎉": 2, "👀": 1 } },
8337
+ { id: "m2", text: "Clicked through it twice. The progress bar is so smooth.", mine: true, reactions: {} },
8338
+ { id: "m3", text: "Ship it Friday?", mine: false, reactions: { "👍": 1 } },
8339
+ ]
8340
+ const hint = "Right-click or long-press a message to react."
8341
+ // Every emoji that can appear needs a name: it is the pill's accessible label.
8342
+ const labels: Record<string, string> = {
8343
+ ...Object.fromEntries(DEFAULT_REACTIONS.map((r) => [r.emoji, r.label])),
8344
+ "👀": "Eyes",
8345
+ }
8346
+
8347
+ export interface MessageReactionsBlockProps {
8348
+ className?: string
8349
+ /** false drops the card's surface and border. Keep it framed on a muted page: incoming bubbles are the muted surface too. */
8350
+ framed?: boolean
8351
+ /** Called when the reader adds or removes a reaction, with the message, the emoji and whether it is now on. */
8352
+ onReact?: (messageId: string, emoji: string, on: boolean) => void
8353
+ }
8354
+
8355
+ export function MessageReactionsBlock({ className, framed = true, onReact }: MessageReactionsBlockProps) {
8356
+ // The reader's own reactions, per message. Everyone else's are the counts above.
8357
+ const [mine, setMine] = useState<Record<string, string[]>>({ m1: ["🎉"] })
8358
+ const toggle = (messageId: string, emoji: string) => {
8359
+ const on = !(mine[messageId] ?? []).includes(emoji)
8360
+ // Decided again from `prev`, so two toggles in one batch cannot add an emoji twice.
8361
+ setMine((prev) => {
8362
+ const current = prev[messageId] ?? []
8363
+ const next = current.includes(emoji) ? current.filter((e) => e !== emoji) : [...current, emoji]
8364
+ return { ...prev, [messageId]: next }
8365
+ })
8366
+ onReact?.(messageId, emoji, on)
8367
+ }
8368
+
8369
+ return (
8370
+ <Card variant={framed ? "default" : "flat"} className={cn("w-full max-w-sm", className)}>
8371
+ <CardContent className="flex flex-col gap-3">
8372
+ {messages.map((m) => {
8373
+ const align = m.mine ? "end" : "start"
8374
+ const chosen = mine[m.id] ?? []
8375
+ // Others' counts, plus the reader's own: a reaction only the reader gave
8376
+ // is a pill of its own, ordered after the ones already there.
8377
+ const counts: Record<string, number> = { ...m.reactions }
8378
+ for (const emoji of chosen) counts[emoji] = (counts[emoji] ?? 0)
8379
+ const pills = Object.entries(counts).map(([emoji, others]) => ({
8380
+ emoji,
8381
+ active: chosen.includes(emoji),
8382
+ count: others + (chosen.includes(emoji) ? 1 : 0),
8383
+ }))
8384
+ return (
8385
+ <Message key={m.id} messageId={m.id} align={m.mine ? "end" : undefined}>
8386
+ <ContextMenu>
8387
+ <ContextMenuTrigger
8388
+ // Focusable, so Shift+F10 or the Menu key opens the same menu.
8389
+ render={<Bubble tabIndex={0} variant={m.mine ? "primary" : undefined} align={align} />}
8390
+ >
8391
+ {m.text}
8392
+ </ContextMenuTrigger>
8393
+ <ContextMenuContent>
8394
+ <MenuReactions selected={chosen} onSelect={(emoji) => toggle(m.id, emoji)} />
8395
+ <ContextMenuSeparator />
8396
+ <ContextMenuItem>Reply</ContextMenuItem>
8397
+ <ContextMenuItem>Copy text</ContextMenuItem>
8398
+ </ContextMenuContent>
8399
+ </ContextMenu>
8400
+ {/* Rendered even while empty (it hides itself), so the first reaction still celebrates. */}
8401
+ <BubbleReactions align={align}>
8402
+ {pills.map((p) => (
8403
+ <BubbleReaction
8404
+ key={p.emoji}
8405
+ emoji={p.emoji}
8406
+ label={labels[p.emoji]}
8407
+ count={p.count}
8408
+ active={p.active}
8409
+ onClick={() => toggle(m.id, p.emoji)}
8410
+ />
8411
+ ))}
8412
+ {pills.length > 0 ? (
8413
+ <BubbleReactionAdd selected={chosen} onSelect={(emoji) => toggle(m.id, emoji)} />
8414
+ ) : null}
8415
+ </BubbleReactions>
8416
+ </Message>
8417
+ )
8418
+ })}
8419
+ </CardContent>
8420
+ <CardFooter className="text-caption text-muted-foreground">{hint}</CardFooter>
8421
+ </Card>
8422
+ )
8423
+ }
8424
+ ```
8425
+
8426
+ ---
8427
+
8292
8428
  # NewsletterBlock
8293
8429
 
8294
8430
  A newsletter sign-up card: the list's name, what it sends and how often, and an email field with Subscribe.
@@ -10134,7 +10270,10 @@ const activity = [
10134
10270
  { title: "Subscription", detail: "Team plan · 2h ago", amount: "+49.00", tone: "1" },
10135
10271
  { title: "Refund", detail: "Order 1042 · Yesterday", amount: "-24.00", tone: "2" },
10136
10272
  { title: "Payout", detail: "To checking · Mon", amount: "-850.00", tone: "3" },
10137
- ]
10273
+ ] as const
10274
+
10275
+ /** Each row's mark: its tone at graphic strength, which every theme tunes. */
10276
+ const TONE_MARK = { "1": "bg-tone-1-mark", "2": "bg-tone-2-mark", "3": "bg-tone-3-mark" } as const
10138
10277
 
10139
10278
  export interface WalletBlockProps {
10140
10279
  className?: string
@@ -10168,9 +10307,7 @@ export function WalletBlock({ className, framed = true, onSend, onReceive }: Wal
10168
10307
  leading={
10169
10308
  <span
10170
10309
  aria-hidden
10171
- className="block size-8 rounded-full"
10172
- // design-lint-disable-next-line inline-color -- relative colour derived from the theme's own --tone token, so it follows every look
10173
- 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)` }}
10310
+ className={cn("block size-8 rounded-full", TONE_MARK[item.tone])}
10174
10311
  />
10175
10312
  }
10176
10313
  title={item.title}
@@ -10967,7 +11104,7 @@ import { AppHeader, AppShell, PageHeader, Region } from "@sableui/react"
10967
11104
 
10968
11105
  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.
10969
11106
 
10970
- **Keywords:** app shell, layout, frame, sidebar, header, bottom nav, mobile, banners, main, dock, player bar, now playing, persistent
11107
+ **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
10971
11108
 
10972
11109
  ## Usage
10973
11110
 
@@ -10979,7 +11116,10 @@ Use AppShell as the outer product frame for authenticated or app-like experience
10979
11116
  - Do: Pass app-wide outage or status messages through banners so they remain above the frame.
10980
11117
  - Do: Use Region inside the main content to choose each route's reading width.
10981
11118
  - 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.
11119
+ - 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.
10982
11120
  - Don't: Nest AppShell inside route cards or panels. It is page chrome, not local content.
11121
+ - 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**
11122
+ - 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.
10983
11123
  - Don't: Put desktop-only navigation in the mobile header slot or mobile-only controls in the sidebar slot.
10984
11124
 
10985
11125
  ### Anatomy
@@ -10989,7 +11129,7 @@ Use AppShell as the outer product frame for authenticated or app-like experience
10989
11129
  | Banners | no | Full-width app status messages rendered above the shell. |
10990
11130
  | Header | no | Mobile top chrome, usually AppHeader. |
10991
11131
  | Sidebar | no | Desktop navigation chrome, usually Sidebar. |
10992
- | Main | yes | The route content landmark with id main-content. |
11132
+ | 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. |
10993
11133
  | Bottom nav | no | Mobile fixed navigation with reserved bottom space. |
10994
11134
  | 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. |
10995
11135
 
@@ -11009,10 +11149,11 @@ Use AppShell as the outer product frame for authenticated or app-like experience
11009
11149
  | bottomNav | ReactNode | — | Mobile bottom navigation (e.g. BottomNav); fixed to the bottom edge. main reserves space for it. |
11010
11150
  | 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. |
11011
11151
  | overlays | ReactNode | — | Floating overlays — a message launcher, FAB, or modal mount points. |
11152
+ | 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. |
11012
11153
 
11013
11154
  ## Tokens used
11014
11155
 
11015
- `background`, `foreground`, `primary`, `primary-foreground`
11156
+ `background`, `foreground`, `primary`, `primary-foreground`, `sidebar`, `shell-ground`, `border`
11016
11157
 
11017
11158
  ## Accessibility
11018
11159
 
@@ -11020,6 +11161,7 @@ Use AppShell as the outer product frame for authenticated or app-like experience
11020
11161
  - Wraps content in a <main id="main-content"> landmark.
11021
11162
  - Slot components own their own ARIA (nav landmarks, labels).
11022
11163
  - The dock follows <main> in DOM order, so keyboard and screen-reader users reach the player after the page content, not before it.
11164
+ - 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.
11023
11165
 
11024
11166
  ## Related
11025
11167
 
@@ -11059,6 +11201,70 @@ import { AppHeader, AppShell, Banner, BottomNav, BottomNavItem, Icon, PageHeader
11059
11201
  </AppShell>
11060
11202
  ```
11061
11203
 
11204
+ ### Inset frame
11205
+
11206
+ ```tsx
11207
+ 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"
11208
+ // <Link> = your router's link (react-router, next/link, @tanstack/router, …)
11209
+
11210
+ <AppShell
11211
+ frame="inset"
11212
+ header={<AppHeader title="Pipeline" />}
11213
+ sidebar={
11214
+ <Sidebar>
11215
+ <SidebarHeader>
11216
+ <SidebarBrand mark={<Icon name="gem" variant="solid" />} label="Relay" render={<Link to="/" />} />
11217
+ </SidebarHeader>
11218
+ <SidebarNav>
11219
+ <SidebarItem icon={<Icon name="inbox" />} label="Inbox" render={<Link to="/inbox" />} />
11220
+ <SidebarItem icon={<Icon name="user" />} label="People" active render={<Link to="/people" />} />
11221
+ <SidebarItem icon={<Icon name="folder" />} label="Companies" render={<Link to="/companies" />} />
11222
+ </SidebarNav>
11223
+ </Sidebar>
11224
+ }
11225
+ bottomNav={
11226
+ <BottomNav>
11227
+ <BottomNavItem icon={<Icon name="inbox" />} label="Inbox" render={<Link to="/inbox" />} />
11228
+ <BottomNavItem icon={<Icon name="user" />} label="People" active render={<Link to="/people" />} />
11229
+ <BottomNavItem icon={<Icon name="folder" />} label="Companies" render={<Link to="/companies" />} />
11230
+ </BottomNav>
11231
+ }
11232
+ >
11233
+ <Page max="wide" gap={4} className="py-6">
11234
+ <PageHeader
11235
+ title="People"
11236
+ description="Everyone in your pipeline, newest first."
11237
+ actions={<Button><Icon name="plus" />Add person</Button>}
11238
+ />
11239
+ <Card>
11240
+ <CardContent flush>
11241
+ <Table>
11242
+ <TableHeader>
11243
+ <TableRow>
11244
+ <TableHead>Name</TableHead>
11245
+ <TableHead>Company</TableHead>
11246
+ <TableHead numeric>Deals</TableHead>
11247
+ </TableRow>
11248
+ </TableHeader>
11249
+ <TableBody>
11250
+ <TableRow>
11251
+ <TableCell>Ada Lovelace</TableCell>
11252
+ <TableCell>Northwind</TableCell>
11253
+ <TableCell numeric>4</TableCell>
11254
+ </TableRow>
11255
+ <TableRow>
11256
+ <TableCell>Grace Hopper</TableCell>
11257
+ <TableCell>Harbor Labs</TableCell>
11258
+ <TableCell numeric>2</TableCell>
11259
+ </TableRow>
11260
+ </TableBody>
11261
+ </Table>
11262
+ </CardContent>
11263
+ </Card>
11264
+ </Page>
11265
+ </AppShell>
11266
+ ```
11267
+
11062
11268
  ### SkipLink in a layout of your own
11063
11269
 
11064
11270
  ```tsx
@@ -11157,7 +11363,7 @@ Use AreaChart for volume over time — visitors, revenue, usage — where the fi
11157
11363
 
11158
11364
  ## Tokens used
11159
11365
 
11160
- `tone-1`, `tone-2`, `tone-3`, `tone-info`, `tone-warning`, `primary`, `muted-foreground`, `border`, `popover`, `popover-foreground`, `foreground`, `background`
11366
+ `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`
11161
11367
 
11162
11368
  ## Motion
11163
11369
 
@@ -11280,7 +11486,7 @@ The card a file takes in a conversation — in the composer while it uploads, an
11280
11486
 
11281
11487
  ## Tokens used
11282
11488
 
11283
- `card`, `border`, `border-strong`, `muted`, `muted-foreground`, `foreground`, `destructive`, `overlay`, `overlay-foreground`, `ring`
11489
+ `card`, `border`, `border-strong`, `muted`, `muted-foreground`, `foreground`, `destructive`, `overlay`, `overlay-foreground`, `ring`, `destructive-mark`
11284
11490
 
11285
11491
  ## Motion
11286
11492
 
@@ -11567,7 +11773,7 @@ Use Badge for any short static label: metadata, categories, tone-coded labels, a
11567
11773
 
11568
11774
  ## Tokens used
11569
11775
 
11570
- `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`
11776
+ `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`
11571
11777
 
11572
11778
  ## Accessibility
11573
11779
 
@@ -11696,7 +11902,7 @@ Use Banner for page-level or app-level messages that affect the current experien
11696
11902
 
11697
11903
  ## Tokens used
11698
11904
 
11699
- `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`
11905
+ `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`
11700
11906
 
11701
11907
  ## Accessibility
11702
11908
 
@@ -11820,7 +12026,7 @@ Use BarChart to compare amounts across categories — the default chart for "how
11820
12026
 
11821
12027
  ## Tokens used
11822
12028
 
11823
- `tone-1`, `tone-2`, `tone-3`, `tone-info`, `tone-warning`, `primary`, `muted-foreground`, `border`, `popover`, `popover-foreground`, `foreground`, `background`
12029
+ `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`
11824
12030
 
11825
12031
  ## Motion
11826
12032
 
@@ -12180,14 +12386,16 @@ The tinted (or untinted) surface a turn's content sits on. Kept separate from Me
12180
12386
  | --- | --- | --- |
12181
12387
  | Bubble | yes | The surface itself. Holds the variant, the side, and the tail. |
12182
12388
  | BubbleReactions | no | The row of reaction pills, just under the bubble at the message column's small gap, so it reads as belonging to that message rather than the next row. |
12183
- | BubbleReaction | no | One reaction pill — emoji, count (hidden at 1), and pressed state. |
12389
+ | BubbleReaction | no | One reaction pill — emoji, count (hidden at 1), and pressed state. Celebrates when it becomes the reader's own. |
12390
+ | BubbleReactionAdd | no | The add-reaction pill at the end of the row: opens a ReactionPicker above it. |
12184
12391
 
12185
12392
  ## Subcomponents
12186
12393
 
12187
12394
  | Name | Type | Description |
12188
12395
  | --- | --- | --- |
12189
12396
  | BubbleReactions | { align?: "start" \| "end" } & ComponentPropsWithRef<"div"> | Row container for reaction pills. Match `align` to the Bubble so the row hangs off the same edge. |
12190
- | BubbleReaction | { emoji: ReactNode; count?: number; active?: boolean } & ComponentPropsWithRef<"button"> | One reaction pill. A toggle, not a label: `active` drives `aria-pressed` as well as the styling, and `count` is hidden at 1 because the pill already says "one". |
12397
+ | BubbleReaction | { emoji: ReactNode; label?: string; count?: number; active?: boolean; motion?: MotionLevel } & ComponentPropsWithRef<"button"> | One reaction pill. A toggle, not a label: `active` drives `aria-pressed` as well as the styling, `label` names it ("Party, 3"), and `count` is hidden at 1 because the pill already says "one". When it turns on — pressed, or newly added already on — the emoji blooms, copies of it burst out and the count rolls; pills the page loads with stay still. |
12398
+ | BubbleReactionAdd | { reactions?: Reaction[]; selected?: string[]; onSelect?: (emoji: string) => void; label?: string; motion?: MotionLevel } & ComponentPropsWithRef<"button"> | The add-reaction pill: a smiley that opens a ReactionPicker above it. Choosing an emoji closes the picker and calls `onSelect`; add the emoji to the row and its new pill celebrates on arrival. |
12191
12399
 
12192
12400
  ## Props
12193
12401
 
@@ -12208,16 +12416,23 @@ The tinted (or untinted) surface a turn's content sits on. Kept separate from Me
12208
12416
 
12209
12417
  `muted`, `foreground`, `primary`, `primary-foreground`, `border`, `card`, `card-foreground`, `accent`, `accent-foreground`, `muted-foreground`, `ring`
12210
12418
 
12419
+ ## Motion
12420
+
12421
+ - **arrival** · Bubble · _no motion_ — A bubble arrives with its Message row, which owns how a new message enters the transcript. A reaction pill that the reader just added arrives with its own celebration (below).
12422
+ - **response** · BubbleReaction · `motion-bloom` — Reacting is the reader's choice, so it is answered like a like: the emoji grows from nothing past its size and settles, while copies of it spray outward and fade (Burst with the emoji as its particle, the way chat apps answer a reaction). Plays when a pill turns on and when a pill mounts already on into a row that has rendered — never for the pills a page loads with. Pills inherit Pill's motion-interactive and motion-press.
12423
+ - **transition** · BubbleReaction · _custom (outside the recipe layer)_ — The count rolls with NumberTicker as people react. Turning a reaction off is quiet: the tint eases out and the count rolls back, with no burst. Under `subtle` or reduced motion the bloom and the spray are dropped and the tint and count alone answer.
12424
+ - **activity** · Bubble · _no motion_ — A bubble is static content; a message still being written is StreamingText or a Marker, not a state of the surface.
12425
+
12211
12426
  ## Accessibility
12212
12427
 
12213
12428
  - The surface is a plain container with no imposed role — a bubble is presentation, and announcing one as a region would put a landmark around every line of a conversation.
12214
- - Reaction pills are real buttons reporting `aria-pressed`, so the reader's own reaction is state rather than colour alone; the emoji is hidden from AT and the count is left readable.
12429
+ - Reaction pills are real buttons reporting `aria-pressed`, so the reader's own reaction is state rather than colour alone. Give each a `label`: it is named "Party, 3", with the emoji and its burst hidden from AT. The add-reaction pill is named "Add reaction" and opens a toolbar picker.
12215
12430
  - `whitespace-pre-wrap` and `break-words` keep pasted logs, long URLs, and hard-wrapped text inside the surface instead of overflowing the conversation.
12216
12431
  - Every variant meets AA against its own surface, including `primary`, which pairs the primary fill with its paired foreground token rather than a hardcoded white.
12217
12432
 
12218
12433
  ## Related
12219
12434
 
12220
- - Components: Message, MessageScroller, Marker, Attachment
12435
+ - Components: Message, MessageScroller, Marker, Attachment, ReactionPicker
12221
12436
 
12222
12437
  ## Examples
12223
12438
 
@@ -12248,13 +12463,14 @@ import { Avatar, Bubble, CodeBlock, Message } from "@sableui/react"
12248
12463
  ### With reactions
12249
12464
 
12250
12465
  ```tsx
12251
- import { Bubble, BubbleReaction, BubbleReactions, Message } from "@sableui/react"
12466
+ import { Bubble, BubbleReaction, BubbleReactionAdd, BubbleReactions, Message } from "@sableui/react"
12252
12467
 
12253
12468
  <Message messageId="m3">
12254
12469
  <Bubble>Shipped 0.45.0.</Bubble>
12255
12470
  <BubbleReactions>
12256
- <BubbleReaction emoji="🎉" count={3} active onClick={() => toggle("🎉")} />
12257
- <BubbleReaction emoji="👀" count={1} onClick={() => toggle("👀")} />
12471
+ <BubbleReaction emoji="🎉" label="Party" count={3} active onClick={() => toggle("🎉")} />
12472
+ <BubbleReaction emoji="👀" label="Eyes" count={1} onClick={() => toggle("👀")} />
12473
+ <BubbleReactionAdd selected={mine} onSelect={toggle} />
12258
12474
  </BubbleReactions>
12259
12475
  </Message>
12260
12476
  ```
@@ -13069,7 +13285,7 @@ Pairs with MessageScroller to make a working chat. A single surface rather than
13069
13285
 
13070
13286
  ## Tokens used
13071
13287
 
13072
- `card`, `input`, `ring`, `destructive`, `foreground`, `muted-foreground`, `primary`, `primary-foreground`, `secondary`, `secondary-foreground`, `accent`, `accent-foreground`
13288
+ `field`, `field-border`, `ring`, `destructive`, `foreground`, `muted-foreground`, `primary`, `primary-foreground`, `secondary`, `secondary-foreground`, `accent`, `accent-foreground`
13073
13289
 
13074
13290
  ## Motion
13075
13291
 
@@ -13785,13 +14001,14 @@ Use Combobox when users choose from a list that benefits from typing, filtering,
13785
14001
  | 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. |
13786
14002
  | 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. |
13787
14003
  | 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. |
14004
+ | 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. |
13788
14005
  | onInputValueChange | (inputValue: string, eventDetails) => void | — | Fires as the reader types. The place to fetch results for a server-backed combobox. |
13789
14006
  | 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. |
13790
14007
  | virtualized | boolean | false | Tells Base UI the DOM holds only part of the list. Required when using ComboboxVirtualList. |
13791
14008
 
13792
14009
  ## Tokens used
13793
14010
 
13794
- `input`, `card`, `foreground`, `muted-foreground`, `ring`, `destructive`, `popover`, `popover-foreground`, `border`, `accent`, `accent-foreground`, `primary`, `secondary`, `secondary-foreground`
14011
+ `highlight`, `highlight-foreground`, `field-border`, `field`, `foreground`, `muted-foreground`, `ring`, `destructive`, `popover`, `popover-foreground`, `border`, `accent`, `accent-foreground`, `primary`, `secondary`, `secondary-foreground`
13795
14012
 
13796
14013
  ## Motion
13797
14014
 
@@ -13913,7 +14130,7 @@ Use CommandMenu for global or contextual keyboard-driven actions: navigation, cr
13913
14130
 
13914
14131
  | Name | Type | Description |
13915
14132
  | --- | --- | --- |
13916
- | 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. |
14133
+ | 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. |
13917
14134
  | 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. |
13918
14135
  | 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. |
13919
14136
  | CommandMenuGroup | { label: string; children } | A labelled group of command rows. |
@@ -13931,6 +14148,7 @@ Use CommandMenu for global or contextual keyboard-driven actions: navigation, cr
13931
14148
  | query | string | — | Controlled query. Leave undefined to let the palette own it; either way it resets on close (a controlled caller gets onQueryChange('')). |
13932
14149
  | onQueryChange | (query: string) => void | — | Called on every query change. Required to control query, optional otherwise. |
13933
14150
  | 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. |
14151
+ | 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. |
13934
14152
  | 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. |
13935
14153
  | placeholder | string | "Type a command or search…" | Placeholder for the search input. |
13936
14154
  | emptyMessage | string | "No results found." | Copy shown when the query matches nothing. |
@@ -13941,7 +14159,7 @@ Use CommandMenu for global or contextual keyboard-driven actions: navigation, cr
13941
14159
 
13942
14160
  ## Tokens used
13943
14161
 
13944
- `card`, `card-foreground`, `border`, `foreground`, `muted-foreground`, `accent`, `accent-foreground`
14162
+ `highlight`, `highlight-foreground`, `card`, `card-foreground`, `border`, `foreground`, `muted-foreground`, `accent`, `accent-foreground`
13945
14163
 
13946
14164
  ## Motion
13947
14165
 
@@ -14477,7 +14695,7 @@ Use DatePicker when users need to choose one date — or, with granularity="minu
14477
14695
 
14478
14696
  ## Tokens used
14479
14697
 
14480
- `primary`, `primary-foreground`, `accent`, `accent-foreground`, `foreground`, `muted-foreground`, `popover`, `popover-foreground`, `border`, `input`, `card`, `ring`
14698
+ `primary`, `primary-foreground`, `accent`, `accent-foreground`, `foreground`, `muted-foreground`, `popover`, `popover-foreground`, `border`, `field-border`, `field`, `ring`
14481
14699
 
14482
14700
  ## Accessibility
14483
14701
 
@@ -15229,12 +15447,13 @@ Use Feedback to collect lightweight sentiment, usefulness, or quality signals fr
15229
15447
 
15230
15448
  ## Tokens used
15231
15449
 
15232
- `popover`, `popover-foreground`, `border`, `input`, `card`, `foreground`, `muted-foreground`, `accent`, `accent-foreground`, `ring`, `success`
15450
+ `popover`, `popover-foreground`, `border`, `input`, `card`, `foreground`, `muted-foreground`, `accent`, `accent-foreground`, `ring`, `success`, `success-mark`
15233
15451
 
15234
15452
  ## Motion
15235
15453
 
15236
15454
  - **arrival** · Popover · `motion-pop` — The form opens from its trigger, so the eye goes from the button it pressed to the field it should type in. Popover owns the recipe.
15237
15455
  - **response** · Rating toggles · `motion-interactive` — The rating row is a choice among a few similar marks. The eased selected fill is what makes the pick read as made, not just hovered.
15456
+ - **response** · Chosen rating mark · `motion-bloom` — The mark itself answers the pick: it blooms past its size and settles, so choosing reads as a small decision landing rather than a fill changing. Only the chosen mark, only on choosing — nothing blooms when the form opens or a pick is let go. Expressive: under subtle the fill alone answers.
15238
15457
  - **transition** · Thanks · `motion-tick` — Sending is the one moment the person needs to know their words arrived. The form fades out in place and a check draws itself in over it, then the popover closes. Without it the popover just vanished, which looks the same as the form being dismissed or the message lost.
15239
15458
  - **activity** · Root · _no motion_ — The thanks shows as soon as the form is sent; delivery is the app's background work. A spinner for a one-line report would make a quick, optional gesture feel heavy.
15240
15459
 
@@ -15859,13 +16078,15 @@ Placeholder artwork painted by a static WebGL shader from the preset's own art d
15859
16078
 
15860
16079
  ## Usage
15861
16080
 
15862
- 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.
16081
+ 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.
15863
16082
 
15864
16083
  ### Best practices
15865
16084
 
15866
16085
  - 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.
15867
16086
  - 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.
15868
16087
  - 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.
16088
+ - 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.
16089
+ - 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.
15869
16090
  - 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.
15870
16091
  - 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.
15871
16092
  - 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.
@@ -15877,12 +16098,14 @@ Use GradientArt where a real image belongs but none exists yet, or where the ima
15877
16098
  | variant | "mesh" \| "grain" | "mesh" | mesh: soft spots of colour pulled together by gentle waves. grain: glowing light over deep corners with a printed texture. |
15878
16099
  | 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. |
15879
16100
  | seed | string \| number | — | Fixes the composition: the same seed paints the same art in every theme. Pass a stable id. |
16101
+ | 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. |
15880
16102
  | children | ReactNode | — | Rendered above the art — initials, a glyph, a short label. |
15881
16103
  | className | string | — | Size and shape: aspect, height, rounding. |
15882
16104
 
15883
16105
  ## Variants
15884
16106
 
15885
16107
  - **variant**: mesh, grain
16108
+ - **intensity**: vivid, soft
15886
16109
 
15887
16110
  ## Tokens used
15888
16111
 
@@ -15930,6 +16153,29 @@ import { GradientArt } from "@sableui/react"
15930
16153
  <GradientArt seed="Mara Okafor" className="grid size-10 place-items-center rounded-full text-label-md text-overlay-foreground">MO</GradientArt>
15931
16154
  ```
15932
16155
 
16156
+ ### Soft header band with logo tiles
16157
+
16158
+ ```tsx
16159
+ import { Button, Card, CardDescription, CardFooter, CardHeader, CardTitle, GradientArt, Icon } from "@sableui/react"
16160
+
16161
+ <Card className="w-full max-w-sm">
16162
+ <GradientArt intensity="soft" seed="Integrations" className="flex h-32 items-center justify-center gap-3 rounded-t-lg">
16163
+ {["mail", "calendar", "chart"].map((name) => (
16164
+ <span key={name} className="grid size-12 place-items-center rounded-lg border border-border-floating bg-popover text-popover-foreground shadow-md">
16165
+ <Icon name={name} className="text-xl" />
16166
+ </span>
16167
+ ))}
16168
+ </GradientArt>
16169
+ <CardHeader>
16170
+ <CardTitle>Connect your tools</CardTitle>
16171
+ <CardDescription>Bring mail, calendar and analytics into one place.</CardDescription>
16172
+ </CardHeader>
16173
+ <CardFooter>
16174
+ <Button>Connect</Button>
16175
+ </CardFooter>
16176
+ </Card>
16177
+ ```
16178
+
15933
16179
  ---
15934
16180
 
15935
16181
  # InlineStatus
@@ -16001,7 +16247,7 @@ import { InlineStatus, Spinner } from "@sableui/react"
16001
16247
 
16002
16248
  **Status:** stable
16003
16249
 
16004
- Styled native text input. Soft-square (rounded-sm), card surface, 2px focus ring; aria-invalid raises the destructive error treatment.
16250
+ 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.
16005
16251
 
16006
16252
  **Keywords:** input, text input, field, form, email, search, file, invalid, validation
16007
16253
 
@@ -16015,6 +16261,8 @@ Use Input for short freeform text, search, email, password, file, and native inp
16015
16261
  - Do: Use type, inputMode, autoComplete, and placeholder as progressive hints, not as the only label.
16016
16262
  - Do: Set aria-invalid or use Field error copy when validation fails.
16017
16263
  - 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.
16264
+ - 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.
16265
+ - 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.
16018
16266
  - 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.
16019
16267
  - Don't: Use placeholder text as the label; it disappears once the user types and is not enough context.
16020
16268
  - Don't: Use Input for long body copy. Use Textarea for multi-line content.
@@ -16044,7 +16292,7 @@ Use Input for short freeform text, search, email, password, file, and native inp
16044
16292
 
16045
16293
  ## Tokens used
16046
16294
 
16047
- `input`, `card`, `foreground`, `muted-foreground`, `ring`, `destructive`
16295
+ `field-border`, `field`, `foreground`, `muted-foreground`, `ring`, `destructive`
16048
16296
 
16049
16297
  ## Accessibility
16050
16298
 
@@ -16159,7 +16407,7 @@ Use InputGroup when something belongs inside the field with the text: a unit, a
16159
16407
 
16160
16408
  ## Tokens used
16161
16409
 
16162
- `input`, `card`, `ring`, `destructive`, `muted-foreground`, `foreground`
16410
+ `field-border`, `field`, `ring`, `destructive`, `muted-foreground`, `foreground`
16163
16411
 
16164
16412
  ## Motion
16165
16413
 
@@ -16363,6 +16611,100 @@ import { Label } from "@sableui/react"
16363
16611
 
16364
16612
  ---
16365
16613
 
16614
+ # LikeButton
16615
+
16616
+ **Status:** stable
16617
+
16618
+ A pressed-or-not toggle for a like, a star or a save, with its count. Pressing it on fills the glyph, which blooms while a ring and a spray of dots burst from it and the count rolls up; pressing it off is quiet.
16619
+
16620
+ **Keywords:** like, heart, favorite, favourite, love, star, bookmark, save, upvote, reaction, count, toggle
16621
+
16622
+ ## Usage
16623
+
16624
+ Use LikeButton for a personal, reversible endorsement that other people can see the tally of: liking a post, starring a repository, saving a recipe. It is the one place in Sable that celebrates, because the person chose it and it costs nothing to undo.
16625
+
16626
+ ### Best practices
16627
+
16628
+ - Do: Pass the server's total as `count` and update it when the save lands. The button shows the viewer's own like at once, before the save, and goes back to `count` exactly whenever it changes, so a failed save that leaves `count` alone can also turn `pressed` back off.
16629
+ - Do: Match the glyph, family and label to the meaning: `icon="star" tone="yellow" label="Star"` for a star. The label is the accessible name, so it must say what pressing does.
16630
+ - Do: Use `format={{ notation: "compact" }}` once counts pass a few thousand, so a feed of buttons keeps one width.
16631
+ - Don't: Do not use it for a setting or a view switch (bold, mute, grid or list). Those are tools, not endorsements, and a burst on every press of a formatting control is noise; use Toggle. → use **Toggle**
16632
+ - Don't: Do not use it for a rating or a verdict on an answer (helpful, not helpful). That is a question with two sides; use Feedback. → use **Feedback**
16633
+ - Don't: Do not press it for the user. A burst is a reply to a person's choice — liking something on their behalf, or on load, turns delight into a notification.
16634
+
16635
+ ## Props
16636
+
16637
+ | Name | Type | Default | Description |
16638
+ | --- | --- | --- | --- |
16639
+ | count | number | — | The total, rolled when it changes. Pressing adds or removes the viewer's like at once; the button shows `count` exactly again whenever `count` changes. Omit it for a bare glyph (an icon-size button). |
16640
+ | pressed | boolean | — | Controlled pressed state. |
16641
+ | defaultPressed | boolean | false | Initial pressed state when uncontrolled. |
16642
+ | onPressedChange | (pressed: boolean, event) => void | — | Called when the user presses it, with the new state. |
16643
+ | icon | string | "heart" | The glyph, by icon name. Its `solid` variant is shown while pressed. |
16644
+ | tone | PaletteColor | "red" | The palette family the glyph fills with while pressed, and the burst blooms from. A family, so it follows the theme. |
16645
+ | label | string | "Like" | The accessible name, read before the count. |
16646
+ | format | Intl.NumberFormatOptions | — | How to write the count, e.g. `{ notation: "compact" }`. |
16647
+ | variant | "ghost" \| "outline" \| "secondary" | "ghost" | The Button surface. |
16648
+ | size | "xs" \| "sm" \| "md" | "sm" | A Button size; without a count, the matching icon size. |
16649
+ | disabled | boolean | false | Disables it. |
16650
+ | motion | "full" \| "subtle" \| "off" | "full" | How much motion this component uses: `full` (default) inherits the app's policy, `subtle` keeps feedback and drops the flourish, `off` stills it. It can only turn motion down — it never overrides a reader's reduce-motion setting. The bloom and the burst are the flourish; the fill and the count changing stay. |
16651
+
16652
+ ## Variants
16653
+
16654
+ - **variant**: ghost, outline, secondary
16655
+ - **size**: xs, sm, md
16656
+ - **tone**: red, orange, yellow, green, teal, blue, purple, pink
16657
+
16658
+ ## Tokens used
16659
+
16660
+ `palette-dot`, `accent`, `ring`
16661
+
16662
+ ## Motion
16663
+
16664
+ - **arrival** · Root · _no motion_ — It arrives with the post or row it belongs to. A page that loads with likes already given must be still: nothing blooms on mount.
16665
+ - **response** · Glyph · `motion-bloom` — Pressing it on is the moment. The glyph is 16px, so it needs real amplitude to read as an event: it grows from nothing past its size and settles, after a 90ms beat in which the ring leads. Inherits Button's motion-press for the press itself. Pressing off has no bloom — taking a like back is not a celebration.
16666
+ - **transition** · Burst · _custom (outside the recipe layer)_ — The follow-through: <Burst> draws a disc that hollows into a ring and shifts colour, then dots thrown outward in the theme's palette families. Expressive only — under `subtle` or reduced motion it renders nothing and the fill alone answers the press. The count rolls with NumberTicker.
16667
+ - **activity** · Root · _no motion_ — There is no in-between state to show: the like is shown at once, before it is saved.
16668
+
16669
+ ## Accessibility
16670
+
16671
+ - A native <button> through Button, with `aria-pressed`. Enter and Space press it.
16672
+ - The accessible name is the label then the count ("Like 1,301"); the burst is aria-hidden and the rolling digits are hidden behind the count's real text.
16673
+ - The pressed state is carried by the filled glyph and `aria-pressed`, never by colour alone.
16674
+ - Under reduced motion the glyph fills and the count changes at once; the bloom and the burst are dropped.
16675
+
16676
+ ## Related
16677
+
16678
+ - Components: Toggle, Feedback, NumberTicker, Button
16679
+
16680
+ ## Examples
16681
+
16682
+ ### A post's likes
16683
+
16684
+ ```tsx
16685
+ import { LikeButton } from "@sableui/react"
16686
+
16687
+ <LikeButton count={post.likes} defaultPressed={post.liked} onPressedChange={(on) => api.like(post.id, on)} />
16688
+ ```
16689
+
16690
+ ### Star a repository
16691
+
16692
+ ```tsx
16693
+ import { LikeButton } from "@sableui/react"
16694
+
16695
+ <LikeButton icon="star" tone="yellow" label="Star" variant="outline" count={repo.stars} format={{ notation: "compact" }} />
16696
+ ```
16697
+
16698
+ ### Bare glyph
16699
+
16700
+ ```tsx
16701
+ import { LikeButton } from "@sableui/react"
16702
+
16703
+ <LikeButton label="Save recipe" />
16704
+ ```
16705
+
16706
+ ---
16707
+
16366
16708
  # LineChart
16367
16709
 
16368
16710
  **Status:** beta
@@ -16411,7 +16753,7 @@ Use LineChart to compare how two or more measures change over the same sequence,
16411
16753
 
16412
16754
  ## Tokens used
16413
16755
 
16414
- `tone-1`, `tone-2`, `tone-3`, `tone-info`, `tone-warning`, `primary`, `muted-foreground`, `border`, `popover`, `popover-foreground`, `foreground`, `background`
16756
+ `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`
16415
16757
 
16416
16758
  ## Motion
16417
16759
 
@@ -17408,7 +17750,7 @@ import { Bubble, Button, Message, MessageScroller } from "@sableui/react"
17408
17750
 
17409
17751
  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.
17410
17752
 
17411
- **Keywords:** meter, usage bar, storage, quota, capacity, limit, password strength, level
17753
+ **Keywords:** meter, usage bar, storage, quota, capacity, limit, password strength, level, segmented, tick bar, score, credits, spend limit
17412
17754
 
17413
17755
  ## Usage
17414
17756
 
@@ -17419,15 +17761,17 @@ Use Meter to show a static measurement against a known maximum — storage used,
17419
17761
  - 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.
17420
17762
  - 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.
17421
17763
  - 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.
17764
+ - 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.
17422
17765
  - 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**
17423
17766
  - 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.
17767
+ - 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**
17424
17768
 
17425
17769
  ### Anatomy
17426
17770
 
17427
17771
  | Part | Required | Description |
17428
17772
  | --- | --- | --- |
17429
17773
  | MeterTrack | yes | The rail that represents the full range. |
17430
- | MeterIndicator | yes | The fill, sized from value and coloured by tone. |
17774
+ | MeterIndicator | yes | The fill, sized from value and coloured by tone. Not used with `segments`: the track draws its ticks instead. |
17431
17775
  | MeterLabel | no | The visible name; also names the meter for assistive tech. |
17432
17776
  | MeterValue | no | The formatted reading. |
17433
17777
 
@@ -17435,7 +17779,7 @@ Use Meter to show a static measurement against a known maximum — storage used,
17435
17779
 
17436
17780
  | Name | Type | Description |
17437
17781
  | --- | --- | --- |
17438
- | MeterTrack | Base UI Track props + className + size | The rail. Rendered automatically when Meter has no children; renders a MeterIndicator by default. |
17782
+ | 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). |
17439
17783
  | MeterIndicator | Base UI Indicator props + className + tone | The fill. Inherits the Meter's tone unless given its own. |
17440
17784
  | MeterLabel | Base UI Label props + className | Visible label; wires aria-labelledby on the root. |
17441
17785
  | MeterValue | Base UI Value props + className | Formatted reading. Accepts a `(formatted, value) => ReactNode` child. |
@@ -17451,6 +17795,7 @@ Use Meter to show a static measurement against a known maximum — storage used,
17451
17795
  | 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. |
17452
17796
  | 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. |
17453
17797
  | showValue | boolean | — | Print the value in the ring's centre (formatted like MeterValue). |
17798
+ | 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". |
17454
17799
  | strokeWidth | number | 6 | Ring stroke width, in viewBox units of 100. |
17455
17800
  | format | Intl.NumberFormatOptions | — | Formatting for MeterValue and aria-valuetext (percent, unit, currency). |
17456
17801
  | getAriaValueText | (formatted: string, value: number) => string | — | Human-readable reading for assistive tech ("7.2 of 10 gigabytes used"). |
@@ -17464,13 +17809,14 @@ Use Meter to show a static measurement against a known maximum — storage used,
17464
17809
 
17465
17810
  ## Tokens used
17466
17811
 
17467
- `secondary`, `primary`, `success`, `tone-warning`, `destructive`, `foreground`, `muted-foreground`
17812
+ `secondary`, `primary`, `tone-3`, `success`, `tone-warning`, `destructive`, `foreground`, `muted-foreground`, `destructive-mark`, `success-mark`, `tone-warning-mark`, `tone-3-mark`
17468
17813
 
17469
17814
  ## Motion
17470
17815
 
17471
17816
  - **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.
17472
17817
  - **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.
17473
17818
  - **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.
17819
+ - **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.
17474
17820
  - **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.
17475
17821
 
17476
17822
  ## Accessibility
@@ -17515,6 +17861,31 @@ import { Meter, MeterLabel, MeterTrack, MeterValue } from "@sableui/react"
17515
17861
  </Meter>
17516
17862
  ```
17517
17863
 
17864
+ ### Segmented spend limit
17865
+
17866
+ ```tsx
17867
+ import { Meter, MeterLabel, MeterTrack, MeterValue } from "@sableui/react"
17868
+
17869
+ <Meter value={1240} max={2000} segments={40} getAriaValueText={() => "€1,240 of €2,000 spent this month"}>
17870
+ <div className="mb-2 flex flex-wrap items-center justify-between gap-x-3 gap-y-1">
17871
+ <MeterLabel>Monthly spend limit</MeterLabel>
17872
+ <MeterValue>{() => "€1,240 / €2,000"}</MeterValue>
17873
+ </div>
17874
+ <MeterTrack />
17875
+ </Meter>
17876
+ ```
17877
+
17878
+ ### Segmented score cell
17879
+
17880
+ ```tsx
17881
+ import { Meter } from "@sableui/react"
17882
+
17883
+ <div className="flex items-center gap-3">
17884
+ <Meter value={8} max={10} segments={10} size="sm" tone="success" className="w-24" aria-label="ICP score" getAriaValueText={(_, v) => `${v} out of 10`} />
17885
+ <span className="text-mono-sm text-foreground">8/10</span>
17886
+ </div>
17887
+ ```
17888
+
17518
17889
  ### Tones
17519
17890
 
17520
17891
  ```tsx
@@ -17747,7 +18118,7 @@ Use Note for helpful supporting information that should sit near the relevant co
17747
18118
 
17748
18119
  ## Tokens used
17749
18120
 
17750
- `foreground`, `border`, `muted`, `muted-foreground`, `tone-info`, `tone-warning`, `success`, `destructive`
18121
+ `foreground`, `border`, `muted`, `muted-foreground`, `tone-info`, `tone-warning`, `success`, `destructive`, `destructive-mark`, `success-mark`, `tone-warning-mark`, `tone-info-mark`
17751
18122
 
17752
18123
  ## Motion
17753
18124
 
@@ -17914,7 +18285,7 @@ Use NumberField for numeric values that benefit from validation, min/max bounds,
17914
18285
 
17915
18286
  ## Tokens used
17916
18287
 
17917
- `input`, `card`, `foreground`, `muted-foreground`, `accent`, `accent-foreground`, `ring`
18288
+ `field-border`, `field`, `foreground`, `muted-foreground`, `accent`, `accent-foreground`, `ring`
17918
18289
 
17919
18290
  ## Accessibility
17920
18291
 
@@ -18092,7 +18463,7 @@ Use OtpField for a short, fixed-length code the user copies from somewhere else
18092
18463
 
18093
18464
  ## Tokens used
18094
18465
 
18095
- `input`, `card`, `foreground`, `muted-foreground`, `border-strong`, `primary`, `ring`, `destructive`
18466
+ `field-border`, `field`, `foreground`, `muted-foreground`, `border-strong`, `primary`, `ring`, `destructive`
18096
18467
 
18097
18468
  ## Motion
18098
18469
 
@@ -18919,7 +19290,7 @@ Use PieChart for a share of one total across two to six categories — traffic s
18919
19290
 
18920
19291
  ## Tokens used
18921
19292
 
18922
- `tone-1`, `tone-2`, `tone-3`, `tone-info`, `tone-warning`, `primary`, `muted-foreground`, `border`, `popover`, `popover-foreground`, `foreground`, `background`
19293
+ `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`
18923
19294
 
18924
19295
  ## Motion
18925
19296
 
@@ -19303,7 +19674,7 @@ Use RadarChart to compare the shape of a profile across five to eight dimensions
19303
19674
 
19304
19675
  ## Tokens used
19305
19676
 
19306
- `tone-1`, `tone-2`, `tone-3`, `tone-info`, `tone-warning`, `primary`, `muted-foreground`, `border`, `popover`, `popover-foreground`, `foreground`, `background`
19677
+ `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`
19307
19678
 
19308
19679
  ## Motion
19309
19680
 
@@ -19415,7 +19786,7 @@ Use RadialChart for progress toward goals (several rings), a single score or usa
19415
19786
 
19416
19787
  ## Tokens used
19417
19788
 
19418
- `tone-1`, `tone-2`, `tone-3`, `tone-info`, `tone-warning`, `primary`, `muted-foreground`, `border`, `popover`, `popover-foreground`, `foreground`, `background`
19789
+ `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`
19419
19790
 
19420
19791
  ## Motion
19421
19792
 
@@ -19674,6 +20045,124 @@ import { RadioCard, RadioGroup } from "@sableui/react"
19674
20045
 
19675
20046
  ---
19676
20047
 
20048
+ # ReactionPicker
20049
+
20050
+ **Status:** stable
20051
+
20052
+ The quick row of emoji you react to a message with: a toolbar of large emoji toggles that bloom in one after another as it opens and magnify under the pointer, with MenuReactions as the same row inside a right-click menu.
20053
+
20054
+ **Keywords:** reaction, reactions, emoji, emoji picker, react, tapback, quick reactions, message actions, chat
20055
+
20056
+ ## Usage
20057
+
20058
+ Use ReactionPicker wherever a reader chooses a reaction for something: in a Popover from an add-reaction control (BubbleReactionAdd is that, ready-made), or as a hover bar over a message. Use MenuReactions for the same row as the first group of the menu a message opens on right-click or long-press, above Reply and Copy. Both report which emoji the reader has already used, so choosing one again removes it.
20059
+
20060
+ ### Best practices
20061
+
20062
+ - Do: Keep the quick set short — six to eight. It is the reactions people actually use; a full emoji catalogue belongs behind a trailing "more" control passed as children.
20063
+ - Do: Give every reaction a `label`. It is the accessible name and the menu's typeahead, and an emoji's own spoken name differs between platforms.
20064
+ - Do: Pass `selected` so the reader's own reactions show pressed, and toggle in `onSelect`: choosing a reaction you already gave takes it back.
20065
+ - Do: Offer the right-click menu and a visible add-reaction control together. A context menu is invisible until you know it is there; the pill is how a first-time reader finds reactions.
20066
+ - Don't: Do not put a ReactionPicker inside a Menu or ContextMenu. A toolbar is not a menu item, so the menu's arrow keys would skip it; use MenuReactions there. → use **MenuReactions**
20067
+ - Don't: Do not use it to rate an answer or collect a verdict. Reactions are social and many-valued; a helpful / not-helpful question is Feedback. → use **Feedback**
20068
+
20069
+ ### Anatomy
20070
+
20071
+ | Part | Required | Description |
20072
+ | --- | --- | --- |
20073
+ | ReactionPicker | yes | The toolbar: one tab stop, arrow keys move between emoji, each a toggle with `aria-pressed`. |
20074
+ | MenuReactions | no | The same row as a group of checkbox menu items, for MenuContent and ContextMenuContent. Choosing one closes the menu. |
20075
+
20076
+ ## Subcomponents
20077
+
20078
+ | Name | Type | Description |
20079
+ | --- | --- | --- |
20080
+ | MenuReactions | { reactions?; selected?; onSelect?; motion? } & ComponentProps<typeof Menu.Group> | The quick row as checkbox menu items, for the first group of a MenuContent or ContextMenuContent. Same props as ReactionPicker, without children. |
20081
+
20082
+ ## Props
20083
+
20084
+ | Name | Type | Default | Description |
20085
+ | --- | --- | --- | --- |
20086
+ | reactions | readonly { emoji: string; label: string }[] | DEFAULT_REACTIONS | The choices, in order. The default is 👍 ❤️ 😂 🎉 😮 😢 🙏. |
20087
+ | selected | readonly string[] | [] | The emoji the reader has already reacted with: shown pressed, and choosing one again removes it. |
20088
+ | onSelect | (emoji: string) => void | — | Called with the emoji the reader chose. Toggle it in or out of `selected`. |
20089
+ | children | ReactNode | — | ReactionPicker only: trailing controls after the emoji, such as a button that opens a full emoji picker. |
20090
+ | aria-label | string | "Reactions" | The name of the toolbar or menu group. |
20091
+ | motion | "full" \| "subtle" \| "off" | "full" | How much motion this component uses: `full` (default) inherits the app's policy, `subtle` keeps the press and drops the wave of emoji blooming in and the magnify, `off` stills it. It can only turn motion down. |
20092
+
20093
+ ## Tokens used
20094
+
20095
+ `accent`, `primary`, `ring`
20096
+
20097
+ ## Motion
20098
+
20099
+ - **arrival** · Glyph · `motion-bloom` — Opening the picker is the reader's own action, so the emoji answer it: each blooms in 24ms after the one before, a short wave from first to last. The surface it sits in (a Popover or a menu) arrives with its own motion-pop. Expressive only — under `subtle` the emoji are simply there.
20100
+ - **response** · Glyph · _custom (outside the recipe layer)_ — The emoji under the pointer or the keyboard highlight magnifies to 1.25 on a fast standard ease, so you can see which one a release will choose; each cell also carries motion-interactive and motion-press. The magnify rides an inner layer so it never fights the bloom over `scale`.
20101
+ - **transition** · Cell · `motion-interactive` — Pressed and not pressed differ by a tint, eased by motion-interactive. The celebration belongs to the reaction that lands under the message (BubbleReaction), not to the picker that chose it.
20102
+ - **activity** · Root · _no motion_ — Choosing is instant; there is no in-between state to show.
20103
+
20104
+ ## Accessibility
20105
+
20106
+ - ReactionPicker is a Base UI toolbar: one tab stop, Left/Right move between emoji, Home/End jump to the ends.
20107
+ - Each emoji is a button named by its `label` and reporting `aria-pressed`, so a screen-reader user hears whether choosing it adds or removes their reaction. The glyph itself is aria-hidden.
20108
+ - MenuReactions renders checkbox menu items (`menuitemcheckbox` with `aria-checked`), reached by the menu's arrow keys and typeahead like every other row.
20109
+ - Under reduced motion the emoji appear at once and do not magnify; pressed state is carried by `aria-pressed` and a tint, never motion.
20110
+
20111
+ ## Related
20112
+
20113
+ - Components: Bubble, ContextMenu, Popover, Toolbar, Feedback
20114
+
20115
+ ## Examples
20116
+
20117
+ ### In a popover
20118
+
20119
+ ```tsx
20120
+ import { Button, Icon, Popover, PopoverContent, PopoverTrigger, ReactionPicker } from "@sableui/react"
20121
+
20122
+ <Popover>
20123
+ <PopoverTrigger render={<Button variant="ghost" size="icon-sm" aria-label="Add reaction" />}>
20124
+ <Icon name="emoji" />
20125
+ </PopoverTrigger>
20126
+ <PopoverContent side="top" className="w-auto rounded-full p-1">
20127
+ <ReactionPicker selected={mine} onSelect={toggle} />
20128
+ </PopoverContent>
20129
+ </Popover>
20130
+ ```
20131
+
20132
+ ### In a right-click menu
20133
+
20134
+ ```tsx
20135
+ import { Bubble, ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuSeparator, ContextMenuTrigger, MenuReactions } from "@sableui/react"
20136
+
20137
+ <ContextMenu>
20138
+ <ContextMenuTrigger render={<Bubble />}>See you at 6.</ContextMenuTrigger>
20139
+ <ContextMenuContent>
20140
+ <MenuReactions selected={mine} onSelect={toggle} />
20141
+ <ContextMenuSeparator />
20142
+ <ContextMenuItem>Reply</ContextMenuItem>
20143
+ <ContextMenuItem>Copy text</ContextMenuItem>
20144
+ </ContextMenuContent>
20145
+ </ContextMenu>
20146
+ ```
20147
+
20148
+ ### A custom set
20149
+
20150
+ ```tsx
20151
+ import { ReactionPicker } from "@sableui/react"
20152
+
20153
+ <ReactionPicker
20154
+ reactions={[
20155
+ { emoji: "✅", label: "Done" },
20156
+ { emoji: "👀", label: "Looking" },
20157
+ { emoji: "🚀", label: "Shipped" },
20158
+ ]}
20159
+ selected={mine}
20160
+ onSelect={toggle}
20161
+ />
20162
+ ```
20163
+
20164
+ ---
20165
+
19677
20166
  # RelativeTime
19678
20167
 
19679
20168
  **Status:** stable
@@ -19957,7 +20446,7 @@ import { PropertyRow, ScrubField, Slider } from "@sableui/react"
19957
20446
 
19958
20447
  **Status:** stable
19959
20448
 
19960
- 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.
20449
+ 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.
19961
20450
 
19962
20451
  **Keywords:** search, input, filter, query
19963
20452
 
@@ -19970,6 +20459,7 @@ Use SearchInput for text queries that filter or search a known content set.
19970
20459
  - Do: Use a clear placeholder that names the content being searched.
19971
20460
  - Do: Provide a visible clear affordance when query state persists.
19972
20461
  - 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.
20462
+ - 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.
19973
20463
  - 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.
19974
20464
  - 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.
19975
20465
 
@@ -19989,7 +20479,7 @@ Use SearchInput for text queries that filter or search a known content set.
19989
20479
 
19990
20480
  ## Tokens used
19991
20481
 
19992
- `input`, `card`, `foreground`, `muted-foreground`, `ring`, `destructive`
20482
+ `field-border`, `field`, `foreground`, `muted-foreground`, `ring`, `destructive`
19993
20483
 
19994
20484
  ## Motion
19995
20485
 
@@ -20037,6 +20527,23 @@ import { Field, SearchInput } from "@sableui/react"
20037
20527
  <Field label="Search posts"><SearchInput defaultValue="roadmap" className="max-w-80" /></Field>
20038
20528
  ```
20039
20529
 
20530
+ ### Highlighting matches in the results
20531
+
20532
+ ```tsx
20533
+ import { SearchInput, useMatchHighlight } from "@sableui/react"
20534
+
20535
+ const [q, setQ] = useState("")
20536
+ const results = useMatchHighlight<HTMLUListElement>(q, { selector: "[data-title]" })
20537
+ const shown = posts.filter((post) => post.title.toLowerCase().includes(q.trim().toLowerCase()))
20538
+
20539
+ <SearchInput value={q} onValueChange={setQ} placeholder="Search posts" />
20540
+ <ul ref={results}>
20541
+ {shown.map((post) => (
20542
+ <li key={post.id}><span data-title>{post.title}</span></li>
20543
+ ))}
20544
+ </ul>
20545
+ ```
20546
+
20040
20547
  ### Disabled
20041
20548
 
20042
20549
  ```tsx
@@ -20236,7 +20743,7 @@ Use SegmentedBar when a stat card or row needs to show how a total divides into
20236
20743
 
20237
20744
  ## Tokens used
20238
20745
 
20239
- `tone-1`, `tone-2`, `tone-3`, `tone-info`, `primary`, `muted-foreground`, `success`, `tone-warning`, `destructive`
20746
+ `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`
20240
20747
 
20241
20748
  ## Motion
20242
20749
 
@@ -20351,7 +20858,7 @@ Use Select when users choose one value from a known, finite set. Use Combobox fo
20351
20858
 
20352
20859
  ## Tokens used
20353
20860
 
20354
- `input`, `card`, `foreground`, `ring`, `border`, `popover`, `popover-foreground`, `accent`, `accent-foreground`, `muted-foreground`
20861
+ `field-border`, `field`, `foreground`, `ring`, `border`, `popover`, `popover-foreground`, `accent`, `accent-foreground`, `muted-foreground`
20355
20862
 
20356
20863
  ## Motion
20357
20864
 
@@ -20836,6 +21343,7 @@ Use Sidebar for persistent desktop navigation or application chrome outside the
20836
21343
  - Do: Use it for stable app destinations that should remain visible while content changes.
20837
21344
  - Do: Keep item labels predictable and active state obvious.
20838
21345
  - 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.
21346
+ - 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.
20839
21347
  - Don't: Do not use Sidebar for page-local settings, filters, or temporary action groups.
20840
21348
  - 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.
20841
21349
 
@@ -20861,7 +21369,7 @@ Use Sidebar for persistent desktop navigation or application chrome outside the
20861
21369
  | 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(). |
20862
21370
  | icons | boolean | true | Render the leading icon on each SidebarItem. Set false for a text-only rail. Ignored while collapsed (icon-only by definition). |
20863
21371
  | 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. |
20864
- | 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. |
21372
+ | 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. |
20865
21373
  | 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. |
20866
21374
 
20867
21375
  ## Variants
@@ -20870,7 +21378,7 @@ Use Sidebar for persistent desktop navigation or application chrome outside the
20870
21378
 
20871
21379
  ## Tokens used
20872
21380
 
20873
- `sidebar`, `sidebar-foreground`, `sidebar-border`, `sidebar-accent`, `sidebar-accent-foreground`, `sidebar-ring`
21381
+ `sidebar`, `sidebar-foreground`, `sidebar-border`, `sidebar-accent`, `sidebar-accent-foreground`, `sidebar-ring`, `shell-ground`
20874
21382
 
20875
21383
  ## Motion
20876
21384
 
@@ -21278,7 +21786,7 @@ Use Sparkline next to a number to show where it has been — a KPI's last 30 day
21278
21786
 
21279
21787
  ## Tokens used
21280
21788
 
21281
- `primary`, `tone-1`, `tone-2`, `tone-3`, `tone-info`, `tone-warning`, `muted-foreground`, `success`, `destructive`
21789
+ `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`
21282
21790
 
21283
21791
  ## Motion
21284
21792
 
@@ -21549,7 +22057,7 @@ Use StatusDot as a compact visual state cue next to text, avatars, rows, or labe
21549
22057
 
21550
22058
  ## Tokens used
21551
22059
 
21552
- `palette-dot`, `muted-foreground`, `tone-3`, `success`, `tone-warning`, `tone-info`, `tone-1`, `tone-2`, `destructive`
22060
+ `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`
21553
22061
 
21554
22062
  ## Accessibility
21555
22063
 
@@ -21634,7 +22142,7 @@ Use Stepper above or beside a flow that is split into a known, small number of n
21634
22142
 
21635
22143
  ## Tokens used
21636
22144
 
21637
- `primary`, `primary-foreground`, `background`, `foreground`, `muted-foreground`, `border-strong`, `destructive`, `destructive-foreground`, `ring`
22145
+ `primary`, `primary-foreground`, `background`, `foreground`, `muted-foreground`, `border-strong`, `destructive`, `destructive-foreground`, `ring`, `destructive-mark`
21638
22146
 
21639
22147
  ## Motion
21640
22148
 
@@ -21927,8 +22435,8 @@ Use Table for structured data with consistent columns and rows. Use DataTable wh
21927
22435
  | TableBody | ComponentPropsWithRef<"tbody"> | The data row group (tbody). |
21928
22436
  | TableFooter | ComponentPropsWithRef<"tfoot"> | Summary row group (tfoot) with a muted fill. |
21929
22437
  | TableRow | ComponentPropsWithRef<"tr"> | A row (tr) — hairline bottom border, hover lift, and a selected state via the data-selected attribute. |
21930
- | TableHead | ComponentPropsWithRef<"th"> | A column header cell (th) in muted label styling. |
21931
- | TableCell | ComponentPropsWithRef<"td"> | A data cell (td). |
22438
+ | 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. |
22439
+ | TableCell | ComponentPropsWithRef<"td"> & { numeric?: boolean } | A data cell (td). `numeric` end-aligns it with tabular figures, matching its column's header. |
21932
22440
  | TableCaption | ComponentPropsWithRef<"caption"> | Accessible description of the table (caption), muted. |
21933
22441
 
21934
22442
  ## Props
@@ -22120,7 +22628,7 @@ import { Tabs, TabsList, TabsPanel, TabsTrigger } from "@sableui/react"
22120
22628
 
22121
22629
  **Status:** stable
22122
22630
 
22123
- Styled native multi-line text control. Soft-square, card surface, --ring focus, and an aria-invalid destructive error state.
22631
+ Styled native multi-line text control. Soft-square, the theme's field surface, --ring focus, and an aria-invalid destructive error state.
22124
22632
 
22125
22633
  **Keywords:** textarea, multi line input, form, text
22126
22634
 
@@ -22149,7 +22657,7 @@ Use Textarea for multi-line freeform text entry such as notes, descriptions, or
22149
22657
 
22150
22658
  ## Tokens used
22151
22659
 
22152
- `input`, `card`, `foreground`, `muted-foreground`, `ring`, `destructive`
22660
+ `field-border`, `field`, `foreground`, `muted-foreground`, `ring`, `destructive`
22153
22661
 
22154
22662
  ## Accessibility
22155
22663
 
@@ -22593,7 +23101,7 @@ Use Toast for transient feedback caused by a user action: saved, failed, copied,
22593
23101
  | closeIcon | ReactNode | <Icon name="xmark" /> | Override the dismiss-button glyph. The default already follows the injected icon set. |
22594
23102
  | useToast / useToastManager | () => { toasts, add, close, update, promise } | — | Manager hook for use inside components. add({ title, description, timeout?, type?, priority?, actionProps? }) returns the new toast id and applies the same persistence default as toast.* — an error, loading, or actionProps toast stays until dismissed unless you pass a timeout. close(id), update(id, opts), and promise(p, opts) round out the API. |
22595
23103
  | toast | (message, options?) => string & { success, error, warning, info, message, loading, promise, dismiss, update, add, custom } | — | Imperative singleton callable OUTSIDE React (event handlers, utilities, non-React code) — the Sonner-shaped surface that makes migrating a lib/toast wrapper a near drop-in. Requires a mounted <ToastProvider> (it binds the shared manager). Each call returns the toast id; toast.dismiss(id?) closes one or all; toast.promise(p, { loading, success, error }) and toast.update(id, opts) handle async + in-place updates. |
22596
- | type (toast option) | "success" \| "error" \| "warning" \| "info" \| "loading" | — | Set by the toast.* helpers. Drives a semantic left accent (success→success, error→destructive, warning→tone-warning, info→tone-info); loading shows a spinner. Both loading and error persist until updated or dismissed unless the call supplies a timeout. |
23104
+ | type (toast option) | "success" \| "error" \| "warning" \| "info" \| "loading" | — | Set by the toast.* helpers. Drives a leading status glyph from the shared tone list, the same one Note and InlineStatus use (success→success, error→destructive, warning→warning, info→info), which blooms in as the toast lands; loading shows a spinner, and the glyph blooms in its place when the toast settles. Both loading and error persist until updated or dismissed unless the call supplies a timeout. |
22597
23105
 
22598
23106
  ## Tokens used
22599
23107
 
@@ -22603,6 +23111,7 @@ Use Toast for transient feedback caused by a user action: saved, failed, copied,
22603
23111
 
22604
23112
  - **arrival** · ToastRoot · `motion-slide-left` — A toast interrupts without being asked, so where it came from is the reader's first question. Sliding in from the viewport edge answers it and marks the toast as belonging to the edge rather than to whatever it happens to cover.
22605
23113
  - **response** · ToastAction · `motion-interactive` — Toasts are on a timer, so the action has to look actionable at a glance — there is no time to hunt for the hit target.
23114
+ - **response** · Status glyph · `motion-bloom` — The glyph is what says how it went, so it lands as its own beat: it blooms in 120ms after the card, and blooms again in the spinner's place when a loading toast settles. A 16px glyph needs that amplitude to read as an answer; the card itself only slides.
22606
23115
  - **transition** · ToastRoot · `motion-slide-left` — Leaving the same way it entered keeps the stack legible when several dismiss at once; toasts that fade in place leave the remaining ones jumping to fill gaps with no visible cause.
22607
23116
  - **transition** · ToastRoot (stacked) · _custom (outside the recipe layer)_ — A stacked toast arrives from the anchored edge in front of the others, which step back and shrink to make room; hovering fans the deck out into the list. Moving every card, rather than snapping them, shows that the new one went on top and the old ones are still there.
22608
23117
  - **activity** · ToastRoot · _no motion_ — A toast reports a result that has already happened. Anything still in flight belongs in the surface that started it, where the reader is still looking.
@@ -23674,7 +24183,7 @@ Which token to reach for, and what it is most often confused with. Generated fro
23674
24183
  - **Not** `border-strong`. Reach for `border-strong` when the line carries *structure* (a grid rule, a table divider, a section separator) and for `border` when it merely bounds a control.
23675
24184
  - **`border-strong`** — A structural divider. Visibly stronger than `border`. *Use for:* Grid lines, table rules, separators between major sections.
23676
24185
  - **Not** `border`. If removing the line would make the layout ambiguous, it is structural and belongs here.
23677
- - **`input`** — The outline of a form control specifically. *Use for:* Text inputs, textareas and selects. Not the edge of a checkbox, radio or switch — that is `control-border`.
24186
+ - **`input`** — The outline of a form control specifically. *Use for:* Text inputs, textareas and selects (through `field-border`, which derives from it). Not the edge of a checkbox, radio or switch — that is `control-border`.
23678
24187
  - **Not** `border`. They are often the same value, but a theme is entitled to make form controls read differently from other chrome, and only this token gives it the hook.
23679
24188
  - **`control-border`** — The resting edge of a control whose outline is the whole control. Held to 3:1 (WCAG 1.4.11) on every surface. *Use for:* An unchecked checkbox, an unselected radio, an off switch track, a selection card's check or radio mark — anywhere removing the edge would make the control disappear.
23680
24189
  - **Not** `input`. A text field is also found by its fill, caret and placeholder, so its outline may stay quiet; a checkbox has nothing else, and at `input`'s strength it measures about 1.4:1.
@@ -23964,7 +24473,7 @@ Which token to reach for, and what it is most often confused with. Generated fro
23964
24473
  - **`type-mono-sm-tracking`** — The monospace family: mono-xl through mono-sm, plus mono-caption. *Use for:* Code, identifiers, hashes, keyboard keys, and any figure where digits must align in a column.
23965
24474
  - **Not** the sans tiers for numeric data. Proportional digits make a column of numbers ragged; that is what `text-ui-num` solves in chrome and mono solves on a page.
23966
24475
 
23967
- ### derived (10)
24476
+ ### derived (20)
23968
24477
 
23969
24478
  - **`canvas`** — The WORK surface — the artboard, the document being edited. *Use for:* Behind the thing the tool operates on. `WorkspaceCanvas` uses it.
23970
24479
  - **Not** `background`. The thing being edited is not the page behind the UI, and collapsing the two makes the document indistinguishable from its chrome.
@@ -23986,6 +24495,26 @@ Which token to reach for, and what it is most often confused with. Generated fro
23986
24495
  - **Not** `hairline`, when the line would otherwise disappear against what it bounds.
23987
24496
  - **`border-floating`** — The edge of a floating surface. Fainter than `border`: the shadow carries the elevation. *Use for:* Every floating surface — popover, menu, select, tooltip, dialog, sheet, toast, a floating panel — as `border-border-floating` or `ring-border-floating`, together with `bg-popover` and a `shadow-md`+ shadow. Derived for every theme (a faint alpha of the foreground); a theme may tune it with `--theme-border-floating`.
23988
24497
  - **Not** `border`. A full hairline plus a shadow stacks into a heavy outline around anything that floats. `border` is for resting edges: inputs, cards, badges.
24498
+ - **`field`** — The fill of a text field: Input, Textarea, Select, Combobox, SearchInput, InputGroup, ChatComposer. *Use for:* Read by every field through its shell, so you rarely name it. Derived for every theme (`card`, an outlined field on the card); a theme fills its fields instead with `--theme-field: var(--muted)` and `--theme-field-border: transparent`, and every field changes at once. Name it only for a field-shaped surface of your own that must match.
24499
+ - **Not** `card` or `muted` on a hand-built field. A field that paints `card` keeps its outline when the theme fills the rest, and one that paints `muted` stays filled when the theme outlines them.
24500
+ - **`field-border`** — The outline of a text field. Derived from `input`; `transparent` in a theme with filled fields. *Use for:* With `field`, on the same field-shaped surfaces. Tune it with `--theme-field-border`.
24501
+ - **Not** `input` on a text field. `input` stays the theme's outline colour for form chrome; this is the field's outline, which a filled theme turns off.
24502
+ - **`shell-ground`** — The ground of an inset app frame: the rail and the gap around the raised main panel. *Use for:* Painted by `<AppShell frame="inset">` and the Sidebar in its slot; you rarely name it. Derived for every theme as one step off `sidebar` toward its ink, so the panel reads as lifted even where the rail is the page colour. Tune it with `--theme-shell-ground`.
24503
+ - **Not** `sidebar` or `canvas` behind an inset panel. `sidebar` is the page colour in most themes, so the panel would sit on itself; `canvas` is a workspace's artboard, not app chrome.
24504
+ - **`destructive-mark`** — `destructive` at graphic strength: the same hue lifted and saturated, for a mark that carries no text. *Use for:* A meter's or progress fill, a segmented bar, a status dot, a chart series, and the soft tint behind a badge or note (`bg-destructive-mark/15`). Held to 2:1 on the page — visibility, not a text ratio — because each of these sits beside text that says the same thing. Every bundled theme tunes its own; a preset sets `--theme-destructive-mark` or gets one derived from `destructive`.
24505
+ - **Not** `bg-destructive` on a mark. The fill is pinned dark enough to carry a white label, so a meter or dot painted with it reads brown, forest or plum. And not for text — that is `text-destructive` — nor for a graphic that is the only carrier of its meaning, which owes 3:1 (WCAG 1.4.11): use the fill or add the text.
24506
+ - **`success-mark`** — `success` at graphic strength: the same hue lifted and saturated, for a mark that carries no text. *Use for:* A meter's or progress fill, a segmented bar, a status dot, a chart series, and the soft tint behind a badge or note (`bg-success-mark/15`). Held to 2:1 on the page — visibility, not a text ratio — because each of these sits beside text that says the same thing. Every bundled theme tunes its own; a preset sets `--theme-success-mark` or gets one derived from `success`.
24507
+ - **Not** `bg-success` on a mark. The fill is pinned dark enough to carry a white label, so a meter or dot painted with it reads brown, forest or plum. And not for text — that is `text-success` — nor for a graphic that is the only carrier of its meaning, which owes 3:1 (WCAG 1.4.11): use the fill or add the text.
24508
+ - **`tone-warning-mark`** — `tone-warning` at graphic strength: the same hue lifted and saturated, for a mark that carries no text. *Use for:* A meter's or progress fill, a segmented bar, a status dot, a chart series, and the soft tint behind a badge or note (`bg-tone-warning-mark/15`). Held to 2:1 on the page — visibility, not a text ratio — because each of these sits beside text that says the same thing. Every bundled theme tunes its own; a preset sets `--theme-tone-warning-mark` or gets one derived from `tone-warning`.
24509
+ - **Not** `bg-tone-warning` on a mark. The fill is pinned dark enough to carry a white label, so a meter or dot painted with it reads brown, forest or plum. And not for text — that is `text-tone-warning` — nor for a graphic that is the only carrier of its meaning, which owes 3:1 (WCAG 1.4.11): use the fill or add the text.
24510
+ - **`tone-info-mark`** — `tone-info` at graphic strength: the same hue lifted and saturated, for a mark that carries no text. *Use for:* A meter's or progress fill, a segmented bar, a status dot, a chart series, and the soft tint behind a badge or note (`bg-tone-info-mark/15`). Held to 2:1 on the page — visibility, not a text ratio — because each of these sits beside text that says the same thing. Every bundled theme tunes its own; a preset sets `--theme-tone-info-mark` or gets one derived from `tone-info`.
24511
+ - **Not** `bg-tone-info` on a mark. The fill is pinned dark enough to carry a white label, so a meter or dot painted with it reads brown, forest or plum. And not for text — that is `text-tone-info` — nor for a graphic that is the only carrier of its meaning, which owes 3:1 (WCAG 1.4.11): use the fill or add the text.
24512
+ - **`tone-1-mark`** — `tone-1` at graphic strength: the same hue lifted and saturated, for a mark that carries no text. *Use for:* A meter's or progress fill, a segmented bar, a status dot, a chart series, and the soft tint behind a badge or note (`bg-tone-1-mark/15`). Held to 2:1 on the page — visibility, not a text ratio — because each of these sits beside text that says the same thing. Every bundled theme tunes its own; a preset sets `--theme-tone-1-mark` or gets one derived from `tone-1`.
24513
+ - **Not** `bg-tone-1` on a mark. The fill is pinned dark enough to carry a white label, so a meter or dot painted with it reads brown, forest or plum. And not for text — that is `text-tone-1` — nor for a graphic that is the only carrier of its meaning, which owes 3:1 (WCAG 1.4.11): use the fill or add the text.
24514
+ - **`tone-2-mark`** — `tone-2` at graphic strength: the same hue lifted and saturated, for a mark that carries no text. *Use for:* A meter's or progress fill, a segmented bar, a status dot, a chart series, and the soft tint behind a badge or note (`bg-tone-2-mark/15`). Held to 2:1 on the page — visibility, not a text ratio — because each of these sits beside text that says the same thing. Every bundled theme tunes its own; a preset sets `--theme-tone-2-mark` or gets one derived from `tone-2`.
24515
+ - **Not** `bg-tone-2` on a mark. The fill is pinned dark enough to carry a white label, so a meter or dot painted with it reads brown, forest or plum. And not for text — that is `text-tone-2` — nor for a graphic that is the only carrier of its meaning, which owes 3:1 (WCAG 1.4.11): use the fill or add the text.
24516
+ - **`tone-3-mark`** — `tone-3` at graphic strength: the same hue lifted and saturated, for a mark that carries no text. *Use for:* A meter's or progress fill, a segmented bar, a status dot, a chart series, and the soft tint behind a badge or note (`bg-tone-3-mark/15`). Held to 2:1 on the page — visibility, not a text ratio — because each of these sits beside text that says the same thing. Every bundled theme tunes its own; a preset sets `--theme-tone-3-mark` or gets one derived from `tone-3`.
24517
+ - **Not** `bg-tone-3` on a mark. The fill is pinned dark enough to carry a white label, so a meter or dot painted with it reads brown, forest or plum. And not for text — that is `text-tone-3` — nor for a graphic that is the only carrier of its meaning, which owes 3:1 (WCAG 1.4.11): use the fill or add the text.
23989
24518
 
23990
24519
  ### Generated palette
23991
24520
 
@@ -24135,7 +24664,7 @@ The `@utility` and `@custom-variant` layer in `styles.css`: what each one is, wh
24135
24664
  - **Instead of** An animated fade or a hand-written `mask-image` gradient. Use `StreamingText` first; this is for a stream rendered without it.
24136
24665
  - `<span className="stream-tail">{latestChunk}</span>`
24137
24666
 
24138
- ### motion (30)
24667
+ ### motion (31)
24139
24668
 
24140
24669
  - **`motion-interactive`** — The transition for interactive chrome: colour, background, border, shadow, opacity, fill, stroke, scale and translate at the fast duration and standard easing. Also removes the mobile tap delay and the platform's grey tap flash. *Use for:* On anything clickable that changes on hover, focus, press or selection: a custom row, a tile, a link, an app-owned button. It is the `response` slot, and the only transition declarer such an element needs.
24141
24670
  - **Instead of** Tailwind's colour-only or all-property transition utilities with a hand-picked duration. Those ignore --motion-scale, skip the transform properties `motion-press` needs, and leave the 300ms tap delay on touch screens. sable-lint's `static-interactive` fires on a hover style with no recipe.
@@ -24183,6 +24712,9 @@ The `@utility` and `@custom-variant` layer in `styles.css`: what each one is, wh
24183
24712
  - **`motion-arrive`** — Items a user action brings into a list settle into place one after another: a short fade down from 0.25rem. Stagger with `--motion-arrive-delay` (raw ms; the recipe scales it). Expressive. *Use for:* Only on items that are NEW because of something the user did: a folder's children on expand, a row just added, results that load in. Tree's expand already uses it.
24184
24713
  - **Instead of** A staggered entrance on every render of a list, which turns a list into a loading delay, or an inline keyframe animation with literal durations.
24185
24714
  - `<li className="motion-arrive" style={{ "--motion-arrive-delay": "36ms" } as CSSProperties}>{row.name}</li>`
24715
+ - **`motion-bloom`** — A small glyph landing in its "on" state grows from nothing past its size (1.2) and settles. The amplitude a 16px glyph needs to read as an event; surfaces never bloom. A keyframe that plays on mount, so remount the glyph (a changing `key`) to replay it. `--motion-bloom-delay` holds it at nothing first (raw ms; the recipe scales it). Expressive. *Use for:* A glyph that a person's choice turns on: a liked heart, a starred item, a status icon arriving in a toast. LikeButton and Toast already use it. Pair with <Burst> for the follow-through on a celebratory toggle.
24716
+ - **Instead of** A bigger motion-press or motion-pop on the whole control (a button or card that overshoots 20% lurches), or a hand-written scale keyframe with literal durations. Never on the resting state: a page that loads with a post already liked must not bloom every heart.
24717
+ - `<span key={likes} className={liked ? "motion-bloom" : undefined}><Icon name="heart" variant={liked ? "solid" : "regular"} /></span>`
24186
24718
  - **`motion-size`** — Eases an element between sizes: between a keyword and a length (0 ↔ auto) through `interpolate-size: allow-keywords`, and between measured pixel sizes when paired with `useAutoSize()`, so a button whose label changes stretches to the new width instead of jumping. A superset of motion-interactive's transition list, so it can sit on a Button. The size is expressive. *Use for:* A control whose content changes length in place: a two-step confirm button, a Copy → Copied label, a chip that gains a count. ConfirmButton already uses it.
24187
24719
  - **Instead of** A hand-written width transition, or a fixed width that clips the longer label. A content-sized box computes `width: auto` before and after its label changes, so on its own the recipe has nothing to ease — that is what `useAutoSize()` is for.
24188
24720
  - `const size = useAutoSize<HTMLButtonElement, HTMLSpanElement>()
@@ -24232,7 +24764,7 @@ The `@utility` and `@custom-variant` layer in `styles.css`: what each one is, wh
24232
24764
  - **Instead of** A hand-written dash-offset animation with literal timing, which keeps animating, or keeps its delay, under reduced motion.
24233
24765
  - `<Reveal><svg viewBox="0 0 100 100"><path className="motion-draw" pathLength="1" d="M10 50 L90 50" /></svg></Reveal>`
24234
24766
 
24235
- ### helper (5)
24767
+ ### helper (6)
24236
24768
 
24237
24769
  - **`hit-target`** — Grows an element's tap target to 44px with a transparent `::after`, without moving its layout by a pixel. `--hit-target-size` overrides the size. *Use for:* A deliberately small mark — a tag's remove ×, a tiny disclosure, a toast close — that must still meet the WCAG touch floor. Shrink it (`[--hit-target-size:32px]`) where targets sit side by side, or the later one steals the taps.
24238
24770
  - **Instead of** Padding the glyph up (moves it off the row's grid and pushes siblings) or a bigger icon. Never on an `<input>` or other replaced element — pseudo-elements silently do nothing there; wrap it in a label.
@@ -24249,3 +24781,6 @@ The `@utility` and `@custom-variant` layer in `styles.css`: what each one is, wh
24249
24781
  - **`checkerboard`** — The transparency checkerboard, painted from `--panel` and a derived darker step so it reads in both modes. `--checkerboard-size` (default 16px) and `--checkerboard-alt` tune it. *Use for:* Behind anything that can be translucent: a colour swatch with alpha, an image or artboard preview, an alpha slider track.
24250
24782
  - **Instead of** A hand-rolled `repeating-conic-gradient` with two hardcoded greys. Deliberately not named `bg-checkerboard`, so `cn()` never merges it away against a real `bg-*` colour — you can layer a colour over it.
24251
24783
  - `<div className="checkerboard [--checkerboard-size:8px] size-6 rounded-sm" />`
24784
+ - **`texture-dither`** — A faint pixel / halftone dither field that dissolves: a small pseudo-random tile of squares painted in `--border-strong` and faded out by a gradient mask. Static, pure CSS, follows every theme and mode. Hooks: `--dither-fade` (direction, any linear-gradient side or angle, default `to bottom`), `--dither-size` (tile, default 48px = 2px squares) and `--dither-color` (default `var(--border-strong)`). *Use for:* Decoration at the top of a content area, a panel or a card, or behind an empty-state heading. Put it on its own `aria-hidden` element, `absolute -z-10` as the first child of a `relative isolate` parent, so the content after it paints on top with no z-index of its own. It sets `pointer-events: none` and no position, so it never takes layout or catches clicks.
24785
+ - **Instead of** A hand-written `radial-gradient` dot grid (reads as graph paper, not dither), a PNG noise texture, a canvas, or hardcoded greys. Deliberately not named `bg-dither`, so `cn()` never merges it away against a real `bg-*` colour class.
24786
+ - `<div className="relative isolate"><div aria-hidden className="texture-dither absolute inset-x-0 top-0 -z-10 h-40" /></div>`