@preprio/toolkit 0.5.0-rc.1 → 0.5.0-rc.11

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 (61) hide show
  1. package/README.md +430 -23
  2. package/dist/astro/index.cjs +1290 -1
  3. package/dist/astro/index.d.cts +47 -1
  4. package/dist/astro/index.d.ts +47 -1
  5. package/dist/astro/index.js +1297 -1
  6. package/dist/{chunk-4JNKYIEV.js → chunk-6423Q744.js} +1449 -642
  7. package/dist/chunk-AOHA5INA.js +26 -0
  8. package/dist/chunk-IPMB32AZ.js +246 -0
  9. package/dist/chunk-KD3GK6TO.js +1221 -0
  10. package/dist/chunk-QSXIHRH4.js +184 -0
  11. package/dist/chunk-S3VT4UWM.js +75 -0
  12. package/dist/chunk-URXSDJOU.js +356 -0
  13. package/dist/codegen/index.cjs +82 -0
  14. package/dist/codegen/index.d.cts +70 -0
  15. package/dist/codegen/index.d.ts +70 -0
  16. package/dist/codegen/index.js +64 -0
  17. package/dist/index.cjs +3367 -416
  18. package/dist/index.d.cts +276 -5
  19. package/dist/index.d.ts +276 -5
  20. package/dist/index.js +307 -144
  21. package/dist/live-preview-DLXGXRE5.js +18 -0
  22. package/dist/nextjs/components.cjs +1608 -386
  23. package/dist/nextjs/components.d.cts +1 -1
  24. package/dist/nextjs/components.d.ts +1 -1
  25. package/dist/nextjs/components.js +4 -1
  26. package/dist/nextjs/index.cjs +1181 -6
  27. package/dist/nextjs/index.d.cts +83 -3
  28. package/dist/nextjs/index.d.ts +83 -3
  29. package/dist/nextjs/index.js +1179 -7
  30. package/dist/nextjs/live-action.cjs +285 -0
  31. package/dist/nextjs/live-action.d.cts +12 -0
  32. package/dist/nextjs/live-action.d.ts +12 -0
  33. package/dist/nextjs/live-action.js +68 -0
  34. package/dist/nextjs/live.cjs +1899 -0
  35. package/dist/nextjs/live.d.cts +54 -0
  36. package/dist/nextjs/live.d.ts +54 -0
  37. package/dist/nextjs/live.js +268 -0
  38. package/dist/nuxt/chunk-BWPGO25E.js +340 -0
  39. package/dist/nuxt/index.cjs +1 -1
  40. package/dist/nuxt/index.js +1 -1
  41. package/dist/nuxt/live-preview-2RCU35W5.js +1189 -0
  42. package/dist/nuxt/live.cjs +1816 -0
  43. package/dist/nuxt/live.d.cts +43 -0
  44. package/dist/nuxt/live.d.ts +43 -0
  45. package/dist/nuxt/live.js +198 -0
  46. package/dist/{pixel-e1fUWl2Y.d.cts → pixel-CX422dCO.d.cts} +18 -3
  47. package/dist/{pixel-e1fUWl2Y.d.ts → pixel-CX422dCO.d.ts} +18 -3
  48. package/dist/react/components.cjs +1608 -386
  49. package/dist/react/components.d.cts +17 -2
  50. package/dist/react/components.d.ts +17 -2
  51. package/dist/react/components.js +1608 -386
  52. package/dist/sveltekit/chunk-BWPGO25E.js +340 -0
  53. package/dist/sveltekit/index.cjs +1 -1
  54. package/dist/sveltekit/index.js +1 -1
  55. package/dist/sveltekit/live-preview-2RCU35W5.js +1189 -0
  56. package/dist/sveltekit/live.cjs +1830 -0
  57. package/dist/sveltekit/live.d.cts +59 -0
  58. package/dist/sveltekit/live.d.ts +59 -0
  59. package/dist/sveltekit/live.js +204 -0
  60. package/package.json +37 -11
  61. package/src/astro/PreprToolbar.astro +7 -1
@@ -96,6 +96,40 @@ var ALL_FEATURES_ENABLED = Object.freeze({
96
96
  editMode: true
97
97
  });
98
98
 
99
+ // src/core/locales/de.json
100
+ var de_default = {
101
+ common: {
102
+ viewingAs: "Ansicht als:",
103
+ user: "Benutzer",
104
+ off: "Aus",
105
+ on: "An",
106
+ reset: "Zur\xFCcksetzen",
107
+ toolbar: "Symbolleiste"
108
+ },
109
+ adaptiveContent: {
110
+ adaptiveContent: "Adaptive Inhalte",
111
+ enablePreview: "Vorschaumodus",
112
+ segment: "Segment",
113
+ ABVariant: "A/B-Variante",
114
+ chooseSegment: "Segment w\xE4hlen",
115
+ searchSegments: "Segmente suchen",
116
+ none: "Keine Segmente",
117
+ allOtherUsers: "Alle anderen Benutzer",
118
+ offDescription: "Die Website als echter Benutzer ansehen (Cookies verwenden)",
119
+ onDescription: "Mit der Symbolleiste Segmente und A/B-Varianten simulieren"
120
+ },
121
+ editingTools: {
122
+ editingTools: "Bearbeitungswerkzeuge",
123
+ editMode: "Bearbeitungsmodus",
124
+ ariaCloseEditMode: "Bearbeitungsmodus schlie\xDFen"
125
+ },
126
+ visualEditor: {
127
+ edit: "Bearbeiten",
128
+ otherItem: "anderes Element",
129
+ linkedItem: "verkn\xFCpftes Element"
130
+ }
131
+ };
132
+
99
133
  // src/core/locales/en.json
100
134
  var en_default = {
101
135
  common: {
@@ -122,6 +156,45 @@ var en_default = {
122
156
  editingTools: "Editing tools",
123
157
  editMode: "Edit mode",
124
158
  ariaCloseEditMode: "Close edit mode"
159
+ },
160
+ visualEditor: {
161
+ edit: "Edit",
162
+ otherItem: "other item",
163
+ linkedItem: "linked item"
164
+ }
165
+ };
166
+
167
+ // src/core/locales/fr.json
168
+ var fr_default = {
169
+ common: {
170
+ viewingAs: "Affichage en tant que :",
171
+ user: "Utilisateur",
172
+ off: "D\xE9sactiv\xE9",
173
+ on: "Activ\xE9",
174
+ reset: "R\xE9initialiser",
175
+ toolbar: "Barre d'outils"
176
+ },
177
+ adaptiveContent: {
178
+ adaptiveContent: "Contenu adaptatif",
179
+ enablePreview: "Mode aper\xE7u",
180
+ segment: "Segment",
181
+ ABVariant: "Variante A/B",
182
+ chooseSegment: "Choisir un segment",
183
+ searchSegments: "Rechercher des segments",
184
+ none: "Aucun segment",
185
+ allOtherUsers: "Tous les autres utilisateurs",
186
+ offDescription: "Voir le site comme un vrai utilisateur (utiliser les cookies)",
187
+ onDescription: "Utilisez la barre d'outils pour simuler des segments et des variantes A/B"
188
+ },
189
+ editingTools: {
190
+ editingTools: "Outils d'\xE9dition",
191
+ editMode: "Mode \xE9dition",
192
+ ariaCloseEditMode: "Fermer le mode \xE9dition"
193
+ },
194
+ visualEditor: {
195
+ edit: "Modifier",
196
+ otherItem: "autre \xE9l\xE9ment",
197
+ linkedItem: "\xE9l\xE9ment li\xE9"
125
198
  }
126
199
  };
127
200
 
@@ -151,13 +224,20 @@ var nl_default = {
151
224
  editingTools: "Editing Tools",
152
225
  editMode: "Edit mode",
153
226
  ariaCloseEditMode: "Edit mode sluiten"
227
+ },
228
+ visualEditor: {
229
+ edit: "Bewerken",
230
+ otherItem: "ander item",
231
+ linkedItem: "gekoppeld item"
154
232
  }
155
233
  };
156
234
 
157
235
  // src/core/i18n.ts
158
236
  var dictionaries = {
159
237
  en: en_default,
160
- nl: nl_default
238
+ nl: nl_default,
239
+ fr: fr_default,
240
+ de: de_default
161
241
  };
162
242
  var LOCALES = Object.keys(dictionaries);
163
243
  function isLocale(value) {
@@ -250,52 +330,53 @@ function createScopedLogger(scopeName) {
250
330
  prefix: `[Prepr][${scopeName}]`
251
331
  });
252
332
  }
253
- function throttle(func, delay) {
254
- let timeoutId = null;
255
- let lastExecTime = 0;
256
- const throttledFunc = ((...args) => {
257
- const currentTime = Date.now();
258
- const timeSinceLastExec = currentTime - lastExecTime;
259
- if (timeSinceLastExec >= delay) {
260
- func(...args);
261
- lastExecTime = currentTime;
262
- } else {
263
- if (timeoutId) {
264
- clearTimeout(timeoutId);
265
- }
266
- timeoutId = setTimeout(() => {
267
- func(...args);
268
- lastExecTime = Date.now();
269
- timeoutId = null;
270
- }, delay - timeSinceLastExec);
271
- }
272
- });
273
- throttledFunc.cancel = () => {
274
- if (timeoutId) {
275
- clearTimeout(timeoutId);
276
- timeoutId = null;
277
- }
278
- };
279
- return throttledFunc;
280
- }
281
- function createElementCache(query, ttl = 1e3) {
282
- let cache = null;
283
- let lastCacheTime = 0;
284
- return () => {
285
- const now = Date.now();
286
- if (!cache || now - lastCacheTime > ttl) {
287
- cache = document.querySelectorAll(query);
288
- lastCacheTime = now;
289
- }
290
- return cache;
291
- };
292
- }
293
333
 
294
334
  // src/core/iframe-bridge.ts
295
335
  var debug = createScopedLogger("iframe-bridge");
336
+ var MAX_REFERENCES = 500;
337
+ var MAX_HREF_LENGTH = 8192;
338
+ var MAX_ASSETS = 500;
339
+ var MAX_ASSET_KEY_LENGTH = 2048;
340
+ var MAX_ASSET_FIELD_LENGTH = 512;
341
+ function parseIds(value) {
342
+ if (!Array.isArray(value)) return void 0;
343
+ return value.slice(0, MAX_REFERENCES).filter((id) => typeof id === "string" && id.length > 0);
344
+ }
345
+ function parseReferences(value) {
346
+ if (!Array.isArray(value)) return void 0;
347
+ const list = [];
348
+ for (const entry of value.slice(0, MAX_REFERENCES)) {
349
+ const id = entry?.id;
350
+ const field = entry?.field;
351
+ if (typeof id === "string" && id && typeof field === "string" && field) {
352
+ list.push({ id, field });
353
+ }
354
+ }
355
+ return list;
356
+ }
357
+ function boundedString(value, max) {
358
+ return typeof value === "string" && value && value.length <= max ? value : void 0;
359
+ }
360
+ function parseAssets(value) {
361
+ if (!Array.isArray(value)) return void 0;
362
+ const list = [];
363
+ for (const entry of value.slice(0, MAX_ASSETS)) {
364
+ if (!entry || typeof entry !== "object") continue;
365
+ const raw = entry;
366
+ const key = boundedString(raw.key, MAX_ASSET_KEY_LENGTH);
367
+ const field = boundedString(raw.field, MAX_ASSET_FIELD_LENGTH);
368
+ if (!key || !field) continue;
369
+ const entryId = boundedString(raw.entryId, MAX_ASSET_FIELD_LENGTH);
370
+ list.push(entryId ? { key, field, entryId } : { key, field });
371
+ }
372
+ return list;
373
+ }
296
374
  var EDITOR_BASE_DOMAIN = "prepr.io";
297
375
  function isAllowedEditorOrigin(origin, allowed) {
298
- if (allowed) return allowed.includes(origin);
376
+ if (allowed) {
377
+ if (allowed.includes("*")) return origin !== "null";
378
+ return allowed.includes(origin);
379
+ }
299
380
  let url;
300
381
  try {
301
382
  url = new URL(origin);
@@ -311,6 +392,10 @@ function isAllowedEditorOrigin(origin, allowed) {
311
392
  function createIframeBridge(store, options = {}) {
312
393
  const allowedOrigins = options.allowedEditorOrigins;
313
394
  const onScrollToField = options.onScrollToField;
395
+ const onHandshake = options.onHandshake;
396
+ const onReferences = options.onReferences;
397
+ const onAssets = options.onAssets;
398
+ const onAllowNavigation = options.onAllowNavigation;
314
399
  let parentOrigin = null;
315
400
  const onKeyDown = (event) => {
316
401
  const key = event.key.toLowerCase();
@@ -320,7 +405,10 @@ function createIframeBridge(store, options = {}) {
320
405
  const onMessage = (evt) => {
321
406
  const data = evt?.data;
322
407
  if (data?.event === "prepr:initVE" && !parentOrigin) {
323
- if (!isAllowedEditorOrigin(evt.origin, allowedOrigins)) return;
408
+ if (!isAllowedEditorOrigin(evt.origin, allowedOrigins)) {
409
+ debug.warn("ignored editor handshake from origin", evt.origin);
410
+ return;
411
+ }
324
412
  parentOrigin = evt.origin;
325
413
  setTrustedParentOrigin(parentOrigin);
326
414
  if (data.scrollPosition != null) {
@@ -328,8 +416,43 @@ function createIframeBridge(store, options = {}) {
328
416
  setTimeout(() => window.scrollTo(0, top), 1);
329
417
  }
330
418
  store?.set({ previewMode: true, editMode: data.editMode ?? true });
419
+ const itemId = typeof data.itemId === "string" && data.itemId ? data.itemId : void 0;
420
+ const referencedIds = parseReferences(data.referencedIds);
421
+ const componentIds = parseIds(data.componentIds);
422
+ const uiLocale = isLocale(data.uiLocale) ? data.uiLocale : void 0;
423
+ const navigationGuard = data.navigationGuard === true;
424
+ const assets = parseAssets(data.assets);
425
+ try {
426
+ onHandshake?.({
427
+ ...itemId && { itemId },
428
+ ...referencedIds && { referencedIds },
429
+ ...componentIds && { componentIds },
430
+ ...uiLocale && { uiLocale },
431
+ ...navigationGuard && { navigationGuard },
432
+ ...assets && { assets }
433
+ });
434
+ } catch (error) {
435
+ debug.warn("handshake handler failed", error);
436
+ }
331
437
  }
332
438
  if (!parentOrigin || evt.origin !== parentOrigin) return;
439
+ if (data?.event === "prepr:references") {
440
+ const assets = parseAssets(data.assets);
441
+ if (assets) {
442
+ try {
443
+ onAssets?.(assets);
444
+ } catch (error) {
445
+ debug.warn("assets handler failed", error);
446
+ }
447
+ }
448
+ const referencedIds = parseReferences(data.referencedIds);
449
+ if (!referencedIds) return;
450
+ try {
451
+ onReferences?.(referencedIds, parseIds(data.componentIds));
452
+ } catch (error) {
453
+ debug.warn("references handler failed", error);
454
+ }
455
+ }
333
456
  if (data?.event === "prepr:getScrollPosition") {
334
457
  const currentScrollY = window.scrollY || document.documentElement.scrollTop;
335
458
  sendPreprEvent("getScrollPosition", { value: currentScrollY });
@@ -337,12 +460,32 @@ function createIframeBridge(store, options = {}) {
337
460
  if (data?.event === "prepr:scrollToField") {
338
461
  if (typeof data.field !== "string" || !data.field) return;
339
462
  if (data.id !== void 0 && typeof data.id !== "string") return;
463
+ const block = typeof data.block === "number" && Number.isInteger(data.block) ? data.block : void 0;
464
+ const entryId = typeof data.entryId === "string" && data.entryId ? data.entryId : void 0;
465
+ const fieldById = typeof data.fieldById === "string" && data.fieldById ? data.fieldById : void 0;
340
466
  try {
341
- onScrollToField?.({ field: data.field, id: data.id });
467
+ onScrollToField?.({
468
+ field: data.field,
469
+ id: data.id,
470
+ block,
471
+ entryId,
472
+ fieldById
473
+ });
342
474
  } catch (error) {
343
475
  debug.warn("scrollToField handler failed", error);
344
476
  }
345
477
  }
478
+ if (data?.event === "prepr:allowNavigation") {
479
+ const href = data.href;
480
+ if (typeof href !== "string" || !href || href.length > MAX_HREF_LENGTH) {
481
+ return;
482
+ }
483
+ try {
484
+ onAllowNavigation?.(href);
485
+ } catch (error) {
486
+ debug.warn("allowNavigation handler failed", error);
487
+ }
488
+ }
346
489
  };
347
490
  return {
348
491
  start() {
@@ -394,6 +537,7 @@ function decodeStega(str) {
394
537
 
395
538
  // src/core/stega/dom.ts
396
539
  var IGNORED_ANCESTORS = "script, style, noscript";
540
+ var AUTO_ENTRY_ATTR = "data-prepr-entry-auto";
397
541
  function isIgnoredTextNode(node) {
398
542
  if (node.parentElement?.closest(IGNORED_ANCESTORS)) return true;
399
543
  return !node.textContent?.trim();
@@ -427,7 +571,9 @@ function parseEditHref(href) {
427
571
  return {
428
572
  id: url.pathname.split("/").pop() || void 0,
429
573
  field: url.searchParams.get("field") || void 0,
430
- locale: url.searchParams.get("locale") || void 0
574
+ locale: url.searchParams.get("locale") || void 0,
575
+ // Added by the toolkit's `annotateEditLinks`, not by Prepr.
576
+ entry: url.searchParams.get("entry") || void 0
431
577
  };
432
578
  } catch {
433
579
  return {};
@@ -441,45 +587,68 @@ function setOrRemove(element, name, value) {
441
587
  }
442
588
  }
443
589
  function tagEncodedElement(element, decoded) {
590
+ if (isSiteTagged(element)) return false;
444
591
  element.setAttribute("data-prepr-encoded", "");
445
592
  element.setAttribute("data-prepr-href", decoded.href);
446
593
  element.setAttribute("data-prepr-origin", decoded.origin);
447
- const { id, field, locale } = parseEditHref(decoded.href);
594
+ const { id, field, locale, entry } = parseEditHref(decoded.href);
448
595
  setOrRemove(element, "data-prepr-id", id);
449
- setOrRemove(element, "data-prepr-field", field);
596
+ const fieldHint = element.getAttribute("data-prepr-edit-target")?.trim() || void 0;
597
+ setOrRemove(element, "data-prepr-field", field ?? fieldHint);
450
598
  setOrRemove(element, "data-prepr-locale", locale);
599
+ const siteEntry = element.hasAttribute("data-prepr-entry-id") && !element.hasAttribute(AUTO_ENTRY_ATTR);
600
+ if (!siteEntry) {
601
+ setOrRemove(element, "data-prepr-entry-id", entry);
602
+ setOrRemove(element, AUTO_ENTRY_ATTR, entry && "true");
603
+ }
604
+ return true;
605
+ }
606
+ function isSiteTagged(element) {
607
+ return element.hasAttribute("data-prepr-encoded") && !element.hasAttribute("data-prepr-href");
608
+ }
609
+ function readEditPayload(element) {
610
+ return {
611
+ href: element.getAttribute("data-prepr-href") ?? "",
612
+ origin: element.getAttribute("data-prepr-origin") ?? "",
613
+ id: element.getAttribute("data-prepr-id") ?? void 0,
614
+ field: element.getAttribute("data-prepr-field") ?? void 0,
615
+ locale: element.getAttribute("data-prepr-locale") ?? void 0,
616
+ entryId: element.getAttribute("data-prepr-entry-id") || void 0
617
+ };
451
618
  }
452
619
 
453
620
  // src/core/stega/auto-clean.ts
454
621
  var debug3 = createScopedLogger("stega:auto-clean");
622
+ var cleanedNodes = /* @__PURE__ */ new WeakMap();
455
623
  function createStegaAutoClean(options) {
456
624
  const stripText = options?.enabled ?? true;
457
625
  let observer = null;
458
626
  let processing = false;
459
- let cleaned = /* @__PURE__ */ new WeakMap();
460
627
  let pendingHidden = /* @__PURE__ */ new Set();
461
628
  let pendingDirty = /* @__PURE__ */ new Set();
462
629
  let debounceTimeout = null;
463
630
  function cleanTextNode(textNode) {
464
631
  if (isIgnoredTextNode(textNode)) return false;
465
632
  const textContent = textNode.textContent;
466
- const info = cleaned.get(textNode);
633
+ const info = cleanedNodes.get(textNode);
467
634
  const settled = stripText ? info?.cleaned : info?.original;
468
635
  if (info && textContent === settled) {
469
636
  if (!info.decoded) return false;
470
637
  const shown = resolveEditTarget(textNode);
471
- if (!shown) return false;
638
+ if (!shown) {
639
+ pendingHidden.add(textNode);
640
+ return false;
641
+ }
472
642
  pendingHidden.delete(textNode);
473
643
  if (shown.hasAttribute("data-prepr-encoded")) return false;
474
- tagEncodedElement(shown, info.decoded);
475
- return true;
644
+ return tagEncodedElement(shown, info.decoded);
476
645
  }
477
646
  try {
478
647
  const { cleaned: stripped, encoded } = (0, import_stega2.vercelStegaSplit)(textContent);
479
648
  if (!encoded || stripped === textContent) return false;
480
649
  const decoded = decodeStega(textContent);
481
650
  if (!decoded?.href && !stripText) return false;
482
- cleaned.set(textNode, {
651
+ cleanedNodes.set(textNode, {
483
652
  original: textContent,
484
653
  cleaned: stripped,
485
654
  decoded: decoded?.href ? decoded : null
@@ -489,8 +658,7 @@ function createStegaAutoClean(options) {
489
658
  const target = resolveEditTarget(textNode);
490
659
  if (target) {
491
660
  pendingHidden.delete(textNode);
492
- tagEncodedElement(target, decoded);
493
- return true;
661
+ return tagEncodedElement(target, decoded);
494
662
  }
495
663
  pendingHidden.add(textNode);
496
664
  } catch (error) {
@@ -521,7 +689,7 @@ function createStegaAutoClean(options) {
521
689
  }
522
690
  if (mutation.type === "characterData" && mutation.target.nodeType === Node.TEXT_NODE) {
523
691
  const textNode = mutation.target;
524
- const info = cleaned.get(textNode);
692
+ const info = cleanedNodes.get(textNode);
525
693
  const text = textNode.textContent ?? "";
526
694
  const settled = stripText ? info?.cleaned : info?.original;
527
695
  if (text !== settled && (0, import_stega2.vercelStegaSplit)(text).encoded) {
@@ -564,7 +732,16 @@ function createStegaAutoClean(options) {
564
732
  retryPendingHidden();
565
733
  }, 50);
566
734
  }
735
+ let resizeTimeout = null;
736
+ function handleResize() {
737
+ if (pendingHidden.size === 0 || resizeTimeout) return;
738
+ resizeTimeout = setTimeout(() => {
739
+ resizeTimeout = null;
740
+ retryPendingHidden();
741
+ }, 100);
742
+ }
567
743
  function setupObserver() {
744
+ window.addEventListener("resize", handleResize, { passive: true });
568
745
  observer = new MutationObserver(handleMutations);
569
746
  observer.observe(document.body, {
570
747
  childList: true,
@@ -587,11 +764,15 @@ function createStegaAutoClean(options) {
587
764
  clearTimeout(debounceTimeout);
588
765
  debounceTimeout = null;
589
766
  }
767
+ if (resizeTimeout) {
768
+ clearTimeout(resizeTimeout);
769
+ resizeTimeout = null;
770
+ }
590
771
  if (observer) {
591
772
  observer.disconnect();
592
773
  observer = null;
774
+ window.removeEventListener("resize", handleResize);
593
775
  }
594
- cleaned = /* @__PURE__ */ new WeakMap();
595
776
  pendingHidden = /* @__PURE__ */ new Set();
596
777
  pendingDirty = /* @__PURE__ */ new Set();
597
778
  debug3.log("stega auto-clean stopped");
@@ -599,6 +780,439 @@ function createStegaAutoClean(options) {
599
780
  return { start, stop };
600
781
  }
601
782
 
783
+ // src/core/visual-editor/overlay.ts
784
+ var HOST_TAG = "prepr-visual-editor";
785
+ var SCROLL_SETTLE_MS = 120;
786
+ var LABEL_SPACE_PX = 22;
787
+ var BOX_PADDING_PX = 3;
788
+ var LABEL_MARGIN_PX = 4;
789
+ var PROBE_POINTS = [
790
+ [0.5, 0.5],
791
+ [0.15, 0.2],
792
+ [0.85, 0.2],
793
+ [0.15, 0.8],
794
+ [0.85, 0.8]
795
+ ];
796
+ function overlapsBox(area, rect) {
797
+ return area.left < rect.right + BOX_PADDING_PX && area.right > rect.left - BOX_PADDING_PX && area.top < rect.bottom + BOX_PADDING_PX && area.bottom > rect.top - BOX_PADDING_PX;
798
+ }
799
+ function overlaps(a2, b2) {
800
+ return a2.left < b2.right && b2.left < a2.right && a2.top < b2.bottom && b2.top < a2.bottom;
801
+ }
802
+ function coverOf(hit, target, x2, y2) {
803
+ let cover = hit.getBoundingClientRect();
804
+ for (let el = hit.parentElement; el && !el.contains(target) && el !== document.body; el = el.parentElement) {
805
+ const r2 = el.getBoundingClientRect();
806
+ if (x2 < r2.left || x2 >= r2.right || y2 < r2.top || y2 >= r2.bottom) break;
807
+ cover = r2;
808
+ }
809
+ return cover;
810
+ }
811
+ function mergeAreas(areas) {
812
+ const merged = areas.slice();
813
+ for (let i2 = 0; i2 < merged.length; i2++) {
814
+ for (let j2 = i2 + 1; j2 < merged.length; j2++) {
815
+ if (!overlaps(merged[i2], merged[j2])) continue;
816
+ merged[i2] = {
817
+ left: Math.min(merged[i2].left, merged[j2].left),
818
+ top: Math.min(merged[i2].top, merged[j2].top),
819
+ right: Math.max(merged[i2].right, merged[j2].right),
820
+ bottom: Math.max(merged[i2].bottom, merged[j2].bottom)
821
+ };
822
+ merged.splice(j2, 1);
823
+ j2 = i2;
824
+ }
825
+ }
826
+ return merged;
827
+ }
828
+ function findCovers(target, rect) {
829
+ const width = window.innerWidth;
830
+ const height = window.innerHeight;
831
+ const covers = [];
832
+ const seen = /* @__PURE__ */ new Set();
833
+ let probed = false;
834
+ let reached = false;
835
+ for (const [fx, fy] of PROBE_POINTS) {
836
+ const x2 = rect.left + rect.width * fx;
837
+ const y2 = rect.top + rect.height * fy;
838
+ if (x2 < 0 || y2 < 0 || x2 >= width || y2 >= height) continue;
839
+ probed = true;
840
+ const hit = document.elementFromPoint(x2, y2);
841
+ if (!hit || hit === target || target.contains(hit)) {
842
+ reached = true;
843
+ continue;
844
+ }
845
+ if (seen.has(hit)) continue;
846
+ seen.add(hit);
847
+ if (covers.some(
848
+ (c2) => x2 >= c2.left && x2 < c2.right && y2 >= c2.top && y2 < c2.bottom
849
+ )) {
850
+ continue;
851
+ }
852
+ covers.push(coverOf(hit, target, x2, y2));
853
+ }
854
+ if (!probed) return [];
855
+ return reached ? mergeAreas(covers) : null;
856
+ }
857
+ function clipPath(x2, y2, covers) {
858
+ const far = 1e5;
859
+ const points = [
860
+ `${-far}px ${-far}px`,
861
+ `${far}px ${-far}px`,
862
+ `${far}px ${far}px`,
863
+ `${-far}px ${far}px`,
864
+ `${-far}px ${-far}px`
865
+ ];
866
+ for (const c2 of covers) {
867
+ const l2 = c2.left - x2;
868
+ const t3 = c2.top - y2;
869
+ const r2 = c2.right - x2;
870
+ const b2 = c2.bottom - y2;
871
+ points.push(
872
+ `${l2}px ${t3}px`,
873
+ `${r2}px ${t3}px`,
874
+ `${r2}px ${b2}px`,
875
+ `${l2}px ${b2}px`,
876
+ `${l2}px ${t3}px`,
877
+ `${-far}px ${-far}px`
878
+ );
879
+ }
880
+ return `polygon(evenodd, ${points.join(", ")})`;
881
+ }
882
+ var STYLES = `
883
+ :host {
884
+ all: initial;
885
+ position: fixed;
886
+ inset: 0;
887
+ pointer-events: none;
888
+ z-index: 2147483646;
889
+ contain: strict;
890
+ }
891
+ .box {
892
+ position: absolute;
893
+ top: 0;
894
+ left: 0;
895
+ box-sizing: border-box;
896
+ border-radius: 6px;
897
+ opacity: 0;
898
+ will-change: transform, width, height;
899
+ transition:
900
+ transform 180ms cubic-bezier(0.2, 0.8, 0.2, 1),
901
+ width 180ms cubic-bezier(0.2, 0.8, 0.2, 1),
902
+ height 180ms cubic-bezier(0.2, 0.8, 0.2, 1),
903
+ opacity 140ms ease;
904
+ }
905
+ .box[data-visible] { opacity: 1; }
906
+ .box[data-instant], :host([data-scrolling]) .box {
907
+ transition: opacity 140ms ease;
908
+ }
909
+ .hover {
910
+ border: 1.5px solid rgba(79, 70, 229, 0.9);
911
+ background: rgba(79, 70, 229, 0.06);
912
+ }
913
+ .selected {
914
+ border: 2px solid #4f46e5;
915
+ box-shadow: 0 0 0 4px rgba(79, 70, 229, 0.18);
916
+ }
917
+ .label {
918
+ position: absolute;
919
+ left: -2px;
920
+ bottom: 100%;
921
+ margin-bottom: 4px;
922
+ padding: 3px 7px;
923
+ border-radius: 5px;
924
+ background: #4f46e5;
925
+ color: #fff;
926
+ font: 500 11px/1.3 ui-sans-serif, system-ui, -apple-system, 'Segoe UI', sans-serif;
927
+ letter-spacing: 0.01em;
928
+ white-space: nowrap;
929
+ display: flex;
930
+ max-width: 240px;
931
+ box-shadow: 0 1px 3px rgba(15, 23, 42, 0.25);
932
+ }
933
+ .text {
934
+ min-width: 0;
935
+ overflow: hidden;
936
+ text-overflow: ellipsis;
937
+ }
938
+ .hover .label { background: rgba(79, 70, 229, 0.92); }
939
+ /* Content of another item than the one open in the editor: still clickable,
940
+ but visibly not part of the item being edited. */
941
+ .hover[data-item-kind='other'] {
942
+ border: 1.5px dashed #a5b4fc;
943
+ background: rgba(100, 116, 139, 0.05);
944
+ }
945
+ .hover[data-item-kind='other'] .label { background: #64748b; }
946
+ .hover[data-item-kind='referenced'] {
947
+ border: 1.5px solid #a5b4fc;
948
+ background: rgba(79, 70, 229, 0.03);
949
+ }
950
+ .hover[data-item-kind='referenced'] .label { background: #818cf8; }
951
+ .box[data-label-covered] .label { visibility: hidden; }
952
+ .box[data-label-inside] .label {
953
+ bottom: auto;
954
+ top: 2px;
955
+ left: 2px;
956
+ margin: 0;
957
+ }
958
+ /* A clickable hover label (edit mode on the live site). Only while shown, so
959
+ a fading box never leaves an invisible click target behind. */
960
+ :host([data-label-action]) .hover[data-visible] .label {
961
+ pointer-events: auto;
962
+ cursor: pointer;
963
+ }
964
+ :host([data-label-action]) .hover .label::after {
965
+ content: '\\2197';
966
+ margin-left: 5px;
967
+ }
968
+ :host([data-label-action]) .hover .label:hover { background: #4338ca; }
969
+ /* Bridges the gap between the label and its element, so the pointer can
970
+ travel from one to the other without the hover ending on the way. */
971
+ :host([data-label-action]) .hover .label::before {
972
+ content: '';
973
+ position: absolute;
974
+ left: 0;
975
+ right: 0;
976
+ top: 100%;
977
+ height: ${LABEL_MARGIN_PX + BOX_PADDING_PX + 2}px;
978
+ }
979
+ @media (prefers-reduced-motion: reduce) {
980
+ .box { transition: opacity 80ms linear; }
981
+ }
982
+ `;
983
+ var VisualEditorOverlay = class {
984
+ constructor(options = {}) {
985
+ this.host = null;
986
+ this.hoverBox = null;
987
+ this.selectedBox = null;
988
+ this.frame = null;
989
+ this.scrollTimeout = null;
990
+ this.resizeObserver = null;
991
+ this.onLabelClick = options.onLabelClick;
992
+ }
993
+ mount() {
994
+ if (this.host) return;
995
+ const host = document.createElement(HOST_TAG);
996
+ const shadow = host.attachShadow({ mode: "open" });
997
+ const style = document.createElement("style");
998
+ style.textContent = STYLES;
999
+ shadow.appendChild(style);
1000
+ this.hoverBox = this.createBox(shadow, "hover");
1001
+ this.selectedBox = this.createBox(shadow, "selected");
1002
+ const onLabelClick = this.onLabelClick;
1003
+ if (onLabelClick) {
1004
+ host.setAttribute("data-label-action", "");
1005
+ const hoverBox = this.hoverBox;
1006
+ hoverBox.label.addEventListener("click", () => {
1007
+ if (hoverBox.target) onLabelClick(hoverBox.target);
1008
+ });
1009
+ }
1010
+ document.documentElement.appendChild(host);
1011
+ this.host = host;
1012
+ if (typeof ResizeObserver !== "undefined") {
1013
+ this.resizeObserver = new ResizeObserver(() => this.schedule());
1014
+ }
1015
+ }
1016
+ unmount() {
1017
+ if (this.frame !== null) cancelAnimationFrame(this.frame);
1018
+ if (this.scrollTimeout) clearTimeout(this.scrollTimeout);
1019
+ this.frame = null;
1020
+ this.scrollTimeout = null;
1021
+ this.resizeObserver?.disconnect();
1022
+ this.resizeObserver = null;
1023
+ this.host?.remove();
1024
+ this.host = null;
1025
+ this.hoverBox = null;
1026
+ this.selectedBox = null;
1027
+ }
1028
+ /**
1029
+ * Whether a pointer event target is this chrome: events from inside the
1030
+ * shadow root reach the page retargeted to its host.
1031
+ */
1032
+ owns(node) {
1033
+ return node !== null && node === this.host;
1034
+ }
1035
+ getHoverTarget() {
1036
+ return this.hoverBox?.target ?? null;
1037
+ }
1038
+ getSelectedTarget() {
1039
+ return this.selectedBox?.target ?? null;
1040
+ }
1041
+ setHover(target, label, options = {}) {
1042
+ if (target) {
1043
+ this.hoverBox?.root.setAttribute(
1044
+ "data-item-kind",
1045
+ options.kind ?? "current"
1046
+ );
1047
+ }
1048
+ this.retarget(this.hoverBox, target, label);
1049
+ }
1050
+ setSelected(target, label) {
1051
+ this.retarget(this.selectedBox, target, label);
1052
+ }
1053
+ /** Call on every scroll; positions follow in the next frame, untransitioned. */
1054
+ onScroll() {
1055
+ if (!this.host) return;
1056
+ this.host.setAttribute("data-scrolling", "");
1057
+ if (this.scrollTimeout) clearTimeout(this.scrollTimeout);
1058
+ this.scrollTimeout = setTimeout(() => {
1059
+ this.host?.removeAttribute("data-scrolling");
1060
+ this.scrollTimeout = null;
1061
+ }, SCROLL_SETTLE_MS);
1062
+ this.schedule();
1063
+ }
1064
+ /** Re-measure both boxes in the next animation frame. */
1065
+ schedule() {
1066
+ if (this.frame !== null || !this.host) return;
1067
+ this.frame = requestAnimationFrame(() => {
1068
+ this.frame = null;
1069
+ this.render();
1070
+ });
1071
+ }
1072
+ createBox(shadow, kind) {
1073
+ const root = document.createElement("div");
1074
+ root.className = `box ${kind}`;
1075
+ const label = document.createElement("div");
1076
+ label.className = "label";
1077
+ const text = document.createElement("span");
1078
+ text.className = "text";
1079
+ label.appendChild(text);
1080
+ root.appendChild(label);
1081
+ shadow.appendChild(root);
1082
+ return { root, label, text, target: null };
1083
+ }
1084
+ retarget(box, target, label) {
1085
+ if (!box || box.target === target) return;
1086
+ const previous = box.target;
1087
+ if (previous) this.resizeObserver?.unobserve(previous);
1088
+ box.target = target;
1089
+ if (target) {
1090
+ this.resizeObserver?.observe(target);
1091
+ box.text.textContent = label;
1092
+ if (!box.root.hasAttribute("data-visible")) {
1093
+ box.root.setAttribute("data-instant", "");
1094
+ }
1095
+ }
1096
+ this.schedule();
1097
+ }
1098
+ render() {
1099
+ const hover = this.hoverBox ? this.measure(this.hoverBox) : null;
1100
+ const selected = this.selectedBox ? this.measure(this.selectedBox) : null;
1101
+ if (this.hoverBox) this.paint(this.hoverBox, hover, selected);
1102
+ if (this.selectedBox) this.paint(this.selectedBox, selected, hover);
1103
+ }
1104
+ measure(box) {
1105
+ const target = box.target;
1106
+ if (!target) return null;
1107
+ if (!target.isConnected) {
1108
+ this.resizeObserver?.unobserve(target);
1109
+ box.target = null;
1110
+ return null;
1111
+ }
1112
+ const rect = target.getBoundingClientRect();
1113
+ if (rect.width === 0 && rect.height === 0) return null;
1114
+ const covers = findCovers(target, rect);
1115
+ if (!covers) return null;
1116
+ return {
1117
+ rect,
1118
+ covers,
1119
+ labelWidth: covers.length ? box.label.offsetWidth : 0,
1120
+ labelHeight: covers.length ? box.label.offsetHeight : 0
1121
+ };
1122
+ }
1123
+ paint(box, measurement, other = null) {
1124
+ const root = box.root;
1125
+ if (!measurement) {
1126
+ root.removeAttribute("data-visible");
1127
+ return;
1128
+ }
1129
+ const { rect, covers } = measurement;
1130
+ const x2 = rect.left - BOX_PADDING_PX;
1131
+ const y2 = rect.top - BOX_PADDING_PX;
1132
+ const labelAbove = {
1133
+ left: x2 - 2,
1134
+ top: y2 - LABEL_SPACE_PX,
1135
+ right: x2 - 2 + (measurement.labelWidth || Math.min(240, rect.width)),
1136
+ bottom: y2
1137
+ };
1138
+ const labelInside = y2 < LABEL_SPACE_PX || other !== null && other.rect !== rect && overlapsBox(labelAbove, other.rect);
1139
+ root.style.transform = `translate3d(${x2}px, ${y2}px, 0)`;
1140
+ root.style.width = `${rect.width + BOX_PADDING_PX * 2}px`;
1141
+ root.style.height = `${rect.height + BOX_PADDING_PX * 2}px`;
1142
+ root.toggleAttribute("data-label-inside", labelInside);
1143
+ root.style.clipPath = covers.length ? clipPath(x2, y2, covers) : "";
1144
+ const labelLeft = labelInside ? x2 + 2 : x2 - 2;
1145
+ const labelTop = labelInside ? y2 + 2 : y2 - LABEL_MARGIN_PX - measurement.labelHeight;
1146
+ const label = {
1147
+ left: labelLeft,
1148
+ top: labelTop,
1149
+ right: labelLeft + measurement.labelWidth,
1150
+ bottom: labelTop + measurement.labelHeight
1151
+ };
1152
+ root.toggleAttribute(
1153
+ "data-label-covered",
1154
+ covers.some((c2) => overlaps(c2, label))
1155
+ );
1156
+ if (root.hasAttribute("data-instant")) {
1157
+ root.setAttribute("data-visible", "");
1158
+ requestAnimationFrame(() => root.removeAttribute("data-instant"));
1159
+ return;
1160
+ }
1161
+ root.setAttribute("data-visible", "");
1162
+ }
1163
+ };
1164
+
1165
+ // src/core/visual-editor/field-path.ts
1166
+ var INDEX_SEGMENT = /^\d+$/;
1167
+ var WORD_SEPARATORS = /[_-]+/g;
1168
+ function normalizeFieldPath(path) {
1169
+ return path.split(".").map(
1170
+ (segment) => INDEX_SEGMENT.test(segment) ? String(Number.parseInt(segment, 10)) : segment
1171
+ ).join(".");
1172
+ }
1173
+ var GENERIC_LEAVES = /* @__PURE__ */ new Set(["value", "text", "body", "content", "label"]);
1174
+ function humanize(name) {
1175
+ const words = name.replace(WORD_SEPARATORS, " ").trim();
1176
+ return words.charAt(0).toUpperCase() + words.slice(1);
1177
+ }
1178
+ function fieldLabel(path, locale = "en") {
1179
+ if (!path) return t("visualEditor.edit", locale);
1180
+ const segments = path.split(".").filter((s2) => s2 && s2 !== "items" && !INDEX_SEGMENT.test(s2));
1181
+ const name = segments[segments.length - 1];
1182
+ if (!name) return t("visualEditor.edit", locale);
1183
+ const parent = segments[segments.length - 2];
1184
+ if (parent && GENERIC_LEAVES.has(name)) {
1185
+ return `${humanize(parent)} \u203A ${humanize(name)}`;
1186
+ }
1187
+ return humanize(name);
1188
+ }
1189
+ function findFieldElement(lookup) {
1190
+ if (!lookup.field) return null;
1191
+ const candidates = document.querySelectorAll(
1192
+ "[data-prepr-encoded]"
1193
+ );
1194
+ if (lookup.entryId) {
1195
+ for (const candidate of candidates) {
1196
+ if (candidate.getAttribute("data-prepr-entry-id") === lookup.entryId) {
1197
+ return candidate;
1198
+ }
1199
+ }
1200
+ }
1201
+ for (const path of [lookup.field, lookup.fieldById]) {
1202
+ if (!path) continue;
1203
+ const wanted = normalizeFieldPath(path);
1204
+ for (const candidate of candidates) {
1205
+ const field = candidate.getAttribute("data-prepr-field");
1206
+ if (!field || normalizeFieldPath(field) !== wanted) continue;
1207
+ if (lookup.id && candidate.getAttribute("data-prepr-id") !== lookup.id) {
1208
+ continue;
1209
+ }
1210
+ return candidate;
1211
+ }
1212
+ }
1213
+ return null;
1214
+ }
1215
+
602
1216
  // src/core/stega/elements.ts
603
1217
  var debug4 = createScopedLogger("stega:elements");
604
1218
  var StegaElements = class {
@@ -688,235 +1302,22 @@ var StegaElements = class {
688
1302
  attributeFilter: ["class", "style", "hidden", "open", "aria-expanded"]
689
1303
  });
690
1304
  }
691
- /** Drops the active class but keeps the data attributes, unlike cleanup(). */
692
- cleanupVisuals() {
693
- document.querySelectorAll(".prepr-overlay-active").forEach((element) => element.classList.remove("prepr-overlay-active"));
694
- }
695
- cleanup() {
1305
+ /**
1306
+ * Stops watching for new content. Tags stay on their elements: the page's
1307
+ * stega characters are stripped once they are read, so a tag removed here
1308
+ * could never be rebuilt when edit mode is turned back on.
1309
+ */
1310
+ disconnect() {
696
1311
  if (this.observer) {
697
1312
  this.observer.disconnect();
698
1313
  this.observer = null;
699
1314
  }
700
- document.querySelectorAll("[data-prepr-encoded]").forEach((element) => {
701
- element.removeAttribute("data-prepr-encoded");
702
- element.removeAttribute("data-prepr-href");
703
- element.removeAttribute("data-prepr-origin");
704
- element.removeAttribute("data-prepr-id");
705
- element.removeAttribute("data-prepr-field");
706
- element.removeAttribute("data-prepr-locale");
707
- element.classList.remove("prepr-overlay-active");
708
- });
709
1315
  this.elements = void 0;
710
1316
  }
711
1317
  };
712
1318
 
713
- // src/core/stega/overlay.ts
714
- var debug5 = createScopedLogger("stega:overlay");
715
- var StegaOverlay = class {
716
- constructor(onEdit) {
717
- this.overlay = null;
718
- this.tooltip = null;
719
- this.hideTimeout = null;
720
- this.currentElement = null;
721
- this.onEdit = onEdit;
722
- }
723
- create() {
724
- const overlay = document.createElement("div");
725
- overlay.className = "prepr-overlay";
726
- overlay.style.display = "none";
727
- const tooltip = document.createElement("div");
728
- tooltip.className = "prepr-tooltip";
729
- tooltip.style.display = "none";
730
- document.body.appendChild(overlay);
731
- document.body.appendChild(tooltip);
732
- this.overlay = overlay;
733
- this.tooltip = tooltip;
734
- return { overlay, tooltip };
735
- }
736
- getTooltip() {
737
- return this.tooltip;
738
- }
739
- hasCurrentElement() {
740
- return this.currentElement !== null;
741
- }
742
- clearHideTimeout() {
743
- if (this.hideTimeout) {
744
- clearTimeout(this.hideTimeout);
745
- this.hideTimeout = null;
746
- }
747
- }
748
- show(element) {
749
- if (!this.overlay || !this.tooltip) return;
750
- this.clearHideTimeout();
751
- if (this.currentElement && this.currentElement !== element) {
752
- this.currentElement.classList.remove("prepr-overlay-active");
753
- }
754
- const rect = element.getBoundingClientRect();
755
- const href = element.getAttribute("data-prepr-href");
756
- const origin = element.getAttribute("data-prepr-origin");
757
- const id = element.getAttribute("data-prepr-id") ?? void 0;
758
- const field = element.getAttribute("data-prepr-field") ?? void 0;
759
- const locale = element.getAttribute("data-prepr-locale") ?? void 0;
760
- debug5.log("showing overlay", { href, origin });
761
- const overlay = this.overlay;
762
- overlay.style.display = "block";
763
- overlay.style.top = `${rect.top + window.scrollY - 2}px`;
764
- overlay.style.left = `${rect.left + window.scrollX - 4}px`;
765
- overlay.style.width = `${rect.width + 8}px`;
766
- overlay.style.height = `${rect.height + 4}px`;
767
- const tooltip = this.tooltip;
768
- if (href && origin) {
769
- const MIN_WIDTH_FOR_TEXT = 80;
770
- const isCompact = rect.width < MIN_WIDTH_FOR_TEXT;
771
- tooltip.textContent = isCompact ? "\u2197" : `${origin} \u2197`;
772
- tooltip.style.display = "block";
773
- tooltip.style.minWidth = isCompact ? "auto" : "80px";
774
- requestAnimationFrame(() => {
775
- let top = rect.top + window.scrollY - tooltip.clientHeight - 2;
776
- let left = rect.right + 4 - tooltip.clientWidth;
777
- const minTop = window.scrollY + 4;
778
- const maxTop = window.scrollY + window.innerHeight - tooltip.clientHeight - 4;
779
- const minLeft = window.scrollX + 4;
780
- const maxLeft = window.scrollX + window.innerWidth - tooltip.clientWidth - 4;
781
- const below = top < minTop;
782
- if (below) {
783
- top = rect.bottom + window.scrollY + 2;
784
- }
785
- top = Math.max(minTop, Math.min(top, maxTop));
786
- left = Math.max(minLeft, Math.min(left, maxLeft));
787
- tooltip.dataset.preprPlacement = below ? "below" : "above";
788
- tooltip.style.top = `${top}px`;
789
- tooltip.style.left = `${left}px`;
790
- });
791
- tooltip.onclick = () => {
792
- this.onEdit({ href, origin, id, field, locale });
793
- };
794
- }
795
- this.currentElement = element;
796
- element.classList.add("prepr-overlay-active");
797
- }
798
- hideImmediate() {
799
- this.clearHideTimeout();
800
- if (this.overlay) this.overlay.style.display = "none";
801
- if (this.tooltip) this.tooltip.style.display = "none";
802
- if (this.currentElement) {
803
- this.currentElement.classList.remove("prepr-overlay-active");
804
- this.currentElement = null;
805
- }
806
- }
807
- hide() {
808
- if (!this.overlay || !this.tooltip) return;
809
- this.clearHideTimeout();
810
- this.hideTimeout = setTimeout(() => this.hideImmediate(), 100);
811
- }
812
- cleanup() {
813
- this.clearHideTimeout();
814
- if (this.overlay) {
815
- this.overlay.style.display = "none";
816
- this.overlay.parentNode?.removeChild(this.overlay);
817
- this.overlay = null;
818
- }
819
- if (this.tooltip) {
820
- this.tooltip.style.display = "none";
821
- this.tooltip.parentNode?.removeChild(this.tooltip);
822
- this.tooltip = null;
823
- }
824
- if (this.currentElement) {
825
- this.currentElement.classList.remove("prepr-overlay-active");
826
- this.currentElement = null;
827
- }
828
- }
829
- };
830
-
831
- // src/core/stega/proximity.ts
832
- var debug6 = createScopedLogger("stega:proximity");
833
- var StegaProximity = class {
834
- constructor() {
835
- this.highlighted = /* @__PURE__ */ new Set();
836
- this.visible = /* @__PURE__ */ new Set();
837
- this.observer = null;
838
- this.getEncodedElements = createElementCache(
839
- "[data-prepr-encoded]",
840
- 200
841
- );
842
- }
843
- /** Call after the encoded element set changes — rebuilds the observer. */
844
- refreshObserving() {
845
- try {
846
- if (typeof IntersectionObserver === "undefined") return;
847
- if (this.observer) {
848
- this.observer.disconnect();
849
- this.observer = null;
850
- }
851
- this.visible = /* @__PURE__ */ new Set();
852
- this.observer = new IntersectionObserver(
853
- (entries) => {
854
- entries.forEach((entry) => {
855
- const el = entry.target;
856
- if (entry.isIntersecting) {
857
- this.visible.add(el);
858
- } else {
859
- this.visible.delete(el);
860
- }
861
- });
862
- },
863
- { root: null, rootMargin: "0px", threshold: 0 }
864
- );
865
- const nodes = this.getEncodedElements();
866
- nodes.forEach((el) => this.observer.observe(el));
867
- debug6.log("observing", nodes.length, "encoded elements");
868
- } catch (error) {
869
- debug6.log("error setting up IntersectionObserver", error);
870
- }
871
- }
872
- stopObserving() {
873
- if (this.observer) {
874
- this.observer.disconnect();
875
- this.observer = null;
876
- this.visible.clear();
877
- }
878
- }
879
- updateElementGradients(cursorX, cursorY) {
880
- const candidates = this.visible.size > 0 ? Array.from(this.visible) : Array.from(this.getEncodedElements());
881
- const newHighlighted = /* @__PURE__ */ new Set();
882
- candidates.forEach((element) => {
883
- const rect = element.getBoundingClientRect();
884
- const distance = Math.min(
885
- Math.abs(cursorX - rect.left),
886
- Math.abs(cursorX - rect.right),
887
- Math.abs(cursorY - rect.top),
888
- Math.abs(cursorY - rect.bottom)
889
- );
890
- if (distance < 150) {
891
- const relativeX = cursorX - rect.left;
892
- const relativeY = cursorY - rect.top;
893
- element.style.setProperty("--cursor-x", `${relativeX}px`);
894
- element.style.setProperty("--cursor-y", `${relativeY}px`);
895
- const baseGradientSize = Math.max(
896
- 150,
897
- Math.max(rect.width, rect.height) * 1.1
898
- );
899
- const distanceScale = Math.max(0, (400 - distance) / 400);
900
- const gradientSize = baseGradientSize * distanceScale;
901
- element.style.setProperty("--gradient-size", `${gradientSize}px`);
902
- element.classList.add("prepr-proximity-highlight");
903
- newHighlighted.add(element);
904
- } else {
905
- element.classList.remove("prepr-proximity-highlight");
906
- }
907
- });
908
- this.highlighted = newHighlighted;
909
- }
910
- clearAllHighlights() {
911
- this.highlighted.forEach(
912
- (element) => element.classList.remove("prepr-proximity-highlight")
913
- );
914
- this.highlighted.clear();
915
- }
916
- };
917
-
918
1319
  // src/core/stega/stega.generated.ts
919
- var STEGA_STYLES = '[data-prepr-encoded]{position:relative}.prepr-overlay{pointer-events:none;z-index:10000;border:2px solid #4f46e5;border-radius:4px 0 4px 4px;transition:all .2s ease-in-out;position:absolute}.prepr-tooltip{color:#fff;white-space:nowrap;z-index:10001;text-align:center;pointer-events:auto;cursor:pointer;background-color:#4f46e5;border-radius:4px 4px 0 0;min-width:80px;padding:4px 8px;font-size:12px;line-height:1;transition:opacity .2s ease-in-out;position:absolute}.prepr-tooltip[data-prepr-placement=below]{border-radius:0 0 4px 4px}.prepr-tooltip:hover{background-color:#4338ca}.prepr-proximity-highlight{z-index:1;position:relative}.prepr-proximity-highlight,.prepr-proximity-highlight *{overflow:visible!important}.prepr-proximity-highlight:before{content:"";pointer-events:none;background:radial-gradient(circle var(--gradient-size) at var(--cursor-x) var(--cursor-y), #4f46e526 0%, #2563eb00 70%);z-index:9999;border:2px solid #4f46e526;border-radius:4px;transition:all .2s;position:absolute;top:-2px;bottom:-2px;left:-4px;right:-4px;overflow:visible}';
1320
+ var STEGA_STYLES = '@property --prepr-presence{syntax:"<number>";inherits:true;initial-value:1}[data-prepr-encoded]{outline:1px dashed rgb(79 70 229/calc(.3 * var(--prepr-presence,1)));outline-offset:2px}';
920
1321
 
921
1322
  // src/core/stega/styles.ts
922
1323
  var STEGA_STYLE_MARKER = "data-prepr-stega";
@@ -936,46 +1337,80 @@ function removeStegaStyles() {
936
1337
  }
937
1338
 
938
1339
  // src/core/stega/scan.ts
939
- var debug7 = createScopedLogger("stega:scan");
1340
+ var debug5 = createScopedLogger("stega:scan");
1341
+ var ENCODED_SELECTOR = "[data-prepr-encoded]";
1342
+ var PRESENCE_FADE_MS = 400;
940
1343
  var EDITOR_MODE_STYLES = "[data-prepr-encoded]{cursor:pointer}";
1344
+ function fadeInPresence() {
1345
+ const root = document.documentElement;
1346
+ if (typeof root.animate !== "function") return;
1347
+ if (window.matchMedia?.("(prefers-reduced-motion: reduce)").matches) return;
1348
+ root.animate(
1349
+ { "--prepr-presence": ["0", "1"] },
1350
+ { duration: PRESENCE_FADE_MS, easing: "ease-out" }
1351
+ );
1352
+ }
941
1353
  function createStegaController(opts) {
942
1354
  const tooltipEnabled = opts.tooltip ?? true;
943
- const overlay = new StegaOverlay(opts.onEdit);
944
- const proximity = new StegaProximity();
1355
+ const locale = opts.locale ?? "en";
945
1356
  const elements = new StegaElements();
1357
+ const overlay = new VisualEditorOverlay({
1358
+ onLabelClick: (target) => opts.onEdit(readEditPayload(target))
1359
+ });
946
1360
  let initialized = false;
947
- let throttledMouseMove = null;
948
- const handleTooltipMouseEnter = () => {
949
- overlay.clearHideTimeout();
1361
+ let pointerFrame = null;
1362
+ let pointerTarget = null;
1363
+ let pointerX = 0;
1364
+ let pointerY = 0;
1365
+ let hitTestPending = false;
1366
+ const updateHover = () => {
1367
+ pointerFrame = null;
1368
+ if (hitTestPending) {
1369
+ hitTestPending = false;
1370
+ pointerTarget = document.elementFromPoint(pointerX, pointerY);
1371
+ }
1372
+ if (overlay.owns(pointerTarget)) return;
1373
+ const encoded = pointerTarget instanceof Element ? pointerTarget.closest(ENCODED_SELECTOR) : null;
1374
+ overlay.setHover(
1375
+ encoded,
1376
+ encoded ? fieldLabel(encoded.getAttribute("data-prepr-field"), locale) : ""
1377
+ );
950
1378
  };
951
- const handleTooltipMouseLeave = () => {
952
- if (!overlay.hasCurrentElement()) {
953
- overlay.hide();
1379
+ const scheduleHover = () => {
1380
+ if (pointerFrame === null) {
1381
+ pointerFrame = requestAnimationFrame(updateHover);
954
1382
  }
955
1383
  };
1384
+ const handlePointer = (e2) => {
1385
+ pointerTarget = e2.target;
1386
+ pointerX = e2.clientX;
1387
+ pointerY = e2.clientY;
1388
+ hitTestPending = false;
1389
+ scheduleHover();
1390
+ };
1391
+ const handlePointerLeave = () => {
1392
+ pointerTarget = null;
1393
+ hitTestPending = false;
1394
+ overlay.setHover(null, "");
1395
+ };
956
1396
  const handleScroll = () => {
957
- overlay.hideImmediate();
958
- proximity.clearAllHighlights();
1397
+ overlay.onScroll();
1398
+ if (pointerTarget === null) return;
1399
+ hitTestPending = true;
1400
+ scheduleHover();
959
1401
  };
1402
+ const handleResize = () => overlay.schedule();
960
1403
  const handleClick = (e2) => {
961
1404
  const target = e2.target;
962
- const encoded = target?.closest(
963
- "[data-prepr-encoded]"
964
- );
1405
+ const encoded = target?.closest(ENCODED_SELECTOR);
965
1406
  if (!encoded) return;
966
1407
  e2.preventDefault();
967
1408
  e2.stopPropagation();
968
- opts.onEdit({
969
- href: encoded.getAttribute("data-prepr-href") ?? "",
970
- origin: encoded.getAttribute("data-prepr-origin") ?? "",
971
- id: encoded.getAttribute("data-prepr-id") ?? void 0,
972
- field: encoded.getAttribute("data-prepr-field") ?? void 0,
973
- locale: encoded.getAttribute("data-prepr-locale") ?? void 0
974
- });
1409
+ opts.onEdit(readEditPayload(encoded));
975
1410
  };
976
1411
  const start = () => {
977
1412
  if (initialized) {
978
- debug7.log("already initialized, skipping setup");
1413
+ debug5.log("already initialized, skipping setup");
979
1414
  return;
980
1415
  }
981
1416
  if (!tooltipEnabled) {
@@ -985,73 +1420,64 @@ function createStegaController(opts) {
985
1420
  });
986
1421
  document.addEventListener("click", handleClick, { capture: true });
987
1422
  initialized = true;
988
- debug7.log("stega controller started (editor mode)");
1423
+ debug5.log("stega controller started (headless)");
989
1424
  return;
990
1425
  }
991
1426
  injectStegaStyles();
992
- const { tooltip } = overlay.create();
993
- tooltip.addEventListener("mouseenter", handleTooltipMouseEnter);
994
- tooltip.addEventListener("mouseleave", handleTooltipMouseLeave);
1427
+ fadeInPresence();
1428
+ overlay.mount();
995
1429
  elements.scanDocument(true);
996
- proximity.refreshObserving();
997
- elements.setupMutationObserver(() => proximity.refreshObserving());
998
- throttledMouseMove = throttle((e2) => {
999
- const mouseEvent = e2;
1000
- const target = mouseEvent.target;
1001
- if (!target) return;
1002
- if (target.closest(".prepr-tooltip")) return;
1003
- proximity.updateElementGradients(mouseEvent.clientX, mouseEvent.clientY);
1004
- const encoded = target.closest("[data-prepr-encoded]");
1005
- if (encoded) {
1006
- overlay.show(encoded);
1007
- } else {
1008
- overlay.hide();
1009
- }
1010
- }, 16);
1011
- document.addEventListener("mousemove", throttledMouseMove, {
1012
- passive: true
1430
+ elements.setupMutationObserver(() => {
1431
+ if (pointerTarget === null) return;
1432
+ hitTestPending = true;
1433
+ scheduleHover();
1013
1434
  });
1435
+ document.addEventListener("pointermove", handlePointer, { passive: true });
1436
+ document.addEventListener("pointerdown", handlePointer, { passive: true });
1437
+ document.documentElement.addEventListener(
1438
+ "pointerleave",
1439
+ handlePointerLeave
1440
+ );
1014
1441
  window.addEventListener("scroll", handleScroll, {
1015
1442
  capture: true,
1016
1443
  passive: true
1017
1444
  });
1445
+ window.addEventListener("resize", handleResize, { passive: true });
1018
1446
  initialized = true;
1019
- debug7.log("stega controller started");
1447
+ debug5.log("stega controller started");
1020
1448
  };
1021
1449
  const stop = () => {
1022
1450
  if (!initialized) return;
1451
+ initialized = false;
1023
1452
  if (!tooltipEnabled) {
1024
1453
  document.removeEventListener("click", handleClick, { capture: true });
1025
- elements.cleanup();
1454
+ elements.disconnect();
1026
1455
  removeStegaStyles();
1027
- initialized = false;
1028
- debug7.log("stega controller stopped (editor mode)");
1456
+ debug5.log("stega controller stopped (headless)");
1029
1457
  return;
1030
1458
  }
1031
- if (throttledMouseMove) {
1032
- document.removeEventListener("mousemove", throttledMouseMove);
1033
- throttledMouseMove.cancel();
1034
- throttledMouseMove = null;
1035
- }
1459
+ if (pointerFrame !== null) cancelAnimationFrame(pointerFrame);
1460
+ pointerFrame = null;
1461
+ pointerTarget = null;
1462
+ hitTestPending = false;
1463
+ document.removeEventListener("pointermove", handlePointer);
1464
+ document.removeEventListener("pointerdown", handlePointer);
1465
+ document.documentElement.removeEventListener(
1466
+ "pointerleave",
1467
+ handlePointerLeave
1468
+ );
1036
1469
  window.removeEventListener("scroll", handleScroll, { capture: true });
1037
- const tooltip = overlay.getTooltip();
1038
- if (tooltip) {
1039
- tooltip.removeEventListener("mouseenter", handleTooltipMouseEnter);
1040
- tooltip.removeEventListener("mouseleave", handleTooltipMouseLeave);
1041
- }
1042
- overlay.cleanup();
1043
- proximity.clearAllHighlights();
1044
- proximity.stopObserving();
1045
- elements.cleanup();
1470
+ window.removeEventListener("resize", handleResize);
1471
+ overlay.unmount();
1472
+ elements.disconnect();
1046
1473
  removeStegaStyles();
1047
- initialized = false;
1048
- debug7.log("stega controller stopped");
1474
+ debug5.log("stega controller stopped");
1049
1475
  };
1050
1476
  return { start, stop };
1051
1477
  }
1052
1478
 
1053
1479
  // src/core/stega/scroll-to-field.ts
1054
- var debug8 = createScopedLogger("stega:scroll");
1480
+ var debug6 = createScopedLogger("stega:scroll");
1055
1481
  var FLASH_DURATION_MS = 1200;
1056
1482
  var flashTimeout = null;
1057
1483
  function scrollToField(request) {
@@ -1068,7 +1494,7 @@ function scrollToField(request) {
1068
1494
  break;
1069
1495
  }
1070
1496
  if (!target) {
1071
- debug8.log("no element found for field", field);
1497
+ debug6.log("no element found for field", field);
1072
1498
  return false;
1073
1499
  }
1074
1500
  target.scrollIntoView({ behavior: "smooth", block: "center" });
@@ -1216,7 +1642,7 @@ function splitPath(full) {
1216
1642
  return [full.slice(0, index), full.slice(index + 1)];
1217
1643
  }
1218
1644
 
1219
- // ../../node_modules/.pnpm/preact@10.29.7/node_modules/preact/dist/preact.mjs
1645
+ // ../../node_modules/.pnpm/preact@10.29.8/node_modules/preact/dist/preact.mjs
1220
1646
  var n;
1221
1647
  var l;
1222
1648
  var u;
@@ -1287,8 +1713,8 @@ function H() {
1287
1713
  }
1288
1714
  }
1289
1715
  function L(n2, l2, u3, t3, i2, r2, o2, e2, f3, c2, a2) {
1290
- var s2, h2, p2, v2, y2, _2, g2, m2 = t3 && t3.__k || w, b2 = l2.length;
1291
- for (f3 = T(u3, l2, m2, f3, b2), s2 = 0; s2 < b2; s2++) null != (p2 = u3.__k[s2]) && (h2 = -1 != p2.__i && m2[p2.__i] || d, p2.__i = s2, _2 = q(n2, p2, h2, i2, r2, o2, e2, f3, c2, a2), v2 = p2.__e, p2.ref && h2.ref != p2.ref && (h2.ref && J(h2.ref, null, p2), a2.push(p2.ref, p2.__c || v2, p2)), null == y2 && null != v2 && (y2 = v2), (g2 = !!(4 & p2.__u)) || h2.__k === p2.__k ? (f3 = j(p2, f3, n2, g2), g2 && h2.__e && (h2.__e = null)) : "function" == typeof p2.type && void 0 !== _2 ? f3 = _2 : v2 && (f3 = v2.nextSibling), p2.__u &= -7);
1716
+ var s2, h2, p2, v2, y2, _2, g2 = t3 && t3.__k || w, m2 = l2.length;
1717
+ for (f3 = T(u3, l2, g2, f3, m2), s2 = 0; s2 < m2; s2++) null != (p2 = u3.__k[s2]) && (h2 = -1 != p2.__i && g2[p2.__i] || d, p2.__i = s2, _2 = q(n2, p2, h2, i2, r2, o2, e2, f3, c2, a2), v2 = p2.__e, p2.ref && h2.ref != p2.ref && (h2.ref && J(h2.ref, null, p2), a2.push(p2.ref, p2.__c || v2, p2)), null == y2 && null != v2 && (y2 = v2), 4 & p2.__u ? (f3 = j(p2, f3, n2), h2.__e && (h2.__e = null)) : "function" == typeof p2.type && void 0 !== _2 ? f3 = _2 : v2 && (f3 = v2.nextSibling), p2.__u &= -7);
1292
1718
  return u3.__e = y2, f3;
1293
1719
  }
1294
1720
  function T(n2, l2, u3, t3, i2) {
@@ -1297,13 +1723,13 @@ function T(n2, l2, u3, t3, i2) {
1297
1723
  if (s2) for (r2 = 0; r2 < a2; r2++) null != (e2 = u3[r2]) && 0 == (2 & e2.__u) && (e2.__e == t3 && (t3 = $(e2)), K(e2, e2));
1298
1724
  return t3;
1299
1725
  }
1300
- function j(n2, l2, u3, t3) {
1301
- var i2, r2;
1726
+ function j(n2, l2, u3) {
1727
+ var t3, i2;
1302
1728
  if ("function" == typeof n2.type) {
1303
- for (i2 = n2.__k, r2 = 0; i2 && r2 < i2.length; r2++) i2[r2] && (i2[r2].__ = n2, l2 = j(i2[r2], l2, u3, t3));
1729
+ for (t3 = n2.__k, i2 = 0; t3 && i2 < t3.length; i2++) t3[i2] && (t3[i2].__ = n2, l2 = j(t3[i2], l2, u3));
1304
1730
  return l2;
1305
1731
  }
1306
- n2.__e != l2 && (t3 && (l2 && n2.type && !l2.parentNode && (l2 = $(n2)), u3.insertBefore(n2.__e, l2 || null)), l2 = n2.__e);
1732
+ n2.__e != l2 && (l2 && n2.type && !l2.parentNode && (l2 = $(n2)), l2 = u3.insertBefore(n2.__e, l2 || null));
1307
1733
  do {
1308
1734
  l2 = l2 && l2.nextSibling;
1309
1735
  } while (null != l2 && 8 == l2.nodeType);
@@ -1349,35 +1775,35 @@ function V(n2) {
1349
1775
  };
1350
1776
  }
1351
1777
  function q(n2, u3, t3, i2, r2, o2, e2, f3, c2, a2) {
1352
- var s2, h2, p2, v2, y2, d2, _2, k2, x2, M, $2, I2, P2, A2, H2, T2, j2 = u3.type;
1778
+ var s2, h2, p2, v2, y2, d2, _2, k2, x2, M, I2, P2, A2, H2, T2, j2, F = u3.type;
1353
1779
  if (void 0 !== u3.constructor) return null;
1354
1780
  128 & t3.__u && (c2 = !!(32 & t3.__u), o2 = [f3 = u3.__e = t3.__e]), (s2 = l.__b) && s2(u3);
1355
- n: if ("function" == typeof j2) {
1781
+ n: if ("function" == typeof F) {
1356
1782
  h2 = e2.length;
1357
1783
  try {
1358
- if (x2 = u3.props, M = j2.prototype && j2.prototype.render, $2 = (s2 = j2.contextType) && i2[s2.__c], I2 = s2 ? $2 ? $2.props.value : s2.__ : i2, t3.__c ? k2 = (p2 = u3.__c = t3.__c).__ = p2.__E : (M ? u3.__c = p2 = new j2(x2, I2) : (u3.__c = p2 = new C(x2, I2), p2.constructor = j2, p2.render = Q), $2 && $2.sub(p2), p2.state || (p2.state = {}), p2.__n = i2, v2 = p2.__d = true, p2.__h = [], p2._sb = []), M && null == p2.__s && (p2.__s = p2.state), M && null != j2.getDerivedStateFromProps && (p2.__s == p2.state && (p2.__s = m({}, p2.__s)), m(p2.__s, j2.getDerivedStateFromProps(x2, p2.__s))), y2 = p2.props, d2 = p2.state, p2.__v = u3, v2) M && null == j2.getDerivedStateFromProps && null != p2.componentWillMount && p2.componentWillMount(), M && null != p2.componentDidMount && p2.__h.push(p2.componentDidMount);
1784
+ if (x2 = u3.props, M = F.prototype && F.prototype.render, I2 = (s2 = F.contextType) && i2[s2.__c], P2 = s2 ? I2 ? I2.props.value : s2.__ : i2, t3.__c ? k2 = (p2 = u3.__c = t3.__c).__ = p2.__E : (M ? u3.__c = p2 = new F(x2, P2) : (u3.__c = p2 = new C(x2, P2), p2.constructor = F, p2.render = Q), I2 && I2.sub(p2), p2.state || (p2.state = {}), p2.__n = i2, v2 = p2.__d = true, p2.__h = [], p2._sb = []), M && null == p2.__s && (p2.__s = p2.state), M && null != F.getDerivedStateFromProps && (p2.__s == p2.state && (p2.__s = m({}, p2.__s)), m(p2.__s, F.getDerivedStateFromProps(x2, p2.__s))), y2 = p2.props, d2 = p2.state, p2.__v = u3, v2) M && null == F.getDerivedStateFromProps && null != p2.componentWillMount && p2.componentWillMount(), M && null != p2.componentDidMount && p2.__h.push(p2.componentDidMount);
1359
1785
  else {
1360
- if (M && null == j2.getDerivedStateFromProps && x2 !== y2 && null != p2.componentWillReceiveProps && p2.componentWillReceiveProps(x2, I2), u3.__v == t3.__v || !p2.__e && null != p2.shouldComponentUpdate && false === p2.shouldComponentUpdate(x2, p2.__s, I2)) {
1786
+ if (M && null == F.getDerivedStateFromProps && x2 !== y2 && null != p2.componentWillReceiveProps && p2.componentWillReceiveProps(x2, P2), u3.__v == t3.__v || !p2.__e && null != p2.shouldComponentUpdate && false === p2.shouldComponentUpdate(x2, p2.__s, P2)) {
1361
1787
  u3.__v != t3.__v && (p2.props = x2, p2.state = p2.__s, p2.__d = false), u3.__e = t3.__e, u3.__k = t3.__k, u3.__k.some(function(n3) {
1362
1788
  n3 && (n3.__ = u3);
1363
- }), w.push.apply(p2.__h, p2._sb), p2._sb = [], p2.__h.length && e2.push(p2);
1789
+ }), w.push.apply(p2.__h, p2._sb), p2._sb = [], p2.__h.length && e2.push(p2), f3 = $(t3);
1364
1790
  break n;
1365
1791
  }
1366
- null != p2.componentWillUpdate && p2.componentWillUpdate(x2, p2.__s, I2), M && null != p2.componentDidUpdate && p2.__h.push(function() {
1792
+ null != p2.componentWillUpdate && p2.componentWillUpdate(x2, p2.__s, P2), M && null != p2.componentDidUpdate && p2.__h.push(function() {
1367
1793
  p2.componentDidUpdate(y2, d2, _2);
1368
1794
  });
1369
1795
  }
1370
- if (p2.context = I2, p2.props = x2, p2.__P = n2, p2.__e = false, P2 = l.__r, A2 = 0, M) p2.state = p2.__s, p2.__d = false, P2 && P2(u3), s2 = p2.render(p2.props, p2.state, p2.context), w.push.apply(p2.__h, p2._sb), p2._sb = [];
1796
+ if (p2.context = P2, p2.props = x2, p2.__P = n2, p2.__e = false, A2 = l.__r, H2 = 0, M) p2.state = p2.__s, p2.__d = false, A2 && A2(u3), s2 = p2.render(p2.props, p2.state, p2.context), w.push.apply(p2.__h, p2._sb), p2._sb = [];
1371
1797
  else do {
1372
- p2.__d = false, P2 && P2(u3), s2 = p2.render(p2.props, p2.state, p2.context), p2.state = p2.__s;
1373
- } while (p2.__d && ++A2 < 25);
1374
- p2.state = p2.__s, null != p2.getChildContext && (i2 = m(m({}, i2), p2.getChildContext())), M && !v2 && null != p2.getSnapshotBeforeUpdate && (_2 = p2.getSnapshotBeforeUpdate(y2, d2)), H2 = null != s2 && s2.type === S && null == s2.key ? E(s2.props.children) : s2, f3 = L(n2, g(H2) ? H2 : [H2], u3, t3, i2, r2, o2, e2, f3, c2, a2), p2.base = u3.__e, u3.__u &= -161, p2.__h.length && e2.push(p2), k2 && (p2.__E = p2.__ = null);
1798
+ p2.__d = false, A2 && A2(u3), s2 = p2.render(p2.props, p2.state, p2.context), p2.state = p2.__s;
1799
+ } while (p2.__d && ++H2 < 25);
1800
+ p2.state = p2.__s, null != p2.getChildContext && (i2 = m(m({}, i2), p2.getChildContext())), M && !v2 && null != p2.getSnapshotBeforeUpdate && (_2 = p2.getSnapshotBeforeUpdate(y2, d2)), T2 = null != s2 && s2.type === S && null == s2.key ? E(s2.props.children) : s2, f3 = L(n2, g(T2) ? T2 : [T2], u3, t3, i2, r2, o2, e2, f3, c2, a2), p2.base = u3.__e, u3.__u &= -161, p2.__h.length && e2.push(p2), k2 && (p2.__E = p2.__ = null);
1375
1801
  } catch (n3) {
1376
1802
  if (e2.length = h2, u3.__v = null, c2 || null != o2) {
1377
1803
  if (n3.then) {
1378
1804
  for (u3.__u |= c2 ? 160 : 128; f3 && 8 == f3.nodeType && f3.nextSibling; ) f3 = f3.nextSibling;
1379
1805
  null != o2 && (o2[o2.indexOf(f3)] = null), u3.__e = f3;
1380
- } else if (null != o2) for (T2 = o2.length; T2--; ) b(o2[T2]);
1806
+ } else if (null != o2) for (j2 = o2.length; j2--; ) b(o2[j2]);
1381
1807
  } else u3.__e = t3.__e;
1382
1808
  null == u3.__k && (u3.__k = t3.__k || []), n3.then || B(u3), l.__e(n3, u3, t3);
1383
1809
  }
@@ -1498,9 +1924,9 @@ var CLOSE_ICON_SVG = `<svg width="10" height="10" viewBox="0 0 10 10" fill="none
1498
1924
  </svg>`;
1499
1925
 
1500
1926
  // src/core/ui/toolbar.generated.ts
1501
- var TOOLBAR_CSS = ":host{--prepr-primary:#4338ca;--prepr-bg:#eef2ff;--prepr-text:#1f2937;--prepr-radius:8px;--prepr-shadow:0px 0px 40px 0px #1f29373d;--prepr-z-index:10000;--prepr-primary-50:#eef2ff;--prepr-primary-100:#e0e7ff;--prepr-primary-800:#3730a3;--prepr-indigo-600:#4f46e5;--prepr-indigo-50:#eef2ff;--prepr-indigo-700:#4338ca;--prepr-secondary-400:#fb923c;--prepr-secondary-500:#f97316;--prepr-gray-100:#f3f4f6;--prepr-gray-200:#e5e7eb;--prepr-gray-300:#d1d5db;--prepr-gray-400:#9ca3af;--prepr-gray-500:#6b7280;--prepr-gray-700:#374151;--prepr-gray-800:#1f2937;--prepr-gray-900:#111827;--prepr-shadow-sm:0px 0px 12px 0px #c3c3c3cc;color:var(--prepr-text);box-sizing:border-box;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica,Arial,sans-serif;font-size:14px;line-height:1.5}:host *,:host :before,:host :after,*,:before,:after{box-sizing:border-box}button{font-family:inherit;font-size:inherit;margin:0}.prepr-regular-text{font-size:.875rem;font-weight:500;line-height:1.5}.prepr-backdrop{z-index:calc(var(--prepr-z-index) - 1);opacity:0;pointer-events:none;background:#0000001a;transition:opacity .2s ease-in-out;position:fixed;top:0;bottom:0;left:0;right:0}.prepr-backdrop[data-open=true]{opacity:1;pointer-events:auto;animation:.2s ease-in-out prepr-fade-in}.prepr-container{z-index:var(--prepr-z-index);justify-content:flex-end;align-items:center;display:flex;position:fixed;top:calc(50% - 64px);right:0}.prepr-toggle-holder{padding:.5rem}.prepr-toggle{cursor:pointer;background:var(--prepr-primary);color:#fff;border:none;border-radius:9999px;justify-content:center;align-items:center;width:2.25rem;height:2.25rem;display:flex}.prepr-toggle>span{display:flex}.prepr-toggle svg{color:#fff}.prepr-panel{z-index:var(--prepr-z-index);opacity:0;pointer-events:none;justify-content:flex-end;padding:.75rem;transition:opacity .2s ease-in-out;display:flex;position:fixed;top:50%;right:0;transform:translateY(-50%)}.prepr-panel[data-open=true]{opacity:1;pointer-events:auto;animation:.2s ease-in-out prepr-fade-in}.prepr-content{box-shadow:var(--prepr-shadow);border-radius:var(--prepr-radius);background:var(--prepr-bg);flex-direction:column;gap:2.5rem;width:100%;padding:1.5rem;display:flex}@media (min-width:640px){.prepr-panel{margin-right:4rem;padding:0}.prepr-content{width:502px;padding:2.5rem}}.prepr-header{justify-content:space-between;align-items:center;gap:.5rem;display:flex}.prepr-header-left{align-items:flex-start;gap:.75rem;display:flex}.prepr-badge{background:var(--prepr-primary-100);height:1.5rem;color:var(--prepr-primary);border-radius:4px;align-items:center;padding:.25rem .75rem;font-size:.875rem;display:flex}.prepr-header-close{cursor:pointer;background:var(--prepr-primary-100);width:2rem;height:2rem;color:var(--prepr-gray-700);border:none;border-radius:6px;justify-content:center;align-items:center;display:flex}.prepr-section{flex-direction:column;gap:.5rem;display:flex}.prepr-section-label{color:var(--prepr-gray-400);font-size:.875rem}.prepr-row{flex-wrap:wrap;justify-content:space-between;align-items:center;gap:.5rem 1.5rem;display:flex}.prepr-row-title{color:var(--prepr-gray-800);margin:0;font-size:1rem;font-weight:600}.prepr-radiogroup{border-radius:var(--prepr-radius);border:1px solid var(--prepr-gray-300);background:#fff;align-items:center;gap:.25rem;height:2.5rem;padding:.25rem;display:flex}.prepr-radiogroup[data-disabled=true]{cursor:not-allowed;opacity:.5}.prepr-radiogroup[data-disabled=true] .prepr-radio{pointer-events:none}.prepr-radio{cursor:pointer;white-space:nowrap;text-align:center;height:2rem;color:var(--prepr-gray-900);background:0 0;border:none;border-radius:6px;justify-content:center;align-items:center;padding:.5rem 1.125rem;font-size:.875rem;font-weight:500;transition:all .2s ease-in-out;display:flex}.prepr-radio[data-checked=true]{background:var(--prepr-indigo-600);color:#fff;box-shadow:var(--prepr-shadow-sm)}.prepr-radio[data-checked=true][data-off=true]{background:var(--prepr-gray-800);color:#fff}.prepr-radio-variant{width:82px}.prepr-listbox{position:relative}.prepr-segment-button{border-radius:var(--prepr-radius);border:1px solid var(--prepr-gray-300);width:240px;height:2.5rem;color:var(--prepr-gray-500);cursor:pointer;background:#fff;flex-shrink:0;align-items:center;gap:.5rem;padding:0 .5rem;font-size:.875rem;font-weight:500;display:flex;overflow:hidden}@media (min-width:768px){.prepr-segment-button{padding:0 1rem}}.prepr-segment-button:disabled{cursor:not-allowed;background:var(--prepr-gray-200);color:var(--prepr-gray-400)}.prepr-segment-button[aria-expanded=true]{border-bottom-color:#fff;border-bottom-right-radius:0;border-bottom-left-radius:0}.prepr-segment-label{white-space:nowrap;text-overflow:ellipsis;text-align:left;width:100%;margin-right:auto;overflow:hidden}.prepr-segment-caret{color:var(--prepr-gray-800);display:flex}.prepr-options{z-index:calc(var(--prepr-z-index) + 1);border-left:1px solid var(--prepr-gray-300);border-right:1px solid var(--prepr-gray-300);border-bottom:1px solid var(--prepr-gray-300);-ms-overflow-style:none;scrollbar-width:none;background:#fff;border-radius:0 0 6px 6px;width:240px;max-height:300px;margin:0;padding:0;list-style:none;position:absolute;top:100%;left:0;overflow-y:auto;box-shadow:0 20px 25px -5px #0000001a,0 8px 10px -6px #0000001a}.prepr-options::-webkit-scrollbar{display:none}.prepr-options[hidden]{display:none}.prepr-option-search{background:#fff;padding:.5rem;position:-webkit-sticky;position:sticky;top:0}.prepr-segment-search{border:1px solid var(--prepr-gray-300);border-radius:var(--prepr-radius);width:100%;height:2rem;font:inherit;color:var(--prepr-gray-900);box-sizing:border-box;background:#fff;padding:0 .5rem;font-size:.875rem}.prepr-segment-search::placeholder{color:var(--prepr-gray-400)}.prepr-segment-search:focus{border-color:var(--prepr-gray-400);outline:none}.prepr-option{cursor:pointer;width:100%;color:var(--prepr-gray-900);background:#fff;align-items:center;padding:.5rem 1rem;font-size:.875rem;font-weight:500;display:flex}.prepr-option:hover{background:var(--prepr-gray-100)}.prepr-option[aria-selected=true]{background:var(--prepr-indigo-50);color:var(--prepr-indigo-700)}.prepr-option-label{white-space:nowrap;text-overflow:ellipsis;text-align:left;width:100%;margin-right:auto;overflow:hidden}.prepr-reset{background:var(--prepr-secondary-400);color:#fff;cursor:pointer;border:none;border-radius:6px;justify-content:center;align-items:center;gap:.5rem;width:100%;height:2.5rem;padding:.5rem .75rem;font-size:.875rem;font-weight:500;display:flex}.prepr-reset:hover:not(:disabled){background:var(--prepr-secondary-500)}.prepr-reset:disabled{background:var(--prepr-gray-400);color:var(--prepr-gray-500);cursor:not-allowed}@media (min-width:768px){.prepr-reset{width:108px}}.prepr-indicators{z-index:var(--prepr-z-index);gap:.75rem;display:flex;position:fixed;bottom:1.5rem;right:1.5rem}.prepr-status-pill{background:var(--prepr-primary);color:#fff;cursor:pointer;border:none;border-radius:9999px;align-items:center;gap:.5rem;padding:.5rem 1rem;font-size:.75rem;font-weight:500;transition:background-color .15s;display:flex;box-shadow:0 10px 15px -3px #0000001a,0 4px 6px -4px #0000001a}.prepr-status-pill[data-interactive=false]{cursor:default;opacity:1;color:#fff}.prepr-status-pill[data-interactive=true]:hover{background:var(--prepr-primary-800)}.prepr-status-pill[hidden]{display:none}.prepr-status-viewing{color:#fff9;font-size:10px}.prepr-status-segment{text-overflow:ellipsis;white-space:nowrap;max-width:120px;display:inline-block;overflow:hidden}.prepr-status-variant{text-overflow:ellipsis;white-space:nowrap;background:#fff3;border-radius:4px;max-width:80px;padding:0 .5rem;display:inline-block;overflow:hidden}.prepr-status-x{color:#fff;width:.75rem;height:.75rem}.prepr-close-edit-pill{background:var(--prepr-primary-50);color:var(--prepr-gray-800);cursor:pointer;border:none;border-radius:9999px;align-items:center;gap:.5rem;padding:.5rem 1rem;font-size:.75rem;font-weight:500;transition:background-color .15s;display:flex;box-shadow:0 10px 15px -3px #0000001a,0 4px 6px -4px #0000001a}.prepr-close-edit-pill:hover{background:var(--prepr-primary-100)}.prepr-close-edit-pill[hidden]{display:none}.prepr-close-edit-x{width:1rem;height:1rem;color:var(--prepr-gray-500);transition:color .15s}.prepr-close-edit-pill:hover .prepr-close-edit-x{color:var(--prepr-gray-700)}.prepr-tip{z-index:calc(var(--prepr-z-index) + 2);overflow-wrap:break-word;background:var(--prepr-gray-900);color:#fff;pointer-events:none;border-radius:6px;width:auto;max-width:min(420px,100vw - 16px);padding:.5rem .75rem;font-size:.75rem;font-weight:500;position:fixed;box-shadow:0 20px 25px -5px #0000001a,0 8px 10px -6px #0000001a}.prepr-tip-arrow{border-left:6px solid #0000;border-right:6px solid #0000;border-top:6px solid var(--prepr-gray-900);width:0;height:0;position:absolute;top:100%;transform:translate(-50%)}[hidden]{display:none!important}@keyframes prepr-fade-in{0%{opacity:0}to{opacity:1}}";
1927
+ var TOOLBAR_CSS = ":host{--prepr-primary:#4338ca;--prepr-bg:#eef2ff;--prepr-text:#1f2937;--prepr-radius:8px;--prepr-shadow:0px 0px 40px 0px #1f29373d;--prepr-z-index:10000;--prepr-primary-50:#eef2ff;--prepr-primary-100:#e0e7ff;--prepr-primary-800:#3730a3;--prepr-indigo-600:#4f46e5;--prepr-indigo-50:#eef2ff;--prepr-indigo-700:#4338ca;--prepr-secondary-400:#fb923c;--prepr-secondary-500:#f97316;--prepr-gray-100:#f3f4f6;--prepr-gray-200:#e5e7eb;--prepr-gray-300:#d1d5db;--prepr-gray-400:#9ca3af;--prepr-gray-500:#6b7280;--prepr-gray-700:#374151;--prepr-gray-800:#1f2937;--prepr-gray-900:#111827;--prepr-shadow-sm:0px 0px 12px 0px #c3c3c3cc;color:var(--prepr-text);box-sizing:border-box;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica,Arial,sans-serif;font-size:14px;line-height:1.5}:host *,:host :before,:host :after,*,:before,:after{box-sizing:border-box}button{font-family:inherit;font-size:inherit;margin:0}.prepr-regular-text{font-size:.875rem;font-weight:500;line-height:1.5}.prepr-backdrop{z-index:calc(var(--prepr-z-index) - 1);opacity:0;pointer-events:none;background:#0000001a;transition:opacity .2s ease-in-out;position:fixed;top:0;bottom:0;left:0;right:0}.prepr-backdrop[data-open=true]{opacity:1;pointer-events:auto;animation:.2s ease-in-out prepr-fade-in}.prepr-container{z-index:var(--prepr-z-index);justify-content:flex-end;align-items:center;display:flex;position:fixed;top:calc(50% - 64px);right:0}.prepr-toggle-holder{padding:.5rem}.prepr-toggle{cursor:pointer;background:var(--prepr-primary);color:#fff;border:none;border-radius:9999px;justify-content:center;align-items:center;width:2.25rem;height:2.25rem;display:flex}.prepr-toggle>span{display:flex}.prepr-toggle svg{color:#fff}.prepr-panel{z-index:var(--prepr-z-index);opacity:0;pointer-events:none;justify-content:flex-end;padding:.75rem;transition:opacity .2s ease-in-out;display:flex;position:fixed;top:50%;right:0;transform:translateY(-50%)}.prepr-panel[data-open=true]{opacity:1;pointer-events:auto;animation:.2s ease-in-out prepr-fade-in}.prepr-content{box-shadow:var(--prepr-shadow);border-radius:var(--prepr-radius);background:var(--prepr-bg);flex-direction:column;gap:2.5rem;width:100%;padding:1.5rem;display:flex}@media (min-width:640px){.prepr-panel{margin-right:4rem;padding:0}.prepr-content{width:502px;padding:2.5rem}}.prepr-header{justify-content:space-between;align-items:center;gap:.5rem;display:flex}.prepr-header-left{align-items:flex-start;gap:.75rem;display:flex}.prepr-badge{background:var(--prepr-primary-100);height:1.5rem;color:var(--prepr-primary);border-radius:4px;align-items:center;padding:.25rem .75rem;font-size:.875rem;display:flex}.prepr-header-close{cursor:pointer;background:var(--prepr-primary-100);width:2rem;height:2rem;color:var(--prepr-gray-700);border:none;border-radius:6px;justify-content:center;align-items:center;display:flex}.prepr-section{flex-direction:column;gap:.5rem;display:flex}.prepr-section-label{color:var(--prepr-gray-400);font-size:.875rem}.prepr-row{flex-wrap:wrap;justify-content:space-between;align-items:center;gap:.5rem 1.5rem;display:flex}.prepr-row-title{color:var(--prepr-gray-800);margin:0;font-size:1rem;font-weight:600}.prepr-radiogroup{border-radius:var(--prepr-radius);border:1px solid var(--prepr-gray-300);background:#fff;align-items:center;gap:.25rem;height:2.5rem;padding:.25rem;display:flex}.prepr-radiogroup[data-disabled=true]{cursor:not-allowed;opacity:.5}.prepr-radiogroup[data-disabled=true] .prepr-radio{pointer-events:none}.prepr-radio{cursor:pointer;white-space:nowrap;text-align:center;height:2rem;color:var(--prepr-gray-900);background:0 0;border:none;border-radius:6px;justify-content:center;align-items:center;padding:.5rem 1.125rem;font-size:.875rem;font-weight:500;transition:all .2s ease-in-out;display:flex}.prepr-radio[data-checked=true]{background:var(--prepr-indigo-600);color:#fff;box-shadow:var(--prepr-shadow-sm)}.prepr-radio[data-checked=true][data-off=true]{background:var(--prepr-gray-800);color:#fff}.prepr-radio-variant{width:82px}.prepr-listbox{position:relative}.prepr-segment-button{border-radius:var(--prepr-radius);border:1px solid var(--prepr-gray-300);width:240px;height:2.5rem;color:var(--prepr-gray-500);cursor:pointer;background:#fff;flex-shrink:0;align-items:center;gap:.5rem;padding:0 .5rem;font-size:.875rem;font-weight:500;display:flex;overflow:hidden}@media (min-width:768px){.prepr-segment-button{padding:0 1rem}}.prepr-segment-button:disabled{cursor:not-allowed;background:var(--prepr-gray-200);color:var(--prepr-gray-400)}.prepr-segment-button[aria-expanded=true]{border-bottom-color:#fff;border-bottom-right-radius:0;border-bottom-left-radius:0}.prepr-segment-label{white-space:nowrap;text-overflow:ellipsis;text-align:left;width:100%;margin-right:auto;overflow:hidden}.prepr-segment-caret{color:var(--prepr-gray-800);display:flex}.prepr-options{z-index:calc(var(--prepr-z-index) + 1);border-left:1px solid var(--prepr-gray-300);border-right:1px solid var(--prepr-gray-300);border-bottom:1px solid var(--prepr-gray-300);-ms-overflow-style:none;scrollbar-width:none;background:#fff;border-radius:0 0 6px 6px;width:240px;max-height:300px;margin:0;padding:0;list-style:none;position:absolute;top:100%;left:0;overflow-y:auto;box-shadow:0 20px 25px -5px #0000001a,0 8px 10px -6px #0000001a}.prepr-options::-webkit-scrollbar{display:none}.prepr-options[hidden]{display:none}.prepr-option-search{background:#fff;padding:.5rem;position:sticky;top:0}.prepr-segment-search{border:1px solid var(--prepr-gray-300);border-radius:var(--prepr-radius);width:100%;height:2rem;font:inherit;color:var(--prepr-gray-900);box-sizing:border-box;background:#fff;padding:0 .5rem;font-size:.875rem}@media (pointer:coarse){.prepr-segment-search{font-size:16px}}.prepr-segment-search::placeholder{color:var(--prepr-gray-400)}.prepr-segment-search:focus{border-color:var(--prepr-gray-400);outline:none}.prepr-option{cursor:pointer;width:100%;color:var(--prepr-gray-900);background:#fff;align-items:center;padding:.5rem 1rem;font-size:.875rem;font-weight:500;display:flex}.prepr-option:hover{background:var(--prepr-gray-100)}.prepr-option[aria-selected=true]{background:var(--prepr-indigo-50);color:var(--prepr-indigo-700)}.prepr-option-label{white-space:nowrap;text-overflow:ellipsis;text-align:left;width:100%;margin-right:auto;overflow:hidden}.prepr-reset{background:var(--prepr-secondary-400);color:#fff;cursor:pointer;border:none;border-radius:6px;justify-content:center;align-items:center;gap:.5rem;width:100%;height:2.5rem;padding:.5rem .75rem;font-size:.875rem;font-weight:500;display:flex}.prepr-reset:hover:not(:disabled){background:var(--prepr-secondary-500)}.prepr-reset:disabled{background:var(--prepr-gray-400);color:var(--prepr-gray-500);cursor:not-allowed}@media (min-width:768px){.prepr-reset{width:108px}}.prepr-indicators{z-index:var(--prepr-z-index);gap:.75rem;display:flex;position:fixed;bottom:1.5rem;right:1.5rem}.prepr-status-pill{background:var(--prepr-primary);color:#fff;cursor:pointer;border:none;border-radius:9999px;align-items:center;gap:.5rem;padding:.5rem 1rem;font-size:.75rem;font-weight:500;transition:background-color .15s;display:flex;box-shadow:0 10px 15px -3px #0000001a,0 4px 6px -4px #0000001a}.prepr-status-pill[data-interactive=false]{cursor:default;opacity:1;color:#fff}.prepr-status-pill[data-interactive=true]:hover{background:var(--prepr-primary-800)}.prepr-status-pill[hidden]{display:none}.prepr-status-viewing{color:#fff9;font-size:10px}.prepr-status-segment{text-overflow:ellipsis;white-space:nowrap;max-width:120px;display:inline-block;overflow:hidden}.prepr-status-variant{text-overflow:ellipsis;white-space:nowrap;background:#fff3;border-radius:4px;max-width:80px;padding:0 .5rem;display:inline-block;overflow:hidden}.prepr-status-x{color:#fff;width:.75rem;height:.75rem}.prepr-close-edit-pill{background:var(--prepr-primary-50);color:var(--prepr-gray-800);cursor:pointer;border:none;border-radius:9999px;align-items:center;gap:.5rem;padding:.5rem 1rem;font-size:.75rem;font-weight:500;transition:background-color .15s;display:flex;box-shadow:0 10px 15px -3px #0000001a,0 4px 6px -4px #0000001a}.prepr-close-edit-pill:hover{background:var(--prepr-primary-100)}.prepr-close-edit-pill[hidden]{display:none}.prepr-close-edit-x{width:1rem;height:1rem;color:var(--prepr-gray-500);transition:color .15s}.prepr-close-edit-pill:hover .prepr-close-edit-x{color:var(--prepr-gray-700)}.prepr-tip{z-index:calc(var(--prepr-z-index) + 2);overflow-wrap:break-word;background:var(--prepr-gray-900);color:#fff;pointer-events:none;border-radius:6px;width:auto;max-width:min(420px,100vw - 16px);padding:.5rem .75rem;font-size:.75rem;font-weight:500;position:fixed;box-shadow:0 20px 25px -5px #0000001a,0 8px 10px -6px #0000001a}.prepr-tip-arrow{border-left:6px solid #0000;border-right:6px solid #0000;border-top:6px solid var(--prepr-gray-900);width:0;height:0;position:absolute;top:100%;transform:translate(-50%)}[hidden]{display:none!important}@keyframes prepr-fade-in{0%{opacity:0}to{opacity:1}}";
1502
1928
 
1503
- // ../../node_modules/.pnpm/preact@10.29.7/node_modules/preact/jsx-runtime/dist/jsxRuntime.mjs
1929
+ // ../../node_modules/.pnpm/preact@10.29.8/node_modules/preact/jsx-runtime/dist/jsxRuntime.mjs
1504
1930
  var f2 = 0;
1505
1931
  function u2(e2, t3, n2, o2, i2, u3) {
1506
1932
  t3 || (t3 = {});
@@ -2193,6 +2619,764 @@ function definePreprToolbar() {
2193
2619
  }
2194
2620
  }
2195
2621
 
2622
+ // src/core/visual-editor/asset-images.ts
2623
+ var MAX_URL_LENGTH = 4096;
2624
+ var MIN_KEY_SEGMENTS = 2;
2625
+ var TRANSFORM_OPTION = /^[a-z]{1,3}_[\w.-]*$/i;
2626
+ var FORMAT_PLACEHOLDER = "{format}";
2627
+ var PROXY_PARAMS = ["url", "href", "src", "image"];
2628
+ var ABSOLUTE_HTTP = /^https?:\/\//i;
2629
+ var SOURCE_URL_ATTRIBUTES = ["src", "data-src"];
2630
+ var SOURCE_SET_ATTRIBUTES = ["srcset", "data-srcset"];
2631
+ var ASSET_ATTR = "data-prepr-asset";
2632
+ var ASSET_SOURCE_ATTRIBUTES = [
2633
+ ...SOURCE_URL_ATTRIBUTES,
2634
+ ...SOURCE_SET_ATTRIBUTES
2635
+ ];
2636
+ var TAG_ATTRIBUTES = [
2637
+ "data-prepr-encoded",
2638
+ "data-prepr-id",
2639
+ "data-prepr-field",
2640
+ "data-prepr-entry-id",
2641
+ ASSET_ATTR
2642
+ ];
2643
+ function decodeSegment(segment) {
2644
+ try {
2645
+ return decodeURIComponent(segment);
2646
+ } catch {
2647
+ return segment;
2648
+ }
2649
+ }
2650
+ function isTransformSegment(segment) {
2651
+ return segment === FORMAT_PLACEHOLDER || segment.split(",").every((option) => TRANSFORM_OPTION.test(option));
2652
+ }
2653
+ function assetKey(url, depth = 0) {
2654
+ if (!url || url.length > MAX_URL_LENGTH) return null;
2655
+ let parsed;
2656
+ try {
2657
+ parsed = new URL(url, "https://base.invalid");
2658
+ } catch {
2659
+ return null;
2660
+ }
2661
+ if (parsed.protocol !== "https:" && parsed.protocol !== "http:") return null;
2662
+ if (depth === 0) {
2663
+ for (const name of PROXY_PARAMS) {
2664
+ const inner = parsed.searchParams.get(name);
2665
+ if (inner && ABSOLUTE_HTTP.test(inner)) return assetKey(inner, 1);
2666
+ }
2667
+ }
2668
+ const segments = parsed.pathname.split("/").filter(Boolean).map(decodeSegment);
2669
+ const file = segments.pop();
2670
+ if (!file) return null;
2671
+ const kept = segments.filter((segment) => !isTransformSegment(segment));
2672
+ kept.push(file);
2673
+ return kept.length >= MIN_KEY_SEGMENTS ? kept.join("/") : null;
2674
+ }
2675
+ function srcsetUrls(srcset) {
2676
+ const urls = [];
2677
+ let expectUrl = true;
2678
+ for (const token of srcset.split(/\s+/)) {
2679
+ if (!token) continue;
2680
+ if (!expectUrl) {
2681
+ const comma = token.indexOf(",");
2682
+ if (comma === -1) continue;
2683
+ expectUrl = true;
2684
+ const rest = token.slice(comma + 1);
2685
+ if (!rest) continue;
2686
+ urls.push(rest.replace(/,+$/, ""));
2687
+ expectUrl = rest.endsWith(",");
2688
+ continue;
2689
+ }
2690
+ const url = token.replace(/^,+|,+$/g, "");
2691
+ if (url) urls.push(url);
2692
+ expectUrl = token.endsWith(",");
2693
+ }
2694
+ return urls;
2695
+ }
2696
+ function createAssetIndex(assets) {
2697
+ const index = /* @__PURE__ */ new Map();
2698
+ for (const asset of assets) {
2699
+ const key = assetKey(asset.key);
2700
+ if (key && !index.has(key)) index.set(key, asset);
2701
+ }
2702
+ return index;
2703
+ }
2704
+ function urlsOf(img) {
2705
+ const urls = [];
2706
+ const collect = (el) => {
2707
+ for (const name of SOURCE_URL_ATTRIBUTES) {
2708
+ const value = el.getAttribute(name);
2709
+ if (value) urls.push(value);
2710
+ }
2711
+ for (const name of SOURCE_SET_ATTRIBUTES) {
2712
+ const value = el.getAttribute(name);
2713
+ if (value) urls.push(...srcsetUrls(value));
2714
+ }
2715
+ };
2716
+ collect(img);
2717
+ const picture = img.parentElement;
2718
+ if (picture?.tagName === "PICTURE") {
2719
+ for (const source of picture.children) {
2720
+ if (source.tagName === "SOURCE") collect(source);
2721
+ }
2722
+ }
2723
+ return urls;
2724
+ }
2725
+ function matchAsset(img, index, keys) {
2726
+ for (const url of urlsOf(img)) {
2727
+ let key = keys.get(url);
2728
+ if (key === void 0) {
2729
+ key = assetKey(url);
2730
+ keys.set(url, key);
2731
+ }
2732
+ const asset = key ? index.get(key) : void 0;
2733
+ if (asset) return asset;
2734
+ }
2735
+ return void 0;
2736
+ }
2737
+ function setIfChanged(el, name, value) {
2738
+ if (value === null) {
2739
+ if (el.hasAttribute(name)) el.removeAttribute(name);
2740
+ } else if (el.getAttribute(name) !== value) {
2741
+ el.setAttribute(name, value);
2742
+ }
2743
+ }
2744
+ function untagAssetImage(img) {
2745
+ for (const name of TAG_ATTRIBUTES) img.removeAttribute(name);
2746
+ }
2747
+ var TEXT_FIELD_SELECTOR = `[data-prepr-encoded]:not([${ASSET_ATTR}])`;
2748
+ function besideOtherItem(img, isCurrentItem) {
2749
+ for (let block = img.parentElement; block && block !== document.body; block = block.parentElement) {
2750
+ const texts = block.querySelectorAll(TEXT_FIELD_SELECTOR);
2751
+ if (texts.length === 0) continue;
2752
+ for (const text of texts) {
2753
+ if (isCurrentItem(text)) return false;
2754
+ }
2755
+ return true;
2756
+ }
2757
+ return false;
2758
+ }
2759
+ function tagAssetImages(index, itemId, keys, options = {}) {
2760
+ const { isCurrentItem } = options;
2761
+ for (const img of document.getElementsByTagName("img")) {
2762
+ const ours = img.hasAttribute(ASSET_ATTR);
2763
+ if (!ours && img.hasAttribute("data-prepr-encoded")) continue;
2764
+ const match = index.size ? matchAsset(img, index, keys) : void 0;
2765
+ const asset = match && isCurrentItem && besideOtherItem(img, isCurrentItem) ? void 0 : match;
2766
+ if (!asset) {
2767
+ if (ours) untagAssetImage(img);
2768
+ continue;
2769
+ }
2770
+ setIfChanged(img, "data-prepr-encoded", "");
2771
+ setIfChanged(img, ASSET_ATTR, "");
2772
+ setIfChanged(img, "data-prepr-id", itemId);
2773
+ setIfChanged(img, "data-prepr-field", asset.field);
2774
+ setIfChanged(img, "data-prepr-entry-id", asset.entryId ?? null);
2775
+ }
2776
+ }
2777
+ function untagAssetImages() {
2778
+ document.querySelectorAll(`img[${ASSET_ATTR}]`).forEach(untagAssetImage);
2779
+ }
2780
+
2781
+ // src/core/visual-editor/navigation-guard.ts
2782
+ var debug7 = createScopedLogger("navigation-guard");
2783
+ var trimSlash = (path) => path.length > 1 && path.endsWith("/") ? path.slice(0, -1) : path;
2784
+ function resolve(url) {
2785
+ try {
2786
+ return new URL(url, document.baseURI);
2787
+ } catch {
2788
+ return null;
2789
+ }
2790
+ }
2791
+ function leavesPage(url) {
2792
+ if (url.protocol !== "http:" && url.protocol !== "https:") return false;
2793
+ const here = window.location;
2794
+ return url.origin !== here.origin || trimSlash(url.pathname) !== trimSlash(here.pathname);
2795
+ }
2796
+ function leavingLinkHref(target) {
2797
+ if (!(target instanceof Element)) return null;
2798
+ const link = target.closest("a[href]");
2799
+ if (!link || link.hasAttribute("download")) return null;
2800
+ const linkTarget = link.getAttribute("target");
2801
+ if (linkTarget && linkTarget !== "_self") return null;
2802
+ const href = link.getAttribute("href") ?? "";
2803
+ if (href.startsWith("#")) return null;
2804
+ const url = resolve(href);
2805
+ return url && leavesPage(url) ? url.href : null;
2806
+ }
2807
+ function submitterAttr(submitter, attr) {
2808
+ if (!submitter?.hasAttribute(attr)) return null;
2809
+ if (attr === "formaction") return submitter.formAction;
2810
+ if (attr === "formmethod") return submitter.formMethod;
2811
+ return submitter.formTarget;
2812
+ }
2813
+ function submissionUrl(form, submitter) {
2814
+ const method = (submitterAttr(submitter, "formmethod") ?? form.method).toLowerCase();
2815
+ if (method === "dialog") return null;
2816
+ const target = submitterAttr(submitter, "formtarget") ?? form.target;
2817
+ if (target && target !== "_self") return null;
2818
+ const url = resolve(
2819
+ submitterAttr(submitter, "formaction") ?? (form.action || location.href)
2820
+ );
2821
+ if (!url) return null;
2822
+ if (method === "get") {
2823
+ let data;
2824
+ try {
2825
+ data = new FormData(form, submitter);
2826
+ } catch {
2827
+ data = new FormData(form);
2828
+ }
2829
+ const query = new URLSearchParams();
2830
+ data.forEach((value, key) => {
2831
+ query.append(key, typeof value === "string" ? value : value.name);
2832
+ });
2833
+ url.search = query.toString();
2834
+ }
2835
+ return url;
2836
+ }
2837
+ function createNavigationGuard(opts) {
2838
+ let active = false;
2839
+ let permitted = null;
2840
+ let pendingForm = null;
2841
+ let originalPush = null;
2842
+ let guardedPush = null;
2843
+ const isPermitted = (url) => permitted === url.href;
2844
+ const guarding = () => active && opts.showsEditedContent();
2845
+ const report = (href) => {
2846
+ debug7.log("blocked navigation to", href);
2847
+ opts.onBlocked(href);
2848
+ };
2849
+ const onSubmit = (e2) => {
2850
+ if (e2.defaultPrevented || !(e2.target instanceof HTMLFormElement)) return;
2851
+ if (!guarding()) return;
2852
+ const submitter = e2.submitter ?? null;
2853
+ const url = submissionUrl(e2.target, submitter);
2854
+ if (!url || !leavesPage(url) || isPermitted(url)) return;
2855
+ e2.preventDefault();
2856
+ pendingForm = { form: e2.target, submitter, href: url.href };
2857
+ report(url.href);
2858
+ };
2859
+ const onNavigate = (event) => {
2860
+ const e2 = event;
2861
+ if (e2.navigationType === "traverse" || e2.navigationType === "reload") {
2862
+ return;
2863
+ }
2864
+ if (!e2.cancelable || e2.hashChange || e2.downloadRequest !== null) return;
2865
+ if (!guarding()) return;
2866
+ if (e2.navigationType === "replace" && e2.destination.sameDocument) return;
2867
+ const url = resolve(e2.destination.url);
2868
+ if (!url || !leavesPage(url) || isPermitted(url)) return;
2869
+ e2.preventDefault();
2870
+ report(url.href);
2871
+ if (e2.destination.sameDocument) window.location.reload();
2872
+ };
2873
+ const navigation = () => window.navigation;
2874
+ function installPushGuard() {
2875
+ const original = history.pushState;
2876
+ const guard = function(data, unused, url) {
2877
+ if (guarding() && guardedPush === guard && url != null) {
2878
+ const target = resolve(url);
2879
+ if (target && leavesPage(target) && !isPermitted(target)) {
2880
+ report(target.href);
2881
+ window.location.reload();
2882
+ return;
2883
+ }
2884
+ }
2885
+ return original.call(this, data, unused, url);
2886
+ };
2887
+ originalPush = original;
2888
+ guardedPush = guard;
2889
+ history.pushState = guard;
2890
+ }
2891
+ function uninstallPushGuard() {
2892
+ if (history.pushState === guardedPush && originalPush) {
2893
+ history.pushState = originalPush;
2894
+ }
2895
+ originalPush = null;
2896
+ guardedPush = null;
2897
+ }
2898
+ return {
2899
+ start() {
2900
+ if (active) return;
2901
+ active = true;
2902
+ installPushGuard();
2903
+ window.addEventListener("submit", onSubmit);
2904
+ navigation()?.addEventListener("navigate", onNavigate);
2905
+ },
2906
+ stop() {
2907
+ if (!active) return;
2908
+ active = false;
2909
+ uninstallPushGuard();
2910
+ window.removeEventListener("submit", onSubmit);
2911
+ navigation()?.removeEventListener("navigate", onNavigate);
2912
+ pendingForm = null;
2913
+ },
2914
+ isActive: guarding,
2915
+ block(href) {
2916
+ report(href);
2917
+ },
2918
+ permit(href) {
2919
+ permitted = resolve(href)?.href ?? null;
2920
+ },
2921
+ allow(href) {
2922
+ const url = resolve(href);
2923
+ if (!url || url.protocol !== "http:" && url.protocol !== "https:") {
2924
+ debug7.warn("ignored navigation to unsupported URL");
2925
+ return;
2926
+ }
2927
+ permitted = url.href;
2928
+ const form = pendingForm;
2929
+ pendingForm = null;
2930
+ if (form && form.href === url.href && form.form.isConnected) {
2931
+ try {
2932
+ form.form.requestSubmit(form.submitter ?? void 0);
2933
+ } catch {
2934
+ form.form.requestSubmit();
2935
+ }
2936
+ return;
2937
+ }
2938
+ window.location.assign(url.href);
2939
+ }
2940
+ };
2941
+ }
2942
+
2943
+ // src/core/visual-editor/index.ts
2944
+ var debug8 = createScopedLogger("visual-editor");
2945
+ var ENCODED_SELECTOR2 = "[data-prepr-encoded]";
2946
+ var REFERENCE_ATTR = "data-prepr-reference";
2947
+ var REFERENCE_SELECTOR = `[${REFERENCE_ATTR}]`;
2948
+ var OTHER_ITEM_ATTR = "data-prepr-other-item";
2949
+ var OTHER_ITEM_SELECTOR = `[${OTHER_ITEM_ATTR}="on"],[${OTHER_ITEM_ATTR}="off"]`;
2950
+ var CHROME_SELECTOR = "header, footer, nav, [role=banner], [role=contentinfo], [role=navigation]";
2951
+ var SECTIONING_SELECTOR = "article, aside, main, nav, section";
2952
+ var PLAIN_HEADER_FOOTER = "header:not([role]), footer:not([role])";
2953
+ var CURSOR_STYLE_MARKER = "data-prepr-visual-editor";
2954
+ var CURSOR_STYLES = `${ENCODED_SELECTOR2}{cursor:pointer}html[data-prepr-ve-suspended] ${ENCODED_SELECTOR2}{cursor:auto}`;
2955
+ var SUSPENDED_ATTR = "data-prepr-ve-suspended";
2956
+ var FIND_RETRY_MS = 150;
2957
+ var FIND_RETRIES = 6;
2958
+ var MAX_CACHED_KEYS = 2e3;
2959
+ var LAYOUT_MUTATIONS = {
2960
+ childList: true,
2961
+ subtree: true,
2962
+ characterData: true,
2963
+ attributes: true,
2964
+ attributeFilter: [
2965
+ "class",
2966
+ "style",
2967
+ "hidden",
2968
+ "open",
2969
+ "data-prepr-encoded",
2970
+ // An image swapped by a carousel or lazy loader can show another asset.
2971
+ ...ASSET_SOURCE_ATTRIBUTES
2972
+ ]
2973
+ };
2974
+ function scrollIntoFrame(el) {
2975
+ const rect = el.getBoundingClientRect();
2976
+ const offset = Math.max(0, (window.innerHeight - rect.height) / 2);
2977
+ window.scrollTo({
2978
+ top: Math.max(0, window.scrollY + rect.top - offset),
2979
+ behavior: "smooth"
2980
+ });
2981
+ }
2982
+ var isVisible = (el) => typeof el.checkVisibility === "function" ? el.checkVisibility() : el.getClientRects().length > 0;
2983
+ function createVisualEditor(opts) {
2984
+ const overlay = new VisualEditorOverlay();
2985
+ let running = false;
2986
+ let currentItemId = null;
2987
+ let locale = opts.locale ?? "en";
2988
+ let suspended = false;
2989
+ let pointerFrame = null;
2990
+ let lastPointerTarget = null;
2991
+ let lastPointerX = 0;
2992
+ let lastPointerY = 0;
2993
+ let mutationFrame = null;
2994
+ let mutationObserver = null;
2995
+ let selection = null;
2996
+ let findTimeout = null;
2997
+ let settlePendingFind = null;
2998
+ let guardEnabled = false;
2999
+ let assetIndex = /* @__PURE__ */ new Map();
3000
+ const assetKeys = /* @__PURE__ */ new Map();
3001
+ const guard = createNavigationGuard({
3002
+ onBlocked: (href) => opts.onNavigationBlocked?.(href),
3003
+ showsEditedContent: () => showsEditedContent()
3004
+ });
3005
+ const encodedFrom = (node) => node instanceof Element ? node.closest(ENCODED_SELECTOR2) : null;
3006
+ const labelFor = (el) => fieldLabel(el.getAttribute("data-prepr-field"), locale);
3007
+ let references = null;
3008
+ let ownComponents = /* @__PURE__ */ new Set();
3009
+ const isCurrentItem = (el) => {
3010
+ if (currentItemId === null) return true;
3011
+ const id = el.getAttribute("data-prepr-id");
3012
+ return !id || id === currentItemId || ownComponents.has(id);
3013
+ };
3014
+ const showsEditedContent = () => {
3015
+ for (const el of document.querySelectorAll(ENCODED_SELECTOR2)) {
3016
+ if (isCurrentItem(el) && isVisible(el)) return true;
3017
+ }
3018
+ return false;
3019
+ };
3020
+ const linkedIdOf = (el) => {
3021
+ if (!references) return null;
3022
+ const id = el.getAttribute("data-prepr-id") ?? "";
3023
+ if (references.has(id)) return id;
3024
+ let container = el.closest(REFERENCE_SELECTOR);
3025
+ while (container) {
3026
+ const linked = container.getAttribute(REFERENCE_ATTR) ?? "";
3027
+ if (references.has(linked)) return linked;
3028
+ container = container.parentElement?.closest(REFERENCE_SELECTOR) ?? null;
3029
+ }
3030
+ return null;
3031
+ };
3032
+ const inSiteChrome = (el) => {
3033
+ let chrome = el.closest(CHROME_SELECTOR);
3034
+ while (chrome) {
3035
+ if (!chrome.matches(PLAIN_HEADER_FOOTER) || !chrome.parentElement?.closest(SECTIONING_SELECTOR)) {
3036
+ return true;
3037
+ }
3038
+ chrome = chrome.parentElement?.closest(CHROME_SELECTOR) ?? null;
3039
+ }
3040
+ return false;
3041
+ };
3042
+ const showsOtherItem = (el) => {
3043
+ const choice = el.closest(OTHER_ITEM_SELECTOR);
3044
+ if (choice) return choice.getAttribute(OTHER_ITEM_ATTR) === "on";
3045
+ return !inSiteChrome(el);
3046
+ };
3047
+ const itemKind = (el) => {
3048
+ if (isCurrentItem(el)) return "current";
3049
+ if (!references) return "other";
3050
+ if (linkedIdOf(el)) return "referenced";
3051
+ return showsOtherItem(el) ? "other" : null;
3052
+ };
3053
+ const referenceFieldOf = (el) => {
3054
+ const linked = linkedIdOf(el);
3055
+ return linked ? references?.get(linked) : void 0;
3056
+ };
3057
+ const hoverLabel = (el, kind) => {
3058
+ if (kind === "other") {
3059
+ return `${labelFor(el)} \xB7 ${t("visualEditor.otherItem", locale)}`;
3060
+ }
3061
+ if (kind === "referenced") {
3062
+ const via = referenceFieldOf(el);
3063
+ const prefix = via ? `${fieldLabel(via, locale)} \u203A ` : "";
3064
+ return `${prefix}${labelFor(el)} \xB7 ${t("visualEditor.linkedItem", locale)}`;
3065
+ }
3066
+ return labelFor(el);
3067
+ };
3068
+ const setSuspended = (value) => {
3069
+ if (suspended === value) return;
3070
+ suspended = value;
3071
+ document.documentElement.toggleAttribute(SUSPENDED_ATTR, value);
3072
+ if (value) overlay.setHover(null, "");
3073
+ };
3074
+ const updateHover = () => {
3075
+ pointerFrame = null;
3076
+ overlay.schedule();
3077
+ if (suspended) return;
3078
+ const encoded = encodedFrom(lastPointerTarget);
3079
+ const target = encoded === overlay.getSelectedTarget() ? null : encoded;
3080
+ if (!target) {
3081
+ overlay.setHover(null, "");
3082
+ return;
3083
+ }
3084
+ const kind = itemKind(target);
3085
+ if (!kind) {
3086
+ overlay.setHover(null, "");
3087
+ return;
3088
+ }
3089
+ overlay.setHover(target, hoverLabel(target, kind), { kind });
3090
+ };
3091
+ const handlePointerMove = (e2) => {
3092
+ setSuspended(e2.altKey);
3093
+ lastPointerTarget = e2.target;
3094
+ lastPointerX = e2.clientX;
3095
+ lastPointerY = e2.clientY;
3096
+ if (pointerFrame === null) {
3097
+ pointerFrame = requestAnimationFrame(updateHover);
3098
+ }
3099
+ };
3100
+ const handlePointerLeave = () => {
3101
+ lastPointerTarget = null;
3102
+ overlay.setHover(null, "");
3103
+ };
3104
+ const handleClick = (e2) => {
3105
+ if (e2.altKey) {
3106
+ const href = leavingLinkHref(e2.target);
3107
+ if (href && guard.isActive()) guard.permit(href);
3108
+ return;
3109
+ }
3110
+ const encoded = encodedFrom(e2.target);
3111
+ const kind = encoded ? itemKind(encoded) : null;
3112
+ if (!encoded || !kind) {
3113
+ clearSelection();
3114
+ if (e2.ctrlKey || e2.metaKey || e2.shiftKey || !showsEditedContent()) return;
3115
+ const href = leavingLinkHref(e2.target);
3116
+ if (!href) return;
3117
+ e2.preventDefault();
3118
+ if (guard.isActive()) {
3119
+ e2.stopPropagation();
3120
+ guard.block(href);
3121
+ }
3122
+ return;
3123
+ }
3124
+ if (kind === "other" && (e2.ctrlKey || e2.metaKey || e2.shiftKey)) return;
3125
+ e2.preventDefault();
3126
+ e2.stopPropagation();
3127
+ const item = listItemIn(encoded, e2.target);
3128
+ if (kind === "current")
3129
+ select(item?.element ?? encoded, encoded, item?.index);
3130
+ const referenceField = kind === "referenced" ? referenceFieldOf(encoded) : void 0;
3131
+ const linkedId = kind === "referenced" ? linkedIdOf(encoded) : null;
3132
+ const linkHref = kind === "other" ? leavingLinkHref(e2.target) : null;
3133
+ opts.onEdit({
3134
+ ...readEditPayload(encoded),
3135
+ ...linkedId && { id: linkedId },
3136
+ ...item && { block: item.index },
3137
+ ...referenceField && { referenceField },
3138
+ ...linkHref && { linkHref }
3139
+ });
3140
+ };
3141
+ const handleKey = (e2) => {
3142
+ if (e2.key === "Alt") setSuspended(e2.type === "keydown");
3143
+ if (e2.type === "keydown" && e2.key === "Escape") {
3144
+ clearSelection();
3145
+ }
3146
+ };
3147
+ const handleBlur = () => setSuspended(false);
3148
+ const handleScroll = () => overlay.onScroll();
3149
+ const handleResize = () => overlay.schedule();
3150
+ const handleSettled = () => overlay.schedule();
3151
+ function listItemIn(encoded, target) {
3152
+ if (!(target instanceof Element)) return null;
3153
+ const li = target.closest("li");
3154
+ if (!li || !encoded.contains(li) || !li.parentElement) return null;
3155
+ const items = Array.from(li.parentElement.children).filter(
3156
+ (child) => child.tagName === "LI"
3157
+ );
3158
+ return { element: li, index: items.indexOf(li) };
3159
+ }
3160
+ function nthListItem(encoded, block) {
3161
+ const list = encoded.querySelector("ul, ol");
3162
+ if (!list) return null;
3163
+ const items = Array.from(list.children).filter(
3164
+ (child) => child.tagName === "LI"
3165
+ );
3166
+ return items[block] ?? null;
3167
+ }
3168
+ function select(el, field, block) {
3169
+ const entryId = field.getAttribute("data-prepr-entry-id");
3170
+ selection = {
3171
+ lookup: {
3172
+ field: field.getAttribute("data-prepr-field") ?? "",
3173
+ id: field.getAttribute("data-prepr-id") ?? void 0,
3174
+ ...entryId && { entryId },
3175
+ ...block !== void 0 && { block }
3176
+ },
3177
+ label: field
3178
+ };
3179
+ overlay.setSelected(el, labelFor(field));
3180
+ if (overlay.getHoverTarget() === el) overlay.setHover(null, "");
3181
+ }
3182
+ function clearSelection() {
3183
+ selection = null;
3184
+ overlay.setSelected(null, "");
3185
+ }
3186
+ function reselect() {
3187
+ if (!selection) return;
3188
+ const current = overlay.getSelectedTarget();
3189
+ if (current?.isConnected && selection.label.isConnected) return;
3190
+ const found = findFieldElement(selection.lookup);
3191
+ if (!found || !isCurrentItem(found)) {
3192
+ overlay.setSelected(null, "");
3193
+ return;
3194
+ }
3195
+ const { block } = selection.lookup;
3196
+ const el = block !== void 0 ? nthListItem(found, block) ?? found : found;
3197
+ select(el, found, block);
3198
+ }
3199
+ function tagAssets() {
3200
+ if (assetKeys.size > MAX_CACHED_KEYS) assetKeys.clear();
3201
+ tagAssetImages(assetIndex, currentItemId, assetKeys, {
3202
+ isCurrentItem: (el) => isCurrentItem(el)
3203
+ });
3204
+ }
3205
+ const handleLayoutFrame = () => {
3206
+ mutationFrame = null;
3207
+ if (assetIndex.size) tagAssets();
3208
+ reselect();
3209
+ const hovered = overlay.getHoverTarget();
3210
+ if (hovered && !hovered.isConnected) {
3211
+ lastPointerTarget = document.elementFromPoint(lastPointerX, lastPointerY);
3212
+ updateHover();
3213
+ return;
3214
+ }
3215
+ overlay.schedule();
3216
+ };
3217
+ const handleMutations = () => {
3218
+ if (mutationFrame !== null) return;
3219
+ if (!selection && !overlay.getHoverTarget() && !assetIndex.size) return;
3220
+ mutationFrame = requestAnimationFrame(handleLayoutFrame);
3221
+ };
3222
+ function injectCursorStyles() {
3223
+ if (document.querySelector(`style[${CURSOR_STYLE_MARKER}]`)) return;
3224
+ const style = document.createElement("style");
3225
+ style.setAttribute(CURSOR_STYLE_MARKER, "");
3226
+ style.textContent = CURSOR_STYLES;
3227
+ document.head.appendChild(style);
3228
+ }
3229
+ function start() {
3230
+ if (running) return;
3231
+ running = true;
3232
+ overlay.mount();
3233
+ injectCursorStyles();
3234
+ document.addEventListener("pointermove", handlePointerMove, {
3235
+ passive: true
3236
+ });
3237
+ document.documentElement.addEventListener(
3238
+ "pointerleave",
3239
+ handlePointerLeave
3240
+ );
3241
+ document.addEventListener("click", handleClick, { capture: true });
3242
+ window.addEventListener("keydown", handleKey);
3243
+ window.addEventListener("keyup", handleKey);
3244
+ window.addEventListener("blur", handleBlur);
3245
+ window.addEventListener("scroll", handleScroll, {
3246
+ capture: true,
3247
+ passive: true
3248
+ });
3249
+ window.addEventListener("resize", handleResize, { passive: true });
3250
+ document.addEventListener("transitionend", handleSettled, {
3251
+ capture: true,
3252
+ passive: true
3253
+ });
3254
+ document.addEventListener("animationend", handleSettled, {
3255
+ capture: true,
3256
+ passive: true
3257
+ });
3258
+ mutationObserver = new MutationObserver(handleMutations);
3259
+ mutationObserver.observe(document.body, LAYOUT_MUTATIONS);
3260
+ if (assetIndex.size) tagAssets();
3261
+ syncGuard();
3262
+ debug8.log("visual editor started");
3263
+ }
3264
+ function stop() {
3265
+ if (!running) return;
3266
+ running = false;
3267
+ if (pointerFrame !== null) cancelAnimationFrame(pointerFrame);
3268
+ if (mutationFrame !== null) cancelAnimationFrame(mutationFrame);
3269
+ mutationObserver?.disconnect();
3270
+ mutationObserver = null;
3271
+ cancelFind();
3272
+ pointerFrame = null;
3273
+ mutationFrame = null;
3274
+ selection = null;
3275
+ lastPointerTarget = null;
3276
+ setSuspended(false);
3277
+ document.removeEventListener("pointermove", handlePointerMove);
3278
+ document.documentElement.removeEventListener(
3279
+ "pointerleave",
3280
+ handlePointerLeave
3281
+ );
3282
+ document.removeEventListener("click", handleClick, { capture: true });
3283
+ window.removeEventListener("keydown", handleKey);
3284
+ window.removeEventListener("keyup", handleKey);
3285
+ window.removeEventListener("blur", handleBlur);
3286
+ window.removeEventListener("scroll", handleScroll, { capture: true });
3287
+ window.removeEventListener("resize", handleResize);
3288
+ document.removeEventListener("transitionend", handleSettled, {
3289
+ capture: true
3290
+ });
3291
+ document.removeEventListener("animationend", handleSettled, {
3292
+ capture: true
3293
+ });
3294
+ document.querySelectorAll(`style[${CURSOR_STYLE_MARKER}]`).forEach((node) => node.remove());
3295
+ overlay.unmount();
3296
+ untagAssetImages();
3297
+ guard.stop();
3298
+ debug8.log("visual editor stopped");
3299
+ }
3300
+ function cancelFind() {
3301
+ if (findTimeout) clearTimeout(findTimeout);
3302
+ findTimeout = null;
3303
+ settlePendingFind?.();
3304
+ settlePendingFind = null;
3305
+ }
3306
+ function focusField(lookup) {
3307
+ cancelFind();
3308
+ return new Promise((resolve2) => {
3309
+ let attempt = 0;
3310
+ const finish = (found) => {
3311
+ settlePendingFind = null;
3312
+ resolve2(found);
3313
+ };
3314
+ settlePendingFind = () => resolve2(false);
3315
+ const tryFind = () => {
3316
+ findTimeout = null;
3317
+ const found = findFieldElement(lookup);
3318
+ if (found) {
3319
+ const el = lookup.block !== void 0 ? nthListItem(found, lookup.block) ?? found : found;
3320
+ scrollIntoFrame(el);
3321
+ if (running && isCurrentItem(found)) select(el, found, lookup.block);
3322
+ finish(true);
3323
+ return;
3324
+ }
3325
+ if (++attempt >= FIND_RETRIES) {
3326
+ debug8.log("no element renders field", lookup.field);
3327
+ finish(false);
3328
+ return;
3329
+ }
3330
+ findTimeout = setTimeout(tryFind, FIND_RETRY_MS);
3331
+ };
3332
+ tryFind();
3333
+ });
3334
+ }
3335
+ function setCurrentItem(itemId, referenceList, componentIds) {
3336
+ currentItemId = itemId;
3337
+ if (referenceList) setReferences(referenceList, componentIds);
3338
+ else if (componentIds) ownComponents = new Set(componentIds);
3339
+ if (running && assetIndex.size) tagAssets();
3340
+ const selected = overlay.getSelectedTarget();
3341
+ const owner = selected ? encodedFrom(selected) : null;
3342
+ if (owner && !isCurrentItem(owner)) clearSelection();
3343
+ }
3344
+ function setAssets(assets) {
3345
+ const hadAssets = assetIndex.size > 0;
3346
+ assetIndex = createAssetIndex(assets);
3347
+ if (running && (hadAssets || assetIndex.size)) tagAssets();
3348
+ }
3349
+ function setReferences(referenceList, componentIds) {
3350
+ if (componentIds) ownComponents = new Set(componentIds);
3351
+ references = /* @__PURE__ */ new Map();
3352
+ for (const { id, field } of referenceList) {
3353
+ if (!references.has(id)) references.set(id, field);
3354
+ }
3355
+ }
3356
+ function setLocale(value) {
3357
+ locale = value;
3358
+ }
3359
+ function syncGuard() {
3360
+ if (running && guardEnabled) guard.start();
3361
+ else guard.stop();
3362
+ }
3363
+ function setNavigationGuard(enabled) {
3364
+ guardEnabled = enabled;
3365
+ syncGuard();
3366
+ }
3367
+ return {
3368
+ start,
3369
+ stop,
3370
+ setCurrentItem,
3371
+ setReferences,
3372
+ setAssets,
3373
+ setLocale,
3374
+ focusField,
3375
+ setNavigationGuard,
3376
+ allowNavigation: (href) => guard.allow(href)
3377
+ };
3378
+ }
3379
+
2196
3380
  // src/core/create-preview.ts
2197
3381
  var debug9 = createScopedLogger("create-preview");
2198
3382
  var controllerStores = /* @__PURE__ */ new WeakMap();
@@ -2265,29 +3449,47 @@ function createPreprPreview(opts = {}) {
2265
3449
  el.connect(store, (key) => t(key, locale));
2266
3450
  }
2267
3451
  const editingEnabled = features.editMode || isIframe;
2268
- const stega = createStegaController({
2269
- // The CMS deep-link tooltip is chrome, so it follows the same decision as
2270
- // the bar: noise inside the editor (clicking the element itself requests
2271
- // the edit there), and unwanted in a headless preview that asked for no UI.
2272
- tooltip: mountUi,
2273
- // In the editor, ask the parent to focus the field instead of opening a new
2274
- // tab. Standalone previews keep the window.open behaviour.
2275
- onEdit: ({ href, id, field, locale: contentLocale }) => {
2276
- const safeHref = href ? safeEditUrl(href) : null;
2277
- if (href && !safeHref) {
2278
- debug9.warn("ignored edit request with unsupported href scheme");
2279
- return;
2280
- }
2281
- if (isIframe) {
2282
- sendPreprEvent("field_edit_requested", {
2283
- id,
2284
- field,
2285
- locale: contentLocale
2286
- });
2287
- } else if (safeHref) {
2288
- window.open(safeHref, "_blank", "noopener,noreferrer");
2289
- }
3452
+ const onEdit = ({
3453
+ href,
3454
+ id,
3455
+ field,
3456
+ locale: contentLocale,
3457
+ block,
3458
+ entryId,
3459
+ referenceField,
3460
+ linkHref
3461
+ }) => {
3462
+ const safeHref = href ? safeEditUrl(href) : null;
3463
+ if (href && !safeHref) {
3464
+ debug9.warn("ignored edit request with unsupported href scheme");
3465
+ return;
2290
3466
  }
3467
+ if (isIframe) {
3468
+ sendPreprEvent("field_edit_requested", {
3469
+ id,
3470
+ field,
3471
+ locale: contentLocale,
3472
+ ...block !== void 0 && { block },
3473
+ ...entryId && { entryId },
3474
+ ...referenceField && { referenceField },
3475
+ ...linkHref && { linkHref }
3476
+ });
3477
+ } else if (safeHref) {
3478
+ window.open(safeHref, "_blank", "noopener,noreferrer");
3479
+ }
3480
+ };
3481
+ const visualEditor = isIframe ? createVisualEditor({
3482
+ onEdit,
3483
+ locale,
3484
+ onNavigationBlocked: (href) => sendPreprEvent("navigation_blocked", { href })
3485
+ }) : null;
3486
+ const stega = visualEditor ?? createStegaController({
3487
+ // The edit-mode marks and hover label are chrome, so they follow the
3488
+ // same decision as the bar: unwanted in a headless preview that asked
3489
+ // for no UI.
3490
+ tooltip: mountUi,
3491
+ onEdit,
3492
+ locale
2291
3493
  });
2292
3494
  const autoClean = createStegaAutoClean({
2293
3495
  enabled: options?.autoClean
@@ -2310,10 +3512,30 @@ function createPreprPreview(opts = {}) {
2310
3512
  });
2311
3513
  const bridge = createIframeBridge(store, {
2312
3514
  allowedEditorOrigins: options?.allowedEditorOrigins,
3515
+ onHandshake: ({
3516
+ itemId,
3517
+ referencedIds,
3518
+ componentIds,
3519
+ uiLocale,
3520
+ navigationGuard,
3521
+ assets
3522
+ }) => {
3523
+ if (uiLocale) visualEditor?.setLocale(uiLocale);
3524
+ visualEditor?.setCurrentItem(itemId ?? null, referencedIds, componentIds);
3525
+ visualEditor?.setNavigationGuard(navigationGuard === true);
3526
+ if (assets) visualEditor?.setAssets(assets);
3527
+ },
3528
+ onAssets: (assets) => visualEditor?.setAssets(assets),
3529
+ onAllowNavigation: (href) => visualEditor?.allowNavigation(href),
3530
+ onReferences: (referencedIds, componentIds) => visualEditor?.setReferences(referencedIds, componentIds),
2313
3531
  // The editor selecting a field scrolls the matching element into view. The
2314
3532
  // lookup relies on the attributes the stega scan writes, so it finds
2315
3533
  // nothing until encoded content has been tagged.
2316
- onScrollToField: ({ field, id }) => {
3534
+ onScrollToField: ({ field, id, block, entryId, fieldById }) => {
3535
+ if (visualEditor) {
3536
+ void visualEditor.focusField({ field, id, block, entryId, fieldById });
3537
+ return;
3538
+ }
2317
3539
  if (!scrollToField({ field, id })) {
2318
3540
  debug9.log("no element on this page renders field", field);
2319
3541
  }
@@ -2348,7 +3570,7 @@ function createPreprPreview(opts = {}) {
2348
3570
  var import_react = require("react");
2349
3571
 
2350
3572
  // src/version.ts
2351
- var VERSION = "0.5.0-rc.1";
3573
+ var VERSION = "0.5.0-rc.11";
2352
3574
 
2353
3575
  // src/core/pixel.ts
2354
3576
  var PIXEL_SCRIPT_URL = "https://cdn.tracking.prepr.io/js/prepr.min.js";