@preprio/toolkit 0.6.0-alpha.6 → 0.6.0-alpha.8

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 (52) hide show
  1. package/README.md +138 -52
  2. package/dist/astro/{chunk-MWRA3732.js → chunk-R2YPGDVU.js} +1 -1
  3. package/dist/astro/graphql.cjs +58 -10
  4. package/dist/astro/graphql.d.cts +33 -38
  5. package/dist/astro/graphql.d.ts +33 -38
  6. package/dist/astro/graphql.js +59 -11
  7. package/dist/astro/index.cjs +1 -1
  8. package/dist/astro/index.js +1 -1
  9. package/dist/{chunk-Y7DWVOOY.js → chunk-2QGUP7VQ.js} +1 -1
  10. package/dist/{chunk-NSQOGI5K.js → chunk-ATPB7GEC.js} +1 -1
  11. package/dist/{chunk-W6AVX6FI.js → chunk-OY7OKO6Q.js} +52 -5
  12. package/dist/{chunk-OQGEL4TV.js → chunk-T7ESBVBI.js} +1 -1
  13. package/dist/graphql/codegen.cjs +51 -4
  14. package/dist/graphql/codegen.js +3 -3
  15. package/dist/graphql.cjs +89 -10
  16. package/dist/graphql.d.cts +69 -39
  17. package/dist/graphql.d.ts +69 -39
  18. package/dist/graphql.js +38 -9
  19. package/dist/index.cjs +1 -1
  20. package/dist/index.d.cts +1 -1
  21. package/dist/index.d.ts +1 -1
  22. package/dist/index.js +3 -3
  23. package/dist/nextjs/{chunk-PIQF5ZI5.js → chunk-O2SBSBDG.js} +58 -53
  24. package/dist/nextjs/components.cjs +205 -236
  25. package/dist/nextjs/components.d.cts +27 -22
  26. package/dist/nextjs/components.d.ts +27 -22
  27. package/dist/nextjs/components.js +198 -231
  28. package/dist/nextjs/graphql.cjs +418 -508
  29. package/dist/nextjs/graphql.d.cts +131 -135
  30. package/dist/nextjs/graphql.d.ts +131 -135
  31. package/dist/nextjs/graphql.js +353 -455
  32. package/dist/nextjs/index.cjs +114 -54
  33. package/dist/nextjs/index.d.cts +59 -33
  34. package/dist/nextjs/index.d.ts +59 -33
  35. package/dist/nextjs/index.js +108 -5
  36. package/dist/nuxt/{chunk-PF6GVWHV.js → chunk-SX3MSVPK.js} +1 -1
  37. package/dist/nuxt/graphql.cjs +58 -10
  38. package/dist/nuxt/graphql.d.cts +33 -38
  39. package/dist/nuxt/graphql.d.ts +33 -38
  40. package/dist/nuxt/graphql.js +59 -11
  41. package/dist/nuxt/index.cjs +1 -1
  42. package/dist/nuxt/index.js +1 -1
  43. package/dist/react/components.cjs +1 -1
  44. package/dist/react/components.js +1 -1
  45. package/dist/sveltekit/{chunk-5K7HWMIF.js → chunk-UOASSHZO.js} +1 -1
  46. package/dist/sveltekit/graphql.cjs +58 -10
  47. package/dist/sveltekit/graphql.d.cts +33 -38
  48. package/dist/sveltekit/graphql.d.ts +33 -38
  49. package/dist/sveltekit/graphql.js +59 -11
  50. package/dist/sveltekit/index.cjs +1 -1
  51. package/dist/sveltekit/index.js +1 -1
  52. package/package.json +1 -1
@@ -1,4 +1,4 @@
1
- import { ReactElement, ReactNode } from 'react';
1
+ import { ReactNode, ReactElement } from 'react';
2
2
  import { k as PreprToolbarComponentProps } from '../types-BYH9XqmN.cjs';
3
3
  import { P as PreprPixelConfig } from '../pixel-DwW7cXJm.cjs';
4
4
 
@@ -23,24 +23,31 @@ declare function PreprTrackingPixel({ id, config, }: PreprTrackingPixelProps): n
23
23
  */
24
24
  declare function PreprToolbar({ options, ...props }: PreprToolbarComponentProps): null;
25
25
  /**
26
- * Applies the visitor's A/B bucket after client-side navigation. A full page
27
- * load is covered by the inline script `PreprStack` renders, but React does
28
- * not run inline scripts that arrive with a client-side navigation; a layout
29
- * effect still runs before the browser paints the new page. Rendered by
30
- * `PreprStack`; you do not need it yourself.
26
+ * Gets each client-side navigation's decision in before the click: when the
27
+ * visitor hovers over a same-site link (or presses it, on touch devices), it
28
+ * asks `createPreprProxy` which variants they get on that page. Clicks are
29
+ * never held: the new page paints with those variants when the answer is in,
30
+ * and keeps its personalized sections hidden until then otherwise. Pages
31
+ * without personalization answer with no groups and are not asked again for
32
+ * an hour.
33
+ *
34
+ * Mount it once in the root layout, so links on pages without a
35
+ * `PreprStack` are covered too. Links to other sites and links that open a
36
+ * new tab or download are left alone.
37
+ *
38
+ * @example
39
+ * <PreprPersonalizationSwitch />
31
40
  */
32
- declare function PreprAbSwitch(): null;
33
- interface PreprPersonalizationSwitchProps {
34
- /**
35
- * Keep segment decisions fresh in the background. Default `true`. Leave it
36
- * on for the instance in your root layout; `PreprStack` renders its own
37
- * with `false`.
38
- */
39
- refresh?: boolean;
40
- /** Same as `createPreprProxy`'s `freshFor`. Default 10 000 ms. */
41
- freshFor?: number;
42
- }
43
- declare function PreprPersonalizationSwitch({ refresh, freshFor, }: PreprPersonalizationSwitchProps): ReactElement | null;
41
+ declare function PreprPersonalizationSwitch(): null;
42
+ /**
43
+ * Applies this page view's decision to the variant wrappers of the
44
+ * `PreprStack` it is rendered by, before the page paints when the decision is
45
+ * already in: asked for on hover, or kept from an earlier view of the page.
46
+ * Back and Forward show the page as this tab last showed it. Any other view
47
+ * keeps its personalized sections hidden until the decision request answers.
48
+ * Rendered by `PreprStack`; you do not need it yourself.
49
+ */
50
+ declare function PreprDecisionApplier(): null;
44
51
  /**
45
52
  * Wraps one rendered variant and, once hydrated, puts the variant keys on the
46
53
  * elements inside, where Prepr's tracking pixel counts impressions and the
@@ -55,11 +62,9 @@ declare function PreprPersonalizationSwitch({ refresh, freshFor, }: PreprPersona
55
62
  * carries a variant key is left alone. Rendered by `PreprStack` with
56
63
  * `trackVariants`; you do not need it yourself.
57
64
  */
58
- declare function PreprVariantTracking({ variantKeys, ab, children, }: {
65
+ declare function PreprVariantTracking({ variantKeys, children, }: {
59
66
  variantKeys: string[];
60
- /** The A/B bucket wrapper this also serves as; its CSS decides `display`. */
61
- ab?: string;
62
67
  children?: ReactNode;
63
68
  }): ReactElement;
64
69
 
65
- export { PreprAbSwitch, PreprPersonalizationSwitch, type PreprPersonalizationSwitchProps, PreprToolbar, PreprTrackingPixel, type PreprTrackingPixelProps, PreprVariantTracking };
70
+ export { PreprDecisionApplier, PreprPersonalizationSwitch, PreprToolbar, PreprTrackingPixel, type PreprTrackingPixelProps, PreprVariantTracking };
@@ -1,4 +1,4 @@
1
- import { ReactElement, ReactNode } from 'react';
1
+ import { ReactNode, ReactElement } from 'react';
2
2
  import { k as PreprToolbarComponentProps } from '../types-BYH9XqmN.js';
3
3
  import { P as PreprPixelConfig } from '../pixel-DwW7cXJm.js';
4
4
 
@@ -23,24 +23,31 @@ declare function PreprTrackingPixel({ id, config, }: PreprTrackingPixelProps): n
23
23
  */
24
24
  declare function PreprToolbar({ options, ...props }: PreprToolbarComponentProps): null;
25
25
  /**
26
- * Applies the visitor's A/B bucket after client-side navigation. A full page
27
- * load is covered by the inline script `PreprStack` renders, but React does
28
- * not run inline scripts that arrive with a client-side navigation; a layout
29
- * effect still runs before the browser paints the new page. Rendered by
30
- * `PreprStack`; you do not need it yourself.
26
+ * Gets each client-side navigation's decision in before the click: when the
27
+ * visitor hovers over a same-site link (or presses it, on touch devices), it
28
+ * asks `createPreprProxy` which variants they get on that page. Clicks are
29
+ * never held: the new page paints with those variants when the answer is in,
30
+ * and keeps its personalized sections hidden until then otherwise. Pages
31
+ * without personalization answer with no groups and are not asked again for
32
+ * an hour.
33
+ *
34
+ * Mount it once in the root layout, so links on pages without a
35
+ * `PreprStack` are covered too. Links to other sites and links that open a
36
+ * new tab or download are left alone.
37
+ *
38
+ * @example
39
+ * <PreprPersonalizationSwitch />
31
40
  */
32
- declare function PreprAbSwitch(): null;
33
- interface PreprPersonalizationSwitchProps {
34
- /**
35
- * Keep segment decisions fresh in the background. Default `true`. Leave it
36
- * on for the instance in your root layout; `PreprStack` renders its own
37
- * with `false`.
38
- */
39
- refresh?: boolean;
40
- /** Same as `createPreprProxy`'s `freshFor`. Default 10 000 ms. */
41
- freshFor?: number;
42
- }
43
- declare function PreprPersonalizationSwitch({ refresh, freshFor, }: PreprPersonalizationSwitchProps): ReactElement | null;
41
+ declare function PreprPersonalizationSwitch(): null;
42
+ /**
43
+ * Applies this page view's decision to the variant wrappers of the
44
+ * `PreprStack` it is rendered by, before the page paints when the decision is
45
+ * already in: asked for on hover, or kept from an earlier view of the page.
46
+ * Back and Forward show the page as this tab last showed it. Any other view
47
+ * keeps its personalized sections hidden until the decision request answers.
48
+ * Rendered by `PreprStack`; you do not need it yourself.
49
+ */
50
+ declare function PreprDecisionApplier(): null;
44
51
  /**
45
52
  * Wraps one rendered variant and, once hydrated, puts the variant keys on the
46
53
  * elements inside, where Prepr's tracking pixel counts impressions and the
@@ -55,11 +62,9 @@ declare function PreprPersonalizationSwitch({ refresh, freshFor, }: PreprPersona
55
62
  * carries a variant key is left alone. Rendered by `PreprStack` with
56
63
  * `trackVariants`; you do not need it yourself.
57
64
  */
58
- declare function PreprVariantTracking({ variantKeys, ab, children, }: {
65
+ declare function PreprVariantTracking({ variantKeys, children, }: {
59
66
  variantKeys: string[];
60
- /** The A/B bucket wrapper this also serves as; its CSS decides `display`. */
61
- ab?: string;
62
67
  children?: ReactNode;
63
68
  }): ReactElement;
64
69
 
65
- export { PreprAbSwitch, PreprPersonalizationSwitch, type PreprPersonalizationSwitchProps, PreprToolbar, PreprTrackingPixel, type PreprTrackingPixelProps, PreprVariantTracking };
70
+ export { PreprDecisionApplier, PreprPersonalizationSwitch, PreprToolbar, PreprTrackingPixel, type PreprTrackingPixelProps, PreprVariantTracking };
@@ -2,9 +2,9 @@
2
2
  import {
3
3
  createPreprPreview,
4
4
  loadTrackingPixel
5
- } from "../chunk-Y7DWVOOY.js";
5
+ } from "../chunk-2QGUP7VQ.js";
6
6
  import "../chunk-S5RWVMDJ.js";
7
- import "../chunk-NSQOGI5K.js";
7
+ import "../chunk-ATPB7GEC.js";
8
8
 
9
9
  // src/nextjs/components.ts
10
10
  import { usePathname, useRouter } from "next/navigation";
@@ -12,9 +12,7 @@ import {
12
12
  createElement,
13
13
  useEffect as useEffect2,
14
14
  useLayoutEffect,
15
- useRef,
16
- useState,
17
- Suspense
15
+ useRef
18
16
  } from "react";
19
17
 
20
18
  // src/react/hydration.ts
@@ -36,197 +34,99 @@ function isReactHydrated(text) {
36
34
  return true;
37
35
  }
38
36
 
39
- // src/core/refresh.ts
40
- var PREPR_REFRESH_COOKIE = "prepr_refresh";
41
- var PREPR_REFRESH_HEADER = "Prepr-Refresh";
42
- var REFRESH_MAX_PATHS = 5;
43
- var SAFE_PATH = /^\/(?![/\\])[\w\-./%]{0,200}$/;
44
- function safePath(path) {
45
- return SAFE_PATH.test(path) ? path : void 0;
46
- }
47
- function isFresh(at, freshFor, now = Date.now()) {
48
- const age = now - at;
49
- return at > 0 && age >= 0 && age < freshFor;
50
- }
51
- function cleanEntries(entries) {
52
- const out = [];
53
- for (const entry of entries) {
54
- if (out.length === REFRESH_MAX_PATHS) break;
55
- if (safePath(entry.path) && !out.some((e) => e.path === entry.path)) {
56
- out.push(entry);
57
- }
58
- }
59
- return out;
60
- }
61
- function parseRefresh(raw) {
62
- return cleanEntries(
63
- (raw ?? "").split("|").map((pair) => {
64
- const split = pair.lastIndexOf("~");
65
- const at = Number(split < 0 ? "" : pair.slice(split + 1));
66
- return {
67
- path: split < 0 ? pair : pair.slice(0, split),
68
- at: Number.isFinite(at) && at > 0 ? Math.floor(at) : 0
69
- };
70
- })
71
- );
72
- }
73
-
74
- // src/core/refresh-scheduler.ts
75
- var VIEW_DELAYS_MS = [3e3, 1e4];
76
- var INTENT_MIN_AGE_MS = 2e3;
77
- var INTENT_EVENTS = ["pointerover", "focusin", "touchstart"];
78
- var LISTENER = { capture: true, passive: true };
79
- var COOKIE_PATTERN = new RegExp(`(?:^|; )${PREPR_REFRESH_COOKIE}=([^;]*)`);
80
- var UNSUPPORTED_KEY = "prepr_refresh_unsupported";
81
- function readEntries() {
82
- const match = document.cookie.match(COOKIE_PATTERN);
83
- try {
84
- return parseRefresh(match ? decodeURIComponent(match[1]) : void 0);
85
- } catch {
86
- return [];
87
- }
88
- }
89
- function unsupported() {
90
- try {
91
- return sessionStorage.getItem(UNSUPPORTED_KEY) === "1";
92
- } catch {
93
- return false;
94
- }
95
- }
96
- function createPreprRefreshScheduler(options = {}) {
97
- const freshFor = options.freshFor ?? 1e4;
98
- let timers = [];
99
- let inFlight = false;
100
- let stopped = unsupported();
101
- function refresh() {
102
- if (inFlight || stopped) return;
103
- const paths = readEntries().map((entry) => entry.path);
104
- if (paths.length === 0) return;
105
- inFlight = true;
106
- const controller = new AbortController();
107
- fetch(location.pathname, {
108
- headers: { [PREPR_REFRESH_HEADER]: paths.join("|") },
109
- cache: "no-store",
110
- credentials: "same-origin",
111
- signal: controller.signal
112
- }).then((response) => {
113
- if (response.status === 204) return;
114
- stopped = true;
115
- controller.abort();
116
- try {
117
- sessionStorage.setItem(UNSUPPORTED_KEY, "1");
118
- } catch {
119
- }
120
- }).catch(() => void 0).finally(() => {
121
- inFlight = false;
37
+ // src/nextjs/decision-switch.ts
38
+ function applyPreprDecisions(decision) {
39
+ const state = window;
40
+ const key = location.pathname + location.search;
41
+ if (decision !== void 0) state.__preprDecision = { key, value: decision };
42
+ const current = state.__preprDecision;
43
+ if (!current || current.key !== key) return false;
44
+ document.querySelectorAll("[data-prepr-decision]").forEach((marker) => {
45
+ marker.setAttribute("data-prepr-decided", "");
46
+ });
47
+ const defaults = current.value === "*";
48
+ const chosen = {};
49
+ current.value.split("~").forEach((pair) => {
50
+ const colon = pair.indexOf(":");
51
+ if (colon > 0)
52
+ chosen[pair.slice(0, colon)] = pair.slice(colon + 1).split("|");
53
+ });
54
+ document.querySelectorAll("[data-prepr-v]").forEach((element) => {
55
+ const nested = element.hasAttribute("data-prepr-nested");
56
+ const show = defaults ? element.hasAttribute("data-prepr-default") : (element.getAttribute("data-prepr-v") || "").split(" ").every((token) => {
57
+ const colon = token.indexOf(":");
58
+ const variant = token.slice(colon + 1);
59
+ const variants = chosen[token.slice(0, colon)];
60
+ return !!variants && (nested ? variants[0] === variant : variants.indexOf(variant) >= 0);
122
61
  });
123
- }
124
- function onIntent(event) {
125
- const anchor = event.target?.closest?.("a[href]");
126
- if (!(anchor instanceof HTMLAnchorElement)) return;
127
- let url;
62
+ if (show) element.setAttribute("data-prepr-show", "");
63
+ else element.removeAttribute("data-prepr-show");
64
+ });
65
+ return true;
66
+ }
67
+ function requestPreprDecision(key, options = {}) {
68
+ const fresh = 1e4;
69
+ const keep = 18e5;
70
+ const keepStatic = 36e5;
71
+ const state = window;
72
+ const asking = state.__preprAsking = state.__preprAsking || {};
73
+ const read = () => {
128
74
  try {
129
- url = new URL(anchor.href, location.href);
75
+ const all = JSON.parse(sessionStorage.getItem("prepr_decisions") || "{}");
76
+ return all && typeof all === "object" ? all : {};
130
77
  } catch {
131
- return;
132
- }
133
- if (url.origin !== location.origin) return;
134
- const entry = readEntries().find((e) => e.path === url.pathname);
135
- if (entry && !isFresh(entry.at, INTENT_MIN_AGE_MS)) refresh();
136
- }
137
- function onVisibility() {
138
- if (document.visibilityState === "visible" && readEntries().some((entry) => !isFresh(entry.at, freshFor))) {
139
- refresh();
140
- }
141
- }
142
- for (const type of INTENT_EVENTS) {
143
- document.addEventListener(type, onIntent, LISTENER);
144
- }
145
- document.addEventListener("visibilitychange", onVisibility, {
146
- passive: true
147
- });
148
- return {
149
- view() {
150
- timers.forEach(clearTimeout);
151
- timers = VIEW_DELAYS_MS.map((delay) => setTimeout(refresh, delay));
152
- },
153
- stop() {
154
- timers.forEach(clearTimeout);
155
- timers = [];
156
- for (const type of INTENT_EVENTS) {
157
- document.removeEventListener(type, onIntent, LISTENER);
158
- }
159
- document.removeEventListener("visibilitychange", onVisibility);
78
+ return {};
160
79
  }
161
80
  };
162
- }
163
-
164
- // src/nextjs/ab-switch.ts
165
- function applyPreprAbBucket() {
166
- const match = document.cookie.match(/(?:^|; )prepr_ab_bucket=([AB])(?:;|$)/);
167
- const bucket = match ? match[1] : "A";
168
- const state = window;
169
- if (state.__preprAbBucket === bucket) return;
170
- state.__preprAbBucket = bucket;
171
- const css = bucket === "A" ? "" : '[data-prepr-ab="A"]{display:none!important}[data-prepr-ab="' + bucket + '"]{display:contents!important}';
172
- try {
173
- if (!state.__preprAbSheet) {
174
- const sheet = new CSSStyleSheet();
175
- document.adoptedStyleSheets = document.adoptedStyleSheets.concat(sheet);
176
- state.__preprAbSheet = sheet;
81
+ const ask = () => {
82
+ if (!asking[key]) {
83
+ asking[key] = fetch(key, {
84
+ headers: { "Prepr-Decide": "1" },
85
+ cache: "no-store",
86
+ credentials: "same-origin",
87
+ // The proxy gives up on Prepr after its own `timeoutMs`; this only
88
+ // covers a proxy that never answers.
89
+ signal: AbortSignal.timeout ? AbortSignal.timeout(6e3) : void 0
90
+ }).then((response) => response.ok ? response.text() : "*").catch(() => "*").then((answer) => {
91
+ delete asking[key];
92
+ try {
93
+ const all = read();
94
+ const at = Date.now();
95
+ Object.keys(all).forEach((stored) => {
96
+ const entry = all[stored];
97
+ if (!(entry && typeof entry.at === "number" && at - entry.at < keepStatic))
98
+ delete all[stored];
99
+ });
100
+ const kept = all[key];
101
+ if (answer !== "*" || !kept || kept.value === "*")
102
+ all[key] = { value: answer, at };
103
+ sessionStorage.setItem("prepr_decisions", JSON.stringify(all));
104
+ } catch {
105
+ }
106
+ return answer;
107
+ });
177
108
  }
178
- state.__preprAbSheet.replaceSync(css);
179
- } catch {
180
- if (!state.__preprAbSheet) {
181
- state.__preprAbSheet = document.createElement("style");
182
- document.head.appendChild(state.__preprAbSheet);
109
+ return asking[key];
110
+ };
111
+ const last = read()[key];
112
+ const age = last && typeof last.at === "number" ? Date.now() - last.at : -1;
113
+ const value = last && typeof last.value === "string" ? last.value : null;
114
+ if (value !== null && !(value === "" && options.personalized) && age >= 0 && age < (value === "*" ? fresh : value === "" ? keepStatic : keep)) {
115
+ if (value !== "" && age >= fresh) {
116
+ ask().then((answer) => {
117
+ if (answer !== "*" && answer !== value && options.onChange)
118
+ options.onChange(answer);
119
+ });
183
120
  }
184
- state.__preprAbSheet.textContent = css;
185
- }
186
- }
187
- function applyPreprDecisions(view) {
188
- const viewState = window;
189
- const current = view === void 0 ? location.pathname + location.search : view;
190
- if (viewState.__preprDecisionsView === current) return;
191
- viewState.__preprDecisionsView = current;
192
- const match = document.cookie.match(/(?:^|; )prepr_decisions=([^;]*)/);
193
- let raw;
194
- try {
195
- raw = match ? decodeURIComponent(match[1]) : "";
196
- } catch {
197
- raw = "";
121
+ return { result: value, value: Promise.resolve(value) };
198
122
  }
199
- const state = window;
200
- if (state.__preprDecisions === raw) return;
201
- state.__preprDecisions = raw;
202
- const id = /^[\w-]+$/;
203
- let css = "";
204
- raw.split("~").forEach((pair) => {
205
- const at = pair.indexOf(":");
206
- const group = pair.slice(0, at);
207
- if (at < 1 || !id.test(group)) return;
208
- const selector = '[data-prepr-group="' + group + '"]';
209
- css += selector + "{display:none!important}";
210
- pair.slice(at + 1).split("|").forEach((variant) => {
211
- if (id.test(variant)) {
212
- css += selector + '[data-prepr-variant="' + variant + '"]{display:contents!important}';
213
- }
214
- });
123
+ const pending = {
124
+ value: ask()
125
+ };
126
+ pending.value.then((answer) => {
127
+ pending.result = answer;
215
128
  });
216
- try {
217
- if (!state.__preprDecisionSheet) {
218
- const sheet = new CSSStyleSheet();
219
- document.adoptedStyleSheets = document.adoptedStyleSheets.concat(sheet);
220
- state.__preprDecisionSheet = sheet;
221
- }
222
- state.__preprDecisionSheet.replaceSync(css);
223
- } catch {
224
- if (!state.__preprDecisionSheet) {
225
- state.__preprDecisionSheet = document.createElement("style");
226
- document.head.appendChild(state.__preprDecisionSheet);
227
- }
228
- state.__preprDecisionSheet.textContent = css;
229
- }
129
+ return pending;
230
130
  }
231
131
 
232
132
  // src/react/components.ts
@@ -264,62 +164,124 @@ function PreprToolbar({
264
164
  }, []);
265
165
  return null;
266
166
  }
267
- function PreprAbSwitch() {
268
- useLayoutEffect(() => {
269
- applyPreprAbBucket();
167
+ var shown = /* @__PURE__ */ new Map();
168
+ var historyStep = false;
169
+ var onPopState = () => {
170
+ historyStep = true;
171
+ };
172
+ function requestDecision(key, personalized = false) {
173
+ return requestPreprDecision(key, {
174
+ personalized,
175
+ onChange: (value) => {
176
+ if (location.pathname + location.search !== key) return;
177
+ applyPreprDecisions(value);
178
+ shown.set(key, value);
179
+ }
270
180
  });
271
- return null;
272
181
  }
273
- var viewCount = 0;
274
- function PreprPersonalizationSwitch({
275
- refresh = true,
276
- freshFor
277
- }) {
278
- const [scheduler, setScheduler] = useState(
279
- null
280
- );
281
- const viewUrl = useRef(null);
282
- useLayoutEffect(() => {
283
- const url = location.pathname + location.search;
284
- if (viewUrl.current === url) return;
285
- viewUrl.current = url;
286
- applyPreprDecisions(`${url}#${++viewCount}`);
287
- });
182
+ function linkTarget(event) {
183
+ const anchor = event.target?.closest?.("a[href]");
184
+ if (!(anchor instanceof HTMLAnchorElement)) return null;
185
+ if (anchor.target && anchor.target !== "_self" || anchor.hasAttribute("download")) {
186
+ return null;
187
+ }
188
+ const url = new URL(anchor.href, location.href);
189
+ const key = url.pathname + url.search;
190
+ if (url.origin !== location.origin || key === location.pathname + location.search) {
191
+ return null;
192
+ }
193
+ return key;
194
+ }
195
+ var HOVER_DELAY_MS = 65;
196
+ var hovering = null;
197
+ function onPointerOver(event) {
198
+ if (event.pointerType !== "mouse") return;
199
+ const key = linkTarget(event);
200
+ if (hovering?.key === key) return;
201
+ if (hovering) clearTimeout(hovering.timer);
202
+ hovering = key ? { key, timer: setTimeout(() => requestDecision(key), HOVER_DELAY_MS) } : null;
203
+ }
204
+ function onPointerDown(event) {
205
+ if (event.button !== 0) return;
206
+ const key = linkTarget(event);
207
+ if (key) requestDecision(key);
208
+ }
209
+ var listeners = 0;
210
+ function useDecisionRequests() {
288
211
  useEffect2(() => {
289
- if (!refresh) return;
290
- const started = createPreprRefreshScheduler({ freshFor });
291
- setScheduler(started);
212
+ if (listeners++ === 0) {
213
+ window.addEventListener("pointerover", onPointerOver, { passive: true });
214
+ window.addEventListener("pointerdown", onPointerDown, {
215
+ capture: true,
216
+ passive: true
217
+ });
218
+ window.addEventListener("popstate", onPopState);
219
+ }
292
220
  return () => {
293
- started.stop();
294
- setScheduler(null);
221
+ if (--listeners === 0) {
222
+ window.removeEventListener("pointerover", onPointerOver);
223
+ window.removeEventListener("pointerdown", onPointerDown, true);
224
+ window.removeEventListener("popstate", onPopState);
225
+ }
295
226
  };
296
- }, [refresh, freshFor]);
297
- return scheduler ? createElement(
298
- Suspense,
299
- { fallback: null },
300
- createElement(ViewCounter, { scheduler })
301
- ) : null;
227
+ }, []);
302
228
  }
303
- function ViewCounter({
304
- scheduler
305
- }) {
306
- const pathname = usePathname();
307
- useEffect2(() => {
308
- scheduler.view();
309
- }, [pathname, scheduler]);
229
+ function PreprPersonalizationSwitch() {
230
+ useDecisionRequests();
231
+ return null;
232
+ }
233
+ function PreprDecisionApplier() {
234
+ useDecisionRequests();
235
+ useLayoutEffect(() => {
236
+ const key = location.pathname + location.search;
237
+ const remember = () => {
238
+ const state = window.__preprDecision;
239
+ if (state) shown.set(key, state.value);
240
+ };
241
+ const previous = historyStep ? shown.get(key) : void 0;
242
+ historyStep = false;
243
+ if (applyPreprDecisions(previous)) {
244
+ remember();
245
+ return;
246
+ }
247
+ const pending = requestDecision(key, true);
248
+ if (pending.result !== void 0) {
249
+ applyPreprDecisions(pending.result);
250
+ remember();
251
+ return;
252
+ }
253
+ let live = true;
254
+ void pending.value.then((value) => {
255
+ if (live && location.pathname + location.search === key) {
256
+ applyPreprDecisions(value);
257
+ remember();
258
+ }
259
+ });
260
+ return () => {
261
+ live = false;
262
+ };
263
+ });
310
264
  return null;
311
265
  }
312
266
  var VARIANT_KEY_ATTRIBUTE = "data-prepr-variant-key";
313
267
  function PreprVariantTracking({
314
268
  variantKeys,
315
- ab,
316
269
  children
317
270
  }) {
318
271
  const ref = useRef(null);
319
272
  const keys = variantKeys.join(" ");
320
273
  useLayoutEffect(() => {
321
274
  let tagged = [];
322
- const timer = setTimeout(() => {
275
+ const wrapper = ref.current?.closest("[data-prepr-v]");
276
+ const shown2 = () => !wrapper || wrapper.hasAttribute("data-prepr-show");
277
+ const untag = () => {
278
+ for (const [element] of tagged)
279
+ element.removeAttribute(VARIANT_KEY_ATTRIBUTE);
280
+ tagged = [];
281
+ };
282
+ const tag = () => {
283
+ if (!shown2()) return untag();
284
+ if (tagged.length > 0) return;
323
285
  const elements = [...ref.current?.children ?? []];
324
286
  const list = keys.split(" ");
325
287
  const pairs = elements.length === list.length ? elements.map((element, i) => [element, list[i]]) : elements.slice(0, 1).map((element) => [element, list[0]]);
@@ -328,22 +290,27 @@ function PreprVariantTracking({
328
290
  element.setAttribute(VARIANT_KEY_ATTRIBUTE, key);
329
291
  return true;
330
292
  });
293
+ };
294
+ const timer = setTimeout(tag);
295
+ const observer = wrapper && new MutationObserver(tag);
296
+ observer?.observe(wrapper, {
297
+ attributes: true,
298
+ attributeFilter: ["data-prepr-show"]
331
299
  });
332
300
  return () => {
333
301
  clearTimeout(timer);
334
- for (const [element] of tagged) {
335
- element.removeAttribute(VARIANT_KEY_ATTRIBUTE);
336
- }
302
+ observer?.disconnect();
303
+ untag();
337
304
  };
338
305
  }, [keys]);
339
306
  return createElement(
340
307
  "div",
341
- ab === void 0 ? { ref, style: { display: "contents" } } : { ref, "data-prepr-ab": ab },
308
+ { ref, style: { display: "contents" } },
342
309
  children
343
310
  );
344
311
  }
345
312
  export {
346
- PreprAbSwitch,
313
+ PreprDecisionApplier,
347
314
  PreprPersonalizationSwitch,
348
315
  PreprToolbar,
349
316
  PreprTrackingPixel,