@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
|
@@ -117,11 +117,18 @@ var barChartMeta = {
|
|
|
117
117
|
default: "false for columns; true for horizontal bars",
|
|
118
118
|
description: "The vertical axis."
|
|
119
119
|
},
|
|
120
|
+
{
|
|
121
|
+
name: "motion",
|
|
122
|
+
type: '"full" | "subtle" | "off"',
|
|
123
|
+
default: '"full"',
|
|
124
|
+
description: "How much motion this component uses: `full` (default) inherits the app's policy, `subtle` keeps feedback and drops the flourish, `off` stills it. It can only turn motion down \u2014 it never overrides a reader's reduce-motion setting. The arrival (bars rising in sequence) and the data-change morph are the flourish; the hover dim and gliding cursor are the feedback `subtle` keeps."
|
|
125
|
+
},
|
|
120
126
|
{
|
|
121
127
|
name: "animate",
|
|
122
128
|
type: "boolean",
|
|
123
129
|
default: "true",
|
|
124
|
-
description:
|
|
130
|
+
description: 'Play the arrival once the chart scrolls into view. Reduced motion shows the finished chart. `animate={false}` is `motion="subtle"`.',
|
|
131
|
+
deprecated: 'motion="subtle" (what animate={false} now maps to: no arrival, hover kept) or motion="off"'
|
|
125
132
|
},
|
|
126
133
|
{
|
|
127
134
|
name: "activeIndex",
|
|
@@ -199,19 +206,19 @@ var barChartMeta = {
|
|
|
199
206
|
slot: "arrival",
|
|
200
207
|
recipe: "custom",
|
|
201
208
|
part: "Bar",
|
|
202
|
-
why:
|
|
209
|
+
why: 'Once on screen, bars rise from the baseline in sequence on the emphasized easing (scale from the zero line) and their labels fade in. On the motion tokens; reduced motion shows the finished chart. The arrival is expressive: it plays at the full motion level only, so `motion="subtle"` (or a subtle app policy) shows the finished chart while hover feedback stays.'
|
|
203
210
|
},
|
|
204
211
|
{
|
|
205
212
|
slot: "response",
|
|
206
213
|
recipe: "none",
|
|
207
214
|
part: "Cursor + tooltip",
|
|
208
|
-
why: "The hovered category gets a band behind it and the other bars dim at once \u2014 a lagging highlight reads as a slow chart."
|
|
215
|
+
why: "The hovered category gets a band behind it and the other bars dim at once \u2014 a lagging highlight reads as a slow chart. The dim mixes each bar toward the surface instead of fading it, so gridlines never strike through a dimmed column and stacked segments stay solid. Essential: `subtle` keeps it."
|
|
209
216
|
},
|
|
210
217
|
{
|
|
211
218
|
slot: "transition",
|
|
212
219
|
recipe: "custom",
|
|
213
220
|
part: "Bar",
|
|
214
|
-
why: "Geometry eases from the old shape to the new one through a CSS `d` transition on the motion tokens (no recipe animates path geometry); browsers without `d` transitions update in place."
|
|
221
|
+
why: "Geometry eases from the old shape to the new one through a CSS `d` transition on the motion tokens (no recipe animates path geometry); browsers without `d` transitions update in place. Expressive, like the arrival: under `subtle` new data swaps in place."
|
|
215
222
|
},
|
|
216
223
|
{
|
|
217
224
|
slot: "activity",
|
|
@@ -38,8 +38,10 @@ function BottomNavItem({
|
|
|
38
38
|
"data-active": active ? "" : void 0,
|
|
39
39
|
"aria-current": active ? "page" : void 0,
|
|
40
40
|
className: cn(
|
|
41
|
+
// motion-press: on a phone there is no hover to say the tap landed, and
|
|
42
|
+
// a route change can take a beat to paint — the settle is the receipt.
|
|
41
43
|
// design-lint-disable-next-line raw-control-height -- the WCAG 2.5.5 touch-target floor, deliberately density-independent: a compact register still owes a 44px hit area.
|
|
42
|
-
"relative flex min-h-[44px] min-w-[44px] flex-col items-center justify-center gap-0.5 rounded-lg text-muted-foreground motion-interactive hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring data-[active]:text-foreground",
|
|
44
|
+
"relative flex min-h-[44px] min-w-[44px] flex-col items-center justify-center gap-0.5 rounded-lg text-muted-foreground motion-interactive motion-press hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring data-[active]:text-foreground",
|
|
43
45
|
className
|
|
44
46
|
),
|
|
45
47
|
children: /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
@@ -57,6 +57,38 @@ var bottomNavMeta = {
|
|
|
57
57
|
description: 'A single tab, placed as a child of BottomNav. `label` is the accessible name (aria-label) and optional visible label; `icon` is the glyph node (consumer swaps fill weight for the active state); `active` marks the current route (active ink + aria-current); `badge` overlays a corner dot/count on the icon; `showLabel` shows the label text beneath the icon \u2014 defaults to false (icon-only); turn it on when the icons alone are not self-evident; `render` renders as another element, e.g. render={<Link to="/x" />}.'
|
|
58
58
|
}
|
|
59
59
|
],
|
|
60
|
+
motion: [
|
|
61
|
+
{
|
|
62
|
+
slot: "arrival",
|
|
63
|
+
recipe: "none",
|
|
64
|
+
part: "Root",
|
|
65
|
+
why: "Fixed app chrome, present from the first paint of every screen. Animating it in on navigation would make each route change look like the app restarting."
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
slot: "response",
|
|
69
|
+
recipe: "motion-interactive",
|
|
70
|
+
part: "BottomNavItem",
|
|
71
|
+
why: "The ink steps up on hover and the focus ring eases in, so a pointer or a keyboard on a tablet gets the same answer as every other control in the app."
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
slot: "response",
|
|
75
|
+
recipe: "motion-press",
|
|
76
|
+
part: "BottomNavItem",
|
|
77
|
+
why: "On a phone there is no hover to say a tap landed, and a route can take a beat to paint. The settle on press is the receipt; without it people tap the tab a second time."
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
slot: "transition",
|
|
81
|
+
recipe: "motion-interactive",
|
|
82
|
+
part: "BottomNavItem",
|
|
83
|
+
why: "Changing tab eases the active ink out of one item and into the next. It is a cross-fade, not a travelling indicator: each item owns its colour, so there is no single mark to glide."
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
slot: "activity",
|
|
87
|
+
recipe: "none",
|
|
88
|
+
part: "Root",
|
|
89
|
+
why: "Navigation has no working state of its own. A slow route shows its pending state in the page it is loading, not in the tab bar."
|
|
90
|
+
}
|
|
91
|
+
],
|
|
60
92
|
a11y: [
|
|
61
93
|
"BottomNav renders a <nav> landmark.",
|
|
62
94
|
"Each tab is a 44px minimum touch target with an aria-label.",
|
|
@@ -89,6 +89,32 @@ var breadcrumbsMeta = {
|
|
|
89
89
|
}
|
|
90
90
|
],
|
|
91
91
|
tokensUsed: ["muted-foreground", "foreground"],
|
|
92
|
+
motion: [
|
|
93
|
+
{
|
|
94
|
+
slot: "arrival",
|
|
95
|
+
recipe: "none",
|
|
96
|
+
part: "Root",
|
|
97
|
+
why: "The trail is page furniture, rendered with the page header. An entrance would replay on every navigation, which is exactly when the reader looks at it to get their bearings."
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
slot: "response",
|
|
101
|
+
recipe: "motion-interactive",
|
|
102
|
+
part: "BreadcrumbLink",
|
|
103
|
+
why: "Ancestors are muted until hovered or focused. Easing the ink makes the trail read as links answering the pointer; a hard snap across a row of small text looks like flicker."
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
slot: "transition",
|
|
107
|
+
recipe: "none",
|
|
108
|
+
part: "Root",
|
|
109
|
+
why: "A breadcrumb has no state of its own. The current page changes by navigation, which replaces the trail; animating between two trails would imply the path itself moved."
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
slot: "activity",
|
|
113
|
+
recipe: "none",
|
|
114
|
+
part: "Root",
|
|
115
|
+
why: "It never works in the background. A route that is still loading says so in the page, not in its address."
|
|
116
|
+
}
|
|
117
|
+
],
|
|
92
118
|
a11y: [
|
|
93
119
|
'The <nav> carries aria-label="Breadcrumb" so assistive tech announces the landmark.',
|
|
94
120
|
"Crumbs sit in an ordered list (<ol>) to convey hierarchy and position.",
|
|
@@ -21,6 +21,7 @@ var BLEED_CLASS = {
|
|
|
21
21
|
end: "-me-(--button-pe,var(--button-px))",
|
|
22
22
|
both: "-ms-(--button-ps,var(--button-px)) -me-(--button-pe,var(--button-px))"
|
|
23
23
|
};
|
|
24
|
+
var LABEL_FADE = "[&>.sable-label-trim]:transition-opacity [&>.sable-label-trim]:duration-(--motion-duration-fast) [&>.sable-label-trim]:ease-standard";
|
|
24
25
|
function Button({
|
|
25
26
|
variant,
|
|
26
27
|
size,
|
|
@@ -71,12 +72,19 @@ function Button({
|
|
|
71
72
|
const stacked = useContext(StackedActionsContext) && !String(size ?? "md").startsWith("icon");
|
|
72
73
|
const resolvedSize = stacked ? "cta" : size ?? "md";
|
|
73
74
|
const hasLabel = children != null && children !== false;
|
|
74
|
-
const spinner = /* @__PURE__ */ jsx(Spinner, { className: "size-(--button-icon)", thickness: 2 });
|
|
75
|
-
const start = busy ? spinner : iconStart;
|
|
76
75
|
const iconOnly = String(resolvedSize).startsWith("icon");
|
|
76
|
+
const overlay = busy && !iconOnly && hasLabel && iconStart == null && loadingLabel == null && badge == null;
|
|
77
|
+
const spinner = /* @__PURE__ */ jsx(
|
|
78
|
+
Spinner,
|
|
79
|
+
{
|
|
80
|
+
className: cn("size-(--button-icon) motion-appear", overlay && "absolute inset-0 m-auto"),
|
|
81
|
+
thickness: 2
|
|
82
|
+
}
|
|
83
|
+
);
|
|
84
|
+
const start = busy ? spinner : iconStart;
|
|
77
85
|
const inner = busy && iconOnly ? spinner : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
78
86
|
start,
|
|
79
|
-
trimLabel(busy ? loadingLabel ?? children : children),
|
|
87
|
+
trimLabel(busy && !overlay ? loadingLabel ?? children : children),
|
|
80
88
|
iconEnd
|
|
81
89
|
] });
|
|
82
90
|
const content = badge != null ? /* @__PURE__ */ jsxs("span", { "data-slot": "button-badge-anchor", className: "relative inline-flex items-center gap-(--control-gap)", children: [
|
|
@@ -84,7 +92,9 @@ function Button({
|
|
|
84
92
|
/* @__PURE__ */ jsx("span", { className: "pointer-events-none absolute -end-1 -top-1 flex", children: badge })
|
|
85
93
|
] }) : inner;
|
|
86
94
|
const optical = cn(
|
|
87
|
-
|
|
95
|
+
// An overlaid spinner is not a leading glyph: trimming the padding for it
|
|
96
|
+
// changed the width by 2px, the exact shift the overlay exists to avoid.
|
|
97
|
+
start && hasLabel && !iconOnly && !overlay ? OPTICAL_START : null,
|
|
88
98
|
iconEnd && hasLabel && !iconOnly ? OPTICAL_END : null
|
|
89
99
|
);
|
|
90
100
|
const bleedable = BORDERLESS.has(variant ?? "primary") && !stacked;
|
|
@@ -104,6 +114,13 @@ function Button({
|
|
|
104
114
|
// double-fire, but fading it to 50% made a working button read as an
|
|
105
115
|
// unavailable one. It keeps full colour; the spinner says "working".
|
|
106
116
|
busy ? "cursor-progress disabled:opacity-100 aria-disabled:opacity-100" : null,
|
|
117
|
+
// The label hands over to the spinner laid on top of it (see
|
|
118
|
+
// `overlay`). Its fade is declared at rest too, so the label also
|
|
119
|
+
// fades BACK when the work settles instead of snapping in. Only the
|
|
120
|
+
// trimLabel span is targeted: a caller's icon may carry a transition
|
|
121
|
+
// of its own (a chevron's rotate) that a child selector would clobber.
|
|
122
|
+
LABEL_FADE,
|
|
123
|
+
overlay ? "relative [&>:not([role=status])]:opacity-0" : null,
|
|
107
124
|
className
|
|
108
125
|
),
|
|
109
126
|
children: content,
|
|
@@ -33,7 +33,7 @@ var buttonMeta = {
|
|
|
33
33
|
},
|
|
34
34
|
{
|
|
35
35
|
guidance: "do",
|
|
36
|
-
description:
|
|
36
|
+
description: 'For a copy control, pair an icon Button with `useCopy(text, { timeout? })`. It takes the text when the hook is called and returns `{ copy, copied }`; `copy()` takes no argument, so pass the value the button copies to the hook, not to `copy`. Text that costs something to produce (a file to fetch) goes in as a function, `useCopy(() => fetch(url).then((r) => r.text()))`, called only on click. Keep both glyphs mounted in a `motion-icon-swap` cell so the copy mark visibly becomes a check (a `copied ? "check" : "copy"` swap snaps between frames and is easy to miss), and say "Copied" in a polite `role="status"` region: the glyph change is silent to a screen reader.'
|
|
37
37
|
},
|
|
38
38
|
{
|
|
39
39
|
guidance: "do",
|
|
@@ -126,7 +126,7 @@ var buttonMeta = {
|
|
|
126
126
|
name: "loading",
|
|
127
127
|
type: "boolean",
|
|
128
128
|
default: "false",
|
|
129
|
-
description: "Show a
|
|
129
|
+
description: "Show a Spinner while an action is in flight and refuse presses. The spinner takes the iconStart slot when there is one; otherwise it lays over the label, which fades out in place, so the button keeps its width. Sets aria-busy and aria-disabled, never the native disabled: disabling the focused button sent focus to <body>, so a sheet the action opened had nowhere to return it."
|
|
130
130
|
},
|
|
131
131
|
{
|
|
132
132
|
name: "onClick",
|
|
@@ -141,7 +141,7 @@ var buttonMeta = {
|
|
|
141
141
|
{
|
|
142
142
|
name: "loadingLabel",
|
|
143
143
|
type: "ReactNode",
|
|
144
|
-
description: "Optional text shown in place of children while loading."
|
|
144
|
+
description: "Optional text shown in place of children while loading, with the spinner beside it. The width follows the new text, so prefer no loadingLabel where the row must not move."
|
|
145
145
|
},
|
|
146
146
|
{
|
|
147
147
|
name: "iconStart",
|
|
@@ -211,9 +211,9 @@ var buttonMeta = {
|
|
|
211
211
|
},
|
|
212
212
|
{
|
|
213
213
|
slot: "transition",
|
|
214
|
-
recipe: "
|
|
215
|
-
part: "
|
|
216
|
-
why: "
|
|
214
|
+
recipe: "motion-appear",
|
|
215
|
+
part: "Spinner",
|
|
216
|
+
why: "Going busy is the one state change a button has. The spinner fades in where it mounts \u2014 in the icon slot, or laid over a plain label that fades out beneath it and keeps its place \u2014 so the width never changes and the row does not shift under the pointer that just pressed it. The label fades back when the work settles. A toggling control is a Switch or a Toggle; those carry their own transition."
|
|
217
217
|
},
|
|
218
218
|
{
|
|
219
219
|
slot: "activity",
|
|
@@ -247,7 +247,7 @@ var buttonMeta = {
|
|
|
247
247
|
},
|
|
248
248
|
{
|
|
249
249
|
label: "Copy to clipboard",
|
|
250
|
-
code: 'const { copy, copied } = useCopy(address)\n<Button variant="ghost" size="icon-sm" aria-label="Copy address" onClick={copy}><Icon name={copied
|
|
250
|
+
code: 'const { copy, copied } = useCopy(address)\n<Row gap={1}><Button variant="ghost" size="icon-sm" aria-label="Copy address" onClick={copy}><span aria-hidden="true" className="motion-icon-swap"><span data-active={!copied} className="inline-flex"><Icon name="copy" /></span><span data-active={copied} className="inline-flex"><Icon name="check" /></span></span></Button><span role="status" className="sr-only">{copied ? "Copied" : ""}</span></Row>'
|
|
251
251
|
},
|
|
252
252
|
{
|
|
253
253
|
label: "As link",
|
|
@@ -25,7 +25,8 @@ var buttonGroupMeta = {
|
|
|
25
25
|
},
|
|
26
26
|
{
|
|
27
27
|
guidance: "dont",
|
|
28
|
-
description: "Do not use it for a selection that stays on. That is ToggleGroup, which carries the pressed state."
|
|
28
|
+
description: "Do not use it for a selection that stays on. That is ToggleGroup, which carries the pressed state.",
|
|
29
|
+
instead: ["ToggleGroup"]
|
|
29
30
|
}
|
|
30
31
|
]
|
|
31
32
|
},
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// src/components/calendar/calendar.tsx
|
|
4
4
|
import { Icon } from "../../icon/index.js";
|
|
5
5
|
import { cn } from "../../lib/cn.js";
|
|
6
|
+
import { motionAttribute, useMotionLevel } from "../../motion/MotionProvider.js";
|
|
6
7
|
import { buttonVariants } from "../button/button.variants.js";
|
|
7
8
|
import {
|
|
8
9
|
DayPicker
|
|
@@ -23,13 +24,18 @@ function Calendar({
|
|
|
23
24
|
showOutsideDays = true,
|
|
24
25
|
components,
|
|
25
26
|
bare = false,
|
|
27
|
+
motion,
|
|
28
|
+
animate,
|
|
26
29
|
...props
|
|
27
30
|
}) {
|
|
31
|
+
const level = useMotionLevel(motion);
|
|
28
32
|
return /* @__PURE__ */ jsx(
|
|
29
33
|
DayPicker,
|
|
30
34
|
{
|
|
31
35
|
"data-slot": "calendar",
|
|
36
|
+
"data-motion": motionAttribute(motion),
|
|
32
37
|
showOutsideDays,
|
|
38
|
+
animate: animate ?? level.expressive,
|
|
33
39
|
className: cn(
|
|
34
40
|
// relative: the nav is absolutely positioned (react-day-picker v9
|
|
35
41
|
// renders it inside `months`, not inside each month), and with no
|
|
@@ -53,7 +59,10 @@ function Calendar({
|
|
|
53
59
|
weekdays: "flex",
|
|
54
60
|
weekday: "flex size-9 items-center justify-center text-caption text-muted-foreground",
|
|
55
61
|
week: "mt-1 flex w-full",
|
|
56
|
-
|
|
62
|
+
// motion-interactive on the cell too: a range's fill is painted on the
|
|
63
|
+
// cells (see range_* below), so without it the band between the
|
|
64
|
+
// endpoints snapped while the endpoint buttons eased.
|
|
65
|
+
day: "p-0 text-center motion-interactive",
|
|
57
66
|
day_button: "inline-flex size-9 items-center justify-center rounded-md text-body-sm motion-interactive outline-none hover:bg-accent hover:text-accent-foreground focus-visible:ring-2 focus-visible:ring-ring",
|
|
58
67
|
// Range fill lives on the CELL so it forms one continuous bar; only the
|
|
59
68
|
// outer ends are rounded. The selected (primary) endpoint buttons sit on
|
|
@@ -76,6 +85,14 @@ function Calendar({
|
|
|
76
85
|
outside: "[&>button]:text-muted-foreground",
|
|
77
86
|
disabled: "[&>button]:pointer-events-none [&>button]:text-muted-foreground [&>button]:opacity-50",
|
|
78
87
|
hidden: "invisible",
|
|
88
|
+
weeks_after_enter: "sable-month-enter-after",
|
|
89
|
+
weeks_before_enter: "sable-month-enter-before",
|
|
90
|
+
weeks_after_exit: "sable-month-exit-after",
|
|
91
|
+
weeks_before_exit: "sable-month-exit-before",
|
|
92
|
+
caption_after_enter: "sable-caption-enter",
|
|
93
|
+
caption_before_enter: "sable-caption-enter",
|
|
94
|
+
caption_after_exit: "sable-caption-exit",
|
|
95
|
+
caption_before_exit: "sable-caption-exit",
|
|
79
96
|
...classNames
|
|
80
97
|
},
|
|
81
98
|
components: { Chevron: CalendarChevron, ...components },
|
|
@@ -84,6 +84,12 @@ var calendarMeta = {
|
|
|
84
84
|
default: "false",
|
|
85
85
|
description: "Drop the card frame (border, fill, padding) so the grid sits flush inside a surface that already has one, such as a Popover. DatePicker renders its Calendar this way."
|
|
86
86
|
},
|
|
87
|
+
{
|
|
88
|
+
name: "motion",
|
|
89
|
+
type: '"full" | "subtle" | "off"',
|
|
90
|
+
default: '"full"',
|
|
91
|
+
description: "How much motion this component uses: `full` inherits the app's policy, `subtle` keeps feedback and drops the month slide, `off` stills it. It can only turn motion down \u2014 it never overrides a reader's reduce-motion setting."
|
|
92
|
+
},
|
|
87
93
|
{
|
|
88
94
|
name: "className",
|
|
89
95
|
type: "string",
|
|
@@ -114,6 +120,38 @@ var calendarMeta = {
|
|
|
114
120
|
"muted-foreground",
|
|
115
121
|
"ring"
|
|
116
122
|
],
|
|
123
|
+
motion: [
|
|
124
|
+
{
|
|
125
|
+
slot: "arrival",
|
|
126
|
+
recipe: "none",
|
|
127
|
+
part: "Root",
|
|
128
|
+
why: "Inline, it arrives with its form; in a DatePicker the Popover carries the entrance. A grid that animated in by itself would put a second motion on the same opening."
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
slot: "response",
|
|
132
|
+
recipe: "motion-interactive",
|
|
133
|
+
part: "Day",
|
|
134
|
+
why: "Days ease their hover wash, and the month arrows are ghost Buttons on Button's own recipe. Across a seven-column grid the pointer crosses many cells at once, and an eased wash reads as a cursor travelling rather than a grid flashing."
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
slot: "transition",
|
|
138
|
+
recipe: "motion-interactive",
|
|
139
|
+
part: "Day",
|
|
140
|
+
why: "Picking a day eases the primary fill onto it and off the previous one, so a click reads as the selection moving rather than two cells flipping. A range's band is painted on the cells, which carry the same recipe, so the band eases along with its endpoints as the range grows."
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
slot: "activity",
|
|
144
|
+
recipe: "none",
|
|
145
|
+
part: "Root",
|
|
146
|
+
why: "Days are resident data; nothing in the grid loads. Availability fetched per month belongs to the surface around the calendar."
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
slot: "transition",
|
|
150
|
+
recipe: "custom",
|
|
151
|
+
part: "Month",
|
|
152
|
+
why: "Paging months slides the outgoing grid toward the edge it is leaving by and brings the new one in from the other, over a short travel with a fade (react-day-picker's `animate` mode on Sable keyframes in styles.css), so forward and back read as direction rather than a grid swapping in one frame. RDP adds a single class per part, which a recipe utility cannot be; mirrored in RTL. Expressive: Calendar turns `animate` on only at full motion, and an explicit `animate` prop still wins."
|
|
153
|
+
}
|
|
154
|
+
],
|
|
117
155
|
a11y: [
|
|
118
156
|
"react-day-picker manages the roving-tabindex grid, arrow-key navigation, and ARIA roles/labels for the calendar.",
|
|
119
157
|
"Selected days use bg-primary/text-primary-foreground for AA contrast; today is marked with a dot under the number (not a ring, which is reserved for keyboard focus) rather than color alone.",
|
|
@@ -14,7 +14,7 @@ import { Icon } from "../../icon/Icon.js";
|
|
|
14
14
|
import { cn } from "../../lib/cn.js";
|
|
15
15
|
import { PlayPauseGlyph } from "../../lib/glyphs.js";
|
|
16
16
|
import { useResizeObserver } from "../../lib/use-resize-observer.js";
|
|
17
|
-
import {
|
|
17
|
+
import { useMotionLevel } from "../../motion/MotionProvider.js";
|
|
18
18
|
import { Button } from "../button/button.js";
|
|
19
19
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
20
20
|
var CAROUSEL_BREAKPOINTS = [
|
|
@@ -92,9 +92,10 @@ function Carousel({
|
|
|
92
92
|
label,
|
|
93
93
|
className,
|
|
94
94
|
slideClassName,
|
|
95
|
-
arrowClassName
|
|
95
|
+
arrowClassName,
|
|
96
|
+
motion
|
|
96
97
|
}) {
|
|
97
|
-
const { enabled } =
|
|
98
|
+
const { enabled, attribute: motionAttr } = useMotionLevel(motion);
|
|
98
99
|
const slides = Children.toArray(children);
|
|
99
100
|
const count = slides.length;
|
|
100
101
|
const perView = Math.min(
|
|
@@ -278,6 +279,7 @@ function Carousel({
|
|
|
278
279
|
{
|
|
279
280
|
"aria-roledescription": "carousel",
|
|
280
281
|
"aria-label": label ?? "Slides",
|
|
282
|
+
"data-motion": motionAttr,
|
|
281
283
|
className: cn("relative flex flex-col", className),
|
|
282
284
|
onMouseEnter: () => setHovered(true),
|
|
283
285
|
onMouseLeave: () => setHovered(false),
|
|
@@ -401,7 +403,7 @@ function Carousel({
|
|
|
401
403
|
{
|
|
402
404
|
"aria-hidden": true,
|
|
403
405
|
className: cn(
|
|
404
|
-
"h-1.5 rounded-full motion-
|
|
406
|
+
"h-1.5 rounded-full motion-glide",
|
|
405
407
|
i === logical ? "w-4 bg-foreground" : "w-1.5 bg-muted-foreground/40 group-hover/dot:bg-muted-foreground/70"
|
|
406
408
|
)
|
|
407
409
|
}
|
|
@@ -60,6 +60,12 @@ var carouselMeta = {
|
|
|
60
60
|
name: "arrowClassName",
|
|
61
61
|
type: "string",
|
|
62
62
|
description: "Class for both prev/next arrow buttons. Merged through cn(), so a radius or shadow utility replaces the default rounded-full shadow-md rather than stacking with it."
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
name: "motion",
|
|
66
|
+
type: '"full" | "subtle" | "off"',
|
|
67
|
+
default: '"full"',
|
|
68
|
+
description: "How much motion this component uses: `full` (default) inherits the app's policy, `subtle` keeps feedback and drops the glide, `off` stills it. It can only turn motion down \u2014 it never overrides a reader's reduce-motion setting. Under subtle the slides still travel and the active dot snaps; off also cuts between slides and pauses autoplay."
|
|
63
69
|
}
|
|
64
70
|
],
|
|
65
71
|
tokensUsed: [
|
|
@@ -70,6 +76,38 @@ var carouselMeta = {
|
|
|
70
76
|
"foreground",
|
|
71
77
|
"muted-foreground"
|
|
72
78
|
],
|
|
79
|
+
motion: [
|
|
80
|
+
{
|
|
81
|
+
slot: "arrival",
|
|
82
|
+
recipe: "none",
|
|
83
|
+
part: "Root",
|
|
84
|
+
why: "A carousel arrives with its section. Its content already moves on every advance; an entrance on top would make the first view feel like an advance nobody asked for."
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
slot: "response",
|
|
88
|
+
recipe: "motion-interactive",
|
|
89
|
+
part: "Dot",
|
|
90
|
+
why: "The dot's 24px hit area eases its focus ring and the dot brightens under the pointer, which is how a 6px mark tells you it is a button."
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
slot: "transition",
|
|
94
|
+
recipe: "custom",
|
|
95
|
+
part: "Track",
|
|
96
|
+
why: `The slide track's translate is set inline from the measured slide pitch, on --motion-duration-slow and the emphasized easing, and switched off during a drag so the track follows the finger. A recipe cannot carry a measured, drag-interrupted offset. Essential: it is the answer to "next", so it plays under subtle.`
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
slot: "transition",
|
|
100
|
+
recipe: "motion-glide",
|
|
101
|
+
part: "Dot",
|
|
102
|
+
why: 'The active dot stretches to a bar while the one it left shrinks, on one timeline, so the row keeps its length and the mark reads as travelling with the slide. It used to change width unanimated and jumped while the slides beside it glided. Expressive: motion="subtle" snaps it.'
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
slot: "activity",
|
|
106
|
+
recipe: "none",
|
|
107
|
+
part: "Root",
|
|
108
|
+
why: "Autoplay's progress is the slides themselves advancing and the play/pause control; a separate timer animation would put a second clock beside the first."
|
|
109
|
+
}
|
|
110
|
+
],
|
|
73
111
|
a11y: [
|
|
74
112
|
"Region carries aria-roledescription='carousel'; each slide is a group with aria-roledescription='slide' and an 'N of M' label; clones are aria-hidden.",
|
|
75
113
|
"Arrows and dots are real buttons (dots set aria-current); Left/Right arrow keys navigate when the viewport is focused.",
|
|
@@ -92,6 +130,10 @@ var carouselMeta = {
|
|
|
92
130
|
{
|
|
93
131
|
label: "Autoplay",
|
|
94
132
|
code: "<Carousel loop autoplay={4000} counter>{slides}</Carousel>"
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
label: "Subtle motion",
|
|
136
|
+
code: '<Carousel loop motion="subtle" label="Gallery">{slides}</Carousel>'
|
|
95
137
|
}
|
|
96
138
|
]
|
|
97
139
|
};
|
|
@@ -6,9 +6,11 @@ import { Icon } from "../../icon/index.js";
|
|
|
6
6
|
import { cn } from "../../lib/cn.js";
|
|
7
7
|
import { useTypeTier } from "../../lib/density.js";
|
|
8
8
|
import { setRef } from "../../lib/refs.js";
|
|
9
|
+
import { motionAttribute } from "../../motion/MotionProvider.js";
|
|
9
10
|
import { Button } from "../button/index.js";
|
|
10
11
|
import { Textarea } from "../textarea/index.js";
|
|
11
12
|
import { chatComposerVariants } from "./chat-composer.variants.js";
|
|
13
|
+
import { useEasedGrowth } from "./use-eased-growth.js";
|
|
12
14
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
13
15
|
function ChatComposer({
|
|
14
16
|
value,
|
|
@@ -31,6 +33,7 @@ function ChatComposer({
|
|
|
31
33
|
stopLabel = "Stop generating",
|
|
32
34
|
footer,
|
|
33
35
|
fieldRef,
|
|
36
|
+
motion,
|
|
34
37
|
className,
|
|
35
38
|
children,
|
|
36
39
|
...props
|
|
@@ -82,9 +85,11 @@ function ChatComposer({
|
|
|
82
85
|
[send, mode]
|
|
83
86
|
);
|
|
84
87
|
const showStop = busy && Boolean(onStop);
|
|
88
|
+
useEasedGrowth(field, text);
|
|
85
89
|
return /* @__PURE__ */ jsxs(
|
|
86
90
|
"form",
|
|
87
91
|
{
|
|
92
|
+
"data-motion": motionAttribute(motion),
|
|
88
93
|
onSubmit: handleSubmit,
|
|
89
94
|
className: cn(chatComposerVariants({ invalid }), className),
|
|
90
95
|
...props,
|
|
@@ -120,6 +125,9 @@ function ChatComposer({
|
|
|
120
125
|
// Inline padding puts the text on the attach glyph's edge: an
|
|
121
126
|
// icon-sm button centres a 1.125rem glyph in --control-sm.
|
|
122
127
|
"min-h-0 max-h-40 resize-none border-0 bg-transparent px-[calc((var(--control-sm)-1.125rem)/2)] py-1 focus-visible:ring-0",
|
|
128
|
+
// Past the cap the field scrolls; the platform bar (stepper arrows on
|
|
129
|
+
// Windows) would poke into the composer's rounded corner.
|
|
130
|
+
"scrollbar-quiet",
|
|
123
131
|
// What you type has to match what it becomes: the field and the
|
|
124
132
|
// Bubble it turns into both read `prose`, so the message does not
|
|
125
133
|
// change size the moment it is sent.
|
|
@@ -143,33 +151,25 @@ function ChatComposer({
|
|
|
143
151
|
leading,
|
|
144
152
|
/* @__PURE__ */ jsx("div", { className: "flex-1" }),
|
|
145
153
|
actions,
|
|
146
|
-
|
|
154
|
+
/* @__PURE__ */ jsx(
|
|
147
155
|
Button,
|
|
148
156
|
{
|
|
149
|
-
type: "button",
|
|
150
|
-
variant: "secondary",
|
|
157
|
+
type: showStop ? "button" : "submit",
|
|
158
|
+
variant: showStop ? "secondary" : "primary",
|
|
151
159
|
size: "icon-sm",
|
|
152
|
-
"aria-label": stopLabel,
|
|
153
|
-
|
|
160
|
+
"aria-label": showStop ? stopLabel : sendLabel,
|
|
161
|
+
disabled: showStop ? false : !canSend,
|
|
162
|
+
loading: !showStop && busy,
|
|
163
|
+
onClick: showStop ? (event) => {
|
|
164
|
+
event.preventDefault();
|
|
154
165
|
onStop?.();
|
|
155
166
|
if (!coarse) field.current?.focus();
|
|
156
|
-
},
|
|
157
|
-
children: /* @__PURE__ */
|
|
167
|
+
} : void 0,
|
|
168
|
+
children: /* @__PURE__ */ jsxs("span", { "aria-hidden": "true", className: "motion-icon-swap", children: [
|
|
169
|
+
/* @__PURE__ */ jsx("span", { "data-active": !showStop, className: "inline-flex", children: /* @__PURE__ */ jsx(Icon, { name: "arrow-up" }) }),
|
|
170
|
+
/* @__PURE__ */ jsx("span", { "data-active": showStop, className: "inline-flex", children: /* @__PURE__ */ jsx(Icon, { name: "stop", variant: "solid", className: "text-[0.75em]" }) })
|
|
171
|
+
] })
|
|
158
172
|
}
|
|
159
|
-
) : (
|
|
160
|
-
// Busy without a stop handler: Button's own `loading` swaps the glyph
|
|
161
|
-
// for its Spinner and keeps full colour, rather than a hand-spun icon.
|
|
162
|
-
/* @__PURE__ */ jsx(
|
|
163
|
-
Button,
|
|
164
|
-
{
|
|
165
|
-
type: "submit",
|
|
166
|
-
size: "icon-sm",
|
|
167
|
-
"aria-label": sendLabel,
|
|
168
|
-
disabled: !canSend,
|
|
169
|
-
loading: busy,
|
|
170
|
-
children: /* @__PURE__ */ jsx(Icon, { name: "arrow-up" })
|
|
171
|
-
}
|
|
172
|
-
)
|
|
173
173
|
)
|
|
174
174
|
] }),
|
|
175
175
|
footerContent ? /* @__PURE__ */ jsx("p", { className: cn("px-[calc((var(--control-sm)-1.125rem)/2)] text-muted-foreground", tier.secondary), children: footerContent }) : null,
|
|
@@ -209,6 +209,12 @@ var chatComposerMeta = {
|
|
|
209
209
|
type: "Ref<HTMLTextAreaElement>",
|
|
210
210
|
description: "Ref to the underlying textarea \u2014 for focusing after a send, or on mount, and the extension point for anything that listens to the field. Pass `useCaretTrigger`'s `fieldRef` here for mentions and slash commands (CaretMenu). An Enter another listener already handled (`preventDefault()`) does not send."
|
|
211
211
|
},
|
|
212
|
+
{
|
|
213
|
+
name: "motion",
|
|
214
|
+
type: '"full" | "subtle" | "off"',
|
|
215
|
+
default: '"full"',
|
|
216
|
+
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 field snaps to its new height and the send/stop glyphs swap without the morph."
|
|
217
|
+
},
|
|
212
218
|
{
|
|
213
219
|
name: "...form props",
|
|
214
220
|
type: 'Omit<ComponentPropsWithRef<"form">, "onSubmit" | "defaultValue">',
|
|
@@ -230,6 +236,38 @@ var chatComposerMeta = {
|
|
|
230
236
|
"accent",
|
|
231
237
|
"accent-foreground"
|
|
232
238
|
],
|
|
239
|
+
motion: [
|
|
240
|
+
{
|
|
241
|
+
slot: "arrival",
|
|
242
|
+
recipe: "none",
|
|
243
|
+
part: "Root",
|
|
244
|
+
why: "The composer is the fixed foot of a thread, there before anything else is. An entrance would only delay the first keystroke."
|
|
245
|
+
},
|
|
246
|
+
{
|
|
247
|
+
slot: "response",
|
|
248
|
+
recipe: "motion-interactive",
|
|
249
|
+
part: "Root",
|
|
250
|
+
why: "The surface's focus ring and invalid border ease in, so focusing the field reads as the whole box waking up rather than a frame flicker."
|
|
251
|
+
},
|
|
252
|
+
{
|
|
253
|
+
slot: "transition",
|
|
254
|
+
recipe: "custom",
|
|
255
|
+
part: "Field",
|
|
256
|
+
why: "As a line breaks or a paste lands, the field eases to its new height (up to its cap) instead of jumping, so the transcript above is visibly pushed rather than teleported. `field-sizing` alone cannot ease \u2014 its height stays `auto` \u2014 so a small hook measures and pins a pixel height. Expressive and short (`fast`): the text itself lands at once, and `subtle` snaps the box."
|
|
257
|
+
},
|
|
258
|
+
{
|
|
259
|
+
slot: "transition",
|
|
260
|
+
recipe: "motion-icon-swap",
|
|
261
|
+
part: "Send button",
|
|
262
|
+
why: "Send turning into stop is the moment a reply starts, and stop turning back is the moment it is over. One button changes role in place \u2014 the fill eases, the arrow collapses into the square \u2014 so it reads as the same control changing what it does, not a different button appearing under the cursor."
|
|
263
|
+
},
|
|
264
|
+
{
|
|
265
|
+
slot: "activity",
|
|
266
|
+
recipe: "motion-spin",
|
|
267
|
+
part: "Send button",
|
|
268
|
+
why: "Busy with no stop handler, Button's own loading Spinner replaces the arrow, so a send in flight is not mistaken for a disabled one."
|
|
269
|
+
}
|
|
270
|
+
],
|
|
233
271
|
a11y: [
|
|
234
272
|
"A real `<form>`, so the send button is a submit button and the whole thing behaves as one control to assistive tech and to password managers alike.",
|
|
235
273
|
"Enter never submits while an IME composition is open (`isComposing`), so a CJK reader confirming a candidate word does not mail a fragment of their first sentence.",
|