@preprio/toolkit 0.2.0 → 0.3.4

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
@@ -1,5 +1,10 @@
1
1
  # @preprio/toolkit
2
2
 
3
+ > **Beta — pre-1.0.** The package is published to `latest` and is safe to use, but the
4
+ > public API can still change between minor versions (`0.2.x` → `0.3.0`). Every change
5
+ > is documented in [Breaking changes](https://github.com/preprio/prepr-toolkit/blob/main/RELEASING.md#breaking-changes). Pin an exact
6
+ > version in production, or use a tilde range (`~0.2.0`) so you only pick up patches.
7
+
3
8
  A framework-agnostic TypeScript library that provides preview functionality, visual editing, and A/B testing for [Prepr CMS](https://prepr.io). Ships thin wrappers for Next.js, Nuxt, Astro, and SvelteKit on top of a vanilla core that runs anywhere.
4
9
 
5
10
  Every integration is the same three steps:
@@ -10,6 +15,8 @@ Every integration is the same three steps:
10
15
 
11
16
  Jump to your framework: [Next.js](#nextjs) · [Astro](#astro) · [SvelteKit](#sveltekit) · [Nuxt](#nuxt) · [React without a framework](#react-no-framework) · [anything else](#any-other-framework).
12
17
 
18
+ Coming from `@preprio/prepr-nextjs`? See the [migration guide](https://github.com/preprio/prepr-toolkit/blob/main/MIGRATION.md).
19
+
13
20
  ## Quick Start (Next.js)
14
21
 
15
22
  ```bash
@@ -624,14 +631,14 @@ controller.destroy();
624
631
  | Option | Type | Description |
625
632
  | ------------ | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
626
633
  | `props` | `PreprToolbarProps` | From `getToolbarProps`. Optional — a headless preview has no segment list to pass. |
627
- | `options` | `PreprPreviewOptions` | `{ debug?, locale?, features?, ui?, allowedEditorOrigins? }`. See [Preview options](#preview-options). |
634
+ | `options` | `PreprPreviewOptions` | `{ debug?, locale?, features?, ui?, allowedEditorOrigins?, autoClean? }`. See [Preview options](#preview-options). |
628
635
  | `navigation` | `PreprNavigationAdapter` | How segment/variant switches navigate. Optional — omitted, the toolbar uses `window.location.assign` (a full page load). `reload` is optional too, runs after a preview-mode toggle, and defaults to `window.location.reload()`. The Next.js and SvelteKit wrappers wire all of this up for you. |
629
636
 
630
637
  `createPreprPreview` is a no-op outside a browser (no `window`/`document`) and returns a controller whose `destroy()` does nothing, so it is safe to call during SSR. Call it **once per page** — two calls start two bridges and announce the preview twice.
631
638
 
632
639
  #### Tracking
633
640
 
634
- - **`loadTrackingPixel(id, config?)`** — installs the CDN tracking pixel. Idempotent, and a no-op outside a browser. A typed facade over Prepr's existing CDN pixel (`https://cdn.tracking.prepr.io/js/prepr-v2.min.js`), reproducing the legacy `<script>` snippet's queue-stub semantics: calls made before the CDN script loads are queued and flushed once it is ready.
641
+ - **`loadTrackingPixel(id, config?)`** — installs the CDN tracking pixel. Idempotent, and a no-op outside a browser. A typed facade over Prepr's existing CDN pixel (`https://cdn.tracking.prepr.io/js/prepr.min.js`), reproducing the legacy `<script>` snippet's queue-stub semantics: calls made before the CDN script loads are queued and flushed once it is ready.
635
642
  - **`trackEvent(name, data?)`** — sends a custom tracking event: `trackEvent('add_to_cart', { productId: 'abc123' })`.
636
643
  - **`setTrackingParam(key, value)`** — sets a persistent tracking parameter: `setTrackingParam('user_type', 'returning')`.
637
644
 
@@ -682,13 +689,14 @@ createPreprMiddleware(request, response, { preview });
682
689
  createPreprPreview({ props, options: { debug: true, locale: 'nl' } });
683
690
  ```
684
691
 
685
- | Option | Type | Default | Description |
686
- | ---------------------- | --------------- | ------------- | --------------------------------------------------------------------------------------------------- |
687
- | `debug` | `boolean` | `false` | Enable debug logging. |
688
- | `locale` | `'en' \| 'nl'` | auto-detected | UI language. Falls back to the first supported browser language, then `en`. |
689
- | `features` | `PreprFeatures` | all enabled | Which features run. See [Feature flags](#feature-flags). |
690
- | `ui` | `boolean` | `true` | Mount the visible toolbar. See [Headless preview](#headless-preview-no-toolbar-ui). |
691
- | `allowedEditorOrigins` | `string[]` | `*.prepr.io` | Exact editor origins allowed to drive this preview, for self-hosted editors. Replaces the wildcard. |
692
+ | Option | Type | Default | Description |
693
+ | ---------------------- | --------------- | ------------- | -------------------------------------------------------------------------------------------------------- |
694
+ | `debug` | `boolean` | `false` | Enable debug logging. |
695
+ | `locale` | `'en' \| 'nl'` | auto-detected | UI language. Falls back to the first supported browser language, then `en`. |
696
+ | `features` | `PreprFeatures` | all enabled | Which features run. See [Feature flags](#feature-flags). |
697
+ | `ui` | `boolean` | `true` | Mount the visible toolbar. See [Headless preview](#headless-preview-no-toolbar-ui). |
698
+ | `allowedEditorOrigins` | `string[]` | `*.prepr.io` | Exact editor origins allowed to drive this preview, for self-hosted editors. Replaces the wildcard. |
699
+ | `autoClean` | `boolean` | `true` | Strip the invisible stega characters from visible text. See [Stega auto-cleaning](#stega-auto-cleaning). |
692
700
 
693
701
  `features` and `ui` are independent: `features` decides _what runs_, `ui` decides _whether the toolbar is visible_.
694
702
 
@@ -812,6 +820,12 @@ createPreprPreview({ props, options: { locale: 'nl' } });
812
820
  - **Confirm the middleware matcher**: if `config.matcher` excludes the current path, no Prepr headers were set for it. `getPreprUUID()` returning `null` confirms this.
813
821
  - **Check for `prepr_hide_bar=true`** in the URL, which suppresses the bar by design.
814
822
 
823
+ ### Click-to-edit not working on some elements
824
+
825
+ - **Check the token permissions**: "Enable edit mode" must be checked on the token in Prepr.
826
+ - **Check whether the encoded text sits in a hidden element.** The parent of the encoded text node is what becomes editable, and a parent that renders no box (`display: none`, `visibility: hidden`, the `hidden` attribute) is skipped — it cannot be hovered or clicked. Put `data-prepr-edit-target` on the visible ancestor that should be editable instead. See [Choosing the element that becomes editable](#choosing-the-element-that-becomes-editable).
827
+ - **Inspect the tagged elements**: `document.querySelectorAll('[data-prepr-encoded]')` in the console lists everything edit mode found. An element you expected to be in that list points at one of the two cases above.
828
+
815
829
  ### Headers not working
816
830
 
817
831
  - **Middleware setup**: confirm `middleware.ts` sits in the project root (or `src/`) and its matcher covers the route.
@@ -883,7 +897,62 @@ The toolbar renders with Preact into a shadow-DOM custom element (`<prepr-toolba
883
897
 
884
898
  With edit mode enabled, the toolkit scans for stega-encoded content, strips the invisible Unicode characters after load so they cannot cause layout shifts, highlights editable elements by cursor proximity, and talks to the Prepr editor over a `postMessage` bridge when running inside the live-preview iframe.
885
899
 
886
- Stega cleaning is automatic. There is no need to call `vercelStegaSplit` or hand-manage hidden spans — the data attributes the toolkit relies on survive the clean, so edit mode still activates instantly.
900
+ Stega cleaning is automatic. There is no need to call `vercelStegaSplit` or to strip the payload yourself before rendering — the data attributes the toolkit relies on survive the clean, so edit mode still activates instantly.
901
+
902
+ #### Stega auto-cleaning
903
+
904
+ Preview content carries its edit payload inside the text itself, as a run of invisible Unicode characters. The toolkit strips them from the DOM after load and keeps stripping them as your framework re-renders.
905
+
906
+ Leaving them in place is not cosmetic. The characters are real string content: they inflate `String.length`, break text measurement and truncation, and are announced by screen readers.
907
+
908
+ Turn the stripping off with `autoClean: false`:
909
+
910
+ ```typescript
911
+ createPreprPreview({ props, options: { autoClean: false } });
912
+ ```
913
+
914
+ Only do this when your app already strips the payload itself — for example by passing every field through `stegaClean` as it renders:
915
+
916
+ ```typescript
917
+ import { stegaClean } from '@preprio/toolkit';
918
+
919
+ <h1>{stegaClean(page.title)}</h1>;
920
+ ```
921
+
922
+ Click-to-edit is unaffected by this option. Elements are still tagged from the payload, so the overlay, tooltip and edit bridge keep working either way.
923
+
924
+ #### Choosing the element that becomes editable
925
+
926
+ The element that gets the outline, the tooltip and the click handler is the **parent of the encoded text node**. Rendering `{title}` inside an `<h2>` makes the `<h2>` editable, which is what you want almost every time.
927
+
928
+ Two rules govern the choice:
929
+
930
+ - A parent that renders no box is skipped. An element hidden with `display: none`, `visibility: hidden`, or the `hidden` attribute cannot be hovered or outlined, so tagging it would produce an element that looks editable to the code and is unreachable to the visitor. Nothing is tagged for it.
931
+ - `data-prepr-edit-target` overrides the parent. The closest ancestor carrying it becomes the editable element instead.
932
+
933
+ That attribute is the supported way to separate _where the payload lives_ from _what the visitor clicks_. It is what makes the hidden-payload pattern work:
934
+
935
+ ```html
936
+ <!-- The payload rides in a visually hidden span; the <article> is what
937
+ gets outlined and clicked. -->
938
+ <article data-prepr-edit-target>
939
+ <h2>Product title</h2>
940
+ <span hidden>{encodedPayload}</span>
941
+ </article>
942
+ ```
943
+
944
+ Without the attribute that span is skipped entirely — it renders no box, so there is nothing to click.
945
+
946
+ It is equally useful when the encoded text sits deep inside markup you do not control, and the natural click target is an ancestor:
947
+
948
+ ```html
949
+ <figure data-prepr-edit-target>
950
+ <img src="..." alt="..." />
951
+ <figcaption><em>{encodedCaption}</em></figcaption>
952
+ </figure>
953
+ ```
954
+
955
+ Without it the `<em>` would be the editable element; with it the whole figure is.
887
956
 
888
957
  ## Examples
889
958
 
@@ -897,6 +966,14 @@ Runnable examples live in the repository root:
897
966
  | `examples/nuxt` | `server/middleware`, `useAsyncData`, `.vue` components |
898
967
  | `examples/express` | Vanilla core — a hand-written adapter for an unsupported framework |
899
968
 
969
+ ## Migrating from `@preprio/prepr-nextjs`
970
+
971
+ `@preprio/toolkit` replaces the Next.js-only `@preprio/prepr-nextjs` package. Most of
972
+ the change is import paths — the middleware signature is unchanged and the server
973
+ helpers keep their names — but the toolbar's provider and the stylesheet are gone.
974
+
975
+ The [migration guide](https://github.com/preprio/prepr-toolkit/blob/main/MIGRATION.md) has the diffs.
976
+
900
977
  ## Support
901
978
 
902
979
  - **Documentation**: [Prepr Documentation](https://docs.prepr.io)
@@ -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.2.0";
33
+ var VERSION = "0.3.4";
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.2.0";
2
+ var VERSION = "0.3.4";
3
3
 
4
4
  // src/core/features.ts
5
5
  function isEnabled(config) {
@@ -169,6 +169,20 @@ function walkTextNodes(root, visit) {
169
169
  visit(node);
170
170
  }
171
171
  }
172
+ function resolveEditTarget(node) {
173
+ const parent = node.parentElement;
174
+ if (!parent) return null;
175
+ const editTarget = parent.closest("[data-prepr-edit-target]");
176
+ if (editTarget) return editTarget;
177
+ return isRenderedElement(parent) ? parent : null;
178
+ }
179
+ function isRenderedElement(element) {
180
+ if (element.hidden) return false;
181
+ if (element.getClientRects().length === 0) return false;
182
+ const style = element.ownerDocument.defaultView?.getComputedStyle(element);
183
+ if (!style) return true;
184
+ return style.visibility !== "hidden" && style.display !== "none";
185
+ }
172
186
  function tagEncodedElement(element, decoded) {
173
187
  element.setAttribute("data-prepr-encoded", "");
174
188
  element.setAttribute("data-prepr-href", decoded.href);
@@ -192,7 +206,7 @@ var StegaElements = class {
192
206
  tagFromTextNode(node) {
193
207
  const decoded = this.decode(node.textContent);
194
208
  if (!decoded?.href) return false;
195
- const target = node.parentElement;
209
+ const target = resolveEditTarget(node);
196
210
  if (!target || target.hasAttribute("data-prepr-encoded")) return false;
197
211
  tagEncodedElement(target, decoded);
198
212
  return true;
@@ -238,8 +252,13 @@ var StegaElements = class {
238
252
  if (mutation.type === "characterData") {
239
253
  addedNodes.add(mutation.target);
240
254
  }
255
+ if (mutation.type === "attributes") {
256
+ addedNodes.add(mutation.target);
257
+ }
258
+ });
259
+ addedNodes.forEach((node) => {
260
+ if (node.isConnected) this.scanNode(node);
241
261
  });
242
- addedNodes.forEach((node) => this.scanNode(node));
243
262
  pending = [];
244
263
  this.elements = document.querySelectorAll("[data-prepr-encoded]");
245
264
  onUpdate?.();
@@ -252,7 +271,11 @@ var StegaElements = class {
252
271
  this.observer.observe(document.body, {
253
272
  childList: true,
254
273
  subtree: true,
255
- characterData: true
274
+ characterData: true,
275
+ // Reveal-on-class/style (dropdowns, accordions) changes no text and adds
276
+ // no nodes, but does change what is hoverable and needs a rescan.
277
+ attributes: true,
278
+ attributeFilter: ["class", "style", "hidden", "open", "aria-expanded"]
256
279
  });
257
280
  }
258
281
  /** Drops the active class but keeps the data attributes, unlike cleanup(). */
@@ -341,11 +364,13 @@ var StegaOverlay = class {
341
364
  const maxTop = window.scrollY + window.innerHeight - tooltip.clientHeight - 4;
342
365
  const minLeft = window.scrollX + 4;
343
366
  const maxLeft = window.scrollX + window.innerWidth - tooltip.clientWidth - 4;
344
- if (top < minTop) {
367
+ const below = top < minTop;
368
+ if (below) {
345
369
  top = rect.bottom + window.scrollY + 2;
346
370
  }
347
371
  top = Math.max(minTop, Math.min(top, maxTop));
348
372
  left = Math.max(minLeft, Math.min(left, maxLeft));
373
+ tooltip.dataset.preprPlacement = below ? "below" : "above";
349
374
  tooltip.style.top = `${top}px`;
350
375
  tooltip.style.left = `${left}px`;
351
376
  });
@@ -477,7 +502,7 @@ var StegaProximity = class {
477
502
  };
478
503
 
479
504
  // src/core/stega/stega.generated.ts
480
- 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: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}';
505
+ 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}';
481
506
 
482
507
  // src/core/stega/styles.ts
483
508
  var STEGA_STYLE_MARKER = "data-prepr-stega";
@@ -613,32 +638,45 @@ function createStegaController(opts) {
613
638
  // src/core/stega/auto-clean.ts
614
639
  import { vercelStegaSplit } from "@vercel/stega";
615
640
  var debug6 = createScopedLogger("stega:auto-clean");
616
- function createStegaAutoClean() {
641
+ function createStegaAutoClean(options) {
642
+ const stripText = options?.enabled ?? true;
617
643
  let observer = null;
618
644
  let processing = false;
619
645
  let cleaned = /* @__PURE__ */ new WeakMap();
646
+ let pendingHidden = /* @__PURE__ */ new Set();
647
+ let pendingDirty = /* @__PURE__ */ new Set();
620
648
  let debounceTimeout = null;
621
649
  function cleanTextNode(textNode) {
622
650
  if (isIgnoredTextNode(textNode)) return false;
623
651
  const textContent = textNode.textContent;
624
652
  const info = cleaned.get(textNode);
625
- if (info && textContent === info.cleaned) return false;
653
+ const settled = stripText ? info?.cleaned : info?.original;
654
+ if (info && textContent === settled) {
655
+ const shown = resolveEditTarget(textNode);
656
+ if (!shown) return false;
657
+ pendingHidden.delete(textNode);
658
+ if (shown.hasAttribute("data-prepr-encoded")) return false;
659
+ tagEncodedElement(shown, info.decoded);
660
+ return true;
661
+ }
626
662
  try {
627
663
  const { cleaned: stripped, encoded } = vercelStegaSplit(textContent);
628
664
  if (!encoded || stripped === textContent) return false;
629
665
  const decoded = decodeStega(textContent);
630
666
  if (!decoded?.href) return false;
631
- cleaned.set(textNode, { original: textContent, cleaned: stripped });
632
- textNode.textContent = stripped;
633
- let target = textNode.parentElement;
634
- const editTargetParent = textNode.parentElement?.closest(
635
- "[data-prepr-edit-target]"
636
- );
637
- if (editTargetParent) target = editTargetParent;
667
+ cleaned.set(textNode, {
668
+ original: textContent,
669
+ cleaned: stripped,
670
+ decoded
671
+ });
672
+ if (stripText) textNode.textContent = stripped;
673
+ const target = resolveEditTarget(textNode);
638
674
  if (target) {
675
+ pendingHidden.delete(textNode);
639
676
  tagEncodedElement(target, decoded);
640
677
  return true;
641
678
  }
679
+ pendingHidden.add(textNode);
642
680
  } catch (error) {
643
681
  debug6.log("error cleaning text node", error);
644
682
  }
@@ -647,8 +685,10 @@ function createStegaAutoClean() {
647
685
  function scanAndClean() {
648
686
  processing = true;
649
687
  walkTextNodes(document.body, cleanTextNode);
650
- const title = vercelStegaSplit(document.title);
651
- if (title.encoded) document.title = title.cleaned;
688
+ if (stripText) {
689
+ const title = vercelStegaSplit(document.title);
690
+ if (title.encoded) document.title = title.cleaned;
691
+ }
652
692
  processing = false;
653
693
  }
654
694
  function collectAffected(mutations) {
@@ -673,15 +713,37 @@ function createStegaAutoClean() {
673
713
  });
674
714
  return affected;
675
715
  }
716
+ function retryPendingHidden() {
717
+ if (pendingHidden.size === 0) return;
718
+ processing = true;
719
+ [...pendingHidden].forEach((textNode) => {
720
+ if (!textNode.isConnected) {
721
+ pendingHidden.delete(textNode);
722
+ return;
723
+ }
724
+ cleanTextNode(textNode);
725
+ });
726
+ processing = false;
727
+ }
676
728
  function handleMutations(mutations) {
677
729
  if (processing) return;
678
730
  const affected = collectAffected(mutations);
679
- if (affected.size === 0) return;
731
+ if (affected.size === 0) {
732
+ retryPendingHidden();
733
+ return;
734
+ }
735
+ affected.forEach((textNode) => pendingDirty.add(textNode));
680
736
  if (debounceTimeout) clearTimeout(debounceTimeout);
681
737
  debounceTimeout = setTimeout(() => {
738
+ debounceTimeout = null;
682
739
  processing = true;
683
- affected.forEach((textNode) => cleanTextNode(textNode));
740
+ const batch = [...pendingDirty];
741
+ pendingDirty.clear();
742
+ batch.forEach((textNode) => {
743
+ if (textNode.isConnected) cleanTextNode(textNode);
744
+ });
684
745
  processing = false;
746
+ retryPendingHidden();
685
747
  }, 50);
686
748
  }
687
749
  function setupObserver() {
@@ -689,7 +751,11 @@ function createStegaAutoClean() {
689
751
  observer.observe(document.body, {
690
752
  childList: true,
691
753
  subtree: true,
692
- characterData: true
754
+ characterData: true,
755
+ // A dropdown opening only toggles a class or inline style; without these
756
+ // the nodes it reveals are never revisited and stay untagged.
757
+ attributes: true,
758
+ attributeFilter: ["class", "style", "hidden", "open", "aria-expanded"]
693
759
  });
694
760
  }
695
761
  function start() {
@@ -708,6 +774,8 @@ function createStegaAutoClean() {
708
774
  observer = null;
709
775
  }
710
776
  cleaned = /* @__PURE__ */ new WeakMap();
777
+ pendingHidden = /* @__PURE__ */ new Set();
778
+ pendingDirty = /* @__PURE__ */ new Set();
711
779
  debug6.log("stega auto-clean stopped");
712
780
  }
713
781
  return { start, stop };
@@ -1031,7 +1099,7 @@ var CLOSE_ICON_SVG = `<svg width="10" height="10" viewBox="0 0 10 10" fill="none
1031
1099
  </svg>`;
1032
1100
 
1033
1101
  // src/core/ui/toolbar.generated.ts
1034
- 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: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}}";
1102
+ 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}}";
1035
1103
 
1036
1104
  // ../../node_modules/.pnpm/preact@10.29.7/node_modules/preact/jsx-runtime/dist/jsxRuntime.mjs
1037
1105
  var f2 = 0;
@@ -1290,13 +1358,17 @@ function StatusPill({
1290
1358
  } else {
1291
1359
  segmentLabel = defaultSegmentName;
1292
1360
  }
1361
+ const dirty = features.segments && state.selectedSegment !== null || features.abTesting && state.selectedVariant === "B";
1362
+ const interactive = !state.previewMode || dirty;
1293
1363
  return /* @__PURE__ */ u2(
1294
1364
  "button",
1295
1365
  {
1296
1366
  type: "button",
1297
1367
  class: "prepr-status-pill",
1298
1368
  "data-prepr": "status-pill",
1369
+ "data-interactive": String(interactive),
1299
1370
  hidden: state.isIframe,
1371
+ disabled: !interactive,
1300
1372
  onClick: () => handlers.onStatusPill(),
1301
1373
  children: [
1302
1374
  /* @__PURE__ */ u2("span", { class: "prepr-status-viewing", "data-prepr": "status-viewing", children: t3("common.viewingAs") }),
@@ -1315,7 +1387,7 @@ function StatusPill({
1315
1387
  {
1316
1388
  class: "prepr-status-x",
1317
1389
  "data-prepr": "status-x",
1318
- hidden: !state.previewMode || !(features.segments && state.selectedSegment !== null || features.abTesting && state.selectedVariant === "B"),
1390
+ hidden: !state.previewMode || !dirty,
1319
1391
  children: /* @__PURE__ */ u2(RawSvg, { svg: XMARK_ICON_SVG })
1320
1392
  }
1321
1393
  )
@@ -2119,7 +2191,9 @@ function createPreprPreview(opts = {}) {
2119
2191
  }
2120
2192
  }
2121
2193
  });
2122
- const autoClean = createStegaAutoClean();
2194
+ const autoClean = createStegaAutoClean({
2195
+ enabled: options?.autoClean
2196
+ });
2123
2197
  autoClean.start();
2124
2198
  const handleChange = createChangeHandler({
2125
2199
  store,
@@ -2165,7 +2239,7 @@ function createPreprPreview(opts = {}) {
2165
2239
  }
2166
2240
 
2167
2241
  // src/core/pixel.ts
2168
- var PIXEL_SCRIPT_URL = "https://cdn.tracking.prepr.io/js/prepr-v2.min.js";
2242
+ var PIXEL_SCRIPT_URL = "https://cdn.tracking.prepr.io/js/prepr.min.js";
2169
2243
  var CACHE_BUST_INTERVAL_MS = 24 * 60 * 60 * 1e3;
2170
2244
  var warnedNoPixel = false;
2171
2245
  function warnNoPixelOnce() {