non-spooky-react-cookie 0.1.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 +1 -0
- package/LICENSE +21 -0
- package/README.md +577 -0
- package/dist/index.cjs +856 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +137 -0
- package/dist/index.d.ts +137 -0
- package/dist/index.js +837 -0
- package/dist/index.js.map +1 -0
- package/dist/resolve-texts-yAIbWOUX.d.cts +456 -0
- package/dist/resolve-texts-yAIbWOUX.d.ts +456 -0
- package/dist/server.cjs +26 -0
- package/dist/server.cjs.map +1 -0
- package/dist/server.d.cts +28 -0
- package/dist/server.d.ts +28 -0
- package/dist/server.js +23 -0
- package/dist/server.js.map +1 -0
- package/dist/storage-Cqed-yMR.cjs +373 -0
- package/dist/storage-Cqed-yMR.cjs.map +1 -0
- package/dist/storage-WH-kuxkg.js +296 -0
- package/dist/storage-WH-kuxkg.js.map +1 -0
- package/dist/styles.css +624 -0
- package/package.json +119 -0
- package/server/README.md +25 -0
- package/server/package.json +5 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,856 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
+
const require_storage = require("./storage-Cqed-yMR.cjs");
|
|
4
|
+
let react = require("react");
|
|
5
|
+
let react_jsx_runtime = require("react/jsx-runtime");
|
|
6
|
+
//#region src/integrations/global-privacy-control.ts
|
|
7
|
+
/**
|
|
8
|
+
* Global Privacy Control (https://w3c.github.io/gpc/) is a browser-level
|
|
9
|
+
* "do not sell or share my data" signal. When the visitor turns it on, the
|
|
10
|
+
* browser sends `Sec-GPC: 1` with every request and exposes
|
|
11
|
+
* `navigator.globalPrivacyControl === true` to scripts; when it is off the
|
|
12
|
+
* header is absent and the property is `false` (or missing entirely in
|
|
13
|
+
* browsers without native support, where extensions may define it).
|
|
14
|
+
*
|
|
15
|
+
* Returns `true` only for an active signal. Safe to call on the server.
|
|
16
|
+
*/
|
|
17
|
+
function readGlobalPrivacyControl() {
|
|
18
|
+
if (typeof navigator === "undefined") return false;
|
|
19
|
+
return navigator.globalPrivacyControl === true;
|
|
20
|
+
}
|
|
21
|
+
//#endregion
|
|
22
|
+
//#region src/integrations/google-tracker.ts
|
|
23
|
+
function ensureGtag() {
|
|
24
|
+
if (typeof window === "undefined") return null;
|
|
25
|
+
window.dataLayer = window.dataLayer ?? [];
|
|
26
|
+
window.gtag = window.gtag ?? function gtag() {
|
|
27
|
+
window.dataLayer?.push(arguments);
|
|
28
|
+
};
|
|
29
|
+
return window.gtag;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Initializes Google's consent mode with everything denied.
|
|
33
|
+
* Call this before any Google tag loads.
|
|
34
|
+
*/
|
|
35
|
+
function initGoogleTracker() {
|
|
36
|
+
const gtag = ensureGtag();
|
|
37
|
+
if (!gtag) return;
|
|
38
|
+
gtag("consent", "default", {
|
|
39
|
+
ad_storage: "denied",
|
|
40
|
+
ad_user_data: "denied",
|
|
41
|
+
ad_personalization: "denied",
|
|
42
|
+
analytics_storage: "denied"
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Updates Google's consent mode based on the stored preferences.
|
|
47
|
+
*
|
|
48
|
+
* Without `categories`, the category ids `analytics` / `marketing` in the
|
|
49
|
+
* accepted map drive the signals. With `categories`, a category grants its
|
|
50
|
+
* signals when the category OR any of its fine-grained items is accepted,
|
|
51
|
+
* so item-level consent (e.g. "only Google Ads") is respected.
|
|
52
|
+
*/
|
|
53
|
+
function updateGoogleTracker(state, categories) {
|
|
54
|
+
const gtag = ensureGtag();
|
|
55
|
+
if (!gtag) return;
|
|
56
|
+
const toValue = (id) => {
|
|
57
|
+
const category = categories?.find((candidate) => candidate.id === id);
|
|
58
|
+
return Boolean(state.accepted[id]) || Boolean(category?.items?.some((item) => state.accepted[item.id])) ? "granted" : "denied";
|
|
59
|
+
};
|
|
60
|
+
const analytics = toValue("analytics");
|
|
61
|
+
const marketing = toValue("marketing");
|
|
62
|
+
gtag("consent", "update", {
|
|
63
|
+
analytics_storage: analytics,
|
|
64
|
+
ad_storage: marketing,
|
|
65
|
+
ad_user_data: marketing,
|
|
66
|
+
ad_personalization: marketing
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
//#endregion
|
|
70
|
+
//#region src/integrations/script-runtime.ts
|
|
71
|
+
const runtimes = /* @__PURE__ */ new Map();
|
|
72
|
+
function getEntry(id) {
|
|
73
|
+
let entry = runtimes.get(id);
|
|
74
|
+
if (!entry) {
|
|
75
|
+
entry = {
|
|
76
|
+
status: "blocked",
|
|
77
|
+
listeners: /* @__PURE__ */ new Set()
|
|
78
|
+
};
|
|
79
|
+
runtimes.set(id, entry);
|
|
80
|
+
}
|
|
81
|
+
return entry;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Subscribe a listener to status changes for `id`.
|
|
85
|
+
* Returns an unsubscribe function. Safe to call on the server
|
|
86
|
+
* (returns a no-op) so `useSyncExternalStore` never crashes during SSR.
|
|
87
|
+
*/
|
|
88
|
+
function subscribeScript(id, notify) {
|
|
89
|
+
if (typeof window === "undefined") return () => {};
|
|
90
|
+
getEntry(id).listeners.add(notify);
|
|
91
|
+
return () => {
|
|
92
|
+
getEntry(id).listeners.delete(notify);
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
/** Primitive snapshot for `useSyncExternalStore` (must be stable). */
|
|
96
|
+
function getScriptStatus(id) {
|
|
97
|
+
return getEntry(id).status;
|
|
98
|
+
}
|
|
99
|
+
/** The last error for `id`, if any. */
|
|
100
|
+
function getScriptError(id) {
|
|
101
|
+
return getEntry(id).error;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Sets the status (and error, if any) for `id` and notifies subscribers.
|
|
105
|
+
* The error is always replaced, so a later `loaded` clears an old failure.
|
|
106
|
+
*/
|
|
107
|
+
function setScriptStatus(id, status, error) {
|
|
108
|
+
const entry = getEntry(id);
|
|
109
|
+
entry.status = status;
|
|
110
|
+
entry.error = error;
|
|
111
|
+
entry.listeners.forEach((listener) => {
|
|
112
|
+
try {
|
|
113
|
+
listener();
|
|
114
|
+
} catch {}
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
//#endregion
|
|
118
|
+
//#region src/integrations/script-loader.ts
|
|
119
|
+
/** Runs a consumer callback; its errors must never break consent enforcement. */
|
|
120
|
+
function safeCall(fn) {
|
|
121
|
+
try {
|
|
122
|
+
fn?.();
|
|
123
|
+
} catch {}
|
|
124
|
+
}
|
|
125
|
+
function findScriptElement(id) {
|
|
126
|
+
const element = document.getElementById(id);
|
|
127
|
+
return element instanceof HTMLScriptElement ? element : null;
|
|
128
|
+
}
|
|
129
|
+
function loadScript({ id, src, attrs, children, async: asyncFlag, defer, onLoad, onError }) {
|
|
130
|
+
if (typeof document === "undefined") return null;
|
|
131
|
+
const existing = findScriptElement(id);
|
|
132
|
+
if (existing) return existing;
|
|
133
|
+
const script = document.createElement("script");
|
|
134
|
+
script.id = id;
|
|
135
|
+
if (defer && !asyncFlag) script.defer = true;
|
|
136
|
+
else script.async = true;
|
|
137
|
+
if (src) script.src = src;
|
|
138
|
+
if (children) script.text = children;
|
|
139
|
+
if (onLoad) script.addEventListener("load", onLoad);
|
|
140
|
+
if (onError) script.addEventListener("error", onError);
|
|
141
|
+
Object.entries(attrs ?? {}).forEach(([key, value]) => {
|
|
142
|
+
script.setAttribute(key, value);
|
|
143
|
+
});
|
|
144
|
+
document.head.appendChild(script);
|
|
145
|
+
return script;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Removes the script element with `id` and runs the optional
|
|
149
|
+
* `cleanup` function.
|
|
150
|
+
*
|
|
151
|
+
* Caveat: this does NOT undo cookies or network requests the script
|
|
152
|
+
* already made — use `cleanup` for integration-specific teardown.
|
|
153
|
+
*/
|
|
154
|
+
function unloadScript(id, cleanup) {
|
|
155
|
+
if (typeof document === "undefined") return;
|
|
156
|
+
findScriptElement(id)?.remove();
|
|
157
|
+
safeCall(cleanup);
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Ensures the script under `id` is loaded, driving the runtime store.
|
|
161
|
+
*
|
|
162
|
+
* - Already in the DOM → status `loaded` (dedup, `onLoad` not re-fired).
|
|
163
|
+
* - Not in the DOM → status `loading`, then `loaded`/`error` when the
|
|
164
|
+
* element settles. `def.onLoad` / `def.onError` run (wrapped so a throw
|
|
165
|
+
* never breaks consent enforcement).
|
|
166
|
+
*/
|
|
167
|
+
function ensureScript(id, def) {
|
|
168
|
+
if (typeof document === "undefined") return;
|
|
169
|
+
if (findScriptElement(id)) {
|
|
170
|
+
setScriptStatus(id, "loaded");
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
const handleLoad = () => {
|
|
174
|
+
setScriptStatus(id, "loaded");
|
|
175
|
+
safeCall(def.onLoad);
|
|
176
|
+
};
|
|
177
|
+
setScriptStatus(id, "loading");
|
|
178
|
+
if (loadScript({
|
|
179
|
+
id,
|
|
180
|
+
src: def.src,
|
|
181
|
+
attrs: def.attrs,
|
|
182
|
+
children: def.children,
|
|
183
|
+
async: def.async,
|
|
184
|
+
defer: def.defer,
|
|
185
|
+
onLoad: handleLoad,
|
|
186
|
+
onError: () => {
|
|
187
|
+
setScriptStatus(id, "error", /* @__PURE__ */ new Error(`Script "${id}" failed to load.`));
|
|
188
|
+
safeCall(def.onError);
|
|
189
|
+
}
|
|
190
|
+
}) && !def.src) handleLoad();
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Removes the script under `id` from the DOM (running `cleanup`) and
|
|
194
|
+
* resets its status back to `blocked`. A script that is already blocked
|
|
195
|
+
* and not in the DOM is left alone, so `cleanup` runs only after a real load.
|
|
196
|
+
*/
|
|
197
|
+
function removeScript(id, cleanup) {
|
|
198
|
+
if (typeof document === "undefined") return;
|
|
199
|
+
if (getScriptStatus(id) === "blocked" && !findScriptElement(id)) return;
|
|
200
|
+
unloadScript(id, cleanup);
|
|
201
|
+
setScriptStatus(id, "blocked");
|
|
202
|
+
}
|
|
203
|
+
//#endregion
|
|
204
|
+
//#region src/CookieBannerConfigurationProvider.tsx
|
|
205
|
+
const DEFAULT_VERSION = "1";
|
|
206
|
+
const defaultCategories = [
|
|
207
|
+
{
|
|
208
|
+
id: "necessary",
|
|
209
|
+
required: true
|
|
210
|
+
},
|
|
211
|
+
{ id: "preferences" },
|
|
212
|
+
{ id: "analytics" },
|
|
213
|
+
{ id: "marketing" }
|
|
214
|
+
];
|
|
215
|
+
/** Maps the object-map `config` prop to the internal category array. */
|
|
216
|
+
function configToCategories(config) {
|
|
217
|
+
return Object.entries(config.categories).map(([id, category]) => ({
|
|
218
|
+
id,
|
|
219
|
+
title: category.name,
|
|
220
|
+
description: category.description,
|
|
221
|
+
required: category.required,
|
|
222
|
+
items: category.items ? Object.entries(category.items).map(([itemId, item]) => ({
|
|
223
|
+
id: itemId,
|
|
224
|
+
title: item.name,
|
|
225
|
+
description: item.description
|
|
226
|
+
})) : void 0
|
|
227
|
+
}));
|
|
228
|
+
}
|
|
229
|
+
const CookieBannerContext = (0, react.createContext)(null);
|
|
230
|
+
/**
|
|
231
|
+
* Builds a complete state. Required categories are always on; optional
|
|
232
|
+
* categories and every item follow `acceptOptional`.
|
|
233
|
+
*/
|
|
234
|
+
function buildState(version, categories, acceptOptional) {
|
|
235
|
+
const accepted = {};
|
|
236
|
+
categories.forEach((category) => {
|
|
237
|
+
accepted[category.id] = Boolean(category.required) || acceptOptional;
|
|
238
|
+
category.items?.forEach((item) => {
|
|
239
|
+
accepted[item.id] = acceptOptional;
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
return {
|
|
243
|
+
version,
|
|
244
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
245
|
+
accepted
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
const themeVariables = {
|
|
249
|
+
primaryColor: "--nsr-primary",
|
|
250
|
+
primaryTextColor: "--nsr-primary-text",
|
|
251
|
+
primaryHoverColor: "--nsr-primary-hover",
|
|
252
|
+
secondaryColor: "--nsr-secondary",
|
|
253
|
+
secondaryTextColor: "--nsr-secondary-text",
|
|
254
|
+
accentColor: "--nsr-accent",
|
|
255
|
+
surfaceColor: "--nsr-surface",
|
|
256
|
+
surfaceMutedColor: "--nsr-surface-muted",
|
|
257
|
+
textColor: "--nsr-text",
|
|
258
|
+
mutedTextColor: "--nsr-muted",
|
|
259
|
+
borderColor: "--nsr-border",
|
|
260
|
+
ringColor: "--nsr-ring",
|
|
261
|
+
switchOffColor: "--nsr-switch-off",
|
|
262
|
+
switchThumbColor: "--nsr-switch-thumb",
|
|
263
|
+
backdropColor: "--nsr-backdrop"
|
|
264
|
+
};
|
|
265
|
+
/** The attribute that scopes a provider's theme rules to its elements. */
|
|
266
|
+
const THEME_ATTRIBUTE = "data-nsr-theme";
|
|
267
|
+
/**
|
|
268
|
+
* Keeps a palette value safe to embed in a stylesheet: a value is a single
|
|
269
|
+
* CSS color, so it never needs a declaration or block terminator.
|
|
270
|
+
*/
|
|
271
|
+
function sanitizeCssValue(value) {
|
|
272
|
+
return value.replace(/[;{}<>]/g, "").trim();
|
|
273
|
+
}
|
|
274
|
+
/** `[--nsr-x, value]` pairs for the set entries of a palette. */
|
|
275
|
+
function themeEntries(theme) {
|
|
276
|
+
return Object.entries(themeVariables).flatMap(([key, variable]) => {
|
|
277
|
+
const value = theme[key];
|
|
278
|
+
return value ? [[variable, sanitizeCssValue(value)]] : [];
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
/** Resolves the theme palette into CSS custom properties (set values only). */
|
|
282
|
+
function themeToStyle(theme) {
|
|
283
|
+
return Object.fromEntries(themeEntries(theme));
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Builds the scoped stylesheet for one provider. `theme` applies to every
|
|
287
|
+
* element carrying the provider's theme attribute; `darkTheme` applies to the
|
|
288
|
+
* same elements under a `.dark` / `[data-theme="dark"]` ancestor. Returns an
|
|
289
|
+
* empty string when neither palette sets anything, so nothing is rendered.
|
|
290
|
+
*/
|
|
291
|
+
function buildThemeCss(id, theme, darkTheme) {
|
|
292
|
+
const scope = `[${THEME_ATTRIBUTE}="${id.replace(/["\\]/g, "")}"]`;
|
|
293
|
+
const block = (entries) => entries.map(([variable, value]) => ` ${variable}: ${value};`).join("\n");
|
|
294
|
+
const light = themeEntries(theme);
|
|
295
|
+
const dark = themeEntries(darkTheme);
|
|
296
|
+
const rules = [];
|
|
297
|
+
if (light.length > 0) rules.push(`${scope} {\n${block(light)}\n}`);
|
|
298
|
+
if (dark.length > 0) rules.push(`:is(.dark, [data-theme="dark"]) ${scope} {\n${block(dark)}\n}`);
|
|
299
|
+
return rules.join("\n");
|
|
300
|
+
}
|
|
301
|
+
function CookieBannerConfigurationProvider({ children, config, scripts, language = "en", texts: textOverrides, theme = {}, darkTheme = {}, components = {}, storageKey = require_storage.DEFAULT_STORAGE_KEY, storage = "localStorage", cookieOptions, initialPreferences, version = DEFAULT_VERSION, googleConsentMode = false, windowJustDont = true, respectGlobalPrivacyControl = true, onDecision }) {
|
|
302
|
+
const cookieOptionsKey = JSON.stringify(cookieOptions ?? null);
|
|
303
|
+
const store = (0, react.useMemo)(() => require_storage.resolveStorage(storage, JSON.parse(cookieOptionsKey) ?? void 0), [storage, cookieOptionsKey]);
|
|
304
|
+
const categories = (0, react.useMemo)(() => config ? configToCategories(config) : defaultCategories, [config]);
|
|
305
|
+
const texts = (0, react.useMemo)(() => require_storage.resolveTexts(language, textOverrides), [language, textOverrides]);
|
|
306
|
+
const themeId = (0, react.useId)();
|
|
307
|
+
const themeKey = JSON.stringify(theme);
|
|
308
|
+
const darkThemeKey = JSON.stringify(darkTheme);
|
|
309
|
+
const themeStyle = (0, react.useMemo)(() => themeToStyle(JSON.parse(themeKey)), [themeKey]);
|
|
310
|
+
const themeCss = (0, react.useMemo)(() => buildThemeCss(themeId, JSON.parse(themeKey), JSON.parse(darkThemeKey)), [
|
|
311
|
+
darkThemeKey,
|
|
312
|
+
themeId,
|
|
313
|
+
themeKey
|
|
314
|
+
]);
|
|
315
|
+
const themeAttributes = (0, react.useMemo)(() => ({ [THEME_ATTRIBUTE]: themeId }), [themeId]);
|
|
316
|
+
const itemToCategory = (0, react.useMemo)(() => {
|
|
317
|
+
const owners = {};
|
|
318
|
+
categories.forEach((category) => {
|
|
319
|
+
category.items?.forEach((item) => {
|
|
320
|
+
owners[item.id] = category.id;
|
|
321
|
+
});
|
|
322
|
+
});
|
|
323
|
+
return owners;
|
|
324
|
+
}, [categories]);
|
|
325
|
+
const restoredInitial = initialPreferences?.version === version ? initialPreferences : null;
|
|
326
|
+
const [loaded, setLoaded] = (0, react.useState)(initialPreferences !== void 0);
|
|
327
|
+
const [hasDecision, setHasDecision] = (0, react.useState)(restoredInitial !== null);
|
|
328
|
+
const [globalPrivacyControl, setGlobalPrivacyControl] = (0, react.useState)(false);
|
|
329
|
+
const [settingsOpen, setSettingsOpen] = (0, react.useState)(false);
|
|
330
|
+
const [state, setState] = (0, react.useState)(() => restoredInitial ?? buildState(version, categories, false));
|
|
331
|
+
const syncGoogle = (0, react.useCallback)((next) => {
|
|
332
|
+
if (googleConsentMode) updateGoogleTracker(next, categories);
|
|
333
|
+
}, [categories, googleConsentMode]);
|
|
334
|
+
const onDecisionRef = (0, react.useRef)(onDecision);
|
|
335
|
+
(0, react.useEffect)(() => {
|
|
336
|
+
onDecisionRef.current = onDecision;
|
|
337
|
+
}, [onDecision]);
|
|
338
|
+
(0, react.useEffect)(() => {
|
|
339
|
+
if (googleConsentMode) initGoogleTracker();
|
|
340
|
+
const stored = require_storage.readPreferences(store, storageKey);
|
|
341
|
+
const restored = stored?.version === version ? stored : null;
|
|
342
|
+
const gpc = respectGlobalPrivacyControl && readGlobalPrivacyControl();
|
|
343
|
+
const next = restored ?? buildState(version, categories, false);
|
|
344
|
+
const decidedByGpc = restored === null && gpc;
|
|
345
|
+
setState(next);
|
|
346
|
+
setHasDecision(restored !== null || decidedByGpc);
|
|
347
|
+
setGlobalPrivacyControl(gpc);
|
|
348
|
+
syncGoogle(next);
|
|
349
|
+
setLoaded(true);
|
|
350
|
+
if (decidedByGpc) onDecisionRef.current?.(next);
|
|
351
|
+
}, [
|
|
352
|
+
categories,
|
|
353
|
+
googleConsentMode,
|
|
354
|
+
respectGlobalPrivacyControl,
|
|
355
|
+
storageKey,
|
|
356
|
+
store,
|
|
357
|
+
syncGoogle,
|
|
358
|
+
version
|
|
359
|
+
]);
|
|
360
|
+
(0, react.useEffect)(() => {
|
|
361
|
+
const entries = Object.entries(scripts ?? {});
|
|
362
|
+
return () => {
|
|
363
|
+
for (const [id, def] of entries) removeScript(id, def.cleanup);
|
|
364
|
+
};
|
|
365
|
+
}, [scripts]);
|
|
366
|
+
(0, react.useEffect)(() => {
|
|
367
|
+
if (!loaded) return;
|
|
368
|
+
Object.entries(scripts ?? {}).forEach(([id, def]) => {
|
|
369
|
+
if (state.accepted[def.category]) ensureScript(id, def);
|
|
370
|
+
else removeScript(id, def.cleanup);
|
|
371
|
+
});
|
|
372
|
+
}, [
|
|
373
|
+
loaded,
|
|
374
|
+
scripts,
|
|
375
|
+
state.accepted
|
|
376
|
+
]);
|
|
377
|
+
const persist = (0, react.useCallback)((next) => {
|
|
378
|
+
setState(next);
|
|
379
|
+
setHasDecision(true);
|
|
380
|
+
setSettingsOpen(false);
|
|
381
|
+
require_storage.writePreferences(store, storageKey, next);
|
|
382
|
+
syncGoogle(next);
|
|
383
|
+
onDecision?.(next);
|
|
384
|
+
}, [
|
|
385
|
+
onDecision,
|
|
386
|
+
storageKey,
|
|
387
|
+
store,
|
|
388
|
+
syncGoogle
|
|
389
|
+
]);
|
|
390
|
+
const acceptAll = (0, react.useCallback)(() => persist(buildState(version, categories, true)), [
|
|
391
|
+
categories,
|
|
392
|
+
persist,
|
|
393
|
+
version
|
|
394
|
+
]);
|
|
395
|
+
const rejectAll = (0, react.useCallback)(() => persist(buildState(version, categories, false)), [
|
|
396
|
+
categories,
|
|
397
|
+
persist,
|
|
398
|
+
version
|
|
399
|
+
]);
|
|
400
|
+
const savePreferences = (0, react.useCallback)((partial) => persist({
|
|
401
|
+
version,
|
|
402
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
403
|
+
accepted: {
|
|
404
|
+
...state.accepted,
|
|
405
|
+
...partial.accepted
|
|
406
|
+
}
|
|
407
|
+
}), [
|
|
408
|
+
persist,
|
|
409
|
+
state.accepted,
|
|
410
|
+
version
|
|
411
|
+
]);
|
|
412
|
+
const resetPreferences = (0, react.useCallback)(() => {
|
|
413
|
+
const initial = buildState(version, categories, false);
|
|
414
|
+
require_storage.removePreferences(store, storageKey);
|
|
415
|
+
setState(initial);
|
|
416
|
+
setHasDecision(false);
|
|
417
|
+
setSettingsOpen(false);
|
|
418
|
+
syncGoogle(initial);
|
|
419
|
+
onDecision?.(initial);
|
|
420
|
+
}, [
|
|
421
|
+
categories,
|
|
422
|
+
onDecision,
|
|
423
|
+
storageKey,
|
|
424
|
+
store,
|
|
425
|
+
syncGoogle,
|
|
426
|
+
version
|
|
427
|
+
]);
|
|
428
|
+
const openSettings = (0, react.useCallback)(() => setSettingsOpen(true), []);
|
|
429
|
+
const closeSettings = (0, react.useCallback)(() => setSettingsOpen(false), []);
|
|
430
|
+
const isAllowed = (0, react.useCallback)((id) => Boolean(state.accepted[id]), [state.accepted]);
|
|
431
|
+
/**
|
|
432
|
+
* Resolves the display title/description for a category or item,
|
|
433
|
+
* preferring the config values, then the texts, then the id.
|
|
434
|
+
*/
|
|
435
|
+
const resolveLabel = (0, react.useCallback)((id, config) => {
|
|
436
|
+
const parent = itemToCategory[id];
|
|
437
|
+
const source = parent ? texts.categories[parent]?.items?.[id] : texts.categories[id];
|
|
438
|
+
return {
|
|
439
|
+
title: config?.title ?? source?.title ?? id,
|
|
440
|
+
description: config?.description ?? source?.description ?? ""
|
|
441
|
+
};
|
|
442
|
+
}, [itemToCategory, texts]);
|
|
443
|
+
const value = (0, react.useMemo)(() => ({
|
|
444
|
+
loaded,
|
|
445
|
+
hasDecision,
|
|
446
|
+
showBanner: loaded && !hasDecision,
|
|
447
|
+
globalPrivacyControl,
|
|
448
|
+
settingsOpen,
|
|
449
|
+
preferences: state,
|
|
450
|
+
texts,
|
|
451
|
+
categories,
|
|
452
|
+
scripts: scripts ?? {},
|
|
453
|
+
theme,
|
|
454
|
+
darkTheme,
|
|
455
|
+
components,
|
|
456
|
+
themeStyle,
|
|
457
|
+
themeAttributes,
|
|
458
|
+
acceptAll,
|
|
459
|
+
rejectAll,
|
|
460
|
+
savePreferences,
|
|
461
|
+
resetPreferences,
|
|
462
|
+
openSettings,
|
|
463
|
+
closeSettings,
|
|
464
|
+
isAllowed,
|
|
465
|
+
resolveLabel
|
|
466
|
+
}), [
|
|
467
|
+
acceptAll,
|
|
468
|
+
categories,
|
|
469
|
+
closeSettings,
|
|
470
|
+
components,
|
|
471
|
+
darkTheme,
|
|
472
|
+
globalPrivacyControl,
|
|
473
|
+
hasDecision,
|
|
474
|
+
isAllowed,
|
|
475
|
+
loaded,
|
|
476
|
+
openSettings,
|
|
477
|
+
rejectAll,
|
|
478
|
+
resetPreferences,
|
|
479
|
+
resolveLabel,
|
|
480
|
+
savePreferences,
|
|
481
|
+
scripts,
|
|
482
|
+
settingsOpen,
|
|
483
|
+
state,
|
|
484
|
+
texts,
|
|
485
|
+
theme,
|
|
486
|
+
themeAttributes,
|
|
487
|
+
themeStyle
|
|
488
|
+
]);
|
|
489
|
+
(0, react.useEffect)(() => {
|
|
490
|
+
if (!windowJustDont || typeof window === "undefined") return;
|
|
491
|
+
const w = window;
|
|
492
|
+
if (typeof w.justDont === "function") console.warn("[non-spooky-react-cookie] window.justDont already exists; the cookie banner will overwrite it.");
|
|
493
|
+
w.justDont = rejectAll;
|
|
494
|
+
return () => {
|
|
495
|
+
if (w.justDont === rejectAll) delete w.justDont;
|
|
496
|
+
};
|
|
497
|
+
}, [rejectAll, windowJustDont]);
|
|
498
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(CookieBannerContext.Provider, {
|
|
499
|
+
value,
|
|
500
|
+
children: [themeCss ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("style", {
|
|
501
|
+
"data-nsr-theme-style": themeId,
|
|
502
|
+
children: themeCss
|
|
503
|
+
}) : null, children]
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
//#endregion
|
|
507
|
+
//#region src/hooks/usePreferences.ts
|
|
508
|
+
/**
|
|
509
|
+
* Access the cookie banner state and actions from anywhere
|
|
510
|
+
* inside a CookieBannerConfigurationProvider.
|
|
511
|
+
*/
|
|
512
|
+
function usePreferences() {
|
|
513
|
+
const context = (0, react.useContext)(CookieBannerContext);
|
|
514
|
+
if (!context) throw new Error("usePreferences must be used within a CookieBannerConfigurationProvider");
|
|
515
|
+
return context;
|
|
516
|
+
}
|
|
517
|
+
//#endregion
|
|
518
|
+
//#region src/ui.tsx
|
|
519
|
+
function cn(...classes) {
|
|
520
|
+
return classes.filter(Boolean).join(" ");
|
|
521
|
+
}
|
|
522
|
+
/**
|
|
523
|
+
* Default Button.
|
|
524
|
+
* Colors come from the `--nsr-*` variables: defaults in `styles.css`,
|
|
525
|
+
* overrides from the provider's `theme` prop.
|
|
526
|
+
*/
|
|
527
|
+
function Button({ className, variant = "secondary", ...props }) {
|
|
528
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
529
|
+
className: cn("nsr-button", `nsr-button--${variant}`, className),
|
|
530
|
+
...props
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
/**
|
|
534
|
+
* Default Switch.
|
|
535
|
+
* The "on" color follows the theme's primary color.
|
|
536
|
+
*/
|
|
537
|
+
function Switch({ checked, disabled, onCheckedChange, "aria-label": ariaLabel }) {
|
|
538
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
539
|
+
type: "button",
|
|
540
|
+
role: "switch",
|
|
541
|
+
"aria-checked": checked,
|
|
542
|
+
"aria-label": ariaLabel,
|
|
543
|
+
disabled,
|
|
544
|
+
onClick: () => onCheckedChange(!checked),
|
|
545
|
+
className: "nsr-switch",
|
|
546
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "nsr-switch__thumb" })
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
/**
|
|
550
|
+
* Default Collapsible.
|
|
551
|
+
* A disclosure that hides its children until the trigger is pressed.
|
|
552
|
+
* Works controlled (`open` + `onOpenChange`) or self-managed (`defaultOpen`).
|
|
553
|
+
* The chevron follows the theme's primary color.
|
|
554
|
+
*/
|
|
555
|
+
function Collapsible({ children, count, label, open, onOpenChange, defaultOpen, className, contentClassName }) {
|
|
556
|
+
const regionId = (0, react.useId)();
|
|
557
|
+
const [internalOpen, setInternalOpen] = (0, react.useState)(defaultOpen ?? false);
|
|
558
|
+
const isOpen = open ?? internalOpen;
|
|
559
|
+
const toggle = () => {
|
|
560
|
+
const next = !isOpen;
|
|
561
|
+
if (open === void 0) setInternalOpen(next);
|
|
562
|
+
onOpenChange?.(next);
|
|
563
|
+
};
|
|
564
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
565
|
+
className: cn("nsr-collapsible", isOpen && "nsr-collapsible--open"),
|
|
566
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
567
|
+
type: "button",
|
|
568
|
+
"aria-controls": regionId,
|
|
569
|
+
"aria-expanded": isOpen,
|
|
570
|
+
className: cn("nsr-collapsible__trigger", className),
|
|
571
|
+
onClick: toggle,
|
|
572
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
573
|
+
className: "nsr-collapsible__label",
|
|
574
|
+
children: [
|
|
575
|
+
label,
|
|
576
|
+
" (",
|
|
577
|
+
count,
|
|
578
|
+
")"
|
|
579
|
+
]
|
|
580
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
|
|
581
|
+
className: "nsr-collapsible__chevron",
|
|
582
|
+
fill: "none",
|
|
583
|
+
stroke: "currentColor",
|
|
584
|
+
strokeWidth: 2,
|
|
585
|
+
strokeLinecap: "round",
|
|
586
|
+
strokeLinejoin: "round",
|
|
587
|
+
viewBox: "0 0 24 24",
|
|
588
|
+
"aria-hidden": "true",
|
|
589
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M6 9l6 6 6-6" })
|
|
590
|
+
})]
|
|
591
|
+
}), isOpen ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
592
|
+
className: cn("nsr-collapsible__content", contentClassName),
|
|
593
|
+
id: regionId,
|
|
594
|
+
children
|
|
595
|
+
}) : null]
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
//#endregion
|
|
599
|
+
//#region src/CookieSettingsDialog.tsx
|
|
600
|
+
/** Renders nothing while closed; the open dialog mounts fresh each time. */
|
|
601
|
+
function CookieSettingsDialog(props) {
|
|
602
|
+
const { settingsOpen } = usePreferences();
|
|
603
|
+
return settingsOpen ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(OpenSettingsDialog, { ...props }) : null;
|
|
604
|
+
}
|
|
605
|
+
function OpenSettingsDialog({ className, overlayClassName, contentClassName, headerClassName, bodyClassName, footerClassName, categoryCardClassName, itemClassName, buttonClassName, components: ownComponents }) {
|
|
606
|
+
const { categories, closeSettings, components: providerComponents, preferences, resolveLabel, savePreferences, texts, themeAttributes } = usePreferences();
|
|
607
|
+
const dialogRef = (0, react.useRef)(null);
|
|
608
|
+
const [draft, setDraft] = (0, react.useState)(preferences.accepted);
|
|
609
|
+
(0, react.useEffect)(() => {
|
|
610
|
+
const dialog = dialogRef.current;
|
|
611
|
+
if (dialog && !dialog.open && typeof dialog.showModal === "function") dialog.showModal();
|
|
612
|
+
}, []);
|
|
613
|
+
const components = {
|
|
614
|
+
...providerComponents,
|
|
615
|
+
...ownComponents
|
|
616
|
+
};
|
|
617
|
+
const ButtonComponent = components.Button ?? Button;
|
|
618
|
+
const SwitchComponent = components.Switch ?? Switch;
|
|
619
|
+
const CollapsibleComponent = components.Collapsible ?? Collapsible;
|
|
620
|
+
const setAccepted = (ids, value) => setDraft((current) => {
|
|
621
|
+
const next = { ...current };
|
|
622
|
+
for (const id of ids) next[id] = value;
|
|
623
|
+
return next;
|
|
624
|
+
});
|
|
625
|
+
const handleCancel = (event) => {
|
|
626
|
+
event.preventDefault();
|
|
627
|
+
closeSettings();
|
|
628
|
+
};
|
|
629
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("dialog", {
|
|
630
|
+
"aria-labelledby": "nsr-settings-title",
|
|
631
|
+
className: cn("nsr-settings-dialog", className),
|
|
632
|
+
onCancel: handleCancel,
|
|
633
|
+
ref: dialogRef,
|
|
634
|
+
...themeAttributes,
|
|
635
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
636
|
+
"aria-label": texts.dialog.close,
|
|
637
|
+
className: cn("nsr-dialog__overlay", overlayClassName),
|
|
638
|
+
onClick: closeSettings,
|
|
639
|
+
tabIndex: -1,
|
|
640
|
+
type: "button"
|
|
641
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
642
|
+
className: cn("nsr-dialog__panel", contentClassName),
|
|
643
|
+
children: [
|
|
644
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
645
|
+
className: cn("nsr-dialog__header", headerClassName),
|
|
646
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
|
|
647
|
+
id: "nsr-settings-title",
|
|
648
|
+
className: "nsr-dialog__title",
|
|
649
|
+
children: texts.dialog.title
|
|
650
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
651
|
+
className: "nsr-dialog__description",
|
|
652
|
+
children: texts.dialog.description
|
|
653
|
+
})]
|
|
654
|
+
}),
|
|
655
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
656
|
+
className: cn("nsr-dialog__body", bodyClassName),
|
|
657
|
+
children: categories.map((category) => {
|
|
658
|
+
const { title, description } = resolveLabel(category.id, category);
|
|
659
|
+
const required = Boolean(category.required);
|
|
660
|
+
const items = category.items ?? [];
|
|
661
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
662
|
+
className: cn("nsr-category", required && "nsr-category--required", categoryCardClassName),
|
|
663
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
664
|
+
className: "nsr-category__header",
|
|
665
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
|
|
666
|
+
className: "nsr-category__title",
|
|
667
|
+
children: title
|
|
668
|
+
}), description ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
669
|
+
className: "nsr-category__description",
|
|
670
|
+
children: description
|
|
671
|
+
}) : null] }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SwitchComponent, {
|
|
672
|
+
"aria-label": title,
|
|
673
|
+
checked: Boolean(draft[category.id]),
|
|
674
|
+
disabled: required,
|
|
675
|
+
onCheckedChange: (value) => setAccepted([category.id, ...items.map((item) => item.id)], value)
|
|
676
|
+
})]
|
|
677
|
+
}), items.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CollapsibleComponent, {
|
|
678
|
+
count: items.length,
|
|
679
|
+
label: texts.dialog.itemsLabel,
|
|
680
|
+
contentClassName: "nsr-category__items",
|
|
681
|
+
children: items.map((item) => {
|
|
682
|
+
const itemLabel = resolveLabel(item.id, item);
|
|
683
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
684
|
+
className: cn("nsr-item", itemClassName),
|
|
685
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h4", {
|
|
686
|
+
className: "nsr-item__title",
|
|
687
|
+
children: itemLabel.title
|
|
688
|
+
}), itemLabel.description ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
689
|
+
className: "nsr-item__description",
|
|
690
|
+
children: itemLabel.description
|
|
691
|
+
}) : null] }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SwitchComponent, {
|
|
692
|
+
"aria-label": itemLabel.title,
|
|
693
|
+
checked: Boolean(draft[item.id]),
|
|
694
|
+
disabled: required,
|
|
695
|
+
onCheckedChange: (value) => setAccepted([item.id], value)
|
|
696
|
+
})]
|
|
697
|
+
}, item.id);
|
|
698
|
+
})
|
|
699
|
+
}) : null]
|
|
700
|
+
}, category.id);
|
|
701
|
+
})
|
|
702
|
+
}),
|
|
703
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
704
|
+
className: cn("nsr-dialog__footer", footerClassName),
|
|
705
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ButtonComponent, {
|
|
706
|
+
className: cn("nsr-dialog__button", buttonClassName),
|
|
707
|
+
onClick: closeSettings,
|
|
708
|
+
type: "button",
|
|
709
|
+
variant: "ghost",
|
|
710
|
+
children: texts.dialog.close
|
|
711
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ButtonComponent, {
|
|
712
|
+
className: cn("nsr-dialog__button", buttonClassName),
|
|
713
|
+
onClick: () => savePreferences({ accepted: draft }),
|
|
714
|
+
type: "button",
|
|
715
|
+
variant: "primary",
|
|
716
|
+
children: texts.dialog.save
|
|
717
|
+
})]
|
|
718
|
+
})
|
|
719
|
+
]
|
|
720
|
+
})]
|
|
721
|
+
});
|
|
722
|
+
}
|
|
723
|
+
//#endregion
|
|
724
|
+
//#region src/CookieBanner.tsx
|
|
725
|
+
function CookieBanner({ policyUrl, className, contentClassName, titleClassName, descriptionClassName, actionsClassName, buttonClassName, components, dialogProps }) {
|
|
726
|
+
const { acceptAll, components: providerComponents, openSettings, rejectAll, showBanner, texts, themeAttributes } = usePreferences();
|
|
727
|
+
const ButtonComponent = components?.Button ?? providerComponents.Button ?? Button;
|
|
728
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [showBanner ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("section", {
|
|
729
|
+
"aria-label": texts.banner.title,
|
|
730
|
+
className: cn("nsr-banner", className),
|
|
731
|
+
...themeAttributes,
|
|
732
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
733
|
+
className: cn("nsr-banner__card", contentClassName),
|
|
734
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
735
|
+
className: "nsr-banner__text",
|
|
736
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
|
|
737
|
+
className: cn("nsr-banner__title", titleClassName),
|
|
738
|
+
children: texts.banner.title
|
|
739
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
|
|
740
|
+
className: cn("nsr-banner__description", descriptionClassName),
|
|
741
|
+
children: [texts.banner.description, policyUrl ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [" ", /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
|
|
742
|
+
className: "nsr-banner__link",
|
|
743
|
+
href: policyUrl,
|
|
744
|
+
children: texts.banner.policyLink
|
|
745
|
+
})] }) : null]
|
|
746
|
+
})]
|
|
747
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
748
|
+
className: cn("nsr-banner__actions", actionsClassName),
|
|
749
|
+
children: [
|
|
750
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ButtonComponent, {
|
|
751
|
+
className: buttonClassName,
|
|
752
|
+
onClick: rejectAll,
|
|
753
|
+
type: "button",
|
|
754
|
+
variant: "secondary",
|
|
755
|
+
children: texts.banner.rejectAll
|
|
756
|
+
}),
|
|
757
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ButtonComponent, {
|
|
758
|
+
className: buttonClassName,
|
|
759
|
+
onClick: openSettings,
|
|
760
|
+
type: "button",
|
|
761
|
+
variant: "secondary",
|
|
762
|
+
children: texts.banner.settings
|
|
763
|
+
}),
|
|
764
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ButtonComponent, {
|
|
765
|
+
className: buttonClassName,
|
|
766
|
+
onClick: acceptAll,
|
|
767
|
+
type: "button",
|
|
768
|
+
variant: "primary",
|
|
769
|
+
children: texts.banner.acceptAll
|
|
770
|
+
})
|
|
771
|
+
]
|
|
772
|
+
})]
|
|
773
|
+
})
|
|
774
|
+
}) : null, /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CookieSettingsDialog, {
|
|
775
|
+
...dialogProps,
|
|
776
|
+
components: {
|
|
777
|
+
...components,
|
|
778
|
+
...dialogProps?.components
|
|
779
|
+
}
|
|
780
|
+
})] });
|
|
781
|
+
}
|
|
782
|
+
//#endregion
|
|
783
|
+
//#region src/CookieSettingsLink.tsx
|
|
784
|
+
/**
|
|
785
|
+
* A small link (e.g. in a footer) that opens the cookie settings dialog.
|
|
786
|
+
* Renders your children, or the built-in "Cookie settings" text.
|
|
787
|
+
*/
|
|
788
|
+
function CookieSettingsLink({ children, className, type = "button", onClick, ...props }) {
|
|
789
|
+
const { openSettings, texts, themeAttributes } = usePreferences();
|
|
790
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
791
|
+
className: cn("nsr-settings-link", className),
|
|
792
|
+
...themeAttributes,
|
|
793
|
+
onClick: (event) => {
|
|
794
|
+
openSettings();
|
|
795
|
+
onClick?.(event);
|
|
796
|
+
},
|
|
797
|
+
type,
|
|
798
|
+
...props,
|
|
799
|
+
children: children ?? texts.footerLink
|
|
800
|
+
});
|
|
801
|
+
}
|
|
802
|
+
//#endregion
|
|
803
|
+
//#region src/hooks/useConsentScript.ts
|
|
804
|
+
/**
|
|
805
|
+
* Reactive, consent-gated load status for a script declared in the
|
|
806
|
+
* provider's `scripts` map.
|
|
807
|
+
*
|
|
808
|
+
* The status is gated on the script's `category`: denied → `blocked`;
|
|
809
|
+
* granted → the provider drives `loading` → `loaded`/`error`. The hook never
|
|
810
|
+
* loads anything itself — the provider is the single enforcement point.
|
|
811
|
+
*
|
|
812
|
+
* Must be rendered inside a `CookieBannerConfigurationProvider`.
|
|
813
|
+
*/
|
|
814
|
+
function useConsentScript(id) {
|
|
815
|
+
const context = (0, react.useContext)(CookieBannerContext);
|
|
816
|
+
const def = context?.scripts[id];
|
|
817
|
+
const subscribe = (0, react.useCallback)((notify) => subscribeScript(id, notify), [id]);
|
|
818
|
+
const runtimeStatus = (0, react.useSyncExternalStore)(subscribe, () => getScriptStatus(id), () => "blocked");
|
|
819
|
+
const runtimeError = (0, react.useSyncExternalStore)(subscribe, () => getScriptError(id), () => void 0);
|
|
820
|
+
if (!context) return {
|
|
821
|
+
status: "error",
|
|
822
|
+
error: /* @__PURE__ */ new Error(`useConsentScript("${id}") must be used within a CookieBannerConfigurationProvider.`)
|
|
823
|
+
};
|
|
824
|
+
if (!def) return {
|
|
825
|
+
status: "error",
|
|
826
|
+
error: /* @__PURE__ */ new Error(`useConsentScript("${id}"): script is not declared in the provider's \`scripts\` map.`)
|
|
827
|
+
};
|
|
828
|
+
if (!context.isAllowed(def.category)) return { status: "blocked" };
|
|
829
|
+
const status = runtimeStatus === "blocked" ? "loading" : runtimeStatus;
|
|
830
|
+
return {
|
|
831
|
+
status,
|
|
832
|
+
error: status === "error" ? runtimeError : void 0
|
|
833
|
+
};
|
|
834
|
+
}
|
|
835
|
+
//#endregion
|
|
836
|
+
exports.BUILT_IN_LANGUAGES = require_storage.BUILT_IN_LANGUAGES;
|
|
837
|
+
exports.Button = Button;
|
|
838
|
+
exports.Collapsible = Collapsible;
|
|
839
|
+
exports.CookieBanner = CookieBanner;
|
|
840
|
+
exports.CookieBannerConfigurationProvider = CookieBannerConfigurationProvider;
|
|
841
|
+
exports.CookieSettingsDialog = CookieSettingsDialog;
|
|
842
|
+
exports.CookieSettingsLink = CookieSettingsLink;
|
|
843
|
+
exports.DEFAULT_STORAGE_KEY = require_storage.DEFAULT_STORAGE_KEY;
|
|
844
|
+
exports.Switch = Switch;
|
|
845
|
+
exports.THEME_ATTRIBUTE = THEME_ATTRIBUTE;
|
|
846
|
+
exports.cn = cn;
|
|
847
|
+
exports.createBothStorage = require_storage.createBothStorage;
|
|
848
|
+
exports.createCookieStorage = require_storage.createCookieStorage;
|
|
849
|
+
exports.getBuiltInTexts = require_storage.getBuiltInTexts;
|
|
850
|
+
exports.initGoogleTracker = initGoogleTracker;
|
|
851
|
+
exports.localStorageAdapter = require_storage.localStorageAdapter;
|
|
852
|
+
exports.updateGoogleTracker = updateGoogleTracker;
|
|
853
|
+
exports.useConsentScript = useConsentScript;
|
|
854
|
+
exports.usePreferences = usePreferences;
|
|
855
|
+
|
|
856
|
+
//# sourceMappingURL=index.cjs.map
|