solid-drift 0.11.0 → 0.13.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 +52 -1
- package/dist/engine.d.ts +9 -1
- package/dist/engine.js +60 -5
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/power.d.ts +44 -0
- package/dist/power.js +83 -0
- package/dist/toast.d.ts +81 -0
- package/dist/toast.js +167 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1094,13 +1094,64 @@ const connect = createConnectButton(() => btn, { strength: 0.35 });
|
|
|
1094
1094
|
|
|
1095
1095
|
Returns `{ copyTick, chainPulse, status }`. `status()` is `"idle"`, `"ticking"` (check visible), or `"pulsing"` (ring expanding). Call `chainPulse()` after a successful connection or network switch. Under reduced motion there is no magnetic pull or scale; `copyTick()` and `chainPulse()` still show their overlays statically.
|
|
1096
1096
|
|
|
1097
|
+
### `createToast(options?)`
|
|
1098
|
+
|
|
1099
|
+
A signal-native toast queue with choreographed lifecycle. The primitive owns timing and state; you own the rendering, so no component opinions leak into your design system. Each toast moves through `"entering"` to `"visible"` to `"leaving"` to removed on the shared animation clock: bind `state` to CSS classes or drift values for enter/exit motion without any timers of your own.
|
|
1100
|
+
|
|
1101
|
+
```tsx
|
|
1102
|
+
import { createToast } from "solid-drift"
|
|
1103
|
+
|
|
1104
|
+
const { toasts, success, dismiss } = createToast()
|
|
1105
|
+
success("Payment sent", { description: "0.5 SOL to alice.sol" })
|
|
1106
|
+
|
|
1107
|
+
<For each={toasts()}>
|
|
1108
|
+
{(t) => (
|
|
1109
|
+
<div
|
|
1110
|
+
class="toast"
|
|
1111
|
+
classList={{
|
|
1112
|
+
"toast-enter": t.state === "entering",
|
|
1113
|
+
"toast-leave": t.state === "leaving",
|
|
1114
|
+
}}
|
|
1115
|
+
>
|
|
1116
|
+
<strong>{t.title}</strong>
|
|
1117
|
+
{t.description && <p>{t.description}</p>}
|
|
1118
|
+
<button onClick={() => dismiss(t.id)}>Dismiss</button>
|
|
1119
|
+
</div>
|
|
1120
|
+
)}
|
|
1121
|
+
</For>
|
|
1122
|
+
```
|
|
1123
|
+
|
|
1124
|
+
| Option | Default | Description |
|
|
1125
|
+
| ---------- | ------- | ---------------------------------------------------------------- |
|
|
1126
|
+
| `max` | `5` | Max toasts in the queue; older ones are dismissed first |
|
|
1127
|
+
| `enterMs` | `250` | Enter transition time in milliseconds |
|
|
1128
|
+
| `leaveMs` | `200` | Leave transition time in milliseconds |
|
|
1129
|
+
| `duration` | `4000` | Default auto-dismiss time in milliseconds |
|
|
1130
|
+
|
|
1131
|
+
Push helpers: `toast(title, options?)`, `info(...)`, `success(...)`, `warning(...)`, `error(...)`. Each returns the toast id. Per-toast options: `kind`, `description`, `duration` (ms; `0` means sticky). `dismiss(id)` starts the leave transition for one toast; `clear()` dismisses all. SSR-safe: toasts pushed on the server start `"visible"`. Under reduced motion the enter and leave transitions are instant, but auto-dismiss timing still applies.
|
|
1132
|
+
|
|
1133
|
+
### `useLowPowerMode(options?)`
|
|
1134
|
+
|
|
1135
|
+
One reactive signal for mobile-first degradation. It combines the OS `prefers-reduced-motion` and `prefers-reduced-data` media queries with low-end device signals (`navigator.deviceMemory`, `navigator.hardwareConcurrency`), so a single check covers user preference, network thrift, and weak hardware. The media queries update live; the device signals are sampled once. There is also a one-shot `isLowPowerMode(options?)` for non-reactive checks.
|
|
1136
|
+
|
|
1137
|
+
```tsx
|
|
1138
|
+
import { useLowPowerMode } from "solid-drift"
|
|
1139
|
+
|
|
1140
|
+
const lowPower = useLowPowerMode()
|
|
1141
|
+
// Degrade gracefully: shorter, cheaper motion on weak devices.
|
|
1142
|
+
const duration = () => (lowPower() ? 0 : 400)
|
|
1143
|
+
const confettiCount = () => (lowPower() ? 20 : 150)
|
|
1144
|
+
```
|
|
1145
|
+
|
|
1146
|
+
Options: `maxDeviceMemory` (GB, default `4`), `maxHardwareConcurrency` (default `4`): a device at or below either threshold counts as low-end. Where the device signals are unsupported they degrade to "not low-end". SSR-safe: always `false` on the server.
|
|
1147
|
+
|
|
1097
1148
|
### Easings
|
|
1098
1149
|
|
|
1099
1150
|
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.
|
|
1100
1151
|
|
|
1101
1152
|
## How it works
|
|
1102
1153
|
|
|
1103
|
-
One shared `requestAnimationFrame` loop drives every animation in the app, so hundreds of springs cost a single rAF tick per frame. Springs integrate with semi-implicit Euler, tweens sample an easing curve. Everything is SSR-safe (animations simply don't run on the server).
|
|
1154
|
+
One shared `requestAnimationFrame` loop drives every animation in the app, so hundreds of springs cost a single rAF tick per frame. Springs integrate with semi-implicit Euler, tweens sample an easing curve. When the tab becomes hidden the engine pauses the loop and freezes its clock, so nothing burns battery in the background; on return the clock continues where it left off and in-flight animations resume seamlessly. Everything is SSR-safe (animations simply don't run on the server).
|
|
1104
1155
|
|
|
1105
1156
|
## License
|
|
1106
1157
|
|
package/dist/engine.d.ts
CHANGED
|
@@ -4,6 +4,11 @@
|
|
|
4
4
|
* A single requestAnimationFrame loop drives every active animation in the
|
|
5
5
|
* app, so hundreds of springs and tweens cost exactly one rAF tick per frame.
|
|
6
6
|
* Tasks return `false` when finished and are removed automatically.
|
|
7
|
+
*
|
|
8
|
+
* Battery saving: when the tab becomes hidden the loop stops and the clock
|
|
9
|
+
* freezes, so no animation burns CPU in the background. When the tab is
|
|
10
|
+
* visible again the loop resumes and the clock continues where it left off,
|
|
11
|
+
* so in-flight animations continue seamlessly instead of jumping forward.
|
|
7
12
|
*/
|
|
8
13
|
export type AnimationTask = (now: number) => boolean;
|
|
9
14
|
/**
|
|
@@ -11,5 +16,8 @@ export type AnimationTask = (now: number) => boolean;
|
|
|
11
16
|
* Safe to call during SSR (no-op without requestAnimationFrame).
|
|
12
17
|
*/
|
|
13
18
|
export declare function schedule(task: AnimationTask): () => void;
|
|
14
|
-
/**
|
|
19
|
+
/**
|
|
20
|
+
* Monotonic clock in milliseconds. Freezes while the tab is hidden and
|
|
21
|
+
* resumes where it left off, so animations never observe the hidden gap.
|
|
22
|
+
*/
|
|
15
23
|
export declare function now(): number;
|
package/dist/engine.js
CHANGED
|
@@ -4,14 +4,31 @@
|
|
|
4
4
|
* A single requestAnimationFrame loop drives every active animation in the
|
|
5
5
|
* app, so hundreds of springs and tweens cost exactly one rAF tick per frame.
|
|
6
6
|
* Tasks return `false` when finished and are removed automatically.
|
|
7
|
+
*
|
|
8
|
+
* Battery saving: when the tab becomes hidden the loop stops and the clock
|
|
9
|
+
* freezes, so no animation burns CPU in the background. When the tab is
|
|
10
|
+
* visible again the loop resumes and the clock continues where it left off,
|
|
11
|
+
* so in-flight animations continue seamlessly instead of jumping forward.
|
|
7
12
|
*/
|
|
8
13
|
const tasks = new Set();
|
|
9
14
|
let rafId = 0;
|
|
10
|
-
|
|
15
|
+
// Milliseconds of hidden time subtracted from the raw clock, so now()
|
|
16
|
+
// freezes while the tab is hidden and resumes seamlessly on return.
|
|
17
|
+
let pausedMs = 0;
|
|
18
|
+
// Raw timestamp of the moment the tab was hidden, or null while visible.
|
|
19
|
+
let hiddenAt = null;
|
|
20
|
+
let visibilityHooked = false;
|
|
21
|
+
function rawNow() {
|
|
22
|
+
return typeof performance !== "undefined" ? performance.now() : Date.now();
|
|
23
|
+
}
|
|
24
|
+
function tick() {
|
|
25
|
+
// Tasks always see the pause-adjusted clock, never the raw rAF stamp,
|
|
26
|
+
// so a hidden-then-visible gap never shows up as a time jump.
|
|
27
|
+
const t = now();
|
|
11
28
|
for (const task of tasks) {
|
|
12
29
|
let alive = false;
|
|
13
30
|
try {
|
|
14
|
-
alive = task(
|
|
31
|
+
alive = task(t);
|
|
15
32
|
}
|
|
16
33
|
catch {
|
|
17
34
|
alive = false;
|
|
@@ -21,6 +38,39 @@ function tick(now) {
|
|
|
21
38
|
}
|
|
22
39
|
rafId = tasks.size > 0 ? requestAnimationFrame(tick) : 0;
|
|
23
40
|
}
|
|
41
|
+
function onVisibilityChange() {
|
|
42
|
+
if (typeof document === "undefined")
|
|
43
|
+
return;
|
|
44
|
+
if (document.hidden) {
|
|
45
|
+
if (hiddenAt === null) {
|
|
46
|
+
hiddenAt = rawNow();
|
|
47
|
+
if (rafId) {
|
|
48
|
+
if (typeof cancelAnimationFrame !== "undefined") {
|
|
49
|
+
cancelAnimationFrame(rafId);
|
|
50
|
+
}
|
|
51
|
+
rafId = 0;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
else if (hiddenAt !== null) {
|
|
56
|
+
pausedMs += rawNow() - hiddenAt;
|
|
57
|
+
hiddenAt = null;
|
|
58
|
+
if (tasks.size > 0 &&
|
|
59
|
+
!rafId &&
|
|
60
|
+
typeof requestAnimationFrame !== "undefined") {
|
|
61
|
+
rafId = requestAnimationFrame(tick);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
function hookVisibility() {
|
|
66
|
+
if (visibilityHooked)
|
|
67
|
+
return;
|
|
68
|
+
visibilityHooked = true;
|
|
69
|
+
if (typeof document !== "undefined" &&
|
|
70
|
+
typeof document.addEventListener === "function") {
|
|
71
|
+
document.addEventListener("visibilitychange", onVisibilityChange);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
24
74
|
/**
|
|
25
75
|
* Register a task on the shared clock. Returns a cancel function.
|
|
26
76
|
* Safe to call during SSR (no-op without requestAnimationFrame).
|
|
@@ -28,14 +78,19 @@ function tick(now) {
|
|
|
28
78
|
export function schedule(task) {
|
|
29
79
|
if (typeof requestAnimationFrame === "undefined")
|
|
30
80
|
return () => { };
|
|
81
|
+
hookVisibility();
|
|
31
82
|
tasks.add(task);
|
|
32
|
-
|
|
83
|
+
const hidden = typeof document !== "undefined" && document.hidden === true;
|
|
84
|
+
if (!rafId && !hidden)
|
|
33
85
|
rafId = requestAnimationFrame(tick);
|
|
34
86
|
return () => {
|
|
35
87
|
tasks.delete(task);
|
|
36
88
|
};
|
|
37
89
|
}
|
|
38
|
-
/**
|
|
90
|
+
/**
|
|
91
|
+
* Monotonic clock in milliseconds. Freezes while the tab is hidden and
|
|
92
|
+
* resumes where it left off, so animations never observe the hidden gap.
|
|
93
|
+
*/
|
|
39
94
|
export function now() {
|
|
40
|
-
return
|
|
95
|
+
return (hiddenAt ?? rawNow()) - pausedMs;
|
|
41
96
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -11,6 +11,7 @@ export { drift, type DriftProps } from "./directive.js";
|
|
|
11
11
|
export { createScrollProgress, type ScrollTarget } from "./scroll.js";
|
|
12
12
|
export { createInView, type InViewOptions } from "./inview.js";
|
|
13
13
|
export { usePrefersReducedMotion, prefersReducedMotion, } from "./reduced-motion.js";
|
|
14
|
+
export { isLowPowerMode, useLowPowerMode, type LowPowerOptions, } from "./power.js";
|
|
14
15
|
export { createStagger } from "./stagger.js";
|
|
15
16
|
export { createHorizontalScroll, type HorizontalScrollOptions, type HorizontalScrollResult, } from "./horizontal.js";
|
|
16
17
|
export { createScrub, type ScrubKeyframe, type ScrubOptions } from "./scrub.js";
|
|
@@ -20,6 +21,7 @@ export { createMagnetic, type MagneticOptions, type MagneticResult, createTilt,
|
|
|
20
21
|
export { createTrail, type TrailOptions } from "./trail.js";
|
|
21
22
|
export { createTimeline, type TimelineStep, type TimelineStatus, type TimelineControls, } from "./timeline.js";
|
|
22
23
|
export { animateFlip, type FlipOptions, createSharedLayout, type SharedLayoutOptions, type SharedLayoutResult, } from "./flip.js";
|
|
24
|
+
export { createToast, type Toast, type ToastControls, type ToastKind, type ToastOptions, type ToastQueueOptions, type ToastState, } from "./toast.js";
|
|
23
25
|
export { createSquashStretch, type SquashStretchOptions, type SquashStretchResult, createFollowThrough, type FollowThroughOptions, createAnticipation, type AnticipationOptions, createWobble, type WobbleOptions, type WobbleResult, } from "./cartoon.js";
|
|
24
26
|
export { createGravity, type GravityOptions, type GravityResult, createPendulum, type PendulumOptions, type PendulumResult, createFling, type FlingOptions, type FlingResult, } from "./physics.js";
|
|
25
27
|
export { createFontSwap, type FontSwapOptions, type FontSwapResult, createTyping, type TypingOptions, type TypingResult, createTextPhysics, type TextPhysicsOptions, type TextPhysicsResult, createTextTunnel, type TextTunnelOptions, type TextTunnelResult, createTextCutout, type TextCutoutOptions, createTextGradient, type TextGradientOptions, createTextScramble, type TextScrambleOptions, type TextScrambleResult, createTextWave, type TextWaveOptions, createCountUp, type CountUpOptions, } from "./typography.js";
|
package/dist/index.js
CHANGED
|
@@ -11,6 +11,7 @@ export { drift } from "./directive.js";
|
|
|
11
11
|
export { createScrollProgress } from "./scroll.js";
|
|
12
12
|
export { createInView } from "./inview.js";
|
|
13
13
|
export { usePrefersReducedMotion, prefersReducedMotion, } from "./reduced-motion.js";
|
|
14
|
+
export { isLowPowerMode, useLowPowerMode, } from "./power.js";
|
|
14
15
|
export { createStagger } from "./stagger.js";
|
|
15
16
|
export { createHorizontalScroll, } from "./horizontal.js";
|
|
16
17
|
export { createScrub } from "./scrub.js";
|
|
@@ -20,6 +21,7 @@ export { createMagnetic, createTilt, } from "./pointer.js";
|
|
|
20
21
|
export { createTrail } from "./trail.js";
|
|
21
22
|
export { createTimeline, } from "./timeline.js";
|
|
22
23
|
export { animateFlip, createSharedLayout, } from "./flip.js";
|
|
24
|
+
export { createToast, } from "./toast.js";
|
|
23
25
|
export { createSquashStretch, createFollowThrough, createAnticipation, createWobble, } from "./cartoon.js";
|
|
24
26
|
export { createGravity, createPendulum, createFling, } from "./physics.js";
|
|
25
27
|
export { createFontSwap, createTyping, createTextPhysics, createTextTunnel, createTextCutout, createTextGradient, createTextScramble, createTextWave, createCountUp, } from "./typography.js";
|
package/dist/power.d.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { type Accessor } from "solid-js";
|
|
2
|
+
export interface LowPowerOptions {
|
|
3
|
+
/** deviceMemory in GB at or below which the device counts as low-end. Default 4. */
|
|
4
|
+
maxDeviceMemory?: number;
|
|
5
|
+
/** hardwareConcurrency at or below which the device counts as low-end. Default 4. */
|
|
6
|
+
maxHardwareConcurrency?: number;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Non-reactive check: should the app conserve power and motion right now?
|
|
10
|
+
*
|
|
11
|
+
* True when any of these hold: the OS prefers reduced motion, the user
|
|
12
|
+
* asked to reduce data usage, or the device looks low-end (small
|
|
13
|
+
* deviceMemory or few CPU cores, both Chrome-only signals that degrade
|
|
14
|
+
* to "not low-end" where unsupported).
|
|
15
|
+
*
|
|
16
|
+
* SSR-safe: always `false` on the server. Pair with the reduced-motion
|
|
17
|
+
* behavior of the primitives: when this is true, prefer shorter or
|
|
18
|
+
* zero durations and skip decorative motion.
|
|
19
|
+
*
|
|
20
|
+
* ```ts
|
|
21
|
+
* if (isLowPowerMode()) {
|
|
22
|
+
* // skip the ambient background animation
|
|
23
|
+
* }
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
export declare function isLowPowerMode(options?: LowPowerOptions): boolean;
|
|
27
|
+
/**
|
|
28
|
+
* Reactive low-power signal for mobile-first degradation.
|
|
29
|
+
*
|
|
30
|
+
* Combines `prefers-reduced-motion`, `prefers-reduced-data`, and low-end
|
|
31
|
+
* device signals into one accessor. The media queries update live if the
|
|
32
|
+
* OS preference changes while the app is running; the device signals are
|
|
33
|
+
* sampled once. SSR-safe: `false` on the server.
|
|
34
|
+
*
|
|
35
|
+
* ```tsx
|
|
36
|
+
* import { isLowPowerMode, useLowPowerMode } from "solid-drift"
|
|
37
|
+
*
|
|
38
|
+
* const lowPower = useLowPowerMode()
|
|
39
|
+
* // Degrade gracefully: shorter, cheaper motion on weak devices.
|
|
40
|
+
* const duration = () => (lowPower() ? 0 : 400)
|
|
41
|
+
* const confettiCount = () => (lowPower() ? 20 : 150)
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
export declare function useLowPowerMode(options?: LowPowerOptions): Accessor<boolean>;
|
package/dist/power.js
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { createSignal, onCleanup } from "solid-js";
|
|
2
|
+
const MOTION_QUERY = "(prefers-reduced-motion: reduce)";
|
|
3
|
+
const DATA_QUERY = "(prefers-reduced-data: reduce)";
|
|
4
|
+
function mediaMatches(query) {
|
|
5
|
+
return (typeof window !== "undefined" &&
|
|
6
|
+
typeof window.matchMedia === "function" &&
|
|
7
|
+
window.matchMedia(query).matches);
|
|
8
|
+
}
|
|
9
|
+
function isLowEndDevice(options) {
|
|
10
|
+
if (typeof navigator === "undefined")
|
|
11
|
+
return false;
|
|
12
|
+
const { maxDeviceMemory = 4, maxHardwareConcurrency = 4 } = options;
|
|
13
|
+
const memory = typeof navigator.deviceMemory ===
|
|
14
|
+
"number"
|
|
15
|
+
? navigator.deviceMemory
|
|
16
|
+
: Infinity;
|
|
17
|
+
const cores = typeof navigator.hardwareConcurrency === "number"
|
|
18
|
+
? navigator.hardwareConcurrency
|
|
19
|
+
: Infinity;
|
|
20
|
+
return memory <= maxDeviceMemory || cores <= maxHardwareConcurrency;
|
|
21
|
+
}
|
|
22
|
+
function checkLowPower(options) {
|
|
23
|
+
return (mediaMatches(MOTION_QUERY) ||
|
|
24
|
+
mediaMatches(DATA_QUERY) ||
|
|
25
|
+
isLowEndDevice(options));
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Non-reactive check: should the app conserve power and motion right now?
|
|
29
|
+
*
|
|
30
|
+
* True when any of these hold: the OS prefers reduced motion, the user
|
|
31
|
+
* asked to reduce data usage, or the device looks low-end (small
|
|
32
|
+
* deviceMemory or few CPU cores, both Chrome-only signals that degrade
|
|
33
|
+
* to "not low-end" where unsupported).
|
|
34
|
+
*
|
|
35
|
+
* SSR-safe: always `false` on the server. Pair with the reduced-motion
|
|
36
|
+
* behavior of the primitives: when this is true, prefer shorter or
|
|
37
|
+
* zero durations and skip decorative motion.
|
|
38
|
+
*
|
|
39
|
+
* ```ts
|
|
40
|
+
* if (isLowPowerMode()) {
|
|
41
|
+
* // skip the ambient background animation
|
|
42
|
+
* }
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
45
|
+
export function isLowPowerMode(options = {}) {
|
|
46
|
+
return checkLowPower(options);
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Reactive low-power signal for mobile-first degradation.
|
|
50
|
+
*
|
|
51
|
+
* Combines `prefers-reduced-motion`, `prefers-reduced-data`, and low-end
|
|
52
|
+
* device signals into one accessor. The media queries update live if the
|
|
53
|
+
* OS preference changes while the app is running; the device signals are
|
|
54
|
+
* sampled once. SSR-safe: `false` on the server.
|
|
55
|
+
*
|
|
56
|
+
* ```tsx
|
|
57
|
+
* import { isLowPowerMode, useLowPowerMode } from "solid-drift"
|
|
58
|
+
*
|
|
59
|
+
* const lowPower = useLowPowerMode()
|
|
60
|
+
* // Degrade gracefully: shorter, cheaper motion on weak devices.
|
|
61
|
+
* const duration = () => (lowPower() ? 0 : 400)
|
|
62
|
+
* const confettiCount = () => (lowPower() ? 20 : 150)
|
|
63
|
+
* ```
|
|
64
|
+
*/
|
|
65
|
+
export function useLowPowerMode(options = {}) {
|
|
66
|
+
const lowEnd = isLowEndDevice(options);
|
|
67
|
+
const [lowPower, setLowPower] = createSignal(checkLowPower(options));
|
|
68
|
+
if (typeof window !== "undefined" &&
|
|
69
|
+
typeof window.matchMedia === "function") {
|
|
70
|
+
const motionMq = window.matchMedia(MOTION_QUERY);
|
|
71
|
+
const dataMq = window.matchMedia(DATA_QUERY);
|
|
72
|
+
const onChange = () => {
|
|
73
|
+
setLowPower(lowEnd || motionMq.matches || dataMq.matches);
|
|
74
|
+
};
|
|
75
|
+
motionMq.addEventListener("change", onChange);
|
|
76
|
+
dataMq.addEventListener("change", onChange);
|
|
77
|
+
onCleanup(() => {
|
|
78
|
+
motionMq.removeEventListener("change", onChange);
|
|
79
|
+
dataMq.removeEventListener("change", onChange);
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
return lowPower;
|
|
83
|
+
}
|
package/dist/toast.d.ts
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { type Accessor } from "solid-js";
|
|
2
|
+
export type ToastKind = "info" | "success" | "warning" | "error";
|
|
3
|
+
/** Lifecycle state of a toast. Bind it to your enter/exit animation. */
|
|
4
|
+
export type ToastState = "entering" | "visible" | "leaving";
|
|
5
|
+
export interface Toast {
|
|
6
|
+
/** Unique id, returned by the push helpers. */
|
|
7
|
+
id: number;
|
|
8
|
+
kind: ToastKind;
|
|
9
|
+
title: string;
|
|
10
|
+
description?: string;
|
|
11
|
+
state: ToastState;
|
|
12
|
+
/** Millisecond timestamp when the toast entered the queue. */
|
|
13
|
+
createdAt: number;
|
|
14
|
+
}
|
|
15
|
+
export interface ToastOptions {
|
|
16
|
+
/** Visual kind. Default "info" (or the shortcut you called). */
|
|
17
|
+
kind?: ToastKind;
|
|
18
|
+
description?: string;
|
|
19
|
+
/** Auto-dismiss after this many ms. Default 4000. 0 means sticky. */
|
|
20
|
+
duration?: number;
|
|
21
|
+
}
|
|
22
|
+
export interface ToastQueueOptions {
|
|
23
|
+
/** Max toasts in the queue; older ones are dismissed first. Default 5. */
|
|
24
|
+
max?: number;
|
|
25
|
+
/** Enter transition time in ms. Default 250. */
|
|
26
|
+
enterMs?: number;
|
|
27
|
+
/** Leave transition time in ms. Default 200. */
|
|
28
|
+
leaveMs?: number;
|
|
29
|
+
/** Default auto-dismiss time in ms. Default 4000. */
|
|
30
|
+
duration?: number;
|
|
31
|
+
}
|
|
32
|
+
export interface ToastControls {
|
|
33
|
+
/** Reactive list of toasts, oldest first. */
|
|
34
|
+
toasts: Accessor<Toast[]>;
|
|
35
|
+
/** Push a toast. Returns its id. */
|
|
36
|
+
toast: (title: string, options?: ToastOptions) => number;
|
|
37
|
+
info: (title: string, options?: ToastOptions) => number;
|
|
38
|
+
success: (title: string, options?: ToastOptions) => number;
|
|
39
|
+
warning: (title: string, options?: ToastOptions) => number;
|
|
40
|
+
error: (title: string, options?: ToastOptions) => number;
|
|
41
|
+
/** Start the leave transition for one toast. */
|
|
42
|
+
dismiss: (id: number) => void;
|
|
43
|
+
/** Dismiss every toast. */
|
|
44
|
+
clear: () => void;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* A signal-native toast queue with choreographed lifecycle.
|
|
48
|
+
*
|
|
49
|
+
* The primitive owns timing and state; you own the rendering. Each toast
|
|
50
|
+
* moves through `entering` -> `visible` -> `leaving` -> removed on the
|
|
51
|
+
* shared animation clock, so you can bind `state` to CSS classes or drift
|
|
52
|
+
* values for enter/exit motion without any timers of your own.
|
|
53
|
+
*
|
|
54
|
+
* SSR-safe: toasts pushed on the server start `visible`. Under reduced
|
|
55
|
+
* motion the enter and leave transitions are instant, but auto-dismiss
|
|
56
|
+
* timing still applies.
|
|
57
|
+
*
|
|
58
|
+
* ```tsx
|
|
59
|
+
* import { createToast } from "solid-drift"
|
|
60
|
+
*
|
|
61
|
+
* const { toasts, success, dismiss } = createToast()
|
|
62
|
+
* success("Payment sent", { description: "0.5 SOL to alice.sol" })
|
|
63
|
+
*
|
|
64
|
+
* <For each={toasts()}>
|
|
65
|
+
* {(t) => (
|
|
66
|
+
* <div
|
|
67
|
+
* class="toast"
|
|
68
|
+
* classList={{
|
|
69
|
+
* "toast-enter": t.state === "entering",
|
|
70
|
+
* "toast-leave": t.state === "leaving",
|
|
71
|
+
* }}
|
|
72
|
+
* >
|
|
73
|
+
* <strong>{t.title}</strong>
|
|
74
|
+
* {t.description && <p>{t.description}</p>}
|
|
75
|
+
* <button onClick={() => dismiss(t.id)}>Dismiss</button>
|
|
76
|
+
* </div>
|
|
77
|
+
* )}
|
|
78
|
+
* </For>
|
|
79
|
+
* ```
|
|
80
|
+
*/
|
|
81
|
+
export declare function createToast(options?: ToastQueueOptions): ToastControls;
|
package/dist/toast.js
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { createSignal, onCleanup, untrack } from "solid-js";
|
|
2
|
+
import { now, schedule } from "./engine.js";
|
|
3
|
+
import { prefersReducedMotion } from "./reduced-motion.js";
|
|
4
|
+
/**
|
|
5
|
+
* A signal-native toast queue with choreographed lifecycle.
|
|
6
|
+
*
|
|
7
|
+
* The primitive owns timing and state; you own the rendering. Each toast
|
|
8
|
+
* moves through `entering` -> `visible` -> `leaving` -> removed on the
|
|
9
|
+
* shared animation clock, so you can bind `state` to CSS classes or drift
|
|
10
|
+
* values for enter/exit motion without any timers of your own.
|
|
11
|
+
*
|
|
12
|
+
* SSR-safe: toasts pushed on the server start `visible`. Under reduced
|
|
13
|
+
* motion the enter and leave transitions are instant, but auto-dismiss
|
|
14
|
+
* timing still applies.
|
|
15
|
+
*
|
|
16
|
+
* ```tsx
|
|
17
|
+
* import { createToast } from "solid-drift"
|
|
18
|
+
*
|
|
19
|
+
* const { toasts, success, dismiss } = createToast()
|
|
20
|
+
* success("Payment sent", { description: "0.5 SOL to alice.sol" })
|
|
21
|
+
*
|
|
22
|
+
* <For each={toasts()}>
|
|
23
|
+
* {(t) => (
|
|
24
|
+
* <div
|
|
25
|
+
* class="toast"
|
|
26
|
+
* classList={{
|
|
27
|
+
* "toast-enter": t.state === "entering",
|
|
28
|
+
* "toast-leave": t.state === "leaving",
|
|
29
|
+
* }}
|
|
30
|
+
* >
|
|
31
|
+
* <strong>{t.title}</strong>
|
|
32
|
+
* {t.description && <p>{t.description}</p>}
|
|
33
|
+
* <button onClick={() => dismiss(t.id)}>Dismiss</button>
|
|
34
|
+
* </div>
|
|
35
|
+
* )}
|
|
36
|
+
* </For>
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
export function createToast(options = {}) {
|
|
40
|
+
const { max = 5, enterMs = 250, leaveMs = 200, duration: defaultDuration = 4000, } = options;
|
|
41
|
+
const [toasts, setToasts] = createSignal([]);
|
|
42
|
+
const timers = new Map();
|
|
43
|
+
let nextId = 1;
|
|
44
|
+
let cancelDriver = null;
|
|
45
|
+
const tick = (t) => {
|
|
46
|
+
const list = untrack(toasts);
|
|
47
|
+
if (list.length === 0) {
|
|
48
|
+
cancelDriver = null;
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
const em = prefersReducedMotion() ? 0 : enterMs;
|
|
52
|
+
const lm = prefersReducedMotion() ? 0 : leaveMs;
|
|
53
|
+
const out = [];
|
|
54
|
+
for (const toast of list) {
|
|
55
|
+
const meta = timers.get(toast.id);
|
|
56
|
+
if (!meta)
|
|
57
|
+
continue;
|
|
58
|
+
if (toast.state === "entering" && t - meta.enteredAt >= em) {
|
|
59
|
+
out.push({ ...toast, state: "visible" });
|
|
60
|
+
}
|
|
61
|
+
else if (toast.state === "visible" &&
|
|
62
|
+
meta.deadline > 0 &&
|
|
63
|
+
t >= meta.deadline) {
|
|
64
|
+
meta.leavingAt = t;
|
|
65
|
+
out.push({ ...toast, state: "leaving" });
|
|
66
|
+
}
|
|
67
|
+
else if (toast.state === "leaving" && t - meta.leavingAt >= lm) {
|
|
68
|
+
timers.delete(toast.id);
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
out.push(toast);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
const changed = out.length !== list.length ||
|
|
75
|
+
out.some((toast, i) => toast !== list[i]);
|
|
76
|
+
if (changed)
|
|
77
|
+
setToasts(out);
|
|
78
|
+
const busy = out.some((toast) => {
|
|
79
|
+
const meta = timers.get(toast.id);
|
|
80
|
+
return (toast.state === "entering" ||
|
|
81
|
+
toast.state === "leaving" ||
|
|
82
|
+
(toast.state === "visible" && !!meta && meta.deadline > 0));
|
|
83
|
+
});
|
|
84
|
+
if (!busy) {
|
|
85
|
+
cancelDriver = null;
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
return true;
|
|
89
|
+
};
|
|
90
|
+
const ensureDriver = () => {
|
|
91
|
+
if (!cancelDriver && typeof window !== "undefined") {
|
|
92
|
+
cancelDriver = schedule(tick);
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
const push = (kind, title, opts = {}) => {
|
|
96
|
+
const id = nextId++;
|
|
97
|
+
const t = now();
|
|
98
|
+
const duration = opts.duration ?? defaultDuration;
|
|
99
|
+
const toast = {
|
|
100
|
+
id,
|
|
101
|
+
kind: opts.kind ?? kind,
|
|
102
|
+
title,
|
|
103
|
+
description: opts.description,
|
|
104
|
+
state: typeof window === "undefined" ? "visible" : "entering",
|
|
105
|
+
createdAt: t,
|
|
106
|
+
};
|
|
107
|
+
timers.set(id, {
|
|
108
|
+
enteredAt: t,
|
|
109
|
+
deadline: duration > 0 ? t + duration : 0,
|
|
110
|
+
leavingAt: 0,
|
|
111
|
+
});
|
|
112
|
+
setToasts((prev) => {
|
|
113
|
+
const next = [...prev, toast];
|
|
114
|
+
if (next.length > max) {
|
|
115
|
+
const excess = next.length - max;
|
|
116
|
+
return next.map((item, i) => {
|
|
117
|
+
if (i < excess && item.state !== "leaving") {
|
|
118
|
+
const meta = timers.get(item.id);
|
|
119
|
+
if (meta)
|
|
120
|
+
meta.leavingAt = now();
|
|
121
|
+
return { ...item, state: "leaving" };
|
|
122
|
+
}
|
|
123
|
+
return item;
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
return next;
|
|
127
|
+
});
|
|
128
|
+
ensureDriver();
|
|
129
|
+
return id;
|
|
130
|
+
};
|
|
131
|
+
const dismiss = (id) => {
|
|
132
|
+
const meta = timers.get(id);
|
|
133
|
+
if (!meta)
|
|
134
|
+
return;
|
|
135
|
+
meta.leavingAt = now();
|
|
136
|
+
setToasts((prev) => prev.map((toast) => toast.id === id && toast.state !== "leaving"
|
|
137
|
+
? { ...toast, state: "leaving" }
|
|
138
|
+
: toast));
|
|
139
|
+
ensureDriver();
|
|
140
|
+
};
|
|
141
|
+
const clear = () => {
|
|
142
|
+
const t = now();
|
|
143
|
+
setToasts((prev) => prev.map((toast) => {
|
|
144
|
+
if (toast.state === "leaving")
|
|
145
|
+
return toast;
|
|
146
|
+
const meta = timers.get(toast.id);
|
|
147
|
+
if (meta)
|
|
148
|
+
meta.leavingAt = t;
|
|
149
|
+
return { ...toast, state: "leaving" };
|
|
150
|
+
}));
|
|
151
|
+
ensureDriver();
|
|
152
|
+
};
|
|
153
|
+
onCleanup(() => {
|
|
154
|
+
cancelDriver?.();
|
|
155
|
+
cancelDriver = null;
|
|
156
|
+
});
|
|
157
|
+
return {
|
|
158
|
+
toasts,
|
|
159
|
+
toast: (title, opts) => push("info", title, opts),
|
|
160
|
+
info: (title, opts) => push("info", title, opts),
|
|
161
|
+
success: (title, opts) => push("success", title, opts),
|
|
162
|
+
warning: (title, opts) => push("warning", title, opts),
|
|
163
|
+
error: (title, opts) => push("error", title, opts),
|
|
164
|
+
dismiss,
|
|
165
|
+
clear,
|
|
166
|
+
};
|
|
167
|
+
}
|
package/package.json
CHANGED