@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.
@@ -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: no
10
- * Google Analytics, no Meta pixel and no ad personalization before Accept, and
11
- * Google Consent Mode v2 starts denied. The brand's own first-party analytics is
12
- * strictly necessary and runs for every visitor; the choice does not govern it.
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
- * - every visitor, wherever they are, is asked: a bar with Manage preferences,
15
- * Accept all and a ×, until they have chosen. Nothing optional loads first.
16
- * - the bar's × is a choice, not a dismissal: it stores essential cookies only
17
- * and closes, so refusing is one press, exactly as accepting is.
18
- * - Manage preferences opens the Cookie Preferences sheet: a modal with one
19
- * section per category, Strictly Necessary always on, the rest switches that
20
- * start off until chosen, and the policy links.
21
- * - Global Privacy Control or Do Not Track is the visitor's answer already: they
22
- * are not asked, nothing optional runs, and the switches are locked off.
23
- * - cloud serves the policy with the site's tag config; the bar redraws when it
24
- * arrives, and asks again when the policy version changes.
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 choices) open the sheet from anywhere.
28
- * `brand` names who is asking; `cookies`, `privacy` and `terms` are the brand's
29
- * own policies.
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
- const kinds = (brand) => [
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: 'Lets Google and Meta use your visit 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: it stays off unless you turn it on.',
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
- /** The panel both wear: the card's ground, edge, corner (`$6`, the sign-in card's) and floating shadow. */
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: '$6',
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 32px, and 12px either side.
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 one filled control in the bar and in the sheet: the ink, as the sign-in card's Continue. */
115
- const ACT = { bg: '$ink', color: '$background', borderColor: '$ink', hoverStyle: { opacity: 0.92 } };
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 off.
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((0, event_1.asks)());
284
+ setShown(front());
162
285
  };
163
286
  (0, react_1.useEffect)(() => {
164
287
  const sync = () => {
165
- setShown((0, event_1.asks)() || asked.current);
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 are off until chosen again.
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
- sync();
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 choices and a link in
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, sync);
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, sync);
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(false);
432
+ setShown(front());
273
433
  setChoosing(false);
274
434
  };
275
- if (choosing)
276
- 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",
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-sm", variant: "ghost", "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: ['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 ', (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: "Your browser asks sites not to track you, so all of these stay off." })) : null, kinds(brand).map((k) => {
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: locked, checked: locked ? false : draft[k.key], onCheckedChange: (v) => setDraft({ ...draft, [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));
282
- })] }), (0, jsx_runtime_1.jsxs)(gui_1.XStack, { flexWrap: "wrap", gap: "$2", px: "$5", py: "$4", borderTopWidth: 1, borderColor: "$borderColor", children: [locked ? null : ((0, jsx_runtime_1.jsx)(button_1.Button, { type: "button", variant: "secondary", grow: 1, onClick: done(event_1.acceptAll), children: "Accept all" })), (0, jsx_runtime_1.jsx)(button_1.Button, { type: "button", ...ACT, grow: 1, onClick: done(() => (0, event_1.saveConsent)(draft)), children: "Save preferences" })] })] })] }));
283
- // One bar: the line, then the buttons. They share a row where the line fits
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
- 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", size: "sm", variant: "secondary", onClick: choose, children: "Manage preferences" }), (0, jsx_runtime_1.jsx)(button_1.Button, { type: "button", size: "sm", ...ACT, onClick: done(event_1.acceptAll), children: "Accept all" })] }), (0, jsx_runtime_1.jsx)(button_1.Button, { type: "button", size: "icon-sm", variant: "ghost", "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 }) })] }));
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
- /** The footer link that reopens the choices, in every footer and every region. */
290
- const ConsentLink = ({ label = 'Cookie settings' }) => ((0, jsx_runtime_1.jsx)(button_1.Button, { ...(0, slot_1.slot)('consent-link'), type: "button", variant: "link", size: "sm", px: 0, minH: 0, onClick: exports.openConsent, children: label }));
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
- /** The footer link that reopens the choices, in every footer and every region. */
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":"AA0CA,uCAAuC;AACvC,eAAO,MAAM,WAAW,QAAO,IAE9B,CAAA;AA0FD,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,uCA4Sd,CAAA;AAED,kFAAkF;AAClF,QAAA,MAAM,WAAW,cAAmC;IAAE,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,gCAIrE,CAAA;AAED,OAAO,EAAE,OAAO,EAAE,WAAW,EAAE,CAAA"}
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"}