@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
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# Motion
|
|
2
2
|
|
|
3
|
-
Motion explains a change: where something came from, what responded, and that work is happening.
|
|
3
|
+
Motion explains a change: where something came from, what responded, and that work is happening. Components ship crafted by default: one indicator travels instead of two blinking, numbers roll, ticks draw, a refused answer shakes. It is built from named recipes, comes in two levels, and one switch turns it off.
|
|
4
4
|
|
|
5
|
-
**4 slots · 3 speeds · enter-only overshoot · 1 kill switch**
|
|
5
|
+
**4 slots · 3 speeds · 2 levels · enter-only overshoot · 1 kill switch**
|
|
6
6
|
|
|
7
7
|
## Three speeds
|
|
8
8
|
|
|
@@ -26,7 +26,13 @@ Reach for one named class before writing a transition.
|
|
|
26
26
|
| `motion-pop` | enter base · spring | Anchored or centred surfaces: popovers, menus, dialogs. |
|
|
27
27
|
| `motion-slide-{up,down,left,right}` | enter base / exit fast | Directional surfaces: sheets, toasts. |
|
|
28
28
|
| `motion-disclosure` | base · standard | Height changes: accordion, collapsible. |
|
|
29
|
-
| `motion-icon-swap` | enter · emphasized cross-fade | Two icons of one control trading places: play/pause, outline/fill. |
|
|
29
|
+
| `motion-icon-swap` | enter · emphasized cross-fade | Two icons of one control trading places: play/pause, outline/fill, copy/check. |
|
|
30
|
+
| `motion-glide` | slow · emphasized · expressive | ONE indicator travelling to the selected option: tabs, segmented thumb, active dot. |
|
|
31
|
+
| `motion-shake` | ~400ms decaying · expressive | A refused answer: wrong code, invalid submit. Plays while data-shake is set (useShake). Never the only signal. |
|
|
32
|
+
| `motion-tick` | fast fade + base wipe · expressive | A check mark that draws itself in. Exits only fade. |
|
|
33
|
+
| `motion-appear` | fast fade + base scale | A small control mounting in place: a clear button, a send arrow, a badge. @starting-style, no JS. |
|
|
34
|
+
| `motion-arrive` | base · decelerate · staggered · expressive | Items a user action brings into a list: a folder's children on expand, a row just added. Never on every render. |
|
|
35
|
+
| `motion-size` | base · emphasized · expressive | An element easing between intrinsic sizes when its label changes: a two-step confirm, Copy → Copied. |
|
|
30
36
|
| `motion-draw` | slower · decelerate | SVG line art drawing on when a <Reveal> scrolls in. The path needs pathLength="1"; stagger siblings with --motion-draw-delay. |
|
|
31
37
|
| `motion-spin · pulse · shimmer · dot-bounce · ping` | loops | Activity. Scale-aware, so reduced motion freezes them. |
|
|
32
38
|
|
|
@@ -40,6 +46,19 @@ Reach for one named class before writing a transition.
|
|
|
40
46
|
| `ease-spring` | ~4% overshoot | A gentle pop on enter. Never on exit. |
|
|
41
47
|
| `ease-bounce` | ~10% overshoot | A playful pop, opt-in only. Enter only, never on exit. |
|
|
42
48
|
|
|
49
|
+
## Two levels
|
|
50
|
+
|
|
51
|
+
Essential motion answers the user; expressive motion narrates continuity. Expressive durations are also multiplied by --motion-expressive, which `subtle` sets to 0. Each level can only turn motion down from what surrounds it.
|
|
52
|
+
|
|
53
|
+
| Name | Value | Use |
|
|
54
|
+
|---|---|---|
|
|
55
|
+
| `essential` | always, unless off | Colour and focus feedback, press, overlay arrival, disclosure height, loaders, chart hover. |
|
|
56
|
+
| `expressive` | full only | Glides, rolling numbers (NumberTicker), drawn ticks, shakes, staggers, chart arrival and morph, scroll reveals, spring overshoot. |
|
|
57
|
+
| `full` | default | Both levels play. The crafted default. |
|
|
58
|
+
| `subtle` | data-motion="subtle" | Essential only; nothing overshoots. For dense tools, embedded widgets, readers who want calm without stillness. |
|
|
59
|
+
| `off` | data-motion="off" · reduce-motion | Nothing moves; every state still changes. |
|
|
60
|
+
| `where` | provider · scope · prop | <MotionProvider policy> for the app, <MotionScope level> for a region, `motion` on a component that owns a flourish. |
|
|
61
|
+
|
|
43
62
|
## The four slots
|
|
44
63
|
|
|
45
64
|
Every interactive component answers all four, by filling the slot or declining it with a reason in meta.motion.
|
|
@@ -60,6 +79,10 @@ Every interactive component answers all four, by filling the slot or declining i
|
|
|
60
79
|
5. **Durations come from tokens, so --motion-scale can stop them.** prefers-reduced-motion and <MotionProvider> off both set --motion-scale to 0. A raw duration ignores that. *Enforced by sable-lint: raw-duration, builtin-animate.*
|
|
61
80
|
6. **Interactive elements always respond. Something styled for hover with no motion recipe snaps.** A snap between states reads as broken. The four prohibitive rules could not catch missing motion, so this one does. *Enforced by sable-lint: static-interactive.*
|
|
62
81
|
7. **JS motion reads useMotion().enabled and does nothing when it is false.** Reduced motion is an accessibility setting, not a style preference.
|
|
82
|
+
8. **One thing travels; two things do not blink. When a selection, a value or a mark changes in place, move the one element that represents it.** A fill switching off here and on there is two events the eye has to connect. A glide, a roll or a draw is one, and it is most of what makes an interface feel made rather than assembled.
|
|
83
|
+
9. **Flourishes are expressive: multiply their durations by --motion-expressive and gate JS ones on useMotion().expressive.** `subtle` promises the answers without the choreography. A glide on a plain token keeps playing after someone asked for calm.
|
|
84
|
+
10. **A state change finishes its story: refused shakes and says why, success confirms in place, armed asks again.** Motion that starts a change and leaves the outcome to a snap reads as unfinished. ConfirmButton, OtpField's invalid shake and the copy-to-check swap are the reference cases.
|
|
85
|
+
11. **Component motion props and MotionScope only turn motion down.** `full` on a component means inherit. Nothing below the provider can switch motion back on for a reader who asked for it off.
|
|
63
86
|
|
|
64
87
|
## Never
|
|
65
88
|
|
|
@@ -82,6 +105,28 @@ Every interactive component answers all four, by filling the slot or declining i
|
|
|
82
105
|
|
|
83
106
|
Same feel, but reduced motion now resolves it instantly.
|
|
84
107
|
|
|
108
|
+
### A selection that travels
|
|
109
|
+
|
|
110
|
+
```tsx
|
|
111
|
+
// before
|
|
112
|
+
<Tab className="data-[active]:border-b-2 data-[active]:border-foreground" />
|
|
113
|
+
// after
|
|
114
|
+
<Tabs.Indicator className="motion-glide left-(--active-tab-left) w-(--active-tab-width)" />
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
The underline used to vanish under one tab and appear under the next. Now it slides, and Tabs renders it for you.
|
|
118
|
+
|
|
119
|
+
### A value that rolls
|
|
120
|
+
|
|
121
|
+
```tsx
|
|
122
|
+
// before
|
|
123
|
+
<span>{total}</span>
|
|
124
|
+
// after
|
|
125
|
+
<NumberTicker value={total} format={{ style: "currency", currency: "USD" }} />
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
The digits that changed roll; the ones that did not stay still, so the eye goes straight to the difference.
|
|
129
|
+
|
|
85
130
|
### Faster exits
|
|
86
131
|
|
|
87
132
|
```tsx
|
|
@@ -130,7 +130,7 @@ The `@utility` and `@custom-variant` layer in `styles.css`: what each one is, wh
|
|
|
130
130
|
- **Instead of** An animated fade or a hand-written `mask-image` gradient. Use `StreamingText` first; this is for a stream rendered without it.
|
|
131
131
|
- `<span className="stream-tail">{latestChunk}</span>`
|
|
132
132
|
|
|
133
|
-
### motion (
|
|
133
|
+
### motion (30)
|
|
134
134
|
|
|
135
135
|
- **`motion-interactive`** — The transition for interactive chrome: colour, background, border, shadow, opacity, fill, stroke, scale and translate at the fast duration and standard easing. Also removes the mobile tap delay and the platform's grey tap flash. *Use for:* On anything clickable that changes on hover, focus, press or selection: a custom row, a tile, a link, an app-owned button. It is the `response` slot, and the only transition declarer such an element needs.
|
|
136
136
|
- **Instead of** Tailwind's colour-only or all-property transition utilities with a hand-picked duration. Those ignore --motion-scale, skip the transform properties `motion-press` needs, and leave the 300ms tap delay on touch screens. sable-lint's `static-interactive` fires on a hover style with no recipe.
|
|
@@ -162,6 +162,28 @@ The `@utility` and `@custom-variant` layer in `styles.css`: what each one is, wh
|
|
|
162
162
|
- **`motion-icon-swap`** — Cross-fades two glyphs stacked in one cell when a control's icon changes with its state: the outgoing one shrinks and blurs out, the incoming one resolves in. The inactive glyph is the one marked `aria-hidden="true"` (or `data-active="false"`). *Use for:* Play/pause, mute/unmute, outline/fill, copy/check, menu/close: one control whose icon changes. Keep both icons mounted; the correct accessibility markup is also the animation hook.
|
|
163
163
|
- **Instead of** Rendering one icon or the other (it snaps between two frames), or absolutely positioning the second icon, which collapses the wrapper. Under reduced motion the swap is instant and the glyph still changes.
|
|
164
164
|
- `<span className="motion-icon-swap"><Icon name="play" aria-hidden={playing} /><Icon name="pause" aria-hidden={!playing} /></span>`
|
|
165
|
+
- **`motion-glide`** — Timing for ONE element that travels to the selected option — position and size ease on the emphasized curve at the slow step. Expressive: `data-motion="subtle"` makes it snap. *Use for:* A selection indicator you position yourself: a custom nav's current-page bar, a segmented control's thumb, an active dot. Tabs, ToggleGroup `segmented`, ThemeSwitcher and Carousel already glide.
|
|
166
|
+
- **Instead of** A fill or border on each option that switches on and off (two things blink instead of one thing moving), or a hand-written transition with a literal duration.
|
|
167
|
+
- `<span aria-hidden className="motion-glide absolute bottom-0 h-0.5 bg-foreground" style={{ left: x, width: w }} />`
|
|
168
|
+
- **`motion-shake`** — A short decaying horizontal shake that plays while `data-shake` is set. `useShake()` sets and clears the attribute and restarts it on a repeat refusal. Expressive: under `subtle` only the error colour remains. *Use for:* An answer the system refused: a wrong one-time code, a submit with invalid fields, a rejected password. Always alongside the error colour and message, never instead of them. OtpField's `invalid` already shakes.
|
|
169
|
+
- **Instead of** A keyframe shake with a literal duration, which keeps moving under reduced motion, or a shake with no error text.
|
|
170
|
+
- `const { shake, shakeProps } = useShake()
|
|
171
|
+
<form className="motion-shake" {...shakeProps} onSubmit={(e) => { if (!valid) { e.preventDefault(); shake() } }}>…</form>`
|
|
172
|
+
- **`motion-tick`** — A selection mark that draws itself in from its leading edge (a clip-path wipe, so it works on any icon set) with an essential fade, keyed to Base UI's starting/ending style. Exits only fade. The wipe is expressive. *Use for:* A check or success glyph that mounts with its state: a custom selection card's tick, a completed step. Checkbox and CheckboxCard already use it.
|
|
173
|
+
- **Instead of** A scale-from-zero pop on a check mark, which reads as a stamp rather than a tick, or a path-length draw that needs to own the SVG.
|
|
174
|
+
- `<Base.Indicator className="motion-tick"><Icon name="check" /></Base.Indicator>`
|
|
175
|
+
- **`motion-appear`** — Fades and scales a small control in on first render through `@starting-style` — no data attribute, no JS. The fade is essential, the scale is expressive. Removal is instant. *Use for:* A control that mounts in place as state changes: a search field's clear button, a send button replacing a mic, a count badge that appears. Put it on a wrapper around a Sable Button or anything carrying `motion-interactive`, so the two do not fight over `transition-property`.
|
|
176
|
+
- **Instead of** Conditionally rendering a control so it pops in on one frame, or wrapping a single icon button in Presence.
|
|
177
|
+
- `{value && <span className="flex motion-appear"><Button size="icon-xs" variant="ghost" aria-label="Clear"><Icon name="xmark" /></Button></span>}`
|
|
178
|
+
- **`motion-arrive`** — Items a user action brings into a list settle into place one after another: a short fade down from 0.25rem. Stagger with `--motion-arrive-delay` (raw ms; the recipe scales it). Expressive. *Use for:* Only on items that are NEW because of something the user did: a folder's children on expand, a row just added, results that load in. Tree's expand already uses it.
|
|
179
|
+
- **Instead of** A staggered entrance on every render of a list, which turns a list into a loading delay, or an inline keyframe animation with literal durations.
|
|
180
|
+
- `<li className="motion-arrive" style={{ "--motion-arrive-delay": "36ms" } as CSSProperties}>{row.name}</li>`
|
|
181
|
+
- **`motion-size`** — Eases an element between sizes: between a keyword and a length (0 ↔ auto) through `interpolate-size: allow-keywords`, and between measured pixel sizes when paired with `useAutoSize()`, so a button whose label changes stretches to the new width instead of jumping. A superset of motion-interactive's transition list, so it can sit on a Button. The size is expressive. *Use for:* A control whose content changes length in place: a two-step confirm button, a Copy → Copied label, a chip that gains a count. ConfirmButton already uses it.
|
|
182
|
+
- **Instead of** A hand-written width transition, or a fixed width that clips the longer label. A content-sized box computes `width: auto` before and after its label changes, so on its own the recipe has nothing to ease — that is what `useAutoSize()` is for.
|
|
183
|
+
- `const size = useAutoSize<HTMLButtonElement, HTMLSpanElement>()
|
|
184
|
+
<button type="button" ref={size.outerRef} className="motion-size">
|
|
185
|
+
<span ref={size.innerRef} className="inline-flex w-max">{copied ? "Copied" : "Copy link"}</span>
|
|
186
|
+
</button>`
|
|
165
187
|
- **`motion-spin`** — A continuous rotation for a spinner glyph, scaled by --motion-scale so reduced motion freezes it. *Use for:* A loading glyph that is not the Spinner component (use Spinner first), such as an icon inside a busy button.
|
|
166
188
|
- **Instead of** Tailwind's built-in spin animation, which ignores --motion-scale. sable-lint's `builtin-animate` flags it.
|
|
167
189
|
- `<Icon name="spinner" className="motion-spin" />`
|
|
@@ -69,6 +69,20 @@ and canceled work does not report success. Inspect the behavior at normal speed:
|
|
|
69
69
|
is the response immediate enough for its frequency, and does the movement explain
|
|
70
70
|
the change without delaying access to content?
|
|
71
71
|
|
|
72
|
+
Check continuity before timing. When a selection, value or mark changes in
|
|
73
|
+
place, one element should travel (an indicator glides, digits roll, a tick
|
|
74
|
+
draws) rather than one thing switching off and another on. A snap between two
|
|
75
|
+
states of the same control — a chevron that jumps, a fill that blinks from one
|
|
76
|
+
tab to the next, an icon replaced between frames — is a finding even when every
|
|
77
|
+
duration is a token. So is a story left unfinished: a refusal with no shake or
|
|
78
|
+
message, a success with no confirmation where it happened.
|
|
79
|
+
|
|
80
|
+
Run it three ways, not two. Under `data-motion="subtle"` (or the component's
|
|
81
|
+
`motion="subtle"`, or `<MotionScope level="subtle">`) feedback, overlays,
|
|
82
|
+
disclosure and loaders should still play while glides, rolls, draws, shakes,
|
|
83
|
+
staggers and overshoot resolve instantly; a flourish still moving there was
|
|
84
|
+
built on a plain token instead of an expressive one.
|
|
85
|
+
|
|
72
86
|
Repeat relevant behavior with reduced motion enabled. This should preserve state,
|
|
73
87
|
focus, status, and controls; hiding the animated content is not an equivalent
|
|
74
88
|
result. Test a custom driver directly—Sable's policy cannot disable an unrelated
|