@roottale/cms-renderer-next 0.61.0 → 0.62.0
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/CHANGELOG.md +20 -0
- package/dist/chunk-L3I4W5NP.js +16 -0
- package/dist/chunk-L3I4W5NP.js.map +1 -0
- package/dist/chunk-SEPUIKVM.js +922 -0
- package/dist/chunk-SEPUIKVM.js.map +1 -0
- package/dist/chunk-UIBT23RJ.js +3 -0
- package/dist/chunk-UIBT23RJ.js.map +1 -0
- package/dist/cms-public.css +111 -13
- package/dist/exposure-client.d.ts +4 -9
- package/dist/exposure-client.js +5 -3
- package/dist/exposures.d.ts +51 -0
- package/dist/exposures.js +17 -0
- package/dist/exposures.js.map +1 -0
- package/dist/routes.d.ts +20 -1
- package/dist/routes.js +41 -2
- package/dist/routes.js.map +1 -1
- package/dist/server.js +6 -4
- package/dist/server.js.map +1 -1
- package/package.json +11 -4
- package/dist/chunk-GVKENK6H.js +0 -75
- package/dist/chunk-GVKENK6H.js.map +0 -1
|
@@ -0,0 +1,922 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
exposureHomePaths,
|
|
5
|
+
isExposureHome
|
|
6
|
+
} from "./chunk-L3I4W5NP.js";
|
|
7
|
+
|
|
8
|
+
// src/exposures.tsx
|
|
9
|
+
import { createContext, useCallback as useCallback2, useContext, useEffect as useEffect6, useRef as useRef4, useState as useState6 } from "react";
|
|
10
|
+
|
|
11
|
+
// src/exposure-decision-client.ts
|
|
12
|
+
import { useEffect, useState } from "react";
|
|
13
|
+
|
|
14
|
+
// src/exposure-runtime-data.ts
|
|
15
|
+
function validLink(value, internal = true) {
|
|
16
|
+
if (value == null) return true;
|
|
17
|
+
if (typeof value !== "string" || /[\u0000-\u0020\u007f\\]/.test(value)) return false;
|
|
18
|
+
if (internal && value.startsWith("/") && !value.startsWith("//")) return true;
|
|
19
|
+
try {
|
|
20
|
+
const url = new URL(value);
|
|
21
|
+
return url.protocol === "https:" && !url.username && !url.password;
|
|
22
|
+
} catch {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function readExposureDecision(value, slots, receivedAt, elapsed) {
|
|
27
|
+
if (!value || typeof value !== "object") return null;
|
|
28
|
+
const data = value;
|
|
29
|
+
const evaluated = Date.parse(data.evaluatedAt);
|
|
30
|
+
const validUntil = Date.parse(data.validUntil);
|
|
31
|
+
if (!Number.isFinite(evaluated) || !Number.isFinite(validUntil) || validUntil <= evaluated || !Array.isArray(data.campaigns) || data.campaigns.length > 200) return null;
|
|
32
|
+
const serverAtReceipt = evaluated + Math.max(0, elapsed);
|
|
33
|
+
const expiresAt = receivedAt + Math.min(validUntil - serverAtReceipt, 12e4);
|
|
34
|
+
if (expiresAt <= receivedAt) return null;
|
|
35
|
+
const campaigns = [];
|
|
36
|
+
for (const item of data.campaigns) {
|
|
37
|
+
if (!item || typeof item.id !== "string" || !slots.includes(item.slotKey) || item.kind !== "banner" && item.kind !== "popup" || !item.content || typeof item.content.title !== "string" || typeof item.content.variant !== "string" || !["always", "session", "day", "never"].includes(item.repeat) || !validLink(item.content.linkUrl) || !validLink(item.content.assetUrl, false) || !validLink(item.content.mobileAssetUrl, false)) return null;
|
|
38
|
+
if (item.endsAt != null && !Number.isFinite(Date.parse(item.endsAt))) return null;
|
|
39
|
+
if (item.delivery) {
|
|
40
|
+
const delivery = item.delivery;
|
|
41
|
+
if (delivery.version !== 2 || !Number.isFinite(delivery.delaySeconds) || delivery.delaySeconds < 0 || delivery.delaySeconds > 30 || ![null, 25, 50, 75].includes(delivery.scrollPercent) || !["small", "medium", "large"].includes(delivery.size) || !["calendar", "rolling"].includes(delivery.dayBoundary) || typeof delivery.dismissible !== "boolean" || typeof delivery.hideForToday !== "boolean") return null;
|
|
42
|
+
try {
|
|
43
|
+
new Intl.DateTimeFormat("en", { timeZone: delivery.timeZone });
|
|
44
|
+
} catch {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
const popupSlot = campaigns.find((existing) => existing.kind === "popup")?.slotKey;
|
|
49
|
+
if (!campaigns.some((existing) => existing.id === item.id) && campaigns.filter((existing) => existing.slotKey === item.slotKey).length < 20 && (item.kind !== "popup" || popupSlot === void 0 || popupSlot === item.slotKey)) campaigns.push(item);
|
|
50
|
+
}
|
|
51
|
+
const boundary = data.nextChangeAt == null ? null : Date.parse(data.nextChangeAt);
|
|
52
|
+
if (boundary !== null && !Number.isFinite(boundary)) return null;
|
|
53
|
+
return {
|
|
54
|
+
decision: { ...data, campaigns },
|
|
55
|
+
receivedAt,
|
|
56
|
+
serverAtReceipt,
|
|
57
|
+
expiresAt,
|
|
58
|
+
nextAt: boundary === null ? null : receivedAt + boundary - serverAtReceipt
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
function exposureServerNow(snapshot) {
|
|
62
|
+
return snapshot.serverAtReceipt + performance.now() - snapshot.receivedAt;
|
|
63
|
+
}
|
|
64
|
+
function liveCampaigns(snapshot) {
|
|
65
|
+
if (!snapshot || performance.now() >= snapshot.expiresAt) return [];
|
|
66
|
+
const now = exposureServerNow(snapshot);
|
|
67
|
+
return snapshot.decision.campaigns.filter((campaign) => !campaign.endsAt || Date.parse(campaign.endsAt) > now);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// src/exposure-decision-client.ts
|
|
71
|
+
function useExposureDecision(endpoint, pathname, device, slots, enabled = true) {
|
|
72
|
+
const slotsKey = slots.join(",");
|
|
73
|
+
const requestKey = JSON.stringify([endpoint, pathname, device, slotsKey, enabled]);
|
|
74
|
+
const [result, setResult] = useState(null);
|
|
75
|
+
useEffect(() => {
|
|
76
|
+
if (!enabled) return;
|
|
77
|
+
let disposed = false;
|
|
78
|
+
let sequence = 0;
|
|
79
|
+
let controller;
|
|
80
|
+
let pollTimer;
|
|
81
|
+
let expiryTimer;
|
|
82
|
+
let requestTimer;
|
|
83
|
+
let current = null;
|
|
84
|
+
const clearPoll = () => {
|
|
85
|
+
clearTimeout(pollTimer);
|
|
86
|
+
clearTimeout(requestTimer);
|
|
87
|
+
controller?.abort();
|
|
88
|
+
sequence++;
|
|
89
|
+
};
|
|
90
|
+
function schedule(delay) {
|
|
91
|
+
clearTimeout(pollTimer);
|
|
92
|
+
if (!disposed && document.visibilityState !== "hidden") pollTimer = setTimeout(refresh, Math.max(100, Math.min(delay, 6e4)));
|
|
93
|
+
}
|
|
94
|
+
function expireKnownCampaigns() {
|
|
95
|
+
if (!current) return;
|
|
96
|
+
setResult(performance.now() >= current.expiresAt ? null : { key: requestKey, snapshot: current });
|
|
97
|
+
armExpiry();
|
|
98
|
+
}
|
|
99
|
+
function armExpiry() {
|
|
100
|
+
clearTimeout(expiryTimer);
|
|
101
|
+
if (!current || current.expiresAt <= performance.now()) return;
|
|
102
|
+
const ends = current.decision.campaigns.flatMap((campaign) => campaign.endsAt ? [current.receivedAt + Date.parse(campaign.endsAt) - current.serverAtReceipt] : []);
|
|
103
|
+
const future = [current.expiresAt, ...ends].filter((time) => time > performance.now());
|
|
104
|
+
expiryTimer = setTimeout(expireKnownCampaigns, Math.max(1, Math.min(...future) - performance.now()));
|
|
105
|
+
}
|
|
106
|
+
async function refresh() {
|
|
107
|
+
if (disposed || document.visibilityState === "hidden") return;
|
|
108
|
+
clearPoll();
|
|
109
|
+
if (current && performance.now() >= current.expiresAt) setResult(null);
|
|
110
|
+
const seq = sequence;
|
|
111
|
+
controller = new AbortController();
|
|
112
|
+
const startedAt = performance.now();
|
|
113
|
+
let refreshed = false;
|
|
114
|
+
requestTimer = setTimeout(() => controller?.abort(), 1e4);
|
|
115
|
+
try {
|
|
116
|
+
const url = new URL(endpoint, window.location.origin);
|
|
117
|
+
if (url.origin !== window.location.origin || url.username || url.password) throw new Error("Exposure endpoint must be same origin");
|
|
118
|
+
url.search = new URLSearchParams({ path: pathname, device }).toString();
|
|
119
|
+
const response = await fetch(url, { cache: "no-store", credentials: "same-origin", signal: controller.signal });
|
|
120
|
+
if (!response.ok) throw new Error("Exposure unavailable");
|
|
121
|
+
const payload = await response.json();
|
|
122
|
+
if (disposed || seq !== sequence) return;
|
|
123
|
+
const receivedAt = performance.now();
|
|
124
|
+
current = readExposureDecision(payload, slotsKey.split(","), receivedAt, receivedAt - startedAt);
|
|
125
|
+
refreshed = current !== null;
|
|
126
|
+
setResult(current ? { key: requestKey, snapshot: current } : null);
|
|
127
|
+
armExpiry();
|
|
128
|
+
} catch {
|
|
129
|
+
if (disposed || seq !== sequence) return;
|
|
130
|
+
if (!current || performance.now() >= current.expiresAt) {
|
|
131
|
+
current = null;
|
|
132
|
+
setResult(null);
|
|
133
|
+
}
|
|
134
|
+
} finally {
|
|
135
|
+
if (!disposed && seq === sequence) {
|
|
136
|
+
clearTimeout(requestTimer);
|
|
137
|
+
const now = performance.now();
|
|
138
|
+
const next = current?.nextAt;
|
|
139
|
+
const boundaryDelay = next != null && next > now ? next - now : 6e4;
|
|
140
|
+
const refreshDelay = refreshed && current ? Math.max(1e3, current.expiresAt - now - 1e4) : 6e4;
|
|
141
|
+
schedule(Math.min(boundaryDelay, refreshDelay));
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
function onVisibility() {
|
|
146
|
+
if (document.visibilityState === "hidden") clearPoll();
|
|
147
|
+
else void refresh();
|
|
148
|
+
}
|
|
149
|
+
void refresh();
|
|
150
|
+
document.addEventListener("visibilitychange", onVisibility);
|
|
151
|
+
window.addEventListener("focus", refresh);
|
|
152
|
+
return () => {
|
|
153
|
+
disposed = true;
|
|
154
|
+
clearPoll();
|
|
155
|
+
clearTimeout(expiryTimer);
|
|
156
|
+
document.removeEventListener("visibilitychange", onVisibility);
|
|
157
|
+
window.removeEventListener("focus", refresh);
|
|
158
|
+
};
|
|
159
|
+
}, [endpoint, pathname, device, slotsKey, requestKey, enabled]);
|
|
160
|
+
return enabled && result?.key === requestKey ? result.snapshot : null;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// src/exposure-group.tsx
|
|
164
|
+
import { useEffect as useEffect5, useLayoutEffect, useRef as useRef3, useState as useState5 } from "react";
|
|
165
|
+
|
|
166
|
+
// src/exposure-content.tsx
|
|
167
|
+
import { useState as useState2 } from "react";
|
|
168
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
169
|
+
function ExposureContent({ campaign, device, controls }) {
|
|
170
|
+
const { content } = campaign;
|
|
171
|
+
const [failedUrls, setFailedUrls] = useState2([]);
|
|
172
|
+
const preferred = device === "mobile" && content.mobileAssetUrl ? content.mobileAssetUrl : content.assetUrl;
|
|
173
|
+
const textOnly = campaign.delivery?.version === 2 && ["notice", "card"].includes(content.variant);
|
|
174
|
+
const assetUrl = textOnly ? void 0 : preferred && !failedUrls.includes(preferred) ? preferred : content.assetUrl && !failedUrls.includes(content.assetUrl) ? content.assetUrl : void 0;
|
|
175
|
+
const assetAlt = device === "mobile" && assetUrl === content.mobileAssetUrl ? content.mobileAssetAlt ?? content.assetAlt : content.assetAlt;
|
|
176
|
+
const imageOnly = content.variant === "image" && Boolean(assetUrl);
|
|
177
|
+
const linkProps = { href: content.linkUrl, ...content.linkTarget === "blank" ? { target: "_blank", rel: "noopener noreferrer" } : {} };
|
|
178
|
+
const asset = assetUrl ? /* @__PURE__ */ jsx(
|
|
179
|
+
"img",
|
|
180
|
+
{
|
|
181
|
+
className: "rt-exposure__asset",
|
|
182
|
+
src: assetUrl,
|
|
183
|
+
alt: assetAlt ?? content.title,
|
|
184
|
+
onError: () => setFailedUrls((previous) => [...previous, assetUrl])
|
|
185
|
+
}
|
|
186
|
+
) : null;
|
|
187
|
+
return /* @__PURE__ */ jsxs("div", { className: `rt-exposure__body${imageOnly ? " rt-exposure__body--image" : ""}`, children: [
|
|
188
|
+
imageOnly && content.linkUrl ? /* @__PURE__ */ jsx("a", { ...linkProps, "aria-label": content.linkLabel ?? content.title, children: asset }) : asset,
|
|
189
|
+
/* @__PURE__ */ jsxs("div", { className: `rt-exposure__copy${imageOnly ? " rt-exposure__sr-only" : ""}`, children: [
|
|
190
|
+
content.eyebrow ? /* @__PURE__ */ jsx("p", { className: "rt-exposure__eyebrow", children: content.eyebrow }) : null,
|
|
191
|
+
/* @__PURE__ */ jsx("h2", { className: "rt-exposure__title", children: content.title }),
|
|
192
|
+
content.description ? /* @__PURE__ */ jsx("p", { className: "rt-exposure__description", children: content.description }) : null,
|
|
193
|
+
!imageOnly && content.linkUrl && content.linkLabel ? /* @__PURE__ */ jsx("a", { className: "rt-exposure__link", ...linkProps, children: content.linkLabel }) : null
|
|
194
|
+
] }),
|
|
195
|
+
controls
|
|
196
|
+
] });
|
|
197
|
+
}
|
|
198
|
+
function RootTaleExposurePreview({ campaign, placement = campaign.kind === "popup" ? "popup" : "inline", device = "desktop", className = "" }) {
|
|
199
|
+
return /* @__PURE__ */ jsx(
|
|
200
|
+
"div",
|
|
201
|
+
{
|
|
202
|
+
className: `rt-exposure rt-exposure--${campaign.kind} rt-exposure--${contentVariant(campaign)} rt-exposure--preview ${className}`,
|
|
203
|
+
"data-placement": placement,
|
|
204
|
+
"data-device": device,
|
|
205
|
+
"data-size": campaign.delivery?.size ?? "medium",
|
|
206
|
+
children: /* @__PURE__ */ jsx(ExposureContent, { campaign, device })
|
|
207
|
+
}
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
function contentVariant(campaign) {
|
|
211
|
+
return campaign.content.variant.replace(/[^a-zA-Z0-9_-]/g, "");
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// src/exposure-dialog.tsx
|
|
215
|
+
import { useEffect as useEffect2, useRef } from "react";
|
|
216
|
+
import { createPortal } from "react-dom";
|
|
217
|
+
import { jsx as jsx2 } from "react/jsx-runtime";
|
|
218
|
+
var activeDialog = null;
|
|
219
|
+
var openingOwner = null;
|
|
220
|
+
function reserveExposurePopup(owner) {
|
|
221
|
+
if (openingOwner !== null && openingOwner !== owner) return false;
|
|
222
|
+
openingOwner = owner;
|
|
223
|
+
return true;
|
|
224
|
+
}
|
|
225
|
+
function releaseExposurePopup(owner) {
|
|
226
|
+
if (openingOwner === owner) openingOwner = null;
|
|
227
|
+
}
|
|
228
|
+
function exposureCanOpenPopup() {
|
|
229
|
+
const focused = document.activeElement;
|
|
230
|
+
if (focused instanceof HTMLElement && (focused.matches("input, textarea, select") || focused.isContentEditable)) return false;
|
|
231
|
+
if (activeDialog?.isConnected) return false;
|
|
232
|
+
return !Array.from(document.querySelectorAll('dialog[open], [role="dialog"][aria-modal="true"]')).some((element) => {
|
|
233
|
+
for (let ancestor = element; ancestor; ancestor = ancestor.parentElement) {
|
|
234
|
+
if (ancestor.hidden || ancestor.inert || ancestor.getAttribute("aria-hidden") === "true") return false;
|
|
235
|
+
const style = getComputedStyle(ancestor);
|
|
236
|
+
if (style.display === "none" || style.visibility === "hidden" || style.visibility === "collapse") return false;
|
|
237
|
+
}
|
|
238
|
+
return true;
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
function ExposureDialog({ title, className, children, onClose, device, size, themeElement }) {
|
|
242
|
+
const rootRef = useRef(null);
|
|
243
|
+
const dialogRef = useRef(null);
|
|
244
|
+
const closeRef = useRef(onClose);
|
|
245
|
+
useEffect2(() => {
|
|
246
|
+
closeRef.current = onClose;
|
|
247
|
+
}, [onClose]);
|
|
248
|
+
useEffect2(() => {
|
|
249
|
+
const target = rootRef.current;
|
|
250
|
+
if (!themeElement || !target) return;
|
|
251
|
+
const computed = getComputedStyle(themeElement);
|
|
252
|
+
target.style.fontFamily = computed.fontFamily;
|
|
253
|
+
target.style.color = computed.color;
|
|
254
|
+
for (let index = 0; index < computed.length; index++) {
|
|
255
|
+
const key = computed.item(index);
|
|
256
|
+
if (key.startsWith("--")) target.style.setProperty(key, computed.getPropertyValue(key));
|
|
257
|
+
}
|
|
258
|
+
}, [themeElement]);
|
|
259
|
+
useEffect2(() => {
|
|
260
|
+
const root = rootRef.current;
|
|
261
|
+
const dialog = dialogRef.current;
|
|
262
|
+
if (!root || !dialog) return;
|
|
263
|
+
activeDialog = dialog;
|
|
264
|
+
const previous = document.activeElement instanceof HTMLElement ? document.activeElement : null;
|
|
265
|
+
const previousOverflow = document.body.style.overflow;
|
|
266
|
+
const previousPadding = document.body.style.paddingRight;
|
|
267
|
+
const gutter = Math.max(0, window.innerWidth - document.documentElement.clientWidth);
|
|
268
|
+
const padding = Number.parseFloat(getComputedStyle(document.body).paddingRight) || 0;
|
|
269
|
+
document.body.style.overflow = "hidden";
|
|
270
|
+
if (gutter > 0) document.body.style.paddingRight = `${padding + gutter}px`;
|
|
271
|
+
const inertBefore = /* @__PURE__ */ new Map();
|
|
272
|
+
function inertBackground() {
|
|
273
|
+
for (const child of document.body.children) {
|
|
274
|
+
if (!(child instanceof HTMLElement) || child === root || child.contains(root) || inertBefore.has(child)) continue;
|
|
275
|
+
inertBefore.set(child, child.inert);
|
|
276
|
+
child.inert = true;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
inertBackground();
|
|
280
|
+
const observer = new MutationObserver(inertBackground);
|
|
281
|
+
observer.observe(document.body, { childList: true });
|
|
282
|
+
const focusable = () => Array.from(dialog.querySelectorAll('a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex="0"]')).filter((element) => !element.hidden && !element.closest('[hidden], [aria-hidden="true"]'));
|
|
283
|
+
(dialog.querySelector("[data-exposure-close]") ?? dialog).focus({ preventScroll: true });
|
|
284
|
+
const handleKey = (event) => {
|
|
285
|
+
if (event.key === "Escape") {
|
|
286
|
+
event.preventDefault();
|
|
287
|
+
event.stopPropagation();
|
|
288
|
+
closeRef.current();
|
|
289
|
+
}
|
|
290
|
+
if (event.key !== "Tab") return;
|
|
291
|
+
const items = focusable();
|
|
292
|
+
const first = items[0];
|
|
293
|
+
const last = items[items.length - 1];
|
|
294
|
+
if (!first || !last) {
|
|
295
|
+
event.preventDefault();
|
|
296
|
+
dialog.focus();
|
|
297
|
+
} else if (event.shiftKey && (document.activeElement === first || document.activeElement === dialog)) {
|
|
298
|
+
event.preventDefault();
|
|
299
|
+
last.focus();
|
|
300
|
+
} else if (!event.shiftKey && (document.activeElement === last || document.activeElement === dialog)) {
|
|
301
|
+
event.preventDefault();
|
|
302
|
+
first.focus();
|
|
303
|
+
}
|
|
304
|
+
};
|
|
305
|
+
const handleFocus = (event) => {
|
|
306
|
+
if (event.target instanceof Node && !dialog.contains(event.target)) (focusable()[0] ?? dialog).focus({ preventScroll: true });
|
|
307
|
+
};
|
|
308
|
+
document.addEventListener("keydown", handleKey, true);
|
|
309
|
+
document.addEventListener("focusin", handleFocus);
|
|
310
|
+
return () => {
|
|
311
|
+
observer.disconnect();
|
|
312
|
+
document.removeEventListener("keydown", handleKey, true);
|
|
313
|
+
document.removeEventListener("focusin", handleFocus);
|
|
314
|
+
for (const [element, wasInert] of inertBefore) element.inert = wasInert;
|
|
315
|
+
if (activeDialog === dialog) activeDialog = null;
|
|
316
|
+
document.body.style.overflow = previousOverflow;
|
|
317
|
+
document.body.style.paddingRight = previousPadding;
|
|
318
|
+
if (previous?.isConnected && !previous.inert) previous.focus({ preventScroll: true });
|
|
319
|
+
else {
|
|
320
|
+
const fallback = document.querySelector('main, [role="main"]') ?? document.body;
|
|
321
|
+
const hadTabIndex = fallback.hasAttribute("tabindex");
|
|
322
|
+
if (!hadTabIndex) fallback.tabIndex = -1;
|
|
323
|
+
fallback.focus({ preventScroll: true });
|
|
324
|
+
if (!hadTabIndex) fallback.removeAttribute("tabindex");
|
|
325
|
+
}
|
|
326
|
+
};
|
|
327
|
+
}, []);
|
|
328
|
+
return createPortal(/* @__PURE__ */ jsx2("div", { ref: rootRef, className: "rt-exposure-backdrop", "data-device": device, children: /* @__PURE__ */ jsx2("section", { ref: dialogRef, className, role: "dialog", "aria-modal": "true", "aria-label": title, tabIndex: -1, "data-size": size, children }) }), document.body);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// src/exposure-carousel.tsx
|
|
332
|
+
import { useCallback, useEffect as useEffect3, useRef as useRef2, useState as useState3 } from "react";
|
|
333
|
+
import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
334
|
+
function useExposureCarousel(campaigns, enabled, durationMs, popup) {
|
|
335
|
+
const [selection, setSelection] = useState3({ id: campaigns[0]?.id, cycle: 0 });
|
|
336
|
+
const [elapsedState, setElapsed] = useState3({ id: campaigns[0]?.id, value: 0 });
|
|
337
|
+
const [userPaused, setUserPaused] = useState3(false);
|
|
338
|
+
const [motionAllowed, setMotionAllowed] = useState3(false);
|
|
339
|
+
const [reducedMotion, setReducedMotion] = useState3(false);
|
|
340
|
+
const [hovered, setHovered] = useState3(false);
|
|
341
|
+
const [focused, setFocused] = useState3(false);
|
|
342
|
+
const [touching, setTouching] = useState3(false);
|
|
343
|
+
const [environmentPaused, setEnvironmentPaused] = useState3(false);
|
|
344
|
+
const elapsedRef = useRef2(0);
|
|
345
|
+
const touchOrigin = useRef2(null);
|
|
346
|
+
const index = Math.max(0, campaigns.findIndex((campaign2) => campaign2.id === selection.id));
|
|
347
|
+
const campaign = campaigns[index];
|
|
348
|
+
const elapsed = elapsedState.id === campaign?.id ? elapsedState.value : 0;
|
|
349
|
+
const latest = useRef2({ campaigns, index });
|
|
350
|
+
useEffect3(() => {
|
|
351
|
+
latest.current = { campaigns, index };
|
|
352
|
+
}, [campaigns, index]);
|
|
353
|
+
const duration = Math.max(3e3, Math.min(3e4, durationMs));
|
|
354
|
+
const paused = userPaused || hovered || focused || touching || environmentPaused || reducedMotion && !motionAllowed;
|
|
355
|
+
const select = useCallback((nextIndex) => {
|
|
356
|
+
const items = latest.current.campaigns;
|
|
357
|
+
if (!items.length) return;
|
|
358
|
+
const normalized = (nextIndex + items.length) % items.length;
|
|
359
|
+
latest.current.index = normalized;
|
|
360
|
+
elapsedRef.current = 0;
|
|
361
|
+
setElapsed({ id: items[normalized].id, value: 0 });
|
|
362
|
+
setSelection((previous2) => ({ id: items[normalized].id, cycle: previous2.cycle + 1 }));
|
|
363
|
+
}, []);
|
|
364
|
+
const next = useCallback(() => select(latest.current.index + 1), [select]);
|
|
365
|
+
const previous = useCallback(() => select(latest.current.index - 1), [select]);
|
|
366
|
+
useEffect3(() => {
|
|
367
|
+
const media = window.matchMedia("(prefers-reduced-motion: reduce)");
|
|
368
|
+
const changed = () => setReducedMotion(media.matches);
|
|
369
|
+
changed();
|
|
370
|
+
media.addEventListener("change", changed);
|
|
371
|
+
return () => media.removeEventListener("change", changed);
|
|
372
|
+
}, []);
|
|
373
|
+
useEffect3(() => {
|
|
374
|
+
const changed = () => setEnvironmentPaused(document.visibilityState === "hidden" || !popup && !exposureCanOpenPopup());
|
|
375
|
+
changed();
|
|
376
|
+
const check = setInterval(changed, 250);
|
|
377
|
+
document.addEventListener("visibilitychange", changed);
|
|
378
|
+
return () => {
|
|
379
|
+
clearInterval(check);
|
|
380
|
+
document.removeEventListener("visibilitychange", changed);
|
|
381
|
+
};
|
|
382
|
+
}, [popup]);
|
|
383
|
+
useEffect3(() => {
|
|
384
|
+
elapsedRef.current = 0;
|
|
385
|
+
}, [campaign?.id, selection.cycle]);
|
|
386
|
+
useEffect3(() => {
|
|
387
|
+
if (!enabled || paused || campaigns.length < 2) return;
|
|
388
|
+
let previousTick = performance.now();
|
|
389
|
+
const timer = setInterval(() => {
|
|
390
|
+
const now = performance.now();
|
|
391
|
+
elapsedRef.current += now - previousTick;
|
|
392
|
+
previousTick = now;
|
|
393
|
+
if (elapsedRef.current >= duration) next();
|
|
394
|
+
else setElapsed({ id: campaign?.id, value: elapsedRef.current });
|
|
395
|
+
}, 100);
|
|
396
|
+
return () => clearInterval(timer);
|
|
397
|
+
}, [campaign?.id, campaigns.length, enabled, paused, duration, next, selection.cycle]);
|
|
398
|
+
function onTouchStart(event) {
|
|
399
|
+
const touch = event.touches[0];
|
|
400
|
+
if (touch) {
|
|
401
|
+
touchOrigin.current = { x: touch.clientX, y: touch.clientY };
|
|
402
|
+
setTouching(true);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
function onTouchEnd(event) {
|
|
406
|
+
const origin = touchOrigin.current;
|
|
407
|
+
const touch = event.changedTouches[0];
|
|
408
|
+
touchOrigin.current = null;
|
|
409
|
+
setTouching(false);
|
|
410
|
+
if (!origin || !touch || campaigns.length < 2) return;
|
|
411
|
+
const dx = touch.clientX - origin.x;
|
|
412
|
+
const dy = touch.clientY - origin.y;
|
|
413
|
+
if (Math.abs(dx) >= 48 && Math.abs(dx) > Math.abs(dy) * 1.4) {
|
|
414
|
+
if (dx < 0) next();
|
|
415
|
+
else previous();
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
function onFocus(event) {
|
|
419
|
+
if (!(event.target instanceof HTMLElement)) return;
|
|
420
|
+
if (event.target.closest("[data-exposure-close], .rt-exposure__hide-today")) {
|
|
421
|
+
setFocused(false);
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
setFocused(true);
|
|
425
|
+
}
|
|
426
|
+
const interactions = {
|
|
427
|
+
onPointerEnter: (event) => {
|
|
428
|
+
if (event.pointerType === "mouse") setHovered(true);
|
|
429
|
+
},
|
|
430
|
+
onPointerLeave: () => setHovered(false),
|
|
431
|
+
onFocusCapture: onFocus,
|
|
432
|
+
onBlurCapture: (event) => {
|
|
433
|
+
if (!(event.relatedTarget instanceof Node) || !event.currentTarget.contains(event.relatedTarget)) setFocused(false);
|
|
434
|
+
},
|
|
435
|
+
onTouchStart,
|
|
436
|
+
onTouchEnd,
|
|
437
|
+
onTouchCancel: () => {
|
|
438
|
+
touchOrigin.current = null;
|
|
439
|
+
setTouching(false);
|
|
440
|
+
}
|
|
441
|
+
};
|
|
442
|
+
const toggle = () => {
|
|
443
|
+
setFocused(false);
|
|
444
|
+
setHovered(false);
|
|
445
|
+
if (reducedMotion && !motionAllowed) {
|
|
446
|
+
setMotionAllowed(true);
|
|
447
|
+
setUserPaused(false);
|
|
448
|
+
} else setUserPaused((value) => !value);
|
|
449
|
+
};
|
|
450
|
+
return {
|
|
451
|
+
campaign,
|
|
452
|
+
index,
|
|
453
|
+
paused,
|
|
454
|
+
userPaused: userPaused || reducedMotion && !motionAllowed,
|
|
455
|
+
progress: Math.min(1, elapsed / duration),
|
|
456
|
+
seconds: Math.max(1, Math.ceil((duration - elapsed) / 1e3)),
|
|
457
|
+
next,
|
|
458
|
+
previous,
|
|
459
|
+
select,
|
|
460
|
+
toggle,
|
|
461
|
+
interactions
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
function ExposureCarouselControls({ carousel, campaigns }) {
|
|
465
|
+
if (campaigns.length < 2) return null;
|
|
466
|
+
const firstDot = Math.max(0, Math.min(campaigns.length - 5, carousel.index - 2));
|
|
467
|
+
return /* @__PURE__ */ jsxs2("div", { className: "rt-exposure-carousel-controls", children: [
|
|
468
|
+
/* @__PURE__ */ jsxs2("div", { className: "rt-exposure-carousel-meta", children: [
|
|
469
|
+
/* @__PURE__ */ jsxs2("span", { className: "rt-exposure-carousel-count", "aria-live": carousel.paused ? "polite" : "off", children: [
|
|
470
|
+
/* @__PURE__ */ jsx3("strong", { children: String(carousel.index + 1).padStart(2, "0") }),
|
|
471
|
+
/* @__PURE__ */ jsxs2("span", { children: [
|
|
472
|
+
" / ",
|
|
473
|
+
String(campaigns.length).padStart(2, "0")
|
|
474
|
+
] })
|
|
475
|
+
] }),
|
|
476
|
+
/* @__PURE__ */ jsx3("span", { className: "rt-exposure-carousel-time", children: carousel.paused ? "\uC804\uD658 \uC77C\uC2DC\uC815\uC9C0" : `${carousel.seconds}\uCD08 \uD6C4 \uB2E4\uC74C \uC548\uB0B4` }),
|
|
477
|
+
/* @__PURE__ */ jsxs2("div", { className: "rt-exposure-carousel-actions", children: [
|
|
478
|
+
/* @__PURE__ */ jsx3("button", { type: "button", "aria-label": "\uC774\uC804 \uC548\uB0B4", onClick: carousel.previous, children: /* @__PURE__ */ jsx3("svg", { viewBox: "0 0 24 24", "aria-hidden": "true", children: /* @__PURE__ */ jsx3("path", { d: "m14 6-6 6 6 6" }) }) }),
|
|
479
|
+
/* @__PURE__ */ jsx3("button", { type: "button", "aria-label": carousel.userPaused ? "\uC790\uB3D9 \uC804\uD658 \uC7AC\uC0DD" : "\uC790\uB3D9 \uC804\uD658 \uC77C\uC2DC\uC815\uC9C0", onClick: carousel.toggle, "aria-pressed": carousel.userPaused, children: /* @__PURE__ */ jsx3("svg", { viewBox: "0 0 24 24", "aria-hidden": "true", children: carousel.userPaused ? /* @__PURE__ */ jsx3("path", { d: "m9 5 10 7-10 7Z" }) : /* @__PURE__ */ jsx3("path", { d: "M9 5v14M15 5v14" }) }) }),
|
|
480
|
+
/* @__PURE__ */ jsx3("button", { type: "button", "aria-label": "\uB2E4\uC74C \uC548\uB0B4", onClick: carousel.next, children: /* @__PURE__ */ jsx3("svg", { viewBox: "0 0 24 24", "aria-hidden": "true", children: /* @__PURE__ */ jsx3("path", { d: "m10 6 6 6-6 6" }) }) })
|
|
481
|
+
] })
|
|
482
|
+
] }),
|
|
483
|
+
/* @__PURE__ */ jsx3("div", { className: "rt-exposure-carousel-progress", role: "progressbar", "aria-label": "\uB2E4\uC74C \uC548\uB0B4\uAE4C\uC9C0 \uC9C4\uD589", "aria-valuemin": 0, "aria-valuemax": 100, "aria-valuenow": Math.round(carousel.progress * 100), children: /* @__PURE__ */ jsx3("span", { style: { transform: `scaleX(${carousel.progress})` } }) }),
|
|
484
|
+
/* @__PURE__ */ jsx3("div", { className: "rt-exposure-carousel-dots", "aria-label": "\uC548\uB0B4 \uC120\uD0DD", children: campaigns.slice(firstDot, firstDot + 5).map((campaign, offset) => {
|
|
485
|
+
const index = firstDot + offset;
|
|
486
|
+
return /* @__PURE__ */ jsx3("button", { type: "button", "aria-label": `${index + 1}\uBC88\uC9F8 \uC548\uB0B4: ${campaign.content.title}`, "aria-current": index === carousel.index ? "true" : void 0, onClick: () => carousel.select(index), children: /* @__PURE__ */ jsx3("span", {}) }, campaign.id);
|
|
487
|
+
}) })
|
|
488
|
+
] });
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// src/exposure-scroll.ts
|
|
492
|
+
import { useEffect as useEffect4, useState as useState4 } from "react";
|
|
493
|
+
function useMobileExposureScroll(enabled) {
|
|
494
|
+
const [hidden, setHidden] = useState4(false);
|
|
495
|
+
useEffect4(() => {
|
|
496
|
+
if (!enabled) {
|
|
497
|
+
setHidden(false);
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
let previous = Math.max(0, window.scrollY);
|
|
501
|
+
let distance = 0;
|
|
502
|
+
const onScroll = () => {
|
|
503
|
+
const current = Math.max(0, window.scrollY);
|
|
504
|
+
const delta = current - previous;
|
|
505
|
+
previous = current;
|
|
506
|
+
if (current < 32) {
|
|
507
|
+
distance = 0;
|
|
508
|
+
setHidden(false);
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
if (Math.sign(delta) !== Math.sign(distance)) distance = 0;
|
|
512
|
+
distance += delta;
|
|
513
|
+
if (distance >= 12) {
|
|
514
|
+
setHidden(true);
|
|
515
|
+
distance = 0;
|
|
516
|
+
} else if (distance <= -8) {
|
|
517
|
+
setHidden(false);
|
|
518
|
+
distance = 0;
|
|
519
|
+
}
|
|
520
|
+
};
|
|
521
|
+
window.addEventListener("scroll", onScroll, { passive: true });
|
|
522
|
+
return () => window.removeEventListener("scroll", onScroll);
|
|
523
|
+
}, [enabled]);
|
|
524
|
+
return enabled && hidden;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// src/exposure-storage.ts
|
|
528
|
+
function read(kind, key) {
|
|
529
|
+
try {
|
|
530
|
+
const store = kind === "session" ? sessionStorage : localStorage;
|
|
531
|
+
return store.getItem(key);
|
|
532
|
+
} catch {
|
|
533
|
+
return null;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
var cookieFallback = /* @__PURE__ */ new Map();
|
|
537
|
+
var sessionCookie = "rt_exposure_session";
|
|
538
|
+
var preferencesCookie = "rt_exposure_preferences";
|
|
539
|
+
function compactKey(value, scope) {
|
|
540
|
+
let left = 2166136261;
|
|
541
|
+
let right = 2246822519;
|
|
542
|
+
for (let index = 0; index < value.length; index++) {
|
|
543
|
+
left = Math.imul(left ^ value.charCodeAt(index), 16777619);
|
|
544
|
+
right = Math.imul(right ^ value.charCodeAt(index), 3266489917);
|
|
545
|
+
}
|
|
546
|
+
return `${scope}${(left >>> 0).toString(16).padStart(8, "0")}${(right >>> 0).toString(16).padStart(8, "0")}`;
|
|
547
|
+
}
|
|
548
|
+
function rawCookie(name) {
|
|
549
|
+
const entry = document.cookie.split(";").map((part) => part.trim()).find((part) => part.startsWith(`${name}=`));
|
|
550
|
+
return entry ? entry.slice(name.length + 1) : null;
|
|
551
|
+
}
|
|
552
|
+
function readPreferences(name) {
|
|
553
|
+
let raw;
|
|
554
|
+
try {
|
|
555
|
+
raw = cookieFallback.get(name) ?? rawCookie(name) ?? null;
|
|
556
|
+
} catch {
|
|
557
|
+
raw = cookieFallback.get(name) ?? null;
|
|
558
|
+
}
|
|
559
|
+
const result = /* @__PURE__ */ new Map();
|
|
560
|
+
for (const entry of raw?.split(".") ?? []) {
|
|
561
|
+
const match = /^([cg][a-f0-9]{16})_([svun])([0-9a-z]+)$/.exec(entry);
|
|
562
|
+
if (match) result.set(match[1], `${match[2]}:${match[2] === "u" ? Number.parseInt(match[3], 36) * 1e3 : 1}`);
|
|
563
|
+
}
|
|
564
|
+
return result;
|
|
565
|
+
}
|
|
566
|
+
function writePreferenceKey(key, value, persistent, now) {
|
|
567
|
+
const name = persistent ? preferencesCookie : sessionCookie;
|
|
568
|
+
const values = readPreferences(name);
|
|
569
|
+
for (const [id, preference] of values) if (preference.startsWith("u:") && Number(preference.slice(2)) <= now) values.delete(id);
|
|
570
|
+
values.delete(key);
|
|
571
|
+
values.set(key, value);
|
|
572
|
+
const packed = [...values].slice(-100).map(([id, preference]) => `${id}_${preference[0]}${preference.startsWith("u:") ? Math.ceil(Number(preference.slice(2)) / 1e3).toString(36) : "1"}`).join(".");
|
|
573
|
+
try {
|
|
574
|
+
const secure = location.protocol === "https:" ? "; Secure" : "";
|
|
575
|
+
document.cookie = `${name}=${packed}; Path=/; SameSite=Lax${secure}${persistent ? "; Max-Age=34560000" : ""}`;
|
|
576
|
+
if (rawCookie(name) !== packed) cookieFallback.set(name, packed);
|
|
577
|
+
else cookieFallback.delete(name);
|
|
578
|
+
} catch {
|
|
579
|
+
cookieFallback.set(name, packed);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
function dateInZone(epoch, timeZone) {
|
|
583
|
+
return new Intl.DateTimeFormat("en-CA", { timeZone, year: "numeric", month: "2-digit", day: "2-digit" }).format(epoch);
|
|
584
|
+
}
|
|
585
|
+
function nextExposureMidnight(now, timeZone) {
|
|
586
|
+
const today = dateInZone(now, timeZone);
|
|
587
|
+
let low = now;
|
|
588
|
+
let high = now + 48 * 36e5;
|
|
589
|
+
while (high - low > 1) {
|
|
590
|
+
const mid = Math.floor((low + high) / 2);
|
|
591
|
+
if (dateInZone(mid, timeZone) === today) low = mid;
|
|
592
|
+
else high = mid;
|
|
593
|
+
}
|
|
594
|
+
return high;
|
|
595
|
+
}
|
|
596
|
+
function exposureWasHidden(campaign, now, legacyPrefixes = []) {
|
|
597
|
+
const persistent = readPreferences(preferencesCookie);
|
|
598
|
+
const preferences = [persistent.get(compactKey(campaign.slotKey, "g")), persistent.get(compactKey(campaign.id, "c")), readPreferences(sessionCookie).get(compactKey(campaign.id, "c"))];
|
|
599
|
+
if (preferences.some((preference) => preference?.startsWith("s:") || preference?.startsWith("v:") || preference === "n:1" || preference?.startsWith("u:") && Number(preference.slice(2)) > now)) return true;
|
|
600
|
+
const until = Number(read("local", `rt-exposure-until:${campaign.id}`));
|
|
601
|
+
if (Number.isFinite(until) && until > now) return true;
|
|
602
|
+
if (campaign.repeat === "always") return false;
|
|
603
|
+
if (campaign.repeat === "session" && read("session", `rt-exposure-viewed:${campaign.id}`)) return true;
|
|
604
|
+
const kind = campaign.repeat === "session" ? "session" : "local";
|
|
605
|
+
for (const prefix of ["rt-exposure-dismissed:", ...legacyPrefixes]) {
|
|
606
|
+
const value = read(kind, `${prefix}${campaign.id}`);
|
|
607
|
+
if (value === null) continue;
|
|
608
|
+
if (campaign.repeat !== "day") return true;
|
|
609
|
+
const dismissedAt = Number(value);
|
|
610
|
+
const calendar = prefix === "rt-exposure-dismissed:" && campaign.delivery?.dayBoundary === "calendar";
|
|
611
|
+
if (Number.isFinite(dismissedAt) && (calendar ? dateInZone(dismissedAt, campaign.delivery.timeZone) === dateInZone(now, campaign.delivery.timeZone) : now - dismissedAt < 864e5)) return true;
|
|
612
|
+
}
|
|
613
|
+
return false;
|
|
614
|
+
}
|
|
615
|
+
function rememberExposureView(campaign, now) {
|
|
616
|
+
if (campaign.repeat === "session") writePreferenceKey(compactKey(campaign.id, "c"), "v:1", false, now);
|
|
617
|
+
if (campaign.delivery && (campaign.repeat === "day" || campaign.repeat === "never")) rememberExposureDismissal(campaign, now);
|
|
618
|
+
}
|
|
619
|
+
function rememberExposureDismissal(campaign, now, forToday = false) {
|
|
620
|
+
if (forToday || campaign.repeat === "day") {
|
|
621
|
+
const until = forToday || campaign.delivery?.dayBoundary === "calendar" ? nextExposureMidnight(now, campaign.delivery?.timeZone ?? "Asia/Seoul") : now + 864e5;
|
|
622
|
+
writePreferenceKey(compactKey(campaign.id, "c"), `u:${until}`, true, now);
|
|
623
|
+
} else if (campaign.repeat === "never") writePreferenceKey(compactKey(campaign.id, "c"), "n:1", true, now);
|
|
624
|
+
else writePreferenceKey(compactKey(campaign.id, "c"), "s:1", false, now);
|
|
625
|
+
}
|
|
626
|
+
function rememberExposureGroupDismissal(campaigns, now, forToday = false) {
|
|
627
|
+
if (forToday && campaigns[0]) {
|
|
628
|
+
writePreferenceKey(compactKey(campaigns[0].slotKey, "g"), `u:${nextExposureMidnight(now, campaigns[0].delivery?.timeZone ?? "Asia/Seoul")}`, true, now);
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
for (const campaign of campaigns) rememberExposureDismissal(campaign, now, forToday);
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// src/exposure-group.tsx
|
|
635
|
+
import { Fragment, jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
636
|
+
function useSlideScrollReset(campaignId) {
|
|
637
|
+
const ref = useRef3(null);
|
|
638
|
+
useLayoutEffect(() => {
|
|
639
|
+
if (ref.current) ref.current.scrollTop = 0;
|
|
640
|
+
}, [campaignId]);
|
|
641
|
+
return ref;
|
|
642
|
+
}
|
|
643
|
+
function exposureGroupSize(campaigns) {
|
|
644
|
+
if (campaigns.some((campaign) => campaign.delivery?.size === "large")) return "large";
|
|
645
|
+
if (campaigns.some((campaign) => (campaign.delivery?.size ?? "medium") === "medium")) return "medium";
|
|
646
|
+
return "small";
|
|
647
|
+
}
|
|
648
|
+
function ExposureCampaignGroup({ campaigns, runtime, placement, className }) {
|
|
649
|
+
const [visible, setVisible] = useState5(false);
|
|
650
|
+
const [dismissed, setDismissed] = useState5(false);
|
|
651
|
+
const [height, setMeasuredHeight] = useState5(0);
|
|
652
|
+
const [inView, setInView] = useState5(() => placement !== "inline" || typeof IntersectionObserver === "undefined");
|
|
653
|
+
const measuredRef = useRef3(null);
|
|
654
|
+
const campaignIds = campaigns.map((campaign2) => campaign2.id).join(",");
|
|
655
|
+
const [admission, setAdmission] = useState5(() => ({
|
|
656
|
+
ids: campaignIds,
|
|
657
|
+
allowed: new Map(campaigns.map((campaign2) => [campaign2.id, !exposureWasHidden(campaign2, runtime.now(), runtime.legacyPrefixes)]))
|
|
658
|
+
}));
|
|
659
|
+
if (admission.ids !== campaignIds) {
|
|
660
|
+
const allowed = new Map(admission.allowed);
|
|
661
|
+
for (const campaign2 of campaigns) if (!allowed.has(campaign2.id)) allowed.set(campaign2.id, !exposureWasHidden(campaign2, runtime.now(), runtime.legacyPrefixes));
|
|
662
|
+
setAdmission({ ids: campaignIds, allowed });
|
|
663
|
+
}
|
|
664
|
+
const viewed = useRef3(/* @__PURE__ */ new Set());
|
|
665
|
+
const owner = useRef3(/* @__PURE__ */ Symbol("exposure-group"));
|
|
666
|
+
const latest = useRef3({ campaigns, runtime });
|
|
667
|
+
useEffect5(() => {
|
|
668
|
+
latest.current = { campaigns, runtime };
|
|
669
|
+
}, [campaigns, runtime]);
|
|
670
|
+
const eligible = campaigns.filter((campaign2) => admission.allowed.get(campaign2.id));
|
|
671
|
+
const first = eligible[0];
|
|
672
|
+
const firstId = first?.id;
|
|
673
|
+
const slotKey = campaigns[0]?.slotKey ?? "";
|
|
674
|
+
const hiddenOnScroll = useMobileExposureScroll(runtime.device === "mobile" && runtime.hideMobileBannersOnScroll && (placement === "top" || placement === "bottom"));
|
|
675
|
+
const active = visible && !dismissed && eligible.length > 0;
|
|
676
|
+
const carousel = useExposureCarousel(eligible, active && !hiddenOnScroll && inView, runtime.slideDurationMs, placement === "popup");
|
|
677
|
+
const campaign = carousel.campaign;
|
|
678
|
+
const activeId = campaign?.id;
|
|
679
|
+
const setHeight = runtime.setHeight;
|
|
680
|
+
const device = runtime.device;
|
|
681
|
+
const slideRef = useSlideScrollReset(activeId);
|
|
682
|
+
useEffect5(() => {
|
|
683
|
+
const first2 = latest.current.campaigns.find((campaign2) => campaign2.id === firstId);
|
|
684
|
+
if (visible || dismissed || !first2) return;
|
|
685
|
+
let opened = false;
|
|
686
|
+
let delayPassed = false;
|
|
687
|
+
const scrollPercent = first2.delivery?.scrollPercent;
|
|
688
|
+
const tryOpen = () => {
|
|
689
|
+
if (opened || !delayPassed || document.visibilityState === "hidden") return;
|
|
690
|
+
if (scrollPercent) {
|
|
691
|
+
const scrollable = document.documentElement.scrollHeight - window.innerHeight;
|
|
692
|
+
if (scrollable <= 0 || window.scrollY / scrollable * 100 < scrollPercent) return;
|
|
693
|
+
}
|
|
694
|
+
if (placement === "popup") {
|
|
695
|
+
if (!exposureCanOpenPopup() || !reserveExposurePopup(owner.current)) return;
|
|
696
|
+
if (!latest.current.runtime.claimPopup(first2.slotKey)) {
|
|
697
|
+
releaseExposurePopup(owner.current);
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
opened = true;
|
|
702
|
+
setVisible(true);
|
|
703
|
+
clearInterval(retry);
|
|
704
|
+
};
|
|
705
|
+
const timer = setTimeout(() => {
|
|
706
|
+
delayPassed = true;
|
|
707
|
+
tryOpen();
|
|
708
|
+
}, placement === "popup" ? (first2.delivery?.delaySeconds ?? 0) * 1e3 : 0);
|
|
709
|
+
const retry = setInterval(tryOpen, 500);
|
|
710
|
+
window.addEventListener("scroll", tryOpen, { passive: true });
|
|
711
|
+
window.addEventListener("resize", tryOpen);
|
|
712
|
+
document.addEventListener("visibilitychange", tryOpen);
|
|
713
|
+
return () => {
|
|
714
|
+
clearTimeout(timer);
|
|
715
|
+
clearInterval(retry);
|
|
716
|
+
window.removeEventListener("scroll", tryOpen);
|
|
717
|
+
window.removeEventListener("resize", tryOpen);
|
|
718
|
+
document.removeEventListener("visibilitychange", tryOpen);
|
|
719
|
+
};
|
|
720
|
+
}, [firstId, visible, dismissed, placement]);
|
|
721
|
+
useEffect5(() => {
|
|
722
|
+
const token = owner.current;
|
|
723
|
+
return () => releaseExposurePopup(token);
|
|
724
|
+
}, []);
|
|
725
|
+
useEffect5(() => {
|
|
726
|
+
if (placement !== "inline" || !active) return;
|
|
727
|
+
if (typeof IntersectionObserver === "undefined") return;
|
|
728
|
+
const element = measuredRef.current;
|
|
729
|
+
const observer = new IntersectionObserver(([entry]) => setInView(Boolean(entry?.isIntersecting)), { threshold: 0.1 });
|
|
730
|
+
if (element) observer.observe(element);
|
|
731
|
+
return () => observer.disconnect();
|
|
732
|
+
}, [placement, active]);
|
|
733
|
+
useEffect5(() => {
|
|
734
|
+
const campaign2 = latest.current.campaigns.find((item) => item.id === activeId);
|
|
735
|
+
if (!active || hiddenOnScroll || !inView || !campaign2 || viewed.current.has(campaign2.id)) return;
|
|
736
|
+
viewed.current.add(campaign2.id);
|
|
737
|
+
rememberExposureView(campaign2, latest.current.runtime.now());
|
|
738
|
+
}, [active, hiddenOnScroll, inView, activeId]);
|
|
739
|
+
useEffect5(() => {
|
|
740
|
+
if (placement !== "top" && placement !== "bottom") return;
|
|
741
|
+
const element = measuredRef.current;
|
|
742
|
+
const measure = () => {
|
|
743
|
+
const rect = element?.getBoundingClientRect();
|
|
744
|
+
const fullHeight = active && rect ? Math.ceil(rect.height) : 0;
|
|
745
|
+
setMeasuredHeight((previous) => active ? Math.max(previous, fullHeight) : 0);
|
|
746
|
+
setHeight(slotKey, placement, hiddenOnScroll ? 0 : placement === "top" && device === "desktop" && rect ? Math.max(0, Math.min(fullHeight, Math.ceil(rect.bottom))) : fullHeight);
|
|
747
|
+
};
|
|
748
|
+
measure();
|
|
749
|
+
const observer = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(measure);
|
|
750
|
+
if (element) observer?.observe(element);
|
|
751
|
+
window.addEventListener("resize", measure);
|
|
752
|
+
window.addEventListener("scroll", measure, { passive: true });
|
|
753
|
+
return () => {
|
|
754
|
+
observer?.disconnect();
|
|
755
|
+
window.removeEventListener("resize", measure);
|
|
756
|
+
window.removeEventListener("scroll", measure);
|
|
757
|
+
setHeight(slotKey, placement, 0);
|
|
758
|
+
};
|
|
759
|
+
}, [active, placement, slotKey, setHeight, hiddenOnScroll, device]);
|
|
760
|
+
function close(forToday = false) {
|
|
761
|
+
releaseExposurePopup(owner.current);
|
|
762
|
+
rememberExposureGroupDismissal(latest.current.campaigns, latest.current.runtime.now(), forToday);
|
|
763
|
+
setDismissed(true);
|
|
764
|
+
}
|
|
765
|
+
if (!active || !campaign) return null;
|
|
766
|
+
const closeControls = /* @__PURE__ */ jsxs3(Fragment, { children: [
|
|
767
|
+
placement === "popup" || placement === "bottom" || (campaign.delivery?.dismissible ?? true) ? /* @__PURE__ */ jsx4("button", { type: "button", "data-exposure-close": "", className: "rt-exposure__close", "aria-label": "\uC548\uB0B4 \uB2EB\uAE30", onClick: () => close(), children: "\xD7" }) : null,
|
|
768
|
+
placement === "popup" && campaigns.some((item) => item.delivery?.hideForToday) ? /* @__PURE__ */ jsx4("button", { type: "button", className: "rt-exposure__hide-today", onClick: () => close(true), children: "\uC624\uB298 \uADF8\uB9CC \uBCF4\uAE30" }) : null
|
|
769
|
+
] });
|
|
770
|
+
const body = /* @__PURE__ */ jsxs3("div", { className: "rt-exposure-carousel", "aria-roledescription": eligible.length > 1 ? "\uCE90\uB7EC\uC140" : void 0, ...carousel.interactions, children: [
|
|
771
|
+
/* @__PURE__ */ jsx4("div", { ref: slideRef, className: "rt-exposure-slide", role: "group", "aria-roledescription": eligible.length > 1 ? "\uC2AC\uB77C\uC774\uB4DC" : void 0, "aria-label": `${carousel.index + 1} / ${eligible.length}`, children: /* @__PURE__ */ jsx4(ExposureContent, { campaign, device: runtime.device }, campaign.id) }),
|
|
772
|
+
closeControls,
|
|
773
|
+
/* @__PURE__ */ jsx4(ExposureCarouselControls, { carousel, campaigns: eligible })
|
|
774
|
+
] });
|
|
775
|
+
const classes = `rt-exposure rt-exposure--${campaign.kind} rt-exposure--${contentVariant(campaign)}`;
|
|
776
|
+
if (placement === "popup") return /* @__PURE__ */ jsx4(
|
|
777
|
+
ExposureDialog,
|
|
778
|
+
{
|
|
779
|
+
title: "\uD648\uD398\uC774\uC9C0 \uC548\uB0B4",
|
|
780
|
+
className: `${classes} ${className}`,
|
|
781
|
+
onClose: () => close(),
|
|
782
|
+
device: runtime.device,
|
|
783
|
+
size: exposureGroupSize(eligible),
|
|
784
|
+
themeElement: runtime.themeElement,
|
|
785
|
+
children: body
|
|
786
|
+
}
|
|
787
|
+
);
|
|
788
|
+
return /* @__PURE__ */ jsx4(
|
|
789
|
+
"div",
|
|
790
|
+
{
|
|
791
|
+
className,
|
|
792
|
+
"data-roottale-exposure-slot": slotKey,
|
|
793
|
+
"data-roottale-exposure-placement": placement,
|
|
794
|
+
style: placement === "bottom" || runtime.device === "mobile" && placement === "top" ? { height } : { minHeight: height },
|
|
795
|
+
children: /* @__PURE__ */ jsx4(
|
|
796
|
+
"div",
|
|
797
|
+
{
|
|
798
|
+
ref: measuredRef,
|
|
799
|
+
className: `rt-exposure-position rt-exposure-position--${placement}`,
|
|
800
|
+
"data-device": runtime.device,
|
|
801
|
+
"data-scroll-hidden": hiddenOnScroll ? "true" : "false",
|
|
802
|
+
"aria-hidden": hiddenOnScroll || void 0,
|
|
803
|
+
inert: hiddenOnScroll || void 0,
|
|
804
|
+
children: /* @__PURE__ */ jsx4("aside", { className: classes, "aria-label": "\uD648\uD398\uC774\uC9C0 \uC548\uB0B4", children: body })
|
|
805
|
+
}
|
|
806
|
+
)
|
|
807
|
+
}
|
|
808
|
+
);
|
|
809
|
+
}
|
|
810
|
+
function RootTaleExposureCarouselPreview({ campaigns, placement = "popup", device = "desktop", slideDurationMs = 5e3 }) {
|
|
811
|
+
const carousel = useExposureCarousel(campaigns, true, slideDurationMs, true);
|
|
812
|
+
const campaign = carousel.campaign;
|
|
813
|
+
const slideRef = useSlideScrollReset(campaign?.id);
|
|
814
|
+
if (!campaign) return null;
|
|
815
|
+
return /* @__PURE__ */ jsx4("div", { className: `rt-exposure rt-exposure--${campaign.kind} rt-exposure--preview`, "data-placement": placement, "data-device": device, "data-size": exposureGroupSize(campaigns), children: /* @__PURE__ */ jsxs3("div", { className: "rt-exposure-carousel", ...carousel.interactions, children: [
|
|
816
|
+
/* @__PURE__ */ jsx4("div", { ref: slideRef, className: "rt-exposure-slide", children: /* @__PURE__ */ jsx4(ExposureContent, { campaign, device }, campaign.id) }),
|
|
817
|
+
/* @__PURE__ */ jsx4(ExposureCarouselControls, { carousel, campaigns })
|
|
818
|
+
] }) });
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
// src/exposures.tsx
|
|
822
|
+
import { jsx as jsx5 } from "react/jsx-runtime";
|
|
823
|
+
var Context = createContext(null);
|
|
824
|
+
var emptyPrefixes = [];
|
|
825
|
+
function RootTaleExposureProvider({
|
|
826
|
+
endpoint,
|
|
827
|
+
pathname,
|
|
828
|
+
slots,
|
|
829
|
+
breakpoint = 768,
|
|
830
|
+
homePaths,
|
|
831
|
+
slideDurationMs = 5e3,
|
|
832
|
+
hideMobileBannersOnScroll = true,
|
|
833
|
+
legacyDismissalKeyPrefixes = emptyPrefixes,
|
|
834
|
+
children
|
|
835
|
+
}) {
|
|
836
|
+
const [themeElement, setThemeElement] = useState6(null);
|
|
837
|
+
const [device, setDevice] = useState6("desktop");
|
|
838
|
+
const [heights, setHeights] = useState6({});
|
|
839
|
+
useEffect6(() => {
|
|
840
|
+
const media = window.matchMedia(`(max-width: ${breakpoint - 1}px)`);
|
|
841
|
+
const update = () => setDevice(media.matches ? "mobile" : "desktop");
|
|
842
|
+
update();
|
|
843
|
+
media.addEventListener("change", update);
|
|
844
|
+
return () => media.removeEventListener("change", update);
|
|
845
|
+
}, [breakpoint]);
|
|
846
|
+
const path = pathname.split(/[?#]/, 1)[0] || "/";
|
|
847
|
+
const home = isExposureHome(path, exposureHomePaths(homePaths));
|
|
848
|
+
const routeKey = `${path}:${device}`;
|
|
849
|
+
const snapshot = useExposureDecision(endpoint, path, device, slots, home);
|
|
850
|
+
const popupVisit = useRef4({ key: routeKey });
|
|
851
|
+
useEffect6(() => {
|
|
852
|
+
popupVisit.current = { key: routeKey };
|
|
853
|
+
}, [routeKey]);
|
|
854
|
+
const now = useCallback2(() => snapshot ? exposureServerNow(snapshot) : Date.now(), [snapshot]);
|
|
855
|
+
const claimPopup = useCallback2((slot) => {
|
|
856
|
+
const claimed = popupVisit.current.slot;
|
|
857
|
+
if (claimed && claimed !== slot) return false;
|
|
858
|
+
popupVisit.current.slot = slot;
|
|
859
|
+
return true;
|
|
860
|
+
}, []);
|
|
861
|
+
const setHeight = useCallback2((key, placement, height) => {
|
|
862
|
+
setHeights((previous) => previous[key]?.height === height && previous[key]?.placement === placement ? previous : { ...previous, [key]: { placement, height } });
|
|
863
|
+
}, []);
|
|
864
|
+
const value = {
|
|
865
|
+
campaigns: liveCampaigns(snapshot),
|
|
866
|
+
device,
|
|
867
|
+
routeKey,
|
|
868
|
+
home,
|
|
869
|
+
slideDurationMs: Number.isFinite(slideDurationMs) ? slideDurationMs : 5e3,
|
|
870
|
+
hideMobileBannersOnScroll,
|
|
871
|
+
now,
|
|
872
|
+
claimPopup,
|
|
873
|
+
setHeight,
|
|
874
|
+
legacyPrefixes: legacyDismissalKeyPrefixes,
|
|
875
|
+
themeElement
|
|
876
|
+
};
|
|
877
|
+
const top = Math.max(0, ...Object.values(heights).filter((item) => item.placement === "top").map((item) => item.height));
|
|
878
|
+
const bottom = Math.max(0, ...Object.values(heights).filter((item) => item.placement === "bottom").map((item) => item.height));
|
|
879
|
+
const style = { display: "contents", "--rt-exposure-top-height": `${top}px`, "--rt-exposure-bottom-height": `${bottom}px` };
|
|
880
|
+
return /* @__PURE__ */ jsx5(Context.Provider, { value, children: /* @__PURE__ */ jsx5("div", { ref: setThemeElement, "data-roottale-exposure-root": "", style, children }) });
|
|
881
|
+
}
|
|
882
|
+
function RootTaleExposureSlot({ slotKey, placement, allowedVariants, fallback = null, className = "" }) {
|
|
883
|
+
const runtime = useContext(Context);
|
|
884
|
+
if (runtime && !runtime.home) return fallback;
|
|
885
|
+
const campaigns = runtime?.campaigns.filter((item) => item.slotKey === slotKey && item.kind === (placement === "popup" ? "popup" : "banner") && allowedVariants.includes(item.content.variant)).slice(0, 20);
|
|
886
|
+
if (!runtime || !campaigns?.length) return fallback;
|
|
887
|
+
return /* @__PURE__ */ jsx5(ExposureCampaignGroup, { campaigns, runtime, placement, className }, `${runtime.routeKey}:${slotKey}`);
|
|
888
|
+
}
|
|
889
|
+
function RootTaleLegacyExposure({ campaign, allowedVariants }) {
|
|
890
|
+
const [device, setDevice] = useState6("desktop");
|
|
891
|
+
useEffect6(() => {
|
|
892
|
+
const media = window.matchMedia("(max-width: 767px)");
|
|
893
|
+
const update = () => setDevice(media.matches ? "mobile" : "desktop");
|
|
894
|
+
update();
|
|
895
|
+
media.addEventListener("change", update);
|
|
896
|
+
return () => media.removeEventListener("change", update);
|
|
897
|
+
}, []);
|
|
898
|
+
if (!allowedVariants.includes(campaign.content.variant)) return null;
|
|
899
|
+
const runtime = {
|
|
900
|
+
campaigns: [campaign],
|
|
901
|
+
device,
|
|
902
|
+
routeKey: "legacy",
|
|
903
|
+
home: true,
|
|
904
|
+
slideDurationMs: 5e3,
|
|
905
|
+
hideMobileBannersOnScroll: true,
|
|
906
|
+
now: Date.now,
|
|
907
|
+
claimPopup: () => true,
|
|
908
|
+
setHeight: () => void 0,
|
|
909
|
+
legacyPrefixes: emptyPrefixes,
|
|
910
|
+
themeElement: null
|
|
911
|
+
};
|
|
912
|
+
return /* @__PURE__ */ jsx5(ExposureCampaignGroup, { campaigns: [campaign], runtime, placement: campaign.kind === "popup" ? "popup" : "inline", className: "" }, campaign.id);
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
export {
|
|
916
|
+
RootTaleExposurePreview,
|
|
917
|
+
RootTaleExposureCarouselPreview,
|
|
918
|
+
RootTaleExposureProvider,
|
|
919
|
+
RootTaleExposureSlot,
|
|
920
|
+
RootTaleLegacyExposure
|
|
921
|
+
};
|
|
922
|
+
//# sourceMappingURL=chunk-SEPUIKVM.js.map
|