@owlmeans/web-consent 0.1.18-rc.22 → 0.1.18-rc.24

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 @@ This package ships embedded agent skills under `agent-meta/`. After installing y
6
6
  your project's skill store (`.agents/skills/`):
7
7
 
8
8
  ```sh
9
- npx @owlmeans/agent-skills@^0.1.18-rc.28
9
+ npx @owlmeans/agent-skills@^0.1.18-rc.31
10
10
  ```
11
11
 
12
12
  The embedded files are version-matched to this package release. Do not edit them
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "schemaVersion": 2,
3
3
  "package": "@owlmeans/web-consent",
4
- "version": "0.1.18-rc.22",
5
- "generatedAt": "2026-09-17T08:16:11.231Z",
4
+ "version": "0.1.18-rc.24",
5
+ "generatedAt": "2026-09-21T21:54:22.962Z",
6
6
  "canonicalRepo": "https://github.com/owlmeans/common",
7
7
  "entries": [
8
8
  {
@@ -8,7 +8,7 @@ user-invocable: false
8
8
  # @owlmeans/web-consent
9
9
 
10
10
  **Layer:** Web (React)
11
- **Install:** `"@owlmeans/web-consent": "^0.1.18-rc.22"` in `dependencies`
11
+ **Install:** `"@owlmeans/web-consent": "^0.1.18-rc.24"` in `dependencies`
12
12
 
13
13
  The browser components of the consent set. The model — categories, storage, migration, the store,
14
14
  Consent Mode signalling — is `@owlmeans/consent`, and this package re-exports a **named selection**
@@ -41,7 +41,7 @@ components — see below.
41
41
  | `useConsent(opts?)` | This document's consent state and the actions over it (`UseConsentModel`) — **it also initialises the store on mount**, see below |
42
42
  | `useConsentCategory(key)` | Whether one category is granted, for a component gating a single thing |
43
43
  | `CookieConsentProps` / `CookiePolicyProps` / `ConsentLink` | The component props |
44
- | Re-exports from `@owlmeans/consent` | The complete list: `consentStore`, `openConsent`, `isConsented`, `readConsent`, `writeConsent`, `clearConsent`, `migrateConsent`, `applyConsent`, `pushConsentDefaults`, `consentBootstrapScript`, `consentDefaults`, `consentUpdate`, `gtagConsent`, `DEFAULT_CONSENT_CATEGORIES`, `DEFAULT_CONSENT_MESSAGES`, `defaultConsentTranslate`, `interpolate`, `CONSENT_KEY`, `CONSENT_COOKIE_DAYS`, `CONSENT_SCHEMA_VERSION`, `CONSENT_LOCALES`, `CONSENT_ESSENTIAL` / `CONSENT_ANALYTICS` / `CONSENT_MARKETING`, and the types `ConsentCategory`, `ConsentOptions`, `ConsentReason`, `ConsentRecord`, `ConsentSignal`, `ConsentState`, `ConsentStore`, `ConsentLocale` |
44
+ | Re-exports from `@owlmeans/consent` | The complete list: `consentStore`, `openConsent`, `isConsented`, `readConsent`, `writeConsent`, `clearConsent`, `migrateConsent`, `applyConsent`, `pushConsentDefaults`, `consentBootstrapScript`, `consentDefaults`, `consentUpdate`, `gtagConsent`, `DEFAULT_CONSENT_CATEGORIES`, `DEFAULT_CONSENT_MESSAGES`, `defaultConsentTranslate`, `interpolate`, `CONSENT_KEY`, `CONSENT_COOKIE_DAYS`, `CONSENT_SCHEMA_VERSION`, `CONSENT_LOCALES`, `CONSENT_ESSENTIAL` / `CONSENT_ANALYTICS` / `CONSENT_MARKETING`, and the types `ConsentCategory`, `ConsentOptions`, `ConsentReason`, `ConsentRecord`, `ConsentService`, `ConsentSignal`, `ConsentState`, `ConsentStore`, `ConsentLocale` |
45
45
 
46
46
  ## Mounting the dialog
47
47
 
@@ -63,7 +63,7 @@ import { CookieConsent } from '@owlmeans/web-consent'
63
63
  goes for `storageKey`, `cookieDays` and `cookieDomain`.
64
64
  - `locale` picks the packaged language; leave it unset and English is used, so an app with a
65
65
  language of its own passes it (or mounts `PanelCookieConsent`, which does). A region tag is
66
- reduced to its base (`pl-PL` → `pl`), and anything outside the seven falls back to English.
66
+ reduced to its base (`pl-PL` → `pl`), and anything outside `CONSENT_LOCALES` falls back to English.
67
67
  - `policyHref` is a plain string, so an app-resolved path, a framework route and a raw href all
68
68
  work — the component must not know how its host does routing.
69
69
  - `noReopenButton` hides the floating button for an app that offers a footer link instead; that link
@@ -72,8 +72,30 @@ import { CookieConsent } from '@owlmeans/web-consent'
72
72
  filled background, no border, no shadow, no hover-scale — `bg-transparent`, dimmed
73
73
  (`opacity-70`) at rest and picked out on hover/focus — because it sits on every page of a site
74
74
  for as long as a visitor stays and must read as a small fixture rather than compete with the
75
- page's own controls. The pictogram itself stays at its original 20px (`h-5 w-5`); only the
76
- chrome around it shrank. `[data-consent-reopen]` is still what any test or CSS override keys on.
75
+ page's own controls. The pictogram is 20px (`h-5 w-5`) inside an invisible 44px hit area
76
+ (`h-11 w-11`). `[data-consent-reopen]` is what any test or CSS override keys on.
77
+
78
+ ## The look — flat, from the host's tokens
79
+
80
+ The dialog is the first thing every new visitor of a generated app sees, so it follows the flat
81
+ rule those apps are held to: **no gradient, shadow, glow, glass or `backdrop-filter`** anywhere in
82
+ it. Everything is a theme token, so light and dark follow the host:
83
+
84
+ | Part | Classes |
85
+ |---|---|
86
+ | Overlay | `bg-black/70`, flat — no blur |
87
+ | Card | `rounded-3xl border border-border bg-background text-foreground`, no shadow or ring |
88
+ | Icon | `rounded-full bg-muted text-primary` |
89
+ | Category row | `rounded-2xl border border-border`; *Required* is plain uppercase muted text, never a badge |
90
+ | Switch | `bg-primary` on / `bg-muted-foreground/40` off, white knob; 44px hit area around the 24×44 track |
91
+ | Accept | the one accent pill — `rounded-full bg-primary text-primary-foreground min-h-11` |
92
+ | Save, policy *Manage preferences* | outlined pill — `rounded-full border-[1.5px] border-foreground bg-transparent`, `hover:bg-muted` |
93
+ | Links | muted, underlined at rest, `min-h-11` |
94
+
95
+ Every control shows a 3px `outline-ring` focus outline 3px off the element (`outline-3
96
+ outline-offset-3`; the switch draws it on its track through `peer-focus-visible`). An outline, not
97
+ a ring, so a host that zeroes shadows keeps it. Never reintroduce a decorative class here — the
98
+ package tests fail on `bg-gradient`, `backdrop-`, `blur`, `shadow` or `ring-primary` in the dialog.
77
99
  - `silent` skips every `dataLayer` and global write. It is for tests and for an app that runs no
78
100
  tags at all.
79
101
  - The draft is **re-seeded from storage every time the dialog opens**, not from the last render — a
@@ -145,13 +167,35 @@ why it is generated rather than written: a hand-written policy drifts the first
145
167
  changes, and nobody notices because nobody reads it until it matters.
146
168
 
147
169
  ```tsx
148
- <CookiePolicy operator="Example Sp. z o.o." privacyHref="/legal/privacy" termsHref="/legal/terms" />
170
+ import { googleTagServices } from '@owlmeans/web-gtm'
171
+
172
+ <CookiePolicy
173
+ operator="Example Sp. z o.o." privacyHref="/legal/privacy" termsHref="/legal/terms"
174
+ services={googleTag !== '' ? googleTagServices(googleTag) : undefined}
175
+ />
149
176
  ```
150
177
 
151
178
  Everything OwlMeans cannot assert on the operator's behalf — who the controller is, the lawful
152
179
  basis, how to exercise rights — is deferred to those two links. It also renders a *Manage
153
180
  preferences* control, so the policy page is a way back into the decision.
154
181
 
182
+ **`services`** (`ConsentService[]`, see the `consent` skill) discloses WHO receives data: each
183
+ service is listed inside the item of the category whose `key` it names — name, provider, purpose,
184
+ cookie names in `<code>`, and a link to the provider's privacy policy (new tab). A service whose
185
+ category is not among `categories` goes into a trailing *Other services* item rather than being
186
+ dropped. Without `services` the page renders exactly as it always has.
187
+
188
+ The structure stays one `h1` and lists: categories are `li`s, a category's services are a nested
189
+ `ul` named by `aria-label` (*Services — ‹category label›*), each service a `li` holding a `dl`.
190
+ Never add headings for categories or services — they would skip a level under the `h1`. Test and
191
+ CSS hooks: `[data-cookie-policy-category="<key>"]`, `[data-cookie-policy-services]`,
192
+ `[data-cookie-policy-service]`, `[data-cookie-policy-other]`.
193
+
194
+ The service labels (`policyProvider`, `policyPurpose`, `policyCookies`, `policyServicesOf`,
195
+ `policyServicePrivacy`, `policyOtherServices`, `policyOtherServicesDesc`) go through `translate`
196
+ with English defaults and are in the packaged bundle for every locale; the service's own `name`,
197
+ `provider` and `purpose` are data and render as given.
198
+
155
199
  ## Tailwind — one `@source` line, pointing at `src`
156
200
 
157
201
  The components emit Tailwind classes that exist nowhere in the consumer's own sources, and
@@ -188,7 +232,13 @@ Describe a glob in words, or keep it out of the comment.
188
232
  ## i18n
189
233
 
190
234
  Given no `translate` prop, the components resolve every string through the packaged bundle for
191
- `locale` — seven languages, the same set the framework supports. **Once a `translate` prop is given,
235
+ `locale` — every language in `CONSENT_LOCALES` (the framework's seven plus French). The bundle
236
+ lives in `@owlmeans/consent` (`src/i18n/<locale>.json`); a new key goes into every one of them,
237
+ and that package's tests fail on a missing key or a dropped `{{placeholder}}` in any locale.
238
+ This package's `tests/bundle.spec.ts` reads every `t('…')` literal from the components, so a new
239
+ string without an English bundle entry fails there. A JSON-only edit is not re-emitted by an
240
+ incremental `tsc -b` — rebuild `@owlmeans/consent` with `tsc -b --force`, or consumers keep the old
241
+ bundle from `build/i18n`. **Once a `translate` prop is given,
192
242
  the packaged bundle is not consulted at all**, so a wrapper that forwards a framework resolver alone
193
243
  renders the English default for every key the application has not overridden, in every language. The
194
244
  resolver must fall through to `defaultConsentTranslate(locale)` for the default — which is exactly
@@ -207,7 +257,8 @@ caller's, already resolved.
207
257
 
208
258
  - `consent` — the model: categories, `globalVar`, storage and migration, Consent Mode v2, the
209
259
  ordering rule. Read it before changing anything a category means
210
- - `web-gtm` — the head snippet that carries a stored decision to the tag manager
260
+ - `web-gtm` — the head snippet that carries a stored decision to a Google tag, and
261
+ `googleTagServices(id)` for this page's `services`
211
262
  - `astro` — stamping that snippet from a static site's layout
212
263
  - `login-methods` / `login-plugins` — the sign-in precondition that raises this dialog with
213
264
  `reason: 'login'`
@@ -1,12 +1,4 @@
1
1
  import type { FC } from 'react';
2
2
  import type { CookieConsentProps } from '../types.js';
3
- /**
4
- * The cookie preferences dialog, and the button that brings it back.
5
- *
6
- * Deliberately built from raw elements rather than shadcn primitives. One of the three surfaces
7
- * this serves is an Astro island on a site that vendors its own component library, and requiring a
8
- * consumer to install a UI family in order to render a consent notice would put the notice out of
9
- * reach of the site that needs it most.
10
- */
11
3
  export declare const CookieConsent: FC<CookieConsentProps>;
12
4
  //# sourceMappingURL=component.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"component.d.ts","sourceRoot":"","sources":["../../src/consent/component.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,OAAO,CAAA;AAS/B,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAA;AAErD;;;;;;;GAOG;AACH,eAAO,MAAM,aAAa,EAAE,EAAE,CAAC,kBAAkB,CAkIhD,CAAA"}
1
+ {"version":3,"file":"component.d.ts","sourceRoot":"","sources":["../../src/consent/component.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,OAAO,CAAA;AAS/B,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAA;AAkCrD,eAAO,MAAM,aAAa,EAAE,EAAE,CAAC,kBAAkB,CA+HhD,CAAA"}
@@ -12,7 +12,22 @@ import { ConsentToggle } from './toggle.js';
12
12
  * this serves is an Astro island on a site that vendors its own component library, and requiring a
13
13
  * consumer to install a UI family in order to render a consent notice would put the notice out of
14
14
  * reach of the site that needs it most.
15
+ *
16
+ * Flat by rule, because it is the first thing every new visitor of a generated app sees: the
17
+ * surface and every colour come from the host's theme tokens (ground, ink, muted, hairline, one
18
+ * accent), depth comes from a hairline border and the overlay alone, and nothing paints a
19
+ * gradient, a shadow, a glow or a `backdrop-filter`. The accept action is the one accent pill,
20
+ * the others are outlined pills; every control is at least 44px tall and shows a 3px focus ring.
21
+ */
22
+ /**
23
+ * The focus ring every control here shows: 3px solid in the theme's ring colour, 3px off the
24
+ * element. An outline rather than a box-shadow ring, so it survives a host that zeroes shadows.
15
25
  */
26
+ const FOCUS = 'focus-visible:outline-3 focus-visible:outline-offset-3 focus-visible:outline-ring';
27
+ /** A pill button: 44px tall at minimum, full width on a phone, sharing the row above that. */
28
+ const PILL = cn('inline-flex min-h-11 flex-1 items-center justify-center rounded-full px-6 py-2.5 text-[15px] transition-colors motion-safe:active:scale-[0.98]', FOCUS);
29
+ /** A text link: muted, underlined at rest (colour is never the only signal), 44px tall to tap. */
30
+ const LINK = cn('inline-flex min-h-11 items-center font-semibold underline decoration-1 underline-offset-4 transition-colors hover:text-foreground hover:decoration-2', FOCUS);
16
31
  export const CookieConsent = props => {
17
32
  const categories = props.categories ?? DEFAULT_CONSENT_CATEGORIES;
18
33
  const t = props.translate ?? defaultConsentTranslate(props.locale);
@@ -49,8 +64,8 @@ export const CookieConsent = props => {
49
64
  // today. Saying so, and relabelling the primary action, is what makes the interruption make
50
65
  // sense rather than look like the page asking twice.
51
66
  const gated = consent.reason === 'login';
52
- return _jsxs(_Fragment, { children: [consent.open && _jsx("div", { className: "fixed inset-0 z-[999998] flex items-center justify-center overflow-y-auto bg-black/70 px-4 py-6 backdrop-blur-md", "aria-modal": "true", role: "dialog", "aria-labelledby": "cc-title", "aria-describedby": "cc-desc", "data-consent-dialog": true, children: _jsxs("div", { className: cn('relative z-[999999] w-full max-w-lg rounded-2xl border border-border bg-popover/95 p-6 shadow-[0_25px_60px_-12px_rgba(0,0,0,0.5)] ring-1 ring-primary/20 backdrop-blur-xl sm:p-8', props.className), children: [_jsx("div", { className: "absolute inset-x-0 top-0 h-1 rounded-t-2xl bg-gradient-to-r from-primary to-secondary" }), _jsxs("div", { className: "mb-4 flex items-center gap-3", children: [_jsx("div", { className: "flex h-10 w-10 items-center justify-center rounded-xl bg-primary/10 text-primary", children: _jsx(Cookie, { className: "h-5 w-5", "aria-hidden": "true" }) }), _jsx("h2", { id: "cc-title", className: "text-xl font-bold text-popover-foreground sm:text-2xl", children: t('title', 'Cookie Preferences') })] }), _jsx("p", { id: "cc-desc", className: "mb-6 text-sm leading-relaxed text-muted-foreground", children: t('description', 'We use cookies to enhance your browsing experience, serve personalized ads or content, and analyze our traffic.') }), _jsx("div", { className: "space-y-3", children: categories.map(category => _jsx(ConsentToggle, { id: `cc-${category.key}`, label: t(category.labelKey, category.key), description: t(category.descriptionKey, ''), checked: draft[category.key] === true, ...(category.required === true ? { required: true } : {}), requiredLabel: t('required', 'Required'), onChange: value => setDraft(current => ({ ...current, [category.key]: value })) }, category.key)) }), gated && _jsx("p", { role: "status", "data-consent-reason": true, className: "mt-4 text-sm text-popover-foreground", children: t('loginReason', 'Signing in stores a session cookie. Accept essential cookies to continue.') }), _jsxs("div", { className: "mt-8 flex flex-col gap-3 sm:flex-row sm:gap-4", children: [_jsx("button", { type: "button", onClick: onSave, "data-consent-save": true, className: "flex-1 rounded-lg border border-border bg-transparent px-4 py-3 text-sm font-semibold text-popover-foreground transition-colors hover:bg-accent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", children: t('savePreferences', 'Save Preferences') }), _jsx("button", { type: "button", onClick: onAcceptAll, "data-consent-accept-all": true, className: "flex-1 rounded-lg bg-primary px-4 py-3 text-sm font-bold text-primary-foreground shadow-lg shadow-primary/25 transition-all hover:bg-primary/90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2", children: gated
67
+ return _jsxs(_Fragment, { children: [consent.open && _jsx("div", { className: "fixed inset-0 z-[999998] flex items-center justify-center overflow-y-auto bg-black/70 px-4 py-6", "aria-modal": "true", role: "dialog", "aria-labelledby": "cc-title", "aria-describedby": "cc-desc", "data-consent-dialog": true, children: _jsxs("div", { className: cn('relative z-[999999] w-full max-w-lg rounded-3xl border border-border bg-background p-6 text-foreground sm:p-8', props.className), children: [_jsxs("div", { className: "mb-4 flex items-center gap-3", children: [_jsx("div", { className: "flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-full bg-muted text-primary", children: _jsx(Cookie, { className: "h-5 w-5", "aria-hidden": "true" }) }), _jsx("h2", { id: "cc-title", className: "text-balance text-xl font-extrabold tracking-tight text-foreground sm:text-2xl", children: t('title', 'Cookie Preferences') })] }), _jsx("p", { id: "cc-desc", className: "mb-6 text-pretty text-sm leading-relaxed text-muted-foreground", children: t('description', 'We use cookies to enhance your browsing experience, serve personalized ads or content, and analyze our traffic.') }), _jsx("div", { className: "space-y-3", children: categories.map(category => _jsx(ConsentToggle, { id: `cc-${category.key}`, label: t(category.labelKey, category.key), description: t(category.descriptionKey, ''), checked: draft[category.key] === true, ...(category.required === true ? { required: true } : {}), requiredLabel: t('required', 'Required'), onChange: value => setDraft(current => ({ ...current, [category.key]: value })) }, category.key)) }), gated && _jsx("p", { role: "status", "data-consent-reason": true, className: "mt-4 text-sm font-semibold text-foreground", children: t('loginReason', 'Signing in stores a session cookie. Accept essential cookies to continue.') }), _jsxs("div", { className: "mt-8 flex flex-col gap-3 sm:flex-row sm:gap-4", children: [_jsx("button", { type: "button", onClick: onSave, "data-consent-save": true, className: cn(PILL, 'border-[1.5px] border-foreground bg-transparent font-semibold text-foreground hover:bg-muted'), children: t('savePreferences', 'Save Preferences') }), _jsx("button", { type: "button", onClick: onAcceptAll, "data-consent-accept-all": true, className: cn(PILL, 'bg-primary font-bold text-primary-foreground hover:bg-primary/90'), children: gated
53
68
  ? t('acceptAndContinue', 'Accept & continue')
54
- : t('acceptAll', 'Accept All') })] }), (props.policyHref != null || props.links != null) && _jsxs("div", { className: "mt-4 flex flex-wrap justify-center gap-3 text-xs text-muted-foreground", children: [props.policyHref != null && _jsx("a", { href: props.policyHref, target: "_blank", rel: "noopener noreferrer", className: "underline underline-offset-2 transition-colors hover:text-popover-foreground", children: t('policyLink', 'Cookie Policy') }), props.links?.map(link => _jsx("a", { href: link.href, target: "_blank", rel: "noopener noreferrer", className: "underline underline-offset-2 transition-colors hover:text-popover-foreground", children: t(link.labelKey, link.defaultLabel) }, link.href))] })] }) }), props.noReopenButton !== true && consent.record != null && !consent.open && _jsx("button", { type: "button", onClick: () => consent.openDialog('reopen'), "aria-label": t('openPreferences', 'Cookie preferences'), "data-consent-reopen": true, className: "fixed bottom-1 left-1 z-[999997] inline-flex items-center justify-center rounded border-0 bg-transparent p-1.5 text-muted-foreground opacity-70 transition-opacity hover:opacity-100 hover:text-primary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", children: _jsx(Cookie, { className: "h-5 w-5", "aria-hidden": "true" }) })] });
69
+ : t('acceptAll', 'Accept All') })] }), (props.policyHref != null || props.links != null) && _jsxs("div", { className: "mt-3 flex flex-wrap justify-center gap-x-4 text-[13px] text-muted-foreground", children: [props.policyHref != null && _jsx("a", { href: props.policyHref, target: "_blank", rel: "noopener noreferrer", className: LINK, children: t('policyLink', 'Cookie Policy') }), props.links?.map(link => _jsx("a", { href: link.href, target: "_blank", rel: "noopener noreferrer", className: LINK, children: t(link.labelKey, link.defaultLabel) }, link.href))] })] }) }), props.noReopenButton !== true && consent.record != null && !consent.open && _jsx("button", { type: "button", onClick: () => consent.openDialog('reopen'), "aria-label": t('openPreferences', 'Cookie preferences'), "data-consent-reopen": true, className: cn('fixed bottom-1 left-1 z-[999997] inline-flex h-11 w-11 items-center justify-center rounded border-0 bg-transparent p-0 text-muted-foreground opacity-70 transition-opacity hover:opacity-100 hover:text-primary focus-visible:opacity-100', FOCUS), children: _jsx(Cookie, { className: "h-5 w-5", "aria-hidden": "true" }) })] });
55
70
  };
56
71
  //# sourceMappingURL=component.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"component.js","sourceRoot":"","sources":["../../src/consent/component.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"component.js","sourceRoot":"","sources":["../../src/consent/component.tsx"],"names":[],"mappings":"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"}
@@ -9,5 +9,14 @@ export interface ConsentToggleProps {
9
9
  requiredLabel: string;
10
10
  onChange: (value: boolean) => void;
11
11
  }
12
+ /**
13
+ * One category row: a hairline card with the label, the description and a switch.
14
+ *
15
+ * Flat like the dialog it sits in — no tinted fill, no shadow, no badge. `Required` is plain muted
16
+ * text beside the label, because a required category is disclosure, not a status to decorate. The
17
+ * switch's hit area is 44px square around the visible 24×44 track, and it shows the dialog's 3px
18
+ * focus ring when reached from the keyboard (the real checkbox is visually hidden, so the ring is
19
+ * drawn on the track through `peer-focus-visible`).
20
+ */
12
21
  export declare const ConsentToggle: FC<ConsentToggleProps>;
13
22
  //# sourceMappingURL=toggle.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"toggle.d.ts","sourceRoot":"","sources":["../../src/consent/toggle.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,OAAO,CAAA;AAG/B,MAAM,WAAW,kBAAkB;IACjC,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,EAAE,MAAM,CAAA;IACb,WAAW,EAAE,MAAM,CAAA;IACnB,OAAO,EAAE,OAAO,CAAA;IAChB,0FAA0F;IAC1F,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,aAAa,EAAE,MAAM,CAAA;IACrB,QAAQ,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAA;CACnC;AAED,eAAO,MAAM,aAAa,EAAE,EAAE,CAAC,kBAAkB,CAgChD,CAAA"}
1
+ {"version":3,"file":"toggle.d.ts","sourceRoot":"","sources":["../../src/consent/toggle.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,OAAO,CAAA;AAG/B,MAAM,WAAW,kBAAkB;IACjC,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,EAAE,MAAM,CAAA;IACb,WAAW,EAAE,MAAM,CAAA;IACnB,OAAO,EAAE,OAAO,CAAA;IAChB,0FAA0F;IAC1F,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,aAAa,EAAE,MAAM,CAAA;IACrB,QAAQ,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAA;CACnC;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,aAAa,EAAE,EAAE,CAAC,kBAAkB,CAoChD,CAAA"}
@@ -1,4 +1,13 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { cn } from '../lib/utils.js';
3
- export const ConsentToggle = ({ id, label, description, checked, required, requiredLabel, onChange }) => (_jsxs("div", { className: "flex items-start justify-between rounded-lg border border-border/50 bg-muted/30 p-3", children: [_jsxs("div", { className: "flex-1 pr-4", children: [_jsxs("div", { className: "mb-0.5 flex items-center gap-2 text-sm font-semibold text-popover-foreground", children: [label, required === true && _jsx("span", { className: "rounded bg-primary/10 px-1.5 py-0.5 text-[10px] font-medium uppercase tracking-wide text-primary", children: requiredLabel })] }), _jsx("p", { className: "text-xs text-muted-foreground", children: description })] }), _jsxs("label", { className: cn('relative h-6 w-11 flex-shrink-0', required === true ? 'cursor-not-allowed opacity-70' : 'cursor-pointer'), children: [_jsx("input", { id: id, type: "checkbox", className: "sr-only", checked: checked || required === true, disabled: required === true, onChange: event => onChange(event.target.checked) }), _jsx("span", { className: cn('absolute inset-0 rounded-full transition-colors duration-300', checked || required === true ? 'bg-primary' : 'bg-muted-foreground/30') }), _jsx("span", { className: cn('absolute top-0.5 h-5 w-5 rounded-full bg-white shadow transition-transform duration-300', checked || required === true ? 'translate-x-5' : 'translate-x-0.5') })] })] }));
3
+ /**
4
+ * One category row: a hairline card with the label, the description and a switch.
5
+ *
6
+ * Flat like the dialog it sits in — no tinted fill, no shadow, no badge. `Required` is plain muted
7
+ * text beside the label, because a required category is disclosure, not a status to decorate. The
8
+ * switch's hit area is 44px square around the visible 24×44 track, and it shows the dialog's 3px
9
+ * focus ring when reached from the keyboard (the real checkbox is visually hidden, so the ring is
10
+ * drawn on the track through `peer-focus-visible`).
11
+ */
12
+ export const ConsentToggle = ({ id, label, description, checked, required, requiredLabel, onChange }) => (_jsxs("div", { className: "flex items-center justify-between gap-4 rounded-2xl border border-border p-4", children: [_jsxs("div", { className: "min-w-0 flex-1", children: [_jsxs("div", { className: "mb-0.5 flex flex-wrap items-center gap-x-2 text-sm font-bold text-foreground", children: [label, required === true && _jsx("span", { className: "text-xs font-bold uppercase tracking-wide text-muted-foreground", children: requiredLabel })] }), _jsx("p", { className: "text-pretty text-xs leading-relaxed text-muted-foreground", children: description })] }), _jsxs("label", { className: cn('relative inline-flex h-11 w-14 flex-shrink-0 items-center justify-center', required === true ? 'cursor-not-allowed opacity-50' : 'cursor-pointer'), children: [_jsx("input", { id: id, type: "checkbox", className: "peer sr-only", checked: checked || required === true, disabled: required === true, onChange: event => onChange(event.target.checked) }), _jsx("span", { className: cn('relative h-6 w-11 rounded-full transition-colors duration-300 peer-focus-visible:outline-3 peer-focus-visible:outline-offset-3 peer-focus-visible:outline-ring', checked || required === true ? 'bg-primary' : 'bg-muted-foreground/40'), children: _jsx("span", { className: cn('absolute top-0.5 left-0 h-5 w-5 rounded-full bg-white transition-transform duration-300', checked || required === true ? 'translate-x-5' : 'translate-x-0.5') }) })] })] }));
4
13
  //# sourceMappingURL=toggle.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"toggle.js","sourceRoot":"","sources":["../../src/consent/toggle.tsx"],"names":[],"mappings":";AACA,OAAO,EAAE,EAAE,EAAE,MAAM,iBAAiB,CAAA;AAapC,MAAM,CAAC,MAAM,aAAa,GAA2B,CACnD,EAAE,EAAE,EAAE,KAAK,EAAE,WAAW,EAAE,OAAO,EAAE,QAAQ,EAAE,aAAa,EAAE,QAAQ,EAAE,EACtE,EAAE,CAAC,CACH,eAAK,SAAS,EAAC,qFAAqF,aAClG,eAAK,SAAS,EAAC,aAAa,aAC1B,eAAK,SAAS,EAAC,8EAA8E,aAC1F,KAAK,EACL,QAAQ,KAAK,IAAI,IAAI,eAAM,SAAS,EAAC,kGAAkG,YACrI,aAAa,GACT,IACH,EACN,YAAG,SAAS,EAAC,+BAA+B,YAAE,WAAW,GAAK,IAC1D,EACN,iBAAO,SAAS,EAAE,EAAE,CAAC,iCAAiC,EAAE,QAAQ,KAAK,IAAI,CAAC,CAAC,CAAC,+BAA+B,CAAC,CAAC,CAAC,gBAAgB,CAAC,aAC7H,gBACE,EAAE,EAAE,EAAE,EACN,IAAI,EAAC,UAAU,EACf,SAAS,EAAC,SAAS,EACnB,OAAO,EAAE,OAAO,IAAI,QAAQ,KAAK,IAAI,EACrC,QAAQ,EAAE,QAAQ,KAAK,IAAI,EAC3B,QAAQ,EAAE,KAAK,CAAC,EAAE,CAAC,QAAQ,CAAC,KAAK,CAAC,MAAM,CAAC,OAAO,CAAC,GACjD,EACF,eAAM,SAAS,EAAE,EAAE,CACjB,8DAA8D,EAC9D,OAAO,IAAI,QAAQ,KAAK,IAAI,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,wBAAwB,CACvE,GAAI,EACL,eAAM,SAAS,EAAE,EAAE,CACjB,yFAAyF,EACzF,OAAO,IAAI,QAAQ,KAAK,IAAI,CAAC,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,iBAAiB,CACnE,GAAI,IACC,IACJ,CACP,CAAA"}
1
+ {"version":3,"file":"toggle.js","sourceRoot":"","sources":["../../src/consent/toggle.tsx"],"names":[],"mappings":";AACA,OAAO,EAAE,EAAE,EAAE,MAAM,iBAAiB,CAAA;AAapC;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,aAAa,GAA2B,CACnD,EAAE,EAAE,EAAE,KAAK,EAAE,WAAW,EAAE,OAAO,EAAE,QAAQ,EAAE,aAAa,EAAE,QAAQ,EAAE,EACtE,EAAE,CAAC,CACH,eAAK,SAAS,EAAC,8EAA8E,aAC3F,eAAK,SAAS,EAAC,gBAAgB,aAC7B,eAAK,SAAS,EAAC,8EAA8E,aAC1F,KAAK,EACL,QAAQ,KAAK,IAAI,IAAI,eAAM,SAAS,EAAC,iEAAiE,YACpG,aAAa,GACT,IACH,EACN,YAAG,SAAS,EAAC,2DAA2D,YAAE,WAAW,GAAK,IACtF,EACN,iBAAO,SAAS,EAAE,EAAE,CAClB,0EAA0E,EAC1E,QAAQ,KAAK,IAAI,CAAC,CAAC,CAAC,+BAA+B,CAAC,CAAC,CAAC,gBAAgB,CACvE,aACC,gBACE,EAAE,EAAE,EAAE,EACN,IAAI,EAAC,UAAU,EACf,SAAS,EAAC,cAAc,EACxB,OAAO,EAAE,OAAO,IAAI,QAAQ,KAAK,IAAI,EACrC,QAAQ,EAAE,QAAQ,KAAK,IAAI,EAC3B,QAAQ,EAAE,KAAK,CAAC,EAAE,CAAC,QAAQ,CAAC,KAAK,CAAC,MAAM,CAAC,OAAO,CAAC,GACjD,EACF,eAAM,SAAS,EAAE,EAAE,CACjB,gKAAgK,EAChK,OAAO,IAAI,QAAQ,KAAK,IAAI,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,wBAAwB,CACvE,YACC,eAAM,SAAS,EAAE,EAAE,CACjB,yFAAyF,EACzF,OAAO,IAAI,QAAQ,KAAK,IAAI,CAAC,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,iBAAiB,CACnE,GAAI,GACA,IACD,IACJ,CACP,CAAA"}
package/build/index.d.ts CHANGED
@@ -5,5 +5,5 @@ export * from './consent/widget.js';
5
5
  export * from './consent/toggle.js';
6
6
  export * from './policy/component.js';
7
7
  export { consentStore, openConsent, isConsented, readConsent, writeConsent, clearConsent, migrateConsent, applyConsent, pushConsentDefaults, consentBootstrapScript, consentDefaults, consentUpdate, gtagConsent, DEFAULT_CONSENT_CATEGORIES, DEFAULT_CONSENT_MESSAGES, defaultConsentTranslate, interpolate, CONSENT_KEY, CONSENT_COOKIE_DAYS, CONSENT_SCHEMA_VERSION, CONSENT_LOCALES, CONSENT_ESSENTIAL, CONSENT_ANALYTICS, CONSENT_MARKETING, } from '@owlmeans/consent';
8
- export type { ConsentCategory, ConsentOptions, ConsentReason, ConsentRecord, ConsentSignal, ConsentState, ConsentStore, ConsentLocale, } from '@owlmeans/consent';
8
+ export type { ConsentCategory, ConsentOptions, ConsentReason, ConsentRecord, ConsentService, ConsentSignal, ConsentState, ConsentStore, ConsentLocale, } from '@owlmeans/consent';
9
9
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,mBAAmB,YAAY,CAAA;AAC/B,cAAc,YAAY,CAAA;AAC1B,cAAc,wBAAwB,CAAA;AACtC,cAAc,qBAAqB,CAAA;AACnC,cAAc,qBAAqB,CAAA;AACnC,cAAc,uBAAuB,CAAA;AAErC,OAAO,EACL,YAAY,EAAE,WAAW,EAAE,WAAW,EAAE,WAAW,EAAE,YAAY,EAAE,YAAY,EAC/E,cAAc,EAAE,YAAY,EAAE,mBAAmB,EAAE,sBAAsB,EACzE,eAAe,EAAE,aAAa,EAAE,WAAW,EAC3C,0BAA0B,EAAE,wBAAwB,EAAE,uBAAuB,EAAE,WAAW,EAC1F,WAAW,EAAE,mBAAmB,EAAE,sBAAsB,EAAE,eAAe,EACzE,iBAAiB,EAAE,iBAAiB,EAAE,iBAAiB,GACxD,MAAM,mBAAmB,CAAA;AAC1B,YAAY,EACV,eAAe,EAAE,cAAc,EAAE,aAAa,EAAE,aAAa,EAAE,aAAa,EAAE,YAAY,EAC1F,YAAY,EAAE,aAAa,GAC5B,MAAM,mBAAmB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,mBAAmB,YAAY,CAAA;AAC/B,cAAc,YAAY,CAAA;AAC1B,cAAc,wBAAwB,CAAA;AACtC,cAAc,qBAAqB,CAAA;AACnC,cAAc,qBAAqB,CAAA;AACnC,cAAc,uBAAuB,CAAA;AAErC,OAAO,EACL,YAAY,EAAE,WAAW,EAAE,WAAW,EAAE,WAAW,EAAE,YAAY,EAAE,YAAY,EAC/E,cAAc,EAAE,YAAY,EAAE,mBAAmB,EAAE,sBAAsB,EACzE,eAAe,EAAE,aAAa,EAAE,WAAW,EAC3C,0BAA0B,EAAE,wBAAwB,EAAE,uBAAuB,EAAE,WAAW,EAC1F,WAAW,EAAE,mBAAmB,EAAE,sBAAsB,EAAE,eAAe,EACzE,iBAAiB,EAAE,iBAAiB,EAAE,iBAAiB,GACxD,MAAM,mBAAmB,CAAA;AAC1B,YAAY,EACV,eAAe,EAAE,cAAc,EAAE,aAAa,EAAE,aAAa,EAAE,cAAc,EAAE,aAAa,EAC5F,YAAY,EAAE,YAAY,EAAE,aAAa,GAC1C,MAAM,mBAAmB,CAAA"}
@@ -11,6 +11,12 @@ import type { CookiePolicyProps } from '../types.js';
11
11
  *
12
12
  * Everything OwlMeans cannot assert on the operator's behalf — who the controller is, what the
13
13
  * lawful basis is, how to exercise rights — is deferred to their own privacy policy and terms.
14
+ *
15
+ * `services` names WHO receives data under each category. It is the one input the widget cannot
16
+ * derive, so it comes from whatever added the tag (`googleTagServices(id)` for a Google tag), and
17
+ * a service is never dropped for want of a matching category: one whose category is not in force
18
+ * is listed in a trailing group, because a service the page runs and does not disclose is worse
19
+ * than one disclosed in the wrong place.
14
20
  */
15
21
  export declare const CookiePolicy: FC<CookiePolicyProps>;
16
22
  //# sourceMappingURL=component.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"component.d.ts","sourceRoot":"","sources":["../../src/policy/component.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,OAAO,CAAA;AAM/B,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAA;AAEpD;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,YAAY,EAAE,EAAE,CAAC,iBAAiB,CAmD9C,CAAA"}
1
+ {"version":3,"file":"component.d.ts","sourceRoot":"","sources":["../../src/policy/component.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,OAAO,CAAA;AAO/B,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAA;AAkDpD;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,YAAY,EAAE,EAAE,CAAC,iBAAiB,CAsE9C,CAAA"}
@@ -1,6 +1,14 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { CONSENT_COOKIE_DAYS, CONSENT_KEY, DEFAULT_CONSENT_CATEGORIES, defaultConsentTranslate, interpolate, openConsent, } from '@owlmeans/consent';
3
3
  import { cn } from '../lib/utils.js';
4
+ /**
5
+ * The services one category gates, as a nested list inside that category's item.
6
+ *
7
+ * A list and a description list rather than headings: the page's only heading is its `h1`, the
8
+ * categories are list items, and a service heading below them would either skip a level or
9
+ * promote a service above the category that governs it.
10
+ */
11
+ const ServiceList = ({ services, label, category, t }) => _jsx("ul", { "aria-label": interpolate(t('policyServicesOf', 'Services — {{category}}'), { category: label }), "data-cookie-policy-services": category ?? '', children: services.map((service, index) => _jsxs("li", { "data-cookie-policy-service": true, children: [_jsx("strong", { children: service.name }), _jsxs("dl", { children: [_jsxs("div", { children: [_jsx("dt", { children: t('policyProvider', 'Provider') }), _jsx("dd", { children: service.provider })] }), service.purpose != null && service.purpose !== '' && _jsxs("div", { children: [_jsx("dt", { children: t('policyPurpose', 'Purpose') }), _jsx("dd", { children: service.purpose })] }), service.cookies != null && service.cookies.length > 0 && _jsxs("div", { children: [_jsx("dt", { children: t('policyCookies', 'Cookies') }), _jsx("dd", { children: service.cookies.map((cookie, at) => _jsxs("span", { children: [at > 0 && ', ', _jsx("code", { children: cookie })] }, `${cookie}-${at}`)) })] })] }), service.privacyHref != null && service.privacyHref !== '' && _jsx("a", { href: service.privacyHref, target: "_blank", rel: "noopener noreferrer", children: interpolate(t('policyServicePrivacy', '{{provider}} privacy policy'), { provider: service.provider }) })] }, `${service.name}-${index}`)) });
4
12
  /**
5
13
  * A cookie policy that states only what this application can actually assert.
6
14
  *
@@ -12,12 +20,26 @@ import { cn } from '../lib/utils.js';
12
20
  *
13
21
  * Everything OwlMeans cannot assert on the operator's behalf — who the controller is, what the
14
22
  * lawful basis is, how to exercise rights — is deferred to their own privacy policy and terms.
23
+ *
24
+ * `services` names WHO receives data under each category. It is the one input the widget cannot
25
+ * derive, so it comes from whatever added the tag (`googleTagServices(id)` for a Google tag), and
26
+ * a service is never dropped for want of a matching category: one whose category is not in force
27
+ * is listed in a trailing group, because a service the page runs and does not disclose is worse
28
+ * than one disclosed in the wrong place.
15
29
  */
16
30
  export const CookiePolicy = props => {
17
31
  const categories = props.categories ?? DEFAULT_CONSENT_CATEGORIES;
18
32
  const t = props.translate ?? defaultConsentTranslate(props.locale);
19
33
  const storageKey = props.storageKey ?? CONSENT_KEY;
20
34
  const days = props.cookieDays ?? CONSENT_COOKIE_DAYS;
21
- return _jsxs("article", { className: cn('prose prose-sm max-w-2xl', props.className), "data-cookie-policy": true, children: [_jsx("h1", { children: t('policyTitle', 'Cookie Policy') }), _jsx("p", { children: t('policyIntro', 'This page describes the cookies and similar storage this application uses, and how you can control them.') }), _jsx("ul", { children: categories.map(category => _jsxs("li", { children: [_jsx("strong", { children: t(category.labelKey, category.key) }), category.required === true && _jsxs(_Fragment, { children: [" \u2014 ", t('required', 'Required')] }), _jsx("br", {}), t(category.descriptionKey, '')] }, category.key)) }), _jsx("p", { children: interpolate(t('policyStorage', 'Your choice is stored in this browser under "{{key}}", both in local storage and as a cookie.'), { key: storageKey }) }), _jsx("p", { children: interpolate(t('policyRetention', 'The record is kept for {{days}} days, after which you will be asked again.'), { days }) }), props.operator != null && props.operator !== '' && _jsxs("p", { children: [interpolate(t('policyOperator', 'This application is operated by {{operator}}.'), { operator: props.operator }), ' ', t('policyContact', 'See their privacy policy and terms for how they handle your data.')] }), _jsxs("p", { children: [props.privacyHref != null && props.privacyHref !== '' && _jsx("a", { href: props.privacyHref, target: "_blank", rel: "noopener noreferrer", children: t('privacy', 'Privacy Policy') }), props.privacyHref != null && props.termsHref != null && ' · ', props.termsHref != null && props.termsHref !== '' && _jsx("a", { href: props.termsHref, target: "_blank", rel: "noopener noreferrer", children: t('terms', 'Terms & Conditions') })] }), _jsx("p", { children: _jsx("button", { type: "button", onClick: () => openConsent('reopen'), "data-cookie-policy-manage": true, children: t('manage', 'Manage preferences') }) })] });
35
+ const services = props.services ?? [];
36
+ const keys = new Set(categories.map(category => category.key));
37
+ const other = services.filter(service => !keys.has(service.category));
38
+ const otherLabel = t('policyOtherServices', 'Other services');
39
+ return _jsxs("article", { className: cn('prose prose-sm max-w-2xl', props.className), "data-cookie-policy": true, children: [_jsx("h1", { children: t('policyTitle', 'Cookie Policy') }), _jsx("p", { children: t('policyIntro', 'This page describes the cookies and similar storage this application uses, and how you can control them.') }), _jsxs("ul", { children: [categories.map(category => {
40
+ const label = t(category.labelKey, category.key);
41
+ const gated = services.filter(service => service.category === category.key);
42
+ return _jsxs("li", { "data-cookie-policy-category": category.key, children: [_jsx("strong", { children: label }), category.required === true && _jsxs(_Fragment, { children: [" \u2014 ", t('required', 'Required')] }), _jsx("br", {}), t(category.descriptionKey, ''), gated.length > 0 && _jsx(ServiceList, { services: gated, label: label, category: category.key, t: t })] }, category.key);
43
+ }), other.length > 0 && _jsxs("li", { "data-cookie-policy-other": true, children: [_jsx("strong", { children: otherLabel }), _jsx("br", {}), t('policyOtherServicesDesc', 'Services this application runs outside the categories above.'), _jsx(ServiceList, { services: other, label: otherLabel, t: t })] }, " other")] }), _jsx("p", { children: interpolate(t('policyStorage', 'Your choice is stored in this browser under "{{key}}", both in local storage and as a cookie.'), { key: storageKey }) }), _jsx("p", { children: interpolate(t('policyRetention', 'The record is kept for {{days}} days, after which you will be asked again.'), { days }) }), props.operator != null && props.operator !== '' && _jsxs("p", { children: [interpolate(t('policyOperator', 'This application is operated by {{operator}}.'), { operator: props.operator }), ' ', t('policyContact', 'See their privacy policy and terms for how they handle your data.')] }), _jsxs("p", { children: [props.privacyHref != null && props.privacyHref !== '' && _jsx("a", { href: props.privacyHref, target: "_blank", rel: "noopener noreferrer", children: t('privacy', 'Privacy Policy') }), props.privacyHref != null && props.termsHref != null && ' · ', props.termsHref != null && props.termsHref !== '' && _jsx("a", { href: props.termsHref, target: "_blank", rel: "noopener noreferrer", children: t('terms', 'Terms & Conditions') })] }), _jsx("p", { children: _jsx("button", { type: "button", onClick: () => openConsent('reopen'), "data-cookie-policy-manage": true, className: "not-prose inline-flex min-h-11 items-center justify-center rounded-full border-[1.5px] border-foreground bg-transparent px-6 py-2.5 text-[15px] font-semibold text-foreground transition-colors hover:bg-muted focus-visible:outline-3 focus-visible:outline-offset-3 focus-visible:outline-ring", children: t('manage', 'Manage preferences') }) })] });
22
44
  };
23
45
  //# sourceMappingURL=component.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"component.js","sourceRoot":"","sources":["../../src/policy/component.tsx"],"names":[],"mappings":";AACA,OAAO,EACL,mBAAmB,EAAE,WAAW,EAAE,0BAA0B,EAC5D,uBAAuB,EAAE,WAAW,EAAE,WAAW,GAClD,MAAM,mBAAmB,CAAA;AAC1B,OAAO,EAAE,EAAE,EAAE,MAAM,iBAAiB,CAAA;AAGpC;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,MAAM,YAAY,GAA0B,KAAK,CAAC,EAAE;IACzD,MAAM,UAAU,GAAG,KAAK,CAAC,UAAU,IAAI,0BAA0B,CAAA;IACjE,MAAM,CAAC,GAAG,KAAK,CAAC,SAAS,IAAI,uBAAuB,CAAC,KAAK,CAAC,MAAM,CAAC,CAAA;IAClE,MAAM,UAAU,GAAG,KAAK,CAAC,UAAU,IAAI,WAAW,CAAA;IAClD,MAAM,IAAI,GAAG,KAAK,CAAC,UAAU,IAAI,mBAAmB,CAAA;IAEpD,OAAO,mBAAS,SAAS,EAAE,EAAE,CAAC,0BAA0B,EAAE,KAAK,CAAC,SAAS,CAAC,yCACxE,uBAAK,CAAC,CAAC,aAAa,EAAE,eAAe,CAAC,GAAM,EAC5C,sBAAI,CAAC,CAAC,aAAa,EAAE,0GAA0G,CAAC,GAAK,EAErI,uBACG,UAAU,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,CAAC,yBAC1B,2BAAS,CAAC,CAAC,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC,GAAG,CAAC,GAAU,EACpD,QAAQ,CAAC,QAAQ,KAAK,IAAI,IAAI,0CAAM,CAAC,CAAC,UAAU,EAAE,UAAU,CAAC,IAAI,EAClE,cAAM,EACL,CAAC,CAAC,QAAQ,CAAC,cAAc,EAAE,EAAE,CAAC,KAJI,QAAQ,CAAC,GAAG,CAK5C,CAAC,GACH,EAEL,sBAAI,WAAW,CACb,CAAC,CAAC,eAAe,EAAE,+FAA+F,CAAC,EACnH,EAAE,GAAG,EAAE,UAAU,EAAE,CACpB,GAAK,EACN,sBAAI,WAAW,CACb,CAAC,CAAC,iBAAiB,EAAE,4EAA4E,CAAC,EAClG,EAAE,IAAI,EAAE,CACT,GAAK,EAEL,KAAK,CAAC,QAAQ,IAAI,IAAI,IAAI,KAAK,CAAC,QAAQ,KAAK,EAAE,IAAI,wBACjD,WAAW,CAAC,CAAC,CAAC,gBAAgB,EAAE,+CAA+C,CAAC,EAC/E,EAAE,QAAQ,EAAE,KAAK,CAAC,QAAQ,EAAE,CAAC,EAC9B,GAAG,EACH,CAAC,CAAC,eAAe,EAAE,mEAAmE,CAAC,IACtF,EAEJ,wBACG,KAAK,CAAC,WAAW,IAAI,IAAI,IAAI,KAAK,CAAC,WAAW,KAAK,EAAE,IAAI,YACxD,IAAI,EAAE,KAAK,CAAC,WAAW,EAAE,MAAM,EAAC,QAAQ,EAAC,GAAG,EAAC,qBAAqB,YAClE,CAAC,CAAC,SAAS,EAAE,gBAAgB,CAAC,GAAK,EACpC,KAAK,CAAC,WAAW,IAAI,IAAI,IAAI,KAAK,CAAC,SAAS,IAAI,IAAI,IAAI,KAAK,EAC7D,KAAK,CAAC,SAAS,IAAI,IAAI,IAAI,KAAK,CAAC,SAAS,KAAK,EAAE,IAAI,YACpD,IAAI,EAAE,KAAK,CAAC,SAAS,EAAE,MAAM,EAAC,QAAQ,EAAC,GAAG,EAAC,qBAAqB,YAChE,CAAC,CAAC,OAAO,EAAE,oBAAoB,CAAC,GAAK,IACrC,EAEJ,sBACE,iBAAQ,IAAI,EAAC,QAAQ,EAAC,OAAO,EAAE,GAAG,EAAE,CAAC,WAAW,CAAC,QAAQ,CAAC,+CACvD,CAAC,CAAC,QAAQ,EAAE,oBAAoB,CAAC,GAC3B,GACP,IACI,CAAA;AACZ,CAAC,CAAA"}
1
+ {"version":3,"file":"component.js","sourceRoot":"","sources":["../../src/policy/component.tsx"],"names":[],"mappings":"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"}
package/build/types.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { ConsentCategory, ConsentReason, ConsentRecord } from '@owlmeans/consent';
1
+ import type { ConsentCategory, ConsentReason, ConsentRecord, ConsentService } from '@owlmeans/consent';
2
2
  export interface ConsentLink {
3
3
  href: string;
4
4
  labelKey: string;
@@ -34,6 +34,13 @@ export interface CookiePolicyProps {
34
34
  termsHref?: string;
35
35
  storageKey?: string;
36
36
  cookieDays?: number;
37
+ /**
38
+ * The third-party services the site runs, each listed under the category that gates it —
39
+ * name, provider, purpose, cookie names and the provider's privacy policy. A service whose
40
+ * category is not among `categories` is listed in a trailing "Other services" group rather than
41
+ * dropped. `@owlmeans/web-gtm`'s `googleTagServices(id)` supplies the entries for a Google tag.
42
+ */
43
+ services?: ConsentService[];
37
44
  className?: string;
38
45
  }
39
46
  export interface ConsentMenuWidgetProps {
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,eAAe,EAAE,aAAa,EAAE,aAAa,EAC9C,MAAM,mBAAmB,CAAA;AAE1B,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAA;IACZ,QAAQ,EAAE,MAAM,CAAA;IAChB,YAAY,EAAE,MAAM,CAAA;CACrB;AAED,MAAM,WAAW,kBAAkB;IACjC,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,UAAU,CAAC,EAAE,eAAe,EAAE,CAAA;IAC9B,uFAAuF;IACvF,SAAS,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,KAAK,MAAM,CAAA;IACzD;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,mEAAmE;IACnE,KAAK,CAAC,EAAE,WAAW,EAAE,CAAA;IACrB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,wFAAwF;IACxF,cAAc,CAAC,EAAE,OAAO,CAAA;IACxB,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,MAAM,WAAW,iBAAiB;IAChC,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,SAAS,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,KAAK,MAAM,CAAA;IACzD,UAAU,CAAC,EAAE,eAAe,EAAE,CAAA;IAC9B,0FAA0F;IAC1F,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,MAAM,WAAW,sBAAsB;IACrC,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,uFAAuF;IACvF,SAAS,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,KAAK,MAAM,CAAA;IACzD,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,4FAA4F;IAC5F,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAA;CACtB;AAED,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,aAAa,GAAG,IAAI,CAAA;IAC5B,IAAI,EAAE,OAAO,CAAA;IACb,MAAM,EAAE,aAAa,GAAG,IAAI,CAAA;IAC5B,OAAO,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAA;IACjC,IAAI,EAAE,CAAC,MAAM,EAAE,aAAa,KAAK,IAAI,CAAA;IACrC,SAAS,EAAE,MAAM,IAAI,CAAA;IACrB,UAAU,EAAE,CAAC,MAAM,CAAC,EAAE,aAAa,KAAK,IAAI,CAAA;IAC5C,KAAK,EAAE,MAAM,IAAI,CAAA;CAClB"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,eAAe,EAAE,aAAa,EAAE,aAAa,EAAE,cAAc,EAC9D,MAAM,mBAAmB,CAAA;AAE1B,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAA;IACZ,QAAQ,EAAE,MAAM,CAAA;IAChB,YAAY,EAAE,MAAM,CAAA;CACrB;AAED,MAAM,WAAW,kBAAkB;IACjC,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,UAAU,CAAC,EAAE,eAAe,EAAE,CAAA;IAC9B,uFAAuF;IACvF,SAAS,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,KAAK,MAAM,CAAA;IACzD;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,mEAAmE;IACnE,KAAK,CAAC,EAAE,WAAW,EAAE,CAAA;IACrB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,wFAAwF;IACxF,cAAc,CAAC,EAAE,OAAO,CAAA;IACxB,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,MAAM,WAAW,iBAAiB;IAChC,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,SAAS,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,KAAK,MAAM,CAAA;IACzD,UAAU,CAAC,EAAE,eAAe,EAAE,CAAA;IAC9B,0FAA0F;IAC1F,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,cAAc,EAAE,CAAA;IAC3B,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,MAAM,WAAW,sBAAsB;IACrC,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,uFAAuF;IACvF,SAAS,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,KAAK,MAAM,CAAA;IACzD,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,4FAA4F;IAC5F,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAA;CACtB;AAED,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,aAAa,GAAG,IAAI,CAAA;IAC5B,IAAI,EAAE,OAAO,CAAA;IACb,MAAM,EAAE,aAAa,GAAG,IAAI,CAAA;IAC5B,OAAO,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAA;IACjC,IAAI,EAAE,CAAC,MAAM,EAAE,aAAa,KAAK,IAAI,CAAA;IACrC,SAAS,EAAE,MAAM,IAAI,CAAA;IACrB,UAAU,EAAE,CAAC,MAAM,CAAC,EAAE,aAAa,KAAK,IAAI,CAAA;IAC5C,KAAK,EAAE,MAAM,IAAI,CAAA;CAClB"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@owlmeans/web-consent",
3
- "version": "0.1.18-rc.22",
3
+ "version": "0.1.18-rc.24",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -22,7 +22,7 @@
22
22
  }
23
23
  },
24
24
  "dependencies": {
25
- "@owlmeans/consent": "^0.1.18-rc.4"
25
+ "@owlmeans/consent": "^0.1.18-rc.5"
26
26
  },
27
27
  "peerDependencies": {
28
28
  "clsx": "*",
@@ -33,7 +33,7 @@
33
33
  },
34
34
  "devDependencies": {
35
35
  "@owlmeans/dep-config": "workspace:*",
36
- "@owlmeans/test-ui": "^0.1.18-rc.30",
36
+ "@owlmeans/test-ui": "^0.1.18-rc.32",
37
37
  "@tailwindcss/vite": "^4.0.0",
38
38
  "@types/bun": "^1.4.0",
39
39
  "@types/react": "^19.2.17",
@@ -17,7 +17,31 @@ import type { CookieConsentProps } from '../types.js'
17
17
  * this serves is an Astro island on a site that vendors its own component library, and requiring a
18
18
  * consumer to install a UI family in order to render a consent notice would put the notice out of
19
19
  * reach of the site that needs it most.
20
+ *
21
+ * Flat by rule, because it is the first thing every new visitor of a generated app sees: the
22
+ * surface and every colour come from the host's theme tokens (ground, ink, muted, hairline, one
23
+ * accent), depth comes from a hairline border and the overlay alone, and nothing paints a
24
+ * gradient, a shadow, a glow or a `backdrop-filter`. The accept action is the one accent pill,
25
+ * the others are outlined pills; every control is at least 44px tall and shows a 3px focus ring.
26
+ */
27
+ /**
28
+ * The focus ring every control here shows: 3px solid in the theme's ring colour, 3px off the
29
+ * element. An outline rather than a box-shadow ring, so it survives a host that zeroes shadows.
20
30
  */
31
+ const FOCUS = 'focus-visible:outline-3 focus-visible:outline-offset-3 focus-visible:outline-ring'
32
+
33
+ /** A pill button: 44px tall at minimum, full width on a phone, sharing the row above that. */
34
+ const PILL = cn(
35
+ 'inline-flex min-h-11 flex-1 items-center justify-center rounded-full px-6 py-2.5 text-[15px] transition-colors motion-safe:active:scale-[0.98]',
36
+ FOCUS
37
+ )
38
+
39
+ /** A text link: muted, underlined at rest (colour is never the only signal), 44px tall to tap. */
40
+ const LINK = cn(
41
+ 'inline-flex min-h-11 items-center font-semibold underline decoration-1 underline-offset-4 transition-colors hover:text-foreground hover:decoration-2',
42
+ FOCUS
43
+ )
44
+
21
45
  export const CookieConsent: FC<CookieConsentProps> = props => {
22
46
  const categories = props.categories ?? DEFAULT_CONSENT_CATEGORIES
23
47
  const t = props.translate ?? defaultConsentTranslate(props.locale)
@@ -66,26 +90,24 @@ export const CookieConsent: FC<CookieConsentProps> = props => {
66
90
 
67
91
  return <>
68
92
  {consent.open && <div
69
- className="fixed inset-0 z-[999998] flex items-center justify-center overflow-y-auto bg-black/70 px-4 py-6 backdrop-blur-md"
93
+ className="fixed inset-0 z-[999998] flex items-center justify-center overflow-y-auto bg-black/70 px-4 py-6"
70
94
  aria-modal="true" role="dialog" aria-labelledby="cc-title" aria-describedby="cc-desc"
71
95
  data-consent-dialog
72
96
  >
73
97
  <div className={cn(
74
- 'relative z-[999999] w-full max-w-lg rounded-2xl border border-border bg-popover/95 p-6 shadow-[0_25px_60px_-12px_rgba(0,0,0,0.5)] ring-1 ring-primary/20 backdrop-blur-xl sm:p-8',
98
+ 'relative z-[999999] w-full max-w-lg rounded-3xl border border-border bg-background p-6 text-foreground sm:p-8',
75
99
  props.className
76
100
  )}>
77
- <div className="absolute inset-x-0 top-0 h-1 rounded-t-2xl bg-gradient-to-r from-primary to-secondary" />
78
-
79
101
  <div className="mb-4 flex items-center gap-3">
80
- <div className="flex h-10 w-10 items-center justify-center rounded-xl bg-primary/10 text-primary">
102
+ <div className="flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-full bg-muted text-primary">
81
103
  <Cookie className="h-5 w-5" aria-hidden="true" />
82
104
  </div>
83
- <h2 id="cc-title" className="text-xl font-bold text-popover-foreground sm:text-2xl">
105
+ <h2 id="cc-title" className="text-balance text-xl font-extrabold tracking-tight text-foreground sm:text-2xl">
84
106
  {t('title', 'Cookie Preferences')}
85
107
  </h2>
86
108
  </div>
87
109
 
88
- <p id="cc-desc" className="mb-6 text-sm leading-relaxed text-muted-foreground">
110
+ <p id="cc-desc" className="mb-6 text-pretty text-sm leading-relaxed text-muted-foreground">
89
111
  {t('description', 'We use cookies to enhance your browsing experience, serve personalized ads or content, and analyze our traffic.')}
90
112
  </p>
91
113
 
@@ -102,31 +124,29 @@ export const CookieConsent: FC<CookieConsentProps> = props => {
102
124
  />)}
103
125
  </div>
104
126
 
105
- {gated && <p role="status" data-consent-reason className="mt-4 text-sm text-popover-foreground">
127
+ {gated && <p role="status" data-consent-reason className="mt-4 text-sm font-semibold text-foreground">
106
128
  {t('loginReason', 'Signing in stores a session cookie. Accept essential cookies to continue.')}
107
129
  </p>}
108
130
 
109
131
  <div className="mt-8 flex flex-col gap-3 sm:flex-row sm:gap-4">
110
132
  <button
111
133
  type="button" onClick={onSave} data-consent-save
112
- className="flex-1 rounded-lg border border-border bg-transparent px-4 py-3 text-sm font-semibold text-popover-foreground transition-colors hover:bg-accent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
134
+ className={cn(PILL, 'border-[1.5px] border-foreground bg-transparent font-semibold text-foreground hover:bg-muted')}
113
135
  >{t('savePreferences', 'Save Preferences')}</button>
114
136
  <button
115
137
  type="button" onClick={onAcceptAll} data-consent-accept-all
116
- className="flex-1 rounded-lg bg-primary px-4 py-3 text-sm font-bold text-primary-foreground shadow-lg shadow-primary/25 transition-all hover:bg-primary/90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
138
+ className={cn(PILL, 'bg-primary font-bold text-primary-foreground hover:bg-primary/90')}
117
139
  >{gated
118
140
  ? t('acceptAndContinue', 'Accept & continue')
119
141
  : t('acceptAll', 'Accept All')}</button>
120
142
  </div>
121
143
 
122
- {(props.policyHref != null || props.links != null) && <div className="mt-4 flex flex-wrap justify-center gap-3 text-xs text-muted-foreground">
144
+ {(props.policyHref != null || props.links != null) && <div className="mt-3 flex flex-wrap justify-center gap-x-4 text-[13px] text-muted-foreground">
123
145
  {props.policyHref != null && <a
124
- href={props.policyHref} target="_blank" rel="noopener noreferrer"
125
- className="underline underline-offset-2 transition-colors hover:text-popover-foreground"
146
+ href={props.policyHref} target="_blank" rel="noopener noreferrer" className={LINK}
126
147
  >{t('policyLink', 'Cookie Policy')}</a>}
127
148
  {props.links?.map(link => <a
128
- key={link.href} href={link.href} target="_blank" rel="noopener noreferrer"
129
- className="underline underline-offset-2 transition-colors hover:text-popover-foreground"
149
+ key={link.href} href={link.href} target="_blank" rel="noopener noreferrer" className={LINK}
130
150
  >{t(link.labelKey, link.defaultLabel)}</a>)}
131
151
  </div>}
132
152
  </div>
@@ -137,13 +157,14 @@ export const CookieConsent: FC<CookieConsentProps> = props => {
137
157
  * stays, on every page of the site, so it must read as a small fixture rather than as a
138
158
  * floating action button competing with the page's own controls. No border, no filled
139
159
  * background, no shadow and no hover-scale — only the pictogram, dimmed at rest and picked
140
- * out on hover/focus, at the exact size (`h-5 w-5`) it always was.
160
+ * out on hover/focus, at the exact size (`h-5 w-5`) it always was. The invisible hit area
161
+ * around it is 44px, the smallest target a finger can reliably press.
141
162
  */}
142
163
  {props.noReopenButton !== true && consent.record != null && !consent.open && <button
143
164
  type="button" onClick={() => consent.openDialog('reopen')}
144
165
  aria-label={t('openPreferences', 'Cookie preferences')}
145
166
  data-consent-reopen
146
- className="fixed bottom-1 left-1 z-[999997] inline-flex items-center justify-center rounded border-0 bg-transparent p-1.5 text-muted-foreground opacity-70 transition-opacity hover:opacity-100 hover:text-primary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
167
+ className={cn('fixed bottom-1 left-1 z-[999997] inline-flex h-11 w-11 items-center justify-center rounded border-0 bg-transparent p-0 text-muted-foreground opacity-70 transition-opacity hover:opacity-100 hover:text-primary focus-visible:opacity-100', FOCUS)}
147
168
  >
148
169
  <Cookie className="h-5 w-5" aria-hidden="true" />
149
170
  </button>}
@@ -12,36 +12,49 @@ export interface ConsentToggleProps {
12
12
  onChange: (value: boolean) => void
13
13
  }
14
14
 
15
+ /**
16
+ * One category row: a hairline card with the label, the description and a switch.
17
+ *
18
+ * Flat like the dialog it sits in — no tinted fill, no shadow, no badge. `Required` is plain muted
19
+ * text beside the label, because a required category is disclosure, not a status to decorate. The
20
+ * switch's hit area is 44px square around the visible 24×44 track, and it shows the dialog's 3px
21
+ * focus ring when reached from the keyboard (the real checkbox is visually hidden, so the ring is
22
+ * drawn on the track through `peer-focus-visible`).
23
+ */
15
24
  export const ConsentToggle: FC<ConsentToggleProps> = (
16
25
  { id, label, description, checked, required, requiredLabel, onChange }
17
26
  ) => (
18
- <div className="flex items-start justify-between rounded-lg border border-border/50 bg-muted/30 p-3">
19
- <div className="flex-1 pr-4">
20
- <div className="mb-0.5 flex items-center gap-2 text-sm font-semibold text-popover-foreground">
27
+ <div className="flex items-center justify-between gap-4 rounded-2xl border border-border p-4">
28
+ <div className="min-w-0 flex-1">
29
+ <div className="mb-0.5 flex flex-wrap items-center gap-x-2 text-sm font-bold text-foreground">
21
30
  {label}
22
- {required === true && <span className="rounded bg-primary/10 px-1.5 py-0.5 text-[10px] font-medium uppercase tracking-wide text-primary">
31
+ {required === true && <span className="text-xs font-bold uppercase tracking-wide text-muted-foreground">
23
32
  {requiredLabel}
24
33
  </span>}
25
34
  </div>
26
- <p className="text-xs text-muted-foreground">{description}</p>
35
+ <p className="text-pretty text-xs leading-relaxed text-muted-foreground">{description}</p>
27
36
  </div>
28
- <label className={cn('relative h-6 w-11 flex-shrink-0', required === true ? 'cursor-not-allowed opacity-70' : 'cursor-pointer')}>
37
+ <label className={cn(
38
+ 'relative inline-flex h-11 w-14 flex-shrink-0 items-center justify-center',
39
+ required === true ? 'cursor-not-allowed opacity-50' : 'cursor-pointer'
40
+ )}>
29
41
  <input
30
42
  id={id}
31
43
  type="checkbox"
32
- className="sr-only"
44
+ className="peer sr-only"
33
45
  checked={checked || required === true}
34
46
  disabled={required === true}
35
47
  onChange={event => onChange(event.target.checked)}
36
48
  />
37
49
  <span className={cn(
38
- 'absolute inset-0 rounded-full transition-colors duration-300',
39
- checked || required === true ? 'bg-primary' : 'bg-muted-foreground/30'
40
- )} />
41
- <span className={cn(
42
- 'absolute top-0.5 h-5 w-5 rounded-full bg-white shadow transition-transform duration-300',
43
- checked || required === true ? 'translate-x-5' : 'translate-x-0.5'
44
- )} />
50
+ 'relative h-6 w-11 rounded-full transition-colors duration-300 peer-focus-visible:outline-3 peer-focus-visible:outline-offset-3 peer-focus-visible:outline-ring',
51
+ checked || required === true ? 'bg-primary' : 'bg-muted-foreground/40'
52
+ )}>
53
+ <span className={cn(
54
+ 'absolute top-0.5 left-0 h-5 w-5 rounded-full bg-white transition-transform duration-300',
55
+ checked || required === true ? 'translate-x-5' : 'translate-x-0.5'
56
+ )} />
57
+ </span>
45
58
  </label>
46
59
  </div>
47
60
  )
package/src/index.ts CHANGED
@@ -14,6 +14,6 @@ export {
14
14
  CONSENT_ESSENTIAL, CONSENT_ANALYTICS, CONSENT_MARKETING,
15
15
  } from '@owlmeans/consent'
16
16
  export type {
17
- ConsentCategory, ConsentOptions, ConsentReason, ConsentRecord, ConsentSignal, ConsentState,
18
- ConsentStore, ConsentLocale,
17
+ ConsentCategory, ConsentOptions, ConsentReason, ConsentRecord, ConsentService, ConsentSignal,
18
+ ConsentState, ConsentStore, ConsentLocale,
19
19
  } from '@owlmeans/consent'
@@ -3,9 +3,58 @@ import {
3
3
  CONSENT_COOKIE_DAYS, CONSENT_KEY, DEFAULT_CONSENT_CATEGORIES,
4
4
  defaultConsentTranslate, interpolate, openConsent,
5
5
  } from '@owlmeans/consent'
6
+ import type { ConsentService } from '@owlmeans/consent'
6
7
  import { cn } from '../lib/utils.js'
7
8
  import type { CookiePolicyProps } from '../types.js'
8
9
 
10
+ type Translate = (key: string, defaultValue: string) => string
11
+
12
+ interface ServiceListProps {
13
+ services: ConsentService[]
14
+ /** The category's own label, already resolved — it names the list for assistive technology. */
15
+ label: string
16
+ /** The category key, for tests and CSS; absent for the trailing group of unmatched services. */
17
+ category?: string
18
+ t: Translate
19
+ }
20
+
21
+ /**
22
+ * The services one category gates, as a nested list inside that category's item.
23
+ *
24
+ * A list and a description list rather than headings: the page's only heading is its `h1`, the
25
+ * categories are list items, and a service heading below them would either skip a level or
26
+ * promote a service above the category that governs it.
27
+ */
28
+ const ServiceList: FC<ServiceListProps> = ({ services, label, category, t }) =>
29
+ <ul
30
+ aria-label={interpolate(t('policyServicesOf', 'Services — {{category}}'), { category: label })}
31
+ data-cookie-policy-services={category ?? ''}
32
+ >
33
+ {services.map((service, index) => <li key={`${service.name}-${index}`} data-cookie-policy-service>
34
+ <strong>{service.name}</strong>
35
+ <dl>
36
+ <div>
37
+ <dt>{t('policyProvider', 'Provider')}</dt>
38
+ <dd>{service.provider}</dd>
39
+ </div>
40
+ {service.purpose != null && service.purpose !== '' && <div>
41
+ <dt>{t('policyPurpose', 'Purpose')}</dt>
42
+ <dd>{service.purpose}</dd>
43
+ </div>}
44
+ {service.cookies != null && service.cookies.length > 0 && <div>
45
+ <dt>{t('policyCookies', 'Cookies')}</dt>
46
+ <dd>{service.cookies.map((cookie, at) => <span key={`${cookie}-${at}`}>
47
+ {at > 0 && ', '}<code>{cookie}</code>
48
+ </span>)}</dd>
49
+ </div>}
50
+ </dl>
51
+ {service.privacyHref != null && service.privacyHref !== '' && <a
52
+ href={service.privacyHref} target="_blank" rel="noopener noreferrer"
53
+ >{interpolate(t('policyServicePrivacy', '{{provider}} privacy policy'),
54
+ { provider: service.provider })}</a>}
55
+ </li>)}
56
+ </ul>
57
+
9
58
  /**
10
59
  * A cookie policy that states only what this application can actually assert.
11
60
  *
@@ -17,24 +66,46 @@ import type { CookiePolicyProps } from '../types.js'
17
66
  *
18
67
  * Everything OwlMeans cannot assert on the operator's behalf — who the controller is, what the
19
68
  * lawful basis is, how to exercise rights — is deferred to their own privacy policy and terms.
69
+ *
70
+ * `services` names WHO receives data under each category. It is the one input the widget cannot
71
+ * derive, so it comes from whatever added the tag (`googleTagServices(id)` for a Google tag), and
72
+ * a service is never dropped for want of a matching category: one whose category is not in force
73
+ * is listed in a trailing group, because a service the page runs and does not disclose is worse
74
+ * than one disclosed in the wrong place.
20
75
  */
21
76
  export const CookiePolicy: FC<CookiePolicyProps> = props => {
22
77
  const categories = props.categories ?? DEFAULT_CONSENT_CATEGORIES
23
78
  const t = props.translate ?? defaultConsentTranslate(props.locale)
24
79
  const storageKey = props.storageKey ?? CONSENT_KEY
25
80
  const days = props.cookieDays ?? CONSENT_COOKIE_DAYS
81
+ const services = props.services ?? []
82
+ const keys = new Set(categories.map(category => category.key))
83
+ const other = services.filter(service => !keys.has(service.category))
84
+ const otherLabel = t('policyOtherServices', 'Other services')
26
85
 
27
86
  return <article className={cn('prose prose-sm max-w-2xl', props.className)} data-cookie-policy>
28
87
  <h1>{t('policyTitle', 'Cookie Policy')}</h1>
29
88
  <p>{t('policyIntro', 'This page describes the cookies and similar storage this application uses, and how you can control them.')}</p>
30
89
 
31
90
  <ul>
32
- {categories.map(category => <li key={category.key}>
33
- <strong>{t(category.labelKey, category.key)}</strong>
34
- {category.required === true && <> — {t('required', 'Required')}</>}
91
+ {categories.map(category => {
92
+ const label = t(category.labelKey, category.key)
93
+ const gated = services.filter(service => service.category === category.key)
94
+
95
+ return <li key={category.key} data-cookie-policy-category={category.key}>
96
+ <strong>{label}</strong>
97
+ {category.required === true && <> — {t('required', 'Required')}</>}
98
+ <br />
99
+ {t(category.descriptionKey, '')}
100
+ {gated.length > 0 && <ServiceList services={gated} label={label} category={category.key} t={t} />}
101
+ </li>
102
+ })}
103
+ {other.length > 0 && <li key=" other" data-cookie-policy-other>
104
+ <strong>{otherLabel}</strong>
35
105
  <br />
36
- {t(category.descriptionKey, '')}
37
- </li>)}
106
+ {t('policyOtherServicesDesc', 'Services this application runs outside the categories above.')}
107
+ <ServiceList services={other} label={otherLabel} t={t} />
108
+ </li>}
38
109
  </ul>
39
110
 
40
111
  <p>{interpolate(
@@ -64,7 +135,10 @@ export const CookiePolicy: FC<CookiePolicyProps> = props => {
64
135
  </p>
65
136
 
66
137
  <p>
67
- <button type="button" onClick={() => openConsent('reopen')} data-cookie-policy-manage>
138
+ <button
139
+ type="button" onClick={() => openConsent('reopen')} data-cookie-policy-manage
140
+ className="not-prose inline-flex min-h-11 items-center justify-center rounded-full border-[1.5px] border-foreground bg-transparent px-6 py-2.5 text-[15px] font-semibold text-foreground transition-colors hover:bg-muted focus-visible:outline-3 focus-visible:outline-offset-3 focus-visible:outline-ring"
141
+ >
68
142
  {t('manage', 'Manage preferences')}
69
143
  </button>
70
144
  </p>
package/src/types.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type {
2
- ConsentCategory, ConsentReason, ConsentRecord,
2
+ ConsentCategory, ConsentReason, ConsentRecord, ConsentService,
3
3
  } from '@owlmeans/consent'
4
4
 
5
5
  export interface ConsentLink {
@@ -39,6 +39,13 @@ export interface CookiePolicyProps {
39
39
  termsHref?: string
40
40
  storageKey?: string
41
41
  cookieDays?: number
42
+ /**
43
+ * The third-party services the site runs, each listed under the category that gates it —
44
+ * name, provider, purpose, cookie names and the provider's privacy policy. A service whose
45
+ * category is not among `categories` is listed in a trailing "Other services" group rather than
46
+ * dropped. `@owlmeans/web-gtm`'s `googleTagServices(id)` supplies the entries for a Google tag.
47
+ */
48
+ services?: ConsentService[]
42
49
  className?: string
43
50
  }
44
51
 
@@ -0,0 +1,36 @@
1
+ import { describe, expect, test } from 'bun:test'
2
+ import { readdirSync, readFileSync, statSync } from 'node:fs'
3
+ import { dirname, join, resolve } from 'node:path'
4
+ import { fileURLToPath } from 'node:url'
5
+ import { DEFAULT_CONSENT_CATEGORIES, DEFAULT_CONSENT_MESSAGES } from '@owlmeans/consent'
6
+
7
+ const src = resolve(dirname(fileURLToPath(import.meta.url)), '../src')
8
+
9
+ const sources = (dir: string): string[] => readdirSync(dir).flatMap(name => {
10
+ const path = join(dir, name)
11
+
12
+ return statSync(path).isDirectory() ? sources(path) : /\.tsx?$/.test(name) ? [path] : []
13
+ })
14
+
15
+ describe('@owlmeans/web-consent — the packaged bundle', () => {
16
+ test('every key a component reads has a bundle entry', () => {
17
+ // A key with no entry does not fail: it renders its English default in every language, and
18
+ // the page looks translated everywhere except that one line. The privacy and terms link
19
+ // labels shipped that way — so the keys are read from the components themselves, and a new
20
+ // `t('…')` without an entry fails here instead of in a reader's language.
21
+ const read = new Set<string>()
22
+ for (const file of sources(src)) {
23
+ for (const match of readFileSync(file, 'utf8').matchAll(/\bt\('([A-Za-z]+)'/g)) {
24
+ read.add(match[1])
25
+ }
26
+ }
27
+ for (const category of DEFAULT_CONSENT_CATEGORIES) {
28
+ read.add(category.labelKey)
29
+ read.add(category.descriptionKey)
30
+ }
31
+
32
+ expect(read.size).toBeGreaterThan(20)
33
+ const missing = [...read].filter(key => DEFAULT_CONSENT_MESSAGES.en[key] == null)
34
+ expect(missing).toEqual([])
35
+ })
36
+ })
@@ -212,6 +212,43 @@ describe('@owlmeans/web-consent — the dialog', () => {
212
212
  }
213
213
  }, TIMEOUT)
214
214
 
215
+ test('the dialog is flat: theme tokens, hairlines and pills — no gradient, shadow or blur', async () => {
216
+ // It is the first thing every new visitor of a generated app sees, and that app's rule is no
217
+ // gradient, glow, shadow or backdrop-filter anywhere. Asserted on the class lists for the same
218
+ // reason as the re-open button below: the harness compiles no stylesheet.
219
+ const { page, close } = await mountComponent({ url: `${base}/` })
220
+ try {
221
+ const dialog = page.locator('[data-consent-dialog]')
222
+ await dialog.waitFor()
223
+ const classes = await dialog.evaluate(root => [root, ...Array.from(root.querySelectorAll('*'))]
224
+ .map(element => element.getAttribute('class') ?? '').join(' '))
225
+
226
+ expect(classes).not.toMatch(/\b(?:bg-gradient|from-|to-secondary|backdrop-|blur|shadow|ring-primary)/)
227
+ // The overlay is a flat translucent black, and the card sits on the ground token.
228
+ expect(await dialog.getAttribute('class')).toContain('bg-black/70')
229
+ const card = dialog.locator(':scope > div')
230
+ expect(await card.getAttribute('class')).toMatch(/\bbg-background\b/)
231
+ expect(await card.getAttribute('class')).toMatch(/\bborder-border\b/)
232
+ expect(await card.getAttribute('class')).toMatch(/\brounded-3xl\b/)
233
+
234
+ // One accent pill for accepting, an outlined pill for the rest; both a 44px target with a
235
+ // visible focus ring.
236
+ const accept = await page.locator('[data-consent-accept-all]').getAttribute('class') ?? ''
237
+ const save = await page.locator('[data-consent-save]').getAttribute('class') ?? ''
238
+ expect(accept).toMatch(/\bbg-primary\b/)
239
+ expect(accept).toMatch(/\btext-primary-foreground\b/)
240
+ expect(save).not.toMatch(/\bbg-primary\b/)
241
+ expect(save).toMatch(/\bborder-foreground\b/)
242
+ for (const cls of [accept, save]) {
243
+ expect(cls).toMatch(/\brounded-full\b/)
244
+ expect(cls).toMatch(/\bmin-h-11\b/)
245
+ expect(cls).toContain('focus-visible:outline-ring')
246
+ }
247
+ } finally {
248
+ await close()
249
+ }
250
+ }, TIMEOUT)
251
+
215
252
  test('the re-open button brings the dialog back with a reason', async () => {
216
253
  const { page, close } = await seeded({ essential: true, analytics: false, marketing: false, v: CONSENT_SCHEMA_VERSION })
217
254
  try {
@@ -263,6 +300,92 @@ describe('@owlmeans/web-consent — the policy page', () => {
263
300
  }
264
301
  }, TIMEOUT)
265
302
 
303
+ test('each service is disclosed under the category that gates it', async () => {
304
+ // "Analytics cookies" says why something is stored, never who receives it — the service row is
305
+ // the part a regulator actually reads, so it must sit inside ITS category, not in a list of
306
+ // its own where nothing ties it to the switch that governs it.
307
+ const { page, close } = await mountComponent({ url: `${base}/?view=policy&services=1` })
308
+ try {
309
+ const analytics = page.locator('[data-cookie-policy-category="analytics"]')
310
+ await analytics.waitFor()
311
+ const text = await analytics.innerText()
312
+
313
+ expect(text).toContain('Example Analytics')
314
+ expect(text).toContain('Example Corp')
315
+ expect(text).toContain('Counts page views.')
316
+ expect(text).toContain('_ex_1')
317
+ expect(await analytics.locator('a[href="https://example.test/vendor-privacy"]').innerText())
318
+ .toContain('Example Corp')
319
+ // The list is named for assistive technology by the category it belongs to.
320
+ expect(await analytics.locator('ul').getAttribute('aria-label')).toContain('Analytics')
321
+
322
+ // Nothing leaked into a category that does not gate it.
323
+ expect(await page.locator('[data-cookie-policy-category="marketing"]').innerText())
324
+ .not.toContain('Example Analytics')
325
+ } finally {
326
+ await close()
327
+ }
328
+ }, TIMEOUT)
329
+
330
+ test('a service whose category is not in force is still disclosed, never dropped', async () => {
331
+ const { page, close } = await mountComponent({ url: `${base}/?view=policy&services=1` })
332
+ try {
333
+ const other = page.locator('[data-cookie-policy-other]')
334
+ await other.waitFor()
335
+
336
+ expect(await other.innerText()).toContain('Orphan Pixel')
337
+ // Still one heading: services are list items, never headings that would skip a level.
338
+ expect(await page.locator('[data-cookie-policy] h1').count()).toBe(1)
339
+ expect(await page.locator('[data-cookie-policy] h2, [data-cookie-policy] h3').count()).toBe(0)
340
+ } finally {
341
+ await close()
342
+ }
343
+ }, TIMEOUT)
344
+
345
+ test('with no services the page renders exactly as before', async () => {
346
+ const { page, close } = await mountComponent({ url: `${base}/?view=policy` })
347
+ try {
348
+ await page.locator('[data-cookie-policy]').waitFor()
349
+
350
+ expect(await page.locator('[data-cookie-policy-service]').count()).toBe(0)
351
+ expect(await page.locator('[data-cookie-policy-other]').count()).toBe(0)
352
+ } finally {
353
+ await close()
354
+ }
355
+ }, TIMEOUT)
356
+
357
+ test('the service labels follow the locale', async () => {
358
+ const { page, close } = await mountComponent({ url: `${base}/?view=policy&services=1&locale=de` })
359
+ try {
360
+ const analytics = page.locator('[data-cookie-policy-category="analytics"]')
361
+ await analytics.waitFor()
362
+ const text = await analytics.innerText()
363
+
364
+ expect(text).toContain(defaultConsentTranslate('de')('policyProvider', ''))
365
+ expect(text).toContain(defaultConsentTranslate('de')('policyPurpose', ''))
366
+ expect(defaultConsentTranslate('de')('policyProvider', '')).not.toBe('Provider')
367
+ } finally {
368
+ await close()
369
+ }
370
+ }, TIMEOUT)
371
+
372
+ test('the privacy and terms links follow the locale', async () => {
373
+ const { page, close } = await mountComponent({ url: `${base}/?view=policy&locale=de` })
374
+ try {
375
+ const policy = page.locator('[data-cookie-policy]')
376
+ await policy.waitFor()
377
+ const de = defaultConsentTranslate('de')
378
+
379
+ expect(await policy.locator('a[href="https://example.test/privacy"]').innerText())
380
+ .toBe(de('privacy', ''))
381
+ expect(await policy.locator('a[href="https://example.test/terms"]').innerText())
382
+ .toBe(de('terms', ''))
383
+ expect(de('privacy', '')).not.toBe('Privacy Policy')
384
+ } finally {
385
+ await close()
386
+ }
387
+ }, TIMEOUT)
388
+
266
389
  test('it offers a way back to the decision', async () => {
267
390
  const { page, close } = await mountComponent({ url: `${base}/?view=policy` })
268
391
  try {
@@ -2,7 +2,7 @@ import type { FC } from 'react'
2
2
  import { useState } from 'react'
3
3
  import { createRoot } from 'react-dom/client'
4
4
  import { CookieConsent, CookiePolicy, DEFAULT_CONSENT_CATEGORIES } from '../../src/index.js'
5
- import type { ConsentCategory } from '../../src/index.js'
5
+ import type { ConsentCategory, ConsentService } from '../../src/index.js'
6
6
 
7
7
  /**
8
8
  * The harness renders whatever the query string asks for, so a spec chooses its case by URL and
@@ -11,6 +11,7 @@ import type { ConsentCategory } from '../../src/index.js'
11
11
  * - `?locale=<lng>` — render the dialog in that language.
12
12
  * - `?categories=custom` — a category set that is NOT the default, with its own global var.
13
13
  * - `?view=policy` — the cookie-policy page instead of the dialog.
14
+ * - `?services=1` — with `view=policy`, disclose {@link SERVICES} on it.
14
15
  */
15
16
  const params = new URLSearchParams(window.location.search)
16
17
 
@@ -34,6 +35,19 @@ const CUSTOM: ConsentCategory[] = [
34
35
 
35
36
  const categories = params.get('categories') === 'custom' ? CUSTOM : DEFAULT_CONSENT_CATEGORIES
36
37
 
38
+ /**
39
+ * One service under a category in force, and one under a category that is not — the second is
40
+ * what proves a service is never silently dropped for want of a matching category.
41
+ */
42
+ const SERVICES: ConsentService[] = [
43
+ {
44
+ name: 'Example Analytics', provider: 'Example Corp', category: 'analytics',
45
+ purpose: 'Counts page views.', cookies: ['_ex', '_ex_1'],
46
+ privacyHref: 'https://example.test/vendor-privacy',
47
+ },
48
+ { name: 'Orphan Pixel', provider: 'Orphan Ltd', category: 'nowhere' },
49
+ ]
50
+
37
51
  const translate = params.get('categories') === 'custom'
38
52
  // Only the custom keys need wording; everything else falls through to the packaged bundle,
39
53
  // which is exactly how a consumer with one extra category is expected to wire it.
@@ -57,6 +71,7 @@ const App: FC = () => {
57
71
  operator="Acme"
58
72
  privacyHref="https://example.test/privacy"
59
73
  termsHref="https://example.test/terms"
74
+ services={params.get('services') != null ? SERVICES : undefined}
60
75
  />}
61
76
  {/*
62
77
  Mounted in EVERY view, including alongside the policy page — that is how an application