keksi 0.1.3 → 0.1.5

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
@@ -6,7 +6,7 @@ texts in English, Finnish and Swedish.
6
6
  keksi loads the trackers itself, and only after consent. You don't add their script tags: you put their ids in the
7
7
  config, so there's nothing to block.
8
8
 
9
- - Google Analytics 4, Google Ads, Meta Pixel and YouTube. The short list is deliberate.
9
+ - Google Analytics 4, Google Ads, Meta Pixel, PostHog and YouTube. The short list is deliberate.
10
10
  - About 5 KB gzipped, inline. No extra requests, no dependencies.
11
11
  - Rendered on the server. Works on static pages, with no layout shift.
12
12
  - The cookie policy table is generated from the same data.
@@ -25,6 +25,7 @@ export default defineConfig({
25
25
  ga: 'G-XXXXXXX',
26
26
  googleAds: 'AW-123456789',
27
27
  meta: '1234567890',
28
+ posthog: { key: 'phc_XXXX' }, // EU cloud by default; set host for US or a proxy
28
29
  youtube: true,
29
30
  policyUrl: '/evasteet',
30
31
  });
@@ -82,16 +83,18 @@ string.
82
83
 
83
84
  ## Tracking events
84
85
 
85
- `gtag()` and `fbq()` are always defined, so you can call them anytime:
86
+ `gtag()`, `fbq()` and `posthog` are always defined, so you can call them anytime:
86
87
 
87
88
  ```ts
88
89
  gtag('event', 'generate_lead');
89
90
  fbq('track', 'Lead');
91
+ posthog.capture('signup');
90
92
  ```
91
93
 
92
94
  - Calls made before consent are queued. They're sent only if the visitor consents on that page.
93
95
  - Google gets Consent Mode v2 signals (basic mode).
94
- - GA4 and Meta Pixel track single-page navigation themselves.
96
+ - GA4, Meta Pixel and PostHog track single-page navigation themselves.
97
+ - PostHog counts as analytics. With the US host, the settings say data is transferred to the US.
95
98
 
96
99
  ## YouTube
97
100
 
@@ -99,9 +102,13 @@ fbq('track', 'Lead');
99
102
  <keksi-youtube video="aqz-KE-bpKQ" label="Esittelyvideo" poster="/img/poster.jpg"></keksi-youtube>
100
103
  ```
101
104
 
102
- - Shows a placeholder until the visitor clicks it, then loads that one video from `youtube-nocookie.com`.
103
- - With marketing consent, videos load directly.
104
- - `poster` is optional and should be self-hosted. Thumbnails from YouTube would contact Google before consent.
105
+ - Shows a YouTube-style placeholder. Clicking play is the consent: it's remembered, and all videos load from
106
+ `youtube-nocookie.com` from then on.
107
+ - Marketing consent in the banner covers YouTube too, and turning it off withdraws YouTube.
108
+ - With YouTube as the only provider, the banner doesn't show by itself. The `data-keksi-open` link still opens the
109
+ settings for withdrawing.
110
+ - `poster` is optional and should be self-hosted. YouTube's thumbnails come from `i.ytimg.com`, which would contact
111
+ Google before consent.
105
112
 
106
113
  ## API
107
114
 
package/dist/cli.js CHANGED
@@ -4,6 +4,9 @@
4
4
  import { existsSync, readFileSync, writeFileSync } from "node:fs";
5
5
  import { parseArgs } from "node:util";
6
6
 
7
+ // src/glob.ts
8
+ var glob = (pattern, name) => new RegExp(`^${pattern.split("*").map((s) => s.replace(/[.+?^${}()|[\]\\]/g, "\\$&")).join(".*")}$`).test(name);
9
+
7
10
  // src/providers.ts
8
11
  var GOOGLE = { company: "Google Ireland Limited", privacyPolicy: "https://policies.google.com/privacy", transfer: "dpf" };
9
12
  var META = { company: "Meta Platforms Ireland Limited", privacyPolicy: "https://www.facebook.com/privacy/policy/", transfer: "dpf" };
@@ -64,6 +67,29 @@ var providers = [
64
67
  ],
65
68
  hosts: ["facebook.net", "facebook.com"]
66
69
  },
70
+ {
71
+ id: "posthog",
72
+ name: "PostHog",
73
+ company: "PostHog Inc.",
74
+ privacyPolicy: "https://posthog.com/privacy",
75
+ category: "analytics",
76
+ purpose: {
77
+ fi: "Tuoteanalytiikka: sivujen katselut, klikkaukset ja k\xE4ytt\xF6tavat. Voi tallentaa istuntoja, jos istuntotallenteet ovat k\xE4yt\xF6ss\xE4.",
78
+ sv: "Produktanalys: sidvisningar, klick och anv\xE4ndningsm\xF6nster. Kan spela in sessioner om sessionsinspelning \xE4r aktiverad.",
79
+ en: "Product analytics: page views, clicks and usage patterns. Can record sessions if session replay is enabled."
80
+ },
81
+ cookies: [
82
+ { name: "ph_*_posthog", duration: "1y" },
83
+ { name: "ph_*_posthog_cpm", duration: "1y" },
84
+ { name: "ph_*_posthog", duration: "persistent", storage: "localStorage" },
85
+ { name: "ph_*_posthog__flags", duration: "persistent", storage: "localStorage" },
86
+ { name: "ph_*_posthog", duration: "session", storage: "sessionStorage" },
87
+ { name: "ph_*_primary_window_exists", duration: "session", storage: "sessionStorage" },
88
+ { name: "ph_*_session_registered_properties", duration: "session", storage: "sessionStorage" },
89
+ { name: "ph_*_window_id", duration: "session", storage: "sessionStorage" }
90
+ ],
91
+ hosts: ["posthog.com"]
92
+ },
67
93
  {
68
94
  id: "youtube",
69
95
  name: "YouTube",
@@ -119,7 +145,7 @@ var purposes = {
119
145
  };
120
146
  var trackerNames = providers.flatMap((p) => p.cookies.flatMap((c) => c.name.split(", ")));
121
147
  var trackerHosts = providers.flatMap((p) => p.hosts);
122
- var matches = (patterns, name) => patterns.some((p) => p.endsWith("*") ? name.startsWith(p.slice(0, -1)) : name === p);
148
+ var matches = (patterns, name) => patterns.some((p) => glob(p, name));
123
149
  function purposeOf(name) {
124
150
  const k = Object.keys(purposes).find((p) => matches([p], name));
125
151
  return k ? purposes[k] : void 0;
package/dist/config.d.ts CHANGED
@@ -3,6 +3,9 @@ import type { Category, KeksiConfig, Locale, Provider, RuntimeData } from './typ
3
3
  export declare function defineConfig(config: KeksiConfig): KeksiConfig;
4
4
  export declare function activeProviders(config: KeksiConfig): Provider[];
5
5
  export declare function categoriesOf(config: KeksiConfig): Category[];
6
+ /** False when YouTube is the only provider: a click on a video is the consent, so the banner doesn't show by itself. */
7
+ export declare function needsBanner(config: KeksiConfig): boolean;
8
+ export declare function policyOf(config: KeksiConfig, locale: Locale): string | undefined;
6
9
  export declare function textsFor(config: KeksiConfig, locale: Locale): Texts;
7
10
  /** Changes when a provider is added or removed, which asks everyone again. Ids and texts can change freely. */
8
11
  export declare function version(config: KeksiConfig): string;
package/dist/glob.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ /** `*` matches any run of characters, e.g. `ph_*_posthog`. */
2
+ export declare const glob: (pattern: string, name: string) => boolean;
package/dist/i18n.d.ts CHANGED
@@ -2,7 +2,7 @@ import type { Locale } from './types.js';
2
2
  declare const fi: {
3
3
  title: string;
4
4
  intro: string;
5
- transfer: string;
5
+ change: string;
6
6
  more: string;
7
7
  accept: string;
8
8
  reject: string;
@@ -22,6 +22,7 @@ declare const fi: {
22
22
  consentPurpose: string;
23
23
  privacyPolicy: string;
24
24
  dpf: string;
25
+ us: string;
25
26
  name: string;
26
27
  duration: string;
27
28
  purpose: string;
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
  var fi = {
3
3
  title: "Ev\xE4steet",
4
4
  intro: "K\xE4yt\xE4mme v\xE4ltt\xE4m\xE4tt\xF6mien ev\xE4steiden lis\xE4ksi seuraavia palveluita vain, jos annat siihen luvan:",
5
- transfer: "Palveluntarjoajat voivat siirt\xE4\xE4 tietoja Yhdysvaltoihin. Voit muuttaa valintaasi milloin tahansa.",
5
+ change: "Voit muuttaa valintaasi milloin tahansa.",
6
6
  more: "Lue lis\xE4\xE4",
7
7
  accept: "Hyv\xE4ksy kaikki",
8
8
  reject: "Hylk\xE4\xE4 kaikki",
@@ -22,6 +22,7 @@ var fi = {
22
22
  consentPurpose: "Tallentaa ev\xE4stevalintasi.",
23
23
  privacyPolicy: "Tietosuojaseloste",
24
24
  dpf: "Tietoja voidaan siirt\xE4\xE4 Yhdysvaltoihin EU:n ja Yhdysvaltojen v\xE4lisen tietosuojakehyksen (DPF) perusteella.",
25
+ us: "Tietoja siirret\xE4\xE4n Yhdysvaltoihin.",
25
26
  name: "Nimi",
26
27
  duration: "Kesto",
27
28
  purpose: "Tarkoitus",
@@ -32,14 +33,14 @@ var fi = {
32
33
  mo: "kuukausi|kuukautta",
33
34
  d: "p\xE4iv\xE4|p\xE4iv\xE4\xE4",
34
35
  min: "minuutti|minuuttia",
35
- ytText: "Video ladataan YouTubesta. YouTube (Google) asettaa ev\xE4steit\xE4 ja voi siirt\xE4\xE4 tietoja Yhdysvaltoihin.",
36
- ytButton: "N\xE4yt\xE4 video",
37
- ytOpen: "Avaa YouTubessa"
36
+ ytText: "Toistamalla videon hyv\xE4ksyt YouTuben ev\xE4steet.",
37
+ ytButton: "Toista video",
38
+ ytOpen: "Katso YouTubessa"
38
39
  };
39
40
  var sv = {
40
41
  title: "Kakor",
41
42
  intro: "Ut\xF6ver n\xF6dv\xE4ndiga kakor anv\xE4nder vi f\xF6ljande tj\xE4nster endast om du ger ditt samtycke:",
42
- transfer: "Tj\xE4nsteleverant\xF6rerna kan \xF6verf\xF6ra uppgifter till USA. Du kan \xE4ndra ditt val n\xE4r som helst.",
43
+ change: "Du kan \xE4ndra ditt val n\xE4r som helst.",
43
44
  more: "L\xE4s mer",
44
45
  accept: "Godk\xE4nn alla",
45
46
  reject: "Avvisa alla",
@@ -59,6 +60,7 @@ var sv = {
59
60
  consentPurpose: "Sparar ditt val av kakor.",
60
61
  privacyPolicy: "Dataskyddsbeskrivning",
61
62
  dpf: "Uppgifter kan \xF6verf\xF6ras till USA med st\xF6d av ramverket f\xF6r dataskydd mellan EU och USA (DPF).",
63
+ us: "Uppgifter \xF6verf\xF6rs till USA.",
62
64
  name: "Namn",
63
65
  duration: "Varaktighet",
64
66
  purpose: "Syfte",
@@ -69,14 +71,14 @@ var sv = {
69
71
  mo: "m\xE5nad|m\xE5nader",
70
72
  d: "dag|dagar",
71
73
  min: "minut|minuter",
72
- ytText: "Videon laddas fr\xE5n YouTube. YouTube (Google) s\xE4tter kakor och kan \xF6verf\xF6ra uppgifter till USA.",
73
- ytButton: "Visa video",
74
- ytOpen: "\xD6ppna p\xE5 YouTube"
74
+ ytText: "Genom att spela upp videon godk\xE4nner du kakor fr\xE5n YouTube.",
75
+ ytButton: "Spela upp video",
76
+ ytOpen: "Titta p\xE5 YouTube"
75
77
  };
76
78
  var en = {
77
79
  title: "Cookies",
78
80
  intro: "Besides necessary cookies, we use the following services only if you allow it:",
79
- transfer: "The providers may transfer data to the United States. You can change your choice at any time.",
81
+ change: "You can change your choice at any time.",
80
82
  more: "Read more",
81
83
  accept: "Accept all",
82
84
  reject: "Reject all",
@@ -96,6 +98,7 @@ var en = {
96
98
  consentPurpose: "Stores your cookie choice.",
97
99
  privacyPolicy: "Privacy policy",
98
100
  dpf: "Data may be transferred to the United States under the EU\u2013US Data Privacy Framework (DPF).",
101
+ us: "Data is transferred to the United States.",
99
102
  name: "Name",
100
103
  duration: "Duration",
101
104
  purpose: "Purpose",
@@ -106,9 +109,9 @@ var en = {
106
109
  mo: "month|months",
107
110
  d: "day|days",
108
111
  min: "minute|minutes",
109
- ytText: "The video loads from YouTube. YouTube (Google) sets cookies and may transfer data to the United States.",
110
- ytButton: "Show video",
111
- ytOpen: "Open on YouTube"
112
+ ytText: "By playing the video, you accept cookies from YouTube.",
113
+ ytButton: "Play video",
114
+ ytOpen: "Watch on YouTube"
112
115
  };
113
116
  var texts = { fi, sv, en };
114
117
 
@@ -172,6 +175,29 @@ var providers = [
172
175
  ],
173
176
  hosts: ["facebook.net", "facebook.com"]
174
177
  },
178
+ {
179
+ id: "posthog",
180
+ name: "PostHog",
181
+ company: "PostHog Inc.",
182
+ privacyPolicy: "https://posthog.com/privacy",
183
+ category: "analytics",
184
+ purpose: {
185
+ fi: "Tuoteanalytiikka: sivujen katselut, klikkaukset ja k\xE4ytt\xF6tavat. Voi tallentaa istuntoja, jos istuntotallenteet ovat k\xE4yt\xF6ss\xE4.",
186
+ sv: "Produktanalys: sidvisningar, klick och anv\xE4ndningsm\xF6nster. Kan spela in sessioner om sessionsinspelning \xE4r aktiverad.",
187
+ en: "Product analytics: page views, clicks and usage patterns. Can record sessions if session replay is enabled."
188
+ },
189
+ cookies: [
190
+ { name: "ph_*_posthog", duration: "1y" },
191
+ { name: "ph_*_posthog_cpm", duration: "1y" },
192
+ { name: "ph_*_posthog", duration: "persistent", storage: "localStorage" },
193
+ { name: "ph_*_posthog__flags", duration: "persistent", storage: "localStorage" },
194
+ { name: "ph_*_posthog", duration: "session", storage: "sessionStorage" },
195
+ { name: "ph_*_primary_window_exists", duration: "session", storage: "sessionStorage" },
196
+ { name: "ph_*_session_registered_properties", duration: "session", storage: "sessionStorage" },
197
+ { name: "ph_*_window_id", duration: "session", storage: "sessionStorage" }
198
+ ],
199
+ hosts: ["posthog.com"]
200
+ },
175
201
  {
176
202
  id: "youtube",
177
203
  name: "YouTube",
@@ -200,12 +226,21 @@ function defineConfig(config) {
200
226
  return config;
201
227
  }
202
228
  var list = (v) => v === void 0 ? [] : Array.isArray(v) ? v : [v];
229
+ var posthogHost = (config) => (config.posthog?.host ?? "https://eu.i.posthog.com").replace(/\/$/, "");
203
230
  function activeProviders(config) {
204
- return providers.filter((p) => p.id === "youtube" ? config.youtube : list(config[p.id]).length);
231
+ return providers.filter(
232
+ (p) => p.id === "youtube" ? config.youtube : p.id === "posthog" ? config.posthog?.key : list(config[p.id]).length
233
+ ).map((p) => p.id === "posthog" && /\/\/us\./.test(posthogHost(config)) ? { ...p, transfer: "us" } : p);
205
234
  }
206
235
  function categoriesOf(config) {
207
236
  return ["analytics", "marketing"].filter((c) => activeProviders(config).some((p) => p.category === c));
208
237
  }
238
+ function needsBanner(config) {
239
+ return activeProviders(config).some((p) => p.id !== "youtube");
240
+ }
241
+ function policyOf(config, locale) {
242
+ return typeof config.policyUrl === "string" ? config.policyUrl : config.policyUrl?.[locale];
243
+ }
209
244
  function textsFor(config, locale) {
210
245
  return { ...texts[locale], ...config.texts?.[locale] };
211
246
  }
@@ -226,48 +261,67 @@ function runtimeData(config, locale) {
226
261
  ga: list(config.ga),
227
262
  ads: list(config.googleAds),
228
263
  meta: list(config.meta),
264
+ ph: config.posthog?.key ? { key: config.posthog.key, host: posthogHost(config) } : void 0,
229
265
  yt: !!config.youtube,
266
+ banner: needsBanner(config),
267
+ policy: policyOf(config, locale),
230
268
  del: { analytics: del("analytics"), marketing: del("marketing") },
231
- t: { ytText: t.ytText, ytButton: t.ytButton, ytOpen: t.ytOpen }
269
+ t: { ytText: t.ytText, ytButton: t.ytButton, ytOpen: t.ytOpen, more: t.more }
232
270
  };
233
271
  }
234
272
 
235
273
  // src/styles.ts
236
274
  var css = [
237
- ":where(.keksi,.keksi-dialog){--_bg:var(--keksi-bg,#fff);--_fg:var(--keksi-fg,#1b1b1b);--_line:var(--keksi-line,#c8c8c8);--_r:var(--keksi-radius,6px);box-sizing:border-box;background:var(--_bg);color:var(--_fg);font:inherit;font-size:15px;line-height:1.5}",
238
- ":where(.keksi-policy){--_line:var(--keksi-line,#c8c8c8)}",
239
- ":where(.keksi){position:fixed;z-index:2147483000;inset:auto 1rem 1rem;max-width:34rem;max-height:calc(100vh - 2rem);overflow:auto;margin-inline:auto;padding:1.25rem;border:1px solid var(--_line);border-radius:var(--_r);box-shadow:0 4px 24px #0003}",
240
- ":where(.keksi,.keksi-dialog) :where(h2){font-size:1.125rem;margin:0 0 .5rem}",
241
- ":where(.keksi) :where(p,ul){margin:0 0 .75rem}",
242
- ":where(.keksi) :where(ul){padding-left:1.25rem}",
243
- ":where(.keksi,.keksi-dialog) :where(a){color:inherit}",
244
- ":where(.keksi-actions){display:flex;flex-wrap:wrap;gap:.5rem;margin-top:1rem}",
245
- ":where(.keksi-btn){flex:1 1 auto;font:inherit;font-weight:600;padding:.6rem 1rem;border:1px solid var(--_fg);border-radius:var(--_r);background:var(--_bg);color:var(--_fg);cursor:pointer}",
275
+ ":where(.keksi,.keksi-policy){--_bg:var(--keksi-bg,#fff);--_fg:var(--keksi-fg,#1b1b1b);--_line:var(--keksi-line,#e3e3e3);--_soft:color-mix(in srgb,var(--_fg) 4%,var(--_bg));--_r:var(--keksi-radius,8px)}",
276
+ ":where(.keksi){box-sizing:border-box;background:var(--_bg);color:var(--_fg);font:inherit;font-size:14px;line-height:1.5;border:1px solid var(--_line);border-radius:var(--_r);box-shadow:0 8px 32px #0000002e}",
277
+ ":where(.keksi){position:fixed;z-index:2147483000;inset:auto 1rem 1rem;display:flex;flex-direction:column;max-width:34rem;max-height:calc(100vh - 2rem);margin-inline:auto;overflow:hidden}",
278
+ ":where(.keksi,.keksi-main,.keksi-settings)[hidden]{display:none}",
279
+ ":where(.keksi-main,.keksi-settings){display:flex;flex-direction:column;min-height:0;margin:0}",
280
+ ":where(.keksi:has(.keksi-settings:not([hidden]))){max-width:40rem}",
281
+ ":where(.keksi-settings .keksi-body){padding-block:0}",
282
+ ":where(.keksi-head){display:flex;align-items:center;justify-content:space-between;gap:1rem;padding:.875rem 1.25rem;border-bottom:1px solid var(--_line)}",
283
+ ":where(.keksi-head) :where(h2){font-size:1rem;font-weight:600;line-height:1.3;margin:0}",
284
+ ":where(.keksi-body){padding:1rem 1.25rem;overflow:auto}",
285
+ ":where(.keksi) :where(.keksi-body>*){margin:0 0 .75rem}",
286
+ ":where(.keksi) :where(.keksi-body>:last-child){margin-bottom:0}",
287
+ ":where(.keksi) :where(ul){list-style:none;padding:0}",
288
+ ":where(.keksi) :where(strong){font-weight:600}",
289
+ ":where(.keksi) :where(a){color:inherit}",
290
+ ".keksi a{text-decoration:underline}",
291
+ ":where(.keksi-actions){display:flex;flex-wrap:wrap;align-items:center;justify-content:flex-end;gap:.5rem;padding:.75rem 1.25rem;background:var(--_soft)}",
292
+ ":where(.keksi-btn){min-width:7.5rem;font:inherit;font-weight:600;padding:.5rem 1rem;border:1px solid var(--_fg);border-radius:calc(var(--_r) - 2px);background:var(--_bg);color:var(--_fg);cursor:pointer}",
246
293
  ":where(.keksi-btn:hover){background:var(--_fg);color:var(--_bg)}",
247
- ":where(.keksi,.keksi-dialog,keksi-youtube) :where(button,a,input,summary):focus-visible{outline:3px solid var(--keksi-focus,#2563eb);outline-offset:2px}",
248
- ":where(.keksi-dialog){width:min(40rem,calc(100% - 2rem));max-height:calc(100% - 2rem);padding:0;border:0;border-radius:var(--_r)}",
249
- ":where(.keksi-dialog)::backdrop{background:#0009}",
250
- ":where(.keksi-dialog form){padding:1.25rem}",
251
- ":where(.keksi-head,.keksi-cat-head){display:flex;align-items:center;justify-content:space-between;gap:1rem}",
252
- ":where(.keksi-close){font:inherit;font-size:1.5rem;line-height:1;padding:.25rem .5rem;border:0;background:none;color:inherit;cursor:pointer}",
253
- ":where(.keksi-cat){padding:.75rem 0;border-top:1px solid var(--_line)}",
254
- ":where(.keksi-cat-head) :where(h2,h3,h4,h5){font-size:1rem;margin:0}",
294
+ ":where(.keksi-text){margin-right:auto;padding:.5rem .25rem;font:inherit;border:0;background:none;color:inherit;text-decoration:underline;text-underline-offset:2px;cursor:pointer}",
295
+ "@media (max-width:30rem){:where(.keksi-btn){flex:1 1 auto}}",
296
+ ":where(.keksi,keksi-youtube) :where(button,a,input,summary):focus-visible{outline:3px solid var(--keksi-focus,#2563eb);outline-offset:2px}",
297
+ ":where(.keksi-close){font:inherit;font-size:1.375rem;line-height:1;margin:-.25rem -.5rem;padding:.25rem .5rem;border:0;background:none;color:inherit;cursor:pointer}",
298
+ ":where(.keksi-cat){padding:1rem 0;border-top:1px solid var(--_line)}",
299
+ ":where(.keksi-body>.keksi-cat:first-child){border-top:0}",
300
+ ":where(.keksi-cat-head){display:flex;align-items:center;justify-content:space-between;gap:1rem}",
301
+ ":where(.keksi-cat-head) :where(h2,h3,h4,h5){font-size:1rem;font-weight:600;margin:0}",
302
+ ":where(.keksi-cat-head) :where(span){font-size:.8125rem;opacity:.7}",
255
303
  ":where(.keksi-cat) :where(p){margin:.25rem 0}",
256
- ":where(.keksi-cat input[role=switch]){appearance:none;position:relative;flex:none;width:2.75rem;height:1.5rem;margin:0;border-radius:1rem;background:var(--_line);cursor:pointer}",
257
- ':where(.keksi-cat input[role=switch])::before{content:"";position:absolute;top:.2rem;left:.2rem;width:1.1rem;height:1.1rem;border-radius:50%;background:#fff;transition:transform .15s}',
304
+ ":where(.keksi-cat input[role=switch]){appearance:none;position:relative;flex:none;width:2.5rem;height:1.375rem;margin:0;border-radius:1rem;background:color-mix(in srgb,var(--_fg) 22%,var(--_bg));cursor:pointer}",
305
+ ':where(.keksi-cat input[role=switch])::before{content:"";position:absolute;top:.1875rem;left:.1875rem;width:1rem;height:1rem;border-radius:50%;background:#fff;transition:transform .15s}',
258
306
  ":where(.keksi-cat input[role=switch]:checked){background:var(--_fg)}",
259
- ":where(.keksi-cat input[role=switch]:checked)::before{transform:translateX(1.25rem)}",
307
+ ":where(.keksi-cat input[role=switch]:checked)::before{transform:translateX(1.125rem)}",
260
308
  ":where(.keksi-cat summary){margin-top:.5rem;font-weight:600;cursor:pointer}",
261
- ":where(.keksi-provider){margin:.75rem 0}",
262
- ":where(.keksi-provider) :where(h3,h4,h5,h6){font-size:.9375rem;margin:0}",
263
- ":where(.keksi-cat table){width:100%;border-collapse:collapse;font-size:.875rem;margin-top:.25rem}",
264
- ":where(.keksi-cat) :where(th,td){padding:.25rem .75rem .25rem 0;border-bottom:1px solid var(--_line);text-align:left;vertical-align:top}",
265
- ":where(keksi-youtube){display:block;aspect-ratio:16/9;background:#111;color:#fff}",
309
+ ":where(.keksi-provider){margin:.75rem 0;padding:.75rem .875rem;border-radius:calc(var(--_r) - 2px);background:var(--_soft)}",
310
+ ":where(.keksi-policy .keksi-provider){padding:0;background:none}",
311
+ ":where(.keksi-provider) :where(h3,h4,h5,h6){font-size:.9375rem;font-weight:600;margin:0}",
312
+ ":where(.keksi-cat table){width:100%;border-collapse:collapse;font-size:.8125rem;margin-top:.5rem}",
313
+ ":where(.keksi-cat) :where(th,td){padding:.3rem .75rem .3rem 0;border-bottom:1px solid var(--_line);text-align:left;vertical-align:top}",
314
+ ":where(.keksi-cat) :where(th){font-weight:600}",
315
+ ":where(keksi-youtube){position:relative;display:block;aspect-ratio:16/9;overflow:hidden;border-radius:12px;background:#0f0f0f;color:#fff;font-family:Roboto,Arial,sans-serif}",
266
316
  ":where(keksi-youtube iframe){display:block;width:100%;height:100%;border:0}",
267
- ":where(.keksi-yt){display:flex;flex-direction:column;align-items:center;justify-content:center;gap:.75rem;height:100%;padding:1rem;box-sizing:border-box;text-align:center}",
268
- ":where(.keksi-yt) :where(p){max-width:32rem;margin:0}",
269
- ":where(.keksi-yt) :where(a){color:inherit}",
270
- ":where(.keksi-yt button){font:inherit;font-weight:600;padding:.6rem 1.25rem;border:0;border-radius:6px;background:#fff;color:#111;cursor:pointer}"
317
+ ":where(keksi-youtube[label]:has(.keksi-yt))::before{content:attr(label);position:absolute;inset:0 0 auto;z-index:1;padding:1rem 1.25rem 2.5rem;font-size:1.125rem;font-weight:500;background:linear-gradient(#000b,#0000);pointer-events:none}",
318
+ ":where(.keksi-yt){position:relative;display:grid;grid-template-rows:1fr auto auto;gap:.25rem;height:100%;padding:0 1.25rem 1rem;box-sizing:border-box;font-size:.875rem;line-height:1.45;background:linear-gradient(#0000 55%,#000c),center/cover var(--keksi-poster,none)}",
319
+ ":where(.keksi-yt) :where(p){margin:0}",
320
+ ":where(.keksi-yt) :where(p:first-of-type){grid-row:2}",
321
+ ":where(.keksi-yt) :where(p:last-of-type){grid-row:3;color:#fffc}",
322
+ ":where(.keksi-yt) :where(a){color:inherit;text-decoration:underline;text-underline-offset:2px}",
323
+ `:where(.keksi-yt button){position:absolute;inset:0;width:68px;height:48px;margin:auto;padding:0;border:0;font-size:0;color:transparent;background:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 68 48'%3E%3Cpath d='M66.52 7.74c-.78-2.93-2.49-5.41-5.42-6.19C55.79.13 34 0 34 0S12.21.13 6.9 1.55c-2.93.78-4.63 3.26-5.42 6.19C.06 13.05 0 24 0 24s.06 10.95 1.48 16.26c.78 2.93 2.49 5.41 5.42 6.19C12.21 47.87 34 48 34 48s21.79-.13 27.1-1.55c2.93-.78 4.64-3.26 5.42-6.19C67.94 34.95 68 24 68 24s-.06-10.95-1.48-16.26z' fill='%23f00'/%3E%3Cpath d='M45 24 27 14v20' fill='%23fff'/%3E%3C/svg%3E") center/contain no-repeat;cursor:pointer;transition:transform .15s}`,
324
+ ":where(.keksi-yt button:hover){transform:scale(1.08)}"
271
325
  ].join("");
272
326
 
273
327
  // src/render.ts
@@ -277,12 +331,12 @@ function renderKeksi(config, { lang = "fi", nonce } = {}) {
277
331
  if (!cats.length) return "";
278
332
  const t = textsFor(config, lang);
279
333
  const active = activeProviders(config);
280
- const policy = typeof config.policyUrl === "string" ? config.policyUrl : config.policyUrl?.[lang];
334
+ const policy = policyOf(config, lang);
281
335
  const n = nonce ? ` nonce="${e(nonce)}"` : "";
282
336
  const btn = (action, label, type = "button") => `<button type="${type}" class="keksi-btn" data-keksi="${action}">${e(label)}</button>`;
283
337
  const uses = cats.map((c) => `<li><strong>${e(t[c])}:</strong> ${e(active.filter((p) => p.category === c).map((p) => p.name).join(", "))}</li>`).join("");
284
338
  const json = JSON.stringify(runtimeData(config, lang)).replace(/</g, "\\u003c");
285
- return `<style${n}>${css}</style><section id="keksi-banner" class="keksi" aria-labelledby="keksi-title" hidden><h2 id="keksi-title">${e(t.title)}</h2><p>${e(t.intro)}</p><ul>${uses}</ul><p>${e(t.transfer)}${policy ? ` <a href="${e(policy)}">${e(t.more)}</a>` : ""}</p><div class="keksi-actions">${btn("accept", t.accept)}${btn("reject", t.reject)}<button type="button" class="keksi-btn" data-keksi-open>${e(t.settings)}</button></div></section><dialog id="keksi-dialog" class="keksi-dialog" aria-labelledby="keksi-dialog-title"><form method="dialog"><div class="keksi-head"><h2 id="keksi-dialog-title">${e(t.settingsTitle)}</h2><button class="keksi-close" aria-label="${e(t.close)}">\xD7</button></div>` + sections(config, lang, 3, true) + `<div class="keksi-actions">${btn("save", t.save, "submit")}${btn("accept", t.accept, "submit")}${btn("reject", t.reject, "submit")}</div></form></dialog><script type="application/json" id="keksi-data">${json}</script><script${n}>${'"use strict";(()=>{var v=["analytics","marketing"],C={analytics:!1,marketing:!1};function g(...a){return arguments}function q(a,o=window){if(typeof o.keksi?.sync=="function")return o.keksi.sync(),o.keksi;let r=o.document,h=e=>r.getElementById(e),l=w(),A=!1,E=!1,b=new Set,$=g("consent","default",{ad_storage:"denied",ad_user_data:"denied",ad_personalization:"denied",analytics_storage:"denied"});if(a.ga.length||a.ads.length){let e=o.dataLayer??(o.dataLayer=[]);o.gtag=function(){e.push(arguments)},e.push($)}if(a.meta.length){let e=function(){e.callMethod?e.callMethod.apply(e,[...arguments]):e.queue.push(arguments)};Object.assign(e,{push:e,loaded:!0,version:"2.0",queue:[]}),o.fbq=o._fbq=e}function w(){let e=r.cookie.split("; ").find(s=>s.startsWith(`${a.name}=`))?.slice(a.name.length+1),[n,,t,i]=e?.split(".")??[];return n!==a.v||!t||!i?null:{id:t,c:{analytics:i.includes("a"),marketing:i.includes("m")}}}function T(e){let n=l?.id??Array.from(o.crypto.getRandomValues(new Uint8Array(8)),i=>i.toString(16).padStart(2,"0")).join(""),t=(e.analytics?"a":"")+(e.marketing?"m":"")||"-";return r.cookie=`${a.name}=${a.v}.${Date.now().toString(36)}.${n}.${t}; Path=/; Max-Age=${a.days*86400}; SameSite=Lax`+(a.domain?`; Domain=${a.domain}`:"")+(o.location.protocol==="https:"?"; Secure":""),{id:n,c:e}}function S(e){let n=r.createElement("script");n.async=!0,n.src=e,r.head.append(n)}function _(e){let n=[...e.analytics?a.ga:[],...e.marketing?a.ads:[]].filter(t=>!b.has(t));if(n.length){let t=e.marketing?"granted":"denied",i=[g("consent","update",{analytics_storage:e.analytics?"granted":"denied",ad_storage:t,ad_user_data:t,ad_personalization:t})];b.size||i.push(g("js",new Date));for(let s of n)i.push(g("config",s)),b.add(s);if(A)for(let s of i)o.dataLayer.push(s);else{let s=o.dataLayer;s.splice(s.indexOf($)+1,0,...i),S(`https://www.googletagmanager.com/gtag/js?id=${encodeURIComponent(n[0])}`),A=!0}}if(e.marketing&&a.meta.length&&!E&&(E=!0,o.fbq.queue.unshift(...a.meta.map(t=>g("init",t)),g("track","PageView")),S("https://connect.facebook.net/en_US/fbevents.js")),e.marketing)for(let t of r.querySelectorAll("keksi-youtube"))t.show(!0)}function M(e){let n=e.flatMap(c=>a.del[c]),t=c=>n.some(u=>u.endsWith("*")?c.startsWith(u.slice(0,-1)):c===u),i=o.location.hostname.split("."),s=["",...i.map((c,u)=>`; Domain=${i.slice(u).join(".")}`)];for(let c of r.cookie.split("; ")){let u=c.split("=")[0];if(t(u))for(let y of s)r.cookie=`${u}=; Max-Age=0; Path=/${y}`}try{for(let c of Object.keys(o.localStorage))t(c)&&o.localStorage.removeItem(c)}catch{}}function f(e){let n=l?.c;l=T(e),k(),r.dispatchEvent(new o.CustomEvent("keksi:change",{detail:{...e}})),M(v.filter(t=>!e[t])),v.some(t=>n?.[t]&&!e[t])?o.location.reload():_(e)}function k(){let e=h("keksi-banner");e&&(e.hidden=!!l)}function x(){let e=h("keksi-dialog");if(e){for(let n of v){let t=e.querySelector(`[name=${n}]`);t&&(t.checked=!!l?.c[n])}e.showModal()}}r.addEventListener("click",e=>{let n=e.target.closest?.("[data-keksi],[data-keksi-open]");if(!n)return;if(n.hasAttribute("data-keksi-open"))return e.preventDefault(),x();let t=n.getAttribute("data-keksi");if(t==="accept")f({analytics:!0,marketing:!0});else if(t==="reject")f(C);else if(t==="save"){let i=s=>!!h("keksi-dialog")?.querySelector(`[name=${s}]`)?.checked;f({analytics:i("analytics"),marketing:i("marketing")})}}),r.addEventListener("astro:after-swap",k);class j extends o.HTMLElement{connectedCallback(){this.show(!!l?.c.marketing)}show(n,t=!1){let i=encodeURIComponent(this.getAttribute("video")??""),s=this.getAttribute("label");if(n){if(this.querySelector("iframe"))return;let d=r.createElement("iframe");d.src=`https://www.youtube-nocookie.com/embed/${i}${t?"?autoplay=1":""}`,d.title=s??"YouTube",d.allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share",d.allowFullscreen=!0,d.referrerPolicy="strict-origin-when-cross-origin",this.replaceChildren(d),t&&d.focus();return}let c=r.createElement("div");c.className="keksi-yt";let u=this.getAttribute("poster");u&&(c.style.background=`linear-gradient(#000a,#000a),center/cover url(${JSON.stringify(u)})`);let y=r.createElement("p");y.textContent=a.t.ytText;let m=r.createElement("button");m.type="button",m.textContent=a.t.ytButton,s&&m.setAttribute("aria-label",`${a.t.ytButton}: ${s}`),m.onclick=()=>this.show(!0,!0);let p=r.createElement("a");p.href=`https://www.youtube.com/watch?v=${i}`,p.target="_blank",p.rel="noopener",p.textContent=a.t.ytOpen,c.append(y,m,p),this.replaceChildren(c)}}a.yt&&!o.customElements.get("keksi-youtube")&&o.customElements.define("keksi-youtube",j);let L={get consent(){return l?{...l.c}:null},open:x,save:e=>f({...l?.c??C,...e}),acceptAll:()=>f({analytics:!0,marketing:!0}),rejectAll:()=>f(C),sync:k};return o.keksi=L,k(),l&&_(l.c),L}q(JSON.parse(document.getElementById("keksi-data").textContent));})();'}</script>`;
339
+ return `<style${n}>${css}</style><section id="keksi-banner" class="keksi" aria-labelledby="keksi-title" hidden><div class="keksi-main"><div class="keksi-head"><h2 id="keksi-title">${e(t.title)}</h2></div><div class="keksi-body"><p>${e(t.intro)}</p><ul>${uses}</ul><p>${e(t.change)}${policy ? ` <a href="${e(policy)}">${e(t.more)}</a>` : ""}</p></div><div class="keksi-actions"><button type="button" class="keksi-text" data-keksi-open>${e(t.settings)}</button>${btn("reject", t.reject)}${btn("accept", t.accept)}</div></div><form id="keksi-settings" class="keksi-settings" aria-labelledby="keksi-settings-title" hidden><div class="keksi-head"><h2 id="keksi-settings-title">${e(t.settingsTitle)}</h2><button type="button" class="keksi-close" data-keksi="close" aria-label="${e(t.close)}">\xD7</button></div><div class="keksi-body">${sections(config, lang, 3, true)}</div><div class="keksi-actions">${btn("save", t.save)}${btn("reject", t.reject)}${btn("accept", t.accept)}</div></form></section><script type="application/json" id="keksi-data">${json}</script><script${n}>${'"use strict";(()=>{var F=(n,s)=>new RegExp(`^${n.split("*").map(i=>i.replace(/[.+?^${}()|[\\]\\\\]/g,"\\\\$&")).join(".*")}$`).test(s);var _=["analytics","marketing"],v={analytics:!1,marketing:!1};function f(...n){return arguments}function H(n,s=window){if(typeof s.keksi?.sync=="function")return s.keksi.sync(),s.keksi;let i=s.document,g=e=>i.getElementById(e),c=I(),A=!1,w=!1,L=!1,E=new Set,x=f("consent","default",{ad_storage:"denied",ad_user_data:"denied",ad_personalization:"denied",analytics_storage:"denied"});if(n.ga.length||n.ads.length){let e=s.dataLayer??(s.dataLayer=[]);s.gtag=function(){e.push(arguments)},e.push(x)}if(n.meta.length){let e=function(){e.callMethod?e.callMethod.apply(e,[...arguments]):e.queue.push(arguments)};Object.assign(e,{push:e,loaded:!0,version:"2.0",queue:[]}),s.fbq=s._fbq=e}if(n.ph){let e=Object.assign([],{_i:[],people:[]});for(let o of"capture identify alias reset register register_once unregister group setPersonProperties getFeatureFlag isFeatureEnabled onFeatureFlags reloadFeatureFlags get_distinct_id startSessionRecording stopSessionRecording captureException on".split(" "))e[o]=(...t)=>e.push([o,...t]);s.posthog=e}function I(){let e=i.cookie.split("; ").find(l=>l.startsWith(`${n.name}=`))?.slice(n.name.length+1),[o,,t,a]=e?.split(".")??[];if(o!==n.v||!t||!a)return null;let r={analytics:a.includes("a"),marketing:a.includes("m")};return{id:t,c:r,yt:r.marketing||a.includes("y"),done:!a.includes("u")}}function T(e,o=e.marketing,t=!0){let a=c?.id??Array.from(s.crypto.getRandomValues(new Uint8Array(8)),l=>l.toString(16).padStart(2,"0")).join(""),r=(t?"":"u")+(e.analytics?"a":"")+(e.marketing?"m":"")+(o&&!e.marketing?"y":"")||"-";return i.cookie=`${n.name}=${n.v}.${Date.now().toString(36)}.${a}.${r}; Path=/; Max-Age=${n.days*86400}; SameSite=Lax`+(n.domain?`; Domain=${n.domain}`:"")+(s.location.protocol==="https:"?"; Secure":""),{id:a,c:e,yt:o,done:t}}function $(e){let o=i.createElement("script");return o.async=!0,o.src=e,i.head.append(o),o}function C(e){let o=[...e.analytics?n.ga:[],...e.marketing?n.ads:[]].filter(t=>!E.has(t));if(o.length){let t=e.marketing?"granted":"denied",a=[f("consent","update",{analytics_storage:e.analytics?"granted":"denied",ad_storage:t,ad_user_data:t,ad_personalization:t})];E.size||a.push(f("js",new Date));for(let r of o)a.push(f("config",r)),E.add(r);if(A)for(let r of a)s.dataLayer.push(r);else{let r=s.dataLayer;r.splice(r.indexOf(x)+1,0,...a),$(`https://www.googletagmanager.com/gtag/js?id=${encodeURIComponent(o[0])}`),A=!0}}if(e.marketing&&n.meta.length&&!w&&(w=!0,s.fbq.queue.unshift(...n.meta.map(t=>f("init",t)),f("track","PageView")),$("https://connect.facebook.net/en_US/fbevents.js")),e.analytics&&n.ph&&!L&&(L=!0,s.posthog._i.push([n.ph.key,{api_host:n.ph.host,defaults:"2026-08-30"},"posthog"]),$(`${n.ph.host.replace(".i.posthog.com","-assets.i.posthog.com")}/static/array.js`).crossOrigin="anonymous"),c?.yt)for(let t of i.querySelectorAll("keksi-youtube"))t.show(!0)}function O(){c?.yt||(c=T(c?.c??v,!0,c?.done??!1)),C(c.c)}function R(e){let o=e.flatMap(l=>n.del[l]),t=l=>o.some(u=>F(u,l)),a=s.location.hostname.split("."),r=["",...a.map((l,u)=>`; Domain=${a.slice(u).join(".")}`)];for(let l of i.cookie.split("; ")){let u=l.split("=")[0];if(t(u))for(let b of r)i.cookie=`${u}=; Max-Age=0; Path=/${b}`}try{for(let l of[s.localStorage,s.sessionStorage])for(let u of Object.keys(l))t(u)&&l.removeItem(u)}catch{}}function p(e){let o=c;c=T(e),h(),i.dispatchEvent(new s.CustomEvent("keksi:change",{detail:{...e}})),R(_.filter(t=>!e[t])),_.some(t=>o?.c[t]&&!e[t])||o?.yt&&!c.yt?s.location.reload():C(e)}function y(e=!1,o=n.banner&&!c?.done||e){let t=g("keksi-banner"),a=g("keksi-settings");!t||!a||(t.hidden=!o,a.hidden=!e,t.firstElementChild.hidden=e)}function h(){y()}function M(){let e=g("keksi-settings");for(let o of _){let t=e?.querySelector(`[name=${o}]`);t&&(t.checked=!!c?.c[o])}y(!0),e?.querySelector(".keksi-close")?.focus()}i.addEventListener("click",e=>{let o=e.target.closest?.("[data-keksi],[data-keksi-open]");if(!o)return;if(o.hasAttribute("data-keksi-open"))return e.preventDefault(),M();let t=o.getAttribute("data-keksi");if(t==="close")y();else if(t==="accept")p({analytics:!0,marketing:!0});else if(t==="reject")p(v);else if(t==="save"){let a=r=>!!g("keksi-settings")?.querySelector(`[name=${r}]`)?.checked;p({analytics:a("analytics"),marketing:a("marketing")})}}),i.addEventListener("keydown",e=>{e.key==="Escape"&&g("keksi-settings")?.hidden===!1&&y()}),i.addEventListener("astro:after-swap",h);class D extends s.HTMLElement{connectedCallback(){this.show(!!c?.yt)}show(o,t=!1){let a=encodeURIComponent(this.getAttribute("video")??""),r=this.getAttribute("label");if(o){if(this.querySelector("iframe"))return;let d=i.createElement("iframe");d.src=`https://www.youtube-nocookie.com/embed/${a}${t?"?autoplay=1":""}`,d.title=r??"YouTube",d.allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share",d.allowFullscreen=!0,d.referrerPolicy="strict-origin-when-cross-origin",this.replaceChildren(d),t&&d.focus();return}let l=i.createElement("div");l.className="keksi-yt";let u=this.getAttribute("poster");u&&l.style.setProperty("--keksi-poster",`url(${JSON.stringify(u)})`);let b=i.createElement("p");b.textContent=n.t.ytText;let m=i.createElement("button");m.type="button",m.textContent=n.t.ytButton,r&&m.setAttribute("aria-label",`${n.t.ytButton}: ${r}`),m.onclick=()=>{this.show(!0,!0),O()};let S=i.createElement("p"),q=(d,N,P=!1)=>{let k=i.createElement("a");k.href=d,k.textContent=N,P&&(k.target="_blank",k.rel="noopener"),S.append(S.childNodes.length?" \\xB7 ":"",k)};q(`https://www.youtube.com/watch?v=${a}`,n.t.ytOpen,!0),n.policy&&q(n.policy,n.t.more),l.append(b,m,S),this.replaceChildren(l)}}n.yt&&!s.customElements.get("keksi-youtube")&&s.customElements.define("keksi-youtube",D);let j={get consent(){return c?.done?{...c.c}:null},open:M,save:e=>p({...c?.c??v,...e}),acceptAll:()=>p({analytics:!0,marketing:!0}),rejectAll:()=>p(v),sync:h};return s.keksi=j,h(),c&&C(c.c),j}H(JSON.parse(document.getElementById("keksi-data").textContent));})();'}</script>`;
286
340
  }
287
341
  function cookieTable(config, { lang = "fi", headingLevel = 2 } = {}) {
288
342
  return `<div class="keksi-policy">${sections(config, lang, headingLevel, false)}</div>`;
@@ -303,7 +357,7 @@ function sections(config, lang, h, dialog) {
303
357
  const items = c === "necessary" ? [site] : active.filter((p) => p.category === c).map((p) => ({ ...p, purpose: p.purpose[lang] }));
304
358
  const head = c === "necessary" ? `<h${h}>${e(t.necessary)}</h${h}><span>${e(t.alwaysOn)}</span>` : dialog ? `<h${h}><label for="keksi-c-${c}">${e(t[c])}</label></h${h}><input type="checkbox" role="switch" id="keksi-c-${c}" name="${c}" aria-describedby="keksi-i-${c}">` : `<h${h}>${e(t[c])}</h${h}>`;
305
359
  const list2 = items.map(
306
- (p) => `<div class="keksi-provider"><h${h + 1}>${e(p.name)}</h${h + 1}>${p.purpose ? `<p>${e(p.purpose)}</p>` : ""}` + (p.company && p.privacyPolicy ? `<p>${e(p.company)} \xB7 <a href="${e(p.privacyPolicy)}" target="_blank" rel="noopener">${e(t.privacyPolicy)}</a></p>` + (p.transfer ? `<p>${e(t.dpf)}</p>` : "") : "") + table(p.cookies, t) + `</div>`
360
+ (p) => `<div class="keksi-provider"><h${h + 1}>${e(p.name)}</h${h + 1}>${p.purpose ? `<p>${e(p.purpose)}</p>` : ""}` + (p.company && p.privacyPolicy ? `<p>${e(p.company)} \xB7 <a href="${e(p.privacyPolicy)}" target="_blank" rel="noopener">${e(t.privacyPolicy)}</a></p>` + (p.transfer ? `<p>${e(t[p.transfer])}</p>` : "") : "") + table(p.cookies, t) + `</div>`
307
361
  ).join("");
308
362
  const body = dialog ? `<details><summary>${e(t.providers)} (${items.length})</summary>${list2}</details>` : list2;
309
363
  return `<section class="keksi-cat"><div class="keksi-cat-head">${head}</div><p id="keksi-i-${c}">${e(t[`${c}Info`])}</p>${body}</section>`;
@@ -2,7 +2,7 @@
2
2
  var fi = {
3
3
  title: "Ev\xE4steet",
4
4
  intro: "K\xE4yt\xE4mme v\xE4ltt\xE4m\xE4tt\xF6mien ev\xE4steiden lis\xE4ksi seuraavia palveluita vain, jos annat siihen luvan:",
5
- transfer: "Palveluntarjoajat voivat siirt\xE4\xE4 tietoja Yhdysvaltoihin. Voit muuttaa valintaasi milloin tahansa.",
5
+ change: "Voit muuttaa valintaasi milloin tahansa.",
6
6
  more: "Lue lis\xE4\xE4",
7
7
  accept: "Hyv\xE4ksy kaikki",
8
8
  reject: "Hylk\xE4\xE4 kaikki",
@@ -22,6 +22,7 @@ var fi = {
22
22
  consentPurpose: "Tallentaa ev\xE4stevalintasi.",
23
23
  privacyPolicy: "Tietosuojaseloste",
24
24
  dpf: "Tietoja voidaan siirt\xE4\xE4 Yhdysvaltoihin EU:n ja Yhdysvaltojen v\xE4lisen tietosuojakehyksen (DPF) perusteella.",
25
+ us: "Tietoja siirret\xE4\xE4n Yhdysvaltoihin.",
25
26
  name: "Nimi",
26
27
  duration: "Kesto",
27
28
  purpose: "Tarkoitus",
@@ -32,14 +33,14 @@ var fi = {
32
33
  mo: "kuukausi|kuukautta",
33
34
  d: "p\xE4iv\xE4|p\xE4iv\xE4\xE4",
34
35
  min: "minuutti|minuuttia",
35
- ytText: "Video ladataan YouTubesta. YouTube (Google) asettaa ev\xE4steit\xE4 ja voi siirt\xE4\xE4 tietoja Yhdysvaltoihin.",
36
- ytButton: "N\xE4yt\xE4 video",
37
- ytOpen: "Avaa YouTubessa"
36
+ ytText: "Toistamalla videon hyv\xE4ksyt YouTuben ev\xE4steet.",
37
+ ytButton: "Toista video",
38
+ ytOpen: "Katso YouTubessa"
38
39
  };
39
40
  var sv = {
40
41
  title: "Kakor",
41
42
  intro: "Ut\xF6ver n\xF6dv\xE4ndiga kakor anv\xE4nder vi f\xF6ljande tj\xE4nster endast om du ger ditt samtycke:",
42
- transfer: "Tj\xE4nsteleverant\xF6rerna kan \xF6verf\xF6ra uppgifter till USA. Du kan \xE4ndra ditt val n\xE4r som helst.",
43
+ change: "Du kan \xE4ndra ditt val n\xE4r som helst.",
43
44
  more: "L\xE4s mer",
44
45
  accept: "Godk\xE4nn alla",
45
46
  reject: "Avvisa alla",
@@ -59,6 +60,7 @@ var sv = {
59
60
  consentPurpose: "Sparar ditt val av kakor.",
60
61
  privacyPolicy: "Dataskyddsbeskrivning",
61
62
  dpf: "Uppgifter kan \xF6verf\xF6ras till USA med st\xF6d av ramverket f\xF6r dataskydd mellan EU och USA (DPF).",
63
+ us: "Uppgifter \xF6verf\xF6rs till USA.",
62
64
  name: "Namn",
63
65
  duration: "Varaktighet",
64
66
  purpose: "Syfte",
@@ -69,14 +71,14 @@ var sv = {
69
71
  mo: "m\xE5nad|m\xE5nader",
70
72
  d: "dag|dagar",
71
73
  min: "minut|minuter",
72
- ytText: "Videon laddas fr\xE5n YouTube. YouTube (Google) s\xE4tter kakor och kan \xF6verf\xF6ra uppgifter till USA.",
73
- ytButton: "Visa video",
74
- ytOpen: "\xD6ppna p\xE5 YouTube"
74
+ ytText: "Genom att spela upp videon godk\xE4nner du kakor fr\xE5n YouTube.",
75
+ ytButton: "Spela upp video",
76
+ ytOpen: "Titta p\xE5 YouTube"
75
77
  };
76
78
  var en = {
77
79
  title: "Cookies",
78
80
  intro: "Besides necessary cookies, we use the following services only if you allow it:",
79
- transfer: "The providers may transfer data to the United States. You can change your choice at any time.",
81
+ change: "You can change your choice at any time.",
80
82
  more: "Read more",
81
83
  accept: "Accept all",
82
84
  reject: "Reject all",
@@ -96,6 +98,7 @@ var en = {
96
98
  consentPurpose: "Stores your cookie choice.",
97
99
  privacyPolicy: "Privacy policy",
98
100
  dpf: "Data may be transferred to the United States under the EU\u2013US Data Privacy Framework (DPF).",
101
+ us: "Data is transferred to the United States.",
99
102
  name: "Name",
100
103
  duration: "Duration",
101
104
  purpose: "Purpose",
@@ -106,9 +109,9 @@ var en = {
106
109
  mo: "month|months",
107
110
  d: "day|days",
108
111
  min: "minute|minutes",
109
- ytText: "The video loads from YouTube. YouTube (Google) sets cookies and may transfer data to the United States.",
110
- ytButton: "Show video",
111
- ytOpen: "Open on YouTube"
112
+ ytText: "By playing the video, you accept cookies from YouTube.",
113
+ ytButton: "Play video",
114
+ ytOpen: "Watch on YouTube"
112
115
  };
113
116
  var texts = { fi, sv, en };
114
117
 
@@ -172,6 +175,29 @@ var providers = [
172
175
  ],
173
176
  hosts: ["facebook.net", "facebook.com"]
174
177
  },
178
+ {
179
+ id: "posthog",
180
+ name: "PostHog",
181
+ company: "PostHog Inc.",
182
+ privacyPolicy: "https://posthog.com/privacy",
183
+ category: "analytics",
184
+ purpose: {
185
+ fi: "Tuoteanalytiikka: sivujen katselut, klikkaukset ja k\xE4ytt\xF6tavat. Voi tallentaa istuntoja, jos istuntotallenteet ovat k\xE4yt\xF6ss\xE4.",
186
+ sv: "Produktanalys: sidvisningar, klick och anv\xE4ndningsm\xF6nster. Kan spela in sessioner om sessionsinspelning \xE4r aktiverad.",
187
+ en: "Product analytics: page views, clicks and usage patterns. Can record sessions if session replay is enabled."
188
+ },
189
+ cookies: [
190
+ { name: "ph_*_posthog", duration: "1y" },
191
+ { name: "ph_*_posthog_cpm", duration: "1y" },
192
+ { name: "ph_*_posthog", duration: "persistent", storage: "localStorage" },
193
+ { name: "ph_*_posthog__flags", duration: "persistent", storage: "localStorage" },
194
+ { name: "ph_*_posthog", duration: "session", storage: "sessionStorage" },
195
+ { name: "ph_*_primary_window_exists", duration: "session", storage: "sessionStorage" },
196
+ { name: "ph_*_session_registered_properties", duration: "session", storage: "sessionStorage" },
197
+ { name: "ph_*_window_id", duration: "session", storage: "sessionStorage" }
198
+ ],
199
+ hosts: ["posthog.com"]
200
+ },
175
201
  {
176
202
  id: "youtube",
177
203
  name: "YouTube",
@@ -197,8 +223,17 @@ var providers = [
197
223
 
198
224
  // src/config.ts
199
225
  var list = (v) => v === void 0 ? [] : Array.isArray(v) ? v : [v];
226
+ var posthogHost = (config) => (config.posthog?.host ?? "https://eu.i.posthog.com").replace(/\/$/, "");
200
227
  function activeProviders(config) {
201
- return providers.filter((p) => p.id === "youtube" ? config.youtube : list(config[p.id]).length);
228
+ return providers.filter(
229
+ (p) => p.id === "youtube" ? config.youtube : p.id === "posthog" ? config.posthog?.key : list(config[p.id]).length
230
+ ).map((p) => p.id === "posthog" && /\/\/us\./.test(posthogHost(config)) ? { ...p, transfer: "us" } : p);
231
+ }
232
+ function needsBanner(config) {
233
+ return activeProviders(config).some((p) => p.id !== "youtube");
234
+ }
235
+ function policyOf(config, locale) {
236
+ return typeof config.policyUrl === "string" ? config.policyUrl : config.policyUrl?.[locale];
202
237
  }
203
238
  function textsFor(config, locale) {
204
239
  return { ...texts[locale], ...config.texts?.[locale] };
@@ -220,9 +255,12 @@ function runtimeData(config, locale) {
220
255
  ga: list(config.ga),
221
256
  ads: list(config.googleAds),
222
257
  meta: list(config.meta),
258
+ ph: config.posthog?.key ? { key: config.posthog.key, host: posthogHost(config) } : void 0,
223
259
  yt: !!config.youtube,
260
+ banner: needsBanner(config),
261
+ policy: policyOf(config, locale),
224
262
  del: { analytics: del("analytics"), marketing: del("marketing") },
225
- t: { ytText: t.ytText, ytButton: t.ytButton, ytOpen: t.ytOpen }
263
+ t: { ytText: t.ytText, ytButton: t.ytButton, ytOpen: t.ytOpen, more: t.more }
226
264
  };
227
265
  }
228
266
 
package/dist/render.d.ts CHANGED
@@ -5,7 +5,7 @@ export interface RenderOptions {
5
5
  /** For a CSP with nonces; applied to the inline `<style>` and `<script>`. */
6
6
  nonce?: string;
7
7
  }
8
- /** Banner, settings dialog and runtime. Place it right after `<body>`. Empty when no provider needs consent. */
8
+ /** Banner with its settings view, and the runtime. Place it right after `<body>`. Empty when no provider needs consent. */
9
9
  export declare function renderKeksi(config: KeksiConfig, { lang, nonce }?: RenderOptions): string;
10
10
  export interface CookieTableOptions {
11
11
  lang?: Locale;
package/dist/runtime.d.ts CHANGED
@@ -21,6 +21,9 @@ declare global {
21
21
  gtag?: (...a: unknown[]) => void;
22
22
  fbq?: Queue;
23
23
  _fbq?: Queue;
24
+ posthog?: unknown[] & Record<string, unknown> & {
25
+ _i: unknown[];
26
+ };
24
27
  }
25
28
  }
26
29
  export declare function boot(d: RuntimeData, w?: Window & typeof globalThis): KeksiApi;
package/dist/scan.js CHANGED
@@ -1,3 +1,6 @@
1
+ // src/glob.ts
2
+ var glob = (pattern, name) => new RegExp(`^${pattern.split("*").map((s) => s.replace(/[.+?^${}()|[\]\\]/g, "\\$&")).join(".*")}$`).test(name);
3
+
1
4
  // src/providers.ts
2
5
  var GOOGLE = { company: "Google Ireland Limited", privacyPolicy: "https://policies.google.com/privacy", transfer: "dpf" };
3
6
  var META = { company: "Meta Platforms Ireland Limited", privacyPolicy: "https://www.facebook.com/privacy/policy/", transfer: "dpf" };
@@ -58,6 +61,29 @@ var providers = [
58
61
  ],
59
62
  hosts: ["facebook.net", "facebook.com"]
60
63
  },
64
+ {
65
+ id: "posthog",
66
+ name: "PostHog",
67
+ company: "PostHog Inc.",
68
+ privacyPolicy: "https://posthog.com/privacy",
69
+ category: "analytics",
70
+ purpose: {
71
+ fi: "Tuoteanalytiikka: sivujen katselut, klikkaukset ja k\xE4ytt\xF6tavat. Voi tallentaa istuntoja, jos istuntotallenteet ovat k\xE4yt\xF6ss\xE4.",
72
+ sv: "Produktanalys: sidvisningar, klick och anv\xE4ndningsm\xF6nster. Kan spela in sessioner om sessionsinspelning \xE4r aktiverad.",
73
+ en: "Product analytics: page views, clicks and usage patterns. Can record sessions if session replay is enabled."
74
+ },
75
+ cookies: [
76
+ { name: "ph_*_posthog", duration: "1y" },
77
+ { name: "ph_*_posthog_cpm", duration: "1y" },
78
+ { name: "ph_*_posthog", duration: "persistent", storage: "localStorage" },
79
+ { name: "ph_*_posthog__flags", duration: "persistent", storage: "localStorage" },
80
+ { name: "ph_*_posthog", duration: "session", storage: "sessionStorage" },
81
+ { name: "ph_*_primary_window_exists", duration: "session", storage: "sessionStorage" },
82
+ { name: "ph_*_session_registered_properties", duration: "session", storage: "sessionStorage" },
83
+ { name: "ph_*_window_id", duration: "session", storage: "sessionStorage" }
84
+ ],
85
+ hosts: ["posthog.com"]
86
+ },
61
87
  {
62
88
  id: "youtube",
63
89
  name: "YouTube",
@@ -113,7 +139,7 @@ var purposes = {
113
139
  };
114
140
  var trackerNames = providers.flatMap((p) => p.cookies.flatMap((c) => c.name.split(", ")));
115
141
  var trackerHosts = providers.flatMap((p) => p.hosts);
116
- var matches = (patterns, name) => patterns.some((p) => p.endsWith("*") ? name.startsWith(p.slice(0, -1)) : name === p);
142
+ var matches = (patterns, name) => patterns.some((p) => glob(p, name));
117
143
  function purposeOf(name) {
118
144
  const k = Object.keys(purposes).find((p) => matches([p], name));
119
145
  return k ? purposes[k] : void 0;
package/dist/sveltekit.js CHANGED
@@ -2,7 +2,7 @@
2
2
  var fi = {
3
3
  title: "Ev\xE4steet",
4
4
  intro: "K\xE4yt\xE4mme v\xE4ltt\xE4m\xE4tt\xF6mien ev\xE4steiden lis\xE4ksi seuraavia palveluita vain, jos annat siihen luvan:",
5
- transfer: "Palveluntarjoajat voivat siirt\xE4\xE4 tietoja Yhdysvaltoihin. Voit muuttaa valintaasi milloin tahansa.",
5
+ change: "Voit muuttaa valintaasi milloin tahansa.",
6
6
  more: "Lue lis\xE4\xE4",
7
7
  accept: "Hyv\xE4ksy kaikki",
8
8
  reject: "Hylk\xE4\xE4 kaikki",
@@ -22,6 +22,7 @@ var fi = {
22
22
  consentPurpose: "Tallentaa ev\xE4stevalintasi.",
23
23
  privacyPolicy: "Tietosuojaseloste",
24
24
  dpf: "Tietoja voidaan siirt\xE4\xE4 Yhdysvaltoihin EU:n ja Yhdysvaltojen v\xE4lisen tietosuojakehyksen (DPF) perusteella.",
25
+ us: "Tietoja siirret\xE4\xE4n Yhdysvaltoihin.",
25
26
  name: "Nimi",
26
27
  duration: "Kesto",
27
28
  purpose: "Tarkoitus",
@@ -32,14 +33,14 @@ var fi = {
32
33
  mo: "kuukausi|kuukautta",
33
34
  d: "p\xE4iv\xE4|p\xE4iv\xE4\xE4",
34
35
  min: "minuutti|minuuttia",
35
- ytText: "Video ladataan YouTubesta. YouTube (Google) asettaa ev\xE4steit\xE4 ja voi siirt\xE4\xE4 tietoja Yhdysvaltoihin.",
36
- ytButton: "N\xE4yt\xE4 video",
37
- ytOpen: "Avaa YouTubessa"
36
+ ytText: "Toistamalla videon hyv\xE4ksyt YouTuben ev\xE4steet.",
37
+ ytButton: "Toista video",
38
+ ytOpen: "Katso YouTubessa"
38
39
  };
39
40
  var sv = {
40
41
  title: "Kakor",
41
42
  intro: "Ut\xF6ver n\xF6dv\xE4ndiga kakor anv\xE4nder vi f\xF6ljande tj\xE4nster endast om du ger ditt samtycke:",
42
- transfer: "Tj\xE4nsteleverant\xF6rerna kan \xF6verf\xF6ra uppgifter till USA. Du kan \xE4ndra ditt val n\xE4r som helst.",
43
+ change: "Du kan \xE4ndra ditt val n\xE4r som helst.",
43
44
  more: "L\xE4s mer",
44
45
  accept: "Godk\xE4nn alla",
45
46
  reject: "Avvisa alla",
@@ -59,6 +60,7 @@ var sv = {
59
60
  consentPurpose: "Sparar ditt val av kakor.",
60
61
  privacyPolicy: "Dataskyddsbeskrivning",
61
62
  dpf: "Uppgifter kan \xF6verf\xF6ras till USA med st\xF6d av ramverket f\xF6r dataskydd mellan EU och USA (DPF).",
63
+ us: "Uppgifter \xF6verf\xF6rs till USA.",
62
64
  name: "Namn",
63
65
  duration: "Varaktighet",
64
66
  purpose: "Syfte",
@@ -69,14 +71,14 @@ var sv = {
69
71
  mo: "m\xE5nad|m\xE5nader",
70
72
  d: "dag|dagar",
71
73
  min: "minut|minuter",
72
- ytText: "Videon laddas fr\xE5n YouTube. YouTube (Google) s\xE4tter kakor och kan \xF6verf\xF6ra uppgifter till USA.",
73
- ytButton: "Visa video",
74
- ytOpen: "\xD6ppna p\xE5 YouTube"
74
+ ytText: "Genom att spela upp videon godk\xE4nner du kakor fr\xE5n YouTube.",
75
+ ytButton: "Spela upp video",
76
+ ytOpen: "Titta p\xE5 YouTube"
75
77
  };
76
78
  var en = {
77
79
  title: "Cookies",
78
80
  intro: "Besides necessary cookies, we use the following services only if you allow it:",
79
- transfer: "The providers may transfer data to the United States. You can change your choice at any time.",
81
+ change: "You can change your choice at any time.",
80
82
  more: "Read more",
81
83
  accept: "Accept all",
82
84
  reject: "Reject all",
@@ -96,6 +98,7 @@ var en = {
96
98
  consentPurpose: "Stores your cookie choice.",
97
99
  privacyPolicy: "Privacy policy",
98
100
  dpf: "Data may be transferred to the United States under the EU\u2013US Data Privacy Framework (DPF).",
101
+ us: "Data is transferred to the United States.",
99
102
  name: "Name",
100
103
  duration: "Duration",
101
104
  purpose: "Purpose",
@@ -106,9 +109,9 @@ var en = {
106
109
  mo: "month|months",
107
110
  d: "day|days",
108
111
  min: "minute|minutes",
109
- ytText: "The video loads from YouTube. YouTube (Google) sets cookies and may transfer data to the United States.",
110
- ytButton: "Show video",
111
- ytOpen: "Open on YouTube"
112
+ ytText: "By playing the video, you accept cookies from YouTube.",
113
+ ytButton: "Play video",
114
+ ytOpen: "Watch on YouTube"
112
115
  };
113
116
  var texts = { fi, sv, en };
114
117
 
@@ -172,6 +175,29 @@ var providers = [
172
175
  ],
173
176
  hosts: ["facebook.net", "facebook.com"]
174
177
  },
178
+ {
179
+ id: "posthog",
180
+ name: "PostHog",
181
+ company: "PostHog Inc.",
182
+ privacyPolicy: "https://posthog.com/privacy",
183
+ category: "analytics",
184
+ purpose: {
185
+ fi: "Tuoteanalytiikka: sivujen katselut, klikkaukset ja k\xE4ytt\xF6tavat. Voi tallentaa istuntoja, jos istuntotallenteet ovat k\xE4yt\xF6ss\xE4.",
186
+ sv: "Produktanalys: sidvisningar, klick och anv\xE4ndningsm\xF6nster. Kan spela in sessioner om sessionsinspelning \xE4r aktiverad.",
187
+ en: "Product analytics: page views, clicks and usage patterns. Can record sessions if session replay is enabled."
188
+ },
189
+ cookies: [
190
+ { name: "ph_*_posthog", duration: "1y" },
191
+ { name: "ph_*_posthog_cpm", duration: "1y" },
192
+ { name: "ph_*_posthog", duration: "persistent", storage: "localStorage" },
193
+ { name: "ph_*_posthog__flags", duration: "persistent", storage: "localStorage" },
194
+ { name: "ph_*_posthog", duration: "session", storage: "sessionStorage" },
195
+ { name: "ph_*_primary_window_exists", duration: "session", storage: "sessionStorage" },
196
+ { name: "ph_*_session_registered_properties", duration: "session", storage: "sessionStorage" },
197
+ { name: "ph_*_window_id", duration: "session", storage: "sessionStorage" }
198
+ ],
199
+ hosts: ["posthog.com"]
200
+ },
175
201
  {
176
202
  id: "youtube",
177
203
  name: "YouTube",
@@ -197,12 +223,21 @@ var providers = [
197
223
 
198
224
  // src/config.ts
199
225
  var list = (v) => v === void 0 ? [] : Array.isArray(v) ? v : [v];
226
+ var posthogHost = (config) => (config.posthog?.host ?? "https://eu.i.posthog.com").replace(/\/$/, "");
200
227
  function activeProviders(config) {
201
- return providers.filter((p) => p.id === "youtube" ? config.youtube : list(config[p.id]).length);
228
+ return providers.filter(
229
+ (p) => p.id === "youtube" ? config.youtube : p.id === "posthog" ? config.posthog?.key : list(config[p.id]).length
230
+ ).map((p) => p.id === "posthog" && /\/\/us\./.test(posthogHost(config)) ? { ...p, transfer: "us" } : p);
202
231
  }
203
232
  function categoriesOf(config) {
204
233
  return ["analytics", "marketing"].filter((c) => activeProviders(config).some((p) => p.category === c));
205
234
  }
235
+ function needsBanner(config) {
236
+ return activeProviders(config).some((p) => p.id !== "youtube");
237
+ }
238
+ function policyOf(config, locale) {
239
+ return typeof config.policyUrl === "string" ? config.policyUrl : config.policyUrl?.[locale];
240
+ }
206
241
  function textsFor(config, locale) {
207
242
  return { ...texts[locale], ...config.texts?.[locale] };
208
243
  }
@@ -223,48 +258,67 @@ function runtimeData(config, locale) {
223
258
  ga: list(config.ga),
224
259
  ads: list(config.googleAds),
225
260
  meta: list(config.meta),
261
+ ph: config.posthog?.key ? { key: config.posthog.key, host: posthogHost(config) } : void 0,
226
262
  yt: !!config.youtube,
263
+ banner: needsBanner(config),
264
+ policy: policyOf(config, locale),
227
265
  del: { analytics: del("analytics"), marketing: del("marketing") },
228
- t: { ytText: t.ytText, ytButton: t.ytButton, ytOpen: t.ytOpen }
266
+ t: { ytText: t.ytText, ytButton: t.ytButton, ytOpen: t.ytOpen, more: t.more }
229
267
  };
230
268
  }
231
269
 
232
270
  // src/styles.ts
233
271
  var css = [
234
- ":where(.keksi,.keksi-dialog){--_bg:var(--keksi-bg,#fff);--_fg:var(--keksi-fg,#1b1b1b);--_line:var(--keksi-line,#c8c8c8);--_r:var(--keksi-radius,6px);box-sizing:border-box;background:var(--_bg);color:var(--_fg);font:inherit;font-size:15px;line-height:1.5}",
235
- ":where(.keksi-policy){--_line:var(--keksi-line,#c8c8c8)}",
236
- ":where(.keksi){position:fixed;z-index:2147483000;inset:auto 1rem 1rem;max-width:34rem;max-height:calc(100vh - 2rem);overflow:auto;margin-inline:auto;padding:1.25rem;border:1px solid var(--_line);border-radius:var(--_r);box-shadow:0 4px 24px #0003}",
237
- ":where(.keksi,.keksi-dialog) :where(h2){font-size:1.125rem;margin:0 0 .5rem}",
238
- ":where(.keksi) :where(p,ul){margin:0 0 .75rem}",
239
- ":where(.keksi) :where(ul){padding-left:1.25rem}",
240
- ":where(.keksi,.keksi-dialog) :where(a){color:inherit}",
241
- ":where(.keksi-actions){display:flex;flex-wrap:wrap;gap:.5rem;margin-top:1rem}",
242
- ":where(.keksi-btn){flex:1 1 auto;font:inherit;font-weight:600;padding:.6rem 1rem;border:1px solid var(--_fg);border-radius:var(--_r);background:var(--_bg);color:var(--_fg);cursor:pointer}",
272
+ ":where(.keksi,.keksi-policy){--_bg:var(--keksi-bg,#fff);--_fg:var(--keksi-fg,#1b1b1b);--_line:var(--keksi-line,#e3e3e3);--_soft:color-mix(in srgb,var(--_fg) 4%,var(--_bg));--_r:var(--keksi-radius,8px)}",
273
+ ":where(.keksi){box-sizing:border-box;background:var(--_bg);color:var(--_fg);font:inherit;font-size:14px;line-height:1.5;border:1px solid var(--_line);border-radius:var(--_r);box-shadow:0 8px 32px #0000002e}",
274
+ ":where(.keksi){position:fixed;z-index:2147483000;inset:auto 1rem 1rem;display:flex;flex-direction:column;max-width:34rem;max-height:calc(100vh - 2rem);margin-inline:auto;overflow:hidden}",
275
+ ":where(.keksi,.keksi-main,.keksi-settings)[hidden]{display:none}",
276
+ ":where(.keksi-main,.keksi-settings){display:flex;flex-direction:column;min-height:0;margin:0}",
277
+ ":where(.keksi:has(.keksi-settings:not([hidden]))){max-width:40rem}",
278
+ ":where(.keksi-settings .keksi-body){padding-block:0}",
279
+ ":where(.keksi-head){display:flex;align-items:center;justify-content:space-between;gap:1rem;padding:.875rem 1.25rem;border-bottom:1px solid var(--_line)}",
280
+ ":where(.keksi-head) :where(h2){font-size:1rem;font-weight:600;line-height:1.3;margin:0}",
281
+ ":where(.keksi-body){padding:1rem 1.25rem;overflow:auto}",
282
+ ":where(.keksi) :where(.keksi-body>*){margin:0 0 .75rem}",
283
+ ":where(.keksi) :where(.keksi-body>:last-child){margin-bottom:0}",
284
+ ":where(.keksi) :where(ul){list-style:none;padding:0}",
285
+ ":where(.keksi) :where(strong){font-weight:600}",
286
+ ":where(.keksi) :where(a){color:inherit}",
287
+ ".keksi a{text-decoration:underline}",
288
+ ":where(.keksi-actions){display:flex;flex-wrap:wrap;align-items:center;justify-content:flex-end;gap:.5rem;padding:.75rem 1.25rem;background:var(--_soft)}",
289
+ ":where(.keksi-btn){min-width:7.5rem;font:inherit;font-weight:600;padding:.5rem 1rem;border:1px solid var(--_fg);border-radius:calc(var(--_r) - 2px);background:var(--_bg);color:var(--_fg);cursor:pointer}",
243
290
  ":where(.keksi-btn:hover){background:var(--_fg);color:var(--_bg)}",
244
- ":where(.keksi,.keksi-dialog,keksi-youtube) :where(button,a,input,summary):focus-visible{outline:3px solid var(--keksi-focus,#2563eb);outline-offset:2px}",
245
- ":where(.keksi-dialog){width:min(40rem,calc(100% - 2rem));max-height:calc(100% - 2rem);padding:0;border:0;border-radius:var(--_r)}",
246
- ":where(.keksi-dialog)::backdrop{background:#0009}",
247
- ":where(.keksi-dialog form){padding:1.25rem}",
248
- ":where(.keksi-head,.keksi-cat-head){display:flex;align-items:center;justify-content:space-between;gap:1rem}",
249
- ":where(.keksi-close){font:inherit;font-size:1.5rem;line-height:1;padding:.25rem .5rem;border:0;background:none;color:inherit;cursor:pointer}",
250
- ":where(.keksi-cat){padding:.75rem 0;border-top:1px solid var(--_line)}",
251
- ":where(.keksi-cat-head) :where(h2,h3,h4,h5){font-size:1rem;margin:0}",
291
+ ":where(.keksi-text){margin-right:auto;padding:.5rem .25rem;font:inherit;border:0;background:none;color:inherit;text-decoration:underline;text-underline-offset:2px;cursor:pointer}",
292
+ "@media (max-width:30rem){:where(.keksi-btn){flex:1 1 auto}}",
293
+ ":where(.keksi,keksi-youtube) :where(button,a,input,summary):focus-visible{outline:3px solid var(--keksi-focus,#2563eb);outline-offset:2px}",
294
+ ":where(.keksi-close){font:inherit;font-size:1.375rem;line-height:1;margin:-.25rem -.5rem;padding:.25rem .5rem;border:0;background:none;color:inherit;cursor:pointer}",
295
+ ":where(.keksi-cat){padding:1rem 0;border-top:1px solid var(--_line)}",
296
+ ":where(.keksi-body>.keksi-cat:first-child){border-top:0}",
297
+ ":where(.keksi-cat-head){display:flex;align-items:center;justify-content:space-between;gap:1rem}",
298
+ ":where(.keksi-cat-head) :where(h2,h3,h4,h5){font-size:1rem;font-weight:600;margin:0}",
299
+ ":where(.keksi-cat-head) :where(span){font-size:.8125rem;opacity:.7}",
252
300
  ":where(.keksi-cat) :where(p){margin:.25rem 0}",
253
- ":where(.keksi-cat input[role=switch]){appearance:none;position:relative;flex:none;width:2.75rem;height:1.5rem;margin:0;border-radius:1rem;background:var(--_line);cursor:pointer}",
254
- ':where(.keksi-cat input[role=switch])::before{content:"";position:absolute;top:.2rem;left:.2rem;width:1.1rem;height:1.1rem;border-radius:50%;background:#fff;transition:transform .15s}',
301
+ ":where(.keksi-cat input[role=switch]){appearance:none;position:relative;flex:none;width:2.5rem;height:1.375rem;margin:0;border-radius:1rem;background:color-mix(in srgb,var(--_fg) 22%,var(--_bg));cursor:pointer}",
302
+ ':where(.keksi-cat input[role=switch])::before{content:"";position:absolute;top:.1875rem;left:.1875rem;width:1rem;height:1rem;border-radius:50%;background:#fff;transition:transform .15s}',
255
303
  ":where(.keksi-cat input[role=switch]:checked){background:var(--_fg)}",
256
- ":where(.keksi-cat input[role=switch]:checked)::before{transform:translateX(1.25rem)}",
304
+ ":where(.keksi-cat input[role=switch]:checked)::before{transform:translateX(1.125rem)}",
257
305
  ":where(.keksi-cat summary){margin-top:.5rem;font-weight:600;cursor:pointer}",
258
- ":where(.keksi-provider){margin:.75rem 0}",
259
- ":where(.keksi-provider) :where(h3,h4,h5,h6){font-size:.9375rem;margin:0}",
260
- ":where(.keksi-cat table){width:100%;border-collapse:collapse;font-size:.875rem;margin-top:.25rem}",
261
- ":where(.keksi-cat) :where(th,td){padding:.25rem .75rem .25rem 0;border-bottom:1px solid var(--_line);text-align:left;vertical-align:top}",
262
- ":where(keksi-youtube){display:block;aspect-ratio:16/9;background:#111;color:#fff}",
306
+ ":where(.keksi-provider){margin:.75rem 0;padding:.75rem .875rem;border-radius:calc(var(--_r) - 2px);background:var(--_soft)}",
307
+ ":where(.keksi-policy .keksi-provider){padding:0;background:none}",
308
+ ":where(.keksi-provider) :where(h3,h4,h5,h6){font-size:.9375rem;font-weight:600;margin:0}",
309
+ ":where(.keksi-cat table){width:100%;border-collapse:collapse;font-size:.8125rem;margin-top:.5rem}",
310
+ ":where(.keksi-cat) :where(th,td){padding:.3rem .75rem .3rem 0;border-bottom:1px solid var(--_line);text-align:left;vertical-align:top}",
311
+ ":where(.keksi-cat) :where(th){font-weight:600}",
312
+ ":where(keksi-youtube){position:relative;display:block;aspect-ratio:16/9;overflow:hidden;border-radius:12px;background:#0f0f0f;color:#fff;font-family:Roboto,Arial,sans-serif}",
263
313
  ":where(keksi-youtube iframe){display:block;width:100%;height:100%;border:0}",
264
- ":where(.keksi-yt){display:flex;flex-direction:column;align-items:center;justify-content:center;gap:.75rem;height:100%;padding:1rem;box-sizing:border-box;text-align:center}",
265
- ":where(.keksi-yt) :where(p){max-width:32rem;margin:0}",
266
- ":where(.keksi-yt) :where(a){color:inherit}",
267
- ":where(.keksi-yt button){font:inherit;font-weight:600;padding:.6rem 1.25rem;border:0;border-radius:6px;background:#fff;color:#111;cursor:pointer}"
314
+ ":where(keksi-youtube[label]:has(.keksi-yt))::before{content:attr(label);position:absolute;inset:0 0 auto;z-index:1;padding:1rem 1.25rem 2.5rem;font-size:1.125rem;font-weight:500;background:linear-gradient(#000b,#0000);pointer-events:none}",
315
+ ":where(.keksi-yt){position:relative;display:grid;grid-template-rows:1fr auto auto;gap:.25rem;height:100%;padding:0 1.25rem 1rem;box-sizing:border-box;font-size:.875rem;line-height:1.45;background:linear-gradient(#0000 55%,#000c),center/cover var(--keksi-poster,none)}",
316
+ ":where(.keksi-yt) :where(p){margin:0}",
317
+ ":where(.keksi-yt) :where(p:first-of-type){grid-row:2}",
318
+ ":where(.keksi-yt) :where(p:last-of-type){grid-row:3;color:#fffc}",
319
+ ":where(.keksi-yt) :where(a){color:inherit;text-decoration:underline;text-underline-offset:2px}",
320
+ `:where(.keksi-yt button){position:absolute;inset:0;width:68px;height:48px;margin:auto;padding:0;border:0;font-size:0;color:transparent;background:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 68 48'%3E%3Cpath d='M66.52 7.74c-.78-2.93-2.49-5.41-5.42-6.19C55.79.13 34 0 34 0S12.21.13 6.9 1.55c-2.93.78-4.63 3.26-5.42 6.19C.06 13.05 0 24 0 24s.06 10.95 1.48 16.26c.78 2.93 2.49 5.41 5.42 6.19C12.21 47.87 34 48 34 48s21.79-.13 27.1-1.55c2.93-.78 4.64-3.26 5.42-6.19C67.94 34.95 68 24 68 24s-.06-10.95-1.48-16.26z' fill='%23f00'/%3E%3Cpath d='M45 24 27 14v20' fill='%23fff'/%3E%3C/svg%3E") center/contain no-repeat;cursor:pointer;transition:transform .15s}`,
321
+ ":where(.keksi-yt button:hover){transform:scale(1.08)}"
268
322
  ].join("");
269
323
 
270
324
  // src/render.ts
@@ -274,12 +328,12 @@ function renderKeksi(config, { lang = "fi", nonce } = {}) {
274
328
  if (!cats.length) return "";
275
329
  const t = textsFor(config, lang);
276
330
  const active = activeProviders(config);
277
- const policy = typeof config.policyUrl === "string" ? config.policyUrl : config.policyUrl?.[lang];
331
+ const policy = policyOf(config, lang);
278
332
  const n = nonce ? ` nonce="${e(nonce)}"` : "";
279
333
  const btn = (action, label, type = "button") => `<button type="${type}" class="keksi-btn" data-keksi="${action}">${e(label)}</button>`;
280
334
  const uses = cats.map((c) => `<li><strong>${e(t[c])}:</strong> ${e(active.filter((p) => p.category === c).map((p) => p.name).join(", "))}</li>`).join("");
281
335
  const json = JSON.stringify(runtimeData(config, lang)).replace(/</g, "\\u003c");
282
- return `<style${n}>${css}</style><section id="keksi-banner" class="keksi" aria-labelledby="keksi-title" hidden><h2 id="keksi-title">${e(t.title)}</h2><p>${e(t.intro)}</p><ul>${uses}</ul><p>${e(t.transfer)}${policy ? ` <a href="${e(policy)}">${e(t.more)}</a>` : ""}</p><div class="keksi-actions">${btn("accept", t.accept)}${btn("reject", t.reject)}<button type="button" class="keksi-btn" data-keksi-open>${e(t.settings)}</button></div></section><dialog id="keksi-dialog" class="keksi-dialog" aria-labelledby="keksi-dialog-title"><form method="dialog"><div class="keksi-head"><h2 id="keksi-dialog-title">${e(t.settingsTitle)}</h2><button class="keksi-close" aria-label="${e(t.close)}">\xD7</button></div>` + sections(config, lang, 3, true) + `<div class="keksi-actions">${btn("save", t.save, "submit")}${btn("accept", t.accept, "submit")}${btn("reject", t.reject, "submit")}</div></form></dialog><script type="application/json" id="keksi-data">${json}</script><script${n}>${'"use strict";(()=>{var v=["analytics","marketing"],C={analytics:!1,marketing:!1};function g(...a){return arguments}function q(a,o=window){if(typeof o.keksi?.sync=="function")return o.keksi.sync(),o.keksi;let r=o.document,h=e=>r.getElementById(e),l=w(),A=!1,E=!1,b=new Set,$=g("consent","default",{ad_storage:"denied",ad_user_data:"denied",ad_personalization:"denied",analytics_storage:"denied"});if(a.ga.length||a.ads.length){let e=o.dataLayer??(o.dataLayer=[]);o.gtag=function(){e.push(arguments)},e.push($)}if(a.meta.length){let e=function(){e.callMethod?e.callMethod.apply(e,[...arguments]):e.queue.push(arguments)};Object.assign(e,{push:e,loaded:!0,version:"2.0",queue:[]}),o.fbq=o._fbq=e}function w(){let e=r.cookie.split("; ").find(s=>s.startsWith(`${a.name}=`))?.slice(a.name.length+1),[n,,t,i]=e?.split(".")??[];return n!==a.v||!t||!i?null:{id:t,c:{analytics:i.includes("a"),marketing:i.includes("m")}}}function T(e){let n=l?.id??Array.from(o.crypto.getRandomValues(new Uint8Array(8)),i=>i.toString(16).padStart(2,"0")).join(""),t=(e.analytics?"a":"")+(e.marketing?"m":"")||"-";return r.cookie=`${a.name}=${a.v}.${Date.now().toString(36)}.${n}.${t}; Path=/; Max-Age=${a.days*86400}; SameSite=Lax`+(a.domain?`; Domain=${a.domain}`:"")+(o.location.protocol==="https:"?"; Secure":""),{id:n,c:e}}function S(e){let n=r.createElement("script");n.async=!0,n.src=e,r.head.append(n)}function _(e){let n=[...e.analytics?a.ga:[],...e.marketing?a.ads:[]].filter(t=>!b.has(t));if(n.length){let t=e.marketing?"granted":"denied",i=[g("consent","update",{analytics_storage:e.analytics?"granted":"denied",ad_storage:t,ad_user_data:t,ad_personalization:t})];b.size||i.push(g("js",new Date));for(let s of n)i.push(g("config",s)),b.add(s);if(A)for(let s of i)o.dataLayer.push(s);else{let s=o.dataLayer;s.splice(s.indexOf($)+1,0,...i),S(`https://www.googletagmanager.com/gtag/js?id=${encodeURIComponent(n[0])}`),A=!0}}if(e.marketing&&a.meta.length&&!E&&(E=!0,o.fbq.queue.unshift(...a.meta.map(t=>g("init",t)),g("track","PageView")),S("https://connect.facebook.net/en_US/fbevents.js")),e.marketing)for(let t of r.querySelectorAll("keksi-youtube"))t.show(!0)}function M(e){let n=e.flatMap(c=>a.del[c]),t=c=>n.some(u=>u.endsWith("*")?c.startsWith(u.slice(0,-1)):c===u),i=o.location.hostname.split("."),s=["",...i.map((c,u)=>`; Domain=${i.slice(u).join(".")}`)];for(let c of r.cookie.split("; ")){let u=c.split("=")[0];if(t(u))for(let y of s)r.cookie=`${u}=; Max-Age=0; Path=/${y}`}try{for(let c of Object.keys(o.localStorage))t(c)&&o.localStorage.removeItem(c)}catch{}}function f(e){let n=l?.c;l=T(e),k(),r.dispatchEvent(new o.CustomEvent("keksi:change",{detail:{...e}})),M(v.filter(t=>!e[t])),v.some(t=>n?.[t]&&!e[t])?o.location.reload():_(e)}function k(){let e=h("keksi-banner");e&&(e.hidden=!!l)}function x(){let e=h("keksi-dialog");if(e){for(let n of v){let t=e.querySelector(`[name=${n}]`);t&&(t.checked=!!l?.c[n])}e.showModal()}}r.addEventListener("click",e=>{let n=e.target.closest?.("[data-keksi],[data-keksi-open]");if(!n)return;if(n.hasAttribute("data-keksi-open"))return e.preventDefault(),x();let t=n.getAttribute("data-keksi");if(t==="accept")f({analytics:!0,marketing:!0});else if(t==="reject")f(C);else if(t==="save"){let i=s=>!!h("keksi-dialog")?.querySelector(`[name=${s}]`)?.checked;f({analytics:i("analytics"),marketing:i("marketing")})}}),r.addEventListener("astro:after-swap",k);class j extends o.HTMLElement{connectedCallback(){this.show(!!l?.c.marketing)}show(n,t=!1){let i=encodeURIComponent(this.getAttribute("video")??""),s=this.getAttribute("label");if(n){if(this.querySelector("iframe"))return;let d=r.createElement("iframe");d.src=`https://www.youtube-nocookie.com/embed/${i}${t?"?autoplay=1":""}`,d.title=s??"YouTube",d.allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share",d.allowFullscreen=!0,d.referrerPolicy="strict-origin-when-cross-origin",this.replaceChildren(d),t&&d.focus();return}let c=r.createElement("div");c.className="keksi-yt";let u=this.getAttribute("poster");u&&(c.style.background=`linear-gradient(#000a,#000a),center/cover url(${JSON.stringify(u)})`);let y=r.createElement("p");y.textContent=a.t.ytText;let m=r.createElement("button");m.type="button",m.textContent=a.t.ytButton,s&&m.setAttribute("aria-label",`${a.t.ytButton}: ${s}`),m.onclick=()=>this.show(!0,!0);let p=r.createElement("a");p.href=`https://www.youtube.com/watch?v=${i}`,p.target="_blank",p.rel="noopener",p.textContent=a.t.ytOpen,c.append(y,m,p),this.replaceChildren(c)}}a.yt&&!o.customElements.get("keksi-youtube")&&o.customElements.define("keksi-youtube",j);let L={get consent(){return l?{...l.c}:null},open:x,save:e=>f({...l?.c??C,...e}),acceptAll:()=>f({analytics:!0,marketing:!0}),rejectAll:()=>f(C),sync:k};return o.keksi=L,k(),l&&_(l.c),L}q(JSON.parse(document.getElementById("keksi-data").textContent));})();'}</script>`;
336
+ return `<style${n}>${css}</style><section id="keksi-banner" class="keksi" aria-labelledby="keksi-title" hidden><div class="keksi-main"><div class="keksi-head"><h2 id="keksi-title">${e(t.title)}</h2></div><div class="keksi-body"><p>${e(t.intro)}</p><ul>${uses}</ul><p>${e(t.change)}${policy ? ` <a href="${e(policy)}">${e(t.more)}</a>` : ""}</p></div><div class="keksi-actions"><button type="button" class="keksi-text" data-keksi-open>${e(t.settings)}</button>${btn("reject", t.reject)}${btn("accept", t.accept)}</div></div><form id="keksi-settings" class="keksi-settings" aria-labelledby="keksi-settings-title" hidden><div class="keksi-head"><h2 id="keksi-settings-title">${e(t.settingsTitle)}</h2><button type="button" class="keksi-close" data-keksi="close" aria-label="${e(t.close)}">\xD7</button></div><div class="keksi-body">${sections(config, lang, 3, true)}</div><div class="keksi-actions">${btn("save", t.save)}${btn("reject", t.reject)}${btn("accept", t.accept)}</div></form></section><script type="application/json" id="keksi-data">${json}</script><script${n}>${'"use strict";(()=>{var F=(n,s)=>new RegExp(`^${n.split("*").map(i=>i.replace(/[.+?^${}()|[\\]\\\\]/g,"\\\\$&")).join(".*")}$`).test(s);var _=["analytics","marketing"],v={analytics:!1,marketing:!1};function f(...n){return arguments}function H(n,s=window){if(typeof s.keksi?.sync=="function")return s.keksi.sync(),s.keksi;let i=s.document,g=e=>i.getElementById(e),c=I(),A=!1,w=!1,L=!1,E=new Set,x=f("consent","default",{ad_storage:"denied",ad_user_data:"denied",ad_personalization:"denied",analytics_storage:"denied"});if(n.ga.length||n.ads.length){let e=s.dataLayer??(s.dataLayer=[]);s.gtag=function(){e.push(arguments)},e.push(x)}if(n.meta.length){let e=function(){e.callMethod?e.callMethod.apply(e,[...arguments]):e.queue.push(arguments)};Object.assign(e,{push:e,loaded:!0,version:"2.0",queue:[]}),s.fbq=s._fbq=e}if(n.ph){let e=Object.assign([],{_i:[],people:[]});for(let o of"capture identify alias reset register register_once unregister group setPersonProperties getFeatureFlag isFeatureEnabled onFeatureFlags reloadFeatureFlags get_distinct_id startSessionRecording stopSessionRecording captureException on".split(" "))e[o]=(...t)=>e.push([o,...t]);s.posthog=e}function I(){let e=i.cookie.split("; ").find(l=>l.startsWith(`${n.name}=`))?.slice(n.name.length+1),[o,,t,a]=e?.split(".")??[];if(o!==n.v||!t||!a)return null;let r={analytics:a.includes("a"),marketing:a.includes("m")};return{id:t,c:r,yt:r.marketing||a.includes("y"),done:!a.includes("u")}}function T(e,o=e.marketing,t=!0){let a=c?.id??Array.from(s.crypto.getRandomValues(new Uint8Array(8)),l=>l.toString(16).padStart(2,"0")).join(""),r=(t?"":"u")+(e.analytics?"a":"")+(e.marketing?"m":"")+(o&&!e.marketing?"y":"")||"-";return i.cookie=`${n.name}=${n.v}.${Date.now().toString(36)}.${a}.${r}; Path=/; Max-Age=${n.days*86400}; SameSite=Lax`+(n.domain?`; Domain=${n.domain}`:"")+(s.location.protocol==="https:"?"; Secure":""),{id:a,c:e,yt:o,done:t}}function $(e){let o=i.createElement("script");return o.async=!0,o.src=e,i.head.append(o),o}function C(e){let o=[...e.analytics?n.ga:[],...e.marketing?n.ads:[]].filter(t=>!E.has(t));if(o.length){let t=e.marketing?"granted":"denied",a=[f("consent","update",{analytics_storage:e.analytics?"granted":"denied",ad_storage:t,ad_user_data:t,ad_personalization:t})];E.size||a.push(f("js",new Date));for(let r of o)a.push(f("config",r)),E.add(r);if(A)for(let r of a)s.dataLayer.push(r);else{let r=s.dataLayer;r.splice(r.indexOf(x)+1,0,...a),$(`https://www.googletagmanager.com/gtag/js?id=${encodeURIComponent(o[0])}`),A=!0}}if(e.marketing&&n.meta.length&&!w&&(w=!0,s.fbq.queue.unshift(...n.meta.map(t=>f("init",t)),f("track","PageView")),$("https://connect.facebook.net/en_US/fbevents.js")),e.analytics&&n.ph&&!L&&(L=!0,s.posthog._i.push([n.ph.key,{api_host:n.ph.host,defaults:"2026-08-30"},"posthog"]),$(`${n.ph.host.replace(".i.posthog.com","-assets.i.posthog.com")}/static/array.js`).crossOrigin="anonymous"),c?.yt)for(let t of i.querySelectorAll("keksi-youtube"))t.show(!0)}function O(){c?.yt||(c=T(c?.c??v,!0,c?.done??!1)),C(c.c)}function R(e){let o=e.flatMap(l=>n.del[l]),t=l=>o.some(u=>F(u,l)),a=s.location.hostname.split("."),r=["",...a.map((l,u)=>`; Domain=${a.slice(u).join(".")}`)];for(let l of i.cookie.split("; ")){let u=l.split("=")[0];if(t(u))for(let b of r)i.cookie=`${u}=; Max-Age=0; Path=/${b}`}try{for(let l of[s.localStorage,s.sessionStorage])for(let u of Object.keys(l))t(u)&&l.removeItem(u)}catch{}}function p(e){let o=c;c=T(e),h(),i.dispatchEvent(new s.CustomEvent("keksi:change",{detail:{...e}})),R(_.filter(t=>!e[t])),_.some(t=>o?.c[t]&&!e[t])||o?.yt&&!c.yt?s.location.reload():C(e)}function y(e=!1,o=n.banner&&!c?.done||e){let t=g("keksi-banner"),a=g("keksi-settings");!t||!a||(t.hidden=!o,a.hidden=!e,t.firstElementChild.hidden=e)}function h(){y()}function M(){let e=g("keksi-settings");for(let o of _){let t=e?.querySelector(`[name=${o}]`);t&&(t.checked=!!c?.c[o])}y(!0),e?.querySelector(".keksi-close")?.focus()}i.addEventListener("click",e=>{let o=e.target.closest?.("[data-keksi],[data-keksi-open]");if(!o)return;if(o.hasAttribute("data-keksi-open"))return e.preventDefault(),M();let t=o.getAttribute("data-keksi");if(t==="close")y();else if(t==="accept")p({analytics:!0,marketing:!0});else if(t==="reject")p(v);else if(t==="save"){let a=r=>!!g("keksi-settings")?.querySelector(`[name=${r}]`)?.checked;p({analytics:a("analytics"),marketing:a("marketing")})}}),i.addEventListener("keydown",e=>{e.key==="Escape"&&g("keksi-settings")?.hidden===!1&&y()}),i.addEventListener("astro:after-swap",h);class D extends s.HTMLElement{connectedCallback(){this.show(!!c?.yt)}show(o,t=!1){let a=encodeURIComponent(this.getAttribute("video")??""),r=this.getAttribute("label");if(o){if(this.querySelector("iframe"))return;let d=i.createElement("iframe");d.src=`https://www.youtube-nocookie.com/embed/${a}${t?"?autoplay=1":""}`,d.title=r??"YouTube",d.allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share",d.allowFullscreen=!0,d.referrerPolicy="strict-origin-when-cross-origin",this.replaceChildren(d),t&&d.focus();return}let l=i.createElement("div");l.className="keksi-yt";let u=this.getAttribute("poster");u&&l.style.setProperty("--keksi-poster",`url(${JSON.stringify(u)})`);let b=i.createElement("p");b.textContent=n.t.ytText;let m=i.createElement("button");m.type="button",m.textContent=n.t.ytButton,r&&m.setAttribute("aria-label",`${n.t.ytButton}: ${r}`),m.onclick=()=>{this.show(!0,!0),O()};let S=i.createElement("p"),q=(d,N,P=!1)=>{let k=i.createElement("a");k.href=d,k.textContent=N,P&&(k.target="_blank",k.rel="noopener"),S.append(S.childNodes.length?" \\xB7 ":"",k)};q(`https://www.youtube.com/watch?v=${a}`,n.t.ytOpen,!0),n.policy&&q(n.policy,n.t.more),l.append(b,m,S),this.replaceChildren(l)}}n.yt&&!s.customElements.get("keksi-youtube")&&s.customElements.define("keksi-youtube",D);let j={get consent(){return c?.done?{...c.c}:null},open:M,save:e=>p({...c?.c??v,...e}),acceptAll:()=>p({analytics:!0,marketing:!0}),rejectAll:()=>p(v),sync:h};return s.keksi=j,h(),c&&C(c.c),j}H(JSON.parse(document.getElementById("keksi-data").textContent));})();'}</script>`;
283
337
  }
284
338
  function sections(config, lang, h, dialog) {
285
339
  const t = textsFor(config, lang);
@@ -297,7 +351,7 @@ function sections(config, lang, h, dialog) {
297
351
  const items = c === "necessary" ? [site] : active.filter((p) => p.category === c).map((p) => ({ ...p, purpose: p.purpose[lang] }));
298
352
  const head = c === "necessary" ? `<h${h}>${e(t.necessary)}</h${h}><span>${e(t.alwaysOn)}</span>` : dialog ? `<h${h}><label for="keksi-c-${c}">${e(t[c])}</label></h${h}><input type="checkbox" role="switch" id="keksi-c-${c}" name="${c}" aria-describedby="keksi-i-${c}">` : `<h${h}>${e(t[c])}</h${h}>`;
299
353
  const list2 = items.map(
300
- (p) => `<div class="keksi-provider"><h${h + 1}>${e(p.name)}</h${h + 1}>${p.purpose ? `<p>${e(p.purpose)}</p>` : ""}` + (p.company && p.privacyPolicy ? `<p>${e(p.company)} \xB7 <a href="${e(p.privacyPolicy)}" target="_blank" rel="noopener">${e(t.privacyPolicy)}</a></p>` + (p.transfer ? `<p>${e(t.dpf)}</p>` : "") : "") + table(p.cookies, t) + `</div>`
354
+ (p) => `<div class="keksi-provider"><h${h + 1}>${e(p.name)}</h${h + 1}>${p.purpose ? `<p>${e(p.purpose)}</p>` : ""}` + (p.company && p.privacyPolicy ? `<p>${e(p.company)} \xB7 <a href="${e(p.privacyPolicy)}" target="_blank" rel="noopener">${e(t.privacyPolicy)}</a></p>` + (p.transfer ? `<p>${e(t[p.transfer])}</p>` : "") : "") + table(p.cookies, t) + `</div>`
301
355
  ).join("");
302
356
  const body = dialog ? `<details><summary>${e(t.providers)} (${items.length})</summary>${list2}</details>` : list2;
303
357
  return `<section class="keksi-cat"><div class="keksi-cat-head">${head}</div><p id="keksi-i-${c}">${e(t[`${c}Info`])}</p>${body}</section>`;
package/dist/types.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import type { Texts } from './i18n.js';
2
2
  export type Locale = 'fi' | 'sv' | 'en';
3
3
  export type Category = 'analytics' | 'marketing';
4
- export type ProviderId = 'ga' | 'googleAds' | 'meta' | 'youtube';
4
+ export type ProviderId = 'ga' | 'googleAds' | 'meta' | 'posthog' | 'youtube';
5
5
  export type Consent = Record<Category, boolean>;
6
6
  export interface KeksiConfig {
7
7
  /** GA4 measurement ids, e.g. `G-XXXXXXX`. */
@@ -10,6 +10,11 @@ export interface KeksiConfig {
10
10
  googleAds?: string | string[];
11
11
  /** Meta Pixel ids. */
12
12
  meta?: string | string[];
13
+ /** PostHog project token and ingestion host (default `https://eu.i.posthog.com`). */
14
+ posthog?: {
15
+ key: string;
16
+ host?: string;
17
+ };
13
18
  /** Enables `<keksi-youtube>` embeds. */
14
19
  youtube?: boolean;
15
20
  /** Cookie policy page, linked from the banner. */
@@ -40,7 +45,7 @@ export interface CookieInfo {
40
45
  duration: Duration;
41
46
  /** Third-party host that sets it. Omitted = set on this site, deleted on withdrawal. */
42
47
  host?: string;
43
- storage?: 'localStorage';
48
+ storage?: 'localStorage' | 'sessionStorage';
44
49
  }
45
50
  export interface Provider {
46
51
  id: ProviderId;
@@ -49,8 +54,8 @@ export interface Provider {
49
54
  company: string;
50
55
  privacyPolicy: string;
51
56
  purpose: Record<Locale, string>;
52
- /** Transfer outside the EU and its legal basis. */
53
- transfer?: 'dpf';
57
+ /** Transfer outside the EU: under the DPF, or to the US with no basis stated. */
58
+ transfer?: 'dpf' | 'us';
54
59
  cookies: CookieInfo[];
55
60
  /** Hosts it contacts, for the Playwright check. */
56
61
  hosts: string[];
@@ -64,8 +69,15 @@ export interface RuntimeData {
64
69
  ga: string[];
65
70
  ads: string[];
66
71
  meta: string[];
72
+ ph?: {
73
+ key: string;
74
+ host: string;
75
+ };
67
76
  yt: boolean;
68
- /** First-party cookie and localStorage names per category, `*` suffix = prefix. */
77
+ /** False with YouTube only: the settings exist but the banner doesn't show by itself. */
78
+ banner: boolean;
79
+ policy?: string;
80
+ /** First-party cookie and storage names per category, `*` = wildcard. */
69
81
  del: Record<Category, string[]>;
70
- t: Pick<Texts, 'ytText' | 'ytButton' | 'ytOpen'>;
82
+ t: Pick<Texts, 'ytText' | 'ytButton' | 'ytOpen' | 'more'>;
71
83
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "keksi",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "Lean cookie consent for Google and Meta trackers",
5
5
  "license": "MIT",
6
6
  "type": "module",