solid-drift 0.28.0 → 0.29.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 +26 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +2 -0
- package/dist/scrollspy.d.ts +42 -0
- package/dist/scrollspy.js +105 -0
- package/dist/skeleton.d.ts +37 -0
- package/dist/skeleton.js +112 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1617,6 +1617,32 @@ await feed.commit(bid, (b) => sendBidTx(b)).catch(() => {
|
|
|
1617
1617
|
|
|
1618
1618
|
`createOptimistic(initial, apply)` gives `{ value, setBase, pending, pendingCount, error, commit, reset }`. `commit(update, task)` applies the update instantly, then runs the task; on success the update is promoted into the base truth (no flicker while the server catches up), and on failure it is rolled back, `error()` is set, and the error is rethrown. `setBase()` folds fresh server truth in (after a refetch); write `apply` idempotently, for example upsert by id, so truth that already includes an optimistic update does not duplicate it. `reset()` drops in-flight updates. Pure signals, SSR-safe.
|
|
1619
1619
|
|
|
1620
|
+
### Skeleton and scroll spy
|
|
1621
|
+
|
|
1622
|
+
```tsx
|
|
1623
|
+
import { createSkeleton, createScrollSpy } from "solid-drift";
|
|
1624
|
+
|
|
1625
|
+
const sk = createSkeleton({ delay: 200, minVisible: 400 });
|
|
1626
|
+
createEffect(() => sk.setLoading(query.loading()));
|
|
1627
|
+
|
|
1628
|
+
const spy = createScrollSpy({ targets: ["intro", "api", "faq"], offset: 80 });
|
|
1629
|
+
|
|
1630
|
+
<Show when={sk.show()} fallback={<ArticleView />}>
|
|
1631
|
+
<div class="skeleton" style={{ "--shine": `${sk.phase() * 100}%` }} />
|
|
1632
|
+
</Show>
|
|
1633
|
+
<nav>
|
|
1634
|
+
<For each={["intro", "api", "faq"]}>
|
|
1635
|
+
{(id) => (
|
|
1636
|
+
<a classList={{ active: spy.active() === id }}
|
|
1637
|
+
onClick={() => spy.scrollTo(id)}>{id}</a>
|
|
1638
|
+
)}
|
|
1639
|
+
</For>
|
|
1640
|
+
</nav>
|
|
1641
|
+
```
|
|
1642
|
+
|
|
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
|
+
- `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
|
+
|
|
1620
1646
|
### Easings
|
|
1621
1647
|
|
|
1622
1648
|
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
|
@@ -48,3 +48,7 @@ export { createBattery, createNetwork, createWakeLock, createContactPick, create
|
|
|
48
48
|
export type { BatteryState, NetworkState, WakeLockState, PickedContact, ContactPickState, OTPState, ShareState, NFCRecord, NFCMessage, NFCState, TorchState, GyroState, ShakeOptions, ShakeState, ScanlineOptions, ScanlineState, } from "./hardware.js";
|
|
49
49
|
export { createOptimistic } from "./optimistic.js";
|
|
50
50
|
export type { OptimisticControls } from "./optimistic.js";
|
|
51
|
+
export { createSkeleton } from "./skeleton.js";
|
|
52
|
+
export type { SkeletonOptions, SkeletonControls } from "./skeleton.js";
|
|
53
|
+
export { createScrollSpy } from "./scrollspy.js";
|
|
54
|
+
export type { ScrollSpyOptions, ScrollSpyControls } from "./scrollspy.js";
|
package/dist/index.js
CHANGED
|
@@ -40,3 +40,5 @@ export { createSSEParser, createSSE, createChatModel, } from "./stream.js";
|
|
|
40
40
|
export { createVoiceState, createMicLevel, createSpeech, createWaveform, createTTS, createThinking, createPrompt, } from "./voice.js";
|
|
41
41
|
export { createBattery, createNetwork, createWakeLock, createContactPick, createOTP, createShare, createNFC, createTorch, createGyro, createShake, createScanline, } from "./hardware.js";
|
|
42
42
|
export { createOptimistic } from "./optimistic.js";
|
|
43
|
+
export { createSkeleton } from "./skeleton.js";
|
|
44
|
+
export { createScrollSpy } from "./scrollspy.js";
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export interface ScrollSpyOptions {
|
|
2
|
+
/** Section element ids in document order, or an accessor for them. */
|
|
3
|
+
targets: string[] | (() => string[]);
|
|
4
|
+
/**
|
|
5
|
+
* Scroll container; defaults to the window. Pass an element for a
|
|
6
|
+
* scrollable panel, or a getter when the ref is not mounted yet.
|
|
7
|
+
*/
|
|
8
|
+
container?: HTMLElement | Window | (() => HTMLElement | Window | null);
|
|
9
|
+
/** Pixels below the container top where a section becomes active. Default 0. */
|
|
10
|
+
offset?: number;
|
|
11
|
+
/** Smooth-scroll in scrollTo. Default true (auto under reduced motion). */
|
|
12
|
+
smooth?: boolean;
|
|
13
|
+
onChange?: (id: string | null) => void;
|
|
14
|
+
}
|
|
15
|
+
export interface ScrollSpyControls {
|
|
16
|
+
/** Id of the deepest section at or above the offset line, or null. */
|
|
17
|
+
active: () => string | null;
|
|
18
|
+
/** Scroll the section into view. No-op on the server or for unknown ids. */
|
|
19
|
+
scrollTo: (id: string) => void;
|
|
20
|
+
/** Recompute the active section immediately. */
|
|
21
|
+
refresh: () => void;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* createScrollSpy
|
|
25
|
+
*
|
|
26
|
+
* Navigation scroll spy: `active()` is the id of the deepest target
|
|
27
|
+
* section whose top sits at or above the `offset` line of the scroll
|
|
28
|
+
* container. Scroll handling is rAF-throttled on the shared clock.
|
|
29
|
+
*
|
|
30
|
+
* ```tsx
|
|
31
|
+
* const spy = createScrollSpy({ targets: ["intro", "api", "faq"], offset: 80 });
|
|
32
|
+
* <nav>
|
|
33
|
+
* <For each={["intro", "api", "faq"]}>
|
|
34
|
+
* {(id) => (
|
|
35
|
+
* <a classList={{ active: spy.active() === id }}
|
|
36
|
+
* onClick={() => spy.scrollTo(id)}>{id}</a>
|
|
37
|
+
* )}
|
|
38
|
+
* </For>
|
|
39
|
+
* </nav>
|
|
40
|
+
* ```
|
|
41
|
+
*/
|
|
42
|
+
export declare function createScrollSpy(options: ScrollSpyOptions): ScrollSpyControls;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { createSignal, onCleanup } from "solid-js";
|
|
2
|
+
import { schedule } from "./engine.js";
|
|
3
|
+
import { prefersReducedMotion } from "./reduced-motion.js";
|
|
4
|
+
function isClient() {
|
|
5
|
+
return typeof globalThis.window !== "undefined";
|
|
6
|
+
}
|
|
7
|
+
function getDocument() {
|
|
8
|
+
return globalThis.document;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* createScrollSpy
|
|
12
|
+
*
|
|
13
|
+
* Navigation scroll spy: `active()` is the id of the deepest target
|
|
14
|
+
* section whose top sits at or above the `offset` line of the scroll
|
|
15
|
+
* container. Scroll handling is rAF-throttled on the shared clock.
|
|
16
|
+
*
|
|
17
|
+
* ```tsx
|
|
18
|
+
* const spy = createScrollSpy({ targets: ["intro", "api", "faq"], offset: 80 });
|
|
19
|
+
* <nav>
|
|
20
|
+
* <For each={["intro", "api", "faq"]}>
|
|
21
|
+
* {(id) => (
|
|
22
|
+
* <a classList={{ active: spy.active() === id }}
|
|
23
|
+
* onClick={() => spy.scrollTo(id)}>{id}</a>
|
|
24
|
+
* )}
|
|
25
|
+
* </For>
|
|
26
|
+
* </nav>
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
export function createScrollSpy(options) {
|
|
30
|
+
const { offset = 0, smooth = true, onChange } = options;
|
|
31
|
+
const [active, setActive] = createSignal(null);
|
|
32
|
+
const resolveTargets = () => typeof options.targets === "function" ? options.targets() : options.targets;
|
|
33
|
+
const resolveContainer = () => {
|
|
34
|
+
if (!isClient())
|
|
35
|
+
return null;
|
|
36
|
+
const c = options.container;
|
|
37
|
+
if (typeof c === "function")
|
|
38
|
+
return c() ?? null;
|
|
39
|
+
return c ?? globalThis.window;
|
|
40
|
+
};
|
|
41
|
+
const sectionTop = (el, container) => {
|
|
42
|
+
const rect = el.getBoundingClientRect().top;
|
|
43
|
+
if (container === globalThis.window)
|
|
44
|
+
return rect;
|
|
45
|
+
const containerRect = container.getBoundingClientRect();
|
|
46
|
+
return rect - containerRect.top;
|
|
47
|
+
};
|
|
48
|
+
const update = () => {
|
|
49
|
+
const doc = getDocument();
|
|
50
|
+
const container = resolveContainer();
|
|
51
|
+
if (!doc || !container)
|
|
52
|
+
return;
|
|
53
|
+
let current = null;
|
|
54
|
+
for (const id of resolveTargets()) {
|
|
55
|
+
const el = doc.getElementById(id);
|
|
56
|
+
if (!el)
|
|
57
|
+
continue;
|
|
58
|
+
if (sectionTop(el, container) <= offset)
|
|
59
|
+
current = id;
|
|
60
|
+
}
|
|
61
|
+
if (current !== active()) {
|
|
62
|
+
setActive(current);
|
|
63
|
+
onChange?.(current);
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
let queued = false;
|
|
67
|
+
const onScroll = () => {
|
|
68
|
+
if (queued)
|
|
69
|
+
return;
|
|
70
|
+
queued = true;
|
|
71
|
+
schedule(() => {
|
|
72
|
+
queued = false;
|
|
73
|
+
update();
|
|
74
|
+
return false;
|
|
75
|
+
});
|
|
76
|
+
};
|
|
77
|
+
const container = resolveContainer();
|
|
78
|
+
const removeListener = (() => {
|
|
79
|
+
if (container &&
|
|
80
|
+
typeof container.addEventListener === "function") {
|
|
81
|
+
const target = container;
|
|
82
|
+
target.addEventListener("scroll", onScroll, { passive: true });
|
|
83
|
+
return () => target.removeEventListener("scroll", onScroll);
|
|
84
|
+
}
|
|
85
|
+
return null;
|
|
86
|
+
})();
|
|
87
|
+
const scrollTo = (id) => {
|
|
88
|
+
const doc = getDocument();
|
|
89
|
+
if (!doc)
|
|
90
|
+
return;
|
|
91
|
+
const el = doc.getElementById(id);
|
|
92
|
+
if (!el || typeof el.scrollIntoView !== "function")
|
|
93
|
+
return;
|
|
94
|
+
el.scrollIntoView({
|
|
95
|
+
behavior: smooth && !prefersReducedMotion() ? "smooth" : "auto",
|
|
96
|
+
block: "start",
|
|
97
|
+
});
|
|
98
|
+
};
|
|
99
|
+
if (isClient())
|
|
100
|
+
update();
|
|
101
|
+
onCleanup(() => {
|
|
102
|
+
removeListener?.();
|
|
103
|
+
});
|
|
104
|
+
return { active, scrollTo, refresh: update };
|
|
105
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export interface SkeletonOptions {
|
|
2
|
+
/** Milliseconds before the skeleton appears; hides flashes for fast loads. Default 200. */
|
|
3
|
+
delay?: number;
|
|
4
|
+
/** Milliseconds the skeleton stays visible once shown. Default 0. */
|
|
5
|
+
minVisible?: number;
|
|
6
|
+
/** Drive a shimmer `phase` while shown. Default true. */
|
|
7
|
+
shimmer?: boolean;
|
|
8
|
+
/** Milliseconds per shimmer sweep. Default 1400. */
|
|
9
|
+
shimmerDuration?: number;
|
|
10
|
+
}
|
|
11
|
+
export interface SkeletonControls {
|
|
12
|
+
/** Raw loading flag, mirrors the last setLoading call. */
|
|
13
|
+
loading: () => boolean;
|
|
14
|
+
/** Debounced visibility: bind `{#if}` / `<Show>` to this. */
|
|
15
|
+
show: () => boolean;
|
|
16
|
+
/** Shimmer sweep 0..1 while shown (0 otherwise); bind to a gradient position. */
|
|
17
|
+
phase: () => number;
|
|
18
|
+
setLoading: (loading: boolean) => void;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* createSkeleton
|
|
22
|
+
*
|
|
23
|
+
* Loading-placeholder controller with flicker protection: the skeleton
|
|
24
|
+
* appears only after `delay` (fast loads never flash it) and, once shown,
|
|
25
|
+
* stays for at least `minVisible`. While shown, `phase()` sweeps 0..1 on
|
|
26
|
+
* the shared clock for a JS-driven shimmer; it freezes under reduced
|
|
27
|
+
* motion. On the server `show()` never flips.
|
|
28
|
+
*
|
|
29
|
+
* ```tsx
|
|
30
|
+
* const sk = createSkeleton({ delay: 200, minVisible: 400 });
|
|
31
|
+
* createEffect(() => { sk.setLoading(query.loading()); });
|
|
32
|
+
* <Show when={sk.show()} fallback={<ArticleView/>}>
|
|
33
|
+
* <div class="skeleton" style={{ "--shine": `${sk.phase() * 100}%` }} />
|
|
34
|
+
* </Show>
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
export declare function createSkeleton(options?: SkeletonOptions): SkeletonControls;
|
package/dist/skeleton.js
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { createSignal, onCleanup } from "solid-js";
|
|
2
|
+
import { schedule } from "./engine.js";
|
|
3
|
+
import { prefersReducedMotion } from "./reduced-motion.js";
|
|
4
|
+
function isClient() {
|
|
5
|
+
return typeof globalThis.window !== "undefined";
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* createSkeleton
|
|
9
|
+
*
|
|
10
|
+
* Loading-placeholder controller with flicker protection: the skeleton
|
|
11
|
+
* appears only after `delay` (fast loads never flash it) and, once shown,
|
|
12
|
+
* stays for at least `minVisible`. While shown, `phase()` sweeps 0..1 on
|
|
13
|
+
* the shared clock for a JS-driven shimmer; it freezes under reduced
|
|
14
|
+
* motion. On the server `show()` never flips.
|
|
15
|
+
*
|
|
16
|
+
* ```tsx
|
|
17
|
+
* const sk = createSkeleton({ delay: 200, minVisible: 400 });
|
|
18
|
+
* createEffect(() => { sk.setLoading(query.loading()); });
|
|
19
|
+
* <Show when={sk.show()} fallback={<ArticleView/>}>
|
|
20
|
+
* <div class="skeleton" style={{ "--shine": `${sk.phase() * 100}%` }} />
|
|
21
|
+
* </Show>
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
export function createSkeleton(options = {}) {
|
|
25
|
+
const { delay = 200, minVisible = 0, shimmer = true, shimmerDuration = 1400, } = options;
|
|
26
|
+
const [loading, setLoadingSignal] = createSignal(false);
|
|
27
|
+
const [show, setShow] = createSignal(false);
|
|
28
|
+
const [phase, setPhase] = createSignal(0);
|
|
29
|
+
let showTimer = null;
|
|
30
|
+
let hideTimer = null;
|
|
31
|
+
let shownAt = 0;
|
|
32
|
+
let stopShimmer = null;
|
|
33
|
+
const clearTimer = (timer) => {
|
|
34
|
+
if (timer !== null)
|
|
35
|
+
clearTimeout(timer);
|
|
36
|
+
};
|
|
37
|
+
const startShimmer = () => {
|
|
38
|
+
if (!shimmer || stopShimmer !== null)
|
|
39
|
+
return;
|
|
40
|
+
if (prefersReducedMotion() || shimmerDuration <= 0) {
|
|
41
|
+
setPhase(0);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
let start = 0;
|
|
45
|
+
let first = true;
|
|
46
|
+
stopShimmer = schedule((t) => {
|
|
47
|
+
if (first) {
|
|
48
|
+
start = t;
|
|
49
|
+
first = false;
|
|
50
|
+
}
|
|
51
|
+
setPhase(((t - start) % shimmerDuration) / shimmerDuration);
|
|
52
|
+
return true;
|
|
53
|
+
});
|
|
54
|
+
};
|
|
55
|
+
const stopShimmerNow = () => {
|
|
56
|
+
stopShimmer?.();
|
|
57
|
+
stopShimmer = null;
|
|
58
|
+
setPhase(0);
|
|
59
|
+
};
|
|
60
|
+
const reveal = () => {
|
|
61
|
+
setShow(true);
|
|
62
|
+
shownAt = Date.now();
|
|
63
|
+
startShimmer();
|
|
64
|
+
};
|
|
65
|
+
const conceal = () => {
|
|
66
|
+
setShow(false);
|
|
67
|
+
stopShimmerNow();
|
|
68
|
+
};
|
|
69
|
+
const setLoading = (value) => {
|
|
70
|
+
setLoadingSignal(value);
|
|
71
|
+
if (!isClient())
|
|
72
|
+
return;
|
|
73
|
+
if (value) {
|
|
74
|
+
clearTimer(hideTimer);
|
|
75
|
+
hideTimer = null;
|
|
76
|
+
if (show() || showTimer !== null)
|
|
77
|
+
return;
|
|
78
|
+
if (delay <= 0) {
|
|
79
|
+
reveal();
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
showTimer = setTimeout(() => {
|
|
83
|
+
showTimer = null;
|
|
84
|
+
reveal();
|
|
85
|
+
}, delay);
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
clearTimer(showTimer);
|
|
89
|
+
showTimer = null;
|
|
90
|
+
if (!show())
|
|
91
|
+
return;
|
|
92
|
+
const remaining = minVisible - (Date.now() - shownAt);
|
|
93
|
+
if (remaining <= 0) {
|
|
94
|
+
conceal();
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
clearTimer(hideTimer);
|
|
98
|
+
hideTimer = setTimeout(() => {
|
|
99
|
+
hideTimer = null;
|
|
100
|
+
conceal();
|
|
101
|
+
}, remaining);
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
onCleanup(() => {
|
|
105
|
+
clearTimer(showTimer);
|
|
106
|
+
clearTimer(hideTimer);
|
|
107
|
+
showTimer = null;
|
|
108
|
+
hideTimer = null;
|
|
109
|
+
stopShimmerNow();
|
|
110
|
+
});
|
|
111
|
+
return { loading, show, phase, setLoading };
|
|
112
|
+
}
|
package/package.json
CHANGED