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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/README.md +207 -10
  2. package/dist/astro/index.cjs +1174 -1
  3. package/dist/astro/index.d.cts +47 -1
  4. package/dist/astro/index.d.ts +47 -1
  5. package/dist/astro/index.js +1177 -1
  6. package/dist/chunk-42Z2VW7G.js +1161 -0
  7. package/dist/chunk-6WY3MWCX.js +246 -0
  8. package/dist/chunk-MDAV2FLM.js +26 -0
  9. package/dist/{chunk-4JNKYIEV.js → chunk-PP3O7RB4.js} +1370 -617
  10. package/dist/chunk-S3VT4UWM.js +75 -0
  11. package/dist/chunk-SXUDBGLZ.js +184 -0
  12. package/dist/chunk-URXSDJOU.js +356 -0
  13. package/dist/index.cjs +3113 -391
  14. package/dist/index.d.cts +240 -5
  15. package/dist/index.d.ts +240 -5
  16. package/dist/index.js +189 -149
  17. package/dist/live-preview-EADA3ZL4.js +18 -0
  18. package/dist/nextjs/components.cjs +1530 -361
  19. package/dist/nextjs/components.d.cts +1 -1
  20. package/dist/nextjs/components.d.ts +1 -1
  21. package/dist/nextjs/components.js +4 -1
  22. package/dist/nextjs/index.cjs +1065 -6
  23. package/dist/nextjs/index.d.cts +81 -3
  24. package/dist/nextjs/index.d.ts +81 -3
  25. package/dist/nextjs/index.js +1059 -7
  26. package/dist/nextjs/live-action.cjs +285 -0
  27. package/dist/nextjs/live-action.d.cts +12 -0
  28. package/dist/nextjs/live-action.d.ts +12 -0
  29. package/dist/nextjs/live-action.js +68 -0
  30. package/dist/nextjs/live.cjs +1838 -0
  31. package/dist/nextjs/live.d.cts +52 -0
  32. package/dist/nextjs/live.d.ts +52 -0
  33. package/dist/nextjs/live.js +265 -0
  34. package/dist/nuxt/index.cjs +1 -1
  35. package/dist/nuxt/index.js +1 -1
  36. package/dist/nuxt/live-preview-PTEC3GPC.js +1462 -0
  37. package/dist/nuxt/live.cjs +1662 -0
  38. package/dist/nuxt/live.d.cts +43 -0
  39. package/dist/nuxt/live.d.ts +43 -0
  40. package/dist/nuxt/live.js +94 -0
  41. package/dist/{pixel-e1fUWl2Y.d.cts → pixel-CX422dCO.d.cts} +18 -3
  42. package/dist/{pixel-e1fUWl2Y.d.ts → pixel-CX422dCO.d.ts} +18 -3
  43. package/dist/react/components.cjs +1530 -361
  44. package/dist/react/components.d.cts +17 -2
  45. package/dist/react/components.d.ts +17 -2
  46. package/dist/react/components.js +1530 -361
  47. package/dist/sveltekit/index.cjs +1 -1
  48. package/dist/sveltekit/index.js +1 -1
  49. package/package.json +12 -1
  50. package/src/astro/PreprToolbar.astro +7 -1
@@ -1,133 +1,21 @@
1
- // src/version.ts
2
- var VERSION = "0.5.0-rc.1";
3
-
4
- // src/core/features.ts
5
- function isEnabled(config) {
6
- if (typeof config === "boolean") return config;
7
- return config?.enabled ?? true;
8
- }
9
- function resolveFeatures(features) {
10
- return {
11
- segments: isEnabled(features?.segments),
12
- abTesting: isEnabled(features?.abTesting),
13
- editMode: isEnabled(features?.editMode)
14
- };
15
- }
16
- var ALL_FEATURES_ENABLED = Object.freeze({
17
- segments: true,
18
- abTesting: true,
19
- editMode: true
20
- });
1
+ import {
2
+ ALL_FEATURES_ENABLED,
3
+ VERSION,
4
+ resolveFeatures
5
+ } from "./chunk-MDAV2FLM.js";
6
+ import {
7
+ createIframeBridge,
8
+ isLocale,
9
+ t
10
+ } from "./chunk-URXSDJOU.js";
11
+ import {
12
+ createScopedLogger,
13
+ initDebugLogger,
14
+ sendPreprEvent
15
+ } from "./chunk-S3VT4UWM.js";
21
16
 
22
17
  // src/core/stega/clean.ts
23
18
  import { vercelStegaClean, vercelStegaDecode } from "@vercel/stega";
24
-
25
- // src/core/utils.ts
26
- var trustedParentOrigin = null;
27
- function setTrustedParentOrigin(origin) {
28
- trustedParentOrigin = origin;
29
- }
30
- function sendPreprEvent(event, data, options) {
31
- if (typeof window !== "undefined") {
32
- const message = {
33
- name: "prepr_preview_bar",
34
- event,
35
- ...data
36
- };
37
- window.dispatchEvent(
38
- new CustomEvent("prepr_preview_bar", { detail: message })
39
- );
40
- if (window.parent && window.parent !== window) {
41
- if (trustedParentOrigin) {
42
- window.parent.postMessage(message, trustedParentOrigin);
43
- } else if (options?.allowUntrustedTarget) {
44
- window.parent.postMessage(message, "*");
45
- }
46
- }
47
- }
48
- }
49
- var DebugLogger = class {
50
- constructor(options) {
51
- this.options = {
52
- prefix: "[Prepr]",
53
- ...options
54
- };
55
- }
56
- // A local `enabled` wins; otherwise defer to the global logger so scoped
57
- // loggers created before initDebugLogger() still pick up the setting.
58
- isEnabled() {
59
- if (this.options.enabled !== void 0) {
60
- return this.options.enabled;
61
- }
62
- return globalDebugLogger?.options?.enabled ?? false;
63
- }
64
- log(message, ...args) {
65
- if (!this.isEnabled()) return;
66
- const prefix = this.options.prefix;
67
- console.log(`${prefix} ${message}`, ...args);
68
- }
69
- warn(message, ...args) {
70
- if (!this.isEnabled()) return;
71
- const prefix = this.options.prefix;
72
- console.warn(`${prefix} ${message}`, ...args);
73
- }
74
- error(message, ...args) {
75
- if (!this.isEnabled()) return;
76
- const prefix = this.options.prefix;
77
- console.error(`${prefix} ${message}`, ...args);
78
- }
79
- };
80
- var globalDebugLogger = null;
81
- function initDebugLogger(enabled = false) {
82
- globalDebugLogger = new DebugLogger({ enabled });
83
- }
84
- function createScopedLogger(scopeName) {
85
- return new DebugLogger({
86
- prefix: `[Prepr][${scopeName}]`
87
- });
88
- }
89
- function throttle(func, delay) {
90
- let timeoutId = null;
91
- let lastExecTime = 0;
92
- const throttledFunc = ((...args) => {
93
- const currentTime = Date.now();
94
- const timeSinceLastExec = currentTime - lastExecTime;
95
- if (timeSinceLastExec >= delay) {
96
- func(...args);
97
- lastExecTime = currentTime;
98
- } else {
99
- if (timeoutId) {
100
- clearTimeout(timeoutId);
101
- }
102
- timeoutId = setTimeout(() => {
103
- func(...args);
104
- lastExecTime = Date.now();
105
- timeoutId = null;
106
- }, delay - timeSinceLastExec);
107
- }
108
- });
109
- throttledFunc.cancel = () => {
110
- if (timeoutId) {
111
- clearTimeout(timeoutId);
112
- timeoutId = null;
113
- }
114
- };
115
- return throttledFunc;
116
- }
117
- function createElementCache(query, ttl = 1e3) {
118
- let cache = null;
119
- let lastCacheTime = 0;
120
- return () => {
121
- const now = Date.now();
122
- if (!cache || now - lastCacheTime > ttl) {
123
- cache = document.querySelectorAll(query);
124
- lastCacheTime = now;
125
- }
126
- return cache;
127
- };
128
- }
129
-
130
- // src/core/stega/clean.ts
131
19
  var debug = createScopedLogger("stega:clean");
132
20
  function stegaClean(text) {
133
21
  if (!text) return text;
@@ -157,6 +45,439 @@ function decodeStega(str) {
157
45
  return null;
158
46
  }
159
47
 
48
+ // src/core/visual-editor/overlay.ts
49
+ var HOST_TAG = "prepr-visual-editor";
50
+ var SCROLL_SETTLE_MS = 120;
51
+ var LABEL_SPACE_PX = 22;
52
+ var BOX_PADDING_PX = 3;
53
+ var LABEL_MARGIN_PX = 4;
54
+ var PROBE_POINTS = [
55
+ [0.5, 0.5],
56
+ [0.15, 0.2],
57
+ [0.85, 0.2],
58
+ [0.15, 0.8],
59
+ [0.85, 0.8]
60
+ ];
61
+ function overlapsBox(area, rect) {
62
+ 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;
63
+ }
64
+ function overlaps(a2, b2) {
65
+ return a2.left < b2.right && b2.left < a2.right && a2.top < b2.bottom && b2.top < a2.bottom;
66
+ }
67
+ function coverOf(hit, target, x2, y2) {
68
+ let cover = hit.getBoundingClientRect();
69
+ for (let el = hit.parentElement; el && !el.contains(target) && el !== document.body; el = el.parentElement) {
70
+ const r2 = el.getBoundingClientRect();
71
+ if (x2 < r2.left || x2 >= r2.right || y2 < r2.top || y2 >= r2.bottom) break;
72
+ cover = r2;
73
+ }
74
+ return cover;
75
+ }
76
+ function mergeAreas(areas) {
77
+ const merged = areas.slice();
78
+ for (let i2 = 0; i2 < merged.length; i2++) {
79
+ for (let j2 = i2 + 1; j2 < merged.length; j2++) {
80
+ if (!overlaps(merged[i2], merged[j2])) continue;
81
+ merged[i2] = {
82
+ left: Math.min(merged[i2].left, merged[j2].left),
83
+ top: Math.min(merged[i2].top, merged[j2].top),
84
+ right: Math.max(merged[i2].right, merged[j2].right),
85
+ bottom: Math.max(merged[i2].bottom, merged[j2].bottom)
86
+ };
87
+ merged.splice(j2, 1);
88
+ j2 = i2;
89
+ }
90
+ }
91
+ return merged;
92
+ }
93
+ function findCovers(target, rect) {
94
+ const width = window.innerWidth;
95
+ const height = window.innerHeight;
96
+ const covers = [];
97
+ const seen = /* @__PURE__ */ new Set();
98
+ let probed = false;
99
+ let reached = false;
100
+ for (const [fx, fy] of PROBE_POINTS) {
101
+ const x2 = rect.left + rect.width * fx;
102
+ const y2 = rect.top + rect.height * fy;
103
+ if (x2 < 0 || y2 < 0 || x2 >= width || y2 >= height) continue;
104
+ probed = true;
105
+ const hit = document.elementFromPoint(x2, y2);
106
+ if (!hit || hit === target || target.contains(hit)) {
107
+ reached = true;
108
+ continue;
109
+ }
110
+ if (seen.has(hit)) continue;
111
+ seen.add(hit);
112
+ if (covers.some(
113
+ (c2) => x2 >= c2.left && x2 < c2.right && y2 >= c2.top && y2 < c2.bottom
114
+ )) {
115
+ continue;
116
+ }
117
+ covers.push(coverOf(hit, target, x2, y2));
118
+ }
119
+ if (!probed) return [];
120
+ return reached ? mergeAreas(covers) : null;
121
+ }
122
+ function clipPath(x2, y2, covers) {
123
+ const far = 1e5;
124
+ const points = [
125
+ `${-far}px ${-far}px`,
126
+ `${far}px ${-far}px`,
127
+ `${far}px ${far}px`,
128
+ `${-far}px ${far}px`,
129
+ `${-far}px ${-far}px`
130
+ ];
131
+ for (const c2 of covers) {
132
+ const l2 = c2.left - x2;
133
+ const t3 = c2.top - y2;
134
+ const r2 = c2.right - x2;
135
+ const b2 = c2.bottom - y2;
136
+ points.push(
137
+ `${l2}px ${t3}px`,
138
+ `${r2}px ${t3}px`,
139
+ `${r2}px ${b2}px`,
140
+ `${l2}px ${b2}px`,
141
+ `${l2}px ${t3}px`,
142
+ `${-far}px ${-far}px`
143
+ );
144
+ }
145
+ return `polygon(evenodd, ${points.join(", ")})`;
146
+ }
147
+ var STYLES = `
148
+ :host {
149
+ all: initial;
150
+ position: fixed;
151
+ inset: 0;
152
+ pointer-events: none;
153
+ z-index: 2147483646;
154
+ contain: strict;
155
+ }
156
+ .box {
157
+ position: absolute;
158
+ top: 0;
159
+ left: 0;
160
+ box-sizing: border-box;
161
+ border-radius: 6px;
162
+ opacity: 0;
163
+ will-change: transform, width, height;
164
+ transition:
165
+ transform 180ms cubic-bezier(0.2, 0.8, 0.2, 1),
166
+ width 180ms cubic-bezier(0.2, 0.8, 0.2, 1),
167
+ height 180ms cubic-bezier(0.2, 0.8, 0.2, 1),
168
+ opacity 140ms ease;
169
+ }
170
+ .box[data-visible] { opacity: 1; }
171
+ .box[data-instant], :host([data-scrolling]) .box {
172
+ transition: opacity 140ms ease;
173
+ }
174
+ .hover {
175
+ border: 1.5px solid rgba(79, 70, 229, 0.9);
176
+ background: rgba(79, 70, 229, 0.06);
177
+ }
178
+ .selected {
179
+ border: 2px solid #4f46e5;
180
+ box-shadow: 0 0 0 4px rgba(79, 70, 229, 0.18);
181
+ }
182
+ .label {
183
+ position: absolute;
184
+ left: -2px;
185
+ bottom: 100%;
186
+ margin-bottom: 4px;
187
+ padding: 3px 7px;
188
+ border-radius: 5px;
189
+ background: #4f46e5;
190
+ color: #fff;
191
+ font: 500 11px/1.3 ui-sans-serif, system-ui, -apple-system, 'Segoe UI', sans-serif;
192
+ letter-spacing: 0.01em;
193
+ white-space: nowrap;
194
+ display: flex;
195
+ max-width: 240px;
196
+ box-shadow: 0 1px 3px rgba(15, 23, 42, 0.25);
197
+ }
198
+ .text {
199
+ min-width: 0;
200
+ overflow: hidden;
201
+ text-overflow: ellipsis;
202
+ }
203
+ .hover .label { background: rgba(79, 70, 229, 0.92); }
204
+ /* Content of another item than the one open in the editor: still clickable,
205
+ but visibly not part of the item being edited. */
206
+ .hover[data-item-kind='other'] {
207
+ border: 1.5px dashed #a5b4fc;
208
+ background: rgba(100, 116, 139, 0.05);
209
+ }
210
+ .hover[data-item-kind='other'] .label { background: #64748b; }
211
+ .hover[data-item-kind='referenced'] {
212
+ border: 1.5px solid #a5b4fc;
213
+ background: rgba(79, 70, 229, 0.03);
214
+ }
215
+ .hover[data-item-kind='referenced'] .label { background: #818cf8; }
216
+ .box[data-label-covered] .label { visibility: hidden; }
217
+ .box[data-label-inside] .label {
218
+ bottom: auto;
219
+ top: 2px;
220
+ left: 2px;
221
+ margin: 0;
222
+ }
223
+ /* A clickable hover label (edit mode on the live site). Only while shown, so
224
+ a fading box never leaves an invisible click target behind. */
225
+ :host([data-label-action]) .hover[data-visible] .label {
226
+ pointer-events: auto;
227
+ cursor: pointer;
228
+ }
229
+ :host([data-label-action]) .hover .label::after {
230
+ content: '\\2197';
231
+ margin-left: 5px;
232
+ }
233
+ :host([data-label-action]) .hover .label:hover { background: #4338ca; }
234
+ /* Bridges the gap between the label and its element, so the pointer can
235
+ travel from one to the other without the hover ending on the way. */
236
+ :host([data-label-action]) .hover .label::before {
237
+ content: '';
238
+ position: absolute;
239
+ left: 0;
240
+ right: 0;
241
+ top: 100%;
242
+ height: ${LABEL_MARGIN_PX + BOX_PADDING_PX + 2}px;
243
+ }
244
+ @media (prefers-reduced-motion: reduce) {
245
+ .box { transition: opacity 80ms linear; }
246
+ }
247
+ `;
248
+ var VisualEditorOverlay = class {
249
+ constructor(options = {}) {
250
+ this.host = null;
251
+ this.hoverBox = null;
252
+ this.selectedBox = null;
253
+ this.frame = null;
254
+ this.scrollTimeout = null;
255
+ this.resizeObserver = null;
256
+ this.onLabelClick = options.onLabelClick;
257
+ }
258
+ mount() {
259
+ if (this.host) return;
260
+ const host = document.createElement(HOST_TAG);
261
+ const shadow = host.attachShadow({ mode: "open" });
262
+ const style = document.createElement("style");
263
+ style.textContent = STYLES;
264
+ shadow.appendChild(style);
265
+ this.hoverBox = this.createBox(shadow, "hover");
266
+ this.selectedBox = this.createBox(shadow, "selected");
267
+ const onLabelClick = this.onLabelClick;
268
+ if (onLabelClick) {
269
+ host.setAttribute("data-label-action", "");
270
+ const hoverBox = this.hoverBox;
271
+ hoverBox.label.addEventListener("click", () => {
272
+ if (hoverBox.target) onLabelClick(hoverBox.target);
273
+ });
274
+ }
275
+ document.documentElement.appendChild(host);
276
+ this.host = host;
277
+ if (typeof ResizeObserver !== "undefined") {
278
+ this.resizeObserver = new ResizeObserver(() => this.schedule());
279
+ }
280
+ }
281
+ unmount() {
282
+ if (this.frame !== null) cancelAnimationFrame(this.frame);
283
+ if (this.scrollTimeout) clearTimeout(this.scrollTimeout);
284
+ this.frame = null;
285
+ this.scrollTimeout = null;
286
+ this.resizeObserver?.disconnect();
287
+ this.resizeObserver = null;
288
+ this.host?.remove();
289
+ this.host = null;
290
+ this.hoverBox = null;
291
+ this.selectedBox = null;
292
+ }
293
+ /**
294
+ * Whether a pointer event target is this chrome: events from inside the
295
+ * shadow root reach the page retargeted to its host.
296
+ */
297
+ owns(node) {
298
+ return node !== null && node === this.host;
299
+ }
300
+ getHoverTarget() {
301
+ return this.hoverBox?.target ?? null;
302
+ }
303
+ getSelectedTarget() {
304
+ return this.selectedBox?.target ?? null;
305
+ }
306
+ setHover(target, label, options = {}) {
307
+ if (target) {
308
+ this.hoverBox?.root.setAttribute(
309
+ "data-item-kind",
310
+ options.kind ?? "current"
311
+ );
312
+ }
313
+ this.retarget(this.hoverBox, target, label);
314
+ }
315
+ setSelected(target, label) {
316
+ this.retarget(this.selectedBox, target, label);
317
+ }
318
+ /** Call on every scroll; positions follow in the next frame, untransitioned. */
319
+ onScroll() {
320
+ if (!this.host) return;
321
+ this.host.setAttribute("data-scrolling", "");
322
+ if (this.scrollTimeout) clearTimeout(this.scrollTimeout);
323
+ this.scrollTimeout = setTimeout(() => {
324
+ this.host?.removeAttribute("data-scrolling");
325
+ this.scrollTimeout = null;
326
+ }, SCROLL_SETTLE_MS);
327
+ this.schedule();
328
+ }
329
+ /** Re-measure both boxes in the next animation frame. */
330
+ schedule() {
331
+ if (this.frame !== null || !this.host) return;
332
+ this.frame = requestAnimationFrame(() => {
333
+ this.frame = null;
334
+ this.render();
335
+ });
336
+ }
337
+ createBox(shadow, kind) {
338
+ const root = document.createElement("div");
339
+ root.className = `box ${kind}`;
340
+ const label = document.createElement("div");
341
+ label.className = "label";
342
+ const text = document.createElement("span");
343
+ text.className = "text";
344
+ label.appendChild(text);
345
+ root.appendChild(label);
346
+ shadow.appendChild(root);
347
+ return { root, label, text, target: null };
348
+ }
349
+ retarget(box, target, label) {
350
+ if (!box || box.target === target) return;
351
+ const previous = box.target;
352
+ if (previous) this.resizeObserver?.unobserve(previous);
353
+ box.target = target;
354
+ if (target) {
355
+ this.resizeObserver?.observe(target);
356
+ box.text.textContent = label;
357
+ if (!box.root.hasAttribute("data-visible")) {
358
+ box.root.setAttribute("data-instant", "");
359
+ }
360
+ }
361
+ this.schedule();
362
+ }
363
+ render() {
364
+ const hover = this.hoverBox ? this.measure(this.hoverBox) : null;
365
+ const selected = this.selectedBox ? this.measure(this.selectedBox) : null;
366
+ if (this.hoverBox) this.paint(this.hoverBox, hover, selected);
367
+ if (this.selectedBox) this.paint(this.selectedBox, selected, hover);
368
+ }
369
+ measure(box) {
370
+ const target = box.target;
371
+ if (!target) return null;
372
+ if (!target.isConnected) {
373
+ this.resizeObserver?.unobserve(target);
374
+ box.target = null;
375
+ return null;
376
+ }
377
+ const rect = target.getBoundingClientRect();
378
+ if (rect.width === 0 && rect.height === 0) return null;
379
+ const covers = findCovers(target, rect);
380
+ if (!covers) return null;
381
+ return {
382
+ rect,
383
+ covers,
384
+ labelWidth: covers.length ? box.label.offsetWidth : 0,
385
+ labelHeight: covers.length ? box.label.offsetHeight : 0
386
+ };
387
+ }
388
+ paint(box, measurement, other = null) {
389
+ const root = box.root;
390
+ if (!measurement) {
391
+ root.removeAttribute("data-visible");
392
+ return;
393
+ }
394
+ const { rect, covers } = measurement;
395
+ const x2 = rect.left - BOX_PADDING_PX;
396
+ const y2 = rect.top - BOX_PADDING_PX;
397
+ const labelAbove = {
398
+ left: x2 - 2,
399
+ top: y2 - LABEL_SPACE_PX,
400
+ right: x2 - 2 + (measurement.labelWidth || Math.min(240, rect.width)),
401
+ bottom: y2
402
+ };
403
+ const labelInside = y2 < LABEL_SPACE_PX || other !== null && other.rect !== rect && overlapsBox(labelAbove, other.rect);
404
+ root.style.transform = `translate3d(${x2}px, ${y2}px, 0)`;
405
+ root.style.width = `${rect.width + BOX_PADDING_PX * 2}px`;
406
+ root.style.height = `${rect.height + BOX_PADDING_PX * 2}px`;
407
+ root.toggleAttribute("data-label-inside", labelInside);
408
+ root.style.clipPath = covers.length ? clipPath(x2, y2, covers) : "";
409
+ const labelLeft = labelInside ? x2 + 2 : x2 - 2;
410
+ const labelTop = labelInside ? y2 + 2 : y2 - LABEL_MARGIN_PX - measurement.labelHeight;
411
+ const label = {
412
+ left: labelLeft,
413
+ top: labelTop,
414
+ right: labelLeft + measurement.labelWidth,
415
+ bottom: labelTop + measurement.labelHeight
416
+ };
417
+ root.toggleAttribute(
418
+ "data-label-covered",
419
+ covers.some((c2) => overlaps(c2, label))
420
+ );
421
+ if (root.hasAttribute("data-instant")) {
422
+ root.setAttribute("data-visible", "");
423
+ requestAnimationFrame(() => root.removeAttribute("data-instant"));
424
+ return;
425
+ }
426
+ root.setAttribute("data-visible", "");
427
+ }
428
+ };
429
+
430
+ // src/core/visual-editor/field-path.ts
431
+ var INDEX_SEGMENT = /^\d+$/;
432
+ var WORD_SEPARATORS = /[_-]+/g;
433
+ function normalizeFieldPath(path) {
434
+ return path.split(".").map(
435
+ (segment) => INDEX_SEGMENT.test(segment) ? String(Number.parseInt(segment, 10)) : segment
436
+ ).join(".");
437
+ }
438
+ var GENERIC_LEAVES = /* @__PURE__ */ new Set(["value", "text", "body", "content", "label"]);
439
+ function humanize(name) {
440
+ const words = name.replace(WORD_SEPARATORS, " ").trim();
441
+ return words.charAt(0).toUpperCase() + words.slice(1);
442
+ }
443
+ function fieldLabel(path, locale = "en") {
444
+ if (!path) return t("visualEditor.edit", locale);
445
+ const segments = path.split(".").filter((s2) => s2 && s2 !== "items" && !INDEX_SEGMENT.test(s2));
446
+ const name = segments[segments.length - 1];
447
+ if (!name) return t("visualEditor.edit", locale);
448
+ const parent = segments[segments.length - 2];
449
+ if (parent && GENERIC_LEAVES.has(name)) {
450
+ return `${humanize(parent)} \u203A ${humanize(name)}`;
451
+ }
452
+ return humanize(name);
453
+ }
454
+ function findFieldElement(lookup) {
455
+ if (!lookup.field) return null;
456
+ const candidates = document.querySelectorAll(
457
+ "[data-prepr-encoded]"
458
+ );
459
+ if (lookup.entryId) {
460
+ for (const candidate of candidates) {
461
+ if (candidate.getAttribute("data-prepr-entry-id") === lookup.entryId) {
462
+ return candidate;
463
+ }
464
+ }
465
+ }
466
+ for (const path of [lookup.field, lookup.fieldById]) {
467
+ if (!path) continue;
468
+ const wanted = normalizeFieldPath(path);
469
+ for (const candidate of candidates) {
470
+ const field = candidate.getAttribute("data-prepr-field");
471
+ if (!field || normalizeFieldPath(field) !== wanted) continue;
472
+ if (lookup.id && candidate.getAttribute("data-prepr-id") !== lookup.id) {
473
+ continue;
474
+ }
475
+ return candidate;
476
+ }
477
+ }
478
+ return null;
479
+ }
480
+
160
481
  // src/core/stega/dom.ts
161
482
  var IGNORED_ANCESTORS = "script, style, noscript";
162
483
  function isIgnoredTextNode(node) {
@@ -211,9 +532,20 @@ function tagEncodedElement(element, decoded) {
211
532
  element.setAttribute("data-prepr-origin", decoded.origin);
212
533
  const { id, field, locale } = parseEditHref(decoded.href);
213
534
  setOrRemove(element, "data-prepr-id", id);
214
- setOrRemove(element, "data-prepr-field", field);
535
+ const fieldHint = element.getAttribute("data-prepr-edit-target")?.trim() || void 0;
536
+ setOrRemove(element, "data-prepr-field", field ?? fieldHint);
215
537
  setOrRemove(element, "data-prepr-locale", locale);
216
538
  }
539
+ function readEditPayload(element) {
540
+ return {
541
+ href: element.getAttribute("data-prepr-href") ?? "",
542
+ origin: element.getAttribute("data-prepr-origin") ?? "",
543
+ id: element.getAttribute("data-prepr-id") ?? void 0,
544
+ field: element.getAttribute("data-prepr-field") ?? void 0,
545
+ locale: element.getAttribute("data-prepr-locale") ?? void 0,
546
+ entryId: element.getAttribute("data-prepr-entry-id") || void 0
547
+ };
548
+ }
217
549
 
218
550
  // src/core/stega/elements.ts
219
551
  var debug2 = createScopedLogger("stega:elements");
@@ -304,235 +636,22 @@ var StegaElements = class {
304
636
  attributeFilter: ["class", "style", "hidden", "open", "aria-expanded"]
305
637
  });
306
638
  }
307
- /** Drops the active class but keeps the data attributes, unlike cleanup(). */
308
- cleanupVisuals() {
309
- document.querySelectorAll(".prepr-overlay-active").forEach((element) => element.classList.remove("prepr-overlay-active"));
310
- }
311
- cleanup() {
639
+ /**
640
+ * Stops watching for new content. Tags stay on their elements: the page's
641
+ * stega characters are stripped once they are read, so a tag removed here
642
+ * could never be rebuilt when edit mode is turned back on.
643
+ */
644
+ disconnect() {
312
645
  if (this.observer) {
313
646
  this.observer.disconnect();
314
647
  this.observer = null;
315
648
  }
316
- document.querySelectorAll("[data-prepr-encoded]").forEach((element) => {
317
- element.removeAttribute("data-prepr-encoded");
318
- element.removeAttribute("data-prepr-href");
319
- element.removeAttribute("data-prepr-origin");
320
- element.removeAttribute("data-prepr-id");
321
- element.removeAttribute("data-prepr-field");
322
- element.removeAttribute("data-prepr-locale");
323
- element.classList.remove("prepr-overlay-active");
324
- });
325
649
  this.elements = void 0;
326
650
  }
327
651
  };
328
652
 
329
- // src/core/stega/overlay.ts
330
- var debug3 = createScopedLogger("stega:overlay");
331
- var StegaOverlay = class {
332
- constructor(onEdit) {
333
- this.overlay = null;
334
- this.tooltip = null;
335
- this.hideTimeout = null;
336
- this.currentElement = null;
337
- this.onEdit = onEdit;
338
- }
339
- create() {
340
- const overlay = document.createElement("div");
341
- overlay.className = "prepr-overlay";
342
- overlay.style.display = "none";
343
- const tooltip = document.createElement("div");
344
- tooltip.className = "prepr-tooltip";
345
- tooltip.style.display = "none";
346
- document.body.appendChild(overlay);
347
- document.body.appendChild(tooltip);
348
- this.overlay = overlay;
349
- this.tooltip = tooltip;
350
- return { overlay, tooltip };
351
- }
352
- getTooltip() {
353
- return this.tooltip;
354
- }
355
- hasCurrentElement() {
356
- return this.currentElement !== null;
357
- }
358
- clearHideTimeout() {
359
- if (this.hideTimeout) {
360
- clearTimeout(this.hideTimeout);
361
- this.hideTimeout = null;
362
- }
363
- }
364
- show(element) {
365
- if (!this.overlay || !this.tooltip) return;
366
- this.clearHideTimeout();
367
- if (this.currentElement && this.currentElement !== element) {
368
- this.currentElement.classList.remove("prepr-overlay-active");
369
- }
370
- const rect = element.getBoundingClientRect();
371
- const href = element.getAttribute("data-prepr-href");
372
- const origin = element.getAttribute("data-prepr-origin");
373
- const id = element.getAttribute("data-prepr-id") ?? void 0;
374
- const field = element.getAttribute("data-prepr-field") ?? void 0;
375
- const locale = element.getAttribute("data-prepr-locale") ?? void 0;
376
- debug3.log("showing overlay", { href, origin });
377
- const overlay = this.overlay;
378
- overlay.style.display = "block";
379
- overlay.style.top = `${rect.top + window.scrollY - 2}px`;
380
- overlay.style.left = `${rect.left + window.scrollX - 4}px`;
381
- overlay.style.width = `${rect.width + 8}px`;
382
- overlay.style.height = `${rect.height + 4}px`;
383
- const tooltip = this.tooltip;
384
- if (href && origin) {
385
- const MIN_WIDTH_FOR_TEXT = 80;
386
- const isCompact = rect.width < MIN_WIDTH_FOR_TEXT;
387
- tooltip.textContent = isCompact ? "\u2197" : `${origin} \u2197`;
388
- tooltip.style.display = "block";
389
- tooltip.style.minWidth = isCompact ? "auto" : "80px";
390
- requestAnimationFrame(() => {
391
- let top = rect.top + window.scrollY - tooltip.clientHeight - 2;
392
- let left = rect.right + 4 - tooltip.clientWidth;
393
- const minTop = window.scrollY + 4;
394
- const maxTop = window.scrollY + window.innerHeight - tooltip.clientHeight - 4;
395
- const minLeft = window.scrollX + 4;
396
- const maxLeft = window.scrollX + window.innerWidth - tooltip.clientWidth - 4;
397
- const below = top < minTop;
398
- if (below) {
399
- top = rect.bottom + window.scrollY + 2;
400
- }
401
- top = Math.max(minTop, Math.min(top, maxTop));
402
- left = Math.max(minLeft, Math.min(left, maxLeft));
403
- tooltip.dataset.preprPlacement = below ? "below" : "above";
404
- tooltip.style.top = `${top}px`;
405
- tooltip.style.left = `${left}px`;
406
- });
407
- tooltip.onclick = () => {
408
- this.onEdit({ href, origin, id, field, locale });
409
- };
410
- }
411
- this.currentElement = element;
412
- element.classList.add("prepr-overlay-active");
413
- }
414
- hideImmediate() {
415
- this.clearHideTimeout();
416
- if (this.overlay) this.overlay.style.display = "none";
417
- if (this.tooltip) this.tooltip.style.display = "none";
418
- if (this.currentElement) {
419
- this.currentElement.classList.remove("prepr-overlay-active");
420
- this.currentElement = null;
421
- }
422
- }
423
- hide() {
424
- if (!this.overlay || !this.tooltip) return;
425
- this.clearHideTimeout();
426
- this.hideTimeout = setTimeout(() => this.hideImmediate(), 100);
427
- }
428
- cleanup() {
429
- this.clearHideTimeout();
430
- if (this.overlay) {
431
- this.overlay.style.display = "none";
432
- this.overlay.parentNode?.removeChild(this.overlay);
433
- this.overlay = null;
434
- }
435
- if (this.tooltip) {
436
- this.tooltip.style.display = "none";
437
- this.tooltip.parentNode?.removeChild(this.tooltip);
438
- this.tooltip = null;
439
- }
440
- if (this.currentElement) {
441
- this.currentElement.classList.remove("prepr-overlay-active");
442
- this.currentElement = null;
443
- }
444
- }
445
- };
446
-
447
- // src/core/stega/proximity.ts
448
- var debug4 = createScopedLogger("stega:proximity");
449
- var StegaProximity = class {
450
- constructor() {
451
- this.highlighted = /* @__PURE__ */ new Set();
452
- this.visible = /* @__PURE__ */ new Set();
453
- this.observer = null;
454
- this.getEncodedElements = createElementCache(
455
- "[data-prepr-encoded]",
456
- 200
457
- );
458
- }
459
- /** Call after the encoded element set changes — rebuilds the observer. */
460
- refreshObserving() {
461
- try {
462
- if (typeof IntersectionObserver === "undefined") return;
463
- if (this.observer) {
464
- this.observer.disconnect();
465
- this.observer = null;
466
- }
467
- this.visible = /* @__PURE__ */ new Set();
468
- this.observer = new IntersectionObserver(
469
- (entries) => {
470
- entries.forEach((entry) => {
471
- const el = entry.target;
472
- if (entry.isIntersecting) {
473
- this.visible.add(el);
474
- } else {
475
- this.visible.delete(el);
476
- }
477
- });
478
- },
479
- { root: null, rootMargin: "0px", threshold: 0 }
480
- );
481
- const nodes = this.getEncodedElements();
482
- nodes.forEach((el) => this.observer.observe(el));
483
- debug4.log("observing", nodes.length, "encoded elements");
484
- } catch (error) {
485
- debug4.log("error setting up IntersectionObserver", error);
486
- }
487
- }
488
- stopObserving() {
489
- if (this.observer) {
490
- this.observer.disconnect();
491
- this.observer = null;
492
- this.visible.clear();
493
- }
494
- }
495
- updateElementGradients(cursorX, cursorY) {
496
- const candidates = this.visible.size > 0 ? Array.from(this.visible) : Array.from(this.getEncodedElements());
497
- const newHighlighted = /* @__PURE__ */ new Set();
498
- candidates.forEach((element) => {
499
- const rect = element.getBoundingClientRect();
500
- const distance = Math.min(
501
- Math.abs(cursorX - rect.left),
502
- Math.abs(cursorX - rect.right),
503
- Math.abs(cursorY - rect.top),
504
- Math.abs(cursorY - rect.bottom)
505
- );
506
- if (distance < 150) {
507
- const relativeX = cursorX - rect.left;
508
- const relativeY = cursorY - rect.top;
509
- element.style.setProperty("--cursor-x", `${relativeX}px`);
510
- element.style.setProperty("--cursor-y", `${relativeY}px`);
511
- const baseGradientSize = Math.max(
512
- 150,
513
- Math.max(rect.width, rect.height) * 1.1
514
- );
515
- const distanceScale = Math.max(0, (400 - distance) / 400);
516
- const gradientSize = baseGradientSize * distanceScale;
517
- element.style.setProperty("--gradient-size", `${gradientSize}px`);
518
- element.classList.add("prepr-proximity-highlight");
519
- newHighlighted.add(element);
520
- } else {
521
- element.classList.remove("prepr-proximity-highlight");
522
- }
523
- });
524
- this.highlighted = newHighlighted;
525
- }
526
- clearAllHighlights() {
527
- this.highlighted.forEach(
528
- (element) => element.classList.remove("prepr-proximity-highlight")
529
- );
530
- this.highlighted.clear();
531
- }
532
- };
533
-
534
653
  // src/core/stega/stega.generated.ts
535
- 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}';
654
+ var STEGA_STYLES = '@property --prepr-presence{syntax:"<number>";inherits:true;initial-value:1}[data-prepr-encoded]{outline:1px dashed rgba(79,70,229,calc(.3 * var(--prepr-presence,1)));outline-offset:2px}';
536
655
 
537
656
  // src/core/stega/styles.ts
538
657
  var STEGA_STYLE_MARKER = "data-prepr-stega";
@@ -552,46 +671,80 @@ function removeStegaStyles() {
552
671
  }
553
672
 
554
673
  // src/core/stega/scan.ts
555
- var debug5 = createScopedLogger("stega:scan");
674
+ var debug3 = createScopedLogger("stega:scan");
675
+ var ENCODED_SELECTOR = "[data-prepr-encoded]";
676
+ var PRESENCE_FADE_MS = 400;
556
677
  var EDITOR_MODE_STYLES = "[data-prepr-encoded]{cursor:pointer}";
678
+ function fadeInPresence() {
679
+ const root = document.documentElement;
680
+ if (typeof root.animate !== "function") return;
681
+ if (window.matchMedia?.("(prefers-reduced-motion: reduce)").matches) return;
682
+ root.animate(
683
+ { "--prepr-presence": ["0", "1"] },
684
+ { duration: PRESENCE_FADE_MS, easing: "ease-out" }
685
+ );
686
+ }
557
687
  function createStegaController(opts) {
558
688
  const tooltipEnabled = opts.tooltip ?? true;
559
- const overlay = new StegaOverlay(opts.onEdit);
560
- const proximity = new StegaProximity();
689
+ const locale = opts.locale ?? "en";
561
690
  const elements = new StegaElements();
691
+ const overlay = new VisualEditorOverlay({
692
+ onLabelClick: (target) => opts.onEdit(readEditPayload(target))
693
+ });
562
694
  let initialized = false;
563
- let throttledMouseMove = null;
564
- const handleTooltipMouseEnter = () => {
565
- overlay.clearHideTimeout();
695
+ let pointerFrame = null;
696
+ let pointerTarget = null;
697
+ let pointerX = 0;
698
+ let pointerY = 0;
699
+ let hitTestPending = false;
700
+ const updateHover = () => {
701
+ pointerFrame = null;
702
+ if (hitTestPending) {
703
+ hitTestPending = false;
704
+ pointerTarget = document.elementFromPoint(pointerX, pointerY);
705
+ }
706
+ if (overlay.owns(pointerTarget)) return;
707
+ const encoded = pointerTarget instanceof Element ? pointerTarget.closest(ENCODED_SELECTOR) : null;
708
+ overlay.setHover(
709
+ encoded,
710
+ encoded ? fieldLabel(encoded.getAttribute("data-prepr-field"), locale) : ""
711
+ );
566
712
  };
567
- const handleTooltipMouseLeave = () => {
568
- if (!overlay.hasCurrentElement()) {
569
- overlay.hide();
713
+ const scheduleHover = () => {
714
+ if (pointerFrame === null) {
715
+ pointerFrame = requestAnimationFrame(updateHover);
570
716
  }
571
717
  };
718
+ const handlePointer = (e2) => {
719
+ pointerTarget = e2.target;
720
+ pointerX = e2.clientX;
721
+ pointerY = e2.clientY;
722
+ hitTestPending = false;
723
+ scheduleHover();
724
+ };
725
+ const handlePointerLeave = () => {
726
+ pointerTarget = null;
727
+ hitTestPending = false;
728
+ overlay.setHover(null, "");
729
+ };
572
730
  const handleScroll = () => {
573
- overlay.hideImmediate();
574
- proximity.clearAllHighlights();
731
+ overlay.onScroll();
732
+ if (pointerTarget === null) return;
733
+ hitTestPending = true;
734
+ scheduleHover();
575
735
  };
736
+ const handleResize = () => overlay.schedule();
576
737
  const handleClick = (e2) => {
577
738
  const target = e2.target;
578
- const encoded = target?.closest(
579
- "[data-prepr-encoded]"
580
- );
739
+ const encoded = target?.closest(ENCODED_SELECTOR);
581
740
  if (!encoded) return;
582
741
  e2.preventDefault();
583
742
  e2.stopPropagation();
584
- opts.onEdit({
585
- href: encoded.getAttribute("data-prepr-href") ?? "",
586
- origin: encoded.getAttribute("data-prepr-origin") ?? "",
587
- id: encoded.getAttribute("data-prepr-id") ?? void 0,
588
- field: encoded.getAttribute("data-prepr-field") ?? void 0,
589
- locale: encoded.getAttribute("data-prepr-locale") ?? void 0
590
- });
743
+ opts.onEdit(readEditPayload(encoded));
591
744
  };
592
745
  const start = () => {
593
746
  if (initialized) {
594
- debug5.log("already initialized, skipping setup");
747
+ debug3.log("already initialized, skipping setup");
595
748
  return;
596
749
  }
597
750
  if (!tooltipEnabled) {
@@ -601,91 +754,85 @@ function createStegaController(opts) {
601
754
  });
602
755
  document.addEventListener("click", handleClick, { capture: true });
603
756
  initialized = true;
604
- debug5.log("stega controller started (editor mode)");
757
+ debug3.log("stega controller started (headless)");
605
758
  return;
606
759
  }
607
760
  injectStegaStyles();
608
- const { tooltip } = overlay.create();
609
- tooltip.addEventListener("mouseenter", handleTooltipMouseEnter);
610
- tooltip.addEventListener("mouseleave", handleTooltipMouseLeave);
761
+ fadeInPresence();
762
+ overlay.mount();
611
763
  elements.scanDocument(true);
612
- proximity.refreshObserving();
613
- elements.setupMutationObserver(() => proximity.refreshObserving());
614
- throttledMouseMove = throttle((e2) => {
615
- const mouseEvent = e2;
616
- const target = mouseEvent.target;
617
- if (!target) return;
618
- if (target.closest(".prepr-tooltip")) return;
619
- proximity.updateElementGradients(mouseEvent.clientX, mouseEvent.clientY);
620
- const encoded = target.closest("[data-prepr-encoded]");
621
- if (encoded) {
622
- overlay.show(encoded);
623
- } else {
624
- overlay.hide();
625
- }
626
- }, 16);
627
- document.addEventListener("mousemove", throttledMouseMove, {
628
- passive: true
764
+ elements.setupMutationObserver(() => {
765
+ if (pointerTarget === null) return;
766
+ hitTestPending = true;
767
+ scheduleHover();
629
768
  });
769
+ document.addEventListener("pointermove", handlePointer, { passive: true });
770
+ document.addEventListener("pointerdown", handlePointer, { passive: true });
771
+ document.documentElement.addEventListener(
772
+ "pointerleave",
773
+ handlePointerLeave
774
+ );
630
775
  window.addEventListener("scroll", handleScroll, {
631
776
  capture: true,
632
777
  passive: true
633
778
  });
779
+ window.addEventListener("resize", handleResize, { passive: true });
634
780
  initialized = true;
635
- debug5.log("stega controller started");
781
+ debug3.log("stega controller started");
636
782
  };
637
783
  const stop = () => {
638
784
  if (!initialized) return;
785
+ initialized = false;
639
786
  if (!tooltipEnabled) {
640
787
  document.removeEventListener("click", handleClick, { capture: true });
641
- elements.cleanup();
788
+ elements.disconnect();
642
789
  removeStegaStyles();
643
- initialized = false;
644
- debug5.log("stega controller stopped (editor mode)");
790
+ debug3.log("stega controller stopped (headless)");
645
791
  return;
646
792
  }
647
- if (throttledMouseMove) {
648
- document.removeEventListener("mousemove", throttledMouseMove);
649
- throttledMouseMove.cancel();
650
- throttledMouseMove = null;
651
- }
793
+ if (pointerFrame !== null) cancelAnimationFrame(pointerFrame);
794
+ pointerFrame = null;
795
+ pointerTarget = null;
796
+ hitTestPending = false;
797
+ document.removeEventListener("pointermove", handlePointer);
798
+ document.removeEventListener("pointerdown", handlePointer);
799
+ document.documentElement.removeEventListener(
800
+ "pointerleave",
801
+ handlePointerLeave
802
+ );
652
803
  window.removeEventListener("scroll", handleScroll, { capture: true });
653
- const tooltip = overlay.getTooltip();
654
- if (tooltip) {
655
- tooltip.removeEventListener("mouseenter", handleTooltipMouseEnter);
656
- tooltip.removeEventListener("mouseleave", handleTooltipMouseLeave);
657
- }
658
- overlay.cleanup();
659
- proximity.clearAllHighlights();
660
- proximity.stopObserving();
661
- elements.cleanup();
804
+ window.removeEventListener("resize", handleResize);
805
+ overlay.unmount();
806
+ elements.disconnect();
662
807
  removeStegaStyles();
663
- initialized = false;
664
- debug5.log("stega controller stopped");
808
+ debug3.log("stega controller stopped");
665
809
  };
666
810
  return { start, stop };
667
811
  }
668
812
 
669
813
  // src/core/stega/auto-clean.ts
670
814
  import { vercelStegaSplit } from "@vercel/stega";
671
- var debug6 = createScopedLogger("stega:auto-clean");
815
+ var debug4 = createScopedLogger("stega:auto-clean");
816
+ var cleanedNodes = /* @__PURE__ */ new WeakMap();
672
817
  function createStegaAutoClean(options) {
673
818
  const stripText = options?.enabled ?? true;
674
819
  let observer = null;
675
820
  let processing = false;
676
- let cleaned = /* @__PURE__ */ new WeakMap();
677
821
  let pendingHidden = /* @__PURE__ */ new Set();
678
822
  let pendingDirty = /* @__PURE__ */ new Set();
679
823
  let debounceTimeout = null;
680
824
  function cleanTextNode(textNode) {
681
825
  if (isIgnoredTextNode(textNode)) return false;
682
826
  const textContent = textNode.textContent;
683
- const info = cleaned.get(textNode);
827
+ const info = cleanedNodes.get(textNode);
684
828
  const settled = stripText ? info?.cleaned : info?.original;
685
829
  if (info && textContent === settled) {
686
830
  if (!info.decoded) return false;
687
831
  const shown = resolveEditTarget(textNode);
688
- if (!shown) return false;
832
+ if (!shown) {
833
+ pendingHidden.add(textNode);
834
+ return false;
835
+ }
689
836
  pendingHidden.delete(textNode);
690
837
  if (shown.hasAttribute("data-prepr-encoded")) return false;
691
838
  tagEncodedElement(shown, info.decoded);
@@ -696,7 +843,7 @@ function createStegaAutoClean(options) {
696
843
  if (!encoded || stripped === textContent) return false;
697
844
  const decoded = decodeStega(textContent);
698
845
  if (!decoded?.href && !stripText) return false;
699
- cleaned.set(textNode, {
846
+ cleanedNodes.set(textNode, {
700
847
  original: textContent,
701
848
  cleaned: stripped,
702
849
  decoded: decoded?.href ? decoded : null
@@ -711,7 +858,7 @@ function createStegaAutoClean(options) {
711
858
  }
712
859
  pendingHidden.add(textNode);
713
860
  } catch (error) {
714
- debug6.log("error cleaning text node", error);
861
+ debug4.log("error cleaning text node", error);
715
862
  }
716
863
  return false;
717
864
  }
@@ -738,7 +885,7 @@ function createStegaAutoClean(options) {
738
885
  }
739
886
  if (mutation.type === "characterData" && mutation.target.nodeType === Node.TEXT_NODE) {
740
887
  const textNode = mutation.target;
741
- const info = cleaned.get(textNode);
888
+ const info = cleanedNodes.get(textNode);
742
889
  const text = textNode.textContent ?? "";
743
890
  const settled = stripText ? info?.cleaned : info?.original;
744
891
  if (text !== settled && vercelStegaSplit(text).encoded) {
@@ -781,7 +928,16 @@ function createStegaAutoClean(options) {
781
928
  retryPendingHidden();
782
929
  }, 50);
783
930
  }
931
+ let resizeTimeout = null;
932
+ function handleResize() {
933
+ if (pendingHidden.size === 0 || resizeTimeout) return;
934
+ resizeTimeout = setTimeout(() => {
935
+ resizeTimeout = null;
936
+ retryPendingHidden();
937
+ }, 100);
938
+ }
784
939
  function setupObserver() {
940
+ window.addEventListener("resize", handleResize, { passive: true });
785
941
  observer = new MutationObserver(handleMutations);
786
942
  observer.observe(document.body, {
787
943
  childList: true,
@@ -797,27 +953,31 @@ function createStegaAutoClean(options) {
797
953
  if (observer || typeof window === "undefined") return;
798
954
  scanAndClean();
799
955
  setupObserver();
800
- debug6.log("stega auto-clean started");
956
+ debug4.log("stega auto-clean started");
801
957
  }
802
958
  function stop() {
803
959
  if (debounceTimeout) {
804
960
  clearTimeout(debounceTimeout);
805
961
  debounceTimeout = null;
806
962
  }
963
+ if (resizeTimeout) {
964
+ clearTimeout(resizeTimeout);
965
+ resizeTimeout = null;
966
+ }
807
967
  if (observer) {
808
968
  observer.disconnect();
809
969
  observer = null;
970
+ window.removeEventListener("resize", handleResize);
810
971
  }
811
- cleaned = /* @__PURE__ */ new WeakMap();
812
972
  pendingHidden = /* @__PURE__ */ new Set();
813
973
  pendingDirty = /* @__PURE__ */ new Set();
814
- debug6.log("stega auto-clean stopped");
974
+ debug4.log("stega auto-clean stopped");
815
975
  }
816
976
  return { start, stop };
817
977
  }
818
978
 
819
979
  // src/core/stega/scroll-to-field.ts
820
- var debug7 = createScopedLogger("stega:scroll");
980
+ var debug5 = createScopedLogger("stega:scroll");
821
981
  var FLASH_DURATION_MS = 1200;
822
982
  var flashTimeout = null;
823
983
  function scrollToField(request) {
@@ -834,7 +994,7 @@ function scrollToField(request) {
834
994
  break;
835
995
  }
836
996
  if (!target) {
837
- debug7.log("no element found for field", field);
997
+ debug5.log("no element found for field", field);
838
998
  return false;
839
999
  }
840
1000
  target.scrollIntoView({ behavior: "smooth", block: "center" });
@@ -888,7 +1048,7 @@ function createToolbarStore(initial = {}) {
888
1048
  var n;
889
1049
  var l;
890
1050
  var u;
891
- var t;
1051
+ var t2;
892
1052
  var i;
893
1053
  var r;
894
1054
  var o;
@@ -1130,7 +1290,7 @@ n = w.slice, l = { __e: function(n2, l2, u3, t3) {
1130
1290
  n2 = l3;
1131
1291
  }
1132
1292
  throw n2;
1133
- } }, u = 0, t = function(n2) {
1293
+ } }, u = 0, t2 = function(n2) {
1134
1294
  return null != n2 && void 0 === n2.constructor;
1135
1295
  }, C.prototype.setState = function(n2, l2) {
1136
1296
  var u3;
@@ -1166,7 +1326,7 @@ var CLOSE_ICON_SVG = `<svg width="10" height="10" viewBox="0 0 10 10" fill="none
1166
1326
  </svg>`;
1167
1327
 
1168
1328
  // src/core/ui/toolbar.generated.ts
1169
- 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}}";
1329
+ var TOOLBAR_CSS = ":host{--prepr-primary:#4338ca;--prepr-bg:#eef2ff;--prepr-text:#1f2937;--prepr-radius:8px;--prepr-shadow:0px 0px 40px 0px #1f29373d;--prepr-z-index:10000;--prepr-primary-50:#eef2ff;--prepr-primary-100:#e0e7ff;--prepr-primary-800:#3730a3;--prepr-indigo-600:#4f46e5;--prepr-indigo-50:#eef2ff;--prepr-indigo-700:#4338ca;--prepr-secondary-400:#fb923c;--prepr-secondary-500:#f97316;--prepr-gray-100:#f3f4f6;--prepr-gray-200:#e5e7eb;--prepr-gray-300:#d1d5db;--prepr-gray-400:#9ca3af;--prepr-gray-500:#6b7280;--prepr-gray-700:#374151;--prepr-gray-800:#1f2937;--prepr-gray-900:#111827;--prepr-shadow-sm:0px 0px 12px 0px #c3c3c3cc;color:var(--prepr-text);box-sizing:border-box;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica,Arial,sans-serif;font-size:14px;line-height:1.5}:host *,:host :before,:host :after,*,:before,:after{box-sizing:border-box}button{font-family:inherit;font-size:inherit;margin:0}.prepr-regular-text{font-size:.875rem;font-weight:500;line-height:1.5}.prepr-backdrop{z-index:calc(var(--prepr-z-index) - 1);opacity:0;pointer-events:none;background:#0000001a;transition:opacity .2s ease-in-out;position:fixed;top:0;bottom:0;left:0;right:0}.prepr-backdrop[data-open=true]{opacity:1;pointer-events:auto;animation:.2s ease-in-out prepr-fade-in}.prepr-container{z-index:var(--prepr-z-index);justify-content:flex-end;align-items:center;display:flex;position:fixed;top:calc(50% - 64px);right:0}.prepr-toggle-holder{padding:.5rem}.prepr-toggle{cursor:pointer;background:var(--prepr-primary);color:#fff;border:none;border-radius:9999px;justify-content:center;align-items:center;width:2.25rem;height:2.25rem;display:flex}.prepr-toggle>span{display:flex}.prepr-toggle svg{color:#fff}.prepr-panel{z-index:var(--prepr-z-index);opacity:0;pointer-events:none;justify-content:flex-end;padding:.75rem;transition:opacity .2s ease-in-out;display:flex;position:fixed;top:50%;right:0;transform:translateY(-50%)}.prepr-panel[data-open=true]{opacity:1;pointer-events:auto;animation:.2s ease-in-out prepr-fade-in}.prepr-content{box-shadow:var(--prepr-shadow);border-radius:var(--prepr-radius);background:var(--prepr-bg);flex-direction:column;gap:2.5rem;width:100%;padding:1.5rem;display:flex}@media (min-width:640px){.prepr-panel{margin-right:4rem;padding:0}.prepr-content{width:502px;padding:2.5rem}}.prepr-header{justify-content:space-between;align-items:center;gap:.5rem;display:flex}.prepr-header-left{align-items:flex-start;gap:.75rem;display:flex}.prepr-badge{background:var(--prepr-primary-100);height:1.5rem;color:var(--prepr-primary);border-radius:4px;align-items:center;padding:.25rem .75rem;font-size:.875rem;display:flex}.prepr-header-close{cursor:pointer;background:var(--prepr-primary-100);width:2rem;height:2rem;color:var(--prepr-gray-700);border:none;border-radius:6px;justify-content:center;align-items:center;display:flex}.prepr-section{flex-direction:column;gap:.5rem;display:flex}.prepr-section-label{color:var(--prepr-gray-400);font-size:.875rem}.prepr-row{flex-wrap:wrap;justify-content:space-between;align-items:center;gap:.5rem 1.5rem;display:flex}.prepr-row-title{color:var(--prepr-gray-800);margin:0;font-size:1rem;font-weight:600}.prepr-radiogroup{border-radius:var(--prepr-radius);border:1px solid var(--prepr-gray-300);background:#fff;align-items:center;gap:.25rem;height:2.5rem;padding:.25rem;display:flex}.prepr-radiogroup[data-disabled=true]{cursor:not-allowed;opacity:.5}.prepr-radiogroup[data-disabled=true] .prepr-radio{pointer-events:none}.prepr-radio{cursor:pointer;white-space:nowrap;text-align:center;height:2rem;color:var(--prepr-gray-900);background:0 0;border:none;border-radius:6px;justify-content:center;align-items:center;padding:.5rem 1.125rem;font-size:.875rem;font-weight:500;transition:all .2s ease-in-out;display:flex}.prepr-radio[data-checked=true]{background:var(--prepr-indigo-600);color:#fff;box-shadow:var(--prepr-shadow-sm)}.prepr-radio[data-checked=true][data-off=true]{background:var(--prepr-gray-800);color:#fff}.prepr-radio-variant{width:82px}.prepr-listbox{position:relative}.prepr-segment-button{border-radius:var(--prepr-radius);border:1px solid var(--prepr-gray-300);width:240px;height:2.5rem;color:var(--prepr-gray-500);cursor:pointer;background:#fff;flex-shrink:0;align-items:center;gap:.5rem;padding:0 .5rem;font-size:.875rem;font-weight:500;display:flex;overflow:hidden}@media (min-width:768px){.prepr-segment-button{padding:0 1rem}}.prepr-segment-button:disabled{cursor:not-allowed;background:var(--prepr-gray-200);color:var(--prepr-gray-400)}.prepr-segment-button[aria-expanded=true]{border-bottom-color:#fff;border-bottom-right-radius:0;border-bottom-left-radius:0}.prepr-segment-label{white-space:nowrap;text-overflow:ellipsis;text-align:left;width:100%;margin-right:auto;overflow:hidden}.prepr-segment-caret{color:var(--prepr-gray-800);display:flex}.prepr-options{z-index:calc(var(--prepr-z-index) + 1);border-left:1px solid var(--prepr-gray-300);border-right:1px solid var(--prepr-gray-300);border-bottom:1px solid var(--prepr-gray-300);-ms-overflow-style:none;scrollbar-width:none;background:#fff;border-radius:0 0 6px 6px;width:240px;max-height:300px;margin:0;padding:0;list-style:none;position:absolute;top:100%;left:0;overflow-y:auto;box-shadow:0 20px 25px -5px #0000001a,0 8px 10px -6px #0000001a}.prepr-options::-webkit-scrollbar{display:none}.prepr-options[hidden]{display:none}.prepr-option-search{background:#fff;padding:.5rem;position:-webkit-sticky;position:sticky;top:0}.prepr-segment-search{border:1px solid var(--prepr-gray-300);border-radius:var(--prepr-radius);width:100%;height:2rem;font:inherit;color:var(--prepr-gray-900);box-sizing:border-box;background:#fff;padding:0 .5rem;font-size:.875rem}@media (pointer:coarse){.prepr-segment-search{font-size:16px}}.prepr-segment-search::placeholder{color:var(--prepr-gray-400)}.prepr-segment-search:focus{border-color:var(--prepr-gray-400);outline:none}.prepr-option{cursor:pointer;width:100%;color:var(--prepr-gray-900);background:#fff;align-items:center;padding:.5rem 1rem;font-size:.875rem;font-weight:500;display:flex}.prepr-option:hover{background:var(--prepr-gray-100)}.prepr-option[aria-selected=true]{background:var(--prepr-indigo-50);color:var(--prepr-indigo-700)}.prepr-option-label{white-space:nowrap;text-overflow:ellipsis;text-align:left;width:100%;margin-right:auto;overflow:hidden}.prepr-reset{background:var(--prepr-secondary-400);color:#fff;cursor:pointer;border:none;border-radius:6px;justify-content:center;align-items:center;gap:.5rem;width:100%;height:2.5rem;padding:.5rem .75rem;font-size:.875rem;font-weight:500;display:flex}.prepr-reset:hover:not(:disabled){background:var(--prepr-secondary-500)}.prepr-reset:disabled{background:var(--prepr-gray-400);color:var(--prepr-gray-500);cursor:not-allowed}@media (min-width:768px){.prepr-reset{width:108px}}.prepr-indicators{z-index:var(--prepr-z-index);gap:.75rem;display:flex;position:fixed;bottom:1.5rem;right:1.5rem}.prepr-status-pill{background:var(--prepr-primary);color:#fff;cursor:pointer;border:none;border-radius:9999px;align-items:center;gap:.5rem;padding:.5rem 1rem;font-size:.75rem;font-weight:500;transition:background-color .15s;display:flex;box-shadow:0 10px 15px -3px #0000001a,0 4px 6px -4px #0000001a}.prepr-status-pill[data-interactive=false]{cursor:default;opacity:1;color:#fff}.prepr-status-pill[data-interactive=true]:hover{background:var(--prepr-primary-800)}.prepr-status-pill[hidden]{display:none}.prepr-status-viewing{color:#fff9;font-size:10px}.prepr-status-segment{text-overflow:ellipsis;white-space:nowrap;max-width:120px;display:inline-block;overflow:hidden}.prepr-status-variant{text-overflow:ellipsis;white-space:nowrap;background:#fff3;border-radius:4px;max-width:80px;padding:0 .5rem;display:inline-block;overflow:hidden}.prepr-status-x{color:#fff;width:.75rem;height:.75rem}.prepr-close-edit-pill{background:var(--prepr-primary-50);color:var(--prepr-gray-800);cursor:pointer;border:none;border-radius:9999px;align-items:center;gap:.5rem;padding:.5rem 1rem;font-size:.75rem;font-weight:500;transition:background-color .15s;display:flex;box-shadow:0 10px 15px -3px #0000001a,0 4px 6px -4px #0000001a}.prepr-close-edit-pill:hover{background:var(--prepr-primary-100)}.prepr-close-edit-pill[hidden]{display:none}.prepr-close-edit-x{width:1rem;height:1rem;color:var(--prepr-gray-500);transition:color .15s}.prepr-close-edit-pill:hover .prepr-close-edit-x{color:var(--prepr-gray-700)}.prepr-tip{z-index:calc(var(--prepr-z-index) + 2);overflow-wrap:break-word;background:var(--prepr-gray-900);color:#fff;pointer-events:none;border-radius:6px;width:auto;max-width:min(420px,100vw - 16px);padding:.5rem .75rem;font-size:.75rem;font-weight:500;position:fixed;box-shadow:0 20px 25px -5px #0000001a,0 8px 10px -6px #0000001a}.prepr-tip-arrow{border-left:6px solid #0000;border-right:6px solid #0000;border-top:6px solid var(--prepr-gray-900);width:0;height:0;position:absolute;top:100%;transform:translate(-50%)}[hidden]{display:none!important}@keyframes prepr-fade-in{0%{opacity:0}to{opacity:1}}";
1170
1330
 
1171
1331
  // ../../node_modules/.pnpm/preact@10.29.7/node_modules/preact/jsx-runtime/dist/jsxRuntime.mjs
1172
1332
  var f2 = 0;
@@ -1872,6 +2032,7 @@ var PARAM_SEGMENT = "prepr_preview_segment";
1872
2032
  var PARAM_VARIANT = "prepr_preview_ab";
1873
2033
  var PARAM_HIDE_BAR = "prepr_hide_bar";
1874
2034
  var ONE_YEAR_SECONDS = 60 * 60 * 24 * 365;
2035
+ var HEADER_LIVE_RENDER = "x-prepr-live";
1875
2036
 
1876
2037
  // src/core/cookies.ts
1877
2038
  function getCookie(name) {
@@ -1913,167 +2074,6 @@ function crossSiteCookieOptions() {
1913
2074
  return { sameSite: "None", secure: true };
1914
2075
  }
1915
2076
 
1916
- // src/core/locales/en.json
1917
- var en_default = {
1918
- common: {
1919
- viewingAs: "Viewing as:",
1920
- user: "User",
1921
- off: "Off",
1922
- on: "On",
1923
- reset: "Reset",
1924
- toolbar: "Toolbar"
1925
- },
1926
- adaptiveContent: {
1927
- adaptiveContent: "Adaptive content",
1928
- enablePreview: "Preview mode",
1929
- segment: "Segment",
1930
- ABVariant: "A/B variant",
1931
- chooseSegment: "Choose segment",
1932
- searchSegments: "Search segments",
1933
- none: "No segments",
1934
- allOtherUsers: "All other users",
1935
- offDescription: "View the site as a real user (use cookies)",
1936
- onDescription: "Use the toolbar to simulate segments and A/B variants"
1937
- },
1938
- editingTools: {
1939
- editingTools: "Editing tools",
1940
- editMode: "Edit mode",
1941
- ariaCloseEditMode: "Close edit mode"
1942
- }
1943
- };
1944
-
1945
- // src/core/locales/nl.json
1946
- var nl_default = {
1947
- common: {
1948
- viewingAs: "Bekijken als:",
1949
- user: "Gebruiker",
1950
- off: "Uit",
1951
- on: "Aan",
1952
- reset: "Resetten",
1953
- toolbar: "Toolbar"
1954
- },
1955
- adaptiveContent: {
1956
- adaptiveContent: "Adaptieve inhoud",
1957
- enablePreview: "Preview modus",
1958
- segment: "Segment",
1959
- ABVariant: "A/B-variant",
1960
- chooseSegment: "Kies segment",
1961
- searchSegments: "Zoek segmenten",
1962
- none: "Geen segmenten",
1963
- allOtherUsers: "Alle andere gebruikers",
1964
- offDescription: "Bekijk de site als een echte gebruiker (gebruik cookies)",
1965
- onDescription: "Gebruik de werkbalk om segmenten en A/B-varianten te simuleren"
1966
- },
1967
- editingTools: {
1968
- editingTools: "Editing Tools",
1969
- editMode: "Edit mode",
1970
- ariaCloseEditMode: "Edit mode sluiten"
1971
- }
1972
- };
1973
-
1974
- // src/core/i18n.ts
1975
- var dictionaries = {
1976
- en: en_default,
1977
- nl: nl_default
1978
- };
1979
- var LOCALES = Object.keys(dictionaries);
1980
- function isLocale(value) {
1981
- return typeof value === "string" && value in dictionaries;
1982
- }
1983
- function getDict(locale) {
1984
- return isLocale(locale) ? dictionaries[locale] : dictionaries.en;
1985
- }
1986
- function getFromPath(obj, path) {
1987
- return path.split(".").reduce(
1988
- (acc, key) => acc && typeof acc === "object" ? acc[key] : void 0,
1989
- obj
1990
- );
1991
- }
1992
- function format(message, vars) {
1993
- if (!vars) return message;
1994
- return Object.keys(vars).reduce(
1995
- (acc, k2) => acc.replace(new RegExp(`\\{${k2}\\}`, "g"), String(vars[k2])),
1996
- message
1997
- );
1998
- }
1999
- function t2(key, locale, vars) {
2000
- const dict = getDict(locale);
2001
- const msg = getFromPath(dict, key);
2002
- if (typeof msg === "string") return format(msg, vars);
2003
- return key;
2004
- }
2005
-
2006
- // src/core/iframe-bridge.ts
2007
- var debug8 = createScopedLogger("iframe-bridge");
2008
- var EDITOR_BASE_DOMAIN = "prepr.io";
2009
- function isAllowedEditorOrigin(origin, allowed) {
2010
- if (allowed) return allowed.includes(origin);
2011
- let url;
2012
- try {
2013
- url = new URL(origin);
2014
- } catch {
2015
- return false;
2016
- }
2017
- if (url.protocol !== "https:" || url.port !== "") return false;
2018
- const suffix = `.${EDITOR_BASE_DOMAIN}`;
2019
- if (!url.hostname.endsWith(suffix)) return false;
2020
- const label = url.hostname.slice(0, -suffix.length);
2021
- return label.length > 0 && !label.includes(".");
2022
- }
2023
- function createIframeBridge(store, options = {}) {
2024
- const allowedOrigins = options.allowedEditorOrigins;
2025
- const onScrollToField = options.onScrollToField;
2026
- let parentOrigin = null;
2027
- const onKeyDown = (event) => {
2028
- const key = event.key.toLowerCase();
2029
- const blocked = (event.ctrlKey || event.metaKey) && ["s", "p", "l"].includes(key);
2030
- if (blocked) event.preventDefault();
2031
- };
2032
- const onMessage = (evt) => {
2033
- const data = evt?.data;
2034
- if (data?.event === "prepr:initVE" && !parentOrigin) {
2035
- if (!isAllowedEditorOrigin(evt.origin, allowedOrigins)) return;
2036
- parentOrigin = evt.origin;
2037
- setTrustedParentOrigin(parentOrigin);
2038
- if (data.scrollPosition != null) {
2039
- const top = data.scrollPosition;
2040
- setTimeout(() => window.scrollTo(0, top), 1);
2041
- }
2042
- store?.set({ previewMode: true, editMode: data.editMode ?? true });
2043
- }
2044
- if (!parentOrigin || evt.origin !== parentOrigin) return;
2045
- if (data?.event === "prepr:getScrollPosition") {
2046
- const currentScrollY = window.scrollY || document.documentElement.scrollTop;
2047
- sendPreprEvent("getScrollPosition", { value: currentScrollY });
2048
- }
2049
- if (data?.event === "prepr:scrollToField") {
2050
- if (typeof data.field !== "string" || !data.field) return;
2051
- if (data.id !== void 0 && typeof data.id !== "string") return;
2052
- try {
2053
- onScrollToField?.({ field: data.field, id: data.id });
2054
- } catch (error) {
2055
- debug8.warn("scrollToField handler failed", error);
2056
- }
2057
- }
2058
- };
2059
- return {
2060
- start() {
2061
- const features = store?.get().features;
2062
- sendPreprEvent("loaded", features ? { features } : void 0, {
2063
- allowUntrustedTarget: true
2064
- });
2065
- window.addEventListener("keydown", onKeyDown);
2066
- window.addEventListener("message", onMessage);
2067
- },
2068
- stop() {
2069
- window.removeEventListener("keydown", onKeyDown);
2070
- window.removeEventListener("message", onMessage);
2071
- parentOrigin = null;
2072
- setTrustedParentOrigin(null);
2073
- }
2074
- };
2075
- }
2076
-
2077
2077
  // src/core/toolbar-change-handler.ts
2078
2078
  function cookieOpts() {
2079
2079
  return {
@@ -2172,8 +2172,726 @@ function splitPath(full) {
2172
2172
  return [full.slice(0, index), full.slice(index + 1)];
2173
2173
  }
2174
2174
 
2175
+ // src/core/visual-editor/asset-images.ts
2176
+ var MAX_URL_LENGTH = 4096;
2177
+ var MIN_KEY_SEGMENTS = 2;
2178
+ var TRANSFORM_OPTION = /^[a-z]{1,3}_[\w.-]*$/i;
2179
+ var FORMAT_PLACEHOLDER = "{format}";
2180
+ var PROXY_PARAMS = ["url", "href", "src", "image"];
2181
+ var ABSOLUTE_HTTP = /^https?:\/\//i;
2182
+ var SOURCE_URL_ATTRIBUTES = ["src", "data-src"];
2183
+ var SOURCE_SET_ATTRIBUTES = ["srcset", "data-srcset"];
2184
+ var ASSET_ATTR = "data-prepr-asset";
2185
+ var ASSET_SOURCE_ATTRIBUTES = [
2186
+ ...SOURCE_URL_ATTRIBUTES,
2187
+ ...SOURCE_SET_ATTRIBUTES
2188
+ ];
2189
+ var TAG_ATTRIBUTES = [
2190
+ "data-prepr-encoded",
2191
+ "data-prepr-id",
2192
+ "data-prepr-field",
2193
+ "data-prepr-entry-id",
2194
+ ASSET_ATTR
2195
+ ];
2196
+ function decodeSegment(segment) {
2197
+ try {
2198
+ return decodeURIComponent(segment);
2199
+ } catch {
2200
+ return segment;
2201
+ }
2202
+ }
2203
+ function isTransformSegment(segment) {
2204
+ return segment === FORMAT_PLACEHOLDER || segment.split(",").every((option) => TRANSFORM_OPTION.test(option));
2205
+ }
2206
+ function assetKey(url, depth = 0) {
2207
+ if (!url || url.length > MAX_URL_LENGTH) return null;
2208
+ let parsed;
2209
+ try {
2210
+ parsed = new URL(url, "https://base.invalid");
2211
+ } catch {
2212
+ return null;
2213
+ }
2214
+ if (parsed.protocol !== "https:" && parsed.protocol !== "http:") return null;
2215
+ if (depth === 0) {
2216
+ for (const name of PROXY_PARAMS) {
2217
+ const inner = parsed.searchParams.get(name);
2218
+ if (inner && ABSOLUTE_HTTP.test(inner)) return assetKey(inner, 1);
2219
+ }
2220
+ }
2221
+ const segments = parsed.pathname.split("/").filter(Boolean).map(decodeSegment);
2222
+ const file = segments.pop();
2223
+ if (!file) return null;
2224
+ const kept = segments.filter((segment) => !isTransformSegment(segment));
2225
+ kept.push(file);
2226
+ return kept.length >= MIN_KEY_SEGMENTS ? kept.join("/") : null;
2227
+ }
2228
+ function srcsetUrls(srcset) {
2229
+ const urls = [];
2230
+ let expectUrl = true;
2231
+ for (const token of srcset.split(/\s+/)) {
2232
+ if (!token) continue;
2233
+ if (!expectUrl) {
2234
+ const comma = token.indexOf(",");
2235
+ if (comma === -1) continue;
2236
+ expectUrl = true;
2237
+ const rest = token.slice(comma + 1);
2238
+ if (!rest) continue;
2239
+ urls.push(rest.replace(/,+$/, ""));
2240
+ expectUrl = rest.endsWith(",");
2241
+ continue;
2242
+ }
2243
+ const url = token.replace(/^,+|,+$/g, "");
2244
+ if (url) urls.push(url);
2245
+ expectUrl = token.endsWith(",");
2246
+ }
2247
+ return urls;
2248
+ }
2249
+ function createAssetIndex(assets) {
2250
+ const index = /* @__PURE__ */ new Map();
2251
+ for (const asset of assets) {
2252
+ const key = assetKey(asset.key);
2253
+ if (key && !index.has(key)) index.set(key, asset);
2254
+ }
2255
+ return index;
2256
+ }
2257
+ function urlsOf(img) {
2258
+ const urls = [];
2259
+ const collect = (el) => {
2260
+ for (const name of SOURCE_URL_ATTRIBUTES) {
2261
+ const value = el.getAttribute(name);
2262
+ if (value) urls.push(value);
2263
+ }
2264
+ for (const name of SOURCE_SET_ATTRIBUTES) {
2265
+ const value = el.getAttribute(name);
2266
+ if (value) urls.push(...srcsetUrls(value));
2267
+ }
2268
+ };
2269
+ collect(img);
2270
+ const picture = img.parentElement;
2271
+ if (picture?.tagName === "PICTURE") {
2272
+ for (const source of picture.children) {
2273
+ if (source.tagName === "SOURCE") collect(source);
2274
+ }
2275
+ }
2276
+ return urls;
2277
+ }
2278
+ function matchAsset(img, index, keys) {
2279
+ for (const url of urlsOf(img)) {
2280
+ let key = keys.get(url);
2281
+ if (key === void 0) {
2282
+ key = assetKey(url);
2283
+ keys.set(url, key);
2284
+ }
2285
+ const asset = key ? index.get(key) : void 0;
2286
+ if (asset) return asset;
2287
+ }
2288
+ return void 0;
2289
+ }
2290
+ function setIfChanged(el, name, value) {
2291
+ if (value === null) {
2292
+ if (el.hasAttribute(name)) el.removeAttribute(name);
2293
+ } else if (el.getAttribute(name) !== value) {
2294
+ el.setAttribute(name, value);
2295
+ }
2296
+ }
2297
+ function untagAssetImage(img) {
2298
+ for (const name of TAG_ATTRIBUTES) img.removeAttribute(name);
2299
+ }
2300
+ function tagAssetImages(index, itemId, keys) {
2301
+ for (const img of document.getElementsByTagName("img")) {
2302
+ const ours = img.hasAttribute(ASSET_ATTR);
2303
+ if (!ours && img.hasAttribute("data-prepr-encoded")) continue;
2304
+ const asset = index.size ? matchAsset(img, index, keys) : void 0;
2305
+ if (!asset) {
2306
+ if (ours) untagAssetImage(img);
2307
+ continue;
2308
+ }
2309
+ setIfChanged(img, "data-prepr-encoded", "");
2310
+ setIfChanged(img, ASSET_ATTR, "");
2311
+ setIfChanged(img, "data-prepr-id", itemId);
2312
+ setIfChanged(img, "data-prepr-field", asset.field);
2313
+ setIfChanged(img, "data-prepr-entry-id", asset.entryId ?? null);
2314
+ }
2315
+ }
2316
+ function untagAssetImages() {
2317
+ document.querySelectorAll(`img[${ASSET_ATTR}]`).forEach(untagAssetImage);
2318
+ }
2319
+
2320
+ // src/core/visual-editor/navigation-guard.ts
2321
+ var debug6 = createScopedLogger("navigation-guard");
2322
+ var trimSlash = (path) => path.length > 1 && path.endsWith("/") ? path.slice(0, -1) : path;
2323
+ function resolve(url) {
2324
+ try {
2325
+ return new URL(url, document.baseURI);
2326
+ } catch {
2327
+ return null;
2328
+ }
2329
+ }
2330
+ function leavesPage(url) {
2331
+ if (url.protocol !== "http:" && url.protocol !== "https:") return false;
2332
+ const here = window.location;
2333
+ return url.origin !== here.origin || trimSlash(url.pathname) !== trimSlash(here.pathname);
2334
+ }
2335
+ function leavingLinkHref(target) {
2336
+ if (!(target instanceof Element)) return null;
2337
+ const link = target.closest("a[href]");
2338
+ if (!link || link.hasAttribute("download")) return null;
2339
+ const linkTarget = link.getAttribute("target");
2340
+ if (linkTarget && linkTarget !== "_self") return null;
2341
+ const href = link.getAttribute("href") ?? "";
2342
+ if (href.startsWith("#")) return null;
2343
+ const url = resolve(href);
2344
+ return url && leavesPage(url) ? url.href : null;
2345
+ }
2346
+ function submitterAttr(submitter, attr) {
2347
+ if (!submitter?.hasAttribute(attr)) return null;
2348
+ if (attr === "formaction") return submitter.formAction;
2349
+ if (attr === "formmethod") return submitter.formMethod;
2350
+ return submitter.formTarget;
2351
+ }
2352
+ function submissionUrl(form, submitter) {
2353
+ const method = (submitterAttr(submitter, "formmethod") ?? form.method).toLowerCase();
2354
+ if (method === "dialog") return null;
2355
+ const target = submitterAttr(submitter, "formtarget") ?? form.target;
2356
+ if (target && target !== "_self") return null;
2357
+ const url = resolve(
2358
+ submitterAttr(submitter, "formaction") ?? (form.action || location.href)
2359
+ );
2360
+ if (!url) return null;
2361
+ if (method === "get") {
2362
+ let data;
2363
+ try {
2364
+ data = new FormData(form, submitter);
2365
+ } catch {
2366
+ data = new FormData(form);
2367
+ }
2368
+ const query = new URLSearchParams();
2369
+ data.forEach((value, key) => {
2370
+ query.append(key, typeof value === "string" ? value : value.name);
2371
+ });
2372
+ url.search = query.toString();
2373
+ }
2374
+ return url;
2375
+ }
2376
+ function createNavigationGuard(opts) {
2377
+ let active = false;
2378
+ let permitted = null;
2379
+ let pendingForm = null;
2380
+ let originalPush = null;
2381
+ let guardedPush = null;
2382
+ const isPermitted = (url) => permitted === url.href;
2383
+ const guarding = () => active && opts.showsEditedContent();
2384
+ const report = (href) => {
2385
+ debug6.log("blocked navigation to", href);
2386
+ opts.onBlocked(href);
2387
+ };
2388
+ const onSubmit = (e2) => {
2389
+ if (e2.defaultPrevented || !(e2.target instanceof HTMLFormElement)) return;
2390
+ if (!guarding()) return;
2391
+ const submitter = e2.submitter ?? null;
2392
+ const url = submissionUrl(e2.target, submitter);
2393
+ if (!url || !leavesPage(url) || isPermitted(url)) return;
2394
+ e2.preventDefault();
2395
+ pendingForm = { form: e2.target, submitter, href: url.href };
2396
+ report(url.href);
2397
+ };
2398
+ const onNavigate = (event) => {
2399
+ const e2 = event;
2400
+ if (e2.navigationType === "traverse" || e2.navigationType === "reload") {
2401
+ return;
2402
+ }
2403
+ if (!e2.cancelable || e2.hashChange || e2.downloadRequest !== null) return;
2404
+ if (!guarding()) return;
2405
+ if (e2.navigationType === "replace" && e2.destination.sameDocument) return;
2406
+ const url = resolve(e2.destination.url);
2407
+ if (!url || !leavesPage(url) || isPermitted(url)) return;
2408
+ e2.preventDefault();
2409
+ report(url.href);
2410
+ if (e2.destination.sameDocument) window.location.reload();
2411
+ };
2412
+ const navigation = () => window.navigation;
2413
+ function installPushGuard() {
2414
+ const original = history.pushState;
2415
+ const guard = function(data, unused, url) {
2416
+ if (guarding() && guardedPush === guard && url != null) {
2417
+ const target = resolve(url);
2418
+ if (target && leavesPage(target) && !isPermitted(target)) {
2419
+ report(target.href);
2420
+ window.location.reload();
2421
+ return;
2422
+ }
2423
+ }
2424
+ return original.call(this, data, unused, url);
2425
+ };
2426
+ originalPush = original;
2427
+ guardedPush = guard;
2428
+ history.pushState = guard;
2429
+ }
2430
+ function uninstallPushGuard() {
2431
+ if (history.pushState === guardedPush && originalPush) {
2432
+ history.pushState = originalPush;
2433
+ }
2434
+ originalPush = null;
2435
+ guardedPush = null;
2436
+ }
2437
+ return {
2438
+ start() {
2439
+ if (active) return;
2440
+ active = true;
2441
+ installPushGuard();
2442
+ window.addEventListener("submit", onSubmit);
2443
+ navigation()?.addEventListener("navigate", onNavigate);
2444
+ },
2445
+ stop() {
2446
+ if (!active) return;
2447
+ active = false;
2448
+ uninstallPushGuard();
2449
+ window.removeEventListener("submit", onSubmit);
2450
+ navigation()?.removeEventListener("navigate", onNavigate);
2451
+ pendingForm = null;
2452
+ },
2453
+ isActive: guarding,
2454
+ block(href) {
2455
+ report(href);
2456
+ },
2457
+ permit(href) {
2458
+ permitted = resolve(href)?.href ?? null;
2459
+ },
2460
+ allow(href) {
2461
+ const url = resolve(href);
2462
+ if (!url || url.protocol !== "http:" && url.protocol !== "https:") {
2463
+ debug6.warn("ignored navigation to unsupported URL");
2464
+ return;
2465
+ }
2466
+ permitted = url.href;
2467
+ const form = pendingForm;
2468
+ pendingForm = null;
2469
+ if (form && form.href === url.href && form.form.isConnected) {
2470
+ try {
2471
+ form.form.requestSubmit(form.submitter ?? void 0);
2472
+ } catch {
2473
+ form.form.requestSubmit();
2474
+ }
2475
+ return;
2476
+ }
2477
+ window.location.assign(url.href);
2478
+ }
2479
+ };
2480
+ }
2481
+
2482
+ // src/core/visual-editor/index.ts
2483
+ var debug7 = createScopedLogger("visual-editor");
2484
+ var ENCODED_SELECTOR2 = "[data-prepr-encoded]";
2485
+ var REFERENCE_ATTR = "data-prepr-reference";
2486
+ var REFERENCE_SELECTOR = `[${REFERENCE_ATTR}]`;
2487
+ var CURSOR_STYLE_MARKER = "data-prepr-visual-editor";
2488
+ var CURSOR_STYLES = `${ENCODED_SELECTOR2}{cursor:pointer}html[data-prepr-ve-suspended] ${ENCODED_SELECTOR2}{cursor:auto}`;
2489
+ var SUSPENDED_ATTR = "data-prepr-ve-suspended";
2490
+ var FIND_RETRY_MS = 150;
2491
+ var FIND_RETRIES = 6;
2492
+ var MAX_CACHED_KEYS = 2e3;
2493
+ var LAYOUT_MUTATIONS = {
2494
+ childList: true,
2495
+ subtree: true,
2496
+ characterData: true,
2497
+ attributes: true,
2498
+ attributeFilter: [
2499
+ "class",
2500
+ "style",
2501
+ "hidden",
2502
+ "open",
2503
+ "data-prepr-encoded",
2504
+ // An image swapped by a carousel or lazy loader can show another asset.
2505
+ ...ASSET_SOURCE_ATTRIBUTES
2506
+ ]
2507
+ };
2508
+ function scrollIntoFrame(el) {
2509
+ const rect = el.getBoundingClientRect();
2510
+ const offset = Math.max(0, (window.innerHeight - rect.height) / 2);
2511
+ window.scrollTo({
2512
+ top: Math.max(0, window.scrollY + rect.top - offset),
2513
+ behavior: "smooth"
2514
+ });
2515
+ }
2516
+ var isVisible = (el) => typeof el.checkVisibility === "function" ? el.checkVisibility() : el.getClientRects().length > 0;
2517
+ function createVisualEditor(opts) {
2518
+ const overlay = new VisualEditorOverlay();
2519
+ let running = false;
2520
+ let currentItemId = null;
2521
+ let locale = opts.locale ?? "en";
2522
+ let suspended = false;
2523
+ let pointerFrame = null;
2524
+ let lastPointerTarget = null;
2525
+ let lastPointerX = 0;
2526
+ let lastPointerY = 0;
2527
+ let mutationFrame = null;
2528
+ let mutationObserver = null;
2529
+ let selection = null;
2530
+ let findTimeout = null;
2531
+ let settlePendingFind = null;
2532
+ let guardEnabled = false;
2533
+ let assetIndex = /* @__PURE__ */ new Map();
2534
+ const assetKeys = /* @__PURE__ */ new Map();
2535
+ const guard = createNavigationGuard({
2536
+ onBlocked: (href) => opts.onNavigationBlocked?.(href),
2537
+ showsEditedContent: () => showsEditedContent()
2538
+ });
2539
+ const encodedFrom = (node) => node instanceof Element ? node.closest(ENCODED_SELECTOR2) : null;
2540
+ const labelFor = (el) => fieldLabel(el.getAttribute("data-prepr-field"), locale);
2541
+ let references = null;
2542
+ let ownComponents = /* @__PURE__ */ new Set();
2543
+ const isCurrentItem = (el) => {
2544
+ if (currentItemId === null) return true;
2545
+ const id = el.getAttribute("data-prepr-id");
2546
+ return !id || id === currentItemId || ownComponents.has(id);
2547
+ };
2548
+ const showsEditedContent = () => {
2549
+ for (const el of document.querySelectorAll(ENCODED_SELECTOR2)) {
2550
+ if (isCurrentItem(el) && isVisible(el)) return true;
2551
+ }
2552
+ return false;
2553
+ };
2554
+ const linkedIdOf = (el) => {
2555
+ if (!references) return null;
2556
+ const id = el.getAttribute("data-prepr-id") ?? "";
2557
+ if (references.has(id)) return id;
2558
+ let container = el.closest(REFERENCE_SELECTOR);
2559
+ while (container) {
2560
+ const linked = container.getAttribute(REFERENCE_ATTR) ?? "";
2561
+ if (references.has(linked)) return linked;
2562
+ container = container.parentElement?.closest(REFERENCE_SELECTOR) ?? null;
2563
+ }
2564
+ return null;
2565
+ };
2566
+ const itemKind = (el) => {
2567
+ if (isCurrentItem(el)) return "current";
2568
+ if (!references) return "other";
2569
+ return linkedIdOf(el) ? "referenced" : null;
2570
+ };
2571
+ const referenceFieldOf = (el) => {
2572
+ const linked = linkedIdOf(el);
2573
+ return linked ? references?.get(linked) : void 0;
2574
+ };
2575
+ const hoverLabel = (el, kind) => {
2576
+ if (kind === "other") {
2577
+ return `${labelFor(el)} \xB7 ${t("visualEditor.otherItem", locale)}`;
2578
+ }
2579
+ if (kind === "referenced") {
2580
+ const via = referenceFieldOf(el);
2581
+ const prefix = via ? `${fieldLabel(via, locale)} \u203A ` : "";
2582
+ return `${prefix}${labelFor(el)} \xB7 ${t("visualEditor.linkedItem", locale)}`;
2583
+ }
2584
+ return labelFor(el);
2585
+ };
2586
+ const setSuspended = (value) => {
2587
+ if (suspended === value) return;
2588
+ suspended = value;
2589
+ document.documentElement.toggleAttribute(SUSPENDED_ATTR, value);
2590
+ if (value) overlay.setHover(null, "");
2591
+ };
2592
+ const updateHover = () => {
2593
+ pointerFrame = null;
2594
+ overlay.schedule();
2595
+ if (suspended) return;
2596
+ const encoded = encodedFrom(lastPointerTarget);
2597
+ const target = encoded === overlay.getSelectedTarget() ? null : encoded;
2598
+ if (!target) {
2599
+ overlay.setHover(null, "");
2600
+ return;
2601
+ }
2602
+ const kind = itemKind(target);
2603
+ if (!kind) {
2604
+ overlay.setHover(null, "");
2605
+ return;
2606
+ }
2607
+ overlay.setHover(target, hoverLabel(target, kind), { kind });
2608
+ };
2609
+ const handlePointerMove = (e2) => {
2610
+ setSuspended(e2.altKey);
2611
+ lastPointerTarget = e2.target;
2612
+ lastPointerX = e2.clientX;
2613
+ lastPointerY = e2.clientY;
2614
+ if (pointerFrame === null) {
2615
+ pointerFrame = requestAnimationFrame(updateHover);
2616
+ }
2617
+ };
2618
+ const handlePointerLeave = () => {
2619
+ lastPointerTarget = null;
2620
+ overlay.setHover(null, "");
2621
+ };
2622
+ const handleClick = (e2) => {
2623
+ if (e2.altKey) {
2624
+ const href = leavingLinkHref(e2.target);
2625
+ if (href && guard.isActive()) guard.permit(href);
2626
+ return;
2627
+ }
2628
+ const encoded = encodedFrom(e2.target);
2629
+ const kind = encoded ? itemKind(encoded) : null;
2630
+ if (!encoded || !kind) {
2631
+ clearSelection();
2632
+ if (e2.ctrlKey || e2.metaKey || e2.shiftKey || !showsEditedContent()) return;
2633
+ const href = leavingLinkHref(e2.target);
2634
+ if (!href) return;
2635
+ e2.preventDefault();
2636
+ if (guard.isActive()) {
2637
+ e2.stopPropagation();
2638
+ guard.block(href);
2639
+ }
2640
+ return;
2641
+ }
2642
+ e2.preventDefault();
2643
+ e2.stopPropagation();
2644
+ const item = listItemIn(encoded, e2.target);
2645
+ if (kind === "current")
2646
+ select(item?.element ?? encoded, encoded, item?.index);
2647
+ const referenceField = kind === "referenced" ? referenceFieldOf(encoded) : void 0;
2648
+ const linkedId = kind === "referenced" ? linkedIdOf(encoded) : null;
2649
+ opts.onEdit({
2650
+ ...readEditPayload(encoded),
2651
+ ...linkedId && { id: linkedId },
2652
+ ...item && { block: item.index },
2653
+ ...referenceField && { referenceField }
2654
+ });
2655
+ };
2656
+ const handleKey = (e2) => {
2657
+ if (e2.key === "Alt") setSuspended(e2.type === "keydown");
2658
+ if (e2.type === "keydown" && e2.key === "Escape") {
2659
+ clearSelection();
2660
+ }
2661
+ };
2662
+ const handleBlur = () => setSuspended(false);
2663
+ const handleScroll = () => overlay.onScroll();
2664
+ const handleResize = () => overlay.schedule();
2665
+ const handleSettled = () => overlay.schedule();
2666
+ function listItemIn(encoded, target) {
2667
+ if (!(target instanceof Element)) return null;
2668
+ const li = target.closest("li");
2669
+ if (!li || !encoded.contains(li) || !li.parentElement) return null;
2670
+ const items = Array.from(li.parentElement.children).filter(
2671
+ (child) => child.tagName === "LI"
2672
+ );
2673
+ return { element: li, index: items.indexOf(li) };
2674
+ }
2675
+ function nthListItem(encoded, block) {
2676
+ const list = encoded.querySelector("ul, ol");
2677
+ if (!list) return null;
2678
+ const items = Array.from(list.children).filter(
2679
+ (child) => child.tagName === "LI"
2680
+ );
2681
+ return items[block] ?? null;
2682
+ }
2683
+ function select(el, field, block) {
2684
+ const entryId = field.getAttribute("data-prepr-entry-id");
2685
+ selection = {
2686
+ lookup: {
2687
+ field: field.getAttribute("data-prepr-field") ?? "",
2688
+ id: field.getAttribute("data-prepr-id") ?? void 0,
2689
+ ...entryId && { entryId },
2690
+ ...block !== void 0 && { block }
2691
+ },
2692
+ label: field
2693
+ };
2694
+ overlay.setSelected(el, labelFor(field));
2695
+ if (overlay.getHoverTarget() === el) overlay.setHover(null, "");
2696
+ }
2697
+ function clearSelection() {
2698
+ selection = null;
2699
+ overlay.setSelected(null, "");
2700
+ }
2701
+ function reselect() {
2702
+ if (!selection) return;
2703
+ const current = overlay.getSelectedTarget();
2704
+ if (current?.isConnected && selection.label.isConnected) return;
2705
+ const found = findFieldElement(selection.lookup);
2706
+ if (!found || !isCurrentItem(found)) {
2707
+ overlay.setSelected(null, "");
2708
+ return;
2709
+ }
2710
+ const { block } = selection.lookup;
2711
+ const el = block !== void 0 ? nthListItem(found, block) ?? found : found;
2712
+ select(el, found, block);
2713
+ }
2714
+ function tagAssets() {
2715
+ if (assetKeys.size > MAX_CACHED_KEYS) assetKeys.clear();
2716
+ tagAssetImages(assetIndex, currentItemId, assetKeys);
2717
+ }
2718
+ const handleLayoutFrame = () => {
2719
+ mutationFrame = null;
2720
+ if (assetIndex.size) tagAssets();
2721
+ reselect();
2722
+ const hovered = overlay.getHoverTarget();
2723
+ if (hovered && !hovered.isConnected) {
2724
+ lastPointerTarget = document.elementFromPoint(lastPointerX, lastPointerY);
2725
+ updateHover();
2726
+ return;
2727
+ }
2728
+ overlay.schedule();
2729
+ };
2730
+ const handleMutations = () => {
2731
+ if (mutationFrame !== null) return;
2732
+ if (!selection && !overlay.getHoverTarget() && !assetIndex.size) return;
2733
+ mutationFrame = requestAnimationFrame(handleLayoutFrame);
2734
+ };
2735
+ function injectCursorStyles() {
2736
+ if (document.querySelector(`style[${CURSOR_STYLE_MARKER}]`)) return;
2737
+ const style = document.createElement("style");
2738
+ style.setAttribute(CURSOR_STYLE_MARKER, "");
2739
+ style.textContent = CURSOR_STYLES;
2740
+ document.head.appendChild(style);
2741
+ }
2742
+ function start() {
2743
+ if (running) return;
2744
+ running = true;
2745
+ overlay.mount();
2746
+ injectCursorStyles();
2747
+ document.addEventListener("pointermove", handlePointerMove, {
2748
+ passive: true
2749
+ });
2750
+ document.documentElement.addEventListener(
2751
+ "pointerleave",
2752
+ handlePointerLeave
2753
+ );
2754
+ document.addEventListener("click", handleClick, { capture: true });
2755
+ window.addEventListener("keydown", handleKey);
2756
+ window.addEventListener("keyup", handleKey);
2757
+ window.addEventListener("blur", handleBlur);
2758
+ window.addEventListener("scroll", handleScroll, {
2759
+ capture: true,
2760
+ passive: true
2761
+ });
2762
+ window.addEventListener("resize", handleResize, { passive: true });
2763
+ document.addEventListener("transitionend", handleSettled, {
2764
+ capture: true,
2765
+ passive: true
2766
+ });
2767
+ document.addEventListener("animationend", handleSettled, {
2768
+ capture: true,
2769
+ passive: true
2770
+ });
2771
+ mutationObserver = new MutationObserver(handleMutations);
2772
+ mutationObserver.observe(document.body, LAYOUT_MUTATIONS);
2773
+ if (assetIndex.size) tagAssets();
2774
+ syncGuard();
2775
+ debug7.log("visual editor started");
2776
+ }
2777
+ function stop() {
2778
+ if (!running) return;
2779
+ running = false;
2780
+ if (pointerFrame !== null) cancelAnimationFrame(pointerFrame);
2781
+ if (mutationFrame !== null) cancelAnimationFrame(mutationFrame);
2782
+ mutationObserver?.disconnect();
2783
+ mutationObserver = null;
2784
+ cancelFind();
2785
+ pointerFrame = null;
2786
+ mutationFrame = null;
2787
+ selection = null;
2788
+ lastPointerTarget = null;
2789
+ setSuspended(false);
2790
+ document.removeEventListener("pointermove", handlePointerMove);
2791
+ document.documentElement.removeEventListener(
2792
+ "pointerleave",
2793
+ handlePointerLeave
2794
+ );
2795
+ document.removeEventListener("click", handleClick, { capture: true });
2796
+ window.removeEventListener("keydown", handleKey);
2797
+ window.removeEventListener("keyup", handleKey);
2798
+ window.removeEventListener("blur", handleBlur);
2799
+ window.removeEventListener("scroll", handleScroll, { capture: true });
2800
+ window.removeEventListener("resize", handleResize);
2801
+ document.removeEventListener("transitionend", handleSettled, {
2802
+ capture: true
2803
+ });
2804
+ document.removeEventListener("animationend", handleSettled, {
2805
+ capture: true
2806
+ });
2807
+ document.querySelectorAll(`style[${CURSOR_STYLE_MARKER}]`).forEach((node) => node.remove());
2808
+ overlay.unmount();
2809
+ untagAssetImages();
2810
+ guard.stop();
2811
+ debug7.log("visual editor stopped");
2812
+ }
2813
+ function cancelFind() {
2814
+ if (findTimeout) clearTimeout(findTimeout);
2815
+ findTimeout = null;
2816
+ settlePendingFind?.();
2817
+ settlePendingFind = null;
2818
+ }
2819
+ function focusField(lookup) {
2820
+ cancelFind();
2821
+ return new Promise((resolve2) => {
2822
+ let attempt = 0;
2823
+ const finish = (found) => {
2824
+ settlePendingFind = null;
2825
+ resolve2(found);
2826
+ };
2827
+ settlePendingFind = () => resolve2(false);
2828
+ const tryFind = () => {
2829
+ findTimeout = null;
2830
+ const found = findFieldElement(lookup);
2831
+ if (found) {
2832
+ const el = lookup.block !== void 0 ? nthListItem(found, lookup.block) ?? found : found;
2833
+ scrollIntoFrame(el);
2834
+ if (running && isCurrentItem(found)) select(el, found, lookup.block);
2835
+ finish(true);
2836
+ return;
2837
+ }
2838
+ if (++attempt >= FIND_RETRIES) {
2839
+ debug7.log("no element renders field", lookup.field);
2840
+ finish(false);
2841
+ return;
2842
+ }
2843
+ findTimeout = setTimeout(tryFind, FIND_RETRY_MS);
2844
+ };
2845
+ tryFind();
2846
+ });
2847
+ }
2848
+ function setCurrentItem(itemId, referenceList, componentIds) {
2849
+ currentItemId = itemId;
2850
+ if (referenceList) setReferences(referenceList, componentIds);
2851
+ else if (componentIds) ownComponents = new Set(componentIds);
2852
+ if (running && assetIndex.size) tagAssets();
2853
+ const selected = overlay.getSelectedTarget();
2854
+ const owner = selected ? encodedFrom(selected) : null;
2855
+ if (owner && !isCurrentItem(owner)) clearSelection();
2856
+ }
2857
+ function setAssets(assets) {
2858
+ const hadAssets = assetIndex.size > 0;
2859
+ assetIndex = createAssetIndex(assets);
2860
+ if (running && (hadAssets || assetIndex.size)) tagAssets();
2861
+ }
2862
+ function setReferences(referenceList, componentIds) {
2863
+ if (componentIds) ownComponents = new Set(componentIds);
2864
+ references = /* @__PURE__ */ new Map();
2865
+ for (const { id, field } of referenceList) {
2866
+ if (!references.has(id)) references.set(id, field);
2867
+ }
2868
+ }
2869
+ function setLocale(value) {
2870
+ locale = value;
2871
+ }
2872
+ function syncGuard() {
2873
+ if (running && guardEnabled) guard.start();
2874
+ else guard.stop();
2875
+ }
2876
+ function setNavigationGuard(enabled) {
2877
+ guardEnabled = enabled;
2878
+ syncGuard();
2879
+ }
2880
+ return {
2881
+ start,
2882
+ stop,
2883
+ setCurrentItem,
2884
+ setReferences,
2885
+ setAssets,
2886
+ setLocale,
2887
+ focusField,
2888
+ setNavigationGuard,
2889
+ allowNavigation: (href) => guard.allow(href)
2890
+ };
2891
+ }
2892
+
2175
2893
  // src/core/create-preview.ts
2176
- var debug9 = createScopedLogger("create-preview");
2894
+ var debug8 = createScopedLogger("create-preview");
2177
2895
  var controllerStores = /* @__PURE__ */ new WeakMap();
2178
2896
  function defaultNavigation() {
2179
2897
  return {
@@ -2215,7 +2933,7 @@ function createPreprPreview(opts = {}) {
2215
2933
  const search = new URLSearchParams(window.location.search);
2216
2934
  const hideBar = search.get(PARAM_HIDE_BAR) === "true";
2217
2935
  if (hideBar) {
2218
- debug9.log(`${PARAM_HIDE_BAR}=true \u2014 no visible toolbar`);
2936
+ debug8.log(`${PARAM_HIDE_BAR}=true \u2014 no visible toolbar`);
2219
2937
  }
2220
2938
  const mountUi = (options?.ui ?? true) && !isIframe && !hideBar;
2221
2939
  const locale = resolveLocale(options);
@@ -2241,32 +2959,48 @@ function createPreprPreview(opts = {}) {
2241
2959
  definePreprToolbar();
2242
2960
  el = document.createElement("prepr-toolbar");
2243
2961
  document.body.appendChild(el);
2244
- el.connect(store, (key) => t2(key, locale));
2962
+ el.connect(store, (key) => t(key, locale));
2245
2963
  }
2246
2964
  const editingEnabled = features.editMode || isIframe;
2247
- const stega = createStegaController({
2248
- // The CMS deep-link tooltip is chrome, so it follows the same decision as
2249
- // the bar: noise inside the editor (clicking the element itself requests
2250
- // the edit there), and unwanted in a headless preview that asked for no UI.
2251
- tooltip: mountUi,
2252
- // In the editor, ask the parent to focus the field instead of opening a new
2253
- // tab. Standalone previews keep the window.open behaviour.
2254
- onEdit: ({ href, id, field, locale: contentLocale }) => {
2255
- const safeHref = href ? safeEditUrl(href) : null;
2256
- if (href && !safeHref) {
2257
- debug9.warn("ignored edit request with unsupported href scheme");
2258
- return;
2259
- }
2260
- if (isIframe) {
2261
- sendPreprEvent("field_edit_requested", {
2262
- id,
2263
- field,
2264
- locale: contentLocale
2265
- });
2266
- } else if (safeHref) {
2267
- window.open(safeHref, "_blank", "noopener,noreferrer");
2268
- }
2965
+ const onEdit = ({
2966
+ href,
2967
+ id,
2968
+ field,
2969
+ locale: contentLocale,
2970
+ block,
2971
+ entryId,
2972
+ referenceField
2973
+ }) => {
2974
+ const safeHref = href ? safeEditUrl(href) : null;
2975
+ if (href && !safeHref) {
2976
+ debug8.warn("ignored edit request with unsupported href scheme");
2977
+ return;
2269
2978
  }
2979
+ if (isIframe) {
2980
+ sendPreprEvent("field_edit_requested", {
2981
+ id,
2982
+ field,
2983
+ locale: contentLocale,
2984
+ ...block !== void 0 && { block },
2985
+ ...entryId && { entryId },
2986
+ ...referenceField && { referenceField }
2987
+ });
2988
+ } else if (safeHref) {
2989
+ window.open(safeHref, "_blank", "noopener,noreferrer");
2990
+ }
2991
+ };
2992
+ const visualEditor = isIframe ? createVisualEditor({
2993
+ onEdit,
2994
+ locale,
2995
+ onNavigationBlocked: (href) => sendPreprEvent("navigation_blocked", { href })
2996
+ }) : null;
2997
+ const stega = visualEditor ?? createStegaController({
2998
+ // The edit-mode marks and hover label are chrome, so they follow the
2999
+ // same decision as the bar: unwanted in a headless preview that asked
3000
+ // for no UI.
3001
+ tooltip: mountUi,
3002
+ onEdit,
3003
+ locale
2270
3004
  });
2271
3005
  const autoClean = createStegaAutoClean({
2272
3006
  enabled: options?.autoClean
@@ -2289,12 +3023,32 @@ function createPreprPreview(opts = {}) {
2289
3023
  });
2290
3024
  const bridge = createIframeBridge(store, {
2291
3025
  allowedEditorOrigins: options?.allowedEditorOrigins,
3026
+ onHandshake: ({
3027
+ itemId,
3028
+ referencedIds,
3029
+ componentIds,
3030
+ uiLocale,
3031
+ navigationGuard,
3032
+ assets
3033
+ }) => {
3034
+ if (uiLocale) visualEditor?.setLocale(uiLocale);
3035
+ visualEditor?.setCurrentItem(itemId ?? null, referencedIds, componentIds);
3036
+ visualEditor?.setNavigationGuard(navigationGuard === true);
3037
+ if (assets) visualEditor?.setAssets(assets);
3038
+ },
3039
+ onAssets: (assets) => visualEditor?.setAssets(assets),
3040
+ onAllowNavigation: (href) => visualEditor?.allowNavigation(href),
3041
+ onReferences: (referencedIds, componentIds) => visualEditor?.setReferences(referencedIds, componentIds),
2292
3042
  // The editor selecting a field scrolls the matching element into view. The
2293
3043
  // lookup relies on the attributes the stega scan writes, so it finds
2294
3044
  // nothing until encoded content has been tagged.
2295
- onScrollToField: ({ field, id }) => {
3045
+ onScrollToField: ({ field, id, block, entryId, fieldById }) => {
3046
+ if (visualEditor) {
3047
+ void visualEditor.focusField({ field, id, block, entryId, fieldById });
3048
+ return;
3049
+ }
2296
3050
  if (!scrollToField({ field, id })) {
2297
- debug9.log("no element on this page renders field", field);
3051
+ debug8.log("no element on this page renders field", field);
2298
3052
  }
2299
3053
  }
2300
3054
  });
@@ -2316,7 +3070,7 @@ function createPreprPreview(opts = {}) {
2316
3070
  bridge.stop();
2317
3071
  }
2318
3072
  el?.remove();
2319
- debug9.log("preview destroyed");
3073
+ debug8.log("preview destroyed");
2320
3074
  }
2321
3075
  const controller = { destroy };
2322
3076
  controllerStores.set(controller, store);
@@ -2389,8 +3143,6 @@ function setTrackingParam(key, value) {
2389
3143
  }
2390
3144
 
2391
3145
  export {
2392
- VERSION,
2393
- resolveFeatures,
2394
3146
  COOKIE_PREVIEW_MODE,
2395
3147
  COOKIE_SEGMENT,
2396
3148
  COOKIE_VARIANT,
@@ -2400,6 +3152,7 @@ export {
2400
3152
  PARAM_VARIANT,
2401
3153
  PARAM_HIDE_BAR,
2402
3154
  ONE_YEAR_SECONDS,
3155
+ HEADER_LIVE_RENDER,
2403
3156
  stegaClean,
2404
3157
  createToolbarStore,
2405
3158
  PreprToolbarElement,