@owlmeans/web-consent 0.1.18-rc.2
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/build/.gitkeep +0 -0
- package/build/@/lib/utils.d.ts +3 -0
- package/build/@/lib/utils.d.ts.map +1 -0
- package/build/@/lib/utils.js +6 -0
- package/build/@/lib/utils.js.map +1 -0
- package/build/consent/component.d.ts +12 -0
- package/build/consent/component.d.ts.map +1 -0
- package/build/consent/component.js +56 -0
- package/build/consent/component.js.map +1 -0
- package/build/consent/toggle.d.ts +13 -0
- package/build/consent/toggle.d.ts.map +1 -0
- package/build/consent/toggle.js +4 -0
- package/build/consent/toggle.js.map +1 -0
- package/build/hooks.d.ts +13 -0
- package/build/hooks.d.ts.map +1 -0
- package/build/hooks.js +32 -0
- package/build/hooks.js.map +1 -0
- package/build/index.d.ts +8 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +6 -0
- package/build/index.js.map +1 -0
- package/build/lib/utils.d.ts +12 -0
- package/build/lib/utils.d.ts.map +1 -0
- package/build/lib/utils.js +15 -0
- package/build/lib/utils.js.map +1 -0
- package/build/policy/component.d.ts +16 -0
- package/build/policy/component.d.ts.map +1 -0
- package/build/policy/component.js +23 -0
- package/build/policy/component.js.map +1 -0
- package/build/types.d.ts +49 -0
- package/build/types.d.ts.map +1 -0
- package/build/types.js +2 -0
- package/build/types.js.map +1 -0
- package/package.json +47 -0
- package/src/consent/component.tsx +144 -0
- package/src/consent/toggle.tsx +47 -0
- package/src/hooks.ts +45 -0
- package/src/index.ts +18 -0
- package/src/lib/utils.ts +15 -0
- package/src/policy/component.tsx +72 -0
- package/src/types.ts +54 -0
- package/tsconfig.json +16 -0
package/build/.gitkeep
ADDED
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
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"}
|
|
@@ -0,0 +1 @@
|
|
|
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"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { FC } from 'react';
|
|
2
|
+
import type { CookieConsentProps } from '../types.js';
|
|
3
|
+
/**
|
|
4
|
+
* The cookie preferences dialog, and the button that brings it back.
|
|
5
|
+
*
|
|
6
|
+
* Deliberately built from raw elements rather than shadcn primitives. One of the three surfaces
|
|
7
|
+
* this serves is an Astro island on a site that vendors its own component library, and requiring a
|
|
8
|
+
* consumer to install a UI family in order to render a consent notice would put the notice out of
|
|
9
|
+
* reach of the site that needs it most.
|
|
10
|
+
*/
|
|
11
|
+
export declare const CookieConsent: FC<CookieConsentProps>;
|
|
12
|
+
//# sourceMappingURL=component.d.ts.map
|
|
@@ -0,0 +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"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback, useEffect, useState } from 'react';
|
|
3
|
+
import { Cookie } from 'lucide-react';
|
|
4
|
+
import { DEFAULT_CONSENT_CATEGORIES, defaultConsentTranslate, readConsent, } from '@owlmeans/consent';
|
|
5
|
+
import { cn } from '../lib/utils.js';
|
|
6
|
+
import { useConsent } from '../hooks.js';
|
|
7
|
+
import { ConsentToggle } from './toggle.js';
|
|
8
|
+
/**
|
|
9
|
+
* The cookie preferences dialog, and the button that brings it back.
|
|
10
|
+
*
|
|
11
|
+
* Deliberately built from raw elements rather than shadcn primitives. One of the three surfaces
|
|
12
|
+
* this serves is an Astro island on a site that vendors its own component library, and requiring a
|
|
13
|
+
* consumer to install a UI family in order to render a consent notice would put the notice out of
|
|
14
|
+
* reach of the site that needs it most.
|
|
15
|
+
*/
|
|
16
|
+
export const CookieConsent = props => {
|
|
17
|
+
const categories = props.categories ?? DEFAULT_CONSENT_CATEGORIES;
|
|
18
|
+
const t = props.translate ?? defaultConsentTranslate(props.locale);
|
|
19
|
+
const consent = useConsent({
|
|
20
|
+
categories,
|
|
21
|
+
...(props.storageKey != null ? { storageKey: props.storageKey } : {}),
|
|
22
|
+
...(props.cookieDays != null ? { cookieDays: props.cookieDays } : {}),
|
|
23
|
+
...(props.cookieDomain != null ? { cookieDomain: props.cookieDomain } : {}),
|
|
24
|
+
...(props.silent != null ? { silent: props.silent } : {}),
|
|
25
|
+
});
|
|
26
|
+
const optional = categories.filter(category => category.required !== true);
|
|
27
|
+
const [draft, setDraft] = useState({});
|
|
28
|
+
// Re-seed the draft whenever the dialog opens, from what is actually stored: a visitor who opens
|
|
29
|
+
// preferences a second time must see the answer they gave, not the one the last render held.
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
if (!consent.open) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
const stored = consent.record ?? readConsent({
|
|
35
|
+
...(props.storageKey != null ? { storageKey: props.storageKey } : {}),
|
|
36
|
+
});
|
|
37
|
+
setDraft(Object.fromEntries(optional.map(category => [category.key, stored?.[category.key] === true])));
|
|
38
|
+
}, [consent.open, consent.record]);
|
|
39
|
+
const persist = useCallback((values) => {
|
|
40
|
+
const record = Object.fromEntries([
|
|
41
|
+
...categories.filter(c => c.required === true).map(c => [c.key, true]),
|
|
42
|
+
...optional.map(c => [c.key, values[c.key] === true]),
|
|
43
|
+
]);
|
|
44
|
+
consent.save(record);
|
|
45
|
+
}, [categories, consent]);
|
|
46
|
+
const onSave = useCallback(() => persist(draft), [draft, persist]);
|
|
47
|
+
const onAcceptAll = useCallback(() => persist(Object.fromEntries(optional.map(c => [c.key, true]))), [optional, persist]);
|
|
48
|
+
// The dialog was raised by something that needs an answer before it can continue — signing in,
|
|
49
|
+
// today. Saying so, and relabelling the primary action, is what makes the interruption make
|
|
50
|
+
// sense rather than look like the page asking twice.
|
|
51
|
+
const gated = consent.reason === 'login';
|
|
52
|
+
return _jsxs(_Fragment, { children: [consent.open && _jsx("div", { className: "fixed inset-0 z-[999998] flex items-center justify-center overflow-y-auto bg-black/70 px-4 py-6 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
|
+
? 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" }) })] });
|
|
55
|
+
};
|
|
56
|
+
//# sourceMappingURL=component.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"component.js","sourceRoot":"","sources":["../../src/consent/component.tsx"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { FC } from 'react';
|
|
2
|
+
export interface ConsentToggleProps {
|
|
3
|
+
id: string;
|
|
4
|
+
label: string;
|
|
5
|
+
description: string;
|
|
6
|
+
checked: boolean;
|
|
7
|
+
/** A required category is disclosure, not a question: locked on, and labelled as such. */
|
|
8
|
+
required?: boolean;
|
|
9
|
+
requiredLabel: string;
|
|
10
|
+
onChange: (value: boolean) => void;
|
|
11
|
+
}
|
|
12
|
+
export declare const ConsentToggle: FC<ConsentToggleProps>;
|
|
13
|
+
//# sourceMappingURL=toggle.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"toggle.d.ts","sourceRoot":"","sources":["../../src/consent/toggle.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,OAAO,CAAA;AAG/B,MAAM,WAAW,kBAAkB;IACjC,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,EAAE,MAAM,CAAA;IACb,WAAW,EAAE,MAAM,CAAA;IACnB,OAAO,EAAE,OAAO,CAAA;IAChB,0FAA0F;IAC1F,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,aAAa,EAAE,MAAM,CAAA;IACrB,QAAQ,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAA;CACnC;AAED,eAAO,MAAM,aAAa,EAAE,EAAE,CAAC,kBAAkB,CAgChD,CAAA"}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { cn } from '../lib/utils.js';
|
|
3
|
+
export const ConsentToggle = ({ id, label, description, checked, required, requiredLabel, onChange }) => (_jsxs("div", { className: "flex items-start justify-between rounded-lg border border-border/50 bg-muted/30 p-3", children: [_jsxs("div", { className: "flex-1 pr-4", children: [_jsxs("div", { className: "mb-0.5 flex items-center gap-2 text-sm font-semibold text-popover-foreground", children: [label, required === true && _jsx("span", { className: "rounded bg-primary/10 px-1.5 py-0.5 text-[10px] font-medium uppercase tracking-wide text-primary", children: requiredLabel })] }), _jsx("p", { className: "text-xs text-muted-foreground", children: description })] }), _jsxs("label", { className: cn('relative h-6 w-11 flex-shrink-0', required === true ? 'cursor-not-allowed opacity-70' : 'cursor-pointer'), children: [_jsx("input", { id: id, type: "checkbox", className: "sr-only", checked: checked || required === true, disabled: required === true, onChange: event => onChange(event.target.checked) }), _jsx("span", { className: cn('absolute inset-0 rounded-full transition-colors duration-300', checked || required === true ? 'bg-primary' : 'bg-muted-foreground/30') }), _jsx("span", { className: cn('absolute top-0.5 h-5 w-5 rounded-full bg-white shadow transition-transform duration-300', checked || required === true ? 'translate-x-5' : 'translate-x-0.5') })] })] }));
|
|
4
|
+
//# sourceMappingURL=toggle.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"toggle.js","sourceRoot":"","sources":["../../src/consent/toggle.tsx"],"names":[],"mappings":";AACA,OAAO,EAAE,EAAE,EAAE,MAAM,iBAAiB,CAAA;AAapC,MAAM,CAAC,MAAM,aAAa,GAA2B,CACnD,EAAE,EAAE,EAAE,KAAK,EAAE,WAAW,EAAE,OAAO,EAAE,QAAQ,EAAE,aAAa,EAAE,QAAQ,EAAE,EACtE,EAAE,CAAC,CACH,eAAK,SAAS,EAAC,qFAAqF,aAClG,eAAK,SAAS,EAAC,aAAa,aAC1B,eAAK,SAAS,EAAC,8EAA8E,aAC1F,KAAK,EACL,QAAQ,KAAK,IAAI,IAAI,eAAM,SAAS,EAAC,kGAAkG,YACrI,aAAa,GACT,IACH,EACN,YAAG,SAAS,EAAC,+BAA+B,YAAE,WAAW,GAAK,IAC1D,EACN,iBAAO,SAAS,EAAE,EAAE,CAAC,iCAAiC,EAAE,QAAQ,KAAK,IAAI,CAAC,CAAC,CAAC,+BAA+B,CAAC,CAAC,CAAC,gBAAgB,CAAC,aAC7H,gBACE,EAAE,EAAE,EAAE,EACN,IAAI,EAAC,UAAU,EACf,SAAS,EAAC,SAAS,EACnB,OAAO,EAAE,OAAO,IAAI,QAAQ,KAAK,IAAI,EACrC,QAAQ,EAAE,QAAQ,KAAK,IAAI,EAC3B,QAAQ,EAAE,KAAK,CAAC,EAAE,CAAC,QAAQ,CAAC,KAAK,CAAC,MAAM,CAAC,OAAO,CAAC,GACjD,EACF,eAAM,SAAS,EAAE,EAAE,CACjB,8DAA8D,EAC9D,OAAO,IAAI,QAAQ,KAAK,IAAI,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,wBAAwB,CACvE,GAAI,EACL,eAAM,SAAS,EAAE,EAAE,CACjB,yFAAyF,EACzF,OAAO,IAAI,QAAQ,KAAK,IAAI,CAAC,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,iBAAiB,CACnE,GAAI,IACC,IACJ,CACP,CAAA"}
|
package/build/hooks.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ConsentOptions } from '@owlmeans/consent';
|
|
2
|
+
import type { UseConsentModel } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Subscribe to this document's consent state.
|
|
5
|
+
*
|
|
6
|
+
* `useSyncExternalStore` rather than local state, because consent is one thing per document and
|
|
7
|
+
* several components read it — the dialog, the re-open button, the policy page, and whatever a
|
|
8
|
+
* host app gates on it. Local copies would disagree the moment one of them saved.
|
|
9
|
+
*/
|
|
10
|
+
export declare const useConsent: (opts?: ConsentOptions) => UseConsentModel;
|
|
11
|
+
/** Whether one category is granted. For a component that gates a single thing. */
|
|
12
|
+
export declare const useConsentCategory: (key: string) => boolean;
|
|
13
|
+
//# sourceMappingURL=hooks.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hooks.d.ts","sourceRoot":"","sources":["../src/hooks.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAA;AACvD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAEjD;;;;;;GAMG;AACH,eAAO,MAAM,UAAU,UAAW,cAAc,KAAG,eAqBlD,CAAA;AAED,kFAAkF;AAClF,eAAO,MAAM,kBAAkB,QAAS,MAAM,KAAG,OAQhD,CAAA"}
|
package/build/hooks.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { useEffect, useSyncExternalStore } from 'react';
|
|
2
|
+
import { consentStore } from '@owlmeans/consent';
|
|
3
|
+
/**
|
|
4
|
+
* Subscribe to this document's consent state.
|
|
5
|
+
*
|
|
6
|
+
* `useSyncExternalStore` rather than local state, because consent is one thing per document and
|
|
7
|
+
* several components read it — the dialog, the re-open button, the policy page, and whatever a
|
|
8
|
+
* host app gates on it. Local copies would disagree the moment one of them saved.
|
|
9
|
+
*/
|
|
10
|
+
export const useConsent = (opts) => {
|
|
11
|
+
const state = useSyncExternalStore(listener => consentStore.subscribe(listener), () => consentStore.get(),
|
|
12
|
+
// On the server there is no browser to have an opinion, and rendering the dialog into static
|
|
13
|
+
// HTML would flash it for every visitor before hydration corrected them.
|
|
14
|
+
() => ({ record: null, open: false, reason: null }));
|
|
15
|
+
useEffect(() => { consentStore.init(opts); }, []);
|
|
16
|
+
return {
|
|
17
|
+
record: state.record,
|
|
18
|
+
open: state.open,
|
|
19
|
+
reason: state.reason,
|
|
20
|
+
granted: key => consentStore.granted(key),
|
|
21
|
+
save: record => consentStore.save(record),
|
|
22
|
+
acceptAll: () => consentStore.acceptAll(),
|
|
23
|
+
openDialog: reason => consentStore.open(reason),
|
|
24
|
+
close: () => consentStore.close(),
|
|
25
|
+
};
|
|
26
|
+
};
|
|
27
|
+
/** Whether one category is granted. For a component that gates a single thing. */
|
|
28
|
+
export const useConsentCategory = (key) => {
|
|
29
|
+
const state = useSyncExternalStore(listener => consentStore.subscribe(listener), () => consentStore.get(), () => ({ record: null, open: false, reason: null }));
|
|
30
|
+
return state.record != null && consentStore.granted(key);
|
|
31
|
+
};
|
|
32
|
+
//# sourceMappingURL=hooks.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hooks.js","sourceRoot":"","sources":["../src/hooks.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,oBAAoB,EAAE,MAAM,OAAO,CAAA;AACvD,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAIhD;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,IAAqB,EAAmB,EAAE;IACnE,MAAM,KAAK,GAAG,oBAAoB,CAChC,QAAQ,CAAC,EAAE,CAAC,YAAY,CAAC,SAAS,CAAC,QAAQ,CAAC,EAC5C,GAAG,EAAE,CAAC,YAAY,CAAC,GAAG,EAAE;IACxB,6FAA6F;IAC7F,yEAAyE;IACzE,GAAG,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CACpD,CAAA;IAED,SAAS,CAAC,GAAG,EAAE,GAAG,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA,CAAC,CAAC,EAAE,EAAE,CAAC,CAAA;IAEhD,OAAO;QACL,MAAM,EAAE,KAAK,CAAC,MAAM;QACpB,IAAI,EAAE,KAAK,CAAC,IAAI;QAChB,MAAM,EAAE,KAAK,CAAC,MAAM;QACpB,OAAO,EAAE,GAAG,CAAC,EAAE,CAAC,YAAY,CAAC,OAAO,CAAC,GAAG,CAAC;QACzC,IAAI,EAAE,MAAM,CAAC,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,MAAM,CAAC;QACzC,SAAS,EAAE,GAAG,EAAE,CAAC,YAAY,CAAC,SAAS,EAAE;QACzC,UAAU,EAAE,MAAM,CAAC,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,MAAM,CAAC;QAC/C,KAAK,EAAE,GAAG,EAAE,CAAC,YAAY,CAAC,KAAK,EAAE;KAClC,CAAA;AACH,CAAC,CAAA;AAED,kFAAkF;AAClF,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC,GAAW,EAAW,EAAE;IACzD,MAAM,KAAK,GAAG,oBAAoB,CAChC,QAAQ,CAAC,EAAE,CAAC,YAAY,CAAC,SAAS,CAAC,QAAQ,CAAC,EAC5C,GAAG,EAAE,CAAC,YAAY,CAAC,GAAG,EAAE,EACxB,GAAG,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CACpD,CAAA;IAED,OAAO,KAAK,CAAC,MAAM,IAAI,IAAI,IAAI,YAAY,CAAC,OAAO,CAAC,GAAG,CAAC,CAAA;AAC1D,CAAC,CAAA"}
|
package/build/index.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export type * from './types.js';
|
|
2
|
+
export * from './hooks.js';
|
|
3
|
+
export * from './consent/component.js';
|
|
4
|
+
export * from './consent/toggle.js';
|
|
5
|
+
export * from './policy/component.js';
|
|
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';
|
|
7
|
+
export type { ConsentCategory, ConsentOptions, ConsentReason, ConsentRecord, ConsentSignal, ConsentState, ConsentStore, ConsentLocale, } from '@owlmeans/consent';
|
|
8
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +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"}
|
package/build/index.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export * from './hooks.js';
|
|
2
|
+
export * from './consent/component.js';
|
|
3
|
+
export * from './consent/toggle.js';
|
|
4
|
+
export * from './policy/component.js';
|
|
5
|
+
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';
|
|
6
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +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"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type ClassValue } from 'clsx';
|
|
2
|
+
/**
|
|
3
|
+
* Class-name merge, owned by this package rather than reached through the `@` alias contract.
|
|
4
|
+
*
|
|
5
|
+
* The `@` contract exists so a consumer's own THEME and shadcn primitives win over a package's
|
|
6
|
+
* copies. `cn` has neither in it — it is four lines of string merging — and requiring an alias for
|
|
7
|
+
* it would mean every consumer of a consent dialog must first adopt an OwlMeans UI contract. One
|
|
8
|
+
* of the sites that needs this dialog most is an Astro site with its own component library and no
|
|
9
|
+
* such alias, so the dialog would simply fail to build there.
|
|
10
|
+
*/
|
|
11
|
+
export declare function cn(...inputs: ClassValue[]): string;
|
|
12
|
+
//# sourceMappingURL=utils.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
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;;;;;;;;GAQG;AACH,wBAAgB,EAAE,CAAC,GAAG,MAAM,EAAE,UAAU,EAAE,UAEzC"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { clsx } from 'clsx';
|
|
2
|
+
import { twMerge } from 'tailwind-merge';
|
|
3
|
+
/**
|
|
4
|
+
* Class-name merge, owned by this package rather than reached through the `@` alias contract.
|
|
5
|
+
*
|
|
6
|
+
* The `@` contract exists so a consumer's own THEME and shadcn primitives win over a package's
|
|
7
|
+
* copies. `cn` has neither in it — it is four lines of string merging — and requiring an alias for
|
|
8
|
+
* it would mean every consumer of a consent dialog must first adopt an OwlMeans UI contract. One
|
|
9
|
+
* of the sites that needs this dialog most is an Astro site with its own component library and no
|
|
10
|
+
* such alias, so the dialog would simply fail to build there.
|
|
11
|
+
*/
|
|
12
|
+
export function cn(...inputs) {
|
|
13
|
+
return twMerge(clsx(inputs));
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=utils.js.map
|
|
@@ -0,0 +1 @@
|
|
|
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;;;;;;;;GAQG;AACH,MAAM,UAAU,EAAE,CAAC,GAAG,MAAoB;IACxC,OAAO,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAA;AAC9B,CAAC"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { FC } from 'react';
|
|
2
|
+
import type { CookiePolicyProps } from '../types.js';
|
|
3
|
+
/**
|
|
4
|
+
* A cookie policy that states only what this application can actually assert.
|
|
5
|
+
*
|
|
6
|
+
* Which categories are in force, what each one drives, where the choice is stored and for how
|
|
7
|
+
* long — all of it read from the same configuration the dialog renders, so the page cannot claim
|
|
8
|
+
* something the widget does not do. That is the whole reason it is generated rather than written:
|
|
9
|
+
* a hand-written policy drifts from the code the first time a category changes, and nobody notices
|
|
10
|
+
* because nobody reads it until it matters.
|
|
11
|
+
*
|
|
12
|
+
* Everything OwlMeans cannot assert on the operator's behalf — who the controller is, what the
|
|
13
|
+
* lawful basis is, how to exercise rights — is deferred to their own privacy policy and terms.
|
|
14
|
+
*/
|
|
15
|
+
export declare const CookiePolicy: FC<CookiePolicyProps>;
|
|
16
|
+
//# sourceMappingURL=component.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"component.d.ts","sourceRoot":"","sources":["../../src/policy/component.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,OAAO,CAAA;AAM/B,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAA;AAEpD;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,YAAY,EAAE,EAAE,CAAC,iBAAiB,CAmD9C,CAAA"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { CONSENT_COOKIE_DAYS, CONSENT_KEY, DEFAULT_CONSENT_CATEGORIES, defaultConsentTranslate, interpolate, openConsent, } from '@owlmeans/consent';
|
|
3
|
+
import { cn } from '../lib/utils.js';
|
|
4
|
+
/**
|
|
5
|
+
* A cookie policy that states only what this application can actually assert.
|
|
6
|
+
*
|
|
7
|
+
* Which categories are in force, what each one drives, where the choice is stored and for how
|
|
8
|
+
* long — all of it read from the same configuration the dialog renders, so the page cannot claim
|
|
9
|
+
* something the widget does not do. That is the whole reason it is generated rather than written:
|
|
10
|
+
* a hand-written policy drifts from the code the first time a category changes, and nobody notices
|
|
11
|
+
* because nobody reads it until it matters.
|
|
12
|
+
*
|
|
13
|
+
* Everything OwlMeans cannot assert on the operator's behalf — who the controller is, what the
|
|
14
|
+
* lawful basis is, how to exercise rights — is deferred to their own privacy policy and terms.
|
|
15
|
+
*/
|
|
16
|
+
export const CookiePolicy = props => {
|
|
17
|
+
const categories = props.categories ?? DEFAULT_CONSENT_CATEGORIES;
|
|
18
|
+
const t = props.translate ?? defaultConsentTranslate(props.locale);
|
|
19
|
+
const storageKey = props.storageKey ?? CONSENT_KEY;
|
|
20
|
+
const days = props.cookieDays ?? CONSENT_COOKIE_DAYS;
|
|
21
|
+
return _jsxs("article", { className: cn('prose prose-sm max-w-2xl', props.className), "data-cookie-policy": true, children: [_jsx("h1", { children: t('policyTitle', 'Cookie Policy') }), _jsx("p", { children: t('policyIntro', 'This page describes the cookies and similar storage this application uses, and how you can control them.') }), _jsx("ul", { children: categories.map(category => _jsxs("li", { children: [_jsx("strong", { children: t(category.labelKey, category.key) }), category.required === true && _jsxs(_Fragment, { children: [" \u2014 ", t('required', 'Required')] }), _jsx("br", {}), t(category.descriptionKey, '')] }, category.key)) }), _jsx("p", { children: interpolate(t('policyStorage', 'Your choice is stored in this browser under "{{key}}", both in local storage and as a cookie.'), { key: storageKey }) }), _jsx("p", { children: interpolate(t('policyRetention', 'The record is kept for {{days}} days, after which you will be asked again.'), { days }) }), props.operator != null && props.operator !== '' && _jsxs("p", { children: [interpolate(t('policyOperator', 'This application is operated by {{operator}}.'), { operator: props.operator }), ' ', t('policyContact', 'See their privacy policy and terms for how they handle your data.')] }), _jsxs("p", { children: [props.privacyHref != null && props.privacyHref !== '' && _jsx("a", { href: props.privacyHref, target: "_blank", rel: "noopener noreferrer", children: t('privacy', 'Privacy Policy') }), props.privacyHref != null && props.termsHref != null && ' · ', props.termsHref != null && props.termsHref !== '' && _jsx("a", { href: props.termsHref, target: "_blank", rel: "noopener noreferrer", children: t('terms', 'Terms & Conditions') })] }), _jsx("p", { children: _jsx("button", { type: "button", onClick: () => openConsent('reopen'), "data-cookie-policy-manage": true, children: t('manage', 'Manage preferences') }) })] });
|
|
22
|
+
};
|
|
23
|
+
//# sourceMappingURL=component.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"component.js","sourceRoot":"","sources":["../../src/policy/component.tsx"],"names":[],"mappings":";AACA,OAAO,EACL,mBAAmB,EAAE,WAAW,EAAE,0BAA0B,EAC5D,uBAAuB,EAAE,WAAW,EAAE,WAAW,GAClD,MAAM,mBAAmB,CAAA;AAC1B,OAAO,EAAE,EAAE,EAAE,MAAM,iBAAiB,CAAA;AAGpC;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,MAAM,YAAY,GAA0B,KAAK,CAAC,EAAE;IACzD,MAAM,UAAU,GAAG,KAAK,CAAC,UAAU,IAAI,0BAA0B,CAAA;IACjE,MAAM,CAAC,GAAG,KAAK,CAAC,SAAS,IAAI,uBAAuB,CAAC,KAAK,CAAC,MAAM,CAAC,CAAA;IAClE,MAAM,UAAU,GAAG,KAAK,CAAC,UAAU,IAAI,WAAW,CAAA;IAClD,MAAM,IAAI,GAAG,KAAK,CAAC,UAAU,IAAI,mBAAmB,CAAA;IAEpD,OAAO,mBAAS,SAAS,EAAE,EAAE,CAAC,0BAA0B,EAAE,KAAK,CAAC,SAAS,CAAC,yCACxE,uBAAK,CAAC,CAAC,aAAa,EAAE,eAAe,CAAC,GAAM,EAC5C,sBAAI,CAAC,CAAC,aAAa,EAAE,0GAA0G,CAAC,GAAK,EAErI,uBACG,UAAU,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,CAAC,yBAC1B,2BAAS,CAAC,CAAC,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC,GAAG,CAAC,GAAU,EACpD,QAAQ,CAAC,QAAQ,KAAK,IAAI,IAAI,0CAAM,CAAC,CAAC,UAAU,EAAE,UAAU,CAAC,IAAI,EAClE,cAAM,EACL,CAAC,CAAC,QAAQ,CAAC,cAAc,EAAE,EAAE,CAAC,KAJI,QAAQ,CAAC,GAAG,CAK5C,CAAC,GACH,EAEL,sBAAI,WAAW,CACb,CAAC,CAAC,eAAe,EAAE,+FAA+F,CAAC,EACnH,EAAE,GAAG,EAAE,UAAU,EAAE,CACpB,GAAK,EACN,sBAAI,WAAW,CACb,CAAC,CAAC,iBAAiB,EAAE,4EAA4E,CAAC,EAClG,EAAE,IAAI,EAAE,CACT,GAAK,EAEL,KAAK,CAAC,QAAQ,IAAI,IAAI,IAAI,KAAK,CAAC,QAAQ,KAAK,EAAE,IAAI,wBACjD,WAAW,CAAC,CAAC,CAAC,gBAAgB,EAAE,+CAA+C,CAAC,EAC/E,EAAE,QAAQ,EAAE,KAAK,CAAC,QAAQ,EAAE,CAAC,EAC9B,GAAG,EACH,CAAC,CAAC,eAAe,EAAE,mEAAmE,CAAC,IACtF,EAEJ,wBACG,KAAK,CAAC,WAAW,IAAI,IAAI,IAAI,KAAK,CAAC,WAAW,KAAK,EAAE,IAAI,YACxD,IAAI,EAAE,KAAK,CAAC,WAAW,EAAE,MAAM,EAAC,QAAQ,EAAC,GAAG,EAAC,qBAAqB,YAClE,CAAC,CAAC,SAAS,EAAE,gBAAgB,CAAC,GAAK,EACpC,KAAK,CAAC,WAAW,IAAI,IAAI,IAAI,KAAK,CAAC,SAAS,IAAI,IAAI,IAAI,KAAK,EAC7D,KAAK,CAAC,SAAS,IAAI,IAAI,IAAI,KAAK,CAAC,SAAS,KAAK,EAAE,IAAI,YACpD,IAAI,EAAE,KAAK,CAAC,SAAS,EAAE,MAAM,EAAC,QAAQ,EAAC,GAAG,EAAC,qBAAqB,YAChE,CAAC,CAAC,OAAO,EAAE,oBAAoB,CAAC,GAAK,IACrC,EAEJ,sBACE,iBAAQ,IAAI,EAAC,QAAQ,EAAC,OAAO,EAAE,GAAG,EAAE,CAAC,WAAW,CAAC,QAAQ,CAAC,+CACvD,CAAC,CAAC,QAAQ,EAAE,oBAAoB,CAAC,GAC3B,GACP,IACI,CAAA;AACZ,CAAC,CAAA"}
|
package/build/types.d.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { ConsentCategory, ConsentReason, ConsentRecord } from '@owlmeans/consent';
|
|
2
|
+
export interface ConsentLink {
|
|
3
|
+
href: string;
|
|
4
|
+
labelKey: string;
|
|
5
|
+
defaultLabel: string;
|
|
6
|
+
}
|
|
7
|
+
export interface CookieConsentProps {
|
|
8
|
+
locale?: string;
|
|
9
|
+
categories?: ConsentCategory[];
|
|
10
|
+
/** `(key, defaultValue) => string`. Defaults to the built-in seven-language bundle. */
|
|
11
|
+
translate?: (key: string, defaultValue: string) => string;
|
|
12
|
+
/**
|
|
13
|
+
* The cookie-policy page. A plain string, so an Astro route, a framework-resolved path and a raw
|
|
14
|
+
* href all work — this component must not know how its host does routing.
|
|
15
|
+
*/
|
|
16
|
+
policyHref?: string;
|
|
17
|
+
/** Anything else worth linking from the dialog: privacy, terms. */
|
|
18
|
+
links?: ConsentLink[];
|
|
19
|
+
storageKey?: string;
|
|
20
|
+
cookieDays?: number;
|
|
21
|
+
cookieDomain?: string;
|
|
22
|
+
silent?: boolean;
|
|
23
|
+
/** Hide the persistent re-open button, for an app that offers a footer link instead. */
|
|
24
|
+
noReopenButton?: boolean;
|
|
25
|
+
className?: string;
|
|
26
|
+
}
|
|
27
|
+
export interface CookiePolicyProps {
|
|
28
|
+
locale?: string;
|
|
29
|
+
translate?: (key: string, defaultValue: string) => string;
|
|
30
|
+
categories?: ConsentCategory[];
|
|
31
|
+
/** Who operates this application — the branding record supplies it in a generated app. */
|
|
32
|
+
operator?: string;
|
|
33
|
+
privacyHref?: string;
|
|
34
|
+
termsHref?: string;
|
|
35
|
+
storageKey?: string;
|
|
36
|
+
cookieDays?: number;
|
|
37
|
+
className?: string;
|
|
38
|
+
}
|
|
39
|
+
export interface UseConsentModel {
|
|
40
|
+
record: ConsentRecord | null;
|
|
41
|
+
open: boolean;
|
|
42
|
+
reason: ConsentReason | null;
|
|
43
|
+
granted: (key: string) => boolean;
|
|
44
|
+
save: (record: ConsentRecord) => void;
|
|
45
|
+
acceptAll: () => void;
|
|
46
|
+
openDialog: (reason?: ConsentReason) => void;
|
|
47
|
+
close: () => void;
|
|
48
|
+
}
|
|
49
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +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"}
|
package/build/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@owlmeans/web-consent",
|
|
3
|
+
"version": "0.1.18-rc.2",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"build": "tsc -b",
|
|
8
|
+
"dev": "sleep 2 && nodemon -e ts,tsx,json --watch src --exec \"tsc -p ./tsconfig.json\"",
|
|
9
|
+
"watch": "tsc -b -w --preserveWatchOutput --pretty",
|
|
10
|
+
"test": "bun test ./tests"
|
|
11
|
+
},
|
|
12
|
+
"main": "build/index.js",
|
|
13
|
+
"module": "build/index.js",
|
|
14
|
+
"types": "build/index.d.ts",
|
|
15
|
+
"exports": {
|
|
16
|
+
".": {
|
|
17
|
+
"import": "./build/index.js",
|
|
18
|
+
"require": "./build/index.js",
|
|
19
|
+
"default": "./build/index.js",
|
|
20
|
+
"module": "./build/index.js",
|
|
21
|
+
"types": "./build/index.d.ts"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@owlmeans/consent": "^0.1.18-rc.2"
|
|
26
|
+
},
|
|
27
|
+
"peerDependencies": {
|
|
28
|
+
"clsx": "*",
|
|
29
|
+
"lucide-react": "*",
|
|
30
|
+
"react": "*",
|
|
31
|
+
"tailwind-merge": "*",
|
|
32
|
+
"tailwindcss": "*"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@owlmeans/dep-config": "workspace:*",
|
|
36
|
+
"@types/bun": "^1.4.0",
|
|
37
|
+
"@types/react": "^19.2.17",
|
|
38
|
+
"clsx": "^2.1.1",
|
|
39
|
+
"lucide-react": "^0.550.0",
|
|
40
|
+
"nodemon": "^3.1.14",
|
|
41
|
+
"tailwind-merge": "^3.4.1",
|
|
42
|
+
"typescript": "^7.0.2"
|
|
43
|
+
},
|
|
44
|
+
"publishConfig": {
|
|
45
|
+
"access": "public"
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { useCallback, useEffect, useState } from 'react'
|
|
2
|
+
import type { FC } from 'react'
|
|
3
|
+
import { Cookie } from 'lucide-react'
|
|
4
|
+
import {
|
|
5
|
+
DEFAULT_CONSENT_CATEGORIES, defaultConsentTranslate, readConsent,
|
|
6
|
+
} from '@owlmeans/consent'
|
|
7
|
+
import type { ConsentRecord } from '@owlmeans/consent'
|
|
8
|
+
import { cn } from '../lib/utils.js'
|
|
9
|
+
import { useConsent } from '../hooks.js'
|
|
10
|
+
import { ConsentToggle } from './toggle.js'
|
|
11
|
+
import type { CookieConsentProps } from '../types.js'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The cookie preferences dialog, and the button that brings it back.
|
|
15
|
+
*
|
|
16
|
+
* Deliberately built from raw elements rather than shadcn primitives. One of the three surfaces
|
|
17
|
+
* this serves is an Astro island on a site that vendors its own component library, and requiring a
|
|
18
|
+
* consumer to install a UI family in order to render a consent notice would put the notice out of
|
|
19
|
+
* reach of the site that needs it most.
|
|
20
|
+
*/
|
|
21
|
+
export const CookieConsent: FC<CookieConsentProps> = props => {
|
|
22
|
+
const categories = props.categories ?? DEFAULT_CONSENT_CATEGORIES
|
|
23
|
+
const t = props.translate ?? defaultConsentTranslate(props.locale)
|
|
24
|
+
|
|
25
|
+
const consent = useConsent({
|
|
26
|
+
categories,
|
|
27
|
+
...(props.storageKey != null ? { storageKey: props.storageKey } : {}),
|
|
28
|
+
...(props.cookieDays != null ? { cookieDays: props.cookieDays } : {}),
|
|
29
|
+
...(props.cookieDomain != null ? { cookieDomain: props.cookieDomain } : {}),
|
|
30
|
+
...(props.silent != null ? { silent: props.silent } : {}),
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
const optional = categories.filter(category => category.required !== true)
|
|
34
|
+
const [draft, setDraft] = useState<Record<string, boolean>>({})
|
|
35
|
+
|
|
36
|
+
// Re-seed the draft whenever the dialog opens, from what is actually stored: a visitor who opens
|
|
37
|
+
// preferences a second time must see the answer they gave, not the one the last render held.
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
if (!consent.open) {
|
|
40
|
+
return
|
|
41
|
+
}
|
|
42
|
+
const stored = consent.record ?? readConsent({
|
|
43
|
+
...(props.storageKey != null ? { storageKey: props.storageKey } : {}),
|
|
44
|
+
})
|
|
45
|
+
setDraft(Object.fromEntries(optional.map(category =>
|
|
46
|
+
[category.key, stored?.[category.key] === true])))
|
|
47
|
+
}, [consent.open, consent.record])
|
|
48
|
+
|
|
49
|
+
const persist = useCallback((values: Record<string, boolean>) => {
|
|
50
|
+
const record: ConsentRecord = Object.fromEntries([
|
|
51
|
+
...categories.filter(c => c.required === true).map(c => [c.key, true]),
|
|
52
|
+
...optional.map(c => [c.key, values[c.key] === true]),
|
|
53
|
+
])
|
|
54
|
+
consent.save(record)
|
|
55
|
+
}, [categories, consent])
|
|
56
|
+
|
|
57
|
+
const onSave = useCallback(() => persist(draft), [draft, persist])
|
|
58
|
+
const onAcceptAll = useCallback(
|
|
59
|
+
() => persist(Object.fromEntries(optional.map(c => [c.key, true]))), [optional, persist]
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
// The dialog was raised by something that needs an answer before it can continue — signing in,
|
|
63
|
+
// today. Saying so, and relabelling the primary action, is what makes the interruption make
|
|
64
|
+
// sense rather than look like the page asking twice.
|
|
65
|
+
const gated = consent.reason === 'login'
|
|
66
|
+
|
|
67
|
+
return <>
|
|
68
|
+
{consent.open && <div
|
|
69
|
+
className="fixed inset-0 z-[999998] flex items-center justify-center overflow-y-auto bg-black/70 px-4 py-6 backdrop-blur-md"
|
|
70
|
+
aria-modal="true" role="dialog" aria-labelledby="cc-title" aria-describedby="cc-desc"
|
|
71
|
+
data-consent-dialog
|
|
72
|
+
>
|
|
73
|
+
<div className={cn(
|
|
74
|
+
'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',
|
|
75
|
+
props.className
|
|
76
|
+
)}>
|
|
77
|
+
<div className="absolute inset-x-0 top-0 h-1 rounded-t-2xl bg-gradient-to-r from-primary to-secondary" />
|
|
78
|
+
|
|
79
|
+
<div className="mb-4 flex items-center gap-3">
|
|
80
|
+
<div className="flex h-10 w-10 items-center justify-center rounded-xl bg-primary/10 text-primary">
|
|
81
|
+
<Cookie className="h-5 w-5" aria-hidden="true" />
|
|
82
|
+
</div>
|
|
83
|
+
<h2 id="cc-title" className="text-xl font-bold text-popover-foreground sm:text-2xl">
|
|
84
|
+
{t('title', 'Cookie Preferences')}
|
|
85
|
+
</h2>
|
|
86
|
+
</div>
|
|
87
|
+
|
|
88
|
+
<p id="cc-desc" className="mb-6 text-sm leading-relaxed text-muted-foreground">
|
|
89
|
+
{t('description', 'We use cookies to enhance your browsing experience, serve personalized ads or content, and analyze our traffic.')}
|
|
90
|
+
</p>
|
|
91
|
+
|
|
92
|
+
<div className="space-y-3">
|
|
93
|
+
{categories.map(category => <ConsentToggle
|
|
94
|
+
key={category.key}
|
|
95
|
+
id={`cc-${category.key}`}
|
|
96
|
+
label={t(category.labelKey, category.key)}
|
|
97
|
+
description={t(category.descriptionKey, '')}
|
|
98
|
+
checked={draft[category.key] === true}
|
|
99
|
+
{...(category.required === true ? { required: true } : {})}
|
|
100
|
+
requiredLabel={t('required', 'Required')}
|
|
101
|
+
onChange={value => setDraft(current => ({ ...current, [category.key]: value }))}
|
|
102
|
+
/>)}
|
|
103
|
+
</div>
|
|
104
|
+
|
|
105
|
+
{gated && <p role="status" data-consent-reason className="mt-4 text-sm text-popover-foreground">
|
|
106
|
+
{t('loginReason', 'Signing in stores a session cookie. Accept essential cookies to continue.')}
|
|
107
|
+
</p>}
|
|
108
|
+
|
|
109
|
+
<div className="mt-8 flex flex-col gap-3 sm:flex-row sm:gap-4">
|
|
110
|
+
<button
|
|
111
|
+
type="button" onClick={onSave} data-consent-save
|
|
112
|
+
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"
|
|
113
|
+
>{t('savePreferences', 'Save Preferences')}</button>
|
|
114
|
+
<button
|
|
115
|
+
type="button" onClick={onAcceptAll} data-consent-accept-all
|
|
116
|
+
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"
|
|
117
|
+
>{gated
|
|
118
|
+
? t('acceptAndContinue', 'Accept & continue')
|
|
119
|
+
: t('acceptAll', 'Accept All')}</button>
|
|
120
|
+
</div>
|
|
121
|
+
|
|
122
|
+
{(props.policyHref != null || props.links != null) && <div className="mt-4 flex flex-wrap justify-center gap-3 text-xs text-muted-foreground">
|
|
123
|
+
{props.policyHref != null && <a
|
|
124
|
+
href={props.policyHref} target="_blank" rel="noopener noreferrer"
|
|
125
|
+
className="underline underline-offset-2 transition-colors hover:text-popover-foreground"
|
|
126
|
+
>{t('policyLink', 'Cookie Policy')}</a>}
|
|
127
|
+
{props.links?.map(link => <a
|
|
128
|
+
key={link.href} href={link.href} target="_blank" rel="noopener noreferrer"
|
|
129
|
+
className="underline underline-offset-2 transition-colors hover:text-popover-foreground"
|
|
130
|
+
>{t(link.labelKey, link.defaultLabel)}</a>)}
|
|
131
|
+
</div>}
|
|
132
|
+
</div>
|
|
133
|
+
</div>}
|
|
134
|
+
|
|
135
|
+
{props.noReopenButton !== true && consent.record != null && !consent.open && <button
|
|
136
|
+
type="button" onClick={() => consent.openDialog('reopen')}
|
|
137
|
+
aria-label={t('openPreferences', 'Cookie preferences')}
|
|
138
|
+
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"
|
|
140
|
+
>
|
|
141
|
+
<Cookie className="h-5 w-5" aria-hidden="true" />
|
|
142
|
+
</button>}
|
|
143
|
+
</>
|
|
144
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { FC } from 'react'
|
|
2
|
+
import { cn } from '../lib/utils.js'
|
|
3
|
+
|
|
4
|
+
export interface ConsentToggleProps {
|
|
5
|
+
id: string
|
|
6
|
+
label: string
|
|
7
|
+
description: string
|
|
8
|
+
checked: boolean
|
|
9
|
+
/** A required category is disclosure, not a question: locked on, and labelled as such. */
|
|
10
|
+
required?: boolean
|
|
11
|
+
requiredLabel: string
|
|
12
|
+
onChange: (value: boolean) => void
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const ConsentToggle: FC<ConsentToggleProps> = (
|
|
16
|
+
{ id, label, description, checked, required, requiredLabel, onChange }
|
|
17
|
+
) => (
|
|
18
|
+
<div className="flex items-start justify-between rounded-lg border border-border/50 bg-muted/30 p-3">
|
|
19
|
+
<div className="flex-1 pr-4">
|
|
20
|
+
<div className="mb-0.5 flex items-center gap-2 text-sm font-semibold text-popover-foreground">
|
|
21
|
+
{label}
|
|
22
|
+
{required === true && <span className="rounded bg-primary/10 px-1.5 py-0.5 text-[10px] font-medium uppercase tracking-wide text-primary">
|
|
23
|
+
{requiredLabel}
|
|
24
|
+
</span>}
|
|
25
|
+
</div>
|
|
26
|
+
<p className="text-xs text-muted-foreground">{description}</p>
|
|
27
|
+
</div>
|
|
28
|
+
<label className={cn('relative h-6 w-11 flex-shrink-0', required === true ? 'cursor-not-allowed opacity-70' : 'cursor-pointer')}>
|
|
29
|
+
<input
|
|
30
|
+
id={id}
|
|
31
|
+
type="checkbox"
|
|
32
|
+
className="sr-only"
|
|
33
|
+
checked={checked || required === true}
|
|
34
|
+
disabled={required === true}
|
|
35
|
+
onChange={event => onChange(event.target.checked)}
|
|
36
|
+
/>
|
|
37
|
+
<span className={cn(
|
|
38
|
+
'absolute inset-0 rounded-full transition-colors duration-300',
|
|
39
|
+
checked || required === true ? 'bg-primary' : 'bg-muted-foreground/30'
|
|
40
|
+
)} />
|
|
41
|
+
<span className={cn(
|
|
42
|
+
'absolute top-0.5 h-5 w-5 rounded-full bg-white shadow transition-transform duration-300',
|
|
43
|
+
checked || required === true ? 'translate-x-5' : 'translate-x-0.5'
|
|
44
|
+
)} />
|
|
45
|
+
</label>
|
|
46
|
+
</div>
|
|
47
|
+
)
|
package/src/hooks.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { useEffect, useSyncExternalStore } from 'react'
|
|
2
|
+
import { consentStore } from '@owlmeans/consent'
|
|
3
|
+
import type { ConsentOptions } from '@owlmeans/consent'
|
|
4
|
+
import type { UseConsentModel } from './types.js'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Subscribe to this document's consent state.
|
|
8
|
+
*
|
|
9
|
+
* `useSyncExternalStore` rather than local state, because consent is one thing per document and
|
|
10
|
+
* several components read it — the dialog, the re-open button, the policy page, and whatever a
|
|
11
|
+
* host app gates on it. Local copies would disagree the moment one of them saved.
|
|
12
|
+
*/
|
|
13
|
+
export const useConsent = (opts?: ConsentOptions): UseConsentModel => {
|
|
14
|
+
const state = useSyncExternalStore(
|
|
15
|
+
listener => consentStore.subscribe(listener),
|
|
16
|
+
() => consentStore.get(),
|
|
17
|
+
// On the server there is no browser to have an opinion, and rendering the dialog into static
|
|
18
|
+
// HTML would flash it for every visitor before hydration corrected them.
|
|
19
|
+
() => ({ record: null, open: false, reason: null })
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
useEffect(() => { consentStore.init(opts) }, [])
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
record: state.record,
|
|
26
|
+
open: state.open,
|
|
27
|
+
reason: state.reason,
|
|
28
|
+
granted: key => consentStore.granted(key),
|
|
29
|
+
save: record => consentStore.save(record),
|
|
30
|
+
acceptAll: () => consentStore.acceptAll(),
|
|
31
|
+
openDialog: reason => consentStore.open(reason),
|
|
32
|
+
close: () => consentStore.close(),
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Whether one category is granted. For a component that gates a single thing. */
|
|
37
|
+
export const useConsentCategory = (key: string): boolean => {
|
|
38
|
+
const state = useSyncExternalStore(
|
|
39
|
+
listener => consentStore.subscribe(listener),
|
|
40
|
+
() => consentStore.get(),
|
|
41
|
+
() => ({ record: null, open: false, reason: null })
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
return state.record != null && consentStore.granted(key)
|
|
45
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export type * from './types.js'
|
|
2
|
+
export * from './hooks.js'
|
|
3
|
+
export * from './consent/component.js'
|
|
4
|
+
export * from './consent/toggle.js'
|
|
5
|
+
export * from './policy/component.js'
|
|
6
|
+
|
|
7
|
+
export {
|
|
8
|
+
consentStore, openConsent, isConsented, readConsent, writeConsent, clearConsent,
|
|
9
|
+
migrateConsent, applyConsent, pushConsentDefaults, consentBootstrapScript,
|
|
10
|
+
consentDefaults, consentUpdate, gtagConsent,
|
|
11
|
+
DEFAULT_CONSENT_CATEGORIES, DEFAULT_CONSENT_MESSAGES, defaultConsentTranslate, interpolate,
|
|
12
|
+
CONSENT_KEY, CONSENT_COOKIE_DAYS, CONSENT_SCHEMA_VERSION, CONSENT_LOCALES,
|
|
13
|
+
CONSENT_ESSENTIAL, CONSENT_ANALYTICS, CONSENT_MARKETING,
|
|
14
|
+
} from '@owlmeans/consent'
|
|
15
|
+
export type {
|
|
16
|
+
ConsentCategory, ConsentOptions, ConsentReason, ConsentRecord, ConsentSignal, ConsentState,
|
|
17
|
+
ConsentStore, ConsentLocale,
|
|
18
|
+
} from '@owlmeans/consent'
|
package/src/lib/utils.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { clsx, type ClassValue } from 'clsx'
|
|
2
|
+
import { twMerge } from 'tailwind-merge'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Class-name merge, owned by this package rather than reached through the `@` alias contract.
|
|
6
|
+
*
|
|
7
|
+
* The `@` contract exists so a consumer's own THEME and shadcn primitives win over a package's
|
|
8
|
+
* copies. `cn` has neither in it — it is four lines of string merging — and requiring an alias for
|
|
9
|
+
* it would mean every consumer of a consent dialog must first adopt an OwlMeans UI contract. One
|
|
10
|
+
* of the sites that needs this dialog most is an Astro site with its own component library and no
|
|
11
|
+
* such alias, so the dialog would simply fail to build there.
|
|
12
|
+
*/
|
|
13
|
+
export function cn(...inputs: ClassValue[]) {
|
|
14
|
+
return twMerge(clsx(inputs))
|
|
15
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { FC } from 'react'
|
|
2
|
+
import {
|
|
3
|
+
CONSENT_COOKIE_DAYS, CONSENT_KEY, DEFAULT_CONSENT_CATEGORIES,
|
|
4
|
+
defaultConsentTranslate, interpolate, openConsent,
|
|
5
|
+
} from '@owlmeans/consent'
|
|
6
|
+
import { cn } from '../lib/utils.js'
|
|
7
|
+
import type { CookiePolicyProps } from '../types.js'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* A cookie policy that states only what this application can actually assert.
|
|
11
|
+
*
|
|
12
|
+
* Which categories are in force, what each one drives, where the choice is stored and for how
|
|
13
|
+
* long — all of it read from the same configuration the dialog renders, so the page cannot claim
|
|
14
|
+
* something the widget does not do. That is the whole reason it is generated rather than written:
|
|
15
|
+
* a hand-written policy drifts from the code the first time a category changes, and nobody notices
|
|
16
|
+
* because nobody reads it until it matters.
|
|
17
|
+
*
|
|
18
|
+
* Everything OwlMeans cannot assert on the operator's behalf — who the controller is, what the
|
|
19
|
+
* lawful basis is, how to exercise rights — is deferred to their own privacy policy and terms.
|
|
20
|
+
*/
|
|
21
|
+
export const CookiePolicy: FC<CookiePolicyProps> = props => {
|
|
22
|
+
const categories = props.categories ?? DEFAULT_CONSENT_CATEGORIES
|
|
23
|
+
const t = props.translate ?? defaultConsentTranslate(props.locale)
|
|
24
|
+
const storageKey = props.storageKey ?? CONSENT_KEY
|
|
25
|
+
const days = props.cookieDays ?? CONSENT_COOKIE_DAYS
|
|
26
|
+
|
|
27
|
+
return <article className={cn('prose prose-sm max-w-2xl', props.className)} data-cookie-policy>
|
|
28
|
+
<h1>{t('policyTitle', 'Cookie Policy')}</h1>
|
|
29
|
+
<p>{t('policyIntro', 'This page describes the cookies and similar storage this application uses, and how you can control them.')}</p>
|
|
30
|
+
|
|
31
|
+
<ul>
|
|
32
|
+
{categories.map(category => <li key={category.key}>
|
|
33
|
+
<strong>{t(category.labelKey, category.key)}</strong>
|
|
34
|
+
{category.required === true && <> — {t('required', 'Required')}</>}
|
|
35
|
+
<br />
|
|
36
|
+
{t(category.descriptionKey, '')}
|
|
37
|
+
</li>)}
|
|
38
|
+
</ul>
|
|
39
|
+
|
|
40
|
+
<p>{interpolate(
|
|
41
|
+
t('policyStorage', 'Your choice is stored in this browser under "{{key}}", both in local storage and as a cookie.'),
|
|
42
|
+
{ key: storageKey }
|
|
43
|
+
)}</p>
|
|
44
|
+
<p>{interpolate(
|
|
45
|
+
t('policyRetention', 'The record is kept for {{days}} days, after which you will be asked again.'),
|
|
46
|
+
{ days }
|
|
47
|
+
)}</p>
|
|
48
|
+
|
|
49
|
+
{props.operator != null && props.operator !== '' && <p>
|
|
50
|
+
{interpolate(t('policyOperator', 'This application is operated by {{operator}}.'),
|
|
51
|
+
{ operator: props.operator })}
|
|
52
|
+
{' '}
|
|
53
|
+
{t('policyContact', 'See their privacy policy and terms for how they handle your data.')}
|
|
54
|
+
</p>}
|
|
55
|
+
|
|
56
|
+
<p>
|
|
57
|
+
{props.privacyHref != null && props.privacyHref !== '' && <a
|
|
58
|
+
href={props.privacyHref} target="_blank" rel="noopener noreferrer"
|
|
59
|
+
>{t('privacy', 'Privacy Policy')}</a>}
|
|
60
|
+
{props.privacyHref != null && props.termsHref != null && ' · '}
|
|
61
|
+
{props.termsHref != null && props.termsHref !== '' && <a
|
|
62
|
+
href={props.termsHref} target="_blank" rel="noopener noreferrer"
|
|
63
|
+
>{t('terms', 'Terms & Conditions')}</a>}
|
|
64
|
+
</p>
|
|
65
|
+
|
|
66
|
+
<p>
|
|
67
|
+
<button type="button" onClick={() => openConsent('reopen')} data-cookie-policy-manage>
|
|
68
|
+
{t('manage', 'Manage preferences')}
|
|
69
|
+
</button>
|
|
70
|
+
</p>
|
|
71
|
+
</article>
|
|
72
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ConsentCategory, ConsentReason, ConsentRecord,
|
|
3
|
+
} from '@owlmeans/consent'
|
|
4
|
+
|
|
5
|
+
export interface ConsentLink {
|
|
6
|
+
href: string
|
|
7
|
+
labelKey: string
|
|
8
|
+
defaultLabel: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface CookieConsentProps {
|
|
12
|
+
locale?: string
|
|
13
|
+
categories?: ConsentCategory[]
|
|
14
|
+
/** `(key, defaultValue) => string`. Defaults to the built-in seven-language bundle. */
|
|
15
|
+
translate?: (key: string, defaultValue: string) => string
|
|
16
|
+
/**
|
|
17
|
+
* The cookie-policy page. A plain string, so an Astro route, a framework-resolved path and a raw
|
|
18
|
+
* href all work — this component must not know how its host does routing.
|
|
19
|
+
*/
|
|
20
|
+
policyHref?: string
|
|
21
|
+
/** Anything else worth linking from the dialog: privacy, terms. */
|
|
22
|
+
links?: ConsentLink[]
|
|
23
|
+
storageKey?: string
|
|
24
|
+
cookieDays?: number
|
|
25
|
+
cookieDomain?: string
|
|
26
|
+
silent?: boolean
|
|
27
|
+
/** Hide the persistent re-open button, for an app that offers a footer link instead. */
|
|
28
|
+
noReopenButton?: boolean
|
|
29
|
+
className?: string
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface CookiePolicyProps {
|
|
33
|
+
locale?: string
|
|
34
|
+
translate?: (key: string, defaultValue: string) => string
|
|
35
|
+
categories?: ConsentCategory[]
|
|
36
|
+
/** Who operates this application — the branding record supplies it in a generated app. */
|
|
37
|
+
operator?: string
|
|
38
|
+
privacyHref?: string
|
|
39
|
+
termsHref?: string
|
|
40
|
+
storageKey?: string
|
|
41
|
+
cookieDays?: number
|
|
42
|
+
className?: string
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface UseConsentModel {
|
|
46
|
+
record: ConsentRecord | null
|
|
47
|
+
open: boolean
|
|
48
|
+
reason: ConsentReason | null
|
|
49
|
+
granted: (key: string) => boolean
|
|
50
|
+
save: (record: ConsentRecord) => void
|
|
51
|
+
acceptAll: () => void
|
|
52
|
+
openDialog: (reason?: ConsentReason) => void
|
|
53
|
+
close: () => void
|
|
54
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": [
|
|
3
|
+
"@owlmeans/dep-config/tsconfig.base.json",
|
|
4
|
+
"@owlmeans/dep-config/tsconfig.react.json"
|
|
5
|
+
],
|
|
6
|
+
"compilerOptions": {
|
|
7
|
+
"rootDir": "./src/",
|
|
8
|
+
"outDir": "./build/"
|
|
9
|
+
},
|
|
10
|
+
"exclude": [
|
|
11
|
+
"./dist/**/*",
|
|
12
|
+
"./build/**/*",
|
|
13
|
+
"./tests/**/*",
|
|
14
|
+
"./*.ts"
|
|
15
|
+
]
|
|
16
|
+
}
|