@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
@@ -0,0 +1,143 @@
1
+ // src/components/confirm-button/confirm-button.meta.ts
2
+ var confirmButtonMeta = {
3
+ name: "ConfirmButton",
4
+ summary: 'An inline two-step confirm for a low-stakes destructive or irreversible action: the first press arms it ("Delete" \u2192 "Confirm delete?", destructive), a second press within a few seconds runs it, and leaving it disarms it.',
5
+ status: "stable",
6
+ keywords: [
7
+ "confirm",
8
+ "confirmation",
9
+ "two-step",
10
+ "press again",
11
+ "click again",
12
+ "arm",
13
+ "delete",
14
+ "remove",
15
+ "destructive",
16
+ "inline confirm",
17
+ "hold to confirm"
18
+ ],
19
+ usage: {
20
+ description: "Use ConfirmButton when an action deserves one beat of second thought but not a modal: deleting a draft, removing a row, revoking one token, clearing a list that can be rebuilt. The question appears on the button the user is already pressing, so confirming costs one more press in the same place and backing out costs nothing.",
21
+ bestPractices: [
22
+ {
23
+ guidance: "do",
24
+ description: 'Keep the idle label a plain verb ("Delete", "Remove", "Revoke") and let the confirm label ask about it ("Confirm delete?" by default, or `confirmLabel`). The user should read the consequence on the very control they are about to press again.'
25
+ },
26
+ {
27
+ guidance: "do",
28
+ description: "Leave room for it to grow. The button eases wider to fit the question, so give it space at the end of a row or in a table's action column rather than wedging it between fixed-width neighbours."
29
+ },
30
+ {
31
+ guidance: "do",
32
+ description: "Return the promise from `onConfirm` when the action is async. The button stays armed with a spinner until it settles and refuses further presses, so a slow network cannot run it twice."
33
+ },
34
+ {
35
+ guidance: "dont",
36
+ description: "Do not use it for high-stakes or wide-reaching actions \u2014 deleting a project, an account, anything other people depend on. Those need the consequence spelled out and a typed or deliberate confirmation; use DestructiveActionModal.",
37
+ instead: ["DestructiveActionModal"]
38
+ },
39
+ {
40
+ guidance: "dont",
41
+ description: "Do not use it where an undo is possible. If the action can be reversed, run it on the first press and offer Undo in a Toast \u2014 no question at all is faster than a good question.",
42
+ instead: ["Toast"]
43
+ },
44
+ {
45
+ guidance: "dont",
46
+ description: 'Do not use it for a non-destructive primary action ("Save", "Send"). Asking twice for something harmless teaches people to press twice without reading, which is what defeats the confirm when it matters; use Button.',
47
+ instead: ["Button"]
48
+ }
49
+ ]
50
+ },
51
+ related: {
52
+ components: ["Button", "DestructiveActionModal", "Toast", "OverflowMenu"]
53
+ },
54
+ props: [
55
+ { name: "children", type: "ReactNode", description: 'The idle label \u2014 the action, e.g. "Delete".' },
56
+ {
57
+ name: "confirmLabel",
58
+ type: "ReactNode",
59
+ default: '"Confirm <label>?"',
60
+ description: 'The label while armed. Defaults to "Confirm delete?" for a text label "Delete" (first letter lowered) and "Confirm?" otherwise. Pass it to localise.'
61
+ },
62
+ {
63
+ name: "onConfirm",
64
+ type: "() => unknown",
65
+ description: "Runs on the second press while armed. Return a promise to hold the button armed with a spinner until it settles."
66
+ },
67
+ { name: "timeout", type: "number", default: "3000", description: "How long it waits for the second press, in ms. A decision time, so it is not scaled by motion settings." },
68
+ { name: "variant", type: "ButtonVariant", default: '"secondary"', description: "The idle look \u2014 any Button variant." },
69
+ { name: "confirmVariant", type: "ButtonVariant", default: '"destructive"', description: "The armed look \u2014 any Button variant." },
70
+ { name: "size", type: '"xs" | "sm" | "md" | "cta"', default: '"md"', description: "A labelled Button size. Icon-only sizes are excluded: the label is the interaction." },
71
+ { name: "icon", type: "ReactNode", description: 'A leading icon kept through both states, e.g. `<Icon name="trash" />`.' },
72
+ { name: "disabled", type: "boolean", default: "false", description: "Disables it; disabling while armed also disarms." },
73
+ {
74
+ name: "confirmHint",
75
+ type: "string",
76
+ default: '"Press again to confirm."',
77
+ description: "Read to assistive tech after the armed label when it arms. Override to localise."
78
+ },
79
+ {
80
+ name: "motion",
81
+ type: '"full" | "subtle" | "off"',
82
+ default: '"full"',
83
+ 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 width easing and the draining countdown are the flourish; the label cross-fade and colour change stay."
84
+ }
85
+ ],
86
+ variants: {
87
+ variant: ["primary", "secondary", "ghost", "outline", "destructive", "link", "floating", "field", "on-color", "on-color-outline", "on-color-ghost"],
88
+ confirmVariant: ["destructive", "primary", "secondary", "outline"],
89
+ size: ["xs", "sm", "md", "cta"]
90
+ },
91
+ tokensUsed: ["secondary", "secondary-foreground", "destructive", "destructive-foreground", "ring"],
92
+ motion: [
93
+ {
94
+ slot: "arrival",
95
+ recipe: "none",
96
+ part: "Root",
97
+ why: "It is a button in a row of actions, and arrives with that row. Its moment is the arming, not the mount."
98
+ },
99
+ {
100
+ slot: "response",
101
+ recipe: "motion-press",
102
+ part: "Root",
103
+ why: "Inherited from Button with motion-interactive. The press settle confirms the first press landed even before the label has finished changing \u2014 which is what stops a second, accidental press."
104
+ },
105
+ {
106
+ slot: "transition",
107
+ recipe: "motion-size",
108
+ part: "Root",
109
+ why: "Arming changes the label, so the button's width changes. Easing it makes the button visibly grow into the question and its neighbours travel, instead of the row jumping under the pointer that is about to press again. useAutoSize measures the content and writes a px width for motion-size to ease between (a content-sized box is width:auto before and after, which cannot transition). The labels cross-fade in one cell so there is never a frame with both or neither."
110
+ },
111
+ {
112
+ slot: "activity",
113
+ recipe: "custom",
114
+ part: "Countdown",
115
+ why: "While armed, a thin line drains along the foot of the button over the timeout \u2014 the only way to see that the question expires. A transition on scale, timed by the `timeout` prop and multiplied by the motion scale and expressive factor, so under reduced motion or `subtle` it is absent rather than frozen full."
116
+ }
117
+ ],
118
+ a11y: [
119
+ "A native <button> through Button. Focus stays on it through both presses; Enter and Space confirm exactly as a click does.",
120
+ "Only the current label is in the accessible name \u2014 the other is aria-hidden \u2014 so the name changes to the question while armed.",
121
+ `Arming is also announced through a polite role=status region outside the button ("Confirm delete? Press again to confirm."), because a focused button's name change is not read by every screen reader.`,
122
+ "Escape disarms and is consumed, so it does not also close the dialog or popover around it. Focus leaving, a press elsewhere, or the timeout disarm it too.",
123
+ "A second press within 300ms of arming is ignored: a double-click is a habit, not a decision.",
124
+ "Under reduced motion the label still changes and the colour still turns; only the width easing and countdown are dropped."
125
+ ],
126
+ examples: [
127
+ {
128
+ label: "Delete a row",
129
+ code: '<ConfirmButton icon={<Icon name="trash" />} onConfirm={() => removeRow(id)}>Delete</ConfirmButton>'
130
+ },
131
+ {
132
+ label: "Custom question, longer wait",
133
+ code: '<ConfirmButton variant="ghost" size="sm" confirmLabel="Revoke this key?" timeout={5000} onConfirm={revokeKey}>Revoke</ConfirmButton>'
134
+ },
135
+ {
136
+ label: "Async confirm",
137
+ code: "<ConfirmButton onConfirm={() => api.deleteDraft(draft.id)}>Discard draft</ConfirmButton>"
138
+ }
139
+ ]
140
+ };
141
+ export {
142
+ confirmButtonMeta
143
+ };
@@ -0,0 +1,7 @@
1
+ // src/components/confirm-button/index.ts
2
+ import { ConfirmButton } from "./confirm-button.js";
3
+ import { confirmButtonMeta } from "./confirm-button.meta.js";
4
+ export {
5
+ ConfirmButton,
6
+ confirmButtonMeta
7
+ };
@@ -122,8 +122,12 @@ function DataTable({
122
122
  /* @__PURE__ */ jsx(
123
123
  Icon,
124
124
  {
125
- name: active && sort.direction === "desc" ? "chevron-down" : "chevron-up",
126
- className: cn("text-xs", !active && "opacity-60")
125
+ name: "chevron-up",
126
+ className: cn(
127
+ "text-xs motion-interactive",
128
+ active && sort.direction === "desc" && "rotate-180",
129
+ !active && "opacity-60"
130
+ )
127
131
  }
128
132
  )
129
133
  ]
@@ -139,6 +139,38 @@ var dataTableMeta = {
139
139
  }
140
140
  ],
141
141
  tokensUsed: ["muted-foreground", "foreground", "ring"],
142
+ motion: [
143
+ {
144
+ slot: "arrival",
145
+ recipe: "none",
146
+ part: "Root",
147
+ why: "Rows render with the page or the query that produced them. Staggering rows in delays reading a table someone opened to scan."
148
+ },
149
+ {
150
+ slot: "response",
151
+ recipe: "motion-interactive",
152
+ part: "Row",
153
+ why: "Body rows ease to a muted wash under the pointer (TableRow's recipe), which keeps the eye on the right line across a wide row of numbers. Sortable headers are ghost Buttons and answer on Button's recipe."
154
+ },
155
+ {
156
+ slot: "transition",
157
+ recipe: "motion-interactive",
158
+ part: "Sort chevron",
159
+ why: "One chevron turns to show direction instead of two glyphs swapping, so ascending to descending reads as the same column reversing. Its opacity rises from the resting 60% as the column becomes the sort."
160
+ },
161
+ {
162
+ slot: "transition",
163
+ recipe: "motion-interactive",
164
+ part: "Row",
165
+ why: "A checked row's fill eases in across its cells, so selecting by checkbox reads as the row being picked rather than a band stamped over it."
166
+ },
167
+ {
168
+ slot: "activity",
169
+ recipe: "none",
170
+ part: "Root",
171
+ why: "DataTable has no loading state. While data is fetched, render Skeleton rows (motion-shimmer) in its place \u2014 only the caller knows a fetch is in flight."
172
+ }
173
+ ],
142
174
  a11y: [
143
175
  "Sorted columns set aria-sort=ascending/descending on the header cell.",
144
176
  "Sortable headers are real <button>s with a focus-visible ring, keyboard-operable.",
@@ -86,6 +86,32 @@ var entityMeta = {
86
86
  density: ["compact", "comfortable"]
87
87
  },
88
88
  tokensUsed: ["accent", "accent-foreground", "foreground", "muted-foreground", "ring"],
89
+ motion: [
90
+ {
91
+ slot: "arrival",
92
+ recipe: "none",
93
+ part: "Root",
94
+ why: "Rows arrive with the list they belong to. Entering one at a time is a stagger, and a stagger on a list someone opened to scan is a delay."
95
+ },
96
+ {
97
+ slot: "response",
98
+ recipe: "motion-interactive",
99
+ part: "Root",
100
+ why: "Only the interactive row reacts: its hover fill, a step off the surface it sits on, eases in. A static row never fakes clickability, and an interactive one answers the pointer without flicker as it moves down a list."
101
+ },
102
+ {
103
+ slot: "transition",
104
+ recipe: "motion-interactive",
105
+ part: "Root",
106
+ why: "`selected` eases the fill onto the current row of a list\u2013detail view, so opening another record reads as the selection moving rather than two rows flashing."
107
+ },
108
+ {
109
+ slot: "activity",
110
+ recipe: "none",
111
+ part: "Root",
112
+ why: "A record row has nothing of its own to wait for. A row whose action is pending puts a loading Button in `action`."
113
+ }
114
+ ],
89
115
  a11y: [
90
116
  "Default rows are presentational layout; semantics come from the content you place in title/subtitle/action.",
91
117
  "interactive rows render a real <button> around the leading visual and text, with its ::after stretched over the whole row \u2014 native focus, a focus-visible ring, and Enter/Space. The action slot sits outside that button and above the overlay, so a Button there stays valid HTML and keeps its own clicks.",
@@ -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
  };
@@ -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",
@@ -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
  };