solid-drift 0.31.0 → 0.32.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 +28 -0
- package/dist/apputils.d.ts +205 -0
- package/dist/apputils.js +326 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1698,6 +1698,34 @@ const draw = createPathDraw(() => mark, { duration: 1600 });
|
|
|
1698
1698
|
- `createPress(ref, options?)` press gesture state: `{ pressed }`. Pointer down/up/cancel/leave plus Enter/Space keys for keyboard parity; `onChange` fires on change only. State only, no animation; pair with `createVariants`. SSR-safe.
|
|
1699
1699
|
- `createHover(ref, options?)` hover gesture state: `{ hovering }`. Pointer enter/leave plus focus/blur for keyboard parity; `onChange` fires on change only. SSR-safe.
|
|
1700
1700
|
|
|
1701
|
+
### App utilities
|
|
1702
|
+
|
|
1703
|
+
```tsx
|
|
1704
|
+
import {
|
|
1705
|
+
createColorScheme,
|
|
1706
|
+
createIdle,
|
|
1707
|
+
createOnline,
|
|
1708
|
+
createInstallPrompt,
|
|
1709
|
+
createUndo,
|
|
1710
|
+
createFullscreen,
|
|
1711
|
+
} from "solid-drift";
|
|
1712
|
+
|
|
1713
|
+
const theme = createColorScheme(); // follows the OS, persists, writes html[data-theme]
|
|
1714
|
+
const { idle } = createIdle({ timeout: 30_000 }); // auto-hide chrome when idle
|
|
1715
|
+
const { online } = createOnline(); // offline banner
|
|
1716
|
+
const install = createInstallPrompt(); // PWA install button
|
|
1717
|
+
const doc = createUndo({ title: "" }); // undoable form state
|
|
1718
|
+
let stage!: HTMLDivElement;
|
|
1719
|
+
const fs = createFullscreen(() => stage); // fullscreen toggle
|
|
1720
|
+
```
|
|
1721
|
+
|
|
1722
|
+
- `createColorScheme(options?)`: `{ scheme, preference, setPreference, toggle }`. Resolves `"system"` through the `(prefers-color-scheme: dark)` media query (reactive to OS changes), persists the preference to localStorage (`storageKey`, null disables), and writes the resolved scheme to `<html data-theme="light|dark">` (configurable `attribute`) plus `color-scheme`. SSR-safe (resolves to light on the server).
|
|
1723
|
+
- `createIdle(options?)`: `{ idle, lastActive, reset }`. `idle()` flips true after `timeout` ms (default 60000) without any of the `events` (default mousemove, mousedown, keydown, touchstart, wheel); activity restarts the timer. SSR-safe.
|
|
1724
|
+
- `createOnline()`: `{ online }`. Seeds from `navigator.onLine`, follows window `online`/`offline` events. SSR-safe (assumes online).
|
|
1725
|
+
- `createInstallPrompt()`: `{ canInstall, prompt }`. Captures `beforeinstallprompt` (preventing the browser mini-bar); `prompt()` shows it from a click handler and resolves to the user's choice, or null when unavailable; each captured event is single-use. SSR-safe.
|
|
1726
|
+
- `createUndo(initial, options?)`: undoable state: `{ value, set, undo, redo, clear, reset, canUndo, canRedo, past, future }`. `set()` (value or updater) records history trimmed to `capacity` (default 50); a new `set()` discards the redo stack. Pure logic, SSR-safe.
|
|
1727
|
+
- `createFullscreen(ref, options?)`: `{ fullscreen, enter, exit, toggle }`. Tracks `document.fullscreenElement` so Escape and external changes stay in sync; failures go to `onError` instead of throwing. SSR-safe.
|
|
1728
|
+
|
|
1701
1729
|
### Easings
|
|
1702
1730
|
|
|
1703
1731
|
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.
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { type Accessor } from "solid-js";
|
|
2
|
+
type MaybeElement = () => Element | null | undefined;
|
|
3
|
+
/** Resolved color scheme. */
|
|
4
|
+
export type ColorScheme = "light" | "dark";
|
|
5
|
+
/** Stored preference: an explicit scheme or following the OS. */
|
|
6
|
+
export type ColorSchemePreference = ColorScheme | "system";
|
|
7
|
+
export interface ColorSchemeOptions {
|
|
8
|
+
/** Preference used when nothing is stored. Default "system". */
|
|
9
|
+
default?: ColorSchemePreference;
|
|
10
|
+
/**
|
|
11
|
+
* localStorage key for the preference. Set to null to disable
|
|
12
|
+
* persistence. Default "solid-drift:color-scheme".
|
|
13
|
+
*/
|
|
14
|
+
storageKey?: string | null;
|
|
15
|
+
/**
|
|
16
|
+
* Attribute written on `<html>` with the resolved scheme, so CSS can
|
|
17
|
+
* hook onto it (`html[data-theme="dark"] { ... }`). Default "data-theme".
|
|
18
|
+
*/
|
|
19
|
+
attribute?: string;
|
|
20
|
+
}
|
|
21
|
+
export interface ColorSchemeControls {
|
|
22
|
+
/** Resolved scheme: the preference, or the OS scheme when "system". */
|
|
23
|
+
scheme: Accessor<ColorScheme>;
|
|
24
|
+
/** The stored preference, "system" included. */
|
|
25
|
+
preference: Accessor<ColorSchemePreference>;
|
|
26
|
+
setPreference: (preference: ColorSchemePreference) => void;
|
|
27
|
+
/** Flips between light and dark, leaving "system" behind. */
|
|
28
|
+
toggle: () => void;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Color scheme manager. Resolves "system" through the
|
|
32
|
+
* `(prefers-color-scheme: dark)` media query (reactive to OS changes),
|
|
33
|
+
* persists the preference to localStorage, and writes the resolved
|
|
34
|
+
* scheme to `<html data-theme="light|dark">` plus `color-scheme` so
|
|
35
|
+
* form controls and scrollbars follow.
|
|
36
|
+
*
|
|
37
|
+
* SSR-safe: resolves "system" to "light" on the server.
|
|
38
|
+
*
|
|
39
|
+
* ```tsx
|
|
40
|
+
* const theme = createColorScheme();
|
|
41
|
+
* <button onClick={theme.toggle}>
|
|
42
|
+
* {theme.scheme() === "dark" ? "Light mode" : "Dark mode"}
|
|
43
|
+
* </button>
|
|
44
|
+
* ```
|
|
45
|
+
*/
|
|
46
|
+
export declare function createColorScheme(options?: ColorSchemeOptions): ColorSchemeControls;
|
|
47
|
+
export interface IdleOptions {
|
|
48
|
+
/** Ms of inactivity before `idle()` flips true. Default 60000. */
|
|
49
|
+
timeout?: number;
|
|
50
|
+
/**
|
|
51
|
+
* Activity events that reset the timer. Default mousemove, mousedown,
|
|
52
|
+
* keydown, touchstart and wheel.
|
|
53
|
+
*/
|
|
54
|
+
events?: string[];
|
|
55
|
+
/** Start in the idle state. Default false. */
|
|
56
|
+
initialIdle?: boolean;
|
|
57
|
+
}
|
|
58
|
+
export interface IdleControls {
|
|
59
|
+
/** True once `timeout` ms passed without activity. */
|
|
60
|
+
idle: Accessor<boolean>;
|
|
61
|
+
/** Timestamp of the last activity. */
|
|
62
|
+
lastActive: Accessor<number>;
|
|
63
|
+
/** Mark the user active now and restart the timer. */
|
|
64
|
+
reset: () => void;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Idle detection. `idle()` becomes true after `timeout` ms without any
|
|
68
|
+
* of the activity events; any activity flips it back and restarts the
|
|
69
|
+
* timer. Useful for auto-hiding chrome, pausing ambient animation, or
|
|
70
|
+
* "are you still there" prompts.
|
|
71
|
+
*
|
|
72
|
+
* SSR-safe: never idle on the server.
|
|
73
|
+
*
|
|
74
|
+
* ```tsx
|
|
75
|
+
* const { idle } = createIdle({ timeout: 30_000 });
|
|
76
|
+
* <div classList={{ "controls-hidden": idle() }}>…</div>
|
|
77
|
+
* ```
|
|
78
|
+
*/
|
|
79
|
+
export declare function createIdle(options?: IdleOptions): IdleControls;
|
|
80
|
+
export interface OnlineControls {
|
|
81
|
+
/** Mirrors `navigator.onLine`, updated on online/offline events. */
|
|
82
|
+
online: Accessor<boolean>;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Network connectivity state. Seeds from `navigator.onLine` and follows
|
|
86
|
+
* the window `online` / `offline` events. SSR-safe: assumes online on
|
|
87
|
+
* the server.
|
|
88
|
+
*
|
|
89
|
+
* ```tsx
|
|
90
|
+
* const { online } = createOnline();
|
|
91
|
+
* <Show when={!online()}>
|
|
92
|
+
* <p>You are offline. Changes will sync when you reconnect.</p>
|
|
93
|
+
* </Show>
|
|
94
|
+
* ```
|
|
95
|
+
*/
|
|
96
|
+
export declare function createOnline(): OnlineControls;
|
|
97
|
+
/** Minimal shape of the PWA `beforeinstallprompt` event. */
|
|
98
|
+
export interface BeforeInstallPromptEvent extends Event {
|
|
99
|
+
prompt: () => Promise<void>;
|
|
100
|
+
userChoice: Promise<{
|
|
101
|
+
outcome: "accepted" | "dismissed";
|
|
102
|
+
}>;
|
|
103
|
+
}
|
|
104
|
+
export interface InstallPromptControls {
|
|
105
|
+
/** True once the browser fired `beforeinstallprompt`. */
|
|
106
|
+
canInstall: Accessor<boolean>;
|
|
107
|
+
/**
|
|
108
|
+
* Shows the install prompt. Returns the user's choice, or null when
|
|
109
|
+
* the browser never offered installation. Each captured event can be
|
|
110
|
+
* used once.
|
|
111
|
+
*/
|
|
112
|
+
prompt: () => Promise<{
|
|
113
|
+
outcome: "accepted" | "dismissed";
|
|
114
|
+
} | null>;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* PWA install prompt. Captures the `beforeinstallprompt` event (calling
|
|
118
|
+
* `preventDefault()` so the browser does not show its own mini-bar) and
|
|
119
|
+
* exposes it through `prompt()`, which is safe to call from a click
|
|
120
|
+
* handler. SSR-safe: never installable on the server.
|
|
121
|
+
*
|
|
122
|
+
* ```tsx
|
|
123
|
+
* const install = createInstallPrompt();
|
|
124
|
+
* <Show when={install.canInstall()}>
|
|
125
|
+
* <button
|
|
126
|
+
* onClick={async () => {
|
|
127
|
+
* const choice = await install.prompt();
|
|
128
|
+
* if (choice?.outcome === "accepted") toast("Installed!");
|
|
129
|
+
* }}
|
|
130
|
+
* >
|
|
131
|
+
* Install app
|
|
132
|
+
* </button>
|
|
133
|
+
* </Show>
|
|
134
|
+
* ```
|
|
135
|
+
*/
|
|
136
|
+
export declare function createInstallPrompt(): InstallPromptControls;
|
|
137
|
+
export interface UndoOptions {
|
|
138
|
+
/** Maximum snapshots kept. Older ones are dropped. Default 50. */
|
|
139
|
+
capacity?: number;
|
|
140
|
+
}
|
|
141
|
+
export interface UndoControls<T> {
|
|
142
|
+
/** Current value. */
|
|
143
|
+
value: Accessor<T>;
|
|
144
|
+
/** Set a new value, recording the previous one for undo. */
|
|
145
|
+
set: (value: T | ((prev: T) => T)) => void;
|
|
146
|
+
/** Step back; no-op when there is nothing to undo. */
|
|
147
|
+
undo: () => void;
|
|
148
|
+
/** Step forward; no-op when there is nothing to redo. */
|
|
149
|
+
redo: () => void;
|
|
150
|
+
/** Drop all history without touching the value. */
|
|
151
|
+
clear: () => void;
|
|
152
|
+
/** Restore the initial value and drop all history. */
|
|
153
|
+
reset: () => void;
|
|
154
|
+
canUndo: Accessor<boolean>;
|
|
155
|
+
canRedo: Accessor<boolean>;
|
|
156
|
+
/** Undo stack, oldest first. */
|
|
157
|
+
past: Accessor<readonly T[]>;
|
|
158
|
+
/** Redo stack, next redo first. */
|
|
159
|
+
future: Accessor<readonly T[]>;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Undoable state. `set()` records the previous value, `undo()` steps
|
|
163
|
+
* back through history, `redo()` steps forward, and any new `set()`
|
|
164
|
+
* discards the redo stack. Pure logic: no DOM, SSR-safe.
|
|
165
|
+
*
|
|
166
|
+
* ```tsx
|
|
167
|
+
* const doc = createUndo({ title: "", body: "" }, { capacity: 100 });
|
|
168
|
+
* <input
|
|
169
|
+
* value={doc.value().title}
|
|
170
|
+
* onInput={(e) => doc.set((d) => ({ ...d, title: e.target.value }))}
|
|
171
|
+
* />
|
|
172
|
+
* <button disabled={!doc.canUndo()} onClick={doc.undo}>Undo</button>
|
|
173
|
+
* <button disabled={!doc.canRedo()} onClick={doc.redo}>Redo</button>
|
|
174
|
+
* ```
|
|
175
|
+
*/
|
|
176
|
+
export declare function createUndo<T>(initial: T, options?: UndoOptions): UndoControls<T>;
|
|
177
|
+
export interface FullscreenOptions {
|
|
178
|
+
/** Called when entering/exiting fails or is unsupported. */
|
|
179
|
+
onError?: (error: unknown) => void;
|
|
180
|
+
}
|
|
181
|
+
export interface FullscreenControls {
|
|
182
|
+
/** True while the referenced element is the fullscreen element. */
|
|
183
|
+
fullscreen: Accessor<boolean>;
|
|
184
|
+
enter: () => Promise<void>;
|
|
185
|
+
exit: () => Promise<void>;
|
|
186
|
+
toggle: () => Promise<void>;
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Fullscreen for an element. Tracks `document.fullscreenElement` so the
|
|
190
|
+
* state stays correct when the user presses Escape or another element
|
|
191
|
+
* takes over. Failures (unsupported browser, denied request) go to
|
|
192
|
+
* `onError` instead of throwing. SSR-safe: never fullscreen on the
|
|
193
|
+
* server.
|
|
194
|
+
*
|
|
195
|
+
* ```tsx
|
|
196
|
+
* let stage!: HTMLDivElement;
|
|
197
|
+
* const fs = createFullscreen(() => stage);
|
|
198
|
+
* <button onClick={fs.toggle}>
|
|
199
|
+
* {fs.fullscreen() ? "Exit fullscreen" : "Go fullscreen"}
|
|
200
|
+
* </button>
|
|
201
|
+
* <div ref={stage}>…</div>
|
|
202
|
+
* ```
|
|
203
|
+
*/
|
|
204
|
+
export declare function createFullscreen(ref: MaybeElement, options?: FullscreenOptions): FullscreenControls;
|
|
205
|
+
export {};
|
package/dist/apputils.js
ADDED
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
import { createEffect, createSignal, onCleanup, untrack, } from "solid-js";
|
|
2
|
+
const isSchemePreference = (value) => value === "light" || value === "dark" || value === "system";
|
|
3
|
+
/**
|
|
4
|
+
* Color scheme manager. Resolves "system" through the
|
|
5
|
+
* `(prefers-color-scheme: dark)` media query (reactive to OS changes),
|
|
6
|
+
* persists the preference to localStorage, and writes the resolved
|
|
7
|
+
* scheme to `<html data-theme="light|dark">` plus `color-scheme` so
|
|
8
|
+
* form controls and scrollbars follow.
|
|
9
|
+
*
|
|
10
|
+
* SSR-safe: resolves "system" to "light" on the server.
|
|
11
|
+
*
|
|
12
|
+
* ```tsx
|
|
13
|
+
* const theme = createColorScheme();
|
|
14
|
+
* <button onClick={theme.toggle}>
|
|
15
|
+
* {theme.scheme() === "dark" ? "Light mode" : "Dark mode"}
|
|
16
|
+
* </button>
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
export function createColorScheme(options = {}) {
|
|
20
|
+
const { default: defaultPreference = "system", storageKey = "solid-drift:color-scheme", attribute = "data-theme", } = options;
|
|
21
|
+
const readStored = () => {
|
|
22
|
+
if (!storageKey || typeof window === "undefined")
|
|
23
|
+
return null;
|
|
24
|
+
try {
|
|
25
|
+
const raw = window.localStorage.getItem(storageKey);
|
|
26
|
+
return isSchemePreference(raw) ? raw : null;
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
const [preference, setPreference] = createSignal(readStored() ?? defaultPreference);
|
|
33
|
+
const [systemDark, setSystemDark] = createSignal(false);
|
|
34
|
+
if (typeof window !== "undefined") {
|
|
35
|
+
createEffect(() => {
|
|
36
|
+
const mq = window.matchMedia("(prefers-color-scheme: dark)");
|
|
37
|
+
setSystemDark(mq.matches);
|
|
38
|
+
const onChange = (event) => {
|
|
39
|
+
setSystemDark(event.matches);
|
|
40
|
+
};
|
|
41
|
+
mq.addEventListener("change", onChange);
|
|
42
|
+
onCleanup(() => mq.removeEventListener("change", onChange));
|
|
43
|
+
});
|
|
44
|
+
createEffect(() => {
|
|
45
|
+
const stored = preference();
|
|
46
|
+
const resolved = stored === "system" ? (systemDark() ? "dark" : "light") : stored;
|
|
47
|
+
const root = document.documentElement;
|
|
48
|
+
root.setAttribute(attribute, resolved);
|
|
49
|
+
root.style.colorScheme = resolved;
|
|
50
|
+
if (storageKey) {
|
|
51
|
+
try {
|
|
52
|
+
window.localStorage.setItem(storageKey, preference());
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
// Storage can be unavailable (private mode); the in-memory
|
|
56
|
+
// preference still works for the session.
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
const scheme = () => {
|
|
62
|
+
const p = preference();
|
|
63
|
+
if (p !== "system")
|
|
64
|
+
return p;
|
|
65
|
+
if (typeof window === "undefined")
|
|
66
|
+
return "light";
|
|
67
|
+
return systemDark() ? "dark" : "light";
|
|
68
|
+
};
|
|
69
|
+
return {
|
|
70
|
+
scheme,
|
|
71
|
+
preference,
|
|
72
|
+
setPreference,
|
|
73
|
+
toggle: () => setPreference(scheme() === "dark" ? "light" : "dark"),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Idle detection. `idle()` becomes true after `timeout` ms without any
|
|
78
|
+
* of the activity events; any activity flips it back and restarts the
|
|
79
|
+
* timer. Useful for auto-hiding chrome, pausing ambient animation, or
|
|
80
|
+
* "are you still there" prompts.
|
|
81
|
+
*
|
|
82
|
+
* SSR-safe: never idle on the server.
|
|
83
|
+
*
|
|
84
|
+
* ```tsx
|
|
85
|
+
* const { idle } = createIdle({ timeout: 30_000 });
|
|
86
|
+
* <div classList={{ "controls-hidden": idle() }}>…</div>
|
|
87
|
+
* ```
|
|
88
|
+
*/
|
|
89
|
+
export function createIdle(options = {}) {
|
|
90
|
+
const { timeout = 60000, events = ["mousemove", "mousedown", "keydown", "touchstart", "wheel"], initialIdle = false, } = options;
|
|
91
|
+
if (typeof window === "undefined") {
|
|
92
|
+
const falsy = () => false;
|
|
93
|
+
return { idle: falsy, lastActive: () => 0, reset: () => { } };
|
|
94
|
+
}
|
|
95
|
+
const [idle, setIdle] = createSignal(initialIdle);
|
|
96
|
+
const [lastActive, setLastActive] = createSignal(Date.now());
|
|
97
|
+
let timer;
|
|
98
|
+
const arm = () => {
|
|
99
|
+
clearTimeout(timer);
|
|
100
|
+
timer = setTimeout(() => setIdle(true), Math.max(timeout, 0));
|
|
101
|
+
};
|
|
102
|
+
const onActivity = () => {
|
|
103
|
+
setLastActive(Date.now());
|
|
104
|
+
setIdle(false);
|
|
105
|
+
arm();
|
|
106
|
+
};
|
|
107
|
+
createEffect(() => {
|
|
108
|
+
for (const name of events) {
|
|
109
|
+
window.addEventListener(name, onActivity, { passive: true });
|
|
110
|
+
}
|
|
111
|
+
arm();
|
|
112
|
+
onCleanup(() => {
|
|
113
|
+
for (const name of events) {
|
|
114
|
+
window.removeEventListener(name, onActivity);
|
|
115
|
+
}
|
|
116
|
+
clearTimeout(timer);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
return { idle, lastActive, reset: onActivity };
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Network connectivity state. Seeds from `navigator.onLine` and follows
|
|
123
|
+
* the window `online` / `offline` events. SSR-safe: assumes online on
|
|
124
|
+
* the server.
|
|
125
|
+
*
|
|
126
|
+
* ```tsx
|
|
127
|
+
* const { online } = createOnline();
|
|
128
|
+
* <Show when={!online()}>
|
|
129
|
+
* <p>You are offline. Changes will sync when you reconnect.</p>
|
|
130
|
+
* </Show>
|
|
131
|
+
* ```
|
|
132
|
+
*/
|
|
133
|
+
export function createOnline() {
|
|
134
|
+
if (typeof window === "undefined")
|
|
135
|
+
return { online: () => true };
|
|
136
|
+
const [online, setOnline] = createSignal(typeof navigator !== "undefined" ? navigator.onLine : true);
|
|
137
|
+
createEffect(() => {
|
|
138
|
+
const goOnline = () => {
|
|
139
|
+
setOnline(true);
|
|
140
|
+
};
|
|
141
|
+
const goOffline = () => {
|
|
142
|
+
setOnline(false);
|
|
143
|
+
};
|
|
144
|
+
window.addEventListener("online", goOnline);
|
|
145
|
+
window.addEventListener("offline", goOffline);
|
|
146
|
+
onCleanup(() => {
|
|
147
|
+
window.removeEventListener("online", goOnline);
|
|
148
|
+
window.removeEventListener("offline", goOffline);
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
return { online };
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* PWA install prompt. Captures the `beforeinstallprompt` event (calling
|
|
155
|
+
* `preventDefault()` so the browser does not show its own mini-bar) and
|
|
156
|
+
* exposes it through `prompt()`, which is safe to call from a click
|
|
157
|
+
* handler. SSR-safe: never installable on the server.
|
|
158
|
+
*
|
|
159
|
+
* ```tsx
|
|
160
|
+
* const install = createInstallPrompt();
|
|
161
|
+
* <Show when={install.canInstall()}>
|
|
162
|
+
* <button
|
|
163
|
+
* onClick={async () => {
|
|
164
|
+
* const choice = await install.prompt();
|
|
165
|
+
* if (choice?.outcome === "accepted") toast("Installed!");
|
|
166
|
+
* }}
|
|
167
|
+
* >
|
|
168
|
+
* Install app
|
|
169
|
+
* </button>
|
|
170
|
+
* </Show>
|
|
171
|
+
* ```
|
|
172
|
+
*/
|
|
173
|
+
export function createInstallPrompt() {
|
|
174
|
+
if (typeof window === "undefined") {
|
|
175
|
+
return { canInstall: () => false, prompt: async () => null };
|
|
176
|
+
}
|
|
177
|
+
const [deferred, setDeferred] = createSignal(null);
|
|
178
|
+
createEffect(() => {
|
|
179
|
+
const onBeforeInstall = (event) => {
|
|
180
|
+
event.preventDefault();
|
|
181
|
+
setDeferred(event);
|
|
182
|
+
};
|
|
183
|
+
window.addEventListener("beforeinstallprompt", onBeforeInstall);
|
|
184
|
+
onCleanup(() => window.removeEventListener("beforeinstallprompt", onBeforeInstall));
|
|
185
|
+
});
|
|
186
|
+
const prompt = async () => {
|
|
187
|
+
const event = deferred();
|
|
188
|
+
if (!event)
|
|
189
|
+
return null;
|
|
190
|
+
setDeferred(null);
|
|
191
|
+
await event.prompt();
|
|
192
|
+
return event.userChoice;
|
|
193
|
+
};
|
|
194
|
+
return { canInstall: () => deferred() !== null, prompt };
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Undoable state. `set()` records the previous value, `undo()` steps
|
|
198
|
+
* back through history, `redo()` steps forward, and any new `set()`
|
|
199
|
+
* discards the redo stack. Pure logic: no DOM, SSR-safe.
|
|
200
|
+
*
|
|
201
|
+
* ```tsx
|
|
202
|
+
* const doc = createUndo({ title: "", body: "" }, { capacity: 100 });
|
|
203
|
+
* <input
|
|
204
|
+
* value={doc.value().title}
|
|
205
|
+
* onInput={(e) => doc.set((d) => ({ ...d, title: e.target.value }))}
|
|
206
|
+
* />
|
|
207
|
+
* <button disabled={!doc.canUndo()} onClick={doc.undo}>Undo</button>
|
|
208
|
+
* <button disabled={!doc.canRedo()} onClick={doc.redo}>Redo</button>
|
|
209
|
+
* ```
|
|
210
|
+
*/
|
|
211
|
+
export function createUndo(initial, options = {}) {
|
|
212
|
+
const { capacity = 50 } = options;
|
|
213
|
+
const limit = Math.max(Math.floor(capacity), 1);
|
|
214
|
+
const [value, setValue] = createSignal(initial);
|
|
215
|
+
const [past, setPast] = createSignal([]);
|
|
216
|
+
const [future, setFuture] = createSignal([]);
|
|
217
|
+
const set = (next) => {
|
|
218
|
+
const current = untrack(value);
|
|
219
|
+
const resolved = typeof next === "function" ? next(current) : next;
|
|
220
|
+
setPast((stack) => [...stack.slice(-(limit - 1)), current]);
|
|
221
|
+
setFuture([]);
|
|
222
|
+
setValue(resolved);
|
|
223
|
+
};
|
|
224
|
+
const undo = () => {
|
|
225
|
+
const stack = untrack(past);
|
|
226
|
+
if (stack.length === 0)
|
|
227
|
+
return;
|
|
228
|
+
const previous = stack[stack.length - 1];
|
|
229
|
+
setFuture((redo) => [untrack(value), ...redo]);
|
|
230
|
+
setPast(stack.slice(0, -1));
|
|
231
|
+
setValue(previous);
|
|
232
|
+
};
|
|
233
|
+
const redo = () => {
|
|
234
|
+
const stack = untrack(future);
|
|
235
|
+
if (stack.length === 0)
|
|
236
|
+
return;
|
|
237
|
+
const [next, ...rest] = stack;
|
|
238
|
+
setPast((done) => [...done.slice(-(limit - 1)), untrack(value)]);
|
|
239
|
+
setFuture(rest);
|
|
240
|
+
setValue(next);
|
|
241
|
+
};
|
|
242
|
+
const clear = () => {
|
|
243
|
+
setPast([]);
|
|
244
|
+
setFuture([]);
|
|
245
|
+
};
|
|
246
|
+
const reset = () => {
|
|
247
|
+
clear();
|
|
248
|
+
setValue(initial);
|
|
249
|
+
};
|
|
250
|
+
return {
|
|
251
|
+
value,
|
|
252
|
+
set,
|
|
253
|
+
undo,
|
|
254
|
+
redo,
|
|
255
|
+
clear,
|
|
256
|
+
reset,
|
|
257
|
+
canUndo: () => past().length > 0,
|
|
258
|
+
canRedo: () => future().length > 0,
|
|
259
|
+
past: past,
|
|
260
|
+
future: future,
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Fullscreen for an element. Tracks `document.fullscreenElement` so the
|
|
265
|
+
* state stays correct when the user presses Escape or another element
|
|
266
|
+
* takes over. Failures (unsupported browser, denied request) go to
|
|
267
|
+
* `onError` instead of throwing. SSR-safe: never fullscreen on the
|
|
268
|
+
* server.
|
|
269
|
+
*
|
|
270
|
+
* ```tsx
|
|
271
|
+
* let stage!: HTMLDivElement;
|
|
272
|
+
* const fs = createFullscreen(() => stage);
|
|
273
|
+
* <button onClick={fs.toggle}>
|
|
274
|
+
* {fs.fullscreen() ? "Exit fullscreen" : "Go fullscreen"}
|
|
275
|
+
* </button>
|
|
276
|
+
* <div ref={stage}>…</div>
|
|
277
|
+
* ```
|
|
278
|
+
*/
|
|
279
|
+
export function createFullscreen(ref, options = {}) {
|
|
280
|
+
const { onError } = options;
|
|
281
|
+
if (typeof window === "undefined") {
|
|
282
|
+
const falsy = () => false;
|
|
283
|
+
const noop = async () => { };
|
|
284
|
+
return { fullscreen: falsy, enter: noop, exit: noop, toggle: noop };
|
|
285
|
+
}
|
|
286
|
+
const [fullscreen, setFullscreen] = createSignal(false);
|
|
287
|
+
const sync = () => {
|
|
288
|
+
const el = ref();
|
|
289
|
+
setFullscreen(el != null && document.fullscreenElement === el);
|
|
290
|
+
};
|
|
291
|
+
createEffect(() => {
|
|
292
|
+
sync();
|
|
293
|
+
document.addEventListener("fullscreenchange", sync);
|
|
294
|
+
onCleanup(() => document.removeEventListener("fullscreenchange", sync));
|
|
295
|
+
});
|
|
296
|
+
const enter = async () => {
|
|
297
|
+
const el = ref();
|
|
298
|
+
if (!el || typeof el.requestFullscreen !== "function") {
|
|
299
|
+
onError?.(new Error("Fullscreen is not supported"));
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
try {
|
|
303
|
+
await el.requestFullscreen();
|
|
304
|
+
}
|
|
305
|
+
catch (error) {
|
|
306
|
+
onError?.(error);
|
|
307
|
+
}
|
|
308
|
+
};
|
|
309
|
+
const exit = async () => {
|
|
310
|
+
if (!document.fullscreenElement)
|
|
311
|
+
return;
|
|
312
|
+
try {
|
|
313
|
+
await document.exitFullscreen();
|
|
314
|
+
}
|
|
315
|
+
catch (error) {
|
|
316
|
+
onError?.(error);
|
|
317
|
+
}
|
|
318
|
+
};
|
|
319
|
+
const toggle = async () => {
|
|
320
|
+
if (fullscreen())
|
|
321
|
+
await exit();
|
|
322
|
+
else
|
|
323
|
+
await enter();
|
|
324
|
+
};
|
|
325
|
+
return { fullscreen, enter, exit, toggle };
|
|
326
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -64,3 +64,5 @@ export { createPathDraw } from "./pathdraw.js";
|
|
|
64
64
|
export type { PathDrawOptions, PathDrawControls } from "./pathdraw.js";
|
|
65
65
|
export { createPress, createHover } from "./press.js";
|
|
66
66
|
export type { PressOptions, PressControls, HoverOptions, HoverControls, } from "./press.js";
|
|
67
|
+
export { createColorScheme, createIdle, createOnline, createInstallPrompt, createUndo, createFullscreen, } from "./apputils.js";
|
|
68
|
+
export type { ColorScheme, ColorSchemePreference, ColorSchemeOptions, ColorSchemeControls, IdleOptions, IdleControls, OnlineControls, BeforeInstallPromptEvent, InstallPromptControls, UndoOptions, UndoControls, FullscreenOptions, FullscreenControls, } from "./apputils.js";
|
package/dist/index.js
CHANGED
|
@@ -48,3 +48,4 @@ export { createMarquee } from "./marquee.js";
|
|
|
48
48
|
export { createVariants } from "./variants.js";
|
|
49
49
|
export { createPathDraw } from "./pathdraw.js";
|
|
50
50
|
export { createPress, createHover } from "./press.js";
|
|
51
|
+
export { createColorScheme, createIdle, createOnline, createInstallPrompt, createUndo, createFullscreen, } from "./apputils.js";
|
package/package.json
CHANGED