@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 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,CA2HhD,CAAA"}
1
+ {"version":3,"file":"component.d.ts","sourceRoot":"","sources":["../../src/consent/component.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,OAAO,CAAA;AAS/B,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAA;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-5 left-5 z-[999997] flex h-12 w-12 items-center justify-center rounded-full border border-border bg-popover text-primary shadow-lg transition-transform hover:scale-110 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", children: _jsx(Cookie, { className: "h-5 w-5", "aria-hidden": "true" }) })] });
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';
@@ -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';
@@ -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;
@@ -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.2",
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.2"
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-5 left-5 z-[999997] flex h-12 w-12 items-center justify-center rounded-full border border-border bg-popover text-primary shadow-lg transition-transform hover:scale-110 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
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
@@ -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
 
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
+ })
@@ -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
@@ -1,3 +0,0 @@
1
- import { type ClassValue } from 'clsx';
2
- export declare function cn(...inputs: ClassValue[]): string;
3
- //# sourceMappingURL=utils.d.ts.map
@@ -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"}
@@ -1,6 +0,0 @@
1
- import { clsx } from 'clsx';
2
- import { twMerge } from 'tailwind-merge';
3
- export function cn(...inputs) {
4
- return twMerge(clsx(inputs));
5
- }
6
- //# sourceMappingURL=utils.js.map
@@ -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"}