@preprio/toolkit 0.1.0-beta.5 → 0.2.0-beta.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.
Files changed (36) hide show
  1. package/README.md +99 -14
  2. package/dist/astro/index.cjs +37 -15
  3. package/dist/astro/index.d.cts +38 -8
  4. package/dist/astro/index.d.ts +38 -8
  5. package/dist/astro/index.js +37 -15
  6. package/dist/{chunk-NLQIRN5O.js → chunk-6OOGNVJ4.js} +83 -41
  7. package/dist/index.cjs +102 -55
  8. package/dist/index.d.cts +59 -16
  9. package/dist/index.d.ts +59 -16
  10. package/dist/index.js +22 -16
  11. package/dist/nextjs/components.cjs +87 -42
  12. package/dist/nextjs/components.d.cts +3 -3
  13. package/dist/nextjs/components.d.ts +3 -3
  14. package/dist/nextjs/components.js +8 -4
  15. package/dist/nextjs/image-loader.cjs +17 -94
  16. package/dist/nextjs/image-loader.d.cts +8 -4
  17. package/dist/nextjs/image-loader.d.ts +8 -4
  18. package/dist/nextjs/image-loader.js +17 -94
  19. package/dist/nextjs/index.cjs +37 -15
  20. package/dist/nextjs/index.d.cts +46 -16
  21. package/dist/nextjs/index.d.ts +46 -16
  22. package/dist/nextjs/index.js +37 -15
  23. package/dist/nuxt/index.cjs +37 -15
  24. package/dist/nuxt/index.d.cts +62 -32
  25. package/dist/nuxt/index.d.ts +62 -32
  26. package/dist/nuxt/index.js +37 -15
  27. package/dist/{pixel-DVSJQC0K.d.cts → pixel-763p7m6A.d.cts} +64 -1
  28. package/dist/{pixel-DVSJQC0K.d.ts → pixel-763p7m6A.d.ts} +64 -1
  29. package/dist/sveltekit/index.cjs +37 -15
  30. package/dist/sveltekit/index.d.cts +62 -32
  31. package/dist/sveltekit/index.d.ts +62 -32
  32. package/dist/sveltekit/index.js +37 -15
  33. package/package.json +1 -1
  34. package/src/astro/PreprToolbar.astro +16 -10
  35. package/src/nuxt/PreprToolbar.vue +10 -8
  36. package/src/sveltekit/PreprToolbar.svelte +11 -4
package/dist/index.d.ts CHANGED
@@ -1,11 +1,18 @@
1
- import { L as Locale, P as PreprSegment, a as PreprVariant, b as PreprToolbarProps, c as PreprToolbarOptions, d as PreprErrorCode, e as PreprHeaders } from './pixel-DVSJQC0K.js';
2
- export { f as PreprEnvironment, g as PreprEventType, h as PreprHeaderName, i as PreprPixelConfig, l as loadTrackingPixel, s as setTrackingParam, t as trackEvent } from './pixel-DVSJQC0K.js';
1
+ import { P as PreprFeatures, R as ResolvedPreprFeatures, L as Locale, a as PreprSegment, b as PreprVariant, c as PreprToolbarProps, d as PreprPreviewOptions, e as PreprErrorCode, f as PreprHeaders } from './pixel-763p7m6A.js';
2
+ export { g as PreprEnvironment, h as PreprEventType, i as PreprFeatureConfig, j as PreprHeaderName, k as PreprPixelConfig, l as PreprToolbarComponentProps, m as PreprToolbarOptions, n as loadTrackingPixel, s as setTrackingParam, t as trackEvent } from './pixel-763p7m6A.js';
3
3
 
4
4
  /**
5
5
  * Package version; must match package.json. Lives in its own leaf module so
6
6
  * core can import it without cycling through the barrel.
7
7
  */
8
- declare const VERSION = "0.1.0-beta.5";
8
+ declare const VERSION = "0.2.0-beta.2";
9
+
10
+ /**
11
+ * Resolve a `PreprFeatures` bag to plain booleans, defaulting every omitted key
12
+ * to enabled. Shared by the toolbar and the middleware so both sides agree on
13
+ * what a given config means.
14
+ */
15
+ declare function resolveFeatures(features?: PreprFeatures): ResolvedPreprFeatures;
9
16
 
10
17
  interface ToolbarState {
11
18
  locale: Locale;
@@ -16,6 +23,11 @@ interface ToolbarState {
16
23
  previewMode: boolean;
17
24
  toolbarOpen: boolean;
18
25
  isIframe: boolean;
26
+ /**
27
+ * Set once at construction, never patched. Lives here so the panel and the
28
+ * element read it the same way they read everything else.
29
+ */
30
+ features: ResolvedPreprFeatures;
19
31
  }
20
32
  interface ToolbarStore {
21
33
  get(): ToolbarState;
@@ -25,7 +37,7 @@ interface ToolbarStore {
25
37
  /**
26
38
  * Framework-free store for toolbar state. Deliberately pure: it holds state and
27
39
  * notifies subscribers, nothing else. Cookies, postMessage and reloads are side
28
- * effects owned by the mount controller (create-toolbar.ts).
40
+ * effects owned by the mount controller (create-preview.ts).
29
41
  */
30
42
  declare function createToolbarStore(initial?: Partial<ToolbarState>): ToolbarStore;
31
43
 
@@ -45,21 +57,32 @@ interface PreprNavigationAdapter {
45
57
  */
46
58
  reload?(): void;
47
59
  }
48
- interface CreatePreprToolbarOptions {
49
- props: PreprToolbarProps;
50
- options?: PreprToolbarOptions;
60
+ interface CreatePreprPreviewOptions {
61
+ /**
62
+ * Toolbar data. Optional: a headless preview (`ui: false`) that only wants
63
+ * click-to-edit or scroll restore has no segment list to pass.
64
+ */
65
+ props?: PreprToolbarProps;
66
+ options?: PreprPreviewOptions;
51
67
  navigation?: PreprNavigationAdapter;
52
68
  }
53
- interface PreprToolbarController {
69
+ interface PreprPreviewController {
54
70
  destroy(): void;
55
71
  }
56
72
  /**
57
- * Composition root: hydrates state from props + cookies, mounts
58
- * `<prepr-toolbar>`, and wires the collaborators — the change handler
59
- * (toolbar-change-handler.ts), the stega click-to-edit controller, the
60
- * auto-clean pass and the editor bridge (iframe-bridge.ts).
73
+ * Composition root for the Prepr preview runtime: hydrates state from props +
74
+ * cookies, optionally mounts `<prepr-toolbar>`, and wires the collaborators —
75
+ * the change handler (toolbar-change-handler.ts), the stega click-to-edit
76
+ * controller, the auto-clean pass and the editor bridge (iframe-bridge.ts).
77
+ *
78
+ * Two independent axes control what runs:
79
+ * - `options.features` — which features are active at all (segments, A/B,
80
+ * click-to-edit).
81
+ * - `options.ui` — whether the visible toolbar is mounted. `ui: false` keeps
82
+ * every non-visual side effect wired, which is how a headless preview gets
83
+ * click-to-edit or editor scroll restore with no chrome of its own.
61
84
  */
62
- declare function createPreprToolbar(opts: CreatePreprToolbarOptions): PreprToolbarController;
85
+ declare function createPreprPreview(opts?: CreatePreprPreviewOptions): PreprPreviewController;
63
86
 
64
87
  type Translate = (key: string) => string;
65
88
  declare const HTMLElementBase: typeof HTMLElement;
@@ -69,7 +92,7 @@ declare const HTMLElementBase: typeof HTMLElement;
69
92
  *
70
93
  * This element is the controller: it owns interaction state (listbox open,
71
94
  * keyboard nav, tooltip positioning, outside-click) and writes back ONLY via
72
- * `store.set(...)`. Cookies/postMessage/navigation belong to create-toolbar.ts,
95
+ * `store.set(...)`. Cookies/postMessage/navigation belong to create-preview.ts,
73
96
  * which subscribes to the same store.
74
97
  *
75
98
  * State reaches `<Panel>` as a prop rather than internal Preact state, so every
@@ -87,6 +110,8 @@ declare class PreprToolbarElement extends HTMLElementBase {
87
110
  disconnectedCallback(): void;
88
111
  private get state();
89
112
  private set;
113
+ /** Reset patch covering only the enabled personalization features. */
114
+ private resetPatch;
90
115
  private renderPanel;
91
116
  private tip;
92
117
  /**
@@ -112,6 +137,14 @@ declare class PreprToolbarElement extends HTMLElementBase {
112
137
  /** Idempotent, including across bundles that each try to define the element. */
113
138
  declare function definePreprToolbar(): void;
114
139
 
140
+ interface IframeBridgeOptions {
141
+ /**
142
+ * Replace the `*.prepr.io` wildcard with an exact origin list. Intended for
143
+ * self-hosted editors; when set, the wildcard no longer applies.
144
+ */
145
+ allowedEditorOrigins?: string[];
146
+ }
147
+
115
148
  /** Error thrown by the token helpers and the segments fetch. */
116
149
  declare class PreprError extends Error {
117
150
  readonly code: PreprErrorCode;
@@ -152,8 +185,12 @@ declare function getPreprEnvironmentSegments(token: string): Promise<PreprSegmen
152
185
  * Never throws: a failed segments fetch yields an empty list so the toolbar
153
186
  * still renders. The list is returned under both `segments` and the deprecated
154
187
  * `data` alias.
188
+ *
189
+ * Pass the same `features` object you give the toolbar and the middleware: with
190
+ * segments disabled the `_Segments` request is skipped entirely, saving a
191
+ * round-trip per page.
155
192
  */
156
- declare function getToolbarPropsFromHeaders(headers: Headers, token: string): Promise<PreprToolbarProps>;
193
+ declare function getToolbarPropsFromHeaders(headers: Headers, token: string, features?: PreprFeatures): Promise<PreprToolbarProps>;
157
194
 
158
195
  interface CookieSpec {
159
196
  name: string;
@@ -177,6 +214,12 @@ interface PreprMiddlewareResult {
177
214
  interface PreprMiddlewareOptions {
178
215
  /** Enable preview-mode headers (segment/AB overrides, preview bar). */
179
216
  preview?: boolean;
217
+ /**
218
+ * Disable features app-wide. Pass the same object you give the toolbar: a
219
+ * disabled feature injects no header and persists no cookie, whatever the
220
+ * request carries.
221
+ */
222
+ features?: PreprFeatures;
180
223
  /** Override the version reported in Prepr-Package (mainly for tests). */
181
224
  version?: string;
182
225
  }
@@ -187,4 +230,4 @@ interface PreprMiddlewareOptions {
187
230
  */
188
231
  declare function processPreprRequest(request: Request, opts?: PreprMiddlewareOptions): PreprMiddlewareResult;
189
232
 
190
- export { type CookieSpec, type CreatePreprToolbarOptions, PreprError, PreprErrorCode, PreprHeaders, type PreprMiddlewareOptions, type PreprMiddlewareResult, type PreprNavigationAdapter, PreprSegment, type PreprToolbarController, PreprToolbarElement, PreprToolbarOptions, PreprToolbarProps, PreprVariant, type ToolbarState, type ToolbarStore, VERSION, createPreprToolbar, createToolbarStore, definePreprToolbar, extractAccessToken, getActiveSegmentFromHeaders, getActiveVariantFromHeaders, getPreprEnvironmentSegments, getPreprHeadersFromHeaders, getPreprUUIDFromHeaders, getToolbarPropsFromHeaders, processPreprRequest, stegaClean, validatePreprToken };
233
+ export { type CookieSpec, type CreatePreprPreviewOptions, type IframeBridgeOptions, PreprError, PreprErrorCode, PreprFeatures, PreprHeaders, type PreprMiddlewareOptions, type PreprMiddlewareResult, type PreprNavigationAdapter, type PreprPreviewController, PreprPreviewOptions, PreprSegment, PreprToolbarElement, PreprToolbarProps, PreprVariant, ResolvedPreprFeatures, type ToolbarState, type ToolbarStore, VERSION, createPreprPreview, createToolbarStore, definePreprToolbar, extractAccessToken, getActiveSegmentFromHeaders, getActiveVariantFromHeaders, getPreprEnvironmentSegments, getPreprHeadersFromHeaders, getPreprUUIDFromHeaders, getToolbarPropsFromHeaders, processPreprRequest, resolveFeatures, stegaClean, validatePreprToken };
package/dist/index.js CHANGED
@@ -9,17 +9,18 @@ import {
9
9
  PARAM_SEGMENT,
10
10
  PARAM_VARIANT,
11
11
  PreprToolbarElement,
12
- createPreprToolbar,
12
+ createPreprPreview,
13
13
  createToolbarStore,
14
14
  definePreprToolbar,
15
15
  loadTrackingPixel,
16
+ resolveFeatures,
16
17
  setTrackingParam,
17
18
  stegaClean,
18
19
  trackEvent
19
- } from "./chunk-NLQIRN5O.js";
20
+ } from "./chunk-6OOGNVJ4.js";
20
21
 
21
22
  // src/version.ts
22
- var VERSION = "0.1.0-beta.5";
23
+ var VERSION = "0.2.0-beta.2";
23
24
 
24
25
  // src/core/server.ts
25
26
  var PreprError = class extends Error {
@@ -157,16 +158,19 @@ async function getPreprEnvironmentSegments(token) {
157
158
  );
158
159
  }
159
160
  }
160
- async function getToolbarPropsFromHeaders(headers, token) {
161
+ async function getToolbarPropsFromHeaders(headers, token, features) {
162
+ const resolved = resolveFeatures(features);
161
163
  let segments = [];
162
- try {
163
- segments = await getPreprEnvironmentSegments(token);
164
- } catch {
165
- segments = [];
164
+ if (resolved.segments) {
165
+ try {
166
+ segments = await getPreprEnvironmentSegments(token);
167
+ } catch {
168
+ segments = [];
169
+ }
166
170
  }
167
171
  return {
168
- activeSegment: getActiveSegmentFromHeaders(headers),
169
- activeVariant: getActiveVariantFromHeaders(headers),
172
+ activeSegment: resolved.segments ? getActiveSegmentFromHeaders(headers) : null,
173
+ activeVariant: resolved.abTesting ? getActiveVariantFromHeaders(headers) : null,
170
174
  segments,
171
175
  data: segments
172
176
  };
@@ -241,7 +245,8 @@ function processPreprRequest(request, opts) {
241
245
  if (!opts?.preview) {
242
246
  return { requestHeaders, responseCookies };
243
247
  }
244
- const hasPreviewParams = searchParams.has(PARAM_SEGMENT) || searchParams.has(PARAM_VARIANT);
248
+ const features = resolveFeatures(opts?.features);
249
+ const hasPreviewParams = features.segments && searchParams.has(PARAM_SEGMENT) || features.abTesting && searchParams.has(PARAM_VARIANT);
245
250
  if (!hasPreviewParams) {
246
251
  const previewModeCookie = requestCookies.get(COOKIE_PREVIEW_MODE);
247
252
  const previewModeEnabled = previewModeCookie !== "false";
@@ -261,16 +266,16 @@ function processPreprRequest(request, opts) {
261
266
  });
262
267
  }
263
268
  if (!isLivePreview) {
264
- const segmentCookie = requestCookies.get(COOKIE_SEGMENT);
269
+ const segmentCookie = features.segments ? requestCookies.get(COOKIE_SEGMENT) : void 0;
265
270
  if (segmentCookie) {
266
271
  requestHeaders.set("Prepr-Segments", segmentCookie);
267
272
  }
268
- const abCookie = requestCookies.get(COOKIE_VARIANT);
273
+ const abCookie = features.abTesting ? requestCookies.get(COOKIE_VARIANT) : void 0;
269
274
  if (abCookie) {
270
275
  requestHeaders.set("Prepr-ABtesting", abCookie);
271
276
  }
272
277
  }
273
- const previewAb = searchParams.get(PARAM_VARIANT);
278
+ const previewAb = features.abTesting ? searchParams.get(PARAM_VARIANT) : null;
274
279
  if (previewAb !== null) {
275
280
  requestHeaders.set("Prepr-ABtesting", previewAb);
276
281
  if (!isLivePreview) {
@@ -283,7 +288,7 @@ function processPreprRequest(request, opts) {
283
288
  });
284
289
  }
285
290
  }
286
- const previewSegment = searchParams.get(PARAM_SEGMENT);
291
+ const previewSegment = features.segments ? searchParams.get(PARAM_SEGMENT) : null;
287
292
  if (previewSegment !== null) {
288
293
  requestHeaders.set("Prepr-Segments", previewSegment);
289
294
  if (!isLivePreview) {
@@ -302,7 +307,7 @@ export {
302
307
  PreprError,
303
308
  PreprToolbarElement,
304
309
  VERSION,
305
- createPreprToolbar,
310
+ createPreprPreview,
306
311
  createToolbarStore,
307
312
  definePreprToolbar,
308
313
  extractAccessToken,
@@ -314,6 +319,7 @@ export {
314
319
  getToolbarPropsFromHeaders,
315
320
  loadTrackingPixel,
316
321
  processPreprRequest,
322
+ resolveFeatures,
317
323
  setTrackingParam,
318
324
  stegaClean,
319
325
  trackEvent,
@@ -78,6 +78,24 @@ function crossSiteCookieOptions() {
78
78
  return { sameSite: "None", secure: true };
79
79
  }
80
80
 
81
+ // src/core/features.ts
82
+ function isEnabled(config) {
83
+ if (typeof config === "boolean") return config;
84
+ return config?.enabled ?? true;
85
+ }
86
+ function resolveFeatures(features) {
87
+ return {
88
+ segments: isEnabled(features?.segments),
89
+ abTesting: isEnabled(features?.abTesting),
90
+ editMode: isEnabled(features?.editMode)
91
+ };
92
+ }
93
+ var ALL_FEATURES_ENABLED = Object.freeze({
94
+ segments: true,
95
+ abTesting: true,
96
+ editMode: true
97
+ });
98
+
81
99
  // src/core/locales/en.json
82
100
  var en_default = {
83
101
  common: {
@@ -307,7 +325,7 @@ function createIframeBridge(store, options = {}) {
307
325
  const top = data.scrollPosition;
308
326
  setTimeout(() => window.scrollTo(0, top), 1);
309
327
  }
310
- store.set({ previewMode: true, editMode: data.editMode ?? true });
328
+ store?.set({ previewMode: true, editMode: data.editMode ?? true });
311
329
  }
312
330
  if (!parentOrigin || evt.origin !== parentOrigin) return;
313
331
  if (data?.event === "prepr:getScrollPosition") {
@@ -929,7 +947,8 @@ var DEFAULT_STATE = {
929
947
  editMode: false,
930
948
  previewMode: false,
931
949
  toolbarOpen: false,
932
- isIframe: false
950
+ isIframe: false,
951
+ features: ALL_FEATURES_ENABLED
933
952
  };
934
953
  function createToolbarStore(initial = {}) {
935
954
  let state = { ...DEFAULT_STATE, ...initial };
@@ -964,7 +983,7 @@ function cookieOpts() {
964
983
  };
965
984
  }
966
985
  function createChangeHandler(deps) {
967
- const { store, stega, syncAutoClean } = deps;
986
+ const { store, stega, syncAutoClean, features, editingEnabled } = deps;
968
987
  function updateParams(patch) {
969
988
  const [path, existing] = splitPath(deps.currentPath());
970
989
  const params = new URLSearchParams(existing);
@@ -980,7 +999,7 @@ function createChangeHandler(deps) {
980
999
  }
981
1000
  return function handleChange(before, after) {
982
1001
  const paramPatch = {};
983
- if (before.selectedSegment !== after.selectedSegment) {
1002
+ if (features.segments && before.selectedSegment !== after.selectedSegment) {
984
1003
  if (after.selectedSegment === null) {
985
1004
  removeCookie(COOKIE_SEGMENT, "/", crossSiteCookieOptions());
986
1005
  } else {
@@ -991,7 +1010,7 @@ function createChangeHandler(deps) {
991
1010
  segment: after.selectedSegment ?? void 0
992
1011
  });
993
1012
  }
994
- if (before.selectedVariant !== after.selectedVariant) {
1013
+ if (features.abTesting && before.selectedVariant !== after.selectedVariant) {
995
1014
  if (after.selectedVariant === null) {
996
1015
  removeCookie(COOKIE_VARIANT, "/", crossSiteCookieOptions());
997
1016
  } else {
@@ -1006,7 +1025,7 @@ function createChangeHandler(deps) {
1006
1025
  updateParams(paramPatch);
1007
1026
  }
1008
1027
  if (before.editMode !== after.editMode) {
1009
- if (after.editMode) {
1028
+ if (after.editMode && editingEnabled) {
1010
1029
  stega.start();
1011
1030
  } else {
1012
1031
  stega.stop();
@@ -1577,9 +1596,10 @@ function StatusPill({
1577
1596
  t: t3,
1578
1597
  handlers
1579
1598
  }) {
1599
+ const { features } = state;
1580
1600
  const defaultSegmentName = state.segments.find((s2) => s2._id === "all_other_users")?.name ?? t3("adaptiveContent.allOtherUsers");
1581
1601
  let segmentLabel;
1582
- if (!state.previewMode) {
1602
+ if (!state.previewMode || !features.segments) {
1583
1603
  segmentLabel = t3("common.user");
1584
1604
  } else if (state.selectedSegment !== null) {
1585
1605
  const seg = state.segments.find((s2) => s2._id === state.selectedSegment);
@@ -1603,7 +1623,7 @@ function StatusPill({
1603
1623
  {
1604
1624
  class: "prepr-status-variant",
1605
1625
  "data-prepr": "status-variant",
1606
- hidden: !state.previewMode,
1626
+ hidden: !features.abTesting || !state.previewMode,
1607
1627
  children: state.selectedVariant === "B" ? "B" : "A"
1608
1628
  }
1609
1629
  ),
@@ -1612,7 +1632,7 @@ function StatusPill({
1612
1632
  {
1613
1633
  class: "prepr-status-x",
1614
1634
  "data-prepr": "status-x",
1615
- hidden: !state.previewMode || state.selectedSegment === null,
1635
+ hidden: !features.segments || !state.previewMode || state.selectedSegment === null,
1616
1636
  children: /* @__PURE__ */ u2(RawSvg, { svg: XMARK_ICON_SVG })
1617
1637
  }
1618
1638
  )
@@ -1632,7 +1652,7 @@ function CloseEditPill({
1632
1652
  class: "prepr-close-edit-pill",
1633
1653
  "data-prepr": "close-edit-pill",
1634
1654
  "aria-label": t3("editingTools.ariaCloseEditMode"),
1635
- hidden: !state.editMode || state.isIframe,
1655
+ hidden: !state.features.editMode || !state.editMode || state.isIframe,
1636
1656
  onClick: () => handlers.onCloseEditPill(),
1637
1657
  children: [
1638
1658
  /* @__PURE__ */ u2("span", { "data-prepr": "close-edit-label", children: t3("editingTools.editMode") }),
@@ -1648,7 +1668,9 @@ function Panel({
1648
1668
  segmentFilter,
1649
1669
  handlers
1650
1670
  }) {
1651
- const hasPersonalization = state.selectedSegment !== null || state.selectedVariant !== null;
1671
+ const { features } = state;
1672
+ const hasPersonalization = features.segments && state.selectedSegment !== null || features.abTesting && state.selectedVariant !== null;
1673
+ const showAdaptive = features.segments || features.abTesting;
1652
1674
  return /* @__PURE__ */ u2(S, { children: [
1653
1675
  /* @__PURE__ */ u2(
1654
1676
  "style",
@@ -1691,12 +1713,12 @@ function Panel({
1691
1713
  children: [
1692
1714
  /* @__PURE__ */ u2(Header, { t: t3, onClose: handlers.onClose }),
1693
1715
  /* @__PURE__ */ u2("div", { class: "prepr-section", "data-prepr": "section-adaptive", children: [
1694
- /* @__PURE__ */ u2("span", { class: "prepr-section-label", "data-prepr": "adaptive-label", children: t3("adaptiveContent.adaptiveContent") }),
1716
+ /* @__PURE__ */ u2("span", { class: "prepr-section-label", "data-prepr": "adaptive-label", children: showAdaptive ? t3("adaptiveContent.adaptiveContent") : t3("adaptiveContent.enablePreview") }),
1695
1717
  /* @__PURE__ */ u2("div", { class: "prepr-row", children: [
1696
1718
  /* @__PURE__ */ u2("h2", { class: "prepr-row-title", "data-prepr": "preview-label", children: t3("adaptiveContent.enablePreview") }),
1697
1719
  /* @__PURE__ */ u2(PreviewSelector, { state, t: t3, handlers })
1698
1720
  ] }),
1699
- /* @__PURE__ */ u2("div", { class: "prepr-row", children: [
1721
+ features.segments && /* @__PURE__ */ u2("div", { class: "prepr-row", children: [
1700
1722
  /* @__PURE__ */ u2("h2", { class: "prepr-row-title", "data-prepr": "segment-label", children: t3("adaptiveContent.segment") }),
1701
1723
  /* @__PURE__ */ u2(
1702
1724
  SegmentListbox,
@@ -1709,12 +1731,12 @@ function Panel({
1709
1731
  }
1710
1732
  )
1711
1733
  ] }),
1712
- /* @__PURE__ */ u2("div", { class: "prepr-row", children: [
1734
+ features.abTesting && /* @__PURE__ */ u2("div", { class: "prepr-row", children: [
1713
1735
  /* @__PURE__ */ u2("h2", { class: "prepr-row-title", "data-prepr": "variant-label", children: t3("adaptiveContent.ABVariant") }),
1714
1736
  /* @__PURE__ */ u2(VariantSelector, { state, handlers })
1715
1737
  ] })
1716
1738
  ] }),
1717
- /* @__PURE__ */ u2("div", { class: "prepr-section", "data-prepr": "section-editing", children: [
1739
+ features.editMode && /* @__PURE__ */ u2("div", { class: "prepr-section", "data-prepr": "section-editing", children: [
1718
1740
  /* @__PURE__ */ u2("span", { class: "prepr-section-label", "data-prepr": "editing-label", children: t3("editingTools.editingTools") }),
1719
1741
  /* @__PURE__ */ u2("div", { class: "prepr-row", children: [
1720
1742
  /* @__PURE__ */ u2("h2", { class: "prepr-row-title", "data-prepr": "edit-label", children: t3("editingTools.editMode") }),
@@ -1767,26 +1789,28 @@ var PreprToolbarElement = class extends HTMLElementBase {
1767
1789
  onToggle: () => this.set({ toolbarOpen: !this.state.toolbarOpen }),
1768
1790
  onClose: () => this.set({ toolbarOpen: false }),
1769
1791
  onPreviewMode: (value) => this.set({ previewMode: value }),
1770
- onEditMode: (value) => this.set({ editMode: value }),
1792
+ onEditMode: (value) => {
1793
+ if (!this.state.features.editMode) return;
1794
+ this.set({ editMode: value });
1795
+ },
1771
1796
  onVariant: (value) => {
1797
+ if (!this.state.features.abTesting) return;
1772
1798
  if (!this.state.previewMode) return;
1773
1799
  this.set({ selectedVariant: value });
1774
1800
  },
1775
1801
  // Reset contract: segment to none, variant to 'A' — NOT null, since the
1776
1802
  // resulting `variant_changed` must carry 'A' on the wire — edit mode off.
1777
- onReset: () => this.set({
1778
- selectedSegment: null,
1779
- selectedVariant: "A",
1780
- editMode: false
1781
- }),
1803
+ // Disabled features are left untouched: their state is already inert and
1804
+ // writing it would emit events for a feature the consumer turned off.
1805
+ onReset: () => this.set(this.resetPatch({ editMode: false })),
1782
1806
  onStatusPill: () => {
1783
1807
  if (!this.state.previewMode) {
1784
1808
  this.set({ previewMode: true });
1785
1809
  return;
1786
1810
  }
1787
- if (this.state.selectedSegment !== null || this.state.selectedVariant !== "A") {
1788
- this.set({ selectedSegment: null, selectedVariant: "A" });
1789
- }
1811
+ const patch = this.resetPatch();
1812
+ const dirty = "selectedSegment" in patch && this.state.selectedSegment !== null || "selectedVariant" in patch && this.state.selectedVariant !== "A";
1813
+ if (dirty) this.set(patch);
1790
1814
  },
1791
1815
  onCloseEditPill: () => this.set({ editMode: false }),
1792
1816
  onSegmentButtonClick: () => this.toggleListbox(!this.listboxOpen),
@@ -1836,6 +1860,14 @@ var PreprToolbarElement = class extends HTMLElementBase {
1836
1860
  set(patch) {
1837
1861
  this.store?.set(patch);
1838
1862
  }
1863
+ /** Reset patch covering only the enabled personalization features. */
1864
+ resetPatch(base = {}) {
1865
+ const { features } = this.state;
1866
+ const patch = { ...base };
1867
+ if (features.segments) patch.selectedSegment = null;
1868
+ if (features.abTesting) patch.selectedVariant = "A";
1869
+ return patch;
1870
+ }
1839
1871
  // Synchronous: Preact's top-level render() mutates the DOM before returning.
1840
1872
  renderPanel(state) {
1841
1873
  const root = this.shadowRoot;
@@ -1994,6 +2026,7 @@ var PreprToolbarElement = class extends HTMLElementBase {
1994
2026
  ) ?? null;
1995
2027
  }
1996
2028
  chooseSegment(id) {
2029
+ if (!this.state.features.segments) return;
1997
2030
  this.listboxOpen = false;
1998
2031
  this.segmentFilter = "";
1999
2032
  this.set({ selectedSegment: id });
@@ -2008,8 +2041,8 @@ function definePreprToolbar() {
2008
2041
  }
2009
2042
  }
2010
2043
 
2011
- // src/core/create-toolbar.ts
2012
- var debug7 = createScopedLogger("create-toolbar");
2044
+ // src/core/create-preview.ts
2045
+ var debug7 = createScopedLogger("create-preview");
2013
2046
  var controllerStores = /* @__PURE__ */ new WeakMap();
2014
2047
  function defaultNavigation() {
2015
2048
  return {
@@ -2030,7 +2063,7 @@ function resolveLocale(options) {
2030
2063
  }
2031
2064
  return "en";
2032
2065
  }
2033
- function createPreprToolbar(opts) {
2066
+ function createPreprPreview(opts = {}) {
2034
2067
  const { props, options } = opts;
2035
2068
  const navigation = opts.navigation ?? defaultNavigation();
2036
2069
  initDebugLogger(options?.debug ?? false);
@@ -2043,36 +2076,40 @@ function createPreprToolbar(opts) {
2043
2076
  const search = new URLSearchParams(window.location.search);
2044
2077
  const hideBar = search.get(PARAM_HIDE_BAR) === "true";
2045
2078
  if (hideBar) {
2046
- debug7.log(`${PARAM_HIDE_BAR}=true \u2014 skipping mount`);
2047
- return noop();
2079
+ debug7.log(`${PARAM_HIDE_BAR}=true \u2014 no visible toolbar`);
2048
2080
  }
2081
+ const mountUi = (options?.ui ?? true) && !isIframe && !hideBar;
2049
2082
  const locale = resolveLocale(options);
2083
+ const features = resolveFeatures(options?.features);
2050
2084
  const previewMode = getCookie(COOKIE_PREVIEW_MODE) === "true";
2051
2085
  const toolbarOpen = getCookie(COOKIE_TOOLBAR_OPEN) === "true";
2052
- const cookieSegment = getCookie(COOKIE_SEGMENT);
2053
- const cookieVariant = getCookie(COOKIE_VARIANT);
2054
- const rawVariant = props.activeVariant ?? cookieVariant;
2086
+ const cookieSegment = features.segments ? getCookie(COOKIE_SEGMENT) : null;
2087
+ const cookieVariant = features.abTesting ? getCookie(COOKIE_VARIANT) : null;
2088
+ const rawVariant = features.abTesting ? props?.activeVariant ?? cookieVariant : null;
2055
2089
  const selectedVariant = rawVariant === "A" || rawVariant === "B" ? rawVariant : null;
2056
2090
  const store = createToolbarStore({
2057
2091
  locale,
2058
- segments: buildSegments(props.segments ?? props.data ?? []),
2059
- selectedSegment: props.activeSegment ?? cookieSegment ?? null,
2092
+ features,
2093
+ segments: features.segments ? buildSegments(props?.segments ?? props?.data ?? []) : [],
2094
+ selectedSegment: features.segments ? props?.activeSegment ?? cookieSegment ?? null : null,
2060
2095
  selectedVariant,
2061
2096
  previewMode,
2062
2097
  toolbarOpen,
2063
2098
  isIframe
2064
2099
  });
2065
2100
  let el = null;
2066
- if (!isIframe) {
2101
+ if (mountUi) {
2067
2102
  definePreprToolbar();
2068
2103
  el = document.createElement("prepr-toolbar");
2069
2104
  document.body.appendChild(el);
2070
2105
  el.connect(store, (key) => t(key, locale));
2071
2106
  }
2107
+ const editingEnabled = features.editMode || isIframe;
2072
2108
  const stega = createStegaController({
2073
- // The CMS deep-link tooltip is noise inside the editor; clicking the
2074
- // element itself requests the edit there.
2075
- tooltip: !isIframe,
2109
+ // The CMS deep-link tooltip is chrome, so it follows the same decision as
2110
+ // the bar: noise inside the editor (clicking the element itself requests
2111
+ // the edit there), and unwanted in a headless preview that asked for no UI.
2112
+ tooltip: mountUi,
2076
2113
  // In the editor, ask the parent to focus the field instead of opening a new
2077
2114
  // tab. Standalone previews keep the window.open behaviour.
2078
2115
  onEdit: ({ href, origin, id, field }) => {
@@ -2096,6 +2133,8 @@ function createPreprToolbar(opts) {
2096
2133
  }
2097
2134
  const handleChange = createChangeHandler({
2098
2135
  store,
2136
+ features,
2137
+ editingEnabled,
2099
2138
  navigate: (url) => navigation.navigate(url),
2100
2139
  currentPath: () => navigation.currentPath(),
2101
2140
  reload: navigation.reload ?? (() => window.location.reload()),
@@ -2109,7 +2148,9 @@ function createPreprToolbar(opts) {
2109
2148
  handleChange(before, state);
2110
2149
  });
2111
2150
  syncAutoClean(store.get().previewMode);
2112
- const bridge = createIframeBridge(store);
2151
+ const bridge = createIframeBridge(store, {
2152
+ allowedEditorOrigins: options?.allowedEditorOrigins
2153
+ });
2113
2154
  sendPreprEvent("getScrollPosition", { value: 0 }, {
2114
2155
  allowUntrustedTarget: true
2115
2156
  });
@@ -2124,7 +2165,7 @@ function createPreprToolbar(opts) {
2124
2165
  bridge.stop();
2125
2166
  }
2126
2167
  el?.remove();
2127
- debug7.log("toolbar destroyed");
2168
+ debug7.log("preview destroyed");
2128
2169
  }
2129
2170
  const controller = { destroy };
2130
2171
  controllerStores.set(controller, store);
@@ -2170,12 +2211,16 @@ function loadTrackingPixel(trackingId, config) {
2170
2211
  }
2171
2212
 
2172
2213
  // src/nextjs/components.ts
2173
- function PreprToolbar(props) {
2214
+ function PreprToolbar({
2215
+ options,
2216
+ ...props
2217
+ }) {
2174
2218
  const router = (0, import_navigation.useRouter)();
2175
2219
  const pathname = (0, import_navigation.usePathname)();
2176
2220
  (0, import_react.useEffect)(() => {
2177
- const toolbar = createPreprToolbar({
2221
+ const toolbar = createPreprPreview({
2178
2222
  props,
2223
+ options,
2179
2224
  navigation: {
2180
2225
  navigate: (url) => router.push(url),
2181
2226
  currentPath: () => pathname + window.location.search
@@ -1,11 +1,11 @@
1
- import { b as PreprToolbarProps, i as PreprPixelConfig } from '../pixel-DVSJQC0K.cjs';
1
+ import { l as PreprToolbarComponentProps, k as PreprPixelConfig } from '../pixel-763p7m6A.cjs';
2
2
 
3
3
  /**
4
4
  * Mounts the Prepr toolbar and wires it to the Next.js router so segment/variant
5
5
  * switches navigate. Renders nothing — the toolbar UI is a custom element that
6
- * `createPreprToolbar` mounts imperatively.
6
+ * `createPreprPreview` mounts imperatively.
7
7
  */
8
- declare function PreprToolbar(props: PreprToolbarProps): null;
8
+ declare function PreprToolbar({ options, ...props }: PreprToolbarComponentProps): null;
9
9
  interface PreprTrackingPixelProps {
10
10
  /** Prepr tracking/access token. */
11
11
  id: string;
@@ -1,11 +1,11 @@
1
- import { b as PreprToolbarProps, i as PreprPixelConfig } from '../pixel-DVSJQC0K.js';
1
+ import { l as PreprToolbarComponentProps, k as PreprPixelConfig } from '../pixel-763p7m6A.js';
2
2
 
3
3
  /**
4
4
  * Mounts the Prepr toolbar and wires it to the Next.js router so segment/variant
5
5
  * switches navigate. Renders nothing — the toolbar UI is a custom element that
6
- * `createPreprToolbar` mounts imperatively.
6
+ * `createPreprPreview` mounts imperatively.
7
7
  */
8
- declare function PreprToolbar(props: PreprToolbarProps): null;
8
+ declare function PreprToolbar({ options, ...props }: PreprToolbarComponentProps): null;
9
9
  interface PreprTrackingPixelProps {
10
10
  /** Prepr tracking/access token. */
11
11
  id: string;
@@ -1,18 +1,22 @@
1
1
  "use client";
2
2
  import {
3
- createPreprToolbar,
3
+ createPreprPreview,
4
4
  loadTrackingPixel
5
- } from "../chunk-NLQIRN5O.js";
5
+ } from "../chunk-6OOGNVJ4.js";
6
6
 
7
7
  // src/nextjs/components.ts
8
8
  import { usePathname, useRouter } from "next/navigation";
9
9
  import { useEffect } from "react";
10
- function PreprToolbar(props) {
10
+ function PreprToolbar({
11
+ options,
12
+ ...props
13
+ }) {
11
14
  const router = useRouter();
12
15
  const pathname = usePathname();
13
16
  useEffect(() => {
14
- const toolbar = createPreprToolbar({
17
+ const toolbar = createPreprPreview({
15
18
  props,
19
+ options,
16
20
  navigation: {
17
21
  navigate: (url) => router.push(url),
18
22
  currentPath: () => pathname + window.location.search