@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/Rail.js ADDED
@@ -0,0 +1,85 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ // The app's rail: its products as marks down the window's left edge, in one
4
+ // order — Chat, Dev, Library, Scheduled tasks, Customize — then More, a menu of
5
+ // the rest, and the account at the foot. It is how the app moves between Chat
6
+ // and Dev; there is no switch anywhere else.
7
+ //
8
+ // Library, Scheduled tasks and Customize are Dev's own places (@hanzo/build's
9
+ // Artifacts, Automations and Customize), raised to the rail; each is lit while
10
+ // the reader stands on it, and Dev is lit for the rest of Dev.
11
+ //
12
+ // It stands on the frame's floor, never cut as a pane (ground.ts): the rail and
13
+ // the sidebar beside it are the window, and the content is the panel inset in
14
+ // it.
15
+ //
16
+ // The sidebar's toggle leads it (`toggle`) where the host has no bar of its own:
17
+ // a browser tab. A host that is a window draws the toggle in its bar beside Back
18
+ // and Forward (App.tsx `bar`), and its rail starts with Chat. On a phone the
19
+ // rail lies along the foot of the drawer (`flat`), the account at its end.
20
+ import { Boxes, Clock, Code2, Ellipsis, FolderGit2, LayoutTemplate, Library, ListTodo, MessageSquare, PanelLeft, Puzzle, Server, SquareTerminal, Users } from 'lucide-react';
21
+ import { XStack, YStack } from '@hanzo/gui';
22
+ import { route } from '@hanzo/build';
23
+ import { DropdownMenu } from '@hanzo/ui';
24
+ import { Me } from './Me.js';
25
+ import { app, TABS, WORKSPACE } from './lib/host.js';
26
+ /** The rail's width, px. */
27
+ export const RAIL = 48;
28
+ /** A mark, px: a 36px square on a 44px pitch. */
29
+ const MARK = 36;
30
+ /**
31
+ * Opens an address of another product in a tab of its own, through an anchor,
32
+ * so a host that sends links out of its window (the desktop's system browser)
33
+ * hears it as the link it is.
34
+ */
35
+ function out(href) {
36
+ const a = document.createElement('a');
37
+ a.href = href;
38
+ a.target = '_blank';
39
+ a.rel = 'noopener noreferrer';
40
+ document.body.appendChild(a);
41
+ a.click();
42
+ a.remove();
43
+ }
44
+ /** The places the rail raises out of Dev, by the builder address each opens. */
45
+ const RAISED = { library: '-/artifacts', scheduled: '-/automations', customize: '-/customize' };
46
+ /** Which raised place a builder address stands on, if any. */
47
+ function raised(at) {
48
+ if (at === null)
49
+ return null;
50
+ const r = route(at);
51
+ if (r.kind === 'screen' && r.screen === 'artifacts')
52
+ return 'library';
53
+ if ((r.kind === 'screen' && r.screen === 'automations') || r.kind === 'automation')
54
+ return 'scheduled';
55
+ if (r.kind === 'customize')
56
+ return 'customize';
57
+ return null;
58
+ }
59
+ /** One mark: an icon button named for where it goes, its name its tooltip, lit where the reader is. */
60
+ export function Mark({ label, on = false, onPress, children, ...rest }) {
61
+ return (_jsx(XStack, { render: "button", "aria-label": label, ...{ title: label }, "aria-current": on ? 'page' : undefined, onPress: onPress, width: MARK, height: MARK, rounded: "$4", items: "center", justify: "center", shrink: 0, cursor: "pointer", opacity: on ? 1 : 0.62, bg: on ? '$edge' : 'transparent', hoverStyle: { bg: on ? '$edge' : '$hover', opacity: 1 }, pressStyle: { bg: '$raised' }, focusVisibleStyle: { outlineWidth: 2, outlineStyle: 'solid', outlineColor: '$outlineColor' },
62
+ // A thumb's 44px on a touch screen.
63
+ $touchable: { width: 44, height: 44 }, ...rest, children: children }));
64
+ }
65
+ /** The sidebar's toggle: the same mark wherever a host draws it. */
66
+ export function Toggle({ open, onPress, label }) {
67
+ return (_jsx(Mark, { label: label ?? (open ? 'Close sidebar' : 'Open sidebar'), "data-slot": "sidebar-toggle", "aria-expanded": open, onPress: onPress, children: _jsx(PanelLeft, { size: 18, "aria-hidden": true }) }));
68
+ }
69
+ export function Rail({ host, mode, at, open, toggle, flat = false, onChat, onDev, go, onOrg, }) {
70
+ const here = mode === 'dev' ? raised(at) : null;
71
+ const to = (p) => () => go(app(p));
72
+ // Everything else the app reaches, behind More: Dev's other places, then the products on hosts of their own.
73
+ const rest = [
74
+ { key: 'projects', label: 'Projects', icon: _jsx(FolderGit2, { size: 16, "aria-hidden": true }), onSelect: to('-/projects') },
75
+ { key: 'templates', label: 'Templates', icon: _jsx(LayoutTemplate, { size: 16, "aria-hidden": true }), onSelect: to('-/templates') },
76
+ { key: 'issues', label: 'Issues', icon: _jsx(ListTodo, { size: 16, "aria-hidden": true }), onSelect: to('-/issues') },
77
+ { key: 'machines', label: 'Machines', icon: _jsx(Server, { size: 16, "aria-hidden": true }), onSelect: to('-/settings/machines') },
78
+ { key: 'environments', label: 'Environments', icon: _jsx(Boxes, { size: 16, "aria-hidden": true }), onSelect: to('-/settings/environments') },
79
+ { type: 'separator', key: 'away' },
80
+ { key: 'tabs', label: 'Tabs', icon: _jsx(SquareTerminal, { size: 16, "aria-hidden": true }), onSelect: () => out(TABS) },
81
+ { key: 'team', label: 'Team', icon: _jsx(Users, { size: 16, "aria-hidden": true }), onSelect: () => out(WORKSPACE) },
82
+ ];
83
+ const Stack = flat ? XStack : YStack;
84
+ return (_jsxs(Stack, { "data-slot": "rail", render: "nav", "aria-label": "Products", ...(flat ? { pt: '$2', mt: '$2', borderTopWidth: 1, borderColor: '$borderColor', gap: '$1' } : { width: RAIL, flex: 1, pt: '$2', pb: '$2', gap: 8 }), shrink: 0, items: "center", children: [toggle ? (_jsx(YStack, { pb: 8, children: _jsx(Toggle, { open: open, onPress: toggle }) })) : null, _jsx(Mark, { label: "Chat", on: mode === 'chat', onPress: onChat, "data-slot": "rail-chat", children: _jsx(MessageSquare, { size: 18, "aria-hidden": true }) }), _jsx(Mark, { label: "Dev", on: mode === 'dev' && !here, onPress: onDev, "data-slot": "rail-dev", children: _jsx(Code2, { size: 18, "aria-hidden": true }) }), _jsx(Mark, { label: "Library", on: here === 'library', onPress: to(RAISED.library), "data-slot": "rail-library", children: _jsx(Library, { size: 18, "aria-hidden": true }) }), _jsx(Mark, { label: "Scheduled tasks", on: here === 'scheduled', onPress: to(RAISED.scheduled), "data-slot": "rail-scheduled", children: _jsx(Clock, { size: 18, "aria-hidden": true }) }), _jsx(Mark, { label: "Customize", on: here === 'customize', onPress: to(RAISED.customize), "data-slot": "rail-customize", children: _jsx(Puzzle, { size: 18, "aria-hidden": true }) }), _jsx(DropdownMenu, { placement: flat ? 'top-start' : 'right-start', minWidth: 220, trigger: _jsx(XStack, { render: "button", "aria-label": "More", ...{ title: 'More' }, "data-slot": "rail-more", width: MARK, height: MARK, rounded: "$4", items: "center", justify: "center", cursor: "pointer", opacity: 0.62, hoverStyle: { bg: '$hover', opacity: 1 }, pressStyle: { bg: '$raised' }, focusVisibleStyle: { outlineWidth: 2, outlineStyle: 'solid', outlineColor: '$outlineColor' }, $touchable: { width: 44, height: 44 }, children: _jsx(Ellipsis, { size: 18, "aria-hidden": true }) }), items: rest }), _jsx(YStack, { flex: 1 }), _jsx(Me, { host: host, narrow: true, above: flat, onOrg: onOrg })] }));
85
+ }
package/dist/Recents.js CHANGED
@@ -9,12 +9,12 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
9
9
  // and Chat's Status shows the archived ones apart.
10
10
  import { useEffect, useId, useMemo, useRef } from 'react';
11
11
  import { ScrollView, SizableText, XStack, YStack } from '@hanzo/gui';
12
- import { ListFilter, MessageSquare } from 'lucide-react';
12
+ import { ListFilter } from 'lucide-react';
13
13
  import { DOTS, route, useSessions } from '@hanzo/build';
14
14
  import { DropdownMenu } from '@hanzo/ui';
15
15
  import { SidebarItem, StatusDot } from '@hanzo/ui/chat';
16
16
  import { useKept } from './kept.js';
17
- import { ago, arrange, chats, MENUS, PLAIN, runs, SINCE, viewOf } from './recent.js';
17
+ import { arrange, chats, MENUS, PINNED, PLAIN, runs, SINCE, viewOf } from './recent.js';
18
18
  import { ConversationMenu, ThreadRow, useThreadList } from './thread.js';
19
19
  /** The menu's items: each section a heading and its options, the one in force checked. */
20
20
  function items(mode, view, set) {
@@ -68,23 +68,35 @@ function List({ mode, rows, words, view, keep, open, reading, error, onOpen, thr
68
68
  const label = useId();
69
69
  const groups = arrange(rows, view, words);
70
70
  const narrowed = view.status !== PLAIN.status || view.since !== PLAIN.since;
71
- const now = new Date();
72
71
  const running = mode === 'dev' ? rows.filter((r) => r.status === 'running').length : 0;
72
+ // A row is its title alone, as the reference draws it: a conversation's ⋯
73
+ // shows on hover and focus, a run's state is a dot at its end.
73
74
  const row = (r) => {
74
- const when = ago(r.at, now);
75
75
  const t = threads?.get(r.id);
76
- const item = (_jsx(SidebarItem, { "data-kind": r.kind, active: open(r), rounded: "$4", minH: 32, pr: when || t ? '$8' : '$2', ...(t ? { $touchable: { pr: 52 } } : null), onKeyDown: (e) => {
76
+ const item = (_jsx(SidebarItem, { "data-kind": r.kind, active: open(r), rounded: "$4", minH: 31, px: "$2", pr: t || r.kind === 'dev' ? '$7' : '$2', ...(t ? { $touchable: { pr: 52 } } : null), onKeyDown: (e) => {
77
77
  if (e.key !== 'Enter' && e.key !== ' ')
78
78
  return;
79
79
  e.preventDefault?.();
80
80
  onOpen(r);
81
- }, icon: r.kind === 'chat' ? (_jsx(MessageSquare, { size: 14, "aria-hidden": true })) : (_jsx(XStack, { width: 14, justify: "center", children: _jsx(StatusDot, { status: DOTS[r.status] ?? 'idle' }) })), onPress: () => onOpen(r), children: r.title }));
82
- // How long ago, at the row's end; the full time is its title.
83
- const age = when ? (_jsx(SizableText, { size: "$1", color: "$soft", pointerEvents: "none", display: "flex", items: "center", fontVariant: ['tabular-nums'], children: _jsx("time", { dateTime: r.at, title: new Date(r.at).toLocaleString(), children: when }) })) : null;
81
+ }, onPress: () => onOpen(r), children: _jsx(SizableText, { size: "$3", color: open(r) ? '$ink' : '$quiet', numberOfLines: 1, display: "block", children: r.title }) }));
84
82
  if (t)
85
- return (_jsx(ThreadRow, { t: t, active: open(r), aside: age, children: item }, r.id));
86
- return (_jsxs(YStack, { role: "listitem", position: "relative", children: [item, age ? (_jsx(XStack, { position: "absolute", r: "$2", t: 0, b: 0, items: "center", pointerEvents: "none", children: age })) : null] }, r.id));
83
+ return (_jsx(ThreadRow, { t: t, active: open(r), children: item }, r.id));
84
+ return (_jsxs(YStack, { role: "listitem", position: "relative", children: [item, r.kind === 'dev' ? (_jsx(XStack, { position: "absolute", r: "$2.5", t: 0, b: 0, items: "center", pointerEvents: "none", children: _jsx(StatusDot, { status: DOTS[r.status] ?? 'idle' }) })) : null] }, r.id));
87
85
  };
88
- const body = groups.length ? (_jsx(YStack, { role: "list", "aria-labelledby": label, gap: 1, children: groups.map((g) => g.title ? (_jsxs(YStack, { role: "listitem", gap: 1, pt: "$2", children: [_jsx(SizableText, { size: "$1", color: "$soft", px: "$2", pb: "$1", children: g.title }), _jsx(YStack, { role: "list", "aria-label": g.title, gap: 1, children: g.rows.map(row) })] }, g.title)) : (g.rows.map(row))) })) : reading ? (_jsx(Note, { children: mode === 'chat' ? 'Reading your chats…' : 'Reading your runs…' })) : error ? null : words.trim() ? (_jsx(Note, { children: `Nothing matches “${words.trim()}”.` })) : view.status === 'archived' ? (_jsx(Note, { children: "No archived chats." })) : narrowed ? (_jsx(Note, { children: "Nothing in this view." })) : (_jsx(Note, { children: mode === 'chat' ? 'No chats yet. Say something and it lands here.' : 'No runs yet.' }));
89
- return (_jsxs(YStack, { "data-slot": "recents", flex: 1, minH: 0, mt: "$2", children: [_jsxs(XStack, { items: "center", justify: "space-between", height: 30, pl: "$2", pr: "$1", shrink: 0, children: [_jsxs(XStack, { items: "center", gap: "$2", children: [_jsx(SizableText, { id: label, size: "$1", color: "$soft", fontWeight: "600", textTransform: "uppercase", letterSpacing: 0.6, children: "Recents" }), running ? (_jsx(XStack, { "aria-label": `${running} running`, minW: 18, height: 18, px: "$1.5", rounded: 999, items: "center", justify: "center", bg: "$edge", borderWidth: 1, borderColor: "$borderColor", children: _jsx(SizableText, { size: "$1", fontWeight: "600", color: "$ink", "aria-hidden": true, children: String(running) }) })) : null] }), _jsx(DropdownMenu, { minWidth: 200, trigger: _jsx(XStack, { render: "button", "aria-label": "Recents view", width: 26, height: 26, rounded: "$2", items: "center", justify: "center", cursor: "pointer", hoverStyle: { bg: '$hover' }, bg: narrowed ? '$hover' : 'transparent', children: _jsx(ListFilter, { size: 14, "aria-hidden": true }) }), items: items(mode, view, keep) })] }), _jsxs(ScrollView, { flex: 1, showsVerticalScrollIndicator: false, children: [error ? _jsx(Note, { children: error.message }) : null, threads ? (_jsx(ConversationMenu, { threads: threads, active: held, onGone: onNew, children: _jsx(YStack, { children: body }) })) : (body)] })] }));
86
+ // The view menu stands at the end of the Recents heading: the target's own, quiet until the pointer is near.
87
+ const menu = (_jsx(DropdownMenu, { minWidth: 200, trigger: _jsx(XStack, { render: "button", "aria-label": "Recents view", width: 26, height: 26, rounded: "$2", items: "center", justify: "center", cursor: "pointer", opacity: narrowed ? 1 : 0, "$group-hover": { opacity: 1 }, focusStyle: { opacity: 1 }, $touchable: { opacity: 1 }, hoverStyle: { bg: '$hover' }, bg: narrowed ? '$hover' : 'transparent', children: _jsx(ListFilter, { size: 14, "aria-hidden": true }) }), items: items(mode, view, keep) }));
88
+ /** A section's heading: the reference's quiet 14px label, the Recents one carrying the count and the menu. */
89
+ const heading = (title, id, lead = false) => (_jsxs(XStack, { group: true, items: "center", justify: "space-between", height: 31, pl: "$2", pr: "$1", mt: "$3", shrink: 0, children: [_jsxs(XStack, { items: "center", gap: "$2", children: [_jsx(SizableText, { id: id, size: "$3", color: "$soft", children: title }), lead && running ? (_jsx(XStack, { "aria-label": `${running} running`, minW: 18, height: 18, px: "$1.5", rounded: 999, items: "center", justify: "center", bg: "$edge", borderWidth: 1, borderColor: "$borderColor", children: _jsx(SizableText, { size: "$1", fontWeight: "600", color: "$ink", "aria-hidden": true, children: String(running) }) })) : null] }), lead ? menu : null] }));
90
+ const loose = groups.filter((g) => g.title !== PINNED);
91
+ const pinned = groups.find((g) => g.title === PINNED);
92
+ const note = reading ? (_jsx(Note, { children: mode === 'chat' ? 'Reading your chats…' : 'Reading your runs…' })) : error ? null : words.trim() ? (_jsx(Note, { children: `Nothing matches “${words.trim()}”.` })) : view.status === 'archived' ? (_jsx(Note, { children: "No archived chats." })) : narrowed ? (_jsx(Note, { children: "Nothing in this view." })) : (_jsx(Note, { children: mode === 'chat' ? 'No chats yet.' : 'No runs yet.' }));
93
+ // Each list of conversations holds its own menu (thread.tsx): the view menu
94
+ // in the Recents heading between them is a menu of its own, never inside one.
95
+ const menus = (rows) => threads ? (_jsx(ConversationMenu, { threads: threads, active: held, onGone: onNew, children: _jsx(YStack, { children: rows }) })) : (rows);
96
+ const rest = loose.length
97
+ ? loose.map((g) => g.title ? (_jsxs(YStack, { gap: 1, pt: "$1", children: [_jsx(SizableText, { size: "$1", color: "$soft", px: "$2", pb: "$1", children: g.title }), _jsx(YStack, { role: "list", "aria-label": g.title, gap: 1, children: g.rows.map(row) })] }, g.title)) : (_jsx(YStack, { role: "list", "aria-label": "All", gap: 1, children: g.rows.map(row) }, "all")))
98
+ : pinned
99
+ ? null
100
+ : note;
101
+ return (_jsx(YStack, { "data-slot": "recents", flex: 1, minH: 0, children: _jsxs(ScrollView, { flex: 1, showsVerticalScrollIndicator: false, children: [error ? _jsx(Note, { children: error.message }) : null, _jsxs(YStack, { role: "list", "aria-labelledby": label, gap: 1, children: [pinned ? (_jsxs(YStack, { role: "listitem", gap: 1, children: [heading(PINNED), menus(_jsx(YStack, { role: "list", "aria-label": PINNED, gap: 1, children: pinned.rows.map(row) }))] })) : null, _jsxs(YStack, { role: "listitem", gap: 1, children: [heading('Recents', label, true), loose.length ? menus(rest) : rest] })] })] }) }));
90
102
  }
@@ -38,3 +38,26 @@ export declare function useKinds({ at, said, held, deck }: {
38
38
  held: Held[];
39
39
  deck: Deck;
40
40
  }): Kind[];
41
+ /**
42
+ * The conversation's summary, from the outline mark in its header: what it
43
+ * produced (Outputs, led by Create file or site, which starts a Dev run) and
44
+ * what went into it (Sources, with the `+` that brings more). Each row opens in
45
+ * a tab beside the conversation. A section with nothing in it keeps its heading
46
+ * and one quiet line.
47
+ */
48
+ export declare function Summary({ at, said, held, deck, onOpen, onCreate }: {
49
+ at: string;
50
+ said: ChatMessage[];
51
+ held: Held[];
52
+ deck: Deck;
53
+ onOpen: () => void;
54
+ onCreate: () => void;
55
+ }): import("react").JSX.Element;
56
+ /**
57
+ * The organization's sites, as an empty tab suggests them: `GET /v1/projects`,
58
+ * every row with a live address. Read once a conversation; nothing on a refusal.
59
+ */
60
+ export declare function useSites(): {
61
+ title: string;
62
+ url: string;
63
+ }[];
package/dist/RightPane.js CHANGED
@@ -16,26 +16,22 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
16
16
  // The panel's tabs, their order and the open one are kept per conversation by
17
17
  // @hanzo/build (`useDeck`, scope `chat:<channel>`); the frame owns the column
18
18
  // it stands in (`Beside` in `Shell.tsx`).
19
- import { useMemo, useState } from 'react';
20
- import { FileCode, Image as ImageMark, Paperclip, Plus, Search } from 'lucide-react';
21
- import { Box, DropdownMenu, DropdownMenuContent, DropdownMenuCheckboxItem, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, Text, YStack, } from '@hanzo/ui';
19
+ import { useEffect, useMemo, useState } from 'react';
20
+ import { FileCode, Globe, Image as ImageMark, ListTodo, Paperclip, Plus, Search } from 'lucide-react';
21
+ import { Box, DropdownMenu, DropdownMenuContent, DropdownMenuCheckboxItem, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, Popover, PopoverContent, PopoverTrigger, Text, XStack, YStack, } from '@hanzo/ui';
22
22
  import { useAi } from '@hanzo/ai/react';
23
23
  import { artifacts, name as titleOf, PAGE } from '@hanzo/build';
24
24
  import { Card, Quiet, Row } from './card.js';
25
25
  import { said as told } from './lib/attach.js';
26
26
  import { download, fileOf } from './lib/files.js';
27
- import { org } from './lib/session.js';
27
+ import { org, scope } from './lib/session.js';
28
+ import { api } from './lib/api.js';
29
+ import { list } from './lib/list.js';
28
30
  import { localOf, take } from './pane.js';
29
31
  import { useProviders, useSkills } from './tools.js';
30
32
  import { activate, startDevice } from './connect.js';
31
33
  /** A conversation's panel before anyone has arranged it. */
32
- export const SEED = {
33
- tabs: [
34
- { id: 'artifacts', kind: 'artifacts', title: 'Artifacts' },
35
- { id: 'sources', kind: 'sources', title: 'Sources' },
36
- ],
37
- at: 'artifacts',
38
- };
34
+ export const SEED = { tabs: [], at: null };
39
35
  /** The text of a turn, whichever shape the wire used. */
40
36
  const words = (m) => typeof m.content === 'string'
41
37
  ? m.content
@@ -222,3 +218,59 @@ function Services() {
222
218
  .catch((e) => setStep(String(e?.message ?? 'Could not start')));
223
219
  }, children: _jsx(Word, { children: p.name }) }, p.id)))] }));
224
220
  }
221
+ /** A row of the summary: its mark and its name, one press from a tab. */
222
+ function Line({ icon, children, onPress, quiet = false }) {
223
+ return (_jsxs(XStack, { render: onPress ? 'button' : undefined, role: onPress ? 'button' : undefined, onPress: onPress, items: "center", gap: "$2.5", px: "$2", minH: 30, width: "100%", rounded: "$3", cursor: onPress ? 'pointer' : undefined, hoverStyle: onPress ? { bg: '$hover' } : undefined, focusVisibleStyle: { outlineWidth: 2, outlineStyle: 'solid', outlineColor: '$outlineColor' }, children: [icon ? (_jsx(XStack, { width: 16, justify: "center", shrink: 0, opacity: 0.75, children: icon })) : null, _jsx(Text, { flex: 1, minW: 0, fontSize: "$2", color: quiet ? '$soft' : '$ink', numberOfLines: 1, children: children })] }));
224
+ }
225
+ /** A heading of the summary, with what it adds at its end. */
226
+ const Head = ({ children, action }) => (_jsxs(XStack, { items: "center", justify: "space-between", px: "$2", pt: "$1.5", pb: "$1", minH: 28, children: [_jsx(Text, { fontSize: "$2", color: "$soft", children: children }), action] }));
227
+ /**
228
+ * The conversation's summary, from the outline mark in its header: what it
229
+ * produced (Outputs, led by Create file or site, which starts a Dev run) and
230
+ * what went into it (Sources, with the `+` that brings more). Each row opens in
231
+ * a tab beside the conversation. A section with nothing in it keeps its heading
232
+ * and one quiet line.
233
+ */
234
+ export function Summary({ at, said, held, deck, onOpen, onCreate }) {
235
+ const ai = useAi();
236
+ const [open, setOpen] = useState(false);
237
+ const made = useMemo(() => outputs(said), [said]);
238
+ const took = useMemo(() => sources(said, held), [said, held]);
239
+ const done = useMemo(() => lookups(said), [said]);
240
+ const show = (m) => {
241
+ setOpen(false);
242
+ void look(deck, m, ai);
243
+ onOpen();
244
+ };
245
+ return (_jsxs(Popover, { open: open, onOpenChange: setOpen, allowFlip: true, children: [_jsx(PopoverTrigger, { children: _jsx(XStack, { render: "button", "data-slot": "summary-toggle", "aria-label": "Summary", "aria-haspopup": "dialog", "aria-expanded": open, ...{ title: 'Summary' }, width: 32, height: 32, rounded: "$3", items: "center", justify: "center", cursor: "pointer", bg: open ? '$hover' : 'transparent', hoverStyle: { bg: '$hover' }, focusVisibleStyle: { outlineWidth: 2, outlineStyle: 'solid', outlineColor: '$outlineColor' }, children: _jsx(ListTodo, { size: 16, "aria-hidden": true }) }) }), _jsx(PopoverContent, { solid: true, align: "end", p: 6, width: 300, children: _jsxs(YStack, { "data-slot": "summary", role: "dialog", "aria-label": "Summary", gap: 1, self: "stretch", width: "100%", maxH: "min(70dvh, 520px)", overflowY: "auto", overflowX: "hidden", children: [_jsx(Head, { children: "Outputs" }), _jsx(Line, { quiet: true, onPress: () => {
246
+ setOpen(false);
247
+ onCreate();
248
+ }, children: "Create file or site" }), made.map((m) => {
249
+ const Glyph = GLYPH[m.kind];
250
+ return (_jsx(Line, { icon: _jsx(Glyph, { size: 15, "aria-hidden": true }), onPress: () => show(m), children: m.name }, m.id));
251
+ }), _jsx(XStack, { height: 1, bg: "$borderColor", my: "$1.5", mx: "$1" }), _jsx(Head, { action: _jsx(Add, { at: at }), children: "Sources" }), done.map((call) => (_jsx(Line, { icon: _jsx(Globe, { size: 15, "aria-hidden": true }), onPress: () => show({ id: call.id, name: call.name, kind: 'file', body: [call.asked, call.got ?? ''].filter(Boolean).join('\n\n'), mime: 'text/plain' }), children: call.name === 'web_search' || call.name === 'search' ? 'Web search' : call.name }, call.id))), took.map((m) => {
252
+ const Glyph = GLYPH[m.kind];
253
+ return (_jsx(Line, { icon: _jsx(Glyph, { size: 15, "aria-hidden": true }), onPress: () => show(m), children: m.name }, m.id));
254
+ }), !done.length && !took.length ? _jsx(Line, { quiet: true, children: "Nothing yet" }) : null] }) })] }));
255
+ }
256
+ /**
257
+ * The organization's sites, as an empty tab suggests them: `GET /v1/projects`,
258
+ * every row with a live address. Read once a conversation; nothing on a refusal.
259
+ */
260
+ export function useSites() {
261
+ const [rows, setRows] = useState([]);
262
+ useEffect(() => {
263
+ const stop = new AbortController();
264
+ fetch(`${api()}/v1/projects`, { headers: scope(), signal: stop.signal })
265
+ .then((r) => (r.ok ? r.json() : null))
266
+ .then((body) => {
267
+ if (stop.signal.aborted)
268
+ return;
269
+ const all = list(Array.isArray(body) ? body : body?.data);
270
+ setRows(all.filter((p) => typeof p.liveUrl === 'string' && /^https:\/\//.test(p.liveUrl)).map((p) => ({ title: p.name || p.slug || 'Site', url: p.liveUrl })));
271
+ })
272
+ .catch(() => { });
273
+ return () => stop.abort();
274
+ }, []);
275
+ return rows;
276
+ }
package/dist/Share.js CHANGED
@@ -10,7 +10,7 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
10
10
  // them, and revokes a link, which ends it for all of them. Recipients read and
11
11
  // do not reply: a conversation belongs to the member who started it.
12
12
  import { useCallback, useEffect, useId, useRef, useState } from 'react';
13
- import { Check, Link2, Share2 } from 'lucide-react';
13
+ import { Check, Link2, Upload } from 'lucide-react';
14
14
  import { Box, Button, Dialog, DialogContent, DialogTitle, Text, XStack, YStack } from '@hanzo/ui';
15
15
  import { hold, heldToken, link, Refused, shares } from './lib/share.js';
16
16
  import { BAD } from './lib/mix.js';
@@ -29,7 +29,7 @@ export function Share({ thread }) {
29
29
  setOpen(false);
30
30
  button.current?.focus();
31
31
  }, []);
32
- return (_jsxs(Box, { position: "relative", children: [_jsx(Box, { ref: button, render: "button", onClick: () => setOpen((o) => !o), "aria-haspopup": "dialog", "aria-expanded": open, "aria-label": "Share this conversation", borderWidth: 0, bg: "transparent", p: "$1", rounded: "$2", hoverStyle: { bg: '$hover' }, children: _jsxs(XStack, { items: "center", gap: "$1", children: [_jsx(Share2, { size: 13, "aria-hidden": true }), _jsx(Text, { fontSize: "$2", color: "$ink", children: "Share" })] }) }), open ? _jsx(Panel, { thread: thread, onClose: close }) : null] }));
32
+ return (_jsxs(Box, { position: "relative", children: [_jsxs(Box, { ref: button, render: "button", onClick: () => setOpen((o) => !o), "aria-haspopup": "dialog", "aria-expanded": open, "aria-label": "Share this conversation", "data-slot": "share", display: "flex", flexDirection: "row", gap: "$1.5", height: 32, px: "$2.5", items: "center", justify: "center", borderWidth: 0, bg: open ? '$hover' : 'transparent', rounded: "$3", cursor: "pointer", hoverStyle: { bg: '$hover' }, children: [_jsx(Upload, { size: 16, "aria-hidden": true }), _jsx(Text, { fontSize: "$3", color: "$ink", children: "Share" })] }), open ? _jsx(Panel, { thread: thread, onClose: close }) : null] }));
33
33
  }
34
34
  /** The header's panel: hung under its button, shut by Escape or a press outside, focus starting inside. */
35
35
  function Panel({ thread, onClose }) {
package/dist/Shell.js CHANGED
@@ -75,7 +75,6 @@ import { Profile } from "./Profile.js";
75
75
  import { SupportPicker } from "./Support.js";
76
76
  import { Find } from "./Find.js";
77
77
  import { Account } from "./Account.js";
78
- import { ModeMark } from "./ModeMark.js";
79
78
  import { called } from "./team.js";
80
79
  import { checkoutUrl } from './lib/pay.js';
81
80
  /** Tabs: the terminals, at their own host. */
@@ -116,7 +115,7 @@ const CONTROL = 28;
116
115
  import { getStoredPeople, getStoredAgents, getCustomThreads, createDirectChat, } from "./contacts-store.js";
117
116
  import { bare } from './lib/bare.js';
118
117
  import { BAD } from './lib/mix.js';
119
- import { brand, site, where } from './where.js';
118
+ import { brand, site, where, wordmark } from './where.js';
120
119
  /**
121
120
  * The destinations, in rail order.
122
121
  *
@@ -570,13 +569,13 @@ function Team({ filter = "", onPick, navigate, }) {
570
569
  // this app climbs (see Home's `Waiting`). "Reading." never resolves for a
571
570
  // reader nobody is going to answer, so the session is asked first.
572
571
  if (!signedIn)
573
- return _jsx(Note, { children: "Sign in to see your org's rooms." });
572
+ return _jsx(Note, { children: "Sign in to see your org's channels." });
574
573
  if (wrong)
575
- return _jsx(Note, { children: say(wrong, "your rooms") });
574
+ return _jsx(Note, { children: say(wrong, "your channels") });
576
575
  if (rooms === null)
577
- return _jsx(Note, { children: "Reading your rooms." });
576
+ return _jsx(Note, { children: "Reading your channels." });
578
577
  if (rooms.length === 0)
579
- return _jsx(Note, { children: "No rooms yet. Open one and the conversation has somewhere to live." });
578
+ return _jsx(Note, { children: "No channels yet." });
580
579
  if (shown.length === 0)
581
580
  return _jsxs(Note, { children: ["No room matches \u201C", filter, "\u201D."] });
582
581
  return (_jsxs(_Fragment, { children: [favourites.length > 0 ? (_jsx(SidebarFolder, { name: "Favorites", defaultOpen: true, children: favourites.map((one) => (_jsx(SidebarItem, { active: teamKey(one) === room, icon: _jsx(Star, { size: 13, "aria-hidden": true }), onPress: () => {
@@ -1181,7 +1180,7 @@ function Frame({ mode, children, navigate, back, forward, }) {
1181
1180
  // Drive names the company rather than the room.
1182
1181
  id: place.id,
1183
1182
  title: place.label,
1184
- category: "Rooms",
1183
+ category: "Apps",
1185
1184
  icon: _jsx(place.icon, { size: 15, "aria-hidden": true }),
1186
1185
  href: place.route,
1187
1186
  })),
@@ -1210,7 +1209,7 @@ function Frame({ mode, children, navigate, back, forward, }) {
1210
1209
  const list = (_jsxs(Sidebar, { ref: side.ref,
1211
1210
  // A phone's sheet is never wider than 280, whatever the laptop kept.
1212
1211
  // inline-style: `--span`, the width a drag rewrites on the element itself (span.ts); no prop sets a variable.
1213
- style: side.style, $md: { width: "var(--span)" }, "$max-md": { width: "100%", maxW: 280 }, ...sheet(0), borderRightWidth: 1, borderColor: "var(--edge, var(--white-06))", position: "relative", children: [_jsx(Grip, { side: "left", span: side.span, floor: FLOOR, ceil: CEIL, reset: SIDEBAR_WIDTH, onSpan: side.move, onKeep: side.keep, onShut: () => setAsked(false), label: "Resize sidebar" }), _jsx(XStack, { "data-slot": "mode-switch", items: "center", px: "$2", pt: "$2", minW: 0, children: _jsx(ModeMark, { mode: mode === "dev" ? "dev" : "chat", onChat: () => navigate?.("/chat"), onDev: () => navigate?.(dev()) }) }), _jsxs(XStack, { "data-slot": "workspace-switcher", items: "center", gap: "$2", px: "$2", pt: "$1", pb: "$1", minW: 0, children: [_jsx(Text, { render: _jsx("a", { href: "/", onClick: (e) => {
1212
+ style: side.style, $md: { width: "var(--span)" }, "$max-md": { width: "100%", maxW: 280 }, ...sheet(0), borderRightWidth: 1, borderColor: "var(--edge, var(--white-06))", position: "relative", children: [_jsx(Grip, { side: "left", span: side.span, floor: FLOOR, ceil: CEIL, reset: SIDEBAR_WIDTH, onSpan: side.move, onKeep: side.keep, onShut: () => setAsked(false), label: "Resize sidebar" }), _jsx(XStack, { "data-slot": "mode-name", items: "center", px: "$2", pt: "$2", minW: 0, children: _jsx(Text, { fontSize: "$4", fontWeight: "600", color: "$ink", numberOfLines: 1, children: wordmark(mode === "dev" ? "dev" : "chat") }) }), _jsxs(XStack, { "data-slot": "workspace-switcher", items: "center", gap: "$2", px: "$2", pt: "$1", pb: "$1", minW: 0, children: [_jsx(Text, { render: _jsx("a", { href: "/", onClick: (e) => {
1214
1213
  if (!navigate || e.metaKey || e.ctrlKey || e.shiftKey || e.button !== 0)
1215
1214
  return;
1216
1215
  e.preventDefault();
package/dist/Talk.js CHANGED
@@ -43,7 +43,7 @@ function when(ms) {
43
43
  * an empty header. The sigil is how a client DRAWS a room and is not stored, so
44
44
  * it is added here and only where there is a name to put it on.
45
45
  */
46
- const titleOf = (room) => room.name ? `#${room.name}` : room.direct ? 'Direct message' : 'Room';
46
+ const titleOf = (room) => room.name ? `#${room.name}` : room.direct ? 'Direct message' : 'Channel';
47
47
  /** Adds `@name ` to a draft, after a space where the draft needs one. */
48
48
  const mention = (draft, name) => `${draft && !/\s$/.test(draft) ? `${draft} ` : draft}@${name} `;
49
49
  export function Talk({ room }) {
@@ -70,14 +70,14 @@ export function Talk({ room }) {
70
70
  }
71
71
  catch (e) {
72
72
  setDraft(text);
73
- setRefused(say(e, 'this room', 'save'));
73
+ setRefused(say(e, 'this channel', 'save'));
74
74
  }
75
75
  finally {
76
76
  setPending((held) => held.filter((one) => one.key !== key));
77
77
  }
78
78
  };
79
79
  const name = (of) => setDraft((held) => mention(held, of));
80
- return (_jsxs(YStack, { flex: 1, minH: 0, children: [_jsxs(ChannelHeader, { name: titleOf(room), children: [room.topic ? (_jsx(Text, { fontSize: "$2", color: "$soft", numberOfLines: 1, children: room.topic })) : null, _jsx(Box, { render: "button", "aria-label": aside === 'roster' ? 'Hide directory & roster' : 'Show directory & roster', "aria-pressed": aside === 'roster', onClick: () => showAside(aside === 'roster' ? null : 'roster'), ml: "auto", p: "$2", rounded: "$3", hoverStyle: { bg: '$hover' }, children: _jsx(Users, { size: 16, "aria-hidden": true }) })] }), _jsxs(YStack, { flex: 1, minH: 0, overflowY: "auto", overflowX: "hidden", p: "$4", gap: "$3", children: [failed ? (_jsx(Text, { fontSize: "$3", color: "$soft", children: say(failed, 'this room') })) : messages === null ? (_jsx(Text, { fontSize: "$3", color: "$soft", children: "Reading the room." })) : messages.length === 0 && pending.length === 0 ? (_jsx(Text, { fontSize: "$3", color: "$soft", children: "Nothing has been said here yet." })) : (messages.map((one) => (_jsx(Line, { member: memberOf(members, one.author), fallback: one.author, stamp: when(one.createdOn), text: one.text }, one.id)))), pending
80
+ return (_jsxs(YStack, { flex: 1, minH: 0, children: [_jsxs(ChannelHeader, { name: titleOf(room), children: [room.topic ? (_jsx(Text, { fontSize: "$2", color: "$soft", numberOfLines: 1, children: room.topic })) : null, _jsx(Box, { render: "button", "aria-label": aside === 'roster' ? 'Hide directory & roster' : 'Show directory & roster', "aria-pressed": aside === 'roster', onClick: () => showAside(aside === 'roster' ? null : 'roster'), ml: "auto", p: "$2", rounded: "$3", hoverStyle: { bg: '$hover' }, children: _jsx(Users, { size: 16, "aria-hidden": true }) })] }), _jsxs(YStack, { flex: 1, minH: 0, overflowY: "auto", overflowX: "hidden", p: "$4", gap: "$3", children: [failed ? (_jsx(Text, { fontSize: "$3", color: "$soft", children: say(failed, 'this channel') })) : messages === null ? (_jsx(Text, { fontSize: "$3", color: "$soft", children: "Reading the room." })) : messages.length === 0 && pending.length === 0 ? (_jsx(Text, { fontSize: "$3", color: "$soft", children: "Nothing has been said here yet." })) : (messages.map((one) => (_jsx(Line, { member: memberOf(members, one.author), fallback: one.author, stamp: when(one.createdOn), text: one.text }, one.id)))), pending
81
81
  .filter((one) => !messages?.some((said) => said.author === me && said.text === one.text))
82
82
  .map((one) => (_jsx(Line, { member: mine, fallback: "You", text: one.text, soft: true }, `pending-${one.key}`)))] }), _jsxs(YStack, { p: "$3", gap: "$2", borderTopWidth: 1, borderColor: "$borderColor", children: [_jsx(Composer, { value: draft, onChange: setDraft, onSend: () => void speak(), placeholder: `Message ${titleOf(room)}`, label: `Message ${titleOf(room)}`, hint: agents.length ? 'Mention an agent with @name and it answers.' : undefined, children: agents.length ? (_jsx(XStack, { gap: "$1", flexWrap: "wrap", shrink: 1, minW: 0, children: agents.map((one) => (_jsx(Box, { render: "button", "aria-label": `Mention ${one.name}`, onClick: () => name(one.name), px: "$2", py: "$1", rounded: "$10", hoverStyle: { bg: '$hover' }, children: _jsxs(Text, { fontSize: "$2", color: "$soft", children: ["@", one.name] }) }, one.id))) })) : null }), refused ? (_jsx(Text, { fontSize: "$2", color: BAD, children: refused })) : null] }), _jsx(Beside, { children: aside === 'roster' ? _jsx(Roster, { members: members, wrong: wrong, onMention: name }) : null })] }));
83
83
  }
package/dist/recent.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // The Recents list as data: each mode's rows, the views a menu offers, and the
2
2
  // list a view makes of them. No React, so a spec reads it as it is.
3
- export const PLAIN = { status: 'all', since: 'any', group: 'date', sort: 'activity' };
3
+ export const PLAIN = { status: 'all', since: 'any', group: 'none', sort: 'activity' };
4
4
  const STATUSES = [
5
5
  { id: 'all', label: 'All' },
6
6
  { id: 'running', label: 'Running' },
package/dist/rooms.js CHANGED
@@ -87,7 +87,7 @@ export function useTeamRooms() {
87
87
  }, [client, user, now]);
88
88
  const open = useCallback(async (room) => {
89
89
  if (!client)
90
- throw Object.assign(new Error('Sign in to open a room'), { status: 401 });
90
+ throw Object.assign(new Error('Sign in to open a channel'), { status: 401 });
91
91
  const made = await client.http.json({
92
92
  method: 'POST',
93
93
  path: '/v1/team/rooms',
@@ -180,7 +180,7 @@ export function useRoomMessages(room, space) {
180
180
  /** Says it and answers the stored row, or throws with the platform's status. */
181
181
  const send = useCallback(async (text) => {
182
182
  if (!client || !room || !space) {
183
- throw Object.assign(new Error('This room has no address to send to'), { status: 503 });
183
+ throw Object.assign(new Error('This channel has no address to send to'), { status: 503 });
184
184
  }
185
185
  const row = await client.http.json({
186
186
  method: 'POST',
package/dist/thread.d.ts CHANGED
@@ -55,3 +55,11 @@ export declare function ThreadRow({ t, active, aside, listed, children, }: {
55
55
  listed?: boolean;
56
56
  children: ReactElement;
57
57
  }): import("react").JSX.Element;
58
+ /**
59
+ * The ⋯ of the conversation a pane holds, in its header: the trigger of the
60
+ * menu its row opens, drawn inside a `ConversationMenu` of that one
61
+ * conversation, so the header and the row offer one menu.
62
+ */
63
+ export declare function ThreadMore({ t }: {
64
+ t: Thread;
65
+ }): import("react").JSX.Element;
package/dist/thread.js CHANGED
@@ -373,6 +373,21 @@ export function ThreadRow({ t, active = false, aside, listed = true, children, }
373
373
  // Before gui's own press handler, which opens the menu and stops there.
374
374
  onPointerDownCapture: aim, onKeyDownCapture: aim, onFocus: () => setFocused(true), onBlur: () => setFocused(false), children: _jsx(MoreHorizontal, { size: 15, "aria-hidden": true }) }) }) })] }));
375
375
  }
376
+ /**
377
+ * The ⋯ of the conversation a pane holds, in its header: the trigger of the
378
+ * menu its row opens, drawn inside a `ConversationMenu` of that one
379
+ * conversation, so the header and the row offer one menu.
380
+ */
381
+ export function ThreadMore({ t }) {
382
+ const menus = useContext(Rows);
383
+ if (!menus)
384
+ throw new Error('ThreadMore is drawn inside a ConversationMenu, which holds its menu.');
385
+ // gui types these handlers by React Native's event; on the web they carry the DOM's.
386
+ const aim = (e) => menus.aim(t, e.currentTarget);
387
+ return (_jsx(DropdownMenuTrigger, { asChild: true, children: _jsx(XStack, { render: "button", "aria-label": "Conversation options", ...{ title: 'More' }, "data-slot": "conversation-options", width: 32, height: 32, rounded: "$3", items: "center", justify: "center", cursor: "pointer", bg: menus.open === t.id ? '$hover' : 'transparent', hoverStyle: { bg: '$hover' }, focusVisibleStyle: { outlineWidth: 2, outlineStyle: 'solid', outlineColor: '$outlineColor' },
388
+ // Before gui's own press handler, which opens the menu and stops there.
389
+ onPointerDownCapture: aim, onKeyDownCapture: aim, children: _jsx(MoreHorizontal, { size: 16, "aria-hidden": true }) }) }));
390
+ }
376
391
  /** A conversation's title, written again: one line, never empty. */
377
392
  function Rename({ t, open, onOpenChange, onCloseAutoFocus, save, }) {
378
393
  const [words, setWords] = useState('');
package/dist/where.d.ts CHANGED
@@ -67,5 +67,7 @@ export declare function configure(given: Partial<Addresses>): void;
67
67
  export declare const where: () => Addresses;
68
68
  /** The product's name, as the host states it. Read at call time, like every address. */
69
69
  export declare const brand: () => string;
70
+ /** The product's name in each mode of the app: the host's brand in Chat (Hanzo AI, Hanzo Team), Hanzo Dev in Dev. */
71
+ export declare const wordmark: (mode: 'chat' | 'dev') => string;
70
72
  /** A page of the app's own, from wherever the rooms are mounted. */
71
73
  export declare const site: (path: string) => string;
package/dist/where.js CHANGED
@@ -11,5 +11,7 @@ export function configure(given) {
11
11
  export const where = () => addresses;
12
12
  /** The product's name, as the host states it. Read at call time, like every address. */
13
13
  export const brand = () => addresses.brand ?? 'Hanzo AI';
14
+ /** The product's name in each mode of the app: the host's brand in Chat (Hanzo AI, Hanzo Team), Hanzo Dev in Dev. */
15
+ export const wordmark = (mode) => (mode === 'dev' ? 'Hanzo Dev' : brand());
14
16
  /** A page of the app's own, from wherever the rooms are mounted. */
15
17
  export const site = (path) => `${addresses.site}${path}`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hanzo/rooms",
3
- "version": "0.1.57",
3
+ "version": "0.1.59",
4
4
  "description": "The workspace rooms — Home, Chat, Inbox, Contacts, Meet, Cal, Drive, Board, Work, Bots — and the frame that holds them, once, for every Hanzo surface.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -66,7 +66,7 @@
66
66
  "devDependencies": {
67
67
  "@hanzo/ai": "0.6.23",
68
68
  "@hanzo/appearance": "0.2.8",
69
- "@hanzo/build": "0.2.53",
69
+ "@hanzo/build": "0.2.55",
70
70
  "@hanzo/composer": "0.2.0",
71
71
  "@hanzo/design": "0.5.34",
72
72
  "@hanzo/event": "0.3.56",
@@ -1,8 +0,0 @@
1
- /** The product's name in each mode: the host's brand in Chat (Hanzo AI, Hanzo Team), Hanzo Dev in Dev. */
2
- export declare const wordmark: (mode: 'chat' | 'dev') => string;
3
- export declare function ModeMark({ mode, onChat, onDev, stacked, }: {
4
- mode: 'chat' | 'dev';
5
- onChat: () => void;
6
- onDev: () => void;
7
- stacked?: boolean;
8
- }): import("react").JSX.Element;
package/dist/ModeMark.js DELETED
@@ -1,19 +0,0 @@
1
- 'use client';
2
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { MessageSquare, Code2 } from 'lucide-react';
4
- import { XStack, YStack, Text, Tooltip, TooltipContent, TooltipTrigger } from '@hanzo/ui';
5
- import { brand } from './where.js';
6
- /** The product's name in each mode: the host's brand in Chat (Hanzo AI, Hanzo Team), Hanzo Dev in Dev. */
7
- export const wordmark = (mode) => (mode === 'dev' ? 'Hanzo Dev' : brand());
8
- /** One side of the switch: an icon button named for its mode, with the name as its tooltip. */
9
- function Side({ name, on, onPress, children }) {
10
- return (_jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { children: _jsx(XStack, { render: "button", "aria-label": name, "aria-pressed": on, onPress: onPress, width: 28, height: 24, rounded: 999, items: "center", justify: "center", bg: on ? '$hover' : 'transparent', hoverStyle: { bg: '$hover' }, cursor: "pointer", children: children }) }), _jsx(TooltipContent, { children: _jsx(Text, { fontSize: "$2", children: name }) })] }));
11
- }
12
- export function ModeMark({ mode, onChat, onDev, stacked = false, }) {
13
- const dev = mode === 'dev';
14
- const Group = stacked ? YStack : XStack;
15
- const toggle = (_jsxs(Group, { role: "group", "aria-label": "Switch Hanzo", items: "center", gap: 2, p: 2, rounded: 999, bg: "$edge", shrink: 0, children: [_jsx(Side, { name: "Chat", on: !dev, onPress: onChat, children: _jsx(MessageSquare, { size: 14, "aria-hidden": true }) }), _jsx(Side, { name: "Dev", on: dev, onPress: onDev, children: _jsx(Code2, { size: 14, "aria-hidden": true }) })] }));
16
- if (stacked)
17
- return toggle;
18
- return (_jsxs(XStack, { flex: 1, items: "center", justify: "space-between", gap: "$2", minW: 0, children: [_jsx(Text, { fontSize: "$4", fontWeight: "600", color: "$ink", numberOfLines: 1, children: wordmark(mode) }), toggle] }));
19
- }