@hanzo/ui 8.29.15 → 8.29.19
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 +241 -50
- package/dist/consent/Consent.d.ts +4 -1
- package/dist/consent/Consent.d.ts.map +1 -1
- package/dist/consent/Consent.js +243 -52
- 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; what a third party may do meanwhile is @hanzo/event's
|
|
17
|
+
* rule (consent.ts, tags.ts). The × is a choice, not a dismissal: essential
|
|
18
|
+
* cookies only, so refusing is one press, 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,51 @@ 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 on this site to #choices (a footer's). */
|
|
83
|
+
function linked() {
|
|
84
|
+
const g = globalThis;
|
|
85
|
+
if ((g[LINKS] ?? 0) > 0)
|
|
86
|
+
return true;
|
|
87
|
+
for (const a of document.querySelectorAll(`a[href*="${HASH}"]`)) {
|
|
88
|
+
try {
|
|
89
|
+
const to = new URL(a.getAttribute('href') ?? '', document.baseURI);
|
|
90
|
+
if (to.hash === HASH && to.origin === window.location.origin)
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
/* an address that does not parse leads nowhere */
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
const kinds = (brand, strict) => [
|
|
49
100
|
{
|
|
50
101
|
id: 'necessary',
|
|
51
102
|
title: 'Strictly Necessary Cookies',
|
|
@@ -67,9 +118,17 @@ const kinds = (brand) => [
|
|
|
67
118
|
id: 'ads',
|
|
68
119
|
key: 'ads',
|
|
69
120
|
title: 'Sell, Share, or Use Your Data for Targeted Advertising',
|
|
70
|
-
body:
|
|
121
|
+
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
122
|
},
|
|
72
123
|
];
|
|
124
|
+
/** The sheet's opening line, by rule: what runs before the visitor does anything, and how to change it. */
|
|
125
|
+
const intro = (strict) => strict
|
|
126
|
+
? '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 '
|
|
127
|
+
: '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 ';
|
|
128
|
+
/** What a browser that sends Global Privacy Control or Do Not Track holds off, by rule. */
|
|
129
|
+
const refusal = (strict) => strict
|
|
130
|
+
? 'Your browser asks sites not to track you, so all of these stay off.'
|
|
131
|
+
: 'Your browser asks sites not to track you, so your data is not sold or shared for targeted advertising.';
|
|
73
132
|
/** 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
133
|
const EDGE = {
|
|
75
134
|
top: 'max(16px, calc(var(--safe-top, env(safe-area-inset-top, 0px)) + 8px))',
|
|
@@ -77,12 +136,18 @@ const EDGE = {
|
|
|
77
136
|
left: 'max(16px, calc(var(--safe-left, env(safe-area-inset-left, 0px)) + 8px))',
|
|
78
137
|
right: 'max(16px, calc(var(--safe-right, env(safe-area-inset-right, 0px)) + 8px))',
|
|
79
138
|
};
|
|
80
|
-
/**
|
|
139
|
+
/**
|
|
140
|
+
* Corners from design's ladder (radius.css), one step apart so the buttons sit
|
|
141
|
+
* concentric in their card: a button on `--radius-lg` (12px), the bar and the
|
|
142
|
+
* sheet on `--radius-xl` (16px). Both follow `--radius-scale`; neither is a pill.
|
|
143
|
+
*/
|
|
144
|
+
const ROUND = { button: 'var(--radius-lg, 12px)', card: 'var(--radius-xl, 16px)' };
|
|
145
|
+
/** The panel both wear: the card's ground, edge, corner and floating shadow. */
|
|
81
146
|
const PANEL = {
|
|
82
147
|
bg: '$background',
|
|
83
148
|
borderWidth: 1,
|
|
84
149
|
borderColor: '$borderColor',
|
|
85
|
-
rounded:
|
|
150
|
+
rounded: ROUND.card,
|
|
86
151
|
shadowColor: 'rgba(0,0,0,0.25)',
|
|
87
152
|
shadowRadius: 24,
|
|
88
153
|
shadowOffset: { width: 0, height: 8 },
|
|
@@ -96,8 +161,6 @@ const PANEL = {
|
|
|
96
161
|
const frame = {
|
|
97
162
|
...(0, slot_1.slot)('consent'),
|
|
98
163
|
...PANEL,
|
|
99
|
-
role: 'dialog',
|
|
100
|
-
'aria-label': 'Cookie preferences',
|
|
101
164
|
position: 'fixed',
|
|
102
165
|
b: EDGE.bottom,
|
|
103
166
|
l: EDGE.left,
|
|
@@ -106,13 +169,59 @@ const frame = {
|
|
|
106
169
|
z: 'var(--z-banner, 50)',
|
|
107
170
|
py: '$3',
|
|
108
171
|
paddingInlineStart: '$4',
|
|
109
|
-
// Clear of the × in the corner: its
|
|
172
|
+
// Clear of the × in the corner: its 36px, 12px outside it and 8px before it.
|
|
110
173
|
paddingInlineEnd: 56,
|
|
111
174
|
};
|
|
112
175
|
/** The bar is as wide as its line and buttons, up to the window less its inset. */
|
|
113
176
|
const FIT = { width: 'fit-content' };
|
|
114
|
-
/** The
|
|
115
|
-
const
|
|
177
|
+
/** The keyboard's mark on any control here: design's focus ring, outside the control. */
|
|
178
|
+
const RING = { outlineColor: 'var(--ring)', outlineStyle: 'solid', outlineWidth: 2, outlineOffset: 2 };
|
|
179
|
+
/**
|
|
180
|
+
* A button's every state from design's button tokens (colors.css), so it follows
|
|
181
|
+
* the theme and an org's accent: at rest the fill, under the pointer and pressed
|
|
182
|
+
* its `-hover` step, focused by keyboard the ring. Each state names its fill and
|
|
183
|
+
* a clear edge outright, because a state that leaves one out takes the Button's
|
|
184
|
+
* own (the frame's hover is a near-black fill and a grey edge, which put dark
|
|
185
|
+
* text on dark under the cursor). 13px type on the 36px floor.
|
|
186
|
+
*/
|
|
187
|
+
const button = (fill, hover, ink) => ({
|
|
188
|
+
size: 'sm',
|
|
189
|
+
minH: 36,
|
|
190
|
+
rounded: ROUND.button,
|
|
191
|
+
bg: fill,
|
|
192
|
+
color: ink,
|
|
193
|
+
borderColor: 'transparent',
|
|
194
|
+
hoverStyle: { bg: hover, borderColor: 'transparent', opacity: 1 },
|
|
195
|
+
pressStyle: { bg: hover, borderColor: 'transparent', opacity: 1 },
|
|
196
|
+
focusStyle: { borderColor: 'transparent' },
|
|
197
|
+
focusVisibleStyle: RING,
|
|
198
|
+
});
|
|
199
|
+
/** The one filled control: Accept all in the bar, Save preferences (or Done) in the sheet. */
|
|
200
|
+
const PRIMARY = button('var(--primary)', 'var(--primary-hover)', 'var(--primary-foreground)');
|
|
201
|
+
/** Beside it: Manage preferences in the bar, Accept all in the sheet. */
|
|
202
|
+
const SECONDARY = button('var(--secondary)', 'var(--secondary-hover)', 'var(--secondary-foreground)');
|
|
203
|
+
/** A ×: no fill at rest, the secondary surface under the pointer. */
|
|
204
|
+
const CLOSE = {
|
|
205
|
+
rounded: ROUND.button,
|
|
206
|
+
bg: 'transparent',
|
|
207
|
+
borderColor: 'transparent',
|
|
208
|
+
hoverStyle: { bg: 'var(--secondary)', borderColor: 'transparent', opacity: 1 },
|
|
209
|
+
pressStyle: { bg: 'var(--secondary-hover)', borderColor: 'transparent', opacity: 1 },
|
|
210
|
+
focusStyle: { borderColor: 'transparent' },
|
|
211
|
+
focusVisibleStyle: RING,
|
|
212
|
+
};
|
|
213
|
+
/** "Your Privacy Choices" as a footer draws a link: the icon, then the words, quiet until pointed at. */
|
|
214
|
+
const CHOICES = {
|
|
215
|
+
type: 'button',
|
|
216
|
+
variant: 'link',
|
|
217
|
+
size: 'sm',
|
|
218
|
+
px: 0,
|
|
219
|
+
minH: 0,
|
|
220
|
+
color: '$aside',
|
|
221
|
+
hoverStyle: { color: '$ink' },
|
|
222
|
+
focusVisibleStyle: RING,
|
|
223
|
+
startIcon: (0, jsx_runtime_1.jsx)(icon_1.PrivacyChoicesIcon, {}),
|
|
224
|
+
};
|
|
116
225
|
/** A link in the sheet's intro: the line's own ink, underlined, never broken across lines. */
|
|
117
226
|
const INLINE = { color: 'inherit', textDecoration: 'underline', whiteSpace: 'nowrap' };
|
|
118
227
|
/** The sheet covers the window, on design's modal rung, over a scrim of its own. */
|
|
@@ -122,10 +231,20 @@ const FOCUSABLE = 'a[href], button:not([disabled]), [tabindex]:not([tabindex="-1
|
|
|
122
231
|
/** A section's button never submits a form the sheet sits in. gui's stack props do not name `type`. */
|
|
123
232
|
const BUTTON = { type: 'button' };
|
|
124
233
|
const Consent = ({ cookies = 'https://hanzo.ai/cookies', privacy = 'https://hanzo.ai/privacy', terms = 'https://hanzo.ai/terms', brand = 'Hanzo', }) => {
|
|
234
|
+
// The rule in force: undefined until cloud (or this tab) has said, and nothing is drawn meanwhile.
|
|
235
|
+
const [mode, setMode] = (0, react_1.useState)();
|
|
236
|
+
// The bar (eu) or the notice (global) at the foot of the window.
|
|
125
237
|
const [shown, setShown] = (0, react_1.useState)(false);
|
|
126
238
|
const [choosing, setChoosing] = (0, react_1.useState)(false);
|
|
127
239
|
const [draft, setDraft] = (0, react_1.useState)({ analytics: false, marketing: false, ads: false });
|
|
240
|
+
// The switches as they stand, for a handler registered once.
|
|
241
|
+
const latest = (0, react_1.useRef)(draft);
|
|
242
|
+
latest.current = draft;
|
|
128
243
|
const [expanded, setExpanded] = (0, react_1.useState)({});
|
|
244
|
+
// Whether the page draws its own way to the sheet; if not, the component draws one.
|
|
245
|
+
const [hosted, setHosted] = (0, react_1.useState)(true);
|
|
246
|
+
// The rule, read by handlers registered once.
|
|
247
|
+
const rule = (0, react_1.useRef)(undefined);
|
|
129
248
|
// The choices are open because the visitor asked for them. A policy that
|
|
130
249
|
// arrives after (cloud answers ~150ms into a page opened at #choices) redraws
|
|
131
250
|
// the bar's question, and must not close what was asked for.
|
|
@@ -135,12 +254,16 @@ const Consent = ({ cookies = 'https://hanzo.ai/cookies', privacy = 'https://hanz
|
|
|
135
254
|
const sheet = (0, react_1.useRef)(null);
|
|
136
255
|
const manage = (0, react_1.useRef)(null);
|
|
137
256
|
const opened = (0, react_1.useRef)(false);
|
|
138
|
-
// The switches were filled from a stored choice, not left
|
|
257
|
+
// The switches were filled from a stored choice, not left at the rule's defaults.
|
|
139
258
|
const seeded = (0, react_1.useRef)(false);
|
|
259
|
+
// The visitor has moved a switch since the sheet opened.
|
|
260
|
+
const touched = (0, react_1.useRef)(false);
|
|
140
261
|
const id = (0, react_1.useId)();
|
|
141
262
|
// The browser refused, as the page sees it or as cloud saw the request. State,
|
|
142
263
|
// so cloud's answer redraws an open sheet.
|
|
143
264
|
const [locked, setLocked] = (0, react_1.useState)(false);
|
|
265
|
+
/** What stands at the foot of the window under a rule: the eu bar until a choice, the global notice until seen. */
|
|
266
|
+
const front = (m = rule.current) => (m === 'eu' ? (0, event_1.asks)() : m === 'global' ? !(0, event_1.noticed)() : false);
|
|
144
267
|
const choose = () => {
|
|
145
268
|
// Already open: the visitor's unsaved switches and the focus to return stay as they are.
|
|
146
269
|
if (asked.current)
|
|
@@ -148,33 +271,67 @@ const Consent = ({ cookies = 'https://hanzo.ai/cookies', privacy = 'https://hanz
|
|
|
148
271
|
const at = document.activeElement;
|
|
149
272
|
back.current = at instanceof HTMLElement && at !== document.body ? at : null;
|
|
150
273
|
asked.current = true;
|
|
274
|
+
touched.current = false;
|
|
151
275
|
setLocked((0, event_1.declined)());
|
|
152
276
|
seeded.current = (0, event_1.storedConsent)();
|
|
153
277
|
setDraft((0, event_1.readConsent)());
|
|
154
278
|
setChoosing(true);
|
|
155
|
-
setShown(true);
|
|
156
279
|
};
|
|
157
|
-
// Closing without a choice: a visitor who has not chosen is asked again by the bar.
|
|
280
|
+
// Closing without a choice: under eu a visitor who has not chosen is asked again by the bar.
|
|
158
281
|
const close = () => {
|
|
159
282
|
asked.current = false;
|
|
160
283
|
setChoosing(false);
|
|
161
|
-
setShown((
|
|
284
|
+
setShown(front());
|
|
162
285
|
};
|
|
163
286
|
(0, react_1.useEffect)(() => {
|
|
164
287
|
const sync = () => {
|
|
165
|
-
setShown((
|
|
288
|
+
setShown(front());
|
|
166
289
|
setLocked((0, event_1.declined)());
|
|
167
290
|
// A policy version the stored choice was not made under: the switches the
|
|
168
|
-
// sheet opened with were an old grant, and
|
|
291
|
+
// sheet opened with were an old grant, and fall back to the rule's own.
|
|
169
292
|
if (asked.current && seeded.current && !(0, event_1.storedConsent)()) {
|
|
170
293
|
seeded.current = false;
|
|
171
294
|
setDraft((0, event_1.readConsent)());
|
|
172
295
|
}
|
|
173
296
|
};
|
|
174
|
-
|
|
297
|
+
const decide = (m) => {
|
|
298
|
+
const changed = rule.current !== m;
|
|
299
|
+
rule.current = m;
|
|
300
|
+
setMode(m);
|
|
301
|
+
sync();
|
|
302
|
+
// A rule that changes under an open sheet changes what its switches mean.
|
|
303
|
+
// Untouched, they show the new rule's defaults; moved, they are the
|
|
304
|
+
// visitor's answer, which an opt-out rule takes as said.
|
|
305
|
+
if (!changed || !asked.current)
|
|
306
|
+
return;
|
|
307
|
+
if (!touched.current)
|
|
308
|
+
setDraft((0, event_1.readConsent)());
|
|
309
|
+
else if (m !== 'eu')
|
|
310
|
+
(0, event_1.saveConsent)(latest.current);
|
|
311
|
+
};
|
|
312
|
+
// Cloud answered on this page: its rule, whether or not it placed the visitor.
|
|
313
|
+
const policy = () => decide((0, event_1.consentPolicy)().mode);
|
|
314
|
+
const known = served();
|
|
315
|
+
if (known)
|
|
316
|
+
decide(known);
|
|
317
|
+
// No answer in time is the strict answer.
|
|
318
|
+
const wait = known ? undefined : window.setTimeout(() => rule.current ?? decide('eu'), WAIT);
|
|
319
|
+
// The page's own links to the sheet, looked for again as it changes, once a frame.
|
|
320
|
+
let frameId = 0;
|
|
321
|
+
const look = () => {
|
|
322
|
+
if (frameId)
|
|
323
|
+
return;
|
|
324
|
+
frameId = window.requestAnimationFrame(() => {
|
|
325
|
+
frameId = 0;
|
|
326
|
+
setHosted(linked());
|
|
327
|
+
});
|
|
328
|
+
};
|
|
329
|
+
look();
|
|
330
|
+
const watch = new MutationObserver(look);
|
|
331
|
+
watch.observe(document.body, { childList: true, subtree: true });
|
|
175
332
|
// `#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.
|
|
333
|
+
// any link on this site to it, so the footer's Your Privacy Choices and a link
|
|
334
|
+
// in a page are the same control.
|
|
178
335
|
const hash = () => {
|
|
179
336
|
if (window.location.hash !== HASH)
|
|
180
337
|
return;
|
|
@@ -209,12 +366,17 @@ const Consent = ({ cookies = 'https://hanzo.ai/cookies', privacy = 'https://hanz
|
|
|
209
366
|
};
|
|
210
367
|
hash();
|
|
211
368
|
window.addEventListener(OPEN, choose);
|
|
212
|
-
window.addEventListener(event_1.POLICY_EVENT,
|
|
369
|
+
window.addEventListener(event_1.POLICY_EVENT, policy);
|
|
370
|
+
window.addEventListener(LINKS_EVENT, look);
|
|
213
371
|
window.addEventListener('hashchange', hash);
|
|
214
372
|
document.addEventListener('click', press, { capture: true });
|
|
215
373
|
return () => {
|
|
374
|
+
window.clearTimeout(wait);
|
|
375
|
+
window.cancelAnimationFrame(frameId);
|
|
376
|
+
watch.disconnect();
|
|
216
377
|
window.removeEventListener(OPEN, choose);
|
|
217
|
-
window.removeEventListener(event_1.POLICY_EVENT,
|
|
378
|
+
window.removeEventListener(event_1.POLICY_EVENT, policy);
|
|
379
|
+
window.removeEventListener(LINKS_EVENT, look);
|
|
218
380
|
window.removeEventListener('hashchange', hash);
|
|
219
381
|
document.removeEventListener('click', press, { capture: true });
|
|
220
382
|
};
|
|
@@ -264,28 +426,57 @@ const Consent = ({ cookies = 'https://hanzo.ai/cookies', privacy = 'https://hanz
|
|
|
264
426
|
document.addEventListener('keydown', keys);
|
|
265
427
|
return () => document.removeEventListener('keydown', keys);
|
|
266
428
|
}, [choosing]);
|
|
267
|
-
if (!shown)
|
|
268
|
-
return null;
|
|
269
429
|
const done = (fn) => () => {
|
|
270
430
|
fn();
|
|
271
431
|
asked.current = false;
|
|
272
|
-
setShown(
|
|
432
|
+
setShown(front());
|
|
273
433
|
setChoosing(false);
|
|
274
434
|
};
|
|
275
|
-
|
|
276
|
-
|
|
435
|
+
// The sheet, drawn over whatever opened it, which stays mounted so focus can go back to it.
|
|
436
|
+
let panel = null;
|
|
437
|
+
if (choosing) {
|
|
438
|
+
// A sheet opened before cloud answered speaks the strict rule.
|
|
439
|
+
const strict = (mode ?? 'eu') === 'eu';
|
|
440
|
+
// eu holds every switch off for a browser that refused; elsewhere only the sale and sharing.
|
|
441
|
+
const held = (key) => locked && (strict || key === 'ads');
|
|
442
|
+
// Under eu the switches wait for Save preferences; elsewhere each one is an opt-out, saved as it moves.
|
|
443
|
+
const flip = (key, on) => {
|
|
444
|
+
touched.current = true;
|
|
445
|
+
const next = { ...draft, [key]: on };
|
|
446
|
+
setDraft(next);
|
|
447
|
+
if (!strict)
|
|
448
|
+
(0, event_1.saveConsent)(next);
|
|
449
|
+
};
|
|
450
|
+
panel = ((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
451
|
// 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
|
|
452
|
+
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
453
|
const open = expanded[k.id] === true;
|
|
280
454
|
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
|
-
|
|
455
|
+
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));
|
|
456
|
+
})] }), (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" })) })] })] }));
|
|
457
|
+
}
|
|
458
|
+
if (!mode)
|
|
459
|
+
return panel;
|
|
460
|
+
// The way to the sheet on a page that draws none: small, quiet, at the foot of the window.
|
|
461
|
+
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;
|
|
462
|
+
let surface = own;
|
|
463
|
+
// The global notice: one line, the way to the choices, and a × that records it as seen.
|
|
464
|
+
if (shown && mode === 'global')
|
|
465
|
+
surface = ((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 }) })] }));
|
|
466
|
+
// The eu bar: the line, then the buttons. They share a row where the line fits
|
|
284
467
|
// beside them and the buttons drop under it where it does not, so a phone shows
|
|
285
468
|
// the line over one row of buttons and nothing breaks mid-control.
|
|
286
|
-
|
|
469
|
+
else if (shown && mode === 'eu')
|
|
470
|
+
surface = ((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 }) })] }));
|
|
471
|
+
return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [surface, panel] }));
|
|
287
472
|
};
|
|
288
473
|
exports.Consent = Consent;
|
|
289
|
-
/**
|
|
290
|
-
|
|
474
|
+
/**
|
|
475
|
+
* "Your Privacy Choices" with California's privacy options icon: the link to the
|
|
476
|
+
* sheet, for a footer. A page that mounts one gets no second link from `Consent`.
|
|
477
|
+
*/
|
|
478
|
+
const ConsentLink = ({ label = 'Your Privacy Choices' }) => {
|
|
479
|
+
(0, react_1.useEffect)(() => hold(), []);
|
|
480
|
+
return ((0, jsx_runtime_1.jsx)(button_1.Button, { ...(0, slot_1.slot)('consent-link'), ...CHOICES, onClick: exports.openConsent, children: label }));
|
|
481
|
+
};
|
|
291
482
|
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;AA+MD,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,uCAkbd,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"}
|