@xenia-martech/sdk-js 1.2.0-dev.8 → 1.2.0-dev.9

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 (96) hide show
  1. package/README.md +69 -5
  2. package/dist/{actions-35V37IT0.d.cts → actions--1_9Z8ig.d.cts} +13 -2
  3. package/dist/{actions-35V37IT0.d.ts → actions--1_9Z8ig.d.ts} +13 -2
  4. package/dist/{api-0IycTYJR.d.cts → api-DbUxT38T.d.ts} +1 -1
  5. package/dist/{api-COXWLL5L.d.ts → api-DgzJ5s-k.d.cts} +1 -1
  6. package/dist/chunk-DPXINHQR.cjs +34 -0
  7. package/dist/chunk-DPXINHQR.cjs.map +1 -0
  8. package/dist/chunk-FSJMHXS6.js +432 -0
  9. package/dist/chunk-FSJMHXS6.js.map +1 -0
  10. package/dist/chunk-GXJLU3RE.cjs +448 -0
  11. package/dist/chunk-GXJLU3RE.cjs.map +1 -0
  12. package/dist/{chunk-2DR5AVPX.cjs → chunk-I5EHLRSR.cjs} +11 -10
  13. package/dist/chunk-I5EHLRSR.cjs.map +1 -0
  14. package/dist/{chunk-LW5AEJ6K.js → chunk-MEULT5X6.js} +4 -241
  15. package/dist/chunk-MEULT5X6.js.map +1 -0
  16. package/dist/{chunk-GWRDZIVA.js → chunk-NDRRSRTU.js} +4 -3
  17. package/dist/chunk-NDRRSRTU.js.map +1 -0
  18. package/dist/chunk-RC46KXDM.js +32 -0
  19. package/dist/chunk-RC46KXDM.js.map +1 -0
  20. package/dist/{chunk-RWE2OCQL.cjs → chunk-SU25SELD.cjs} +4 -244
  21. package/dist/chunk-SU25SELD.cjs.map +1 -0
  22. package/dist/{chunk-WHGK55JP.cjs → chunk-UUIKUU5W.cjs} +5 -5
  23. package/dist/{chunk-WHGK55JP.cjs.map → chunk-UUIKUU5W.cjs.map} +1 -1
  24. package/dist/{chunk-QEYONKWY.js → chunk-UVTFGQR6.js} +3 -3
  25. package/dist/{chunk-QEYONKWY.js.map → chunk-UVTFGQR6.js.map} +1 -1
  26. package/dist/{chunk-F3WZFXYK.js → chunk-V4EGU3PM.js} +45 -10
  27. package/dist/chunk-V4EGU3PM.js.map +1 -0
  28. package/dist/{chunk-IOY6IPUW.cjs → chunk-WMGCQPHY.cjs} +49 -12
  29. package/dist/chunk-WMGCQPHY.cjs.map +1 -0
  30. package/dist/client/index.cjs +26 -10
  31. package/dist/client/index.cjs.map +1 -1
  32. package/dist/client/index.d.cts +2 -2
  33. package/dist/client/index.d.ts +2 -2
  34. package/dist/client/index.js +26 -10
  35. package/dist/client/index.js.map +1 -1
  36. package/dist/client/react/index.cjs +73 -15
  37. package/dist/client/react/index.cjs.map +1 -1
  38. package/dist/client/react/index.d.cts +9 -3
  39. package/dist/client/react/index.d.ts +9 -3
  40. package/dist/client/react/index.js +73 -15
  41. package/dist/client/react/index.js.map +1 -1
  42. package/dist/cms/contentful.cjs +7 -24
  43. package/dist/cms/contentful.cjs.map +1 -1
  44. package/dist/cms/contentful.d.cts +5 -4
  45. package/dist/cms/contentful.d.ts +5 -4
  46. package/dist/cms/contentful.js +4 -23
  47. package/dist/cms/contentful.js.map +1 -1
  48. package/dist/cms/contentstack.cjs +120 -0
  49. package/dist/cms/contentstack.cjs.map +1 -0
  50. package/dist/cms/contentstack.d.cts +41 -0
  51. package/dist/cms/contentstack.d.ts +41 -0
  52. package/dist/cms/contentstack.js +117 -0
  53. package/dist/cms/contentstack.js.map +1 -0
  54. package/dist/{consent-DRCV0N0d.d.ts → consent-Dtiqzz2B.d.cts} +71 -11
  55. package/dist/{consent-BgAETCkL.d.cts → consent-byYiDMFr.d.ts} +71 -11
  56. package/dist/{cookie-names-DU3MtcNG.d.ts → cookie-names-B4hda2rX.d.cts} +1 -1
  57. package/dist/{cookie-names-DFItnSLK.d.cts → cookie-names-DioZXuV0.d.ts} +1 -1
  58. package/dist/edge/index.cjs +122 -29
  59. package/dist/edge/index.cjs.map +1 -1
  60. package/dist/edge/index.d.cts +41 -5
  61. package/dist/edge/index.d.ts +41 -5
  62. package/dist/edge/index.js +92 -9
  63. package/dist/edge/index.js.map +1 -1
  64. package/dist/index.cjs +31 -31
  65. package/dist/index.d.cts +4 -5
  66. package/dist/index.d.ts +4 -5
  67. package/dist/index.js +2 -2
  68. package/dist/next/index.cjs +34 -33
  69. package/dist/next/index.cjs.map +1 -1
  70. package/dist/next/index.d.cts +2 -3
  71. package/dist/next/index.d.ts +2 -3
  72. package/dist/next/index.js +8 -7
  73. package/dist/next/index.js.map +1 -1
  74. package/dist/{types-DgVHS4Qw.d.ts → personalize-content-D6xeBI7-.d.cts} +150 -1
  75. package/dist/{types-DgVHS4Qw.d.cts → personalize-content-D6xeBI7-.d.ts} +150 -1
  76. package/dist/server/index.cjs +30 -30
  77. package/dist/server/index.d.cts +5 -6
  78. package/dist/server/index.d.ts +5 -6
  79. package/dist/server/index.js +5 -5
  80. package/dist/xenia-sdk.iife.js +3 -3
  81. package/dist/xenia-sdk.iife.js.map +1 -1
  82. package/dist/{xenia-server-Cf24mL0c.d.ts → xenia-server-BcgeUHX4.d.ts} +2 -3
  83. package/dist/{xenia-server-D-bx14lw.d.cts → xenia-server-lv6LtJZP.d.cts} +2 -3
  84. package/package.json +14 -1
  85. package/dist/chunk-2DR5AVPX.cjs.map +0 -1
  86. package/dist/chunk-BQ5PJCZ7.js +0 -179
  87. package/dist/chunk-BQ5PJCZ7.js.map +0 -1
  88. package/dist/chunk-F3WZFXYK.js.map +0 -1
  89. package/dist/chunk-GWRDZIVA.js.map +0 -1
  90. package/dist/chunk-IOY6IPUW.cjs.map +0 -1
  91. package/dist/chunk-LW5AEJ6K.js.map +0 -1
  92. package/dist/chunk-N5M4PI7C.cjs +0 -192
  93. package/dist/chunk-N5M4PI7C.cjs.map +0 -1
  94. package/dist/chunk-RWE2OCQL.cjs.map +0 -1
  95. package/dist/personalize-content-Cwc2AomC.d.ts +0 -141
  96. package/dist/personalize-content-CxSdplMV.d.cts +0 -141
@@ -1,7 +1,7 @@
1
1
  import * as react from 'react';
2
2
  import { ReactNode, ElementType, ComponentType } from 'react';
3
- import { E as XeniaClientOptions, D as XeniaClient, w as ResolveResult, u as ResolveBatchResult, J as JsonValue, x as WebhookResponse, R as ResolveBatchItem, g as AnalyticsEventName, T as TrackOptions, P as PagePersonalizeOptions, j as AppliedSwap, s as PageRef, e as XeniaAction, I as InteractionType, q as MessageMountContext, c as MessageComponentRenderer } from '../../actions-35V37IT0.cjs';
4
- export { A as AnalyticsContext, h as AnalyticsIdentity, i as AnalyticsIngestResponse, k as ClientEvent, l as CmsAdapter, C as ConsentState, m as ContentSwap, n as IdentifyRequest, o as IdentifyResponse, M as MessageCta, f as MessagePlacement, r as MessageRenderContext, b as MessageTemplateFn, t as PersonalizationSwap, v as ResolveOptions, X as XeniaConfig, a as XeniaEvent, F as XeniaMarker, G as createXeniaClient, H as extractContentSwaps, K as parseActions, L as pendingTargets, N as personalizeContent, O as personalizePage, Q as spliceSlots } from '../../actions-35V37IT0.cjs';
3
+ import { E as XeniaClientOptions, D as XeniaClient, w as ResolveResult, u as ResolveBatchResult, J as JsonValue, x as WebhookResponse, R as ResolveBatchItem, g as AnalyticsEventName, T as TrackOptions, P as PagePersonalizeOptions, j as AppliedSwap, s as PageRef, e as XeniaAction, I as InteractionType, q as MessageMountContext, c as MessageComponentRenderer } from '../../actions--1_9Z8ig.cjs';
4
+ export { A as AnalyticsContext, h as AnalyticsIdentity, i as AnalyticsIngestResponse, k as ClientEvent, l as CmsAdapter, C as ConsentState, m as ContentSwap, n as IdentifyRequest, o as IdentifyResponse, M as MessageCta, f as MessagePlacement, r as MessageRenderContext, b as MessageTemplateFn, t as PersonalizationSwap, v as ResolveOptions, X as XeniaConfig, a as XeniaEvent, F as XeniaMarker, G as createXeniaClient, H as extractContentSwaps, K as parseActions, L as pendingTargets, N as personalizeContent, O as personalizePage, Q as spliceSlots } from '../../actions--1_9Z8ig.cjs';
5
5
 
6
6
  interface XeniaProviderProps extends XeniaClientOptions {
7
7
  children: ReactNode;
@@ -240,6 +240,12 @@ interface XeniaDebugPanelProps {
240
240
  * or closes it, their choice wins for the rest of the session.
241
241
  */
242
242
  defaultOpen?: boolean;
243
+ /**
244
+ * Start with the card pinned open: clicks elsewhere on the page and Escape leave it open, and only
245
+ * the Xenia launcher closes it. The card's pin button toggles this; the visitor's choice wins for the
246
+ * rest of the session. Default `false`.
247
+ */
248
+ keepOpen?: boolean;
243
249
  /**
244
250
  * Content of your own for the Site tab, beyond `custom` (which the panel shows by itself), from
245
251
  * the fields your proxy's `debugInfo` added.
@@ -255,7 +261,7 @@ declare function debugStatus(entry: DebugEntry): DebugStatus;
255
261
  * site's own rows (`debugInfo`'s `custom`). Reads the debug cookie `xeniaEdgeProxy({ debug: true })`
256
262
  * writes; renders nothing without an entry for this page.
257
263
  */
258
- declare function XeniaDebugPanel({ prefix, position, defaultOpen, renderExtra }: XeniaDebugPanelProps): react.JSX.Element | null;
264
+ declare function XeniaDebugPanel({ prefix, position, defaultOpen, keepOpen, renderExtra, }: XeniaDebugPanelProps): react.JSX.Element | null;
259
265
 
260
266
  export { AnalyticsEventName, AppliedSwap, InteractionType, JsonValue, MessageComponentRenderer, MessageMountContext, PagePersonalizeOptions, PageRef, ResolveBatchItem, ResolveBatchResult, ResolveResult, TrackOptions, WebhookResponse, XeniaAction, XeniaClient, XeniaClientOptions, XeniaContentProvider, XeniaContext, XeniaDebugPanel, XeniaMessage, XeniaProvider, XeniaTrack, debugStatus, reactMessageComponent, usePersonalization, usePersonalizationResolve, usePersonalizationResolveSlots, usePersonalizationSlots, usePersonalizedContent, usePersonalizedPage, useTrack, useWebhook, useXenia, useXeniaEntry, useXeniaResponse };
261
267
  export type { DebugStatus, EntryStatus, UsePersonalizationResolveResult, UsePersonalizationResolveSlotsResult, UsePersonalizationResult, UsePersonalizationSlotsResult, UsePersonalizedContentOptions, UsePersonalizedContentResult, UseWebhookResult, XeniaDebugPanelProps, XeniaMessageComponentProps, XeniaMessageProps, XeniaProviderProps, XeniaTrackProps };
@@ -1,7 +1,7 @@
1
1
  import * as react from 'react';
2
2
  import { ReactNode, ElementType, ComponentType } from 'react';
3
- import { E as XeniaClientOptions, D as XeniaClient, w as ResolveResult, u as ResolveBatchResult, J as JsonValue, x as WebhookResponse, R as ResolveBatchItem, g as AnalyticsEventName, T as TrackOptions, P as PagePersonalizeOptions, j as AppliedSwap, s as PageRef, e as XeniaAction, I as InteractionType, q as MessageMountContext, c as MessageComponentRenderer } from '../../actions-35V37IT0.js';
4
- export { A as AnalyticsContext, h as AnalyticsIdentity, i as AnalyticsIngestResponse, k as ClientEvent, l as CmsAdapter, C as ConsentState, m as ContentSwap, n as IdentifyRequest, o as IdentifyResponse, M as MessageCta, f as MessagePlacement, r as MessageRenderContext, b as MessageTemplateFn, t as PersonalizationSwap, v as ResolveOptions, X as XeniaConfig, a as XeniaEvent, F as XeniaMarker, G as createXeniaClient, H as extractContentSwaps, K as parseActions, L as pendingTargets, N as personalizeContent, O as personalizePage, Q as spliceSlots } from '../../actions-35V37IT0.js';
3
+ import { E as XeniaClientOptions, D as XeniaClient, w as ResolveResult, u as ResolveBatchResult, J as JsonValue, x as WebhookResponse, R as ResolveBatchItem, g as AnalyticsEventName, T as TrackOptions, P as PagePersonalizeOptions, j as AppliedSwap, s as PageRef, e as XeniaAction, I as InteractionType, q as MessageMountContext, c as MessageComponentRenderer } from '../../actions--1_9Z8ig.js';
4
+ export { A as AnalyticsContext, h as AnalyticsIdentity, i as AnalyticsIngestResponse, k as ClientEvent, l as CmsAdapter, C as ConsentState, m as ContentSwap, n as IdentifyRequest, o as IdentifyResponse, M as MessageCta, f as MessagePlacement, r as MessageRenderContext, b as MessageTemplateFn, t as PersonalizationSwap, v as ResolveOptions, X as XeniaConfig, a as XeniaEvent, F as XeniaMarker, G as createXeniaClient, H as extractContentSwaps, K as parseActions, L as pendingTargets, N as personalizeContent, O as personalizePage, Q as spliceSlots } from '../../actions--1_9Z8ig.js';
5
5
 
6
6
  interface XeniaProviderProps extends XeniaClientOptions {
7
7
  children: ReactNode;
@@ -240,6 +240,12 @@ interface XeniaDebugPanelProps {
240
240
  * or closes it, their choice wins for the rest of the session.
241
241
  */
242
242
  defaultOpen?: boolean;
243
+ /**
244
+ * Start with the card pinned open: clicks elsewhere on the page and Escape leave it open, and only
245
+ * the Xenia launcher closes it. The card's pin button toggles this; the visitor's choice wins for the
246
+ * rest of the session. Default `false`.
247
+ */
248
+ keepOpen?: boolean;
243
249
  /**
244
250
  * Content of your own for the Site tab, beyond `custom` (which the panel shows by itself), from
245
251
  * the fields your proxy's `debugInfo` added.
@@ -255,7 +261,7 @@ declare function debugStatus(entry: DebugEntry): DebugStatus;
255
261
  * site's own rows (`debugInfo`'s `custom`). Reads the debug cookie `xeniaEdgeProxy({ debug: true })`
256
262
  * writes; renders nothing without an entry for this page.
257
263
  */
258
- declare function XeniaDebugPanel({ prefix, position, defaultOpen, renderExtra }: XeniaDebugPanelProps): react.JSX.Element | null;
264
+ declare function XeniaDebugPanel({ prefix, position, defaultOpen, keepOpen, renderExtra, }: XeniaDebugPanelProps): react.JSX.Element | null;
259
265
 
260
266
  export { AnalyticsEventName, AppliedSwap, InteractionType, JsonValue, MessageComponentRenderer, MessageMountContext, PagePersonalizeOptions, PageRef, ResolveBatchItem, ResolveBatchResult, ResolveResult, TrackOptions, WebhookResponse, XeniaAction, XeniaClient, XeniaClientOptions, XeniaContentProvider, XeniaContext, XeniaDebugPanel, XeniaMessage, XeniaProvider, XeniaTrack, debugStatus, reactMessageComponent, usePersonalization, usePersonalizationResolve, usePersonalizationResolveSlots, usePersonalizationSlots, usePersonalizedContent, usePersonalizedPage, useTrack, useWebhook, useXenia, useXeniaEntry, useXeniaResponse };
261
267
  export type { DebugStatus, EntryStatus, UsePersonalizationResolveResult, UsePersonalizationResolveSlotsResult, UsePersonalizationResult, UsePersonalizationSlotsResult, UsePersonalizedContentOptions, UsePersonalizedContentResult, UseWebhookResult, XeniaDebugPanelProps, XeniaMessageComponentProps, XeniaMessageProps, XeniaProviderProps, XeniaTrackProps };
@@ -720,7 +720,7 @@ async function personalizeContent(payload, options) {
720
720
  const next = apply(target, op, d.decision.field ?? target.marker.field, items, cms);
721
721
  report({
722
722
  target: target.id,
723
- kind: "runtime",
723
+ kind: target.local ? "static" : "runtime",
724
724
  op,
725
725
  entryIds: items.map((e) => cms.entryId(e)).filter((id) => !!id),
726
726
  contentKey: d.decision.contentKey,
@@ -730,20 +730,23 @@ async function personalizeContent(payload, options) {
730
730
  return next;
731
731
  };
732
732
  for (let round = 0; ; round++) {
733
- const pending = walk(frontier, cms, settled, round, report, (t) => {
733
+ const pending = walk(frontier, cms, settled, round, report, options.contentKey, (t) => {
734
734
  const d = decided.get(t.id);
735
735
  return d ? settle(t, d, round) : void 0;
736
736
  });
737
737
  if (pending.size === 0) return { content: box.root, applied, complete: true };
738
- if (!options.resolve || round >= maxRounds) {
739
- return { content: box.root, applied, complete: !options.resolve };
738
+ const remote = [...pending.values()].filter((p) => !p.local).map((p) => p.id);
739
+ const local = [...pending.values()].flatMap((p) => p.local ? [p.local] : []);
740
+ if (!options.resolve && local.length === 0 || round >= maxRounds) {
741
+ return { content: box.root, applied, complete: !options.resolve && round < maxRounds };
740
742
  }
741
743
  progress(options, { content: box.root, round, pending: [...pending.keys()] });
742
744
  let decisions;
743
745
  let entriesById;
744
746
  let queried;
745
747
  try {
746
- decisions = await withDeadline(options.resolve([...pending.keys()]), deadline);
748
+ decisions = options.resolve && remote.length > 0 ? await withDeadline(options.resolve(remote), deadline) : [];
749
+ decisions = [...decisions, ...local];
747
750
  ({ entriesById, queried } = await withDeadline(loadReplacements(decisions, cms, options.locale), deadline));
748
751
  } catch (error) {
749
752
  return { content: box.root, applied, complete: false, error: error === TIMEOUT ? void 0 : error };
@@ -766,7 +769,7 @@ function progress(options, p) {
766
769
  } catch {
767
770
  }
768
771
  }
769
- function walk(frontier, cms, settled, round, report, reuse) {
772
+ function walk(frontier, cms, settled, round, report, contentKey, reuse) {
770
773
  const pending = /* @__PURE__ */ new Map();
771
774
  const seen = /* @__PURE__ */ new WeakSet();
772
775
  const deferred = /* @__PURE__ */ new WeakSet();
@@ -777,7 +780,8 @@ function walk(frontier, cms, settled, round, report, reuse) {
777
780
  if (!value || typeof value !== "object" || deferred.has(value)) continue;
778
781
  const id = settled.has(value) ? void 0 : cms.entryId(value);
779
782
  const marker = id ? cms.marker(value) : null;
780
- if (id && marker?.kind === "runtime") {
783
+ const local = id && marker?.kind === "static" && marker.ids?.length && cms.staticVariants(value).length === 0 ? localDecision(id, marker, contentKey) : void 0;
784
+ if (id && marker && (marker.kind === "runtime" || local)) {
781
785
  const again = reuse({ id, marker, entry: value, slots: [slot] });
782
786
  if (again) {
783
787
  queue.push(...again);
@@ -787,7 +791,7 @@ function walk(frontier, cms, settled, round, report, reuse) {
787
791
  if (existing) {
788
792
  existing.slots.push(slot);
789
793
  } else {
790
- pending.set(id, { id, marker, entry: value, slots: [slot] });
794
+ pending.set(id, { id, marker, entry: value, slots: [slot], ...local ? { local } : {} });
791
795
  if (marker.op === "items" && marker.field) {
792
796
  const held = cms.getField(value, marker.field);
793
797
  if (held && typeof held === "object") deferred.add(held);
@@ -821,6 +825,16 @@ function walk(frontier, cms, settled, round, report, reuse) {
821
825
  }
822
826
  return pending;
823
827
  }
828
+ function localDecision(id, marker, contentKey) {
829
+ return {
830
+ target: id,
831
+ op: marker.op ?? "replace",
832
+ ...marker.field ? { field: marker.field } : {},
833
+ ids: marker.ids,
834
+ ...marker.ct ? { contentType: marker.ct } : {},
835
+ contentKey
836
+ };
837
+ }
824
838
  function apply(target, op, field, items, cms) {
825
839
  if (op === "items") {
826
840
  if (field) {
@@ -859,9 +873,11 @@ function children(value) {
859
873
  }
860
874
  async function loadReplacements(decisions, cms, locale) {
861
875
  const ids = [...new Set(decisions.flatMap((d) => d.ids ?? []))];
876
+ const types = {};
877
+ for (const d of decisions) if (d.contentType) for (const id of d.ids ?? []) types[id] = d.contentType;
862
878
  const queries = decisions.filter((d) => !d.ids && d.query);
863
879
  const [fetched, ...results] = await Promise.all([
864
- ids.length > 0 ? cms.fetchEntries(ids, locale) : Promise.resolve([]),
880
+ ids.length > 0 ? cms.fetchEntries(ids, locale, types) : Promise.resolve([]),
865
881
  ...queries.map((d) => cms.queryEntries(d.query, locale).catch(() => []))
866
882
  ]);
867
883
  const entriesById = /* @__PURE__ */ new Map();
@@ -892,7 +908,7 @@ function pendingTargets(payload, adapter, contentKey) {
892
908
  content = cms.prepare ? cms.prepare(content) : content;
893
909
  const root = { parent: { root: content }, key: "root", value: content };
894
910
  return [...walk([root], cms, /* @__PURE__ */ new WeakSet(), 0, () => {
895
- }, () => void 0).keys()];
911
+ }, contentKey, () => void 0).keys()];
896
912
  } catch {
897
913
  return [];
898
914
  }
@@ -3330,6 +3346,7 @@ function readEntry(prefix) {
3330
3346
  return readDebugEntries(raw)[location.pathname] ?? null;
3331
3347
  }
3332
3348
  var OPEN_KEY = "xenia_debug_open";
3349
+ var PIN_KEY = "xenia_debug_pinned";
3333
3350
  var TAB_KEY = "xenia_debug_tab";
3334
3351
  function Mark({ size }) {
3335
3352
  return /* @__PURE__ */ jsxs("svg", { width: size, height: size, viewBox: "0 0 32 20", "aria-hidden": "true", focusable: "false", children: [
@@ -3344,9 +3361,16 @@ function Id({ value }) {
3344
3361
  ] }) : /* @__PURE__ */ jsx(Fragment, { children: value });
3345
3362
  }
3346
3363
  var percent = (n) => `${Math.round(n <= 1 ? n * 100 : n)}%`;
3347
- function XeniaDebugPanel({ prefix = DEFAULT_PREFIX, position = "bottom-right", defaultOpen = false, renderExtra }) {
3364
+ function XeniaDebugPanel({
3365
+ prefix = DEFAULT_PREFIX,
3366
+ position = "bottom-right",
3367
+ defaultOpen = false,
3368
+ keepOpen = false,
3369
+ renderExtra
3370
+ }) {
3348
3371
  const [entry, setEntry] = useState(null);
3349
3372
  const [open, setOpen] = useState(false);
3373
+ const [pinned, setPinned] = useState(keepOpen);
3350
3374
  const [tab, setTab] = useState("Decision");
3351
3375
  const root = useRef(null);
3352
3376
  const launcher = useRef(null);
@@ -3366,31 +3390,39 @@ function XeniaDebugPanel({ prefix = DEFAULT_PREFIX, position = "bottom-right", d
3366
3390
  try {
3367
3391
  const saved = sessionStorage.getItem(OPEN_KEY);
3368
3392
  setOpen(saved === null ? defaultOpen : saved === "1");
3393
+ const savedPin = sessionStorage.getItem(PIN_KEY);
3394
+ setPinned(savedPin === null ? keepOpen : savedPin === "1");
3369
3395
  const savedTab = sessionStorage.getItem(TAB_KEY);
3370
3396
  if (savedTab === "Decision" || savedTab === "Visitor" || savedTab === "Request" || savedTab === "Site") setTab(savedTab);
3371
3397
  } catch {
3372
3398
  setOpen(defaultOpen);
3399
+ setPinned(keepOpen);
3373
3400
  }
3374
3401
  const unwatch = watchNavigation(onNavigate);
3375
3402
  return () => {
3376
3403
  clearTimeout(timer);
3377
3404
  unwatch();
3378
3405
  };
3379
- }, [prefix, defaultOpen]);
3406
+ }, [prefix, defaultOpen, keepOpen]);
3380
3407
  const toggle = useCallback((next, focusLauncher = false) => {
3381
3408
  setOpen(next);
3382
3409
  focusOnOpen.current = next;
3383
3410
  if (focusLauncher) launcher.current?.focus();
3384
3411
  remember(OPEN_KEY, next ? "1" : "0");
3385
3412
  }, []);
3413
+ const pin = (next) => {
3414
+ setPinned(next);
3415
+ remember(PIN_KEY, next ? "1" : "0");
3416
+ };
3386
3417
  useEffect(() => {
3387
3418
  if (!open) return;
3388
3419
  if (focusOnOpen.current) card.current?.focus();
3420
+ if (pinned) return;
3389
3421
  const onKey = (e) => {
3390
3422
  if (e.key === "Escape" && root.current?.contains(document.activeElement)) toggle(false, true);
3391
3423
  };
3392
3424
  const onDown = (e) => {
3393
- if (root.current && !root.current.contains(e.target)) toggle(false);
3425
+ if (root.current && !root.current.contains(e.target)) setOpen(false);
3394
3426
  };
3395
3427
  document.addEventListener("keydown", onKey);
3396
3428
  document.addEventListener("mousedown", onDown);
@@ -3398,7 +3430,7 @@ function XeniaDebugPanel({ prefix = DEFAULT_PREFIX, position = "bottom-right", d
3398
3430
  document.removeEventListener("keydown", onKey);
3399
3431
  document.removeEventListener("mousedown", onDown);
3400
3432
  };
3401
- }, [open, toggle]);
3433
+ }, [open, pinned, toggle]);
3402
3434
  if (!entry) return null;
3403
3435
  const status = debugStatus(entry);
3404
3436
  const v = view(entry);
@@ -3443,7 +3475,28 @@ function XeniaDebugPanel({ prefix = DEFAULT_PREFIX, position = "bottom-right", d
3443
3475
  typeof v.ms === "number" && v.source === "xenia" ? /* @__PURE__ */ jsxs("span", { className: "xdp-ms", children: [
3444
3476
  v.ms,
3445
3477
  " ms"
3446
- ] }) : null
3478
+ ] }) : null,
3479
+ /* @__PURE__ */ jsx(
3480
+ "button",
3481
+ {
3482
+ type: "button",
3483
+ className: "xdp-pin",
3484
+ "aria-pressed": pinned,
3485
+ "aria-label": "Keep open",
3486
+ title: pinned ? "Pinned: only the Xenia button closes this" : "Keep open: clicks elsewhere won't close it",
3487
+ onClick: () => pin(!pinned),
3488
+ children: /* @__PURE__ */ jsx("svg", { width: "14", height: "14", viewBox: "0 0 24 24", "aria-hidden": "true", focusable: "false", children: /* @__PURE__ */ jsx(
3489
+ "path",
3490
+ {
3491
+ d: "M15 3l6 6-3 1-4 4 1 5-2 2-4-4-5 5-1-1 5-5-4-4 2-2 5 1 4-4z",
3492
+ fill: pinned ? "currentColor" : "none",
3493
+ stroke: "currentColor",
3494
+ strokeWidth: "1.8",
3495
+ strokeLinejoin: "round"
3496
+ }
3497
+ ) })
3498
+ }
3499
+ )
3447
3500
  ] }),
3448
3501
  /* @__PURE__ */ jsx("code", { className: "xdp-path", title: v.page, children: v.page }),
3449
3502
  status === "attention" ? /* @__PURE__ */ jsx("p", { className: "xdp-alert", children: v.source === "timeout" ? "Xenia didn't answer within the time budget." : "The decision failed; the proxy's onError has the cause." }) : null
@@ -3612,6 +3665,11 @@ var STYLES2 = `
3612
3665
  code.xdp-chip{font-size:11.5px}
3613
3666
  .xdp-chip[data-tone=holdout]{background:var(--xdp-warn-tint);color:var(--xdp-warn);font-weight:600}
3614
3667
  .xdp-ms{margin-left:auto;color:var(--xdp-faint)}
3668
+ .xdp-pin{margin-left:auto;width:26px;height:26px;display:grid;place-items:center;border:0;border-radius:7px;background:transparent;color:var(--xdp-faint);cursor:pointer}
3669
+ .xdp-ms + .xdp-pin{margin-left:4px}
3670
+ .xdp-pin:hover{background:var(--xdp-soft);color:var(--xdp-muted)}
3671
+ .xdp-pin[aria-pressed=true]{color:var(--xdp-blue);background:var(--xdp-soft)}
3672
+ .xdp-pin:focus-visible{outline:2px solid var(--xdp-blue);outline-offset:1px}
3615
3673
  .xdp-path{display:block;margin-top:8px;font-size:13px;color:var(--xdp-ink);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
3616
3674
  .xdp-alert{margin:8px 0 0;padding:8px 10px;border-radius:8px;background:var(--xdp-warn-tint);color:var(--xdp-warn)}
3617
3675
  .xdp-tabs{display:flex;gap:4px;padding:0 12px;border-bottom:1px solid var(--xdp-border)}