@hanzo/rooms 0.1.68 → 0.1.69

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
@@ -1,7 +1,9 @@
1
1
  'use client';
2
2
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- // The web app as the whole window, in two modes: Chat, a conversation with the
4
- // product's AI, and Dev, @hanzo/build's `<Builder>` without a rail of its own.
3
+ // The web app as the whole window, in three modes: Chat, a conversation with
4
+ // the product's AI; Dev, @hanzo/build's `<Builder>` without a rail of its own;
5
+ // and Studio, @hanzo/studio's `<Studio>` the same way, its places (`nav`) in the
6
+ // sidebar where Dev's stand for Dev.
5
7
  //
6
8
  // OUTER the rail (Rail.tsx) and the sidebar beside it (Column.tsx), on the
7
9
  // window's floor, with the window's bar above them where the host has
@@ -14,11 +16,12 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
14
16
  // search, New, Dev's places and the reader's Recents. The address says the
15
17
  // mode, as a path (lib/host `talk`, `app`, `place`): `/` and `/chat` are Chat on
16
18
  // a new conversation, `/chat/<id>` one of the reader's, and a path under `/dev`
17
- // is Dev: `/dev/run/<id>` a run, `/dev/projects/<org>/<repo>` a project. Moving
19
+ // is Dev: `/dev/run/<id>` a run, `/dev/projects/<org>/<repo>` a project; a path
20
+ // under `/studio` is Studio, its own address after it (`c/<id>`, `library`). Moving
18
21
  // between the modes is a history entry (`go`), never a load, so a host serves
19
22
  // this one component for every path under each.
20
23
  //
21
- // Every host draws this: hanzo.ai at `/`, `/chat` and `/dev`, and the desktop
24
+ // Every host draws this: hanzo.ai at `/`, `/chat`, `/dev` and `/studio`, and the desktop
22
25
  // app as its main window. The host mounts IamProvider and `<Rooms>` with its
23
26
  // router and the path, and draws this only for a signed-in reader; what a
24
27
  // stranger sees is the host's.
@@ -46,13 +49,19 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
46
49
  // EVERY MOVE PUTS THE DRAWER AWAY, the one to where the reader already is
47
50
  // included.
48
51
  //
52
+ // STUDIO LOADS WITH ITS MODE. Its module (and three.js behind a 3D tab) is
53
+ // fetched the first time the reader stands in Studio; its places are a few
54
+ // lines and come with the app. Another organization opens it at its New, so
55
+ // nothing it read under the last one stays.
56
+ //
49
57
  // THE PLANS' OWN ADDRESS LEAVES for the pay site in its place (dev.tsx `leave`),
50
58
  // so the builder's own plans are never drawn.
51
- import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
59
+ import { lazy, Suspense, useCallback, useEffect, useMemo, useRef, useState } from 'react';
52
60
  import { ArrowLeft, ArrowRight, Menu } from 'lucide-react';
53
61
  import { AiProvider } from '@hanzo/ai/react';
54
62
  import { Text, XStack, YStack } from '@hanzo/gui';
55
63
  import { Builder, HostProvider, path as written, route } from '@hanzo/build';
64
+ import { nav as places } from '@hanzo/studio';
56
65
  import { SidebarIconButton } from '@hanzo/ui/chat';
57
66
  import { useAi } from './lib/ai.js';
58
67
  import { Chat } from './Chat.js';
@@ -66,8 +75,10 @@ import { empty, openThread, useOpen } from './open.js';
66
75
  import { Paned } from './Shell.js';
67
76
  import { floor, pane } from './ground.js';
68
77
  import { pick } from './lib/session.js';
69
- import { app, go, place, talk } from './lib/host.js';
78
+ import { app, go, place, studio, talk } from './lib/host.js';
70
79
  import { wordmark } from './where.js';
80
+ /** Studio, fetched the first time the reader stands in it. */
81
+ const Studio = lazy(() => import('@hanzo/studio').then((m) => ({ default: m.Studio })));
71
82
  /** `/` is Chat on a new conversation. */
72
83
  const HOME = { mode: 'chat', thread: null };
73
84
  /** The bar's height, px: the reference window's title bar. */
@@ -85,7 +96,7 @@ const INSET = {
85
96
  left: 'max(var(--pane-gap), var(--safe-left))',
86
97
  };
87
98
  /** Where each mode was last, so the rail goes back to it. */
88
- const last = { chat: '', dev: '' };
99
+ const last = { chat: '', dev: '', studio: '' };
89
100
  /** Whether a key press is the reader typing, where the app's keys are the field's. */
90
101
  const typing = (e) => {
91
102
  const el = e.target;
@@ -119,6 +130,22 @@ function Window({ bar }) {
119
130
  const at = place(here) ?? HOME;
120
131
  const mode = at.mode;
121
132
  const where = at.mode === 'dev' ? at.at : null;
133
+ // Studio's host: Dev's, at Studio's address, its moves under /studio, and
134
+ // another organization opening Studio's New in it.
135
+ const made = at.mode === 'studio' ? at.at : '';
136
+ const maker = useMemo(() => ({
137
+ ...host,
138
+ name: 'Hanzo Studio',
139
+ path: made,
140
+ go: (to, how) => {
141
+ shut();
142
+ go(studio(to), how?.replace);
143
+ },
144
+ chooseOrg: (next) => {
145
+ pick(next);
146
+ window.location.assign(studio(''));
147
+ },
148
+ }), [host, made, shut]);
122
149
  // A run floats two panes of its own, so the pane it stands in is not cut.
123
150
  const run = where !== null && route(where).kind === 'run';
124
151
  const asked = at.mode === 'chat' ? at.thread : null;
@@ -202,11 +229,13 @@ function Window({ bar }) {
202
229
  }, [bar, router, move]);
203
230
  const toChat = () => move(last.chat || talk());
204
231
  const toDev = () => move(last.dev || app(''));
232
+ const toStudio = () => move(last.studio || studio(''));
205
233
  // Another organization: picked, and the app opened in it at the mode's New.
206
234
  const org = (o) => {
207
235
  pick(o);
208
- window.location.assign(mode === 'dev' ? app('') : talk());
236
+ window.location.assign(mode === 'dev' ? app('') : mode === 'studio' ? studio('') : talk());
209
237
  };
238
+ const sidebar = mode === 'studio' ? places(maker, (p) => maker.go(p)) : undefined;
210
239
  const flip = () => setClosed(!closed);
211
- return (_jsx(AiProvider, { client: client ?? undefined, children: _jsx(HostProvider, { host: host, children: _jsxs(YStack, { "data-slot": "window", ...(bar ? floor : null), flex: 1, minW: 0, height: "100dvh", overflow: "hidden", children: [bar ? (_jsxs(XStack, { "data-slot": "title-bar", height: BAR, shrink: 0, items: "center", gap: 2, pl: (bar.inset ?? 0) + 6, pr: "$2", ...bar.drag, children: [_jsx(XStack, { "$max-md": { display: 'none' }, children: _jsx(Toggle, { open: !closed, onPress: flip }) }), _jsx(XStack, { $md: { display: 'none' }, children: _jsx(Toggle, { open: drawer, onPress: () => setDrawer(true), label: "Open sidebar" }) }), _jsx(Mark, { label: "Back", "data-slot": "back", onPress: () => router.back(), children: _jsx(ArrowLeft, { size: 18, "aria-hidden": true }) }), _jsx(Mark, { label: "Forward", "data-slot": "forward", onPress: () => router.forward(), children: _jsx(ArrowRight, { size: 18, "aria-hidden": true }) }), _jsx(XStack, { flex: 1, self: "stretch", ...bar.drag })] })) : null, _jsxs(XStack, { "data-room": "", "data-app": mode, "data-slot": "frame", ...(bar ? null : floor), flex: 1, minW: 0, minH: 0, pr: "var(--pane-gap)", pb: "var(--pane-gap)", pt: bar ? 0 : 'var(--pane-gap)', "$max-md": { p: 0 }, children: [_jsx(YStack, { height: "100%", "$max-md": { display: 'none' }, children: _jsx(Rail, { host: host, mode: mode, at: where, open: !closed, toggle: bar ? undefined : flip, onChat: toChat, onDev: toDev, go: move, onOrg: org }) }), closed ? null : (_jsx(YStack, { "$max-md": { display: 'none' }, children: _jsx(Column, { host: host, mode: mode, thread: thread, go: move, onShut: () => setClosed(true), seek: seek }) })), _jsxs(YStack, { "data-slot": "stage", flex: 1, minW: 0, minH: 0, ml: "var(--pane-gap)", "$max-md": { ml: 0 }, children: [bar ? null : (_jsxs(XStack, { "data-slot": "app-bar", height: 44, px: "$1", gap: "$1", items: "center", shrink: 0, $md: { display: 'none' }, children: [_jsx(SidebarIconButton, { label: "Open sidebar", onPress: () => setDrawer(true), width: 44, height: 44, rounded: 999, children: _jsx(Menu, { size: 18 }) }), _jsx(Text, { fontSize: "$4", fontWeight: "600", color: "$ink", numberOfLines: 1, children: wordmark(mode) })] })), _jsx(XStack, { "data-slot": "panes", flex: 1, minW: 0, minH: 0, children: mode === 'chat' ? (_jsx(Paned, { framed: true, children: _jsx(Chat, {}) })) : (_jsx(YStack, { render: "main", "data-slot": "pane", ...(run ? null : { ...pane(), overflow: 'hidden' }), flex: 1, minW: 0, minH: 0, children: plans ? null : _jsx(Builder, { host: host, rail: false }) })) })] })] }), drawer ? (_jsxs(_Fragment, { children: [_jsx(YStack, { render: "button", "aria-label": "Close sidebar", onPress: () => setDrawer(false), bg: DIM, position: "absolute", t: 0, r: 0, b: 0, l: 0, z: "var(--z-overlay)", $md: { display: 'none' } }), _jsx(YStack, { "data-slot": "drawer", position: "absolute", t: INSET.top, b: INSET.bottom, l: INSET.left, width: "min(320px, calc(100% - 2 * var(--pane-gap) - 48px))", z: "var(--z-modal)", $md: { display: 'none' }, children: _jsx(Column, { host: host, mode: mode, thread: thread, go: move, onShut: shut, seek: seek, drawer: true, foot: _jsx(Rail, { host: host, mode: mode, at: where, open: true, flat: true, onChat: toChat, onDev: toDev, go: move, onOrg: org }) }) })] })) : null] }) }) }));
240
+ return (_jsx(AiProvider, { client: client ?? undefined, children: _jsx(HostProvider, { host: host, children: _jsxs(YStack, { "data-slot": "window", ...(bar ? floor : null), flex: 1, minW: 0, height: "100dvh", overflow: "hidden", children: [bar ? (_jsxs(XStack, { "data-slot": "title-bar", height: BAR, shrink: 0, items: "center", gap: 2, pl: (bar.inset ?? 0) + 6, pr: "$2", ...bar.drag, children: [_jsx(XStack, { "$max-md": { display: 'none' }, children: _jsx(Toggle, { open: !closed, onPress: flip }) }), _jsx(XStack, { $md: { display: 'none' }, children: _jsx(Toggle, { open: drawer, onPress: () => setDrawer(true), label: "Open sidebar" }) }), _jsx(Mark, { label: "Back", "data-slot": "back", onPress: () => router.back(), children: _jsx(ArrowLeft, { size: 18, "aria-hidden": true }) }), _jsx(Mark, { label: "Forward", "data-slot": "forward", onPress: () => router.forward(), children: _jsx(ArrowRight, { size: 18, "aria-hidden": true }) }), _jsx(XStack, { flex: 1, self: "stretch", ...bar.drag })] })) : null, _jsxs(XStack, { "data-room": "", "data-app": mode, "data-slot": "frame", ...(bar ? null : floor), flex: 1, minW: 0, minH: 0, pr: "var(--pane-gap)", pb: "var(--pane-gap)", pt: bar ? 0 : 'var(--pane-gap)', "$max-md": { p: 0 }, children: [_jsx(YStack, { height: "100%", "$max-md": { display: 'none' }, children: _jsx(Rail, { host: host, mode: mode, at: where, open: !closed, toggle: bar ? undefined : flip, onChat: toChat, onDev: toDev, onStudio: toStudio, go: move, onOrg: org }) }), closed ? null : (_jsx(YStack, { "$max-md": { display: 'none' }, children: _jsx(Column, { host: host, mode: mode, thread: thread, go: move, onShut: () => setClosed(true), seek: seek, places: sidebar }) })), _jsxs(YStack, { "data-slot": "stage", flex: 1, minW: 0, minH: 0, ml: "var(--pane-gap)", "$max-md": { ml: 0 }, children: [bar ? null : (_jsxs(XStack, { "data-slot": "app-bar", height: 44, px: "$1", gap: "$1", items: "center", shrink: 0, $md: { display: 'none' }, children: [_jsx(SidebarIconButton, { label: "Open sidebar", onPress: () => setDrawer(true), width: 44, height: 44, rounded: 999, children: _jsx(Menu, { size: 18 }) }), _jsx(Text, { fontSize: "$4", fontWeight: "600", color: "$ink", numberOfLines: 1, children: wordmark(mode) })] })), _jsx(XStack, { "data-slot": "panes", flex: 1, minW: 0, minH: 0, children: mode === 'chat' ? (_jsx(Paned, { framed: true, children: _jsx(Chat, {}) })) : mode === 'studio' ? (_jsx(YStack, { render: "main", "data-slot": "pane", ...pane(), overflow: "hidden", flex: 1, minW: 0, minH: 0, children: _jsx(YStack, { flex: 1, minH: 0, height: "100%", children: _jsx(Suspense, { fallback: null, children: _jsx(Studio, { host: maker, rail: false }) }) }) })) : (_jsx(YStack, { render: "main", "data-slot": "pane", ...(run ? null : { ...pane(), overflow: 'hidden' }), flex: 1, minW: 0, minH: 0, children: plans ? null : _jsx(Builder, { host: host, rail: false }) })) })] })] }), drawer ? (_jsxs(_Fragment, { children: [_jsx(YStack, { render: "button", "aria-label": "Close sidebar", onPress: () => setDrawer(false), bg: DIM, position: "absolute", t: 0, r: 0, b: 0, l: 0, z: "var(--z-overlay)", $md: { display: 'none' } }), _jsx(YStack, { "data-slot": "drawer", position: "absolute", t: INSET.top, b: INSET.bottom, l: INSET.left, width: "min(320px, calc(100% - 2 * var(--pane-gap) - 48px))", z: "var(--z-modal)", $md: { display: 'none' }, children: _jsx(Column, { host: host, mode: mode, thread: thread, go: move, onShut: shut, seek: seek, drawer: true, places: sidebar, foot: _jsx(Rail, { host: host, mode: mode, at: where, open: true, flat: true, onChat: toChat, onDev: toDev, onStudio: toStudio, go: move, onOrg: org }) }) })] })) : null] }) }) }));
212
241
  }
package/dist/Column.d.ts CHANGED
@@ -1,9 +1,10 @@
1
1
  import { type Host } from '@hanzo/build';
2
+ import { type RailLink } from '@hanzo/ui/chat';
2
3
  import { type ReactNode } from 'react';
3
- import { type Mode } from './recent.js';
4
- export declare function Column({ host, mode, thread, go, onShut, seek, drawer, foot, }: {
4
+ import { type Place } from './lib/host.js';
5
+ export declare function Column({ host, mode, thread, go, onShut, seek, drawer, foot, places, }: {
5
6
  host: Host;
6
- mode: Mode;
7
+ mode: Place['mode'];
7
8
  /** The Chat conversation the pane holds, or null. */
8
9
  thread: string | null;
9
10
  /** Moves the app to an address of this page. */
@@ -16,4 +17,10 @@ export declare function Column({ host, mode, thread, go, onShut, seek, drawer, f
16
17
  drawer?: boolean;
17
18
  /** What stands at its foot: the drawer's rail, laid flat. */
18
19
  foot?: ReactNode;
20
+ /** The mode's places, where they are not Dev's (Studio's `nav`). */
21
+ places?: {
22
+ id: string;
23
+ label: string;
24
+ links: RailLink[];
25
+ }[];
19
26
  }): import("react").JSX.Element;
package/dist/Column.js CHANGED
@@ -89,7 +89,7 @@ function Activity({ host }) {
89
89
  ];
90
90
  return (_jsx(DropdownMenu, { minWidth: 280, trigger: _jsx(XStack, { render: "button", "aria-label": "Activity", ...{ title: 'Activity' }, ...HEAD, items: "center", justify: "center", cursor: "pointer", opacity: 0.7, hoverStyle: { bg: '$hover', opacity: 1 }, focusVisibleStyle: { outlineWidth: 2, outlineStyle: 'solid', outlineColor: '$outlineColor' }, children: _jsx(Bell, { size: 16, "aria-hidden": true }) }), items: items }));
91
91
  }
92
- export function Column({ host, mode, thread, go, onShut, seek = 0, drawer = false, foot, }) {
92
+ export function Column({ host, mode, thread, go, onShut, seek = 0, drawer = false, foot, places, }) {
93
93
  const [words, setWords] = useState('');
94
94
  // The search field shows once asked for, and while it holds words.
95
95
  const [seeking, setSeeking] = useState(false);
@@ -98,7 +98,7 @@ export function Column({ host, mode, thread, go, onShut, seek = 0, drawer = fals
98
98
  const uid = useId();
99
99
  const span = useSpan(SPAN, WIDTH, FLOOR, CEIL);
100
100
  const dev = (p) => host.go(p);
101
- const groups = nav(host, dev);
101
+ const groups = places ?? nav(host, dev);
102
102
  const open = (row) => (row.kind === 'chat' ? go(talk(row.id)) : dev(row.id));
103
103
  // Chat's New; Dev's is the first of its places.
104
104
  const create = () => {
@@ -119,13 +119,13 @@ export function Column({ host, mode, thread, go, onShut, seek = 0, drawer = fals
119
119
  }, [seek]);
120
120
  return (_jsxs(YStack, { "data-slot": "app-column", ref: drawer ? undefined : span.ref,
121
121
  // inline-style: `--span`, the width a drag rewrites on the element itself (span.ts); no prop sets a variable.
122
- style: drawer ? undefined : span.style, position: "relative", flex: drawer ? 1 : undefined, shrink: drawer ? 1 : 0, minW: 0, height: "100%", width: drawer ? undefined : 'var(--span)', children: [_jsxs(Sidebar, { "data-slot": "app-rail", role: "navigation", "aria-label": brand(), ...(drawer ? { ...pane(), ...FLOAT } : { bg: 'transparent', borderWidth: 0 }), ...FILL, px: "$2", pt: drawer ? '$2' : 0, pb: "$2", gap: "$0", children: [_jsxs(XStack, { "data-slot": "app-head", items: "center", gap: "$1", pl: "$2", pr: 2, height: 52, shrink: 0, minW: 0, children: [_jsx(Text, { flex: 1, minW: 0, fontSize: 18, lineHeight: 24, fontWeight: "600", color: "$ink", numberOfLines: 1, children: wordmark(mode) }), _jsx(Activity, { host: host }), _jsx(SidebarIconButton, { label: mode === 'chat' ? 'Search chats' : 'Search runs', onPress: seeking ? done : ask, "aria-expanded": seeking || !!words, ...HEAD, children: _jsx(Search, { size: 16, "aria-hidden": true }) })] }), seeking || words ? (_jsxs(XStack, { "data-slot": "app-filter", items: "center", gap: "$2", pl: "$3", pr: "$1", height: 36, rounded: 999, borderWidth: 1, borderColor: "$borderColor", bg: "$edge", shrink: 0, mb: "$1.5", children: [_jsx(Search, { size: 15, "aria-hidden": true, color: "var(--soft, var(--muted-foreground))" }), _jsx(Text, { render: _jsx("input", { ref: field, value: words, onChange: (e) => setWords(e.target.value), onKeyDown: (e) => {
122
+ style: drawer ? undefined : span.style, position: "relative", flex: drawer ? 1 : undefined, shrink: drawer ? 1 : 0, minW: 0, height: "100%", width: drawer ? undefined : 'var(--span)', children: [_jsxs(Sidebar, { "data-slot": "app-rail", role: "navigation", "aria-label": brand(), ...(drawer ? { ...pane(), ...FLOAT } : { bg: 'transparent', borderWidth: 0 }), ...FILL, px: "$2", pt: drawer ? '$2' : 0, pb: "$2", gap: "$0", children: [_jsxs(XStack, { "data-slot": "app-head", items: "center", gap: "$1", pl: "$2", pr: 2, height: 52, shrink: 0, minW: 0, children: [_jsx(Text, { flex: 1, minW: 0, fontSize: 18, lineHeight: 24, fontWeight: "600", color: "$ink", numberOfLines: 1, children: wordmark(mode) }), _jsx(Activity, { host: host }), mode === 'studio' ? null : (_jsx(SidebarIconButton, { label: mode === 'chat' ? 'Search chats' : 'Search runs', onPress: seeking ? done : ask, "aria-expanded": seeking || !!words, ...HEAD, children: _jsx(Search, { size: 16, "aria-hidden": true }) }))] }), seeking || words ? (_jsxs(XStack, { "data-slot": "app-filter", items: "center", gap: "$2", pl: "$3", pr: "$1", height: 36, rounded: 999, borderWidth: 1, borderColor: "$borderColor", bg: "$edge", shrink: 0, mb: "$1.5", children: [_jsx(Search, { size: 15, "aria-hidden": true, color: "var(--soft, var(--muted-foreground))" }), _jsx(Text, { render: _jsx("input", { ref: field, value: words, onChange: (e) => setWords(e.target.value), onKeyDown: (e) => {
123
123
  if (e.key === 'Escape')
124
124
  done();
125
125
  }, onBlur: () => {
126
126
  if (!words)
127
127
  setSeeking(false);
128
- }, placeholder: mode === 'chat' ? 'Search chats' : 'Search runs', "aria-label": "Filter recents" }), flex: 1, minW: 0, bg: "transparent", borderWidth: 0, outlineStyle: "none", color: "inherit", fontSize: "$3" }), words ? (_jsx(SidebarIconButton, { label: "Clear search", onPress: done, width: 28, height: 28, rounded: 999, children: _jsx(X, { size: 14, "aria-hidden": true }) })) : null] })) : null, mode === 'chat' ? (_jsx(YStack, { role: "list", gap: 2, shrink: 0, children: _jsx(YStack, { role: "listitem", children: _jsx(SidebarItem, { "data-slot": "app-new", ...ROW, active: !thread, bg: "transparent", hoverStyle: { bg: '$hover' }, icon: _jsx(SquarePen, { size: 16, "aria-hidden": true }), onPress: create, ...keys(create), children: _jsx(Text, { fontSize: "$3", color: "$ink", children: "New chat" }) }) }) })) : (_jsx(YStack, { "data-slot": "app-places", shrink: 0, children: groups.map((g, i) => (_jsxs(YStack, { pt: i ? '$2' : 0, children: [i ? (_jsx(Text, { id: `${uid}-${g.id}`, pl: "$2", pb: 2, fontSize: "$3", color: "$soft", children: g.label })) : null, _jsx(YStack, { role: "list", "aria-labelledby": i ? `${uid}-${g.id}` : undefined, "aria-label": i ? undefined : g.label, gap: 1, children: g.links.map((link) => (_jsx(YStack, { role: "listitem", children: _jsx(SidebarItem, { ...ROW, ...(link.id === NEW ? { 'data-slot': 'app-new' } : {}), active: link.active, icon: link.id === NEW ? _jsx(SquarePen, { size: 16, "aria-hidden": true }) : link.icon, onPress: link.onPress, ...keys(() => link.onPress?.()), children: _jsx(Text, { fontSize: "$3", color: link.active ? '$ink' : '$quiet', children: link.label }) }) }, link.id))) })] }, g.id))) })), _jsx(Recents, { host: host, mode: mode, thread: thread, words: words, onOpen: open, onNew: create }), _jsxs(YStack, { gap: "$2", pt: "$2", shrink: 0, children: [_jsx(Made, {}), _jsx(Setup, {})] }), foot] }), drawer ? null : (_jsx(Grip, { side: "left", span: span.span, floor: FLOOR, ceil: CEIL, reset: WIDTH, onSpan: span.move, onKeep: span.keep, onShut: onShut, label: "Resize sidebar",
128
+ }, placeholder: mode === 'chat' ? 'Search chats' : 'Search runs', "aria-label": "Filter recents" }), flex: 1, minW: 0, bg: "transparent", borderWidth: 0, outlineStyle: "none", color: "inherit", fontSize: "$3" }), words ? (_jsx(SidebarIconButton, { label: "Clear search", onPress: done, width: 28, height: 28, rounded: 999, children: _jsx(X, { size: 14, "aria-hidden": true }) })) : null] })) : null, mode === 'chat' ? (_jsx(YStack, { role: "list", gap: 2, shrink: 0, children: _jsx(YStack, { role: "listitem", children: _jsx(SidebarItem, { "data-slot": "app-new", ...ROW, active: !thread, bg: "transparent", hoverStyle: { bg: '$hover' }, icon: _jsx(SquarePen, { size: 16, "aria-hidden": true }), onPress: create, ...keys(create), children: _jsx(Text, { fontSize: "$3", color: "$ink", children: "New chat" }) }) }) })) : (_jsx(YStack, { "data-slot": "app-places", shrink: 0, children: groups.map((g, i) => (_jsxs(YStack, { pt: i ? '$2' : 0, children: [i ? (_jsx(Text, { id: `${uid}-${g.id}`, pl: "$2", pb: 2, fontSize: "$3", color: "$soft", children: g.label })) : null, _jsx(YStack, { role: "list", "aria-labelledby": i ? `${uid}-${g.id}` : undefined, "aria-label": i ? undefined : g.label, gap: 1, children: g.links.map((link) => (_jsx(YStack, { role: "listitem", children: _jsx(SidebarItem, { ...ROW, ...(link.id === NEW ? { 'data-slot': 'app-new' } : {}), active: link.active, icon: link.id === NEW ? _jsx(SquarePen, { size: 16, "aria-hidden": true }) : link.icon, onPress: link.onPress, ...keys(() => link.onPress?.()), children: _jsx(Text, { fontSize: "$3", color: link.active ? '$ink' : '$quiet', children: link.label }) }) }, link.id))) })] }, g.id))) })), mode === 'studio' ? null : _jsx(Recents, { host: host, mode: mode, thread: thread, words: words, onOpen: open, onNew: create }), _jsxs(YStack, { gap: "$2", pt: "$2", shrink: 0, children: [_jsx(Made, {}), _jsx(Setup, {})] }), foot] }), drawer ? null : (_jsx(Grip, { side: "left", span: span.span, floor: FLOOR, ceil: CEIL, reset: WIDTH, onSpan: span.move, onKeep: span.keep, onShut: onShut, label: "Resize sidebar",
129
129
  // In the gutter between the sidebar and the panel, not over the sidebar's own rows.
130
130
  r: GUTTER_END, width: "var(--pane-gap)" }))] }));
131
131
  }
package/dist/Rail.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import type { ReactNode } from 'react';
2
2
  import { type Host } from '@hanzo/build';
3
- import type { Mode } from './recent.js';
3
+ import { type Place } from './lib/host.js';
4
4
  /** The rail's width, px. */
5
5
  export declare const RAIL = 48;
6
6
  /** One mark: an icon button named for where it goes, its name its tooltip, lit where the reader is. */
@@ -18,9 +18,9 @@ export declare function Toggle({ open, onPress, label }: {
18
18
  onPress: () => void;
19
19
  label?: string;
20
20
  }): import("react").JSX.Element;
21
- export declare function Rail({ host, mode, at, open, toggle, flat, onChat, onDev, go, onOrg, }: {
21
+ export declare function Rail({ host, mode, at, open, toggle, flat, onChat, onDev, onStudio, go, onOrg, }: {
22
22
  host: Host;
23
- mode: Mode;
23
+ mode: Place['mode'];
24
24
  /** Dev's builder address, or null in Chat. */
25
25
  at: string | null;
26
26
  /** The sidebar beside the rail is open. */
@@ -29,9 +29,10 @@ export declare function Rail({ host, mode, at, open, toggle, flat, onChat, onDev
29
29
  toggle?: () => void;
30
30
  /** Laid along the foot of the phone's drawer rather than down the window's edge. */
31
31
  flat?: boolean;
32
- /** To Chat and to Dev, each where the reader left it. */
32
+ /** To Chat, Dev and Studio, each where the reader left it. */
33
33
  onChat: () => void;
34
34
  onDev: () => void;
35
+ onStudio: () => void;
35
36
  /** Moves the app to an address of this page. */
36
37
  go: (address: string) => void;
37
38
  /** Work in another organization. */
package/dist/Rail.js CHANGED
@@ -5,8 +5,8 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
5
5
  // menu of the rest, and the account at the foot. It is how the app moves
6
6
  // between its products; there is no switch anywhere else.
7
7
  //
8
- // Studio is its own page, hanzo.ai/studio, opened in this tab: the app draws it
9
- // there for a signed-in reader.
8
+ // Studio is the app's third mode (@hanzo/studio at /studio), each of the three
9
+ // opened where the reader left it.
10
10
  //
11
11
  // Library, Scheduled tasks and Customize are Dev's own places (@hanzo/build's
12
12
  // Artifacts, Automations and Customize), raised to the rail; each is lit while
@@ -27,8 +27,7 @@ import { XStack, YStack } from '@hanzo/gui';
27
27
  import { route } from '@hanzo/build';
28
28
  import { DropdownMenu } from '@hanzo/ui';
29
29
  import { Me } from './Me.js';
30
- import { app, STUDIO, TABS, WORKSPACE } from './lib/host.js';
31
- import { site } from './where.js';
30
+ import { app, TABS, WORKSPACE } from './lib/host.js';
32
31
  /** The rail's width, px. */
33
32
  export const RAIL = 48;
34
33
  /** A mark, px: a 36px square on a 44px pitch. */
@@ -72,7 +71,7 @@ export function Mark({ label, on = false, onPress, children, ...rest }) {
72
71
  export function Toggle({ open, onPress, label }) {
73
72
  return (_jsx(Mark, { label: label ?? (open ? 'Close sidebar' : 'Open sidebar'), "data-slot": "sidebar-toggle", "aria-expanded": open, onPress: onPress, children: _jsx(PanelLeft, { size: 18, "aria-hidden": true }) }));
74
73
  }
75
- export function Rail({ host, mode, at, open, toggle, flat = false, onChat, onDev, go, onOrg, }) {
74
+ export function Rail({ host, mode, at, open, toggle, flat = false, onChat, onDev, onStudio, go, onOrg, }) {
76
75
  const here = mode === 'dev' ? raised(at) : null;
77
76
  const to = (p) => () => go(app(p));
78
77
  // Everything else the app reaches, behind More: on a phone the raised places first, then Dev's other places, then the products on hosts of their own.
@@ -95,5 +94,5 @@ export function Rail({ host, mode, at, open, toggle, flat = false, onChat, onDev
95
94
  { key: 'team', label: 'Team', icon: _jsx(Users, { size: 16, "aria-hidden": true }), onSelect: () => out(WORKSPACE) },
96
95
  ];
97
96
  const Stack = flat ? XStack : YStack;
98
- return (_jsxs(Stack, { "data-slot": "rail", render: "nav", "aria-label": "Products", ...(flat ? { pt: '$2', mt: '$2', borderTopWidth: 1, borderColor: '$borderColor', gap: '$1' } : { width: RAIL, flex: 1, pt: '$2', pb: '$2', gap: 8 }), shrink: 0, items: "center", children: [toggle ? (_jsx(YStack, { pb: 8, children: _jsx(Toggle, { open: open, onPress: toggle }) })) : null, _jsx(Mark, { label: "Chat", on: mode === 'chat', onPress: onChat, "data-slot": "rail-chat", children: _jsx(MessageSquare, { size: 18, "aria-hidden": true }) }), _jsx(Mark, { label: "Dev", on: mode === 'dev' && !here, onPress: onDev, "data-slot": "rail-dev", children: _jsx(Code2, { size: 18, "aria-hidden": true }) }), _jsx(Mark, { label: "Studio", onPress: () => window.location.assign(site(STUDIO)), "data-slot": "rail-studio", children: _jsx(WandSparkles, { size: 18, "aria-hidden": true }) }), flat ? null : (_jsxs(_Fragment, { children: [_jsx(Mark, { label: "Library", on: here === 'library', onPress: to(RAISED.library), "data-slot": "rail-library", children: _jsx(Library, { size: 18, "aria-hidden": true }) }), _jsx(Mark, { label: "Scheduled tasks", on: here === 'scheduled', onPress: to(RAISED.scheduled), "data-slot": "rail-scheduled", children: _jsx(Clock, { size: 18, "aria-hidden": true }) }), _jsx(Mark, { label: "Customize", on: here === 'customize', onPress: to(RAISED.customize), "data-slot": "rail-customize", children: _jsx(Puzzle, { size: 18, "aria-hidden": true }) })] })), _jsx(DropdownMenu, { placement: flat ? 'top-start' : 'right-start', minWidth: 220, trigger: _jsx(XStack, { render: "button", "aria-label": "More", ...{ title: 'More' }, "data-slot": "rail-more", width: MARK, height: MARK, rounded: "$4", items: "center", justify: "center", cursor: "pointer", opacity: 0.62, hoverStyle: { bg: '$hover', opacity: 1 }, pressStyle: { bg: '$raised' }, focusVisibleStyle: { outlineWidth: 2, outlineStyle: 'solid', outlineColor: '$outlineColor' }, $touchable: { width: 44, height: 44 }, children: _jsx(Ellipsis, { size: 18, "aria-hidden": true }) }), items: rest }), _jsx(YStack, { flex: 1 }), _jsx(Me, { host: host, narrow: true, above: flat, onOrg: onOrg })] }));
97
+ return (_jsxs(Stack, { "data-slot": "rail", render: "nav", "aria-label": "Products", ...(flat ? { pt: '$2', mt: '$2', borderTopWidth: 1, borderColor: '$borderColor', gap: '$1' } : { width: RAIL, flex: 1, pt: '$2', pb: '$2', gap: 8 }), shrink: 0, items: "center", children: [toggle ? (_jsx(YStack, { pb: 8, children: _jsx(Toggle, { open: open, onPress: toggle }) })) : null, _jsx(Mark, { label: "Chat", on: mode === 'chat', onPress: onChat, "data-slot": "rail-chat", children: _jsx(MessageSquare, { size: 18, "aria-hidden": true }) }), _jsx(Mark, { label: "Dev", on: mode === 'dev' && !here, onPress: onDev, "data-slot": "rail-dev", children: _jsx(Code2, { size: 18, "aria-hidden": true }) }), _jsx(Mark, { label: "Studio", on: mode === 'studio', onPress: onStudio, "data-slot": "rail-studio", children: _jsx(WandSparkles, { size: 18, "aria-hidden": true }) }), flat ? null : (_jsxs(_Fragment, { children: [_jsx(Mark, { label: "Library", on: here === 'library', onPress: to(RAISED.library), "data-slot": "rail-library", children: _jsx(Library, { size: 18, "aria-hidden": true }) }), _jsx(Mark, { label: "Scheduled tasks", on: here === 'scheduled', onPress: to(RAISED.scheduled), "data-slot": "rail-scheduled", children: _jsx(Clock, { size: 18, "aria-hidden": true }) }), _jsx(Mark, { label: "Customize", on: here === 'customize', onPress: to(RAISED.customize), "data-slot": "rail-customize", children: _jsx(Puzzle, { size: 18, "aria-hidden": true }) })] })), _jsx(DropdownMenu, { placement: flat ? 'top-start' : 'right-start', minWidth: 220, trigger: _jsx(XStack, { render: "button", "aria-label": "More", ...{ title: 'More' }, "data-slot": "rail-more", width: MARK, height: MARK, rounded: "$4", items: "center", justify: "center", cursor: "pointer", opacity: 0.62, hoverStyle: { bg: '$hover', opacity: 1 }, pressStyle: { bg: '$raised' }, focusVisibleStyle: { outlineWidth: 2, outlineStyle: 'solid', outlineColor: '$outlineColor' }, $touchable: { width: 44, height: 44 }, children: _jsx(Ellipsis, { size: 18, "aria-hidden": true }) }), items: rest }), _jsx(YStack, { flex: 1 }), _jsx(Me, { host: host, narrow: true, above: flat, onOrg: onOrg })] }));
99
98
  }
@@ -51,6 +51,8 @@ export declare const DEV = "/dev";
51
51
  * hanzo.ai/studio, whose page draws the app for a signed-in reader.
52
52
  */
53
53
  export declare const STUDIO = "/studio";
54
+ /** Studio at an address of its own (@hanzo/studio `route`): '' is New, /studio. */
55
+ export declare const studio: (at: string) => string;
54
56
  /** Chat on a conversation, or /chat itself for a new one. */
55
57
  export declare const talk: (id?: string | null) => string;
56
58
  /** Dev at a builder address (`route` in @hanzo/build); '' is New, /dev. What is not an address is New. */
@@ -62,6 +64,9 @@ export type Place = {
62
64
  } | {
63
65
  mode: 'dev';
64
66
  at: string;
67
+ } | {
68
+ mode: 'studio';
69
+ at: string;
65
70
  };
66
71
  /** The place a path names, or null when it names none of the app's. */
67
72
  export declare function place(pathname: string): Place | null;
package/dist/lib/host.js CHANGED
@@ -54,6 +54,8 @@ export const DEV = '/dev';
54
54
  * hanzo.ai/studio, whose page draws the app for a signed-in reader.
55
55
  */
56
56
  export const STUDIO = '/studio';
57
+ /** Studio at an address of its own (@hanzo/studio `route`): '' is New, /studio. */
58
+ export const studio = (at) => (at ? `${STUDIO}/${at.replace(/^\/+/, '')}` : STUDIO);
57
59
  /** Chat on a conversation, or /chat itself for a new one. */
58
60
  export const talk = (id) => (id ? `${CHAT}/${encodeURIComponent(id)}` : CHAT);
59
61
  /** Dev at a builder address (`route` in @hanzo/build); '' is New, /dev. What is not an address is New. */
@@ -67,6 +69,8 @@ export function place(pathname) {
67
69
  return { mode: 'chat', thread: null };
68
70
  if (p === DEV || p.startsWith(`${DEV}/`))
69
71
  return { mode: 'dev', at: path(parse(p.slice(DEV.length))) };
72
+ if (p === STUDIO || p.startsWith(`${STUDIO}/`))
73
+ return { mode: 'studio', at: p.slice(STUDIO.length + 1) };
70
74
  if (!p.startsWith(`${CHAT}/`))
71
75
  return null;
72
76
  const id = p.slice(CHAT.length + 1);
@@ -136,7 +140,8 @@ export function remember(mode) {
136
140
  /** Where `/` takes a member: the mode they were last in, Chat when none is kept. */
137
141
  export function resume() {
138
142
  try {
139
- return window.localStorage.getItem(MODE) === 'dev' ? DEV : CHAT;
143
+ const kept = window.localStorage.getItem(MODE);
144
+ return kept === 'dev' ? DEV : kept === 'studio' ? STUDIO : CHAT;
140
145
  }
141
146
  catch {
142
147
  return CHAT;
package/dist/where.d.ts CHANGED
@@ -68,6 +68,6 @@ export declare const where: () => Addresses;
68
68
  /** The product's name, as the host states it. Read at call time, like every address. */
69
69
  export declare const brand: () => string;
70
70
  /** The product's name in each mode of the app: the host's brand in Chat (Hanzo AI, Hanzo Team), Hanzo Dev in Dev. */
71
- export declare const wordmark: (mode: 'chat' | 'dev') => string;
71
+ export declare const wordmark: (mode: 'chat' | 'dev' | 'studio') => string;
72
72
  /** A page of the app's own, from wherever the rooms are mounted. */
73
73
  export declare const site: (path: string) => string;
package/dist/where.js CHANGED
@@ -12,6 +12,6 @@ export const where = () => addresses;
12
12
  /** The product's name, as the host states it. Read at call time, like every address. */
13
13
  export const brand = () => addresses.brand ?? 'Hanzo AI';
14
14
  /** The product's name in each mode of the app: the host's brand in Chat (Hanzo AI, Hanzo Team), Hanzo Dev in Dev. */
15
- export const wordmark = (mode) => (mode === 'dev' ? 'Hanzo Dev' : brand());
15
+ export const wordmark = (mode) => (mode === 'dev' ? 'Hanzo Dev' : mode === 'studio' ? 'Hanzo Studio' : brand());
16
16
  /** A page of the app's own, from wherever the rooms are mounted. */
17
17
  export const site = (path) => `${addresses.site}${path}`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hanzo/rooms",
3
- "version": "0.1.68",
3
+ "version": "0.1.69",
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",
@@ -55,6 +55,7 @@
55
55
  "@hanzo/gui": "^8.3.5",
56
56
  "@hanzo/iam": ">=0.21.28",
57
57
  "@hanzo/logo": ">=1.0.26",
58
+ "@hanzo/studio": ">=0.1.0",
58
59
  "@hanzo/ui": ">=8.27.67",
59
60
  "@hanzo/usage": ">=0.1.18",
60
61
  "@hanzo/voice": ">=0.1.16",
@@ -73,6 +74,7 @@
73
74
  "@hanzo/gui": "8.3.5",
74
75
  "@hanzo/iam": "0.21.28",
75
76
  "@hanzo/logo": "1.0.26",
77
+ "@hanzo/studio": "0.1.0",
76
78
  "@hanzo/ui": "8.27.67",
77
79
  "@hanzo/usage": "0.1.18",
78
80
  "@hanzo/voice": "0.1.16",