solid-drift 0.10.0 → 0.12.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
@@ -776,6 +776,25 @@ createTextWave(() => headline, {
776
776
 
777
777
  Options: `amplitude`, `wavelength` (characters per wave), `period`, `tilt` (default true), `progress`. Returns `{ stop }`. Under reduced motion the text sits still.
778
778
 
779
+ ### `createCountUp(source, options?)`
780
+
781
+ A signal that counts toward a source number with an eased tween, rendering as a formatted string: dashboard stats, prices, scores. When the source changes mid-count the tween retargets from the current displayed value, with no snapping.
782
+
783
+ ```tsx
784
+ import { createCountUp } from "solid-drift"
785
+
786
+ const [revenue, setRevenue] = createSignal(0)
787
+ const display = createCountUp(revenue, {
788
+ decimals: 2,
789
+ prefix: "$",
790
+ separator: ",",
791
+ })
792
+ <div>{display()}</div> // "$1,234.50" gliding up from "$0.00"
793
+ setRevenue(1234.5)
794
+ ```
795
+
796
+ Options: `decimals` (default 0), `duration` (ms, default 1000), `easing` (default `"easeOutExpo"`), `prefix` (default `""`), `suffix` (default `""`), `separator` (thousands separator, default `""`). Returns an accessor of the formatted string. SSR-safe: renders the formatted source value. Under reduced motion the text jumps straight to each new value.
797
+
779
798
  ### `createKineticType(ref, options?)`
780
799
 
781
800
  Kinetic typography: each character (or word) flies in with position, blur, scale, and opacity, staggered for that showreel title feel. One master clock drives every unit, so a 40-character headline costs a single rAF task.
@@ -1075,6 +1094,42 @@ const connect = createConnectButton(() => btn, { strength: 0.35 });
1075
1094
 
1076
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.
1077
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
+
1078
1133
  ### Easings
1079
1134
 
1080
1135
  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/index.d.ts CHANGED
@@ -20,9 +20,10 @@ export { createMagnetic, type MagneticOptions, type MagneticResult, createTilt,
20
20
  export { createTrail, type TrailOptions } from "./trail.js";
21
21
  export { createTimeline, type TimelineStep, type TimelineStatus, type TimelineControls, } from "./timeline.js";
22
22
  export { animateFlip, type FlipOptions, createSharedLayout, type SharedLayoutOptions, type SharedLayoutResult, } from "./flip.js";
23
+ export { createToast, type Toast, type ToastControls, type ToastKind, type ToastOptions, type ToastQueueOptions, type ToastState, } from "./toast.js";
23
24
  export { createSquashStretch, type SquashStretchOptions, type SquashStretchResult, createFollowThrough, type FollowThroughOptions, createAnticipation, type AnticipationOptions, createWobble, type WobbleOptions, type WobbleResult, } from "./cartoon.js";
24
25
  export { createGravity, type GravityOptions, type GravityResult, createPendulum, type PendulumOptions, type PendulumResult, createFling, type FlingOptions, type FlingResult, } from "./physics.js";
25
- 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, } from "./typography.js";
26
+ 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";
26
27
  export { easings, cubicBezier, linear, easeInQuad, easeOutQuad, easeInOutQuad, easeInCubic, easeOutCubic, easeInOutCubic, easeInQuart, easeOutQuart, easeInOutQuart, easeOutExpo, easeOutBack, easeInBack, easeInOutBack, easeOutElastic, easeOutBounce, resolveEasing, type Easing, type EasingName, } from "./easing.js";
27
28
  export { createKineticType, createScenePlayer, createShowreel, createCamera, createColorShift, createTransition, createBeat, createBeatCuts, } from "./motion.js";
28
29
  export type { KineticTypeFrom, KineticTypeOptions, KineticTypeStatus, KineticTypeControls, MotionScene, ScenePlayerStatus, ScenePlayerControls, ShowreelScene, ShowreelSceneKind, CameraKeyframe, CameraOptions, ColorShiftOptions, ColorShiftStatus, ColorShiftControls, TransitionType, TransitionDirection, TransitionOptions, TransitionLayerStyle, TransitionStatus, TransitionControls, BeatOptions, BeatStatus, BeatControls, BeatCutOptions, } from "./motion.js";
package/dist/index.js CHANGED
@@ -20,9 +20,10 @@ export { createMagnetic, createTilt, } from "./pointer.js";
20
20
  export { createTrail } from "./trail.js";
21
21
  export { createTimeline, } from "./timeline.js";
22
22
  export { animateFlip, createSharedLayout, } from "./flip.js";
23
+ export { createToast, } from "./toast.js";
23
24
  export { createSquashStretch, createFollowThrough, createAnticipation, createWobble, } from "./cartoon.js";
24
25
  export { createGravity, createPendulum, createFling, } from "./physics.js";
25
- export { createFontSwap, createTyping, createTextPhysics, createTextTunnel, createTextCutout, createTextGradient, createTextScramble, createTextWave, } from "./typography.js";
26
+ export { createFontSwap, createTyping, createTextPhysics, createTextTunnel, createTextCutout, createTextGradient, createTextScramble, createTextWave, createCountUp, } from "./typography.js";
26
27
  export { easings, cubicBezier, linear, easeInQuad, easeOutQuad, easeInOutQuad, easeInCubic, easeOutCubic, easeInOutCubic, easeInQuart, easeOutQuart, easeInOutQuart, easeOutExpo, easeOutBack, easeInBack, easeInOutBack, easeOutElastic, easeOutBounce, resolveEasing, } from "./easing.js";
27
28
  export { createKineticType, createScenePlayer, createShowreel, createCamera, createColorShift, createTransition, createBeat, createBeatCuts, } from "./motion.js";
28
29
  export { createDrag, } from "./gesture.js";
@@ -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
+ }
@@ -238,4 +238,43 @@ export interface TextWaveOptions {
238
238
  export declare function createTextWave(ref: MaybeElement, options?: TextWaveOptions): {
239
239
  stop: () => void;
240
240
  };
241
+ export interface CountUpOptions {
242
+ /** Decimal places in the output. Default 0. */
243
+ decimals?: number;
244
+ /** Tween duration in milliseconds when the source changes. Default 1000. */
245
+ duration?: number;
246
+ /** Easing for the count. Default "easeOutExpo". */
247
+ easing?: Easing | EasingName;
248
+ /** String placed before the number, e.g. "$". Default "". */
249
+ prefix?: string;
250
+ /** String placed after the number, e.g. "%". Default "". */
251
+ suffix?: string;
252
+ /** Thousands separator, e.g. ",". Default "" (no grouping). */
253
+ separator?: string;
254
+ }
255
+ /**
256
+ * A signal that counts toward a source number with an eased tween,
257
+ * rendering as a formatted string: dashboard stats, prices, scores.
258
+ *
259
+ * When the source changes mid-count the tween retargets from the
260
+ * current displayed value, with no snapping. Formatting (decimals,
261
+ * prefix, suffix, thousands separator) applies to every frame.
262
+ *
263
+ * SSR-safe: renders the formatted source value. Under reduced motion
264
+ * the text jumps straight to each new value with no tween.
265
+ *
266
+ * ```tsx
267
+ * import { createCountUp } from "solid-drift"
268
+ *
269
+ * const [revenue, setRevenue] = createSignal(0)
270
+ * const display = createCountUp(revenue, {
271
+ * decimals: 2,
272
+ * prefix: "$",
273
+ * separator: ",",
274
+ * })
275
+ * <div>{display()}</div> // "$1,234.50" gliding up from "$0.00"
276
+ * setRevenue(1234.5)
277
+ * ```
278
+ */
279
+ export declare function createCountUp(source: Accessor<number>, options?: CountUpOptions): Accessor<string>;
241
280
  export {};
@@ -1,5 +1,6 @@
1
1
  import { createEffect, createSignal, onCleanup, untrack, } from "solid-js";
2
2
  import { animate } from "./animate.js";
3
+ import { createTween } from "./tween.js";
3
4
  import { resolveEasing } from "./easing.js";
4
5
  import { now, schedule } from "./engine.js";
5
6
  import { prefersReducedMotion } from "./reduced-motion.js";
@@ -810,3 +811,47 @@ export function createTextWave(ref, options = {}) {
810
811
  },
811
812
  };
812
813
  }
814
+ function groupInteger(int, separator) {
815
+ if (!separator)
816
+ return int;
817
+ const negative = int.startsWith("-");
818
+ const digits = negative ? int.slice(1) : int;
819
+ const grouped = digits.replace(/\B(?=(\d{3})+(?!\d))/g, separator);
820
+ return (negative ? "-" : "") + grouped;
821
+ }
822
+ /**
823
+ * A signal that counts toward a source number with an eased tween,
824
+ * rendering as a formatted string: dashboard stats, prices, scores.
825
+ *
826
+ * When the source changes mid-count the tween retargets from the
827
+ * current displayed value, with no snapping. Formatting (decimals,
828
+ * prefix, suffix, thousands separator) applies to every frame.
829
+ *
830
+ * SSR-safe: renders the formatted source value. Under reduced motion
831
+ * the text jumps straight to each new value with no tween.
832
+ *
833
+ * ```tsx
834
+ * import { createCountUp } from "solid-drift"
835
+ *
836
+ * const [revenue, setRevenue] = createSignal(0)
837
+ * const display = createCountUp(revenue, {
838
+ * decimals: 2,
839
+ * prefix: "$",
840
+ * separator: ",",
841
+ * })
842
+ * <div>{display()}</div> // "$1,234.50" gliding up from "$0.00"
843
+ * setRevenue(1234.5)
844
+ * ```
845
+ */
846
+ export function createCountUp(source, options = {}) {
847
+ const { decimals = 0, duration = 1000, easing = "easeOutExpo", prefix = "", suffix = "", separator = "", } = options;
848
+ const tweened = createTween(source, { duration, easing });
849
+ const format = (value) => {
850
+ const fixed = value.toFixed(decimals);
851
+ const dot = fixed.indexOf(".");
852
+ const head = dot === -1 ? fixed : fixed.slice(0, dot);
853
+ const tail = dot === -1 ? "" : fixed.slice(dot);
854
+ return prefix + groupInteger(head, separator) + tail + suffix;
855
+ };
856
+ return () => format(tweened());
857
+ }
package/package.json CHANGED
@@ -43,5 +43,5 @@
43
43
  },
44
44
  "type": "module",
45
45
  "types": "./dist/index.d.ts",
46
- "version": "0.10.0"
46
+ "version": "0.12.0"
47
47
  }