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