@peekling/runtime 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.
Files changed (120) hide show
  1. package/AUTHORS +8 -0
  2. package/LICENSE +202 -0
  3. package/LICENSING.md +20 -0
  4. package/NOTICE +6 -0
  5. package/README.md +294 -0
  6. package/dist/configuration-validation.d.ts +19 -0
  7. package/dist/configuration-validation.d.ts.map +1 -0
  8. package/dist/configuration-validation.js +298 -0
  9. package/dist/content.d.ts +44 -0
  10. package/dist/content.d.ts.map +1 -0
  11. package/dist/content.js +738 -0
  12. package/dist/contracts.d.ts +10 -0
  13. package/dist/contracts.d.ts.map +1 -0
  14. package/dist/contracts.js +106 -0
  15. package/dist/defaults.d.ts +7 -0
  16. package/dist/defaults.d.ts.map +1 -0
  17. package/dist/defaults.js +6 -0
  18. package/dist/diagnostics.d.ts +25 -0
  19. package/dist/diagnostics.d.ts.map +1 -0
  20. package/dist/diagnostics.js +122 -0
  21. package/dist/direction.d.ts +3 -0
  22. package/dist/direction.d.ts.map +1 -0
  23. package/dist/direction.js +6 -0
  24. package/dist/errors.d.ts +15 -0
  25. package/dist/errors.d.ts.map +1 -0
  26. package/dist/errors.js +53 -0
  27. package/dist/events.d.ts +5 -0
  28. package/dist/events.d.ts.map +1 -0
  29. package/dist/events.js +60 -0
  30. package/dist/index.d.ts +6 -0
  31. package/dist/index.d.ts.map +1 -0
  32. package/dist/index.js +3 -0
  33. package/dist/input.d.ts +31 -0
  34. package/dist/input.d.ts.map +1 -0
  35. package/dist/input.js +215 -0
  36. package/dist/json.d.ts +3 -0
  37. package/dist/json.d.ts.map +1 -0
  38. package/dist/json.js +109 -0
  39. package/dist/loader.d.ts +33 -0
  40. package/dist/loader.d.ts.map +1 -0
  41. package/dist/loader.js +477 -0
  42. package/dist/locomotion.d.ts +8 -0
  43. package/dist/locomotion.d.ts.map +1 -0
  44. package/dist/locomotion.js +29 -0
  45. package/dist/normalized.d.ts +5 -0
  46. package/dist/normalized.d.ts.map +1 -0
  47. package/dist/normalized.js +266 -0
  48. package/dist/overrides.d.ts +29 -0
  49. package/dist/overrides.d.ts.map +1 -0
  50. package/dist/overrides.js +296 -0
  51. package/dist/own-data.d.ts +18 -0
  52. package/dist/own-data.d.ts.map +1 -0
  53. package/dist/own-data.js +205 -0
  54. package/dist/pack-api.d.ts +9 -0
  55. package/dist/pack-api.d.ts.map +1 -0
  56. package/dist/pack-api.js +6 -0
  57. package/dist/pack-shared.d.ts +28 -0
  58. package/dist/pack-shared.d.ts.map +1 -0
  59. package/dist/pack-shared.js +408 -0
  60. package/dist/pack.d.ts +6 -0
  61. package/dist/pack.d.ts.map +1 -0
  62. package/dist/pack.js +316 -0
  63. package/dist/peekling.css +1 -0
  64. package/dist/peekling.css.sri +1 -0
  65. package/dist/peekling.js +7469 -0
  66. package/dist/peekling.js.sri +1 -0
  67. package/dist/peekling.min.js +2 -0
  68. package/dist/peekling.min.js.sri +1 -0
  69. package/dist/plan-compiler.d.ts +82 -0
  70. package/dist/plan-compiler.d.ts.map +1 -0
  71. package/dist/plan-compiler.js +724 -0
  72. package/dist/plan.d.ts +21 -0
  73. package/dist/plan.d.ts.map +1 -0
  74. package/dist/plan.js +287 -0
  75. package/dist/preflight-api.d.ts +3 -0
  76. package/dist/preflight-api.d.ts.map +1 -0
  77. package/dist/preflight-api.js +1 -0
  78. package/dist/preflight.d.ts +22 -0
  79. package/dist/preflight.d.ts.map +1 -0
  80. package/dist/preflight.js +158 -0
  81. package/dist/registry.d.ts +9 -0
  82. package/dist/registry.d.ts.map +1 -0
  83. package/dist/registry.js +24 -0
  84. package/dist/renderer.d.ts +16 -0
  85. package/dist/renderer.d.ts.map +1 -0
  86. package/dist/renderer.js +176 -0
  87. package/dist/resolver.d.ts +3 -0
  88. package/dist/resolver.d.ts.map +1 -0
  89. package/dist/resolver.js +30 -0
  90. package/dist/runtime-diagnostics.d.ts +3 -0
  91. package/dist/runtime-diagnostics.d.ts.map +1 -0
  92. package/dist/runtime-diagnostics.js +5 -0
  93. package/dist/runtime-validation.d.ts +6 -0
  94. package/dist/runtime-validation.d.ts.map +1 -0
  95. package/dist/runtime-validation.js +104 -0
  96. package/dist/runtime.d.ts +87 -0
  97. package/dist/runtime.d.ts.map +1 -0
  98. package/dist/runtime.js +1015 -0
  99. package/dist/sections.d.ts +15 -0
  100. package/dist/sections.d.ts.map +1 -0
  101. package/dist/sections.js +221 -0
  102. package/dist/styles.d.ts +18 -0
  103. package/dist/styles.d.ts.map +1 -0
  104. package/dist/styles.js +132 -0
  105. package/dist/tooling-preflight.d.ts +23 -0
  106. package/dist/tooling-preflight.d.ts.map +1 -0
  107. package/dist/tooling-preflight.js +207 -0
  108. package/dist/tooling-validation.d.ts +8 -0
  109. package/dist/tooling-validation.d.ts.map +1 -0
  110. package/dist/tooling-validation.js +94 -0
  111. package/dist/types.d.ts +403 -0
  112. package/dist/types.d.ts.map +1 -0
  113. package/dist/types.js +29 -0
  114. package/dist/visibility.d.ts +12 -0
  115. package/dist/visibility.d.ts.map +1 -0
  116. package/dist/visibility.js +333 -0
  117. package/dist/web-component.d.ts +31 -0
  118. package/dist/web-component.d.ts.map +1 -0
  119. package/dist/web-component.js +236 -0
  120. package/package.json +74 -0
@@ -0,0 +1,333 @@
1
+ import { connectRuntimeHost, ShadowStyleSheet, } from "./styles.js";
2
+ import { runtimeMessage } from "./runtime-diagnostics.js";
3
+ const PREFERENCE_KEY = "peekling:site-preference";
4
+ const SESSION_KEY = "peekling:hidden-session";
5
+ const CORNER_SIZE = 64;
6
+ const HOLD_MS = 1_000;
7
+ const HIDE_OPTIONS = [
8
+ ["10 minutes", "10-minutes"],
9
+ ["1 hour", "1-hour"],
10
+ ["Until tomorrow", "until-tomorrow"],
11
+ ["Until I return", "session"],
12
+ ["Forever on this site", "forever"],
13
+ ];
14
+ const controllers = new WeakMap();
15
+ function readVisibility(window, now = Date.now()) {
16
+ try {
17
+ if (window.sessionStorage.getItem(SESSION_KEY) === "1")
18
+ return [true];
19
+ const value = JSON.parse(window.localStorage.getItem(PREFERENCE_KEY) ?? "null");
20
+ if (value?.visibility !== "hidden")
21
+ return [false];
22
+ if (value.until === null)
23
+ return [true];
24
+ if (Number.isFinite(value.until) && value.until > now)
25
+ return [true, value.until];
26
+ window.localStorage.removeItem(PREFERENCE_KEY);
27
+ }
28
+ catch {
29
+ // Storage is optional. Fail open so a broken preference never traps a host.
30
+ }
31
+ return [false];
32
+ }
33
+ function readHidden(window, now = Date.now()) {
34
+ return readVisibility(window, now)[0];
35
+ }
36
+ function writeHidden(window, duration) {
37
+ const option = HIDE_OPTIONS.findIndex((item) => item[1] === duration);
38
+ if (option < 0)
39
+ throw new TypeError(runtimeMessage("visibility.duration", "Invalid Peekling hide duration"));
40
+ const now = new Date();
41
+ const until = option > 2
42
+ ? undefined
43
+ : option < 2
44
+ ? now.getTime() + (option ? 60 : 10) * 60_000
45
+ : new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1).getTime();
46
+ try {
47
+ window.sessionStorage.removeItem(SESSION_KEY);
48
+ if (option === 3) {
49
+ window.localStorage.removeItem(PREFERENCE_KEY);
50
+ window.sessionStorage.setItem(SESSION_KEY, "1");
51
+ return until;
52
+ }
53
+ window.localStorage.setItem(PREFERENCE_KEY, JSON.stringify({
54
+ visibility: "hidden",
55
+ until: option === 4 ? null : until,
56
+ }));
57
+ }
58
+ catch {
59
+ // The current page still hides even when storage is unavailable.
60
+ }
61
+ return until;
62
+ }
63
+ function clearHidden(window) {
64
+ try {
65
+ window.localStorage.removeItem(PREFERENCE_KEY);
66
+ window.sessionStorage.removeItem(SESSION_KEY);
67
+ }
68
+ catch {
69
+ // The current page still restores even when storage is unavailable.
70
+ }
71
+ }
72
+ class SiteVisibilityController {
73
+ ready;
74
+ #document;
75
+ #window;
76
+ #listeners = new Set();
77
+ #persistentRemovers = [];
78
+ #interactionRemovers = [];
79
+ #host;
80
+ #root;
81
+ #panel;
82
+ #keyboardTrigger;
83
+ #styles;
84
+ #styleKey;
85
+ #styleIntegrity;
86
+ #hidden;
87
+ #holdTimer = 0;
88
+ #expiryTimer = 0;
89
+ #open = false;
90
+ #destroyed = false;
91
+ constructor(document, window, styles) {
92
+ this.#document = document;
93
+ this.#window = window;
94
+ const initial = readVisibility(window);
95
+ this.#hidden = initial[0];
96
+ this.#host = document.createElement("div");
97
+ this.#host.dataset.peeklingControls = "";
98
+ this.#root = this.#host.attachShadow({ mode: "closed" });
99
+ this.#styleKey = `${styles.url}\n${styles.integrity ?? ""}`;
100
+ this.#styleIntegrity = styles.integrity;
101
+ this.#styles = new ShadowStyleSheet(document, this.#root, styles);
102
+ this.ready = this.#styles.ready;
103
+ void this.ready.catch(() => { });
104
+ this.#keyboardTrigger = document.createElement("button");
105
+ this.#keyboardTrigger.className = "peekling-visibility-trigger";
106
+ this.#keyboardTrigger.type = "button";
107
+ this.#keyboardTrigger.setAttribute("aria-label", "Open Peekling visibility controls");
108
+ this.#keyboardTrigger.textContent = "🐾";
109
+ this.#panel = document.createElement("section");
110
+ this.#panel.className = "peekling-visibility-panel";
111
+ this.#panel.hidden = true;
112
+ this.#panel.setAttribute("role", "dialog");
113
+ this.#panel.setAttribute("aria-label", "Let Peekling rest");
114
+ const heading = document.createElement("h2");
115
+ heading.textContent = "Let Peekling rest?";
116
+ this.#panel.append(heading);
117
+ for (const [label, duration] of HIDE_OPTIONS) {
118
+ const button = document.createElement("button");
119
+ button.className = "peekling-visibility-option";
120
+ button.type = "button";
121
+ button.textContent = label;
122
+ button.addEventListener("click", () => this.hide(duration));
123
+ this.#panel.append(button);
124
+ }
125
+ this.#root.append(this.#keyboardTrigger, this.#panel);
126
+ this.#repair();
127
+ this.#listen(window, "storage", () => this.refresh(), this.#persistentRemovers);
128
+ this.#applyHidden(initial[0], false);
129
+ this.#scheduleExpiry(initial[1]);
130
+ }
131
+ #attachInteractions() {
132
+ if (this.#interactionRemovers.length)
133
+ return;
134
+ this.#listen(this.#document, "pointermove", (event) => {
135
+ const pointer = event;
136
+ if (pointer.pointerType === "touch" || pointer.pointerType === "pen")
137
+ return;
138
+ this.#updateCorner(pointer.clientX, pointer.clientY);
139
+ }, this.#interactionRemovers);
140
+ this.#listen(this.#document, "pointerdown", (event) => {
141
+ const pointer = event;
142
+ if (this.#open && !this.#host.contains(event.target))
143
+ this.close();
144
+ if (pointer.pointerType === "touch" || pointer.pointerType === "pen")
145
+ this.#updateCorner(pointer.clientX, pointer.clientY);
146
+ }, this.#interactionRemovers);
147
+ for (const type of ["pointerup", "pointercancel", "pointerout"])
148
+ this.#listen(this.#document, type, (event) => {
149
+ if (type !== "pointerout" ||
150
+ event.relatedTarget === null)
151
+ this.#cancelHold();
152
+ }, this.#interactionRemovers);
153
+ this.#listen(this.#document, "keydown", (event) => {
154
+ if (event.key === "Escape")
155
+ this.close(true);
156
+ }, this.#interactionRemovers);
157
+ const open = () => this.open(true);
158
+ this.#listen(this.#keyboardTrigger, "click", open, this.#interactionRemovers);
159
+ }
160
+ #detachInteractions() {
161
+ this.#cancelHold();
162
+ this.#remove(this.#interactionRemovers);
163
+ }
164
+ register(listener) {
165
+ this.#listeners.add(listener);
166
+ listener(this.#hidden);
167
+ return () => {
168
+ this.#listeners.delete(listener);
169
+ if (!this.#listeners.size)
170
+ this.destroy();
171
+ };
172
+ }
173
+ assertStyles(styles) {
174
+ if (`${styles.url}\n${styles.integrity ?? ""}` === this.#styleKey)
175
+ return;
176
+ if (styles.integrity && styles.integrity === this.#styleIntegrity)
177
+ return;
178
+ throw new TypeError(runtimeMessage("visibility.styles-conflict", "Shared visibility stylesheet conflicts with the existing document controller"));
179
+ }
180
+ isHidden() {
181
+ return this.#hidden;
182
+ }
183
+ hide(duration) {
184
+ const until = writeHidden(this.#window, duration);
185
+ this.#setHidden(true);
186
+ this.#scheduleExpiry(until);
187
+ this.close();
188
+ }
189
+ show() {
190
+ clearHidden(this.#window);
191
+ this.refresh();
192
+ }
193
+ refresh() {
194
+ try {
195
+ this.#repair();
196
+ }
197
+ catch {
198
+ const listeners = [...this.#listeners];
199
+ this.destroy();
200
+ for (const listener of listeners)
201
+ listener(null);
202
+ return;
203
+ }
204
+ const state = readVisibility(this.#window);
205
+ this.#setHidden(state[0]);
206
+ this.#scheduleExpiry(state[1]);
207
+ }
208
+ open(focus = false) {
209
+ if (this.#hidden)
210
+ return;
211
+ this.#cancelHold();
212
+ this.#open = true;
213
+ this.#host.setAttribute("data-open", "");
214
+ this.#panel.hidden = false;
215
+ if (focus)
216
+ this.#panel.querySelector("button")?.focus();
217
+ }
218
+ close(returnFocus = false) {
219
+ if (!this.#open)
220
+ return;
221
+ this.#open = false;
222
+ this.#host.removeAttribute("data-open");
223
+ this.#panel.hidden = true;
224
+ if (returnFocus)
225
+ this.#keyboardTrigger.focus();
226
+ }
227
+ destroy() {
228
+ if (this.#destroyed)
229
+ return;
230
+ this.#destroyed = true;
231
+ this.#detachInteractions();
232
+ this.#scheduleExpiry();
233
+ this.#remove(this.#persistentRemovers);
234
+ this.#styles.destroy();
235
+ this.#host.remove();
236
+ this.#listeners.clear();
237
+ controllers.delete(this.#document);
238
+ }
239
+ #updateCorner(x, y) {
240
+ if (this.#hidden ||
241
+ this.#open ||
242
+ x < this.#window.innerWidth - CORNER_SIZE ||
243
+ y < this.#window.innerHeight - CORNER_SIZE) {
244
+ this.#cancelHold();
245
+ return;
246
+ }
247
+ if (!this.#holdTimer)
248
+ this.#holdTimer = this.#window.setTimeout(() => this.open(), HOLD_MS);
249
+ }
250
+ #cancelHold() {
251
+ if (this.#holdTimer)
252
+ this.#window.clearTimeout(this.#holdTimer);
253
+ this.#holdTimer = 0;
254
+ }
255
+ #scheduleExpiry(until) {
256
+ if (this.#expiryTimer)
257
+ this.#window.clearTimeout(this.#expiryTimer);
258
+ this.#expiryTimer = 0;
259
+ if (until === undefined || this.#destroyed)
260
+ return;
261
+ const delay = Math.max(0, Math.min(until - Date.now(), 2_147_000_000));
262
+ this.#expiryTimer = this.#window.setTimeout(() => {
263
+ this.#expiryTimer = 0;
264
+ this.refresh();
265
+ }, delay);
266
+ }
267
+ #setHidden(hidden) {
268
+ if (hidden === this.#hidden)
269
+ return;
270
+ this.#applyHidden(hidden, true);
271
+ }
272
+ #applyHidden(hidden, notify) {
273
+ this.#hidden = hidden;
274
+ this.#host.toggleAttribute("data-hidden", hidden);
275
+ this.#keyboardTrigger.hidden = hidden;
276
+ if (hidden)
277
+ this.#detachInteractions();
278
+ else
279
+ this.#attachInteractions();
280
+ if (notify) {
281
+ for (const listener of this.#listeners)
282
+ listener(hidden);
283
+ }
284
+ }
285
+ #listen(target, type, listener, removers) {
286
+ target.addEventListener(type, listener, { passive: true });
287
+ removers.push([target, type, listener]);
288
+ }
289
+ #remove(bindings) {
290
+ for (const [target, type, listener] of bindings.splice(0))
291
+ target.removeEventListener(type, listener);
292
+ }
293
+ #repair() {
294
+ connectRuntimeHost(this.#host, this.#document, "data-peekling-controls");
295
+ if (this.#root.host !== this.#host ||
296
+ this.#keyboardTrigger.getRootNode() !== this.#root ||
297
+ this.#panel.getRootNode() !== this.#root)
298
+ throw new Error(runtimeMessage("visibility.root", "Peekling visibility root structure was changed"));
299
+ }
300
+ }
301
+ export function registerSiteVisibility(document, window, listener, styles) {
302
+ let controller = controllers.get(document);
303
+ if (!controller) {
304
+ controller = new SiteVisibilityController(document, window, styles);
305
+ controllers.set(document, controller);
306
+ }
307
+ else
308
+ controller.assertStyles(styles);
309
+ return { unregister: controller.register(listener), ready: controller.ready };
310
+ }
311
+ export function hidePeekling(duration, document = globalThis.document, window = globalThis.window) {
312
+ if (!document || !window)
313
+ throw new Error(runtimeMessage("browser.context", "Browser document/window required"));
314
+ const controller = controllers.get(document);
315
+ if (controller)
316
+ controller.hide(duration);
317
+ else
318
+ writeHidden(window, duration);
319
+ }
320
+ export function showPeekling(document = globalThis.document, window = globalThis.window) {
321
+ if (!document || !window)
322
+ throw new Error(runtimeMessage("browser.context", "Browser document/window required"));
323
+ const controller = controllers.get(document);
324
+ if (controller)
325
+ controller.show();
326
+ else
327
+ clearHidden(window);
328
+ }
329
+ export function isPeeklingHidden(window = globalThis.window) {
330
+ if (!window)
331
+ throw new Error(runtimeMessage("browser.window", "Browser window required"));
332
+ return controllers.get(window.document)?.isHidden() ?? readHidden(window);
333
+ }
@@ -0,0 +1,31 @@
1
+ import { type PeeklingInstance, type PeeklingOptions } from "./runtime.js";
2
+ import type { Plan, DiagnosticRecord, EmitResult, HostBindings, HostContent, OverrideInput, OverrideHandle, SurfaceTheme } from "./types.js";
3
+ export declare const PEEKLING_ELEMENT_TAG = "peekling-character";
4
+ export declare function reportBrowserCollision(name: string): void;
5
+ export interface PeeklingElement extends HTMLElement {
6
+ options: PeeklingOptions;
7
+ plan?: Plan;
8
+ content?: HostContent;
9
+ bindings?: HostBindings;
10
+ theme?: SurfaceTheme;
11
+ styles?: PeeklingOptions["styles"];
12
+ accessibility?: PeeklingOptions["accessibility"];
13
+ diagnostics?: PeeklingOptions["diagnostics"];
14
+ logger?: (record: Readonly<DiagnosticRecord>) => void;
15
+ readonly instance?: PeeklingInstance;
16
+ readonly ready: Promise<PeeklingInstance>;
17
+ emit(name: string, payload?: unknown): EmitResult;
18
+ override(value: OverrideInput): OverrideHandle;
19
+ pause(): void;
20
+ resume(): void;
21
+ }
22
+ declare global {
23
+ interface HTMLElementTagNameMap {
24
+ "peekling-character": PeeklingElement;
25
+ }
26
+ interface WindowEventMap {
27
+ "peekling:collision": CustomEvent<"Peekling" | typeof PEEKLING_ELEMENT_TAG>;
28
+ }
29
+ }
30
+ export declare function definePeeklingElement(): void;
31
+ //# sourceMappingURL=web-component.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"web-component.d.ts","sourceRoot":"","sources":["../src/web-component.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,gBAAgB,EACrB,KAAK,eAAe,EACrB,MAAM,cAAc,CAAC;AACtB,OAAO,KAAK,EACV,IAAI,EACJ,gBAAgB,EAChB,UAAU,EACV,YAAY,EACZ,WAAW,EACX,aAAa,EACb,cAAc,EACd,YAAY,EACb,MAAM,YAAY,CAAC;AAIpB,eAAO,MAAM,oBAAoB,uBAAuB,CAAC;AAIzD,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAQzD;AAED,MAAM,WAAW,eAAgB,SAAQ,WAAW;IAClD,OAAO,EAAE,eAAe,CAAC;IACzB,IAAI,CAAC,EAAE,IAAI,CAAC;IACZ,OAAO,CAAC,EAAE,WAAW,CAAC;IACtB,QAAQ,CAAC,EAAE,YAAY,CAAC;IACxB,KAAK,CAAC,EAAE,YAAY,CAAC;IACrB,MAAM,CAAC,EAAE,eAAe,CAAC,QAAQ,CAAC,CAAC;IACnC,aAAa,CAAC,EAAE,eAAe,CAAC,eAAe,CAAC,CAAC;IACjD,WAAW,CAAC,EAAE,eAAe,CAAC,aAAa,CAAC,CAAC;IAC7C,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,QAAQ,CAAC,gBAAgB,CAAC,KAAK,IAAI,CAAC;IACtD,QAAQ,CAAC,QAAQ,CAAC,EAAE,gBAAgB,CAAC;IACrC,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC,gBAAgB,CAAC,CAAC;IAC1C,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,OAAO,GAAG,UAAU,CAAC;IAClD,QAAQ,CAAC,KAAK,EAAE,aAAa,GAAG,cAAc,CAAC;IAC/C,KAAK,IAAI,IAAI,CAAC;IACd,MAAM,IAAI,IAAI,CAAC;CAChB;AAED,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,qBAAqB;QAC7B,oBAAoB,EAAE,eAAe,CAAC;KACvC;IACD,UAAU,cAAc;QACtB,oBAAoB,EAAE,WAAW,CAAC,UAAU,GAAG,OAAO,oBAAoB,CAAC,CAAC;KAC7E;CACF;AAmBD,wBAAgB,qBAAqB,IAAI,IAAI,CA6O5C"}
@@ -0,0 +1,236 @@
1
+ import { PeeklingRuntime, } from "./runtime.js";
2
+ import { runtimeMessage } from "./runtime-diagnostics.js";
3
+ import { OwnDataError, snapshotConfiguration } from "./own-data.js";
4
+ export const PEEKLING_ELEMENT_TAG = "peekling-character";
5
+ let disconnectedRequestId = 0;
6
+ let supportedElement;
7
+ export function reportBrowserCollision(name) {
8
+ try {
9
+ globalThis.dispatchEvent(new CustomEvent("peekling:collision", {
10
+ detail: name,
11
+ }));
12
+ }
13
+ catch { }
14
+ }
15
+ function readyState() {
16
+ let resolve;
17
+ let reject;
18
+ const promise = new Promise((accept, decline) => {
19
+ resolve = accept;
20
+ reject = decline;
21
+ });
22
+ void promise.catch(() => { });
23
+ return [promise, resolve, reject];
24
+ }
25
+ export function definePeeklingElement() {
26
+ try {
27
+ const registry = globalThis.customElements;
28
+ const Base = globalThis.HTMLElement;
29
+ if (!registry || !Base)
30
+ return;
31
+ const existing = registry.get(PEEKLING_ELEMENT_TAG);
32
+ if (existing) {
33
+ if (existing !== supportedElement) {
34
+ reportBrowserCollision(PEEKLING_ELEMENT_TAG);
35
+ }
36
+ return;
37
+ }
38
+ class PeeklingElement extends Base {
39
+ static observedAttributes = [
40
+ "character",
41
+ "pack-url",
42
+ "name",
43
+ "styles-url",
44
+ "styles-integrity",
45
+ ];
46
+ static {
47
+ for (const key of [
48
+ "plan",
49
+ "content",
50
+ "bindings",
51
+ "theme",
52
+ "styles",
53
+ "accessibility",
54
+ "diagnostics",
55
+ "logger",
56
+ ]) {
57
+ Object.defineProperty(this.prototype, key, {
58
+ configurable: true,
59
+ get() {
60
+ return this.#overrides[key];
61
+ },
62
+ set(value) {
63
+ this.#set(key, value);
64
+ },
65
+ });
66
+ }
67
+ }
68
+ #instance;
69
+ #options = Object.create(null);
70
+ #optionsError;
71
+ #overrides = Object.create(null);
72
+ #scheduled = false;
73
+ #mountedOnce = false;
74
+ #generation = 0;
75
+ #ready = readyState();
76
+ #pageShow = (event) => {
77
+ if (event.persisted)
78
+ this.#remount();
79
+ };
80
+ /** Full JavaScript API configuration. Scalar attributes override matching fields. */
81
+ get options() {
82
+ return this.#options;
83
+ }
84
+ set options(value) {
85
+ try {
86
+ this.#options = snapshotConfiguration(value);
87
+ this.#optionsError = undefined;
88
+ }
89
+ catch (error) {
90
+ this.#options = Object.create(null);
91
+ this.#optionsError = error;
92
+ }
93
+ this.#remount();
94
+ }
95
+ get instance() {
96
+ return this.#instance;
97
+ }
98
+ /** Resolves when the current connected mount is fully initialized. */
99
+ get ready() {
100
+ return this.#ready[0];
101
+ }
102
+ emit(name, payload) {
103
+ return (this.#instance?.emit(name, payload) ?? {
104
+ accepted: false,
105
+ reason: "not-ready",
106
+ });
107
+ }
108
+ override(value) {
109
+ if (!this.#instance) {
110
+ const id = `override:disconnected:${++disconnectedRequestId}`;
111
+ return {
112
+ id,
113
+ status: "rejected",
114
+ finished: Promise.resolve({ id, reason: "rejected" }),
115
+ cancel: () => { },
116
+ };
117
+ }
118
+ return this.#instance.override(value);
119
+ }
120
+ pause() {
121
+ this.#instance?.pause();
122
+ }
123
+ resume() {
124
+ this.#instance?.resume();
125
+ }
126
+ connectedCallback() {
127
+ globalThis.addEventListener?.("pageshow", this.#pageShow);
128
+ this.#remount();
129
+ }
130
+ attributeChangedCallback(name) {
131
+ const attribute = this.getAttribute("name");
132
+ if (name === "name" &&
133
+ this.#instance &&
134
+ this.#instance.updateComponentName(attribute === null
135
+ ? this.#options.name
136
+ : attribute || true)) {
137
+ return;
138
+ }
139
+ this.#remount();
140
+ }
141
+ disconnectedCallback() {
142
+ globalThis.removeEventListener?.("pageshow", this.#pageShow);
143
+ this.#generation += 1;
144
+ this.#scheduled = false;
145
+ this.#ready[2](new DOMException(runtimeMessage("element.disconnected", "Element disconnected"), "AbortError"));
146
+ this.#instance?.destroy();
147
+ this.#instance = undefined;
148
+ }
149
+ #set(key, value) {
150
+ if (value === undefined)
151
+ delete this.#overrides[key];
152
+ else
153
+ this.#overrides[key] = value;
154
+ this.#remount();
155
+ }
156
+ #remount() {
157
+ if (!this.isConnected || this.#scheduled)
158
+ return;
159
+ this.#scheduled = true;
160
+ const generation = ++this.#generation;
161
+ if (this.#mountedOnce) {
162
+ this.#ready[2](new DOMException(runtimeMessage("element.remounted", "Element remounted"), "AbortError"));
163
+ this.#ready = readyState();
164
+ }
165
+ else
166
+ this.#mountedOnce = true;
167
+ queueMicrotask(() => {
168
+ if (generation !== this.#generation)
169
+ return;
170
+ this.#scheduled = false;
171
+ if (!this.isConnected)
172
+ return;
173
+ this.#instance?.destroy();
174
+ this.#instance = undefined;
175
+ void this.#mount(generation);
176
+ });
177
+ }
178
+ async #mount(generation) {
179
+ try {
180
+ if (this.#optionsError)
181
+ throw this.#optionsError;
182
+ if (!this.#options ||
183
+ typeof this.#options !== "object" ||
184
+ Array.isArray(this.#options)) {
185
+ throw new OwnDataError("$", runtimeMessage("data.plain", "must use this realm's Object prototype or a null prototype"));
186
+ }
187
+ const configured = Object.create(null);
188
+ for (const source of [
189
+ this.#options,
190
+ this.#overrides,
191
+ ]) {
192
+ for (const key in source) {
193
+ configured[key] = source[key];
194
+ }
195
+ }
196
+ const character = this.getAttribute("character");
197
+ if (character !== null)
198
+ configured.character = character;
199
+ const packUrl = this.getAttribute("pack-url");
200
+ if (packUrl !== null)
201
+ configured.packUrl = packUrl;
202
+ const name = this.getAttribute("name");
203
+ if (name !== null)
204
+ configured.name = name || true;
205
+ const stylesUrl = this.getAttribute("styles-url");
206
+ if (stylesUrl !== null) {
207
+ const stylesIntegrity = this.getAttribute("styles-integrity");
208
+ configured.styles = stylesIntegrity
209
+ ? { url: stylesUrl, integrity: stylesIntegrity }
210
+ : { url: stylesUrl };
211
+ }
212
+ const instance = PeeklingRuntime.hatch(configured);
213
+ this.#instance = instance;
214
+ await instance.ready;
215
+ if (generation !== this.#generation || !this.isConnected) {
216
+ instance.destroy();
217
+ return;
218
+ }
219
+ this.#ready[1](instance);
220
+ }
221
+ catch (error) {
222
+ if (generation !== this.#generation)
223
+ return;
224
+ this.#instance?.destroy();
225
+ this.#instance = undefined;
226
+ this.#ready[2](error);
227
+ }
228
+ }
229
+ }
230
+ registry.define(PEEKLING_ELEMENT_TAG, PeeklingElement);
231
+ supportedElement = PeeklingElement;
232
+ }
233
+ catch {
234
+ reportBrowserCollision(PEEKLING_ELEMENT_TAG);
235
+ }
236
+ }
package/package.json ADDED
@@ -0,0 +1,74 @@
1
+ {
2
+ "name": "@peekling/runtime",
3
+ "version": "0.1.0",
4
+ "description": "Dependency-free browser runtime for interactive Peekling characters",
5
+ "license": "Apache-2.0",
6
+ "author": {
7
+ "name": "Prajwal S. Venkateshmurthy",
8
+ "url": "https://prajwal.me"
9
+ },
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "git+https://github.com/peekling/peekling-engine.git"
13
+ },
14
+ "homepage": "https://github.com/peekling/peekling-engine#readme",
15
+ "bugs": {
16
+ "url": "https://github.com/peekling/peekling-engine/issues"
17
+ },
18
+ "type": "module",
19
+ "engines": {
20
+ "node": ">=22.14.0"
21
+ },
22
+ "publishConfig": {
23
+ "access": "public",
24
+ "provenance": true
25
+ },
26
+ "sideEffects": [
27
+ "./dist/peekling.min.js"
28
+ ],
29
+ "files": [
30
+ "dist/**/*.js",
31
+ "dist/*.css",
32
+ "dist/*.sri",
33
+ "dist/**/*.d.ts",
34
+ "dist/**/*.d.ts.map",
35
+ "!dist/arbiter.*",
36
+ "!dist/browser.*",
37
+ "!dist/requests.*",
38
+ "README.md",
39
+ "LICENSE",
40
+ "NOTICE",
41
+ "AUTHORS",
42
+ "LICENSING.md"
43
+ ],
44
+ "main": "./dist/index.js",
45
+ "types": "./dist/index.d.ts",
46
+ "jsdelivr": "./dist/peekling.min.js",
47
+ "unpkg": "./dist/peekling.min.js",
48
+ "exports": {
49
+ ".": {
50
+ "types": "./dist/index.d.ts",
51
+ "import": "./dist/index.js"
52
+ },
53
+ "./pack": {
54
+ "types": "./dist/pack-api.d.ts",
55
+ "import": "./dist/pack-api.js"
56
+ },
57
+ "./preflight": {
58
+ "types": "./dist/preflight-api.d.ts",
59
+ "import": "./dist/preflight-api.js"
60
+ },
61
+ "./browser": "./dist/peekling.min.js",
62
+ "./peekling.css": "./dist/peekling.css"
63
+ },
64
+ "dependencies": {},
65
+ "scripts": {
66
+ "build": "tsc -b",
67
+ "prepack": "npm --prefix ../.. run build",
68
+ "test": "npm run build && node --test --experimental-strip-types test/*.test.ts",
69
+ "typecheck": "tsc -b --pretty false"
70
+ },
71
+ "devDependencies": {
72
+ "typescript": "^7.0.2"
73
+ }
74
+ }