@hanzo/rooms 0.1.99 → 0.1.101

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/App.js CHANGED
@@ -62,7 +62,7 @@ import { lazy, Suspense, useCallback, useEffect, useMemo, useRef, useState } fro
62
62
  import { ArrowLeft, ArrowRight } from 'lucide-react';
63
63
  import { AiProvider } from '@hanzo/ai/react';
64
64
  import { XStack, YStack } from '@hanzo/gui';
65
- import { Builder, HostProvider, PrefsProvider, path as written, route } from '@hanzo/build';
65
+ import { Builder, HostProvider, PrefsProvider, route } from '@hanzo/build';
66
66
  import { nav as places } from '@hanzo/studio';
67
67
  import { useAi } from './lib/ai.js';
68
68
  import { Chat } from './Chat.js';
@@ -71,15 +71,15 @@ import { isPlans, leave, useDevHost } from './dev.js';
71
71
  import { Orgs } from './Orgs.js';
72
72
  import { useRooms } from './host.js';
73
73
  import { useKept } from './kept.js';
74
- import { Mark, Rail, spot, Toggle } from './Rail.js';
74
+ import { Mark, raised, Rail, Toggle } from './Rail.js';
75
75
  import { Foot, Layer, Lift, RAISED, useDrawer } from './Drawer.js';
76
- import { empty, openThread, useOpen } from './open.js';
76
+ import { empty, openThread, showSettings, useOpen } from './open.js';
77
77
  import { Paned } from './Shell.js';
78
78
  import { Palette } from './Palette.js';
79
79
  import { summons } from './seek.js';
80
80
  import { floor, pane } from './ground.js';
81
81
  import { pick } from './lib/session.js';
82
- import { app, go, place, studio, talk, TERMINALS } from './lib/host.js';
82
+ import { app, go, place, settings, studio, talk, TERMINALS, visit } from './lib/host.js';
83
83
  import { Top } from './Top.js';
84
84
  import { FOOT, HEIGHT, TOP, useView } from './view.js';
85
85
  /** Studio, fetched the first time the reader stands in it. */
@@ -97,15 +97,13 @@ const typing = (e) => {
97
97
  const el = e.target;
98
98
  return !!el && (el.isContentEditable || el.tagName === 'INPUT' || el.tagName === 'TEXTAREA' || el.tagName === 'SELECT');
99
99
  };
100
- /** The builder's Settings, general section. */
101
- const SETTINGS = written({ kind: 'settings', section: 'general' });
102
100
  export function App({ bar } = {}) {
103
101
  return (_jsx(Orgs, { children: _jsx(Window, { bar: bar }) }));
104
102
  }
105
103
  /** The window itself, drawn once the organization is settled. */
106
104
  function Window({ bar }) {
107
105
  const drawer = useDrawer();
108
- const { close: shut, depart } = drawer;
106
+ const { close: shut, depart, exit } = drawer;
109
107
  // On a phone the window stands in the visual viewport, so the keyboard never covers the composer (view.ts).
110
108
  const view = useView();
111
109
  const hold = drawer.window;
@@ -119,11 +117,12 @@ function Window({ bar }) {
119
117
  const [finding, setFinding] = useState(false);
120
118
  const found = useCallback(() => setFinding(false), []);
121
119
  const dev = useDevHost();
122
- // The builder's moves put the drawer away, in its place in the history.
120
+ // The builder's moves put the drawer away, in its place in the history; one to
121
+ // a page outside the app (Settings) leaves it through the router.
123
122
  const host = useMemo(() => ({
124
123
  ...dev,
125
- go: (to, how) => depart((replace) => dev.go(to, replace ? { ...how, replace } : how)),
126
- }), [dev, depart]);
124
+ go: (to, how) => (place(app(to)) ? depart : exit)((replace) => dev.go(to, replace ? { ...how, replace } : how)),
125
+ }), [dev, depart, exit]);
127
126
  const { client } = useAi();
128
127
  const { router, path } = useRooms();
129
128
  const here = path ?? '/';
@@ -146,13 +145,7 @@ function Window({ bar }) {
146
145
  // A run floats two panes of its own, so the pane it stands in is not cut.
147
146
  const run = where !== null && route(where).kind === 'run';
148
147
  const asked = at.mode === 'chat' ? at.thread : null;
149
- const { thread, settings } = useOpen();
150
- // A row of the account menu asks for Settings: the drawer goes away even when
151
- // the address does not move (the reader was on that page already).
152
- useEffect(() => {
153
- if (settings)
154
- shut();
155
- }, [settings, shut]);
148
+ const { thread, settings: wanted } = useOpen();
156
149
  const plans = where !== null && isPlans(where);
157
150
  useEffect(() => {
158
151
  if (plans)
@@ -192,15 +185,26 @@ function Window({ bar }) {
192
185
  drop();
193
186
  }, [mode, here, drop]);
194
187
  // Where the reader was before a place that takes the sidebar (Library, Scheduled
195
- // tasks, Customize, Settings): its Back returns there, Chat or Dev alike.
188
+ // tasks, Customize): its Back returns there, Chat or Dev alike.
196
189
  const away = useRef('');
197
- const inPlace = mode === 'dev' && spot(where) !== null;
190
+ const inPlace = mode === 'dev' && raised(where) !== null;
198
191
  useEffect(() => {
199
192
  if (!inPlace)
200
193
  away.current = `${window.location.pathname}${window.location.search}`;
201
194
  }, [inPlace, here]);
202
- /** A move of the app's own: the drawer away, the address in its place. */
203
- const move = useCallback((address) => depart((replace) => go(address, replace)), [depart]);
195
+ /**
196
+ * A move: the drawer away, the address in its place. One of the app's moves
197
+ * the app; any other page of the site (Settings) is the router's, and leaves it.
198
+ */
199
+ const move = useCallback((address) => (place(address.split(/[?#]/)[0]) ? depart : exit)((replace) => visit(router, address, replace)), [depart, exit, router]);
200
+ // A pane asks for Settings — the account menu's rows, Chat's See usage: its
201
+ // page (lib/host `settings`), through the router, the drawer away.
202
+ useEffect(() => {
203
+ if (!wanted)
204
+ return;
205
+ showSettings(null);
206
+ move(settings(wanted));
207
+ }, [wanted, move]);
204
208
  // The app's keys.
205
209
  useEffect(() => {
206
210
  const key = (e) => {
@@ -214,7 +218,7 @@ function Window({ bar }) {
214
218
  const mod = e.metaKey || e.ctrlKey;
215
219
  if (mod && !e.shiftKey && !e.altKey && e.key === ',') {
216
220
  e.preventDefault();
217
- move(app(SETTINGS));
221
+ move(settings());
218
222
  return;
219
223
  }
220
224
  if (!bar)
package/dist/Column.js CHANGED
@@ -10,28 +10,29 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
10
10
  // content beside it is the panel, inset by the gutter.
11
11
  //
12
12
  // The bell is the org's run activity (@hanzo/build `useSessions`): the runs
13
- // that moved last, each one press from its page, and Notifications in Settings.
13
+ // that moved last, each one press from its page.
14
14
  // The search opens the palette (Palette.tsx; ⌘K / Ctrl+K from anywhere).
15
15
  //
16
16
  // Dev's places are @hanzo/build's `own`: New run, Projects, Issues, Templates.
17
- // A place the rail holds (Library, Scheduled tasks, Customize) and Settings
18
- // each take the sidebar instead (Rail.tsx `spot`): Back, the place's name, and
19
- // its own rows (Settings' sections, Customize's tabs), never Dev's beside them.
17
+ // A place the rail holds (Library, Scheduled tasks, Customize) takes the sidebar
18
+ // instead (Rail.tsx `raised`): Back, the place's name, and its own rows
19
+ // (Customize's tabs, the Library's kinds), never Dev's beside them. Settings is
20
+ // the site's pages at /settings, with a column of their own.
20
21
  //
21
22
  // Its width is the edge's in the gutter to its right: dragged, keyed or
22
23
  // double-clicked back (@hanzo/build's `Grip`), kept per browser. Pulled in past
23
24
  // its floor it shuts (`onShut`), as the sidebar's toggle shuts it.
24
25
  import { useI18n } from '@hanzo/i18n/react';
25
26
  import { Text, XStack, YStack } from '@hanzo/gui';
26
- import { ArrowLeft, Bell, Search, Settings2, SquarePen } from 'lucide-react';
27
- import { DOTS, Gutter, library, NEW, own, path as written, sections, tabs, useScheduled, useSessions } from '@hanzo/build';
27
+ import { ArrowLeft, Bell, Search, SquarePen } from 'lucide-react';
28
+ import { DOTS, Gutter, library, NEW, own, tabs, useScheduled, useSessions } from '@hanzo/build';
28
29
  import { tidy } from './lib/title.js';
29
30
  import { DropdownMenu } from '@hanzo/ui';
30
31
  import { Sidebar, SidebarIconButton, SidebarItem, StatusDot } from '@hanzo/ui/chat';
31
32
  import { useId } from 'react';
32
33
  import { Made } from './Me.js';
33
34
  import { Recents } from './Recents.js';
34
- import { spot } from './Rail.js';
35
+ import { raised } from './Rail.js';
35
36
  import { Setup } from './Setup.js';
36
37
  import { ago } from './recent.js';
37
38
  import { empty } from './open.js';
@@ -61,7 +62,7 @@ const ROW = { rounded: '$4', minH: 31, py: 0, px: '$2', $touchable: { minH: 44 }
61
62
  /** The run activity behind the bell: the runs that moved last, newest first. */
62
63
  const RECENT = 6;
63
64
  /** What a place that takes the sidebar is called at its head. */
64
- const TITLE = { library: 'Library', scheduled: 'Scheduled tasks', customize: 'Customize', settings: 'Settings' };
65
+ const TITLE = { library: 'Library', scheduled: 'Scheduled tasks', customize: 'Customize' };
65
66
  /** A header mark: a 16px glyph in a 32px target, a thumb's 44 on a touch screen. */
66
67
  const HEAD = { width: 32, height: 32, rounded: '$3', $touchable: { width: 44, height: 44 } };
67
68
  function Activity({ host }) {
@@ -79,8 +80,6 @@ function Activity({ host }) {
79
80
  onSelect: () => host.go(s.id),
80
81
  }))
81
82
  : [{ key: 'none', label: 'Nothing new', disabled: true, onSelect: () => { } }]),
82
- { type: 'separator', key: 'line' },
83
- { key: 'settings', label: 'Notification settings', icon: _jsx(Settings2, { size: 16, "aria-hidden": true }), onSelect: () => host.go(written({ kind: 'settings', section: 'notifications' })) },
84
83
  ];
85
84
  return (_jsx(DropdownMenu, { minWidth: 280, trigger: _jsx(XStack, { render: "button", "aria-label": t("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 }));
86
85
  }
@@ -90,19 +89,17 @@ export function Column({ host, mode, thread, go, onShut, onSeek, onBack, drawer
90
89
  const uid = useId();
91
90
  const span = useSpan(SPAN, WIDTH, FLOOR, CEIL);
92
91
  const dev = (p) => host.go(p);
93
- const at = mode === 'dev' ? spot(host.path) : null;
92
+ const at = mode === 'dev' ? raised(host.path) : null;
94
93
  // Each place the rail holds lists its own rows: Library its kinds and Upload, Scheduled tasks its tasks and New task.
95
94
  const scheduled = useScheduled(host, dev, at === 'scheduled');
96
95
  const groups = places ??
97
- (at === 'settings'
98
- ? sections(host, dev)
99
- : at === 'customize'
100
- ? tabs(host, dev)
101
- : at === 'library'
102
- ? library(host, dev)
103
- : at === 'scheduled'
104
- ? scheduled
105
- : own(host, dev));
96
+ (at === 'customize'
97
+ ? tabs(host, dev)
98
+ : at === 'library'
99
+ ? library(host, dev)
100
+ : at === 'scheduled'
101
+ ? scheduled
102
+ : own(host, dev));
106
103
  const open = (row) => (row.kind === 'chat' ? go(talk(row.id)) : dev(row.id));
107
104
  // Chat's New; Dev's is the first of its places.
108
105
  const create = () => {
package/dist/Drawer.d.ts CHANGED
@@ -9,6 +9,12 @@ export interface Drawer {
9
9
  close: () => void;
10
10
  /** Moves the app from the menu: the move replaces the menu's history entry, and the menu goes. */
11
11
  depart: (move: Move) => void;
12
+ /**
13
+ * Leaves the app from the menu for a page the host's router draws: the page
14
+ * takes the menu's history entry, and the menu goes. Nothing is popped, for
15
+ * the router moves the address after this returns.
16
+ */
17
+ exit: (move: Move) => void;
12
18
  /** The address moved under the menu, by a way that was not `depart`: the menu goes, and its entry stays behind the move. */
13
19
  drop: () => void;
14
20
  /** The window: everything in it but the menu is inert while the menu is out. */
package/dist/Drawer.js CHANGED
@@ -31,7 +31,9 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
31
31
  // back to what opened it as it shuts; Escape closes it, unless a menu or dialog
32
32
  // it opened holds the focus; Back closes it, because opening it pushed an entry
33
33
  // of the same address that Back pops. A move made from inside it takes that
34
- // entry's place (`depart`), so the history holds the move and never the menu.
34
+ // entry's place (`depart`), so the history holds the move and never the menu;
35
+ // so does a page the host's router draws outside the app (`exit`: Settings),
36
+ // whose address moves only after the press returns.
35
37
  // Its list's scroll stays its own (Recents' `overscrollBehavior`), and the pane,
36
38
  // never unmounted, keeps its own.
37
39
  import { useI18n } from '@hanzo/i18n/react';
@@ -112,6 +114,12 @@ export function useDrawer() {
112
114
  window.history.back();
113
115
  home();
114
116
  }, [home]);
117
+ const exit = useCallback((move) => {
118
+ const replace = marked.current;
119
+ marked.current = false;
120
+ move(replace);
121
+ home();
122
+ }, [home]);
115
123
  const drop = useCallback(() => {
116
124
  marked.current = false;
117
125
  home();
@@ -287,6 +295,7 @@ export function useDrawer() {
287
295
  open,
288
296
  close,
289
297
  depart,
298
+ exit,
290
299
  drop,
291
300
  window: useCallback((node) => {
292
301
  win.current = element(node);
package/dist/Me.js CHANGED
@@ -9,8 +9,8 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
9
9
  // the person's place on the beta waitlist (the site's /beta/status); then
10
10
  // Help ›, the site's own pages and the keyboard shortcuts (⌘/ / Ctrl+/ from
11
11
  // anywhere); and Log out. Every row opens a real destination: Settings'
12
- // sections through `showSettings` (open.ts), which the app answers with the
13
- // builder's Settings (dev.tsx) and the shell with its own pane; the plans on the
12
+ // pages through `showSettings` (open.ts), which the app answers with the
13
+ // site's Settings at /settings (App.tsx) and the shell with its own pane; the plans on the
14
14
  // pay site (`leave`); the site's pages through `site()`, so hanzo.team names
15
15
  // hanzo.ai (help.ts).
16
16
  //
@@ -39,15 +39,13 @@ import { api } from './lib/api.js';
39
39
  import { createOrg, say } from './lib/org.js';
40
40
  import { list } from './lib/list.js';
41
41
  import { choose, org as current, orgs, pick, renew, scope, superAdmin } from './lib/session.js';
42
- import { ENTRY } from './lib/host.js';
42
+ import { ENTRY, settings } from './lib/host.js';
43
43
  import { leave } from './dev.js';
44
44
  import { payUrl } from './lib/pay.js';
45
45
  import { asked, KEYS, legal, out, pages } from './help.js';
46
46
  import { showSettings } from './open.js';
47
47
  import { site, where } from './where.js';
48
48
  import { BAD } from './lib/mix.js';
49
- /** Where an organization's own settings live: a page of the site (`site()`), not of the app. */
50
- const ORG_SETTINGS = '/settings/organization';
51
49
  /** An organization this tab created, until the token carries it. */
52
50
  const MADE = 'hanzo:org:made';
53
51
  /** Each Help page's icon, by its key in help.ts. */
@@ -201,7 +199,7 @@ export function Me({ host, narrow = false, above = false, onOrg, }) {
201
199
  row.current = el instanceof HTMLElement ? el : null;
202
200
  }, render: "button", "data-slot": "me", "aria-label": `Account: ${[who, current].filter(Boolean).join(' · ')}`, ...{ title: who }, width: narrow ? 36 : '100%', minH: narrow ? 36 : 48,
203
201
  // A thumb's 44 on a touch screen.
204
- $touchable: narrow ? { width: 44, minH: 44 } : undefined, 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 }), working ? (_jsx(SizableText, { size: "$1", color: "$soft", numberOfLines: 1, text: "left", children: working })) : null] }), _jsx(ChevronsUpDown, { size: 14, "aria-hidden": true, opacity: 0.6 })] }))] }) }), _jsxs(DropdownMenuContent, { minW: 260, maxW: "calc(100vw - 16px)", "aria-labelledby": undefined, "aria-label": t("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: host.person.email || working })] })] }), account ? (_jsxs(DropdownMenuSubContent, { minW: 260, maxW: "calc(100vw - 16px)", pointerEvents: "auto", children: [_jsx(Meter, { read: read, onPlans: () => leave('here'), onBilling: () => (setOpen(false), showSettings('billing')) }), _jsx(DropdownMenuSeparator, {}), _jsx(DropdownMenuLabel, { children: t("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": t("Current") }) : null, children: o.display }) }, o.name))), _jsx(DropdownMenuItem, { onSelect: () => setCreating(true), children: _jsx(Line, { icon: _jsx(Plus, { size: 16, "aria-hidden": true }), children: t("Create organization") }) }), _jsx(DropdownMenuItem, { onSelect: () => out(site(ORG_SETTINGS)), children: _jsx(Line, { icon: _jsx(Settings, { size: 16, "aria-hidden": true }), children: t("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: t("Open in Hanzo Team") }) })) : (_jsx(DropdownMenuItem, { onSelect: () => setCreating(true), children: _jsx(Line, { icon: _jsx(Users, { size: 16, "aria-hidden": true }), children: t("Create a team") }) })), superAdmin() ? (_jsx(DropdownMenuItem, { onSelect: choose, children: _jsx(Line, { icon: _jsx(Building2, { size: 16, "aria-hidden": true }), children: t("All organizations") }) })) : null] })) : null] }), team ? (_jsx(DropdownMenuItem, { "data-slot": "me-team", onSelect: () => team.name !== current && onOrg(team.name), children: _jsx(Line, { icon: _jsx(Mark, { org: team }), trail: team.name === current ? _jsx(Check, { size: 16, "aria-label": t("Current") }) : null, children: `${team.display} · ${teamPlan || 'No plan'}` }) })) : null, personal ? (_jsx(DropdownMenuItem, { "data-slot": "me-personal", onSelect: () => personal.name !== current && onOrg(personal.name), actionsVisibility: "always", actions: ownPlan ? undefined : (_jsx(Button, { size: "xs", variant: "outline", onPress: getPro, children: t("Get Pro") })), children: _jsx(Line, { icon: _jsx(CircleUserRound, { size: 16, "aria-hidden": true }), trail: personal.name === current ? _jsx(Check, { size: 16, "aria-label": t("Current") }) : null, children: `Personal · ${ownPlan || 'No plan'}` }) })) : 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: t("Personalization") }) }), _jsx(DropdownMenuItem, { onSelect: () => showSettings('account'), children: _jsx(Line, { icon: _jsx(CircleUserRound, { size: 16, "aria-hidden": true }), children: t("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: t("Settings") }) }), _jsx(DropdownMenuItem, { onSelect: () => window.location.assign(site('/beta/status')), children: _jsx(Line, { icon: _jsx(FlaskConical, { size: 16, "aria-hidden": true }), children: t("Beta") }) }), _jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuSub, { open: help, onOpenChange: setHelp, children: [_jsx(DropdownMenuSubTrigger, { children: _jsx(Line, { icon: _jsx(LifeBuoy, { size: 16, "aria-hidden": true }), children: t("Help") }) }), help ? (_jsxs(DropdownMenuSubContent, { minW: 240, maxW: "calc(100vw - 16px)", pointerEvents: "auto", 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: t("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: t("Log out") }) })) : null] })] }), _jsx(Create, { open: creating, onOpenChange: setCreating }), _jsx(Shortcuts, { open: keys, onOpenChange: setKeys, apple: apple })] }));
202
+ $touchable: narrow ? { width: 44, minH: 44 } : undefined, 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 }), working ? (_jsx(SizableText, { size: "$1", color: "$soft", numberOfLines: 1, text: "left", children: working })) : null] }), _jsx(ChevronsUpDown, { size: 14, "aria-hidden": true, opacity: 0.6 })] }))] }) }), _jsxs(DropdownMenuContent, { minW: 260, maxW: "calc(100vw - 16px)", "aria-labelledby": undefined, "aria-label": t("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: host.person.email || working })] })] }), account ? (_jsxs(DropdownMenuSubContent, { minW: 260, maxW: "calc(100vw - 16px)", pointerEvents: "auto", children: [_jsx(Meter, { read: read, onPlans: () => leave('here'), onBilling: () => (setOpen(false), showSettings('billing')) }), _jsx(DropdownMenuSeparator, {}), _jsx(DropdownMenuLabel, { children: t("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": t("Current") }) : null, children: o.display }) }, o.name))), _jsx(DropdownMenuItem, { onSelect: () => setCreating(true), children: _jsx(Line, { icon: _jsx(Plus, { size: 16, "aria-hidden": true }), children: t("Create organization") }) }), _jsx(DropdownMenuItem, { onSelect: () => out(site(settings('organization'))), children: _jsx(Line, { icon: _jsx(Settings, { size: 16, "aria-hidden": true }), children: t("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: t("Open in Hanzo Team") }) })) : (_jsx(DropdownMenuItem, { onSelect: () => setCreating(true), children: _jsx(Line, { icon: _jsx(Users, { size: 16, "aria-hidden": true }), children: t("Create a team") }) })), superAdmin() ? (_jsx(DropdownMenuItem, { onSelect: choose, children: _jsx(Line, { icon: _jsx(Building2, { size: 16, "aria-hidden": true }), children: t("All organizations") }) })) : null] })) : null] }), team ? (_jsx(DropdownMenuItem, { "data-slot": "me-team", onSelect: () => team.name !== current && onOrg(team.name), children: _jsx(Line, { icon: _jsx(Mark, { org: team }), trail: team.name === current ? _jsx(Check, { size: 16, "aria-label": t("Current") }) : null, children: `${team.display} · ${teamPlan || 'No plan'}` }) })) : null, personal ? (_jsx(DropdownMenuItem, { "data-slot": "me-personal", onSelect: () => personal.name !== current && onOrg(personal.name), actionsVisibility: "always", actions: ownPlan ? undefined : (_jsx(Button, { size: "xs", variant: "outline", onPress: getPro, children: t("Get Pro") })), children: _jsx(Line, { icon: _jsx(CircleUserRound, { size: 16, "aria-hidden": true }), trail: personal.name === current ? _jsx(Check, { size: 16, "aria-label": t("Current") }) : null, children: `Personal · ${ownPlan || 'No plan'}` }) })) : 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: t("Personalization") }) }), _jsx(DropdownMenuItem, { onSelect: () => showSettings('account'), children: _jsx(Line, { icon: _jsx(CircleUserRound, { size: 16, "aria-hidden": true }), children: t("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: t("Settings") }) }), _jsx(DropdownMenuItem, { onSelect: () => window.location.assign(site('/beta/status')), children: _jsx(Line, { icon: _jsx(FlaskConical, { size: 16, "aria-hidden": true }), children: t("Beta") }) }), _jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuSub, { open: help, onOpenChange: setHelp, children: [_jsx(DropdownMenuSubTrigger, { children: _jsx(Line, { icon: _jsx(LifeBuoy, { size: 16, "aria-hidden": true }), children: t("Help") }) }), help ? (_jsxs(DropdownMenuSubContent, { minW: 240, maxW: "calc(100vw - 16px)", pointerEvents: "auto", 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: t("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: t("Log out") }) })) : null] })] }), _jsx(Create, { open: creating, onOpenChange: setCreating }), _jsx(Shortcuts, { open: keys, onOpenChange: setKeys, apple: apple })] }));
205
203
  }
206
204
  /** The app's keys (help.ts `KEYS`), one row each, spelled for this platform. */
207
205
  function Shortcuts({ open, onOpenChange, apple }) {
@@ -10,6 +10,7 @@ import { iam } from './lib/api.js';
10
10
  import { pick } from './lib/session.js';
11
11
  import { Text, View, XStack, YStack } from '@hanzo/gui';
12
12
  import { site } from './where.js';
13
+ import { settings } from './lib/host.js';
13
14
  /** design's smallest rung (`--text-floor`, 10px), below gui's `$1`; gui's font size takes a rung or a number. */
14
15
  const FLOOR = 10;
15
16
  /** A row at the foot of the menu that leaves for another page: a ghost Button, label to the left. */
@@ -95,7 +96,7 @@ export function OrgSwitcherDropdown({ open, onClose, activeOrgId, onSelect, alig
95
96
  onSelect(o.id);
96
97
  onClose();
97
98
  }, width: "100%", justify: "space-between", px: 10, py: 6, bg: isSelected ? '$hover' : 'transparent', children: [_jsxs(XStack, { items: "center", gap: 10, minW: 0, shrink: 1, children: [_jsx(Text, { width: 24, height: 24, rounded: 6, bg: "$raised", display: "flex", items: "center", justify: "center", fontSize: "$2", fontWeight: "700", color: "$ink", shrink: 0, overflow: "hidden", children: o.logo ? (_jsx(View, { render: _jsx("img", { src: o.logo, alt: "" }), width: "100%", height: "100%", objectFit: "cover" })) : (o.displayName.toLowerCase().includes('hanzo') || o.name.toLowerCase().includes('hanzo')) ? (_jsx(HanzoMark, { size: 14 })) : (o.name.slice(0, 3).toUpperCase()) }), _jsxs(YStack, { minW: 0, shrink: 1, items: "flex-start", children: [_jsx(Text, { fontSize: "$2", fontWeight: isSelected ? '600' : '500', color: "$ink", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis", children: o.displayName }), o.role ? (_jsx(Text, { fontSize: FLOOR, color: "$faint", children: o.role })) : null] })] }), isSelected ? _jsx(Check, { size: 14, color: "currentColor" }) : null] }, o.id));
98
- }), _jsx(View, { height: 1, bg: "$borderColor", my: 4 }), _jsx(Button, { asChild: true, ...LINK, children: _jsxs("a", { href: iam(), target: "_blank", rel: "noreferrer", children: [_jsx(Plus, { size: 14 }), "Create Organization"] }) }), _jsx(Button, { asChild: true, ...LINK, children: _jsxs("a", { href: site("/settings/organization"), children: [_jsx(Settings, { size: 14 }), "Organization settings"] }) })] }));
99
+ }), _jsx(View, { height: 1, bg: "$borderColor", my: 4 }), _jsx(Button, { asChild: true, ...LINK, children: _jsxs("a", { href: iam(), target: "_blank", rel: "noreferrer", children: [_jsx(Plus, { size: 14 }), "Create Organization"] }) }), _jsx(Button, { asChild: true, ...LINK, children: _jsxs("a", { href: site(settings("organization")), children: [_jsx(Settings, { size: 14 }), "Organization settings"] }) })] }));
99
100
  }
100
101
  /**
101
102
  * THE WORKSPACE YOU ARE STANDING IN, and the way to another one.
package/dist/Palette.js CHANGED
@@ -5,14 +5,15 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
5
5
  //
6
6
  // An empty field lists Recent (the reader's chats and Dev tasks, newest first,
7
7
  // each with its kind's icon), Actions, and Go to: the products, the places of
8
- // the app and the apps in beta the reader may open. A query searches all of it, deeper places too (Settings' sections,
8
+ // the app and the apps in beta the reader may open. A query searches all of it, deeper places too (Settings' pages,
9
9
  // Customize's tabs, the Library's kinds, Studio's places, Help), and the
10
10
  // reader's chats, Dev tasks, projects, agents, skills and connectors by name;
11
11
  // each group keeps its best five and the group with the best answer leads
12
12
  // (seek.ts `find`). A row opens where it lives; an action runs.
13
13
  //
14
14
  // GO TO IS DERIVED, never listed here: the places are @hanzo/build's own
15
- // registries (`nav` for Dev's places, `sections` for Settings, `tabs` for
15
+ // registries (`nav` for Dev's places, `sections` for Settings — the sections
16
+ // with a page at /settings (lib/host.ts `paged`), each opened there — `tabs` for
16
17
  // Customize, `library` for the Library's kinds), @hanzo/studio's `nav`, and
17
18
  // help.ts's pages, lib/betas.ts's granted apps: the same lists the sidebars and
18
19
  // menus draw. A page added to
@@ -40,7 +41,7 @@ import { empty, openThread, withAgents } from './open.js';
40
41
  import { useThreadList } from './thread.js';
41
42
  import { useSignOut } from './lib/signout.js';
42
43
  import { choose } from './lib/session.js';
43
- import { app, studio, talk, TERMINALS, WORKSPACE } from './lib/host.js';
44
+ import { app, paged, settings, studio, talk, TERMINALS, WORKSPACE } from './lib/host.js';
44
45
  import { BETAS } from './lib/betas.js';
45
46
  import { useBetas } from './Betas.js';
46
47
  import { tidy } from './lib/title.js';
@@ -48,15 +49,11 @@ import { KEYS, legal, out, pages } from './help.js';
48
49
  import { site } from './where.js';
49
50
  import { find } from './seek.js';
50
51
  const ICON = { size: 16, 'aria-hidden': true };
51
- /** The builder's Settings, its first section. */
52
- const SETTINGS = written({ kind: 'settings', section: 'general' });
53
52
  const SKILLS = written({ kind: 'customize', tab: 'skills' });
54
53
  const CONNECTORS = written({ kind: 'customize', tab: 'connectors' });
55
- /** The words a Settings section is also asked for by: the account menu's names for two of them, and the obvious ones. */
54
+ /** The words a Settings page is also asked for by: the account menu's names for General, and the obvious ones. */
56
55
  const ALSO = {
57
- general: ['personalization', 'appearance', 'theme'],
58
- account: ['profile'],
59
- privacy: ['security'],
56
+ general: ['personalization', 'appearance', 'theme', 'profile', 'account'],
60
57
  billing: ['plans', 'subscription'],
61
58
  members: ['organization', 'team'],
62
59
  };
@@ -79,7 +76,7 @@ export function places(host, go) {
79
76
  { id: 'tabs', label: 'Tabs', icon: _jsx(SquareTerminal, { ...ICON }), run: () => go(TERMINALS) },
80
77
  ...(host ? [{ id: 'team', label: 'Team', icon: _jsx(Users, { ...ICON }), keywords: ['workspace'], run: () => out(WORKSPACE) }] : []),
81
78
  ...(host ? rows(nav(host, builder).map((g) => ({ links: g.links.filter((l) => l.id !== NEW) })), '', 'dev') : []),
82
- { id: 'settings', label: 'Settings', icon: _jsx(Settings, { ...ICON }), shortcut: ['Mod', ','], run: () => go('/settings') },
79
+ { id: 'settings', label: 'Settings', icon: _jsx(Settings, { ...ICON }), shortcut: ['Mod', ','], run: () => go(settings()) },
83
80
  // The site's own pages, outside the app: the browser loads them.
84
81
  { id: 'referral', label: 'Referral', icon: _jsx(Gift, { ...ICON }), keywords: ['invite', 'share'], run: () => window.location.assign(site('/referral')) },
85
82
  { id: 'beta', label: 'Beta status', icon: _jsx(FlaskConical, { ...ICON }), keywords: ['waitlist'], run: () => window.location.assign(site('/beta/status')) },
@@ -90,7 +87,7 @@ export function places(host, go) {
90
87
  const deep = [
91
88
  ...(host
92
89
  ? [
93
- ...rows(sections(host, builder), 'Settings', 'settings'),
90
+ ...rows(sections(host, builder).map((g) => ({ links: g.links.filter((l) => paged(l.id)) })), 'Settings', 'settings'),
94
91
  ...rows(tabs(host, builder), 'Customize', 'customize'),
95
92
  ...rows(library(host, builder).slice(1), 'Library', 'library'),
96
93
  ...rows(made(host, (p) => go(studio(p))), 'Studio', 'studio').filter((a) => a.id !== `studio-${NEW}`),
@@ -100,8 +97,7 @@ export function places(host, go) {
100
97
  .filter((p) => p.key !== 'help')
101
98
  .map((p) => ({ id: `help-${p.key}`, label: p.label, where: 'Help', keywords: ['Help'], run: () => out(p.href) })),
102
99
  ];
103
- // A place two registries hold (Machines is Dev's and a Settings section) is listed once, as Dev's.
104
- return { top, deep: deep.filter((a) => !top.some((t) => t.id === a.id.replace(/^settings-/, 'dev-'))) };
100
+ return { top, deep };
105
101
  }
106
102
  /** The composer's model menu, opened as a press would: a page without a composer opens New chat's. */
107
103
  function tune(go) {
package/dist/Rail.d.ts CHANGED
@@ -10,11 +10,7 @@ export declare const RAISED: {
10
10
  readonly customize: '-/customize';
11
11
  };
12
12
  export type Raised = keyof typeof RAISED;
13
- /** A place that takes the sidebar: one of the rail's, or Settings (the account menu's). */
14
- export type Spot = Raised | 'settings';
15
- /** The place a builder address stands on, if it is one that takes the sidebar. */
16
- export declare const spot: (at: string | null) => Spot | null;
17
- /** Which of them a builder address stands on, if any: an agent's page is Customize's, an automation Scheduled tasks'. */
13
+ /** Which of them a builder address stands on, if any, and so takes the sidebar: an agent's page is Customize's, an automation Scheduled tasks'. */
18
14
  export declare function raised(at: string | null): Raised | null;
19
15
  /** One mark: an icon button named for where it goes, its name its tooltip, lit where the reader is. */
20
16
  export declare function Mark({ label, on, onPress, children, ...rest }: {
package/dist/Rail.js CHANGED
@@ -15,8 +15,9 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
15
15
  // conversations, Dev its own places (@hanzo/build `own`: New run, Projects,
16
16
  // Issues, Templates) and its runs, Studio its own; Library, Scheduled tasks and
17
17
  // Customize (@hanzo/build's Artifacts, Automations and Customize) each their
18
- // own, and Settings its sections (Column.tsx). Nothing on the rail is listed
19
- // inside Dev: one level of the app a place, each lit while the reader is on it.
18
+ // own (Column.tsx); Settings is the site's pages at /settings, beside the app
19
+ // (lib/host.ts `settings`). Nothing on the rail is listed inside Dev: one level
20
+ // of the app a place, each lit while the reader is on it.
20
21
  //
21
22
  // It stands on the frame's floor, never cut as a pane (ground.ts): the rail and
22
23
  // the sidebar beside it are the window, and the content is the panel inset in
@@ -47,9 +48,7 @@ export const RAIL = 48;
47
48
  const MARK = 36;
48
49
  /** The places the rail holds beside the products, by the builder address each opens. */
49
50
  export const RAISED = { library: '-/artifacts', scheduled: '-/automations', customize: '-/customize' };
50
- /** The place a builder address stands on, if it is one that takes the sidebar. */
51
- export const spot = (at) => (at !== null && route(at).kind === 'settings' ? 'settings' : raised(at));
52
- /** Which of them a builder address stands on, if any: an agent's page is Customize's, an automation Scheduled tasks'. */
51
+ /** Which of them a builder address stands on, if any, and so takes the sidebar: an agent's page is Customize's, an automation Scheduled tasks'. */
53
52
  export function raised(at) {
54
53
  if (at === null)
55
54
  return null;
@@ -85,7 +84,7 @@ export function Toggle({ open, onPress, label }) {
85
84
  }
86
85
  export function Rail({ host, mode, at, open, toggle, rows = false, onChat, onDev, onStudio, onTabs, go, onOrg, }) {
87
86
  const { t } = useI18n();
88
- const here = mode === 'dev' ? spot(at) : null;
87
+ const here = mode === 'dev' ? raised(at) : null;
89
88
  const to = (p) => () => go(app(p));
90
89
  // The products on hosts of their own, behind More; on a phone the raised places lead it.
91
90
  // Studio offers no Team, so on a laptop it draws no More.
package/dist/dev.js CHANGED
@@ -9,16 +9,18 @@
9
9
  //
10
10
  // THE ADDRESS IS A PATH. Dev is /dev, and every place the builder shows is a
11
11
  // path under it in the builder's real-path grammar (lib/host.ts `app`, `place`;
12
- // @hanzo/build/route `href`, `parse`): a run, a screen, a Settings section, a
13
- // Customize tab, the plans and a project all survive a reload, and Back steps
14
- // through them. The host hands the rooms the path (`<Rooms path>`); a move is a
15
- // native history entry, which the Next router folds into `usePathname`, so
16
- // reading the address and moving it are one path.
12
+ // @hanzo/build/route `href`, `parse`): a run, a screen, a Customize tab, the
13
+ // plans and a project all survive a reload, and Back steps through them. The
14
+ // host hands the rooms the path (`<Rooms path>`); a move is a native history
15
+ // entry, which the Next router folds into `usePathname`, so reading the address
16
+ // and moving it are one path.
17
17
  //
18
- // A PANE THAT ASKS FOR SETTINGS (`showSettings`, open.ts) is answered here: the
19
- // rooms' shell draws its own Settings pane, and the app has none, so Chat's
20
- // "See usage" opens the builder's Usage — the page the account menu's Usage
21
- // opens — rather than nothing.
18
+ // SETTINGS IS THE SITE'S, never Dev's: one tree at /settings, the host's own
19
+ // pages (lib/host.ts `settings`). A builder section the builder moves to
20
+ // (`go`) and a link it opens (`open`) reach that page through the host's router
21
+ // (`visit`); a pane that asks for Settings (`showSettings`, open.ts) is App's to
22
+ // answer, where the phone's menu is. The rooms' shell draws its own Settings
23
+ // pane; the app has none.
22
24
  //
23
25
  // THE PLANS ARE THE PAY SITE'S. Every way the app reaches the plans — the
24
26
  // account menu's View all plans and its plan's Upgrade, Billing's Adjust plan,
@@ -27,7 +29,7 @@
27
29
  // coming back once paid. A typed or reloaded plans address leaves the same way
28
30
  // (App.tsx). A page load, at once, not a move inside the app: the pay site is
29
31
  // where a plan is bought.
30
- import { useEffect, useMemo } from 'react';
32
+ import { useMemo } from 'react';
31
33
  import { useLook, useRooms } from './host.js';
32
34
  import { useIam } from '@hanzo/iam/react';
33
35
  import { useSignOut } from './lib/signout.js';
@@ -36,15 +38,14 @@ import { bearer, named, org, orgs, pick } from './lib/session.js';
36
38
  import { enter } from './lib/destination.js';
37
39
  import { api } from './lib/api.js';
38
40
  import { CAST } from './agents.js';
39
- import { app, go, place } from './lib/host.js';
41
+ import { app, place, settings, visit } from './lib/host.js';
40
42
  import { link } from './lib/tags.js';
41
43
  import { payUrl } from './lib/pay.js';
42
- import { showSettings, useOpen } from './open.js';
43
44
  /** Where the builder links out: every one of them an address of this site. */
44
45
  const LINKS = {
45
46
  github: app('-/sync'),
46
47
  customize: app('-/customize'),
47
- settings: '/settings',
48
+ settings: settings(),
48
49
  home: '/',
49
50
  };
50
51
  /** The builder's address for the plans. */
@@ -79,24 +80,6 @@ export function useDevHost() {
79
80
  const { router, path: pathname } = useRooms();
80
81
  const here = path(route(at(pathname)));
81
82
  const look = useLook();
82
- const { settings } = useOpen();
83
- // Settings is top-level /settings; organization settings are at /settings/organization.
84
- useEffect(() => {
85
- if (!settings)
86
- return;
87
- showSettings(null);
88
- const s = settings.toLowerCase();
89
- if (s.includes('member'))
90
- router.push('/settings/organization/members');
91
- else if (s.includes('billing'))
92
- router.push('/settings/organization/billing');
93
- else if (s.includes('usage'))
94
- router.push('/settings/organization/usage');
95
- else if (s.includes('org'))
96
- router.push('/settings/organization');
97
- else
98
- router.push('/settings');
99
- }, [settings, router]);
100
83
  // The token's claims change only when the token does: a sign-in, a refresh.
101
84
  const memberships = useMemo(() => orgs(), [accessToken]); // eslint-disable-line react-hooks/exhaustive-deps
102
85
  const scoped = useMemo(() => org(), [accessToken]); // eslint-disable-line react-hooks/exhaustive-deps
@@ -124,7 +107,7 @@ export function useDevHost() {
124
107
  person: { name, email, avatar },
125
108
  admin: administers(bearer(), scoped),
126
109
  path: here,
127
- go: (to, how) => (isPlans(to) ? leave('here') : go(app(to), how?.replace)),
110
+ go: (to, how) => (isPlans(to) ? leave('here') : visit(router, app(to), how?.replace)),
128
111
  links: LINKS,
129
112
  // The sidebar lists Settings' sections and Customize's tabs (Column.tsx), and Customize › Agents offers the cast.
130
113
  sections: true,
@@ -146,10 +129,8 @@ export function useDevHost() {
146
129
  const to = `${url.pathname}${url.search}${url.hash}`;
147
130
  if (url.origin !== window.location.origin)
148
131
  window.location.assign(link(url.href));
149
- else if (place(url.pathname))
150
- go(to);
151
132
  else
152
- router.push(to);
133
+ visit(router, to);
153
134
  },
154
135
  signIn: () => enter(),
155
136
  signOut: () => void logout(),
package/dist/lib/home.js CHANGED
@@ -12,6 +12,7 @@
12
12
  * two is how a card comes to say "0 projects" about a question nobody asked.
13
13
  */
14
14
  import { site } from '../where.js';
15
+ import { settings } from './host.js';
15
16
  /**
16
17
  * Where a teammate is invited.
17
18
  *
@@ -34,7 +35,7 @@ import { site } from '../where.js';
34
35
  * other page of this app is, and the three doors that offer it (the empty
35
36
  * room's tile, the Create menu, the sidebar's row) all read this one line.
36
37
  */
37
- export const invite = () => site('/settings/organization/members');
38
+ export const invite = () => site(settings('members'));
38
39
  /** The org's projects, newest first. */
39
40
  export async function projects(client, signal) {
40
41
  const rows = await client.http.json({ path: '/v1/projects', signal });
@@ -39,10 +39,12 @@ export declare const TABS = "https://tabs.hanzo.ai";
39
39
  * read by `href`/`parse` (@hanzo/build/route): /dev/run/<id>, /dev/projects,
40
40
  * /dev/projects/<org>/<repo> a repository, /dev/projects/<slug> a deployed
41
41
  * site, /dev/issues, /dev/artifacts, /dev/templates, /dev/automations[/<id>],
42
- * /dev/machines, /dev/environments, /dev/customize[/<tab>],
43
- * /dev/settings[/<section>], /dev/plans. The export holds one page per root,
44
- * /chat and /dev, and the edge serves it for every path under it (hanzo.ai's
45
- * lib/edge.ts `APP`), so every address reloads.
42
+ * /dev/machines, /dev/environments, /dev/customize[/<tab>], /dev/plans. The
43
+ * export holds one page per root, /chat and /dev, and the edge serves it for
44
+ * every path under it (hanzo.ai's lib/edge.ts `APP`), so every address reloads.
45
+ *
46
+ * Settings is none of them: it is the host's own pages, one tree at /settings
47
+ * (`settings`), and no builder section is written under /dev.
46
48
  */
47
49
  export declare const CHAT = "/chat";
48
50
  export declare const DEV = "/dev";
@@ -60,8 +62,25 @@ export declare const TERMINALS = "/tabs";
60
62
  export declare const studio: (at: string) => string;
61
63
  /** Chat on a conversation, or /chat itself for a new one. */
62
64
  export declare const talk: (id?: string | null) => string;
63
- /** Dev at a builder address (`route` in @hanzo/build); '' is New, /dev. What is not an address is New. */
64
- export declare const app: (at: string) => string;
65
+ /**
66
+ * Settings, and every page under it: the person's and the organization's, the
67
+ * host's own pages beside the app (hanzo.ai's app/(app)/settings), served
68
+ * wherever the app is.
69
+ */
70
+ export declare const SETTINGS = "/settings";
71
+ /** Whether a section of Settings (a builder section, or what `showSettings` asks for) has a page of its own. */
72
+ export declare const paged: (section: string) => boolean;
73
+ /** The Settings page a section names; one with no page of its own opens General, /settings. */
74
+ export declare const settings: (section?: string | null) => string;
75
+ /**
76
+ * The app's address for a builder address (`route` in @hanzo/build): Dev's
77
+ * path, '' New at /dev, and what is not an address New too. A section of the
78
+ * builder's Settings is the Settings page that answers for it (`settings`),
79
+ * never a path under /dev. Machines and Environments, which the builder files
80
+ * under Settings and writes as places of their own (/machines, /environments),
81
+ * stay Dev's.
82
+ */
83
+ export declare function app(at: string): string;
65
84
  /** A place in the app: Chat on a conversation or none, or Dev at a builder address. */
66
85
  export type Place = {
67
86
  mode: 'chat';
@@ -95,6 +114,17 @@ export declare const dev: (ref?: string | null) => string;
95
114
  * history entry into `usePathname`, so the app follows the path it reads.
96
115
  */
97
116
  export declare function go(address: string, replace?: boolean): void;
117
+ /** The host's router, as far as moving to a page goes (`useRooms().router`). */
118
+ interface Router {
119
+ push(href: string): void;
120
+ replace(href: string): void;
121
+ }
122
+ /**
123
+ * Move to an address of this site: one of the app's (`place`) by `go`, which
124
+ * the app follows; any other page — Settings — through the host's router, which
125
+ * draws that page. A history entry, or in place of this one.
126
+ */
127
+ export declare function visit(router: Router, address: string, replace?: boolean): void;
98
128
  /** Keep the mode the reader is in, for `resume`. */
99
129
  export declare function remember(mode: Place['mode']): void;
100
130
  /** Where `/` takes a member: the mode they were last in, Chat when none is kept. */
@@ -162,3 +192,4 @@ export declare const visits: (hostname: string) => boolean;
162
192
  * cloud.yaml).
163
193
  */
164
194
  export declare const answers: (hostname: string) => boolean;
195
+ export {};
package/dist/lib/host.js CHANGED
@@ -42,10 +42,12 @@ export const TABS = 'https://tabs.hanzo.ai';
42
42
  * read by `href`/`parse` (@hanzo/build/route): /dev/run/<id>, /dev/projects,
43
43
  * /dev/projects/<org>/<repo> a repository, /dev/projects/<slug> a deployed
44
44
  * site, /dev/issues, /dev/artifacts, /dev/templates, /dev/automations[/<id>],
45
- * /dev/machines, /dev/environments, /dev/customize[/<tab>],
46
- * /dev/settings[/<section>], /dev/plans. The export holds one page per root,
47
- * /chat and /dev, and the edge serves it for every path under it (hanzo.ai's
48
- * lib/edge.ts `APP`), so every address reloads.
45
+ * /dev/machines, /dev/environments, /dev/customize[/<tab>], /dev/plans. The
46
+ * export holds one page per root, /chat and /dev, and the edge serves it for
47
+ * every path under it (hanzo.ai's lib/edge.ts `APP`), so every address reloads.
48
+ *
49
+ * Settings is none of them: it is the host's own pages, one tree at /settings
50
+ * (`settings`), and no builder section is written under /dev.
49
51
  */
50
52
  export const CHAT = '/chat';
51
53
  export const DEV = '/dev';
@@ -63,8 +65,40 @@ export const TERMINALS = '/tabs';
63
65
  export const studio = (at) => (at ? `${STUDIO}/${at.replace(/^\/+/, '')}` : STUDIO);
64
66
  /** Chat on a conversation, or /chat itself for a new one. */
65
67
  export const talk = (id) => (id ? `${CHAT}/${encodeURIComponent(id)}` : CHAT);
66
- /** Dev at a builder address (`route` in @hanzo/build); '' is New, /dev. What is not an address is New. */
67
- export const app = (at) => DEV + href(route(at));
68
+ /**
69
+ * Settings, and every page under it: the person's and the organization's, the
70
+ * host's own pages beside the app (hanzo.ai's app/(app)/settings), served
71
+ * wherever the app is.
72
+ */
73
+ export const SETTINGS = '/settings';
74
+ /** The organization's settings. */
75
+ const ORGANIZATION = `${SETTINGS}/organization`;
76
+ /** Settings' pages, by the name a surface asks for each: General at /settings, the organization's under it. */
77
+ const PAGES = {
78
+ general: SETTINGS,
79
+ organization: ORGANIZATION,
80
+ members: `${ORGANIZATION}/members`,
81
+ billing: `${ORGANIZATION}/billing`,
82
+ usage: `${ORGANIZATION}/usage`,
83
+ keys: `${ORGANIZATION}/keys`,
84
+ };
85
+ /** Whether a section of Settings (a builder section, or what `showSettings` asks for) has a page of its own. */
86
+ export const paged = (section) => Object.hasOwn(PAGES, section);
87
+ /** The Settings page a section names; one with no page of its own opens General, /settings. */
88
+ export const settings = (section) => (section && paged(section) ? PAGES[section] : SETTINGS);
89
+ /**
90
+ * The app's address for a builder address (`route` in @hanzo/build): Dev's
91
+ * path, '' New at /dev, and what is not an address New too. A section of the
92
+ * builder's Settings is the Settings page that answers for it (`settings`),
93
+ * never a path under /dev. Machines and Environments, which the builder files
94
+ * under Settings and writes as places of their own (/machines, /environments),
95
+ * stay Dev's.
96
+ */
97
+ export function app(at) {
98
+ const r = route(at);
99
+ const to = href(r);
100
+ return r.kind === 'settings' && (to === SETTINGS || to.startsWith(`${SETTINGS}/`)) ? settings(r.section) : DEV + to;
101
+ }
68
102
  /** /chat/shared is a page of its own, where a chat shared by link is read (lib/share.ts). */
69
103
  const SHARED = 'shared';
70
104
  /** The place a path names, or null when it names none of the app's. */
@@ -137,6 +171,19 @@ export function go(address, replace = false) {
137
171
  else
138
172
  window.history.pushState(null, '', address);
139
173
  }
174
+ /**
175
+ * Move to an address of this site: one of the app's (`place`) by `go`, which
176
+ * the app follows; any other page — Settings — through the host's router, which
177
+ * draws that page. A history entry, or in place of this one.
178
+ */
179
+ export function visit(router, address, replace = false) {
180
+ if (place(address.split(/[?#]/)[0]))
181
+ go(address, replace);
182
+ else if (replace)
183
+ router.replace(address);
184
+ else
185
+ router.push(address);
186
+ }
140
187
  /** Where this browser keeps the mode its reader was last in. */
141
188
  const MODE = 'hanzo.app.mode';
142
189
  /** Keep the mode the reader is in, for `resume`. */
@@ -160,8 +207,6 @@ export function resume() {
160
207
  }
161
208
  /** The routes under app/(app) the apex keeps: the app — /chat, a chat shared by link, and /dev. */
162
209
  const KEPT = new Set(['/chat', '/chat/shared', '/dev']);
163
- /** Settings, and every page under it: the person's and the organization's, served wherever the app is. */
164
- const SETTINGS = '/settings';
165
210
  /**
166
211
  * The workspace's rooms: every route under app/(app) the apex does not keep.
167
212
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hanzo/rooms",
3
- "version": "0.1.99",
3
+ "version": "0.1.101",
4
4
  "description": "The workspace rooms — Home, Chat, Inbox, Contacts, Meet, Cal, Drive, Board, Work, Bots — and the frame that holds them, once, for every Hanzo surface.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",