@hanzo/ui 8.29.0 → 8.29.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/gallery.html CHANGED
@@ -1,4 +1,4 @@
1
- <!doctype html><html lang="en" class="t_dark"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>@hanzo/ui 8.29.0</title><style>/* @hanzo/ui — generated by scripts/gen-css.mjs. Do not edit. */
1
+ <!doctype html><html lang="en" class="t_dark"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>@hanzo/ui 8.29.1</title><style>/* @hanzo/ui — generated by scripts/gen-css.mjs. Do not edit. */
2
2
  :root { --hanzo-ui-styles: 1; }
3
3
  /* Hanzo Design System — the entry point. Import THIS one file.
4
4
  *
@@ -0,0 +1,351 @@
1
+ 'use client';"use strict";
2
+ 'use client';
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.HEADER = void 0;
5
+ exports.Masthead = Masthead;
6
+ const jsx_runtime_1 = require("react/jsx-runtime");
7
+ const react_1 = require("react");
8
+ const gui_1 = require("@hanzo/gui");
9
+ const shell_1 = require("@hanzogui/shell");
10
+ const nav_1 = require("./nav.cjs");
11
+ /** A menu stays open this long after the pointer leaves, so a diagonal path to a row keeps it. */
12
+ const GRACE = 160;
13
+ /** And opens after the pointer rests this long, so crossing the bar opens nothing. */
14
+ const REST = 70;
15
+ /** The bar's height, under the notch. */
16
+ exports.HEADER = 60;
17
+ /** The bar's two inks over the page: a wash for a lit pill, and its hairline. */
18
+ const WASH = '$ink/6';
19
+ const RING = 'inset 0 0 0 1px color-mix(in srgb, var(--foreground) 10%, transparent)';
20
+ /** A pill lit: under the pointer, or holding its menu open. */
21
+ const LIT = { color: 'var(--foreground)', bg: WASH, boxShadow: RING };
22
+ /** Where what hangs from the bar begins: under the bar and the notch above it. */
23
+ const UNDER = `calc(${exports.HEADER}px + env(safe-area-inset-top))`;
24
+ /** The open bar, the planes, the log-in card and the sheet stand on one opaque ground. */
25
+ const PANEL = 'var(--chrome-panel)';
26
+ /** The drop under what hangs from the bar. */
27
+ const DROP = '0 24px 60px -16px color-mix(in srgb, var(--pure-black) 75%, transparent)';
28
+ /** The bar's glass, once the page moves under it. */
29
+ const GLASS = '$background/72';
30
+ /** The highest z-index there is. The search palette stands in a fixed layer at
31
+ * it, so nothing fixed to the window draws over an open palette. */
32
+ const TOP = 2147483647;
33
+ /**
34
+ * The shell's account menu paints with `var(--white-92…)`, and @hanzo/design
35
+ * keeps those rungs white in both themes. The bar follows the theme, so inside
36
+ * it the four flip to black: `light-dark()` reads the `color-scheme` @hanzo/design
37
+ * sets with the theme. The dimmest is PUSHED rather than mirrored — .45 black on
38
+ * white is 3.35:1, .56 is 4.5:1 on the bar and on the panel.
39
+ */
40
+ const ink = (dark, light) => `light-dark(color-mix(in srgb, var(--pure-black) ${light}%, transparent), color-mix(in srgb, var(--pure-white) ${dark}%, transparent))`;
41
+ const INK = {
42
+ '--white-92': ink(92, 92),
43
+ '--white-60': ink(60, 60),
44
+ '--white-45': ink(45, 56),
45
+ '--white-10': 'var(--border)',
46
+ };
47
+ /** The bar: a sticky row under the notch, on the page's gutter. Its height is
48
+ * published as `--header`, which @hanzo/design's scroll padding reads. */
49
+ const BAR = {
50
+ position: 'sticky',
51
+ t: 0,
52
+ z: 'var(--z-header)',
53
+ items: 'center',
54
+ gap: 12,
55
+ boxSizing: 'content-box',
56
+ height: exports.HEADER,
57
+ pt: 'calc(0px + env(safe-area-inset-top))',
58
+ pb: 0,
59
+ px: 'var(--page-gutter)',
60
+ };
61
+ /** The name of the place, lit under the pointer and while the company menu is open. */
62
+ const BRAND = { items: 'center', shrink: 0, height: 34, ml: -4, rounded: 999, hoverStyle: { bg: WASH, boxShadow: RING } };
63
+ /** The wordmark: home, and never dim. */
64
+ const NAME = { display: 'flex', items: 'center', height: '100%', pl: 12, pr: 12, color: 'var(--foreground)', whiteSpace: 'nowrap', textDecorationLine: 'none' };
65
+ /** A pill in the bar: dim at rest, lit under the pointer. A button's own padding is the browser's, so it is stated. */
66
+ const PILL = {
67
+ display: 'flex',
68
+ items: 'center',
69
+ shrink: 0,
70
+ height: 34,
71
+ p: 0,
72
+ borderWidth: 0,
73
+ rounded: 999,
74
+ bg: 'transparent',
75
+ color: 'var(--muted-foreground)',
76
+ cursor: 'pointer',
77
+ hoverStyle: LIT,
78
+ };
79
+ /** A pill that opens a menu: its label, then a chevron. */
80
+ const TRIGGER = { ...PILL, gap: 6, px: 12, fontSize: '$2', lineHeight: 20, fontWeight: '500', whiteSpace: 'nowrap' };
81
+ /** The one filled control, in the bar and in the sheet. */
82
+ const ACT = {
83
+ display: 'flex',
84
+ items: 'center',
85
+ justify: 'center',
86
+ gap: 2,
87
+ height: 34,
88
+ px: 14,
89
+ rounded: 999,
90
+ bg: 'var(--foreground)',
91
+ color: 'var(--background)',
92
+ fontSize: '$2',
93
+ lineHeight: 20,
94
+ fontWeight: '600',
95
+ whiteSpace: 'nowrap',
96
+ hoverStyle: { opacity: 0.88 },
97
+ };
98
+ /** What hangs from the bar arrives: up 4px, faded in. */
99
+ const ARRIVE = { enterStyle: { opacity: 0, y: -4 }, transition: 'quickest' };
100
+ /**
101
+ * A row in a plane. As wide as its words, so a keyboard ring hugs the words it
102
+ * names rather than the column; its padding steps out of the column, so the
103
+ * words stand on the column's edge.
104
+ */
105
+ const ROW = {
106
+ display: 'flex',
107
+ items: 'center',
108
+ self: 'flex-start',
109
+ mx: -8,
110
+ px: 8,
111
+ rounded: 8,
112
+ whiteSpace: 'nowrap',
113
+ hoverStyle: { color: 'var(--foreground)' },
114
+ focusVisibleStyle: { color: 'var(--foreground)' },
115
+ };
116
+ /** The destinations someone arrives wanting, in display type. */
117
+ const LEAD = { ...ROW, py: 2, fontSize: '$8', lineHeight: 33.28, fontWeight: '400', letterSpacing: -0.26, color: 'var(--foreground)' };
118
+ const PLAIN = { ...ROW, py: 5, fontSize: '$2', lineHeight: 20, fontWeight: '500', color: 'var(--muted-foreground)' };
119
+ /** A row in the sheet: a thumb's height, the column's width. */
120
+ const LINE = {
121
+ display: 'flex',
122
+ items: 'center',
123
+ justify: 'flex-start',
124
+ minH: 44,
125
+ rounded: 8,
126
+ fontWeight: '400',
127
+ color: 'var(--muted-foreground)',
128
+ hoverStyle: { color: 'var(--foreground)' },
129
+ focusVisibleStyle: { color: 'var(--foreground)' },
130
+ };
131
+ /** The sheet's doors: Log in's rows, or the reader's own page. */
132
+ const DOOR = { display: 'flex', items: 'center', justify: 'flex-start', minH: 44, fontSize: '$4', lineHeight: 20, fontWeight: '400', color: 'var(--foreground)' };
133
+ /** A menu's mark. In the bar it waits for `$xl`; in the sheet it always shows. */
134
+ function Mark({ name, bar = false }) {
135
+ const Glyph = shell_1.MARKS[name];
136
+ return ((0, jsx_runtime_1.jsx)(gui_1.View, { render: "span", "data-slot": "mark", "aria-hidden": true, display: bar ? 'none' : 'flex', $xl: bar ? { display: 'flex' } : undefined, opacity: 0.9, children: (0, jsx_runtime_1.jsx)(Glyph, { size: 16 }) }));
137
+ }
138
+ /** ▾, turned over while what it opens is open. */
139
+ function Chevron({ turned, end = false }) {
140
+ return ((0, jsx_runtime_1.jsx)(gui_1.View, { render: "span", "aria-hidden": true, display: "flex", shrink: 0, ml: end ? 'auto' : 0, rotate: turned ? '180deg' : '0deg', transition: "quickest", children: (0, jsx_runtime_1.jsx)("svg", { width: 12, height: 12, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2.2, strokeLinecap: "round", strokeLinejoin: "round", children: (0, jsx_runtime_1.jsx)("path", { d: "M6 9l6 6 6-6" }) }) }));
141
+ }
142
+ /** ↗ — the shell's outbound arrow, riding the label's size and ink. */
143
+ function Out() {
144
+ return ((0, jsx_runtime_1.jsx)(gui_1.View, { render: "span", "aria-hidden": true, display: "flex", ml: "calc(0.28em)", opacity: 0.7, children: (0, jsx_runtime_1.jsx)("svg", { width: "0.75em", height: "0.75em", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2.5, strokeLinecap: "round", strokeLinejoin: "round", children: (0, jsx_runtime_1.jsx)("path", { d: "M7 17 17 7M9 7h8v8" }) }) }));
145
+ }
146
+ /** A row: the label, and ↗ with a new tab when it leaves. */
147
+ function Row({ link, place, onClick, ...style }) {
148
+ const out = (0, nav_1.leaves)(link.href, place.here);
149
+ return ((0, jsx_runtime_1.jsxs)(gui_1.Anchor, { href: (0, nav_1.at)(link.href, place.origin, place.here), target: out ? '_blank' : undefined, rel: out ? 'noreferrer' : undefined, onClick: onClick, textDecorationLine: "none", ...style, children: [link.label, out ? (0, jsx_runtime_1.jsx)(Out, {}) : null] }));
150
+ }
151
+ /** A column's title. In the sheet it is the quiet label over a section's rows. */
152
+ function Title({ sheet = false, children }) {
153
+ return ((0, jsx_runtime_1.jsx)(gui_1.Text, { render: "p", mt: sheet ? 4 : 0, mb: sheet ? 2 : 10, mx: 0, fontSize: sheet ? '$1' : '$2', lineHeight: 20, fontWeight: "600", color: sheet ? 'var(--text-tertiary)' : 'var(--foreground)', children: children }));
154
+ }
155
+ /**
156
+ * A menu's columns, laid out from the wordmark's column: the lead column in
157
+ * display type, the rest as lists. The column under the pointer is the one
158
+ * being read; the others step back to 45%.
159
+ */
160
+ function Columns({ menu, place, onPick }) {
161
+ const [hot, setHot] = (0, react_1.useState)(null);
162
+ return ((0, jsx_runtime_1.jsx)(gui_1.View, { display: "grid", gridAutoFlow: "column", gridAutoColumns: "max-content", justify: "flex-start", gap: "max(24px, min(3vw, 64px))", pt: 36, pb: 48, px: "calc(var(--page-gutter) + 8px)", children: menu.groups.map((group, i) => ((0, jsx_runtime_1.jsxs)(gui_1.YStack, { gap: 2, minW: 0, opacity: hot === null || hot === i ? 1 : 0.45, transition: "quickest", onPointerEnter: () => setHot(i), onPointerLeave: () => setHot(null), children: [(0, jsx_runtime_1.jsx)(Title, { children: group.title }), group.links.map((link) => ((0, jsx_runtime_1.jsx)(Row, { link: link, place: place, onClick: onPick, ...(group.lead ? LEAD : PLAIN) }, link.label)))] }, group.title))) }));
163
+ }
164
+ /** The DOM node a gui element stands for, on the web. */
165
+ const node = (el) => (el instanceof HTMLElement ? el : null);
166
+ /** Publish the bar's content height to `--header` on the document for as long as it is mounted. */
167
+ function measure(el) {
168
+ if (!el || typeof ResizeObserver === 'undefined')
169
+ return;
170
+ const root = document.documentElement.style;
171
+ const size = new ResizeObserver(([entry]) => root.setProperty('--header', `${entry.contentRect.height}px`));
172
+ size.observe(el);
173
+ return () => {
174
+ size.disconnect();
175
+ root.removeProperty('--header');
176
+ };
177
+ }
178
+ /** The bar's rows as ⌘K entries: what a reader can reach from here, they can find. */
179
+ function entries(company, menus, login, place) {
180
+ return [company, ...menus]
181
+ .flatMap((menu) => menu.groups.flatMap((group) => group.links.map((link) => ({ menu, link }))))
182
+ .concat(login.map((link) => ({ menu: company, link })))
183
+ .filter(({ link }) => !link.href.startsWith(`${nav_1.DOCS}/`))
184
+ .map(({ menu, link }) => ({
185
+ id: `nav/${menu.id}/${link.label}`,
186
+ title: link.label,
187
+ href: (0, nav_1.at)(link.href, place.origin, place.here),
188
+ group: menu.label,
189
+ external: (0, nav_1.leaves)(link.href, place.here),
190
+ }));
191
+ }
192
+ function Masthead({ label = 'Hanzo AI', home = '/', origin = '', here = '', tree = {}, person, onSignOut, commands = [], onAsk, askHref, bare = false, }) {
193
+ const company = tree.company ?? nav_1.COMPANY;
194
+ const menus = tree.menus ?? nav_1.MENUS;
195
+ const login = tree.login ?? nav_1.LOGIN;
196
+ const account = tree.account ?? nav_1.ACCOUNT;
197
+ const act = person ? (tree.open ?? nav_1.OPEN) : (tree.try ?? nav_1.TRY);
198
+ const place = { origin, here };
199
+ const homeHref = (0, nav_1.at)(home, origin, here);
200
+ const [open, setOpen] = (0, react_1.useState)(null);
201
+ const [sheet, setSheet] = (0, react_1.useState)(false);
202
+ const [search, setSearch] = (0, react_1.useState)(false);
203
+ const [grounded, setGrounded] = (0, react_1.useState)(false);
204
+ const timer = (0, react_1.useRef)(undefined);
205
+ const pinned = (0, react_1.useRef)(false);
206
+ const bar = (0, react_1.useRef)(null);
207
+ const toggle = (0, react_1.useRef)(null);
208
+ /** The bar's element, held for the menus and measured for `--header`. */
209
+ const masthead = (0, react_1.useCallback)((el) => {
210
+ bar.current = el;
211
+ return measure(node(el));
212
+ }, []);
213
+ const close = (0, react_1.useCallback)(() => {
214
+ clearTimeout(timer.current);
215
+ pinned.current = false;
216
+ setOpen(null);
217
+ }, []);
218
+ // The bar is clear over the top of the page and takes the glass once the page
219
+ // moves under it.
220
+ (0, react_1.useEffect)(() => {
221
+ const read = () => setGrounded(window.scrollY > 4);
222
+ read();
223
+ window.addEventListener('scroll', read, { passive: true });
224
+ return () => window.removeEventListener('scroll', read);
225
+ }, []);
226
+ // Escape closes whatever is open and hands focus back to what opened it.
227
+ (0, react_1.useEffect)(() => {
228
+ if (!open && !sheet)
229
+ return;
230
+ const key = (e) => {
231
+ if (e.key !== 'Escape')
232
+ return;
233
+ if (open) {
234
+ node(bar.current)?.querySelector(`[aria-controls="menu-${open}"]`)?.focus();
235
+ close();
236
+ }
237
+ if (sheet) {
238
+ setSheet(false);
239
+ node(toggle.current)?.focus();
240
+ }
241
+ };
242
+ document.addEventListener('keydown', key);
243
+ return () => document.removeEventListener('keydown', key);
244
+ }, [open, sheet, close]);
245
+ // A press anywhere outside the bar closes an open menu.
246
+ (0, react_1.useEffect)(() => {
247
+ if (!open)
248
+ return;
249
+ const outside = (e) => {
250
+ if (!node(bar.current)?.contains(e.target))
251
+ close();
252
+ };
253
+ document.addEventListener('pointerdown', outside);
254
+ return () => document.removeEventListener('pointerdown', outside);
255
+ }, [open, close]);
256
+ // The sheet holds the screen: the page under it does not scroll, and it goes
257
+ // away when the window grows into the bar.
258
+ (0, react_1.useEffect)(() => {
259
+ if (!sheet)
260
+ return;
261
+ const root = document.documentElement;
262
+ root.style.overflow = 'hidden';
263
+ const wide = matchMedia('(min-width: 1024px)');
264
+ const grow = () => wide.matches && setSheet(false);
265
+ wide.addEventListener('change', grow);
266
+ return () => {
267
+ root.style.overflow = '';
268
+ wide.removeEventListener('change', grow);
269
+ };
270
+ }, [sheet]);
271
+ const rest = (id) => (e) => {
272
+ if (e.pointerType !== 'mouse')
273
+ return;
274
+ clearTimeout(timer.current);
275
+ if (open === id)
276
+ return;
277
+ if (open) {
278
+ pinned.current = false;
279
+ setOpen(id);
280
+ }
281
+ else
282
+ timer.current = setTimeout(() => setOpen(id), REST);
283
+ };
284
+ const leave = (e) => {
285
+ if (e.pointerType !== 'mouse' || pinned.current)
286
+ return;
287
+ clearTimeout(timer.current);
288
+ timer.current = setTimeout(() => setOpen(null), GRACE);
289
+ };
290
+ const press = (id) => () => {
291
+ clearTimeout(timer.current);
292
+ // A click on a menu the pointer already opened keeps it open; a second one closes it.
293
+ if (open === id && pinned.current)
294
+ return close();
295
+ pinned.current = true;
296
+ setOpen(id);
297
+ };
298
+ const step = (id) => (e) => {
299
+ if (e.key !== 'ArrowDown')
300
+ return;
301
+ e.preventDefault();
302
+ pinned.current = true;
303
+ setOpen(id);
304
+ requestAnimationFrame(() => document.querySelector(`#menu-${id} a`)?.focus());
305
+ };
306
+ // Focus that leaves a menu closes it.
307
+ const blur = (e) => {
308
+ if (!e.currentTarget.contains(e.relatedTarget))
309
+ close();
310
+ };
311
+ const find = () => {
312
+ close();
313
+ setSheet(false);
314
+ setSearch(true);
315
+ };
316
+ /** What a pill wears while its menu is open. */
317
+ const lit = (id) => (open === id ? LIT : {});
318
+ const menu = (item, trigger) => ((0, jsx_runtime_1.jsxs)(gui_1.XStack, { position: "relative", onPointerEnter: rest(item.id), onPointerLeave: leave, onBlur: blur, children: [trigger, open === item.id ? ((0, jsx_runtime_1.jsx)(gui_1.YStack, { id: `menu-${item.id}`, role: "group", "aria-label": item.label, position: "fixed", t: UNDER, l: 0, r: 0, z: "var(--z-raised)", maxH: `calc(100dvh - ${exports.HEADER}px)`, overflowY: "auto", bg: PANEL, boxShadow: DROP, ...ARRIVE, children: (0, jsx_runtime_1.jsx)(Columns, { menu: item, place: place, onPick: close }) })) : null] }));
319
+ const trigger = (item) => ((0, jsx_runtime_1.jsxs)(gui_1.Text, { render: "button", ...TRIGGER, ...lit(item.id), "aria-expanded": open === item.id, "aria-controls": `menu-${item.id}`, onClick: press(item.id), onKeyDown: step(item.id), children: [(0, jsx_runtime_1.jsx)(Mark, { name: item.glyph, bar: true }), item.label, (0, jsx_runtime_1.jsx)(Chevron, { turned: open === item.id })] }));
320
+ // On the way in and on the way to pay, the name alone. The two bars are
321
+ // different elements (`key`), so moving between them mounts the other bar
322
+ // rather than re-rendering one with and without its `transition`, which
323
+ // changes the hooks @hanzo/gui calls for it.
324
+ if (bare) {
325
+ return ((0, jsx_runtime_1.jsx)(gui_1.XStack, { ref: masthead, render: "header", "data-slot": "masthead", ...BAR, children: (0, jsx_runtime_1.jsx)(gui_1.XStack, { render: "span", ...BRAND, children: (0, jsx_runtime_1.jsx)(gui_1.Anchor, { href: homeHref, "aria-label": `${label} home`, ...NAME, children: (0, jsx_runtime_1.jsx)(shell_1.HanzoWordmark, { label: label, size: 22 }) }) }) }, "bare"));
326
+ }
327
+ const lifted = open !== null || sheet;
328
+ return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsxs)(gui_1.XStack, { ref: masthead, render: "header", "data-slot": "masthead", ...BAR, bg: lifted ? PANEL : grounded ? GLASS : 'transparent',
329
+ // The chrome's own blur, by name: @hanzo/design's one radius, the glass every bar wears.
330
+ backdropFilter: !lifted && grounded ? 'var(--chrome-blur)' : 'none', transition: "quickest",
331
+ // inline-style: custom properties @hanzogui/shell's account menu reads; no prop sets a variable.
332
+ style: INK, children: [menu(company, (0, jsx_runtime_1.jsxs)(gui_1.XStack, { render: "span", ...BRAND, ...(open === company.id ? { bg: WASH, boxShadow: RING } : {}), children: [(0, jsx_runtime_1.jsx)(gui_1.Anchor, { href: homeHref, "aria-label": `${label} home`, ...NAME, $lg: { pr: 0 }, children: (0, jsx_runtime_1.jsx)(shell_1.HanzoWordmark, { label: label, size: 22 }) }), (0, jsx_runtime_1.jsx)(gui_1.Text, { render: "button", "aria-label": company.label, "aria-expanded": open === company.id, "aria-controls": `menu-${company.id}`, onClick: press(company.id), onKeyDown: step(company.id), display: "none", $lg: { display: 'grid' }, placeItems: "center", width: 30, height: "100%", p: 0, pr: 6, borderWidth: 0, borderTopRightRadius: 999, borderBottomRightRadius: 999, bg: "transparent", color: "var(--foreground)", cursor: "pointer", children: (0, jsx_runtime_1.jsx)(Chevron, { turned: open === company.id }) })] })), (0, jsx_runtime_1.jsx)(gui_1.XStack, { render: "nav", "aria-label": "Main", display: "none", $lg: { display: 'flex' }, items: "center", gap: 2, children: menus.map((item) => ((0, jsx_runtime_1.jsx)(react_1.Fragment, { children: menu(item, trigger(item)) }, item.id))) }), open && open !== 'login' ? ((0, jsx_runtime_1.jsx)(gui_1.View, { "aria-hidden": true, onPress: close, position: "fixed", t: UNDER, l: 0, r: 0, b: 0, z: "var(--z-base)", bg: "$black/40", backdropFilter: "var(--chrome-blur)" })) : null, (0, jsx_runtime_1.jsx)(gui_1.Text, { render: "button", "aria-label": "Search Hanzo or ask AI", onClick: find, ...PILL, justify: "center", width: 44, height: 44, ml: "auto", $lg: { width: 34, height: 34, ml: 0 }, children: (0, jsx_runtime_1.jsx)(shell_1.MARKS.search, { size: 16 }) }), (0, jsx_runtime_1.jsxs)(gui_1.XStack, { items: "center", gap: 8, $lg: { ml: 'auto' }, children: [person ? ((0, jsx_runtime_1.jsx)(shell_1.HanzoIdentity, { auth: { user: person, onSignOut, items: [{ id: 'account', label: account.label, href: (0, nav_1.at)(account.href, origin, here) }] } })) : ((0, jsx_runtime_1.jsxs)(gui_1.XStack, { position: "relative", display: "none", $lg: { display: 'flex' }, onPointerEnter: rest('login'), onPointerLeave: leave, onBlur: blur, children: [(0, jsx_runtime_1.jsxs)(gui_1.Text, { render: "button", ...TRIGGER, ...lit('login'), "aria-expanded": open === 'login', "aria-controls": "menu-login", onClick: press('login'), onKeyDown: step('login'), children: ["Log in", (0, jsx_runtime_1.jsx)(Chevron, { turned: open === 'login' })] }), open === 'login' ? ((0, jsx_runtime_1.jsx)(gui_1.YStack, { id: "menu-login", role: "group", "aria-label": "Log in", position: "absolute", t: "calc(100% + 8px)", r: 0, display: "grid", gap: 2, minW: 240, p: 6, borderWidth: 1, borderColor: "var(--border)", rounded: 12, bg: PANEL, boxShadow: DROP, ...ARRIVE, children: login.map((link) => ((0, jsx_runtime_1.jsxs)(gui_1.Anchor, { href: (0, nav_1.at)(link.href, origin, here), target: "_blank", rel: "noreferrer", onClick: close, display: "grid", gap: 2, py: 8, px: 10, rounded: 8, color: "var(--foreground)", fontSize: "$2", lineHeight: 20, fontWeight: "600", textDecorationLine: "none", hoverStyle: { bg: WASH }, focusVisibleStyle: { bg: WASH }, children: [link.label, (0, jsx_runtime_1.jsx)(gui_1.Text, { render: "small", fontSize: "$1", lineHeight: 20, fontWeight: "400", color: "var(--muted-foreground)", children: link.hint })] }, link.label))) })) : null] })), (0, jsx_runtime_1.jsx)(Row, { link: act, place: place, ...ACT, display: "none", $lg: { display: 'flex' } }), (0, jsx_runtime_1.jsx)(gui_1.Text, { ref: toggle, render: "button", "aria-label": sheet ? 'Close menu' : 'Open menu', "aria-expanded": sheet, "aria-controls": "sheet", onClick: () => {
333
+ close();
334
+ setSheet((s) => !s);
335
+ }, display: "grid", $lg: { display: 'none' }, placeItems: "center", width: 44, height: 44, p: 0, mr: -10, borderWidth: 0, bg: "transparent", color: "var(--foreground)", cursor: "pointer", children: (0, jsx_runtime_1.jsx)("svg", { width: 22, height: 22, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", "aria-hidden": "true", children: (0, jsx_runtime_1.jsx)("path", { d: sheet ? 'M6 6l12 12M18 6L6 18' : 'M3 9h18M3 15h18' }) }) })] })] }, "full"), sheet ? ((0, jsx_runtime_1.jsx)(Sheet, { company: company, menus: menus, login: login, door: person ? account : null, act: act, place: place, onClose: () => setSheet(false) })) : null, (0, jsx_runtime_1.jsx)(gui_1.View, { position: "fixed", z: TOP, children: (0, jsx_runtime_1.jsx)(shell_1.HanzoCommandPalette, { commands: [...commands, ...entries(company, menus, login, place).filter((e) => !commands.some((c) => c.href === e.href))], open: search, onOpenChange: setSearch, onAsk: onAsk, askHref: askHref }) })] }));
336
+ }
337
+ /**
338
+ * The phone's menu: the whole tree, one section open at a time, then the two
339
+ * doors. A `<details>` per section, so the browser keeps which one is open and
340
+ * closes the rest (`name`); the chevron reads it back. It stands under the bar
341
+ * and carries the bottom inset, as the bar carries the top one.
342
+ */
343
+ function Sheet({ company, menus, login, door, act, place, onClose, }) {
344
+ const first = (0, react_1.useRef)(null);
345
+ const [section, setSection] = (0, react_1.useState)(null);
346
+ (0, react_1.useEffect)(() => node(first.current)?.focus(), []);
347
+ return ((0, jsx_runtime_1.jsxs)(gui_1.View, { id: "sheet", role: "dialog", "aria-modal": true, "aria-label": "Menu", display: "block", position: "fixed", t: UNDER, l: 0, r: 0, b: 0, z: "var(--z-overlay)", overflowY: "auto", "$platform-web": { overscrollBehaviorX: 'contain', overscrollBehaviorY: 'contain' }, pt: 4, px: "var(--page-gutter)", pb: "calc(24px + env(safe-area-inset-bottom))", bg: PANEL, ...ARRIVE, children: [(0, jsx_runtime_1.jsx)(gui_1.View, { render: "nav", "aria-label": "Main", display: "block", children: [company, ...menus].map((item, i) => ((0, jsx_runtime_1.jsxs)(gui_1.View, { render: (0, jsx_runtime_1.jsx)("details", { name: "sheet", onToggle: (e) => {
348
+ const now = e.currentTarget.open;
349
+ setSection((s) => (now ? item.id : s === item.id ? null : s));
350
+ } }), display: "block", borderBottomWidth: 1, borderColor: "var(--border)", children: [(0, jsx_runtime_1.jsxs)(gui_1.Text, { ref: i === 0 ? first : undefined, render: "summary", display: "flex", items: "center", gap: 10, minH: 56, fontSize: "$6", lineHeight: 20, fontWeight: "500", color: "var(--foreground)", cursor: "pointer", children: [(0, jsx_runtime_1.jsx)(Mark, { name: item.glyph }), item.label, (0, jsx_runtime_1.jsx)(Chevron, { turned: section === item.id, end: true })] }), item.groups.map((group) => ((0, jsx_runtime_1.jsxs)(gui_1.YStack, { gap: 2, minW: 0, pb: 16, children: [(0, jsx_runtime_1.jsx)(Title, { sheet: true, children: group.title }), group.links.map((link) => ((0, jsx_runtime_1.jsx)(Row, { link: link, place: place, onClick: onClose, ...LINE, fontSize: group.lead ? '$6' : '$4', lineHeight: group.lead ? 20.4 : 18 }, link.label)))] }, group.title)))] }, item.id))) }), (0, jsx_runtime_1.jsxs)(gui_1.View, { display: "grid", gap: 4, pt: 20, children: [door ? ((0, jsx_runtime_1.jsx)(Row, { link: door, place: place, onClick: onClose, ...DOOR })) : ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)(Title, { sheet: true, children: "Log in" }), login.map((link) => ((0, jsx_runtime_1.jsx)(Row, { link: link, place: place, onClick: onClose, ...DOOR }, link.label)))] })), (0, jsx_runtime_1.jsx)(Row, { link: act, place: place, onClick: onClose, ...ACT, height: 48, mt: 12, fontSize: "$3" })] })] }));
351
+ }
@@ -0,0 +1,75 @@
1
+ import { type HanzoCommandEntry } from '@hanzogui/shell';
2
+ import { type Link, type Menu } from './nav.js';
3
+ /**
4
+ * hanzo.ai's header, for every site that wears it: one nav tree (`./nav`) for
5
+ * the bar and the phone sheet.
6
+ *
7
+ * The bar is `@hanzogui/shell`'s vocabulary — its marks, its wordmark, its ⌘K
8
+ * palette and its account menu — laid out the way hanzo.ai reads: the wordmark
9
+ * opens the company, the five menus follow with a mark before each label,
10
+ * search closes the row, and Log in ▾ and Try Hanzo stand at the far end.
11
+ *
12
+ * Every style is a gui prop on the element that wears it; no stylesheet rule
13
+ * reaches the bar, a plane, the log-in card or the sheet. The geometry is the
14
+ * shell's — a 60px row, 34px pills, 13px labels, a 16px mark before each, a
15
+ * 12px chevron after — and every ink and ground is a theme token, so the light
16
+ * theme needs no second copy. Below gui's `$lg` (1024) the bar is the name,
17
+ * search and a menu button; the marks before the five labels wait for `$xl`
18
+ * (1280), because between the two the row fits its labels and not its marks.
19
+ *
20
+ * THREE GROUNDS for the bar. Clear over the top of a page; glass once the page
21
+ * moves under it; and the plane's own opaque ground while a menu or the sheet
22
+ * hangs from it, so bar and plane are one surface. The open bar carries no
23
+ * backdrop filter on purpose: a filtered element is a backdrop root, and the
24
+ * veil inside it would then blur the bar instead of the page.
25
+ *
26
+ * A row's look is read from the tree, not from where it lands: a path is a page
27
+ * on hanzo.ai and reads as one on every host (no ↗, the same tab), and a row
28
+ * that names a host says ↗ and opens a new tab — unless it names `here`, the
29
+ * host the page is on, when it is that site's own page.
30
+ */
31
+ /** Who is signed in, as the account menu draws them. */
32
+ export interface Person {
33
+ name: string;
34
+ email?: string;
35
+ avatar?: string;
36
+ }
37
+ /** The rows the bar draws. Each defaults to hanzo.ai's own, in `./nav`. */
38
+ export interface Tree {
39
+ company: Menu;
40
+ menus: readonly Menu[];
41
+ login: readonly Link[];
42
+ /** The one filled control for a stranger. */
43
+ try: Link;
44
+ /** …and for a reader who is signed in. */
45
+ open: Link;
46
+ /** The signed-in reader's own page, in the account menu and the sheet. */
47
+ account: Link;
48
+ }
49
+ export interface MastheadProps {
50
+ /** The name in the corner: `Hanzo AI` on hanzo.ai. */
51
+ label?: string;
52
+ /** Where the name goes, read as a row is. */
53
+ home?: string;
54
+ /** The host the tree's paths are pages on: empty on hanzo.ai, `https://hanzo.ai` on any other site. */
55
+ origin?: string;
56
+ /** The host this page is on, so a row that names it is a page here (`https://hanzo.agency`). */
57
+ here?: string;
58
+ /** The rows, where they are not hanzo.ai's. */
59
+ tree?: Partial<Tree>;
60
+ /** The signed-in reader. Absent, the bar offers Log in ▾ and Try Hanzo. */
61
+ person?: Person | null;
62
+ onSignOut?: () => void;
63
+ /** What ⌘K finds, beside the bar's own rows. */
64
+ commands?: readonly HanzoCommandEntry[];
65
+ /** A question typed into ⌘K, handed to the host's own assistant. Absent, it goes to `askHref`. */
66
+ onAsk?: (question: string) => void;
67
+ /** Where ⌘K sends a question, as `<askHref>?q=`. */
68
+ askHref?: string;
69
+ /** The name alone, on the way in and on the way to pay: home is one click away. */
70
+ bare?: boolean;
71
+ }
72
+ /** The bar's height, under the notch. */
73
+ export declare const HEADER = 60;
74
+ export declare function Masthead({ label, home, origin, here, tree, person, onSignOut, commands, onAsk, askHref, bare, }: MastheadProps): import("react").JSX.Element;
75
+ //# sourceMappingURL=Masthead.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Masthead.d.ts","sourceRoot":"","sources":["../../src/masthead/Masthead.tsx"],"names":[],"mappings":"AAIA,OAAO,EAA4E,KAAK,iBAAiB,EAAE,MAAM,iBAAiB,CAAA;AAElI,OAAO,EAA+D,KAAK,IAAI,EAAE,KAAK,IAAI,EAAE,MAAM,OAAO,CAAA;AAEzG;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH,wDAAwD;AACxD,MAAM,WAAW,MAAM;IACrB,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB;AAED,2EAA2E;AAC3E,MAAM,WAAW,IAAI;IACnB,OAAO,EAAE,IAAI,CAAA;IACb,KAAK,EAAE,SAAS,IAAI,EAAE,CAAA;IACtB,KAAK,EAAE,SAAS,IAAI,EAAE,CAAA;IACtB,6CAA6C;IAC7C,GAAG,EAAE,IAAI,CAAA;IACT,0CAA0C;IAC1C,IAAI,EAAE,IAAI,CAAA;IACV,0EAA0E;IAC1E,OAAO,EAAE,IAAI,CAAA;CACd;AAED,MAAM,WAAW,aAAa;IAC5B,sDAAsD;IACtD,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,6CAA6C;IAC7C,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,uGAAuG;IACvG,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,gGAAgG;IAChG,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,+CAA+C;IAC/C,IAAI,CAAC,EAAE,OAAO,CAAC,IAAI,CAAC,CAAA;IACpB,2EAA2E;IAC3E,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACtB,SAAS,CAAC,EAAE,MAAM,IAAI,CAAA;IACtB,gDAAgD;IAChD,QAAQ,CAAC,EAAE,SAAS,iBAAiB,EAAE,CAAA;IACvC,kGAAkG;IAClG,KAAK,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAA;IAClC,oDAAoD;IACpD,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,mFAAmF;IACnF,IAAI,CAAC,EAAE,OAAO,CAAA;CACf;AAOD,yCAAyC;AACzC,eAAO,MAAM,MAAM,KAAK,CAAA;AAkQxB,wBAAgB,QAAQ,CAAC,EACvB,KAAkB,EAClB,IAAU,EACV,MAAW,EACX,IAAS,EACT,IAAS,EACT,MAAM,EACN,SAAS,EACT,QAAa,EACb,KAAK,EACL,OAAO,EACP,IAAY,GACb,EAAE,aAAa,+BA6Xf"}