@hanzo/rooms 0.1.73 → 0.1.74

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.
Files changed (108) hide show
  1. package/assets/personas/ada.md +187 -0
  2. package/assets/personas/adi_shamir.md +155 -0
  3. package/assets/personas/al_khwarizmi.md +168 -0
  4. package/assets/personas/al_kindi.md +172 -0
  5. package/assets/personas/alan_kay.md +154 -0
  6. package/assets/personas/analyst.md +138 -0
  7. package/assets/personas/andrew_yao.md +155 -0
  8. package/assets/personas/auguste_kerckhoffs.md +168 -0
  9. package/assets/personas/barbara_liskov.md +154 -0
  10. package/assets/personas/butler_lampson.md +156 -0
  11. package/assets/personas/charles_babbage.md +175 -0
  12. package/assets/personas/creative.md +141 -0
  13. package/assets/personas/curry.md +169 -0
  14. package/assets/personas/cynthia_dwork.md +158 -0
  15. package/assets/personas/david_chaum.md +157 -0
  16. package/assets/personas/david_parnas.md +153 -0
  17. package/assets/personas/dennis_ritchie.md +153 -0
  18. package/assets/personas/dijkstra.md +187 -0
  19. package/assets/personas/donald_knuth.md +156 -0
  20. package/assets/personas/douglas_engelbart.md +175 -0
  21. package/assets/personas/edgar_codd.md +151 -0
  22. package/assets/personas/engineer.md +140 -0
  23. package/assets/personas/feynman.md +178 -0
  24. package/assets/personas/finney.md +187 -0
  25. package/assets/personas/fred_brooks.md +152 -0
  26. package/assets/personas/geoffrey_hinton.md +194 -0
  27. package/assets/personas/herbert_simon.md +190 -0
  28. package/assets/personas/hickey.md +169 -0
  29. package/assets/personas/hopper.md +191 -0
  30. package/assets/personas/hubert_dreyfus.md +184 -0
  31. package/assets/personas/humberto_maturana.md +156 -0
  32. package/assets/personas/index.json +1 -0
  33. package/assets/personas/jcr_licklider.md +178 -0
  34. package/assets/personas/jobs.md +177 -0
  35. package/assets/personas/joe_armstrong.md +155 -0
  36. package/assets/personas/john_backus.md +179 -0
  37. package/assets/personas/john_mccarthy.md +177 -0
  38. package/assets/personas/jon_postel.md +149 -0
  39. package/assets/personas/joseph_weizenbaum.md +185 -0
  40. package/assets/personas/judea_pearl.md +192 -0
  41. package/assets/personas/karen_sparck_jones.md +154 -0
  42. package/assets/personas/konrad_zuse.md +175 -0
  43. package/assets/personas/kunihiko_fukushima.md +152 -0
  44. package/assets/personas/leslie_lamport.md +156 -0
  45. package/assets/personas/markov.md +171 -0
  46. package/assets/personas/mentor.md +141 -0
  47. package/assets/personas/nancy_lynch.md +154 -0
  48. package/assets/personas/niklaus_wirth.md +154 -0
  49. package/assets/personas/norbert_wiener.md +179 -0
  50. package/assets/personas/north-star.md +138 -0
  51. package/assets/personas/peter_naur.md +151 -0
  52. package/assets/personas/pike.md +165 -0
  53. package/assets/personas/radia_perlman.md +156 -0
  54. package/assets/personas/raj_reddy.md +154 -0
  55. package/assets/personas/ralph_merkle.md +152 -0
  56. package/assets/personas/richard_sutton.md +156 -0
  57. package/assets/personas/robin_milner.md +149 -0
  58. package/assets/personas/shafi_goldwasser.md +156 -0
  59. package/assets/personas/shannon.md +184 -0
  60. package/assets/personas/stafford_beer.md +157 -0
  61. package/assets/personas/taher_elgamal.md +180 -0
  62. package/assets/personas/tesla.md +181 -0
  63. package/assets/personas/tony_hoare.md +153 -0
  64. package/assets/personas/torvalds.md +188 -0
  65. package/assets/personas/tpope.md +163 -0
  66. package/assets/personas/turing.md +188 -0
  67. package/assets/personas/whitfield_diffie.md +159 -0
  68. package/assets/personas/zeekay.md +151 -0
  69. package/dist/App.js +26 -22
  70. package/dist/Chat.js +51 -38
  71. package/dist/Column.d.ts +3 -3
  72. package/dist/Column.js +5 -27
  73. package/dist/Me.d.ts +8 -3
  74. package/dist/Me.js +79 -108
  75. package/dist/Palette.d.ts +18 -0
  76. package/dist/Palette.js +131 -0
  77. package/dist/Rail.js +3 -15
  78. package/dist/Recents.d.ts +1 -2
  79. package/dist/Recents.js +9 -9
  80. package/dist/Shell.d.ts +7 -0
  81. package/dist/Shell.js +65 -46
  82. package/dist/Top.d.ts +17 -0
  83. package/dist/Top.js +58 -0
  84. package/dist/agents.d.ts +64 -0
  85. package/dist/agents.js +62 -0
  86. package/dist/crew.d.ts +26 -5
  87. package/dist/crew.js +208 -83
  88. package/dist/help.d.ts +43 -0
  89. package/dist/help.js +58 -0
  90. package/dist/lib/web.d.ts +6 -0
  91. package/dist/lib/web.js +39 -7
  92. package/dist/recent.d.ts +2 -2
  93. package/dist/recent.js +3 -5
  94. package/dist/seek.d.ts +37 -0
  95. package/dist/seek.js +55 -0
  96. package/dist/team.js +3 -11
  97. package/dist/thread.js +7 -2
  98. package/dist/view.d.ts +44 -0
  99. package/dist/view.js +98 -0
  100. package/package.json +5 -5
  101. package/dist/Account.d.ts +0 -12
  102. package/dist/Account.js +0 -65
  103. package/dist/Find.d.ts +0 -7
  104. package/dist/Find.js +0 -82
  105. package/dist/Profile.d.ts +0 -4
  106. package/dist/Profile.js +0 -67
  107. package/dist/Standing.d.ts +0 -4
  108. package/dist/Standing.js +0 -16
package/dist/Column.js CHANGED
@@ -10,7 +10,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
10
10
  //
11
11
  // The bell is the org's run activity (@hanzo/build `useSessions`): the runs
12
12
  // that moved last, each one press from its page, and Notifications in Settings.
13
- // The search opens the filter over Recents (⌘K / Ctrl+K from anywhere, `seek`).
13
+ // The search opens the palette (Palette.tsx; ⌘K / Ctrl+K from anywhere).
14
14
  //
15
15
  // Dev's places are @hanzo/build's `nav`, the one list every rail draws, each a
16
16
  // row under a quiet group label.
@@ -19,11 +19,11 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
19
19
  // double-clicked back (@hanzo/build's `Grip`), kept per browser. Pulled in past
20
20
  // its floor it shuts (`onShut`), as the sidebar's toggle shuts it.
21
21
  import { Text, XStack, YStack } from '@hanzo/gui';
22
- import { Bell, Search, Settings2, SquarePen, X } from 'lucide-react';
22
+ import { Bell, Search, Settings2, SquarePen } from 'lucide-react';
23
23
  import { DOTS, Grip, nav, NEW, path as written, useSessions } from '@hanzo/build';
24
24
  import { DropdownMenu } from '@hanzo/ui';
25
25
  import { Sidebar, SidebarIconButton, SidebarItem, StatusDot } from '@hanzo/ui/chat';
26
- import { useEffect, useId, useRef, useState } from 'react';
26
+ import { useId } from 'react';
27
27
  import { Made } from './Me.js';
28
28
  import { Recents } from './Recents.js';
29
29
  import { Setup } from './Setup.js';
@@ -89,11 +89,7 @@ function Activity({ host }) {
89
89
  ];
90
90
  return (_jsx(DropdownMenu, { minWidth: 280, trigger: _jsx(XStack, { render: "button", "aria-label": "Activity", ...{ title: 'Activity' }, ...HEAD, items: "center", justify: "center", cursor: "pointer", opacity: 0.7, hoverStyle: { bg: '$hover', opacity: 1 }, focusVisibleStyle: { outlineWidth: 2, outlineStyle: 'solid', outlineColor: '$outlineColor' }, children: _jsx(Bell, { size: 16, "aria-hidden": true }) }), items: items }));
91
91
  }
92
- export function Column({ host, mode, thread, go, onShut, seek = 0, drawer = false, foot, places, }) {
93
- const [words, setWords] = useState('');
94
- // The search field shows once asked for, and while it holds words.
95
- const [seeking, setSeeking] = useState(false);
96
- const field = useRef(null);
92
+ export function Column({ host, mode, thread, go, onShut, onSeek, drawer = false, foot, places, }) {
97
93
  // The desktop sidebar and a phone's drawer can both be in the page: each names its own group labels.
98
94
  const uid = useId();
99
95
  const span = useSpan(SPAN, WIDTH, FLOOR, CEIL);
@@ -105,27 +101,9 @@ export function Column({ host, mode, thread, go, onShut, seek = 0, drawer = fals
105
101
  empty();
106
102
  go(talk());
107
103
  };
108
- const ask = () => {
109
- setSeeking(true);
110
- requestAnimationFrame(() => field.current?.focus());
111
- };
112
- const done = () => {
113
- setWords('');
114
- setSeeking(false);
115
- };
116
- useEffect(() => {
117
- if (seek)
118
- ask();
119
- }, [seek]);
120
104
  return (_jsxs(YStack, { "data-slot": "app-column", ref: drawer ? undefined : span.ref,
121
105
  // inline-style: `--span`, the width a drag rewrites on the element itself (span.ts); no prop sets a variable.
122
- style: drawer ? undefined : span.style, position: "relative", flex: drawer ? 1 : undefined, shrink: drawer ? 1 : 0, minW: 0, height: "100%", width: drawer ? undefined : 'var(--span)', children: [_jsxs(Sidebar, { "data-slot": "app-rail", role: "navigation", "aria-label": brand(), ...(drawer ? { ...pane(), ...FLOAT } : { bg: 'transparent', borderWidth: 0 }), ...FILL, px: "$2", pt: drawer ? '$2' : 0, pb: "$2", gap: "$0", children: [_jsxs(XStack, { "data-slot": "app-head", items: "center", gap: "$1", pl: "$2", pr: 2, height: 52, shrink: 0, minW: 0, children: [_jsx(Text, { flex: 1, minW: 0, fontSize: 18, lineHeight: 24, fontWeight: "600", color: "$ink", numberOfLines: 1, children: wordmark(mode) }), _jsx(Activity, { host: host }), mode === 'studio' ? null : (_jsx(SidebarIconButton, { label: mode === 'chat' ? 'Search chats' : 'Search runs', onPress: seeking ? done : ask, "aria-expanded": seeking || !!words, ...HEAD, children: _jsx(Search, { size: 16, "aria-hidden": true }) }))] }), seeking || words ? (_jsxs(XStack, { "data-slot": "app-filter", items: "center", gap: "$2", pl: "$3", pr: "$1", height: 36, rounded: 999, borderWidth: 1, borderColor: "$borderColor", bg: "$edge", shrink: 0, mb: "$1.5", children: [_jsx(Search, { size: 15, "aria-hidden": true, color: "var(--soft, var(--muted-foreground))" }), _jsx(Text, { render: _jsx("input", { ref: field, value: words, onChange: (e) => setWords(e.target.value), onKeyDown: (e) => {
123
- if (e.key === 'Escape')
124
- done();
125
- }, onBlur: () => {
126
- if (!words)
127
- setSeeking(false);
128
- }, placeholder: mode === 'chat' ? 'Search chats' : 'Search runs', "aria-label": "Filter recents" }), flex: 1, minW: 0, bg: "transparent", borderWidth: 0, outlineStyle: "none", color: "inherit", fontSize: "$3" }), words ? (_jsx(SidebarIconButton, { label: "Clear search", onPress: done, width: 28, height: 28, rounded: 999, children: _jsx(X, { size: 14, "aria-hidden": true }) })) : null] })) : null, mode === 'chat' ? (_jsx(YStack, { role: "list", gap: 2, shrink: 0, children: _jsx(YStack, { role: "listitem", children: _jsx(SidebarItem, { "data-slot": "app-new", ...ROW, active: !thread, bg: "transparent", hoverStyle: { bg: '$hover' }, icon: _jsx(SquarePen, { size: 16, "aria-hidden": true }), onPress: create, ...keys(create), children: _jsx(Text, { fontSize: "$3", color: "$ink", children: "New chat" }) }) }) })) : (_jsx(YStack, { "data-slot": "app-places", shrink: 0, children: groups.map((g, i) => (_jsxs(YStack, { pt: i ? '$2' : 0, children: [i ? (_jsx(Text, { id: `${uid}-${g.id}`, pl: "$2", pb: 2, fontSize: "$3", color: "$soft", children: g.label })) : null, _jsx(YStack, { role: "list", "aria-labelledby": i ? `${uid}-${g.id}` : undefined, "aria-label": i ? undefined : g.label, gap: 1, children: g.links.map((link) => (_jsx(YStack, { role: "listitem", children: _jsx(SidebarItem, { ...ROW, ...(link.id === NEW ? { 'data-slot': 'app-new' } : {}), active: link.active, icon: link.id === NEW ? _jsx(SquarePen, { size: 16, "aria-hidden": true }) : link.icon, onPress: link.onPress, ...keys(() => link.onPress?.()), children: _jsx(Text, { fontSize: "$3", color: link.active ? '$ink' : '$quiet', children: link.label }) }) }, link.id))) })] }, g.id))) })), mode === 'studio' ? null : _jsx(Recents, { host: host, mode: mode, thread: thread, words: words, onOpen: open, onNew: create }), _jsxs(YStack, { gap: "$2", pt: "$2", shrink: 0, children: [_jsx(Made, {}), _jsx(Setup, {})] }), foot] }), drawer ? null : (_jsx(Grip, { side: "left", span: span.span, floor: FLOOR, ceil: CEIL, reset: WIDTH, onSpan: span.move, onKeep: span.keep, onShut: onShut, label: "Resize sidebar",
106
+ style: drawer ? undefined : span.style, position: "relative", flex: drawer ? 1 : undefined, shrink: drawer ? 1 : 0, minW: 0, height: "100%", width: drawer ? undefined : 'var(--span)', children: [_jsxs(Sidebar, { "data-slot": "app-rail", role: "navigation", "aria-label": brand(), ...(drawer ? { ...pane(), ...FLOAT } : { bg: 'transparent', borderWidth: 0 }), ...FILL, px: "$2", pt: drawer ? '$2' : 0, pb: "$2", gap: "$0", children: [_jsxs(XStack, { "data-slot": "app-head", items: "center", gap: "$1", pl: "$2", pr: 2, height: 52, shrink: 0, minW: 0, children: [_jsx(Text, { flex: 1, minW: 0, fontSize: 18, lineHeight: 24, fontWeight: "600", color: "$ink", numberOfLines: 1, children: wordmark(mode) }), _jsx(Activity, { host: host }), _jsx(SidebarIconButton, { label: "Search", onPress: onSeek, "aria-haspopup": "dialog", ...HEAD, children: _jsx(Search, { size: 16, "aria-hidden": true }) })] }), mode === 'chat' ? (_jsx(YStack, { role: "list", gap: 2, shrink: 0, children: _jsx(YStack, { role: "listitem", children: _jsx(SidebarItem, { "data-slot": "app-new", ...ROW, active: !thread, bg: "transparent", hoverStyle: { bg: '$hover' }, icon: _jsx(SquarePen, { size: 16, "aria-hidden": true }), onPress: create, ...keys(create), children: _jsx(Text, { fontSize: "$3", color: "$ink", children: "New chat" }) }) }) })) : (_jsx(YStack, { "data-slot": "app-places", shrink: 0, children: groups.map((g, i) => (_jsxs(YStack, { pt: i ? '$2' : 0, children: [i ? (_jsx(Text, { id: `${uid}-${g.id}`, pl: "$2", pb: 2, fontSize: "$3", color: "$soft", children: g.label })) : null, _jsx(YStack, { role: "list", "aria-labelledby": i ? `${uid}-${g.id}` : undefined, "aria-label": i ? undefined : g.label, gap: 1, children: g.links.map((link) => (_jsx(YStack, { role: "listitem", children: _jsx(SidebarItem, { ...ROW, ...(link.id === NEW ? { 'data-slot': 'app-new' } : {}), active: link.active, icon: link.id === NEW ? _jsx(SquarePen, { size: 16, "aria-hidden": true }) : link.icon, onPress: link.onPress, ...keys(() => link.onPress?.()), children: _jsx(Text, { fontSize: "$3", color: link.active ? '$ink' : '$quiet', children: link.label }) }) }, link.id))) })] }, g.id))) })), mode === 'studio' ? null : _jsx(Recents, { host: host, mode: mode, thread: thread, onOpen: open, onNew: create }), _jsxs(YStack, { gap: "$2", pt: "$2", shrink: 0, children: [_jsx(Made, {}), _jsx(Setup, {})] }), foot] }), drawer ? null : (_jsx(Grip, { side: "left", span: span.span, floor: FLOOR, ceil: CEIL, reset: WIDTH, onSpan: span.move, onKeep: span.keep, onShut: onShut, label: "Resize sidebar",
129
107
  // In the gutter between the sidebar and the panel, not over the sidebar's own rows.
130
108
  r: GUTTER_END, width: "var(--pane-gap)" }))] }));
131
109
  }
package/dist/Me.d.ts CHANGED
@@ -1,4 +1,6 @@
1
1
  import { type Host } from '@hanzo/build';
2
+ /** What the menu reads of its host: who is signed in, in which organization, and how they leave. */
3
+ export type Who = Pick<Host, 'api' | 'token' | 'org' | 'person' | 'signOut'>;
2
4
  /** One organization as the menu draws it. */
3
5
  export interface Org {
4
6
  name: string;
@@ -10,7 +12,7 @@ export interface Org {
10
12
  export declare const initials: (name: string) => string;
11
13
  /** The person's picture, or their initials. */
12
14
  export declare function Face({ host, size }: {
13
- host: Host;
15
+ host: Who;
14
16
  size?: number;
15
17
  }): import("react").JSX.Element;
16
18
  /**
@@ -20,10 +22,13 @@ export declare function Face({ host, size }: {
20
22
  */
21
23
  export declare const upgrade: (family: string | null | undefined) => string;
22
24
  export declare function Me({ host, narrow, above, onOrg, }: {
23
- host: Host;
25
+ host: Who;
24
26
  /** The rail's: the face alone, the menu opening beside it. */
25
27
  narrow?: boolean;
26
- /** The menu opens above the face, not beside it: the rail laid along a phone drawer's foot. */
28
+ /**
29
+ * The menu opens above the face, not beside it: the rail laid along a phone
30
+ * drawer's foot. That copy leaves ⌘/ to the rail's own, so one press opens one dialog.
31
+ */
27
32
  above?: boolean;
28
33
  /** Work in another organization: picked, then the app opened in it. */
29
34
  onOrg: (org: string) => void;
package/dist/Me.js CHANGED
@@ -1,51 +1,62 @@
1
1
  'use client';
2
2
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- // The account row at the foot of the app's column, and the menu it opens.
3
+ // The account row and the menu it opens: the one account menu. The app's rail
4
+ // draws it for Chat, Dev and Studio (Rail.tsx) and the shell's sidebar for every
5
+ // other app (Shell.tsx).
4
6
  //
5
- // The row says who is signed in and where: the person's face or initials, their
6
- // name, and their role in the organization they work in. The menu lists every
7
- // organization the token names — the personal one first, with its plan — and
8
- // switches with `pick()` (lib/auth/session.ts), the one switch the app has.
7
+ // Top to bottom: who, with their plan, a row whose › opens the account itself;
8
+ // then Upgrade plan, Personalization, Profile and Settings (⌘, / Ctrl+,); then
9
+ // Help ›, the site's own pages and the keyboard shortcuts (⌘/ / Ctrl+/ from
10
+ // anywhere); and Log out. Every row opens a real destination: Settings'
11
+ // sections through `showSettings` (open.ts), which the app answers with the
12
+ // builder's Settings (dev.tsx) and the shell with its own pane; the plans on the
13
+ // pay site (`leave`); the site's pages through `site()`, so hanzo.team names
14
+ // hanzo.ai (help.ts).
9
15
  //
10
- // THE PLAN LEADS: @hanzo/build's `Meter`, the block every account menu opens
11
- // with — the plan by its family (`Max` with `20x` beside it) and, on a plan, its
12
- // windows as shares; the free allowance left today on Free; the balance only
13
- // for an account with no plan, whose meter it is. Credits are Billing's, so a
14
- // plan never sits beside a balance here.
15
- //
16
- // Under the list: Create organization, Organization settings, and Hanzo Team —
17
- // opened in the same organization for a team, offered as "Create a team" for a
18
- // personal one. Then Settings, Usage, Billing, API keys, the plans, help and Log
19
- // out. A SuperAdmin also gets All organizations (support mode).
16
+ // THE ACCOUNT'S › LEADS WITH THE PLAN: @hanzo/build's `Meter`, the plan by its
17
+ // family (`Max` with `20x` beside it) and, on a plan, its windows as shares; the
18
+ // free allowance left today on Free; the balance only for an account with no
19
+ // plan, whose meter it is. Then every organization the token names — the
20
+ // personal one first, with its plan — switched with `onOrg`; Create
21
+ // organization, Organization settings, and Hanzo Team (opened in the same
22
+ // organization for a team, offered as "Create a team" for a personal one); and
23
+ // All organizations for a SuperAdmin (support mode).
20
24
  //
21
25
  // IAM is the source of every fact here: names and roles from the token's `orgs`
22
26
  // claim, display names and which org is personal from `GET /v1/iam/organizations`,
23
27
  // the plan from billing and the limits. An organization is created by the same
24
- // `POST /v1/account/orgs` the no-organization gate uses (components/workspace/
25
- // Orgs.tsx); the next load mints a token that carries it.
28
+ // `POST /v1/account/orgs` the no-organization gate uses (Orgs.tsx); the next load
29
+ // mints a token that carries it.
26
30
  import { useEffect, useMemo, useRef, useState } from 'react';
27
- import { Popover, SizableText, usePopperContextSlow, View, XStack, YStack } from '@hanzo/gui';
28
- import { Building2, ChevronLeft, ChevronRight, ChevronsUpDown, CreditCard, ExternalLink, Gauge, KeyRound, LifeBuoy, LogOut, Mail, Plus, Settings, Sparkles, Users, X } from 'lucide-react';
29
- import { label, Meter, path, said as spoken, useStanding, useWho } from '@hanzo/build';
31
+ import { SizableText, XStack, YStack } from '@hanzo/gui';
32
+ import { ArrowUpRight, Building2, Check, ChevronsUpDown, CircleHelp, CircleUserRound, Download, FileText, Info, Keyboard, LifeBuoy, LogOut, Palette, Plus, ScrollText, Settings, ShieldCheck, Sparkles, Users, X, } from 'lucide-react';
33
+ import { label, Meter, said as spoken, useStanding } from '@hanzo/build';
30
34
  import { useOrganizations } from '@hanzo/iam/react';
31
- import { Button, Dialog, DialogContent, DialogTitle, Input } from '@hanzo/ui';
32
- import { glass } from '@hanzo/ui/glass';
35
+ import { Button, Dialog, DialogContent, DialogTitle, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, Input, Kbd, } from '@hanzo/ui';
33
36
  import { api } from './lib/api.js';
34
37
  import { createOrg, say } from './lib/org.js';
35
38
  import { list } from './lib/list.js';
36
39
  import { org as current, orgs, pick, renew, scope, superAdmin } from './lib/session.js';
37
40
  import { ENTRY } from './lib/host.js';
41
+ import { leave } from './dev.js';
42
+ import { asked, KEYS, legal, out, pages } from './help.js';
43
+ import { showSettings } from './open.js';
38
44
  import { site, where } from './where.js';
39
45
  import { SupportPicker } from './Support.js';
40
46
  import { BAD } from './lib/mix.js';
41
- /** The documentation Get help opens. */
42
- const DOCS = 'https://docs.hanzo.ai/docs/dev';
43
47
  /** Where an organization's own settings live: a page of the site (`site()`), not of the app. */
44
48
  const ORG_SETTINGS = '/settings/organization';
45
- /** The organization's API keys: one home, the platform's keys page, never a second copy in an app. */
46
- const KEYS = 'https://platform.hanzo.ai/api-keys';
47
49
  /** An organization this tab created, until the token carries it. */
48
50
  const MADE = 'hanzo:org:made';
51
+ /** Each Help page's icon, by its key in help.ts. */
52
+ const ICONS = {
53
+ help: _jsx(CircleHelp, { size: 16, "aria-hidden": true }),
54
+ center: _jsx(ShieldCheck, { size: 16, "aria-hidden": true }),
55
+ releases: _jsx(ScrollText, { size: 16, "aria-hidden": true }),
56
+ download: _jsx(Download, { size: 16, "aria-hidden": true }),
57
+ terms: _jsx(FileText, { size: 16, "aria-hidden": true }),
58
+ privacy: _jsx(Info, { size: 16, "aria-hidden": true }),
59
+ };
49
60
  /** A role as a person reads it: `owner` → Owner. */
50
61
  const said = (role) => (role ? role.charAt(0).toUpperCase() + role.slice(1) : '');
51
62
  /** The first letters of a name, at most two: Antje Worring → AW. */
@@ -108,48 +119,12 @@ export function Face({ host, size = 28 }) {
108
119
  const p = host.person;
109
120
  return (_jsx(XStack, { width: size, height: size, rounded: 9999, items: "center", justify: "center", bg: "$raised", overflow: "hidden", shrink: 0, borderWidth: 1, borderColor: "$borderColor", children: p?.avatar ? (_jsx("img", { src: p.avatar, alt: "", width: size, height: size })) : (_jsx(SizableText, { size: "$1", fontWeight: "600", color: "$ink", children: initials(p?.name || p?.email || '') })) }));
110
121
  }
111
- /** One row of the menu: a real button, so Enter, Space and focus are the browser's. */
112
- function Item({ icon, label, sub, checked, onPress, trail, }) {
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: 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] }));
122
+ /** A row's face: its icon in a 16px well, its words, and what trails them. */
123
+ function Line({ icon, children, sub, trail }) {
124
+ return (_jsxs(_Fragment, { children: [_jsx(XStack, { width: 16, height: 16, items: "center", justify: "center", shrink: 0, children: icon }), _jsxs(YStack, { flex: 1, minW: 0, children: [_jsx(SizableText, { size: "$2", color: "$ink", numberOfLines: 1, children: children }), sub ? (_jsx(SizableText, { size: "$1", color: "$soft", numberOfLines: 1, children: sub })) : null] }), trail] }));
115
125
  }
116
- const Rule = () => _jsx(XStack, { height: 1, bg: "$borderColor", my: "$1", mx: "$1" });
117
- const Label = ({ children }) => (_jsx(SizableText, { size: "$1", color: "$soft", px: "$2", pt: "$1.5", pb: "$1", children: children }));
118
126
  /** An organization's mark in the list: its initials on a rounded square. */
119
- const Mark = ({ org }) => (_jsx(XStack, { width: 20, height: 20, rounded: "$2", items: "center", justify: "center", bg: "$raised", shrink: 0, children: org.personal ? (_jsx(SizableText, { size: "$1", fontWeight: "600", color: "$ink", children: initials(org.display) })) : (_jsx(Building2, { size: 12, "aria-hidden": true })) }));
120
- /** Arrows, Home and End move between the menu's rows; Escape closes it. */
121
- function rove(e, close) {
122
- const rows = [...e.currentTarget.querySelectorAll('[data-item]')];
123
- const at = rows.indexOf(document.activeElement);
124
- const to = e.key === 'ArrowDown' ? (at + 1) % rows.length : e.key === 'ArrowUp' ? (at - 1 + rows.length) % rows.length : e.key === 'Home' ? 0 : e.key === 'End' ? rows.length - 1 : -2;
125
- if (e.key === 'Escape') {
126
- e.preventDefault();
127
- close();
128
- return;
129
- }
130
- if (to === -2 || !rows.length)
131
- return;
132
- e.preventDefault();
133
- rows[to]?.focus();
134
- }
135
- /**
136
- * The menu grows once the plan is read (`Meter`), after it was placed, and the
137
- * popover follows its anchor, not its own size. So it is placed again whenever
138
- * its content changes size, and never hangs over the row it opens from.
139
- */
140
- function Refit({ children }) {
141
- const { update } = usePopperContextSlow();
142
- const box = useRef(null);
143
- useEffect(() => {
144
- const node = box.current;
145
- if (!node || typeof ResizeObserver === 'undefined')
146
- return;
147
- const watch = new ResizeObserver(() => update());
148
- watch.observe(node);
149
- return () => watch.disconnect();
150
- }, [update]);
151
- return (_jsx(View, { render: _jsx("div", { ref: box }), self: "stretch", minW: 0, children: children }));
152
- }
127
+ const Mark = ({ org }) => (_jsx(XStack, { width: 16, height: 16, rounded: "$2", items: "center", justify: "center", bg: "$raised", shrink: 0, children: org.personal ? (_jsx(SizableText, { size: "$1", fontWeight: "600", color: "$ink", children: initials(org.display).charAt(0) })) : (_jsx(Building2, { size: 12, "aria-hidden": true })) }));
153
128
  /**
154
129
  * The plans row, named by the plan the organization is on: Enterprise and
155
130
  * Agency are plans a person stays on, so the row names theirs; every other
@@ -158,66 +133,62 @@ function Refit({ children }) {
158
133
  export const upgrade = (family) => family === 'Enterprise' ? 'Enterprise plan' : family === 'Agency' ? 'Agency plan' : 'Upgrade plan';
159
134
  /** The keys a row prints, in this platform's spelling. */
160
135
  const mac = () => typeof navigator !== 'undefined' && /Mac|iPhone|iPad/.test(navigator.platform || navigator.userAgent);
161
- /** A row's keys at its end, quiet. */
162
- const Keys = ({ children }) => (_jsx(SizableText, { size: "$1", color: "$soft", shrink: 0, children: children }));
163
- /** A row that opens a page of the menu. */
164
- const More = () => _jsx(ChevronRight, { size: 14, "aria-hidden": true, opacity: 0.6 });
165
136
  export function Me({ host, narrow = false, above = false, onOrg, }) {
166
- const menu = useWho();
167
- const [page, setPage] = useState('main');
137
+ const [open, setOpen] = useState(false);
138
+ const [account, setAccount] = useState(false);
139
+ const [help, setHelp] = useState(false);
168
140
  const [creating, setCreating] = useState(false);
169
141
  const [supporting, setSupporting] = useState(false);
142
+ const [keys, setKeys] = useState(false);
170
143
  const row = useRef(null);
171
- const content = useRef(null);
172
144
  const current = host.org;
173
145
  const all = useOrgs(current);
174
146
  const here = all.find((o) => o.name === current) ?? null;
175
147
  const personal = all.find((o) => o.personal) ?? null;
176
148
  const target = useMemo(() => ({ api: host.api, token: host.token, org: host.org }), [host.api, host.token, host.org]);
177
- const read = useStanding(target, menu.open);
178
- const other = usePlan(personal && personal.name !== current ? personal.name : null, menu.open);
149
+ const read = useStanding(target, open);
150
+ const other = usePlan(personal && personal.name !== current ? personal.name : null, open);
179
151
  const planFor = (o) => (o.name === current ? spoken(read.value?.label ?? null) : o.personal ? other : '');
180
152
  const who = host.person?.name || host.person?.email || '';
181
153
  const plan = spoken(read.value?.label ?? null);
182
154
  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.
185
- const land = (node) => {
186
- content.current = node;
187
- if (!node)
155
+ useEffect(() => {
156
+ if (above)
188
157
  return;
189
- requestAnimationFrame(() => requestAnimationFrame(() => (node.querySelector('[data-item][aria-checked="true"]') ?? node.querySelector('[data-item]'))?.focus()));
190
- };
191
- const turn = (to) => {
192
- setPage(to);
193
- requestAnimationFrame(() => land(content.current));
194
- };
195
- const toggle = () => {
196
- setPage('main');
197
- menu.toggle();
198
- };
199
- const close = () => {
200
- menu.onOpenChange(false);
201
- row.current?.focus();
202
- };
203
- const go = (p) => {
204
- menu.onOpenChange(false);
205
- host.go(p);
158
+ const press = (e) => {
159
+ if (!asked(e))
160
+ return;
161
+ e.preventDefault();
162
+ setOpen(false);
163
+ setKeys(true);
164
+ };
165
+ window.addEventListener('keydown', press);
166
+ return () => window.removeEventListener('keydown', press);
167
+ }, [above]);
168
+ // Focus goes back to the row only when nothing the menu opened (a dialog) took it.
169
+ const back = (e) => {
170
+ e.preventDefault();
171
+ requestAnimationFrame(() => {
172
+ const now = document.activeElement;
173
+ if (!now || now === document.body)
174
+ row.current?.focus();
175
+ });
206
176
  };
207
- const back = (title) => _jsx(Item, { icon: _jsx(ChevronLeft, { size: 15, "aria-hidden": true }), label: title, onPress: () => turn('main') });
208
177
  if (!host.person)
209
178
  return null;
210
- return (_jsxs(_Fragment, { children: [_jsxs(Popover, { open: menu.open, onOpenChange: (open) => {
211
- if (open)
212
- setPage('main');
213
- menu.onOpenChange(open);
214
- }, placement: narrow && !above ? 'right-end' : above ? 'top-end' : 'top-start', allowFlip: true, stayInFrame: true, offset: 8, children: [_jsx(Popover.Anchor, { width: narrow ? undefined : '100%', children: _jsxs(XStack, { ref: (el) => {
179
+ return (_jsxs(_Fragment, { children: [_jsxs(DropdownMenu, { open: open, onOpenChange: (next) => {
180
+ setOpen(next);
181
+ if (next)
182
+ return;
183
+ setAccount(false);
184
+ setHelp(false);
185
+ }, placement: narrow && !above ? 'right-end' : above ? 'top-end' : 'top-start', children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsxs(XStack, { ref: (el) => {
215
186
  row.current = el instanceof HTMLElement ? el : null;
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: () => {
217
- menu.onOpenChange(false);
218
- if (o.name !== current)
219
- onOrg(o.name);
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] }));
187
+ }, render: "button", "data-slot": "me", "aria-label": `Account: ${[who, current].filter(Boolean).join(' · ')}`, ...{ title: who }, width: narrow ? 36 : '100%', minH: narrow ? 36 : 48, px: narrow ? 0 : '$2', gap: "$2.5", rounded: narrow ? 999 : '$5', items: "center", justify: narrow ? 'center' : 'flex-start', hoverStyle: { bg: '$hover' }, focusVisibleStyle: { outlineWidth: 2, outlineStyle: 'solid', outlineColor: '$outlineColor' }, cursor: "pointer", children: [_jsx(Face, { host: host, size: narrow ? 26 : 32 }), narrow ? null : (_jsxs(_Fragment, { children: [_jsxs(YStack, { flex: 1, minW: 0, children: [_jsx(SizableText, { size: "$3", fontWeight: "600", color: "$ink", numberOfLines: 1, text: "left", children: who }), plan ? (_jsx(SizableText, { size: "$1", color: "$soft", numberOfLines: 1, text: "left", children: plan })) : null] }), _jsx(ChevronsUpDown, { size: 14, "aria-hidden": true, opacity: 0.6 })] }))] }) }), _jsxs(DropdownMenuContent, { minW: 260, maxW: "calc(100vw - 16px)", "aria-labelledby": undefined, "aria-label": "Account", onCloseAutoFocus: back, children: [_jsxs(DropdownMenuSub, { open: account, onOpenChange: setAccount, children: [_jsxs(DropdownMenuSubTrigger, { minH: 48, gap: "$2.5", children: [_jsx(Face, { host: host, size: 28 }), _jsxs(YStack, { flex: 1, minW: 0, children: [_jsx(SizableText, { size: "$2", fontWeight: "600", color: "$ink", numberOfLines: 1, children: who }), _jsx(SizableText, { size: "$1", color: "$soft", numberOfLines: 1, children: plan || host.person.email || '' })] })] }), account ? (_jsxs(DropdownMenuSubContent, { minW: 260, maxW: "calc(100vw - 16px)", children: [_jsx(Meter, { read: read, onPlans: () => leave('here'), onBilling: () => (setOpen(false), showSettings('billing')) }), _jsx(DropdownMenuSeparator, {}), _jsx(DropdownMenuLabel, { children: "Organizations" }), all.map((o) => (_jsx(DropdownMenuItem, { onSelect: () => o.name !== current && onOrg(o.name), children: _jsx(Line, { icon: _jsx(Mark, { org: o }), sub: [o.personal ? 'Personal' : said(o.role), planFor(o)].filter(Boolean).join(' · '), trail: o.name === current ? _jsx(Check, { size: 16, "aria-label": "Current" }) : null, children: o.display }) }, o.name))), _jsx(DropdownMenuItem, { onSelect: () => setCreating(true), children: _jsx(Line, { icon: _jsx(Plus, { size: 16, "aria-hidden": true }), children: "Create organization" }) }), _jsx(DropdownMenuItem, { onSelect: () => window.location.assign(site(ORG_SETTINGS)), children: _jsx(Line, { icon: _jsx(Settings, { size: 16, "aria-hidden": true }), children: "Organization settings" }) }), where().team ? null : here && !here.personal ? (_jsx(DropdownMenuItem, { onSelect: () => out(`${ENTRY}&org=${encodeURIComponent(here.name)}`), children: _jsx(Line, { icon: _jsx(Users, { size: 16, "aria-hidden": true }), trail: _jsx(ArrowUpRight, { size: 14, "aria-hidden": true, opacity: 0.6 }), children: "Open in Hanzo Team" }) })) : (_jsx(DropdownMenuItem, { onSelect: () => setCreating(true), children: _jsx(Line, { icon: _jsx(Users, { size: 16, "aria-hidden": true }), children: "Create a team" }) })), superAdmin() ? (_jsx(DropdownMenuItem, { onSelect: () => setSupporting(true), children: _jsx(Line, { icon: _jsx(Building2, { size: 16, "aria-hidden": true }), children: "All organizations" }) })) : null] })) : null] }), _jsx(DropdownMenuSeparator, {}), _jsx(DropdownMenuItem, { onSelect: () => leave('here'), children: _jsx(Line, { icon: _jsx(Sparkles, { size: 16, "aria-hidden": true }), children: upgrade(read.value?.label?.name) }) }), _jsx(DropdownMenuItem, { onSelect: () => showSettings('general'), children: _jsx(Line, { icon: _jsx(Palette, { size: 16, "aria-hidden": true }), children: "Personalization" }) }), _jsx(DropdownMenuItem, { onSelect: () => showSettings('account'), children: _jsx(Line, { icon: _jsx(CircleUserRound, { size: 16, "aria-hidden": true }), children: "Profile" }) }), _jsx(DropdownMenuItem, { "aria-keyshortcuts": "Control+, Meta+,", onSelect: () => showSettings('general'), children: _jsx(Line, { icon: _jsx(Settings, { size: 16, "aria-hidden": true }), trail: _jsx(DropdownMenuShortcut, { "aria-hidden": true, children: apple ? '⌘,' : 'Ctrl+,' }), children: "Settings" }) }), _jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuSub, { open: help, onOpenChange: setHelp, children: [_jsx(DropdownMenuSubTrigger, { children: _jsx(Line, { icon: _jsx(LifeBuoy, { size: 16, "aria-hidden": true }), children: "Help" }) }), help ? (_jsxs(DropdownMenuSubContent, { minW: 240, maxW: "calc(100vw - 16px)", children: [pages().map((p) => (_jsx(DropdownMenuItem, { onSelect: () => out(p.href), children: _jsx(Line, { icon: ICONS[p.key], trail: p.key === 'help' ? _jsx(ArrowUpRight, { size: 14, "aria-hidden": true, opacity: 0.6 }) : null, children: p.label }) }, p.key))), _jsx(DropdownMenuItem, { "aria-keyshortcuts": "Control+/ Meta+/", onSelect: () => setKeys(true), children: _jsx(Line, { icon: _jsx(Keyboard, { size: 16, "aria-hidden": true }), trail: _jsx(DropdownMenuShortcut, { "aria-hidden": true, children: apple ? '⌘/' : 'Ctrl+/' }), children: "Keyboard shortcuts" }) }), _jsx(DropdownMenuSeparator, {}), legal().map((p) => (_jsx(DropdownMenuItem, { onSelect: () => out(p.href), children: _jsx(Line, { icon: ICONS[p.key], children: p.label }) }, p.key)))] })) : null] }), host.signOut ? (_jsx(DropdownMenuItem, { onSelect: () => host.signOut?.(), children: _jsx(Line, { icon: _jsx(LogOut, { size: 16, "aria-hidden": true }), children: "Log out" }) })) : null] })] }), _jsx(Create, { open: creating, onOpenChange: setCreating }), _jsx(Shortcuts, { open: keys, onOpenChange: setKeys, apple: apple }), supporting ? _jsx(SupportPicker, { onClose: () => setSupporting(false) }) : null] }));
188
+ }
189
+ /** The app's keys (help.ts `KEYS`), one row each, spelled for this platform. */
190
+ function Shortcuts({ open, onOpenChange, apple }) {
191
+ return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { maxW: 440, rounded: "$6", gap: "$3", children: [_jsx(DialogTitle, { children: "Keyboard shortcuts" }), _jsx(YStack, { gap: "$1", children: KEYS.map((k) => (_jsxs(XStack, { items: "center", justify: "space-between", gap: "$3", minH: 32, children: [_jsx(SizableText, { size: "$2", color: "$ink", children: k.does }), _jsx(Kbd, { children: apple ? k.mac : k.pc })] }, k.does))) })] }) }));
221
192
  }
222
193
  /**
223
194
  * The sheet that creates an organization: a name, then `POST /v1/account/orgs`,
@@ -0,0 +1,18 @@
1
+ import { type ReactNode } from 'react';
2
+ import { type ShortcutKeys } from '@hanzo/ui';
3
+ import { type Hit } from './seek.js';
4
+ /** A row: what it is called, its glyph, and what pressing it does. */
5
+ export interface Action extends Hit {
6
+ icon?: ReactNode;
7
+ shortcut?: ShortcutKeys;
8
+ run: () => void;
9
+ /** Pressing it keeps the palette open. */
10
+ stay?: boolean;
11
+ }
12
+ export declare function Palette({ onClose, go, actions, }: {
13
+ onClose: () => void;
14
+ /** Moves the app to one of its addresses (lib/host `talk`, `app`). */
15
+ go: (address: string) => void;
16
+ /** The frame's own actions, after the palette's; one with a palette action's id stands in for it. */
17
+ actions?: Action[];
18
+ }): import("react").JSX.Element;
@@ -0,0 +1,131 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ // The one search. ⌘K / Ctrl+K from anywhere and the sidebar's search open it:
4
+ // a dialog with the field on top, listing Recent chats while the field is empty
5
+ // and, for a query, Chats, Projects, Agents, Skills and Actions, each cut to its
6
+ // best five (seek.ts `find`). A row opens where it lives: a chat at its
7
+ // address, a project in Dev, an agent in a new chat with it, a skill in Dev's
8
+ // Customize, an action runs. Arrows move, Enter opens, Escape closes
9
+ // (@hanzo/ui's CommandMenu); Keyboard shortcuts lists the app's keys, and
10
+ // Backspace on an empty field comes back.
11
+ //
12
+ // MOUNTED ONLY WHILE OPEN, and every list is read once as it opens, then
13
+ // searched here as the reader types: the conversations store (hanzoai/agent)
14
+ // answers its whole list and searches nothing, titles or words, so there is no
15
+ // read per key to wait for or to cancel. The chats are the sidebar's last read
16
+ // (thread.tsx `useThreadList`), drawn at once and read again behind it; the
17
+ // projects (`/v1/projects`), the active skills (`/v1/tool/skills?activated=true`)
18
+ // and the agents (crew.tsx) arrive while the reader types, and a read the
19
+ // palette closes on is aborted.
20
+ import { useEffect, useMemo, useState } from 'react';
21
+ import { BookOpen, Bot, Code2, FolderGit2, Keyboard, LogOut, MessageCircle, Puzzle, Settings, SquarePen } from 'lucide-react';
22
+ import { useAi } from '@hanzo/ai/react';
23
+ import { path as written } from '@hanzo/build';
24
+ import { Dialog, DialogContent, DialogTitle } from '@hanzo/ui';
25
+ import { CommandMenu } from '@hanzo/ui/product/CommandMenu';
26
+ import { ordered } from './agents.js';
27
+ import { useCrew } from './crew.js';
28
+ import { called } from './team.js';
29
+ import { empty, openThread, withAgents } from './open.js';
30
+ import { useThreadList } from './thread.js';
31
+ import { useSignOut } from './lib/signout.js';
32
+ import { app, talk } from './lib/host.js';
33
+ import { find } from './seek.js';
34
+ const ICON = { size: 16, 'aria-hidden': true };
35
+ /** The app's keys, as Keyboard shortcuts lists them. */
36
+ const KEYS = [
37
+ { id: 'search', label: 'Search', shortcut: ['Mod', 'K'] },
38
+ { id: 'settings', label: 'Settings', shortcut: ['Mod', ','] },
39
+ { id: 'panel', label: 'Side panel', shortcut: ['Mod', '.'] },
40
+ { id: 'rename', label: 'Rename chat', shortcut: ['Ctrl', 'Alt', 'R'] },
41
+ { id: 'pin', label: 'Pin or unpin chat', shortcut: ['Ctrl', 'Alt', 'P'] },
42
+ { id: 'back', label: 'Back, in the desktop app', shortcut: ['Alt', 'ArrowLeft'] },
43
+ { id: 'forward', label: 'Forward, in the desktop app', shortcut: ['Alt', 'ArrowRight'] },
44
+ ];
45
+ const SKILLS = written({ kind: 'customize', tab: 'skills' });
46
+ /** A list read once as the palette opens; a refusal leaves it empty and the rest still answer. */
47
+ function useRead(path, shape) {
48
+ const ai = useAi();
49
+ const [rows, setRows] = useState([]);
50
+ useEffect(() => {
51
+ const stop = new AbortController();
52
+ ai.http.json({ path, signal: stop.signal }).then((body) => setRows(shape(body)), () => { });
53
+ return () => stop.abort();
54
+ // `shape` is a module constant at each call.
55
+ // eslint-disable-next-line react-hooks/exhaustive-deps
56
+ }, [ai, path]);
57
+ return rows;
58
+ }
59
+ const obj = (v) => (v && typeof v === 'object' ? v : {});
60
+ const str = (v) => (typeof v === 'string' ? v : '');
61
+ const rows = (v) => (Array.isArray(v) ? v : []);
62
+ /** `/v1/projects`: a project by its slug, found by its name, slug and repository. */
63
+ const projects = (body) => (Array.isArray(body) ? body : rows(obj(body).data))
64
+ .map(obj)
65
+ .filter((p) => str(p.slug))
66
+ .map((p) => ({ id: str(p.slug), label: str(p.name) || str(p.slug), keywords: [str(p.slug), str(obj(p.repo).url)] }));
67
+ /** `/v1/tool/skills?activated=true`: a skill by its name, its tool's less `skill_`. */
68
+ const skills = (body) => rows(obj(body).tools)
69
+ .map(obj)
70
+ .filter((t) => str(t.name))
71
+ .map((t) => ({ id: str(t.name), label: str(t.name).replace(/^skill_/, ''), keywords: [str(t.description)] }));
72
+ export function Palette({ onClose, go, actions = [], }) {
73
+ const [query, setQuery] = useState('');
74
+ const [keys, setKeys] = useState(false);
75
+ const { threads } = useThreadList();
76
+ const { agents } = useCrew();
77
+ const made = useRead('/v1/projects', projects);
78
+ const known = useRead('/v1/tool/skills?activated=true', skills);
79
+ const signOut = useSignOut();
80
+ const groups = useMemo(() => {
81
+ const own = [
82
+ { id: 'new-chat', label: 'New chat', icon: _jsx(SquarePen, { ...ICON }), run: () => (empty(), go(talk())) },
83
+ { id: 'new-task', label: 'New Dev task', icon: _jsx(Code2, { ...ICON }), run: () => go(app('')) },
84
+ { id: 'settings', label: 'Settings', icon: _jsx(Settings, { ...ICON }), shortcut: ['Mod', ','], run: () => go(app(written({ kind: 'settings', section: 'general' }))) },
85
+ { id: 'customize', label: 'Customize', icon: _jsx(Puzzle, { ...ICON }), run: () => go(app(SKILLS)) },
86
+ { id: 'keys', label: 'Keyboard shortcuts', icon: _jsx(Keyboard, { ...ICON }), stay: true, run: () => (setQuery(''), setKeys(true)) },
87
+ { id: 'logout', label: 'Log out', icon: _jsx(LogOut, { ...ICON }), run: () => void signOut() },
88
+ ];
89
+ const doing = [...own.map((a) => actions.find((b) => b.id === a.id) ?? a), ...actions.filter((b) => !own.some((a) => a.id === b.id))];
90
+ const chats = threads.map((t) => ({
91
+ id: `chat-${t.id}`,
92
+ label: t.title || 'Untitled',
93
+ at: t.updatedAt,
94
+ icon: _jsx(MessageCircle, { ...ICON }),
95
+ run: () => (openThread(t.id), go(talk(t.id))),
96
+ }));
97
+ const shown = find(query, chats, [
98
+ { id: 'projects', label: 'Projects', items: made.map((p) => ({ ...p, id: `project-${p.id}`, icon: _jsx(FolderGit2, { ...ICON }), run: () => go(app(p.id)) })) },
99
+ {
100
+ id: 'agents',
101
+ label: 'Agents',
102
+ items: ordered(agents).map((one) => {
103
+ const name = called(one.name);
104
+ return { id: `agent-${one.id || one.name}`, label: name, icon: _jsx(Bot, { ...ICON }), run: () => (withAgents([name]), go(talk())) };
105
+ }),
106
+ },
107
+ { id: 'skills', label: 'Skills', items: known.map((s) => ({ ...s, id: `skill-${s.id}`, icon: _jsx(BookOpen, { ...ICON }), run: () => go(app(SKILLS)) })) },
108
+ { id: 'actions', label: 'Actions', items: doing },
109
+ ]);
110
+ return shown.map((g) => ({
111
+ ...g,
112
+ items: g.items.map((a) => ({
113
+ id: a.id,
114
+ label: a.label,
115
+ icon: a.icon,
116
+ shortcut: a.shortcut,
117
+ // The palette ranked these against the query; the menu's own filter passes what it kept.
118
+ keywords: [query],
119
+ onSelect: () => {
120
+ if (!a.stay)
121
+ onClose();
122
+ a.run();
123
+ },
124
+ })),
125
+ }));
126
+ }, [query, threads, agents, made, known, actions, go, onClose, signOut]);
127
+ const listed = keys
128
+ ? [{ id: 'keys', label: 'Keyboard shortcuts', items: KEYS.map((k) => ({ ...k, id: `key-${k.id}`, onSelect: () => setKeys(false) })) }]
129
+ : groups;
130
+ return (_jsx(Dialog, { open: true, onOpenChange: (open) => (open ? null : onClose()), children: _jsxs(DialogContent, { "data-slot": "palette", p: 0, gap: 0, width: "100%", maxW: 680, height: "min(520px, calc(100dvh - 32px))", rounded: "$6", overflow: "hidden", "$max-md": { width: '100vw', maxW: '100vw', height: '100dvh', rounded: 0, borderWidth: 0 }, children: [_jsx(DialogTitle, { position: "absolute", width: 1, height: 1, overflow: "hidden", opacity: 0, children: "Search" }), _jsx(CommandMenu, { groups: listed, query: query, onQueryChange: setQuery, placeholder: "Search\u2026", label: "Search", empty: keys ? 'No such key.' : query.trim() ? `Nothing matches “${query.trim()}”.` : 'No chats yet.', onBack: keys ? () => setKeys(false) : undefined, onClose: onClose })] }) }));
131
+ }
package/dist/Rail.js CHANGED
@@ -3,7 +3,8 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
3
3
  // The app's rail: its products as marks down the window's left edge, in one
4
4
  // order — Chat, Dev, Studio, Library, Scheduled tasks, Customize — then More, a
5
5
  // menu of the rest, and the account at the foot. It is how the app moves
6
- // between its products; there is no switch anywhere else.
6
+ // between its products; a phone, which has no rail, switches Chat and Dev in
7
+ // its bar (Top.tsx).
7
8
  //
8
9
  // Studio is the app's third mode (@hanzo/studio at /studio), each of the three
9
10
  // opened where the reader left it.
@@ -26,26 +27,13 @@ import { Boxes, Clock, Code2, Ellipsis, FolderGit2, LayoutTemplate, Library, Lis
26
27
  import { XStack, YStack } from '@hanzo/gui';
27
28
  import { route } from '@hanzo/build';
28
29
  import { DropdownMenu } from '@hanzo/ui';
30
+ import { out } from './help.js';
29
31
  import { Me } from './Me.js';
30
32
  import { app, TABS, WORKSPACE } from './lib/host.js';
31
33
  /** The rail's width, px. */
32
34
  export const RAIL = 48;
33
35
  /** A mark, px: a 36px square on a 44px pitch. */
34
36
  const MARK = 36;
35
- /**
36
- * Opens an address of another product in a tab of its own, through an anchor,
37
- * so a host that sends links out of its window (the desktop's system browser)
38
- * hears it as the link it is.
39
- */
40
- function out(href) {
41
- const a = document.createElement('a');
42
- a.href = href;
43
- a.target = '_blank';
44
- a.rel = 'noopener noreferrer';
45
- document.body.appendChild(a);
46
- a.click();
47
- a.remove();
48
- }
49
37
  /** The places the rail raises out of Dev, by the builder address each opens. */
50
38
  const RAISED = { library: '-/artifacts', scheduled: '-/automations', customize: '-/customize' };
51
39
  /** Which raised place a builder address stands on, if any. */
package/dist/Recents.d.ts CHANGED
@@ -1,11 +1,10 @@
1
1
  import { type Host } from '@hanzo/build';
2
2
  import { type Mode, type Recent } from './recent.js';
3
- export declare function Recents({ host, mode, thread, words, onOpen, onNew, }: {
3
+ export declare function Recents({ host, mode, thread, onOpen, onNew, }: {
4
4
  host: Host;
5
5
  mode: Mode;
6
6
  /** The Chat conversation the pane holds, or null. */
7
7
  thread: string | null;
8
- words: string;
9
8
  onOpen: (r: Recent) => void;
10
9
  /** Moves the pane to a new conversation: where it goes when the one it holds is deleted. */
11
10
  onNew: () => void;