@sableui/react 0.13.1 → 0.14.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 (231) hide show
  1. package/AGENTS.md +2 -2
  2. package/CHANGELOG.md +132 -0
  3. package/CONTRIBUTING.md +7 -5
  4. package/DESIGN.md +8 -3
  5. package/bin/sable.mjs +17 -1
  6. package/dist/blocks/approval-block.js +93 -0
  7. package/dist/blocks/approval-block.meta.js +23 -0
  8. package/dist/blocks/index.d.ts +17 -2
  9. package/dist/blocks/index.js +4 -0
  10. package/dist/components/accordion/accordion.meta.js +10 -3
  11. package/dist/components/area-chart/area-chart.meta.js +10 -3
  12. package/dist/components/attachment/attachment.meta.js +26 -0
  13. package/dist/components/badge/badge.meta.js +2 -1
  14. package/dist/components/banner/banner.meta.js +4 -2
  15. package/dist/components/bar-chart/bar-chart.meta.js +11 -4
  16. package/dist/components/bottom-nav/bottom-nav.js +3 -1
  17. package/dist/components/bottom-nav/bottom-nav.meta.js +32 -0
  18. package/dist/components/breadcrumbs/breadcrumbs.meta.js +26 -0
  19. package/dist/components/button/button.js +21 -4
  20. package/dist/components/button/button.meta.js +7 -7
  21. package/dist/components/button-group/button-group.meta.js +2 -1
  22. package/dist/components/calendar/calendar.js +18 -1
  23. package/dist/components/calendar/calendar.meta.js +38 -0
  24. package/dist/components/carousel/carousel.js +6 -4
  25. package/dist/components/carousel/carousel.meta.js +42 -0
  26. package/dist/components/chat-composer/chat-composer.js +21 -21
  27. package/dist/components/chat-composer/chat-composer.meta.js +38 -0
  28. package/dist/components/chat-composer/use-eased-growth.js +70 -0
  29. package/dist/components/checkbox/checkbox-mark.js +8 -1
  30. package/dist/components/checkbox/checkbox.js +3 -1
  31. package/dist/components/checkbox/checkbox.meta.js +34 -1
  32. package/dist/components/checkbox-card/checkbox-card.js +3 -0
  33. package/dist/components/checkbox-card/checkbox-card.meta.js +32 -0
  34. package/dist/components/code-block/code-block.js +14 -3
  35. package/dist/components/code-block/code-block.meta.js +28 -1
  36. package/dist/components/collapsible/collapsible.meta.js +34 -1
  37. package/dist/components/color-field/color-field.meta.js +26 -0
  38. package/dist/components/combobox/combobox.meta.js +48 -2
  39. package/dist/components/command-menu/command-menu.js +8 -3
  40. package/dist/components/command-menu/command-menu.meta.js +44 -3
  41. package/dist/components/confirm-button/confirm-button.js +179 -0
  42. package/dist/components/confirm-button/confirm-button.meta.js +143 -0
  43. package/dist/components/confirm-button/index.js +7 -0
  44. package/dist/components/context-card/context-card.meta.js +2 -1
  45. package/dist/components/data-table/data-table.js +6 -2
  46. package/dist/components/data-table/data-table.meta.js +32 -0
  47. package/dist/components/dialog/dialog.meta.js +6 -3
  48. package/dist/components/disclosure-picker/disclosure-picker.meta.js +2 -1
  49. package/dist/components/entity/entity.meta.js +26 -0
  50. package/dist/components/feedback/feedback.js +113 -47
  51. package/dist/components/feedback/feedback.meta.js +49 -4
  52. package/dist/components/inline-status/inline-status.meta.js +2 -1
  53. package/dist/components/input-group/input-group.meta.js +27 -1
  54. package/dist/components/line-chart/line-chart.meta.js +10 -3
  55. package/dist/components/menu/menu.meta.js +2 -1
  56. package/dist/components/menubar/menubar.meta.js +2 -1
  57. package/dist/components/meter/meter.meta.js +2 -1
  58. package/dist/components/middle-truncate/middle-truncate.meta.js +1 -1
  59. package/dist/components/note/note.meta.js +34 -1
  60. package/dist/components/notification-badge/notification-badge.js +17 -5
  61. package/dist/components/notification-badge/notification-badge.meta.js +34 -2
  62. package/dist/components/number-ticker/index.js +7 -0
  63. package/dist/components/number-ticker/number-ticker.js +246 -0
  64. package/dist/components/number-ticker/number-ticker.meta.js +159 -0
  65. package/dist/components/otp-field/otp-field.js +23 -3
  66. package/dist/components/otp-field/otp-field.meta.js +23 -2
  67. package/dist/components/page-loader/page-loader.meta.js +2 -1
  68. package/dist/components/panel-group/panel-group.meta.js +26 -0
  69. package/dist/components/panel-section/panel-section.js +37 -10
  70. package/dist/components/panel-section/panel-section.meta.js +32 -0
  71. package/dist/components/pie-chart/pie-chart.js +20 -11
  72. package/dist/components/pie-chart/pie-chart.meta.js +17 -4
  73. package/dist/components/pill/pill.meta.js +34 -1
  74. package/dist/components/popover/popover.meta.js +4 -2
  75. package/dist/components/progress/progress.meta.js +2 -1
  76. package/dist/components/radar-chart/radar-chart.js +4 -3
  77. package/dist/components/radar-chart/radar-chart.meta.js +10 -3
  78. package/dist/components/radial-chart/radial-chart.js +22 -22
  79. package/dist/components/radial-chart/radial-chart.meta.js +17 -4
  80. package/dist/components/radio/radio.meta.js +28 -1
  81. package/dist/components/radio-card/radio-card.meta.js +38 -0
  82. package/dist/components/relative-time/relative-time.meta.js +32 -0
  83. package/dist/components/scroller/scroller.meta.js +32 -0
  84. package/dist/components/scrub-field/scrub-field.meta.js +32 -0
  85. package/dist/components/search-input/search-input.js +9 -7
  86. package/dist/components/search-input/search-input.meta.js +26 -0
  87. package/dist/components/segmented-bar/segmented-bar.meta.js +2 -1
  88. package/dist/components/select/select.meta.js +4 -2
  89. package/dist/components/sheet/sheet.meta.js +4 -2
  90. package/dist/components/show-more/show-more.js +1 -4
  91. package/dist/components/show-more/show-more.meta.js +38 -0
  92. package/dist/components/side-nav/side-nav.js +21 -1
  93. package/dist/components/side-nav/side-nav.meta.js +34 -1
  94. package/dist/components/side-nav/side-nav.variants.js +3 -1
  95. package/dist/components/sidebar/sidebar.js +31 -8
  96. package/dist/components/sidebar/sidebar.meta.js +38 -0
  97. package/dist/components/skeleton/skeleton.meta.js +2 -1
  98. package/dist/components/slider/slider.meta.js +26 -0
  99. package/dist/components/sparkline/sparkline.js +7 -4
  100. package/dist/components/sparkline/sparkline.meta.js +9 -2
  101. package/dist/components/spinner/spinner.meta.js +2 -1
  102. package/dist/components/stepper/stepper.meta.js +4 -2
  103. package/dist/components/switch/switch.meta.js +2 -1
  104. package/dist/components/table/table.meta.js +28 -1
  105. package/dist/components/tabs/tabs.js +27 -8
  106. package/dist/components/tabs/tabs.meta.js +50 -5
  107. package/dist/components/theme-selector/theme-selector.meta.js +32 -0
  108. package/dist/components/theme-switcher/theme-switcher.js +4 -1
  109. package/dist/components/theme-switcher/theme-switcher.meta.js +42 -0
  110. package/dist/components/thinking-orb/thinking-orb.meta.js +4 -2
  111. package/dist/components/toast/toast.meta.js +2 -1
  112. package/dist/components/toggle/toggle.js +28 -5
  113. package/dist/components/toggle/toggle.meta.js +46 -3
  114. package/dist/components/tool-call/index.js +8 -0
  115. package/dist/components/tool-call/tool-call.js +117 -0
  116. package/dist/components/tool-call/tool-call.meta.js +147 -0
  117. package/dist/components/tool-call/tool-call.variants.js +33 -0
  118. package/dist/components/toolbar/toolbar.meta.js +2 -1
  119. package/dist/components/tooltip/index.js +3 -1
  120. package/dist/components/tooltip/tooltip.js +84 -0
  121. package/dist/components/tooltip/tooltip.meta.js +18 -2
  122. package/dist/components/tree/tree.js +71 -6
  123. package/dist/components/tree/tree.meta.js +32 -0
  124. package/dist/components/video/video.meta.js +32 -0
  125. package/dist/file-hashes.json +139 -122
  126. package/dist/foundations/motion.js +49 -3
  127. package/dist/index.d.ts +616 -46
  128. package/dist/index.js +4 -1
  129. package/dist/lib/chart/cartesian.js +9 -10
  130. package/dist/lib/chart/chart-center.js +30 -0
  131. package/dist/lib/chart/chart-frame.js +15 -8
  132. package/dist/lib/chart/dim.js +13 -0
  133. package/dist/lib/chart/format.js +7 -1
  134. package/dist/lib/chart/motion.js +6 -4
  135. package/dist/lib/highlight-glide.js +68 -10
  136. package/dist/lib/taxonomy.js +3 -0
  137. package/dist/{meta-DVG8ceze.d.ts → meta-DX5r5wQL.d.ts} +8 -0
  138. package/dist/motion/MotionProvider.js +45 -3
  139. package/dist/motion/Reveal.js +1 -1
  140. package/dist/motion/TextReveal.js +1 -1
  141. package/dist/motion/index.js +15 -2
  142. package/dist/motion/useAutoSize.js +42 -0
  143. package/dist/motion/useShake.js +25 -0
  144. package/dist/patterns/index.d.ts +1 -1
  145. package/dist/styles.css +298 -5
  146. package/llms/blocks/approval-block.md +128 -0
  147. package/llms/components/accordion.md +4 -3
  148. package/llms/components/area-chart.md +4 -3
  149. package/llms/components/attachment.md +7 -0
  150. package/llms/components/badge.md +1 -1
  151. package/llms/components/banner.md +2 -2
  152. package/llms/components/bar-chart.md +5 -4
  153. package/llms/components/bottom-nav.md +8 -0
  154. package/llms/components/breadcrumbs.md +7 -0
  155. package/llms/components/button-group.md +1 -1
  156. package/llms/components/button.md +6 -6
  157. package/llms/components/calendar.md +9 -0
  158. package/llms/components/carousel.md +17 -0
  159. package/llms/components/chat-composer.md +9 -0
  160. package/llms/components/checkbox-card.md +8 -0
  161. package/llms/components/checkbox.md +9 -1
  162. package/llms/components/code-block.md +8 -0
  163. package/llms/components/collapsible.md +9 -1
  164. package/llms/components/color-field.md +7 -0
  165. package/llms/components/combobox.md +12 -2
  166. package/llms/components/command-menu.md +12 -3
  167. package/llms/components/confirm-button.md +92 -0
  168. package/llms/components/context-card.md +1 -1
  169. package/llms/components/data-table.md +8 -0
  170. package/llms/components/dialog.md +3 -3
  171. package/llms/components/disclosure-picker.md +1 -1
  172. package/llms/components/entity.md +7 -0
  173. package/llms/components/feedback.md +22 -3
  174. package/llms/components/inline-status.md +1 -1
  175. package/llms/components/input-group.md +8 -1
  176. package/llms/components/line-chart.md +4 -3
  177. package/llms/components/menu.md +1 -1
  178. package/llms/components/menubar.md +1 -1
  179. package/llms/components/meter.md +1 -1
  180. package/llms/components/middle-truncate.md +1 -1
  181. package/llms/components/note.md +9 -1
  182. package/llms/components/notification-badge.md +10 -2
  183. package/llms/components/number-ticker.md +86 -0
  184. package/llms/components/otp-field.md +15 -1
  185. package/llms/components/page-loader.md +1 -1
  186. package/llms/components/panel-group.md +7 -0
  187. package/llms/components/panel-section.md +8 -0
  188. package/llms/components/pie-chart.md +6 -4
  189. package/llms/components/pill.md +9 -1
  190. package/llms/components/popover.md +2 -2
  191. package/llms/components/progress.md +1 -1
  192. package/llms/components/radar-chart.md +4 -3
  193. package/llms/components/radial-chart.md +6 -4
  194. package/llms/components/radio-card.md +9 -0
  195. package/llms/components/radio.md +8 -1
  196. package/llms/components/relative-time.md +8 -0
  197. package/llms/components/scroller.md +8 -0
  198. package/llms/components/scrub-field.md +8 -0
  199. package/llms/components/search-input.md +7 -0
  200. package/llms/components/segmented-bar.md +1 -1
  201. package/llms/components/select.md +2 -2
  202. package/llms/components/sheet.md +2 -2
  203. package/llms/components/show-more.md +9 -0
  204. package/llms/components/side-nav.md +9 -1
  205. package/llms/components/sidebar.md +9 -0
  206. package/llms/components/skeleton.md +1 -1
  207. package/llms/components/slider.md +7 -0
  208. package/llms/components/sparkline.md +3 -2
  209. package/llms/components/spinner.md +1 -1
  210. package/llms/components/stepper.md +2 -2
  211. package/llms/components/switch.md +1 -1
  212. package/llms/components/table.md +8 -1
  213. package/llms/components/tabs.md +22 -4
  214. package/llms/components/theme-selector.md +8 -0
  215. package/llms/components/theme-switcher.md +17 -0
  216. package/llms/components/thinking-orb.md +2 -2
  217. package/llms/components/toast.md +1 -1
  218. package/llms/components/toggle.md +20 -3
  219. package/llms/components/tool-call.md +115 -0
  220. package/llms/components/toolbar.md +1 -1
  221. package/llms/components/tooltip.md +12 -2
  222. package/llms/components/tree.md +8 -0
  223. package/llms/components/video.md +8 -0
  224. package/llms/foundations/motion.md +48 -3
  225. package/llms/reference/utilities.md +23 -1
  226. package/llms/tasks/reviewing-motion.md +14 -0
  227. package/llms-full.txt +1004 -98
  228. package/llms.txt +16 -11
  229. package/package.json +1 -1
  230. package/registry.json +2708 -187
  231. package/targets/ios.json +27 -0
package/dist/styles.css CHANGED
@@ -1201,7 +1201,8 @@
1201
1201
  drops the hiding rule entirely and the action simply stays visible. */
1202
1202
  @utility reveal-on-hover {
1203
1203
  transition-property: color, background-color, border-color, box-shadow,
1204
- opacity, fill, stroke, scale, translate;
1204
+ opacity, fill, stroke, scale, translate, rotate, outline-color, outline-width,
1205
+ outline-offset;
1205
1206
  transition-duration: var(--motion-duration-fast);
1206
1207
  transition-timing-function: var(--motion-ease-standard);
1207
1208
 
@@ -1844,8 +1845,18 @@
1844
1845
  collapse --motion-scale to 0 — one switch makes all token-driven motion
1845
1846
  static (transitions resolve instantly; loaders hold their first frame).
1846
1847
 
1847
- Keep motion subtle: opacity + transform only (compositor-friendly), short
1848
- durations, no spring/overshoot. See DESIGN.md → Motion.
1848
+ Two LEVELS sit under the switch. ESSENTIAL motion answers the user: colour
1849
+ and focus feedback, press, overlay arrival, disclosure height, loaders.
1850
+ EXPRESSIVE motion narrates continuity: an indicator that travels between
1851
+ options (motion-glide), digits that roll (NumberTicker), a tick that draws
1852
+ (motion-tick), a shake on a wrong answer (motion-shake), staggered arrivals
1853
+ and spring overshoot. Expressive durations are multiplied by a second
1854
+ factor, --motion-expressive, which `data-motion="subtle"` sets to 0 (and
1855
+ which also takes the overshoot out of ease-spring/ease-bounce). So "subtle"
1856
+ keeps every answer and drops every flourish; "off" drops both.
1857
+
1858
+ Opacity + transform only (compositor-friendly), short durations, overshoot on
1859
+ enter only. See DESIGN.md → Motion.
1849
1860
  ========================================================================== */
1850
1861
 
1851
1862
  @keyframes sable-spin {
@@ -2009,10 +2020,22 @@
2009
2020
  `motion-press` and the hover/active surface — the flash is a second, worse
2010
2021
  answer. Both are inert on pointer devices, and neither is motion, so
2011
2022
  --motion-scale: 0 and prefers-reduced-motion leave them untouched: the delay
2012
- fix keeps working when animation is off, which is when it matters most. */
2023
+ fix keeps working when animation is off, which is when it matters most.
2024
+
2025
+ `rotate` is in the list because Tailwind v4's `rotate-*` utilities write the
2026
+ standalone `rotate` property, not `transform`. Without it every disclosure
2027
+ chevron carrying this recipe (Accordion, Collapsible, Tree) jumped 180° in a
2028
+ single frame while the panel beside it eased open.
2029
+
2030
+ The outline properties are there for focus rings drawn with `outline-*`
2031
+ (the box-shadow `ring-*` ones were already covered). An outline's computed
2032
+ width is 0 while its style is `none`, so on focus the ring grows from the
2033
+ element's edge to its full width at the fast step instead of appearing in
2034
+ one frame; on blur it shrinks back. Under reduced motion it is instant. */
2013
2035
  @utility motion-interactive {
2014
2036
  transition-property: color, background-color, border-color, box-shadow,
2015
- opacity, fill, stroke, scale, translate;
2037
+ opacity, fill, stroke, scale, translate, rotate, outline-color, outline-width,
2038
+ outline-offset;
2016
2039
  transition-duration: var(--motion-duration-fast);
2017
2040
  transition-timing-function: var(--motion-ease-standard);
2018
2041
  touch-action: manipulation;
@@ -2196,6 +2219,262 @@
2196
2219
  }
2197
2220
  }
2198
2221
 
2222
+ /* --------------------------------------------------------------------------
2223
+ Expressive recipes — continuity and choreography. Each duration is a token
2224
+ multiplied by --motion-expressive, so `data-motion="subtle"` resolves them
2225
+ instantly while the essential recipes above keep answering. Under the
2226
+ off/reduced policy the token itself is already 0.
2227
+ -------------------------------------------------------------------------- */
2228
+
2229
+ /* Glide — ONE element that travels to the selected option, instead of each
2230
+ option blinking its own fill: a tab's underline or pill, a segmented thumb,
2231
+ a carousel's active dot, a nav's current-page bar. The component positions it
2232
+ (Base UI's --active-tab-* vars, a measured box, or an index step); this
2233
+ recipe carries the timing.
2234
+
2235
+ Width and height are in the list on purpose. The glider is absolutely
2236
+ positioned, so resizing it lays out nothing else, and a pill that stretches
2237
+ to its new option's label width cannot be a scale without distorting its
2238
+ corners. Emphasized easing: it leaves quickly and settles, which is what
2239
+ reads as "one thing moved" rather than "two things swapped". */
2240
+ @utility motion-glide {
2241
+ transition-property: translate, width, height, inset, left, top, opacity, background-color;
2242
+ /* The travel is expressive; the glider fading in or out and its colour
2243
+ answering a hover are essential, so "subtle" keeps those. One duration
2244
+ per property — a shorter list repeats from the start. */
2245
+ transition-duration:
2246
+ calc(var(--motion-duration-slow) * var(--motion-expressive, 1)),
2247
+ calc(var(--motion-duration-slow) * var(--motion-expressive, 1)),
2248
+ calc(var(--motion-duration-slow) * var(--motion-expressive, 1)),
2249
+ calc(var(--motion-duration-slow) * var(--motion-expressive, 1)),
2250
+ calc(var(--motion-duration-slow) * var(--motion-expressive, 1)),
2251
+ calc(var(--motion-duration-slow) * var(--motion-expressive, 1)),
2252
+ var(--motion-duration-fast),
2253
+ var(--motion-duration-fast);
2254
+ transition-timing-function: var(--motion-ease-emphasized), var(--motion-ease-emphasized),
2255
+ var(--motion-ease-emphasized), var(--motion-ease-emphasized), var(--motion-ease-emphasized),
2256
+ var(--motion-ease-emphasized), var(--motion-ease-standard), var(--motion-ease-standard);
2257
+ }
2258
+
2259
+ /* Shake — a short horizontal no, for an answer that was refused: a wrong
2260
+ one-time code, a submit with invalid fields. Plays while `data-shake` is set;
2261
+ `useShake()` (src/motion) sets it, clears it on animationend, and restarts it
2262
+ when the next refusal lands mid-shake. Decaying amplitude, about a fifth of a
2263
+ second per swing — anything longer reads as a tantrum.
2264
+
2265
+ Never the only signal. The field turns destructive and says why; the shake
2266
+ is what makes a repeated mistake feel noticed. Under "subtle" the duration
2267
+ is 0 and only the colour remains. */
2268
+ @keyframes sable-shake {
2269
+ 0%,
2270
+ 100% {
2271
+ translate: 0 0;
2272
+ }
2273
+ 20% {
2274
+ translate: -0.375rem 0;
2275
+ }
2276
+ 40% {
2277
+ translate: 0.3rem 0;
2278
+ }
2279
+ 60% {
2280
+ translate: -0.2rem 0;
2281
+ }
2282
+ 80% {
2283
+ translate: 0.1rem 0;
2284
+ }
2285
+ }
2286
+ @utility motion-shake {
2287
+ &[data-shake] {
2288
+ animation: sable-shake calc(var(--motion-duration-slower) * 0.8 * var(--motion-expressive, 1))
2289
+ var(--motion-ease-standard);
2290
+ }
2291
+ }
2292
+
2293
+ /* Tick — a selection mark that draws itself in rather than appearing: the
2294
+ check in a Checkbox, a success glyph. The mark wipes in from its leading
2295
+ stroke (clip-path, so it works on any injected icon set — a path-length draw
2296
+ would need to own the SVG) while its opacity takes the essential fade.
2297
+ Keyed to Base UI's starting/ending style like the overlay recipes; exits only
2298
+ fade, at the exit speed, so un-checking is not a performance.
2299
+
2300
+ Two durations: the fade is essential (fast), the wipe is expressive, so
2301
+ "subtle" keeps a clean fade and drops the draw. */
2302
+ @utility motion-tick {
2303
+ /* Both ends of the wipe must be inset() so they interpolate: from the
2304
+ default `none` the clip-path flipped in one frame and only the fade and
2305
+ scale played. Outset by 2px so the resting clip never shaves an injected
2306
+ glyph's round caps. */
2307
+ clip-path: inset(-2px);
2308
+ transition-property: opacity, clip-path, scale;
2309
+ transition-duration: var(--motion-duration-fast),
2310
+ calc(var(--motion-duration-base) * var(--motion-expressive, 1)),
2311
+ calc(var(--motion-duration-base) * var(--motion-expressive, 1));
2312
+ transition-timing-function: var(--motion-ease-standard), var(--motion-ease-decelerate),
2313
+ var(--motion-ease-spring);
2314
+ &[data-starting-style] {
2315
+ opacity: 0;
2316
+ clip-path: inset(-2px 100% -2px -2px);
2317
+ scale: 0.6;
2318
+ }
2319
+ &[data-ending-style] {
2320
+ opacity: 0;
2321
+ transition-duration: var(--motion-duration-exit), 0s, 0s;
2322
+ }
2323
+ }
2324
+
2325
+ /* Appear — a small control that mounts in place and should not pop into
2326
+ existence: a search field's clear button, a send button replacing a mic, a
2327
+ count badge. Uses @starting-style, so it needs no data attribute and no JS;
2328
+ the element simply transitions in from its starting style on first render.
2329
+ The fade is essential, the scale is expressive. Removal is instant — an
2330
+ unmounted element has nothing to animate, and a control leaving should get
2331
+ out of the way. It declares its own transition-property, so put it on a
2332
+ WRAPPER around a Button or anything carrying motion-interactive — on the
2333
+ same element one of the two lists would silently win. */
2334
+ @utility motion-appear {
2335
+ transition-property: opacity, scale;
2336
+ transition-duration: var(--motion-duration-fast),
2337
+ calc(var(--motion-duration-base) * var(--motion-expressive, 1));
2338
+ transition-timing-function: var(--motion-ease-standard), var(--motion-ease-spring);
2339
+ @starting-style {
2340
+ opacity: 0;
2341
+ scale: 0.6;
2342
+ }
2343
+ }
2344
+
2345
+ /* Arrive — items that a user action brings into a list settle into place one
2346
+ after another, so the eye can follow where they came from: a tree folder's
2347
+ children on expand, results that load in, a row that was just added.
2348
+ A CSS animation rather than a transition, so it composes with the
2349
+ motion-interactive every row already carries. Set the item's stagger as a
2350
+ raw delay in --motion-arrive-delay (index x ~18ms, capped around ten items so
2351
+ a long list settles as fast as a short one); the recipe scales it, so
2352
+ "subtle" and reduced motion show the items at once with no dead wait.
2353
+ `backwards` holds the start pose through the delay and then lets go, so a
2354
+ later opacity (a dimmed row) is never pinned.
2355
+
2356
+ Only on a user action. A list that arrives this way every time it renders is
2357
+ a title sequence: mount the class only on items that are new. */
2358
+ @keyframes sable-arrive {
2359
+ from {
2360
+ opacity: 0;
2361
+ translate: 0 -0.25rem;
2362
+ }
2363
+ }
2364
+ @utility motion-arrive {
2365
+ animation: sable-arrive calc(var(--motion-duration-base) * var(--motion-expressive, 1))
2366
+ var(--motion-ease-decelerate)
2367
+ calc(var(--motion-arrive-delay, 0ms) * var(--motion-scale, 1) * var(--motion-expressive, 1))
2368
+ backwards;
2369
+ }
2370
+
2371
+ /* Calendar month paging — react-day-picker's `animate` mode, which Calendar
2372
+ turns on only while motion is expressive. RDP keeps the outgoing month as a
2373
+ snapshot laid over the incoming one and adds ONE class to each part (so
2374
+ these are plain classes, not utilities: a class list would not survive
2375
+ classList.add). The months trade places over a short travel with a fade,
2376
+ not RDP's default full-width slide, which drags a whole grid across the
2377
+ card. Enter at the slow step on the emphasized curve; exit at the exit step,
2378
+ so the old month is out of the way before the new one settles.
2379
+
2380
+ Naming follows RDP: "after" means the month that arrives is AFTER the one
2381
+ leaving (paging forward), so it comes in from the end and the old one leaves
2382
+ toward the start. Mirrored under [dir=rtl]. */
2383
+ @keyframes sable-month-from-start {
2384
+ from {
2385
+ opacity: 0;
2386
+ translate: -1.5rem 0;
2387
+ }
2388
+ }
2389
+ @keyframes sable-month-from-end {
2390
+ from {
2391
+ opacity: 0;
2392
+ translate: 1.5rem 0;
2393
+ }
2394
+ }
2395
+ @keyframes sable-month-to-start {
2396
+ to {
2397
+ opacity: 0;
2398
+ translate: -1.5rem 0;
2399
+ }
2400
+ }
2401
+ @keyframes sable-month-to-end {
2402
+ to {
2403
+ opacity: 0;
2404
+ translate: 1.5rem 0;
2405
+ }
2406
+ }
2407
+ @keyframes sable-fade-out {
2408
+ to {
2409
+ opacity: 0;
2410
+ }
2411
+ }
2412
+ .sable-month-enter-after,
2413
+ .sable-month-enter-before {
2414
+ animation: sable-month-from-end calc(var(--motion-duration-slow) * var(--motion-expressive, 1))
2415
+ var(--motion-ease-emphasized) both;
2416
+ }
2417
+ .sable-month-enter-before,
2418
+ [dir="rtl"] .sable-month-enter-after {
2419
+ animation-name: sable-month-from-start;
2420
+ }
2421
+ [dir="rtl"] .sable-month-enter-before {
2422
+ animation-name: sable-month-from-end;
2423
+ }
2424
+ .sable-month-exit-after,
2425
+ .sable-month-exit-before {
2426
+ animation: sable-month-to-start calc(var(--motion-duration-exit) * var(--motion-expressive, 1))
2427
+ var(--motion-ease-standard) both;
2428
+ }
2429
+ .sable-month-exit-after,
2430
+ [dir="rtl"] .sable-month-exit-before {
2431
+ animation-name: sable-month-to-end;
2432
+ }
2433
+ [dir="rtl"] .sable-month-exit-after {
2434
+ animation-name: sable-month-to-start;
2435
+ }
2436
+ .sable-caption-enter {
2437
+ animation: sable-fade-in calc(var(--motion-duration-slow) * var(--motion-expressive, 1))
2438
+ var(--motion-ease-decelerate) both;
2439
+ }
2440
+ .sable-caption-exit {
2441
+ animation: sable-fade-out calc(var(--motion-duration-exit) * var(--motion-expressive, 1))
2442
+ var(--motion-ease-standard) both;
2443
+ }
2444
+
2445
+ /* Size — eases an element between sizes: a button whose label changes
2446
+ ("Delete" → "Confirm delete?"), a chip that gains a count, a panel going from
2447
+ 0 to its natural height.
2448
+
2449
+ A transition needs the computed size to CHANGE. Between a keyword and a
2450
+ length (0 ↔ auto) `interpolate-size: allow-keywords` supplies that. A
2451
+ content-sized box whose content changes computes `auto` both times, so pair
2452
+ the recipe with `useAutoSize()` (src/motion), which writes the content's
2453
+ measured size as pixels for this transition to ease between. Where
2454
+ interpolate-size is unsupported the keyword case simply snaps, which is the
2455
+ old behaviour. Expressive: under "subtle" the size snaps and the label still
2456
+ changes. */
2457
+ @utility motion-size {
2458
+ interpolate-size: allow-keywords;
2459
+ /* A superset of motion-interactive's list, in the same order after the two
2460
+ sizes, so it can sit on a Button (which carries motion-interactive) and
2461
+ whichever declaration wins the cascade, nothing the other animated stops
2462
+ animating. Only the two sizes are expressive. */
2463
+ transition-property: width, height, color, background-color, border-color, box-shadow,
2464
+ opacity, fill, stroke, scale, translate, rotate, outline-color, outline-width,
2465
+ outline-offset;
2466
+ /* One entry per property: a shorter list would REPEAT from the start. */
2467
+ transition-duration: calc(var(--motion-duration-base) * var(--motion-expressive, 1)),
2468
+ calc(var(--motion-duration-base) * var(--motion-expressive, 1)),
2469
+ var(--motion-duration-fast), var(--motion-duration-fast), var(--motion-duration-fast), var(--motion-duration-fast), var(--motion-duration-fast),
2470
+ var(--motion-duration-fast), var(--motion-duration-fast), var(--motion-duration-fast), var(--motion-duration-fast), var(--motion-duration-fast),
2471
+ var(--motion-duration-fast), var(--motion-duration-fast), var(--motion-duration-fast);
2472
+ transition-timing-function: var(--motion-ease-emphasized), var(--motion-ease-emphasized),
2473
+ var(--motion-ease-standard), var(--motion-ease-standard), var(--motion-ease-standard), var(--motion-ease-standard), var(--motion-ease-standard),
2474
+ var(--motion-ease-standard), var(--motion-ease-standard), var(--motion-ease-standard), var(--motion-ease-standard), var(--motion-ease-standard),
2475
+ var(--motion-ease-standard), var(--motion-ease-standard), var(--motion-ease-standard);
2476
+ }
2477
+
2199
2478
  /* --------------------------------------------------------------------------
2200
2479
  Loader recipes — looping indicators. Durations scale with --motion-scale, so
2201
2480
  the policy switch freezes them to a static first frame.
@@ -2459,3 +2738,17 @@
2459
2738
  [data-motion="off"] [data-sable-theme] {
2460
2739
  --motion-scale: 0 !important;
2461
2740
  }
2741
+
2742
+ /* "subtle": essential motion plays, expressive motion resolves instantly and
2743
+ nothing overshoots. It never touches --motion-scale, so it can only turn
2744
+ motion DOWN: nested inside "off", or under the OS reduce preference, the
2745
+ scale is still 0. That is what makes it safe as a per-component prop and on
2746
+ <MotionScope> — neither can switch motion back on for a reader who asked for
2747
+ it off. The easings are re-pointed rather than zeroed: a spring pop becomes a
2748
+ clean decelerate, which is still an answer. */
2749
+ [data-motion="subtle"],
2750
+ [data-motion="subtle"] [data-sable-theme] {
2751
+ --motion-expressive: 0 !important;
2752
+ --motion-ease-spring: var(--motion-ease-decelerate) !important;
2753
+ --motion-ease-bounce: var(--motion-ease-decelerate) !important;
2754
+ }
@@ -0,0 +1,128 @@
1
+ # ApprovalBlock
2
+
3
+ An agent asking permission for one tool call: the question, what will happen and what can't be undone, the call's details, an "allow for the rest of this chat" Checkbox, and Deny / Allow.
4
+
5
+ **Intent:** Reach for this in a chat or agent run when a tool call waits on the person — sending, paying, deleting, publishing. The call itself, before and after the decision, is a ToolCall with status="awaiting", then "complete" or "denied". A destructive action outside an agent is DestructiveActionModal.
6
+
7
+ **Block:** AI & agents · card · designed for 18rem–28rem wide (fills its container up to the max)
8
+
9
+ ## Notes
10
+
11
+ - The title is the question in plain words ("Send the invoice to Ada?"), not the tool's name; the tool's name is one row of the details, for the person who wants to know exactly what runs.
12
+ - The description says what can't be undone. That sentence is what the person is deciding on.
13
+ - Deny comes first and Allow is the primary on the end, the same order as every Sable confirmation. Neither is focused on arrival: an approval that a stray Enter can grant is not an approval.
14
+ - After a decision the buttons give way to one InlineStatus line saying what happened, so the thread keeps a record of the answer.
15
+
16
+ ## Accessibility
17
+
18
+ - Focus moves to the outcome line when the buttons leave, instead of dropping to the page; the line is a role=status region, so the decision is announced.
19
+ - The details are a dl, so each value is read with its label.
20
+ - The remember Checkbox has its own Label, so the tool name inside it is part of its accessible name.
21
+
22
+ ## Source
23
+
24
+ ```tsx
25
+ import { useState, useEffect, useRef, useId } from "react"
26
+ import { Button, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, Checkbox, cn, Code, InlineStatus, Label } from "@sableui/react"
27
+
28
+ const request = {
29
+ tool: "send_email",
30
+ title: "Send the invoice to Ada?",
31
+ description: "The agent wants to email invoice INV-1042. An email can't be unsent.",
32
+ details: [
33
+ { label: "To", value: "ada@example.com" },
34
+ { label: "Subject", value: "INV-1042 for October" },
35
+ { label: "Attached", value: "INV-1042.pdf" },
36
+ ],
37
+ allowed: "Allowed. Sending the email.",
38
+ denied: "Denied. The agent won't send it.",
39
+ }
40
+
41
+ export interface ApprovalBlockProps {
42
+ className?: string
43
+ /** false drops the card's surface and border — for a chat column that already frames it. */
44
+ framed?: boolean
45
+ /** The person allowed the call. `remember` is the "for the rest of this chat" choice. */
46
+ onApprove?: (options: { remember: boolean }) => void
47
+ /** The person declined the call. */
48
+ onDeny?: () => void
49
+ }
50
+
51
+ export function ApprovalBlock({ className, framed = true, onApprove, onDeny }: ApprovalBlockProps) {
52
+ const [decision, setDecision] = useState<"allowed" | "denied" | null>(null)
53
+ const [remember, setRemember] = useState(false)
54
+ const id = useId()
55
+ const outcome = useRef<HTMLDivElement>(null)
56
+ // The buttons leave with the decision, so focus moves to the line that
57
+ // replaces them instead of falling back to the top of the page.
58
+ useEffect(() => {
59
+ if (decision) outcome.current?.focus()
60
+ }, [decision])
61
+
62
+ return (
63
+ <Card variant={framed ? "default" : "flat"} className={cn("w-full max-w-md", className)}>
64
+ <CardHeader>
65
+ <CardTitle>{request.title}</CardTitle>
66
+ <CardDescription>{request.description}</CardDescription>
67
+ </CardHeader>
68
+ <CardContent className="flex flex-col gap-4">
69
+ <dl className="grid grid-cols-[auto_minmax(0,1fr)] gap-x-4 gap-y-1.5 text-body-sm">
70
+ {request.details.map((d) => (
71
+ <div key={d.label} className="contents">
72
+ <dt className="text-muted-foreground">{d.label}</dt>
73
+ <dd className="truncate text-foreground">{d.value}</dd>
74
+ </div>
75
+ ))}
76
+ <div className="contents">
77
+ <dt className="text-muted-foreground">Tool</dt>
78
+ <dd className="min-w-0">
79
+ <Code>{request.tool}</Code>
80
+ </dd>
81
+ </div>
82
+ </dl>
83
+ {decision ? null : (
84
+ <div className="flex items-center gap-2">
85
+ <Checkbox id={`${id}-remember`} checked={remember} onCheckedChange={(v: boolean) => setRemember(v)} />
86
+ <Label htmlFor={`${id}-remember`} className="text-body-sm">
87
+ Allow <Code>{request.tool}</Code> for the rest of this chat
88
+ </Label>
89
+ </div>
90
+ )}
91
+ </CardContent>
92
+ <CardFooter actions className="justify-end">
93
+ {decision ? (
94
+ <InlineStatus
95
+ ref={outcome}
96
+ tabIndex={-1}
97
+ role="status"
98
+ tone={decision === "allowed" ? "success" : "neutral"}
99
+ className="me-auto outline-none"
100
+ >
101
+ {decision === "allowed" ? request.allowed : request.denied}
102
+ </InlineStatus>
103
+ ) : (
104
+ <>
105
+ <Button
106
+ variant="outline"
107
+ onClick={() => {
108
+ setDecision("denied")
109
+ onDeny?.()
110
+ }}
111
+ >
112
+ Deny
113
+ </Button>
114
+ <Button
115
+ onClick={() => {
116
+ setDecision("allowed")
117
+ onApprove?.({ remember })
118
+ }}
119
+ >
120
+ Allow
121
+ </Button>
122
+ </>
123
+ )}
124
+ </CardFooter>
125
+ </Card>
126
+ )
127
+ }
128
+ ```
@@ -15,7 +15,7 @@ Use Accordion to reveal a vertical set of related sections while keeping the sur
15
15
  - Do: Use concise trigger text that describes the content revealed below it.
16
16
  - Do: Keep section content focused so expanding one item does not feel like navigating to a new page.
17
17
  - Don't: Do not hide required fields, primary actions, or critical errors inside collapsed sections.
18
- - Don't: Do not nest an Accordion inside an Accordion. Two layers of roving focus fight each other, and the arrow keys become ambiguous — flatten the hierarchy or use a Tree.
18
+ - Don't: Do not nest an Accordion inside an Accordion. Two layers of roving focus fight each other, and the arrow keys become ambiguous — flatten the hierarchy or use a Tree. → use **Tree**
19
19
 
20
20
  ### Anatomy
21
21
 
@@ -52,6 +52,7 @@ Use Accordion to reveal a vertical set of related sections while keeping the sur
52
52
  - **arrival** · Root · _no motion_ — An accordion is page furniture that is present from first paint. Animating it in would stage content the reader is already scanning past.
53
53
  - **response** · AccordionTrigger · `motion-interactive` — The whole header row is the hit target, and it looks like a heading rather than a control. The hover shift is what tells the reader it can be clicked before they click it.
54
54
  - **transition** · AccordionPanel · `motion-disclosure` — Panel height is the only cue that content came from THIS header rather than appearing at the bottom of the page. Snapping it open moves everything below by a screenful with nothing connecting cause to effect.
55
+ - **transition** · Chevron · `motion-interactive` — The chevron turning is the header's half of the same event as the panel opening. motion-interactive lists `rotate` (what Tailwind's rotate-* writes), so it turns with the panel instead of flipping 180° in one frame beside an easing panel. A custom `indicator` that rotates should carry motion-interactive itself.
55
56
  - **activity** · Root · _no motion_ — Nothing here is indeterminate — the panel content is already resident. A panel that loads on expand should put its own loader inside the panel.
56
57
 
57
58
  ## Accessibility
@@ -60,7 +61,7 @@ Use Accordion to reveal a vertical set of related sections while keeping the sur
60
61
  - Arrow keys move roving focus between triggers; Home/End jump to the first/last; loopFocus wraps by default.
61
62
  - Each trigger toggles its panel with Enter/Space; the panel is announced as a labelled region.
62
63
  - A disabled AccordionItem dims and is skipped by keyboard focus while neighbouring items stay interactive.
63
- - The chevron rotation is driven by data-[panel-open] and the focus ring by focus-visible — both honor reduced-motion via the consumer's transitions.
64
+ - The chevron rotation is driven by data-[panel-open] and the focus ring by focus-visible; both ride motion-interactive, so reduced motion stills them with everything else.
64
65
 
65
66
  ## Related
66
67
 
@@ -89,5 +90,5 @@ import { Accordion, AccordionItem, AccordionPanel, AccordionTrigger } from "@sab
89
90
  ```tsx
90
91
  import { Accordion, AccordionItem, AccordionPanel, AccordionTrigger, Icon } from "@sableui/react"
91
92
 
92
- <Accordion defaultValue={["a"]}><AccordionItem value="a"><AccordionTrigger indicator={<Icon name="plus" className="text-xs text-muted-foreground transition-transform [[data-panel-open]_&]:rotate-45" />}>Can I cancel anytime?</AccordionTrigger><AccordionPanel>Yes — from billing settings.</AccordionPanel></AccordionItem></Accordion>
93
+ <Accordion defaultValue={["a"]}><AccordionItem value="a"><AccordionTrigger indicator={<Icon name="plus" className="text-xs text-muted-foreground motion-interactive [[data-panel-open]_&]:rotate-45" />}>Can I cancel anytime?</AccordionTrigger><AccordionPanel>Yes — from billing settings.</AccordionPanel></AccordionItem></Accordion>
93
94
  ```
@@ -34,7 +34,8 @@ Use AreaChart for volume over time — visitors, revenue, usage — where the fi
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 (draw-on, rising fill) and the data-change morph are the flourish; the gliding cursor 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. |
@@ -48,9 +49,9 @@ Use AreaChart for volume over time — visitors, revenue, usage — where the fi
48
49
 
49
50
  ## Motion
50
51
 
51
- - **arrival** · Area + line · _custom (outside the recipe layer)_ — Once on screen, each line draws on (stroke-dashoffset) and its fill rises from the baseline, series staggered — the trend is read as it is drawn. On the motion tokens; reduced motion shows the finished chart.
52
+ - **arrival** · Area + line · _custom (outside the recipe layer)_ — Once on screen, each line draws on (stroke-dashoffset) and its fill rises from the baseline, series staggered — the trend is read as it is drawn. 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
53
  - **response** · Cursor + tooltip · _no motion_ — Hover and focus move the cursor and tooltip instantly — a lagging cursor reads as a slow chart.
53
- - **transition** · Area + line · _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.
54
+ - **transition** · Area + line · _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.
54
55
  - **activity** · AreaChart · _no motion_ — A chart has no indeterminate state; show a Skeleton while the data loads.
55
56
 
56
57
  ## Accessibility
@@ -54,6 +54,13 @@ The card a file takes in a conversation — in the composer while it uploads, an
54
54
 
55
55
  `card`, `border`, `border-strong`, `muted`, `muted-foreground`, `foreground`, `destructive`, `overlay`, `overlay-foreground`, `ring`
56
56
 
57
+ ## Motion
58
+
59
+ - **arrival** · Root · _no motion_ — An attachment lands as part of a composer or a message, and that surface owns the entrance. A card that animated in on its own would read as a second, separate event — as if another file had been added.
60
+ - **response** · Root · `motion-interactive` — With `onOpen`, the edge steps up under the pointer; it is the only cue that the card itself opens the file, not just its actions. Eased, it reads as the card answering the cursor rather than a border flickering as the pointer crosses a busy composer.
61
+ - **transition** · Root · `motion-interactive` — Uploading to failed re-colours the edge and fill without changing the shape. The ease keeps it legible as the same file turning red; snapped, a failed card looks like a different card replaced the one that was sent.
62
+ - **activity** · Progress · `motion-pulse` — While uploading with no known `progress`, the bar is indeterminate and pulses (Progress's own recipe); with a number, the fill eases toward each new value. A still bar at a fixed width is indistinguishable from an upload that has stalled.
63
+
57
64
  ## Accessibility
58
65
 
59
66
  - The full-card trigger is a real stretched `<button>` with an accessible label, not a click handler on a div — so it is one tab stop, keyboard-activatable, and focus-ringed.
@@ -16,7 +16,7 @@ Use Badge for any short static label: metadata, categories, tone-coded labels, a
16
16
  - Do: Use tone variants when the badge carries meaning, such as success, warning or destructive, or a decorative tone for a category or plan.
17
17
  - Do: Pair icons with text; the icon reinforces meaning but should not be the only cue.
18
18
  - Do: Choose appearance and tone separately: appearance is how loud it is (soft by default, solid for emphasis, overlay on images, plain for a status line); tone is what it means.
19
- - Don't: Use a Badge as a button. If it triggers an action, use Button, Toggle, or a menu item.
19
+ - Don't: Use a Badge as a button. If it triggers an action, use Button, Toggle, or a menu item. → use **Button**, **Toggle**
20
20
  - Don't: Stack many loud tone badges in one row; too many semantic colors compete with the actual content.
21
21
 
22
22
  ### Anatomy
@@ -16,8 +16,8 @@ Use Banner for page-level or app-level messages that affect the current experien
16
16
  - Do: Set live='polite' or live='assertive' only when the banner appears or changes dynamically.
17
17
  - Do: Include one clear action when the user can resolve or inspect the state.
18
18
  - Don't: Use multiple stacked solid banners unless the page is genuinely blocked by multiple independent states.
19
- - Don't: Use Banner for field-level validation, row status, or small local hints; use Field, InlineStatus, or Badge instead.
20
- - Don't: Use Banner inside a dialog, a form, or a section to qualify the content around it — that is Note. Banner is a full-width announcement strip for page- or app-level state, so in a modal it reads as a slab heavier than the fields it warns about, and its soft appearance has no border and no tone-colored icon: the severity survives only as a faint wash.
19
+ - Don't: Use Banner for field-level validation, row status, or small local hints; use Field, InlineStatus, or Badge instead. → use **InlineStatus**, **Badge**
20
+ - Don't: Use Banner inside a dialog, a form, or a section to qualify the content around it — that is Note. Banner is a full-width announcement strip for page- or app-level state, so in a modal it reads as a slab heavier than the fields it warns about, and its soft appearance has no border and no tone-colored icon: the severity survives only as a faint wash. → use **Note**
21
21
 
22
22
  ### Anatomy
23
23
 
@@ -34,7 +34,8 @@ Use BarChart to compare amounts across categories — the default chart for "how
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 (bars rising in sequence) and the data-change morph are the flourish; the hover dim and gliding cursor 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. |
@@ -52,9 +53,9 @@ Use BarChart to compare amounts across categories — the default chart for "how
52
53
 
53
54
  ## Motion
54
55
 
55
- - **arrival** · Bar · _custom (outside the recipe layer)_ — Once on screen, bars rise from the baseline in sequence on the emphasized easing (scale from the zero line) and their labels fade in. On the motion tokens; reduced motion shows the finished chart.
56
- - **response** · Cursor + tooltip · _no motion_ — The hovered category gets a band behind it and the other bars dim at once — a lagging highlight reads as a slow chart.
57
- - **transition** · Bar · _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.
56
+ - **arrival** · Bar · _custom (outside the recipe layer)_ — Once on screen, bars rise from the baseline in sequence on the emphasized easing (scale from the zero line) and their 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.
57
+ - **response** · Cursor + tooltip · _no motion_ — The hovered category gets a band behind it and the other bars dim at once — a lagging highlight reads as a slow chart. The dim mixes each bar toward the surface instead of fading it, so gridlines never strike through a dimmed column and stacked segments stay solid. Essential: `subtle` keeps it.
58
+ - **transition** · Bar · _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.
58
59
  - **activity** · BarChart · _no motion_ — A chart has no indeterminate state; show a Skeleton while the data loads.
59
60
 
60
61
  ## Accessibility
@@ -34,6 +34,14 @@ Use BottomNav for a small set of top-level mobile destinations that should remai
34
34
 
35
35
  `background`, `border`, `muted-foreground`, `foreground`, `ring`
36
36
 
37
+ ## Motion
38
+
39
+ - **arrival** · Root · _no motion_ — Fixed app chrome, present from the first paint of every screen. Animating it in on navigation would make each route change look like the app restarting.
40
+ - **response** · BottomNavItem · `motion-interactive` — The ink steps up on hover and the focus ring eases in, so a pointer or a keyboard on a tablet gets the same answer as every other control in the app.
41
+ - **response** · BottomNavItem · `motion-press` — On a phone there is no hover to say a tap landed, and a route can take a beat to paint. The settle on press is the receipt; without it people tap the tab a second time.
42
+ - **transition** · BottomNavItem · `motion-interactive` — Changing tab eases the active ink out of one item and into the next. It is a cross-fade, not a travelling indicator: each item owns its colour, so there is no single mark to glide.
43
+ - **activity** · Root · _no motion_ — Navigation has no working state of its own. A slow route shows its pending state in the page it is loading, not in the tab bar.
44
+
37
45
  ## Accessibility
38
46
 
39
47
  - BottomNav renders a <nav> landmark.
@@ -45,6 +45,13 @@ Use Breadcrumbs to show the user's location inside a hierarchy and provide light
45
45
 
46
46
  `muted-foreground`, `foreground`
47
47
 
48
+ ## Motion
49
+
50
+ - **arrival** · Root · _no motion_ — The trail is page furniture, rendered with the page header. An entrance would replay on every navigation, which is exactly when the reader looks at it to get their bearings.
51
+ - **response** · BreadcrumbLink · `motion-interactive` — Ancestors are muted until hovered or focused. Easing the ink makes the trail read as links answering the pointer; a hard snap across a row of small text looks like flicker.
52
+ - **transition** · Root · _no motion_ — A breadcrumb has no state of its own. The current page changes by navigation, which replaces the trail; animating between two trails would imply the path itself moved.
53
+ - **activity** · Root · _no motion_ — It never works in the background. A route that is still loading says so in the page, not in its address.
54
+
48
55
  ## Accessibility
49
56
 
50
57
  - The <nav> carries aria-label="Breadcrumb" so assistive tech announces the landmark.
@@ -16,7 +16,7 @@ Use ButtonGroup when one action has close alternatives (Copy page, with Open in
16
16
  - Do: For a split button, put the main action first and a Menu whose trigger is the chevron Button second, with an aria-label on the trigger ("More page actions").
17
17
  - Do: Name the group with aria-label when the buttons alone do not say what they act on.
18
18
  - Don't: Do not round or border the children yourself to make them meet; that is the group's job, and an override breaks under a theme's button shape.
19
- - Don't: Do not use it for a selection that stays on. That is ToggleGroup, which carries the pressed state.
19
+ - Don't: Do not use it for a selection that stays on. That is ToggleGroup, which carries the pressed state. → use **ToggleGroup**
20
20
 
21
21
  ## Props
22
22