@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.
- package/AGENTS.md +2 -2
- package/CHANGELOG.md +172 -0
- package/CONTRIBUTING.md +6 -4
- package/DESIGN.md +10 -3
- package/dist/blocks/approval-block.js +93 -0
- package/dist/blocks/approval-block.meta.js +23 -0
- package/dist/blocks/index.d.ts +27 -1
- package/dist/blocks/index.js +8 -0
- package/dist/blocks/message-reactions-block.js +87 -0
- package/dist/blocks/message-reactions-block.meta.js +23 -0
- package/dist/components/accordion/accordion.meta.js +8 -2
- package/dist/components/area-chart/area-chart.meta.js +10 -3
- package/dist/components/attachment/attachment.meta.js +26 -0
- package/dist/components/bar-chart/bar-chart.meta.js +11 -4
- package/dist/components/bottom-nav/bottom-nav.js +3 -1
- package/dist/components/bottom-nav/bottom-nav.meta.js +32 -0
- package/dist/components/breadcrumbs/breadcrumbs.meta.js +26 -0
- package/dist/components/bubble/bubble.js +105 -5
- package/dist/components/bubble/bubble.meta.js +42 -6
- package/dist/components/bubble/bubble.variants.js +1 -1
- package/dist/components/bubble/index.js +2 -0
- package/dist/components/button/button.js +21 -4
- package/dist/components/button/button.meta.js +7 -7
- package/dist/components/calendar/calendar.js +18 -1
- package/dist/components/calendar/calendar.meta.js +38 -0
- package/dist/components/carousel/carousel.js +6 -4
- package/dist/components/carousel/carousel.meta.js +42 -0
- package/dist/components/chat-composer/chat-composer.js +21 -21
- package/dist/components/chat-composer/chat-composer.meta.js +38 -0
- package/dist/components/chat-composer/use-eased-growth.js +70 -0
- package/dist/components/checkbox/checkbox-mark.js +8 -1
- package/dist/components/checkbox/checkbox.js +3 -1
- package/dist/components/checkbox/checkbox.meta.js +32 -0
- package/dist/components/checkbox-card/checkbox-card.js +3 -0
- package/dist/components/checkbox-card/checkbox-card.meta.js +32 -0
- package/dist/components/code-block/code-block.js +14 -3
- package/dist/components/code-block/code-block.meta.js +28 -1
- package/dist/components/collapsible/collapsible.meta.js +32 -0
- package/dist/components/color-field/color-field.meta.js +26 -0
- package/dist/components/combobox/combobox.meta.js +44 -0
- package/dist/components/command-menu/command-menu.js +8 -3
- package/dist/components/command-menu/command-menu.meta.js +38 -0
- package/dist/components/confirm-button/confirm-button.js +179 -0
- package/dist/components/confirm-button/confirm-button.meta.js +143 -0
- package/dist/components/confirm-button/index.js +7 -0
- package/dist/components/data-table/data-table.js +6 -2
- package/dist/components/data-table/data-table.meta.js +32 -0
- package/dist/components/entity/entity.meta.js +26 -0
- package/dist/components/feedback/feedback.js +120 -47
- package/dist/components/feedback/feedback.meta.js +55 -4
- package/dist/components/input-group/input-group.meta.js +27 -1
- package/dist/components/like-button/index.js +7 -0
- package/dist/components/like-button/like-button.js +89 -0
- package/dist/components/like-button/like-button.meta.js +132 -0
- package/dist/components/line-chart/line-chart.meta.js +10 -3
- package/dist/components/middle-truncate/middle-truncate.meta.js +1 -1
- package/dist/components/note/note.meta.js +32 -0
- package/dist/components/notification-badge/notification-badge.js +17 -5
- package/dist/components/notification-badge/notification-badge.meta.js +34 -2
- package/dist/components/number-ticker/index.js +7 -0
- package/dist/components/number-ticker/number-ticker.js +246 -0
- package/dist/components/number-ticker/number-ticker.meta.js +159 -0
- package/dist/components/otp-field/otp-field.js +23 -3
- package/dist/components/otp-field/otp-field.meta.js +23 -2
- package/dist/components/panel-group/panel-group.meta.js +26 -0
- package/dist/components/panel-section/panel-section.js +37 -10
- package/dist/components/panel-section/panel-section.meta.js +32 -0
- package/dist/components/pie-chart/pie-chart.js +20 -11
- package/dist/components/pie-chart/pie-chart.meta.js +17 -4
- package/dist/components/pill/pill.meta.js +32 -0
- package/dist/components/radar-chart/radar-chart.js +4 -3
- package/dist/components/radar-chart/radar-chart.meta.js +10 -3
- package/dist/components/radial-chart/radial-chart.js +22 -22
- package/dist/components/radial-chart/radial-chart.meta.js +17 -4
- package/dist/components/radio/radio.meta.js +26 -0
- package/dist/components/radio-card/radio-card.meta.js +38 -0
- package/dist/components/reaction-picker/index.js +13 -0
- package/dist/components/reaction-picker/reaction-picker.js +104 -0
- package/dist/components/reaction-picker/reaction-picker.meta.js +156 -0
- package/dist/components/relative-time/relative-time.meta.js +32 -0
- package/dist/components/scroller/scroller.meta.js +32 -0
- package/dist/components/scrub-field/scrub-field.meta.js +32 -0
- package/dist/components/search-input/search-input.js +9 -7
- package/dist/components/search-input/search-input.meta.js +26 -0
- package/dist/components/show-more/show-more.js +1 -4
- package/dist/components/show-more/show-more.meta.js +38 -0
- package/dist/components/side-nav/side-nav.js +21 -1
- package/dist/components/side-nav/side-nav.meta.js +32 -0
- package/dist/components/side-nav/side-nav.variants.js +3 -1
- package/dist/components/sidebar/sidebar.js +31 -8
- package/dist/components/sidebar/sidebar.meta.js +38 -0
- package/dist/components/slider/slider.meta.js +26 -0
- package/dist/components/sparkline/sparkline.js +7 -4
- package/dist/components/sparkline/sparkline.meta.js +9 -2
- package/dist/components/table/table.meta.js +26 -0
- package/dist/components/tabs/tabs.js +27 -8
- package/dist/components/tabs/tabs.meta.js +46 -3
- package/dist/components/theme-selector/theme-selector.meta.js +32 -0
- package/dist/components/theme-switcher/theme-switcher.js +4 -1
- package/dist/components/theme-switcher/theme-switcher.meta.js +42 -0
- package/dist/components/toast/toast.js +25 -13
- package/dist/components/toast/toast.meta.js +7 -1
- package/dist/components/toggle/toggle.js +28 -5
- package/dist/components/toggle/toggle.meta.js +44 -2
- package/dist/components/tool-call/index.js +8 -0
- package/dist/components/tool-call/tool-call.js +117 -0
- package/dist/components/tool-call/tool-call.meta.js +147 -0
- package/dist/components/tool-call/tool-call.variants.js +33 -0
- package/dist/components/tooltip/index.js +3 -1
- package/dist/components/tooltip/tooltip.js +84 -0
- package/dist/components/tooltip/tooltip.meta.js +16 -1
- package/dist/components/tree/tree.js +71 -6
- package/dist/components/tree/tree.meta.js +32 -0
- package/dist/components/video/video.meta.js +32 -0
- package/dist/file-hashes.json +130 -104
- package/dist/foundations/motion.js +69 -3
- package/dist/icon-sets/reicon.js +11 -1
- package/dist/index.d.ts +907 -104
- package/dist/index.js +6 -1
- package/dist/lib/chart/cartesian.js +9 -10
- package/dist/lib/chart/chart-center.js +30 -0
- package/dist/lib/chart/chart-frame.js +15 -8
- package/dist/lib/chart/dim.js +13 -0
- package/dist/lib/chart/format.js +7 -1
- package/dist/lib/chart/motion.js +6 -4
- package/dist/lib/highlight-glide.js +68 -10
- package/dist/lib/taxonomy.js +5 -0
- package/dist/motion/Burst.js +79 -0
- package/dist/motion/MotionProvider.js +45 -3
- package/dist/motion/Reveal.js +1 -1
- package/dist/motion/TextReveal.js +1 -1
- package/dist/motion/index.js +17 -2
- package/dist/motion/useAutoSize.js +42 -0
- package/dist/motion/useShake.js +25 -0
- package/dist/styles.css +439 -5
- package/llms/blocks/approval-block.md +128 -0
- package/llms/blocks/message-reactions-block.md +116 -0
- package/llms/components/accordion.md +3 -2
- package/llms/components/area-chart.md +4 -3
- package/llms/components/attachment.md +7 -0
- package/llms/components/bar-chart.md +5 -4
- package/llms/components/bottom-nav.md +8 -0
- package/llms/components/breadcrumbs.md +7 -0
- package/llms/components/bubble.md +17 -7
- package/llms/components/button.md +6 -6
- package/llms/components/calendar.md +9 -0
- package/llms/components/carousel.md +17 -0
- package/llms/components/chat-composer.md +9 -0
- package/llms/components/checkbox-card.md +8 -0
- package/llms/components/checkbox.md +8 -0
- package/llms/components/code-block.md +8 -0
- package/llms/components/collapsible.md +8 -0
- package/llms/components/color-field.md +7 -0
- package/llms/components/combobox.md +10 -0
- package/llms/components/command-menu.md +9 -0
- package/llms/components/confirm-button.md +92 -0
- package/llms/components/data-table.md +8 -0
- package/llms/components/entity.md +7 -0
- package/llms/components/feedback.md +23 -3
- package/llms/components/input-group.md +8 -1
- package/llms/components/like-button.md +91 -0
- package/llms/components/line-chart.md +4 -3
- package/llms/components/middle-truncate.md +1 -1
- package/llms/components/note.md +8 -0
- package/llms/components/notification-badge.md +10 -2
- package/llms/components/number-ticker.md +86 -0
- package/llms/components/otp-field.md +15 -1
- package/llms/components/panel-group.md +7 -0
- package/llms/components/panel-section.md +8 -0
- package/llms/components/pie-chart.md +6 -4
- package/llms/components/pill.md +8 -0
- package/llms/components/radar-chart.md +4 -3
- package/llms/components/radial-chart.md +6 -4
- package/llms/components/radio-card.md +9 -0
- package/llms/components/radio.md +7 -0
- package/llms/components/reaction-picker.md +115 -0
- package/llms/components/relative-time.md +8 -0
- package/llms/components/scroller.md +8 -0
- package/llms/components/scrub-field.md +8 -0
- package/llms/components/search-input.md +7 -0
- package/llms/components/show-more.md +9 -0
- package/llms/components/side-nav.md +8 -0
- package/llms/components/sidebar.md +9 -0
- package/llms/components/slider.md +7 -0
- package/llms/components/sparkline.md +3 -2
- package/llms/components/table.md +7 -0
- package/llms/components/tabs.md +20 -2
- package/llms/components/theme-selector.md +8 -0
- package/llms/components/theme-switcher.md +17 -0
- package/llms/components/toast.md +2 -1
- package/llms/components/toggle.md +19 -2
- package/llms/components/tool-call.md +115 -0
- package/llms/components/tooltip.md +11 -1
- package/llms/components/tree.md +8 -0
- package/llms/components/video.md +8 -0
- package/llms/foundations/motion.md +64 -3
- package/llms/reference/utilities.md +26 -1
- package/llms/tasks/reviewing-motion.md +14 -0
- package/llms-full.txt +1337 -69
- package/llms.txt +19 -11
- package/package.json +1 -1
- package/registry.json +3040 -124
- package/targets/ios.json +45 -0
|
@@ -92,6 +92,38 @@ var collapsibleMeta = {
|
|
|
92
92
|
}
|
|
93
93
|
],
|
|
94
94
|
tokensUsed: ["foreground", "muted-foreground"],
|
|
95
|
+
motion: [
|
|
96
|
+
{
|
|
97
|
+
slot: "arrival",
|
|
98
|
+
recipe: "none",
|
|
99
|
+
part: "Root",
|
|
100
|
+
why: "A collapsible is part of the page it sits in, present from first paint. Its trigger is what the reader notices, not an entrance."
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
slot: "response",
|
|
104
|
+
recipe: "none",
|
|
105
|
+
part: "CollapsibleTrigger",
|
|
106
|
+
why: "The trigger is whatever the consumer renders (usually a Button), and that control brings its own response. Adding one here would double it."
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
slot: "transition",
|
|
110
|
+
recipe: "motion-disclosure",
|
|
111
|
+
part: "CollapsiblePanel",
|
|
112
|
+
why: "The height change ties the content that appears to the trigger that was pressed; snapped open, everything below jumps with nothing linking cause to effect."
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
slot: "transition",
|
|
116
|
+
recipe: "motion-interactive",
|
|
117
|
+
part: "CollapsibleIndicator",
|
|
118
|
+
why: "The chevron turns with the panel rather than flipping in one frame: motion-interactive lists `rotate`, the property rotate-180 writes."
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
slot: "activity",
|
|
122
|
+
recipe: "none",
|
|
123
|
+
part: "Root",
|
|
124
|
+
why: "Nothing here is indeterminate; content that loads on open should put its own loader inside the panel."
|
|
125
|
+
}
|
|
126
|
+
],
|
|
95
127
|
a11y: [
|
|
96
128
|
"Base UI wires aria-expanded on the trigger and aria-controls/id between trigger and panel.",
|
|
97
129
|
"Trigger is a real <button>; toggles on click and Enter/Space.",
|
|
@@ -95,6 +95,32 @@ var colorFieldMeta = {
|
|
|
95
95
|
description: "Accessible name for the trigger; the current value is appended to it."
|
|
96
96
|
}
|
|
97
97
|
],
|
|
98
|
+
motion: [
|
|
99
|
+
{
|
|
100
|
+
slot: "arrival",
|
|
101
|
+
recipe: "motion-pop",
|
|
102
|
+
part: "Popover",
|
|
103
|
+
why: "The picker grows out of the swatch it edits (PopoverContent's recipe, its origin on the anchor). In a property panel with a column of swatches, that origin is what tells you which colour you are now editing."
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
slot: "response",
|
|
107
|
+
recipe: "motion-interactive",
|
|
108
|
+
part: "Swatch",
|
|
109
|
+
why: "Preset swatches lift slightly under the pointer. They are 20px chips of colour with no label, and the eased lift is what tells you which one a click will take."
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
slot: "transition",
|
|
113
|
+
recipe: "none",
|
|
114
|
+
part: "Handles",
|
|
115
|
+
why: "The saturation, hue and alpha handles follow the pointer exactly. A handle that trailed the drag would misreport the colour under the cursor, and a swatch or a typed hex moving them is the value changing, which the field should show at once."
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
slot: "activity",
|
|
119
|
+
recipe: "none",
|
|
120
|
+
part: "Root",
|
|
121
|
+
why: "Picking is synchronous. The EyeDropper is the browser's own modal, with its own feedback."
|
|
122
|
+
}
|
|
123
|
+
],
|
|
98
124
|
a11y: [
|
|
99
125
|
`The trigger's accessible name includes the current value, so a screen reader announces the colour rather than just "button" \u2014 the swatch itself conveys nothing without sight.`,
|
|
100
126
|
"The Transparent entry is a toggle with aria-pressed, because it is a state of the value and not a navigation action.",
|
|
@@ -203,6 +203,50 @@ var comboboxMeta = {
|
|
|
203
203
|
"secondary",
|
|
204
204
|
"secondary-foreground"
|
|
205
205
|
],
|
|
206
|
+
motion: [
|
|
207
|
+
{
|
|
208
|
+
slot: "arrival",
|
|
209
|
+
recipe: "motion-pop",
|
|
210
|
+
part: "ComboboxContent",
|
|
211
|
+
why: "The list grows from the field it filters, its origin on Base UI's anchor. With several comboboxes in a form, that is what attaches the open list to the one being typed in."
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
slot: "response",
|
|
215
|
+
recipe: "motion-interactive",
|
|
216
|
+
part: "ComboboxItem",
|
|
217
|
+
why: "A row's ink eases as the pointer or the arrow keys reach it, so moving through a long filtered list reads as continuous movement rather than rows blinking."
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
slot: "response",
|
|
221
|
+
recipe: "custom",
|
|
222
|
+
part: "Highlight",
|
|
223
|
+
why: "One highlight travels to the active row (lib/highlight-glide: a measured translate on the motion tokens) instead of each row painting its own fill. When the list re-filters under the caret, the highlight sliding to the new match is what shows the keyboard target moved."
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
slot: "response",
|
|
227
|
+
recipe: "motion-interactive",
|
|
228
|
+
part: "Toggle",
|
|
229
|
+
why: "The chevron toggle at the field's edge eases its ink on hover; the change is what tells you it is a separate target from the text box."
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
slot: "transition",
|
|
233
|
+
recipe: "motion-interactive",
|
|
234
|
+
part: "Toggle",
|
|
235
|
+
why: "The chevron turns 180\xB0 as the list opens and closes (motion-interactive lists `rotate`), in step with the popup, so it reads as one control changing direction."
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
slot: "transition",
|
|
239
|
+
recipe: "motion-pop",
|
|
240
|
+
part: "ComboboxContent",
|
|
241
|
+
why: "Closing shrinks back toward the field, returning the eye to the input that now holds the value."
|
|
242
|
+
},
|
|
243
|
+
{
|
|
244
|
+
slot: "activity",
|
|
245
|
+
recipe: "none",
|
|
246
|
+
part: "Root",
|
|
247
|
+
why: "Combobox ships no loading part. A list fetched as you type shows a Spinner (motion-spin) in ComboboxEmpty or the content \u2014 where the results will land \u2014 and only the caller knows a fetch is in flight."
|
|
248
|
+
}
|
|
249
|
+
],
|
|
206
250
|
a11y: [
|
|
207
251
|
"Base UI provides role=combobox/listbox/option, roving tabindex, arrow-key navigation, and type-to-filter.",
|
|
208
252
|
"Esc and outside-press dismiss; focus returns to the input on close.",
|
|
@@ -187,8 +187,13 @@ function CommandMenuBody({
|
|
|
187
187
|
),
|
|
188
188
|
query ? (
|
|
189
189
|
// Empties the query and hands focus back to the field, which owns
|
|
190
|
-
// the keyboard (the list is driven from it).
|
|
191
|
-
|
|
190
|
+
// the keyboard (the list is driven from it). It mounts on the first
|
|
191
|
+
// keystroke, beside the caret the eye is on, so the wrapper fades it
|
|
192
|
+
// in (motion-appear) rather than letting it blink into the header.
|
|
193
|
+
// On the wrapper, not the Button: motion-appear declares its own
|
|
194
|
+
// transition list and would fight motion-interactive for the one
|
|
195
|
+
// transition-property slot. Removal stays instant.
|
|
196
|
+
/* @__PURE__ */ jsx("span", { className: "flex motion-appear", children: /* @__PURE__ */ jsx(
|
|
192
197
|
Button,
|
|
193
198
|
{
|
|
194
199
|
variant: "ghost",
|
|
@@ -203,7 +208,7 @@ function CommandMenuBody({
|
|
|
203
208
|
},
|
|
204
209
|
children: /* @__PURE__ */ jsx(Icon, { name: "xmark" })
|
|
205
210
|
}
|
|
206
|
-
)
|
|
211
|
+
) })
|
|
207
212
|
) : null
|
|
208
213
|
] }),
|
|
209
214
|
/* @__PURE__ */ jsx("div", { role: "status", "aria-live": "polite", className: "sr-only", children: resultCount === 0 ? "No results" : `${resultCount} result${resultCount === 1 ? "" : "s"}` }),
|
|
@@ -229,6 +229,44 @@ var commandMenuMeta = {
|
|
|
229
229
|
"accent",
|
|
230
230
|
"accent-foreground"
|
|
231
231
|
],
|
|
232
|
+
motion: [
|
|
233
|
+
{
|
|
234
|
+
slot: "arrival",
|
|
235
|
+
recipe: "motion-pop",
|
|
236
|
+
part: "Dialog",
|
|
237
|
+
why: "The palette opens on DialogContent's recipe over a fading backdrop. It is summoned by a keystroke from nowhere on screen, and the short scale-in is what says a layer arrived rather than the page changing. The inline CommandMenuPanel has no entrance: it is part of the page."
|
|
238
|
+
},
|
|
239
|
+
{
|
|
240
|
+
slot: "arrival",
|
|
241
|
+
recipe: "motion-appear",
|
|
242
|
+
part: "Clear button",
|
|
243
|
+
why: "The clear button mounts on the first keystroke, beside the caret the eye is already on. Fading in, it reads as the field offering a way back instead of a control blinking into the header. Removal is instant, so an emptied query does not wait on its X."
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
slot: "response",
|
|
247
|
+
recipe: "motion-interactive",
|
|
248
|
+
part: "CommandItem",
|
|
249
|
+
why: "The active row follows both the arrow keys and the pointer, and its fill eases between rows, so scanning results while typing reads as one highlight moving rather than rows flashing in turn."
|
|
250
|
+
},
|
|
251
|
+
{
|
|
252
|
+
slot: "response",
|
|
253
|
+
recipe: "motion-interactive",
|
|
254
|
+
part: "CommandMenuTrigger",
|
|
255
|
+
why: "The search-shaped trigger eases its ink on hover like the field shell it imitates, so it reads as the field it opens."
|
|
256
|
+
},
|
|
257
|
+
{
|
|
258
|
+
slot: "transition",
|
|
259
|
+
recipe: "motion-pop",
|
|
260
|
+
part: "Dialog",
|
|
261
|
+
why: "Closing on Enter or Esc shrinks the palette away fast and returns focus where it came from; the exit is the confirmation that the command ran or was dismissed."
|
|
262
|
+
},
|
|
263
|
+
{
|
|
264
|
+
slot: "activity",
|
|
265
|
+
recipe: "none",
|
|
266
|
+
part: "List",
|
|
267
|
+
why: "Results are filtered synchronously from the items passed in. Async results are the caller's to mark with a Spinner (motion-spin) in the list, where the person is already looking."
|
|
268
|
+
}
|
|
269
|
+
],
|
|
232
270
|
a11y: [
|
|
233
271
|
"CommandMenuTrigger is a real <button> named by its visible placeholder, with aria-haspopup=dialog and aria-keyshortcuts (Meta+K on macOS, Control+K elsewhere). Its Kbd hint is aria-hidden, so the shortcut is announced once rather than read as part of the name.",
|
|
234
272
|
"The shortcut never fires from inside a text field, textarea, select or contenteditable (Ctrl+K is delete-to-end-of-line in macOS text views), except the palette's own input.",
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// src/components/confirm-button/confirm-button.tsx
|
|
4
|
+
import {
|
|
5
|
+
useCallback,
|
|
6
|
+
useContext,
|
|
7
|
+
useEffect,
|
|
8
|
+
useRef,
|
|
9
|
+
useState
|
|
10
|
+
} from "react";
|
|
11
|
+
import { cn } from "../../lib/cn.js";
|
|
12
|
+
import { isPromise } from "../../lib/is-promise.js";
|
|
13
|
+
import { StackedActionsContext } from "../../lib/presentation.js";
|
|
14
|
+
import { motionAttribute } from "../../motion/MotionProvider.js";
|
|
15
|
+
import { useAutoSize } from "../../motion/useAutoSize.js";
|
|
16
|
+
import { Button } from "../button/button.js";
|
|
17
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
18
|
+
var OPTICAL_START = "[--button-ps:calc(var(--button-px)-(--spacing(0.5)))] ps-(--button-ps)";
|
|
19
|
+
var DOUBLE_PRESS_GUARD_MS = 300;
|
|
20
|
+
function defaultConfirmLabel(label) {
|
|
21
|
+
if (typeof label !== "string" || label.trim() === "") return "Confirm?";
|
|
22
|
+
return `Confirm ${label.charAt(0).toLowerCase()}${label.slice(1)}?`;
|
|
23
|
+
}
|
|
24
|
+
function ConfirmButton({
|
|
25
|
+
children,
|
|
26
|
+
confirmLabel,
|
|
27
|
+
onConfirm,
|
|
28
|
+
timeout = 3e3,
|
|
29
|
+
variant = "secondary",
|
|
30
|
+
confirmVariant = "destructive",
|
|
31
|
+
size,
|
|
32
|
+
icon,
|
|
33
|
+
confirmHint = "Press again to confirm.",
|
|
34
|
+
motion,
|
|
35
|
+
disabled,
|
|
36
|
+
className,
|
|
37
|
+
onBlur,
|
|
38
|
+
onKeyDown,
|
|
39
|
+
ref,
|
|
40
|
+
...props
|
|
41
|
+
}) {
|
|
42
|
+
const stacked = useContext(StackedActionsContext);
|
|
43
|
+
const callerSized = /(^|\s)([\w-]+:)*(w-|flex-1|grow(\s|$)|basis-)/.test(className ?? "");
|
|
44
|
+
const autoSize = useAutoSize({ enabled: !stacked && !callerSized });
|
|
45
|
+
const [phase, setPhase] = useState("idle");
|
|
46
|
+
const armedAt = useRef(0);
|
|
47
|
+
const rootRef = useRef(null);
|
|
48
|
+
const armed = phase !== "idle";
|
|
49
|
+
const prompt = confirmLabel ?? defaultConfirmLabel(children);
|
|
50
|
+
const disarm = useCallback(() => setPhase((p) => p === "armed" ? "idle" : p), []);
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
if (phase !== "armed") return;
|
|
53
|
+
const timer = setTimeout(disarm, timeout);
|
|
54
|
+
return () => clearTimeout(timer);
|
|
55
|
+
}, [phase, timeout, disarm]);
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
if (phase !== "armed") return;
|
|
58
|
+
const onPointerDown = (event) => {
|
|
59
|
+
if (!rootRef.current?.contains(event.target)) disarm();
|
|
60
|
+
};
|
|
61
|
+
document.addEventListener("pointerdown", onPointerDown, true);
|
|
62
|
+
return () => document.removeEventListener("pointerdown", onPointerDown, true);
|
|
63
|
+
}, [phase, disarm]);
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
if (disabled) setPhase((p) => p === "armed" ? "idle" : p);
|
|
66
|
+
}, [disabled]);
|
|
67
|
+
const handleClick = (_event) => {
|
|
68
|
+
if (phase === "idle") {
|
|
69
|
+
armedAt.current = Date.now();
|
|
70
|
+
setPhase("armed");
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
if (phase !== "armed" || Date.now() - armedAt.current < DOUBLE_PRESS_GUARD_MS) return;
|
|
74
|
+
const result = onConfirm();
|
|
75
|
+
if (isPromise(result)) {
|
|
76
|
+
setPhase("confirming");
|
|
77
|
+
const idle = () => setPhase("idle");
|
|
78
|
+
result.then(idle, idle);
|
|
79
|
+
return result;
|
|
80
|
+
}
|
|
81
|
+
setPhase("idle");
|
|
82
|
+
return result;
|
|
83
|
+
};
|
|
84
|
+
const handleKeyDown = (event) => {
|
|
85
|
+
if (event.key === "Escape" && phase === "armed") {
|
|
86
|
+
event.preventDefault();
|
|
87
|
+
event.stopPropagation();
|
|
88
|
+
disarm();
|
|
89
|
+
}
|
|
90
|
+
onKeyDown?.(event);
|
|
91
|
+
};
|
|
92
|
+
const handleBlur = (event) => {
|
|
93
|
+
disarm();
|
|
94
|
+
onBlur?.(event);
|
|
95
|
+
};
|
|
96
|
+
const setRefs = (node) => {
|
|
97
|
+
rootRef.current = node;
|
|
98
|
+
autoSize.outerRef.current = node;
|
|
99
|
+
if (typeof ref === "function") ref(node);
|
|
100
|
+
else if (ref) ref.current = node;
|
|
101
|
+
};
|
|
102
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
103
|
+
/* @__PURE__ */ jsxs(
|
|
104
|
+
Button,
|
|
105
|
+
{
|
|
106
|
+
...props,
|
|
107
|
+
ref: setRefs,
|
|
108
|
+
size,
|
|
109
|
+
variant: armed ? confirmVariant : variant,
|
|
110
|
+
disabled,
|
|
111
|
+
"data-state": armed ? "armed" : "idle",
|
|
112
|
+
"data-motion": motionAttribute(motion),
|
|
113
|
+
className: cn(
|
|
114
|
+
// overflow-clip: while the width eases, the content is already at
|
|
115
|
+
// its new size, so it is clipped to the button rather than painting
|
|
116
|
+
// past its fill. A ring is a box-shadow and is not clipped.
|
|
117
|
+
"relative overflow-clip motion-size",
|
|
118
|
+
// The icon lives inside the measured span (below), not in Button's
|
|
119
|
+
// iconStart slot, so Button's optical start padding is restated.
|
|
120
|
+
icon != null && OPTICAL_START,
|
|
121
|
+
className
|
|
122
|
+
),
|
|
123
|
+
onClick: handleClick,
|
|
124
|
+
onKeyDown: handleKeyDown,
|
|
125
|
+
onBlur: handleBlur,
|
|
126
|
+
children: [
|
|
127
|
+
/* @__PURE__ */ jsxs(
|
|
128
|
+
"span",
|
|
129
|
+
{
|
|
130
|
+
ref: autoSize.innerRef,
|
|
131
|
+
className: "inline-flex w-max items-center gap-(--control-gap) transition-opacity duration-(--motion-duration-fast) ease-standard",
|
|
132
|
+
children: [
|
|
133
|
+
icon,
|
|
134
|
+
/* @__PURE__ */ jsxs("span", { className: "inline-grid overflow-x-clip", children: [
|
|
135
|
+
/* @__PURE__ */ jsx(ConfirmLabel, { active: !armed, children }),
|
|
136
|
+
/* @__PURE__ */ jsx(ConfirmLabel, { active: armed, children: prompt })
|
|
137
|
+
] })
|
|
138
|
+
]
|
|
139
|
+
}
|
|
140
|
+
),
|
|
141
|
+
phase === "armed" ? /* @__PURE__ */ jsx(
|
|
142
|
+
"span",
|
|
143
|
+
{
|
|
144
|
+
"aria-hidden": "true",
|
|
145
|
+
"data-slot": "confirm-countdown",
|
|
146
|
+
className: "pointer-events-none absolute inset-x-(--button-px) bottom-1 h-0.5 origin-left rounded-full bg-current opacity-40 transition-[scale] ease-linear scale-x-0 starting:scale-x-100 rtl:origin-right",
|
|
147
|
+
style: {
|
|
148
|
+
transitionDuration: `calc(${timeout}ms * var(--motion-scale, 1) * var(--motion-expressive, 1))`
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
) : null
|
|
152
|
+
]
|
|
153
|
+
}
|
|
154
|
+
),
|
|
155
|
+
/* @__PURE__ */ jsx("span", { role: "status", className: "sr-only", children: phase === "armed" ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
156
|
+
prompt,
|
|
157
|
+
" ",
|
|
158
|
+
confirmHint
|
|
159
|
+
] }) : null })
|
|
160
|
+
] });
|
|
161
|
+
}
|
|
162
|
+
ConfirmButton.displayName = "ConfirmButton";
|
|
163
|
+
function ConfirmLabel({ active, children }) {
|
|
164
|
+
return /* @__PURE__ */ jsx(
|
|
165
|
+
"span",
|
|
166
|
+
{
|
|
167
|
+
"aria-hidden": active ? void 0 : "true",
|
|
168
|
+
"data-active": active,
|
|
169
|
+
className: cn(
|
|
170
|
+
"col-start-1 row-start-1 flex justify-center transition-opacity duration-(--motion-duration-fast) ease-standard",
|
|
171
|
+
active ? "w-max" : "w-0 opacity-0"
|
|
172
|
+
),
|
|
173
|
+
children: /* @__PURE__ */ jsx("span", { className: "sable-label-trim", children })
|
|
174
|
+
}
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
export {
|
|
178
|
+
ConfirmButton
|
|
179
|
+
};
|
|
@@ -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
|
+
};
|
|
@@ -122,8 +122,12 @@ function DataTable({
|
|
|
122
122
|
/* @__PURE__ */ jsx(
|
|
123
123
|
Icon,
|
|
124
124
|
{
|
|
125
|
-
name:
|
|
126
|
-
className: cn(
|
|
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.",
|