@owlmeans/web-consent 0.1.18-rc.43 → 0.1.18-rc.44
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 +225 -1
- package/agent-meta/manifest.json +2 -2
- package/agent-meta/skills/web-consent/SKILL.md +79 -21
- package/build/consent/bar.d.ts +22 -0
- package/build/consent/bar.d.ts.map +1 -0
- package/build/consent/bar.js +37 -0
- package/build/consent/bar.js.map +1 -0
- package/build/consent/component.d.ts +11 -0
- package/build/consent/component.d.ts.map +1 -1
- package/build/consent/component.js +25 -31
- package/build/consent/component.js.map +1 -1
- package/build/consent/consts.local.d.ts +14 -0
- package/build/consent/consts.local.d.ts.map +1 -1
- package/build/consent/consts.local.js +14 -0
- package/build/consent/consts.local.js.map +1 -1
- package/build/consent/focus.d.ts +13 -0
- package/build/consent/focus.d.ts.map +1 -0
- package/build/consent/focus.js +55 -0
- package/build/consent/focus.js.map +1 -0
- package/build/consent/links.d.ts +10 -0
- package/build/consent/links.d.ts.map +1 -0
- package/build/consent/links.js +13 -0
- package/build/consent/links.js.map +1 -0
- package/build/consent/locating.d.ts +10 -0
- package/build/consent/locating.d.ts.map +1 -0
- package/build/consent/locating.js +19 -0
- package/build/consent/locating.js.map +1 -0
- package/build/consent/types.local.d.ts +36 -0
- package/build/consent/types.local.d.ts.map +1 -0
- package/build/consent/types.local.js +2 -0
- package/build/consent/types.local.js.map +1 -0
- package/build/consent/window.d.ts +13 -0
- package/build/consent/window.d.ts.map +1 -0
- package/build/consent/window.js +34 -0
- package/build/consent/window.js.map +1 -0
- package/build/consts.d.ts +4 -0
- package/build/consts.d.ts.map +1 -0
- package/build/consts.js +3 -0
- package/build/consts.js.map +1 -0
- package/build/hooks.d.ts.map +1 -1
- package/build/hooks.js +6 -4
- package/build/hooks.js.map +1 -1
- package/build/index.d.ts +3 -2
- package/build/index.d.ts.map +1 -1
- package/build/index.js +2 -1
- package/build/index.js.map +1 -1
- package/build/policy/component.d.ts +3 -0
- package/build/policy/component.d.ts.map +1 -1
- package/build/policy/component.js +4 -1
- package/build/policy/component.js.map +1 -1
- package/build/types.d.ts +30 -3
- package/build/types.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/consent/bar.tsx +94 -0
- package/src/consent/component.tsx +45 -111
- package/src/consent/consts.local.ts +21 -0
- package/src/consent/focus.ts +56 -0
- package/src/consent/links.tsx +22 -0
- package/src/consent/locating.tsx +30 -0
- package/src/consent/types.local.ts +38 -0
- package/src/consent/window.tsx +105 -0
- package/src/consts.ts +4 -0
- package/src/hooks.ts +6 -4
- package/src/index.ts +6 -0
- package/src/policy/component.tsx +7 -0
- package/src/types.ts +32 -3
- package/tests/a11y.spec.ts +22 -7
- package/tests/bar.spec.ts +225 -0
- package/tests/consent.spec.ts +31 -25
- package/tests/context.ts +50 -1
- package/tests/geo.spec.ts +265 -0
- package/tests/harness/mount.tsx +40 -3
- package/tests/ssr.spec.tsx +13 -0
package/README.md
CHANGED
|
@@ -1,3 +1,227 @@
|
|
|
1
|
+
# @owlmeans/web-consent
|
|
2
|
+
|
|
3
|
+
The React components of OwlMeans cookie consent: the preferences dialog with its persistent re-open
|
|
4
|
+
button, a menu row for hosts that reopen it from their own navigation, the generated cookie-policy
|
|
5
|
+
page, and the `useConsent` hooks. It re-exports a named selection of `@owlmeans/consent` (the store,
|
|
6
|
+
storage, Consent Mode and linker helpers, constants and types), so a web app usually needs one
|
|
7
|
+
import. Any React surface uses it — a panel app, a generated app, an Astro island — because it
|
|
8
|
+
depends on no shadcn primitive, no `@` alias and no OwlMeans context. An app built on
|
|
9
|
+
`@owlmeans/web-panel` mounts `PanelCookieConsent` / `PanelCookiePolicy` from
|
|
10
|
+
`@owlmeans/web-panel/consent` instead, which binds these components to OwlMeans i18n; code that only
|
|
11
|
+
needs the model (no React) imports `@owlmeans/consent`; the Google tag snippet is `@owlmeans/web-gtm`.
|
|
12
|
+
The `web-consent` skill covers the components, the `consent` skill the model behind them.
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
bun add @owlmeans/web-consent@^0.1.18-rc.44
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Peer dependencies the app provides: `react`, `tailwindcss`, `tailwind-merge`, `clsx`, `lucide-react`.
|
|
21
|
+
|
|
22
|
+
The components emit Tailwind classes that appear nowhere in the app's own sources, so the app's
|
|
23
|
+
Tailwind entry must scan the package's `src`:
|
|
24
|
+
|
|
25
|
+
```css
|
|
26
|
+
@import "tailwindcss";
|
|
27
|
+
|
|
28
|
+
@source "<relative path to node_modules>/@owlmeans/web-consent/src";
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Concepts
|
|
32
|
+
|
|
33
|
+
- **Category** — a `ConsentCategory` (`essential` required, `analytics`, `marketing` by default). A
|
|
34
|
+
required category is disclosure, not a question: it renders locked and labelled *Required*.
|
|
35
|
+
- **Record** — the visitor's decision, `ConsentRecord`, stored under `site_cookie_consent` in both
|
|
36
|
+
localStorage and a cookie.
|
|
37
|
+
- **Store** — `consentStore`, a module singleton: consent belongs to the document, not to a component
|
|
38
|
+
tree. Both hooks subscribe to it through `useSyncExternalStore`.
|
|
39
|
+
- **Reason** — why the dialog is open: `'initial'` (no record), `'reopen'`, or `'login'` (raised by
|
|
40
|
+
the sign-in precondition, which relabels the primary action *Accept & continue*).
|
|
41
|
+
- **Parity** — the category set, `storageKey`, `cookieDays` and `cookieDomain` given to the dialog
|
|
42
|
+
must match what stamps the head snippet and what renders the policy page.
|
|
43
|
+
- **Linker** — optional cross-domain consent (`linker: { domains }`): a decision made on one listed
|
|
44
|
+
first-party domain is carried to another by a decorated link.
|
|
45
|
+
|
|
46
|
+
## Usage
|
|
47
|
+
|
|
48
|
+
### 1. Mount it once
|
|
49
|
+
|
|
50
|
+
At the application root or layout. It opens itself when no decision is stored and renders only the
|
|
51
|
+
re-open button once one is. A first-time visitor is asked with a tall bar across the bottom of a
|
|
52
|
+
transparent overlay — "Cookie preferences", "Accept only mandatory", "Accept all" — and every later
|
|
53
|
+
opening is the preferences window; `mode="window"` asks with the window straight away.
|
|
54
|
+
|
|
55
|
+
```tsx
|
|
56
|
+
import { CookieConsent } from '@owlmeans/web-consent'
|
|
57
|
+
|
|
58
|
+
export const Layout = ({ children, locale }: { children: ReactNode, locale: string }) => <>
|
|
59
|
+
{children}
|
|
60
|
+
<CookieConsent
|
|
61
|
+
locale={locale}
|
|
62
|
+
policyHref="/legal/cookies"
|
|
63
|
+
links={[{ href: '/legal/privacy', labelKey: 'privacy', defaultLabel: 'Privacy Policy' }]}
|
|
64
|
+
/>
|
|
65
|
+
</>
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Pass `geo={{ cloudflare: true }}` on a Cloudflare-proxied host to ask only visitors located in a
|
|
69
|
+
consent country (a spinner shows while they are located; anyone else is decided for automatically —
|
|
70
|
+
see `@owlmeans/consent`). Pass `linker={{ domains: ['owlmeans.com', 'owlmeans.pl'] }}` to share the
|
|
71
|
+
decision across domains; the surface then discloses the domain list. `noReopenButton` hides the floating button for an app
|
|
72
|
+
that offers a footer link calling `consentStore.open('reopen')`.
|
|
73
|
+
|
|
74
|
+
### 2. Read the decision
|
|
75
|
+
|
|
76
|
+
```tsx
|
|
77
|
+
import { consentStore, useConsentCategory, CONSENT_ANALYTICS } from '@owlmeans/web-consent'
|
|
78
|
+
|
|
79
|
+
export const AnalyticsChart = () => {
|
|
80
|
+
const allowed = useConsentCategory(CONSENT_ANALYTICS) // subscribes, does not init
|
|
81
|
+
|
|
82
|
+
return allowed ? <Chart /> : <p>Enable analytics cookies to see this chart.</p>
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// outside React — a click handler, a service
|
|
86
|
+
if (consentStore.granted(CONSENT_ANALYTICS)) {
|
|
87
|
+
startTracking()
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
`useConsent(opts?)` returns the whole model (`record`, `open`, `reason`, `granted`, `save`,
|
|
92
|
+
`acceptAll`, `openDialog`, `close`) but also runs `consentStore.init(opts)` on mount — use it only
|
|
93
|
+
where the dialog's own options are in hand.
|
|
94
|
+
|
|
95
|
+
### 3. Reopen from a host menu
|
|
96
|
+
|
|
97
|
+
```tsx
|
|
98
|
+
import { ConsentMenuWidget } from '@owlmeans/web-consent'
|
|
99
|
+
|
|
100
|
+
<nav aria-label="Settings">
|
|
101
|
+
<ConsentMenuWidget locale={locale} className="px-3 py-2" />
|
|
102
|
+
</nav>
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
The row calls `consentStore.open('reopen')` (or your `onSelect`). It does not hide the floating
|
|
106
|
+
button by itself; pass `noReopenButton` to `CookieConsent` when the menu fully replaces it.
|
|
107
|
+
|
|
108
|
+
### 4. Render the cookie policy
|
|
109
|
+
|
|
110
|
+
```tsx
|
|
111
|
+
import { CookiePolicy } from '@owlmeans/web-consent'
|
|
112
|
+
import { googleTagHelper } from '@owlmeans/web-gtm'
|
|
113
|
+
|
|
114
|
+
<CookiePolicy
|
|
115
|
+
locale={locale}
|
|
116
|
+
operator="Example Sp. z o.o."
|
|
117
|
+
privacyHref="/legal/privacy"
|
|
118
|
+
termsHref="/legal/terms"
|
|
119
|
+
services={googleTag !== '' ? googleTagHelper.googleTagServices(googleTag) : undefined}
|
|
120
|
+
/>
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
The page lists the categories in force, the services each one gates (unmatched services go to a
|
|
124
|
+
trailing *Other services* item), the storage key, the retention and, with `linker`, the domains. It
|
|
125
|
+
ends with a *Manage preferences* button that reopens the dialog.
|
|
126
|
+
|
|
127
|
+
### 5. Build your own dialog body
|
|
128
|
+
|
|
129
|
+
`ConsentToggle` is one accessible category row; its wording is the caller's, already resolved.
|
|
130
|
+
|
|
131
|
+
```tsx
|
|
132
|
+
import { ConsentToggle } from '@owlmeans/web-consent'
|
|
133
|
+
|
|
134
|
+
<ConsentToggle
|
|
135
|
+
id="cc-analytics" label="Analytics" description="Helps us understand usage."
|
|
136
|
+
checked={draft.analytics === true} requiredLabel="Required"
|
|
137
|
+
onChange={value => setDraft({ ...draft, analytics: value })}
|
|
138
|
+
/>
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## API
|
|
142
|
+
|
|
143
|
+
### Components and hooks
|
|
144
|
+
|
|
145
|
+
| Symbol | Kind | Purpose |
|
|
146
|
+
|---|---|---|
|
|
147
|
+
| `CookieConsent` | component (`CookieConsentProps`) | The consent UI — the bar, the preferences window, the locating spinner — and the floating re-open button (`[data-consent-reopen]`) |
|
|
148
|
+
| `CONSENT_DEFAULT_MODE`, `ConsentDisplayMode` | const, type | `'bar'`; `'bar' \| 'window'` |
|
|
149
|
+
| `ConsentMenuWidget` | component (`ConsentMenuWidgetProps`) | One menu row reopening the dialog; never initialises the store |
|
|
150
|
+
| `CookiePolicy` | component (`CookiePolicyProps`) | The generated cookie-policy page |
|
|
151
|
+
| `ConsentToggle` | component (`ConsentToggleProps`) | One category row with a switch; locked when `required` |
|
|
152
|
+
| `useConsent(opts?)` | hook → `UseConsentModel` | Full consent state and actions; **initialises the store on mount** |
|
|
153
|
+
| `useConsentCategory(key)` | hook → `boolean` | Whether one category is granted (false while no record exists); no init |
|
|
154
|
+
|
|
155
|
+
### Props
|
|
156
|
+
|
|
157
|
+
| Type | Fields |
|
|
158
|
+
|---|---|
|
|
159
|
+
| `CookieConsentProps` | `locale?`, `categories?`, `translate?`, `mode?`, `geo?`, `policyHref?`, `links?: ConsentLink[]`, `storageKey?`, `cookieDays?`, `cookieDomain?`, `silent?`, `noReopenButton?`, `className?` (the window's card), `barClassName?`, `linker?` |
|
|
160
|
+
| `CookiePolicyProps` | `locale?`, `translate?`, `categories?`, `operator?`, `privacyHref?`, `termsHref?`, `storageKey?`, `cookieDays?`, `services?: ConsentService[]`, `className?`, `linker?`, `geo?` (adds the regional rule) |
|
|
161
|
+
| `ConsentMenuWidgetProps` | `locale?`, `translate?`, `label?`, `className?`, `onSelect?` |
|
|
162
|
+
| `ConsentToggleProps` | `id`, `label`, `description`, `checked`, `required?`, `requiredLabel`, `onChange(value)` |
|
|
163
|
+
| `ConsentLink` | `{ href, labelKey, defaultLabel }` |
|
|
164
|
+
| `UseConsentModel` | `{ record, open, reason, locating, granted(key), save(record), acceptAll(), openDialog(reason?), close() }` |
|
|
165
|
+
|
|
166
|
+
`translate` is `(key, defaultValue) => string`; without it the packaged bundle for `locale` is used
|
|
167
|
+
(`CONSENT_LOCALES`: en, pl, ru, be, uk, es, de, fr; anything else falls back to English).
|
|
168
|
+
|
|
169
|
+
### Re-exported from `@owlmeans/consent`
|
|
170
|
+
|
|
171
|
+
| Group | Symbols |
|
|
172
|
+
|---|---|
|
|
173
|
+
| Store | `consentStore` |
|
|
174
|
+
| Helpers | `consentStorageHelper`, `consentModeHelper`, `consentI18nHelper`, `consentPluginHelper`, `consentLinkHelper`, `consentGeoHelper` |
|
|
175
|
+
| Geo gate | `CONSENT_REQUIRED_COUNTRIES`, `CONSENT_COUNTRIES_GDPR`, `CONSENT_COUNTRIES_ALIGNED`, `CONSENT_COUNTRIES_OPT_IN`, `CONSENT_GEO_UNKNOWN`, `CONSENT_TRACE_PATH`, `CONSENT_GEO_TIMEOUT`, `CONSENT_AUTO_MAX_AGE`, `CONSENT_IDLE_STATE`, `CONSENT_STATE_ATTRIBUTE` and the types `ConsentGeoOptions`, `ConsentGeoPlugin`, `ConsentGeoLocation`, `ConsentCloudflareOptions`, `ConsentLocating`, `ConsentGeoHelper`, `ConsentGeoVerdict`, `ConsentAutoState` |
|
|
176
|
+
| Defaults | `DEFAULT_CONSENT_CATEGORIES`, `DEFAULT_CONSENT_MESSAGES` |
|
|
177
|
+
| Constants | `CONSENT_KEY`, `CONSENT_COOKIE_DAYS`, `CONSENT_SCHEMA_VERSION`, `CONSENT_LOCALES`, `CONSENT_ESSENTIAL`, `CONSENT_ANALYTICS`, `CONSENT_MARKETING`, `CONSENT_LANGUAGE_KEY`, `CONSENT_EVENT`, `CONSENT_LINK_PARAM`, `CONSENT_LINK_MAX_AGE`, `CONSENT_LINK_SKEW` |
|
|
178
|
+
| Deprecated wrappers | `openConsent`, `isConsented`, `readConsent`, `writeConsent`, `clearConsent`, `consentBootstrapScript`, `defaultConsentTranslate`, `decorateConsentUrl`, `encodeConsentLink`, `consentLinkerScript` — use the helper members instead |
|
|
179
|
+
| Types | `ConsentCategory`, `ConsentOptions`, `ConsentReason`, `ConsentRecord`, `ConsentService`, `ConsentSignal`, `ConsentState`, `ConsentStore`, `ConsentLocale`, `ConsentLinkerOptions`, `ConsentLinkerLanguage`, `ConsentPlugin`, `ConsentLinkPayload`, and the five helper interfaces |
|
|
180
|
+
|
|
181
|
+
`makeConsentStore`, `CONSENT_SETUP_FLAG`, `CONSENT_SIGNAL_DEFAULTS` and `ConsentListener` are not
|
|
182
|
+
re-exported; import them from `@owlmeans/consent`.
|
|
183
|
+
|
|
184
|
+
## Test and styling hooks
|
|
185
|
+
|
|
186
|
+
| Selector | Element |
|
|
187
|
+
|---|---|
|
|
188
|
+
| `[data-consent-dialog]` | whichever surface asks — the bar or the window (`data-consent-mode="bar\|window"`) |
|
|
189
|
+
| `[data-consent-bar]`, `[data-consent-overlay]` | the bar, and the transparent overlay it sits on |
|
|
190
|
+
| `[data-consent-preferences]`, `[data-consent-mandatory]`, `[data-consent-accept-all]` | the bar's three answers |
|
|
191
|
+
| `[data-consent-save]`, `[data-consent-accept-all]` | the window's two actions |
|
|
192
|
+
| `[data-consent-locating="first"]`, `[data-consent-spinner]` | the locating overlay and its spinner |
|
|
193
|
+
| `[data-consent-links]`, `[data-consent-auto]` | the legal links row; the "applied automatically" line |
|
|
194
|
+
| `html[data-consent]` | the phase: `locating`, `open`, `decided`, `idle` |
|
|
195
|
+
| `[data-consent-reopen]` | the floating re-open button |
|
|
196
|
+
| `[data-consent-reason]`, `[data-consent-domains]` | login-reason line, disclosed domains |
|
|
197
|
+
| `[data-consent-menu-widget]` | the menu row |
|
|
198
|
+
| `[data-cookie-policy]`, `[data-cookie-policy-category="<key>"]`, `[data-cookie-policy-services]`, `[data-cookie-policy-service]`, `[data-cookie-policy-other]`, `[data-cookie-policy-domains]`, `[data-cookie-policy-regional]`, `[data-cookie-policy-manage]` | the policy page |
|
|
199
|
+
|
|
200
|
+
## Common pitfalls
|
|
201
|
+
|
|
202
|
+
- Pointing `@source` at `build` instead of `src`: under a linked workspace it scans zero files and
|
|
203
|
+
the dialog renders half-styled with no error. Verify with `grep -c 'max-w-lg' dist/assets/*.css`.
|
|
204
|
+
- A bare `useConsent()` in a component that only reads opens the dialog for a first-time visitor and
|
|
205
|
+
may fix the Consent Mode defaults from the default categories. Read with `useConsentCategory` or
|
|
206
|
+
`consentStore.granted`.
|
|
207
|
+
- `useConsent` reads `opts` only on its mounting pass; changing them later has no effect.
|
|
208
|
+
- Passing `translate` disables the packaged bundle entirely — a resolver must fall back to
|
|
209
|
+
`consentI18nHelper.defaultConsentTranslate(locale)` or every unmapped key renders in English.
|
|
210
|
+
- Different `categories`, `storageKey`, `cookieDays` or `cookieDomain` between the dialog, the head
|
|
211
|
+
snippet and the policy page make them disagree about what was asked.
|
|
212
|
+
- Do not add decorative classes (gradients, shadows, blur) to the dialog; the package tests reject
|
|
213
|
+
them, and the look comes from the host's theme tokens.
|
|
214
|
+
- `silent` suppresses runtime `dataLayer` and global writes only; the stamped bootstrap snippet still
|
|
215
|
+
pushes Consent Mode commands.
|
|
216
|
+
|
|
217
|
+
## Related packages
|
|
218
|
+
|
|
219
|
+
- [`@owlmeans/consent`](../consent) — the model: categories, storage, store, Consent Mode, linker
|
|
220
|
+
- [`@owlmeans/web-gtm`](../web-gtm) — the Google tag head snippet and `googleTagServices` for the policy page
|
|
221
|
+
- [`@owlmeans/web-panel`](../web-panel) — `./consent` subpath binding these components to OwlMeans i18n
|
|
222
|
+
- [`@owlmeans/web-log`](../web-log) — consent-gated analytics plugins for `@owlmeans/log`
|
|
223
|
+
- [`@owlmeans/astro`](../astro) — stamping the consent scripts from a static site's layout
|
|
224
|
+
|
|
1
225
|
<!-- owlmeans:agent-guidance:start -->
|
|
2
226
|
## Agent guidance
|
|
3
227
|
|
|
@@ -6,7 +230,7 @@ This package ships embedded agent skills under `agent-meta/`. After installing y
|
|
|
6
230
|
your project's skill store (`.agents/skills/`):
|
|
7
231
|
|
|
8
232
|
```sh
|
|
9
|
-
npx @owlmeans/agent-skills@^0.1.18-rc.
|
|
233
|
+
npx @owlmeans/agent-skills@^0.1.18-rc.52
|
|
10
234
|
```
|
|
11
235
|
|
|
12
236
|
The embedded files are version-matched to this package release. Do not edit them
|
package/agent-meta/manifest.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 2,
|
|
3
3
|
"package": "@owlmeans/web-consent",
|
|
4
|
-
"version": "0.1.18-rc.
|
|
5
|
-
"generatedAt": "2026-10-
|
|
4
|
+
"version": "0.1.18-rc.44",
|
|
5
|
+
"generatedAt": "2026-10-08T21:27:54.304Z",
|
|
6
6
|
"canonicalRepo": "https://github.com/owlmeans/common",
|
|
7
7
|
"entries": [
|
|
8
8
|
{
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: web-consent
|
|
3
|
-
description: How to use @owlmeans/web-consent — the React cookie-consent
|
|
3
|
+
description: How to use @owlmeans/web-consent — the React cookie-consent UI (the bottom bar by default, the preferences window, the locating spinner), its re-open button, the geo gate's props, the generated cookie-policy page and the useConsent hooks, plus the Tailwind @source line every consumer must add. Auto-invoked when mounting a consent bar or dialog, rendering a cookie policy, gating a feature on a consent category, or importing CookieConsent, CookiePolicy or useConsent.
|
|
4
4
|
user-invocable: false
|
|
5
5
|
---
|
|
6
6
|
<!-- AUTO-GENERATED — do not edit. Regenerate via sync-agent-meta. -->
|
|
@@ -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.
|
|
11
|
+
**Install:** `"@owlmeans/web-consent": "^0.1.18-rc.44"` 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**
|
|
@@ -34,20 +34,34 @@ components — see below.
|
|
|
34
34
|
|
|
35
35
|
| Export | Description |
|
|
36
36
|
|--------|-------------|
|
|
37
|
-
| `CookieConsent` | The preferences
|
|
37
|
+
| `CookieConsent` | The consent UI — the bar, the preferences window, the locating spinner — **and** the persistent re-open button |
|
|
38
|
+
| `ConsentDisplayMode` / `CONSENT_DEFAULT_MODE` | `'bar'` (the default) or `'window'` — how a first-time visitor is asked |
|
|
38
39
|
| `ConsentMenuWidget` | A plain row a HOST'S OWN menu renders to reopen the dialog, in place of (or beside) `CookieConsent`'s own floating button — see below |
|
|
39
40
|
| `CookiePolicy` | The cookie-policy page, generated from the configuration in force |
|
|
40
41
|
| `ConsentToggle` | One category row — locked and labelled when the category is required |
|
|
41
42
|
| `useConsent(opts?)` | This document's consent state and the actions over it (`UseConsentModel`) — **it also initialises the store on mount**, see below |
|
|
42
43
|
| `useConsentCategory(key)` | Whether one category is granted, for a component gating a single thing |
|
|
43
44
|
| `CookieConsentProps` / `CookiePolicyProps` / `ConsentLink` | The component props |
|
|
44
|
-
| Re-exports from `@owlmeans/consent` | The complete list: `consentStore`; `consentStorageHelper` (`readConsent`, `writeConsent`, `clearConsent`, `migrateConsent`); `consentModeHelper` (`applyConsent`, `pushConsentDefaults`, `consentBootstrapScript`, `consentDefaults`, `consentUpdate`, `gtagConsent`, `trackingGranted`, `consentGateScript`); `consentI18nHelper` (`defaultConsentTranslate`, `interpolate`, `normalizeLocale`); `DEFAULT_CONSENT_CATEGORIES`, `DEFAULT_CONSENT_MESSAGES`, `CONSENT_KEY`, `CONSENT_COOKIE_DAYS`, `CONSENT_SCHEMA_VERSION`, `CONSENT_LOCALES`, `CONSENT_ESSENTIAL` / `CONSENT_ANALYTICS` / `CONSENT_MARKETING`, the plugin seam (`consentPluginHelper`: `registerConsentPlugin`, `consentPlugins`, `decorateConsentUrl`, `consentDomains`, `adoptConsent`, `startConsentPlugins`, `adoptConsentLanguage`), the linker (`consentLinkHelper`: `consentLinker`, `encodeConsentLink`, `decodeConsentLink`, `stripConsentLinkParam`, `consentLinkerScript`, `writeConsentLanguage`; `CONSENT_LANGUAGE_KEY`, `CONSENT_EVENT`, `CONSENT_LINK_PARAM`, `CONSENT_LINK_MAX_AGE`, `CONSENT_LINK_SKEW`), and the types `ConsentCategory`, `ConsentOptions`, `ConsentReason`, `ConsentRecord`, `ConsentService`, `ConsentSignal`, `ConsentState`, `ConsentStore`, `ConsentLocale`, `ConsentLinkerOptions`, `ConsentLinkerLanguage`, `ConsentPlugin`, `ConsentLinkPayload` and the five helper interfaces |
|
|
45
|
+
| Re-exports from `@owlmeans/consent` | The complete list: `consentStore`; `consentStorageHelper` (`readConsent`, `writeConsent`, `clearConsent`, `migrateConsent`); `consentModeHelper` (`applyConsent`, `pushConsentDefaults`, `consentBootstrapScript`, `consentDefaults`, `consentUpdate`, `gtagConsent`, `trackingGranted`, `consentGateScript`); `consentI18nHelper` (`defaultConsentTranslate`, `interpolate`, `normalizeLocale`); `DEFAULT_CONSENT_CATEGORIES`, `DEFAULT_CONSENT_MESSAGES`, `CONSENT_KEY`, `CONSENT_COOKIE_DAYS`, `CONSENT_SCHEMA_VERSION`, `CONSENT_LOCALES`, `CONSENT_ESSENTIAL` / `CONSENT_ANALYTICS` / `CONSENT_MARKETING`, the plugin seam (`consentPluginHelper`: `registerConsentPlugin`, `unregisterConsentPlugin`, `consentPlugins`, `decorateConsentUrl`, `consentDomains`, `adoptConsent`, `startConsentPlugins`, `adoptConsentLanguage`, `locateConsent`), the geo gate (`consentGeoHelper`; `CONSENT_REQUIRED_COUNTRIES`, `CONSENT_COUNTRIES_GDPR` / `_ALIGNED` / `_OPT_IN`, `CONSENT_GEO_UNKNOWN`, `CONSENT_TRACE_PATH`, `CONSENT_GEO_TIMEOUT`, `CONSENT_AUTO_MAX_AGE`, `CONSENT_IDLE_STATE`, `CONSENT_STATE_ATTRIBUTE`; the types `ConsentGeoOptions`, `ConsentGeoPlugin`, `ConsentGeoLocation`, `ConsentCloudflareOptions`, `ConsentLocating`, `ConsentGeoHelper`, `ConsentGeoVerdict`, `ConsentAutoState`), the linker (`consentLinkHelper`: `consentLinker`, `encodeConsentLink`, `decodeConsentLink`, `stripConsentLinkParam`, `consentLinkerScript`, `writeConsentLanguage`; `CONSENT_LANGUAGE_KEY`, `CONSENT_EVENT`, `CONSENT_LINK_PARAM`, `CONSENT_LINK_MAX_AGE`, `CONSENT_LINK_SKEW`), and the types `ConsentCategory`, `ConsentOptions`, `ConsentReason`, `ConsentRecord`, `ConsentService`, `ConsentSignal`, `ConsentState`, `ConsentStore`, `ConsentLocale`, `ConsentLinkerOptions`, `ConsentLinkerLanguage`, `ConsentPlugin`, `ConsentLinkPayload` and the five helper interfaces |
|
|
45
46
|
|
|
46
47
|
## Mounting the dialog
|
|
47
48
|
|
|
48
49
|
`CookieConsent` is mounted **once**, at the application root or in the layout. It opens itself when
|
|
49
50
|
no decision is stored, renders nothing but the re-open button once one is, and needs no state from
|
|
50
|
-
the caller:
|
|
51
|
+
the caller. One surface at a time:
|
|
52
|
+
|
|
53
|
+
- **while a first-time visitor is located** (`geo`): a transparent overlay that blocks the page,
|
|
54
|
+
and a spinner (`[data-consent-locating="first"]`, `[data-consent-spinner]` after
|
|
55
|
+
`CONSENT_LOCATING_DELAY`, 300 ms) — never the bar;
|
|
56
|
+
- **the first ask** (`reason: 'initial'`) in `bar` mode, the default: the bar;
|
|
57
|
+
- **every other opening** — the bar's own "Cookie preferences" (`'preferences'`), a footer link or
|
|
58
|
+
the corner button (`'reopen'`), the sign-in gate (`'login'`) — and the first ask in `window`
|
|
59
|
+
mode: the preferences window.
|
|
60
|
+
|
|
61
|
+
`[data-consent-dialog]` (with `role="dialog"`, `aria-modal`) and `[data-consent-accept-all]` sit on
|
|
62
|
+
whichever surface is up, so a test or a host keys on them without knowing the mode;
|
|
63
|
+
`data-consent-mode="bar|window"` and `[data-consent-bar]` tell them apart. "Cookie preferences"
|
|
64
|
+
REPLACES the bar with the window — exactly one of each exists at a time.
|
|
51
65
|
|
|
52
66
|
```tsx
|
|
53
67
|
import { CookieConsent } from '@owlmeans/web-consent'
|
|
@@ -68,6 +82,11 @@ import { CookieConsent } from '@owlmeans/web-consent'
|
|
|
68
82
|
work — the component must not know how its host does routing.
|
|
69
83
|
- `noReopenButton` hides the floating button for an app that offers a footer link instead; that link
|
|
70
84
|
calls `consentStore.open('reopen')`.
|
|
85
|
+
- **`mode`** (`'bar'` | `'window'`, default `CONSENT_DEFAULT_MODE`) picks the first ask's surface.
|
|
86
|
+
- **`geo`** (`ConsentGeoOptions`, plain data — an Astro island can pass it) turns on the geo gate:
|
|
87
|
+
`geo={{ cloudflare: true }}` on a Cloudflare-proxied host. It is passed through to
|
|
88
|
+
`consentStore.init` like `linker`; the `consent` skill's "Where consent is asked" is the model. A
|
|
89
|
+
locator plugin is code and is registered, not passed. Pass the same `geo` to `CookiePolicy`.
|
|
71
90
|
- **The re-open button is a bare icon in the very corner (`bottom-1 left-1`), not a card.** No
|
|
72
91
|
filled background, no border, no shadow, no hover-scale — `bg-transparent`, dimmed
|
|
73
92
|
(`opacity-70`) at rest and picked out on hover/focus — because it sits on every page of a site
|
|
@@ -87,15 +106,45 @@ import { CookieConsent } from '@owlmeans/web-consent'
|
|
|
87
106
|
analytics and marketing toggles only; the language is strictly necessary storage that no toggle
|
|
88
107
|
governs (the `consent` skill). The built-in bundle (8 languages) describes it in the required row.
|
|
89
108
|
|
|
109
|
+
## The bar
|
|
110
|
+
|
|
111
|
+
A tall bar across the bottom of a **transparent** overlay (`[data-consent-overlay]`,
|
|
112
|
+
`fixed inset-0 bg-transparent`): the page stays in sight and scrolls, but nothing on it can be
|
|
113
|
+
pressed until the visitor answers. The bar (`fixed inset-x-0 bottom-0 max-h-[85vh] border-t
|
|
114
|
+
bg-background`) holds, left to right on a wide screen and stacked on a phone:
|
|
115
|
+
|
|
116
|
+
- the cookie icon and `barTitle`;
|
|
117
|
+
- `barDescription` — what the essential cookies do, the optional categories in force as a list
|
|
118
|
+
after a colon (`{{categories}}`, so no language has to decline a label), that they stay off until
|
|
119
|
+
allowed, and that the choice can be changed or withdrawn at any time; `barDescriptionEssential`
|
|
120
|
+
when nothing is optional;
|
|
121
|
+
- the cross-domain line (`[data-consent-domains]`) when `linker` names more than this host;
|
|
122
|
+
- the links row (`[data-consent-links]`: `policyHref` then `links`, 44px targets, new tab);
|
|
123
|
+
- three answers: `[data-consent-preferences]` "Cookie preferences" (outlined), then
|
|
124
|
+
`[data-consent-mandatory]` "Accept only mandatory" and `[data-consent-accept-all]` "Accept All" —
|
|
125
|
+
the two answers carry the SAME accent class: refusing is exactly as prominent as accepting, and
|
|
126
|
+
one click away. With no optional category there is nothing to refuse, so "mandatory" is absent.
|
|
127
|
+
|
|
128
|
+
`barClassName` styles the bar; `className` still styles the window's card.
|
|
129
|
+
|
|
130
|
+
**Focus.** On open, focus moves to the surface itself (`tabIndex={-1}`) — never to an answer, which
|
|
131
|
+
would be a nudge; Tab and Shift+Tab cycle inside it; on close focus returns where it was. Nothing
|
|
132
|
+
pulls focus back on `focusin` (a script focusing a field is not a keyboard user escaping). The
|
|
133
|
+
spinner overlay takes no focus.
|
|
134
|
+
|
|
90
135
|
## The look — flat, from the host's tokens
|
|
91
136
|
|
|
92
|
-
The
|
|
93
|
-
rule those apps are held to: **no gradient, shadow, glow, glass or
|
|
94
|
-
|
|
137
|
+
The bar and the window are the first thing every new visitor of a generated app sees, so they
|
|
138
|
+
follow the flat rule those apps are held to: **no gradient, shadow, glow, glass or
|
|
139
|
+
`backdrop-filter`** anywhere. Everything is a theme token, so light and dark follow the host:
|
|
95
140
|
|
|
96
141
|
| Part | Classes |
|
|
97
142
|
|---|---|
|
|
98
|
-
|
|
|
143
|
+
| Bar overlay, locating overlay | `bg-transparent` — the page stays in sight |
|
|
144
|
+
| Bar | `border-t border-border bg-background text-foreground`, no shadow |
|
|
145
|
+
| Bar answers | "Accept only mandatory" and "Accept All" both the accent pill; "Cookie preferences" outlined |
|
|
146
|
+
| Spinner | `text-muted-foreground motion-safe:animate-spin` |
|
|
147
|
+
| Window overlay | `bg-black/70`, flat — no blur |
|
|
99
148
|
| Card | `rounded-3xl border border-border bg-background text-foreground`, no shadow or ring |
|
|
100
149
|
| Icon | `rounded-full bg-muted text-primary` |
|
|
101
150
|
| Category row | `rounded-2xl border border-border`; *Required* is plain uppercase muted text, never a badge |
|
|
@@ -115,19 +164,24 @@ carries `aria-labelledby="<id>-label"` (the category label) and `aria-describedb
|
|
|
115
164
|
`<id>-required` and `<id>-desc`. Drop those and axe reports three unnamed checkboxes (`label`,
|
|
116
165
|
critical). A required category stays `disabled` and keeps its name.
|
|
117
166
|
|
|
118
|
-
`tests/a11y.spec.ts` runs axe-core (a dev dependency) against the open
|
|
119
|
-
re-open button, and the policy page with services
|
|
120
|
-
violation. It loads the harness with `?styled=1` (plus `&theme=dark`), which compiles the package
|
|
167
|
+
`tests/a11y.spec.ts` runs axe-core (a dev dependency) against the open bar and the open window in
|
|
168
|
+
both schemes, the locating overlay, the re-open button, and the policy page with services and the
|
|
169
|
+
regional rule, and fails on any serious or critical violation. It loads the harness with `?styled=1` (plus `&theme=dark`), which compiles the package
|
|
121
170
|
sources with a generated app's neutral tokens — contrast cannot be judged on the unstyled harness
|
|
122
|
-
the other specs need.
|
|
171
|
+
the other specs need. The harness also takes `?mode=bar|window`, `?categories=essential`,
|
|
172
|
+
`?geo=<CC>|fail|html|hang` (its server answers `/cdn-cgi/trace` from the page's own query, so every
|
|
173
|
+
case is still chosen by URL; `&geoDelay=<ms>`, `&geoTimeout=<ms>`), `?geoPlugin=<CC>|fail` and
|
|
174
|
+
`?gpc=1` — `tests/bar.spec.ts` and `tests/geo.spec.ts` drive them.
|
|
123
175
|
- `silent` skips every `dataLayer` and global write. It is for tests and for an app that runs no
|
|
124
176
|
tags at all.
|
|
125
|
-
- The
|
|
126
|
-
visitor reopening preferences must see the answer they gave.
|
|
177
|
+
- The window's switches are **seeded from storage every time it opens** — it is mounted only while
|
|
178
|
+
open — never from the last render: a visitor reopening preferences must see the answer they gave.
|
|
179
|
+
An automatic decision past its age seeds nothing.
|
|
127
180
|
|
|
128
|
-
**When the
|
|
181
|
+
**When the window was raised by something waiting on it** — `reason === 'login'`, which the sign-in
|
|
129
182
|
precondition in `@owlmeans/client-iam` raises — it says so and relabels the primary action *Accept
|
|
130
|
-
& continue*.
|
|
183
|
+
& continue*. The sign-in gate always gets the window, never the bar. An automatic decision is said
|
|
184
|
+
to be one when the window opens over it (`[data-consent-auto]`, `autoReason`). That is what makes the interruption legible instead of looking like the page asking
|
|
131
185
|
twice. Word that path as an acknowledgement, never as "you must consent to essential cookies": a
|
|
132
186
|
required category is disclosure, not a question.
|
|
133
187
|
|
|
@@ -162,14 +216,15 @@ if (consentStore.granted('analytics')) { /* outside React — a click handler, a
|
|
|
162
216
|
Both hooks subscribe through `useSyncExternalStore` over the module-singleton store, because consent
|
|
163
217
|
is a property of the DOCUMENT rather than of a component tree: the dialog, the re-open button, the
|
|
164
218
|
policy page and whatever an app gates on it all read one value, and local copies would disagree the
|
|
165
|
-
moment one of them saved. During server rendering the snapshot is
|
|
166
|
-
|
|
219
|
+
moment one of them saved. During server rendering the snapshot is `CONSENT_IDLE_STATE` — one frozen
|
|
220
|
+
object, "no record, closed, not locating" — so no surface flashes into static HTML before hydration
|
|
221
|
+
corrects it (`tests/ssr.spec.tsx`). `UseConsentModel.locating` exposes the lookup.
|
|
167
222
|
|
|
168
223
|
**`useConsent` is not a pure reader — it runs `consentStore.init(opts)` on mount.** Two consequences
|
|
169
224
|
a component that only wanted to read has to plan for:
|
|
170
225
|
|
|
171
|
-
- **It opens the
|
|
172
|
-
`open: true, reason: 'initial'
|
|
226
|
+
- **It opens the consent UI.** `init` reads storage, and with no stored record it publishes
|
|
227
|
+
`open: true, reason: 'initial'` — or, with `geo`, starts locating. So a `useConsent()` in a card that merely wanted `granted('analytics')`
|
|
173
228
|
gates the page for a first-time visitor. Read a single category with `useConsentCategory(key)` —
|
|
174
229
|
that hook subscribes and does **not** init — or `consentStore.granted(key)` outside React.
|
|
175
230
|
- **The first `useConsent` to mount fixes the Consent Mode defaults.** `init` calls
|
|
@@ -203,6 +258,9 @@ Everything OwlMeans cannot assert on the operator's behalf — who the controlle
|
|
|
203
258
|
basis, how to exercise rights — is deferred to those two links. It also renders a *Manage
|
|
204
259
|
preferences* control, so the policy page is a way back into the decision.
|
|
205
260
|
|
|
261
|
+
**`geo`**, the same object the dialog was given, adds the regional rule
|
|
262
|
+
(`[data-cookie-policy-regional]`, `policyRegional`) after the category list.
|
|
263
|
+
|
|
206
264
|
**`services`** (`ConsentService[]`, see the `consent` skill) discloses WHO receives data: each
|
|
207
265
|
service is listed inside the item of the category whose `key` it names — name, provider, purpose,
|
|
208
266
|
cookie names in `<code>`, and a link to the provider's privacy policy (new tab). A service whose
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type FC } from 'react';
|
|
2
|
+
import type { ConsentBarProps } from './types.local.js';
|
|
3
|
+
/**
|
|
4
|
+
* The first question a visitor is asked, as a tall bar across the bottom of the page.
|
|
5
|
+
*
|
|
6
|
+
* It sits on a TRANSPARENT overlay: the page stays in sight and scrolls, but nothing on it can be
|
|
7
|
+
* pressed until the visitor answers. The text says what the essential cookies do, names the
|
|
8
|
+
* optional categories in force (as a list after a colon, so no language has to decline a label),
|
|
9
|
+
* that they stay off until allowed, and that the choice can be changed or withdrawn — then the
|
|
10
|
+
* policy and the host's other links.
|
|
11
|
+
*
|
|
12
|
+
* Three answers: "Cookie preferences" replaces the bar with the preferences window, "Accept only
|
|
13
|
+
* mandatory" refuses every optional category, "Accept all" grants them. The two answers carry the
|
|
14
|
+
* same accent — refusing is exactly as prominent as accepting, and exactly one click away. With no
|
|
15
|
+
* optional category at all there is nothing to refuse, so "Accept only mandatory" is not offered.
|
|
16
|
+
*
|
|
17
|
+
* `[data-consent-dialog]` and `[data-consent-accept-all]` are here as on the window — one surface is
|
|
18
|
+
* open at a time, so a test or a host keys on them without knowing the mode; `[data-consent-bar]`
|
|
19
|
+
* tells the two apart.
|
|
20
|
+
*/
|
|
21
|
+
export declare const ConsentBar: FC<ConsentBarProps>;
|
|
22
|
+
//# sourceMappingURL=bar.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bar.d.ts","sourceRoot":"","sources":["../../src/consent/bar.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAU,KAAK,EAAE,EAAE,MAAM,OAAO,CAAA;AAOvC,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAA;AAEvD;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,UAAU,EAAE,EAAE,CAAC,eAAe,CAkE1C,CAAA"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useRef } from 'react';
|
|
3
|
+
import { Cookie } from 'lucide-react';
|
|
4
|
+
import { consentI18nHelper } from '@owlmeans/consent';
|
|
5
|
+
import { webConsentUtils } from '../lib/utils.js';
|
|
6
|
+
import { ACCENT, BAR_PILL, OUTLINED } from './consts.local.js';
|
|
7
|
+
import { useConsentFocus } from './focus.js';
|
|
8
|
+
import { ConsentLinks } from './links.js';
|
|
9
|
+
/**
|
|
10
|
+
* The first question a visitor is asked, as a tall bar across the bottom of the page.
|
|
11
|
+
*
|
|
12
|
+
* It sits on a TRANSPARENT overlay: the page stays in sight and scrolls, but nothing on it can be
|
|
13
|
+
* pressed until the visitor answers. The text says what the essential cookies do, names the
|
|
14
|
+
* optional categories in force (as a list after a colon, so no language has to decline a label),
|
|
15
|
+
* that they stay off until allowed, and that the choice can be changed or withdrawn — then the
|
|
16
|
+
* policy and the host's other links.
|
|
17
|
+
*
|
|
18
|
+
* Three answers: "Cookie preferences" replaces the bar with the preferences window, "Accept only
|
|
19
|
+
* mandatory" refuses every optional category, "Accept all" grants them. The two answers carry the
|
|
20
|
+
* same accent — refusing is exactly as prominent as accepting, and exactly one click away. With no
|
|
21
|
+
* optional category at all there is nothing to refuse, so "Accept only mandatory" is not offered.
|
|
22
|
+
*
|
|
23
|
+
* `[data-consent-dialog]` and `[data-consent-accept-all]` are here as on the window — one surface is
|
|
24
|
+
* open at a time, so a test or a host keys on them without knowing the mode; `[data-consent-bar]`
|
|
25
|
+
* tells the two apart.
|
|
26
|
+
*/
|
|
27
|
+
export const ConsentBar = props => {
|
|
28
|
+
const { t, categories, domains } = props;
|
|
29
|
+
const surface = useRef(null);
|
|
30
|
+
useConsentFocus(surface, true);
|
|
31
|
+
const optional = categories.filter(category => category.required !== true);
|
|
32
|
+
const description = optional.length > 0
|
|
33
|
+
? consentI18nHelper.interpolate(t('barDescription', 'We use essential cookies to make this site work — for signing in, security and remembering your choices. With your consent we would also use these optional cookies: {{categories}}. They stay off until you allow them, and you can change or withdraw your choice at any time in the cookie preferences.'), { categories: optional.map(category => t(category.labelKey, category.key)).join(', ') })
|
|
34
|
+
: t('barDescriptionEssential', 'This site uses only essential cookies — for signing in, security and remembering your choices. They need no consent and cannot be switched off.');
|
|
35
|
+
return _jsx("div", { className: "fixed inset-0 z-[999998] bg-transparent", "data-consent-overlay": true, children: _jsx("div", { ref: surface, tabIndex: -1, className: webConsentUtils.cn('fixed inset-x-0 bottom-0 z-[999999] max-h-[85vh] overflow-y-auto border-t border-border bg-background text-foreground outline-none', props.barClassName), role: "dialog", "aria-modal": "true", "aria-labelledby": "cc-bar-title", "aria-describedby": domains.length > 1 ? 'cc-bar-desc cc-bar-domains' : 'cc-bar-desc', "data-consent-dialog": true, "data-consent-bar": true, "data-consent-mode": "bar", children: _jsxs("div", { className: "mx-auto flex max-w-6xl flex-col gap-6 px-4 py-6 sm:px-8 sm:py-8 lg:flex-row lg:items-end lg:gap-10", children: [_jsxs("div", { className: "min-w-0 flex-1", children: [_jsxs("div", { className: "mb-3 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-bar-title", className: "text-balance text-lg font-extrabold tracking-tight text-foreground sm:text-xl", children: t('barTitle', 'Cookies on this site') })] }), _jsx("p", { id: "cc-bar-desc", className: "text-pretty text-sm leading-relaxed text-muted-foreground", children: description }), domains.length > 1 && _jsx("p", { id: "cc-bar-domains", "data-consent-domains": true, className: "mt-2 text-pretty text-xs text-muted-foreground", children: consentI18nHelper.interpolate(t('domains', 'This choice applies to {{domains}}.'), { domains: domains.join(', ') }) }), _jsx(ConsentLinks, { t: t, policyHref: props.policyHref, links: props.links, className: "mt-1" })] }), _jsxs("div", { className: "flex w-full flex-col gap-3 sm:flex-row sm:flex-wrap sm:justify-end lg:w-auto lg:max-w-xl lg:flex-shrink-0", children: [_jsx("button", { type: "button", onClick: props.onPreferences, "data-consent-preferences": true, className: webConsentUtils.cn(BAR_PILL, OUTLINED), children: t('openPreferences', 'Cookie preferences') }), optional.length > 0 && _jsx("button", { type: "button", onClick: props.onMandatory, "data-consent-mandatory": true, className: webConsentUtils.cn(BAR_PILL, ACCENT), children: t('acceptMandatory', 'Accept only mandatory') }), _jsx("button", { type: "button", onClick: props.onAcceptAll, "data-consent-accept-all": true, className: webConsentUtils.cn(BAR_PILL, ACCENT), children: t('acceptAll', 'Accept All') })] })] }) }) });
|
|
36
|
+
};
|
|
37
|
+
//# sourceMappingURL=bar.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bar.js","sourceRoot":"","sources":["../../src/consent/bar.tsx"],"names":[],"mappings":"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"}
|
|
@@ -1,4 +1,15 @@
|
|
|
1
1
|
import { type FC } from 'react';
|
|
2
2
|
import type { CookieConsentProps } from '../types.js';
|
|
3
|
+
/**
|
|
4
|
+
* The cookie-consent UI of a document: one of three surfaces at a time, and the corner button.
|
|
5
|
+
*
|
|
6
|
+
* - While a first-time visitor is being located (`geo`), a transparent overlay with a spinner.
|
|
7
|
+
* - The first ask (`reason: 'initial'`) in `bar` mode — the default — the bar.
|
|
8
|
+
* - Every other opening, and the first ask in `window` mode, the preferences window.
|
|
9
|
+
*
|
|
10
|
+
* Mounted once, at the application root, outside the router: a surface mounted in a route is torn
|
|
11
|
+
* down by the first navigation. The bar and the window share one save path, so whichever answers,
|
|
12
|
+
* the record carries every required category on and exactly the optional ones chosen.
|
|
13
|
+
*/
|
|
3
14
|
export declare const CookieConsent: FC<CookieConsentProps>;
|
|
4
15
|
//# sourceMappingURL=component.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"component.d.ts","sourceRoot":"","sources":["../../src/consent/component.tsx"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"component.d.ts","sourceRoot":"","sources":["../../src/consent/component.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAe,KAAK,EAAE,EAAE,MAAM,OAAO,CAAA;AAM5C,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAA;AAMrD;;;;;;;;;;GAUG;AACH,eAAO,MAAM,aAAa,EAAE,EAAE,CAAC,kBAAkB,CAsEhD,CAAA"}
|
|
@@ -1,41 +1,40 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useCallback
|
|
2
|
+
import { useCallback } from 'react';
|
|
3
3
|
import { Cookie } from 'lucide-react';
|
|
4
|
-
import { DEFAULT_CONSENT_CATEGORIES, consentI18nHelper
|
|
4
|
+
import { DEFAULT_CONSENT_CATEGORIES, consentI18nHelper } from '@owlmeans/consent';
|
|
5
5
|
import { webConsentUtils } from '../lib/utils.js';
|
|
6
6
|
import { useConsent } from '../hooks.js';
|
|
7
|
-
import {
|
|
7
|
+
import { CONSENT_DEFAULT_MODE } from '../consts.js';
|
|
8
|
+
import { ConsentBar } from './bar.js';
|
|
9
|
+
import { ConsentLocatingOverlay } from './locating.js';
|
|
10
|
+
import { ConsentWindow } from './window.js';
|
|
8
11
|
import { FOCUS } from './consts.local.js';
|
|
9
|
-
/**
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
12
|
+
/**
|
|
13
|
+
* The cookie-consent UI of a document: one of three surfaces at a time, and the corner button.
|
|
14
|
+
*
|
|
15
|
+
* - While a first-time visitor is being located (`geo`), a transparent overlay with a spinner.
|
|
16
|
+
* - The first ask (`reason: 'initial'`) in `bar` mode — the default — the bar.
|
|
17
|
+
* - Every other opening, and the first ask in `window` mode, the preferences window.
|
|
18
|
+
*
|
|
19
|
+
* Mounted once, at the application root, outside the router: a surface mounted in a route is torn
|
|
20
|
+
* down by the first navigation. The bar and the window share one save path, so whichever answers,
|
|
21
|
+
* the record carries every required category on and exactly the optional ones chosen.
|
|
22
|
+
*/
|
|
13
23
|
export const CookieConsent = props => {
|
|
14
24
|
const categories = props.categories ?? DEFAULT_CONSENT_CATEGORIES;
|
|
15
25
|
const t = props.translate ?? consentI18nHelper.defaultConsentTranslate(props.locale);
|
|
16
|
-
const
|
|
26
|
+
const mode = props.mode ?? CONSENT_DEFAULT_MODE;
|
|
27
|
+
const consent = useConsent({
|
|
17
28
|
categories,
|
|
18
29
|
...(props.storageKey != null ? { storageKey: props.storageKey } : {}),
|
|
19
30
|
...(props.cookieDays != null ? { cookieDays: props.cookieDays } : {}),
|
|
20
31
|
...(props.cookieDomain != null ? { cookieDomain: props.cookieDomain } : {}),
|
|
21
32
|
...(props.silent != null ? { silent: props.silent } : {}),
|
|
22
33
|
...(props.linker != null ? { linker: props.linker } : {}),
|
|
23
|
-
|
|
24
|
-
|
|
34
|
+
...(props.geo != null ? { geo: props.geo } : {}),
|
|
35
|
+
});
|
|
25
36
|
const domains = webConsentUtils.disclosedDomains(props.linker);
|
|
26
37
|
const optional = categories.filter(category => category.required !== true);
|
|
27
|
-
const [draft, setDraft] = useState({});
|
|
28
|
-
// Re-seed the draft whenever the dialog opens, from what is actually stored: a visitor who opens
|
|
29
|
-
// preferences a second time must see the answer they gave, not the one the last render held.
|
|
30
|
-
useEffect(() => {
|
|
31
|
-
if (!consent.open) {
|
|
32
|
-
return;
|
|
33
|
-
}
|
|
34
|
-
const stored = consent.record ?? consentStorageHelper.readConsent({
|
|
35
|
-
...(props.storageKey != null ? { storageKey: props.storageKey } : {}),
|
|
36
|
-
});
|
|
37
|
-
setDraft(Object.fromEntries(optional.map(category => [category.key, stored?.[category.key] === true])));
|
|
38
|
-
}, [consent.open, consent.record]);
|
|
39
38
|
const persist = useCallback((values) => {
|
|
40
39
|
const record = Object.fromEntries([
|
|
41
40
|
...categories.filter(c => c.required === true).map(c => [c.key, true]),
|
|
@@ -43,14 +42,9 @@ export const CookieConsent = props => {
|
|
|
43
42
|
]);
|
|
44
43
|
consent.save(record);
|
|
45
44
|
}, [categories, consent]);
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
// sense rather than look like the page asking twice.
|
|
51
|
-
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", "aria-modal": "true", role: "dialog", "aria-labelledby": "cc-title", "aria-describedby": domains.length > 1 ? 'cc-desc cc-domains' : 'cc-desc', "data-consent-dialog": true, children: _jsxs("div", { className: webConsentUtils.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.') }), domains.length > 1 && _jsx("p", { id: "cc-domains", "data-consent-domains": true, className: "mb-6 -mt-3 text-pretty text-xs text-muted-foreground", children: consentI18nHelper.interpolate(t('domains', 'This choice applies to {{domains}}.'), { domains: domains.join(', ') }) }), _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: webConsentUtils.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: webConsentUtils.cn(PILL, 'bg-primary font-bold text-primary-foreground hover:bg-primary/90'), children: gated
|
|
53
|
-
? t('acceptAndContinue', 'Accept & continue')
|
|
54
|
-
: 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: webConsentUtils.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" }) })] });
|
|
45
|
+
const surface = consent.open
|
|
46
|
+
? mode === 'bar' && consent.reason === 'initial' ? 'bar' : 'window'
|
|
47
|
+
: consent.locating === 'first' ? 'locating' : null;
|
|
48
|
+
return _jsxs(_Fragment, { children: [surface === 'locating' && _jsx(ConsentLocatingOverlay, { t: t }), surface === 'bar' && _jsx(ConsentBar, { t: t, categories: categories, domains: domains, policyHref: props.policyHref, links: props.links, barClassName: props.barClassName, onPreferences: () => consent.openDialog('preferences'), onMandatory: () => persist({}), onAcceptAll: () => persist(Object.fromEntries(optional.map(c => [c.key, true]))) }), surface === 'window' && _jsx(ConsentWindow, { t: t, categories: categories, domains: domains, record: consent.record, ...(props.storageKey != null ? { storageKey: props.storageKey } : {}), gated: consent.reason === 'login', policyHref: props.policyHref, links: props.links, className: props.className, onSave: persist }, consent.reason ?? 'window'), props.noReopenButton !== true && consent.record != null && !consent.open && consent.locating == null && _jsx("button", { type: "button", onClick: () => consent.openDialog('reopen'), "aria-label": t('openPreferences', 'Cookie preferences'), "data-consent-reopen": true, className: webConsentUtils.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
49
|
};
|
|
56
50
|
//# sourceMappingURL=component.js.map
|