@hanzo/rooms 0.1.99 → 0.1.100

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,7 +71,7 @@ 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
76
  import { empty, openThread, useOpen } from './open.js';
77
77
  import { Paned } from './Shell.js';
@@ -79,7 +79,7 @@ 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,8 +97,6 @@ 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
  }
@@ -146,13 +144,13 @@ function Window({ bar }) {
146
144
  // A run floats two panes of its own, so the pane it stands in is not cut.
147
145
  const run = where !== null && route(where).kind === 'run';
148
146
  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).
147
+ const { thread, settings: wanted } = useOpen();
148
+ // A row of the account menu asks for Settings: the drawer goes away as the
149
+ // router leaves for the page (dev.tsx).
152
150
  useEffect(() => {
153
- if (settings)
151
+ if (wanted)
154
152
  shut();
155
- }, [settings, shut]);
153
+ }, [wanted, shut]);
156
154
  const plans = where !== null && isPlans(where);
157
155
  useEffect(() => {
158
156
  if (plans)
@@ -192,15 +190,15 @@ function Window({ bar }) {
192
190
  drop();
193
191
  }, [mode, here, drop]);
194
192
  // 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.
193
+ // tasks, Customize): its Back returns there, Chat or Dev alike.
196
194
  const away = useRef('');
197
- const inPlace = mode === 'dev' && spot(where) !== null;
195
+ const inPlace = mode === 'dev' && raised(where) !== null;
198
196
  useEffect(() => {
199
197
  if (!inPlace)
200
198
  away.current = `${window.location.pathname}${window.location.search}`;
201
199
  }, [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]);
200
+ /** A move: the drawer away, the address in its place; a page that is not the app's (Settings) through the router. */
201
+ const move = useCallback((address) => depart((replace) => visit(router, address, replace)), [depart, router]);
204
202
  // The app's keys.
205
203
  useEffect(() => {
206
204
  const key = (e) => {
@@ -214,7 +212,7 @@ function Window({ bar }) {
214
212
  const mod = e.metaKey || e.ctrlKey;
215
213
  if (mod && !e.shiftKey && !e.altKey && e.key === ',') {
216
214
  e.preventDefault();
217
- move(app(SETTINGS));
215
+ move(settings());
218
216
  return;
219
217
  }
220
218
  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/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 (dev.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`), a link it opens (`open`) and a pane that asks for Settings
21
+ // (`showSettings`, open.ts: the account menu's rows, Chat's "See usage") all
22
+ // reach that page through the host's router (`visit`). The rooms' shell draws
23
+ // its own Settings 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,
@@ -36,7 +38,7 @@ 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
44
  import { showSettings, useOpen } from './open.js';
@@ -44,7 +46,7 @@ import { showSettings, useOpen } from './open.js';
44
46
  const LINKS = {
45
47
  github: app('-/sync'),
46
48
  customize: app('-/customize'),
47
- settings: '/settings',
49
+ settings: settings(),
48
50
  home: '/',
49
51
  };
50
52
  /** The builder's address for the plans. */
@@ -79,24 +81,14 @@ export function useDevHost() {
79
81
  const { router, path: pathname } = useRooms();
80
82
  const here = path(route(at(pathname)));
81
83
  const look = useLook();
82
- const { settings } = useOpen();
83
- // Settings is top-level /settings; organization settings are at /settings/organization.
84
+ const { settings: asked } = useOpen();
85
+ // A pane asked for Settings: its page, through the router.
84
86
  useEffect(() => {
85
- if (!settings)
87
+ if (!asked)
86
88
  return;
87
89
  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]);
90
+ router.push(settings(asked));
91
+ }, [asked, router]);
100
92
  // The token's claims change only when the token does: a sign-in, a refresh.
101
93
  const memberships = useMemo(() => orgs(), [accessToken]); // eslint-disable-line react-hooks/exhaustive-deps
102
94
  const scoped = useMemo(() => org(), [accessToken]); // eslint-disable-line react-hooks/exhaustive-deps
@@ -124,7 +116,7 @@ export function useDevHost() {
124
116
  person: { name, email, avatar },
125
117
  admin: administers(bearer(), scoped),
126
118
  path: here,
127
- go: (to, how) => (isPlans(to) ? leave('here') : go(app(to), how?.replace)),
119
+ go: (to, how) => (isPlans(to) ? leave('here') : visit(router, app(to), how?.replace)),
128
120
  links: LINKS,
129
121
  // The sidebar lists Settings' sections and Customize's tabs (Column.tsx), and Customize › Agents offers the cast.
130
122
  sections: true,
@@ -146,10 +138,8 @@ export function useDevHost() {
146
138
  const to = `${url.pathname}${url.search}${url.hash}`;
147
139
  if (url.origin !== window.location.origin)
148
140
  window.location.assign(link(url.href));
149
- else if (place(url.pathname))
150
- go(to);
151
141
  else
152
- router.push(to);
142
+ visit(router, to);
153
143
  },
154
144
  signIn: () => enter(),
155
145
  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.100",
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",