@hanzo/rooms 0.1.57 → 0.1.58

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/App.d.ts CHANGED
@@ -1 +1,13 @@
1
- export declare function App(): import("react").JSX.Element;
1
+ /**
2
+ * The window's bar, for a host that is a window of its own: `inset` px kept
3
+ * clear at its left for the window's own controls (macOS paints its traffic
4
+ * lights over the page there), and `drag`, the attributes the host's shell
5
+ * reads as the region that moves the window.
6
+ */
7
+ export interface Bar {
8
+ inset?: number;
9
+ drag?: Record<string, unknown>;
10
+ }
11
+ export declare function App({ bar }?: {
12
+ bar?: Bar;
13
+ }): import("react").JSX.Element;
package/dist/App.js CHANGED
@@ -2,58 +2,74 @@
2
2
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  // The web app as the whole window, in two modes: Chat, a conversation with the
4
4
  // product's AI, and Dev, @hanzo/build's `<Builder>` without a rail of its own.
5
- // One column serves both (Column.tsx): the wordmark and the switch, New, Dev's
6
- // places, the reader's Recents in both modes, and the account. The address says
7
- // the mode, as a path (lib/host `talk`, `app`, `place`): `/` and `/chat` are
8
- // Chat on a new conversation, `/chat/<id>` one of the reader's, and a path under
9
- // `/dev` is Dev: `/dev/run/<id>` a run, `/dev/projects/<org>/<repo>` a project.
10
- // Moving between the modes is a history entry (`go`), never a load, so a host
11
- // serves this one component for every path under each.
12
5
  //
13
- // Every host draws this: hanzo.ai at `/`, the desktop app as its main window.
14
- // The host mounts IamProvider and `<Rooms>` with its router and the path, and
15
- // draws this only for a signed-in reader; what a stranger sees is the host's.
6
+ // OUTER the rail (Rail.tsx) and the sidebar beside it (Column.tsx), on the
7
+ // window's floor, with the window's bar above them where the host has
8
+ // no browser around it (`bar`).
9
+ // INNER the content, cut as one rounded panel inset from the window's edges
10
+ // and from the sidebar by the gutter (`--pane-gap`): Chat's
11
+ // conversation with its tabs beside it (Paned), or Dev's builder.
12
+ //
13
+ // The rail moves between the modes; the sidebar holds the mode's name, its
14
+ // search, New, Dev's places and the reader's Recents. The address says the
15
+ // mode, as a path (lib/host `talk`, `app`, `place`): `/` and `/chat` are Chat on
16
+ // a new conversation, `/chat/<id>` one of the reader's, and a path under `/dev`
17
+ // is Dev: `/dev/run/<id>` a run, `/dev/projects/<org>/<repo>` a project. Moving
18
+ // between the modes is a history entry (`go`), never a load, so a host serves
19
+ // this one component for every path under each.
20
+ //
21
+ // Every host draws this: hanzo.ai at `/`, `/chat` and `/dev`, and the desktop
22
+ // app as its main window. The host mounts IamProvider and `<Rooms>` with its
23
+ // router and the path, and draws this only for a signed-in reader; what a
24
+ // stranger sees is the host's.
25
+ //
26
+ // THE SIDEBAR'S TOGGLE IS AT THE WINDOW'S TOP-LEFT. In a browser tab it leads
27
+ // the rail. A host that is a window of its own (`bar`) has no browser around
28
+ // it, so the app draws the window's bar: the toggle, then Back and Forward
29
+ // through the host's router, clear of the window's own controls, and the bar
30
+ // drags the window. Shut, the sidebar leaves the rail alone at the edge; the
31
+ // choice is kept per browser under the key the builder's rail kept it under.
32
+ //
33
+ // THE KEYS: ⌘K / Ctrl+K searches the sidebar (opening it first), ⌘, / Ctrl+,
34
+ // opens Settings, and in a window ⌘[ ⌘] / Alt+← Alt+→ are Back and Forward —
35
+ // never while the reader types.
16
36
  //
17
37
  // DEV'S PANE HAS NO COLUMN BESIDE IT. The column beside a pane is Chat's (its
18
- // pinned summary, `useOpen().aside`), and that choice outlives a move to Dev,
19
- // where nothing fills it: drawn there it was an empty pane to the right of the
20
- // builder. So Dev's pane is cut on its own and the summary is Chat's alone.
38
+ // tabs, `useOpen().aside`), and that choice outlives a move to Dev, where
39
+ // nothing fills it. So Dev's pane is cut on its own and the tabs are Chat's.
21
40
  //
22
41
  // EVERY MOVE PUTS THE DRAWER AWAY, the one to where the reader already is
23
- // included. Pressed on the page it names, a row moves nothing, and a drawer
24
- // that closed only on a new address stayed open over the page it had asked for.
42
+ // included.
25
43
  //
26
44
  // THE PLANS' OWN ADDRESS LEAVES for the pay site in its place (dev.tsx `leave`),
27
45
  // so the builder's own plans are never drawn.
28
- //
29
- // THE WINDOW IS A FRAME (`data-slot="frame"`): the column, the pane and the
30
- // column beside it are three panes floating on a lit floor (ground.ts), one
31
- // gutter round them and between them, `--pane-gap`, each sized by the edge
32
- // beside it.
33
- import { useCallback, useEffect, useMemo, useState } from 'react';
34
- import { Menu } from 'lucide-react';
46
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
47
+ import { ArrowLeft, ArrowRight, Menu } from 'lucide-react';
35
48
  import { AiProvider } from '@hanzo/ai/react';
36
49
  import { Text, XStack, YStack } from '@hanzo/gui';
37
- import { Builder, HostProvider, route } from '@hanzo/build';
50
+ import { Builder, HostProvider, path as written, route } from '@hanzo/build';
38
51
  import { SidebarIconButton } from '@hanzo/ui/chat';
39
52
  import { useAi } from './lib/ai.js';
40
53
  import { Chat } from './Chat.js';
41
54
  import { Column } from './Column.js';
42
55
  import { isPlans, leave, useDevHost } from './dev.js';
43
56
  import { useRooms } from './host.js';
44
- import { wordmark } from './ModeMark.js';
57
+ import { useKept } from './kept.js';
58
+ import { Mark, Rail, Toggle } from './Rail.js';
45
59
  import { empty, openThread, useOpen } from './open.js';
46
60
  import { Paned } from './Shell.js';
47
61
  import { floor, pane } from './ground.js';
62
+ import { pick } from './lib/session.js';
48
63
  import { app, go, place, talk } from './lib/host.js';
64
+ import { wordmark } from './where.js';
49
65
  /** `/` is Chat on a new conversation. */
50
66
  const HOME = { mode: 'chat', thread: null };
67
+ /** The bar's height, px: the reference window's title bar. */
68
+ const BAR = 40;
51
69
  /**
52
70
  * The dim under the phone's drawer: design's scrim at two fifths, so the page
53
- * still reads through the drawer's glass the way it reads through the browser's
54
- * own bar, rather than a black wall the glass has nothing to show of. gui
55
- * carries a `color-mix()` to the page unchanged; its types only name custom
56
- * properties.
71
+ * still reads through the drawer's glass. gui carries a `color-mix()` to the
72
+ * page unchanged; its types only name custom properties.
57
73
  */
58
74
  const DIM = 'color-mix(in srgb, var(--surface-scrim) 40%, transparent)';
59
75
  /** The phone's drawer stands the gutter off each edge, or the device's inset where that is wider. */
@@ -62,11 +78,22 @@ const INSET = {
62
78
  bottom: 'max(var(--pane-gap), var(--safe-bottom))',
63
79
  left: 'max(var(--pane-gap), var(--safe-left))',
64
80
  };
65
- /** Where each mode was last, so the switch goes back to it. */
81
+ /** Where each mode was last, so the rail goes back to it. */
66
82
  const last = { chat: '', dev: '' };
67
- export function App() {
83
+ /** Whether a key press is the reader typing, where the app's keys are the field's. */
84
+ const typing = (e) => {
85
+ const el = e.target;
86
+ return !!el && (el.isContentEditable || el.tagName === 'INPUT' || el.tagName === 'TEXTAREA' || el.tagName === 'SELECT');
87
+ };
88
+ /** The builder's Settings, general section. */
89
+ const SETTINGS = written({ kind: 'settings', section: 'general' });
90
+ export function App({ bar } = {}) {
68
91
  const [drawer, setDrawer] = useState(false);
69
92
  const shut = useCallback(() => setDrawer(false), []);
93
+ // The sidebar shut, kept per browser under the key the builder's rail kept it under.
94
+ const [closed, setClosed] = useKept('hanzo.build.rail', false);
95
+ // Each ⌘K: the sidebar's search, asked for again.
96
+ const [seek, setSeek] = useState(0);
70
97
  const dev = useDevHost();
71
98
  // The builder's moves put the drawer away first.
72
99
  const host = useMemo(() => ({
@@ -77,19 +104,22 @@ export function App() {
77
104
  },
78
105
  }), [dev, shut]);
79
106
  const { client } = useAi();
80
- const here = useRooms().path ?? '/';
107
+ const { router, path } = useRooms();
108
+ const here = path ?? '/';
81
109
  const at = place(here) ?? HOME;
82
110
  const mode = at.mode;
111
+ const where = at.mode === 'dev' ? at.at : null;
83
112
  // A run floats two panes of its own, so the pane it stands in is not cut.
84
- const run = at.mode === 'dev' && route(at.at).kind === 'run';
113
+ const run = where !== null && route(where).kind === 'run';
85
114
  const asked = at.mode === 'chat' ? at.thread : null;
86
115
  const { thread } = useOpen();
87
- const plans = at.mode === 'dev' && isPlans(at.at);
116
+ const plans = where !== null && isPlans(where);
88
117
  useEffect(() => {
89
118
  if (plans)
90
119
  leave('app');
91
120
  }, [plans]);
92
- // An address names Chat's conversation, and one just begun writes its id back.
121
+ // An address names Chat's conversation, a conversation just begun writes its
122
+ // id back, and one emptied (New chat, wherever it was pressed) leaves for /chat.
93
123
  useEffect(() => {
94
124
  if (mode !== 'chat')
95
125
  return;
@@ -100,9 +130,18 @@ export function App() {
100
130
  // The address decides; the selection follows it.
101
131
  // eslint-disable-next-line react-hooks/exhaustive-deps
102
132
  }, [mode, asked]);
133
+ // The conversation the pane held before this render: an emptied one is the
134
+ // address's own conversation let go of, never a first render before it opens.
135
+ const held = useRef(null);
103
136
  useEffect(() => {
104
- if (mode === 'chat' && thread && thread !== asked)
137
+ const was = held.current;
138
+ held.current = thread;
139
+ if (mode !== 'chat')
140
+ return;
141
+ if (thread && thread !== asked)
105
142
  go(talk(thread), true);
143
+ else if (!thread && was && asked === was)
144
+ go(talk());
106
145
  // The selection writes the address only when it moves.
107
146
  // eslint-disable-next-line react-hooks/exhaustive-deps
108
147
  }, [mode, thread]);
@@ -115,7 +154,49 @@ export function App() {
115
154
  shut();
116
155
  go(address);
117
156
  }, [shut]);
157
+ // The app's keys.
158
+ useEffect(() => {
159
+ const key = (e) => {
160
+ const mod = e.metaKey || e.ctrlKey;
161
+ if (mod && !e.shiftKey && !e.altKey && e.key.toLowerCase() === 'k') {
162
+ e.preventDefault();
163
+ setClosed(false);
164
+ if (window.matchMedia('(max-width: 767.98px)').matches)
165
+ setDrawer(true);
166
+ setSeek((n) => n + 1);
167
+ return;
168
+ }
169
+ if (typing(e))
170
+ return;
171
+ if (mod && !e.shiftKey && !e.altKey && e.key === ',') {
172
+ e.preventDefault();
173
+ move(app(SETTINGS));
174
+ return;
175
+ }
176
+ if (!bar)
177
+ return;
178
+ const back = (e.metaKey && e.key === '[') || (e.altKey && e.key === 'ArrowLeft');
179
+ const forth = (e.metaKey && e.key === ']') || (e.altKey && e.key === 'ArrowRight');
180
+ if ((!back && !forth) || e.ctrlKey || e.shiftKey)
181
+ return;
182
+ e.preventDefault();
183
+ if (back)
184
+ router.back();
185
+ else
186
+ router.forward();
187
+ };
188
+ window.addEventListener('keydown', key);
189
+ return () => window.removeEventListener('keydown', key);
190
+ // setClosed is the kept setter of this render; the keys read it fresh each time.
191
+ // eslint-disable-next-line react-hooks/exhaustive-deps
192
+ }, [bar, router, move]);
118
193
  const toChat = () => move(last.chat || talk());
119
194
  const toDev = () => move(last.dev || app(''));
120
- return (_jsx(AiProvider, { client: client ?? undefined, children: _jsx(HostProvider, { host: host, children: _jsxs(XStack, { "data-room": "", "data-app": mode, "data-slot": "frame", ...floor, flex: 1, minW: 0, p: "var(--pane-gap)", gap: "var(--pane-gap)", height: "100dvh", overflow: "hidden", "$max-md": { p: 0, gap: 0 }, children: [_jsx(Column, { host: host, mode: mode, thread: thread, onChat: toChat, onDev: toDev, go: move }), _jsxs(YStack, { "data-slot": "stage", flex: 1, minW: 0, minH: 0, gap: "var(--pane-gap)", "$max-md": { gap: 0 }, children: [_jsxs(XStack, { "data-slot": "app-bar", height: 44, px: "$1", gap: "$1", items: "center", shrink: 0, $md: { display: 'none' }, children: [_jsx(SidebarIconButton, { label: "Open sidebar", onPress: () => setDrawer(true), width: 44, height: 44, rounded: 999, children: _jsx(Menu, { size: 18 }) }), _jsx(Text, { fontSize: "$4", fontWeight: "600", color: "$ink", numberOfLines: 1, children: wordmark(mode) })] }), _jsx(XStack, { "data-slot": "panes", flex: 1, minW: 0, minH: 0, children: mode === 'chat' ? (_jsx(Paned, { framed: true, children: _jsx(Chat, {}) })) : (_jsx(YStack, { render: "main", "data-slot": "pane", ...(run ? null : { ...pane(), overflow: 'hidden' }), flex: 1, minW: 0, minH: 0, children: plans ? null : _jsx(Builder, { host: host, rail: false }) })) })] }), drawer ? (_jsxs(_Fragment, { children: [_jsx(YStack, { render: "button", "aria-label": "Close sidebar", onPress: () => setDrawer(false), bg: DIM, position: "absolute", t: 0, r: 0, b: 0, l: 0, z: "var(--z-overlay)", $md: { display: 'none' } }), _jsx(YStack, { "data-slot": "drawer", position: "absolute", t: INSET.top, b: INSET.bottom, l: INSET.left, width: "min(320px, calc(100% - 2 * var(--pane-gap) - 48px))", z: "var(--z-modal)", $md: { display: 'none' }, children: _jsx(Column, { host: host, mode: mode, thread: thread, onChat: toChat, onDev: toDev, go: move, drawer: true }) })] })) : null] }) }) }));
195
+ // Another organization: picked, and the app opened in it at the mode's New.
196
+ const org = (o) => {
197
+ pick(o);
198
+ window.location.assign(mode === 'dev' ? app('') : talk());
199
+ };
200
+ const flip = () => setClosed(!closed);
201
+ return (_jsx(AiProvider, { client: client ?? undefined, children: _jsx(HostProvider, { host: host, children: _jsxs(YStack, { "data-slot": "window", ...(bar ? floor : null), flex: 1, minW: 0, height: "100dvh", overflow: "hidden", children: [bar ? (_jsxs(XStack, { "data-slot": "title-bar", height: BAR, shrink: 0, items: "center", gap: 2, pl: (bar.inset ?? 0) + 6, pr: "$2", ...bar.drag, children: [_jsx(XStack, { "$max-md": { display: 'none' }, children: _jsx(Toggle, { open: !closed, onPress: flip }) }), _jsx(XStack, { $md: { display: 'none' }, children: _jsx(Toggle, { open: drawer, onPress: () => setDrawer(true), label: "Open sidebar" }) }), _jsx(Mark, { label: "Back", "data-slot": "back", onPress: () => router.back(), children: _jsx(ArrowLeft, { size: 18, "aria-hidden": true }) }), _jsx(Mark, { label: "Forward", "data-slot": "forward", onPress: () => router.forward(), children: _jsx(ArrowRight, { size: 18, "aria-hidden": true }) }), _jsx(XStack, { flex: 1, self: "stretch", ...bar.drag })] })) : null, _jsxs(XStack, { "data-room": "", "data-app": mode, "data-slot": "frame", ...(bar ? null : floor), flex: 1, minW: 0, minH: 0, pr: "var(--pane-gap)", pb: "var(--pane-gap)", pt: bar ? 0 : 'var(--pane-gap)', "$max-md": { p: 0 }, children: [_jsx(YStack, { height: "100%", "$max-md": { display: 'none' }, children: _jsx(Rail, { host: host, mode: mode, at: where, open: !closed, toggle: bar ? undefined : flip, onChat: toChat, onDev: toDev, go: move, onOrg: org }) }), closed ? null : (_jsx(YStack, { "$max-md": { display: 'none' }, children: _jsx(Column, { host: host, mode: mode, thread: thread, go: move, onShut: () => setClosed(true), seek: seek }) })), _jsxs(YStack, { "data-slot": "stage", flex: 1, minW: 0, minH: 0, ml: "var(--pane-gap)", "$max-md": { ml: 0 }, children: [bar ? null : (_jsxs(XStack, { "data-slot": "app-bar", height: 44, px: "$1", gap: "$1", items: "center", shrink: 0, $md: { display: 'none' }, children: [_jsx(SidebarIconButton, { label: "Open sidebar", onPress: () => setDrawer(true), width: 44, height: 44, rounded: 999, children: _jsx(Menu, { size: 18 }) }), _jsx(Text, { fontSize: "$4", fontWeight: "600", color: "$ink", numberOfLines: 1, children: wordmark(mode) })] })), _jsx(XStack, { "data-slot": "panes", flex: 1, minW: 0, minH: 0, children: mode === 'chat' ? (_jsx(Paned, { framed: true, children: _jsx(Chat, {}) })) : (_jsx(YStack, { render: "main", "data-slot": "pane", ...(run ? null : { ...pane(), overflow: 'hidden' }), flex: 1, minW: 0, minH: 0, children: plans ? null : _jsx(Builder, { host: host, rail: false }) })) })] })] }), drawer ? (_jsxs(_Fragment, { children: [_jsx(YStack, { render: "button", "aria-label": "Close sidebar", onPress: () => setDrawer(false), bg: DIM, position: "absolute", t: 0, r: 0, b: 0, l: 0, z: "var(--z-overlay)", $md: { display: 'none' } }), _jsx(YStack, { "data-slot": "drawer", position: "absolute", t: INSET.top, b: INSET.bottom, l: INSET.left, width: "min(320px, calc(100% - 2 * var(--pane-gap) - 48px))", z: "var(--z-modal)", $md: { display: 'none' }, children: _jsx(Column, { host: host, mode: mode, thread: thread, go: move, onShut: shut, seek: seek, drawer: true, foot: _jsx(Rail, { host: host, mode: mode, at: where, open: true, flat: true, onChat: toChat, onDev: toDev, go: move, onOrg: org }) }) })] })) : null] }) }) }));
121
202
  }
package/dist/Chat.js CHANGED
@@ -52,13 +52,13 @@ import { useHydrated } from "./lib/hydrated.js";
52
52
  import { base, served } from "./lib/ai.js";
53
53
  import { onServed } from "./lib/served.js";
54
54
  import { refused as worded, speech, useDictation, useTalk, useVoice } from "@hanzo/voice";
55
- import { FileText, Image as ImageMark, Paperclip, Pin, X } from "lucide-react";
55
+ import { FileText, Image as ImageMark, Paperclip, X } from "lucide-react";
56
56
  import { FREE } from "./lib/ai.js";
57
57
  import { openThread, useOpen } from "./open.js";
58
58
  import { Beside, Framed } from "./Shell.js";
59
- import { SEED, look, useKinds } from "./RightPane.js";
59
+ import { SEED, Summary, look } from "./RightPane.js";
60
60
  import { Share } from "./Share.js";
61
- import { useThreadActions, useThreadList } from "./thread.js";
61
+ import { ConversationMenu, ThreadMore, useThreadList } from "./thread.js";
62
62
  import { artifacts, lead, nameOf, PAGE, Panel, PanelToggle, prompt, renders, Reply, Served, useDeck, useKey, useMind, usePanel, } from "@hanzo/build";
63
63
  import { checkoutUrl } from './lib/pay.js';
64
64
  import { Upgrade, useFree } from './Upgrade.js';
@@ -239,6 +239,8 @@ export function Ask({ placeholder, idle, }) {
239
239
  * accessible name does not move when the placeholder does.
240
240
  */
241
241
  const message = () => `Message ${brand()}`;
242
+ /** The panel offers no kinds of its own in Chat: its tabs are pages. */
243
+ const NONE = [];
242
244
  /** The id the room's system turn is held under in the thread. */
243
245
  const ROOM = "room";
244
246
  /** The models that route a turn rather than name the one that answers it. */
@@ -489,13 +491,12 @@ function Thread({ placeholder = message(), initial, anonymous = false, heading,
489
491
  const framed = useContext(Framed);
490
492
  const opened = useOpen();
491
493
  const { thread: open, open: reopen, agents: room, openAgent, seat, emptied, aside, showAside, showSettings } = opened;
492
- // PINNED HERE, READ IN THE COLUMN, and kept by the server (thread.tsx): the
493
- // pin this header sets is the one the row's menu sets, on every device the
494
- // reader signs in on. A conversation not in the list yet (just begun, or
495
- // archived) reads unpinned.
494
+ // THE HEADER'S ⋯ IS THE ROW'S MENU (thread.tsx): rename, pin, share, its
495
+ // link, a new tab, archive and delete, kept by the server. A conversation not
496
+ // in the list yet (just begun, or archived) has no menu until it is.
496
497
  const listing = useThreadList(false, open);
497
- const acts = useThreadActions();
498
498
  const mine = open ? listing.threads.find((t) => t.id === open) : undefined;
499
+ const alone = useMemo(() => new Map(mine ? [[mine.id, mine]] : []), [mine]);
499
500
  // The conversation's key in `pane.ts`, the one the column beside it reads.
500
501
  const at = channel(opened);
501
502
  // The first of them: what a surface that addresses one person reads.
@@ -1042,8 +1043,16 @@ function Thread({ placeholder = message(), initial, anonymous = false, heading,
1042
1043
  // record's own picture. Its own answer comes first, which is the order
1043
1044
  // `Face` already states for every other surface.
1044
1045
  const portrait = mate?.avatar ?? persona?.avatar ?? roleOf(withWhom)?.portrait;
1045
- // Chat's kinds of tab in the side panel: Artifacts and Sources, read from the turns.
1046
- const kinds = useKinds({ at, said, held: files, deck });
1046
+ // THE PANEL IS TABS OF PAGES, a browser beside the conversation; what the
1047
+ // conversation produced and took in is its Summary (RightPane.tsx), each row
1048
+ // opening here. A deck kept from when Artifacts and Sources were tabs lets them go.
1049
+ useEffect(() => {
1050
+ for (const t of deck.tabs)
1051
+ if (t.kind !== PAGE)
1052
+ deck.shut(t.id);
1053
+ // A sweep of what was kept, once the deck is read.
1054
+ // eslint-disable-next-line react-hooks/exhaustive-deps
1055
+ }, [at]);
1047
1056
  /** Whether the field has focus, so the FRAME can wear the ring the field is
1048
1057
  * no longer allowed to draw. `:focus-within` would say this in CSS; the
1049
1058
  * stylesheet that would hold it cannot reach past gui's own rule, so the
@@ -1226,7 +1235,7 @@ function Thread({ placeholder = message(), initial, anonymous = false, heading,
1226
1235
  e.preventDefault();
1227
1236
  // A dropped folder arrives as entries; each file under it goes up with its path.
1228
1237
  void dropped(e.dataTransfer).then((list) => hold(at, list, via));
1229
- }, children: [dragging ? (_jsx(YStack, { "aria-hidden": true, position: "absolute", t: 0, r: 0, b: 0, l: 0, z: 10, items: "center", justify: "center", bg: "var(--surface-scrim)", borderWidth: 2, borderStyle: "dashed", borderColor: "$borderColor", rounded: "var(--pane-round)", pointerEvents: "none", children: _jsxs(XStack, { items: "center", gap: "$2", px: "$4", py: "$2.5", rounded: "$10", bg: "$background", borderWidth: 1, borderColor: "$borderColor", children: [_jsx(Paperclip, { size: 15, "aria-hidden": true }), _jsx(Text, { fontSize: "$4", color: "$ink", children: "Drop files to attach" })] }) })) : null, _jsxs(XStack, { height: 48, items: "center", justify: "space-between", px: "$3", borderBottomWidth: 1, borderColor: "$borderColor", children: [_jsxs(XStack, { items: "center", gap: "$2", minW: 0, children: [_jsx(Box, { render: "button", onClick: () => mine && void acts.pin(mine), "aria-disabled": !mine, "aria-label": mine?.pinned ? "Unpin this conversation" : "Pin this conversation", "aria-pressed": !!mine?.pinned, p: "$1.5", rounded: "$2", opacity: mine ? 1 : 0.5, hoverStyle: { bg: '$hover' }, children: _jsx(Pin, { size: 15, "aria-hidden": true, color: mine?.pinned ? "var(--foreground)" : "var(--muted-foreground)", fill: mine?.pinned ? "currentColor" : "none" }) }), room.length ? (_jsx(XStack, { role: "list", "aria-label": "Agents in this chat", items: "center", gap: "$1.5", minW: 0, overflowX: "auto", "$platform-web": { scrollbarWidth: "none" }, children: room.map((name) => (_jsxs(XStack, { role: "listitem", items: "center", gap: "$1.5", pl: "$1", pr: "$0.5", py: "$0.5", rounded: 999, borderWidth: 1, borderColor: "$borderColor", shrink: 0, children: [_jsx(Face, { src: faces[name]?.avatar ?? (name === withWhom ? portrait : undefined), emoji: faces[name]?.emoji ?? (name === withWhom ? persona?.emoji : undefined), name: name, size: 22 }), _jsx(Text, { fontSize: "$2", fontWeight: "600", color: "$color", numberOfLines: 1, display: "none", $sm: { display: "flex" }, children: name }), _jsx(Box, { render: "button", onClick: () => openAgent(name), "aria-label": `Remove ${name} from this chat`, title: `Remove ${name}`, width: 24, height: 24, rounded: 999, items: "center", justify: "center", hoverStyle: { bg: "$hover" }, children: _jsx(X, { size: 13, "aria-hidden": true, color: "var(--soft, var(--muted-foreground))" }) })] }, name))) })) : null] }), _jsxs(XStack, { items: "center", gap: "$2", children: [open ? _jsx(Share, { thread: open }) : null, _jsx(PanelToggle, { open: panel, onToggle: () => setPanel(!panel) })] })] }), wide && aside !== null ? (_jsx(Beside, { children: _jsx(Panel, { side: deck, kinds: kinds, onHide: () => setPanel(false), label: "Conversation details" }) })) : null, !wide && panel ? _jsx(Panel, { side: deck, kinds: kinds, sheet: true, onHide: () => setPanel(false), label: "Conversation details" }) : null, isFree(model) ? (_jsxs(Text, { fontSize: "$2", color: "$soft", text: "center", maxW: MEASURE, mx: "auto", px: "$4", py: "$2", children: [freeCopy.notice, " ", _jsx(Anchor, { href: site("/legal/privacy"), children: freeCopy.noticeCta })] })) : null, _jsx(ChatSurface, { ...chat, messages: shown, send: guarded,
1238
+ }, children: [dragging ? (_jsx(YStack, { "aria-hidden": true, position: "absolute", t: 0, r: 0, b: 0, l: 0, z: 10, items: "center", justify: "center", bg: "var(--surface-scrim)", borderWidth: 2, borderStyle: "dashed", borderColor: "$borderColor", rounded: "var(--pane-round)", pointerEvents: "none", children: _jsxs(XStack, { items: "center", gap: "$2", px: "$4", py: "$2.5", rounded: "$10", bg: "$background", borderWidth: 1, borderColor: "$borderColor", children: [_jsx(Paperclip, { size: 15, "aria-hidden": true }), _jsx(Text, { fontSize: "$4", color: "$ink", children: "Drop files to attach" })] }) })) : null, _jsxs(XStack, { "data-slot": "conversation-head", height: 52, items: "center", justify: "space-between", gap: "$3", pl: "$4", pr: "$2", shrink: 0, children: [_jsxs(XStack, { items: "center", gap: "$2", minW: 0, flex: 1, children: [mine ? (_jsx(Text, { fontSize: 15, lineHeight: 20, color: "$ink", numberOfLines: 1, shrink: 1, minW: 0, children: mine.title || "Untitled" })) : null, room.length ? (_jsx(XStack, { role: "list", "aria-label": "Agents in this chat", items: "center", gap: "$1.5", minW: 0, overflowX: "auto", "$platform-web": { scrollbarWidth: "none" }, children: room.map((name) => (_jsxs(XStack, { role: "listitem", items: "center", gap: "$1.5", pl: "$1", pr: "$0.5", py: "$0.5", rounded: 999, borderWidth: 1, borderColor: "$borderColor", shrink: 0, children: [_jsx(Face, { src: faces[name]?.avatar ?? (name === withWhom ? portrait : undefined), emoji: faces[name]?.emoji ?? (name === withWhom ? persona?.emoji : undefined), name: name, size: 22 }), _jsx(Text, { fontSize: "$2", fontWeight: "600", color: "$color", numberOfLines: 1, display: "none", $sm: { display: "flex" }, children: name }), _jsx(Box, { render: "button", onClick: () => openAgent(name), "aria-label": `Remove ${name} from this chat`, title: `Remove ${name}`, width: 24, height: 24, rounded: 999, items: "center", justify: "center", hoverStyle: { bg: "$hover" }, children: _jsx(X, { size: 13, "aria-hidden": true, color: "var(--soft, var(--muted-foreground))" }) })] }, name))) })) : null] }), _jsxs(XStack, { "data-slot": "conversation-controls", items: "center", gap: 2, shrink: 0, children: [open ? _jsx(Share, { thread: open }) : null, mine ? (_jsx(ConversationMenu, { threads: alone, active: open, children: _jsx(XStack, { children: _jsx(ThreadMore, { t: mine }) }) })) : null, _jsx(Summary, { at: at, said: said, held: files, deck: deck, onOpen: () => setPanel(true), onCreate: () => window.location.assign(site("/dev")) }), _jsx(PanelToggle, { open: panel, onToggle: () => setPanel(!panel) })] })] }), wide && aside !== null ? (_jsx(Beside, { children: _jsx(Panel, { side: deck, kinds: NONE, onHide: () => setPanel(false), label: "Tabs" }) })) : null, !wide && panel ? _jsx(Panel, { side: deck, kinds: NONE, sheet: true, onHide: () => setPanel(false), label: "Tabs" }) : null, isFree(model) ? (_jsxs(Text, { fontSize: "$2", color: "$soft", text: "center", maxW: MEASURE, mx: "auto", px: "$4", py: "$2", children: [freeCopy.notice, " ", _jsx(Anchor, { href: site("/legal/privacy"), children: freeCopy.noticeCta })] })) : null, _jsx(ChatSurface, { ...chat, messages: shown, send: guarded,
1230
1239
  // AN EMPTY ROOM CENTRES ITS COMPOSER. While nothing has been said the
1231
1240
  // opening (the heading and the starters) and the composer sit as one
1232
1241
  // group in the middle of the height, with or without the plan bars in
package/dist/Column.d.ts CHANGED
@@ -1,15 +1,19 @@
1
1
  import { type Host } from '@hanzo/build';
2
- import type { Mode } from './recent.js';
3
- export declare function Column({ host, mode, thread, onChat, onDev, go, drawer, }: {
2
+ import { type ReactNode } from 'react';
3
+ import { type Mode } from './recent.js';
4
+ export declare function Column({ host, mode, thread, go, onShut, seek, drawer, foot, }: {
4
5
  host: Host;
5
6
  mode: Mode;
6
7
  /** The Chat conversation the pane holds, or null. */
7
8
  thread: string | null;
8
- /** The switch: to Chat and to Dev, each where the reader left it. */
9
- onChat: () => void;
10
- onDev: () => void;
11
9
  /** Moves the app to an address of this page. */
12
10
  go: (address: string) => void;
13
- /** Drawn in the phone's drawer: no collapse, no edge, always open. */
11
+ /** Shut the sidebar: its edge pulled in past the floor. */
12
+ onShut: () => void;
13
+ /** Each new value asks for the search, focused (⌘K). */
14
+ seek?: number;
15
+ /** Drawn in the phone's drawer: no edge, cut as a floating pane. */
14
16
  drawer?: boolean;
17
+ /** What stands at its foot: the drawer's rail, laid flat. */
18
+ foot?: ReactNode;
15
19
  }): import("react").JSX.Element;
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.