@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
@@ -34,6 +34,10 @@ var otpFieldMeta = {
34
34
  guidance: "dont",
35
35
  description: 'Do not use OtpField for a password, an arbitrary-length PIN, or anything the user types from memory. Slots promise a fixed length; use Input with `type="password"` and `autocomplete="current-password"` instead.'
36
36
  },
37
+ {
38
+ guidance: "do",
39
+ description: "Clear the error as soon as the user edits the code (`onValueChange` \u2192 reset the error), and set it again when the next code is refused. Each refusal is then a fresh change to invalid, so the field shakes for every wrong code, not only the first."
40
+ },
37
41
  {
38
42
  guidance: "dont",
39
43
  description: "Do not intercept paste or clear the field on a wrong code before the user has seen the error. Blocking paste breaks SMS autofill and password managers, and wiping the slots erases the evidence of which digit was wrong."
@@ -123,7 +127,13 @@ var otpFieldMeta = {
123
127
  {
124
128
  name: "aria-invalid",
125
129
  type: "boolean",
126
- description: "Marks every slot invalid (destructive border). `<Field error>` sets it for you."
130
+ description: "Marks every slot invalid (destructive border and digits). Turning it on after mount shakes the field once. `<Field error>` sets it for you."
131
+ },
132
+ {
133
+ name: "motion",
134
+ type: '"full" | "subtle" | "off"',
135
+ default: '"full"',
136
+ description: "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. The shake is the flourish; the destructive colour stays at every level."
127
137
  },
128
138
  {
129
139
  name: "disabled / readOnly / required / name / id / form",
@@ -160,6 +170,12 @@ var otpFieldMeta = {
160
170
  part: "Slot",
161
171
  why: "A filled slot strengthens its border, so a pasted code visibly lands in every slot at once. Easing it reads as the code arriving rather than the field flickering."
162
172
  },
173
+ {
174
+ slot: "transition",
175
+ recipe: "motion-shake",
176
+ part: "Root",
177
+ why: "A refused code shakes the field once as it turns invalid \u2014 the short horizontal no that makes a second wrong code feel noticed rather than ignored. It never carries the refusal alone: the digits and borders turn destructive and the Field says why. It plays on the change to invalid, not on mount, and is expressive, so `subtle` keeps only the colour."
178
+ },
163
179
  {
164
180
  slot: "activity",
165
181
  recipe: "none",
@@ -171,7 +187,8 @@ var otpFieldMeta = {
171
187
  'The root is a role=group named by the field label; the first slot shares the label through `id`/`htmlFor`, and later slots are announced by position ("Character 2 of 6").',
172
188
  "Only one slot is in the tab order. Arrow keys, Home/End and Backspace move between slots; Tab leaves the whole field.",
173
189
  'The first slot carries autocomplete="one-time-code" and the numeric virtual keyboard, so iOS and Android offer the SMS code above the keyboard.',
174
- "Slots render at 20px (`text-mono-lg`), above the 16px floor that stops iOS Safari zooming on focus."
190
+ "Slots render at 20px (`text-mono-lg`), above the 16px floor that stops iOS Safari zooming on focus.",
191
+ "An invalid code is never signalled by motion alone: every slot carries aria-invalid, the digits and borders turn destructive, and the Field's error message is wired through aria-describedby. The shake resolves instantly under reduced motion and `subtle`."
175
192
  ],
176
193
  examples: [
177
194
  {
@@ -186,6 +203,10 @@ var otpFieldMeta = {
186
203
  label: "With an error",
187
204
  code: '<Field label="Verification code" error="That code has expired. Request a new code and try again."><OtpField length={6} groupSize={3} defaultValue="482913" /></Field>'
188
205
  },
206
+ {
207
+ label: "Verify on completion, shake on a wrong code",
208
+ code: 'const [code, setCode] = useState("")\nconst [wrong, setWrong] = useState(false)\n<Field label="Verification code" error={wrong ? "That code does not match. Check the message and try again." : undefined}><OtpField length={6} groupSize={3} value={code} onValueChange={(next) => { setCode(next); setWrong(false) }} onValueComplete={(value) => setWrong(value !== expected)} /></Field>'
209
+ },
189
210
  {
190
211
  label: "Alphanumeric",
191
212
  code: '<Field label="Pairing code"><OtpField length={8} groupSize={4} validationType="alphanumeric" /></Field>'
@@ -83,6 +83,32 @@ var panelGroupMeta = {
83
83
  description: "Controlled width for a Panel, clamped and optionally persisted to localStorage (only when `storageKey` is passed). Read through useSyncExternalStore, so there is no server/client hydration mismatch and no setState in an effect; do not hand-roll a localStorage read during render or a write from an effect. Without `storageKey` the width is kept in memory per hook instance. The third element is the `{ min, max }` it clamps to: spread it onto the paired PanelResizer so the separator's aria-valuemin/max and Home/End match the real clamp."
84
84
  }
85
85
  ],
86
+ motion: [
87
+ {
88
+ slot: "arrival",
89
+ recipe: "none",
90
+ part: "Root",
91
+ why: "Panels are the app's frame, present from first paint. A dock that slid in on load would make the workspace look like it is still assembling."
92
+ },
93
+ {
94
+ slot: "response",
95
+ recipe: "motion-interactive",
96
+ part: "PanelResizer",
97
+ why: "The hairline widens into a visible bar on hover and turns primary on keyboard focus, eased, so a 1px edge says it can be grabbed before the pointer commits."
98
+ },
99
+ {
100
+ slot: "transition",
101
+ recipe: "none",
102
+ part: "Panel",
103
+ why: "Width tracks the pointer one-to-one while dragging, and keyboard steps land at once. An eased width would trail the cursor, and resizing is a layout change: easing it would reflow the canvas beside the panel on every frame."
104
+ },
105
+ {
106
+ slot: "activity",
107
+ recipe: "none",
108
+ part: "Root",
109
+ why: "Layout chrome has no working state."
110
+ }
111
+ ],
86
112
  a11y: [
87
113
  "PanelResizer is a role=separator with aria-orientation, aria-valuenow/min/max, and is focusable \u2014 a pointer-only resizer is a keyboard trap.",
88
114
  "Arrow keys resize by `step`; Home and End jump to the minimum and maximum.",
@@ -1,6 +1,7 @@
1
1
  "use client";
2
2
 
3
3
  // src/components/panel-section/panel-section.tsx
4
+ import { useState } from "react";
4
5
  import { Icon } from "../../icon/index.js";
5
6
  import { cn } from "../../lib/cn.js";
6
7
  import { beginScrub } from "../../lib/scrub-gesture.js";
@@ -110,16 +111,42 @@ function PropertyPair({ labels, className, children, ...props }) {
110
111
  ] });
111
112
  }
112
113
  PropertyPair.displayName = "PropertyPair";
113
- function PropertyGroup({ label, summary, className, children, ...props }) {
114
- return /* @__PURE__ */ jsxs(Collapsible, { className: cn("border-t border-hairline", className), ...props, children: [
115
- /* @__PURE__ */ jsxs(CollapsibleTrigger, { className: "group/property-group flex h-(--control-sm) w-full items-center gap-2 rounded-xs text-start motion-interactive focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", children: [
116
- /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate text-ui-md text-foreground", children: label }),
117
- summary != null ? /* @__PURE__ */ jsx("span", { className: "flex min-w-0 shrink items-center gap-2 truncate text-ui-md text-muted-foreground", children: summary }) : null,
118
- /* @__PURE__ */ jsx(Icon, { name: "plus", className: "shrink-0 text-xs text-muted-foreground group-data-[panel-open]/property-group:hidden" }),
119
- /* @__PURE__ */ jsx(Icon, { name: "minus", className: "hidden shrink-0 text-xs text-muted-foreground group-data-[panel-open]/property-group:block" })
120
- ] }),
121
- /* @__PURE__ */ jsx(CollapsiblePanel, { children: /* @__PURE__ */ jsx("div", { className: "flex flex-col gap-1.5 pt-0.5 pb-2", children }) })
122
- ] });
114
+ function PropertyGroup({
115
+ label,
116
+ summary,
117
+ className,
118
+ children,
119
+ open: openProp,
120
+ defaultOpen = false,
121
+ onOpenChange,
122
+ ...props
123
+ }) {
124
+ const [openState, setOpenState] = useState(defaultOpen);
125
+ const open = openProp ?? openState;
126
+ return /* @__PURE__ */ jsxs(
127
+ Collapsible,
128
+ {
129
+ className: cn("border-t border-hairline", className),
130
+ open: openProp,
131
+ defaultOpen,
132
+ onOpenChange: (next, details) => {
133
+ setOpenState(next);
134
+ onOpenChange?.(next, details);
135
+ },
136
+ ...props,
137
+ children: [
138
+ /* @__PURE__ */ jsxs(CollapsibleTrigger, { className: "flex h-(--control-sm) w-full items-center gap-2 rounded-xs text-start motion-interactive focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", children: [
139
+ /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate text-ui-md text-foreground", children: label }),
140
+ summary != null ? /* @__PURE__ */ jsx("span", { className: "flex min-w-0 shrink items-center gap-2 truncate text-ui-md text-muted-foreground", children: summary }) : null,
141
+ /* @__PURE__ */ jsxs("span", { "aria-hidden": "true", className: "motion-icon-swap shrink-0 text-xs text-muted-foreground", children: [
142
+ /* @__PURE__ */ jsx("span", { "data-active": !open, className: "inline-flex", children: /* @__PURE__ */ jsx(Icon, { name: "plus" }) }),
143
+ /* @__PURE__ */ jsx("span", { "data-active": open, className: "inline-flex", children: /* @__PURE__ */ jsx(Icon, { name: "minus" }) })
144
+ ] })
145
+ ] }),
146
+ /* @__PURE__ */ jsx(CollapsiblePanel, { children: /* @__PURE__ */ jsx("div", { className: "flex flex-col gap-1.5 pt-0.5 pb-2", children }) })
147
+ ]
148
+ }
149
+ );
123
150
  }
124
151
  PropertyGroup.displayName = "PropertyGroup";
125
152
  export {
@@ -101,6 +101,38 @@ var panelSectionMeta = {
101
101
  description: "A collapsible set of properties inside a section, at the panel's scale (an Accordion is page furniture). The row shows the label, then the summary (the group's value, readable while closed: a word, a few swatches, or both), then a + that turns to \u2212 when open. Groups open independently and stack with a hairline between them. Built on Collapsible."
102
102
  }
103
103
  ],
104
+ motion: [
105
+ {
106
+ slot: "arrival",
107
+ recipe: "none",
108
+ part: "Root",
109
+ why: "Sections are part of the inspector they sit in and arrive with it. Animating each in would make a panel switch look like a page load."
110
+ },
111
+ {
112
+ slot: "response",
113
+ recipe: "motion-interactive",
114
+ part: "PropertyGroup trigger",
115
+ why: "The fold row eases its focus ring, so arrowing down a dense inspector shows where the keyboard is without the ring flickering from row to row."
116
+ },
117
+ {
118
+ slot: "transition",
119
+ recipe: "motion-disclosure",
120
+ part: "PropertyGroup panel",
121
+ why: "The group opens by easing its height, so the rows below slide down to make room instead of jumping. In an inspector, the next section moving is otherwise how you lose your place."
122
+ },
123
+ {
124
+ slot: "transition",
125
+ recipe: "motion-icon-swap",
126
+ part: "PropertyGroup indicator",
127
+ why: "The + collapses into \u2212 as the group opens, both glyphs stacked as one mark, so the indicator reads as this row changing state rather than a different icon landing."
128
+ },
129
+ {
130
+ slot: "activity",
131
+ recipe: "none",
132
+ part: "Root",
133
+ why: "The inspector edits values in place; nothing in it loads."
134
+ }
135
+ ],
104
136
  a11y: [
105
137
  "PropertyGroup's row is a real button with aria-expanded (Collapsible); its summary is part of the button's name, so a closed group still announces its value.",
106
138
  "PropertyRow renders a real <label>; pass htmlFor with the control's id so clicking the label focuses it.",
@@ -2,10 +2,12 @@
2
2
 
3
3
  // src/components/pie-chart/pie-chart.tsx
4
4
  import { useId } from "react";
5
- import { ChartFrame } from "../../lib/chart/chart-frame.js";
5
+ import { ChartCenter } from "../../lib/chart/chart-center.js";
6
+ import { ChartFrame, chartMotion } from "../../lib/chart/chart-frame.js";
6
7
  import { ChartLegend } from "../../lib/chart/chart-legend.js";
7
8
  import { ChartTooltipContent } from "../../lib/chart/chart-tooltip.js";
8
- import { categoryText, centerValueText, chartFormatters } from "../../lib/chart/format.js";
9
+ import { FILL_HOVER, dimmed } from "../../lib/chart/dim.js";
10
+ import { categoryText, chartFormatters } from "../../lib/chart/format.js";
9
11
  import { arcPath, polar } from "../../lib/chart/geometry.js";
10
12
  import { DRAW_DURATION, morphTransition, pathStyle } from "../../lib/chart/motion.js";
11
13
  import {
@@ -30,7 +32,8 @@ function PieChart({
30
32
  tooltip = true,
31
33
  valueFormatter,
32
34
  locale,
33
- animate = true,
35
+ motion,
36
+ animate,
34
37
  activeIndex,
35
38
  defaultActiveIndex,
36
39
  onActiveIndexChange
@@ -52,6 +55,7 @@ function PieChart({
52
55
  });
53
56
  });
54
57
  const tooltipOptions = typeof tooltip === "object" ? tooltip : {};
58
+ const level = chartMotion(motion, animate);
55
59
  const ratio = donut === true ? 0.6 : typeof donut === "number" ? Math.min(0.9, Math.max(0, donut)) : 0;
56
60
  const geometry = (size) => {
57
61
  const margin = labels === "outside" ? 30 : 8;
@@ -105,7 +109,7 @@ function PieChart({
105
109
  className: cn("h-64", className),
106
110
  count: data.length,
107
111
  interactive: tooltip !== false,
108
- animate,
112
+ motion: level,
109
113
  activeIndex,
110
114
  defaultActiveIndex,
111
115
  onActiveIndexChange,
@@ -117,6 +121,10 @@ function PieChart({
117
121
  rows: data.map((_, i) => [styles[i].label, ...keys.map((_2, r) => fmt.full(rings[r][i].v))])
118
122
  },
119
123
  legend: legend ?? labels === "none" ? /* @__PURE__ */ jsx(ChartLegend, { items: names.map((n, i) => ({ key: n, label: styles[i].label, color: styles[i].color, icon: styles[i].icon })) }) : null,
124
+ overlay: centerLabel && ratio > 0 ? ({ size }) => {
125
+ const g = geometry(size);
126
+ return /* @__PURE__ */ jsx(ChartCenter, { center: centerLabel, x: g.cx, y: g.cy, locale, motion: level });
127
+ } : void 0,
120
128
  children: ({ size, activeIndex: active }) => {
121
129
  const g = geometry(size);
122
130
  const maskId = `${uid}-sweep`;
@@ -148,8 +156,13 @@ function PieChart({
148
156
  d,
149
157
  strokeWidth: separator ? 2 : 0,
150
158
  strokeLinejoin: "round",
151
- className: cn(separator && "stroke-(--sable-surface-bg,var(--background))", active != null && active !== i && "opacity-60"),
152
- style: { ...pathStyle(d), fill: styles[i].color, transition: morphTransition() }
159
+ className: cn(separator && "stroke-(--sable-surface-bg,var(--background))"),
160
+ style: {
161
+ ...pathStyle(d),
162
+ // Stepped back toward the surface rather than faded (see lib/chart/dim).
163
+ fill: active != null && active !== i ? dimmed(styles[i].color, 60) : styles[i].color,
164
+ transition: `${morphTransition()}, ${FILL_HOVER}`
165
+ }
153
166
  },
154
167
  `${r}-${names[i]}`
155
168
  );
@@ -175,11 +188,7 @@ function PieChart({
175
188
  },
176
189
  `l-${names[i]}`
177
190
  );
178
- }) : null,
179
- centerLabel && g.inner > 0 ? /* @__PURE__ */ jsxs("text", { x: g.cx, y: g.cy, textAnchor: "middle", className: "[[data-reveal=out]_&]:opacity-0", style: { transition: morphTransition() }, children: [
180
- /* @__PURE__ */ jsx("tspan", { x: g.cx, dy: "0.35em", className: "fill-foreground text-heading-2 tabular-nums", children: centerValueText(centerLabel.value, locale) }),
181
- centerLabel.label ? /* @__PURE__ */ jsx("tspan", { x: g.cx, dy: "1.9em", className: "fill-muted-foreground text-caption", children: centerLabel.label }) : null
182
- ] }) : null
191
+ }) : null
183
192
  ] });
184
193
  }
185
194
  }
@@ -127,11 +127,18 @@ var pieChartMeta = {
127
127
  default: "runtime locale",
128
128
  description: "BCP 47 locale for default number and date formatting."
129
129
  },
130
+ {
131
+ name: "motion",
132
+ type: '"full" | "subtle" | "off"',
133
+ default: '"full"',
134
+ description: "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. The sweep, the slice morph and the rolling centre number are the flourish; the hover pull-out and dim are the feedback `subtle` keeps."
135
+ },
130
136
  {
131
137
  name: "animate",
132
138
  type: "boolean",
133
139
  default: "true",
134
- description: "Sweep the slices in clockwise when the chart scrolls into view."
140
+ description: 'Sweep the slices in clockwise when the chart scrolls into view. `animate={false}` is `motion="subtle"`.',
141
+ deprecated: 'motion="subtle" (what animate={false} now maps to: no arrival, hover kept) or motion="off"'
135
142
  },
136
143
  {
137
144
  name: "activeIndex",
@@ -168,19 +175,25 @@ var pieChartMeta = {
168
175
  slot: "arrival",
169
176
  recipe: "custom",
170
177
  part: "Slices",
171
- why: "Once on screen the slices are revealed by a mask that sweeps clockwise from 12 o'clock \u2014 the whole is read as it fills. On the motion tokens; reduced motion shows the finished chart."
178
+ why: 'Once on screen the slices are revealed by a mask that sweeps clockwise from 12 o\'clock \u2014 the whole is read as it fills. On the motion tokens; reduced motion shows the finished chart. The arrival is expressive: it plays at the full motion level only, so `motion="subtle"` (or a subtle app policy) shows the finished chart while hover feedback stays.'
172
179
  },
173
180
  {
174
181
  slot: "response",
175
182
  recipe: "none",
176
183
  part: "Active slice",
177
- why: "The hovered slice pulls outward and the rest dim at once; the geometry change itself eases (see transition)."
184
+ why: "The hovered slice pulls outward and the rest dim at once (mixed toward the surface, not faded); the geometry change itself eases (see transition). Essential: `subtle` keeps it."
185
+ },
186
+ {
187
+ slot: "transition",
188
+ recipe: "custom",
189
+ part: "Centre value",
190
+ why: "A numeric centre value rolls to its new total digit by digit (NumberTicker) when the data changes, so the headline is seen moving with the slices instead of being found different afterwards. Expressive: under `subtle` it updates in place."
178
191
  },
179
192
  {
180
193
  slot: "transition",
181
194
  recipe: "custom",
182
195
  part: "Slices",
183
- why: "Geometry eases from the old shape to the new one through a CSS `d` transition on the motion tokens (no recipe animates path geometry); browsers without `d` transitions update in place."
196
+ why: "Geometry eases from the old shape to the new one through a CSS `d` transition on the motion tokens (no recipe animates path geometry); browsers without `d` transitions update in place. Expressive, like the arrival: under `subtle` new data swaps in place."
184
197
  },
185
198
  {
186
199
  slot: "activity",
@@ -98,6 +98,38 @@ var pillMeta = {
98
98
  "muted-foreground",
99
99
  "foreground"
100
100
  ],
101
+ motion: [
102
+ {
103
+ slot: "arrival",
104
+ recipe: "none",
105
+ part: "Root",
106
+ why: "Pills arrive with the filter row or the content they annotate. A chip that animated in alone would read as a filter someone just added."
107
+ },
108
+ {
109
+ slot: "response",
110
+ recipe: "motion-interactive",
111
+ part: "Root",
112
+ why: "The hover wash eases, so sweeping the pointer along a filter row reads as one cursor moving rather than chips flashing in turn."
113
+ },
114
+ {
115
+ slot: "response",
116
+ recipe: "motion-press",
117
+ part: "Root",
118
+ why: "A pill often toggles a filter that takes a moment to reach the results; the settle on press confirms the click landed before they change."
119
+ },
120
+ {
121
+ slot: "transition",
122
+ recipe: "motion-interactive",
123
+ part: "Root",
124
+ why: "`selected` lifts the chip onto its selected surface on the same ease, so toggling reads as this chip changing state, not a different chip appearing in its place."
125
+ },
126
+ {
127
+ slot: "activity",
128
+ recipe: "none",
129
+ part: "Root",
130
+ why: "A chip is a choice, not a process. If applying it is slow, the results show it."
131
+ }
132
+ ],
101
133
  a11y: [
102
134
  'Renders a native <button type="button"> for focus and keyboard activation.',
103
135
  "Selected state is exposed via aria-pressed."
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
 
3
3
  // src/components/radar-chart/radar-chart.tsx
4
- import { ChartFrame } from "../../lib/chart/chart-frame.js";
4
+ import { ChartFrame, chartMotion } from "../../lib/chart/chart-frame.js";
5
5
  import { ChartLegend } from "../../lib/chart/chart-legend.js";
6
6
  import { ChartTooltipContent } from "../../lib/chart/chart-tooltip.js";
7
7
  import { approxTextWidth, categoryText, chartFormatters } from "../../lib/chart/format.js";
@@ -29,7 +29,8 @@ function RadarChart({
29
29
  tooltip = true,
30
30
  valueFormatter,
31
31
  locale,
32
- animate = true,
32
+ motion,
33
+ animate,
33
34
  activeIndex,
34
35
  defaultActiveIndex,
35
36
  onActiveIndexChange
@@ -80,7 +81,7 @@ function RadarChart({
80
81
  className: cn("h-72", className),
81
82
  count: n,
82
83
  interactive: tooltip !== false,
83
- animate,
84
+ motion: chartMotion(motion, animate),
84
85
  activeIndex,
85
86
  defaultActiveIndex,
86
87
  onActiveIndexChange,
@@ -121,11 +121,18 @@ var radarChartMeta = {
121
121
  default: "runtime locale",
122
122
  description: "BCP 47 locale for default number and date formatting."
123
123
  },
124
+ {
125
+ name: "motion",
126
+ type: '"full" | "subtle" | "off"',
127
+ default: '"full"',
128
+ description: "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. The polygons growing in and morphing are the flourish; the hover highlight is the feedback `subtle` keeps."
129
+ },
124
130
  {
125
131
  name: "animate",
126
132
  type: "boolean",
127
133
  default: "true",
128
- description: "Grow the polygons from the centre when the chart scrolls into view."
134
+ description: 'Grow the polygons from the centre when the chart scrolls into view. `animate={false}` is `motion="subtle"`.',
135
+ deprecated: 'motion="subtle" (what animate={false} now maps to: no arrival, hover kept) or motion="off"'
129
136
  },
130
137
  {
131
138
  name: "activeIndex",
@@ -162,7 +169,7 @@ var radarChartMeta = {
162
169
  slot: "arrival",
163
170
  recipe: "custom",
164
171
  part: "Polygon",
165
- why: "Once on screen each polygon grows from the centre and fades in, series staggered, on the emphasized easing. On the motion tokens; reduced motion shows the finished chart."
172
+ why: 'Once on screen each polygon grows from the centre and fades in, series staggered, on the emphasized easing. On the motion tokens; reduced motion shows the finished chart. The arrival is expressive: it plays at the full motion level only, so `motion="subtle"` (or a subtle app policy) shows the finished chart while hover feedback stays.'
166
173
  },
167
174
  {
168
175
  slot: "response",
@@ -174,7 +181,7 @@ var radarChartMeta = {
174
181
  slot: "transition",
175
182
  recipe: "custom",
176
183
  part: "Polygon",
177
- why: "Geometry eases from the old shape to the new one through a CSS `d` transition on the motion tokens (no recipe animates path geometry); browsers without `d` transitions update in place."
184
+ why: "Geometry eases from the old shape to the new one through a CSS `d` transition on the motion tokens (no recipe animates path geometry); browsers without `d` transitions update in place. Expressive, like the arrival: under `subtle` new data swaps in place."
178
185
  },
179
186
  {
180
187
  slot: "activity",
@@ -1,12 +1,14 @@
1
1
  "use client";
2
2
 
3
3
  // src/components/radial-chart/radial-chart.tsx
4
- import { ChartFrame } from "../../lib/chart/chart-frame.js";
4
+ import { ChartCenter } from "../../lib/chart/chart-center.js";
5
+ import { ChartFrame, chartMotion } from "../../lib/chart/chart-frame.js";
5
6
  import { ChartLegend } from "../../lib/chart/chart-legend.js";
6
7
  import { ChartTooltipContent } from "../../lib/chart/chart-tooltip.js";
7
- import { categoryText, centerValueText, chartFormatters } from "../../lib/chart/format.js";
8
+ import { STROKE_HOVER, dimmed } from "../../lib/chart/dim.js";
9
+ import { categoryText, chartFormatters } from "../../lib/chart/format.js";
8
10
  import { polar, strokeArcPath } from "../../lib/chart/geometry.js";
9
- import { drawTransition, morphTransition, pathStyle, stagger } from "../../lib/chart/motion.js";
11
+ import { drawTransition, pathStyle, stagger } from "../../lib/chart/motion.js";
10
12
  import {
11
13
  categoryStyle,
12
14
  numberValue
@@ -30,7 +32,8 @@ function RadialChart({
30
32
  tooltip = true,
31
33
  valueFormatter,
32
34
  locale,
33
- animate = true,
35
+ motion,
36
+ animate,
34
37
  activeIndex,
35
38
  defaultActiveIndex,
36
39
  onActiveIndexChange
@@ -45,6 +48,7 @@ function RadialChart({
45
48
  const start = -span / 2 + (sweep >= 360 ? span / 2 : 0);
46
49
  const tooltipOptions = typeof tooltip === "object" ? tooltip : {};
47
50
  const gauge = sweep < 360;
51
+ const level = chartMotion(motion, animate);
48
52
  const geometry = (size) => {
49
53
  const outer = gauge ? Math.min(size.width / 2, size.height * (sweep <= 180 ? 0.92 : 0.6)) - 8 : Math.min(size.width, size.height) / 2 - 8;
50
54
  const cx = size.width / 2;
@@ -102,7 +106,7 @@ function RadialChart({
102
106
  className: cn("h-64", className),
103
107
  count: data.length,
104
108
  interactive: tooltip !== false,
105
- animate,
109
+ motion: level,
106
110
  activeIndex,
107
111
  defaultActiveIndex,
108
112
  onActiveIndexChange,
@@ -114,6 +118,10 @@ function RadialChart({
114
118
  rows: data.map((_, i) => [styles[i].label, fmt.full(values[i])])
115
119
  },
116
120
  legend: legend ?? !labels ? /* @__PURE__ */ jsx(ChartLegend, { items: names.map((n, i) => ({ key: n, label: styles[i].label, color: styles[i].color, icon: styles[i].icon })) }) : null,
121
+ overlay: centerLabel ? ({ size }) => {
122
+ const g = geometry(size);
123
+ return gauge && sweep <= 180 ? /* @__PURE__ */ jsx(ChartCenter, { center: centerLabel, x: g.cx, y: g.cy - 6, placement: "gauge", locale, motion: level }) : /* @__PURE__ */ jsx(ChartCenter, { center: centerLabel, x: g.cx, y: g.cy, locale, motion: level });
124
+ } : void 0,
117
125
  children: ({ size, activeIndex: active }) => {
118
126
  const g = geometry(size);
119
127
  const ringCount = stacked ? 1 : data.length;
@@ -140,8 +148,14 @@ function RadialChart({
140
148
  fill: "none",
141
149
  strokeWidth: g.thickness,
142
150
  strokeLinecap: stacked ? "butt" : "round",
143
- className: cn("[stroke-dasharray:1] [stroke-dashoffset:0] [[data-reveal=out]_&]:[stroke-dashoffset:1]", active != null && active !== a.i && "opacity-60"),
144
- style: { ...pathStyle(d), stroke: styles[a.i].color, transition: drawTransition(stagger(a.i, 90)) }
151
+ className: "[stroke-dasharray:1] [stroke-dashoffset:0] [[data-reveal=out]_&]:[stroke-dashoffset:1]",
152
+ style: {
153
+ ...pathStyle(d),
154
+ // Stepped back toward the surface rather than faded: a faded ring
155
+ // lets its muted track show through and turns muddy (lib/chart/dim).
156
+ stroke: active != null && active !== a.i ? dimmed(styles[a.i].color, 60) : styles[a.i].color,
157
+ transition: `${drawTransition(stagger(a.i, 90))}, ${STROKE_HOVER}`
158
+ }
145
159
  },
146
160
  names[a.i]
147
161
  );
@@ -149,21 +163,7 @@ function RadialChart({
149
163
  labels && !stacked ? arcs.map((a) => {
150
164
  const [x, y] = polar(g.cx, g.cy, radiusOf(g, a.ring), start);
151
165
  return /* @__PURE__ */ jsx("text", { x: x - 8, y, dy: "0.32em", textAnchor: "end", className: "fill-muted-foreground text-caption", children: styles[a.i].label }, `l${a.i}`);
152
- }) : null,
153
- centerLabel ? /* @__PURE__ */ jsxs(
154
- "text",
155
- {
156
- x: g.cx,
157
- y: gauge && sweep <= 180 ? g.cy - 6 : g.cy,
158
- textAnchor: "middle",
159
- className: "[[data-reveal=out]_&]:opacity-0",
160
- style: { transition: morphTransition() },
161
- children: [
162
- /* @__PURE__ */ jsx("tspan", { x: g.cx, dy: gauge && sweep <= 180 ? centerLabel.label ? "-1.4em" : "0" : "0.35em", className: "fill-foreground text-heading-2 tabular-nums", children: centerValueText(centerLabel.value, locale) }),
163
- centerLabel.label ? /* @__PURE__ */ jsx("tspan", { x: g.cx, dy: gauge && sweep <= 180 ? "1.6em" : "1.9em", className: "fill-muted-foreground text-caption", children: centerLabel.label }) : null
164
- ]
165
- }
166
- ) : null
166
+ }) : null
167
167
  ] });
168
168
  }
169
169
  }
@@ -133,11 +133,18 @@ var radialChartMeta = {
133
133
  default: "runtime locale",
134
134
  description: "BCP 47 locale for default number and date formatting."
135
135
  },
136
+ {
137
+ name: "motion",
138
+ type: '"full" | "subtle" | "off"',
139
+ default: '"full"',
140
+ description: "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. The draw-on, the ring morph and the rolling centre number are the flourish; the hover dim is the feedback `subtle` keeps."
141
+ },
136
142
  {
137
143
  name: "animate",
138
144
  type: "boolean",
139
145
  default: "true",
140
- description: "Draw the rings round from their start when the chart scrolls into view."
146
+ description: 'Draw the rings round from their start when the chart scrolls into view. `animate={false}` is `motion="subtle"`.',
147
+ deprecated: 'motion="subtle" (what animate={false} now maps to: no arrival, hover kept) or motion="off"'
141
148
  },
142
149
  {
143
150
  name: "activeIndex",
@@ -174,19 +181,25 @@ var radialChartMeta = {
174
181
  slot: "arrival",
175
182
  recipe: "custom",
176
183
  part: "Ring",
177
- why: "Once on screen each ring draws round from its start (stroke-dashoffset on a round-capped arc), staggered outside in. On the motion tokens; reduced motion shows the finished chart."
184
+ why: 'Once on screen each ring draws round from its start (stroke-dashoffset on a round-capped arc), staggered outside in. On the motion tokens; reduced motion shows the finished chart. The arrival is expressive: it plays at the full motion level only, so `motion="subtle"` (or a subtle app policy) shows the finished chart while hover feedback stays.'
178
185
  },
179
186
  {
180
187
  slot: "response",
181
188
  recipe: "none",
182
189
  part: "Active ring",
183
- why: "The hovered ring stays full strength and the others dim at once."
190
+ why: "The hovered ring stays full strength and the others dim at once \u2014 mixed toward the surface rather than faded, so a dimmed ring stays solid over its track instead of turning muddy. Essential: `subtle` keeps it."
191
+ },
192
+ {
193
+ slot: "transition",
194
+ recipe: "custom",
195
+ part: "Centre value",
196
+ why: "A numeric centre value rolls to its new total digit by digit (NumberTicker) when the data changes, so the headline is seen moving with the rings instead of being found different afterwards. Expressive: under `subtle` it updates in place."
184
197
  },
185
198
  {
186
199
  slot: "transition",
187
200
  recipe: "custom",
188
201
  part: "Ring",
189
- why: "Geometry eases from the old shape to the new one through a CSS `d` transition on the motion tokens (no recipe animates path geometry); browsers without `d` transitions update in place."
202
+ why: "Geometry eases from the old shape to the new one through a CSS `d` transition on the motion tokens (no recipe animates path geometry); browsers without `d` transitions update in place. Expressive, like the arrival: under `subtle` new data swaps in place."
190
203
  },
191
204
  {
192
205
  slot: "activity",
@@ -67,6 +67,32 @@ var radioMeta = {
67
67
  }
68
68
  ],
69
69
  tokensUsed: ["input", "card", "primary", "ring"],
70
+ motion: [
71
+ {
72
+ slot: "arrival",
73
+ recipe: "none",
74
+ part: "Root",
75
+ why: "Radios arrive with the form they belong to. Animating each one in turns a question into a title sequence."
76
+ },
77
+ {
78
+ slot: "response",
79
+ recipe: "motion-interactive",
80
+ part: "Root",
81
+ why: "The ring's edge and focus ring ease, so arrowing through a group \u2014 which moves the selection with focus \u2014 reads as one choice moving down the list."
82
+ },
83
+ {
84
+ slot: "transition",
85
+ recipe: "motion-interactive",
86
+ part: "Indicator",
87
+ why: "The dot grows from the centre on a spring and shrinks out of the radio being left. A group's selection moves from one option to another, and the paired grow and shrink is what makes it read as moving rather than two dots blinking."
88
+ },
89
+ {
90
+ slot: "activity",
91
+ recipe: "none",
92
+ part: "Root",
93
+ why: "A radio commits instantly. A slow write belongs to the surrounding form's pending state, not the control."
94
+ }
95
+ ],
70
96
  a11y: [
71
97
  "Base UI provides the radiogroup/radio roles, aria-checked, hidden native inputs, and roving tabindex.",
72
98
  "Arrow keys move and select between items; Space selects the focused item; Tab enters/leaves the group.",