@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,738 @@
1
+ import { CONTENT_REGIONS, } from "./types.js";
2
+ import { isHttpPath, NAME_PATTERN } from "./contracts.js";
3
+ import { validateEventPayload } from "./events.js";
4
+ import { runtimeMessage } from "./runtime-diagnostics.js";
5
+ import { connectRuntimeHost, resolveStyleAsset, ShadowStyleSheet, } from "./styles.js";
6
+ export function validateHostContent(input, baseUrl) {
7
+ if (input === undefined)
8
+ return {};
9
+ if (!input || typeof input !== "object" || Array.isArray(input)) {
10
+ throw new TypeError(runtimeMessage("content.object", () => "content must be an object"));
11
+ }
12
+ const entries = Object.entries(input);
13
+ if (entries.length > 32)
14
+ throw new TypeError(runtimeMessage("content.limit", () => "content exceeds 32 items"));
15
+ const result = {};
16
+ for (const [id, item] of entries) {
17
+ if (!NAME_PATTERN.test(id))
18
+ throw new TypeError(runtimeMessage("content.id", () => `Invalid content id: ${id}`));
19
+ if (!item || typeof item !== "object" || Array.isArray(item)) {
20
+ throw new TypeError(runtimeMessage("content.item", () => `Invalid content item: ${id}`));
21
+ }
22
+ closed(item, CONTENT_REGIONS, id);
23
+ const normalized = {};
24
+ for (const region of CONTENT_REGIONS) {
25
+ const surface = item[region];
26
+ if (surface !== undefined) {
27
+ normalized[region] = validateSurface(surface, baseUrl, `${id}.${region}`);
28
+ }
29
+ }
30
+ if (!Object.keys(normalized).length) {
31
+ throw new TypeError(runtimeMessage("content.empty", () => `Content item is empty: ${id}`));
32
+ }
33
+ result[id] = normalized;
34
+ }
35
+ return result;
36
+ }
37
+ function validateSurface(value, baseUrl, path) {
38
+ if (typeof value === "string")
39
+ return boundedText(value, 500, path);
40
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
41
+ throw new TypeError(runtimeMessage("content.surface", () => `Invalid content surface: ${path}`));
42
+ }
43
+ closed(value, ["title", "text", "link", "mountId", "announce"], path);
44
+ const title = boundedText(value.title, 120, `${path}.title`);
45
+ const contentText = boundedText(value.text, 500, `${path}.text`);
46
+ if (value.announce !== undefined && typeof value.announce !== "boolean") {
47
+ throw new TypeError(runtimeMessage("content.announce", () => `Invalid announce flag: ${path}.announce`));
48
+ }
49
+ if (value.mountId !== undefined &&
50
+ (typeof value.mountId !== "string" || !NAME_PATTERN.test(value.mountId))) {
51
+ throw new TypeError(runtimeMessage("content.binding", () => `Invalid binding: ${path}.mountId`));
52
+ }
53
+ let link;
54
+ if (value.link !== undefined) {
55
+ if (value.link && typeof value.link === "object") {
56
+ closed(value.link, ["label", "href"], `${path}.link`);
57
+ }
58
+ if (!value.link ||
59
+ typeof value.link.label !== "string" ||
60
+ typeof value.link.href !== "string" ||
61
+ !isHttpPath(value.link.href, baseUrl)) {
62
+ throw new TypeError(runtimeMessage("content.link", () => `Invalid content link protocol or credentials: ${path}.link. Use a relative path or absolute HTTPS URL`));
63
+ }
64
+ const href = new URL(value.link.href, baseUrl);
65
+ link = {
66
+ label: boundedText(value.link.label, 120, `${path}.link.label`),
67
+ href: href.href,
68
+ };
69
+ }
70
+ if (!title && !contentText && !link && !value.mountId) {
71
+ throw new TypeError(runtimeMessage("content.empty", () => `Content is empty: ${path}`));
72
+ }
73
+ return {
74
+ ...(title ? { title } : {}),
75
+ ...(contentText ? { text: contentText } : {}),
76
+ ...(link ? { link } : {}),
77
+ ...(value.mountId ? { mountId: value.mountId } : {}),
78
+ ...(value.announce === true ? { announce: true } : {}),
79
+ };
80
+ }
81
+ function closed(value, allowed, path) {
82
+ for (const key of Object.keys(value)) {
83
+ if (!allowed.includes(key))
84
+ throw new TypeError(runtimeMessage("content.field", () => `Unknown field: ${path}.${key}`));
85
+ }
86
+ }
87
+ function boundedText(value, maximum, path) {
88
+ if (value === undefined)
89
+ return undefined;
90
+ if (typeof value !== "string" ||
91
+ value.length < 1 ||
92
+ value.length > maximum ||
93
+ /[\u0000-\u001f\u007f]/.test(value)) {
94
+ throw new TypeError(runtimeMessage("content.text", () => `Invalid content text: ${path}`));
95
+ }
96
+ return value;
97
+ }
98
+ export function validateContentReferences(ids, content) {
99
+ for (const id of ids) {
100
+ if (id && !Object.hasOwn(content, id))
101
+ throw new TypeError(runtimeMessage("content.reference", () => `Unknown contentId: ${id}`));
102
+ }
103
+ }
104
+ export function placeContentRegion(region, position, viewport, character, surface) {
105
+ const margin = 8;
106
+ const gap = 12;
107
+ const above = position.y - character.height / 2 - gap - surface.height;
108
+ const below = position.y + character.height / 2 + gap;
109
+ const left = position.x - character.width / 2 - gap - surface.width;
110
+ const center = position.x - surface.width / 2;
111
+ const right = position.x + character.width / 2 + gap;
112
+ const topX = region === "top-left" ? left : region === "top-right" ? right : center;
113
+ const wantsBottom = region === "bottom";
114
+ const preferredY = wantsBottom ? below : above;
115
+ const alternateY = wantsBottom ? above : below;
116
+ return {
117
+ x: clamp(topX, margin, viewport.width - surface.width - margin),
118
+ y: clamp(preferredY >= margin &&
119
+ preferredY + surface.height <= viewport.height - margin
120
+ ? preferredY
121
+ : alternateY, margin, viewport.height - surface.height - margin),
122
+ };
123
+ }
124
+ function clamp(value, minimum, maximum) {
125
+ return Math.max(minimum, Math.min(value, Math.max(minimum, maximum)));
126
+ }
127
+ export class ContentRenderer {
128
+ ready;
129
+ #document;
130
+ #host;
131
+ #root;
132
+ #surfaces = new Map();
133
+ #origin;
134
+ #diagnostic;
135
+ #bindings;
136
+ #announce;
137
+ #emit;
138
+ #wake;
139
+ #styles;
140
+ #label;
141
+ #positionId = 0;
142
+ #selectionGeneration = 0;
143
+ #placementDirty = true;
144
+ #positionX = Number.NaN;
145
+ #positionY = Number.NaN;
146
+ #viewportWidth = Number.NaN;
147
+ #viewportHeight = Number.NaN;
148
+ #characterWidth = Number.NaN;
149
+ #characterHeight = Number.NaN;
150
+ #name;
151
+ #nameSelection;
152
+ #nameItem;
153
+ #destroyed = false;
154
+ constructor(document, options = {}) {
155
+ this.#document = document;
156
+ this.#origin = new URL(document.baseURI ??
157
+ globalThis.location?.href ??
158
+ "https://peekling.invalid/").origin;
159
+ this.#diagnostic = options.diagnostic ?? (() => { });
160
+ this.#bindings = options.bindings ?? {};
161
+ this.#announce = options.announce === true;
162
+ this.#emit =
163
+ options.emit ??
164
+ (() => ({ accepted: false, reason: "not-ready" }));
165
+ this.#wake = options.wake ?? (() => { });
166
+ this.#host = document.createElement("aside");
167
+ this.#host.hidden = true;
168
+ this.#host.dataset.peeklingContent = "";
169
+ this.#label = options.label ?? "Peekling content";
170
+ this.#host.setAttribute("aria-label", this.#label);
171
+ this.#root = this.#host.attachShadow({ mode: "closed" });
172
+ this.#styles = new ShadowStyleSheet(document, this.#root, options.styles ??
173
+ resolveStyleAsset(undefined, document.baseURI ?? "https://peekling.invalid/"));
174
+ this.ready = this.#styles.ready.then(() => {
175
+ if (this.#destroyed)
176
+ return;
177
+ applyTheme(this.#styles.addRule(":host"), options.theme);
178
+ });
179
+ void this.ready.catch(() => { });
180
+ this.#repair();
181
+ }
182
+ renderSurfaces(selections, content, position, viewport, character, name) {
183
+ if (this.#destroyed)
184
+ return;
185
+ this.#repair();
186
+ if (this.#destroyed)
187
+ return;
188
+ this.#selectionGeneration += 1;
189
+ if (this.#selectionGeneration >= Number.MAX_SAFE_INTEGER) {
190
+ this.#selectionGeneration = 1;
191
+ for (const surface of this.#surfaces.values())
192
+ surface.seen = 0;
193
+ }
194
+ const generation = this.#selectionGeneration;
195
+ for (const selection of selections) {
196
+ if (!Object.hasOwn(content, selection.contentId))
197
+ continue;
198
+ this.#select(selection, content[selection.contentId], generation);
199
+ if (this.#destroyed)
200
+ return;
201
+ }
202
+ if (name) {
203
+ if (name !== this.#name) {
204
+ this.#name = name;
205
+ this.#nameSelection = {
206
+ id: "peekling:name",
207
+ contentId: "peekling:name",
208
+ key: `peekling:name:${name}`,
209
+ };
210
+ this.#nameItem = { bottom: name };
211
+ }
212
+ this.#select(this.#nameSelection, this.#nameItem, generation);
213
+ if (this.#destroyed)
214
+ return;
215
+ }
216
+ else {
217
+ this.#name = undefined;
218
+ this.#nameSelection = undefined;
219
+ this.#nameItem = undefined;
220
+ }
221
+ for (const surface of this.#surfaces.values()) {
222
+ if (surface.seen !== generation)
223
+ this.#removeSurface(surface);
224
+ if (this.#destroyed)
225
+ return;
226
+ }
227
+ const viewportChanged = viewport.width !== this.#viewportWidth ||
228
+ viewport.height !== this.#viewportHeight;
229
+ const placementChanged = viewportChanged ||
230
+ position.x !== this.#positionX ||
231
+ position.y !== this.#positionY ||
232
+ character.width !== this.#characterWidth ||
233
+ character.height !== this.#characterHeight;
234
+ this.#positionX = position.x;
235
+ this.#positionY = position.y;
236
+ this.#viewportWidth = viewport.width;
237
+ this.#viewportHeight = viewport.height;
238
+ this.#characterWidth = character.width;
239
+ this.#characterHeight = character.height;
240
+ if (viewportChanged) {
241
+ for (const surface of this.#surfaces.values()) {
242
+ if (!surface.host.hidden)
243
+ surface.measureDirty = true;
244
+ }
245
+ }
246
+ if (placementChanged)
247
+ this.#placementDirty = true;
248
+ for (const surface of this.#surfaces.values()) {
249
+ if (surface.host.hidden || !surface.measureDirty)
250
+ continue;
251
+ const rect = surface.host.getBoundingClientRect();
252
+ if (this.#destroyed)
253
+ return;
254
+ surface.measuredWidth = rect.width;
255
+ surface.measuredHeight = rect.height;
256
+ surface.measureDirty = false;
257
+ this.#placementDirty = true;
258
+ }
259
+ if (this.#placementDirty) {
260
+ let topLeftOffset = 0;
261
+ let topCenterOffset = 0;
262
+ let topRightOffset = 0;
263
+ let bottomOffset = 0;
264
+ const surfaces = [...this.#surfaces.values()].sort((left, right) => left.key < right.key ? -1 : left.key > right.key ? 1 : 0);
265
+ for (const surface of surfaces) {
266
+ if (surface.host.hidden)
267
+ continue;
268
+ let offset;
269
+ switch (surface.region) {
270
+ case "top-left":
271
+ offset = topLeftOffset;
272
+ topLeftOffset += surface.measuredHeight + 8;
273
+ break;
274
+ case "top-center":
275
+ offset = topCenterOffset;
276
+ topCenterOffset += surface.measuredHeight + 8;
277
+ break;
278
+ case "top-right":
279
+ offset = topRightOffset;
280
+ topRightOffset += surface.measuredHeight + 8;
281
+ break;
282
+ case "bottom":
283
+ offset = bottomOffset;
284
+ bottomOffset += surface.measuredHeight + 8;
285
+ break;
286
+ }
287
+ const point = placeContentRegion(surface.region, position, viewport, character, {
288
+ width: surface.measuredWidth,
289
+ height: surface.measuredHeight,
290
+ });
291
+ const left = `${point.x}px`;
292
+ const top = `${clamp(point.y + offset, 8, viewport.height - surface.measuredHeight - 8)}px`;
293
+ if (surface.left !== left) {
294
+ surface.positionStyle.left = left;
295
+ if (this.#destroyed)
296
+ return;
297
+ surface.left = left;
298
+ }
299
+ if (surface.top !== top) {
300
+ surface.positionStyle.top = top;
301
+ if (this.#destroyed)
302
+ return;
303
+ surface.top = top;
304
+ }
305
+ }
306
+ this.#placementDirty = false;
307
+ }
308
+ let visible = false;
309
+ for (const surface of this.#surfaces.values()) {
310
+ if (!surface.host.hidden) {
311
+ visible = true;
312
+ break;
313
+ }
314
+ }
315
+ if (this.#host.hidden === visible)
316
+ this.#host.hidden = !visible;
317
+ }
318
+ #select(selection, item, generation) {
319
+ for (const region of CONTENT_REGIONS) {
320
+ const primitive = item[region];
321
+ if (primitive === undefined)
322
+ continue;
323
+ const key = `${selection.id}\0${region}`;
324
+ const surface = this.#surfaces.get(key) ??
325
+ this.#createSurface(key, selection.id, selection.contentId, region);
326
+ surface.seen = generation;
327
+ if (surface.contentId !== selection.contentId) {
328
+ surface.contentId = selection.contentId;
329
+ this.#invalidateSurface(surface);
330
+ }
331
+ this.#apply(surface, primitive, selection);
332
+ if (this.#destroyed)
333
+ return;
334
+ }
335
+ }
336
+ #invalidateSurface(surface, wake = false) {
337
+ surface.measureDirty = true;
338
+ this.#placementDirty = true;
339
+ if (wake && !this.#destroyed)
340
+ this.#wake();
341
+ }
342
+ #setHidden(surface, hidden) {
343
+ if (surface.host.hidden === hidden)
344
+ return;
345
+ surface.host.hidden = hidden;
346
+ this.#placementDirty = true;
347
+ if (!hidden)
348
+ surface.measureDirty = true;
349
+ }
350
+ #createSurface(key, surfaceId, contentId, region) {
351
+ const host = this.#document.createElement("section");
352
+ const positionClass = `peekling-position-${++this.#positionId}`;
353
+ host.className = `surface ${region} ${positionClass}`;
354
+ host.hidden = true;
355
+ host.setAttribute("part", `surface ${region}`);
356
+ host.dataset.peeklingSurface = surfaceId;
357
+ const title = this.#document.createElement("strong");
358
+ title.className = "title";
359
+ const text = this.#document.createElement("p");
360
+ const link = this.#document.createElement("a");
361
+ const custom = this.#document.createElement("div");
362
+ custom.dataset.peeklingMount = "";
363
+ host.append(title, text, link, custom);
364
+ this.#root.append(host);
365
+ const surface = {
366
+ key,
367
+ surfaceId,
368
+ contentId,
369
+ region,
370
+ host,
371
+ title,
372
+ text,
373
+ link,
374
+ custom,
375
+ mounted: undefined,
376
+ renderKey: "",
377
+ failedKey: "",
378
+ seen: 0,
379
+ measureDirty: true,
380
+ measuredWidth: 0,
381
+ measuredHeight: 0,
382
+ left: "",
383
+ top: "",
384
+ positionStyle: this.#styles.addRule(`.${positionClass}`),
385
+ };
386
+ this.#surfaces.set(key, surface);
387
+ this.#placementDirty = true;
388
+ return surface;
389
+ }
390
+ #apply(elements, primitive, selection) {
391
+ if (this.#destroyed)
392
+ return;
393
+ if (elements.primitive !== primitive) {
394
+ elements.primitive = primitive;
395
+ elements.item =
396
+ typeof primitive === "string" ? { text: primitive } : primitive;
397
+ const item = elements.item;
398
+ this.#invalidateSurface(elements);
399
+ elements.title.textContent = item.title ?? "";
400
+ elements.title.hidden = !!item.mountId || !item.title;
401
+ elements.text.textContent = item.text ?? "";
402
+ elements.text.hidden = !!item.mountId || !item.text;
403
+ elements.link.hidden = !!item.mountId || !item.link;
404
+ if (!item.mountId && item.link) {
405
+ elements.link.textContent = item.link.label;
406
+ elements.link.href = item.link.href;
407
+ if (new URL(item.link.href).origin !== this.#origin) {
408
+ elements.link.target = "_blank";
409
+ elements.link.rel = "noopener noreferrer";
410
+ }
411
+ else {
412
+ elements.link.removeAttribute("target");
413
+ elements.link.removeAttribute("rel");
414
+ }
415
+ }
416
+ else {
417
+ elements.link.textContent = "";
418
+ elements.link.removeAttribute("href");
419
+ elements.link.removeAttribute("target");
420
+ elements.link.removeAttribute("rel");
421
+ }
422
+ if (item.announce || this.#announce) {
423
+ elements.host.setAttribute("aria-live", "polite");
424
+ elements.host.setAttribute("aria-atomic", "true");
425
+ }
426
+ else {
427
+ elements.host.removeAttribute("aria-live");
428
+ elements.host.removeAttribute("aria-atomic");
429
+ }
430
+ }
431
+ const item = elements.item;
432
+ if (item.mountId) {
433
+ const desiredKey = `${item.mountId}\0${selection.key}`;
434
+ if (elements.failedKey === desiredKey) {
435
+ this.#setHidden(elements, true);
436
+ return;
437
+ }
438
+ if (elements.failedKey)
439
+ elements.failedKey = "";
440
+ this.#mount(elements, item.mountId, selection);
441
+ }
442
+ else {
443
+ elements.failedKey = "";
444
+ this.#releaseMount(elements);
445
+ }
446
+ if (this.#destroyed)
447
+ return;
448
+ this.#setHidden(elements, false);
449
+ }
450
+ #mount(surface, mountId, selection) {
451
+ if (this.#destroyed)
452
+ return;
453
+ const renderKey = `${mountId}\0${selection.key}`;
454
+ if (surface.mounted?.mountId === mountId &&
455
+ surface.renderKey === renderKey) {
456
+ return;
457
+ }
458
+ let data;
459
+ try {
460
+ data = validateEventPayload(selection.data);
461
+ }
462
+ catch {
463
+ this.#failSurface(surface, "data-failed", renderKey);
464
+ return;
465
+ }
466
+ if (surface.mounted?.mountId === mountId) {
467
+ surface.renderKey = renderKey;
468
+ surface.mounted.data = data;
469
+ if (surface.mounted.result?.update) {
470
+ this.#updateMount(surface, surface.mounted, data);
471
+ return;
472
+ }
473
+ if (!surface.mounted.result) {
474
+ surface.mounted.pendingUpdate = true;
475
+ surface.mounted.key = selection.key;
476
+ return;
477
+ }
478
+ this.#releaseMount(surface);
479
+ }
480
+ else {
481
+ this.#releaseMount(surface);
482
+ }
483
+ if (this.#destroyed)
484
+ return;
485
+ const mounts = this.#bindings.mounts;
486
+ const mount = mounts && Object.hasOwn(mounts, mountId) ? mounts[mountId] : undefined;
487
+ if (!mount) {
488
+ this.#failSurface(surface, "mount-binding-is-missing", renderKey);
489
+ return;
490
+ }
491
+ const abort = new AbortController();
492
+ const root = this.#document.createElement("div");
493
+ const mounted = {
494
+ mountId,
495
+ abort,
496
+ root,
497
+ result: undefined,
498
+ data,
499
+ pendingUpdate: false,
500
+ updating: false,
501
+ key: selection.key,
502
+ };
503
+ surface.mounted = mounted;
504
+ surface.renderKey = renderKey;
505
+ surface.custom.replaceChildren(root);
506
+ this.#invalidateSurface(surface);
507
+ let result;
508
+ try {
509
+ result = mount(Object.freeze({
510
+ surfaceId: surface.surfaceId,
511
+ contentId: surface.contentId,
512
+ region: surface.region,
513
+ root,
514
+ ...(data === undefined ? {} : { data }),
515
+ signal: abort.signal,
516
+ emit: this.#emit,
517
+ }));
518
+ }
519
+ catch {
520
+ this.#failSurface(surface, "mount-failed", renderKey);
521
+ return;
522
+ }
523
+ void Promise.resolve(result).then((controller) => {
524
+ const safeController = snapshotMountResult(controller);
525
+ if (this.#destroyed ||
526
+ surface.mounted !== mounted ||
527
+ abort.signal.aborted) {
528
+ if (safeController)
529
+ this.#cleanupResult(safeController, surface);
530
+ return;
531
+ }
532
+ if (!safeController) {
533
+ this.#failSurface(surface, "mount-failed", renderKey);
534
+ return;
535
+ }
536
+ mounted.result = safeController;
537
+ this.#invalidateSurface(surface, true);
538
+ if (mounted.pendingUpdate) {
539
+ mounted.pendingUpdate = false;
540
+ if (safeController.update) {
541
+ this.#updateMount(surface, mounted, mounted.data);
542
+ }
543
+ else {
544
+ const latest = {
545
+ id: surface.surfaceId,
546
+ contentId: surface.contentId,
547
+ ...(mounted.data === undefined ? {} : { data: mounted.data }),
548
+ key: mounted.key,
549
+ };
550
+ this.#releaseMount(surface);
551
+ this.#mount(surface, mountId, latest);
552
+ }
553
+ }
554
+ }, () => {
555
+ if (!this.#destroyed &&
556
+ surface.mounted === mounted &&
557
+ !abort.signal.aborted) {
558
+ this.#failSurface(surface, "mount-failed", renderKey);
559
+ }
560
+ });
561
+ }
562
+ #updateMount(surface, mounted, data) {
563
+ if (this.#destroyed)
564
+ return;
565
+ const update = mounted.result?.update;
566
+ if (!update)
567
+ return;
568
+ if (mounted.updating) {
569
+ mounted.pendingUpdate = true;
570
+ return;
571
+ }
572
+ mounted.updating = true;
573
+ this.#invalidateSurface(surface);
574
+ try {
575
+ const pending = update(data);
576
+ void Promise.resolve(pending).then(() => {
577
+ if (this.#destroyed || surface.mounted !== mounted)
578
+ return;
579
+ mounted.updating = false;
580
+ this.#invalidateSurface(surface, true);
581
+ if (mounted.pendingUpdate) {
582
+ mounted.pendingUpdate = false;
583
+ this.#updateMount(surface, mounted, mounted.data);
584
+ }
585
+ }, () => {
586
+ if (!this.#destroyed && surface.mounted === mounted) {
587
+ this.#failSurface(surface, "update-failed", surface.renderKey);
588
+ }
589
+ });
590
+ }
591
+ catch {
592
+ if (!this.#destroyed && surface.mounted === mounted) {
593
+ this.#failSurface(surface, "update-failed", surface.renderKey);
594
+ }
595
+ }
596
+ }
597
+ #failSurface(surface, reason, failedKey = surface.renderKey) {
598
+ if (this.#destroyed)
599
+ return;
600
+ this.#diagnostic(runtimeMessage(`content.${reason}`, () => `Content ${reason.replaceAll("-", " ")} at content.${surface.contentId}.${surface.region}`));
601
+ if (this.#destroyed)
602
+ return;
603
+ this.#releaseMount(surface);
604
+ if (this.#destroyed)
605
+ return;
606
+ surface.failedKey = failedKey;
607
+ this.#setHidden(surface, true);
608
+ }
609
+ #releaseMount(surface) {
610
+ const mounted = surface.mounted;
611
+ surface.mounted = undefined;
612
+ surface.renderKey = "";
613
+ if (!mounted)
614
+ return;
615
+ mounted.abort.abort();
616
+ if (mounted.result)
617
+ this.#cleanupResult(mounted.result, surface);
618
+ mounted.root.remove();
619
+ surface.custom.replaceChildren();
620
+ this.#invalidateSurface(surface);
621
+ }
622
+ #cleanupResult(result, surface) {
623
+ try {
624
+ const pending = result.cleanup();
625
+ void Promise.resolve(pending).catch(() => this.#diagnostic(runtimeMessage("content.cleanup-failed", () => `Content cleanup failed at content.${surface.contentId}.${surface.region}`)));
626
+ }
627
+ catch {
628
+ this.#diagnostic(runtimeMessage("content.cleanup-failed", () => `Content cleanup failed at content.${surface.contentId}.${surface.region}`));
629
+ }
630
+ }
631
+ #removeSurface(surface) {
632
+ this.#releaseMount(surface);
633
+ if (this.#destroyed)
634
+ return;
635
+ this.#destroySurface(surface);
636
+ }
637
+ #destroySurface(surface) {
638
+ if (surface.mounted)
639
+ this.#releaseMount(surface);
640
+ this.#styles.removeRule(surface.positionStyle);
641
+ surface.host.remove();
642
+ this.#surfaces.delete(surface.key);
643
+ this.#placementDirty = true;
644
+ }
645
+ destroy() {
646
+ if (this.#destroyed)
647
+ return;
648
+ this.#destroyed = true;
649
+ for (const surface of [...this.#surfaces.values()]) {
650
+ this.#destroySurface(surface);
651
+ }
652
+ this.#styles.destroy();
653
+ this.#host.remove();
654
+ }
655
+ #repair() {
656
+ if (this.#destroyed)
657
+ return;
658
+ const hostRepaired = this.#host.parentNode !== this.#document.documentElement ||
659
+ !this.#host.hasAttribute("data-peekling-content");
660
+ connectRuntimeHost(this.#host, this.#document, "data-peekling-content");
661
+ if (this.#destroyed)
662
+ return;
663
+ if (this.#root.host !== this.#host)
664
+ throw new Error(runtimeMessage("content.root", () => "Peekling content root structure was changed"));
665
+ if (this.#host.getAttribute("aria-label") !== this.#label)
666
+ this.#host.setAttribute("aria-label", this.#label);
667
+ for (const surface of this.#surfaces.values()) {
668
+ if (hostRepaired)
669
+ this.#invalidateSurface(surface);
670
+ if (surface.host.ownerDocument !== this.#document)
671
+ throw new Error(runtimeMessage("content.document", () => "Peekling surface was adopted into another document"));
672
+ if (surface.host.parentNode !== this.#root) {
673
+ this.#root.append(surface.host);
674
+ this.#invalidateSurface(surface);
675
+ }
676
+ if (surface.title.parentNode !== surface.host ||
677
+ surface.text.parentNode !== surface.host ||
678
+ surface.link.parentNode !== surface.host ||
679
+ surface.custom.parentNode !== surface.host) {
680
+ surface.host.replaceChildren(surface.title, surface.text, surface.link, surface.custom);
681
+ this.#invalidateSurface(surface);
682
+ }
683
+ const mounted = surface.mounted;
684
+ if (!mounted)
685
+ continue;
686
+ if (mounted.root.ownerDocument !== this.#document) {
687
+ this.#failSurface(surface, "root-adopted");
688
+ if (this.#destroyed)
689
+ return;
690
+ continue;
691
+ }
692
+ if (mounted.root.parentNode !== surface.custom) {
693
+ surface.custom.replaceChildren(mounted.root);
694
+ this.#invalidateSurface(surface);
695
+ }
696
+ }
697
+ }
698
+ }
699
+ function snapshotMountResult(value) {
700
+ if (!value || typeof value !== "object")
701
+ return undefined;
702
+ let descriptors;
703
+ try {
704
+ descriptors = Object.getOwnPropertyDescriptors(value);
705
+ }
706
+ catch {
707
+ return undefined;
708
+ }
709
+ const cleanup = descriptors.cleanup;
710
+ const update = descriptors.update;
711
+ if (!cleanup || !("value" in cleanup) || typeof cleanup.value !== "function")
712
+ return undefined;
713
+ if (update && (!("value" in update) || typeof update.value !== "function"))
714
+ return undefined;
715
+ return Object.freeze({
716
+ cleanup: cleanup.value,
717
+ ...(update
718
+ ? { update: update.value }
719
+ : {}),
720
+ });
721
+ }
722
+ function applyTheme(style, theme) {
723
+ if (!theme)
724
+ return;
725
+ const values = [
726
+ ["--peekling-surface-background", theme.background, ""],
727
+ ["--peekling-surface-color", theme.color, ""],
728
+ ["--peekling-surface-link", theme.linkColor, ""],
729
+ ["--peekling-surface-border", theme.borderColor, ""],
730
+ ["--peekling-surface-radius", theme.radius, "px"],
731
+ ["--peekling-surface-width", theme.width, "px"],
732
+ ["--peekling-surface-padding", theme.padding, "px"],
733
+ ];
734
+ for (const [name, value, unit] of values) {
735
+ if (value !== undefined)
736
+ style.setProperty(name, `${value}${unit}`);
737
+ }
738
+ }