@sableui/react 0.13.2 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (182) hide show
  1. package/AGENTS.md +2 -2
  2. package/CHANGELOG.md +114 -0
  3. package/CONTRIBUTING.md +6 -4
  4. package/DESIGN.md +8 -3
  5. package/dist/blocks/approval-block.js +93 -0
  6. package/dist/blocks/approval-block.meta.js +23 -0
  7. package/dist/blocks/index.d.ts +16 -1
  8. package/dist/blocks/index.js +4 -0
  9. package/dist/components/accordion/accordion.meta.js +8 -2
  10. package/dist/components/area-chart/area-chart.meta.js +10 -3
  11. package/dist/components/attachment/attachment.meta.js +26 -0
  12. package/dist/components/bar-chart/bar-chart.meta.js +11 -4
  13. package/dist/components/bottom-nav/bottom-nav.js +3 -1
  14. package/dist/components/bottom-nav/bottom-nav.meta.js +32 -0
  15. package/dist/components/breadcrumbs/breadcrumbs.meta.js +26 -0
  16. package/dist/components/button/button.js +21 -4
  17. package/dist/components/button/button.meta.js +7 -7
  18. package/dist/components/calendar/calendar.js +18 -1
  19. package/dist/components/calendar/calendar.meta.js +38 -0
  20. package/dist/components/carousel/carousel.js +6 -4
  21. package/dist/components/carousel/carousel.meta.js +42 -0
  22. package/dist/components/chat-composer/chat-composer.js +21 -21
  23. package/dist/components/chat-composer/chat-composer.meta.js +38 -0
  24. package/dist/components/chat-composer/use-eased-growth.js +70 -0
  25. package/dist/components/checkbox/checkbox-mark.js +8 -1
  26. package/dist/components/checkbox/checkbox.js +3 -1
  27. package/dist/components/checkbox/checkbox.meta.js +32 -0
  28. package/dist/components/checkbox-card/checkbox-card.js +3 -0
  29. package/dist/components/checkbox-card/checkbox-card.meta.js +32 -0
  30. package/dist/components/code-block/code-block.js +14 -3
  31. package/dist/components/code-block/code-block.meta.js +28 -1
  32. package/dist/components/collapsible/collapsible.meta.js +32 -0
  33. package/dist/components/color-field/color-field.meta.js +26 -0
  34. package/dist/components/combobox/combobox.meta.js +44 -0
  35. package/dist/components/command-menu/command-menu.js +8 -3
  36. package/dist/components/command-menu/command-menu.meta.js +38 -0
  37. package/dist/components/confirm-button/confirm-button.js +179 -0
  38. package/dist/components/confirm-button/confirm-button.meta.js +143 -0
  39. package/dist/components/confirm-button/index.js +7 -0
  40. package/dist/components/data-table/data-table.js +6 -2
  41. package/dist/components/data-table/data-table.meta.js +32 -0
  42. package/dist/components/entity/entity.meta.js +26 -0
  43. package/dist/components/feedback/feedback.js +113 -47
  44. package/dist/components/feedback/feedback.meta.js +49 -4
  45. package/dist/components/input-group/input-group.meta.js +27 -1
  46. package/dist/components/line-chart/line-chart.meta.js +10 -3
  47. package/dist/components/middle-truncate/middle-truncate.meta.js +1 -1
  48. package/dist/components/note/note.meta.js +32 -0
  49. package/dist/components/notification-badge/notification-badge.js +17 -5
  50. package/dist/components/notification-badge/notification-badge.meta.js +34 -2
  51. package/dist/components/number-ticker/index.js +7 -0
  52. package/dist/components/number-ticker/number-ticker.js +246 -0
  53. package/dist/components/number-ticker/number-ticker.meta.js +159 -0
  54. package/dist/components/otp-field/otp-field.js +23 -3
  55. package/dist/components/otp-field/otp-field.meta.js +23 -2
  56. package/dist/components/panel-group/panel-group.meta.js +26 -0
  57. package/dist/components/panel-section/panel-section.js +37 -10
  58. package/dist/components/panel-section/panel-section.meta.js +32 -0
  59. package/dist/components/pie-chart/pie-chart.js +20 -11
  60. package/dist/components/pie-chart/pie-chart.meta.js +17 -4
  61. package/dist/components/pill/pill.meta.js +32 -0
  62. package/dist/components/radar-chart/radar-chart.js +4 -3
  63. package/dist/components/radar-chart/radar-chart.meta.js +10 -3
  64. package/dist/components/radial-chart/radial-chart.js +22 -22
  65. package/dist/components/radial-chart/radial-chart.meta.js +17 -4
  66. package/dist/components/radio/radio.meta.js +26 -0
  67. package/dist/components/radio-card/radio-card.meta.js +38 -0
  68. package/dist/components/relative-time/relative-time.meta.js +32 -0
  69. package/dist/components/scroller/scroller.meta.js +32 -0
  70. package/dist/components/scrub-field/scrub-field.meta.js +32 -0
  71. package/dist/components/search-input/search-input.js +9 -7
  72. package/dist/components/search-input/search-input.meta.js +26 -0
  73. package/dist/components/show-more/show-more.js +1 -4
  74. package/dist/components/show-more/show-more.meta.js +38 -0
  75. package/dist/components/side-nav/side-nav.js +21 -1
  76. package/dist/components/side-nav/side-nav.meta.js +32 -0
  77. package/dist/components/side-nav/side-nav.variants.js +3 -1
  78. package/dist/components/sidebar/sidebar.js +31 -8
  79. package/dist/components/sidebar/sidebar.meta.js +38 -0
  80. package/dist/components/slider/slider.meta.js +26 -0
  81. package/dist/components/sparkline/sparkline.js +7 -4
  82. package/dist/components/sparkline/sparkline.meta.js +9 -2
  83. package/dist/components/table/table.meta.js +26 -0
  84. package/dist/components/tabs/tabs.js +27 -8
  85. package/dist/components/tabs/tabs.meta.js +46 -3
  86. package/dist/components/theme-selector/theme-selector.meta.js +32 -0
  87. package/dist/components/theme-switcher/theme-switcher.js +4 -1
  88. package/dist/components/theme-switcher/theme-switcher.meta.js +42 -0
  89. package/dist/components/toggle/toggle.js +28 -5
  90. package/dist/components/toggle/toggle.meta.js +44 -2
  91. package/dist/components/tool-call/index.js +8 -0
  92. package/dist/components/tool-call/tool-call.js +117 -0
  93. package/dist/components/tool-call/tool-call.meta.js +147 -0
  94. package/dist/components/tool-call/tool-call.variants.js +33 -0
  95. package/dist/components/tooltip/index.js +3 -1
  96. package/dist/components/tooltip/tooltip.js +84 -0
  97. package/dist/components/tooltip/tooltip.meta.js +16 -1
  98. package/dist/components/tree/tree.js +71 -6
  99. package/dist/components/tree/tree.meta.js +32 -0
  100. package/dist/components/video/video.meta.js +32 -0
  101. package/dist/file-hashes.json +114 -97
  102. package/dist/foundations/motion.js +49 -3
  103. package/dist/index.d.ts +614 -44
  104. package/dist/index.js +4 -1
  105. package/dist/lib/chart/cartesian.js +9 -10
  106. package/dist/lib/chart/chart-center.js +30 -0
  107. package/dist/lib/chart/chart-frame.js +15 -8
  108. package/dist/lib/chart/dim.js +13 -0
  109. package/dist/lib/chart/format.js +7 -1
  110. package/dist/lib/chart/motion.js +6 -4
  111. package/dist/lib/highlight-glide.js +68 -10
  112. package/dist/lib/taxonomy.js +3 -0
  113. package/dist/motion/MotionProvider.js +45 -3
  114. package/dist/motion/Reveal.js +1 -1
  115. package/dist/motion/TextReveal.js +1 -1
  116. package/dist/motion/index.js +15 -2
  117. package/dist/motion/useAutoSize.js +42 -0
  118. package/dist/motion/useShake.js +25 -0
  119. package/dist/styles.css +298 -5
  120. package/llms/blocks/approval-block.md +128 -0
  121. package/llms/components/accordion.md +3 -2
  122. package/llms/components/area-chart.md +4 -3
  123. package/llms/components/attachment.md +7 -0
  124. package/llms/components/bar-chart.md +5 -4
  125. package/llms/components/bottom-nav.md +8 -0
  126. package/llms/components/breadcrumbs.md +7 -0
  127. package/llms/components/button.md +6 -6
  128. package/llms/components/calendar.md +9 -0
  129. package/llms/components/carousel.md +17 -0
  130. package/llms/components/chat-composer.md +9 -0
  131. package/llms/components/checkbox-card.md +8 -0
  132. package/llms/components/checkbox.md +8 -0
  133. package/llms/components/code-block.md +8 -0
  134. package/llms/components/collapsible.md +8 -0
  135. package/llms/components/color-field.md +7 -0
  136. package/llms/components/combobox.md +10 -0
  137. package/llms/components/command-menu.md +9 -0
  138. package/llms/components/confirm-button.md +92 -0
  139. package/llms/components/data-table.md +8 -0
  140. package/llms/components/entity.md +7 -0
  141. package/llms/components/feedback.md +22 -3
  142. package/llms/components/input-group.md +8 -1
  143. package/llms/components/line-chart.md +4 -3
  144. package/llms/components/middle-truncate.md +1 -1
  145. package/llms/components/note.md +8 -0
  146. package/llms/components/notification-badge.md +10 -2
  147. package/llms/components/number-ticker.md +86 -0
  148. package/llms/components/otp-field.md +15 -1
  149. package/llms/components/panel-group.md +7 -0
  150. package/llms/components/panel-section.md +8 -0
  151. package/llms/components/pie-chart.md +6 -4
  152. package/llms/components/pill.md +8 -0
  153. package/llms/components/radar-chart.md +4 -3
  154. package/llms/components/radial-chart.md +6 -4
  155. package/llms/components/radio-card.md +9 -0
  156. package/llms/components/radio.md +7 -0
  157. package/llms/components/relative-time.md +8 -0
  158. package/llms/components/scroller.md +8 -0
  159. package/llms/components/scrub-field.md +8 -0
  160. package/llms/components/search-input.md +7 -0
  161. package/llms/components/show-more.md +9 -0
  162. package/llms/components/side-nav.md +8 -0
  163. package/llms/components/sidebar.md +9 -0
  164. package/llms/components/slider.md +7 -0
  165. package/llms/components/sparkline.md +3 -2
  166. package/llms/components/table.md +7 -0
  167. package/llms/components/tabs.md +20 -2
  168. package/llms/components/theme-selector.md +8 -0
  169. package/llms/components/theme-switcher.md +17 -0
  170. package/llms/components/toggle.md +19 -2
  171. package/llms/components/tool-call.md +115 -0
  172. package/llms/components/tooltip.md +11 -1
  173. package/llms/components/tree.md +8 -0
  174. package/llms/components/video.md +8 -0
  175. package/llms/foundations/motion.md +48 -3
  176. package/llms/reference/utilities.md +23 -1
  177. package/llms/tasks/reviewing-motion.md +14 -0
  178. package/llms-full.txt +955 -49
  179. package/llms.txt +15 -10
  180. package/package.json +1 -1
  181. package/registry.json +2565 -210
  182. package/targets/ios.json +27 -0
package/dist/index.js CHANGED
@@ -43,6 +43,7 @@ export * from "./icon/index.js";
43
43
  export * from "./motion/index.js";
44
44
  export * from "./components/button/index.js";
45
45
  export * from "./components/button-group/index.js";
46
+ export * from "./components/confirm-button/index.js";
46
47
  export * from "./components/card/index.js";
47
48
  export * from "./components/caret-menu/index.js";
48
49
  export * from "./components/badge/index.js";
@@ -63,10 +64,12 @@ export * from "./components/avatar/index.js";
63
64
  export * from "./components/skeleton/index.js";
64
65
  export * from "./components/spinner/index.js";
65
66
  export * from "./components/thinking-orb/index.js";
67
+ export * from "./components/tool-call/index.js";
66
68
  export * from "./components/streaming-text/index.js";
67
69
  export * from "./components/gliding-highlight/index.js";
68
70
  export * from "./components/progress/index.js";
69
71
  export * from "./components/meter/index.js";
72
+ export * from "./components/number-ticker/index.js";
70
73
  export * from "./components/segmented-bar/index.js";
71
74
  export * from "./components/area-chart/index.js";
72
75
  export * from "./components/bar-chart/index.js";
@@ -172,7 +175,7 @@ import {
172
175
  } from "./lib/use-caret-trigger.js";
173
176
  import { STATUS_TONES, STATUS_TONE_ICON } from "./lib/status-tone.js";
174
177
  import { PALETTE_COLORS, PALETTE_FAMILIES, PALETTE_SCALES, PALETTE_STEPS, PALETTE_TONES, isPaletteColor } from "./lib/palette.js";
175
- var SABLE_UI_VERSION = "0.13.2";
178
+ var SABLE_UI_VERSION = "0.14.0";
176
179
  export {
177
180
  ART_TOKENS,
178
181
  CHART_SERIES_COUNT,
@@ -3,10 +3,11 @@
3
3
  // src/lib/chart/cartesian.tsx
4
4
  import { useId } from "react";
5
5
  import { cn } from "../cn.js";
6
- import { ChartFrame } from "./chart-frame.js";
6
+ import { ChartFrame, chartMotion } from "./chart-frame.js";
7
7
  import { ChartLegend } from "./chart-legend.js";
8
8
  import { ChartTooltipContent } from "./chart-tooltip.js";
9
9
  import { approxTextWidth, categoryText, chartFormatters } from "./format.js";
10
+ import { FILL_HOVER, dimmed } from "./dim.js";
10
11
  import { areaPath, linePath, rectPath } from "./geometry.js";
11
12
  import { GLIDE, barTransition, drawTransition, morphTransition, pathStyle, riseTransition, stagger } from "./motion.js";
12
13
  import { bandScale, extent, linearScale, niceTicks, pointScale, stack } from "./scale.js";
@@ -34,7 +35,8 @@ function CartesianChart(props) {
34
35
  locale,
35
36
  tooltip = true,
36
37
  legend,
37
- animate = true,
38
+ motion,
39
+ animate,
38
40
  activeIndex,
39
41
  defaultActiveIndex,
40
42
  onActiveIndexChange,
@@ -150,7 +152,7 @@ function CartesianChart(props) {
150
152
  className,
151
153
  count: n,
152
154
  interactive: tooltip !== false,
153
- animate,
155
+ motion: chartMotion(motion, animate),
154
156
  activeIndex,
155
157
  defaultActiveIndex,
156
158
  onActiveIndexChange,
@@ -232,15 +234,12 @@ function CartesianChart(props) {
232
234
  "path",
233
235
  {
234
236
  d,
235
- className: cn(
236
- horizontal ? "[[data-reveal=out]_&]:scale-x-0" : "[[data-reveal=out]_&]:scale-y-0",
237
- dim && "opacity-40"
238
- ),
237
+ className: horizontal ? "[[data-reveal=out]_&]:scale-x-0" : "[[data-reveal=out]_&]:scale-y-0",
239
238
  style: {
240
239
  ...pathStyle(d),
241
- fill: colorAt(k, i),
240
+ fill: dim ? dimmed(colorAt(k, i), 38) : colorAt(k, i),
242
241
  transformOrigin: origin,
243
- transition: barTransition(stagger(i))
242
+ transition: `${barTransition(stagger(i))}, ${FILL_HOVER}`
244
243
  }
245
244
  },
246
245
  `b${k}-${i}`
@@ -329,7 +328,7 @@ function CartesianChart(props) {
329
328
  r: 4,
330
329
  strokeWidth: 2,
331
330
  className: cn(SURFACE, "[[data-reveal=out]_&]:opacity-0"),
332
- style: { fill: colorBy === "index" ? colorAt(k, i) : s.colorValue, transition: morphTransition(), transitionDelay: `calc(${300 + i * 25}ms * var(--motion-scale, 1))` }
331
+ style: { fill: colorBy === "index" ? colorAt(k, i) : s.colorValue, transition: morphTransition(), transitionDelay: `calc(${300 + i * 25}ms * var(--motion-scale, 1) * var(--motion-expressive, 1))` }
333
332
  },
334
333
  `p${k}-${i}`
335
334
  )
@@ -0,0 +1,30 @@
1
+ "use client";
2
+
3
+ // src/lib/chart/chart-center.tsx
4
+ import { NumberTicker } from "../../components/number-ticker/number-ticker.js";
5
+ import { cn } from "../cn.js";
6
+ import { centerValueFormat } from "./format.js";
7
+ import { morphTransition } from "./motion.js";
8
+ import { jsx, jsxs } from "react/jsx-runtime";
9
+ function ChartCenter({ center, x, y, placement = "middle", locale, motion }) {
10
+ const value = typeof center.value === "number" ? /* @__PURE__ */ jsx(NumberTicker, { value: center.value, format: centerValueFormat(center.value), locales: locale, motion, className: "text-heading-2 text-foreground" }) : /* @__PURE__ */ jsx("span", { className: "text-heading-2 text-foreground tabular-nums", children: center.value });
11
+ const middle = placement === "middle";
12
+ return /* @__PURE__ */ jsx(
13
+ "div",
14
+ {
15
+ className: "absolute whitespace-nowrap [[data-reveal=out]_&]:opacity-0",
16
+ style: { left: x, top: y, transform: middle ? "translate(-50%, -50%)" : "translate(-50%, -100%)", transition: morphTransition() },
17
+ children: /* @__PURE__ */ jsxs("div", { className: cn("relative flex flex-col items-center", !middle && center.label && "pb-4"), children: [
18
+ value,
19
+ center.label ? middle ? (
20
+ // Out of flow, so the number alone is centred; centred by flex so a
21
+ // caption wider than the number overhangs it evenly on both sides.
22
+ /* @__PURE__ */ jsx("span", { className: "absolute inset-x-0 top-full mt-1 flex justify-center", children: /* @__PURE__ */ jsx("span", { className: "text-caption text-muted-foreground", children: center.label }) })
23
+ ) : /* @__PURE__ */ jsx("span", { className: "text-caption text-muted-foreground", children: center.label }) : null
24
+ ] })
25
+ }
26
+ );
27
+ }
28
+ export {
29
+ ChartCenter
30
+ };
@@ -2,16 +2,19 @@
2
2
 
3
3
  // src/lib/chart/chart-frame.tsx
4
4
  import { useEffect, useId, useRef, useState } from "react";
5
- import { useMotion } from "../../motion/MotionProvider.js";
5
+ import { motionAttribute, useMotionLevel } from "../../motion/MotionProvider.js";
6
6
  import { useArrival } from "./use-arrival.js";
7
7
  import { cn } from "../cn.js";
8
8
  import { jsx, jsxs } from "react/jsx-runtime";
9
+ function chartMotion(motion, animate) {
10
+ return motion ?? (animate === false ? "subtle" : void 0);
11
+ }
9
12
  function ChartFrame({
10
13
  label,
11
14
  className,
12
15
  count,
13
16
  interactive,
14
- animate,
17
+ motion,
15
18
  activeIndex: controlled,
16
19
  defaultActiveIndex = null,
17
20
  onActiveIndexChange,
@@ -20,13 +23,14 @@ function ChartFrame({
20
23
  describe,
21
24
  legend,
22
25
  table,
23
- children
26
+ children,
27
+ overlay
24
28
  }) {
25
- const { enabled } = useMotion();
29
+ const { expressive } = useMotionLevel(motion);
26
30
  const [viewRef, inView] = useArrival();
27
31
  const plotRef = useRef(null);
28
32
  const [size, setSize] = useState(null);
29
- const [shown, setShown] = useState(!animate);
33
+ const [shown, setShown] = useState(motion === "off" || motion === "subtle");
30
34
  const [uncontrolled, setUncontrolled] = useState(defaultActiveIndex);
31
35
  const [viaKeyboard, setViaKeyboard] = useState(false);
32
36
  const appeared = useRef(true);
@@ -54,7 +58,7 @@ function ChartFrame({
54
58
  }, []);
55
59
  useEffect(() => {
56
60
  if (shown) return;
57
- if (!animate || !enabled) {
61
+ if (!expressive) {
58
62
  setShown(true);
59
63
  return;
60
64
  }
@@ -67,7 +71,7 @@ function ChartFrame({
67
71
  cancelAnimationFrame(outer);
68
72
  cancelAnimationFrame(inner);
69
73
  };
70
- }, [shown, animate, enabled, size, inView]);
74
+ }, [shown, expressive, size, inView]);
71
75
  const local = (event) => {
72
76
  const r = event.currentTarget.getBoundingClientRect();
73
77
  return { x: event.clientX - r.left, y: event.clientY - r.top };
@@ -98,6 +102,7 @@ function ChartFrame({
98
102
  {
99
103
  ref: viewRef,
100
104
  "data-reveal": shown ? "in" : "out",
105
+ "data-motion": motionAttribute(motion),
101
106
  className: cn("m-0 flex h-60 w-full min-w-0 flex-col gap-3", className),
102
107
  children: [
103
108
  /* @__PURE__ */ jsxs(
@@ -129,6 +134,7 @@ function ChartFrame({
129
134
  children: children({ size, activeIndex: interactive ? active : null })
130
135
  }
131
136
  ) : null,
137
+ overlay && size && size.width > 0 && size.height > 0 ? /* @__PURE__ */ jsx("div", { "aria-hidden": true, className: "pointer-events-none absolute inset-0", children: overlay({ size, activeIndex: interactive ? active : null }) }) : null,
132
138
  placement ? /* @__PURE__ */ jsx(
133
139
  "div",
134
140
  {
@@ -159,5 +165,6 @@ function ChartFrame({
159
165
  );
160
166
  }
161
167
  export {
162
- ChartFrame
168
+ ChartFrame,
169
+ chartMotion
163
170
  };
@@ -0,0 +1,13 @@
1
+ // src/lib/chart/dim.ts
2
+ import { HOVER } from "./motion.js";
3
+ var SURFACE = "var(--sable-surface-bg, var(--background))";
4
+ function dimmed(color, keep) {
5
+ return `color-mix(in oklab, ${color} ${keep}%, ${SURFACE})`;
6
+ }
7
+ var FILL_HOVER = `fill ${HOVER}`;
8
+ var STROKE_HOVER = `stroke ${HOVER}`;
9
+ export {
10
+ FILL_HOVER,
11
+ STROKE_HOVER,
12
+ dimmed
13
+ };
@@ -14,11 +14,17 @@ function approxTextWidth(text) {
14
14
  }
15
15
  function centerValueText(value, locale) {
16
16
  if (typeof value === "string") return value;
17
- return Math.abs(value) < 1e4 ? new Intl.NumberFormat(locale, { maximumFractionDigits: 1 }).format(value) : new Intl.NumberFormat(locale, { notation: "compact", maximumFractionDigits: 1 }).format(value);
17
+ return new Intl.NumberFormat(locale, centerValueFormat(value)).format(value);
18
18
  }
19
+ function centerValueFormat(value) {
20
+ return Math.abs(value) < 1e4 ? FULL_CENTER : COMPACT_CENTER;
21
+ }
22
+ var FULL_CENTER = { maximumFractionDigits: 1 };
23
+ var COMPACT_CENTER = { notation: "compact", maximumFractionDigits: 1 };
19
24
  export {
20
25
  approxTextWidth,
21
26
  categoryText,
27
+ centerValueFormat,
22
28
  centerValueText,
23
29
  chartFormatters
24
30
  };
@@ -1,14 +1,15 @@
1
1
  // src/lib/chart/motion.ts
2
- var DRAW_DURATION = "calc(var(--motion-duration-slower) * 1.8)";
3
- var RISE = "var(--motion-duration-slower) var(--motion-ease-emphasized)";
4
- var MORPH = "var(--motion-duration-slow) var(--motion-ease-standard)";
2
+ var expressive = (token, factor = 1) => `calc(var(${token}) * ${factor} * var(--motion-expressive, 1))`;
3
+ var DRAW_DURATION = expressive("--motion-duration-slower", 1.8);
4
+ var RISE = `${expressive("--motion-duration-slower")} var(--motion-ease-emphasized)`;
5
+ var MORPH = `${expressive("--motion-duration-slow")} var(--motion-ease-standard)`;
5
6
  var HOVER = "var(--motion-duration-fast) var(--motion-ease-standard)";
6
7
  var GLIDE = `transform ${HOVER}, opacity ${HOVER}, cx ${HOVER}, cy ${HOVER}`;
7
8
  function barTransition(delay = "0ms") {
8
9
  return `scale ${RISE} ${delay}, opacity ${HOVER}, d ${MORPH}`;
9
10
  }
10
11
  function stagger(index, stepMs = 35) {
11
- return `calc(${Math.min(index, 24) * stepMs}ms * var(--motion-scale, 1))`;
12
+ return `calc(${Math.min(index, 24) * stepMs}ms * var(--motion-scale, 1) * var(--motion-expressive, 1))`;
12
13
  }
13
14
  function drawTransition(delay = "0ms") {
14
15
  return `stroke-dashoffset ${DRAW_DURATION} var(--motion-ease-decelerate) ${delay}, d ${MORPH}, opacity ${HOVER}`;
@@ -25,6 +26,7 @@ function pathStyle(d) {
25
26
  export {
26
27
  DRAW_DURATION,
27
28
  GLIDE,
29
+ HOVER,
28
30
  barTransition,
29
31
  drawTransition,
30
32
  morphTransition,
@@ -3,17 +3,26 @@
3
3
  // src/lib/highlight-glide.tsx
4
4
  import { useLayoutEffect, useRef } from "react";
5
5
  import { jsx } from "react/jsx-runtime";
6
- function HighlightGlide({ className = "rounded-popup-item bg-accent" }) {
6
+ var expressive = (token) => `calc(var(${token}) * var(--motion-expressive, 1))`;
7
+ function HighlightGlide({
8
+ className = "rounded-popup-item bg-accent",
9
+ follow = "data-highlighted",
10
+ timing = "highlight"
11
+ }) {
7
12
  const ref = useRef(null);
8
13
  useLayoutEffect(() => {
9
14
  const glide = ref.current;
10
15
  const host = glide?.parentElement;
11
16
  if (!glide || !host) return;
12
17
  host.setAttribute("data-glide", "");
18
+ const selection = timing === "selection";
13
19
  let shown = false;
20
+ let placed = false;
21
+ let lastRow = null;
22
+ let lastTarget = "";
14
23
  let frame = 0;
15
24
  const place = () => {
16
- const row = host.querySelector("[data-highlighted]:not([data-highlight-glide])");
25
+ const row = host.querySelector(`[${follow}]:not([data-highlight-glide])`);
17
26
  if (!row) {
18
27
  shown = false;
19
28
  glide.style.opacity = "0";
@@ -21,20 +30,59 @@ function HighlightGlide({ className = "rounded-popup-item bg-accent" }) {
21
30
  }
22
31
  const hostBox = host.getBoundingClientRect();
23
32
  const rowBox = row.getBoundingClientRect();
24
- const scale = host.offsetWidth > 0 ? hostBox.width / host.offsetWidth || 1 : 1;
33
+ const layoutWidth = selection ? Number.parseFloat(getComputedStyle(host).width) || host.offsetWidth : host.offsetWidth;
34
+ const scale = layoutWidth > 0 ? hostBox.width / layoutWidth || 1 : 1;
25
35
  const x = (rowBox.left - hostBox.left) / scale + host.scrollLeft - host.clientLeft;
26
36
  const y = (rowBox.top - hostBox.top) / scale + host.scrollTop - host.clientTop;
27
37
  const geometry = (s) => {
28
38
  s.width = `${rowBox.width / scale}px`;
29
39
  s.height = `${rowBox.height / scale}px`;
30
- s.transform = `translate(${x}px, ${y}px)`;
40
+ if (selection) s.translate = `${x}px ${y}px`;
41
+ else s.transform = `translate(${x}px, ${y}px)`;
31
42
  };
43
+ if (selection) {
44
+ const target = `${x},${y},${rowBox.width / scale},${rowBox.height / scale}`;
45
+ const sameRow = placed && shown && row === lastRow;
46
+ lastRow = row;
47
+ const moved = target !== lastTarget;
48
+ lastTarget = target;
49
+ if (sameRow) {
50
+ if (moved) {
51
+ glide.style.transitionProperty = "none";
52
+ geometry(glide.style);
53
+ void glide.offsetWidth;
54
+ glide.style.transitionProperty = "";
55
+ }
56
+ return;
57
+ }
58
+ if (!placed) {
59
+ glide.style.transitionProperty = "none";
60
+ geometry(glide.style);
61
+ glide.style.opacity = "1";
62
+ void glide.offsetWidth;
63
+ glide.style.transitionProperty = "";
64
+ placed = true;
65
+ shown = true;
66
+ return;
67
+ }
68
+ if (!shown) {
69
+ glide.style.transitionProperty = "opacity";
70
+ geometry(glide.style);
71
+ void glide.offsetWidth;
72
+ glide.style.transitionProperty = "";
73
+ shown = true;
74
+ } else {
75
+ geometry(glide.style);
76
+ }
77
+ glide.style.opacity = "1";
78
+ return;
79
+ }
32
80
  if (!shown) {
33
81
  glide.style.transitionProperty = "opacity";
34
82
  geometry(glide.style);
35
83
  cancelAnimationFrame(frame);
36
84
  frame = requestAnimationFrame(() => {
37
- glide.style.transitionProperty = "transform, width, height, opacity";
85
+ glide.style.transitionProperty = "opacity, transform, width, height";
38
86
  });
39
87
  shown = true;
40
88
  } else {
@@ -44,7 +92,7 @@ function HighlightGlide({ className = "rounded-popup-item bg-accent" }) {
44
92
  };
45
93
  place();
46
94
  const mutations = new MutationObserver(place);
47
- mutations.observe(host, { attributes: true, attributeFilter: ["data-highlighted"], subtree: true, childList: true });
95
+ mutations.observe(host, { attributes: true, attributeFilter: [follow], subtree: true, childList: true });
48
96
  const resize = new ResizeObserver(place);
49
97
  resize.observe(host);
50
98
  host.addEventListener("scroll", place, true);
@@ -59,7 +107,7 @@ function HighlightGlide({ className = "rounded-popup-item bg-accent" }) {
59
107
  host.removeEventListener("animationend", place);
60
108
  host.removeAttribute("data-glide");
61
109
  };
62
- }, []);
110
+ }, [follow, timing]);
63
111
  return /* @__PURE__ */ jsx(
64
112
  "span",
65
113
  {
@@ -72,9 +120,19 @@ function HighlightGlide({ className = "rounded-popup-item bg-accent" }) {
72
120
  // getBoundingClientRect, which is physical in RTL too.
73
121
  left: 0,
74
122
  opacity: 0,
75
- transitionProperty: "opacity",
76
- transitionDuration: "var(--motion-duration-instant)",
77
- transitionTimingFunction: "var(--motion-ease-standard)"
123
+ ...timing === "highlight" && {
124
+ transitionProperty: "opacity",
125
+ // One duration per property in "opacity, transform, width, height"
126
+ // order. The fade that shows or hides the highlight is essential;
127
+ // the travel is expressive.
128
+ transitionDuration: [
129
+ "var(--motion-duration-instant)",
130
+ expressive("--motion-duration-instant"),
131
+ expressive("--motion-duration-instant"),
132
+ expressive("--motion-duration-instant")
133
+ ].join(", "),
134
+ transitionTimingFunction: "var(--motion-ease-standard)"
135
+ }
78
136
  }
79
137
  }
80
138
  );
@@ -25,6 +25,7 @@ var COMPONENT_TAXONOMY = {
25
25
  what it is. Copying is `useCopy` on a Button, not a component. */
26
26
  Button: ["Actions", "either"],
27
27
  ButtonGroup: ["Actions", "either"],
28
+ ConfirmButton: ["Actions", "either"],
28
29
  Menubar: ["Actions", "either"],
29
30
  OverflowMenu: ["Actions", "either"],
30
31
  Toolbar: ["Actions", "either"],
@@ -136,6 +137,7 @@ var COMPONENT_TAXONOMY = {
136
137
  Entity: ["Data display", "either"],
137
138
  GlidingHighlight: ["Data display", "either"],
138
139
  Meter: ["Data display", "either"],
140
+ NumberTicker: ["Data display", "either"],
139
141
  RelativeTime: ["Data display", "either"],
140
142
  SegmentedBar: ["Data display", "either"],
141
143
  Table: ["Data display", "either"],
@@ -184,6 +186,7 @@ var COMPONENT_TAXONOMY = {
184
186
  MessageScroller: ["Chat & AI", "either"],
185
187
  StreamingText: ["Chat & AI", "either"],
186
188
  ThinkingOrb: ["Chat & AI", "either"],
189
+ ToolCall: ["Chat & AI", "either"],
187
190
  /* ── Media & mockups ──────────────────────────────────────────────────────
188
191
  Presenting images and video: players, galleries, scrolling strips, and the
189
192
  device frames a product image sits in. `Video` was filed as a mockup; it is
@@ -17,6 +17,9 @@ function resolveEnabled(policy, prefersReducedMotion) {
17
17
  if (policy === "full") return true;
18
18
  return !prefersReducedMotion;
19
19
  }
20
+ function resolveExpressive(policy, prefersReducedMotion) {
21
+ return policy !== "subtle" && resolveEnabled(policy, prefersReducedMotion);
22
+ }
20
23
  function MotionProvider({
21
24
  children,
22
25
  defaultPolicy = "system",
@@ -51,12 +54,47 @@ function MotionProvider({
51
54
  policy,
52
55
  setPolicy,
53
56
  prefersReducedMotion,
54
- enabled: resolveEnabled(policy, prefersReducedMotion)
57
+ enabled: resolveEnabled(policy, prefersReducedMotion),
58
+ expressive: resolveExpressive(policy, prefersReducedMotion)
55
59
  }),
56
60
  [policy, setPolicy, prefersReducedMotion]
57
61
  );
58
62
  return /* @__PURE__ */ jsx(MotionContext.Provider, { value, children });
59
63
  }
64
+ var LEVEL_RANK = { off: 0, subtle: 1, full: 2 };
65
+ function lowerLevel(a, b) {
66
+ return LEVEL_RANK[a] <= LEVEL_RANK[b] ? a : b;
67
+ }
68
+ function contextLevel(ctx) {
69
+ if (!ctx.enabled) return "off";
70
+ return ctx.expressive ? "full" : "subtle";
71
+ }
72
+ function motionAttribute(level) {
73
+ return level === void 0 || level === "full" ? void 0 : level;
74
+ }
75
+ function useMotionLevel(level) {
76
+ const ctx = useMotion();
77
+ const resolved = lowerLevel(contextLevel(ctx), level ?? "full");
78
+ return {
79
+ level: resolved,
80
+ enabled: resolved !== "off",
81
+ expressive: resolved === "full",
82
+ attribute: motionAttribute(level)
83
+ };
84
+ }
85
+ function MotionScope({ children, level }) {
86
+ const parent = useMotion();
87
+ const resolved = lowerLevel(contextLevel(parent), level);
88
+ const value = useMemo(
89
+ () => ({
90
+ ...parent,
91
+ enabled: resolved !== "off",
92
+ expressive: resolved === "full"
93
+ }),
94
+ [parent, resolved]
95
+ );
96
+ return /* @__PURE__ */ jsx(MotionContext.Provider, { value, children: /* @__PURE__ */ jsx("div", { "data-motion": level, style: { display: "contents" }, children }) });
97
+ }
60
98
  function useMotion() {
61
99
  const ctx = useContext(MotionContext);
62
100
  const prefersReducedMotion = useReducedMotion();
@@ -66,10 +104,14 @@ function useMotion() {
66
104
  setPolicy: () => {
67
105
  },
68
106
  prefersReducedMotion,
69
- enabled: resolveEnabled("system", prefersReducedMotion)
107
+ enabled: resolveEnabled("system", prefersReducedMotion),
108
+ expressive: resolveExpressive("system", prefersReducedMotion)
70
109
  };
71
110
  }
72
111
  export {
73
112
  MotionProvider,
74
- useMotion
113
+ MotionScope,
114
+ motionAttribute,
115
+ useMotion,
116
+ useMotionLevel
75
117
  };
@@ -18,7 +18,7 @@ function Reveal({
18
18
  ...props
19
19
  }) {
20
20
  const Comp = as ?? "div";
21
- const { enabled } = useMotion();
21
+ const { expressive: enabled } = useMotion();
22
22
  const { ref, inView } = useInView({ once, amount, rootMargin });
23
23
  const shown = inView && when || !enabled;
24
24
  return /* @__PURE__ */ jsx(
@@ -18,7 +18,7 @@ function TextReveal({
18
18
  ...props
19
19
  }) {
20
20
  const Comp = as ?? "span";
21
- const { enabled } = useMotion();
21
+ const { expressive: enabled } = useMotion();
22
22
  const { ref, inView } = useInView({ once, amount, rootMargin });
23
23
  const shown = inView && when || !enabled;
24
24
  const units = split === "char" ? typeof Intl.Segmenter === "function" ? Array.from(new Intl.Segmenter(void 0, { granularity: "grapheme" }).segment(text), ({ segment }) => segment) : Array.from(text) : text.split(/(\s+)/).filter(Boolean);
@@ -1,6 +1,14 @@
1
1
  // src/motion/index.ts
2
- import { MotionProvider, useMotion } from "./MotionProvider.js";
2
+ import {
3
+ MotionProvider,
4
+ MotionScope,
5
+ motionAttribute,
6
+ useMotion,
7
+ useMotionLevel
8
+ } from "./MotionProvider.js";
3
9
  import { useReducedMotion } from "./useReducedMotion.js";
10
+ import { useShake } from "./useShake.js";
11
+ import { useAutoSize } from "./useAutoSize.js";
4
12
  import { useInView } from "./useInView.js";
5
13
  import { Reveal } from "./Reveal.js";
6
14
  import { TextReveal } from "./TextReveal.js";
@@ -12,13 +20,18 @@ import {
12
20
  } from "./viewTransition.js";
13
21
  export {
14
22
  MotionProvider,
23
+ MotionScope,
15
24
  Presence,
16
25
  Reveal,
17
26
  TextReveal,
18
27
  ViewTransitionName,
28
+ motionAttribute,
19
29
  startViewTransition,
20
30
  supportsViewTransitions,
31
+ useAutoSize,
21
32
  useInView,
22
33
  useMotion,
23
- useReducedMotion
34
+ useMotionLevel,
35
+ useReducedMotion,
36
+ useShake
24
37
  };
@@ -0,0 +1,42 @@
1
+ "use client";
2
+
3
+ // src/motion/useAutoSize.ts
4
+ import { useLayoutEffect, useRef } from "react";
5
+ function useAutoSize({ axis = "width", enabled = true } = {}) {
6
+ const outerRef = useRef(null);
7
+ const innerRef = useRef(null);
8
+ useLayoutEffect(() => {
9
+ const outer = outerRef.current;
10
+ const inner = innerRef.current;
11
+ if (!enabled || !outer || !inner || typeof ResizeObserver === "undefined") return;
12
+ let first = true;
13
+ const apply = () => {
14
+ const style = getComputedStyle(outer);
15
+ const px = (value) => Number.parseFloat(value) || 0;
16
+ const extraX = px(style.paddingLeft) + px(style.paddingRight) + px(style.borderLeftWidth) + px(style.borderRightWidth);
17
+ const extraY = px(style.paddingTop) + px(style.paddingBottom) + px(style.borderTopWidth) + px(style.borderBottomWidth);
18
+ const width = inner.offsetWidth || inner.getBoundingClientRect().width;
19
+ const height = inner.offsetHeight || inner.getBoundingClientRect().height;
20
+ if (first) outer.style.transition = "none";
21
+ if (axis !== "height") outer.style.width = `${width + extraX}px`;
22
+ if (axis !== "width") outer.style.height = `${height + extraY}px`;
23
+ if (first) {
24
+ void outer.offsetWidth;
25
+ outer.style.transition = "";
26
+ first = false;
27
+ }
28
+ };
29
+ apply();
30
+ const observer = new ResizeObserver(apply);
31
+ observer.observe(inner);
32
+ return () => {
33
+ observer.disconnect();
34
+ outer.style.width = "";
35
+ outer.style.height = "";
36
+ };
37
+ }, [axis, enabled]);
38
+ return { outerRef, innerRef };
39
+ }
40
+ export {
41
+ useAutoSize
42
+ };
@@ -0,0 +1,25 @@
1
+ "use client";
2
+
3
+ // src/motion/useShake.ts
4
+ import { useCallback, useEffect, useRef, useState } from "react";
5
+ function useShake() {
6
+ const [shaking, setShaking] = useState(false);
7
+ const frame = useRef(0);
8
+ useEffect(() => () => cancelAnimationFrame(frame.current), []);
9
+ const shake = useCallback(() => {
10
+ cancelAnimationFrame(frame.current);
11
+ setShaking(false);
12
+ frame.current = requestAnimationFrame(() => {
13
+ frame.current = requestAnimationFrame(() => setShaking(true));
14
+ });
15
+ }, []);
16
+ const onAnimationEnd = useCallback((event) => {
17
+ if (event.target === event.currentTarget && event.animationName === "sable-shake") {
18
+ setShaking(false);
19
+ }
20
+ }, []);
21
+ return { shake, shakeProps: { "data-shake": shaking ? "" : void 0, onAnimationEnd } };
22
+ }
23
+ export {
24
+ useShake
25
+ };