@solvenza/cookies-compliance 1.2.1 → 1.5.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/README.md +960 -36
  2. package/bin/audit-cli.js +134 -13
  3. package/dist/audit.cjs +811 -0
  4. package/dist/audit.cjs.map +1 -0
  5. package/dist/audit.d.cts +60 -0
  6. package/dist/audit.d.ts +60 -0
  7. package/dist/audit.js +380 -0
  8. package/dist/audit.js.map +1 -0
  9. package/dist/chunk-C5QPXHTH.js +3557 -0
  10. package/dist/chunk-C5QPXHTH.js.map +1 -0
  11. package/dist/chunk-OHOHIAK6.js +442 -0
  12. package/dist/chunk-OHOHIAK6.js.map +1 -0
  13. package/dist/chunk-Q3FZFXQE.js +89 -0
  14. package/dist/chunk-UQU54ANZ.js +225 -0
  15. package/dist/chunk-UQU54ANZ.js.map +1 -0
  16. package/dist/chunk-W5MG56VI.js +269 -0
  17. package/dist/chunk-W5MG56VI.js.map +1 -0
  18. package/dist/consent.esm.cjs +2195 -84
  19. package/dist/consent.esm.cjs.map +1 -1
  20. package/dist/consent.esm.d.cts +173 -6
  21. package/dist/consent.esm.d.ts +173 -6
  22. package/dist/consent.esm.js +57 -23
  23. package/dist/consent.esm.js.map +1 -1
  24. package/dist/consent.min.js +437 -55
  25. package/dist/consent.min.js.map +1 -1
  26. package/dist/presets.cjs +495 -0
  27. package/dist/presets.cjs.map +1 -0
  28. package/dist/presets.d.cts +51 -0
  29. package/dist/presets.d.ts +51 -0
  30. package/dist/presets.js +16 -0
  31. package/dist/presets.js.map +1 -0
  32. package/dist/types--kyI7OOR.d.cts +314 -0
  33. package/dist/types--kyI7OOR.d.ts +314 -0
  34. package/dist/wrappers/angular.cjs +2341 -65
  35. package/dist/wrappers/angular.cjs.map +1 -1
  36. package/dist/wrappers/angular.d.cts +85 -5
  37. package/dist/wrappers/angular.d.ts +85 -5
  38. package/dist/wrappers/angular.js +217 -3
  39. package/dist/wrappers/angular.js.map +1 -1
  40. package/dist/wrappers/next.cjs +2353 -103
  41. package/dist/wrappers/next.cjs.map +1 -1
  42. package/dist/wrappers/next.d.cts +25 -1
  43. package/dist/wrappers/next.d.ts +25 -1
  44. package/dist/wrappers/next.js +30 -5
  45. package/dist/wrappers/next.js.map +1 -1
  46. package/dist/wrappers/nuxt.cjs +4171 -0
  47. package/dist/wrappers/nuxt.cjs.map +1 -0
  48. package/dist/wrappers/nuxt.d.cts +33 -0
  49. package/dist/wrappers/nuxt.d.ts +33 -0
  50. package/dist/wrappers/nuxt.js +51 -0
  51. package/dist/wrappers/nuxt.js.map +1 -0
  52. package/dist/wrappers/react.cjs +2336 -141
  53. package/dist/wrappers/react.cjs.map +1 -1
  54. package/dist/wrappers/react.d.cts +87 -1
  55. package/dist/wrappers/react.d.ts +87 -1
  56. package/dist/wrappers/react.js +21 -51
  57. package/dist/wrappers/react.js.map +1 -1
  58. package/dist/wrappers/vue.cjs +4163 -0
  59. package/dist/wrappers/vue.cjs.map +1 -0
  60. package/dist/wrappers/vue.d.cts +144 -0
  61. package/dist/wrappers/vue.d.ts +144 -0
  62. package/dist/wrappers/vue.js +28 -0
  63. package/dist/wrappers/vue.js.map +1 -0
  64. package/dist/wrappers/wordpress.js +1 -1
  65. package/package.json +26 -5
  66. package/schema.json +143 -4
  67. package/dist/chunk-DDAAVRWG.js +0 -25
  68. package/dist/chunk-FBRP4MSA.js +0 -1907
  69. package/dist/chunk-FBRP4MSA.js.map +0 -1
  70. package/dist/consent-engine-DOMyEKfk.d.cts +0 -193
  71. package/dist/consent-engine-DOMyEKfk.d.ts +0 -193
  72. /package/dist/{chunk-DDAAVRWG.js.map → chunk-Q3FZFXQE.js.map} +0 -0
package/README.md CHANGED
@@ -30,7 +30,10 @@ npm install @solvenza/cookies-compliance
30
30
  ### CDN (1 sola línea)
31
31
 
32
32
  ```html
33
- <script src="https://cdn.jsdelivr.net/npm/@solvenza/cookies-compliance@1/dist/consent.min.js" data-config="/consent.json"></script>
33
+ <script
34
+ src="https://cdn.jsdelivr.net/npm/@solvenza/cookies-compliance@1/dist/consent.min.js"
35
+ data-config="/consent.json"
36
+ ></script>
34
37
  ```
35
38
 
36
39
  ---
@@ -56,6 +59,14 @@ Ejemplo de `consent.json`:
56
59
  "privacyUrl": "/politica-privacidad",
57
60
  "cookiesUrl": "/politica-cookies"
58
61
  },
62
+ "ui": {
63
+ "floatingBadge": {
64
+ "enabled": true,
65
+ "position": "bottom-left",
66
+ "icon": "cookie",
67
+ "label": "Cookies"
68
+ }
69
+ },
59
70
  "categories": {
60
71
  "necessary": {
61
72
  "required": true,
@@ -74,74 +85,584 @@ Ejemplo de `consent.json`:
74
85
  }
75
86
  },
76
87
  "services": {
77
- "ga4": { "category": "analytics", "label": "Google Analytics 4", "provider": "Google" },
78
- "youtube": { "category": "marketing", "label": "YouTube Embed", "provider": "Google" }
88
+ "ga4": {
89
+ "category": "analytics",
90
+ "label": "Google Analytics 4",
91
+ "provider": "Google"
92
+ },
93
+ "youtube": {
94
+ "category": "marketing",
95
+ "label": "YouTube Embed",
96
+ "provider": "Google"
97
+ }
98
+ }
99
+ }
100
+ ```
101
+
102
+ ---
103
+
104
+ ## Botón Flotante de Revocación Permanente (AEPD / RGPD)
105
+
106
+ De acuerdo con la **Guía de la AEPD sobre el uso de cookies**, el usuario debe tener a su disposición un mecanismo permanente y fácilmente accesible para modificar sus preferencias o **declinar/revocar el consentimiento en cualquier momento**.
107
+
108
+ Puedes activarlo directamente en `consent.json`:
109
+
110
+ ```json
111
+ {
112
+ "ui": {
113
+ "floatingBadge": {
114
+ "enabled": true,
115
+ "position": "bottom-left",
116
+ "icon": "cookie",
117
+ "label": "Cookies",
118
+ "showLabel": false,
119
+ "visibility": "after-consent"
120
+ }
121
+ }
122
+ }
123
+ ```
124
+
125
+ O de forma abreviada:
126
+
127
+ ```json
128
+ {
129
+ "ui": {
130
+ "floatingBadge": true
131
+ }
132
+ }
133
+ ```
134
+
135
+ ### Opciones de configuración:
136
+
137
+ | Parámetro | Tipo | Por defecto | Descripción |
138
+ | ------------ | -------------------------------------------------------------------- | --------------------------------- | --------------------------------------------------------------- |
139
+ | `enabled` | `boolean` | `true` (si se declara objeto) | Activa o desactiva el widget flotante |
140
+ | `position` | `"bottom-left"` \| `"bottom-right"` \| `"top-left"` \| `"top-right"` | `"bottom-left"` | Esquina de anclaje en pantalla |
141
+ | `icon` | `"cookie"` \| `"shield"` \| `"settings"` | `"cookie"` | Icono SVG estilizado |
142
+ | `label` | `string` | `"Cookies"` | Texto accesible y etiqueta en píldora |
143
+ | `tooltip` | `string` | `"Configurar o declinar cookies"` | Texto descriptivo emergente al pasar el cursor (hover) |
144
+ | `showLabel` | `boolean` | `false` | Muestra la etiqueta de texto junto al icono en forma de píldora |
145
+ | `visibility` | `"after-consent"` \| `"always"` | `"after-consent"` | Muestra el botón tras cerrar el banner o en todo momento |
146
+
147
+ > **Ciclo de vida automático con el modal**: Para garantizar una experiencia de usuario impecable y sin elementos superpuestos, el botón flotante se oculta mientras el modal de preferencias está abierto. **En cuanto el modal se cierra** (mediante el botón 'X', clic en el backdrop, tecla Escape o al guardar la selección), **el botón flotante reaparece de inmediato**.
148
+
149
+ ### API Programática:
150
+
151
+ ```ts
152
+ // Mostrar u ocultar manualmente
153
+ Consent.showFloatingBadge();
154
+ Consent.hideFloatingBadge();
155
+
156
+ // Eventos
157
+ Consent.on("floating-badge:shown", () => console.log("Badge visible"));
158
+ Consent.on("floating-badge:hidden", () => console.log("Badge oculto"));
159
+
160
+ // Eventos nativos DOM (Zero-code)
161
+ document.dispatchEvent(new Event("solvenza:badge:show"));
162
+ document.dispatchEvent(new Event("solvenza:badge:hide"));
163
+ ```
164
+
165
+ ---
166
+
167
+ ## Internacionalización y Sincronización Multilingüe (i18n)
168
+
169
+ El sistema de internacionalización (`I18nEngine`) está diseñado bajo el principio de **cero boilerplate** y **sincronización transparente con la aplicación padre**:
170
+
171
+ > [!NOTE]
172
+ > **Sin selectores invasivos en el banner**: La librería **no** incluye selectores ni desplegables de idioma dentro del banner ni del modal de preferencias. El idioma de navegación es responsabilidad exclusiva de la aplicación anfitriona (padre). El banner y el modal se adaptan y sincronizan automáticamente al idioma de la web en tiempo real.
173
+
174
+ ### Mecanismos de Sincronización Automática (Zero Boilerplate)
175
+
176
+ 1. **Observador Reactivo del HTML (`<html lang="...">`)**:
177
+ - La librería observa dinámicamente mediante `MutationObserver` el atributo `lang` en `document.documentElement`.
178
+ - Cuando tu aplicación padre cambia de idioma (ej. `document.documentElement.lang = "en"`), el banner, modal y botón flotante se traducen y renderizan de inmediato **sin requerir reinicialización ni código de pegamento**.
179
+ 2. **Detección Automática por Ruta y Query Params**:
180
+ - Detecta prefijos en la URL (como `/en/...` o `/ca/...`) o parámetros de consulta (`?lang=en`, `?locale=en`).
181
+ 3. **Sincronización Directa de Estado (`Consent.syncLocale`)**:
182
+ - Función declarativa para enlazar el estado de traducción de tu aplicación (`react-i18next`, `next-intl`, `@ngx-translate`, etc.).
183
+
184
+ ### Idiomas Integrados por Defecto
185
+
186
+ El SDK incluye diccionarios oficiales para la normativa española y europea sin necesidad de configurar textos:
187
+
188
+ - **Español (`es`)** [Por defecto]
189
+ - **Inglés (`en`)**
190
+ - **Catalán (`ca`)**
191
+ - **Euskera (`eu`)**
192
+ - **Gallego (`gl`)**
193
+
194
+ ### Configuración Declarativa en `consent.json`:
195
+
196
+ ```json
197
+ {
198
+ "locale": {
199
+ "default": "es",
200
+ "syncHtmlLang": true,
201
+ "syncUrl": true,
202
+ "autoDetect": true,
203
+ "supported": ["es", "en", "ca", "eu", "gl"]
204
+ },
205
+ "translations": {
206
+ "en": {
207
+ "policy": {
208
+ "privacyUrl": "/en/privacy-policy",
209
+ "cookiesUrl": "/en/cookie-policy"
210
+ },
211
+ "ui": {
212
+ "banner": {
213
+ "title": "Your privacy, your choice",
214
+ "accept": "Accept all",
215
+ "reject": "Reject all",
216
+ "configure": "Configure"
217
+ },
218
+ "preferences": {
219
+ "title": "Privacy Preferences",
220
+ "save": "Save preferences"
221
+ },
222
+ "floatingBadge": {
223
+ "label": "Cookies",
224
+ "tooltip": "Configure or decline cookies"
225
+ }
226
+ },
227
+ "categories": {
228
+ "analytics": {
229
+ "label": "Usage Analytics",
230
+ "description": "Allows aggregated performance measurement."
231
+ }
232
+ }
233
+ }
234
+ }
235
+ }
236
+ ```
237
+
238
+ ### Sincronización Programática:
239
+
240
+ ```ts
241
+ // 1. Sincronizar el idioma desde la app padre
242
+ Consent.syncLocale("en");
243
+
244
+ // 2. Obtener idioma activo
245
+ console.log(Consent.getLocale()); // "en"
246
+
247
+ // 3. Escuchar cambios de idioma
248
+ Consent.on("locale:changed", ({ locale, previousLocale }) => {
249
+ console.log(`Idioma cambiado de ${previousLocale} a ${locale}`);
250
+ });
251
+
252
+ // 4. Conmutación mediante CustomEvent del DOM (Zero-code / Microfrontends)
253
+ document.dispatchEvent(
254
+ new CustomEvent("solvenza:locale", { detail: { locale: "en" } }),
255
+ );
256
+ ```
257
+
258
+ ### Configuración con `ConsentConfigBuilder` (TypeScript):
259
+
260
+ ```ts
261
+ const config = new ConsentConfigBuilder("1.0.0")
262
+ .setLocale("es", true, ["es", "en", "ca"])
263
+ .addTranslation("en", {
264
+ ui: {
265
+ banner: { title: "Your privacy, your choice" },
266
+ floatingBadge: { tooltip: "Cookie Settings" },
267
+ },
268
+ categories: {
269
+ analytics: { label: "Analytics", description: "Aggregated telemetry." },
270
+ },
271
+ })
272
+ .build();
273
+ ```
274
+
275
+ ---
276
+
277
+ ## Purga Automática de Web Storage (localStorage, sessionStorage y Cookies)
278
+
279
+ A partir de la versión **1.5.3**, cuando el usuario desmarca una categoría en el panel de preferencias o revoca su elección (`Consent.withdraw()`), el SDK no solo elimina las cookies en `document.cookie`, sino que también **purga de forma reactiva las claves guardadas en `localStorage` y `sessionStorage`** por bibliotecas de analítica o marketing (Google Analytics 4, PostHog, Mixpanel, Hotjar, etc.).
280
+
281
+ ### Soporte de Comodines (Glob Wildcards)
282
+
283
+ Puedes declarar patrones exactos o con comodín `*`:
284
+
285
+ - `_ga*`: coincide con `_ga`, `_gid`, `_ga_G123456`, etc.
286
+ - `ph_*_posthog`: coincide con identificadores dinámicos de PostHog.
287
+ - `*session*`: coincide con cualquier clave que contenga `session`.
288
+
289
+ ### Configuración en `consent.json`:
290
+
291
+ ```json
292
+ {
293
+ "categories": {
294
+ "analytics": {
295
+ "label": "Analítica",
296
+ "description": "Medición agregada del tráfico.",
297
+ "storageKeys": ["_ga*", "_gid*"]
298
+ }
299
+ },
300
+ "services": {
301
+ "ga4": {
302
+ "category": "analytics",
303
+ "label": "Google Analytics 4",
304
+ "cookies": [{ "name": "_ga" }, { "name": "_ga_*" }, { "name": "_gid" }],
305
+ "storageKeys": ["_ga*", "_gid*"]
306
+ },
307
+ "posthog": {
308
+ "category": "analytics",
309
+ "label": "PostHog",
310
+ "localStorage": ["ph_*_posthog"],
311
+ "sessionStorage": ["ph_*_posthog"]
312
+ }
79
313
  }
80
314
  }
81
315
  ```
82
316
 
317
+ ### Opciones de purga disponibles:
318
+
319
+ | Campo | Nivel | Descripción |
320
+ | ---------------- | -------------------- | --------------------------------------------------------------------------- |
321
+ | `storageKeys` | Categoría o Servicio | Claves o patrones a purgar tanto de `localStorage` como de `sessionStorage` |
322
+ | `localStorage` | Categoría o Servicio | Claves o patrones a purgar exclusivamente de `localStorage` |
323
+ | `sessionStorage` | Categoría o Servicio | Claves o patrones a purgar exclusivamente de `sessionStorage` |
324
+ | `cookies[].name` | Servicio | Nombres de cookies (soporta comodines como `_ga_*`) |
325
+
326
+ ### API Programática:
327
+
328
+ ```ts
329
+ import { Consent, StorageCleaner } from "@solvenza/cookies-compliance";
330
+
331
+ // 1. Purgar manualmente el almacenamiento de una categoría
332
+ const report = Consent.purgeCategory("analytics");
333
+ console.log(report.purgedCookies); // ["_ga", "_ga_G123456"]
334
+ console.log(report.purgedLocalStorage); // ["_ga", "ph_client_posthog"]
335
+ console.log(report.purgedSessionStorage); // ["temp_analytics_session"]
336
+
337
+ // 2. Escuchar eventos de purga
338
+ Consent.on("storage:purged", ({ category, report }) => {
339
+ console.log(`Almacenamiento purgado para la categoría ${category}:`, report);
340
+ });
341
+
342
+ // 3. Utilidad independiente
343
+ StorageCleaner.purgeLocalStorage(["_ga*", "temp_*"]);
344
+ ```
345
+
83
346
  ---
84
347
 
85
- ## Uso por Frameworks
348
+ ## Presets de Servicios Comunes (GA4, Meta, Hotjar, etc.)
349
+
350
+ El SDK incluye un catálogo oficial de más de 20 presets predefinidos con todos los metadatos necesarios (proveedor legal, categorías por defecto, patrones de cookies con duración y propósito, claves de Web Storage y políticas oficiales):
351
+
352
+ ### Catálogo de Servicios Soportados:
353
+
354
+ | Categoría | Presets Disponibles |
355
+ | -------------------------------- | ------------------------------------------------------------------------------------------------------------ |
356
+ | **Analítica** | `ga4`, `google_analytics`, `posthog`, `hotjar`, `clarity`, `matomo`, `plausible` |
357
+ | **Marketing & Ads** | `meta_pixel`, `facebook_pixel`, `google_ads`, `tiktok_pixel`, `linkedin_insight`, `twitter_pixel`, `hubspot` |
358
+ | **Media Embebida** | `youtube`, `vimeo`, `spotify` |
359
+ | **Chat & Soporte** | `intercom`, `crisp` |
360
+ | **Técnicas / Seguridad / Pagos** | `gtm`, `google_recaptcha`, `cloudflare`, `stripe`, `paypal` |
361
+
362
+ ### Uso Declarativo en `consent.json`:
363
+
364
+ Basta con especificar `"preset": "<nombre>"` para que el SDK hidrate automáticamente cookies, almacenamiento y proveedor:
365
+
366
+ ```json
367
+ {
368
+ "schemaVersion": 1,
369
+ "policyVersion": "2026-09-30",
370
+ "categories": {
371
+ "necessary": {
372
+ "required": true,
373
+ "label": "Necesarias",
374
+ "description": "Cookies técnicas de seguridad y pago."
375
+ },
376
+ "analytics": {
377
+ "required": false,
378
+ "label": "Analítica",
379
+ "description": "Medición del tráfico y uso."
380
+ },
381
+ "marketing": {
382
+ "required": false,
383
+ "label": "Marketing",
384
+ "description": "Publicidad y contenido embebido."
385
+ }
386
+ },
387
+ "services": {
388
+ "ga4": { "preset": "ga4" },
389
+ "facebook": { "preset": "meta_pixel" },
390
+ "hotjar": { "preset": "hotjar" },
391
+ "youtube": { "preset": "youtube" },
392
+ "stripe": { "preset": "stripe" }
393
+ }
394
+ }
395
+ ```
396
+
397
+ ### Uso Programático en TypeScript / JavaScript:
398
+
399
+ ```ts
400
+ import {
401
+ Consent,
402
+ getPreset,
403
+ defineServices,
404
+ SERVICE_PRESETS,
405
+ } from "@solvenza/cookies-compliance";
406
+ // o importación directa:
407
+ // import { getPreset, defineServices } from "@solvenza/cookies-compliance/presets";
408
+
409
+ // 1. Obtener preset individual con personalizaciones
410
+ const customGA4 = getPreset("ga4", {
411
+ label: "Google Analytics (Región UE)",
412
+ provider: "Google Ireland Ltd.",
413
+ });
414
+
415
+ // 2. Definir múltiples servicios de forma concisa y tipada
416
+ const services = defineServices({
417
+ ga4: "ga4",
418
+ meta: "meta_pixel",
419
+ hotjar: { preset: "hotjar", label: "Mapas de Calor UX" },
420
+ customApi: { category: "necessary", label: "API Interna de Autenticación" },
421
+ });
422
+ ```
423
+
424
+ ---
425
+
426
+ ## Soporte Global Privacy Control (GPC) & Do Not Track (DNT)
427
+
428
+ El SDK detecta automáticamente la señal del estándar W3C **Global Privacy Control (`navigator.globalPrivacyControl === true`)** y **Do Not Track (`navigator.doNotTrack === "1"`)** enviada por navegadores como Firefox, Brave, DuckDuckGo o extensiones de privacidad.
429
+
430
+ ### Comportamiento y Cumplimiento:
431
+
432
+ 1. **Aplicación automática**: Si el usuario no ha expresado un consentimiento explícito previo y tiene GPC activado, el motor desactiva automáticamente las cookies y almacenamiento no esenciales.
433
+ 2. **Generación de Comprobante Legal**: Se emite un recibo firmado con `source: "gpc"` y `gpc: true`.
434
+ 3. **Purga Automática**: Se limpian de inmediato las cookies y Web Storage (`localStorage` / `sessionStorage`) de terceros.
435
+ 4. **Sincronización con Google Consent Mode**: Se actualiza el modo de consentimiento de Google como `denied` para analítica y publicidad.
436
+
437
+ ### Configuración en `consent.json`:
438
+
439
+ ```json
440
+ {
441
+ "schemaVersion": 1,
442
+ "policyVersion": "2026-09-30",
443
+ "gpc": {
444
+ "enabled": true,
445
+ "respectSignal": true,
446
+ "mode": "auto-reject",
447
+ "categories": ["analytics", "marketing"]
448
+ },
449
+ "categories": {
450
+ "necessary": {
451
+ "required": true,
452
+ "label": "Necesarias",
453
+ "description": "Técnicas"
454
+ },
455
+ "analytics": {
456
+ "required": false,
457
+ "label": "Analítica",
458
+ "description": "Métricas"
459
+ },
460
+ "marketing": {
461
+ "required": false,
462
+ "label": "Marketing",
463
+ "description": "Publicidad"
464
+ }
465
+ }
466
+ }
467
+ ```
468
+
469
+ ### API Programática:
470
+
471
+ ```ts
472
+ import { Consent } from "@solvenza/cookies-compliance";
473
+
474
+ // Consultar si la señal GPC está activa en el navegador
475
+ if (Consent.isGpcActive()) {
476
+ console.log("Navegador con señal GPC / DNT activa.");
477
+ }
478
+
479
+ // Escuchar evento de detección de señal
480
+ Consent.on("gpc:detected", ({ signal, autoApplied, choices }) => {
481
+ console.log("Señal de privacidad procesada:", {
482
+ signal,
483
+ autoApplied,
484
+ choices,
485
+ });
486
+ });
487
+ ```
488
+
489
+ En React / Next:
490
+
491
+ ```tsx
492
+ import { useGpc } from "@solvenza/cookies-compliance/react";
493
+
494
+ export function PrivacyNotice() {
495
+ const isGpc = useGpc();
496
+ return isGpc ? (
497
+ <p>Señal de privacidad Global Privacy Control detectada.</p>
498
+ ) : null;
499
+ }
500
+ ```
501
+
502
+ En Vue 3 / Nuxt 3:
503
+
504
+ ```vue
505
+ <script setup>
506
+ import { useGpc } from "@solvenza/cookies-compliance/vue";
507
+ const isGpc = useGpc();
508
+ </script>
509
+ ```
510
+
511
+ ---
86
512
 
87
513
  ### Vanilla HTML5
88
514
 
89
- Para bloquear scripts o elementos dinámicos, usa los atributos `data-consent` y `data-src`:
515
+ Incrusta el script compilado y tu configuración. El botón flotante de revocación se activará automáticamente según lo definido en `consent.json`:
90
516
 
91
517
  ```html
92
- <!-- Script analítico bloqueado previamente -->
93
- <script
94
- type="text/plain"
95
- data-consent="analytics"
96
- data-service="ga4"
97
- data-src="https://www.googletagmanager.com/gtag/js?id=G-DEMO123">
98
- </script>
518
+ <!-- 1. Carga automática con script compilado y configuración JSON -->
519
+ <script src="./consent.min.js" data-config="./consent.json"></script>
520
+
521
+ <!-- 2. Scripts y recursos bloqueados previamente -->
522
+ <script
523
+ type="text/plain"
524
+ data-consent="analytics"
525
+ data-service="ga4"
526
+ data-src="https://www.googletagmanager.com/gtag/js?id=G-DEMO123"
527
+ ></script>
99
528
 
100
- <!-- IFrame de YouTube bloqueado previamente -->
101
- <iframe
102
- data-consent="marketing"
103
- data-service="youtube"
104
- data-src="https://www.youtube.com/embed/dQw4w9WgXcQ"
105
- width="560" height="315">
529
+ <iframe
530
+ data-consent="marketing"
531
+ data-service="youtube"
532
+ data-src="https://www.youtube.com/embed/dQw4w9WgXcQ"
533
+ width="560"
534
+ height="315"
535
+ >
106
536
  </iframe>
107
537
 
108
- <!-- Botón de revocación de consentimiento en footer -->
109
- <button type="button" data-consent-open>Configurar cookies</button>
538
+ <!-- 3. Botón de revocación opcional en footer (adicional al badge flotante permanente) -->
539
+ <button type="button" data-consent-open>Gestionar cookies</button>
110
540
  ```
111
541
 
112
542
  ### React 18+ / Vite
113
543
 
544
+ Puedes sincronizar el idioma de la aplicación (ej. procedente de `react-i18next` o de tu estado) sin boilerplate utilizando el hook `useSyncConsentLocale`:
545
+
114
546
  ```tsx
115
- import { useEffect } from "react";
116
- import { Consent } from "@solvenza/cookies-compliance";
117
- import { useConsent } from "@solvenza/cookies-compliance/react";
547
+ import { useState, useEffect } from "react";
548
+ import { Consent, ConsentConfigBuilder } from "@solvenza/cookies-compliance";
549
+ import {
550
+ useConsent,
551
+ useSyncConsentLocale,
552
+ ConsentGate,
553
+ } from "@solvenza/cookies-compliance/react";
118
554
 
119
555
  export function App() {
556
+ const [appLang, setAppLang] = useState("es");
120
557
  const isAnalyticsAllowed = useConsent("analytics");
121
558
 
559
+ // Sincronización automática de 1 línea con el estado de tu app
560
+ useSyncConsentLocale(appLang);
561
+
122
562
  useEffect(() => {
123
- void Consent.init("/consent.json");
563
+ // Inicialización declarativa con FloatingBadge activado
564
+ const config = new ConsentConfigBuilder("2026-08-23")
565
+ .setPolicyUrls("/politica-privacidad", "/politica-cookies")
566
+ .setLocale("es", true, ["es", "en", "ca"])
567
+ .setFloatingBadge({
568
+ enabled: true,
569
+ position: "bottom-left",
570
+ icon: "cookie",
571
+ label: "Cookies",
572
+ })
573
+ .addCategory("analytics", {
574
+ required: false,
575
+ label: "Analítica de uso",
576
+ description: "Permite medir de forma agregada el uso de la web.",
577
+ })
578
+ .addCategory("marketing", {
579
+ required: false,
580
+ label: "Marketing y Vídeo",
581
+ description: "Permite reproducir vídeos y contenido interactivo.",
582
+ })
583
+ .build();
584
+
585
+ void Consent.init(config);
124
586
  }, []);
125
587
 
126
588
  return (
127
589
  <div>
590
+ <header>
591
+ <button onClick={() => setAppLang("es")}>ES</button>
592
+ <button onClick={() => setAppLang("en")}>EN</button>
593
+ </header>
128
594
  <p>Analítica: {isAnalyticsAllowed ? "Activa" : "Bloqueada"}</p>
129
- <button onClick={() => Consent.openPreferences()}>Ajustes de Cookies</button>
595
+
596
+ {/* Componente Declarativo ConsentGate */}
597
+ <ConsentGate
598
+ category="marketing"
599
+ fallback={
600
+ <div className="cookie-blocked-placeholder">
601
+ <p>
602
+ El reproductor de vídeo requiere permiso de cookies de marketing.
603
+ </p>
604
+ <button onClick={() => Consent.openPreferences()}>
605
+ Ajustes de Cookies
606
+ </button>
607
+ </div>
608
+ }
609
+ >
610
+ <iframe
611
+ src="https://www.youtube.com/embed/dQw4w9WgXcQ"
612
+ width="560"
613
+ height="315"
614
+ />
615
+ </ConsentGate>
130
616
  </div>
131
617
  );
132
618
  }
133
619
  ```
134
620
 
135
- ### Next.js (App Router)
621
+ ### Next.js (App Router & Pages)
622
+
623
+ Importa hooks y el componente `<ConsentGate>` directamente desde `@solvenza/cookies-compliance/next`:
136
624
 
137
625
  ```tsx
138
- // app/layout.tsx
626
+ // app/components/VideoPlayer.tsx
627
+ "use client";
628
+ import { ConsentGate, useConsent } from "@solvenza/cookies-compliance/next";
629
+
630
+ export function VideoPlayer() {
631
+ return (
632
+ <ConsentGate
633
+ category="marketing"
634
+ fallback={({ openPreferences }) => (
635
+ <div className="banner-blocked">
636
+ <p>Vídeo bloqueado por privacidad.</p>
637
+ <button onClick={openPreferences}>
638
+ Aceptar cookies de marketing
639
+ </button>
640
+ </div>
641
+ )}
642
+ >
643
+ <iframe src="https://www.youtube.com/embed/..." />
644
+ </ConsentGate>
645
+ );
646
+ }
647
+ ```
648
+
649
+ Configura el SDK en el `RootLayout` con `strategy="beforeInteractive"`:
650
+
651
+ ```tsx
652
+ // app/[locale]/layout.tsx
139
653
  import Script from "next/script";
140
654
 
141
- export default function RootLayout({ children }: { children: React.ReactNode }) {
655
+ export default function RootLayout({
656
+ children,
657
+ params: { locale },
658
+ }: {
659
+ children: React.ReactNode;
660
+ params: { locale: string };
661
+ }) {
142
662
  return (
143
- <html lang="es">
663
+ <html lang={locale}>
144
664
  <head>
665
+ {/* Sincronización automática con el atributo lang sin necesidad de boilerplate */}
145
666
  <Script
146
667
  src="/vendor/consent.min.js"
147
668
  data-config="/consent.json"
@@ -156,9 +677,15 @@ export default function RootLayout({ children }: { children: React.ReactNode })
156
677
 
157
678
  ### Angular 20 Standalone
158
679
 
680
+ Carga la configuración con `provideAppInitializer` inyectando `ConsentService`. Puedes sincronizar el idioma en cualquier momento con `consentService.syncLocale(locale)` (ej. conectado a `@ngx-translate` o `Transloco`):
681
+
159
682
  ```typescript
160
683
  // app.config.ts
161
- import { ApplicationConfig, provideAppInitializer, inject } from "@angular/core";
684
+ import {
685
+ ApplicationConfig,
686
+ provideAppInitializer,
687
+ inject,
688
+ } from "@angular/core";
162
689
  import { ConsentService } from "@solvenza/cookies-compliance/angular";
163
690
 
164
691
  export const appConfig: ApplicationConfig = {
@@ -166,12 +693,196 @@ export const appConfig: ApplicationConfig = {
166
693
  ConsentService,
167
694
  provideAppInitializer(async () => {
168
695
  const consentService = inject(ConsentService);
169
- await consentService.init("/assets/consent.json");
696
+ await consentService.init({
697
+ schemaVersion: 1,
698
+ policyVersion: "2026-08-23",
699
+ locale: { default: "es", autoDetect: true },
700
+ policy: {
701
+ privacyUrl: "/politica-privacidad",
702
+ cookiesUrl: "/politica-cookies",
703
+ },
704
+ ui: {
705
+ floatingBadge: {
706
+ enabled: true,
707
+ position: "bottom-left",
708
+ icon: "cookie",
709
+ label: "Cookies",
710
+ },
711
+ },
712
+ categories: {
713
+ necessary: {
714
+ required: true,
715
+ label: "Necesarias",
716
+ description: "Imprescindibles.",
717
+ },
718
+ analytics: {
719
+ required: false,
720
+ label: "Analítica",
721
+ description: "Medición agregada.",
722
+ },
723
+ marketing: {
724
+ required: false,
725
+ label: "Marketing",
726
+ description: "Vídeo y contenido interactivo.",
727
+ },
728
+ },
729
+ });
170
730
  }),
171
731
  ],
172
732
  };
173
733
  ```
174
734
 
735
+ Uso de la directiva estructural `*consentGate` en componentes standalone de Angular:
736
+
737
+ ```typescript
738
+ // video-player.component.ts
739
+ import { Component } from "@angular/core";
740
+ import {
741
+ ConsentGateDirective,
742
+ ConsentService,
743
+ } from "@solvenza/cookies-compliance/angular";
744
+
745
+ @Component({
746
+ selector: "app-video-player",
747
+ standalone: true,
748
+ imports: [ConsentGateDirective],
749
+ template: `
750
+ <!-- Renderizado condicional reactivo -->
751
+ <div *consentGate="'marketing'; else videoBlocked">
752
+ <iframe
753
+ src="https://www.youtube.com/embed/dQw4w9WgXcQ"
754
+ width="560"
755
+ height="315"
756
+ ></iframe>
757
+ </div>
758
+
759
+ <ng-template #videoBlocked>
760
+ <div class="video-placeholder">
761
+ <p>Vídeo bloqueado. Requiere consentimiento de marketing.</p>
762
+ <button (click)="openCookies()">Configurar cookies</button>
763
+ </div>
764
+ </ng-template>
765
+ `,
766
+ })
767
+ export class VideoPlayerComponent {
768
+ constructor(private consentService: ConsentService) {}
769
+
770
+ openCookies() {
771
+ this.consentService.openPreferences();
772
+ }
773
+ }
774
+ ```
775
+
776
+ ### Vue 3 (Composition API & Componente ConsentGate)
777
+
778
+ Usa los composables reactivos y el componente declarativo `<ConsentGate>`:
779
+
780
+ ```vue
781
+ <script setup lang="ts">
782
+ import {
783
+ useConsent,
784
+ useSyncConsentLocale,
785
+ ConsentGate,
786
+ } from "@solvenza/cookies-compliance/vue";
787
+ import { useI18n } from "vue-i18n";
788
+
789
+ const { locale } = useI18n();
790
+ // Sincronización automática de idioma con vue-i18n
791
+ useSyncConsentLocale(locale);
792
+
793
+ const isAnalyticsAllowed = useConsent("analytics");
794
+ </script>
795
+
796
+ <template>
797
+ <div>
798
+ <p>
799
+ Estado de analítica: {{ isAnalyticsAllowed ? "Permitida" : "Bloqueada" }}
800
+ </p>
801
+
802
+ <!-- Renderizado declarativo condicional -->
803
+ <ConsentGate category="marketing">
804
+ <template #default>
805
+ <iframe src="https://www.youtube.com/embed/dQw4w9WgXcQ" />
806
+ </template>
807
+ <template #fallback>
808
+ <div class="blocked-card">
809
+ <p>
810
+ Vídeo bloqueado por privacidad. Acepta cookies de Marketing para
811
+ reproducirlo.
812
+ </p>
813
+ </div>
814
+ </template>
815
+ </ConsentGate>
816
+ </div>
817
+ </template>
818
+ ```
819
+
820
+ Plugin global en `main.ts`:
821
+
822
+ ```ts
823
+ import { createApp } from "vue";
824
+ import { createConsentPlugin } from "@solvenza/cookies-compliance/vue";
825
+ import App from "./App.vue";
826
+
827
+ const app = createApp(App);
828
+ app.use(createConsentPlugin("/consent.json"));
829
+ app.mount("#app");
830
+ ```
831
+
832
+ ### Nuxt 3 (SSR & Universal)
833
+
834
+ Crea un plugin cliente en `plugins/consent.client.ts`:
835
+
836
+ ```ts
837
+ // plugins/consent.client.ts
838
+ import { defineNuxtConsentPlugin } from "@solvenza/cookies-compliance/nuxt";
839
+
840
+ export default defineNuxtPlugin(defineNuxtConsentPlugin("/consent.json"));
841
+ ```
842
+
843
+ Y usa los composables y `<ConsentGate>` directamente en cualquier página o componente:
844
+
845
+ ```vue
846
+ <!-- pages/index.vue -->
847
+ <script setup lang="ts">
848
+ import { useConsent, ConsentGate } from "@solvenza/cookies-compliance/nuxt";
849
+
850
+ const isAnalyticsAllowed = useConsent("analytics");
851
+ </script>
852
+
853
+ <template>
854
+ <main>
855
+ <h1>Mi aplicación Nuxt 3</h1>
856
+ <ConsentGate category="marketing">
857
+ <template #default>
858
+ <iframe src="https://www.youtube.com/embed/dQw4w9WgXcQ" />
859
+ </template>
860
+ <template #fallback>
861
+ <p>Vídeo bloqueado. Por favor, autoriza la categoría de marketing.</p>
862
+ </template>
863
+ </ConsentGate>
864
+ </main>
865
+ </template>
866
+ ```
867
+
868
+ ### WordPress
869
+
870
+ ```php
871
+ // functions.php o plugin personalizado
872
+ function enqueue_solvenza_cookies() {
873
+ wp_enqueue_script(
874
+ "solvenza-cookies",
875
+ get_template_directory_uri() . "/vendor/consent.min.js",
876
+ array(),
877
+ "1.5.3",
878
+ false // En <head> para cumplir LSSI antes de scripts de analítica
879
+ );
880
+ // Asocia la configuración JSON con el badge flotante habilitado
881
+ wp_script_add_data("solvenza-cookies", "data-config", get_template_directory_uri() . "/consent.json");
882
+ }
883
+ add_action("wp_enqueue_scripts", "enqueue_solvenza_cookies");
884
+ ```
885
+
175
886
  ---
176
887
 
177
888
  ## API JavaScript
@@ -201,22 +912,231 @@ const unsubscribe = Consent.on("consent:changed", ({ choices, receipt }) => {
201
912
  console.log("Nuevo consentimiento:", choices);
202
913
  });
203
914
 
204
- // Generar tabla de política de cookies dinámicamente
205
- Consent.mountPolicy("#contenedor-politica");
915
+ // Generar documentos legales y tablas dinámicamente en Vanilla JS
916
+ Consent.mountPolicy("#contenedor-politica", { view: "full" });
917
+ Consent.mountLegalNotice("#contenedor-aviso-legal");
918
+ Consent.mountPrivacyPolicy("#contenedor-privacidad");
919
+
920
+ // O renderizar como string HTML para SSR
921
+ const htmlPolitica = Consent.renderPolicyHtml({ view: "full" });
922
+ ```
923
+
924
+ ---
925
+
926
+ ## Componentes de Textos Legales y Tablas de Cookies
927
+
928
+ La librería incluye componentes declarativos y utilidades para generar automáticamente la **Política de Cookies**, el **Aviso Legal (LSSI-CE art. 10)** y la **Política de Privacidad (RGPD)** totalmente adaptados a la configuración y servicios declarados.
929
+
930
+ ### 1. Configuración de datos del Titular (`legalEntity` y `legalNotice`)
931
+
932
+ En `consent.json`:
933
+
934
+ ```json
935
+ {
936
+ "schemaVersion": 1,
937
+ "policyVersion": "2026-08-23",
938
+ "legalEntity": {
939
+ "name": "Mi Empresa S.L.",
940
+ "tradeName": "Mi Marca",
941
+ "taxId": "B-12345678",
942
+ "address": "Calle Principal 123, 28001 Madrid, España",
943
+ "email": "privacidad@mi-sitio.com",
944
+ "phone": "+34 910 000 000",
945
+ "registryData": "Registro Mercantil de Madrid, Tomo 123, Folio 45",
946
+ "dpoEmail": "dpo@mi-sitio.com"
947
+ },
948
+ "legalNotice": {
949
+ "applicableLaw": "Legislación española (LSSI-CE, LOPDGDD) y RGPD",
950
+ "jurisdiction": "Juzgados y Tribunales de Madrid"
951
+ }
952
+ }
953
+ ```
954
+
955
+ O con `ConsentConfigBuilder`:
956
+
957
+ ```typescript
958
+ const config = new ConsentConfigBuilder("2026-08-23")
959
+ .setLegalEntity({
960
+ name: "Mi Empresa S.L.",
961
+ tradeName: "Mi Marca",
962
+ taxId: "B-12345678",
963
+ address: "Calle Principal 123, Madrid",
964
+ email: "privacidad@mi-sitio.com",
965
+ })
966
+ .setLegalNotice({
967
+ jurisdiction: "Juzgados y Tribunales de Madrid",
968
+ })
969
+ .build();
206
970
  ```
207
971
 
208
972
  ---
209
973
 
210
- ## Herramienta CLI de auditoría
974
+ ### 2. Uso en React & Next.js
975
+
976
+ ```tsx
977
+ import {
978
+ CookiePolicy,
979
+ LegalNotice,
980
+ PrivacyPolicy,
981
+ } from "@solvenza/cookies-compliance/react";
982
+ // O desde "@solvenza/cookies-compliance/next"
983
+
984
+ export function CookiePolicyPage() {
985
+ return (
986
+ <div>
987
+ {/* 1. Documento completo de Política de Cookies (con botones AEPD y guías de navegador) */}
988
+ <CookiePolicy view="full" />
211
989
 
212
- Puedes auditar el aislamiento previo de recursos en cualquier dominio con:
990
+ {/* 2. O solo la tabla interactiva de cookies registradas */}
991
+ <CookiePolicy view="table-only" />
992
+
993
+ {/* 3. Aviso Legal LSSI-CE */}
994
+ <LegalNotice />
995
+
996
+ {/* 4. Política de Privacidad RGPD */}
997
+ <PrivacyPolicy />
998
+ </div>
999
+ );
1000
+ }
1001
+ ```
1002
+
1003
+ ---
1004
+
1005
+ ### 3. Uso en Vue 3 & Nuxt 3
1006
+
1007
+ ```vue
1008
+ <script setup lang="ts">
1009
+ import {
1010
+ CookiePolicy,
1011
+ LegalNotice,
1012
+ PrivacyPolicy,
1013
+ } from "@solvenza/cookies-compliance/vue";
1014
+ // O desde "@solvenza/cookies-compliance/nuxt"
1015
+ </script>
1016
+
1017
+ <template>
1018
+ <main>
1019
+ <!-- Documento completo con revocación AEPD interactiva -->
1020
+ <CookiePolicy view="full" />
1021
+
1022
+ <!-- Solo inventario de cookies -->
1023
+ <CookiePolicy view="table-only" />
1024
+
1025
+ <!-- Aviso Legal LSSI-CE -->
1026
+ <LegalNotice />
1027
+ </main>
1028
+ </template>
1029
+ ```
1030
+
1031
+ ---
1032
+
1033
+ ### 4. Uso en Angular 17+ / 20
1034
+
1035
+ ```html
1036
+ <!-- app.component.html -->
1037
+ <solvenza-cookie-policy [view]="'full'"></solvenza-cookie-policy>
1038
+ <solvenza-cookie-policy [view]="'table-only'"></solvenza-cookie-policy>
1039
+ <solvenza-legal-notice></solvenza-legal-notice>
1040
+ <solvenza-privacy-policy></solvenza-privacy-policy>
1041
+ ```
1042
+
1043
+ ---
1044
+
1045
+ ---
1046
+
1047
+ ## GitHub Action de Auditoría LSSI & AEPD
1048
+
1049
+ Automatiza la validación continua de cumplimiento legal (LSSI art. 22.2, directrices AEPD 2024 y RGPD) en tus flujos de CI/CD de GitHub Actions antes de desplegar a producción.
1050
+
1051
+ ### Uso rápido en `.github/workflows/lssi-audit.yml`:
1052
+
1053
+ ```yaml
1054
+ name: "Auditoría LSSI & AEPD"
1055
+
1056
+ on:
1057
+ push:
1058
+ branches: [main, develop]
1059
+ pull_request:
1060
+ branches: [main]
1061
+
1062
+ jobs:
1063
+ lssi-compliance:
1064
+ name: "Verificación de Cumplimiento LSSI"
1065
+ runs-on: ubuntu-latest
1066
+ steps:
1067
+ - name: Checkout del código
1068
+ uses: actions/checkout@v4
1069
+
1070
+ - name: Ejecutar Auditoría LSSI & AEPD
1071
+ uses: solvenza/cookies-compliance@v1
1072
+ with:
1073
+ config-path: "./consent.json"
1074
+ src-dir: "./src"
1075
+ fail-on-error: "true"
1076
+ ```
1077
+
1078
+ ### Entradas (Inputs) de la GitHub Action:
1079
+
1080
+ | Input | Descripción | Valor por defecto |
1081
+ | --------------- | ------------------------------------------------------------------------------------- | ----------------- |
1082
+ | `config-path` | Ruta relativa al archivo `consent.json` | `./consent.json` |
1083
+ | `src-dir` | Directorio con archivos fuente a escanear (`.html`, `.jsx`, `.tsx`, `.vue`, etc.) | `./` |
1084
+ | `url` | URL en vivo a auditar (ej: `https://staging.mi-sitio.com`) | _(Opcional)_ |
1085
+ | `fail-on-error` | Bloquear el pipeline con código de salida no nulo si se detectan violaciones críticas | `true` |
1086
+
1087
+ ### Resumen automático en el Step Summary de GitHub:
1088
+
1089
+ La GitHub Action genera anotaciones directas en las líneas de código (`::error::` / `::warning::`) y añade una tabla resumen visual en el **Job Step Summary** con la puntuación de 0 a 100, estado de cumplimiento y recomendaciones de subsanación.
1090
+
1091
+ ---
1092
+
1093
+ ## Herramienta CLI de auditoría LSSI
1094
+
1095
+ Puedes ejecutar el motor de auditoría de forma manual o en tus scripts de `package.json`:
213
1096
 
214
1097
  ```bash
215
- npx @solvenza/cookies-compliance consent-audit https://mi-sitio.com
1098
+ # Auditar configuración y código fuente local
1099
+ npx @solvenza/cookies-compliance consent-audit --config ./consent.json --src ./src --fail-on-error
1100
+
1101
+ # Auditar una URL remota en vivo
1102
+ npx @solvenza/cookies-compliance consent-audit --url https://mi-sitio.com
1103
+
1104
+ # Generar reporte estructurado en formato JSON
1105
+ npx @solvenza/cookies-compliance consent-audit --config ./consent.json --src ./src --json
1106
+ ```
1107
+
1108
+ ### Uso programático en Node.js / TypeScript:
1109
+
1110
+ ```typescript
1111
+ import { LssiAuditor } from "@solvenza/cookies-compliance/audit";
1112
+
1113
+ const report = await LssiAuditor.run({
1114
+ configPath: "./consent.json",
1115
+ srcDir: "./src",
1116
+ url: "https://staging.mi-sitio.com",
1117
+ });
1118
+
1119
+ console.log(`Puntuación: ${report.score}/100 - Cumple: ${report.compliant}`);
216
1120
  ```
217
1121
 
218
1122
  ---
219
1123
 
1124
+ ## Ejemplos de integración por framework
1125
+
1126
+ El repositorio incluye ejemplos interactivos listos para ejecutar:
1127
+
1128
+ | Directorio | Framework / Stack | Características clave |
1129
+ | --------------------------------------------------- | ---------------------- | --------------------------------------------------------------------------------------- |
1130
+ | [`examples/vanilla-html/`](./examples/vanilla-html) | Vanilla HTML5 / JS | Inclusión vía `<script data-config="...">`, iframe gate nativo y CSS personalizado |
1131
+ | [`examples/react-app/`](./examples/react-app) | React 18+ & Vite | `useConsent`, `useSyncConsentLocale`, `<ConsentGate>` y tabla de política dinámica |
1132
+ | [`examples/next-app/`](./examples/next-app) | Next.js (App Router) | Script con `strategy="beforeInteractive"`, componentes de cliente y SSR |
1133
+ | [`examples/vue-app/`](./examples/vue-app) | Vue 3 & Vite | Composables reactivos `useConsent`, `useGpc`, `useSyncConsentLocale` y `<ConsentGate>` |
1134
+ | [`examples/nuxt-app/`](./examples/nuxt-app) | Nuxt 3 | Módulo / plugin SSR universal `@solvenza/cookies-compliance/nuxt` |
1135
+ | [`examples/angular-app/`](./examples/angular-app) | Angular 17+ Standalone | `ConsentService`, directiva estructural `*consentGate` y `provideAppInitializer` |
1136
+ | [`playground/`](./playground) | Sandbox Interactivo | Panel de pruebas con simulador de presets, purga de Web Storage e inspección de recibos |
1137
+
1138
+ ---
1139
+
220
1140
  ## Entorno de desarrollo local
221
1141
 
222
1142
  1. Clona el repositorio:
@@ -237,6 +1157,10 @@ npx @solvenza/cookies-compliance consent-audit https://mi-sitio.com
237
1157
  ```bash
238
1158
  npm run playground
239
1159
  ```
1160
+ 5. Inicia el ejemplo de Vue 3:
1161
+ ```bash
1162
+ cd examples/vue-app && npm install && npm run dev
1163
+ ```
240
1164
 
241
1165
  ---
242
1166