@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
@@ -136,7 +136,7 @@ var toastMeta = {
136
136
  name: "type (toast option)",
137
137
  type: '"success" | "error" | "warning" | "info" | "loading"',
138
138
  default: "\u2014",
139
- description: "Set by the toast.* helpers. Drives a semantic left accent (success\u2192success, error\u2192destructive, warning\u2192tone-warning, info\u2192tone-info); loading shows a spinner. Both loading and error persist until updated or dismissed unless the call supplies a timeout."
139
+ description: "Set by the toast.* helpers. Drives a leading status glyph from the shared tone list, the same one Note and InlineStatus use (success\u2192success, error\u2192destructive, warning\u2192warning, info\u2192info), which blooms in as the toast lands; loading shows a spinner, and the glyph blooms in its place when the toast settles. Both loading and error persist until updated or dismissed unless the call supplies a timeout."
140
140
  }
141
141
  ],
142
142
  subcomponents: [
@@ -211,6 +211,12 @@ var toastMeta = {
211
211
  part: "ToastRoot (stacked)",
212
212
  why: "A stacked toast arrives from the anchored edge in front of the others, which step back and shrink to make room; hovering fans the deck out into the list. Moving every card, rather than snapping them, shows that the new one went on top and the old ones are still there."
213
213
  },
214
+ {
215
+ slot: "response",
216
+ recipe: "motion-bloom",
217
+ part: "Status glyph",
218
+ why: "The glyph is what says how it went, so it lands as its own beat: it blooms in 120ms after the card, and blooms again in the spinner's place when a loading toast settles. A 16px glyph needs that amplitude to read as an answer; the card itself only slides."
219
+ },
214
220
  {
215
221
  slot: "activity",
216
222
  recipe: "none",
@@ -3,8 +3,10 @@ import { Toggle as BaseToggle } from "@base-ui/react/toggle";
3
3
  import { ToggleGroup as BaseToggleGroup } from "@base-ui/react/toggle-group";
4
4
  import { createContext, useContext } from "react";
5
5
  import { cn } from "../../lib/cn.js";
6
- import { SEGMENTED_ITEM, SEGMENTED_TRACK } from "../../lib/segmented.js";
7
- import { jsx } from "react/jsx-runtime";
6
+ import { HighlightGlide } from "../../lib/highlight-glide.js";
7
+ import { SEGMENTED_INNER_RADIUS, SEGMENTED_ITEM, SEGMENTED_TRACK } from "../../lib/segmented.js";
8
+ import { motionAttribute } from "../../motion/MotionProvider.js";
9
+ import { jsx, jsxs } from "react/jsx-runtime";
8
10
  var ToggleAppearanceContext = createContext("default");
9
11
  function Toggle({ className, ...props }) {
10
12
  const appearance = useContext(ToggleAppearanceContext);
@@ -31,17 +33,38 @@ function Toggle({ className, ...props }) {
31
33
  );
32
34
  }
33
35
  Toggle.displayName = "Toggle";
34
- function ToggleGroup({ className, appearance = "default", ...props }) {
35
- return /* @__PURE__ */ jsx(ToggleAppearanceContext.Provider, { value: appearance, children: /* @__PURE__ */ jsx(
36
+ var SEGMENTED_GLIDE_HANDOFF = "[&[data-glide]>[data-pressed]]:bg-transparent";
37
+ function ToggleGroup({
38
+ className,
39
+ appearance = "default",
40
+ motion,
41
+ children,
42
+ ...props
43
+ }) {
44
+ const glide = appearance === "segmented" && !props.multiple;
45
+ return /* @__PURE__ */ jsx(ToggleAppearanceContext.Provider, { value: appearance, children: /* @__PURE__ */ jsxs(
36
46
  BaseToggleGroup,
37
47
  {
38
48
  "data-appearance": appearance,
49
+ "data-motion": motionAttribute(motion),
39
50
  className: cn(
40
51
  appearance === "segmented" ? SEGMENTED_TRACK : "inline-flex items-center gap-1",
52
+ glide && SEGMENTED_GLIDE_HANDOFF,
41
53
  className
42
54
  ),
43
55
  ...props,
44
- "aria-orientation": props.role === "toolbar" ? props.orientation ?? "horizontal" : void 0
56
+ "aria-orientation": props.role === "toolbar" ? props.orientation ?? "horizontal" : void 0,
57
+ children: [
58
+ glide && /* @__PURE__ */ jsx(
59
+ HighlightGlide,
60
+ {
61
+ follow: "data-pressed",
62
+ timing: "selection",
63
+ className: cn(SEGMENTED_INNER_RADIUS, "bg-accent motion-glide")
64
+ }
65
+ ),
66
+ children
67
+ ]
45
68
  }
46
69
  ) });
47
70
  }
@@ -69,7 +69,7 @@ var toggleMeta = {
69
69
  name: "appearance",
70
70
  type: '"default" | "segmented"',
71
71
  default: '"default"',
72
- description: "On ToggleGroup: `default` is a loose row of toggles; `segmented` draws the capsule segmented control (one track, the pressed option filled) shared with ThemeSwitcher. Toggles inside inherit it."
72
+ description: "On ToggleGroup: `default` is a loose row of toggles; `segmented` draws the capsule segmented control (one track, the pressed option filled) shared with ThemeSwitcher. Single-select segmented draws ONE fill that glides to the pressed option and resizes to its label; multi-select keeps a fill per pressed option. Toggles inside inherit it."
73
73
  },
74
74
  {
75
75
  name: "multiple",
@@ -77,6 +77,12 @@ var toggleMeta = {
77
77
  default: "false",
78
78
  description: "On ToggleGroup: allow several toggles pressed at once. The default is single-select, which behaves like a radio group."
79
79
  },
80
+ {
81
+ name: "motion",
82
+ type: '"full" | "subtle" | "off"',
83
+ default: '"full"',
84
+ description: "On ToggleGroup: how much motion this component uses: `full` (default) inherits the app's policy, `subtle` keeps feedback and drops the glide, `off` stills it. It can only turn motion down \u2014 it never overrides a reader's reduce-motion setting."
85
+ },
80
86
  {
81
87
  name: "Toggle",
82
88
  type: "Base UI Toggle props (pressed, defaultPressed, onPressedChange, disabled, value) + className",
@@ -87,7 +93,7 @@ var toggleMeta = {
87
93
  subcomponents: [
88
94
  {
89
95
  name: "ToggleGroup",
90
- type: 'Base UI ToggleGroup props (value, defaultValue, onValueChange, multiple, disabled, orientation, loopFocus) + appearance?: "default" | "segmented" + className',
96
+ type: 'Base UI ToggleGroup props (value, defaultValue, onValueChange, multiple, disabled, orientation, loopFocus) + appearance?: "default" | "segmented" + motion?: "full" | "subtle" | "off" + className',
91
97
  description: "Groups child Toggles and shares pressed state across them \u2014 each Toggle inside carries a value. Defaults to multiple: false (single-select) \u2014 the fit for view-mode switches and segmented controls; pass multiple to allow several pressed at once, e.g. a row of formatting toggles."
92
98
  },
93
99
  {
@@ -100,6 +106,38 @@ var toggleMeta = {
100
106
  appearance: ["default", "segmented"]
101
107
  },
102
108
  tokensUsed: ["muted-foreground", "foreground", "accent", "accent-foreground", "card", "border", "ring"],
109
+ motion: [
110
+ {
111
+ slot: "arrival",
112
+ recipe: "none",
113
+ part: "Toggle",
114
+ why: "Toggles arrive with the toolbar or header they sit in. Animating each one in would make a row of controls ripple for no reason."
115
+ },
116
+ {
117
+ slot: "response",
118
+ recipe: "motion-interactive",
119
+ part: "Toggle",
120
+ why: "Hover and the pressed fill ease rather than flip, so pressing reads as the same button changing state, not a different button appearing."
121
+ },
122
+ {
123
+ slot: "response",
124
+ recipe: "motion-press",
125
+ part: "Toggle",
126
+ why: "The small settle under the finger confirms the press landed before the state change is read \u2014 the difference matters most on touch, where there is no hover."
127
+ },
128
+ {
129
+ slot: "transition",
130
+ recipe: "motion-glide",
131
+ part: "Segmented fill",
132
+ why: 'In a single-select segmented group one fill travels to the chosen option and stretches to its label, so a view switch reads as one control now pointing elsewhere. Measured, because labels differ in width. Expressive: motion="subtle" lands it in place.'
133
+ },
134
+ {
135
+ slot: "activity",
136
+ recipe: "none",
137
+ part: "Toggle",
138
+ why: "A toggle commits instantly. Work that follows from the new state shows where its result appears, not on the control."
139
+ }
140
+ ],
103
141
  a11y: [
104
142
  "Base UI sets aria-pressed on each Toggle and manages the pressed/disabled states.",
105
143
  "ToggleGroup provides arrow-key roving focus across its items; loopFocus wraps at the ends.",
@@ -119,6 +157,10 @@ var toggleMeta = {
119
157
  label: "Segmented view switch",
120
158
  code: '<ToggleGroup appearance="segmented" defaultValue={["list"]} aria-label="View"><Toggle value="list" aria-label="List view"><Icon name="list" /></Toggle><Toggle value="grid" aria-label="Grid view"><Icon name="grid" /></Toggle></ToggleGroup>'
121
159
  },
160
+ {
161
+ label: "Segmented with labels, subtle motion",
162
+ code: '<ToggleGroup appearance="segmented" motion="subtle" defaultValue={["week"]} aria-label="Range"><Toggle value="day">Day</Toggle><Toggle value="week">Week</Toggle><Toggle value="month">Month</Toggle></ToggleGroup>'
163
+ },
122
164
  {
123
165
  label: "Multi-select group",
124
166
  code: '<ToggleGroup multiple defaultValue={["bold"]}><Toggle value="bold">Bold</Toggle><Toggle value="italic">Italic</Toggle></ToggleGroup>'
@@ -0,0 +1,8 @@
1
+ // src/components/tool-call/index.ts
2
+ import { TOOL_CALL_STATUS_LABELS, ToolCall } from "./tool-call.js";
3
+ import { toolCallMeta } from "./tool-call.meta.js";
4
+ export {
5
+ TOOL_CALL_STATUS_LABELS,
6
+ ToolCall,
7
+ toolCallMeta
8
+ };
@@ -0,0 +1,117 @@
1
+ // src/components/tool-call/tool-call.tsx
2
+ import { useRef } from "react";
3
+ import { Icon } from "../../icon/index.js";
4
+ import { cn } from "../../lib/cn.js";
5
+ import { useTypeTier } from "../../lib/density.js";
6
+ import { CodeBlock } from "../code-block/index.js";
7
+ import {
8
+ Collapsible,
9
+ CollapsibleIndicator,
10
+ CollapsiblePanel,
11
+ CollapsibleTrigger
12
+ } from "../collapsible/index.js";
13
+ import { Spinner } from "../spinner/index.js";
14
+ import { toolCallHeaderVariants, toolCallVariants } from "./tool-call.variants.js";
15
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
16
+ var TOOL_CALL_STATUS_LABELS = {
17
+ running: "Running",
18
+ awaiting: "Needs approval",
19
+ complete: "Done",
20
+ error: "Failed",
21
+ denied: "Denied"
22
+ };
23
+ var STATUSES = Object.keys(TOOL_CALL_STATUS_LABELS);
24
+ function formatData(value) {
25
+ if (typeof value === "string") return value;
26
+ try {
27
+ return JSON.stringify(value, null, 2) ?? String(value);
28
+ } catch {
29
+ return String(value);
30
+ }
31
+ }
32
+ function ToolCall({
33
+ name,
34
+ title,
35
+ status = "running",
36
+ statusLabel,
37
+ input,
38
+ output,
39
+ error,
40
+ children,
41
+ defaultOpen = false,
42
+ open,
43
+ onOpenChange,
44
+ className,
45
+ ...props
46
+ }) {
47
+ const tier = useTypeTier();
48
+ const word = statusLabel ?? TOOL_CALL_STATUS_LABELS[status];
49
+ const mountedStatus = useRef(status).current;
50
+ const announcement = status === mountedStatus ? "" : `${typeof title === "string" ? title : name}: ${word}`;
51
+ const hasInput = input !== void 0;
52
+ const hasOutput = children !== void 0 || output !== void 0;
53
+ const hasDetails = hasInput || hasOutput;
54
+ const row = /* @__PURE__ */ jsxs(Fragment, { children: [
55
+ /* @__PURE__ */ jsx("span", { "aria-hidden": "true", className: "motion-icon-swap size-4 shrink-0", children: STATUSES.map((s) => /* @__PURE__ */ jsx("span", { "data-active": s === status, className: "inline-flex", children: s === "running" ? /* @__PURE__ */ jsx(
56
+ Spinner,
57
+ {
58
+ size: "sm",
59
+ role: "none",
60
+ "aria-label": void 0,
61
+ className: cn("text-muted-foreground", status !== "running" && "[&_svg]:[animation-play-state:paused]")
62
+ }
63
+ ) : /* @__PURE__ */ jsx(Icon, { name: GLYPHS[s].name, className: GLYPHS[s].className }) }, s)) }),
64
+ /* @__PURE__ */ jsxs("span", { className: cn("flex min-w-0 flex-1 items-baseline gap-2", tier.body), children: [
65
+ /* @__PURE__ */ jsx("span", { className: cn("truncate text-foreground", !title && "font-mono"), children: title ?? name }),
66
+ title ? /* @__PURE__ */ jsx("span", { className: cn("hidden truncate font-mono text-muted-foreground sm:inline", tier.secondary), children: name }) : null
67
+ ] }),
68
+ /* @__PURE__ */ jsx("span", { className: cn("shrink-0 text-muted-foreground", tier.secondary, status === "error" && "text-destructive"), children: word }),
69
+ hasDetails ? /* @__PURE__ */ jsx(CollapsibleIndicator, {}) : null
70
+ ] });
71
+ const errorLine = status === "error" && error ? /* @__PURE__ */ jsx("p", { className: cn("px-3 pb-2 text-destructive", tier.supporting), children: error }) : null;
72
+ const live = /* @__PURE__ */ jsx("span", { role: "status", className: "sr-only", children: announcement });
73
+ if (!hasDetails) {
74
+ return /* @__PURE__ */ jsxs("div", { "data-status": status, className: cn(toolCallVariants({ status }), className), ...props, children: [
75
+ /* @__PURE__ */ jsx("div", { className: toolCallHeaderVariants({ interactive: false }), children: row }),
76
+ errorLine,
77
+ live
78
+ ] });
79
+ }
80
+ return /* @__PURE__ */ jsxs(
81
+ Collapsible,
82
+ {
83
+ "data-status": status,
84
+ defaultOpen,
85
+ open,
86
+ onOpenChange: onOpenChange ? (next) => onOpenChange(next) : void 0,
87
+ className: cn(toolCallVariants({ status }), className),
88
+ ...props,
89
+ children: [
90
+ /* @__PURE__ */ jsx(CollapsibleTrigger, { className: cn("group/collapsible", toolCallHeaderVariants({ interactive: true })), children: row }),
91
+ errorLine,
92
+ /* @__PURE__ */ jsx(CollapsiblePanel, { children: /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-3 border-t border-border p-3", children: [
93
+ hasInput ? /* @__PURE__ */ jsx(Detail, { label: "Input", tier: tier.label, code: formatData(input) }) : null,
94
+ children !== void 0 ? children : output !== void 0 ? /* @__PURE__ */ jsx(Detail, { label: "Output", tier: tier.label, code: formatData(output) }) : null
95
+ ] }) }),
96
+ live
97
+ ]
98
+ }
99
+ );
100
+ }
101
+ ToolCall.displayName = "ToolCall";
102
+ var GLYPHS = {
103
+ awaiting: { name: "clock", className: "text-tone-warning" },
104
+ complete: { name: "check", className: "text-muted-foreground" },
105
+ error: { name: "warning", className: "text-destructive" },
106
+ denied: { name: "xmark", className: "text-muted-foreground" }
107
+ };
108
+ function Detail({ label, tier, code }) {
109
+ return /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 flex-col gap-1.5", children: [
110
+ /* @__PURE__ */ jsx("span", { className: cn("text-muted-foreground", tier), children: label }),
111
+ /* @__PURE__ */ jsx(CodeBlock, { code })
112
+ ] });
113
+ }
114
+ export {
115
+ TOOL_CALL_STATUS_LABELS,
116
+ ToolCall
117
+ };
@@ -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
+ };
@@ -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
@@ -101,7 +101,12 @@ var tooltipMeta = {
101
101
  {
102
102
  name: "SimpleTooltip",
103
103
  type: "Base UI Tooltip.Root props + content: ReactNode + children: ReactElement + side | align | sideOffset",
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."
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."
105
110
  }
106
111
  ],
107
112
  tokensUsed: ["popover", "popover-foreground", "border-floating"],
@@ -129,6 +134,12 @@ var tooltipMeta = {
129
134
  recipe: "none",
130
135
  part: "Content",
131
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."
132
143
  }
133
144
  ],
134
145
  a11y: [
@@ -149,6 +160,10 @@ var tooltipMeta = {
149
160
  {
150
161
  label: "Shorthand (SimpleTooltip)",
151
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>'
152
167
  }
153
168
  ]
154
169
  };