@hanzo/rooms 0.1.82 → 0.1.84
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 +1 -1
- package/dist/Chat.js +8 -6
- package/dist/Column.js +2 -1
- package/dist/Me.js +3 -5
- package/dist/Orgs.js +266 -8
- package/dist/Palette.d.ts +7 -1
- package/dist/Palette.js +66 -27
- package/dist/Rail.d.ts +1 -5
- package/dist/Rail.js +16 -48
- package/dist/Shell.js +6 -5
- package/dist/Support.d.ts +0 -7
- package/dist/Support.js +11 -88
- package/dist/lib/orgname.d.ts +29 -0
- package/dist/lib/orgname.js +51 -0
- package/dist/lib/session.d.ts +4 -0
- package/dist/lib/session.js +6 -0
- package/dist/lib/title.d.ts +7 -0
- package/dist/lib/title.js +36 -0
- package/dist/seek.d.ts +9 -0
- package/dist/seek.js +11 -5
- package/dist/thread.js +2 -1
- package/package.json +1 -1
package/dist/App.js
CHANGED
|
@@ -241,5 +241,5 @@ function Window({ bar }) {
|
|
|
241
241
|
shut();
|
|
242
242
|
setFinding(true);
|
|
243
243
|
};
|
|
244
|
-
return (_jsx(AiProvider, { client: client ?? undefined, children: _jsx(HostProvider, { host: host, children: _jsx(PrefsProvider, { children: _jsxs(YStack, { ref: view, "data-slot": "window", ...(bar ? floor : null), flex: 1, minW: 0, height: "100dvh", overflow: "hidden", "$max-md": { position: 'fixed', t: TOP, l: 0, r: 0, height: HEIGHT }, 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,
|
|
244
|
+
return (_jsx(AiProvider, { client: client ?? undefined, children: _jsx(HostProvider, { host: host, children: _jsx(PrefsProvider, { children: _jsxs(YStack, { ref: view, "data-slot": "window", ...(bar ? floor : null), flex: 1, minW: 0, height: "100dvh", overflow: "hidden", "$max-md": { position: 'fixed', t: TOP, l: 0, r: 0, height: HEIGHT }, 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, open: !closed, toggle: bar ? undefined : flip, onChat: toChat, onDev: toDev, onStudio: toStudio, onOrg: org }) }), closed ? null : (_jsx(YStack, { "$max-md": { display: 'none' }, children: _jsx(Column, { host: host, mode: mode, thread: thread, go: move, onShut: () => setClosed(true), onSeek: seek, places: sidebar }) })), _jsxs(YStack, { "data-slot": "stage", flex: 1, minW: 0, minH: 0, ml: "var(--pane-gap)", "$max-md": { ml: 0, pb: FOOT }, children: [bar ? null : _jsx(Top, { host: host, mode: mode, places: sidebar, go: move, onMenu: () => setDrawer(true), onChat: toChat, onDev: toDev }), _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, onSeek: seek, drawer: true, places: sidebar, foot: _jsx(Rail, { host: host, mode: mode, open: true, flat: true, onChat: toChat, onDev: toDev, onStudio: toStudio, onOrg: org }) }) })] })) : null, finding ? _jsx(Palette, { go: move, onClose: found, mode: mode, host: host }) : null] }) }) }) }));
|
|
245
245
|
}
|
package/dist/Chat.js
CHANGED
|
@@ -42,6 +42,7 @@ import { AgentMaker, HOUSE, useAgentShelf, useCrew } from "./crew.js";
|
|
|
42
42
|
import { called } from "./team.js";
|
|
43
43
|
import { Anchor, Box, Button, Text, View, XStack, YStack } from "@hanzo/ui";
|
|
44
44
|
import { useMedia } from "@hanzo/gui";
|
|
45
|
+
import { tidy } from "./lib/title.js";
|
|
45
46
|
import { useIam, useIamToken } from "@hanzo/iam/react";
|
|
46
47
|
import { bearer, hasSession, org } from "./lib/session.js";
|
|
47
48
|
import { api } from "./lib/api.js";
|
|
@@ -540,12 +541,13 @@ function Thread({ placeholder = message(), initial, anonymous = false, heading,
|
|
|
540
541
|
const at = channel(opened);
|
|
541
542
|
// The first of them: what a surface that addresses one person reads.
|
|
542
543
|
const withWhom = room[0] ?? null;
|
|
543
|
-
// THE SIDE PANEL (@hanzo/build `Panel`), the one Dev's runs open too:
|
|
544
|
-
//
|
|
545
|
-
// kept per conversation
|
|
544
|
+
// THE SIDE PANEL (@hanzo/build `Panel`), the one Dev's runs open too: shut
|
|
545
|
+
// until the reader opens it or a preview lands (`see`, a link that frames),
|
|
546
|
+
// kept per conversation with its tabs, so a chat opens as it was left and
|
|
547
|
+
// never as the last chat was. The header's Panel button and ⌘. move it.
|
|
546
548
|
const media = useMedia();
|
|
547
549
|
const wide = media.lg;
|
|
548
|
-
const [panel, setPanel] = usePanel(
|
|
550
|
+
const [panel, setPanel] = usePanel(`chat:${at}`, false, wide);
|
|
549
551
|
const deck = useDeck(`chat:${at}`, SEED);
|
|
550
552
|
// The org's sites, which an empty tab suggests.
|
|
551
553
|
const sites = useSites();
|
|
@@ -1464,7 +1466,7 @@ function Thread({ placeholder = message(), initial, anonymous = false, heading,
|
|
|
1464
1466
|
}, children: [_jsx(Schedule, { open: plan !== null, onOpenChange: (open) => {
|
|
1465
1467
|
if (!open)
|
|
1466
1468
|
setPlan(null);
|
|
1467
|
-
}, prompt: plan ?? "", model: model, models: models.map((m) => ({ id: m.id, name: nameOf(m.id, listed) })) }), dragging ? (_jsx(YStack, { "aria-hidden": true, position: "absolute", t: 0, r: 0, b: 0, l: 0, z: 10, items: "center", justify: "center", bg: "var(--surface-scrim)", borderWidth: 2, borderStyle: "dashed", borderColor: "$borderColor", rounded: "var(--pane-round)", pointerEvents: "none", children: _jsxs(XStack, { items: "center", gap: "$2", px: "$4", py: "$2.5", rounded: "$10", bg: "$background", borderWidth: 1, borderColor: "$borderColor", children: [_jsx(Paperclip, { size: 15, "aria-hidden": true }), _jsx(Text, { fontSize: "$4", color: "$ink", children: "Drop files to attach" })] }) })) : null, _jsxs(XStack, { "data-slot": "conversation-head", height: 52, items: "center", justify: "space-between", gap: "$3", pl: "$4", pr: "$2", shrink: 0, children: [_jsxs(XStack, { items: "center", gap: "$2", minW: 0, flex: 1, children: [mine ? (_jsx(Text, { fontSize: 15, lineHeight: 20, color: "$ink", numberOfLines: 1, shrink: 1, minW: 0, children: mine.title
|
|
1469
|
+
}, prompt: plan ?? "", model: model, models: models.map((m) => ({ id: m.id, name: nameOf(m.id, listed) })) }), dragging ? (_jsx(YStack, { "aria-hidden": true, position: "absolute", t: 0, r: 0, b: 0, l: 0, z: 10, items: "center", justify: "center", bg: "var(--surface-scrim)", borderWidth: 2, borderStyle: "dashed", borderColor: "$borderColor", rounded: "var(--pane-round)", pointerEvents: "none", children: _jsxs(XStack, { items: "center", gap: "$2", px: "$4", py: "$2.5", rounded: "$10", bg: "$background", borderWidth: 1, borderColor: "$borderColor", children: [_jsx(Paperclip, { size: 15, "aria-hidden": true }), _jsx(Text, { fontSize: "$4", color: "$ink", children: "Drop files to attach" })] }) })) : null, _jsxs(XStack, { "data-slot": "conversation-head", height: 52, items: "center", justify: "space-between", gap: "$3", pl: "$4", pr: "$2", shrink: 0, children: [_jsxs(XStack, { items: "center", gap: "$2", minW: 0, flex: 1, children: [mine ? (_jsx(Text, { fontSize: 15, lineHeight: 20, color: "$ink", numberOfLines: 1, shrink: 1, minW: 0, children: tidy(mine.title) })) : null, room.length ? (_jsx(XStack, { role: "list", "aria-label": "Agents in this chat", items: "center", gap: "$1.5", minW: 0, overflowX: "auto", "$platform-web": { scrollbarWidth: "none" }, children: room.map((name) => (_jsxs(XStack, { role: "listitem", items: "center", gap: "$1.5", pl: "$1", pr: "$0.5", py: "$0.5", rounded: 999, borderWidth: 1, borderColor: "$borderColor", shrink: 0, children: [_jsx(Face, { src: faces[name]?.avatar ?? (name === withWhom ? portrait : undefined), emoji: faces[name]?.emoji ?? (name === withWhom ? persona?.emoji : undefined), name: name, size: 22 }), _jsx(Text, { fontSize: "$2", fontWeight: "600", color: "$color", numberOfLines: 1, display: "none", $sm: { display: "flex" }, children: name }), _jsx(Box, { render: "button", onClick: () => openAgent(name), "aria-label": `Remove ${name} from this chat`, title: `Remove ${name}`, width: 24, height: 24, rounded: 999, items: "center", justify: "center", hoverStyle: { bg: "$hover" }, children: _jsx(X, { size: 13, "aria-hidden": true, color: "var(--soft, var(--muted-foreground))" }) })] }, name))) })) : null] }), _jsxs(XStack, { "data-slot": "conversation-controls", items: "center", gap: 2, shrink: 0, children: [open ? _jsx(Share, { thread: open }) : null, mine ? (_jsx(ConversationMenu, { threads: alone, active: open, onSchedule: anonymous ? undefined : () => setPlan(lastAsked()), children: _jsx(XStack, { children: _jsx(ThreadMore, { t: mine }) }) })) : null, _jsx(Summary, { sections: sections, onOpen: () => setPanel(true) }), _jsx(PanelToggle, { open: panel, onToggle: () => setPanel(!panel) })] })] }), wide && aside !== null ? (_jsx(Beside, { children: _jsx(Panel, { side: deck, kinds: NONE, onHide: () => setPanel(false), label: "Tabs", ...browsing }) })) : null, !wide && panel ? _jsx(Panel, { side: deck, kinds: NONE, sheet: true, onHide: () => setPanel(false), label: "Tabs", ...browsing }) : null, _jsx(ChatSurface, { ...chat, messages: shown, send: guarded,
|
|
1468
1470
|
// AN EMPTY ROOM CENTRES ITS COMPOSER on a laptop. While nothing has been
|
|
1469
1471
|
// said the opening (the heading) and the composer sit as one group in the
|
|
1470
1472
|
// middle of the height; the moment a turn exists the transcript takes the
|
|
@@ -1531,7 +1533,7 @@ function Thread({ placeholder = message(), initial, anonymous = false, heading,
|
|
|
1531
1533
|
empty: heading === null ? undefined : (_jsxs(YStack, { "data-slot": "opening", items: "center", width: "100%", py: "$6", ...(media.sm ? null : { flex: 1, justify: "center" }), children: [room.length > 1 ? (_jsx(XStack, { items: "center", justify: "center", mb: "$4", children: room.map((name, i) => (_jsx(Box, { ml: i ? -16 : 0, rounded: 40, borderWidth: 2, borderColor: "$background", z: room.length - i, children: _jsx(Face, { src: faces[name]?.avatar, emoji: faces[name]?.emoji, name: name, size: 72 }) }, name))) })) : portrait ? (_jsx(Box, { width: 160, height: 160, rounded: 24, overflow: "hidden", borderWidth: 2, borderColor: "var(--white-15)", boxShadow: "0 10px 24px color-mix(in srgb, var(--pure-black) 70%, transparent)", mb: "$4", bg: "var(--neutral-900)", children: _jsx(View, { render: _jsx("img", { src: portrait, alt: withWhom }), width: "100%", height: "100%", objectFit: "cover" }) })) : null, room.length === 1 && !portrait ? (_jsx(Box, { mb: "$3", children: _jsx(Face, { emoji: mate?.emoji ?? persona?.emoji, name: withWhom, size: 72 }) })) : null, _jsx(Text, { render: "h1", fontSize: "$7", lineHeight: "$7", fontWeight: "700", color: "$ink", text: "center", mb: "$1", children: join(room) || heading || "What can I help with?" }), room.length === 1 && (mate?.description ?? persona?.description) ? (_jsx(Text, { fontSize: "$3", color: "$soft", text: "center", maxW: 500, mb: "$4", lineHeight: "$4", children: mate?.description ?? persona?.description })) : null] })),
|
|
1532
1534
|
// The transcript reads at the same measure. `Thread` defaults to 768
|
|
1533
1535
|
// itself; handing it the column here is what lets Width reach it.
|
|
1534
|
-
column: { maxW: MEASURE }, composer: prompt({ ...composing, field: mention.field }, mention.state) }), _jsx(AgentMaker, {}), carry.dialog,
|
|
1536
|
+
column: { maxW: MEASURE }, composer: prompt({ ...composing, field: mention.field }, mention.state) }), _jsx(AgentMaker, {}), carry.dialog, _jsxs(XStack, { items: "center", justify: "center", py: "$1", px: "$3", gap: "$2", children: [_jsx(Anchor, { href: site("/legal/terms"), fontSize: "$1", color: "$soft", children: "Terms" }), _jsx(Text, { fontSize: "$1", color: "$soft", children: "\u00B7" }), _jsx(Anchor, { href: site("/legal/privacy"), fontSize: "$1", color: "$soft", children: "Privacy" })] }), consent, _jsx(Upgrade, { ask: ask, onClose: () => setAsk(null) })] }));
|
|
1535
1537
|
}
|
|
1536
1538
|
/** The agreement free asks for, in @hanzo/ai's words so every surface says one thing. */
|
|
1537
1539
|
function Consent({ question, onAgree, onCancel, }) {
|
package/dist/Column.js
CHANGED
|
@@ -21,6 +21,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
21
21
|
import { Text, XStack, YStack } from '@hanzo/gui';
|
|
22
22
|
import { Bell, Search, Settings2, SquarePen } from 'lucide-react';
|
|
23
23
|
import { DOTS, Grip, nav, NEW, path as written, useSessions } from '@hanzo/build';
|
|
24
|
+
import { tidy } from './lib/title.js';
|
|
24
25
|
import { DropdownMenu } from '@hanzo/ui';
|
|
25
26
|
import { Sidebar, SidebarIconButton, SidebarItem, StatusDot } from '@hanzo/ui/chat';
|
|
26
27
|
import { useId } from 'react';
|
|
@@ -78,7 +79,7 @@ function Activity({ host }) {
|
|
|
78
79
|
...(latest.length
|
|
79
80
|
? latest.map((s) => ({
|
|
80
81
|
key: s.id,
|
|
81
|
-
label: s.title
|
|
82
|
+
label: tidy(s.title, 56, 'Untitled run'),
|
|
82
83
|
description: [s.status, ago(s.updatedAt || s.createdAt)].filter(Boolean).join(' · '),
|
|
83
84
|
icon: _jsx(StatusDot, { status: DOTS[s.status] ?? 'idle' }),
|
|
84
85
|
onSelect: () => host.go(s.id),
|
package/dist/Me.js
CHANGED
|
@@ -20,7 +20,7 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
|
|
|
20
20
|
// personal one first, with its plan — switched with `onOrg`; Create
|
|
21
21
|
// organization, Organization settings, and Hanzo Team (opened in the same
|
|
22
22
|
// organization for a team, offered as "Create a team" for a personal one); and
|
|
23
|
-
// All organizations for a SuperAdmin (
|
|
23
|
+
// All organizations for a SuperAdmin, which opens the chooser (Orgs.tsx).
|
|
24
24
|
//
|
|
25
25
|
// IAM is the source of every fact here: names and roles from the token's `orgs`
|
|
26
26
|
// claim, display names and which org is personal from `GET /v1/iam/organizations`,
|
|
@@ -36,13 +36,12 @@ import { Button, Dialog, DialogContent, DialogTitle, DropdownMenu, DropdownMenuC
|
|
|
36
36
|
import { api } from './lib/api.js';
|
|
37
37
|
import { createOrg, say } from './lib/org.js';
|
|
38
38
|
import { list } from './lib/list.js';
|
|
39
|
-
import { org as current, orgs, pick, renew, scope, superAdmin } from './lib/session.js';
|
|
39
|
+
import { choose, org as current, orgs, pick, renew, scope, superAdmin } from './lib/session.js';
|
|
40
40
|
import { ENTRY } from './lib/host.js';
|
|
41
41
|
import { leave } from './dev.js';
|
|
42
42
|
import { asked, KEYS, legal, out, pages } from './help.js';
|
|
43
43
|
import { showSettings } from './open.js';
|
|
44
44
|
import { site, where } from './where.js';
|
|
45
|
-
import { SupportPicker } from './Support.js';
|
|
46
45
|
import { BAD } from './lib/mix.js';
|
|
47
46
|
/** Where an organization's own settings live: a page of the site (`site()`), not of the app. */
|
|
48
47
|
const ORG_SETTINGS = '/settings/organization';
|
|
@@ -138,7 +137,6 @@ export function Me({ host, narrow = false, above = false, onOrg, }) {
|
|
|
138
137
|
const [account, setAccount] = useState(false);
|
|
139
138
|
const [help, setHelp] = useState(false);
|
|
140
139
|
const [creating, setCreating] = useState(false);
|
|
141
|
-
const [supporting, setSupporting] = useState(false);
|
|
142
140
|
const [keys, setKeys] = useState(false);
|
|
143
141
|
const row = useRef(null);
|
|
144
142
|
const current = host.org;
|
|
@@ -184,7 +182,7 @@ export function Me({ host, narrow = false, above = false, onOrg, }) {
|
|
|
184
182
|
setHelp(false);
|
|
185
183
|
}, placement: narrow && !above ? 'right-end' : above ? 'top-end' : 'top-start', children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsxs(XStack, { ref: (el) => {
|
|
186
184
|
row.current = el instanceof HTMLElement ? el : null;
|
|
187
|
-
}, render: "button", "data-slot": "me", "aria-label": `Account: ${[who, current].filter(Boolean).join(' · ')}`, ...{ title: who }, width: narrow ? 36 : '100%', minH: narrow ? 36 : 48, px: narrow ? 0 : '$2', gap: "$2.5", rounded: narrow ? 999 : '$5', items: "center", justify: narrow ? 'center' : 'flex-start', hoverStyle: { bg: '$hover' }, focusVisibleStyle: { outlineWidth: 2, outlineStyle: 'solid', outlineColor: '$outlineColor' }, cursor: "pointer", children: [_jsx(Face, { host: host, size: narrow ? 26 : 32 }), narrow ? null : (_jsxs(_Fragment, { children: [_jsxs(YStack, { flex: 1, minW: 0, children: [_jsx(SizableText, { size: "$3", fontWeight: "600", color: "$ink", numberOfLines: 1, text: "left", children: who }), plan ? (_jsx(SizableText, { size: "$1", color: "$soft", numberOfLines: 1, text: "left", children: plan })) : null] }), _jsx(ChevronsUpDown, { size: 14, "aria-hidden": true, opacity: 0.6 })] }))] }) }), _jsxs(DropdownMenuContent, { minW: 260, maxW: "calc(100vw - 16px)", "aria-labelledby": undefined, "aria-label": "Account", onCloseAutoFocus: back, children: [_jsxs(DropdownMenuSub, { open: account, onOpenChange: setAccount, children: [_jsxs(DropdownMenuSubTrigger, { minH: 48, gap: "$2.5", children: [_jsx(Face, { host: host, size: 28 }), _jsxs(YStack, { flex: 1, minW: 0, children: [_jsx(SizableText, { size: "$2", fontWeight: "600", color: "$ink", numberOfLines: 1, children: who }), _jsx(SizableText, { size: "$1", color: "$soft", numberOfLines: 1, children: plan || host.person.email || '' })] })] }), account ? (_jsxs(DropdownMenuSubContent, { minW: 260, maxW: "calc(100vw - 16px)", pointerEvents: "auto", children: [_jsx(Meter, { read: read, onPlans: () => leave('here'), onBilling: () => (setOpen(false), showSettings('billing')) }), _jsx(DropdownMenuSeparator, {}), _jsx(DropdownMenuLabel, { children: "Organizations" }), all.map((o) => (_jsx(DropdownMenuItem, { onSelect: () => o.name !== current && onOrg(o.name), children: _jsx(Line, { icon: _jsx(Mark, { org: o }), sub: [o.personal ? 'Personal' : said(o.role), planFor(o)].filter(Boolean).join(' · '), trail: o.name === current ? _jsx(Check, { size: 16, "aria-label": "Current" }) : null, children: o.display }) }, o.name))), _jsx(DropdownMenuItem, { onSelect: () => setCreating(true), children: _jsx(Line, { icon: _jsx(Plus, { size: 16, "aria-hidden": true }), children: "Create organization" }) }), _jsx(DropdownMenuItem, { onSelect: () => window.location.assign(site(ORG_SETTINGS)), children: _jsx(Line, { icon: _jsx(Settings, { size: 16, "aria-hidden": true }), children: "Organization settings" }) }), where().team ? null : here && !here.personal ? (_jsx(DropdownMenuItem, { onSelect: () => out(`${ENTRY}&org=${encodeURIComponent(here.name)}`), children: _jsx(Line, { icon: _jsx(Users, { size: 16, "aria-hidden": true }), trail: _jsx(ArrowUpRight, { size: 14, "aria-hidden": true, opacity: 0.6 }), children: "Open in Hanzo Team" }) })) : (_jsx(DropdownMenuItem, { onSelect: () => setCreating(true), children: _jsx(Line, { icon: _jsx(Users, { size: 16, "aria-hidden": true }), children: "Create a team" }) })), superAdmin() ? (_jsx(DropdownMenuItem, { onSelect:
|
|
185
|
+
}, render: "button", "data-slot": "me", "aria-label": `Account: ${[who, current].filter(Boolean).join(' · ')}`, ...{ title: who }, width: narrow ? 36 : '100%', minH: narrow ? 36 : 48, px: narrow ? 0 : '$2', gap: "$2.5", rounded: narrow ? 999 : '$5', items: "center", justify: narrow ? 'center' : 'flex-start', hoverStyle: { bg: '$hover' }, focusVisibleStyle: { outlineWidth: 2, outlineStyle: 'solid', outlineColor: '$outlineColor' }, cursor: "pointer", children: [_jsx(Face, { host: host, size: narrow ? 26 : 32 }), narrow ? null : (_jsxs(_Fragment, { children: [_jsxs(YStack, { flex: 1, minW: 0, children: [_jsx(SizableText, { size: "$3", fontWeight: "600", color: "$ink", numberOfLines: 1, text: "left", children: who }), plan ? (_jsx(SizableText, { size: "$1", color: "$soft", numberOfLines: 1, text: "left", children: plan })) : null] }), _jsx(ChevronsUpDown, { size: 14, "aria-hidden": true, opacity: 0.6 })] }))] }) }), _jsxs(DropdownMenuContent, { minW: 260, maxW: "calc(100vw - 16px)", "aria-labelledby": undefined, "aria-label": "Account", onCloseAutoFocus: back, children: [_jsxs(DropdownMenuSub, { open: account, onOpenChange: setAccount, children: [_jsxs(DropdownMenuSubTrigger, { minH: 48, gap: "$2.5", children: [_jsx(Face, { host: host, size: 28 }), _jsxs(YStack, { flex: 1, minW: 0, children: [_jsx(SizableText, { size: "$2", fontWeight: "600", color: "$ink", numberOfLines: 1, children: who }), _jsx(SizableText, { size: "$1", color: "$soft", numberOfLines: 1, children: plan || host.person.email || '' })] })] }), account ? (_jsxs(DropdownMenuSubContent, { minW: 260, maxW: "calc(100vw - 16px)", pointerEvents: "auto", children: [_jsx(Meter, { read: read, onPlans: () => leave('here'), onBilling: () => (setOpen(false), showSettings('billing')) }), _jsx(DropdownMenuSeparator, {}), _jsx(DropdownMenuLabel, { children: "Organizations" }), all.map((o) => (_jsx(DropdownMenuItem, { onSelect: () => o.name !== current && onOrg(o.name), children: _jsx(Line, { icon: _jsx(Mark, { org: o }), sub: [o.personal ? 'Personal' : said(o.role), planFor(o)].filter(Boolean).join(' · '), trail: o.name === current ? _jsx(Check, { size: 16, "aria-label": "Current" }) : null, children: o.display }) }, o.name))), _jsx(DropdownMenuItem, { onSelect: () => setCreating(true), children: _jsx(Line, { icon: _jsx(Plus, { size: 16, "aria-hidden": true }), children: "Create organization" }) }), _jsx(DropdownMenuItem, { onSelect: () => window.location.assign(site(ORG_SETTINGS)), children: _jsx(Line, { icon: _jsx(Settings, { size: 16, "aria-hidden": true }), children: "Organization settings" }) }), where().team ? null : here && !here.personal ? (_jsx(DropdownMenuItem, { onSelect: () => out(`${ENTRY}&org=${encodeURIComponent(here.name)}`), children: _jsx(Line, { icon: _jsx(Users, { size: 16, "aria-hidden": true }), trail: _jsx(ArrowUpRight, { size: 14, "aria-hidden": true, opacity: 0.6 }), children: "Open in Hanzo Team" }) })) : (_jsx(DropdownMenuItem, { onSelect: () => setCreating(true), children: _jsx(Line, { icon: _jsx(Users, { size: 16, "aria-hidden": true }), children: "Create a team" }) })), superAdmin() ? (_jsx(DropdownMenuItem, { onSelect: choose, children: _jsx(Line, { icon: _jsx(Building2, { size: 16, "aria-hidden": true }), children: "All organizations" }) })) : null] })) : null] }), _jsx(DropdownMenuSeparator, {}), _jsx(DropdownMenuItem, { onSelect: () => leave('here'), children: _jsx(Line, { icon: _jsx(Sparkles, { size: 16, "aria-hidden": true }), children: upgrade(read.value?.label?.name) }) }), _jsx(DropdownMenuItem, { onSelect: () => showSettings('general'), children: _jsx(Line, { icon: _jsx(Palette, { size: 16, "aria-hidden": true }), children: "Personalization" }) }), _jsx(DropdownMenuItem, { onSelect: () => showSettings('account'), children: _jsx(Line, { icon: _jsx(CircleUserRound, { size: 16, "aria-hidden": true }), children: "Profile" }) }), _jsx(DropdownMenuItem, { "aria-keyshortcuts": "Control+, Meta+,", onSelect: () => showSettings('general'), children: _jsx(Line, { icon: _jsx(Settings, { size: 16, "aria-hidden": true }), trail: _jsx(DropdownMenuShortcut, { "aria-hidden": true, children: apple ? '⌘,' : 'Ctrl+,' }), children: "Settings" }) }), _jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuSub, { open: help, onOpenChange: setHelp, children: [_jsx(DropdownMenuSubTrigger, { children: _jsx(Line, { icon: _jsx(LifeBuoy, { size: 16, "aria-hidden": true }), children: "Help" }) }), help ? (_jsxs(DropdownMenuSubContent, { minW: 240, maxW: "calc(100vw - 16px)", 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: "Keyboard shortcuts" }) }), _jsx(DropdownMenuSeparator, {}), legal().map((p) => (_jsx(DropdownMenuItem, { onSelect: () => out(p.href), children: _jsx(Line, { icon: ICONS[p.key], children: p.label }) }, p.key)))] })) : null] }), host.signOut ? (_jsx(DropdownMenuItem, { onSelect: () => host.signOut?.(), children: _jsx(Line, { icon: _jsx(LogOut, { size: 16, "aria-hidden": true }), children: "Log out" }) })) : null] })] }), _jsx(Create, { open: creating, onOpenChange: setCreating }), _jsx(Shortcuts, { open: keys, onOpenChange: setKeys, apple: apple })] }));
|
|
188
186
|
}
|
|
189
187
|
/** The app's keys (help.ts `KEYS`), one row each, spelled for this platform. */
|
|
190
188
|
function Shortcuts({ open, onOpenChange, apple }) {
|
package/dist/Orgs.js
CHANGED
|
@@ -28,11 +28,13 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
|
|
|
28
28
|
// Free is offered a team, and an open one is sent on (`forward`). Behind the
|
|
29
29
|
// door, and only there, nothing opens without a live plan.
|
|
30
30
|
import { dev } from './lib/host.js';
|
|
31
|
-
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
31
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
32
32
|
import { useIam } from '@hanzo/iam/react';
|
|
33
33
|
import { Text, View, XStack, YStack } from '@hanzo/gui';
|
|
34
|
-
import { Button } from '@hanzo/ui';
|
|
35
|
-
import {
|
|
34
|
+
import { Avatar, Button, Tag } from '@hanzo/ui';
|
|
35
|
+
import { SearchInput } from '@hanzo/ui/product/Filters';
|
|
36
|
+
import { label, said as spoken } from '@hanzo/build/plan';
|
|
37
|
+
import { Check, Minus, Plus, X } from 'lucide-react';
|
|
36
38
|
import { Waiting } from './waiting.js';
|
|
37
39
|
import { LOGIN, enter } from './lib/destination.js';
|
|
38
40
|
import { useAi } from './lib/ai.js';
|
|
@@ -40,7 +42,8 @@ import { useHost } from './lib/hostname.js';
|
|
|
40
42
|
import { useHydrated } from './lib/hydrated.js';
|
|
41
43
|
import { api } from './lib/api.js';
|
|
42
44
|
import { usePersonal, useTier } from './lib/tier.js';
|
|
43
|
-
import { ORG, PICKED, assumed, hasSession, org, orgs as claimed, pick, renew, scope } from './lib/session.js';
|
|
45
|
+
import { CHOOSE, ORG, PICKED, assumed, bearer, hasSession, org, orgs as claimed, pick, renew, role, scope, superAdmin, support } from './lib/session.js';
|
|
46
|
+
import { matches, refusal, slugify } from './lib/orgname.js';
|
|
44
47
|
import { team as workspace } from './lib/host.js';
|
|
45
48
|
import { checkoutUrl, money } from './lib/pay.js';
|
|
46
49
|
import { cartUrl, charge, priced, usePlan, usePlans } from './lib/plans.js';
|
|
@@ -119,9 +122,17 @@ export function Orgs({ children, front = false, forward, signup = false, }) {
|
|
|
119
122
|
window.removeEventListener('focus', back);
|
|
120
123
|
};
|
|
121
124
|
}, [reload]);
|
|
125
|
+
// The chooser over the room, asked for from the account menu (session.ts `choose`).
|
|
126
|
+
const [choosing, setChoosing] = useState(false);
|
|
127
|
+
useEffect(() => {
|
|
128
|
+
const ask = () => setChoosing(true);
|
|
129
|
+
window.addEventListener(CHOOSE, ask);
|
|
130
|
+
return () => window.removeEventListener(CHOOSE, ask);
|
|
131
|
+
}, []);
|
|
122
132
|
const choose = useCallback((o) => {
|
|
123
133
|
pick(o);
|
|
124
134
|
setPicked(true);
|
|
135
|
+
setChoosing(false);
|
|
125
136
|
}, []);
|
|
126
137
|
// The organization a link arrives for. hanzo.ai opens the workspace at
|
|
127
138
|
// `?signin&org=<the org it is working in>` (Me.tsx); once signed in, an org
|
|
@@ -198,7 +209,10 @@ export function Orgs({ children, front = false, forward, signup = false, }) {
|
|
|
198
209
|
return door ? _jsx(Start, { onFounded: setFounding, first: true }) : _jsx(Create, {});
|
|
199
210
|
}
|
|
200
211
|
if (!current || (members.length > 1 && !picked))
|
|
201
|
-
return _jsx(Pick, { members: members, onPick: choose });
|
|
212
|
+
return _jsx(Pick, { members: members, current: current, onPick: choose });
|
|
213
|
+
// Asked for over an open room, the room keeps its place under the chooser and closing it goes back.
|
|
214
|
+
if (choosing)
|
|
215
|
+
return _jsxs(_Fragment, { children: [_jsx(Held, { veiled: true, children: children }), _jsx(Pick, { members: members, current: current, onPick: choose, onClose: () => setChoosing(false) })] });
|
|
202
216
|
if (open)
|
|
203
217
|
return forward ? _jsx(Forward, { to: forward }) : _jsx(_Fragment, { children: _jsx(Held, { veiled: false, children: children }) });
|
|
204
218
|
// IN THE WORKSPACE NOTHING OPENS WITHOUT A LIVE PLAN. Every branch here
|
|
@@ -458,9 +472,253 @@ function CreateDialog({ onClose }) {
|
|
|
458
472
|
? `A team organization runs on ${plan.name} at ${price} per seat a month, at least ${Number(plan.limits?.minSeats ?? 1)} seats.`
|
|
459
473
|
: `A personal organization runs on ${plan.name} at ${price} a month.`, ' ', "Nothing in it works until that plan is live, so creating one takes you straight to checkout, where you choose the seats and the term.", ' ', "Each plan includes session and daily usage limits; usage never draws your credit balance.", ' ', "Payment is on the provider's hosted page."] })) : null, refused ? (_jsx(Text, { render: "p", role: "alert", mt: 14, fontSize: "$3", color: "var(--foreground)", children: refused })) : null, _jsxs(XStack, { justify: "flex-end", gap: 10, mt: 24, flexWrap: "wrap", children: [_jsx(Button, { type: "button", size: "lg", onClick: onClose, children: "Cancel" }), _jsx(Button, { type: "submit", variant: "primary", size: "lg", shrink: 1, py: "$2", disabled: !plan || busy, children: _jsx(Text, { fontFamily: "$body", fontSize: "$3", color: "inherit", text: "center", children: busy ? 'Creating…' : paid ? `Create and subscribe — ${price}${seat} / month` : 'Create' }) })] }), _jsx(Rule, { mt: 18, mb: 8 }), _jsx(Text, { render: "p", text: "right", fontSize: "$2", color: muted, fontFamily: "$mono", children: "Esc to close" })] }) }));
|
|
460
474
|
}
|
|
461
|
-
/**
|
|
462
|
-
|
|
463
|
-
|
|
475
|
+
/**
|
|
476
|
+
* A page of IAM's organization list (`GET /v1/iam/organizations`): the reader's
|
|
477
|
+
* own organizations whole on the first page, then, for a SuperAdmin only, every
|
|
478
|
+
* other one, newest first. `q` narrows it by name or display name on the server;
|
|
479
|
+
* `cursor` continues it. IAM decides the scope from the bearer, so this asks the
|
|
480
|
+
* same thing for everyone.
|
|
481
|
+
*/
|
|
482
|
+
async function listing(q, cursor, limit, signal) {
|
|
483
|
+
const query = new URLSearchParams({ limit: String(limit) });
|
|
484
|
+
if (q.trim())
|
|
485
|
+
query.set('q', q.trim());
|
|
486
|
+
if (cursor)
|
|
487
|
+
query.set('cursor', cursor);
|
|
488
|
+
const res = await fetch(`${api()}/v1/iam/organizations?${query}`, {
|
|
489
|
+
headers: { Authorization: `Bearer ${bearer() ?? ''}` },
|
|
490
|
+
signal,
|
|
491
|
+
});
|
|
492
|
+
if (!res.ok)
|
|
493
|
+
throw new Error(`IAM answered ${res.status}.`);
|
|
494
|
+
return (await res.json());
|
|
495
|
+
}
|
|
496
|
+
/** How each of the reader's organizations appears, by name, from IAM's rows: the first page carries all of them. */
|
|
497
|
+
function useMarks(members) {
|
|
498
|
+
const [rows, setRows] = useState({});
|
|
499
|
+
const key = members.join(',');
|
|
500
|
+
useEffect(() => {
|
|
501
|
+
if (!key)
|
|
502
|
+
return;
|
|
503
|
+
const stop = new AbortController();
|
|
504
|
+
listing('', '', 1, stop.signal)
|
|
505
|
+
.then((page) => {
|
|
506
|
+
if (!stop.signal.aborted)
|
|
507
|
+
setRows(Object.fromEntries((page.organizations ?? []).map((o) => [o.name, o])));
|
|
508
|
+
})
|
|
509
|
+
.catch(() => { });
|
|
510
|
+
return () => stop.abort();
|
|
511
|
+
}, [key]);
|
|
512
|
+
return rows;
|
|
513
|
+
}
|
|
514
|
+
/** Each organization's plan by its family (`Max 20x`), billing asked in its name; absent where it names none. */
|
|
515
|
+
function useBilled(members) {
|
|
516
|
+
const [plans, setPlans] = useState({});
|
|
517
|
+
const key = members.join(',');
|
|
518
|
+
useEffect(() => {
|
|
519
|
+
const stop = new AbortController();
|
|
520
|
+
for (const o of key.split(',').filter(Boolean)) {
|
|
521
|
+
fetch(`${api()}/v1/billing/tier`, { headers: { ...scope(), 'X-Org-Id': o }, signal: stop.signal })
|
|
522
|
+
.then((r) => (r.ok ? r.json() : null))
|
|
523
|
+
.then((t) => {
|
|
524
|
+
const plan = t ? spoken(label(t.plan, t.tier?.name)) : '';
|
|
525
|
+
if (plan && !stop.signal.aborted)
|
|
526
|
+
setPlans((had) => ({ ...had, [o]: plan }));
|
|
527
|
+
})
|
|
528
|
+
.catch(() => { });
|
|
529
|
+
}
|
|
530
|
+
return () => stop.abort();
|
|
531
|
+
}, [key]);
|
|
532
|
+
return plans;
|
|
533
|
+
}
|
|
534
|
+
/** A role as a person reads it: `owner` → Owner. */
|
|
535
|
+
const said = (role) => (role ? role.charAt(0).toUpperCase() + role.slice(1) : '');
|
|
536
|
+
/** An organization's mark: its image, else its emoji, else its initial in its own hue (keyed on the slug, so a rename keeps it). */
|
|
537
|
+
function Mark({ org }) {
|
|
538
|
+
const emoji = org.mark?.avatar ? '' : org.mark?.emoji;
|
|
539
|
+
return (_jsx(Avatar, { size: "xl", shape: "rounded", src: org.mark?.avatar || (emoji ? null : org.mark?.logo) || null, name: org.display, colorSeed: org.name, icon: emoji ? _jsx(Text, { fontSize: "$7", children: emoji }) : undefined, alt: "" }));
|
|
540
|
+
}
|
|
541
|
+
/** One organization: its mark, its name and slug, the reader's role and its plan; the current one checked. */
|
|
542
|
+
function Choice({ org, current = false, note, disabled, onPress }) {
|
|
543
|
+
return (_jsx(View, { render: "li", display: "block", children: _jsxs(XStack, { render: "button", onClick: onPress, disabled: disabled, "aria-current": current || undefined, width: "100%", items: "center", gap: 12, px: 12, py: 10, rounded: 12, borderWidth: 1, borderColor: current ? '$ink' : '$edge', bg: "transparent", cursor: "pointer", hoverStyle: { bg: '$hover' }, children: [_jsx(Mark, { org: org }), _jsxs(YStack, { flex: 1, minW: 0, gap: 4, children: [_jsx(Text, { fontSize: "$4", fontWeight: "500", color: "$ink", numberOfLines: 1, text: "left", children: org.display }), _jsxs(XStack, { items: "center", gap: 6, flexWrap: "wrap", children: [_jsx(Text, { fontSize: "$2", fontFamily: "$mono", color: "$soft", numberOfLines: 1, children: org.name }), org.role ? _jsx(Tag, { variant: "outline", children: said(org.role) }) : null, org.plan ? _jsx(Tag, { children: org.plan }) : null, note ? (_jsx(Text, { fontSize: "$2", color: "$soft", children: note })) : null] })] }), current ? _jsx(Check, { size: 16, "aria-label": "Current" }) : null] }) }));
|
|
544
|
+
}
|
|
545
|
+
/**
|
|
546
|
+
* WHICH ORGANIZATION, THIS TIME — and, for a SuperAdmin, any organization.
|
|
547
|
+
*
|
|
548
|
+
* The reader's organizations are the token's `orgs` (no other list is theirs),
|
|
549
|
+
* drawn with IAM's rows beside them: the mark, the display name, the role the
|
|
550
|
+
* token signs and the plan billing reads in each one's name. The search filters
|
|
551
|
+
* them as it is typed, by slug or display name. A person founds another here
|
|
552
|
+
* (`Found`); a SuperAdmin, whom IAM never lets own a customer organization, is
|
|
553
|
+
* shown every organization instead (`Everyone`), stepped into through IAM.
|
|
554
|
+
*/
|
|
555
|
+
function Pick({ members, current, onPick, onClose, }) {
|
|
556
|
+
// IAM's verdict as it signed it into the token (authz `Claims.Sudo`). It only
|
|
557
|
+
// decides what is drawn: IAM answers anyone else's list with their own
|
|
558
|
+
// organizations and refuses them the step in.
|
|
559
|
+
const sudo = superAdmin();
|
|
560
|
+
const [q, setQ] = useState('');
|
|
561
|
+
const [founding, setFounding] = useState(false);
|
|
562
|
+
const marks = useMarks(members);
|
|
563
|
+
const plans = useBilled(members);
|
|
564
|
+
const mine = members.map((name) => ({
|
|
565
|
+
name,
|
|
566
|
+
display: marks[name]?.displayName || name,
|
|
567
|
+
mark: marks[name],
|
|
568
|
+
role: role(name) ?? '',
|
|
569
|
+
plan: plans[name],
|
|
570
|
+
}));
|
|
571
|
+
const shown = mine.filter((o) => matches(o, q));
|
|
572
|
+
useEffect(() => {
|
|
573
|
+
if (!onClose || founding)
|
|
574
|
+
return;
|
|
575
|
+
const key = (e) => {
|
|
576
|
+
if (e.key === 'Escape')
|
|
577
|
+
onClose();
|
|
578
|
+
};
|
|
579
|
+
document.addEventListener('keydown', key);
|
|
580
|
+
return () => document.removeEventListener('keydown', key);
|
|
581
|
+
}, [onClose, founding]);
|
|
582
|
+
return (_jsxs(Page, { label: "Choose an organization", children: [_jsxs(Column, { children: [_jsxs(XStack, { items: "flex-start", justify: "space-between", gap: 12, children: [_jsx(Brand, {}), onClose ? (_jsx(Button, { variant: "ghost", size: "icon-sm", mt: -6, mr: -8, onClick: onClose, "aria-label": "Close", children: _jsx(X, { size: 16, "aria-hidden": true }) })) : null] }), _jsx(Heading, { title: "Choose an organization", lede: _jsx(_Fragment, { children: "Everything you open \u2014 chats, agents, credit \u2014 is this one's." }) }), _jsx(Rule, {}), _jsx(XStack, { render: "form", onSubmit: (e) => {
|
|
583
|
+
e.preventDefault();
|
|
584
|
+
if (shown[0])
|
|
585
|
+
onPick(shown[0].name);
|
|
586
|
+
}, children: _jsx(SearchInput, { name: "organizations", value: q, onChange: setQ, placeholder: sudo ? 'Search every organization' : 'Find an organization', actions: sudo ? null : (_jsxs(Button, { type: "button", onClick: () => setFounding(true), children: [_jsx(Plus, { size: 16, "aria-hidden": true }), "New organization"] })) }) }), _jsx(YStack, { render: "ul", "aria-label": "Your organizations", m: 0, p: 0, mt: 14, gap: 8, children: shown.map((o) => (_jsx(Choice, { org: o, current: o.name === current, onPress: () => onPick(o.name) }, o.name))) }), shown.length === 0 ? (_jsxs(Text, { render: "p", mt: 14, fontSize: "$3", color: "$soft", children: ["None of yours matches \u201C", q.trim(), "\u201D."] })) : null, sudo ? _jsx(Everyone, { q: q, skip: members }) : null] }), founding ? _jsx(Found, { onClose: () => setFounding(false), onMade: onPick }) : null] }));
|
|
587
|
+
}
|
|
588
|
+
/**
|
|
589
|
+
* EVERY ORGANIZATION, for a SuperAdmin to step into.
|
|
590
|
+
*
|
|
591
|
+
* The list and the search are IAM's, a page at a time, and IAM records each new
|
|
592
|
+
* search under the operator's name. Opening one is IAM's `assume` (session.ts
|
|
593
|
+
* `support`): the operator's own token re-scoped to it and the step recorded in
|
|
594
|
+
* that organization's audit log. Nobody is made a member of anything.
|
|
595
|
+
*/
|
|
596
|
+
function Everyone({ q, skip }) {
|
|
597
|
+
const [rows, setRows] = useState([]);
|
|
598
|
+
const [cursor, setCursor] = useState('');
|
|
599
|
+
const [loading, setLoading] = useState(false);
|
|
600
|
+
const [refused, setRefused] = useState(null);
|
|
601
|
+
const [stepping, setStepping] = useState(null);
|
|
602
|
+
// Only the newest read lands: a search typed while a page is in flight
|
|
603
|
+
// supersedes it, so a slow answer to an old query never overwrites the new one.
|
|
604
|
+
const asked = useRef(0);
|
|
605
|
+
const load = useCallback(async (query, from) => {
|
|
606
|
+
const mine = ++asked.current;
|
|
607
|
+
setLoading(true);
|
|
608
|
+
setRefused(null);
|
|
609
|
+
try {
|
|
610
|
+
const next = await listing(query, from, 20);
|
|
611
|
+
if (mine !== asked.current)
|
|
612
|
+
return;
|
|
613
|
+
setRows((had) => {
|
|
614
|
+
// IAM's cursor is a position, so an org created mid-walk can shift a row
|
|
615
|
+
// across a page edge; each org is kept once.
|
|
616
|
+
const all = from ? [...had, ...(next.organizations ?? [])] : next.organizations ?? [];
|
|
617
|
+
return all.filter((o, i) => all.findIndex((x) => x.name === o.name) === i);
|
|
618
|
+
});
|
|
619
|
+
setCursor(next.cursor ?? '');
|
|
620
|
+
}
|
|
621
|
+
catch (e) {
|
|
622
|
+
if (mine === asked.current)
|
|
623
|
+
setRefused(e instanceof Error ? e.message : 'The list did not load.');
|
|
624
|
+
}
|
|
625
|
+
finally {
|
|
626
|
+
if (mine === asked.current)
|
|
627
|
+
setLoading(false);
|
|
628
|
+
}
|
|
629
|
+
}, []);
|
|
630
|
+
useEffect(() => {
|
|
631
|
+
const id = setTimeout(() => void load(q, ''), 250);
|
|
632
|
+
return () => clearTimeout(id);
|
|
633
|
+
}, [q, load]);
|
|
634
|
+
const enter = (org) => {
|
|
635
|
+
setStepping(org);
|
|
636
|
+
setRefused(null);
|
|
637
|
+
support(org).catch((e) => {
|
|
638
|
+
setRefused(e instanceof Error ? e.message : 'IAM did not step in.');
|
|
639
|
+
setStepping(null);
|
|
640
|
+
});
|
|
641
|
+
};
|
|
642
|
+
const others = rows.filter((o) => !skip.includes(o.name));
|
|
643
|
+
return (_jsxs(YStack, { render: "section", "aria-label": "All organizations", mt: 36, children: [_jsx(Text, { render: "h2", fontSize: "$6", fontWeight: "500", color: "$ink", children: "All organizations" }), _jsx(Text, { render: "p", mt: 4, fontSize: "$3", color: "$soft", lineHeight: 21, children: "Support mode. Step into any organization to see it as it does; IAM records every step under your name." }), _jsx(YStack, { render: "ul", "aria-label": "Every organization", m: 0, p: 0, mt: 14, gap: 8, children: others.map((o) => (_jsx(Choice, { org: { name: o.name, display: o.displayName || o.name, mark: o }, note: stepping === o.name ? 'Stepping in…' : undefined, disabled: stepping !== null, onPress: () => enter(o.name) }, o.name))) }), !loading && others.length === 0 && !refused ? (_jsx(Text, { render: "p", mt: 8, fontSize: "$3", color: "$soft", children: "No organization matches." })) : null, refused ? (_jsx(Text, { render: "p", role: "alert", mt: 8, fontSize: "$3", color: BAD, children: refused })) : null, cursor ? (_jsx(Button, { self: "flex-start", mt: 10, onClick: () => void load(q, cursor), disabled: loading, children: loading ? 'Loading…' : 'More' })) : null] }));
|
|
644
|
+
}
|
|
645
|
+
/**
|
|
646
|
+
* FOUND AN ORGANIZATION: its name, then the slug IAM keys it by.
|
|
647
|
+
*
|
|
648
|
+
* The slug follows the name the way IAM's onboarding makes it, until it is typed
|
|
649
|
+
* over; a slug typed is IAM's to take as it is, so one IAM would not keep is
|
|
650
|
+
* refused here with the rule, never rewritten. IAM's own onboarding makes it
|
|
651
|
+
* (`POST /v1/iam/onboard`): the organization, the reader as its owner and its
|
|
652
|
+
* first API key, the founder's account untouched. It is sent the name when the
|
|
653
|
+
* name makes the slug, and the slug when the slug was typed, the display name
|
|
654
|
+
* then written on the row (`/v1/iam/organizations/profile`). The token in hand
|
|
655
|
+
* predates it, so it is refreshed before the organization opens.
|
|
656
|
+
*/
|
|
657
|
+
function Found({ onClose, onMade }) {
|
|
658
|
+
const { sdk } = useIam();
|
|
659
|
+
const [name, setName] = useState('');
|
|
660
|
+
const [typed, setTyped] = useState(null);
|
|
661
|
+
const slug = typed ?? slugify(name);
|
|
662
|
+
const wrong = slug ? refusal(slug) : null;
|
|
663
|
+
const [busy, setBusy] = useState(false);
|
|
664
|
+
const [refused, setRefused] = useState(null);
|
|
665
|
+
useEffect(() => {
|
|
666
|
+
const key = (e) => {
|
|
667
|
+
if (e.key === 'Escape')
|
|
668
|
+
onClose();
|
|
669
|
+
};
|
|
670
|
+
document.addEventListener('keydown', key);
|
|
671
|
+
return () => document.removeEventListener('keydown', key);
|
|
672
|
+
}, [onClose]);
|
|
673
|
+
const submit = async (e) => {
|
|
674
|
+
e.preventDefault();
|
|
675
|
+
const display = name.trim();
|
|
676
|
+
if (busy)
|
|
677
|
+
return;
|
|
678
|
+
if (!display)
|
|
679
|
+
return setRefused('Give it a name.');
|
|
680
|
+
if (wrong)
|
|
681
|
+
return setRefused(wrong);
|
|
682
|
+
setBusy(true);
|
|
683
|
+
setRefused(null);
|
|
684
|
+
const headers = { Authorization: `Bearer ${bearer() ?? ''}`, 'Content-Type': 'application/json' };
|
|
685
|
+
const why = async (r) => {
|
|
686
|
+
const body = (await r.json().catch(() => null));
|
|
687
|
+
return body?.error || body?.detail || body?.msg || body?.message || `IAM answered ${r.status}.`;
|
|
688
|
+
};
|
|
689
|
+
try {
|
|
690
|
+
const named = slugify(display) === slug;
|
|
691
|
+
const r = await fetch(`${api()}/v1/iam/onboard`, { method: 'POST', headers, body: JSON.stringify({ name: named ? display : slug }) });
|
|
692
|
+
if (!r.ok)
|
|
693
|
+
throw new Error(await why(r));
|
|
694
|
+
const made = (await r.json().catch(() => null))?.org;
|
|
695
|
+
if (!made)
|
|
696
|
+
throw new Error('IAM made no organization. Try again.');
|
|
697
|
+
renew();
|
|
698
|
+
await sdk.refreshAccessToken().catch(() => null);
|
|
699
|
+
if (!named) {
|
|
700
|
+
const p = await fetch(`${api()}/v1/iam/organizations/profile`, {
|
|
701
|
+
method: 'POST',
|
|
702
|
+
headers: { ...headers, Authorization: `Bearer ${bearer() ?? ''}` },
|
|
703
|
+
body: JSON.stringify({ owner: 'admin', name: made, displayName: display }),
|
|
704
|
+
});
|
|
705
|
+
if (!p.ok)
|
|
706
|
+
throw new Error(`${made} is made, and it is still called ${made}: ${await why(p)}`);
|
|
707
|
+
}
|
|
708
|
+
// A token that does not carry it yet is minted on the next load, which opens it.
|
|
709
|
+
if (!claimed().includes(made)) {
|
|
710
|
+
pick(made);
|
|
711
|
+
window.location.reload();
|
|
712
|
+
return;
|
|
713
|
+
}
|
|
714
|
+
onMade(made);
|
|
715
|
+
}
|
|
716
|
+
catch (err) {
|
|
717
|
+
setRefused(err instanceof Error ? err.message : 'Could not create it.');
|
|
718
|
+
setBusy(false);
|
|
719
|
+
}
|
|
720
|
+
};
|
|
721
|
+
return (_jsx(YStack, { role: "dialog", "aria-modal": true, "aria-label": "New organization", position: "fixed", inset: 0, z: DIALOG, bg: shade(28), overflowY: "auto", py: "6vh", px: 16, onClick: onClose, children: _jsxs(YStack, { render: "form", onSubmit: submit, onClick: (e) => e.stopPropagation(), width: "100%", maxW: 480, mx: "auto", bg: "var(--background)", rounded: 16, borderWidth: 1, borderColor: "$edge", boxShadow: `0 24px 64px ${shade(28)}`, pt: 24, px: 24, pb: 20, children: [_jsxs(XStack, { items: "flex-start", justify: "space-between", gap: 12, children: [_jsxs(YStack, { shrink: 1, minW: "auto", children: [_jsx(Text, { render: "h2", fontSize: "$8", fontWeight: "500", color: "$ink", children: "New organization" }), _jsx(Text, { render: "p", mt: 6, fontSize: "$3", color: "$soft", lineHeight: 21, children: "You own it. Its agents, credit and members are its own." })] }), _jsx(Button, { type: "button", variant: "ghost", size: "icon-sm", mr: -8, onClick: onClose, "aria-label": "Close", children: _jsx(X, { size: 16, "aria-hidden": true }) })] }), _jsxs(View, { render: "label", display: "block", mt: 20, children: [_jsx(Text, { display: "block", fontSize: "$4", fontWeight: "500", mb: 8, color: "$ink", children: "Name" }), _jsx(Text, { render: _jsx("input", { value: name, onChange: (e) => setName(e.target.value), placeholder: "Acme", maxLength: 100, autoFocus: true }), ...FIELD })] }), _jsxs(View, { render: "label", display: "block", mt: 16, children: [_jsx(Text, { display: "block", fontSize: "$4", fontWeight: "500", mb: 8, color: "$ink", children: "Slug" }), _jsx(Text, { render: _jsx("input", { value: slug, onChange: (e) => setTyped(e.target.value), placeholder: "acme", spellCheck: false, autoCapitalize: "off", autoCorrect: "off", "aria-invalid": Boolean(slug && wrong) || undefined }), ...FIELD, fontFamily: "$mono" }), _jsx(Text, { display: "block", fontSize: "$3", color: slug && wrong ? BAD : '$soft', mt: 8, children: slug && wrong ? wrong : 'How every request names it: lowercase letters, digits and hyphens. It does not change.' })] }), refused ? (_jsx(Text, { render: "p", role: "alert", mt: 14, fontSize: "$3", color: BAD, children: refused })) : null, _jsxs(XStack, { justify: "flex-end", gap: 10, mt: 22, flexWrap: "wrap", children: [_jsx(Button, { type: "button", onClick: onClose, children: "Cancel" }), _jsx(Button, { type: "submit", variant: "primary", disabled: busy || !name.trim() || Boolean(wrong), children: busy ? 'Creating…' : 'Create and open' })] })] }) }));
|
|
464
722
|
}
|
|
465
723
|
/** On the workspace's host, an organization whose plan is not live: it is offered the per-seat Team plan. */
|
|
466
724
|
function Subscribe({ org, members, onSwitch, }) {
|
package/dist/Palette.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { type ReactNode } from 'react';
|
|
2
|
+
import { type Host } from '@hanzo/build';
|
|
2
3
|
import { type ShortcutKeys } from '@hanzo/ui';
|
|
4
|
+
import { type Place } from './lib/host.js';
|
|
3
5
|
import { type Hit } from './seek.js';
|
|
4
6
|
/** A row: what it is called, its glyph, and what pressing it does. */
|
|
5
7
|
export interface Action extends Hit {
|
|
@@ -9,10 +11,14 @@ export interface Action extends Hit {
|
|
|
9
11
|
/** Pressing it keeps the palette open. */
|
|
10
12
|
stay?: boolean;
|
|
11
13
|
}
|
|
12
|
-
export declare function Palette({ onClose, go, actions, }: {
|
|
14
|
+
export declare function Palette({ onClose, go, actions, mode, host, }: {
|
|
13
15
|
onClose: () => void;
|
|
14
16
|
/** Moves the app to one of its addresses (lib/host `talk`, `app`). */
|
|
15
17
|
go: (address: string) => void;
|
|
16
18
|
/** The frame's own actions, after the palette's; one with a palette action's id stands in for it. */
|
|
17
19
|
actions?: Action[];
|
|
20
|
+
/** The product the reader is in: what the empty field lists and what a query ranks first. */
|
|
21
|
+
mode?: Place['mode'];
|
|
22
|
+
/** Dev's host, whose runs the palette lists in Dev. */
|
|
23
|
+
host?: Host;
|
|
18
24
|
}): import("react").JSX.Element;
|