@hanzo/rooms 0.1.96 → 0.1.98

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/Drawer.js ADDED
@@ -0,0 +1,334 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ // A phone's menu (App.tsx, under gui's `md`), drawn the way the ChatGPT iOS app
4
+ // draws its own: the menu stands BEHIND, on the window's subdued floor, and the
5
+ // pane slides toward the END side over it, raised — the pane's own brighter
6
+ // material, its start corners rounded, a drop falling on the menu. A strip of
7
+ // the pane stays in sight; a press on it, or a pull back toward the start, slides
8
+ // it home.
9
+ //
10
+ // ┌──────────────────────────┬────╮
11
+ // │ Hanzo AI 🔔 ⌕ │ │ the pane: STRIP px of it, inert but for
12
+ // │ ✎ New chat │ │ the press that closes (`drawer-close`)
13
+ // │ ◇ Studio ⌨ Tabs ⋯ More │ │
14
+ // │ Pinned … Recents … │ │
15
+ // │ (ZK) (✎ Chat) │ │ the foot: the account and the accent New chat
16
+ // └──────────────────────────┴────╯
17
+ //
18
+ // Three states, because the slide home has to be seen: shut (no menu, the pane
19
+ // as it always is), open, and closing (the menu still drawn, the pane sliding
20
+ // home), after which the menu unmounts. The pane's offset is a transform, so the
21
+ // slide is the compositor's; under reduced motion it is not animated at all.
22
+ //
23
+ // THE SLIDE IS LOGICAL but for its sign (slide.ts): the menu stands at the
24
+ // start, the strip at the end, both by logical insets; the transform's sign is
25
+ // read off the pane's own `direction` as the menu opens, so an RTL page (by
26
+ // @hanzo/i18n or the host's own `dir`) slides the pane left, and a drag reads
27
+ // along the line the same way.
28
+ //
29
+ // WHILE IT IS OUT the window behind it is inert (`inert` on every other child of
30
+ // the window) and the menu is a modal dialog: focus moves into it as it opens and
31
+ // back to what opened it as it shuts; Escape closes it, unless a menu or dialog
32
+ // it opened holds the focus; Back closes it, because opening it pushed an entry
33
+ // of the same address that Back pops. A move made from inside it takes that
34
+ // entry's place (`depart`), so the history holds the move and never the menu.
35
+ // Its list's scroll stays its own (Recents' `overscrollBehavior`), and the pane,
36
+ // never unmounted, keeps its own.
37
+ import { useI18n } from '@hanzo/i18n/react';
38
+ import { Text, XStack, YStack, useMedia } from '@hanzo/gui';
39
+ import { SquarePen } from 'lucide-react';
40
+ import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react';
41
+ import { Me } from './Me.js';
42
+ import { axis, OPEN, pull, settle, sign } from './slide.js';
43
+ /**
44
+ * The pane's offset as a transform: `--drawer-way` the end side's sign,
45
+ * `--drawer-at` the distance, both written on the pane by the drawer (a drag
46
+ * rewrites the distance every frame, no React commit). Its start corners round.
47
+ */
48
+ const SLIDE = {
49
+ transform: 'translateX(calc(var(--drawer-way, 1) * var(--drawer-at, 0px)))',
50
+ borderStartStartRadius: 'var(--radius-composer)',
51
+ borderEndStartRadius: 'var(--radius-composer)',
52
+ };
53
+ /** The slide's motion: the system's base duration on its entrance curve. */
54
+ const MOTION = 'transform var(--duration-base) var(--ease-emphasis)';
55
+ /** The element a gui stack hands its ref, on the web. */
56
+ const element = (node) => (node instanceof HTMLElement ? node : null);
57
+ /** The reader asked for no motion. */
58
+ function useStill() {
59
+ const [still, setStill] = useState(false);
60
+ useEffect(() => {
61
+ const query = window.matchMedia?.('(prefers-reduced-motion: reduce)');
62
+ if (!query)
63
+ return;
64
+ const read = () => setStill(query.matches);
65
+ read();
66
+ query.addEventListener('change', read);
67
+ return () => query.removeEventListener('change', read);
68
+ }, []);
69
+ return still;
70
+ }
71
+ /** The pane's distance from home, px, measured: where it stands against the layer, which stands where it rests. */
72
+ const travel = (pane, layer) => Math.abs(pane.getBoundingClientRect().left - layer.getBoundingClientRect().left);
73
+ export function useDrawer() {
74
+ const [state, setState] = useState('shut');
75
+ // Which way the end side is; read as the menu opens.
76
+ const [way, setWay] = useState(1);
77
+ const still = useStill();
78
+ const media = useMedia();
79
+ const win = useRef(null);
80
+ const pane = useRef(null);
81
+ const layer = useRef(null);
82
+ const opener = useRef(null);
83
+ // The entry opening pushed is the one the history stands on.
84
+ const marked = useRef(false);
85
+ const home = useCallback((now = false) => setState((s) => (s === 'shut' || now || still ? 'shut' : 'closing')), [still]);
86
+ const open = useCallback(() => {
87
+ const el = pane.current;
88
+ setWay(sign(el ? getComputedStyle(el).direction : 'ltr'));
89
+ const at = document.activeElement;
90
+ if (at instanceof HTMLElement && at !== document.body)
91
+ opener.current = at;
92
+ if (!marked.current) {
93
+ window.history.pushState(window.history.state, '');
94
+ marked.current = true;
95
+ }
96
+ setState('open');
97
+ }, []);
98
+ const close = useCallback(() => {
99
+ if (marked.current) {
100
+ marked.current = false;
101
+ window.history.back();
102
+ }
103
+ home();
104
+ }, [home]);
105
+ const depart = useCallback((move) => {
106
+ const replace = marked.current;
107
+ marked.current = false;
108
+ const before = window.location.href;
109
+ move(replace);
110
+ // Nothing moved (the reader was there already): the menu's entry is popped instead.
111
+ if (replace && window.location.href === before)
112
+ window.history.back();
113
+ home();
114
+ }, [home]);
115
+ const drop = useCallback(() => {
116
+ marked.current = false;
117
+ home();
118
+ }, [home]);
119
+ // Back closes it: the entry it pushed is the one popped.
120
+ useEffect(() => {
121
+ if (state !== 'open')
122
+ return;
123
+ const back = () => {
124
+ if (!marked.current)
125
+ return;
126
+ marked.current = false;
127
+ home();
128
+ };
129
+ window.addEventListener('popstate', back);
130
+ return () => window.removeEventListener('popstate', back);
131
+ }, [state, home]);
132
+ // Escape closes it, unless a menu or dialog it opened holds the focus: that one closes first.
133
+ useEffect(() => {
134
+ if (state !== 'open')
135
+ return;
136
+ const key = (e) => {
137
+ if (e.key !== 'Escape' || e.defaultPrevented)
138
+ return;
139
+ const at = document.activeElement;
140
+ if (at && at !== document.body && !layer.current?.contains(at))
141
+ return;
142
+ e.preventDefault();
143
+ close();
144
+ };
145
+ document.addEventListener('keydown', key);
146
+ return () => document.removeEventListener('keydown', key);
147
+ }, [state, close]);
148
+ // A window that grows past a phone has no drawer: it goes at once.
149
+ useEffect(() => {
150
+ if (!media.md || state === 'shut')
151
+ return;
152
+ if (marked.current) {
153
+ marked.current = false;
154
+ window.history.back();
155
+ }
156
+ home(true);
157
+ }, [media.md, state, home]);
158
+ // Everything in the window but the menu is inert while it is out, and the focus goes in.
159
+ useLayoutEffect(() => {
160
+ const w = win.current;
161
+ const out = state !== 'shut';
162
+ if (w)
163
+ for (const child of Array.from(w.children))
164
+ if (child !== layer.current && child instanceof HTMLElement)
165
+ child.inert = out;
166
+ if (state === 'open')
167
+ layer.current?.focus({ preventScroll: true });
168
+ if (state !== 'shut')
169
+ return;
170
+ // Shut: the focus goes back to what opened it, where nothing else took it.
171
+ const back = opener.current;
172
+ opener.current = null;
173
+ const at = document.activeElement;
174
+ if (back?.isConnected && (!at || at === document.body))
175
+ back.focus({ preventScroll: true });
176
+ }, [state]);
177
+ // The slide, written on the pane: its way, its motion, and how far it stands from home.
178
+ useLayoutEffect(() => {
179
+ const s = pane.current?.style;
180
+ if (!s)
181
+ return;
182
+ if (state === 'shut') {
183
+ for (const p of ['--drawer-at', '--drawer-way', 'transition'])
184
+ s.removeProperty(p);
185
+ return;
186
+ }
187
+ s.setProperty('--drawer-way', String(way));
188
+ s.setProperty('transition', still ? 'none' : MOTION);
189
+ s.setProperty('--drawer-at', state === 'open' ? OPEN : '0px');
190
+ }, [state, way, still]);
191
+ // Closing ends when the slide does, or after it would have.
192
+ useEffect(() => {
193
+ if (state !== 'closing')
194
+ return;
195
+ const el = pane.current;
196
+ const done = () => setState((s) => (s === 'closing' ? 'shut' : s));
197
+ const end = (e) => {
198
+ if (e.target === el && e.propertyName === 'transform')
199
+ done();
200
+ };
201
+ el?.addEventListener('transitionend', end);
202
+ const late = window.setTimeout(done, 700);
203
+ return () => {
204
+ el?.removeEventListener('transitionend', end);
205
+ window.clearTimeout(late);
206
+ };
207
+ }, [state]);
208
+ // A finger on the menu or the strip pulls the pane along the line; a scroll stays a scroll.
209
+ useEffect(() => {
210
+ const el = pane.current;
211
+ const over = layer.current;
212
+ if (state !== 'open' || !el || !over)
213
+ return;
214
+ let grip = null;
215
+ let pulling = false;
216
+ let at = 0;
217
+ let last = { along: 0, t: 0 };
218
+ let speed = 0;
219
+ const down = (e) => {
220
+ if (e.pointerType === 'mouse' && e.button !== 0)
221
+ return;
222
+ grip = { id: e.pointerId, x: e.clientX, y: e.clientY, reach: travel(el, over) };
223
+ pulling = false;
224
+ at = grip.reach;
225
+ last = { along: 0, t: e.timeStamp };
226
+ speed = 0;
227
+ };
228
+ const move = (e) => {
229
+ if (!grip || e.pointerId !== grip.id)
230
+ return;
231
+ const dx = e.clientX - grip.x;
232
+ if (!pulling) {
233
+ const a = axis(dx, e.clientY - grip.y);
234
+ if (a === null)
235
+ return;
236
+ if (a === 'y') {
237
+ grip = null;
238
+ return;
239
+ }
240
+ pulling = true;
241
+ over.setPointerCapture?.(e.pointerId);
242
+ el.style.setProperty('transition', 'none');
243
+ }
244
+ e.preventDefault();
245
+ at = pull(grip.reach, dx, way, grip.reach);
246
+ const along = dx * way;
247
+ const dt = e.timeStamp - last.t;
248
+ if (dt > 0)
249
+ speed = (along - last.along) / dt;
250
+ last = { along, t: e.timeStamp };
251
+ el.style.setProperty('--drawer-at', `${at}px`);
252
+ };
253
+ const up = (e) => {
254
+ if (!grip || e.pointerId !== grip.id)
255
+ return;
256
+ const reach = grip.reach;
257
+ grip = null;
258
+ if (!pulling)
259
+ return;
260
+ pulling = false;
261
+ el.style.setProperty('transition', still ? 'none' : MOTION);
262
+ // The press that ends a pull is not a tap on the strip.
263
+ const swallow = (c) => {
264
+ c.stopPropagation();
265
+ c.preventDefault();
266
+ };
267
+ window.addEventListener('click', swallow, { capture: true, once: true });
268
+ window.setTimeout(() => window.removeEventListener('click', swallow, { capture: true }), 0);
269
+ if (e.type === 'pointerup' && !settle(at, reach, speed))
270
+ close();
271
+ else
272
+ el.style.setProperty('--drawer-at', OPEN);
273
+ };
274
+ over.addEventListener('pointerdown', down);
275
+ over.addEventListener('pointermove', move);
276
+ over.addEventListener('pointerup', up);
277
+ over.addEventListener('pointercancel', up);
278
+ return () => {
279
+ over.removeEventListener('pointerdown', down);
280
+ over.removeEventListener('pointermove', move);
281
+ over.removeEventListener('pointerup', up);
282
+ over.removeEventListener('pointercancel', up);
283
+ };
284
+ }, [state, way, still, close]);
285
+ return {
286
+ state,
287
+ open,
288
+ close,
289
+ depart,
290
+ drop,
291
+ window: useCallback((node) => {
292
+ win.current = element(node);
293
+ }, []),
294
+ pane: useCallback((node) => {
295
+ pane.current = element(node);
296
+ }, []),
297
+ layer: useCallback((node) => {
298
+ layer.current = element(node);
299
+ }, []),
300
+ slide: state === 'shut' ? null : SLIDE,
301
+ };
302
+ }
303
+ /**
304
+ * The pane while the menu is out: raised over the floor, its edge drawn and a
305
+ * drop falling on the menu, clipped to its corners. Spread on the pane beside
306
+ * `slide`; `Lift` is its fill.
307
+ */
308
+ export const RAISED = {
309
+ z: 'var(--z-raised)',
310
+ overflow: 'hidden',
311
+ boxShadow: '0 0 0 1px var(--pane-edge), var(--shadow-floating)',
312
+ };
313
+ /**
314
+ * The raised pane's fill: the pane's own material composited opaque, a step
315
+ * brighter than the floor the menu stands on, in both themes. Drawn UNDER the
316
+ * pane's content (`--z-under` inside the pane's own stacking) rather than as the
317
+ * pane's background, which is the frame's and a host may paint it at rest.
318
+ */
319
+ export function Lift() {
320
+ return _jsx(YStack, { "data-slot": "lift", position: "absolute", inset: 0, bg: "var(--pane-solid)", z: "var(--z-under)", pointerEvents: "none" });
321
+ }
322
+ /**
323
+ * The menu's layer: the menu at the start on the floor, and over the strip of
324
+ * pane left in sight the press that closes it. Drawn while the drawer is not shut.
325
+ */
326
+ export function Layer({ drawer, top = 0, children }) {
327
+ const { t } = useI18n();
328
+ return (_jsxs(YStack, { ref: drawer.layer, role: "dialog", "aria-modal": true, "aria-label": t('Menu'), tabIndex: -1, "data-slot": "drawer", "data-state": drawer.state, position: "absolute", t: top, b: 0, insetInlineStart: 0, insetInlineEnd: 0, outlineStyle: "none", $md: { display: 'none' }, children: [_jsx(YStack, { "data-slot": "menu", position: "absolute", t: 0, b: 0, insetInlineStart: 0, width: OPEN, bg: "var(--pane-floor)", pt: "var(--safe-top)", pb: "max(var(--pane-gap), var(--safe-bottom))", ...{ paddingInlineStart: 'max(var(--safe-left), var(--safe-right))', touchAction: 'pan-y' }, children: children }), drawer.state === 'open' ? (_jsx(YStack, { render: "button", "data-slot": "drawer-close", "aria-label": t('Close sidebar'), onPress: drawer.close, position: "absolute", t: 0, b: 0, insetInlineEnd: 0, z: "var(--z-panel)", bg: "transparent", borderWidth: 0, cursor: "pointer", ...{ insetInlineStart: OPEN, touchAction: 'none' }, focusVisibleStyle: { outlineWidth: 2, outlineStyle: 'solid', outlineColor: '$outlineColor' } })) : null] }));
329
+ }
330
+ /** The menu's foot: the account (its menu opens above it) and the accent New chat. */
331
+ export function Foot({ host, onChat, onOrg }) {
332
+ const { t } = useI18n();
333
+ return (_jsxs(XStack, { "data-slot": "drawer-foot", items: "center", justify: "space-between", gap: "$2", pt: "$2", px: "$1", shrink: 0, children: [_jsx(Me, { host: host, narrow: true, above: true, onOrg: onOrg }), _jsxs(XStack, { render: "button", "data-slot": "drawer-chat", "aria-label": t('New chat'), onPress: onChat, height: 44, px: "$4", gap: "$2", rounded: 9999, items: "center", justify: "center", bg: "var(--accent)", borderWidth: 0, cursor: "pointer", hoverStyle: { bg: 'var(--accent-hover)' }, pressStyle: { bg: 'var(--accent-hover)' }, focusVisibleStyle: { outlineWidth: 2, outlineStyle: 'solid', outlineColor: '$outlineColor' }, children: [_jsx(SquarePen, { size: 18, color: "var(--accent-foreground)", "aria-hidden": true }), _jsx(Text, { fontSize: "$4", fontWeight: "600", color: "var(--accent-foreground)", children: t('Chat') })] })] }));
334
+ }
package/dist/Me.d.ts CHANGED
@@ -26,8 +26,8 @@ export declare function Me({ host, narrow, above, onOrg, }: {
26
26
  /** The rail's: the face alone, the menu opening beside it. */
27
27
  narrow?: boolean;
28
28
  /**
29
- * The menu opens above the face, not beside it: the rail laid along a phone
30
- * drawer's foot. That copy leaves ⌘/ to the rail's own, so one press opens one dialog.
29
+ * The menu opens above the face, not beside it: the foot of a phone's menu
30
+ * (Drawer.tsx). That copy leaves ⌘/ to the rail's own, so one press opens one dialog.
31
31
  */
32
32
  above?: boolean;
33
33
  /** Work in another organization: picked, then the app opened in it. */
package/dist/Me.js CHANGED
@@ -28,6 +28,7 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
28
28
  // the plan from billing and the limits. An organization is created by the same
29
29
  // `POST /v1/account/orgs` the no-organization gate uses (Orgs.tsx); the next load
30
30
  // mints a token that carries it.
31
+ import { useI18n } from '@hanzo/i18n/react';
31
32
  import { useEffect, useMemo, useRef, useState } from 'react';
32
33
  import { SizableText, XStack, YStack } from '@hanzo/gui';
33
34
  import { ArrowUpRight, Building2, Check, ChevronsUpDown, CircleHelp, CircleUserRound, Download, FileText, FlaskConical, Info, Keyboard, LifeBuoy, LogOut, Palette, Plus, ScrollText, Settings, ShieldCheck, Sparkles, Users, X, } from 'lucide-react';
@@ -135,6 +136,7 @@ export const upgrade = (family) => family === 'Enterprise' ? 'Enterprise plan' :
135
136
  /** The keys a row prints, in this platform's spelling. */
136
137
  const mac = () => typeof navigator !== 'undefined' && /Mac|iPhone|iPad/.test(navigator.platform || navigator.userAgent);
137
138
  export function Me({ host, narrow = false, above = false, onOrg, }) {
139
+ const { t } = useI18n();
138
140
  const [open, setOpen] = useState(false);
139
141
  const [account, setAccount] = useState(false);
140
142
  const [help, setHelp] = useState(false);
@@ -197,11 +199,14 @@ export function Me({ host, narrow = false, above = false, onOrg, }) {
197
199
  // Clear of the rail's mark (or the row) it opens from, and flush with its foot.
198
200
  offset: 8, children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsxs(XStack, { ref: (el) => {
199
201
  row.current = el instanceof HTMLElement ? el : null;
200
- }, render: "button", "data-slot": "me", "aria-label": `Account: ${[who, current].filter(Boolean).join(' · ')}`, ...{ title: who }, width: narrow ? 36 : '100%', minH: narrow ? 36 : 48, px: narrow ? 0 : '$2', gap: "$2.5", rounded: narrow ? 999 : '$5', items: "center", justify: narrow ? 'center' : 'flex-start', hoverStyle: { bg: '$hover' }, focusVisibleStyle: { outlineWidth: 2, outlineStyle: 'solid', outlineColor: '$outlineColor' }, cursor: "pointer", children: [_jsx(Face, { host: host, size: narrow ? 26 : 32 }), narrow ? null : (_jsxs(_Fragment, { children: [_jsxs(YStack, { flex: 1, minW: 0, children: [_jsx(SizableText, { size: "$3", fontWeight: "600", color: "$ink", numberOfLines: 1, text: "left", children: who }), working ? (_jsx(SizableText, { size: "$1", color: "$soft", numberOfLines: 1, text: "left", children: working })) : null] }), _jsx(ChevronsUpDown, { size: 14, "aria-hidden": true, opacity: 0.6 })] }))] }) }), _jsxs(DropdownMenuContent, { minW: 260, maxW: "calc(100vw - 16px)", "aria-labelledby": undefined, "aria-label": "Account", onCloseAutoFocus: back, children: [_jsxs(DropdownMenuSub, { open: account, onOpenChange: setAccount, children: [_jsxs(DropdownMenuSubTrigger, { minH: 48, gap: "$2.5", children: [_jsx(Face, { host: host, size: 28 }), _jsxs(YStack, { flex: 1, minW: 0, children: [_jsx(SizableText, { size: "$2", fontWeight: "600", color: "$ink", numberOfLines: 1, children: who }), _jsx(SizableText, { size: "$1", color: "$soft", numberOfLines: 1, children: host.person.email || working })] })] }), account ? (_jsxs(DropdownMenuSubContent, { minW: 260, maxW: "calc(100vw - 16px)", pointerEvents: "auto", children: [_jsx(Meter, { read: read, onPlans: () => leave('here'), onBilling: () => (setOpen(false), showSettings('billing')) }), _jsx(DropdownMenuSeparator, {}), _jsx(DropdownMenuLabel, { children: "Organizations" }), all.map((o) => (_jsx(DropdownMenuItem, { onSelect: () => o.name !== current && onOrg(o.name), children: _jsx(Line, { icon: _jsx(Mark, { org: o }), sub: [o.personal ? 'Personal' : said(o.role), planFor(o)].filter(Boolean).join(' · '), trail: o.name === current ? _jsx(Check, { size: 16, "aria-label": "Current" }) : null, children: o.display }) }, o.name))), _jsx(DropdownMenuItem, { onSelect: () => setCreating(true), children: _jsx(Line, { icon: _jsx(Plus, { size: 16, "aria-hidden": true }), children: "Create organization" }) }), _jsx(DropdownMenuItem, { onSelect: () => window.location.assign(site(ORG_SETTINGS)), children: _jsx(Line, { icon: _jsx(Settings, { size: 16, "aria-hidden": true }), children: "Organization settings" }) }), where().team ? null : here && !here.personal ? (_jsx(DropdownMenuItem, { onSelect: () => out(`${ENTRY}&org=${encodeURIComponent(here.name)}`), children: _jsx(Line, { icon: _jsx(Users, { size: 16, "aria-hidden": true }), trail: _jsx(ArrowUpRight, { size: 14, "aria-hidden": true, opacity: 0.6 }), children: "Open in Hanzo Team" }) })) : (_jsx(DropdownMenuItem, { onSelect: () => setCreating(true), children: _jsx(Line, { icon: _jsx(Users, { size: 16, "aria-hidden": true }), children: "Create a team" }) })), superAdmin() ? (_jsx(DropdownMenuItem, { onSelect: choose, children: _jsx(Line, { icon: _jsx(Building2, { size: 16, "aria-hidden": true }), children: "All organizations" }) })) : null] })) : null] }), team ? (_jsx(DropdownMenuItem, { "data-slot": "me-team", onSelect: () => team.name !== current && onOrg(team.name), children: _jsx(Line, { icon: _jsx(Mark, { org: team }), trail: team.name === current ? _jsx(Check, { size: 16, "aria-label": "Current" }) : null, children: `${team.display} · ${teamPlan || 'No plan'}` }) })) : null, personal ? (_jsx(DropdownMenuItem, { "data-slot": "me-personal", onSelect: () => personal.name !== current && onOrg(personal.name), actionsVisibility: "always", actions: ownPlan ? undefined : (_jsx(Button, { size: "xs", variant: "outline", onPress: getPro, children: "Get Pro" })), children: _jsx(Line, { icon: _jsx(CircleUserRound, { size: 16, "aria-hidden": true }), trail: personal.name === current ? _jsx(Check, { size: 16, "aria-label": "Current" }) : null, children: `Personal · ${ownPlan || 'No plan'}` }) })) : null, _jsx(DropdownMenuSeparator, {}), _jsx(DropdownMenuItem, { onSelect: () => leave('here'), children: _jsx(Line, { icon: _jsx(Sparkles, { size: 16, "aria-hidden": true }), children: upgrade(read.value?.label?.name) }) }), _jsx(DropdownMenuItem, { onSelect: () => showSettings('general'), children: _jsx(Line, { icon: _jsx(Palette, { size: 16, "aria-hidden": true }), children: "Personalization" }) }), _jsx(DropdownMenuItem, { onSelect: () => showSettings('account'), children: _jsx(Line, { icon: _jsx(CircleUserRound, { size: 16, "aria-hidden": true }), children: "Profile" }) }), _jsx(DropdownMenuItem, { "aria-keyshortcuts": "Control+, Meta+,", onSelect: () => showSettings('general'), children: _jsx(Line, { icon: _jsx(Settings, { size: 16, "aria-hidden": true }), trail: _jsx(DropdownMenuShortcut, { "aria-hidden": true, children: apple ? '⌘,' : 'Ctrl+,' }), children: "Settings" }) }), _jsx(DropdownMenuItem, { onSelect: () => window.location.assign(site('/beta/status')), children: _jsx(Line, { icon: _jsx(FlaskConical, { size: 16, "aria-hidden": true }), children: "Beta" }) }), _jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuSub, { open: help, onOpenChange: setHelp, children: [_jsx(DropdownMenuSubTrigger, { children: _jsx(Line, { icon: _jsx(LifeBuoy, { size: 16, "aria-hidden": true }), children: "Help" }) }), help ? (_jsxs(DropdownMenuSubContent, { minW: 240, maxW: "calc(100vw - 16px)", pointerEvents: "auto", children: [pages().map((p) => (_jsx(DropdownMenuItem, { onSelect: () => out(p.href), children: _jsx(Line, { icon: ICONS[p.key], trail: p.key === 'help' ? _jsx(ArrowUpRight, { size: 14, "aria-hidden": true, opacity: 0.6 }) : null, children: p.label }) }, p.key))), _jsx(DropdownMenuItem, { "aria-keyshortcuts": "Control+/ Meta+/", onSelect: () => setKeys(true), children: _jsx(Line, { icon: _jsx(Keyboard, { size: 16, "aria-hidden": true }), trail: _jsx(DropdownMenuShortcut, { "aria-hidden": true, children: apple ? '⌘/' : 'Ctrl+/' }), children: "Keyboard shortcuts" }) }), _jsx(DropdownMenuSeparator, {}), legal().map((p) => (_jsx(DropdownMenuItem, { onSelect: () => out(p.href), children: _jsx(Line, { icon: ICONS[p.key], children: p.label }) }, p.key)))] })) : null] }), host.signOut ? (_jsx(DropdownMenuItem, { onSelect: () => host.signOut?.(), children: _jsx(Line, { icon: _jsx(LogOut, { size: 16, "aria-hidden": true }), children: "Log out" }) })) : null] })] }), _jsx(Create, { open: creating, onOpenChange: setCreating }), _jsx(Shortcuts, { open: keys, onOpenChange: setKeys, apple: apple })] }));
202
+ }, render: "button", "data-slot": "me", "aria-label": `Account: ${[who, current].filter(Boolean).join(' · ')}`, ...{ title: who }, width: narrow ? 36 : '100%', minH: narrow ? 36 : 48,
203
+ // A thumb's 44 on a touch screen.
204
+ $touchable: narrow ? { width: 44, minH: 44 } : undefined, px: narrow ? 0 : '$2', gap: "$2.5", rounded: narrow ? 999 : '$5', items: "center", justify: narrow ? 'center' : 'flex-start', hoverStyle: { bg: '$hover' }, focusVisibleStyle: { outlineWidth: 2, outlineStyle: 'solid', outlineColor: '$outlineColor' }, cursor: "pointer", children: [_jsx(Face, { host: host, size: narrow ? 26 : 32 }), narrow ? null : (_jsxs(_Fragment, { children: [_jsxs(YStack, { flex: 1, minW: 0, children: [_jsx(SizableText, { size: "$3", fontWeight: "600", color: "$ink", numberOfLines: 1, text: "left", children: who }), working ? (_jsx(SizableText, { size: "$1", color: "$soft", numberOfLines: 1, text: "left", children: working })) : null] }), _jsx(ChevronsUpDown, { size: 14, "aria-hidden": true, opacity: 0.6 })] }))] }) }), _jsxs(DropdownMenuContent, { minW: 260, maxW: "calc(100vw - 16px)", "aria-labelledby": undefined, "aria-label": t("Account"), onCloseAutoFocus: back, children: [_jsxs(DropdownMenuSub, { open: account, onOpenChange: setAccount, children: [_jsxs(DropdownMenuSubTrigger, { minH: 48, gap: "$2.5", children: [_jsx(Face, { host: host, size: 28 }), _jsxs(YStack, { flex: 1, minW: 0, children: [_jsx(SizableText, { size: "$2", fontWeight: "600", color: "$ink", numberOfLines: 1, children: who }), _jsx(SizableText, { size: "$1", color: "$soft", numberOfLines: 1, children: host.person.email || working })] })] }), account ? (_jsxs(DropdownMenuSubContent, { minW: 260, maxW: "calc(100vw - 16px)", pointerEvents: "auto", children: [_jsx(Meter, { read: read, onPlans: () => leave('here'), onBilling: () => (setOpen(false), showSettings('billing')) }), _jsx(DropdownMenuSeparator, {}), _jsx(DropdownMenuLabel, { children: t("Organizations") }), all.map((o) => (_jsx(DropdownMenuItem, { onSelect: () => o.name !== current && onOrg(o.name), children: _jsx(Line, { icon: _jsx(Mark, { org: o }), sub: [o.personal ? 'Personal' : said(o.role), planFor(o)].filter(Boolean).join(' · '), trail: o.name === current ? _jsx(Check, { size: 16, "aria-label": t("Current") }) : null, children: o.display }) }, o.name))), _jsx(DropdownMenuItem, { onSelect: () => setCreating(true), children: _jsx(Line, { icon: _jsx(Plus, { size: 16, "aria-hidden": true }), children: t("Create organization") }) }), _jsx(DropdownMenuItem, { onSelect: () => out(site(ORG_SETTINGS)), children: _jsx(Line, { icon: _jsx(Settings, { size: 16, "aria-hidden": true }), children: t("Organization settings") }) }), where().team ? null : here && !here.personal ? (_jsx(DropdownMenuItem, { onSelect: () => out(`${ENTRY}&org=${encodeURIComponent(here.name)}`), children: _jsx(Line, { icon: _jsx(Users, { size: 16, "aria-hidden": true }), trail: _jsx(ArrowUpRight, { size: 14, "aria-hidden": true, opacity: 0.6 }), children: t("Open in Hanzo Team") }) })) : (_jsx(DropdownMenuItem, { onSelect: () => setCreating(true), children: _jsx(Line, { icon: _jsx(Users, { size: 16, "aria-hidden": true }), children: t("Create a team") }) })), superAdmin() ? (_jsx(DropdownMenuItem, { onSelect: choose, children: _jsx(Line, { icon: _jsx(Building2, { size: 16, "aria-hidden": true }), children: t("All organizations") }) })) : null] })) : null] }), team ? (_jsx(DropdownMenuItem, { "data-slot": "me-team", onSelect: () => team.name !== current && onOrg(team.name), children: _jsx(Line, { icon: _jsx(Mark, { org: team }), trail: team.name === current ? _jsx(Check, { size: 16, "aria-label": t("Current") }) : null, children: `${team.display} · ${teamPlan || 'No plan'}` }) })) : null, personal ? (_jsx(DropdownMenuItem, { "data-slot": "me-personal", onSelect: () => personal.name !== current && onOrg(personal.name), actionsVisibility: "always", actions: ownPlan ? undefined : (_jsx(Button, { size: "xs", variant: "outline", onPress: getPro, children: t("Get Pro") })), children: _jsx(Line, { icon: _jsx(CircleUserRound, { size: 16, "aria-hidden": true }), trail: personal.name === current ? _jsx(Check, { size: 16, "aria-label": t("Current") }) : null, children: `Personal · ${ownPlan || 'No plan'}` }) })) : null, _jsx(DropdownMenuSeparator, {}), _jsx(DropdownMenuItem, { onSelect: () => leave('here'), children: _jsx(Line, { icon: _jsx(Sparkles, { size: 16, "aria-hidden": true }), children: upgrade(read.value?.label?.name) }) }), _jsx(DropdownMenuItem, { onSelect: () => showSettings('general'), children: _jsx(Line, { icon: _jsx(Palette, { size: 16, "aria-hidden": true }), children: t("Personalization") }) }), _jsx(DropdownMenuItem, { onSelect: () => showSettings('account'), children: _jsx(Line, { icon: _jsx(CircleUserRound, { size: 16, "aria-hidden": true }), children: t("Profile") }) }), _jsx(DropdownMenuItem, { "aria-keyshortcuts": "Control+, Meta+,", onSelect: () => showSettings('general'), children: _jsx(Line, { icon: _jsx(Settings, { size: 16, "aria-hidden": true }), trail: _jsx(DropdownMenuShortcut, { "aria-hidden": true, children: apple ? '⌘,' : 'Ctrl+,' }), children: t("Settings") }) }), _jsx(DropdownMenuItem, { onSelect: () => window.location.assign(site('/beta/status')), children: _jsx(Line, { icon: _jsx(FlaskConical, { size: 16, "aria-hidden": true }), children: t("Beta") }) }), _jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuSub, { open: help, onOpenChange: setHelp, children: [_jsx(DropdownMenuSubTrigger, { children: _jsx(Line, { icon: _jsx(LifeBuoy, { size: 16, "aria-hidden": true }), children: t("Help") }) }), help ? (_jsxs(DropdownMenuSubContent, { minW: 240, maxW: "calc(100vw - 16px)", pointerEvents: "auto", children: [pages().map((p) => (_jsx(DropdownMenuItem, { onSelect: () => out(p.href), children: _jsx(Line, { icon: ICONS[p.key], trail: p.key === 'help' ? _jsx(ArrowUpRight, { size: 14, "aria-hidden": true, opacity: 0.6 }) : null, children: p.label }) }, p.key))), _jsx(DropdownMenuItem, { "aria-keyshortcuts": "Control+/ Meta+/", onSelect: () => setKeys(true), children: _jsx(Line, { icon: _jsx(Keyboard, { size: 16, "aria-hidden": true }), trail: _jsx(DropdownMenuShortcut, { "aria-hidden": true, children: apple ? '⌘/' : 'Ctrl+/' }), children: t("Keyboard shortcuts") }) }), _jsx(DropdownMenuSeparator, {}), legal().map((p) => (_jsx(DropdownMenuItem, { onSelect: () => out(p.href), children: _jsx(Line, { icon: ICONS[p.key], children: p.label }) }, p.key)))] })) : null] }), host.signOut ? (_jsx(DropdownMenuItem, { onSelect: () => host.signOut?.(), children: _jsx(Line, { icon: _jsx(LogOut, { size: 16, "aria-hidden": true }), children: t("Log out") }) })) : null] })] }), _jsx(Create, { open: creating, onOpenChange: setCreating }), _jsx(Shortcuts, { open: keys, onOpenChange: setKeys, apple: apple })] }));
201
205
  }
202
206
  /** The app's keys (help.ts `KEYS`), one row each, spelled for this platform. */
203
207
  function Shortcuts({ open, onOpenChange, apple }) {
204
- return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { maxW: 440, rounded: "$6", gap: "$3", children: [_jsx(DialogTitle, { children: "Keyboard shortcuts" }), _jsx(YStack, { gap: "$1", children: KEYS.map((k) => (_jsxs(XStack, { items: "center", justify: "space-between", gap: "$3", minH: 32, children: [_jsx(SizableText, { size: "$2", color: "$ink", children: k.does }), _jsx(Kbd, { children: apple ? k.mac : k.pc })] }, k.does))) })] }) }));
208
+ const { t } = useI18n();
209
+ return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { maxW: 440, rounded: "$6", gap: "$3", children: [_jsx(DialogTitle, { children: t("Keyboard shortcuts") }), _jsx(YStack, { gap: "$1", children: KEYS.map((k) => (_jsxs(XStack, { items: "center", justify: "space-between", gap: "$3", minH: 32, children: [_jsx(SizableText, { size: "$2", color: "$ink", children: k.does }), _jsx(Kbd, { children: apple ? k.mac : k.pc })] }, k.does))) })] }) }));
205
210
  }
206
211
  /**
207
212
  * The sheet that creates an organization: a name, then `POST /v1/account/orgs`,
@@ -210,6 +215,7 @@ function Shortcuts({ open, onOpenChange, apple }) {
210
215
  * it yet.
211
216
  */
212
217
  function Create({ open, onOpenChange }) {
218
+ const { t } = useI18n();
213
219
  const [name, setName] = useState('');
214
220
  const [busy, setBusy] = useState(false);
215
221
  const [refused, setRefused] = useState('');
@@ -244,16 +250,17 @@ function Create({ open, onOpenChange }) {
244
250
  setBusy(false);
245
251
  }
246
252
  };
247
- return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { maxW: 440, rounded: "$6", gap: "$3", children: [_jsx(DialogTitle, { children: "Create organization" }), _jsx(SizableText, { size: "$2", color: "$soft", children: "You own it. Agents, credits and members belong to it, and you switch to it from this menu." }), _jsx(Input, { autoFocus: true, value: name, onChangeText: setName, "aria-label": "Organization name", placeholder: "Acme", maxLength: 80, onKeyDown: (e) => {
253
+ return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { maxW: 440, rounded: "$6", gap: "$3", children: [_jsx(DialogTitle, { children: t("Create organization") }), _jsx(SizableText, { size: "$2", color: "$soft", children: t("You own it. Agents, credits and members belong to it, and you switch to it from this menu.") }), _jsx(Input, { autoFocus: true, value: name, onChangeText: setName, "aria-label": t("Organization name"), placeholder: t("Acme"), maxLength: 80, onKeyDown: (e) => {
248
254
  if (e.key === 'Enter')
249
255
  void submit();
250
- } }), refused ? (_jsx(SizableText, { size: "$1", color: BAD, role: "alert", children: refused })) : null, _jsxs(XStack, { gap: "$2", justify: "flex-end", children: [_jsx(Button, { size: "sm", variant: "outline", rounded: 999, onPress: () => onOpenChange(false), children: "Cancel" }), _jsx(Button, { size: "sm", rounded: 999, disabled: busy || !name.trim(), onPress: () => void submit(), children: busy ? 'Creating…' : 'Create' })] })] }) }));
256
+ } }), refused ? (_jsx(SizableText, { size: "$1", color: BAD, role: "alert", children: refused })) : null, _jsxs(XStack, { gap: "$2", justify: "flex-end", children: [_jsx(Button, { size: "sm", variant: "outline", rounded: 999, onPress: () => onOpenChange(false), children: t("Cancel") }), _jsx(Button, { size: "sm", rounded: 999, disabled: busy || !name.trim(), onPress: () => void submit(), children: busy ? 'Creating…' : 'Create' })] })] }) }));
251
257
  }
252
258
  /**
253
259
  * What became of an organization this tab created: nothing to say once the token
254
260
  * carries it (the app is already in it), and a plain line while it does not.
255
261
  */
256
262
  export function Made() {
263
+ const { t } = useI18n();
257
264
  const [made, setMade] = useState(null);
258
265
  useEffect(() => {
259
266
  try {
@@ -284,5 +291,5 @@ export function Made() {
284
291
  }
285
292
  setMade(null);
286
293
  };
287
- return (_jsxs(XStack, { "data-slot": "made", role: "status", items: "flex-start", gap: "$2", px: "$3", py: "$2.5", rounded: "$5", borderWidth: 1, borderColor: "$borderColor", bg: "$edge", children: [_jsx(SizableText, { flex: 1, minW: 0, size: "$1", color: "$ink", children: `${made.name} was created, and your sign-in does not include it yet, so you are still in this organization. It appears here once IAM adds you to it.` }), _jsx(XStack, { render: "button", "aria-label": "Dismiss", onPress: dismiss, p: "$1", rounded: 999, hoverStyle: { bg: '$hover' }, children: _jsx(X, { size: 13, "aria-hidden": true }) })] }));
294
+ return (_jsxs(XStack, { "data-slot": "made", role: "status", items: "flex-start", gap: "$2", px: "$3", py: "$2.5", rounded: "$5", borderWidth: 1, borderColor: "$borderColor", bg: "$edge", children: [_jsx(SizableText, { flex: 1, minW: 0, size: "$1", color: "$ink", children: `${made.name} was created, and your sign-in does not include it yet, so you are still in this organization. It appears here once IAM adds you to it.` }), _jsx(XStack, { render: "button", "aria-label": t("Dismiss"), onPress: dismiss, p: "$1", rounded: 999, hoverStyle: { bg: '$hover' }, children: _jsx(X, { size: 13, "aria-hidden": true }) })] }));
288
295
  }
package/dist/Orgs.js CHANGED
@@ -277,7 +277,7 @@ function useArrival(door, hydrated, isLoading, isAuthenticated) {
277
277
  const SHEET = 'calc(var(--z-modal) + 10)';
278
278
  const DIALOG = 'calc(var(--z-modal) + 20)';
279
279
  /** The sheet's top inset: a share of the window's height, held between two lengths. */
280
- const TOP = 'clamp(32px, 12vh, 120px)';
280
+ const TOP = 'clamp(72px, 12vh, 120px)';
281
281
  const muted = 'var(--muted-foreground)';
282
282
  /** The black that veils and shadows, at a share of itself. */
283
283
  const shade = (share) => mix('var(--pure-black)', share, 'srgb');
package/dist/Palette.js CHANGED
@@ -79,7 +79,7 @@ export function places(host, go) {
79
79
  { id: 'tabs', label: 'Tabs', icon: _jsx(SquareTerminal, { ...ICON }), run: () => go(TERMINALS) },
80
80
  ...(host ? [{ id: 'team', label: 'Team', icon: _jsx(Users, { ...ICON }), keywords: ['workspace'], run: () => out(WORKSPACE) }] : []),
81
81
  ...(host ? rows(nav(host, builder).map((g) => ({ links: g.links.filter((l) => l.id !== NEW) })), '', 'dev') : []),
82
- { id: 'settings', label: 'Settings', icon: _jsx(Settings, { ...ICON }), shortcut: ['Mod', ','], run: () => go(app(SETTINGS)) },
82
+ { id: 'settings', label: 'Settings', icon: _jsx(Settings, { ...ICON }), shortcut: ['Mod', ','], run: () => go('/settings') },
83
83
  // The site's own pages, outside the app: the browser loads them.
84
84
  { id: 'referral', label: 'Referral', icon: _jsx(Gift, { ...ICON }), keywords: ['invite', 'share'], run: () => window.location.assign(site('/referral')) },
85
85
  { id: 'beta', label: 'Beta status', icon: _jsx(FlaskConical, { ...ICON }), keywords: ['waitlist'], run: () => window.location.assign(site('/beta/status')) },
@@ -107,7 +107,8 @@ const MARKS = {
107
107
  // disc (zoo-disc.svg): top green, bottom-left red, bottom-right blue, whose
108
108
  // pairwise overlaps are the secondaries the full disc shows. The enclosing
109
109
  // ring stays `currentColor` — it is the frame, not one of the three, and a
110
- // fourth hue there would read as a fourth petal.
110
+ // fourth hue there would read as a fourth petal. Under `ink` the petals'
111
+ // strokes come off (`INKED`) and they take the group's `currentColor`.
111
112
  '<defs><clipPath id="zoo-cut"><circle cx="12" cy="12" r="11.5"></circle></clipPath></defs><g clip-path="url(#zoo-cut)" fill="none" stroke="currentColor"><circle cx="12.203" cy="6.27" r="9.509" stroke-width="1.341" stroke="#00A652"></circle><circle cx="6.189" cy="15.454" r="9.509" stroke-width="1.341" stroke="#ED1C24"></circle><circle cx="17.486" cy="15.454" r="9.509" stroke-width="1.341" stroke="#2E3192"></circle><circle cx="12" cy="12" r="10.769" stroke-width="1.463"></circle></g>',
112
113
  // THE CHANNELS, keyed by the registry's provider id. These are the brands'
113
114
  // own published marks, from simple-icons, so a connect tile is drawn the way
@@ -169,6 +170,8 @@ const OF = {
169
170
  'z-ai': 'zai',
170
171
  zen: 'zoo',
171
172
  };
173
+ /** A body's own stroke hues: under `ink` they come off, so it draws in the page's ink. */
174
+ const INKED = / stroke="#[0-9A-Fa-f]{6}"/g;
172
175
  /**
173
176
  * The mark's NAME, resolved through the alias table — not its body. The body
174
177
  * and the hue are both read under this one key, so a lab cannot end up wearing
@@ -270,5 +273,5 @@ export function ProviderMark({ org, provider, model, size = 32, ink, }) {
270
273
  // stroke, so one presentation attribute colours a filled glyph and a
271
274
  // drawn one alike. A lab with no hue inherits the page's ink, which is
272
275
  // the whole point.
273
- color: ink ? undefined : HUE[mark], dangerouslySetInnerHTML: { __html: MARKS[mark] } }) }));
276
+ color: ink ? undefined : HUE[mark], dangerouslySetInnerHTML: { __html: ink ? MARKS[mark].replace(INKED, '') : MARKS[mark] } }) }));
274
277
  }
package/dist/Rail.d.ts CHANGED
@@ -31,7 +31,7 @@ export declare function Toggle({ open, onPress, label }: {
31
31
  onPress: () => void;
32
32
  label?: string;
33
33
  }): import("react").JSX.Element;
34
- export declare function Rail({ host, mode, at, open, toggle, flat, onChat, onDev, onStudio, onTabs, go, onOrg, }: {
34
+ export declare function Rail({ host, mode, at, open, toggle, rows, onChat, onDev, onStudio, onTabs, go, onOrg, }: {
35
35
  host: Host;
36
36
  mode: Place['mode'];
37
37
  /** Dev's builder address, or null outside Dev. */
@@ -40,8 +40,8 @@ export declare function Rail({ host, mode, at, open, toggle, flat, onChat, onDev
40
40
  open: boolean;
41
41
  /** Lead with the sidebar's toggle; absent where the host's bar holds it. */
42
42
  toggle?: () => void;
43
- /** Laid along the foot of the phone's drawer rather than down the window's edge. */
44
- flat?: boolean;
43
+ /** Listed as rows in a phone's menu rather than marks down the window's edge. */
44
+ rows?: boolean;
45
45
  /** To Chat, Dev and Studio, each where the reader left it. */
46
46
  onChat: () => void;
47
47
  onDev: () => void;
package/dist/Rail.js CHANGED
@@ -1,5 +1,5 @@
1
1
  'use client';
2
- import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  // The app's rail: its products as marks down the window's left edge, in one
4
4
  // order — Chat, Dev, Studio, Tabs, Library, Scheduled tasks, Customize — then
5
5
  // More (Team, on a host of its own, then the apps in beta this reader may open
@@ -24,15 +24,18 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
24
24
  //
25
25
  // The sidebar's toggle leads it (`toggle`) where the host has no bar of its own:
26
26
  // a browser tab. A host that is a window draws the toggle in its bar beside Back
27
- // and Forward (App.tsx `bar`), and its rail starts with Chat. On a phone the
28
- // rail lies along the foot of the drawer (`flat`), the account at its end; the
29
- // drawer is too narrow for every mark, so Library, Scheduled tasks and
30
- // Customize lead More there.
27
+ // and Forward (App.tsx `bar`), and its rail starts with Chat. A phone has no
28
+ // rail: its menu (Drawer.tsx) lists the places as rows (`rows`) under the mode's
29
+ // own — Studio, Tabs, then More, which Library, Scheduled tasks and Customize
30
+ // lead there. Chat and Dev are the phone's bar's (Top.tsx), and the account is
31
+ // the menu's foot.
32
+ import { useI18n } from '@hanzo/i18n/react';
31
33
  import { Clock, Code2, Ellipsis, FlaskConical, Library, MessageSquare, PanelLeft, Puzzle, Sparkles, SquareTerminal, Users, WandSparkles } from 'lucide-react';
32
34
  import { useState } from 'react';
33
- import { XStack, YStack } from '@hanzo/gui';
35
+ import { Text, XStack, YStack } from '@hanzo/gui';
34
36
  import { route } from '@hanzo/build';
35
37
  import { DropdownMenu } from '@hanzo/ui';
38
+ import { SidebarItem } from '@hanzo/ui/chat';
36
39
  import { BetasDialog, useBetas } from './Betas.js';
37
40
  import { out } from './help.js';
38
41
  import { BETAS } from './lib/betas.js';
@@ -61,15 +64,27 @@ export function raised(at) {
61
64
  }
62
65
  /** One mark: an icon button named for where it goes, its name its tooltip, lit where the reader is. */
63
66
  export function Mark({ label, on = false, onPress, children, ...rest }) {
64
- return (_jsx(XStack, { render: "button", "aria-label": label, ...{ title: label }, "aria-current": on ? 'page' : undefined, onPress: onPress, width: MARK, height: MARK, rounded: "$4", items: "center", justify: "center", shrink: 0, cursor: "pointer", opacity: on ? 1 : 0.62, bg: on ? '$edge' : 'transparent', hoverStyle: { bg: on ? '$edge' : '$hover', opacity: 1 }, pressStyle: { bg: '$raised' }, focusVisibleStyle: { outlineWidth: 2, outlineStyle: 'solid', outlineColor: '$outlineColor' },
67
+ const { t } = useI18n();
68
+ return (_jsx(XStack, { render: "button", "aria-label": t(label), ...{ title: label }, "aria-current": on ? 'page' : undefined, onPress: onPress, width: MARK, height: MARK, rounded: "$4", items: "center", justify: "center", shrink: 0, cursor: "pointer", opacity: on ? 1 : 0.62, bg: on ? '$edge' : 'transparent', hoverStyle: { bg: on ? '$edge' : '$hover', opacity: 1 }, pressStyle: { bg: '$raised' }, focusVisibleStyle: { outlineWidth: 2, outlineStyle: 'solid', outlineColor: '$outlineColor' },
65
69
  // A thumb's 44px on a touch screen.
66
70
  $touchable: { width: 44, height: 44 }, ...rest, children: children }));
67
71
  }
72
+ /** A place as a phone's menu lists it: a row on the sidebar's pitch, a thumb's 44 on a touch screen, lit where the reader is. */
73
+ function Row({ label, on = false, onPress, icon, ...rest }) {
74
+ const { t } = useI18n();
75
+ return (_jsx(SidebarItem, { rounded: "$4", minH: 31, py: 0, px: "$2", $touchable: { minH: 44 }, active: on, icon: icon, onPress: onPress, onKeyDown: (e) => {
76
+ if (!onPress || (e.key !== 'Enter' && e.key !== ' '))
77
+ return;
78
+ e.preventDefault?.();
79
+ onPress();
80
+ }, ...rest, children: _jsx(Text, { fontSize: "$3", color: on ? '$ink' : '$quiet', children: t(label) }) }));
81
+ }
68
82
  /** The sidebar's toggle: the same mark wherever a host draws it. */
69
83
  export function Toggle({ open, onPress, label }) {
70
84
  return (_jsx(Mark, { label: label ?? (open ? 'Close sidebar' : 'Open sidebar'), "data-slot": "sidebar-toggle", "aria-expanded": open, onPress: onPress, children: _jsx(PanelLeft, { size: 18, "aria-hidden": true }) }));
71
85
  }
72
- export function Rail({ host, mode, at, open, toggle, flat = false, onChat, onDev, onStudio, onTabs, go, onOrg, }) {
86
+ export function Rail({ host, mode, at, open, toggle, rows = false, onChat, onDev, onStudio, onTabs, go, onOrg, }) {
87
+ const { t } = useI18n();
73
88
  const here = mode === 'dev' ? spot(at) : null;
74
89
  const to = (p) => () => go(app(p));
75
90
  // The products on hosts of their own, behind More; on a phone the raised places lead it.
@@ -80,23 +95,25 @@ export function Rail({ host, mode, at, open, toggle, flat = false, onChat, onDev
80
95
  // The apps in beta this reader may open, each where it lives.
81
96
  const granted = BETAS.filter((b) => b.href && opened(b.id));
82
97
  const rest = [
83
- ...(flat
98
+ ...(rows
84
99
  ? [
85
- { key: 'library', label: 'Library', icon: _jsx(Library, { size: 16, "aria-hidden": true }), onSelect: to(RAISED.library) },
86
- { key: 'scheduled', label: 'Scheduled tasks', icon: _jsx(Clock, { size: 16, "aria-hidden": true }), onSelect: to(RAISED.scheduled) },
87
- { key: 'customize', label: 'Customize', icon: _jsx(Puzzle, { size: 16, "aria-hidden": true }), onSelect: to(RAISED.customize) },
100
+ { key: 'library', label: t('Library'), icon: _jsx(Library, { size: 16, "aria-hidden": true }), onSelect: to(RAISED.library) },
101
+ { key: 'scheduled', label: t('Scheduled tasks'), icon: _jsx(Clock, { size: 16, "aria-hidden": true }), onSelect: to(RAISED.scheduled) },
102
+ { key: 'customize', label: t('Customize'), icon: _jsx(Puzzle, { size: 16, "aria-hidden": true }), onSelect: to(RAISED.customize) },
88
103
  ...(team ? [{ type: 'separator', key: 'raised' }] : []),
89
104
  ]
90
105
  : []),
91
106
  ...(team
92
107
  ? [
93
- { key: 'team', label: 'Team', icon: _jsx(Users, { size: 16, "aria-hidden": true }), onSelect: () => out(WORKSPACE) },
108
+ { key: 'team', label: t('Team'), icon: _jsx(Users, { size: 16, "aria-hidden": true }), onSelect: () => out(WORKSPACE) },
94
109
  ...granted.map((b) => ({ key: `beta-${b.id}`, label: b.name, icon: _jsx(Sparkles, { size: 16, "aria-hidden": true }), onSelect: () => out(b.href) })),
95
110
  { type: 'separator', key: 'betas-rule' },
96
- { key: 'betas', label: 'Betas', icon: _jsx(FlaskConical, { size: 16, "aria-hidden": true }), onSelect: () => setBetas(true) },
111
+ { key: 'betas', label: t('Betas'), icon: _jsx(FlaskConical, { size: 16, "aria-hidden": true }), onSelect: () => setBetas(true) },
97
112
  ]
98
113
  : []),
99
114
  ];
100
- const Stack = flat ? XStack : YStack;
101
- return (_jsxs(Stack, { "data-slot": "rail", render: "nav", "aria-label": "Products", ...(flat ? { pt: '$2', mt: '$2', borderTopWidth: 1, borderColor: '$borderColor', gap: '$1' } : { width: RAIL, flex: 1, pt: '$2', pb: '$2', gap: 8 }), shrink: 0, items: "center", children: [toggle ? (_jsx(YStack, { pb: 8, children: _jsx(Toggle, { open: open, onPress: toggle }) })) : null, _jsx(Mark, { label: "Chat", on: mode === 'chat', onPress: onChat, "data-slot": "rail-chat", children: _jsx(MessageSquare, { size: 18, "aria-hidden": true }) }), _jsx(Mark, { label: "Dev", on: mode === 'dev' && !here, onPress: onDev, "data-slot": "rail-dev", children: _jsx(Code2, { size: 18, "aria-hidden": true }) }), _jsx(Mark, { label: "Studio", on: mode === 'studio', onPress: onStudio, "data-slot": "rail-studio", children: _jsx(WandSparkles, { size: 18, "aria-hidden": true }) }), _jsx(Mark, { label: "Tabs", on: mode === 'tabs', onPress: onTabs, "data-slot": "rail-tabs", children: _jsx(SquareTerminal, { size: 18, "aria-hidden": true }) }), flat ? null : (_jsxs(_Fragment, { children: [_jsx(Mark, { label: "Library", on: here === 'library', onPress: to(RAISED.library), "data-slot": "rail-library", children: _jsx(Library, { size: 18, "aria-hidden": true }) }), _jsx(Mark, { label: "Scheduled tasks", on: here === 'scheduled', onPress: to(RAISED.scheduled), "data-slot": "rail-scheduled", children: _jsx(Clock, { size: 18, "aria-hidden": true }) }), _jsx(Mark, { label: "Customize", on: here === 'customize', onPress: to(RAISED.customize), "data-slot": "rail-customize", children: _jsx(Puzzle, { size: 18, "aria-hidden": true }) })] })), _jsx(BetasDialog, { open: betas, onOpenChange: setBetas, go: out }), rest.length ? (_jsx(DropdownMenu, { placement: flat ? 'top-start' : 'right-start', minWidth: 220, trigger: _jsx(XStack, { render: "button", "aria-label": "More", ...{ title: 'More' }, "data-slot": "rail-more", width: MARK, height: MARK, rounded: "$4", items: "center", justify: "center", cursor: "pointer", opacity: 0.62, hoverStyle: { bg: '$hover', opacity: 1 }, pressStyle: { bg: '$raised' }, focusVisibleStyle: { outlineWidth: 2, outlineStyle: 'solid', outlineColor: '$outlineColor' }, $touchable: { width: 44, height: 44 }, children: _jsx(Ellipsis, { size: 18, "aria-hidden": true }) }), items: rest })) : null, _jsx(YStack, { flex: 1 }), _jsx(Me, { host: host, narrow: true, above: flat, onOrg: onOrg })] }));
115
+ const betasDialog = _jsx(BetasDialog, { open: betas, onOpenChange: setBetas, go: out });
116
+ if (rows)
117
+ return (_jsxs(YStack, { "data-slot": "places", render: "nav", "aria-label": t('Products'), pt: "$2", shrink: 0, children: [_jsxs(YStack, { role: "list", gap: 2, children: [_jsx(YStack, { role: "listitem", children: _jsx(Row, { label: "Studio", on: mode === 'studio', onPress: onStudio, "data-slot": "place-studio", icon: _jsx(WandSparkles, { size: 16, "aria-hidden": true }) }) }), _jsx(YStack, { role: "listitem", children: _jsx(Row, { label: "Tabs", on: mode === 'tabs', onPress: onTabs, "data-slot": "place-tabs", icon: _jsx(SquareTerminal, { size: 16, "aria-hidden": true }) }) }), _jsx(YStack, { role: "listitem", children: _jsx(DropdownMenu, { placement: "bottom-start", minWidth: 220, trigger: _jsx(Row, { label: "More", "data-slot": "place-more", "aria-haspopup": "menu", icon: _jsx(Ellipsis, { size: 16, "aria-hidden": true }) }), items: rest }) })] }), betasDialog] }));
118
+ return (_jsxs(YStack, { "data-slot": "rail", render: "nav", "aria-label": t('Products'), width: RAIL, flex: 1, pt: "$2", pb: "$2", gap: 8, shrink: 0, items: "center", children: [toggle ? (_jsx(YStack, { pb: 8, children: _jsx(Toggle, { open: open, onPress: toggle }) })) : null, _jsx(Mark, { label: t("Chat"), on: mode === 'chat', onPress: onChat, "data-slot": "rail-chat", children: _jsx(MessageSquare, { size: 18, "aria-hidden": true }) }), _jsx(Mark, { label: t("Dev"), on: mode === 'dev' && !here, onPress: onDev, "data-slot": "rail-dev", children: _jsx(Code2, { size: 18, "aria-hidden": true }) }), _jsx(Mark, { label: t("Studio"), on: mode === 'studio', onPress: onStudio, "data-slot": "rail-studio", children: _jsx(WandSparkles, { size: 18, "aria-hidden": true }) }), _jsx(Mark, { label: t("Tabs"), on: mode === 'tabs', onPress: onTabs, "data-slot": "rail-tabs", children: _jsx(SquareTerminal, { size: 18, "aria-hidden": true }) }), _jsx(Mark, { label: t("Library"), on: here === 'library', onPress: to(RAISED.library), "data-slot": "rail-library", children: _jsx(Library, { size: 18, "aria-hidden": true }) }), _jsx(Mark, { label: t("Scheduled tasks"), on: here === 'scheduled', onPress: to(RAISED.scheduled), "data-slot": "rail-scheduled", children: _jsx(Clock, { size: 18, "aria-hidden": true }) }), _jsx(Mark, { label: t("Customize"), on: here === 'customize', onPress: to(RAISED.customize), "data-slot": "rail-customize", children: _jsx(Puzzle, { size: 18, "aria-hidden": true }) }), betasDialog, rest.length ? (_jsx(DropdownMenu, { placement: "right-start", minWidth: 220, trigger: _jsx(XStack, { render: "button", "aria-label": t("More"), ...{ title: 'More' }, "data-slot": "rail-more", width: MARK, height: MARK, rounded: "$4", items: "center", justify: "center", cursor: "pointer", opacity: 0.62, hoverStyle: { bg: '$hover', opacity: 1 }, pressStyle: { bg: '$raised' }, focusVisibleStyle: { outlineWidth: 2, outlineStyle: 'solid', outlineColor: '$outlineColor' }, $touchable: { width: 44, height: 44 }, children: _jsx(Ellipsis, { size: 18, "aria-hidden": true }) }), items: rest })) : null, _jsx(YStack, { flex: 1 }), _jsx(Me, { host: host, narrow: true, onOrg: onOrg })] }));
102
119
  }