solid-drift 0.29.0 → 0.30.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
@@ -1643,6 +1643,25 @@ 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
+
1646
1665
  ### Easings
1647
1666
 
1648
1667
  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,7 @@ 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";
package/dist/index.js CHANGED
@@ -42,3 +42,5 @@ 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";
package/package.json CHANGED
@@ -43,5 +43,5 @@
43
43
  },
44
44
  "type": "module",
45
45
  "types": "./dist/index.d.ts",
46
- "version": "0.29.0"
46
+ "version": "0.30.0"
47
47
  }