@preprio/toolkit 0.4.3 → 0.5.0-rc.2

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.
package/README.md CHANGED
@@ -694,14 +694,14 @@ createPreprMiddleware(request, response, { preview });
694
694
  createPreprPreview({ props, options: { debug: true, locale: 'nl' } });
695
695
  ```
696
696
 
697
- | Option | Type | Default | Description |
698
- | ---------------------- | --------------- | ------------- | -------------------------------------------------------------------------------------------------------- |
699
- | `debug` | `boolean` | `false` | Enable debug logging. |
700
- | `locale` | `'en' \| 'nl'` | auto-detected | UI language. Falls back to the first supported browser language, then `en`. |
701
- | `features` | `PreprFeatures` | all enabled | Which features run. See [Feature flags](#feature-flags). |
702
- | `ui` | `boolean` | `true` | Mount the visible toolbar. See [Headless preview](#headless-preview-no-toolbar-ui). |
703
- | `allowedEditorOrigins` | `string[]` | `*.prepr.io` | Exact editor origins allowed to drive this preview, for self-hosted editors. Replaces the wildcard. |
704
- | `autoClean` | `boolean` | `true` | Strip the invisible stega characters from visible text. See [Stega auto-cleaning](#stega-auto-cleaning). |
697
+ | Option | Type | Default | Description |
698
+ | ---------------------- | --------------- | ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
699
+ | `debug` | `boolean` | `false` | Enable debug logging. |
700
+ | `locale` | `'en' \| 'nl'` | auto-detected | UI language. Falls back to the first supported browser language, then `en`. |
701
+ | `features` | `PreprFeatures` | all enabled | Which features run. See [Feature flags](#feature-flags). |
702
+ | `ui` | `boolean` | `true` | Mount the visible toolbar. See [Headless preview](#headless-preview-no-toolbar-ui). |
703
+ | `allowedEditorOrigins` | `string[]` | `*.prepr.io` | Exact editor origins allowed to drive this preview, for self-hosted or local editors. Replaces the wildcard. `['*']` accepts any origin — development only. |
704
+ | `autoClean` | `boolean` | `true` | Strip the invisible stega characters from visible text. See [Stega auto-cleaning](#stega-auto-cleaning). |
705
705
 
706
706
  `features` and `ui` are independent: `features` decides _what runs_, `ui` decides _whether the toolbar is visible_.
707
707
 
@@ -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.3";
33
+ var VERSION = "0.5.0-rc.2";
34
34
 
35
35
  // src/core/features.ts
36
36
  function isEnabled(config) {
@@ -1,5 +1,5 @@
1
1
  // src/version.ts
2
- var VERSION = "0.4.3";
2
+ var VERSION = "0.5.0-rc.2";
3
3
 
4
4
  // src/core/features.ts
5
5
  function isEnabled(config) {
@@ -1,5 +1,5 @@
1
1
  // src/version.ts
2
- var VERSION = "0.4.3";
2
+ var VERSION = "0.5.0-rc.2";
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,9 +2004,13 @@ 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
- if (allowed) return allowed.includes(origin);
2010
+ if (allowed) {
2011
+ if (allowed.includes("*")) return origin !== "null";
2012
+ return allowed.includes(origin);
2013
+ }
1950
2014
  let url;
1951
2015
  try {
1952
2016
  url = new URL(origin);
@@ -1961,6 +2025,7 @@ function isAllowedEditorOrigin(origin, allowed) {
1961
2025
  }
1962
2026
  function createIframeBridge(store, options = {}) {
1963
2027
  const allowedOrigins = options.allowedEditorOrigins;
2028
+ const onScrollToField = options.onScrollToField;
1964
2029
  let parentOrigin = null;
1965
2030
  const onKeyDown = (event) => {
1966
2031
  const key = event.key.toLowerCase();
@@ -1970,7 +2035,10 @@ function createIframeBridge(store, options = {}) {
1970
2035
  const onMessage = (evt) => {
1971
2036
  const data = evt?.data;
1972
2037
  if (data?.event === "prepr:initVE" && !parentOrigin) {
1973
- if (!isAllowedEditorOrigin(evt.origin, allowedOrigins)) return;
2038
+ if (!isAllowedEditorOrigin(evt.origin, allowedOrigins)) {
2039
+ debug8.warn("ignored editor handshake from origin", evt.origin);
2040
+ return;
2041
+ }
1974
2042
  parentOrigin = evt.origin;
1975
2043
  setTrustedParentOrigin(parentOrigin);
1976
2044
  if (data.scrollPosition != null) {
@@ -1984,6 +2052,15 @@ function createIframeBridge(store, options = {}) {
1984
2052
  const currentScrollY = window.scrollY || document.documentElement.scrollTop;
1985
2053
  sendPreprEvent("getScrollPosition", { value: currentScrollY });
1986
2054
  }
2055
+ if (data?.event === "prepr:scrollToField") {
2056
+ if (typeof data.field !== "string" || !data.field) return;
2057
+ if (data.id !== void 0 && typeof data.id !== "string") return;
2058
+ try {
2059
+ onScrollToField?.({ field: data.field, id: data.id });
2060
+ } catch (error) {
2061
+ debug8.warn("scrollToField handler failed", error);
2062
+ }
2063
+ }
1987
2064
  };
1988
2065
  return {
1989
2066
  start() {
@@ -2102,7 +2179,7 @@ function splitPath(full) {
2102
2179
  }
2103
2180
 
2104
2181
  // src/core/create-preview.ts
2105
- var debug7 = createScopedLogger("create-preview");
2182
+ var debug9 = createScopedLogger("create-preview");
2106
2183
  var controllerStores = /* @__PURE__ */ new WeakMap();
2107
2184
  function defaultNavigation() {
2108
2185
  return {
@@ -2144,7 +2221,7 @@ function createPreprPreview(opts = {}) {
2144
2221
  const search = new URLSearchParams(window.location.search);
2145
2222
  const hideBar = search.get(PARAM_HIDE_BAR) === "true";
2146
2223
  if (hideBar) {
2147
- debug7.log(`${PARAM_HIDE_BAR}=true \u2014 no visible toolbar`);
2224
+ debug9.log(`${PARAM_HIDE_BAR}=true \u2014 no visible toolbar`);
2148
2225
  }
2149
2226
  const mountUi = (options?.ui ?? true) && !isIframe && !hideBar;
2150
2227
  const locale = resolveLocale(options);
@@ -2180,18 +2257,17 @@ function createPreprPreview(opts = {}) {
2180
2257
  tooltip: mountUi,
2181
2258
  // In the editor, ask the parent to focus the field instead of opening a new
2182
2259
  // tab. Standalone previews keep the window.open behaviour.
2183
- onEdit: ({ href, origin, id, field }) => {
2260
+ onEdit: ({ href, id, field, locale: contentLocale }) => {
2184
2261
  const safeHref = href ? safeEditUrl(href) : null;
2185
2262
  if (href && !safeHref) {
2186
- debug7.warn("ignored edit request with unsupported href scheme");
2263
+ debug9.warn("ignored edit request with unsupported href scheme");
2187
2264
  return;
2188
2265
  }
2189
2266
  if (isIframe) {
2190
2267
  sendPreprEvent("field_edit_requested", {
2191
- href: safeHref ?? void 0,
2192
- origin,
2193
2268
  id,
2194
- field
2269
+ field,
2270
+ locale: contentLocale
2195
2271
  });
2196
2272
  } else if (safeHref) {
2197
2273
  window.open(safeHref, "_blank", "noopener,noreferrer");
@@ -2218,7 +2294,15 @@ function createPreprPreview(opts = {}) {
2218
2294
  handleChange(before, state);
2219
2295
  });
2220
2296
  const bridge = createIframeBridge(store, {
2221
- allowedEditorOrigins: options?.allowedEditorOrigins
2297
+ allowedEditorOrigins: options?.allowedEditorOrigins,
2298
+ // The editor selecting a field scrolls the matching element into view. The
2299
+ // lookup relies on the attributes the stega scan writes, so it finds
2300
+ // nothing until encoded content has been tagged.
2301
+ onScrollToField: ({ field, id }) => {
2302
+ if (!scrollToField({ field, id })) {
2303
+ debug9.log("no element on this page renders field", field);
2304
+ }
2305
+ }
2222
2306
  });
2223
2307
  sendPreprEvent(
2224
2308
  "getScrollPosition",
@@ -2238,7 +2322,7 @@ function createPreprPreview(opts = {}) {
2238
2322
  bridge.stop();
2239
2323
  }
2240
2324
  el?.remove();
2241
- debug7.log("preview destroyed");
2325
+ debug9.log("preview destroyed");
2242
2326
  }
2243
2327
  const controller = { destroy };
2244
2328
  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.3";
47
+ var VERSION = "0.5.0-rc.2";
48
48
 
49
49
  // src/core/features.ts
50
50
  function isEnabled(config) {
@@ -312,9 +312,13 @@ 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
- if (allowed) return allowed.includes(origin);
318
+ if (allowed) {
319
+ if (allowed.includes("*")) return origin !== "null";
320
+ return allowed.includes(origin);
321
+ }
318
322
  let url;
319
323
  try {
320
324
  url = new URL(origin);
@@ -329,6 +333,7 @@ function isAllowedEditorOrigin(origin, allowed) {
329
333
  }
330
334
  function createIframeBridge(store, options = {}) {
331
335
  const allowedOrigins = options.allowedEditorOrigins;
336
+ const onScrollToField = options.onScrollToField;
332
337
  let parentOrigin = null;
333
338
  const onKeyDown = (event) => {
334
339
  const key = event.key.toLowerCase();
@@ -338,7 +343,10 @@ function createIframeBridge(store, options = {}) {
338
343
  const onMessage = (evt) => {
339
344
  const data = evt?.data;
340
345
  if (data?.event === "prepr:initVE" && !parentOrigin) {
341
- if (!isAllowedEditorOrigin(evt.origin, allowedOrigins)) return;
346
+ if (!isAllowedEditorOrigin(evt.origin, allowedOrigins)) {
347
+ debug.warn("ignored editor handshake from origin", evt.origin);
348
+ return;
349
+ }
342
350
  parentOrigin = evt.origin;
343
351
  setTrustedParentOrigin(parentOrigin);
344
352
  if (data.scrollPosition != null) {
@@ -352,6 +360,15 @@ function createIframeBridge(store, options = {}) {
352
360
  const currentScrollY = window.scrollY || document.documentElement.scrollTop;
353
361
  sendPreprEvent("getScrollPosition", { value: currentScrollY });
354
362
  }
363
+ if (data?.event === "prepr:scrollToField") {
364
+ if (typeof data.field !== "string" || !data.field) return;
365
+ if (data.id !== void 0 && typeof data.id !== "string") return;
366
+ try {
367
+ onScrollToField?.({ field: data.field, id: data.id });
368
+ } catch (error) {
369
+ debug.warn("scrollToField handler failed", error);
370
+ }
371
+ }
355
372
  };
356
373
  return {
357
374
  start() {
@@ -376,7 +393,7 @@ var import_stega2 = require("@vercel/stega");
376
393
 
377
394
  // src/core/stega/clean.ts
378
395
  var import_stega = require("@vercel/stega");
379
- var debug = createScopedLogger("stega:clean");
396
+ var debug2 = createScopedLogger("stega:clean");
380
397
  function stegaClean(text) {
381
398
  if (!text) return text;
382
399
  return (0, import_stega.vercelStegaClean)(text);
@@ -389,7 +406,7 @@ function decodeStega(str) {
389
406
  return decoded;
390
407
  }
391
408
  } catch (error) {
392
- debug.log("error decoding stega data", error);
409
+ debug2.log("error decoding stega data", error);
393
410
  const match = str.match(/{"origin.*?}/);
394
411
  if (match) {
395
412
  try {
@@ -398,7 +415,7 @@ function decodeStega(str) {
398
415
  return decodedMatch;
399
416
  }
400
417
  } catch (innerError) {
401
- debug.log("error decoding stega regex match", innerError);
418
+ debug2.log("error decoding stega regex match", innerError);
402
419
  }
403
420
  }
404
421
  }
@@ -434,14 +451,37 @@ function isRenderedElement(element) {
434
451
  if (!style) return true;
435
452
  return style.visibility !== "hidden" && style.display !== "none";
436
453
  }
454
+ function parseEditHref(href) {
455
+ try {
456
+ const url = new URL(href, window.location.origin);
457
+ return {
458
+ id: url.pathname.split("/").pop() || void 0,
459
+ field: url.searchParams.get("field") || void 0,
460
+ locale: url.searchParams.get("locale") || void 0
461
+ };
462
+ } catch {
463
+ return {};
464
+ }
465
+ }
466
+ function setOrRemove(element, name, value) {
467
+ if (value) {
468
+ element.setAttribute(name, value);
469
+ } else {
470
+ element.removeAttribute(name);
471
+ }
472
+ }
437
473
  function tagEncodedElement(element, decoded) {
438
474
  element.setAttribute("data-prepr-encoded", "");
439
475
  element.setAttribute("data-prepr-href", decoded.href);
440
476
  element.setAttribute("data-prepr-origin", decoded.origin);
477
+ const { id, field, locale } = parseEditHref(decoded.href);
478
+ setOrRemove(element, "data-prepr-id", id);
479
+ setOrRemove(element, "data-prepr-field", field);
480
+ setOrRemove(element, "data-prepr-locale", locale);
441
481
  }
442
482
 
443
483
  // src/core/stega/auto-clean.ts
444
- var debug2 = createScopedLogger("stega:auto-clean");
484
+ var debug3 = createScopedLogger("stega:auto-clean");
445
485
  function createStegaAutoClean(options) {
446
486
  const stripText = options?.enabled ?? true;
447
487
  let observer = null;
@@ -484,7 +524,7 @@ function createStegaAutoClean(options) {
484
524
  }
485
525
  pendingHidden.add(textNode);
486
526
  } catch (error) {
487
- debug2.log("error cleaning text node", error);
527
+ debug3.log("error cleaning text node", error);
488
528
  }
489
529
  return false;
490
530
  }
@@ -570,7 +610,7 @@ function createStegaAutoClean(options) {
570
610
  if (observer || typeof window === "undefined") return;
571
611
  scanAndClean();
572
612
  setupObserver();
573
- debug2.log("stega auto-clean started");
613
+ debug3.log("stega auto-clean started");
574
614
  }
575
615
  function stop() {
576
616
  if (debounceTimeout) {
@@ -584,13 +624,13 @@ function createStegaAutoClean(options) {
584
624
  cleaned = /* @__PURE__ */ new WeakMap();
585
625
  pendingHidden = /* @__PURE__ */ new Set();
586
626
  pendingDirty = /* @__PURE__ */ new Set();
587
- debug2.log("stega auto-clean stopped");
627
+ debug3.log("stega auto-clean stopped");
588
628
  }
589
629
  return { start, stop };
590
630
  }
591
631
 
592
632
  // src/core/stega/elements.ts
593
- var debug3 = createScopedLogger("stega:elements");
633
+ var debug4 = createScopedLogger("stega:elements");
594
634
  var StegaElements = class {
595
635
  constructor(decode = decodeStega) {
596
636
  this.observer = null;
@@ -635,7 +675,7 @@ var StegaElements = class {
635
675
  walkTextNodes(document.body, (textNode) => {
636
676
  if (this.tagFromTextNode(textNode)) encodedCount++;
637
677
  });
638
- debug3.log("document scan complete, encoded", encodedCount, "elements");
678
+ debug4.log("document scan complete, encoded", encodedCount, "elements");
639
679
  this.elements = document.querySelectorAll("[data-prepr-encoded]");
640
680
  }
641
681
  /** `onUpdate` fires after each debounced batch, for refreshing derived state. */
@@ -691,6 +731,9 @@ var StegaElements = class {
691
731
  element.removeAttribute("data-prepr-encoded");
692
732
  element.removeAttribute("data-prepr-href");
693
733
  element.removeAttribute("data-prepr-origin");
734
+ element.removeAttribute("data-prepr-id");
735
+ element.removeAttribute("data-prepr-field");
736
+ element.removeAttribute("data-prepr-locale");
694
737
  element.classList.remove("prepr-overlay-active");
695
738
  });
696
739
  this.elements = void 0;
@@ -698,7 +741,7 @@ var StegaElements = class {
698
741
  };
699
742
 
700
743
  // src/core/stega/overlay.ts
701
- var debug4 = createScopedLogger("stega:overlay");
744
+ var debug5 = createScopedLogger("stega:overlay");
702
745
  var StegaOverlay = class {
703
746
  constructor(onEdit) {
704
747
  this.overlay = null;
@@ -743,7 +786,8 @@ var StegaOverlay = class {
743
786
  const origin = element.getAttribute("data-prepr-origin");
744
787
  const id = element.getAttribute("data-prepr-id") ?? void 0;
745
788
  const field = element.getAttribute("data-prepr-field") ?? void 0;
746
- debug4.log("showing overlay", { href, origin });
789
+ const locale = element.getAttribute("data-prepr-locale") ?? void 0;
790
+ debug5.log("showing overlay", { href, origin });
747
791
  const overlay = this.overlay;
748
792
  overlay.style.display = "block";
749
793
  overlay.style.top = `${rect.top + window.scrollY - 2}px`;
@@ -775,7 +819,7 @@ var StegaOverlay = class {
775
819
  tooltip.style.left = `${left}px`;
776
820
  });
777
821
  tooltip.onclick = () => {
778
- this.onEdit({ href, origin, id, field });
822
+ this.onEdit({ href, origin, id, field, locale });
779
823
  };
780
824
  }
781
825
  this.currentElement = element;
@@ -815,7 +859,7 @@ var StegaOverlay = class {
815
859
  };
816
860
 
817
861
  // src/core/stega/proximity.ts
818
- var debug5 = createScopedLogger("stega:proximity");
862
+ var debug6 = createScopedLogger("stega:proximity");
819
863
  var StegaProximity = class {
820
864
  constructor() {
821
865
  this.highlighted = /* @__PURE__ */ new Set();
@@ -850,9 +894,9 @@ var StegaProximity = class {
850
894
  );
851
895
  const nodes = this.getEncodedElements();
852
896
  nodes.forEach((el) => this.observer.observe(el));
853
- debug5.log("observing", nodes.length, "encoded elements");
897
+ debug6.log("observing", nodes.length, "encoded elements");
854
898
  } catch (error) {
855
- debug5.log("error setting up IntersectionObserver", error);
899
+ debug6.log("error setting up IntersectionObserver", error);
856
900
  }
857
901
  }
858
902
  stopObserving() {
@@ -922,7 +966,7 @@ function removeStegaStyles() {
922
966
  }
923
967
 
924
968
  // src/core/stega/scan.ts
925
- var debug6 = createScopedLogger("stega:scan");
969
+ var debug7 = createScopedLogger("stega:scan");
926
970
  var EDITOR_MODE_STYLES = "[data-prepr-encoded]{cursor:pointer}";
927
971
  function createStegaController(opts) {
928
972
  const tooltipEnabled = opts.tooltip ?? true;
@@ -955,12 +999,13 @@ function createStegaController(opts) {
955
999
  href: encoded.getAttribute("data-prepr-href") ?? "",
956
1000
  origin: encoded.getAttribute("data-prepr-origin") ?? "",
957
1001
  id: encoded.getAttribute("data-prepr-id") ?? void 0,
958
- field: encoded.getAttribute("data-prepr-field") ?? void 0
1002
+ field: encoded.getAttribute("data-prepr-field") ?? void 0,
1003
+ locale: encoded.getAttribute("data-prepr-locale") ?? void 0
959
1004
  });
960
1005
  };
961
1006
  const start = () => {
962
1007
  if (initialized) {
963
- debug6.log("already initialized, skipping setup");
1008
+ debug7.log("already initialized, skipping setup");
964
1009
  return;
965
1010
  }
966
1011
  if (!tooltipEnabled) {
@@ -970,7 +1015,7 @@ function createStegaController(opts) {
970
1015
  });
971
1016
  document.addEventListener("click", handleClick, { capture: true });
972
1017
  initialized = true;
973
- debug6.log("stega controller started (editor mode)");
1018
+ debug7.log("stega controller started (editor mode)");
974
1019
  return;
975
1020
  }
976
1021
  injectStegaStyles();
@@ -1001,7 +1046,7 @@ function createStegaController(opts) {
1001
1046
  passive: true
1002
1047
  });
1003
1048
  initialized = true;
1004
- debug6.log("stega controller started");
1049
+ debug7.log("stega controller started");
1005
1050
  };
1006
1051
  const stop = () => {
1007
1052
  if (!initialized) return;
@@ -1010,7 +1055,7 @@ function createStegaController(opts) {
1010
1055
  elements.cleanup();
1011
1056
  removeStegaStyles();
1012
1057
  initialized = false;
1013
- debug6.log("stega controller stopped (editor mode)");
1058
+ debug7.log("stega controller stopped (editor mode)");
1014
1059
  return;
1015
1060
  }
1016
1061
  if (throttledMouseMove) {
@@ -1030,11 +1075,43 @@ function createStegaController(opts) {
1030
1075
  elements.cleanup();
1031
1076
  removeStegaStyles();
1032
1077
  initialized = false;
1033
- debug6.log("stega controller stopped");
1078
+ debug7.log("stega controller stopped");
1034
1079
  };
1035
1080
  return { start, stop };
1036
1081
  }
1037
1082
 
1083
+ // src/core/stega/scroll-to-field.ts
1084
+ var debug8 = createScopedLogger("stega:scroll");
1085
+ var FLASH_DURATION_MS = 1200;
1086
+ var flashTimeout = null;
1087
+ function scrollToField(request) {
1088
+ const { field, id } = request;
1089
+ if (!field) return false;
1090
+ const candidates = document.querySelectorAll(
1091
+ "[data-prepr-encoded]"
1092
+ );
1093
+ let target = null;
1094
+ for (const candidate of candidates) {
1095
+ if (candidate.getAttribute("data-prepr-field") !== field) continue;
1096
+ if (id && candidate.getAttribute("data-prepr-id") !== id) continue;
1097
+ target = candidate;
1098
+ break;
1099
+ }
1100
+ if (!target) {
1101
+ debug8.log("no element found for field", field);
1102
+ return false;
1103
+ }
1104
+ target.scrollIntoView({ behavior: "smooth", block: "center" });
1105
+ if (flashTimeout) clearTimeout(flashTimeout);
1106
+ document.querySelectorAll(".prepr-overlay-active").forEach((el) => el.classList.remove("prepr-overlay-active"));
1107
+ target.classList.add("prepr-overlay-active");
1108
+ flashTimeout = setTimeout(() => {
1109
+ target.classList.remove("prepr-overlay-active");
1110
+ flashTimeout = null;
1111
+ }, FLASH_DURATION_MS);
1112
+ return true;
1113
+ }
1114
+
1038
1115
  // src/core/store.ts
1039
1116
  var DEFAULT_STATE = {
1040
1117
  locale: "en",
@@ -2147,7 +2224,7 @@ function definePreprToolbar() {
2147
2224
  }
2148
2225
 
2149
2226
  // src/core/create-preview.ts
2150
- var debug7 = createScopedLogger("create-preview");
2227
+ var debug9 = createScopedLogger("create-preview");
2151
2228
  var controllerStores = /* @__PURE__ */ new WeakMap();
2152
2229
  function defaultNavigation() {
2153
2230
  return {
@@ -2189,7 +2266,7 @@ function createPreprPreview(opts = {}) {
2189
2266
  const search = new URLSearchParams(window.location.search);
2190
2267
  const hideBar = search.get(PARAM_HIDE_BAR) === "true";
2191
2268
  if (hideBar) {
2192
- debug7.log(`${PARAM_HIDE_BAR}=true \u2014 no visible toolbar`);
2269
+ debug9.log(`${PARAM_HIDE_BAR}=true \u2014 no visible toolbar`);
2193
2270
  }
2194
2271
  const mountUi = (options?.ui ?? true) && !isIframe && !hideBar;
2195
2272
  const locale = resolveLocale(options);
@@ -2225,18 +2302,17 @@ function createPreprPreview(opts = {}) {
2225
2302
  tooltip: mountUi,
2226
2303
  // In the editor, ask the parent to focus the field instead of opening a new
2227
2304
  // tab. Standalone previews keep the window.open behaviour.
2228
- onEdit: ({ href, origin, id, field }) => {
2305
+ onEdit: ({ href, id, field, locale: contentLocale }) => {
2229
2306
  const safeHref = href ? safeEditUrl(href) : null;
2230
2307
  if (href && !safeHref) {
2231
- debug7.warn("ignored edit request with unsupported href scheme");
2308
+ debug9.warn("ignored edit request with unsupported href scheme");
2232
2309
  return;
2233
2310
  }
2234
2311
  if (isIframe) {
2235
2312
  sendPreprEvent("field_edit_requested", {
2236
- href: safeHref ?? void 0,
2237
- origin,
2238
2313
  id,
2239
- field
2314
+ field,
2315
+ locale: contentLocale
2240
2316
  });
2241
2317
  } else if (safeHref) {
2242
2318
  window.open(safeHref, "_blank", "noopener,noreferrer");
@@ -2263,7 +2339,15 @@ function createPreprPreview(opts = {}) {
2263
2339
  handleChange(before, state);
2264
2340
  });
2265
2341
  const bridge = createIframeBridge(store, {
2266
- allowedEditorOrigins: options?.allowedEditorOrigins
2342
+ allowedEditorOrigins: options?.allowedEditorOrigins,
2343
+ // The editor selecting a field scrolls the matching element into view. The
2344
+ // lookup relies on the attributes the stega scan writes, so it finds
2345
+ // nothing until encoded content has been tagged.
2346
+ onScrollToField: ({ field, id }) => {
2347
+ if (!scrollToField({ field, id })) {
2348
+ debug9.log("no element on this page renders field", field);
2349
+ }
2350
+ }
2267
2351
  });
2268
2352
  sendPreprEvent(
2269
2353
  "getScrollPosition",
@@ -2283,7 +2367,7 @@ function createPreprPreview(opts = {}) {
2283
2367
  bridge.stop();
2284
2368
  }
2285
2369
  el?.remove();
2286
- debug7.log("preview destroyed");
2370
+ debug9.log("preview destroyed");
2287
2371
  }
2288
2372
  const controller = { destroy };
2289
2373
  controllerStores.set(controller, store);