@peekling/runtime 0.1.0 → 0.1.2

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.
Files changed (52) hide show
  1. package/README.md +59 -8
  2. package/dist/canvas-renderer.d.ts +17 -0
  3. package/dist/canvas-renderer.d.ts.map +1 -0
  4. package/dist/canvas-renderer.js +192 -0
  5. package/dist/canvas.d.ts +5 -0
  6. package/dist/canvas.d.ts.map +1 -0
  7. package/dist/canvas.js +6 -0
  8. package/dist/configuration-validation.d.ts.map +1 -1
  9. package/dist/configuration-validation.js +176 -1
  10. package/dist/content.d.ts +3 -0
  11. package/dist/content.d.ts.map +1 -1
  12. package/dist/content.js +33 -7
  13. package/dist/index.d.ts +2 -2
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/input.d.ts.map +1 -1
  16. package/dist/input.js +4 -1
  17. package/dist/interaction.d.ts +37 -0
  18. package/dist/interaction.d.ts.map +1 -0
  19. package/dist/interaction.js +204 -0
  20. package/dist/overrides.d.ts +8 -0
  21. package/dist/overrides.d.ts.map +1 -1
  22. package/dist/overrides.js +18 -16
  23. package/dist/path-sampler.d.ts +9 -0
  24. package/dist/path-sampler.d.ts.map +1 -0
  25. package/dist/path-sampler.js +47 -0
  26. package/dist/peekling.css +1 -1
  27. package/dist/peekling.css.sri +1 -1
  28. package/dist/peekling.js +1365 -86
  29. package/dist/peekling.js.sri +1 -1
  30. package/dist/peekling.min.js +1 -1
  31. package/dist/peekling.min.js.sri +1 -1
  32. package/dist/plan-compiler.d.ts +2 -0
  33. package/dist/plan-compiler.d.ts.map +1 -1
  34. package/dist/plan-compiler.js +71 -14
  35. package/dist/plan.d.ts +27 -1
  36. package/dist/plan.d.ts.map +1 -1
  37. package/dist/plan.js +281 -22
  38. package/dist/registry.js +2 -2
  39. package/dist/renderer.d.ts +18 -1
  40. package/dist/renderer.d.ts.map +1 -1
  41. package/dist/renderer.js +1 -0
  42. package/dist/runtime-validation.d.ts.map +1 -1
  43. package/dist/runtime-validation.js +35 -0
  44. package/dist/runtime.d.ts +49 -3
  45. package/dist/runtime.d.ts.map +1 -1
  46. package/dist/runtime.js +365 -14
  47. package/dist/targets.d.ts +11 -0
  48. package/dist/targets.d.ts.map +1 -0
  49. package/dist/targets.js +106 -0
  50. package/dist/types.d.ts +61 -1
  51. package/dist/types.d.ts.map +1 -1
  52. package/package.json +5 -1
package/README.md CHANGED
@@ -10,7 +10,7 @@ data cannot contain code, callbacks, DOM nodes, or raw HTML.
10
10
  ## Install
11
11
 
12
12
  ```sh
13
- npm install @peekling/runtime@0.1.0
13
+ npm install @peekling/runtime@0.1.2
14
14
  ```
15
15
 
16
16
  The runtime supports modern ESM applications and the complete browser bundle.
@@ -48,6 +48,11 @@ After `ready` resolves, the Pack, stylesheet, initial State, and owned DOM are
48
48
  ready. `character: "peek"` is a networked selection. It resolves an exact pinned
49
49
  manifest and verifies the manifest and selected atlas before browser decoding.
50
50
 
51
+ The default `companion` behavior follows the pointer when the Pack provides
52
+ locomotion. The character can also be dragged and thrown. Clicking the character
53
+ shows or hides its owned content bubble. Set `interaction: false` only when the
54
+ host needs a pointer-transparent character with no direct control.
55
+
51
56
  Other bundlers can copy `@peekling/runtime/peekling.css` to a public asset and
52
57
  pass its final URL through `styles.url`. An HTTP or HTTPS ESM module can omit
53
58
  `styles` when a usable `peekling.css` sits beside the module. A module location
@@ -85,6 +90,8 @@ mirrored asset.
85
90
  | `override(input)` | Request temporary ownership of declared presentation channels. |
86
91
  | `pause()` | Add the host pause reason and park runtime work. |
87
92
  | `resume()` | Remove the host pause reason. Work resumes after every suspension reason clears. |
93
+ | `refreshTargets()` | Refresh host-approved target geometry after an application-specific layout change. |
94
+ | `setIndicator(value)` | Replace the accessible dot or count notification, including its optional color. Pass `null` to clear it. |
88
95
  | `destroy()` | Abort pending work and release the instance's listeners, observers, frames, timers, roots, and object URLs. |
89
96
 
90
97
  Configuration failures that can be decided without Pack bytes throw before
@@ -138,6 +145,46 @@ The selected State must exist in the Pack. An Override never edits the Plan.
138
145
  When its lifetime ends, the runtime reevaluates the unchanged Plan from current
139
146
  world state.
140
147
 
148
+ ## Drag, throw, click, and custom routes
149
+
150
+ Direct manipulation is enabled without adding Plan rules:
151
+
152
+ ```js
153
+ const companion = hatch({
154
+ character: "peek",
155
+ interaction: {
156
+ gravity: 1800,
157
+ maxThrowSpeed: 1800,
158
+ bounce: 0.35,
159
+ label: "Open controls or drag Peek",
160
+ },
161
+ });
162
+ ```
163
+
164
+ Press actions can toggle, show, or hide content. They can also emit one bounded
165
+ application Event. A drag suppresses its trailing click. Throw integration is
166
+ time based, bounded to the viewport, and releases ownership back to the Plan
167
+ after landing.
168
+
169
+ Targets connect motion or catching to host-approved geometry. Custom SVG paths
170
+ remain data and are sampled with detached browser geometry. See
171
+ [Character interaction and motion](../../docs/interaction-and-motion.md) for a
172
+ complete sunlit nook, viewport traversal, target catching, notification badge,
173
+ and custom path examples.
174
+
175
+ ## Optional Canvas renderer
176
+
177
+ Use the Canvas entry point when character frame presentation should use Canvas
178
+ 2D:
179
+
180
+ ```js
181
+ import { hatchCanvas as hatch } from "@peekling/runtime/canvas";
182
+ ```
183
+
184
+ Canvas uses the same Configuration, Pack, Plan, Event queue, interaction,
185
+ physics, targets, and lifecycle as the default DOM renderer. Content bubbles
186
+ remain DOM surfaces so native controls and accessibility keep working.
187
+
141
188
  See the repository [configuration guide](../../docs/configuration.md) for Plan
142
189
  syntax and recipes. The [execution model](../../docs/execution-model.md) defines
143
190
  Event ordering, channel ownership, Override conflicts, and cleanup.
@@ -150,14 +197,14 @@ free. The element is a lifecycle facade over hatch, not a second engine.
150
197
  ```html
151
198
  <script
152
199
  defer
153
- src="https://cdn.jsdelivr.net/npm/@peekling/runtime@0.1.0/dist/peekling.min.js"
200
+ src="https://cdn.jsdelivr.net/npm/@peekling/runtime@0.1.2/dist/peekling.min.js"
154
201
  integrity="sha384-<runtime-release-hash>"
155
202
  crossorigin="anonymous"
156
203
  ></script>
157
204
 
158
205
  <peekling-character
159
206
  character="peek"
160
- styles-url="https://cdn.jsdelivr.net/npm/@peekling/runtime@0.1.0/dist/peekling.css"
207
+ styles-url="https://cdn.jsdelivr.net/npm/@peekling/runtime@0.1.2/dist/peekling.css"
161
208
  styles-integrity="sha256-<stylesheet-release-hash>"
162
209
  ></peekling-character>
163
210
  ```
@@ -255,6 +302,7 @@ admission, use the repository
255
302
  | Import | Contents |
256
303
  | -------------------------------- | ---------------------------------------------------------------------------------------------------------- |
257
304
  | `@peekling/runtime` | Side-effect-free ESM hatch API, public types, visibility helpers, and explicit Web Component registration. |
305
+ | `@peekling/runtime/canvas` | Optional Canvas 2D character renderer using the same runtime contract. |
258
306
  | `@peekling/runtime/browser` | Complete browser-global artifact. |
259
307
  | `@peekling/runtime/peekling.css` | Required runtime stylesheet asset. |
260
308
  | `@peekling/runtime/pack` | Supported Pack parsing and validation surface for authoring tools. |
@@ -272,17 +320,20 @@ host surfaces, blocked stylesheets, suspension, dismissal recovery, remount
272
320
  cleanup, hostile host layout, root repair, and contained failures in Chromium,
273
321
  Firefox, and WebKit.
274
322
 
323
+ The 0.1.2 record below measures the interaction and renderer release with the
324
+ exact release toolchain and required reserve.
325
+
275
326
  <!-- peekling-size-evidence:start -->
276
327
 
277
- The recorded canonical delivery measurement is 32,287 bytes gzip and 28,635
278
- bytes Brotli. Against the 32 KiB gzip and 32 KiB Brotli caps, that recorded
279
- build leaves 481 bytes of gzip headroom and 4,133 bytes of Brotli headroom.
280
- After the required 256-byte reserve, 225 gzip bytes and 3,877 Brotli bytes
328
+ The recorded canonical delivery measurement is 38,139 bytes gzip and 33,418
329
+ bytes Brotli. Against the 40 KiB gzip and 40 KiB Brotli caps, that recorded
330
+ build leaves 2,821 bytes of gzip headroom and 7,542 bytes of Brotli headroom.
331
+ After the required 256-byte reserve, 2,565 gzip bytes and 7,286 Brotli bytes
281
332
  remain for that build.
282
333
  <!-- peekling-size-evidence:end -->
283
334
 
284
335
  The record is bound to artifact hashes. Node 22.14.0, npm 11.16.0, and Node's
285
- default zlib compression certify the recorded values. Other supported Node
336
+ default zlib compression certify those recorded values. Other supported Node
286
337
  versions still enforce their local size cap and reserve.
287
338
 
288
339
  The [browser performance guide](../../docs/browser-performance.md) explains the
@@ -0,0 +1,17 @@
1
+ import { type CharacterRenderer, type CharacterRendererFactory } from "./renderer.js";
2
+ import { type RuntimeStyleAsset } from "./styles.js";
3
+ import type { NormalizedPack, Point } from "./types.js";
4
+ /** Optional Canvas 2D presentation for the same runtime and Plan evaluator. */
5
+ export declare class CanvasRenderer implements CharacterRenderer {
6
+ #private;
7
+ readonly host: HTMLElement;
8
+ readonly ready: Promise<void>;
9
+ constructor(document: Document, pack: NormalizedPack, atlasUrl: string, scale: number, styles: RuntimeStyleAsset);
10
+ swapAtlas(pack: NormalizedPack, atlasUrl: string): Promise<void>;
11
+ setHidden(hidden: boolean): void;
12
+ nextFrameIn(now: number): number | undefined;
13
+ render(stateName: string, now: number, position: Point, devicePixelRatio?: number, lift?: number): void;
14
+ destroy(): void;
15
+ }
16
+ export declare const createCanvasRenderer: CharacterRendererFactory;
17
+ //# sourceMappingURL=canvas-renderer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"canvas-renderer.d.ts","sourceRoot":"","sources":["../src/canvas-renderer.ts"],"names":[],"mappings":"AACA,OAAO,EAGL,KAAK,iBAAiB,EACtB,KAAK,wBAAwB,EAC9B,MAAM,eAAe,CAAC;AAEvB,OAAO,EAGL,KAAK,iBAAiB,EACvB,MAAM,aAAa,CAAC;AACrB,OAAO,KAAK,EAAE,cAAc,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAExD,+EAA+E;AAC/E,qBAAa,cAAe,YAAW,iBAAiB;;IACtD,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAC;IAC3B,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC;IAkB9B,YACE,QAAQ,EAAE,QAAQ,EAClB,IAAI,EAAE,cAAc,EACpB,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,iBAAiB,EAkC1B;IAEK,SAAS,CAAC,IAAI,EAAE,cAAc,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAMrE;IAED,SAAS,CAAC,MAAM,EAAE,OAAO,GAAG,IAAI,CAE/B;IAED,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAmB3C;IAED,MAAM,CACJ,SAAS,EAAE,MAAM,EACjB,GAAG,EAAE,MAAM,EACX,QAAQ,EAAE,KAAK,EACf,gBAAgB,SAAI,EACpB,IAAI,SAAI,GACP,IAAI,CA8DN;IAED,OAAO,IAAI,IAAI,CAOd;CA4DF;AAED,eAAO,MAAM,oBAAoB,EAAE,wBAOhC,CAAC"}
@@ -0,0 +1,192 @@
1
+ import { animationCycleMs } from "./locomotion.js";
2
+ import { frameAt, snapToDevicePixel, } from "./renderer.js";
3
+ import { runtimeMessage } from "./runtime-diagnostics.js";
4
+ import { connectRuntimeHost, ShadowStyleSheet, } from "./styles.js";
5
+ /** Optional Canvas 2D presentation for the same runtime and Plan evaluator. */
6
+ export class CanvasRenderer {
7
+ host;
8
+ ready;
9
+ #document;
10
+ #root;
11
+ #canvas;
12
+ #context;
13
+ #styles;
14
+ #canvasRule;
15
+ #pack;
16
+ #image;
17
+ #scale;
18
+ #logicalWidth = 0;
19
+ #logicalHeight = 0;
20
+ #state = "";
21
+ #stateStartedAt = 0;
22
+ #frame = -1;
23
+ #destroyed = false;
24
+ #loadGeneration = 0;
25
+ constructor(document, pack, atlasUrl, scale, styles) {
26
+ this.#document = document;
27
+ this.#pack = pack;
28
+ this.#scale = scale;
29
+ this.host = document.createElement("div");
30
+ this.host.setAttribute("aria-hidden", "true");
31
+ this.host.setAttribute("data-peekling-host", "");
32
+ this.host.setAttribute("data-peekling-renderer", "canvas");
33
+ this.#root = this.host.attachShadow({ mode: "closed" });
34
+ this.#styles = new ShadowStyleSheet(document, this.#root, styles);
35
+ this.#canvas = document.createElement("canvas");
36
+ this.#canvas.className = "sprite";
37
+ const context = this.#canvas.getContext("2d", { alpha: true });
38
+ if (!context) {
39
+ throw new Error(runtimeMessage("renderer.canvas", "Canvas 2D is unavailable"));
40
+ }
41
+ this.#context = context;
42
+ this.#image = document.createElement("img");
43
+ this.#root.append(this.#canvas);
44
+ this.ready = Promise.all([
45
+ this.#styles.ready.then(() => {
46
+ if (this.#destroyed)
47
+ return;
48
+ this.#canvasRule = this.#styles.addRule(".sprite");
49
+ }),
50
+ this.#loadAtlas(pack, atlasUrl),
51
+ ]).then(() => {
52
+ if (this.#destroyed)
53
+ return;
54
+ this.#applyGeometry(pack);
55
+ });
56
+ void this.ready.catch(() => { });
57
+ this.#repair();
58
+ }
59
+ async swapAtlas(pack, atlasUrl) {
60
+ if (this.#destroyed)
61
+ return;
62
+ await this.#loadAtlas(pack, atlasUrl);
63
+ if (this.#destroyed || this.#pack !== pack)
64
+ return;
65
+ this.#applyGeometry(pack);
66
+ this.#frame = -1;
67
+ }
68
+ setHidden(hidden) {
69
+ if (!this.#destroyed)
70
+ this.host.toggleAttribute("hidden", hidden);
71
+ }
72
+ nextFrameIn(now) {
73
+ if (this.#destroyed)
74
+ return;
75
+ const state = this.#pack.states[this.#state];
76
+ if (!state || state.frames.length < 2)
77
+ return;
78
+ const elapsed = Math.max(0, now - this.#stateStartedAt);
79
+ if (state.durations?.length === state.frames.length) {
80
+ const total = animationCycleMs(state);
81
+ if (!state.loop && elapsed >= total - state.durations.at(-1))
82
+ return;
83
+ const active = state.loop ? elapsed % total : elapsed;
84
+ let boundary = 0;
85
+ for (const duration of state.durations) {
86
+ boundary += duration;
87
+ if (boundary > active)
88
+ return boundary - active;
89
+ }
90
+ return state.loop ? state.durations[0] : undefined;
91
+ }
92
+ const interval = 1000 / (state.fps ?? 1);
93
+ if (!state.loop && elapsed >= interval * (state.frames.length - 1))
94
+ return;
95
+ return interval - (elapsed % interval || 0);
96
+ }
97
+ render(stateName, now, position, devicePixelRatio = 1, lift = 0) {
98
+ if (this.#destroyed)
99
+ return;
100
+ this.#repair();
101
+ const state = this.#pack.states[stateName];
102
+ if (!state) {
103
+ throw new Error(runtimeMessage("renderer.state", `Renderer received unknown state ${stateName}`));
104
+ }
105
+ if (stateName !== this.#state) {
106
+ this.#state = stateName;
107
+ this.#stateStartedAt = now;
108
+ this.#frame = -1;
109
+ this.host.setAttribute("data-peekling-state", stateName);
110
+ }
111
+ const frame = frameAt(state, now - this.#stateStartedAt);
112
+ if (frame !== this.#frame && this.#image.complete) {
113
+ const column = frame % this.#pack.atlas.columns;
114
+ const row = Math.floor(frame / this.#pack.atlas.columns);
115
+ const sourceWidth = this.#pack.atlas.cellWidth;
116
+ const sourceHeight = this.#pack.atlas.cellHeight;
117
+ this.#context.clearRect(0, 0, sourceWidth, sourceHeight);
118
+ this.#context.drawImage(this.#image, column * sourceWidth, row * sourceHeight, sourceWidth, sourceHeight, 0, 0, sourceWidth, sourceHeight);
119
+ this.#frame = frame;
120
+ this.host.setAttribute("data-peekling-frame", String(frame));
121
+ }
122
+ const width = this.#logicalWidth * this.#scale;
123
+ const height = this.#logicalHeight * this.#scale;
124
+ const x = snapToDevicePixel(position.x - width / 2, devicePixelRatio);
125
+ const y = snapToDevicePixel(position.y - height / 2 - lift, devicePixelRatio);
126
+ const rule = this.#canvasRule;
127
+ if (!rule) {
128
+ throw new Error(runtimeMessage("renderer.styles", "Peekling stylesheet is not ready"));
129
+ }
130
+ const transform = `translate3d(${x}px,${y}px,0) scale(${this.#scale})`;
131
+ if (rule.transform !== transform)
132
+ rule.transform = transform;
133
+ const xValue = String(x);
134
+ const yValue = String(y);
135
+ if (this.host.getAttribute("data-peekling-x") !== xValue) {
136
+ this.host.setAttribute("data-peekling-x", xValue);
137
+ }
138
+ if (this.host.getAttribute("data-peekling-y") !== yValue) {
139
+ this.host.setAttribute("data-peekling-y", yValue);
140
+ }
141
+ }
142
+ destroy() {
143
+ if (this.#destroyed)
144
+ return;
145
+ this.#destroyed = true;
146
+ this.#loadGeneration += 1;
147
+ this.#image.src = "";
148
+ this.#styles.destroy();
149
+ this.host.remove();
150
+ }
151
+ async #loadAtlas(pack, atlasUrl) {
152
+ const generation = ++this.#loadGeneration;
153
+ const image = this.#document.createElement("img");
154
+ image.decoding = "async";
155
+ await new Promise((resolve, reject) => {
156
+ image.addEventListener("load", () => resolve(), { once: true });
157
+ image.addEventListener("error", () => reject(new Error(runtimeMessage("renderer.canvas-atlas", "Canvas atlas failed"))), { once: true });
158
+ image.src = atlasUrl;
159
+ });
160
+ if (this.#destroyed || generation !== this.#loadGeneration)
161
+ return;
162
+ this.#pack = pack;
163
+ this.#image = image;
164
+ }
165
+ #applyGeometry(pack) {
166
+ const rule = this.#canvasRule;
167
+ if (!rule) {
168
+ throw new Error(runtimeMessage("renderer.styles", "Peekling stylesheet is not ready"));
169
+ }
170
+ this.#logicalWidth = pack.atlas.logicalWidth ?? pack.atlas.cellWidth;
171
+ this.#logicalHeight = pack.atlas.logicalHeight ?? pack.atlas.cellHeight;
172
+ this.#canvas.width = pack.atlas.cellWidth;
173
+ this.#canvas.height = pack.atlas.cellHeight;
174
+ rule.width = `${this.#logicalWidth}px`;
175
+ rule.height = `${this.#logicalHeight}px`;
176
+ rule.imageRendering =
177
+ (pack.atlas.density ?? 1) === 1 ? "pixelated" : "auto";
178
+ }
179
+ #repair() {
180
+ if (this.#destroyed)
181
+ return;
182
+ connectRuntimeHost(this.host, this.#document, "data-peekling-host");
183
+ if (this.#root.host !== this.host ||
184
+ this.#canvas.getRootNode() !== this.#root) {
185
+ throw new Error(runtimeMessage("renderer.root", "Peekling character root structure was changed"));
186
+ }
187
+ if (this.host.getAttribute("aria-hidden") !== "true") {
188
+ this.host.setAttribute("aria-hidden", "true");
189
+ }
190
+ }
191
+ }
192
+ export const createCanvasRenderer = (options) => new CanvasRenderer(options.document, options.pack, options.atlasUrl, options.scale, options.styles);
@@ -0,0 +1,5 @@
1
+ import { type PeeklingHatchInput, type PeeklingInstance, type PeeklingOptions } from "./runtime.js";
2
+ /** Hatch with Canvas 2D character pixels and accessible DOM content surfaces. */
3
+ export declare function hatchCanvas(input: PeeklingHatchInput | PeeklingOptions): PeeklingInstance;
4
+ export type { PeeklingHatchInput, PeeklingInstance, PeeklingOptions, } from "./runtime.js";
5
+ //# sourceMappingURL=canvas.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"canvas.d.ts","sourceRoot":"","sources":["../src/canvas.ts"],"names":[],"mappings":"AACA,OAAO,EAEL,KAAK,kBAAkB,EACvB,KAAK,gBAAgB,EACrB,KAAK,eAAe,EACrB,MAAM,cAAc,CAAC;AAEtB,iFAAiF;AACjF,wBAAgB,WAAW,CACzB,KAAK,EAAE,kBAAkB,GAAG,eAAe,GAC1C,gBAAgB,CAElB;AAED,YAAY,EACV,kBAAkB,EAClB,gBAAgB,EAChB,eAAe,GAChB,MAAM,cAAc,CAAC"}
package/dist/canvas.js ADDED
@@ -0,0 +1,6 @@
1
+ import { createCanvasRenderer } from "./canvas-renderer.js";
2
+ import { PeeklingRuntime, } from "./runtime.js";
3
+ /** Hatch with Canvas 2D character pixels and accessible DOM content surfaces. */
4
+ export function hatchCanvas(input) {
5
+ return PeeklingRuntime.hatch(input, createCanvasRenderer);
6
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"configuration-validation.d.ts","sourceRoot":"","sources":["../src/configuration-validation.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAEpD,OAAO,EAAmB,KAAK,WAAW,EAAE,MAAM,YAAY,CAAC;AA4B/D,MAAM,MAAM,sBAAsB,GAAG,CACnC,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,EACZ,MAAM,CAAC,EAAE,MAAM,KACZ,IAAI,CAAC;AAEV,MAAM,WAAW,yBAAyB;IACxC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED,qBAAa,sBAAuB,SAAQ,SAAS;IACnD,QAAQ,CAAC,MAAM,EAAE,SAAS,yBAAyB,EAAE,CAAC;IAEtD,YAAY,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAItD;CACF;AAED;;;GAGG;AACH,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,eAAe,EACxB,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,sBAAsB,GAC5B,WAAW,CA4Ib;AAED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,eAAe,CAAC,MAAM,CAAC,GAAG,OAAO,CAExE"}
1
+ {"version":3,"file":"configuration-validation.d.ts","sourceRoot":"","sources":["../src/configuration-validation.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAEpD,OAAO,EAAmB,KAAK,WAAW,EAAE,MAAM,YAAY,CAAC;AAgC/D,MAAM,MAAM,sBAAsB,GAAG,CACnC,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,EACZ,MAAM,CAAC,EAAE,MAAM,KACZ,IAAI,CAAC;AAEV,MAAM,WAAW,yBAAyB;IACxC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED,qBAAa,sBAAuB,SAAQ,SAAS;IACnD,QAAQ,CAAC,MAAM,EAAE,SAAS,yBAAyB,EAAE,CAAC;IAEtD,YAAY,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAItD;CACF;AAED;;;GAGG;AACH,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,eAAe,EACxB,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,sBAAsB,GAC5B,WAAW,CA6Jb;AAkQD,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,eAAe,CAAC,MAAM,CAAC,GAAG,OAAO,CAExE"}
@@ -1,5 +1,5 @@
1
1
  import { validateHostContent } from "./content.js";
2
- import { isHttpPath, isSurfaceColor, NAME_PATTERN } from "./contracts.js";
2
+ import { EVENT_NAME_PATTERN, isHttpPath, isSurfaceColor, NAME_PATTERN, STATE_NAME_PATTERN, } from "./contracts.js";
3
3
  import { isRegisteredCharacter } from "./registry.js";
4
4
  import { compactRuntimeDiagnostics } from "./runtime-diagnostics.js";
5
5
  import { validStyleIntegrity } from "./styles.js";
@@ -13,6 +13,10 @@ const CONFIGURATION_FIELDS = [
13
13
  "atlasUrl",
14
14
  "styles",
15
15
  "plan",
16
+ "preset",
17
+ "targets",
18
+ "interaction",
19
+ "indicator",
16
20
  "content",
17
21
  "bindings",
18
22
  "theme",
@@ -50,6 +54,13 @@ export function validateConfiguration(options, baseUrl, fault) {
50
54
  if (options.format !== undefined && options.format !== 1) {
51
55
  reportConfigurationFault("unsupported-format", "$.format", fault);
52
56
  }
57
+ if (options.plan !== undefined && options.preset !== undefined) {
58
+ reportConfigurationFault("incompatible-behavior", "$.preset", fault);
59
+ }
60
+ if (options.preset !== undefined &&
61
+ !["companion", "still", "bottom-patrol", "viewport-roam"].includes(options.preset)) {
62
+ reportConfigurationFault("invalid-preset", "$.preset", fault);
63
+ }
53
64
  if (options.character !== undefined) {
54
65
  if (typeof options.character !== "string" ||
55
66
  !NAME_PATTERN.test(options.character)) {
@@ -127,6 +138,12 @@ export function validateConfiguration(options, baseUrl, fault) {
127
138
  content = {};
128
139
  }
129
140
  validateBindings(options.bindings, content, fault);
141
+ validateTargets(options.targets, fault);
142
+ validateInteraction(options.interaction, options.targets, fault);
143
+ validateIndicator(options.indicator, fault);
144
+ if (options.interaction === false && options.indicator !== undefined) {
145
+ reportConfigurationFault("incompatible-indicator", "$.indicator", fault);
146
+ }
130
147
  validateTheme(options.theme, fault);
131
148
  validateDiagnostics(options.diagnostics, fault);
132
149
  validateAccessibility(options.accessibility, fault);
@@ -137,6 +154,164 @@ export function validateConfiguration(options, baseUrl, fault) {
137
154
  }
138
155
  return content;
139
156
  }
157
+ function validateInteraction(interaction, targets, fault) {
158
+ if (interaction === undefined || interaction === false)
159
+ return;
160
+ const checked = shape(interaction, "invalid-interaction", "$.interaction", [
161
+ "press",
162
+ "pressEvent",
163
+ "drag",
164
+ "throw",
165
+ "dragState",
166
+ "riseState",
167
+ "fallState",
168
+ "landState",
169
+ "gravity",
170
+ "maxThrowSpeed",
171
+ "bounce",
172
+ "floorInset",
173
+ "label",
174
+ "contentInitiallyHidden",
175
+ "clearIndicatorOnPress",
176
+ "catchTarget",
177
+ "catchAnchor",
178
+ "catchMargin",
179
+ "catchEvent",
180
+ ], fault);
181
+ if (collectConfigurationFaults && !checked)
182
+ return;
183
+ const value = checked;
184
+ if (value.press !== undefined &&
185
+ ![
186
+ "toggle-content",
187
+ "show-content",
188
+ "hide-content",
189
+ "emit",
190
+ "none",
191
+ ].includes(value.press)) {
192
+ reportConfigurationFault("invalid-interaction", "$.interaction.press", fault);
193
+ }
194
+ if (value.pressEvent !== undefined &&
195
+ (typeof value.pressEvent !== "string" ||
196
+ !EVENT_NAME_PATTERN.test(value.pressEvent))) {
197
+ reportConfigurationFault("invalid-interaction-event", "$.interaction.pressEvent", fault);
198
+ }
199
+ if (value.press === "emit" && value.pressEvent === undefined) {
200
+ reportConfigurationFault("missing-interaction-event", "$.interaction.pressEvent", fault);
201
+ }
202
+ if (value.pressEvent !== undefined &&
203
+ value.press !== undefined &&
204
+ value.press !== "emit") {
205
+ reportConfigurationFault("incompatible-interaction-event", "$.interaction.pressEvent", fault);
206
+ }
207
+ for (const field of [
208
+ "drag",
209
+ "throw",
210
+ "contentInitiallyHidden",
211
+ "clearIndicatorOnPress",
212
+ ]) {
213
+ if (value[field] !== undefined && typeof value[field] !== "boolean") {
214
+ reportConfigurationFault("invalid-interaction", `$.interaction.${field}`, fault);
215
+ }
216
+ }
217
+ for (const field of [
218
+ "dragState",
219
+ "riseState",
220
+ "fallState",
221
+ "landState",
222
+ ]) {
223
+ if (value[field] !== undefined &&
224
+ (typeof value[field] !== "string" ||
225
+ !STATE_NAME_PATTERN.test(value[field]))) {
226
+ reportConfigurationFault("invalid-interaction-state", `$.interaction.${field}`, fault);
227
+ }
228
+ }
229
+ for (const [field, minimum, maximum] of [
230
+ ["gravity", 0, 10_000],
231
+ ["maxThrowSpeed", 0, 10_000],
232
+ ["bounce", 0, 1],
233
+ ["floorInset", 0, 1_000],
234
+ ["catchMargin", 0, 1_000],
235
+ ]) {
236
+ const item = value[field];
237
+ if (item !== undefined &&
238
+ (typeof item !== "number" ||
239
+ !Number.isFinite(item) ||
240
+ item < minimum ||
241
+ item > maximum)) {
242
+ reportConfigurationFault("invalid-interaction-range", `$.interaction.${field}`, fault);
243
+ }
244
+ }
245
+ if (value.label !== undefined && !text(value.label, 120)) {
246
+ reportConfigurationFault("invalid-interaction-label", "$.interaction.label", fault);
247
+ }
248
+ if (value.catchTarget !== undefined &&
249
+ (typeof value.catchTarget !== "string" ||
250
+ !NAME_PATTERN.test(value.catchTarget) ||
251
+ !Object.hasOwn(targets ?? {}, value.catchTarget))) {
252
+ reportConfigurationFault("invalid-catch-target", "$.interaction.catchTarget", fault);
253
+ }
254
+ if (value.catchAnchor !== undefined &&
255
+ !["center", "top", "right", "bottom", "left"].includes(value.catchAnchor)) {
256
+ reportConfigurationFault("invalid-catch-anchor", "$.interaction.catchAnchor", fault);
257
+ }
258
+ if (value.catchEvent !== undefined &&
259
+ (typeof value.catchEvent !== "string" ||
260
+ !EVENT_NAME_PATTERN.test(value.catchEvent))) {
261
+ reportConfigurationFault("invalid-interaction-event", "$.interaction.catchEvent", fault);
262
+ }
263
+ }
264
+ function validateIndicator(indicator, fault) {
265
+ if (indicator === undefined)
266
+ return;
267
+ const checked = shape(indicator, "invalid-indicator", "$.indicator", ["kind", "count", "label", "color", "visible"], fault);
268
+ if (collectConfigurationFaults && !checked)
269
+ return;
270
+ const value = checked;
271
+ if (value.kind !== undefined &&
272
+ value.kind !== "dot" &&
273
+ value.kind !== "count") {
274
+ reportConfigurationFault("invalid-indicator", "$.indicator.kind", fault);
275
+ }
276
+ if (!text(value.label, 120)) {
277
+ reportConfigurationFault("invalid-indicator", "$.indicator.label", fault);
278
+ }
279
+ if (value.count !== undefined &&
280
+ (typeof value.count !== "number" ||
281
+ !Number.isInteger(value.count) ||
282
+ value.count < 0 ||
283
+ value.count > 999)) {
284
+ reportConfigurationFault("invalid-indicator", "$.indicator.count", fault);
285
+ }
286
+ if (value.color !== undefined && !isSurfaceColor(value.color)) {
287
+ reportConfigurationFault("invalid-indicator", "$.indicator.color", fault);
288
+ }
289
+ if (value.visible !== undefined && typeof value.visible !== "boolean") {
290
+ reportConfigurationFault("invalid-indicator", "$.indicator.visible", fault);
291
+ }
292
+ }
293
+ function validateTargets(targets, fault) {
294
+ if (targets === undefined)
295
+ return;
296
+ const checked = shape(targets, "invalid-targets", "$.targets", undefined, fault);
297
+ if (collectConfigurationFaults && !checked)
298
+ return;
299
+ const entries = Object.entries(checked);
300
+ if (entries.length > 16) {
301
+ reportConfigurationFault("target-limit", "$.targets", fault);
302
+ }
303
+ for (const [id, selector] of entries) {
304
+ if (!NAME_PATTERN.test(id)) {
305
+ reportConfigurationFault("invalid-target", `$.targets.${id}`, fault, id);
306
+ }
307
+ if (typeof selector !== "string" ||
308
+ selector.length < 1 ||
309
+ selector.length > 256 ||
310
+ /[\0\r\n]/.test(selector)) {
311
+ reportConfigurationFault("invalid-target-selector", `$.targets.${id}`, fault, id);
312
+ }
313
+ }
314
+ }
140
315
  export function validRuntimeName(value) {
141
316
  return value === undefined || typeof value === "boolean" || text(value, 80);
142
317
  }
package/dist/content.d.ts CHANGED
@@ -39,6 +39,9 @@ export declare class ContentRenderer {
39
39
  width: number;
40
40
  height: number;
41
41
  }, name?: string): void;
42
+ setUserHidden(hidden: boolean): boolean;
43
+ toggleUserHidden(): boolean;
44
+ get userHidden(): boolean;
42
45
  destroy(): void;
43
46
  }
44
47
  //# sourceMappingURL=content.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"content.d.ts","sourceRoot":"","sources":["../src/content.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,aAAa,EAClB,KAAK,WAAW,EAGhB,KAAK,YAAY,EAGjB,KAAK,SAAS,EACd,KAAK,KAAK,EACV,KAAK,YAAY,EAClB,MAAM,YAAY,CAAC;AAIpB,OAAO,EAIL,KAAK,iBAAiB,EACvB,MAAM,aAAa,CAAC;AAErB,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,WAAW,GAAG,SAAS,EAC9B,OAAO,EAAE,MAAM,GACd,WAAW,CA2Cb;AAyGD,wBAAgB,yBAAyB,CACvC,GAAG,EAAE,SAAS,CAAC,MAAM,GAAG,SAAS,CAAC,EAAE,EACpC,OAAO,EAAE,WAAW,GACnB,IAAI,CAON;AAED,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,aAAa,EACrB,QAAQ,EAAE,KAAK,EACf,QAAQ,EAAE;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,EAC3C,SAAS,EAAE;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,EAC5C,OAAO,EAAE;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GACzC,KAAK,CAwBP;AA8CD,MAAM,WAAW,gBAAgB;IAC/B,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB,GAAG,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,sBAAsB;IACrC,UAAU,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACvC,QAAQ,CAAC,EAAE,YAAY,CAAC;IACxB,KAAK,CAAC,EAAE,YAAY,CAAC;IACrB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,OAAO,KAAK,OAAO,YAAY,EAAE,UAAU,CAAC;IAC5E,IAAI,CAAC,EAAE,MAAM,IAAI,CAAC;IAClB,MAAM,CAAC,EAAE,iBAAiB,CAAC;CAC5B;AAED,qBAAa,eAAe;;IAC1B,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC;IA2B9B,YAAY,QAAQ,EAAE,QAAQ,EAAE,OAAO,GAAE,sBAA2B,EAmCnE;IAED,cAAc,CACZ,UAAU,EAAE,SAAS,gBAAgB,EAAE,EACvC,OAAO,EAAE,WAAW,EACpB,QAAQ,EAAE,KAAK,EACf,QAAQ,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,EAC3C,SAAS,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,EAC5C,IAAI,CAAC,EAAE,MAAM,GACZ,IAAI,CAuIN;IAmXD,OAAO,IAAI,IAAI,CAQd;CA0DF"}
1
+ {"version":3,"file":"content.d.ts","sourceRoot":"","sources":["../src/content.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,aAAa,EAClB,KAAK,WAAW,EAGhB,KAAK,YAAY,EAGjB,KAAK,SAAS,EACd,KAAK,KAAK,EACV,KAAK,YAAY,EAClB,MAAM,YAAY,CAAC;AAIpB,OAAO,EAIL,KAAK,iBAAiB,EACvB,MAAM,aAAa,CAAC;AAErB,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,WAAW,GAAG,SAAS,EAC9B,OAAO,EAAE,MAAM,GACd,WAAW,CA2Cb;AAyGD,wBAAgB,yBAAyB,CACvC,GAAG,EAAE,SAAS,CAAC,MAAM,GAAG,SAAS,CAAC,EAAE,EACpC,OAAO,EAAE,WAAW,GACnB,IAAI,CAON;AAED,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,aAAa,EACrB,QAAQ,EAAE,KAAK,EACf,QAAQ,EAAE;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,EAC3C,SAAS,EAAE;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,EAC5C,OAAO,EAAE;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GACzC,KAAK,CAwBP;AA8CD,MAAM,WAAW,gBAAgB;IAC/B,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB,GAAG,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,sBAAsB;IACrC,UAAU,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACvC,QAAQ,CAAC,EAAE,YAAY,CAAC;IACxB,KAAK,CAAC,EAAE,YAAY,CAAC;IACrB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,OAAO,KAAK,OAAO,YAAY,EAAE,UAAU,CAAC;IAC5E,IAAI,CAAC,EAAE,MAAM,IAAI,CAAC;IAClB,MAAM,CAAC,EAAE,iBAAiB,CAAC;CAC5B;AAED,qBAAa,eAAe;;IAC1B,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC;IA6B9B,YAAY,QAAQ,EAAE,QAAQ,EAAE,OAAO,GAAE,sBAA2B,EAmCnE;IAED,cAAc,CACZ,UAAU,EAAE,SAAS,gBAAgB,EAAE,EACvC,OAAO,EAAE,WAAW,EACpB,QAAQ,EAAE,KAAK,EACf,QAAQ,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,EAC3C,SAAS,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,EAC5C,IAAI,CAAC,EAAE,MAAM,GACZ,IAAI,CA0IN;IAED,aAAa,CAAC,MAAM,EAAE,OAAO,GAAG,OAAO,CAatC;IAED,gBAAgB,IAAI,OAAO,CAE1B;IAED,IAAI,UAAU,IAAI,OAAO,CAExB;IAmXD,OAAO,IAAI,IAAI,CAQd;CA0DF"}
package/dist/content.js CHANGED
@@ -150,6 +150,8 @@ export class ContentRenderer {
150
150
  #name;
151
151
  #nameSelection;
152
152
  #nameItem;
153
+ #userHidden = false;
154
+ #hasVisibleSurface = false;
153
155
  #destroyed = false;
154
156
  constructor(document, options = {}) {
155
157
  this.#document = document;
@@ -224,6 +226,17 @@ export class ContentRenderer {
224
226
  if (this.#destroyed)
225
227
  return;
226
228
  }
229
+ let visible = false;
230
+ for (const surface of this.#surfaces.values()) {
231
+ if (!surface.host.hidden) {
232
+ visible = true;
233
+ break;
234
+ }
235
+ }
236
+ this.#hasVisibleSurface = visible;
237
+ const hidden = this.#userHidden || !visible;
238
+ if (this.#host.hidden !== hidden)
239
+ this.#host.hidden = hidden;
227
240
  const viewportChanged = viewport.width !== this.#viewportWidth ||
228
241
  viewport.height !== this.#viewportHeight;
229
242
  const placementChanged = viewportChanged ||
@@ -305,15 +318,28 @@ export class ContentRenderer {
305
318
  }
306
319
  this.#placementDirty = false;
307
320
  }
308
- let visible = false;
309
- for (const surface of this.#surfaces.values()) {
310
- if (!surface.host.hidden) {
311
- visible = true;
312
- break;
321
+ }
322
+ setUserHidden(hidden) {
323
+ if (this.#destroyed)
324
+ return false;
325
+ const wasHidden = this.#userHidden;
326
+ this.#userHidden = hidden;
327
+ this.#host.hidden = hidden || !this.#hasVisibleSurface;
328
+ if (wasHidden && !hidden && !this.#host.hidden) {
329
+ for (const surface of this.#surfaces.values()) {
330
+ if (!surface.host.hidden)
331
+ surface.measureDirty = true;
313
332
  }
333
+ this.#placementDirty = true;
334
+ this.#wake();
314
335
  }
315
- if (this.#host.hidden === visible)
316
- this.#host.hidden = !visible;
336
+ return !this.#userHidden;
337
+ }
338
+ toggleUserHidden() {
339
+ return this.setUserHidden(!this.#userHidden);
340
+ }
341
+ get userHidden() {
342
+ return this.#userHidden;
317
343
  }
318
344
  #select(selection, item, generation) {
319
345
  for (const region of CONTENT_REGIONS) {