@owlmeans/web-consent 0.1.18-rc.47 → 0.1.18-rc.48
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 +2 -2
- package/agent-meta/manifest.json +92 -4
- package/agent-meta/skills/web-consent/SKILL.md +17 -261
- package/agent-meta/skills/web-consent/parts/dialog.md +110 -0
- package/agent-meta/skills/web-consent/parts/look.md +52 -0
- package/agent-meta/skills/web-consent/parts/menu.md +28 -0
- package/agent-meta/skills/web-consent/parts/policy-page.md +55 -0
- package/agent-meta/skills/web-consent/parts/setup.md +62 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -14,7 +14,7 @@ The `web-consent` skill covers the components, the `consent` skill the model beh
|
|
|
14
14
|
## Installation
|
|
15
15
|
|
|
16
16
|
```bash
|
|
17
|
-
bun add @owlmeans/web-consent@^0.1.18-rc.
|
|
17
|
+
bun add @owlmeans/web-consent@^0.1.18-rc.48
|
|
18
18
|
```
|
|
19
19
|
|
|
20
20
|
Peer dependencies the app provides: `react`, `tailwindcss`, `tailwind-merge`, `clsx`, `lucide-react`.
|
|
@@ -230,7 +230,7 @@ This package ships embedded agent skills under `agent-meta/`. After installing y
|
|
|
230
230
|
your project's skill store (`.agents/skills/`):
|
|
231
231
|
|
|
232
232
|
```sh
|
|
233
|
-
npx @owlmeans/agent-skills@^0.1.18-rc.
|
|
233
|
+
npx @owlmeans/agent-skills@^0.1.18-rc.57
|
|
234
234
|
```
|
|
235
235
|
|
|
236
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
|
-
"schemaVersion":
|
|
2
|
+
"schemaVersion": 3,
|
|
3
3
|
"package": "@owlmeans/web-consent",
|
|
4
|
-
"version": "0.1.18-rc.
|
|
5
|
-
"generatedAt": "2026-10-
|
|
4
|
+
"version": "0.1.18-rc.48",
|
|
5
|
+
"generatedAt": "2026-10-20T23:19:58.629Z",
|
|
6
6
|
"canonicalRepo": "https://github.com/owlmeans/common",
|
|
7
7
|
"entries": [
|
|
8
8
|
{
|
|
@@ -10,7 +10,95 @@
|
|
|
10
10
|
"name": "web-consent",
|
|
11
11
|
"category": "package-specific",
|
|
12
12
|
"file": "skills/web-consent/SKILL.md",
|
|
13
|
-
"canonicalPath": ".agents/skills/web-consent/SKILL.md"
|
|
13
|
+
"canonicalPath": ".agents/skills/web-consent/SKILL.md",
|
|
14
|
+
"parts": [
|
|
15
|
+
{
|
|
16
|
+
"name": "dialog",
|
|
17
|
+
"file": "skills/web-consent/parts/dialog.md",
|
|
18
|
+
"description": "Mounting CookieConsent once — its surfaces (locating spinner, bar, preferences window), its props, the re-open button, the linker line, the bar, focus, and the login-raised window.",
|
|
19
|
+
"purposes": [
|
|
20
|
+
"consent",
|
|
21
|
+
"dialog",
|
|
22
|
+
"layout"
|
|
23
|
+
],
|
|
24
|
+
"mentions": [
|
|
25
|
+
"CookieConsent",
|
|
26
|
+
"CookieConsentProps",
|
|
27
|
+
"ConsentDisplayMode",
|
|
28
|
+
"CONSENT_DEFAULT_MODE",
|
|
29
|
+
"noReopenButton",
|
|
30
|
+
"policyHref",
|
|
31
|
+
"ConsentGeoOptions",
|
|
32
|
+
"ConsentLinkerOptions",
|
|
33
|
+
"barClassName",
|
|
34
|
+
"data-consent-dialog",
|
|
35
|
+
"data-consent-bar",
|
|
36
|
+
"data-consent-reopen"
|
|
37
|
+
]
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"name": "look",
|
|
41
|
+
"file": "skills/web-consent/parts/look.md",
|
|
42
|
+
"description": "The flat look from the host's tokens — the class table, focus outlines, switches named by reference, and the axe and harness checks that pin them.",
|
|
43
|
+
"purposes": [
|
|
44
|
+
"consent",
|
|
45
|
+
"theme"
|
|
46
|
+
],
|
|
47
|
+
"mentions": [
|
|
48
|
+
"barClassName",
|
|
49
|
+
"ConsentToggle",
|
|
50
|
+
"data-consent-overlay",
|
|
51
|
+
"a11y.spec",
|
|
52
|
+
"outline-ring"
|
|
53
|
+
]
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
"name": "menu",
|
|
57
|
+
"file": "skills/web-consent/parts/menu.md",
|
|
58
|
+
"description": "ConsentMenuWidget, a plain row a host's own menu renders to reopen the dialog, and where its presence signal must come from.",
|
|
59
|
+
"purposes": [
|
|
60
|
+
"consent",
|
|
61
|
+
"navigation"
|
|
62
|
+
],
|
|
63
|
+
"mentions": [
|
|
64
|
+
"ConsentMenuWidget",
|
|
65
|
+
"useConsentMenuPresence",
|
|
66
|
+
"consentStore.open"
|
|
67
|
+
]
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
"name": "policy-page",
|
|
71
|
+
"file": "skills/web-consent/parts/policy-page.md",
|
|
72
|
+
"description": "The generated CookiePolicy page — what it states, the regional rule, the services disclosure, the linker domain list and its heading structure.",
|
|
73
|
+
"purposes": [
|
|
74
|
+
"consent",
|
|
75
|
+
"screen"
|
|
76
|
+
],
|
|
77
|
+
"mentions": [
|
|
78
|
+
"CookiePolicy",
|
|
79
|
+
"CookiePolicyProps",
|
|
80
|
+
"ConsentService",
|
|
81
|
+
"googleTagServices",
|
|
82
|
+
"data-cookie-policy"
|
|
83
|
+
]
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
"name": "setup",
|
|
87
|
+
"file": "skills/web-consent/parts/setup.md",
|
|
88
|
+
"description": "The Tailwind @source line pointing at src, and the packaged i18n bundle with the translate fall-through.",
|
|
89
|
+
"purposes": [
|
|
90
|
+
"consent",
|
|
91
|
+
"config"
|
|
92
|
+
],
|
|
93
|
+
"mentions": [
|
|
94
|
+
"defaultConsentTranslate",
|
|
95
|
+
"consentI18nHelper",
|
|
96
|
+
"CONSENT_LOCALES",
|
|
97
|
+
"web-consent/src",
|
|
98
|
+
"@source"
|
|
99
|
+
]
|
|
100
|
+
}
|
|
101
|
+
]
|
|
14
102
|
}
|
|
15
103
|
]
|
|
16
104
|
}
|
|
@@ -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.48"` 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**
|
|
@@ -28,7 +28,7 @@ no such alias, where that would simply fail to build.
|
|
|
28
28
|
|
|
29
29
|
For the same reason it is not part of `@owlmeans/web-panel`: that would drag ~30 packages onto a
|
|
30
30
|
cookie notice. `@owlmeans/web-panel/consent` is the thin binding to OwlMeans i18n on top of these
|
|
31
|
-
components — see
|
|
31
|
+
components — see the i18n section of [parts/setup.md](parts/setup.md).
|
|
32
32
|
|
|
33
33
|
## Key Exports
|
|
34
34
|
|
|
@@ -36,7 +36,7 @@ components — see below.
|
|
|
36
36
|
|--------|-------------|
|
|
37
37
|
| `CookieConsent` | The consent UI — the bar, the preferences window, the locating spinner — **and** the persistent re-open button |
|
|
38
38
|
| `ConsentDisplayMode` / `CONSENT_DEFAULT_MODE` | `'bar'` (the default) or `'window'` — how a first-time visitor is asked |
|
|
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
|
|
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 [parts/menu.md](parts/menu.md) |
|
|
40
40
|
| `CookiePolicy` | The cookie-policy page, generated from the configuration in force |
|
|
41
41
|
| `ConsentToggle` | One category row — locked and labelled when the category is required |
|
|
42
42
|
| `useConsent(opts?)` | This document's consent state and the actions over it (`UseConsentModel`) — **it also initialises the store on mount**, see below |
|
|
@@ -44,166 +44,6 @@ components — see below.
|
|
|
44
44
|
| `CookieConsentProps` / `CookiePolicyProps` / `ConsentLink` | The component props |
|
|
45
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 |
|
|
46
46
|
|
|
47
|
-
## Mounting the dialog
|
|
48
|
-
|
|
49
|
-
`CookieConsent` is mounted **once**, at the application root or in the layout. It opens itself when
|
|
50
|
-
no decision is stored, renders nothing but the re-open button once one is, and needs no state from
|
|
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.
|
|
65
|
-
|
|
66
|
-
```tsx
|
|
67
|
-
import { CookieConsent } from '@owlmeans/web-consent'
|
|
68
|
-
|
|
69
|
-
<CookieConsent
|
|
70
|
-
policyHref="/legal/cookies"
|
|
71
|
-
links={[{ href: '/legal/privacy', labelKey: 'privacy', defaultLabel: 'Privacy Policy' }]}
|
|
72
|
-
/>
|
|
73
|
-
```
|
|
74
|
-
|
|
75
|
-
- **The category set the dialog renders is the set it saves.** Pass `categories` here and pass the
|
|
76
|
-
same set to whatever stamps the head snippet, or the two disagree about what was asked. The same
|
|
77
|
-
goes for `storageKey`, `cookieDays` and `cookieDomain`.
|
|
78
|
-
- `locale` picks the packaged language; leave it unset and English is used, so an app with a
|
|
79
|
-
language of its own passes it (or mounts `PanelCookieConsent`, which does). A region tag is
|
|
80
|
-
reduced to its base (`pl-PL` → `pl`), and anything outside `CONSENT_LOCALES` falls back to English.
|
|
81
|
-
- `policyHref` is a plain string, so an app-resolved path, a framework route and a raw href all
|
|
82
|
-
work — the component must not know how its host does routing.
|
|
83
|
-
- `noReopenButton` hides the floating button for an app that offers a footer link instead; that link
|
|
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`.
|
|
90
|
-
- **The re-open button is a bare icon in the very corner (`bottom-1 left-1`), not a card.** No
|
|
91
|
-
filled background, no border, no shadow, no hover-scale — `bg-transparent`, dimmed
|
|
92
|
-
(`opacity-70`) at rest and picked out on hover/focus — because it sits on every page of a site
|
|
93
|
-
for as long as a visitor stays and must read as a small fixture rather than compete with the
|
|
94
|
-
page's own controls. The pictogram is 20px (`h-5 w-5`) inside an invisible 44px hit area
|
|
95
|
-
(`h-11 w-11`). `[data-consent-reopen]` is what any test or CSS override keys on.
|
|
96
|
-
- **`linker` (`ConsentLinkerOptions`)** turns on cross-domain consent (see the `consent` skill's
|
|
97
|
-
plugin-seam section): `<CookieConsent linker={{ domains: ['owlmeans.com', 'owlmeans.pl'] }} />`
|
|
98
|
-
passes it straight to `useConsent`, which registers `consentLinkHelper.consentLinker` on mount. With it set and
|
|
99
|
-
more than just the current host to disclose, a domain line renders right after the description
|
|
100
|
-
(`[data-consent-domains]`, added to the dialog's `aria-describedby`) — computed DIRECTLY from
|
|
101
|
-
`linker.domains` plus the current host, not through the plugin registry, so it never depends on
|
|
102
|
-
registration having finished yet. Omit `linker` and nothing here changes at all. The same object
|
|
103
|
-
carries `language` (`{}` to send the page's language, `{ supported }` to adopt one — the `consent`
|
|
104
|
-
skill's language section): the dialog only forwards it to the store and discloses nothing extra.
|
|
105
|
-
- **The interface language is not a category.** The default set renders the required row and the
|
|
106
|
-
analytics and marketing toggles only; the language is strictly necessary storage that no toggle
|
|
107
|
-
governs (the `consent` skill). The built-in bundle (8 languages) describes it in the required row.
|
|
108
|
-
|
|
109
|
-
## The bar
|
|
110
|
-
|
|
111
|
-
A tall bar across the bottom of a **dimmed** overlay (`[data-consent-overlay]`,
|
|
112
|
-
`fixed inset-0 bg-black/70`): 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
|
-
|
|
135
|
-
## The look — flat, from the host's tokens
|
|
136
|
-
|
|
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:
|
|
140
|
-
|
|
141
|
-
| Part | Classes |
|
|
142
|
-
|---|---|
|
|
143
|
-
| Bar overlay | `bg-black/70`, matching the preferences window — the page is dimmed beneath the opaque bar |
|
|
144
|
-
| Locating overlay | `bg-transparent` — a quick region lookup flashes no backdrop |
|
|
145
|
-
| Bar | `border-t border-border bg-background text-foreground`, no shadow |
|
|
146
|
-
| Bar answers | "Accept only mandatory" and "Accept All" both the accent pill; "Cookie preferences" outlined |
|
|
147
|
-
| Spinner | `text-muted-foreground motion-safe:animate-spin` |
|
|
148
|
-
| Window overlay | `bg-black/70`, flat — no blur |
|
|
149
|
-
| Card | `rounded-3xl border border-border bg-background text-foreground`, no shadow or ring |
|
|
150
|
-
| Icon | `rounded-full bg-muted text-primary` |
|
|
151
|
-
| Category row | `rounded-2xl border border-border`; *Required* is plain uppercase muted text, never a badge |
|
|
152
|
-
| Switch | `bg-primary` on / `bg-muted-foreground/40` off, white knob; 44px hit area around the 24×44 track |
|
|
153
|
-
| Accept | the one accent pill — `rounded-full bg-primary text-primary-foreground min-h-11` |
|
|
154
|
-
| Save, policy *Manage preferences* | outlined pill — `rounded-full border-[1.5px] border-foreground bg-transparent`, `hover:bg-muted` |
|
|
155
|
-
| Links | muted, underlined at rest, `min-h-11` |
|
|
156
|
-
|
|
157
|
-
Every control shows a 3px `outline-ring` focus outline 3px off the element (`outline-3
|
|
158
|
-
outline-offset-3`; the switch draws it on its track through `peer-focus-visible`). An outline, not
|
|
159
|
-
a ring, so a host that zeroes shadows keeps it. Never reintroduce a decorative class here — the
|
|
160
|
-
package tests fail on `bg-gradient`, `backdrop-`, `blur`, `shadow` or `ring-primary` in the dialog.
|
|
161
|
-
|
|
162
|
-
**Each switch is named by reference.** The `<label>` around a switch's checkbox holds only the
|
|
163
|
-
drawn track, and the category's words sit in a sibling column it cannot reach, so the checkbox
|
|
164
|
-
carries `aria-labelledby="<id>-label"` (the category label) and `aria-describedby` pointing at
|
|
165
|
-
`<id>-required` and `<id>-desc`. Drop those and axe reports three unnamed checkboxes (`label`,
|
|
166
|
-
critical). A required category stays `disabled` and keeps its name.
|
|
167
|
-
|
|
168
|
-
`tests/a11y.spec.ts` runs axe-core (a dev dependency) against the open bar and the open window in
|
|
169
|
-
both schemes, the locating overlay, the re-open button, and the policy page with services and the
|
|
170
|
-
regional rule, and fails on any serious or critical violation. It loads the harness with `?styled=1` (plus `&theme=dark`), which compiles the package
|
|
171
|
-
sources with a generated app's neutral tokens — contrast cannot be judged on the unstyled harness
|
|
172
|
-
the other specs need. The harness also takes `?mode=bar|window`, `?categories=essential`,
|
|
173
|
-
`?geo=<CC>|fail|html|hang` (its server answers `/cdn-cgi/trace` from the page's own query, so every
|
|
174
|
-
case is still chosen by URL; `&geoDelay=<ms>`, `&geoTimeout=<ms>`), `?geoPlugin=<CC>|fail` and
|
|
175
|
-
`?gpc=1` — `tests/bar.spec.ts` and `tests/geo.spec.ts` drive them.
|
|
176
|
-
- `silent` skips every `dataLayer` and global write. It is for tests and for an app that runs no
|
|
177
|
-
tags at all.
|
|
178
|
-
- The window's switches are **seeded from storage every time it opens** — it is mounted only while
|
|
179
|
-
open — never from the last render: a visitor reopening preferences must see the answer they gave.
|
|
180
|
-
An automatic decision past its age seeds nothing.
|
|
181
|
-
|
|
182
|
-
**When the window was raised by something waiting on it** — `reason === 'login'`, which the sign-in
|
|
183
|
-
precondition in `@owlmeans/client-iam` raises — it says so and relabels the primary action *Accept
|
|
184
|
-
& continue*. The sign-in gate always gets the window, never the bar. An automatic decision is said
|
|
185
|
-
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
|
|
186
|
-
twice. Word that path as an acknowledgement, never as "you must consent to essential cookies": a
|
|
187
|
-
required category is disclosure, not a question.
|
|
188
|
-
|
|
189
|
-
## A menu row for a host that already has one
|
|
190
|
-
|
|
191
|
-
`ConsentMenuWidget` renders one row — icon, translated label, `onClick` reopening the dialog
|
|
192
|
-
(`consentStore.open('reopen')` by default, or a caller's own `onSelect`) — for a host whose own
|
|
193
|
-
navigation already carries a settings menu and would rather offer cookie preferences there than as
|
|
194
|
-
a second floating button. It calls `consentStore.open` directly, never `useConsent()`, for the same
|
|
195
|
-
init-on-mount reason as everywhere else in this document.
|
|
196
|
-
|
|
197
|
-
**It carries no presence signalling of its own.** A host that wants `CookieConsent`'s floating
|
|
198
|
-
button to hide while this row is reachable needs to know that itself, from something that stays
|
|
199
|
-
mounted for as long as the row is reachable — never from the row's own mount. A row placed inside a
|
|
200
|
-
host menu's lazily-rendered content (Radix's `DropdownMenuContent`, and most headless menu
|
|
201
|
-
primitives, only mount their children while the menu is actually OPEN) reports "present" for a
|
|
202
|
-
fraction of the time the menu itself is on screen, and the floating button reappears the instant
|
|
203
|
-
the menu closes. `@owlmeans/web-panel/consent`'s `useConsentMenuPresence()` is the fix for a
|
|
204
|
-
context-aware host: call it from the menu's own always-mounted shell component, not from inside the
|
|
205
|
-
row. A context-free host (an Astro island with its own menu) owns that judgement itself.
|
|
206
|
-
|
|
207
47
|
## Reading the decision
|
|
208
48
|
|
|
209
49
|
```tsx
|
|
@@ -233,109 +73,13 @@ a component that only wanted to read has to plan for:
|
|
|
233
73
|
gets there first declares the `consent/default` signals from *its* categories, and every later
|
|
234
74
|
call returns immediately. A bare `useConsent()` mounting before `CookieConsent` therefore declares
|
|
235
75
|
`DEFAULT_CONSENT_CATEGORIES`' signals and the app's own `categories` never declare theirs — which
|
|
236
|
-
silently breaks the parity rule
|
|
76
|
+
silently breaks the parity rule (the category set the dialog renders is the set it saves —
|
|
77
|
+
[parts/dialog.md](parts/dialog.md)). Pass the app's `opts` wherever `useConsent` is called, or
|
|
237
78
|
do not call it outside the dialog.
|
|
238
79
|
|
|
239
80
|
`opts` are read on the mounting pass only (the effect's dependency list is empty), so changing them
|
|
240
81
|
in a later render has no effect on that mount.
|
|
241
82
|
|
|
242
|
-
## The policy page
|
|
243
|
-
|
|
244
|
-
`CookiePolicy` states only what the widget provably does — the categories in force, the storage key,
|
|
245
|
-
the dual storage, the retention — all read from the same configuration the dialog renders. That is
|
|
246
|
-
why it is generated rather than written: a hand-written policy drifts the first time a category
|
|
247
|
-
changes, and nobody notices because nobody reads it until it matters.
|
|
248
|
-
|
|
249
|
-
```tsx
|
|
250
|
-
import { googleTagHelper } from '@owlmeans/web-gtm'
|
|
251
|
-
|
|
252
|
-
<CookiePolicy
|
|
253
|
-
operator="Example Sp. z o.o." privacyHref="/legal/privacy" termsHref="/legal/terms"
|
|
254
|
-
services={googleTag !== '' ? googleTagHelper.googleTagServices(googleTag) : undefined}
|
|
255
|
-
/>
|
|
256
|
-
```
|
|
257
|
-
|
|
258
|
-
Everything OwlMeans cannot assert on the operator's behalf — who the controller is, the lawful
|
|
259
|
-
basis, how to exercise rights — is deferred to those two links. It also renders a *Manage
|
|
260
|
-
preferences* control, so the policy page is a way back into the decision.
|
|
261
|
-
|
|
262
|
-
**`geo`**, the same object the dialog was given, adds the regional rule
|
|
263
|
-
(`[data-cookie-policy-regional]`, `policyRegional`) after the category list.
|
|
264
|
-
|
|
265
|
-
**`services`** (`ConsentService[]`, see the `consent` skill) discloses WHO receives data: each
|
|
266
|
-
service is listed inside the item of the category whose `key` it names — name, provider, purpose,
|
|
267
|
-
cookie names in `<code>`, and a link to the provider's privacy policy (new tab). A service whose
|
|
268
|
-
category is not among `categories` goes into a trailing *Other services* item rather than being
|
|
269
|
-
dropped. Without `services` the page renders exactly as it always has.
|
|
270
|
-
|
|
271
|
-
The structure stays one `h1` and lists: categories are `li`s, a category's services are a nested
|
|
272
|
-
`ul` named by `aria-label` (*Services — ‹category label›*), each service a `li` holding a `dl`.
|
|
273
|
-
Never add headings for categories or services — they would skip a level under the `h1`. Test and
|
|
274
|
-
CSS hooks: `[data-cookie-policy-category="<key>"]`, `[data-cookie-policy-services]`,
|
|
275
|
-
`[data-cookie-policy-service]`, `[data-cookie-policy-other]`.
|
|
276
|
-
|
|
277
|
-
**`linker`**, same shape as `CookieConsent`'s, adds a plain `[data-cookie-policy-domains]` LIST
|
|
278
|
-
(never a heading) right after the storage-key paragraph — the current host plus every domain
|
|
279
|
-
`linker.domains` names, computed the same direct way the dialog computes it. Omitted or with
|
|
280
|
-
nothing beyond the current host to disclose, nothing here renders.
|
|
281
|
-
|
|
282
|
-
The service labels (`policyProvider`, `policyPurpose`, `policyCookies`, `policyServicesOf`,
|
|
283
|
-
`policyServicePrivacy`, `policyOtherServices`, `policyOtherServicesDesc`) go through `translate`
|
|
284
|
-
with English defaults and are in the packaged bundle for every locale; the service's own `name`,
|
|
285
|
-
`provider` and `purpose` are data and render as given.
|
|
286
|
-
|
|
287
|
-
## Tailwind — one `@source` line, pointing at `src`
|
|
288
|
-
|
|
289
|
-
The components emit Tailwind classes that exist nowhere in the consumer's own sources, and
|
|
290
|
-
Tailwind's scanner reads the CSS root plus `@source` directives only, excluding `node_modules`. Add
|
|
291
|
-
the line to the app's Tailwind entry:
|
|
292
|
-
|
|
293
|
-
```css
|
|
294
|
-
@import "tailwindcss";
|
|
295
|
-
|
|
296
|
-
@source "<relative path to node_modules>/@owlmeans/web-consent/src";
|
|
297
|
-
```
|
|
298
|
-
|
|
299
|
-
**Point it at `src`, never at `build`.** The scanner applies the `.gitignore` of whatever repository
|
|
300
|
-
a path resolves into, and under a linked workspace the `node_modules` entry is a symlink into a
|
|
301
|
-
monorepo where every package's build output is ignored. A `build` source there scans **zero files
|
|
302
|
-
and reports nothing**: the build succeeds, the CSS is emitted, and the dialog renders *half*-styled
|
|
303
|
-
— the utilities the app happens to use elsewhere still exist while the ones only this package asks
|
|
304
|
-
for (`max-w-lg`, `bg-black/70`, `z-[999998]`) do not. What reaches the screen is a full-width,
|
|
305
|
-
backdrop-less dialog with the page bleeding through, and nothing in the app's sources looks wrong.
|
|
306
|
-
`src` is tracked and ships in the published tarball, so one path serves both a linked checkout and
|
|
307
|
-
an npm install.
|
|
308
|
-
|
|
309
|
-
The failure is silent, so verify rather than assume — grep the emitted stylesheet for a class only
|
|
310
|
-
this package uses:
|
|
311
|
-
|
|
312
|
-
```sh
|
|
313
|
-
grep -c 'max-w-lg' dist/assets/*.css
|
|
314
|
-
```
|
|
315
|
-
|
|
316
|
-
A `*/` inside a CSS comment ends it early, so a comment above an `@source` that spells out a glob
|
|
317
|
-
closes the comment at that star-slash and turns the next `@source` into an invalid declaration.
|
|
318
|
-
Describe a glob in words, or keep it out of the comment.
|
|
319
|
-
|
|
320
|
-
## i18n
|
|
321
|
-
|
|
322
|
-
Given no `translate` prop, the components resolve every string through the packaged bundle for
|
|
323
|
-
`locale` — every language in `CONSENT_LOCALES` (the framework's seven plus French). The bundle
|
|
324
|
-
lives in `@owlmeans/consent` (`src/i18n/<locale>.json`); a new key goes into every one of them,
|
|
325
|
-
and that package's tests fail on a missing key or a dropped `{{placeholder}}` in any locale.
|
|
326
|
-
This package's `tests/bundle.spec.ts` reads every `t('…')` literal from the components, so a new
|
|
327
|
-
string without an English bundle entry fails there. A JSON-only edit is not re-emitted by an
|
|
328
|
-
incremental `tsc -b` — rebuild `@owlmeans/consent` with `tsc -b --force`, or consumers keep the old
|
|
329
|
-
bundle from `build/i18n`. **Once a `translate` prop is given,
|
|
330
|
-
the packaged bundle is not consulted at all**, so a wrapper that forwards a framework resolver alone
|
|
331
|
-
renders the English default for every key the application has not overridden, in every language. The
|
|
332
|
-
resolver must fall through to `consentI18nHelper.defaultConsentTranslate(locale)` for the default — which is exactly
|
|
333
|
-
what `@owlmeans/web-panel/consent` does, and why an app inside the panel family mounts
|
|
334
|
-
`PanelCookieConsent` / `PanelCookiePolicy` rather than these components directly.
|
|
335
|
-
|
|
336
|
-
`ConsentToggle` is exported for a host that builds its own dialog body; the wording it shows is the
|
|
337
|
-
caller's, already resolved.
|
|
338
|
-
|
|
339
83
|
## Depends On
|
|
340
84
|
|
|
341
85
|
- `@owlmeans/consent` — the model; the named selection above is re-exported from this package's root
|
|
@@ -351,3 +95,15 @@ caller's, already resolved.
|
|
|
351
95
|
- `login-methods` / `login-plugins` — the sign-in precondition that raises this dialog with
|
|
352
96
|
`reason: 'login'`
|
|
353
97
|
- `web-panel` — its `./consent` subpath, the OwlMeans-i18n binding of these components
|
|
98
|
+
|
|
99
|
+
## Parts
|
|
100
|
+
|
|
101
|
+
Load a part on top of this core when its job is in play.
|
|
102
|
+
|
|
103
|
+
| Part | Load it when |
|
|
104
|
+
|---|---|
|
|
105
|
+
| [dialog](parts/dialog.md) | Mounting `CookieConsent` — its surfaces, props (`mode`, `geo`, `linker`, `categories`), the re-open button, the bar, focus, the login-raised window |
|
|
106
|
+
| [look](parts/look.md) | Restyling the bar or the window, or checking the flat look, focus outlines, switch names and the a11y harness |
|
|
107
|
+
| [menu](parts/menu.md) | Offering cookie preferences from a host's own menu (`ConsentMenuWidget`) |
|
|
108
|
+
| [policy-page](parts/policy-page.md) | Rendering the generated `CookiePolicy` page, its services disclosure or regional rule |
|
|
109
|
+
| [setup](parts/setup.md) | Adding the Tailwind `@source` line every consumer needs (pointing at `src`), or passing a `translate` resolver / adding a string |
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: dialog
|
|
3
|
+
description: Mounting CookieConsent once — its surfaces (locating spinner, bar, preferences window), its props, the re-open button, the linker line, the bar, focus, and the login-raised window.
|
|
4
|
+
metadata:
|
|
5
|
+
purposes: consent, dialog, layout
|
|
6
|
+
mentions: CookieConsent, CookieConsentProps, ConsentDisplayMode, CONSENT_DEFAULT_MODE, noReopenButton, policyHref, ConsentGeoOptions, ConsentLinkerOptions, barClassName, data-consent-dialog, data-consent-bar, data-consent-reopen
|
|
7
|
+
---
|
|
8
|
+
<!-- AUTO-GENERATED — do not edit. Regenerate via sync-agent-meta. -->
|
|
9
|
+
|
|
10
|
+
# @owlmeans/web-consent — The dialog and the bar
|
|
11
|
+
|
|
12
|
+
## Mounting the dialog
|
|
13
|
+
|
|
14
|
+
`CookieConsent` is mounted **once**, at the application root or in the layout. It opens itself when
|
|
15
|
+
no decision is stored, renders nothing but the re-open button once one is, and needs no state from
|
|
16
|
+
the caller. One surface at a time:
|
|
17
|
+
|
|
18
|
+
- **while a first-time visitor is located** (`geo`): a transparent overlay that blocks the page,
|
|
19
|
+
and a spinner (`[data-consent-locating="first"]`, `[data-consent-spinner]` after
|
|
20
|
+
`CONSENT_LOCATING_DELAY`, 300 ms) — never the bar;
|
|
21
|
+
- **the first ask** (`reason: 'initial'`) in `bar` mode, the default: the bar;
|
|
22
|
+
- **every other opening** — the bar's own "Cookie preferences" (`'preferences'`), a footer link or
|
|
23
|
+
the corner button (`'reopen'`), the sign-in gate (`'login'`) — and the first ask in `window`
|
|
24
|
+
mode: the preferences window.
|
|
25
|
+
|
|
26
|
+
`[data-consent-dialog]` (with `role="dialog"`, `aria-modal`) and `[data-consent-accept-all]` sit on
|
|
27
|
+
whichever surface is up, so a test or a host keys on them without knowing the mode;
|
|
28
|
+
`data-consent-mode="bar|window"` and `[data-consent-bar]` tell them apart. "Cookie preferences"
|
|
29
|
+
REPLACES the bar with the window — exactly one of each exists at a time.
|
|
30
|
+
|
|
31
|
+
```tsx
|
|
32
|
+
import { CookieConsent } from '@owlmeans/web-consent'
|
|
33
|
+
|
|
34
|
+
<CookieConsent
|
|
35
|
+
policyHref="/legal/cookies"
|
|
36
|
+
links={[{ href: '/legal/privacy', labelKey: 'privacy', defaultLabel: 'Privacy Policy' }]}
|
|
37
|
+
/>
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
- **The category set the dialog renders is the set it saves.** Pass `categories` here and pass the
|
|
41
|
+
same set to whatever stamps the head snippet, or the two disagree about what was asked. The same
|
|
42
|
+
goes for `storageKey`, `cookieDays` and `cookieDomain`.
|
|
43
|
+
- `locale` picks the packaged language; leave it unset and English is used, so an app with a
|
|
44
|
+
language of its own passes it (or mounts `PanelCookieConsent`, which does). A region tag is
|
|
45
|
+
reduced to its base (`pl-PL` → `pl`), and anything outside `CONSENT_LOCALES` falls back to English.
|
|
46
|
+
- `policyHref` is a plain string, so an app-resolved path, a framework route and a raw href all
|
|
47
|
+
work — the component must not know how its host does routing.
|
|
48
|
+
- `noReopenButton` hides the floating button for an app that offers a footer link instead; that link
|
|
49
|
+
calls `consentStore.open('reopen')`.
|
|
50
|
+
- **`mode`** (`'bar'` | `'window'`, default `CONSENT_DEFAULT_MODE`) picks the first ask's surface.
|
|
51
|
+
- **`geo`** (`ConsentGeoOptions`, plain data — an Astro island can pass it) turns on the geo gate:
|
|
52
|
+
`geo={{ cloudflare: true }}` on a Cloudflare-proxied host. It is passed through to
|
|
53
|
+
`consentStore.init` like `linker`; the `consent` skill's "Where consent is asked" is the model. A
|
|
54
|
+
locator plugin is code and is registered, not passed. Pass the same `geo` to `CookiePolicy`.
|
|
55
|
+
- **The re-open button is a bare icon in the very corner (`bottom-1 left-1`), not a card.** No
|
|
56
|
+
filled background, no border, no shadow, no hover-scale — `bg-transparent`, dimmed
|
|
57
|
+
(`opacity-70`) at rest and picked out on hover/focus — because it sits on every page of a site
|
|
58
|
+
for as long as a visitor stays and must read as a small fixture rather than compete with the
|
|
59
|
+
page's own controls. The pictogram is 20px (`h-5 w-5`) inside an invisible 44px hit area
|
|
60
|
+
(`h-11 w-11`). `[data-consent-reopen]` is what any test or CSS override keys on.
|
|
61
|
+
- **`linker` (`ConsentLinkerOptions`)** turns on cross-domain consent (see the `consent` skill's
|
|
62
|
+
plugin-seam section): `<CookieConsent linker={{ domains: ['owlmeans.com', 'owlmeans.pl'] }} />`
|
|
63
|
+
passes it straight to `useConsent`, which registers `consentLinkHelper.consentLinker` on mount. With it set and
|
|
64
|
+
more than just the current host to disclose, a domain line renders right after the description
|
|
65
|
+
(`[data-consent-domains]`, added to the dialog's `aria-describedby`) — computed DIRECTLY from
|
|
66
|
+
`linker.domains` plus the current host, not through the plugin registry, so it never depends on
|
|
67
|
+
registration having finished yet. Omit `linker` and nothing here changes at all. The same object
|
|
68
|
+
carries `language` (`{}` to send the page's language, `{ supported }` to adopt one — the `consent`
|
|
69
|
+
skill's language section): the dialog only forwards it to the store and discloses nothing extra.
|
|
70
|
+
- **The interface language is not a category.** The default set renders the required row and the
|
|
71
|
+
analytics and marketing toggles only; the language is strictly necessary storage that no toggle
|
|
72
|
+
governs (the `consent` skill). The built-in bundle (8 languages) describes it in the required row.
|
|
73
|
+
- `silent` skips every `dataLayer` and global write. It is for tests and for an app that runs no
|
|
74
|
+
tags at all.
|
|
75
|
+
- The window's switches are **seeded from storage every time it opens** — it is mounted only while
|
|
76
|
+
open — never from the last render: a visitor reopening preferences must see the answer they gave.
|
|
77
|
+
An automatic decision past its age seeds nothing.
|
|
78
|
+
|
|
79
|
+
**When the window was raised by something waiting on it** — `reason === 'login'`, which the sign-in
|
|
80
|
+
precondition in `@owlmeans/client-iam` raises — it says so and relabels the primary action *Accept
|
|
81
|
+
& continue*. The sign-in gate always gets the window, never the bar. An automatic decision is said
|
|
82
|
+
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
|
|
83
|
+
twice. Word that path as an acknowledgement, never as "you must consent to essential cookies": a
|
|
84
|
+
required category is disclosure, not a question.
|
|
85
|
+
|
|
86
|
+
## The bar
|
|
87
|
+
|
|
88
|
+
A tall bar across the bottom of a **dimmed** overlay (`[data-consent-overlay]`,
|
|
89
|
+
`fixed inset-0 bg-black/70`): the page stays in sight and scrolls, but nothing on it can be
|
|
90
|
+
pressed until the visitor answers. The bar (`fixed inset-x-0 bottom-0 max-h-[85vh] border-t
|
|
91
|
+
bg-background`) holds, left to right on a wide screen and stacked on a phone:
|
|
92
|
+
|
|
93
|
+
- the cookie icon and `barTitle`;
|
|
94
|
+
- `barDescription` — what the essential cookies do, the optional categories in force as a list
|
|
95
|
+
after a colon (`{{categories}}`, so no language has to decline a label), that they stay off until
|
|
96
|
+
allowed, and that the choice can be changed or withdrawn at any time; `barDescriptionEssential`
|
|
97
|
+
when nothing is optional;
|
|
98
|
+
- the cross-domain line (`[data-consent-domains]`) when `linker` names more than this host;
|
|
99
|
+
- the links row (`[data-consent-links]`: `policyHref` then `links`, 44px targets, new tab);
|
|
100
|
+
- three answers: `[data-consent-preferences]` "Cookie preferences" (outlined), then
|
|
101
|
+
`[data-consent-mandatory]` "Accept only mandatory" and `[data-consent-accept-all]` "Accept All" —
|
|
102
|
+
the two answers carry the SAME accent class: refusing is exactly as prominent as accepting, and
|
|
103
|
+
one click away. With no optional category there is nothing to refuse, so "mandatory" is absent.
|
|
104
|
+
|
|
105
|
+
`barClassName` styles the bar; `className` still styles the window's card.
|
|
106
|
+
|
|
107
|
+
**Focus.** On open, focus moves to the surface itself (`tabIndex={-1}`) — never to an answer, which
|
|
108
|
+
would be a nudge; Tab and Shift+Tab cycle inside it; on close focus returns where it was. Nothing
|
|
109
|
+
pulls focus back on `focusin` (a script focusing a field is not a keyboard user escaping). The
|
|
110
|
+
spinner overlay takes no focus.
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: look
|
|
3
|
+
description: The flat look from the host's tokens — the class table, focus outlines, switches named by reference, and the axe and harness checks that pin them.
|
|
4
|
+
metadata:
|
|
5
|
+
purposes: consent, theme
|
|
6
|
+
mentions: barClassName, ConsentToggle, data-consent-overlay, a11y.spec, outline-ring
|
|
7
|
+
---
|
|
8
|
+
<!-- AUTO-GENERATED — do not edit. Regenerate via sync-agent-meta. -->
|
|
9
|
+
|
|
10
|
+
# @owlmeans/web-consent — The look
|
|
11
|
+
|
|
12
|
+
## The look — flat, from the host's tokens
|
|
13
|
+
|
|
14
|
+
The bar and the window are the first thing every new visitor of a generated app sees, so they
|
|
15
|
+
follow the flat rule those apps are held to: **no gradient, shadow, glow, glass or
|
|
16
|
+
`backdrop-filter`** anywhere. Everything is a theme token, so light and dark follow the host:
|
|
17
|
+
|
|
18
|
+
| Part | Classes |
|
|
19
|
+
|---|---|
|
|
20
|
+
| Bar overlay | `bg-black/70`, matching the preferences window — the page is dimmed beneath the opaque bar |
|
|
21
|
+
| Locating overlay | `bg-transparent` — a quick region lookup flashes no backdrop |
|
|
22
|
+
| Bar | `border-t border-border bg-background text-foreground`, no shadow |
|
|
23
|
+
| Bar answers | "Accept only mandatory" and "Accept All" both the accent pill; "Cookie preferences" outlined |
|
|
24
|
+
| Spinner | `text-muted-foreground motion-safe:animate-spin` |
|
|
25
|
+
| Window overlay | `bg-black/70`, flat — no blur |
|
|
26
|
+
| Card | `rounded-3xl border border-border bg-background text-foreground`, no shadow or ring |
|
|
27
|
+
| Icon | `rounded-full bg-muted text-primary` |
|
|
28
|
+
| Category row | `rounded-2xl border border-border`; *Required* is plain uppercase muted text, never a badge |
|
|
29
|
+
| Switch | `bg-primary` on / `bg-muted-foreground/40` off, white knob; 44px hit area around the 24×44 track |
|
|
30
|
+
| Accept | the one accent pill — `rounded-full bg-primary text-primary-foreground min-h-11` |
|
|
31
|
+
| Save, policy *Manage preferences* | outlined pill — `rounded-full border-[1.5px] border-foreground bg-transparent`, `hover:bg-muted` |
|
|
32
|
+
| Links | muted, underlined at rest, `min-h-11` |
|
|
33
|
+
|
|
34
|
+
Every control shows a 3px `outline-ring` focus outline 3px off the element (`outline-3
|
|
35
|
+
outline-offset-3`; the switch draws it on its track through `peer-focus-visible`). An outline, not
|
|
36
|
+
a ring, so a host that zeroes shadows keeps it. Never reintroduce a decorative class here — the
|
|
37
|
+
package tests fail on `bg-gradient`, `backdrop-`, `blur`, `shadow` or `ring-primary` in the dialog.
|
|
38
|
+
|
|
39
|
+
**Each switch is named by reference.** The `<label>` around a switch's checkbox holds only the
|
|
40
|
+
drawn track, and the category's words sit in a sibling column it cannot reach, so the checkbox
|
|
41
|
+
carries `aria-labelledby="<id>-label"` (the category label) and `aria-describedby` pointing at
|
|
42
|
+
`<id>-required` and `<id>-desc`. Drop those and axe reports three unnamed checkboxes (`label`,
|
|
43
|
+
critical). A required category stays `disabled` and keeps its name.
|
|
44
|
+
|
|
45
|
+
`tests/a11y.spec.ts` runs axe-core (a dev dependency) against the open bar and the open window in
|
|
46
|
+
both schemes, the locating overlay, the re-open button, and the policy page with services and the
|
|
47
|
+
regional rule, and fails on any serious or critical violation. It loads the harness with `?styled=1` (plus `&theme=dark`), which compiles the package
|
|
48
|
+
sources with a generated app's neutral tokens — contrast cannot be judged on the unstyled harness
|
|
49
|
+
the other specs need. The harness also takes `?mode=bar|window`, `?categories=essential`,
|
|
50
|
+
`?geo=<CC>|fail|html|hang` (its server answers `/cdn-cgi/trace` from the page's own query, so every
|
|
51
|
+
case is still chosen by URL; `&geoDelay=<ms>`, `&geoTimeout=<ms>`), `?geoPlugin=<CC>|fail` and
|
|
52
|
+
`?gpc=1` — `tests/bar.spec.ts` and `tests/geo.spec.ts` drive them.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: menu
|
|
3
|
+
description: ConsentMenuWidget, a plain row a host's own menu renders to reopen the dialog, and where its presence signal must come from.
|
|
4
|
+
metadata:
|
|
5
|
+
purposes: consent, navigation
|
|
6
|
+
mentions: ConsentMenuWidget, useConsentMenuPresence, consentStore.open
|
|
7
|
+
---
|
|
8
|
+
<!-- AUTO-GENERATED — do not edit. Regenerate via sync-agent-meta. -->
|
|
9
|
+
|
|
10
|
+
# @owlmeans/web-consent — A menu row
|
|
11
|
+
|
|
12
|
+
## A menu row for a host that already has one
|
|
13
|
+
|
|
14
|
+
`ConsentMenuWidget` renders one row — icon, translated label, `onClick` reopening the dialog
|
|
15
|
+
(`consentStore.open('reopen')` by default, or a caller's own `onSelect`) — for a host whose own
|
|
16
|
+
navigation already carries a settings menu and would rather offer cookie preferences there than as
|
|
17
|
+
a second floating button. It calls `consentStore.open` directly, never `useConsent()`, for the same
|
|
18
|
+
init-on-mount reason as everywhere else in this skill (*Reading the decision* in [SKILL.md](../SKILL.md)).
|
|
19
|
+
|
|
20
|
+
**It carries no presence signalling of its own.** A host that wants `CookieConsent`'s floating
|
|
21
|
+
button to hide while this row is reachable needs to know that itself, from something that stays
|
|
22
|
+
mounted for as long as the row is reachable — never from the row's own mount. A row placed inside a
|
|
23
|
+
host menu's lazily-rendered content (Radix's `DropdownMenuContent`, and most headless menu
|
|
24
|
+
primitives, only mount their children while the menu is actually OPEN) reports "present" for a
|
|
25
|
+
fraction of the time the menu itself is on screen, and the floating button reappears the instant
|
|
26
|
+
the menu closes. `@owlmeans/web-panel/consent`'s `useConsentMenuPresence()` is the fix for a
|
|
27
|
+
context-aware host: call it from the menu's own always-mounted shell component, not from inside the
|
|
28
|
+
row. A context-free host (an Astro island with its own menu) owns that judgement itself.
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: policy-page
|
|
3
|
+
description: The generated CookiePolicy page — what it states, the regional rule, the services disclosure, the linker domain list and its heading structure.
|
|
4
|
+
metadata:
|
|
5
|
+
purposes: consent, screen
|
|
6
|
+
mentions: CookiePolicy, CookiePolicyProps, ConsentService, googleTagServices, data-cookie-policy
|
|
7
|
+
---
|
|
8
|
+
<!-- AUTO-GENERATED — do not edit. Regenerate via sync-agent-meta. -->
|
|
9
|
+
|
|
10
|
+
# @owlmeans/web-consent — The policy page
|
|
11
|
+
|
|
12
|
+
## The policy page
|
|
13
|
+
|
|
14
|
+
`CookiePolicy` states only what the widget provably does — the categories in force, the storage key,
|
|
15
|
+
the dual storage, the retention — all read from the same configuration the dialog renders. That is
|
|
16
|
+
why it is generated rather than written: a hand-written policy drifts the first time a category
|
|
17
|
+
changes, and nobody notices because nobody reads it until it matters.
|
|
18
|
+
|
|
19
|
+
```tsx
|
|
20
|
+
import { googleTagHelper } from '@owlmeans/web-gtm'
|
|
21
|
+
|
|
22
|
+
<CookiePolicy
|
|
23
|
+
operator="Example Sp. z o.o." privacyHref="/legal/privacy" termsHref="/legal/terms"
|
|
24
|
+
services={googleTag !== '' ? googleTagHelper.googleTagServices(googleTag) : undefined}
|
|
25
|
+
/>
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Everything OwlMeans cannot assert on the operator's behalf — who the controller is, the lawful
|
|
29
|
+
basis, how to exercise rights — is deferred to those two links. It also renders a *Manage
|
|
30
|
+
preferences* control, so the policy page is a way back into the decision.
|
|
31
|
+
|
|
32
|
+
**`geo`**, the same object the dialog was given, adds the regional rule
|
|
33
|
+
(`[data-cookie-policy-regional]`, `policyRegional`) after the category list.
|
|
34
|
+
|
|
35
|
+
**`services`** (`ConsentService[]`, see the `consent` skill) discloses WHO receives data: each
|
|
36
|
+
service is listed inside the item of the category whose `key` it names — name, provider, purpose,
|
|
37
|
+
cookie names in `<code>`, and a link to the provider's privacy policy (new tab). A service whose
|
|
38
|
+
category is not among `categories` goes into a trailing *Other services* item rather than being
|
|
39
|
+
dropped. Without `services` the page renders exactly as it always has.
|
|
40
|
+
|
|
41
|
+
The structure stays one `h1` and lists: categories are `li`s, a category's services are a nested
|
|
42
|
+
`ul` named by `aria-label` (*Services — ‹category label›*), each service a `li` holding a `dl`.
|
|
43
|
+
Never add headings for categories or services — they would skip a level under the `h1`. Test and
|
|
44
|
+
CSS hooks: `[data-cookie-policy-category="<key>"]`, `[data-cookie-policy-services]`,
|
|
45
|
+
`[data-cookie-policy-service]`, `[data-cookie-policy-other]`.
|
|
46
|
+
|
|
47
|
+
**`linker`**, same shape as `CookieConsent`'s, adds a plain `[data-cookie-policy-domains]` LIST
|
|
48
|
+
(never a heading) right after the storage-key paragraph — the current host plus every domain
|
|
49
|
+
`linker.domains` names, computed the same direct way the dialog computes it. Omitted or with
|
|
50
|
+
nothing beyond the current host to disclose, nothing here renders.
|
|
51
|
+
|
|
52
|
+
The service labels (`policyProvider`, `policyPurpose`, `policyCookies`, `policyServicesOf`,
|
|
53
|
+
`policyServicePrivacy`, `policyOtherServices`, `policyOtherServicesDesc`) go through `translate`
|
|
54
|
+
with English defaults and are in the packaged bundle for every locale; the service's own `name`,
|
|
55
|
+
`provider` and `purpose` are data and render as given.
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: setup
|
|
3
|
+
description: The Tailwind @source line pointing at src, and the packaged i18n bundle with the translate fall-through.
|
|
4
|
+
metadata:
|
|
5
|
+
purposes: consent, config
|
|
6
|
+
mentions: defaultConsentTranslate, consentI18nHelper, CONSENT_LOCALES, web-consent/src, @source
|
|
7
|
+
---
|
|
8
|
+
<!-- AUTO-GENERATED — do not edit. Regenerate via sync-agent-meta. -->
|
|
9
|
+
|
|
10
|
+
# @owlmeans/web-consent — Tailwind and i18n setup
|
|
11
|
+
|
|
12
|
+
## Tailwind — one `@source` line, pointing at `src`
|
|
13
|
+
|
|
14
|
+
The components emit Tailwind classes that exist nowhere in the consumer's own sources, and
|
|
15
|
+
Tailwind's scanner reads the CSS root plus `@source` directives only, excluding `node_modules`. Add
|
|
16
|
+
the line to the app's Tailwind entry:
|
|
17
|
+
|
|
18
|
+
```css
|
|
19
|
+
@import "tailwindcss";
|
|
20
|
+
|
|
21
|
+
@source "<relative path to node_modules>/@owlmeans/web-consent/src";
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
**Point it at `src`, never at `build`.** The scanner applies the `.gitignore` of whatever repository
|
|
25
|
+
a path resolves into, and under a linked workspace the `node_modules` entry is a symlink into a
|
|
26
|
+
monorepo where every package's build output is ignored. A `build` source there scans **zero files
|
|
27
|
+
and reports nothing**: the build succeeds, the CSS is emitted, and the dialog renders *half*-styled
|
|
28
|
+
— the utilities the app happens to use elsewhere still exist while the ones only this package asks
|
|
29
|
+
for (`max-w-lg`, `bg-black/70`, `z-[999998]`) do not. What reaches the screen is a full-width,
|
|
30
|
+
backdrop-less dialog with the page bleeding through, and nothing in the app's sources looks wrong.
|
|
31
|
+
`src` is tracked and ships in the published tarball, so one path serves both a linked checkout and
|
|
32
|
+
an npm install.
|
|
33
|
+
|
|
34
|
+
The failure is silent, so verify rather than assume — grep the emitted stylesheet for a class only
|
|
35
|
+
this package uses:
|
|
36
|
+
|
|
37
|
+
```sh
|
|
38
|
+
grep -c 'max-w-lg' dist/assets/*.css
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
A `*/` inside a CSS comment ends it early, so a comment above an `@source` that spells out a glob
|
|
42
|
+
closes the comment at that star-slash and turns the next `@source` into an invalid declaration.
|
|
43
|
+
Describe a glob in words, or keep it out of the comment.
|
|
44
|
+
|
|
45
|
+
## i18n
|
|
46
|
+
|
|
47
|
+
Given no `translate` prop, the components resolve every string through the packaged bundle for
|
|
48
|
+
`locale` — every language in `CONSENT_LOCALES` (the framework's seven plus French). The bundle
|
|
49
|
+
lives in `@owlmeans/consent` (`src/i18n/<locale>.json`); a new key goes into every one of them,
|
|
50
|
+
and that package's tests fail on a missing key or a dropped `{{placeholder}}` in any locale.
|
|
51
|
+
This package's `tests/bundle.spec.ts` reads every `t('…')` literal from the components, so a new
|
|
52
|
+
string without an English bundle entry fails there. A JSON-only edit is not re-emitted by an
|
|
53
|
+
incremental `tsc -b` — rebuild `@owlmeans/consent` with `tsc -b --force`, or consumers keep the old
|
|
54
|
+
bundle from `build/i18n`. **Once a `translate` prop is given,
|
|
55
|
+
the packaged bundle is not consulted at all**, so a wrapper that forwards a framework resolver alone
|
|
56
|
+
renders the English default for every key the application has not overridden, in every language. The
|
|
57
|
+
resolver must fall through to `consentI18nHelper.defaultConsentTranslate(locale)` for the default — which is exactly
|
|
58
|
+
what `@owlmeans/web-panel/consent` does, and why an app inside the panel family mounts
|
|
59
|
+
`PanelCookieConsent` / `PanelCookiePolicy` rather than these components directly.
|
|
60
|
+
|
|
61
|
+
`ConsentToggle` is exported for a host that builds its own dialog body; the wording it shows is the
|
|
62
|
+
caller's, already resolved.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@owlmeans/web-consent",
|
|
3
|
-
"version": "0.1.18-rc.
|
|
3
|
+
"version": "0.1.18-rc.48",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|
|
35
35
|
"@owlmeans/dep-config": "workspace:*",
|
|
36
|
-
"@owlmeans/test-ui": "^0.1.18-rc.
|
|
36
|
+
"@owlmeans/test-ui": "^0.1.18-rc.55",
|
|
37
37
|
"@tailwindcss/vite": "^4.3.3",
|
|
38
38
|
"@types/bun": "^1.4.2",
|
|
39
39
|
"@types/react": "^19.3.0",
|