diff --git a/packages/docs/.vitepress/reference/catalog.ts b/packages/docs/.vitepress/reference/catalog.ts index ea256f9b..6b3987f2 100644 --- a/packages/docs/.vitepress/reference/catalog.ts +++ b/packages/docs/.vitepress/reference/catalog.ts @@ -992,13 +992,7 @@ export const referenceCatalog = [ packages: [motionPackage], status: 'stable', symbols: [ - motionSymbol('Motion', '/reference/items/Motion/js-api#motion'), - motionSymbol( - 'MotionScrollTimeline', - '/reference/items/Motion/js-api#motionscrolltimeline', - ), - motionSymbol('MotionSequence', '/reference/items/Motion/js-api#motionsequence'), - motionSymbol('MotionView', '/reference/items/Motion/js-api#motionview'), + motionSymbol('Motion', '/reference/items/Motion/js-api'), motionSymbol( 'provideMotion', '/reference/items/Motion/js-api#providing-the-motion-dependency', @@ -1016,7 +1010,62 @@ export const referenceCatalog = [ ), ], capabilities: ['enter animations', 'spring physics', 'imperative playback'], - related: ['action', 'transition', 'scroll-reveal'], + related: [ + 'motion-scroll-timeline', + 'motion-sequence', + 'motion-view', + 'action', + 'transition', + 'scroll-reveal', + ], + }, + { + id: 'motion-scroll-timeline', + title: 'MotionScrollTimeline', + summary: 'Drive the Motion children of an element with its scroll progress.', + kind: 'component', + path: '/reference/items/MotionScrollTimeline/', + family: 'animation', + primaryTask: 'animation', + tags: ['motion', 'animation', 'scroll', 'timeline'], + surfaces: ['js'], + packages: [motionPackage], + status: 'stable', + symbols: [motionSymbol('MotionScrollTimeline', '/reference/items/MotionScrollTimeline/js-api')], + capabilities: ['scroll-driven animations', 'hardware-accelerated scrubbing'], + related: ['motion', 'motion-sequence', 'scroll-animation'], + }, + { + id: 'motion-sequence', + title: 'MotionSequence', + summary: 'Compose Motion children into a single staggered animation timeline.', + kind: 'component', + path: '/reference/items/MotionSequence/', + family: 'animation', + primaryTask: 'animation', + tags: ['motion', 'animation', 'sequence', 'stagger'], + surfaces: ['js'], + packages: [motionPackage], + status: 'stable', + symbols: [motionSymbol('MotionSequence', '/reference/items/MotionSequence/js-api')], + capabilities: ['staggered choreography', 'single-timeline playback'], + related: ['motion', 'motion-scroll-timeline', 'action'], + }, + { + id: 'motion-view', + title: 'MotionView', + summary: 'Play DOM updates as view transitions declared with Motion keyframes.', + kind: 'component', + path: '/reference/items/MotionView/', + family: 'animation', + primaryTask: 'animation', + tags: ['motion', 'animation', 'view-transition', 'spring'], + surfaces: ['js'], + packages: [motionPackage], + status: 'stable', + symbols: [motionSymbol('MotionView', '/reference/items/MotionView/js-api')], + capabilities: ['animated DOM updates', 'spring view transitions', 'ambient wiring'], + related: ['motion', 'view-transition', 'dialog', 'data-bind'], }, { id: 'panel', diff --git a/packages/docs/.vitepress/reference/public-contracts.ts b/packages/docs/.vitepress/reference/public-contracts.ts index 9df6dea4..9a0090b0 100644 --- a/packages/docs/.vitepress/reference/public-contracts.ts +++ b/packages/docs/.vitepress/reference/public-contracts.ts @@ -931,7 +931,7 @@ export const publicContractSymbols = [ kind: 'type', package: 'npm:@studiometa/ui-motion', importPath: '@studiometa/ui-motion', - href: '/reference/items/Motion/js-api', + href: '/reference/items/MotionScrollTimeline/js-api', status: 'stable', }, { @@ -939,7 +939,7 @@ export const publicContractSymbols = [ kind: 'type', package: 'npm:@studiometa/ui-motion', importPath: '@studiometa/ui-motion', - href: '/reference/items/Motion/js-api', + href: '/reference/items/MotionSequence/js-api', status: 'stable', }, { @@ -947,7 +947,7 @@ export const publicContractSymbols = [ kind: 'type', package: 'npm:@studiometa/ui-motion', importPath: '@studiometa/ui-motion', - href: '/reference/items/Motion/js-api', + href: '/reference/items/MotionView/js-api', status: 'stable', }, ] satisfies ReferenceSymbol[]; diff --git a/packages/docs/reference/items/DataBind/js-api.md b/packages/docs/reference/items/DataBind/js-api.md index 5c0aaf58..31c4cebc 100644 --- a/packages/docs/reference/items/DataBind/js-api.md +++ b/packages/docs/reference/items/DataBind/js-api.md @@ -95,7 +95,7 @@ Use `data-bind:if` when the element must not exist in the DOM — a form control ### Wrapping the DOM change with the `dom-update` event -Before `data-bind:if` inserts or removes the template content, the component emits the bubbling `dom-update` protocol event — the shared announcement components use before an imminent DOM change. Its `detail` carries the new logical state as `isPresent` and a `wrap(runner)` function: a listener can call `wrap()` to substitute what runs the DOM change. The runner is either a function receiving an `apply()` callback that performs the actual insertion or removal, or a duck-typed transitioner exposing an `update(mutate)` method — like `MotionView` from `@studiometa/ui-motion` — whose `update()` receives the callback. +Before `data-bind:if` inserts or removes the template content, the component emits the bubbling `dom-update` protocol event — the shared announcement components use before an imminent DOM change. Its `detail` carries the new logical state as `isPresent` and a `wrap(runner)` function: a listener can call `wrap()` to substitute what runs the DOM change. The runner is either a function receiving an `apply()` callback that performs the actual insertion or removal, or a duck-typed transitioner exposing an `update(mutate)` method — like [`MotionView`](/reference/items/MotionView/) from `@studiometa/ui-motion` — whose `update()` receives the callback. ```ts interface DomUpdateTransitioner { diff --git a/packages/docs/reference/items/Dialog/js-api.md b/packages/docs/reference/items/Dialog/js-api.md index 864715b2..f8ae7ddd 100644 --- a/packages/docs/reference/items/Dialog/js-api.md +++ b/packages/docs/reference/items/Dialog/js-api.md @@ -100,7 +100,7 @@ Both lifecycle events expose `event.detail.waitUntil(x)`, modeled on the Service `waitUntil()` accepts either form: - a **promise** (any thenable), awaited as is -- a **transitioner**: a duck-typed object with `enter()` and `leave()` methods, e.g. `MotionView` from `@studiometa/ui-motion` — the dialog follows its lifecycle by awaiting `enter()` on `open` and `leave()` on `close` +- a **transitioner**: a duck-typed object with `enter()` and `leave()` methods, e.g. [`MotionView`](/reference/items/MotionView/) from `@studiometa/ui-motion` — the dialog follows its lifecycle by awaiting `enter()` on `open` and `leave()` on `close` For example, a [`Motion`](/reference/items/Motion/) box can spring in and out with the dialog through two [`Action`](/reference/items/Action/) attributes: diff --git a/packages/docs/reference/items/Fetch/js-api.md b/packages/docs/reference/items/Fetch/js-api.md index 0258a36c..045cb702 100644 --- a/packages/docs/reference/items/Fetch/js-api.md +++ b/packages/docs/reference/items/Fetch/js-api.md @@ -329,7 +329,7 @@ Before applying the fetched content, `Fetch` dispatches the bubbling [`dom-updat `wrap()` accepts a `DomUpdateRunner`, which is either form: - a **function** with the signature `(apply: () => void) => void | Promise`: it receives an `apply` function that injects the fetched content into the DOM, and its return value is awaited before the [`fetch-update-after` event](#fetch-update-after) is emitted -- a **transitioner**: any duck-typed object with an `update(mutate)` method (the `DomUpdateTransitioner` interface), e.g. `MotionView` from `@studiometa/ui-motion` — its `update()` method receives the apply function and its return value is awaited the same way +- a **transitioner**: any duck-typed object with an `update(mutate)` method (the `DomUpdateTransitioner` interface), e.g. [`MotionView`](/reference/items/MotionView/) from `@studiometa/ui-motion` — its `update()` method receives the apply function and its return value is awaited the same way The protocol enforces three rules: @@ -337,7 +337,7 @@ The protocol enforces three rules: - **Last call wins**: a single runner is kept, the last `wrap` call during dispatch replaces any previous one. - **The content is never lost**: if the runner throws or rejects, the error is logged with a warning and the content is applied directly when it has not been applied yet. The `fetch-update-after` event is always emitted. -With the upcoming ambient `MotionView` from `@studiometa/ui-motion`, the common case is pure nesting: a `MotionView` wrapping the updated content picks up the bubbling event by itself, with no attributes to write. When the transitioner lives elsewhere in the tree, an [Action](/reference/items/Action/) is the explicit escape hatch to route the event to it: +With the [ambient `MotionView`](/reference/items/MotionView/js-api#ambient-wiring) from `@studiometa/ui-motion`, the common case is pure nesting: a `MotionView` wrapping the updated content picks up the bubbling event by itself, with no attributes to write. When the transitioner lives elsewhere in the tree, an [Action](/reference/items/Action/) is the explicit escape hatch to route the event to it: ```html
diff --git a/packages/docs/reference/items/Motion/examples.md b/packages/docs/reference/items/Motion/examples.md index e37c84c6..d93e7549 100644 --- a/packages/docs/reference/items/Motion/examples.md +++ b/packages/docs/reference/items/Motion/examples.md @@ -129,66 +129,3 @@ A single element carries `Action`, `Motion` and [`TimerProgress`](/reference/ite ::: - -## Staggered sequence - -[`MotionSequence`](./js-api#motionsequence) composes its `Motion` children into one timeline: the `stagger` option spreads the first three items, the finale positions itself with `data-option-at="+0.2"`, and one `Action` target plays or reverses the entire choreography — a single animation under the hood. - - - - - - -:::code-group - -<<< ./stories/sequence/app.twig -<<< ./stories/sequence/app.js - -::: - - - -## Animated state change with `MotionView` - -[`MotionView`](./js-api#motionview) wraps DOM updates in Motion's `animateView()`, as a drop-in alternative to [`ViewTransition`](/reference/items/ViewTransition/): toggling swaps the `enterTo`/`leaveTo` classes inside a view transition, and the spring `transition` with the `layout` morph animates the card between its two states — no `::view-transition-*` CSS needed. Where view transitions are unavailable, the classes still swap, only without animation. - - - - - - -:::code-group - -<<< ./stories/view/app.twig -<<< ./stories/view/app.js - -::: - - - -## Scroll-driven timeline - -[`MotionScrollTimeline`](./js-api#motionscrolltimeline) separates the timeline from the animations, like the `ScrollAnimation` family: the tall section defines the scroll range, and every `Motion` inside it is driven by that progress through Motion's `scroll()` — hardware-accelerated where the browser supports `ScrollTimeline`. Keyframe arrays give each child a multi-step track across the same range, and registering the timeline is enough: it mounts its `Motion` children itself. - - - - - - -:::code-group - -<<< ./stories/with-scroll-timeline/app.twig -<<< ./stories/with-scroll-timeline/app.js - -::: - - diff --git a/packages/docs/reference/items/Motion/index.md b/packages/docs/reference/items/Motion/index.md index 3a3e990a..31d85c10 100644 --- a/packages/docs/reference/items/Motion/index.md +++ b/packages/docs/reference/items/Motion/index.md @@ -10,6 +10,8 @@ The component is a thin, headless playback surface exposing imperative methods t The `motion` peer dependency is resolved with a lazy `import()` the first time an animation is built, so it stays out of your main bundle until needed. See [providing the Motion dependency](./js-api#providing-the-motion-dependency) to inject a specific build such as `motion/mini`. +`Motion` is the playback primitive of `@studiometa/ui-motion`, which also ships [`MotionScrollTimeline`](/reference/items/MotionScrollTimeline/) to drive a group of `Motion` children with the scroll, [`MotionSequence`](/reference/items/MotionSequence/) to compose them into one staggered timeline, and [`MotionView`](/reference/items/MotionView/) to play DOM updates as view transitions. + ## Usage Option values are parsed as JSON, so object keys must be quoted: diff --git a/packages/docs/reference/items/Motion/js-api.md b/packages/docs/reference/items/Motion/js-api.md index bfd5ab6e..902c6be3 100644 --- a/packages/docs/reference/items/Motion/js-api.md +++ b/packages/docs/reference/items/Motion/js-api.md @@ -4,15 +4,13 @@ title: Motion JS API # JS API -## `Motion` - Animate the component's root element with [Motion](https://motion.dev). -### Options +## Options Object options are parsed as JSON: quote the keys (`data-option-animate='{ "x": 100 }'`). The available keyframes and transition settings are Motion's own — see the [`animate()` documentation](https://motion.dev/docs/animate) for the full vocabulary. -#### `initial` +### `initial` - Type: `DOMKeyframesDefinition` - Default: `{}` @@ -30,14 +28,14 @@ Styles applied to the element on mount, before anything plays. Use it to define ``` -#### `animate` +### `animate` - Type: `DOMKeyframesDefinition` - Default: `{}` The target keyframes of the animation. They play automatically on mount unless [`autoplay`](#autoplay) is disabled. -#### `transition` +### `transition` - Type: `AnimationOptions` - Default: `{}` @@ -55,7 +53,7 @@ Motion's animation options: `duration`, `delay`, `ease`, `type` (`"tween"`, `"sp ``` -#### `autoplay` +### `autoplay` - Type: `boolean` - Default: `true` @@ -73,7 +71,7 @@ Whether the `animate` keyframes play automatically on mount. Since the default i ``` -#### `hover` +### `hover` - Type: `DOMKeyframesDefinition` - Default: `{}` @@ -90,14 +88,14 @@ Keyframes applied while the element is hovered, through Motion's [`hover()`](htt ``` -#### `press` +### `press` - Type: `DOMKeyframesDefinition` - Default: `{}` Keyframes applied while the element is pressed, through Motion's [`press()`](https://motion.dev/docs/press) — pointer and keyboard alike, so the state is accessible for free. Reverts like `hover` when the press ends. -#### `inView` +### `inView` - Type: `DOMKeyframesDefinition` - Default: `{}` @@ -116,21 +114,21 @@ Keyframes applied when the element enters the viewport, through Motion's [`inVie ``` -#### `inViewMargin` +### `inViewMargin` - Type: `string` - Default: `''` The viewport margin for the `inView` detection, in CSS margin syntax (e.g. `"-100px"` to trigger 100px inside the viewport). -#### `inViewAmount` +### `inViewAmount` - Type: `'some' | 'all' | number` - Default: `'some'` How much of the element must be visible to trigger `inView`: `"some"`, `"all"`, or a `0`–`1` proportion. -#### `once` +### `once` - Type: `boolean` - Default: `false` @@ -141,7 +139,7 @@ When set, the `inView` keyframes play once and the reached styles persist — th The gesture options animate alongside the declared animation: they never become the [current animation](#methods) and emit no lifecycle events. Like `scroll()`, the gesture functions are not part of `motion/mini` — a gesture option warns and is skipped when the [provided module](#providing-the-motion-dependency) lacks its function. ::: -### Events +## Events All events are dispatched as bubbling `CustomEvent`s on the component's element, so they can be listened to with [`Action`](/reference/items/Action/)'s `data-on:` attribute — either on the same element or on an ancestor. Use the `.stop` modifier to contain them. @@ -164,7 +162,7 @@ All events are dispatched as bubbling `CustomEvent`s on the component's element, ``` -### Methods +## Methods The component holds a single current animation. `play()` and `reverse()` always drive the animation declared by the options — recreating it when an imperative `animate()` call superseded it — while `pause()`, `seek()`, `stop()`, `cancel()` and `complete()` act on whichever animation is current. `play()`, `reverse()` and `animate()` return a promise that resolves when the animation settles and never rejects. @@ -190,7 +188,7 @@ Methods are callable from an `Action` effect — on the same element or through ``` -### Getters +## Getters | Getter | Description | | ---------- | ---------------------------------------------------------- | @@ -199,213 +197,6 @@ Methods are callable from an `Action` effect — on the same element or through | `duration` | The current animation duration in seconds. | | `progress` | The current playback progress, from `0` to `1`. | -## `MotionScrollTimeline` - -The scroll driver for a group of animations: the element's traversal of the viewport defines the timeline, and every `Motion` child it contains is bound to that progress with Motion's [`scroll()`](https://motion.dev/docs/scroll) — hardware-accelerated where the browser supports `ScrollTimeline`. The children declare their keyframes as usual (arrays give multi-step tracks) and keep their whole playback surface; add `data-option-no-autoplay` so they do not play before the scroll link takes over. - - -```html {2,6} -
-
- … -
-
-``` - - -### Options - -#### `offset` - -- Type: `string[]` -- Default: `["start end", "end start"]` - -The scroll range, in Motion's [offset syntax](https://motion.dev/docs/scroll#offset): each entry pairs a point of the timeline element with a point of the viewport. The default maps progress `0` to the element entering the viewport and `1` to it leaving. - -#### `axis` - -- Type: `'x' | 'y'` -- Default: `'y'` - -The scroll axis driving the timeline. - -### Notes - -- `scroll()` is not part of `motion/mini`: when the [provided module](#providing-the-motion-dependency) lacks it, the timeline warns and leaves its children untouched. -- Each child gets its own `scroll()` link, released when the timeline is destroyed. - -## `MotionSequence` - -Orchestrate the `Motion` children as one animation sequence: each child declares its keyframes as usual, and the sequence composes them — in DOM order — into a single timeline with Motion's [sequencing](https://motion.dev/docs/animate#timeline-sequencing). The whole playback surface applies to the sequence: an [`Action`](/reference/items/Action/) can `play()`, `reverse()` or `seek()` the entire choreography, and a [`MotionScrollTimeline`](#motionscrolltimeline) can scrub it. Give the children `data-option-no-autoplay` — the sequence owns their playback. - - -```html {1} -
    -
  • One
  • -
  • Two
  • -
-``` - - -### Options - -#### `stagger` - -- Type: `number` -- Default: `0` - -Spreads the segments automatically: each child starts `stagger` seconds after the previous one. Without it, segments run one after another (Motion's default). - -#### `at` (on the children) - -- Type: `string` -- Default: `''` - -A child's explicit position in the sequence, taking precedence over `stagger`: a time in seconds (`"2"`), a relative offset (`"-0.2"`), or `"<"` for "with the previous segment". See Motion's [sequencing options](https://motion.dev/docs/animate#timeline-sequencing). - -### Notes - -- The sequence element's own `transition` option is passed as the sequence-level options (e.g. a shared `duration` or `repeat`). -- Children without `animate` keyframes are skipped. -- Sequences need the full `motion` entry: `motion/mini`'s `animate()` does not support them. - -## `MotionView` - -Wrap DOM updates in Motion's [`animateView()`](https://motion.dev/docs/animate-view) so the change plays as a view transition. A drop-in alternative to the [`ViewTransition`](/reference/items/ViewTransition/) component — same `enter()`/`leave()`/`toggle()` methods, `state` property, events and `viewTransitionName`/`enterTo`/`leaveTo` options — but the animation is declared with Motion keyframes and transitions (including springs) instead of the `::view-transition-*` CSS pseudo-elements. - - -```html {2,3} -
- … -
- - -``` - - -### Options - -Object options are parsed as JSON: quote the keys (`data-option-new='{ "opacity": [0, 1] }'`). - -#### `viewTransitionName` - -- Type: `string` -- Default: `''` - -Assigned as the element's [`view-transition-name`](https://developer.mozilla.org/en-US/docs/Web/CSS/view-transition-name) on mount, exactly like `ViewTransition`. Optional with `MotionView`: `animateView()` names the subjects it animates automatically. - -#### `enterTo` - -- Type: `string` -- Default: `''` - -Classes describing the shown state. Added on `enter`, removed on `leave`. - -#### `leaveTo` - -- Type: `string` -- Default: `''` - -Classes describing the hidden state. Added on `leave`, removed on `enter`. Usually also the element's initial class so it starts hidden. - -#### `transition` - -- Type: `ViewTransitionOptions` -- Default: `{}` - -The root [`animateView()` options](https://motion.dev/docs/animate-view): a default transition (`duration`, `ease`, `type: "spring"`, …) for every layer of the view transition. - -#### `add` - -- Type: `string` -- Default: `''` - -A selector resolved within the component's element: every matched element becomes an animated subject of the transition. When empty, the element itself is the subject. - -#### `new`, `old`, `enter`, `exit` - -- Type: `DOMKeyframesDefinition` -- Default: `{}` - -Per-layer keyframes applied to each subject, mapping to the builder's [`new()`/`old()`/`enter()`/`exit()` methods](https://motion.dev/docs/animate-view): `new` and `old` animate the new and old views whether the element persists or not, while `enter` and `exit` only fire for a pure newcomer or leaver. - -#### `layout` - -- Type: `boolean` -- Default: `false` - -Enable the layout morph on each subject (the builder's `layout()`), so position and size changes animate smoothly. - -#### `auto` - -- Type: `boolean` -- Default: `true` - -Enable [ambient wiring](#ambient-wiring): the component wraps any `dom-update` announced inside its subtree and joins the lifecycle of a containing `Dialog`. Opt out with `data-option-no-auto`. - -### Events - -The same events as [`ViewTransition`](/reference/items/ViewTransition/js-api#events), in the same order: `enter`, `enter-start`, `enter-end` around the enter transition and `leave`, `leave-start`, `leave-end` around the leave transition. - -### Methods - -| Method | Description | -| ---------------- | ------------------------------------------------------------------------------------------------------------- | -| `enter()` | Swap `leaveTo` for `enterTo` inside a view transition. Resolves once the animation settles. | -| `leave()` | Swap `enterTo` for `leaveTo` inside a view transition. Resolves once the animation settles. | -| `toggle()` | Toggle between enter and leave, entering first. | -| `update(mutate)` | The underlying primitive: run any DOM mutation as a view transition configured by the options. Never rejects. | - -### Ambient wiring - -Containment is the wiring: with the `auto` option (on by default), a mounted `MotionView` listens for the bubbling `dom-update` event that mutating components — [`Fetch`](/reference/items/Fetch/), [`DataBind`](/reference/items/DataBind/)'s `data-bind:if` — announce before changing the DOM, and runs the announced change through `update()` so it plays as a view transition. Nesting the mutators inside the component is enough, with zero wiring attributes on either side: - - -```html {1} -
-
- -
- - -
-``` - - -A `MotionView` placed inside a [`Dialog`](/reference/items/Dialog/) also joins its lifecycle: the dialog's extendable `open` and `close` events bubble past the component, which hands itself to `detail.waitUntil()` — the dialog then awaits `enter()` on open and `leave()` on close. - -Opt out with `data-option-no-auto`. Explicit wiring through [`Action`](/reference/items/Action/) remains for cross-subtree topologies, where the mutator and the animated subtree are not nested: - - -```html {4} -
- -
- -
    - … -
-``` - - -### Notes - -- The mutation is never lost: in browsers without the View Transitions API — or when the animation rejects — the update still applies, only without animation. -- `animateView()` is not part of `motion/mini`: when the [provided module](#providing-the-motion-dependency) lacks it, the component warns and applies updates directly. - ## Providing the Motion dependency By default the component resolves `motion` with a lazy `import()` the first time an animation is built. To control which build is used — a specific version, the smaller `motion/mini` entry, or a module served from an import map or a CDN — inject it once with `provideMotion()` before the components mount: diff --git a/packages/docs/reference/items/MotionScrollTimeline/examples.md b/packages/docs/reference/items/MotionScrollTimeline/examples.md new file mode 100644 index 00000000..f1cccdaa --- /dev/null +++ b/packages/docs/reference/items/MotionScrollTimeline/examples.md @@ -0,0 +1,26 @@ +--- +title: MotionScrollTimeline examples +--- + +# Examples + +## Scroll-driven timeline + +[`MotionScrollTimeline`](./js-api) separates the timeline from the animations, like the `ScrollAnimation` family: the tall section defines the scroll range, and every `Motion` inside it is driven by that progress through Motion's `scroll()` — hardware-accelerated where the browser supports `ScrollTimeline`. Keyframe arrays give each child a multi-step track across the same range, and registering the timeline is enough: it mounts its `Motion` children itself. + + + + + + +:::code-group + +<<< ./stories/with-scroll-timeline/app.twig +<<< ./stories/with-scroll-timeline/app.js + +::: + + diff --git a/packages/docs/reference/items/MotionScrollTimeline/index.md b/packages/docs/reference/items/MotionScrollTimeline/index.md new file mode 100644 index 00000000..f4e3d4a0 --- /dev/null +++ b/packages/docs/reference/items/MotionScrollTimeline/index.md @@ -0,0 +1,30 @@ +--- +badges: [JS] +--- + +# MotionScrollTimeline + +The `MotionScrollTimeline` component is the scroll driver for a group of animations: the element's traversal of the viewport defines the timeline, and every [`Motion`](/reference/items/Motion/) child it contains is bound to that progress with Motion's [`scroll()`](https://motion.dev/docs/scroll) — hardware-accelerated where the browser supports `ScrollTimeline`. + +The children declare their keyframes as usual (arrays give multi-step tracks) and keep their whole playback surface; add `data-option-no-autoplay` so they do not play before the scroll link takes over. Registering the timeline is enough: it mounts its `Motion` children itself. + +`MotionScrollTimeline` is part of `@studiometa/ui-motion`, alongside [`Motion`](/reference/items/Motion/), [`MotionSequence`](/reference/items/MotionSequence/) and [`MotionView`](/reference/items/MotionView/). + +## Usage + + +```html {2,6} +
+
+ … +
+
+``` + + +See the [examples](./examples.md) for a live demo, and the [JavaScript API](./js-api.md) for the full list of options. diff --git a/packages/docs/reference/items/MotionScrollTimeline/js-api.md b/packages/docs/reference/items/MotionScrollTimeline/js-api.md new file mode 100644 index 00000000..56efb662 --- /dev/null +++ b/packages/docs/reference/items/MotionScrollTimeline/js-api.md @@ -0,0 +1,43 @@ +--- +title: MotionScrollTimeline JS API +--- + +# JS API + +The scroll driver for a group of animations: the element's traversal of the viewport defines the timeline, and every [`Motion`](/reference/items/Motion/) child it contains is bound to that progress with Motion's [`scroll()`](https://motion.dev/docs/scroll) — hardware-accelerated where the browser supports `ScrollTimeline`. The children declare their keyframes as usual (arrays give multi-step tracks) and keep their whole playback surface; add `data-option-no-autoplay` so they do not play before the scroll link takes over. + + +```html {2,6} +
+
+ … +
+
+``` + + +## Options + +### `offset` + +- Type: `string[]` +- Default: `["start end", "end start"]` + +The scroll range, in Motion's [offset syntax](https://motion.dev/docs/scroll#offset): each entry pairs a point of the timeline element with a point of the viewport. The default maps progress `0` to the element entering the viewport and `1` to it leaving. + +### `axis` + +- Type: `'x' | 'y'` +- Default: `'y'` + +The scroll axis driving the timeline. + +## Notes + +- `scroll()` is not part of `motion/mini`: when the [provided module](/reference/items/Motion/js-api#providing-the-motion-dependency) lacks it, the timeline warns and leaves its children untouched. +- Each child gets its own `scroll()` link, released when the timeline is destroyed. diff --git a/packages/docs/reference/items/Motion/stories/with-scroll-timeline/app.js b/packages/docs/reference/items/MotionScrollTimeline/stories/with-scroll-timeline/app.js similarity index 100% rename from packages/docs/reference/items/Motion/stories/with-scroll-timeline/app.js rename to packages/docs/reference/items/MotionScrollTimeline/stories/with-scroll-timeline/app.js diff --git a/packages/docs/reference/items/Motion/stories/with-scroll-timeline/app.twig b/packages/docs/reference/items/MotionScrollTimeline/stories/with-scroll-timeline/app.twig similarity index 100% rename from packages/docs/reference/items/Motion/stories/with-scroll-timeline/app.twig rename to packages/docs/reference/items/MotionScrollTimeline/stories/with-scroll-timeline/app.twig diff --git a/packages/docs/reference/items/MotionSequence/examples.md b/packages/docs/reference/items/MotionSequence/examples.md new file mode 100644 index 00000000..589c04be --- /dev/null +++ b/packages/docs/reference/items/MotionSequence/examples.md @@ -0,0 +1,26 @@ +--- +title: MotionSequence examples +--- + +# Examples + +## Staggered sequence + +[`MotionSequence`](./js-api) composes its `Motion` children into one timeline: the `stagger` option spreads the first three items, the finale positions itself with `data-option-at="+0.2"`, and one `Action` target plays or reverses the entire choreography — a single animation under the hood. + + + + + + +:::code-group + +<<< ./stories/sequence/app.twig +<<< ./stories/sequence/app.js + +::: + + diff --git a/packages/docs/reference/items/MotionSequence/index.md b/packages/docs/reference/items/MotionSequence/index.md new file mode 100644 index 00000000..605cf8e5 --- /dev/null +++ b/packages/docs/reference/items/MotionSequence/index.md @@ -0,0 +1,24 @@ +--- +badges: [JS] +--- + +# MotionSequence + +The `MotionSequence` component orchestrates its [`Motion`](/reference/items/Motion/) children as one animation sequence: each child declares its keyframes as usual, and the sequence composes them — in DOM order — into a single timeline with Motion's [sequencing](https://motion.dev/docs/animate#timeline-sequencing). + +The whole playback surface applies to the sequence: an [`Action`](/reference/items/Action/) can `play()`, `reverse()` or `seek()` the entire choreography, and a [`MotionScrollTimeline`](/reference/items/MotionScrollTimeline/) can scrub it. Give the children `data-option-no-autoplay` — the sequence owns their playback. + +`MotionSequence` is part of `@studiometa/ui-motion`, alongside [`Motion`](/reference/items/Motion/), [`MotionScrollTimeline`](/reference/items/MotionScrollTimeline/) and [`MotionView`](/reference/items/MotionView/). + +## Usage + + +```html {1} +
    +
  • One
  • +
  • Two
  • +
+``` + + +See the [examples](./examples.md) for a live demo, and the [JavaScript API](./js-api.md) for the full list of options. diff --git a/packages/docs/reference/items/MotionSequence/js-api.md b/packages/docs/reference/items/MotionSequence/js-api.md new file mode 100644 index 00000000..e6a956b5 --- /dev/null +++ b/packages/docs/reference/items/MotionSequence/js-api.md @@ -0,0 +1,38 @@ +--- +title: MotionSequence JS API +--- + +# JS API + +Orchestrate the [`Motion`](/reference/items/Motion/) children as one animation sequence: each child declares its keyframes as usual, and the sequence composes them — in DOM order — into a single timeline with Motion's [sequencing](https://motion.dev/docs/animate#timeline-sequencing). The whole playback surface applies to the sequence: an [`Action`](/reference/items/Action/) can `play()`, `reverse()` or `seek()` the entire choreography, and a [`MotionScrollTimeline`](/reference/items/MotionScrollTimeline/) can scrub it. Give the children `data-option-no-autoplay` — the sequence owns their playback. + + +```html {1} +
    +
  • One
  • +
  • Two
  • +
+``` + + +## Options + +### `stagger` + +- Type: `number` +- Default: `0` + +Spreads the segments automatically: each child starts `stagger` seconds after the previous one. Without it, segments run one after another (Motion's default). + +### `at` (on the children) + +- Type: `string` +- Default: `''` + +A child's explicit position in the sequence, taking precedence over `stagger`: a time in seconds (`"2"`), a relative offset (`"-0.2"`), or `"<"` for "with the previous segment". See Motion's [sequencing options](https://motion.dev/docs/animate#timeline-sequencing). + +## Notes + +- The sequence element's own `transition` option is passed as the sequence-level options (e.g. a shared `duration` or `repeat`). +- Children without `animate` keyframes are skipped. +- Sequences need the full `motion` entry: `motion/mini`'s `animate()` does not support them. See [providing the Motion dependency](/reference/items/Motion/js-api#providing-the-motion-dependency). diff --git a/packages/docs/reference/items/Motion/stories/sequence/app.js b/packages/docs/reference/items/MotionSequence/stories/sequence/app.js similarity index 100% rename from packages/docs/reference/items/Motion/stories/sequence/app.js rename to packages/docs/reference/items/MotionSequence/stories/sequence/app.js diff --git a/packages/docs/reference/items/Motion/stories/sequence/app.twig b/packages/docs/reference/items/MotionSequence/stories/sequence/app.twig similarity index 100% rename from packages/docs/reference/items/Motion/stories/sequence/app.twig rename to packages/docs/reference/items/MotionSequence/stories/sequence/app.twig diff --git a/packages/docs/reference/items/MotionView/examples.md b/packages/docs/reference/items/MotionView/examples.md new file mode 100644 index 00000000..3ef32ace --- /dev/null +++ b/packages/docs/reference/items/MotionView/examples.md @@ -0,0 +1,68 @@ +--- +title: MotionView examples +--- + +# Examples + +## Animated state change + +[`MotionView`](./js-api) wraps DOM updates in Motion's `animateView()`, as a drop-in alternative to [`ViewTransition`](/reference/items/ViewTransition/): toggling swaps the `enterTo`/`leaveTo` classes inside a view transition, and the spring `transition` with the `layout` morph animates the card between its two states — no `::view-transition-*` CSS needed. Where view transitions are unavailable, the classes still swap, only without animation. + + + + + + +:::code-group + +<<< ./stories/view/app.twig +<<< ./stories/view/app.js + +::: + + + +## Zero-wiring exit animation for `data-bind:if` + +[Ambient wiring](./js-api#ambient-wiring) in action: the `MotionView` has no wiring attribute at all — it wraps the `dom-update` event that [`data-bind:if`](/reference/items/DataBind/js-api#conditional-rendering-with-data-bind-if) announces before changing the DOM, so the template content animates in **and out**, even though the removed nodes are already gone when the exit plays. The `layout` option morphs the container's size with a spring. + + + + + + +:::code-group + +<<< ./stories/ambient-bind-if/app.twig +<<< ./stories/ambient-bind-if/app.js + +::: + + + +## Ambient view transitions in a `Dialog` + +The same containment story with [`Dialog`](/reference/items/Dialog/): nested inside it, the `MotionView` joins the bubbling `open`/`close` lifecycle by itself — the dialog calls its `enter()`/`leave()` through `waitUntil()` and stays painted until the spring view transition settles. Compare with the [explicit `Motion` wiring](/reference/items/Motion/examples#spring-entrance-and-exit-for-a-dialog): same result, zero attributes. + + + + + + +:::code-group + +<<< ./stories/ambient-dialog/app.twig +<<< ./stories/ambient-dialog/app.js + +::: + + diff --git a/packages/docs/reference/items/MotionView/index.md b/packages/docs/reference/items/MotionView/index.md new file mode 100644 index 00000000..ce94d776 --- /dev/null +++ b/packages/docs/reference/items/MotionView/index.md @@ -0,0 +1,30 @@ +--- +badges: [JS] +--- + +# MotionView + +The `MotionView` component wraps DOM updates in Motion's [`animateView()`](https://motion.dev/docs/animate-view) so the change plays as a view transition. It is a drop-in alternative to the [`ViewTransition`](/reference/items/ViewTransition/) component — same `enter()`/`leave()`/`toggle()` methods, `state` property, events and `viewTransitionName`/`enterTo`/`leaveTo` options — but the animation is declared with Motion keyframes and transitions (including springs) instead of the `::view-transition-*` CSS pseudo-elements. + +Containment is the wiring: with the [`auto` option](./js-api#auto) (on by default), a mounted `MotionView` wraps any `dom-update` announced inside its subtree — by [`Fetch`](/reference/items/Fetch/) or [`DataBind`](/reference/items/DataBind/)'s `data-bind:if` — and joins the lifecycle of a containing [`Dialog`](/reference/items/Dialog/), with zero wiring attributes on either side. See [ambient wiring](./js-api#ambient-wiring). + +`MotionView` is part of `@studiometa/ui-motion`, alongside [`Motion`](/reference/items/Motion/), [`MotionScrollTimeline`](/reference/items/MotionScrollTimeline/) and [`MotionSequence`](/reference/items/MotionSequence/). + +## Usage + +Option values are parsed as JSON, so object keys must be quoted: + + +```html {2,3} +
+ … +
+ + +``` + + +See the [examples](./examples.md) for live demos of the ambient integrations, and the [JavaScript API](./js-api.md) for the full list of options, methods and events. diff --git a/packages/docs/reference/items/MotionView/js-api.md b/packages/docs/reference/items/MotionView/js-api.md new file mode 100644 index 00000000..8eb8ef2c --- /dev/null +++ b/packages/docs/reference/items/MotionView/js-api.md @@ -0,0 +1,135 @@ +--- +title: MotionView JS API +--- + +# JS API + +Wrap DOM updates in Motion's [`animateView()`](https://motion.dev/docs/animate-view) so the change plays as a view transition. A drop-in alternative to the [`ViewTransition`](/reference/items/ViewTransition/) component — same `enter()`/`leave()`/`toggle()` methods, `state` property, events and `viewTransitionName`/`enterTo`/`leaveTo` options — but the animation is declared with Motion keyframes and transitions (including springs) instead of the `::view-transition-*` CSS pseudo-elements. + + +```html {2,3} +
+ … +
+ + +``` + + +## Options + +Object options are parsed as JSON: quote the keys (`data-option-new='{ "opacity": [0, 1] }'`). + +### `viewTransitionName` + +- Type: `string` +- Default: `''` + +Assigned as the element's [`view-transition-name`](https://developer.mozilla.org/en-US/docs/Web/CSS/view-transition-name) on mount, exactly like `ViewTransition`. Optional with `MotionView`: `animateView()` names the subjects it animates automatically. + +### `enterTo` + +- Type: `string` +- Default: `''` + +Classes describing the shown state. Added on `enter`, removed on `leave`. + +### `leaveTo` + +- Type: `string` +- Default: `''` + +Classes describing the hidden state. Added on `leave`, removed on `enter`. Usually also the element's initial class so it starts hidden. + +### `transition` + +- Type: `ViewTransitionOptions` +- Default: `{}` + +The root [`animateView()` options](https://motion.dev/docs/animate-view): a default transition (`duration`, `ease`, `type: "spring"`, …) for every layer of the view transition. + +### `add` + +- Type: `string` +- Default: `''` + +A selector resolved within the component's element: every matched element becomes an animated subject of the transition. When empty, the element itself is the subject. + +### `new`, `old`, `enter`, `exit` + +- Type: `DOMKeyframesDefinition` +- Default: `{}` + +Per-layer keyframes applied to each subject, mapping to the builder's [`new()`/`old()`/`enter()`/`exit()` methods](https://motion.dev/docs/animate-view): `new` and `old` animate the new and old views whether the element persists or not, while `enter` and `exit` only fire for a pure newcomer or leaver. + +### `layout` + +- Type: `boolean` +- Default: `false` + +Enable the layout morph on each subject (the builder's `layout()`), so position and size changes animate smoothly. + +### `auto` + +- Type: `boolean` +- Default: `true` + +Enable [ambient wiring](#ambient-wiring): the component wraps any `dom-update` announced inside its subtree and joins the lifecycle of a containing `Dialog`. Opt out with `data-option-no-auto`. + +## Events + +The same events as [`ViewTransition`](/reference/items/ViewTransition/js-api#events), in the same order: `enter`, `enter-start`, `enter-end` around the enter transition and `leave`, `leave-start`, `leave-end` around the leave transition. + +## Methods + +| Method | Description | +| ---------------- | ------------------------------------------------------------------------------------------------------------- | +| `enter()` | Swap `leaveTo` for `enterTo` inside a view transition. Resolves once the animation settles. | +| `leave()` | Swap `enterTo` for `leaveTo` inside a view transition. Resolves once the animation settles. | +| `toggle()` | Toggle between enter and leave, entering first. | +| `update(mutate)` | The underlying primitive: run any DOM mutation as a view transition configured by the options. Never rejects. | + +## Ambient wiring + +Containment is the wiring: with the `auto` option (on by default), a mounted `MotionView` listens for the bubbling `dom-update` event that mutating components — [`Fetch`](/reference/items/Fetch/), [`DataBind`](/reference/items/DataBind/)'s `data-bind:if` — announce before changing the DOM, and runs the announced change through `update()` so it plays as a view transition. Nesting the mutators inside the component is enough, with zero wiring attributes on either side: + + +```html {1} +
+
+ +
+ + +
+``` + + +A `MotionView` placed inside a [`Dialog`](/reference/items/Dialog/) also joins its lifecycle: the dialog's extendable `open` and `close` events bubble past the component, which hands itself to `detail.waitUntil()` — the dialog then awaits `enter()` on open and `leave()` on close. + +Opt out with `data-option-no-auto`. Explicit wiring through [`Action`](/reference/items/Action/) remains for cross-subtree topologies, where the mutator and the animated subtree are not nested: + + +```html {4} +
+ +
+ +
    + … +
+``` + + +## Notes + +- The mutation is never lost: in browsers without the View Transitions API — or when the animation rejects — the update still applies, only without animation. +- `animateView()` is not part of `motion/mini`: when the [provided module](/reference/items/Motion/js-api#providing-the-motion-dependency) lacks it, the component warns and applies updates directly. diff --git a/packages/docs/reference/items/MotionView/stories/ambient-bind-if/app.js b/packages/docs/reference/items/MotionView/stories/ambient-bind-if/app.js new file mode 100644 index 00000000..f5c22443 --- /dev/null +++ b/packages/docs/reference/items/MotionView/stories/ambient-bind-if/app.js @@ -0,0 +1,5 @@ +import { registerComponents } from '@studiometa/js-toolkit'; +import { DataBind, DataModel, DataScope } from '@studiometa/ui'; +import { MotionView } from '@studiometa/ui-motion'; + +registerComponents(DataScope, DataBind, DataModel, MotionView); diff --git a/packages/docs/reference/items/MotionView/stories/ambient-bind-if/app.twig b/packages/docs/reference/items/MotionView/stories/ambient-bind-if/app.twig new file mode 100644 index 00000000..d01dcaf8 --- /dev/null +++ b/packages/docs/reference/items/MotionView/stories/ambient-bind-if/app.twig @@ -0,0 +1,24 @@ +{# The MotionView carries no wiring attribute: it ambiently wraps the + `dom-update` event that `data-bind:if` announces before changing the DOM, + so the insertion AND the removal play as one spring view transition. #} +
+
+ + +
+
diff --git a/packages/docs/reference/items/MotionView/stories/ambient-dialog/app.js b/packages/docs/reference/items/MotionView/stories/ambient-dialog/app.js new file mode 100644 index 00000000..d03721ad --- /dev/null +++ b/packages/docs/reference/items/MotionView/stories/ambient-dialog/app.js @@ -0,0 +1,5 @@ +import { registerComponents } from '@studiometa/js-toolkit'; +import { Action, Dialog } from '@studiometa/ui'; +import { MotionView } from '@studiometa/ui-motion'; + +registerComponents(Action, Dialog, MotionView); diff --git a/packages/docs/reference/items/MotionView/stories/ambient-dialog/app.twig b/packages/docs/reference/items/MotionView/stories/ambient-dialog/app.twig new file mode 100644 index 00000000..b01d7069 --- /dev/null +++ b/packages/docs/reference/items/MotionView/stories/ambient-dialog/app.twig @@ -0,0 +1,55 @@ + + +{# The MotionView box carries no wiring attribute: nested inside the Dialog, + it ambiently joins the bubbling `open`/`close` lifecycle — the Dialog calls + its `enter()`/`leave()` through `waitUntil()` and stays painted until the + view transition finishes. #} + +
+ +
+
+

Ambient view transitions

+

+ No data-on:open, no data-on:close: the + MotionView heard the dialog's lifecycle because it lives inside + it, and swaps its state classes inside a spring view transition. +

+ +
+
+
+ + diff --git a/packages/docs/reference/items/Motion/stories/view/app.js b/packages/docs/reference/items/MotionView/stories/view/app.js similarity index 100% rename from packages/docs/reference/items/Motion/stories/view/app.js rename to packages/docs/reference/items/MotionView/stories/view/app.js diff --git a/packages/docs/reference/items/Motion/stories/view/app.twig b/packages/docs/reference/items/MotionView/stories/view/app.twig similarity index 100% rename from packages/docs/reference/items/Motion/stories/view/app.twig rename to packages/docs/reference/items/MotionView/stories/view/app.twig diff --git a/packages/ui-motion/src/MotionScrollTimeline.ts b/packages/ui-motion/src/MotionScrollTimeline.ts index 2f8b6948..df895739 100644 --- a/packages/ui-motion/src/MotionScrollTimeline.ts +++ b/packages/ui-motion/src/MotionScrollTimeline.ts @@ -39,7 +39,7 @@ export interface MotionScrollTimelineProps extends BaseProps { * * ``` * - * @link https://ui.studiometa.dev/reference/items/Motion/js-api#motionscrolltimeline + * @link https://ui.studiometa.dev/reference/items/MotionScrollTimeline/ */ export class MotionScrollTimeline extends Base< MotionScrollTimelineProps & T diff --git a/packages/ui-motion/src/MotionSequence.ts b/packages/ui-motion/src/MotionSequence.ts index 3ed6f84e..c3f2b9e9 100644 --- a/packages/ui-motion/src/MotionSequence.ts +++ b/packages/ui-motion/src/MotionSequence.ts @@ -39,7 +39,7 @@ export interface MotionSequenceProps extends BaseProps { * * ``` * - * @link https://ui.studiometa.dev/reference/items/Motion/js-api#motionsequence + * @link https://ui.studiometa.dev/reference/items/MotionSequence/ */ export class MotionSequence extends Motion< MotionSequenceProps & T diff --git a/packages/ui-motion/src/MotionView.ts b/packages/ui-motion/src/MotionView.ts index 3242f45d..4d69d9e0 100644 --- a/packages/ui-motion/src/MotionView.ts +++ b/packages/ui-motion/src/MotionView.ts @@ -52,7 +52,7 @@ export interface MotionViewProps extends BaseProps { * * ``` * - * @link https://ui.studiometa.dev/reference/items/Motion/js-api#motionview + * @link https://ui.studiometa.dev/reference/items/MotionView/ */ export class MotionView extends Base { /** diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts index 96369a78..95734002 100644 --- a/packages/ui/src/index.ts +++ b/packages/ui/src/index.ts @@ -189,7 +189,6 @@ export { type TrackShopifyProps, } from './Track/index.js'; export { Transition, type TransitionConstructor } from './Transition/index.js'; -export type { DomUpdateRunner, DomUpdateTransitioner } from './utils/dom-update.js'; export { viewTransition, ViewTransition,