@hanzo/rooms 0.1.0
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/LICENSE-APACHE +205 -0
- package/LICENSE-MIT +21 -0
- package/README.md +23 -0
- package/dist/Account.d.ts +8 -0
- package/dist/Account.js +58 -0
- package/dist/Beluga.d.ts +12 -0
- package/dist/Beluga.js +152 -0
- package/dist/Billing.d.ts +1 -0
- package/dist/Billing.js +31 -0
- package/dist/Board.d.ts +9 -0
- package/dist/Board.js +447 -0
- package/dist/Boxes.d.ts +1 -0
- package/dist/Boxes.js +173 -0
- package/dist/CalScheduler.d.ts +15 -0
- package/dist/CalScheduler.js +196 -0
- package/dist/Chat.d.ts +35 -0
- package/dist/Chat.js +1049 -0
- package/dist/Column.d.ts +15 -0
- package/dist/Column.js +88 -0
- package/dist/Contacts.d.ts +1 -0
- package/dist/Contacts.js +301 -0
- package/dist/Directory.d.ts +24 -0
- package/dist/Directory.js +356 -0
- package/dist/Drive.d.ts +2 -0
- package/dist/Drive.js +466 -0
- package/dist/Find.d.ts +7 -0
- package/dist/Find.js +82 -0
- package/dist/Guide.d.ts +6 -0
- package/dist/Guide.js +272 -0
- package/dist/Home.d.ts +39 -0
- package/dist/Home.js +306 -0
- package/dist/Inbox.d.ts +9 -0
- package/dist/Inbox.js +193 -0
- package/dist/Market.d.ts +3 -0
- package/dist/Market.js +149 -0
- package/dist/Me.d.ts +27 -0
- package/dist/Me.js +251 -0
- package/dist/Meet.d.ts +9 -0
- package/dist/Meet.js +370 -0
- package/dist/ModeMark.d.ts +8 -0
- package/dist/ModeMark.js +18 -0
- package/dist/OrgSwitcher.d.ts +31 -0
- package/dist/OrgSwitcher.js +120 -0
- package/dist/Orgs.d.ts +10 -0
- package/dist/Orgs.js +582 -0
- package/dist/Profile.d.ts +4 -0
- package/dist/Profile.js +67 -0
- package/dist/Prose.d.ts +5 -0
- package/dist/Prose.js +111 -0
- package/dist/ProviderMark.d.ts +17 -0
- package/dist/ProviderMark.js +277 -0
- package/dist/Recents.d.ts +10 -0
- package/dist/Recents.js +91 -0
- package/dist/RightPane.d.ts +13 -0
- package/dist/RightPane.js +426 -0
- package/dist/Room.d.ts +8 -0
- package/dist/Room.js +50 -0
- package/dist/Scene.d.ts +12 -0
- package/dist/Scene.js +105 -0
- package/dist/Settings.d.ts +46 -0
- package/dist/Settings.js +321 -0
- package/dist/Setup.d.ts +1 -0
- package/dist/Setup.js +36 -0
- package/dist/Share.d.ts +3 -0
- package/dist/Share.js +130 -0
- package/dist/Shared.d.ts +1 -0
- package/dist/Shared.js +113 -0
- package/dist/Shell.d.ts +63 -0
- package/dist/Shell.js +1225 -0
- package/dist/Start.d.ts +1 -0
- package/dist/Start.js +96 -0
- package/dist/Support.d.ts +14 -0
- package/dist/Support.js +122 -0
- package/dist/Talk.d.ts +4 -0
- package/dist/Talk.js +107 -0
- package/dist/Upgrade.d.ts +11 -0
- package/dist/Upgrade.js +75 -0
- package/dist/Usage.d.ts +1 -0
- package/dist/Usage.js +21 -0
- package/dist/Vibe.d.ts +2 -0
- package/dist/Vibe.js +188 -0
- package/dist/Work.d.ts +12 -0
- package/dist/Work.js +256 -0
- package/dist/bots/Bots.d.ts +2 -0
- package/dist/bots/Bots.js +456 -0
- package/dist/bots/Lights.d.ts +8 -0
- package/dist/bots/Lights.js +13 -0
- package/dist/bots/api.d.ts +120 -0
- package/dist/bots/api.js +75 -0
- package/dist/bots/face.d.ts +5 -0
- package/dist/bots/face.js +13 -0
- package/dist/bots/ink.d.ts +7 -0
- package/dist/bots/ink.js +29 -0
- package/dist/bots/kit.d.ts +113 -0
- package/dist/bots/kit.js +43 -0
- package/dist/bots/runtime.d.ts +4 -0
- package/dist/bots/runtime.js +326 -0
- package/dist/card.d.ts +52 -0
- package/dist/card.js +52 -0
- package/dist/cast.d.ts +143 -0
- package/dist/cast.js +251 -0
- package/dist/connect.d.ts +29 -0
- package/dist/connect.js +64 -0
- package/dist/contacts-store.d.ts +111 -0
- package/dist/contacts-store.js +276 -0
- package/dist/conversations.d.ts +54 -0
- package/dist/conversations.js +98 -0
- package/dist/copy.d.ts +23 -0
- package/dist/copy.js +39 -0
- package/dist/crew.d.ts +36 -0
- package/dist/crew.js +304 -0
- package/dist/dev.d.ts +3 -0
- package/dist/dev.js +102 -0
- package/dist/enso.d.ts +70 -0
- package/dist/enso.js +112 -0
- package/dist/failure.d.ts +57 -0
- package/dist/failure.js +143 -0
- package/dist/ground.d.ts +32 -0
- package/dist/ground.js +32 -0
- package/dist/host.d.ts +76 -0
- package/dist/host.js +76 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +12 -0
- package/dist/kept.d.ts +5 -0
- package/dist/kept.js +27 -0
- package/dist/lib/account.d.ts +49 -0
- package/dist/lib/account.js +229 -0
- package/dist/lib/ai.d.ts +46 -0
- package/dist/lib/ai.js +140 -0
- package/dist/lib/api.d.ts +43 -0
- package/dist/lib/api.js +62 -0
- package/dist/lib/bare.d.ts +9 -0
- package/dist/lib/bare.js +7 -0
- package/dist/lib/coding.d.ts +105 -0
- package/dist/lib/coding.js +87 -0
- package/dist/lib/destination.d.ts +102 -0
- package/dist/lib/destination.js +260 -0
- package/dist/lib/durable.d.ts +66 -0
- package/dist/lib/durable.js +107 -0
- package/dist/lib/first.d.ts +11 -0
- package/dist/lib/first.js +23 -0
- package/dist/lib/home.d.ts +61 -0
- package/dist/lib/home.js +54 -0
- package/dist/lib/host.d.ts +104 -0
- package/dist/lib/host.js +132 -0
- package/dist/lib/hostname.d.ts +1 -0
- package/dist/lib/hostname.js +14 -0
- package/dist/lib/hydrated.d.ts +1 -0
- package/dist/lib/hydrated.js +20 -0
- package/dist/lib/integrations.d.ts +21 -0
- package/dist/lib/integrations.js +1 -0
- package/dist/lib/intent.d.ts +6 -0
- package/dist/lib/intent.js +71 -0
- package/dist/lib/invite.d.ts +155 -0
- package/dist/lib/invite.js +245 -0
- package/dist/lib/limits.d.ts +55 -0
- package/dist/lib/limits.js +119 -0
- package/dist/lib/list.d.ts +9 -0
- package/dist/lib/list.js +11 -0
- package/dist/lib/mark.d.ts +13 -0
- package/dist/lib/mark.js +85 -0
- package/dist/lib/mix.d.ts +22 -0
- package/dist/lib/mix.js +22 -0
- package/dist/lib/models.d.ts +27 -0
- package/dist/lib/models.js +106 -0
- package/dist/lib/org.d.ts +109 -0
- package/dist/lib/org.js +140 -0
- package/dist/lib/path.d.ts +9 -0
- package/dist/lib/path.js +29 -0
- package/dist/lib/plans.d.ts +193 -0
- package/dist/lib/plans.js +310 -0
- package/dist/lib/prose.d.ts +13 -0
- package/dist/lib/prose.js +62 -0
- package/dist/lib/reach.d.ts +17 -0
- package/dist/lib/reach.js +70 -0
- package/dist/lib/rooms.json +23 -0
- package/dist/lib/served.d.ts +20 -0
- package/dist/lib/served.js +92 -0
- package/dist/lib/session.d.ts +211 -0
- package/dist/lib/session.js +493 -0
- package/dist/lib/share.d.ts +131 -0
- package/dist/lib/share.js +216 -0
- package/dist/lib/signout.d.ts +14 -0
- package/dist/lib/signout.js +40 -0
- package/dist/lib/tags.d.ts +27 -0
- package/dist/lib/tags.js +46 -0
- package/dist/lib/tap.d.ts +17 -0
- package/dist/lib/tap.js +9 -0
- package/dist/lib/tier.d.ts +115 -0
- package/dist/lib/tier.js +207 -0
- package/dist/lib/tiers.d.ts +9 -0
- package/dist/lib/tiers.js +141 -0
- package/dist/lib/todo.d.ts +125 -0
- package/dist/lib/todo.js +379 -0
- package/dist/look.d.ts +1 -0
- package/dist/look.js +21 -0
- package/dist/meters.d.ts +8 -0
- package/dist/meters.js +31 -0
- package/dist/model.d.ts +4 -0
- package/dist/model.js +44 -0
- package/dist/network.d.ts +64 -0
- package/dist/network.js +216 -0
- package/dist/open.d.ts +81 -0
- package/dist/open.js +149 -0
- package/dist/pane.d.ts +92 -0
- package/dist/pane.js +231 -0
- package/dist/progress.d.ts +46 -0
- package/dist/progress.js +51 -0
- package/dist/recent.d.ts +67 -0
- package/dist/recent.js +161 -0
- package/dist/rooms.d.ts +131 -0
- package/dist/rooms.js +195 -0
- package/dist/roster.d.ts +28 -0
- package/dist/roster.js +70 -0
- package/dist/span.d.ts +14 -0
- package/dist/span.js +50 -0
- package/dist/speech.d.ts +44 -0
- package/dist/speech.js +229 -0
- package/dist/stars.d.ts +16 -0
- package/dist/stars.js +75 -0
- package/dist/tabs.d.ts +26 -0
- package/dist/tabs.js +25 -0
- package/dist/team.d.ts +11 -0
- package/dist/team.gen.d.ts +10 -0
- package/dist/team.gen.js +11 -0
- package/dist/team.js +30 -0
- package/dist/tools.d.ts +35 -0
- package/dist/tools.js +143 -0
- package/dist/tours.d.ts +24 -0
- package/dist/tours.js +311 -0
- package/dist/waiting.d.ts +15 -0
- package/dist/waiting.js +17 -0
- package/dist/where.d.ts +54 -0
- package/dist/where.js +14 -0
- package/package.json +108 -0
package/dist/Column.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type Host } from '@hanzo/build';
|
|
2
|
+
import type { Mode } from './recent.js';
|
|
3
|
+
export declare function Column({ host, mode, thread, onChat, onDev, go, drawer, }: {
|
|
4
|
+
host: Host;
|
|
5
|
+
mode: Mode;
|
|
6
|
+
/** The Chat conversation the pane holds, or null. */
|
|
7
|
+
thread: string | null;
|
|
8
|
+
/** The switch: to Chat and to Dev, each where the reader left it. */
|
|
9
|
+
onChat: () => void;
|
|
10
|
+
onDev: () => void;
|
|
11
|
+
/** Moves the app to an address of this page. */
|
|
12
|
+
go: (address: string) => void;
|
|
13
|
+
/** Drawn in the phone's drawer: no collapse, no edge, always open. */
|
|
14
|
+
drawer?: boolean;
|
|
15
|
+
}): import("react").JSX.Element;
|
package/dist/Column.js
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
// The app's one column, the same in both modes: the Chat / Dev switch, a filter,
|
|
4
|
+
// New, Dev's places, the Recents of the mode it is in, and at the foot the
|
|
5
|
+
// Slack card and the account (components/workspace/Me.tsx).
|
|
6
|
+
//
|
|
7
|
+
// It is a pane on the frame's ground (app/_web.tsx), sized by the edge in the
|
|
8
|
+
// gutter to its right: dragged, keyed or double-clicked back (@hanzo/build's
|
|
9
|
+
// `Grip`), kept per browser. Pulled in past its floor it collapses to a rail of
|
|
10
|
+
// marks, kept under the key the builder's rail kept it under, so a reader's
|
|
11
|
+
// choice outlives the move.
|
|
12
|
+
import { Text, XStack, YStack } from '@hanzo/gui';
|
|
13
|
+
import { ChevronDown, ChevronUp, PanelLeft, Plus, Search } from 'lucide-react';
|
|
14
|
+
import { Grip, nav, path, route, Slack } from '@hanzo/build';
|
|
15
|
+
import { Sidebar, SidebarIconButton, SidebarItem } from '@hanzo/ui/chat';
|
|
16
|
+
import { HanzoMark } from '@hanzogui/shell';
|
|
17
|
+
import { useState } from 'react';
|
|
18
|
+
import { ModeMark } from './ModeMark.js';
|
|
19
|
+
import { useKept } from './kept.js';
|
|
20
|
+
import { Made, Me } from './Me.js';
|
|
21
|
+
import { Recents } from './Recents.js';
|
|
22
|
+
import { Setup } from './Setup.js';
|
|
23
|
+
import { empty } from './open.js';
|
|
24
|
+
import { useSpan } from './span.js';
|
|
25
|
+
import { pane } from './ground.js';
|
|
26
|
+
import { pick } from './lib/session.js';
|
|
27
|
+
import { app, talk } from './lib/host.js';
|
|
28
|
+
/** The grip stands in the gutter between the column and the pane. gui carries a `calc()` to the page unchanged; its
|
|
29
|
+
* types only name custom properties. */
|
|
30
|
+
const GUTTER_END = 'calc(-1 * var(--pane-gap))';
|
|
31
|
+
/** The sidebar fills the column. Its `width` is typed in px alone, though its frame takes any gui size — the type is
|
|
32
|
+
* @hanzo/ui's to widen. */
|
|
33
|
+
const FILL = { width: '100%' };
|
|
34
|
+
/** The column's width, px: its default, and the two a drag holds it between. */
|
|
35
|
+
const WIDTH = 272;
|
|
36
|
+
const FLOOR = 220;
|
|
37
|
+
const CEIL = 420;
|
|
38
|
+
/** The collapsed rail's width, px. */
|
|
39
|
+
const RAIL = 60;
|
|
40
|
+
/** Per reader, per browser. */
|
|
41
|
+
const SPAN = 'hanzo.app.column';
|
|
42
|
+
/** Enter and Space press a row the way a click does. */
|
|
43
|
+
const keys = (press) => ({
|
|
44
|
+
onKeyDown: (e) => {
|
|
45
|
+
if (e.key !== 'Enter' && e.key !== ' ')
|
|
46
|
+
return;
|
|
47
|
+
e.preventDefault?.();
|
|
48
|
+
press();
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
/** A row of the column: a pill, lit where the reader is. */
|
|
52
|
+
const ROW = { rounded: '$4', minH: 34 };
|
|
53
|
+
export function Column({ host, mode, thread, onChat, onDev, go, drawer = false, }) {
|
|
54
|
+
const [kept, setCollapsed] = useKept('hanzo.build.rail', false);
|
|
55
|
+
const collapsed = kept && !drawer;
|
|
56
|
+
const [slack, setSlack] = useKept('hanzo.build.slack', false);
|
|
57
|
+
const [unfolded, setUnfolded] = useState(false);
|
|
58
|
+
const [words, setWords] = useState('');
|
|
59
|
+
const span = useSpan(SPAN, WIDTH, FLOOR, CEIL);
|
|
60
|
+
const dev = (p) => host.go(p);
|
|
61
|
+
const r = route(host.path);
|
|
62
|
+
const fresh = mode === 'chat' ? !thread : r.kind === 'new';
|
|
63
|
+
const { links, more } = nav(host, dev);
|
|
64
|
+
const open = (row) => (row.kind === 'chat' ? go(talk(row.id)) : dev(row.id));
|
|
65
|
+
const create = () => {
|
|
66
|
+
if (mode === 'dev')
|
|
67
|
+
return dev('');
|
|
68
|
+
empty();
|
|
69
|
+
go(talk());
|
|
70
|
+
};
|
|
71
|
+
// Another organization: picked, and the app opened in it at the mode's New.
|
|
72
|
+
const org = (o) => {
|
|
73
|
+
pick(o);
|
|
74
|
+
window.location.assign(mode === 'dev' ? app('') : talk());
|
|
75
|
+
};
|
|
76
|
+
if (collapsed)
|
|
77
|
+
return (_jsx(YStack, { "data-slot": "app-column", "data-collapsed": "", position: "relative", shrink: 0, width: RAIL, "$max-md": { display: 'none' }, children: _jsxs(Sidebar, { "data-slot": "app-rail", "data-collapsed": "true", role: "navigation", "aria-label": "Hanzo", ...pane(), ...FILL, px: "$1.5", py: "$2.5", gap: "$1.5", items: "center", children: [_jsx(SidebarIconButton, { label: "Expand sidebar", onPress: () => setCollapsed(false), width: 36, height: 36, rounded: 999, children: _jsx(PanelLeft, { size: 16 }) }), _jsx(ModeMark, { mode: mode, onChat: onChat, onDev: onDev, stacked: true }), _jsx(SidebarIconButton, { label: mode === 'chat' ? 'New chat' : 'New run', onPress: create, width: 36, height: 36, rounded: 999, children: _jsx(Plus, { size: 16 }) }), _jsx(YStack, { flex: 1 }), _jsx(Me, { host: host, narrow: true, onOrg: org })] }) }));
|
|
78
|
+
return (_jsxs(YStack, { "data-slot": "app-column", ref: drawer ? undefined : span.ref,
|
|
79
|
+
// inline-style: `--span`, the width a drag rewrites on the element itself (span.ts); no prop sets a variable.
|
|
80
|
+
style: drawer ? undefined : span.style, position: "relative", shrink: 0, height: "100%", width: drawer ? '100%' : 'var(--span)', ...(drawer ? {} : { '$max-md': { display: 'none' } }), children: [_jsxs(Sidebar, { "data-slot": "app-rail", "data-collapsed": "false", role: "navigation", "aria-label": "Hanzo", ...pane(), ...FILL, px: "$2.5", pt: "$3", pb: "$2", gap: "$0", children: [_jsxs(XStack, { "data-slot": "mode-switch", items: "center", gap: "$2", px: "$1.5", pb: "$3", minW: 0, children: [_jsx(HanzoMark, { size: 18 }), _jsx(ModeMark, { mode: mode, onChat: onChat, onDev: onDev })] }), _jsxs(XStack, { "data-slot": "app-filter", items: "center", gap: "$2", px: "$3", height: 36, rounded: 999, borderWidth: 1, borderColor: "$borderColor", bg: "$edge", shrink: 0, children: [_jsx(Search, { size: 15, "aria-hidden": true, color: "var(--soft, var(--muted-foreground))" }), _jsx(Text, { render: _jsx("input", { value: words, onChange: (e) => setWords(e.target.value), onKeyDown: (e) => {
|
|
81
|
+
if (e.key === 'Escape')
|
|
82
|
+
setWords('');
|
|
83
|
+
}, placeholder: "Filter", "aria-label": "Filter recents" }), flex: 1, minW: 0, bg: "transparent", borderWidth: 0, outlineStyle: "none", color: "inherit", fontSize: "$2" })] }), _jsxs(YStack, { role: "list", mt: "$2.5", gap: 2, shrink: 0, children: [_jsx(YStack, { role: "listitem", children: _jsx(SidebarItem, { "data-slot": "app-new", ...ROW, active: fresh, icon: _jsx(Plus, { size: 16, "aria-hidden": true }), onPress: create, ...keys(create), children: mode === 'chat' ? 'New chat' : 'New run' }) }), mode === 'dev'
|
|
84
|
+
? links.map((link) => (_jsx(YStack, { role: "listitem", children: _jsx(SidebarItem, { ...ROW, active: link.active, icon: link.icon, onPress: link.onPress, ...keys(() => link.onPress?.()), children: link.label }) }, link.id)))
|
|
85
|
+
: null, mode === 'dev' ? (_jsxs(YStack, { role: "listitem", children: [_jsx(SidebarItem, { ...ROW, "aria-expanded": unfolded, icon: unfolded ? _jsx(ChevronUp, { size: 16, "aria-hidden": true }) : _jsx(ChevronDown, { size: 16, "aria-hidden": true }), onPress: () => setUnfolded((v) => !v), ...keys(() => setUnfolded((v) => !v)), children: "More" }), unfolded ? (_jsx(YStack, { role: "list", gap: 2, children: more.map((link) => (_jsx(YStack, { role: "listitem", children: _jsx(SidebarItem, { ...ROW, active: link.active, icon: link.icon, onPress: link.onPress, ...keys(() => link.onPress?.()), children: link.label }) }, link.id))) })) : null] })) : null] }), _jsx(XStack, { height: 1, bg: "$borderColor", mx: "$1.5", mt: "$3", shrink: 0 }), _jsx(Recents, { host: host, mode: mode, thread: thread, words: words, onOpen: open }), _jsxs(YStack, { gap: "$2", pt: "$2", shrink: 0, children: [_jsx(Made, {}), _jsx(Setup, {}), host.person && !slack ? (_jsx(YStack, { "data-slot": "app-slack", children: _jsx(Slack, { onOpen: () => dev(path({ kind: 'settings', section: 'integrations' })), onDismiss: () => setSlack(true) }) })) : null] }), _jsxs(XStack, { "data-slot": "app-foot", items: "center", gap: "$1", pt: "$2", shrink: 0, children: [_jsx(YStack, { flex: 1, minW: 0, children: _jsx(Me, { host: host, onOrg: org }) }), drawer ? null : (_jsx(SidebarIconButton, { label: "Collapse sidebar", onPress: () => setCollapsed(true), width: 32, height: 32, rounded: 999, children: _jsx(PanelLeft, { size: 16 }) }))] })] }), drawer ? null : (_jsx(Grip, { side: "left", span: span.span, floor: FLOOR, ceil: CEIL, reset: WIDTH, onSpan: span.move, onKeep: span.keep, onShut: () => setCollapsed(true), label: "Resize sidebar",
|
|
86
|
+
// In the gutter between the column and the pane, not over the column's own rows.
|
|
87
|
+
r: GUTTER_END, width: "var(--pane-gap)" }))] }));
|
|
88
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function Contacts(): import("react").JSX.Element;
|
package/dist/Contacts.js
ADDED
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
// The directory: who is in this org, and who you can put in a room.
|
|
4
|
+
//
|
|
5
|
+
// IT IS A LIST OF SUBJECTS AND NOTHING ELSE. People, the org's agents, and the
|
|
6
|
+
// presets an org can install as one. It holds no conversations, keeps no
|
|
7
|
+
// threads, and remembers nothing of its own — the rooms it starts live in the
|
|
8
|
+
// thread store with every other conversation, which is `conversations.ts`.
|
|
9
|
+
//
|
|
10
|
+
// WHAT IT USED TO DO. Four `localStorage` keys: a roster of people it invented,
|
|
11
|
+
// a list of agents it invented, a set of `direct_…`/`group_…`/`call_…` threads
|
|
12
|
+
// nothing on the platform had ever seen, and an event the sidebar merged them
|
|
13
|
+
// in through. Adding a person wrote a row into that store — no invitation, no
|
|
14
|
+
// account, nobody told — and the row was gone on the next machine. Pressing
|
|
15
|
+
// Call minted a thread whose title began with a telephone and rang nowhere.
|
|
16
|
+
//
|
|
17
|
+
// EVERY ROW IS NOW A READ, and every control a write:
|
|
18
|
+
// people GET /v1/iam/scim/v2/Users?owner=<org> (`usePeople`)
|
|
19
|
+
// agents GET /v1/agents (`useCrew`, crew.tsx)
|
|
20
|
+
// presets GET /v1/agent/chat/presets (the catalog, public)
|
|
21
|
+
// invite POST /v1/iam/invitations (`createInvitation`)
|
|
22
|
+
// agent POST /v1/agents (`Hire`, the crew's sheet)
|
|
23
|
+
// room POST /v1/agents/chat/conversations (`start`)
|
|
24
|
+
//
|
|
25
|
+
// THERE IS NO CALL CONTROL, because there is no call. Measured against the
|
|
26
|
+
// platform's own contract — 1629 paths in /v1/openapi.json — nothing serves
|
|
27
|
+
// signalling, an SFU, a room token or a TURN credential. A button that says
|
|
28
|
+
// Call and opens a chat is worse than no button: it reports a thing that did
|
|
29
|
+
// not happen. When the estate serves a call, the room it attaches to is the one
|
|
30
|
+
// `start` already writes, and this file gains a verb rather than a store.
|
|
31
|
+
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
32
|
+
import { useRooms } from './host.js';
|
|
33
|
+
import { Check, Plus, Search, UserPlus } from 'lucide-react';
|
|
34
|
+
import { Box, Text, XStack, YStack } from '@hanzo/ui';
|
|
35
|
+
import { useAi, useModels, usePeople, useThreads } from '@hanzo/ai/react';
|
|
36
|
+
import { useIam, useOrganizations } from '@hanzo/iam/react';
|
|
37
|
+
import { hasSession } from './lib/session.js';
|
|
38
|
+
import { useHydrated } from './lib/hydrated.js';
|
|
39
|
+
import { ENSO, served } from './lib/ai.js';
|
|
40
|
+
import { createInvitation, inviteLink } from './lib/invite.js';
|
|
41
|
+
import { reach } from './lib/reach.js';
|
|
42
|
+
import { Face } from './cast.js';
|
|
43
|
+
import { Take } from './copy.js';
|
|
44
|
+
import { Hire, useCrew } from './crew.js';
|
|
45
|
+
import { called } from './team.js';
|
|
46
|
+
import { say } from './failure.js';
|
|
47
|
+
import { openThread } from './open.js';
|
|
48
|
+
import { spokenAt, start } from "./conversations.js";
|
|
49
|
+
const TABS = [
|
|
50
|
+
{ id: 'all', label: 'All' },
|
|
51
|
+
{ id: 'person', label: 'People' },
|
|
52
|
+
{ id: 'agent', label: 'Agents' },
|
|
53
|
+
{ id: 'preset', label: 'Presets' },
|
|
54
|
+
];
|
|
55
|
+
export function Contacts() {
|
|
56
|
+
const hydrated = useHydrated();
|
|
57
|
+
const { isAuthenticated } = useIam();
|
|
58
|
+
const { currentOrgId } = useOrganizations();
|
|
59
|
+
const signedIn = isAuthenticated || hasSession();
|
|
60
|
+
// A prerender knows neither the reader nor the org, so it draws the heading
|
|
61
|
+
// and waits. Announcing "sign in" before hydration tells a signed-in reader
|
|
62
|
+
// they are signed out for a frame.
|
|
63
|
+
if (!hydrated)
|
|
64
|
+
return _jsx(Alone, { children: "\u00A0" });
|
|
65
|
+
if (!signedIn)
|
|
66
|
+
return _jsx(Alone, { children: "Sign in to see who is in your organization." });
|
|
67
|
+
if (!currentOrgId)
|
|
68
|
+
return _jsx(Alone, { children: "This account is not in an organization yet." });
|
|
69
|
+
return _jsx(Roster, { owner: currentOrgId });
|
|
70
|
+
}
|
|
71
|
+
/** The room's one heading, for every state that has nothing to list. */
|
|
72
|
+
function Alone({ children }) {
|
|
73
|
+
return (_jsxs(YStack, { flex: 1, items: "center", justify: "center", p: "$6", gap: "$3", children: [_jsx(Text, { render: "h1", fontSize: "$4", fontWeight: "600", color: "$ink", children: "Contacts" }), _jsx(Text, { fontSize: "$3", color: "$soft", maxW: 420, text: "center", children: children })] }));
|
|
74
|
+
}
|
|
75
|
+
/** A read that answered no, said once, above whatever else is on screen. */
|
|
76
|
+
function Note({ children }) {
|
|
77
|
+
return (_jsx(Text, { fontSize: "$2", color: "$soft", px: "$3", py: "$2", children: children }));
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* The directory itself.
|
|
81
|
+
*
|
|
82
|
+
* SPLIT OUT because `usePeople` needs an owner and a hook cannot be called
|
|
83
|
+
* conditionally — the same reason Home and Vibe split theirs.
|
|
84
|
+
*/
|
|
85
|
+
function Roster({ owner }) {
|
|
86
|
+
const ai = useAi();
|
|
87
|
+
const { router } = useRooms();
|
|
88
|
+
const { people, loading: reading, error: unread } = usePeople({ owner, service: false });
|
|
89
|
+
// `create` and not `ai.agents.create`: the hook's own writer refreshes the
|
|
90
|
+
// list it read, which is what moves an installed preset out of the catalog
|
|
91
|
+
// and into the org's agents without a reload.
|
|
92
|
+
const { agents, create, loading: gathering, error: ungathered } = useCrew();
|
|
93
|
+
// WHAT "RECENTLY" MEANS: the conversations this reader has. It is the same
|
|
94
|
+
// read the sidebar makes, and the only record of who a room was with.
|
|
95
|
+
const { threads } = useThreads();
|
|
96
|
+
// An installed preset needs a model, and `POST /v1/agents` refuses one this
|
|
97
|
+
// deployment does not serve — so it is named against the served catalog by
|
|
98
|
+
// the one function that does that, rather than typed in and hoped for.
|
|
99
|
+
const { models } = useModels();
|
|
100
|
+
// `null` is the catalog unknown; `[]` is a deployment that publishes none.
|
|
101
|
+
const [presets, setPresets] = useState(null);
|
|
102
|
+
const [unlisted, setUnlisted] = useState(null);
|
|
103
|
+
useEffect(() => {
|
|
104
|
+
const stop = new AbortController();
|
|
105
|
+
ai.http
|
|
106
|
+
.collection('presets', {
|
|
107
|
+
method: 'GET',
|
|
108
|
+
path: '/v1/agent/chat/presets',
|
|
109
|
+
signal: stop.signal,
|
|
110
|
+
})
|
|
111
|
+
.then(setPresets)
|
|
112
|
+
.catch((e) => {
|
|
113
|
+
if (!stop.signal.aborted)
|
|
114
|
+
setUnlisted(e);
|
|
115
|
+
});
|
|
116
|
+
return () => stop.abort();
|
|
117
|
+
}, [ai]);
|
|
118
|
+
const [tab, setTab] = useState('all');
|
|
119
|
+
const [order, setOrder] = useState('name');
|
|
120
|
+
const [look, setLook] = useState('');
|
|
121
|
+
const [picked, setPicked] = useState([]);
|
|
122
|
+
const [inviting, setInviting] = useState(false);
|
|
123
|
+
const [hiring, setHiring] = useState(false);
|
|
124
|
+
const [busy, setBusy] = useState(null);
|
|
125
|
+
const [wrong, setWrong] = useState(null);
|
|
126
|
+
const entries = useMemo(() => {
|
|
127
|
+
const rows = [];
|
|
128
|
+
for (const one of people) {
|
|
129
|
+
const label = one.displayName || one.name;
|
|
130
|
+
rows.push({
|
|
131
|
+
key: `person:${one.id}`,
|
|
132
|
+
kind: 'person',
|
|
133
|
+
name: one.name,
|
|
134
|
+
label,
|
|
135
|
+
note: one.email ?? '',
|
|
136
|
+
at: spokenAt(threads, label),
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
for (const one of agents) {
|
|
140
|
+
rows.push({
|
|
141
|
+
key: `agent:${one.id}`,
|
|
142
|
+
kind: 'agent',
|
|
143
|
+
name: one.name,
|
|
144
|
+
label: called(one.name),
|
|
145
|
+
note: one.description || one.model || '',
|
|
146
|
+
avatar: one.avatar,
|
|
147
|
+
emoji: one.emoji,
|
|
148
|
+
at: spokenAt(threads, one.name),
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
// A preset the org has already installed IS the agent above it. Drawing
|
|
152
|
+
// both would offer to install what is already here.
|
|
153
|
+
for (const one of presets ?? []) {
|
|
154
|
+
if (agents.some((made) => made.name === one.id))
|
|
155
|
+
continue;
|
|
156
|
+
rows.push({
|
|
157
|
+
key: `preset:${one.id}`,
|
|
158
|
+
kind: 'preset',
|
|
159
|
+
name: one.id,
|
|
160
|
+
label: one.title || one.id,
|
|
161
|
+
note: 'Not installed',
|
|
162
|
+
at: 0,
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
return rows;
|
|
166
|
+
}, [people, agents, presets, threads]);
|
|
167
|
+
const shown = useMemo(() => {
|
|
168
|
+
const q = look.trim().toLowerCase();
|
|
169
|
+
return entries
|
|
170
|
+
.filter((one) => tab === 'all' || one.kind === tab)
|
|
171
|
+
.filter((one) => !q || `${one.label} ${one.name} ${one.note}`.toLowerCase().includes(q))
|
|
172
|
+
.sort((a, b) => order === 'name'
|
|
173
|
+
? a.label.localeCompare(b.label)
|
|
174
|
+
: b.at - a.at || a.label.localeCompare(b.label));
|
|
175
|
+
}, [entries, tab, look, order]);
|
|
176
|
+
const install = useCallback(async (one) => {
|
|
177
|
+
const preset = (presets ?? []).find((p) => p.id === one.name);
|
|
178
|
+
if (!preset || busy)
|
|
179
|
+
return;
|
|
180
|
+
setBusy(one.key);
|
|
181
|
+
setWrong(null);
|
|
182
|
+
try {
|
|
183
|
+
// The list this row leaves and the list it joins are both `useAgents`,
|
|
184
|
+
// so the row moves by itself.
|
|
185
|
+
await create({
|
|
186
|
+
name: preset.id,
|
|
187
|
+
model: served(models, ENSO),
|
|
188
|
+
...(preset.systemPrompt ? { instructions: preset.systemPrompt } : {}),
|
|
189
|
+
...(preset.title ? { description: preset.title } : {}),
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
catch (e) {
|
|
193
|
+
setWrong(reach(e));
|
|
194
|
+
}
|
|
195
|
+
finally {
|
|
196
|
+
setBusy(null);
|
|
197
|
+
}
|
|
198
|
+
}, [busy, create, models, presets]);
|
|
199
|
+
/**
|
|
200
|
+
* INTO A ROOM, by the one call that starts one.
|
|
201
|
+
*
|
|
202
|
+
* One selected is a direct conversation, several is a group, and a group of
|
|
203
|
+
* agents or a mix of both is the same act — nothing here branches on how many
|
|
204
|
+
* or on what kind, because the thread store does not either.
|
|
205
|
+
*/
|
|
206
|
+
const enter = useCallback(async () => {
|
|
207
|
+
const members = picked
|
|
208
|
+
.map((key) => entries.find((one) => one.key === key))
|
|
209
|
+
.filter((one) => Boolean(one))
|
|
210
|
+
.map((one) => ({ id: one.key, name: one.label || one.name, agent: one.kind === 'agent' }));
|
|
211
|
+
if (members.length === 0 || busy)
|
|
212
|
+
return;
|
|
213
|
+
setBusy('room');
|
|
214
|
+
setWrong(null);
|
|
215
|
+
try {
|
|
216
|
+
const id = await start(ai, members);
|
|
217
|
+
// The selection the chat pane reads, and the address a reload lands on.
|
|
218
|
+
openThread(id);
|
|
219
|
+
router.push(`/chat?thread=${encodeURIComponent(id)}`);
|
|
220
|
+
}
|
|
221
|
+
catch (e) {
|
|
222
|
+
setWrong(reach(e));
|
|
223
|
+
}
|
|
224
|
+
finally {
|
|
225
|
+
setBusy(null);
|
|
226
|
+
}
|
|
227
|
+
}, [ai, busy, entries, picked, router]);
|
|
228
|
+
const waiting = (reading && people.length === 0) || (gathering && agents.length === 0);
|
|
229
|
+
return (_jsxs(YStack, { flex: 1, minH: 0, children: [_jsxs(XStack, { height: 48, shrink: 0, items: "center", gap: "$2", px: "$3", borderBottomWidth: 1, borderColor: "$borderColor", children: [_jsx(Text, { render: "h1", flex: 1, fontSize: "$3", fontWeight: "600", color: "$ink", numberOfLines: 1, children: "Contacts" }), _jsx(Control, { onPress: () => setInviting(true), icon: _jsx(UserPlus, { size: 13, "aria-hidden": true }), children: "Invite" }), _jsx(Control, { onPress: () => setHiring(true), icon: _jsx(Plus, { size: 13, "aria-hidden": true }), children: "New agent" })] }), _jsxs(XStack, { shrink: 0, items: "center", gap: "$2", px: "$3", py: "$2", borderBottomWidth: 1, borderColor: "$borderColor", flexWrap: "wrap", children: [_jsxs(XStack, { items: "center", gap: "$1.5", flex: 1, minW: 160, px: "$2", borderWidth: 1, borderColor: "$borderColor", rounded: "$3", children: [_jsx(Search, { size: 13, "aria-hidden": true }), _jsx(Text, { render: _jsx("input", { value: look, onChange: (e) => setLook(e.target.value), placeholder: "Search", "aria-label": "Search contacts" }), flex: 1, minW: 0, bg: "transparent", borderWidth: 0, outlineStyle: "none", color: "inherit", fontSize: "$2", px: 0, py: 6 })] }), TABS.map((one) => (_jsx(Pill, { on: tab === one.id, onPress: () => setTab(one.id), children: one.label }, one.id))), _jsx(Box, { width: 1, height: 18, bg: "$borderColor" }), _jsx(Pill, { on: order === 'name', onPress: () => setOrder('name'), children: "A\u2013Z" }), _jsx(Pill, { on: order === 'recent', onPress: () => setOrder('recent'), children: "Recent" })] }), unread ? _jsx(Note, { children: say(unread, 'your organization') }) : null, ungathered ? _jsx(Note, { children: say(ungathered, "this org's agents") }) : null, unlisted ? _jsx(Note, { children: say(unlisted, 'the agent presets') }) : null, wrong ? _jsx(Note, { children: wrong }) : null, _jsx(YStack, { flex: 1, minH: 0, overflow: "scroll", children: waiting && shown.length === 0 ? (_jsx(Note, { children: "Reading your directory." })) : shown.length === 0 ? (_jsx(Note, { children: look.trim()
|
|
230
|
+
? `Nobody here matches “${look.trim()}”.`
|
|
231
|
+
: tab === 'agent'
|
|
232
|
+
? 'No agents yet. New agent writes one.'
|
|
233
|
+
: tab === 'preset'
|
|
234
|
+
? 'Every preset this deployment publishes is installed.'
|
|
235
|
+
: 'Nobody else in this organization yet. Invite sends the link.' })) : (shown.map((one) => (_jsx(Row, { one: one, on: picked.includes(one.key), busy: busy === one.key, onPress: () => one.kind === 'preset'
|
|
236
|
+
? void install(one)
|
|
237
|
+
: setPicked((was) => was.includes(one.key)
|
|
238
|
+
? was.filter((k) => k !== one.key)
|
|
239
|
+
: [...was, one.key]) }, one.key)))) }), picked.length > 0 ? (_jsxs(XStack, { shrink: 0, items: "center", gap: "$2", p: "$3", borderTopWidth: 1, borderColor: "$borderColor", children: [_jsx(Text, { flex: 1, fontSize: "$2", color: "$soft", numberOfLines: 1, children: picked.length === 1 ? '1 selected' : `${picked.length} selected` }), _jsx(Control, { onPress: () => setPicked([]), children: "Clear" }), _jsx(Control, { onPress: () => void enter(), strong: true, children: busy === 'room' ? 'Opening…' : 'Start conversation' })] })) : null, inviting ? _jsx(Invite, { owner: owner, onClose: () => setInviting(false) }) : null, hiring ? _jsx(Hire, { onClose: () => setHiring(false), onMade: () => setHiring(false) }) : null] }));
|
|
240
|
+
}
|
|
241
|
+
/** One row: a face, a name, a line about them, and what pressing it does. */
|
|
242
|
+
function Row({ one, on, busy, onPress, }) {
|
|
243
|
+
return (_jsx(Box, { render: "button", onClick: onPress, "aria-label": one.label, "aria-pressed": one.kind === 'preset' ? undefined : on, width: "100%", display: "block", cursor: "pointer", borderWidth: 0, borderBottomWidth: 1, borderBottomColor: "var(--borderColor, var(--white-08))", bg: on ? 'var(--raised, var(--white-06))' : 'transparent', children: _jsxs(XStack, { gap: "$3", items: "center", p: "$3", children: [_jsx(Face, { src: one.avatar, emoji: one.emoji, name: one.name, size: 32 }), _jsxs(YStack, { flex: 1, minW: 0, children: [_jsx(Text, { fontSize: "$2", color: "$ink", numberOfLines: 1, text: "left", children: one.label }), one.note ? (_jsx(Text, { fontSize: "$1", color: "$soft", numberOfLines: 1, text: "left", children: one.note })) : null] }), one.kind === 'preset' ? (_jsx(Text, { fontSize: "$2", color: "$soft", children: busy ? 'Installing…' : 'Install' })) : on ? (_jsx(Check, { size: 14, "aria-hidden": true })) : null] }) }));
|
|
244
|
+
}
|
|
245
|
+
/** A named control. One shape for every button in this room. */
|
|
246
|
+
function Control({ children, onPress, icon, strong, }) {
|
|
247
|
+
return (_jsx(Box, { render: "button", onClick: onPress, px: "$3", py: "$2", rounded: "$3", borderWidth: strong ? 0 : 1, borderColor: "$borderColor", bg: strong ? '$ink' : 'transparent', hoverStyle: { bg: strong ? '$ink' : '$hover' }, cursor: "pointer", children: _jsxs(XStack, { items: "center", gap: "$1.5", children: [icon, _jsx(Text, { fontSize: "$2", color: strong ? '$background' : '$ink', fontWeight: strong ? '600' : undefined, numberOfLines: 1, children: children })] }) }));
|
|
248
|
+
}
|
|
249
|
+
/** A switch that is on or off — a tab, an order. Same shape, so they read alike. */
|
|
250
|
+
function Pill({ children, on, onPress, }) {
|
|
251
|
+
return (_jsx(Box, { render: "button", onClick: onPress, "aria-pressed": on, px: "$2.5", py: "$1.5", rounded: "$3", borderWidth: 1, borderColor: on ? '$ink' : '$borderColor', hoverStyle: { bg: '$hover' }, cursor: "pointer", children: _jsx(Text, { fontSize: "$2", color: on ? '$ink' : '$soft', numberOfLines: 1, children: children }) }));
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* ADDING A PERSON IS AN INVITATION, and it is the only honest shape for it.
|
|
255
|
+
*
|
|
256
|
+
* The other way in is `POST /v1/iam/users`, which takes the password the person
|
|
257
|
+
* will sign in with — so adding a colleague would mean choosing their
|
|
258
|
+
* credential and then telling it to them. An invitation is a code they redeem
|
|
259
|
+
* on hanzo.id under a password only they ever see.
|
|
260
|
+
*
|
|
261
|
+
* The link is the deliverable. Nothing here sends mail: no route on the
|
|
262
|
+
* platform mails an invitation, so this hands over the address instead of
|
|
263
|
+
* claiming to have sent one.
|
|
264
|
+
*/
|
|
265
|
+
function Invite({ owner, onClose }) {
|
|
266
|
+
const [email, setEmail] = useState('');
|
|
267
|
+
const [link, setLink] = useState(null);
|
|
268
|
+
const [busy, setBusy] = useState(false);
|
|
269
|
+
const [wrong, setWrong] = useState(null);
|
|
270
|
+
useEffect(() => {
|
|
271
|
+
const key = (e) => {
|
|
272
|
+
if (e.key === 'Escape')
|
|
273
|
+
onClose();
|
|
274
|
+
};
|
|
275
|
+
window.addEventListener('keydown', key);
|
|
276
|
+
return () => window.removeEventListener('keydown', key);
|
|
277
|
+
}, [onClose]);
|
|
278
|
+
const make = async () => {
|
|
279
|
+
if (busy)
|
|
280
|
+
return;
|
|
281
|
+
setBusy(true);
|
|
282
|
+
setWrong(null);
|
|
283
|
+
try {
|
|
284
|
+
const invitation = await createInvitation(owner, email.trim() || undefined);
|
|
285
|
+
setLink(inviteLink(invitation.owner, invitation.code));
|
|
286
|
+
}
|
|
287
|
+
catch (e) {
|
|
288
|
+
setWrong(reach(e));
|
|
289
|
+
}
|
|
290
|
+
finally {
|
|
291
|
+
setBusy(false);
|
|
292
|
+
}
|
|
293
|
+
};
|
|
294
|
+
return (_jsx(Box, { position: "fixed", inset: 0, z: "var(--z-modal)", display: "flex", items: "center", justify: "center", p: "$4", bg: "$sunken", onClick: (e) => {
|
|
295
|
+
if (e.target === e.currentTarget)
|
|
296
|
+
onClose();
|
|
297
|
+
}, children: _jsxs(YStack, { role: "dialog", "aria-label": "Invite", width: "100%", maxW: 440, gap: "$3", p: "$5", rounded: "$6", borderWidth: 1, borderColor: "$borderColor", bg: "$background", children: [_jsx(Text, { fontSize: "$5", fontWeight: "600", color: "$ink", children: "Invite" }), link ? (_jsxs(_Fragment, { children: [_jsxs(Text, { fontSize: "$2", color: "$soft", children: ["This link joins ", owner, ". It redeems once."] }), _jsxs(XStack, { items: "center", gap: "$2", children: [_jsx(Text, { flex: 1, fontSize: "$2", color: "$ink", numberOfLines: 1, children: link }), _jsx(Take, { text: link, says: "the invitation link", label: "Copy" })] })] })) : (_jsxs(_Fragment, { children: [_jsx(Text, { render: _jsx("input", { value: email, onChange: (e) => setEmail(e.target.value), onKeyDown: (e) => {
|
|
298
|
+
if (e.key === 'Enter')
|
|
299
|
+
void make();
|
|
300
|
+
}, placeholder: "Their email \u2014 or leave it open to anyone with the link", "aria-label": "Email", autoFocus: true }), width: "100%", bg: "transparent", borderWidth: 1, borderColor: "var(--border)", rounded: "var(--radius-md)", px: 10, py: 8, outlineStyle: "none", color: "inherit", fontSize: "$2" }), wrong ? (_jsx(Text, { fontSize: "$2", color: "$soft", children: wrong })) : null] })), _jsxs(XStack, { justify: "flex-end", gap: "$2", children: [_jsx(Control, { onPress: onClose, children: link ? 'Done' : 'Cancel' }), link ? null : (_jsx(Control, { onPress: () => void make(), strong: true, children: busy ? 'Making…' : 'Make a link' }))] })] }) }));
|
|
301
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The four ways this experience is customized, and the whole taxonomy.
|
|
3
|
+
*
|
|
4
|
+
* APPS are connected services — the thing that used to be called Connectors,
|
|
5
|
+
* which named the wire rather than what a reader installs. CHANNELS are
|
|
6
|
+
* transports: where a conversation reaches you. PLUGINS extend the client.
|
|
7
|
+
* SKILLS extend what an agent knows how to do.
|
|
8
|
+
*
|
|
9
|
+
* Order is the ruling's: Apps, Channels, Plugins, Skills.
|
|
10
|
+
*/
|
|
11
|
+
export type Tab = 'apps' | 'channels' | 'plugins' | 'skills';
|
|
12
|
+
export declare function Directory({ open, onClose, tab: first, }: {
|
|
13
|
+
open: boolean;
|
|
14
|
+
onClose: () => void;
|
|
15
|
+
/** The tab the directory opens on. A door that leads to channels opens on them. */
|
|
16
|
+
tab?: Tab;
|
|
17
|
+
}): import("react").JSX.Element;
|
|
18
|
+
/**
|
|
19
|
+
* The body. Rendered inside the Directory modal AND inside Settings, which is
|
|
20
|
+
* why it is exported and takes the tab as a prop.
|
|
21
|
+
*/
|
|
22
|
+
export declare function Catalog({ tab }: {
|
|
23
|
+
tab: Tab;
|
|
24
|
+
}): import("react").JSX.Element;
|