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 +19 -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 +4 -0
- package/dist/index.js +2 -0
- package/package.json +1 -1
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