@sableui/react 0.13.2 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (203) hide show
  1. package/AGENTS.md +2 -2
  2. package/CHANGELOG.md +172 -0
  3. package/CONTRIBUTING.md +6 -4
  4. package/DESIGN.md +10 -3
  5. package/dist/blocks/approval-block.js +93 -0
  6. package/dist/blocks/approval-block.meta.js +23 -0
  7. package/dist/blocks/index.d.ts +27 -1
  8. package/dist/blocks/index.js +8 -0
  9. package/dist/blocks/message-reactions-block.js +87 -0
  10. package/dist/blocks/message-reactions-block.meta.js +23 -0
  11. package/dist/components/accordion/accordion.meta.js +8 -2
  12. package/dist/components/area-chart/area-chart.meta.js +10 -3
  13. package/dist/components/attachment/attachment.meta.js +26 -0
  14. package/dist/components/bar-chart/bar-chart.meta.js +11 -4
  15. package/dist/components/bottom-nav/bottom-nav.js +3 -1
  16. package/dist/components/bottom-nav/bottom-nav.meta.js +32 -0
  17. package/dist/components/breadcrumbs/breadcrumbs.meta.js +26 -0
  18. package/dist/components/bubble/bubble.js +105 -5
  19. package/dist/components/bubble/bubble.meta.js +42 -6
  20. package/dist/components/bubble/bubble.variants.js +1 -1
  21. package/dist/components/bubble/index.js +2 -0
  22. package/dist/components/button/button.js +21 -4
  23. package/dist/components/button/button.meta.js +7 -7
  24. package/dist/components/calendar/calendar.js +18 -1
  25. package/dist/components/calendar/calendar.meta.js +38 -0
  26. package/dist/components/carousel/carousel.js +6 -4
  27. package/dist/components/carousel/carousel.meta.js +42 -0
  28. package/dist/components/chat-composer/chat-composer.js +21 -21
  29. package/dist/components/chat-composer/chat-composer.meta.js +38 -0
  30. package/dist/components/chat-composer/use-eased-growth.js +70 -0
  31. package/dist/components/checkbox/checkbox-mark.js +8 -1
  32. package/dist/components/checkbox/checkbox.js +3 -1
  33. package/dist/components/checkbox/checkbox.meta.js +32 -0
  34. package/dist/components/checkbox-card/checkbox-card.js +3 -0
  35. package/dist/components/checkbox-card/checkbox-card.meta.js +32 -0
  36. package/dist/components/code-block/code-block.js +14 -3
  37. package/dist/components/code-block/code-block.meta.js +28 -1
  38. package/dist/components/collapsible/collapsible.meta.js +32 -0
  39. package/dist/components/color-field/color-field.meta.js +26 -0
  40. package/dist/components/combobox/combobox.meta.js +44 -0
  41. package/dist/components/command-menu/command-menu.js +8 -3
  42. package/dist/components/command-menu/command-menu.meta.js +38 -0
  43. package/dist/components/confirm-button/confirm-button.js +179 -0
  44. package/dist/components/confirm-button/confirm-button.meta.js +143 -0
  45. package/dist/components/confirm-button/index.js +7 -0
  46. package/dist/components/data-table/data-table.js +6 -2
  47. package/dist/components/data-table/data-table.meta.js +32 -0
  48. package/dist/components/entity/entity.meta.js +26 -0
  49. package/dist/components/feedback/feedback.js +120 -47
  50. package/dist/components/feedback/feedback.meta.js +55 -4
  51. package/dist/components/input-group/input-group.meta.js +27 -1
  52. package/dist/components/like-button/index.js +7 -0
  53. package/dist/components/like-button/like-button.js +89 -0
  54. package/dist/components/like-button/like-button.meta.js +132 -0
  55. package/dist/components/line-chart/line-chart.meta.js +10 -3
  56. package/dist/components/middle-truncate/middle-truncate.meta.js +1 -1
  57. package/dist/components/note/note.meta.js +32 -0
  58. package/dist/components/notification-badge/notification-badge.js +17 -5
  59. package/dist/components/notification-badge/notification-badge.meta.js +34 -2
  60. package/dist/components/number-ticker/index.js +7 -0
  61. package/dist/components/number-ticker/number-ticker.js +246 -0
  62. package/dist/components/number-ticker/number-ticker.meta.js +159 -0
  63. package/dist/components/otp-field/otp-field.js +23 -3
  64. package/dist/components/otp-field/otp-field.meta.js +23 -2
  65. package/dist/components/panel-group/panel-group.meta.js +26 -0
  66. package/dist/components/panel-section/panel-section.js +37 -10
  67. package/dist/components/panel-section/panel-section.meta.js +32 -0
  68. package/dist/components/pie-chart/pie-chart.js +20 -11
  69. package/dist/components/pie-chart/pie-chart.meta.js +17 -4
  70. package/dist/components/pill/pill.meta.js +32 -0
  71. package/dist/components/radar-chart/radar-chart.js +4 -3
  72. package/dist/components/radar-chart/radar-chart.meta.js +10 -3
  73. package/dist/components/radial-chart/radial-chart.js +22 -22
  74. package/dist/components/radial-chart/radial-chart.meta.js +17 -4
  75. package/dist/components/radio/radio.meta.js +26 -0
  76. package/dist/components/radio-card/radio-card.meta.js +38 -0
  77. package/dist/components/reaction-picker/index.js +13 -0
  78. package/dist/components/reaction-picker/reaction-picker.js +104 -0
  79. package/dist/components/reaction-picker/reaction-picker.meta.js +156 -0
  80. package/dist/components/relative-time/relative-time.meta.js +32 -0
  81. package/dist/components/scroller/scroller.meta.js +32 -0
  82. package/dist/components/scrub-field/scrub-field.meta.js +32 -0
  83. package/dist/components/search-input/search-input.js +9 -7
  84. package/dist/components/search-input/search-input.meta.js +26 -0
  85. package/dist/components/show-more/show-more.js +1 -4
  86. package/dist/components/show-more/show-more.meta.js +38 -0
  87. package/dist/components/side-nav/side-nav.js +21 -1
  88. package/dist/components/side-nav/side-nav.meta.js +32 -0
  89. package/dist/components/side-nav/side-nav.variants.js +3 -1
  90. package/dist/components/sidebar/sidebar.js +31 -8
  91. package/dist/components/sidebar/sidebar.meta.js +38 -0
  92. package/dist/components/slider/slider.meta.js +26 -0
  93. package/dist/components/sparkline/sparkline.js +7 -4
  94. package/dist/components/sparkline/sparkline.meta.js +9 -2
  95. package/dist/components/table/table.meta.js +26 -0
  96. package/dist/components/tabs/tabs.js +27 -8
  97. package/dist/components/tabs/tabs.meta.js +46 -3
  98. package/dist/components/theme-selector/theme-selector.meta.js +32 -0
  99. package/dist/components/theme-switcher/theme-switcher.js +4 -1
  100. package/dist/components/theme-switcher/theme-switcher.meta.js +42 -0
  101. package/dist/components/toast/toast.js +25 -13
  102. package/dist/components/toast/toast.meta.js +7 -1
  103. package/dist/components/toggle/toggle.js +28 -5
  104. package/dist/components/toggle/toggle.meta.js +44 -2
  105. package/dist/components/tool-call/index.js +8 -0
  106. package/dist/components/tool-call/tool-call.js +117 -0
  107. package/dist/components/tool-call/tool-call.meta.js +147 -0
  108. package/dist/components/tool-call/tool-call.variants.js +33 -0
  109. package/dist/components/tooltip/index.js +3 -1
  110. package/dist/components/tooltip/tooltip.js +84 -0
  111. package/dist/components/tooltip/tooltip.meta.js +16 -1
  112. package/dist/components/tree/tree.js +71 -6
  113. package/dist/components/tree/tree.meta.js +32 -0
  114. package/dist/components/video/video.meta.js +32 -0
  115. package/dist/file-hashes.json +130 -104
  116. package/dist/foundations/motion.js +69 -3
  117. package/dist/icon-sets/reicon.js +11 -1
  118. package/dist/index.d.ts +907 -104
  119. package/dist/index.js +6 -1
  120. package/dist/lib/chart/cartesian.js +9 -10
  121. package/dist/lib/chart/chart-center.js +30 -0
  122. package/dist/lib/chart/chart-frame.js +15 -8
  123. package/dist/lib/chart/dim.js +13 -0
  124. package/dist/lib/chart/format.js +7 -1
  125. package/dist/lib/chart/motion.js +6 -4
  126. package/dist/lib/highlight-glide.js +68 -10
  127. package/dist/lib/taxonomy.js +5 -0
  128. package/dist/motion/Burst.js +79 -0
  129. package/dist/motion/MotionProvider.js +45 -3
  130. package/dist/motion/Reveal.js +1 -1
  131. package/dist/motion/TextReveal.js +1 -1
  132. package/dist/motion/index.js +17 -2
  133. package/dist/motion/useAutoSize.js +42 -0
  134. package/dist/motion/useShake.js +25 -0
  135. package/dist/styles.css +439 -5
  136. package/llms/blocks/approval-block.md +128 -0
  137. package/llms/blocks/message-reactions-block.md +116 -0
  138. package/llms/components/accordion.md +3 -2
  139. package/llms/components/area-chart.md +4 -3
  140. package/llms/components/attachment.md +7 -0
  141. package/llms/components/bar-chart.md +5 -4
  142. package/llms/components/bottom-nav.md +8 -0
  143. package/llms/components/breadcrumbs.md +7 -0
  144. package/llms/components/bubble.md +17 -7
  145. package/llms/components/button.md +6 -6
  146. package/llms/components/calendar.md +9 -0
  147. package/llms/components/carousel.md +17 -0
  148. package/llms/components/chat-composer.md +9 -0
  149. package/llms/components/checkbox-card.md +8 -0
  150. package/llms/components/checkbox.md +8 -0
  151. package/llms/components/code-block.md +8 -0
  152. package/llms/components/collapsible.md +8 -0
  153. package/llms/components/color-field.md +7 -0
  154. package/llms/components/combobox.md +10 -0
  155. package/llms/components/command-menu.md +9 -0
  156. package/llms/components/confirm-button.md +92 -0
  157. package/llms/components/data-table.md +8 -0
  158. package/llms/components/entity.md +7 -0
  159. package/llms/components/feedback.md +23 -3
  160. package/llms/components/input-group.md +8 -1
  161. package/llms/components/like-button.md +91 -0
  162. package/llms/components/line-chart.md +4 -3
  163. package/llms/components/middle-truncate.md +1 -1
  164. package/llms/components/note.md +8 -0
  165. package/llms/components/notification-badge.md +10 -2
  166. package/llms/components/number-ticker.md +86 -0
  167. package/llms/components/otp-field.md +15 -1
  168. package/llms/components/panel-group.md +7 -0
  169. package/llms/components/panel-section.md +8 -0
  170. package/llms/components/pie-chart.md +6 -4
  171. package/llms/components/pill.md +8 -0
  172. package/llms/components/radar-chart.md +4 -3
  173. package/llms/components/radial-chart.md +6 -4
  174. package/llms/components/radio-card.md +9 -0
  175. package/llms/components/radio.md +7 -0
  176. package/llms/components/reaction-picker.md +115 -0
  177. package/llms/components/relative-time.md +8 -0
  178. package/llms/components/scroller.md +8 -0
  179. package/llms/components/scrub-field.md +8 -0
  180. package/llms/components/search-input.md +7 -0
  181. package/llms/components/show-more.md +9 -0
  182. package/llms/components/side-nav.md +8 -0
  183. package/llms/components/sidebar.md +9 -0
  184. package/llms/components/slider.md +7 -0
  185. package/llms/components/sparkline.md +3 -2
  186. package/llms/components/table.md +7 -0
  187. package/llms/components/tabs.md +20 -2
  188. package/llms/components/theme-selector.md +8 -0
  189. package/llms/components/theme-switcher.md +17 -0
  190. package/llms/components/toast.md +2 -1
  191. package/llms/components/toggle.md +19 -2
  192. package/llms/components/tool-call.md +115 -0
  193. package/llms/components/tooltip.md +11 -1
  194. package/llms/components/tree.md +8 -0
  195. package/llms/components/video.md +8 -0
  196. package/llms/foundations/motion.md +64 -3
  197. package/llms/reference/utilities.md +26 -1
  198. package/llms/tasks/reviewing-motion.md +14 -0
  199. package/llms-full.txt +1337 -69
  200. package/llms.txt +19 -11
  201. package/package.json +1 -1
  202. package/registry.json +3040 -124
  203. package/targets/ios.json +45 -0
@@ -0,0 +1,115 @@
1
+ # ToolCall
2
+
3
+ **Status:** stable
4
+
5
+ One tool an agent called, as a quiet row inside its turn: a status glyph, what it did, the tool's name and where it is — with the arguments and result behind a disclosure.
6
+
7
+ **Keywords:** tool call, function call, tool use, agent step, mcp, ai, tool result, agent activity
8
+
9
+ ## Usage
10
+
11
+ Use it for each tool call in an assistant's turn, so the person can see what the agent did and check any step. The row is the summary — glyph, a plain-words title, the tool's name, the status — and stays one line, so a turn with ten calls is still scannable. Input and output sit behind the disclosure, because they are read rarely and, when they are, in full.
12
+
13
+ ### Best practices
14
+
15
+ - Do: Give it a `title` in plain words, in the tense of the status: "Searching the docs" while running, "Searched the docs" once done. The `name` stays beside it in mono for the person who wants to know exactly which tool ran.
16
+ - Do: Drive `status` from the call's real state and keep the same element mounted as it changes. The spinner swaps for the outcome in place, and the change is announced to screen readers once.
17
+ - Do: Pass `error` with `status="error"`. It shows under the row without opening anything, because a failure is the one result everyone needs to read.
18
+ - Do: Pass a rendered result as `children` when the output is something to look at — a list of hits, a diff — rather than data to inspect. It replaces the output's code view, and `input` still shows above it.
19
+ - Don't: Do not put the approve and deny buttons in a ToolCall. A call held for permission is `status="awaiting"` here, and the decision itself is ApprovalBlock, which says what will happen and asks once.
20
+ - Don't: Do not use it for the agent's thinking or a progress note. A wait with no tool behind it is ThinkingOrb or a Spinner with a status line, and a row that claims a tool ran when none did makes every other row less trustworthy. → use **ThinkingOrb**, **Spinner**
21
+ - Don't: Do not open every call by default. A turn whose calls all show their JSON buries the answer; leave `defaultOpen` for the one call the person is likely to check, if any.
22
+
23
+ ### Anatomy
24
+
25
+ | Part | Required | Description |
26
+ | --- | --- | --- |
27
+ | Status glyph | yes | Spinner while running, then the outcome: check, warning, clock (awaiting approval) or ×. The only part that moves. |
28
+ | Title | no | What the call does, in plain words. Without one, the tool's name leads. |
29
+ | Name | yes | The tool's own name in mono, after the title. Hidden below the sm breakpoint when a title is present. |
30
+ | Status | yes | One word for where the call is: Running, Needs approval, Done, Failed, Denied. |
31
+ | Error line | no | The failure message, under the row and always visible. |
32
+ | Details | no | Input and output as code, or a rendered result. A disclosure; the row is its trigger. |
33
+
34
+ ## Props
35
+
36
+ | Name | Type | Default | Description |
37
+ | --- | --- | --- | --- |
38
+ | name | string | — | The tool's own name, as the model called it (`search_docs`). Shown in mono. |
39
+ | title | ReactNode | — | What the call does, in words a person reads. Leads the row; omit it and `name` leads. |
40
+ | status | "running" \| "awaiting" \| "complete" \| "error" \| "denied" | "running" | Where the call is. `awaiting` is held for the person's approval; `denied` is one they declined. |
41
+ | statusLabel | string | — | Overrides the status word, for another language or a more specific phrase. |
42
+ | input | unknown | — | The arguments. A string shows as is; anything else as formatted JSON. |
43
+ | output | unknown | — | The result, shown the same way as `input`. |
44
+ | error | ReactNode | — | What went wrong, for `status="error"`. Always visible under the row. |
45
+ | children | ReactNode | — | A rendered result in place of `output`'s code view. |
46
+ | defaultOpen | boolean | false | Open the details on mount. |
47
+ | open | boolean | — | Controlled open state of the details. |
48
+ | onOpenChange | (open: boolean) => void | — | Called when the details open or close. |
49
+
50
+ ## Variants
51
+
52
+ - **status**: running, awaiting, complete, error, denied
53
+
54
+ ## Tokens used
55
+
56
+ `card`, `border`, `destructive`, `muted`, `muted-foreground`, `foreground`, `tone-warning`, `ring`
57
+
58
+ ## Motion
59
+
60
+ - **arrival** · Root · _no motion_ — It arrives as part of the assistant's turn, and the turn's own arrival covers it. A second entrance per call would make a busy turn twitch.
61
+ - **response** · Header · `motion-interactive` — The header is the disclosure trigger but reads as a status row. The hover step is what tells the person it opens.
62
+ - **transition** · Status glyph · `motion-icon-swap` — The spinner becoming a check or a warning is the moment the call settles, and it is the one change in the row worth catching from the corner of the eye. The swap reads as one mark replaced, not a flicker.
63
+ - **transition** · Details · `motion-disclosure` — The height change connects the code that appears to the row that was pressed, in a thread where several rows look alike.
64
+ - **activity** · Status glyph · `motion-spin` — While the call runs, the spinner is what separates working from stuck. It pauses once the call settles, so a long thread of finished calls costs nothing.
65
+
66
+ ## Accessibility
67
+
68
+ - With details, the header is a button with aria-expanded (Base UI Collapsible); its name is the title, the tool name and the status word, so it reads as one sentence.
69
+ - A status change after mount is announced once through a polite role=status region ("Searched the docs: Done"). The status a call mounts with is not announced, so a thread restored from history stays quiet.
70
+ - The glyph is aria-hidden: the status word beside it carries the meaning, so colour and shape are never the only signal.
71
+
72
+ ## Related
73
+
74
+ - Components: Message, ThinkingOrb, StreamingText, CodeBlock, Collapsible, InlineStatus
75
+
76
+ ## Examples
77
+
78
+ ### A finished call
79
+
80
+ ```tsx
81
+ import { ToolCall } from "@sableui/react"
82
+
83
+ <ToolCall
84
+ name="search_docs"
85
+ title="Searched the docs"
86
+ status="complete"
87
+ input={{ query: "date range picker" }}
88
+ output={{ hits: ["DatePicker", "Calendar", "DateTile"] }}
89
+ />
90
+ ```
91
+
92
+ ### Running, then failed
93
+
94
+ ```tsx
95
+ import { ToolCall } from "@sableui/react"
96
+
97
+ <div className="flex flex-col gap-2">
98
+ <ToolCall name="read_file" title="Reading package.json" status="running" input={{ path: "package.json" }} />
99
+ <ToolCall
100
+ name="run_tests"
101
+ title="Ran the tests"
102
+ status="error"
103
+ error="3 of 412 tests failed in date-picker.test.tsx."
104
+ input={{ filter: "date-picker" }}
105
+ />
106
+ </div>
107
+ ```
108
+
109
+ ### Waiting for approval
110
+
111
+ ```tsx
112
+ import { ToolCall } from "@sableui/react"
113
+
114
+ <ToolCall name="send_email" title="Send the invoice to Ada" status="awaiting" input={{ to: "ada@example.com", subject: "INV-1042" }} />
115
+ ```
@@ -35,7 +35,8 @@ Use Tooltip for brief nonessential hints tied to a specific control or piece of
35
35
  | TooltipTrigger | Base UI Trigger props + render | The anchor element. Use render={<Button/>} to compose with another component. |
36
36
  | TooltipContent | Base UI Popup props + side \| align \| sideOffset + className | Portals + positions + paints the popup. side ("top" \| "bottom" \| "left" \| "right" \| "inline-*") picks which side of the trigger to render against, defaults to "top", and auto-flips on collision; align ("start" \| "center" \| "end") defaults to "center"; sideOffset (gap between trigger and popup, in pixels) defaults to 6. |
37
37
  | TooltipArrow | Base UI Arrow props + className | Optional pointer connecting the popup to its trigger — a popover-coloured square turned 45° that carries the border on its outward edges. Place inside TooltipContent. |
38
- | SimpleTooltip | Base UI Tooltip.Root props + content: ReactNode + children: ReactElement + side \| align \| sideOffset | Shorthand wrapping Root + Trigger + Content for the common case. The single child becomes the trigger (via Base UI render); content sets the body; side / align / sideOffset forward to the content with the same defaults. Composition parts remain for advanced use. |
38
+ | SimpleTooltip | Base UI Tooltip.Root props + content: ReactNode + children: ReactElement + side \| align \| sideOffset | Shorthand wrapping Root + Trigger + Content for the common case. The single child becomes the trigger (via Base UI render); content sets the body; side / align / sideOffset forward to the content with the same defaults. Composition parts remain for advanced use. Inside a TooltipGroup it becomes a trigger of the group's shared tooltip instead. |
39
+ | TooltipGroup | { children: ReactNode; motion?: "full" \| "subtle" \| "off" } | One tooltip shared by every SimpleTooltip inside it — for a toolbar or a row of icon buttons. Moving the pointer along the row glides the tooltip to the next trigger and swaps its label (old fades out, new fades in, each sliding toward the direction of travel), instead of closing one tooltip and opening another a few pixels away. Renders no element. Each SimpleTooltip keeps its own content / side / align / sideOffset and `disabled`; root props such as open / onOpenChange belong to the shared tooltip and are ignored inside a group. `motion` turns it down: `subtle` jumps between triggers and only cross-fades the label. The composable Tooltip parts are unaffected. |
39
40
 
40
41
  ## Props
41
42
 
@@ -53,6 +54,7 @@ Use Tooltip for brief nonessential hints tied to a specific control or piece of
53
54
  - **arrival** · Content · `motion-pop` — The transform-origin follows the anchor, so the tooltip appears to grow out of the element it describes. That is what binds it to its trigger — a bubble that materialises at full size near two possible triggers belongs to neither.
54
55
  - **response** · Content · _no motion_ — The tooltip is not a hover target; pointing at it is how you dismiss it. Styling it for the pointer would suggest otherwise.
55
56
  - **transition** · Content · `motion-pop` — Same recipe, exit direction — it collapses back toward the anchor. Content that fades from a fixed size on close reads as a second, unrelated element disappearing.
57
+ - **transition** · TooltipGroup · _custom (outside the recipe layer)_ — Moving between triggers in a group, the one tooltip glides to the next trigger, resizes to its label and cross-fades the text with a short slide toward the direction of travel. Separate tooltips would close here and open there, two surfaces blinking side by side; the glide says it is the same hint following the pointer. Base UI's handle + Viewport supply the shared popup and the previous/current content; the timing is the expressive glide (slow, emphasized), so `subtle` keeps only the cross-fade.
56
58
  - **activity** · Content · _no motion_ — Tooltip copy is resident before the tooltip opens. Anything that has to be fetched is a Popover with its own loading state.
57
59
 
58
60
  ## Accessibility
@@ -91,3 +93,11 @@ import { Button, Icon, SimpleTooltip } from "@sableui/react"
91
93
 
92
94
  <SimpleTooltip content="Saved to your workspace."><Button variant="ghost" size="icon-sm" aria-label="Info"><Icon name="info" /></Button></SimpleTooltip>
93
95
  ```
96
+
97
+ ### One tooltip gliding along a toolbar (TooltipGroup)
98
+
99
+ ```tsx
100
+ import { Icon, SimpleTooltip, Toolbar, ToolbarButton, TooltipGroup } from "@sableui/react"
101
+
102
+ <TooltipGroup><Toolbar aria-label="Formatting"><SimpleTooltip content="Bold"><ToolbarButton size="icon-sm" aria-label="Bold"><Icon name="bold" /></ToolbarButton></SimpleTooltip><SimpleTooltip content="Italic"><ToolbarButton size="icon-sm" aria-label="Italic"><Icon name="italic" /></ToolbarButton></SimpleTooltip><SimpleTooltip content="Insert link"><ToolbarButton size="icon-sm" aria-label="Insert link"><Icon name="link" /></ToolbarButton></SimpleTooltip></Toolbar></TooltipGroup>
103
+ ```
@@ -61,11 +61,19 @@ Use Tree for the hierarchy panel in a workspace: layers, objects, an inspector o
61
61
  | disclosureIcon | ReactNode | — | Replaces TreeRow's semantic chevron-down icon. Omit to use the chevron-down from the app's IconProvider set. |
62
62
  | actions | ReactNode | — | Right-rail controls for a TreeRow: visibility, solo, a row menu. |
63
63
  | label | ReactNode | — | TreeAddRow's visible label. Name what gets added — 'Add effect', not 'Add'. |
64
+ | motion | "full" \| "subtle" \| "off" | "full" | On Tree. How much motion this component uses: `full` (default) inherits the app's policy, `subtle` keeps feedback and drops the flourish, `off` stills it. It can only turn motion down — it never overrides a reader's reduce-motion setting. Under `subtle` expanded rows appear at once instead of unfolding; the chevron still turns. |
64
65
 
65
66
  ## Tokens used
66
67
 
67
68
  `panel-raised`, `hairline`, `foreground`, `muted-foreground`, `primary`, `ring`
68
69
 
70
+ ## Motion
71
+
72
+ - **arrival** · TreeRow · `motion-arrive` — Rows a user's expand reveals fade down into place in document order (a capped 18ms stagger), so the eye can follow where the children came from. An animation (motion-arrive) rather than a transition, because the rows already wear motion-interactive and a second transition list would fight it. Only on a user expand of a `subtree` — a tree that mounts open shows its rows at once, and rows added later to an open folder do not cascade. Expressive: `subtle` shows them at once.
73
+ - **response** · TreeRow · `motion-interactive` — Hover and selection fills ease, so sweeping the pointer down a dense panel reads as a highlight following it rather than rows flickering.
74
+ - **transition** · Disclosure · `motion-interactive` — The chevron turns with the rows arriving under it; motion-interactive lists `rotate`, which is what -rotate-90 writes, so it no longer flips in one frame.
75
+ - **activity** · Tree · _no motion_ — A tree has no indeterminate state of its own; a branch that loads its children should render a TreeEmpty or a loading row inside it.
76
+
69
77
  ## Accessibility
70
78
 
71
79
  - Tree implements the ARIA tree view pattern: Tree is role="tree", each TreeRow and TreeAddRow is role="treeitem" with aria-level, expandable rows carry aria-expanded, and a TreeSection is a role="group" labelled by its heading. Rows stay flat in the DOM — hierarchy is aria-level — so the fills span the panel's full width.
@@ -30,6 +30,14 @@ Use Video to embed playable or previewable video media inside a page.
30
30
 
31
31
  `border`, `overlay`, `overlay-foreground`, `ring`
32
32
 
33
+ ## Motion
34
+
35
+ - **arrival** · Root · _no motion_ — The frame arrives with its page. Once there is a source, the poster and controls are the browser's.
36
+ - **response** · Play button · `motion-interactive` — With `onRequestPlay`, the play disc firms its scrim on hover, eased, so the mark reads as pressable over any image.
37
+ - **response** · Play button · `motion-press` — Loading a source can take a moment; the settle on press confirms the tap before the player appears.
38
+ - **transition** · Root · _no motion_ — Placeholder to player is a swap of elements: the native video mounts where the poster was. Animating the swap would delay the playback the person just asked for.
39
+ - **activity** · Root · _no motion_ — Buffering belongs to the native controls, which draw their own indicator. A second loader over the frame would compete with the browser's.
40
+
33
41
  ## Accessibility
34
42
 
35
43
  - Mounts a native <video> element, preserving built-in keyboard controls when controls is enabled.
@@ -1,8 +1,8 @@
1
1
  # Motion
2
2
 
3
- Motion explains a change: where something came from, what responded, and that work is happening. It is subtle by default, built from named recipes, and one switch turns it off.
3
+ Motion explains a change: where something came from, what responded, and that work is happening. Components ship crafted by default: one indicator travels instead of two blinking, numbers roll, ticks draw, a refused answer shakes. It is built from named recipes, comes in two levels, and one switch turns it off.
4
4
 
5
- **4 slots · 3 speeds · enter-only overshoot · 1 kill switch**
5
+ **4 slots · 3 speeds · 2 levels · enter-only overshoot · 1 kill switch**
6
6
 
7
7
  ## Three speeds
8
8
 
@@ -26,7 +26,15 @@ Reach for one named class before writing a transition.
26
26
  | `motion-pop` | enter base · spring | Anchored or centred surfaces: popovers, menus, dialogs. |
27
27
  | `motion-slide-{up,down,left,right}` | enter base / exit fast | Directional surfaces: sheets, toasts. |
28
28
  | `motion-disclosure` | base · standard | Height changes: accordion, collapsible. |
29
- | `motion-icon-swap` | enter · emphasized cross-fade | Two icons of one control trading places: play/pause, outline/fill. |
29
+ | `motion-icon-swap` | enter · emphasized cross-fade | Two icons of one control trading places: play/pause, outline/fill, copy/check. |
30
+ | `motion-glide` | slow · emphasized · expressive | ONE indicator travelling to the selected option: tabs, segmented thumb, active dot. |
31
+ | `motion-shake` | ~400ms decaying · expressive | A refused answer: wrong code, invalid submit. Plays while data-shake is set (useShake). Never the only signal. |
32
+ | `motion-tick` | fast fade + base wipe · expressive | A check mark that draws itself in. Exits only fade. |
33
+ | `motion-appear` | fast fade + base scale | A small control mounting in place: a clear button, a send arrow, a badge. @starting-style, no JS. |
34
+ | `motion-arrive` | base · decelerate · staggered · expressive | Items a user action brings into a list: a folder's children on expand, a row just added. Never on every render. |
35
+ | `motion-size` | base · emphasized · expressive | An element easing between intrinsic sizes when its label changes: a two-step confirm, Copy → Copied. |
36
+ | `motion-bloom` | slower · 0 → 1.2 → 1 · expressive | A small glyph landing in its on state: a liked heart, a picked rating, a toast's status icon. Remount it (key) to replay; never on the resting state. |
37
+ | `<Burst>` | slower ring + thrown dots · expressive | The follow-through behind a bloom, in the theme's palette families. Only on a choice a person made: a like, a star, a finished task. |
30
38
  | `motion-draw` | slower · decelerate | SVG line art drawing on when a <Reveal> scrolls in. The path needs pathLength="1"; stagger siblings with --motion-draw-delay. |
31
39
  | `motion-spin · pulse · shimmer · dot-bounce · ping` | loops | Activity. Scale-aware, so reduced motion freezes them. |
32
40
 
@@ -40,6 +48,19 @@ Reach for one named class before writing a transition.
40
48
  | `ease-spring` | ~4% overshoot | A gentle pop on enter. Never on exit. |
41
49
  | `ease-bounce` | ~10% overshoot | A playful pop, opt-in only. Enter only, never on exit. |
42
50
 
51
+ ## Two levels
52
+
53
+ Essential motion answers the user; expressive motion narrates continuity. Expressive durations are also multiplied by --motion-expressive, which `subtle` sets to 0. Each level can only turn motion down from what surrounds it.
54
+
55
+ | Name | Value | Use |
56
+ |---|---|---|
57
+ | `essential` | always, unless off | Colour and focus feedback, press, overlay arrival, disclosure height, loaders, chart hover. |
58
+ | `expressive` | full only | Glides, rolling numbers (NumberTicker), drawn ticks, blooms and bursts, shakes, staggers, chart arrival and morph, scroll reveals, spring overshoot. |
59
+ | `full` | default | Both levels play. The crafted default. |
60
+ | `subtle` | data-motion="subtle" | Essential only; nothing overshoots. For dense tools, embedded widgets, readers who want calm without stillness. |
61
+ | `off` | data-motion="off" · reduce-motion | Nothing moves; every state still changes. |
62
+ | `where` | provider · scope · prop | <MotionProvider policy> for the app, <MotionScope level> for a region, `motion` on a component that owns a flourish. |
63
+
43
64
  ## The four slots
44
65
 
45
66
  Every interactive component answers all four, by filling the slot or declining it with a reason in meta.motion.
@@ -60,6 +81,13 @@ Every interactive component answers all four, by filling the slot or declining i
60
81
  5. **Durations come from tokens, so --motion-scale can stop them.** prefers-reduced-motion and <MotionProvider> off both set --motion-scale to 0. A raw duration ignores that. *Enforced by sable-lint: raw-duration, builtin-animate.*
61
82
  6. **Interactive elements always respond. Something styled for hover with no motion recipe snaps.** A snap between states reads as broken. The four prohibitive rules could not catch missing motion, so this one does. *Enforced by sable-lint: static-interactive.*
62
83
  7. **JS motion reads useMotion().enabled and does nothing when it is false.** Reduced motion is an accessibility setting, not a style preference.
84
+ 8. **One thing travels; two things do not blink. When a selection, a value or a mark changes in place, move the one element that represents it.** A fill switching off here and on there is two events the eye has to connect. A glide, a roll or a draw is one, and it is most of what makes an interface feel made rather than assembled.
85
+ 9. **Flourishes are expressive: multiply their durations by --motion-expressive and gate JS ones on useMotion().expressive.** `subtle` promises the answers without the choreography. A glide on a plain token keeps playing after someone asked for calm.
86
+ 10. **A state change finishes its story: refused shakes and says why, success confirms in place, armed asks again.** Motion that starts a change and leaves the outcome to a snap reads as unfinished. ConfirmButton, OtpField's invalid shake and the copy-to-check swap are the reference cases.
87
+ 11. **Amplitude scales with smallness. A 16px glyph that turns on grows from nothing past its size; a surface travels from 0.96 and a press settles to 0.97.** The same 4% that is right for a dialog is invisible on an icon, which is why a state change on a small control used to read as a colour swap with no life in it. A dialog that overshot 20% would lurch. Size, not importance, sets how far a thing moves.
88
+ 12. **Stage a moment: the main object lands first, the follow-through after it.** A ring that opens, a glyph that blooms 90ms later, dots thrown 60ms after that read as one event with a cause and an effect. Everything starting on the same frame reads as a flash. LikeButton and the toast's status glyph are the reference cases.
89
+ 13. **Celebrate only what a person chose, and only at full motion.** A burst is a reply. On a like it is delight; on a system event, on load, or on every row of a list it is a notification wearing confetti. Under subtle the fill alone answers.
90
+ 14. **Component motion props and MotionScope only turn motion down.** `full` on a component means inherit. Nothing below the provider can switch motion back on for a reader who asked for it off.
63
91
 
64
92
  ## Never
65
93
 
@@ -82,6 +110,39 @@ Every interactive component answers all four, by filling the slot or declining i
82
110
 
83
111
  Same feel, but reduced motion now resolves it instantly.
84
112
 
113
+ ### A selection that travels
114
+
115
+ ```tsx
116
+ // before
117
+ <Tab className="data-[active]:border-b-2 data-[active]:border-foreground" />
118
+ // after
119
+ <Tabs.Indicator className="motion-glide left-(--active-tab-left) w-(--active-tab-width)" />
120
+ ```
121
+
122
+ The underline used to vanish under one tab and appear under the next. Now it slides, and Tabs renders it for you.
123
+
124
+ ### A value that rolls
125
+
126
+ ```tsx
127
+ // before
128
+ <span>{total}</span>
129
+ // after
130
+ <NumberTicker value={total} format={{ style: "currency", currency: "USD" }} />
131
+ ```
132
+
133
+ The digits that changed roll; the ones that did not stay still, so the eye goes straight to the difference.
134
+
135
+ ### A glyph that lands
136
+
137
+ ```tsx
138
+ // before
139
+ <Icon name="heart" variant={liked ? "solid" : "regular"} />
140
+ // after
141
+ <LikeButton count={likes} defaultPressed={liked} />
142
+ ```
143
+
144
+ The heart used to swap fill in one frame. Now it blooms past its size and settles while a ring and dots burst from it, and the count rolls: the same state, told as an event.
145
+
85
146
  ### Faster exits
86
147
 
87
148
  ```tsx
@@ -130,7 +130,7 @@ The `@utility` and `@custom-variant` layer in `styles.css`: what each one is, wh
130
130
  - **Instead of** An animated fade or a hand-written `mask-image` gradient. Use `StreamingText` first; this is for a stream rendered without it.
131
131
  - `<span className="stream-tail">{latestChunk}</span>`
132
132
 
133
- ### motion (24)
133
+ ### motion (31)
134
134
 
135
135
  - **`motion-interactive`** — The transition for interactive chrome: colour, background, border, shadow, opacity, fill, stroke, scale and translate at the fast duration and standard easing. Also removes the mobile tap delay and the platform's grey tap flash. *Use for:* On anything clickable that changes on hover, focus, press or selection: a custom row, a tile, a link, an app-owned button. It is the `response` slot, and the only transition declarer such an element needs.
136
136
  - **Instead of** Tailwind's colour-only or all-property transition utilities with a hand-picked duration. Those ignore --motion-scale, skip the transform properties `motion-press` needs, and leave the 300ms tap delay on touch screens. sable-lint's `static-interactive` fires on a hover style with no recipe.
@@ -162,6 +162,31 @@ The `@utility` and `@custom-variant` layer in `styles.css`: what each one is, wh
162
162
  - **`motion-icon-swap`** — Cross-fades two glyphs stacked in one cell when a control's icon changes with its state: the outgoing one shrinks and blurs out, the incoming one resolves in. The inactive glyph is the one marked `aria-hidden="true"` (or `data-active="false"`). *Use for:* Play/pause, mute/unmute, outline/fill, copy/check, menu/close: one control whose icon changes. Keep both icons mounted; the correct accessibility markup is also the animation hook.
163
163
  - **Instead of** Rendering one icon or the other (it snaps between two frames), or absolutely positioning the second icon, which collapses the wrapper. Under reduced motion the swap is instant and the glyph still changes.
164
164
  - `<span className="motion-icon-swap"><Icon name="play" aria-hidden={playing} /><Icon name="pause" aria-hidden={!playing} /></span>`
165
+ - **`motion-glide`** — Timing for ONE element that travels to the selected option — position and size ease on the emphasized curve at the slow step. Expressive: `data-motion="subtle"` makes it snap. *Use for:* A selection indicator you position yourself: a custom nav's current-page bar, a segmented control's thumb, an active dot. Tabs, ToggleGroup `segmented`, ThemeSwitcher and Carousel already glide.
166
+ - **Instead of** A fill or border on each option that switches on and off (two things blink instead of one thing moving), or a hand-written transition with a literal duration.
167
+ - `<span aria-hidden className="motion-glide absolute bottom-0 h-0.5 bg-foreground" style={{ left: x, width: w }} />`
168
+ - **`motion-shake`** — A short decaying horizontal shake that plays while `data-shake` is set. `useShake()` sets and clears the attribute and restarts it on a repeat refusal. Expressive: under `subtle` only the error colour remains. *Use for:* An answer the system refused: a wrong one-time code, a submit with invalid fields, a rejected password. Always alongside the error colour and message, never instead of them. OtpField's `invalid` already shakes.
169
+ - **Instead of** A keyframe shake with a literal duration, which keeps moving under reduced motion, or a shake with no error text.
170
+ - `const { shake, shakeProps } = useShake()
171
+ <form className="motion-shake" {...shakeProps} onSubmit={(e) => { if (!valid) { e.preventDefault(); shake() } }}>…</form>`
172
+ - **`motion-tick`** — A selection mark that draws itself in from its leading edge (a clip-path wipe, so it works on any icon set) with an essential fade, keyed to Base UI's starting/ending style. Exits only fade. The wipe is expressive. *Use for:* A check or success glyph that mounts with its state: a custom selection card's tick, a completed step. Checkbox and CheckboxCard already use it.
173
+ - **Instead of** A scale-from-zero pop on a check mark, which reads as a stamp rather than a tick, or a path-length draw that needs to own the SVG.
174
+ - `<Base.Indicator className="motion-tick"><Icon name="check" /></Base.Indicator>`
175
+ - **`motion-appear`** — Fades and scales a small control in on first render through `@starting-style` — no data attribute, no JS. The fade is essential, the scale is expressive. Removal is instant. *Use for:* A control that mounts in place as state changes: a search field's clear button, a send button replacing a mic, a count badge that appears. Put it on a wrapper around a Sable Button or anything carrying `motion-interactive`, so the two do not fight over `transition-property`.
176
+ - **Instead of** Conditionally rendering a control so it pops in on one frame, or wrapping a single icon button in Presence.
177
+ - `{value && <span className="flex motion-appear"><Button size="icon-xs" variant="ghost" aria-label="Clear"><Icon name="xmark" /></Button></span>}`
178
+ - **`motion-arrive`** — Items a user action brings into a list settle into place one after another: a short fade down from 0.25rem. Stagger with `--motion-arrive-delay` (raw ms; the recipe scales it). Expressive. *Use for:* Only on items that are NEW because of something the user did: a folder's children on expand, a row just added, results that load in. Tree's expand already uses it.
179
+ - **Instead of** A staggered entrance on every render of a list, which turns a list into a loading delay, or an inline keyframe animation with literal durations.
180
+ - `<li className="motion-arrive" style={{ "--motion-arrive-delay": "36ms" } as CSSProperties}>{row.name}</li>`
181
+ - **`motion-bloom`** — A small glyph landing in its "on" state grows from nothing past its size (1.2) and settles. The amplitude a 16px glyph needs to read as an event; surfaces never bloom. A keyframe that plays on mount, so remount the glyph (a changing `key`) to replay it. `--motion-bloom-delay` holds it at nothing first (raw ms; the recipe scales it). Expressive. *Use for:* A glyph that a person's choice turns on: a liked heart, a starred item, a status icon arriving in a toast. LikeButton and Toast already use it. Pair with <Burst> for the follow-through on a celebratory toggle.
182
+ - **Instead of** A bigger motion-press or motion-pop on the whole control (a button or card that overshoots 20% lurches), or a hand-written scale keyframe with literal durations. Never on the resting state: a page that loads with a post already liked must not bloom every heart.
183
+ - `<span key={likes} className={liked ? "motion-bloom" : undefined}><Icon name="heart" variant={liked ? "solid" : "regular"} /></span>`
184
+ - **`motion-size`** — Eases an element between sizes: between a keyword and a length (0 ↔ auto) through `interpolate-size: allow-keywords`, and between measured pixel sizes when paired with `useAutoSize()`, so a button whose label changes stretches to the new width instead of jumping. A superset of motion-interactive's transition list, so it can sit on a Button. The size is expressive. *Use for:* A control whose content changes length in place: a two-step confirm button, a Copy → Copied label, a chip that gains a count. ConfirmButton already uses it.
185
+ - **Instead of** A hand-written width transition, or a fixed width that clips the longer label. A content-sized box computes `width: auto` before and after its label changes, so on its own the recipe has nothing to ease — that is what `useAutoSize()` is for.
186
+ - `const size = useAutoSize<HTMLButtonElement, HTMLSpanElement>()
187
+ <button type="button" ref={size.outerRef} className="motion-size">
188
+ <span ref={size.innerRef} className="inline-flex w-max">{copied ? "Copied" : "Copy link"}</span>
189
+ </button>`
165
190
  - **`motion-spin`** — A continuous rotation for a spinner glyph, scaled by --motion-scale so reduced motion freezes it. *Use for:* A loading glyph that is not the Spinner component (use Spinner first), such as an icon inside a busy button.
166
191
  - **Instead of** Tailwind's built-in spin animation, which ignores --motion-scale. sable-lint's `builtin-animate` flags it.
167
192
  - `<Icon name="spinner" className="motion-spin" />`
@@ -69,6 +69,20 @@ and canceled work does not report success. Inspect the behavior at normal speed:
69
69
  is the response immediate enough for its frequency, and does the movement explain
70
70
  the change without delaying access to content?
71
71
 
72
+ Check continuity before timing. When a selection, value or mark changes in
73
+ place, one element should travel (an indicator glides, digits roll, a tick
74
+ draws) rather than one thing switching off and another on. A snap between two
75
+ states of the same control — a chevron that jumps, a fill that blinks from one
76
+ tab to the next, an icon replaced between frames — is a finding even when every
77
+ duration is a token. So is a story left unfinished: a refusal with no shake or
78
+ message, a success with no confirmation where it happened.
79
+
80
+ Run it three ways, not two. Under `data-motion="subtle"` (or the component's
81
+ `motion="subtle"`, or `<MotionScope level="subtle">`) feedback, overlays,
82
+ disclosure and loaders should still play while glides, rolls, draws, shakes,
83
+ staggers and overshoot resolve instantly; a flourish still moving there was
84
+ built on a plain token instead of an expressive one.
85
+
72
86
  Repeat relevant behavior with reduced motion enabled. This should preserve state,
73
87
  focus, status, and controls; hiding the animated content is not an equivalent
74
88
  result. Test a custom driver directly—Sable's policy cannot disable an unrelated