@ecomconsult/consentkit 0.4.0 → 0.5.1

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/README.md CHANGED
@@ -179,12 +179,16 @@ Pass any subset to `init()`. Nested objects merge with the defaults.
179
179
  |---|---|---|---|
180
180
  | `policyVersion` | `string \| number` | `"1"` | Bump to invalidate stored consent and re-show the banner |
181
181
  | `language` | `string` | `"auto"` | `"auto"` reads `navigator.language`. Falls back `pt-BR` → `pt` → `en` |
182
- | `layout.type` | `"bar" \| "modal" \| "box"` | `"bar"` | `box` is a compact ~360px card |
182
+ | `layout.type` | `"bar" \| "modal" \| "box"` | `"bar"` | `box` is a corner card, up to 540px wide |
183
183
  | `layout.position` | `string` | per type | `bar`: `bottom` (default) / `top`. `box`: `bottom-left` (default) / `bottom-right`. `modal` is always centred. A position that does not belong to the chosen type falls back to that type's default; the type itself is unaffected |
184
184
  | `theme.accent` | `string` | `"#2B50D8"` | Exposed as `--ck-accent` |
185
- | `theme.radius` | `string` | `"10px"` | Exposed as `--ck-radius` |
185
+ | `theme.font` | `"inherit" \| "system"` | `"inherit"` | v0.5.0. `inherit` takes the host page's font family; `system` restores the pre-0.5.0 system stack. Font *sizes* are fixed either way |
186
+ | `theme.radius` | `{ card, button }` | `{ card: 16, button: 8 }` | v0.5.0. px, clamped 0–32. A bare string or number is the pre-0.5.0 form and still sets the card radius |
187
+ | `theme.buttons` | `{ accept, reject, settings }` | see below | v0.5.0. Per-button appearance. **Contrast is enforced automatically** — see [Button appearance](#button-appearance) |
186
188
  | `theme.mode` | `"auto" \| "light" \| "dark"` | `"auto"` | `auto` follows `prefers-color-scheme` |
187
- | `theme.dark` | `{ bg, ink, accent }` | built-in | Overrides the dark palette |
189
+ | `theme.dark` | `{ bg, ink, accent, onAccent }` | built-in | Overrides the dark palette |
190
+ | `texts.policyUrl` | `string` | — | v0.5.0. Cookie policy address. `http(s)` only; anything else is ignored |
191
+ | `texts.detailsAction` | `"policy" \| "settings" \| "hide"` | see notes | v0.5.0. What «Learn more» does. Defaults to `policy` when `policyUrl` is set, `settings` when it is not. `policy` without a usable URL falls back to `settings` rather than rendering a dead link |
188
192
  | `categories.*.enabled` | `boolean` | `true` | Per category: `functional`, `analytics`, `marketing`. Hides the toggle when `false` |
189
193
  | `consentTtlDays` | `number` | `365` | Lifetime of the stored decision |
190
194
  | `integrations.gcm` | `boolean` | `true` | Google Consent Mode v2 signals |
@@ -200,6 +204,98 @@ Pass any subset to `init()`. Nested objects merge with the defaults.
200
204
  { name: '_ga', category: 'analytics', vendor: 'Google', purpose: 'Visit statistics', expiry: '2 years' }
201
205
  ```
202
206
 
207
+ ### Button appearance
208
+
209
+ Each of the three banner buttons can be styled independently:
210
+
211
+ ```js
212
+ theme: {
213
+ accent: '#2B50D8',
214
+ font: 'inherit',
215
+ radius: { card: 16, button: 8 },
216
+ buttons: {
217
+ accept: { variant: 'filled' }, // accent fill
218
+ reject: { variant: 'filled' }, // always matches accept
219
+ settings: { variant: 'outline', borderWidth: 1 } // accent border
220
+ }
221
+ }
222
+ ```
223
+
224
+ Each entry takes `variant` (`"filled"` or `"outline"`), and optionally `bg`,
225
+ `fg`, `border` and `borderWidth` (`1` or `2`). Omitted colours come from
226
+ `theme.accent`.
227
+
228
+ **Accept and reject are always equal.** They render at the same size and weight,
229
+ and they always share one variant — if the two disagree in the config,
230
+ `accept.variant` is used for both. A reject button that looks weaker than
231
+ accept is a dark pattern, and consent collected through one is not freely
232
+ given, so the config simply cannot express it. `settings` is independent and
233
+ may itself be filled.
234
+
235
+ **Contrast is enforced automatically and cannot be switched off.** A colour
236
+ combination that would be unreadable is corrected before it reaches the
237
+ stylesheet:
238
+
239
+ - button text keeps the colour you set only when it clears **4.5:1** against
240
+ the fill it sits on; otherwise it becomes white or `#161616`, whichever
241
+ contrasts more;
242
+ - an `outline` border is darkened (light card) or lightened (dark card) in
243
+ small steps until it clears **3:1** against the card, so it stays
244
+ recognisably your colour rather than jumping to black or white;
245
+ - that resolved border colour is then the outline button's text colour,
246
+ subject to the same 4.5:1 rule.
247
+
248
+ The card is `#ffffff` in light mode and `#1c1c1e` in dark. A colour the
249
+ arithmetic cannot read — a CSS colour name, an `rgb()` string — is left exactly
250
+ as you wrote it rather than being silently replaced.
251
+
252
+ The same arithmetic is exposed as pure functions on `ConsentKit._contrast`
253
+ (`relativeLuminance`, `contrastRatio`, `ensureContrast`, `stepToContrast`,
254
+ `resolveButtonStyles`, `resolveRadius`, `resolveFont`, `resolveDetails`,
255
+ `buildThemeCss`) so a theme editor can show the same numbers the banner paints
256
+ instead of reimplementing them. It is present whenever `src/ck-ui.js` is loaded,
257
+ and it is safe to call in Node — nothing in it touches the DOM. The debug
258
+ panel's **Appearance** section reads it directly and reports each button's
259
+ resolved colours, its contrast ratio, and whether the value was adjusted.
260
+
261
+ ### The «Learn more» link
262
+
263
+ `texts.detailsAction` decides what the link at the end of the banner copy does:
264
+
265
+ | Value | Renders |
266
+ |---|---|
267
+ | `"policy"` | A link to `texts.policyUrl`, opened with `target="_blank" rel="noopener"` |
268
+ | `"settings"` | A button that opens the preferences panel |
269
+ | `"hide"` | Nothing at all |
270
+
271
+ The default follows `policyUrl`: `policy` when one is set, `settings` when it
272
+ is not — so supplying only a URL does the obvious thing.
273
+
274
+ > Before 0.5.0 this control was rendered as `<a href="#">` with no handler at
275
+ > all: clicking it jumped to the top of the page and nothing else. Any site
276
+ > running 0.4.x or earlier has a dead «Learn more» link.
277
+
278
+ ### Infrastructure
279
+
280
+ Some third-party hosts are not a consent decision at all: they are where a site
281
+ builder or hosting platform serves the site's **own** markup, styles and
282
+ scripts from. A Tilda page loads its layout from `tildacdn.com`, a Wix page
283
+ loads its from `parastorage.com`, and a page using Google Fonts loads its
284
+ typefaces from `fonts.gstatic.com`. ConsentKit ships these as a separate class
285
+ of host — readable as `ConsentKit._infra()`, tested per URL or hostname with
286
+ `ConsentKit._isInfra(url)` — covering the CDNs of Tilda, Wix, Shopify,
287
+ Squarespace and Webflow, the general asset CDNs (`cdn.jsdelivr.net`,
288
+ `unpkg.com`, `cdnjs.cloudflare.com`, `code.jquery.com`, `ajax.googleapis.com`),
289
+ Google Fonts and hCaptcha. Strict mode never intercepts them, because blocking a
290
+ builder's own CDN breaks the page without protecting anyone; the hosted service
291
+ also leaves them out of scan reports, since there is nothing for a site owner to
292
+ decide. Membership is a claim that a host delivers the site's own assets, not
293
+ that it is harmless in general — anything that *measures* keeps a real consent
294
+ category instead, which is why `static.cloudflareinsights.com` (Cloudflare Web
295
+ Analytics) is classified as `analytics` and blocked before consent even though
296
+ the rest of Cloudflare's CDN is infrastructure. Both lists are matched by suffix
297
+ and returned as copies, so reading them cannot widen what strict mode allows.
298
+
203
299
  ## API
204
300
 
205
301
  All methods are safe to call at any time and never throw.
@@ -360,11 +456,10 @@ Four things are never intercepted:
360
456
  the answer is unclear it says same-site, because wrongly blocking a
361
457
  first-party asset breaks the site.
362
458
  2. **`blocking.allow`** — your own list, matched by suffix.
363
- 3. **The built-in allowlist**, readable as `ConsentKit._baseAllow`: asset CDNs
364
- (`cdn.jsdelivr.net`, `unpkg.com`, `cdnjs.cloudflare.com`, `code.jquery.com`,
365
- `fonts.googleapis.com`, `fonts.gstatic.com`) and things a page is unusable
366
- without (`js.stripe.com`, `pay.google.com`, `checkout.creem.io`,
367
- `hcaptcha.com`, and reCAPTCHA — scoped to `www.google.com/recaptcha` and
459
+ 3. **The built-in allowlist**, readable as `ConsentKit._baseAllow`. Two parts:
460
+ **infrastructure** (`ConsentKit._infra()`, see below) and things a page is
461
+ unusable without (`js.stripe.com`, `pay.google.com`, `checkout.creem.io`,
462
+ and reCAPTCHA — scoped to `www.google.com/recaptcha` and
368
463
  `www.gstatic.com/recaptcha`, not to those hosts at large).
369
464
  4. **Known `necessary` / `functional` hosts** already granted, which keep their
370
465
  real category rather than being swept up as marketing.
@@ -481,23 +576,26 @@ external requests. Rebuild them with `tools/build-inline.mjs` (see
481
576
  [`tools/README.md`](tools/README.md)); each block's header records the exact
482
577
  command that produced it.
483
578
 
484
- ConsentKit 0.4.0, rebuilt 2026-09-05, uncompressed — gzip on the server cuts
579
+ ConsentKit 0.5.0, rebuilt 2026-09-05, uncompressed — gzip on the server cuts
485
580
  this roughly three- to fourfold. Every block includes the branding extension
486
581
  and the attribution line; `--no-branding` drops both the code and the config
487
582
  and takes **~24 KB** back off:
488
583
 
489
584
  | Block | Languages | Bytes | gzip | `--no-branding` |
490
585
  |---|---|---|---|---|
491
- | `ready/en-bar.txt` | en | 134,969 | 40,519 | 110,907 |
492
- | `ready/ru-bar.txt` | ru, ro, en | 136,590 | 41,315 | 112,508 |
493
- | `ready/ru-box.txt` | ru, ro, en | 136,605 | 41,320 | 112,523 |
494
- | `ready/ru-box-right.txt` | ru, ro, en | 136,614 | 41,326 | 112,526 |
495
- | `ready/ru-modal.txt` | ru, ro, en | 136,598 | 41,318 | 112,514 |
496
- | `ready/eu-bar.txt` | 34 languages | 185,244 | 59,665 | 161,182 |
497
-
498
- 0.4.0 adds roughly 16.1 KB over 0.3.6: iframe interception, strict mode
499
- (same-site detection, the allowlists, the public-suffix table) and the
500
- extensible tracker database.
586
+ | `ready/en-bar.txt` | en | 160,388 | 48,950 | 136,326 |
587
+ | `ready/ru-bar.txt` | ru, ro, en | 162,009 | 49,711 | 137,927 |
588
+ | `ready/ru-box.txt` | ru, ro, en | 162,024 | 49,718 | 137,942 |
589
+ | `ready/ru-box-right.txt` | ru, ro, en | 162,033 | 49,718 | 137,945 |
590
+ | `ready/ru-modal.txt` | ru, ro, en | 162,017 | 49,713 | 137,933 |
591
+ | `ready/eu-bar.txt` | 34 languages | 210,663 | 68,094 | 186,601 |
592
+
593
+ 0.5.0 adds roughly 18.5 KB over 0.4.1 (about 6.1 KB gzipped): the contrast
594
+ engine, the per-button token resolution, the new stylesheet rules and the
595
+ comments explaining the rules the arithmetic implements. 0.4.1 had added
596
+ roughly 6.3 KB over 0.4.0 (the infrastructure list), and 0.4.0 roughly 16.1 KB
597
+ over 0.3.6: iframe interception, strict mode (same-site detection, the
598
+ allowlists, the public-suffix table) and the extensible tracker database.
501
599
 
502
600
  Size is driven almost entirely by the bundled languages: `en` and `ru` are
503
601
  built into the UI and cost nothing extra, while layout, position, theme and
package/npm/index.d.ts CHANGED
@@ -59,17 +59,87 @@ export interface CkDarkTheme {
59
59
  accent?: string;
60
60
  }
61
61
 
62
+ /** v0.5.0 (SPEC V1.6 §1). Corner radii in px, clamped to 0–32. */
63
+ export interface CkRadiusConfig {
64
+ /** Banner, panel and table corners. Default `16`. */
65
+ card?: number;
66
+ /** Button corners. Default `8`. */
67
+ button?: number;
68
+ }
69
+
70
+ /**
71
+ * v0.5.0 (SPEC V1.6 §1). One button's appearance.
72
+ *
73
+ * Contrast is enforced automatically and cannot be switched off: `fg` is kept
74
+ * only when it clears 4.5:1 against the fill it sits on, otherwise it becomes
75
+ * white or `#161616`, whichever wins. An `outline` `border` is darkened or
76
+ * lightened in steps until it clears 3:1 against the card, and that resolved
77
+ * border colour is then the text colour, subject to the same 4.5:1 rule.
78
+ * `ConsentKit._contrast` exposes the arithmetic.
79
+ */
80
+ export interface CkButtonStyle {
81
+ variant?: 'filled' | 'outline';
82
+ /** Fill. `filled` defaults to the accent; `outline` is transparent. */
83
+ bg?: string;
84
+ /** Text. Computed from the contrast rules when omitted. */
85
+ fg?: string;
86
+ /** Border. Defaults to the accent. */
87
+ border?: string;
88
+ /** `1` (default) or `2`. */
89
+ borderWidth?: 1 | 2;
90
+ }
91
+
92
+ /**
93
+ * v0.5.0 (SPEC V1.6 §1). Per-button appearance.
94
+ *
95
+ * `accept` and `reject` are held to the equal-buttons invariant: they always
96
+ * render at the same size and weight, and they always share one variant. If
97
+ * the two disagree, `accept.variant` is used for both.
98
+ */
99
+ export interface CkButtonsConfig {
100
+ /** Default `{ variant: 'filled' }` with the accent as the fill. */
101
+ accept?: CkButtonStyle;
102
+ /** Default `{ variant: 'filled' }`; always follows `accept.variant`. */
103
+ reject?: CkButtonStyle;
104
+ /** Default `{ variant: 'outline' }` with the accent as the border. */
105
+ settings?: CkButtonStyle;
106
+ }
107
+
62
108
  export interface CkThemeConfig {
63
109
  /** Accent colour, exposed as `--ck-accent`. Default `'#2B50D8'`. */
64
110
  accent?: string;
65
- /** Corner radius, exposed as `--ck-radius`. Default `'10px'`. */
66
- radius?: string;
111
+ /**
112
+ * v0.5.0. `{ card, button }` in px, 0–32. Default `{ card: 16, button: 8 }`.
113
+ *
114
+ * A bare string or number is the pre-0.5.0 form and still works: it sets the
115
+ * card radius and leaves the button radius at its default.
116
+ */
117
+ radius?: CkRadiusConfig | string | number;
118
+ /**
119
+ * v0.5.0. `'inherit'` (default) takes the host page's font family — sizes
120
+ * stay fixed either way. `'system'` restores the pre-0.5.0 system stack.
121
+ */
122
+ font?: 'inherit' | 'system';
123
+ /** v0.5.0. Per-button appearance; contrast is enforced automatically. */
124
+ buttons?: CkButtonsConfig;
67
125
  /** v0.2. Default `'auto'` — follows `prefers-color-scheme`. */
68
126
  mode?: 'auto' | 'light' | 'dark';
69
127
  /** v0.2. Overrides the built-in dark palette. */
70
128
  dark?: CkDarkTheme;
71
129
  }
72
130
 
131
+ /** v0.5.0 (SPEC V1.6 §2). Copy and the «Learn more» control. */
132
+ export interface CkTextsConfig {
133
+ /** Cookie policy address. `http(s)` only; anything else is ignored. */
134
+ policyUrl?: string;
135
+ /**
136
+ * What «Learn more» does. Defaults to `'policy'` when `policyUrl` is set and
137
+ * `'settings'` when it is not. `'policy'` without a usable URL falls back to
138
+ * `'settings'` rather than rendering a dead link. `'hide'` renders nothing.
139
+ */
140
+ detailsAction?: 'policy' | 'settings' | 'hide';
141
+ }
142
+
73
143
  /** Whether a category is offered in the preferences panel at all. */
74
144
  export interface CkCategoryConfig {
75
145
  enabled?: boolean;
@@ -119,6 +189,8 @@ export interface CkConfig {
119
189
  language?: string;
120
190
  layout?: CkLayoutConfig;
121
191
  theme?: CkThemeConfig;
192
+ /** v0.5.0. */
193
+ texts?: CkTextsConfig;
122
194
  categories?: CkCategoriesConfig;
123
195
  /** Lifetime of the stored decision, in days. Default `365`. */
124
196
  consentTtlDays?: number;
@@ -180,14 +252,100 @@ export interface ConsentKitApi {
180
252
  /**
181
253
  * v0.4.0 (§2). The built-in strict-mode allowlist, as hosts plus a few
182
254
  * `host/path` entries (reCAPTCHA). A copy: mutating it changes nothing.
255
+ *
256
+ * Since v0.4.1 this includes the whole of `_infra()`.
183
257
  */
184
258
  readonly _baseAllow: string[];
185
259
 
260
+ /**
261
+ * v0.4.1 (§8). The infrastructure list: CDNs and static hosts of site
262
+ * builders and hosting platforms, general asset CDNs, fonts and captcha.
263
+ *
264
+ * A CLASS of host, not a consent category — these serve the site's own
265
+ * assets, so there is no consent decision to make about them. Strict mode
266
+ * never intercepts them, and the SaaS scanner keeps them out of the report.
267
+ * A copy: mutating it changes nothing.
268
+ */
269
+ _infra(): string[];
270
+
271
+ /**
272
+ * v0.4.1 (§8). Whether a URL — or a bare hostname — is infrastructure.
273
+ */
274
+ _isInfra(url: string): boolean;
275
+
186
276
  /** The category the engine would assign to a URL, or `null` if unknown. */
187
277
  _categoryForUrl(url: string): CkCategory | null;
188
278
 
189
279
  /** What is currently held back, host+path only — never a query string. */
190
280
  _blocked(): CkBlockedEntry[];
281
+
282
+ /**
283
+ * v0.5.0 (SPEC V1.6 §1). The contrast and theme arithmetic the banner
284
+ * actually paints with, as pure functions — so a theme editor quotes the
285
+ * same numbers rather than reimplementing them and drifting.
286
+ *
287
+ * Present only when `src/ck-ui.js` is loaded (the core alone does not draw).
288
+ * Colour arguments are hex; anything unmeasurable (a colour name, an `rgb()`
289
+ * string) yields `null` from the ratio functions and is returned untouched,
290
+ * with `adjusted: false`, by the rest.
291
+ */
292
+ readonly _contrast?: CkContrastApi;
293
+ }
294
+
295
+ /** One resolved button, as `ConsentKit._contrast.resolveButtonStyles()` returns it. */
296
+ export interface CkResolvedButton {
297
+ variant: 'filled' | 'outline';
298
+ bg: string;
299
+ fg: string;
300
+ border: string;
301
+ borderWidth: 1 | 2;
302
+ /** Text contrast against `against`. `null` when it could not be measured. */
303
+ ratio: number | null;
304
+ /** `true` when the contrast rules overrode what the config asked for. */
305
+ adjusted: boolean;
306
+ /** Border contrast against the card (`outline` only). */
307
+ borderRatio?: number | null;
308
+ borderAdjusted?: boolean;
309
+ /** What `ratio` was measured against: the button's own fill, or the card. */
310
+ against: string;
311
+ }
312
+
313
+ export interface CkResolvedButtons {
314
+ mode: 'light' | 'dark';
315
+ cardBg: string;
316
+ accent: string;
317
+ buttons: { accept: CkResolvedButton; reject: CkResolvedButton; settings: CkResolvedButton };
318
+ }
319
+
320
+ /** v0.5.0. `ConsentKit._contrast` — pure, DOM-free, safe to call in Node. */
321
+ export interface CkContrastApi {
322
+ /** WCAG 2.1 relative luminance, 0–1. `null` for an unmeasurable colour. */
323
+ relativeLuminance(hex: string): number | null;
324
+ /** WCAG 2.1 contrast ratio, 1–21. `null` when either side is unmeasurable. */
325
+ contrastRatio(a: string, b: string): number | null;
326
+ /** Keeps `fg` when it clears `min`, else white or `#161616` — whichever wins. */
327
+ ensureContrast(fg: string, bg: string, min?: number):
328
+ { color: string; adjusted: boolean; ratio: number | null };
329
+ /** Darkens or lightens `color` in steps until it clears `min` against `bg`. */
330
+ stepToContrast(color: string, bg: string, min?: number):
331
+ { color: string; adjusted: boolean; ratio: number | null };
332
+ /** The three buttons resolved for one palette, contrast already applied. */
333
+ resolveButtonStyles(theme: CkThemeConfig, mode: 'light' | 'dark',
334
+ palette?: Record<string, string>): CkResolvedButtons;
335
+ resolveRadius(theme: CkThemeConfig): { card: number; button: number };
336
+ resolveFont(theme: CkThemeConfig): string;
337
+ /** Takes the whole config, not just `texts`. */
338
+ resolveDetails(config: CkConfig):
339
+ { kind: 'policy' | 'settings' | 'hide'; href: string | null };
340
+ /** Takes the whole config. Returns the generated stylesheet and both palettes. */
341
+ buildThemeCss(config: CkConfig): {
342
+ css: string;
343
+ mode: 'auto' | 'light' | 'dark';
344
+ radius: { card: number; button: number };
345
+ font: string;
346
+ light: CkResolvedButtons;
347
+ dark: CkResolvedButtons;
348
+ };
191
349
  }
192
350
 
193
351
  /** One entry of `ConsentKit._blocked()`. */
@@ -28,7 +28,7 @@ export function undecidedState() {
28
28
  */
29
29
  export function createStub() {
30
30
  const stub = {
31
- version: '0.4.0',
31
+ version: '0.5.1',
32
32
  config: {},
33
33
  init: function () { return undecidedState(); },
34
34
  allowed: function (cat) { return cat === 'necessary'; },
@@ -44,6 +44,11 @@ export function createStub() {
44
44
  _extendHostDb: function () { return 0; },
45
45
  _baseAllow: [],
46
46
  _categoryForUrl: function () { return null; },
47
+ // v0.4.1 (§8): the infrastructure list. Empty here for the same reason
48
+ // _baseAllow is — a stub means the engine never attached, so it is not
49
+ // letting anything through and must not claim a list it does not enforce.
50
+ _infra: function () { return []; },
51
+ _isInfra: function () { return false; },
47
52
  _blocked: function () { return []; },
48
53
  _isStub: true
49
54
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ecomconsult/consentkit",
3
- "version": "0.4.0",
3
+ "version": "0.5.1",
4
4
  "description": "GDPR cookie consent core with blocking engine, Shadow DOM UI and Google Consent Mode v2. Zero dependencies, no build step.",
5
5
  "repository": {
6
6
  "type": "git",