@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.
- package/AGENTS.md +2 -2
- package/CHANGELOG.md +114 -0
- package/CONTRIBUTING.md +6 -4
- package/DESIGN.md +8 -3
- package/dist/blocks/approval-block.js +93 -0
- package/dist/blocks/approval-block.meta.js +23 -0
- package/dist/blocks/index.d.ts +16 -1
- package/dist/blocks/index.js +4 -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/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 +113 -47
- package/dist/components/feedback/feedback.meta.js +49 -4
- 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/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/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/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 +114 -97
- package/dist/foundations/motion.js +49 -3
- package/dist/index.d.ts +614 -44
- 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/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/styles.css +298 -5
- package/llms/blocks/approval-block.md +128 -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/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 +22 -3
- package/llms/components/input-group.md +8 -1
- 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/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/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 +48 -3
- package/llms/reference/utilities.md +23 -1
- package/llms/tasks/reviewing-motion.md +14 -0
- package/llms-full.txt +955 -49
- package/llms.txt +15 -10
- package/package.json +1 -1
- package/registry.json +2565 -210
- package/targets/ios.json +27 -0
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { Checkbox as Base } from "@base-ui/react/checkbox";
|
|
3
3
|
import { CheckboxGroup as BaseGroup } from "@base-ui/react/checkbox-group";
|
|
4
4
|
import { cn } from "../../lib/cn.js";
|
|
5
|
+
import { motionAttribute } from "../../motion/MotionProvider.js";
|
|
5
6
|
import {
|
|
6
7
|
CHECK_BOX,
|
|
7
8
|
SELECTION_CARD_GLYPH,
|
|
@@ -52,6 +53,7 @@ function CheckboxCard({
|
|
|
52
53
|
description,
|
|
53
54
|
icon,
|
|
54
55
|
tone = "neutral",
|
|
56
|
+
motion,
|
|
55
57
|
...props
|
|
56
58
|
}) {
|
|
57
59
|
const t = checkboxCardToneClasses[tone];
|
|
@@ -59,6 +61,7 @@ function CheckboxCard({
|
|
|
59
61
|
return /* @__PURE__ */ jsxs(
|
|
60
62
|
Base.Root,
|
|
61
63
|
{
|
|
64
|
+
"data-motion": motionAttribute(motion),
|
|
62
65
|
className: cn(
|
|
63
66
|
SELECTION_CARD_SHELL,
|
|
64
67
|
icon != null ? SELECTION_CARD_LAYOUT.tile : SELECTION_CARD_LAYOUT.row,
|
|
@@ -92,6 +92,12 @@ var checkboxCardMeta = {
|
|
|
92
92
|
name: "checked / onCheckedChange / defaultChecked / disabled",
|
|
93
93
|
type: "Base UI Checkbox.Root props",
|
|
94
94
|
description: "Standard checkbox state. Without a group-owned value, each card is independent \u2014 drive checked/onCheckedChange per item for multi-select."
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
name: "motion",
|
|
98
|
+
type: '"full" | "subtle" | "off"',
|
|
99
|
+
default: '"full"',
|
|
100
|
+
description: "How much motion this component uses: `full` (default) inherits the app's policy, `subtle` keeps feedback and drops the flourish, `off` stills it. It can only turn motion down \u2014 it never overrides a reader's reduce-motion setting. Under `subtle` the tick fades in instead of drawing itself."
|
|
95
101
|
}
|
|
96
102
|
],
|
|
97
103
|
subcomponents: [
|
|
@@ -124,6 +130,32 @@ var checkboxCardMeta = {
|
|
|
124
130
|
"tone-warning",
|
|
125
131
|
"tone-warning-foreground"
|
|
126
132
|
],
|
|
133
|
+
motion: [
|
|
134
|
+
{
|
|
135
|
+
slot: "arrival",
|
|
136
|
+
recipe: "none",
|
|
137
|
+
part: "Root",
|
|
138
|
+
why: "It arrives with the form it belongs to. Animating each box in turns a settings list into a title sequence."
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
slot: "response",
|
|
142
|
+
recipe: "motion-interactive",
|
|
143
|
+
part: "Box",
|
|
144
|
+
why: "The box fills with the primary colour as it is checked; easing the fill keeps the mark and the box changing as one event rather than two frames apart."
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
slot: "transition",
|
|
148
|
+
recipe: "motion-tick",
|
|
149
|
+
part: "Mark",
|
|
150
|
+
why: "The check draws itself in from its leading stroke, so checking reads as the box being ticked rather than a glyph appearing. The dash wipes the same way. Unchecking only fades, so clearing a list is quick; under `subtle` checking fades too."
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
slot: "activity",
|
|
154
|
+
recipe: "none",
|
|
155
|
+
part: "Root",
|
|
156
|
+
why: "A checkbox commits instantly. A slow write belongs to the surrounding form's pending state, not the control."
|
|
157
|
+
}
|
|
158
|
+
],
|
|
127
159
|
a11y: [
|
|
128
160
|
"Base UI provides the checkbox role, aria-checked, hidden native input, keyboard toggle, and data-[checked]/data-disabled state.",
|
|
129
161
|
"The whole card is the hit target; focus draws a 2px outline outside the card (not a ring, so the checked tone ring survives focus); Space toggles the focused card.",
|
|
@@ -7,6 +7,15 @@ import { Button } from "../button/button.js";
|
|
|
7
7
|
import { cn } from "../../lib/cn.js";
|
|
8
8
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
9
9
|
var CodeHighlightContext = createContext(null);
|
|
10
|
+
function CopyGlyph({ copied }) {
|
|
11
|
+
return /* @__PURE__ */ jsxs("span", { "aria-hidden": "true", className: "motion-icon-swap", children: [
|
|
12
|
+
/* @__PURE__ */ jsx("span", { "data-active": !copied, className: "inline-flex", children: /* @__PURE__ */ jsx(Icon, { name: "copy" }) }),
|
|
13
|
+
/* @__PURE__ */ jsx("span", { "data-active": copied, className: "inline-flex", children: /* @__PURE__ */ jsx(Icon, { name: "check" }) })
|
|
14
|
+
] });
|
|
15
|
+
}
|
|
16
|
+
function CopiedStatus({ copied }) {
|
|
17
|
+
return /* @__PURE__ */ jsx("span", { role: "status", className: "sr-only", children: copied ? "Copied" : "" });
|
|
18
|
+
}
|
|
10
19
|
function CodeHighlightProvider({ highlight, children }) {
|
|
11
20
|
return /* @__PURE__ */ jsx(CodeHighlightContext.Provider, { value: highlight, children });
|
|
12
21
|
}
|
|
@@ -39,7 +48,8 @@ function CodeBlock({
|
|
|
39
48
|
] }),
|
|
40
49
|
code
|
|
41
50
|
] }),
|
|
42
|
-
showCopy ? /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "icon-sm", "aria-label": "Copy command", onClick: copy, className: "shrink-0 text-muted-foreground", children: /* @__PURE__ */ jsx(
|
|
51
|
+
showCopy ? /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "icon-sm", "aria-label": "Copy command", onClick: copy, className: "shrink-0 text-muted-foreground", children: /* @__PURE__ */ jsx(CopyGlyph, { copied }) }) : null,
|
|
52
|
+
showCopy ? /* @__PURE__ */ jsx(CopiedStatus, { copied }) : null
|
|
43
53
|
]
|
|
44
54
|
}
|
|
45
55
|
);
|
|
@@ -57,7 +67,7 @@ function CodeBlock({
|
|
|
57
67
|
children: [
|
|
58
68
|
hasHeader ? /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-3 border-b border-border px-4 py-2", children: [
|
|
59
69
|
/* @__PURE__ */ jsx("span", { className: "text-mono-caption text-muted-foreground", children: filename }),
|
|
60
|
-
showCopy ? /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "icon-sm", "aria-label": "Copy code", onClick: copy, className: "text-muted-foreground", children: /* @__PURE__ */ jsx(
|
|
70
|
+
showCopy ? /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "icon-sm", "aria-label": "Copy code", onClick: copy, className: "text-muted-foreground", children: /* @__PURE__ */ jsx(CopyGlyph, { copied }) }) : null
|
|
61
71
|
] }) : null,
|
|
62
72
|
/* @__PURE__ */ jsx(
|
|
63
73
|
"pre",
|
|
@@ -72,7 +82,8 @@ function CodeBlock({
|
|
|
72
82
|
children: /* @__PURE__ */ jsx("code", { children: highlighter ? highlighter(code) : code })
|
|
73
83
|
}
|
|
74
84
|
),
|
|
75
|
-
copyColumn ? /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "icon-sm", "aria-label": "Copy code", onClick: copy, className: "m-2 ms-0 shrink-0 text-muted-foreground", children: /* @__PURE__ */ jsx(
|
|
85
|
+
copyColumn ? /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "icon-sm", "aria-label": "Copy code", onClick: copy, className: "m-2 ms-0 shrink-0 text-muted-foreground", children: /* @__PURE__ */ jsx(CopyGlyph, { copied }) }) : null,
|
|
86
|
+
showCopy ? /* @__PURE__ */ jsx(CopiedStatus, { copied }) : null
|
|
76
87
|
]
|
|
77
88
|
}
|
|
78
89
|
);
|
|
@@ -79,9 +79,36 @@ var codeBlockMeta = {
|
|
|
79
79
|
}
|
|
80
80
|
],
|
|
81
81
|
tokensUsed: ["border", "muted", "muted-foreground", "foreground"],
|
|
82
|
+
motion: [
|
|
83
|
+
{
|
|
84
|
+
slot: "arrival",
|
|
85
|
+
recipe: "none",
|
|
86
|
+
part: "Root",
|
|
87
|
+
why: "Code is reading content and arrives with the page or message it belongs to. An entrance of its own would delay the text someone came to copy."
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
slot: "response",
|
|
91
|
+
recipe: "motion-interactive",
|
|
92
|
+
part: "Copy button",
|
|
93
|
+
why: "The copy button is a quiet ghost icon in the corner of a block. Its hover fill is what tells the pointer it is a control and not decoration."
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
slot: "transition",
|
|
97
|
+
recipe: "motion-icon-swap",
|
|
98
|
+
part: "Copy glyph",
|
|
99
|
+
why: "The copy mark turning into a check is the only proof the clipboard took the text. Snapped between frames it was easy to miss at the edge of a block; the swap reads as one mark being replaced."
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
slot: "activity",
|
|
103
|
+
recipe: "none",
|
|
104
|
+
part: "Root",
|
|
105
|
+
why: "Copying is instant. There is no in-between worth showing, and a spinner for a clipboard write would read as a slow one."
|
|
106
|
+
}
|
|
107
|
+
],
|
|
82
108
|
a11y: [
|
|
83
109
|
"Code renders inside a <pre><code> for correct semantics and screen-reader handling.",
|
|
84
|
-
"Copy is the ghost icon Button with `useCopy`, which guards clipboard access for SSR safety."
|
|
110
|
+
"Copy is the ghost icon Button with `useCopy`, which guards clipboard access for SSR safety.",
|
|
111
|
+
'A successful copy is announced as "Copied" through a polite role=status region that is always mounted; the button keeps its label, which names what it does. Both glyphs are aria-hidden.'
|
|
85
112
|
],
|
|
86
113
|
examples: [
|
|
87
114
|
{
|
|
@@ -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
|
+
};
|