@preprio/toolkit 0.5.0-rc.2 → 0.5.0-rc.3

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 (50) hide show
  1. package/README.md +207 -10
  2. package/dist/astro/index.cjs +1174 -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 +1177 -1
  6. package/dist/chunk-42Z2VW7G.js +1161 -0
  7. package/dist/chunk-6WY3MWCX.js +246 -0
  8. package/dist/chunk-MDAV2FLM.js +26 -0
  9. package/dist/{chunk-FLPNN3CV.js → chunk-PP3O7RB4.js} +1370 -623
  10. package/dist/chunk-S3VT4UWM.js +75 -0
  11. package/dist/chunk-SXUDBGLZ.js +184 -0
  12. package/dist/chunk-URXSDJOU.js +356 -0
  13. package/dist/index.cjs +3108 -392
  14. package/dist/index.d.cts +238 -4
  15. package/dist/index.d.ts +238 -4
  16. package/dist/index.js +189 -149
  17. package/dist/live-preview-EADA3ZL4.js +18 -0
  18. package/dist/nextjs/components.cjs +1522 -359
  19. package/dist/nextjs/components.d.cts +1 -1
  20. package/dist/nextjs/components.d.ts +1 -1
  21. package/dist/nextjs/components.js +4 -1
  22. package/dist/nextjs/index.cjs +1065 -6
  23. package/dist/nextjs/index.d.cts +73 -1
  24. package/dist/nextjs/index.d.ts +73 -1
  25. package/dist/nextjs/index.js +1059 -7
  26. package/dist/nextjs/live-action.cjs +285 -0
  27. package/dist/nextjs/live-action.d.cts +12 -0
  28. package/dist/nextjs/live-action.d.ts +12 -0
  29. package/dist/nextjs/live-action.js +68 -0
  30. package/dist/nextjs/live.cjs +1838 -0
  31. package/dist/nextjs/live.d.cts +52 -0
  32. package/dist/nextjs/live.d.ts +52 -0
  33. package/dist/nextjs/live.js +265 -0
  34. package/dist/nuxt/index.cjs +1 -1
  35. package/dist/nuxt/index.js +1 -1
  36. package/dist/nuxt/live-preview-PTEC3GPC.js +1462 -0
  37. package/dist/nuxt/live.cjs +1662 -0
  38. package/dist/nuxt/live.d.cts +43 -0
  39. package/dist/nuxt/live.d.ts +43 -0
  40. package/dist/nuxt/live.js +94 -0
  41. package/dist/{pixel-B3oQNbZ3.d.cts → pixel-CX422dCO.d.cts} +10 -1
  42. package/dist/{pixel-B3oQNbZ3.d.ts → pixel-CX422dCO.d.ts} +10 -1
  43. package/dist/react/components.cjs +1522 -359
  44. package/dist/react/components.d.cts +9 -0
  45. package/dist/react/components.d.ts +9 -0
  46. package/dist/react/components.js +1522 -359
  47. package/dist/sveltekit/index.cjs +1 -1
  48. package/dist/sveltekit/index.js +1 -1
  49. package/package.json +12 -1
  50. package/src/astro/PreprToolbar.astro +7 -1
@@ -71,6 +71,40 @@ var ALL_FEATURES_ENABLED = Object.freeze({
71
71
  editMode: true
72
72
  });
73
73
 
74
+ // src/core/locales/de.json
75
+ var de_default = {
76
+ common: {
77
+ viewingAs: "Ansicht als:",
78
+ user: "Benutzer",
79
+ off: "Aus",
80
+ on: "An",
81
+ reset: "Zur\xFCcksetzen",
82
+ toolbar: "Symbolleiste"
83
+ },
84
+ adaptiveContent: {
85
+ adaptiveContent: "Adaptive Inhalte",
86
+ enablePreview: "Vorschaumodus",
87
+ segment: "Segment",
88
+ ABVariant: "A/B-Variante",
89
+ chooseSegment: "Segment w\xE4hlen",
90
+ searchSegments: "Segmente suchen",
91
+ none: "Keine Segmente",
92
+ allOtherUsers: "Alle anderen Benutzer",
93
+ offDescription: "Die Website als echter Benutzer ansehen (Cookies verwenden)",
94
+ onDescription: "Mit der Symbolleiste Segmente und A/B-Varianten simulieren"
95
+ },
96
+ editingTools: {
97
+ editingTools: "Bearbeitungswerkzeuge",
98
+ editMode: "Bearbeitungsmodus",
99
+ ariaCloseEditMode: "Bearbeitungsmodus schlie\xDFen"
100
+ },
101
+ visualEditor: {
102
+ edit: "Bearbeiten",
103
+ otherItem: "anderes Element",
104
+ linkedItem: "verkn\xFCpftes Element"
105
+ }
106
+ };
107
+
74
108
  // src/core/locales/en.json
75
109
  var en_default = {
76
110
  common: {
@@ -97,6 +131,45 @@ var en_default = {
97
131
  editingTools: "Editing tools",
98
132
  editMode: "Edit mode",
99
133
  ariaCloseEditMode: "Close edit mode"
134
+ },
135
+ visualEditor: {
136
+ edit: "Edit",
137
+ otherItem: "other item",
138
+ linkedItem: "linked item"
139
+ }
140
+ };
141
+
142
+ // src/core/locales/fr.json
143
+ var fr_default = {
144
+ common: {
145
+ viewingAs: "Affichage en tant que :",
146
+ user: "Utilisateur",
147
+ off: "D\xE9sactiv\xE9",
148
+ on: "Activ\xE9",
149
+ reset: "R\xE9initialiser",
150
+ toolbar: "Barre d'outils"
151
+ },
152
+ adaptiveContent: {
153
+ adaptiveContent: "Contenu adaptatif",
154
+ enablePreview: "Mode aper\xE7u",
155
+ segment: "Segment",
156
+ ABVariant: "Variante A/B",
157
+ chooseSegment: "Choisir un segment",
158
+ searchSegments: "Rechercher des segments",
159
+ none: "Aucun segment",
160
+ allOtherUsers: "Tous les autres utilisateurs",
161
+ offDescription: "Voir le site comme un vrai utilisateur (utiliser les cookies)",
162
+ onDescription: "Utilisez la barre d'outils pour simuler des segments et des variantes A/B"
163
+ },
164
+ editingTools: {
165
+ editingTools: "Outils d'\xE9dition",
166
+ editMode: "Mode \xE9dition",
167
+ ariaCloseEditMode: "Fermer le mode \xE9dition"
168
+ },
169
+ visualEditor: {
170
+ edit: "Modifier",
171
+ otherItem: "autre \xE9l\xE9ment",
172
+ linkedItem: "\xE9l\xE9ment li\xE9"
100
173
  }
101
174
  };
102
175
 
@@ -126,13 +199,20 @@ var nl_default = {
126
199
  editingTools: "Editing Tools",
127
200
  editMode: "Edit mode",
128
201
  ariaCloseEditMode: "Edit mode sluiten"
202
+ },
203
+ visualEditor: {
204
+ edit: "Bewerken",
205
+ otherItem: "ander item",
206
+ linkedItem: "gekoppeld item"
129
207
  }
130
208
  };
131
209
 
132
210
  // src/core/i18n.ts
133
211
  var dictionaries = {
134
212
  en: en_default,
135
- nl: nl_default
213
+ nl: nl_default,
214
+ fr: fr_default,
215
+ de: de_default
136
216
  };
137
217
  var LOCALES = Object.keys(dictionaries);
138
218
  function isLocale(value) {
@@ -225,49 +305,47 @@ function createScopedLogger(scopeName) {
225
305
  prefix: `[Prepr][${scopeName}]`
226
306
  });
227
307
  }
228
- function throttle(func, delay) {
229
- let timeoutId = null;
230
- let lastExecTime = 0;
231
- const throttledFunc = ((...args) => {
232
- const currentTime = Date.now();
233
- const timeSinceLastExec = currentTime - lastExecTime;
234
- if (timeSinceLastExec >= delay) {
235
- func(...args);
236
- lastExecTime = currentTime;
237
- } else {
238
- if (timeoutId) {
239
- clearTimeout(timeoutId);
240
- }
241
- timeoutId = setTimeout(() => {
242
- func(...args);
243
- lastExecTime = Date.now();
244
- timeoutId = null;
245
- }, delay - timeSinceLastExec);
246
- }
247
- });
248
- throttledFunc.cancel = () => {
249
- if (timeoutId) {
250
- clearTimeout(timeoutId);
251
- timeoutId = null;
252
- }
253
- };
254
- return throttledFunc;
255
- }
256
- function createElementCache(query, ttl = 1e3) {
257
- let cache = null;
258
- let lastCacheTime = 0;
259
- return () => {
260
- const now = Date.now();
261
- if (!cache || now - lastCacheTime > ttl) {
262
- cache = document.querySelectorAll(query);
263
- lastCacheTime = now;
264
- }
265
- return cache;
266
- };
267
- }
268
308
 
269
309
  // src/core/iframe-bridge.ts
270
310
  var debug = createScopedLogger("iframe-bridge");
311
+ var MAX_REFERENCES = 500;
312
+ var MAX_HREF_LENGTH = 8192;
313
+ var MAX_ASSETS = 500;
314
+ var MAX_ASSET_KEY_LENGTH = 2048;
315
+ var MAX_ASSET_FIELD_LENGTH = 512;
316
+ function parseIds(value) {
317
+ if (!Array.isArray(value)) return void 0;
318
+ return value.slice(0, MAX_REFERENCES).filter((id) => typeof id === "string" && id.length > 0);
319
+ }
320
+ function parseReferences(value) {
321
+ if (!Array.isArray(value)) return void 0;
322
+ const list = [];
323
+ for (const entry of value.slice(0, MAX_REFERENCES)) {
324
+ const id = entry?.id;
325
+ const field = entry?.field;
326
+ if (typeof id === "string" && id && typeof field === "string" && field) {
327
+ list.push({ id, field });
328
+ }
329
+ }
330
+ return list;
331
+ }
332
+ function boundedString(value, max) {
333
+ return typeof value === "string" && value && value.length <= max ? value : void 0;
334
+ }
335
+ function parseAssets(value) {
336
+ if (!Array.isArray(value)) return void 0;
337
+ const list = [];
338
+ for (const entry of value.slice(0, MAX_ASSETS)) {
339
+ if (!entry || typeof entry !== "object") continue;
340
+ const raw = entry;
341
+ const key = boundedString(raw.key, MAX_ASSET_KEY_LENGTH);
342
+ const field = boundedString(raw.field, MAX_ASSET_FIELD_LENGTH);
343
+ if (!key || !field) continue;
344
+ const entryId = boundedString(raw.entryId, MAX_ASSET_FIELD_LENGTH);
345
+ list.push(entryId ? { key, field, entryId } : { key, field });
346
+ }
347
+ return list;
348
+ }
271
349
  var EDITOR_BASE_DOMAIN = "prepr.io";
272
350
  function isAllowedEditorOrigin(origin, allowed) {
273
351
  if (allowed) {
@@ -289,6 +367,10 @@ function isAllowedEditorOrigin(origin, allowed) {
289
367
  function createIframeBridge(store, options = {}) {
290
368
  const allowedOrigins = options.allowedEditorOrigins;
291
369
  const onScrollToField = options.onScrollToField;
370
+ const onHandshake = options.onHandshake;
371
+ const onReferences = options.onReferences;
372
+ const onAssets = options.onAssets;
373
+ const onAllowNavigation = options.onAllowNavigation;
292
374
  let parentOrigin = null;
293
375
  const onKeyDown = (event) => {
294
376
  const key = event.key.toLowerCase();
@@ -309,8 +391,43 @@ function createIframeBridge(store, options = {}) {
309
391
  setTimeout(() => window.scrollTo(0, top), 1);
310
392
  }
311
393
  store?.set({ previewMode: true, editMode: data.editMode ?? true });
394
+ const itemId = typeof data.itemId === "string" && data.itemId ? data.itemId : void 0;
395
+ const referencedIds = parseReferences(data.referencedIds);
396
+ const componentIds = parseIds(data.componentIds);
397
+ const uiLocale = isLocale(data.uiLocale) ? data.uiLocale : void 0;
398
+ const navigationGuard = data.navigationGuard === true;
399
+ const assets = parseAssets(data.assets);
400
+ try {
401
+ onHandshake?.({
402
+ ...itemId && { itemId },
403
+ ...referencedIds && { referencedIds },
404
+ ...componentIds && { componentIds },
405
+ ...uiLocale && { uiLocale },
406
+ ...navigationGuard && { navigationGuard },
407
+ ...assets && { assets }
408
+ });
409
+ } catch (error) {
410
+ debug.warn("handshake handler failed", error);
411
+ }
312
412
  }
313
413
  if (!parentOrigin || evt.origin !== parentOrigin) return;
414
+ if (data?.event === "prepr:references") {
415
+ const assets = parseAssets(data.assets);
416
+ if (assets) {
417
+ try {
418
+ onAssets?.(assets);
419
+ } catch (error) {
420
+ debug.warn("assets handler failed", error);
421
+ }
422
+ }
423
+ const referencedIds = parseReferences(data.referencedIds);
424
+ if (!referencedIds) return;
425
+ try {
426
+ onReferences?.(referencedIds, parseIds(data.componentIds));
427
+ } catch (error) {
428
+ debug.warn("references handler failed", error);
429
+ }
430
+ }
314
431
  if (data?.event === "prepr:getScrollPosition") {
315
432
  const currentScrollY = window.scrollY || document.documentElement.scrollTop;
316
433
  sendPreprEvent("getScrollPosition", { value: currentScrollY });
@@ -318,12 +435,32 @@ function createIframeBridge(store, options = {}) {
318
435
  if (data?.event === "prepr:scrollToField") {
319
436
  if (typeof data.field !== "string" || !data.field) return;
320
437
  if (data.id !== void 0 && typeof data.id !== "string") return;
438
+ const block = typeof data.block === "number" && Number.isInteger(data.block) ? data.block : void 0;
439
+ const entryId = typeof data.entryId === "string" && data.entryId ? data.entryId : void 0;
440
+ const fieldById = typeof data.fieldById === "string" && data.fieldById ? data.fieldById : void 0;
321
441
  try {
322
- onScrollToField?.({ field: data.field, id: data.id });
442
+ onScrollToField?.({
443
+ field: data.field,
444
+ id: data.id,
445
+ block,
446
+ entryId,
447
+ fieldById
448
+ });
323
449
  } catch (error) {
324
450
  debug.warn("scrollToField handler failed", error);
325
451
  }
326
452
  }
453
+ if (data?.event === "prepr:allowNavigation") {
454
+ const href = data.href;
455
+ if (typeof href !== "string" || !href || href.length > MAX_HREF_LENGTH) {
456
+ return;
457
+ }
458
+ try {
459
+ onAllowNavigation?.(href);
460
+ } catch (error) {
461
+ debug.warn("allowNavigation handler failed", error);
462
+ }
463
+ }
327
464
  };
328
465
  return {
329
466
  start() {
@@ -427,29 +564,43 @@ function tagEncodedElement(element, decoded) {
427
564
  element.setAttribute("data-prepr-origin", decoded.origin);
428
565
  const { id, field, locale } = parseEditHref(decoded.href);
429
566
  setOrRemove(element, "data-prepr-id", id);
430
- setOrRemove(element, "data-prepr-field", field);
567
+ const fieldHint = element.getAttribute("data-prepr-edit-target")?.trim() || void 0;
568
+ setOrRemove(element, "data-prepr-field", field ?? fieldHint);
431
569
  setOrRemove(element, "data-prepr-locale", locale);
432
570
  }
571
+ function readEditPayload(element) {
572
+ return {
573
+ href: element.getAttribute("data-prepr-href") ?? "",
574
+ origin: element.getAttribute("data-prepr-origin") ?? "",
575
+ id: element.getAttribute("data-prepr-id") ?? void 0,
576
+ field: element.getAttribute("data-prepr-field") ?? void 0,
577
+ locale: element.getAttribute("data-prepr-locale") ?? void 0,
578
+ entryId: element.getAttribute("data-prepr-entry-id") || void 0
579
+ };
580
+ }
433
581
 
434
582
  // src/core/stega/auto-clean.ts
435
583
  var debug3 = createScopedLogger("stega:auto-clean");
584
+ var cleanedNodes = /* @__PURE__ */ new WeakMap();
436
585
  function createStegaAutoClean(options) {
437
586
  const stripText = options?.enabled ?? true;
438
587
  let observer = null;
439
588
  let processing = false;
440
- let cleaned = /* @__PURE__ */ new WeakMap();
441
589
  let pendingHidden = /* @__PURE__ */ new Set();
442
590
  let pendingDirty = /* @__PURE__ */ new Set();
443
591
  let debounceTimeout = null;
444
592
  function cleanTextNode(textNode) {
445
593
  if (isIgnoredTextNode(textNode)) return false;
446
594
  const textContent = textNode.textContent;
447
- const info = cleaned.get(textNode);
595
+ const info = cleanedNodes.get(textNode);
448
596
  const settled = stripText ? info?.cleaned : info?.original;
449
597
  if (info && textContent === settled) {
450
598
  if (!info.decoded) return false;
451
599
  const shown = resolveEditTarget(textNode);
452
- if (!shown) return false;
600
+ if (!shown) {
601
+ pendingHidden.add(textNode);
602
+ return false;
603
+ }
453
604
  pendingHidden.delete(textNode);
454
605
  if (shown.hasAttribute("data-prepr-encoded")) return false;
455
606
  tagEncodedElement(shown, info.decoded);
@@ -460,7 +611,7 @@ function createStegaAutoClean(options) {
460
611
  if (!encoded || stripped === textContent) return false;
461
612
  const decoded = decodeStega(textContent);
462
613
  if (!decoded?.href && !stripText) return false;
463
- cleaned.set(textNode, {
614
+ cleanedNodes.set(textNode, {
464
615
  original: textContent,
465
616
  cleaned: stripped,
466
617
  decoded: decoded?.href ? decoded : null
@@ -502,7 +653,7 @@ function createStegaAutoClean(options) {
502
653
  }
503
654
  if (mutation.type === "characterData" && mutation.target.nodeType === Node.TEXT_NODE) {
504
655
  const textNode = mutation.target;
505
- const info = cleaned.get(textNode);
656
+ const info = cleanedNodes.get(textNode);
506
657
  const text = textNode.textContent ?? "";
507
658
  const settled = stripText ? info?.cleaned : info?.original;
508
659
  if (text !== settled && vercelStegaSplit(text).encoded) {
@@ -545,7 +696,16 @@ function createStegaAutoClean(options) {
545
696
  retryPendingHidden();
546
697
  }, 50);
547
698
  }
699
+ let resizeTimeout = null;
700
+ function handleResize() {
701
+ if (pendingHidden.size === 0 || resizeTimeout) return;
702
+ resizeTimeout = setTimeout(() => {
703
+ resizeTimeout = null;
704
+ retryPendingHidden();
705
+ }, 100);
706
+ }
548
707
  function setupObserver() {
708
+ window.addEventListener("resize", handleResize, { passive: true });
549
709
  observer = new MutationObserver(handleMutations);
550
710
  observer.observe(document.body, {
551
711
  childList: true,
@@ -568,11 +728,15 @@ function createStegaAutoClean(options) {
568
728
  clearTimeout(debounceTimeout);
569
729
  debounceTimeout = null;
570
730
  }
731
+ if (resizeTimeout) {
732
+ clearTimeout(resizeTimeout);
733
+ resizeTimeout = null;
734
+ }
571
735
  if (observer) {
572
736
  observer.disconnect();
573
737
  observer = null;
738
+ window.removeEventListener("resize", handleResize);
574
739
  }
575
- cleaned = /* @__PURE__ */ new WeakMap();
576
740
  pendingHidden = /* @__PURE__ */ new Set();
577
741
  pendingDirty = /* @__PURE__ */ new Set();
578
742
  debug3.log("stega auto-clean stopped");
@@ -580,6 +744,439 @@ function createStegaAutoClean(options) {
580
744
  return { start, stop };
581
745
  }
582
746
 
747
+ // src/core/visual-editor/overlay.ts
748
+ var HOST_TAG = "prepr-visual-editor";
749
+ var SCROLL_SETTLE_MS = 120;
750
+ var LABEL_SPACE_PX = 22;
751
+ var BOX_PADDING_PX = 3;
752
+ var LABEL_MARGIN_PX = 4;
753
+ var PROBE_POINTS = [
754
+ [0.5, 0.5],
755
+ [0.15, 0.2],
756
+ [0.85, 0.2],
757
+ [0.15, 0.8],
758
+ [0.85, 0.8]
759
+ ];
760
+ function overlapsBox(area, rect) {
761
+ 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;
762
+ }
763
+ function overlaps(a2, b2) {
764
+ return a2.left < b2.right && b2.left < a2.right && a2.top < b2.bottom && b2.top < a2.bottom;
765
+ }
766
+ function coverOf(hit, target, x2, y2) {
767
+ let cover = hit.getBoundingClientRect();
768
+ for (let el = hit.parentElement; el && !el.contains(target) && el !== document.body; el = el.parentElement) {
769
+ const r2 = el.getBoundingClientRect();
770
+ if (x2 < r2.left || x2 >= r2.right || y2 < r2.top || y2 >= r2.bottom) break;
771
+ cover = r2;
772
+ }
773
+ return cover;
774
+ }
775
+ function mergeAreas(areas) {
776
+ const merged = areas.slice();
777
+ for (let i2 = 0; i2 < merged.length; i2++) {
778
+ for (let j2 = i2 + 1; j2 < merged.length; j2++) {
779
+ if (!overlaps(merged[i2], merged[j2])) continue;
780
+ merged[i2] = {
781
+ left: Math.min(merged[i2].left, merged[j2].left),
782
+ top: Math.min(merged[i2].top, merged[j2].top),
783
+ right: Math.max(merged[i2].right, merged[j2].right),
784
+ bottom: Math.max(merged[i2].bottom, merged[j2].bottom)
785
+ };
786
+ merged.splice(j2, 1);
787
+ j2 = i2;
788
+ }
789
+ }
790
+ return merged;
791
+ }
792
+ function findCovers(target, rect) {
793
+ const width = window.innerWidth;
794
+ const height = window.innerHeight;
795
+ const covers = [];
796
+ const seen = /* @__PURE__ */ new Set();
797
+ let probed = false;
798
+ let reached = false;
799
+ for (const [fx, fy] of PROBE_POINTS) {
800
+ const x2 = rect.left + rect.width * fx;
801
+ const y2 = rect.top + rect.height * fy;
802
+ if (x2 < 0 || y2 < 0 || x2 >= width || y2 >= height) continue;
803
+ probed = true;
804
+ const hit = document.elementFromPoint(x2, y2);
805
+ if (!hit || hit === target || target.contains(hit)) {
806
+ reached = true;
807
+ continue;
808
+ }
809
+ if (seen.has(hit)) continue;
810
+ seen.add(hit);
811
+ if (covers.some(
812
+ (c2) => x2 >= c2.left && x2 < c2.right && y2 >= c2.top && y2 < c2.bottom
813
+ )) {
814
+ continue;
815
+ }
816
+ covers.push(coverOf(hit, target, x2, y2));
817
+ }
818
+ if (!probed) return [];
819
+ return reached ? mergeAreas(covers) : null;
820
+ }
821
+ function clipPath(x2, y2, covers) {
822
+ const far = 1e5;
823
+ const points = [
824
+ `${-far}px ${-far}px`,
825
+ `${far}px ${-far}px`,
826
+ `${far}px ${far}px`,
827
+ `${-far}px ${far}px`,
828
+ `${-far}px ${-far}px`
829
+ ];
830
+ for (const c2 of covers) {
831
+ const l2 = c2.left - x2;
832
+ const t3 = c2.top - y2;
833
+ const r2 = c2.right - x2;
834
+ const b2 = c2.bottom - y2;
835
+ points.push(
836
+ `${l2}px ${t3}px`,
837
+ `${r2}px ${t3}px`,
838
+ `${r2}px ${b2}px`,
839
+ `${l2}px ${b2}px`,
840
+ `${l2}px ${t3}px`,
841
+ `${-far}px ${-far}px`
842
+ );
843
+ }
844
+ return `polygon(evenodd, ${points.join(", ")})`;
845
+ }
846
+ var STYLES = `
847
+ :host {
848
+ all: initial;
849
+ position: fixed;
850
+ inset: 0;
851
+ pointer-events: none;
852
+ z-index: 2147483646;
853
+ contain: strict;
854
+ }
855
+ .box {
856
+ position: absolute;
857
+ top: 0;
858
+ left: 0;
859
+ box-sizing: border-box;
860
+ border-radius: 6px;
861
+ opacity: 0;
862
+ will-change: transform, width, height;
863
+ transition:
864
+ transform 180ms cubic-bezier(0.2, 0.8, 0.2, 1),
865
+ width 180ms cubic-bezier(0.2, 0.8, 0.2, 1),
866
+ height 180ms cubic-bezier(0.2, 0.8, 0.2, 1),
867
+ opacity 140ms ease;
868
+ }
869
+ .box[data-visible] { opacity: 1; }
870
+ .box[data-instant], :host([data-scrolling]) .box {
871
+ transition: opacity 140ms ease;
872
+ }
873
+ .hover {
874
+ border: 1.5px solid rgba(79, 70, 229, 0.9);
875
+ background: rgba(79, 70, 229, 0.06);
876
+ }
877
+ .selected {
878
+ border: 2px solid #4f46e5;
879
+ box-shadow: 0 0 0 4px rgba(79, 70, 229, 0.18);
880
+ }
881
+ .label {
882
+ position: absolute;
883
+ left: -2px;
884
+ bottom: 100%;
885
+ margin-bottom: 4px;
886
+ padding: 3px 7px;
887
+ border-radius: 5px;
888
+ background: #4f46e5;
889
+ color: #fff;
890
+ font: 500 11px/1.3 ui-sans-serif, system-ui, -apple-system, 'Segoe UI', sans-serif;
891
+ letter-spacing: 0.01em;
892
+ white-space: nowrap;
893
+ display: flex;
894
+ max-width: 240px;
895
+ box-shadow: 0 1px 3px rgba(15, 23, 42, 0.25);
896
+ }
897
+ .text {
898
+ min-width: 0;
899
+ overflow: hidden;
900
+ text-overflow: ellipsis;
901
+ }
902
+ .hover .label { background: rgba(79, 70, 229, 0.92); }
903
+ /* Content of another item than the one open in the editor: still clickable,
904
+ but visibly not part of the item being edited. */
905
+ .hover[data-item-kind='other'] {
906
+ border: 1.5px dashed #a5b4fc;
907
+ background: rgba(100, 116, 139, 0.05);
908
+ }
909
+ .hover[data-item-kind='other'] .label { background: #64748b; }
910
+ .hover[data-item-kind='referenced'] {
911
+ border: 1.5px solid #a5b4fc;
912
+ background: rgba(79, 70, 229, 0.03);
913
+ }
914
+ .hover[data-item-kind='referenced'] .label { background: #818cf8; }
915
+ .box[data-label-covered] .label { visibility: hidden; }
916
+ .box[data-label-inside] .label {
917
+ bottom: auto;
918
+ top: 2px;
919
+ left: 2px;
920
+ margin: 0;
921
+ }
922
+ /* A clickable hover label (edit mode on the live site). Only while shown, so
923
+ a fading box never leaves an invisible click target behind. */
924
+ :host([data-label-action]) .hover[data-visible] .label {
925
+ pointer-events: auto;
926
+ cursor: pointer;
927
+ }
928
+ :host([data-label-action]) .hover .label::after {
929
+ content: '\\2197';
930
+ margin-left: 5px;
931
+ }
932
+ :host([data-label-action]) .hover .label:hover { background: #4338ca; }
933
+ /* Bridges the gap between the label and its element, so the pointer can
934
+ travel from one to the other without the hover ending on the way. */
935
+ :host([data-label-action]) .hover .label::before {
936
+ content: '';
937
+ position: absolute;
938
+ left: 0;
939
+ right: 0;
940
+ top: 100%;
941
+ height: ${LABEL_MARGIN_PX + BOX_PADDING_PX + 2}px;
942
+ }
943
+ @media (prefers-reduced-motion: reduce) {
944
+ .box { transition: opacity 80ms linear; }
945
+ }
946
+ `;
947
+ var VisualEditorOverlay = class {
948
+ constructor(options = {}) {
949
+ this.host = null;
950
+ this.hoverBox = null;
951
+ this.selectedBox = null;
952
+ this.frame = null;
953
+ this.scrollTimeout = null;
954
+ this.resizeObserver = null;
955
+ this.onLabelClick = options.onLabelClick;
956
+ }
957
+ mount() {
958
+ if (this.host) return;
959
+ const host = document.createElement(HOST_TAG);
960
+ const shadow = host.attachShadow({ mode: "open" });
961
+ const style = document.createElement("style");
962
+ style.textContent = STYLES;
963
+ shadow.appendChild(style);
964
+ this.hoverBox = this.createBox(shadow, "hover");
965
+ this.selectedBox = this.createBox(shadow, "selected");
966
+ const onLabelClick = this.onLabelClick;
967
+ if (onLabelClick) {
968
+ host.setAttribute("data-label-action", "");
969
+ const hoverBox = this.hoverBox;
970
+ hoverBox.label.addEventListener("click", () => {
971
+ if (hoverBox.target) onLabelClick(hoverBox.target);
972
+ });
973
+ }
974
+ document.documentElement.appendChild(host);
975
+ this.host = host;
976
+ if (typeof ResizeObserver !== "undefined") {
977
+ this.resizeObserver = new ResizeObserver(() => this.schedule());
978
+ }
979
+ }
980
+ unmount() {
981
+ if (this.frame !== null) cancelAnimationFrame(this.frame);
982
+ if (this.scrollTimeout) clearTimeout(this.scrollTimeout);
983
+ this.frame = null;
984
+ this.scrollTimeout = null;
985
+ this.resizeObserver?.disconnect();
986
+ this.resizeObserver = null;
987
+ this.host?.remove();
988
+ this.host = null;
989
+ this.hoverBox = null;
990
+ this.selectedBox = null;
991
+ }
992
+ /**
993
+ * Whether a pointer event target is this chrome: events from inside the
994
+ * shadow root reach the page retargeted to its host.
995
+ */
996
+ owns(node) {
997
+ return node !== null && node === this.host;
998
+ }
999
+ getHoverTarget() {
1000
+ return this.hoverBox?.target ?? null;
1001
+ }
1002
+ getSelectedTarget() {
1003
+ return this.selectedBox?.target ?? null;
1004
+ }
1005
+ setHover(target, label, options = {}) {
1006
+ if (target) {
1007
+ this.hoverBox?.root.setAttribute(
1008
+ "data-item-kind",
1009
+ options.kind ?? "current"
1010
+ );
1011
+ }
1012
+ this.retarget(this.hoverBox, target, label);
1013
+ }
1014
+ setSelected(target, label) {
1015
+ this.retarget(this.selectedBox, target, label);
1016
+ }
1017
+ /** Call on every scroll; positions follow in the next frame, untransitioned. */
1018
+ onScroll() {
1019
+ if (!this.host) return;
1020
+ this.host.setAttribute("data-scrolling", "");
1021
+ if (this.scrollTimeout) clearTimeout(this.scrollTimeout);
1022
+ this.scrollTimeout = setTimeout(() => {
1023
+ this.host?.removeAttribute("data-scrolling");
1024
+ this.scrollTimeout = null;
1025
+ }, SCROLL_SETTLE_MS);
1026
+ this.schedule();
1027
+ }
1028
+ /** Re-measure both boxes in the next animation frame. */
1029
+ schedule() {
1030
+ if (this.frame !== null || !this.host) return;
1031
+ this.frame = requestAnimationFrame(() => {
1032
+ this.frame = null;
1033
+ this.render();
1034
+ });
1035
+ }
1036
+ createBox(shadow, kind) {
1037
+ const root = document.createElement("div");
1038
+ root.className = `box ${kind}`;
1039
+ const label = document.createElement("div");
1040
+ label.className = "label";
1041
+ const text = document.createElement("span");
1042
+ text.className = "text";
1043
+ label.appendChild(text);
1044
+ root.appendChild(label);
1045
+ shadow.appendChild(root);
1046
+ return { root, label, text, target: null };
1047
+ }
1048
+ retarget(box, target, label) {
1049
+ if (!box || box.target === target) return;
1050
+ const previous = box.target;
1051
+ if (previous) this.resizeObserver?.unobserve(previous);
1052
+ box.target = target;
1053
+ if (target) {
1054
+ this.resizeObserver?.observe(target);
1055
+ box.text.textContent = label;
1056
+ if (!box.root.hasAttribute("data-visible")) {
1057
+ box.root.setAttribute("data-instant", "");
1058
+ }
1059
+ }
1060
+ this.schedule();
1061
+ }
1062
+ render() {
1063
+ const hover = this.hoverBox ? this.measure(this.hoverBox) : null;
1064
+ const selected = this.selectedBox ? this.measure(this.selectedBox) : null;
1065
+ if (this.hoverBox) this.paint(this.hoverBox, hover, selected);
1066
+ if (this.selectedBox) this.paint(this.selectedBox, selected, hover);
1067
+ }
1068
+ measure(box) {
1069
+ const target = box.target;
1070
+ if (!target) return null;
1071
+ if (!target.isConnected) {
1072
+ this.resizeObserver?.unobserve(target);
1073
+ box.target = null;
1074
+ return null;
1075
+ }
1076
+ const rect = target.getBoundingClientRect();
1077
+ if (rect.width === 0 && rect.height === 0) return null;
1078
+ const covers = findCovers(target, rect);
1079
+ if (!covers) return null;
1080
+ return {
1081
+ rect,
1082
+ covers,
1083
+ labelWidth: covers.length ? box.label.offsetWidth : 0,
1084
+ labelHeight: covers.length ? box.label.offsetHeight : 0
1085
+ };
1086
+ }
1087
+ paint(box, measurement, other = null) {
1088
+ const root = box.root;
1089
+ if (!measurement) {
1090
+ root.removeAttribute("data-visible");
1091
+ return;
1092
+ }
1093
+ const { rect, covers } = measurement;
1094
+ const x2 = rect.left - BOX_PADDING_PX;
1095
+ const y2 = rect.top - BOX_PADDING_PX;
1096
+ const labelAbove = {
1097
+ left: x2 - 2,
1098
+ top: y2 - LABEL_SPACE_PX,
1099
+ right: x2 - 2 + (measurement.labelWidth || Math.min(240, rect.width)),
1100
+ bottom: y2
1101
+ };
1102
+ const labelInside = y2 < LABEL_SPACE_PX || other !== null && other.rect !== rect && overlapsBox(labelAbove, other.rect);
1103
+ root.style.transform = `translate3d(${x2}px, ${y2}px, 0)`;
1104
+ root.style.width = `${rect.width + BOX_PADDING_PX * 2}px`;
1105
+ root.style.height = `${rect.height + BOX_PADDING_PX * 2}px`;
1106
+ root.toggleAttribute("data-label-inside", labelInside);
1107
+ root.style.clipPath = covers.length ? clipPath(x2, y2, covers) : "";
1108
+ const labelLeft = labelInside ? x2 + 2 : x2 - 2;
1109
+ const labelTop = labelInside ? y2 + 2 : y2 - LABEL_MARGIN_PX - measurement.labelHeight;
1110
+ const label = {
1111
+ left: labelLeft,
1112
+ top: labelTop,
1113
+ right: labelLeft + measurement.labelWidth,
1114
+ bottom: labelTop + measurement.labelHeight
1115
+ };
1116
+ root.toggleAttribute(
1117
+ "data-label-covered",
1118
+ covers.some((c2) => overlaps(c2, label))
1119
+ );
1120
+ if (root.hasAttribute("data-instant")) {
1121
+ root.setAttribute("data-visible", "");
1122
+ requestAnimationFrame(() => root.removeAttribute("data-instant"));
1123
+ return;
1124
+ }
1125
+ root.setAttribute("data-visible", "");
1126
+ }
1127
+ };
1128
+
1129
+ // src/core/visual-editor/field-path.ts
1130
+ var INDEX_SEGMENT = /^\d+$/;
1131
+ var WORD_SEPARATORS = /[_-]+/g;
1132
+ function normalizeFieldPath(path) {
1133
+ return path.split(".").map(
1134
+ (segment) => INDEX_SEGMENT.test(segment) ? String(Number.parseInt(segment, 10)) : segment
1135
+ ).join(".");
1136
+ }
1137
+ var GENERIC_LEAVES = /* @__PURE__ */ new Set(["value", "text", "body", "content", "label"]);
1138
+ function humanize(name) {
1139
+ const words = name.replace(WORD_SEPARATORS, " ").trim();
1140
+ return words.charAt(0).toUpperCase() + words.slice(1);
1141
+ }
1142
+ function fieldLabel(path, locale = "en") {
1143
+ if (!path) return t("visualEditor.edit", locale);
1144
+ const segments = path.split(".").filter((s2) => s2 && s2 !== "items" && !INDEX_SEGMENT.test(s2));
1145
+ const name = segments[segments.length - 1];
1146
+ if (!name) return t("visualEditor.edit", locale);
1147
+ const parent = segments[segments.length - 2];
1148
+ if (parent && GENERIC_LEAVES.has(name)) {
1149
+ return `${humanize(parent)} \u203A ${humanize(name)}`;
1150
+ }
1151
+ return humanize(name);
1152
+ }
1153
+ function findFieldElement(lookup) {
1154
+ if (!lookup.field) return null;
1155
+ const candidates = document.querySelectorAll(
1156
+ "[data-prepr-encoded]"
1157
+ );
1158
+ if (lookup.entryId) {
1159
+ for (const candidate of candidates) {
1160
+ if (candidate.getAttribute("data-prepr-entry-id") === lookup.entryId) {
1161
+ return candidate;
1162
+ }
1163
+ }
1164
+ }
1165
+ for (const path of [lookup.field, lookup.fieldById]) {
1166
+ if (!path) continue;
1167
+ const wanted = normalizeFieldPath(path);
1168
+ for (const candidate of candidates) {
1169
+ const field = candidate.getAttribute("data-prepr-field");
1170
+ if (!field || normalizeFieldPath(field) !== wanted) continue;
1171
+ if (lookup.id && candidate.getAttribute("data-prepr-id") !== lookup.id) {
1172
+ continue;
1173
+ }
1174
+ return candidate;
1175
+ }
1176
+ }
1177
+ return null;
1178
+ }
1179
+
583
1180
  // src/core/stega/elements.ts
584
1181
  var debug4 = createScopedLogger("stega:elements");
585
1182
  var StegaElements = class {
@@ -669,235 +1266,22 @@ var StegaElements = class {
669
1266
  attributeFilter: ["class", "style", "hidden", "open", "aria-expanded"]
670
1267
  });
671
1268
  }
672
- /** Drops the active class but keeps the data attributes, unlike cleanup(). */
673
- cleanupVisuals() {
674
- document.querySelectorAll(".prepr-overlay-active").forEach((element) => element.classList.remove("prepr-overlay-active"));
675
- }
676
- cleanup() {
1269
+ /**
1270
+ * Stops watching for new content. Tags stay on their elements: the page's
1271
+ * stega characters are stripped once they are read, so a tag removed here
1272
+ * could never be rebuilt when edit mode is turned back on.
1273
+ */
1274
+ disconnect() {
677
1275
  if (this.observer) {
678
1276
  this.observer.disconnect();
679
1277
  this.observer = null;
680
1278
  }
681
- document.querySelectorAll("[data-prepr-encoded]").forEach((element) => {
682
- element.removeAttribute("data-prepr-encoded");
683
- element.removeAttribute("data-prepr-href");
684
- element.removeAttribute("data-prepr-origin");
685
- element.removeAttribute("data-prepr-id");
686
- element.removeAttribute("data-prepr-field");
687
- element.removeAttribute("data-prepr-locale");
688
- element.classList.remove("prepr-overlay-active");
689
- });
690
1279
  this.elements = void 0;
691
1280
  }
692
1281
  };
693
1282
 
694
- // src/core/stega/overlay.ts
695
- var debug5 = createScopedLogger("stega:overlay");
696
- var StegaOverlay = class {
697
- constructor(onEdit) {
698
- this.overlay = null;
699
- this.tooltip = null;
700
- this.hideTimeout = null;
701
- this.currentElement = null;
702
- this.onEdit = onEdit;
703
- }
704
- create() {
705
- const overlay = document.createElement("div");
706
- overlay.className = "prepr-overlay";
707
- overlay.style.display = "none";
708
- const tooltip = document.createElement("div");
709
- tooltip.className = "prepr-tooltip";
710
- tooltip.style.display = "none";
711
- document.body.appendChild(overlay);
712
- document.body.appendChild(tooltip);
713
- this.overlay = overlay;
714
- this.tooltip = tooltip;
715
- return { overlay, tooltip };
716
- }
717
- getTooltip() {
718
- return this.tooltip;
719
- }
720
- hasCurrentElement() {
721
- return this.currentElement !== null;
722
- }
723
- clearHideTimeout() {
724
- if (this.hideTimeout) {
725
- clearTimeout(this.hideTimeout);
726
- this.hideTimeout = null;
727
- }
728
- }
729
- show(element) {
730
- if (!this.overlay || !this.tooltip) return;
731
- this.clearHideTimeout();
732
- if (this.currentElement && this.currentElement !== element) {
733
- this.currentElement.classList.remove("prepr-overlay-active");
734
- }
735
- const rect = element.getBoundingClientRect();
736
- const href = element.getAttribute("data-prepr-href");
737
- const origin = element.getAttribute("data-prepr-origin");
738
- const id = element.getAttribute("data-prepr-id") ?? void 0;
739
- const field = element.getAttribute("data-prepr-field") ?? void 0;
740
- const locale = element.getAttribute("data-prepr-locale") ?? void 0;
741
- debug5.log("showing overlay", { href, origin });
742
- const overlay = this.overlay;
743
- overlay.style.display = "block";
744
- overlay.style.top = `${rect.top + window.scrollY - 2}px`;
745
- overlay.style.left = `${rect.left + window.scrollX - 4}px`;
746
- overlay.style.width = `${rect.width + 8}px`;
747
- overlay.style.height = `${rect.height + 4}px`;
748
- const tooltip = this.tooltip;
749
- if (href && origin) {
750
- const MIN_WIDTH_FOR_TEXT = 80;
751
- const isCompact = rect.width < MIN_WIDTH_FOR_TEXT;
752
- tooltip.textContent = isCompact ? "\u2197" : `${origin} \u2197`;
753
- tooltip.style.display = "block";
754
- tooltip.style.minWidth = isCompact ? "auto" : "80px";
755
- requestAnimationFrame(() => {
756
- let top = rect.top + window.scrollY - tooltip.clientHeight - 2;
757
- let left = rect.right + 4 - tooltip.clientWidth;
758
- const minTop = window.scrollY + 4;
759
- const maxTop = window.scrollY + window.innerHeight - tooltip.clientHeight - 4;
760
- const minLeft = window.scrollX + 4;
761
- const maxLeft = window.scrollX + window.innerWidth - tooltip.clientWidth - 4;
762
- const below = top < minTop;
763
- if (below) {
764
- top = rect.bottom + window.scrollY + 2;
765
- }
766
- top = Math.max(minTop, Math.min(top, maxTop));
767
- left = Math.max(minLeft, Math.min(left, maxLeft));
768
- tooltip.dataset.preprPlacement = below ? "below" : "above";
769
- tooltip.style.top = `${top}px`;
770
- tooltip.style.left = `${left}px`;
771
- });
772
- tooltip.onclick = () => {
773
- this.onEdit({ href, origin, id, field, locale });
774
- };
775
- }
776
- this.currentElement = element;
777
- element.classList.add("prepr-overlay-active");
778
- }
779
- hideImmediate() {
780
- this.clearHideTimeout();
781
- if (this.overlay) this.overlay.style.display = "none";
782
- if (this.tooltip) this.tooltip.style.display = "none";
783
- if (this.currentElement) {
784
- this.currentElement.classList.remove("prepr-overlay-active");
785
- this.currentElement = null;
786
- }
787
- }
788
- hide() {
789
- if (!this.overlay || !this.tooltip) return;
790
- this.clearHideTimeout();
791
- this.hideTimeout = setTimeout(() => this.hideImmediate(), 100);
792
- }
793
- cleanup() {
794
- this.clearHideTimeout();
795
- if (this.overlay) {
796
- this.overlay.style.display = "none";
797
- this.overlay.parentNode?.removeChild(this.overlay);
798
- this.overlay = null;
799
- }
800
- if (this.tooltip) {
801
- this.tooltip.style.display = "none";
802
- this.tooltip.parentNode?.removeChild(this.tooltip);
803
- this.tooltip = null;
804
- }
805
- if (this.currentElement) {
806
- this.currentElement.classList.remove("prepr-overlay-active");
807
- this.currentElement = null;
808
- }
809
- }
810
- };
811
-
812
- // src/core/stega/proximity.ts
813
- var debug6 = createScopedLogger("stega:proximity");
814
- var StegaProximity = class {
815
- constructor() {
816
- this.highlighted = /* @__PURE__ */ new Set();
817
- this.visible = /* @__PURE__ */ new Set();
818
- this.observer = null;
819
- this.getEncodedElements = createElementCache(
820
- "[data-prepr-encoded]",
821
- 200
822
- );
823
- }
824
- /** Call after the encoded element set changes — rebuilds the observer. */
825
- refreshObserving() {
826
- try {
827
- if (typeof IntersectionObserver === "undefined") return;
828
- if (this.observer) {
829
- this.observer.disconnect();
830
- this.observer = null;
831
- }
832
- this.visible = /* @__PURE__ */ new Set();
833
- this.observer = new IntersectionObserver(
834
- (entries) => {
835
- entries.forEach((entry) => {
836
- const el = entry.target;
837
- if (entry.isIntersecting) {
838
- this.visible.add(el);
839
- } else {
840
- this.visible.delete(el);
841
- }
842
- });
843
- },
844
- { root: null, rootMargin: "0px", threshold: 0 }
845
- );
846
- const nodes = this.getEncodedElements();
847
- nodes.forEach((el) => this.observer.observe(el));
848
- debug6.log("observing", nodes.length, "encoded elements");
849
- } catch (error) {
850
- debug6.log("error setting up IntersectionObserver", error);
851
- }
852
- }
853
- stopObserving() {
854
- if (this.observer) {
855
- this.observer.disconnect();
856
- this.observer = null;
857
- this.visible.clear();
858
- }
859
- }
860
- updateElementGradients(cursorX, cursorY) {
861
- const candidates = this.visible.size > 0 ? Array.from(this.visible) : Array.from(this.getEncodedElements());
862
- const newHighlighted = /* @__PURE__ */ new Set();
863
- candidates.forEach((element) => {
864
- const rect = element.getBoundingClientRect();
865
- const distance = Math.min(
866
- Math.abs(cursorX - rect.left),
867
- Math.abs(cursorX - rect.right),
868
- Math.abs(cursorY - rect.top),
869
- Math.abs(cursorY - rect.bottom)
870
- );
871
- if (distance < 150) {
872
- const relativeX = cursorX - rect.left;
873
- const relativeY = cursorY - rect.top;
874
- element.style.setProperty("--cursor-x", `${relativeX}px`);
875
- element.style.setProperty("--cursor-y", `${relativeY}px`);
876
- const baseGradientSize = Math.max(
877
- 150,
878
- Math.max(rect.width, rect.height) * 1.1
879
- );
880
- const distanceScale = Math.max(0, (400 - distance) / 400);
881
- const gradientSize = baseGradientSize * distanceScale;
882
- element.style.setProperty("--gradient-size", `${gradientSize}px`);
883
- element.classList.add("prepr-proximity-highlight");
884
- newHighlighted.add(element);
885
- } else {
886
- element.classList.remove("prepr-proximity-highlight");
887
- }
888
- });
889
- this.highlighted = newHighlighted;
890
- }
891
- clearAllHighlights() {
892
- this.highlighted.forEach(
893
- (element) => element.classList.remove("prepr-proximity-highlight")
894
- );
895
- this.highlighted.clear();
896
- }
897
- };
898
-
899
1283
  // src/core/stega/stega.generated.ts
900
- 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}';
1284
+ var STEGA_STYLES = '@property --prepr-presence{syntax:"<number>";inherits:true;initial-value:1}[data-prepr-encoded]{outline:1px dashed rgba(79,70,229,calc(.3 * var(--prepr-presence,1)));outline-offset:2px}';
901
1285
 
902
1286
  // src/core/stega/styles.ts
903
1287
  var STEGA_STYLE_MARKER = "data-prepr-stega";
@@ -917,46 +1301,80 @@ function removeStegaStyles() {
917
1301
  }
918
1302
 
919
1303
  // src/core/stega/scan.ts
920
- var debug7 = createScopedLogger("stega:scan");
1304
+ var debug5 = createScopedLogger("stega:scan");
1305
+ var ENCODED_SELECTOR = "[data-prepr-encoded]";
1306
+ var PRESENCE_FADE_MS = 400;
921
1307
  var EDITOR_MODE_STYLES = "[data-prepr-encoded]{cursor:pointer}";
1308
+ function fadeInPresence() {
1309
+ const root = document.documentElement;
1310
+ if (typeof root.animate !== "function") return;
1311
+ if (window.matchMedia?.("(prefers-reduced-motion: reduce)").matches) return;
1312
+ root.animate(
1313
+ { "--prepr-presence": ["0", "1"] },
1314
+ { duration: PRESENCE_FADE_MS, easing: "ease-out" }
1315
+ );
1316
+ }
922
1317
  function createStegaController(opts) {
923
1318
  const tooltipEnabled = opts.tooltip ?? true;
924
- const overlay = new StegaOverlay(opts.onEdit);
925
- const proximity = new StegaProximity();
1319
+ const locale = opts.locale ?? "en";
926
1320
  const elements = new StegaElements();
1321
+ const overlay = new VisualEditorOverlay({
1322
+ onLabelClick: (target) => opts.onEdit(readEditPayload(target))
1323
+ });
927
1324
  let initialized = false;
928
- let throttledMouseMove = null;
929
- const handleTooltipMouseEnter = () => {
930
- overlay.clearHideTimeout();
1325
+ let pointerFrame = null;
1326
+ let pointerTarget = null;
1327
+ let pointerX = 0;
1328
+ let pointerY = 0;
1329
+ let hitTestPending = false;
1330
+ const updateHover = () => {
1331
+ pointerFrame = null;
1332
+ if (hitTestPending) {
1333
+ hitTestPending = false;
1334
+ pointerTarget = document.elementFromPoint(pointerX, pointerY);
1335
+ }
1336
+ if (overlay.owns(pointerTarget)) return;
1337
+ const encoded = pointerTarget instanceof Element ? pointerTarget.closest(ENCODED_SELECTOR) : null;
1338
+ overlay.setHover(
1339
+ encoded,
1340
+ encoded ? fieldLabel(encoded.getAttribute("data-prepr-field"), locale) : ""
1341
+ );
931
1342
  };
932
- const handleTooltipMouseLeave = () => {
933
- if (!overlay.hasCurrentElement()) {
934
- overlay.hide();
1343
+ const scheduleHover = () => {
1344
+ if (pointerFrame === null) {
1345
+ pointerFrame = requestAnimationFrame(updateHover);
935
1346
  }
936
1347
  };
1348
+ const handlePointer = (e2) => {
1349
+ pointerTarget = e2.target;
1350
+ pointerX = e2.clientX;
1351
+ pointerY = e2.clientY;
1352
+ hitTestPending = false;
1353
+ scheduleHover();
1354
+ };
1355
+ const handlePointerLeave = () => {
1356
+ pointerTarget = null;
1357
+ hitTestPending = false;
1358
+ overlay.setHover(null, "");
1359
+ };
937
1360
  const handleScroll = () => {
938
- overlay.hideImmediate();
939
- proximity.clearAllHighlights();
1361
+ overlay.onScroll();
1362
+ if (pointerTarget === null) return;
1363
+ hitTestPending = true;
1364
+ scheduleHover();
940
1365
  };
1366
+ const handleResize = () => overlay.schedule();
941
1367
  const handleClick = (e2) => {
942
1368
  const target = e2.target;
943
- const encoded = target?.closest(
944
- "[data-prepr-encoded]"
945
- );
1369
+ const encoded = target?.closest(ENCODED_SELECTOR);
946
1370
  if (!encoded) return;
947
1371
  e2.preventDefault();
948
1372
  e2.stopPropagation();
949
- opts.onEdit({
950
- href: encoded.getAttribute("data-prepr-href") ?? "",
951
- origin: encoded.getAttribute("data-prepr-origin") ?? "",
952
- id: encoded.getAttribute("data-prepr-id") ?? void 0,
953
- field: encoded.getAttribute("data-prepr-field") ?? void 0,
954
- locale: encoded.getAttribute("data-prepr-locale") ?? void 0
955
- });
1373
+ opts.onEdit(readEditPayload(encoded));
956
1374
  };
957
1375
  const start = () => {
958
1376
  if (initialized) {
959
- debug7.log("already initialized, skipping setup");
1377
+ debug5.log("already initialized, skipping setup");
960
1378
  return;
961
1379
  }
962
1380
  if (!tooltipEnabled) {
@@ -966,73 +1384,64 @@ function createStegaController(opts) {
966
1384
  });
967
1385
  document.addEventListener("click", handleClick, { capture: true });
968
1386
  initialized = true;
969
- debug7.log("stega controller started (editor mode)");
1387
+ debug5.log("stega controller started (headless)");
970
1388
  return;
971
1389
  }
972
1390
  injectStegaStyles();
973
- const { tooltip } = overlay.create();
974
- tooltip.addEventListener("mouseenter", handleTooltipMouseEnter);
975
- tooltip.addEventListener("mouseleave", handleTooltipMouseLeave);
1391
+ fadeInPresence();
1392
+ overlay.mount();
976
1393
  elements.scanDocument(true);
977
- proximity.refreshObserving();
978
- elements.setupMutationObserver(() => proximity.refreshObserving());
979
- throttledMouseMove = throttle((e2) => {
980
- const mouseEvent = e2;
981
- const target = mouseEvent.target;
982
- if (!target) return;
983
- if (target.closest(".prepr-tooltip")) return;
984
- proximity.updateElementGradients(mouseEvent.clientX, mouseEvent.clientY);
985
- const encoded = target.closest("[data-prepr-encoded]");
986
- if (encoded) {
987
- overlay.show(encoded);
988
- } else {
989
- overlay.hide();
990
- }
991
- }, 16);
992
- document.addEventListener("mousemove", throttledMouseMove, {
993
- passive: true
1394
+ elements.setupMutationObserver(() => {
1395
+ if (pointerTarget === null) return;
1396
+ hitTestPending = true;
1397
+ scheduleHover();
994
1398
  });
1399
+ document.addEventListener("pointermove", handlePointer, { passive: true });
1400
+ document.addEventListener("pointerdown", handlePointer, { passive: true });
1401
+ document.documentElement.addEventListener(
1402
+ "pointerleave",
1403
+ handlePointerLeave
1404
+ );
995
1405
  window.addEventListener("scroll", handleScroll, {
996
1406
  capture: true,
997
1407
  passive: true
998
1408
  });
1409
+ window.addEventListener("resize", handleResize, { passive: true });
999
1410
  initialized = true;
1000
- debug7.log("stega controller started");
1411
+ debug5.log("stega controller started");
1001
1412
  };
1002
1413
  const stop = () => {
1003
1414
  if (!initialized) return;
1415
+ initialized = false;
1004
1416
  if (!tooltipEnabled) {
1005
1417
  document.removeEventListener("click", handleClick, { capture: true });
1006
- elements.cleanup();
1418
+ elements.disconnect();
1007
1419
  removeStegaStyles();
1008
- initialized = false;
1009
- debug7.log("stega controller stopped (editor mode)");
1420
+ debug5.log("stega controller stopped (headless)");
1010
1421
  return;
1011
1422
  }
1012
- if (throttledMouseMove) {
1013
- document.removeEventListener("mousemove", throttledMouseMove);
1014
- throttledMouseMove.cancel();
1015
- throttledMouseMove = null;
1016
- }
1423
+ if (pointerFrame !== null) cancelAnimationFrame(pointerFrame);
1424
+ pointerFrame = null;
1425
+ pointerTarget = null;
1426
+ hitTestPending = false;
1427
+ document.removeEventListener("pointermove", handlePointer);
1428
+ document.removeEventListener("pointerdown", handlePointer);
1429
+ document.documentElement.removeEventListener(
1430
+ "pointerleave",
1431
+ handlePointerLeave
1432
+ );
1017
1433
  window.removeEventListener("scroll", handleScroll, { capture: true });
1018
- const tooltip = overlay.getTooltip();
1019
- if (tooltip) {
1020
- tooltip.removeEventListener("mouseenter", handleTooltipMouseEnter);
1021
- tooltip.removeEventListener("mouseleave", handleTooltipMouseLeave);
1022
- }
1023
- overlay.cleanup();
1024
- proximity.clearAllHighlights();
1025
- proximity.stopObserving();
1026
- elements.cleanup();
1434
+ window.removeEventListener("resize", handleResize);
1435
+ overlay.unmount();
1436
+ elements.disconnect();
1027
1437
  removeStegaStyles();
1028
- initialized = false;
1029
- debug7.log("stega controller stopped");
1438
+ debug5.log("stega controller stopped");
1030
1439
  };
1031
1440
  return { start, stop };
1032
1441
  }
1033
1442
 
1034
1443
  // src/core/stega/scroll-to-field.ts
1035
- var debug8 = createScopedLogger("stega:scroll");
1444
+ var debug6 = createScopedLogger("stega:scroll");
1036
1445
  var FLASH_DURATION_MS = 1200;
1037
1446
  var flashTimeout = null;
1038
1447
  function scrollToField(request) {
@@ -1049,7 +1458,7 @@ function scrollToField(request) {
1049
1458
  break;
1050
1459
  }
1051
1460
  if (!target) {
1052
- debug8.log("no element found for field", field);
1461
+ debug6.log("no element found for field", field);
1053
1462
  return false;
1054
1463
  }
1055
1464
  target.scrollIntoView({ behavior: "smooth", block: "center" });
@@ -1479,7 +1888,7 @@ var CLOSE_ICON_SVG = `<svg width="10" height="10" viewBox="0 0 10 10" fill="none
1479
1888
  </svg>`;
1480
1889
 
1481
1890
  // src/core/ui/toolbar.generated.ts
1482
- 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}}";
1891
+ 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}@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}}";
1483
1892
 
1484
1893
  // ../../node_modules/.pnpm/preact@10.29.7/node_modules/preact/jsx-runtime/dist/jsxRuntime.mjs
1485
1894
  var f2 = 0;
@@ -2174,6 +2583,724 @@ function definePreprToolbar() {
2174
2583
  }
2175
2584
  }
2176
2585
 
2586
+ // src/core/visual-editor/asset-images.ts
2587
+ var MAX_URL_LENGTH = 4096;
2588
+ var MIN_KEY_SEGMENTS = 2;
2589
+ var TRANSFORM_OPTION = /^[a-z]{1,3}_[\w.-]*$/i;
2590
+ var FORMAT_PLACEHOLDER = "{format}";
2591
+ var PROXY_PARAMS = ["url", "href", "src", "image"];
2592
+ var ABSOLUTE_HTTP = /^https?:\/\//i;
2593
+ var SOURCE_URL_ATTRIBUTES = ["src", "data-src"];
2594
+ var SOURCE_SET_ATTRIBUTES = ["srcset", "data-srcset"];
2595
+ var ASSET_ATTR = "data-prepr-asset";
2596
+ var ASSET_SOURCE_ATTRIBUTES = [
2597
+ ...SOURCE_URL_ATTRIBUTES,
2598
+ ...SOURCE_SET_ATTRIBUTES
2599
+ ];
2600
+ var TAG_ATTRIBUTES = [
2601
+ "data-prepr-encoded",
2602
+ "data-prepr-id",
2603
+ "data-prepr-field",
2604
+ "data-prepr-entry-id",
2605
+ ASSET_ATTR
2606
+ ];
2607
+ function decodeSegment(segment) {
2608
+ try {
2609
+ return decodeURIComponent(segment);
2610
+ } catch {
2611
+ return segment;
2612
+ }
2613
+ }
2614
+ function isTransformSegment(segment) {
2615
+ return segment === FORMAT_PLACEHOLDER || segment.split(",").every((option) => TRANSFORM_OPTION.test(option));
2616
+ }
2617
+ function assetKey(url, depth = 0) {
2618
+ if (!url || url.length > MAX_URL_LENGTH) return null;
2619
+ let parsed;
2620
+ try {
2621
+ parsed = new URL(url, "https://base.invalid");
2622
+ } catch {
2623
+ return null;
2624
+ }
2625
+ if (parsed.protocol !== "https:" && parsed.protocol !== "http:") return null;
2626
+ if (depth === 0) {
2627
+ for (const name of PROXY_PARAMS) {
2628
+ const inner = parsed.searchParams.get(name);
2629
+ if (inner && ABSOLUTE_HTTP.test(inner)) return assetKey(inner, 1);
2630
+ }
2631
+ }
2632
+ const segments = parsed.pathname.split("/").filter(Boolean).map(decodeSegment);
2633
+ const file = segments.pop();
2634
+ if (!file) return null;
2635
+ const kept = segments.filter((segment) => !isTransformSegment(segment));
2636
+ kept.push(file);
2637
+ return kept.length >= MIN_KEY_SEGMENTS ? kept.join("/") : null;
2638
+ }
2639
+ function srcsetUrls(srcset) {
2640
+ const urls = [];
2641
+ let expectUrl = true;
2642
+ for (const token of srcset.split(/\s+/)) {
2643
+ if (!token) continue;
2644
+ if (!expectUrl) {
2645
+ const comma = token.indexOf(",");
2646
+ if (comma === -1) continue;
2647
+ expectUrl = true;
2648
+ const rest = token.slice(comma + 1);
2649
+ if (!rest) continue;
2650
+ urls.push(rest.replace(/,+$/, ""));
2651
+ expectUrl = rest.endsWith(",");
2652
+ continue;
2653
+ }
2654
+ const url = token.replace(/^,+|,+$/g, "");
2655
+ if (url) urls.push(url);
2656
+ expectUrl = token.endsWith(",");
2657
+ }
2658
+ return urls;
2659
+ }
2660
+ function createAssetIndex(assets) {
2661
+ const index = /* @__PURE__ */ new Map();
2662
+ for (const asset of assets) {
2663
+ const key = assetKey(asset.key);
2664
+ if (key && !index.has(key)) index.set(key, asset);
2665
+ }
2666
+ return index;
2667
+ }
2668
+ function urlsOf(img) {
2669
+ const urls = [];
2670
+ const collect = (el) => {
2671
+ for (const name of SOURCE_URL_ATTRIBUTES) {
2672
+ const value = el.getAttribute(name);
2673
+ if (value) urls.push(value);
2674
+ }
2675
+ for (const name of SOURCE_SET_ATTRIBUTES) {
2676
+ const value = el.getAttribute(name);
2677
+ if (value) urls.push(...srcsetUrls(value));
2678
+ }
2679
+ };
2680
+ collect(img);
2681
+ const picture = img.parentElement;
2682
+ if (picture?.tagName === "PICTURE") {
2683
+ for (const source of picture.children) {
2684
+ if (source.tagName === "SOURCE") collect(source);
2685
+ }
2686
+ }
2687
+ return urls;
2688
+ }
2689
+ function matchAsset(img, index, keys) {
2690
+ for (const url of urlsOf(img)) {
2691
+ let key = keys.get(url);
2692
+ if (key === void 0) {
2693
+ key = assetKey(url);
2694
+ keys.set(url, key);
2695
+ }
2696
+ const asset = key ? index.get(key) : void 0;
2697
+ if (asset) return asset;
2698
+ }
2699
+ return void 0;
2700
+ }
2701
+ function setIfChanged(el, name, value) {
2702
+ if (value === null) {
2703
+ if (el.hasAttribute(name)) el.removeAttribute(name);
2704
+ } else if (el.getAttribute(name) !== value) {
2705
+ el.setAttribute(name, value);
2706
+ }
2707
+ }
2708
+ function untagAssetImage(img) {
2709
+ for (const name of TAG_ATTRIBUTES) img.removeAttribute(name);
2710
+ }
2711
+ function tagAssetImages(index, itemId, keys) {
2712
+ for (const img of document.getElementsByTagName("img")) {
2713
+ const ours = img.hasAttribute(ASSET_ATTR);
2714
+ if (!ours && img.hasAttribute("data-prepr-encoded")) continue;
2715
+ const asset = index.size ? matchAsset(img, index, keys) : void 0;
2716
+ if (!asset) {
2717
+ if (ours) untagAssetImage(img);
2718
+ continue;
2719
+ }
2720
+ setIfChanged(img, "data-prepr-encoded", "");
2721
+ setIfChanged(img, ASSET_ATTR, "");
2722
+ setIfChanged(img, "data-prepr-id", itemId);
2723
+ setIfChanged(img, "data-prepr-field", asset.field);
2724
+ setIfChanged(img, "data-prepr-entry-id", asset.entryId ?? null);
2725
+ }
2726
+ }
2727
+ function untagAssetImages() {
2728
+ document.querySelectorAll(`img[${ASSET_ATTR}]`).forEach(untagAssetImage);
2729
+ }
2730
+
2731
+ // src/core/visual-editor/navigation-guard.ts
2732
+ var debug7 = createScopedLogger("navigation-guard");
2733
+ var trimSlash = (path) => path.length > 1 && path.endsWith("/") ? path.slice(0, -1) : path;
2734
+ function resolve(url) {
2735
+ try {
2736
+ return new URL(url, document.baseURI);
2737
+ } catch {
2738
+ return null;
2739
+ }
2740
+ }
2741
+ function leavesPage(url) {
2742
+ if (url.protocol !== "http:" && url.protocol !== "https:") return false;
2743
+ const here = window.location;
2744
+ return url.origin !== here.origin || trimSlash(url.pathname) !== trimSlash(here.pathname);
2745
+ }
2746
+ function leavingLinkHref(target) {
2747
+ if (!(target instanceof Element)) return null;
2748
+ const link = target.closest("a[href]");
2749
+ if (!link || link.hasAttribute("download")) return null;
2750
+ const linkTarget = link.getAttribute("target");
2751
+ if (linkTarget && linkTarget !== "_self") return null;
2752
+ const href = link.getAttribute("href") ?? "";
2753
+ if (href.startsWith("#")) return null;
2754
+ const url = resolve(href);
2755
+ return url && leavesPage(url) ? url.href : null;
2756
+ }
2757
+ function submitterAttr(submitter, attr) {
2758
+ if (!submitter?.hasAttribute(attr)) return null;
2759
+ if (attr === "formaction") return submitter.formAction;
2760
+ if (attr === "formmethod") return submitter.formMethod;
2761
+ return submitter.formTarget;
2762
+ }
2763
+ function submissionUrl(form, submitter) {
2764
+ const method = (submitterAttr(submitter, "formmethod") ?? form.method).toLowerCase();
2765
+ if (method === "dialog") return null;
2766
+ const target = submitterAttr(submitter, "formtarget") ?? form.target;
2767
+ if (target && target !== "_self") return null;
2768
+ const url = resolve(
2769
+ submitterAttr(submitter, "formaction") ?? (form.action || location.href)
2770
+ );
2771
+ if (!url) return null;
2772
+ if (method === "get") {
2773
+ let data;
2774
+ try {
2775
+ data = new FormData(form, submitter);
2776
+ } catch {
2777
+ data = new FormData(form);
2778
+ }
2779
+ const query = new URLSearchParams();
2780
+ data.forEach((value, key) => {
2781
+ query.append(key, typeof value === "string" ? value : value.name);
2782
+ });
2783
+ url.search = query.toString();
2784
+ }
2785
+ return url;
2786
+ }
2787
+ function createNavigationGuard(opts) {
2788
+ let active = false;
2789
+ let permitted = null;
2790
+ let pendingForm = null;
2791
+ let originalPush = null;
2792
+ let guardedPush = null;
2793
+ const isPermitted = (url) => permitted === url.href;
2794
+ const guarding = () => active && opts.showsEditedContent();
2795
+ const report = (href) => {
2796
+ debug7.log("blocked navigation to", href);
2797
+ opts.onBlocked(href);
2798
+ };
2799
+ const onSubmit = (e2) => {
2800
+ if (e2.defaultPrevented || !(e2.target instanceof HTMLFormElement)) return;
2801
+ if (!guarding()) return;
2802
+ const submitter = e2.submitter ?? null;
2803
+ const url = submissionUrl(e2.target, submitter);
2804
+ if (!url || !leavesPage(url) || isPermitted(url)) return;
2805
+ e2.preventDefault();
2806
+ pendingForm = { form: e2.target, submitter, href: url.href };
2807
+ report(url.href);
2808
+ };
2809
+ const onNavigate = (event) => {
2810
+ const e2 = event;
2811
+ if (e2.navigationType === "traverse" || e2.navigationType === "reload") {
2812
+ return;
2813
+ }
2814
+ if (!e2.cancelable || e2.hashChange || e2.downloadRequest !== null) return;
2815
+ if (!guarding()) return;
2816
+ if (e2.navigationType === "replace" && e2.destination.sameDocument) return;
2817
+ const url = resolve(e2.destination.url);
2818
+ if (!url || !leavesPage(url) || isPermitted(url)) return;
2819
+ e2.preventDefault();
2820
+ report(url.href);
2821
+ if (e2.destination.sameDocument) window.location.reload();
2822
+ };
2823
+ const navigation = () => window.navigation;
2824
+ function installPushGuard() {
2825
+ const original = history.pushState;
2826
+ const guard = function(data, unused, url) {
2827
+ if (guarding() && guardedPush === guard && url != null) {
2828
+ const target = resolve(url);
2829
+ if (target && leavesPage(target) && !isPermitted(target)) {
2830
+ report(target.href);
2831
+ window.location.reload();
2832
+ return;
2833
+ }
2834
+ }
2835
+ return original.call(this, data, unused, url);
2836
+ };
2837
+ originalPush = original;
2838
+ guardedPush = guard;
2839
+ history.pushState = guard;
2840
+ }
2841
+ function uninstallPushGuard() {
2842
+ if (history.pushState === guardedPush && originalPush) {
2843
+ history.pushState = originalPush;
2844
+ }
2845
+ originalPush = null;
2846
+ guardedPush = null;
2847
+ }
2848
+ return {
2849
+ start() {
2850
+ if (active) return;
2851
+ active = true;
2852
+ installPushGuard();
2853
+ window.addEventListener("submit", onSubmit);
2854
+ navigation()?.addEventListener("navigate", onNavigate);
2855
+ },
2856
+ stop() {
2857
+ if (!active) return;
2858
+ active = false;
2859
+ uninstallPushGuard();
2860
+ window.removeEventListener("submit", onSubmit);
2861
+ navigation()?.removeEventListener("navigate", onNavigate);
2862
+ pendingForm = null;
2863
+ },
2864
+ isActive: guarding,
2865
+ block(href) {
2866
+ report(href);
2867
+ },
2868
+ permit(href) {
2869
+ permitted = resolve(href)?.href ?? null;
2870
+ },
2871
+ allow(href) {
2872
+ const url = resolve(href);
2873
+ if (!url || url.protocol !== "http:" && url.protocol !== "https:") {
2874
+ debug7.warn("ignored navigation to unsupported URL");
2875
+ return;
2876
+ }
2877
+ permitted = url.href;
2878
+ const form = pendingForm;
2879
+ pendingForm = null;
2880
+ if (form && form.href === url.href && form.form.isConnected) {
2881
+ try {
2882
+ form.form.requestSubmit(form.submitter ?? void 0);
2883
+ } catch {
2884
+ form.form.requestSubmit();
2885
+ }
2886
+ return;
2887
+ }
2888
+ window.location.assign(url.href);
2889
+ }
2890
+ };
2891
+ }
2892
+
2893
+ // src/core/visual-editor/index.ts
2894
+ var debug8 = createScopedLogger("visual-editor");
2895
+ var ENCODED_SELECTOR2 = "[data-prepr-encoded]";
2896
+ var REFERENCE_ATTR = "data-prepr-reference";
2897
+ var REFERENCE_SELECTOR = `[${REFERENCE_ATTR}]`;
2898
+ var CURSOR_STYLE_MARKER = "data-prepr-visual-editor";
2899
+ var CURSOR_STYLES = `${ENCODED_SELECTOR2}{cursor:pointer}html[data-prepr-ve-suspended] ${ENCODED_SELECTOR2}{cursor:auto}`;
2900
+ var SUSPENDED_ATTR = "data-prepr-ve-suspended";
2901
+ var FIND_RETRY_MS = 150;
2902
+ var FIND_RETRIES = 6;
2903
+ var MAX_CACHED_KEYS = 2e3;
2904
+ var LAYOUT_MUTATIONS = {
2905
+ childList: true,
2906
+ subtree: true,
2907
+ characterData: true,
2908
+ attributes: true,
2909
+ attributeFilter: [
2910
+ "class",
2911
+ "style",
2912
+ "hidden",
2913
+ "open",
2914
+ "data-prepr-encoded",
2915
+ // An image swapped by a carousel or lazy loader can show another asset.
2916
+ ...ASSET_SOURCE_ATTRIBUTES
2917
+ ]
2918
+ };
2919
+ function scrollIntoFrame(el) {
2920
+ const rect = el.getBoundingClientRect();
2921
+ const offset = Math.max(0, (window.innerHeight - rect.height) / 2);
2922
+ window.scrollTo({
2923
+ top: Math.max(0, window.scrollY + rect.top - offset),
2924
+ behavior: "smooth"
2925
+ });
2926
+ }
2927
+ var isVisible = (el) => typeof el.checkVisibility === "function" ? el.checkVisibility() : el.getClientRects().length > 0;
2928
+ function createVisualEditor(opts) {
2929
+ const overlay = new VisualEditorOverlay();
2930
+ let running = false;
2931
+ let currentItemId = null;
2932
+ let locale = opts.locale ?? "en";
2933
+ let suspended = false;
2934
+ let pointerFrame = null;
2935
+ let lastPointerTarget = null;
2936
+ let lastPointerX = 0;
2937
+ let lastPointerY = 0;
2938
+ let mutationFrame = null;
2939
+ let mutationObserver = null;
2940
+ let selection = null;
2941
+ let findTimeout = null;
2942
+ let settlePendingFind = null;
2943
+ let guardEnabled = false;
2944
+ let assetIndex = /* @__PURE__ */ new Map();
2945
+ const assetKeys = /* @__PURE__ */ new Map();
2946
+ const guard = createNavigationGuard({
2947
+ onBlocked: (href) => opts.onNavigationBlocked?.(href),
2948
+ showsEditedContent: () => showsEditedContent()
2949
+ });
2950
+ const encodedFrom = (node) => node instanceof Element ? node.closest(ENCODED_SELECTOR2) : null;
2951
+ const labelFor = (el) => fieldLabel(el.getAttribute("data-prepr-field"), locale);
2952
+ let references = null;
2953
+ let ownComponents = /* @__PURE__ */ new Set();
2954
+ const isCurrentItem = (el) => {
2955
+ if (currentItemId === null) return true;
2956
+ const id = el.getAttribute("data-prepr-id");
2957
+ return !id || id === currentItemId || ownComponents.has(id);
2958
+ };
2959
+ const showsEditedContent = () => {
2960
+ for (const el of document.querySelectorAll(ENCODED_SELECTOR2)) {
2961
+ if (isCurrentItem(el) && isVisible(el)) return true;
2962
+ }
2963
+ return false;
2964
+ };
2965
+ const linkedIdOf = (el) => {
2966
+ if (!references) return null;
2967
+ const id = el.getAttribute("data-prepr-id") ?? "";
2968
+ if (references.has(id)) return id;
2969
+ let container = el.closest(REFERENCE_SELECTOR);
2970
+ while (container) {
2971
+ const linked = container.getAttribute(REFERENCE_ATTR) ?? "";
2972
+ if (references.has(linked)) return linked;
2973
+ container = container.parentElement?.closest(REFERENCE_SELECTOR) ?? null;
2974
+ }
2975
+ return null;
2976
+ };
2977
+ const itemKind = (el) => {
2978
+ if (isCurrentItem(el)) return "current";
2979
+ if (!references) return "other";
2980
+ return linkedIdOf(el) ? "referenced" : null;
2981
+ };
2982
+ const referenceFieldOf = (el) => {
2983
+ const linked = linkedIdOf(el);
2984
+ return linked ? references?.get(linked) : void 0;
2985
+ };
2986
+ const hoverLabel = (el, kind) => {
2987
+ if (kind === "other") {
2988
+ return `${labelFor(el)} \xB7 ${t("visualEditor.otherItem", locale)}`;
2989
+ }
2990
+ if (kind === "referenced") {
2991
+ const via = referenceFieldOf(el);
2992
+ const prefix = via ? `${fieldLabel(via, locale)} \u203A ` : "";
2993
+ return `${prefix}${labelFor(el)} \xB7 ${t("visualEditor.linkedItem", locale)}`;
2994
+ }
2995
+ return labelFor(el);
2996
+ };
2997
+ const setSuspended = (value) => {
2998
+ if (suspended === value) return;
2999
+ suspended = value;
3000
+ document.documentElement.toggleAttribute(SUSPENDED_ATTR, value);
3001
+ if (value) overlay.setHover(null, "");
3002
+ };
3003
+ const updateHover = () => {
3004
+ pointerFrame = null;
3005
+ overlay.schedule();
3006
+ if (suspended) return;
3007
+ const encoded = encodedFrom(lastPointerTarget);
3008
+ const target = encoded === overlay.getSelectedTarget() ? null : encoded;
3009
+ if (!target) {
3010
+ overlay.setHover(null, "");
3011
+ return;
3012
+ }
3013
+ const kind = itemKind(target);
3014
+ if (!kind) {
3015
+ overlay.setHover(null, "");
3016
+ return;
3017
+ }
3018
+ overlay.setHover(target, hoverLabel(target, kind), { kind });
3019
+ };
3020
+ const handlePointerMove = (e2) => {
3021
+ setSuspended(e2.altKey);
3022
+ lastPointerTarget = e2.target;
3023
+ lastPointerX = e2.clientX;
3024
+ lastPointerY = e2.clientY;
3025
+ if (pointerFrame === null) {
3026
+ pointerFrame = requestAnimationFrame(updateHover);
3027
+ }
3028
+ };
3029
+ const handlePointerLeave = () => {
3030
+ lastPointerTarget = null;
3031
+ overlay.setHover(null, "");
3032
+ };
3033
+ const handleClick = (e2) => {
3034
+ if (e2.altKey) {
3035
+ const href = leavingLinkHref(e2.target);
3036
+ if (href && guard.isActive()) guard.permit(href);
3037
+ return;
3038
+ }
3039
+ const encoded = encodedFrom(e2.target);
3040
+ const kind = encoded ? itemKind(encoded) : null;
3041
+ if (!encoded || !kind) {
3042
+ clearSelection();
3043
+ if (e2.ctrlKey || e2.metaKey || e2.shiftKey || !showsEditedContent()) return;
3044
+ const href = leavingLinkHref(e2.target);
3045
+ if (!href) return;
3046
+ e2.preventDefault();
3047
+ if (guard.isActive()) {
3048
+ e2.stopPropagation();
3049
+ guard.block(href);
3050
+ }
3051
+ return;
3052
+ }
3053
+ e2.preventDefault();
3054
+ e2.stopPropagation();
3055
+ const item = listItemIn(encoded, e2.target);
3056
+ if (kind === "current")
3057
+ select(item?.element ?? encoded, encoded, item?.index);
3058
+ const referenceField = kind === "referenced" ? referenceFieldOf(encoded) : void 0;
3059
+ const linkedId = kind === "referenced" ? linkedIdOf(encoded) : null;
3060
+ opts.onEdit({
3061
+ ...readEditPayload(encoded),
3062
+ ...linkedId && { id: linkedId },
3063
+ ...item && { block: item.index },
3064
+ ...referenceField && { referenceField }
3065
+ });
3066
+ };
3067
+ const handleKey = (e2) => {
3068
+ if (e2.key === "Alt") setSuspended(e2.type === "keydown");
3069
+ if (e2.type === "keydown" && e2.key === "Escape") {
3070
+ clearSelection();
3071
+ }
3072
+ };
3073
+ const handleBlur = () => setSuspended(false);
3074
+ const handleScroll = () => overlay.onScroll();
3075
+ const handleResize = () => overlay.schedule();
3076
+ const handleSettled = () => overlay.schedule();
3077
+ function listItemIn(encoded, target) {
3078
+ if (!(target instanceof Element)) return null;
3079
+ const li = target.closest("li");
3080
+ if (!li || !encoded.contains(li) || !li.parentElement) return null;
3081
+ const items = Array.from(li.parentElement.children).filter(
3082
+ (child) => child.tagName === "LI"
3083
+ );
3084
+ return { element: li, index: items.indexOf(li) };
3085
+ }
3086
+ function nthListItem(encoded, block) {
3087
+ const list = encoded.querySelector("ul, ol");
3088
+ if (!list) return null;
3089
+ const items = Array.from(list.children).filter(
3090
+ (child) => child.tagName === "LI"
3091
+ );
3092
+ return items[block] ?? null;
3093
+ }
3094
+ function select(el, field, block) {
3095
+ const entryId = field.getAttribute("data-prepr-entry-id");
3096
+ selection = {
3097
+ lookup: {
3098
+ field: field.getAttribute("data-prepr-field") ?? "",
3099
+ id: field.getAttribute("data-prepr-id") ?? void 0,
3100
+ ...entryId && { entryId },
3101
+ ...block !== void 0 && { block }
3102
+ },
3103
+ label: field
3104
+ };
3105
+ overlay.setSelected(el, labelFor(field));
3106
+ if (overlay.getHoverTarget() === el) overlay.setHover(null, "");
3107
+ }
3108
+ function clearSelection() {
3109
+ selection = null;
3110
+ overlay.setSelected(null, "");
3111
+ }
3112
+ function reselect() {
3113
+ if (!selection) return;
3114
+ const current = overlay.getSelectedTarget();
3115
+ if (current?.isConnected && selection.label.isConnected) return;
3116
+ const found = findFieldElement(selection.lookup);
3117
+ if (!found || !isCurrentItem(found)) {
3118
+ overlay.setSelected(null, "");
3119
+ return;
3120
+ }
3121
+ const { block } = selection.lookup;
3122
+ const el = block !== void 0 ? nthListItem(found, block) ?? found : found;
3123
+ select(el, found, block);
3124
+ }
3125
+ function tagAssets() {
3126
+ if (assetKeys.size > MAX_CACHED_KEYS) assetKeys.clear();
3127
+ tagAssetImages(assetIndex, currentItemId, assetKeys);
3128
+ }
3129
+ const handleLayoutFrame = () => {
3130
+ mutationFrame = null;
3131
+ if (assetIndex.size) tagAssets();
3132
+ reselect();
3133
+ const hovered = overlay.getHoverTarget();
3134
+ if (hovered && !hovered.isConnected) {
3135
+ lastPointerTarget = document.elementFromPoint(lastPointerX, lastPointerY);
3136
+ updateHover();
3137
+ return;
3138
+ }
3139
+ overlay.schedule();
3140
+ };
3141
+ const handleMutations = () => {
3142
+ if (mutationFrame !== null) return;
3143
+ if (!selection && !overlay.getHoverTarget() && !assetIndex.size) return;
3144
+ mutationFrame = requestAnimationFrame(handleLayoutFrame);
3145
+ };
3146
+ function injectCursorStyles() {
3147
+ if (document.querySelector(`style[${CURSOR_STYLE_MARKER}]`)) return;
3148
+ const style = document.createElement("style");
3149
+ style.setAttribute(CURSOR_STYLE_MARKER, "");
3150
+ style.textContent = CURSOR_STYLES;
3151
+ document.head.appendChild(style);
3152
+ }
3153
+ function start() {
3154
+ if (running) return;
3155
+ running = true;
3156
+ overlay.mount();
3157
+ injectCursorStyles();
3158
+ document.addEventListener("pointermove", handlePointerMove, {
3159
+ passive: true
3160
+ });
3161
+ document.documentElement.addEventListener(
3162
+ "pointerleave",
3163
+ handlePointerLeave
3164
+ );
3165
+ document.addEventListener("click", handleClick, { capture: true });
3166
+ window.addEventListener("keydown", handleKey);
3167
+ window.addEventListener("keyup", handleKey);
3168
+ window.addEventListener("blur", handleBlur);
3169
+ window.addEventListener("scroll", handleScroll, {
3170
+ capture: true,
3171
+ passive: true
3172
+ });
3173
+ window.addEventListener("resize", handleResize, { passive: true });
3174
+ document.addEventListener("transitionend", handleSettled, {
3175
+ capture: true,
3176
+ passive: true
3177
+ });
3178
+ document.addEventListener("animationend", handleSettled, {
3179
+ capture: true,
3180
+ passive: true
3181
+ });
3182
+ mutationObserver = new MutationObserver(handleMutations);
3183
+ mutationObserver.observe(document.body, LAYOUT_MUTATIONS);
3184
+ if (assetIndex.size) tagAssets();
3185
+ syncGuard();
3186
+ debug8.log("visual editor started");
3187
+ }
3188
+ function stop() {
3189
+ if (!running) return;
3190
+ running = false;
3191
+ if (pointerFrame !== null) cancelAnimationFrame(pointerFrame);
3192
+ if (mutationFrame !== null) cancelAnimationFrame(mutationFrame);
3193
+ mutationObserver?.disconnect();
3194
+ mutationObserver = null;
3195
+ cancelFind();
3196
+ pointerFrame = null;
3197
+ mutationFrame = null;
3198
+ selection = null;
3199
+ lastPointerTarget = null;
3200
+ setSuspended(false);
3201
+ document.removeEventListener("pointermove", handlePointerMove);
3202
+ document.documentElement.removeEventListener(
3203
+ "pointerleave",
3204
+ handlePointerLeave
3205
+ );
3206
+ document.removeEventListener("click", handleClick, { capture: true });
3207
+ window.removeEventListener("keydown", handleKey);
3208
+ window.removeEventListener("keyup", handleKey);
3209
+ window.removeEventListener("blur", handleBlur);
3210
+ window.removeEventListener("scroll", handleScroll, { capture: true });
3211
+ window.removeEventListener("resize", handleResize);
3212
+ document.removeEventListener("transitionend", handleSettled, {
3213
+ capture: true
3214
+ });
3215
+ document.removeEventListener("animationend", handleSettled, {
3216
+ capture: true
3217
+ });
3218
+ document.querySelectorAll(`style[${CURSOR_STYLE_MARKER}]`).forEach((node) => node.remove());
3219
+ overlay.unmount();
3220
+ untagAssetImages();
3221
+ guard.stop();
3222
+ debug8.log("visual editor stopped");
3223
+ }
3224
+ function cancelFind() {
3225
+ if (findTimeout) clearTimeout(findTimeout);
3226
+ findTimeout = null;
3227
+ settlePendingFind?.();
3228
+ settlePendingFind = null;
3229
+ }
3230
+ function focusField(lookup) {
3231
+ cancelFind();
3232
+ return new Promise((resolve2) => {
3233
+ let attempt = 0;
3234
+ const finish = (found) => {
3235
+ settlePendingFind = null;
3236
+ resolve2(found);
3237
+ };
3238
+ settlePendingFind = () => resolve2(false);
3239
+ const tryFind = () => {
3240
+ findTimeout = null;
3241
+ const found = findFieldElement(lookup);
3242
+ if (found) {
3243
+ const el = lookup.block !== void 0 ? nthListItem(found, lookup.block) ?? found : found;
3244
+ scrollIntoFrame(el);
3245
+ if (running && isCurrentItem(found)) select(el, found, lookup.block);
3246
+ finish(true);
3247
+ return;
3248
+ }
3249
+ if (++attempt >= FIND_RETRIES) {
3250
+ debug8.log("no element renders field", lookup.field);
3251
+ finish(false);
3252
+ return;
3253
+ }
3254
+ findTimeout = setTimeout(tryFind, FIND_RETRY_MS);
3255
+ };
3256
+ tryFind();
3257
+ });
3258
+ }
3259
+ function setCurrentItem(itemId, referenceList, componentIds) {
3260
+ currentItemId = itemId;
3261
+ if (referenceList) setReferences(referenceList, componentIds);
3262
+ else if (componentIds) ownComponents = new Set(componentIds);
3263
+ if (running && assetIndex.size) tagAssets();
3264
+ const selected = overlay.getSelectedTarget();
3265
+ const owner = selected ? encodedFrom(selected) : null;
3266
+ if (owner && !isCurrentItem(owner)) clearSelection();
3267
+ }
3268
+ function setAssets(assets) {
3269
+ const hadAssets = assetIndex.size > 0;
3270
+ assetIndex = createAssetIndex(assets);
3271
+ if (running && (hadAssets || assetIndex.size)) tagAssets();
3272
+ }
3273
+ function setReferences(referenceList, componentIds) {
3274
+ if (componentIds) ownComponents = new Set(componentIds);
3275
+ references = /* @__PURE__ */ new Map();
3276
+ for (const { id, field } of referenceList) {
3277
+ if (!references.has(id)) references.set(id, field);
3278
+ }
3279
+ }
3280
+ function setLocale(value) {
3281
+ locale = value;
3282
+ }
3283
+ function syncGuard() {
3284
+ if (running && guardEnabled) guard.start();
3285
+ else guard.stop();
3286
+ }
3287
+ function setNavigationGuard(enabled) {
3288
+ guardEnabled = enabled;
3289
+ syncGuard();
3290
+ }
3291
+ return {
3292
+ start,
3293
+ stop,
3294
+ setCurrentItem,
3295
+ setReferences,
3296
+ setAssets,
3297
+ setLocale,
3298
+ focusField,
3299
+ setNavigationGuard,
3300
+ allowNavigation: (href) => guard.allow(href)
3301
+ };
3302
+ }
3303
+
2177
3304
  // src/core/create-preview.ts
2178
3305
  var debug9 = createScopedLogger("create-preview");
2179
3306
  var controllerStores = /* @__PURE__ */ new WeakMap();
@@ -2246,29 +3373,45 @@ function createPreprPreview(opts = {}) {
2246
3373
  el.connect(store, (key) => t(key, locale));
2247
3374
  }
2248
3375
  const editingEnabled = features.editMode || isIframe;
2249
- const stega = createStegaController({
2250
- // The CMS deep-link tooltip is chrome, so it follows the same decision as
2251
- // the bar: noise inside the editor (clicking the element itself requests
2252
- // the edit there), and unwanted in a headless preview that asked for no UI.
2253
- tooltip: mountUi,
2254
- // In the editor, ask the parent to focus the field instead of opening a new
2255
- // tab. Standalone previews keep the window.open behaviour.
2256
- onEdit: ({ href, id, field, locale: contentLocale }) => {
2257
- const safeHref = href ? safeEditUrl(href) : null;
2258
- if (href && !safeHref) {
2259
- debug9.warn("ignored edit request with unsupported href scheme");
2260
- return;
2261
- }
2262
- if (isIframe) {
2263
- sendPreprEvent("field_edit_requested", {
2264
- id,
2265
- field,
2266
- locale: contentLocale
2267
- });
2268
- } else if (safeHref) {
2269
- window.open(safeHref, "_blank", "noopener,noreferrer");
2270
- }
3376
+ const onEdit = ({
3377
+ href,
3378
+ id,
3379
+ field,
3380
+ locale: contentLocale,
3381
+ block,
3382
+ entryId,
3383
+ referenceField
3384
+ }) => {
3385
+ const safeHref = href ? safeEditUrl(href) : null;
3386
+ if (href && !safeHref) {
3387
+ debug9.warn("ignored edit request with unsupported href scheme");
3388
+ return;
2271
3389
  }
3390
+ if (isIframe) {
3391
+ sendPreprEvent("field_edit_requested", {
3392
+ id,
3393
+ field,
3394
+ locale: contentLocale,
3395
+ ...block !== void 0 && { block },
3396
+ ...entryId && { entryId },
3397
+ ...referenceField && { referenceField }
3398
+ });
3399
+ } else if (safeHref) {
3400
+ window.open(safeHref, "_blank", "noopener,noreferrer");
3401
+ }
3402
+ };
3403
+ const visualEditor = isIframe ? createVisualEditor({
3404
+ onEdit,
3405
+ locale,
3406
+ onNavigationBlocked: (href) => sendPreprEvent("navigation_blocked", { href })
3407
+ }) : null;
3408
+ const stega = visualEditor ?? createStegaController({
3409
+ // The edit-mode marks and hover label are chrome, so they follow the
3410
+ // same decision as the bar: unwanted in a headless preview that asked
3411
+ // for no UI.
3412
+ tooltip: mountUi,
3413
+ onEdit,
3414
+ locale
2272
3415
  });
2273
3416
  const autoClean = createStegaAutoClean({
2274
3417
  enabled: options?.autoClean
@@ -2291,10 +3434,30 @@ function createPreprPreview(opts = {}) {
2291
3434
  });
2292
3435
  const bridge = createIframeBridge(store, {
2293
3436
  allowedEditorOrigins: options?.allowedEditorOrigins,
3437
+ onHandshake: ({
3438
+ itemId,
3439
+ referencedIds,
3440
+ componentIds,
3441
+ uiLocale,
3442
+ navigationGuard,
3443
+ assets
3444
+ }) => {
3445
+ if (uiLocale) visualEditor?.setLocale(uiLocale);
3446
+ visualEditor?.setCurrentItem(itemId ?? null, referencedIds, componentIds);
3447
+ visualEditor?.setNavigationGuard(navigationGuard === true);
3448
+ if (assets) visualEditor?.setAssets(assets);
3449
+ },
3450
+ onAssets: (assets) => visualEditor?.setAssets(assets),
3451
+ onAllowNavigation: (href) => visualEditor?.allowNavigation(href),
3452
+ onReferences: (referencedIds, componentIds) => visualEditor?.setReferences(referencedIds, componentIds),
2294
3453
  // The editor selecting a field scrolls the matching element into view. The
2295
3454
  // lookup relies on the attributes the stega scan writes, so it finds
2296
3455
  // nothing until encoded content has been tagged.
2297
- onScrollToField: ({ field, id }) => {
3456
+ onScrollToField: ({ field, id, block, entryId, fieldById }) => {
3457
+ if (visualEditor) {
3458
+ void visualEditor.focusField({ field, id, block, entryId, fieldById });
3459
+ return;
3460
+ }
2298
3461
  if (!scrollToField({ field, id })) {
2299
3462
  debug9.log("no element on this page renders field", field);
2300
3463
  }
@@ -2326,7 +3489,7 @@ function createPreprPreview(opts = {}) {
2326
3489
  }
2327
3490
 
2328
3491
  // src/version.ts
2329
- var VERSION = "0.5.0-rc.2";
3492
+ var VERSION = "0.5.0-rc.3";
2330
3493
 
2331
3494
  // src/core/pixel.ts
2332
3495
  var PIXEL_SCRIPT_URL = "https://cdn.tracking.prepr.io/js/prepr.min.js";