@idle-screens/core 0.1.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/LICENSE +21 -0
- package/README.md +5 -0
- package/dist/index.d.ts +320 -0
- package/dist/index.js +760 -0
- package/dist/index.js.map +1 -0
- package/package.json +50 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Shawn Partridge
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
import { ReadonlySignal } from '@preact/signals-core';
|
|
2
|
+
export { ReadonlySignal, Signal, batch, computed, effect, signal, untracked } from '@preact/signals-core';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A tiny, fast, seedable PRNG (splitmix32). Deterministic: the same seed always
|
|
6
|
+
* yields the same sequence, on any machine. This is what makes "same program +
|
|
7
|
+
* seed + control-track = identical frames" hold. Savers take an `Rng` from their
|
|
8
|
+
* `SaverContext` instead of calling `Math.random()`.
|
|
9
|
+
*/
|
|
10
|
+
interface Rng {
|
|
11
|
+
/** Next float in [0, 1). */
|
|
12
|
+
next(): number;
|
|
13
|
+
/** Float in [min, max). */
|
|
14
|
+
range(min: number, max: number): number;
|
|
15
|
+
/** Integer in [min, max] inclusive. */
|
|
16
|
+
int(min: number, max: number): number;
|
|
17
|
+
/** Random element of a non-empty array. */
|
|
18
|
+
pick<T>(arr: readonly T[]): T;
|
|
19
|
+
/** A fresh independent stream (e.g. per subsystem), derived from this seed. */
|
|
20
|
+
fork(salt: number): Rng;
|
|
21
|
+
}
|
|
22
|
+
declare function createRng(seed: number): Rng;
|
|
23
|
+
|
|
24
|
+
/** How a parameter interpolates between control-track deltas. */
|
|
25
|
+
type Ease = 'step' | 'linear' | 'smooth';
|
|
26
|
+
type ParamType = 'number' | 'color' | 'bool' | 'enum';
|
|
27
|
+
type ParamValue = number | string | boolean;
|
|
28
|
+
/** One typed, ranged, interpolatable knob: an agent's steering surface. */
|
|
29
|
+
interface ParamDef {
|
|
30
|
+
type: ParamType;
|
|
31
|
+
default: ParamValue;
|
|
32
|
+
min?: number;
|
|
33
|
+
max?: number;
|
|
34
|
+
options?: string[];
|
|
35
|
+
/** Default interpolation when a delta targets this knob. */
|
|
36
|
+
ease?: Ease;
|
|
37
|
+
}
|
|
38
|
+
type ParamSpace = Record<string, ParamDef>;
|
|
39
|
+
/** A sparse, timestamped, interpolatable parameter change. Agents stream THESE
|
|
40
|
+
* (over AG-UI state-patches / MCP resource updates), never frames. */
|
|
41
|
+
interface ParamDelta {
|
|
42
|
+
/** ms from track start. */
|
|
43
|
+
t: number;
|
|
44
|
+
/** key in the saver's paramSpace. */
|
|
45
|
+
path: string;
|
|
46
|
+
value: ParamValue;
|
|
47
|
+
ease?: Ease;
|
|
48
|
+
/** ramp duration ms from the previous value (0 = instant). */
|
|
49
|
+
dur?: number;
|
|
50
|
+
}
|
|
51
|
+
/** A recorded or live steering performance for a seeded program. */
|
|
52
|
+
interface ControlTrack {
|
|
53
|
+
program: string;
|
|
54
|
+
seed: number;
|
|
55
|
+
duration?: number;
|
|
56
|
+
loop?: boolean;
|
|
57
|
+
deltas: ParamDelta[];
|
|
58
|
+
}
|
|
59
|
+
/** Page-context seam: passthrough savers read/eat the live page through this,
|
|
60
|
+
* rather than querying `document` directly. */
|
|
61
|
+
interface PageContext {
|
|
62
|
+
/** Sample dominant colors of the page (for palette-reactive savers). */
|
|
63
|
+
palette(): string[];
|
|
64
|
+
/** Visible page blocks a passthrough saver may transform ("victims"). */
|
|
65
|
+
victims(selector: string): HTMLElement[];
|
|
66
|
+
}
|
|
67
|
+
/** What the host hands a saver when it mounts. */
|
|
68
|
+
interface SaverContext {
|
|
69
|
+
/** A full-viewport container the saver renders into. Canvas savers create a
|
|
70
|
+
* canvas inside it; DOM savers (toasters, DVD, messages) append elements. The
|
|
71
|
+
* host is cleared by the runtime on dispose. */
|
|
72
|
+
host: HTMLElement;
|
|
73
|
+
width: number;
|
|
74
|
+
height: number;
|
|
75
|
+
/** Seeded PRNG. Use this, never Math.random(), for determinism. */
|
|
76
|
+
rng: Rng;
|
|
77
|
+
seed: number;
|
|
78
|
+
reducedMotion: boolean;
|
|
79
|
+
/** Present for passthrough savers (over the live page). */
|
|
80
|
+
page?: PageContext;
|
|
81
|
+
}
|
|
82
|
+
/** A mounted, running saver visual. */
|
|
83
|
+
interface SaverInstance {
|
|
84
|
+
/** Freeze/unfreeze the render loop (paused, reduced-motion, or screensaver end). */
|
|
85
|
+
setPaused(paused: boolean): void;
|
|
86
|
+
resize(width: number, height: number): void;
|
|
87
|
+
/** Deterministic, frame-addressable render: draw the state at logical time `t`
|
|
88
|
+
* (ms) for `seed`. Pure w.r.t. (program, seed, applied track, t). Optional. */
|
|
89
|
+
renderFrame?(t: number, seed: number): void;
|
|
90
|
+
/** Steer parameters over time. Optional. */
|
|
91
|
+
applyTrack?(track: ControlTrack): void;
|
|
92
|
+
/** Tear down; MUST restore any live-page mutations. */
|
|
93
|
+
dispose(): void;
|
|
94
|
+
}
|
|
95
|
+
/** Self-describing metadata, readable WITHOUT executing the saver. */
|
|
96
|
+
interface SaverManifest {
|
|
97
|
+
id: string;
|
|
98
|
+
label: string;
|
|
99
|
+
passthrough?: boolean;
|
|
100
|
+
minBackend?: 'css' | 'canvas2d' | 'webgl2' | 'webgpu';
|
|
101
|
+
costTier?: 'idle' | 'low' | 'medium' | 'high';
|
|
102
|
+
motionIntensity?: 'calm' | 'moderate' | 'energetic';
|
|
103
|
+
reducedMotionFallback?: 'static' | 'slow' | 'hide';
|
|
104
|
+
palette?: string[];
|
|
105
|
+
paramSpace?: ParamSpace;
|
|
106
|
+
a11y?: {
|
|
107
|
+
flashSafe?: boolean;
|
|
108
|
+
notes?: string;
|
|
109
|
+
};
|
|
110
|
+
provenance?: {
|
|
111
|
+
prompt?: string;
|
|
112
|
+
seed?: number;
|
|
113
|
+
model?: string;
|
|
114
|
+
};
|
|
115
|
+
thumbnail?: string;
|
|
116
|
+
}
|
|
117
|
+
/** A registered saver: manifest + a (possibly lazy/async) mount factory. */
|
|
118
|
+
interface SaverPlugin {
|
|
119
|
+
manifest: SaverManifest;
|
|
120
|
+
mount(ctx: SaverContext): SaverInstance | Promise<SaverInstance>;
|
|
121
|
+
}
|
|
122
|
+
/** Engine configuration. */
|
|
123
|
+
interface IdleScreensConfig {
|
|
124
|
+
/** Idle time before sleeping, in ms. */
|
|
125
|
+
timeoutMs: number;
|
|
126
|
+
/** Also sleep when the window loses focus. */
|
|
127
|
+
sleepOnBlur: boolean;
|
|
128
|
+
/** Do not run on localhost (debug hooks bypass this). */
|
|
129
|
+
disableOnLocalhost: boolean;
|
|
130
|
+
/** Which registered saver to show. */
|
|
131
|
+
defaultPluginId: string;
|
|
132
|
+
/** How to pick across sleeps. */
|
|
133
|
+
selection: 'fixed' | 'random' | 'rotate';
|
|
134
|
+
/** Show the clock chrome (non-passthrough savers only). */
|
|
135
|
+
showClock: boolean;
|
|
136
|
+
/** Exact document title while asleep. */
|
|
137
|
+
sleepTitle?: string;
|
|
138
|
+
/** Master seed for plugin selection + the default saver seed. */
|
|
139
|
+
seed: number;
|
|
140
|
+
/** Return true for a URL where the saver must never sleep. */
|
|
141
|
+
suppress?: (url: string) => boolean;
|
|
142
|
+
/** Pluggable persistence (defaults to localStorage when available). */
|
|
143
|
+
storage?: {
|
|
144
|
+
get(key: string): string | null;
|
|
145
|
+
set(key: string, value: string): void;
|
|
146
|
+
};
|
|
147
|
+
/** Built-in, keyboard-opened config menu (saver picker). `true`/omitted =
|
|
148
|
+
* enabled with ⌘K / Ctrl+K; `false` = no hotkey and no built-in menu (the
|
|
149
|
+
* `configMenuOpen` signal + open/close API still work, so a host may render
|
|
150
|
+
* its own). Pass an object to reconfigure. */
|
|
151
|
+
configMenu?: boolean | ConfigMenuConfig;
|
|
152
|
+
}
|
|
153
|
+
/** Options for the built-in, keyboard-opened config menu. */
|
|
154
|
+
interface ConfigMenuConfig {
|
|
155
|
+
/** Master switch. Default true. */
|
|
156
|
+
enabled?: boolean;
|
|
157
|
+
/** Decides whether a keydown opens/toggles the menu. Default: ⌘K / Ctrl+K. */
|
|
158
|
+
hotkey?: (e: KeyboardEvent) => boolean;
|
|
159
|
+
/** Show the saver picker control. Default true. */
|
|
160
|
+
showPicker?: boolean;
|
|
161
|
+
/** Preview the picked saver immediately (sleep to show it). Default true. */
|
|
162
|
+
previewOnPick?: boolean;
|
|
163
|
+
/** Heading text. Default 'Screen Saver'. */
|
|
164
|
+
title?: string;
|
|
165
|
+
}
|
|
166
|
+
/** Default menu hotkey: ⌘K (mac) / Ctrl+K, no other modifiers. */
|
|
167
|
+
declare const DEFAULT_MENU_HOTKEY: (e: KeyboardEvent) => boolean;
|
|
168
|
+
/** Normalize `configMenu` to a filled config, or null when disabled. */
|
|
169
|
+
declare function resolveConfigMenu(c: boolean | ConfigMenuConfig | undefined): Required<ConfigMenuConfig> | null;
|
|
170
|
+
declare const DEFAULT_CONFIG: IdleScreensConfig;
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Sample a control track: given the saver's param space (defaults + types) and a
|
|
174
|
+
* track, return the interpolated parameter values at logical time `t` (ms). Pure
|
|
175
|
+
* and deterministic: identical `(space, track, t)` yields identical output.
|
|
176
|
+
*/
|
|
177
|
+
declare function sampleTrack(space: ParamSpace, track: ControlTrack, t: number): Record<string, ParamValue>;
|
|
178
|
+
/** Default param values (the "resting" program before any track is applied). */
|
|
179
|
+
declare function defaultParams(space: ParamSpace): Record<string, ParamValue>;
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Detects user inactivity. `idle` flips true once no activity for `timeoutMs`.
|
|
183
|
+
* Framework-agnostic port of the Angular IdleService: 6 activity events + a
|
|
184
|
+
* single `setTimeout` (not a poll) whose reschedule is throttled to >=500ms so
|
|
185
|
+
* frequent pointermove events do not thrash timers.
|
|
186
|
+
*/
|
|
187
|
+
declare class IdleDetector {
|
|
188
|
+
private readonly timeoutMs;
|
|
189
|
+
private readonly _idle;
|
|
190
|
+
readonly idle: ReadonlySignal<boolean>;
|
|
191
|
+
private timer;
|
|
192
|
+
private lastReset;
|
|
193
|
+
private running;
|
|
194
|
+
private static readonly EVENTS;
|
|
195
|
+
constructor(timeoutMs: number);
|
|
196
|
+
private readonly onActivity;
|
|
197
|
+
start(): void;
|
|
198
|
+
stop(): void;
|
|
199
|
+
/** Force an activity pulse (resets the countdown, wakes from idle). */
|
|
200
|
+
markActive(): void;
|
|
201
|
+
private schedule;
|
|
202
|
+
private clear;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
type EngineState = 'awake' | 'sleeping';
|
|
206
|
+
/**
|
|
207
|
+
* The framework-agnostic idle-screens state machine (port of the Angular
|
|
208
|
+
* ScreensaverService). Owns: awake/sleeping state, plugin selection, the sleep
|
|
209
|
+
* clock, reduced-motion + visibility, idle/blur triggers, and the
|
|
210
|
+
* `window.__idleScreens` debug hook. It does NOT render; the `<idle-screen>`
|
|
211
|
+
* element observes its signals.
|
|
212
|
+
*/
|
|
213
|
+
declare class IdleScreensEngine {
|
|
214
|
+
readonly config: IdleScreensConfig;
|
|
215
|
+
private readonly plugins;
|
|
216
|
+
private readonly storage;
|
|
217
|
+
private readonly idle;
|
|
218
|
+
private readonly selectionRng;
|
|
219
|
+
private readonly _state;
|
|
220
|
+
readonly state: ReadonlySignal<EngineState>;
|
|
221
|
+
readonly isSleeping: ReadonlySignal<boolean>;
|
|
222
|
+
private readonly _activeId;
|
|
223
|
+
readonly activePlugin: ReadonlySignal<SaverPlugin | null>;
|
|
224
|
+
readonly activeIsPassthrough: ReadonlySignal<boolean>;
|
|
225
|
+
private readonly _now;
|
|
226
|
+
readonly now: ReadonlySignal<Date>;
|
|
227
|
+
private readonly _hidden;
|
|
228
|
+
readonly hidden: ReadonlySignal<boolean>;
|
|
229
|
+
private readonly _reducedMotion;
|
|
230
|
+
readonly reducedMotion: ReadonlySignal<boolean>;
|
|
231
|
+
/** Resolved built-in config-menu options, or null when disabled. */
|
|
232
|
+
readonly configMenu: Required<ConfigMenuConfig> | null;
|
|
233
|
+
private readonly _configMenuOpen;
|
|
234
|
+
/** Whether the config menu is open. Drives the built-in `<idle-screen>` menu;
|
|
235
|
+
* a host may also observe this to render its own. */
|
|
236
|
+
readonly configMenuOpen: ReadonlySignal<boolean>;
|
|
237
|
+
private clockTimer;
|
|
238
|
+
private started;
|
|
239
|
+
private disposers;
|
|
240
|
+
constructor(config: Partial<IdleScreensConfig>, plugins: SaverPlugin[]);
|
|
241
|
+
get pluginList(): Array<{
|
|
242
|
+
id: string;
|
|
243
|
+
label: string;
|
|
244
|
+
}>;
|
|
245
|
+
/** Begin idle detection + listeners + debug hook. */
|
|
246
|
+
init(): void;
|
|
247
|
+
destroy(): void;
|
|
248
|
+
private suppressed;
|
|
249
|
+
sleep(): void;
|
|
250
|
+
/** Sleep ignoring suppression rules (debug / manual). */
|
|
251
|
+
forceSleep(): void;
|
|
252
|
+
wake(): void;
|
|
253
|
+
toggle(): void;
|
|
254
|
+
openConfigMenu(): void;
|
|
255
|
+
closeConfigMenu(): void;
|
|
256
|
+
toggleConfigMenu(): void;
|
|
257
|
+
setPlugin(id: string): void;
|
|
258
|
+
private pickPlugin;
|
|
259
|
+
private installDebugHook;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** Node-safe base: during SSR/prerender there is no `HTMLElement`, and importing
|
|
263
|
+
* this module (for the engine/types) must not crash. The element is only ever
|
|
264
|
+
* constructed/registered in the browser. */
|
|
265
|
+
declare const HostBase: typeof HTMLElement;
|
|
266
|
+
/**
|
|
267
|
+
* `<idle-screen>` — the framework-agnostic overlay (port of the Angular
|
|
268
|
+
* ScreensaverComponent). Owns the top-layer `<dialog>`, mounts the active saver
|
|
269
|
+
* into a canvas, shows the clock + "press any key" hint, fades in/out, wakes on
|
|
270
|
+
* input with a 450ms arm-guard, and goes transparent for passthrough savers.
|
|
271
|
+
*
|
|
272
|
+
* Configure by setting the `.config` and `.plugins` properties (Angular can do
|
|
273
|
+
* this via `[config]` / `[plugins]` with CUSTOM_ELEMENTS_SCHEMA).
|
|
274
|
+
*/
|
|
275
|
+
declare class IdleScreenElement extends HostBase {
|
|
276
|
+
private _engine;
|
|
277
|
+
private ownsEngine;
|
|
278
|
+
private _externalEngine;
|
|
279
|
+
private _config;
|
|
280
|
+
private _plugins;
|
|
281
|
+
private dialog;
|
|
282
|
+
private surface;
|
|
283
|
+
private clockEl;
|
|
284
|
+
private hintEl;
|
|
285
|
+
private menuDialog;
|
|
286
|
+
private menuSelect;
|
|
287
|
+
private menuBuilt;
|
|
288
|
+
private instance;
|
|
289
|
+
private mountToken;
|
|
290
|
+
private armed;
|
|
291
|
+
private armTimer;
|
|
292
|
+
private hintTimer;
|
|
293
|
+
private closeTimer;
|
|
294
|
+
private disposers;
|
|
295
|
+
set config(c: Partial<IdleScreensConfig>);
|
|
296
|
+
set plugins(p: SaverPlugin[]);
|
|
297
|
+
/** Use an externally-owned, already-initialized engine (e.g. an Angular service
|
|
298
|
+
* that also drives a config menu) instead of creating one from config+plugins. */
|
|
299
|
+
set engine(e: IdleScreensEngine);
|
|
300
|
+
/** The live engine (for host code / tests). */
|
|
301
|
+
get idleEngine(): IdleScreensEngine | null;
|
|
302
|
+
connectedCallback(): void;
|
|
303
|
+
disconnectedCallback(): void;
|
|
304
|
+
private buildDom;
|
|
305
|
+
private maybeStart;
|
|
306
|
+
private buildMenu;
|
|
307
|
+
private syncMenuSelection;
|
|
308
|
+
private pageContext;
|
|
309
|
+
private open;
|
|
310
|
+
private close;
|
|
311
|
+
private disposeInstance;
|
|
312
|
+
private arm;
|
|
313
|
+
private scheduleHint;
|
|
314
|
+
private onWakeInput;
|
|
315
|
+
private teardown;
|
|
316
|
+
}
|
|
317
|
+
/** Register the <idle-screen> custom element (idempotent). */
|
|
318
|
+
declare function defineIdleScreen(tag?: string): void;
|
|
319
|
+
|
|
320
|
+
export { type ConfigMenuConfig, type ControlTrack, DEFAULT_CONFIG, DEFAULT_MENU_HOTKEY, type Ease, type EngineState, IdleDetector, IdleScreenElement, type IdleScreensConfig, IdleScreensEngine, type PageContext, type ParamDef, type ParamDelta, type ParamSpace, type ParamType, type ParamValue, type Rng, type SaverContext, type SaverInstance, type SaverManifest, type SaverPlugin, createRng, defaultParams, defineIdleScreen, resolveConfigMenu, sampleTrack };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,760 @@
|
|
|
1
|
+
// src/types.ts
|
|
2
|
+
var DEFAULT_MENU_HOTKEY = (e) => (e.metaKey || e.ctrlKey) && !e.altKey && !e.shiftKey && e.key.toLowerCase() === "k";
|
|
3
|
+
function resolveConfigMenu(c) {
|
|
4
|
+
if (c === false) return null;
|
|
5
|
+
const o = c === true || c === void 0 ? {} : c;
|
|
6
|
+
if (o.enabled === false) return null;
|
|
7
|
+
return {
|
|
8
|
+
enabled: true,
|
|
9
|
+
hotkey: o.hotkey ?? DEFAULT_MENU_HOTKEY,
|
|
10
|
+
showPicker: o.showPicker ?? true,
|
|
11
|
+
previewOnPick: o.previewOnPick ?? true,
|
|
12
|
+
title: o.title ?? "Screen Saver"
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
var DEFAULT_CONFIG = {
|
|
16
|
+
timeoutMs: 6e4,
|
|
17
|
+
sleepOnBlur: true,
|
|
18
|
+
disableOnLocalhost: false,
|
|
19
|
+
defaultPluginId: "",
|
|
20
|
+
selection: "fixed",
|
|
21
|
+
showClock: true,
|
|
22
|
+
seed: 1,
|
|
23
|
+
configMenu: true
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
// src/rng.ts
|
|
27
|
+
function createRng(seed) {
|
|
28
|
+
let a = seed >>> 0;
|
|
29
|
+
const next = () => {
|
|
30
|
+
a |= 0;
|
|
31
|
+
a = a + 2654435769 | 0;
|
|
32
|
+
let t = a ^ a >>> 16;
|
|
33
|
+
t = Math.imul(t, 569420461);
|
|
34
|
+
t = t ^ t >>> 15;
|
|
35
|
+
t = Math.imul(t, 1935289751);
|
|
36
|
+
return ((t = t ^ t >>> 15) >>> 0) / 4294967296;
|
|
37
|
+
};
|
|
38
|
+
const rng = {
|
|
39
|
+
next,
|
|
40
|
+
range: (min, max) => min + (max - min) * next(),
|
|
41
|
+
int: (min, max) => min + Math.floor(next() * (max - min + 1)),
|
|
42
|
+
pick: (arr) => arr[Math.floor(next() * arr.length)],
|
|
43
|
+
fork: (salt) => createRng((seed ^ Math.imul(salt + 1, 2654435769)) >>> 0)
|
|
44
|
+
};
|
|
45
|
+
return rng;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// src/reactive.ts
|
|
49
|
+
import {
|
|
50
|
+
signal,
|
|
51
|
+
computed,
|
|
52
|
+
effect,
|
|
53
|
+
batch,
|
|
54
|
+
untracked
|
|
55
|
+
} from "@preact/signals-core";
|
|
56
|
+
|
|
57
|
+
// src/control-track.ts
|
|
58
|
+
function easeFn(e, p) {
|
|
59
|
+
const x = p <= 0 ? 0 : p >= 1 ? 1 : p;
|
|
60
|
+
switch (e) {
|
|
61
|
+
case "step":
|
|
62
|
+
return x >= 1 ? 1 : 0;
|
|
63
|
+
case "smooth":
|
|
64
|
+
return x * x * (3 - 2 * x);
|
|
65
|
+
case "linear":
|
|
66
|
+
default:
|
|
67
|
+
return x;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
function lerp(a, b, p) {
|
|
71
|
+
return a + (b - a) * p;
|
|
72
|
+
}
|
|
73
|
+
function parseHex(c) {
|
|
74
|
+
const m = /^#?([0-9a-f]{6})$/i.exec(c.trim());
|
|
75
|
+
if (!m) return null;
|
|
76
|
+
const n = parseInt(m[1], 16);
|
|
77
|
+
return [n >> 16 & 255, n >> 8 & 255, n & 255];
|
|
78
|
+
}
|
|
79
|
+
function toHex(rgb) {
|
|
80
|
+
const h = (v) => Math.round(Math.max(0, Math.min(255, v))).toString(16).padStart(2, "0");
|
|
81
|
+
return `#${h(rgb[0])}${h(rgb[1])}${h(rgb[2])}`;
|
|
82
|
+
}
|
|
83
|
+
function lerpValue(type, a, b, p) {
|
|
84
|
+
if (type === "number" && typeof a === "number" && typeof b === "number") return lerp(a, b, p);
|
|
85
|
+
if (type === "color" && typeof a === "string" && typeof b === "string") {
|
|
86
|
+
const ca = parseHex(a);
|
|
87
|
+
const cb = parseHex(b);
|
|
88
|
+
if (ca && cb) return toHex([lerp(ca[0], cb[0], p), lerp(ca[1], cb[1], p), lerp(ca[2], cb[2], p)]);
|
|
89
|
+
}
|
|
90
|
+
return p >= 1 ? b : a;
|
|
91
|
+
}
|
|
92
|
+
function evalPath(dflt, type, defaultEase, deltas, t) {
|
|
93
|
+
if (deltas.length === 0) return dflt;
|
|
94
|
+
let prevVal = dflt;
|
|
95
|
+
let prevT = -Infinity;
|
|
96
|
+
for (const k of deltas) {
|
|
97
|
+
if (t < k.t) {
|
|
98
|
+
const rampStart = k.dur != null ? k.t - k.dur : prevT === -Infinity ? k.t : prevT;
|
|
99
|
+
if (t <= rampStart) return prevVal;
|
|
100
|
+
const p = easeFn(k.ease ?? defaultEase, (t - rampStart) / (k.t - rampStart || 1));
|
|
101
|
+
return lerpValue(type, prevVal, k.value, p);
|
|
102
|
+
}
|
|
103
|
+
prevVal = k.value;
|
|
104
|
+
prevT = k.t;
|
|
105
|
+
}
|
|
106
|
+
return prevVal;
|
|
107
|
+
}
|
|
108
|
+
function sampleTrack(space, track, t) {
|
|
109
|
+
const byPath = /* @__PURE__ */ new Map();
|
|
110
|
+
for (const d of track.deltas) {
|
|
111
|
+
const arr = byPath.get(d.path) ?? [];
|
|
112
|
+
arr.push(d);
|
|
113
|
+
byPath.set(d.path, arr);
|
|
114
|
+
}
|
|
115
|
+
const wrapped = track.loop && track.duration && track.duration > 0 ? t % track.duration : t;
|
|
116
|
+
const out = {};
|
|
117
|
+
for (const [path, def] of Object.entries(space)) {
|
|
118
|
+
const deltas = (byPath.get(path) ?? []).slice().sort((a, b) => a.t - b.t);
|
|
119
|
+
out[path] = evalPath(def.default, def.type, def.ease ?? "linear", deltas, wrapped);
|
|
120
|
+
}
|
|
121
|
+
return out;
|
|
122
|
+
}
|
|
123
|
+
function defaultParams(space) {
|
|
124
|
+
const out = {};
|
|
125
|
+
for (const [path, def] of Object.entries(space)) out[path] = def.default;
|
|
126
|
+
return out;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// src/idle-detector.ts
|
|
130
|
+
var IdleDetector = class _IdleDetector {
|
|
131
|
+
constructor(timeoutMs) {
|
|
132
|
+
this.timeoutMs = timeoutMs;
|
|
133
|
+
}
|
|
134
|
+
timeoutMs;
|
|
135
|
+
_idle = signal(false);
|
|
136
|
+
idle = this._idle;
|
|
137
|
+
timer = null;
|
|
138
|
+
lastReset = 0;
|
|
139
|
+
running = false;
|
|
140
|
+
static EVENTS = [
|
|
141
|
+
"pointerdown",
|
|
142
|
+
"pointermove",
|
|
143
|
+
"keydown",
|
|
144
|
+
"wheel",
|
|
145
|
+
"touchstart",
|
|
146
|
+
"scroll"
|
|
147
|
+
];
|
|
148
|
+
onActivity = () => this.markActive();
|
|
149
|
+
start() {
|
|
150
|
+
if (this.running || typeof window === "undefined") return;
|
|
151
|
+
this.running = true;
|
|
152
|
+
for (const e of _IdleDetector.EVENTS) {
|
|
153
|
+
window.addEventListener(e, this.onActivity, { passive: true });
|
|
154
|
+
}
|
|
155
|
+
this.schedule();
|
|
156
|
+
}
|
|
157
|
+
stop() {
|
|
158
|
+
if (!this.running) return;
|
|
159
|
+
this.running = false;
|
|
160
|
+
for (const e of _IdleDetector.EVENTS) {
|
|
161
|
+
window.removeEventListener(e, this.onActivity);
|
|
162
|
+
}
|
|
163
|
+
this.clear();
|
|
164
|
+
}
|
|
165
|
+
/** Force an activity pulse (resets the countdown, wakes from idle). */
|
|
166
|
+
markActive() {
|
|
167
|
+
if (this._idle.value) this._idle.value = false;
|
|
168
|
+
const now = typeof performance !== "undefined" ? performance.now() : Date.now();
|
|
169
|
+
if (now - this.lastReset < 500) return;
|
|
170
|
+
this.lastReset = now;
|
|
171
|
+
this.schedule();
|
|
172
|
+
}
|
|
173
|
+
schedule() {
|
|
174
|
+
this.clear();
|
|
175
|
+
this.timer = setTimeout(() => {
|
|
176
|
+
this._idle.value = true;
|
|
177
|
+
}, this.timeoutMs);
|
|
178
|
+
}
|
|
179
|
+
clear() {
|
|
180
|
+
if (this.timer !== null) {
|
|
181
|
+
clearTimeout(this.timer);
|
|
182
|
+
this.timer = null;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
// src/engine.ts
|
|
188
|
+
var hasWindow = typeof window !== "undefined";
|
|
189
|
+
var hasDoc = typeof document !== "undefined";
|
|
190
|
+
function defaultStorage() {
|
|
191
|
+
return {
|
|
192
|
+
get: (k) => {
|
|
193
|
+
try {
|
|
194
|
+
return hasWindow ? window.localStorage.getItem(k) : null;
|
|
195
|
+
} catch {
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
},
|
|
199
|
+
set: (k, v) => {
|
|
200
|
+
try {
|
|
201
|
+
if (hasWindow) window.localStorage.setItem(k, v);
|
|
202
|
+
} catch {
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
var IdleScreensEngine = class {
|
|
208
|
+
config;
|
|
209
|
+
plugins;
|
|
210
|
+
storage;
|
|
211
|
+
idle;
|
|
212
|
+
selectionRng;
|
|
213
|
+
_state = signal("awake");
|
|
214
|
+
state = this._state;
|
|
215
|
+
isSleeping = computed(() => this._state.value === "sleeping");
|
|
216
|
+
_activeId = signal("");
|
|
217
|
+
activePlugin = computed(
|
|
218
|
+
() => this.plugins.find((p) => p.manifest.id === this._activeId.value) ?? this.plugins[0] ?? null
|
|
219
|
+
);
|
|
220
|
+
activeIsPassthrough = computed(
|
|
221
|
+
() => this.activePlugin.value?.manifest.passthrough ?? false
|
|
222
|
+
);
|
|
223
|
+
_now = signal(/* @__PURE__ */ new Date(0));
|
|
224
|
+
now = this._now;
|
|
225
|
+
_hidden = signal(false);
|
|
226
|
+
hidden = this._hidden;
|
|
227
|
+
_reducedMotion = signal(false);
|
|
228
|
+
reducedMotion = this._reducedMotion;
|
|
229
|
+
/** Resolved built-in config-menu options, or null when disabled. */
|
|
230
|
+
configMenu;
|
|
231
|
+
_configMenuOpen = signal(false);
|
|
232
|
+
/** Whether the config menu is open. Drives the built-in `<idle-screen>` menu;
|
|
233
|
+
* a host may also observe this to render its own. */
|
|
234
|
+
configMenuOpen = this._configMenuOpen;
|
|
235
|
+
clockTimer = null;
|
|
236
|
+
started = false;
|
|
237
|
+
disposers = [];
|
|
238
|
+
constructor(config, plugins) {
|
|
239
|
+
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
240
|
+
this.plugins = plugins;
|
|
241
|
+
this.configMenu = resolveConfigMenu(this.config.configMenu);
|
|
242
|
+
this.storage = this.config.storage ?? defaultStorage();
|
|
243
|
+
this.idle = new IdleDetector(this.config.timeoutMs);
|
|
244
|
+
this.selectionRng = createRng(this.config.seed >>> 0);
|
|
245
|
+
const stored = this.storage.get("idleScreens.plugin");
|
|
246
|
+
const initialId = stored && plugins.some((p) => p.manifest.id === stored) && stored || this.config.defaultPluginId || plugins[0]?.manifest.id || "";
|
|
247
|
+
this._activeId.value = initialId;
|
|
248
|
+
}
|
|
249
|
+
get pluginList() {
|
|
250
|
+
return this.plugins.map((p) => ({ id: p.manifest.id, label: p.manifest.label }));
|
|
251
|
+
}
|
|
252
|
+
/** Begin idle detection + listeners + debug hook. */
|
|
253
|
+
init() {
|
|
254
|
+
if (this.started || !hasWindow) return;
|
|
255
|
+
this.started = true;
|
|
256
|
+
if (hasDoc) {
|
|
257
|
+
this._hidden.value = document.hidden;
|
|
258
|
+
const onVis = () => {
|
|
259
|
+
this._hidden.value = document.hidden;
|
|
260
|
+
};
|
|
261
|
+
document.addEventListener("visibilitychange", onVis);
|
|
262
|
+
this.disposers.push(() => document.removeEventListener("visibilitychange", onVis));
|
|
263
|
+
}
|
|
264
|
+
const mq = hasWindow ? window.matchMedia("(prefers-reduced-motion: reduce)") : null;
|
|
265
|
+
if (mq) {
|
|
266
|
+
this._reducedMotion.value = mq.matches;
|
|
267
|
+
const onMq = () => {
|
|
268
|
+
this._reducedMotion.value = mq.matches;
|
|
269
|
+
};
|
|
270
|
+
mq.addEventListener("change", onMq);
|
|
271
|
+
this.disposers.push(() => mq.removeEventListener("change", onMq));
|
|
272
|
+
}
|
|
273
|
+
this.idle.start();
|
|
274
|
+
this.disposers.push(
|
|
275
|
+
effect(() => {
|
|
276
|
+
if (this.idle.idle.value) untracked(() => this.sleep());
|
|
277
|
+
})
|
|
278
|
+
);
|
|
279
|
+
if (this.config.sleepOnBlur) {
|
|
280
|
+
const onBlur = () => this.sleep();
|
|
281
|
+
window.addEventListener("blur", onBlur);
|
|
282
|
+
this.disposers.push(() => window.removeEventListener("blur", onBlur));
|
|
283
|
+
}
|
|
284
|
+
if (this.configMenu) {
|
|
285
|
+
const hotkey = this.configMenu.hotkey;
|
|
286
|
+
const onKey = (e) => {
|
|
287
|
+
if (hotkey(e)) {
|
|
288
|
+
e.preventDefault();
|
|
289
|
+
this.toggleConfigMenu();
|
|
290
|
+
}
|
|
291
|
+
};
|
|
292
|
+
window.addEventListener("keydown", onKey);
|
|
293
|
+
this.disposers.push(() => window.removeEventListener("keydown", onKey));
|
|
294
|
+
}
|
|
295
|
+
this.disposers.push(
|
|
296
|
+
effect(() => {
|
|
297
|
+
const ticking = this._state.value === "sleeping" && !this._hidden.value;
|
|
298
|
+
if (ticking && this.clockTimer === null) {
|
|
299
|
+
this._now.value = /* @__PURE__ */ new Date();
|
|
300
|
+
this.clockTimer = setInterval(() => {
|
|
301
|
+
this._now.value = /* @__PURE__ */ new Date();
|
|
302
|
+
}, 1e3);
|
|
303
|
+
} else if (!ticking && this.clockTimer !== null) {
|
|
304
|
+
clearInterval(this.clockTimer);
|
|
305
|
+
this.clockTimer = null;
|
|
306
|
+
}
|
|
307
|
+
})
|
|
308
|
+
);
|
|
309
|
+
this.installDebugHook();
|
|
310
|
+
}
|
|
311
|
+
destroy() {
|
|
312
|
+
this.idle.stop();
|
|
313
|
+
if (this.clockTimer !== null) {
|
|
314
|
+
clearInterval(this.clockTimer);
|
|
315
|
+
this.clockTimer = null;
|
|
316
|
+
}
|
|
317
|
+
for (const d of this.disposers) d();
|
|
318
|
+
this.disposers = [];
|
|
319
|
+
this.started = false;
|
|
320
|
+
}
|
|
321
|
+
suppressed() {
|
|
322
|
+
if (this.config.disableOnLocalhost && hasWindow && /^(localhost|127\.|\[::1\])/.test(location.hostname)) {
|
|
323
|
+
return true;
|
|
324
|
+
}
|
|
325
|
+
const url = hasWindow ? location.pathname + location.search : "";
|
|
326
|
+
return this.config.suppress ? this.config.suppress(url) : false;
|
|
327
|
+
}
|
|
328
|
+
sleep() {
|
|
329
|
+
if (this._state.value === "sleeping" || this.suppressed()) return;
|
|
330
|
+
this.pickPlugin();
|
|
331
|
+
this._state.value = "sleeping";
|
|
332
|
+
}
|
|
333
|
+
/** Sleep ignoring suppression rules (debug / manual). */
|
|
334
|
+
forceSleep() {
|
|
335
|
+
if (this._state.value === "sleeping") return;
|
|
336
|
+
this.pickPlugin();
|
|
337
|
+
this._state.value = "sleeping";
|
|
338
|
+
}
|
|
339
|
+
wake() {
|
|
340
|
+
if (this._state.value === "awake") return;
|
|
341
|
+
this._state.value = "awake";
|
|
342
|
+
this.idle.markActive();
|
|
343
|
+
}
|
|
344
|
+
toggle() {
|
|
345
|
+
if (this.isSleeping.value) this.wake();
|
|
346
|
+
else this.forceSleep();
|
|
347
|
+
}
|
|
348
|
+
openConfigMenu() {
|
|
349
|
+
this._configMenuOpen.value = true;
|
|
350
|
+
}
|
|
351
|
+
closeConfigMenu() {
|
|
352
|
+
this._configMenuOpen.value = false;
|
|
353
|
+
}
|
|
354
|
+
toggleConfigMenu() {
|
|
355
|
+
this._configMenuOpen.value = !this._configMenuOpen.value;
|
|
356
|
+
}
|
|
357
|
+
setPlugin(id) {
|
|
358
|
+
if (!this.plugins.some((p) => p.manifest.id === id)) return;
|
|
359
|
+
this._activeId.value = id;
|
|
360
|
+
this.storage.set("idleScreens.plugin", id);
|
|
361
|
+
}
|
|
362
|
+
pickPlugin() {
|
|
363
|
+
if (this.plugins.length === 0) return;
|
|
364
|
+
switch (this.config.selection) {
|
|
365
|
+
case "random": {
|
|
366
|
+
const i = this.selectionRng.int(0, this.plugins.length - 1);
|
|
367
|
+
this._activeId.value = this.plugins[i].manifest.id;
|
|
368
|
+
break;
|
|
369
|
+
}
|
|
370
|
+
case "rotate": {
|
|
371
|
+
const cur = this.plugins.findIndex((p) => p.manifest.id === this._activeId.value);
|
|
372
|
+
const next = this.plugins[(cur + 1) % this.plugins.length];
|
|
373
|
+
this._activeId.value = next.manifest.id;
|
|
374
|
+
break;
|
|
375
|
+
}
|
|
376
|
+
case "fixed":
|
|
377
|
+
default:
|
|
378
|
+
break;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
installDebugHook() {
|
|
382
|
+
if (!hasWindow) return;
|
|
383
|
+
window.__idleScreens = {
|
|
384
|
+
sleep: () => this.forceSleep(),
|
|
385
|
+
wake: () => this.wake(),
|
|
386
|
+
toggle: () => this.toggle(),
|
|
387
|
+
setPlugin: (id) => this.setPlugin(id),
|
|
388
|
+
openMenu: () => this.openConfigMenu(),
|
|
389
|
+
closeMenu: () => this.closeConfigMenu(),
|
|
390
|
+
toggleMenu: () => this.toggleConfigMenu(),
|
|
391
|
+
state: () => this._state.value,
|
|
392
|
+
menuOpen: () => this._configMenuOpen.value,
|
|
393
|
+
active: () => this.activePlugin.value?.manifest.id ?? null,
|
|
394
|
+
plugins: this.pluginList
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
// src/idle-screen.element.ts
|
|
400
|
+
var STYLE = `
|
|
401
|
+
:host { all: initial; }
|
|
402
|
+
dialog.frame {
|
|
403
|
+
position: fixed; inset: 0; width: 100%; height: 100%;
|
|
404
|
+
max-width: none; max-height: none; margin: 0; border: 0; padding: 0;
|
|
405
|
+
background: #0a0a0f; overflow: hidden; cursor: none; color: #fff;
|
|
406
|
+
}
|
|
407
|
+
dialog.frame[open] { animation: ss-in 0.5s ease; }
|
|
408
|
+
dialog.frame.leaving { animation: ss-out 0.32s ease forwards; }
|
|
409
|
+
dialog.frame.reduced { animation: none; }
|
|
410
|
+
dialog.frame::backdrop { background: #0a0a0f; }
|
|
411
|
+
dialog.frame.passthrough, dialog.frame.passthrough::backdrop { background: transparent; }
|
|
412
|
+
@keyframes ss-in { from { opacity: 0; } to { opacity: 1; } }
|
|
413
|
+
@keyframes ss-out { from { opacity: 1; } to { opacity: 0; } }
|
|
414
|
+
.surface { position: absolute; inset: 0; display: block; width: 100%; height: 100%; }
|
|
415
|
+
.surface > canvas { display: block; width: 100%; height: 100%; }
|
|
416
|
+
.clock {
|
|
417
|
+
position: absolute; top: 10px; right: 12px; text-align: right;
|
|
418
|
+
font-family: 'Orbitron', system-ui, sans-serif; pointer-events: none;
|
|
419
|
+
}
|
|
420
|
+
.clock .day { font-size: 34px; opacity: 0.9; }
|
|
421
|
+
.clock .time { font-size: 54px; }
|
|
422
|
+
.hint {
|
|
423
|
+
position: fixed; bottom: 26px; left: 50%; transform: translateX(-50%);
|
|
424
|
+
color: rgba(230,230,230,0.6); font: 12px ui-monospace, monospace;
|
|
425
|
+
letter-spacing: 0.25em; text-transform: uppercase; opacity: 0;
|
|
426
|
+
transition: opacity 1s ease; pointer-events: none;
|
|
427
|
+
}
|
|
428
|
+
.hint.show { opacity: 1; }
|
|
429
|
+
|
|
430
|
+
/* Built-in config menu: a normal (interactive) centered dialog, distinct from
|
|
431
|
+
the cursor-hidden saver frame. */
|
|
432
|
+
dialog.menu {
|
|
433
|
+
position: fixed; inset: 0; margin: auto; border: 0; padding: 0;
|
|
434
|
+
width: min(420px, calc(100vw - 32px)); max-width: none;
|
|
435
|
+
background: transparent; color: #f2f2f5;
|
|
436
|
+
}
|
|
437
|
+
dialog.menu::backdrop { background: rgba(6,6,10,0.5); backdrop-filter: blur(2px); }
|
|
438
|
+
.menu-card {
|
|
439
|
+
background: #14141b; border: 1px solid rgba(255,255,255,0.12);
|
|
440
|
+
border-radius: 14px; padding: 20px; box-shadow: 0 24px 60px rgba(0,0,0,0.5);
|
|
441
|
+
font-family: system-ui, sans-serif;
|
|
442
|
+
}
|
|
443
|
+
.menu-title { font-size: 18px; font-weight: 600; margin: 0 0 14px; }
|
|
444
|
+
.menu-row { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 14px; }
|
|
445
|
+
.menu-row > span { font-size: 14px; opacity: 0.85; }
|
|
446
|
+
.menu-select {
|
|
447
|
+
flex: 1; max-width: 60%; padding: 7px 10px; border-radius: 8px;
|
|
448
|
+
background: #0d0d12; color: #f2f2f5; border: 1px solid rgba(255,255,255,0.16);
|
|
449
|
+
font-size: 14px;
|
|
450
|
+
}
|
|
451
|
+
.menu-actions { display: flex; justify-content: flex-end; }
|
|
452
|
+
.menu-close {
|
|
453
|
+
padding: 7px 16px; border-radius: 8px; cursor: pointer; font-size: 14px;
|
|
454
|
+
background: #2a2a37; color: #f2f2f5; border: 1px solid rgba(255,255,255,0.14);
|
|
455
|
+
}
|
|
456
|
+
.menu-close:hover { background: #34344a; }
|
|
457
|
+
`;
|
|
458
|
+
var HostBase = typeof HTMLElement !== "undefined" ? HTMLElement : class {
|
|
459
|
+
};
|
|
460
|
+
var IdleScreenElement = class extends HostBase {
|
|
461
|
+
_engine = null;
|
|
462
|
+
ownsEngine = false;
|
|
463
|
+
_externalEngine = null;
|
|
464
|
+
_config = null;
|
|
465
|
+
_plugins = null;
|
|
466
|
+
dialog;
|
|
467
|
+
surface;
|
|
468
|
+
clockEl;
|
|
469
|
+
hintEl;
|
|
470
|
+
menuDialog = null;
|
|
471
|
+
menuSelect = null;
|
|
472
|
+
menuBuilt = false;
|
|
473
|
+
instance = null;
|
|
474
|
+
mountToken = 0;
|
|
475
|
+
armed = false;
|
|
476
|
+
armTimer = null;
|
|
477
|
+
hintTimer = null;
|
|
478
|
+
closeTimer = null;
|
|
479
|
+
disposers = [];
|
|
480
|
+
set config(c) {
|
|
481
|
+
this._config = c;
|
|
482
|
+
this.maybeStart();
|
|
483
|
+
}
|
|
484
|
+
set plugins(p) {
|
|
485
|
+
this._plugins = p;
|
|
486
|
+
this.maybeStart();
|
|
487
|
+
}
|
|
488
|
+
/** Use an externally-owned, already-initialized engine (e.g. an Angular service
|
|
489
|
+
* that also drives a config menu) instead of creating one from config+plugins. */
|
|
490
|
+
set engine(e) {
|
|
491
|
+
this._externalEngine = e;
|
|
492
|
+
this.maybeStart();
|
|
493
|
+
}
|
|
494
|
+
/** The live engine (for host code / tests). */
|
|
495
|
+
get idleEngine() {
|
|
496
|
+
return this._engine;
|
|
497
|
+
}
|
|
498
|
+
connectedCallback() {
|
|
499
|
+
if (!this.shadowRoot) this.buildDom();
|
|
500
|
+
this.maybeStart();
|
|
501
|
+
}
|
|
502
|
+
disconnectedCallback() {
|
|
503
|
+
this.teardown();
|
|
504
|
+
}
|
|
505
|
+
buildDom() {
|
|
506
|
+
const root = this.attachShadow({ mode: "open" });
|
|
507
|
+
const style = document.createElement("style");
|
|
508
|
+
style.textContent = STYLE;
|
|
509
|
+
this.dialog = document.createElement("dialog");
|
|
510
|
+
this.dialog.className = "frame";
|
|
511
|
+
this.surface = document.createElement("div");
|
|
512
|
+
this.surface.className = "surface";
|
|
513
|
+
this.clockEl = document.createElement("div");
|
|
514
|
+
this.clockEl.className = "clock";
|
|
515
|
+
this.hintEl = document.createElement("div");
|
|
516
|
+
this.hintEl.className = "hint";
|
|
517
|
+
this.hintEl.textContent = "press any key";
|
|
518
|
+
this.dialog.append(this.surface, this.clockEl, this.hintEl);
|
|
519
|
+
root.append(style, this.dialog);
|
|
520
|
+
for (const ev of ["pointerdown", "pointermove", "keydown", "wheel", "touchstart"]) {
|
|
521
|
+
this.dialog.addEventListener(ev, () => this.onWakeInput(), { passive: true });
|
|
522
|
+
}
|
|
523
|
+
this.dialog.addEventListener("cancel", (e) => {
|
|
524
|
+
e.preventDefault();
|
|
525
|
+
this._engine?.wake();
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
maybeStart() {
|
|
529
|
+
if (this._engine || !this.shadowRoot || typeof window === "undefined") return;
|
|
530
|
+
if (this._externalEngine) {
|
|
531
|
+
this._engine = this._externalEngine;
|
|
532
|
+
this.ownsEngine = false;
|
|
533
|
+
} else if (this._config && this._plugins) {
|
|
534
|
+
this._engine = new IdleScreensEngine(this._config, this._plugins);
|
|
535
|
+
this._engine.init();
|
|
536
|
+
this.ownsEngine = true;
|
|
537
|
+
} else {
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
const eng = this._engine;
|
|
541
|
+
this.disposers.push(
|
|
542
|
+
effect(() => {
|
|
543
|
+
const sleeping = eng.isSleeping.value;
|
|
544
|
+
const active = eng.activePlugin.value;
|
|
545
|
+
const passthrough = eng.activeIsPassthrough.value;
|
|
546
|
+
if (sleeping && active) this.open(active, passthrough);
|
|
547
|
+
else this.close();
|
|
548
|
+
})
|
|
549
|
+
);
|
|
550
|
+
this.disposers.push(
|
|
551
|
+
effect(() => {
|
|
552
|
+
const show = this._engine.config.showClock && !eng.activeIsPassthrough.value;
|
|
553
|
+
const now = eng.now.value;
|
|
554
|
+
this.clockEl.style.display = show && eng.isSleeping.value ? "block" : "none";
|
|
555
|
+
if (show) {
|
|
556
|
+
const day = now.toLocaleDateString(void 0, {
|
|
557
|
+
weekday: "long",
|
|
558
|
+
month: "long",
|
|
559
|
+
day: "numeric"
|
|
560
|
+
});
|
|
561
|
+
const time = now.toLocaleTimeString();
|
|
562
|
+
this.clockEl.innerHTML = `<div class="day">${day}</div><div class="time">${time}</div>`;
|
|
563
|
+
}
|
|
564
|
+
})
|
|
565
|
+
);
|
|
566
|
+
if (eng.configMenu) {
|
|
567
|
+
this.buildMenu(eng.configMenu);
|
|
568
|
+
this.disposers.push(
|
|
569
|
+
effect(() => {
|
|
570
|
+
const open = eng.configMenuOpen.value;
|
|
571
|
+
const d = this.menuDialog;
|
|
572
|
+
if (!d) return;
|
|
573
|
+
if (open && !d.open) {
|
|
574
|
+
this.syncMenuSelection();
|
|
575
|
+
d.showModal();
|
|
576
|
+
} else if (!open && d.open) {
|
|
577
|
+
d.close();
|
|
578
|
+
}
|
|
579
|
+
})
|
|
580
|
+
);
|
|
581
|
+
this.disposers.push(
|
|
582
|
+
effect(() => {
|
|
583
|
+
const id = eng.activePlugin.value?.manifest.id ?? "";
|
|
584
|
+
if (this.menuSelect && this.menuSelect.value !== id) this.menuSelect.value = id;
|
|
585
|
+
})
|
|
586
|
+
);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
buildMenu(cfg) {
|
|
590
|
+
if (this.menuBuilt || !this.shadowRoot) return;
|
|
591
|
+
const eng = this._engine;
|
|
592
|
+
const d = document.createElement("dialog");
|
|
593
|
+
d.className = "menu";
|
|
594
|
+
const card = document.createElement("div");
|
|
595
|
+
card.className = "menu-card";
|
|
596
|
+
const title = document.createElement("div");
|
|
597
|
+
title.className = "menu-title";
|
|
598
|
+
title.textContent = cfg.title;
|
|
599
|
+
card.append(title);
|
|
600
|
+
if (cfg.showPicker) {
|
|
601
|
+
const row = document.createElement("label");
|
|
602
|
+
row.className = "menu-row";
|
|
603
|
+
const label = document.createElement("span");
|
|
604
|
+
label.textContent = "Saver";
|
|
605
|
+
const select = document.createElement("select");
|
|
606
|
+
select.className = "menu-select";
|
|
607
|
+
for (const p of eng.pluginList) {
|
|
608
|
+
const opt = document.createElement("option");
|
|
609
|
+
opt.value = p.id;
|
|
610
|
+
opt.textContent = p.label;
|
|
611
|
+
select.append(opt);
|
|
612
|
+
}
|
|
613
|
+
select.addEventListener("change", () => {
|
|
614
|
+
eng.setPlugin(select.value);
|
|
615
|
+
if (cfg.previewOnPick) {
|
|
616
|
+
eng.closeConfigMenu();
|
|
617
|
+
eng.forceSleep();
|
|
618
|
+
}
|
|
619
|
+
});
|
|
620
|
+
row.append(label, select);
|
|
621
|
+
card.append(row);
|
|
622
|
+
this.menuSelect = select;
|
|
623
|
+
}
|
|
624
|
+
const actions = document.createElement("div");
|
|
625
|
+
actions.className = "menu-actions";
|
|
626
|
+
const close = document.createElement("button");
|
|
627
|
+
close.className = "menu-close";
|
|
628
|
+
close.type = "button";
|
|
629
|
+
close.textContent = "Close";
|
|
630
|
+
close.addEventListener("click", () => eng.closeConfigMenu());
|
|
631
|
+
actions.append(close);
|
|
632
|
+
card.append(actions);
|
|
633
|
+
d.append(card);
|
|
634
|
+
d.addEventListener("cancel", (e) => {
|
|
635
|
+
e.preventDefault();
|
|
636
|
+
eng.closeConfigMenu();
|
|
637
|
+
});
|
|
638
|
+
d.addEventListener("click", (e) => {
|
|
639
|
+
if (e.target === d) eng.closeConfigMenu();
|
|
640
|
+
});
|
|
641
|
+
this.shadowRoot.append(d);
|
|
642
|
+
this.menuDialog = d;
|
|
643
|
+
this.menuBuilt = true;
|
|
644
|
+
}
|
|
645
|
+
syncMenuSelection() {
|
|
646
|
+
if (this.menuSelect) {
|
|
647
|
+
this.menuSelect.value = this._engine?.activePlugin.value?.manifest.id ?? "";
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
pageContext() {
|
|
651
|
+
return {
|
|
652
|
+
palette: () => [],
|
|
653
|
+
victims: (selector) => typeof document !== "undefined" ? Array.from(document.querySelectorAll(selector)) : []
|
|
654
|
+
};
|
|
655
|
+
}
|
|
656
|
+
async open(plugin, passthrough) {
|
|
657
|
+
const eng = this._engine;
|
|
658
|
+
this.dialog.classList.toggle("passthrough", passthrough);
|
|
659
|
+
this.dialog.classList.toggle("reduced", eng.reducedMotion.value);
|
|
660
|
+
if (!this.dialog.open) {
|
|
661
|
+
this.dialog.classList.remove("leaving");
|
|
662
|
+
this.dialog.showModal();
|
|
663
|
+
this.arm();
|
|
664
|
+
this.scheduleHint();
|
|
665
|
+
}
|
|
666
|
+
const token = ++this.mountToken;
|
|
667
|
+
await this.disposeInstance();
|
|
668
|
+
const w = window.innerWidth;
|
|
669
|
+
const h = window.innerHeight;
|
|
670
|
+
const seed = eng.config.seed >>> 0;
|
|
671
|
+
const inst = await plugin.mount({
|
|
672
|
+
host: this.surface,
|
|
673
|
+
width: w,
|
|
674
|
+
height: h,
|
|
675
|
+
rng: createRng(seed),
|
|
676
|
+
seed,
|
|
677
|
+
reducedMotion: eng.reducedMotion.value,
|
|
678
|
+
page: plugin.manifest.passthrough ? this.pageContext() : void 0
|
|
679
|
+
});
|
|
680
|
+
if (token !== this.mountToken) {
|
|
681
|
+
inst.dispose();
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
this.instance = inst;
|
|
685
|
+
inst.setPaused(eng.reducedMotion.value);
|
|
686
|
+
}
|
|
687
|
+
close() {
|
|
688
|
+
if (!this.dialog?.open) return;
|
|
689
|
+
this.hintEl.classList.remove("show");
|
|
690
|
+
if (this.hintTimer) clearTimeout(this.hintTimer);
|
|
691
|
+
void this.disposeInstance();
|
|
692
|
+
const reduced = this._engine?.reducedMotion.value;
|
|
693
|
+
if (reduced) {
|
|
694
|
+
this.dialog.close();
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
697
|
+
this.dialog.classList.add("leaving");
|
|
698
|
+
if (this.closeTimer) clearTimeout(this.closeTimer);
|
|
699
|
+
this.closeTimer = setTimeout(() => {
|
|
700
|
+
if (this.dialog.open) this.dialog.close();
|
|
701
|
+
this.dialog.classList.remove("leaving");
|
|
702
|
+
}, 320);
|
|
703
|
+
}
|
|
704
|
+
async disposeInstance() {
|
|
705
|
+
if (this.instance) {
|
|
706
|
+
this.instance.dispose();
|
|
707
|
+
this.instance = null;
|
|
708
|
+
}
|
|
709
|
+
this.surface?.replaceChildren();
|
|
710
|
+
}
|
|
711
|
+
arm() {
|
|
712
|
+
this.armed = false;
|
|
713
|
+
if (this.armTimer) clearTimeout(this.armTimer);
|
|
714
|
+
this.armTimer = setTimeout(() => {
|
|
715
|
+
this.armed = true;
|
|
716
|
+
}, 450);
|
|
717
|
+
}
|
|
718
|
+
scheduleHint() {
|
|
719
|
+
if (this.hintTimer) clearTimeout(this.hintTimer);
|
|
720
|
+
this.hintEl.classList.remove("show");
|
|
721
|
+
this.hintTimer = setTimeout(() => this.hintEl.classList.add("show"), 3e3);
|
|
722
|
+
}
|
|
723
|
+
onWakeInput() {
|
|
724
|
+
if (!this.armed) return;
|
|
725
|
+
this._engine?.wake();
|
|
726
|
+
}
|
|
727
|
+
teardown() {
|
|
728
|
+
void this.disposeInstance();
|
|
729
|
+
if (this.armTimer) clearTimeout(this.armTimer);
|
|
730
|
+
if (this.hintTimer) clearTimeout(this.hintTimer);
|
|
731
|
+
if (this.closeTimer) clearTimeout(this.closeTimer);
|
|
732
|
+
for (const d of this.disposers) d();
|
|
733
|
+
this.disposers = [];
|
|
734
|
+
if (this.ownsEngine) this._engine?.destroy();
|
|
735
|
+
this._engine = null;
|
|
736
|
+
}
|
|
737
|
+
};
|
|
738
|
+
function defineIdleScreen(tag = "idle-screen") {
|
|
739
|
+
if (typeof customElements !== "undefined" && !customElements.get(tag)) {
|
|
740
|
+
customElements.define(tag, IdleScreenElement);
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
export {
|
|
744
|
+
DEFAULT_CONFIG,
|
|
745
|
+
DEFAULT_MENU_HOTKEY,
|
|
746
|
+
IdleDetector,
|
|
747
|
+
IdleScreenElement,
|
|
748
|
+
IdleScreensEngine,
|
|
749
|
+
batch,
|
|
750
|
+
computed,
|
|
751
|
+
createRng,
|
|
752
|
+
defaultParams,
|
|
753
|
+
defineIdleScreen,
|
|
754
|
+
effect,
|
|
755
|
+
resolveConfigMenu,
|
|
756
|
+
sampleTrack,
|
|
757
|
+
signal,
|
|
758
|
+
untracked
|
|
759
|
+
};
|
|
760
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/types.ts","../src/rng.ts","../src/reactive.ts","../src/control-track.ts","../src/idle-detector.ts","../src/engine.ts","../src/idle-screen.element.ts"],"sourcesContent":["import type { Rng } from './rng';\n\n/** How a parameter interpolates between control-track deltas. */\nexport type Ease = 'step' | 'linear' | 'smooth';\n\nexport type ParamType = 'number' | 'color' | 'bool' | 'enum';\nexport type ParamValue = number | string | boolean;\n\n/** One typed, ranged, interpolatable knob: an agent's steering surface. */\nexport interface ParamDef {\n type: ParamType;\n default: ParamValue;\n min?: number;\n max?: number;\n options?: string[];\n /** Default interpolation when a delta targets this knob. */\n ease?: Ease;\n}\n\nexport type ParamSpace = Record<string, ParamDef>;\n\n/** A sparse, timestamped, interpolatable parameter change. Agents stream THESE\n * (over AG-UI state-patches / MCP resource updates), never frames. */\nexport interface ParamDelta {\n /** ms from track start. */\n t: number;\n /** key in the saver's paramSpace. */\n path: string;\n value: ParamValue;\n ease?: Ease;\n /** ramp duration ms from the previous value (0 = instant). */\n dur?: number;\n}\n\n/** A recorded or live steering performance for a seeded program. */\nexport interface ControlTrack {\n program: string;\n seed: number;\n duration?: number;\n loop?: boolean;\n deltas: ParamDelta[];\n}\n\n/** Page-context seam: passthrough savers read/eat the live page through this,\n * rather than querying `document` directly. */\nexport interface PageContext {\n /** Sample dominant colors of the page (for palette-reactive savers). */\n palette(): string[];\n /** Visible page blocks a passthrough saver may transform (\"victims\"). */\n victims(selector: string): HTMLElement[];\n}\n\n/** What the host hands a saver when it mounts. */\nexport interface SaverContext {\n /** A full-viewport container the saver renders into. Canvas savers create a\n * canvas inside it; DOM savers (toasters, DVD, messages) append elements. The\n * host is cleared by the runtime on dispose. */\n host: HTMLElement;\n width: number;\n height: number;\n /** Seeded PRNG. Use this, never Math.random(), for determinism. */\n rng: Rng;\n seed: number;\n reducedMotion: boolean;\n /** Present for passthrough savers (over the live page). */\n page?: PageContext;\n}\n\n/** A mounted, running saver visual. */\nexport interface SaverInstance {\n /** Freeze/unfreeze the render loop (paused, reduced-motion, or screensaver end). */\n setPaused(paused: boolean): void;\n resize(width: number, height: number): void;\n /** Deterministic, frame-addressable render: draw the state at logical time `t`\n * (ms) for `seed`. Pure w.r.t. (program, seed, applied track, t). Optional. */\n renderFrame?(t: number, seed: number): void;\n /** Steer parameters over time. Optional. */\n applyTrack?(track: ControlTrack): void;\n /** Tear down; MUST restore any live-page mutations. */\n dispose(): void;\n}\n\n/** Self-describing metadata, readable WITHOUT executing the saver. */\nexport interface SaverManifest {\n id: string;\n label: string;\n passthrough?: boolean;\n minBackend?: 'css' | 'canvas2d' | 'webgl2' | 'webgpu';\n costTier?: 'idle' | 'low' | 'medium' | 'high';\n motionIntensity?: 'calm' | 'moderate' | 'energetic';\n reducedMotionFallback?: 'static' | 'slow' | 'hide';\n palette?: string[];\n paramSpace?: ParamSpace;\n a11y?: { flashSafe?: boolean; notes?: string };\n provenance?: { prompt?: string; seed?: number; model?: string };\n thumbnail?: string;\n}\n\n/** A registered saver: manifest + a (possibly lazy/async) mount factory. */\nexport interface SaverPlugin {\n manifest: SaverManifest;\n mount(ctx: SaverContext): SaverInstance | Promise<SaverInstance>;\n}\n\n/** Engine configuration. */\nexport interface IdleScreensConfig {\n /** Idle time before sleeping, in ms. */\n timeoutMs: number;\n /** Also sleep when the window loses focus. */\n sleepOnBlur: boolean;\n /** Do not run on localhost (debug hooks bypass this). */\n disableOnLocalhost: boolean;\n /** Which registered saver to show. */\n defaultPluginId: string;\n /** How to pick across sleeps. */\n selection: 'fixed' | 'random' | 'rotate';\n /** Show the clock chrome (non-passthrough savers only). */\n showClock: boolean;\n /** Exact document title while asleep. */\n sleepTitle?: string;\n /** Master seed for plugin selection + the default saver seed. */\n seed: number;\n /** Return true for a URL where the saver must never sleep. */\n suppress?: (url: string) => boolean;\n /** Pluggable persistence (defaults to localStorage when available). */\n storage?: { get(key: string): string | null; set(key: string, value: string): void };\n /** Built-in, keyboard-opened config menu (saver picker). `true`/omitted =\n * enabled with ⌘K / Ctrl+K; `false` = no hotkey and no built-in menu (the\n * `configMenuOpen` signal + open/close API still work, so a host may render\n * its own). Pass an object to reconfigure. */\n configMenu?: boolean | ConfigMenuConfig;\n}\n\n/** Options for the built-in, keyboard-opened config menu. */\nexport interface ConfigMenuConfig {\n /** Master switch. Default true. */\n enabled?: boolean;\n /** Decides whether a keydown opens/toggles the menu. Default: ⌘K / Ctrl+K. */\n hotkey?: (e: KeyboardEvent) => boolean;\n /** Show the saver picker control. Default true. */\n showPicker?: boolean;\n /** Preview the picked saver immediately (sleep to show it). Default true. */\n previewOnPick?: boolean;\n /** Heading text. Default 'Screen Saver'. */\n title?: string;\n}\n\n/** Default menu hotkey: ⌘K (mac) / Ctrl+K, no other modifiers. */\nexport const DEFAULT_MENU_HOTKEY = (e: KeyboardEvent): boolean =>\n (e.metaKey || e.ctrlKey) && !e.altKey && !e.shiftKey && e.key.toLowerCase() === 'k';\n\n/** Normalize `configMenu` to a filled config, or null when disabled. */\nexport function resolveConfigMenu(\n c: boolean | ConfigMenuConfig | undefined,\n): Required<ConfigMenuConfig> | null {\n if (c === false) return null;\n const o: ConfigMenuConfig = c === true || c === undefined ? {} : c;\n if (o.enabled === false) return null;\n return {\n enabled: true,\n hotkey: o.hotkey ?? DEFAULT_MENU_HOTKEY,\n showPicker: o.showPicker ?? true,\n previewOnPick: o.previewOnPick ?? true,\n title: o.title ?? 'Screen Saver',\n };\n}\n\nexport const DEFAULT_CONFIG: IdleScreensConfig = {\n timeoutMs: 60_000,\n sleepOnBlur: true,\n disableOnLocalhost: false,\n defaultPluginId: '',\n selection: 'fixed',\n showClock: true,\n seed: 1,\n configMenu: true,\n};\n","/**\n * A tiny, fast, seedable PRNG (splitmix32). Deterministic: the same seed always\n * yields the same sequence, on any machine. This is what makes \"same program +\n * seed + control-track = identical frames\" hold. Savers take an `Rng` from their\n * `SaverContext` instead of calling `Math.random()`.\n */\nexport interface Rng {\n /** Next float in [0, 1). */\n next(): number;\n /** Float in [min, max). */\n range(min: number, max: number): number;\n /** Integer in [min, max] inclusive. */\n int(min: number, max: number): number;\n /** Random element of a non-empty array. */\n pick<T>(arr: readonly T[]): T;\n /** A fresh independent stream (e.g. per subsystem), derived from this seed. */\n fork(salt: number): Rng;\n}\n\nexport function createRng(seed: number): Rng {\n let a = seed >>> 0;\n const next = (): number => {\n a |= 0;\n a = (a + 0x9e3779b9) | 0;\n let t = a ^ (a >>> 16);\n t = Math.imul(t, 0x21f0aaad);\n t = t ^ (t >>> 15);\n t = Math.imul(t, 0x735a2d97);\n return ((t = t ^ (t >>> 15)) >>> 0) / 4294967296;\n };\n const rng: Rng = {\n next,\n range: (min, max) => min + (max - min) * next(),\n int: (min, max) => min + Math.floor(next() * (max - min + 1)),\n pick: (arr) => arr[Math.floor(next() * arr.length)] as (typeof arr)[number],\n fork: (salt) => createRng((seed ^ Math.imul(salt + 1, 0x9e3779b9)) >>> 0),\n };\n return rng;\n}\n","/**\n * Framework-agnostic reactivity. We re-export `@preact/signals-core` so the rest\n * of the engine uses the same `signal` / `computed` / `effect` primitives it did\n * as Angular signals, with near-identical semantics.\n */\nexport {\n signal,\n computed,\n effect,\n batch,\n untracked,\n type Signal,\n type ReadonlySignal,\n} from '@preact/signals-core';\n","import type { ControlTrack, Ease, ParamDelta, ParamSpace, ParamValue } from './types';\n\nfunction easeFn(e: Ease, p: number): number {\n const x = p <= 0 ? 0 : p >= 1 ? 1 : p;\n switch (e) {\n case 'step':\n return x >= 1 ? 1 : 0;\n case 'smooth':\n return x * x * (3 - 2 * x);\n case 'linear':\n default:\n return x;\n }\n}\n\nfunction lerp(a: number, b: number, p: number): number {\n return a + (b - a) * p;\n}\n\nfunction parseHex(c: string): [number, number, number] | null {\n const m = /^#?([0-9a-f]{6})$/i.exec(c.trim());\n if (!m) return null;\n const n = parseInt(m[1]!, 16);\n return [(n >> 16) & 255, (n >> 8) & 255, n & 255];\n}\n\nfunction toHex(rgb: [number, number, number]): string {\n const h = (v: number): string =>\n Math.round(Math.max(0, Math.min(255, v)))\n .toString(16)\n .padStart(2, '0');\n return `#${h(rgb[0])}${h(rgb[1])}${h(rgb[2])}`;\n}\n\nfunction lerpValue(type: string, a: ParamValue, b: ParamValue, p: number): ParamValue {\n if (type === 'number' && typeof a === 'number' && typeof b === 'number') return lerp(a, b, p);\n if (type === 'color' && typeof a === 'string' && typeof b === 'string') {\n const ca = parseHex(a);\n const cb = parseHex(b);\n if (ca && cb) return toHex([lerp(ca[0], cb[0], p), lerp(ca[1], cb[1], p), lerp(ca[2], cb[2], p)]);\n }\n return p >= 1 ? b : a; // bool / enum / step\n}\n\nfunction evalPath(\n dflt: ParamValue,\n type: string,\n defaultEase: Ease,\n deltas: ParamDelta[],\n t: number,\n): ParamValue {\n if (deltas.length === 0) return dflt;\n let prevVal = dflt;\n let prevT = -Infinity;\n for (const k of deltas) {\n if (t < k.t) {\n const rampStart = k.dur != null ? k.t - k.dur : prevT === -Infinity ? k.t : prevT;\n if (t <= rampStart) return prevVal;\n const p = easeFn(k.ease ?? defaultEase, (t - rampStart) / (k.t - rampStart || 1));\n return lerpValue(type, prevVal, k.value, p);\n }\n prevVal = k.value;\n prevT = k.t;\n }\n return prevVal; // after the last keyframe\n}\n\n/**\n * Sample a control track: given the saver's param space (defaults + types) and a\n * track, return the interpolated parameter values at logical time `t` (ms). Pure\n * and deterministic: identical `(space, track, t)` yields identical output.\n */\nexport function sampleTrack(\n space: ParamSpace,\n track: ControlTrack,\n t: number,\n): Record<string, ParamValue> {\n const byPath = new Map<string, ParamDelta[]>();\n for (const d of track.deltas) {\n const arr = byPath.get(d.path) ?? [];\n arr.push(d);\n byPath.set(d.path, arr);\n }\n const wrapped =\n track.loop && track.duration && track.duration > 0 ? t % track.duration : t;\n\n const out: Record<string, ParamValue> = {};\n for (const [path, def] of Object.entries(space)) {\n const deltas = (byPath.get(path) ?? []).slice().sort((a, b) => a.t - b.t);\n out[path] = evalPath(def.default, def.type, def.ease ?? 'linear', deltas, wrapped);\n }\n return out;\n}\n\n/** Default param values (the \"resting\" program before any track is applied). */\nexport function defaultParams(space: ParamSpace): Record<string, ParamValue> {\n const out: Record<string, ParamValue> = {};\n for (const [path, def] of Object.entries(space)) out[path] = def.default;\n return out;\n}\n","import { signal, type ReadonlySignal } from './reactive';\n\n/**\n * Detects user inactivity. `idle` flips true once no activity for `timeoutMs`.\n * Framework-agnostic port of the Angular IdleService: 6 activity events + a\n * single `setTimeout` (not a poll) whose reschedule is throttled to >=500ms so\n * frequent pointermove events do not thrash timers.\n */\nexport class IdleDetector {\n private readonly _idle = signal(false);\n readonly idle: ReadonlySignal<boolean> = this._idle;\n\n private timer: ReturnType<typeof setTimeout> | null = null;\n private lastReset = 0;\n private running = false;\n\n private static readonly EVENTS = [\n 'pointerdown',\n 'pointermove',\n 'keydown',\n 'wheel',\n 'touchstart',\n 'scroll',\n ] as const;\n\n constructor(private readonly timeoutMs: number) {}\n\n private readonly onActivity = (): void => this.markActive();\n\n start(): void {\n if (this.running || typeof window === 'undefined') return;\n this.running = true;\n for (const e of IdleDetector.EVENTS) {\n window.addEventListener(e, this.onActivity, { passive: true });\n }\n this.schedule();\n }\n\n stop(): void {\n if (!this.running) return;\n this.running = false;\n for (const e of IdleDetector.EVENTS) {\n window.removeEventListener(e, this.onActivity);\n }\n this.clear();\n }\n\n /** Force an activity pulse (resets the countdown, wakes from idle). */\n markActive(): void {\n if (this._idle.value) this._idle.value = false;\n const now = typeof performance !== 'undefined' ? performance.now() : Date.now();\n if (now - this.lastReset < 500) return; // throttle reschedules\n this.lastReset = now;\n this.schedule();\n }\n\n private schedule(): void {\n this.clear();\n this.timer = setTimeout(() => {\n this._idle.value = true;\n }, this.timeoutMs);\n }\n\n private clear(): void {\n if (this.timer !== null) {\n clearTimeout(this.timer);\n this.timer = null;\n }\n }\n}\n","import { computed, effect, signal, untracked, type ReadonlySignal } from './reactive';\nimport { IdleDetector } from './idle-detector';\nimport { createRng, type Rng } from './rng';\nimport {\n DEFAULT_CONFIG,\n resolveConfigMenu,\n type ConfigMenuConfig,\n type IdleScreensConfig,\n type SaverPlugin,\n} from './types';\n\nexport type EngineState = 'awake' | 'sleeping';\n\nconst hasWindow = typeof window !== 'undefined';\nconst hasDoc = typeof document !== 'undefined';\n\nfunction defaultStorage(): IdleScreensConfig['storage'] {\n return {\n get: (k) => {\n try {\n return hasWindow ? window.localStorage.getItem(k) : null;\n } catch {\n return null;\n }\n },\n set: (k, v) => {\n try {\n if (hasWindow) window.localStorage.setItem(k, v);\n } catch {\n /* private mode / disabled storage */\n }\n },\n };\n}\n\n/**\n * The framework-agnostic idle-screens state machine (port of the Angular\n * ScreensaverService). Owns: awake/sleeping state, plugin selection, the sleep\n * clock, reduced-motion + visibility, idle/blur triggers, and the\n * `window.__idleScreens` debug hook. It does NOT render; the `<idle-screen>`\n * element observes its signals.\n */\nexport class IdleScreensEngine {\n readonly config: IdleScreensConfig;\n private readonly plugins: SaverPlugin[];\n private readonly storage: NonNullable<IdleScreensConfig['storage']>;\n private readonly idle: IdleDetector;\n private readonly selectionRng: Rng;\n\n private readonly _state = signal<EngineState>('awake');\n readonly state: ReadonlySignal<EngineState> = this._state;\n readonly isSleeping = computed(() => this._state.value === 'sleeping');\n\n private readonly _activeId = signal('');\n readonly activePlugin = computed(\n () =>\n this.plugins.find((p) => p.manifest.id === this._activeId.value) ??\n this.plugins[0] ??\n null,\n );\n readonly activeIsPassthrough = computed(\n () => this.activePlugin.value?.manifest.passthrough ?? false,\n );\n\n private readonly _now = signal(new Date(0));\n readonly now: ReadonlySignal<Date> = this._now;\n private readonly _hidden = signal(false);\n readonly hidden: ReadonlySignal<boolean> = this._hidden;\n private readonly _reducedMotion = signal(false);\n readonly reducedMotion: ReadonlySignal<boolean> = this._reducedMotion;\n\n /** Resolved built-in config-menu options, or null when disabled. */\n readonly configMenu: Required<ConfigMenuConfig> | null;\n private readonly _configMenuOpen = signal(false);\n /** Whether the config menu is open. Drives the built-in `<idle-screen>` menu;\n * a host may also observe this to render its own. */\n readonly configMenuOpen: ReadonlySignal<boolean> = this._configMenuOpen;\n\n private clockTimer: ReturnType<typeof setInterval> | null = null;\n private started = false;\n private disposers: Array<() => void> = [];\n\n constructor(config: Partial<IdleScreensConfig>, plugins: SaverPlugin[]) {\n this.config = { ...DEFAULT_CONFIG, ...config };\n this.plugins = plugins;\n this.configMenu = resolveConfigMenu(this.config.configMenu);\n this.storage = this.config.storage ?? defaultStorage()!;\n this.idle = new IdleDetector(this.config.timeoutMs);\n this.selectionRng = createRng(this.config.seed >>> 0);\n\n const stored = this.storage.get('idleScreens.plugin');\n const initialId =\n (stored && plugins.some((p) => p.manifest.id === stored) && stored) ||\n this.config.defaultPluginId ||\n plugins[0]?.manifest.id ||\n '';\n this._activeId.value = initialId;\n }\n\n get pluginList(): Array<{ id: string; label: string }> {\n return this.plugins.map((p) => ({ id: p.manifest.id, label: p.manifest.label }));\n }\n\n /** Begin idle detection + listeners + debug hook. */\n init(): void {\n if (this.started || !hasWindow) return;\n this.started = true;\n\n if (hasDoc) {\n this._hidden.value = document.hidden;\n const onVis = (): void => {\n this._hidden.value = document.hidden;\n };\n document.addEventListener('visibilitychange', onVis);\n this.disposers.push(() => document.removeEventListener('visibilitychange', onVis));\n }\n\n const mq = hasWindow ? window.matchMedia('(prefers-reduced-motion: reduce)') : null;\n if (mq) {\n this._reducedMotion.value = mq.matches;\n const onMq = (): void => {\n this._reducedMotion.value = mq.matches;\n };\n mq.addEventListener('change', onMq);\n this.disposers.push(() => mq.removeEventListener('change', onMq));\n }\n\n // Sleep when idle fires (unless suppressed). `sleep()` reads `state` in its\n // guard; wrap it in `untracked` so this effect depends ONLY on the idle\n // signal — otherwise flipping `state` to awake in wake() (while idle is still\n // latched true) would re-trigger this effect and immediately re-sleep.\n this.idle.start();\n this.disposers.push(\n effect(() => {\n if (this.idle.idle.value) untracked(() => this.sleep());\n }),\n );\n\n if (this.config.sleepOnBlur) {\n const onBlur = (): void => this.sleep();\n window.addEventListener('blur', onBlur);\n this.disposers.push(() => window.removeEventListener('blur', onBlur));\n }\n\n // Optional built-in config-menu hotkey (⌘K / Ctrl+K by default).\n if (this.configMenu) {\n const hotkey = this.configMenu.hotkey;\n const onKey = (e: KeyboardEvent): void => {\n if (hotkey(e)) {\n e.preventDefault();\n this.toggleConfigMenu();\n }\n };\n window.addEventListener('keydown', onKey);\n this.disposers.push(() => window.removeEventListener('keydown', onKey));\n }\n\n // Drive the clock only while asleep + visible.\n this.disposers.push(\n effect(() => {\n const ticking = this._state.value === 'sleeping' && !this._hidden.value;\n if (ticking && this.clockTimer === null) {\n this._now.value = new Date();\n this.clockTimer = setInterval(() => {\n this._now.value = new Date();\n }, 1000);\n } else if (!ticking && this.clockTimer !== null) {\n clearInterval(this.clockTimer);\n this.clockTimer = null;\n }\n }),\n );\n\n this.installDebugHook();\n }\n\n destroy(): void {\n this.idle.stop();\n if (this.clockTimer !== null) {\n clearInterval(this.clockTimer);\n this.clockTimer = null;\n }\n for (const d of this.disposers) d();\n this.disposers = [];\n this.started = false;\n }\n\n private suppressed(): boolean {\n if (this.config.disableOnLocalhost && hasWindow && /^(localhost|127\\.|\\[::1\\])/.test(location.hostname)) {\n return true;\n }\n const url = hasWindow ? location.pathname + location.search : '';\n return this.config.suppress ? this.config.suppress(url) : false;\n }\n\n sleep(): void {\n if (this._state.value === 'sleeping' || this.suppressed()) return;\n this.pickPlugin();\n this._state.value = 'sleeping';\n }\n\n /** Sleep ignoring suppression rules (debug / manual). */\n forceSleep(): void {\n if (this._state.value === 'sleeping') return;\n this.pickPlugin();\n this._state.value = 'sleeping';\n }\n\n wake(): void {\n if (this._state.value === 'awake') return;\n this._state.value = 'awake';\n this.idle.markActive();\n }\n\n toggle(): void {\n if (this.isSleeping.value) this.wake();\n else this.forceSleep();\n }\n\n openConfigMenu(): void {\n this._configMenuOpen.value = true;\n }\n closeConfigMenu(): void {\n this._configMenuOpen.value = false;\n }\n toggleConfigMenu(): void {\n this._configMenuOpen.value = !this._configMenuOpen.value;\n }\n\n setPlugin(id: string): void {\n if (!this.plugins.some((p) => p.manifest.id === id)) return;\n this._activeId.value = id;\n this.storage.set('idleScreens.plugin', id);\n }\n\n private pickPlugin(): void {\n if (this.plugins.length === 0) return;\n switch (this.config.selection) {\n case 'random': {\n const i = this.selectionRng.int(0, this.plugins.length - 1);\n this._activeId.value = this.plugins[i]!.manifest.id;\n break;\n }\n case 'rotate': {\n const cur = this.plugins.findIndex((p) => p.manifest.id === this._activeId.value);\n const next = this.plugins[(cur + 1) % this.plugins.length]!;\n this._activeId.value = next.manifest.id;\n break;\n }\n case 'fixed':\n default:\n // Keep the current (persisted / menu-chosen) plugin. `defaultPluginId`\n // is only the INITIAL pick (see constructor); once the user chooses a\n // saver via setPlugin/the menu it sticks across sleeps.\n break;\n }\n }\n\n private installDebugHook(): void {\n if (!hasWindow) return;\n (window as unknown as { __idleScreens?: unknown }).__idleScreens = {\n sleep: () => this.forceSleep(),\n wake: () => this.wake(),\n toggle: () => this.toggle(),\n setPlugin: (id: string) => this.setPlugin(id),\n openMenu: () => this.openConfigMenu(),\n closeMenu: () => this.closeConfigMenu(),\n toggleMenu: () => this.toggleConfigMenu(),\n state: () => this._state.value,\n menuOpen: () => this._configMenuOpen.value,\n active: () => this.activePlugin.value?.manifest.id ?? null,\n plugins: this.pluginList,\n };\n }\n}\n","import { effect } from './reactive';\nimport { IdleScreensEngine } from './engine';\nimport { createRng } from './rng';\nimport type { IdleScreensConfig, PageContext, SaverInstance, SaverPlugin } from './types';\n\nconst STYLE = `\n :host { all: initial; }\n dialog.frame {\n position: fixed; inset: 0; width: 100%; height: 100%;\n max-width: none; max-height: none; margin: 0; border: 0; padding: 0;\n background: #0a0a0f; overflow: hidden; cursor: none; color: #fff;\n }\n dialog.frame[open] { animation: ss-in 0.5s ease; }\n dialog.frame.leaving { animation: ss-out 0.32s ease forwards; }\n dialog.frame.reduced { animation: none; }\n dialog.frame::backdrop { background: #0a0a0f; }\n dialog.frame.passthrough, dialog.frame.passthrough::backdrop { background: transparent; }\n @keyframes ss-in { from { opacity: 0; } to { opacity: 1; } }\n @keyframes ss-out { from { opacity: 1; } to { opacity: 0; } }\n .surface { position: absolute; inset: 0; display: block; width: 100%; height: 100%; }\n .surface > canvas { display: block; width: 100%; height: 100%; }\n .clock {\n position: absolute; top: 10px; right: 12px; text-align: right;\n font-family: 'Orbitron', system-ui, sans-serif; pointer-events: none;\n }\n .clock .day { font-size: 34px; opacity: 0.9; }\n .clock .time { font-size: 54px; }\n .hint {\n position: fixed; bottom: 26px; left: 50%; transform: translateX(-50%);\n color: rgba(230,230,230,0.6); font: 12px ui-monospace, monospace;\n letter-spacing: 0.25em; text-transform: uppercase; opacity: 0;\n transition: opacity 1s ease; pointer-events: none;\n }\n .hint.show { opacity: 1; }\n\n /* Built-in config menu: a normal (interactive) centered dialog, distinct from\n the cursor-hidden saver frame. */\n dialog.menu {\n position: fixed; inset: 0; margin: auto; border: 0; padding: 0;\n width: min(420px, calc(100vw - 32px)); max-width: none;\n background: transparent; color: #f2f2f5;\n }\n dialog.menu::backdrop { background: rgba(6,6,10,0.5); backdrop-filter: blur(2px); }\n .menu-card {\n background: #14141b; border: 1px solid rgba(255,255,255,0.12);\n border-radius: 14px; padding: 20px; box-shadow: 0 24px 60px rgba(0,0,0,0.5);\n font-family: system-ui, sans-serif;\n }\n .menu-title { font-size: 18px; font-weight: 600; margin: 0 0 14px; }\n .menu-row { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 14px; }\n .menu-row > span { font-size: 14px; opacity: 0.85; }\n .menu-select {\n flex: 1; max-width: 60%; padding: 7px 10px; border-radius: 8px;\n background: #0d0d12; color: #f2f2f5; border: 1px solid rgba(255,255,255,0.16);\n font-size: 14px;\n }\n .menu-actions { display: flex; justify-content: flex-end; }\n .menu-close {\n padding: 7px 16px; border-radius: 8px; cursor: pointer; font-size: 14px;\n background: #2a2a37; color: #f2f2f5; border: 1px solid rgba(255,255,255,0.14);\n }\n .menu-close:hover { background: #34344a; }\n`;\n\n/** Node-safe base: during SSR/prerender there is no `HTMLElement`, and importing\n * this module (for the engine/types) must not crash. The element is only ever\n * constructed/registered in the browser. */\nconst HostBase: typeof HTMLElement =\n typeof HTMLElement !== 'undefined'\n ? HTMLElement\n : (class {} as unknown as typeof HTMLElement);\n\n/**\n * `<idle-screen>` — the framework-agnostic overlay (port of the Angular\n * ScreensaverComponent). Owns the top-layer `<dialog>`, mounts the active saver\n * into a canvas, shows the clock + \"press any key\" hint, fades in/out, wakes on\n * input with a 450ms arm-guard, and goes transparent for passthrough savers.\n *\n * Configure by setting the `.config` and `.plugins` properties (Angular can do\n * this via `[config]` / `[plugins]` with CUSTOM_ELEMENTS_SCHEMA).\n */\nexport class IdleScreenElement extends HostBase {\n private _engine: IdleScreensEngine | null = null;\n private ownsEngine = false;\n private _externalEngine: IdleScreensEngine | null = null;\n private _config: Partial<IdleScreensConfig> | null = null;\n private _plugins: SaverPlugin[] | null = null;\n\n private dialog!: HTMLDialogElement;\n private surface!: HTMLDivElement;\n private clockEl!: HTMLDivElement;\n private hintEl!: HTMLDivElement;\n private menuDialog: HTMLDialogElement | null = null;\n private menuSelect: HTMLSelectElement | null = null;\n private menuBuilt = false;\n\n private instance: SaverInstance | null = null;\n private mountToken = 0;\n private armed = false;\n private armTimer: ReturnType<typeof setTimeout> | null = null;\n private hintTimer: ReturnType<typeof setTimeout> | null = null;\n private closeTimer: ReturnType<typeof setTimeout> | null = null;\n private disposers: Array<() => void> = [];\n\n set config(c: Partial<IdleScreensConfig>) {\n this._config = c;\n this.maybeStart();\n }\n set plugins(p: SaverPlugin[]) {\n this._plugins = p;\n this.maybeStart();\n }\n /** Use an externally-owned, already-initialized engine (e.g. an Angular service\n * that also drives a config menu) instead of creating one from config+plugins. */\n set engine(e: IdleScreensEngine) {\n this._externalEngine = e;\n this.maybeStart();\n }\n /** The live engine (for host code / tests). */\n get idleEngine(): IdleScreensEngine | null {\n return this._engine;\n }\n\n connectedCallback(): void {\n if (!this.shadowRoot) this.buildDom();\n this.maybeStart();\n }\n\n disconnectedCallback(): void {\n this.teardown();\n }\n\n private buildDom(): void {\n const root = this.attachShadow({ mode: 'open' });\n const style = document.createElement('style');\n style.textContent = STYLE;\n this.dialog = document.createElement('dialog');\n this.dialog.className = 'frame';\n this.surface = document.createElement('div');\n this.surface.className = 'surface';\n this.clockEl = document.createElement('div');\n this.clockEl.className = 'clock';\n this.hintEl = document.createElement('div');\n this.hintEl.className = 'hint';\n this.hintEl.textContent = 'press any key';\n this.dialog.append(this.surface, this.clockEl, this.hintEl);\n root.append(style, this.dialog);\n\n for (const ev of ['pointerdown', 'pointermove', 'keydown', 'wheel', 'touchstart'] as const) {\n this.dialog.addEventListener(ev, () => this.onWakeInput(), { passive: true });\n }\n this.dialog.addEventListener('cancel', (e) => {\n e.preventDefault();\n this._engine?.wake();\n });\n }\n\n private maybeStart(): void {\n if (this._engine || !this.shadowRoot || typeof window === 'undefined') return;\n if (this._externalEngine) {\n this._engine = this._externalEngine; // caller owns lifecycle + init()\n this.ownsEngine = false;\n } else if (this._config && this._plugins) {\n this._engine = new IdleScreensEngine(this._config, this._plugins);\n this._engine.init();\n this.ownsEngine = true;\n } else {\n return;\n }\n const eng = this._engine;\n\n this.disposers.push(\n effect(() => {\n // Read reactive deps up front.\n const sleeping = eng.isSleeping.value;\n const active = eng.activePlugin.value;\n const passthrough = eng.activeIsPassthrough.value;\n if (sleeping && active) this.open(active, passthrough);\n else this.close();\n }),\n );\n\n // Clock chrome.\n this.disposers.push(\n effect(() => {\n const show = this._engine!.config.showClock && !eng.activeIsPassthrough.value;\n const now = eng.now.value;\n this.clockEl.style.display = show && eng.isSleeping.value ? 'block' : 'none';\n if (show) {\n const day = now.toLocaleDateString(undefined, {\n weekday: 'long',\n month: 'long',\n day: 'numeric',\n });\n const time = now.toLocaleTimeString();\n this.clockEl.innerHTML = `<div class=\"day\">${day}</div><div class=\"time\">${time}</div>`;\n }\n }),\n );\n\n // Optional built-in config menu (saver picker), opened via engine hotkey/API.\n if (eng.configMenu) {\n this.buildMenu(eng.configMenu);\n this.disposers.push(\n effect(() => {\n const open = eng.configMenuOpen.value;\n const d = this.menuDialog;\n if (!d) return;\n if (open && !d.open) {\n this.syncMenuSelection();\n d.showModal();\n } else if (!open && d.open) {\n d.close();\n }\n }),\n );\n // Keep the picker in sync if the active saver changes elsewhere.\n this.disposers.push(\n effect(() => {\n const id = eng.activePlugin.value?.manifest.id ?? '';\n if (this.menuSelect && this.menuSelect.value !== id) this.menuSelect.value = id;\n }),\n );\n }\n }\n\n private buildMenu(cfg: NonNullable<IdleScreensEngine['configMenu']>): void {\n if (this.menuBuilt || !this.shadowRoot) return;\n const eng = this._engine!;\n const d = document.createElement('dialog');\n d.className = 'menu';\n const card = document.createElement('div');\n card.className = 'menu-card';\n\n const title = document.createElement('div');\n title.className = 'menu-title';\n title.textContent = cfg.title;\n card.append(title);\n\n if (cfg.showPicker) {\n const row = document.createElement('label');\n row.className = 'menu-row';\n const label = document.createElement('span');\n label.textContent = 'Saver';\n const select = document.createElement('select');\n select.className = 'menu-select';\n for (const p of eng.pluginList) {\n const opt = document.createElement('option');\n opt.value = p.id;\n opt.textContent = p.label;\n select.append(opt);\n }\n select.addEventListener('change', () => {\n eng.setPlugin(select.value);\n if (cfg.previewOnPick) {\n // Close the menu first so the saver isn't stacked behind this modal,\n // then show the chosen saver right away.\n eng.closeConfigMenu();\n eng.forceSleep();\n }\n });\n row.append(label, select);\n card.append(row);\n this.menuSelect = select;\n }\n\n const actions = document.createElement('div');\n actions.className = 'menu-actions';\n const close = document.createElement('button');\n close.className = 'menu-close';\n close.type = 'button';\n close.textContent = 'Close';\n close.addEventListener('click', () => eng.closeConfigMenu());\n actions.append(close);\n card.append(actions);\n\n d.append(card);\n // Escape and backdrop click both close (without waking the saver).\n d.addEventListener('cancel', (e) => {\n e.preventDefault();\n eng.closeConfigMenu();\n });\n d.addEventListener('click', (e) => {\n if (e.target === d) eng.closeConfigMenu();\n });\n this.shadowRoot.append(d);\n this.menuDialog = d;\n this.menuBuilt = true;\n }\n\n private syncMenuSelection(): void {\n if (this.menuSelect) {\n this.menuSelect.value = this._engine?.activePlugin.value?.manifest.id ?? '';\n }\n }\n\n private pageContext(): PageContext {\n return {\n palette: () => [],\n victims: (selector: string) =>\n typeof document !== 'undefined'\n ? Array.from(document.querySelectorAll<HTMLElement>(selector))\n : [],\n };\n }\n\n private async open(plugin: SaverPlugin, passthrough: boolean): Promise<void> {\n const eng = this._engine!;\n this.dialog.classList.toggle('passthrough', passthrough);\n this.dialog.classList.toggle('reduced', eng.reducedMotion.value);\n if (!this.dialog.open) {\n this.dialog.classList.remove('leaving');\n this.dialog.showModal();\n this.arm();\n this.scheduleHint();\n }\n // (Re)mount the active saver.\n const token = ++this.mountToken;\n await this.disposeInstance();\n const w = window.innerWidth;\n const h = window.innerHeight;\n const seed = eng.config.seed >>> 0;\n const inst = await plugin.mount({\n host: this.surface,\n width: w,\n height: h,\n rng: createRng(seed),\n seed,\n reducedMotion: eng.reducedMotion.value,\n page: plugin.manifest.passthrough ? this.pageContext() : undefined,\n });\n if (token !== this.mountToken) {\n inst.dispose(); // superseded while awaiting\n return;\n }\n this.instance = inst;\n inst.setPaused(eng.reducedMotion.value);\n }\n\n private close(): void {\n if (!this.dialog?.open) return;\n this.hintEl.classList.remove('show');\n if (this.hintTimer) clearTimeout(this.hintTimer);\n void this.disposeInstance();\n const reduced = this._engine?.reducedMotion.value;\n if (reduced) {\n this.dialog.close();\n return;\n }\n this.dialog.classList.add('leaving');\n if (this.closeTimer) clearTimeout(this.closeTimer);\n this.closeTimer = setTimeout(() => {\n if (this.dialog.open) this.dialog.close();\n this.dialog.classList.remove('leaving');\n }, 320);\n }\n\n private async disposeInstance(): Promise<void> {\n if (this.instance) {\n this.instance.dispose();\n this.instance = null;\n }\n this.surface?.replaceChildren(); // clear any saver DOM / canvas\n }\n\n private arm(): void {\n this.armed = false;\n if (this.armTimer) clearTimeout(this.armTimer);\n this.armTimer = setTimeout(() => {\n this.armed = true;\n }, 450);\n }\n\n private scheduleHint(): void {\n if (this.hintTimer) clearTimeout(this.hintTimer);\n this.hintEl.classList.remove('show');\n this.hintTimer = setTimeout(() => this.hintEl.classList.add('show'), 3000);\n }\n\n private onWakeInput(): void {\n if (!this.armed) return; // grace period after sleeping\n this._engine?.wake();\n }\n\n private teardown(): void {\n void this.disposeInstance();\n if (this.armTimer) clearTimeout(this.armTimer);\n if (this.hintTimer) clearTimeout(this.hintTimer);\n if (this.closeTimer) clearTimeout(this.closeTimer);\n for (const d of this.disposers) d();\n this.disposers = [];\n if (this.ownsEngine) this._engine?.destroy(); // external engines are the caller's to destroy\n this._engine = null;\n }\n}\n\n/** Register the <idle-screen> custom element (idempotent). */\nexport function defineIdleScreen(tag = 'idle-screen'): void {\n if (typeof customElements !== 'undefined' && !customElements.get(tag)) {\n customElements.define(tag, IdleScreenElement);\n }\n}\n"],"mappings":"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","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@idle-screens/core",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Engine, <idle-screen> custom element, plugin registry, seeded RNG, control-track, and types for idle-screens.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Shawn Partridge",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/shaaaaawn/idle-screens",
|
|
10
|
+
"directory": "packages/core"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://github.com/shaaaaawn/idle-screens#readme",
|
|
13
|
+
"type": "module",
|
|
14
|
+
"sideEffects": false,
|
|
15
|
+
"main": "./dist/index.js",
|
|
16
|
+
"module": "./dist/index.js",
|
|
17
|
+
"types": "./dist/index.d.ts",
|
|
18
|
+
"exports": {
|
|
19
|
+
".": {
|
|
20
|
+
"types": "./dist/index.d.ts",
|
|
21
|
+
"import": "./dist/index.js"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"publishConfig": {
|
|
25
|
+
"access": "public"
|
|
26
|
+
},
|
|
27
|
+
"keywords": [
|
|
28
|
+
"screensaver",
|
|
29
|
+
"idle",
|
|
30
|
+
"ambient",
|
|
31
|
+
"web-components",
|
|
32
|
+
"canvas",
|
|
33
|
+
"custom-element"
|
|
34
|
+
],
|
|
35
|
+
"bugs": {
|
|
36
|
+
"url": "https://github.com/shaaaaawn/idle-screens/issues"
|
|
37
|
+
},
|
|
38
|
+
"files": [
|
|
39
|
+
"dist"
|
|
40
|
+
],
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"@preact/signals-core": "^1.8.0"
|
|
43
|
+
},
|
|
44
|
+
"scripts": {
|
|
45
|
+
"build": "tsup",
|
|
46
|
+
"dev": "tsup --watch",
|
|
47
|
+
"typecheck": "tsc --noEmit",
|
|
48
|
+
"lint": "eslint src"
|
|
49
|
+
}
|
|
50
|
+
}
|