@sableui/react 0.13.1 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (231) hide show
  1. package/AGENTS.md +2 -2
  2. package/CHANGELOG.md +132 -0
  3. package/CONTRIBUTING.md +7 -5
  4. package/DESIGN.md +8 -3
  5. package/bin/sable.mjs +17 -1
  6. package/dist/blocks/approval-block.js +93 -0
  7. package/dist/blocks/approval-block.meta.js +23 -0
  8. package/dist/blocks/index.d.ts +17 -2
  9. package/dist/blocks/index.js +4 -0
  10. package/dist/components/accordion/accordion.meta.js +10 -3
  11. package/dist/components/area-chart/area-chart.meta.js +10 -3
  12. package/dist/components/attachment/attachment.meta.js +26 -0
  13. package/dist/components/badge/badge.meta.js +2 -1
  14. package/dist/components/banner/banner.meta.js +4 -2
  15. package/dist/components/bar-chart/bar-chart.meta.js +11 -4
  16. package/dist/components/bottom-nav/bottom-nav.js +3 -1
  17. package/dist/components/bottom-nav/bottom-nav.meta.js +32 -0
  18. package/dist/components/breadcrumbs/breadcrumbs.meta.js +26 -0
  19. package/dist/components/button/button.js +21 -4
  20. package/dist/components/button/button.meta.js +7 -7
  21. package/dist/components/button-group/button-group.meta.js +2 -1
  22. package/dist/components/calendar/calendar.js +18 -1
  23. package/dist/components/calendar/calendar.meta.js +38 -0
  24. package/dist/components/carousel/carousel.js +6 -4
  25. package/dist/components/carousel/carousel.meta.js +42 -0
  26. package/dist/components/chat-composer/chat-composer.js +21 -21
  27. package/dist/components/chat-composer/chat-composer.meta.js +38 -0
  28. package/dist/components/chat-composer/use-eased-growth.js +70 -0
  29. package/dist/components/checkbox/checkbox-mark.js +8 -1
  30. package/dist/components/checkbox/checkbox.js +3 -1
  31. package/dist/components/checkbox/checkbox.meta.js +34 -1
  32. package/dist/components/checkbox-card/checkbox-card.js +3 -0
  33. package/dist/components/checkbox-card/checkbox-card.meta.js +32 -0
  34. package/dist/components/code-block/code-block.js +14 -3
  35. package/dist/components/code-block/code-block.meta.js +28 -1
  36. package/dist/components/collapsible/collapsible.meta.js +34 -1
  37. package/dist/components/color-field/color-field.meta.js +26 -0
  38. package/dist/components/combobox/combobox.meta.js +48 -2
  39. package/dist/components/command-menu/command-menu.js +8 -3
  40. package/dist/components/command-menu/command-menu.meta.js +44 -3
  41. package/dist/components/confirm-button/confirm-button.js +179 -0
  42. package/dist/components/confirm-button/confirm-button.meta.js +143 -0
  43. package/dist/components/confirm-button/index.js +7 -0
  44. package/dist/components/context-card/context-card.meta.js +2 -1
  45. package/dist/components/data-table/data-table.js +6 -2
  46. package/dist/components/data-table/data-table.meta.js +32 -0
  47. package/dist/components/dialog/dialog.meta.js +6 -3
  48. package/dist/components/disclosure-picker/disclosure-picker.meta.js +2 -1
  49. package/dist/components/entity/entity.meta.js +26 -0
  50. package/dist/components/feedback/feedback.js +113 -47
  51. package/dist/components/feedback/feedback.meta.js +49 -4
  52. package/dist/components/inline-status/inline-status.meta.js +2 -1
  53. package/dist/components/input-group/input-group.meta.js +27 -1
  54. package/dist/components/line-chart/line-chart.meta.js +10 -3
  55. package/dist/components/menu/menu.meta.js +2 -1
  56. package/dist/components/menubar/menubar.meta.js +2 -1
  57. package/dist/components/meter/meter.meta.js +2 -1
  58. package/dist/components/middle-truncate/middle-truncate.meta.js +1 -1
  59. package/dist/components/note/note.meta.js +34 -1
  60. package/dist/components/notification-badge/notification-badge.js +17 -5
  61. package/dist/components/notification-badge/notification-badge.meta.js +34 -2
  62. package/dist/components/number-ticker/index.js +7 -0
  63. package/dist/components/number-ticker/number-ticker.js +246 -0
  64. package/dist/components/number-ticker/number-ticker.meta.js +159 -0
  65. package/dist/components/otp-field/otp-field.js +23 -3
  66. package/dist/components/otp-field/otp-field.meta.js +23 -2
  67. package/dist/components/page-loader/page-loader.meta.js +2 -1
  68. package/dist/components/panel-group/panel-group.meta.js +26 -0
  69. package/dist/components/panel-section/panel-section.js +37 -10
  70. package/dist/components/panel-section/panel-section.meta.js +32 -0
  71. package/dist/components/pie-chart/pie-chart.js +20 -11
  72. package/dist/components/pie-chart/pie-chart.meta.js +17 -4
  73. package/dist/components/pill/pill.meta.js +34 -1
  74. package/dist/components/popover/popover.meta.js +4 -2
  75. package/dist/components/progress/progress.meta.js +2 -1
  76. package/dist/components/radar-chart/radar-chart.js +4 -3
  77. package/dist/components/radar-chart/radar-chart.meta.js +10 -3
  78. package/dist/components/radial-chart/radial-chart.js +22 -22
  79. package/dist/components/radial-chart/radial-chart.meta.js +17 -4
  80. package/dist/components/radio/radio.meta.js +28 -1
  81. package/dist/components/radio-card/radio-card.meta.js +38 -0
  82. package/dist/components/relative-time/relative-time.meta.js +32 -0
  83. package/dist/components/scroller/scroller.meta.js +32 -0
  84. package/dist/components/scrub-field/scrub-field.meta.js +32 -0
  85. package/dist/components/search-input/search-input.js +9 -7
  86. package/dist/components/search-input/search-input.meta.js +26 -0
  87. package/dist/components/segmented-bar/segmented-bar.meta.js +2 -1
  88. package/dist/components/select/select.meta.js +4 -2
  89. package/dist/components/sheet/sheet.meta.js +4 -2
  90. package/dist/components/show-more/show-more.js +1 -4
  91. package/dist/components/show-more/show-more.meta.js +38 -0
  92. package/dist/components/side-nav/side-nav.js +21 -1
  93. package/dist/components/side-nav/side-nav.meta.js +34 -1
  94. package/dist/components/side-nav/side-nav.variants.js +3 -1
  95. package/dist/components/sidebar/sidebar.js +31 -8
  96. package/dist/components/sidebar/sidebar.meta.js +38 -0
  97. package/dist/components/skeleton/skeleton.meta.js +2 -1
  98. package/dist/components/slider/slider.meta.js +26 -0
  99. package/dist/components/sparkline/sparkline.js +7 -4
  100. package/dist/components/sparkline/sparkline.meta.js +9 -2
  101. package/dist/components/spinner/spinner.meta.js +2 -1
  102. package/dist/components/stepper/stepper.meta.js +4 -2
  103. package/dist/components/switch/switch.meta.js +2 -1
  104. package/dist/components/table/table.meta.js +28 -1
  105. package/dist/components/tabs/tabs.js +27 -8
  106. package/dist/components/tabs/tabs.meta.js +50 -5
  107. package/dist/components/theme-selector/theme-selector.meta.js +32 -0
  108. package/dist/components/theme-switcher/theme-switcher.js +4 -1
  109. package/dist/components/theme-switcher/theme-switcher.meta.js +42 -0
  110. package/dist/components/thinking-orb/thinking-orb.meta.js +4 -2
  111. package/dist/components/toast/toast.meta.js +2 -1
  112. package/dist/components/toggle/toggle.js +28 -5
  113. package/dist/components/toggle/toggle.meta.js +46 -3
  114. package/dist/components/tool-call/index.js +8 -0
  115. package/dist/components/tool-call/tool-call.js +117 -0
  116. package/dist/components/tool-call/tool-call.meta.js +147 -0
  117. package/dist/components/tool-call/tool-call.variants.js +33 -0
  118. package/dist/components/toolbar/toolbar.meta.js +2 -1
  119. package/dist/components/tooltip/index.js +3 -1
  120. package/dist/components/tooltip/tooltip.js +84 -0
  121. package/dist/components/tooltip/tooltip.meta.js +18 -2
  122. package/dist/components/tree/tree.js +71 -6
  123. package/dist/components/tree/tree.meta.js +32 -0
  124. package/dist/components/video/video.meta.js +32 -0
  125. package/dist/file-hashes.json +139 -122
  126. package/dist/foundations/motion.js +49 -3
  127. package/dist/index.d.ts +616 -46
  128. package/dist/index.js +4 -1
  129. package/dist/lib/chart/cartesian.js +9 -10
  130. package/dist/lib/chart/chart-center.js +30 -0
  131. package/dist/lib/chart/chart-frame.js +15 -8
  132. package/dist/lib/chart/dim.js +13 -0
  133. package/dist/lib/chart/format.js +7 -1
  134. package/dist/lib/chart/motion.js +6 -4
  135. package/dist/lib/highlight-glide.js +68 -10
  136. package/dist/lib/taxonomy.js +3 -0
  137. package/dist/{meta-DVG8ceze.d.ts → meta-DX5r5wQL.d.ts} +8 -0
  138. package/dist/motion/MotionProvider.js +45 -3
  139. package/dist/motion/Reveal.js +1 -1
  140. package/dist/motion/TextReveal.js +1 -1
  141. package/dist/motion/index.js +15 -2
  142. package/dist/motion/useAutoSize.js +42 -0
  143. package/dist/motion/useShake.js +25 -0
  144. package/dist/patterns/index.d.ts +1 -1
  145. package/dist/styles.css +298 -5
  146. package/llms/blocks/approval-block.md +128 -0
  147. package/llms/components/accordion.md +4 -3
  148. package/llms/components/area-chart.md +4 -3
  149. package/llms/components/attachment.md +7 -0
  150. package/llms/components/badge.md +1 -1
  151. package/llms/components/banner.md +2 -2
  152. package/llms/components/bar-chart.md +5 -4
  153. package/llms/components/bottom-nav.md +8 -0
  154. package/llms/components/breadcrumbs.md +7 -0
  155. package/llms/components/button-group.md +1 -1
  156. package/llms/components/button.md +6 -6
  157. package/llms/components/calendar.md +9 -0
  158. package/llms/components/carousel.md +17 -0
  159. package/llms/components/chat-composer.md +9 -0
  160. package/llms/components/checkbox-card.md +8 -0
  161. package/llms/components/checkbox.md +9 -1
  162. package/llms/components/code-block.md +8 -0
  163. package/llms/components/collapsible.md +9 -1
  164. package/llms/components/color-field.md +7 -0
  165. package/llms/components/combobox.md +12 -2
  166. package/llms/components/command-menu.md +12 -3
  167. package/llms/components/confirm-button.md +92 -0
  168. package/llms/components/context-card.md +1 -1
  169. package/llms/components/data-table.md +8 -0
  170. package/llms/components/dialog.md +3 -3
  171. package/llms/components/disclosure-picker.md +1 -1
  172. package/llms/components/entity.md +7 -0
  173. package/llms/components/feedback.md +22 -3
  174. package/llms/components/inline-status.md +1 -1
  175. package/llms/components/input-group.md +8 -1
  176. package/llms/components/line-chart.md +4 -3
  177. package/llms/components/menu.md +1 -1
  178. package/llms/components/menubar.md +1 -1
  179. package/llms/components/meter.md +1 -1
  180. package/llms/components/middle-truncate.md +1 -1
  181. package/llms/components/note.md +9 -1
  182. package/llms/components/notification-badge.md +10 -2
  183. package/llms/components/number-ticker.md +86 -0
  184. package/llms/components/otp-field.md +15 -1
  185. package/llms/components/page-loader.md +1 -1
  186. package/llms/components/panel-group.md +7 -0
  187. package/llms/components/panel-section.md +8 -0
  188. package/llms/components/pie-chart.md +6 -4
  189. package/llms/components/pill.md +9 -1
  190. package/llms/components/popover.md +2 -2
  191. package/llms/components/progress.md +1 -1
  192. package/llms/components/radar-chart.md +4 -3
  193. package/llms/components/radial-chart.md +6 -4
  194. package/llms/components/radio-card.md +9 -0
  195. package/llms/components/radio.md +8 -1
  196. package/llms/components/relative-time.md +8 -0
  197. package/llms/components/scroller.md +8 -0
  198. package/llms/components/scrub-field.md +8 -0
  199. package/llms/components/search-input.md +7 -0
  200. package/llms/components/segmented-bar.md +1 -1
  201. package/llms/components/select.md +2 -2
  202. package/llms/components/sheet.md +2 -2
  203. package/llms/components/show-more.md +9 -0
  204. package/llms/components/side-nav.md +9 -1
  205. package/llms/components/sidebar.md +9 -0
  206. package/llms/components/skeleton.md +1 -1
  207. package/llms/components/slider.md +7 -0
  208. package/llms/components/sparkline.md +3 -2
  209. package/llms/components/spinner.md +1 -1
  210. package/llms/components/stepper.md +2 -2
  211. package/llms/components/switch.md +1 -1
  212. package/llms/components/table.md +8 -1
  213. package/llms/components/tabs.md +22 -4
  214. package/llms/components/theme-selector.md +8 -0
  215. package/llms/components/theme-switcher.md +17 -0
  216. package/llms/components/thinking-orb.md +2 -2
  217. package/llms/components/toast.md +1 -1
  218. package/llms/components/toggle.md +20 -3
  219. package/llms/components/tool-call.md +115 -0
  220. package/llms/components/toolbar.md +1 -1
  221. package/llms/components/tooltip.md +12 -2
  222. package/llms/components/tree.md +8 -0
  223. package/llms/components/video.md +8 -0
  224. package/llms/foundations/motion.md +48 -3
  225. package/llms/reference/utilities.md +23 -1
  226. package/llms/tasks/reviewing-motion.md +14 -0
  227. package/llms-full.txt +1004 -98
  228. package/llms.txt +16 -11
  229. package/package.json +1 -1
  230. package/registry.json +2708 -187
  231. package/targets/ios.json +27 -0
@@ -0,0 +1,147 @@
1
+ // src/components/tool-call/tool-call.meta.ts
2
+ var toolCallMeta = {
3
+ name: "ToolCall",
4
+ summary: "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 \u2014 with the arguments and result behind a disclosure.",
5
+ status: "stable",
6
+ keywords: [
7
+ "tool call",
8
+ "function call",
9
+ "tool use",
10
+ "agent step",
11
+ "mcp",
12
+ "ai",
13
+ "tool result",
14
+ "agent activity"
15
+ ],
16
+ usage: {
17
+ description: "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 \u2014 glyph, a plain-words title, the tool's name, the status \u2014 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.",
18
+ bestPractices: [
19
+ {
20
+ guidance: "do",
21
+ description: '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.'
22
+ },
23
+ {
24
+ guidance: "do",
25
+ description: "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."
26
+ },
27
+ {
28
+ guidance: "do",
29
+ description: 'Pass `error` with `status="error"`. It shows under the row without opening anything, because a failure is the one result everyone needs to read.'
30
+ },
31
+ {
32
+ guidance: "do",
33
+ description: "Pass a rendered result as `children` when the output is something to look at \u2014 a list of hits, a diff \u2014 rather than data to inspect. It replaces the output's code view, and `input` still shows above it."
34
+ },
35
+ {
36
+ guidance: "dont",
37
+ description: '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.'
38
+ },
39
+ {
40
+ guidance: "dont",
41
+ description: "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.",
42
+ instead: ["ThinkingOrb", "Spinner"]
43
+ },
44
+ {
45
+ guidance: "dont",
46
+ description: "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."
47
+ }
48
+ ],
49
+ anatomy: [
50
+ { name: "Status glyph", required: true, description: "Spinner while running, then the outcome: check, warning, clock (awaiting approval) or \xD7. The only part that moves." },
51
+ { name: "Title", required: false, description: "What the call does, in plain words. Without one, the tool's name leads." },
52
+ { name: "Name", required: true, description: "The tool's own name in mono, after the title. Hidden below the sm breakpoint when a title is present." },
53
+ { name: "Status", required: true, description: "One word for where the call is: Running, Needs approval, Done, Failed, Denied." },
54
+ { name: "Error line", required: false, description: "The failure message, under the row and always visible." },
55
+ { name: "Details", required: false, description: "Input and output as code, or a rendered result. A disclosure; the row is its trigger." }
56
+ ]
57
+ },
58
+ related: {
59
+ components: ["Message", "ThinkingOrb", "StreamingText", "CodeBlock", "Collapsible", "InlineStatus"]
60
+ },
61
+ props: [
62
+ { name: "name", type: "string", description: "The tool's own name, as the model called it (`search_docs`). Shown in mono." },
63
+ { name: "title", type: "ReactNode", description: "What the call does, in words a person reads. Leads the row; omit it and `name` leads." },
64
+ { name: "status", type: '"running" | "awaiting" | "complete" | "error" | "denied"', default: '"running"', description: "Where the call is. `awaiting` is held for the person's approval; `denied` is one they declined." },
65
+ { name: "statusLabel", type: "string", description: "Overrides the status word, for another language or a more specific phrase." },
66
+ { name: "input", type: "unknown", description: "The arguments. A string shows as is; anything else as formatted JSON." },
67
+ { name: "output", type: "unknown", description: "The result, shown the same way as `input`." },
68
+ { name: "error", type: "ReactNode", description: 'What went wrong, for `status="error"`. Always visible under the row.' },
69
+ { name: "children", type: "ReactNode", description: "A rendered result in place of `output`'s code view." },
70
+ { name: "defaultOpen", type: "boolean", default: "false", description: "Open the details on mount." },
71
+ { name: "open", type: "boolean", description: "Controlled open state of the details." },
72
+ { name: "onOpenChange", type: "(open: boolean) => void", description: "Called when the details open or close." }
73
+ ],
74
+ variants: {
75
+ status: ["running", "awaiting", "complete", "error", "denied"]
76
+ },
77
+ tokensUsed: ["card", "border", "destructive", "muted", "muted-foreground", "foreground", "tone-warning", "ring"],
78
+ motion: [
79
+ {
80
+ slot: "arrival",
81
+ recipe: "none",
82
+ part: "Root",
83
+ why: "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."
84
+ },
85
+ {
86
+ slot: "response",
87
+ recipe: "motion-interactive",
88
+ part: "Header",
89
+ why: "The header is the disclosure trigger but reads as a status row. The hover step is what tells the person it opens."
90
+ },
91
+ {
92
+ slot: "transition",
93
+ recipe: "motion-icon-swap",
94
+ part: "Status glyph",
95
+ why: "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."
96
+ },
97
+ {
98
+ slot: "transition",
99
+ recipe: "motion-disclosure",
100
+ part: "Details",
101
+ why: "The height change connects the code that appears to the row that was pressed, in a thread where several rows look alike."
102
+ },
103
+ {
104
+ slot: "activity",
105
+ recipe: "motion-spin",
106
+ part: "Status glyph",
107
+ why: "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."
108
+ }
109
+ ],
110
+ a11y: [
111
+ "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.",
112
+ '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.',
113
+ "The glyph is aria-hidden: the status word beside it carries the meaning, so colour and shape are never the only signal."
114
+ ],
115
+ examples: [
116
+ {
117
+ label: "A finished call",
118
+ code: `<ToolCall
119
+ name="search_docs"
120
+ title="Searched the docs"
121
+ status="complete"
122
+ input={{ query: "date range picker" }}
123
+ output={{ hits: ["DatePicker", "Calendar", "DateTile"] }}
124
+ />`
125
+ },
126
+ {
127
+ label: "Running, then failed",
128
+ code: `<div className="flex flex-col gap-2">
129
+ <ToolCall name="read_file" title="Reading package.json" status="running" input={{ path: "package.json" }} />
130
+ <ToolCall
131
+ name="run_tests"
132
+ title="Ran the tests"
133
+ status="error"
134
+ error="3 of 412 tests failed in date-picker.test.tsx."
135
+ input={{ filter: "date-picker" }}
136
+ />
137
+ </div>`
138
+ },
139
+ {
140
+ label: "Waiting for approval",
141
+ code: `<ToolCall name="send_email" title="Send the invoice to Ada" status="awaiting" input={{ to: "ada@example.com", subject: "INV-1042" }} />`
142
+ }
143
+ ]
144
+ };
145
+ export {
146
+ toolCallMeta
147
+ };
@@ -0,0 +1,33 @@
1
+ // src/components/tool-call/tool-call.variants.ts
2
+ import { cva } from "class-variance-authority";
3
+ var toolCallVariants = cva(
4
+ "w-full overflow-hidden rounded-md border bg-card text-start",
5
+ {
6
+ variants: {
7
+ status: {
8
+ running: "border-border",
9
+ awaiting: "border-border",
10
+ complete: "border-border",
11
+ error: "border-destructive/40",
12
+ denied: "border-border"
13
+ }
14
+ },
15
+ defaultVariants: { status: "running" }
16
+ }
17
+ );
18
+ var toolCallHeaderVariants = cva(
19
+ "flex w-full min-w-0 items-center gap-2 px-3 py-2 text-start",
20
+ {
21
+ variants: {
22
+ interactive: {
23
+ true: "cursor-pointer motion-interactive sable-surface-interactive focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset",
24
+ false: ""
25
+ }
26
+ },
27
+ defaultVariants: { interactive: false }
28
+ }
29
+ );
30
+ export {
31
+ toolCallHeaderVariants,
32
+ toolCallVariants
33
+ };
@@ -43,7 +43,8 @@ var toolbarMeta = {
43
43
  },
44
44
  {
45
45
  guidance: "dont",
46
- description: "Use Toolbar for navigation between pages or views. Links that go somewhere are Tabs, SideNav or Breadcrumbs; ToolbarLink is for the odd link that sits among actions, like Help."
46
+ description: "Use Toolbar for navigation between pages or views. Links that go somewhere are Tabs, SideNav or Breadcrumbs; ToolbarLink is for the odd link that sits among actions, like Help.",
47
+ instead: ["Tabs", "SideNav", "Breadcrumbs"]
47
48
  },
48
49
  {
49
50
  guidance: "dont",
@@ -6,7 +6,8 @@ import {
6
6
  TooltipPortal,
7
7
  TooltipContent,
8
8
  TooltipArrow,
9
- SimpleTooltip
9
+ SimpleTooltip,
10
+ TooltipGroup
10
11
  } from "./tooltip.js";
11
12
  import { tooltipMeta } from "./tooltip.meta.js";
12
13
  export {
@@ -14,6 +15,7 @@ export {
14
15
  Tooltip,
15
16
  TooltipArrow,
16
17
  TooltipContent,
18
+ TooltipGroup,
17
19
  TooltipPortal,
18
20
  TooltipProvider,
19
21
  TooltipTrigger,
@@ -1,8 +1,10 @@
1
1
  // src/components/tooltip/tooltip.tsx
2
2
  import { Tooltip as Base } from "@base-ui/react/tooltip";
3
+ import { createContext, useContext, useMemo, useState } from "react";
3
4
  import { cn } from "../../lib/cn.js";
4
5
  import { withPortalThemeScope } from "../../lib/portal-theme-scope.js";
5
6
  import { useAnchoredDensity } from "../../lib/density.js";
7
+ import { motionAttribute } from "../../motion/MotionProvider.js";
6
8
  import { jsx, jsxs } from "react/jsx-runtime";
7
9
  var TooltipProvider = Base.Provider;
8
10
  var Tooltip = Base.Root;
@@ -58,16 +60,98 @@ function SimpleTooltip({
58
60
  sideOffset,
59
61
  ...root
60
62
  }) {
63
+ const group = useContext(TooltipGroupContext);
64
+ const payload = useMemo(
65
+ () => ({ content, side, align, sideOffset }),
66
+ [content, side, align, sideOffset]
67
+ );
68
+ if (group) {
69
+ return /* @__PURE__ */ jsx(Base.Trigger, { handle: group, payload, disabled: root.disabled, render: children });
70
+ }
61
71
  return /* @__PURE__ */ jsxs(Base.Root, { ...root, children: [
62
72
  /* @__PURE__ */ jsx(Base.Trigger, { render: children }),
63
73
  /* @__PURE__ */ jsx(TooltipContent, { side, align, sideOffset, children: content })
64
74
  ] });
65
75
  }
76
+ var TooltipGroupContext = createContext(null);
77
+ var GLIDE_DURATION = "calc(var(--motion-duration-slow) * var(--motion-expressive, 1))";
78
+ var GROUP_POSITIONER = cn(
79
+ "z-(--z-overlay) h-(--positioner-height) w-(--positioner-width)",
80
+ "[&:has([data-transitioning])]:[transition-property:top,right,bottom,left]",
81
+ "[&:has([data-transitioning])]:[transition-duration:calc(var(--motion-duration-slow)*var(--motion-expressive,1))]",
82
+ "[&:has([data-transitioning])]:ease-emphasized"
83
+ );
84
+ function groupPopupStyle(state) {
85
+ if (state.transitionStatus === "starting") return void 0;
86
+ const closing = state.transitionStatus === "ending";
87
+ return {
88
+ transitionProperty: "opacity, scale, width, height",
89
+ transitionDuration: closing ? `var(--motion-duration-exit), var(--motion-duration-exit), ${GLIDE_DURATION}, ${GLIDE_DURATION}` : `var(--motion-duration-fast), var(--motion-duration-fast), ${GLIDE_DURATION}, ${GLIDE_DURATION}`,
90
+ transitionTimingFunction: closing ? "var(--motion-ease-standard), var(--motion-ease-standard), var(--motion-ease-emphasized), var(--motion-ease-emphasized)" : "var(--motion-ease-standard), var(--motion-ease-spring), var(--motion-ease-emphasized), var(--motion-ease-emphasized)"
91
+ };
92
+ }
93
+ var GROUP_VIEWPORT = cn(
94
+ "relative",
95
+ "[&>*]:[transition-property:opacity,translate]",
96
+ "[&>*]:[transition-duration:var(--motion-duration-fast),calc(var(--motion-duration-slow)*var(--motion-expressive,1))]",
97
+ "[&>*]:[transition-timing-function:var(--motion-ease-standard),var(--motion-ease-emphasized)]",
98
+ "[&>[data-previous]]:w-(--popup-width) [&>[data-previous]]:whitespace-nowrap",
99
+ "[&>[data-current][data-starting-style]]:opacity-0 [&>[data-previous][data-ending-style]]:opacity-0",
100
+ // Travelling right: the new label comes in from the right, the old leaves left.
101
+ "[&[data-activation-direction~=right]>[data-current][data-starting-style]]:translate-x-2",
102
+ "[&[data-activation-direction~=right]>[data-previous][data-ending-style]]:-translate-x-2",
103
+ "[&[data-activation-direction~=left]>[data-current][data-starting-style]]:-translate-x-2",
104
+ "[&[data-activation-direction~=left]>[data-previous][data-ending-style]]:translate-x-2",
105
+ "[&[data-activation-direction~=down]>[data-current][data-starting-style]]:translate-y-2",
106
+ "[&[data-activation-direction~=down]>[data-previous][data-ending-style]]:-translate-y-2",
107
+ "[&[data-activation-direction~=up]>[data-current][data-starting-style]]:-translate-y-2",
108
+ "[&[data-activation-direction~=up]>[data-previous][data-ending-style]]:translate-y-2"
109
+ );
110
+ function TooltipGroup({ children, motion }) {
111
+ const [handle] = useState(() => Base.createHandle());
112
+ return /* @__PURE__ */ jsxs(TooltipGroupContext.Provider, { value: handle, children: [
113
+ children,
114
+ /* @__PURE__ */ jsx(Base.Root, { handle, children: ({ payload }) => /* @__PURE__ */ jsx(TooltipGroupContent, { payload, motion }) })
115
+ ] });
116
+ }
117
+ TooltipGroup.displayName = "TooltipGroup";
118
+ function TooltipGroupContent({
119
+ payload,
120
+ motion
121
+ }) {
122
+ const anchoredDensity = useAnchoredDensity();
123
+ return /* @__PURE__ */ jsx(TooltipPortal, { children: /* @__PURE__ */ jsx(
124
+ Base.Positioner,
125
+ {
126
+ side: payload?.side ?? "top",
127
+ align: payload?.align ?? "center",
128
+ sideOffset: payload?.sideOffset ?? 6,
129
+ "data-motion": motionAttribute(motion),
130
+ className: GROUP_POSITIONER,
131
+ children: /* @__PURE__ */ jsx(
132
+ Base.Popup,
133
+ {
134
+ ...anchoredDensity,
135
+ style: groupPopupStyle,
136
+ className: cn(
137
+ "select-none rounded-sm border border-border-floating bg-popover px-2.5 py-1.5 text-label-md text-popover-foreground shadow-md outline-none",
138
+ "h-[var(--popup-height,auto)] w-[var(--popup-width,auto)] overflow-clip",
139
+ // The enter/exit targets and transform origin; the transition
140
+ // list itself is groupPopupStyle's.
141
+ "motion-pop"
142
+ ),
143
+ children: /* @__PURE__ */ jsx(Base.Viewport, { className: GROUP_VIEWPORT, children: payload?.content })
144
+ }
145
+ )
146
+ }
147
+ ) });
148
+ }
66
149
  export {
67
150
  SimpleTooltip,
68
151
  Tooltip,
69
152
  TooltipArrow,
70
153
  TooltipContent,
154
+ TooltipGroup,
71
155
  TooltipPortal,
72
156
  TooltipProvider,
73
157
  TooltipTrigger
@@ -22,7 +22,8 @@ var tooltipMeta = {
22
22
  },
23
23
  {
24
24
  guidance: "dont",
25
- description: "Do not put critical instructions, errors, or interactive content inside Tooltip."
25
+ description: "Do not put critical instructions, errors, or interactive content inside Tooltip.",
26
+ instead: ["Popover", "InlineStatus"]
26
27
  },
27
28
  {
28
29
  guidance: "dont",
@@ -100,7 +101,12 @@ var tooltipMeta = {
100
101
  {
101
102
  name: "SimpleTooltip",
102
103
  type: "Base UI Tooltip.Root props + content: ReactNode + children: ReactElement + side | align | sideOffset",
103
- description: "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."
104
+ description: "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."
105
+ },
106
+ {
107
+ name: "TooltipGroup",
108
+ type: '{ children: ReactNode; motion?: "full" | "subtle" | "off" }',
109
+ description: "One tooltip shared by every SimpleTooltip inside it \u2014 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."
104
110
  }
105
111
  ],
106
112
  tokensUsed: ["popover", "popover-foreground", "border-floating"],
@@ -128,6 +134,12 @@ var tooltipMeta = {
128
134
  recipe: "none",
129
135
  part: "Content",
130
136
  why: "Tooltip copy is resident before the tooltip opens. Anything that has to be fetched is a Popover with its own loading state."
137
+ },
138
+ {
139
+ slot: "transition",
140
+ recipe: "custom",
141
+ part: "TooltipGroup",
142
+ why: "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."
131
143
  }
132
144
  ],
133
145
  a11y: [
@@ -148,6 +160,10 @@ var tooltipMeta = {
148
160
  {
149
161
  label: "Shorthand (SimpleTooltip)",
150
162
  code: '<SimpleTooltip content="Saved to your workspace."><Button variant="ghost" size="icon-sm" aria-label="Info"><Icon name="info" /></Button></SimpleTooltip>'
163
+ },
164
+ {
165
+ label: "One tooltip gliding along a toolbar (TooltipGroup)",
166
+ code: '<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>'
151
167
  }
152
168
  ]
153
169
  };
@@ -15,6 +15,7 @@ import {
15
15
  import { Icon } from "../../icon/index.js";
16
16
  import { cn } from "../../lib/cn.js";
17
17
  import { hitTargetMin } from "../../lib/hit-target.js";
18
+ import { motionAttribute, useMotionLevel } from "../../motion/MotionProvider.js";
18
19
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
19
20
  function treeIndent(depth) {
20
21
  return `calc(var(--tree-rail) + var(--tree-indent) * ${Math.max(0, depth)})`;
@@ -43,8 +44,39 @@ function useTreeItem(idProp) {
43
44
  claimFocus: () => ctx?.setFocusedId(id)
44
45
  };
45
46
  }
47
+ var ARRIVAL_CAP = 10;
48
+ var ARRIVAL_STEP_MS = 18;
49
+ function createArrival() {
50
+ const seen = /* @__PURE__ */ new Map();
51
+ return {
52
+ index(id) {
53
+ let at = seen.get(id);
54
+ if (at === void 0) {
55
+ at = seen.size;
56
+ seen.set(id, at);
57
+ }
58
+ return at;
59
+ }
60
+ };
61
+ }
62
+ var TreeArrivalContext = createContext(null);
63
+ var TreeMotionContext = createContext(void 0);
64
+ function arrivalProps(index) {
65
+ return {
66
+ className: "motion-arrive",
67
+ style: { "--motion-arrive-delay": `${Math.min(index, ARRIVAL_CAP) * ARRIVAL_STEP_MS}ms` }
68
+ };
69
+ }
70
+ function useArrival(id) {
71
+ const arrival = useContext(TreeArrivalContext);
72
+ const [props, setProps] = useState(() => arrival ? arrivalProps(arrival.index(id)) : {});
73
+ const onAnimationEnd = useCallback((event) => {
74
+ if (event.target === event.currentTarget && event.animationName === "sable-arrive") setProps({});
75
+ }, []);
76
+ return { ...props, onAnimationEnd };
77
+ }
46
78
  var ITEM_FOCUS = "focus-visible:outline-2 focus-visible:outline-offset-[-2px] focus-visible:outline-ring";
47
- function Tree({ className, children, onKeyDown, ...props }) {
79
+ function Tree({ className, children, onKeyDown, motion, ...props }) {
48
80
  const [focusedId, setFocusedId] = useState(null);
49
81
  const rootRef = useRef(null);
50
82
  const items = () => Array.from(rootRef.current?.querySelectorAll(ITEM) ?? []).filter(
@@ -120,17 +152,18 @@ function Tree({ className, children, onKeyDown, ...props }) {
120
152
  break;
121
153
  }
122
154
  };
123
- return /* @__PURE__ */ jsx(TreeContext.Provider, { value: context, children: /* @__PURE__ */ jsx(
155
+ return /* @__PURE__ */ jsx(TreeContext.Provider, { value: context, children: /* @__PURE__ */ jsx(TreeMotionContext.Provider, { value: motion, children: /* @__PURE__ */ jsx(
124
156
  "div",
125
157
  {
126
158
  ref: rootRef,
127
159
  role: "tree",
160
+ "data-motion": motionAttribute(motion),
128
161
  onKeyDown: handleKeyDown,
129
162
  className: cn("flex flex-col py-1", className),
130
163
  ...props,
131
164
  children
132
165
  }
133
- ) });
166
+ ) }) });
134
167
  }
135
168
  Tree.displayName = "Tree";
136
169
  function TreeSection({ title, hint, action, className, children, ...props }) {
@@ -199,10 +232,26 @@ function TreeRow({
199
232
  };
200
233
  const item = useTreeItem(idProp);
201
234
  const nameId = useId();
235
+ const arrivalOwn = useArrival(item.id);
236
+ const inheritedArrival = useContext(TreeArrivalContext);
237
+ const motionLevel = useMotionLevel(useContext(TreeMotionContext));
238
+ const [seenExpanded, setSeenExpanded] = useState(expanded);
239
+ const [arrival, setArrival] = useState(null);
240
+ if (seenExpanded !== expanded) {
241
+ setSeenExpanded(expanded);
242
+ setArrival(expanded && subtree !== void 0 && motionLevel.expressive ? createArrival() : null);
243
+ }
244
+ useEffect(() => {
245
+ if (arrival) setArrival(null);
246
+ }, [arrival]);
202
247
  const row = /* @__PURE__ */ jsxs(
203
248
  "div",
204
249
  {
205
250
  ...props,
251
+ onAnimationEnd: (event) => {
252
+ arrivalOwn.onAnimationEnd(event);
253
+ props.onAnimationEnd?.(event);
254
+ },
206
255
  id: item.inTree ? item.id : idProp,
207
256
  "data-selected": selected || void 0,
208
257
  role: item.inTree ? "treeitem" : void 0,
@@ -217,9 +266,10 @@ function TreeRow({
217
266
  onFocus?.(event);
218
267
  },
219
268
  onClick: onClick ?? (expandable ? () => setExpanded(!expanded) : void 0),
220
- style: { paddingInlineStart: treeIndent(depth), ...style },
269
+ style: { paddingInlineStart: treeIndent(depth), ...arrivalOwn.style, ...style },
221
270
  className: cn(
222
271
  "group relative flex h-(--control-xs) items-center gap-1.5 rounded-xs pe-1 motion-interactive",
272
+ arrivalOwn.className,
223
273
  selected ? "bg-panel-raised" : "hover:bg-panel-raised/60",
224
274
  item.inTree && ITEM_FOCUS,
225
275
  dimmed && "opacity-50",
@@ -253,6 +303,8 @@ function TreeRow({
253
303
  // The drawn box stays on --tree-column because that column is also
254
304
  // the indent step; a bigger button moves every name in the tree.
255
305
  hitTargetMin,
306
+ // motion-interactive lists `rotate` (what -rotate-90 writes), so the
307
+ // chevron turns with the rows arriving under it instead of snapping.
256
308
  "rounded-xs text-muted-foreground motion-interactive hover:text-foreground",
257
309
  !expanded && "-rotate-90"
258
310
  ),
@@ -268,7 +320,7 @@ function TreeRow({
268
320
  if (subtree === void 0) return row;
269
321
  return /* @__PURE__ */ jsxs(Fragment, { children: [
270
322
  row,
271
- expanded ? /* @__PURE__ */ jsx(TreeDepthContext.Provider, { value: depth + 1, children: subtree }) : null
323
+ expanded ? /* @__PURE__ */ jsx(TreeDepthContext.Provider, { value: depth + 1, children: /* @__PURE__ */ jsx(TreeArrivalContext.Provider, { value: arrival ?? inheritedArrival, children: subtree }) }) : null
272
324
  ] });
273
325
  }
274
326
  TreeRow.displayName = "TreeRow";
@@ -283,6 +335,7 @@ function TreeEmpty({
283
335
  }) {
284
336
  const inheritedDepth = useContext(TreeDepthContext);
285
337
  const depth = depthProp ?? inheritedDepth;
338
+ const arrival = useArrival(useId());
286
339
  return /* @__PURE__ */ jsxs(
287
340
  "div",
288
341
  {
@@ -290,9 +343,15 @@ function TreeEmpty({
290
343
  className: cn(
291
344
  "flex flex-col",
292
345
  dropTarget && "mx-2 my-1 gap-1 rounded-sm border border-dashed border-hairline-strong p-2",
346
+ arrival.className,
293
347
  className
294
348
  ),
295
349
  ...props,
350
+ onAnimationEnd: (event) => {
351
+ arrival.onAnimationEnd(event);
352
+ props.onAnimationEnd?.(event);
353
+ },
354
+ style: arrival.style ? { ...arrival.style, ...props.style } : props.style,
296
355
  children: [
297
356
  /* @__PURE__ */ jsx(
298
357
  "p",
@@ -325,11 +384,16 @@ function TreeAddRow({
325
384
  const inheritedDepth = useContext(TreeDepthContext);
326
385
  const depth = depthProp ?? inheritedDepth;
327
386
  const item = useTreeItem(idProp);
387
+ const arrival = useArrival(item.id);
328
388
  return /* @__PURE__ */ jsxs(
329
389
  "button",
330
390
  {
331
391
  type: "button",
332
392
  ...props,
393
+ onAnimationEnd: (event) => {
394
+ arrival.onAnimationEnd(event);
395
+ props.onAnimationEnd?.(event);
396
+ },
333
397
  id: item.inTree ? item.id : idProp,
334
398
  role: item.inTree ? "treeitem" : void 0,
335
399
  "aria-level": item.inTree ? depth + 1 : void 0,
@@ -338,9 +402,10 @@ function TreeAddRow({
338
402
  item.claimFocus();
339
403
  onFocus?.(event);
340
404
  },
341
- style: { paddingInlineStart: treeIndent(depth), ...style },
405
+ style: { paddingInlineStart: treeIndent(depth), ...arrival.style, ...style },
342
406
  className: cn(
343
407
  "group flex h-(--control-xs) w-full items-center gap-1.5 rounded-xs pe-1 motion-interactive",
408
+ arrival.className,
344
409
  "text-ui-md text-muted-foreground hover:bg-panel-raised/60 hover:text-foreground",
345
410
  ITEM_FOCUS,
346
411
  className
@@ -167,6 +167,12 @@ var treeMeta = {
167
167
  name: "label",
168
168
  type: "ReactNode",
169
169
  description: "TreeAddRow's visible label. Name what gets added \u2014 'Add effect', not 'Add'."
170
+ },
171
+ {
172
+ name: "motion",
173
+ type: '"full" | "subtle" | "off"',
174
+ default: '"full"',
175
+ description: "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 \u2014 it never overrides a reader's reduce-motion setting. Under `subtle` expanded rows appear at once instead of unfolding; the chevron still turns."
170
176
  }
171
177
  ],
172
178
  subcomponents: [
@@ -208,6 +214,32 @@ var treeMeta = {
208
214
  "Right-rail controls that appear on hover must also appear on focus-visible, or they are unreachable by keyboard."
209
215
  ],
210
216
  tokensUsed: ["panel-raised", "hairline", "foreground", "muted-foreground", "primary", "ring"],
217
+ motion: [
218
+ {
219
+ slot: "arrival",
220
+ recipe: "motion-arrive",
221
+ part: "TreeRow",
222
+ why: "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` \u2014 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."
223
+ },
224
+ {
225
+ slot: "response",
226
+ recipe: "motion-interactive",
227
+ part: "TreeRow",
228
+ why: "Hover and selection fills ease, so sweeping the pointer down a dense panel reads as a highlight following it rather than rows flickering."
229
+ },
230
+ {
231
+ slot: "transition",
232
+ recipe: "motion-interactive",
233
+ part: "Disclosure",
234
+ why: "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."
235
+ },
236
+ {
237
+ slot: "activity",
238
+ recipe: "none",
239
+ part: "Tree",
240
+ why: "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."
241
+ }
242
+ ],
211
243
  examples: [
212
244
  {
213
245
  label: "File tree",
@@ -57,6 +57,38 @@ var videoMeta = {
57
57
  }
58
58
  ],
59
59
  tokensUsed: ["border", "overlay", "overlay-foreground", "ring"],
60
+ motion: [
61
+ {
62
+ slot: "arrival",
63
+ recipe: "none",
64
+ part: "Root",
65
+ why: "The frame arrives with its page. Once there is a source, the poster and controls are the browser's."
66
+ },
67
+ {
68
+ slot: "response",
69
+ recipe: "motion-interactive",
70
+ part: "Play button",
71
+ why: "With `onRequestPlay`, the play disc firms its scrim on hover, eased, so the mark reads as pressable over any image."
72
+ },
73
+ {
74
+ slot: "response",
75
+ recipe: "motion-press",
76
+ part: "Play button",
77
+ why: "Loading a source can take a moment; the settle on press confirms the tap before the player appears."
78
+ },
79
+ {
80
+ slot: "transition",
81
+ recipe: "none",
82
+ part: "Root",
83
+ why: "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."
84
+ },
85
+ {
86
+ slot: "activity",
87
+ recipe: "none",
88
+ part: "Root",
89
+ why: "Buffering belongs to the native controls, which draw their own indicator. A second loader over the frame would compete with the browser's."
90
+ }
91
+ ],
60
92
  a11y: [
61
93
  "Mounts a native <video> element, preserving built-in keyboard controls when controls is enabled.",
62
94
  'Without onRequestPlay the placeholder play mark is decorative (aria-hidden) \u2014 nothing on the frame pretends to be pressable. With it, the mark is a native button named "Play video".'