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 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
- /** Monotonic clock in milliseconds. */
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
- function tick(now) {
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(now);
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
- if (!rafId)
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
- /** Monotonic clock in milliseconds. */
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 typeof performance !== "undefined" ? performance.now() : Date.now();
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";
@@ -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
+ }
@@ -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
@@ -43,5 +43,5 @@
43
43
  },
44
44
  "type": "module",
45
45
  "types": "./dist/index.d.ts",
46
- "version": "0.11.0"
46
+ "version": "0.13.0"
47
47
  }