@preprio/toolkit 0.4.2 → 0.5.0-rc.1

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.
@@ -30,7 +30,7 @@ __export(astro_exports, {
30
30
  module.exports = __toCommonJS(astro_exports);
31
31
 
32
32
  // src/version.ts
33
- var VERSION = "0.4.2";
33
+ var VERSION = "0.5.0-rc.1";
34
34
 
35
35
  // src/core/features.ts
36
36
  function isEnabled(config) {
@@ -83,7 +83,7 @@ var PREPR_HEADER_KEYS = [
83
83
  "Prepr-Visitor-IP",
84
84
  "Prepr-Hubspot-Id",
85
85
  "Prepr-Customer-Id-Created",
86
- "Prepr-User-Agent",
86
+ "User-Agent",
87
87
  "Prepr-Package"
88
88
  ];
89
89
  function getPreprHeadersFromHeaders(headers) {
@@ -272,7 +272,7 @@ function processFrameworkRequest(request, opts, framework) {
272
272
  }
273
273
  const userAgent = request.headers.get("user-agent");
274
274
  if (userAgent) {
275
- requestHeaders.set("Prepr-User-Agent", sanitizeHeaderValue(userAgent));
275
+ requestHeaders.set("User-Agent", sanitizeHeaderValue(userAgent));
276
276
  }
277
277
  const packageParts = [`@preprio/toolkit@${opts?.version ?? VERSION}`];
278
278
  if (framework) packageParts.push(`framework=${framework}`);
@@ -59,6 +59,9 @@ interface PreprHeaders {
59
59
  readonly 'Prepr-Visitor-IP'?: string;
60
60
  readonly 'Prepr-Hubspot-Id'?: string;
61
61
  readonly 'Prepr-Customer-Id-Created'?: 'true';
62
+ /** The visitor's browser User-Agent, used by the API for device detection. */
63
+ readonly 'User-Agent'?: string;
64
+ /** @deprecated Never populated; the visitor's value is under `User-Agent`. */
62
65
  readonly 'Prepr-User-Agent'?: string;
63
66
  readonly 'Prepr-Package'?: string;
64
67
  }
@@ -59,6 +59,9 @@ interface PreprHeaders {
59
59
  readonly 'Prepr-Visitor-IP'?: string;
60
60
  readonly 'Prepr-Hubspot-Id'?: string;
61
61
  readonly 'Prepr-Customer-Id-Created'?: 'true';
62
+ /** The visitor's browser User-Agent, used by the API for device detection. */
63
+ readonly 'User-Agent'?: string;
64
+ /** @deprecated Never populated; the visitor's value is under `User-Agent`. */
62
65
  readonly 'Prepr-User-Agent'?: string;
63
66
  readonly 'Prepr-Package'?: string;
64
67
  }
@@ -1,5 +1,5 @@
1
1
  // src/version.ts
2
- var VERSION = "0.4.2";
2
+ var VERSION = "0.5.0-rc.1";
3
3
 
4
4
  // src/core/features.ts
5
5
  function isEnabled(config) {
@@ -52,7 +52,7 @@ var PREPR_HEADER_KEYS = [
52
52
  "Prepr-Visitor-IP",
53
53
  "Prepr-Hubspot-Id",
54
54
  "Prepr-Customer-Id-Created",
55
- "Prepr-User-Agent",
55
+ "User-Agent",
56
56
  "Prepr-Package"
57
57
  ];
58
58
  function getPreprHeadersFromHeaders(headers) {
@@ -241,7 +241,7 @@ function processFrameworkRequest(request, opts, framework) {
241
241
  }
242
242
  const userAgent = request.headers.get("user-agent");
243
243
  if (userAgent) {
244
- requestHeaders.set("Prepr-User-Agent", sanitizeHeaderValue(userAgent));
244
+ requestHeaders.set("User-Agent", sanitizeHeaderValue(userAgent));
245
245
  }
246
246
  const packageParts = [`@preprio/toolkit@${opts?.version ?? VERSION}`];
247
247
  if (framework) packageParts.push(`framework=${framework}`);
@@ -1,5 +1,5 @@
1
1
  // src/version.ts
2
- var VERSION = "0.4.2";
2
+ var VERSION = "0.5.0-rc.1";
3
3
 
4
4
  // src/core/features.ts
5
5
  function isEnabled(config) {
@@ -186,10 +186,33 @@ function isRenderedElement(element) {
186
186
  if (!style) return true;
187
187
  return style.visibility !== "hidden" && style.display !== "none";
188
188
  }
189
+ function parseEditHref(href) {
190
+ try {
191
+ const url = new URL(href, window.location.origin);
192
+ return {
193
+ id: url.pathname.split("/").pop() || void 0,
194
+ field: url.searchParams.get("field") || void 0,
195
+ locale: url.searchParams.get("locale") || void 0
196
+ };
197
+ } catch {
198
+ return {};
199
+ }
200
+ }
201
+ function setOrRemove(element, name, value) {
202
+ if (value) {
203
+ element.setAttribute(name, value);
204
+ } else {
205
+ element.removeAttribute(name);
206
+ }
207
+ }
189
208
  function tagEncodedElement(element, decoded) {
190
209
  element.setAttribute("data-prepr-encoded", "");
191
210
  element.setAttribute("data-prepr-href", decoded.href);
192
211
  element.setAttribute("data-prepr-origin", decoded.origin);
212
+ const { id, field, locale } = parseEditHref(decoded.href);
213
+ setOrRemove(element, "data-prepr-id", id);
214
+ setOrRemove(element, "data-prepr-field", field);
215
+ setOrRemove(element, "data-prepr-locale", locale);
193
216
  }
194
217
 
195
218
  // src/core/stega/elements.ts
@@ -294,6 +317,9 @@ var StegaElements = class {
294
317
  element.removeAttribute("data-prepr-encoded");
295
318
  element.removeAttribute("data-prepr-href");
296
319
  element.removeAttribute("data-prepr-origin");
320
+ element.removeAttribute("data-prepr-id");
321
+ element.removeAttribute("data-prepr-field");
322
+ element.removeAttribute("data-prepr-locale");
297
323
  element.classList.remove("prepr-overlay-active");
298
324
  });
299
325
  this.elements = void 0;
@@ -346,6 +372,7 @@ var StegaOverlay = class {
346
372
  const origin = element.getAttribute("data-prepr-origin");
347
373
  const id = element.getAttribute("data-prepr-id") ?? void 0;
348
374
  const field = element.getAttribute("data-prepr-field") ?? void 0;
375
+ const locale = element.getAttribute("data-prepr-locale") ?? void 0;
349
376
  debug3.log("showing overlay", { href, origin });
350
377
  const overlay = this.overlay;
351
378
  overlay.style.display = "block";
@@ -378,7 +405,7 @@ var StegaOverlay = class {
378
405
  tooltip.style.left = `${left}px`;
379
406
  });
380
407
  tooltip.onclick = () => {
381
- this.onEdit({ href, origin, id, field });
408
+ this.onEdit({ href, origin, id, field, locale });
382
409
  };
383
410
  }
384
411
  this.currentElement = element;
@@ -558,7 +585,8 @@ function createStegaController(opts) {
558
585
  href: encoded.getAttribute("data-prepr-href") ?? "",
559
586
  origin: encoded.getAttribute("data-prepr-origin") ?? "",
560
587
  id: encoded.getAttribute("data-prepr-id") ?? void 0,
561
- field: encoded.getAttribute("data-prepr-field") ?? void 0
588
+ field: encoded.getAttribute("data-prepr-field") ?? void 0,
589
+ locale: encoded.getAttribute("data-prepr-locale") ?? void 0
562
590
  });
563
591
  };
564
592
  const start = () => {
@@ -788,6 +816,38 @@ function createStegaAutoClean(options) {
788
816
  return { start, stop };
789
817
  }
790
818
 
819
+ // src/core/stega/scroll-to-field.ts
820
+ var debug7 = createScopedLogger("stega:scroll");
821
+ var FLASH_DURATION_MS = 1200;
822
+ var flashTimeout = null;
823
+ function scrollToField(request) {
824
+ const { field, id } = request;
825
+ if (!field) return false;
826
+ const candidates = document.querySelectorAll(
827
+ "[data-prepr-encoded]"
828
+ );
829
+ let target = null;
830
+ for (const candidate of candidates) {
831
+ if (candidate.getAttribute("data-prepr-field") !== field) continue;
832
+ if (id && candidate.getAttribute("data-prepr-id") !== id) continue;
833
+ target = candidate;
834
+ break;
835
+ }
836
+ if (!target) {
837
+ debug7.log("no element found for field", field);
838
+ return false;
839
+ }
840
+ target.scrollIntoView({ behavior: "smooth", block: "center" });
841
+ if (flashTimeout) clearTimeout(flashTimeout);
842
+ document.querySelectorAll(".prepr-overlay-active").forEach((el) => el.classList.remove("prepr-overlay-active"));
843
+ target.classList.add("prepr-overlay-active");
844
+ flashTimeout = setTimeout(() => {
845
+ target.classList.remove("prepr-overlay-active");
846
+ flashTimeout = null;
847
+ }, FLASH_DURATION_MS);
848
+ return true;
849
+ }
850
+
791
851
  // src/core/store.ts
792
852
  var DEFAULT_STATE = {
793
853
  locale: "en",
@@ -1944,6 +2004,7 @@ function t2(key, locale, vars) {
1944
2004
  }
1945
2005
 
1946
2006
  // src/core/iframe-bridge.ts
2007
+ var debug8 = createScopedLogger("iframe-bridge");
1947
2008
  var EDITOR_BASE_DOMAIN = "prepr.io";
1948
2009
  function isAllowedEditorOrigin(origin, allowed) {
1949
2010
  if (allowed) return allowed.includes(origin);
@@ -1961,6 +2022,7 @@ function isAllowedEditorOrigin(origin, allowed) {
1961
2022
  }
1962
2023
  function createIframeBridge(store, options = {}) {
1963
2024
  const allowedOrigins = options.allowedEditorOrigins;
2025
+ const onScrollToField = options.onScrollToField;
1964
2026
  let parentOrigin = null;
1965
2027
  const onKeyDown = (event) => {
1966
2028
  const key = event.key.toLowerCase();
@@ -1984,6 +2046,15 @@ function createIframeBridge(store, options = {}) {
1984
2046
  const currentScrollY = window.scrollY || document.documentElement.scrollTop;
1985
2047
  sendPreprEvent("getScrollPosition", { value: currentScrollY });
1986
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
+ }
1987
2058
  };
1988
2059
  return {
1989
2060
  start() {
@@ -2102,7 +2173,7 @@ function splitPath(full) {
2102
2173
  }
2103
2174
 
2104
2175
  // src/core/create-preview.ts
2105
- var debug7 = createScopedLogger("create-preview");
2176
+ var debug9 = createScopedLogger("create-preview");
2106
2177
  var controllerStores = /* @__PURE__ */ new WeakMap();
2107
2178
  function defaultNavigation() {
2108
2179
  return {
@@ -2144,7 +2215,7 @@ function createPreprPreview(opts = {}) {
2144
2215
  const search = new URLSearchParams(window.location.search);
2145
2216
  const hideBar = search.get(PARAM_HIDE_BAR) === "true";
2146
2217
  if (hideBar) {
2147
- debug7.log(`${PARAM_HIDE_BAR}=true \u2014 no visible toolbar`);
2218
+ debug9.log(`${PARAM_HIDE_BAR}=true \u2014 no visible toolbar`);
2148
2219
  }
2149
2220
  const mountUi = (options?.ui ?? true) && !isIframe && !hideBar;
2150
2221
  const locale = resolveLocale(options);
@@ -2180,18 +2251,17 @@ function createPreprPreview(opts = {}) {
2180
2251
  tooltip: mountUi,
2181
2252
  // In the editor, ask the parent to focus the field instead of opening a new
2182
2253
  // tab. Standalone previews keep the window.open behaviour.
2183
- onEdit: ({ href, origin, id, field }) => {
2254
+ onEdit: ({ href, id, field, locale: contentLocale }) => {
2184
2255
  const safeHref = href ? safeEditUrl(href) : null;
2185
2256
  if (href && !safeHref) {
2186
- debug7.warn("ignored edit request with unsupported href scheme");
2257
+ debug9.warn("ignored edit request with unsupported href scheme");
2187
2258
  return;
2188
2259
  }
2189
2260
  if (isIframe) {
2190
2261
  sendPreprEvent("field_edit_requested", {
2191
- href: safeHref ?? void 0,
2192
- origin,
2193
2262
  id,
2194
- field
2263
+ field,
2264
+ locale: contentLocale
2195
2265
  });
2196
2266
  } else if (safeHref) {
2197
2267
  window.open(safeHref, "_blank", "noopener,noreferrer");
@@ -2218,7 +2288,15 @@ function createPreprPreview(opts = {}) {
2218
2288
  handleChange(before, state);
2219
2289
  });
2220
2290
  const bridge = createIframeBridge(store, {
2221
- allowedEditorOrigins: options?.allowedEditorOrigins
2291
+ allowedEditorOrigins: options?.allowedEditorOrigins,
2292
+ // The editor selecting a field scrolls the matching element into view. The
2293
+ // lookup relies on the attributes the stega scan writes, so it finds
2294
+ // nothing until encoded content has been tagged.
2295
+ onScrollToField: ({ field, id }) => {
2296
+ if (!scrollToField({ field, id })) {
2297
+ debug9.log("no element on this page renders field", field);
2298
+ }
2299
+ }
2222
2300
  });
2223
2301
  sendPreprEvent(
2224
2302
  "getScrollPosition",
@@ -2238,7 +2316,7 @@ function createPreprPreview(opts = {}) {
2238
2316
  bridge.stop();
2239
2317
  }
2240
2318
  el?.remove();
2241
- debug7.log("preview destroyed");
2319
+ debug9.log("preview destroyed");
2242
2320
  }
2243
2321
  const controller = { destroy };
2244
2322
  controllerStores.set(controller, store);
package/dist/index.cjs CHANGED
@@ -44,7 +44,7 @@ __export(src_exports, {
44
44
  module.exports = __toCommonJS(src_exports);
45
45
 
46
46
  // src/version.ts
47
- var VERSION = "0.4.2";
47
+ var VERSION = "0.5.0-rc.1";
48
48
 
49
49
  // src/core/features.ts
50
50
  function isEnabled(config) {
@@ -312,6 +312,7 @@ function createElementCache(query, ttl = 1e3) {
312
312
  }
313
313
 
314
314
  // src/core/iframe-bridge.ts
315
+ var debug = createScopedLogger("iframe-bridge");
315
316
  var EDITOR_BASE_DOMAIN = "prepr.io";
316
317
  function isAllowedEditorOrigin(origin, allowed) {
317
318
  if (allowed) return allowed.includes(origin);
@@ -329,6 +330,7 @@ function isAllowedEditorOrigin(origin, allowed) {
329
330
  }
330
331
  function createIframeBridge(store, options = {}) {
331
332
  const allowedOrigins = options.allowedEditorOrigins;
333
+ const onScrollToField = options.onScrollToField;
332
334
  let parentOrigin = null;
333
335
  const onKeyDown = (event) => {
334
336
  const key = event.key.toLowerCase();
@@ -352,6 +354,15 @@ function createIframeBridge(store, options = {}) {
352
354
  const currentScrollY = window.scrollY || document.documentElement.scrollTop;
353
355
  sendPreprEvent("getScrollPosition", { value: currentScrollY });
354
356
  }
357
+ if (data?.event === "prepr:scrollToField") {
358
+ if (typeof data.field !== "string" || !data.field) return;
359
+ if (data.id !== void 0 && typeof data.id !== "string") return;
360
+ try {
361
+ onScrollToField?.({ field: data.field, id: data.id });
362
+ } catch (error) {
363
+ debug.warn("scrollToField handler failed", error);
364
+ }
365
+ }
355
366
  };
356
367
  return {
357
368
  start() {
@@ -376,7 +387,7 @@ var import_stega2 = require("@vercel/stega");
376
387
 
377
388
  // src/core/stega/clean.ts
378
389
  var import_stega = require("@vercel/stega");
379
- var debug = createScopedLogger("stega:clean");
390
+ var debug2 = createScopedLogger("stega:clean");
380
391
  function stegaClean(text) {
381
392
  if (!text) return text;
382
393
  return (0, import_stega.vercelStegaClean)(text);
@@ -389,7 +400,7 @@ function decodeStega(str) {
389
400
  return decoded;
390
401
  }
391
402
  } catch (error) {
392
- debug.log("error decoding stega data", error);
403
+ debug2.log("error decoding stega data", error);
393
404
  const match = str.match(/{"origin.*?}/);
394
405
  if (match) {
395
406
  try {
@@ -398,7 +409,7 @@ function decodeStega(str) {
398
409
  return decodedMatch;
399
410
  }
400
411
  } catch (innerError) {
401
- debug.log("error decoding stega regex match", innerError);
412
+ debug2.log("error decoding stega regex match", innerError);
402
413
  }
403
414
  }
404
415
  }
@@ -434,14 +445,37 @@ function isRenderedElement(element) {
434
445
  if (!style) return true;
435
446
  return style.visibility !== "hidden" && style.display !== "none";
436
447
  }
448
+ function parseEditHref(href) {
449
+ try {
450
+ const url = new URL(href, window.location.origin);
451
+ return {
452
+ id: url.pathname.split("/").pop() || void 0,
453
+ field: url.searchParams.get("field") || void 0,
454
+ locale: url.searchParams.get("locale") || void 0
455
+ };
456
+ } catch {
457
+ return {};
458
+ }
459
+ }
460
+ function setOrRemove(element, name, value) {
461
+ if (value) {
462
+ element.setAttribute(name, value);
463
+ } else {
464
+ element.removeAttribute(name);
465
+ }
466
+ }
437
467
  function tagEncodedElement(element, decoded) {
438
468
  element.setAttribute("data-prepr-encoded", "");
439
469
  element.setAttribute("data-prepr-href", decoded.href);
440
470
  element.setAttribute("data-prepr-origin", decoded.origin);
471
+ const { id, field, locale } = parseEditHref(decoded.href);
472
+ setOrRemove(element, "data-prepr-id", id);
473
+ setOrRemove(element, "data-prepr-field", field);
474
+ setOrRemove(element, "data-prepr-locale", locale);
441
475
  }
442
476
 
443
477
  // src/core/stega/auto-clean.ts
444
- var debug2 = createScopedLogger("stega:auto-clean");
478
+ var debug3 = createScopedLogger("stega:auto-clean");
445
479
  function createStegaAutoClean(options) {
446
480
  const stripText = options?.enabled ?? true;
447
481
  let observer = null;
@@ -484,7 +518,7 @@ function createStegaAutoClean(options) {
484
518
  }
485
519
  pendingHidden.add(textNode);
486
520
  } catch (error) {
487
- debug2.log("error cleaning text node", error);
521
+ debug3.log("error cleaning text node", error);
488
522
  }
489
523
  return false;
490
524
  }
@@ -570,7 +604,7 @@ function createStegaAutoClean(options) {
570
604
  if (observer || typeof window === "undefined") return;
571
605
  scanAndClean();
572
606
  setupObserver();
573
- debug2.log("stega auto-clean started");
607
+ debug3.log("stega auto-clean started");
574
608
  }
575
609
  function stop() {
576
610
  if (debounceTimeout) {
@@ -584,13 +618,13 @@ function createStegaAutoClean(options) {
584
618
  cleaned = /* @__PURE__ */ new WeakMap();
585
619
  pendingHidden = /* @__PURE__ */ new Set();
586
620
  pendingDirty = /* @__PURE__ */ new Set();
587
- debug2.log("stega auto-clean stopped");
621
+ debug3.log("stega auto-clean stopped");
588
622
  }
589
623
  return { start, stop };
590
624
  }
591
625
 
592
626
  // src/core/stega/elements.ts
593
- var debug3 = createScopedLogger("stega:elements");
627
+ var debug4 = createScopedLogger("stega:elements");
594
628
  var StegaElements = class {
595
629
  constructor(decode = decodeStega) {
596
630
  this.observer = null;
@@ -635,7 +669,7 @@ var StegaElements = class {
635
669
  walkTextNodes(document.body, (textNode) => {
636
670
  if (this.tagFromTextNode(textNode)) encodedCount++;
637
671
  });
638
- debug3.log("document scan complete, encoded", encodedCount, "elements");
672
+ debug4.log("document scan complete, encoded", encodedCount, "elements");
639
673
  this.elements = document.querySelectorAll("[data-prepr-encoded]");
640
674
  }
641
675
  /** `onUpdate` fires after each debounced batch, for refreshing derived state. */
@@ -691,6 +725,9 @@ var StegaElements = class {
691
725
  element.removeAttribute("data-prepr-encoded");
692
726
  element.removeAttribute("data-prepr-href");
693
727
  element.removeAttribute("data-prepr-origin");
728
+ element.removeAttribute("data-prepr-id");
729
+ element.removeAttribute("data-prepr-field");
730
+ element.removeAttribute("data-prepr-locale");
694
731
  element.classList.remove("prepr-overlay-active");
695
732
  });
696
733
  this.elements = void 0;
@@ -698,7 +735,7 @@ var StegaElements = class {
698
735
  };
699
736
 
700
737
  // src/core/stega/overlay.ts
701
- var debug4 = createScopedLogger("stega:overlay");
738
+ var debug5 = createScopedLogger("stega:overlay");
702
739
  var StegaOverlay = class {
703
740
  constructor(onEdit) {
704
741
  this.overlay = null;
@@ -743,7 +780,8 @@ var StegaOverlay = class {
743
780
  const origin = element.getAttribute("data-prepr-origin");
744
781
  const id = element.getAttribute("data-prepr-id") ?? void 0;
745
782
  const field = element.getAttribute("data-prepr-field") ?? void 0;
746
- debug4.log("showing overlay", { href, origin });
783
+ const locale = element.getAttribute("data-prepr-locale") ?? void 0;
784
+ debug5.log("showing overlay", { href, origin });
747
785
  const overlay = this.overlay;
748
786
  overlay.style.display = "block";
749
787
  overlay.style.top = `${rect.top + window.scrollY - 2}px`;
@@ -775,7 +813,7 @@ var StegaOverlay = class {
775
813
  tooltip.style.left = `${left}px`;
776
814
  });
777
815
  tooltip.onclick = () => {
778
- this.onEdit({ href, origin, id, field });
816
+ this.onEdit({ href, origin, id, field, locale });
779
817
  };
780
818
  }
781
819
  this.currentElement = element;
@@ -815,7 +853,7 @@ var StegaOverlay = class {
815
853
  };
816
854
 
817
855
  // src/core/stega/proximity.ts
818
- var debug5 = createScopedLogger("stega:proximity");
856
+ var debug6 = createScopedLogger("stega:proximity");
819
857
  var StegaProximity = class {
820
858
  constructor() {
821
859
  this.highlighted = /* @__PURE__ */ new Set();
@@ -850,9 +888,9 @@ var StegaProximity = class {
850
888
  );
851
889
  const nodes = this.getEncodedElements();
852
890
  nodes.forEach((el) => this.observer.observe(el));
853
- debug5.log("observing", nodes.length, "encoded elements");
891
+ debug6.log("observing", nodes.length, "encoded elements");
854
892
  } catch (error) {
855
- debug5.log("error setting up IntersectionObserver", error);
893
+ debug6.log("error setting up IntersectionObserver", error);
856
894
  }
857
895
  }
858
896
  stopObserving() {
@@ -922,7 +960,7 @@ function removeStegaStyles() {
922
960
  }
923
961
 
924
962
  // src/core/stega/scan.ts
925
- var debug6 = createScopedLogger("stega:scan");
963
+ var debug7 = createScopedLogger("stega:scan");
926
964
  var EDITOR_MODE_STYLES = "[data-prepr-encoded]{cursor:pointer}";
927
965
  function createStegaController(opts) {
928
966
  const tooltipEnabled = opts.tooltip ?? true;
@@ -955,12 +993,13 @@ function createStegaController(opts) {
955
993
  href: encoded.getAttribute("data-prepr-href") ?? "",
956
994
  origin: encoded.getAttribute("data-prepr-origin") ?? "",
957
995
  id: encoded.getAttribute("data-prepr-id") ?? void 0,
958
- field: encoded.getAttribute("data-prepr-field") ?? void 0
996
+ field: encoded.getAttribute("data-prepr-field") ?? void 0,
997
+ locale: encoded.getAttribute("data-prepr-locale") ?? void 0
959
998
  });
960
999
  };
961
1000
  const start = () => {
962
1001
  if (initialized) {
963
- debug6.log("already initialized, skipping setup");
1002
+ debug7.log("already initialized, skipping setup");
964
1003
  return;
965
1004
  }
966
1005
  if (!tooltipEnabled) {
@@ -970,7 +1009,7 @@ function createStegaController(opts) {
970
1009
  });
971
1010
  document.addEventListener("click", handleClick, { capture: true });
972
1011
  initialized = true;
973
- debug6.log("stega controller started (editor mode)");
1012
+ debug7.log("stega controller started (editor mode)");
974
1013
  return;
975
1014
  }
976
1015
  injectStegaStyles();
@@ -1001,7 +1040,7 @@ function createStegaController(opts) {
1001
1040
  passive: true
1002
1041
  });
1003
1042
  initialized = true;
1004
- debug6.log("stega controller started");
1043
+ debug7.log("stega controller started");
1005
1044
  };
1006
1045
  const stop = () => {
1007
1046
  if (!initialized) return;
@@ -1010,7 +1049,7 @@ function createStegaController(opts) {
1010
1049
  elements.cleanup();
1011
1050
  removeStegaStyles();
1012
1051
  initialized = false;
1013
- debug6.log("stega controller stopped (editor mode)");
1052
+ debug7.log("stega controller stopped (editor mode)");
1014
1053
  return;
1015
1054
  }
1016
1055
  if (throttledMouseMove) {
@@ -1030,11 +1069,43 @@ function createStegaController(opts) {
1030
1069
  elements.cleanup();
1031
1070
  removeStegaStyles();
1032
1071
  initialized = false;
1033
- debug6.log("stega controller stopped");
1072
+ debug7.log("stega controller stopped");
1034
1073
  };
1035
1074
  return { start, stop };
1036
1075
  }
1037
1076
 
1077
+ // src/core/stega/scroll-to-field.ts
1078
+ var debug8 = createScopedLogger("stega:scroll");
1079
+ var FLASH_DURATION_MS = 1200;
1080
+ var flashTimeout = null;
1081
+ function scrollToField(request) {
1082
+ const { field, id } = request;
1083
+ if (!field) return false;
1084
+ const candidates = document.querySelectorAll(
1085
+ "[data-prepr-encoded]"
1086
+ );
1087
+ let target = null;
1088
+ for (const candidate of candidates) {
1089
+ if (candidate.getAttribute("data-prepr-field") !== field) continue;
1090
+ if (id && candidate.getAttribute("data-prepr-id") !== id) continue;
1091
+ target = candidate;
1092
+ break;
1093
+ }
1094
+ if (!target) {
1095
+ debug8.log("no element found for field", field);
1096
+ return false;
1097
+ }
1098
+ target.scrollIntoView({ behavior: "smooth", block: "center" });
1099
+ if (flashTimeout) clearTimeout(flashTimeout);
1100
+ document.querySelectorAll(".prepr-overlay-active").forEach((el) => el.classList.remove("prepr-overlay-active"));
1101
+ target.classList.add("prepr-overlay-active");
1102
+ flashTimeout = setTimeout(() => {
1103
+ target.classList.remove("prepr-overlay-active");
1104
+ flashTimeout = null;
1105
+ }, FLASH_DURATION_MS);
1106
+ return true;
1107
+ }
1108
+
1038
1109
  // src/core/store.ts
1039
1110
  var DEFAULT_STATE = {
1040
1111
  locale: "en",
@@ -2147,7 +2218,7 @@ function definePreprToolbar() {
2147
2218
  }
2148
2219
 
2149
2220
  // src/core/create-preview.ts
2150
- var debug7 = createScopedLogger("create-preview");
2221
+ var debug9 = createScopedLogger("create-preview");
2151
2222
  var controllerStores = /* @__PURE__ */ new WeakMap();
2152
2223
  function defaultNavigation() {
2153
2224
  return {
@@ -2189,7 +2260,7 @@ function createPreprPreview(opts = {}) {
2189
2260
  const search = new URLSearchParams(window.location.search);
2190
2261
  const hideBar = search.get(PARAM_HIDE_BAR) === "true";
2191
2262
  if (hideBar) {
2192
- debug7.log(`${PARAM_HIDE_BAR}=true \u2014 no visible toolbar`);
2263
+ debug9.log(`${PARAM_HIDE_BAR}=true \u2014 no visible toolbar`);
2193
2264
  }
2194
2265
  const mountUi = (options?.ui ?? true) && !isIframe && !hideBar;
2195
2266
  const locale = resolveLocale(options);
@@ -2225,18 +2296,17 @@ function createPreprPreview(opts = {}) {
2225
2296
  tooltip: mountUi,
2226
2297
  // In the editor, ask the parent to focus the field instead of opening a new
2227
2298
  // tab. Standalone previews keep the window.open behaviour.
2228
- onEdit: ({ href, origin, id, field }) => {
2299
+ onEdit: ({ href, id, field, locale: contentLocale }) => {
2229
2300
  const safeHref = href ? safeEditUrl(href) : null;
2230
2301
  if (href && !safeHref) {
2231
- debug7.warn("ignored edit request with unsupported href scheme");
2302
+ debug9.warn("ignored edit request with unsupported href scheme");
2232
2303
  return;
2233
2304
  }
2234
2305
  if (isIframe) {
2235
2306
  sendPreprEvent("field_edit_requested", {
2236
- href: safeHref ?? void 0,
2237
- origin,
2238
2307
  id,
2239
- field
2308
+ field,
2309
+ locale: contentLocale
2240
2310
  });
2241
2311
  } else if (safeHref) {
2242
2312
  window.open(safeHref, "_blank", "noopener,noreferrer");
@@ -2263,7 +2333,15 @@ function createPreprPreview(opts = {}) {
2263
2333
  handleChange(before, state);
2264
2334
  });
2265
2335
  const bridge = createIframeBridge(store, {
2266
- allowedEditorOrigins: options?.allowedEditorOrigins
2336
+ allowedEditorOrigins: options?.allowedEditorOrigins,
2337
+ // The editor selecting a field scrolls the matching element into view. The
2338
+ // lookup relies on the attributes the stega scan writes, so it finds
2339
+ // nothing until encoded content has been tagged.
2340
+ onScrollToField: ({ field, id }) => {
2341
+ if (!scrollToField({ field, id })) {
2342
+ debug9.log("no element on this page renders field", field);
2343
+ }
2344
+ }
2267
2345
  });
2268
2346
  sendPreprEvent(
2269
2347
  "getScrollPosition",
@@ -2283,7 +2361,7 @@ function createPreprPreview(opts = {}) {
2283
2361
  bridge.stop();
2284
2362
  }
2285
2363
  el?.remove();
2286
- debug7.log("preview destroyed");
2364
+ debug9.log("preview destroyed");
2287
2365
  }
2288
2366
  const controller = { destroy };
2289
2367
  controllerStores.set(controller, store);
@@ -2323,7 +2401,7 @@ var PREPR_HEADER_KEYS = [
2323
2401
  "Prepr-Visitor-IP",
2324
2402
  "Prepr-Hubspot-Id",
2325
2403
  "Prepr-Customer-Id-Created",
2326
- "Prepr-User-Agent",
2404
+ "User-Agent",
2327
2405
  "Prepr-Package"
2328
2406
  ];
2329
2407
  function getPreprHeadersFromHeaders(headers) {
@@ -2520,7 +2598,7 @@ function processFrameworkRequest(request, opts, framework) {
2520
2598
  }
2521
2599
  const userAgent = request.headers.get("user-agent");
2522
2600
  if (userAgent) {
2523
- requestHeaders.set("Prepr-User-Agent", sanitizeHeaderValue(userAgent));
2601
+ requestHeaders.set("User-Agent", sanitizeHeaderValue(userAgent));
2524
2602
  }
2525
2603
  const packageParts = [`@preprio/toolkit@${opts?.version ?? VERSION}`];
2526
2604
  if (framework) packageParts.push(`framework=${framework}`);