@hanzo/ui 8.29.15 → 8.29.18
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/consent/Consent.cjs +208 -49
- package/dist/consent/Consent.d.ts +4 -1
- package/dist/consent/Consent.d.ts.map +1 -1
- package/dist/consent/Consent.js +210 -51
- package/dist/consent/Consent.js.map +1 -1
- package/dist/consent/icon.cjs +14 -0
- package/dist/consent/icon.d.ts +13 -0
- package/dist/consent/icon.d.ts.map +1 -0
- package/dist/consent/icon.js +11 -0
- package/dist/consent/icon.js.map +1 -0
- package/dist/consent/index.cjs +3 -1
- package/dist/consent/index.d.ts +1 -0
- package/dist/consent/index.d.ts.map +1 -1
- package/dist/consent/index.js +1 -0
- package/dist/consent/index.js.map +1 -1
- package/dist/gallery.html +1 -1
- package/dist/marketing/Footer.cjs +4 -1
- package/dist/marketing/Footer.d.ts.map +1 -1
- package/dist/marketing/Footer.js +4 -1
- package/dist/marketing/Footer.js.map +1 -1
- package/dist/masthead/nav.cjs +1 -1
- package/dist/masthead/nav.js +1 -1
- package/dist/masthead/nav.js.map +1 -1
- package/package.json +3 -3
package/dist/consent/Consent.cjs
CHANGED
|
@@ -6,27 +6,33 @@ const jsx_runtime_1 = require("react/jsx-runtime");
|
|
|
6
6
|
/**
|
|
7
7
|
* Consent — the one cookie banner every Hanzo, Lux and Zoo site mounts.
|
|
8
8
|
*
|
|
9
|
-
* It draws the choice; @hanzo/event stores it (`hz_consent`) and acts on it
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
9
|
+
* It draws the choice; @hanzo/event stores it (`hz_consent`) and acts on it. The
|
|
10
|
+
* brand's own first-party analytics is strictly necessary and runs for every
|
|
11
|
+
* visitor. What the third parties (Google Analytics, the Meta pixel, their ad
|
|
12
|
+
* personalization) may do is regional: cloud places the visitor by GeoIP and
|
|
13
|
+
* serves a `mode` with the site's tag config (`consentPolicy()`).
|
|
13
14
|
*
|
|
14
|
-
* -
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* -
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* -
|
|
22
|
-
*
|
|
23
|
-
* -
|
|
24
|
-
*
|
|
15
|
+
* - `eu`: opt-in. A bar with Manage preferences, Accept all and a ×, until the
|
|
16
|
+
* visitor has chosen; nothing of the third parties loads first. The × is a
|
|
17
|
+
* choice, not a dismissal: essential cookies only, so refusing is one press,
|
|
18
|
+
* exactly as accepting is.
|
|
19
|
+
* - `us_privacy`: opt-out. No bar; Google Analytics and Meta run by default, and
|
|
20
|
+
* "Your Privacy Choices" opens the sheet, where turning a switch off saves at
|
|
21
|
+
* once. The advertising switch is the sale and sharing opt-out.
|
|
22
|
+
* - `global`: a notice. One line and a × that records it as seen; the same
|
|
23
|
+
* opt-out sheet stays one press away.
|
|
24
|
+
* - Global Privacy Control or Do Not Track is an answer already: in `eu` every
|
|
25
|
+
* switch is held off and the visitor is not asked; elsewhere the sale and
|
|
26
|
+
* sharing for targeted advertising is held off.
|
|
27
|
+
* - Until cloud answers, nothing is drawn: the answer this tab was last served
|
|
28
|
+
* (@hanzo/event keeps it) is used at once, and with none by 1.5s the strict
|
|
29
|
+
* `eu` rule applies.
|
|
25
30
|
*
|
|
26
31
|
* `Consent` mounts once near the root. `openConsent()`, `ConsentLink` and any
|
|
27
|
-
* link to `#choices` (the footer's Privacy
|
|
28
|
-
*
|
|
29
|
-
*
|
|
32
|
+
* link to `#choices` (the footer's Your Privacy Choices) open the sheet from
|
|
33
|
+
* anywhere. A page with no such link in it gets a small one, fixed to the foot of
|
|
34
|
+
* the window, so the choices are always one press away. `brand` names who is
|
|
35
|
+
* asking; `cookies`, `privacy` and `terms` are the brand's own policies.
|
|
30
36
|
*/
|
|
31
37
|
const gui_1 = require("@hanzo/gui");
|
|
32
38
|
const lucide_icons_2_1 = require("@hanzogui/lucide-icons-2");
|
|
@@ -36,6 +42,7 @@ const button_1 = require("../backends/gui/button.cjs");
|
|
|
36
42
|
const dir_1 = require("../backends/gui/dir.cjs");
|
|
37
43
|
const switch_1 = require("../backends/gui/switch.cjs");
|
|
38
44
|
const slot_1 = require("../backends/gui/slot.cjs");
|
|
45
|
+
const icon_1 = require("./icon.cjs");
|
|
39
46
|
const OPEN = 'hzconsent-open';
|
|
40
47
|
/** The address that opens the sheet, from any link on any page. */
|
|
41
48
|
const HASH = '#choices';
|
|
@@ -45,7 +52,39 @@ const openConsent = () => {
|
|
|
45
52
|
window.dispatchEvent(new Event(OPEN));
|
|
46
53
|
};
|
|
47
54
|
exports.openConsent = openConsent;
|
|
48
|
-
|
|
55
|
+
/** How long a page waits for cloud's rule before the strict one applies. */
|
|
56
|
+
const WAIT = 1500;
|
|
57
|
+
/**
|
|
58
|
+
* The rule, once one has been served: on this page, or to this tab before (then
|
|
59
|
+
* it carries the region cloud placed the visitor in). `consentPolicy()` answers
|
|
60
|
+
* `eu` before either, which is the rule to wait out rather than draw.
|
|
61
|
+
*/
|
|
62
|
+
const served = () => {
|
|
63
|
+
const p = (0, event_1.consentPolicy)();
|
|
64
|
+
return p.region ? p.mode : undefined;
|
|
65
|
+
};
|
|
66
|
+
/*
|
|
67
|
+
* The links a page draws to the sheet, counted on the page (not in module scope,
|
|
68
|
+
* since a bundle may carry two copies of this module), so the component draws its
|
|
69
|
+
* own only where the page has none.
|
|
70
|
+
*/
|
|
71
|
+
const LINKS = Symbol.for('hanzo.ui.consent.links');
|
|
72
|
+
const LINKS_EVENT = 'hzconsent-links';
|
|
73
|
+
function hold() {
|
|
74
|
+
const g = globalThis;
|
|
75
|
+
g[LINKS] = (g[LINKS] ?? 0) + 1;
|
|
76
|
+
window.dispatchEvent(new Event(LINKS_EVENT));
|
|
77
|
+
return () => {
|
|
78
|
+
g[LINKS] = Math.max(0, (g[LINKS] ?? 1) - 1);
|
|
79
|
+
window.dispatchEvent(new Event(LINKS_EVENT));
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
/** Whether the page draws a way to the sheet: a ConsentLink, or a link to #choices (a footer's). */
|
|
83
|
+
function linked() {
|
|
84
|
+
const g = globalThis;
|
|
85
|
+
return (g[LINKS] ?? 0) > 0 || document.querySelector(`a[href$="${HASH}"]`) !== null;
|
|
86
|
+
}
|
|
87
|
+
const kinds = (brand, strict) => [
|
|
49
88
|
{
|
|
50
89
|
id: 'necessary',
|
|
51
90
|
title: 'Strictly Necessary Cookies',
|
|
@@ -67,9 +106,17 @@ const kinds = (brand) => [
|
|
|
67
106
|
id: 'ads',
|
|
68
107
|
key: 'ads',
|
|
69
108
|
title: 'Sell, Share, or Use Your Data for Targeted Advertising',
|
|
70
|
-
body:
|
|
109
|
+
body: `Lets Google and Meta use your visits to choose the ads they show you on other sites and apps. Some US state privacy laws call this a “sale” or “sharing” of your personal information, or “targeted advertising”, and give you the right to opt out: ${strict ? 'it stays off unless you turn it on.' : 'turn this off to opt out.'}`,
|
|
71
110
|
},
|
|
72
111
|
];
|
|
112
|
+
/** The sheet's opening line, by rule: what runs before the visitor does anything, and how to change it. */
|
|
113
|
+
const intro = (strict) => strict
|
|
114
|
+
? 'We use cookies and similar technologies to provide our services, measure performance, and support advertising. Essential cookies are required for the site to work and cannot be turned off. You can allow or reject optional cookies below. Learn more in our '
|
|
115
|
+
: 'We use cookies and similar technologies to provide our services, measure performance, and support advertising. Google Analytics and the Meta pixel run on this site by default. Turn either off below, or turn off the last switch to opt out of the sale or sharing of your personal information for targeted advertising; each switch is saved as soon as you change it. Essential cookies are required for the site to work and cannot be turned off. Learn more in our ';
|
|
116
|
+
/** What a browser that sends Global Privacy Control or Do Not Track holds off, by rule. */
|
|
117
|
+
const refusal = (strict) => strict
|
|
118
|
+
? 'Your browser asks sites not to track you, so all of these stay off.'
|
|
119
|
+
: 'Your browser asks sites not to track you, so your data is not sold or shared for targeted advertising.';
|
|
73
120
|
/** The bar's and the sheet's inset from the window's edges: 16px, or the device's inset and 8px where that is wider. */
|
|
74
121
|
const EDGE = {
|
|
75
122
|
top: 'max(16px, calc(var(--safe-top, env(safe-area-inset-top, 0px)) + 8px))',
|
|
@@ -77,12 +124,18 @@ const EDGE = {
|
|
|
77
124
|
left: 'max(16px, calc(var(--safe-left, env(safe-area-inset-left, 0px)) + 8px))',
|
|
78
125
|
right: 'max(16px, calc(var(--safe-right, env(safe-area-inset-right, 0px)) + 8px))',
|
|
79
126
|
};
|
|
80
|
-
/**
|
|
127
|
+
/**
|
|
128
|
+
* Corners from design's ladder (radius.css), one step apart so the buttons sit
|
|
129
|
+
* concentric in their card: a button on `--radius-lg` (12px), the bar and the
|
|
130
|
+
* sheet on `--radius-xl` (16px). Both follow `--radius-scale`; neither is a pill.
|
|
131
|
+
*/
|
|
132
|
+
const ROUND = { button: 'var(--radius-lg, 12px)', card: 'var(--radius-xl, 16px)' };
|
|
133
|
+
/** The panel both wear: the card's ground, edge, corner and floating shadow. */
|
|
81
134
|
const PANEL = {
|
|
82
135
|
bg: '$background',
|
|
83
136
|
borderWidth: 1,
|
|
84
137
|
borderColor: '$borderColor',
|
|
85
|
-
rounded:
|
|
138
|
+
rounded: ROUND.card,
|
|
86
139
|
shadowColor: 'rgba(0,0,0,0.25)',
|
|
87
140
|
shadowRadius: 24,
|
|
88
141
|
shadowOffset: { width: 0, height: 8 },
|
|
@@ -96,8 +149,6 @@ const PANEL = {
|
|
|
96
149
|
const frame = {
|
|
97
150
|
...(0, slot_1.slot)('consent'),
|
|
98
151
|
...PANEL,
|
|
99
|
-
role: 'dialog',
|
|
100
|
-
'aria-label': 'Cookie preferences',
|
|
101
152
|
position: 'fixed',
|
|
102
153
|
b: EDGE.bottom,
|
|
103
154
|
l: EDGE.left,
|
|
@@ -106,13 +157,59 @@ const frame = {
|
|
|
106
157
|
z: 'var(--z-banner, 50)',
|
|
107
158
|
py: '$3',
|
|
108
159
|
paddingInlineStart: '$4',
|
|
109
|
-
// Clear of the × in the corner: its
|
|
160
|
+
// Clear of the × in the corner: its 36px, 12px outside it and 8px before it.
|
|
110
161
|
paddingInlineEnd: 56,
|
|
111
162
|
};
|
|
112
163
|
/** The bar is as wide as its line and buttons, up to the window less its inset. */
|
|
113
164
|
const FIT = { width: 'fit-content' };
|
|
114
|
-
/** The
|
|
115
|
-
const
|
|
165
|
+
/** The keyboard's mark on any control here: design's focus ring, outside the control. */
|
|
166
|
+
const RING = { outlineColor: 'var(--ring)', outlineStyle: 'solid', outlineWidth: 2, outlineOffset: 2 };
|
|
167
|
+
/**
|
|
168
|
+
* A button's every state from design's button tokens (colors.css), so it follows
|
|
169
|
+
* the theme and an org's accent: at rest the fill, under the pointer and pressed
|
|
170
|
+
* its `-hover` step, focused by keyboard the ring. Each state names its fill and
|
|
171
|
+
* a clear edge outright, because a state that leaves one out takes the Button's
|
|
172
|
+
* own (the frame's hover is a near-black fill and a grey edge, which put dark
|
|
173
|
+
* text on dark under the cursor). 13px type on the 36px floor.
|
|
174
|
+
*/
|
|
175
|
+
const button = (fill, hover, ink) => ({
|
|
176
|
+
size: 'sm',
|
|
177
|
+
minH: 36,
|
|
178
|
+
rounded: ROUND.button,
|
|
179
|
+
bg: fill,
|
|
180
|
+
color: ink,
|
|
181
|
+
borderColor: 'transparent',
|
|
182
|
+
hoverStyle: { bg: hover, borderColor: 'transparent', opacity: 1 },
|
|
183
|
+
pressStyle: { bg: hover, borderColor: 'transparent', opacity: 1 },
|
|
184
|
+
focusStyle: { borderColor: 'transparent' },
|
|
185
|
+
focusVisibleStyle: RING,
|
|
186
|
+
});
|
|
187
|
+
/** The one filled control: Accept all in the bar, Save preferences (or Done) in the sheet. */
|
|
188
|
+
const PRIMARY = button('var(--primary)', 'var(--primary-hover)', 'var(--primary-foreground)');
|
|
189
|
+
/** Beside it: Manage preferences in the bar, Accept all in the sheet. */
|
|
190
|
+
const SECONDARY = button('var(--secondary)', 'var(--secondary-hover)', 'var(--secondary-foreground)');
|
|
191
|
+
/** A ×: no fill at rest, the secondary surface under the pointer. */
|
|
192
|
+
const CLOSE = {
|
|
193
|
+
rounded: ROUND.button,
|
|
194
|
+
bg: 'transparent',
|
|
195
|
+
borderColor: 'transparent',
|
|
196
|
+
hoverStyle: { bg: 'var(--secondary)', borderColor: 'transparent', opacity: 1 },
|
|
197
|
+
pressStyle: { bg: 'var(--secondary-hover)', borderColor: 'transparent', opacity: 1 },
|
|
198
|
+
focusStyle: { borderColor: 'transparent' },
|
|
199
|
+
focusVisibleStyle: RING,
|
|
200
|
+
};
|
|
201
|
+
/** "Your Privacy Choices" as a footer draws a link: the icon, then the words, quiet until pointed at. */
|
|
202
|
+
const CHOICES = {
|
|
203
|
+
type: 'button',
|
|
204
|
+
variant: 'link',
|
|
205
|
+
size: 'sm',
|
|
206
|
+
px: 0,
|
|
207
|
+
minH: 0,
|
|
208
|
+
color: '$aside',
|
|
209
|
+
hoverStyle: { color: '$ink' },
|
|
210
|
+
focusVisibleStyle: RING,
|
|
211
|
+
startIcon: (0, jsx_runtime_1.jsx)(icon_1.PrivacyChoicesIcon, {}),
|
|
212
|
+
};
|
|
116
213
|
/** A link in the sheet's intro: the line's own ink, underlined, never broken across lines. */
|
|
117
214
|
const INLINE = { color: 'inherit', textDecoration: 'underline', whiteSpace: 'nowrap' };
|
|
118
215
|
/** The sheet covers the window, on design's modal rung, over a scrim of its own. */
|
|
@@ -122,10 +219,17 @@ const FOCUSABLE = 'a[href], button:not([disabled]), [tabindex]:not([tabindex="-1
|
|
|
122
219
|
/** A section's button never submits a form the sheet sits in. gui's stack props do not name `type`. */
|
|
123
220
|
const BUTTON = { type: 'button' };
|
|
124
221
|
const Consent = ({ cookies = 'https://hanzo.ai/cookies', privacy = 'https://hanzo.ai/privacy', terms = 'https://hanzo.ai/terms', brand = 'Hanzo', }) => {
|
|
222
|
+
// The rule in force: undefined until cloud (or this tab) has said, and nothing is drawn meanwhile.
|
|
223
|
+
const [mode, setMode] = (0, react_1.useState)();
|
|
224
|
+
// The bar (eu) or the notice (global) at the foot of the window.
|
|
125
225
|
const [shown, setShown] = (0, react_1.useState)(false);
|
|
126
226
|
const [choosing, setChoosing] = (0, react_1.useState)(false);
|
|
127
227
|
const [draft, setDraft] = (0, react_1.useState)({ analytics: false, marketing: false, ads: false });
|
|
128
228
|
const [expanded, setExpanded] = (0, react_1.useState)({});
|
|
229
|
+
// Whether the page draws its own way to the sheet; if not, the component draws one.
|
|
230
|
+
const [hosted, setHosted] = (0, react_1.useState)(true);
|
|
231
|
+
// The rule, read by handlers registered once.
|
|
232
|
+
const rule = (0, react_1.useRef)(undefined);
|
|
129
233
|
// The choices are open because the visitor asked for them. A policy that
|
|
130
234
|
// arrives after (cloud answers ~150ms into a page opened at #choices) redraws
|
|
131
235
|
// the bar's question, and must not close what was asked for.
|
|
@@ -135,12 +239,14 @@ const Consent = ({ cookies = 'https://hanzo.ai/cookies', privacy = 'https://hanz
|
|
|
135
239
|
const sheet = (0, react_1.useRef)(null);
|
|
136
240
|
const manage = (0, react_1.useRef)(null);
|
|
137
241
|
const opened = (0, react_1.useRef)(false);
|
|
138
|
-
// The switches were filled from a stored choice, not left
|
|
242
|
+
// The switches were filled from a stored choice, not left at the rule's defaults.
|
|
139
243
|
const seeded = (0, react_1.useRef)(false);
|
|
140
244
|
const id = (0, react_1.useId)();
|
|
141
245
|
// The browser refused, as the page sees it or as cloud saw the request. State,
|
|
142
246
|
// so cloud's answer redraws an open sheet.
|
|
143
247
|
const [locked, setLocked] = (0, react_1.useState)(false);
|
|
248
|
+
/** What stands at the foot of the window under a rule: the eu bar until a choice, the global notice until seen. */
|
|
249
|
+
const front = (m = rule.current) => (m === 'eu' ? (0, event_1.asks)() : m === 'global' ? !(0, event_1.noticed)() : false);
|
|
144
250
|
const choose = () => {
|
|
145
251
|
// Already open: the visitor's unsaved switches and the focus to return stay as they are.
|
|
146
252
|
if (asked.current)
|
|
@@ -152,29 +258,52 @@ const Consent = ({ cookies = 'https://hanzo.ai/cookies', privacy = 'https://hanz
|
|
|
152
258
|
seeded.current = (0, event_1.storedConsent)();
|
|
153
259
|
setDraft((0, event_1.readConsent)());
|
|
154
260
|
setChoosing(true);
|
|
155
|
-
setShown(true);
|
|
156
261
|
};
|
|
157
|
-
// Closing without a choice: a visitor who has not chosen is asked again by the bar.
|
|
262
|
+
// Closing without a choice: under eu a visitor who has not chosen is asked again by the bar.
|
|
158
263
|
const close = () => {
|
|
159
264
|
asked.current = false;
|
|
160
265
|
setChoosing(false);
|
|
161
|
-
setShown((
|
|
266
|
+
setShown(front());
|
|
162
267
|
};
|
|
163
268
|
(0, react_1.useEffect)(() => {
|
|
164
269
|
const sync = () => {
|
|
165
|
-
setShown((
|
|
270
|
+
setShown(front());
|
|
166
271
|
setLocked((0, event_1.declined)());
|
|
167
272
|
// A policy version the stored choice was not made under: the switches the
|
|
168
|
-
// sheet opened with were an old grant, and
|
|
273
|
+
// sheet opened with were an old grant, and fall back to the rule's own.
|
|
169
274
|
if (asked.current && seeded.current && !(0, event_1.storedConsent)()) {
|
|
170
275
|
seeded.current = false;
|
|
171
276
|
setDraft((0, event_1.readConsent)());
|
|
172
277
|
}
|
|
173
278
|
};
|
|
174
|
-
|
|
279
|
+
const decide = (m) => {
|
|
280
|
+
rule.current = m;
|
|
281
|
+
setMode(m);
|
|
282
|
+
sync();
|
|
283
|
+
};
|
|
284
|
+
// Cloud answered on this page: its rule, whether or not it placed the visitor.
|
|
285
|
+
const policy = () => decide((0, event_1.consentPolicy)().mode);
|
|
286
|
+
const known = served();
|
|
287
|
+
if (known)
|
|
288
|
+
decide(known);
|
|
289
|
+
// No answer in time is the strict answer.
|
|
290
|
+
const wait = known ? undefined : window.setTimeout(() => rule.current ?? decide('eu'), WAIT);
|
|
291
|
+
// The page's own links to the sheet, looked for again as it changes, once a frame.
|
|
292
|
+
let frameId = 0;
|
|
293
|
+
const look = () => {
|
|
294
|
+
if (frameId)
|
|
295
|
+
return;
|
|
296
|
+
frameId = window.requestAnimationFrame(() => {
|
|
297
|
+
frameId = 0;
|
|
298
|
+
setHosted(linked());
|
|
299
|
+
});
|
|
300
|
+
};
|
|
301
|
+
look();
|
|
302
|
+
const watch = new MutationObserver(look);
|
|
303
|
+
watch.observe(document.body, { childList: true, subtree: true });
|
|
175
304
|
// `#choices` opens the sheet: on arrival, on a hash change, and on a press of
|
|
176
|
-
// any link on this site to it, so the footer's Privacy
|
|
177
|
-
// a page are the same control.
|
|
305
|
+
// any link on this site to it, so the footer's Your Privacy Choices and a link
|
|
306
|
+
// in a page are the same control.
|
|
178
307
|
const hash = () => {
|
|
179
308
|
if (window.location.hash !== HASH)
|
|
180
309
|
return;
|
|
@@ -209,12 +338,17 @@ const Consent = ({ cookies = 'https://hanzo.ai/cookies', privacy = 'https://hanz
|
|
|
209
338
|
};
|
|
210
339
|
hash();
|
|
211
340
|
window.addEventListener(OPEN, choose);
|
|
212
|
-
window.addEventListener(event_1.POLICY_EVENT,
|
|
341
|
+
window.addEventListener(event_1.POLICY_EVENT, policy);
|
|
342
|
+
window.addEventListener(LINKS_EVENT, look);
|
|
213
343
|
window.addEventListener('hashchange', hash);
|
|
214
344
|
document.addEventListener('click', press, { capture: true });
|
|
215
345
|
return () => {
|
|
346
|
+
window.clearTimeout(wait);
|
|
347
|
+
window.cancelAnimationFrame(frameId);
|
|
348
|
+
watch.disconnect();
|
|
216
349
|
window.removeEventListener(OPEN, choose);
|
|
217
|
-
window.removeEventListener(event_1.POLICY_EVENT,
|
|
350
|
+
window.removeEventListener(event_1.POLICY_EVENT, policy);
|
|
351
|
+
window.removeEventListener(LINKS_EVENT, look);
|
|
218
352
|
window.removeEventListener('hashchange', hash);
|
|
219
353
|
document.removeEventListener('click', press, { capture: true });
|
|
220
354
|
};
|
|
@@ -264,28 +398,53 @@ const Consent = ({ cookies = 'https://hanzo.ai/cookies', privacy = 'https://hanz
|
|
|
264
398
|
document.addEventListener('keydown', keys);
|
|
265
399
|
return () => document.removeEventListener('keydown', keys);
|
|
266
400
|
}, [choosing]);
|
|
267
|
-
if (!shown)
|
|
268
|
-
return null;
|
|
269
401
|
const done = (fn) => () => {
|
|
270
402
|
fn();
|
|
271
403
|
asked.current = false;
|
|
272
|
-
setShown(
|
|
404
|
+
setShown(front());
|
|
273
405
|
setChoosing(false);
|
|
274
406
|
};
|
|
275
|
-
if (choosing)
|
|
407
|
+
if (choosing) {
|
|
408
|
+
// A sheet opened before cloud answered speaks the strict rule.
|
|
409
|
+
const strict = (mode ?? 'eu') === 'eu';
|
|
410
|
+
// eu holds every switch off for a browser that refused; elsewhere only the sale and sharing.
|
|
411
|
+
const held = (key) => locked && (strict || key === 'ads');
|
|
412
|
+
// Under eu the switches wait for Save preferences; elsewhere each one is an opt-out, saved as it moves.
|
|
413
|
+
const flip = (key, on) => {
|
|
414
|
+
const next = { ...draft, [key]: on };
|
|
415
|
+
setDraft(next);
|
|
416
|
+
if (!strict)
|
|
417
|
+
(0, event_1.saveConsent)(next);
|
|
418
|
+
};
|
|
276
419
|
return ((0, jsx_runtime_1.jsxs)(gui_1.YStack, { ...(0, slot_1.slot)('consent-sheet'), ...COVER, z: "var(--z-modal, 600)", items: "center", justify: "center",
|
|
277
420
|
// The device's insets are physical sides, so the shorthand names them in order.
|
|
278
|
-
style: { padding: `${EDGE.top} ${EDGE.right} ${EDGE.bottom} ${EDGE.left}` }, children: [(0, jsx_runtime_1.jsx)(gui_1.YStack, { ...COVER, position: "absolute", z: 0, bg: "rgba(0,0,0,0.6)", "aria-hidden": true, onPress: close }), (0, jsx_runtime_1.jsxs)(gui_1.YStack, { ...(0, slot_1.slot)('consent'), ...PANEL, ref: sheet, role: "dialog", "aria-modal": true, "aria-labelledby": `${id}-title`, position: "relative", width: "100%", maxW: 560, maxH: "100%", overflow: "hidden", children: [(0, jsx_runtime_1.jsxs)(gui_1.XStack, { items: "center", justify: "space-between", gap: "$3", pt: "$5", pb: "$3", paddingInlineStart: "$5", paddingInlineEnd: "$4", children: [(0, jsx_runtime_1.jsx)(gui_1.Text, { render: "h2", id: `${id}-title`, m: 0, fontSize: "$6", lineHeight: "$6", fontWeight: "600", color: "$ink", children: "Cookie Preferences" }), (0, jsx_runtime_1.jsx)(button_1.Button, { type: "button", size: "icon
|
|
421
|
+
style: { padding: `${EDGE.top} ${EDGE.right} ${EDGE.bottom} ${EDGE.left}` }, children: [(0, jsx_runtime_1.jsx)(gui_1.YStack, { ...COVER, position: "absolute", z: 0, bg: "rgba(0,0,0,0.6)", "aria-hidden": true, onPress: close }), (0, jsx_runtime_1.jsxs)(gui_1.YStack, { ...(0, slot_1.slot)('consent'), ...PANEL, ref: sheet, role: "dialog", "aria-modal": true, "aria-labelledby": `${id}-title`, position: "relative", width: "100%", maxW: 560, maxH: "100%", overflow: "hidden", children: [(0, jsx_runtime_1.jsxs)(gui_1.XStack, { items: "center", justify: "space-between", gap: "$3", pt: "$5", pb: "$3", paddingInlineStart: "$5", paddingInlineEnd: "$4", children: [(0, jsx_runtime_1.jsx)(gui_1.Text, { render: "h2", id: `${id}-title`, m: 0, fontSize: "$6", lineHeight: "$6", fontWeight: "600", color: "$ink", children: "Cookie Preferences" }), (0, jsx_runtime_1.jsx)(button_1.Button, { type: "button", size: "icon", ...CLOSE, "aria-label": "Close", onClick: close, children: (0, jsx_runtime_1.jsx)(lucide_icons_2_1.X, { size: 16 }) })] }), (0, jsx_runtime_1.jsxs)(gui_1.YStack, { shrink: 1, minH: 0, overflowY: "auto", px: "$5", pb: "$2", style: { overscrollBehavior: 'contain' }, children: [(0, jsx_runtime_1.jsxs)(gui_1.Paragraph, { size: "$2", color: "$aside", m: 0, pb: "$3", children: [intro(strict), (0, jsx_runtime_1.jsx)("a", { href: cookies, style: INLINE, children: "Cookie Policy" }), ', ', (0, jsx_runtime_1.jsx)("a", { href: privacy, style: INLINE, children: "Privacy Policy" }), ', and ', (0, jsx_runtime_1.jsx)("a", { href: terms, style: INLINE, children: "Terms of Service" }), "."] }), locked ? ((0, jsx_runtime_1.jsx)(gui_1.Paragraph, { size: "$2", color: "$ink", m: 0, pb: "$3", children: refusal(strict) })) : null, kinds(brand, strict).map((k) => {
|
|
279
422
|
const open = expanded[k.id] === true;
|
|
280
423
|
const body = `${id}-${k.id}`;
|
|
281
|
-
return ((0, jsx_runtime_1.jsxs)(gui_1.YStack, { ...(0, slot_1.slot)('consent-section'), borderTopWidth: 1, borderColor: "$borderColor", children: [(0, jsx_runtime_1.jsxs)(gui_1.XStack, { items: "center", gap: "$3", minH: 52, py: "$2", children: [(0, jsx_runtime_1.jsx)(gui_1.XStack, { render: "h3", flex: 1, minW: 0, m: 0, children: (0, jsx_runtime_1.jsxs)(gui_1.XStack, { render: "button", ...BUTTON, "aria-expanded": open, "aria-controls": body, onPress: () => setExpanded({ ...expanded, [k.id]: !open }), flex: 1, minW: 0, items: "center", gap: "$2", p: 0, bg: "transparent", borderWidth: 0, cursor: "pointer", children: [(0, jsx_runtime_1.jsx)(gui_1.XStack, { shrink: 0, rotate: open ? '180deg' : '0deg', opacity: 0.7, children: (0, jsx_runtime_1.jsx)(lucide_icons_2_1.ChevronDown, { size: 16 }) }), (0, jsx_runtime_1.jsx)(gui_1.Text, { fontSize: "$3", lineHeight: "$3", fontWeight: "600", color: "$ink", ...(0, dir_1.align)('start'), children: k.title })] }) }), k.key ? ((0, jsx_runtime_1.jsx)(switch_1.Switch, { "aria-label": k.title, "aria-describedby": body, disabled:
|
|
282
|
-
})] }), (0, jsx_runtime_1.
|
|
283
|
-
|
|
424
|
+
return ((0, jsx_runtime_1.jsxs)(gui_1.YStack, { ...(0, slot_1.slot)('consent-section'), borderTopWidth: 1, borderColor: "$borderColor", children: [(0, jsx_runtime_1.jsxs)(gui_1.XStack, { items: "center", gap: "$3", minH: 52, py: "$2", children: [(0, jsx_runtime_1.jsx)(gui_1.XStack, { render: "h3", flex: 1, minW: 0, m: 0, children: (0, jsx_runtime_1.jsxs)(gui_1.XStack, { render: "button", ...BUTTON, "aria-expanded": open, "aria-controls": body, onPress: () => setExpanded({ ...expanded, [k.id]: !open }), flex: 1, minW: 0, items: "center", gap: "$2", p: 0, bg: "transparent", borderWidth: 0, cursor: "pointer", children: [(0, jsx_runtime_1.jsx)(gui_1.XStack, { shrink: 0, rotate: open ? '180deg' : '0deg', opacity: 0.7, children: (0, jsx_runtime_1.jsx)(lucide_icons_2_1.ChevronDown, { size: 16 }) }), (0, jsx_runtime_1.jsx)(gui_1.Text, { fontSize: "$3", lineHeight: "$3", fontWeight: "600", color: "$ink", ...(0, dir_1.align)('start'), children: k.title })] }) }), k.key ? ((0, jsx_runtime_1.jsx)(switch_1.Switch, { "aria-label": k.title, "aria-describedby": body, disabled: held(k.key), checked: held(k.key) ? false : draft[k.key], onCheckedChange: (v) => flip(k.key, v === true) })) : ((0, jsx_runtime_1.jsx)(gui_1.Text, { fontSize: "$2", fontWeight: "600", color: "$ink", whiteSpace: "nowrap", children: "Always Active" }))] }), (0, jsx_runtime_1.jsx)(gui_1.YStack, { id: body, display: open ? 'flex' : 'none', pb: "$3", paddingInlineStart: 24, children: (0, jsx_runtime_1.jsx)(gui_1.Paragraph, { size: "$2", color: "$aside", m: 0, children: k.body }) })] }, k.id));
|
|
425
|
+
})] }), (0, jsx_runtime_1.jsx)(gui_1.XStack, { flexWrap: "wrap", gap: "$2", px: "$5", py: "$4", borderTopWidth: 1, borderColor: "$borderColor", children: strict ? ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [locked ? null : ((0, jsx_runtime_1.jsx)(button_1.Button, { type: "button", ...SECONDARY, grow: 1, onClick: done(event_1.acceptAll), children: "Accept all" })), (0, jsx_runtime_1.jsx)(button_1.Button, { type: "button", ...PRIMARY, grow: 1, onClick: done(() => (0, event_1.saveConsent)(draft)), children: "Save preferences" })] })) : ((0, jsx_runtime_1.jsx)(button_1.Button, { type: "button", ...PRIMARY, grow: 1, onClick: close, children: "Done" })) })] })] }));
|
|
426
|
+
}
|
|
427
|
+
if (!mode)
|
|
428
|
+
return null;
|
|
429
|
+
// The way to the sheet on a page that draws none: small, quiet, at the foot of the window.
|
|
430
|
+
const own = !hosted && !shown ? ((0, jsx_runtime_1.jsx)(gui_1.XStack, { ...(0, slot_1.slot)('consent-choices'), position: "fixed", b: EDGE.bottom, l: EDGE.left, z: "var(--z-banner, 50)", px: "$2.5", py: "$1", bg: "$background", borderWidth: 1, borderColor: "$borderColor", rounded: ROUND.button, children: (0, jsx_runtime_1.jsx)(button_1.Button, { ...CHOICES, onClick: choose, children: "Your Privacy Choices" }) })) : null;
|
|
431
|
+
if (!shown)
|
|
432
|
+
return own;
|
|
433
|
+
// The global notice: one line, the way to the choices, and a × that records it as seen.
|
|
434
|
+
if (mode === 'global')
|
|
435
|
+
return ((0, jsx_runtime_1.jsxs)(gui_1.XStack, { ...frame, role: "region", "aria-label": "Cookie notice", style: FIT, maxW: 1200, flexWrap: "wrap", items: "center", columnGap: "$4", rowGap: "$2", children: [(0, jsx_runtime_1.jsx)(gui_1.YStack, { shrink: 1, minW: 0, children: (0, jsx_runtime_1.jsx)(gui_1.Paragraph, { size: "$2", color: "$ink", m: 0, children: "We use cookies to improve your experience. By continuing to browse, you agree." }) }), (0, jsx_runtime_1.jsx)(button_1.Button, { ...CHOICES, onClick: choose, children: "Your Privacy Choices" }), (0, jsx_runtime_1.jsx)(button_1.Button, { type: "button", size: "icon", ...CLOSE, "aria-label": "Close", position: "absolute", t: "$3", insetInlineEnd: "$3", onClick: done(event_1.notice), children: (0, jsx_runtime_1.jsx)(lucide_icons_2_1.X, { size: 16 }) })] }));
|
|
436
|
+
// The eu bar: the line, then the buttons. They share a row where the line fits
|
|
284
437
|
// beside them and the buttons drop under it where it does not, so a phone shows
|
|
285
438
|
// the line over one row of buttons and nothing breaks mid-control.
|
|
286
|
-
return ((0, jsx_runtime_1.jsxs)(gui_1.XStack, { ...frame, style: FIT, maxW: 1200, flexWrap: "wrap", items: "center", columnGap: "$4", rowGap: "$3", children: [(0, jsx_runtime_1.jsx)(gui_1.YStack, { shrink: 1, minW: 0, children: (0, jsx_runtime_1.jsx)(gui_1.Paragraph, { size: "$2", color: "$ink", m: 0, children: `Essential cookies keep ${brand} running. With your OK, optional cookies help us measure performance and ads.` }) }), (0, jsx_runtime_1.jsxs)(gui_1.XStack, { gap: "$2", marginInlineStart: "auto", items: "center", flexWrap: "wrap", children: [(0, jsx_runtime_1.jsx)(button_1.Button, { ref: manage, type: "button",
|
|
439
|
+
return ((0, jsx_runtime_1.jsxs)(gui_1.XStack, { ...frame, role: "dialog", "aria-label": "Cookie preferences", style: FIT, maxW: 1200, flexWrap: "wrap", items: "center", columnGap: "$4", rowGap: "$3", children: [(0, jsx_runtime_1.jsx)(gui_1.YStack, { shrink: 1, minW: 0, children: (0, jsx_runtime_1.jsx)(gui_1.Paragraph, { size: "$2", color: "$ink", m: 0, children: `Essential cookies keep ${brand} running. With your OK, optional cookies help us measure performance and ads.` }) }), (0, jsx_runtime_1.jsxs)(gui_1.XStack, { gap: "$2", marginInlineStart: "auto", items: "center", flexWrap: "wrap", children: [(0, jsx_runtime_1.jsx)(button_1.Button, { ref: manage, type: "button", ...SECONDARY, onClick: choose, children: "Manage preferences" }), (0, jsx_runtime_1.jsx)(button_1.Button, { type: "button", ...PRIMARY, onClick: done(event_1.acceptAll), children: "Accept all" })] }), (0, jsx_runtime_1.jsx)(button_1.Button, { type: "button", size: "icon", ...CLOSE, "aria-label": "Close and use essential cookies only", position: "absolute", t: "$3", insetInlineEnd: "$3", onClick: done(event_1.rejectAll), children: (0, jsx_runtime_1.jsx)(lucide_icons_2_1.X, { size: 16 }) })] }));
|
|
287
440
|
};
|
|
288
441
|
exports.Consent = Consent;
|
|
289
|
-
/**
|
|
290
|
-
|
|
442
|
+
/**
|
|
443
|
+
* "Your Privacy Choices" with California's privacy options icon: the link to the
|
|
444
|
+
* sheet, for a footer. A page that mounts one gets no second link from `Consent`.
|
|
445
|
+
*/
|
|
446
|
+
const ConsentLink = ({ label = 'Your Privacy Choices' }) => {
|
|
447
|
+
(0, react_1.useEffect)(() => hold(), []);
|
|
448
|
+
return ((0, jsx_runtime_1.jsx)(button_1.Button, { ...(0, slot_1.slot)('consent-link'), ...CHOICES, onClick: exports.openConsent, children: label }));
|
|
449
|
+
};
|
|
291
450
|
exports.ConsentLink = ConsentLink;
|
|
@@ -11,7 +11,10 @@ export type ConsentProps = {
|
|
|
11
11
|
brand?: string;
|
|
12
12
|
};
|
|
13
13
|
declare const Consent: ({ cookies, privacy, terms, brand, }: ConsentProps) => import("react").JSX.Element | null;
|
|
14
|
-
/**
|
|
14
|
+
/**
|
|
15
|
+
* "Your Privacy Choices" with California's privacy options icon: the link to the
|
|
16
|
+
* sheet, for a footer. A page that mounts one gets no second link from `Consent`.
|
|
17
|
+
*/
|
|
15
18
|
declare const ConsentLink: ({ label }: {
|
|
16
19
|
label?: string;
|
|
17
20
|
}) => import("react").JSX.Element;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Consent.d.ts","sourceRoot":"","sources":["../../src/consent/Consent.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"Consent.d.ts","sourceRoot":"","sources":["../../src/consent/Consent.tsx"],"names":[],"mappings":"AA+DA,uCAAuC;AACvC,eAAO,MAAM,WAAW,QAAO,IAE9B,CAAA;AAsMD,MAAM,MAAM,YAAY,GAAG;IACzB,qCAAqC;IACrC,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,sCAAsC;IACtC,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,uCAAuC;IACvC,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,kDAAkD;IAClD,KAAK,CAAC,EAAE,MAAM,CAAA;CACf,CAAA;AAED,QAAA,MAAM,OAAO,wCAKV,YAAY,uCA0Zd,CAAA;AAED;;;GAGG;AACH,QAAA,MAAM,WAAW,cAAwC;IAAE,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,gCAO1E,CAAA;AAED,OAAO,EAAE,OAAO,EAAE,WAAW,EAAE,CAAA"}
|