@hanzo/rooms 0.1.57 → 0.1.59

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/Column.js CHANGED
@@ -1,38 +1,39 @@
1
1
  'use client';
2
- import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- // The app's one column, the same in both modes: the Chat / Dev switch, a filter,
4
- // New, Dev's places, the Recents of the mode it is in, and at the foot the
5
- // account (components/workspace/Me.tsx).
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ // The app's sidebar, beside the rail on the window's floor (App.tsx): the
4
+ // mode's name with the bell and the search at its right, New, Dev's places in
5
+ // Dev, the reader's Recents (Pinned first), and the first-run cards. Which
6
+ // product it lists is the rail's to say; the account is at the rail's foot.
6
7
  //
7
- // Dev's places are @hanzo/build's `nav`, the one list every rail draws, each a
8
- // row of its own under a quiet group label: nothing waits behind a More.
8
+ // It is the window, not a panel: no fill, no edge, the floor's own ground. The
9
+ // content beside it is the panel, inset by the gutter.
10
+ //
11
+ // The bell is the org's run activity (@hanzo/build `useSessions`): the runs
12
+ // that moved last, each one press from its page, and Notifications in Settings.
13
+ // The search opens the filter over Recents (⌘K / Ctrl+K from anywhere, `seek`).
9
14
  //
10
- // Tabs sits under New in both modes. Hanzo Tabs lives at tabs.hanzo.ai, so the
11
- // row is a link there, in a tab of its own.
15
+ // Dev's places are @hanzo/build's `nav`, the one list every rail draws, each a
16
+ // row under a quiet group label.
12
17
  //
13
- // It is a pane on the frame's ground (app/_web.tsx), sized by the edge in the
14
- // gutter to its right: dragged, keyed or double-clicked back (@hanzo/build's
15
- // `Grip`), kept per browser. Pulled in past its floor it collapses to a rail of
16
- // marks, kept under the key the builder's rail kept it under, so a reader's
17
- // choice outlives the move.
18
+ // Its width is the edge's in the gutter to its right: dragged, keyed or
19
+ // double-clicked back (@hanzo/build's `Grip`), kept per browser. Pulled in past
20
+ // its floor it shuts (`onShut`), as the sidebar's toggle shuts it.
18
21
  import { Text, XStack, YStack } from '@hanzo/gui';
19
- import { PanelLeft, Plus, Search, SquareTerminal } from 'lucide-react';
20
- import { Grip, nav, NEW } from '@hanzo/build';
21
- import { Sidebar, SidebarIconButton, SidebarItem } from '@hanzo/ui/chat';
22
- import { HanzoMark } from '@hanzogui/shell';
23
- import { Fragment, useId, useState } from 'react';
24
- import { ModeMark } from './ModeMark.js';
25
- import { useKept } from './kept.js';
26
- import { Made, Me } from './Me.js';
22
+ import { Bell, Search, Settings2, SquarePen, X } from 'lucide-react';
23
+ import { DOTS, Grip, nav, NEW, path as written, useSessions } from '@hanzo/build';
24
+ import { DropdownMenu } from '@hanzo/ui';
25
+ import { Sidebar, SidebarIconButton, SidebarItem, StatusDot } from '@hanzo/ui/chat';
26
+ import { useEffect, useId, useRef, useState } from 'react';
27
+ import { Made } from './Me.js';
27
28
  import { Recents } from './Recents.js';
28
29
  import { Setup } from './Setup.js';
30
+ import { ago } from './recent.js';
29
31
  import { empty } from './open.js';
30
32
  import { useSpan } from './span.js';
31
33
  import { pane } from './ground.js';
32
- import { pick } from './lib/session.js';
33
- import { app, TABS, talk } from './lib/host.js';
34
- import { brand } from './where.js';
35
- /** The grip stands in the gutter between the column and the pane. gui carries a `calc()` to the page unchanged; its
34
+ import { talk } from './lib/host.js';
35
+ import { brand, wordmark } from './where.js';
36
+ /** The grip stands in the gutter between the sidebar and the panel. gui carries a `calc()` to the page unchanged; its
36
37
  * types only name custom properties. */
37
38
  const GUTTER_END = 'calc(-1 * var(--pane-gap))';
38
39
  /** The sidebar fills the column. Its `width` is typed in px alone, though its frame takes any gui size — the type is
@@ -48,12 +49,10 @@ const FLOAT = {
48
49
  borderColor: 'var(--border-strong)',
49
50
  boxShadow: 'var(--edge-highlight), var(--shadow-floating)',
50
51
  };
51
- /** The column's width, px: its default, and the two a drag holds it between. */
52
- const WIDTH = 272;
53
- const FLOOR = 220;
52
+ /** The sidebar's width, px: its default, and the two a drag holds it between. */
53
+ const WIDTH = 240;
54
+ const FLOOR = 200;
54
55
  const CEIL = 420;
55
- /** The collapsed rail's width, px. */
56
- const RAIL = 60;
57
56
  /** Per reader, per browser. */
58
57
  const SPAN = 'hanzo.app.column';
59
58
  /** Enter and Space press a row the way a click does. */
@@ -65,19 +64,37 @@ const keys = (press) => ({
65
64
  press();
66
65
  },
67
66
  });
68
- /** A row of the column: a pill, lit where the reader is. Compact, so every place fits a laptop's height with Recents under them. */
69
- const ROW = { rounded: '$4', minH: 30, py: 0 };
70
- /** Tabs, a row drawn as a link out: another origin, its own tab, no handle back to this page. */
71
- const OUT = { 'data-slot': 'app-tabs', role: 'link', render: _jsx("a", { href: TABS, target: "_blank", rel: "noopener noreferrer" }) };
72
- /** Tabs in the rail, as its mark. */
73
- const Mark = () => (_jsx(SidebarIconButton, { label: "Tabs", width: 36, height: 32, rounded: 999, ...OUT, children: _jsx(SquareTerminal, { size: 16 }) }));
74
- /** Tabs in the column, as a row. */
75
- const Row = () => (_jsx(YStack, { role: "listitem", children: _jsx(SidebarItem, { ...ROW, icon: _jsx(SquareTerminal, { size: 16, "aria-hidden": true }), ...OUT, children: "Tabs" }) }));
76
- export function Column({ host, mode, thread, onChat, onDev, go, drawer = false, }) {
77
- const [kept, setCollapsed] = useKept('hanzo.build.rail', false);
78
- const collapsed = kept && !drawer;
67
+ /** A row of the sidebar: a pill on a 31px pitch, lit where the reader is. */
68
+ const ROW = { rounded: '$4', minH: 31, py: 0, px: '$2' };
69
+ /** The run activity behind the bell: the runs that moved last, newest first. */
70
+ const RECENT = 6;
71
+ /** A header mark: a 16px glyph in a 32px target. */
72
+ const HEAD = { width: 32, height: 32, rounded: '$3' };
73
+ function Activity({ host }) {
74
+ const store = useSessions(host);
75
+ const latest = [...store.value].sort((a, b) => (a.updatedAt < b.updatedAt ? 1 : -1)).slice(0, RECENT);
76
+ const items = [
77
+ { type: 'label', key: 'head', label: 'Activity' },
78
+ ...(latest.length
79
+ ? latest.map((s) => ({
80
+ key: s.id,
81
+ label: s.title || 'Untitled run',
82
+ description: [s.status, ago(s.updatedAt || s.createdAt)].filter(Boolean).join(' · '),
83
+ icon: _jsx(StatusDot, { status: DOTS[s.status] ?? 'idle' }),
84
+ onSelect: () => host.go(s.id),
85
+ }))
86
+ : [{ key: 'none', label: 'Nothing new', disabled: true, onSelect: () => { } }]),
87
+ { type: 'separator', key: 'line' },
88
+ { key: 'settings', label: 'Notification settings', icon: _jsx(Settings2, { size: 16, "aria-hidden": true }), onSelect: () => host.go(written({ kind: 'settings', section: 'notifications' })) },
89
+ ];
90
+ return (_jsx(DropdownMenu, { minWidth: 280, trigger: _jsx(XStack, { render: "button", "aria-label": "Activity", ...{ title: 'Activity' }, ...HEAD, items: "center", justify: "center", cursor: "pointer", opacity: 0.7, hoverStyle: { bg: '$hover', opacity: 1 }, focusVisibleStyle: { outlineWidth: 2, outlineStyle: 'solid', outlineColor: '$outlineColor' }, children: _jsx(Bell, { size: 16, "aria-hidden": true }) }), items: items }));
91
+ }
92
+ export function Column({ host, mode, thread, go, onShut, seek = 0, drawer = false, foot, }) {
79
93
  const [words, setWords] = useState('');
80
- // The desktop column and a phone's drawer can both be in the page: each names its own group labels.
94
+ // The search field shows once asked for, and while it holds words.
95
+ const [seeking, setSeeking] = useState(false);
96
+ const field = useRef(null);
97
+ // The desktop sidebar and a phone's drawer can both be in the page: each names its own group labels.
81
98
  const uid = useId();
82
99
  const span = useSpan(SPAN, WIDTH, FLOOR, CEIL);
83
100
  const dev = (p) => host.go(p);
@@ -88,21 +105,27 @@ export function Column({ host, mode, thread, onChat, onDev, go, drawer = false,
88
105
  empty();
89
106
  go(talk());
90
107
  };
91
- // Another organization: picked, and the app opened in it at the mode's New.
92
- const org = (o) => {
93
- pick(o);
94
- window.location.assign(mode === 'dev' ? app('') : talk());
108
+ const ask = () => {
109
+ setSeeking(true);
110
+ requestAnimationFrame(() => field.current?.focus());
111
+ };
112
+ const done = () => {
113
+ setWords('');
114
+ setSeeking(false);
95
115
  };
96
- if (collapsed)
97
- return (_jsx(YStack, { "data-slot": "app-column", "data-collapsed": "", position: "relative", shrink: 0, width: RAIL, "$max-md": { display: 'none' }, children: _jsxs(Sidebar, { "data-slot": "app-rail", "data-collapsed": "true", role: "navigation", "aria-label": brand(), ...pane(), ...FILL, px: "$1.5", py: "$2.5", gap: "$1.5", items: "center", children: [_jsx(SidebarIconButton, { label: "Expand sidebar", onPress: () => setCollapsed(false), width: 36, height: 36, rounded: 999, children: _jsx(PanelLeft, { size: 16 }) }), _jsx(ModeMark, { mode: mode, onChat: onChat, onDev: onDev, stacked: true }), mode === 'chat' ? (_jsxs(_Fragment, { children: [_jsx(SidebarIconButton, { label: "New chat", onPress: create, width: 36, height: 36, rounded: 999, children: _jsx(Plus, { size: 16 }) }), _jsx(Mark, {})] })) : (
98
- // Every place, as its mark: collapsed is narrower, never shorter of a destination.
99
- groups.map((g) => (_jsx(YStack, { role: "group", "aria-label": g.label, items: "center", gap: 2, pt: "$1", children: g.links.map((link) => (_jsxs(Fragment, { children: [_jsx(SidebarIconButton, { label: link.label, onPress: () => link.onPress?.(), width: 36, height: 32, rounded: 999, "aria-current": link.active ? 'page' : undefined, children: link.icon }), link.id === NEW ? _jsx(Mark, {}) : null] }, link.id))) }, g.id)))), _jsx(YStack, { flex: 1 }), _jsx(Me, { host: host, narrow: true, onOrg: org })] }) }));
116
+ useEffect(() => {
117
+ if (seek)
118
+ ask();
119
+ }, [seek]);
100
120
  return (_jsxs(YStack, { "data-slot": "app-column", ref: drawer ? undefined : span.ref,
101
121
  // inline-style: `--span`, the width a drag rewrites on the element itself (span.ts); no prop sets a variable.
102
- style: drawer ? undefined : span.style, position: "relative", shrink: 0, height: "100%", width: drawer ? '100%' : 'var(--span)', ...(drawer ? {} : { '$max-md': { display: 'none' } }), children: [_jsxs(Sidebar, { "data-slot": "app-rail", "data-collapsed": "false", role: "navigation", "aria-label": brand(), ...pane(), ...(drawer ? FLOAT : null), ...FILL, px: "$2.5", pt: "$3", pb: "$2", gap: "$0", children: [_jsxs(XStack, { "data-slot": "mode-switch", items: "center", gap: "$2", px: "$1.5", pb: "$3", minW: 0, children: [_jsx(HanzoMark, { size: 18 }), _jsx(ModeMark, { mode: mode, onChat: onChat, onDev: onDev })] }), _jsxs(XStack, { "data-slot": "app-filter", items: "center", gap: "$2", px: "$3", height: 36, rounded: 999, borderWidth: 1, borderColor: "$borderColor", bg: "$edge", shrink: 0, children: [_jsx(Search, { size: 15, "aria-hidden": true, color: "var(--soft, var(--muted-foreground))" }), _jsx(Text, { render: _jsx("input", { value: words, onChange: (e) => setWords(e.target.value), onKeyDown: (e) => {
122
+ style: drawer ? undefined : span.style, position: "relative", flex: drawer ? 1 : undefined, shrink: drawer ? 1 : 0, minW: 0, height: "100%", width: drawer ? undefined : 'var(--span)', children: [_jsxs(Sidebar, { "data-slot": "app-rail", role: "navigation", "aria-label": brand(), ...(drawer ? { ...pane(), ...FLOAT } : { bg: 'transparent', borderWidth: 0 }), ...FILL, px: "$2", pt: drawer ? '$2' : 0, pb: "$2", gap: "$0", children: [_jsxs(XStack, { "data-slot": "app-head", items: "center", gap: "$1", pl: "$2", pr: 2, height: 52, shrink: 0, minW: 0, children: [_jsx(Text, { flex: 1, minW: 0, fontSize: 18, lineHeight: 24, fontWeight: "600", color: "$ink", numberOfLines: 1, children: wordmark(mode) }), _jsx(Activity, { host: host }), _jsx(SidebarIconButton, { label: mode === 'chat' ? 'Search chats' : 'Search runs', onPress: seeking ? done : ask, "aria-expanded": seeking || !!words, ...HEAD, children: _jsx(Search, { size: 16, "aria-hidden": true }) })] }), seeking || words ? (_jsxs(XStack, { "data-slot": "app-filter", items: "center", gap: "$2", pl: "$3", pr: "$1", height: 36, rounded: 999, borderWidth: 1, borderColor: "$borderColor", bg: "$edge", shrink: 0, mb: "$1.5", children: [_jsx(Search, { size: 15, "aria-hidden": true, color: "var(--soft, var(--muted-foreground))" }), _jsx(Text, { render: _jsx("input", { ref: field, value: words, onChange: (e) => setWords(e.target.value), onKeyDown: (e) => {
103
123
  if (e.key === 'Escape')
104
- setWords('');
105
- }, placeholder: mode === 'chat' ? 'Filter chats' : 'Filter runs', "aria-label": "Filter recents" }), flex: 1, minW: 0, bg: "transparent", borderWidth: 0, outlineStyle: "none", color: "inherit", fontSize: "$2" })] }), mode === 'chat' ? (_jsxs(YStack, { role: "list", mt: "$2.5", gap: 2, shrink: 0, children: [_jsx(YStack, { role: "listitem", children: _jsx(SidebarItem, { "data-slot": "app-new", ...ROW, active: !thread, icon: _jsx(Plus, { size: 16, "aria-hidden": true }), onPress: create, ...keys(create), children: "New chat" }) }), _jsx(Row, {})] })) : (_jsx(YStack, { "data-slot": "app-places", mt: "$1.5", shrink: 0, children: groups.map((g) => (_jsxs(YStack, { pt: "$1.5", children: [_jsx(Text, { id: `${uid}-${g.id}`, pl: "$2", pb: 2, fontSize: "$1", color: "$soft", fontWeight: "600", textTransform: "uppercase", letterSpacing: 0.6, children: g.label }), _jsx(YStack, { role: "list", "aria-labelledby": `${uid}-${g.id}`, gap: 1, children: g.links.map((link) => (_jsxs(Fragment, { children: [_jsx(YStack, { role: "listitem", children: _jsx(SidebarItem, { ...ROW, ...(link.id === NEW ? { 'data-slot': 'app-new' } : {}), active: link.active, icon: link.icon, onPress: link.onPress, ...keys(() => link.onPress?.()), children: link.label }) }), link.id === NEW ? _jsx(Row, {}) : null] }, link.id))) })] }, g.id))) })), _jsx(XStack, { height: 1, bg: "$borderColor", mx: "$1.5", mt: "$3", shrink: 0 }), _jsx(Recents, { host: host, mode: mode, thread: thread, words: words, onOpen: open, onNew: create }), _jsxs(YStack, { gap: "$2", pt: "$2", shrink: 0, children: [_jsx(Made, {}), _jsx(Setup, {})] }), _jsxs(XStack, { "data-slot": "app-foot", items: "center", gap: "$1", pt: "$2", shrink: 0, children: [_jsx(YStack, { flex: 1, minW: 0, children: _jsx(Me, { host: host, onOrg: org }) }), drawer ? null : (_jsx(SidebarIconButton, { label: "Collapse sidebar", onPress: () => setCollapsed(true), width: 32, height: 32, rounded: 999, children: _jsx(PanelLeft, { size: 16 }) }))] })] }), drawer ? null : (_jsx(Grip, { side: "left", span: span.span, floor: FLOOR, ceil: CEIL, reset: WIDTH, onSpan: span.move, onKeep: span.keep, onShut: () => setCollapsed(true), label: "Resize sidebar",
106
- // In the gutter between the column and the pane, not over the column's own rows.
124
+ done();
125
+ }, onBlur: () => {
126
+ if (!words)
127
+ setSeeking(false);
128
+ }, placeholder: mode === 'chat' ? 'Search chats' : 'Search runs', "aria-label": "Filter recents" }), flex: 1, minW: 0, bg: "transparent", borderWidth: 0, outlineStyle: "none", color: "inherit", fontSize: "$3" }), words ? (_jsx(SidebarIconButton, { label: "Clear search", onPress: done, width: 28, height: 28, rounded: 999, children: _jsx(X, { size: 14, "aria-hidden": true }) })) : null] })) : null, mode === 'chat' ? (_jsx(YStack, { role: "list", gap: 2, shrink: 0, children: _jsx(YStack, { role: "listitem", children: _jsx(SidebarItem, { "data-slot": "app-new", ...ROW, active: !thread, bg: "transparent", hoverStyle: { bg: '$hover' }, icon: _jsx(SquarePen, { size: 16, "aria-hidden": true }), onPress: create, ...keys(create), children: _jsx(Text, { fontSize: "$3", color: "$ink", children: "New chat" }) }) }) })) : (_jsx(YStack, { "data-slot": "app-places", shrink: 0, children: groups.map((g, i) => (_jsxs(YStack, { pt: i ? '$2' : 0, children: [i ? (_jsx(Text, { id: `${uid}-${g.id}`, pl: "$2", pb: 2, fontSize: "$3", color: "$soft", children: g.label })) : null, _jsx(YStack, { role: "list", "aria-labelledby": i ? `${uid}-${g.id}` : undefined, "aria-label": i ? undefined : g.label, gap: 1, children: g.links.map((link) => (_jsx(YStack, { role: "listitem", children: _jsx(SidebarItem, { ...ROW, ...(link.id === NEW ? { 'data-slot': 'app-new' } : {}), active: link.active, icon: link.id === NEW ? _jsx(SquarePen, { size: 16, "aria-hidden": true }) : link.icon, onPress: link.onPress, ...keys(() => link.onPress?.()), children: _jsx(Text, { fontSize: "$3", color: link.active ? '$ink' : '$quiet', children: link.label }) }) }, link.id))) })] }, g.id))) })), _jsx(Recents, { host: host, mode: mode, thread: thread, words: words, onOpen: open, onNew: create }), _jsxs(YStack, { gap: "$2", pt: "$2", shrink: 0, children: [_jsx(Made, {}), _jsx(Setup, {})] }), foot] }), drawer ? null : (_jsx(Grip, { side: "left", span: span.span, floor: FLOOR, ceil: CEIL, reset: WIDTH, onSpan: span.move, onKeep: span.keep, onShut: onShut, label: "Resize sidebar",
129
+ // In the gutter between the sidebar and the panel, not over the sidebar's own rows.
107
130
  r: GUTTER_END, width: "var(--pane-gap)" }))] }));
108
131
  }
package/dist/Home.js CHANGED
@@ -110,12 +110,12 @@ function Open({ at, rooms, back, }) {
110
110
  return _jsx(Talk, { room: found }, teamKey(found));
111
111
  const reading = isLoading || (isAuthenticated && rooms.rooms === null && !rooms.wrong);
112
112
  const line = reading
113
- ? 'Reading the room.'
113
+ ? 'Reading the channel.'
114
114
  : !isAuthenticated
115
- ? 'Sign in to read this room.'
115
+ ? 'Sign in to read this channel.'
116
116
  : rooms.wrong
117
- ? say(rooms.wrong, 'this room')
118
- : 'That room is not here.';
117
+ ? say(rooms.wrong, 'this channel')
118
+ : 'That channel is not here.';
119
119
  return (_jsxs(YStack, { flex: 1, items: "center", justify: "center", p: "$6", gap: "$3", children: [_jsx(Text, { fontSize: "$3", color: SOFT, children: line }), reading ? null : (_jsx(Box, { render: "button", onClick: back, ...tap, children: _jsx(Text, { fontSize: "$2", fontWeight: "500", color: INK, children: "Back to home" }) }))] }));
120
120
  }
121
121
  /* ─────────────────────────────────────────────────────────────────────────────
@@ -151,18 +151,18 @@ function Rooms({ rooms, wrong, open }) {
151
151
  openRoom(teamKey(made));
152
152
  }
153
153
  catch (e) {
154
- setRefused(plainly(e, 'this room'));
154
+ setRefused(plainly(e, 'this channel'));
155
155
  }
156
156
  finally {
157
157
  setOpening(false);
158
158
  }
159
159
  };
160
- return (_jsxs(Card, { kind: "rooms", icon: _jsx(Hash, { size: 14, "aria-hidden": true }), title: "Rooms", children: [rooms === null ? (_jsx(Waiting, { wrong: wrong, subject: "your rooms" })) : rooms.length === 0 ? (_jsx(Quiet, { children: "No rooms yet. Open one and the conversation has somewhere to live." })) : (_jsx(YStack, { gap: "$1", children: rooms.slice(0, 6).map((one) => (_jsx(Row, { icon: one.direct ? _jsx(AtSign, { size: 12, "aria-hidden": true }) : _jsx(Hash, { size: 12, "aria-hidden": true }), onPress: () => openRoom(teamKey(one)), children: one.name || 'Direct message' }, teamKey(one)))) })), !me ? null : naming ? (_jsxs(YStack, { gap: "$1", children: [_jsxs(XStack, { gap: "$1", items: "center", children: [_jsx(Text, { render: _jsx("input", { value: name, onChange: (e) => setName(e.target.value), onKeyDown: (e) => {
160
+ return (_jsxs(Card, { kind: "rooms", icon: _jsx(Hash, { size: 14, "aria-hidden": true }), title: "Channels", children: [rooms === null ? (_jsx(Waiting, { wrong: wrong, subject: "your channels" })) : rooms.length === 0 ? (_jsx(Quiet, { children: "No channels yet." })) : (_jsx(YStack, { gap: "$1", children: rooms.slice(0, 6).map((one) => (_jsx(Row, { icon: one.direct ? _jsx(AtSign, { size: 12, "aria-hidden": true }) : _jsx(Hash, { size: 12, "aria-hidden": true }), onPress: () => openRoom(teamKey(one)), children: one.name || 'Direct message' }, teamKey(one)))) })), !me ? null : naming ? (_jsxs(YStack, { gap: "$1", children: [_jsxs(XStack, { gap: "$1", items: "center", children: [_jsx(Text, { render: _jsx("input", { value: name, onChange: (e) => setName(e.target.value), onKeyDown: (e) => {
161
161
  if (e.key === 'Enter')
162
162
  void make();
163
163
  if (e.key === 'Escape')
164
164
  setNaming(false);
165
- }, placeholder: "Name", "aria-label": "Room name", autoFocus: true }), flex: 1, minW: 0, bg: "transparent", borderWidth: 1, borderColor: "var(--border)", rounded: 6, px: 8, py: 6, color: "inherit", fontSize: "$2" }), _jsx(Box, { render: "button", onClick: () => void make(), "aria-disabled": opening, opacity: opening ? 0.5 : 1, px: "$2", py: "$1.5", rounded: "$2", borderWidth: 1, borderColor: "$borderColor", hoverStyle: { bg: '$hover' }, children: _jsx(Text, { fontSize: "$2", fontWeight: "500", color: INK, children: "Open it" }) })] }), refused ? (_jsx(Text, { fontSize: "$2", color: BAD, children: refused })) : null] })) : (_jsx(Row, { icon: _jsx(Plus, { size: 12, "aria-hidden": true }), onPress: () => setNaming(true), children: "New room" }))] }));
165
+ }, placeholder: "Name", "aria-label": "Channel name", autoFocus: true }), flex: 1, minW: 0, bg: "transparent", borderWidth: 1, borderColor: "var(--border)", rounded: 6, px: 8, py: 6, color: "inherit", fontSize: "$2" }), _jsx(Box, { render: "button", onClick: () => void make(), "aria-disabled": opening, opacity: opening ? 0.5 : 1, px: "$2", py: "$1.5", rounded: "$2", borderWidth: 1, borderColor: "$borderColor", hoverStyle: { bg: '$hover' }, children: _jsx(Text, { fontSize: "$2", fontWeight: "500", color: INK, children: "Open it" }) })] }), refused ? (_jsx(Text, { fontSize: "$2", color: BAD, children: refused })) : null] })) : (_jsx(Row, { icon: _jsx(Plus, { size: 12, "aria-hidden": true }), onPress: () => setNaming(true), children: "New room" }))] }));
166
166
  }
167
167
  /**
168
168
  * A read the SDK has no hook for.
package/dist/Inbox.js CHANGED
@@ -127,7 +127,7 @@ function Reading({ room, reload, }) {
127
127
  reload();
128
128
  }
129
129
  catch (e) {
130
- setFailed(plainly(e, 'this room'));
130
+ setFailed(plainly(e, 'this conversation'));
131
131
  }
132
132
  finally {
133
133
  setSending(false);
package/dist/Me.d.ts CHANGED
@@ -13,10 +13,18 @@ export declare function Face({ host, size }: {
13
13
  host: Host;
14
14
  size?: number;
15
15
  }): import("react").JSX.Element;
16
- export declare function Me({ host, narrow, onOrg, }: {
16
+ /**
17
+ * The plans row, named by the plan the organization is on: Enterprise and
18
+ * Agency are plans a person stays on, so the row names theirs; every other
19
+ * plan, Free included, is one to move up from.
20
+ */
21
+ export declare const upgrade: (family: string | null | undefined) => string;
22
+ export declare function Me({ host, narrow, above, onOrg, }: {
17
23
  host: Host;
18
- /** The collapsed rail's: the face alone, the menu opening beside it. */
24
+ /** The rail's: the face alone, the menu opening beside it. */
19
25
  narrow?: boolean;
26
+ /** The menu opens above the face, not beside it: the rail laid along a phone drawer's foot. */
27
+ above?: boolean;
20
28
  /** Work in another organization: picked, then the app opened in it. */
21
29
  onOrg: (org: string) => void;
22
30
  }): import("react").JSX.Element;
package/dist/Me.js CHANGED
@@ -25,7 +25,7 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
25
25
  // Orgs.tsx); the next load mints a token that carries it.
26
26
  import { useEffect, useMemo, useRef, useState } from 'react';
27
27
  import { Popover, SizableText, usePopperContextSlow, View, XStack, YStack } from '@hanzo/gui';
28
- import { Building2, ChevronsUpDown, CreditCard, ExternalLink, Gauge, KeyRound, LifeBuoy, LogOut, Plus, Settings, Sparkles, Users, X } from 'lucide-react';
28
+ import { Building2, ChevronLeft, ChevronRight, ChevronsUpDown, CreditCard, ExternalLink, Gauge, KeyRound, LifeBuoy, LogOut, Mail, Plus, Settings, Sparkles, Users, X } from 'lucide-react';
29
29
  import { label, Meter, path, said as spoken, useStanding, useWho } from '@hanzo/build';
30
30
  import { useOrganizations } from '@hanzo/iam/react';
31
31
  import { Button, Dialog, DialogContent, DialogTitle, Input } from '@hanzo/ui';
@@ -111,7 +111,7 @@ export function Face({ host, size = 28 }) {
111
111
  /** One row of the menu: a real button, so Enter, Space and focus are the browser's. */
112
112
  function Item({ icon, label, sub, checked, onPress, trail, }) {
113
113
  const radio = checked !== undefined;
114
- return (_jsxs(XStack, { render: "button", ...{ role: radio ? 'menuitemradio' : 'menuitem' }, "aria-checked": radio ? checked : undefined, "data-item": "", onPress: onPress, items: "center", gap: "$2.5", px: "$2", minH: 36, py: "$1.5", rounded: "$4", bg: checked ? '$edge' : 'transparent', hoverStyle: { bg: '$hover' }, focusVisibleStyle: { bg: '$hover', outlineWidth: 2, outlineStyle: 'solid', outlineColor: '$outlineColor' }, cursor: "pointer", width: "100%", children: [icon ? (_jsx(XStack, { width: 20, justify: "center", shrink: 0, opacity: 0.8, children: icon })) : null, _jsxs(YStack, { flex: 1, minW: 0, children: [_jsx(SizableText, { size: "$2", color: "$ink", numberOfLines: 1, text: "left", children: label }), sub ? (_jsx(SizableText, { size: "$1", color: "$soft", numberOfLines: 1, text: "left", children: sub })) : null] }), trail] }));
114
+ return (_jsxs(XStack, { render: "button", ...{ role: radio ? 'menuitemradio' : 'menuitem' }, "aria-checked": radio ? checked : undefined, "data-item": "", onPress: onPress, items: "center", gap: "$2.5", px: "$2", minH: 30, py: 4, rounded: "$3", bg: checked ? '$edge' : 'transparent', hoverStyle: { bg: '$hover' }, focusVisibleStyle: { bg: '$hover', outlineWidth: 2, outlineStyle: 'solid', outlineColor: '$outlineColor' }, cursor: "pointer", width: "100%", children: [icon ? (_jsx(XStack, { width: 20, justify: "center", shrink: 0, opacity: 0.8, children: icon })) : null, _jsxs(YStack, { flex: 1, minW: 0, children: [_jsx(SizableText, { size: "$2", color: "$ink", numberOfLines: 1, text: "left", children: label }), sub ? (_jsx(SizableText, { size: "$1", color: "$soft", numberOfLines: 1, text: "left", children: sub })) : null] }), trail] }));
115
115
  }
116
116
  const Rule = () => _jsx(XStack, { height: 1, bg: "$borderColor", my: "$1", mx: "$1" });
117
117
  const Label = ({ children }) => (_jsx(SizableText, { size: "$1", color: "$soft", px: "$2", pt: "$1.5", pb: "$1", children: children }));
@@ -150,8 +150,21 @@ function Refit({ children }) {
150
150
  }, [update]);
151
151
  return (_jsx(View, { render: _jsx("div", { ref: box }), self: "stretch", minW: 0, children: children }));
152
152
  }
153
- export function Me({ host, narrow = false, onOrg, }) {
153
+ /**
154
+ * The plans row, named by the plan the organization is on: Enterprise and
155
+ * Agency are plans a person stays on, so the row names theirs; every other
156
+ * plan, Free included, is one to move up from.
157
+ */
158
+ export const upgrade = (family) => family === 'Enterprise' ? 'Enterprise plan' : family === 'Agency' ? 'Agency plan' : 'Upgrade plan';
159
+ /** The keys a row prints, in this platform's spelling. */
160
+ const mac = () => typeof navigator !== 'undefined' && /Mac|iPhone|iPad/.test(navigator.platform || navigator.userAgent);
161
+ /** A row's keys at its end, quiet. */
162
+ const Keys = ({ children }) => (_jsx(SizableText, { size: "$1", color: "$soft", shrink: 0, children: children }));
163
+ /** A row that opens a page of the menu. */
164
+ const More = () => _jsx(ChevronRight, { size: 14, "aria-hidden": true, opacity: 0.6 });
165
+ export function Me({ host, narrow = false, above = false, onOrg, }) {
154
166
  const menu = useWho();
167
+ const [page, setPage] = useState('main');
155
168
  const [creating, setCreating] = useState(false);
156
169
  const [supporting, setSupporting] = useState(false);
157
170
  const row = useRef(null);
@@ -165,15 +178,24 @@ export function Me({ host, narrow = false, onOrg, }) {
165
178
  const other = usePlan(personal && personal.name !== current ? personal.name : null, menu.open);
166
179
  const planFor = (o) => (o.name === current ? spoken(read.value?.label ?? null) : o.personal ? other : '');
167
180
  const who = host.person?.name || host.person?.email || '';
168
- const standing = [said(here?.role ?? ''), here?.display ?? current].filter(Boolean).join(' · ');
169
- // The organization in use takes focus once the menu is drawn — after the
170
- // popover has placed its own focus — so the keys reach the rows at once.
181
+ const plan = spoken(read.value?.label ?? null);
182
+ const apple = mac();
183
+ // Each page opens on its first row, once it is drawn — after the popover has
184
+ // placed its own focus — so the keys reach the rows at once.
171
185
  const land = (node) => {
172
186
  content.current = node;
173
187
  if (!node)
174
188
  return;
175
189
  requestAnimationFrame(() => requestAnimationFrame(() => (node.querySelector('[data-item][aria-checked="true"]') ?? node.querySelector('[data-item]'))?.focus()));
176
190
  };
191
+ const turn = (to) => {
192
+ setPage(to);
193
+ requestAnimationFrame(() => land(content.current));
194
+ };
195
+ const toggle = () => {
196
+ setPage('main');
197
+ menu.toggle();
198
+ };
177
199
  const close = () => {
178
200
  menu.onOpenChange(false);
179
201
  row.current?.focus();
@@ -182,15 +204,20 @@ export function Me({ host, narrow = false, onOrg, }) {
182
204
  menu.onOpenChange(false);
183
205
  host.go(p);
184
206
  };
207
+ const back = (title) => _jsx(Item, { icon: _jsx(ChevronLeft, { size: 15, "aria-hidden": true }), label: title, onPress: () => turn('main') });
185
208
  if (!host.person)
186
209
  return null;
187
- return (_jsxs(_Fragment, { children: [_jsxs(Popover, { open: menu.open, onOpenChange: menu.onOpenChange, placement: narrow ? 'right-end' : 'top-start', allowFlip: true, stayInFrame: true, offset: 8, children: [_jsx(Popover.Anchor, { width: "100%", children: _jsxs(XStack, { ref: (el) => {
210
+ return (_jsxs(_Fragment, { children: [_jsxs(Popover, { open: menu.open, onOpenChange: (open) => {
211
+ if (open)
212
+ setPage('main');
213
+ menu.onOpenChange(open);
214
+ }, placement: narrow && !above ? 'right-end' : above ? 'top-end' : 'top-start', allowFlip: true, stayInFrame: true, offset: 8, children: [_jsx(Popover.Anchor, { width: narrow ? undefined : '100%', children: _jsxs(XStack, { ref: (el) => {
188
215
  row.current = el instanceof HTMLElement ? el : null;
189
- }, render: "button", "data-slot": "me", "aria-label": `Account: ${[who, current].filter(Boolean).join(' · ')}`, "aria-haspopup": "menu", "aria-expanded": menu.open, onPress: menu.toggle, width: narrow ? 40 : '100%', minH: narrow ? 40 : 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 ? 30 : 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 }), standing ? (_jsx(SizableText, { size: "$1", color: "$soft", numberOfLines: 1, text: "left", children: standing })) : null] }), _jsx(ChevronsUpDown, { size: 14, "aria-hidden": true, opacity: 0.6 })] }))] }) }), _jsx(Popover.Content, { ...glass(2), items: "stretch", borderWidth: 1, rounded: "$6", p: "$1.5", width: 288, maxW: "calc(100vw - 16px)", maxH: "calc(100dvh - 32px)", overflowY: "auto", overflowX: "hidden", children: _jsx(Refit, { children: _jsx(View, { render: _jsx("div", { ref: land, role: "menu", "aria-label": "Account", onKeyDown: (e) => rove(e, close) }), self: "stretch", minW: 0, children: _jsxs(YStack, { gap: 2, children: [_jsx(SizableText, { size: "$2", color: "$soft", px: "$2", py: "$1.5", numberOfLines: 1, children: host.person.email || who }), _jsx(Rule, {}), _jsx(Meter, { read: read, onPlans: () => go(path({ kind: 'screen', screen: 'plans' })), onBilling: () => go(path({ kind: 'settings', section: 'billing' })) }), _jsx(Rule, {}), _jsx(Label, { children: "Organizations" }), _jsx(YStack, { role: "group", "aria-label": "Organizations", gap: 2, children: all.map((o) => (_jsx(Item, { icon: _jsx(Mark, { org: o }), label: o.display, sub: [o.personal ? 'Personal' : said(o.role), planFor(o)].filter(Boolean).join(' · '), checked: o.name === current, onPress: () => {
216
+ }, render: "button", "data-slot": "me", "aria-label": `Account: ${[who, current].filter(Boolean).join(' · ')}`, ...{ title: who }, "aria-haspopup": "menu", "aria-expanded": menu.open, onPress: toggle, 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 }), plan ? (_jsx(SizableText, { size: "$1", color: "$soft", numberOfLines: 1, text: "left", children: plan })) : null] }), _jsx(ChevronsUpDown, { size: 14, "aria-hidden": true, opacity: 0.6 })] }))] }) }), _jsx(Popover.Content, { ...glass(2), items: "stretch", borderWidth: 1, rounded: "$5", p: "$1", width: 240, maxW: "calc(100vw - 16px)", maxH: "calc(100dvh - 32px)", overflowY: "auto", overflowX: "hidden", children: _jsx(Refit, { children: _jsx(View, { render: _jsx("div", { ref: land, role: "menu", "aria-label": "Account", onKeyDown: (e) => rove(e, close) }), self: "stretch", minW: 0, children: page === 'main' ? (_jsxs(YStack, { gap: 1, children: [_jsxs(XStack, { items: "center", gap: "$2.5", px: "$2", pt: "$1.5", pb: "$2", 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: plan || host.person.email || '' })] })] }), _jsx(Rule, {}), _jsx(Item, { icon: _jsx(Gauge, { size: 15, "aria-hidden": true }), label: "Usage remaining", trail: _jsx(More, {}), onPress: () => turn('usage') }), _jsx(Item, { icon: _jsx(Building2, { size: 15, "aria-hidden": true }), label: here?.display ?? current ?? 'Organization', sub: "Organization", trail: _jsx(More, {}), onPress: () => turn('orgs') }), _jsx(Item, { icon: _jsx(KeyRound, { size: 15, "aria-hidden": true }), label: "API keys", trail: _jsx(ExternalLink, { size: 13, "aria-hidden": true, opacity: 0.6 }), onPress: () => (menu.onOpenChange(false), window.location.assign(KEYS)) }), _jsx(Item, { icon: _jsx(Settings, { size: 15, "aria-hidden": true }), label: "Settings", trail: _jsx(Keys, { children: apple ? '⌘,' : 'Ctrl+,' }), onPress: () => go(path({ kind: 'settings', section: 'general' })) }), _jsx(Rule, {}), _jsx(Item, { icon: _jsx(LifeBuoy, { size: 15, "aria-hidden": true }), label: "Help", trail: _jsx(More, {}), onPress: () => turn('help') }), host.signOut ? _jsx(Item, { icon: _jsx(LogOut, { size: 15, "aria-hidden": true }), label: "Log out", onPress: () => (menu.onOpenChange(false), host.signOut?.()) }) : null] })) : page === 'usage' ? (_jsxs(YStack, { gap: 1, children: [back('Usage remaining'), _jsx(Rule, {}), _jsx(Meter, { read: read, onPlans: () => go(path({ kind: 'screen', screen: 'plans' })), onBilling: () => go(path({ kind: 'settings', section: 'billing' })) }), _jsx(Rule, {}), _jsx(Item, { icon: _jsx(Gauge, { size: 15, "aria-hidden": true }), label: "Usage", onPress: () => go(path({ kind: 'settings', section: 'usage' })) }), _jsx(Item, { icon: _jsx(CreditCard, { size: 15, "aria-hidden": true }), label: "Billing", onPress: () => go(path({ kind: 'settings', section: 'billing' })) }), _jsx(Item, { icon: _jsx(Sparkles, { size: 15, "aria-hidden": true }), label: upgrade(read.value?.label?.name), onPress: () => go(path({ kind: 'screen', screen: 'plans' })) })] })) : page === 'orgs' ? (_jsxs(YStack, { gap: 1, children: [back('Organization'), _jsx(Rule, {}), _jsx(Label, { children: "Organizations" }), _jsx(YStack, { role: "group", "aria-label": "Organizations", gap: 2, children: all.map((o) => (_jsx(Item, { icon: _jsx(Mark, { org: o }), label: o.display, sub: [o.personal ? 'Personal' : said(o.role), planFor(o)].filter(Boolean).join(' · '), checked: o.name === current, onPress: () => {
190
217
  menu.onOpenChange(false);
191
218
  if (o.name !== current)
192
219
  onOrg(o.name);
193
- } }, o.name))) }), _jsx(Item, { icon: _jsx(Plus, { size: 15, "aria-hidden": true }), label: "Create organization", onPress: () => (menu.onOpenChange(false), setCreating(true)) }), _jsx(Item, { icon: _jsx(Settings, { size: 15, "aria-hidden": true }), label: "Organization settings", onPress: () => (menu.onOpenChange(false), window.location.assign(site(ORG_SETTINGS))) }), where().team ? null : here && !here.personal ? (_jsx(Item, { icon: _jsx(Users, { size: 15, "aria-hidden": true }), label: "Open in Hanzo Team", trail: _jsx(ExternalLink, { size: 13, "aria-hidden": true, opacity: 0.6 }), onPress: () => (menu.onOpenChange(false), window.location.assign(`${ENTRY}&org=${encodeURIComponent(here.name)}`)) })) : (_jsx(Item, { icon: _jsx(Users, { size: 15, "aria-hidden": true }), label: "Create a team", onPress: () => (menu.onOpenChange(false), setCreating(true)) })), superAdmin() ? (_jsx(Item, { icon: _jsx(Building2, { size: 15, "aria-hidden": true }), label: "All organizations", onPress: () => (menu.onOpenChange(false), setSupporting(true)) })) : null, _jsx(Rule, {}), _jsx(Item, { icon: _jsx(Settings, { size: 15, "aria-hidden": true }), label: "Settings", onPress: () => go(path({ kind: 'settings', section: 'general' })) }), _jsx(Item, { icon: _jsx(Gauge, { size: 15, "aria-hidden": true }), label: "Usage", onPress: () => go(path({ kind: 'settings', section: 'usage' })) }), _jsx(Item, { icon: _jsx(CreditCard, { size: 15, "aria-hidden": true }), label: "Billing", onPress: () => go(path({ kind: 'settings', section: 'billing' })) }), _jsx(Item, { icon: _jsx(KeyRound, { size: 15, "aria-hidden": true }), label: "API keys", onPress: () => (menu.onOpenChange(false), window.location.assign(KEYS)) }), _jsx(Item, { icon: _jsx(Sparkles, { size: 15, "aria-hidden": true }), label: "View all plans", onPress: () => go(path({ kind: 'screen', screen: 'plans' })) }), _jsx(Item, { icon: _jsx(LifeBuoy, { size: 15, "aria-hidden": true }), label: "Get help", onPress: () => (menu.onOpenChange(false), window.open(DOCS, '_blank', 'noopener,noreferrer')) }), host.signOut ? (_jsxs(_Fragment, { children: [_jsx(Rule, {}), _jsx(Item, { icon: _jsx(LogOut, { size: 15, "aria-hidden": true }), label: "Log out", onPress: () => (menu.onOpenChange(false), host.signOut?.()) })] })) : null] }) }) }) })] }), _jsx(Create, { open: creating, onOpenChange: setCreating }), supporting ? _jsx(SupportPicker, { onClose: () => setSupporting(false) }) : null] }));
220
+ } }, o.name))) }), _jsx(Item, { icon: _jsx(Plus, { size: 15, "aria-hidden": true }), label: "Create organization", onPress: () => (menu.onOpenChange(false), setCreating(true)) }), _jsx(Item, { icon: _jsx(Settings, { size: 15, "aria-hidden": true }), label: "Organization settings", onPress: () => (menu.onOpenChange(false), window.location.assign(site(ORG_SETTINGS))) }), where().team ? null : here && !here.personal ? (_jsx(Item, { icon: _jsx(Users, { size: 15, "aria-hidden": true }), label: "Open in Hanzo Team", trail: _jsx(ExternalLink, { size: 13, "aria-hidden": true, opacity: 0.6 }), onPress: () => (menu.onOpenChange(false), window.location.assign(`${ENTRY}&org=${encodeURIComponent(here.name)}`)) })) : (_jsx(Item, { icon: _jsx(Users, { size: 15, "aria-hidden": true }), label: "Create a team", onPress: () => (menu.onOpenChange(false), setCreating(true)) })), superAdmin() ? (_jsx(Item, { icon: _jsx(Building2, { size: 15, "aria-hidden": true }), label: "All organizations", onPress: () => (menu.onOpenChange(false), setSupporting(true)) })) : null] })) : (_jsxs(YStack, { gap: 1, children: [back('Help'), _jsx(Rule, {}), _jsx(Item, { icon: _jsx(LifeBuoy, { size: 15, "aria-hidden": true }), label: "Get help", trail: _jsx(ExternalLink, { size: 13, "aria-hidden": true, opacity: 0.6 }), onPress: () => (menu.onOpenChange(false), window.open(DOCS, '_blank', 'noopener,noreferrer')) }), _jsx(Item, { icon: _jsx(Mail, { size: 15, "aria-hidden": true }), label: "Contact us", onPress: () => (menu.onOpenChange(false), window.location.assign(site('/contact'))) })] })) }) }) })] }), _jsx(Create, { open: creating, onOpenChange: setCreating }), supporting ? _jsx(SupportPicker, { onClose: () => setSupporting(false) }) : null] }));
194
221
  }
195
222
  /**
196
223
  * The sheet that creates an organization: a name, then `POST /v1/account/orgs`,
package/dist/Meet.js CHANGED
@@ -273,7 +273,7 @@ export function Meet() {
273
273
  return (_jsxs(YStack, { width: "100%", height: "100%", bg: "$background", overflowX: "auto", overflowY: "auto", children: [_jsxs(XStack, { minH: 56, px: 20, py: 8, gap: 10, flexWrap: "wrap", bg: "$panel", borderBottomWidth: 1, borderBottomColor: "$borderColor", items: "center", justify: "space-between", children: [_jsxs(XStack, { items: "center", gap: 12, shrink: 1, minW: "auto", children: [_jsx(XStack, { width: 32, height: 32, rounded: 8, bg: "$raised", items: "center", justify: "center", shrink: 1, minW: "auto", children: _jsx(Video, { size: 18, color: "var(--foreground)" }) }), _jsxs(YStack, { shrink: 1, minW: "auto", children: [_jsxs(XStack, { items: "center", gap: 8, children: [_jsx(Text, { fontSize: "$4", fontWeight: "700", color: "$ink", children: "Hanzo Meet" }), _jsx(Text, { fontSize: FLOOR, fontWeight: "700", bg: "$hover", color: "$soft", borderWidth: 1, borderColor: "$borderColor", px: 6, py: 1, rounded: 4, children: "LIVE \u00B7 meet.hanzo.ai" })] }), _jsx(Text, { fontSize: "$1", color: "$soft", children: "Native WebRTC video conferencing with AI meeting bot transcription & voice" })] })] }), activeMeetingRoom ? (_jsxs(XStack, { items: "center", gap: 8, shrink: 1, minW: "auto", flexWrap: "wrap", children: [_jsx(XStack, { bg: "$hover", rounded: 8, p: 2, gap: 2, borderWidth: 1, borderColor: "$borderColor", shrink: 1, minW: "auto", children: [
274
274
  ['iframe', 'SFU Stage', Tv],
275
275
  ['grid', 'Agent Grid', LayoutGrid],
276
- ].map(([view, label, Icon]) => (_jsxs(Button, { size: "sm", variant: meetingView === view ? 'primary' : 'ghost', ...(meetingView === view ? null : { color: '$soft' }), "aria-pressed": meetingView === view, onClick: () => setMeetingView(view), children: [_jsx(Icon, { size: 12 }), label] }, view))) }), botAssisting ? (_jsx(View, { display: "none", "$max-md": { display: 'flex' }, children: _jsxs(Button, { size: "sm", variant: copilot ? 'secondary' : 'default', onClick: () => setCopilot((v) => !v), "aria-label": "Co-pilot", "aria-expanded": copilot, "aria-controls": "meet-copilot", children: [_jsx(Bot, { size: 13 }), "Co-pilot"] }) })) : null, _jsxs(Button, { size: "sm", onClick: () => handleCopyLink(activeMeetingRoom), children: [copied ? _jsx(Check, { size: 13 }) : _jsx(Copy, { size: 13 }), copied ? 'Link Copied' : 'Copy Room Link'] }), _jsxs(Button, { size: "sm", variant: "destructive", onClick: () => {
276
+ ].map(([view, label, Icon]) => (_jsxs(Button, { size: "sm", variant: meetingView === view ? 'primary' : 'ghost', ...(meetingView === view ? null : { color: '$soft' }), "aria-pressed": meetingView === view, onClick: () => setMeetingView(view), children: [_jsx(Icon, { size: 12 }), label] }, view))) }), botAssisting ? (_jsx(View, { display: "none", "$max-md": { display: 'flex' }, children: _jsxs(Button, { size: "sm", variant: copilot ? 'secondary' : 'default', onClick: () => setCopilot((v) => !v), "aria-label": "Co-pilot", "aria-expanded": copilot, "aria-controls": "meet-copilot", children: [_jsx(Bot, { size: 13 }), "Co-pilot"] }) })) : null, _jsxs(Button, { size: "sm", onClick: () => handleCopyLink(activeMeetingRoom), children: [copied ? _jsx(Check, { size: 13 }) : _jsx(Copy, { size: 13 }), copied ? 'Link Copied' : 'Copy meeting link'] }), _jsxs(Button, { size: "sm", variant: "destructive", onClick: () => {
277
277
  stopAgentSpeech();
278
278
  // Leaving the call hangs up the hands-free conversation and
279
279
  // settles the transcript with it.
package/dist/Prose.js CHANGED
@@ -7,8 +7,15 @@ const INK = 'var(--foreground)';
7
7
  const QUIET = 'var(--muted-foreground)';
8
8
  const EDGE = 'var(--border)';
9
9
  const GROUND = 'var(--secondary)';
10
- /** A heading's rung, by depth: one step down the type ramp per level. */
11
- const RUNG = ['$6', '$4', '$3', '$2', '$2', '$2'];
10
+ /**
11
+ * The reading type, px: a 16px body on a 24px line, and a heading's size and
12
+ * line by depth, as the reference conversation sets them — the body a reader
13
+ * reads at arm's length, not a sidebar's 14.
14
+ */
15
+ const BODY = 16;
16
+ const LINE = 24;
17
+ const RUNG = [28, 24, 20, 17, 16, 16];
18
+ const LEAD = [36, 32, 28, 24, 24, 24];
12
19
  /** The character references a model writes, as the characters they name. An
13
20
  * HTML renderer let the browser decode `&amp;`; a string React sets is shown
14
21
  * exactly as written. */
@@ -21,7 +28,7 @@ const decode = (s) => s.replace(/&(?:#x([0-9a-f]{1,6})|#(\d{1,7})|([a-z]+));/gi,
21
28
  return NAMED[name.toLowerCase()] ?? all;
22
29
  });
23
30
  /** One run of inline tokens, in `tone`, at the block's `rung`. */
24
- function inline(tokens, tone, rung = '$2') {
31
+ function inline(tokens, tone, rung = BODY) {
25
32
  return (tokens ?? []).map((t, i) => {
26
33
  switch (t.type) {
27
34
  case 'strong':
@@ -31,7 +38,7 @@ function inline(tokens, tone, rung = '$2') {
31
38
  case 'del':
32
39
  return (_jsx(Text, { render: "del", color: tone, textDecorationLine: "line-through", opacity: 0.7, children: inline(t.tokens, tone, rung) }, i));
33
40
  case 'codespan':
34
- return (_jsx(Text, { render: "code", color: tone, fontFamily: "$mono", fontSize: "$1", px: "$1", rounded: 4, bg: GROUND, children: decode(t.text) }, i));
41
+ return (_jsx(Text, { render: "code", color: tone, fontFamily: "$mono", fontSize: Math.round(rung * 0.875), px: "$1", rounded: 4, bg: GROUND, children: decode(t.text) }, i));
35
42
  case 'br':
36
43
  return _jsx("br", {}, i);
37
44
  case 'link': {
@@ -41,7 +48,7 @@ function inline(tokens, tone, rung = '$2') {
41
48
  // AWAY IS A NEW TAB, HOME IS NOT. Someone else's page opened from a
42
49
  // message never gets a handle on this one and never inherits the
43
50
  // referrer; a link back into this app is a link.
44
- return (_jsx(Anchor, { href: to.href, ...(to.away ? { target: '_blank', rel: 'noopener noreferrer nofollow' } : {}), ...(t.title ? { title: t.title } : {}), size: rung, color: INK, textDecorationLine: "underline", children: inline(t.tokens, INK, rung) }, i));
51
+ return (_jsx(Anchor, { href: to.href, ...(to.away ? { target: '_blank', rel: 'noopener noreferrer nofollow' } : {}), ...(t.title ? { title: t.title } : {}), fontSize: rung, color: INK, textDecorationLine: "underline", children: inline(t.tokens, INK, rung) }, i));
45
52
  }
46
53
  // An image is a fetch to wherever the message says, which is a reader's
47
54
  // address leaking to whoever wrote it. The alt text is the message.
@@ -59,11 +66,11 @@ function inline(tokens, tone, rung = '$2') {
59
66
  }
60
67
  /** Running text: a paragraph, or a list item's own words. */
61
68
  function Line({ tokens, tone }) {
62
- return (_jsx(Text, { render: "p", color: tone, fontSize: "$2", lineHeight: "$3", children: inline(tokens, tone) }));
69
+ return (_jsx(Text, { render: "p", color: tone, fontSize: BODY, lineHeight: LINE, children: inline(tokens, tone) }));
63
70
  }
64
71
  function List({ list, tone }) {
65
72
  const start = typeof list.start === 'number' ? list.start : 1;
66
- return (_jsx(YStack, { render: list.ordered ? 'ol' : 'ul', m: 0, p: 0, gap: "$1", children: list.items.map((item, i) => (_jsxs(XStack, { render: "li", gap: "$1", children: [_jsx(Text, { "aria-hidden": true, color: tone, fontSize: "$2", lineHeight: "$3", minW: 14, text: "right", children: item.task ? (item.checked ? '☑' : '☐') : list.ordered ? `${start + i}.` : '•' }), _jsx(YStack, { flex: 1, minW: 0, gap: "$1", children: blocks(item.tokens, tone) })] }, i))) }));
73
+ return (_jsx(YStack, { render: list.ordered ? 'ol' : 'ul', m: 0, p: 0, gap: "$1", children: list.items.map((item, i) => (_jsxs(XStack, { render: "li", gap: "$1", children: [_jsx(Text, { "aria-hidden": true, color: tone, fontSize: BODY, lineHeight: LINE, minW: 16, text: "right", children: item.task ? (item.checked ? '☑' : '☐') : list.ordered ? `${start + i}.` : '•' }), _jsx(YStack, { flex: 1, minW: 0, gap: "$1", children: blocks(item.tokens, tone) })] }, i))) }));
67
74
  }
68
75
  /** A table is a grid: one track per column, as wide as what the column holds,
69
76
  * in a box that scrolls sideways rather than widening the message. Cells
@@ -71,7 +78,7 @@ function List({ list, tone }) {
71
78
  * the top and the first column the left. */
72
79
  function Table({ table, tone }) {
73
80
  const align = (c) => table.align[c] ?? 'left';
74
- return (_jsx(View, { role: "table", display: "grid", gridTemplateColumns: `repeat(${table.header.length}, max-content)`, self: "flex-start", maxW: "100%", overflowX: "auto", children: [table.header, ...table.rows].map((row, r) => (_jsx(View, { role: "row", display: "contents", children: row.map((cell, c) => (_jsx(Text, { role: r ? 'cell' : 'columnheader', color: tone, fontSize: "$2", lineHeight: "$3", fontWeight: r ? '400' : '600', text: align(c), px: "$2", py: "$1", bg: r ? undefined : GROUND, borderColor: EDGE, borderStyle: "solid", borderRightWidth: 1, borderBottomWidth: 1, borderTopWidth: r ? 0 : 1, borderLeftWidth: c ? 0 : 1, children: inline(cell.tokens, tone) }, c))) }, r))) }));
81
+ return (_jsx(View, { role: "table", display: "grid", gridTemplateColumns: `repeat(${table.header.length}, max-content)`, self: "flex-start", maxW: "100%", overflowX: "auto", children: [table.header, ...table.rows].map((row, r) => (_jsx(View, { role: "row", display: "contents", children: row.map((cell, c) => (_jsx(Text, { role: r ? 'cell' : 'columnheader', color: tone, fontSize: 14, lineHeight: 20, fontWeight: r ? '400' : '600', text: align(c), px: "$2", py: "$1", bg: r ? undefined : GROUND, borderColor: EDGE, borderStyle: "solid", borderRightWidth: 1, borderBottomWidth: 1, borderTopWidth: r ? 0 : 1, borderLeftWidth: c ? 0 : 1, children: inline(cell.tokens, tone, 14) }, c))) }, r))) }));
75
82
  }
76
83
  /** A run of block tokens. A tight list item's words arrive as a `text` block. */
77
84
  function blocks(tokens, tone) {
@@ -82,7 +89,7 @@ function blocks(tokens, tone) {
82
89
  case 'text':
83
90
  return _jsx(Line, { tokens: 'tokens' in t && t.tokens ? t.tokens : [t], tone: tone }, i);
84
91
  case 'heading':
85
- return (_jsx(Text, { render: `h${t.depth}`, m: 0, color: tone, fontSize: RUNG[t.depth - 1], lineHeight: RUNG[t.depth - 1], fontWeight: "600", children: inline(t.tokens, tone, RUNG[t.depth - 1]) }, i));
92
+ return (_jsx(Text, { render: `h${t.depth}`, m: 0, mt: t.depth < 3 ? 8 : 4, color: tone, fontSize: RUNG[t.depth - 1], lineHeight: LEAD[t.depth - 1], fontWeight: "600", children: inline(t.tokens, tone, RUNG[t.depth - 1]) }, i));
86
93
  case 'list':
87
94
  return _jsx(List, { list: t, tone: tone }, i);
88
95
  case 'table':
package/dist/Rail.d.ts ADDED
@@ -0,0 +1,39 @@
1
+ import type { ReactNode } from 'react';
2
+ import { type Host } from '@hanzo/build';
3
+ import type { Mode } from './recent.js';
4
+ /** The rail's width, px. */
5
+ export declare const RAIL = 48;
6
+ /** One mark: an icon button named for where it goes, its name its tooltip, lit where the reader is. */
7
+ export declare function Mark({ label, on, onPress, children, ...rest }: {
8
+ label: string;
9
+ /** The reader is there. */
10
+ on?: boolean;
11
+ onPress?: () => void;
12
+ children: ReactNode;
13
+ [attribute: `data-${string}` | `aria-${string}`]: unknown;
14
+ }): import("react").JSX.Element;
15
+ /** The sidebar's toggle: the same mark wherever a host draws it. */
16
+ export declare function Toggle({ open, onPress, label }: {
17
+ open: boolean;
18
+ onPress: () => void;
19
+ label?: string;
20
+ }): import("react").JSX.Element;
21
+ export declare function Rail({ host, mode, at, open, toggle, flat, onChat, onDev, go, onOrg, }: {
22
+ host: Host;
23
+ mode: Mode;
24
+ /** Dev's builder address, or null in Chat. */
25
+ at: string | null;
26
+ /** The sidebar beside the rail is open. */
27
+ open: boolean;
28
+ /** Lead with the sidebar's toggle; absent where the host's bar holds it. */
29
+ toggle?: () => void;
30
+ /** Laid along the foot of the phone's drawer rather than down the window's edge. */
31
+ flat?: boolean;
32
+ /** To Chat and to Dev, each where the reader left it. */
33
+ onChat: () => void;
34
+ onDev: () => void;
35
+ /** Moves the app to an address of this page. */
36
+ go: (address: string) => void;
37
+ /** Work in another organization. */
38
+ onOrg: (org: string) => void;
39
+ }): import("react").JSX.Element;