@sableui/react 0.13.2 → 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 (182) hide show
  1. package/AGENTS.md +2 -2
  2. package/CHANGELOG.md +114 -0
  3. package/CONTRIBUTING.md +6 -4
  4. package/DESIGN.md +8 -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 +16 -1
  8. package/dist/blocks/index.js +4 -0
  9. package/dist/components/accordion/accordion.meta.js +8 -2
  10. package/dist/components/area-chart/area-chart.meta.js +10 -3
  11. package/dist/components/attachment/attachment.meta.js +26 -0
  12. package/dist/components/bar-chart/bar-chart.meta.js +11 -4
  13. package/dist/components/bottom-nav/bottom-nav.js +3 -1
  14. package/dist/components/bottom-nav/bottom-nav.meta.js +32 -0
  15. package/dist/components/breadcrumbs/breadcrumbs.meta.js +26 -0
  16. package/dist/components/button/button.js +21 -4
  17. package/dist/components/button/button.meta.js +7 -7
  18. package/dist/components/calendar/calendar.js +18 -1
  19. package/dist/components/calendar/calendar.meta.js +38 -0
  20. package/dist/components/carousel/carousel.js +6 -4
  21. package/dist/components/carousel/carousel.meta.js +42 -0
  22. package/dist/components/chat-composer/chat-composer.js +21 -21
  23. package/dist/components/chat-composer/chat-composer.meta.js +38 -0
  24. package/dist/components/chat-composer/use-eased-growth.js +70 -0
  25. package/dist/components/checkbox/checkbox-mark.js +8 -1
  26. package/dist/components/checkbox/checkbox.js +3 -1
  27. package/dist/components/checkbox/checkbox.meta.js +32 -0
  28. package/dist/components/checkbox-card/checkbox-card.js +3 -0
  29. package/dist/components/checkbox-card/checkbox-card.meta.js +32 -0
  30. package/dist/components/code-block/code-block.js +14 -3
  31. package/dist/components/code-block/code-block.meta.js +28 -1
  32. package/dist/components/collapsible/collapsible.meta.js +32 -0
  33. package/dist/components/color-field/color-field.meta.js +26 -0
  34. package/dist/components/combobox/combobox.meta.js +44 -0
  35. package/dist/components/command-menu/command-menu.js +8 -3
  36. package/dist/components/command-menu/command-menu.meta.js +38 -0
  37. package/dist/components/confirm-button/confirm-button.js +179 -0
  38. package/dist/components/confirm-button/confirm-button.meta.js +143 -0
  39. package/dist/components/confirm-button/index.js +7 -0
  40. package/dist/components/data-table/data-table.js +6 -2
  41. package/dist/components/data-table/data-table.meta.js +32 -0
  42. package/dist/components/entity/entity.meta.js +26 -0
  43. package/dist/components/feedback/feedback.js +113 -47
  44. package/dist/components/feedback/feedback.meta.js +49 -4
  45. package/dist/components/input-group/input-group.meta.js +27 -1
  46. package/dist/components/line-chart/line-chart.meta.js +10 -3
  47. package/dist/components/middle-truncate/middle-truncate.meta.js +1 -1
  48. package/dist/components/note/note.meta.js +32 -0
  49. package/dist/components/notification-badge/notification-badge.js +17 -5
  50. package/dist/components/notification-badge/notification-badge.meta.js +34 -2
  51. package/dist/components/number-ticker/index.js +7 -0
  52. package/dist/components/number-ticker/number-ticker.js +246 -0
  53. package/dist/components/number-ticker/number-ticker.meta.js +159 -0
  54. package/dist/components/otp-field/otp-field.js +23 -3
  55. package/dist/components/otp-field/otp-field.meta.js +23 -2
  56. package/dist/components/panel-group/panel-group.meta.js +26 -0
  57. package/dist/components/panel-section/panel-section.js +37 -10
  58. package/dist/components/panel-section/panel-section.meta.js +32 -0
  59. package/dist/components/pie-chart/pie-chart.js +20 -11
  60. package/dist/components/pie-chart/pie-chart.meta.js +17 -4
  61. package/dist/components/pill/pill.meta.js +32 -0
  62. package/dist/components/radar-chart/radar-chart.js +4 -3
  63. package/dist/components/radar-chart/radar-chart.meta.js +10 -3
  64. package/dist/components/radial-chart/radial-chart.js +22 -22
  65. package/dist/components/radial-chart/radial-chart.meta.js +17 -4
  66. package/dist/components/radio/radio.meta.js +26 -0
  67. package/dist/components/radio-card/radio-card.meta.js +38 -0
  68. package/dist/components/relative-time/relative-time.meta.js +32 -0
  69. package/dist/components/scroller/scroller.meta.js +32 -0
  70. package/dist/components/scrub-field/scrub-field.meta.js +32 -0
  71. package/dist/components/search-input/search-input.js +9 -7
  72. package/dist/components/search-input/search-input.meta.js +26 -0
  73. package/dist/components/show-more/show-more.js +1 -4
  74. package/dist/components/show-more/show-more.meta.js +38 -0
  75. package/dist/components/side-nav/side-nav.js +21 -1
  76. package/dist/components/side-nav/side-nav.meta.js +32 -0
  77. package/dist/components/side-nav/side-nav.variants.js +3 -1
  78. package/dist/components/sidebar/sidebar.js +31 -8
  79. package/dist/components/sidebar/sidebar.meta.js +38 -0
  80. package/dist/components/slider/slider.meta.js +26 -0
  81. package/dist/components/sparkline/sparkline.js +7 -4
  82. package/dist/components/sparkline/sparkline.meta.js +9 -2
  83. package/dist/components/table/table.meta.js +26 -0
  84. package/dist/components/tabs/tabs.js +27 -8
  85. package/dist/components/tabs/tabs.meta.js +46 -3
  86. package/dist/components/theme-selector/theme-selector.meta.js +32 -0
  87. package/dist/components/theme-switcher/theme-switcher.js +4 -1
  88. package/dist/components/theme-switcher/theme-switcher.meta.js +42 -0
  89. package/dist/components/toggle/toggle.js +28 -5
  90. package/dist/components/toggle/toggle.meta.js +44 -2
  91. package/dist/components/tool-call/index.js +8 -0
  92. package/dist/components/tool-call/tool-call.js +117 -0
  93. package/dist/components/tool-call/tool-call.meta.js +147 -0
  94. package/dist/components/tool-call/tool-call.variants.js +33 -0
  95. package/dist/components/tooltip/index.js +3 -1
  96. package/dist/components/tooltip/tooltip.js +84 -0
  97. package/dist/components/tooltip/tooltip.meta.js +16 -1
  98. package/dist/components/tree/tree.js +71 -6
  99. package/dist/components/tree/tree.meta.js +32 -0
  100. package/dist/components/video/video.meta.js +32 -0
  101. package/dist/file-hashes.json +114 -97
  102. package/dist/foundations/motion.js +49 -3
  103. package/dist/index.d.ts +614 -44
  104. package/dist/index.js +4 -1
  105. package/dist/lib/chart/cartesian.js +9 -10
  106. package/dist/lib/chart/chart-center.js +30 -0
  107. package/dist/lib/chart/chart-frame.js +15 -8
  108. package/dist/lib/chart/dim.js +13 -0
  109. package/dist/lib/chart/format.js +7 -1
  110. package/dist/lib/chart/motion.js +6 -4
  111. package/dist/lib/highlight-glide.js +68 -10
  112. package/dist/lib/taxonomy.js +3 -0
  113. package/dist/motion/MotionProvider.js +45 -3
  114. package/dist/motion/Reveal.js +1 -1
  115. package/dist/motion/TextReveal.js +1 -1
  116. package/dist/motion/index.js +15 -2
  117. package/dist/motion/useAutoSize.js +42 -0
  118. package/dist/motion/useShake.js +25 -0
  119. package/dist/styles.css +298 -5
  120. package/llms/blocks/approval-block.md +128 -0
  121. package/llms/components/accordion.md +3 -2
  122. package/llms/components/area-chart.md +4 -3
  123. package/llms/components/attachment.md +7 -0
  124. package/llms/components/bar-chart.md +5 -4
  125. package/llms/components/bottom-nav.md +8 -0
  126. package/llms/components/breadcrumbs.md +7 -0
  127. package/llms/components/button.md +6 -6
  128. package/llms/components/calendar.md +9 -0
  129. package/llms/components/carousel.md +17 -0
  130. package/llms/components/chat-composer.md +9 -0
  131. package/llms/components/checkbox-card.md +8 -0
  132. package/llms/components/checkbox.md +8 -0
  133. package/llms/components/code-block.md +8 -0
  134. package/llms/components/collapsible.md +8 -0
  135. package/llms/components/color-field.md +7 -0
  136. package/llms/components/combobox.md +10 -0
  137. package/llms/components/command-menu.md +9 -0
  138. package/llms/components/confirm-button.md +92 -0
  139. package/llms/components/data-table.md +8 -0
  140. package/llms/components/entity.md +7 -0
  141. package/llms/components/feedback.md +22 -3
  142. package/llms/components/input-group.md +8 -1
  143. package/llms/components/line-chart.md +4 -3
  144. package/llms/components/middle-truncate.md +1 -1
  145. package/llms/components/note.md +8 -0
  146. package/llms/components/notification-badge.md +10 -2
  147. package/llms/components/number-ticker.md +86 -0
  148. package/llms/components/otp-field.md +15 -1
  149. package/llms/components/panel-group.md +7 -0
  150. package/llms/components/panel-section.md +8 -0
  151. package/llms/components/pie-chart.md +6 -4
  152. package/llms/components/pill.md +8 -0
  153. package/llms/components/radar-chart.md +4 -3
  154. package/llms/components/radial-chart.md +6 -4
  155. package/llms/components/radio-card.md +9 -0
  156. package/llms/components/radio.md +7 -0
  157. package/llms/components/relative-time.md +8 -0
  158. package/llms/components/scroller.md +8 -0
  159. package/llms/components/scrub-field.md +8 -0
  160. package/llms/components/search-input.md +7 -0
  161. package/llms/components/show-more.md +9 -0
  162. package/llms/components/side-nav.md +8 -0
  163. package/llms/components/sidebar.md +9 -0
  164. package/llms/components/slider.md +7 -0
  165. package/llms/components/sparkline.md +3 -2
  166. package/llms/components/table.md +7 -0
  167. package/llms/components/tabs.md +20 -2
  168. package/llms/components/theme-selector.md +8 -0
  169. package/llms/components/theme-switcher.md +17 -0
  170. package/llms/components/toggle.md +19 -2
  171. package/llms/components/tool-call.md +115 -0
  172. package/llms/components/tooltip.md +11 -1
  173. package/llms/components/tree.md +8 -0
  174. package/llms/components/video.md +8 -0
  175. package/llms/foundations/motion.md +48 -3
  176. package/llms/reference/utilities.md +23 -1
  177. package/llms/tasks/reviewing-motion.md +14 -0
  178. package/llms-full.txt +955 -49
  179. package/llms.txt +15 -10
  180. package/package.json +1 -1
  181. package/registry.json +2565 -210
  182. package/targets/ios.json +27 -0
package/AGENTS.md CHANGED
@@ -42,7 +42,7 @@ These are short on purpose. Each links to a foundations doc with the full vocabu
42
42
  3. **Space on the 4px scale, by role.** Inset, stack, inline; related things closer than unrelated things. No off-scale `p-[15px]`. Padding, gaps and insets stay on the scale (a step, or `--spacing(n)` inside an arbitrary value) so the density control moves them; fixed `rem`/`px` only for what must not scale, like a hairline. Single-column content in a card is a flex column, not a grid. → `sable docs spacing`
43
43
  4. **Controls read the ladder.** Heights come from `--control-*`, so `<Density>` can retune a region at once. Never `h-[40px]`. → `sable docs spacing`
44
44
  5. **Shape and depth are roles.** Radii from the `rounded-*` ladder, concentric when nested (inner = outer − inset; `rounded-concentric-*`, `sable-frame-inner`); elevation by transience (resting → raised → overlay → modal → toast), one shadow per surface. → `sable docs shape` · `sable docs surfaces`
45
- 6. **Motion has a reason and a recipe.** Apply a named `motion-*` recipe; drop to `duration-(--motion-duration-*)` + `ease-*` tokens only when none fits. Never Tailwind's `animate-*`, never a raw `duration-150`. Exits are faster than entrances and never overshoot. → `sable docs motion`
45
+ 6. **Motion has a reason and a recipe.** Apply a named `motion-*` recipe; drop to `duration-(--motion-duration-*)` + `ease-*` tokens only when none fits. Never Tailwind's `animate-*`, never a raw `duration-150`. Exits are faster than entrances and never overshoot. Components ship their craft (gliding indicators, rolling numbers, drawn ticks) on by default; to calm a dense view use `motion="subtle"` on the component or `<MotionScope level="subtle">` around a region, never a CSS override. → `sable docs motion`
46
46
  7. **Icons by semantic name.** `<Icon name="check" />`; size with `text-{size}`, never type utilities. A mark attached to a line of type (dot, bullet, leading icon) sits in an `h-[1lh] items-center` box and is sized in em, never nudged with px. → `sable docs typography`
47
47
  8. **Every scroll container declares a policy:** `scrollbar-quiet` (vertical) or `scrollbar-hidden` (horizontal, with an edge fade carrying the affordance).
48
48
  9. **Accessible by construction.** WCAG AA contrast (and a label on a brand or status fill also clears APCA Lc 60 — `label-contrast`), 44px hit areas, visible focus, logical properties, live regions that exist before they speak. → `sable docs accessibility`
@@ -65,7 +65,7 @@ The machine-readable version of this section is the `consume` block in `registry
65
65
 
66
66
  `styles.css` carries **Sable only** — the default theme, and the layout floor every other theme inherits (`--page-inset`, `--region-*`, `--rhythm-unit` on a bare `:root`). The bundled **alternates are opt-in**, because they are `[data-sable-theme]`-scoped and an app that never sets the attribute ships CSS that can never match: 5.5 KB gzip of permanently-dead weight, which is exactly how a real app's build audit found this. Using one? `@import "@sableui/react/theme/prism.css";`. Offering the whole set through a `<ThemeSelector>`? `@import "@sableui/react/themes.css";`. **Symptom of forgetting:** `data-sable-theme` is set, nothing matches, and the page silently renders as the Sable theme — no error, just the wrong theme.
67
67
  4. **Fonts:** load Figtree (sans) + DM Mono (mono) — the theme declares the stacks, the host provides the files. Apply `antialiased` (`-webkit-font-smoothing: antialiased` / `-moz-osx-font-smoothing: grayscale`) **once on the app root**: Sable's type scale was drawn against smoothed text, and without it macOS renders every weight heavier than designed. The package deliberately doesn't set it — a library has no business styling the host's whole page — so it has to happen here.
68
- 5. **Providers:** `<IconProvider>` is **required** — wrap the app once in `<IconProvider set={reiconIconSet}>` with `import { reiconIconSet } from "@sableui/react/icons/reicon"` (without it every `<Icon>` renders nothing; development builds warn once). Pass another icon package's set instead to replace Reicon, or extend it using `composeIconSets(appExtras, reiconIconSet)` (list the added names in `.sable/icons.json` as `{ "names": [...] }` so `sable grade` accepts them); `<MotionProvider>` is optional too. Use `<SableThemeProvider>` + `useSableTheme()` when the app should expose user-selectable themes; pair `themeId` / `setThemeId` with `<ThemeSelector>` and `preference` / `setPreference` with `<ThemeSwitcher>`. The provider stores `light` / `dark` / `system`, toggles the `dark` class on `<html>`, stores the active design theme, and writes `data-sable-theme` for non-default themes. Apps with their own state layer can call `applySableColorMode()` / `applySableTheme()` instead. You only need a Tailwind `@custom-variant dark` if **your** app code uses `dark:` utilities (Sable ships none).
68
+ 5. **Providers:** `<IconProvider>` is **required** — wrap the app once in `<IconProvider set={reiconIconSet}>` with `import { reiconIconSet } from "@sableui/react/icons/reicon"` (without it every `<Icon>` renders nothing; development builds warn once). Pass another icon package's set instead to replace Reicon, or extend it using `composeIconSets(appExtras, reiconIconSet)` (list the added names in `.sable/icons.json` as `{ "names": [...] }` so `sable grade` accepts them); `<MotionProvider>` is optional too (`policy="subtle"` keeps feedback and drops flourishes app-wide). Use `<SableThemeProvider>` + `useSableTheme()` when the app should expose user-selectable themes; pair `themeId` / `setThemeId` with `<ThemeSelector>` and `preference` / `setPreference` with `<ThemeSwitcher>`. The provider stores `light` / `dark` / `system`, toggles the `dark` class on `<html>`, stores the active design theme, and writes `data-sable-theme` for non-default themes. Apps with their own state layer can call `applySableColorMode()` / `applySableTheme()` instead. You only need a Tailwind `@custom-variant dark` if **your** app code uses `dark:` utilities (Sable ships none).
69
69
  6. Import from the root barrel: `import { Button } from "@sableui/react"`. Page shells live on the `patterns` subpath (`import { SettingsPage } from "@sableui/react/patterns"`) — preview one, then copy its source from `registry.json → patterns` when the route needs to diverge. Blocks — section-sized compositions such as a sign-in card or a pricing card — live on the `blocks` subpath (`import { SignInBlock } from "@sableui/react/blocks"`) and ship the same way (`registry.json → blocks`); copy one and edit its content constants. A block's props are only `className`, `framed` and its primary action's callback, never content.
70
70
 
71
71
  7. **Check the wiring:** `npx sable doctor`. Every check it runs corresponds to a way Sable fails *silently* — a second React (blank page, "invalid hook call"), a missing `@source` (Tailwind emits none of the package's classes, everything renders unstyled), or `data-sable-theme` set with no theme CSS imported (every theme renders as the Sable theme and the switcher looks broken). Exits 1 on any failure; `--json` for the structured report.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,119 @@
1
1
  # What's new
2
2
 
3
+ ## 0.14.0
4
+
5
+ Motion that is crafted by default and calm on request, and the first pieces for agent products.
6
+
7
+ - **New: two motion levels, and `subtle`.** Motion is now *essential*
8
+ (colour and focus feedback, press, overlay arrival, disclosure, loaders,
9
+ chart hover) or *expressive* (glides, rolling numbers, drawn ticks,
10
+ shakes, staggers, chart arrival and morph, scroll reveals, spring
11
+ overshoot). Expressive durations also multiply by `--motion-expressive`,
12
+ which the new `subtle` policy sets to 0, and `subtle` takes the overshoot
13
+ out of `ease-spring` and `ease-bounce`. So `full` (the default) plays both,
14
+ `subtle` keeps every answer and drops every flourish, and `off` stills
15
+ everything. Set it app-wide with `<MotionProvider policy="subtle">`, for a
16
+ region with the new `<MotionScope level="subtle">`, or per component with a
17
+ new `motion` prop. Scopes and props can only turn motion **down**: `full`
18
+ on a component means "inherit", so nothing can override a reader's
19
+ reduce-motion setting. `useMotion()` gains `expressive`, and
20
+ `useMotionLevel(motion)` resolves a component's prop against everything
21
+ around it. `Reveal` and `TextReveal` are expressive: under `subtle` the
22
+ content is simply there.
23
+ - **New recipes:** `motion-glide` (one indicator travelling to the selected
24
+ option), `motion-shake` (a refused answer, driven by the new `useShake()`),
25
+ `motion-tick` (a mark that draws itself in), `motion-appear` (a small
26
+ control mounting in place, through `@starting-style`), `motion-arrive`
27
+ (items a user action adds to a list, staggered) and `motion-size` (easing
28
+ between sizes, with the new `useAutoSize()` measuring a label change).
29
+ `sable utility <name>` has each one.
30
+ - **New: `NumberTicker`.** A number that rolls to its new value, digit by
31
+ digit, keyed by place value, so 999 → 1,000 rolls the columns it keeps and
32
+ grows the one it gains. It formats through `Intl.NumberFormat`, reads as
33
+ plain text to assistive tech and copy, and lands without a roll under
34
+ `subtle`. `NotificationBadge` counts and the `PieChart` / `RadialChart`
35
+ centre values now roll through it.
36
+ - **New: `ConfirmButton`.** An inline two-step for low-stakes destructive
37
+ actions. The first press arms it: the label changes, the button eases to
38
+ the new width, a thin line drains while it waits, and the armed state is
39
+ announced. A second press within `timeout` confirms; Escape, a click
40
+ elsewhere, blur or the timeout disarms it, and a double-click cannot
41
+ confirm. `DestructiveActionModal` stays the answer for high stakes.
42
+ - **New: `TooltipGroup`.** Wrap a row of `SimpleTooltip`s and they share one
43
+ tooltip: moving along a toolbar, it glides to the next button and its label
44
+ slides across instead of closing and reopening.
45
+ - **Selections travel.** `Tabs` renders a gliding indicator in both variants
46
+ (the underline and the pill), and a changed panel fades in. Single-select
47
+ `ToggleGroup appearance="segmented"` has one fill that glides and resizes
48
+ to the pressed option. `ThemeSwitcher` uses the same recipe, and
49
+ `Carousel`'s active dot eases its width. `SideNav` and `SidebarNav` draw
50
+ the current page as one fill that glides to the new item when the page
51
+ changes inside a persistent layout. Menu, Select and Combobox highlights
52
+ keep their glide, which is now expressive.
53
+ - **Calendar pages with direction.** Going to the next or previous month
54
+ slides the old grid out toward the edge it leaves by and the new one in
55
+ from the other, over a short travel with a fade (react-day-picker's
56
+ `animate` mode on Sable keyframes, mirrored in RTL). It is on at full
57
+ motion only (a `DatePicker`'s calendar follows the app or scope policy);
58
+ `Calendar` takes `motion`, and an explicit `animate` still wins.
59
+ - **Sidebar collapse eases.** Collapsing to the icon rail eases the width
60
+ instead of jumping 192px in one frame, so the content beside it visibly
61
+ takes the room. Icons stay on one column throughout and labels fade back
62
+ in on reopen. `Sidebar` takes `motion`; `subtle` snaps.
63
+ - **Focus outlines ease in.** `motion-interactive` now also transitions
64
+ `outline-color`, `outline-width` and `outline-offset`, so a keyboard focus
65
+ ring drawn with `outline-*` grows from the element's edge over the fast
66
+ step instead of appearing in one frame. Instant under reduced motion.
67
+ - **States finish their story.** `OtpField` shakes once when it turns
68
+ invalid, and its digits take the destructive colour. `CodeBlock`'s copy
69
+ button swaps to a check in place and announces "Copied". `Feedback` shows a
70
+ drawn check and a thank-you before it closes. A loading `Button` keeps its
71
+ width: the spinner lays over the label instead of replacing it. The
72
+ `ChatComposer` grows with an ease, and its send and stop swap in one
73
+ button. `SearchInput` and `CommandMenu`'s clear buttons appear rather than
74
+ pop. Expanding a `Tree` folder settles its rows in, staggered. The
75
+ `Checkbox` tick draws in from its leading stroke.
76
+ - **Fixed: disclosure chevrons snapped.** Tailwind v4's `rotate-*` writes the
77
+ standalone `rotate` property, which `motion-interactive` did not list, so
78
+ the chevrons on `Accordion`, `Collapsible`, `Tree` and `ShowMore` jumped
79
+ 180° in one frame while the panel beside them eased. The recipe now lists
80
+ `rotate`. `DataTable`'s sort chevron rotates instead of swapping glyphs.
81
+ - **Chart hover dims mix toward the surface.** The bars, slices and rings
82
+ that are not under the pointer now step back by mixing toward the surface
83
+ colour instead of fading, so gridlines no longer strike through a dimmed
84
+ bar and a dimmed ring stays solid over its track.
85
+ - **Deprecated: `animate` on charts.** Every chart takes `motion` instead.
86
+ `animate={false}` maps to `motion="subtle"` (no arrival or morph, hover
87
+ feedback kept); use `motion="off"` to still everything.
88
+ - **Motion declaration is a build gate.** A component that reacts to the
89
+ pointer or carries state must declare its four motion slots (arrival,
90
+ response, transition, activity) in its meta, or decline each with a reason.
91
+ It used to be a report, and 36 components said nothing; all of them now
92
+ answer.
93
+ - **Docs:** a new **Interactions** page shows the catalog by task (inspect,
94
+ select, confirm, recover, succeed, finish loading, type, point, notify).
95
+ Every preview has its own motion level and Replay. Playgrounds gain a Motion
96
+ row and **Copy for agent**, which copies the exact JSX, imports, install
97
+ commands, the active theme and `sable component <Name>`. The Motion page
98
+ sets the policy to Full, Subtle or Off and shows the two levels side by
99
+ side.
100
+
101
+ - **New: `ToolCall`.** One tool an agent called, as a quiet row inside its
102
+ turn: a status glyph, what it did in plain words, the tool's name in mono,
103
+ and one word for where it is (`running`, `awaiting`, `complete`, `error`,
104
+ `denied`). Input and output sit behind a disclosure, as formatted JSON or
105
+ as a rendered result passed as `children`. A failure's message shows under
106
+ the row without opening anything. When a call settles, its spinner swaps
107
+ for the outcome in place (`motion-icon-swap`), and that's the only motion
108
+ in the row. The change is announced once, and a thread restored from
109
+ history announces nothing.
110
+ - **New: `ApprovalBlock`**, in a new "AI & agents" block family. An agent
111
+ asking permission for one call: the question in plain words, what can't be
112
+ undone, the call's details, an "allow for the rest of this chat" Checkbox,
113
+ and Deny / Allow. Neither button is focused on arrival. After a decision,
114
+ one status line replaces the buttons, focus moves to it, and it's
115
+ announced. `onApprove({ remember })` and `onDeny()` report the answer.
116
+
3
117
  ## 0.13.2
4
118
 
5
119
  What to use instead, wherever a component is the wrong pick.
package/CONTRIBUTING.md CHANGED
@@ -168,7 +168,9 @@ artwork, and nothing in the core graph imports `src/icon-sets/`.
168
168
 
169
169
  ## Motion
170
170
 
171
- Motion is a first-class layer — **subtle by default, tasteful spring welcome, zero runtime dependency** (CSS tokens/recipes + tiny browser-native JS). When a component has any interaction, wire it to the system; don't leave it static or hand-roll a transition.
171
+ Motion is a first-class layer — **crafted by default, calm on request, zero runtime dependency** (CSS tokens/recipes + tiny browser-native JS). When a component has any interaction, wire it to the system; don't leave it static or hand-roll a transition. When its selection, value or mark changes in place, move the one element that represents it (`motion-glide`, `NumberTicker`, `motion-tick`, `motion-icon-swap`) rather than switching two things on and off.
172
+
173
+ **Two levels.** Essential motion (feedback, press, overlay arrival, disclosure, loaders) runs on the plain tokens. Expressive motion (glides, rolling numbers, drawn ticks, shakes, staggers, arrival choreography, overshoot) multiplies its durations by `var(--motion-expressive, 1)` as well — the shipped expressive recipes already do — and JS-driven flourishes gate on `useMotionLevel(motion).expressive`. A component that owns a flourish takes `motion?: MotionLevel` (`full` | `subtle` | `off`) and spreads `data-motion={motionAttribute(motion)}` on its root; the prop only turns motion down.
172
174
 
173
175
  **Four slots.** Every component answers all four — by filling the slot, or by declining it in `meta.motion` with `recipe: "none"` and a reason:
174
176
 
@@ -179,13 +181,13 @@ Motion is a first-class layer — **subtle by default, tasteful spring welcome,
179
181
  | `transition` | how does it move between its own states? | `motion-disclosure`, or ease the transform |
180
182
  | `activity` | how do you tell "working" from "frozen"? | `motion-spin` · `motion-pulse` · `motion-shimmer` |
181
183
 
182
- This paragraph said "don't leave it static" for many releases while **nothing checked it**. The four motion rules below are all prohibitive — they fire on motion that is present and wrong, and none can fire on motion that is absent — so a component that snapped between every state passed the whole contract, and motion stayed a thing a human had to request by name. Two gates now close that: `static-interactive` (lint, fires on an element styled for pointer states with no recipe on it) and `meta.motion` + `pnpm build:registry -- --motion-report` (declaration and coverage).
184
+ This paragraph said "don't leave it static" for many releases while **nothing checked it**. The four motion rules below are all prohibitive — they fire on motion that is present and wrong, and none can fire on motion that is absent — so a component that snapped between every state passed the whole contract, and motion stayed a thing a human had to request by name. Two gates now close that: `static-interactive` (lint, fires on an element styled for pointer states with no recipe on it) and `meta.motion` — `build-registry` **fails** when a component that reacts to the pointer or carries state declares no `motion` array (`pnpm build:registry -- --motion-report` prints the slot-by-slot coverage).
183
185
 
184
- 1. **Reach for a recipe first** — apply one named class instead of a bespoke transition string: `motion-interactive` (default hover/focus/press chrome — **replaces** `transition-colors`), `motion-press` (tactile press; always pair with `motion-interactive`), `motion-fade`/`motion-pop`/`motion-slide-{up,down,left,right}` (overlay enter/exit, on Base UI `data-[starting-style]`/`data-[ending-style]`), `motion-disclosure` (height), and loaders `motion-{spin,shimmer,pulse,dot-bounce,ping}`.
186
+ 1. **Reach for a recipe first** — apply one named class instead of a bespoke transition string: `motion-interactive` (default hover/focus/press chrome — **replaces** `transition-colors`), `motion-press` (tactile press; always pair with `motion-interactive`), `motion-fade`/`motion-pop`/`motion-slide-{up,down,left,right}` (overlay enter/exit, on Base UI `data-[starting-style]`/`data-[ending-style]`), `motion-disclosure` (height), `motion-icon-swap` (a glyph that changes with state), the expressive `motion-{glide,shake,tick,appear,arrive,size}`, and loaders `motion-{spin,shimmer,pulse,dot-bounce,ping}`.
185
187
  2. **Drop to tokens only when a recipe doesn't fit.** Durations via `duration-(--motion-duration-{instant|fast|base|slow|slower})` — **never** a raw `duration-150` (it bypasses the kill-switch). Easings via `ease-{standard|decelerate|emphasized|spring|bounce}`. Spring/bounce overshoot is **enter-only**; exits stay clean.
186
188
  3. **Never** use Tailwind's built-in `animate-spin`/`animate-pulse`/`animate-ping`/`animate-bounce` — they ignore `--motion-scale`, so the on/off policy can't freeze them. Use the scale-aware `motion-*` loader recipes.
187
189
  4. **Compositor-first & subtle:** animate opacity + transform (scale/translate) only; short durations; no exit overshoot; no gaudy motion.
188
- 5. **Policy:** all token-driven motion collapses to static under `prefers-reduced-motion` or `<MotionProvider>` off (one `--motion-scale: 0`). JS primitives read `useMotion().enabled` and must no-op when motion is off.
190
+ 5. **Policy:** all token-driven motion collapses to static under `prefers-reduced-motion` or `<MotionProvider>` off (one `--motion-scale: 0`); `subtle` zeroes `--motion-expressive` only. JS primitives read `useMotion().enabled` and must no-op when motion is off; JS flourishes read `expressive`.
189
191
  6. **JS motion lives in `src/motion/`** (zero-dep, browser-native: WAAPI / IntersectionObserver / View Transitions). Do **not** add the `motion` library to core — drag/gesture, FLIP layout, and velocity springs are the future optional `@sableui/motion` adapter (peer-dep `motion`).
190
192
 
191
193
  Full token table + rationale: `DESIGN.md → Motion`. Live demo: docs → Foundations → Motion (also the on/off toggle).
package/DESIGN.md CHANGED
@@ -477,7 +477,11 @@ Reference via `z-(--z-overlay)`, never a raw `z-[9999]`. Base UI overlays portal
477
477
 
478
478
  ## Motion
479
479
 
480
- Motion is a first-class token layer, not an afterthought. **Subtle by default, with room for tasteful delight** — quick, deliberate, skippable, and never gaudy. The feel is restrained but alive, built compositor-first (opacity + transform only).
480
+ Motion is a first-class token layer, not an afterthought. **Crafted by default, calm on request** — quick, deliberate, skippable, and never gaudy. Sable components ship the small continuity that makes an interface feel made rather than assembled: one indicator travels to the selected tab instead of two fills blinking, a total rolls to its new value, a check draws itself in, a wrong code shakes and clears. Built compositor-first (opacity + transform, with height and an absolutely positioned glider's size as the sanctioned exceptions).
481
+
482
+ **The principle behind the craft: one thing travels; two things do not blink.** When a selection, a value or a mark changes in place, move the element that represents it. And finish the story — a refused answer shakes *and* says why, a success confirms where it happened, an armed action asks again before it acts.
483
+
484
+ **Two levels.** *Essential* motion answers the user — colour and focus feedback, press, overlay arrival, disclosure height, loaders, chart hover. *Expressive* motion narrates — glides, rolling numbers, drawn ticks, shakes, staggers, chart arrival and morph, scroll reveals, spring overshoot. Expressive durations are multiplied by a second factor, `--motion-expressive`, which `data-motion="subtle"` sets to 0 (and which takes the overshoot out of `ease-spring`). So the policy ladder is `full` (default: both) → `subtle` (essential only) → `off` (nothing moves; every state still changes). Set it app-wide on `<MotionProvider policy>`, for a region with `<MotionScope level>`, or per component with the `motion` prop that every component owning a flourish takes. Scopes and props can only turn motion **down** — `full` on a component means "inherit", so nothing below the provider can override a reader's reduce-motion setting.
481
485
 
482
486
  **Three parts:**
483
487
 
@@ -487,9 +491,10 @@ Motion is a first-class token layer, not an afterthought. **Subtle by default, w
487
491
  - Consume easings via the `ease-*` utilities; durations via `duration-(--motion-duration-*)` (so the kill-switch always applies).
488
492
 
489
493
  2. **Recipes** (`styles.css`) — named, reusable behaviours a component opts into with one class instead of a bespoke transition string:
490
- - `motion-interactive` (hover/focus colour + transform transitions) · `motion-press` (tactile press settle) · `motion-fade` (scrims) · `motion-pop` (anchored/centred surfaces — gentle spring on open) · `motion-slide-{up,down,left,right}` (directional fade-slide) · `motion-disclosure` (height) · `motion-draw` (SVG line-art draw-on — a `pathLength="1"` path inside a `<Reveal>` draws in when scrolled into view; stagger siblings via `--motion-draw-delay`) · loaders `motion-spin`/`motion-pulse`/`motion-shimmer`/`motion-dot-bounce`. Enter/exit recipes drive off Base UI's `data-[starting-style]`/`data-[ending-style]`.
494
+ - `motion-interactive` (hover/focus colour + transform transitions, including `rotate`) · `motion-press` (tactile press settle) · `motion-fade` (scrims) · `motion-pop` (anchored/centred surfaces — gentle spring on open) · `motion-slide-{up,down,left,right}` (directional fade-slide) · `motion-disclosure` (height) · `motion-draw` (SVG line-art draw-on — a `pathLength="1"` path inside a `<Reveal>` draws in when scrolled into view; stagger siblings via `--motion-draw-delay`) · loaders `motion-spin`/`motion-pulse`/`motion-shimmer`/`motion-dot-bounce`. Enter/exit recipes drive off Base UI's `data-[starting-style]`/`data-[ending-style]`.
495
+ - Expressive: `motion-glide` (one indicator travelling to the selection) · `motion-shake` (a refused answer, driven by `useShake()`) · `motion-tick` (a mark that draws itself in) · `motion-appear` (a small control mounting via `@starting-style`) · `motion-arrive` (items a user action adds to a list, staggered) · `motion-size` (easing between intrinsic sizes via `interpolate-size`). `NumberTicker` is the rolling-number primitive.
491
496
 
492
- 3. **Policy** — `prefers-reduced-motion` and `<MotionProvider>` collapse `--motion-scale` to 0 in one place: transitions resolve instantly, loaders hold their first frame. Motion is fully **toggleable on/off** in-app via `useMotion()`.
497
+ 3. **Policy** — `prefers-reduced-motion` and `<MotionProvider>` collapse `--motion-scale` to 0 in one place: transitions resolve instantly, loaders hold their first frame. `subtle` zeroes `--motion-expressive` instead, so essential motion keeps playing. In-app, `useMotion()` reads and sets the policy; `useMotionLevel(motion)` resolves a component's own prop against everything around it, and JS-driven flourishes gate on its `expressive`.
493
498
 
494
499
  Richer physics (interruptible velocity-springs, FLIP layout, drag/gesture) is intentionally **out of core** — it's the future optional `@sableui/motion` adapter (peer-dep `motion`), reached for only when CSS genuinely can't express a surface.
495
500
 
@@ -0,0 +1,93 @@
1
+ // src/blocks/approval-block.tsx
2
+ import { useEffect, useId, useRef, useState } from "react";
3
+ import { Button } from "../components/button/index.js";
4
+ import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "../components/card/index.js";
5
+ import { Checkbox } from "../components/checkbox/index.js";
6
+ import { Code } from "../components/code/index.js";
7
+ import { InlineStatus } from "../components/inline-status/index.js";
8
+ import { Label } from "../components/label/index.js";
9
+ import { cn } from "../lib/cn.js";
10
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
11
+ var request = {
12
+ tool: "send_email",
13
+ title: "Send the invoice to Ada?",
14
+ description: "The agent wants to email invoice INV-1042. An email can't be unsent.",
15
+ details: [
16
+ { label: "To", value: "ada@example.com" },
17
+ { label: "Subject", value: "INV-1042 for October" },
18
+ { label: "Attached", value: "INV-1042.pdf" }
19
+ ],
20
+ allowed: "Allowed. Sending the email.",
21
+ denied: "Denied. The agent won't send it."
22
+ };
23
+ function ApprovalBlock({ className, framed = true, onApprove, onDeny }) {
24
+ const [decision, setDecision] = useState(null);
25
+ const [remember, setRemember] = useState(false);
26
+ const id = useId();
27
+ const outcome = useRef(null);
28
+ useEffect(() => {
29
+ if (decision) outcome.current?.focus();
30
+ }, [decision]);
31
+ return /* @__PURE__ */ jsxs(Card, { variant: framed ? "default" : "flat", className: cn("w-full max-w-md", className), children: [
32
+ /* @__PURE__ */ jsxs(CardHeader, { children: [
33
+ /* @__PURE__ */ jsx(CardTitle, { children: request.title }),
34
+ /* @__PURE__ */ jsx(CardDescription, { children: request.description })
35
+ ] }),
36
+ /* @__PURE__ */ jsxs(CardContent, { className: "flex flex-col gap-4", children: [
37
+ /* @__PURE__ */ jsxs("dl", { className: "grid grid-cols-[auto_minmax(0,1fr)] gap-x-4 gap-y-1.5 text-body-sm", children: [
38
+ request.details.map((d) => /* @__PURE__ */ jsxs("div", { className: "contents", children: [
39
+ /* @__PURE__ */ jsx("dt", { className: "text-muted-foreground", children: d.label }),
40
+ /* @__PURE__ */ jsx("dd", { className: "truncate text-foreground", children: d.value })
41
+ ] }, d.label)),
42
+ /* @__PURE__ */ jsxs("div", { className: "contents", children: [
43
+ /* @__PURE__ */ jsx("dt", { className: "text-muted-foreground", children: "Tool" }),
44
+ /* @__PURE__ */ jsx("dd", { className: "min-w-0", children: /* @__PURE__ */ jsx(Code, { children: request.tool }) })
45
+ ] })
46
+ ] }),
47
+ decision ? null : /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
48
+ /* @__PURE__ */ jsx(Checkbox, { id: `${id}-remember`, checked: remember, onCheckedChange: (v) => setRemember(v) }),
49
+ /* @__PURE__ */ jsxs(Label, { htmlFor: `${id}-remember`, className: "text-body-sm", children: [
50
+ "Allow ",
51
+ /* @__PURE__ */ jsx(Code, { children: request.tool }),
52
+ " for the rest of this chat"
53
+ ] })
54
+ ] })
55
+ ] }),
56
+ /* @__PURE__ */ jsx(CardFooter, { actions: true, className: "justify-end", children: decision ? /* @__PURE__ */ jsx(
57
+ InlineStatus,
58
+ {
59
+ ref: outcome,
60
+ tabIndex: -1,
61
+ role: "status",
62
+ tone: decision === "allowed" ? "success" : "neutral",
63
+ className: "me-auto outline-none",
64
+ children: decision === "allowed" ? request.allowed : request.denied
65
+ }
66
+ ) : /* @__PURE__ */ jsxs(Fragment, { children: [
67
+ /* @__PURE__ */ jsx(
68
+ Button,
69
+ {
70
+ variant: "outline",
71
+ onClick: () => {
72
+ setDecision("denied");
73
+ onDeny?.();
74
+ },
75
+ children: "Deny"
76
+ }
77
+ ),
78
+ /* @__PURE__ */ jsx(
79
+ Button,
80
+ {
81
+ onClick: () => {
82
+ setDecision("allowed");
83
+ onApprove?.({ remember });
84
+ },
85
+ children: "Allow"
86
+ }
87
+ )
88
+ ] }) })
89
+ ] });
90
+ }
91
+ export {
92
+ ApprovalBlock
93
+ };
@@ -0,0 +1,23 @@
1
+ // src/blocks/approval-block.meta.ts
2
+ var approvalBlockMeta = {
3
+ name: "ApprovalBlock",
4
+ family: "AI & agents",
5
+ kind: "card",
6
+ width: { min: "18rem", max: "28rem" },
7
+ summary: `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.`,
8
+ intent: 'Reach for this in a chat or agent run when a tool call waits on the person \u2014 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.',
9
+ notes: [
10
+ `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.`,
11
+ "The description says what can't be undone. That sentence is what the person is deciding on.",
12
+ "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.",
13
+ "After a decision the buttons give way to one InlineStatus line saying what happened, so the thread keeps a record of the answer."
14
+ ],
15
+ a11y: [
16
+ "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.",
17
+ "The details are a dl, so each value is read with its label.",
18
+ "The remember Checkbox has its own Label, so the tool name inside it is part of its accessible name."
19
+ ]
20
+ };
21
+ export {
22
+ approvalBlockMeta
23
+ };
@@ -13,6 +13,21 @@ declare function ApiKeysBlock({ className, framed, onCreate, onKeyAction }: ApiK
13
13
 
14
14
  declare const apiKeysBlockMeta: BlockMeta;
15
15
 
16
+ interface ApprovalBlockProps {
17
+ className?: string;
18
+ /** false drops the card's surface and border — for a chat column that already frames it. */
19
+ framed?: boolean;
20
+ /** The person allowed the call. `remember` is the "for the rest of this chat" choice. */
21
+ onApprove?: (options: {
22
+ remember: boolean;
23
+ }) => void;
24
+ /** The person declined the call. */
25
+ onDeny?: () => void;
26
+ }
27
+ declare function ApprovalBlock({ className, framed, onApprove, onDeny }: ApprovalBlockProps): react.JSX.Element;
28
+
29
+ declare const approvalBlockMeta: BlockMeta;
30
+
16
31
  interface ChatThreadBlockProps {
17
32
  className?: string;
18
33
  /** false drops the card's surface and border. Keep it framed on a muted page: incoming bubbles are the muted surface too. */
@@ -393,4 +408,4 @@ declare function WalletBlock({ className, framed, onSend, onReceive }: WalletBlo
393
408
 
394
409
  declare const walletBlockMeta: BlockMeta;
395
410
 
396
- export { ApiKeysBlock, ChatThreadBlock, CommandPaletteBlock, CtaBandBlock, DangerZoneBlock, DeviceComparisonBlock, EventCardBlock, FaqBlock, FeatureGridBlock, FeedbackFormBlock, GoalProgressBlock, HeroBlock, InviteTeamBlock, InvoiceHistoryBlock, KpiSparklineBlock, MediaPlayerBlock, NewsletterBlock, NotFoundBlock, NotificationListBlock, NotificationSettingsBlock, OrderTrackingBlock, PricingCardBlock, PricingTableBlock, ProductCardBlock, ProfileCardBlock, ProfileFormBlock, RevenueBreakdownBlock, SignInBlock, SignUpBlock, SiteFooterBlock, StatRowBlock, StepsBlock, TaskListBlock, TeamMembersBlock, TestimonialBlock, TrafficSourcesBlock, TrafficTrendBlock, UploadListBlock, VerifyCodeBlock, WalletBlock, apiKeysBlockMeta, chatThreadBlockMeta, commandPaletteBlockMeta, ctaBandBlockMeta, dangerZoneBlockMeta, deviceComparisonBlockMeta, eventCardBlockMeta, faqBlockMeta, featureGridBlockMeta, feedbackFormBlockMeta, goalProgressBlockMeta, heroBlockMeta, inviteTeamBlockMeta, invoiceHistoryBlockMeta, kpiSparklineBlockMeta, mediaPlayerBlockMeta, newsletterBlockMeta, notFoundBlockMeta, notificationListBlockMeta, notificationSettingsBlockMeta, orderTrackingBlockMeta, pricingCardBlockMeta, pricingTableBlockMeta, productCardBlockMeta, profileCardBlockMeta, profileFormBlockMeta, revenueBreakdownBlockMeta, signInBlockMeta, signUpBlockMeta, siteFooterBlockMeta, statRowBlockMeta, stepsBlockMeta, taskListBlockMeta, teamMembersBlockMeta, testimonialBlockMeta, trafficSourcesBlockMeta, trafficTrendBlockMeta, uploadListBlockMeta, verifyCodeBlockMeta, walletBlockMeta };
411
+ export { ApiKeysBlock, ApprovalBlock, ChatThreadBlock, CommandPaletteBlock, CtaBandBlock, DangerZoneBlock, DeviceComparisonBlock, EventCardBlock, FaqBlock, FeatureGridBlock, FeedbackFormBlock, GoalProgressBlock, HeroBlock, InviteTeamBlock, InvoiceHistoryBlock, KpiSparklineBlock, MediaPlayerBlock, NewsletterBlock, NotFoundBlock, NotificationListBlock, NotificationSettingsBlock, OrderTrackingBlock, PricingCardBlock, PricingTableBlock, ProductCardBlock, ProfileCardBlock, ProfileFormBlock, RevenueBreakdownBlock, SignInBlock, SignUpBlock, SiteFooterBlock, StatRowBlock, StepsBlock, TaskListBlock, TeamMembersBlock, TestimonialBlock, TrafficSourcesBlock, TrafficTrendBlock, UploadListBlock, VerifyCodeBlock, WalletBlock, apiKeysBlockMeta, approvalBlockMeta, chatThreadBlockMeta, commandPaletteBlockMeta, ctaBandBlockMeta, dangerZoneBlockMeta, deviceComparisonBlockMeta, eventCardBlockMeta, faqBlockMeta, featureGridBlockMeta, feedbackFormBlockMeta, goalProgressBlockMeta, heroBlockMeta, inviteTeamBlockMeta, invoiceHistoryBlockMeta, kpiSparklineBlockMeta, mediaPlayerBlockMeta, newsletterBlockMeta, notFoundBlockMeta, notificationListBlockMeta, notificationSettingsBlockMeta, orderTrackingBlockMeta, pricingCardBlockMeta, pricingTableBlockMeta, productCardBlockMeta, profileCardBlockMeta, profileFormBlockMeta, revenueBreakdownBlockMeta, signInBlockMeta, signUpBlockMeta, siteFooterBlockMeta, statRowBlockMeta, stepsBlockMeta, taskListBlockMeta, teamMembersBlockMeta, testimonialBlockMeta, trafficSourcesBlockMeta, trafficTrendBlockMeta, uploadListBlockMeta, verifyCodeBlockMeta, walletBlockMeta };
@@ -1,6 +1,8 @@
1
1
  // src/blocks/index.ts
2
2
  import { ApiKeysBlock } from "./api-keys-block.js";
3
3
  import { apiKeysBlockMeta } from "./api-keys-block.meta.js";
4
+ import { ApprovalBlock } from "./approval-block.js";
5
+ import { approvalBlockMeta } from "./approval-block.meta.js";
4
6
  import { ChatThreadBlock } from "./chat-thread-block.js";
5
7
  import { chatThreadBlockMeta } from "./chat-thread-block.meta.js";
6
8
  import { CommandPaletteBlock } from "./command-palette-block.js";
@@ -81,6 +83,7 @@ import { WalletBlock } from "./wallet-block.js";
81
83
  import { walletBlockMeta } from "./wallet-block.meta.js";
82
84
  export {
83
85
  ApiKeysBlock,
86
+ ApprovalBlock,
84
87
  ChatThreadBlock,
85
88
  CommandPaletteBlock,
86
89
  CtaBandBlock,
@@ -121,6 +124,7 @@ export {
121
124
  VerifyCodeBlock,
122
125
  WalletBlock,
123
126
  apiKeysBlockMeta,
127
+ approvalBlockMeta,
124
128
  chatThreadBlockMeta,
125
129
  commandPaletteBlockMeta,
126
130
  ctaBandBlockMeta,
@@ -122,6 +122,12 @@ var accordionMeta = {
122
122
  part: "AccordionPanel",
123
123
  why: "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."
124
124
  },
125
+ {
126
+ slot: "transition",
127
+ recipe: "motion-interactive",
128
+ part: "Chevron",
129
+ why: "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\xB0 in one frame beside an easing panel. A custom `indicator` that rotates should carry motion-interactive itself."
130
+ },
125
131
  {
126
132
  slot: "activity",
127
133
  recipe: "none",
@@ -134,7 +140,7 @@ var accordionMeta = {
134
140
  "Arrow keys move roving focus between triggers; Home/End jump to the first/last; loopFocus wraps by default.",
135
141
  "Each trigger toggles its panel with Enter/Space; the panel is announced as a labelled region.",
136
142
  "A disabled AccordionItem dims and is skipped by keyboard focus while neighbouring items stay interactive.",
137
- "The chevron rotation is driven by data-[panel-open] and the focus ring by focus-visible \u2014 both honor reduced-motion via the consumer's transitions."
143
+ "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."
138
144
  ],
139
145
  examples: [
140
146
  {
@@ -147,7 +153,7 @@ var accordionMeta = {
147
153
  },
148
154
  {
149
155
  label: "Custom indicator (plus-becomes-\xD7)",
150
- code: '<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 \u2014 from billing settings.</AccordionPanel></AccordionItem></Accordion>'
156
+ code: '<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 \u2014 from billing settings.</AccordionPanel></AccordionItem></Accordion>'
151
157
  }
152
158
  ]
153
159
  };
@@ -117,11 +117,18 @@ var areaChartMeta = {
117
117
  default: "false for columns; true for horizontal bars",
118
118
  description: "The vertical axis."
119
119
  },
120
+ {
121
+ name: "motion",
122
+ type: '"full" | "subtle" | "off"',
123
+ default: '"full"',
124
+ description: "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 \u2014 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."
125
+ },
120
126
  {
121
127
  name: "animate",
122
128
  type: "boolean",
123
129
  default: "true",
124
- description: "Play the arrival once the chart scrolls into view. Reduced motion shows the finished chart."
130
+ description: 'Play the arrival once the chart scrolls into view. Reduced motion shows the finished chart. `animate={false}` is `motion="subtle"`.',
131
+ deprecated: 'motion="subtle" (what animate={false} now maps to: no arrival, hover kept) or motion="off"'
125
132
  },
126
133
  {
127
134
  name: "activeIndex",
@@ -176,7 +183,7 @@ var areaChartMeta = {
176
183
  slot: "arrival",
177
184
  recipe: "custom",
178
185
  part: "Area + line",
179
- why: "Once on screen, each line draws on (stroke-dashoffset) and its fill rises from the baseline, series staggered \u2014 the trend is read as it is drawn. On the motion tokens; reduced motion shows the finished chart."
186
+ why: 'Once on screen, each line draws on (stroke-dashoffset) and its fill rises from the baseline, series staggered \u2014 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.'
180
187
  },
181
188
  {
182
189
  slot: "response",
@@ -188,7 +195,7 @@ var areaChartMeta = {
188
195
  slot: "transition",
189
196
  recipe: "custom",
190
197
  part: "Area + line",
191
- why: "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."
198
+ why: "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."
192
199
  },
193
200
  {
194
201
  slot: "activity",
@@ -148,6 +148,32 @@ var attachmentMeta = {
148
148
  "overlay-foreground",
149
149
  "ring"
150
150
  ],
151
+ motion: [
152
+ {
153
+ slot: "arrival",
154
+ recipe: "none",
155
+ part: "Root",
156
+ why: "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 \u2014 as if another file had been added."
157
+ },
158
+ {
159
+ slot: "response",
160
+ recipe: "motion-interactive",
161
+ part: "Root",
162
+ why: "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."
163
+ },
164
+ {
165
+ slot: "transition",
166
+ recipe: "motion-interactive",
167
+ part: "Root",
168
+ why: "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."
169
+ },
170
+ {
171
+ slot: "activity",
172
+ recipe: "motion-pulse",
173
+ part: "Progress",
174
+ why: "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."
175
+ }
176
+ ],
151
177
  a11y: [
152
178
  "The full-card trigger is a real stretched `<button>` with an accessible label, not a click handler on a div \u2014 so it is one tab stop, keyboard-activatable, and focus-ringed.",
153
179
  "Actions sit above that trigger in the stacking order, which keeps them individually focusable and stops a click on `remove` from also opening the file. Text is only made click-through when a trigger is present, so a filename stays selectable otherwise.",
@@ -117,11 +117,18 @@ var barChartMeta = {
117
117
  default: "false for columns; true for horizontal bars",
118
118
  description: "The vertical axis."
119
119
  },
120
+ {
121
+ name: "motion",
122
+ type: '"full" | "subtle" | "off"',
123
+ default: '"full"',
124
+ description: "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 \u2014 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."
125
+ },
120
126
  {
121
127
  name: "animate",
122
128
  type: "boolean",
123
129
  default: "true",
124
- description: "Play the arrival once the chart scrolls into view. Reduced motion shows the finished chart."
130
+ description: 'Play the arrival once the chart scrolls into view. Reduced motion shows the finished chart. `animate={false}` is `motion="subtle"`.',
131
+ deprecated: 'motion="subtle" (what animate={false} now maps to: no arrival, hover kept) or motion="off"'
125
132
  },
126
133
  {
127
134
  name: "activeIndex",
@@ -199,19 +206,19 @@ var barChartMeta = {
199
206
  slot: "arrival",
200
207
  recipe: "custom",
201
208
  part: "Bar",
202
- why: "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."
209
+ why: '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.'
203
210
  },
204
211
  {
205
212
  slot: "response",
206
213
  recipe: "none",
207
214
  part: "Cursor + tooltip",
208
- why: "The hovered category gets a band behind it and the other bars dim at once \u2014 a lagging highlight reads as a slow chart."
215
+ why: "The hovered category gets a band behind it and the other bars dim at once \u2014 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."
209
216
  },
210
217
  {
211
218
  slot: "transition",
212
219
  recipe: "custom",
213
220
  part: "Bar",
214
- why: "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."
221
+ why: "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."
215
222
  },
216
223
  {
217
224
  slot: "activity",
@@ -38,8 +38,10 @@ function BottomNavItem({
38
38
  "data-active": active ? "" : void 0,
39
39
  "aria-current": active ? "page" : void 0,
40
40
  className: cn(
41
+ // motion-press: on a phone there is no hover to say the tap landed, and
42
+ // a route change can take a beat to paint — the settle is the receipt.
41
43
  // design-lint-disable-next-line raw-control-height -- the WCAG 2.5.5 touch-target floor, deliberately density-independent: a compact register still owes a 44px hit area.
42
- "relative flex min-h-[44px] min-w-[44px] flex-col items-center justify-center gap-0.5 rounded-lg text-muted-foreground motion-interactive hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring data-[active]:text-foreground",
44
+ "relative flex min-h-[44px] min-w-[44px] flex-col items-center justify-center gap-0.5 rounded-lg text-muted-foreground motion-interactive motion-press hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring data-[active]:text-foreground",
43
45
  className
44
46
  ),
45
47
  children: /* @__PURE__ */ jsxs(Fragment, { children: [
@@ -57,6 +57,38 @@ var bottomNavMeta = {
57
57
  description: 'A single tab, placed as a child of BottomNav. `label` is the accessible name (aria-label) and optional visible label; `icon` is the glyph node (consumer swaps fill weight for the active state); `active` marks the current route (active ink + aria-current); `badge` overlays a corner dot/count on the icon; `showLabel` shows the label text beneath the icon \u2014 defaults to false (icon-only); turn it on when the icons alone are not self-evident; `render` renders as another element, e.g. render={<Link to="/x" />}.'
58
58
  }
59
59
  ],
60
+ motion: [
61
+ {
62
+ slot: "arrival",
63
+ recipe: "none",
64
+ part: "Root",
65
+ why: "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."
66
+ },
67
+ {
68
+ slot: "response",
69
+ recipe: "motion-interactive",
70
+ part: "BottomNavItem",
71
+ why: "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."
72
+ },
73
+ {
74
+ slot: "response",
75
+ recipe: "motion-press",
76
+ part: "BottomNavItem",
77
+ why: "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."
78
+ },
79
+ {
80
+ slot: "transition",
81
+ recipe: "motion-interactive",
82
+ part: "BottomNavItem",
83
+ why: "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."
84
+ },
85
+ {
86
+ slot: "activity",
87
+ recipe: "none",
88
+ part: "Root",
89
+ why: "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."
90
+ }
91
+ ],
60
92
  a11y: [
61
93
  "BottomNav renders a <nav> landmark.",
62
94
  "Each tab is a 44px minimum touch target with an aria-label.",