@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.
- package/AUTHORS +8 -0
- package/LICENSE +202 -0
- package/LICENSING.md +20 -0
- package/NOTICE +6 -0
- package/README.md +294 -0
- package/dist/configuration-validation.d.ts +19 -0
- package/dist/configuration-validation.d.ts.map +1 -0
- package/dist/configuration-validation.js +298 -0
- package/dist/content.d.ts +44 -0
- package/dist/content.d.ts.map +1 -0
- package/dist/content.js +738 -0
- package/dist/contracts.d.ts +10 -0
- package/dist/contracts.d.ts.map +1 -0
- package/dist/contracts.js +106 -0
- package/dist/defaults.d.ts +7 -0
- package/dist/defaults.d.ts.map +1 -0
- package/dist/defaults.js +6 -0
- package/dist/diagnostics.d.ts +25 -0
- package/dist/diagnostics.d.ts.map +1 -0
- package/dist/diagnostics.js +122 -0
- package/dist/direction.d.ts +3 -0
- package/dist/direction.d.ts.map +1 -0
- package/dist/direction.js +6 -0
- package/dist/errors.d.ts +15 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +53 -0
- package/dist/events.d.ts +5 -0
- package/dist/events.d.ts.map +1 -0
- package/dist/events.js +60 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/input.d.ts +31 -0
- package/dist/input.d.ts.map +1 -0
- package/dist/input.js +215 -0
- package/dist/json.d.ts +3 -0
- package/dist/json.d.ts.map +1 -0
- package/dist/json.js +109 -0
- package/dist/loader.d.ts +33 -0
- package/dist/loader.d.ts.map +1 -0
- package/dist/loader.js +477 -0
- package/dist/locomotion.d.ts +8 -0
- package/dist/locomotion.d.ts.map +1 -0
- package/dist/locomotion.js +29 -0
- package/dist/normalized.d.ts +5 -0
- package/dist/normalized.d.ts.map +1 -0
- package/dist/normalized.js +266 -0
- package/dist/overrides.d.ts +29 -0
- package/dist/overrides.d.ts.map +1 -0
- package/dist/overrides.js +296 -0
- package/dist/own-data.d.ts +18 -0
- package/dist/own-data.d.ts.map +1 -0
- package/dist/own-data.js +205 -0
- package/dist/pack-api.d.ts +9 -0
- package/dist/pack-api.d.ts.map +1 -0
- package/dist/pack-api.js +6 -0
- package/dist/pack-shared.d.ts +28 -0
- package/dist/pack-shared.d.ts.map +1 -0
- package/dist/pack-shared.js +408 -0
- package/dist/pack.d.ts +6 -0
- package/dist/pack.d.ts.map +1 -0
- package/dist/pack.js +316 -0
- package/dist/peekling.css +1 -0
- package/dist/peekling.css.sri +1 -0
- package/dist/peekling.js +7469 -0
- package/dist/peekling.js.sri +1 -0
- package/dist/peekling.min.js +2 -0
- package/dist/peekling.min.js.sri +1 -0
- package/dist/plan-compiler.d.ts +82 -0
- package/dist/plan-compiler.d.ts.map +1 -0
- package/dist/plan-compiler.js +724 -0
- package/dist/plan.d.ts +21 -0
- package/dist/plan.d.ts.map +1 -0
- package/dist/plan.js +287 -0
- package/dist/preflight-api.d.ts +3 -0
- package/dist/preflight-api.d.ts.map +1 -0
- package/dist/preflight-api.js +1 -0
- package/dist/preflight.d.ts +22 -0
- package/dist/preflight.d.ts.map +1 -0
- package/dist/preflight.js +158 -0
- package/dist/registry.d.ts +9 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +24 -0
- package/dist/renderer.d.ts +16 -0
- package/dist/renderer.d.ts.map +1 -0
- package/dist/renderer.js +176 -0
- package/dist/resolver.d.ts +3 -0
- package/dist/resolver.d.ts.map +1 -0
- package/dist/resolver.js +30 -0
- package/dist/runtime-diagnostics.d.ts +3 -0
- package/dist/runtime-diagnostics.d.ts.map +1 -0
- package/dist/runtime-diagnostics.js +5 -0
- package/dist/runtime-validation.d.ts +6 -0
- package/dist/runtime-validation.d.ts.map +1 -0
- package/dist/runtime-validation.js +104 -0
- package/dist/runtime.d.ts +87 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +1015 -0
- package/dist/sections.d.ts +15 -0
- package/dist/sections.d.ts.map +1 -0
- package/dist/sections.js +221 -0
- package/dist/styles.d.ts +18 -0
- package/dist/styles.d.ts.map +1 -0
- package/dist/styles.js +132 -0
- package/dist/tooling-preflight.d.ts +23 -0
- package/dist/tooling-preflight.d.ts.map +1 -0
- package/dist/tooling-preflight.js +207 -0
- package/dist/tooling-validation.d.ts +8 -0
- package/dist/tooling-validation.d.ts.map +1 -0
- package/dist/tooling-validation.js +94 -0
- package/dist/types.d.ts +403 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +29 -0
- package/dist/visibility.d.ts +12 -0
- package/dist/visibility.d.ts.map +1 -0
- package/dist/visibility.js +333 -0
- package/dist/web-component.d.ts +31 -0
- package/dist/web-component.d.ts.map +1 -0
- package/dist/web-component.js +236 -0
- 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
|
+
}
|