@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
@@ -0,0 +1,91 @@
1
+ # LikeButton
2
+
3
+ **Status:** stable
4
+
5
+ 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.
6
+
7
+ **Keywords:** like, heart, favorite, favourite, love, star, bookmark, save, upvote, reaction, count, toggle
8
+
9
+ ## Usage
10
+
11
+ 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.
12
+
13
+ ### Best practices
14
+
15
+ - 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.
16
+ - 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.
17
+ - Do: Use `format={{ notation: "compact" }}` once counts pass a few thousand, so a feed of buttons keeps one width.
18
+ - 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**
19
+ - 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**
20
+ - 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.
21
+
22
+ ## Props
23
+
24
+ | Name | Type | Default | Description |
25
+ | --- | --- | --- | --- |
26
+ | 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). |
27
+ | pressed | boolean | — | Controlled pressed state. |
28
+ | defaultPressed | boolean | false | Initial pressed state when uncontrolled. |
29
+ | onPressedChange | (pressed: boolean, event) => void | — | Called when the user presses it, with the new state. |
30
+ | icon | string | "heart" | The glyph, by icon name. Its `solid` variant is shown while pressed. |
31
+ | tone | PaletteColor | "red" | The palette family the glyph fills with while pressed, and the burst blooms from. A family, so it follows the theme. |
32
+ | label | string | "Like" | The accessible name, read before the count. |
33
+ | format | Intl.NumberFormatOptions | — | How to write the count, e.g. `{ notation: "compact" }`. |
34
+ | variant | "ghost" \| "outline" \| "secondary" | "ghost" | The Button surface. |
35
+ | size | "xs" \| "sm" \| "md" | "sm" | A Button size; without a count, the matching icon size. |
36
+ | disabled | boolean | false | Disables it. |
37
+ | 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. |
38
+
39
+ ## Variants
40
+
41
+ - **variant**: ghost, outline, secondary
42
+ - **size**: xs, sm, md
43
+ - **tone**: red, orange, yellow, green, teal, blue, purple, pink
44
+
45
+ ## Tokens used
46
+
47
+ `palette-dot`, `accent`, `ring`
48
+
49
+ ## Motion
50
+
51
+ - **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.
52
+ - **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.
53
+ - **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.
54
+ - **activity** · Root · _no motion_ — There is no in-between state to show: the like is shown at once, before it is saved.
55
+
56
+ ## Accessibility
57
+
58
+ - A native <button> through Button, with `aria-pressed`. Enter and Space press it.
59
+ - 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.
60
+ - The pressed state is carried by the filled glyph and `aria-pressed`, never by colour alone.
61
+ - Under reduced motion the glyph fills and the count changes at once; the bloom and the burst are dropped.
62
+
63
+ ## Related
64
+
65
+ - Components: Toggle, Feedback, NumberTicker, Button
66
+
67
+ ## Examples
68
+
69
+ ### A post's likes
70
+
71
+ ```tsx
72
+ import { LikeButton } from "@sableui/react"
73
+
74
+ <LikeButton count={post.likes} defaultPressed={post.liked} onPressedChange={(on) => api.like(post.id, on)} />
75
+ ```
76
+
77
+ ### Star a repository
78
+
79
+ ```tsx
80
+ import { LikeButton } from "@sableui/react"
81
+
82
+ <LikeButton icon="star" tone="yellow" label="Star" variant="outline" count={repo.stars} format={{ notation: "compact" }} />
83
+ ```
84
+
85
+ ### Bare glyph
86
+
87
+ ```tsx
88
+ import { LikeButton } from "@sableui/react"
89
+
90
+ <LikeButton label="Save recipe" />
91
+ ```
@@ -46,7 +46,7 @@ Use LineChart to compare how two or more measures change over the same sequence,
46
46
 
47
47
  ## Tokens used
48
48
 
49
- `tone-1`, `tone-2`, `tone-3`, `tone-info`, `tone-warning`, `primary`, `muted-foreground`, `border`, `popover`, `popover-foreground`, `foreground`, `background`
49
+ `tone-1`, `tone-2`, `tone-3`, `tone-info`, `tone-warning`, `primary`, `muted-foreground`, `border`, `popover`, `popover-foreground`, `foreground`, `background`, `tone-warning-mark`, `tone-info-mark`, `tone-1-mark`, `tone-2-mark`, `tone-3-mark`
50
50
 
51
51
  ## Motion
52
52
 
@@ -4,7 +4,7 @@
4
4
 
5
5
  A role=meter reading over Base UI Meter, drawn as a bar or a ring — how full something is within a known range (storage, quota, strength), with a tone for good/caution/bad. Not task progress.
6
6
 
7
- **Keywords:** meter, usage bar, storage, quota, capacity, limit, password strength, level
7
+ **Keywords:** meter, usage bar, storage, quota, capacity, limit, password strength, level, segmented, tick bar, score, credits, spend limit
8
8
 
9
9
  ## Usage
10
10
 
@@ -15,15 +15,17 @@ Use Meter to show a static measurement against a known maximum — storage used,
15
15
  - Do: Label it and show the reading in units people use ("7.2 of 10 GB"), with MeterLabel and MeterValue. The bar shows proportion; the number is what gets acted on.
16
16
  - Do: Pick `tone` from thresholds you own and apply them consistently across the page — warning near the limit, destructive at or over it. The component does not guess where good becomes bad.
17
17
  - Do: Use shape="ring" when the reading is a score that belongs in a small, glanceable spot on a card. It is the same measurement drawn as a circular gauge, not a different component.
18
+ - Do: Use `segments` (a tick bar, ▮▮▮▮▮▮▯▯▯▯) for a discrete budget, score or quota read at a glance — a spend limit, credits left, an ICP lead score, months of data coverage. Pick a count that matches the unit when there is one (10 for a score out of 10, 12 for months) and otherwise size it to the track: a tick reads as a tick when it is taller than it is wide, about one per 8px of track (40 across a card row, 20 in a narrow table cell). Any reading above min shows at least one tick.
18
19
  - Don't: Do not use Meter for a task in flight — an upload, an install, a step count. That is Progress; Meter tells assistive tech nothing is happening. → use **Progress**
19
20
  - Don't: Do not use it for a value without a meaningful maximum (followers, total spend). A bar implies a full scale; an unbounded count is a stat.
21
+ - Don't: Do not segment a fine-grained continuous value where the exact proportion matters (disk at 73.4%) — ticks round it to the nearest mark; use the continuous bar. And do not use a tick bar for a task in flight or steps completed: that is Progress, or a Stepper for named steps. → use **Progress**
20
22
 
21
23
  ### Anatomy
22
24
 
23
25
  | Part | Required | Description |
24
26
  | --- | --- | --- |
25
27
  | MeterTrack | yes | The rail that represents the full range. |
26
- | MeterIndicator | yes | The fill, sized from value and coloured by tone. |
28
+ | MeterIndicator | yes | The fill, sized from value and coloured by tone. Not used with `segments`: the track draws its ticks instead. |
27
29
  | MeterLabel | no | The visible name; also names the meter for assistive tech. |
28
30
  | MeterValue | no | The formatted reading. |
29
31
 
@@ -31,7 +33,7 @@ Use Meter to show a static measurement against a known maximum — storage used,
31
33
 
32
34
  | Name | Type | Description |
33
35
  | --- | --- | --- |
34
- | MeterTrack | Base UI Track props + className + size | The rail. Rendered automatically when Meter has no children; renders a MeterIndicator by default. |
36
+ | MeterTrack | Base UI Track props + className + size | The rail. Rendered automatically when Meter has no children; renders a MeterIndicator by default — or, when the Meter has `segments`, its row of ticks (children are not rendered then). |
35
37
  | MeterIndicator | Base UI Indicator props + className + tone | The fill. Inherits the Meter's tone unless given its own. |
36
38
  | MeterLabel | Base UI Label props + className | Visible label; wires aria-labelledby on the root. |
37
39
  | MeterValue | Base UI Value props + className | Formatted reading. Accepts a `(formatted, value) => ReactNode` child. |
@@ -47,6 +49,7 @@ Use Meter to show a static measurement against a known maximum — storage used,
47
49
  | shape | "bar" \| "ring" | "bar" | bar is a horizontal track; ring is a circular gauge for a small, glanceable reading — the same value, min, max and tones. |
48
50
  | size | "sm" \| "md" \| "lg" | "md" | Ring diameter (40 / 64 / 96px at the default spacing; the centre value steps with it), or bar thickness on the same sm / md / lg rails as Progress. |
49
51
  | showValue | boolean | — | Print the value in the ring's centre (formatted like MeterValue). |
52
+ | segments | number | — | Draw the bar as this many separate ticks instead of a continuous fill (a quota, score or spend limit). Filled ticks = round(share × N), at least one for any reading above min, none at min. Ticks take the tone; empty ones sit on the rail colour. The segmented track has its own, taller sm / md / lg ladder. Bar only: ignored for shape="ring". |
50
53
  | strokeWidth | number | 6 | Ring stroke width, in viewBox units of 100. |
51
54
  | format | Intl.NumberFormatOptions | — | Formatting for MeterValue and aria-valuetext (percent, unit, currency). |
52
55
  | getAriaValueText | (formatted: string, value: number) => string | — | Human-readable reading for assistive tech ("7.2 of 10 gigabytes used"). |
@@ -60,13 +63,14 @@ Use Meter to show a static measurement against a known maximum — storage used,
60
63
 
61
64
  ## Tokens used
62
65
 
63
- `secondary`, `primary`, `success`, `tone-warning`, `destructive`, `foreground`, `muted-foreground`
66
+ `secondary`, `primary`, `tone-3`, `success`, `tone-warning`, `destructive`, `foreground`, `muted-foreground`, `destructive-mark`, `success-mark`, `tone-warning-mark`, `tone-3-mark`
64
67
 
65
68
  ## Motion
66
69
 
67
70
  - **arrival** · Meter · _no motion_ — A meter is a reading, present with the page that reports it. Animating the fill up from zero on load would act out a change that did not happen.
68
71
  - **response** · Meter · _no motion_ — It reports and accepts no input. A bar that reacted to the pointer would read as a Slider the user could drag.
69
72
  - **transition** · MeterIndicator · _custom (outside the recipe layer)_ — Width and colour ease on the motion tokens (no recipe animates width) when the reading changes — freeing space after a delete visibly shrinks the bar, so the user sees the cause and effect rather than a different bar.
73
+ - **transition** · MeterTrack (segments) · _custom (outside the recipe layer)_ — With segments, the fill runs through the ticks from the edge it is leaving — left to right as it grows, right to left as it shrinks — each tick easing its colour on the fast token after a stagger that is a share of the slow token (a full sweep is one slow duration) × --motion-expressive. Every tick switching at once would read as a different bar, not the same budget moving; `subtle` lands them together and off is instant.
70
74
  - **activity** · Meter · _no motion_ — A meter has no indeterminate state; a reading is either known or not shown. Unknown-but-working is Progress with no value.
71
75
 
72
76
  ## Accessibility
@@ -111,6 +115,31 @@ import { Meter, MeterLabel, MeterTrack, MeterValue } from "@sableui/react"
111
115
  </Meter>
112
116
  ```
113
117
 
118
+ ### Segmented spend limit
119
+
120
+ ```tsx
121
+ import { Meter, MeterLabel, MeterTrack, MeterValue } from "@sableui/react"
122
+
123
+ <Meter value={1240} max={2000} segments={40} getAriaValueText={() => "€1,240 of €2,000 spent this month"}>
124
+ <div className="mb-2 flex flex-wrap items-center justify-between gap-x-3 gap-y-1">
125
+ <MeterLabel>Monthly spend limit</MeterLabel>
126
+ <MeterValue>{() => "€1,240 / €2,000"}</MeterValue>
127
+ </div>
128
+ <MeterTrack />
129
+ </Meter>
130
+ ```
131
+
132
+ ### Segmented score cell
133
+
134
+ ```tsx
135
+ import { Meter } from "@sableui/react"
136
+
137
+ <div className="flex items-center gap-3">
138
+ <Meter value={8} max={10} segments={10} size="sm" tone="success" className="w-24" aria-label="ICP score" getAriaValueText={(_, v) => `${v} out of 10`} />
139
+ <span className="text-mono-sm text-foreground">8/10</span>
140
+ </div>
141
+ ```
142
+
114
143
  ### Tones
115
144
 
116
145
  ```tsx
@@ -35,7 +35,7 @@ Use Note for helpful supporting information that should sit near the relevant co
35
35
 
36
36
  ## Tokens used
37
37
 
38
- `foreground`, `border`, `muted`, `muted-foreground`, `tone-info`, `tone-warning`, `success`, `destructive`
38
+ `foreground`, `border`, `muted`, `muted-foreground`, `tone-info`, `tone-warning`, `success`, `destructive`, `destructive-mark`, `success-mark`, `tone-warning-mark`, `tone-info-mark`
39
39
 
40
40
  ## Motion
41
41
 
@@ -32,7 +32,7 @@ Use NumberField for numeric values that benefit from validation, min/max bounds,
32
32
 
33
33
  ## Tokens used
34
34
 
35
- `input`, `card`, `foreground`, `muted-foreground`, `accent`, `accent-foreground`, `ring`
35
+ `field-border`, `field`, `foreground`, `muted-foreground`, `accent`, `accent-foreground`, `ring`
36
36
 
37
37
  ## Accessibility
38
38
 
@@ -48,7 +48,7 @@ Use OtpField for a short, fixed-length code the user copies from somewhere else
48
48
 
49
49
  ## Tokens used
50
50
 
51
- `input`, `card`, `foreground`, `muted-foreground`, `border-strong`, `primary`, `ring`, `destructive`
51
+ `field-border`, `field`, `foreground`, `muted-foreground`, `border-strong`, `primary`, `ring`, `destructive`
52
52
 
53
53
  ## Motion
54
54
 
@@ -44,7 +44,7 @@ Use PieChart for a share of one total across two to six categories — traffic s
44
44
 
45
45
  ## Tokens used
46
46
 
47
- `tone-1`, `tone-2`, `tone-3`, `tone-info`, `tone-warning`, `primary`, `muted-foreground`, `border`, `popover`, `popover-foreground`, `foreground`, `background`
47
+ `tone-1`, `tone-2`, `tone-3`, `tone-info`, `tone-warning`, `primary`, `muted-foreground`, `border`, `popover`, `popover-foreground`, `foreground`, `background`, `tone-warning-mark`, `tone-info-mark`, `tone-1-mark`, `tone-2-mark`, `tone-3-mark`
48
48
 
49
49
  ## Motion
50
50
 
@@ -43,7 +43,7 @@ Use RadarChart to compare the shape of a profile across five to eight dimensions
43
43
 
44
44
  ## Tokens used
45
45
 
46
- `tone-1`, `tone-2`, `tone-3`, `tone-info`, `tone-warning`, `primary`, `muted-foreground`, `border`, `popover`, `popover-foreground`, `foreground`, `background`
46
+ `tone-1`, `tone-2`, `tone-3`, `tone-info`, `tone-warning`, `primary`, `muted-foreground`, `border`, `popover`, `popover-foreground`, `foreground`, `background`, `tone-warning-mark`, `tone-info-mark`, `tone-1-mark`, `tone-2-mark`, `tone-3-mark`
47
47
 
48
48
  ## Motion
49
49
 
@@ -45,7 +45,7 @@ Use RadialChart for progress toward goals (several rings), a single score or usa
45
45
 
46
46
  ## Tokens used
47
47
 
48
- `tone-1`, `tone-2`, `tone-3`, `tone-info`, `tone-warning`, `primary`, `muted-foreground`, `border`, `popover`, `popover-foreground`, `foreground`, `background`
48
+ `tone-1`, `tone-2`, `tone-3`, `tone-info`, `tone-warning`, `primary`, `muted-foreground`, `border`, `popover`, `popover-foreground`, `foreground`, `background`, `tone-warning-mark`, `tone-info-mark`, `tone-1-mark`, `tone-2-mark`, `tone-3-mark`
49
49
 
50
50
  ## Motion
51
51
 
@@ -0,0 +1,115 @@
1
+ # ReactionPicker
2
+
3
+ **Status:** stable
4
+
5
+ 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.
6
+
7
+ **Keywords:** reaction, reactions, emoji, emoji picker, react, tapback, quick reactions, message actions, chat
8
+
9
+ ## Usage
10
+
11
+ 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.
12
+
13
+ ### Best practices
14
+
15
+ - 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.
16
+ - 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.
17
+ - Do: Pass `selected` so the reader's own reactions show pressed, and toggle in `onSelect`: choosing a reaction you already gave takes it back.
18
+ - 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.
19
+ - 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**
20
+ - 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**
21
+
22
+ ### Anatomy
23
+
24
+ | Part | Required | Description |
25
+ | --- | --- | --- |
26
+ | ReactionPicker | yes | The toolbar: one tab stop, arrow keys move between emoji, each a toggle with `aria-pressed`. |
27
+ | MenuReactions | no | The same row as a group of checkbox menu items, for MenuContent and ContextMenuContent. Choosing one closes the menu. |
28
+
29
+ ## Subcomponents
30
+
31
+ | Name | Type | Description |
32
+ | --- | --- | --- |
33
+ | 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. |
34
+
35
+ ## Props
36
+
37
+ | Name | Type | Default | Description |
38
+ | --- | --- | --- | --- |
39
+ | reactions | readonly { emoji: string; label: string }[] | DEFAULT_REACTIONS | The choices, in order. The default is 👍 ❤️ 😂 🎉 😮 😢 🙏. |
40
+ | selected | readonly string[] | [] | The emoji the reader has already reacted with: shown pressed, and choosing one again removes it. |
41
+ | onSelect | (emoji: string) => void | — | Called with the emoji the reader chose. Toggle it in or out of `selected`. |
42
+ | children | ReactNode | — | ReactionPicker only: trailing controls after the emoji, such as a button that opens a full emoji picker. |
43
+ | aria-label | string | "Reactions" | The name of the toolbar or menu group. |
44
+ | 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. |
45
+
46
+ ## Tokens used
47
+
48
+ `accent`, `primary`, `ring`
49
+
50
+ ## Motion
51
+
52
+ - **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.
53
+ - **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`.
54
+ - **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.
55
+ - **activity** · Root · _no motion_ — Choosing is instant; there is no in-between state to show.
56
+
57
+ ## Accessibility
58
+
59
+ - ReactionPicker is a Base UI toolbar: one tab stop, Left/Right move between emoji, Home/End jump to the ends.
60
+ - 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.
61
+ - MenuReactions renders checkbox menu items (`menuitemcheckbox` with `aria-checked`), reached by the menu's arrow keys and typeahead like every other row.
62
+ - Under reduced motion the emoji appear at once and do not magnify; pressed state is carried by `aria-pressed` and a tint, never motion.
63
+
64
+ ## Related
65
+
66
+ - Components: Bubble, ContextMenu, Popover, Toolbar, Feedback
67
+
68
+ ## Examples
69
+
70
+ ### In a popover
71
+
72
+ ```tsx
73
+ import { Button, Icon, Popover, PopoverContent, PopoverTrigger, ReactionPicker } from "@sableui/react"
74
+
75
+ <Popover>
76
+ <PopoverTrigger render={<Button variant="ghost" size="icon-sm" aria-label="Add reaction" />}>
77
+ <Icon name="emoji" />
78
+ </PopoverTrigger>
79
+ <PopoverContent side="top" className="w-auto rounded-full p-1">
80
+ <ReactionPicker selected={mine} onSelect={toggle} />
81
+ </PopoverContent>
82
+ </Popover>
83
+ ```
84
+
85
+ ### In a right-click menu
86
+
87
+ ```tsx
88
+ import { Bubble, ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuSeparator, ContextMenuTrigger, MenuReactions } from "@sableui/react"
89
+
90
+ <ContextMenu>
91
+ <ContextMenuTrigger render={<Bubble />}>See you at 6.</ContextMenuTrigger>
92
+ <ContextMenuContent>
93
+ <MenuReactions selected={mine} onSelect={toggle} />
94
+ <ContextMenuSeparator />
95
+ <ContextMenuItem>Reply</ContextMenuItem>
96
+ <ContextMenuItem>Copy text</ContextMenuItem>
97
+ </ContextMenuContent>
98
+ </ContextMenu>
99
+ ```
100
+
101
+ ### A custom set
102
+
103
+ ```tsx
104
+ import { ReactionPicker } from "@sableui/react"
105
+
106
+ <ReactionPicker
107
+ reactions={[
108
+ { emoji: "✅", label: "Done" },
109
+ { emoji: "👀", label: "Looking" },
110
+ { emoji: "🚀", label: "Shipped" },
111
+ ]}
112
+ selected={mine}
113
+ onSelect={toggle}
114
+ />
115
+ ```
@@ -2,7 +2,7 @@
2
2
 
3
3
  **Status:** stable
4
4
 
5
- Styled search field with a leading magnifier and a clearable X. Soft-square (rounded-sm), card surface, 2px focus ring; the clear button appears only when there is a value.
5
+ Styled search field with a leading magnifier and a clearable X. Soft-square (rounded-sm), the theme's field surface, 2px focus ring; the clear button appears only when there is a value.
6
6
 
7
7
  **Keywords:** search, input, filter, query
8
8
 
@@ -15,6 +15,7 @@ Use SearchInput for text queries that filter or search a known content set.
15
15
  - Do: Use a clear placeholder that names the content being searched.
16
16
  - Do: Provide a visible clear affordance when query state persists.
17
17
  - Do: When a button stands in for search (a header trigger that opens CommandMenu with ⌘K), use CommandMenuTrigger. It wears this field's shell from the same source, so the two cannot drift; a hand-styled button already did once, into a borderless chip that matched neither the field nor a button.
18
+ - Do: Light the query in the results it filters with `useMatchHighlight(query)` — a callback ref for the results container, with `selector` to keep it to titles. It paints on the `highlight` pair without wrapping matches in `<mark>`, the same paint Combobox and CommandMenu use for their rows.
18
19
  - Don't: Do not use SearchInput as a command palette; use CommandMenu for global commands, opened from the header by CommandMenuTrigger. A SearchInput that opens a dialog on focus is a text box you cannot type in.
19
20
  - Don't: Do not search on every keystroke against an expensive backend without debouncing. The user sees results flicker and reorder under them, and you send a request per character.
20
21
 
@@ -34,7 +35,7 @@ Use SearchInput for text queries that filter or search a known content set.
34
35
 
35
36
  ## Tokens used
36
37
 
37
- `input`, `card`, `foreground`, `muted-foreground`, `ring`, `destructive`
38
+ `field-border`, `field`, `foreground`, `muted-foreground`, `ring`, `destructive`
38
39
 
39
40
  ## Motion
40
41
 
@@ -82,6 +83,23 @@ import { Field, SearchInput } from "@sableui/react"
82
83
  <Field label="Search posts"><SearchInput defaultValue="roadmap" className="max-w-80" /></Field>
83
84
  ```
84
85
 
86
+ ### Highlighting matches in the results
87
+
88
+ ```tsx
89
+ import { SearchInput, useMatchHighlight } from "@sableui/react"
90
+
91
+ const [q, setQ] = useState("")
92
+ const results = useMatchHighlight<HTMLUListElement>(q, { selector: "[data-title]" })
93
+ const shown = posts.filter((post) => post.title.toLowerCase().includes(q.trim().toLowerCase()))
94
+
95
+ <SearchInput value={q} onValueChange={setQ} placeholder="Search posts" />
96
+ <ul ref={results}>
97
+ {shown.map((post) => (
98
+ <li key={post.id}><span data-title>{post.title}</span></li>
99
+ ))}
100
+ </ul>
101
+ ```
102
+
85
103
  ### Disabled
86
104
 
87
105
  ```tsx
@@ -40,7 +40,7 @@ Use SegmentedBar when a stat card or row needs to show how a total divides into
40
40
 
41
41
  ## Tokens used
42
42
 
43
- `tone-1`, `tone-2`, `tone-3`, `tone-info`, `primary`, `muted-foreground`, `success`, `tone-warning`, `destructive`
43
+ `tone-1`, `tone-2`, `tone-3`, `tone-info`, `primary`, `muted-foreground`, `success`, `tone-warning`, `destructive`, `destructive-mark`, `success-mark`, `tone-warning-mark`, `tone-info-mark`, `tone-1-mark`, `tone-2-mark`, `tone-3-mark`
44
44
 
45
45
  ## Motion
46
46
 
@@ -50,7 +50,7 @@ Use Select when users choose one value from a known, finite set. Use Combobox fo
50
50
 
51
51
  ## Tokens used
52
52
 
53
- `input`, `card`, `foreground`, `ring`, `border`, `popover`, `popover-foreground`, `accent`, `accent-foreground`, `muted-foreground`
53
+ `field-border`, `field`, `foreground`, `ring`, `border`, `popover`, `popover-foreground`, `accent`, `accent-foreground`, `muted-foreground`
54
54
 
55
55
  ## Motion
56
56
 
@@ -15,6 +15,7 @@ Use Sidebar for persistent desktop navigation or application chrome outside the
15
15
  - Do: Use it for stable app destinations that should remain visible while content changes.
16
16
  - Do: Keep item labels predictable and active state obvious.
17
17
  - Do: Set surface="page" when a full-width top bar sits above the rail. A theme's tinted or dark rail is for a full-height rail that frames the app.
18
+ - Do: Keep surface="rail" inside AppShell frame="inset": the rail's tint becomes the ground the main panel floats on. Do not add a border-r class back — the gap is the divider.
18
19
  - Don't: Do not use Sidebar for page-local settings, filters, or temporary action groups.
19
20
  - Don't: Do not put page content in it. It is fixed-width chrome OUTSIDE the content grid, so anything in it is exempt from the page's measure and rhythm.
20
21
 
@@ -40,7 +41,7 @@ Use Sidebar for persistent desktop navigation or application chrome outside the
40
41
  | collapsed | boolean | false | Collapse the rail to an icon-only width (--sidebar-width-collapsed). Items hide their label + badge and reveal the label in a hover tooltip; the content area reflows. Controlled — wire a toggle from app state. Read the state in custom header/footer content via useSidebar(). |
41
42
  | icons | boolean | true | Render the leading icon on each SidebarItem. Set false for a text-only rail. Ignored while collapsed (icon-only by definition). |
42
43
  | variant | "app" \| "inline" | "app" | Where the rail lives. "app" is the page's chrome column — sticky, full viewport height, hidden below lg where AppHeader + BottomNav take over. "inline" is always visible and fills its parent's height, for a rail inside a bounded frame (a nested shell, an embedded preview). Use it instead of overriding the layout with !important classes. |
43
- | surface | "rail" \| "page" | "rail" | Which surface the rail is painted on. "rail" is its own --sidebar-* surface with a border — right for a full-height rail that frames the app, which a theme may tint or paint dark. "page" uses the page's surface and ink with no border — use it when a full-width header sits above the rail, where a tinted column starting under a page-coloured bar reads as pasted on. Use it instead of overriding bg-/text- classes, which the rail's re-pointed tokens defeat. |
44
+ | surface | "rail" \| "page" | "rail" | Which surface the rail is painted on. "rail" is its own --sidebar-* surface with a border — right for a full-height rail that frames the app, which a theme may tint or paint dark. "page" uses the page's surface and ink with no border — use it when a full-width header sits above the rail, where a tinted column starting under a page-coloured bar reads as pasted on. Use it instead of overriding bg-/text- classes, which the rail's re-pointed tokens defeat. In AppShell's sidebar slot with frame="inset", the rail's surface is also the shell's ground: from lg a "rail" rail drops its border (the gap around the main panel divides them) and either surface stretches to the shell's body row instead of sizing to the viewport. AppShell signals this to its sidebar slot only, so a Sidebar elsewhere in the page is unaffected. |
44
45
  | motion | "full" \| "subtle" \| "off" | "full" | How much motion this component uses: `full` inherits the app's policy, `subtle` keeps feedback and drops the collapse ease and the current-page glide, `off` stills it. It can only turn motion down — it never overrides a reader's reduce-motion setting. |
45
46
 
46
47
  ## Variants
@@ -49,7 +50,7 @@ Use Sidebar for persistent desktop navigation or application chrome outside the
49
50
 
50
51
  ## Tokens used
51
52
 
52
- `sidebar`, `sidebar-foreground`, `sidebar-border`, `sidebar-accent`, `sidebar-accent-foreground`, `sidebar-ring`
53
+ `sidebar`, `sidebar-foreground`, `sidebar-border`, `sidebar-accent`, `sidebar-accent-foreground`, `sidebar-ring`, `shell-ground`
53
54
 
54
55
  ## Motion
55
56
 
@@ -36,7 +36,7 @@ Use Sparkline next to a number to show where it has been — a KPI's last 30 day
36
36
 
37
37
  ## Tokens used
38
38
 
39
- `primary`, `tone-1`, `tone-2`, `tone-3`, `tone-info`, `tone-warning`, `muted-foreground`, `success`, `destructive`
39
+ `primary`, `tone-1`, `tone-2`, `tone-3`, `tone-info`, `tone-warning`, `muted-foreground`, `success`, `destructive`, `destructive-mark`, `success-mark`, `tone-warning-mark`, `tone-info-mark`, `tone-1-mark`, `tone-2-mark`, `tone-3-mark`
40
40
 
41
41
  ## Motion
42
42
 
@@ -39,7 +39,7 @@ Use StatusDot as a compact visual state cue next to text, avatars, rows, or labe
39
39
 
40
40
  ## Tokens used
41
41
 
42
- `palette-dot`, `muted-foreground`, `tone-3`, `success`, `tone-warning`, `tone-info`, `tone-1`, `tone-2`, `destructive`
42
+ `palette-dot`, `muted-foreground`, `tone-3`, `success`, `tone-warning`, `tone-info`, `tone-1`, `tone-2`, `destructive`, `destructive-mark`, `success-mark`, `tone-warning-mark`, `tone-info-mark`, `tone-1-mark`, `tone-2-mark`, `tone-3-mark`
43
43
 
44
44
  ## Accessibility
45
45
 
@@ -51,7 +51,7 @@ Use Stepper above or beside a flow that is split into a known, small number of n
51
51
 
52
52
  ## Tokens used
53
53
 
54
- `primary`, `primary-foreground`, `background`, `foreground`, `muted-foreground`, `border-strong`, `destructive`, `destructive-foreground`, `ring`
54
+ `primary`, `primary-foreground`, `background`, `foreground`, `muted-foreground`, `border-strong`, `destructive`, `destructive-foreground`, `ring`, `destructive-mark`
55
55
 
56
56
  ## Motion
57
57
 
@@ -36,8 +36,8 @@ Use Table for structured data with consistent columns and rows. Use DataTable wh
36
36
  | TableBody | ComponentPropsWithRef<"tbody"> | The data row group (tbody). |
37
37
  | TableFooter | ComponentPropsWithRef<"tfoot"> | Summary row group (tfoot) with a muted fill. |
38
38
  | TableRow | ComponentPropsWithRef<"tr"> | A row (tr) — hairline bottom border, hover lift, and a selected state via the data-selected attribute. |
39
- | TableHead | ComponentPropsWithRef<"th"> | A column header cell (th) in muted label styling. |
40
- | TableCell | ComponentPropsWithRef<"td"> | A data cell (td). |
39
+ | TableHead | ComponentPropsWithRef<"th"> & { numeric?: boolean } | A column header cell (th) in muted label styling. `numeric` marks a number column: end-aligned, tabular figures — set it on the column's cells too. |
40
+ | TableCell | ComponentPropsWithRef<"td"> & { numeric?: boolean } | A data cell (td). `numeric` end-aligns it with tabular figures, matching its column's header. |
41
41
  | TableCaption | ComponentPropsWithRef<"caption"> | Accessible description of the table (caption), muted. |
42
42
 
43
43
  ## Props
@@ -2,7 +2,7 @@
2
2
 
3
3
  **Status:** stable
4
4
 
5
- Styled native multi-line text control. Soft-square, card surface, --ring focus, and an aria-invalid destructive error state.
5
+ Styled native multi-line text control. Soft-square, the theme's field surface, --ring focus, and an aria-invalid destructive error state.
6
6
 
7
7
  **Keywords:** textarea, multi line input, form, text
8
8
 
@@ -31,7 +31,7 @@ Use Textarea for multi-line freeform text entry such as notes, descriptions, or
31
31
 
32
32
  ## Tokens used
33
33
 
34
- `input`, `card`, `foreground`, `muted-foreground`, `ring`, `destructive`
34
+ `field-border`, `field`, `foreground`, `muted-foreground`, `ring`, `destructive`
35
35
 
36
36
  ## Accessibility
37
37
 
@@ -53,7 +53,7 @@ Use Toast for transient feedback caused by a user action: saved, failed, copied,
53
53
  | closeIcon | ReactNode | <Icon name="xmark" /> | Override the dismiss-button glyph. The default already follows the injected icon set. |
54
54
  | 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. |
55
55
  | 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. |
56
- | 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. |
56
+ | 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. |
57
57
 
58
58
  ## Tokens used
59
59
 
@@ -63,6 +63,7 @@ Use Toast for transient feedback caused by a user action: saved, failed, copied,
63
63
 
64
64
  - **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.
65
65
  - **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.
66
+ - **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.
66
67
  - **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.
67
68
  - **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.
68
69
  - **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.
@@ -30,6 +30,7 @@ Sable's components ship accessible behavior from Base UI. These rules cover what
30
30
  - **Never:** Color as the only signal (a red border with no message). **Instead:** Color plus text or an icon. Roughly 1 in 12 men cannot tell red from green.
31
31
  - **Never:** Opacity on the focus ring. **Instead:** ring-ring at full opacity; tune the token in the theme. A faded ring loses the contrast the theme tested. *Enforced by sable-lint: faded-focus-ring.*
32
32
  - **Never:** A brand or status fill retuned under its old label — dark ink on a bright red that passes 4.5:1 at APCA Lc 42. **Instead:** A label that clears 4.5:1 and APCA Lc 60 in both modes. Move the fill's lightness, not its hue, keeping the ink its colour takes. A status colour on a surface is its own token (`--destructive-text`), so the fill can move freely. The ratio alone passes labels that read muddy. A deliberate brand pair is allowed with a recorded reason (lint disable or preset exceptions). *Enforced by sable-lint: label-contrast.*
33
+ - **Never:** A label fill painted on a mark that carries no text — a meter, a segmented bar, a status dot, a chart series, a soft tint. **Instead:** The colour's `-mark` token (`bg-success-mark`, `bg-tone-warning-mark/15`): the same hue lifted and saturated, held to 2:1 on the page. Text contrast is for text; a graphic owes visibility, and 3:1 (WCAG 1.4.11) only when it is the sole carrier of its meaning. A fill is pinned dark enough for a white label. On a bar or dot with nothing to label, that darkness only reads as mud: a brown warning, a forest-green success.
33
34
  - **Never:** Blocking paste, or type="number" for codes and card numbers. **Instead:** type="text" inputMode="numeric" with the right autocomplete. Blocking paste breaks password managers; number inputs scroll-change identifiers.
34
35
  - **Never:** forced-color-adjust: none to 'fix' high-contrast mode. **Instead:** Let the system colors apply; verify the result. It removes the user's chosen colors exactly where they need them.
35
36
  - **Never:** outline: none without a replacement. **Instead:** focus-visible:ring-2 ring-ring. Keyboard users lose their place.
@@ -33,6 +33,8 @@ Reach for one named class before writing a transition.
33
33
  | `motion-appear` | fast fade + base scale | A small control mounting in place: a clear button, a send arrow, a badge. @starting-style, no JS. |
34
34
  | `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. |
35
35
  | `motion-size` | base · emphasized · expressive | An element easing between intrinsic sizes when its label changes: a two-step confirm, Copy → Copied. |
36
+ | `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. |
37
+ | `<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. |
36
38
  | `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. |
37
39
  | `motion-spin · pulse · shimmer · dot-bounce · ping` | loops | Activity. Scale-aware, so reduced motion freezes them. |
38
40
 
@@ -53,7 +55,7 @@ Essential motion answers the user; expressive motion narrates continuity. Expres
53
55
  | Name | Value | Use |
54
56
  |---|---|---|
55
57
  | `essential` | always, unless off | Colour and focus feedback, press, overlay arrival, disclosure height, loaders, chart hover. |
56
- | `expressive` | full only | Glides, rolling numbers (NumberTicker), drawn ticks, shakes, staggers, chart arrival and morph, scroll reveals, spring overshoot. |
58
+ | `expressive` | full only | Glides, rolling numbers (NumberTicker), drawn ticks, blooms and bursts, shakes, staggers, chart arrival and morph, scroll reveals, spring overshoot. |
57
59
  | `full` | default | Both levels play. The crafted default. |
58
60
  | `subtle` | data-motion="subtle" | Essential only; nothing overshoots. For dense tools, embedded widgets, readers who want calm without stillness. |
59
61
  | `off` | data-motion="off" · reduce-motion | Nothing moves; every state still changes. |
@@ -82,7 +84,10 @@ Every interactive component answers all four, by filling the slot or declining i
82
84
  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.
83
85
  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.
84
86
  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.
85
- 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.
87
+ 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.
88
+ 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.
89
+ 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.
90
+ 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.
86
91
 
87
92
  ## Never
88
93
 
@@ -127,6 +132,17 @@ The underline used to vanish under one tab and appear under the next. Now it sli
127
132
 
128
133
  The digits that changed roll; the ones that did not stay still, so the eye goes straight to the difference.
129
134
 
135
+ ### A glyph that lands
136
+
137
+ ```tsx
138
+ // before
139
+ <Icon name="heart" variant={liked ? "solid" : "regular"} />
140
+ // after
141
+ <LikeButton count={likes} defaultPressed={liked} />
142
+ ```
143
+
144
+ 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.
145
+
130
146
  ### Faster exits
131
147
 
132
148
  ```tsx