consentric 1.0.0 → 2.0.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/README.md CHANGED
@@ -59,16 +59,21 @@ npm run example # opens http://localhost:5173
59
59
  - **Google Consent Mode v2** — pushes `default` (all denied) on mount and `update` on
60
60
  the visitor's choice, mapping the four categories to the right Consent Mode signals.
61
61
  - **Persistent & versioned** — the choice is stored in a cookie for one year and
62
- restored on the next visit; bumping the schema version re-prompts everyone.
62
+ restored on the next visit; the stored value is versioned, so a future release can
63
+ re-prompt everyone after a policy change.
63
64
  - **Familiar tabbed UX** — Consent / Details / About tabs, four categories, a per-cookie
64
65
  accordion. Recognisable to visitors, nothing for you to design.
65
66
  - **10 built-in languages** — English (default), German, French, Spanish, Italian,
66
67
  Portuguese, Dutch, Polish, Czech and Slovak, picked with one `locale` prop. Every
67
68
  category, cookie row and UI string is also overridable for any other language.
69
+ - **Light & dark** — follows the OS `prefers-color-scheme` by default; ships light and
70
+ dark palettes, or pass your own (one for both themes, or `{ light, dark }`). Borders and
71
+ fills derive from your `text` colour, so a light theme needs no host-CSS overrides.
68
72
  - **Accessible** — focus-trapped modal, focus restored on close, arrow-key tab
69
73
  navigation, full ARIA wiring, and `prefers-reduced-motion` support.
70
- - **No dark patterns** — "Allow all" and "Deny" get equal visual weight, the way EU
71
- regulators expect a consent dialog to behave.
74
+ - **No dark patterns by default** — "Allow all" and "Deny" get equal visual weight, the
75
+ way EU regulators expect a consent dialog to behave. If you want the familiar
76
+ emphasised "Allow all", opt in with `primaryAction="allowAll"`.
72
77
  - **Works with Next.js** — ships as a Client Component (`"use client"`), so it drops
73
78
  straight into the App Router.
74
79
  - **Zero dependencies, zero Tailwind** — styles are scoped and injected; colours come
@@ -114,9 +119,14 @@ the root of your app.
114
119
  | `locale` | `'en'` | Built-in language pack (`de`, `fr`, `es`, `it`, `pt`, `nl`, `pl`, `cs`, `sk`). English is the default and fallback; `pt-BR` resolves to `pt`. |
115
120
  | `defaultOpen` | `false` | Open the dialog on mount even if a choice is stored (for previews/Storybook). |
116
121
  | `defaultTab` | `'consent'` | Tab to show first: `consent` / `details` / `about`. |
117
- | `colors` | dark theme | `{ brand, brandDeep, surface, surfaceAlt, text, textMuted, backdrop, onBrand }`. |
122
+ | `theme` | `'auto'` | `'auto'` follows the OS `prefers-color-scheme` (and re-renders on change); `'light'` / `'dark'` pin it. |
123
+ | `colors` | light + dark | One palette (applied to both themes) **or** `{ light, dark }`. See [Light & dark themes](#light--dark-themes). |
124
+ | `logo` | — | Brand mark. A single node, or `{ light, dark }` to swap it by theme. |
118
125
  | `categories` | built-in EN | Per-category content overrides — see below. |
119
126
  | `labels` | built-in EN | UI string overrides for localisation — see below. |
127
+ | `primaryAction` | `'save'` | Which button gets brand emphasis: `'save'` (only once a category is on — equal-weight otherwise), `'allowAll'` (always), or `'none'`. |
128
+ | `deferOpen` | `true` | Defer the first-visit auto-open past first paint (avoids Lighthouse `NO_LCP`). Ignored when `defaultOpen`. |
129
+ | `fontFamily` | system stack | Override the font stack. |
120
130
  | `onChange` | — | `(choices) => void`, fired after the user makes or changes a choice. |
121
131
  | `autoClearCookies` | `true` | When a category is denied or revoked, delete the cookies declared in its table (names support a trailing `*` wildcard). The consent cookie is never touched. |
122
132
 
@@ -191,22 +201,68 @@ language.
191
201
  />
192
202
  ```
193
203
 
194
- ## Light theme
204
+ ## Light & dark themes
195
205
 
196
- Defaults assume a dark `surface`. For a light theme, also set `onBrand` (the text/icon
197
- colour on top of the primary button) so it keeps enough contrast.
206
+ The banner ships **built-in light and dark palettes** and, by default (`theme="auto"`),
207
+ follows the visitor's OS `prefers-color-scheme` re-rendering if they flip it. So the
208
+ zero-config banner is light on a light OS and dark on a dark one. Pin it with
209
+ `theme="light"` or `theme="dark"`.
210
+
211
+ Borders, toggle tracks, badges and cookie rows are derived from `text` (via `color-mix`),
212
+ so a **light theme works from just `brand` / `surface` / `text`** — no host-CSS overrides,
213
+ nothing washed out on a light surface.
214
+
215
+ ### One palette, or one per theme
216
+
217
+ Pass a **single palette** and it applies to both themes (this is the v1 behaviour — if you
218
+ already pass `colors`, nothing changes):
219
+
220
+ ```tsx
221
+ <CookieConsent colors={{ brand: '#FAE762', surface: '#050506', text: '#EDEDEF', onBrand: '#0a0a0c' }} />
222
+ ```
223
+
224
+ Or pass **`{ light, dark }`** and the component switches with the theme:
198
225
 
199
226
  ```tsx
200
227
  <CookieConsent
228
+ theme="auto" // 'auto' | 'light' | 'dark'
201
229
  colors={{
202
- brand: '#2563eb',
203
- brandDeep: '#1d4ed8',
204
- surface: '#ffffff',
205
- surfaceAlt: 'rgba(0,0,0,0.04)',
206
- text: '#0f172a',
207
- textMuted: '#64748b',
208
- backdrop: 'rgba(15,23,42,0.4)',
209
- onBrand: '#ffffff',
230
+ light: { brand: '#2563eb', surface: '#ffffff', text: '#0f172a', textMuted: '#64748b' },
231
+ dark: { brand: '#3b82f6', surface: '#0b1220', text: '#e5e7eb', textMuted: '#94a3b8' },
232
+ }}
233
+ />
234
+ ```
235
+
236
+ Each palette key is optional and falls back to the built-in default for that theme:
237
+
238
+ | Key | What it colours |
239
+ |---|---|
240
+ | `brand` / `brandDeep` | Primary button, active tab, toggle-on, links; `brandDeep` is the hover. |
241
+ | `surface` / `surfaceAlt` | Card background / the raised category tiles. |
242
+ | `text` / `textMuted` | Primary / secondary text. Overlays (borders, tracks, badges) derive from `text`. |
243
+ | `backdrop` | The dimmed page behind the dialog. |
244
+ | `onBrand` | Text/icon on top of `brand` — primary button label, toggle knob, FAB icon. Set this whenever `brand` is light/saturated. |
245
+ | `border`, `hover`, `trackOff`, `badgeBg`, `cookieBg`, `link` | Fine control of the derived overlay tokens — override only if you need to. |
246
+
247
+ ### Following a `data-theme` host
248
+
249
+ Already flip your own CSS variables on `html[data-theme]`? Point `colors` at them and the
250
+ banner follows your toggle with no JS:
251
+
252
+ ```tsx
253
+ <CookieConsent colors={{ surface: 'var(--bg)', text: 'var(--fg)', textMuted: 'var(--fg-muted)', brand: '#3c3fde', onBrand: '#fff' }} />
254
+ ```
255
+
256
+ ### Per-theme logo
257
+
258
+ Pass `{ light, dark }` to `logo` so the mark swaps with the theme (e.g. a dark wordmark on
259
+ light, a light one on dark):
260
+
261
+ ```tsx
262
+ <CookieConsent
263
+ logo={{
264
+ light: <img src="/logo.svg" alt="Acme" height={20} />,
265
+ dark: <img src="/logo-dark.svg" alt="Acme" height={20} />,
210
266
  }}
211
267
  />
212
268
  ```
package/dist/index.cjs CHANGED
@@ -600,11 +600,11 @@ function resolveLocale(locale) {
600
600
 
601
601
  // src/styles.ts
602
602
  var CSS = `
603
- .tc-root,.tc-fab{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif;box-sizing:border-box}
603
+ .tc-root,.tc-fab{font-family:var(--tc-font,-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif);box-sizing:border-box}
604
604
  .tc-root *,.tc-fab *{box-sizing:border-box}
605
605
  .tc-root{position:fixed;inset:0;z-index:2147483600;display:flex;align-items:center;justify-content:center;padding:0}
606
606
  .tc-backdrop{position:absolute;inset:0;background:var(--tc-backdrop);backdrop-filter:blur(4px)}
607
- .tc-card{position:relative;display:flex;flex-direction:column;width:100%;height:100dvh;overflow:hidden;background:var(--tc-surface);box-shadow:0 30px 80px -20px rgba(0,0,0,.7);opacity:0;transform:translateY(18px);transition:opacity .26s ease,transform .26s cubic-bezier(.22,1,.36,1)}
607
+ .tc-card{position:relative;display:flex;flex-direction:column;width:100%;min-width:0;height:100dvh;overflow:hidden;background:var(--tc-surface);box-shadow:0 30px 80px -20px rgba(0,0,0,.7);opacity:0;transform:translateY(18px);transition:opacity .26s ease,transform .26s cubic-bezier(.22,1,.36,1)}
608
608
  .tc-card:focus{outline:none}
609
609
  .tc-card.tc-shown{opacity:1;transform:none}
610
610
  .tc-head{display:flex;align-items:baseline;justify-content:space-between;gap:1rem;padding:1.25rem 1.5rem 0}
@@ -614,7 +614,7 @@ var CSS = `
614
614
  .tc-tab.tc-active{color:var(--tc-text)}
615
615
  .tc-tab.tc-active::after{content:'';position:absolute;left:0;right:0;bottom:-1px;height:2px;border-radius:2px;background:var(--tc-brand)}
616
616
  .tc-logo{flex-shrink:0;color:var(--tc-text);font-weight:700;font-size:15px}
617
- .tc-hr{border:0;border-top:1px solid rgba(255,255,255,.07);margin:0}
617
+ .tc-hr{border:0;border-top:1px solid var(--tc-line,rgba(128,128,128,.16));margin:0}
618
618
  .tc-body{flex:1;min-height:0;overflow-y:auto}
619
619
  .tc-panel{padding:1.75rem 1.5rem}
620
620
  .tc-h2{margin:0;font-size:1rem;font-weight:600;color:var(--tc-text)}
@@ -625,18 +625,18 @@ var CSS = `
625
625
  .tc-cat-desc{margin:.375rem 0 0;flex:1;font-size:11.5px;line-height:1.35;color:var(--tc-text-muted)}
626
626
  .tc-switch{position:relative;display:inline-flex;align-items:center;margin-top:1rem;cursor:pointer}
627
627
  .tc-switch input{position:absolute;width:1px;height:1px;opacity:0;margin:0}
628
- .tc-track{width:2.75rem;height:1.5rem;border-radius:999px;background:rgba(255,255,255,.15);transition:background .2s}
628
+ .tc-track{width:2.75rem;height:1.5rem;border-radius:999px;background:var(--tc-track-off,rgba(128,128,128,.32));transition:background .2s}
629
629
  .tc-thumb{position:absolute;left:2px;top:2px;width:1.25rem;height:1.25rem;border-radius:999px;background:var(--tc-text);transition:transform .2s}
630
630
  .tc-switch input:checked~.tc-track{background:var(--tc-brand)}
631
631
  .tc-switch input:checked~.tc-thumb{transform:translateX(1.25rem);background:var(--tc-on-brand)}
632
632
  .tc-switch input:disabled{cursor:default}
633
633
  .tc-switch input:focus-visible~.tc-track{outline:2px solid var(--tc-brand);outline-offset:2px}
634
- .tc-acc{padding:1.25rem 0;border-top:1px solid rgba(255,255,255,.07)}
634
+ .tc-acc{padding:1.25rem 0;border-top:1px solid var(--tc-line,rgba(128,128,128,.16))}
635
635
  .tc-acc:first-child{border-top:0}
636
636
  .tc-acc-head{display:flex;align-items:center;gap:.75rem}
637
637
  .tc-acc-btn{display:flex;flex:1;align-items:center;gap:.625rem;text-align:left;background:none;border:0;color:var(--tc-text);cursor:pointer;font:inherit}
638
638
  .tc-acc-name{font-size:.875rem;font-weight:600}
639
- .tc-badge{display:grid;place-items:center;min-width:1.25rem;height:1.25rem;padding:0 .375rem;border-radius:999px;background:rgba(255,255,255,.1);font-size:10px;color:var(--tc-text-muted);font-family:ui-monospace,monospace}
639
+ .tc-badge{display:grid;place-items:center;min-width:1.25rem;height:1.25rem;padding:0 .375rem;border-radius:999px;background:var(--tc-badge-bg,rgba(128,128,128,.2));font-size:10px;color:var(--tc-text-muted);font-family:ui-monospace,monospace}
640
640
  .tc-chev{flex-shrink:0;color:var(--tc-text-muted);transition:transform .2s}
641
641
  .tc-acc.tc-open .tc-chev{transform:rotate(180deg)}
642
642
  .tc-acc-about{margin:.75rem 0 0;padding-left:1.625rem;font-size:13px;line-height:1.6;color:var(--tc-text-muted)}
@@ -644,19 +644,19 @@ var CSS = `
644
644
  .tc-acc.tc-open .tc-acc-panel{grid-template-rows:1fr}
645
645
  .tc-acc-inner{overflow:hidden;min-height:0;display:flex;flex-direction:column;gap:.5rem}
646
646
  .tc-acc.tc-open .tc-acc-inner{padding-top:.75rem}
647
- .tc-cookie{border-radius:.75rem;background:rgba(255,255,255,.03);padding:.75rem}
647
+ .tc-cookie{border-radius:.75rem;background:var(--tc-cookie-bg,rgba(128,128,128,.06));padding:.75rem}
648
648
  .tc-cookie-name{margin:0;font-family:ui-monospace,monospace;font-size:12px;color:var(--tc-text)}
649
649
  .tc-cookie-purpose{margin:.25rem 0 0;font-size:12px;color:var(--tc-text-muted)}
650
650
  .tc-cookie-meta{margin:.25rem 0 0;font-family:ui-monospace,monospace;font-size:10.5px;text-transform:uppercase;letter-spacing:.04em;color:var(--tc-text-muted)}
651
651
  .tc-cookie-empty{font-size:12px;font-style:italic;color:var(--tc-text-muted)}
652
652
  .tc-about p{margin:0 0 .75rem;font-size:13.5px;line-height:1.6;color:var(--tc-text-muted)}
653
- .tc-link{color:var(--tc-brand);text-decoration:underline;text-underline-offset:2px}
653
+ .tc-link{color:var(--tc-link,var(--tc-brand));text-decoration:underline;text-underline-offset:2px}
654
654
  .tc-actions{display:flex;flex-direction:column;gap:.625rem;padding:1.25rem 1.5rem}
655
- .tc-btn{flex:1;padding:.8rem 1.25rem;border-radius:999px;font-size:.875rem;font-weight:600;cursor:pointer;border:1px solid rgba(255,255,255,.14);background:transparent;color:var(--tc-text);transition:background .15s,border-color .15s}
656
- .tc-btn:hover{background:rgba(255,255,255,.05)}
655
+ .tc-btn{flex:1;padding:.8rem 1.25rem;border-radius:999px;font-size:.875rem;font-weight:600;cursor:pointer;border:1px solid var(--tc-btn-border,rgba(128,128,128,.3));background:transparent;color:var(--tc-text);transition:background .15s,border-color .15s}
656
+ .tc-btn:hover{background:var(--tc-btn-hover,rgba(128,128,128,.12))}
657
657
  .tc-btn-primary{border-color:transparent;background:var(--tc-brand);color:var(--tc-on-brand)}
658
658
  .tc-btn-primary:hover{background:var(--tc-brand-deep)}
659
- .tc-fab{position:fixed;bottom:1.25rem;left:1.25rem;z-index:2147483600;display:grid;place-items:center;width:2.75rem;height:2.75rem;border-radius:999px;border:0;cursor:pointer;background:var(--tc-brand);color:var(--tc-text);box-shadow:0 12px 30px -8px rgba(0,0,0,.6);transition:background .15s}
659
+ .tc-fab{position:fixed;bottom:1.25rem;left:1.25rem;z-index:2147483600;display:grid;place-items:center;width:2.75rem;height:2.75rem;border-radius:999px;border:0;cursor:pointer;background:var(--tc-brand);color:var(--tc-on-brand);box-shadow:0 12px 30px -8px rgba(0,0,0,.6);transition:background .15s}
660
660
  .tc-fab:hover{background:var(--tc-brand-deep)}
661
661
  @media (min-width:640px){
662
662
  .tc-root{padding:1rem}
@@ -675,6 +675,26 @@ var CSS = `
675
675
  `;
676
676
  var TABS = ["consent", "details", "about"];
677
677
  var EMPTY = { preferences: false, statistics: false, marketing: false };
678
+ var DARK_PALETTE = {
679
+ brand: "#ff5c3a",
680
+ brandDeep: "#eb5535",
681
+ surface: "#0c0814",
682
+ surfaceAlt: "rgba(255,255,255,0.035)",
683
+ text: "#f4ede3",
684
+ textMuted: "#a89cb8",
685
+ backdrop: "rgba(7,5,13,0.8)",
686
+ onBrand: "#0c0814"
687
+ };
688
+ var LIGHT_PALETTE = {
689
+ brand: "#ff5c3a",
690
+ brandDeep: "#eb5535",
691
+ surface: "#ffffff",
692
+ surfaceAlt: "rgba(18,12,28,0.04)",
693
+ text: "#1a1523",
694
+ textMuted: "#6b6577",
695
+ backdrop: "rgba(18,12,28,0.4)",
696
+ onBrand: "#1a1523"
697
+ };
678
698
  var DEFAULT_LABELS = {
679
699
  tabConsent: "Consent",
680
700
  tabDetails: "Details",
@@ -775,9 +795,13 @@ function CookieConsent({
775
795
  locale,
776
796
  defaultOpen = false,
777
797
  defaultTab,
798
+ theme = "auto",
778
799
  colors,
779
800
  categories,
780
801
  labels,
802
+ primaryAction = "save",
803
+ deferOpen = true,
804
+ fontFamily,
781
805
  onChange,
782
806
  autoClearCookies = true
783
807
  }) {
@@ -787,6 +811,11 @@ function CookieConsent({
787
811
  const [tab, setTab] = react.useState(defaultTab ?? "consent");
788
812
  const [choices, setChoices] = react.useState(EMPTY);
789
813
  const [accOpen, setAccOpen] = react.useState({});
814
+ const [sysDark, setSysDark] = react.useState(() => {
815
+ if (typeof window === "undefined" || typeof window.matchMedia !== "function") return true;
816
+ return window.matchMedia("(prefers-color-scheme: dark)").matches;
817
+ });
818
+ const resolvedTheme = theme === "light" ? "light" : theme === "dark" ? "dark" : sysDark ? "dark" : "light";
790
819
  const cardRef = react.useRef(null);
791
820
  const fabRef = react.useRef(null);
792
821
  const restoreFocusRef = react.useRef(null);
@@ -828,7 +857,33 @@ function CookieConsent({
828
857
  applyConsent(saved);
829
858
  setDecided(true);
830
859
  }
831
- if (!saved || defaultOpen) setOpen(true);
860
+ if (defaultOpen) {
861
+ setOpen(true);
862
+ return;
863
+ }
864
+ if (saved) return;
865
+ if (!deferOpen || typeof window === "undefined") {
866
+ setOpen(true);
867
+ return;
868
+ }
869
+ let cancel = () => {
870
+ };
871
+ const openNow = () => setOpen(true);
872
+ const schedule = () => {
873
+ const ric = window.requestIdleCallback;
874
+ if (typeof ric === "function") {
875
+ const id = ric(openNow, { timeout: 600 });
876
+ cancel = () => window.cancelIdleCallback?.(id);
877
+ } else {
878
+ const id = window.setTimeout(openNow, 600);
879
+ cancel = () => window.clearTimeout(id);
880
+ }
881
+ };
882
+ const raf = requestAnimationFrame(() => requestAnimationFrame(schedule));
883
+ return () => {
884
+ cancelAnimationFrame(raf);
885
+ cancel();
886
+ };
832
887
  }, []);
833
888
  react.useEffect(() => {
834
889
  if (!open) {
@@ -877,6 +932,18 @@ function CookieConsent({
877
932
  window.addEventListener("keydown", onKey);
878
933
  return () => window.removeEventListener("keydown", onKey);
879
934
  }, [decided]);
935
+ react.useEffect(() => {
936
+ if (theme !== "auto" || typeof window === "undefined" || typeof window.matchMedia !== "function") return;
937
+ const mq = window.matchMedia("(prefers-color-scheme: dark)");
938
+ const on = () => setSysDark(mq.matches);
939
+ on();
940
+ if (mq.addEventListener) {
941
+ mq.addEventListener("change", on);
942
+ return () => mq.removeEventListener("change", on);
943
+ }
944
+ mq.addListener(on);
945
+ return () => mq.removeListener(on);
946
+ }, [theme]);
880
947
  const commit = (ch) => {
881
948
  writeCookie(cookieName, ch);
882
949
  applyConsent(ch);
@@ -907,28 +974,32 @@ function CookieConsent({
907
974
  setTab(id);
908
975
  requestAnimationFrame(() => document.getElementById("tc-tab-" + id)?.focus());
909
976
  };
910
- const palette = {
911
- brand: colors?.brand ?? "#ff5c3a",
912
- brandDeep: colors?.brandDeep ?? "#eb5535",
913
- surface: colors?.surface ?? "#0c0814",
914
- surfaceAlt: colors?.surfaceAlt ?? "rgba(255,255,255,0.035)",
915
- text: colors?.text ?? "#f4ede3",
916
- textMuted: colors?.textMuted ?? "#a89cb8",
917
- backdrop: colors?.backdrop ?? "rgba(7,5,13,0.8)",
918
- onBrand: colors?.onBrand ?? colors?.surface ?? "#0c0814"
919
- };
977
+ const userPalette = colors ? "light" in colors || "dark" in colors ? colors[resolvedTheme] : colors : void 0;
978
+ const P = { ...resolvedTheme === "light" ? LIGHT_PALETTE : DARK_PALETTE, ...userPalette };
979
+ const mix = (c, pct) => `color-mix(in srgb, ${c} ${pct}%, transparent)`;
920
980
  const vars = {
921
- "--tc-brand": palette.brand,
922
- "--tc-brand-deep": palette.brandDeep,
923
- "--tc-surface": palette.surface,
924
- "--tc-surface-alt": palette.surfaceAlt,
925
- "--tc-text": palette.text,
926
- "--tc-text-muted": palette.textMuted,
927
- "--tc-backdrop": palette.backdrop,
928
- "--tc-on-brand": palette.onBrand
981
+ "--tc-brand": P.brand,
982
+ "--tc-brand-deep": P.brandDeep,
983
+ "--tc-surface": P.surface,
984
+ "--tc-surface-alt": P.surfaceAlt,
985
+ "--tc-text": P.text,
986
+ "--tc-text-muted": P.textMuted,
987
+ "--tc-backdrop": P.backdrop,
988
+ "--tc-on-brand": P.onBrand,
989
+ "--tc-line": P.border ?? mix(P.text, 7),
990
+ "--tc-btn-border": P.border ?? mix(P.text, 14),
991
+ "--tc-btn-hover": P.hover ?? mix(P.text, 6),
992
+ "--tc-track-off": P.trackOff ?? mix(P.text, 15),
993
+ "--tc-badge-bg": P.badgeBg ?? mix(P.text, 10),
994
+ "--tc-cookie-bg": P.cookieBg ?? mix(P.text, 3),
995
+ "--tc-link": P.link ?? P.brand,
996
+ "--tc-font": fontFamily
929
997
  };
998
+ const logoNode = logo && typeof logo === "object" && !("$$typeof" in logo) && ("light" in logo || "dark" in logo) ? logo[resolvedTheme] : logo;
930
999
  const tabLabel = (t) => t === "consent" ? L.tabConsent : t === "details" ? L.tabDetails : L.tabAbout;
931
1000
  const hasSelection = choices.preferences || choices.statistics || choices.marketing;
1001
+ const savePrimary = primaryAction === "save" && hasSelection;
1002
+ const allowPrimary = primaryAction === "allowAll";
932
1003
  return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
933
1004
  /* @__PURE__ */ jsxRuntime.jsx("style", { children: CSS }),
934
1005
  open && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "tc-root", style: vars, role: "dialog", "aria-modal": "true", "aria-label": L.dialogLabel, children: [
@@ -951,7 +1022,7 @@ function CookieConsent({
951
1022
  },
952
1023
  t
953
1024
  )) }),
954
- logo ?? (company ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "tc-logo", children: company }) : null)
1025
+ logoNode ?? (company ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "tc-logo", children: company }) : null)
955
1026
  ] }),
956
1027
  /* @__PURE__ */ jsxRuntime.jsx("hr", { className: "tc-hr" }),
957
1028
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "tc-body", children: [
@@ -997,8 +1068,8 @@ function CookieConsent({
997
1068
  /* @__PURE__ */ jsxRuntime.jsx("hr", { className: "tc-hr" }),
998
1069
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "tc-actions", children: [
999
1070
  /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "tc-btn", onClick: denyAll, children: L.deny }),
1000
- /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "tc-btn" + (hasSelection ? " tc-btn-primary" : ""), onClick: saveChoices, children: L.save }),
1001
- /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "tc-btn", onClick: acceptAll, children: L.allowAll })
1071
+ /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "tc-btn" + (savePrimary ? " tc-btn-primary" : ""), onClick: saveChoices, children: L.save }),
1072
+ /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "tc-btn" + (allowPrimary ? " tc-btn-primary" : ""), onClick: acceptAll, children: L.allowAll })
1002
1073
  ] })
1003
1074
  ] })
1004
1075
  ] }),
@@ -1015,5 +1086,3 @@ exports.LOCALES = LOCALES;
1015
1086
  exports.SUPPORTED_LOCALES = SUPPORTED_LOCALES;
1016
1087
  exports.default = CookieConsent_default;
1017
1088
  exports.resolveLocale = resolveLocale;
1018
- //# sourceMappingURL=index.cjs.map
1019
- //# sourceMappingURL=index.cjs.map
package/dist/index.d.cts CHANGED
@@ -29,6 +29,45 @@ interface CategoryContent {
29
29
  /** Cookies in this category (the count badge is derived from this list). */
30
30
  cookies?: CookieInfo[];
31
31
  }
32
+ /** A single theme's colour palette. Every key is optional; anything omitted falls
33
+ * back to the built-in default for the active theme. The last six ("overlay")
34
+ * tokens are derived from `text` via `color-mix` when omitted, so a light theme
35
+ * works from `brand`/`surface`/`text` alone — override them only for fine control. */
36
+ interface Palette {
37
+ brand?: string;
38
+ brandDeep?: string;
39
+ surface?: string;
40
+ surfaceAlt?: string;
41
+ text?: string;
42
+ textMuted?: string;
43
+ backdrop?: string;
44
+ /** Text/icon colour on top of `brand` (primary button, toggle knob, FAB icon). */
45
+ onBrand?: string;
46
+ /** Hairlines and button borders. Default: `color-mix` of `text`. */
47
+ border?: string;
48
+ /** Button hover fill. Default: `color-mix` of `text`. */
49
+ hover?: string;
50
+ /** Toggle track when off. Default: `color-mix` of `text`. */
51
+ trackOff?: string;
52
+ /** Count-badge background. Default: `color-mix` of `text`. */
53
+ badgeBg?: string;
54
+ /** Cookie-row background. Default: `color-mix` of `text`. */
55
+ cookieBg?: string;
56
+ /** Policy-link colour on the About tab. Default: `brand`. */
57
+ link?: string;
58
+ }
59
+ /** The `colors` prop. Either one palette (pinned to both themes — back-compatible
60
+ * with v1) or a `{ light, dark }` pair the component switches between by theme. */
61
+ type ColorsProp = Palette | {
62
+ light?: Palette;
63
+ dark?: Palette;
64
+ };
65
+ /** A brand mark. Either one node (both themes) or a `{ light, dark }` pair the
66
+ * component swaps by the active theme — so the logo follows light/dark too. */
67
+ type LogoProp = ReactNode | {
68
+ light?: ReactNode;
69
+ dark?: ReactNode;
70
+ };
32
71
  /** Per-category overrides. The four keys are fixed (they map to Consent Mode signals). */
33
72
  interface CategoriesConfig {
34
73
  necessary?: CategoryContent;
@@ -69,8 +108,9 @@ interface CookieConsentProps {
69
108
  cookieName?: string;
70
109
  /** Operator / brand name shown in the header and About panel. */
71
110
  company?: string;
72
- /** Optional brand mark for the header (e.g. an <svg/> or <img/>). */
73
- logo?: ReactNode;
111
+ /** Optional brand mark for the header (an <svg/> or <img/>). Pass a single node,
112
+ * or `{ light, dark }` to swap the mark by theme. */
113
+ logo?: LogoProp;
74
114
  privacyUrl?: string;
75
115
  termsUrl?: string;
76
116
  /** Built-in language pack to use (e.g. 'de', 'pl'). English is the default and
@@ -82,23 +122,33 @@ interface CookieConsentProps {
82
122
  defaultOpen?: boolean;
83
123
  /** Tab to show first. Default: 'consent'. */
84
124
  defaultTab?: 'consent' | 'details' | 'about';
85
- /** Brand palette. Defaults match the dark reference theme. For a light theme,
86
- * also set `onBrand` to a colour that reads on top of `brand`. */
87
- colors?: {
88
- brand?: string;
89
- brandDeep?: string;
90
- surface?: string;
91
- surfaceAlt?: string;
92
- text?: string;
93
- textMuted?: string;
94
- backdrop?: string;
95
- /** Text/icon colour on top of `brand` (primary button). Default: `surface`. */
96
- onBrand?: string;
97
- };
125
+ /** Which theme to render. `'auto'` (default) follows the OS `prefers-color-scheme`
126
+ * and re-renders when it changes; `'light'`/`'dark'` pin it. Only affects which
127
+ * half of a `{ light, dark }` `colors`/`logo` is used — a single palette applies
128
+ * to both themes. */
129
+ theme?: 'auto' | 'light' | 'dark';
130
+ /** Brand palette. Either one palette (applied to both themes) or `{ light, dark }`.
131
+ * Built-in defaults cover both themes; a light theme works from `brand`/`surface`/
132
+ * `text` alone (the overlay tokens are derived from `text`). */
133
+ colors?: ColorsProp;
98
134
  /** Per-category content overrides (names, descriptions, cookie tables). */
99
135
  categories?: CategoriesConfig;
100
136
  /** UI string overrides for localisation. */
101
137
  labels?: ConsentLabels;
138
+ /** Which action button gets primary (filled/brand) emphasis.
139
+ * - `'save'` (default): "Save choices" is emphasised only once the visitor turns
140
+ * an optional category on — Accept/Deny stay equal-weight otherwise, the
141
+ * GDPR-friendly default (regulators expect no dark patterns).
142
+ * - `'allowAll'`: "Allow all" is always emphasised (familiar consumer look).
143
+ * - `'none'`: no button is ever emphasised. */
144
+ primaryAction?: 'save' | 'allowAll' | 'none';
145
+ /** Defer the first-visit auto-open until the browser is idle (double-rAF +
146
+ * `requestIdleCallback`, capped ~600ms). Keeps the full-screen card from being
147
+ * the only thing painted on first frame, which otherwise starves Lighthouse of
148
+ * an LCP candidate (`NO_LCP`). Ignored when `defaultOpen`. Default: true. */
149
+ deferOpen?: boolean;
150
+ /** Override the font stack. Default: the system UI stack. */
151
+ fontFamily?: string;
102
152
  /** Fired after the user makes (or changes) a choice. */
103
153
  onChange?: (choices: Choices) => void;
104
154
  /** When a category is denied or revoked, delete the cookies declared in its
@@ -107,7 +157,7 @@ interface CookieConsentProps {
107
157
  autoClearCookies?: boolean;
108
158
  }
109
159
 
110
- declare function CookieConsent({ manageDefault, cookieName, company, logo, privacyUrl, termsUrl, locale, defaultOpen, defaultTab, colors, categories, labels, onChange, autoClearCookies, }: CookieConsentProps): react.JSX.Element;
160
+ declare function CookieConsent({ manageDefault, cookieName, company, logo, privacyUrl, termsUrl, locale, defaultOpen, defaultTab, theme, colors, categories, labels, primaryAction, deferOpen, fontFamily, onChange, autoClearCookies, }: CookieConsentProps): react.JSX.Element;
111
161
 
112
162
  /** Localised name + descriptions for one category. */
113
163
  interface LocaleCategoryText {
@@ -154,4 +204,4 @@ declare const SUPPORTED_LOCALES: readonly ["en", "de", "fr", "es", "it", "pt", "
154
204
  * Case-insensitive and region-tolerant: 'pt-BR' falls back to 'pt'. */
155
205
  declare function resolveLocale(locale?: string): LocalePack | undefined;
156
206
 
157
- export { type CategoriesConfig, type CategoryContent, type Choices, type ConsentLabels, CookieConsent, type CookieConsentProps, type CookieInfo, LOCALES, type LocaleCategoryText, type LocaleCookieText, type LocalePack, SUPPORTED_LOCALES, CookieConsent as default, resolveLocale };
207
+ export { type CategoriesConfig, type CategoryContent, type Choices, type ColorsProp, type ConsentLabels, CookieConsent, type CookieConsentProps, type CookieInfo, LOCALES, type LocaleCategoryText, type LocaleCookieText, type LocalePack, type LogoProp, type Palette, SUPPORTED_LOCALES, CookieConsent as default, resolveLocale };
package/dist/index.d.ts CHANGED
@@ -29,6 +29,45 @@ interface CategoryContent {
29
29
  /** Cookies in this category (the count badge is derived from this list). */
30
30
  cookies?: CookieInfo[];
31
31
  }
32
+ /** A single theme's colour palette. Every key is optional; anything omitted falls
33
+ * back to the built-in default for the active theme. The last six ("overlay")
34
+ * tokens are derived from `text` via `color-mix` when omitted, so a light theme
35
+ * works from `brand`/`surface`/`text` alone — override them only for fine control. */
36
+ interface Palette {
37
+ brand?: string;
38
+ brandDeep?: string;
39
+ surface?: string;
40
+ surfaceAlt?: string;
41
+ text?: string;
42
+ textMuted?: string;
43
+ backdrop?: string;
44
+ /** Text/icon colour on top of `brand` (primary button, toggle knob, FAB icon). */
45
+ onBrand?: string;
46
+ /** Hairlines and button borders. Default: `color-mix` of `text`. */
47
+ border?: string;
48
+ /** Button hover fill. Default: `color-mix` of `text`. */
49
+ hover?: string;
50
+ /** Toggle track when off. Default: `color-mix` of `text`. */
51
+ trackOff?: string;
52
+ /** Count-badge background. Default: `color-mix` of `text`. */
53
+ badgeBg?: string;
54
+ /** Cookie-row background. Default: `color-mix` of `text`. */
55
+ cookieBg?: string;
56
+ /** Policy-link colour on the About tab. Default: `brand`. */
57
+ link?: string;
58
+ }
59
+ /** The `colors` prop. Either one palette (pinned to both themes — back-compatible
60
+ * with v1) or a `{ light, dark }` pair the component switches between by theme. */
61
+ type ColorsProp = Palette | {
62
+ light?: Palette;
63
+ dark?: Palette;
64
+ };
65
+ /** A brand mark. Either one node (both themes) or a `{ light, dark }` pair the
66
+ * component swaps by the active theme — so the logo follows light/dark too. */
67
+ type LogoProp = ReactNode | {
68
+ light?: ReactNode;
69
+ dark?: ReactNode;
70
+ };
32
71
  /** Per-category overrides. The four keys are fixed (they map to Consent Mode signals). */
33
72
  interface CategoriesConfig {
34
73
  necessary?: CategoryContent;
@@ -69,8 +108,9 @@ interface CookieConsentProps {
69
108
  cookieName?: string;
70
109
  /** Operator / brand name shown in the header and About panel. */
71
110
  company?: string;
72
- /** Optional brand mark for the header (e.g. an <svg/> or <img/>). */
73
- logo?: ReactNode;
111
+ /** Optional brand mark for the header (an <svg/> or <img/>). Pass a single node,
112
+ * or `{ light, dark }` to swap the mark by theme. */
113
+ logo?: LogoProp;
74
114
  privacyUrl?: string;
75
115
  termsUrl?: string;
76
116
  /** Built-in language pack to use (e.g. 'de', 'pl'). English is the default and
@@ -82,23 +122,33 @@ interface CookieConsentProps {
82
122
  defaultOpen?: boolean;
83
123
  /** Tab to show first. Default: 'consent'. */
84
124
  defaultTab?: 'consent' | 'details' | 'about';
85
- /** Brand palette. Defaults match the dark reference theme. For a light theme,
86
- * also set `onBrand` to a colour that reads on top of `brand`. */
87
- colors?: {
88
- brand?: string;
89
- brandDeep?: string;
90
- surface?: string;
91
- surfaceAlt?: string;
92
- text?: string;
93
- textMuted?: string;
94
- backdrop?: string;
95
- /** Text/icon colour on top of `brand` (primary button). Default: `surface`. */
96
- onBrand?: string;
97
- };
125
+ /** Which theme to render. `'auto'` (default) follows the OS `prefers-color-scheme`
126
+ * and re-renders when it changes; `'light'`/`'dark'` pin it. Only affects which
127
+ * half of a `{ light, dark }` `colors`/`logo` is used — a single palette applies
128
+ * to both themes. */
129
+ theme?: 'auto' | 'light' | 'dark';
130
+ /** Brand palette. Either one palette (applied to both themes) or `{ light, dark }`.
131
+ * Built-in defaults cover both themes; a light theme works from `brand`/`surface`/
132
+ * `text` alone (the overlay tokens are derived from `text`). */
133
+ colors?: ColorsProp;
98
134
  /** Per-category content overrides (names, descriptions, cookie tables). */
99
135
  categories?: CategoriesConfig;
100
136
  /** UI string overrides for localisation. */
101
137
  labels?: ConsentLabels;
138
+ /** Which action button gets primary (filled/brand) emphasis.
139
+ * - `'save'` (default): "Save choices" is emphasised only once the visitor turns
140
+ * an optional category on — Accept/Deny stay equal-weight otherwise, the
141
+ * GDPR-friendly default (regulators expect no dark patterns).
142
+ * - `'allowAll'`: "Allow all" is always emphasised (familiar consumer look).
143
+ * - `'none'`: no button is ever emphasised. */
144
+ primaryAction?: 'save' | 'allowAll' | 'none';
145
+ /** Defer the first-visit auto-open until the browser is idle (double-rAF +
146
+ * `requestIdleCallback`, capped ~600ms). Keeps the full-screen card from being
147
+ * the only thing painted on first frame, which otherwise starves Lighthouse of
148
+ * an LCP candidate (`NO_LCP`). Ignored when `defaultOpen`. Default: true. */
149
+ deferOpen?: boolean;
150
+ /** Override the font stack. Default: the system UI stack. */
151
+ fontFamily?: string;
102
152
  /** Fired after the user makes (or changes) a choice. */
103
153
  onChange?: (choices: Choices) => void;
104
154
  /** When a category is denied or revoked, delete the cookies declared in its
@@ -107,7 +157,7 @@ interface CookieConsentProps {
107
157
  autoClearCookies?: boolean;
108
158
  }
109
159
 
110
- declare function CookieConsent({ manageDefault, cookieName, company, logo, privacyUrl, termsUrl, locale, defaultOpen, defaultTab, colors, categories, labels, onChange, autoClearCookies, }: CookieConsentProps): react.JSX.Element;
160
+ declare function CookieConsent({ manageDefault, cookieName, company, logo, privacyUrl, termsUrl, locale, defaultOpen, defaultTab, theme, colors, categories, labels, primaryAction, deferOpen, fontFamily, onChange, autoClearCookies, }: CookieConsentProps): react.JSX.Element;
111
161
 
112
162
  /** Localised name + descriptions for one category. */
113
163
  interface LocaleCategoryText {
@@ -154,4 +204,4 @@ declare const SUPPORTED_LOCALES: readonly ["en", "de", "fr", "es", "it", "pt", "
154
204
  * Case-insensitive and region-tolerant: 'pt-BR' falls back to 'pt'. */
155
205
  declare function resolveLocale(locale?: string): LocalePack | undefined;
156
206
 
157
- export { type CategoriesConfig, type CategoryContent, type Choices, type ConsentLabels, CookieConsent, type CookieConsentProps, type CookieInfo, LOCALES, type LocaleCategoryText, type LocaleCookieText, type LocalePack, SUPPORTED_LOCALES, CookieConsent as default, resolveLocale };
207
+ export { type CategoriesConfig, type CategoryContent, type Choices, type ColorsProp, type ConsentLabels, CookieConsent, type CookieConsentProps, type CookieInfo, LOCALES, type LocaleCategoryText, type LocaleCookieText, type LocalePack, type LogoProp, type Palette, SUPPORTED_LOCALES, CookieConsent as default, resolveLocale };