@lovince/motion 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +254 -2
- package/dist/accessibility/focus-management.d.ts +9 -0
- package/dist/accessibility/index.d.ts +2 -0
- package/dist/accessibility/reduced-motion.d.ts +7 -0
- package/dist/core/cleanup.d.ts +10 -0
- package/dist/core/config.d.ts +7 -0
- package/dist/core/context.d.ts +40 -0
- package/dist/core/entrance.d.ts +22 -0
- package/dist/core/index.d.ts +7 -0
- package/dist/core/preferences.d.ts +6 -0
- package/dist/core/targets.d.ts +4 -0
- package/dist/core/types.d.ts +20 -0
- package/dist/css/dom.d.ts +9 -0
- package/dist/css/fade-in.d.ts +25 -0
- package/dist/css/index.d.ts +3 -0
- package/dist/css/reveal.d.ts +22 -0
- package/dist/dom-CK6FnKcV.js +74 -0
- package/dist/focus-management-DTJ2C7Pn.js +8 -0
- package/dist/gsap/engine.d.ts +17 -0
- package/dist/gsap/horizontal-scroll.d.ts +14 -0
- package/dist/gsap/index.d.ts +8 -0
- package/dist/gsap/parallax.d.ts +14 -0
- package/dist/gsap/scroll-reveal.d.ts +18 -0
- package/dist/gsap/split-text.d.ts +27 -0
- package/dist/gsap/stagger.d.ts +15 -0
- package/dist/gsap/timeline.d.ts +23 -0
- package/dist/gsap.js +96 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +75 -0
- package/dist/presets/entrance.d.ts +20 -0
- package/dist/presets/image.d.ts +14 -0
- package/dist/presets/index.d.ts +5 -0
- package/dist/presets/navigation.d.ts +17 -0
- package/dist/presets/page-transition.d.ts +26 -0
- package/dist/presets/typography.d.ts +20 -0
- package/dist/presets.js +138 -0
- package/dist/reveal-By5zvLLc.js +39 -0
- package/dist/scroll/create-smooth-scroll.d.ts +37 -0
- package/dist/scroll/gsap-integration.d.ts +8 -0
- package/dist/scroll/index.d.ts +1 -0
- package/dist/scroll.js +89 -0
- package/dist/split-text-Cnl9gkUu.js +141 -0
- package/dist/targets-x7JiHhWI.js +49 -0
- package/dist/timeline-Cg3eq5ft.js +60 -0
- package/dist/vue/index.d.ts +27 -0
- package/dist/vue.js +33 -0
- package/package.json +97 -4
- package/styles/motion.css +4 -0
- package/styles/presets.css +58 -0
- package/styles/tokens.css +23 -0
- package/styles/utilities.css +128 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Lovince Tech
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,255 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @lovince/motion
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A framework-agnostic motion system. **CSS** handles light interface motion,
|
|
4
|
+
**GSAP** orchestrates timelines and scroll scenes, and **Lenis** provides
|
|
5
|
+
smooth scrolling. Every animation has an owner, a cleanup and a
|
|
6
|
+
reduced-motion policy.
|
|
7
|
+
|
|
8
|
+
> **Status: `0.1.0`, experimental.** The API may change before `1.0`.
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
bun add @lovince/motion
|
|
12
|
+
bun add gsap lenis # only for the entries that use them
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
The goal is not to animate everything. It is to make the motion a site does
|
|
16
|
+
have consistent, interruptible and accessible.
|
|
17
|
+
|
|
18
|
+
## Entries
|
|
19
|
+
|
|
20
|
+
Each engine sits behind its own entry, so an app pays only for what it imports
|
|
21
|
+
and a missing optional peer breaks nothing else.
|
|
22
|
+
|
|
23
|
+
| Import | Needs | Gives |
|
|
24
|
+
|---|---|---|
|
|
25
|
+
| `@lovince/motion` | — | `createMotion`, preferences, `revealOnScroll`, `fadeIn`, `splitText`, `focusTarget`, `whenMotionAllowed`, motion config and tokens |
|
|
26
|
+
| `@lovince/motion/gsap` | `gsap` | `createTimeline`, `createStagger`, `createScrollReveal`, `createParallax`, `createHorizontalScroll`, `gsapScrollDriver` |
|
|
27
|
+
| `@lovince/motion/scroll` | `lenis` | `createSmoothScroll` |
|
|
28
|
+
| `@lovince/motion/presets` | `gsap` | `heroEntrance`, `revealText`, `revealImage`, `followState`, `createPageTransition` |
|
|
29
|
+
| `@lovince/motion/vue` | `vue` | `useMotion`, `useMotionEffect`, `v-reveal` |
|
|
30
|
+
| `@lovince/motion/styles.css` | — | tokens + utilities + reveal presets (or `tokens.css`, `utilities.css`, `presets.css` on their own) |
|
|
31
|
+
|
|
32
|
+
`gsap`, `lenis` and `vue` are optional peers, so your app controls their
|
|
33
|
+
versions and there is never a second copy at runtime.
|
|
34
|
+
|
|
35
|
+
## Who owns what
|
|
36
|
+
|
|
37
|
+
| Engine | Owns |
|
|
38
|
+
|---|---|
|
|
39
|
+
| CSS | Hover, focus and press feedback, underlines, small transitions, decorative loops, simple scroll reveals |
|
|
40
|
+
| GSAP | Hero sequences, staggered and split typography, scroll-linked scenes, pinning, horizontal galleries, page transitions |
|
|
41
|
+
| Lenis | Smooth scrolling, as one service per scroller, on GSAP's clock when both are used |
|
|
42
|
+
|
|
43
|
+
Give each animation one owner. Never animate the same element with CSS and
|
|
44
|
+
GSAP at once.
|
|
45
|
+
|
|
46
|
+
## Quick start
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
import '@lovince/motion/styles.css'
|
|
50
|
+
import { createMotion, revealOnScroll } from '@lovince/motion'
|
|
51
|
+
import { gsapScrollDriver } from '@lovince/motion/gsap'
|
|
52
|
+
import { heroEntrance } from '@lovince/motion/presets'
|
|
53
|
+
import { createSmoothScroll } from '@lovince/motion/scroll'
|
|
54
|
+
|
|
55
|
+
const motion = createMotion({ root: '#app' })
|
|
56
|
+
|
|
57
|
+
heroEntrance({ label: '.eyebrow', heading: 'h1', body: '.lede', actions: '.ctas', media: '.hero-image' }, { context: motion })
|
|
58
|
+
revealOnScroll('[data-reveal]', { context: motion })
|
|
59
|
+
const scroll = createSmoothScroll({ driver: gsapScrollDriver() })
|
|
60
|
+
|
|
61
|
+
// When this view goes away:
|
|
62
|
+
motion.destroy() // reverts every tween, trigger, observer and split it owns
|
|
63
|
+
scroll.destroy()
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## The model
|
|
67
|
+
|
|
68
|
+
- **`createMotion()` is the owner.** It scopes selectors to a root, holds every
|
|
69
|
+
effect and cleanup, and `destroy()` reverts them all, newest first. Each API
|
|
70
|
+
takes `{ context }`. Without one, the call gets its own context and returns
|
|
71
|
+
a cleanup.
|
|
72
|
+
- **Effects re-run when the preference changes.** An effect sets up and returns
|
|
73
|
+
its undo. If the user switches reduced motion on or off while the page is
|
|
74
|
+
open, the effect is undone and run again with `scope.reduced` updated.
|
|
75
|
+
Progress carries over, so a finished entrance never replays.
|
|
76
|
+
- **Outcomes, not timeouts.** Anything that ends returns a promise that settles
|
|
77
|
+
once, `'finished'` or `'cancelled'`, and never rejects. Destroying the
|
|
78
|
+
owning context cancels it. Nothing waits on a timer.
|
|
79
|
+
- **One vocabulary.** The entrances `fade`, `fade-up`, `fade-down`,
|
|
80
|
+
`slide-left`, `slide-right`, `scale` and `clip` mean the same in CSS
|
|
81
|
+
reveals, `fadeIn` (Web Animations) and every GSAP API.
|
|
82
|
+
- **Tokens decide.** Durations, curves and travel distances come from
|
|
83
|
+
`@lovince/tokens`. `defaultMotionConfig` uses `duration.travel` and
|
|
84
|
+
`ease.out` (`power3.out`), and the CSS `--motion-*` properties are aliases
|
|
85
|
+
of `--lovince-*` ones, so CSS and GSAP move alike.
|
|
86
|
+
|
|
87
|
+
```ts
|
|
88
|
+
const config: MotionConfig = { duration: 0.72, ease: 'power3.out', stagger: 0.08, respectReducedMotion: true }
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Reduced motion
|
|
92
|
+
|
|
93
|
+
One central policy: under `prefers-reduced-motion: reduce`, motion is replaced
|
|
94
|
+
by its end state, not merely shortened.
|
|
95
|
+
|
|
96
|
+
| API | Under reduced motion |
|
|
97
|
+
|---|---|
|
|
98
|
+
| `revealOnScroll`, `createScrollReveal` | Content is shown; nothing is hidden |
|
|
99
|
+
| `fadeIn`, `createTimeline`, presets | Jump to the end state; promises settle `finished` |
|
|
100
|
+
| `createParallax`, `createHorizontalScroll` | Not created; the static layout stands |
|
|
101
|
+
| `revealText` | The text is never split |
|
|
102
|
+
| `followState` | States apply instantly |
|
|
103
|
+
| `createSmoothScroll` | No Lenis; native scrolling, switching live |
|
|
104
|
+
|
|
105
|
+
The stylesheet has the same rules, so CSS-only motion respects it too. Set
|
|
106
|
+
`respectReducedMotion: false` only for motion that carries meaning.
|
|
107
|
+
|
|
108
|
+
## Progressive enhancement
|
|
109
|
+
|
|
110
|
+
Content is never hidden by CSS alone. A reveal hides an element only by
|
|
111
|
+
setting `data-motion-state="hidden"` from script, and cleanup removes it. If
|
|
112
|
+
JavaScript, GSAP or the stylesheet fails to load, the page is simply static.
|
|
113
|
+
On the server no effect runs, and every entry imports safely.
|
|
114
|
+
|
|
115
|
+
## CSS layer
|
|
116
|
+
|
|
117
|
+
```html
|
|
118
|
+
<a class="motion-link" href="/docs">Docs</a>
|
|
119
|
+
<section data-motion-reveal="fade-up">…</section>
|
|
120
|
+
<h1 class="motion-fade-up" style="--motion-delay: 0.1s">Hello</h1>
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
`revealOnScroll(targets, { effect, once, threshold, rootMargin, stagger })` uses
|
|
124
|
+
IntersectionObserver, with no GSAP. Elements entering together get a
|
|
125
|
+
`--motion-delay` stagger. `fadeIn(targets, { effect, distance, duration,
|
|
126
|
+
stagger })` runs a Web Animations entrance with `fill: backwards`, so nothing
|
|
127
|
+
lingers afterwards.
|
|
128
|
+
|
|
129
|
+
Utilities: `.motion-link`, `.motion-lift`, `.motion-press`, `.motion-fade-up`,
|
|
130
|
+
`.motion-fade-in`, `.motion-scale-in`, `.motion-spin`, `.motion-pulse`. All of
|
|
131
|
+
them are opt-in classes, and none touches an element that didn't ask for it.
|
|
132
|
+
|
|
133
|
+
## GSAP layer
|
|
134
|
+
|
|
135
|
+
```ts
|
|
136
|
+
const intro = createTimeline((tl, scope) => {
|
|
137
|
+
tl.from(scope.select('h1'), { autoAlpha: 0, y: 28 })
|
|
138
|
+
.from(scope.select('.lede'), { autoAlpha: 0, y: 18, duration: 0.5 }, '-=0.4')
|
|
139
|
+
}, { context: motion })
|
|
140
|
+
|
|
141
|
+
await intro.finished // 'finished' | 'cancelled'
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
- `createTimeline(build, { context, vars, scrollTrigger })`: tweens are
|
|
145
|
+
reverted on destroy. A `scrollTrigger` is dropped under reduced motion.
|
|
146
|
+
- `createStagger(targets, { effect, stagger, onScroll })`: a collection, in
|
|
147
|
+
document order.
|
|
148
|
+
- `createScrollReveal(targets, { effect, start, repeat })`: batches items
|
|
149
|
+
entering together.
|
|
150
|
+
- `createParallax(targets, { speed, axis })`: linear, scrubbed, measured once.
|
|
151
|
+
Keep `speed` small.
|
|
152
|
+
- `createHorizontalScroll(section, track, { minWidth })`: pins below
|
|
153
|
+
`minWidth` only. Tabbing to an off-screen item scrolls it into view.
|
|
154
|
+
- `splitText(element, { by, mask, onResplit })`: words, chars or lines. It
|
|
155
|
+
keeps inline markup, handles any script via `Intl.Segmenter`, reads whole
|
|
156
|
+
words to screen readers, and re-splits lines when the width changes.
|
|
157
|
+
`revert()` restores the original nodes.
|
|
158
|
+
|
|
159
|
+
ScrollTrigger is registered on first use, not at import.
|
|
160
|
+
|
|
161
|
+
## Smooth scrolling
|
|
162
|
+
|
|
163
|
+
```ts
|
|
164
|
+
const scroll = createSmoothScroll({ driver: gsapScrollDriver(), lenis: { lerp: 0.1 } })
|
|
165
|
+
scroll.scrollTo('#pricing', { offset: -64 })
|
|
166
|
+
scroll.stop() // e.g. while a modal is open
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
There is one service per scroller. A second call for the same scroller
|
|
170
|
+
shares the instance, and the last `destroy()` tears it down, so components
|
|
171
|
+
can each ask for it safely. With `gsapScrollDriver()`, Lenis runs on GSAP's
|
|
172
|
+
ticker and keeps ScrollTrigger in sync: one frame loop, not two. Without a
|
|
173
|
+
driver, Lenis runs its own loop. Prefer native scrolling for apps, long forms
|
|
174
|
+
and content-heavy pages.
|
|
175
|
+
|
|
176
|
+
## Presets
|
|
177
|
+
|
|
178
|
+
- `heroEntrance({ label, heading, body, actions, media })`: reading order,
|
|
179
|
+
overlapped into one entrance. `actions` children stagger.
|
|
180
|
+
- `revealText(target, { by: 'words' | 'lines' | 'chars', onScroll })`: line
|
|
181
|
+
reveals rise from behind a mask, and rebuild where they were on resize.
|
|
182
|
+
- `revealImage(frames, { scale })`: the frame opens upward while the image
|
|
183
|
+
settles from a slight zoom. Only clip-path and transform change.
|
|
184
|
+
- `followState(target, { attribute, states })`: animates to whatever state the
|
|
185
|
+
component reports (e.g. `data-state` from `@lovince/ui-vue`), so a menu can
|
|
186
|
+
never look open while it is closed.
|
|
187
|
+
- `createPageTransition({ leave, enter, focus })`: `await leave(old)`, swap,
|
|
188
|
+
`enter(next)`. Every call settles, a new call cancels the last, and focus
|
|
189
|
+
moves into the new page.
|
|
190
|
+
|
|
191
|
+
## Frameworks
|
|
192
|
+
|
|
193
|
+
**Vanilla.** Create a context after the DOM exists, and call
|
|
194
|
+
`destroy()` when the view is removed.
|
|
195
|
+
|
|
196
|
+
**Vue / Nuxt.**
|
|
197
|
+
|
|
198
|
+
```vue
|
|
199
|
+
<script setup lang="ts">
|
|
200
|
+
import { useMotion, useMotionEffect, vReveal } from '@lovince/motion/vue'
|
|
201
|
+
import { createSmoothScroll } from '@lovince/motion/scroll'
|
|
202
|
+
import { gsapScrollDriver } from '@lovince/motion/gsap'
|
|
203
|
+
import { heroEntrance } from '@lovince/motion/presets'
|
|
204
|
+
|
|
205
|
+
const root = ref<HTMLElement | null>(null)
|
|
206
|
+
useMotion((motion) => heroEntrance({ heading: 'h1', body: 'p' }, { context: motion }), { root })
|
|
207
|
+
const scroll = useMotionEffect(() => createSmoothScroll({ driver: gsapScrollDriver() }))
|
|
208
|
+
</script>
|
|
209
|
+
|
|
210
|
+
<template>
|
|
211
|
+
<section ref="root"><h1>Hello</h1><p v-reveal="'fade-up'">…</p></section>
|
|
212
|
+
</template>
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
Everything starts on mount and is reverted on unmount. Nothing runs during
|
|
216
|
+
server rendering.
|
|
217
|
+
|
|
218
|
+
**React.**
|
|
219
|
+
|
|
220
|
+
```tsx
|
|
221
|
+
useLayoutEffect(() => {
|
|
222
|
+
const motion = createMotion({ root: ref.current })
|
|
223
|
+
heroEntrance({ heading: 'h1' }, { context: motion })
|
|
224
|
+
return motion.destroy // Strict Mode's double run is safe: destroy reverts fully
|
|
225
|
+
}, [])
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
**Svelte.**
|
|
229
|
+
|
|
230
|
+
```svelte
|
|
231
|
+
<script>
|
|
232
|
+
import { onMount } from 'svelte'
|
|
233
|
+
import { createMotion, revealOnScroll } from '@lovince/motion'
|
|
234
|
+
let root
|
|
235
|
+
onMount(() => {
|
|
236
|
+
const motion = createMotion({ root })
|
|
237
|
+
revealOnScroll('[data-reveal]', { context: motion })
|
|
238
|
+
return motion.destroy
|
|
239
|
+
})
|
|
240
|
+
</script>
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
## Performance
|
|
244
|
+
|
|
245
|
+
- Only `transform`, `opacity` and, for reveals, `clip-path` animate.
|
|
246
|
+
- Parallax and pinning measure through ScrollTrigger's refresh, never per
|
|
247
|
+
scroll event. Line splits re-measure only when the width changes.
|
|
248
|
+
- No permanent `will-change`. GSAP sets it per tween.
|
|
249
|
+
- One clock: Lenis rides GSAP's ticker when both are present.
|
|
250
|
+
- The main entry is under 6 KB gzip with no engine. GSAP and Lenis load only
|
|
251
|
+
through their entries.
|
|
252
|
+
|
|
253
|
+
## License
|
|
254
|
+
|
|
255
|
+
MIT
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { MotionTarget } from '../core/types';
|
|
2
|
+
/**
|
|
3
|
+
* Moves focus to the first element a target names, making it focusable for
|
|
4
|
+
* this purpose if it is not. Used after page transitions, so keyboard and
|
|
5
|
+
* screen-reader users land on the new content. Returns whether focus moved.
|
|
6
|
+
*/
|
|
7
|
+
export declare const focusTarget: (target: MotionTarget, options?: {
|
|
8
|
+
readonly preventScroll?: boolean;
|
|
9
|
+
}) => boolean;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { Cleanup } from '../core/types';
|
|
2
|
+
/**
|
|
3
|
+
* Runs `setup` only while the user has not asked for reduced motion, tearing
|
|
4
|
+
* it down when they do and setting it up again if they change back. For
|
|
5
|
+
* motion that has no reduced form at all, such as decorative loops.
|
|
6
|
+
*/
|
|
7
|
+
export declare const whenMotionAllowed: (setup: () => Cleanup | void) => Cleanup;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Cleanup } from './types';
|
|
2
|
+
export declare const noop: Cleanup;
|
|
3
|
+
export type Disposer = {
|
|
4
|
+
/** Registers a cleanup; runs it at once if already disposed. Returns a function that runs it early. */
|
|
5
|
+
readonly add: (cleanup: Cleanup) => Cleanup;
|
|
6
|
+
/** Runs every cleanup, newest first. Safe to call more than once. */
|
|
7
|
+
readonly dispose: () => void;
|
|
8
|
+
readonly disposed: () => boolean;
|
|
9
|
+
};
|
|
10
|
+
export declare const createDisposer: () => Disposer;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { DistanceName, MotionConfig } from './types';
|
|
2
|
+
export declare const defaultMotionConfig: MotionConfig;
|
|
3
|
+
export declare const resolveConfig: (overrides?: Partial<MotionConfig>) => MotionConfig;
|
|
4
|
+
/** Travel distances, in rem, on the spacing scale. The CSS layer reads the same steps as `--motion-distance-*`. */
|
|
5
|
+
export declare const motionDistance: Readonly<Record<DistanceName, string>>;
|
|
6
|
+
/** A distance in pixels, for engines that animate numbers. Follows the user's root font size. */
|
|
7
|
+
export declare const distancePx: (name: DistanceName) => number;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { Cleanup, MotionConfig, MotionTarget } from './types';
|
|
2
|
+
export type MotionRoot = Element | Document;
|
|
3
|
+
/** What an effect sees each time it runs. */
|
|
4
|
+
export type MotionScope = {
|
|
5
|
+
readonly root: MotionRoot;
|
|
6
|
+
readonly config: MotionConfig;
|
|
7
|
+
/** True when motion should be skipped: the user asked for less and the config respects it. */
|
|
8
|
+
readonly reduced: boolean;
|
|
9
|
+
readonly select: (target: MotionTarget) => Element[];
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Sets an animation up and returns how to undo it. It runs again, after its
|
|
13
|
+
* cleanup, whenever the motion preference changes, so carry anything that must
|
|
14
|
+
* survive a re-run (progress, "already played") outside the function.
|
|
15
|
+
*/
|
|
16
|
+
export type MotionEffect = (scope: MotionScope) => Cleanup | void;
|
|
17
|
+
export type MotionContext = {
|
|
18
|
+
readonly root: MotionRoot | null;
|
|
19
|
+
readonly config: MotionConfig;
|
|
20
|
+
readonly reduced: () => boolean;
|
|
21
|
+
readonly select: (target: MotionTarget) => Element[];
|
|
22
|
+
readonly effect: (setup: MotionEffect) => Cleanup;
|
|
23
|
+
/** Ties any other teardown to this context. */
|
|
24
|
+
readonly add: (cleanup: Cleanup) => Cleanup;
|
|
25
|
+
readonly destroy: () => void;
|
|
26
|
+
readonly destroyed: () => boolean;
|
|
27
|
+
};
|
|
28
|
+
export type MotionOptions = {
|
|
29
|
+
/** Scopes selectors and GSAP contexts. A selector is looked up in the document. */
|
|
30
|
+
readonly root?: MotionRoot | string | null;
|
|
31
|
+
readonly config?: Partial<MotionConfig>;
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* A scope that owns animations: every effect and cleanup registered here ends
|
|
35
|
+
* with `destroy()`. On the server, and when its root is missing, effects never
|
|
36
|
+
* run, so nothing is hidden that could not be revealed.
|
|
37
|
+
*/
|
|
38
|
+
export declare const createMotion: (options?: MotionOptions) => MotionContext;
|
|
39
|
+
/** Runs an effect in the given context, or in one of its own that the returned cleanup destroys. */
|
|
40
|
+
export declare const runInContext: (context: MotionContext | undefined, setup: MotionEffect, config?: Partial<MotionConfig>) => Cleanup;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { DistanceName, EntranceName } from './types';
|
|
2
|
+
/** Where an entrance starts, as engine-neutral numbers. `x`/`y` are in units of the chosen travel distance. */
|
|
3
|
+
export type EntranceFrom = {
|
|
4
|
+
readonly opacity: number;
|
|
5
|
+
readonly x: number;
|
|
6
|
+
readonly y: number;
|
|
7
|
+
readonly scale: number;
|
|
8
|
+
/** Starts clipped away from the bottom edge, revealed upward. */
|
|
9
|
+
readonly clip: boolean;
|
|
10
|
+
};
|
|
11
|
+
export declare const ENTRANCES: Readonly<Record<EntranceName, EntranceFrom>>;
|
|
12
|
+
export declare const CLIP_HIDDEN = "inset(0% 0% 100% 0%)";
|
|
13
|
+
export declare const CLIP_SHOWN = "inset(0% 0% 0% 0%)";
|
|
14
|
+
export type ResolvedEntrance = {
|
|
15
|
+
readonly opacity: number;
|
|
16
|
+
readonly x: number;
|
|
17
|
+
readonly y: number;
|
|
18
|
+
readonly scale: number;
|
|
19
|
+
readonly clipPath: string | null;
|
|
20
|
+
};
|
|
21
|
+
/** An entrance's starting state in pixels, ready for any engine. */
|
|
22
|
+
export declare const resolveEntrance: (name: EntranceName, distance?: DistanceName) => ResolvedEntrance;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { createDisposer, noop, type Disposer } from './cleanup';
|
|
2
|
+
export { defaultMotionConfig, distancePx, motionDistance, resolveConfig } from './config';
|
|
3
|
+
export { createMotion, runInContext, type MotionContext, type MotionEffect, type MotionOptions, type MotionRoot, type MotionScope, } from './context';
|
|
4
|
+
export { CLIP_HIDDEN, CLIP_SHOWN, ENTRANCES, resolveEntrance, type EntranceFrom, type ResolvedEntrance } from './entrance';
|
|
5
|
+
export { getMotionPreference, prefersReducedMotion, watchMotionPreference } from './preferences';
|
|
6
|
+
export { isElement, resolveTargets } from './targets';
|
|
7
|
+
export { DISTANCE_NAMES, ENTRANCE_NAMES, type Cleanup, type DistanceName, type EntranceName, type MotionConfig, type MotionOutcome, type MotionPreference, type MotionTarget, } from './types';
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { Cleanup, MotionPreference } from './types';
|
|
2
|
+
/** The user's motion preference. `no-preference` on the server and where it cannot be read. */
|
|
3
|
+
export declare const getMotionPreference: () => MotionPreference;
|
|
4
|
+
export declare const prefersReducedMotion: () => boolean;
|
|
5
|
+
/** Calls back whenever the preference changes while the page is open. */
|
|
6
|
+
export declare const watchMotionPreference: (listener: (preference: MotionPreference) => void) => Cleanup;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { MotionTarget } from './types';
|
|
2
|
+
export declare const isElement: (value: unknown) => value is Element;
|
|
3
|
+
/** Every element a target names, in order and without duplicates. Missing targets and bad selectors give `[]`, never a throw. */
|
|
4
|
+
export declare const resolveTargets: (target: MotionTarget, scope?: ParentNode | null) => Element[];
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** What an animation acts on. Selectors resolve inside the owning context's root. */
|
|
2
|
+
export type MotionTarget = Element | string | Iterable<Element> | ArrayLike<Element> | null | undefined;
|
|
3
|
+
export type Cleanup = () => void;
|
|
4
|
+
export type MotionPreference = 'reduce' | 'no-preference';
|
|
5
|
+
export type MotionConfig = {
|
|
6
|
+
/** Seconds, as GSAP counts them. */
|
|
7
|
+
readonly duration: number;
|
|
8
|
+
/** A GSAP ease name; the CSS layer uses the matching token curve. */
|
|
9
|
+
readonly ease: string;
|
|
10
|
+
/** Seconds between items in a collection. */
|
|
11
|
+
readonly stagger: number;
|
|
12
|
+
readonly respectReducedMotion: boolean;
|
|
13
|
+
};
|
|
14
|
+
export declare const ENTRANCE_NAMES: readonly ["fade", "fade-up", "fade-down", "slide-left", "slide-right", "scale", "clip"];
|
|
15
|
+
/** One vocabulary for every engine: CSS reveals, WAAPI and GSAP all accept these names. */
|
|
16
|
+
export type EntranceName = (typeof ENTRANCE_NAMES)[number];
|
|
17
|
+
export declare const DISTANCE_NAMES: readonly ["sm", "md", "lg"];
|
|
18
|
+
export type DistanceName = (typeof DISTANCE_NAMES)[number];
|
|
19
|
+
/** How an animation ended. Cancellation is an outcome, never a rejection. */
|
|
20
|
+
export type MotionOutcome = 'finished' | 'cancelled';
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export declare const styleOf: (element: Element) => CSSStyleDeclaration | null;
|
|
2
|
+
/** The CSS curve for a GSAP ease name from the token set, so both engines move alike. */
|
|
3
|
+
export declare const cssEase: (gsapEase: string) => string;
|
|
4
|
+
/** Resolves once, with an outcome; later calls are ignored. */
|
|
5
|
+
export declare const createOutcome: <T>() => {
|
|
6
|
+
readonly promise: Promise<T>;
|
|
7
|
+
readonly settle: (value: T) => void;
|
|
8
|
+
readonly settled: () => boolean;
|
|
9
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { MotionContext } from '../core/context';
|
|
2
|
+
import { DistanceName, EntranceName, MotionOutcome, MotionTarget } from '../core/types';
|
|
3
|
+
export type FadeInOptions = {
|
|
4
|
+
readonly context?: MotionContext;
|
|
5
|
+
readonly effect?: EntranceName;
|
|
6
|
+
readonly distance?: DistanceName;
|
|
7
|
+
/** Seconds. Defaults to the context's duration. */
|
|
8
|
+
readonly duration?: number;
|
|
9
|
+
readonly delay?: number;
|
|
10
|
+
readonly stagger?: number;
|
|
11
|
+
/** A CSS easing; defaults to the token curve matching the context's GSAP ease. */
|
|
12
|
+
readonly easing?: string;
|
|
13
|
+
};
|
|
14
|
+
export type MotionAnimation = {
|
|
15
|
+
/** Settles once: `finished`, or `cancelled` when stopped or its context is destroyed. Never rejects. */
|
|
16
|
+
readonly finished: Promise<MotionOutcome>;
|
|
17
|
+
readonly cancel: () => void;
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* A one-shot entrance on the Web Animations API: no GSAP, nothing left behind.
|
|
21
|
+
* `fill: backwards` hides elements only while they wait their turn; once done
|
|
22
|
+
* the animation drops away and the element's own styles apply. With reduced
|
|
23
|
+
* motion it settles at once and the content simply stays visible.
|
|
24
|
+
*/
|
|
25
|
+
export declare const fadeIn: (targets: MotionTarget, options?: FadeInOptions) => MotionAnimation;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { MotionContext } from '../core/context';
|
|
2
|
+
import { Cleanup, EntranceName, MotionTarget } from '../core/types';
|
|
3
|
+
export declare const REVEAL_ATTR = "data-motion-reveal";
|
|
4
|
+
export declare const STATE_ATTR = "data-motion-state";
|
|
5
|
+
export type RevealOptions = {
|
|
6
|
+
readonly context?: MotionContext;
|
|
7
|
+
/** Used when an element has no `data-motion-reveal` of its own. */
|
|
8
|
+
readonly effect?: EntranceName;
|
|
9
|
+
/** Reveal once, or hide again on leaving the viewport. */
|
|
10
|
+
readonly once?: boolean;
|
|
11
|
+
readonly threshold?: number;
|
|
12
|
+
readonly rootMargin?: string;
|
|
13
|
+
/** Seconds between elements that enter together. */
|
|
14
|
+
readonly stagger?: number;
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Reveals elements as they scroll into view, with the stylesheet's
|
|
18
|
+
* `[data-motion-reveal]` presets doing the motion. Content is hidden only once
|
|
19
|
+
* this has run, and cleanup leaves it visible, so a failed or skipped setup
|
|
20
|
+
* never strands it invisible. Needs no GSAP.
|
|
21
|
+
*/
|
|
22
|
+
export declare const revealOnScroll: (targets: MotionTarget, options?: RevealOptions) => Cleanup;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { a as e, n as t, o as n, r, s as i, t as a } from "./targets-x7JiHhWI.js";
|
|
2
|
+
import { duration as o, ease as s, space as c } from "@lovince/tokens";
|
|
3
|
+
//#region src/core/config.ts
|
|
4
|
+
var l = {
|
|
5
|
+
duration: o.travel,
|
|
6
|
+
ease: s.out.gsap,
|
|
7
|
+
stagger: .08,
|
|
8
|
+
respectReducedMotion: !0
|
|
9
|
+
}, u = (e = {}) => ({
|
|
10
|
+
...l,
|
|
11
|
+
...e
|
|
12
|
+
}), d = {
|
|
13
|
+
sm: c[3],
|
|
14
|
+
md: c[6],
|
|
15
|
+
lg: c[12]
|
|
16
|
+
}, f = () => {
|
|
17
|
+
if (typeof document > "u") return 16;
|
|
18
|
+
let e = Number.parseFloat(getComputedStyle(document.documentElement).fontSize);
|
|
19
|
+
return Number.isFinite(e) && e > 0 ? e : 16;
|
|
20
|
+
}, p = (e) => Number.parseFloat(d[e]) * f(), m = (e) => typeof document > "u" ? null : e == null ? document : typeof e == "string" ? t(e)[0] ?? null : a(e) || e instanceof Document ? e : null, h = (a = {}) => {
|
|
21
|
+
let o = u(a.config), s = m(a.root), c = n(), l = /* @__PURE__ */ new Set(), d = r(), f = () => o.respectReducedMotion && d === "reduce", p = (e) => s === null ? [] : t(e, s), h = () => s === null ? null : {
|
|
22
|
+
root: s,
|
|
23
|
+
config: o,
|
|
24
|
+
reduced: f(),
|
|
25
|
+
select: p
|
|
26
|
+
}, g = (e) => {
|
|
27
|
+
let t = h();
|
|
28
|
+
e.cleanup = (t === null ? void 0 : e.setup(t)) ?? i;
|
|
29
|
+
};
|
|
30
|
+
return c.add(e((e) => {
|
|
31
|
+
if (e !== d) {
|
|
32
|
+
d = e;
|
|
33
|
+
for (let e of l) e.cleanup(), g(e);
|
|
34
|
+
}
|
|
35
|
+
})), c.add(() => {
|
|
36
|
+
for (let e of [...l].reverse()) e.cleanup();
|
|
37
|
+
l.clear();
|
|
38
|
+
}), {
|
|
39
|
+
root: s,
|
|
40
|
+
config: o,
|
|
41
|
+
reduced: f,
|
|
42
|
+
select: p,
|
|
43
|
+
effect: (e) => {
|
|
44
|
+
if (c.disposed()) return i;
|
|
45
|
+
let t = {
|
|
46
|
+
setup: e,
|
|
47
|
+
cleanup: i
|
|
48
|
+
};
|
|
49
|
+
return l.add(t), g(t), () => {
|
|
50
|
+
l.delete(t) && t.cleanup();
|
|
51
|
+
};
|
|
52
|
+
},
|
|
53
|
+
add: c.add,
|
|
54
|
+
destroy: c.dispose,
|
|
55
|
+
destroyed: c.disposed
|
|
56
|
+
};
|
|
57
|
+
}, g = (e, t, n) => {
|
|
58
|
+
if (e !== void 0) return e.effect(t);
|
|
59
|
+
let r = h({ config: n });
|
|
60
|
+
return r.effect(t), r.destroy;
|
|
61
|
+
}, _ = (e) => typeof HTMLElement < "u" && (e instanceof HTMLElement || e instanceof SVGElement) ? e.style : null, v = (e) => Object.values(s).find((t) => t.gsap === e)?.css ?? s.out.css, y = () => {
|
|
62
|
+
let e = () => void 0, t = !1;
|
|
63
|
+
return {
|
|
64
|
+
promise: new Promise((t) => {
|
|
65
|
+
e = t;
|
|
66
|
+
}),
|
|
67
|
+
settle: (n) => {
|
|
68
|
+
t || (t = !0, e(n));
|
|
69
|
+
},
|
|
70
|
+
settled: () => t
|
|
71
|
+
};
|
|
72
|
+
};
|
|
73
|
+
//#endregion
|
|
74
|
+
export { g as a, d as c, h as i, u as l, v as n, l as o, _ as r, p as s, y as t };
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { n as e } from "./targets-x7JiHhWI.js";
|
|
2
|
+
//#region src/accessibility/focus-management.ts
|
|
3
|
+
var t = "a[href], button, input, select, textarea, summary, [tabindex], [contenteditable=\"true\"]", n = (n, r = {}) => {
|
|
4
|
+
let i = e(n)[0];
|
|
5
|
+
return i === void 0 || !(i instanceof HTMLElement || i instanceof SVGElement) ? !1 : (i.matches(t) || (i.setAttribute("tabindex", "-1"), i.addEventListener("blur", () => i.removeAttribute("tabindex"), { once: !0 })), i.focus({ preventScroll: r.preventScroll ?? !0 }), i.ownerDocument.activeElement === i);
|
|
6
|
+
};
|
|
7
|
+
//#endregion
|
|
8
|
+
export { n as t };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { ScrollTrigger } from 'gsap/ScrollTrigger';
|
|
2
|
+
import { MotionScope } from '../core/context';
|
|
3
|
+
import { DistanceName, EntranceName } from '../core/types';
|
|
4
|
+
/** ScrollTrigger, registered on first use rather than at import, so importing this layer has no side effects. */
|
|
5
|
+
export declare const scrollTrigger: () => typeof ScrollTrigger;
|
|
6
|
+
/** The element GSAP should scope selectors and `gsap.context` to; the document needs none. */
|
|
7
|
+
export declare const scopeElement: (scope: MotionScope) => Element | undefined;
|
|
8
|
+
/** Where an entrance starts, as GSAP vars. `autoAlpha` also hides it from assistive tech until it shows. */
|
|
9
|
+
export declare const entranceFrom: (effect: EntranceName, distance?: DistanceName) => gsap.TweenVars;
|
|
10
|
+
/** Where every entrance ends. */
|
|
11
|
+
export declare const entranceTo: (effect: EntranceName) => gsap.TweenVars;
|
|
12
|
+
/** Adds an entrance to a timeline. `fromTo` keeps clip reveals exact; `gsap.context` reverts either way. */
|
|
13
|
+
export declare const addEntrance: (timeline: gsap.core.Timeline, targets: gsap.TweenTarget, effect: EntranceName, options?: {
|
|
14
|
+
readonly distance?: DistanceName;
|
|
15
|
+
readonly vars?: gsap.TweenVars;
|
|
16
|
+
readonly position?: gsap.Position;
|
|
17
|
+
}) => gsap.core.Timeline;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { MotionContext } from '../core/context';
|
|
2
|
+
import { Cleanup, MotionTarget } from '../core/types';
|
|
3
|
+
export type HorizontalScrollOptions = {
|
|
4
|
+
readonly context?: MotionContext;
|
|
5
|
+
/** Below this width the section stays a normal, natively scrollable layout. */
|
|
6
|
+
readonly minWidth?: string;
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* Pins a section and moves its track sideways as the page scrolls. Narrow
|
|
10
|
+
* screens and reduced motion get the static layout. Keyboard focus moving to
|
|
11
|
+
* an off-screen item scrolls the page to the point where that item is in view,
|
|
12
|
+
* so tabbing through the gallery always shows what is focused.
|
|
13
|
+
*/
|
|
14
|
+
export declare const createHorizontalScroll: (section: MotionTarget, track: MotionTarget, options?: HorizontalScrollOptions) => Cleanup;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { addEntrance, entranceFrom, entranceTo, scrollTrigger } from './engine';
|
|
2
|
+
export { createHorizontalScroll, type HorizontalScrollOptions } from './horizontal-scroll';
|
|
3
|
+
export { createParallax, type ParallaxOptions } from './parallax';
|
|
4
|
+
export { createScrollReveal, type ScrollRevealOptions } from './scroll-reveal';
|
|
5
|
+
export { splitText, type SplitBy, type SplitOptions, type SplitText } from './split-text';
|
|
6
|
+
export { createStagger, type StaggerOptions } from './stagger';
|
|
7
|
+
export { createTimeline, type ManagedTimeline, type TimelineBuild, type TimelineOptions } from './timeline';
|
|
8
|
+
export { gsapScrollDriver } from '../scroll/gsap-integration';
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { MotionContext } from '../core/context';
|
|
2
|
+
import { Cleanup, MotionTarget } from '../core/types';
|
|
3
|
+
export type ParallaxOptions = {
|
|
4
|
+
readonly context?: MotionContext;
|
|
5
|
+
/** Travel across the viewport as a fraction of the element's size; negative moves against the scroll. Keep it small. */
|
|
6
|
+
readonly speed?: number;
|
|
7
|
+
readonly axis?: 'x' | 'y';
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Scroll-linked drift, linear and scrubbed to scroll progress. Each element
|
|
11
|
+
* moves relative to its own scroll range, so nothing is measured on scroll.
|
|
12
|
+
* Skipped entirely under reduced motion: parallax has no gentle form.
|
|
13
|
+
*/
|
|
14
|
+
export declare const createParallax: (targets: MotionTarget, options?: ParallaxOptions) => Cleanup;
|