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 +69 -14
- package/dist/index.cjs +104 -33
- package/dist/index.d.cts +67 -17
- package/dist/index.d.ts +67 -17
- package/dist/index.js +104 -33
- package/package.json +7 -4
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
|
|
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
|
-
| `
|
|
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
|
|
204
|
+
## Light & dark themes
|
|
196
205
|
|
|
197
|
-
|
|
198
|
-
|
|
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
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
656
|
-
.tc-btn:hover{background:rgba(
|
|
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-
|
|
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 (
|
|
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
|
|
911
|
-
|
|
912
|
-
|
|
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":
|
|
922
|
-
"--tc-brand-deep":
|
|
923
|
-
"--tc-surface":
|
|
924
|
-
"--tc-surface-alt":
|
|
925
|
-
"--tc-text":
|
|
926
|
-
"--tc-text-muted":
|
|
927
|
-
"--tc-backdrop":
|
|
928
|
-
"--tc-on-brand":
|
|
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
|
-
|
|
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" + (
|
|
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 (
|
|
73
|
-
|
|
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
|
-
/**
|
|
86
|
-
*
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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 (
|
|
73
|
-
|
|
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
|
-
/**
|
|
86
|
-
*
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
652
|
-
.tc-btn:hover{background:rgba(
|
|
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-
|
|
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 (
|
|
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
|
|
907
|
-
|
|
908
|
-
|
|
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":
|
|
918
|
-
"--tc-brand-deep":
|
|
919
|
-
"--tc-surface":
|
|
920
|
-
"--tc-surface-alt":
|
|
921
|
-
"--tc-text":
|
|
922
|
-
"--tc-text-muted":
|
|
923
|
-
"--tc-backdrop":
|
|
924
|
-
"--tc-on-brand":
|
|
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
|
-
|
|
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" + (
|
|
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": "
|
|
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": "^
|
|
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": "^
|
|
66
|
-
"vitest": "^4.1.
|
|
68
|
+
"vite": "^8.0.16",
|
|
69
|
+
"vitest": "^4.1.9"
|
|
67
70
|
}
|
|
68
71
|
}
|