consentric 1.0.1 → 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
@@ -66,10 +66,14 @@ npm run example # opens http://localhost:5173
66
66
  - **10 built-in languages** — English (default), German, French, Spanish, Italian,
67
67
  Portuguese, Dutch, Polish, Czech and Slovak, picked with one `locale` prop. Every
68
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.
69
72
  - **Accessible** — focus-trapped modal, focus restored on close, arrow-key tab
70
73
  navigation, full ARIA wiring, and `prefers-reduced-motion` support.
71
- - **No dark patterns** — "Allow all" and "Deny" get equal visual weight, the way EU
72
- 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"`.
73
77
  - **Works with Next.js** — ships as a Client Component (`"use client"`), so it drops
74
78
  straight into the App Router.
75
79
  - **Zero dependencies, zero Tailwind** — styles are scoped and injected; colours come
@@ -115,9 +119,14 @@ the root of your app.
115
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`. |
116
120
  | `defaultOpen` | `false` | Open the dialog on mount even if a choice is stored (for previews/Storybook). |
117
121
  | `defaultTab` | `'consent'` | Tab to show first: `consent` / `details` / `about`. |
118
- | `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. |
119
125
  | `categories` | built-in EN | Per-category content overrides — see below. |
120
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. |
121
130
  | `onChange` | — | `(choices) => void`, fired after the user makes or changes a choice. |
122
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. |
123
132
 
@@ -192,22 +201,68 @@ language.
192
201
  />
193
202
  ```
194
203
 
195
- ## Light theme
204
+ ## Light & dark themes
196
205
 
197
- Defaults assume a dark `surface`. For a light theme, also set `onBrand` (the text/icon
198
- 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:
199
225
 
200
226
  ```tsx
201
227
  <CookieConsent
228
+ theme="auto" // 'auto' | 'light' | 'dark'
202
229
  colors={{
203
- brand: '#2563eb',
204
- brandDeep: '#1d4ed8',
205
- surface: '#ffffff',
206
- surfaceAlt: 'rgba(0,0,0,0.04)',
207
- text: '#0f172a',
208
- textMuted: '#64748b',
209
- backdrop: 'rgba(15,23,42,0.4)',
210
- 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} />,
211
266
  }}
212
267
  />
213
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
  ] }),
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 };
package/dist/index.js CHANGED
@@ -596,11 +596,11 @@ function resolveLocale(locale) {
596
596
 
597
597
  // src/styles.ts
598
598
  var CSS = `
599
- .tc-root,.tc-fab{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif;box-sizing:border-box}
599
+ .tc-root,.tc-fab{font-family:var(--tc-font,-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif);box-sizing:border-box}
600
600
  .tc-root *,.tc-fab *{box-sizing:border-box}
601
601
  .tc-root{position:fixed;inset:0;z-index:2147483600;display:flex;align-items:center;justify-content:center;padding:0}
602
602
  .tc-backdrop{position:absolute;inset:0;background:var(--tc-backdrop);backdrop-filter:blur(4px)}
603
- .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)}
603
+ .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)}
604
604
  .tc-card:focus{outline:none}
605
605
  .tc-card.tc-shown{opacity:1;transform:none}
606
606
  .tc-head{display:flex;align-items:baseline;justify-content:space-between;gap:1rem;padding:1.25rem 1.5rem 0}
@@ -610,7 +610,7 @@ var CSS = `
610
610
  .tc-tab.tc-active{color:var(--tc-text)}
611
611
  .tc-tab.tc-active::after{content:'';position:absolute;left:0;right:0;bottom:-1px;height:2px;border-radius:2px;background:var(--tc-brand)}
612
612
  .tc-logo{flex-shrink:0;color:var(--tc-text);font-weight:700;font-size:15px}
613
- .tc-hr{border:0;border-top:1px solid rgba(255,255,255,.07);margin:0}
613
+ .tc-hr{border:0;border-top:1px solid var(--tc-line,rgba(128,128,128,.16));margin:0}
614
614
  .tc-body{flex:1;min-height:0;overflow-y:auto}
615
615
  .tc-panel{padding:1.75rem 1.5rem}
616
616
  .tc-h2{margin:0;font-size:1rem;font-weight:600;color:var(--tc-text)}
@@ -621,18 +621,18 @@ var CSS = `
621
621
  .tc-cat-desc{margin:.375rem 0 0;flex:1;font-size:11.5px;line-height:1.35;color:var(--tc-text-muted)}
622
622
  .tc-switch{position:relative;display:inline-flex;align-items:center;margin-top:1rem;cursor:pointer}
623
623
  .tc-switch input{position:absolute;width:1px;height:1px;opacity:0;margin:0}
624
- .tc-track{width:2.75rem;height:1.5rem;border-radius:999px;background:rgba(255,255,255,.15);transition:background .2s}
624
+ .tc-track{width:2.75rem;height:1.5rem;border-radius:999px;background:var(--tc-track-off,rgba(128,128,128,.32));transition:background .2s}
625
625
  .tc-thumb{position:absolute;left:2px;top:2px;width:1.25rem;height:1.25rem;border-radius:999px;background:var(--tc-text);transition:transform .2s}
626
626
  .tc-switch input:checked~.tc-track{background:var(--tc-brand)}
627
627
  .tc-switch input:checked~.tc-thumb{transform:translateX(1.25rem);background:var(--tc-on-brand)}
628
628
  .tc-switch input:disabled{cursor:default}
629
629
  .tc-switch input:focus-visible~.tc-track{outline:2px solid var(--tc-brand);outline-offset:2px}
630
- .tc-acc{padding:1.25rem 0;border-top:1px solid rgba(255,255,255,.07)}
630
+ .tc-acc{padding:1.25rem 0;border-top:1px solid var(--tc-line,rgba(128,128,128,.16))}
631
631
  .tc-acc:first-child{border-top:0}
632
632
  .tc-acc-head{display:flex;align-items:center;gap:.75rem}
633
633
  .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}
634
634
  .tc-acc-name{font-size:.875rem;font-weight:600}
635
- .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}
635
+ .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}
636
636
  .tc-chev{flex-shrink:0;color:var(--tc-text-muted);transition:transform .2s}
637
637
  .tc-acc.tc-open .tc-chev{transform:rotate(180deg)}
638
638
  .tc-acc-about{margin:.75rem 0 0;padding-left:1.625rem;font-size:13px;line-height:1.6;color:var(--tc-text-muted)}
@@ -640,19 +640,19 @@ var CSS = `
640
640
  .tc-acc.tc-open .tc-acc-panel{grid-template-rows:1fr}
641
641
  .tc-acc-inner{overflow:hidden;min-height:0;display:flex;flex-direction:column;gap:.5rem}
642
642
  .tc-acc.tc-open .tc-acc-inner{padding-top:.75rem}
643
- .tc-cookie{border-radius:.75rem;background:rgba(255,255,255,.03);padding:.75rem}
643
+ .tc-cookie{border-radius:.75rem;background:var(--tc-cookie-bg,rgba(128,128,128,.06));padding:.75rem}
644
644
  .tc-cookie-name{margin:0;font-family:ui-monospace,monospace;font-size:12px;color:var(--tc-text)}
645
645
  .tc-cookie-purpose{margin:.25rem 0 0;font-size:12px;color:var(--tc-text-muted)}
646
646
  .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)}
647
647
  .tc-cookie-empty{font-size:12px;font-style:italic;color:var(--tc-text-muted)}
648
648
  .tc-about p{margin:0 0 .75rem;font-size:13.5px;line-height:1.6;color:var(--tc-text-muted)}
649
- .tc-link{color:var(--tc-brand);text-decoration:underline;text-underline-offset:2px}
649
+ .tc-link{color:var(--tc-link,var(--tc-brand));text-decoration:underline;text-underline-offset:2px}
650
650
  .tc-actions{display:flex;flex-direction:column;gap:.625rem;padding:1.25rem 1.5rem}
651
- .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}
652
- .tc-btn:hover{background:rgba(255,255,255,.05)}
651
+ .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}
652
+ .tc-btn:hover{background:var(--tc-btn-hover,rgba(128,128,128,.12))}
653
653
  .tc-btn-primary{border-color:transparent;background:var(--tc-brand);color:var(--tc-on-brand)}
654
654
  .tc-btn-primary:hover{background:var(--tc-brand-deep)}
655
- .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}
655
+ .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}
656
656
  .tc-fab:hover{background:var(--tc-brand-deep)}
657
657
  @media (min-width:640px){
658
658
  .tc-root{padding:1rem}
@@ -671,6 +671,26 @@ var CSS = `
671
671
  `;
672
672
  var TABS = ["consent", "details", "about"];
673
673
  var EMPTY = { preferences: false, statistics: false, marketing: false };
674
+ var DARK_PALETTE = {
675
+ brand: "#ff5c3a",
676
+ brandDeep: "#eb5535",
677
+ surface: "#0c0814",
678
+ surfaceAlt: "rgba(255,255,255,0.035)",
679
+ text: "#f4ede3",
680
+ textMuted: "#a89cb8",
681
+ backdrop: "rgba(7,5,13,0.8)",
682
+ onBrand: "#0c0814"
683
+ };
684
+ var LIGHT_PALETTE = {
685
+ brand: "#ff5c3a",
686
+ brandDeep: "#eb5535",
687
+ surface: "#ffffff",
688
+ surfaceAlt: "rgba(18,12,28,0.04)",
689
+ text: "#1a1523",
690
+ textMuted: "#6b6577",
691
+ backdrop: "rgba(18,12,28,0.4)",
692
+ onBrand: "#1a1523"
693
+ };
674
694
  var DEFAULT_LABELS = {
675
695
  tabConsent: "Consent",
676
696
  tabDetails: "Details",
@@ -771,9 +791,13 @@ function CookieConsent({
771
791
  locale,
772
792
  defaultOpen = false,
773
793
  defaultTab,
794
+ theme = "auto",
774
795
  colors,
775
796
  categories,
776
797
  labels,
798
+ primaryAction = "save",
799
+ deferOpen = true,
800
+ fontFamily,
777
801
  onChange,
778
802
  autoClearCookies = true
779
803
  }) {
@@ -783,6 +807,11 @@ function CookieConsent({
783
807
  const [tab, setTab] = useState(defaultTab ?? "consent");
784
808
  const [choices, setChoices] = useState(EMPTY);
785
809
  const [accOpen, setAccOpen] = useState({});
810
+ const [sysDark, setSysDark] = useState(() => {
811
+ if (typeof window === "undefined" || typeof window.matchMedia !== "function") return true;
812
+ return window.matchMedia("(prefers-color-scheme: dark)").matches;
813
+ });
814
+ const resolvedTheme = theme === "light" ? "light" : theme === "dark" ? "dark" : sysDark ? "dark" : "light";
786
815
  const cardRef = useRef(null);
787
816
  const fabRef = useRef(null);
788
817
  const restoreFocusRef = useRef(null);
@@ -824,7 +853,33 @@ function CookieConsent({
824
853
  applyConsent(saved);
825
854
  setDecided(true);
826
855
  }
827
- if (!saved || defaultOpen) setOpen(true);
856
+ if (defaultOpen) {
857
+ setOpen(true);
858
+ return;
859
+ }
860
+ if (saved) return;
861
+ if (!deferOpen || typeof window === "undefined") {
862
+ setOpen(true);
863
+ return;
864
+ }
865
+ let cancel = () => {
866
+ };
867
+ const openNow = () => setOpen(true);
868
+ const schedule = () => {
869
+ const ric = window.requestIdleCallback;
870
+ if (typeof ric === "function") {
871
+ const id = ric(openNow, { timeout: 600 });
872
+ cancel = () => window.cancelIdleCallback?.(id);
873
+ } else {
874
+ const id = window.setTimeout(openNow, 600);
875
+ cancel = () => window.clearTimeout(id);
876
+ }
877
+ };
878
+ const raf = requestAnimationFrame(() => requestAnimationFrame(schedule));
879
+ return () => {
880
+ cancelAnimationFrame(raf);
881
+ cancel();
882
+ };
828
883
  }, []);
829
884
  useEffect(() => {
830
885
  if (!open) {
@@ -873,6 +928,18 @@ function CookieConsent({
873
928
  window.addEventListener("keydown", onKey);
874
929
  return () => window.removeEventListener("keydown", onKey);
875
930
  }, [decided]);
931
+ useEffect(() => {
932
+ if (theme !== "auto" || typeof window === "undefined" || typeof window.matchMedia !== "function") return;
933
+ const mq = window.matchMedia("(prefers-color-scheme: dark)");
934
+ const on = () => setSysDark(mq.matches);
935
+ on();
936
+ if (mq.addEventListener) {
937
+ mq.addEventListener("change", on);
938
+ return () => mq.removeEventListener("change", on);
939
+ }
940
+ mq.addListener(on);
941
+ return () => mq.removeListener(on);
942
+ }, [theme]);
876
943
  const commit = (ch) => {
877
944
  writeCookie(cookieName, ch);
878
945
  applyConsent(ch);
@@ -903,28 +970,32 @@ function CookieConsent({
903
970
  setTab(id);
904
971
  requestAnimationFrame(() => document.getElementById("tc-tab-" + id)?.focus());
905
972
  };
906
- const palette = {
907
- brand: colors?.brand ?? "#ff5c3a",
908
- brandDeep: colors?.brandDeep ?? "#eb5535",
909
- surface: colors?.surface ?? "#0c0814",
910
- surfaceAlt: colors?.surfaceAlt ?? "rgba(255,255,255,0.035)",
911
- text: colors?.text ?? "#f4ede3",
912
- textMuted: colors?.textMuted ?? "#a89cb8",
913
- backdrop: colors?.backdrop ?? "rgba(7,5,13,0.8)",
914
- onBrand: colors?.onBrand ?? colors?.surface ?? "#0c0814"
915
- };
973
+ const userPalette = colors ? "light" in colors || "dark" in colors ? colors[resolvedTheme] : colors : void 0;
974
+ const P = { ...resolvedTheme === "light" ? LIGHT_PALETTE : DARK_PALETTE, ...userPalette };
975
+ const mix = (c, pct) => `color-mix(in srgb, ${c} ${pct}%, transparent)`;
916
976
  const vars = {
917
- "--tc-brand": palette.brand,
918
- "--tc-brand-deep": palette.brandDeep,
919
- "--tc-surface": palette.surface,
920
- "--tc-surface-alt": palette.surfaceAlt,
921
- "--tc-text": palette.text,
922
- "--tc-text-muted": palette.textMuted,
923
- "--tc-backdrop": palette.backdrop,
924
- "--tc-on-brand": palette.onBrand
977
+ "--tc-brand": P.brand,
978
+ "--tc-brand-deep": P.brandDeep,
979
+ "--tc-surface": P.surface,
980
+ "--tc-surface-alt": P.surfaceAlt,
981
+ "--tc-text": P.text,
982
+ "--tc-text-muted": P.textMuted,
983
+ "--tc-backdrop": P.backdrop,
984
+ "--tc-on-brand": P.onBrand,
985
+ "--tc-line": P.border ?? mix(P.text, 7),
986
+ "--tc-btn-border": P.border ?? mix(P.text, 14),
987
+ "--tc-btn-hover": P.hover ?? mix(P.text, 6),
988
+ "--tc-track-off": P.trackOff ?? mix(P.text, 15),
989
+ "--tc-badge-bg": P.badgeBg ?? mix(P.text, 10),
990
+ "--tc-cookie-bg": P.cookieBg ?? mix(P.text, 3),
991
+ "--tc-link": P.link ?? P.brand,
992
+ "--tc-font": fontFamily
925
993
  };
994
+ const logoNode = logo && typeof logo === "object" && !("$$typeof" in logo) && ("light" in logo || "dark" in logo) ? logo[resolvedTheme] : logo;
926
995
  const tabLabel = (t) => t === "consent" ? L.tabConsent : t === "details" ? L.tabDetails : L.tabAbout;
927
996
  const hasSelection = choices.preferences || choices.statistics || choices.marketing;
997
+ const savePrimary = primaryAction === "save" && hasSelection;
998
+ const allowPrimary = primaryAction === "allowAll";
928
999
  return /* @__PURE__ */ jsxs(Fragment, { children: [
929
1000
  /* @__PURE__ */ jsx("style", { children: CSS }),
930
1001
  open && /* @__PURE__ */ jsxs("div", { className: "tc-root", style: vars, role: "dialog", "aria-modal": "true", "aria-label": L.dialogLabel, children: [
@@ -947,7 +1018,7 @@ function CookieConsent({
947
1018
  },
948
1019
  t
949
1020
  )) }),
950
- logo ?? (company ? /* @__PURE__ */ jsx("span", { className: "tc-logo", children: company }) : null)
1021
+ logoNode ?? (company ? /* @__PURE__ */ jsx("span", { className: "tc-logo", children: company }) : null)
951
1022
  ] }),
952
1023
  /* @__PURE__ */ jsx("hr", { className: "tc-hr" }),
953
1024
  /* @__PURE__ */ jsxs("div", { className: "tc-body", children: [
@@ -993,8 +1064,8 @@ function CookieConsent({
993
1064
  /* @__PURE__ */ jsx("hr", { className: "tc-hr" }),
994
1065
  /* @__PURE__ */ jsxs("div", { className: "tc-actions", children: [
995
1066
  /* @__PURE__ */ jsx("button", { type: "button", className: "tc-btn", onClick: denyAll, children: L.deny }),
996
- /* @__PURE__ */ jsx("button", { type: "button", className: "tc-btn" + (hasSelection ? " tc-btn-primary" : ""), onClick: saveChoices, children: L.save }),
997
- /* @__PURE__ */ jsx("button", { type: "button", className: "tc-btn", onClick: acceptAll, children: L.allowAll })
1067
+ /* @__PURE__ */ jsx("button", { type: "button", className: "tc-btn" + (savePrimary ? " tc-btn-primary" : ""), onClick: saveChoices, children: L.save }),
1068
+ /* @__PURE__ */ jsx("button", { type: "button", className: "tc-btn" + (allowPrimary ? " tc-btn-primary" : ""), onClick: acceptAll, children: L.allowAll })
998
1069
  ] })
999
1070
  ] })
1000
1071
  ] }),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "consentric",
3
- "version": "1.0.1",
3
+ "version": "2.0.0",
4
4
  "description": "Self-contained, configurable GDPR cookie consent for React. Google Consent Mode v2, 10 built-in languages, zero dependencies, zero Tailwind.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -51,18 +51,21 @@
51
51
  "peerDependencies": {
52
52
  "react": ">=17"
53
53
  },
54
+ "overrides": {
55
+ "esbuild": "^0.28.1"
56
+ },
54
57
  "devDependencies": {
55
58
  "@testing-library/dom": "^10.4.1",
56
59
  "@testing-library/react": "^16.3.2",
57
60
  "@types/react": "^18.3.31",
58
61
  "@types/react-dom": "^18.3.7",
59
- "@vitejs/plugin-react": "^4.3.4",
62
+ "@vitejs/plugin-react": "^6.0.2",
60
63
  "jsdom": "^29.1.1",
61
64
  "react": "^18.3.1",
62
65
  "react-dom": "^18.3.1",
63
66
  "tsup": "^8.5.1",
64
67
  "typescript": "^5.9.3",
65
- "vite": "^5.4.11",
66
- "vitest": "^4.1.8"
68
+ "vite": "^8.0.16",
69
+ "vitest": "^4.1.9"
67
70
  }
68
71
  }