@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.
- package/AGENTS.md +2 -2
- package/CHANGELOG.md +132 -0
- package/CONTRIBUTING.md +7 -5
- package/DESIGN.md +8 -3
- package/bin/sable.mjs +17 -1
- package/dist/blocks/approval-block.js +93 -0
- package/dist/blocks/approval-block.meta.js +23 -0
- package/dist/blocks/index.d.ts +17 -2
- package/dist/blocks/index.js +4 -0
- package/dist/components/accordion/accordion.meta.js +10 -3
- package/dist/components/area-chart/area-chart.meta.js +10 -3
- package/dist/components/attachment/attachment.meta.js +26 -0
- package/dist/components/badge/badge.meta.js +2 -1
- package/dist/components/banner/banner.meta.js +4 -2
- 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/button/button.js +21 -4
- package/dist/components/button/button.meta.js +7 -7
- package/dist/components/button-group/button-group.meta.js +2 -1
- 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 +34 -1
- 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 +34 -1
- package/dist/components/color-field/color-field.meta.js +26 -0
- package/dist/components/combobox/combobox.meta.js +48 -2
- package/dist/components/command-menu/command-menu.js +8 -3
- package/dist/components/command-menu/command-menu.meta.js +44 -3
- 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/context-card/context-card.meta.js +2 -1
- package/dist/components/data-table/data-table.js +6 -2
- package/dist/components/data-table/data-table.meta.js +32 -0
- package/dist/components/dialog/dialog.meta.js +6 -3
- package/dist/components/disclosure-picker/disclosure-picker.meta.js +2 -1
- package/dist/components/entity/entity.meta.js +26 -0
- package/dist/components/feedback/feedback.js +113 -47
- package/dist/components/feedback/feedback.meta.js +49 -4
- package/dist/components/inline-status/inline-status.meta.js +2 -1
- package/dist/components/input-group/input-group.meta.js +27 -1
- package/dist/components/line-chart/line-chart.meta.js +10 -3
- package/dist/components/menu/menu.meta.js +2 -1
- package/dist/components/menubar/menubar.meta.js +2 -1
- package/dist/components/meter/meter.meta.js +2 -1
- package/dist/components/middle-truncate/middle-truncate.meta.js +1 -1
- package/dist/components/note/note.meta.js +34 -1
- 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/page-loader/page-loader.meta.js +2 -1
- 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 +34 -1
- package/dist/components/popover/popover.meta.js +4 -2
- package/dist/components/progress/progress.meta.js +2 -1
- 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 +28 -1
- package/dist/components/radio-card/radio-card.meta.js +38 -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/segmented-bar/segmented-bar.meta.js +2 -1
- package/dist/components/select/select.meta.js +4 -2
- package/dist/components/sheet/sheet.meta.js +4 -2
- 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 +34 -1
- 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/skeleton/skeleton.meta.js +2 -1
- 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/spinner/spinner.meta.js +2 -1
- package/dist/components/stepper/stepper.meta.js +4 -2
- package/dist/components/switch/switch.meta.js +2 -1
- package/dist/components/table/table.meta.js +28 -1
- package/dist/components/tabs/tabs.js +27 -8
- package/dist/components/tabs/tabs.meta.js +50 -5
- 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/thinking-orb/thinking-orb.meta.js +4 -2
- package/dist/components/toast/toast.meta.js +2 -1
- package/dist/components/toggle/toggle.js +28 -5
- package/dist/components/toggle/toggle.meta.js +46 -3
- 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/toolbar/toolbar.meta.js +2 -1
- package/dist/components/tooltip/index.js +3 -1
- package/dist/components/tooltip/tooltip.js +84 -0
- package/dist/components/tooltip/tooltip.meta.js +18 -2
- 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 +139 -122
- package/dist/foundations/motion.js +49 -3
- package/dist/index.d.ts +616 -46
- package/dist/index.js +4 -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 +3 -0
- package/dist/{meta-DVG8ceze.d.ts → meta-DX5r5wQL.d.ts} +8 -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 +15 -2
- package/dist/motion/useAutoSize.js +42 -0
- package/dist/motion/useShake.js +25 -0
- package/dist/patterns/index.d.ts +1 -1
- package/dist/styles.css +298 -5
- package/llms/blocks/approval-block.md +128 -0
- package/llms/components/accordion.md +4 -3
- package/llms/components/area-chart.md +4 -3
- package/llms/components/attachment.md +7 -0
- package/llms/components/badge.md +1 -1
- package/llms/components/banner.md +2 -2
- 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/button-group.md +1 -1
- 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 +9 -1
- package/llms/components/code-block.md +8 -0
- package/llms/components/collapsible.md +9 -1
- package/llms/components/color-field.md +7 -0
- package/llms/components/combobox.md +12 -2
- package/llms/components/command-menu.md +12 -3
- package/llms/components/confirm-button.md +92 -0
- package/llms/components/context-card.md +1 -1
- package/llms/components/data-table.md +8 -0
- package/llms/components/dialog.md +3 -3
- package/llms/components/disclosure-picker.md +1 -1
- package/llms/components/entity.md +7 -0
- package/llms/components/feedback.md +22 -3
- package/llms/components/inline-status.md +1 -1
- package/llms/components/input-group.md +8 -1
- package/llms/components/line-chart.md +4 -3
- package/llms/components/menu.md +1 -1
- package/llms/components/menubar.md +1 -1
- package/llms/components/meter.md +1 -1
- package/llms/components/middle-truncate.md +1 -1
- package/llms/components/note.md +9 -1
- 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/page-loader.md +1 -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 +9 -1
- package/llms/components/popover.md +2 -2
- package/llms/components/progress.md +1 -1
- 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 +8 -1
- 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/segmented-bar.md +1 -1
- package/llms/components/select.md +2 -2
- package/llms/components/sheet.md +2 -2
- package/llms/components/show-more.md +9 -0
- package/llms/components/side-nav.md +9 -1
- package/llms/components/sidebar.md +9 -0
- package/llms/components/skeleton.md +1 -1
- package/llms/components/slider.md +7 -0
- package/llms/components/sparkline.md +3 -2
- package/llms/components/spinner.md +1 -1
- package/llms/components/stepper.md +2 -2
- package/llms/components/switch.md +1 -1
- package/llms/components/table.md +8 -1
- package/llms/components/tabs.md +22 -4
- package/llms/components/theme-selector.md +8 -0
- package/llms/components/theme-switcher.md +17 -0
- package/llms/components/thinking-orb.md +2 -2
- package/llms/components/toast.md +1 -1
- package/llms/components/toggle.md +20 -3
- package/llms/components/tool-call.md +115 -0
- package/llms/components/toolbar.md +1 -1
- package/llms/components/tooltip.md +12 -2
- package/llms/components/tree.md +8 -0
- package/llms/components/video.md +8 -0
- package/llms/foundations/motion.md +48 -3
- package/llms/reference/utilities.md +23 -1
- package/llms/tasks/reviewing-motion.md +14 -0
- package/llms-full.txt +1004 -98
- package/llms.txt +16 -11
- package/package.json +1 -1
- package/registry.json +2708 -187
- package/targets/ios.json +27 -0
|
@@ -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
|
+
};
|
|
@@ -26,7 +26,8 @@ var contextCardMeta = {
|
|
|
26
26
|
},
|
|
27
27
|
{
|
|
28
28
|
guidance: "dont",
|
|
29
|
-
description: "Do not open it on click. It is a hover/focus preview; making it click-triggered gives you a Popover with no dismiss affordance and no way to reach the content it previews."
|
|
29
|
+
description: "Do not open it on click. It is a hover/focus preview; making it click-triggered gives you a Popover with no dismiss affordance and no way to reach the content it previews.",
|
|
30
|
+
instead: ["Popover"]
|
|
30
31
|
}
|
|
31
32
|
],
|
|
32
33
|
anatomy: [
|
|
@@ -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.",
|
|
@@ -38,7 +38,8 @@ var dialogMeta = {
|
|
|
38
38
|
},
|
|
39
39
|
{
|
|
40
40
|
guidance: "dont",
|
|
41
|
-
description: "Put large multipage workflows in a centered dialog. Use Sheet or a page route when the task needs space."
|
|
41
|
+
description: "Put large multipage workflows in a centered dialog. Use Sheet or a page route when the task needs space.",
|
|
42
|
+
instead: ["Sheet"]
|
|
42
43
|
},
|
|
43
44
|
{
|
|
44
45
|
guidance: "do",
|
|
@@ -46,7 +47,8 @@ var dialogMeta = {
|
|
|
46
47
|
},
|
|
47
48
|
{
|
|
48
49
|
guidance: "dont",
|
|
49
|
-
description: `Use size="fullscreen" for forms, confirmations, or anything that isn't media. A dialog that happens to need room is a Sheet or a page; the fullscreen affordances (dark scrim, corner close, centred content) all assume something to look at
|
|
50
|
+
description: `Use size="fullscreen" for forms, confirmations, or anything that isn't media. A dialog that happens to need room is a Sheet or a page; the fullscreen affordances (dark scrim, corner close, centred content) all assume something to look at.`,
|
|
51
|
+
instead: ["Sheet"]
|
|
50
52
|
},
|
|
51
53
|
{
|
|
52
54
|
guidance: "dont",
|
|
@@ -54,7 +56,8 @@ var dialogMeta = {
|
|
|
54
56
|
},
|
|
55
57
|
{
|
|
56
58
|
guidance: "dont",
|
|
57
|
-
description: "Open a dialog for passive status updates. Use Toast, Banner, or InlineStatus instead."
|
|
59
|
+
description: "Open a dialog for passive status updates. Use Toast, Banner, or InlineStatus instead.",
|
|
60
|
+
instead: ["Toast", "Banner", "InlineStatus"]
|
|
58
61
|
},
|
|
59
62
|
{
|
|
60
63
|
guidance: "do",
|
|
@@ -47,7 +47,8 @@ var disclosurePickerMeta = {
|
|
|
47
47
|
},
|
|
48
48
|
{
|
|
49
49
|
guidance: "dont",
|
|
50
|
-
description: "Use it in a page-register form. A form field that pushes the form down on open reads as a layout jump; that is Select's job."
|
|
50
|
+
description: "Use it in a page-register form. A form field that pushes the form down on open reads as a layout jump; that is Select's job.",
|
|
51
|
+
instead: ["Select"]
|
|
51
52
|
},
|
|
52
53
|
{
|
|
53
54
|
guidance: "dont",
|
|
@@ -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.",
|