@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.
@@ -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; 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 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,39 @@ 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 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: '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.',
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
- /** The panel both wear: the card's ground, edge, corner (`$6`, the sign-in card's) and floating shadow. */
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: '$6',
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 32px, and 12px either side.
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 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 } };
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 off.
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((0, event_1.asks)());
266
+ setShown(front());
162
267
  };
163
268
  (0, react_1.useEffect)(() => {
164
269
  const sync = () => {
165
- setShown((0, event_1.asks)() || asked.current);
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 are off until chosen again.
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
- sync();
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 choices and a link in
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, sync);
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, sync);
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(false);
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-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) => {
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: 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
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", 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 }) })] }));
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
- /** 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 }));
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
- /** 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;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"}