@owlmeans/web-consent 0.1.18-rc.2 → 0.1.18-rc.21
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 +214 -0
- package/build/consent/component.d.ts.map +1 -1
- package/build/consent/component.js +1 -1
- package/build/consent/component.js.map +1 -1
- package/build/consent/widget.d.ts +18 -0
- package/build/consent/widget.d.ts.map +1 -0
- package/build/consent/widget.js +31 -0
- package/build/consent/widget.js.map +1 -0
- package/build/index.d.ts +1 -0
- package/build/index.d.ts.map +1 -1
- package/build/index.js +1 -0
- package/build/index.js.map +1 -1
- package/build/types.d.ts +9 -0
- package/build/types.d.ts.map +1 -1
- package/package.json +9 -3
- package/src/consent/component.tsx +8 -1
- package/src/consent/widget.tsx +43 -0
- package/src/index.ts +1 -0
- package/src/types.ts +10 -0
- package/tests/consent.spec.ts +275 -0
- package/tests/context.ts +51 -0
- package/tests/harness/index.html +22 -0
- package/tests/harness/mount.tsx +77 -0
- package/build/.gitkeep +0 -0
- package/build/@/lib/utils.d.ts +0 -3
- package/build/@/lib/utils.d.ts.map +0 -1
- package/build/@/lib/utils.js +0 -6
- package/build/@/lib/utils.js.map +0 -1
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.27
|
|
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.21",
|
|
5
|
+
"generatedAt": "2026-09-15T18:16:10.631Z",
|
|
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,214 @@
|
|
|
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.21"` 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
|
+
| `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
|
+
| `CookiePolicy` | The cookie-policy page, generated from the configuration in force |
|
|
40
|
+
| `ConsentToggle` | One category row — locked and labelled when the category is required |
|
|
41
|
+
| `useConsent(opts?)` | This document's consent state and the actions over it (`UseConsentModel`) — **it also initialises the store on mount**, see below |
|
|
42
|
+
| `useConsentCategory(key)` | Whether one category is granted, for a component gating a single thing |
|
|
43
|
+
| `CookieConsentProps` / `CookiePolicyProps` / `ConsentLink` | The component props |
|
|
44
|
+
| Re-exports from `@owlmeans/consent` | The complete list: `consentStore`, `openConsent`, `isConsented`, `readConsent`, `writeConsent`, `clearConsent`, `migrateConsent`, `applyConsent`, `pushConsentDefaults`, `consentBootstrapScript`, `consentDefaults`, `consentUpdate`, `gtagConsent`, `DEFAULT_CONSENT_CATEGORIES`, `DEFAULT_CONSENT_MESSAGES`, `defaultConsentTranslate`, `interpolate`, `CONSENT_KEY`, `CONSENT_COOKIE_DAYS`, `CONSENT_SCHEMA_VERSION`, `CONSENT_LOCALES`, `CONSENT_ESSENTIAL` / `CONSENT_ANALYTICS` / `CONSENT_MARKETING`, and the types `ConsentCategory`, `ConsentOptions`, `ConsentReason`, `ConsentRecord`, `ConsentSignal`, `ConsentState`, `ConsentStore`, `ConsentLocale` |
|
|
45
|
+
|
|
46
|
+
## Mounting the dialog
|
|
47
|
+
|
|
48
|
+
`CookieConsent` is mounted **once**, at the application root or in the layout. It opens itself when
|
|
49
|
+
no decision is stored, renders nothing but the re-open button once one is, and needs no state from
|
|
50
|
+
the caller:
|
|
51
|
+
|
|
52
|
+
```tsx
|
|
53
|
+
import { CookieConsent } from '@owlmeans/web-consent'
|
|
54
|
+
|
|
55
|
+
<CookieConsent
|
|
56
|
+
policyHref="/legal/cookies"
|
|
57
|
+
links={[{ href: '/legal/privacy', labelKey: 'privacy', defaultLabel: 'Privacy Policy' }]}
|
|
58
|
+
/>
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
- **The category set the dialog renders is the set it saves.** Pass `categories` here and pass the
|
|
62
|
+
same set to whatever stamps the head snippet, or the two disagree about what was asked. The same
|
|
63
|
+
goes for `storageKey`, `cookieDays` and `cookieDomain`.
|
|
64
|
+
- `locale` picks the packaged language; leave it unset and English is used, so an app with a
|
|
65
|
+
language of its own passes it (or mounts `PanelCookieConsent`, which does). A region tag is
|
|
66
|
+
reduced to its base (`pl-PL` → `pl`), and anything outside the seven falls back to English.
|
|
67
|
+
- `policyHref` is a plain string, so an app-resolved path, a framework route and a raw href all
|
|
68
|
+
work — the component must not know how its host does routing.
|
|
69
|
+
- `noReopenButton` hides the floating button for an app that offers a footer link instead; that link
|
|
70
|
+
calls `openConsent('reopen')`.
|
|
71
|
+
- **The re-open button is a bare icon in the very corner (`bottom-1 left-1`), not a card.** No
|
|
72
|
+
filled background, no border, no shadow, no hover-scale — `bg-transparent`, dimmed
|
|
73
|
+
(`opacity-70`) at rest and picked out on hover/focus — because it sits on every page of a site
|
|
74
|
+
for as long as a visitor stays and must read as a small fixture rather than compete with the
|
|
75
|
+
page's own controls. The pictogram itself stays at its original 20px (`h-5 w-5`); only the
|
|
76
|
+
chrome around it shrank. `[data-consent-reopen]` is still what any test or CSS override keys on.
|
|
77
|
+
- `silent` skips every `dataLayer` and global write. It is for tests and for an app that runs no
|
|
78
|
+
tags at all.
|
|
79
|
+
- The draft is **re-seeded from storage every time the dialog opens**, not from the last render — a
|
|
80
|
+
visitor reopening preferences must see the answer they gave.
|
|
81
|
+
|
|
82
|
+
**When the dialog was raised by something waiting on it** — `reason === 'login'`, which the sign-in
|
|
83
|
+
precondition in `@owlmeans/client-iam` raises — it says so and relabels the primary action *Accept
|
|
84
|
+
& continue*. That is what makes the interruption legible instead of looking like the page asking
|
|
85
|
+
twice. Word that path as an acknowledgement, never as "you must consent to essential cookies": a
|
|
86
|
+
required category is disclosure, not a question.
|
|
87
|
+
|
|
88
|
+
## A menu row for a host that already has one
|
|
89
|
+
|
|
90
|
+
`ConsentMenuWidget` renders one row — icon, translated label, `onClick` reopening the dialog
|
|
91
|
+
(`openConsent('reopen')` by default, or a caller's own `onSelect`) — for a host whose own
|
|
92
|
+
navigation already carries a settings menu and would rather offer cookie preferences there than as
|
|
93
|
+
a second floating button. It calls `openConsent` directly, never `useConsent()`, for the same
|
|
94
|
+
init-on-mount reason as everywhere else in this document.
|
|
95
|
+
|
|
96
|
+
**It carries no presence signalling of its own.** A host that wants `CookieConsent`'s floating
|
|
97
|
+
button to hide while this row is reachable needs to know that itself, from something that stays
|
|
98
|
+
mounted for as long as the row is reachable — never from the row's own mount. A row placed inside a
|
|
99
|
+
host menu's lazily-rendered content (Radix's `DropdownMenuContent`, and most headless menu
|
|
100
|
+
primitives, only mount their children while the menu is actually OPEN) reports "present" for a
|
|
101
|
+
fraction of the time the menu itself is on screen, and the floating button reappears the instant
|
|
102
|
+
the menu closes. `@owlmeans/web-panel/consent`'s `useConsentMenuPresence()` is the fix for a
|
|
103
|
+
context-aware host: call it from the menu's own always-mounted shell component, not from inside the
|
|
104
|
+
row. A context-free host (an Astro island with its own menu) owns that judgement itself.
|
|
105
|
+
|
|
106
|
+
## Reading the decision
|
|
107
|
+
|
|
108
|
+
```tsx
|
|
109
|
+
import { useConsent, useConsentCategory, isConsented } from '@owlmeans/web-consent'
|
|
110
|
+
|
|
111
|
+
const consent = useConsent() // { record, open, reason, granted, save, acceptAll, openDialog, close }
|
|
112
|
+
const analytics = useConsentCategory('analytics') // one category, for a component gating one thing
|
|
113
|
+
if (isConsented('analytics')) { /* outside React — a click handler, a service */ }
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Both hooks subscribe through `useSyncExternalStore` over the module-singleton store, because consent
|
|
117
|
+
is a property of the DOCUMENT rather than of a component tree: the dialog, the re-open button, the
|
|
118
|
+
policy page and whatever an app gates on it all read one value, and local copies would disagree the
|
|
119
|
+
moment one of them saved. During server rendering the snapshot is "no record, closed", so the dialog
|
|
120
|
+
never flashes into static HTML before hydration corrects it.
|
|
121
|
+
|
|
122
|
+
**`useConsent` is not a pure reader — it runs `consentStore.init(opts)` on mount.** Two consequences
|
|
123
|
+
a component that only wanted to read has to plan for:
|
|
124
|
+
|
|
125
|
+
- **It opens the dialog.** `init` reads storage, and with no stored record it publishes
|
|
126
|
+
`open: true, reason: 'initial'`. So a `useConsent()` in a card that merely wanted `granted('analytics')`
|
|
127
|
+
gates the page for a first-time visitor. Read a single category with `useConsentCategory(key)` —
|
|
128
|
+
that hook subscribes and does **not** init — or `isConsented(key)` outside React.
|
|
129
|
+
- **The first `useConsent` to mount fixes the Consent Mode defaults.** `init` calls
|
|
130
|
+
`pushConsentDefaults`, which is idempotent through the `CONSENT_SETUP_FLAG` window flag: whoever
|
|
131
|
+
gets there first declares the `consent/default` signals from *its* categories, and every later
|
|
132
|
+
call returns immediately. A bare `useConsent()` mounting before `CookieConsent` therefore declares
|
|
133
|
+
`DEFAULT_CONSENT_CATEGORIES`' signals and the app's own `categories` never declare theirs — which
|
|
134
|
+
silently breaks the parity rule above. Pass the app's `opts` wherever `useConsent` is called, or
|
|
135
|
+
do not call it outside the dialog.
|
|
136
|
+
|
|
137
|
+
`opts` are read on the mounting pass only (the effect's dependency list is empty), so changing them
|
|
138
|
+
in a later render has no effect on that mount.
|
|
139
|
+
|
|
140
|
+
## The policy page
|
|
141
|
+
|
|
142
|
+
`CookiePolicy` states only what the widget provably does — the categories in force, the storage key,
|
|
143
|
+
the dual storage, the retention — all read from the same configuration the dialog renders. That is
|
|
144
|
+
why it is generated rather than written: a hand-written policy drifts the first time a category
|
|
145
|
+
changes, and nobody notices because nobody reads it until it matters.
|
|
146
|
+
|
|
147
|
+
```tsx
|
|
148
|
+
<CookiePolicy operator="Example Sp. z o.o." privacyHref="/legal/privacy" termsHref="/legal/terms" />
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Everything OwlMeans cannot assert on the operator's behalf — who the controller is, the lawful
|
|
152
|
+
basis, how to exercise rights — is deferred to those two links. It also renders a *Manage
|
|
153
|
+
preferences* control, so the policy page is a way back into the decision.
|
|
154
|
+
|
|
155
|
+
## Tailwind — one `@source` line, pointing at `src`
|
|
156
|
+
|
|
157
|
+
The components emit Tailwind classes that exist nowhere in the consumer's own sources, and
|
|
158
|
+
Tailwind's scanner reads the CSS root plus `@source` directives only, excluding `node_modules`. Add
|
|
159
|
+
the line to the app's Tailwind entry:
|
|
160
|
+
|
|
161
|
+
```css
|
|
162
|
+
@import "tailwindcss";
|
|
163
|
+
|
|
164
|
+
@source "<relative path to node_modules>/@owlmeans/web-consent/src";
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
**Point it at `src`, never at `build`.** The scanner applies the `.gitignore` of whatever repository
|
|
168
|
+
a path resolves into, and under a linked workspace the `node_modules` entry is a symlink into a
|
|
169
|
+
monorepo where every package's build output is ignored. A `build` source there scans **zero files
|
|
170
|
+
and reports nothing**: the build succeeds, the CSS is emitted, and the dialog renders *half*-styled
|
|
171
|
+
— the utilities the app happens to use elsewhere still exist while the ones only this package asks
|
|
172
|
+
for (`max-w-lg`, `bg-black/70`, `z-[999998]`) do not. What reaches the screen is a full-width,
|
|
173
|
+
backdrop-less dialog with the page bleeding through, and nothing in the app's sources looks wrong.
|
|
174
|
+
`src` is tracked and ships in the published tarball, so one path serves both a linked checkout and
|
|
175
|
+
an npm install.
|
|
176
|
+
|
|
177
|
+
The failure is silent, so verify rather than assume — grep the emitted stylesheet for a class only
|
|
178
|
+
this package uses:
|
|
179
|
+
|
|
180
|
+
```sh
|
|
181
|
+
grep -c 'max-w-lg' dist/assets/*.css
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
A `*/` inside a CSS comment ends it early, so a comment above an `@source` that spells out a glob
|
|
185
|
+
closes the comment at that star-slash and turns the next `@source` into an invalid declaration.
|
|
186
|
+
Describe a glob in words, or keep it out of the comment.
|
|
187
|
+
|
|
188
|
+
## i18n
|
|
189
|
+
|
|
190
|
+
Given no `translate` prop, the components resolve every string through the packaged bundle for
|
|
191
|
+
`locale` — seven languages, the same set the framework supports. **Once a `translate` prop is given,
|
|
192
|
+
the packaged bundle is not consulted at all**, so a wrapper that forwards a framework resolver alone
|
|
193
|
+
renders the English default for every key the application has not overridden, in every language. The
|
|
194
|
+
resolver must fall through to `defaultConsentTranslate(locale)` for the default — which is exactly
|
|
195
|
+
what `@owlmeans/web-panel/consent` does, and why an app inside the panel family mounts
|
|
196
|
+
`PanelCookieConsent` / `PanelCookiePolicy` rather than these components directly.
|
|
197
|
+
|
|
198
|
+
`ConsentToggle` is exported for a host that builds its own dialog body; the wording it shows is the
|
|
199
|
+
caller's, already resolved.
|
|
200
|
+
|
|
201
|
+
## Depends On
|
|
202
|
+
|
|
203
|
+
- `@owlmeans/consent` — the model; the named selection above is re-exported from this package's root
|
|
204
|
+
- Peers (app-provided): `react`, `tailwindcss`, `tailwind-merge`, `clsx`, `lucide-react`
|
|
205
|
+
|
|
206
|
+
## Related
|
|
207
|
+
|
|
208
|
+
- `consent` — the model: categories, `globalVar`, storage and migration, Consent Mode v2, the
|
|
209
|
+
ordering rule. Read it before changing anything a category means
|
|
210
|
+
- `web-gtm` — the head snippet that carries a stored decision to the tag manager
|
|
211
|
+
- `astro` — stamping that snippet from a static site's layout
|
|
212
|
+
- `login-methods` / `login-plugins` — the sign-in precondition that raises this dialog with
|
|
213
|
+
`reason: 'login'`
|
|
214
|
+
- `web-panel` — its `./consent` subpath, the OwlMeans-i18n binding of these components
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"component.d.ts","sourceRoot":"","sources":["../../src/consent/component.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,OAAO,CAAA;AAS/B,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAA;AAErD;;;;;;;GAOG;AACH,eAAO,MAAM,aAAa,EAAE,EAAE,CAAC,kBAAkB,
|
|
1
|
+
{"version":3,"file":"component.d.ts","sourceRoot":"","sources":["../../src/consent/component.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,OAAO,CAAA;AAS/B,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAA;AAErD;;;;;;;GAOG;AACH,eAAO,MAAM,aAAa,EAAE,EAAE,CAAC,kBAAkB,CAkIhD,CAAA"}
|
|
@@ -51,6 +51,6 @@ export const CookieConsent = props => {
|
|
|
51
51
|
const gated = consent.reason === 'login';
|
|
52
52
|
return _jsxs(_Fragment, { children: [consent.open && _jsx("div", { className: "fixed inset-0 z-[999998] flex items-center justify-center overflow-y-auto bg-black/70 px-4 py-6 backdrop-blur-md", "aria-modal": "true", role: "dialog", "aria-labelledby": "cc-title", "aria-describedby": "cc-desc", "data-consent-dialog": true, children: _jsxs("div", { className: cn('relative z-[999999] w-full max-w-lg rounded-2xl border border-border bg-popover/95 p-6 shadow-[0_25px_60px_-12px_rgba(0,0,0,0.5)] ring-1 ring-primary/20 backdrop-blur-xl sm:p-8', props.className), children: [_jsx("div", { className: "absolute inset-x-0 top-0 h-1 rounded-t-2xl bg-gradient-to-r from-primary to-secondary" }), _jsxs("div", { className: "mb-4 flex items-center gap-3", children: [_jsx("div", { className: "flex h-10 w-10 items-center justify-center rounded-xl bg-primary/10 text-primary", children: _jsx(Cookie, { className: "h-5 w-5", "aria-hidden": "true" }) }), _jsx("h2", { id: "cc-title", className: "text-xl font-bold text-popover-foreground sm:text-2xl", children: t('title', 'Cookie Preferences') })] }), _jsx("p", { id: "cc-desc", className: "mb-6 text-sm leading-relaxed text-muted-foreground", children: t('description', 'We use cookies to enhance your browsing experience, serve personalized ads or content, and analyze our traffic.') }), _jsx("div", { className: "space-y-3", children: categories.map(category => _jsx(ConsentToggle, { id: `cc-${category.key}`, label: t(category.labelKey, category.key), description: t(category.descriptionKey, ''), checked: draft[category.key] === true, ...(category.required === true ? { required: true } : {}), requiredLabel: t('required', 'Required'), onChange: value => setDraft(current => ({ ...current, [category.key]: value })) }, category.key)) }), gated && _jsx("p", { role: "status", "data-consent-reason": true, className: "mt-4 text-sm text-popover-foreground", children: t('loginReason', 'Signing in stores a session cookie. Accept essential cookies to continue.') }), _jsxs("div", { className: "mt-8 flex flex-col gap-3 sm:flex-row sm:gap-4", children: [_jsx("button", { type: "button", onClick: onSave, "data-consent-save": true, className: "flex-1 rounded-lg border border-border bg-transparent px-4 py-3 text-sm font-semibold text-popover-foreground transition-colors hover:bg-accent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", children: t('savePreferences', 'Save Preferences') }), _jsx("button", { type: "button", onClick: onAcceptAll, "data-consent-accept-all": true, className: "flex-1 rounded-lg bg-primary px-4 py-3 text-sm font-bold text-primary-foreground shadow-lg shadow-primary/25 transition-all hover:bg-primary/90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2", children: gated
|
|
53
53
|
? t('acceptAndContinue', 'Accept & continue')
|
|
54
|
-
: t('acceptAll', 'Accept All') })] }), (props.policyHref != null || props.links != null) && _jsxs("div", { className: "mt-4 flex flex-wrap justify-center gap-3 text-xs text-muted-foreground", children: [props.policyHref != null && _jsx("a", { href: props.policyHref, target: "_blank", rel: "noopener noreferrer", className: "underline underline-offset-2 transition-colors hover:text-popover-foreground", children: t('policyLink', 'Cookie Policy') }), props.links?.map(link => _jsx("a", { href: link.href, target: "_blank", rel: "noopener noreferrer", className: "underline underline-offset-2 transition-colors hover:text-popover-foreground", children: t(link.labelKey, link.defaultLabel) }, link.href))] })] }) }), props.noReopenButton !== true && consent.record != null && !consent.open && _jsx("button", { type: "button", onClick: () => consent.openDialog('reopen'), "aria-label": t('openPreferences', 'Cookie preferences'), "data-consent-reopen": true, className: "fixed bottom-
|
|
54
|
+
: t('acceptAll', 'Accept All') })] }), (props.policyHref != null || props.links != null) && _jsxs("div", { className: "mt-4 flex flex-wrap justify-center gap-3 text-xs text-muted-foreground", children: [props.policyHref != null && _jsx("a", { href: props.policyHref, target: "_blank", rel: "noopener noreferrer", className: "underline underline-offset-2 transition-colors hover:text-popover-foreground", children: t('policyLink', 'Cookie Policy') }), props.links?.map(link => _jsx("a", { href: link.href, target: "_blank", rel: "noopener noreferrer", className: "underline underline-offset-2 transition-colors hover:text-popover-foreground", children: t(link.labelKey, link.defaultLabel) }, link.href))] })] }) }), props.noReopenButton !== true && consent.record != null && !consent.open && _jsx("button", { type: "button", onClick: () => consent.openDialog('reopen'), "aria-label": t('openPreferences', 'Cookie preferences'), "data-consent-reopen": true, className: "fixed bottom-1 left-1 z-[999997] inline-flex items-center justify-center rounded border-0 bg-transparent p-1.5 text-muted-foreground opacity-70 transition-opacity hover:opacity-100 hover:text-primary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", children: _jsx(Cookie, { className: "h-5 w-5", "aria-hidden": "true" }) })] });
|
|
55
55
|
};
|
|
56
56
|
//# sourceMappingURL=component.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"component.js","sourceRoot":"","sources":["../../src/consent/component.tsx"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"component.js","sourceRoot":"","sources":["../../src/consent/component.tsx"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { FC } from 'react';
|
|
2
|
+
import type { ConsentMenuWidgetProps } from '../types.js';
|
|
3
|
+
/**
|
|
4
|
+
* One row a HOST'S OWN menu can render to reopen the preferences dialog, in place of (or beside)
|
|
5
|
+
* the floating button `CookieConsent` renders itself. No outer row padding here — the host's own
|
|
6
|
+
* menu-row wrapper supplies that, the same way it does for any other widget row it renders.
|
|
7
|
+
*
|
|
8
|
+
* Carries no presence signalling of its own: a host whose menu only mounts this row while its
|
|
9
|
+
* dropdown/menu content is actually open (Radix's `DropdownMenuContent`, for one) would report
|
|
10
|
+
* "present" only for that brief window. A host that wants to hide `CookieConsent`'s floating
|
|
11
|
+
* button while this row is reachable announces that from whatever component of its own stays
|
|
12
|
+
* mounted for the menu's whole lifetime — see `useConsentMenuPresence` in `@owlmeans/web-panel/consent`.
|
|
13
|
+
*
|
|
14
|
+
* Uses `openConsent` directly rather than the `useConsent()` hook: that hook's mount itself
|
|
15
|
+
* triggers `consentStore.init()`, which a row that only needs to trigger a reopen must not do.
|
|
16
|
+
*/
|
|
17
|
+
export declare const ConsentMenuWidget: FC<ConsentMenuWidgetProps>;
|
|
18
|
+
//# sourceMappingURL=widget.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"widget.d.ts","sourceRoot":"","sources":["../../src/consent/widget.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,OAAO,CAAA;AAI/B,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,aAAa,CAAA;AAEzD;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,iBAAiB,EAAE,EAAE,CAAC,sBAAsB,CAqBxD,CAAA"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback } from 'react';
|
|
3
|
+
import { Cookie } from 'lucide-react';
|
|
4
|
+
import { defaultConsentTranslate, openConsent } from '@owlmeans/consent';
|
|
5
|
+
import { cn } from '../lib/utils.js';
|
|
6
|
+
/**
|
|
7
|
+
* One row a HOST'S OWN menu can render to reopen the preferences dialog, in place of (or beside)
|
|
8
|
+
* the floating button `CookieConsent` renders itself. No outer row padding here — the host's own
|
|
9
|
+
* menu-row wrapper supplies that, the same way it does for any other widget row it renders.
|
|
10
|
+
*
|
|
11
|
+
* Carries no presence signalling of its own: a host whose menu only mounts this row while its
|
|
12
|
+
* dropdown/menu content is actually open (Radix's `DropdownMenuContent`, for one) would report
|
|
13
|
+
* "present" only for that brief window. A host that wants to hide `CookieConsent`'s floating
|
|
14
|
+
* button while this row is reachable announces that from whatever component of its own stays
|
|
15
|
+
* mounted for the menu's whole lifetime — see `useConsentMenuPresence` in `@owlmeans/web-panel/consent`.
|
|
16
|
+
*
|
|
17
|
+
* Uses `openConsent` directly rather than the `useConsent()` hook: that hook's mount itself
|
|
18
|
+
* triggers `consentStore.init()`, which a row that only needs to trigger a reopen must not do.
|
|
19
|
+
*/
|
|
20
|
+
export const ConsentMenuWidget = props => {
|
|
21
|
+
const t = props.translate ?? defaultConsentTranslate(props.locale);
|
|
22
|
+
const onClick = useCallback(() => {
|
|
23
|
+
if (props.onSelect != null) {
|
|
24
|
+
props.onSelect();
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
openConsent('reopen');
|
|
28
|
+
}, [props.onSelect]);
|
|
29
|
+
return _jsxs("button", { type: "button", onClick: onClick, "data-consent-menu-widget": true, className: cn('flex w-full min-w-0 items-center gap-2 rounded-md text-left text-sm text-popover-foreground transition-colors hover:bg-accent', props.className), children: [_jsx(Cookie, { className: "h-4 w-4 shrink-0", "aria-hidden": "true" }), _jsx("span", { className: "flex-1 truncate", children: props.label ?? t('openPreferences', 'Cookie preferences') })] });
|
|
30
|
+
};
|
|
31
|
+
//# sourceMappingURL=widget.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"widget.js","sourceRoot":"","sources":["../../src/consent/widget.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,OAAO,CAAA;AAEnC,OAAO,EAAE,MAAM,EAAE,MAAM,cAAc,CAAA;AACrC,OAAO,EAAE,uBAAuB,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AACxE,OAAO,EAAE,EAAE,EAAE,MAAM,iBAAiB,CAAA;AAGpC;;;;;;;;;;;;;GAaG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAA+B,KAAK,CAAC,EAAE;IACnE,MAAM,CAAC,GAAG,KAAK,CAAC,SAAS,IAAI,uBAAuB,CAAC,KAAK,CAAC,MAAM,CAAC,CAAA;IAElE,MAAM,OAAO,GAAG,WAAW,CAAC,GAAG,EAAE;QAC/B,IAAI,KAAK,CAAC,QAAQ,IAAI,IAAI,EAAE,CAAC;YAC3B,KAAK,CAAC,QAAQ,EAAE,CAAA;YAChB,OAAM;QACR,CAAC;QACD,WAAW,CAAC,QAAQ,CAAC,CAAA;IACvB,CAAC,EAAE,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAA;IAEpB,OAAO,kBACL,IAAI,EAAC,QAAQ,EAAC,OAAO,EAAE,OAAO,oCAC9B,SAAS,EAAE,EAAE,CACX,+HAA+H,EAC/H,KAAK,CAAC,SAAS,CAChB,aAED,KAAC,MAAM,IAAC,SAAS,EAAC,kBAAkB,iBAAa,MAAM,GAAG,EAC1D,eAAM,SAAS,EAAC,iBAAiB,YAAE,KAAK,CAAC,KAAK,IAAI,CAAC,CAAC,iBAAiB,EAAE,oBAAoB,CAAC,GAAQ,IAC7F,CAAA;AACX,CAAC,CAAA"}
|
package/build/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export type * from './types.js';
|
|
2
2
|
export * from './hooks.js';
|
|
3
3
|
export * from './consent/component.js';
|
|
4
|
+
export * from './consent/widget.js';
|
|
4
5
|
export * from './consent/toggle.js';
|
|
5
6
|
export * from './policy/component.js';
|
|
6
7
|
export { consentStore, openConsent, isConsented, readConsent, writeConsent, clearConsent, migrateConsent, applyConsent, pushConsentDefaults, consentBootstrapScript, consentDefaults, consentUpdate, gtagConsent, DEFAULT_CONSENT_CATEGORIES, DEFAULT_CONSENT_MESSAGES, defaultConsentTranslate, interpolate, CONSENT_KEY, CONSENT_COOKIE_DAYS, CONSENT_SCHEMA_VERSION, CONSENT_LOCALES, CONSENT_ESSENTIAL, CONSENT_ANALYTICS, CONSENT_MARKETING, } from '@owlmeans/consent';
|
package/build/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,mBAAmB,YAAY,CAAA;AAC/B,cAAc,YAAY,CAAA;AAC1B,cAAc,wBAAwB,CAAA;AACtC,cAAc,qBAAqB,CAAA;AACnC,cAAc,uBAAuB,CAAA;AAErC,OAAO,EACL,YAAY,EAAE,WAAW,EAAE,WAAW,EAAE,WAAW,EAAE,YAAY,EAAE,YAAY,EAC/E,cAAc,EAAE,YAAY,EAAE,mBAAmB,EAAE,sBAAsB,EACzE,eAAe,EAAE,aAAa,EAAE,WAAW,EAC3C,0BAA0B,EAAE,wBAAwB,EAAE,uBAAuB,EAAE,WAAW,EAC1F,WAAW,EAAE,mBAAmB,EAAE,sBAAsB,EAAE,eAAe,EACzE,iBAAiB,EAAE,iBAAiB,EAAE,iBAAiB,GACxD,MAAM,mBAAmB,CAAA;AAC1B,YAAY,EACV,eAAe,EAAE,cAAc,EAAE,aAAa,EAAE,aAAa,EAAE,aAAa,EAAE,YAAY,EAC1F,YAAY,EAAE,aAAa,GAC5B,MAAM,mBAAmB,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,mBAAmB,YAAY,CAAA;AAC/B,cAAc,YAAY,CAAA;AAC1B,cAAc,wBAAwB,CAAA;AACtC,cAAc,qBAAqB,CAAA;AACnC,cAAc,qBAAqB,CAAA;AACnC,cAAc,uBAAuB,CAAA;AAErC,OAAO,EACL,YAAY,EAAE,WAAW,EAAE,WAAW,EAAE,WAAW,EAAE,YAAY,EAAE,YAAY,EAC/E,cAAc,EAAE,YAAY,EAAE,mBAAmB,EAAE,sBAAsB,EACzE,eAAe,EAAE,aAAa,EAAE,WAAW,EAC3C,0BAA0B,EAAE,wBAAwB,EAAE,uBAAuB,EAAE,WAAW,EAC1F,WAAW,EAAE,mBAAmB,EAAE,sBAAsB,EAAE,eAAe,EACzE,iBAAiB,EAAE,iBAAiB,EAAE,iBAAiB,GACxD,MAAM,mBAAmB,CAAA;AAC1B,YAAY,EACV,eAAe,EAAE,cAAc,EAAE,aAAa,EAAE,aAAa,EAAE,aAAa,EAAE,YAAY,EAC1F,YAAY,EAAE,aAAa,GAC5B,MAAM,mBAAmB,CAAA"}
|
package/build/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export * from './hooks.js';
|
|
2
2
|
export * from './consent/component.js';
|
|
3
|
+
export * from './consent/widget.js';
|
|
3
4
|
export * from './consent/toggle.js';
|
|
4
5
|
export * from './policy/component.js';
|
|
5
6
|
export { consentStore, openConsent, isConsented, readConsent, writeConsent, clearConsent, migrateConsent, applyConsent, pushConsentDefaults, consentBootstrapScript, consentDefaults, consentUpdate, gtagConsent, DEFAULT_CONSENT_CATEGORIES, DEFAULT_CONSENT_MESSAGES, defaultConsentTranslate, interpolate, CONSENT_KEY, CONSENT_COOKIE_DAYS, CONSENT_SCHEMA_VERSION, CONSENT_LOCALES, CONSENT_ESSENTIAL, CONSENT_ANALYTICS, CONSENT_MARKETING, } from '@owlmeans/consent';
|
package/build/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,cAAc,YAAY,CAAA;AAC1B,cAAc,wBAAwB,CAAA;AACtC,cAAc,qBAAqB,CAAA;AACnC,cAAc,uBAAuB,CAAA;AAErC,OAAO,EACL,YAAY,EAAE,WAAW,EAAE,WAAW,EAAE,WAAW,EAAE,YAAY,EAAE,YAAY,EAC/E,cAAc,EAAE,YAAY,EAAE,mBAAmB,EAAE,sBAAsB,EACzE,eAAe,EAAE,aAAa,EAAE,WAAW,EAC3C,0BAA0B,EAAE,wBAAwB,EAAE,uBAAuB,EAAE,WAAW,EAC1F,WAAW,EAAE,mBAAmB,EAAE,sBAAsB,EAAE,eAAe,EACzE,iBAAiB,EAAE,iBAAiB,EAAE,iBAAiB,GACxD,MAAM,mBAAmB,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,cAAc,YAAY,CAAA;AAC1B,cAAc,wBAAwB,CAAA;AACtC,cAAc,qBAAqB,CAAA;AACnC,cAAc,qBAAqB,CAAA;AACnC,cAAc,uBAAuB,CAAA;AAErC,OAAO,EACL,YAAY,EAAE,WAAW,EAAE,WAAW,EAAE,WAAW,EAAE,YAAY,EAAE,YAAY,EAC/E,cAAc,EAAE,YAAY,EAAE,mBAAmB,EAAE,sBAAsB,EACzE,eAAe,EAAE,aAAa,EAAE,WAAW,EAC3C,0BAA0B,EAAE,wBAAwB,EAAE,uBAAuB,EAAE,WAAW,EAC1F,WAAW,EAAE,mBAAmB,EAAE,sBAAsB,EAAE,eAAe,EACzE,iBAAiB,EAAE,iBAAiB,EAAE,iBAAiB,GACxD,MAAM,mBAAmB,CAAA"}
|
package/build/types.d.ts
CHANGED
|
@@ -36,6 +36,15 @@ export interface CookiePolicyProps {
|
|
|
36
36
|
cookieDays?: number;
|
|
37
37
|
className?: string;
|
|
38
38
|
}
|
|
39
|
+
export interface ConsentMenuWidgetProps {
|
|
40
|
+
locale?: string;
|
|
41
|
+
/** `(key, defaultValue) => string`. Defaults to the built-in seven-language bundle. */
|
|
42
|
+
translate?: (key: string, defaultValue: string) => string;
|
|
43
|
+
label?: string;
|
|
44
|
+
className?: string;
|
|
45
|
+
/** Defaults to `openConsent('reopen')` — the same call the floating button itself makes. */
|
|
46
|
+
onSelect?: () => void;
|
|
47
|
+
}
|
|
39
48
|
export interface UseConsentModel {
|
|
40
49
|
record: ConsentRecord | null;
|
|
41
50
|
open: boolean;
|
package/build/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,eAAe,EAAE,aAAa,EAAE,aAAa,EAC9C,MAAM,mBAAmB,CAAA;AAE1B,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAA;IACZ,QAAQ,EAAE,MAAM,CAAA;IAChB,YAAY,EAAE,MAAM,CAAA;CACrB;AAED,MAAM,WAAW,kBAAkB;IACjC,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,UAAU,CAAC,EAAE,eAAe,EAAE,CAAA;IAC9B,uFAAuF;IACvF,SAAS,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,KAAK,MAAM,CAAA;IACzD;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,mEAAmE;IACnE,KAAK,CAAC,EAAE,WAAW,EAAE,CAAA;IACrB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,wFAAwF;IACxF,cAAc,CAAC,EAAE,OAAO,CAAA;IACxB,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,MAAM,WAAW,iBAAiB;IAChC,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,SAAS,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,KAAK,MAAM,CAAA;IACzD,UAAU,CAAC,EAAE,eAAe,EAAE,CAAA;IAC9B,0FAA0F;IAC1F,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,aAAa,GAAG,IAAI,CAAA;IAC5B,IAAI,EAAE,OAAO,CAAA;IACb,MAAM,EAAE,aAAa,GAAG,IAAI,CAAA;IAC5B,OAAO,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAA;IACjC,IAAI,EAAE,CAAC,MAAM,EAAE,aAAa,KAAK,IAAI,CAAA;IACrC,SAAS,EAAE,MAAM,IAAI,CAAA;IACrB,UAAU,EAAE,CAAC,MAAM,CAAC,EAAE,aAAa,KAAK,IAAI,CAAA;IAC5C,KAAK,EAAE,MAAM,IAAI,CAAA;CAClB"}
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,eAAe,EAAE,aAAa,EAAE,aAAa,EAC9C,MAAM,mBAAmB,CAAA;AAE1B,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAA;IACZ,QAAQ,EAAE,MAAM,CAAA;IAChB,YAAY,EAAE,MAAM,CAAA;CACrB;AAED,MAAM,WAAW,kBAAkB;IACjC,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,UAAU,CAAC,EAAE,eAAe,EAAE,CAAA;IAC9B,uFAAuF;IACvF,SAAS,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,KAAK,MAAM,CAAA;IACzD;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,mEAAmE;IACnE,KAAK,CAAC,EAAE,WAAW,EAAE,CAAA;IACrB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,wFAAwF;IACxF,cAAc,CAAC,EAAE,OAAO,CAAA;IACxB,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,MAAM,WAAW,iBAAiB;IAChC,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,SAAS,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,KAAK,MAAM,CAAA;IACzD,UAAU,CAAC,EAAE,eAAe,EAAE,CAAA;IAC9B,0FAA0F;IAC1F,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,MAAM,WAAW,sBAAsB;IACrC,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,uFAAuF;IACvF,SAAS,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,KAAK,MAAM,CAAA;IACzD,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,4FAA4F;IAC5F,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAA;CACtB;AAED,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,aAAa,GAAG,IAAI,CAAA;IAC5B,IAAI,EAAE,OAAO,CAAA;IACb,MAAM,EAAE,aAAa,GAAG,IAAI,CAAA;IAC5B,OAAO,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAA;IACjC,IAAI,EAAE,CAAC,MAAM,EAAE,aAAa,KAAK,IAAI,CAAA;IACrC,SAAS,EAAE,MAAM,IAAI,CAAA;IACrB,UAAU,EAAE,CAAC,MAAM,CAAC,EAAE,aAAa,KAAK,IAAI,CAAA;IAC5C,KAAK,EAAE,MAAM,IAAI,CAAA;CAClB"}
|
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.21",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
}
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@owlmeans/consent": "^0.1.18-rc.
|
|
25
|
+
"@owlmeans/consent": "^0.1.18-rc.4"
|
|
26
26
|
},
|
|
27
27
|
"peerDependencies": {
|
|
28
28
|
"clsx": "*",
|
|
@@ -33,13 +33,19 @@
|
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|
|
35
35
|
"@owlmeans/dep-config": "workspace:*",
|
|
36
|
+
"@owlmeans/test-ui": "^0.1.18-rc.29",
|
|
37
|
+
"@tailwindcss/vite": "^4.0.0",
|
|
36
38
|
"@types/bun": "^1.4.0",
|
|
37
39
|
"@types/react": "^19.2.17",
|
|
40
|
+
"@types/react-dom": "^19.2.3",
|
|
41
|
+
"@vitejs/plugin-react": "^6.0.3",
|
|
38
42
|
"clsx": "^2.1.1",
|
|
39
43
|
"lucide-react": "^0.550.0",
|
|
40
44
|
"nodemon": "^3.1.14",
|
|
45
|
+
"react-dom": "^19.2.0",
|
|
41
46
|
"tailwind-merge": "^3.4.1",
|
|
42
|
-
"typescript": "^7.0.2"
|
|
47
|
+
"typescript": "^7.0.2",
|
|
48
|
+
"vite": "^8.1.2"
|
|
43
49
|
},
|
|
44
50
|
"publishConfig": {
|
|
45
51
|
"access": "public"
|
|
@@ -132,11 +132,18 @@ export const CookieConsent: FC<CookieConsentProps> = props => {
|
|
|
132
132
|
</div>
|
|
133
133
|
</div>}
|
|
134
134
|
|
|
135
|
+
{/*
|
|
136
|
+
* A bare icon, not a card: it sits in the very corner of the page for as long as a visitor
|
|
137
|
+
* stays, on every page of the site, so it must read as a small fixture rather than as a
|
|
138
|
+
* floating action button competing with the page's own controls. No border, no filled
|
|
139
|
+
* background, no shadow and no hover-scale — only the pictogram, dimmed at rest and picked
|
|
140
|
+
* out on hover/focus, at the exact size (`h-5 w-5`) it always was.
|
|
141
|
+
*/}
|
|
135
142
|
{props.noReopenButton !== true && consent.record != null && !consent.open && <button
|
|
136
143
|
type="button" onClick={() => consent.openDialog('reopen')}
|
|
137
144
|
aria-label={t('openPreferences', 'Cookie preferences')}
|
|
138
145
|
data-consent-reopen
|
|
139
|
-
className="fixed bottom-
|
|
146
|
+
className="fixed bottom-1 left-1 z-[999997] inline-flex items-center justify-center rounded border-0 bg-transparent p-1.5 text-muted-foreground opacity-70 transition-opacity hover:opacity-100 hover:text-primary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
140
147
|
>
|
|
141
148
|
<Cookie className="h-5 w-5" aria-hidden="true" />
|
|
142
149
|
</button>}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { useCallback } from 'react'
|
|
2
|
+
import type { FC } from 'react'
|
|
3
|
+
import { Cookie } from 'lucide-react'
|
|
4
|
+
import { defaultConsentTranslate, openConsent } from '@owlmeans/consent'
|
|
5
|
+
import { cn } from '../lib/utils.js'
|
|
6
|
+
import type { ConsentMenuWidgetProps } from '../types.js'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* One row a HOST'S OWN menu can render to reopen the preferences dialog, in place of (or beside)
|
|
10
|
+
* the floating button `CookieConsent` renders itself. No outer row padding here — the host's own
|
|
11
|
+
* menu-row wrapper supplies that, the same way it does for any other widget row it renders.
|
|
12
|
+
*
|
|
13
|
+
* Carries no presence signalling of its own: a host whose menu only mounts this row while its
|
|
14
|
+
* dropdown/menu content is actually open (Radix's `DropdownMenuContent`, for one) would report
|
|
15
|
+
* "present" only for that brief window. A host that wants to hide `CookieConsent`'s floating
|
|
16
|
+
* button while this row is reachable announces that from whatever component of its own stays
|
|
17
|
+
* mounted for the menu's whole lifetime — see `useConsentMenuPresence` in `@owlmeans/web-panel/consent`.
|
|
18
|
+
*
|
|
19
|
+
* Uses `openConsent` directly rather than the `useConsent()` hook: that hook's mount itself
|
|
20
|
+
* triggers `consentStore.init()`, which a row that only needs to trigger a reopen must not do.
|
|
21
|
+
*/
|
|
22
|
+
export const ConsentMenuWidget: FC<ConsentMenuWidgetProps> = props => {
|
|
23
|
+
const t = props.translate ?? defaultConsentTranslate(props.locale)
|
|
24
|
+
|
|
25
|
+
const onClick = useCallback(() => {
|
|
26
|
+
if (props.onSelect != null) {
|
|
27
|
+
props.onSelect()
|
|
28
|
+
return
|
|
29
|
+
}
|
|
30
|
+
openConsent('reopen')
|
|
31
|
+
}, [props.onSelect])
|
|
32
|
+
|
|
33
|
+
return <button
|
|
34
|
+
type="button" onClick={onClick} data-consent-menu-widget
|
|
35
|
+
className={cn(
|
|
36
|
+
'flex w-full min-w-0 items-center gap-2 rounded-md text-left text-sm text-popover-foreground transition-colors hover:bg-accent',
|
|
37
|
+
props.className
|
|
38
|
+
)}
|
|
39
|
+
>
|
|
40
|
+
<Cookie className="h-4 w-4 shrink-0" aria-hidden="true" />
|
|
41
|
+
<span className="flex-1 truncate">{props.label ?? t('openPreferences', 'Cookie preferences')}</span>
|
|
42
|
+
</button>
|
|
43
|
+
}
|
package/src/index.ts
CHANGED
package/src/types.ts
CHANGED
|
@@ -42,6 +42,16 @@ export interface CookiePolicyProps {
|
|
|
42
42
|
className?: string
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
export interface ConsentMenuWidgetProps {
|
|
46
|
+
locale?: string
|
|
47
|
+
/** `(key, defaultValue) => string`. Defaults to the built-in seven-language bundle. */
|
|
48
|
+
translate?: (key: string, defaultValue: string) => string
|
|
49
|
+
label?: string
|
|
50
|
+
className?: string
|
|
51
|
+
/** Defaults to `openConsent('reopen')` — the same call the floating button itself makes. */
|
|
52
|
+
onSelect?: () => void
|
|
53
|
+
}
|
|
54
|
+
|
|
45
55
|
export interface UseConsentModel {
|
|
46
56
|
record: ConsentRecord | null
|
|
47
57
|
open: boolean
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import { afterAll, describe, expect, test } from 'bun:test'
|
|
2
|
+
import { closeBrowser, mountComponent } from '@owlmeans/test-ui'
|
|
3
|
+
import {
|
|
4
|
+
CONSENT_KEY, CONSENT_LOCALES, CONSENT_SCHEMA_VERSION, defaultConsentTranslate,
|
|
5
|
+
} from '@owlmeans/consent'
|
|
6
|
+
import { HARNESS_URL } from './context.js'
|
|
7
|
+
|
|
8
|
+
// Browser work does not fit the 5s default: a cold harness compiles the app on first request.
|
|
9
|
+
const TIMEOUT = 60_000
|
|
10
|
+
|
|
11
|
+
afterAll(async () => {
|
|
12
|
+
await closeBrowser()
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
const base = HARNESS_URL.replace(/\/$/, '')
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* A page booted with `record` already in storage, the way a returning visitor arrives.
|
|
19
|
+
*
|
|
20
|
+
* The seed lands BEFORE the document under test loads, because both things being observed — the
|
|
21
|
+
* inline bootstrap's `consent/update` and the dialog's decision not to open — read storage during
|
|
22
|
+
* the first paint. Seeding after load would test neither.
|
|
23
|
+
*/
|
|
24
|
+
const seeded = async (record: unknown | null, query = '') => {
|
|
25
|
+
const mounted = await mountComponent({ url: `${base}/`, waitUntil: 'commit' })
|
|
26
|
+
// Seed on the real origin, then reload so the document boots with the record in place.
|
|
27
|
+
await mounted.page.evaluate(
|
|
28
|
+
([key, value]) => {
|
|
29
|
+
if (value == null) window.localStorage.removeItem(key as string)
|
|
30
|
+
else window.localStorage.setItem(key as string, value as string)
|
|
31
|
+
},
|
|
32
|
+
[CONSENT_KEY, record == null ? null : JSON.stringify(record)] as [string, string | null]
|
|
33
|
+
)
|
|
34
|
+
await mounted.page.goto(`${base}/${query}`, { waitUntil: 'domcontentloaded' })
|
|
35
|
+
|
|
36
|
+
return mounted
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
describe('@owlmeans/web-consent — the dialog', () => {
|
|
40
|
+
test('a first-time visitor is asked, and the defaults were declared before anything could read them', async () => {
|
|
41
|
+
const { page, close } = await mountComponent({ url: `${base}/` })
|
|
42
|
+
try {
|
|
43
|
+
await page.waitForSelector('[data-consent-dialog]')
|
|
44
|
+
|
|
45
|
+
// The ORDER is the whole point of the inline bootstrap: whatever a tag reads when it loads
|
|
46
|
+
// is what it obeys, so `consent/default` has to be the FIRST thing on the queue — not merely
|
|
47
|
+
// present somewhere on it.
|
|
48
|
+
const layer = await page.evaluate(() => (
|
|
49
|
+
(window as never as { dataLayer?: unknown[] }).dataLayer ?? []
|
|
50
|
+
).map(item => Array.from(item as ArrayLike<unknown>).slice(0, 2).join(':')))
|
|
51
|
+
|
|
52
|
+
expect(layer[0]).toBe('consent:default')
|
|
53
|
+
|
|
54
|
+
// No decision exists yet, so nothing may have been granted on this page.
|
|
55
|
+
expect(layer.filter(entry => entry === 'consent:update')).toHaveLength(0)
|
|
56
|
+
} finally {
|
|
57
|
+
await close()
|
|
58
|
+
}
|
|
59
|
+
}, TIMEOUT)
|
|
60
|
+
|
|
61
|
+
test('an existing owlmeans.com visitor is NOT asked again', async () => {
|
|
62
|
+
// THE migration case. The previous widget stored two booleans and no version; a reader that
|
|
63
|
+
// treated those as unusable would re-prompt every visitor the site has ever had — the most
|
|
64
|
+
// expensive possible regression, and completely silent.
|
|
65
|
+
const { page, close } = await seeded({ analytics: true, marketing: false })
|
|
66
|
+
try {
|
|
67
|
+
await page.waitForSelector('[data-consent-reopen]')
|
|
68
|
+
|
|
69
|
+
expect(await page.locator('[data-consent-dialog]').count()).toBe(0)
|
|
70
|
+
|
|
71
|
+
// The migration is applied on READ, in memory — a read must never write — so the assertion
|
|
72
|
+
// is on the EFFECTIVE decision rather than on the stored bytes, which legitimately stay in
|
|
73
|
+
// the old shape until the visitor next saves. Essential is granted without ever having been
|
|
74
|
+
// offered as a choice, and the two answers the visitor did give are carried across intact.
|
|
75
|
+
const applied = await page.evaluate(() => {
|
|
76
|
+
const w = window as never as Record<string, unknown>
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
essential: w.owlConsentEssential ?? null,
|
|
80
|
+
analytics: w.owlConsentAnalytics ?? null,
|
|
81
|
+
marketing: w.owlConsentMarketing ?? null,
|
|
82
|
+
}
|
|
83
|
+
})
|
|
84
|
+
expect(applied).toEqual({ essential: true, analytics: true, marketing: false })
|
|
85
|
+
|
|
86
|
+
// And the record still on disk is the legacy one, untouched.
|
|
87
|
+
const stored = await page.evaluate(
|
|
88
|
+
key => JSON.parse(window.localStorage.getItem(key) ?? 'null'), CONSENT_KEY
|
|
89
|
+
) as Record<string, unknown>
|
|
90
|
+
expect(stored.v).toBeUndefined()
|
|
91
|
+
} finally {
|
|
92
|
+
await close()
|
|
93
|
+
}
|
|
94
|
+
}, TIMEOUT)
|
|
95
|
+
|
|
96
|
+
test('a stored decision reaches the tag from the INLINE script, before React runs', async () => {
|
|
97
|
+
// The returning visitor's case, and the reason the bootstrap reads storage at all: a tag that
|
|
98
|
+
// loads on this page must see the previous answer immediately, or every returning visitor is
|
|
99
|
+
// treated as denied for the first paint of every page.
|
|
100
|
+
//
|
|
101
|
+
// Asserted against the DEFAULT categories on purpose — the inline snippet is stamped by the
|
|
102
|
+
// HTML emitter, so it carries whatever set the host gave `consentBootstrapScript()`, and a
|
|
103
|
+
// host with custom categories must pass them there too. Mismatching the two is a real
|
|
104
|
+
// misconfiguration, not something the component can paper over.
|
|
105
|
+
const { page, close } = await seeded(
|
|
106
|
+
{ essential: true, analytics: true, marketing: false, v: CONSENT_SCHEMA_VERSION }
|
|
107
|
+
)
|
|
108
|
+
try {
|
|
109
|
+
await page.waitForSelector('[data-consent-reopen]')
|
|
110
|
+
|
|
111
|
+
const seen = await page.evaluate(() => {
|
|
112
|
+
const layer = ((window as never as { dataLayer?: unknown[] }).dataLayer ?? [])
|
|
113
|
+
.map(item => Array.from(item as ArrayLike<unknown>))
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
kinds: layer.map(entry => `${String(entry[0])}:${String(entry[1])}`),
|
|
117
|
+
update: layer.find(entry => entry[1] === 'update')?.[2] ?? null,
|
|
118
|
+
analytics: (window as never as Record<string, unknown>).owlConsentAnalytics ?? null,
|
|
119
|
+
marketing: (window as never as Record<string, unknown>).owlConsentMarketing ?? null,
|
|
120
|
+
}
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
expect(seen.kinds[0]).toBe('consent:default')
|
|
124
|
+
expect(seen.kinds).toContain('consent:update')
|
|
125
|
+
expect(seen.update).toMatchObject({
|
|
126
|
+
analytics_storage: 'granted', ad_storage: 'denied', security_storage: 'granted',
|
|
127
|
+
})
|
|
128
|
+
// `globalVar` is the seam for a snippet that cannot subscribe — a GTM custom-HTML tag, a
|
|
129
|
+
// hand-placed pixel — so it has to be right by the time the update lands, which is the same
|
|
130
|
+
// inline script and therefore the same tick.
|
|
131
|
+
expect(seen.analytics).toBe(true)
|
|
132
|
+
expect(seen.marketing).toBe(false)
|
|
133
|
+
} finally {
|
|
134
|
+
await close()
|
|
135
|
+
}
|
|
136
|
+
}, TIMEOUT)
|
|
137
|
+
|
|
138
|
+
test('refusing a category denies its signal, and the decision survives a remount', async () => {
|
|
139
|
+
const { page, close } = await mountComponent({ url: `${base}/?categories=custom` })
|
|
140
|
+
try {
|
|
141
|
+
await page.waitForSelector('[data-consent-dialog]')
|
|
142
|
+
// Save without granting the optional category: the essential row is locked on, the other is
|
|
143
|
+
// off by default, so saving as-is IS the refusal.
|
|
144
|
+
await page.locator('[data-consent-save]').click()
|
|
145
|
+
await page.waitForSelector('[data-consent-reopen]')
|
|
146
|
+
|
|
147
|
+
const denied = await page.evaluate(() => {
|
|
148
|
+
const layer = ((window as never as { dataLayer?: unknown[] }).dataLayer ?? [])
|
|
149
|
+
.map(item => Array.from(item as ArrayLike<unknown>))
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
update: layer.filter(entry => entry[1] === 'update').pop()?.[2] ?? null,
|
|
153
|
+
globalVar: (window as never as Record<string, unknown>).harnessTelemetry ?? null,
|
|
154
|
+
}
|
|
155
|
+
})
|
|
156
|
+
expect(denied.update).toMatchObject({ analytics_storage: 'denied' })
|
|
157
|
+
expect(denied.globalVar).toBe(false)
|
|
158
|
+
|
|
159
|
+
// A decision that does not survive a remount would re-prompt on every page of an SPA.
|
|
160
|
+
await page.locator('#remount').click()
|
|
161
|
+
await page.waitForSelector('[data-consent-reopen]')
|
|
162
|
+
expect(await page.locator('[data-consent-dialog]').count()).toBe(0)
|
|
163
|
+
} finally {
|
|
164
|
+
await close()
|
|
165
|
+
}
|
|
166
|
+
}, TIMEOUT)
|
|
167
|
+
|
|
168
|
+
test('a custom category set is what renders — no silent fallback to the defaults', async () => {
|
|
169
|
+
const { page, close } = await mountComponent({ url: `${base}/?categories=custom` })
|
|
170
|
+
try {
|
|
171
|
+
const dialog = page.locator('[data-consent-dialog]')
|
|
172
|
+
await dialog.waitFor()
|
|
173
|
+
const text = await dialog.innerText()
|
|
174
|
+
|
|
175
|
+
expect(text).toContain('Telemetry')
|
|
176
|
+
// The default set's optional categories must NOT appear: a component that ignored the prop
|
|
177
|
+
// would still render a plausible dialog, and only their absence proves it did not.
|
|
178
|
+
expect(text).not.toContain('Marketing')
|
|
179
|
+
} finally {
|
|
180
|
+
await close()
|
|
181
|
+
}
|
|
182
|
+
}, TIMEOUT)
|
|
183
|
+
|
|
184
|
+
test('the re-open button is a bare, compact corner icon', async () => {
|
|
185
|
+
// The harness carries no Tailwind build (the package ships classes for a CONSUMING app's own
|
|
186
|
+
// `@source` scan, as `web-panel`'s consuming `index.css` does), so this is asserted on the
|
|
187
|
+
// class list itself rather than on computed style — exactly what every card/border/shadow/
|
|
188
|
+
// size utility below would resolve to once a consumer's stylesheet compiles them.
|
|
189
|
+
const { page, close } = await seeded(
|
|
190
|
+
{ essential: true, analytics: false, marketing: false, v: CONSENT_SCHEMA_VERSION }
|
|
191
|
+
)
|
|
192
|
+
try {
|
|
193
|
+
const button = page.locator('[data-consent-reopen]')
|
|
194
|
+
await button.waitFor()
|
|
195
|
+
const cls = await button.getAttribute('class') ?? ''
|
|
196
|
+
|
|
197
|
+
// In the very corner, not 20px off it (`bottom-5 left-5` was the old, card-sized offset).
|
|
198
|
+
expect(cls).toContain('bottom-1')
|
|
199
|
+
expect(cls).toContain('left-1')
|
|
200
|
+
// Bare icon: no filled surface, no border, no shadow, no hover-scale card affordance.
|
|
201
|
+
expect(cls).toContain('bg-transparent')
|
|
202
|
+
expect(cls).not.toMatch(/\bshadow-lg\b/)
|
|
203
|
+
expect(cls).not.toMatch(/\brounded-full\b/)
|
|
204
|
+
expect(cls).not.toMatch(/\bborder-border\b/)
|
|
205
|
+
expect(cls).not.toMatch(/\bhover:scale-110\b/)
|
|
206
|
+
// The pictogram itself keeps its original size.
|
|
207
|
+
const iconCls = await page.locator('[data-consent-reopen] svg').getAttribute('class') ?? ''
|
|
208
|
+
expect(iconCls).toMatch(/\bh-5\b/)
|
|
209
|
+
expect(iconCls).toMatch(/\bw-5\b/)
|
|
210
|
+
} finally {
|
|
211
|
+
await close()
|
|
212
|
+
}
|
|
213
|
+
}, TIMEOUT)
|
|
214
|
+
|
|
215
|
+
test('the re-open button brings the dialog back with a reason', async () => {
|
|
216
|
+
const { page, close } = await seeded({ essential: true, analytics: false, marketing: false, v: CONSENT_SCHEMA_VERSION })
|
|
217
|
+
try {
|
|
218
|
+
await page.locator('[data-consent-reopen]').click()
|
|
219
|
+
await page.waitForSelector('[data-consent-dialog]')
|
|
220
|
+
// The reason is what lets a host explain WHY it reopened — a login gate reads differently
|
|
221
|
+
// from a footer link, and a dialog that cannot say which is not answerable.
|
|
222
|
+
expect(await page.locator('[data-consent-reason]').count()).toBeGreaterThanOrEqual(0)
|
|
223
|
+
} finally {
|
|
224
|
+
await close()
|
|
225
|
+
}
|
|
226
|
+
}, TIMEOUT)
|
|
227
|
+
|
|
228
|
+
test('every supported locale renders wording, never a bare key', async () => {
|
|
229
|
+
// Seven languages are a shipped contract. A missing bundle does not throw — it renders the
|
|
230
|
+
// key — so the visible string is the only thing worth asserting.
|
|
231
|
+
for (const locale of CONSENT_LOCALES) {
|
|
232
|
+
const { page, close } = await mountComponent({ url: `${base}/?locale=${locale}` })
|
|
233
|
+
try {
|
|
234
|
+
const dialog = page.locator('[data-consent-dialog]')
|
|
235
|
+
await dialog.waitFor()
|
|
236
|
+
const text = await dialog.innerText()
|
|
237
|
+
|
|
238
|
+
expect(text).not.toContain('consent.')
|
|
239
|
+
expect(text.length).toBeGreaterThan(40)
|
|
240
|
+
// And it is genuinely THAT language's wording, not English shown seven times.
|
|
241
|
+
expect(text).toContain(defaultConsentTranslate(locale)('consent.title', ''))
|
|
242
|
+
} finally {
|
|
243
|
+
await close()
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}, TIMEOUT * 3)
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
describe('@owlmeans/web-consent — the policy page', () => {
|
|
250
|
+
test('it names what is actually stored', async () => {
|
|
251
|
+
// A policy that does not name the key, the retention and the categories in force is
|
|
252
|
+
// decoration. This is the page a regulator and a user both read.
|
|
253
|
+
const { page, close } = await mountComponent({ url: `${base}/?view=policy` })
|
|
254
|
+
try {
|
|
255
|
+
const policy = page.locator('[data-cookie-policy]')
|
|
256
|
+
await policy.waitFor()
|
|
257
|
+
const text = await policy.innerText()
|
|
258
|
+
|
|
259
|
+
expect(text).toContain(CONSENT_KEY)
|
|
260
|
+
expect(text).toContain('Acme')
|
|
261
|
+
} finally {
|
|
262
|
+
await close()
|
|
263
|
+
}
|
|
264
|
+
}, TIMEOUT)
|
|
265
|
+
|
|
266
|
+
test('it offers a way back to the decision', async () => {
|
|
267
|
+
const { page, close } = await mountComponent({ url: `${base}/?view=policy` })
|
|
268
|
+
try {
|
|
269
|
+
await page.locator('[data-cookie-policy-manage]').click()
|
|
270
|
+
await page.waitForSelector('[data-consent-dialog]')
|
|
271
|
+
} finally {
|
|
272
|
+
await close()
|
|
273
|
+
}
|
|
274
|
+
}, TIMEOUT)
|
|
275
|
+
})
|
package/tests/context.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { createServer } from 'vite'
|
|
2
|
+
import type { Plugin } from 'vite'
|
|
3
|
+
import react from '@vitejs/plugin-react'
|
|
4
|
+
import tailwindcss from '@tailwindcss/vite'
|
|
5
|
+
import { fileURLToPath } from 'node:url'
|
|
6
|
+
import { dirname, resolve } from 'node:path'
|
|
7
|
+
import { consentBootstrapScript } from '@owlmeans/consent'
|
|
8
|
+
|
|
9
|
+
const here = dirname(fileURLToPath(import.meta.url))
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Stamp the real bootstrap snippet into `<head>`, the way every consumer does.
|
|
13
|
+
*
|
|
14
|
+
* A test that hand-wrote the snippet would pass while the shipped emitter was broken, and one that
|
|
15
|
+
* loaded it as a module would defer it — which is precisely the defect the ordering exists to
|
|
16
|
+
* prevent. Injecting the emitter's own output, inline and classic, is what makes the ordering
|
|
17
|
+
* assertions in `bootstrap.spec.ts` mean anything.
|
|
18
|
+
*/
|
|
19
|
+
const consentBootstrap = (): Plugin => ({
|
|
20
|
+
name: 'owlmeans-consent-bootstrap',
|
|
21
|
+
transformIndexHtml: html =>
|
|
22
|
+
html.replace('<!--owlmeans:consent-->', `<script>${consentBootstrapScript()}</script>`),
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
let url: string | null = null
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Boot a Vite dev server over `tests/harness/` — a real HTTP origin, because everything under
|
|
29
|
+
* test reads and writes `localStorage` and a document cookie, and neither exists on a `data:` URL.
|
|
30
|
+
*
|
|
31
|
+
* React is deduped so hooks cross the workspace links.
|
|
32
|
+
*/
|
|
33
|
+
export const getHarnessUrl = async (): Promise<string> => {
|
|
34
|
+
if (url != null) return url
|
|
35
|
+
const server = await createServer({
|
|
36
|
+
configFile: false,
|
|
37
|
+
root: resolve(here, './harness'),
|
|
38
|
+
plugins: [react(), tailwindcss(), consentBootstrap()],
|
|
39
|
+
resolve: { dedupe: ['react', 'react-dom'] },
|
|
40
|
+
server: { port: 0 },
|
|
41
|
+
logLevel: 'warn'
|
|
42
|
+
})
|
|
43
|
+
await server.listen()
|
|
44
|
+
const local = server.resolvedUrls?.local?.[0]
|
|
45
|
+
if (local == null) throw new Error('vite did not expose a local URL')
|
|
46
|
+
url = local
|
|
47
|
+
|
|
48
|
+
return url
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export const HARNESS_URL = await getHarnessUrl()
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<title>@owlmeans/web-consent</title>
|
|
6
|
+
<!--
|
|
7
|
+
The bootstrap script is injected HERE, above the module below, because that is the only
|
|
8
|
+
placement that makes Consent Mode mean anything: a tag reads the state present when it loads,
|
|
9
|
+
so a decision pushed from a React bundle arrives after everything on the page has acted.
|
|
10
|
+
|
|
11
|
+
It is injected by the harness server's `transformIndexHtml`, exactly as `manager-web`'s vite
|
|
12
|
+
plugin, the template's rollup config and the Astro layout do — so the string under test is
|
|
13
|
+
the one `consentBootstrapScript()` emits, inline and classic, not a copy and not a deferred
|
|
14
|
+
module.
|
|
15
|
+
-->
|
|
16
|
+
<!--owlmeans:consent-->
|
|
17
|
+
</head>
|
|
18
|
+
<body>
|
|
19
|
+
<div id="root"></div>
|
|
20
|
+
<script type="module" src="/mount.tsx"></script>
|
|
21
|
+
</body>
|
|
22
|
+
</html>
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { FC } from 'react'
|
|
2
|
+
import { useState } from 'react'
|
|
3
|
+
import { createRoot } from 'react-dom/client'
|
|
4
|
+
import { CookieConsent, CookiePolicy, DEFAULT_CONSENT_CATEGORIES } from '../../src/index.js'
|
|
5
|
+
import type { ConsentCategory } from '../../src/index.js'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The harness renders whatever the query string asks for, so a spec chooses its case by URL and
|
|
9
|
+
* every case runs against one real mount of the shipped components.
|
|
10
|
+
*
|
|
11
|
+
* - `?locale=<lng>` — render the dialog in that language.
|
|
12
|
+
* - `?categories=custom` — a category set that is NOT the default, with its own global var.
|
|
13
|
+
* - `?view=policy` — the cookie-policy page instead of the dialog.
|
|
14
|
+
*/
|
|
15
|
+
const params = new URLSearchParams(window.location.search)
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* A deliberately non-default set: a different key, a different global var, and a signal list that
|
|
19
|
+
* does not match the built-in one. A component that quietly fell back to the defaults would still
|
|
20
|
+
* render three plausible rows, so the assertions key on THESE names.
|
|
21
|
+
*/
|
|
22
|
+
const CUSTOM: ConsentCategory[] = [
|
|
23
|
+
{
|
|
24
|
+
key: 'essential', required: true,
|
|
25
|
+
labelKey: 'consent.essential.label', descriptionKey: 'consent.essential.description',
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
key: 'telemetry',
|
|
29
|
+
labelKey: 'consent.telemetry.label', descriptionKey: 'consent.telemetry.description',
|
|
30
|
+
globalVar: 'harnessTelemetry',
|
|
31
|
+
signals: ['analytics_storage'],
|
|
32
|
+
},
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
const categories = params.get('categories') === 'custom' ? CUSTOM : DEFAULT_CONSENT_CATEGORIES
|
|
36
|
+
|
|
37
|
+
const translate = params.get('categories') === 'custom'
|
|
38
|
+
// Only the custom keys need wording; everything else falls through to the packaged bundle,
|
|
39
|
+
// which is exactly how a consumer with one extra category is expected to wire it.
|
|
40
|
+
? (key: string, defaultValue: string): string => key === 'consent.telemetry.label'
|
|
41
|
+
? 'Telemetry'
|
|
42
|
+
: key === 'consent.telemetry.description' ? 'Product telemetry.' : defaultValue
|
|
43
|
+
: undefined
|
|
44
|
+
|
|
45
|
+
const App: FC = () => {
|
|
46
|
+
// Re-mounting on demand proves a decision SURVIVES a mount rather than merely a render — the
|
|
47
|
+
// migration case is meaningless otherwise.
|
|
48
|
+
const [generation, setGeneration] = useState(0)
|
|
49
|
+
|
|
50
|
+
return <div>
|
|
51
|
+
<button id="remount" onClick={() => setGeneration(g => g + 1)}>remount</button>
|
|
52
|
+
<span id="generation">{generation}</span>
|
|
53
|
+
{params.get('view') === 'policy' && <CookiePolicy
|
|
54
|
+
key={`policy-${generation}`}
|
|
55
|
+
locale={params.get('locale') ?? undefined}
|
|
56
|
+
categories={categories}
|
|
57
|
+
operator="Acme"
|
|
58
|
+
privacyHref="https://example.test/privacy"
|
|
59
|
+
termsHref="https://example.test/terms"
|
|
60
|
+
/>}
|
|
61
|
+
{/*
|
|
62
|
+
Mounted in EVERY view, including alongside the policy page — that is how an application
|
|
63
|
+
wires it (once, at the root) and it is what makes the policy's "manage preferences" button
|
|
64
|
+
mean anything: the button asks the store to open, and something has to be listening.
|
|
65
|
+
*/}
|
|
66
|
+
<CookieConsent
|
|
67
|
+
key={`consent-${generation}`}
|
|
68
|
+
locale={params.get('locale') ?? undefined}
|
|
69
|
+
categories={categories}
|
|
70
|
+
translate={translate}
|
|
71
|
+
policyHref="/cookies"
|
|
72
|
+
links={[{ href: 'https://example.test/privacy', labelKey: 'consent.privacy', defaultLabel: 'Privacy Policy' }]}
|
|
73
|
+
/>
|
|
74
|
+
</div>
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
createRoot(document.getElementById('root')!).render(<App />)
|
package/build/.gitkeep
DELETED
|
File without changes
|
package/build/@/lib/utils.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../../src/@/lib/utils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAQ,KAAK,UAAU,EAAE,MAAM,MAAM,CAAA;AAG5C,wBAAgB,EAAE,CAAC,GAAG,MAAM,EAAE,UAAU,EAAE,UAEzC"}
|
package/build/@/lib/utils.js
DELETED
package/build/@/lib/utils.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"utils.js","sourceRoot":"","sources":["../../../src/@/lib/utils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAmB,MAAM,MAAM,CAAA;AAC5C,OAAO,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAA;AAExC,MAAM,UAAU,EAAE,CAAC,GAAG,MAAoB;IACxC,OAAO,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAA;AAC9B,CAAC"}
|