@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
package/dist/styles.css
CHANGED
|
@@ -1201,7 +1201,8 @@
|
|
|
1201
1201
|
drops the hiding rule entirely and the action simply stays visible. */
|
|
1202
1202
|
@utility reveal-on-hover {
|
|
1203
1203
|
transition-property: color, background-color, border-color, box-shadow,
|
|
1204
|
-
opacity, fill, stroke, scale, translate
|
|
1204
|
+
opacity, fill, stroke, scale, translate, rotate, outline-color, outline-width,
|
|
1205
|
+
outline-offset;
|
|
1205
1206
|
transition-duration: var(--motion-duration-fast);
|
|
1206
1207
|
transition-timing-function: var(--motion-ease-standard);
|
|
1207
1208
|
|
|
@@ -1844,8 +1845,18 @@
|
|
|
1844
1845
|
collapse --motion-scale to 0 — one switch makes all token-driven motion
|
|
1845
1846
|
static (transitions resolve instantly; loaders hold their first frame).
|
|
1846
1847
|
|
|
1847
|
-
|
|
1848
|
-
|
|
1848
|
+
Two LEVELS sit under the switch. ESSENTIAL motion answers the user: colour
|
|
1849
|
+
and focus feedback, press, overlay arrival, disclosure height, loaders.
|
|
1850
|
+
EXPRESSIVE motion narrates continuity: an indicator that travels between
|
|
1851
|
+
options (motion-glide), digits that roll (NumberTicker), a tick that draws
|
|
1852
|
+
(motion-tick), a shake on a wrong answer (motion-shake), staggered arrivals
|
|
1853
|
+
and spring overshoot. Expressive durations are multiplied by a second
|
|
1854
|
+
factor, --motion-expressive, which `data-motion="subtle"` sets to 0 (and
|
|
1855
|
+
which also takes the overshoot out of ease-spring/ease-bounce). So "subtle"
|
|
1856
|
+
keeps every answer and drops every flourish; "off" drops both.
|
|
1857
|
+
|
|
1858
|
+
Opacity + transform only (compositor-friendly), short durations, overshoot on
|
|
1859
|
+
enter only. See DESIGN.md → Motion.
|
|
1849
1860
|
========================================================================== */
|
|
1850
1861
|
|
|
1851
1862
|
@keyframes sable-spin {
|
|
@@ -2009,10 +2020,22 @@
|
|
|
2009
2020
|
`motion-press` and the hover/active surface — the flash is a second, worse
|
|
2010
2021
|
answer. Both are inert on pointer devices, and neither is motion, so
|
|
2011
2022
|
--motion-scale: 0 and prefers-reduced-motion leave them untouched: the delay
|
|
2012
|
-
fix keeps working when animation is off, which is when it matters most.
|
|
2023
|
+
fix keeps working when animation is off, which is when it matters most.
|
|
2024
|
+
|
|
2025
|
+
`rotate` is in the list because Tailwind v4's `rotate-*` utilities write the
|
|
2026
|
+
standalone `rotate` property, not `transform`. Without it every disclosure
|
|
2027
|
+
chevron carrying this recipe (Accordion, Collapsible, Tree) jumped 180° in a
|
|
2028
|
+
single frame while the panel beside it eased open.
|
|
2029
|
+
|
|
2030
|
+
The outline properties are there for focus rings drawn with `outline-*`
|
|
2031
|
+
(the box-shadow `ring-*` ones were already covered). An outline's computed
|
|
2032
|
+
width is 0 while its style is `none`, so on focus the ring grows from the
|
|
2033
|
+
element's edge to its full width at the fast step instead of appearing in
|
|
2034
|
+
one frame; on blur it shrinks back. Under reduced motion it is instant. */
|
|
2013
2035
|
@utility motion-interactive {
|
|
2014
2036
|
transition-property: color, background-color, border-color, box-shadow,
|
|
2015
|
-
opacity, fill, stroke, scale, translate
|
|
2037
|
+
opacity, fill, stroke, scale, translate, rotate, outline-color, outline-width,
|
|
2038
|
+
outline-offset;
|
|
2016
2039
|
transition-duration: var(--motion-duration-fast);
|
|
2017
2040
|
transition-timing-function: var(--motion-ease-standard);
|
|
2018
2041
|
touch-action: manipulation;
|
|
@@ -2196,6 +2219,262 @@
|
|
|
2196
2219
|
}
|
|
2197
2220
|
}
|
|
2198
2221
|
|
|
2222
|
+
/* --------------------------------------------------------------------------
|
|
2223
|
+
Expressive recipes — continuity and choreography. Each duration is a token
|
|
2224
|
+
multiplied by --motion-expressive, so `data-motion="subtle"` resolves them
|
|
2225
|
+
instantly while the essential recipes above keep answering. Under the
|
|
2226
|
+
off/reduced policy the token itself is already 0.
|
|
2227
|
+
-------------------------------------------------------------------------- */
|
|
2228
|
+
|
|
2229
|
+
/* Glide — ONE element that travels to the selected option, instead of each
|
|
2230
|
+
option blinking its own fill: a tab's underline or pill, a segmented thumb,
|
|
2231
|
+
a carousel's active dot, a nav's current-page bar. The component positions it
|
|
2232
|
+
(Base UI's --active-tab-* vars, a measured box, or an index step); this
|
|
2233
|
+
recipe carries the timing.
|
|
2234
|
+
|
|
2235
|
+
Width and height are in the list on purpose. The glider is absolutely
|
|
2236
|
+
positioned, so resizing it lays out nothing else, and a pill that stretches
|
|
2237
|
+
to its new option's label width cannot be a scale without distorting its
|
|
2238
|
+
corners. Emphasized easing: it leaves quickly and settles, which is what
|
|
2239
|
+
reads as "one thing moved" rather than "two things swapped". */
|
|
2240
|
+
@utility motion-glide {
|
|
2241
|
+
transition-property: translate, width, height, inset, left, top, opacity, background-color;
|
|
2242
|
+
/* The travel is expressive; the glider fading in or out and its colour
|
|
2243
|
+
answering a hover are essential, so "subtle" keeps those. One duration
|
|
2244
|
+
per property — a shorter list repeats from the start. */
|
|
2245
|
+
transition-duration:
|
|
2246
|
+
calc(var(--motion-duration-slow) * var(--motion-expressive, 1)),
|
|
2247
|
+
calc(var(--motion-duration-slow) * var(--motion-expressive, 1)),
|
|
2248
|
+
calc(var(--motion-duration-slow) * var(--motion-expressive, 1)),
|
|
2249
|
+
calc(var(--motion-duration-slow) * var(--motion-expressive, 1)),
|
|
2250
|
+
calc(var(--motion-duration-slow) * var(--motion-expressive, 1)),
|
|
2251
|
+
calc(var(--motion-duration-slow) * var(--motion-expressive, 1)),
|
|
2252
|
+
var(--motion-duration-fast),
|
|
2253
|
+
var(--motion-duration-fast);
|
|
2254
|
+
transition-timing-function: var(--motion-ease-emphasized), var(--motion-ease-emphasized),
|
|
2255
|
+
var(--motion-ease-emphasized), var(--motion-ease-emphasized), var(--motion-ease-emphasized),
|
|
2256
|
+
var(--motion-ease-emphasized), var(--motion-ease-standard), var(--motion-ease-standard);
|
|
2257
|
+
}
|
|
2258
|
+
|
|
2259
|
+
/* Shake — a short horizontal no, for an answer that was refused: a wrong
|
|
2260
|
+
one-time code, a submit with invalid fields. Plays while `data-shake` is set;
|
|
2261
|
+
`useShake()` (src/motion) sets it, clears it on animationend, and restarts it
|
|
2262
|
+
when the next refusal lands mid-shake. Decaying amplitude, about a fifth of a
|
|
2263
|
+
second per swing — anything longer reads as a tantrum.
|
|
2264
|
+
|
|
2265
|
+
Never the only signal. The field turns destructive and says why; the shake
|
|
2266
|
+
is what makes a repeated mistake feel noticed. Under "subtle" the duration
|
|
2267
|
+
is 0 and only the colour remains. */
|
|
2268
|
+
@keyframes sable-shake {
|
|
2269
|
+
0%,
|
|
2270
|
+
100% {
|
|
2271
|
+
translate: 0 0;
|
|
2272
|
+
}
|
|
2273
|
+
20% {
|
|
2274
|
+
translate: -0.375rem 0;
|
|
2275
|
+
}
|
|
2276
|
+
40% {
|
|
2277
|
+
translate: 0.3rem 0;
|
|
2278
|
+
}
|
|
2279
|
+
60% {
|
|
2280
|
+
translate: -0.2rem 0;
|
|
2281
|
+
}
|
|
2282
|
+
80% {
|
|
2283
|
+
translate: 0.1rem 0;
|
|
2284
|
+
}
|
|
2285
|
+
}
|
|
2286
|
+
@utility motion-shake {
|
|
2287
|
+
&[data-shake] {
|
|
2288
|
+
animation: sable-shake calc(var(--motion-duration-slower) * 0.8 * var(--motion-expressive, 1))
|
|
2289
|
+
var(--motion-ease-standard);
|
|
2290
|
+
}
|
|
2291
|
+
}
|
|
2292
|
+
|
|
2293
|
+
/* Tick — a selection mark that draws itself in rather than appearing: the
|
|
2294
|
+
check in a Checkbox, a success glyph. The mark wipes in from its leading
|
|
2295
|
+
stroke (clip-path, so it works on any injected icon set — a path-length draw
|
|
2296
|
+
would need to own the SVG) while its opacity takes the essential fade.
|
|
2297
|
+
Keyed to Base UI's starting/ending style like the overlay recipes; exits only
|
|
2298
|
+
fade, at the exit speed, so un-checking is not a performance.
|
|
2299
|
+
|
|
2300
|
+
Two durations: the fade is essential (fast), the wipe is expressive, so
|
|
2301
|
+
"subtle" keeps a clean fade and drops the draw. */
|
|
2302
|
+
@utility motion-tick {
|
|
2303
|
+
/* Both ends of the wipe must be inset() so they interpolate: from the
|
|
2304
|
+
default `none` the clip-path flipped in one frame and only the fade and
|
|
2305
|
+
scale played. Outset by 2px so the resting clip never shaves an injected
|
|
2306
|
+
glyph's round caps. */
|
|
2307
|
+
clip-path: inset(-2px);
|
|
2308
|
+
transition-property: opacity, clip-path, scale;
|
|
2309
|
+
transition-duration: var(--motion-duration-fast),
|
|
2310
|
+
calc(var(--motion-duration-base) * var(--motion-expressive, 1)),
|
|
2311
|
+
calc(var(--motion-duration-base) * var(--motion-expressive, 1));
|
|
2312
|
+
transition-timing-function: var(--motion-ease-standard), var(--motion-ease-decelerate),
|
|
2313
|
+
var(--motion-ease-spring);
|
|
2314
|
+
&[data-starting-style] {
|
|
2315
|
+
opacity: 0;
|
|
2316
|
+
clip-path: inset(-2px 100% -2px -2px);
|
|
2317
|
+
scale: 0.6;
|
|
2318
|
+
}
|
|
2319
|
+
&[data-ending-style] {
|
|
2320
|
+
opacity: 0;
|
|
2321
|
+
transition-duration: var(--motion-duration-exit), 0s, 0s;
|
|
2322
|
+
}
|
|
2323
|
+
}
|
|
2324
|
+
|
|
2325
|
+
/* Appear — a small control that mounts in place and should not pop into
|
|
2326
|
+
existence: a search field's clear button, a send button replacing a mic, a
|
|
2327
|
+
count badge. Uses @starting-style, so it needs no data attribute and no JS;
|
|
2328
|
+
the element simply transitions in from its starting style on first render.
|
|
2329
|
+
The fade is essential, the scale is expressive. Removal is instant — an
|
|
2330
|
+
unmounted element has nothing to animate, and a control leaving should get
|
|
2331
|
+
out of the way. It declares its own transition-property, so put it on a
|
|
2332
|
+
WRAPPER around a Button or anything carrying motion-interactive — on the
|
|
2333
|
+
same element one of the two lists would silently win. */
|
|
2334
|
+
@utility motion-appear {
|
|
2335
|
+
transition-property: opacity, scale;
|
|
2336
|
+
transition-duration: var(--motion-duration-fast),
|
|
2337
|
+
calc(var(--motion-duration-base) * var(--motion-expressive, 1));
|
|
2338
|
+
transition-timing-function: var(--motion-ease-standard), var(--motion-ease-spring);
|
|
2339
|
+
@starting-style {
|
|
2340
|
+
opacity: 0;
|
|
2341
|
+
scale: 0.6;
|
|
2342
|
+
}
|
|
2343
|
+
}
|
|
2344
|
+
|
|
2345
|
+
/* Arrive — items that a user action brings into a list settle into place one
|
|
2346
|
+
after another, so the eye can follow where they came from: a tree folder's
|
|
2347
|
+
children on expand, results that load in, a row that was just added.
|
|
2348
|
+
A CSS animation rather than a transition, so it composes with the
|
|
2349
|
+
motion-interactive every row already carries. Set the item's stagger as a
|
|
2350
|
+
raw delay in --motion-arrive-delay (index x ~18ms, capped around ten items so
|
|
2351
|
+
a long list settles as fast as a short one); the recipe scales it, so
|
|
2352
|
+
"subtle" and reduced motion show the items at once with no dead wait.
|
|
2353
|
+
`backwards` holds the start pose through the delay and then lets go, so a
|
|
2354
|
+
later opacity (a dimmed row) is never pinned.
|
|
2355
|
+
|
|
2356
|
+
Only on a user action. A list that arrives this way every time it renders is
|
|
2357
|
+
a title sequence: mount the class only on items that are new. */
|
|
2358
|
+
@keyframes sable-arrive {
|
|
2359
|
+
from {
|
|
2360
|
+
opacity: 0;
|
|
2361
|
+
translate: 0 -0.25rem;
|
|
2362
|
+
}
|
|
2363
|
+
}
|
|
2364
|
+
@utility motion-arrive {
|
|
2365
|
+
animation: sable-arrive calc(var(--motion-duration-base) * var(--motion-expressive, 1))
|
|
2366
|
+
var(--motion-ease-decelerate)
|
|
2367
|
+
calc(var(--motion-arrive-delay, 0ms) * var(--motion-scale, 1) * var(--motion-expressive, 1))
|
|
2368
|
+
backwards;
|
|
2369
|
+
}
|
|
2370
|
+
|
|
2371
|
+
/* Calendar month paging — react-day-picker's `animate` mode, which Calendar
|
|
2372
|
+
turns on only while motion is expressive. RDP keeps the outgoing month as a
|
|
2373
|
+
snapshot laid over the incoming one and adds ONE class to each part (so
|
|
2374
|
+
these are plain classes, not utilities: a class list would not survive
|
|
2375
|
+
classList.add). The months trade places over a short travel with a fade,
|
|
2376
|
+
not RDP's default full-width slide, which drags a whole grid across the
|
|
2377
|
+
card. Enter at the slow step on the emphasized curve; exit at the exit step,
|
|
2378
|
+
so the old month is out of the way before the new one settles.
|
|
2379
|
+
|
|
2380
|
+
Naming follows RDP: "after" means the month that arrives is AFTER the one
|
|
2381
|
+
leaving (paging forward), so it comes in from the end and the old one leaves
|
|
2382
|
+
toward the start. Mirrored under [dir=rtl]. */
|
|
2383
|
+
@keyframes sable-month-from-start {
|
|
2384
|
+
from {
|
|
2385
|
+
opacity: 0;
|
|
2386
|
+
translate: -1.5rem 0;
|
|
2387
|
+
}
|
|
2388
|
+
}
|
|
2389
|
+
@keyframes sable-month-from-end {
|
|
2390
|
+
from {
|
|
2391
|
+
opacity: 0;
|
|
2392
|
+
translate: 1.5rem 0;
|
|
2393
|
+
}
|
|
2394
|
+
}
|
|
2395
|
+
@keyframes sable-month-to-start {
|
|
2396
|
+
to {
|
|
2397
|
+
opacity: 0;
|
|
2398
|
+
translate: -1.5rem 0;
|
|
2399
|
+
}
|
|
2400
|
+
}
|
|
2401
|
+
@keyframes sable-month-to-end {
|
|
2402
|
+
to {
|
|
2403
|
+
opacity: 0;
|
|
2404
|
+
translate: 1.5rem 0;
|
|
2405
|
+
}
|
|
2406
|
+
}
|
|
2407
|
+
@keyframes sable-fade-out {
|
|
2408
|
+
to {
|
|
2409
|
+
opacity: 0;
|
|
2410
|
+
}
|
|
2411
|
+
}
|
|
2412
|
+
.sable-month-enter-after,
|
|
2413
|
+
.sable-month-enter-before {
|
|
2414
|
+
animation: sable-month-from-end calc(var(--motion-duration-slow) * var(--motion-expressive, 1))
|
|
2415
|
+
var(--motion-ease-emphasized) both;
|
|
2416
|
+
}
|
|
2417
|
+
.sable-month-enter-before,
|
|
2418
|
+
[dir="rtl"] .sable-month-enter-after {
|
|
2419
|
+
animation-name: sable-month-from-start;
|
|
2420
|
+
}
|
|
2421
|
+
[dir="rtl"] .sable-month-enter-before {
|
|
2422
|
+
animation-name: sable-month-from-end;
|
|
2423
|
+
}
|
|
2424
|
+
.sable-month-exit-after,
|
|
2425
|
+
.sable-month-exit-before {
|
|
2426
|
+
animation: sable-month-to-start calc(var(--motion-duration-exit) * var(--motion-expressive, 1))
|
|
2427
|
+
var(--motion-ease-standard) both;
|
|
2428
|
+
}
|
|
2429
|
+
.sable-month-exit-after,
|
|
2430
|
+
[dir="rtl"] .sable-month-exit-before {
|
|
2431
|
+
animation-name: sable-month-to-end;
|
|
2432
|
+
}
|
|
2433
|
+
[dir="rtl"] .sable-month-exit-after {
|
|
2434
|
+
animation-name: sable-month-to-start;
|
|
2435
|
+
}
|
|
2436
|
+
.sable-caption-enter {
|
|
2437
|
+
animation: sable-fade-in calc(var(--motion-duration-slow) * var(--motion-expressive, 1))
|
|
2438
|
+
var(--motion-ease-decelerate) both;
|
|
2439
|
+
}
|
|
2440
|
+
.sable-caption-exit {
|
|
2441
|
+
animation: sable-fade-out calc(var(--motion-duration-exit) * var(--motion-expressive, 1))
|
|
2442
|
+
var(--motion-ease-standard) both;
|
|
2443
|
+
}
|
|
2444
|
+
|
|
2445
|
+
/* Size — eases an element between sizes: a button whose label changes
|
|
2446
|
+
("Delete" → "Confirm delete?"), a chip that gains a count, a panel going from
|
|
2447
|
+
0 to its natural height.
|
|
2448
|
+
|
|
2449
|
+
A transition needs the computed size to CHANGE. Between a keyword and a
|
|
2450
|
+
length (0 ↔ auto) `interpolate-size: allow-keywords` supplies that. A
|
|
2451
|
+
content-sized box whose content changes computes `auto` both times, so pair
|
|
2452
|
+
the recipe with `useAutoSize()` (src/motion), which writes the content's
|
|
2453
|
+
measured size as pixels for this transition to ease between. Where
|
|
2454
|
+
interpolate-size is unsupported the keyword case simply snaps, which is the
|
|
2455
|
+
old behaviour. Expressive: under "subtle" the size snaps and the label still
|
|
2456
|
+
changes. */
|
|
2457
|
+
@utility motion-size {
|
|
2458
|
+
interpolate-size: allow-keywords;
|
|
2459
|
+
/* A superset of motion-interactive's list, in the same order after the two
|
|
2460
|
+
sizes, so it can sit on a Button (which carries motion-interactive) and
|
|
2461
|
+
whichever declaration wins the cascade, nothing the other animated stops
|
|
2462
|
+
animating. Only the two sizes are expressive. */
|
|
2463
|
+
transition-property: width, height, color, background-color, border-color, box-shadow,
|
|
2464
|
+
opacity, fill, stroke, scale, translate, rotate, outline-color, outline-width,
|
|
2465
|
+
outline-offset;
|
|
2466
|
+
/* One entry per property: a shorter list would REPEAT from the start. */
|
|
2467
|
+
transition-duration: calc(var(--motion-duration-base) * var(--motion-expressive, 1)),
|
|
2468
|
+
calc(var(--motion-duration-base) * var(--motion-expressive, 1)),
|
|
2469
|
+
var(--motion-duration-fast), var(--motion-duration-fast), var(--motion-duration-fast), var(--motion-duration-fast), var(--motion-duration-fast),
|
|
2470
|
+
var(--motion-duration-fast), var(--motion-duration-fast), var(--motion-duration-fast), var(--motion-duration-fast), var(--motion-duration-fast),
|
|
2471
|
+
var(--motion-duration-fast), var(--motion-duration-fast), var(--motion-duration-fast);
|
|
2472
|
+
transition-timing-function: var(--motion-ease-emphasized), var(--motion-ease-emphasized),
|
|
2473
|
+
var(--motion-ease-standard), var(--motion-ease-standard), var(--motion-ease-standard), var(--motion-ease-standard), var(--motion-ease-standard),
|
|
2474
|
+
var(--motion-ease-standard), var(--motion-ease-standard), var(--motion-ease-standard), var(--motion-ease-standard), var(--motion-ease-standard),
|
|
2475
|
+
var(--motion-ease-standard), var(--motion-ease-standard), var(--motion-ease-standard);
|
|
2476
|
+
}
|
|
2477
|
+
|
|
2199
2478
|
/* --------------------------------------------------------------------------
|
|
2200
2479
|
Loader recipes — looping indicators. Durations scale with --motion-scale, so
|
|
2201
2480
|
the policy switch freezes them to a static first frame.
|
|
@@ -2459,3 +2738,17 @@
|
|
|
2459
2738
|
[data-motion="off"] [data-sable-theme] {
|
|
2460
2739
|
--motion-scale: 0 !important;
|
|
2461
2740
|
}
|
|
2741
|
+
|
|
2742
|
+
/* "subtle": essential motion plays, expressive motion resolves instantly and
|
|
2743
|
+
nothing overshoots. It never touches --motion-scale, so it can only turn
|
|
2744
|
+
motion DOWN: nested inside "off", or under the OS reduce preference, the
|
|
2745
|
+
scale is still 0. That is what makes it safe as a per-component prop and on
|
|
2746
|
+
<MotionScope> — neither can switch motion back on for a reader who asked for
|
|
2747
|
+
it off. The easings are re-pointed rather than zeroed: a spring pop becomes a
|
|
2748
|
+
clean decelerate, which is still an answer. */
|
|
2749
|
+
[data-motion="subtle"],
|
|
2750
|
+
[data-motion="subtle"] [data-sable-theme] {
|
|
2751
|
+
--motion-expressive: 0 !important;
|
|
2752
|
+
--motion-ease-spring: var(--motion-ease-decelerate) !important;
|
|
2753
|
+
--motion-ease-bounce: var(--motion-ease-decelerate) !important;
|
|
2754
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# ApprovalBlock
|
|
2
|
+
|
|
3
|
+
An agent asking permission for one tool call: the question, what will happen and what can't be undone, the call's details, an "allow for the rest of this chat" Checkbox, and Deny / Allow.
|
|
4
|
+
|
|
5
|
+
**Intent:** Reach for this in a chat or agent run when a tool call waits on the person — sending, paying, deleting, publishing. The call itself, before and after the decision, is a ToolCall with status="awaiting", then "complete" or "denied". A destructive action outside an agent is DestructiveActionModal.
|
|
6
|
+
|
|
7
|
+
**Block:** AI & agents · card · designed for 18rem–28rem wide (fills its container up to the max)
|
|
8
|
+
|
|
9
|
+
## Notes
|
|
10
|
+
|
|
11
|
+
- The title is the question in plain words ("Send the invoice to Ada?"), not the tool's name; the tool's name is one row of the details, for the person who wants to know exactly what runs.
|
|
12
|
+
- The description says what can't be undone. That sentence is what the person is deciding on.
|
|
13
|
+
- Deny comes first and Allow is the primary on the end, the same order as every Sable confirmation. Neither is focused on arrival: an approval that a stray Enter can grant is not an approval.
|
|
14
|
+
- After a decision the buttons give way to one InlineStatus line saying what happened, so the thread keeps a record of the answer.
|
|
15
|
+
|
|
16
|
+
## Accessibility
|
|
17
|
+
|
|
18
|
+
- Focus moves to the outcome line when the buttons leave, instead of dropping to the page; the line is a role=status region, so the decision is announced.
|
|
19
|
+
- The details are a dl, so each value is read with its label.
|
|
20
|
+
- The remember Checkbox has its own Label, so the tool name inside it is part of its accessible name.
|
|
21
|
+
|
|
22
|
+
## Source
|
|
23
|
+
|
|
24
|
+
```tsx
|
|
25
|
+
import { useState, useEffect, useRef, useId } from "react"
|
|
26
|
+
import { Button, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, Checkbox, cn, Code, InlineStatus, Label } from "@sableui/react"
|
|
27
|
+
|
|
28
|
+
const request = {
|
|
29
|
+
tool: "send_email",
|
|
30
|
+
title: "Send the invoice to Ada?",
|
|
31
|
+
description: "The agent wants to email invoice INV-1042. An email can't be unsent.",
|
|
32
|
+
details: [
|
|
33
|
+
{ label: "To", value: "ada@example.com" },
|
|
34
|
+
{ label: "Subject", value: "INV-1042 for October" },
|
|
35
|
+
{ label: "Attached", value: "INV-1042.pdf" },
|
|
36
|
+
],
|
|
37
|
+
allowed: "Allowed. Sending the email.",
|
|
38
|
+
denied: "Denied. The agent won't send it.",
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface ApprovalBlockProps {
|
|
42
|
+
className?: string
|
|
43
|
+
/** false drops the card's surface and border — for a chat column that already frames it. */
|
|
44
|
+
framed?: boolean
|
|
45
|
+
/** The person allowed the call. `remember` is the "for the rest of this chat" choice. */
|
|
46
|
+
onApprove?: (options: { remember: boolean }) => void
|
|
47
|
+
/** The person declined the call. */
|
|
48
|
+
onDeny?: () => void
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function ApprovalBlock({ className, framed = true, onApprove, onDeny }: ApprovalBlockProps) {
|
|
52
|
+
const [decision, setDecision] = useState<"allowed" | "denied" | null>(null)
|
|
53
|
+
const [remember, setRemember] = useState(false)
|
|
54
|
+
const id = useId()
|
|
55
|
+
const outcome = useRef<HTMLDivElement>(null)
|
|
56
|
+
// The buttons leave with the decision, so focus moves to the line that
|
|
57
|
+
// replaces them instead of falling back to the top of the page.
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
if (decision) outcome.current?.focus()
|
|
60
|
+
}, [decision])
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<Card variant={framed ? "default" : "flat"} className={cn("w-full max-w-md", className)}>
|
|
64
|
+
<CardHeader>
|
|
65
|
+
<CardTitle>{request.title}</CardTitle>
|
|
66
|
+
<CardDescription>{request.description}</CardDescription>
|
|
67
|
+
</CardHeader>
|
|
68
|
+
<CardContent className="flex flex-col gap-4">
|
|
69
|
+
<dl className="grid grid-cols-[auto_minmax(0,1fr)] gap-x-4 gap-y-1.5 text-body-sm">
|
|
70
|
+
{request.details.map((d) => (
|
|
71
|
+
<div key={d.label} className="contents">
|
|
72
|
+
<dt className="text-muted-foreground">{d.label}</dt>
|
|
73
|
+
<dd className="truncate text-foreground">{d.value}</dd>
|
|
74
|
+
</div>
|
|
75
|
+
))}
|
|
76
|
+
<div className="contents">
|
|
77
|
+
<dt className="text-muted-foreground">Tool</dt>
|
|
78
|
+
<dd className="min-w-0">
|
|
79
|
+
<Code>{request.tool}</Code>
|
|
80
|
+
</dd>
|
|
81
|
+
</div>
|
|
82
|
+
</dl>
|
|
83
|
+
{decision ? null : (
|
|
84
|
+
<div className="flex items-center gap-2">
|
|
85
|
+
<Checkbox id={`${id}-remember`} checked={remember} onCheckedChange={(v: boolean) => setRemember(v)} />
|
|
86
|
+
<Label htmlFor={`${id}-remember`} className="text-body-sm">
|
|
87
|
+
Allow <Code>{request.tool}</Code> for the rest of this chat
|
|
88
|
+
</Label>
|
|
89
|
+
</div>
|
|
90
|
+
)}
|
|
91
|
+
</CardContent>
|
|
92
|
+
<CardFooter actions className="justify-end">
|
|
93
|
+
{decision ? (
|
|
94
|
+
<InlineStatus
|
|
95
|
+
ref={outcome}
|
|
96
|
+
tabIndex={-1}
|
|
97
|
+
role="status"
|
|
98
|
+
tone={decision === "allowed" ? "success" : "neutral"}
|
|
99
|
+
className="me-auto outline-none"
|
|
100
|
+
>
|
|
101
|
+
{decision === "allowed" ? request.allowed : request.denied}
|
|
102
|
+
</InlineStatus>
|
|
103
|
+
) : (
|
|
104
|
+
<>
|
|
105
|
+
<Button
|
|
106
|
+
variant="outline"
|
|
107
|
+
onClick={() => {
|
|
108
|
+
setDecision("denied")
|
|
109
|
+
onDeny?.()
|
|
110
|
+
}}
|
|
111
|
+
>
|
|
112
|
+
Deny
|
|
113
|
+
</Button>
|
|
114
|
+
<Button
|
|
115
|
+
onClick={() => {
|
|
116
|
+
setDecision("allowed")
|
|
117
|
+
onApprove?.({ remember })
|
|
118
|
+
}}
|
|
119
|
+
>
|
|
120
|
+
Allow
|
|
121
|
+
</Button>
|
|
122
|
+
</>
|
|
123
|
+
)}
|
|
124
|
+
</CardFooter>
|
|
125
|
+
</Card>
|
|
126
|
+
)
|
|
127
|
+
}
|
|
128
|
+
```
|
|
@@ -52,6 +52,7 @@ Use Accordion to reveal a vertical set of related sections while keeping the sur
|
|
|
52
52
|
- **arrival** · Root · _no motion_ — An accordion is page furniture that is present from first paint. Animating it in would stage content the reader is already scanning past.
|
|
53
53
|
- **response** · AccordionTrigger · `motion-interactive` — The whole header row is the hit target, and it looks like a heading rather than a control. The hover shift is what tells the reader it can be clicked before they click it.
|
|
54
54
|
- **transition** · AccordionPanel · `motion-disclosure` — Panel height is the only cue that content came from THIS header rather than appearing at the bottom of the page. Snapping it open moves everything below by a screenful with nothing connecting cause to effect.
|
|
55
|
+
- **transition** · Chevron · `motion-interactive` — The chevron turning is the header's half of the same event as the panel opening. motion-interactive lists `rotate` (what Tailwind's rotate-* writes), so it turns with the panel instead of flipping 180° in one frame beside an easing panel. A custom `indicator` that rotates should carry motion-interactive itself.
|
|
55
56
|
- **activity** · Root · _no motion_ — Nothing here is indeterminate — the panel content is already resident. A panel that loads on expand should put its own loader inside the panel.
|
|
56
57
|
|
|
57
58
|
## Accessibility
|
|
@@ -60,7 +61,7 @@ Use Accordion to reveal a vertical set of related sections while keeping the sur
|
|
|
60
61
|
- Arrow keys move roving focus between triggers; Home/End jump to the first/last; loopFocus wraps by default.
|
|
61
62
|
- Each trigger toggles its panel with Enter/Space; the panel is announced as a labelled region.
|
|
62
63
|
- A disabled AccordionItem dims and is skipped by keyboard focus while neighbouring items stay interactive.
|
|
63
|
-
- The chevron rotation is driven by data-[panel-open] and the focus ring by focus-visible
|
|
64
|
+
- The chevron rotation is driven by data-[panel-open] and the focus ring by focus-visible; both ride motion-interactive, so reduced motion stills them with everything else.
|
|
64
65
|
|
|
65
66
|
## Related
|
|
66
67
|
|
|
@@ -89,5 +90,5 @@ import { Accordion, AccordionItem, AccordionPanel, AccordionTrigger } from "@sab
|
|
|
89
90
|
```tsx
|
|
90
91
|
import { Accordion, AccordionItem, AccordionPanel, AccordionTrigger, Icon } from "@sableui/react"
|
|
91
92
|
|
|
92
|
-
<Accordion defaultValue={["a"]}><AccordionItem value="a"><AccordionTrigger indicator={<Icon name="plus" className="text-xs text-muted-foreground
|
|
93
|
+
<Accordion defaultValue={["a"]}><AccordionItem value="a"><AccordionTrigger indicator={<Icon name="plus" className="text-xs text-muted-foreground motion-interactive [[data-panel-open]_&]:rotate-45" />}>Can I cancel anytime?</AccordionTrigger><AccordionPanel>Yes — from billing settings.</AccordionPanel></AccordionItem></Accordion>
|
|
93
94
|
```
|
|
@@ -34,7 +34,8 @@ Use AreaChart for volume over time — visitors, revenue, usage — where the fi
|
|
|
34
34
|
| grid | boolean | true (false for horizontal bars) | Hairline gridlines along the value axis. |
|
|
35
35
|
| xAxis | boolean | true for columns; false for horizontal bars | The horizontal axis. |
|
|
36
36
|
| yAxis | boolean | false for columns; true for horizontal bars | The vertical axis. |
|
|
37
|
-
|
|
|
37
|
+
| motion | "full" \| "subtle" \| "off" | "full" | 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 — it never overrides a reader's reduce-motion setting. The arrival (draw-on, rising fill) and the data-change morph are the flourish; the gliding cursor and tooltip are the feedback `subtle` keeps. |
|
|
38
|
+
| animate | boolean | true | Play the arrival once the chart scrolls into view. Reduced motion shows the finished chart. `animate={false}` is `motion="subtle"`. |
|
|
38
39
|
| activeIndex | number \| null | — | The highlighted row (controlled); `null` for none. |
|
|
39
40
|
| defaultActiveIndex | number \| null | — | The row highlighted at first — a tooltip already open before anyone hovers. |
|
|
40
41
|
| onActiveIndexChange | (index: number \| null) => void | — | Called as the pointer or keyboard moves between rows. |
|
|
@@ -48,9 +49,9 @@ Use AreaChart for volume over time — visitors, revenue, usage — where the fi
|
|
|
48
49
|
|
|
49
50
|
## Motion
|
|
50
51
|
|
|
51
|
-
- **arrival** · Area + line · _custom (outside the recipe layer)_ — Once on screen, each line draws on (stroke-dashoffset) and its fill rises from the baseline, series staggered — the trend is read as it is drawn. On the motion tokens; reduced motion shows the finished chart.
|
|
52
|
+
- **arrival** · Area + line · _custom (outside the recipe layer)_ — Once on screen, each line draws on (stroke-dashoffset) and its fill rises from the baseline, series staggered — the trend is read as it is drawn. 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.
|
|
52
53
|
- **response** · Cursor + tooltip · _no motion_ — Hover and focus move the cursor and tooltip instantly — a lagging cursor reads as a slow chart.
|
|
53
|
-
- **transition** · Area + line · _custom (outside the recipe layer)_ — 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.
|
|
54
|
+
- **transition** · Area + line · _custom (outside the recipe layer)_ — 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.
|
|
54
55
|
- **activity** · AreaChart · _no motion_ — A chart has no indeterminate state; show a Skeleton while the data loads.
|
|
55
56
|
|
|
56
57
|
## Accessibility
|
|
@@ -54,6 +54,13 @@ The card a file takes in a conversation — in the composer while it uploads, an
|
|
|
54
54
|
|
|
55
55
|
`card`, `border`, `border-strong`, `muted`, `muted-foreground`, `foreground`, `destructive`, `overlay`, `overlay-foreground`, `ring`
|
|
56
56
|
|
|
57
|
+
## Motion
|
|
58
|
+
|
|
59
|
+
- **arrival** · Root · _no motion_ — An attachment lands as part of a composer or a message, and that surface owns the entrance. A card that animated in on its own would read as a second, separate event — as if another file had been added.
|
|
60
|
+
- **response** · Root · `motion-interactive` — With `onOpen`, the edge steps up under the pointer; it is the only cue that the card itself opens the file, not just its actions. Eased, it reads as the card answering the cursor rather than a border flickering as the pointer crosses a busy composer.
|
|
61
|
+
- **transition** · Root · `motion-interactive` — Uploading to failed re-colours the edge and fill without changing the shape. The ease keeps it legible as the same file turning red; snapped, a failed card looks like a different card replaced the one that was sent.
|
|
62
|
+
- **activity** · Progress · `motion-pulse` — While uploading with no known `progress`, the bar is indeterminate and pulses (Progress's own recipe); with a number, the fill eases toward each new value. A still bar at a fixed width is indistinguishable from an upload that has stalled.
|
|
63
|
+
|
|
57
64
|
## Accessibility
|
|
58
65
|
|
|
59
66
|
- The full-card trigger is a real stretched `<button>` with an accessible label, not a click handler on a div — so it is one tab stop, keyboard-activatable, and focus-ringed.
|
|
@@ -34,7 +34,8 @@ Use BarChart to compare amounts across categories — the default chart for "how
|
|
|
34
34
|
| grid | boolean | true (false for horizontal bars) | Hairline gridlines along the value axis. |
|
|
35
35
|
| xAxis | boolean | true for columns; false for horizontal bars | The horizontal axis. |
|
|
36
36
|
| yAxis | boolean | false for columns; true for horizontal bars | The vertical axis. |
|
|
37
|
-
|
|
|
37
|
+
| motion | "full" \| "subtle" \| "off" | "full" | 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 — 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. |
|
|
38
|
+
| animate | boolean | true | Play the arrival once the chart scrolls into view. Reduced motion shows the finished chart. `animate={false}` is `motion="subtle"`. |
|
|
38
39
|
| activeIndex | number \| null | — | The highlighted row (controlled); `null` for none. |
|
|
39
40
|
| defaultActiveIndex | number \| null | — | The row highlighted at first — a tooltip already open before anyone hovers. |
|
|
40
41
|
| onActiveIndexChange | (index: number \| null) => void | — | Called as the pointer or keyboard moves between rows. |
|
|
@@ -52,9 +53,9 @@ Use BarChart to compare amounts across categories — the default chart for "how
|
|
|
52
53
|
|
|
53
54
|
## Motion
|
|
54
55
|
|
|
55
|
-
- **arrival** · Bar · _custom (outside the recipe layer)_ — 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.
|
|
56
|
-
- **response** · Cursor + tooltip · _no motion_ — The hovered category gets a band behind it and the other bars dim at once — a lagging highlight reads as a slow chart.
|
|
57
|
-
- **transition** · Bar · _custom (outside the recipe layer)_ — 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.
|
|
56
|
+
- **arrival** · Bar · _custom (outside the recipe layer)_ — 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.
|
|
57
|
+
- **response** · Cursor + tooltip · _no motion_ — The hovered category gets a band behind it and the other bars dim at once — 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.
|
|
58
|
+
- **transition** · Bar · _custom (outside the recipe layer)_ — 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.
|
|
58
59
|
- **activity** · BarChart · _no motion_ — A chart has no indeterminate state; show a Skeleton while the data loads.
|
|
59
60
|
|
|
60
61
|
## Accessibility
|
|
@@ -34,6 +34,14 @@ Use BottomNav for a small set of top-level mobile destinations that should remai
|
|
|
34
34
|
|
|
35
35
|
`background`, `border`, `muted-foreground`, `foreground`, `ring`
|
|
36
36
|
|
|
37
|
+
## Motion
|
|
38
|
+
|
|
39
|
+
- **arrival** · Root · _no motion_ — 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.
|
|
40
|
+
- **response** · BottomNavItem · `motion-interactive` — 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.
|
|
41
|
+
- **response** · BottomNavItem · `motion-press` — 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.
|
|
42
|
+
- **transition** · BottomNavItem · `motion-interactive` — 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.
|
|
43
|
+
- **activity** · Root · _no motion_ — 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.
|
|
44
|
+
|
|
37
45
|
## Accessibility
|
|
38
46
|
|
|
39
47
|
- BottomNav renders a <nav> landmark.
|
|
@@ -45,6 +45,13 @@ Use Breadcrumbs to show the user's location inside a hierarchy and provide light
|
|
|
45
45
|
|
|
46
46
|
`muted-foreground`, `foreground`
|
|
47
47
|
|
|
48
|
+
## Motion
|
|
49
|
+
|
|
50
|
+
- **arrival** · Root · _no motion_ — 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.
|
|
51
|
+
- **response** · BreadcrumbLink · `motion-interactive` — 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.
|
|
52
|
+
- **transition** · Root · _no motion_ — 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.
|
|
53
|
+
- **activity** · Root · _no motion_ — It never works in the background. A route that is still loading says so in the page, not in its address.
|
|
54
|
+
|
|
48
55
|
## Accessibility
|
|
49
56
|
|
|
50
57
|
- The <nav> carries aria-label="Breadcrumb" so assistive tech announces the landmark.
|
|
@@ -13,7 +13,7 @@ Use Button when the user commits an action, submits a form, opens UI, or confirm
|
|
|
13
13
|
### Best practices
|
|
14
14
|
|
|
15
15
|
- Do: Use a single primary button in a view or dense panel so the action hierarchy stays obvious.
|
|
16
|
-
- Do: 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.
|
|
16
|
+
- Do: 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.
|
|
17
17
|
- Do: Write labels as actions, such as "Save changes" or "Delete draft", instead of vague labels like "OK".
|
|
18
18
|
- Do: Use loading with loadingLabel for async work so the action cannot double-fire and the state is announced.
|
|
19
19
|
- Do: Put icons in iconStart / iconEnd rather than as children, so the button applies its optical padding. Start icons say what the action is (plus, share); end icons say where it goes (chevron-right, external).
|
|
@@ -41,10 +41,10 @@ Use Button when the user commits an action, submits a form, opens UI, or confirm
|
|
|
41
41
|
| variant | "primary" \| "secondary" \| "ghost" \| "outline" \| "destructive" \| "link" \| "floating" \| "field" \| "on-color" \| "on-color-outline" \| "on-color-ghost" | "primary" | Visual emphasis tier. One primary per screen. The on-color tiers are for buttons ON a coloured fill: they paint with the inherited text colour (the fill's ink) — on-color is solid (ink background, fill-coloured label via the optional --on-color-fill var, else black/white), on-color-outline and on-color-ghost are transparent with an ink label. Never use them on page or card surfaces. floating is for a control that sits OVER content (carousel arrows, a marquee pause, a jump-to-latest over a conversation): its own edge, lift and translucent popover fill, so it reads on arbitrary content beneath. field is for a button docked in a workspace panel among xs fields: the inset panel-field fill and the field's corner, so a picker trigger or an add action reads as one of the row's controls; pair it with size xs or icon-xs. |
|
|
42
42
|
| size | "xs" \| "sm" \| "md" \| "cta" \| "icon" \| "icon-sm" \| "icon-xs" | "md" | Reads the density ladder, so every value differs by register: page 28/32/40/44, workspace 24/28/32/36. xs and icon-xs were added in 0.31.0 for the workspace control floor and are the sizes tool chrome wants; md is the default everywhere. icon/icon-sm/icon-xs are square. |
|
|
43
43
|
| render | ReactElement \| (props, state) => ReactElement | — | Compose with another element/component through the Base UI render prop. |
|
|
44
|
-
| loading | boolean | false | Show a
|
|
44
|
+
| loading | boolean | false | 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. |
|
|
45
45
|
| onClick | (event: MouseEvent<HTMLButtonElement>) => unknown | — | May return a promise: the button shows `loading` until it settles and ignores further presses meanwhile, from the first click on. Prefer it to a `busy` flag in state, which takes effect a render late (three fast clicks ran three times). |
|
|
46
46
|
| badge | ReactNode | — | A corner indicator (usually <NotificationBadge dot /> or a count) overlaid on the top-end corner of the glyph. For icon-only sizes; never hand-position a badge over a Button with absolute offsets. |
|
|
47
|
-
| loadingLabel | ReactNode | — | Optional text shown in place of children while loading. |
|
|
47
|
+
| loadingLabel | ReactNode | — | 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. |
|
|
48
48
|
| iconStart | ReactNode | — | Icon before the label. The button takes one 2px step off its start padding so icon + label read as centred. Replaced by the spinner while loading. |
|
|
49
49
|
| iconEnd | ReactNode | — | Icon after the label (disclosure, external link, next). Same optical correction on the end side. |
|
|
50
50
|
| bleed | "start" \| "end" \| "both" \| "none" | — | Optical alignment for a borderless button (ghost, link, on-color-ghost): a negative inline margin of exactly its own padding on that side — --button-px, or the icon side's optical padding when an icon sits there; an icon-only size bleeds by its inset to the glyph — so the label (or glyph) lands on the content edge. Hit area and hover/focus fill keep their full size. Logical sides, so it mirrors in RTL. A no-op on variants with a visible edge (primary, secondary, outline, destructive, floating, field, on-color, on-color-outline): a filled button aligns its edge, not its label. Unset, a borderless button renders data-bleed="auto" and SectionHeader, PageHeader and CardAction bleed it when it is their outermost child; none opts out. |
|
|
@@ -63,7 +63,7 @@ Use Button when the user commits an action, submits a form, opens UI, or confirm
|
|
|
63
63
|
- **arrival** · Root · _no motion_ — A button belongs to the layout it ships in. Whatever staged that region owns the entrance; a button that fades in on its own is a button that is briefly un-clickable for no stated reason.
|
|
64
64
|
- **response** · Root · `motion-interactive` — Hover, focus ring and disabled fade all ease on one declaration. This is the reference case for the whole recipe: it lists scale and translate alongside the colours specifically so motion-press can add a transform without a second, conflicting transition-property.
|
|
65
65
|
- **response** · Root · `motion-press` — The 0.97 settle on pointer-down is the only confirmation the press registered before the action's own result arrives — which on a slow network is the difference between waiting and clicking again. It skips disabled elements, so a dead button stays visibly dead.
|
|
66
|
-
- **transition** ·
|
|
66
|
+
- **transition** · Spinner · `motion-appear` — Going busy is the one state change a button has. The spinner fades in where it mounts — in the icon slot, or laid over a plain label that fades out beneath it and keeps its place — 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.
|
|
67
67
|
- **activity** · Root · _no motion_ — Pending work is passed in via `loading`, which renders a Spinner — the activity motion lives there rather than being duplicated here.
|
|
68
68
|
|
|
69
69
|
## Accessibility
|
|
@@ -118,10 +118,10 @@ import { Button, Icon, Menu, MenuContent, MenuItem, MenuTrigger, Row } from "@sa
|
|
|
118
118
|
### Copy to clipboard
|
|
119
119
|
|
|
120
120
|
```tsx
|
|
121
|
-
import { Button, Icon, useCopy } from "@sableui/react"
|
|
121
|
+
import { Button, Icon, Row, useCopy } from "@sableui/react"
|
|
122
122
|
|
|
123
123
|
const { copy, copied } = useCopy(address)
|
|
124
|
-
<Button variant="ghost" size="icon-sm" aria-label="Copy address" onClick={copy}><Icon name={copied
|
|
124
|
+
<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>
|
|
125
125
|
```
|
|
126
126
|
|
|
127
127
|
### As link
|