@preprio/toolkit 0.4.3 → 0.5.0-rc.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/README.md +416 -23
  2. package/dist/astro/index.cjs +1290 -1
  3. package/dist/astro/index.d.cts +47 -1
  4. package/dist/astro/index.d.ts +47 -1
  5. package/dist/astro/index.js +1297 -1
  6. package/dist/chunk-F46GJS6Y.js +26 -0
  7. package/dist/{chunk-H7VA7NGY.js → chunk-IDEC77LZ.js} +1480 -621
  8. package/dist/chunk-IPMB32AZ.js +246 -0
  9. package/dist/chunk-KD3GK6TO.js +1221 -0
  10. package/dist/chunk-S3VT4UWM.js +75 -0
  11. package/dist/chunk-TBBU3LDD.js +184 -0
  12. package/dist/chunk-URXSDJOU.js +356 -0
  13. package/dist/codegen/index.cjs +82 -0
  14. package/dist/codegen/index.d.cts +70 -0
  15. package/dist/codegen/index.d.ts +70 -0
  16. package/dist/codegen/index.js +64 -0
  17. package/dist/index.cjs +3412 -409
  18. package/dist/index.d.cts +287 -5
  19. package/dist/index.d.ts +287 -5
  20. package/dist/index.js +307 -144
  21. package/dist/live-preview-DLXGXRE5.js +18 -0
  22. package/dist/nextjs/components.cjs +1662 -388
  23. package/dist/nextjs/components.d.cts +1 -1
  24. package/dist/nextjs/components.d.ts +1 -1
  25. package/dist/nextjs/components.js +4 -1
  26. package/dist/nextjs/index.cjs +1181 -6
  27. package/dist/nextjs/index.d.cts +83 -3
  28. package/dist/nextjs/index.d.ts +83 -3
  29. package/dist/nextjs/index.js +1179 -7
  30. package/dist/nextjs/live-action.cjs +285 -0
  31. package/dist/nextjs/live-action.d.cts +12 -0
  32. package/dist/nextjs/live-action.d.ts +12 -0
  33. package/dist/nextjs/live-action.js +68 -0
  34. package/dist/nextjs/live.cjs +1899 -0
  35. package/dist/nextjs/live.d.cts +54 -0
  36. package/dist/nextjs/live.d.ts +54 -0
  37. package/dist/nextjs/live.js +268 -0
  38. package/dist/nuxt/chunk-BWPGO25E.js +340 -0
  39. package/dist/nuxt/index.cjs +1 -1
  40. package/dist/nuxt/index.js +1 -1
  41. package/dist/nuxt/live-preview-2RCU35W5.js +1189 -0
  42. package/dist/nuxt/live.cjs +1816 -0
  43. package/dist/nuxt/live.d.cts +43 -0
  44. package/dist/nuxt/live.d.ts +43 -0
  45. package/dist/nuxt/live.js +198 -0
  46. package/dist/{pixel-e1fUWl2Y.d.cts → pixel-CX422dCO.d.cts} +18 -3
  47. package/dist/{pixel-e1fUWl2Y.d.ts → pixel-CX422dCO.d.ts} +18 -3
  48. package/dist/react/components.cjs +1662 -388
  49. package/dist/react/components.d.cts +17 -2
  50. package/dist/react/components.d.ts +17 -2
  51. package/dist/react/components.js +1662 -388
  52. package/dist/sveltekit/chunk-BWPGO25E.js +340 -0
  53. package/dist/sveltekit/index.cjs +1 -1
  54. package/dist/sveltekit/index.js +1 -1
  55. package/dist/sveltekit/live-preview-2RCU35W5.js +1189 -0
  56. package/dist/sveltekit/live.cjs +1830 -0
  57. package/dist/sveltekit/live.d.cts +59 -0
  58. package/dist/sveltekit/live.d.ts +59 -0
  59. package/dist/sveltekit/live.js +204 -0
  60. package/package.json +37 -11
  61. package/src/astro/PreprToolbar.astro +7 -1
@@ -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,51 +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
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
+ }
294
373
  var EDITOR_BASE_DOMAIN = "prepr.io";
295
374
  function isAllowedEditorOrigin(origin, allowed) {
296
- if (allowed) return allowed.includes(origin);
375
+ if (allowed) {
376
+ if (allowed.includes("*")) return origin !== "null";
377
+ return allowed.includes(origin);
378
+ }
297
379
  let url;
298
380
  try {
299
381
  url = new URL(origin);
@@ -308,6 +390,11 @@ function isAllowedEditorOrigin(origin, allowed) {
308
390
  }
309
391
  function createIframeBridge(store, options = {}) {
310
392
  const allowedOrigins = options.allowedEditorOrigins;
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;
311
398
  let parentOrigin = null;
312
399
  const onKeyDown = (event) => {
313
400
  const key = event.key.toLowerCase();
@@ -317,7 +404,10 @@ function createIframeBridge(store, options = {}) {
317
404
  const onMessage = (evt) => {
318
405
  const data = evt?.data;
319
406
  if (data?.event === "prepr:initVE" && !parentOrigin) {
320
- 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
+ }
321
411
  parentOrigin = evt.origin;
322
412
  setTrustedParentOrigin(parentOrigin);
323
413
  if (data.scrollPosition != null) {
@@ -325,12 +415,76 @@ function createIframeBridge(store, options = {}) {
325
415
  setTimeout(() => window.scrollTo(0, top), 1);
326
416
  }
327
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
+ }
328
436
  }
329
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
+ }
330
455
  if (data?.event === "prepr:getScrollPosition") {
331
456
  const currentScrollY = window.scrollY || document.documentElement.scrollTop;
332
457
  sendPreprEvent("getScrollPosition", { value: currentScrollY });
333
458
  }
459
+ if (data?.event === "prepr:scrollToField") {
460
+ if (typeof data.field !== "string" || !data.field) return;
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;
465
+ try {
466
+ onScrollToField?.({
467
+ field: data.field,
468
+ id: data.id,
469
+ block,
470
+ entryId,
471
+ fieldById
472
+ });
473
+ } catch (error) {
474
+ debug.warn("scrollToField handler failed", error);
475
+ }
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
+ }
334
488
  };
335
489
  return {
336
490
  start() {
@@ -355,7 +509,7 @@ var import_stega2 = require("@vercel/stega");
355
509
 
356
510
  // src/core/stega/clean.ts
357
511
  var import_stega = require("@vercel/stega");
358
- var debug = createScopedLogger("stega:clean");
512
+ var debug2 = createScopedLogger("stega:clean");
359
513
  function decodeStega(str) {
360
514
  if (!str) return null;
361
515
  try {
@@ -364,7 +518,7 @@ function decodeStega(str) {
364
518
  return decoded;
365
519
  }
366
520
  } catch (error) {
367
- debug.log("error decoding stega data", error);
521
+ debug2.log("error decoding stega data", error);
368
522
  const match = str.match(/{"origin.*?}/);
369
523
  if (match) {
370
524
  try {
@@ -373,7 +527,7 @@ function decodeStega(str) {
373
527
  return decodedMatch;
374
528
  }
375
529
  } catch (innerError) {
376
- debug.log("error decoding stega regex match", innerError);
530
+ debug2.log("error decoding stega regex match", innerError);
377
531
  }
378
532
  }
379
533
  }
@@ -382,6 +536,7 @@ function decodeStega(str) {
382
536
 
383
537
  // src/core/stega/dom.ts
384
538
  var IGNORED_ANCESTORS = "script, style, noscript";
539
+ var AUTO_ENTRY_ATTR = "data-prepr-entry-auto";
385
540
  function isIgnoredTextNode(node) {
386
541
  if (node.parentElement?.closest(IGNORED_ANCESTORS)) return true;
387
542
  return !node.textContent?.trim();
@@ -409,42 +564,90 @@ function isRenderedElement(element) {
409
564
  if (!style) return true;
410
565
  return style.visibility !== "hidden" && style.display !== "none";
411
566
  }
567
+ function parseEditHref(href) {
568
+ try {
569
+ const url = new URL(href, window.location.origin);
570
+ return {
571
+ id: url.pathname.split("/").pop() || void 0,
572
+ field: url.searchParams.get("field") || void 0,
573
+ locale: url.searchParams.get("locale") || void 0,
574
+ // Added by the toolkit's `annotateEditLinks`, not by Prepr.
575
+ entry: url.searchParams.get("entry") || void 0
576
+ };
577
+ } catch {
578
+ return {};
579
+ }
580
+ }
581
+ function setOrRemove(element, name, value) {
582
+ if (value) {
583
+ element.setAttribute(name, value);
584
+ } else {
585
+ element.removeAttribute(name);
586
+ }
587
+ }
412
588
  function tagEncodedElement(element, decoded) {
589
+ if (isSiteTagged(element)) return false;
413
590
  element.setAttribute("data-prepr-encoded", "");
414
591
  element.setAttribute("data-prepr-href", decoded.href);
415
592
  element.setAttribute("data-prepr-origin", decoded.origin);
593
+ const { id, field, locale, entry } = parseEditHref(decoded.href);
594
+ setOrRemove(element, "data-prepr-id", id);
595
+ const fieldHint = element.getAttribute("data-prepr-edit-target")?.trim() || void 0;
596
+ setOrRemove(element, "data-prepr-field", field ?? fieldHint);
597
+ setOrRemove(element, "data-prepr-locale", locale);
598
+ const siteEntry = element.hasAttribute("data-prepr-entry-id") && !element.hasAttribute(AUTO_ENTRY_ATTR);
599
+ if (!siteEntry) {
600
+ setOrRemove(element, "data-prepr-entry-id", entry);
601
+ setOrRemove(element, AUTO_ENTRY_ATTR, entry && "true");
602
+ }
603
+ return true;
604
+ }
605
+ function isSiteTagged(element) {
606
+ return element.hasAttribute("data-prepr-encoded") && !element.hasAttribute("data-prepr-href");
607
+ }
608
+ function readEditPayload(element) {
609
+ return {
610
+ href: element.getAttribute("data-prepr-href") ?? "",
611
+ origin: element.getAttribute("data-prepr-origin") ?? "",
612
+ id: element.getAttribute("data-prepr-id") ?? void 0,
613
+ field: element.getAttribute("data-prepr-field") ?? void 0,
614
+ locale: element.getAttribute("data-prepr-locale") ?? void 0,
615
+ entryId: element.getAttribute("data-prepr-entry-id") || void 0
616
+ };
416
617
  }
417
618
 
418
619
  // src/core/stega/auto-clean.ts
419
- var debug2 = createScopedLogger("stega:auto-clean");
620
+ var debug3 = createScopedLogger("stega:auto-clean");
621
+ var cleanedNodes = /* @__PURE__ */ new WeakMap();
420
622
  function createStegaAutoClean(options) {
421
623
  const stripText = options?.enabled ?? true;
422
624
  let observer = null;
423
625
  let processing = false;
424
- let cleaned = /* @__PURE__ */ new WeakMap();
425
626
  let pendingHidden = /* @__PURE__ */ new Set();
426
627
  let pendingDirty = /* @__PURE__ */ new Set();
427
628
  let debounceTimeout = null;
428
629
  function cleanTextNode(textNode) {
429
630
  if (isIgnoredTextNode(textNode)) return false;
430
631
  const textContent = textNode.textContent;
431
- const info = cleaned.get(textNode);
632
+ const info = cleanedNodes.get(textNode);
432
633
  const settled = stripText ? info?.cleaned : info?.original;
433
634
  if (info && textContent === settled) {
434
635
  if (!info.decoded) return false;
435
636
  const shown = resolveEditTarget(textNode);
436
- if (!shown) return false;
637
+ if (!shown) {
638
+ pendingHidden.add(textNode);
639
+ return false;
640
+ }
437
641
  pendingHidden.delete(textNode);
438
642
  if (shown.hasAttribute("data-prepr-encoded")) return false;
439
- tagEncodedElement(shown, info.decoded);
440
- return true;
643
+ return tagEncodedElement(shown, info.decoded);
441
644
  }
442
645
  try {
443
646
  const { cleaned: stripped, encoded } = (0, import_stega2.vercelStegaSplit)(textContent);
444
647
  if (!encoded || stripped === textContent) return false;
445
648
  const decoded = decodeStega(textContent);
446
649
  if (!decoded?.href && !stripText) return false;
447
- cleaned.set(textNode, {
650
+ cleanedNodes.set(textNode, {
448
651
  original: textContent,
449
652
  cleaned: stripped,
450
653
  decoded: decoded?.href ? decoded : null
@@ -454,12 +657,11 @@ function createStegaAutoClean(options) {
454
657
  const target = resolveEditTarget(textNode);
455
658
  if (target) {
456
659
  pendingHidden.delete(textNode);
457
- tagEncodedElement(target, decoded);
458
- return true;
660
+ return tagEncodedElement(target, decoded);
459
661
  }
460
662
  pendingHidden.add(textNode);
461
663
  } catch (error) {
462
- debug2.log("error cleaning text node", error);
664
+ debug3.log("error cleaning text node", error);
463
665
  }
464
666
  return false;
465
667
  }
@@ -486,7 +688,7 @@ function createStegaAutoClean(options) {
486
688
  }
487
689
  if (mutation.type === "characterData" && mutation.target.nodeType === Node.TEXT_NODE) {
488
690
  const textNode = mutation.target;
489
- const info = cleaned.get(textNode);
691
+ const info = cleanedNodes.get(textNode);
490
692
  const text = textNode.textContent ?? "";
491
693
  const settled = stripText ? info?.cleaned : info?.original;
492
694
  if (text !== settled && (0, import_stega2.vercelStegaSplit)(text).encoded) {
@@ -529,7 +731,16 @@ function createStegaAutoClean(options) {
529
731
  retryPendingHidden();
530
732
  }, 50);
531
733
  }
734
+ let resizeTimeout = null;
735
+ function handleResize() {
736
+ if (pendingHidden.size === 0 || resizeTimeout) return;
737
+ resizeTimeout = setTimeout(() => {
738
+ resizeTimeout = null;
739
+ retryPendingHidden();
740
+ }, 100);
741
+ }
532
742
  function setupObserver() {
743
+ window.addEventListener("resize", handleResize, { passive: true });
533
744
  observer = new MutationObserver(handleMutations);
534
745
  observer.observe(document.body, {
535
746
  childList: true,
@@ -545,27 +756,464 @@ function createStegaAutoClean(options) {
545
756
  if (observer || typeof window === "undefined") return;
546
757
  scanAndClean();
547
758
  setupObserver();
548
- debug2.log("stega auto-clean started");
759
+ debug3.log("stega auto-clean started");
549
760
  }
550
761
  function stop() {
551
762
  if (debounceTimeout) {
552
763
  clearTimeout(debounceTimeout);
553
764
  debounceTimeout = null;
554
765
  }
766
+ if (resizeTimeout) {
767
+ clearTimeout(resizeTimeout);
768
+ resizeTimeout = null;
769
+ }
555
770
  if (observer) {
556
771
  observer.disconnect();
557
772
  observer = null;
773
+ window.removeEventListener("resize", handleResize);
558
774
  }
559
- cleaned = /* @__PURE__ */ new WeakMap();
560
775
  pendingHidden = /* @__PURE__ */ new Set();
561
776
  pendingDirty = /* @__PURE__ */ new Set();
562
- debug2.log("stega auto-clean stopped");
777
+ debug3.log("stega auto-clean stopped");
563
778
  }
564
779
  return { start, stop };
565
780
  }
566
781
 
782
+ // src/core/visual-editor/overlay.ts
783
+ var HOST_TAG = "prepr-visual-editor";
784
+ var SCROLL_SETTLE_MS = 120;
785
+ var LABEL_SPACE_PX = 22;
786
+ var BOX_PADDING_PX = 3;
787
+ var LABEL_MARGIN_PX = 4;
788
+ var PROBE_POINTS = [
789
+ [0.5, 0.5],
790
+ [0.15, 0.2],
791
+ [0.85, 0.2],
792
+ [0.15, 0.8],
793
+ [0.85, 0.8]
794
+ ];
795
+ function overlapsBox(area, rect) {
796
+ 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;
797
+ }
798
+ function overlaps(a2, b2) {
799
+ return a2.left < b2.right && b2.left < a2.right && a2.top < b2.bottom && b2.top < a2.bottom;
800
+ }
801
+ function coverOf(hit, target, x2, y2) {
802
+ let cover = hit.getBoundingClientRect();
803
+ for (let el = hit.parentElement; el && !el.contains(target) && el !== document.body; el = el.parentElement) {
804
+ const r2 = el.getBoundingClientRect();
805
+ if (x2 < r2.left || x2 >= r2.right || y2 < r2.top || y2 >= r2.bottom) break;
806
+ cover = r2;
807
+ }
808
+ return cover;
809
+ }
810
+ function mergeAreas(areas) {
811
+ const merged = areas.slice();
812
+ for (let i2 = 0; i2 < merged.length; i2++) {
813
+ for (let j2 = i2 + 1; j2 < merged.length; j2++) {
814
+ if (!overlaps(merged[i2], merged[j2])) continue;
815
+ merged[i2] = {
816
+ left: Math.min(merged[i2].left, merged[j2].left),
817
+ top: Math.min(merged[i2].top, merged[j2].top),
818
+ right: Math.max(merged[i2].right, merged[j2].right),
819
+ bottom: Math.max(merged[i2].bottom, merged[j2].bottom)
820
+ };
821
+ merged.splice(j2, 1);
822
+ j2 = i2;
823
+ }
824
+ }
825
+ return merged;
826
+ }
827
+ function findCovers(target, rect) {
828
+ const width = window.innerWidth;
829
+ const height = window.innerHeight;
830
+ const covers = [];
831
+ const seen = /* @__PURE__ */ new Set();
832
+ let probed = false;
833
+ let reached = false;
834
+ for (const [fx, fy] of PROBE_POINTS) {
835
+ const x2 = rect.left + rect.width * fx;
836
+ const y2 = rect.top + rect.height * fy;
837
+ if (x2 < 0 || y2 < 0 || x2 >= width || y2 >= height) continue;
838
+ probed = true;
839
+ const hit = document.elementFromPoint(x2, y2);
840
+ if (!hit || hit === target || target.contains(hit)) {
841
+ reached = true;
842
+ continue;
843
+ }
844
+ if (seen.has(hit)) continue;
845
+ seen.add(hit);
846
+ if (covers.some(
847
+ (c2) => x2 >= c2.left && x2 < c2.right && y2 >= c2.top && y2 < c2.bottom
848
+ )) {
849
+ continue;
850
+ }
851
+ covers.push(coverOf(hit, target, x2, y2));
852
+ }
853
+ if (!probed) return [];
854
+ return reached ? mergeAreas(covers) : null;
855
+ }
856
+ function clipPath(x2, y2, covers) {
857
+ const far = 1e5;
858
+ const points = [
859
+ `${-far}px ${-far}px`,
860
+ `${far}px ${-far}px`,
861
+ `${far}px ${far}px`,
862
+ `${-far}px ${far}px`,
863
+ `${-far}px ${-far}px`
864
+ ];
865
+ for (const c2 of covers) {
866
+ const l2 = c2.left - x2;
867
+ const t3 = c2.top - y2;
868
+ const r2 = c2.right - x2;
869
+ const b2 = c2.bottom - y2;
870
+ points.push(
871
+ `${l2}px ${t3}px`,
872
+ `${r2}px ${t3}px`,
873
+ `${r2}px ${b2}px`,
874
+ `${l2}px ${b2}px`,
875
+ `${l2}px ${t3}px`,
876
+ `${-far}px ${-far}px`
877
+ );
878
+ }
879
+ return `polygon(evenodd, ${points.join(", ")})`;
880
+ }
881
+ var STYLES = `
882
+ :host {
883
+ all: initial;
884
+ position: fixed;
885
+ inset: 0;
886
+ pointer-events: none;
887
+ z-index: 2147483646;
888
+ contain: strict;
889
+ }
890
+ .box {
891
+ position: absolute;
892
+ top: 0;
893
+ left: 0;
894
+ box-sizing: border-box;
895
+ border-radius: 6px;
896
+ opacity: 0;
897
+ will-change: transform, width, height;
898
+ transition:
899
+ transform 180ms cubic-bezier(0.2, 0.8, 0.2, 1),
900
+ width 180ms cubic-bezier(0.2, 0.8, 0.2, 1),
901
+ height 180ms cubic-bezier(0.2, 0.8, 0.2, 1),
902
+ opacity 140ms ease;
903
+ }
904
+ .box[data-visible] { opacity: 1; }
905
+ .box[data-instant], :host([data-scrolling]) .box {
906
+ transition: opacity 140ms ease;
907
+ }
908
+ .hover {
909
+ border: 1.5px solid rgba(79, 70, 229, 0.9);
910
+ background: rgba(79, 70, 229, 0.06);
911
+ }
912
+ .selected {
913
+ border: 2px solid #4f46e5;
914
+ box-shadow: 0 0 0 4px rgba(79, 70, 229, 0.18);
915
+ }
916
+ .label {
917
+ position: absolute;
918
+ left: -2px;
919
+ bottom: 100%;
920
+ margin-bottom: 4px;
921
+ padding: 3px 7px;
922
+ border-radius: 5px;
923
+ background: #4f46e5;
924
+ color: #fff;
925
+ font: 500 11px/1.3 ui-sans-serif, system-ui, -apple-system, 'Segoe UI', sans-serif;
926
+ letter-spacing: 0.01em;
927
+ white-space: nowrap;
928
+ display: flex;
929
+ max-width: 240px;
930
+ box-shadow: 0 1px 3px rgba(15, 23, 42, 0.25);
931
+ }
932
+ .text {
933
+ min-width: 0;
934
+ overflow: hidden;
935
+ text-overflow: ellipsis;
936
+ }
937
+ .hover .label { background: rgba(79, 70, 229, 0.92); }
938
+ /* Content of another item than the one open in the editor: still clickable,
939
+ but visibly not part of the item being edited. */
940
+ .hover[data-item-kind='other'] {
941
+ border: 1.5px dashed #a5b4fc;
942
+ background: rgba(100, 116, 139, 0.05);
943
+ }
944
+ .hover[data-item-kind='other'] .label { background: #64748b; }
945
+ .hover[data-item-kind='referenced'] {
946
+ border: 1.5px solid #a5b4fc;
947
+ background: rgba(79, 70, 229, 0.03);
948
+ }
949
+ .hover[data-item-kind='referenced'] .label { background: #818cf8; }
950
+ .box[data-label-covered] .label { visibility: hidden; }
951
+ .box[data-label-inside] .label {
952
+ bottom: auto;
953
+ top: 2px;
954
+ left: 2px;
955
+ margin: 0;
956
+ }
957
+ /* A clickable hover label (edit mode on the live site). Only while shown, so
958
+ a fading box never leaves an invisible click target behind. */
959
+ :host([data-label-action]) .hover[data-visible] .label {
960
+ pointer-events: auto;
961
+ cursor: pointer;
962
+ }
963
+ :host([data-label-action]) .hover .label::after {
964
+ content: '\\2197';
965
+ margin-left: 5px;
966
+ }
967
+ :host([data-label-action]) .hover .label:hover { background: #4338ca; }
968
+ /* Bridges the gap between the label and its element, so the pointer can
969
+ travel from one to the other without the hover ending on the way. */
970
+ :host([data-label-action]) .hover .label::before {
971
+ content: '';
972
+ position: absolute;
973
+ left: 0;
974
+ right: 0;
975
+ top: 100%;
976
+ height: ${LABEL_MARGIN_PX + BOX_PADDING_PX + 2}px;
977
+ }
978
+ @media (prefers-reduced-motion: reduce) {
979
+ .box { transition: opacity 80ms linear; }
980
+ }
981
+ `;
982
+ var VisualEditorOverlay = class {
983
+ constructor(options = {}) {
984
+ this.host = null;
985
+ this.hoverBox = null;
986
+ this.selectedBox = null;
987
+ this.frame = null;
988
+ this.scrollTimeout = null;
989
+ this.resizeObserver = null;
990
+ this.onLabelClick = options.onLabelClick;
991
+ }
992
+ mount() {
993
+ if (this.host) return;
994
+ const host = document.createElement(HOST_TAG);
995
+ const shadow = host.attachShadow({ mode: "open" });
996
+ const style = document.createElement("style");
997
+ style.textContent = STYLES;
998
+ shadow.appendChild(style);
999
+ this.hoverBox = this.createBox(shadow, "hover");
1000
+ this.selectedBox = this.createBox(shadow, "selected");
1001
+ const onLabelClick = this.onLabelClick;
1002
+ if (onLabelClick) {
1003
+ host.setAttribute("data-label-action", "");
1004
+ const hoverBox = this.hoverBox;
1005
+ hoverBox.label.addEventListener("click", () => {
1006
+ if (hoverBox.target) onLabelClick(hoverBox.target);
1007
+ });
1008
+ }
1009
+ document.documentElement.appendChild(host);
1010
+ this.host = host;
1011
+ if (typeof ResizeObserver !== "undefined") {
1012
+ this.resizeObserver = new ResizeObserver(() => this.schedule());
1013
+ }
1014
+ }
1015
+ unmount() {
1016
+ if (this.frame !== null) cancelAnimationFrame(this.frame);
1017
+ if (this.scrollTimeout) clearTimeout(this.scrollTimeout);
1018
+ this.frame = null;
1019
+ this.scrollTimeout = null;
1020
+ this.resizeObserver?.disconnect();
1021
+ this.resizeObserver = null;
1022
+ this.host?.remove();
1023
+ this.host = null;
1024
+ this.hoverBox = null;
1025
+ this.selectedBox = null;
1026
+ }
1027
+ /**
1028
+ * Whether a pointer event target is this chrome: events from inside the
1029
+ * shadow root reach the page retargeted to its host.
1030
+ */
1031
+ owns(node) {
1032
+ return node !== null && node === this.host;
1033
+ }
1034
+ getHoverTarget() {
1035
+ return this.hoverBox?.target ?? null;
1036
+ }
1037
+ getSelectedTarget() {
1038
+ return this.selectedBox?.target ?? null;
1039
+ }
1040
+ setHover(target, label, options = {}) {
1041
+ if (target) {
1042
+ this.hoverBox?.root.setAttribute(
1043
+ "data-item-kind",
1044
+ options.kind ?? "current"
1045
+ );
1046
+ }
1047
+ this.retarget(this.hoverBox, target, label);
1048
+ }
1049
+ setSelected(target, label) {
1050
+ this.retarget(this.selectedBox, target, label);
1051
+ }
1052
+ /** Call on every scroll; positions follow in the next frame, untransitioned. */
1053
+ onScroll() {
1054
+ if (!this.host) return;
1055
+ this.host.setAttribute("data-scrolling", "");
1056
+ if (this.scrollTimeout) clearTimeout(this.scrollTimeout);
1057
+ this.scrollTimeout = setTimeout(() => {
1058
+ this.host?.removeAttribute("data-scrolling");
1059
+ this.scrollTimeout = null;
1060
+ }, SCROLL_SETTLE_MS);
1061
+ this.schedule();
1062
+ }
1063
+ /** Re-measure both boxes in the next animation frame. */
1064
+ schedule() {
1065
+ if (this.frame !== null || !this.host) return;
1066
+ this.frame = requestAnimationFrame(() => {
1067
+ this.frame = null;
1068
+ this.render();
1069
+ });
1070
+ }
1071
+ createBox(shadow, kind) {
1072
+ const root = document.createElement("div");
1073
+ root.className = `box ${kind}`;
1074
+ const label = document.createElement("div");
1075
+ label.className = "label";
1076
+ const text = document.createElement("span");
1077
+ text.className = "text";
1078
+ label.appendChild(text);
1079
+ root.appendChild(label);
1080
+ shadow.appendChild(root);
1081
+ return { root, label, text, target: null };
1082
+ }
1083
+ retarget(box, target, label) {
1084
+ if (!box || box.target === target) return;
1085
+ const previous = box.target;
1086
+ if (previous) this.resizeObserver?.unobserve(previous);
1087
+ box.target = target;
1088
+ if (target) {
1089
+ this.resizeObserver?.observe(target);
1090
+ box.text.textContent = label;
1091
+ if (!box.root.hasAttribute("data-visible")) {
1092
+ box.root.setAttribute("data-instant", "");
1093
+ }
1094
+ }
1095
+ this.schedule();
1096
+ }
1097
+ render() {
1098
+ const hover = this.hoverBox ? this.measure(this.hoverBox) : null;
1099
+ const selected = this.selectedBox ? this.measure(this.selectedBox) : null;
1100
+ if (this.hoverBox) this.paint(this.hoverBox, hover, selected);
1101
+ if (this.selectedBox) this.paint(this.selectedBox, selected, hover);
1102
+ }
1103
+ measure(box) {
1104
+ const target = box.target;
1105
+ if (!target) return null;
1106
+ if (!target.isConnected) {
1107
+ this.resizeObserver?.unobserve(target);
1108
+ box.target = null;
1109
+ return null;
1110
+ }
1111
+ const rect = target.getBoundingClientRect();
1112
+ if (rect.width === 0 && rect.height === 0) return null;
1113
+ const covers = findCovers(target, rect);
1114
+ if (!covers) return null;
1115
+ return {
1116
+ rect,
1117
+ covers,
1118
+ labelWidth: covers.length ? box.label.offsetWidth : 0,
1119
+ labelHeight: covers.length ? box.label.offsetHeight : 0
1120
+ };
1121
+ }
1122
+ paint(box, measurement, other = null) {
1123
+ const root = box.root;
1124
+ if (!measurement) {
1125
+ root.removeAttribute("data-visible");
1126
+ return;
1127
+ }
1128
+ const { rect, covers } = measurement;
1129
+ const x2 = rect.left - BOX_PADDING_PX;
1130
+ const y2 = rect.top - BOX_PADDING_PX;
1131
+ const labelAbove = {
1132
+ left: x2 - 2,
1133
+ top: y2 - LABEL_SPACE_PX,
1134
+ right: x2 - 2 + (measurement.labelWidth || Math.min(240, rect.width)),
1135
+ bottom: y2
1136
+ };
1137
+ const labelInside = y2 < LABEL_SPACE_PX || other !== null && other.rect !== rect && overlapsBox(labelAbove, other.rect);
1138
+ root.style.transform = `translate3d(${x2}px, ${y2}px, 0)`;
1139
+ root.style.width = `${rect.width + BOX_PADDING_PX * 2}px`;
1140
+ root.style.height = `${rect.height + BOX_PADDING_PX * 2}px`;
1141
+ root.toggleAttribute("data-label-inside", labelInside);
1142
+ root.style.clipPath = covers.length ? clipPath(x2, y2, covers) : "";
1143
+ const labelLeft = labelInside ? x2 + 2 : x2 - 2;
1144
+ const labelTop = labelInside ? y2 + 2 : y2 - LABEL_MARGIN_PX - measurement.labelHeight;
1145
+ const label = {
1146
+ left: labelLeft,
1147
+ top: labelTop,
1148
+ right: labelLeft + measurement.labelWidth,
1149
+ bottom: labelTop + measurement.labelHeight
1150
+ };
1151
+ root.toggleAttribute(
1152
+ "data-label-covered",
1153
+ covers.some((c2) => overlaps(c2, label))
1154
+ );
1155
+ if (root.hasAttribute("data-instant")) {
1156
+ root.setAttribute("data-visible", "");
1157
+ requestAnimationFrame(() => root.removeAttribute("data-instant"));
1158
+ return;
1159
+ }
1160
+ root.setAttribute("data-visible", "");
1161
+ }
1162
+ };
1163
+
1164
+ // src/core/visual-editor/field-path.ts
1165
+ var INDEX_SEGMENT = /^\d+$/;
1166
+ var WORD_SEPARATORS = /[_-]+/g;
1167
+ function normalizeFieldPath(path) {
1168
+ return path.split(".").map(
1169
+ (segment) => INDEX_SEGMENT.test(segment) ? String(Number.parseInt(segment, 10)) : segment
1170
+ ).join(".");
1171
+ }
1172
+ var GENERIC_LEAVES = /* @__PURE__ */ new Set(["value", "text", "body", "content", "label"]);
1173
+ function humanize(name) {
1174
+ const words = name.replace(WORD_SEPARATORS, " ").trim();
1175
+ return words.charAt(0).toUpperCase() + words.slice(1);
1176
+ }
1177
+ function fieldLabel(path, locale = "en") {
1178
+ if (!path) return t("visualEditor.edit", locale);
1179
+ const segments = path.split(".").filter((s2) => s2 && s2 !== "items" && !INDEX_SEGMENT.test(s2));
1180
+ const name = segments[segments.length - 1];
1181
+ if (!name) return t("visualEditor.edit", locale);
1182
+ const parent = segments[segments.length - 2];
1183
+ if (parent && GENERIC_LEAVES.has(name)) {
1184
+ return `${humanize(parent)} \u203A ${humanize(name)}`;
1185
+ }
1186
+ return humanize(name);
1187
+ }
1188
+ function findFieldElement(lookup) {
1189
+ if (!lookup.field) return null;
1190
+ const candidates = document.querySelectorAll(
1191
+ "[data-prepr-encoded]"
1192
+ );
1193
+ if (lookup.entryId) {
1194
+ for (const candidate of candidates) {
1195
+ if (candidate.getAttribute("data-prepr-entry-id") === lookup.entryId) {
1196
+ return candidate;
1197
+ }
1198
+ }
1199
+ }
1200
+ for (const path of [lookup.field, lookup.fieldById]) {
1201
+ if (!path) continue;
1202
+ const wanted = normalizeFieldPath(path);
1203
+ for (const candidate of candidates) {
1204
+ const field = candidate.getAttribute("data-prepr-field");
1205
+ if (!field || normalizeFieldPath(field) !== wanted) continue;
1206
+ if (lookup.id && candidate.getAttribute("data-prepr-id") !== lookup.id) {
1207
+ continue;
1208
+ }
1209
+ return candidate;
1210
+ }
1211
+ }
1212
+ return null;
1213
+ }
1214
+
567
1215
  // src/core/stega/elements.ts
568
- var debug3 = createScopedLogger("stega:elements");
1216
+ var debug4 = createScopedLogger("stega:elements");
569
1217
  var StegaElements = class {
570
1218
  constructor(decode = decodeStega) {
571
1219
  this.observer = null;
@@ -610,7 +1258,7 @@ var StegaElements = class {
610
1258
  walkTextNodes(document.body, (textNode) => {
611
1259
  if (this.tagFromTextNode(textNode)) encodedCount++;
612
1260
  });
613
- debug3.log("document scan complete, encoded", encodedCount, "elements");
1261
+ debug4.log("document scan complete, encoded", encodedCount, "elements");
614
1262
  this.elements = document.querySelectorAll("[data-prepr-encoded]");
615
1263
  }
616
1264
  /** `onUpdate` fires after each debounced batch, for refreshing derived state. */
@@ -653,231 +1301,22 @@ var StegaElements = class {
653
1301
  attributeFilter: ["class", "style", "hidden", "open", "aria-expanded"]
654
1302
  });
655
1303
  }
656
- /** Drops the active class but keeps the data attributes, unlike cleanup(). */
657
- cleanupVisuals() {
658
- document.querySelectorAll(".prepr-overlay-active").forEach((element) => element.classList.remove("prepr-overlay-active"));
659
- }
660
- cleanup() {
1304
+ /**
1305
+ * Stops watching for new content. Tags stay on their elements: the page's
1306
+ * stega characters are stripped once they are read, so a tag removed here
1307
+ * could never be rebuilt when edit mode is turned back on.
1308
+ */
1309
+ disconnect() {
661
1310
  if (this.observer) {
662
1311
  this.observer.disconnect();
663
1312
  this.observer = null;
664
1313
  }
665
- document.querySelectorAll("[data-prepr-encoded]").forEach((element) => {
666
- element.removeAttribute("data-prepr-encoded");
667
- element.removeAttribute("data-prepr-href");
668
- element.removeAttribute("data-prepr-origin");
669
- element.classList.remove("prepr-overlay-active");
670
- });
671
1314
  this.elements = void 0;
672
1315
  }
673
1316
  };
674
1317
 
675
- // src/core/stega/overlay.ts
676
- var debug4 = createScopedLogger("stega:overlay");
677
- var StegaOverlay = class {
678
- constructor(onEdit) {
679
- this.overlay = null;
680
- this.tooltip = null;
681
- this.hideTimeout = null;
682
- this.currentElement = null;
683
- this.onEdit = onEdit;
684
- }
685
- create() {
686
- const overlay = document.createElement("div");
687
- overlay.className = "prepr-overlay";
688
- overlay.style.display = "none";
689
- const tooltip = document.createElement("div");
690
- tooltip.className = "prepr-tooltip";
691
- tooltip.style.display = "none";
692
- document.body.appendChild(overlay);
693
- document.body.appendChild(tooltip);
694
- this.overlay = overlay;
695
- this.tooltip = tooltip;
696
- return { overlay, tooltip };
697
- }
698
- getTooltip() {
699
- return this.tooltip;
700
- }
701
- hasCurrentElement() {
702
- return this.currentElement !== null;
703
- }
704
- clearHideTimeout() {
705
- if (this.hideTimeout) {
706
- clearTimeout(this.hideTimeout);
707
- this.hideTimeout = null;
708
- }
709
- }
710
- show(element) {
711
- if (!this.overlay || !this.tooltip) return;
712
- this.clearHideTimeout();
713
- if (this.currentElement && this.currentElement !== element) {
714
- this.currentElement.classList.remove("prepr-overlay-active");
715
- }
716
- const rect = element.getBoundingClientRect();
717
- const href = element.getAttribute("data-prepr-href");
718
- const origin = element.getAttribute("data-prepr-origin");
719
- const id = element.getAttribute("data-prepr-id") ?? void 0;
720
- const field = element.getAttribute("data-prepr-field") ?? void 0;
721
- debug4.log("showing overlay", { href, origin });
722
- const overlay = this.overlay;
723
- overlay.style.display = "block";
724
- overlay.style.top = `${rect.top + window.scrollY - 2}px`;
725
- overlay.style.left = `${rect.left + window.scrollX - 4}px`;
726
- overlay.style.width = `${rect.width + 8}px`;
727
- overlay.style.height = `${rect.height + 4}px`;
728
- const tooltip = this.tooltip;
729
- if (href && origin) {
730
- const MIN_WIDTH_FOR_TEXT = 80;
731
- const isCompact = rect.width < MIN_WIDTH_FOR_TEXT;
732
- tooltip.textContent = isCompact ? "\u2197" : `${origin} \u2197`;
733
- tooltip.style.display = "block";
734
- tooltip.style.minWidth = isCompact ? "auto" : "80px";
735
- requestAnimationFrame(() => {
736
- let top = rect.top + window.scrollY - tooltip.clientHeight - 2;
737
- let left = rect.right + 4 - tooltip.clientWidth;
738
- const minTop = window.scrollY + 4;
739
- const maxTop = window.scrollY + window.innerHeight - tooltip.clientHeight - 4;
740
- const minLeft = window.scrollX + 4;
741
- const maxLeft = window.scrollX + window.innerWidth - tooltip.clientWidth - 4;
742
- const below = top < minTop;
743
- if (below) {
744
- top = rect.bottom + window.scrollY + 2;
745
- }
746
- top = Math.max(minTop, Math.min(top, maxTop));
747
- left = Math.max(minLeft, Math.min(left, maxLeft));
748
- tooltip.dataset.preprPlacement = below ? "below" : "above";
749
- tooltip.style.top = `${top}px`;
750
- tooltip.style.left = `${left}px`;
751
- });
752
- tooltip.onclick = () => {
753
- this.onEdit({ href, origin, id, field });
754
- };
755
- }
756
- this.currentElement = element;
757
- element.classList.add("prepr-overlay-active");
758
- }
759
- hideImmediate() {
760
- this.clearHideTimeout();
761
- if (this.overlay) this.overlay.style.display = "none";
762
- if (this.tooltip) this.tooltip.style.display = "none";
763
- if (this.currentElement) {
764
- this.currentElement.classList.remove("prepr-overlay-active");
765
- this.currentElement = null;
766
- }
767
- }
768
- hide() {
769
- if (!this.overlay || !this.tooltip) return;
770
- this.clearHideTimeout();
771
- this.hideTimeout = setTimeout(() => this.hideImmediate(), 100);
772
- }
773
- cleanup() {
774
- this.clearHideTimeout();
775
- if (this.overlay) {
776
- this.overlay.style.display = "none";
777
- this.overlay.parentNode?.removeChild(this.overlay);
778
- this.overlay = null;
779
- }
780
- if (this.tooltip) {
781
- this.tooltip.style.display = "none";
782
- this.tooltip.parentNode?.removeChild(this.tooltip);
783
- this.tooltip = null;
784
- }
785
- if (this.currentElement) {
786
- this.currentElement.classList.remove("prepr-overlay-active");
787
- this.currentElement = null;
788
- }
789
- }
790
- };
791
-
792
- // src/core/stega/proximity.ts
793
- var debug5 = createScopedLogger("stega:proximity");
794
- var StegaProximity = class {
795
- constructor() {
796
- this.highlighted = /* @__PURE__ */ new Set();
797
- this.visible = /* @__PURE__ */ new Set();
798
- this.observer = null;
799
- this.getEncodedElements = createElementCache(
800
- "[data-prepr-encoded]",
801
- 200
802
- );
803
- }
804
- /** Call after the encoded element set changes — rebuilds the observer. */
805
- refreshObserving() {
806
- try {
807
- if (typeof IntersectionObserver === "undefined") return;
808
- if (this.observer) {
809
- this.observer.disconnect();
810
- this.observer = null;
811
- }
812
- this.visible = /* @__PURE__ */ new Set();
813
- this.observer = new IntersectionObserver(
814
- (entries) => {
815
- entries.forEach((entry) => {
816
- const el = entry.target;
817
- if (entry.isIntersecting) {
818
- this.visible.add(el);
819
- } else {
820
- this.visible.delete(el);
821
- }
822
- });
823
- },
824
- { root: null, rootMargin: "0px", threshold: 0 }
825
- );
826
- const nodes = this.getEncodedElements();
827
- nodes.forEach((el) => this.observer.observe(el));
828
- debug5.log("observing", nodes.length, "encoded elements");
829
- } catch (error) {
830
- debug5.log("error setting up IntersectionObserver", error);
831
- }
832
- }
833
- stopObserving() {
834
- if (this.observer) {
835
- this.observer.disconnect();
836
- this.observer = null;
837
- this.visible.clear();
838
- }
839
- }
840
- updateElementGradients(cursorX, cursorY) {
841
- const candidates = this.visible.size > 0 ? Array.from(this.visible) : Array.from(this.getEncodedElements());
842
- const newHighlighted = /* @__PURE__ */ new Set();
843
- candidates.forEach((element) => {
844
- const rect = element.getBoundingClientRect();
845
- const distance = Math.min(
846
- Math.abs(cursorX - rect.left),
847
- Math.abs(cursorX - rect.right),
848
- Math.abs(cursorY - rect.top),
849
- Math.abs(cursorY - rect.bottom)
850
- );
851
- if (distance < 150) {
852
- const relativeX = cursorX - rect.left;
853
- const relativeY = cursorY - rect.top;
854
- element.style.setProperty("--cursor-x", `${relativeX}px`);
855
- element.style.setProperty("--cursor-y", `${relativeY}px`);
856
- const baseGradientSize = Math.max(
857
- 150,
858
- Math.max(rect.width, rect.height) * 1.1
859
- );
860
- const distanceScale = Math.max(0, (400 - distance) / 400);
861
- const gradientSize = baseGradientSize * distanceScale;
862
- element.style.setProperty("--gradient-size", `${gradientSize}px`);
863
- element.classList.add("prepr-proximity-highlight");
864
- newHighlighted.add(element);
865
- } else {
866
- element.classList.remove("prepr-proximity-highlight");
867
- }
868
- });
869
- this.highlighted = newHighlighted;
870
- }
871
- clearAllHighlights() {
872
- this.highlighted.forEach(
873
- (element) => element.classList.remove("prepr-proximity-highlight")
874
- );
875
- this.highlighted.clear();
876
- }
877
- };
878
-
879
1318
  // src/core/stega/stega.generated.ts
880
- 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}';
1319
+ var STEGA_STYLES = '@property --prepr-presence{syntax:"<number>";inherits:true;initial-value:1}[data-prepr-encoded]{outline:1px dashed rgb(79 70 229/calc(.3 * var(--prepr-presence,1)));outline-offset:2px}';
881
1320
 
882
1321
  // src/core/stega/styles.ts
883
1322
  var STEGA_STYLE_MARKER = "data-prepr-stega";
@@ -897,45 +1336,80 @@ function removeStegaStyles() {
897
1336
  }
898
1337
 
899
1338
  // src/core/stega/scan.ts
900
- var debug6 = createScopedLogger("stega:scan");
1339
+ var debug5 = createScopedLogger("stega:scan");
1340
+ var ENCODED_SELECTOR = "[data-prepr-encoded]";
1341
+ var PRESENCE_FADE_MS = 400;
901
1342
  var EDITOR_MODE_STYLES = "[data-prepr-encoded]{cursor:pointer}";
1343
+ function fadeInPresence() {
1344
+ const root = document.documentElement;
1345
+ if (typeof root.animate !== "function") return;
1346
+ if (window.matchMedia?.("(prefers-reduced-motion: reduce)").matches) return;
1347
+ root.animate(
1348
+ { "--prepr-presence": ["0", "1"] },
1349
+ { duration: PRESENCE_FADE_MS, easing: "ease-out" }
1350
+ );
1351
+ }
902
1352
  function createStegaController(opts) {
903
1353
  const tooltipEnabled = opts.tooltip ?? true;
904
- const overlay = new StegaOverlay(opts.onEdit);
905
- const proximity = new StegaProximity();
1354
+ const locale = opts.locale ?? "en";
906
1355
  const elements = new StegaElements();
1356
+ const overlay = new VisualEditorOverlay({
1357
+ onLabelClick: (target) => opts.onEdit(readEditPayload(target))
1358
+ });
907
1359
  let initialized = false;
908
- let throttledMouseMove = null;
909
- const handleTooltipMouseEnter = () => {
910
- overlay.clearHideTimeout();
1360
+ let pointerFrame = null;
1361
+ let pointerTarget = null;
1362
+ let pointerX = 0;
1363
+ let pointerY = 0;
1364
+ let hitTestPending = false;
1365
+ const updateHover = () => {
1366
+ pointerFrame = null;
1367
+ if (hitTestPending) {
1368
+ hitTestPending = false;
1369
+ pointerTarget = document.elementFromPoint(pointerX, pointerY);
1370
+ }
1371
+ if (overlay.owns(pointerTarget)) return;
1372
+ const encoded = pointerTarget instanceof Element ? pointerTarget.closest(ENCODED_SELECTOR) : null;
1373
+ overlay.setHover(
1374
+ encoded,
1375
+ encoded ? fieldLabel(encoded.getAttribute("data-prepr-field"), locale) : ""
1376
+ );
911
1377
  };
912
- const handleTooltipMouseLeave = () => {
913
- if (!overlay.hasCurrentElement()) {
914
- overlay.hide();
1378
+ const scheduleHover = () => {
1379
+ if (pointerFrame === null) {
1380
+ pointerFrame = requestAnimationFrame(updateHover);
915
1381
  }
916
1382
  };
1383
+ const handlePointer = (e2) => {
1384
+ pointerTarget = e2.target;
1385
+ pointerX = e2.clientX;
1386
+ pointerY = e2.clientY;
1387
+ hitTestPending = false;
1388
+ scheduleHover();
1389
+ };
1390
+ const handlePointerLeave = () => {
1391
+ pointerTarget = null;
1392
+ hitTestPending = false;
1393
+ overlay.setHover(null, "");
1394
+ };
917
1395
  const handleScroll = () => {
918
- overlay.hideImmediate();
919
- proximity.clearAllHighlights();
1396
+ overlay.onScroll();
1397
+ if (pointerTarget === null) return;
1398
+ hitTestPending = true;
1399
+ scheduleHover();
920
1400
  };
1401
+ const handleResize = () => overlay.schedule();
921
1402
  const handleClick = (e2) => {
922
1403
  const target = e2.target;
923
- const encoded = target?.closest(
924
- "[data-prepr-encoded]"
925
- );
1404
+ const encoded = target?.closest(ENCODED_SELECTOR);
926
1405
  if (!encoded) return;
927
1406
  e2.preventDefault();
928
1407
  e2.stopPropagation();
929
- opts.onEdit({
930
- href: encoded.getAttribute("data-prepr-href") ?? "",
931
- origin: encoded.getAttribute("data-prepr-origin") ?? "",
932
- id: encoded.getAttribute("data-prepr-id") ?? void 0,
933
- field: encoded.getAttribute("data-prepr-field") ?? void 0
934
- });
1408
+ opts.onEdit(readEditPayload(encoded));
935
1409
  };
936
1410
  const start = () => {
937
1411
  if (initialized) {
938
- debug6.log("already initialized, skipping setup");
1412
+ debug5.log("already initialized, skipping setup");
939
1413
  return;
940
1414
  }
941
1415
  if (!tooltipEnabled) {
@@ -945,71 +1419,94 @@ function createStegaController(opts) {
945
1419
  });
946
1420
  document.addEventListener("click", handleClick, { capture: true });
947
1421
  initialized = true;
948
- debug6.log("stega controller started (editor mode)");
1422
+ debug5.log("stega controller started (headless)");
949
1423
  return;
950
1424
  }
951
1425
  injectStegaStyles();
952
- const { tooltip } = overlay.create();
953
- tooltip.addEventListener("mouseenter", handleTooltipMouseEnter);
954
- tooltip.addEventListener("mouseleave", handleTooltipMouseLeave);
1426
+ fadeInPresence();
1427
+ overlay.mount();
955
1428
  elements.scanDocument(true);
956
- proximity.refreshObserving();
957
- elements.setupMutationObserver(() => proximity.refreshObserving());
958
- throttledMouseMove = throttle((e2) => {
959
- const mouseEvent = e2;
960
- const target = mouseEvent.target;
961
- if (!target) return;
962
- if (target.closest(".prepr-tooltip")) return;
963
- proximity.updateElementGradients(mouseEvent.clientX, mouseEvent.clientY);
964
- const encoded = target.closest("[data-prepr-encoded]");
965
- if (encoded) {
966
- overlay.show(encoded);
967
- } else {
968
- overlay.hide();
969
- }
970
- }, 16);
971
- document.addEventListener("mousemove", throttledMouseMove, {
972
- passive: true
1429
+ elements.setupMutationObserver(() => {
1430
+ if (pointerTarget === null) return;
1431
+ hitTestPending = true;
1432
+ scheduleHover();
973
1433
  });
1434
+ document.addEventListener("pointermove", handlePointer, { passive: true });
1435
+ document.addEventListener("pointerdown", handlePointer, { passive: true });
1436
+ document.documentElement.addEventListener(
1437
+ "pointerleave",
1438
+ handlePointerLeave
1439
+ );
974
1440
  window.addEventListener("scroll", handleScroll, {
975
1441
  capture: true,
976
1442
  passive: true
977
1443
  });
1444
+ window.addEventListener("resize", handleResize, { passive: true });
978
1445
  initialized = true;
979
- debug6.log("stega controller started");
1446
+ debug5.log("stega controller started");
980
1447
  };
981
1448
  const stop = () => {
982
1449
  if (!initialized) return;
1450
+ initialized = false;
983
1451
  if (!tooltipEnabled) {
984
1452
  document.removeEventListener("click", handleClick, { capture: true });
985
- elements.cleanup();
1453
+ elements.disconnect();
986
1454
  removeStegaStyles();
987
- initialized = false;
988
- debug6.log("stega controller stopped (editor mode)");
1455
+ debug5.log("stega controller stopped (headless)");
989
1456
  return;
990
1457
  }
991
- if (throttledMouseMove) {
992
- document.removeEventListener("mousemove", throttledMouseMove);
993
- throttledMouseMove.cancel();
994
- throttledMouseMove = null;
995
- }
1458
+ if (pointerFrame !== null) cancelAnimationFrame(pointerFrame);
1459
+ pointerFrame = null;
1460
+ pointerTarget = null;
1461
+ hitTestPending = false;
1462
+ document.removeEventListener("pointermove", handlePointer);
1463
+ document.removeEventListener("pointerdown", handlePointer);
1464
+ document.documentElement.removeEventListener(
1465
+ "pointerleave",
1466
+ handlePointerLeave
1467
+ );
996
1468
  window.removeEventListener("scroll", handleScroll, { capture: true });
997
- const tooltip = overlay.getTooltip();
998
- if (tooltip) {
999
- tooltip.removeEventListener("mouseenter", handleTooltipMouseEnter);
1000
- tooltip.removeEventListener("mouseleave", handleTooltipMouseLeave);
1001
- }
1002
- overlay.cleanup();
1003
- proximity.clearAllHighlights();
1004
- proximity.stopObserving();
1005
- elements.cleanup();
1469
+ window.removeEventListener("resize", handleResize);
1470
+ overlay.unmount();
1471
+ elements.disconnect();
1006
1472
  removeStegaStyles();
1007
- initialized = false;
1008
- debug6.log("stega controller stopped");
1473
+ debug5.log("stega controller stopped");
1009
1474
  };
1010
1475
  return { start, stop };
1011
1476
  }
1012
1477
 
1478
+ // src/core/stega/scroll-to-field.ts
1479
+ var debug6 = createScopedLogger("stega:scroll");
1480
+ var FLASH_DURATION_MS = 1200;
1481
+ var flashTimeout = null;
1482
+ function scrollToField(request) {
1483
+ const { field, id } = request;
1484
+ if (!field) return false;
1485
+ const candidates = document.querySelectorAll(
1486
+ "[data-prepr-encoded]"
1487
+ );
1488
+ let target = null;
1489
+ for (const candidate of candidates) {
1490
+ if (candidate.getAttribute("data-prepr-field") !== field) continue;
1491
+ if (id && candidate.getAttribute("data-prepr-id") !== id) continue;
1492
+ target = candidate;
1493
+ break;
1494
+ }
1495
+ if (!target) {
1496
+ debug6.log("no element found for field", field);
1497
+ return false;
1498
+ }
1499
+ target.scrollIntoView({ behavior: "smooth", block: "center" });
1500
+ if (flashTimeout) clearTimeout(flashTimeout);
1501
+ document.querySelectorAll(".prepr-overlay-active").forEach((el) => el.classList.remove("prepr-overlay-active"));
1502
+ target.classList.add("prepr-overlay-active");
1503
+ flashTimeout = setTimeout(() => {
1504
+ target.classList.remove("prepr-overlay-active");
1505
+ flashTimeout = null;
1506
+ }, FLASH_DURATION_MS);
1507
+ return true;
1508
+ }
1509
+
1013
1510
  // src/core/store.ts
1014
1511
  var DEFAULT_STATE = {
1015
1512
  locale: "en",
@@ -1144,7 +1641,7 @@ function splitPath(full) {
1144
1641
  return [full.slice(0, index), full.slice(index + 1)];
1145
1642
  }
1146
1643
 
1147
- // ../../node_modules/.pnpm/preact@10.29.7/node_modules/preact/dist/preact.mjs
1644
+ // ../../node_modules/.pnpm/preact@10.29.8/node_modules/preact/dist/preact.mjs
1148
1645
  var n;
1149
1646
  var l;
1150
1647
  var u;
@@ -1215,8 +1712,8 @@ function H() {
1215
1712
  }
1216
1713
  }
1217
1714
  function L(n2, l2, u3, t3, i2, r2, o2, e2, f3, c2, a2) {
1218
- var s2, h2, p2, v2, y2, _2, g2, m2 = t3 && t3.__k || w, b2 = l2.length;
1219
- for (f3 = T(u3, l2, m2, f3, b2), s2 = 0; s2 < b2; s2++) null != (p2 = u3.__k[s2]) && (h2 = -1 != p2.__i && m2[p2.__i] || d, p2.__i = s2, _2 = q(n2, p2, h2, i2, r2, o2, e2, f3, c2, a2), v2 = p2.__e, p2.ref && h2.ref != p2.ref && (h2.ref && J(h2.ref, null, p2), a2.push(p2.ref, p2.__c || v2, p2)), null == y2 && null != v2 && (y2 = v2), (g2 = !!(4 & p2.__u)) || h2.__k === p2.__k ? (f3 = j(p2, f3, n2, g2), g2 && h2.__e && (h2.__e = null)) : "function" == typeof p2.type && void 0 !== _2 ? f3 = _2 : v2 && (f3 = v2.nextSibling), p2.__u &= -7);
1715
+ var s2, h2, p2, v2, y2, _2, g2 = t3 && t3.__k || w, m2 = l2.length;
1716
+ for (f3 = T(u3, l2, g2, f3, m2), s2 = 0; s2 < m2; s2++) null != (p2 = u3.__k[s2]) && (h2 = -1 != p2.__i && g2[p2.__i] || d, p2.__i = s2, _2 = q(n2, p2, h2, i2, r2, o2, e2, f3, c2, a2), v2 = p2.__e, p2.ref && h2.ref != p2.ref && (h2.ref && J(h2.ref, null, p2), a2.push(p2.ref, p2.__c || v2, p2)), null == y2 && null != v2 && (y2 = v2), 4 & p2.__u ? (f3 = j(p2, f3, n2), h2.__e && (h2.__e = null)) : "function" == typeof p2.type && void 0 !== _2 ? f3 = _2 : v2 && (f3 = v2.nextSibling), p2.__u &= -7);
1220
1717
  return u3.__e = y2, f3;
1221
1718
  }
1222
1719
  function T(n2, l2, u3, t3, i2) {
@@ -1225,13 +1722,13 @@ function T(n2, l2, u3, t3, i2) {
1225
1722
  if (s2) for (r2 = 0; r2 < a2; r2++) null != (e2 = u3[r2]) && 0 == (2 & e2.__u) && (e2.__e == t3 && (t3 = $(e2)), K(e2, e2));
1226
1723
  return t3;
1227
1724
  }
1228
- function j(n2, l2, u3, t3) {
1229
- var i2, r2;
1725
+ function j(n2, l2, u3) {
1726
+ var t3, i2;
1230
1727
  if ("function" == typeof n2.type) {
1231
- for (i2 = n2.__k, r2 = 0; i2 && r2 < i2.length; r2++) i2[r2] && (i2[r2].__ = n2, l2 = j(i2[r2], l2, u3, t3));
1728
+ for (t3 = n2.__k, i2 = 0; t3 && i2 < t3.length; i2++) t3[i2] && (t3[i2].__ = n2, l2 = j(t3[i2], l2, u3));
1232
1729
  return l2;
1233
1730
  }
1234
- n2.__e != l2 && (t3 && (l2 && n2.type && !l2.parentNode && (l2 = $(n2)), u3.insertBefore(n2.__e, l2 || null)), l2 = n2.__e);
1731
+ n2.__e != l2 && (l2 && n2.type && !l2.parentNode && (l2 = $(n2)), l2 = u3.insertBefore(n2.__e, l2 || null));
1235
1732
  do {
1236
1733
  l2 = l2 && l2.nextSibling;
1237
1734
  } while (null != l2 && 8 == l2.nodeType);
@@ -1277,35 +1774,35 @@ function V(n2) {
1277
1774
  };
1278
1775
  }
1279
1776
  function q(n2, u3, t3, i2, r2, o2, e2, f3, c2, a2) {
1280
- var s2, h2, p2, v2, y2, d2, _2, k2, x2, M, $2, I2, P2, A2, H2, T2, j2 = u3.type;
1777
+ var s2, h2, p2, v2, y2, d2, _2, k2, x2, M, I2, P2, A2, H2, T2, j2, F = u3.type;
1281
1778
  if (void 0 !== u3.constructor) return null;
1282
1779
  128 & t3.__u && (c2 = !!(32 & t3.__u), o2 = [f3 = u3.__e = t3.__e]), (s2 = l.__b) && s2(u3);
1283
- n: if ("function" == typeof j2) {
1780
+ n: if ("function" == typeof F) {
1284
1781
  h2 = e2.length;
1285
1782
  try {
1286
- if (x2 = u3.props, M = j2.prototype && j2.prototype.render, $2 = (s2 = j2.contextType) && i2[s2.__c], I2 = s2 ? $2 ? $2.props.value : s2.__ : i2, t3.__c ? k2 = (p2 = u3.__c = t3.__c).__ = p2.__E : (M ? u3.__c = p2 = new j2(x2, I2) : (u3.__c = p2 = new C(x2, I2), p2.constructor = j2, p2.render = Q), $2 && $2.sub(p2), p2.state || (p2.state = {}), p2.__n = i2, v2 = p2.__d = true, p2.__h = [], p2._sb = []), M && null == p2.__s && (p2.__s = p2.state), M && null != j2.getDerivedStateFromProps && (p2.__s == p2.state && (p2.__s = m({}, p2.__s)), m(p2.__s, j2.getDerivedStateFromProps(x2, p2.__s))), y2 = p2.props, d2 = p2.state, p2.__v = u3, v2) M && null == j2.getDerivedStateFromProps && null != p2.componentWillMount && p2.componentWillMount(), M && null != p2.componentDidMount && p2.__h.push(p2.componentDidMount);
1783
+ if (x2 = u3.props, M = F.prototype && F.prototype.render, I2 = (s2 = F.contextType) && i2[s2.__c], P2 = s2 ? I2 ? I2.props.value : s2.__ : i2, t3.__c ? k2 = (p2 = u3.__c = t3.__c).__ = p2.__E : (M ? u3.__c = p2 = new F(x2, P2) : (u3.__c = p2 = new C(x2, P2), p2.constructor = F, p2.render = Q), I2 && I2.sub(p2), p2.state || (p2.state = {}), p2.__n = i2, v2 = p2.__d = true, p2.__h = [], p2._sb = []), M && null == p2.__s && (p2.__s = p2.state), M && null != F.getDerivedStateFromProps && (p2.__s == p2.state && (p2.__s = m({}, p2.__s)), m(p2.__s, F.getDerivedStateFromProps(x2, p2.__s))), y2 = p2.props, d2 = p2.state, p2.__v = u3, v2) M && null == F.getDerivedStateFromProps && null != p2.componentWillMount && p2.componentWillMount(), M && null != p2.componentDidMount && p2.__h.push(p2.componentDidMount);
1287
1784
  else {
1288
- if (M && null == j2.getDerivedStateFromProps && x2 !== y2 && null != p2.componentWillReceiveProps && p2.componentWillReceiveProps(x2, I2), u3.__v == t3.__v || !p2.__e && null != p2.shouldComponentUpdate && false === p2.shouldComponentUpdate(x2, p2.__s, I2)) {
1785
+ if (M && null == F.getDerivedStateFromProps && x2 !== y2 && null != p2.componentWillReceiveProps && p2.componentWillReceiveProps(x2, P2), u3.__v == t3.__v || !p2.__e && null != p2.shouldComponentUpdate && false === p2.shouldComponentUpdate(x2, p2.__s, P2)) {
1289
1786
  u3.__v != t3.__v && (p2.props = x2, p2.state = p2.__s, p2.__d = false), u3.__e = t3.__e, u3.__k = t3.__k, u3.__k.some(function(n3) {
1290
1787
  n3 && (n3.__ = u3);
1291
- }), w.push.apply(p2.__h, p2._sb), p2._sb = [], p2.__h.length && e2.push(p2);
1788
+ }), w.push.apply(p2.__h, p2._sb), p2._sb = [], p2.__h.length && e2.push(p2), f3 = $(t3);
1292
1789
  break n;
1293
1790
  }
1294
- null != p2.componentWillUpdate && p2.componentWillUpdate(x2, p2.__s, I2), M && null != p2.componentDidUpdate && p2.__h.push(function() {
1791
+ null != p2.componentWillUpdate && p2.componentWillUpdate(x2, p2.__s, P2), M && null != p2.componentDidUpdate && p2.__h.push(function() {
1295
1792
  p2.componentDidUpdate(y2, d2, _2);
1296
1793
  });
1297
1794
  }
1298
- if (p2.context = I2, p2.props = x2, p2.__P = n2, p2.__e = false, P2 = l.__r, A2 = 0, M) p2.state = p2.__s, p2.__d = false, P2 && P2(u3), s2 = p2.render(p2.props, p2.state, p2.context), w.push.apply(p2.__h, p2._sb), p2._sb = [];
1795
+ if (p2.context = P2, p2.props = x2, p2.__P = n2, p2.__e = false, A2 = l.__r, H2 = 0, M) p2.state = p2.__s, p2.__d = false, A2 && A2(u3), s2 = p2.render(p2.props, p2.state, p2.context), w.push.apply(p2.__h, p2._sb), p2._sb = [];
1299
1796
  else do {
1300
- p2.__d = false, P2 && P2(u3), s2 = p2.render(p2.props, p2.state, p2.context), p2.state = p2.__s;
1301
- } while (p2.__d && ++A2 < 25);
1302
- p2.state = p2.__s, null != p2.getChildContext && (i2 = m(m({}, i2), p2.getChildContext())), M && !v2 && null != p2.getSnapshotBeforeUpdate && (_2 = p2.getSnapshotBeforeUpdate(y2, d2)), H2 = null != s2 && s2.type === S && null == s2.key ? E(s2.props.children) : s2, f3 = L(n2, g(H2) ? H2 : [H2], u3, t3, i2, r2, o2, e2, f3, c2, a2), p2.base = u3.__e, u3.__u &= -161, p2.__h.length && e2.push(p2), k2 && (p2.__E = p2.__ = null);
1797
+ p2.__d = false, A2 && A2(u3), s2 = p2.render(p2.props, p2.state, p2.context), p2.state = p2.__s;
1798
+ } while (p2.__d && ++H2 < 25);
1799
+ p2.state = p2.__s, null != p2.getChildContext && (i2 = m(m({}, i2), p2.getChildContext())), M && !v2 && null != p2.getSnapshotBeforeUpdate && (_2 = p2.getSnapshotBeforeUpdate(y2, d2)), T2 = null != s2 && s2.type === S && null == s2.key ? E(s2.props.children) : s2, f3 = L(n2, g(T2) ? T2 : [T2], u3, t3, i2, r2, o2, e2, f3, c2, a2), p2.base = u3.__e, u3.__u &= -161, p2.__h.length && e2.push(p2), k2 && (p2.__E = p2.__ = null);
1303
1800
  } catch (n3) {
1304
1801
  if (e2.length = h2, u3.__v = null, c2 || null != o2) {
1305
1802
  if (n3.then) {
1306
1803
  for (u3.__u |= c2 ? 160 : 128; f3 && 8 == f3.nodeType && f3.nextSibling; ) f3 = f3.nextSibling;
1307
1804
  null != o2 && (o2[o2.indexOf(f3)] = null), u3.__e = f3;
1308
- } else if (null != o2) for (T2 = o2.length; T2--; ) b(o2[T2]);
1805
+ } else if (null != o2) for (j2 = o2.length; j2--; ) b(o2[j2]);
1309
1806
  } else u3.__e = t3.__e;
1310
1807
  null == u3.__k && (u3.__k = t3.__k || []), n3.then || B(u3), l.__e(n3, u3, t3);
1311
1808
  }
@@ -1426,9 +1923,9 @@ var CLOSE_ICON_SVG = `<svg width="10" height="10" viewBox="0 0 10 10" fill="none
1426
1923
  </svg>`;
1427
1924
 
1428
1925
  // src/core/ui/toolbar.generated.ts
1429
- 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}}";
1926
+ var TOOLBAR_CSS = ":host{--prepr-primary:#4338ca;--prepr-bg:#eef2ff;--prepr-text:#1f2937;--prepr-radius:8px;--prepr-shadow:0px 0px 40px 0px #1f29373d;--prepr-z-index:10000;--prepr-primary-50:#eef2ff;--prepr-primary-100:#e0e7ff;--prepr-primary-800:#3730a3;--prepr-indigo-600:#4f46e5;--prepr-indigo-50:#eef2ff;--prepr-indigo-700:#4338ca;--prepr-secondary-400:#fb923c;--prepr-secondary-500:#f97316;--prepr-gray-100:#f3f4f6;--prepr-gray-200:#e5e7eb;--prepr-gray-300:#d1d5db;--prepr-gray-400:#9ca3af;--prepr-gray-500:#6b7280;--prepr-gray-700:#374151;--prepr-gray-800:#1f2937;--prepr-gray-900:#111827;--prepr-shadow-sm:0px 0px 12px 0px #c3c3c3cc;color:var(--prepr-text);box-sizing:border-box;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica,Arial,sans-serif;font-size:14px;line-height:1.5}:host *,:host :before,:host :after,*,:before,:after{box-sizing:border-box}button{font-family:inherit;font-size:inherit;margin:0}.prepr-regular-text{font-size:.875rem;font-weight:500;line-height:1.5}.prepr-backdrop{z-index:calc(var(--prepr-z-index) - 1);opacity:0;pointer-events:none;background:#0000001a;transition:opacity .2s ease-in-out;position:fixed;top:0;bottom:0;left:0;right:0}.prepr-backdrop[data-open=true]{opacity:1;pointer-events:auto;animation:.2s ease-in-out prepr-fade-in}.prepr-container{z-index:var(--prepr-z-index);justify-content:flex-end;align-items:center;display:flex;position:fixed;top:calc(50% - 64px);right:0}.prepr-toggle-holder{padding:.5rem}.prepr-toggle{cursor:pointer;background:var(--prepr-primary);color:#fff;border:none;border-radius:9999px;justify-content:center;align-items:center;width:2.25rem;height:2.25rem;display:flex}.prepr-toggle>span{display:flex}.prepr-toggle svg{color:#fff}.prepr-panel{z-index:var(--prepr-z-index);opacity:0;pointer-events:none;justify-content:flex-end;padding:.75rem;transition:opacity .2s ease-in-out;display:flex;position:fixed;top:50%;right:0;transform:translateY(-50%)}.prepr-panel[data-open=true]{opacity:1;pointer-events:auto;animation:.2s ease-in-out prepr-fade-in}.prepr-content{box-shadow:var(--prepr-shadow);border-radius:var(--prepr-radius);background:var(--prepr-bg);flex-direction:column;gap:2.5rem;width:100%;padding:1.5rem;display:flex}@media (min-width:640px){.prepr-panel{margin-right:4rem;padding:0}.prepr-content{width:502px;padding:2.5rem}}.prepr-header{justify-content:space-between;align-items:center;gap:.5rem;display:flex}.prepr-header-left{align-items:flex-start;gap:.75rem;display:flex}.prepr-badge{background:var(--prepr-primary-100);height:1.5rem;color:var(--prepr-primary);border-radius:4px;align-items:center;padding:.25rem .75rem;font-size:.875rem;display:flex}.prepr-header-close{cursor:pointer;background:var(--prepr-primary-100);width:2rem;height:2rem;color:var(--prepr-gray-700);border:none;border-radius:6px;justify-content:center;align-items:center;display:flex}.prepr-section{flex-direction:column;gap:.5rem;display:flex}.prepr-section-label{color:var(--prepr-gray-400);font-size:.875rem}.prepr-row{flex-wrap:wrap;justify-content:space-between;align-items:center;gap:.5rem 1.5rem;display:flex}.prepr-row-title{color:var(--prepr-gray-800);margin:0;font-size:1rem;font-weight:600}.prepr-radiogroup{border-radius:var(--prepr-radius);border:1px solid var(--prepr-gray-300);background:#fff;align-items:center;gap:.25rem;height:2.5rem;padding:.25rem;display:flex}.prepr-radiogroup[data-disabled=true]{cursor:not-allowed;opacity:.5}.prepr-radiogroup[data-disabled=true] .prepr-radio{pointer-events:none}.prepr-radio{cursor:pointer;white-space:nowrap;text-align:center;height:2rem;color:var(--prepr-gray-900);background:0 0;border:none;border-radius:6px;justify-content:center;align-items:center;padding:.5rem 1.125rem;font-size:.875rem;font-weight:500;transition:all .2s ease-in-out;display:flex}.prepr-radio[data-checked=true]{background:var(--prepr-indigo-600);color:#fff;box-shadow:var(--prepr-shadow-sm)}.prepr-radio[data-checked=true][data-off=true]{background:var(--prepr-gray-800);color:#fff}.prepr-radio-variant{width:82px}.prepr-listbox{position:relative}.prepr-segment-button{border-radius:var(--prepr-radius);border:1px solid var(--prepr-gray-300);width:240px;height:2.5rem;color:var(--prepr-gray-500);cursor:pointer;background:#fff;flex-shrink:0;align-items:center;gap:.5rem;padding:0 .5rem;font-size:.875rem;font-weight:500;display:flex;overflow:hidden}@media (min-width:768px){.prepr-segment-button{padding:0 1rem}}.prepr-segment-button:disabled{cursor:not-allowed;background:var(--prepr-gray-200);color:var(--prepr-gray-400)}.prepr-segment-button[aria-expanded=true]{border-bottom-color:#fff;border-bottom-right-radius:0;border-bottom-left-radius:0}.prepr-segment-label{white-space:nowrap;text-overflow:ellipsis;text-align:left;width:100%;margin-right:auto;overflow:hidden}.prepr-segment-caret{color:var(--prepr-gray-800);display:flex}.prepr-options{z-index:calc(var(--prepr-z-index) + 1);border-left:1px solid var(--prepr-gray-300);border-right:1px solid var(--prepr-gray-300);border-bottom:1px solid var(--prepr-gray-300);-ms-overflow-style:none;scrollbar-width:none;background:#fff;border-radius:0 0 6px 6px;width:240px;max-height:300px;margin:0;padding:0;list-style:none;position:absolute;top:100%;left:0;overflow-y:auto;box-shadow:0 20px 25px -5px #0000001a,0 8px 10px -6px #0000001a}.prepr-options::-webkit-scrollbar{display:none}.prepr-options[hidden]{display:none}.prepr-option-search{background:#fff;padding:.5rem;position:sticky;top:0}.prepr-segment-search{border:1px solid var(--prepr-gray-300);border-radius:var(--prepr-radius);width:100%;height:2rem;font:inherit;color:var(--prepr-gray-900);box-sizing:border-box;background:#fff;padding:0 .5rem;font-size:.875rem}@media (pointer:coarse){.prepr-segment-search{font-size:16px}}.prepr-segment-search::placeholder{color:var(--prepr-gray-400)}.prepr-segment-search:focus{border-color:var(--prepr-gray-400);outline:none}.prepr-option{cursor:pointer;width:100%;color:var(--prepr-gray-900);background:#fff;align-items:center;padding:.5rem 1rem;font-size:.875rem;font-weight:500;display:flex}.prepr-option:hover{background:var(--prepr-gray-100)}.prepr-option[aria-selected=true]{background:var(--prepr-indigo-50);color:var(--prepr-indigo-700)}.prepr-option-label{white-space:nowrap;text-overflow:ellipsis;text-align:left;width:100%;margin-right:auto;overflow:hidden}.prepr-reset{background:var(--prepr-secondary-400);color:#fff;cursor:pointer;border:none;border-radius:6px;justify-content:center;align-items:center;gap:.5rem;width:100%;height:2.5rem;padding:.5rem .75rem;font-size:.875rem;font-weight:500;display:flex}.prepr-reset:hover:not(:disabled){background:var(--prepr-secondary-500)}.prepr-reset:disabled{background:var(--prepr-gray-400);color:var(--prepr-gray-500);cursor:not-allowed}@media (min-width:768px){.prepr-reset{width:108px}}.prepr-indicators{z-index:var(--prepr-z-index);gap:.75rem;display:flex;position:fixed;bottom:1.5rem;right:1.5rem}.prepr-status-pill{background:var(--prepr-primary);color:#fff;cursor:pointer;border:none;border-radius:9999px;align-items:center;gap:.5rem;padding:.5rem 1rem;font-size:.75rem;font-weight:500;transition:background-color .15s;display:flex;box-shadow:0 10px 15px -3px #0000001a,0 4px 6px -4px #0000001a}.prepr-status-pill[data-interactive=false]{cursor:default;opacity:1;color:#fff}.prepr-status-pill[data-interactive=true]:hover{background:var(--prepr-primary-800)}.prepr-status-pill[hidden]{display:none}.prepr-status-viewing{color:#fff9;font-size:10px}.prepr-status-segment{text-overflow:ellipsis;white-space:nowrap;max-width:120px;display:inline-block;overflow:hidden}.prepr-status-variant{text-overflow:ellipsis;white-space:nowrap;background:#fff3;border-radius:4px;max-width:80px;padding:0 .5rem;display:inline-block;overflow:hidden}.prepr-status-x{color:#fff;width:.75rem;height:.75rem}.prepr-close-edit-pill{background:var(--prepr-primary-50);color:var(--prepr-gray-800);cursor:pointer;border:none;border-radius:9999px;align-items:center;gap:.5rem;padding:.5rem 1rem;font-size:.75rem;font-weight:500;transition:background-color .15s;display:flex;box-shadow:0 10px 15px -3px #0000001a,0 4px 6px -4px #0000001a}.prepr-close-edit-pill:hover{background:var(--prepr-primary-100)}.prepr-close-edit-pill[hidden]{display:none}.prepr-close-edit-x{width:1rem;height:1rem;color:var(--prepr-gray-500);transition:color .15s}.prepr-close-edit-pill:hover .prepr-close-edit-x{color:var(--prepr-gray-700)}.prepr-tip{z-index:calc(var(--prepr-z-index) + 2);overflow-wrap:break-word;background:var(--prepr-gray-900);color:#fff;pointer-events:none;border-radius:6px;width:auto;max-width:min(420px,100vw - 16px);padding:.5rem .75rem;font-size:.75rem;font-weight:500;position:fixed;box-shadow:0 20px 25px -5px #0000001a,0 8px 10px -6px #0000001a}.prepr-tip-arrow{border-left:6px solid #0000;border-right:6px solid #0000;border-top:6px solid var(--prepr-gray-900);width:0;height:0;position:absolute;top:100%;transform:translate(-50%)}[hidden]{display:none!important}@keyframes prepr-fade-in{0%{opacity:0}to{opacity:1}}";
1430
1927
 
1431
- // ../../node_modules/.pnpm/preact@10.29.7/node_modules/preact/jsx-runtime/dist/jsxRuntime.mjs
1928
+ // ../../node_modules/.pnpm/preact@10.29.8/node_modules/preact/jsx-runtime/dist/jsxRuntime.mjs
1432
1929
  var f2 = 0;
1433
1930
  function u2(e2, t3, n2, o2, i2, u3) {
1434
1931
  t3 || (t3 = {});
@@ -2121,8 +2618,742 @@ function definePreprToolbar() {
2121
2618
  }
2122
2619
  }
2123
2620
 
2621
+ // src/core/visual-editor/asset-images.ts
2622
+ var MAX_URL_LENGTH = 4096;
2623
+ var MIN_KEY_SEGMENTS = 2;
2624
+ var TRANSFORM_OPTION = /^[a-z]{1,3}_[\w.-]*$/i;
2625
+ var FORMAT_PLACEHOLDER = "{format}";
2626
+ var PROXY_PARAMS = ["url", "href", "src", "image"];
2627
+ var ABSOLUTE_HTTP = /^https?:\/\//i;
2628
+ var SOURCE_URL_ATTRIBUTES = ["src", "data-src"];
2629
+ var SOURCE_SET_ATTRIBUTES = ["srcset", "data-srcset"];
2630
+ var ASSET_ATTR = "data-prepr-asset";
2631
+ var ASSET_SOURCE_ATTRIBUTES = [
2632
+ ...SOURCE_URL_ATTRIBUTES,
2633
+ ...SOURCE_SET_ATTRIBUTES
2634
+ ];
2635
+ var TAG_ATTRIBUTES = [
2636
+ "data-prepr-encoded",
2637
+ "data-prepr-id",
2638
+ "data-prepr-field",
2639
+ "data-prepr-entry-id",
2640
+ ASSET_ATTR
2641
+ ];
2642
+ function decodeSegment(segment) {
2643
+ try {
2644
+ return decodeURIComponent(segment);
2645
+ } catch {
2646
+ return segment;
2647
+ }
2648
+ }
2649
+ function isTransformSegment(segment) {
2650
+ return segment === FORMAT_PLACEHOLDER || segment.split(",").every((option) => TRANSFORM_OPTION.test(option));
2651
+ }
2652
+ function assetKey(url, depth = 0) {
2653
+ if (!url || url.length > MAX_URL_LENGTH) return null;
2654
+ let parsed;
2655
+ try {
2656
+ parsed = new URL(url, "https://base.invalid");
2657
+ } catch {
2658
+ return null;
2659
+ }
2660
+ if (parsed.protocol !== "https:" && parsed.protocol !== "http:") return null;
2661
+ if (depth === 0) {
2662
+ for (const name of PROXY_PARAMS) {
2663
+ const inner = parsed.searchParams.get(name);
2664
+ if (inner && ABSOLUTE_HTTP.test(inner)) return assetKey(inner, 1);
2665
+ }
2666
+ }
2667
+ const segments = parsed.pathname.split("/").filter(Boolean).map(decodeSegment);
2668
+ const file = segments.pop();
2669
+ if (!file) return null;
2670
+ const kept = segments.filter((segment) => !isTransformSegment(segment));
2671
+ kept.push(file);
2672
+ return kept.length >= MIN_KEY_SEGMENTS ? kept.join("/") : null;
2673
+ }
2674
+ function srcsetUrls(srcset) {
2675
+ const urls = [];
2676
+ let expectUrl = true;
2677
+ for (const token of srcset.split(/\s+/)) {
2678
+ if (!token) continue;
2679
+ if (!expectUrl) {
2680
+ const comma = token.indexOf(",");
2681
+ if (comma === -1) continue;
2682
+ expectUrl = true;
2683
+ const rest = token.slice(comma + 1);
2684
+ if (!rest) continue;
2685
+ urls.push(rest.replace(/,+$/, ""));
2686
+ expectUrl = rest.endsWith(",");
2687
+ continue;
2688
+ }
2689
+ const url = token.replace(/^,+|,+$/g, "");
2690
+ if (url) urls.push(url);
2691
+ expectUrl = token.endsWith(",");
2692
+ }
2693
+ return urls;
2694
+ }
2695
+ function createAssetIndex(assets) {
2696
+ const index = /* @__PURE__ */ new Map();
2697
+ for (const asset of assets) {
2698
+ const key = assetKey(asset.key);
2699
+ if (key && !index.has(key)) index.set(key, asset);
2700
+ }
2701
+ return index;
2702
+ }
2703
+ function urlsOf(img) {
2704
+ const urls = [];
2705
+ const collect = (el) => {
2706
+ for (const name of SOURCE_URL_ATTRIBUTES) {
2707
+ const value = el.getAttribute(name);
2708
+ if (value) urls.push(value);
2709
+ }
2710
+ for (const name of SOURCE_SET_ATTRIBUTES) {
2711
+ const value = el.getAttribute(name);
2712
+ if (value) urls.push(...srcsetUrls(value));
2713
+ }
2714
+ };
2715
+ collect(img);
2716
+ const picture = img.parentElement;
2717
+ if (picture?.tagName === "PICTURE") {
2718
+ for (const source of picture.children) {
2719
+ if (source.tagName === "SOURCE") collect(source);
2720
+ }
2721
+ }
2722
+ return urls;
2723
+ }
2724
+ function matchAsset(img, index, keys) {
2725
+ for (const url of urlsOf(img)) {
2726
+ let key = keys.get(url);
2727
+ if (key === void 0) {
2728
+ key = assetKey(url);
2729
+ keys.set(url, key);
2730
+ }
2731
+ const asset = key ? index.get(key) : void 0;
2732
+ if (asset) return asset;
2733
+ }
2734
+ return void 0;
2735
+ }
2736
+ function setIfChanged(el, name, value) {
2737
+ if (value === null) {
2738
+ if (el.hasAttribute(name)) el.removeAttribute(name);
2739
+ } else if (el.getAttribute(name) !== value) {
2740
+ el.setAttribute(name, value);
2741
+ }
2742
+ }
2743
+ function untagAssetImage(img) {
2744
+ for (const name of TAG_ATTRIBUTES) img.removeAttribute(name);
2745
+ }
2746
+ var TEXT_FIELD_SELECTOR = `[data-prepr-encoded]:not([${ASSET_ATTR}])`;
2747
+ function besideOtherItem(img, isCurrentItem) {
2748
+ for (let block = img.parentElement; block && block !== document.body; block = block.parentElement) {
2749
+ const texts = block.querySelectorAll(TEXT_FIELD_SELECTOR);
2750
+ if (texts.length === 0) continue;
2751
+ for (const text of texts) {
2752
+ if (isCurrentItem(text)) return false;
2753
+ }
2754
+ return true;
2755
+ }
2756
+ return false;
2757
+ }
2758
+ function tagAssetImages(index, itemId, keys, options = {}) {
2759
+ const { isCurrentItem } = options;
2760
+ for (const img of document.getElementsByTagName("img")) {
2761
+ const ours = img.hasAttribute(ASSET_ATTR);
2762
+ if (!ours && img.hasAttribute("data-prepr-encoded")) continue;
2763
+ const match = index.size ? matchAsset(img, index, keys) : void 0;
2764
+ const asset = match && isCurrentItem && besideOtherItem(img, isCurrentItem) ? void 0 : match;
2765
+ if (!asset) {
2766
+ if (ours) untagAssetImage(img);
2767
+ continue;
2768
+ }
2769
+ setIfChanged(img, "data-prepr-encoded", "");
2770
+ setIfChanged(img, ASSET_ATTR, "");
2771
+ setIfChanged(img, "data-prepr-id", itemId);
2772
+ setIfChanged(img, "data-prepr-field", asset.field);
2773
+ setIfChanged(img, "data-prepr-entry-id", asset.entryId ?? null);
2774
+ }
2775
+ }
2776
+ function untagAssetImages() {
2777
+ document.querySelectorAll(`img[${ASSET_ATTR}]`).forEach(untagAssetImage);
2778
+ }
2779
+
2780
+ // src/core/visual-editor/navigation-guard.ts
2781
+ var debug7 = createScopedLogger("navigation-guard");
2782
+ var trimSlash = (path) => path.length > 1 && path.endsWith("/") ? path.slice(0, -1) : path;
2783
+ function resolve(url) {
2784
+ try {
2785
+ return new URL(url, document.baseURI);
2786
+ } catch {
2787
+ return null;
2788
+ }
2789
+ }
2790
+ function leavesPage(url) {
2791
+ if (url.protocol !== "http:" && url.protocol !== "https:") return false;
2792
+ const here = window.location;
2793
+ return url.origin !== here.origin || trimSlash(url.pathname) !== trimSlash(here.pathname);
2794
+ }
2795
+ function leavingLinkHref(target) {
2796
+ if (!(target instanceof Element)) return null;
2797
+ const link = target.closest("a[href]");
2798
+ if (!link || link.hasAttribute("download")) return null;
2799
+ const linkTarget = link.getAttribute("target");
2800
+ if (linkTarget && linkTarget !== "_self") return null;
2801
+ const href = link.getAttribute("href") ?? "";
2802
+ if (href.startsWith("#")) return null;
2803
+ const url = resolve(href);
2804
+ return url && leavesPage(url) ? url.href : null;
2805
+ }
2806
+ function submitterAttr(submitter, attr) {
2807
+ if (!submitter?.hasAttribute(attr)) return null;
2808
+ if (attr === "formaction") return submitter.formAction;
2809
+ if (attr === "formmethod") return submitter.formMethod;
2810
+ return submitter.formTarget;
2811
+ }
2812
+ function submissionUrl(form, submitter) {
2813
+ const method = (submitterAttr(submitter, "formmethod") ?? form.method).toLowerCase();
2814
+ if (method === "dialog") return null;
2815
+ const target = submitterAttr(submitter, "formtarget") ?? form.target;
2816
+ if (target && target !== "_self") return null;
2817
+ const url = resolve(
2818
+ submitterAttr(submitter, "formaction") ?? (form.action || location.href)
2819
+ );
2820
+ if (!url) return null;
2821
+ if (method === "get") {
2822
+ let data;
2823
+ try {
2824
+ data = new FormData(form, submitter);
2825
+ } catch {
2826
+ data = new FormData(form);
2827
+ }
2828
+ const query = new URLSearchParams();
2829
+ data.forEach((value, key) => {
2830
+ query.append(key, typeof value === "string" ? value : value.name);
2831
+ });
2832
+ url.search = query.toString();
2833
+ }
2834
+ return url;
2835
+ }
2836
+ function createNavigationGuard(opts) {
2837
+ let active = false;
2838
+ let permitted = null;
2839
+ let pendingForm = null;
2840
+ let originalPush = null;
2841
+ let guardedPush = null;
2842
+ const isPermitted = (url) => permitted === url.href;
2843
+ const guarding = () => active && opts.showsEditedContent();
2844
+ const report = (href) => {
2845
+ debug7.log("blocked navigation to", href);
2846
+ opts.onBlocked(href);
2847
+ };
2848
+ const onSubmit = (e2) => {
2849
+ if (e2.defaultPrevented || !(e2.target instanceof HTMLFormElement)) return;
2850
+ if (!guarding()) return;
2851
+ const submitter = e2.submitter ?? null;
2852
+ const url = submissionUrl(e2.target, submitter);
2853
+ if (!url || !leavesPage(url) || isPermitted(url)) return;
2854
+ e2.preventDefault();
2855
+ pendingForm = { form: e2.target, submitter, href: url.href };
2856
+ report(url.href);
2857
+ };
2858
+ const onNavigate = (event) => {
2859
+ const e2 = event;
2860
+ if (e2.navigationType === "traverse" || e2.navigationType === "reload") {
2861
+ return;
2862
+ }
2863
+ if (!e2.cancelable || e2.hashChange || e2.downloadRequest !== null) return;
2864
+ if (!guarding()) return;
2865
+ if (e2.navigationType === "replace" && e2.destination.sameDocument) return;
2866
+ const url = resolve(e2.destination.url);
2867
+ if (!url || !leavesPage(url) || isPermitted(url)) return;
2868
+ e2.preventDefault();
2869
+ report(url.href);
2870
+ if (e2.destination.sameDocument) window.location.reload();
2871
+ };
2872
+ const navigation = () => window.navigation;
2873
+ function installPushGuard() {
2874
+ const original = history.pushState;
2875
+ const guard = function(data, unused, url) {
2876
+ if (guarding() && guardedPush === guard && url != null) {
2877
+ const target = resolve(url);
2878
+ if (target && leavesPage(target) && !isPermitted(target)) {
2879
+ report(target.href);
2880
+ window.location.reload();
2881
+ return;
2882
+ }
2883
+ }
2884
+ return original.call(this, data, unused, url);
2885
+ };
2886
+ originalPush = original;
2887
+ guardedPush = guard;
2888
+ history.pushState = guard;
2889
+ }
2890
+ function uninstallPushGuard() {
2891
+ if (history.pushState === guardedPush && originalPush) {
2892
+ history.pushState = originalPush;
2893
+ }
2894
+ originalPush = null;
2895
+ guardedPush = null;
2896
+ }
2897
+ return {
2898
+ start() {
2899
+ if (active) return;
2900
+ active = true;
2901
+ installPushGuard();
2902
+ window.addEventListener("submit", onSubmit);
2903
+ navigation()?.addEventListener("navigate", onNavigate);
2904
+ },
2905
+ stop() {
2906
+ if (!active) return;
2907
+ active = false;
2908
+ uninstallPushGuard();
2909
+ window.removeEventListener("submit", onSubmit);
2910
+ navigation()?.removeEventListener("navigate", onNavigate);
2911
+ pendingForm = null;
2912
+ },
2913
+ isActive: guarding,
2914
+ block(href) {
2915
+ report(href);
2916
+ },
2917
+ permit(href) {
2918
+ permitted = resolve(href)?.href ?? null;
2919
+ },
2920
+ allow(href) {
2921
+ const url = resolve(href);
2922
+ if (!url || url.protocol !== "http:" && url.protocol !== "https:") {
2923
+ debug7.warn("ignored navigation to unsupported URL");
2924
+ return;
2925
+ }
2926
+ permitted = url.href;
2927
+ const form = pendingForm;
2928
+ pendingForm = null;
2929
+ if (form && form.href === url.href && form.form.isConnected) {
2930
+ try {
2931
+ form.form.requestSubmit(form.submitter ?? void 0);
2932
+ } catch {
2933
+ form.form.requestSubmit();
2934
+ }
2935
+ return;
2936
+ }
2937
+ window.location.assign(url.href);
2938
+ }
2939
+ };
2940
+ }
2941
+
2942
+ // src/core/visual-editor/index.ts
2943
+ var debug8 = createScopedLogger("visual-editor");
2944
+ var ENCODED_SELECTOR2 = "[data-prepr-encoded]";
2945
+ var REFERENCE_ATTR = "data-prepr-reference";
2946
+ var REFERENCE_SELECTOR = `[${REFERENCE_ATTR}]`;
2947
+ var CURSOR_STYLE_MARKER = "data-prepr-visual-editor";
2948
+ var CURSOR_STYLES = `${ENCODED_SELECTOR2}{cursor:pointer}html[data-prepr-ve-suspended] ${ENCODED_SELECTOR2}{cursor:auto}`;
2949
+ var SUSPENDED_ATTR = "data-prepr-ve-suspended";
2950
+ var FIND_RETRY_MS = 150;
2951
+ var FIND_RETRIES = 6;
2952
+ var MAX_CACHED_KEYS = 2e3;
2953
+ var LAYOUT_MUTATIONS = {
2954
+ childList: true,
2955
+ subtree: true,
2956
+ characterData: true,
2957
+ attributes: true,
2958
+ attributeFilter: [
2959
+ "class",
2960
+ "style",
2961
+ "hidden",
2962
+ "open",
2963
+ "data-prepr-encoded",
2964
+ // An image swapped by a carousel or lazy loader can show another asset.
2965
+ ...ASSET_SOURCE_ATTRIBUTES
2966
+ ]
2967
+ };
2968
+ function scrollIntoFrame(el) {
2969
+ const rect = el.getBoundingClientRect();
2970
+ const offset = Math.max(0, (window.innerHeight - rect.height) / 2);
2971
+ window.scrollTo({
2972
+ top: Math.max(0, window.scrollY + rect.top - offset),
2973
+ behavior: "smooth"
2974
+ });
2975
+ }
2976
+ var isVisible = (el) => typeof el.checkVisibility === "function" ? el.checkVisibility() : el.getClientRects().length > 0;
2977
+ function createVisualEditor(opts) {
2978
+ const overlay = new VisualEditorOverlay();
2979
+ let running = false;
2980
+ let currentItemId = null;
2981
+ let locale = opts.locale ?? "en";
2982
+ let suspended = false;
2983
+ let pointerFrame = null;
2984
+ let lastPointerTarget = null;
2985
+ let lastPointerX = 0;
2986
+ let lastPointerY = 0;
2987
+ let mutationFrame = null;
2988
+ let mutationObserver = null;
2989
+ let selection = null;
2990
+ let findTimeout = null;
2991
+ let settlePendingFind = null;
2992
+ let guardEnabled = false;
2993
+ let assetIndex = /* @__PURE__ */ new Map();
2994
+ const assetKeys = /* @__PURE__ */ new Map();
2995
+ const guard = createNavigationGuard({
2996
+ onBlocked: (href) => opts.onNavigationBlocked?.(href),
2997
+ showsEditedContent: () => showsEditedContent()
2998
+ });
2999
+ const encodedFrom = (node) => node instanceof Element ? node.closest(ENCODED_SELECTOR2) : null;
3000
+ const labelFor = (el) => fieldLabel(el.getAttribute("data-prepr-field"), locale);
3001
+ let references = null;
3002
+ let ownComponents = /* @__PURE__ */ new Set();
3003
+ const isCurrentItem = (el) => {
3004
+ if (currentItemId === null) return true;
3005
+ const id = el.getAttribute("data-prepr-id");
3006
+ return !id || id === currentItemId || ownComponents.has(id);
3007
+ };
3008
+ const showsEditedContent = () => {
3009
+ for (const el of document.querySelectorAll(ENCODED_SELECTOR2)) {
3010
+ if (isCurrentItem(el) && isVisible(el)) return true;
3011
+ }
3012
+ return false;
3013
+ };
3014
+ const linkedIdOf = (el) => {
3015
+ if (!references) return null;
3016
+ const id = el.getAttribute("data-prepr-id") ?? "";
3017
+ if (references.has(id)) return id;
3018
+ let container = el.closest(REFERENCE_SELECTOR);
3019
+ while (container) {
3020
+ const linked = container.getAttribute(REFERENCE_ATTR) ?? "";
3021
+ if (references.has(linked)) return linked;
3022
+ container = container.parentElement?.closest(REFERENCE_SELECTOR) ?? null;
3023
+ }
3024
+ return null;
3025
+ };
3026
+ const itemKind = (el) => {
3027
+ if (isCurrentItem(el)) return "current";
3028
+ if (!references) return "other";
3029
+ return linkedIdOf(el) ? "referenced" : null;
3030
+ };
3031
+ const referenceFieldOf = (el) => {
3032
+ const linked = linkedIdOf(el);
3033
+ return linked ? references?.get(linked) : void 0;
3034
+ };
3035
+ const hoverLabel = (el, kind) => {
3036
+ if (kind === "other") {
3037
+ return `${labelFor(el)} \xB7 ${t("visualEditor.otherItem", locale)}`;
3038
+ }
3039
+ if (kind === "referenced") {
3040
+ const via = referenceFieldOf(el);
3041
+ const prefix = via ? `${fieldLabel(via, locale)} \u203A ` : "";
3042
+ return `${prefix}${labelFor(el)} \xB7 ${t("visualEditor.linkedItem", locale)}`;
3043
+ }
3044
+ return labelFor(el);
3045
+ };
3046
+ const setSuspended = (value) => {
3047
+ if (suspended === value) return;
3048
+ suspended = value;
3049
+ document.documentElement.toggleAttribute(SUSPENDED_ATTR, value);
3050
+ if (value) overlay.setHover(null, "");
3051
+ };
3052
+ const updateHover = () => {
3053
+ pointerFrame = null;
3054
+ overlay.schedule();
3055
+ if (suspended) return;
3056
+ const encoded = encodedFrom(lastPointerTarget);
3057
+ const target = encoded === overlay.getSelectedTarget() ? null : encoded;
3058
+ if (!target) {
3059
+ overlay.setHover(null, "");
3060
+ return;
3061
+ }
3062
+ const kind = itemKind(target);
3063
+ if (!kind) {
3064
+ overlay.setHover(null, "");
3065
+ return;
3066
+ }
3067
+ overlay.setHover(target, hoverLabel(target, kind), { kind });
3068
+ };
3069
+ const handlePointerMove = (e2) => {
3070
+ setSuspended(e2.altKey);
3071
+ lastPointerTarget = e2.target;
3072
+ lastPointerX = e2.clientX;
3073
+ lastPointerY = e2.clientY;
3074
+ if (pointerFrame === null) {
3075
+ pointerFrame = requestAnimationFrame(updateHover);
3076
+ }
3077
+ };
3078
+ const handlePointerLeave = () => {
3079
+ lastPointerTarget = null;
3080
+ overlay.setHover(null, "");
3081
+ };
3082
+ const handleClick = (e2) => {
3083
+ if (e2.altKey) {
3084
+ const href = leavingLinkHref(e2.target);
3085
+ if (href && guard.isActive()) guard.permit(href);
3086
+ return;
3087
+ }
3088
+ const encoded = encodedFrom(e2.target);
3089
+ const kind = encoded ? itemKind(encoded) : null;
3090
+ if (!encoded || !kind) {
3091
+ clearSelection();
3092
+ if (e2.ctrlKey || e2.metaKey || e2.shiftKey || !showsEditedContent()) return;
3093
+ const href = leavingLinkHref(e2.target);
3094
+ if (!href) return;
3095
+ e2.preventDefault();
3096
+ if (guard.isActive()) {
3097
+ e2.stopPropagation();
3098
+ guard.block(href);
3099
+ }
3100
+ return;
3101
+ }
3102
+ e2.preventDefault();
3103
+ e2.stopPropagation();
3104
+ const item = listItemIn(encoded, e2.target);
3105
+ if (kind === "current")
3106
+ select(item?.element ?? encoded, encoded, item?.index);
3107
+ const referenceField = kind === "referenced" ? referenceFieldOf(encoded) : void 0;
3108
+ const linkedId = kind === "referenced" ? linkedIdOf(encoded) : null;
3109
+ opts.onEdit({
3110
+ ...readEditPayload(encoded),
3111
+ ...linkedId && { id: linkedId },
3112
+ ...item && { block: item.index },
3113
+ ...referenceField && { referenceField }
3114
+ });
3115
+ };
3116
+ const handleKey = (e2) => {
3117
+ if (e2.key === "Alt") setSuspended(e2.type === "keydown");
3118
+ if (e2.type === "keydown" && e2.key === "Escape") {
3119
+ clearSelection();
3120
+ }
3121
+ };
3122
+ const handleBlur = () => setSuspended(false);
3123
+ const handleScroll = () => overlay.onScroll();
3124
+ const handleResize = () => overlay.schedule();
3125
+ const handleSettled = () => overlay.schedule();
3126
+ function listItemIn(encoded, target) {
3127
+ if (!(target instanceof Element)) return null;
3128
+ const li = target.closest("li");
3129
+ if (!li || !encoded.contains(li) || !li.parentElement) return null;
3130
+ const items = Array.from(li.parentElement.children).filter(
3131
+ (child) => child.tagName === "LI"
3132
+ );
3133
+ return { element: li, index: items.indexOf(li) };
3134
+ }
3135
+ function nthListItem(encoded, block) {
3136
+ const list = encoded.querySelector("ul, ol");
3137
+ if (!list) return null;
3138
+ const items = Array.from(list.children).filter(
3139
+ (child) => child.tagName === "LI"
3140
+ );
3141
+ return items[block] ?? null;
3142
+ }
3143
+ function select(el, field, block) {
3144
+ const entryId = field.getAttribute("data-prepr-entry-id");
3145
+ selection = {
3146
+ lookup: {
3147
+ field: field.getAttribute("data-prepr-field") ?? "",
3148
+ id: field.getAttribute("data-prepr-id") ?? void 0,
3149
+ ...entryId && { entryId },
3150
+ ...block !== void 0 && { block }
3151
+ },
3152
+ label: field
3153
+ };
3154
+ overlay.setSelected(el, labelFor(field));
3155
+ if (overlay.getHoverTarget() === el) overlay.setHover(null, "");
3156
+ }
3157
+ function clearSelection() {
3158
+ selection = null;
3159
+ overlay.setSelected(null, "");
3160
+ }
3161
+ function reselect() {
3162
+ if (!selection) return;
3163
+ const current = overlay.getSelectedTarget();
3164
+ if (current?.isConnected && selection.label.isConnected) return;
3165
+ const found = findFieldElement(selection.lookup);
3166
+ if (!found || !isCurrentItem(found)) {
3167
+ overlay.setSelected(null, "");
3168
+ return;
3169
+ }
3170
+ const { block } = selection.lookup;
3171
+ const el = block !== void 0 ? nthListItem(found, block) ?? found : found;
3172
+ select(el, found, block);
3173
+ }
3174
+ function tagAssets() {
3175
+ if (assetKeys.size > MAX_CACHED_KEYS) assetKeys.clear();
3176
+ tagAssetImages(assetIndex, currentItemId, assetKeys, {
3177
+ isCurrentItem: (el) => isCurrentItem(el)
3178
+ });
3179
+ }
3180
+ const handleLayoutFrame = () => {
3181
+ mutationFrame = null;
3182
+ if (assetIndex.size) tagAssets();
3183
+ reselect();
3184
+ const hovered = overlay.getHoverTarget();
3185
+ if (hovered && !hovered.isConnected) {
3186
+ lastPointerTarget = document.elementFromPoint(lastPointerX, lastPointerY);
3187
+ updateHover();
3188
+ return;
3189
+ }
3190
+ overlay.schedule();
3191
+ };
3192
+ const handleMutations = () => {
3193
+ if (mutationFrame !== null) return;
3194
+ if (!selection && !overlay.getHoverTarget() && !assetIndex.size) return;
3195
+ mutationFrame = requestAnimationFrame(handleLayoutFrame);
3196
+ };
3197
+ function injectCursorStyles() {
3198
+ if (document.querySelector(`style[${CURSOR_STYLE_MARKER}]`)) return;
3199
+ const style = document.createElement("style");
3200
+ style.setAttribute(CURSOR_STYLE_MARKER, "");
3201
+ style.textContent = CURSOR_STYLES;
3202
+ document.head.appendChild(style);
3203
+ }
3204
+ function start() {
3205
+ if (running) return;
3206
+ running = true;
3207
+ overlay.mount();
3208
+ injectCursorStyles();
3209
+ document.addEventListener("pointermove", handlePointerMove, {
3210
+ passive: true
3211
+ });
3212
+ document.documentElement.addEventListener(
3213
+ "pointerleave",
3214
+ handlePointerLeave
3215
+ );
3216
+ document.addEventListener("click", handleClick, { capture: true });
3217
+ window.addEventListener("keydown", handleKey);
3218
+ window.addEventListener("keyup", handleKey);
3219
+ window.addEventListener("blur", handleBlur);
3220
+ window.addEventListener("scroll", handleScroll, {
3221
+ capture: true,
3222
+ passive: true
3223
+ });
3224
+ window.addEventListener("resize", handleResize, { passive: true });
3225
+ document.addEventListener("transitionend", handleSettled, {
3226
+ capture: true,
3227
+ passive: true
3228
+ });
3229
+ document.addEventListener("animationend", handleSettled, {
3230
+ capture: true,
3231
+ passive: true
3232
+ });
3233
+ mutationObserver = new MutationObserver(handleMutations);
3234
+ mutationObserver.observe(document.body, LAYOUT_MUTATIONS);
3235
+ if (assetIndex.size) tagAssets();
3236
+ syncGuard();
3237
+ debug8.log("visual editor started");
3238
+ }
3239
+ function stop() {
3240
+ if (!running) return;
3241
+ running = false;
3242
+ if (pointerFrame !== null) cancelAnimationFrame(pointerFrame);
3243
+ if (mutationFrame !== null) cancelAnimationFrame(mutationFrame);
3244
+ mutationObserver?.disconnect();
3245
+ mutationObserver = null;
3246
+ cancelFind();
3247
+ pointerFrame = null;
3248
+ mutationFrame = null;
3249
+ selection = null;
3250
+ lastPointerTarget = null;
3251
+ setSuspended(false);
3252
+ document.removeEventListener("pointermove", handlePointerMove);
3253
+ document.documentElement.removeEventListener(
3254
+ "pointerleave",
3255
+ handlePointerLeave
3256
+ );
3257
+ document.removeEventListener("click", handleClick, { capture: true });
3258
+ window.removeEventListener("keydown", handleKey);
3259
+ window.removeEventListener("keyup", handleKey);
3260
+ window.removeEventListener("blur", handleBlur);
3261
+ window.removeEventListener("scroll", handleScroll, { capture: true });
3262
+ window.removeEventListener("resize", handleResize);
3263
+ document.removeEventListener("transitionend", handleSettled, {
3264
+ capture: true
3265
+ });
3266
+ document.removeEventListener("animationend", handleSettled, {
3267
+ capture: true
3268
+ });
3269
+ document.querySelectorAll(`style[${CURSOR_STYLE_MARKER}]`).forEach((node) => node.remove());
3270
+ overlay.unmount();
3271
+ untagAssetImages();
3272
+ guard.stop();
3273
+ debug8.log("visual editor stopped");
3274
+ }
3275
+ function cancelFind() {
3276
+ if (findTimeout) clearTimeout(findTimeout);
3277
+ findTimeout = null;
3278
+ settlePendingFind?.();
3279
+ settlePendingFind = null;
3280
+ }
3281
+ function focusField(lookup) {
3282
+ cancelFind();
3283
+ return new Promise((resolve2) => {
3284
+ let attempt = 0;
3285
+ const finish = (found) => {
3286
+ settlePendingFind = null;
3287
+ resolve2(found);
3288
+ };
3289
+ settlePendingFind = () => resolve2(false);
3290
+ const tryFind = () => {
3291
+ findTimeout = null;
3292
+ const found = findFieldElement(lookup);
3293
+ if (found) {
3294
+ const el = lookup.block !== void 0 ? nthListItem(found, lookup.block) ?? found : found;
3295
+ scrollIntoFrame(el);
3296
+ if (running && isCurrentItem(found)) select(el, found, lookup.block);
3297
+ finish(true);
3298
+ return;
3299
+ }
3300
+ if (++attempt >= FIND_RETRIES) {
3301
+ debug8.log("no element renders field", lookup.field);
3302
+ finish(false);
3303
+ return;
3304
+ }
3305
+ findTimeout = setTimeout(tryFind, FIND_RETRY_MS);
3306
+ };
3307
+ tryFind();
3308
+ });
3309
+ }
3310
+ function setCurrentItem(itemId, referenceList, componentIds) {
3311
+ currentItemId = itemId;
3312
+ if (referenceList) setReferences(referenceList, componentIds);
3313
+ else if (componentIds) ownComponents = new Set(componentIds);
3314
+ if (running && assetIndex.size) tagAssets();
3315
+ const selected = overlay.getSelectedTarget();
3316
+ const owner = selected ? encodedFrom(selected) : null;
3317
+ if (owner && !isCurrentItem(owner)) clearSelection();
3318
+ }
3319
+ function setAssets(assets) {
3320
+ const hadAssets = assetIndex.size > 0;
3321
+ assetIndex = createAssetIndex(assets);
3322
+ if (running && (hadAssets || assetIndex.size)) tagAssets();
3323
+ }
3324
+ function setReferences(referenceList, componentIds) {
3325
+ if (componentIds) ownComponents = new Set(componentIds);
3326
+ references = /* @__PURE__ */ new Map();
3327
+ for (const { id, field } of referenceList) {
3328
+ if (!references.has(id)) references.set(id, field);
3329
+ }
3330
+ }
3331
+ function setLocale(value) {
3332
+ locale = value;
3333
+ }
3334
+ function syncGuard() {
3335
+ if (running && guardEnabled) guard.start();
3336
+ else guard.stop();
3337
+ }
3338
+ function setNavigationGuard(enabled) {
3339
+ guardEnabled = enabled;
3340
+ syncGuard();
3341
+ }
3342
+ return {
3343
+ start,
3344
+ stop,
3345
+ setCurrentItem,
3346
+ setReferences,
3347
+ setAssets,
3348
+ setLocale,
3349
+ focusField,
3350
+ setNavigationGuard,
3351
+ allowNavigation: (href) => guard.allow(href)
3352
+ };
3353
+ }
3354
+
2124
3355
  // src/core/create-preview.ts
2125
- var debug7 = createScopedLogger("create-preview");
3356
+ var debug9 = createScopedLogger("create-preview");
2126
3357
  var controllerStores = /* @__PURE__ */ new WeakMap();
2127
3358
  function defaultNavigation() {
2128
3359
  return {
@@ -2164,7 +3395,7 @@ function createPreprPreview(opts = {}) {
2164
3395
  const search = new URLSearchParams(window.location.search);
2165
3396
  const hideBar = search.get(PARAM_HIDE_BAR) === "true";
2166
3397
  if (hideBar) {
2167
- debug7.log(`${PARAM_HIDE_BAR}=true \u2014 no visible toolbar`);
3398
+ debug9.log(`${PARAM_HIDE_BAR}=true \u2014 no visible toolbar`);
2168
3399
  }
2169
3400
  const mountUi = (options?.ui ?? true) && !isIframe && !hideBar;
2170
3401
  const locale = resolveLocale(options);
@@ -2193,30 +3424,45 @@ function createPreprPreview(opts = {}) {
2193
3424
  el.connect(store, (key) => t(key, locale));
2194
3425
  }
2195
3426
  const editingEnabled = features.editMode || isIframe;
2196
- const stega = createStegaController({
2197
- // The CMS deep-link tooltip is chrome, so it follows the same decision as
2198
- // the bar: noise inside the editor (clicking the element itself requests
2199
- // the edit there), and unwanted in a headless preview that asked for no UI.
2200
- tooltip: mountUi,
2201
- // In the editor, ask the parent to focus the field instead of opening a new
2202
- // tab. Standalone previews keep the window.open behaviour.
2203
- onEdit: ({ href, origin, id, field }) => {
2204
- const safeHref = href ? safeEditUrl(href) : null;
2205
- if (href && !safeHref) {
2206
- debug7.warn("ignored edit request with unsupported href scheme");
2207
- return;
2208
- }
2209
- if (isIframe) {
2210
- sendPreprEvent("field_edit_requested", {
2211
- href: safeHref ?? void 0,
2212
- origin,
2213
- id,
2214
- field
2215
- });
2216
- } else if (safeHref) {
2217
- window.open(safeHref, "_blank", "noopener,noreferrer");
2218
- }
3427
+ const onEdit = ({
3428
+ href,
3429
+ id,
3430
+ field,
3431
+ locale: contentLocale,
3432
+ block,
3433
+ entryId,
3434
+ referenceField
3435
+ }) => {
3436
+ const safeHref = href ? safeEditUrl(href) : null;
3437
+ if (href && !safeHref) {
3438
+ debug9.warn("ignored edit request with unsupported href scheme");
3439
+ return;
2219
3440
  }
3441
+ if (isIframe) {
3442
+ sendPreprEvent("field_edit_requested", {
3443
+ id,
3444
+ field,
3445
+ locale: contentLocale,
3446
+ ...block !== void 0 && { block },
3447
+ ...entryId && { entryId },
3448
+ ...referenceField && { referenceField }
3449
+ });
3450
+ } else if (safeHref) {
3451
+ window.open(safeHref, "_blank", "noopener,noreferrer");
3452
+ }
3453
+ };
3454
+ const visualEditor = isIframe ? createVisualEditor({
3455
+ onEdit,
3456
+ locale,
3457
+ onNavigationBlocked: (href) => sendPreprEvent("navigation_blocked", { href })
3458
+ }) : null;
3459
+ const stega = visualEditor ?? createStegaController({
3460
+ // The edit-mode marks and hover label are chrome, so they follow the
3461
+ // same decision as the bar: unwanted in a headless preview that asked
3462
+ // for no UI.
3463
+ tooltip: mountUi,
3464
+ onEdit,
3465
+ locale
2220
3466
  });
2221
3467
  const autoClean = createStegaAutoClean({
2222
3468
  enabled: options?.autoClean
@@ -2238,7 +3484,35 @@ function createPreprPreview(opts = {}) {
2238
3484
  handleChange(before, state);
2239
3485
  });
2240
3486
  const bridge = createIframeBridge(store, {
2241
- allowedEditorOrigins: options?.allowedEditorOrigins
3487
+ allowedEditorOrigins: options?.allowedEditorOrigins,
3488
+ onHandshake: ({
3489
+ itemId,
3490
+ referencedIds,
3491
+ componentIds,
3492
+ uiLocale,
3493
+ navigationGuard,
3494
+ assets
3495
+ }) => {
3496
+ if (uiLocale) visualEditor?.setLocale(uiLocale);
3497
+ visualEditor?.setCurrentItem(itemId ?? null, referencedIds, componentIds);
3498
+ visualEditor?.setNavigationGuard(navigationGuard === true);
3499
+ if (assets) visualEditor?.setAssets(assets);
3500
+ },
3501
+ onAssets: (assets) => visualEditor?.setAssets(assets),
3502
+ onAllowNavigation: (href) => visualEditor?.allowNavigation(href),
3503
+ onReferences: (referencedIds, componentIds) => visualEditor?.setReferences(referencedIds, componentIds),
3504
+ // The editor selecting a field scrolls the matching element into view. The
3505
+ // lookup relies on the attributes the stega scan writes, so it finds
3506
+ // nothing until encoded content has been tagged.
3507
+ onScrollToField: ({ field, id, block, entryId, fieldById }) => {
3508
+ if (visualEditor) {
3509
+ void visualEditor.focusField({ field, id, block, entryId, fieldById });
3510
+ return;
3511
+ }
3512
+ if (!scrollToField({ field, id })) {
3513
+ debug9.log("no element on this page renders field", field);
3514
+ }
3515
+ }
2242
3516
  });
2243
3517
  sendPreprEvent(
2244
3518
  "getScrollPosition",
@@ -2258,7 +3532,7 @@ function createPreprPreview(opts = {}) {
2258
3532
  bridge.stop();
2259
3533
  }
2260
3534
  el?.remove();
2261
- debug7.log("preview destroyed");
3535
+ debug9.log("preview destroyed");
2262
3536
  }
2263
3537
  const controller = { destroy };
2264
3538
  controllerStores.set(controller, store);
@@ -2266,7 +3540,7 @@ function createPreprPreview(opts = {}) {
2266
3540
  }
2267
3541
 
2268
3542
  // src/version.ts
2269
- var VERSION = "0.4.3";
3543
+ var VERSION = "0.5.0-rc.10";
2270
3544
 
2271
3545
  // src/core/pixel.ts
2272
3546
  var PIXEL_SCRIPT_URL = "https://cdn.tracking.prepr.io/js/prepr.min.js";