@reblu/site-next 1.8.0 → 1.10.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 +51 -0
- package/README.md +31 -8
- package/dist/academy-server.cjs +52 -0
- package/dist/academy-server.cjs.map +1 -0
- package/dist/academy-server.d.cts +84 -0
- package/dist/academy-server.d.ts +84 -0
- package/dist/academy-server.js +25 -0
- package/dist/academy-server.js.map +1 -0
- package/dist/academy.cjs.map +1 -1
- package/dist/academy.d.cts +3 -82
- package/dist/academy.d.ts +3 -82
- package/dist/academy.js.map +1 -1
- package/dist/consent-server.cjs +88 -0
- package/dist/consent-server.cjs.map +1 -0
- package/dist/consent-server.d.cts +22 -0
- package/dist/consent-server.d.ts +22 -0
- package/dist/consent-server.js +58 -0
- package/dist/consent-server.js.map +1 -0
- package/dist/consent-shared-DbdhEXW2.d.cts +71 -0
- package/dist/consent-shared-DbdhEXW2.d.ts +71 -0
- package/dist/consent.cjs +404 -0
- package/dist/consent.cjs.map +1 -0
- package/dist/consent.d.cts +172 -0
- package/dist/consent.d.ts +172 -0
- package/dist/consent.js +362 -0
- package/dist/consent.js.map +1 -0
- package/dist/site-manifest.cjs +48 -0
- package/dist/site-manifest.cjs.map +1 -0
- package/dist/site-manifest.d.cts +50 -0
- package/dist/site-manifest.d.ts +50 -0
- package/dist/site-manifest.js +23 -0
- package/dist/site-manifest.js.map +1 -0
- package/package.json +45 -2
package/dist/consent.cjs
ADDED
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
"use client";
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __export = (target, all) => {
|
|
10
|
+
for (var name in all)
|
|
11
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
12
|
+
};
|
|
13
|
+
var __copyProps = (to, from, except, desc) => {
|
|
14
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
15
|
+
for (let key of __getOwnPropNames(from))
|
|
16
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
17
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
18
|
+
}
|
|
19
|
+
return to;
|
|
20
|
+
};
|
|
21
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
22
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
23
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
24
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
25
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
26
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
27
|
+
mod
|
|
28
|
+
));
|
|
29
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
30
|
+
|
|
31
|
+
// src/consent/index.ts
|
|
32
|
+
var consent_exports = {};
|
|
33
|
+
__export(consent_exports, {
|
|
34
|
+
ConsentGate: () => ConsentGate,
|
|
35
|
+
ConsentProvider: () => ConsentProvider,
|
|
36
|
+
ConsentedAnalytics: () => ConsentedAnalytics,
|
|
37
|
+
CookieBanner: () => CookieBanner,
|
|
38
|
+
CookiePreferencesTrigger: () => CookiePreferencesTrigger,
|
|
39
|
+
DEFAULT_CONSENT_BANNER_TEXTS: () => DEFAULT_CONSENT_BANNER_TEXTS,
|
|
40
|
+
useConsent: () => useConsent
|
|
41
|
+
});
|
|
42
|
+
module.exports = __toCommonJS(consent_exports);
|
|
43
|
+
|
|
44
|
+
// src/consent/context.tsx
|
|
45
|
+
var import_react = require("react");
|
|
46
|
+
|
|
47
|
+
// src/consent-shared.ts
|
|
48
|
+
var CONSENT_COOKIE_NAME = "reblu_consent";
|
|
49
|
+
var CONSENT_CATEGORIES = ["analytics", "marketing", "preferences"];
|
|
50
|
+
var DEFAULT_CONSENT_CATEGORIES = Object.freeze(
|
|
51
|
+
CONSENT_CATEGORIES.reduce(
|
|
52
|
+
(state, category) => ({ ...state, [category]: false }),
|
|
53
|
+
{}
|
|
54
|
+
)
|
|
55
|
+
);
|
|
56
|
+
function isPlainObject(value) {
|
|
57
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
58
|
+
}
|
|
59
|
+
function parseCategories(value) {
|
|
60
|
+
if (!isPlainObject(value)) return null;
|
|
61
|
+
const result = {};
|
|
62
|
+
for (const category of CONSENT_CATEGORIES) {
|
|
63
|
+
const raw = value[category];
|
|
64
|
+
if (typeof raw !== "boolean") return null;
|
|
65
|
+
result[category] = raw;
|
|
66
|
+
}
|
|
67
|
+
return result;
|
|
68
|
+
}
|
|
69
|
+
function parseConsentCookie(raw) {
|
|
70
|
+
if (!raw) return null;
|
|
71
|
+
let json;
|
|
72
|
+
try {
|
|
73
|
+
json = JSON.parse(raw);
|
|
74
|
+
} catch {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
if (!isPlainObject(json)) return null;
|
|
78
|
+
const categories = parseCategories(json.categories);
|
|
79
|
+
if (!categories) return null;
|
|
80
|
+
const policyVersion = json.policyVersion;
|
|
81
|
+
if (policyVersion !== null && typeof policyVersion !== "string") return null;
|
|
82
|
+
return { categories, policyVersion };
|
|
83
|
+
}
|
|
84
|
+
function serializeConsentCookie(payload) {
|
|
85
|
+
return JSON.stringify(payload);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// src/consent/context.tsx
|
|
89
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
90
|
+
var ConsentContext = (0, import_react.createContext)(null);
|
|
91
|
+
var COOKIE_MAX_AGE_SECONDS = 60 * 60 * 24 * 365;
|
|
92
|
+
function readCookie(name) {
|
|
93
|
+
if (typeof document === "undefined") return null;
|
|
94
|
+
const match = document.cookie.match(new RegExp(`(?:^|; )${name}=([^;]*)`));
|
|
95
|
+
return match ? decodeURIComponent(match[1]) : null;
|
|
96
|
+
}
|
|
97
|
+
function writeCookie(name, value) {
|
|
98
|
+
if (typeof document === "undefined") return;
|
|
99
|
+
document.cookie = `${name}=${encodeURIComponent(value)}; path=/; max-age=${COOKIE_MAX_AGE_SECONDS}; SameSite=Lax`;
|
|
100
|
+
}
|
|
101
|
+
function allCategories(granted) {
|
|
102
|
+
return CONSENT_CATEGORIES.reduce(
|
|
103
|
+
(state, category) => ({ ...state, [category]: granted }),
|
|
104
|
+
{}
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
function ConsentProvider({ children, policyVersion }) {
|
|
108
|
+
const [status, setStatus] = (0, import_react.useState)("unknown");
|
|
109
|
+
const [categories, setCategories] = (0, import_react.useState)(DEFAULT_CONSENT_CATEGORIES);
|
|
110
|
+
const [preferencesOpen, setPreferencesOpen] = (0, import_react.useState)(false);
|
|
111
|
+
const lastFocusedRef = (0, import_react.useRef)(null);
|
|
112
|
+
(0, import_react.useEffect)(() => {
|
|
113
|
+
const parsed = parseConsentCookie(readCookie(CONSENT_COOKIE_NAME));
|
|
114
|
+
if (parsed && parsed.policyVersion === policyVersion) {
|
|
115
|
+
setCategories(parsed.categories);
|
|
116
|
+
setStatus("resolved");
|
|
117
|
+
} else {
|
|
118
|
+
setCategories(DEFAULT_CONSENT_CATEGORIES);
|
|
119
|
+
setStatus("unknown");
|
|
120
|
+
}
|
|
121
|
+
}, [policyVersion]);
|
|
122
|
+
function persist(next) {
|
|
123
|
+
writeCookie(CONSENT_COOKIE_NAME, serializeConsentCookie({ categories: next, policyVersion }));
|
|
124
|
+
setCategories(next);
|
|
125
|
+
setStatus("resolved");
|
|
126
|
+
setPreferencesOpen(false);
|
|
127
|
+
}
|
|
128
|
+
const value = (0, import_react.useMemo)(
|
|
129
|
+
() => ({
|
|
130
|
+
status,
|
|
131
|
+
categories,
|
|
132
|
+
isGranted: (category) => status === "resolved" && categories[category] === true,
|
|
133
|
+
acceptAll: () => persist(allCategories(true)),
|
|
134
|
+
rejectAll: () => persist(allCategories(false)),
|
|
135
|
+
savePreferences: (next) => persist(next),
|
|
136
|
+
preferencesOpen,
|
|
137
|
+
openPreferences: () => {
|
|
138
|
+
lastFocusedRef.current = typeof document !== "undefined" ? document.activeElement : null;
|
|
139
|
+
setPreferencesOpen(true);
|
|
140
|
+
},
|
|
141
|
+
closePreferences: () => setPreferencesOpen(false),
|
|
142
|
+
restoreLastFocus: () => lastFocusedRef.current?.focus()
|
|
143
|
+
}),
|
|
144
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
145
|
+
[status, categories, preferencesOpen, policyVersion]
|
|
146
|
+
);
|
|
147
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ConsentContext.Provider, { value, children });
|
|
148
|
+
}
|
|
149
|
+
function useConsent() {
|
|
150
|
+
const ctx = (0, import_react.useContext)(ConsentContext);
|
|
151
|
+
if (!ctx) throw new Error("useConsent() must be used within a <ConsentProvider>");
|
|
152
|
+
return ctx;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// src/consent/gate.tsx
|
|
156
|
+
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
157
|
+
function ConsentGate({ category, children }) {
|
|
158
|
+
const { status, isGranted } = useConsent();
|
|
159
|
+
if (status !== "resolved") return null;
|
|
160
|
+
if (!isGranted(category)) return null;
|
|
161
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_jsx_runtime2.Fragment, { children });
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// src/analytics.tsx
|
|
165
|
+
var import_script = __toESM(require("next/script"), 1);
|
|
166
|
+
|
|
167
|
+
// src/json-ld.tsx
|
|
168
|
+
var import_jsx_runtime3 = require("react/jsx-runtime");
|
|
169
|
+
var SCRIPT_BOUNDARY_UNSAFE = /[<>&\u2028\u2029]/g;
|
|
170
|
+
var ESCAPES = {
|
|
171
|
+
"<": "\\u003c",
|
|
172
|
+
">": "\\u003e",
|
|
173
|
+
"&": "\\u0026",
|
|
174
|
+
"\u2028": "\\u2028",
|
|
175
|
+
"\u2029": "\\u2029"
|
|
176
|
+
};
|
|
177
|
+
function jsonLdScriptContent(value) {
|
|
178
|
+
const json = JSON.stringify(value) ?? "null";
|
|
179
|
+
return json.replace(SCRIPT_BOUNDARY_UNSAFE, (char) => ESCAPES[char]);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// src/analytics.tsx
|
|
183
|
+
var import_jsx_runtime4 = require("react/jsx-runtime");
|
|
184
|
+
var GA_MEASUREMENT_ID_PATTERN = /^G-[A-Z0-9]{1,32}$/;
|
|
185
|
+
var CLARITY_PROJECT_ID_PATTERN = /^[a-z0-9]{1,32}$/;
|
|
186
|
+
function isValidGaMeasurementId(value) {
|
|
187
|
+
return typeof value === "string" && GA_MEASUREMENT_ID_PATTERN.test(value);
|
|
188
|
+
}
|
|
189
|
+
function isValidClarityProjectId(value) {
|
|
190
|
+
return typeof value === "string" && CLARITY_PROJECT_ID_PATTERN.test(value);
|
|
191
|
+
}
|
|
192
|
+
function analyticsScriptLiteral(id) {
|
|
193
|
+
return jsonLdScriptContent(id);
|
|
194
|
+
}
|
|
195
|
+
function gaInitScript(gaId) {
|
|
196
|
+
return `window.dataLayer=window.dataLayer||[];function gtag(){dataLayer.push(arguments);}gtag('js',new Date());gtag('config',${analyticsScriptLiteral(gaId)});`;
|
|
197
|
+
}
|
|
198
|
+
function clarityInitScript(clarityId) {
|
|
199
|
+
return `(function(c,l,a,r,i,t,y){c[a]=c[a]||function(){(c[a].q=c[a].q||[]).push(arguments)};t=l.createElement(r);t.async=1;t.src="https://www.clarity.ms/tag/"+i;y=l.getElementsByTagName(r)[0];y.parentNode.insertBefore(t,y)})(window,document,"clarity","script",${analyticsScriptLiteral(clarityId)});`;
|
|
200
|
+
}
|
|
201
|
+
function AnalyticsScripts({ gaId, clarityId }) {
|
|
202
|
+
const safeGaId = isValidGaMeasurementId(gaId) ? gaId : null;
|
|
203
|
+
const safeClarityId = isValidClarityProjectId(clarityId) ? clarityId : null;
|
|
204
|
+
if (!safeGaId && !safeClarityId) return null;
|
|
205
|
+
return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_jsx_runtime4.Fragment, { children: [
|
|
206
|
+
safeGaId && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_jsx_runtime4.Fragment, { children: [
|
|
207
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
208
|
+
import_script.default,
|
|
209
|
+
{
|
|
210
|
+
src: `https://www.googletagmanager.com/gtag/js?id=${encodeURIComponent(safeGaId)}`,
|
|
211
|
+
strategy: "afterInteractive"
|
|
212
|
+
}
|
|
213
|
+
),
|
|
214
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
215
|
+
import_script.default,
|
|
216
|
+
{
|
|
217
|
+
id: "ga-init",
|
|
218
|
+
strategy: "afterInteractive",
|
|
219
|
+
dangerouslySetInnerHTML: { __html: gaInitScript(safeGaId) }
|
|
220
|
+
}
|
|
221
|
+
)
|
|
222
|
+
] }),
|
|
223
|
+
safeClarityId && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
224
|
+
import_script.default,
|
|
225
|
+
{
|
|
226
|
+
id: "clarity-init",
|
|
227
|
+
strategy: "afterInteractive",
|
|
228
|
+
dangerouslySetInnerHTML: { __html: clarityInitScript(safeClarityId) }
|
|
229
|
+
}
|
|
230
|
+
)
|
|
231
|
+
] });
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// src/consent/consented-analytics.tsx
|
|
235
|
+
var import_jsx_runtime5 = require("react/jsx-runtime");
|
|
236
|
+
function ConsentedAnalytics(props) {
|
|
237
|
+
return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(ConsentGate, { category: "analytics", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(AnalyticsScripts, { ...props }) });
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// src/consent/banner.tsx
|
|
241
|
+
var import_react2 = require("react");
|
|
242
|
+
var Dialog = __toESM(require("@radix-ui/react-dialog"), 1);
|
|
243
|
+
var import_site_ui = require("@reblu/site-ui");
|
|
244
|
+
|
|
245
|
+
// src/consent/texts.ts
|
|
246
|
+
var DEFAULT_CONSENT_BANNER_TEXTS = {
|
|
247
|
+
title: "Utilizamos cookies",
|
|
248
|
+
description: "Usamos cookies estrictamente necesarias para el funcionamiento del sitio. Con tu permiso, tambi\xE9n usar\xEDamos cookies de anal\xEDtica y marketing para mejorar tu experiencia.",
|
|
249
|
+
acceptLabel: "Aceptar todas",
|
|
250
|
+
rejectLabel: "Rechazar todas",
|
|
251
|
+
manageLabel: "Configurar",
|
|
252
|
+
policyLinkLabel: "Pol\xEDtica de cookies",
|
|
253
|
+
preferencesTitle: "Preferencias de cookies",
|
|
254
|
+
preferencesDescription: "Elige qu\xE9 categor\xEDas de cookies opcionales permites. Las cookies necesarias no se pueden desactivar.",
|
|
255
|
+
saveLabel: "Guardar preferencias",
|
|
256
|
+
necessaryLabel: "Necesarias",
|
|
257
|
+
necessaryDescription: "Imprescindibles para el funcionamiento del sitio, como la seguridad del formulario de contacto. Siempre activas.",
|
|
258
|
+
categoryLabels: {
|
|
259
|
+
analytics: "Anal\xEDtica",
|
|
260
|
+
marketing: "Marketing",
|
|
261
|
+
preferences: "Preferencias"
|
|
262
|
+
},
|
|
263
|
+
categoryDescriptions: {
|
|
264
|
+
analytics: "Nos ayudan a entender c\xF3mo se usa el sitio (por ejemplo, Google Analytics o Microsoft Clarity).",
|
|
265
|
+
marketing: "Permiten mostrar contenido relevante de terceros (por ejemplo, Meta Pixel).",
|
|
266
|
+
preferences: "Recuerdan tus preferencias de navegaci\xF3n entre visitas."
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
// src/consent/banner.tsx
|
|
271
|
+
var import_jsx_runtime6 = require("react/jsx-runtime");
|
|
272
|
+
function resolveTexts(overrides) {
|
|
273
|
+
return { ...DEFAULT_CONSENT_BANNER_TEXTS, ...overrides };
|
|
274
|
+
}
|
|
275
|
+
function CookieBanner({ texts: textsOverride, policyHref = "/cookies" }) {
|
|
276
|
+
const { status, acceptAll, rejectAll, openPreferences } = useConsent();
|
|
277
|
+
const texts = resolveTexts(textsOverride);
|
|
278
|
+
return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_jsx_runtime6.Fragment, { children: [
|
|
279
|
+
status === "unknown" && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
|
|
280
|
+
"div",
|
|
281
|
+
{
|
|
282
|
+
role: "region",
|
|
283
|
+
"aria-label": texts.title,
|
|
284
|
+
className: "fixed inset-x-0 bottom-0 z-50 border-t border-border bg-background/95 px-4 py-4 backdrop-blur supports-[backdrop-filter]:bg-background/80",
|
|
285
|
+
"data-slot": "cookie-banner",
|
|
286
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "mx-auto flex max-w-4xl flex-col gap-3 sm:flex-row sm:items-center sm:justify-between", children: [
|
|
287
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "text-sm", children: [
|
|
288
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { className: "font-semibold", children: texts.title }),
|
|
289
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("p", { className: "text-muted-foreground", children: [
|
|
290
|
+
texts.description,
|
|
291
|
+
" ",
|
|
292
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("a", { href: policyHref, className: "underline underline-offset-2", children: texts.policyLinkLabel })
|
|
293
|
+
] })
|
|
294
|
+
] }),
|
|
295
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "flex shrink-0 flex-wrap gap-2", children: [
|
|
296
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_site_ui.Button, { type: "button", variant: "outline", size: "sm", onClick: rejectAll, children: texts.rejectLabel }),
|
|
297
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_site_ui.Button, { type: "button", variant: "outline", size: "sm", onClick: openPreferences, children: texts.manageLabel }),
|
|
298
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_site_ui.Button, { type: "button", variant: "outline", size: "sm", onClick: acceptAll, children: texts.acceptLabel })
|
|
299
|
+
] })
|
|
300
|
+
] })
|
|
301
|
+
}
|
|
302
|
+
),
|
|
303
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)(ConsentPreferencesDialog, { texts })
|
|
304
|
+
] });
|
|
305
|
+
}
|
|
306
|
+
function CookiePreferencesTrigger({
|
|
307
|
+
children,
|
|
308
|
+
className,
|
|
309
|
+
texts: textsOverride
|
|
310
|
+
}) {
|
|
311
|
+
const { openPreferences } = useConsent();
|
|
312
|
+
const texts = resolveTexts(textsOverride);
|
|
313
|
+
return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("button", { type: "button", onClick: openPreferences, className, "data-slot": "cookie-preferences-trigger", children: children ?? `${texts.manageLabel} cookies` });
|
|
314
|
+
}
|
|
315
|
+
function ConsentPreferencesDialog({ texts }) {
|
|
316
|
+
const { categories, preferencesOpen, closePreferences, savePreferences, restoreLastFocus } = useConsent();
|
|
317
|
+
const [draft, setDraft] = (0, import_react2.useState)(categories);
|
|
318
|
+
(0, import_react2.useEffect)(() => {
|
|
319
|
+
if (preferencesOpen) setDraft(categories);
|
|
320
|
+
}, [preferencesOpen]);
|
|
321
|
+
return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
|
|
322
|
+
Dialog.Root,
|
|
323
|
+
{
|
|
324
|
+
open: preferencesOpen,
|
|
325
|
+
onOpenChange: (open) => {
|
|
326
|
+
if (!open) closePreferences();
|
|
327
|
+
},
|
|
328
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(Dialog.Portal, { children: [
|
|
329
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Dialog.Overlay, { className: "fixed inset-0 z-50 bg-black/50" }),
|
|
330
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
|
|
331
|
+
Dialog.Content,
|
|
332
|
+
{
|
|
333
|
+
className: "fixed left-1/2 top-1/2 z-50 w-full max-w-md -translate-x-1/2 -translate-y-1/2 rounded-lg border border-border bg-background p-6 shadow-lg",
|
|
334
|
+
"data-slot": "cookie-preferences-dialog",
|
|
335
|
+
onCloseAutoFocus: (event) => {
|
|
336
|
+
event.preventDefault();
|
|
337
|
+
restoreLastFocus();
|
|
338
|
+
},
|
|
339
|
+
children: [
|
|
340
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Dialog.Title, { className: "text-base font-semibold", children: texts.preferencesTitle }),
|
|
341
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Dialog.Description, { className: "mt-1 text-sm text-muted-foreground", children: texts.preferencesDescription }),
|
|
342
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "mt-4 flex flex-col gap-3", children: [
|
|
343
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
|
|
344
|
+
CategoryRow,
|
|
345
|
+
{
|
|
346
|
+
id: "consent-category-necessary",
|
|
347
|
+
label: texts.necessaryLabel,
|
|
348
|
+
description: texts.necessaryDescription,
|
|
349
|
+
checked: true,
|
|
350
|
+
disabled: true,
|
|
351
|
+
onChange: () => void 0
|
|
352
|
+
}
|
|
353
|
+
),
|
|
354
|
+
CONSENT_CATEGORIES.map((category) => /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
|
|
355
|
+
CategoryRow,
|
|
356
|
+
{
|
|
357
|
+
id: `consent-category-${category}`,
|
|
358
|
+
label: texts.categoryLabels[category],
|
|
359
|
+
description: texts.categoryDescriptions[category],
|
|
360
|
+
checked: draft[category],
|
|
361
|
+
disabled: false,
|
|
362
|
+
onChange: (checked) => setDraft((prev) => ({ ...prev, [category]: checked }))
|
|
363
|
+
},
|
|
364
|
+
category
|
|
365
|
+
))
|
|
366
|
+
] }),
|
|
367
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "mt-6 flex justify-end", children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_site_ui.Button, { type: "button", size: "sm", onClick: () => savePreferences(draft), children: texts.saveLabel }) })
|
|
368
|
+
]
|
|
369
|
+
}
|
|
370
|
+
)
|
|
371
|
+
] })
|
|
372
|
+
}
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
function CategoryRow({ id, label, description, checked, disabled, onChange }) {
|
|
376
|
+
return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: (0, import_site_ui.cn)("flex items-start gap-3", disabled && "opacity-70"), children: [
|
|
377
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
|
|
378
|
+
"input",
|
|
379
|
+
{
|
|
380
|
+
id,
|
|
381
|
+
type: "checkbox",
|
|
382
|
+
checked,
|
|
383
|
+
disabled,
|
|
384
|
+
onChange: (event) => onChange(event.target.checked),
|
|
385
|
+
className: "mt-1 size-4"
|
|
386
|
+
}
|
|
387
|
+
),
|
|
388
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("label", { htmlFor: id, className: "flex flex-col text-sm", children: [
|
|
389
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "font-medium", children: label }),
|
|
390
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "text-muted-foreground", children: description })
|
|
391
|
+
] })
|
|
392
|
+
] });
|
|
393
|
+
}
|
|
394
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
395
|
+
0 && (module.exports = {
|
|
396
|
+
ConsentGate,
|
|
397
|
+
ConsentProvider,
|
|
398
|
+
ConsentedAnalytics,
|
|
399
|
+
CookieBanner,
|
|
400
|
+
CookiePreferencesTrigger,
|
|
401
|
+
DEFAULT_CONSENT_BANNER_TEXTS,
|
|
402
|
+
useConsent
|
|
403
|
+
});
|
|
404
|
+
//# sourceMappingURL=consent.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/consent/index.ts","../src/consent/context.tsx","../src/consent-shared.ts","../src/consent/gate.tsx","../src/analytics.tsx","../src/json-ld.tsx","../src/consent/consented-analytics.tsx","../src/consent/banner.tsx","../src/consent/texts.ts"],"sourcesContent":["'use client'\n\n/**\n * `@reblu/site-next/consent` — REB-642's consent mechanism.\n *\n * This barrel MUST begin with its OWN leading `'use client'` directive: tsup\n * only preserves a directive prologue declared by the entry file itself, not\n * one merely re-exported from a child module (see the `cta-buttons` /\n * `auth` R4 precedent in `../../tsup.config.ts`).\n *\n * See `docs/architecture/cookie-consent.md` for the full design.\n *\n * Deliberately does NOT re-export `CONSENT_CATEGORIES`, `CONSENT_COOKIE_NAME`,\n * `ConsentCategory` or anything else from `../consent-server` (or\n * `../consent-shared`) here — even though every one of those individual names\n * is itself dependency-free. This package's `treeshake: false` build (see the\n * R4 note in `../../tsup.config.ts`) bundles a re-exporting barrel's IMPORTED\n * FILES wholesale, and keeping this barrel's only source-level edge to\n * anything under `../consent-server*` at zero is what makes that provable by\n * `tests/consent-subpath-isolation.test.ts` rather than merely asserted by\n * this comment. A caller that needs the category taxonomy or the cookie name\n * imports them directly from `@reblu/site-next/consent-server`, which stays\n * the one full public surface (client + server share the same values).\n */\nexport {\n ConsentProvider,\n useConsent,\n type ConsentStatus,\n type ConsentContextValue,\n type ConsentProviderProps,\n} from './context'\nexport { ConsentGate, type ConsentGateProps } from './gate'\nexport { ConsentedAnalytics } from './consented-analytics'\nexport {\n CookieBanner,\n CookiePreferencesTrigger,\n type CookieBannerProps,\n type CookiePreferencesTriggerProps,\n} from './banner'\nexport { DEFAULT_CONSENT_BANNER_TEXTS, type ConsentBannerTexts } from './texts'\n","'use client'\n\nimport { createContext, useContext, useEffect, useMemo, useRef, useState, type ReactNode } from 'react'\n\nimport {\n CONSENT_CATEGORIES,\n CONSENT_COOKIE_NAME,\n DEFAULT_CONSENT_CATEGORIES,\n parseConsentCookie,\n serializeConsentCookie,\n type ConsentCategory,\n type ConsentCategoryState,\n // Deliberately `../consent-shared`, NEVER `../consent-server`: the latter\n // pulls in `cookiePolicyVersion` and its `node:crypto` import, which must\n // never reach this client entry — see `../consent-shared.ts`'s doc comment.\n} from '../consent-shared'\n\n/**\n * `<ConsentProvider>` / `useConsent()` — the client half of cookie consent\n * (REB-642). See `../consent-shared` for the fail-closed parsing this relies\n * on (imported from there, not `../consent-server`, to keep `node:crypto` out\n * of this client entry), and `docs/architecture/cookie-consent.md` for the\n * full design.\n *\n * `status: 'unknown'` is the DEFAULT-DENIED state and the value every\n * `<ConsentGate>` treats as \"not granted\" — see `./gate.tsx`. It covers three\n * cases identically, on purpose: no cookie yet, a cookie that failed\n * `parseConsentCookie`, and a cookie recorded under a `policyVersion` that no\n * longer matches (the tenant edited the cookie policy — REB-642's \"re-ask when\n * the version changes\"). None of the three may ever resolve to granted.\n */\n\n/** Whether the visitor's choice for the CURRENT policy version is known. */\nexport type ConsentStatus = 'unknown' | 'resolved'\n\nexport interface ConsentContextValue {\n status: ConsentStatus\n categories: ConsentCategoryState\n /** True only when `status === 'resolved'` AND the category was explicitly granted. */\n isGranted: (category: ConsentCategory) => boolean\n /** Grants every category and persists immediately. */\n acceptAll: () => void\n /** Denies every category and persists immediately — the denial persists across reloads. */\n rejectAll: () => void\n /** Persists exactly the categories passed — no category is granted implicitly. */\n savePreferences: (categories: ConsentCategoryState) => void\n preferencesOpen: boolean\n openPreferences: () => void\n closePreferences: () => void\n /**\n * Refocuses whatever element had focus immediately before `openPreferences`\n * was called. The preferences dialog is openable from more than one place\n * (the banner's \"Configure\" button, `<CookiePreferencesTrigger>` elsewhere\n * in the tree) with no single `<Dialog.Trigger>` for Radix's own\n * focus-restore to key off, so this is tracked explicitly here — the one\n * funnel every opener already goes through.\n */\n restoreLastFocus: () => void\n}\n\nconst ConsentContext = createContext<ConsentContextValue | null>(null)\n\nexport interface ConsentProviderProps {\n children: ReactNode\n /**\n * `cookiePolicyVersion(tenant.cookiePolicy)` from the SITE's analytics\n * config, or `null` when no cookie policy is stored. A stored consent\n * recorded under a DIFFERENT version is treated as unresolved.\n */\n policyVersion: string | null\n}\n\n/** 1 year — matches the retention window discussed in the design proposal §9. */\nconst COOKIE_MAX_AGE_SECONDS = 60 * 60 * 24 * 365\n\nfunction readCookie(name: string): string | null {\n if (typeof document === 'undefined') return null\n const match = document.cookie.match(new RegExp(`(?:^|; )${name}=([^;]*)`))\n return match ? decodeURIComponent(match[1]) : null\n}\n\nfunction writeCookie(name: string, value: string) {\n if (typeof document === 'undefined') return\n document.cookie = `${name}=${encodeURIComponent(value)}; path=/; max-age=${COOKIE_MAX_AGE_SECONDS}; SameSite=Lax`\n}\n\nfunction allCategories(granted: boolean): ConsentCategoryState {\n return CONSENT_CATEGORIES.reduce(\n (state, category) => ({ ...state, [category]: granted }),\n {} as ConsentCategoryState,\n )\n}\n\nexport function ConsentProvider({ children, policyVersion }: ConsentProviderProps) {\n const [status, setStatus] = useState<ConsentStatus>('unknown')\n const [categories, setCategories] = useState<ConsentCategoryState>(DEFAULT_CONSENT_CATEGORIES)\n const [preferencesOpen, setPreferencesOpen] = useState(false)\n const lastFocusedRef = useRef<HTMLElement | null>(null)\n\n // Resolve from the cookie on mount, and whenever the tenant's policy version\n // changes (e.g. a client-navigation that re-fetches SITE config). `document`\n // is unavailable during SSR, so this is deliberately an effect, not a\n // useState initializer — the first render is always 'unknown', avoiding a\n // server/client markup mismatch, and resolves one tick later if a valid\n // cookie exists.\n useEffect(() => {\n const parsed = parseConsentCookie(readCookie(CONSENT_COOKIE_NAME))\n if (parsed && parsed.policyVersion === policyVersion) {\n setCategories(parsed.categories)\n setStatus('resolved')\n } else {\n setCategories(DEFAULT_CONSENT_CATEGORIES)\n setStatus('unknown')\n }\n }, [policyVersion])\n\n function persist(next: ConsentCategoryState) {\n writeCookie(CONSENT_COOKIE_NAME, serializeConsentCookie({ categories: next, policyVersion }))\n setCategories(next)\n setStatus('resolved')\n setPreferencesOpen(false)\n }\n\n const value = useMemo<ConsentContextValue>(\n () => ({\n status,\n categories,\n isGranted: (category) => status === 'resolved' && categories[category] === true,\n acceptAll: () => persist(allCategories(true)),\n rejectAll: () => persist(allCategories(false)),\n savePreferences: (next) => persist(next),\n preferencesOpen,\n openPreferences: () => {\n lastFocusedRef.current =\n typeof document !== 'undefined' ? (document.activeElement as HTMLElement | null) : null\n setPreferencesOpen(true)\n },\n closePreferences: () => setPreferencesOpen(false),\n restoreLastFocus: () => lastFocusedRef.current?.focus(),\n }),\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [status, categories, preferencesOpen, policyVersion],\n )\n\n return <ConsentContext.Provider value={value}>{children}</ConsentContext.Provider>\n}\n\n/** Throws outside a `<ConsentProvider>` — a silent default would be a silent consent bypass. */\nexport function useConsent(): ConsentContextValue {\n const ctx = useContext(ConsentContext)\n if (!ctx) throw new Error('useConsent() must be used within a <ConsentProvider>')\n return ctx\n}\n","/**\n * `../consent-shared` — the truly isomorphic core of cookie consent (REB-642):\n * the cookie name, the category taxonomy, and fail-closed parse/serialise.\n * ZERO dependencies, including Node builtins — this is the one thing that\n * matters about this file's existence.\n *\n * ─────────────────────────────────────────────────────────────────────────────\n * WHY THIS IS A SEPARATE FILE FROM `./consent-server.ts`\n * ─────────────────────────────────────────────────────────────────────────────\n *\n * `./consent` (the client entry) needs `CONSENT_CATEGORIES` and friends, and\n * imports them straight from here. `./consent-server.ts` ALSO needs them, plus\n * `cookiePolicyVersion()`, which needs `node:crypto`. Those two facts collide\n * under this package's build rules: `tsup.config.ts` runs with\n * `treeshake: false` — REQUIRED to keep the `'use client'` prologue on the\n * `./consent` entry from being stripped (see the R4 note there) — and with\n * tree-shaking off, esbuild bundles an imported FILE WHOLESALE, not just the\n * specific binding a module asked for. So if `cookiePolicyVersion` (and its\n * `node:crypto` import) lived in the SAME file as `CONSENT_CATEGORIES`, every\n * client component that only wanted the category list would drag `node:crypto`\n * into the browser bundle too — dead weight at best, a broken build on a\n * bundler stricter than tsup about Node builtins in client code at worst. This\n * was caught live: `tests/consent-subpath-isolation.test.ts` asserts `./consent`\n * never references `crypto`, and failed until this split existed.\n *\n * `./consent-server.ts` re-exports everything here, so\n * `@reblu/site-next/consent-server` keeps serving the full public surface —\n * this split is purely internal.\n *\n * A cookie is attacker- and drift-controlled input: a visitor can edit it in\n * devtools, an older SDK version can leave a differently-shaped payload\n * behind, and a proxy or extension can mangle it in transit.\n * `parseConsentCookie` never coerces a doubtful value toward \"granted\" —\n * anything that is not EXACTLY the expected shape (an object, every known\n * category present and a strict boolean, `policyVersion` a string or `null`)\n * resolves to `null`, which `./consent`'s provider treats identically to \"no\n * cookie yet\": ask again, default every category denied. Silently granting a\n * truthy-but-malformed value is the one mistake this module is built to make\n * impossible.\n */\n\n/** The first-party cookie a visitor's consent choice is stored in. */\nexport const CONSENT_COOKIE_NAME = 'reblu_consent'\n\n/**\n * The fixed, non-tenant-editable category taxonomy (REB-642 §7.1): \"If each\n * SITE invents its own categories, no two clinics can be audited the same\n * way.\" `necessary` is deliberately NOT a member — it is not a choice, it has\n * no toggle, and it is never read from or written to the cookie.\n */\nexport const CONSENT_CATEGORIES = ['analytics', 'marketing', 'preferences'] as const\n\n/** One of the fixed consent categories. */\nexport type ConsentCategory = (typeof CONSENT_CATEGORIES)[number]\n\n/** A visitor's granted/denied state for every optional category. */\nexport type ConsentCategoryState = Record<ConsentCategory, boolean>\n\n/** The full payload stored in the consent cookie. */\nexport interface ConsentCookiePayload {\n categories: ConsentCategoryState\n /** The `cookiePolicyVersion()` in force when this choice was recorded, or `null`. */\n policyVersion: string | null\n}\n\n/** Every optional category denied — the only safe default. */\nexport const DEFAULT_CONSENT_CATEGORIES: Readonly<ConsentCategoryState> = Object.freeze(\n CONSENT_CATEGORIES.reduce(\n (state, category) => ({ ...state, [category]: false }),\n {} as ConsentCategoryState,\n ),\n)\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value)\n}\n\n/**\n * Reads and validates the categories object, requiring every known category to\n * be present as a STRICT boolean. Returns `null` on any deviation — a missing\n * category, an extra one being the only thing wrong is fine (ignored, never\n * leaked into the result), but a category that is present and not `true`/`false`\n * literally, or one that is simply absent, fails the whole parse closed. A\n * partial payload does NOT default the missing categories to `false`: that\n * would make \"a payload half-stripped by a proxy\" indistinguishable from \"a\n * visitor who explicitly denied the rest\", which is not a distinction this\n * function is in a position to make safely.\n */\nfunction parseCategories(value: unknown): ConsentCategoryState | null {\n if (!isPlainObject(value)) return null\n\n const result = {} as ConsentCategoryState\n for (const category of CONSENT_CATEGORIES) {\n const raw = value[category]\n if (typeof raw !== 'boolean') return null\n result[category] = raw\n }\n return result\n}\n\n/**\n * Parses a raw consent cookie value into a validated payload, or `null` if the\n * value is absent or does not match the expected shape exactly. See the module\n * doc for why this fails closed rather than coercing.\n */\nexport function parseConsentCookie(raw: string | null | undefined): ConsentCookiePayload | null {\n if (!raw) return null\n\n let json: unknown\n try {\n json = JSON.parse(raw)\n } catch {\n return null\n }\n\n if (!isPlainObject(json)) return null\n\n const categories = parseCategories(json.categories)\n if (!categories) return null\n\n const policyVersion = json.policyVersion\n if (policyVersion !== null && typeof policyVersion !== 'string') return null\n\n return { categories, policyVersion }\n}\n\n/** Serialises a consent payload for storage in the cookie. */\nexport function serializeConsentCookie(payload: ConsentCookiePayload): string {\n return JSON.stringify(payload)\n}\n","'use client'\n\nimport type { ReactNode } from 'react'\n\n// Deliberately `../consent-shared`, not `../consent-server` — see that\n// module's doc comment for why `node:crypto` must never reach this client entry.\nimport type { ConsentCategory } from '../consent-shared'\nimport { useConsent } from './context'\n\nexport interface ConsentGateProps {\n category: ConsentCategory\n children: ReactNode\n}\n\n/**\n * `<ConsentGate>` — the allowlist enforced by construction that REB-642's\n * design settles on: a third party is never rendered except through a\n * component that names a consent category, and a denied or UNRESOLVED\n * category renders `null`.\n *\n * The `status !== 'resolved'` branch matters as much as the denied branch: a\n * gate that rendered children while consent is still `'unknown'` would load\n * the third party on every fresh visitor for the split second before their\n * cookie is read, which is exactly the race `docs/architecture/cookie-consent.md`\n * §6.1 designs around by building tags inside a client component instead of\n * passing a server-rendered `<AnalyticsScripts>` through as `children`.\n */\nexport function ConsentGate({ category, children }: ConsentGateProps) {\n const { status, isGranted } = useConsent()\n\n if (status !== 'resolved') return null\n if (!isGranted(category)) return null\n\n return <>{children}</>\n}\n","import Script from 'next/script'\n\nimport { jsonLdScriptContent } from './json-ld'\n\n/**\n * `<AnalyticsScripts>` — safe rendering of tenant-configured analytics tags (REB-617).\n *\n * THE HOLE THIS CLOSES. Every SITE built on this SDK interpolated the tenant's\n * analytics ids straight into the body of an executing `<script>`:\n *\n * gtag('config','${analytics.gaId}');\n * …\"clarity\",\"script\",\"${analytics.clarityId}\");\n *\n * No validation, no escaping. A `gaId` of `x');alert(document.cookie);//` closes\n * the string literal, closes the call, comments out the rest, and runs arbitrary\n * JS on the TENANT'S OWN ORIGIN — on every page including checkout, same origin\n * as the portal session cookie. For a medical tenant that is patient-session\n * territory. It is the same trust boundary as the nexus-authored HTML that\n * REB-613 closed on the sink next door, except this one reached the JS\n * interpreter with no sanitiser anywhere in the path.\n *\n * TWO INDEPENDENT LAYERS, because either alone is one refactor away from failing:\n *\n * 1. VALIDATION — an id that does not match its vendor-documented shape is\n * dropped and NO script renders. Analytics silently does not load; the site\n * keeps working. Failing closed matters more than failing loud here: the\n * value is cosmetic, the sink is not.\n * 2. ESCAPING — `analyticsScriptLiteral` emits the id as a JSON string literal\n * with the `<script>` boundary neutralised, so even a value that skipped\n * layer 1 (a future field with a looser shape, a caller that reaches for the\n * builders directly) is inert DATA rather than code.\n *\n * WHY VALIDATION ALONE IS NOT ENOUGH, and escaping alone is not either: layer 1\n * is a policy that a product decision can widen (someone adds `GT-` ids, or a\n * Meta Pixel field whose shape is numeric-but-unbounded), and layer 2 is a\n * mechanism that cannot express \"this is not a usable analytics id\". Keeping both\n * means widening the policy never reopens the sink.\n *\n * This lives in the SDK, not in `apps/demo-site`, because every external SITE repo\n * consuming `@reblu/site-contracts` carries the identical snippet — publishing the\n * safe component is the only way the fix reaches them.\n */\n\n/**\n * GA4 measurement id: `G-` followed by alphanumeric characters.\n *\n * Vendor source: Google Analytics Help 12270356 (\"formatted as 'G-' followed by\n * alphanumeric characters\", example `G-PSW1MY7HB4`).\n *\n * NO MINIMUM LENGTH, deliberately. The familiar id is ten characters, but Google\n * documents no length at all and its own docs print `gtag('config', 'G-12345')`\n * (Help 11259997) — inventing a floor here would reject an id Google itself\n * publishes. The upper bound is sanity, not security: the `[A-Z0-9]` class already\n * excludes every character that could leave a JS string literal, so length adds\n * nothing to the guarantee.\n *\n * Uppercase only — every documented example is uppercase, and a lowercase variant\n * is not a Google-issued id. Google tag ids also exist as `GT-` and `AW-`\n * (Help 12326985), but this field is specifically the GA4 *measurement id*\n * (the nexus form labels it so, and the SITE reads it as `gaId`); accepting the\n * other prefixes is a product decision, not a security one, and is deliberately\n * left out of this fix.\n */\nexport const GA_MEASUREMENT_ID_PATTERN = /^G-[A-Z0-9]{1,32}$/\n\n/**\n * Microsoft Clarity project id: lowercase alphanumeric.\n *\n * Vendor source: Microsoft Learn `clarity-setup` publishes a live demo project\n * under id `3t0wlogvdz`; every id Microsoft prints is lowercase alphanumeric of\n * about ten characters. Microsoft documents no formal grammar, so — as with GA —\n * the bound is generous and the character class carries the guarantee.\n *\n * RESIDUAL RISK, stated plainly: no vendor page enumerates the legal Clarity\n * alphabet, so this is inferred from published examples. If some tenant holds an\n * id containing a character outside `[a-z0-9]`, this rejects it. That is why the\n * API coerces a non-matching id to `null` instead of erroring — analytics stops\n * loading for that tenant, the site keeps serving, and the misconfiguration is\n * visible and fixable rather than an outage.\n */\nexport const CLARITY_PROJECT_ID_PATTERN = /^[a-z0-9]{1,32}$/\n\n/** True only for a string matching the vendor-documented GA4 measurement id shape. */\nexport function isValidGaMeasurementId(value: unknown): value is string {\n return typeof value === 'string' && GA_MEASUREMENT_ID_PATTERN.test(value)\n}\n\n/** True only for a string matching the vendor-documented Clarity project id shape. */\nexport function isValidClarityProjectId(value: unknown): value is string {\n return typeof value === 'string' && CLARITY_PROJECT_ID_PATTERN.test(value)\n}\n\n/**\n * Serialises `id` as a JS string literal safe to embed in a `<script>` body.\n *\n * Reuses `jsonLdScriptContent` rather than growing a second escaper: a JSON string\n * literal IS a valid JS string literal, and that helper already does the two things\n * required here — `JSON.stringify` for the quote/backslash/newline boundary, plus\n * `<` `>` `&` U+2028 U+2029 escaping for the HTML raw-text boundary that\n * `JSON.stringify` leaves wide open. One audited primitive, one place to fix.\n */\nexport function analyticsScriptLiteral(id: string): string {\n return jsonLdScriptContent(id)\n}\n\n/** Body of the GA4 bootstrap script. Always escapes; never trusts its caller. */\nexport function gaInitScript(gaId: string): string {\n return `window.dataLayer=window.dataLayer||[];function gtag(){dataLayer.push(arguments);}gtag('js',new Date());gtag('config',${analyticsScriptLiteral(gaId)});`\n}\n\n/** Body of the Microsoft Clarity bootstrap script. Always escapes; never trusts its caller. */\nexport function clarityInitScript(clarityId: string): string {\n return `(function(c,l,a,r,i,t,y){c[a]=c[a]||function(){(c[a].q=c[a].q||[]).push(arguments)};t=l.createElement(r);t.async=1;t.src=\"https://www.clarity.ms/tag/\"+i;y=l.getElementsByTagName(r)[0];y.parentNode.insertBefore(t,y)})(window,document,\"clarity\",\"script\",${analyticsScriptLiteral(clarityId)});`\n}\n\nexport interface AnalyticsScriptsProps {\n /** GA4 measurement id from the SITE analytics contract. Ignored unless it matches `G-…`. */\n gaId?: string | null\n /** Microsoft Clarity project id from the SITE analytics contract. Ignored unless lowercase alphanumeric. */\n clarityId?: string | null\n}\n\n/**\n * Renders the Google Analytics and Microsoft Clarity tags for a tenant, dropping\n * any id that does not match its vendor-documented shape.\n */\nexport function AnalyticsScripts({ gaId, clarityId }: AnalyticsScriptsProps) {\n const safeGaId = isValidGaMeasurementId(gaId) ? gaId : null\n const safeClarityId = isValidClarityProjectId(clarityId) ? clarityId : null\n\n if (!safeGaId && !safeClarityId) return null\n\n return (\n <>\n {safeGaId && (\n <>\n <Script\n src={`https://www.googletagmanager.com/gtag/js?id=${encodeURIComponent(safeGaId)}`}\n strategy='afterInteractive'\n />\n <Script\n id='ga-init'\n strategy='afterInteractive'\n dangerouslySetInnerHTML={{ __html: gaInitScript(safeGaId) }}\n />\n </>\n )}\n\n {safeClarityId && (\n <Script\n id='clarity-init'\n strategy='afterInteractive'\n dangerouslySetInnerHTML={{ __html: clarityInitScript(safeClarityId) }}\n />\n )}\n </>\n )\n}\n","/**\n * `<JsonLd>` — safe `application/ld+json` script renderer (REB-622).\n *\n * A `<script>` element is HTML's \"raw text\" context: the parser closes it on the\n * first case-insensitive `</script` byte sequence it sees, no matter whether\n * that sequence sits inside a JSON string. `JSON.stringify` escapes for JSON\n * syntax, not for this HTML boundary, so it leaves `<` verbatim — turning any\n * JSON-LD payload built from tenant-authored data (page title, description,\n * FAQ copy, …) into a stored-XSS sink: a value containing\n * `</script><img src=x onerror=alert(1)>` breaks out of the element and runs.\n *\n * `jsonLdScriptContent` neutralises exactly that boundary while staying\n * byte-for-byte valid JSON: `<` / `>` / `&` are standard JSON\n * character escapes, so `JSON.parse` recovers the original value unchanged —\n * this is an ESCAPER, not a strip/allowlist sanitiser (see `sanitizeHtml` in\n * `@reblu/site-blocks` for that job, at the HTML boundary rather than the\n * script boundary). U+2028/U+2029 are escaped too: both are valid inside a\n * JSON string but are raw JS line terminators, and some `<script>` consumers\n * (older JS parsers, some static analysers) choke on them unescaped.\n *\n * This package, not `@reblu/site-blocks`, is the home for it: `site-blocks`'s\n * main entry bundles `parse5` + the `hast-util-*` chain + `marked` for\n * `sanitizeHtml` with `treeshake: false` — dist/index.js is ~463 KB, and PR #848\n * spent a full round removing exactly that weight (89 KB gzipped) from a client\n * bundle that only needed one HTML-escaping helper. `<JsonLd>` is Next-specific\n * (App Router's `suppressHydrationWarning` pattern) and this 40-line escaper has\n * zero dependencies of its own, so it ships as its OWN tsup entry\n * (`@reblu/site-next/json-ld`) rather than folding into the root `index` barrel\n * — importing it must never drag in `CtaButtons`/`EditableCta`'s `next/link` +\n * `@reblu/site-ui` graph, the same isolation `./cache` and `./route-handlers`\n * already use for the same reason.\n *\n * @see https://nextjs.org/docs/app/guides/json-ld\n */\n\nconst SCRIPT_BOUNDARY_UNSAFE = /[<>&\\u2028\\u2029]/g\n\nconst ESCAPES: Record<string, string> = {\n '<': '\\\\u003c',\n '>': '\\\\u003e',\n '&': '\\\\u0026',\n '\\u2028': '\\\\u2028',\n '\\u2029': '\\\\u2029',\n}\n\n/**\n * Serialises `value` for embedding inside `<script type=\"application/ld+json\">`.\n * Escapes `<`, `>`, `&`, U+2028 and U+2029 — every escape is a standard JSON\n * character escape, so `JSON.parse(jsonLdScriptContent(value))` always recovers\n * `value` unchanged. This only ever ADDS escaping; it never strips or rewrites\n * data, unlike an HTML sanitiser.\n *\n * `JSON.stringify(undefined)` returns the JS value `undefined`, not a string\n * (`undefined` has no JSON representation), which would otherwise throw here.\n * `data` is typed `unknown` with zero friction toward `undefined`, so this\n * falls back to the JSON literal `'null'` rather than crashing an entire page\n * render over one missing field.\n */\nexport function jsonLdScriptContent(value: unknown): string {\n const json = JSON.stringify(value) ?? 'null'\n return json.replace(SCRIPT_BOUNDARY_UNSAFE, (char) => ESCAPES[char])\n}\n\nexport interface JsonLdProps {\n /** Suffix for the rendered `<script>` id: `json-ld-${id}`. */\n id: string\n /** Structured data to serialise. Any JSON-serialisable value. */\n data: unknown\n}\n\n/**\n * Renders one `application/ld+json` `<script>` tag using the official Next.js\n * App Router pattern. `suppressHydrationWarning` is required here: React warns\n * on a server/client `textContent` mismatch for this element even though there\n * is no real divergence — `dangerouslySetInnerHTML` content is opaque to React's\n * hydration diff.\n */\nexport function JsonLd({ id, data }: JsonLdProps) {\n return (\n <script\n id={`json-ld-${id}`}\n type='application/ld+json'\n suppressHydrationWarning\n dangerouslySetInnerHTML={{ __html: jsonLdScriptContent(data) }}\n />\n )\n}\n","'use client'\n\nimport { AnalyticsScripts, type AnalyticsScriptsProps } from '../analytics'\nimport { ConsentGate } from './gate'\n\n/**\n * `<ConsentedAnalytics>` — the `'analytics'`-gated replacement for rendering\n * `<AnalyticsScripts>` directly in a SITE layout (REB-642 Slice 1).\n *\n * Reuses the REB-617 validation + escaping builders verbatim via\n * `<AnalyticsScripts>` — this component adds NOTHING to the sink-safety\n * argument in `../analytics.tsx`, it only decides WHETHER that already-safe\n * renderer mounts at all. A SITE layout that still imports\n * `@reblu/site-next/analytics` directly bypasses consent entirely, which is\n * exactly what the source-scanning guard (REB-642 Slice 1, demo-site) exists\n * to catch — this component is the mechanism that guard enforces.\n */\nexport function ConsentedAnalytics(props: AnalyticsScriptsProps) {\n return (\n <ConsentGate category='analytics'>\n <AnalyticsScripts {...props} />\n </ConsentGate>\n )\n}\n","'use client'\n\nimport { useEffect, useState, type ReactNode } from 'react'\nimport * as Dialog from '@radix-ui/react-dialog'\nimport { Button, cn } from '@reblu/site-ui'\n\n// Deliberately `../consent-shared`, not `../consent-server` — see that\n// module's doc comment for why `node:crypto` must never reach this client entry.\nimport { CONSENT_CATEGORIES, type ConsentCategoryState } from '../consent-shared'\nimport { useConsent } from './context'\nimport { DEFAULT_CONSENT_BANNER_TEXTS, type ConsentBannerTexts } from './texts'\n\n/**\n * `<CookieBanner>` + `<CookiePreferencesTrigger>` — the visible layer of\n * REB-642's consent mechanism.\n *\n * ─────────────────────────────────────────────────────────────────────────────\n * ARCHITECTURE: ONE DIALOG, TWO OPENERS\n * ─────────────────────────────────────────────────────────────────────────────\n *\n * `<CookieBanner>` owns rendering the preferences dialog (mount it ONCE, at\n * the root of the SITE layout — it portals to `<body>` regardless of where it\n * is mounted). `<CookiePreferencesTrigger>` is a small, unstyled-by-default\n * button usable ANYWHERE ELSE in the tree (a footer link, a settings page) that\n * opens the SAME dialog via `useConsent().openPreferences()`. Both read the\n * single `preferencesOpen` boolean the `<ConsentProvider>` owns, so there is\n * never more than one dialog instance and never two focus traps competing.\n *\n * This is what REB-642 calls \"permanent withdrawal\" — a visitor's route back\n * to their preferences does not depend on the first-layer banner still being\n * on screen; the banner disappears once resolved, the trigger does not.\n *\n * ─────────────────────────────────────────────────────────────────────────────\n * THE THREE-BUTTON RULE (owner decision, not configurable)\n * ─────────────────────────────────────────────────────────────────────────────\n *\n * Reject / Configure / Accept render as three same-variant, same-size buttons\n * in that visual order — no button is styled as more prominent than another.\n * A tenant can override `title`/`description` text (REB-642 §7.1 recommends\n * v1 ship with NO tenant-editable copy at all, but the prop exists so a later\n * slice can add it without touching this component's contract); the buttons,\n * their order, and the category taxonomy are fixed.\n */\n\nexport interface CookieBannerProps {\n /** Overrides merged onto `DEFAULT_CONSENT_BANNER_TEXTS`. */\n texts?: Partial<ConsentBannerTexts>\n /** Where \"Política de cookies\" links to. */\n policyHref?: string\n}\n\nfunction resolveTexts(overrides?: Partial<ConsentBannerTexts>): ConsentBannerTexts {\n return { ...DEFAULT_CONSENT_BANNER_TEXTS, ...overrides }\n}\n\nexport function CookieBanner({ texts: textsOverride, policyHref = '/cookies' }: CookieBannerProps) {\n const { status, acceptAll, rejectAll, openPreferences } = useConsent()\n const texts = resolveTexts(textsOverride)\n\n return (\n <>\n {status === 'unknown' && (\n <div\n role='region'\n aria-label={texts.title}\n className='fixed inset-x-0 bottom-0 z-50 border-t border-border bg-background/95 px-4 py-4 backdrop-blur supports-[backdrop-filter]:bg-background/80'\n data-slot='cookie-banner'\n >\n <div className='mx-auto flex max-w-4xl flex-col gap-3 sm:flex-row sm:items-center sm:justify-between'>\n <div className='text-sm'>\n <p className='font-semibold'>{texts.title}</p>\n <p className='text-muted-foreground'>\n {texts.description}{' '}\n <a href={policyHref} className='underline underline-offset-2'>\n {texts.policyLinkLabel}\n </a>\n </p>\n </div>\n {/* Three equal-weight buttons, fixed order: reject, configure, accept.\n Same variant/size for all three — rejecting must cost exactly what\n accepting costs (AEPD first-layer requirement, REB-642). */}\n <div className='flex shrink-0 flex-wrap gap-2'>\n <Button type='button' variant='outline' size='sm' onClick={rejectAll}>\n {texts.rejectLabel}\n </Button>\n <Button type='button' variant='outline' size='sm' onClick={openPreferences}>\n {texts.manageLabel}\n </Button>\n <Button type='button' variant='outline' size='sm' onClick={acceptAll}>\n {texts.acceptLabel}\n </Button>\n </div>\n </div>\n </div>\n )}\n <ConsentPreferencesDialog texts={texts} />\n </>\n )\n}\n\nexport interface CookiePreferencesTriggerProps {\n children?: ReactNode\n className?: string\n texts?: Partial<ConsentBannerTexts>\n}\n\n/**\n * A persistent \"configure cookies\" entry point, safe to place anywhere in the\n * tree (typically a footer link) once `<CookieBanner>` is mounted at the root.\n * Opens the same preferences dialog `<CookieBanner>` owns.\n */\nexport function CookiePreferencesTrigger({\n children,\n className,\n texts: textsOverride,\n}: CookiePreferencesTriggerProps) {\n const { openPreferences } = useConsent()\n const texts = resolveTexts(textsOverride)\n\n return (\n <button type='button' onClick={openPreferences} className={className} data-slot='cookie-preferences-trigger'>\n {children ?? `${texts.manageLabel} cookies`}\n </button>\n )\n}\n\ninterface ConsentPreferencesDialogProps {\n texts: ConsentBannerTexts\n}\n\n/**\n * The second layer — a per-category preferences panel. `necessary` is\n * rendered as an always-checked, disabled checkbox for transparency (a\n * visitor can SEE that necessary cookies exist and why) without offering a\n * toggle that has no real effect, which would be dishonest UI.\n */\nfunction ConsentPreferencesDialog({ texts }: ConsentPreferencesDialogProps) {\n const { categories, preferencesOpen, closePreferences, savePreferences, restoreLastFocus } = useConsent()\n const [draft, setDraft] = useState<ConsentCategoryState>(categories)\n\n // Re-seed the draft from the last-saved categories every time the dialog\n // opens, so stale unsaved toggles from a previous open are never carried\n // into the next one.\n useEffect(() => {\n if (preferencesOpen) setDraft(categories)\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [preferencesOpen])\n\n return (\n <Dialog.Root\n open={preferencesOpen}\n onOpenChange={(open) => {\n // Radix only calls this for state changes IT initiates (Escape,\n // overlay click, `<Dialog.Close>`) — opening is entirely driven by\n // `useConsent().openPreferences()` from outside, which is also where\n // focus is captured (see `../consent/context.tsx`). So this handler\n // only ever needs to handle the closing transition.\n if (!open) closePreferences()\n }}\n >\n <Dialog.Portal>\n <Dialog.Overlay className='fixed inset-0 z-50 bg-black/50' />\n <Dialog.Content\n className='fixed left-1/2 top-1/2 z-50 w-full max-w-md -translate-x-1/2 -translate-y-1/2 rounded-lg border border-border bg-background p-6 shadow-lg'\n data-slot='cookie-preferences-dialog'\n onCloseAutoFocus={(event) => {\n event.preventDefault()\n restoreLastFocus()\n }}\n >\n <Dialog.Title className='text-base font-semibold'>{texts.preferencesTitle}</Dialog.Title>\n <Dialog.Description className='mt-1 text-sm text-muted-foreground'>\n {texts.preferencesDescription}\n </Dialog.Description>\n\n <div className='mt-4 flex flex-col gap-3'>\n <CategoryRow\n id='consent-category-necessary'\n label={texts.necessaryLabel}\n description={texts.necessaryDescription}\n checked\n disabled\n onChange={() => undefined}\n />\n {CONSENT_CATEGORIES.map((category) => (\n <CategoryRow\n key={category}\n id={`consent-category-${category}`}\n label={texts.categoryLabels[category]}\n description={texts.categoryDescriptions[category]}\n checked={draft[category]}\n disabled={false}\n onChange={(checked) => setDraft((prev) => ({ ...prev, [category]: checked }))}\n />\n ))}\n </div>\n\n <div className='mt-6 flex justify-end'>\n <Button type='button' size='sm' onClick={() => savePreferences(draft)}>\n {texts.saveLabel}\n </Button>\n </div>\n </Dialog.Content>\n </Dialog.Portal>\n </Dialog.Root>\n )\n}\n\ninterface CategoryRowProps {\n id: string\n label: string\n description: string\n checked: boolean\n disabled: boolean\n onChange: (checked: boolean) => void\n}\n\nfunction CategoryRow({ id, label, description, checked, disabled, onChange }: CategoryRowProps) {\n return (\n <div className={cn('flex items-start gap-3', disabled && 'opacity-70')}>\n <input\n id={id}\n type='checkbox'\n checked={checked}\n disabled={disabled}\n onChange={(event) => onChange(event.target.checked)}\n className='mt-1 size-4'\n />\n <label htmlFor={id} className='flex flex-col text-sm'>\n <span className='font-medium'>{label}</span>\n <span className='text-muted-foreground'>{description}</span>\n </label>\n </div>\n )\n}\n","import type { ConsentCategory } from '../consent-shared'\n\n/**\n * Copy for `<CookieBanner>`. SDK-owned defaults in Spanish (every Reblu SITE\n * renders `<html lang='es'>` today).\n *\n * REB-642 §7.1 recommends tenant-editable copy be limited to a TITLE/BODY\n * override for v1 — the per-category descriptions, the \"necessary\" copy and\n * every button label stay fixed, because the copy is a compliance surface: a\n * tenant writing \"we use cookies — OK\" with no reject option is the exact\n * misconfiguration this design exists to prevent. This type is intentionally\n * a `Partial<ConsentBannerTexts>` at the call site (see `CookieBanner`), so a\n * later tenant-configuration slice can override `title`/`description` without\n * touching this component's contract.\n */\nexport interface ConsentBannerTexts {\n title: string\n description: string\n acceptLabel: string\n rejectLabel: string\n manageLabel: string\n policyLinkLabel: string\n preferencesTitle: string\n preferencesDescription: string\n saveLabel: string\n necessaryLabel: string\n necessaryDescription: string\n categoryLabels: Record<ConsentCategory, string>\n categoryDescriptions: Record<ConsentCategory, string>\n}\n\nexport const DEFAULT_CONSENT_BANNER_TEXTS: ConsentBannerTexts = {\n title: 'Utilizamos cookies',\n description:\n 'Usamos cookies estrictamente necesarias para el funcionamiento del sitio. Con tu permiso, también usaríamos cookies de analítica y marketing para mejorar tu experiencia.',\n acceptLabel: 'Aceptar todas',\n rejectLabel: 'Rechazar todas',\n manageLabel: 'Configurar',\n policyLinkLabel: 'Política de cookies',\n preferencesTitle: 'Preferencias de cookies',\n preferencesDescription:\n 'Elige qué categorías de cookies opcionales permites. Las cookies necesarias no se pueden desactivar.',\n saveLabel: 'Guardar preferencias',\n necessaryLabel: 'Necesarias',\n necessaryDescription:\n 'Imprescindibles para el funcionamiento del sitio, como la seguridad del formulario de contacto. Siempre activas.',\n categoryLabels: {\n analytics: 'Analítica',\n marketing: 'Marketing',\n preferences: 'Preferencias',\n },\n categoryDescriptions: {\n analytics: 'Nos ayudan a entender cómo se usa el sitio (por ejemplo, Google Analytics o Microsoft Clarity).',\n marketing: 'Permiten mostrar contenido relevante de terceros (por ejemplo, Meta Pixel).',\n preferences: 'Recuerdan tus preferencias de navegación entre visitas.',\n },\n}\n"],"mappings":"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","names":["import_jsx_runtime","import_jsx_runtime","import_jsx_runtime","Script","import_jsx_runtime","import_react","import_jsx_runtime"]}
|