@uninspired/cookie-banner 0.0.4 → 0.0.6
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/dist/components/Banner/Banner.js +35 -0
- package/dist/components/Banner/index.js +1 -0
- package/dist/components/BannerItem/BannerItem.js +24 -0
- package/dist/components/BannerItem/index.js +1 -0
- package/dist/components/Button/Button.js +6 -0
- package/dist/components/Button/index.js +1 -0
- package/dist/components/Switch/Switch.js +7 -0
- package/dist/components/Switch/index.js +1 -0
- package/dist/components/Text/Text.js +9 -0
- package/dist/components/Text/index.js +1 -0
- package/dist/contexts/SelectionContext.js +101 -0
- package/dist/react/index.cjs +950 -329
- package/dist/react/index.d.ts +2 -2
- package/dist/react/index.js +951 -330
- package/dist/react.js +1 -0
- package/dist/script/index.cjs +18 -18
- package/dist/script/index.js +1885 -1603
- package/dist/script.js +1 -0
- package/dist/types/components/Banner/Banner.d.ts +21 -0
- package/dist/types/components/Banner/index.d.ts +1 -0
- package/dist/types/components/BannerItem/BannerItem.d.ts +23 -0
- package/dist/types/components/BannerItem/index.d.ts +1 -0
- package/dist/types/components/Button/Button.d.ts +5 -0
- package/dist/types/components/Button/index.d.ts +1 -0
- package/dist/types/components/Switch/Switch.d.ts +5 -0
- package/dist/types/components/Switch/index.d.ts +1 -0
- package/dist/types/components/Text/Text.d.ts +8 -0
- package/dist/types/components/Text/index.d.ts +1 -0
- package/dist/types/contexts/SelectionContext.d.ts +23 -0
- package/dist/types/react.d.ts +1 -0
- package/dist/types/script.d.ts +1 -0
- package/dist/types/utils/classes.d.ts +2 -0
- package/dist/types/utils/mountBanner.d.ts +2 -0
- package/dist/types/utils/scriptDefinition.d.ts +35 -0
- package/dist/types/utils/selection.d.ts +3 -0
- package/dist/utils/classes.js +9 -0
- package/dist/utils/mountBanner.js +18 -0
- package/dist/utils/scriptDefinition.js +21 -0
- package/dist/utils/selection.js +2 -0
- package/package.json +7 -7
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "preact/jsx-runtime";
|
|
2
|
+
import { useMemo, useState } from "react";
|
|
3
|
+
import { cls, clx } from "../../utils/classes";
|
|
4
|
+
import { Button } from "../Button";
|
|
5
|
+
import { Text } from "../Text";
|
|
6
|
+
import classes from "./Banner.module.css";
|
|
7
|
+
import { BannerItem } from "../BannerItem";
|
|
8
|
+
import { Root as AccordionRoot } from "@radix-ui/react-accordion";
|
|
9
|
+
import { ChevronUp } from "lucide-react";
|
|
10
|
+
import { SelectionProvider, useSelection, } from "../../contexts/SelectionContext";
|
|
11
|
+
import { Root as DialogRoot, Content as DialogContent, Title as DialogTitle, Description as DialogDescription, Close as DialogClose, } from "@radix-ui/react-dialog";
|
|
12
|
+
import { Root as CollapsibleRoot, Trigger as CollapsibleTrigger, Content as CollapsibleContent, } from "@radix-ui/react-collapsible";
|
|
13
|
+
import "../../styles/variables.css";
|
|
14
|
+
const BannerContent = ({ noTarget = true, heading = "We use cookies.", subheading = "Please define your selection below.", selectLabel = "Select", hideLabel = "Hide", saveLabel = "Save selection", declineLabel = "Decline all", defaultSettingsOpen = true, privacyPolicy, items, }) => {
|
|
15
|
+
const { onSave, onDeclineAll, selectionTaken } = useSelection();
|
|
16
|
+
const [openItem, setOpenItem] = useState(undefined);
|
|
17
|
+
const [settingsOpen, setSettingsOpen] = useState(defaultSettingsOpen);
|
|
18
|
+
return (_jsx(DialogRoot, { open: !selectionTaken, children: _jsx(DialogContent, { className: cls(classes.root, noTarget ? classes.noTarget : ""), children: _jsxs(CollapsibleRoot, { open: settingsOpen, onOpenChange: setSettingsOpen, className: classes.collapsible, children: [_jsxs("div", { className: classes.header, children: [_jsxs("div", { className: classes.heading, children: [_jsx(DialogTitle, { asChild: true, children: _jsx(Text, { weight: "bold", children: heading }) }), _jsx(DialogDescription, { asChild: true, children: _jsx(Text, { size: "caption", color: "muted", children: subheading }) })] }), _jsx(CollapsibleTrigger, { asChild: true, children: _jsxs(Button, { variant: "ghost", children: [settingsOpen ? hideLabel : selectLabel, "\u00A0", _jsx(ChevronUp, { size: 12, className: clx({
|
|
19
|
+
[classes.chevron]: true,
|
|
20
|
+
[classes.open]: settingsOpen,
|
|
21
|
+
}) })] }) })] }), _jsx(CollapsibleContent, { className: clx({
|
|
22
|
+
[classes.settings]: true,
|
|
23
|
+
}), children: _jsx(AccordionRoot, { type: "single", collapsible: true, value: openItem, onValueChange: setOpenItem, children: items.map((item) => (_jsx(BannerItem, { ...item, openItem: openItem }, item.value))) }) }), _jsxs("div", { className: classes.footer, children: [_jsx("div", { className: classes.legal, children: privacyPolicy && (_jsx(Text, { size: "caption", children: _jsx("a", { href: privacyPolicy.url, target: "_blank", children: privacyPolicy.label }) })) }), _jsxs("div", { className: classes.actions, children: [_jsx(DialogClose, { asChild: true, children: _jsx(Button, { variant: "neutral", onClick: () => onDeclineAll(), children: declineLabel }) }), _jsx(DialogClose, { asChild: true, children: _jsx(Button, { variant: "brand", onClick: () => onSave(), children: saveLabel }) })] })] })] }) }) }));
|
|
24
|
+
};
|
|
25
|
+
export const Banner = ({ localStorageKey = "cb-selection", items, ...rest }) => {
|
|
26
|
+
const selectionItems = useMemo(() => items
|
|
27
|
+
.filter((item) => !item.required)
|
|
28
|
+
.map((item) => ({
|
|
29
|
+
value: item.value,
|
|
30
|
+
script: item.script,
|
|
31
|
+
defaultSelected: item.defaultSelected,
|
|
32
|
+
required: item.required,
|
|
33
|
+
})), [items]);
|
|
34
|
+
return (_jsx(SelectionProvider, { items: selectionItems, localStorageKey: localStorageKey, children: _jsx(BannerContent, { items: items, ...rest }) }));
|
|
35
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./Banner";
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "preact/jsx-runtime";
|
|
2
|
+
import classes from "./BannerItem.module.css";
|
|
3
|
+
import {} from "../../utils/scriptDefinition";
|
|
4
|
+
import { Text } from "../Text";
|
|
5
|
+
import { Item, Trigger, Content } from "@radix-ui/react-accordion";
|
|
6
|
+
import { ChevronDown } from "lucide-react";
|
|
7
|
+
import { clx } from "../../utils/classes";
|
|
8
|
+
import { Switch } from "../Switch";
|
|
9
|
+
import { useEffect, useMemo } from "react";
|
|
10
|
+
import { useSelection } from "../../contexts/SelectionContext";
|
|
11
|
+
export const BannerItem = ({ value, label, sublabel, required = false, description, openItem, }) => {
|
|
12
|
+
const { toggleSelection, selection } = useSelection();
|
|
13
|
+
const selected = useMemo(() => selection?.[value] ?? false, [selection, value]);
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
toggleSelection(value, selected);
|
|
16
|
+
}, [value, selected]);
|
|
17
|
+
const checked = useMemo(() => (required ? true : selected), [required, selected]);
|
|
18
|
+
const onCheckedChange = useMemo(() => (checked) => required ? () => { } : toggleSelection(value, checked), [required, toggleSelection, value]);
|
|
19
|
+
const disabled = useMemo(() => required, [required]);
|
|
20
|
+
return (_jsxs(Item, { value: value, className: classes.root, children: [_jsxs("div", { className: classes.header, children: [_jsx(Trigger, { asChild: true, children: _jsxs("div", { className: classes.label, children: [_jsx(Text, { weight: "bold", children: label }), sublabel && (_jsx(Text, { size: "caption", color: "muted", children: sublabel }))] }) }), _jsxs("div", { className: classes.actions, children: [_jsx(Switch, { checked: checked, onCheckedChange: onCheckedChange, disabled: disabled }), _jsx(Trigger, { asChild: true, children: _jsx(ChevronDown, { size: 16, className: clx({
|
|
21
|
+
[classes.chevron]: true,
|
|
22
|
+
[classes.open]: openItem === value,
|
|
23
|
+
}) }) })] })] }), description && (_jsx(Content, { className: classes.description, children: _jsx(Text, { size: "caption", dangerouslySetInnerHTML: { __html: description } }) }))] }));
|
|
24
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./BannerItem";
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { jsx as _jsx } from "preact/jsx-runtime";
|
|
2
|
+
import classes from "./Button.module.css";
|
|
3
|
+
import { cls } from "../../utils/classes";
|
|
4
|
+
export const Button = ({ variant = "neutral", ...rest }) => {
|
|
5
|
+
return _jsx("button", { className: cls(classes.root, classes[variant]), ...rest });
|
|
6
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./Button";
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "preact/jsx-runtime";
|
|
2
|
+
import { Root, Thumb, } from "@radix-ui/react-switch";
|
|
3
|
+
import classes from "./Switch.module.css";
|
|
4
|
+
import { Text } from "../Text";
|
|
5
|
+
export const Switch = ({ label, id, ...rest }) => {
|
|
6
|
+
return (_jsxs("div", { className: classes.root, children: [label && (_jsx(Text, { className: classes.label, as: "label", htmlFor: id, children: label })), _jsx(Root, { className: classes.switch, ...rest, children: _jsx(Thumb, { className: classes.thumb }) })] }));
|
|
7
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./Switch";
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { jsx as _jsx } from "preact/jsx-runtime";
|
|
2
|
+
import { cls } from "../../utils/classes";
|
|
3
|
+
import classes from "./Text.module.css";
|
|
4
|
+
export const Text = ({ as = "span", size = "body", weight = "normal", color = "default", ...rest }) => {
|
|
5
|
+
const Component = as ?? "span";
|
|
6
|
+
return (
|
|
7
|
+
// @ts-ignore
|
|
8
|
+
_jsx(Component, { className: cls(classes.root, classes[size], classes[weight], classes[color]), ...rest }));
|
|
9
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./Text";
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { jsx as _jsx } from "preact/jsx-runtime";
|
|
2
|
+
import { createContext, useCallback, useContext, useEffect, useMemo, useState, } from "react";
|
|
3
|
+
import { selectionSchema } from "../utils/selection";
|
|
4
|
+
import { scriptDefinitionSchema } from "../utils/scriptDefinition";
|
|
5
|
+
export const SelectionContext = createContext(null);
|
|
6
|
+
export const SelectionProvider = ({ children, items, localStorageKey, }) => {
|
|
7
|
+
function getLsSelection(localStorageKey) {
|
|
8
|
+
const storedSelection = localStorage.getItem(localStorageKey);
|
|
9
|
+
if (!storedSelection)
|
|
10
|
+
return null;
|
|
11
|
+
const { success, data } = selectionSchema.safeParse(JSON.parse(storedSelection));
|
|
12
|
+
if (!success)
|
|
13
|
+
return null;
|
|
14
|
+
return data;
|
|
15
|
+
}
|
|
16
|
+
const defaultSelection = useMemo(() => Object.fromEntries(items.map((item) => [item.value, item.defaultSelected ?? false])), [items]);
|
|
17
|
+
const [selection, setSelection] = useState(getLsSelection(localStorageKey) ?? defaultSelection);
|
|
18
|
+
const [savedSelection, setSavedSelection] = useState(getLsSelection(localStorageKey));
|
|
19
|
+
function toggleSelection(key, value) {
|
|
20
|
+
const newSelection = selection
|
|
21
|
+
? { ...selection, [key]: value }
|
|
22
|
+
: { [key]: value };
|
|
23
|
+
setSelection(newSelection);
|
|
24
|
+
}
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
let selectedValues = [];
|
|
27
|
+
if (savedSelection !== null) {
|
|
28
|
+
selectedValues = Object.entries(savedSelection)
|
|
29
|
+
.filter(([_, value]) => value)
|
|
30
|
+
.map(([key]) => key);
|
|
31
|
+
}
|
|
32
|
+
const mountedItems = items.filter((item) => selectedValues.includes(item.value));
|
|
33
|
+
for (const item of items) {
|
|
34
|
+
const elem = document.head.querySelector(`#${item.value}`);
|
|
35
|
+
if (!elem)
|
|
36
|
+
continue;
|
|
37
|
+
elem.remove();
|
|
38
|
+
}
|
|
39
|
+
for (const item of mountedItems) {
|
|
40
|
+
const elem = document.createElement("script");
|
|
41
|
+
elem.id = item.value;
|
|
42
|
+
if (!item.script) {
|
|
43
|
+
if (!item.required)
|
|
44
|
+
console.warn("CookieBanner: Missing script for", item.value);
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
const { success, data: script } = scriptDefinitionSchema.safeParse(item.script);
|
|
48
|
+
if (!success) {
|
|
49
|
+
console.error("CookieBanner: Invalid script definition for", item.value);
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
elem.type = script.type ?? "text/javascript";
|
|
53
|
+
elem.async = script.async ?? false;
|
|
54
|
+
elem.defer = script.defer ?? false;
|
|
55
|
+
switch (script.variant) {
|
|
56
|
+
case "inline":
|
|
57
|
+
elem.textContent = script.content;
|
|
58
|
+
break;
|
|
59
|
+
case "remote":
|
|
60
|
+
elem.src = script.src;
|
|
61
|
+
elem.crossOrigin = script.crossorigin ?? null;
|
|
62
|
+
elem.integrity = script.integrity ?? "";
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
document.head.appendChild(elem);
|
|
66
|
+
}
|
|
67
|
+
}, [savedSelection, items]);
|
|
68
|
+
function saveSelection(newSelection) {
|
|
69
|
+
setSavedSelection(newSelection);
|
|
70
|
+
localStorage.setItem(localStorageKey, JSON.stringify(newSelection));
|
|
71
|
+
}
|
|
72
|
+
const onDeclineAll = useCallback(() => {
|
|
73
|
+
const newSelection = Object.fromEntries(items.map((item) => [item.value, false]));
|
|
74
|
+
setSelection(newSelection);
|
|
75
|
+
saveSelection(newSelection);
|
|
76
|
+
}, [items]);
|
|
77
|
+
const onSave = useCallback(() => {
|
|
78
|
+
saveSelection(selection);
|
|
79
|
+
}, [selection]);
|
|
80
|
+
const selectionTaken = useMemo(() => {
|
|
81
|
+
if (!savedSelection)
|
|
82
|
+
return false;
|
|
83
|
+
const selectedKeys = Object.keys(savedSelection);
|
|
84
|
+
const itemKeys = items.map((item) => item.value);
|
|
85
|
+
return itemKeys.every((key) => selectedKeys.includes(key));
|
|
86
|
+
}, [savedSelection, items]);
|
|
87
|
+
return (_jsx(SelectionContext.Provider, { value: {
|
|
88
|
+
selection,
|
|
89
|
+
toggleSelection,
|
|
90
|
+
onDeclineAll,
|
|
91
|
+
onSave,
|
|
92
|
+
selectionTaken,
|
|
93
|
+
}, children: children }));
|
|
94
|
+
};
|
|
95
|
+
export function useSelection() {
|
|
96
|
+
const context = useContext(SelectionContext);
|
|
97
|
+
if (!context) {
|
|
98
|
+
throw new Error("useSelection must be used within a SelectionProvider");
|
|
99
|
+
}
|
|
100
|
+
return context;
|
|
101
|
+
}
|