@sableui/react 0.13.1 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (231) hide show
  1. package/AGENTS.md +2 -2
  2. package/CHANGELOG.md +132 -0
  3. package/CONTRIBUTING.md +7 -5
  4. package/DESIGN.md +8 -3
  5. package/bin/sable.mjs +17 -1
  6. package/dist/blocks/approval-block.js +93 -0
  7. package/dist/blocks/approval-block.meta.js +23 -0
  8. package/dist/blocks/index.d.ts +17 -2
  9. package/dist/blocks/index.js +4 -0
  10. package/dist/components/accordion/accordion.meta.js +10 -3
  11. package/dist/components/area-chart/area-chart.meta.js +10 -3
  12. package/dist/components/attachment/attachment.meta.js +26 -0
  13. package/dist/components/badge/badge.meta.js +2 -1
  14. package/dist/components/banner/banner.meta.js +4 -2
  15. package/dist/components/bar-chart/bar-chart.meta.js +11 -4
  16. package/dist/components/bottom-nav/bottom-nav.js +3 -1
  17. package/dist/components/bottom-nav/bottom-nav.meta.js +32 -0
  18. package/dist/components/breadcrumbs/breadcrumbs.meta.js +26 -0
  19. package/dist/components/button/button.js +21 -4
  20. package/dist/components/button/button.meta.js +7 -7
  21. package/dist/components/button-group/button-group.meta.js +2 -1
  22. package/dist/components/calendar/calendar.js +18 -1
  23. package/dist/components/calendar/calendar.meta.js +38 -0
  24. package/dist/components/carousel/carousel.js +6 -4
  25. package/dist/components/carousel/carousel.meta.js +42 -0
  26. package/dist/components/chat-composer/chat-composer.js +21 -21
  27. package/dist/components/chat-composer/chat-composer.meta.js +38 -0
  28. package/dist/components/chat-composer/use-eased-growth.js +70 -0
  29. package/dist/components/checkbox/checkbox-mark.js +8 -1
  30. package/dist/components/checkbox/checkbox.js +3 -1
  31. package/dist/components/checkbox/checkbox.meta.js +34 -1
  32. package/dist/components/checkbox-card/checkbox-card.js +3 -0
  33. package/dist/components/checkbox-card/checkbox-card.meta.js +32 -0
  34. package/dist/components/code-block/code-block.js +14 -3
  35. package/dist/components/code-block/code-block.meta.js +28 -1
  36. package/dist/components/collapsible/collapsible.meta.js +34 -1
  37. package/dist/components/color-field/color-field.meta.js +26 -0
  38. package/dist/components/combobox/combobox.meta.js +48 -2
  39. package/dist/components/command-menu/command-menu.js +8 -3
  40. package/dist/components/command-menu/command-menu.meta.js +44 -3
  41. package/dist/components/confirm-button/confirm-button.js +179 -0
  42. package/dist/components/confirm-button/confirm-button.meta.js +143 -0
  43. package/dist/components/confirm-button/index.js +7 -0
  44. package/dist/components/context-card/context-card.meta.js +2 -1
  45. package/dist/components/data-table/data-table.js +6 -2
  46. package/dist/components/data-table/data-table.meta.js +32 -0
  47. package/dist/components/dialog/dialog.meta.js +6 -3
  48. package/dist/components/disclosure-picker/disclosure-picker.meta.js +2 -1
  49. package/dist/components/entity/entity.meta.js +26 -0
  50. package/dist/components/feedback/feedback.js +113 -47
  51. package/dist/components/feedback/feedback.meta.js +49 -4
  52. package/dist/components/inline-status/inline-status.meta.js +2 -1
  53. package/dist/components/input-group/input-group.meta.js +27 -1
  54. package/dist/components/line-chart/line-chart.meta.js +10 -3
  55. package/dist/components/menu/menu.meta.js +2 -1
  56. package/dist/components/menubar/menubar.meta.js +2 -1
  57. package/dist/components/meter/meter.meta.js +2 -1
  58. package/dist/components/middle-truncate/middle-truncate.meta.js +1 -1
  59. package/dist/components/note/note.meta.js +34 -1
  60. package/dist/components/notification-badge/notification-badge.js +17 -5
  61. package/dist/components/notification-badge/notification-badge.meta.js +34 -2
  62. package/dist/components/number-ticker/index.js +7 -0
  63. package/dist/components/number-ticker/number-ticker.js +246 -0
  64. package/dist/components/number-ticker/number-ticker.meta.js +159 -0
  65. package/dist/components/otp-field/otp-field.js +23 -3
  66. package/dist/components/otp-field/otp-field.meta.js +23 -2
  67. package/dist/components/page-loader/page-loader.meta.js +2 -1
  68. package/dist/components/panel-group/panel-group.meta.js +26 -0
  69. package/dist/components/panel-section/panel-section.js +37 -10
  70. package/dist/components/panel-section/panel-section.meta.js +32 -0
  71. package/dist/components/pie-chart/pie-chart.js +20 -11
  72. package/dist/components/pie-chart/pie-chart.meta.js +17 -4
  73. package/dist/components/pill/pill.meta.js +34 -1
  74. package/dist/components/popover/popover.meta.js +4 -2
  75. package/dist/components/progress/progress.meta.js +2 -1
  76. package/dist/components/radar-chart/radar-chart.js +4 -3
  77. package/dist/components/radar-chart/radar-chart.meta.js +10 -3
  78. package/dist/components/radial-chart/radial-chart.js +22 -22
  79. package/dist/components/radial-chart/radial-chart.meta.js +17 -4
  80. package/dist/components/radio/radio.meta.js +28 -1
  81. package/dist/components/radio-card/radio-card.meta.js +38 -0
  82. package/dist/components/relative-time/relative-time.meta.js +32 -0
  83. package/dist/components/scroller/scroller.meta.js +32 -0
  84. package/dist/components/scrub-field/scrub-field.meta.js +32 -0
  85. package/dist/components/search-input/search-input.js +9 -7
  86. package/dist/components/search-input/search-input.meta.js +26 -0
  87. package/dist/components/segmented-bar/segmented-bar.meta.js +2 -1
  88. package/dist/components/select/select.meta.js +4 -2
  89. package/dist/components/sheet/sheet.meta.js +4 -2
  90. package/dist/components/show-more/show-more.js +1 -4
  91. package/dist/components/show-more/show-more.meta.js +38 -0
  92. package/dist/components/side-nav/side-nav.js +21 -1
  93. package/dist/components/side-nav/side-nav.meta.js +34 -1
  94. package/dist/components/side-nav/side-nav.variants.js +3 -1
  95. package/dist/components/sidebar/sidebar.js +31 -8
  96. package/dist/components/sidebar/sidebar.meta.js +38 -0
  97. package/dist/components/skeleton/skeleton.meta.js +2 -1
  98. package/dist/components/slider/slider.meta.js +26 -0
  99. package/dist/components/sparkline/sparkline.js +7 -4
  100. package/dist/components/sparkline/sparkline.meta.js +9 -2
  101. package/dist/components/spinner/spinner.meta.js +2 -1
  102. package/dist/components/stepper/stepper.meta.js +4 -2
  103. package/dist/components/switch/switch.meta.js +2 -1
  104. package/dist/components/table/table.meta.js +28 -1
  105. package/dist/components/tabs/tabs.js +27 -8
  106. package/dist/components/tabs/tabs.meta.js +50 -5
  107. package/dist/components/theme-selector/theme-selector.meta.js +32 -0
  108. package/dist/components/theme-switcher/theme-switcher.js +4 -1
  109. package/dist/components/theme-switcher/theme-switcher.meta.js +42 -0
  110. package/dist/components/thinking-orb/thinking-orb.meta.js +4 -2
  111. package/dist/components/toast/toast.meta.js +2 -1
  112. package/dist/components/toggle/toggle.js +28 -5
  113. package/dist/components/toggle/toggle.meta.js +46 -3
  114. package/dist/components/tool-call/index.js +8 -0
  115. package/dist/components/tool-call/tool-call.js +117 -0
  116. package/dist/components/tool-call/tool-call.meta.js +147 -0
  117. package/dist/components/tool-call/tool-call.variants.js +33 -0
  118. package/dist/components/toolbar/toolbar.meta.js +2 -1
  119. package/dist/components/tooltip/index.js +3 -1
  120. package/dist/components/tooltip/tooltip.js +84 -0
  121. package/dist/components/tooltip/tooltip.meta.js +18 -2
  122. package/dist/components/tree/tree.js +71 -6
  123. package/dist/components/tree/tree.meta.js +32 -0
  124. package/dist/components/video/video.meta.js +32 -0
  125. package/dist/file-hashes.json +139 -122
  126. package/dist/foundations/motion.js +49 -3
  127. package/dist/index.d.ts +616 -46
  128. package/dist/index.js +4 -1
  129. package/dist/lib/chart/cartesian.js +9 -10
  130. package/dist/lib/chart/chart-center.js +30 -0
  131. package/dist/lib/chart/chart-frame.js +15 -8
  132. package/dist/lib/chart/dim.js +13 -0
  133. package/dist/lib/chart/format.js +7 -1
  134. package/dist/lib/chart/motion.js +6 -4
  135. package/dist/lib/highlight-glide.js +68 -10
  136. package/dist/lib/taxonomy.js +3 -0
  137. package/dist/{meta-DVG8ceze.d.ts → meta-DX5r5wQL.d.ts} +8 -0
  138. package/dist/motion/MotionProvider.js +45 -3
  139. package/dist/motion/Reveal.js +1 -1
  140. package/dist/motion/TextReveal.js +1 -1
  141. package/dist/motion/index.js +15 -2
  142. package/dist/motion/useAutoSize.js +42 -0
  143. package/dist/motion/useShake.js +25 -0
  144. package/dist/patterns/index.d.ts +1 -1
  145. package/dist/styles.css +298 -5
  146. package/llms/blocks/approval-block.md +128 -0
  147. package/llms/components/accordion.md +4 -3
  148. package/llms/components/area-chart.md +4 -3
  149. package/llms/components/attachment.md +7 -0
  150. package/llms/components/badge.md +1 -1
  151. package/llms/components/banner.md +2 -2
  152. package/llms/components/bar-chart.md +5 -4
  153. package/llms/components/bottom-nav.md +8 -0
  154. package/llms/components/breadcrumbs.md +7 -0
  155. package/llms/components/button-group.md +1 -1
  156. package/llms/components/button.md +6 -6
  157. package/llms/components/calendar.md +9 -0
  158. package/llms/components/carousel.md +17 -0
  159. package/llms/components/chat-composer.md +9 -0
  160. package/llms/components/checkbox-card.md +8 -0
  161. package/llms/components/checkbox.md +9 -1
  162. package/llms/components/code-block.md +8 -0
  163. package/llms/components/collapsible.md +9 -1
  164. package/llms/components/color-field.md +7 -0
  165. package/llms/components/combobox.md +12 -2
  166. package/llms/components/command-menu.md +12 -3
  167. package/llms/components/confirm-button.md +92 -0
  168. package/llms/components/context-card.md +1 -1
  169. package/llms/components/data-table.md +8 -0
  170. package/llms/components/dialog.md +3 -3
  171. package/llms/components/disclosure-picker.md +1 -1
  172. package/llms/components/entity.md +7 -0
  173. package/llms/components/feedback.md +22 -3
  174. package/llms/components/inline-status.md +1 -1
  175. package/llms/components/input-group.md +8 -1
  176. package/llms/components/line-chart.md +4 -3
  177. package/llms/components/menu.md +1 -1
  178. package/llms/components/menubar.md +1 -1
  179. package/llms/components/meter.md +1 -1
  180. package/llms/components/middle-truncate.md +1 -1
  181. package/llms/components/note.md +9 -1
  182. package/llms/components/notification-badge.md +10 -2
  183. package/llms/components/number-ticker.md +86 -0
  184. package/llms/components/otp-field.md +15 -1
  185. package/llms/components/page-loader.md +1 -1
  186. package/llms/components/panel-group.md +7 -0
  187. package/llms/components/panel-section.md +8 -0
  188. package/llms/components/pie-chart.md +6 -4
  189. package/llms/components/pill.md +9 -1
  190. package/llms/components/popover.md +2 -2
  191. package/llms/components/progress.md +1 -1
  192. package/llms/components/radar-chart.md +4 -3
  193. package/llms/components/radial-chart.md +6 -4
  194. package/llms/components/radio-card.md +9 -0
  195. package/llms/components/radio.md +8 -1
  196. package/llms/components/relative-time.md +8 -0
  197. package/llms/components/scroller.md +8 -0
  198. package/llms/components/scrub-field.md +8 -0
  199. package/llms/components/search-input.md +7 -0
  200. package/llms/components/segmented-bar.md +1 -1
  201. package/llms/components/select.md +2 -2
  202. package/llms/components/sheet.md +2 -2
  203. package/llms/components/show-more.md +9 -0
  204. package/llms/components/side-nav.md +9 -1
  205. package/llms/components/sidebar.md +9 -0
  206. package/llms/components/skeleton.md +1 -1
  207. package/llms/components/slider.md +7 -0
  208. package/llms/components/sparkline.md +3 -2
  209. package/llms/components/spinner.md +1 -1
  210. package/llms/components/stepper.md +2 -2
  211. package/llms/components/switch.md +1 -1
  212. package/llms/components/table.md +8 -1
  213. package/llms/components/tabs.md +22 -4
  214. package/llms/components/theme-selector.md +8 -0
  215. package/llms/components/theme-switcher.md +17 -0
  216. package/llms/components/thinking-orb.md +2 -2
  217. package/llms/components/toast.md +1 -1
  218. package/llms/components/toggle.md +20 -3
  219. package/llms/components/tool-call.md +115 -0
  220. package/llms/components/toolbar.md +1 -1
  221. package/llms/components/tooltip.md +12 -2
  222. package/llms/components/tree.md +8 -0
  223. package/llms/components/video.md +8 -0
  224. package/llms/foundations/motion.md +48 -3
  225. package/llms/reference/utilities.md +23 -1
  226. package/llms/tasks/reviewing-motion.md +14 -0
  227. package/llms-full.txt +1004 -98
  228. package/llms.txt +16 -11
  229. package/package.json +1 -1
  230. package/registry.json +2708 -187
  231. package/targets/ios.json +27 -0
@@ -1,8 +1,16 @@
1
1
  "use client";
2
2
 
3
3
  // src/components/feedback/feedback.tsx
4
- import { useId, useState } from "react";
4
+ import {
5
+ useEffect,
6
+ useId,
7
+ useLayoutEffect,
8
+ useRef,
9
+ useState
10
+ } from "react";
11
+ import { Icon } from "../../icon/index.js";
5
12
  import { cn } from "../../lib/cn.js";
13
+ import { motionAttribute } from "../../motion/MotionProvider.js";
6
14
  import { Button } from "../button/index.js";
7
15
  import { Popover, PopoverContent, PopoverTrigger } from "../popover/index.js";
8
16
  import { Textarea } from "../textarea/index.js";
@@ -13,79 +21,137 @@ var DEFAULT_RATINGS = [
13
21
  { value: "ok", label: "\u{1F610}" },
14
22
  { value: "good", label: "\u{1F642}" }
15
23
  ];
24
+ var SUCCESS_DWELL_MS = 1200;
16
25
  function Feedback({
17
26
  label = "Feedback",
18
27
  heading = "Send feedback",
19
28
  placeholder = "What can we improve?",
20
29
  submitLabel = "Send",
21
30
  ratings = DEFAULT_RATINGS,
31
+ successMessage = "Thanks for the feedback.",
22
32
  onSubmit,
33
+ motion,
23
34
  className,
24
35
  ...props
25
36
  }) {
26
37
  const [open, setOpen] = useState(false);
27
38
  const [rating, setRating] = useState(void 0);
28
39
  const [message, setMessage] = useState("");
40
+ const [sent, setSent] = useState(false);
41
+ const [drawn, setDrawn] = useState(false);
42
+ const thanksRef = useRef(null);
29
43
  const headingId = useId();
30
44
  function reset() {
31
45
  setRating(void 0);
32
46
  setMessage("");
47
+ setSent(false);
48
+ setDrawn(false);
33
49
  }
34
50
  function handleSubmit(event) {
35
51
  event.preventDefault();
36
- if (!rating && message.trim() === "") return;
52
+ if (sent || !rating && message.trim() === "") return;
37
53
  onSubmit?.({ rating, message });
38
- reset();
39
- setOpen(false);
54
+ setSent(true);
40
55
  }
41
- return /* @__PURE__ */ jsxs(Popover, { open, onOpenChange: setOpen, children: [
42
- /* @__PURE__ */ jsx(
43
- PopoverTrigger,
44
- {
45
- render: /* @__PURE__ */ jsx(Button, { variant: "secondary", size: "sm", children: label })
46
- }
47
- ),
48
- /* @__PURE__ */ jsx(PopoverContent, { className: "w-80", children: /* @__PURE__ */ jsxs(
49
- "form",
50
- {
51
- className: cn("flex flex-col gap-3", className),
52
- "aria-labelledby": headingId,
53
- onSubmit: handleSubmit,
54
- ...props,
55
- children: [
56
- /* @__PURE__ */ jsx("p", { id: headingId, className: "text-title-sm text-popover-foreground", children: heading }),
57
- /* @__PURE__ */ jsx(
58
- ToggleGroup,
56
+ useLayoutEffect(() => {
57
+ if (sent) thanksRef.current?.focus({ preventScroll: true });
58
+ }, [sent]);
59
+ useEffect(() => {
60
+ if (!sent) return;
61
+ let frame = requestAnimationFrame(() => {
62
+ frame = requestAnimationFrame(() => setDrawn(true));
63
+ });
64
+ const timer = setTimeout(() => setOpen(false), SUCCESS_DWELL_MS);
65
+ return () => {
66
+ cancelAnimationFrame(frame);
67
+ clearTimeout(timer);
68
+ };
69
+ }, [sent]);
70
+ return /* @__PURE__ */ jsxs(
71
+ Popover,
72
+ {
73
+ open,
74
+ onOpenChange: setOpen,
75
+ onOpenChangeComplete: (isOpen) => {
76
+ if (!isOpen) reset();
77
+ },
78
+ children: [
79
+ /* @__PURE__ */ jsx(
80
+ PopoverTrigger,
81
+ {
82
+ render: /* @__PURE__ */ jsx(Button, { variant: "secondary", size: "sm", children: label })
83
+ }
84
+ ),
85
+ /* @__PURE__ */ jsxs(PopoverContent, { className: "grid w-80", "data-motion": motionAttribute(motion), children: [
86
+ /* @__PURE__ */ jsxs(
87
+ "form",
59
88
  {
60
- value: rating ? [rating] : [],
61
- onValueChange: (value) => setRating(value[0]),
62
- "aria-label": "Rating",
63
- children: ratings.map((option) => /* @__PURE__ */ jsx(
64
- Toggle,
65
- {
66
- value: option.value,
67
- "aria-label": option.value,
68
- className: "flex-1 text-base",
69
- children: option.label
70
- },
71
- option.value
72
- ))
89
+ className: cn(
90
+ "col-start-1 row-start-1 flex flex-col gap-3 transition-opacity duration-(--motion-duration-fast) ease-standard",
91
+ sent && "opacity-0",
92
+ className
93
+ ),
94
+ "aria-labelledby": headingId,
95
+ inert: sent,
96
+ onSubmit: handleSubmit,
97
+ ...props,
98
+ children: [
99
+ /* @__PURE__ */ jsx("p", { id: headingId, className: "text-title-sm text-popover-foreground", children: heading }),
100
+ /* @__PURE__ */ jsx(
101
+ ToggleGroup,
102
+ {
103
+ value: rating ? [rating] : [],
104
+ onValueChange: (value) => setRating(value[0]),
105
+ "aria-label": "Rating",
106
+ children: ratings.map((option) => /* @__PURE__ */ jsx(
107
+ Toggle,
108
+ {
109
+ value: option.value,
110
+ "aria-label": option.value,
111
+ className: "flex-1 text-base",
112
+ children: option.label
113
+ },
114
+ option.value
115
+ ))
116
+ }
117
+ ),
118
+ /* @__PURE__ */ jsx(
119
+ Textarea,
120
+ {
121
+ value: message,
122
+ onChange: (event) => setMessage(event.target.value),
123
+ placeholder,
124
+ "aria-label": placeholder
125
+ }
126
+ ),
127
+ /* @__PURE__ */ jsx("div", { className: "flex items-center justify-end gap-2", children: /* @__PURE__ */ jsx(Button, { type: "submit", size: "sm", disabled: !rating && message.trim() === "", children: submitLabel }) })
128
+ ]
73
129
  }
74
130
  ),
75
- /* @__PURE__ */ jsx(
76
- Textarea,
131
+ sent ? /* @__PURE__ */ jsxs(
132
+ "div",
77
133
  {
78
- value: message,
79
- onChange: (event) => setMessage(event.target.value),
80
- placeholder,
81
- "aria-label": placeholder
134
+ ref: thanksRef,
135
+ tabIndex: -1,
136
+ "data-slot": "feedback-thanks",
137
+ className: "col-start-1 row-start-1 flex flex-col items-center justify-center gap-2 text-center outline-none transition-opacity duration-(--motion-duration-base) ease-standard starting:opacity-0",
138
+ children: [
139
+ /* @__PURE__ */ jsx(
140
+ "span",
141
+ {
142
+ "aria-hidden": "true",
143
+ className: "inline-flex size-10 items-center justify-center rounded-full bg-success/15 text-2xl text-success",
144
+ children: /* @__PURE__ */ jsx("span", { "data-starting-style": drawn ? void 0 : "", className: "inline-flex motion-tick", children: /* @__PURE__ */ jsx(Icon, { name: "check" }) })
145
+ }
146
+ ),
147
+ /* @__PURE__ */ jsx("p", { className: "text-body-md text-popover-foreground", children: successMessage })
148
+ ]
82
149
  }
83
- ),
84
- /* @__PURE__ */ jsx("div", { className: "flex items-center justify-end gap-2", children: /* @__PURE__ */ jsx(Button, { type: "submit", size: "sm", disabled: !rating && message.trim() === "", children: submitLabel }) })
85
- ]
86
- }
87
- ) })
88
- ] });
150
+ ) : null
151
+ ] })
152
+ ]
153
+ }
154
+ );
89
155
  }
90
156
  Feedback.displayName = "Feedback";
91
157
  export {
@@ -1,7 +1,7 @@
1
1
  // src/components/feedback/feedback.meta.ts
2
2
  var feedbackMeta = {
3
3
  name: "Feedback",
4
- summary: "A compact feedback popover. Composes Popover + Textarea + Button + Toggle into a rating row plus a message field; submitting reports the rating + message and closes the popover.",
4
+ summary: "A compact feedback popover. Composes Popover + Textarea + Button + Toggle into a rating row plus a message field; submitting reports the rating + message, shows a brief drawn-check thanks, and closes the popover.",
5
5
  status: "stable",
6
6
  composition: "Popover, Textarea, ToggleGroup, Button",
7
7
  keywords: [
@@ -72,7 +72,19 @@ var feedbackMeta = {
72
72
  {
73
73
  name: "onSubmit",
74
74
  type: "(data: { rating?: string; message: string }) => void",
75
- description: "Called with the selected rating + message when the form is submitted."
75
+ description: "Called with the selected rating + message when the form is submitted. The thanks shows right away; send the report in the background."
76
+ },
77
+ {
78
+ name: "successMessage",
79
+ type: "string",
80
+ default: '"Thanks for the feedback."',
81
+ description: "The line shown, and read out, for about 1.2s after sending, before the popover closes on its own."
82
+ },
83
+ {
84
+ name: "motion",
85
+ type: '"full" | "subtle" | "off"',
86
+ default: '"full"',
87
+ 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 drawn check is the flourish; the thanks line and its dwell stay."
76
88
  }
77
89
  ],
78
90
  tokensUsed: [
@@ -85,14 +97,43 @@ var feedbackMeta = {
85
97
  "muted-foreground",
86
98
  "accent",
87
99
  "accent-foreground",
88
- "ring"
100
+ "ring",
101
+ "success"
102
+ ],
103
+ motion: [
104
+ {
105
+ slot: "arrival",
106
+ recipe: "motion-pop",
107
+ part: "Popover",
108
+ why: "The form opens from its trigger, so the eye goes from the button it pressed to the field it should type in. Popover owns the recipe."
109
+ },
110
+ {
111
+ slot: "response",
112
+ recipe: "motion-interactive",
113
+ part: "Rating toggles",
114
+ why: "The rating row is a choice among a few similar marks. The eased selected fill is what makes the pick read as made, not just hovered."
115
+ },
116
+ {
117
+ slot: "transition",
118
+ recipe: "motion-tick",
119
+ part: "Thanks",
120
+ why: "Sending is the one moment the person needs to know their words arrived. The form fades out in place and a check draws itself in over it, then the popover closes. Without it the popover just vanished, which looks the same as the form being dismissed or the message lost."
121
+ },
122
+ {
123
+ slot: "activity",
124
+ recipe: "none",
125
+ part: "Root",
126
+ why: "The thanks shows as soon as the form is sent; delivery is the app's background work. A spinner for a one-line report would make a quick, optional gesture feel heavy."
127
+ }
89
128
  ],
90
129
  a11y: [
91
130
  "Trigger opens the popover on press; Base UI manages focus into the form and restores it to the trigger on close.",
92
131
  "The rating row is a Base UI ToggleGroup with arrow-key roving focus; each toggle carries an aria-label.",
93
132
  'The form is named by its heading (aria-labelledby), so it is announced as "Send feedback" rather than an unnamed form.',
94
133
  "The message field carries an aria-label matching its placeholder.",
95
- "Submit is disabled until there is a rating or a non-whitespace message. Either alone is a complete answer."
134
+ "Submit is disabled until there is a rating or a non-whitespace message. Either alone is a complete answer.",
135
+ "On send, focus moves to the thanks line (tabIndex=-1), which reads it out, and the form becomes inert so focus cannot land in invisible fields. The line is not also a live region, which read it twice. When the popover closes, Base UI returns focus to the trigger.",
136
+ "The thanks dwell (about 1.2s) is a reading time, not motion, so it is the same under reduced motion."
96
137
  ],
97
138
  examples: [
98
139
  {
@@ -102,6 +143,10 @@ var feedbackMeta = {
102
143
  {
103
144
  label: "Custom labels",
104
145
  code: '<Feedback label="Tell us" submitLabel="Send it" placeholder="Describe the issue\u2026" />'
146
+ },
147
+ {
148
+ label: "Custom thanks",
149
+ code: '<Feedback successMessage="Got it \u2014 thank you." onSubmit={({ rating, message }) => report(rating, message)} />'
105
150
  }
106
151
  ]
107
152
  };
@@ -22,7 +22,8 @@ var inlineStatusMeta = {
22
22
  },
23
23
  {
24
24
  guidance: "dont",
25
- description: "Do not use InlineStatus for global announcements or page-level alerts."
25
+ description: "Do not use InlineStatus for global announcements or page-level alerts.",
26
+ instead: ["Banner"]
26
27
  },
27
28
  {
28
29
  guidance: "dont",
@@ -76,6 +76,32 @@ var inputGroupMeta = {
76
76
  }
77
77
  ],
78
78
  tokensUsed: ["input", "card", "ring", "destructive", "muted-foreground", "foreground"],
79
+ motion: [
80
+ {
81
+ slot: "arrival",
82
+ recipe: "none",
83
+ part: "Root",
84
+ why: "It arrives with the form it belongs to. A field that animated in on its own would be briefly unusable for no stated reason."
85
+ },
86
+ {
87
+ slot: "response",
88
+ recipe: "motion-interactive",
89
+ part: "Root",
90
+ why: "The shell owns one focus ring for the input and its addons and eases it in, so focusing the text, or tabbing to a copy button inside the field, reads as the one field taking focus."
91
+ },
92
+ {
93
+ slot: "transition",
94
+ recipe: "motion-interactive",
95
+ part: "Root",
96
+ why: "An invalid value turns the shell's edge destructive on the same ease, so the error lands as the field changing state rather than a red box swapping in."
97
+ },
98
+ {
99
+ slot: "activity",
100
+ recipe: "none",
101
+ part: "Root",
102
+ why: "A field does no work of its own. A pending check (an availability lookup) puts a Spinner in `trailing`, which is where its answer will appear."
103
+ }
104
+ ],
79
105
  a11y: [
80
106
  "The Input stays the focusable control; the group draws the focus ring around the whole field while it is focused.",
81
107
  "Inside a Field, the label, description and error are wired to the Input itself, not the group.",
@@ -93,7 +119,7 @@ var inputGroupMeta = {
93
119
  },
94
120
  {
95
121
  label: "Copy button",
96
- code: '<Field label="API key"><InputGroup trailing={<Button variant="ghost" size="icon-sm" aria-label="Copy key" onClick={copy}><Icon name={copied ? "check" : "copy"} /></Button>}><Input readOnly value={key} /></InputGroup></Field>'
122
+ code: '<Field label="API key"><InputGroup trailing={<Button variant="ghost" size="icon-sm" aria-label="Copy key" onClick={copy}><span aria-hidden="true" className="motion-icon-swap"><span data-active={!copied} className="inline-flex"><Icon name="copy" /></span><span data-active={copied} className="inline-flex"><Icon name="check" /></span></span></Button>}><Input readOnly value={key} /></InputGroup></Field>'
97
123
  },
98
124
  {
99
125
  label: "Password with reveal",
@@ -115,11 +115,18 @@ var lineChartMeta = {
115
115
  default: "false for columns; true for horizontal bars",
116
116
  description: "The vertical axis."
117
117
  },
118
+ {
119
+ name: "motion",
120
+ type: '"full" | "subtle" | "off"',
121
+ default: '"full"',
122
+ 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 arrival (lines drawing on) and the data-change morph are the flourish; the gliding cursor, dots and tooltip are the feedback `subtle` keeps."
123
+ },
118
124
  {
119
125
  name: "animate",
120
126
  type: "boolean",
121
127
  default: "true",
122
- description: "Play the arrival once the chart scrolls into view. Reduced motion shows the finished chart."
128
+ description: 'Play the arrival once the chart scrolls into view. Reduced motion shows the finished chart. `animate={false}` is `motion="subtle"`.',
129
+ deprecated: 'motion="subtle" (what animate={false} now maps to: no arrival, hover kept) or motion="off"'
123
130
  },
124
131
  {
125
132
  name: "activeIndex",
@@ -180,7 +187,7 @@ var lineChartMeta = {
180
187
  slot: "arrival",
181
188
  recipe: "custom",
182
189
  part: "Line",
183
- why: "Once on screen each line draws on from the first point (stroke-dashoffset), series staggered, then the dots and labels fade in. On the motion tokens; reduced motion shows the finished chart."
190
+ why: 'Once on screen each line draws on from the first point (stroke-dashoffset), series staggered, then the dots and labels fade 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.'
184
191
  },
185
192
  {
186
193
  slot: "response",
@@ -192,7 +199,7 @@ var lineChartMeta = {
192
199
  slot: "transition",
193
200
  recipe: "custom",
194
201
  part: "Line + dots",
195
- 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."
196
203
  },
197
204
  {
198
205
  slot: "activity",
@@ -26,7 +26,8 @@ var menuMeta = {
26
26
  },
27
27
  {
28
28
  guidance: "dont",
29
- description: "Do not build a Select out of it. A menu fires actions and has no value; a Select holds one and reports changes. Swapping them loses form semantics and keyboard type-ahead."
29
+ description: "Do not build a Select out of it. A menu fires actions and has no value; a Select holds one and reports changes. Swapping them loses form semantics and keyboard type-ahead.",
30
+ instead: ["Select"]
30
31
  },
31
32
  {
32
33
  guidance: "do",
@@ -25,7 +25,8 @@ var menubarMeta = {
25
25
  },
26
26
  {
27
27
  guidance: "dont",
28
- description: "Do not use a menubar for site navigation. Its items are commands in menus (role=menuitem), not links between pages."
28
+ description: "Do not use a menubar for site navigation. Its items are commands in menus (role=menuitem), not links between pages.",
29
+ instead: ["NavigationMenu"]
29
30
  }
30
31
  ],
31
32
  anatomy: [
@@ -26,7 +26,8 @@ var meterMeta = {
26
26
  },
27
27
  {
28
28
  guidance: "dont",
29
- description: "Do not use Meter for a task in flight \u2014 an upload, an install, a step count. That is Progress; Meter tells assistive tech nothing is happening."
29
+ description: "Do not use Meter for a task in flight \u2014 an upload, an install, a step count. That is Progress; Meter tells assistive tech nothing is happening.",
30
+ instead: ["Progress"]
30
31
  },
31
32
  {
32
33
  guidance: "dont",
@@ -68,7 +68,7 @@ var middleTruncateMeta = {
68
68
  },
69
69
  {
70
70
  label: "Copyable value",
71
- code: 'const { copy, copied } = useCopy(commitHash)\n<Row gap={1}><MiddleTruncate text={commitHash} /><Button variant="ghost" size="icon-xs" aria-label="Copy commit hash" onClick={copy}><Icon name={copied ? "check" : "copy"} /></Button></Row>'
71
+ code: 'const { copy, copied } = useCopy(commitHash)\n<Row gap={1}><MiddleTruncate text={commitHash} /><Button variant="ghost" size="icon-xs" aria-label="Copy commit hash" onClick={copy}><span aria-hidden="true" className="motion-icon-swap"><span data-active={!copied} className="inline-flex"><Icon name="copy" /></span><span data-active={copied} className="inline-flex"><Icon name="check" /></span></span></Button><span role="status" className="sr-only">{copied ? "Copied" : ""}</span></Row>'
72
72
  }
73
73
  ]
74
74
  };
@@ -33,7 +33,8 @@ var noteMeta = {
33
33
  },
34
34
  {
35
35
  guidance: "dont",
36
- description: "Do not use Note for state the whole page or app is in (an outage, an offline session, a blocked account) \u2014 that is Banner \u2014 or for a message tied to one field, which is the field's own error text."
36
+ description: "Do not use Note for state the whole page or app is in (an outage, an offline session, a blocked account) \u2014 that is Banner \u2014 or for a message tied to one field, which is the field's own error text.",
37
+ instead: ["Banner"]
37
38
  },
38
39
  {
39
40
  guidance: "dont",
@@ -84,6 +85,38 @@ var noteMeta = {
84
85
  "success",
85
86
  "destructive"
86
87
  ],
88
+ motion: [
89
+ {
90
+ slot: "arrival",
91
+ recipe: "none",
92
+ part: "Root",
93
+ why: "A note is part of the content it annotates and renders with it. A callout that animated in would pull the eye before the reader has reached it in the flow."
94
+ },
95
+ {
96
+ slot: "response",
97
+ recipe: "motion-interactive",
98
+ part: "Action",
99
+ why: "The action's focus ring eases in, and its underline thickens on hover. Inside a block of guidance, that is what separates the one thing to do from the prose around it."
100
+ },
101
+ {
102
+ slot: "response",
103
+ recipe: "motion-press",
104
+ part: "Action",
105
+ why: "A note's action usually navigates or opens something; the small settle on press is the only confirmation before the next surface paints."
106
+ },
107
+ {
108
+ slot: "transition",
109
+ recipe: "none",
110
+ part: "Root",
111
+ why: "A note has no states to move between: its tone is fixed when it renders. A note that changes tone is a new message, and should arrive as one."
112
+ },
113
+ {
114
+ slot: "activity",
115
+ recipe: "none",
116
+ part: "Root",
117
+ why: "Static guidance. Work in progress belongs in a Progress or a Spinner, not in a callout."
118
+ }
119
+ ],
87
120
  a11y: [
88
121
  'Rendered with role="note" so assistive tech can announce it as an aside.',
89
122
  "Color is never the sole signal \u2014 the leading icon and optional title reinforce severity."
@@ -1,6 +1,10 @@
1
+ "use client";
2
+
1
3
  // src/components/notification-badge/notification-badge.tsx
4
+ import { useState } from "react";
2
5
  import { cn } from "../../lib/cn.js";
3
- import { trimLabel } from "../../lib/trim-label.js";
6
+ import { motionAttribute } from "../../motion/MotionProvider.js";
7
+ import { NumberTicker } from "../number-ticker/number-ticker.js";
4
8
  import {
5
9
  notificationCountVariants,
6
10
  notificationDotVariants
@@ -13,16 +17,22 @@ function NotificationBadge({
13
17
  tone,
14
18
  size,
15
19
  overlay = false,
20
+ motion,
16
21
  className,
17
22
  ...props
18
23
  }) {
24
+ const empty = !dot && (count === void 0 || count <= 0);
25
+ const [wasEmpty, setWasEmpty] = useState(empty);
26
+ if (empty && !wasEmpty) setWasEmpty(true);
27
+ const appear = wasEmpty && "motion-appear";
19
28
  if (dot) {
20
29
  return /* @__PURE__ */ jsx(
21
30
  "span",
22
31
  {
23
32
  "data-slot": "notification-badge",
33
+ "data-motion": motionAttribute(motion),
24
34
  "aria-hidden": true,
25
- className: cn(notificationDotVariants({ tone, size, overlay }), className),
35
+ className: cn(notificationDotVariants({ tone, size, overlay }), appear, className),
26
36
  ...props
27
37
  }
28
38
  );
@@ -30,18 +40,20 @@ function NotificationBadge({
30
40
  if (count === void 0 || count <= 0) {
31
41
  return null;
32
42
  }
33
- const label = count > max ? `${max}+` : String(count);
43
+ const over = count > max;
34
44
  return /* @__PURE__ */ jsx(
35
45
  "span",
36
46
  {
37
47
  "data-slot": "notification-badge",
48
+ "data-motion": motionAttribute(motion),
38
49
  "aria-hidden": true,
39
- className: cn(notificationCountVariants({ tone, size, overlay }), className),
50
+ className: cn(notificationCountVariants({ tone, size, overlay }), appear, className),
40
51
  ...props,
41
- children: trimLabel(label)
52
+ children: /* @__PURE__ */ jsx("span", { className: "sable-label-trim", children: /* @__PURE__ */ jsx(NumberTicker, { value: over ? max : count, suffix: over ? "+" : void 0, format: NO_GROUPING, motion }) })
42
53
  }
43
54
  );
44
55
  }
56
+ var NO_GROUPING = { useGrouping: false };
45
57
  NotificationBadge.displayName = "NotificationBadge";
46
58
  export {
47
59
  NotificationBadge
@@ -1,7 +1,7 @@
1
1
  // src/components/notification-badge/notification-badge.meta.ts
2
2
  var notificationBadgeMeta = {
3
3
  name: "NotificationBadge",
4
- summary: `An unread indicator. Renders a bare dot or a count pill (capped as "{max}+"), or nothing when there's nothing to show. overlay pins it to the corner of a relative host; it is aria-hidden, so the host's name carries the count.`,
4
+ summary: `An unread indicator. Renders a bare dot or a count pill (capped as "{max}+"), or nothing when there's nothing to show. A changing count rolls to its new number, and a badge that appears after being empty pops in. overlay pins it to the corner of a relative host; it is aria-hidden, so the host's name carries the count.`,
5
5
  status: "stable",
6
6
  keywords: [
7
7
  "notification",
@@ -35,7 +35,7 @@ var notificationBadgeMeta = {
35
35
  ]
36
36
  },
37
37
  related: {
38
- components: ["Badge", "StatusDot"]
38
+ components: ["Badge", "StatusDot", "NumberTicker"]
39
39
  },
40
40
  props: [
41
41
  {
@@ -72,6 +72,12 @@ var notificationBadgeMeta = {
72
72
  type: "boolean",
73
73
  default: "false",
74
74
  description: "Pin to the top-end corner of the nearest relative host (an icon button) \u2014 one placement for every caller instead of hand-tuned absolute offsets."
75
+ },
76
+ {
77
+ name: "motion",
78
+ type: '"full" | "subtle" | "off"',
79
+ default: '"full"',
80
+ 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. Under `subtle` the count updates in place and the badge fades in without the pop."
75
81
  }
76
82
  ],
77
83
  variants: {
@@ -87,6 +93,32 @@ var notificationBadgeMeta = {
87
93
  "secondary",
88
94
  "secondary-foreground"
89
95
  ],
96
+ motion: [
97
+ {
98
+ slot: "arrival",
99
+ recipe: "motion-appear",
100
+ part: "root",
101
+ why: "A badge that shows up after being empty is the news \u2014 something just arrived \u2014 so it fades and scales in where it sits instead of popping into existence. Never on first paint: a badge that mounts with a count is content, and a page of them popping on load is noise."
102
+ },
103
+ {
104
+ slot: "response",
105
+ recipe: "none",
106
+ part: "root",
107
+ why: "It is decorative and not interactive; the host it sits on answers the pointer."
108
+ },
109
+ {
110
+ slot: "transition",
111
+ recipe: "custom",
112
+ part: "Count",
113
+ why: 'The count rolls to its new number through NumberTicker (expressive), so 3 \u2192 4 reads as one more arriving rather than a different badge. Past the cap the ticker holds at max with a static "+", so a count climbing beyond 99 does not keep rolling a badge that has not changed.'
114
+ },
115
+ {
116
+ slot: "activity",
117
+ recipe: "none",
118
+ part: "root",
119
+ why: "An unread count has no working state."
120
+ }
121
+ ],
90
122
  a11y: [
91
123
  'The badge is aria-hidden: a count rendered on a control is not associated with it, so the host needs an accessible name that includes the count \u2014 "Notifications, 3 unread" \u2014 or a screen-reader user hears "Notifications, button" and never learns there is anything new. Pass aria-hidden={false} only for a standalone badge whose number is the information.',
92
124
  "A dot carries no number and no words: on its own it tells a sighted user only that something changed, and tells everyone else nothing. Pair it with a label that says what is new, and never make its colour the only difference between two states."
@@ -0,0 +1,7 @@
1
+ // src/components/number-ticker/index.ts
2
+ import { NumberTicker } from "./number-ticker.js";
3
+ import { numberTickerMeta } from "./number-ticker.meta.js";
4
+ export {
5
+ NumberTicker,
6
+ numberTickerMeta
7
+ };