@sableui/react 0.13.2 → 0.15.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 (203) hide show
  1. package/AGENTS.md +2 -2
  2. package/CHANGELOG.md +172 -0
  3. package/CONTRIBUTING.md +6 -4
  4. package/DESIGN.md +10 -3
  5. package/dist/blocks/approval-block.js +93 -0
  6. package/dist/blocks/approval-block.meta.js +23 -0
  7. package/dist/blocks/index.d.ts +27 -1
  8. package/dist/blocks/index.js +8 -0
  9. package/dist/blocks/message-reactions-block.js +87 -0
  10. package/dist/blocks/message-reactions-block.meta.js +23 -0
  11. package/dist/components/accordion/accordion.meta.js +8 -2
  12. package/dist/components/area-chart/area-chart.meta.js +10 -3
  13. package/dist/components/attachment/attachment.meta.js +26 -0
  14. package/dist/components/bar-chart/bar-chart.meta.js +11 -4
  15. package/dist/components/bottom-nav/bottom-nav.js +3 -1
  16. package/dist/components/bottom-nav/bottom-nav.meta.js +32 -0
  17. package/dist/components/breadcrumbs/breadcrumbs.meta.js +26 -0
  18. package/dist/components/bubble/bubble.js +105 -5
  19. package/dist/components/bubble/bubble.meta.js +42 -6
  20. package/dist/components/bubble/bubble.variants.js +1 -1
  21. package/dist/components/bubble/index.js +2 -0
  22. package/dist/components/button/button.js +21 -4
  23. package/dist/components/button/button.meta.js +7 -7
  24. package/dist/components/calendar/calendar.js +18 -1
  25. package/dist/components/calendar/calendar.meta.js +38 -0
  26. package/dist/components/carousel/carousel.js +6 -4
  27. package/dist/components/carousel/carousel.meta.js +42 -0
  28. package/dist/components/chat-composer/chat-composer.js +21 -21
  29. package/dist/components/chat-composer/chat-composer.meta.js +38 -0
  30. package/dist/components/chat-composer/use-eased-growth.js +70 -0
  31. package/dist/components/checkbox/checkbox-mark.js +8 -1
  32. package/dist/components/checkbox/checkbox.js +3 -1
  33. package/dist/components/checkbox/checkbox.meta.js +32 -0
  34. package/dist/components/checkbox-card/checkbox-card.js +3 -0
  35. package/dist/components/checkbox-card/checkbox-card.meta.js +32 -0
  36. package/dist/components/code-block/code-block.js +14 -3
  37. package/dist/components/code-block/code-block.meta.js +28 -1
  38. package/dist/components/collapsible/collapsible.meta.js +32 -0
  39. package/dist/components/color-field/color-field.meta.js +26 -0
  40. package/dist/components/combobox/combobox.meta.js +44 -0
  41. package/dist/components/command-menu/command-menu.js +8 -3
  42. package/dist/components/command-menu/command-menu.meta.js +38 -0
  43. package/dist/components/confirm-button/confirm-button.js +179 -0
  44. package/dist/components/confirm-button/confirm-button.meta.js +143 -0
  45. package/dist/components/confirm-button/index.js +7 -0
  46. package/dist/components/data-table/data-table.js +6 -2
  47. package/dist/components/data-table/data-table.meta.js +32 -0
  48. package/dist/components/entity/entity.meta.js +26 -0
  49. package/dist/components/feedback/feedback.js +120 -47
  50. package/dist/components/feedback/feedback.meta.js +55 -4
  51. package/dist/components/input-group/input-group.meta.js +27 -1
  52. package/dist/components/like-button/index.js +7 -0
  53. package/dist/components/like-button/like-button.js +89 -0
  54. package/dist/components/like-button/like-button.meta.js +132 -0
  55. package/dist/components/line-chart/line-chart.meta.js +10 -3
  56. package/dist/components/middle-truncate/middle-truncate.meta.js +1 -1
  57. package/dist/components/note/note.meta.js +32 -0
  58. package/dist/components/notification-badge/notification-badge.js +17 -5
  59. package/dist/components/notification-badge/notification-badge.meta.js +34 -2
  60. package/dist/components/number-ticker/index.js +7 -0
  61. package/dist/components/number-ticker/number-ticker.js +246 -0
  62. package/dist/components/number-ticker/number-ticker.meta.js +159 -0
  63. package/dist/components/otp-field/otp-field.js +23 -3
  64. package/dist/components/otp-field/otp-field.meta.js +23 -2
  65. package/dist/components/panel-group/panel-group.meta.js +26 -0
  66. package/dist/components/panel-section/panel-section.js +37 -10
  67. package/dist/components/panel-section/panel-section.meta.js +32 -0
  68. package/dist/components/pie-chart/pie-chart.js +20 -11
  69. package/dist/components/pie-chart/pie-chart.meta.js +17 -4
  70. package/dist/components/pill/pill.meta.js +32 -0
  71. package/dist/components/radar-chart/radar-chart.js +4 -3
  72. package/dist/components/radar-chart/radar-chart.meta.js +10 -3
  73. package/dist/components/radial-chart/radial-chart.js +22 -22
  74. package/dist/components/radial-chart/radial-chart.meta.js +17 -4
  75. package/dist/components/radio/radio.meta.js +26 -0
  76. package/dist/components/radio-card/radio-card.meta.js +38 -0
  77. package/dist/components/reaction-picker/index.js +13 -0
  78. package/dist/components/reaction-picker/reaction-picker.js +104 -0
  79. package/dist/components/reaction-picker/reaction-picker.meta.js +156 -0
  80. package/dist/components/relative-time/relative-time.meta.js +32 -0
  81. package/dist/components/scroller/scroller.meta.js +32 -0
  82. package/dist/components/scrub-field/scrub-field.meta.js +32 -0
  83. package/dist/components/search-input/search-input.js +9 -7
  84. package/dist/components/search-input/search-input.meta.js +26 -0
  85. package/dist/components/show-more/show-more.js +1 -4
  86. package/dist/components/show-more/show-more.meta.js +38 -0
  87. package/dist/components/side-nav/side-nav.js +21 -1
  88. package/dist/components/side-nav/side-nav.meta.js +32 -0
  89. package/dist/components/side-nav/side-nav.variants.js +3 -1
  90. package/dist/components/sidebar/sidebar.js +31 -8
  91. package/dist/components/sidebar/sidebar.meta.js +38 -0
  92. package/dist/components/slider/slider.meta.js +26 -0
  93. package/dist/components/sparkline/sparkline.js +7 -4
  94. package/dist/components/sparkline/sparkline.meta.js +9 -2
  95. package/dist/components/table/table.meta.js +26 -0
  96. package/dist/components/tabs/tabs.js +27 -8
  97. package/dist/components/tabs/tabs.meta.js +46 -3
  98. package/dist/components/theme-selector/theme-selector.meta.js +32 -0
  99. package/dist/components/theme-switcher/theme-switcher.js +4 -1
  100. package/dist/components/theme-switcher/theme-switcher.meta.js +42 -0
  101. package/dist/components/toast/toast.js +25 -13
  102. package/dist/components/toast/toast.meta.js +7 -1
  103. package/dist/components/toggle/toggle.js +28 -5
  104. package/dist/components/toggle/toggle.meta.js +44 -2
  105. package/dist/components/tool-call/index.js +8 -0
  106. package/dist/components/tool-call/tool-call.js +117 -0
  107. package/dist/components/tool-call/tool-call.meta.js +147 -0
  108. package/dist/components/tool-call/tool-call.variants.js +33 -0
  109. package/dist/components/tooltip/index.js +3 -1
  110. package/dist/components/tooltip/tooltip.js +84 -0
  111. package/dist/components/tooltip/tooltip.meta.js +16 -1
  112. package/dist/components/tree/tree.js +71 -6
  113. package/dist/components/tree/tree.meta.js +32 -0
  114. package/dist/components/video/video.meta.js +32 -0
  115. package/dist/file-hashes.json +130 -104
  116. package/dist/foundations/motion.js +69 -3
  117. package/dist/icon-sets/reicon.js +11 -1
  118. package/dist/index.d.ts +907 -104
  119. package/dist/index.js +6 -1
  120. package/dist/lib/chart/cartesian.js +9 -10
  121. package/dist/lib/chart/chart-center.js +30 -0
  122. package/dist/lib/chart/chart-frame.js +15 -8
  123. package/dist/lib/chart/dim.js +13 -0
  124. package/dist/lib/chart/format.js +7 -1
  125. package/dist/lib/chart/motion.js +6 -4
  126. package/dist/lib/highlight-glide.js +68 -10
  127. package/dist/lib/taxonomy.js +5 -0
  128. package/dist/motion/Burst.js +79 -0
  129. package/dist/motion/MotionProvider.js +45 -3
  130. package/dist/motion/Reveal.js +1 -1
  131. package/dist/motion/TextReveal.js +1 -1
  132. package/dist/motion/index.js +17 -2
  133. package/dist/motion/useAutoSize.js +42 -0
  134. package/dist/motion/useShake.js +25 -0
  135. package/dist/styles.css +439 -5
  136. package/llms/blocks/approval-block.md +128 -0
  137. package/llms/blocks/message-reactions-block.md +116 -0
  138. package/llms/components/accordion.md +3 -2
  139. package/llms/components/area-chart.md +4 -3
  140. package/llms/components/attachment.md +7 -0
  141. package/llms/components/bar-chart.md +5 -4
  142. package/llms/components/bottom-nav.md +8 -0
  143. package/llms/components/breadcrumbs.md +7 -0
  144. package/llms/components/bubble.md +17 -7
  145. package/llms/components/button.md +6 -6
  146. package/llms/components/calendar.md +9 -0
  147. package/llms/components/carousel.md +17 -0
  148. package/llms/components/chat-composer.md +9 -0
  149. package/llms/components/checkbox-card.md +8 -0
  150. package/llms/components/checkbox.md +8 -0
  151. package/llms/components/code-block.md +8 -0
  152. package/llms/components/collapsible.md +8 -0
  153. package/llms/components/color-field.md +7 -0
  154. package/llms/components/combobox.md +10 -0
  155. package/llms/components/command-menu.md +9 -0
  156. package/llms/components/confirm-button.md +92 -0
  157. package/llms/components/data-table.md +8 -0
  158. package/llms/components/entity.md +7 -0
  159. package/llms/components/feedback.md +23 -3
  160. package/llms/components/input-group.md +8 -1
  161. package/llms/components/like-button.md +91 -0
  162. package/llms/components/line-chart.md +4 -3
  163. package/llms/components/middle-truncate.md +1 -1
  164. package/llms/components/note.md +8 -0
  165. package/llms/components/notification-badge.md +10 -2
  166. package/llms/components/number-ticker.md +86 -0
  167. package/llms/components/otp-field.md +15 -1
  168. package/llms/components/panel-group.md +7 -0
  169. package/llms/components/panel-section.md +8 -0
  170. package/llms/components/pie-chart.md +6 -4
  171. package/llms/components/pill.md +8 -0
  172. package/llms/components/radar-chart.md +4 -3
  173. package/llms/components/radial-chart.md +6 -4
  174. package/llms/components/radio-card.md +9 -0
  175. package/llms/components/radio.md +7 -0
  176. package/llms/components/reaction-picker.md +115 -0
  177. package/llms/components/relative-time.md +8 -0
  178. package/llms/components/scroller.md +8 -0
  179. package/llms/components/scrub-field.md +8 -0
  180. package/llms/components/search-input.md +7 -0
  181. package/llms/components/show-more.md +9 -0
  182. package/llms/components/side-nav.md +8 -0
  183. package/llms/components/sidebar.md +9 -0
  184. package/llms/components/slider.md +7 -0
  185. package/llms/components/sparkline.md +3 -2
  186. package/llms/components/table.md +7 -0
  187. package/llms/components/tabs.md +20 -2
  188. package/llms/components/theme-selector.md +8 -0
  189. package/llms/components/theme-switcher.md +17 -0
  190. package/llms/components/toast.md +2 -1
  191. package/llms/components/toggle.md +19 -2
  192. package/llms/components/tool-call.md +115 -0
  193. package/llms/components/tooltip.md +11 -1
  194. package/llms/components/tree.md +8 -0
  195. package/llms/components/video.md +8 -0
  196. package/llms/foundations/motion.md +64 -3
  197. package/llms/reference/utilities.md +26 -1
  198. package/llms/tasks/reviewing-motion.md +14 -0
  199. package/llms-full.txt +1337 -69
  200. package/llms.txt +19 -11
  201. package/package.json +1 -1
  202. package/registry.json +3040 -124
  203. package/targets/ios.json +45 -0
@@ -0,0 +1,92 @@
1
+ # ConfirmButton
2
+
3
+ **Status:** stable
4
+
5
+ An inline two-step confirm for a low-stakes destructive or irreversible action: the first press arms it ("Delete" → "Confirm delete?", destructive), a second press within a few seconds runs it, and leaving it disarms it.
6
+
7
+ **Keywords:** confirm, confirmation, two-step, press again, click again, arm, delete, remove, destructive, inline confirm, hold to confirm
8
+
9
+ ## Usage
10
+
11
+ Use ConfirmButton when an action deserves one beat of second thought but not a modal: deleting a draft, removing a row, revoking one token, clearing a list that can be rebuilt. The question appears on the button the user is already pressing, so confirming costs one more press in the same place and backing out costs nothing.
12
+
13
+ ### Best practices
14
+
15
+ - Do: Keep the idle label a plain verb ("Delete", "Remove", "Revoke") and let the confirm label ask about it ("Confirm delete?" by default, or `confirmLabel`). The user should read the consequence on the very control they are about to press again.
16
+ - Do: Leave room for it to grow. The button eases wider to fit the question, so give it space at the end of a row or in a table's action column rather than wedging it between fixed-width neighbours.
17
+ - Do: Return the promise from `onConfirm` when the action is async. The button stays armed with a spinner until it settles and refuses further presses, so a slow network cannot run it twice.
18
+ - Don't: Do not use it for high-stakes or wide-reaching actions — deleting a project, an account, anything other people depend on. Those need the consequence spelled out and a typed or deliberate confirmation; use DestructiveActionModal. → use **DestructiveActionModal**
19
+ - Don't: Do not use it where an undo is possible. If the action can be reversed, run it on the first press and offer Undo in a Toast — no question at all is faster than a good question. → use **Toast**
20
+ - Don't: Do not use it for a non-destructive primary action ("Save", "Send"). Asking twice for something harmless teaches people to press twice without reading, which is what defeats the confirm when it matters; use Button. → use **Button**
21
+
22
+ ## Props
23
+
24
+ | Name | Type | Default | Description |
25
+ | --- | --- | --- | --- |
26
+ | children | ReactNode | — | The idle label — the action, e.g. "Delete". |
27
+ | confirmLabel | ReactNode | "Confirm <label>?" | The label while armed. Defaults to "Confirm delete?" for a text label "Delete" (first letter lowered) and "Confirm?" otherwise. Pass it to localise. |
28
+ | onConfirm | () => unknown | — | Runs on the second press while armed. Return a promise to hold the button armed with a spinner until it settles. |
29
+ | timeout | number | 3000 | How long it waits for the second press, in ms. A decision time, so it is not scaled by motion settings. |
30
+ | variant | ButtonVariant | "secondary" | The idle look — any Button variant. |
31
+ | confirmVariant | ButtonVariant | "destructive" | The armed look — any Button variant. |
32
+ | size | "xs" \| "sm" \| "md" \| "cta" | "md" | A labelled Button size. Icon-only sizes are excluded: the label is the interaction. |
33
+ | icon | ReactNode | — | A leading icon kept through both states, e.g. `<Icon name="trash" />`. |
34
+ | disabled | boolean | false | Disables it; disabling while armed also disarms. |
35
+ | confirmHint | string | "Press again to confirm." | Read to assistive tech after the armed label when it arms. Override to localise. |
36
+ | 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 width easing and the draining countdown are the flourish; the label cross-fade and colour change stay. |
37
+
38
+ ## Variants
39
+
40
+ - **variant**: primary, secondary, ghost, outline, destructive, link, floating, field, on-color, on-color-outline, on-color-ghost
41
+ - **confirmVariant**: destructive, primary, secondary, outline
42
+ - **size**: xs, sm, md, cta
43
+
44
+ ## Tokens used
45
+
46
+ `secondary`, `secondary-foreground`, `destructive`, `destructive-foreground`, `ring`
47
+
48
+ ## Motion
49
+
50
+ - **arrival** · Root · _no motion_ — It is a button in a row of actions, and arrives with that row. Its moment is the arming, not the mount.
51
+ - **response** · Root · `motion-press` — Inherited from Button with motion-interactive. The press settle confirms the first press landed even before the label has finished changing — which is what stops a second, accidental press.
52
+ - **transition** · Root · `motion-size` — Arming changes the label, so the button's width changes. Easing it makes the button visibly grow into the question and its neighbours travel, instead of the row jumping under the pointer that is about to press again. useAutoSize measures the content and writes a px width for motion-size to ease between (a content-sized box is width:auto before and after, which cannot transition). The labels cross-fade in one cell so there is never a frame with both or neither.
53
+ - **activity** · Countdown · _custom (outside the recipe layer)_ — While armed, a thin line drains along the foot of the button over the timeout — the only way to see that the question expires. A transition on scale, timed by the `timeout` prop and multiplied by the motion scale and expressive factor, so under reduced motion or `subtle` it is absent rather than frozen full.
54
+
55
+ ## Accessibility
56
+
57
+ - A native <button> through Button. Focus stays on it through both presses; Enter and Space confirm exactly as a click does.
58
+ - Only the current label is in the accessible name — the other is aria-hidden — so the name changes to the question while armed.
59
+ - Arming is also announced through a polite role=status region outside the button ("Confirm delete? Press again to confirm."), because a focused button's name change is not read by every screen reader.
60
+ - Escape disarms and is consumed, so it does not also close the dialog or popover around it. Focus leaving, a press elsewhere, or the timeout disarm it too.
61
+ - A second press within 300ms of arming is ignored: a double-click is a habit, not a decision.
62
+ - Under reduced motion the label still changes and the colour still turns; only the width easing and countdown are dropped.
63
+
64
+ ## Related
65
+
66
+ - Components: Button, DestructiveActionModal, Toast, OverflowMenu
67
+
68
+ ## Examples
69
+
70
+ ### Delete a row
71
+
72
+ ```tsx
73
+ import { ConfirmButton, Icon } from "@sableui/react"
74
+
75
+ <ConfirmButton icon={<Icon name="trash" />} onConfirm={() => removeRow(id)}>Delete</ConfirmButton>
76
+ ```
77
+
78
+ ### Custom question, longer wait
79
+
80
+ ```tsx
81
+ import { ConfirmButton } from "@sableui/react"
82
+
83
+ <ConfirmButton variant="ghost" size="sm" confirmLabel="Revoke this key?" timeout={5000} onConfirm={revokeKey}>Revoke</ConfirmButton>
84
+ ```
85
+
86
+ ### Async confirm
87
+
88
+ ```tsx
89
+ import { ConfirmButton } from "@sableui/react"
90
+
91
+ <ConfirmButton onConfirm={() => api.deleteDraft(draft.id)}>Discard draft</ConfirmButton>
92
+ ```
@@ -50,6 +50,14 @@ Use DataTable when a data array and column definition should render a sortable,
50
50
 
51
51
  `muted-foreground`, `foreground`, `ring`
52
52
 
53
+ ## Motion
54
+
55
+ - **arrival** · Root · _no motion_ — Rows render with the page or the query that produced them. Staggering rows in delays reading a table someone opened to scan.
56
+ - **response** · Row · `motion-interactive` — Body rows ease to a muted wash under the pointer (TableRow's recipe), which keeps the eye on the right line across a wide row of numbers. Sortable headers are ghost Buttons and answer on Button's recipe.
57
+ - **transition** · Sort chevron · `motion-interactive` — One chevron turns to show direction instead of two glyphs swapping, so ascending to descending reads as the same column reversing. Its opacity rises from the resting 60% as the column becomes the sort.
58
+ - **transition** · Row · `motion-interactive` — A checked row's fill eases in across its cells, so selecting by checkbox reads as the row being picked rather than a band stamped over it.
59
+ - **activity** · Root · _no motion_ — DataTable has no loading state. While data is fetched, render Skeleton rows (motion-shimmer) in its place — only the caller knows a fetch is in flight.
60
+
53
61
  ## Accessibility
54
62
 
55
63
  - Sorted columns set aria-sort=ascending/descending on the header cell.
@@ -39,6 +39,13 @@ Use Entity to show a compact identity block for a person, organization, file, pr
39
39
 
40
40
  `accent`, `accent-foreground`, `foreground`, `muted-foreground`, `ring`
41
41
 
42
+ ## Motion
43
+
44
+ - **arrival** · Root · _no motion_ — Rows arrive with the list they belong to. Entering one at a time is a stagger, and a stagger on a list someone opened to scan is a delay.
45
+ - **response** · Root · `motion-interactive` — Only the interactive row reacts: its hover fill, a step off the surface it sits on, eases in. A static row never fakes clickability, and an interactive one answers the pointer without flicker as it moves down a list.
46
+ - **transition** · Root · `motion-interactive` — `selected` eases the fill onto the current row of a list–detail view, so opening another record reads as the selection moving rather than two rows flashing.
47
+ - **activity** · Root · _no motion_ — A record row has nothing of its own to wait for. A row whose action is pending puts a loading Button in `action`.
48
+
42
49
  ## Accessibility
43
50
 
44
51
  - Default rows are presentational layout; semantics come from the content you place in title/subtitle/action.
@@ -2,7 +2,7 @@
2
2
 
3
3
  **Status:** stable
4
4
 
5
- A compact feedback popover. Composes Popover + Textarea + Button + Toggle into a rating row plus a message field; submitting reports the rating + message and closes the popover.
5
+ A compact feedback popover. Composes Popover + Textarea + Button + Toggle into a rating row plus a message field; submitting reports the rating + message, shows a brief drawn-check thanks, and closes the popover.
6
6
 
7
7
  **Keywords:** feedback, sentiment, rating, prompt
8
8
 
@@ -26,11 +26,21 @@ Use Feedback to collect lightweight sentiment, usefulness, or quality signals fr
26
26
  | placeholder | string | "What can we improve?" | Placeholder for the message field. |
27
27
  | submitLabel | string | "Send" | Submit button text. |
28
28
  | ratings | FeedbackRating[] | 🙁 / 😐 / 🙂 | The rating options shown as a row of toggles. Each is { value, label }. |
29
- | onSubmit | (data: { rating?: string; message: string }) => void | — | Called with the selected rating + message when the form is submitted. |
29
+ | onSubmit | (data: { rating?: string; message: string }) => void | — | Called with the selected rating + message when the form is submitted. The thanks shows right away; send the report in the background. |
30
+ | successMessage | string | "Thanks for the feedback." | The line shown, and read out, for about 1.2s after sending, before the popover closes on its own. |
31
+ | 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 drawn check is the flourish; the thanks line and its dwell stay. |
30
32
 
31
33
  ## Tokens used
32
34
 
33
- `popover`, `popover-foreground`, `border`, `input`, `card`, `foreground`, `muted-foreground`, `accent`, `accent-foreground`, `ring`
35
+ `popover`, `popover-foreground`, `border`, `input`, `card`, `foreground`, `muted-foreground`, `accent`, `accent-foreground`, `ring`, `success`
36
+
37
+ ## Motion
38
+
39
+ - **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.
40
+ - **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.
41
+ - **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.
42
+ - **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.
43
+ - **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.
34
44
 
35
45
  ## Accessibility
36
46
 
@@ -39,6 +49,8 @@ Use Feedback to collect lightweight sentiment, usefulness, or quality signals fr
39
49
  - The form is named by its heading (aria-labelledby), so it is announced as "Send feedback" rather than an unnamed form.
40
50
  - The message field carries an aria-label matching its placeholder.
41
51
  - Submit is disabled until there is a rating or a non-whitespace message. Either alone is a complete answer.
52
+ - On send, focus moves to the thanks line (tabIndex=-1), which reads it out, and the form becomes inert so focus cannot land in invisible fields. The line is not also a live region, which read it twice. When the popover closes, Base UI returns focus to the trigger.
53
+ - The thanks dwell (about 1.2s) is a reading time, not motion, so it is the same under reduced motion.
42
54
 
43
55
  ## Related
44
56
 
@@ -61,3 +73,11 @@ import { Feedback } from "@sableui/react"
61
73
 
62
74
  <Feedback label="Tell us" submitLabel="Send it" placeholder="Describe the issue…" />
63
75
  ```
76
+
77
+ ### Custom thanks
78
+
79
+ ```tsx
80
+ import { Feedback } from "@sableui/react"
81
+
82
+ <Feedback successMessage="Got it — thank you." onSubmit={({ rating, message }) => report(rating, message)} />
83
+ ```
@@ -32,6 +32,13 @@ Use InputGroup when something belongs inside the field with the text: a unit, a
32
32
 
33
33
  `input`, `card`, `ring`, `destructive`, `muted-foreground`, `foreground`
34
34
 
35
+ ## Motion
36
+
37
+ - **arrival** · Root · _no motion_ — It arrives with the form it belongs to. A field that animated in on its own would be briefly unusable for no stated reason.
38
+ - **response** · Root · `motion-interactive` — The shell owns one focus ring for the input and its addons and eases it in, so focusing the text, or tabbing to a copy button inside the field, reads as the one field taking focus.
39
+ - **transition** · Root · `motion-interactive` — An invalid value turns the shell's edge destructive on the same ease, so the error lands as the field changing state rather than a red box swapping in.
40
+ - **activity** · Root · _no motion_ — A field does no work of its own. A pending check (an availability lookup) puts a Spinner in `trailing`, which is where its answer will appear.
41
+
35
42
  ## Accessibility
36
43
 
37
44
  - The Input stays the focusable control; the group draws the focus ring around the whole field while it is focused.
@@ -66,7 +73,7 @@ import { Field, Icon, Input, InputGroup } from "@sableui/react"
66
73
  ```tsx
67
74
  import { Button, Field, Icon, Input, InputGroup } from "@sableui/react"
68
75
 
69
- <Field label="API key"><InputGroup trailing={<Button variant="ghost" size="icon-sm" aria-label="Copy key" onClick={copy}><Icon name={copied ? "check" : "copy"} /></Button>}><Input readOnly value={key} /></InputGroup></Field>
76
+ <Field label="API key"><InputGroup trailing={<Button variant="ghost" size="icon-sm" aria-label="Copy key" onClick={copy}><span aria-hidden="true" className="motion-icon-swap"><span data-active={!copied} className="inline-flex"><Icon name="copy" /></span><span data-active={copied} className="inline-flex"><Icon name="check" /></span></span></Button>}><Input readOnly value={key} /></InputGroup></Field>
70
77
  ```
71
78
 
72
79
  ### Password with reveal
@@ -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
+ ```
@@ -34,7 +34,8 @@ Use LineChart to compare how two or more measures change over the same sequence,
34
34
  | grid | boolean | true (false for horizontal bars) | Hairline gridlines along the value axis. |
35
35
  | xAxis | boolean | true for columns; false for horizontal bars | The horizontal axis. |
36
36
  | yAxis | boolean | false for columns; true for horizontal bars | The vertical axis. |
37
- | animate | boolean | true | Play the arrival once the chart scrolls into view. Reduced motion shows the finished chart. |
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 arrival (lines drawing on) and the data-change morph are the flourish; the gliding cursor, dots and tooltip are the feedback `subtle` keeps. |
38
+ | animate | boolean | true | Play the arrival once the chart scrolls into view. Reduced motion shows the finished chart. `animate={false}` is `motion="subtle"`. |
38
39
  | activeIndex | number \| null | — | The highlighted row (controlled); `null` for none. |
39
40
  | defaultActiveIndex | number \| null | — | The row highlighted at first — a tooltip already open before anyone hovers. |
40
41
  | onActiveIndexChange | (index: number \| null) => void | — | Called as the pointer or keyboard moves between rows. |
@@ -49,9 +50,9 @@ Use LineChart to compare how two or more measures change over the same sequence,
49
50
 
50
51
  ## Motion
51
52
 
52
- - **arrival** · Line · _custom (outside the recipe layer)_ — Once on screen each line draws on from the first point (stroke-dashoffset), series staggered, then the dots and labels fade in. On the motion tokens; reduced motion shows the finished chart.
53
+ - **arrival** · Line · _custom (outside the recipe layer)_ — Once on screen each line draws on from the first point (stroke-dashoffset), series staggered, then the dots and labels fade in. On the motion tokens; reduced motion shows the finished chart. The arrival is expressive: it plays at the full motion level only, so `motion="subtle"` (or a subtle app policy) shows the finished chart while hover feedback stays.
53
54
  - **response** · Cursor + tooltip · _no motion_ — Hover and focus move the cursor and tooltip instantly — a lagging cursor reads as a slow chart.
54
- - **transition** · Line + dots · _custom (outside the recipe layer)_ — Geometry eases from the old shape to the new one through a CSS `d` transition on the motion tokens (no recipe animates path geometry); browsers without `d` transitions update in place.
55
+ - **transition** · Line + dots · _custom (outside the recipe layer)_ — Geometry eases from the old shape to the new one through a CSS `d` transition on the motion tokens (no recipe animates path geometry); browsers without `d` transitions update in place. Expressive, like the arrival: under `subtle` new data swaps in place.
55
56
  - **activity** · LineChart · _no motion_ — A chart has no indeterminate state; show a Skeleton while the data loads.
56
57
 
57
58
  ## Accessibility
@@ -58,5 +58,5 @@ import { MiddleTruncate } from "@sableui/react"
58
58
  import { Button, Icon, MiddleTruncate, Row, useCopy } from "@sableui/react"
59
59
 
60
60
  const { copy, copied } = useCopy(commitHash)
61
- <Row gap={1}><MiddleTruncate text={commitHash} /><Button variant="ghost" size="icon-xs" aria-label="Copy commit hash" onClick={copy}><Icon name={copied ? "check" : "copy"} /></Button></Row>
61
+ <Row gap={1}><MiddleTruncate text={commitHash} /><Button variant="ghost" size="icon-xs" aria-label="Copy commit hash" onClick={copy}><span aria-hidden="true" className="motion-icon-swap"><span data-active={!copied} className="inline-flex"><Icon name="copy" /></span><span data-active={copied} className="inline-flex"><Icon name="check" /></span></span></Button><span role="status" className="sr-only">{copied ? "Copied" : ""}</span></Row>
62
62
  ```
@@ -37,6 +37,14 @@ Use Note for helpful supporting information that should sit near the relevant co
37
37
 
38
38
  `foreground`, `border`, `muted`, `muted-foreground`, `tone-info`, `tone-warning`, `success`, `destructive`
39
39
 
40
+ ## Motion
41
+
42
+ - **arrival** · Root · _no motion_ — A note is part of the content it annotates and renders with it. A callout that animated in would pull the eye before the reader has reached it in the flow.
43
+ - **response** · Action · `motion-interactive` — The action's focus ring eases in, and its underline thickens on hover. Inside a block of guidance, that is what separates the one thing to do from the prose around it.
44
+ - **response** · Action · `motion-press` — A note's action usually navigates or opens something; the small settle on press is the only confirmation before the next surface paints.
45
+ - **transition** · Root · _no motion_ — A note has no states to move between: its tone is fixed when it renders. A note that changes tone is a new message, and should arrive as one.
46
+ - **activity** · Root · _no motion_ — Static guidance. Work in progress belongs in a Progress or a Spinner, not in a callout.
47
+
40
48
  ## Accessibility
41
49
 
42
50
  - Rendered with role="note" so assistive tech can announce it as an aside.
@@ -2,7 +2,7 @@
2
2
 
3
3
  **Status:** stable
4
4
 
5
- An unread indicator. Renders a bare dot or a count pill (capped as "{max}+"), or nothing when there's nothing to show. overlay pins it to the corner of a relative host; it is aria-hidden, so the host's name carries the count.
5
+ An unread indicator. Renders a bare dot or a count pill (capped as "{max}+"), or nothing when there's nothing to show. A changing count rolls to its new number, and a badge that appears after being empty pops in. overlay pins it to the corner of a relative host; it is aria-hidden, so the host's name carries the count.
6
6
 
7
7
  **Keywords:** notification, badge, count, unread
8
8
 
@@ -28,6 +28,7 @@ Use NotificationBadge to show a compact unread count, alert count, or attention
28
28
  | tone | "destructive" \| "primary" \| "neutral" | "destructive" | Fill color. destructive for alarming counts; primary/neutral for non-alarming ones. |
29
29
  | size | "sm" \| "md" \| "lg" | "md" | Scales the dot/pill on the spacing scale (count pill: sm 16px, md 18px, lg 20px at the default density). |
30
30
  | overlay | boolean | false | Pin to the top-end corner of the nearest relative host (an icon button) — one placement for every caller instead of hand-tuned absolute offsets. |
31
+ | 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. Under `subtle` the count updates in place and the badge fades in without the pop. |
31
32
 
32
33
  ## Variants
33
34
 
@@ -39,6 +40,13 @@ Use NotificationBadge to show a compact unread count, alert count, or attention
39
40
 
40
41
  `destructive`, `destructive-foreground`, `primary`, `primary-foreground`, `secondary`, `secondary-foreground`
41
42
 
43
+ ## Motion
44
+
45
+ - **arrival** · root · `motion-appear` — A badge that shows up after being empty is the news — something just arrived — so it fades and scales in where it sits instead of popping into existence. Never on first paint: a badge that mounts with a count is content, and a page of them popping on load is noise.
46
+ - **response** · root · _no motion_ — It is decorative and not interactive; the host it sits on answers the pointer.
47
+ - **transition** · Count · _custom (outside the recipe layer)_ — The count rolls to its new number through NumberTicker (expressive), so 3 → 4 reads as one more arriving rather than a different badge. Past the cap the ticker holds at max with a static "+", so a count climbing beyond 99 does not keep rolling a badge that has not changed.
48
+ - **activity** · root · _no motion_ — An unread count has no working state.
49
+
42
50
  ## Accessibility
43
51
 
44
52
  - The badge is aria-hidden: a count rendered on a control is not associated with it, so the host needs an accessible name that includes the count — "Notifications, 3 unread" — or a screen-reader user hears "Notifications, button" and never learns there is anything new. Pass aria-hidden={false} only for a standalone badge whose number is the information.
@@ -46,7 +54,7 @@ Use NotificationBadge to show a compact unread count, alert count, or attention
46
54
 
47
55
  ## Related
48
56
 
49
- - Components: Badge, StatusDot
57
+ - Components: Badge, StatusDot, NumberTicker
50
58
 
51
59
  ## Examples
52
60
 
@@ -0,0 +1,86 @@
1
+ # NumberTicker
2
+
3
+ **Status:** stable
4
+
5
+ A number that rolls to its new value digit by digit, like an odometer. Formatted with Intl.NumberFormat (currency, percent, compact, any locale); separators, signs and units hold still while each digit column turns, and a digit that appears or goes away eases its width so nothing around it jumps. At rest on first paint; under subtle or reduced motion the value simply updates.
6
+
7
+ **Keywords:** number, counter, odometer, rolling number, animated number, ticker, count up, stat, kpi, metric, price
8
+
9
+ ## Usage
10
+
11
+ Use NumberTicker for a figure that changes in place while someone is looking at it — a dashboard total after a filter changes, a live count, a price as a plan's options change, a score. The roll says "this number moved, and by roughly this much"; the reader sees the change happen instead of noticing, later, that a number is different.
12
+
13
+ ### Best practices
14
+
15
+ - Do: Use it for the headline figure that a control on the same screen changes — the total above a chart, the price beside a quantity stepper — so cause and effect are seen together.
16
+ - Do: Format through `format` (currency, percent, compact) rather than pre-formatting a string, so separators and units stay put while only the digits roll.
17
+ - Do: Pass `aria-live="polite"` only when the change itself is news the reader would otherwise miss; most tickers sit beside the control that changed them and need no announcement.
18
+ - Don't: Do not use it for values that change many times a second (a stopwatch, a stock tick, a progress percentage) — the rolls never land and read as a blur. Render the number as plain tabular text.
19
+ - Don't: Do not use it for numbers that are names rather than amounts — an order ID, a phone number, a year, a version. Rolling implies a quantity going up or down.
20
+ - Don't: Do not animate a number on page load to make it feel alive. The first paint is at rest on purpose; for an entrance, reveal the card it sits in (the Reveal motion primitive).
21
+ - Don't: Do not use it for a value inside a known range that needs a picture of how full it is — show a Meter, with the number beside it. → use **Meter**
22
+
23
+ ## Props
24
+
25
+ | Name | Type | Default | Description |
26
+ | --- | --- | --- | --- |
27
+ | value | number | — | The number to show. When it changes, the digits roll to it. |
28
+ | format | Intl.NumberFormatOptions | — | How to write the number — `{ style: "currency", currency: "USD" }`, `{ style: "percent" }`, `{ notation: "compact" }`, fraction digits. Passed to Intl.NumberFormat; an inline object is fine (it is compared by value). |
29
+ | locales | string \| string[] | runtime locale | BCP 47 locale(s) for the formatting. Digits roll through the locale's own numbering system. |
30
+ | prefix | ReactNode | — | Before the number. A string becomes part of the number's text (and rolls in or out with it); an element is rendered as given. |
31
+ | suffix | ReactNode | — | After the number ("+", " pts"). A string becomes part of the number's text; an element is rendered as given. |
32
+ | trend | "auto" \| "up" \| "down" | "auto" | Which way the digits roll. `auto` rolls up as the value grows and down as it shrinks; `up` / `down` fix it (a countdown, a score that only climbs). |
33
+ | 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 roll is expressive, so `subtle` updates the value in place. |
34
+ | className | string | — | Merged onto the root span. Set the type tier here (`text-heading-2`, `text-display-lg`); the ticker adds tabular figures so widths hold still. |
35
+ | ...span props | Omit<ComponentPropsWithRef<"span">, "children" \| "prefix"> | — | Remaining span attributes pass through to the root — `aria-live` to announce changes, `id`, data attributes. |
36
+
37
+ ## Motion
38
+
39
+ - **arrival** · Root · _no motion_ — The first paint is at rest. A number counting up on page load is decoration that delays the reading; the roll is kept for the moment the value actually changes.
40
+ - **response** · Root · _no motion_ — It is a reading, not a control; the control that changes it gives the feedback.
41
+ - **transition** · Digit · _custom (outside the recipe layer)_ — Each digit column turns to its new place on the expressive roll (the slower token, emphasized easing, staggered from the ones column), and arriving or departing columns ease their width. The roll is what shows a change happened and how big it was — the hundreds turning reads as a bigger move than the ones. No recipe can translate per-digit columns, so it is custom; it is multiplied by --motion-expressive, so `subtle` and reduced motion land the value at once.
42
+ - **activity** · Root · _no motion_ — A number has no working state; show a Skeleton while it loads.
43
+
44
+ ## Accessibility
45
+
46
+ - The whole written number (with any string prefix or suffix) is real text in a visually hidden span; the rolling columns are aria-hidden and draw their glyphs as generated content, so screen readers, find-in-page and copy all get "$1,234.50", never a stack of digits.
47
+ - It is not a live region by default — a figure that changes because the reader moved a control is already where their attention is. Pass `aria-live="polite"` when a change elsewhere should be announced.
48
+ - Tabular figures keep every digit the same width, so a roll never shifts the text around it.
49
+
50
+ ## Related
51
+
52
+ - Components: Meter, NotificationBadge, PieChart, RadialChart, Sparkline
53
+
54
+ ## Examples
55
+
56
+ ### Basic
57
+
58
+ ```tsx
59
+ import { NumberTicker } from "@sableui/react"
60
+
61
+ <NumberTicker value={1284} className="text-heading-2" />
62
+ ```
63
+
64
+ ### Currency
65
+
66
+ ```tsx
67
+ import { NumberTicker } from "@sableui/react"
68
+
69
+ <NumberTicker value={4280.5} format={{ style: "currency", currency: "USD" }} className="text-display-lg" />
70
+ ```
71
+
72
+ ### Percent, announced
73
+
74
+ ```tsx
75
+ import { NumberTicker } from "@sableui/react"
76
+
77
+ <NumberTicker value={0.428} format={{ style: "percent", maximumFractionDigits: 1 }} aria-live="polite" />
78
+ ```
79
+
80
+ ### Subtle
81
+
82
+ ```tsx
83
+ import { NumberTicker } from "@sableui/react"
84
+
85
+ <NumberTicker value={92} suffix=" pts" motion="subtle" />
86
+ ```
@@ -15,6 +15,7 @@ Use OtpField for a short, fixed-length code the user copies from somewhere else
15
15
  - Do: Set `length` to the exact length the server sends, and verify in `onValueComplete` (or with `autoSubmit` inside a form). Most users paste or autofill the whole code at once, so the moment of completion is the natural submit.
16
16
  - Do: Label it through a `Field` ("Verification code") and say where the code was sent in the description. The first slot takes the field's name; the rest are announced by position.
17
17
  - Do: Use `groupSize` for codes of 6 or more so the user can check the code in chunks against the message they are reading from.
18
+ - Do: Clear the error as soon as the user edits the code (`onValueChange` → reset the error), and set it again when the next code is refused. Each refusal is then a fresh change to invalid, so the field shakes for every wrong code, not only the first.
18
19
  - Don't: Do not use OtpField for a password, an arbitrary-length PIN, or anything the user types from memory. Slots promise a fixed length; use Input with `type="password"` and `autocomplete="current-password"` instead.
19
20
  - Don't: Do not intercept paste or clear the field on a wrong code before the user has seen the error. Blocking paste breaks SMS autofill and password managers, and wiping the slots erases the evidence of which digit was wrong.
20
21
  - Don't: Do not swap it for NumberField because the code is digits. A code is an identifier, not a quantity: NumberField strips leading zeros and offers steppers that make no sense here.
@@ -41,7 +42,8 @@ Use OtpField for a short, fixed-length code the user copies from somewhere else
41
42
  | autoSubmit | boolean | false | Submit the owning form when the code becomes complete. |
42
43
  | mask | boolean | false | Render slots as password inputs. Only for codes that are secret on screen, such as a card PIN. |
43
44
  | getSlotLabel | (index: number, length: number) => string | Character N of L | Accessible name for slots after the first. Override to localize. |
44
- | aria-invalid | boolean | — | Marks every slot invalid (destructive border). `<Field error>` sets it for you. |
45
+ | aria-invalid | boolean | — | Marks every slot invalid (destructive border and digits). Turning it on after mount shakes the field once. `<Field error>` sets it for you. |
46
+ | 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 shake is the flourish; the destructive colour stays at every level. |
45
47
  | disabled / readOnly / required / name / id / form | — | — | Standard form-control props. `id` lands on the first slot, so `<Field>` and `<label htmlFor>` name the field. |
46
48
 
47
49
  ## Tokens used
@@ -53,6 +55,7 @@ Use OtpField for a short, fixed-length code the user copies from somewhere else
53
55
  - **arrival** · Root · _no motion_ — A code field is part of the form it sits in and appears with it. Animating it in would delay the one thing the user arrived to do — paste the code they just copied.
54
56
  - **response** · Slot · `motion-interactive` — Focus moves between slots on every keystroke. The eased ring and border are what let the eye follow the cursor across six identical boxes instead of hunting for it after each digit.
55
57
  - **transition** · Slot · `motion-interactive` — A filled slot strengthens its border, so a pasted code visibly lands in every slot at once. Easing it reads as the code arriving rather than the field flickering.
58
+ - **transition** · Root · `motion-shake` — A refused code shakes the field once as it turns invalid — the short horizontal no that makes a second wrong code feel noticed rather than ignored. It never carries the refusal alone: the digits and borders turn destructive and the Field says why. It plays on the change to invalid, not on mount, and is expressive, so `subtle` keeps only the colour.
56
59
  - **activity** · Root · _no motion_ — Verifying the code is the form's work, not the field's. Show it on the submit Button's `loading` state, where the user expects the answer to appear.
57
60
 
58
61
  ## Accessibility
@@ -61,6 +64,7 @@ Use OtpField for a short, fixed-length code the user copies from somewhere else
61
64
  - Only one slot is in the tab order. Arrow keys, Home/End and Backspace move between slots; Tab leaves the whole field.
62
65
  - The first slot carries autocomplete="one-time-code" and the numeric virtual keyboard, so iOS and Android offer the SMS code above the keyboard.
63
66
  - Slots render at 20px (`text-mono-lg`), above the 16px floor that stops iOS Safari zooming on focus.
67
+ - An invalid code is never signalled by motion alone: every slot carries aria-invalid, the digits and borders turn destructive, and the Field's error message is wired through aria-describedby. The shake resolves instantly under reduced motion and `subtle`.
64
68
 
65
69
  ## Related
66
70
 
@@ -92,6 +96,16 @@ import { Field, OtpField } from "@sableui/react"
92
96
  <Field label="Verification code" error="That code has expired. Request a new code and try again."><OtpField length={6} groupSize={3} defaultValue="482913" /></Field>
93
97
  ```
94
98
 
99
+ ### Verify on completion, shake on a wrong code
100
+
101
+ ```tsx
102
+ import { Field, OtpField } from "@sableui/react"
103
+
104
+ const [code, setCode] = useState("")
105
+ const [wrong, setWrong] = useState(false)
106
+ <Field label="Verification code" error={wrong ? "That code does not match. Check the message and try again." : undefined}><OtpField length={6} groupSize={3} value={code} onValueChange={(next) => { setCode(next); setWrong(false) }} onValueComplete={(value) => setWrong(value !== expected)} /></Field>
107
+ ```
108
+
95
109
  ### Alphanumeric
96
110
 
97
111
  ```tsx
@@ -39,6 +39,13 @@ Use PanelGroup for the body of a tool workspace: fixed-width Panels docked left
39
39
 
40
40
  `primary`, `panel`, `hairline`, `hairline-strong`
41
41
 
42
+ ## Motion
43
+
44
+ - **arrival** · Root · _no motion_ — Panels are the app's frame, present from first paint. A dock that slid in on load would make the workspace look like it is still assembling.
45
+ - **response** · PanelResizer · `motion-interactive` — The hairline widens into a visible bar on hover and turns primary on keyboard focus, eased, so a 1px edge says it can be grabbed before the pointer commits.
46
+ - **transition** · Panel · _no motion_ — Width tracks the pointer one-to-one while dragging, and keyboard steps land at once. An eased width would trail the cursor, and resizing is a layout change: easing it would reflow the canvas beside the panel on every frame.
47
+ - **activity** · Root · _no motion_ — Layout chrome has no working state.
48
+
42
49
  ## Accessibility
43
50
 
44
51
  - PanelResizer is a role=separator with aria-orientation, aria-valuenow/min/max, and is focusable — a pointer-only resizer is a keyboard trap.
@@ -48,6 +48,14 @@ Use PanelSection to group properties inside an inspector or docked panel, with P
48
48
 
49
49
  `foreground`, `muted-foreground`, `hairline`
50
50
 
51
+ ## Motion
52
+
53
+ - **arrival** · Root · _no motion_ — Sections are part of the inspector they sit in and arrive with it. Animating each in would make a panel switch look like a page load.
54
+ - **response** · PropertyGroup trigger · `motion-interactive` — The fold row eases its focus ring, so arrowing down a dense inspector shows where the keyboard is without the ring flickering from row to row.
55
+ - **transition** · PropertyGroup panel · `motion-disclosure` — The group opens by easing its height, so the rows below slide down to make room instead of jumping. In an inspector, the next section moving is otherwise how you lose your place.
56
+ - **transition** · PropertyGroup indicator · `motion-icon-swap` — The + collapses into − as the group opens, both glyphs stacked as one mark, so the indicator reads as this row changing state rather than a different icon landing.
57
+ - **activity** · Root · _no motion_ — The inspector edits values in place; nothing in it loads.
58
+
51
59
  ## Accessibility
52
60
 
53
61
  - PropertyGroup's row is a real button with aria-expanded (Collapsible); its summary is part of the button's name, so a closed group still announces its value.
@@ -36,7 +36,8 @@ Use PieChart for a share of one total across two to six categories — traffic s
36
36
  | tooltip | boolean \| { indicator?: "dot" \| "line" \| "dashed" \| "none"; hideLabel?: boolean; labelFormatter?: (label, datum) => ReactNode; showTotal?: boolean } | true | Show a tooltip on hover and keyboard focus, or configure it: the row marker, hiding the heading, rewriting it, adding a Total row. |
37
37
  | valueFormatter | (value: number) => string | — | Format values everywhere (axis, labels, tooltip, table) — units like "$12,480". Default: compact on the axis, full elsewhere. |
38
38
  | locale | string | runtime locale | BCP 47 locale for default number and date formatting. |
39
- | animate | boolean | true | Sweep the slices in clockwise when the chart scrolls into view. |
39
+ | 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 sweep, the slice morph and the rolling centre number are the flourish; the hover pull-out and dim are the feedback `subtle` keeps. |
40
+ | animate | boolean | true | Sweep the slices in clockwise when the chart scrolls into view. `animate={false}` is `motion="subtle"`. |
40
41
  | activeIndex | number \| null | — | The highlighted row (controlled); `null` for none. |
41
42
  | defaultActiveIndex | number \| null | — | The row highlighted at first — a tooltip already open before anyone hovers. |
42
43
  | onActiveIndexChange | (index: number \| null) => void | — | Called as the pointer or keyboard moves between rows. |
@@ -47,9 +48,10 @@ Use PieChart for a share of one total across two to six categories — traffic s
47
48
 
48
49
  ## Motion
49
50
 
50
- - **arrival** · Slices · _custom (outside the recipe layer)_ — Once on screen the slices are revealed by a mask that sweeps clockwise from 12 o'clock — the whole is read as it fills. On the motion tokens; reduced motion shows the finished chart.
51
- - **response** · Active slice · _no motion_ — The hovered slice pulls outward and the rest dim at once; the geometry change itself eases (see transition).
52
- - **transition** · Slices · _custom (outside the recipe layer)_ — Geometry eases from the old shape to the new one through a CSS `d` transition on the motion tokens (no recipe animates path geometry); browsers without `d` transitions update in place.
51
+ - **arrival** · Slices · _custom (outside the recipe layer)_ — Once on screen the slices are revealed by a mask that sweeps clockwise from 12 o'clock — the whole is read as it fills. On the motion tokens; reduced motion shows the finished chart. The arrival is expressive: it plays at the full motion level only, so `motion="subtle"` (or a subtle app policy) shows the finished chart while hover feedback stays.
52
+ - **response** · Active slice · _no motion_ — The hovered slice pulls outward and the rest dim at once (mixed toward the surface, not faded); the geometry change itself eases (see transition). Essential: `subtle` keeps it.
53
+ - **transition** · Centre value · _custom (outside the recipe layer)_ — A numeric centre value rolls to its new total digit by digit (NumberTicker) when the data changes, so the headline is seen moving with the slices instead of being found different afterwards. Expressive: under `subtle` it updates in place.
54
+ - **transition** · Slices · _custom (outside the recipe layer)_ — Geometry eases from the old shape to the new one through a CSS `d` transition on the motion tokens (no recipe animates path geometry); browsers without `d` transitions update in place. Expressive, like the arrival: under `subtle` new data swaps in place.
53
55
  - **activity** · PieChart · _no motion_ — A chart has no indeterminate state; show a Skeleton while the data loads.
54
56
 
55
57
  ## Accessibility
@@ -39,6 +39,14 @@ Use Pill for a row of filters or choices a person clicks to narrow or toggle wha
39
39
 
40
40
  `palette-wash`, `palette-wash-strong`, `palette-edge`, `palette-solid`, `palette-text`, `palette-solid-ink`, `secondary`, `secondary-foreground`, `accent`, `accent-foreground`, `primary`, `primary-foreground`, `card`, `border`, `muted-foreground`, `foreground`
41
41
 
42
+ ## Motion
43
+
44
+ - **arrival** · Root · _no motion_ — Pills arrive with the filter row or the content they annotate. A chip that animated in alone would read as a filter someone just added.
45
+ - **response** · Root · `motion-interactive` — The hover wash eases, so sweeping the pointer along a filter row reads as one cursor moving rather than chips flashing in turn.
46
+ - **response** · Root · `motion-press` — A pill often toggles a filter that takes a moment to reach the results; the settle on press confirms the click landed before they change.
47
+ - **transition** · Root · `motion-interactive` — `selected` lifts the chip onto its selected surface on the same ease, so toggling reads as this chip changing state, not a different chip appearing in its place.
48
+ - **activity** · Root · _no motion_ — A chip is a choice, not a process. If applying it is slow, the results show it.
49
+
42
50
  ## Accessibility
43
51
 
44
52
  - Renders a native <button type="button"> for focus and keyboard activation.