@sableui/react 0.13.2 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (203) hide show
  1. package/AGENTS.md +2 -2
  2. package/CHANGELOG.md +172 -0
  3. package/CONTRIBUTING.md +6 -4
  4. package/DESIGN.md +10 -3
  5. package/dist/blocks/approval-block.js +93 -0
  6. package/dist/blocks/approval-block.meta.js +23 -0
  7. package/dist/blocks/index.d.ts +27 -1
  8. package/dist/blocks/index.js +8 -0
  9. package/dist/blocks/message-reactions-block.js +87 -0
  10. package/dist/blocks/message-reactions-block.meta.js +23 -0
  11. package/dist/components/accordion/accordion.meta.js +8 -2
  12. package/dist/components/area-chart/area-chart.meta.js +10 -3
  13. package/dist/components/attachment/attachment.meta.js +26 -0
  14. package/dist/components/bar-chart/bar-chart.meta.js +11 -4
  15. package/dist/components/bottom-nav/bottom-nav.js +3 -1
  16. package/dist/components/bottom-nav/bottom-nav.meta.js +32 -0
  17. package/dist/components/breadcrumbs/breadcrumbs.meta.js +26 -0
  18. package/dist/components/bubble/bubble.js +105 -5
  19. package/dist/components/bubble/bubble.meta.js +42 -6
  20. package/dist/components/bubble/bubble.variants.js +1 -1
  21. package/dist/components/bubble/index.js +2 -0
  22. package/dist/components/button/button.js +21 -4
  23. package/dist/components/button/button.meta.js +7 -7
  24. package/dist/components/calendar/calendar.js +18 -1
  25. package/dist/components/calendar/calendar.meta.js +38 -0
  26. package/dist/components/carousel/carousel.js +6 -4
  27. package/dist/components/carousel/carousel.meta.js +42 -0
  28. package/dist/components/chat-composer/chat-composer.js +21 -21
  29. package/dist/components/chat-composer/chat-composer.meta.js +38 -0
  30. package/dist/components/chat-composer/use-eased-growth.js +70 -0
  31. package/dist/components/checkbox/checkbox-mark.js +8 -1
  32. package/dist/components/checkbox/checkbox.js +3 -1
  33. package/dist/components/checkbox/checkbox.meta.js +32 -0
  34. package/dist/components/checkbox-card/checkbox-card.js +3 -0
  35. package/dist/components/checkbox-card/checkbox-card.meta.js +32 -0
  36. package/dist/components/code-block/code-block.js +14 -3
  37. package/dist/components/code-block/code-block.meta.js +28 -1
  38. package/dist/components/collapsible/collapsible.meta.js +32 -0
  39. package/dist/components/color-field/color-field.meta.js +26 -0
  40. package/dist/components/combobox/combobox.meta.js +44 -0
  41. package/dist/components/command-menu/command-menu.js +8 -3
  42. package/dist/components/command-menu/command-menu.meta.js +38 -0
  43. package/dist/components/confirm-button/confirm-button.js +179 -0
  44. package/dist/components/confirm-button/confirm-button.meta.js +143 -0
  45. package/dist/components/confirm-button/index.js +7 -0
  46. package/dist/components/data-table/data-table.js +6 -2
  47. package/dist/components/data-table/data-table.meta.js +32 -0
  48. package/dist/components/entity/entity.meta.js +26 -0
  49. package/dist/components/feedback/feedback.js +120 -47
  50. package/dist/components/feedback/feedback.meta.js +55 -4
  51. package/dist/components/input-group/input-group.meta.js +27 -1
  52. package/dist/components/like-button/index.js +7 -0
  53. package/dist/components/like-button/like-button.js +89 -0
  54. package/dist/components/like-button/like-button.meta.js +132 -0
  55. package/dist/components/line-chart/line-chart.meta.js +10 -3
  56. package/dist/components/middle-truncate/middle-truncate.meta.js +1 -1
  57. package/dist/components/note/note.meta.js +32 -0
  58. package/dist/components/notification-badge/notification-badge.js +17 -5
  59. package/dist/components/notification-badge/notification-badge.meta.js +34 -2
  60. package/dist/components/number-ticker/index.js +7 -0
  61. package/dist/components/number-ticker/number-ticker.js +246 -0
  62. package/dist/components/number-ticker/number-ticker.meta.js +159 -0
  63. package/dist/components/otp-field/otp-field.js +23 -3
  64. package/dist/components/otp-field/otp-field.meta.js +23 -2
  65. package/dist/components/panel-group/panel-group.meta.js +26 -0
  66. package/dist/components/panel-section/panel-section.js +37 -10
  67. package/dist/components/panel-section/panel-section.meta.js +32 -0
  68. package/dist/components/pie-chart/pie-chart.js +20 -11
  69. package/dist/components/pie-chart/pie-chart.meta.js +17 -4
  70. package/dist/components/pill/pill.meta.js +32 -0
  71. package/dist/components/radar-chart/radar-chart.js +4 -3
  72. package/dist/components/radar-chart/radar-chart.meta.js +10 -3
  73. package/dist/components/radial-chart/radial-chart.js +22 -22
  74. package/dist/components/radial-chart/radial-chart.meta.js +17 -4
  75. package/dist/components/radio/radio.meta.js +26 -0
  76. package/dist/components/radio-card/radio-card.meta.js +38 -0
  77. package/dist/components/reaction-picker/index.js +13 -0
  78. package/dist/components/reaction-picker/reaction-picker.js +104 -0
  79. package/dist/components/reaction-picker/reaction-picker.meta.js +156 -0
  80. package/dist/components/relative-time/relative-time.meta.js +32 -0
  81. package/dist/components/scroller/scroller.meta.js +32 -0
  82. package/dist/components/scrub-field/scrub-field.meta.js +32 -0
  83. package/dist/components/search-input/search-input.js +9 -7
  84. package/dist/components/search-input/search-input.meta.js +26 -0
  85. package/dist/components/show-more/show-more.js +1 -4
  86. package/dist/components/show-more/show-more.meta.js +38 -0
  87. package/dist/components/side-nav/side-nav.js +21 -1
  88. package/dist/components/side-nav/side-nav.meta.js +32 -0
  89. package/dist/components/side-nav/side-nav.variants.js +3 -1
  90. package/dist/components/sidebar/sidebar.js +31 -8
  91. package/dist/components/sidebar/sidebar.meta.js +38 -0
  92. package/dist/components/slider/slider.meta.js +26 -0
  93. package/dist/components/sparkline/sparkline.js +7 -4
  94. package/dist/components/sparkline/sparkline.meta.js +9 -2
  95. package/dist/components/table/table.meta.js +26 -0
  96. package/dist/components/tabs/tabs.js +27 -8
  97. package/dist/components/tabs/tabs.meta.js +46 -3
  98. package/dist/components/theme-selector/theme-selector.meta.js +32 -0
  99. package/dist/components/theme-switcher/theme-switcher.js +4 -1
  100. package/dist/components/theme-switcher/theme-switcher.meta.js +42 -0
  101. package/dist/components/toast/toast.js +25 -13
  102. package/dist/components/toast/toast.meta.js +7 -1
  103. package/dist/components/toggle/toggle.js +28 -5
  104. package/dist/components/toggle/toggle.meta.js +44 -2
  105. package/dist/components/tool-call/index.js +8 -0
  106. package/dist/components/tool-call/tool-call.js +117 -0
  107. package/dist/components/tool-call/tool-call.meta.js +147 -0
  108. package/dist/components/tool-call/tool-call.variants.js +33 -0
  109. package/dist/components/tooltip/index.js +3 -1
  110. package/dist/components/tooltip/tooltip.js +84 -0
  111. package/dist/components/tooltip/tooltip.meta.js +16 -1
  112. package/dist/components/tree/tree.js +71 -6
  113. package/dist/components/tree/tree.meta.js +32 -0
  114. package/dist/components/video/video.meta.js +32 -0
  115. package/dist/file-hashes.json +130 -104
  116. package/dist/foundations/motion.js +69 -3
  117. package/dist/icon-sets/reicon.js +11 -1
  118. package/dist/index.d.ts +907 -104
  119. package/dist/index.js +6 -1
  120. package/dist/lib/chart/cartesian.js +9 -10
  121. package/dist/lib/chart/chart-center.js +30 -0
  122. package/dist/lib/chart/chart-frame.js +15 -8
  123. package/dist/lib/chart/dim.js +13 -0
  124. package/dist/lib/chart/format.js +7 -1
  125. package/dist/lib/chart/motion.js +6 -4
  126. package/dist/lib/highlight-glide.js +68 -10
  127. package/dist/lib/taxonomy.js +5 -0
  128. package/dist/motion/Burst.js +79 -0
  129. package/dist/motion/MotionProvider.js +45 -3
  130. package/dist/motion/Reveal.js +1 -1
  131. package/dist/motion/TextReveal.js +1 -1
  132. package/dist/motion/index.js +17 -2
  133. package/dist/motion/useAutoSize.js +42 -0
  134. package/dist/motion/useShake.js +25 -0
  135. package/dist/styles.css +439 -5
  136. package/llms/blocks/approval-block.md +128 -0
  137. package/llms/blocks/message-reactions-block.md +116 -0
  138. package/llms/components/accordion.md +3 -2
  139. package/llms/components/area-chart.md +4 -3
  140. package/llms/components/attachment.md +7 -0
  141. package/llms/components/bar-chart.md +5 -4
  142. package/llms/components/bottom-nav.md +8 -0
  143. package/llms/components/breadcrumbs.md +7 -0
  144. package/llms/components/bubble.md +17 -7
  145. package/llms/components/button.md +6 -6
  146. package/llms/components/calendar.md +9 -0
  147. package/llms/components/carousel.md +17 -0
  148. package/llms/components/chat-composer.md +9 -0
  149. package/llms/components/checkbox-card.md +8 -0
  150. package/llms/components/checkbox.md +8 -0
  151. package/llms/components/code-block.md +8 -0
  152. package/llms/components/collapsible.md +8 -0
  153. package/llms/components/color-field.md +7 -0
  154. package/llms/components/combobox.md +10 -0
  155. package/llms/components/command-menu.md +9 -0
  156. package/llms/components/confirm-button.md +92 -0
  157. package/llms/components/data-table.md +8 -0
  158. package/llms/components/entity.md +7 -0
  159. package/llms/components/feedback.md +23 -3
  160. package/llms/components/input-group.md +8 -1
  161. package/llms/components/like-button.md +91 -0
  162. package/llms/components/line-chart.md +4 -3
  163. package/llms/components/middle-truncate.md +1 -1
  164. package/llms/components/note.md +8 -0
  165. package/llms/components/notification-badge.md +10 -2
  166. package/llms/components/number-ticker.md +86 -0
  167. package/llms/components/otp-field.md +15 -1
  168. package/llms/components/panel-group.md +7 -0
  169. package/llms/components/panel-section.md +8 -0
  170. package/llms/components/pie-chart.md +6 -4
  171. package/llms/components/pill.md +8 -0
  172. package/llms/components/radar-chart.md +4 -3
  173. package/llms/components/radial-chart.md +6 -4
  174. package/llms/components/radio-card.md +9 -0
  175. package/llms/components/radio.md +7 -0
  176. package/llms/components/reaction-picker.md +115 -0
  177. package/llms/components/relative-time.md +8 -0
  178. package/llms/components/scroller.md +8 -0
  179. package/llms/components/scrub-field.md +8 -0
  180. package/llms/components/search-input.md +7 -0
  181. package/llms/components/show-more.md +9 -0
  182. package/llms/components/side-nav.md +8 -0
  183. package/llms/components/sidebar.md +9 -0
  184. package/llms/components/slider.md +7 -0
  185. package/llms/components/sparkline.md +3 -2
  186. package/llms/components/table.md +7 -0
  187. package/llms/components/tabs.md +20 -2
  188. package/llms/components/theme-selector.md +8 -0
  189. package/llms/components/theme-switcher.md +17 -0
  190. package/llms/components/toast.md +2 -1
  191. package/llms/components/toggle.md +19 -2
  192. package/llms/components/tool-call.md +115 -0
  193. package/llms/components/tooltip.md +11 -1
  194. package/llms/components/tree.md +8 -0
  195. package/llms/components/video.md +8 -0
  196. package/llms/foundations/motion.md +64 -3
  197. package/llms/reference/utilities.md +26 -1
  198. package/llms/tasks/reviewing-motion.md +14 -0
  199. package/llms-full.txt +1337 -69
  200. package/llms.txt +19 -11
  201. package/package.json +1 -1
  202. package/registry.json +3040 -124
  203. package/targets/ios.json +45 -0
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,177 @@
1
1
  # What's new
2
2
 
3
+ ## 0.15.0
4
+
5
+ Motion with some life in it: small things that turn on now land like events,
6
+ told through the theme rather than one-off styles, and chat reactions that
7
+ answer you the way a messaging app does.
8
+
9
+ - **New: `LikeButton`.** A pressed-or-not toggle for a like, a star or a
10
+ save, with its count. Pressing it on fills the glyph, which blooms past
11
+ its size and settles while a ring opens and dots spray out in the theme's
12
+ palette families, and the count rolls up a digit; pressing it off is quiet.
13
+ It shows the viewer's like at once and goes back to `count` exactly
14
+ whenever `count` changes, so pass the server's total. `icon`, `tone` (a
15
+ palette family) and `label` make it a star or a save; `motion="subtle"`
16
+ keeps the fill and drops the flourish.
17
+ - **New recipe: `motion-bloom`.** A small glyph landing in its on state grows
18
+ from nothing to 1.2 and settles. Remount the glyph (a changing `key`) to
19
+ replay it; `--motion-bloom-delay` lets a ring lead. Expressive.
20
+ - **New: `<Burst>`.** The follow-through behind a bloom — a disc that hollows
21
+ into a ring, then dots thrown outward — coloured by palette families, so it
22
+ re-themes. Plays when its `trigger` changes, never on mount, and renders
23
+ nothing below full motion.
24
+ - **Toast: a status glyph instead of an accent bar.** A typed toast now leads
25
+ with the same glyph and ink as Note and InlineStatus (success, info,
26
+ warning, and the destructive glyph for errors), and it blooms in once the
27
+ card has landed — and again in the spinner's place when a loading toast
28
+ settles. The coloured bar down the leading edge told the type by colour
29
+ alone. A stacked deck now springs as cards step back and fan out; leaving
30
+ toasts still take the standard curve.
31
+ - **Feedback:** the chosen rating mark blooms as it is picked.
32
+ - **Icons: `replay` and `motion`.** A single circular arrow for watching
33
+ something again from its start (`refresh` reads as fetching again), and
34
+ a play mark in a dashed ring for a motion or playback setting (`sparkles`
35
+ reads as AI).
36
+ - **New: `ReactionPicker` and `MenuReactions`.** The quick row of emoji you
37
+ react to a message with. `ReactionPicker` is a toolbar for a popover or a
38
+ hover bar; `MenuReactions` is the same row as checkbox menu items, for the
39
+ top of the menu a message opens on right click or long press. The emoji
40
+ bloom in one after another as it opens and magnify under the pointer.
41
+ - **Bubble reactions celebrate.** When a `BubbleReaction` becomes the
42
+ reader's own — pressed, or added already on to a row that has rendered —
43
+ the emoji blooms and copies of it spray out, and the count rolls. Taking
44
+ a reaction back is quiet, and the reactions a page loads with stay still.
45
+ New `label` names a pill ("Party, 3"); new `BubbleReactionAdd` is the
46
+ smiley pill that opens a picker. `BubbleReactions` now hides itself while
47
+ empty, so render it for every message: staying mounted is how the first
48
+ reaction still celebrates. A Bubble shows a focus ring when it is made
49
+ focusable as a menu trigger.
50
+ - **New block: `MessageReactionsBlock`.** A thread you can react to from a
51
+ right-click / long-press menu or the add-reaction pill.
52
+ - **`<Burst>`: `particle` and `playOnMount`.** Throw copies of a glyph
53
+ instead of dots (no ring, travelling upright), and play once on mount for
54
+ a glyph that exists because of the action.
55
+ - **Icon: `emoji`.** A smiley, for an add-reaction control.
56
+ - **Motion guidance:** amplitude scales with smallness (a glyph travels far, a
57
+ surface barely), a moment is staged (main object first, follow-through
58
+ after), and only a person's own choice is celebrated. In `sable docs
59
+ motion` and DESIGN.md.
60
+
61
+ ## 0.14.0
62
+
63
+ Motion that is crafted by default and calm on request, and the first pieces for agent products.
64
+
65
+ - **New: two motion levels, and `subtle`.** Motion is now *essential*
66
+ (colour and focus feedback, press, overlay arrival, disclosure, loaders,
67
+ chart hover) or *expressive* (glides, rolling numbers, drawn ticks,
68
+ shakes, staggers, chart arrival and morph, scroll reveals, spring
69
+ overshoot). Expressive durations also multiply by `--motion-expressive`,
70
+ which the new `subtle` policy sets to 0, and `subtle` takes the overshoot
71
+ out of `ease-spring` and `ease-bounce`. So `full` (the default) plays both,
72
+ `subtle` keeps every answer and drops every flourish, and `off` stills
73
+ everything. Set it app-wide with `<MotionProvider policy="subtle">`, for a
74
+ region with the new `<MotionScope level="subtle">`, or per component with a
75
+ new `motion` prop. Scopes and props can only turn motion **down**: `full`
76
+ on a component means "inherit", so nothing can override a reader's
77
+ reduce-motion setting. `useMotion()` gains `expressive`, and
78
+ `useMotionLevel(motion)` resolves a component's prop against everything
79
+ around it. `Reveal` and `TextReveal` are expressive: under `subtle` the
80
+ content is simply there.
81
+ - **New recipes:** `motion-glide` (one indicator travelling to the selected
82
+ option), `motion-shake` (a refused answer, driven by the new `useShake()`),
83
+ `motion-tick` (a mark that draws itself in), `motion-appear` (a small
84
+ control mounting in place, through `@starting-style`), `motion-arrive`
85
+ (items a user action adds to a list, staggered) and `motion-size` (easing
86
+ between sizes, with the new `useAutoSize()` measuring a label change).
87
+ `sable utility <name>` has each one.
88
+ - **New: `NumberTicker`.** A number that rolls to its new value, digit by
89
+ digit, keyed by place value, so 999 → 1,000 rolls the columns it keeps and
90
+ grows the one it gains. It formats through `Intl.NumberFormat`, reads as
91
+ plain text to assistive tech and copy, and lands without a roll under
92
+ `subtle`. `NotificationBadge` counts and the `PieChart` / `RadialChart`
93
+ centre values now roll through it.
94
+ - **New: `ConfirmButton`.** An inline two-step for low-stakes destructive
95
+ actions. The first press arms it: the label changes, the button eases to
96
+ the new width, a thin line drains while it waits, and the armed state is
97
+ announced. A second press within `timeout` confirms; Escape, a click
98
+ elsewhere, blur or the timeout disarms it, and a double-click cannot
99
+ confirm. `DestructiveActionModal` stays the answer for high stakes.
100
+ - **New: `TooltipGroup`.** Wrap a row of `SimpleTooltip`s and they share one
101
+ tooltip: moving along a toolbar, it glides to the next button and its label
102
+ slides across instead of closing and reopening.
103
+ - **Selections travel.** `Tabs` renders a gliding indicator in both variants
104
+ (the underline and the pill), and a changed panel fades in. Single-select
105
+ `ToggleGroup appearance="segmented"` has one fill that glides and resizes
106
+ to the pressed option. `ThemeSwitcher` uses the same recipe, and
107
+ `Carousel`'s active dot eases its width. `SideNav` and `SidebarNav` draw
108
+ the current page as one fill that glides to the new item when the page
109
+ changes inside a persistent layout. Menu, Select and Combobox highlights
110
+ keep their glide, which is now expressive.
111
+ - **Calendar pages with direction.** Going to the next or previous month
112
+ slides the old grid out toward the edge it leaves by and the new one in
113
+ from the other, over a short travel with a fade (react-day-picker's
114
+ `animate` mode on Sable keyframes, mirrored in RTL). It is on at full
115
+ motion only (a `DatePicker`'s calendar follows the app or scope policy);
116
+ `Calendar` takes `motion`, and an explicit `animate` still wins.
117
+ - **Sidebar collapse eases.** Collapsing to the icon rail eases the width
118
+ instead of jumping 192px in one frame, so the content beside it visibly
119
+ takes the room. Icons stay on one column throughout and labels fade back
120
+ in on reopen. `Sidebar` takes `motion`; `subtle` snaps.
121
+ - **Focus outlines ease in.** `motion-interactive` now also transitions
122
+ `outline-color`, `outline-width` and `outline-offset`, so a keyboard focus
123
+ ring drawn with `outline-*` grows from the element's edge over the fast
124
+ step instead of appearing in one frame. Instant under reduced motion.
125
+ - **States finish their story.** `OtpField` shakes once when it turns
126
+ invalid, and its digits take the destructive colour. `CodeBlock`'s copy
127
+ button swaps to a check in place and announces "Copied". `Feedback` shows a
128
+ drawn check and a thank-you before it closes. A loading `Button` keeps its
129
+ width: the spinner lays over the label instead of replacing it. The
130
+ `ChatComposer` grows with an ease, and its send and stop swap in one
131
+ button. `SearchInput` and `CommandMenu`'s clear buttons appear rather than
132
+ pop. Expanding a `Tree` folder settles its rows in, staggered. The
133
+ `Checkbox` tick draws in from its leading stroke.
134
+ - **Fixed: disclosure chevrons snapped.** Tailwind v4's `rotate-*` writes the
135
+ standalone `rotate` property, which `motion-interactive` did not list, so
136
+ the chevrons on `Accordion`, `Collapsible`, `Tree` and `ShowMore` jumped
137
+ 180° in one frame while the panel beside them eased. The recipe now lists
138
+ `rotate`. `DataTable`'s sort chevron rotates instead of swapping glyphs.
139
+ - **Chart hover dims mix toward the surface.** The bars, slices and rings
140
+ that are not under the pointer now step back by mixing toward the surface
141
+ colour instead of fading, so gridlines no longer strike through a dimmed
142
+ bar and a dimmed ring stays solid over its track.
143
+ - **Deprecated: `animate` on charts.** Every chart takes `motion` instead.
144
+ `animate={false}` maps to `motion="subtle"` (no arrival or morph, hover
145
+ feedback kept); use `motion="off"` to still everything.
146
+ - **Motion declaration is a build gate.** A component that reacts to the
147
+ pointer or carries state must declare its four motion slots (arrival,
148
+ response, transition, activity) in its meta, or decline each with a reason.
149
+ It used to be a report, and 36 components said nothing; all of them now
150
+ answer.
151
+ - **Docs:** a new **Interactions** page shows the catalog by task (inspect,
152
+ select, confirm, recover, succeed, finish loading, type, point, notify).
153
+ Every preview has its own motion level and Replay. Playgrounds gain a Motion
154
+ row and **Copy for agent**, which copies the exact JSX, imports, install
155
+ commands, the active theme and `sable component <Name>`. The Motion page
156
+ sets the policy to Full, Subtle or Off and shows the two levels side by
157
+ side.
158
+
159
+ - **New: `ToolCall`.** One tool an agent called, as a quiet row inside its
160
+ turn: a status glyph, what it did in plain words, the tool's name in mono,
161
+ and one word for where it is (`running`, `awaiting`, `complete`, `error`,
162
+ `denied`). Input and output sit behind a disclosure, as formatted JSON or
163
+ as a rendered result passed as `children`. A failure's message shows under
164
+ the row without opening anything. When a call settles, its spinner swaps
165
+ for the outcome in place (`motion-icon-swap`), and that's the only motion
166
+ in the row. The change is announced once, and a thread restored from
167
+ history announces nothing.
168
+ - **New: `ApprovalBlock`**, in a new "AI & agents" block family. An agent
169
+ asking permission for one call: the question in plain words, what can't be
170
+ undone, the call's details, an "allow for the rest of this chat" Checkbox,
171
+ and Deny / Allow. Neither button is focused on arrival. After a decision,
172
+ one status line replaces the buttons, focus moves to it, and it's
173
+ announced. `onApprove({ remember })` and `onDeny()` report the answer.
174
+
3
175
  ## 0.13.2
4
176
 
5
177
  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,13 @@ 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
+ **Amplitude scales with smallness.** Motion felt lifeless when one amplitude served everything: the 4% a dialog needs is invisible on a 16px icon, so a heart turning on read as a colour swap. Small glyphs that a person turns on now travel far — grow from nothing past their size and settle — while surfaces keep their 0.96 → 1 and presses their 0.97. A moment is staged rather than flashed: the main object lands, then its follow-through (LikeButton's ring opens, the heart blooms 90ms later, dots are thrown after it). And only a person's own choice is celebrated, only at full motion: a burst on a system event or on load is a notification wearing confetti. The look of all of it still comes from the theme — palette families, tone glyphs, the radius scale — never from one-off values.
485
+
486
+ **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
487
 
482
488
  **Three parts:**
483
489
 
@@ -487,9 +493,10 @@ Motion is a first-class token layer, not an afterthought. **Subtle by default, w
487
493
  - Consume easings via the `ease-*` utilities; durations via `duration-(--motion-duration-*)` (so the kill-switch always applies).
488
494
 
489
495
  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]`.
496
+ - `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]`.
497
+ - 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`) · `motion-bloom` (a small glyph landing in its on state, 0 → 1.2 → 1). `NumberTicker` is the rolling-number primitive; `<Burst>` is the follow-through behind a bloom (a ring and thrown dots in the theme's palette families), used by `LikeButton`.
491
498
 
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()`.
499
+ 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
500
 
494
501
  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
502
 
@@ -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. */
@@ -155,6 +170,17 @@ declare function MediaPlayerBlock({ className, framed, onPlayingChange, onPrevio
155
170
 
156
171
  declare const mediaPlayerBlockMeta: BlockMeta;
157
172
 
173
+ interface MessageReactionsBlockProps {
174
+ className?: string;
175
+ /** false drops the card's surface and border. Keep it framed on a muted page: incoming bubbles are the muted surface too. */
176
+ framed?: boolean;
177
+ /** Called when the reader adds or removes a reaction, with the message, the emoji and whether it is now on. */
178
+ onReact?: (messageId: string, emoji: string, on: boolean) => void;
179
+ }
180
+ declare function MessageReactionsBlock({ className, framed, onReact }: MessageReactionsBlockProps): react.JSX.Element;
181
+
182
+ declare const messageReactionsBlockMeta: BlockMeta;
183
+
158
184
  interface NewsletterBlockProps {
159
185
  className?: string;
160
186
  /** false drops the card's surface and border — for a footer or muted section that already frames it. */
@@ -393,4 +419,4 @@ declare function WalletBlock({ className, framed, onSend, onReceive }: WalletBlo
393
419
 
394
420
  declare const walletBlockMeta: BlockMeta;
395
421
 
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 };
422
+ export { ApiKeysBlock, ApprovalBlock, ChatThreadBlock, CommandPaletteBlock, CtaBandBlock, DangerZoneBlock, DeviceComparisonBlock, EventCardBlock, FaqBlock, FeatureGridBlock, FeedbackFormBlock, GoalProgressBlock, HeroBlock, InviteTeamBlock, InvoiceHistoryBlock, KpiSparklineBlock, MediaPlayerBlock, MessageReactionsBlock, 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, messageReactionsBlockMeta, 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";
@@ -31,6 +33,8 @@ import { KpiSparklineBlock } from "./kpi-sparkline-block.js";
31
33
  import { kpiSparklineBlockMeta } from "./kpi-sparkline-block.meta.js";
32
34
  import { MediaPlayerBlock } from "./media-player-block.js";
33
35
  import { mediaPlayerBlockMeta } from "./media-player-block.meta.js";
36
+ import { MessageReactionsBlock } from "./message-reactions-block.js";
37
+ import { messageReactionsBlockMeta } from "./message-reactions-block.meta.js";
34
38
  import { NewsletterBlock } from "./newsletter-block.js";
35
39
  import { newsletterBlockMeta } from "./newsletter-block.meta.js";
36
40
  import { NotFoundBlock } from "./not-found-block.js";
@@ -81,6 +85,7 @@ import { WalletBlock } from "./wallet-block.js";
81
85
  import { walletBlockMeta } from "./wallet-block.meta.js";
82
86
  export {
83
87
  ApiKeysBlock,
88
+ ApprovalBlock,
84
89
  ChatThreadBlock,
85
90
  CommandPaletteBlock,
86
91
  CtaBandBlock,
@@ -96,6 +101,7 @@ export {
96
101
  InvoiceHistoryBlock,
97
102
  KpiSparklineBlock,
98
103
  MediaPlayerBlock,
104
+ MessageReactionsBlock,
99
105
  NewsletterBlock,
100
106
  NotFoundBlock,
101
107
  NotificationListBlock,
@@ -121,6 +127,7 @@ export {
121
127
  VerifyCodeBlock,
122
128
  WalletBlock,
123
129
  apiKeysBlockMeta,
130
+ approvalBlockMeta,
124
131
  chatThreadBlockMeta,
125
132
  commandPaletteBlockMeta,
126
133
  ctaBandBlockMeta,
@@ -136,6 +143,7 @@ export {
136
143
  invoiceHistoryBlockMeta,
137
144
  kpiSparklineBlockMeta,
138
145
  mediaPlayerBlockMeta,
146
+ messageReactionsBlockMeta,
139
147
  newsletterBlockMeta,
140
148
  notFoundBlockMeta,
141
149
  notificationListBlockMeta,
@@ -0,0 +1,87 @@
1
+ "use client";
2
+
3
+ // src/blocks/message-reactions-block.tsx
4
+ import { useState } from "react";
5
+ import { Bubble, BubbleReaction, BubbleReactionAdd, BubbleReactions } from "../components/bubble/index.js";
6
+ import { Card, CardContent, CardFooter } from "../components/card/index.js";
7
+ import {
8
+ ContextMenu,
9
+ ContextMenuContent,
10
+ ContextMenuItem,
11
+ ContextMenuSeparator,
12
+ ContextMenuTrigger
13
+ } from "../components/context-menu/index.js";
14
+ import { Message } from "../components/message/index.js";
15
+ import { DEFAULT_REACTIONS, MenuReactions } from "../components/reaction-picker/index.js";
16
+ import { cn } from "../lib/cn.js";
17
+ import { jsx, jsxs } from "react/jsx-runtime";
18
+ var messages = [
19
+ { id: "m1", text: "The new onboarding flow is live on staging \u{1F680}", mine: false, reactions: { "\u{1F389}": 2, "\u{1F440}": 1 } },
20
+ { id: "m2", text: "Clicked through it twice. The progress bar is so smooth.", mine: true, reactions: {} },
21
+ { id: "m3", text: "Ship it Friday?", mine: false, reactions: { "\u{1F44D}": 1 } }
22
+ ];
23
+ var hint = "Right-click or long-press a message to react.";
24
+ var labels = {
25
+ ...Object.fromEntries(DEFAULT_REACTIONS.map((r) => [r.emoji, r.label])),
26
+ "\u{1F440}": "Eyes"
27
+ };
28
+ function MessageReactionsBlock({ className, framed = true, onReact }) {
29
+ const [mine, setMine] = useState({ m1: ["\u{1F389}"] });
30
+ const toggle = (messageId, emoji) => {
31
+ const on = !(mine[messageId] ?? []).includes(emoji);
32
+ setMine((prev) => {
33
+ const current = prev[messageId] ?? [];
34
+ const next = current.includes(emoji) ? current.filter((e) => e !== emoji) : [...current, emoji];
35
+ return { ...prev, [messageId]: next };
36
+ });
37
+ onReact?.(messageId, emoji, on);
38
+ };
39
+ return /* @__PURE__ */ jsxs(Card, { variant: framed ? "default" : "flat", className: cn("w-full max-w-sm", className), children: [
40
+ /* @__PURE__ */ jsx(CardContent, { className: "flex flex-col gap-3", children: messages.map((m) => {
41
+ const align = m.mine ? "end" : "start";
42
+ const chosen = mine[m.id] ?? [];
43
+ const counts = { ...m.reactions };
44
+ for (const emoji of chosen) counts[emoji] = counts[emoji] ?? 0;
45
+ const pills = Object.entries(counts).map(([emoji, others]) => ({
46
+ emoji,
47
+ active: chosen.includes(emoji),
48
+ count: others + (chosen.includes(emoji) ? 1 : 0)
49
+ }));
50
+ return /* @__PURE__ */ jsxs(Message, { messageId: m.id, align: m.mine ? "end" : void 0, children: [
51
+ /* @__PURE__ */ jsxs(ContextMenu, { children: [
52
+ /* @__PURE__ */ jsx(
53
+ ContextMenuTrigger,
54
+ {
55
+ render: /* @__PURE__ */ jsx(Bubble, { tabIndex: 0, variant: m.mine ? "primary" : void 0, align }),
56
+ children: m.text
57
+ }
58
+ ),
59
+ /* @__PURE__ */ jsxs(ContextMenuContent, { children: [
60
+ /* @__PURE__ */ jsx(MenuReactions, { selected: chosen, onSelect: (emoji) => toggle(m.id, emoji) }),
61
+ /* @__PURE__ */ jsx(ContextMenuSeparator, {}),
62
+ /* @__PURE__ */ jsx(ContextMenuItem, { children: "Reply" }),
63
+ /* @__PURE__ */ jsx(ContextMenuItem, { children: "Copy text" })
64
+ ] })
65
+ ] }),
66
+ /* @__PURE__ */ jsxs(BubbleReactions, { align, children: [
67
+ pills.map((p) => /* @__PURE__ */ jsx(
68
+ BubbleReaction,
69
+ {
70
+ emoji: p.emoji,
71
+ label: labels[p.emoji],
72
+ count: p.count,
73
+ active: p.active,
74
+ onClick: () => toggle(m.id, p.emoji)
75
+ },
76
+ p.emoji
77
+ )),
78
+ pills.length > 0 ? /* @__PURE__ */ jsx(BubbleReactionAdd, { selected: chosen, onSelect: (emoji) => toggle(m.id, emoji) }) : null
79
+ ] })
80
+ ] }, m.id);
81
+ }) }),
82
+ /* @__PURE__ */ jsx(CardFooter, { className: "text-caption text-muted-foreground", children: hint })
83
+ ] });
84
+ }
85
+ export {
86
+ MessageReactionsBlock
87
+ };
@@ -0,0 +1,23 @@
1
+ // src/blocks/message-reactions-block.meta.ts
2
+ var messageReactionsBlockMeta = {
3
+ name: "MessageReactionsBlock",
4
+ family: "Social & messaging",
5
+ kind: "card",
6
+ width: { min: "18rem", max: "24rem" },
7
+ summary: "A short thread you can react to: right-click or long-press a bubble for a menu with a row of quick reactions above Reply and Copy, or use the add-reaction pill under a message. A new reaction blooms in and sprays copies of its emoji.",
8
+ intent: "Reach for this when a conversation excerpt should be reactable \u2014 a team thread, a comment card, a support transcript. A read-only excerpt is ChatThreadBlock; a working chat with a composer and history is the ChatPanel page shell.",
9
+ notes: [
10
+ "Two ways in, on purpose: the right-click / long-press menu is fast once you know it is there, and the add-reaction pill is how a first-time reader finds reactions at all.",
11
+ "The quick row in the menu is MenuReactions (checkbox menu items) and the one in the pill's popover is ReactionPicker (a toolbar), so each is reachable by the keyboard model of the surface it sits in.",
12
+ "Only the reader's own reaction celebrates. Counts from other people roll quietly, and the reactions a page loads with never play.",
13
+ "The add-reaction pill appears only on messages that already have reactions; a message with none is reacted to from its menu, which keeps quiet rows quiet."
14
+ ],
15
+ a11y: [
16
+ "Each bubble is focusable, and its menu opens on right click, long press, Shift+F10 or the Menu key; its reactions are checkbox items reporting whether the reader has used each one.",
17
+ 'Each reaction pill is a toggle named by its meaning and count ("Party, 3") with `aria-pressed`; the emoji and the burst are hidden from assistive tech.',
18
+ "Under reduced motion the reaction appears and the count changes at once; nothing else is lost."
19
+ ]
20
+ };
21
+ export {
22
+ messageReactionsBlockMeta
23
+ };