@hanzo/ui 8.29.11 → 8.29.13

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.
@@ -10,23 +10,34 @@ const jsx_runtime_1 = require("react/jsx-runtime");
10
10
  * nothing from an ad platform, no Google, and no event on our own stream before
11
11
  * Accept, and Google Consent Mode v2 starts denied.
12
12
  *
13
- * - every visitor, wherever they are, is asked: a banner with Accept all,
14
- * Reject all and Choose, until they have chosen. Nothing optional loads first.
13
+ * - every visitor, wherever they are, is asked: a bar with Manage preferences,
14
+ * Accept all and a ×, until they have chosen. Nothing optional loads first.
15
+ * - the bar's × is a choice, not a dismissal: it stores essential cookies only
16
+ * and closes, so refusing is one press, exactly as accepting is.
17
+ * - Manage preferences opens the Cookie Preferences sheet: a modal with one
18
+ * section per category, Strictly Necessary always on, the rest switches that
19
+ * start off until chosen, and the policy links.
15
20
  * - Global Privacy Control or Do Not Track is the visitor's answer already: they
16
21
  * are not asked, nothing optional runs, and the switches are locked off.
17
- * - cloud serves the policy with the site's tag config; the banner redraws when
18
- * it arrives, and asks again when the policy version changes.
22
+ * - cloud serves the policy with the site's tag config; the bar redraws when it
23
+ * arrives, and asks again when the policy version changes.
19
24
  *
20
- * `Consent` mounts once near the root; `ConsentLink` goes wherever a footer
21
- * link goes and opens the panel from anywhere. `brand` names who is asking.
25
+ * `Consent` mounts once near the root. `openConsent()`, `ConsentLink` and any
26
+ * link to `#choices` (the footer's Privacy choices) open the sheet from anywhere.
27
+ * `brand` names who is asking; `cookies`, `privacy` and `terms` are the brand's
28
+ * own policies.
22
29
  */
23
30
  const gui_1 = require("@hanzo/gui");
31
+ const lucide_icons_2_1 = require("@hanzogui/lucide-icons-2");
24
32
  const event_1 = require("@hanzo/event");
25
33
  const react_1 = require("react");
26
34
  const button_1 = require("../backends/gui/button.cjs");
35
+ const dir_1 = require("../backends/gui/dir.cjs");
27
36
  const switch_1 = require("../backends/gui/switch.cjs");
28
37
  const slot_1 = require("../backends/gui/slot.cjs");
29
38
  const OPEN = 'hzconsent-open';
39
+ /** The address that opens the sheet, from any link on any page. */
40
+ const HASH = '#choices';
30
41
  /** Opens the choices from anywhere. */
31
42
  const openConsent = () => {
32
43
  if (typeof window !== 'undefined')
@@ -34,24 +45,56 @@ const openConsent = () => {
34
45
  };
35
46
  exports.openConsent = openConsent;
36
47
  const kinds = (brand) => [
37
- { key: 'analytics', title: 'Analytics', body: `Counts visits and finds what is broken, so ${brand} can get better.` },
38
- { key: 'marketing', title: 'Marketing', body: 'Lets ad platforms measure the sign-ups and orders their ads led to.' },
39
- { key: 'ads', title: 'Personalized ads', body: 'Lets those platforms use your visit to show you ads elsewhere.' },
48
+ {
49
+ id: 'necessary',
50
+ title: 'Strictly Necessary Cookies',
51
+ body: 'These cookies are needed for the site to work, so they are always on. They keep you signed in and your session secure, protect forms against cross-site request forgery, spread traffic across servers, remember the choice you make here, and keep what you have typed into a form. They are not used to track you, and they store no personally identifiable tracking information.',
52
+ },
53
+ {
54
+ id: 'analytics',
55
+ key: 'analytics',
56
+ title: 'Performance Cookies',
57
+ body: `These cookies count visits and where they come from, so ${brand} can measure and improve how the site performs: which pages are used most and least, how visitors move around, and what is broken. What they collect is aggregated.`,
58
+ },
59
+ {
60
+ id: 'marketing',
61
+ key: 'marketing',
62
+ title: 'Marketing Cookies',
63
+ body: `These cookies let advertising platforms measure the sign-ups and orders their ads led to, so ${brand} can tell which ads work. The platforms set them through this site, and they remember the ad you arrived from.`,
64
+ },
65
+ {
66
+ id: 'ads',
67
+ key: 'ads',
68
+ title: 'Sell, Share, or Use Your Data for Targeted Advertising',
69
+ body: 'Some US state privacy laws treat letting advertising platforms use what they learn from your visit to show you ads on other sites and apps as a “sale” or “sharing” of your personal information, or as “targeted advertising”, and give you the right to opt out. On, those platforms may use your visit to choose the ads you see elsewhere. It stays off unless you turn it on; to opt out, move this toggle to the left and select Save preferences.',
70
+ },
40
71
  ];
41
- /** The banner's inset from the window's foot and sides: 16px, or the device's inset and 8px where that is wider. */
72
+ /** 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. */
42
73
  const EDGE = {
74
+ top: 'max(16px, calc(var(--safe-top, env(safe-area-inset-top, 0px)) + 8px))',
43
75
  bottom: 'max(16px, calc(var(--safe-bottom, env(safe-area-inset-bottom, 0px)) + 8px))',
44
76
  left: 'max(16px, calc(var(--safe-left, env(safe-area-inset-left, 0px)) + 8px))',
45
77
  right: 'max(16px, calc(var(--safe-right, env(safe-area-inset-right, 0px)) + 8px))',
46
78
  };
79
+ /** The panel both wear: the card's ground, edge, corner (`$6`, the sign-in card's) and floating shadow. */
80
+ const PANEL = {
81
+ bg: '$background',
82
+ borderWidth: 1,
83
+ borderColor: '$borderColor',
84
+ rounded: '$6',
85
+ shadowColor: 'rgba(0,0,0,0.25)',
86
+ shadowRadius: 24,
87
+ shadowOffset: { width: 0, height: 8 },
88
+ };
47
89
  /**
48
- * Where the banner sits: fixed to the foot of the window, clear of the home
90
+ * Where the bar sits: fixed to the foot of the window, clear of the home
49
91
  * indicator and the notch, centred. It stands on design's banner rung
50
92
  * (`--z-banner`): over the page and its dock, under every overlay — a dialog, a
51
93
  * menu, a phone drawer — and is answered once they close.
52
94
  */
53
95
  const frame = {
54
96
  ...(0, slot_1.slot)('consent'),
97
+ ...PANEL,
55
98
  role: 'dialog',
56
99
  'aria-label': 'Cookie preferences',
57
100
  position: 'fixed',
@@ -60,46 +103,166 @@ const frame = {
60
103
  r: EDGE.right,
61
104
  mx: 'auto',
62
105
  z: 'var(--z-banner, 50)',
63
- py: '$2.5',
64
- px: '$3.5',
65
- rounded: '$5',
66
- borderWidth: 1,
67
- borderColor: '$borderColor',
68
- bg: '$background',
69
- shadowColor: 'rgba(0,0,0,0.25)',
70
- shadowRadius: 24,
71
- shadowOffset: { width: 0, height: 8 },
106
+ py: '$3',
107
+ paddingInlineStart: '$4',
108
+ // Clear of the × in the corner: its 32px, and 12px either side.
109
+ paddingInlineEnd: 56,
72
110
  };
73
111
  /** The bar is as wide as its line and buttons, up to the window less its inset. */
74
112
  const FIT = { width: 'fit-content' };
75
- /** A link in the banner's line: the line's own ink, underlined, never broken across lines. */
113
+ /** The one filled control in the bar and in the sheet: the ink, as the sign-in card's Continue. */
114
+ const ACT = { bg: '$ink', color: '$background', borderColor: '$ink', hoverStyle: { opacity: 0.92 } };
115
+ /** A link in the sheet's intro: the line's own ink, underlined, never broken across lines. */
76
116
  const INLINE = { color: 'inherit', textDecoration: 'underline', whiteSpace: 'nowrap' };
77
- const Consent = ({ privacy = 'https://hanzo.ai/privacy', brand = 'Hanzo' }) => {
117
+ /** The sheet covers the window, on design's modal rung, over a scrim of its own. */
118
+ const COVER = { position: 'fixed', t: 0, r: 0, b: 0, l: 0 };
119
+ /** What Tab may land on inside the sheet. */
120
+ const FOCUSABLE = 'a[href], button:not([disabled]), [tabindex]:not([tabindex="-1"])';
121
+ /** A section's button never submits a form the sheet sits in. gui's stack props do not name `type`. */
122
+ const BUTTON = { type: 'button' };
123
+ const Consent = ({ cookies = 'https://hanzo.ai/cookies', privacy = 'https://hanzo.ai/privacy', terms = 'https://hanzo.ai/terms', brand = 'Hanzo', }) => {
78
124
  const [shown, setShown] = (0, react_1.useState)(false);
79
125
  const [choosing, setChoosing] = (0, react_1.useState)(false);
80
126
  const [draft, setDraft] = (0, react_1.useState)({ analytics: false, marketing: false, ads: false });
127
+ const [expanded, setExpanded] = (0, react_1.useState)({});
81
128
  // The choices are open because the visitor asked for them. A policy that
82
129
  // arrives after (cloud answers ~150ms into a page opened at #choices) redraws
83
- // the banner's question, and must not close what was asked for.
130
+ // the bar's question, and must not close what was asked for.
84
131
  const asked = (0, react_1.useRef)(false);
85
- // The browser refused, as the page sees it or as cloud saw the request.
86
- const locked = typeof window !== 'undefined' && (0, event_1.declined)();
132
+ // What had focus when the sheet opened, given it back when the sheet closes.
133
+ const back = (0, react_1.useRef)(null);
134
+ const sheet = (0, react_1.useRef)(null);
135
+ const manage = (0, react_1.useRef)(null);
136
+ const opened = (0, react_1.useRef)(false);
137
+ // The switches were filled from a stored choice, not left off.
138
+ const seeded = (0, react_1.useRef)(false);
139
+ const id = (0, react_1.useId)();
140
+ // The browser refused, as the page sees it or as cloud saw the request. State,
141
+ // so cloud's answer redraws an open sheet.
142
+ const [locked, setLocked] = (0, react_1.useState)(false);
143
+ const choose = () => {
144
+ // Already open: the visitor's unsaved switches and the focus to return stay as they are.
145
+ if (asked.current)
146
+ return;
147
+ const at = document.activeElement;
148
+ back.current = at instanceof HTMLElement && at !== document.body ? at : null;
149
+ asked.current = true;
150
+ setLocked((0, event_1.declined)());
151
+ seeded.current = (0, event_1.storedConsent)();
152
+ setDraft((0, event_1.readConsent)());
153
+ setChoosing(true);
154
+ setShown(true);
155
+ };
156
+ // Closing without a choice: a visitor who has not chosen is asked again by the bar.
157
+ const close = () => {
158
+ asked.current = false;
159
+ setChoosing(false);
160
+ setShown((0, event_1.asks)());
161
+ };
87
162
  (0, react_1.useEffect)(() => {
88
- const sync = () => setShown((0, event_1.asks)() || asked.current);
163
+ const sync = () => {
164
+ setShown((0, event_1.asks)() || asked.current);
165
+ setLocked((0, event_1.declined)());
166
+ // A policy version the stored choice was not made under: the switches the
167
+ // sheet opened with were an old grant, and are off until chosen again.
168
+ if (asked.current && seeded.current && !(0, event_1.storedConsent)()) {
169
+ seeded.current = false;
170
+ setDraft((0, event_1.readConsent)());
171
+ }
172
+ };
89
173
  sync();
90
- const open = () => {
91
- asked.current = true;
92
- setDraft((0, event_1.readConsent)());
93
- setChoosing(true);
94
- setShown(true);
174
+ // `#choices` opens the sheet: on arrival, on a hash change, and on a press of
175
+ // any link on this site to it, so the footer's Privacy choices and a link in
176
+ // a page are the same control.
177
+ const hash = () => {
178
+ if (window.location.hash !== HASH)
179
+ return;
180
+ // The router's own entry state is kept: only the address loses its hash. A
181
+ // microtask, so a router that wraps replaceState in its own mount effect
182
+ // (Next's, which runs after this child's) records the address without it.
183
+ queueMicrotask(() => history.replaceState(history.state, '', window.location.pathname + window.location.search));
184
+ choose();
185
+ };
186
+ // A plain press only: a modified one (new tab, new window) keeps the browser's
187
+ // meaning, and the page it opens at #choices opens the sheet on arrival.
188
+ const press = (e) => {
189
+ if (e.defaultPrevented || e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey)
190
+ return;
191
+ // The attribute, not `.href`: an SVG link's `.href` is not a string.
192
+ const a = e.target?.closest?.('a[href]');
193
+ const href = a?.getAttribute('href');
194
+ const target = a?.getAttribute('target');
195
+ if (!href || a?.hasAttribute('download') || (target && target !== '_self'))
196
+ return;
197
+ let to;
198
+ try {
199
+ to = new URL(href, document.baseURI);
200
+ }
201
+ catch {
202
+ return;
203
+ }
204
+ if (to.hash !== HASH || to.origin !== window.location.origin)
205
+ return;
206
+ e.preventDefault();
207
+ choose();
95
208
  };
96
- window.addEventListener(OPEN, open);
209
+ hash();
210
+ window.addEventListener(OPEN, choose);
97
211
  window.addEventListener(event_1.POLICY_EVENT, sync);
212
+ window.addEventListener('hashchange', hash);
213
+ document.addEventListener('click', press, { capture: true });
98
214
  return () => {
99
- window.removeEventListener(OPEN, open);
215
+ window.removeEventListener(OPEN, choose);
100
216
  window.removeEventListener(event_1.POLICY_EVENT, sync);
217
+ window.removeEventListener('hashchange', hash);
218
+ document.removeEventListener('click', press, { capture: true });
101
219
  };
102
220
  }, []);
221
+ // Focus moves into the sheet when it opens, stays inside it while it is open,
222
+ // and returns to what opened it (or the bar's Manage preferences) when it closes.
223
+ (0, react_1.useEffect)(() => {
224
+ if (!choosing) {
225
+ if (!opened.current)
226
+ return;
227
+ opened.current = false;
228
+ const el = back.current;
229
+ back.current = null;
230
+ if (el?.isConnected)
231
+ el.focus();
232
+ else
233
+ manage.current?.focus();
234
+ return;
235
+ }
236
+ opened.current = true;
237
+ const all = () => [...(sheet.current?.querySelectorAll(FOCUSABLE) ?? [])];
238
+ all()[0]?.focus();
239
+ const keys = (e) => {
240
+ if (e.key === 'Escape') {
241
+ e.preventDefault();
242
+ close();
243
+ return;
244
+ }
245
+ if (e.key !== 'Tab')
246
+ return;
247
+ const list = all();
248
+ const first = list[0];
249
+ const last = list[list.length - 1];
250
+ if (!first || !last)
251
+ return;
252
+ const at = document.activeElement;
253
+ const inside = !!at && !!sheet.current?.contains(at);
254
+ if (e.shiftKey && (at === first || !inside)) {
255
+ e.preventDefault();
256
+ last.focus();
257
+ }
258
+ else if (!e.shiftKey && (at === last || !inside)) {
259
+ e.preventDefault();
260
+ first.focus();
261
+ }
262
+ };
263
+ document.addEventListener('keydown', keys);
264
+ return () => document.removeEventListener('keydown', keys);
265
+ }, [choosing]);
103
266
  if (!shown)
104
267
  return null;
105
268
  const done = (fn) => () => {
@@ -108,21 +271,18 @@ const Consent = ({ privacy = 'https://hanzo.ai/privacy', brand = 'Hanzo' }) => {
108
271
  setShown(false);
109
272
  setChoosing(false);
110
273
  };
111
- const choose = () => {
112
- asked.current = true;
113
- setDraft((0, event_1.readConsent)());
114
- setChoosing(true);
115
- setShown(true);
116
- };
117
274
  if (choosing)
118
- return ((0, jsx_runtime_1.jsxs)(gui_1.YStack, { ...frame, maxW: 440, gap: "$3", maxH: `calc(100dvh - var(--safe-top, env(safe-area-inset-top, 0px)) - var(--header, 0px) - ${EDGE.bottom} - 16px)`, overflowY: "auto", children: [(0, jsx_runtime_1.jsxs)(gui_1.Paragraph, { size: "$2", color: "$ink", m: 0, children: [`Choose what ${brand} may measure and share. You can change this at any time. `, (0, jsx_runtime_1.jsx)("a", { href: privacy, style: INLINE, children: "Privacy policy" })] }), locked ? ((0, jsx_runtime_1.jsx)(gui_1.Paragraph, { size: "$2", color: "$soft", m: 0, children: "Your browser asks sites not to track you, so all of these stay off." })) : null, kinds(brand).map((k) => {
119
- const off = locked;
120
- return ((0, jsx_runtime_1.jsxs)(gui_1.XStack, { gap: "$3", items: "center", justify: "space-between", children: [(0, jsx_runtime_1.jsxs)(gui_1.YStack, { flex: 1, children: [(0, jsx_runtime_1.jsx)(gui_1.Paragraph, { size: "$2", color: "$ink", fontWeight: "500", m: 0, children: k.title }), (0, jsx_runtime_1.jsx)(gui_1.Paragraph, { size: "$1", color: "$soft", m: 0, children: k.body })] }), (0, jsx_runtime_1.jsx)(switch_1.Switch, { "aria-label": k.title, disabled: off, checked: off ? false : draft[k.key], onCheckedChange: (v) => setDraft({ ...draft, [k.key]: v === true }) })] }, k.key));
121
- }), (0, jsx_runtime_1.jsxs)(gui_1.XStack, { gap: "$2", justify: "flex-end", children: [(0, jsx_runtime_1.jsx)(button_1.Button, { type: "button", size: "sm", variant: "secondary", onClick: done(event_1.rejectAll), children: "Reject all" }), (0, jsx_runtime_1.jsx)(button_1.Button, { type: "button", size: "sm", onClick: done(() => (0, event_1.saveConsent)(draft)), children: "Save choices" })] })] }));
122
- // One bar: the line and its link, then the buttons. They share a row where
123
- // the line fits beside them and the buttons drop under it where it does not,
124
- // so a phone shows the line over one row of buttons and nothing breaks mid-control.
125
- return ((0, jsx_runtime_1.jsxs)(gui_1.XStack, { ...frame, style: FIT, maxW: 960, 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.jsxs)(gui_1.Paragraph, { size: "$2", color: "$ink", m: 0, children: ['Cookies measure visits and ads once you accept. ', (0, jsx_runtime_1.jsx)("a", { href: privacy, style: INLINE, children: "Privacy policy" })] }) }), (0, jsx_runtime_1.jsxs)(gui_1.XStack, { gap: "$2", marginInlineStart: "auto", items: "center", children: [(0, jsx_runtime_1.jsx)(button_1.Button, { type: "button", size: "sm", variant: "link", onClick: choose, children: "Choose" }), (0, jsx_runtime_1.jsx)(button_1.Button, { type: "button", size: "sm", variant: "secondary", onClick: done(event_1.rejectAll), children: "Reject all" }), (0, jsx_runtime_1.jsx)(button_1.Button, { type: "button", size: "sm", onClick: done(event_1.acceptAll), children: "Accept all" })] })] }));
275
+ 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",
276
+ // The device's insets are physical sides, so the shorthand names them in order.
277
+ 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) => {
278
+ const open = expanded[k.id] === true;
279
+ const body = `${id}-${k.id}`;
280
+ 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));
281
+ })] }), (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" })] })] })] }));
282
+ // One bar: the line, then the buttons. They share a row where the line fits
283
+ // beside them and the buttons drop under it where it does not, so a phone shows
284
+ // the line over one row of buttons and nothing breaks mid-control.
285
+ 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 }) })] }));
126
286
  };
127
287
  exports.Consent = Consent;
128
288
  /** The footer link that reopens the choices, in every footer and every region. */
@@ -1,12 +1,16 @@
1
1
  /** Opens the choices from anywhere. */
2
2
  export declare const openConsent: () => void;
3
3
  export type ConsentProps = {
4
+ /** Where the cookie policy lives. */
5
+ cookies?: string;
4
6
  /** Where the privacy policy lives. */
5
7
  privacy?: string;
8
+ /** Where the terms of service live. */
9
+ terms?: string;
6
10
  /** Who is asking: the brand the visitor is on. */
7
11
  brand?: string;
8
12
  };
9
- declare const Consent: ({ privacy, brand }: ConsentProps) => import("react").JSX.Element | null;
13
+ declare const Consent: ({ cookies, privacy, terms, brand, }: ConsentProps) => import("react").JSX.Element | null;
10
14
  /** The footer link that reopens the choices, in every footer and every region. */
11
15
  declare const ConsentLink: ({ label }: {
12
16
  label?: string;
@@ -1 +1 @@
1
- {"version":3,"file":"Consent.d.ts","sourceRoot":"","sources":["../../src/consent/Consent.tsx"],"names":[],"mappings":"AA6BA,uCAAuC;AACvC,eAAO,MAAM,WAAW,QAAO,IAE9B,CAAA;AAiDD,MAAM,MAAM,YAAY,GAAG;IACzB,sCAAsC;IACtC,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,kDAAkD;IAClD,KAAK,CAAC,EAAE,MAAM,CAAA;CACf,CAAA;AAED,QAAA,MAAM,OAAO,uBAA+D,YAAY,uCA0HvF,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":"AAyCA,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"}