solid-drift 0.29.0 → 0.31.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/README.md +55 -0
- package/dist/copy.d.ts +27 -0
- package/dist/copy.js +80 -0
- package/dist/countdown.d.ts +38 -0
- package/dist/countdown.js +90 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +6 -0
- package/dist/marquee.d.ts +57 -0
- package/dist/marquee.js +88 -0
- package/dist/pathdraw.d.ts +49 -0
- package/dist/pathdraw.js +106 -0
- package/dist/press.d.ts +57 -0
- package/dist/press.js +116 -0
- package/dist/variants.d.ts +63 -0
- package/dist/variants.js +86 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1643,6 +1643,61 @@ const spy = createScrollSpy({ targets: ["intro", "api", "faq"], offset: 80 });
|
|
|
1643
1643
|
- `createSkeleton(options?)` is a loading-placeholder controller with flicker protection: `{ loading, show, phase, setLoading }`. `show()` flips true only after `delay` ms (default 200), so fast loads never flash a skeleton, and stays true for at least `minVisible` ms once shown. `phase()` sweeps 0..1 on the shared clock while shown for a JS-driven shimmer (bind it to a gradient stop); it freezes under reduced motion. On the server `show()` never flips.
|
|
1644
1644
|
- `createScrollSpy(options)` tracks the deepest section at or above the offset line: `{ active, scrollTo, refresh }`. `targets` is an id list or accessor; `container` defaults to the window (pass an element for a scrollable panel); scroll handling is rAF-throttled on the shared clock; `scrollTo(id)` smooth-scrolls (auto under reduced motion); `onChange` fires only when the active id changes. SSR-safe.
|
|
1645
1645
|
|
|
1646
|
+
### Copy and countdown
|
|
1647
|
+
|
|
1648
|
+
```tsx
|
|
1649
|
+
import { createCopy, createCountdown } from "solid-drift";
|
|
1650
|
+
|
|
1651
|
+
const clipboard = createCopy();
|
|
1652
|
+
const sale = createCountdown(new Date("2026-12-01T00:00:00"), {
|
|
1653
|
+
onDone: () => toast("The sale has ended"),
|
|
1654
|
+
});
|
|
1655
|
+
|
|
1656
|
+
<button onClick={() => clipboard.copy(link())}>
|
|
1657
|
+
{clipboard.copied() ? "Copied!" : "Copy link"}
|
|
1658
|
+
</button>
|
|
1659
|
+
<p>{sale.days()}d {sale.hours()}h {sale.minutes()}m {sale.seconds()}s</p>
|
|
1660
|
+
```
|
|
1661
|
+
|
|
1662
|
+
- `createCopy(options?)` copies text to the clipboard: `{ copied, error, copy, reset }`. Uses the async Clipboard API with an `execCommand` fallback (`noFallback: true` disables it). `copied()` flips true for `resetDelay` ms (default 2000) for transient "Copied!" feedback. SSR-safe.
|
|
1663
|
+
- `createCountdown(target, options?)` counts down to a date, timestamp, or accessor: `{ remaining, days, hours, minutes, seconds, done, running, start, stop, reset }`. Wall-clock based (the moment is fixed even if the tab hides); recomputes on the shared clock throttled to `interval` ms (default 1000); stops itself at zero and fires `onDone` once. SSR-safe.
|
|
1664
|
+
|
|
1665
|
+
### Marquee, variants, path drawing, press and hover
|
|
1666
|
+
|
|
1667
|
+
```tsx
|
|
1668
|
+
import {
|
|
1669
|
+
createMarquee,
|
|
1670
|
+
createVariants,
|
|
1671
|
+
createPathDraw,
|
|
1672
|
+
createPress,
|
|
1673
|
+
createHover,
|
|
1674
|
+
} from "solid-drift";
|
|
1675
|
+
|
|
1676
|
+
const marquee = createMarquee({ speed: 80 });
|
|
1677
|
+
const card = createVariants(
|
|
1678
|
+
{
|
|
1679
|
+
idle: { scale: 1 },
|
|
1680
|
+
hover: { scale: 1.04 },
|
|
1681
|
+
press: { scale: 0.96 },
|
|
1682
|
+
},
|
|
1683
|
+
{ initial: "idle", duration: 180 },
|
|
1684
|
+
);
|
|
1685
|
+
|
|
1686
|
+
let strip!: HTMLDivElement;
|
|
1687
|
+
let btn!: HTMLButtonElement;
|
|
1688
|
+
let mark!: SVGPathElement;
|
|
1689
|
+
createEffect(() => marquee.setContentSize(strip.scrollWidth / 2));
|
|
1690
|
+
createHover(() => btn, { onChange: (h) => card.go(h ? "hover" : "idle") });
|
|
1691
|
+
createPress(() => btn, { onChange: (p) => card.go(p ? "press" : "idle") });
|
|
1692
|
+
const draw = createPathDraw(() => mark, { duration: 1600 });
|
|
1693
|
+
```
|
|
1694
|
+
|
|
1695
|
+
- `createMarquee(options?)` infinite scroller: `{ offset, running, setContentSize, start, stop }`. The offset advances at `speed` px/s (`direction` left/right/up/down) on the shared clock and wraps at the content size; render the content twice and translate by `-offset()`. Measure one loop unit and pass it to `setContentSize`. Static under reduced motion; SSR-safe.
|
|
1696
|
+
- `createVariants(defs, options?)` named animation states: `{ current, values, go }`. `go(name)` tweens numeric props from the current values to the target variant (`duration` ms, easing) and snaps non-numeric props at the end; unknown names are ignored. Snaps instantly under reduced motion; SSR-safe.
|
|
1697
|
+
- `createPathDraw(ref, options?)` SVG stroke draw-on: `{ progress, running, start, stop, reset }`. Reads the length with `getTotalLength()` and drives `stroke-dashoffset` to 0, eased; `onDone` fires once; resume keeps a constant speed. Renders fully drawn under reduced motion; SSR-safe.
|
|
1698
|
+
- `createPress(ref, options?)` press gesture state: `{ pressed }`. Pointer down/up/cancel/leave plus Enter/Space keys for keyboard parity; `onChange` fires on change only. State only, no animation; pair with `createVariants`. SSR-safe.
|
|
1699
|
+
- `createHover(ref, options?)` hover gesture state: `{ hovering }`. Pointer enter/leave plus focus/blur for keyboard parity; `onChange` fires on change only. SSR-safe.
|
|
1700
|
+
|
|
1646
1701
|
### Easings
|
|
1647
1702
|
|
|
1648
1703
|
Named easings: `linear`, `easeInQuad`, `easeOutQuad`, `easeInOutQuad`, `easeInCubic`, `easeOutCubic`, `easeInOutCubic`, `easeInQuart`, `easeOutQuart`, `easeInOutQuart`, `easeOutExpo`, `easeOutBack`, plus the cartoon set: `easeInBack` (anticipation dip before movement), `easeInOutBack` (wind-up, overshoot, settle), `easeOutElastic` (decaying rubber-band oscillation), `easeOutBounce` (shrinking cartoon bounces). Also `cubicBezier(x1, y1, x2, y2)` for CSS-style curves. Pass a name or a custom `(t) => number` function anywhere an easing is accepted.
|
package/dist/copy.d.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export interface CopyOptions {
|
|
2
|
+
/** Milliseconds before `copied` flips back to false. Default 2000. */
|
|
3
|
+
resetDelay?: number;
|
|
4
|
+
/** Skip the execCommand fallback and require the async Clipboard API. Default false. */
|
|
5
|
+
noFallback?: boolean;
|
|
6
|
+
}
|
|
7
|
+
export interface CopyControls {
|
|
8
|
+
/** True briefly after a successful copy (for "Copied!" feedback). */
|
|
9
|
+
copied: () => boolean;
|
|
10
|
+
error: () => Error | null;
|
|
11
|
+
copy: (text: string) => Promise<void>;
|
|
12
|
+
reset: () => void;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* createCopy
|
|
16
|
+
*
|
|
17
|
+
* Copy-to-clipboard with a `copied` flag for transient feedback. Uses the
|
|
18
|
+
* async Clipboard API with an `execCommand` fallback for older browsers.
|
|
19
|
+
*
|
|
20
|
+
* ```tsx
|
|
21
|
+
* const clipboard = createCopy();
|
|
22
|
+
* <button onClick={() => clipboard.copy(link())}>
|
|
23
|
+
* {clipboard.copied() ? "Copied!" : "Copy link"}
|
|
24
|
+
* </button>
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
export declare function createCopy(options?: CopyOptions): CopyControls;
|
package/dist/copy.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { createSignal, onCleanup } from "solid-js";
|
|
2
|
+
function legacyCopy(text) {
|
|
3
|
+
const doc = globalThis.document;
|
|
4
|
+
if (!doc || typeof doc.execCommand !== "function") {
|
|
5
|
+
throw new Error("Clipboard API is not supported.");
|
|
6
|
+
}
|
|
7
|
+
const area = doc.createElement("textarea");
|
|
8
|
+
area.value = text;
|
|
9
|
+
area.setAttribute("readonly", "");
|
|
10
|
+
area.style.position = "fixed";
|
|
11
|
+
area.style.opacity = "0";
|
|
12
|
+
doc.body.appendChild(area);
|
|
13
|
+
area.select();
|
|
14
|
+
const ok = doc.execCommand("copy");
|
|
15
|
+
doc.body.removeChild(area);
|
|
16
|
+
if (!ok)
|
|
17
|
+
throw new Error("Copy command failed.");
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* createCopy
|
|
21
|
+
*
|
|
22
|
+
* Copy-to-clipboard with a `copied` flag for transient feedback. Uses the
|
|
23
|
+
* async Clipboard API with an `execCommand` fallback for older browsers.
|
|
24
|
+
*
|
|
25
|
+
* ```tsx
|
|
26
|
+
* const clipboard = createCopy();
|
|
27
|
+
* <button onClick={() => clipboard.copy(link())}>
|
|
28
|
+
* {clipboard.copied() ? "Copied!" : "Copy link"}
|
|
29
|
+
* </button>
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
export function createCopy(options = {}) {
|
|
33
|
+
const { resetDelay = 2000, noFallback = false } = options;
|
|
34
|
+
const [copied, setCopied] = createSignal(false);
|
|
35
|
+
const [error, setError] = createSignal(null);
|
|
36
|
+
let timer = null;
|
|
37
|
+
const markCopied = () => {
|
|
38
|
+
setCopied(true);
|
|
39
|
+
if (timer !== null)
|
|
40
|
+
clearTimeout(timer);
|
|
41
|
+
timer = null;
|
|
42
|
+
if (resetDelay > 0) {
|
|
43
|
+
timer = setTimeout(() => {
|
|
44
|
+
timer = null;
|
|
45
|
+
setCopied(false);
|
|
46
|
+
}, resetDelay);
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
const copy = async (text) => {
|
|
50
|
+
setError(null);
|
|
51
|
+
const nav = globalThis.navigator;
|
|
52
|
+
try {
|
|
53
|
+
const writeText = nav?.clipboard?.writeText;
|
|
54
|
+
if (typeof writeText === "function") {
|
|
55
|
+
await writeText.call(nav?.clipboard, text);
|
|
56
|
+
markCopied();
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
if (!noFallback) {
|
|
60
|
+
legacyCopy(text);
|
|
61
|
+
markCopied();
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
throw new Error("Clipboard API is not supported.");
|
|
65
|
+
}
|
|
66
|
+
catch (e) {
|
|
67
|
+
const err = e instanceof Error ? e : new Error(String(e));
|
|
68
|
+
setError(err);
|
|
69
|
+
throw err;
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
const reset = () => {
|
|
73
|
+
if (timer !== null)
|
|
74
|
+
clearTimeout(timer);
|
|
75
|
+
timer = null;
|
|
76
|
+
setCopied(false);
|
|
77
|
+
};
|
|
78
|
+
onCleanup(reset);
|
|
79
|
+
return { copied, error, copy, reset };
|
|
80
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export interface CountdownOptions {
|
|
2
|
+
/** Milliseconds between recomputes. Default 1000. */
|
|
3
|
+
interval?: number;
|
|
4
|
+
/** Start immediately on creation. Default true. */
|
|
5
|
+
autoStart?: boolean;
|
|
6
|
+
onDone?: () => void;
|
|
7
|
+
}
|
|
8
|
+
export interface CountdownControls {
|
|
9
|
+
/** Milliseconds remaining, clamped at 0. */
|
|
10
|
+
remaining: () => number;
|
|
11
|
+
days: () => number;
|
|
12
|
+
hours: () => number;
|
|
13
|
+
minutes: () => number;
|
|
14
|
+
seconds: () => number;
|
|
15
|
+
done: () => boolean;
|
|
16
|
+
running: () => boolean;
|
|
17
|
+
start: () => void;
|
|
18
|
+
stop: () => void;
|
|
19
|
+
reset: () => void;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* createCountdown
|
|
23
|
+
*
|
|
24
|
+
* Countdown to a target date or timestamp. Uses wall-clock time (a sale
|
|
25
|
+
* ends at a real moment even if the tab was hidden) and recomputes on
|
|
26
|
+
* the shared clock, throttled to `interval`. The task stops itself at
|
|
27
|
+
* zero and `onDone` fires exactly once. `stop()` halts display updates;
|
|
28
|
+
* the target is a fixed wall-clock moment, so `start()` resumes against
|
|
29
|
+
* the same target.
|
|
30
|
+
*
|
|
31
|
+
* ```tsx
|
|
32
|
+
* const sale = createCountdown(new Date("2026-12-01T00:00:00"), {
|
|
33
|
+
* onDone: () => toast("The sale has ended"),
|
|
34
|
+
* });
|
|
35
|
+
* <p>{sale.days()}d {sale.hours()}h {sale.minutes()}m {sale.seconds()}s</p>
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
export declare function createCountdown(target: Date | number | (() => Date | number), options?: CountdownOptions): CountdownControls;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { createSignal, onCleanup } from "solid-js";
|
|
2
|
+
import { schedule } from "./engine.js";
|
|
3
|
+
/**
|
|
4
|
+
* createCountdown
|
|
5
|
+
*
|
|
6
|
+
* Countdown to a target date or timestamp. Uses wall-clock time (a sale
|
|
7
|
+
* ends at a real moment even if the tab was hidden) and recomputes on
|
|
8
|
+
* the shared clock, throttled to `interval`. The task stops itself at
|
|
9
|
+
* zero and `onDone` fires exactly once. `stop()` halts display updates;
|
|
10
|
+
* the target is a fixed wall-clock moment, so `start()` resumes against
|
|
11
|
+
* the same target.
|
|
12
|
+
*
|
|
13
|
+
* ```tsx
|
|
14
|
+
* const sale = createCountdown(new Date("2026-12-01T00:00:00"), {
|
|
15
|
+
* onDone: () => toast("The sale has ended"),
|
|
16
|
+
* });
|
|
17
|
+
* <p>{sale.days()}d {sale.hours()}h {sale.minutes()}m {sale.seconds()}s</p>
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
export function createCountdown(target, options = {}) {
|
|
21
|
+
const { interval = 1000, autoStart = true, onDone } = options;
|
|
22
|
+
const [remaining, setRemaining] = createSignal(0);
|
|
23
|
+
const [done, setDone] = createSignal(false);
|
|
24
|
+
const [running, setRunning] = createSignal(false);
|
|
25
|
+
let stopClock = null;
|
|
26
|
+
let doneFired = false;
|
|
27
|
+
const targetMs = () => {
|
|
28
|
+
const t = typeof target === "function" ? target() : target;
|
|
29
|
+
return t instanceof Date ? t.getTime() : t;
|
|
30
|
+
};
|
|
31
|
+
const compute = (nowMs) => {
|
|
32
|
+
const left = Math.max(0, targetMs() - nowMs);
|
|
33
|
+
setRemaining(left);
|
|
34
|
+
if (left <= 0 && !doneFired) {
|
|
35
|
+
doneFired = true;
|
|
36
|
+
setDone(true);
|
|
37
|
+
stopClock?.();
|
|
38
|
+
stopClock = null;
|
|
39
|
+
setRunning(false);
|
|
40
|
+
onDone?.();
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
const start = () => {
|
|
44
|
+
if (running())
|
|
45
|
+
return;
|
|
46
|
+
doneFired = false;
|
|
47
|
+
setDone(false);
|
|
48
|
+
compute(Date.now());
|
|
49
|
+
if (done())
|
|
50
|
+
return;
|
|
51
|
+
if (typeof globalThis.requestAnimationFrame === "undefined")
|
|
52
|
+
return;
|
|
53
|
+
setRunning(true);
|
|
54
|
+
let last = Date.now();
|
|
55
|
+
stopClock = schedule(() => {
|
|
56
|
+
const nowMs = Date.now();
|
|
57
|
+
if (nowMs - last >= interval) {
|
|
58
|
+
last = nowMs;
|
|
59
|
+
compute(nowMs);
|
|
60
|
+
}
|
|
61
|
+
return !done();
|
|
62
|
+
});
|
|
63
|
+
};
|
|
64
|
+
const stop = () => {
|
|
65
|
+
stopClock?.();
|
|
66
|
+
stopClock = null;
|
|
67
|
+
setRunning(false);
|
|
68
|
+
};
|
|
69
|
+
const reset = () => {
|
|
70
|
+
stop();
|
|
71
|
+
doneFired = false;
|
|
72
|
+
setDone(false);
|
|
73
|
+
compute(Date.now());
|
|
74
|
+
};
|
|
75
|
+
onCleanup(stop);
|
|
76
|
+
if (autoStart)
|
|
77
|
+
start();
|
|
78
|
+
return {
|
|
79
|
+
remaining,
|
|
80
|
+
days: () => Math.floor(remaining() / 86400000),
|
|
81
|
+
hours: () => Math.floor(remaining() / 3600000) % 24,
|
|
82
|
+
minutes: () => Math.floor(remaining() / 60000) % 60,
|
|
83
|
+
seconds: () => Math.floor(remaining() / 1000) % 60,
|
|
84
|
+
done,
|
|
85
|
+
running,
|
|
86
|
+
start,
|
|
87
|
+
stop,
|
|
88
|
+
reset,
|
|
89
|
+
};
|
|
90
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -52,3 +52,15 @@ export { createSkeleton } from "./skeleton.js";
|
|
|
52
52
|
export type { SkeletonOptions, SkeletonControls } from "./skeleton.js";
|
|
53
53
|
export { createScrollSpy } from "./scrollspy.js";
|
|
54
54
|
export type { ScrollSpyOptions, ScrollSpyControls } from "./scrollspy.js";
|
|
55
|
+
export { createCopy } from "./copy.js";
|
|
56
|
+
export type { CopyOptions, CopyControls } from "./copy.js";
|
|
57
|
+
export { createCountdown } from "./countdown.js";
|
|
58
|
+
export type { CountdownOptions, CountdownControls } from "./countdown.js";
|
|
59
|
+
export { createMarquee } from "./marquee.js";
|
|
60
|
+
export type { MarqueeDirection, MarqueeOptions, MarqueeControls, } from "./marquee.js";
|
|
61
|
+
export { createVariants } from "./variants.js";
|
|
62
|
+
export type { VariantDef, VariantsOptions, VariantsControls, } from "./variants.js";
|
|
63
|
+
export { createPathDraw } from "./pathdraw.js";
|
|
64
|
+
export type { PathDrawOptions, PathDrawControls } from "./pathdraw.js";
|
|
65
|
+
export { createPress, createHover } from "./press.js";
|
|
66
|
+
export type { PressOptions, PressControls, HoverOptions, HoverControls, } from "./press.js";
|
package/dist/index.js
CHANGED
|
@@ -42,3 +42,9 @@ export { createBattery, createNetwork, createWakeLock, createContactPick, create
|
|
|
42
42
|
export { createOptimistic } from "./optimistic.js";
|
|
43
43
|
export { createSkeleton } from "./skeleton.js";
|
|
44
44
|
export { createScrollSpy } from "./scrollspy.js";
|
|
45
|
+
export { createCopy } from "./copy.js";
|
|
46
|
+
export { createCountdown } from "./countdown.js";
|
|
47
|
+
export { createMarquee } from "./marquee.js";
|
|
48
|
+
export { createVariants } from "./variants.js";
|
|
49
|
+
export { createPathDraw } from "./pathdraw.js";
|
|
50
|
+
export { createPress, createHover } from "./press.js";
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { type Accessor } from "solid-js";
|
|
2
|
+
/** Marquee scroll direction. Default "left". */
|
|
3
|
+
export type MarqueeDirection = "left" | "right" | "up" | "down";
|
|
4
|
+
export interface MarqueeOptions {
|
|
5
|
+
/** Pixels per second. Default 60. */
|
|
6
|
+
speed?: number;
|
|
7
|
+
/** Scroll direction. Default "left". */
|
|
8
|
+
direction?: MarqueeDirection;
|
|
9
|
+
/** Start scrolling immediately. Default true. */
|
|
10
|
+
autoStart?: boolean;
|
|
11
|
+
}
|
|
12
|
+
export interface MarqueeControls {
|
|
13
|
+
/**
|
|
14
|
+
* Current offset in px. Wraps at the content size, so rendering two
|
|
15
|
+
* copies of the content side by side and translating by `-offset()`
|
|
16
|
+
* (or `-offset()` on Y for vertical) loops seamlessly.
|
|
17
|
+
*/
|
|
18
|
+
offset: Accessor<number>;
|
|
19
|
+
/** Whether the marquee clock task is running. */
|
|
20
|
+
running: Accessor<boolean>;
|
|
21
|
+
/**
|
|
22
|
+
* Width (horizontal directions) or height (vertical directions) of one
|
|
23
|
+
* loop unit in px. Measure the content and call this once it is known.
|
|
24
|
+
*/
|
|
25
|
+
setContentSize: (px: number) => void;
|
|
26
|
+
start: () => void;
|
|
27
|
+
stop: () => void;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Infinite marquee scroller. The offset advances at `speed` px/s on the
|
|
31
|
+
* shared clock and wraps at the content size, so a doubled content strip
|
|
32
|
+
* loops seamlessly.
|
|
33
|
+
*
|
|
34
|
+
* Reduced-motion aware: marquees are pure motion, so under reduced motion
|
|
35
|
+
* the marquee stays static (offset 0, never runs). SSR-safe.
|
|
36
|
+
*
|
|
37
|
+
* ```tsx
|
|
38
|
+
* const marquee = createMarquee({ speed: 80 });
|
|
39
|
+
* let strip!: HTMLDivElement;
|
|
40
|
+
* createEffect(() => {
|
|
41
|
+
* marquee.setContentSize(strip.scrollWidth / 2);
|
|
42
|
+
* });
|
|
43
|
+
* <div style={{ overflow: "hidden" }}>
|
|
44
|
+
* <div
|
|
45
|
+
* ref={strip}
|
|
46
|
+
* style={{
|
|
47
|
+
* display: "flex",
|
|
48
|
+
* transform: `translateX(${-marquee.offset()}px)`,
|
|
49
|
+
* "will-change": "transform",
|
|
50
|
+
* }}
|
|
51
|
+
* >
|
|
52
|
+
* {items}{items}
|
|
53
|
+
* </div>
|
|
54
|
+
* </div>
|
|
55
|
+
* ```
|
|
56
|
+
*/
|
|
57
|
+
export declare function createMarquee(options?: MarqueeOptions): MarqueeControls;
|
package/dist/marquee.js
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { createSignal, onCleanup, untrack } from "solid-js";
|
|
2
|
+
import { now, schedule } from "./engine.js";
|
|
3
|
+
import { prefersReducedMotion } from "./reduced-motion.js";
|
|
4
|
+
/**
|
|
5
|
+
* Infinite marquee scroller. The offset advances at `speed` px/s on the
|
|
6
|
+
* shared clock and wraps at the content size, so a doubled content strip
|
|
7
|
+
* loops seamlessly.
|
|
8
|
+
*
|
|
9
|
+
* Reduced-motion aware: marquees are pure motion, so under reduced motion
|
|
10
|
+
* the marquee stays static (offset 0, never runs). SSR-safe.
|
|
11
|
+
*
|
|
12
|
+
* ```tsx
|
|
13
|
+
* const marquee = createMarquee({ speed: 80 });
|
|
14
|
+
* let strip!: HTMLDivElement;
|
|
15
|
+
* createEffect(() => {
|
|
16
|
+
* marquee.setContentSize(strip.scrollWidth / 2);
|
|
17
|
+
* });
|
|
18
|
+
* <div style={{ overflow: "hidden" }}>
|
|
19
|
+
* <div
|
|
20
|
+
* ref={strip}
|
|
21
|
+
* style={{
|
|
22
|
+
* display: "flex",
|
|
23
|
+
* transform: `translateX(${-marquee.offset()}px)`,
|
|
24
|
+
* "will-change": "transform",
|
|
25
|
+
* }}
|
|
26
|
+
* >
|
|
27
|
+
* {items}{items}
|
|
28
|
+
* </div>
|
|
29
|
+
* </div>
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
export function createMarquee(options = {}) {
|
|
33
|
+
const { speed = 60, direction = "left", autoStart = true } = options;
|
|
34
|
+
if (typeof window === "undefined") {
|
|
35
|
+
const zero = () => 0;
|
|
36
|
+
const falsy = () => false;
|
|
37
|
+
const noop = () => { };
|
|
38
|
+
return {
|
|
39
|
+
offset: zero,
|
|
40
|
+
running: falsy,
|
|
41
|
+
setContentSize: noop,
|
|
42
|
+
start: noop,
|
|
43
|
+
stop: noop,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
const sign = direction === "left" || direction === "up" ? 1 : -1;
|
|
47
|
+
const [offset, setOffset] = createSignal(0);
|
|
48
|
+
const [running, setRunning] = createSignal(false);
|
|
49
|
+
let size = 0;
|
|
50
|
+
let phase = 0;
|
|
51
|
+
let last = 0;
|
|
52
|
+
let stopClock = null;
|
|
53
|
+
const task = (t) => {
|
|
54
|
+
const dt = Math.min(Math.max((t - last) / 1000, 0), 0.25);
|
|
55
|
+
last = t;
|
|
56
|
+
phase += speed * dt;
|
|
57
|
+
if (size > 0) {
|
|
58
|
+
setOffset((((phase * sign) % size) + size) % size);
|
|
59
|
+
}
|
|
60
|
+
return true;
|
|
61
|
+
};
|
|
62
|
+
const start = () => {
|
|
63
|
+
if (untrack(running) || prefersReducedMotion())
|
|
64
|
+
return;
|
|
65
|
+
setRunning(true);
|
|
66
|
+
last = now();
|
|
67
|
+
stopClock = schedule(task);
|
|
68
|
+
};
|
|
69
|
+
const stop = () => {
|
|
70
|
+
stopClock?.();
|
|
71
|
+
stopClock = null;
|
|
72
|
+
setRunning(false);
|
|
73
|
+
};
|
|
74
|
+
onCleanup(stop);
|
|
75
|
+
if (autoStart)
|
|
76
|
+
start();
|
|
77
|
+
return {
|
|
78
|
+
offset,
|
|
79
|
+
running,
|
|
80
|
+
setContentSize: (px) => {
|
|
81
|
+
size = Math.max(px, 0);
|
|
82
|
+
phase = 0;
|
|
83
|
+
setOffset(0);
|
|
84
|
+
},
|
|
85
|
+
start,
|
|
86
|
+
stop,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { type Accessor } from "solid-js";
|
|
2
|
+
import { type Easing } from "./easing.js";
|
|
3
|
+
export interface PathDrawOptions {
|
|
4
|
+
/** Draw duration in ms. Default 1200. */
|
|
5
|
+
duration?: number;
|
|
6
|
+
/** Easing for the draw progress. Default easeInOutCubic. */
|
|
7
|
+
easing?: Easing;
|
|
8
|
+
/** Start drawing immediately. Default true. */
|
|
9
|
+
autoStart?: boolean;
|
|
10
|
+
/** Fires once when the draw completes. */
|
|
11
|
+
onDone?: () => void;
|
|
12
|
+
}
|
|
13
|
+
export interface PathDrawControls {
|
|
14
|
+
/** Eased draw progress, 0 to 1. */
|
|
15
|
+
progress: Accessor<number>;
|
|
16
|
+
/** Whether the draw clock task is running. */
|
|
17
|
+
running: Accessor<boolean>;
|
|
18
|
+
start: () => void;
|
|
19
|
+
stop: () => void;
|
|
20
|
+
/** Back to undrawn (progress 0). */
|
|
21
|
+
reset: () => void;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* SVG path drawing animation. Drives `stroke-dashoffset` from the full
|
|
25
|
+
* path length to 0 so the stroke draws itself on, eased on the shared
|
|
26
|
+
* clock. The length is read with `getTotalLength()`, so any path shape
|
|
27
|
+
* works with no manual measuring.
|
|
28
|
+
*
|
|
29
|
+
* Under reduced motion (and on the server) the path renders fully drawn.
|
|
30
|
+
* SSR-safe.
|
|
31
|
+
*
|
|
32
|
+
* ```tsx
|
|
33
|
+
* let path!: SVGPathElement;
|
|
34
|
+
* const draw = createPathDraw(() => path, {
|
|
35
|
+
* duration: 1600,
|
|
36
|
+
* onDone: () => console.log("drawn"),
|
|
37
|
+
* });
|
|
38
|
+
* <svg viewBox="0 0 100 100">
|
|
39
|
+
* <path
|
|
40
|
+
* ref={path}
|
|
41
|
+
* d="M10 80 C 40 10, 60 10, 90 80"
|
|
42
|
+
* fill="none"
|
|
43
|
+
* stroke="currentColor"
|
|
44
|
+
* stroke-width="3"
|
|
45
|
+
* />
|
|
46
|
+
* </svg>
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
export declare function createPathDraw(ref: () => SVGPathElement | null | undefined, options?: PathDrawOptions): PathDrawControls;
|
package/dist/pathdraw.js
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { createSignal, onCleanup } from "solid-js";
|
|
2
|
+
import { now, schedule } from "./engine.js";
|
|
3
|
+
import { easeInOutCubic } from "./easing.js";
|
|
4
|
+
import { prefersReducedMotion } from "./reduced-motion.js";
|
|
5
|
+
/**
|
|
6
|
+
* SVG path drawing animation. Drives `stroke-dashoffset` from the full
|
|
7
|
+
* path length to 0 so the stroke draws itself on, eased on the shared
|
|
8
|
+
* clock. The length is read with `getTotalLength()`, so any path shape
|
|
9
|
+
* works with no manual measuring.
|
|
10
|
+
*
|
|
11
|
+
* Under reduced motion (and on the server) the path renders fully drawn.
|
|
12
|
+
* SSR-safe.
|
|
13
|
+
*
|
|
14
|
+
* ```tsx
|
|
15
|
+
* let path!: SVGPathElement;
|
|
16
|
+
* const draw = createPathDraw(() => path, {
|
|
17
|
+
* duration: 1600,
|
|
18
|
+
* onDone: () => console.log("drawn"),
|
|
19
|
+
* });
|
|
20
|
+
* <svg viewBox="0 0 100 100">
|
|
21
|
+
* <path
|
|
22
|
+
* ref={path}
|
|
23
|
+
* d="M10 80 C 40 10, 60 10, 90 80"
|
|
24
|
+
* fill="none"
|
|
25
|
+
* stroke="currentColor"
|
|
26
|
+
* stroke-width="3"
|
|
27
|
+
* />
|
|
28
|
+
* </svg>
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
export function createPathDraw(ref, options = {}) {
|
|
32
|
+
const { duration = 1200, easing = easeInOutCubic, autoStart = true, onDone, } = options;
|
|
33
|
+
const [progress, setProgress] = createSignal(0);
|
|
34
|
+
const [running, setRunning] = createSignal(false);
|
|
35
|
+
if (typeof window === "undefined") {
|
|
36
|
+
const noop = () => { };
|
|
37
|
+
return { progress, running, start: noop, stop: noop, reset: noop };
|
|
38
|
+
}
|
|
39
|
+
let stopClock = null;
|
|
40
|
+
let doneFired = false;
|
|
41
|
+
const paint = (p) => {
|
|
42
|
+
const el = ref();
|
|
43
|
+
if (!el || typeof el.getTotalLength !== "function")
|
|
44
|
+
return;
|
|
45
|
+
const len = el.getTotalLength();
|
|
46
|
+
el.style.strokeDasharray = `${len}`;
|
|
47
|
+
el.style.strokeDashoffset = `${len * (1 - p)}`;
|
|
48
|
+
};
|
|
49
|
+
const finish = () => {
|
|
50
|
+
setProgress(1);
|
|
51
|
+
paint(1);
|
|
52
|
+
setRunning(false);
|
|
53
|
+
stopClock = null;
|
|
54
|
+
if (!doneFired) {
|
|
55
|
+
doneFired = true;
|
|
56
|
+
onDone?.();
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
const start = () => {
|
|
60
|
+
if (running())
|
|
61
|
+
return;
|
|
62
|
+
if (duration <= 0 || prefersReducedMotion()) {
|
|
63
|
+
setProgress(1);
|
|
64
|
+
finish();
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
doneFired = false;
|
|
68
|
+
setRunning(true);
|
|
69
|
+
const from = progress();
|
|
70
|
+
if (from >= 1) {
|
|
71
|
+
finish();
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
// Remaining time is proportional, so resume keeps a constant speed.
|
|
75
|
+
const span = Math.max(duration * (1 - from), 1);
|
|
76
|
+
const t0 = now();
|
|
77
|
+
paint(from);
|
|
78
|
+
stopClock?.();
|
|
79
|
+
stopClock = schedule((t) => {
|
|
80
|
+
const p = Math.min(Math.max((t - t0) / span, 0), 1);
|
|
81
|
+
const eased = from + (1 - from) * easing(p);
|
|
82
|
+
setProgress(eased);
|
|
83
|
+
paint(eased);
|
|
84
|
+
if (p >= 1) {
|
|
85
|
+
finish();
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
return true;
|
|
89
|
+
});
|
|
90
|
+
};
|
|
91
|
+
const stop = () => {
|
|
92
|
+
stopClock?.();
|
|
93
|
+
stopClock = null;
|
|
94
|
+
setRunning(false);
|
|
95
|
+
};
|
|
96
|
+
const reset = () => {
|
|
97
|
+
stop();
|
|
98
|
+
doneFired = false;
|
|
99
|
+
setProgress(0);
|
|
100
|
+
paint(0);
|
|
101
|
+
};
|
|
102
|
+
onCleanup(stop);
|
|
103
|
+
if (autoStart)
|
|
104
|
+
start();
|
|
105
|
+
return { progress, running, start, stop, reset };
|
|
106
|
+
}
|
package/dist/press.d.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { type Accessor } from "solid-js";
|
|
2
|
+
type MaybeElement = () => Element | null | undefined;
|
|
3
|
+
export interface PressOptions {
|
|
4
|
+
/** Fires whenever the pressed state changes. */
|
|
5
|
+
onChange?: (pressed: boolean) => void;
|
|
6
|
+
}
|
|
7
|
+
export interface PressControls {
|
|
8
|
+
/** True while the pointer (or keyboard) is pressing the element. */
|
|
9
|
+
pressed: Accessor<boolean>;
|
|
10
|
+
}
|
|
11
|
+
export interface HoverOptions {
|
|
12
|
+
/** Fires whenever the hovering state changes. */
|
|
13
|
+
onChange?: (hovering: boolean) => void;
|
|
14
|
+
}
|
|
15
|
+
export interface HoverControls {
|
|
16
|
+
/** True while the pointer is over the element (or it has focus). */
|
|
17
|
+
hovering: Accessor<boolean>;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Press gesture state. Tracks pointer down/up on the element (mouse,
|
|
21
|
+
* touch, and pen via pointer events) plus Enter/Space key presses for
|
|
22
|
+
* keyboard parity. There is no animation here: the host decides how to
|
|
23
|
+
* respond, typically by driving `createVariants`.
|
|
24
|
+
*
|
|
25
|
+
* SSR-safe: always unpressed on the server.
|
|
26
|
+
*
|
|
27
|
+
* ```tsx
|
|
28
|
+
* let btn!: HTMLButtonElement;
|
|
29
|
+
* const press = createPress(() => btn);
|
|
30
|
+
* <button
|
|
31
|
+
* ref={btn}
|
|
32
|
+
* style={{
|
|
33
|
+
* transform: press.pressed() ? "scale(0.96)" : "scale(1)",
|
|
34
|
+
* transition: "transform 120ms",
|
|
35
|
+
* }}
|
|
36
|
+
* >
|
|
37
|
+
* Hold me
|
|
38
|
+
* </button>
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
export declare function createPress(ref: MaybeElement, options?: PressOptions): PressControls;
|
|
42
|
+
/**
|
|
43
|
+
* Hover gesture state. Tracks pointer enter/leave plus focus/blur so
|
|
44
|
+
* keyboard users get the same state. Like `createPress` this is state
|
|
45
|
+
* only: pair it with `createVariants` (or plain styles) to animate.
|
|
46
|
+
*
|
|
47
|
+
* SSR-safe: never hovering on the server.
|
|
48
|
+
*
|
|
49
|
+
* ```tsx
|
|
50
|
+
* let card!: HTMLDivElement;
|
|
51
|
+
* const hover = createHover(() => card, {
|
|
52
|
+
* onChange: (h) => variants.go(h ? "hover" : "idle"),
|
|
53
|
+
* });
|
|
54
|
+
* ```
|
|
55
|
+
*/
|
|
56
|
+
export declare function createHover(ref: MaybeElement, options?: HoverOptions): HoverControls;
|
|
57
|
+
export {};
|
package/dist/press.js
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { createEffect, createSignal, onCleanup } from "solid-js";
|
|
2
|
+
const falsy = () => false;
|
|
3
|
+
/**
|
|
4
|
+
* Press gesture state. Tracks pointer down/up on the element (mouse,
|
|
5
|
+
* touch, and pen via pointer events) plus Enter/Space key presses for
|
|
6
|
+
* keyboard parity. There is no animation here: the host decides how to
|
|
7
|
+
* respond, typically by driving `createVariants`.
|
|
8
|
+
*
|
|
9
|
+
* SSR-safe: always unpressed on the server.
|
|
10
|
+
*
|
|
11
|
+
* ```tsx
|
|
12
|
+
* let btn!: HTMLButtonElement;
|
|
13
|
+
* const press = createPress(() => btn);
|
|
14
|
+
* <button
|
|
15
|
+
* ref={btn}
|
|
16
|
+
* style={{
|
|
17
|
+
* transform: press.pressed() ? "scale(0.96)" : "scale(1)",
|
|
18
|
+
* transition: "transform 120ms",
|
|
19
|
+
* }}
|
|
20
|
+
* >
|
|
21
|
+
* Hold me
|
|
22
|
+
* </button>
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export function createPress(ref, options = {}) {
|
|
26
|
+
const { onChange } = options;
|
|
27
|
+
if (typeof window === "undefined")
|
|
28
|
+
return { pressed: falsy };
|
|
29
|
+
const [pressed, setPressed] = createSignal(false);
|
|
30
|
+
const set = (value) => {
|
|
31
|
+
setPressed((prev) => {
|
|
32
|
+
if (prev !== value)
|
|
33
|
+
onChange?.(value);
|
|
34
|
+
return value;
|
|
35
|
+
});
|
|
36
|
+
};
|
|
37
|
+
createEffect(() => {
|
|
38
|
+
const el = ref();
|
|
39
|
+
if (!el)
|
|
40
|
+
return;
|
|
41
|
+
const down = () => set(true);
|
|
42
|
+
const up = () => set(false);
|
|
43
|
+
const onKeyDown = (event) => {
|
|
44
|
+
if (event.key === "Enter" || event.key === " ") {
|
|
45
|
+
if (!event.repeat)
|
|
46
|
+
set(true);
|
|
47
|
+
if (event.key === " ")
|
|
48
|
+
event.preventDefault?.();
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
const onKeyUp = (event) => {
|
|
52
|
+
if (event.key === "Enter" || event.key === " ")
|
|
53
|
+
set(false);
|
|
54
|
+
};
|
|
55
|
+
el.addEventListener("pointerdown", down);
|
|
56
|
+
el.addEventListener("pointerup", up);
|
|
57
|
+
el.addEventListener("pointercancel", up);
|
|
58
|
+
el.addEventListener("pointerleave", up);
|
|
59
|
+
el.addEventListener("keydown", onKeyDown);
|
|
60
|
+
el.addEventListener("keyup", onKeyUp);
|
|
61
|
+
onCleanup(() => {
|
|
62
|
+
el.removeEventListener("pointerdown", down);
|
|
63
|
+
el.removeEventListener("pointerup", up);
|
|
64
|
+
el.removeEventListener("pointercancel", up);
|
|
65
|
+
el.removeEventListener("pointerleave", up);
|
|
66
|
+
el.removeEventListener("keydown", onKeyDown);
|
|
67
|
+
el.removeEventListener("keyup", onKeyUp);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
return { pressed };
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Hover gesture state. Tracks pointer enter/leave plus focus/blur so
|
|
74
|
+
* keyboard users get the same state. Like `createPress` this is state
|
|
75
|
+
* only: pair it with `createVariants` (or plain styles) to animate.
|
|
76
|
+
*
|
|
77
|
+
* SSR-safe: never hovering on the server.
|
|
78
|
+
*
|
|
79
|
+
* ```tsx
|
|
80
|
+
* let card!: HTMLDivElement;
|
|
81
|
+
* const hover = createHover(() => card, {
|
|
82
|
+
* onChange: (h) => variants.go(h ? "hover" : "idle"),
|
|
83
|
+
* });
|
|
84
|
+
* ```
|
|
85
|
+
*/
|
|
86
|
+
export function createHover(ref, options = {}) {
|
|
87
|
+
const { onChange } = options;
|
|
88
|
+
if (typeof window === "undefined")
|
|
89
|
+
return { hovering: falsy };
|
|
90
|
+
const [hovering, setHovering] = createSignal(false);
|
|
91
|
+
const set = (value) => {
|
|
92
|
+
setHovering((prev) => {
|
|
93
|
+
if (prev !== value)
|
|
94
|
+
onChange?.(value);
|
|
95
|
+
return value;
|
|
96
|
+
});
|
|
97
|
+
};
|
|
98
|
+
createEffect(() => {
|
|
99
|
+
const el = ref();
|
|
100
|
+
if (!el)
|
|
101
|
+
return;
|
|
102
|
+
const enter = () => set(true);
|
|
103
|
+
const leave = () => set(false);
|
|
104
|
+
el.addEventListener("pointerenter", enter);
|
|
105
|
+
el.addEventListener("pointerleave", leave);
|
|
106
|
+
el.addEventListener("focus", enter);
|
|
107
|
+
el.addEventListener("blur", leave);
|
|
108
|
+
onCleanup(() => {
|
|
109
|
+
el.removeEventListener("pointerenter", enter);
|
|
110
|
+
el.removeEventListener("pointerleave", leave);
|
|
111
|
+
el.removeEventListener("focus", enter);
|
|
112
|
+
el.removeEventListener("blur", leave);
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
return { hovering };
|
|
116
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { type Accessor } from "solid-js";
|
|
2
|
+
import { type Easing } from "./easing.js";
|
|
3
|
+
/**
|
|
4
|
+
* One named animation state: CSS-ish property values. Numeric values
|
|
5
|
+
* interpolate; anything else snaps to the target when the transition
|
|
6
|
+
* finishes.
|
|
7
|
+
*/
|
|
8
|
+
export interface VariantDef {
|
|
9
|
+
[prop: string]: number | string;
|
|
10
|
+
}
|
|
11
|
+
export interface VariantsOptions {
|
|
12
|
+
/** Variant applied immediately on creation. */
|
|
13
|
+
initial?: string;
|
|
14
|
+
/** Transition length in ms. Default 250. */
|
|
15
|
+
duration?: number;
|
|
16
|
+
/** Easing for numeric interpolation. Default easeOutCubic. */
|
|
17
|
+
easing?: Easing;
|
|
18
|
+
}
|
|
19
|
+
export interface VariantsControls {
|
|
20
|
+
/** Name of the last variant passed to `go`. */
|
|
21
|
+
current: Accessor<string | undefined>;
|
|
22
|
+
/**
|
|
23
|
+
* Current interpolated values. Bind these straight into styles:
|
|
24
|
+
* `style={{ transform: `scale(${v.values().scale})` }}`.
|
|
25
|
+
*/
|
|
26
|
+
values: Accessor<Record<string, number | string>>;
|
|
27
|
+
/** Transition to the named variant. Unknown names are ignored. */
|
|
28
|
+
go: (name: string) => void;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Named animation states with tweened transitions, in the vocabulary of
|
|
32
|
+
* "variants". `go(name)` interpolates every numeric property from the
|
|
33
|
+
* current values to the target variant on the shared clock; non-numeric
|
|
34
|
+
* properties (colors, keywords) snap at the end of the transition.
|
|
35
|
+
*
|
|
36
|
+
* Pairs with `createHover` / `createPress`: map gesture states to variant
|
|
37
|
+
* names and the element animates between them.
|
|
38
|
+
*
|
|
39
|
+
* Under reduced motion (and on the server) transitions snap instantly.
|
|
40
|
+
* SSR-safe.
|
|
41
|
+
*
|
|
42
|
+
* ```tsx
|
|
43
|
+
* const card = createVariants(
|
|
44
|
+
* {
|
|
45
|
+
* idle: { scale: 1, shadow: 0 },
|
|
46
|
+
* hover: { scale: 1.04, shadow: 12 },
|
|
47
|
+
* press: { scale: 0.96, shadow: 4 },
|
|
48
|
+
* },
|
|
49
|
+
* { initial: "idle", duration: 180 },
|
|
50
|
+
* );
|
|
51
|
+
* createHover(() => el, {
|
|
52
|
+
* onChange: (h) => card.go(h ? "hover" : "idle"),
|
|
53
|
+
* });
|
|
54
|
+
* <div
|
|
55
|
+
* ref={el}
|
|
56
|
+
* style={{
|
|
57
|
+
* transform: `scale(${card.values().scale})`,
|
|
58
|
+
* "box-shadow": `0 ${card.values().shadow}px 24px rgb(0 0 0 / 0.12)`,
|
|
59
|
+
* }}
|
|
60
|
+
* />
|
|
61
|
+
* ```
|
|
62
|
+
*/
|
|
63
|
+
export declare function createVariants(variants: Record<string, VariantDef>, options?: VariantsOptions): VariantsControls;
|
package/dist/variants.js
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { createSignal, onCleanup, untrack } from "solid-js";
|
|
2
|
+
import { now, schedule } from "./engine.js";
|
|
3
|
+
import { easeOutCubic } from "./easing.js";
|
|
4
|
+
import { prefersReducedMotion } from "./reduced-motion.js";
|
|
5
|
+
/**
|
|
6
|
+
* Named animation states with tweened transitions, in the vocabulary of
|
|
7
|
+
* "variants". `go(name)` interpolates every numeric property from the
|
|
8
|
+
* current values to the target variant on the shared clock; non-numeric
|
|
9
|
+
* properties (colors, keywords) snap at the end of the transition.
|
|
10
|
+
*
|
|
11
|
+
* Pairs with `createHover` / `createPress`: map gesture states to variant
|
|
12
|
+
* names and the element animates between them.
|
|
13
|
+
*
|
|
14
|
+
* Under reduced motion (and on the server) transitions snap instantly.
|
|
15
|
+
* SSR-safe.
|
|
16
|
+
*
|
|
17
|
+
* ```tsx
|
|
18
|
+
* const card = createVariants(
|
|
19
|
+
* {
|
|
20
|
+
* idle: { scale: 1, shadow: 0 },
|
|
21
|
+
* hover: { scale: 1.04, shadow: 12 },
|
|
22
|
+
* press: { scale: 0.96, shadow: 4 },
|
|
23
|
+
* },
|
|
24
|
+
* { initial: "idle", duration: 180 },
|
|
25
|
+
* );
|
|
26
|
+
* createHover(() => el, {
|
|
27
|
+
* onChange: (h) => card.go(h ? "hover" : "idle"),
|
|
28
|
+
* });
|
|
29
|
+
* <div
|
|
30
|
+
* ref={el}
|
|
31
|
+
* style={{
|
|
32
|
+
* transform: `scale(${card.values().scale})`,
|
|
33
|
+
* "box-shadow": `0 ${card.values().shadow}px 24px rgb(0 0 0 / 0.12)`,
|
|
34
|
+
* }}
|
|
35
|
+
* />
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
export function createVariants(variants, options = {}) {
|
|
39
|
+
const { initial, duration = 250, easing = easeOutCubic } = options;
|
|
40
|
+
const first = initial ?? Object.keys(variants)[0];
|
|
41
|
+
const [current, setCurrent] = createSignal(first);
|
|
42
|
+
const [values, setValues] = createSignal({
|
|
43
|
+
...(first ? variants[first] : {}),
|
|
44
|
+
});
|
|
45
|
+
if (typeof window === "undefined") {
|
|
46
|
+
return { current, values, go: () => { } };
|
|
47
|
+
}
|
|
48
|
+
let stopClock = null;
|
|
49
|
+
const go = (name) => {
|
|
50
|
+
const target = variants[name];
|
|
51
|
+
if (!target)
|
|
52
|
+
return;
|
|
53
|
+
setCurrent(name);
|
|
54
|
+
stopClock?.();
|
|
55
|
+
stopClock = null;
|
|
56
|
+
const from = untrack(values);
|
|
57
|
+
if (duration <= 0 || prefersReducedMotion()) {
|
|
58
|
+
setValues({ ...target });
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
const start = now();
|
|
62
|
+
const numeric = Object.keys(target).filter((k) => typeof target[k] === "number" && typeof from[k] === "number");
|
|
63
|
+
stopClock = schedule((t) => {
|
|
64
|
+
const p = Math.min(Math.max((t - start) / duration, 0), 1);
|
|
65
|
+
const e = easing(p);
|
|
66
|
+
const next = { ...untrack(values) };
|
|
67
|
+
for (const k of numeric) {
|
|
68
|
+
const a = from[k];
|
|
69
|
+
const b = target[k];
|
|
70
|
+
next[k] = a + (b - a) * e;
|
|
71
|
+
}
|
|
72
|
+
if (p >= 1) {
|
|
73
|
+
// Snap non-numeric props (and exact numeric targets) at the end.
|
|
74
|
+
for (const k of Object.keys(target))
|
|
75
|
+
next[k] = target[k];
|
|
76
|
+
setValues(next);
|
|
77
|
+
stopClock = null;
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
setValues(next);
|
|
81
|
+
return true;
|
|
82
|
+
});
|
|
83
|
+
};
|
|
84
|
+
onCleanup(() => stopClock?.());
|
|
85
|
+
return { current, values, go };
|
|
86
|
+
}
|
package/package.json
CHANGED