@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/Inbox.js CHANGED
@@ -127,7 +127,7 @@ function Reading({ room, reload, }) {
127
127
  reload();
128
128
  }
129
129
  catch (e) {
130
- setFailed(plainly(e, 'this room'));
130
+ setFailed(plainly(e, 'this conversation'));
131
131
  }
132
132
  finally {
133
133
  setSending(false);
package/dist/Me.d.ts CHANGED
@@ -13,9 +13,15 @@ export declare function Face({ host, size }: {
13
13
  host: Host;
14
14
  size?: number;
15
15
  }): import("react").JSX.Element;
16
+ /**
17
+ * The plans row, named by the plan the organization is on: Enterprise and
18
+ * Agency are plans a person stays on, so the row names theirs; every other
19
+ * plan, Free included, is one to move up from.
20
+ */
21
+ export declare const upgrade: (family: string | null | undefined) => string;
16
22
  export declare function Me({ host, narrow, onOrg, }: {
17
23
  host: Host;
18
- /** The collapsed rail's: the face alone, the menu opening beside it. */
24
+ /** The rail's: the face alone, the menu opening beside it. */
19
25
  narrow?: boolean;
20
26
  /** Work in another organization: picked, then the app opened in it. */
21
27
  onOrg: (org: string) => void;
package/dist/Me.js CHANGED
@@ -25,7 +25,7 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
25
25
  // Orgs.tsx); the next load mints a token that carries it.
26
26
  import { useEffect, useMemo, useRef, useState } from 'react';
27
27
  import { Popover, SizableText, usePopperContextSlow, View, XStack, YStack } from '@hanzo/gui';
28
- import { Building2, ChevronsUpDown, CreditCard, ExternalLink, Gauge, KeyRound, LifeBuoy, LogOut, Plus, Settings, Sparkles, Users, X } from 'lucide-react';
28
+ import { Building2, ChevronLeft, ChevronRight, ChevronsUpDown, CreditCard, ExternalLink, Gauge, KeyRound, LifeBuoy, LogOut, Mail, Plus, Settings, Sparkles, Users, X } from 'lucide-react';
29
29
  import { label, Meter, path, said as spoken, useStanding, useWho } from '@hanzo/build';
30
30
  import { useOrganizations } from '@hanzo/iam/react';
31
31
  import { Button, Dialog, DialogContent, DialogTitle, Input } from '@hanzo/ui';
@@ -111,7 +111,7 @@ export function Face({ host, size = 28 }) {
111
111
  /** One row of the menu: a real button, so Enter, Space and focus are the browser's. */
112
112
  function Item({ icon, label, sub, checked, onPress, trail, }) {
113
113
  const radio = checked !== undefined;
114
- return (_jsxs(XStack, { render: "button", ...{ role: radio ? 'menuitemradio' : 'menuitem' }, "aria-checked": radio ? checked : undefined, "data-item": "", onPress: onPress, items: "center", gap: "$2.5", px: "$2", minH: 36, py: "$1.5", rounded: "$4", bg: checked ? '$edge' : 'transparent', hoverStyle: { bg: '$hover' }, focusVisibleStyle: { bg: '$hover', outlineWidth: 2, outlineStyle: 'solid', outlineColor: '$outlineColor' }, cursor: "pointer", width: "100%", children: [icon ? (_jsx(XStack, { width: 20, justify: "center", shrink: 0, opacity: 0.8, children: icon })) : null, _jsxs(YStack, { flex: 1, minW: 0, children: [_jsx(SizableText, { size: "$2", color: "$ink", numberOfLines: 1, text: "left", children: label }), sub ? (_jsx(SizableText, { size: "$1", color: "$soft", numberOfLines: 1, text: "left", children: sub })) : null] }), trail] }));
114
+ return (_jsxs(XStack, { render: "button", ...{ role: radio ? 'menuitemradio' : 'menuitem' }, "aria-checked": radio ? checked : undefined, "data-item": "", onPress: onPress, items: "center", gap: "$2.5", px: "$2", minH: 30, py: 4, rounded: "$3", bg: checked ? '$edge' : 'transparent', hoverStyle: { bg: '$hover' }, focusVisibleStyle: { bg: '$hover', outlineWidth: 2, outlineStyle: 'solid', outlineColor: '$outlineColor' }, cursor: "pointer", width: "100%", children: [icon ? (_jsx(XStack, { width: 20, justify: "center", shrink: 0, opacity: 0.8, children: icon })) : null, _jsxs(YStack, { flex: 1, minW: 0, children: [_jsx(SizableText, { size: "$2", color: "$ink", numberOfLines: 1, text: "left", children: label }), sub ? (_jsx(SizableText, { size: "$1", color: "$soft", numberOfLines: 1, text: "left", children: sub })) : null] }), trail] }));
115
115
  }
116
116
  const Rule = () => _jsx(XStack, { height: 1, bg: "$borderColor", my: "$1", mx: "$1" });
117
117
  const Label = ({ children }) => (_jsx(SizableText, { size: "$1", color: "$soft", px: "$2", pt: "$1.5", pb: "$1", children: children }));
@@ -150,8 +150,21 @@ function Refit({ children }) {
150
150
  }, [update]);
151
151
  return (_jsx(View, { render: _jsx("div", { ref: box }), self: "stretch", minW: 0, children: children }));
152
152
  }
153
+ /**
154
+ * The plans row, named by the plan the organization is on: Enterprise and
155
+ * Agency are plans a person stays on, so the row names theirs; every other
156
+ * plan, Free included, is one to move up from.
157
+ */
158
+ export const upgrade = (family) => family === 'Enterprise' ? 'Enterprise plan' : family === 'Agency' ? 'Agency plan' : 'Upgrade plan';
159
+ /** The keys a row prints, in this platform's spelling. */
160
+ const mac = () => typeof navigator !== 'undefined' && /Mac|iPhone|iPad/.test(navigator.platform || navigator.userAgent);
161
+ /** A row's keys at its end, quiet. */
162
+ const Keys = ({ children }) => (_jsx(SizableText, { size: "$1", color: "$soft", shrink: 0, children: children }));
163
+ /** A row that opens a page of the menu. */
164
+ const More = () => _jsx(ChevronRight, { size: 14, "aria-hidden": true, opacity: 0.6 });
153
165
  export function Me({ host, narrow = false, onOrg, }) {
154
166
  const menu = useWho();
167
+ const [page, setPage] = useState('main');
155
168
  const [creating, setCreating] = useState(false);
156
169
  const [supporting, setSupporting] = useState(false);
157
170
  const row = useRef(null);
@@ -165,15 +178,24 @@ export function Me({ host, narrow = false, onOrg, }) {
165
178
  const other = usePlan(personal && personal.name !== current ? personal.name : null, menu.open);
166
179
  const planFor = (o) => (o.name === current ? spoken(read.value?.label ?? null) : o.personal ? other : '');
167
180
  const who = host.person?.name || host.person?.email || '';
168
- const standing = [said(here?.role ?? ''), here?.display ?? current].filter(Boolean).join(' · ');
169
- // The organization in use takes focus once the menu is drawn — after the
170
- // popover has placed its own focus — so the keys reach the rows at once.
181
+ const plan = spoken(read.value?.label ?? null);
182
+ const apple = mac();
183
+ // Each page opens on its first row, once it is drawn — after the popover has
184
+ // placed its own focus — so the keys reach the rows at once.
171
185
  const land = (node) => {
172
186
  content.current = node;
173
187
  if (!node)
174
188
  return;
175
189
  requestAnimationFrame(() => requestAnimationFrame(() => (node.querySelector('[data-item][aria-checked="true"]') ?? node.querySelector('[data-item]'))?.focus()));
176
190
  };
191
+ const turn = (to) => {
192
+ setPage(to);
193
+ requestAnimationFrame(() => land(content.current));
194
+ };
195
+ const toggle = () => {
196
+ setPage('main');
197
+ menu.toggle();
198
+ };
177
199
  const close = () => {
178
200
  menu.onOpenChange(false);
179
201
  row.current?.focus();
@@ -182,15 +204,20 @@ export function Me({ host, narrow = false, onOrg, }) {
182
204
  menu.onOpenChange(false);
183
205
  host.go(p);
184
206
  };
207
+ const back = (title) => _jsx(Item, { icon: _jsx(ChevronLeft, { size: 15, "aria-hidden": true }), label: title, onPress: () => turn('main') });
185
208
  if (!host.person)
186
209
  return null;
187
- return (_jsxs(_Fragment, { children: [_jsxs(Popover, { open: menu.open, onOpenChange: menu.onOpenChange, placement: narrow ? 'right-end' : 'top-start', allowFlip: true, stayInFrame: true, offset: 8, children: [_jsx(Popover.Anchor, { width: "100%", children: _jsxs(XStack, { ref: (el) => {
210
+ return (_jsxs(_Fragment, { children: [_jsxs(Popover, { open: menu.open, onOpenChange: (open) => {
211
+ if (open)
212
+ setPage('main');
213
+ menu.onOpenChange(open);
214
+ }, placement: narrow ? 'right-end' : 'top-start', allowFlip: true, stayInFrame: true, offset: 8, children: [_jsx(Popover.Anchor, { width: narrow ? undefined : '100%', children: _jsxs(XStack, { ref: (el) => {
188
215
  row.current = el instanceof HTMLElement ? el : null;
189
- }, render: "button", "data-slot": "me", "aria-label": `Account: ${[who, current].filter(Boolean).join(' · ')}`, "aria-haspopup": "menu", "aria-expanded": menu.open, onPress: menu.toggle, width: narrow ? 40 : '100%', minH: narrow ? 40 : 48, px: narrow ? 0 : '$2', gap: "$2.5", rounded: narrow ? 999 : '$5', items: "center", justify: narrow ? 'center' : 'flex-start', hoverStyle: { bg: '$hover' }, focusVisibleStyle: { outlineWidth: 2, outlineStyle: 'solid', outlineColor: '$outlineColor' }, cursor: "pointer", children: [_jsx(Face, { host: host, size: narrow ? 30 : 32 }), narrow ? null : (_jsxs(_Fragment, { children: [_jsxs(YStack, { flex: 1, minW: 0, children: [_jsx(SizableText, { size: "$3", fontWeight: "600", color: "$ink", numberOfLines: 1, text: "left", children: who }), standing ? (_jsx(SizableText, { size: "$1", color: "$soft", numberOfLines: 1, text: "left", children: standing })) : null] }), _jsx(ChevronsUpDown, { size: 14, "aria-hidden": true, opacity: 0.6 })] }))] }) }), _jsx(Popover.Content, { ...glass(2), items: "stretch", borderWidth: 1, rounded: "$6", p: "$1.5", width: 288, maxW: "calc(100vw - 16px)", maxH: "calc(100dvh - 32px)", overflowY: "auto", overflowX: "hidden", children: _jsx(Refit, { children: _jsx(View, { render: _jsx("div", { ref: land, role: "menu", "aria-label": "Account", onKeyDown: (e) => rove(e, close) }), self: "stretch", minW: 0, children: _jsxs(YStack, { gap: 2, children: [_jsx(SizableText, { size: "$2", color: "$soft", px: "$2", py: "$1.5", numberOfLines: 1, children: host.person.email || who }), _jsx(Rule, {}), _jsx(Meter, { read: read, onPlans: () => go(path({ kind: 'screen', screen: 'plans' })), onBilling: () => go(path({ kind: 'settings', section: 'billing' })) }), _jsx(Rule, {}), _jsx(Label, { children: "Organizations" }), _jsx(YStack, { role: "group", "aria-label": "Organizations", gap: 2, children: all.map((o) => (_jsx(Item, { icon: _jsx(Mark, { org: o }), label: o.display, sub: [o.personal ? 'Personal' : said(o.role), planFor(o)].filter(Boolean).join(' · '), checked: o.name === current, onPress: () => {
216
+ }, render: "button", "data-slot": "me", "aria-label": `Account: ${[who, current].filter(Boolean).join(' · ')}`, ...{ title: who }, "aria-haspopup": "menu", "aria-expanded": menu.open, onPress: toggle, width: narrow ? 36 : '100%', minH: narrow ? 36 : 48, px: narrow ? 0 : '$2', gap: "$2.5", rounded: narrow ? 999 : '$5', items: "center", justify: narrow ? 'center' : 'flex-start', hoverStyle: { bg: '$hover' }, focusVisibleStyle: { outlineWidth: 2, outlineStyle: 'solid', outlineColor: '$outlineColor' }, cursor: "pointer", children: [_jsx(Face, { host: host, size: narrow ? 26 : 32 }), narrow ? null : (_jsxs(_Fragment, { children: [_jsxs(YStack, { flex: 1, minW: 0, children: [_jsx(SizableText, { size: "$3", fontWeight: "600", color: "$ink", numberOfLines: 1, text: "left", children: who }), plan ? (_jsx(SizableText, { size: "$1", color: "$soft", numberOfLines: 1, text: "left", children: plan })) : null] }), _jsx(ChevronsUpDown, { size: 14, "aria-hidden": true, opacity: 0.6 })] }))] }) }), _jsx(Popover.Content, { ...glass(2), items: "stretch", borderWidth: 1, rounded: "$5", p: "$1", width: 240, maxW: "calc(100vw - 16px)", maxH: "calc(100dvh - 32px)", overflowY: "auto", overflowX: "hidden", children: _jsx(Refit, { children: _jsx(View, { render: _jsx("div", { ref: land, role: "menu", "aria-label": "Account", onKeyDown: (e) => rove(e, close) }), self: "stretch", minW: 0, children: page === 'main' ? (_jsxs(YStack, { gap: 1, children: [_jsxs(XStack, { items: "center", gap: "$2.5", px: "$2", pt: "$1.5", pb: "$2", children: [_jsx(Face, { host: host, size: 28 }), _jsxs(YStack, { flex: 1, minW: 0, children: [_jsx(SizableText, { size: "$2", fontWeight: "600", color: "$ink", numberOfLines: 1, children: who }), _jsx(SizableText, { size: "$1", color: "$soft", numberOfLines: 1, children: plan || host.person.email || '' })] })] }), _jsx(Rule, {}), _jsx(Item, { icon: _jsx(Gauge, { size: 15, "aria-hidden": true }), label: "Usage remaining", trail: _jsx(More, {}), onPress: () => turn('usage') }), _jsx(Item, { icon: _jsx(Building2, { size: 15, "aria-hidden": true }), label: here?.display ?? current ?? 'Organization', sub: "Organization", trail: _jsx(More, {}), onPress: () => turn('orgs') }), _jsx(Item, { icon: _jsx(KeyRound, { size: 15, "aria-hidden": true }), label: "API keys", trail: _jsx(ExternalLink, { size: 13, "aria-hidden": true, opacity: 0.6 }), onPress: () => (menu.onOpenChange(false), window.location.assign(KEYS)) }), _jsx(Item, { icon: _jsx(Settings, { size: 15, "aria-hidden": true }), label: "Settings", trail: _jsx(Keys, { children: apple ? '⌘,' : 'Ctrl+,' }), onPress: () => go(path({ kind: 'settings', section: 'general' })) }), _jsx(Rule, {}), _jsx(Item, { icon: _jsx(LifeBuoy, { size: 15, "aria-hidden": true }), label: "Help", trail: _jsx(More, {}), onPress: () => turn('help') }), host.signOut ? _jsx(Item, { icon: _jsx(LogOut, { size: 15, "aria-hidden": true }), label: "Log out", onPress: () => (menu.onOpenChange(false), host.signOut?.()) }) : null] })) : page === 'usage' ? (_jsxs(YStack, { gap: 1, children: [back('Usage remaining'), _jsx(Rule, {}), _jsx(Meter, { read: read, onPlans: () => go(path({ kind: 'screen', screen: 'plans' })), onBilling: () => go(path({ kind: 'settings', section: 'billing' })) }), _jsx(Rule, {}), _jsx(Item, { icon: _jsx(Gauge, { size: 15, "aria-hidden": true }), label: "Usage", onPress: () => go(path({ kind: 'settings', section: 'usage' })) }), _jsx(Item, { icon: _jsx(CreditCard, { size: 15, "aria-hidden": true }), label: "Billing", onPress: () => go(path({ kind: 'settings', section: 'billing' })) }), _jsx(Item, { icon: _jsx(Sparkles, { size: 15, "aria-hidden": true }), label: upgrade(read.value?.label?.name), onPress: () => go(path({ kind: 'screen', screen: 'plans' })) })] })) : page === 'orgs' ? (_jsxs(YStack, { gap: 1, children: [back('Organization'), _jsx(Rule, {}), _jsx(Label, { children: "Organizations" }), _jsx(YStack, { role: "group", "aria-label": "Organizations", gap: 2, children: all.map((o) => (_jsx(Item, { icon: _jsx(Mark, { org: o }), label: o.display, sub: [o.personal ? 'Personal' : said(o.role), planFor(o)].filter(Boolean).join(' · '), checked: o.name === current, onPress: () => {
190
217
  menu.onOpenChange(false);
191
218
  if (o.name !== current)
192
219
  onOrg(o.name);
193
- } }, o.name))) }), _jsx(Item, { icon: _jsx(Plus, { size: 15, "aria-hidden": true }), label: "Create organization", onPress: () => (menu.onOpenChange(false), setCreating(true)) }), _jsx(Item, { icon: _jsx(Settings, { size: 15, "aria-hidden": true }), label: "Organization settings", onPress: () => (menu.onOpenChange(false), window.location.assign(site(ORG_SETTINGS))) }), where().team ? null : here && !here.personal ? (_jsx(Item, { icon: _jsx(Users, { size: 15, "aria-hidden": true }), label: "Open in Hanzo Team", trail: _jsx(ExternalLink, { size: 13, "aria-hidden": true, opacity: 0.6 }), onPress: () => (menu.onOpenChange(false), window.location.assign(`${ENTRY}&org=${encodeURIComponent(here.name)}`)) })) : (_jsx(Item, { icon: _jsx(Users, { size: 15, "aria-hidden": true }), label: "Create a team", onPress: () => (menu.onOpenChange(false), setCreating(true)) })), superAdmin() ? (_jsx(Item, { icon: _jsx(Building2, { size: 15, "aria-hidden": true }), label: "All organizations", onPress: () => (menu.onOpenChange(false), setSupporting(true)) })) : null, _jsx(Rule, {}), _jsx(Item, { icon: _jsx(Settings, { size: 15, "aria-hidden": true }), label: "Settings", onPress: () => go(path({ kind: 'settings', section: 'general' })) }), _jsx(Item, { icon: _jsx(Gauge, { size: 15, "aria-hidden": true }), label: "Usage", onPress: () => go(path({ kind: 'settings', section: 'usage' })) }), _jsx(Item, { icon: _jsx(CreditCard, { size: 15, "aria-hidden": true }), label: "Billing", onPress: () => go(path({ kind: 'settings', section: 'billing' })) }), _jsx(Item, { icon: _jsx(KeyRound, { size: 15, "aria-hidden": true }), label: "API keys", onPress: () => (menu.onOpenChange(false), window.location.assign(KEYS)) }), _jsx(Item, { icon: _jsx(Sparkles, { size: 15, "aria-hidden": true }), label: "View all plans", onPress: () => go(path({ kind: 'screen', screen: 'plans' })) }), _jsx(Item, { icon: _jsx(LifeBuoy, { size: 15, "aria-hidden": true }), label: "Get help", onPress: () => (menu.onOpenChange(false), window.open(DOCS, '_blank', 'noopener,noreferrer')) }), host.signOut ? (_jsxs(_Fragment, { children: [_jsx(Rule, {}), _jsx(Item, { icon: _jsx(LogOut, { size: 15, "aria-hidden": true }), label: "Log out", onPress: () => (menu.onOpenChange(false), host.signOut?.()) })] })) : null] }) }) }) })] }), _jsx(Create, { open: creating, onOpenChange: setCreating }), supporting ? _jsx(SupportPicker, { onClose: () => setSupporting(false) }) : null] }));
220
+ } }, o.name))) }), _jsx(Item, { icon: _jsx(Plus, { size: 15, "aria-hidden": true }), label: "Create organization", onPress: () => (menu.onOpenChange(false), setCreating(true)) }), _jsx(Item, { icon: _jsx(Settings, { size: 15, "aria-hidden": true }), label: "Organization settings", onPress: () => (menu.onOpenChange(false), window.location.assign(site(ORG_SETTINGS))) }), where().team ? null : here && !here.personal ? (_jsx(Item, { icon: _jsx(Users, { size: 15, "aria-hidden": true }), label: "Open in Hanzo Team", trail: _jsx(ExternalLink, { size: 13, "aria-hidden": true, opacity: 0.6 }), onPress: () => (menu.onOpenChange(false), window.location.assign(`${ENTRY}&org=${encodeURIComponent(here.name)}`)) })) : (_jsx(Item, { icon: _jsx(Users, { size: 15, "aria-hidden": true }), label: "Create a team", onPress: () => (menu.onOpenChange(false), setCreating(true)) })), superAdmin() ? (_jsx(Item, { icon: _jsx(Building2, { size: 15, "aria-hidden": true }), label: "All organizations", onPress: () => (menu.onOpenChange(false), setSupporting(true)) })) : null] })) : (_jsxs(YStack, { gap: 1, children: [back('Help'), _jsx(Rule, {}), _jsx(Item, { icon: _jsx(LifeBuoy, { size: 15, "aria-hidden": true }), label: "Get help", trail: _jsx(ExternalLink, { size: 13, "aria-hidden": true, opacity: 0.6 }), onPress: () => (menu.onOpenChange(false), window.open(DOCS, '_blank', 'noopener,noreferrer')) }), _jsx(Item, { icon: _jsx(Mail, { size: 15, "aria-hidden": true }), label: "Contact us", onPress: () => (menu.onOpenChange(false), window.location.assign(site('/contact'))) })] })) }) }) })] }), _jsx(Create, { open: creating, onOpenChange: setCreating }), supporting ? _jsx(SupportPicker, { onClose: () => setSupporting(false) }) : null] }));
194
221
  }
195
222
  /**
196
223
  * The sheet that creates an organization: a name, then `POST /v1/account/orgs`,
package/dist/Meet.js CHANGED
@@ -273,7 +273,7 @@ export function Meet() {
273
273
  return (_jsxs(YStack, { width: "100%", height: "100%", bg: "$background", overflowX: "auto", overflowY: "auto", children: [_jsxs(XStack, { minH: 56, px: 20, py: 8, gap: 10, flexWrap: "wrap", bg: "$panel", borderBottomWidth: 1, borderBottomColor: "$borderColor", items: "center", justify: "space-between", children: [_jsxs(XStack, { items: "center", gap: 12, shrink: 1, minW: "auto", children: [_jsx(XStack, { width: 32, height: 32, rounded: 8, bg: "$raised", items: "center", justify: "center", shrink: 1, minW: "auto", children: _jsx(Video, { size: 18, color: "var(--foreground)" }) }), _jsxs(YStack, { shrink: 1, minW: "auto", children: [_jsxs(XStack, { items: "center", gap: 8, children: [_jsx(Text, { fontSize: "$4", fontWeight: "700", color: "$ink", children: "Hanzo Meet" }), _jsx(Text, { fontSize: FLOOR, fontWeight: "700", bg: "$hover", color: "$soft", borderWidth: 1, borderColor: "$borderColor", px: 6, py: 1, rounded: 4, children: "LIVE \u00B7 meet.hanzo.ai" })] }), _jsx(Text, { fontSize: "$1", color: "$soft", children: "Native WebRTC video conferencing with AI meeting bot transcription & voice" })] })] }), activeMeetingRoom ? (_jsxs(XStack, { items: "center", gap: 8, shrink: 1, minW: "auto", flexWrap: "wrap", children: [_jsx(XStack, { bg: "$hover", rounded: 8, p: 2, gap: 2, borderWidth: 1, borderColor: "$borderColor", shrink: 1, minW: "auto", children: [
274
274
  ['iframe', 'SFU Stage', Tv],
275
275
  ['grid', 'Agent Grid', LayoutGrid],
276
- ].map(([view, label, Icon]) => (_jsxs(Button, { size: "sm", variant: meetingView === view ? 'primary' : 'ghost', ...(meetingView === view ? null : { color: '$soft' }), "aria-pressed": meetingView === view, onClick: () => setMeetingView(view), children: [_jsx(Icon, { size: 12 }), label] }, view))) }), botAssisting ? (_jsx(View, { display: "none", "$max-md": { display: 'flex' }, children: _jsxs(Button, { size: "sm", variant: copilot ? 'secondary' : 'default', onClick: () => setCopilot((v) => !v), "aria-label": "Co-pilot", "aria-expanded": copilot, "aria-controls": "meet-copilot", children: [_jsx(Bot, { size: 13 }), "Co-pilot"] }) })) : null, _jsxs(Button, { size: "sm", onClick: () => handleCopyLink(activeMeetingRoom), children: [copied ? _jsx(Check, { size: 13 }) : _jsx(Copy, { size: 13 }), copied ? 'Link Copied' : 'Copy Room Link'] }), _jsxs(Button, { size: "sm", variant: "destructive", onClick: () => {
276
+ ].map(([view, label, Icon]) => (_jsxs(Button, { size: "sm", variant: meetingView === view ? 'primary' : 'ghost', ...(meetingView === view ? null : { color: '$soft' }), "aria-pressed": meetingView === view, onClick: () => setMeetingView(view), children: [_jsx(Icon, { size: 12 }), label] }, view))) }), botAssisting ? (_jsx(View, { display: "none", "$max-md": { display: 'flex' }, children: _jsxs(Button, { size: "sm", variant: copilot ? 'secondary' : 'default', onClick: () => setCopilot((v) => !v), "aria-label": "Co-pilot", "aria-expanded": copilot, "aria-controls": "meet-copilot", children: [_jsx(Bot, { size: 13 }), "Co-pilot"] }) })) : null, _jsxs(Button, { size: "sm", onClick: () => handleCopyLink(activeMeetingRoom), children: [copied ? _jsx(Check, { size: 13 }) : _jsx(Copy, { size: 13 }), copied ? 'Link Copied' : 'Copy meeting link'] }), _jsxs(Button, { size: "sm", variant: "destructive", onClick: () => {
277
277
  stopAgentSpeech();
278
278
  // Leaving the call hangs up the hands-free conversation and
279
279
  // settles the transcript with it.
package/dist/Prose.js CHANGED
@@ -7,8 +7,15 @@ const INK = 'var(--foreground)';
7
7
  const QUIET = 'var(--muted-foreground)';
8
8
  const EDGE = 'var(--border)';
9
9
  const GROUND = 'var(--secondary)';
10
- /** A heading's rung, by depth: one step down the type ramp per level. */
11
- const RUNG = ['$6', '$4', '$3', '$2', '$2', '$2'];
10
+ /**
11
+ * The reading type, px: a 16px body on a 24px line, and a heading's size and
12
+ * line by depth, as the reference conversation sets them — the body a reader
13
+ * reads at arm's length, not a sidebar's 14.
14
+ */
15
+ const BODY = 16;
16
+ const LINE = 24;
17
+ const RUNG = [28, 24, 20, 17, 16, 16];
18
+ const LEAD = [36, 32, 28, 24, 24, 24];
12
19
  /** The character references a model writes, as the characters they name. An
13
20
  * HTML renderer let the browser decode `&`; a string React sets is shown
14
21
  * exactly as written. */
@@ -21,7 +28,7 @@ const decode = (s) => s.replace(/&(?:#x([0-9a-f]{1,6})|#(\d{1,7})|([a-z]+));/gi,
21
28
  return NAMED[name.toLowerCase()] ?? all;
22
29
  });
23
30
  /** One run of inline tokens, in `tone`, at the block's `rung`. */
24
- function inline(tokens, tone, rung = '$2') {
31
+ function inline(tokens, tone, rung = BODY) {
25
32
  return (tokens ?? []).map((t, i) => {
26
33
  switch (t.type) {
27
34
  case 'strong':
@@ -31,7 +38,7 @@ function inline(tokens, tone, rung = '$2') {
31
38
  case 'del':
32
39
  return (_jsx(Text, { render: "del", color: tone, textDecorationLine: "line-through", opacity: 0.7, children: inline(t.tokens, tone, rung) }, i));
33
40
  case 'codespan':
34
- return (_jsx(Text, { render: "code", color: tone, fontFamily: "$mono", fontSize: "$1", px: "$1", rounded: 4, bg: GROUND, children: decode(t.text) }, i));
41
+ return (_jsx(Text, { render: "code", color: tone, fontFamily: "$mono", fontSize: Math.round(rung * 0.875), px: "$1", rounded: 4, bg: GROUND, children: decode(t.text) }, i));
35
42
  case 'br':
36
43
  return _jsx("br", {}, i);
37
44
  case 'link': {
@@ -41,7 +48,7 @@ function inline(tokens, tone, rung = '$2') {
41
48
  // AWAY IS A NEW TAB, HOME IS NOT. Someone else's page opened from a
42
49
  // message never gets a handle on this one and never inherits the
43
50
  // referrer; a link back into this app is a link.
44
- return (_jsx(Anchor, { href: to.href, ...(to.away ? { target: '_blank', rel: 'noopener noreferrer nofollow' } : {}), ...(t.title ? { title: t.title } : {}), size: rung, color: INK, textDecorationLine: "underline", children: inline(t.tokens, INK, rung) }, i));
51
+ return (_jsx(Anchor, { href: to.href, ...(to.away ? { target: '_blank', rel: 'noopener noreferrer nofollow' } : {}), ...(t.title ? { title: t.title } : {}), fontSize: rung, color: INK, textDecorationLine: "underline", children: inline(t.tokens, INK, rung) }, i));
45
52
  }
46
53
  // An image is a fetch to wherever the message says, which is a reader's
47
54
  // address leaking to whoever wrote it. The alt text is the message.
@@ -59,11 +66,11 @@ function inline(tokens, tone, rung = '$2') {
59
66
  }
60
67
  /** Running text: a paragraph, or a list item's own words. */
61
68
  function Line({ tokens, tone }) {
62
- return (_jsx(Text, { render: "p", color: tone, fontSize: "$2", lineHeight: "$3", children: inline(tokens, tone) }));
69
+ return (_jsx(Text, { render: "p", color: tone, fontSize: BODY, lineHeight: LINE, children: inline(tokens, tone) }));
63
70
  }
64
71
  function List({ list, tone }) {
65
72
  const start = typeof list.start === 'number' ? list.start : 1;
66
- return (_jsx(YStack, { render: list.ordered ? 'ol' : 'ul', m: 0, p: 0, gap: "$1", children: list.items.map((item, i) => (_jsxs(XStack, { render: "li", gap: "$1", children: [_jsx(Text, { "aria-hidden": true, color: tone, fontSize: "$2", lineHeight: "$3", minW: 14, text: "right", children: item.task ? (item.checked ? '☑' : '☐') : list.ordered ? `${start + i}.` : '•' }), _jsx(YStack, { flex: 1, minW: 0, gap: "$1", children: blocks(item.tokens, tone) })] }, i))) }));
73
+ return (_jsx(YStack, { render: list.ordered ? 'ol' : 'ul', m: 0, p: 0, gap: "$1", children: list.items.map((item, i) => (_jsxs(XStack, { render: "li", gap: "$1", children: [_jsx(Text, { "aria-hidden": true, color: tone, fontSize: BODY, lineHeight: LINE, minW: 16, text: "right", children: item.task ? (item.checked ? '☑' : '☐') : list.ordered ? `${start + i}.` : '•' }), _jsx(YStack, { flex: 1, minW: 0, gap: "$1", children: blocks(item.tokens, tone) })] }, i))) }));
67
74
  }
68
75
  /** A table is a grid: one track per column, as wide as what the column holds,
69
76
  * in a box that scrolls sideways rather than widening the message. Cells
@@ -71,7 +78,7 @@ function List({ list, tone }) {
71
78
  * the top and the first column the left. */
72
79
  function Table({ table, tone }) {
73
80
  const align = (c) => table.align[c] ?? 'left';
74
- return (_jsx(View, { role: "table", display: "grid", gridTemplateColumns: `repeat(${table.header.length}, max-content)`, self: "flex-start", maxW: "100%", overflowX: "auto", children: [table.header, ...table.rows].map((row, r) => (_jsx(View, { role: "row", display: "contents", children: row.map((cell, c) => (_jsx(Text, { role: r ? 'cell' : 'columnheader', color: tone, fontSize: "$2", lineHeight: "$3", fontWeight: r ? '400' : '600', text: align(c), px: "$2", py: "$1", bg: r ? undefined : GROUND, borderColor: EDGE, borderStyle: "solid", borderRightWidth: 1, borderBottomWidth: 1, borderTopWidth: r ? 0 : 1, borderLeftWidth: c ? 0 : 1, children: inline(cell.tokens, tone) }, c))) }, r))) }));
81
+ return (_jsx(View, { role: "table", display: "grid", gridTemplateColumns: `repeat(${table.header.length}, max-content)`, self: "flex-start", maxW: "100%", overflowX: "auto", children: [table.header, ...table.rows].map((row, r) => (_jsx(View, { role: "row", display: "contents", children: row.map((cell, c) => (_jsx(Text, { role: r ? 'cell' : 'columnheader', color: tone, fontSize: 14, lineHeight: 20, fontWeight: r ? '400' : '600', text: align(c), px: "$2", py: "$1", bg: r ? undefined : GROUND, borderColor: EDGE, borderStyle: "solid", borderRightWidth: 1, borderBottomWidth: 1, borderTopWidth: r ? 0 : 1, borderLeftWidth: c ? 0 : 1, children: inline(cell.tokens, tone, 14) }, c))) }, r))) }));
75
82
  }
76
83
  /** A run of block tokens. A tight list item's words arrive as a `text` block. */
77
84
  function blocks(tokens, tone) {
@@ -82,7 +89,7 @@ function blocks(tokens, tone) {
82
89
  case 'text':
83
90
  return _jsx(Line, { tokens: 'tokens' in t && t.tokens ? t.tokens : [t], tone: tone }, i);
84
91
  case 'heading':
85
- return (_jsx(Text, { render: `h${t.depth}`, m: 0, color: tone, fontSize: RUNG[t.depth - 1], lineHeight: RUNG[t.depth - 1], fontWeight: "600", children: inline(t.tokens, tone, RUNG[t.depth - 1]) }, i));
92
+ return (_jsx(Text, { render: `h${t.depth}`, m: 0, mt: t.depth < 3 ? 8 : 4, color: tone, fontSize: RUNG[t.depth - 1], lineHeight: LEAD[t.depth - 1], fontWeight: "600", children: inline(t.tokens, tone, RUNG[t.depth - 1]) }, i));
86
93
  case 'list':
87
94
  return _jsx(List, { list: t, tone: tone }, i);
88
95
  case 'table':
package/dist/Rail.d.ts ADDED
@@ -0,0 +1,39 @@
1
+ import type { ReactNode } from 'react';
2
+ import { type Host } from '@hanzo/build';
3
+ import type { Mode } from './recent.js';
4
+ /** The rail's width, px. */
5
+ export declare const RAIL = 48;
6
+ /** One mark: an icon button named for where it goes, its name its tooltip, lit where the reader is. */
7
+ export declare function Mark({ label, on, onPress, children, ...rest }: {
8
+ label: string;
9
+ /** The reader is there. */
10
+ on?: boolean;
11
+ onPress?: () => void;
12
+ children: ReactNode;
13
+ [attribute: `data-${string}` | `aria-${string}`]: unknown;
14
+ }): import("react").JSX.Element;
15
+ /** The sidebar's toggle: the same mark wherever a host draws it. */
16
+ export declare function Toggle({ open, onPress, label }: {
17
+ open: boolean;
18
+ onPress: () => void;
19
+ label?: string;
20
+ }): import("react").JSX.Element;
21
+ export declare function Rail({ host, mode, at, open, toggle, flat, onChat, onDev, go, onOrg, }: {
22
+ host: Host;
23
+ mode: Mode;
24
+ /** Dev's builder address, or null in Chat. */
25
+ at: string | null;
26
+ /** The sidebar beside the rail is open. */
27
+ open: boolean;
28
+ /** Lead with the sidebar's toggle; absent where the host's bar holds it. */
29
+ toggle?: () => void;
30
+ /** Laid along the foot of the phone's drawer rather than down the window's edge. */
31
+ flat?: boolean;
32
+ /** To Chat and to Dev, each where the reader left it. */
33
+ onChat: () => void;
34
+ onDev: () => void;
35
+ /** Moves the app to an address of this page. */
36
+ go: (address: string) => void;
37
+ /** Work in another organization. */
38
+ onOrg: (org: string) => void;
39
+ }): import("react").JSX.Element;
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, 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,32 @@ 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, 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
+ const body = (_jsxs(YStack, { role: "list", "aria-labelledby": label, gap: 1, children: [pinned ? (_jsxs(YStack, { role: "listitem", gap: 1, children: [heading(PINNED), _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
94
+ ? 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")))
95
+ : pinned
96
+ ? null
97
+ : note] })] }));
98
+ 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, threads ? (_jsx(ConversationMenu, { threads: threads, active: held, onGone: onNew, children: _jsx(YStack, { children: body }) })) : (body)] }) }));
90
99
  }
@@ -38,3 +38,18 @@ 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;
package/dist/RightPane.js CHANGED
@@ -17,8 +17,8 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
17
17
  // @hanzo/build (`useDeck`, scope `chat:<channel>`); the frame owns the column
18
18
  // it stands in (`Beside` in `Shell.tsx`).
19
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';
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';
@@ -29,13 +29,7 @@ import { localOf, take } from './pane.js';
29
29
  import { useProviders, useSkills } from './tools.js';
30
30
  import { activate, startDevice } from './connect.js';
31
31
  /** 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
- };
32
+ export const SEED = { tabs: [], at: null };
39
33
  /** The text of a turn, whichever shape the wire used. */
40
34
  const words = (m) => typeof m.content === 'string'
41
35
  ? m.content
@@ -222,3 +216,38 @@ function Services() {
222
216
  .catch((e) => setStep(String(e?.message ?? 'Could not start')));
223
217
  }, children: _jsx(Word, { children: p.name }) }, p.id)))] }));
224
218
  }
219
+ /** A row of the summary: its mark and its name, one press from a tab. */
220
+ function Line({ icon, children, onPress, quiet = false }) {
221
+ 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 })] }));
222
+ }
223
+ /** A heading of the summary, with what it adds at its end. */
224
+ 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] }));
225
+ /**
226
+ * The conversation's summary, from the outline mark in its header: what it
227
+ * produced (Outputs, led by Create file or site, which starts a Dev run) and
228
+ * what went into it (Sources, with the `+` that brings more). Each row opens in
229
+ * a tab beside the conversation. A section with nothing in it keeps its heading
230
+ * and one quiet line.
231
+ */
232
+ export function Summary({ at, said, held, deck, onOpen, onCreate }) {
233
+ const ai = useAi();
234
+ const [open, setOpen] = useState(false);
235
+ const made = useMemo(() => outputs(said), [said]);
236
+ const took = useMemo(() => sources(said, held), [said, held]);
237
+ const done = useMemo(() => lookups(said), [said]);
238
+ const show = (m) => {
239
+ setOpen(false);
240
+ void look(deck, m, ai);
241
+ onOpen();
242
+ };
243
+ 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: () => {
244
+ setOpen(false);
245
+ onCreate();
246
+ }, children: "Create file or site" }), made.map((m) => {
247
+ const Glyph = GLYPH[m.kind];
248
+ return (_jsx(Line, { icon: _jsx(Glyph, { size: 15, "aria-hidden": true }), onPress: () => show(m), children: m.name }, m.id));
249
+ }), _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) => {
250
+ const Glyph = GLYPH[m.kind];
251
+ return (_jsx(Line, { icon: _jsx(Glyph, { size: 15, "aria-hidden": true }), onPress: () => show(m), children: m.name }, m.id));
252
+ }), !done.length && !took.length ? _jsx(Line, { quiet: true, children: "Nothing yet" }) : null] }) })] }));
253
+ }
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 }) {