@owlmeans/web-consent 0.1.18-rc.5 → 0.1.18-rc.7
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 +15 -0
- package/agent-meta/manifest.json +16 -0
- package/agent-meta/skills/web-consent/SKILL.md +189 -0
- package/package.json +2 -2
package/README.md
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<!-- owlmeans:agent-guidance:start -->
|
|
2
|
+
## Agent guidance
|
|
3
|
+
|
|
4
|
+
This package ships embedded agent skills under `agent-meta/`. After installing your
|
|
5
|
+
`@owlmeans/*` packages, run the OwlMeans agent-skills installer to place them into
|
|
6
|
+
your project's skill store (`.agents/skills/`):
|
|
7
|
+
|
|
8
|
+
```sh
|
|
9
|
+
npx @owlmeans/agent-skills@^0.1.18-rc.12
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
The embedded files are version-matched to this package release. Do not edit them
|
|
13
|
+
directly — they are regenerated on each publish. To contribute guidance edits,
|
|
14
|
+
open a PR against the source monorepo.
|
|
15
|
+
<!-- owlmeans:agent-guidance:end -->
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 2,
|
|
3
|
+
"package": "@owlmeans/web-consent",
|
|
4
|
+
"version": "0.1.18-rc.7",
|
|
5
|
+
"generatedAt": "2026-09-04T22:43:25.446Z",
|
|
6
|
+
"canonicalRepo": "https://github.com/owlmeans/common",
|
|
7
|
+
"entries": [
|
|
8
|
+
{
|
|
9
|
+
"kind": "skill",
|
|
10
|
+
"name": "web-consent",
|
|
11
|
+
"category": "package-specific",
|
|
12
|
+
"file": "skills/web-consent/SKILL.md",
|
|
13
|
+
"canonicalPath": ".agents/skills/web-consent/SKILL.md"
|
|
14
|
+
}
|
|
15
|
+
]
|
|
16
|
+
}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: web-consent
|
|
3
|
+
description: How to use @owlmeans/web-consent — the React cookie-consent dialog, its re-open button, the generated cookie-policy page and the useConsent hooks, plus the Tailwind @source line every consumer must add. Auto-invoked when mounting a consent dialog, rendering a cookie policy, gating a feature on a consent category, or importing CookieConsent, CookiePolicy or useConsent.
|
|
4
|
+
user-invocable: false
|
|
5
|
+
---
|
|
6
|
+
<!-- AUTO-GENERATED — do not edit. Regenerate via sync-agent-meta. -->
|
|
7
|
+
|
|
8
|
+
# @owlmeans/web-consent
|
|
9
|
+
|
|
10
|
+
**Layer:** Web (React)
|
|
11
|
+
**Install:** `"@owlmeans/web-consent": "^0.1.18-rc.7"` in `dependencies`
|
|
12
|
+
|
|
13
|
+
The browser components of the consent set. The model — categories, storage, migration, the store,
|
|
14
|
+
Consent Mode signalling — is `@owlmeans/consent`, and this package re-exports a **named selection**
|
|
15
|
+
of it (listed under Key Exports) so an application usually has one import. Five public
|
|
16
|
+
`@owlmeans/consent` exports are deliberately not in that list — `makeConsentStore`,
|
|
17
|
+
`normalizeLocale`, `CONSENT_SETUP_FLAG`, `CONSENT_SIGNAL_DEFAULTS` and the `ConsentListener` type —
|
|
18
|
+
so a caller that needs one of them imports it from `@owlmeans/consent` directly. **Read the
|
|
19
|
+
`consent` skill for the model; read this one for the components.**
|
|
20
|
+
|
|
21
|
+
## Deliberately not a shadcn package
|
|
22
|
+
|
|
23
|
+
It uses **no shadcn primitive and no `@` alias**, and owns a four-line `cn` of its own. The `@`
|
|
24
|
+
contract exists so a consumer's THEME and primitives win — `cn` has neither in it, and requiring an
|
|
25
|
+
alias for it would mean adopting an OwlMeans UI contract before you may render a consent notice.
|
|
26
|
+
One of the surfaces that needs this dialog most is a static site with its own component library and
|
|
27
|
+
no such alias, where that would simply fail to build.
|
|
28
|
+
|
|
29
|
+
For the same reason it is not part of `@owlmeans/web-panel`: that would drag ~30 packages onto a
|
|
30
|
+
cookie notice. `@owlmeans/web-panel/consent` is the thin binding to OwlMeans i18n on top of these
|
|
31
|
+
components — see below.
|
|
32
|
+
|
|
33
|
+
## Key Exports
|
|
34
|
+
|
|
35
|
+
| Export | Description |
|
|
36
|
+
|--------|-------------|
|
|
37
|
+
| `CookieConsent` | The preferences dialog **and** the persistent re-open button |
|
|
38
|
+
| `CookiePolicy` | The cookie-policy page, generated from the configuration in force |
|
|
39
|
+
| `ConsentToggle` | One category row — locked and labelled when the category is required |
|
|
40
|
+
| `useConsent(opts?)` | This document's consent state and the actions over it (`UseConsentModel`) — **it also initialises the store on mount**, see below |
|
|
41
|
+
| `useConsentCategory(key)` | Whether one category is granted, for a component gating a single thing |
|
|
42
|
+
| `CookieConsentProps` / `CookiePolicyProps` / `ConsentLink` | The component props |
|
|
43
|
+
| Re-exports from `@owlmeans/consent` | The complete list: `consentStore`, `openConsent`, `isConsented`, `readConsent`, `writeConsent`, `clearConsent`, `migrateConsent`, `applyConsent`, `pushConsentDefaults`, `consentBootstrapScript`, `consentDefaults`, `consentUpdate`, `gtagConsent`, `DEFAULT_CONSENT_CATEGORIES`, `DEFAULT_CONSENT_MESSAGES`, `defaultConsentTranslate`, `interpolate`, `CONSENT_KEY`, `CONSENT_COOKIE_DAYS`, `CONSENT_SCHEMA_VERSION`, `CONSENT_LOCALES`, `CONSENT_ESSENTIAL` / `CONSENT_ANALYTICS` / `CONSENT_MARKETING`, and the types `ConsentCategory`, `ConsentOptions`, `ConsentReason`, `ConsentRecord`, `ConsentSignal`, `ConsentState`, `ConsentStore`, `ConsentLocale` |
|
|
44
|
+
|
|
45
|
+
## Mounting the dialog
|
|
46
|
+
|
|
47
|
+
`CookieConsent` is mounted **once**, at the application root or in the layout. It opens itself when
|
|
48
|
+
no decision is stored, renders nothing but the re-open button once one is, and needs no state from
|
|
49
|
+
the caller:
|
|
50
|
+
|
|
51
|
+
```tsx
|
|
52
|
+
import { CookieConsent } from '@owlmeans/web-consent'
|
|
53
|
+
|
|
54
|
+
<CookieConsent
|
|
55
|
+
policyHref="/legal/cookies"
|
|
56
|
+
links={[{ href: '/legal/privacy', labelKey: 'privacy', defaultLabel: 'Privacy Policy' }]}
|
|
57
|
+
/>
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
- **The category set the dialog renders is the set it saves.** Pass `categories` here and pass the
|
|
61
|
+
same set to whatever stamps the head snippet, or the two disagree about what was asked. The same
|
|
62
|
+
goes for `storageKey`, `cookieDays` and `cookieDomain`.
|
|
63
|
+
- `locale` picks the packaged language; leave it unset and English is used, so an app with a
|
|
64
|
+
language of its own passes it (or mounts `PanelCookieConsent`, which does). A region tag is
|
|
65
|
+
reduced to its base (`pl-PL` → `pl`), and anything outside the seven falls back to English.
|
|
66
|
+
- `policyHref` is a plain string, so an app-resolved path, a framework route and a raw href all
|
|
67
|
+
work — the component must not know how its host does routing.
|
|
68
|
+
- `noReopenButton` hides the floating button for an app that offers a footer link instead; that link
|
|
69
|
+
calls `openConsent('reopen')`.
|
|
70
|
+
- `silent` skips every `dataLayer` and global write. It is for tests and for an app that runs no
|
|
71
|
+
tags at all.
|
|
72
|
+
- The draft is **re-seeded from storage every time the dialog opens**, not from the last render — a
|
|
73
|
+
visitor reopening preferences must see the answer they gave.
|
|
74
|
+
|
|
75
|
+
**When the dialog was raised by something waiting on it** — `reason === 'login'`, which the sign-in
|
|
76
|
+
precondition in `@owlmeans/client-iam` raises — it says so and relabels the primary action *Accept
|
|
77
|
+
& continue*. That is what makes the interruption legible instead of looking like the page asking
|
|
78
|
+
twice. Word that path as an acknowledgement, never as "you must consent to essential cookies": a
|
|
79
|
+
required category is disclosure, not a question.
|
|
80
|
+
|
|
81
|
+
## Reading the decision
|
|
82
|
+
|
|
83
|
+
```tsx
|
|
84
|
+
import { useConsent, useConsentCategory, isConsented } from '@owlmeans/web-consent'
|
|
85
|
+
|
|
86
|
+
const consent = useConsent() // { record, open, reason, granted, save, acceptAll, openDialog, close }
|
|
87
|
+
const analytics = useConsentCategory('analytics') // one category, for a component gating one thing
|
|
88
|
+
if (isConsented('analytics')) { /* outside React — a click handler, a service */ }
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Both hooks subscribe through `useSyncExternalStore` over the module-singleton store, because consent
|
|
92
|
+
is a property of the DOCUMENT rather than of a component tree: the dialog, the re-open button, the
|
|
93
|
+
policy page and whatever an app gates on it all read one value, and local copies would disagree the
|
|
94
|
+
moment one of them saved. During server rendering the snapshot is "no record, closed", so the dialog
|
|
95
|
+
never flashes into static HTML before hydration corrects it.
|
|
96
|
+
|
|
97
|
+
**`useConsent` is not a pure reader — it runs `consentStore.init(opts)` on mount.** Two consequences
|
|
98
|
+
a component that only wanted to read has to plan for:
|
|
99
|
+
|
|
100
|
+
- **It opens the dialog.** `init` reads storage, and with no stored record it publishes
|
|
101
|
+
`open: true, reason: 'initial'`. So a `useConsent()` in a card that merely wanted `granted('analytics')`
|
|
102
|
+
gates the page for a first-time visitor. Read a single category with `useConsentCategory(key)` —
|
|
103
|
+
that hook subscribes and does **not** init — or `isConsented(key)` outside React.
|
|
104
|
+
- **The first `useConsent` to mount fixes the Consent Mode defaults.** `init` calls
|
|
105
|
+
`pushConsentDefaults`, which is idempotent through the `CONSENT_SETUP_FLAG` window flag: whoever
|
|
106
|
+
gets there first declares the `consent/default` signals from *its* categories, and every later
|
|
107
|
+
call returns immediately. A bare `useConsent()` mounting before `CookieConsent` therefore declares
|
|
108
|
+
`DEFAULT_CONSENT_CATEGORIES`' signals and the app's own `categories` never declare theirs — which
|
|
109
|
+
silently breaks the parity rule above. Pass the app's `opts` wherever `useConsent` is called, or
|
|
110
|
+
do not call it outside the dialog.
|
|
111
|
+
|
|
112
|
+
`opts` are read on the mounting pass only (the effect's dependency list is empty), so changing them
|
|
113
|
+
in a later render has no effect on that mount.
|
|
114
|
+
|
|
115
|
+
## The policy page
|
|
116
|
+
|
|
117
|
+
`CookiePolicy` states only what the widget provably does — the categories in force, the storage key,
|
|
118
|
+
the dual storage, the retention — all read from the same configuration the dialog renders. That is
|
|
119
|
+
why it is generated rather than written: a hand-written policy drifts the first time a category
|
|
120
|
+
changes, and nobody notices because nobody reads it until it matters.
|
|
121
|
+
|
|
122
|
+
```tsx
|
|
123
|
+
<CookiePolicy operator="Example Sp. z o.o." privacyHref="/legal/privacy" termsHref="/legal/terms" />
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Everything OwlMeans cannot assert on the operator's behalf — who the controller is, the lawful
|
|
127
|
+
basis, how to exercise rights — is deferred to those two links. It also renders a *Manage
|
|
128
|
+
preferences* control, so the policy page is a way back into the decision.
|
|
129
|
+
|
|
130
|
+
## Tailwind — one `@source` line, pointing at `src`
|
|
131
|
+
|
|
132
|
+
The components emit Tailwind classes that exist nowhere in the consumer's own sources, and
|
|
133
|
+
Tailwind's scanner reads the CSS root plus `@source` directives only, excluding `node_modules`. Add
|
|
134
|
+
the line to the app's Tailwind entry:
|
|
135
|
+
|
|
136
|
+
```css
|
|
137
|
+
@import "tailwindcss";
|
|
138
|
+
|
|
139
|
+
@source "<relative path to node_modules>/@owlmeans/web-consent/src";
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
**Point it at `src`, never at `build`.** The scanner applies the `.gitignore` of whatever repository
|
|
143
|
+
a path resolves into, and under a linked workspace the `node_modules` entry is a symlink into a
|
|
144
|
+
monorepo where every package's build output is ignored. A `build` source there scans **zero files
|
|
145
|
+
and reports nothing**: the build succeeds, the CSS is emitted, and the dialog renders *half*-styled
|
|
146
|
+
— the utilities the app happens to use elsewhere still exist while the ones only this package asks
|
|
147
|
+
for (`max-w-lg`, `bg-black/70`, `z-[999998]`) do not. What reaches the screen is a full-width,
|
|
148
|
+
backdrop-less dialog with the page bleeding through, and nothing in the app's sources looks wrong.
|
|
149
|
+
`src` is tracked and ships in the published tarball, so one path serves both a linked checkout and
|
|
150
|
+
an npm install.
|
|
151
|
+
|
|
152
|
+
The failure is silent, so verify rather than assume — grep the emitted stylesheet for a class only
|
|
153
|
+
this package uses:
|
|
154
|
+
|
|
155
|
+
```sh
|
|
156
|
+
grep -c 'max-w-lg' dist/assets/*.css
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
A `*/` inside a CSS comment ends it early, so a comment above an `@source` that spells out a glob
|
|
160
|
+
closes the comment at that star-slash and turns the next `@source` into an invalid declaration.
|
|
161
|
+
Describe a glob in words, or keep it out of the comment.
|
|
162
|
+
|
|
163
|
+
## i18n
|
|
164
|
+
|
|
165
|
+
Given no `translate` prop, the components resolve every string through the packaged bundle for
|
|
166
|
+
`locale` — seven languages, the same set the framework supports. **Once a `translate` prop is given,
|
|
167
|
+
the packaged bundle is not consulted at all**, so a wrapper that forwards a framework resolver alone
|
|
168
|
+
renders the English default for every key the application has not overridden, in every language. The
|
|
169
|
+
resolver must fall through to `defaultConsentTranslate(locale)` for the default — which is exactly
|
|
170
|
+
what `@owlmeans/web-panel/consent` does, and why an app inside the panel family mounts
|
|
171
|
+
`PanelCookieConsent` / `PanelCookiePolicy` rather than these components directly.
|
|
172
|
+
|
|
173
|
+
`ConsentToggle` is exported for a host that builds its own dialog body; the wording it shows is the
|
|
174
|
+
caller's, already resolved.
|
|
175
|
+
|
|
176
|
+
## Depends On
|
|
177
|
+
|
|
178
|
+
- `@owlmeans/consent` — the model; the named selection above is re-exported from this package's root
|
|
179
|
+
- Peers (app-provided): `react`, `tailwindcss`, `tailwind-merge`, `clsx`, `lucide-react`
|
|
180
|
+
|
|
181
|
+
## Related
|
|
182
|
+
|
|
183
|
+
- `consent` — the model: categories, `globalVar`, storage and migration, Consent Mode v2, the
|
|
184
|
+
ordering rule. Read it before changing anything a category means
|
|
185
|
+
- `web-gtm` — the head snippet that carries a stored decision to the tag manager
|
|
186
|
+
- `astro` — stamping that snippet from a static site's layout
|
|
187
|
+
- `login-methods` / `login-plugins` — the sign-in precondition that raises this dialog with
|
|
188
|
+
`reason: 'login'`
|
|
189
|
+
- `web-panel` — its `./consent` subpath, the OwlMeans-i18n binding of these components
|
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.7",
|
|
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.15",
|
|
37
37
|
"@tailwindcss/vite": "^4.0.0",
|
|
38
38
|
"@types/bun": "^1.4.0",
|
|
39
39
|
"@types/react": "^19.2.17",
|