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