diff --git a/bun.lock b/bun.lock index a3e8c026a..9bc93fec9 100644 --- a/bun.lock +++ b/bun.lock @@ -393,19 +393,11 @@ "version": "0.0.1", "dependencies": { "@corbits/api-query": "workspace:*", - "@corbits/bench": "workspace:*", - "@corbits/icons": "workspace:*", - "@corbits/react-ui": "github:corbitsdev/react-ui#3b122812a307ccb35be31386f7696020c5a84635", "@intx/types": "0.3.0", "arktype": "catalog:", - "react": "^19.2.0", - "react-dom": "^19.2.0", }, "devDependencies": { - "@happy-dom/global-registrator": "^20.11.2", "@types/bun": "catalog:", - "@types/react": "^19.2.2", - "@types/react-dom": "^19.2.1", "typescript": "catalog:", }, }, @@ -3448,9 +3440,9 @@ "@corbits/artifact-ui/@corbits/react-ui": ["@corbits/react-ui@github:corbitsdev/react-ui#3b12281", { "dependencies": { "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "tailwind-merge": "^3.3.1" }, "peerDependencies": { "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-tooltip": "^1.2.8", "@tanstack/react-query": "^5.90.2", "lucide-react": "^0.545.0 || ^1.0.0", "react": "^18.2.0 || ^19.0.0", "react-dom": "^18.2.0 || ^19.0.0", "sonner": "^2.0.7" }, "optionalPeers": ["@tanstack/react-query"] }, "corbitsdev-react-ui-3b12281", "sha512-Abvm/DO0Gqg0ITHGT9355ZxyKRPMVJLSSQSjpd3a8qt4JPrSMOLIOS4sX8ZMNNaArIbnY9F+VKrOWkUJUyO4Nw=="], - "@corbits/bench-ui/@corbits/react-ui": ["@corbits/react-ui@github:corbitsdev/react-ui#3b12281", { "dependencies": { "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "tailwind-merge": "^3.3.1" }, "peerDependencies": { "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-tooltip": "^1.2.8", "@tanstack/react-query": "^5.90.2", "lucide-react": "^0.545.0 || ^1.0.0", "react": "^18.2.0 || ^19.0.0", "react-dom": "^18.2.0 || ^19.0.0", "sonner": "^2.0.7" }, "optionalPeers": ["@tanstack/react-query"] }, "corbitsdev-react-ui-3b12281", "sha512-Abvm/DO0Gqg0ITHGT9355ZxyKRPMVJLSSQSjpd3a8qt4JPrSMOLIOS4sX8ZMNNaArIbnY9F+VKrOWkUJUyO4Nw=="], + "@corbits/chat-ui/@corbits/react-ui": ["@corbits/react-ui@github:corbitsdev/react-ui#3b12281", { "dependencies": { "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "tailwind-merge": "^3.3.1" }, "peerDependencies": { "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-tooltip": "^1.2.8", "@tanstack/react-query": "^5.90.2", "lucide-react": "^0.545.0 || ^1.0.0", "react": "^18.2.0 || ^19.0.0", "react-dom": "^18.2.0 || ^19.0.0", "sonner": "^2.0.7" }, "optionalPeers": ["@tanstack/react-query"] }, "corbitsdev-react-ui-3b12281", "sha512-Abvm/DO0Gqg0ITHGT9355ZxyKRPMVJLSSQSjpd3a8qt4JPrSMOLIOS4sX8ZMNNaArIbnY9F+VKrOWkUJUyO4Nw=="], - "@corbits/memory-hub/@corbits/memory": ["@corbits/memory@github:corbitsdev/corbits-memory#9e6f213", { "dependencies": { "@intx/agent": "0.2.2", "@intx/authz": "0.2.2", "@intx/hub-api": "0.2.2", "@intx/log": "0.2.2", "@intx/workflow": "0.2.2", "arktype": "^2.1.29", "drizzle-orm": "^0.45.1", "hono": "^4.9.0", "hono-openapi": "^1.3.1", "postgres": "^3.4.7" } }, "corbitsdev-corbits-memory-9e6f213", "sha512-utnM4ZT2zmslcPXYWAAqxlDNLcpGsXFiTOtj8h7+OXnhCP0Eaw8yl25+yCTyHpvt3jcdeG4h5uFsSj7ou0BZCA=="], + "@corbits/context-menu/@corbits/react-ui": ["@corbits/react-ui@github:corbitsdev/react-ui#3b12281", { "dependencies": { "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "tailwind-merge": "^3.3.1" }, "peerDependencies": { "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-tooltip": "^1.2.8", "@tanstack/react-query": "^5.90.2", "lucide-react": "^0.545.0 || ^1.0.0", "react": "^18.2.0 || ^19.0.0", "react-dom": "^18.2.0 || ^19.0.0", "sonner": "^2.0.7" }, "optionalPeers": ["@tanstack/react-query"] }, "corbitsdev-react-ui-3b12281", "sha512-Abvm/DO0Gqg0ITHGT9355ZxyKRPMVJLSSQSjpd3a8qt4JPrSMOLIOS4sX8ZMNNaArIbnY9F+VKrOWkUJUyO4Nw=="], "@corbits/plugins-ui/@corbits/react-ui": ["@corbits/react-ui@github:corbitsdev/react-ui#3b12281", { "dependencies": { "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "tailwind-merge": "^3.3.1" }, "peerDependencies": { "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-tooltip": "^1.2.8", "@tanstack/react-query": "^5.90.2", "lucide-react": "^0.545.0 || ^1.0.0", "react": "^18.2.0 || ^19.0.0", "react-dom": "^18.2.0 || ^19.0.0", "sonner": "^2.0.7" }, "optionalPeers": ["@tanstack/react-query"] }, "corbitsdev-react-ui-3b12281", "sha512-Abvm/DO0Gqg0ITHGT9355ZxyKRPMVJLSSQSjpd3a8qt4JPrSMOLIOS4sX8ZMNNaArIbnY9F+VKrOWkUJUyO4Nw=="], @@ -3478,8 +3470,6 @@ "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.6", "", {}, "sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw=="], - "@workbench/hub/@corbits/memory": ["@corbits/memory@github:corbitsdev/corbits-memory#9e6f213", { "dependencies": { "@intx/agent": "0.2.2", "@intx/authz": "0.2.2", "@intx/hub-api": "0.2.2", "@intx/log": "0.2.2", "@intx/workflow": "0.2.2", "arktype": "^2.1.29", "drizzle-orm": "^0.45.1", "hono": "^4.9.0", "hono-openapi": "^1.3.1", "postgres": "^3.4.7" } }, "corbitsdev-corbits-memory-9e6f213", "sha512-utnM4ZT2zmslcPXYWAAqxlDNLcpGsXFiTOtj8h7+OXnhCP0Eaw8yl25+yCTyHpvt3jcdeG4h5uFsSj7ou0BZCA=="], - "@workbench/web/@corbits/react-ui": ["@corbits/react-ui@github:corbitsdev/react-ui#3b12281", { "dependencies": { "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "tailwind-merge": "^3.3.1" }, "peerDependencies": { "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-tooltip": "^1.2.8", "@tanstack/react-query": "^5.90.2", "lucide-react": "^0.545.0 || ^1.0.0", "react": "^18.2.0 || ^19.0.0", "react-dom": "^18.2.0 || ^19.0.0", "sonner": "^2.0.7" }, "optionalPeers": ["@tanstack/react-query"] }, "corbitsdev-react-ui-3b12281", "sha512-Abvm/DO0Gqg0ITHGT9355ZxyKRPMVJLSSQSjpd3a8qt4JPrSMOLIOS4sX8ZMNNaArIbnY9F+VKrOWkUJUyO4Nw=="], "ajv-formats/ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="], diff --git a/docs/TENANCY.md b/docs/TENANCY.md index a5b0b947e..e12f5b0f1 100644 --- a/docs/TENANCY.md +++ b/docs/TENANCY.md @@ -118,10 +118,10 @@ same way: `"owners"` (default), `"owners-admins"`, or `"none"`. `@workbench/access-policy`'s `POST /api/tenants/:tenantId/access-policy/ -child-tenants` is the polished UI-facing wrapper `@corbits/bench-ui`'s -`createBench` calls whenever a `parentId` is given — it makes the same -decision and gives a clean pre-flight 403, but the guard above is what -actually closes the gap; the wrapper alone would not. +child-tenants` is the polished UI-facing wrapper for creating a child +tenant under a parent — it makes the same decision and gives a clean +pre-flight 403, but the guard above is what actually closes the gap; the +wrapper alone would not. Interchange currently does **not** validate `parentId` on POST and has **no** cycle constraint — see gaps below. @@ -208,13 +208,13 @@ Creation helper: `createDmWorkbenchSpec` in `@corbits/bench-ui`. dual-membership path) — that path is unaffected by, and independent of, the projection machinery above. -### Tenancy kind (bench switcher) +### Tenancy kind (bench vs workbench child) A native tenant row carries no `kind`/`type` field, so `/api/me/principals` returns one row per tenant a principal belongs to — workbenches and workbench child tenancies alike, indistinguishable to the platform. The -bench switcher needs to show only real workbenches, so workbench owns -the discriminator: +web client needs to treat only real workbenches as selectable benches, so +workbench owns the discriminator: - `packages/chat`'s `workbench_tenancy` link table is the source of truth for "this tenant is a workbench". `WorkbenchTenancyStore.listWorkbenchTenantIds` @@ -223,9 +223,9 @@ the discriminator: the web client for the caller's own tenant ids. - `@corbits/bench-ui`'s `classifyBenchMembership` combines that set with `isRawIdentifier` (a tenant with no human-assigned name never renders, - regardless of kind) to produce a `TenancyKind`: `"workbench"`, - `"workbench"`, or `"unknown"`. `filterWorkbenchMemberships` is what the - switcher renders from. + regardless of kind) to produce a `TenancyKind`: `"bench"`, + `"workbench"`, or `"unknown"`. `filterBenchMemberships` is what callers + use to keep only real benches. This is the extension point for every other tenancy kind the product adds (sub-workbenches, DMs, shared workbenches): each is still a tenant @@ -303,7 +303,7 @@ needs a weaker role, that is an Interchange conversation first. ## Related packages -- `@corbits/bench-ui` — switcher, create dialog, members, tenancy contracts +- `@corbits/bench-ui` — tenancy-kind helpers, workbench-tenancy client, tenancy contracts - `@workbench/onboarding` — personal bench provision under operator parent - `@workbench/access-policy` — closed-by-default signup/sub-workbench- creation policy, pending invites (CL-5886) diff --git a/packages/bench-ui/README.md b/packages/bench-ui/README.md index a40d3ab4e..b0fa531a4 100644 --- a/packages/bench-ui/README.md +++ b/packages/bench-ui/README.md @@ -1,33 +1,22 @@ # @corbits/bench-ui -Bench (tenant) management UI: the bench switcher, bench creation, and -member/invite management, built over Interchange's native tenancy routes. -Presentational primitives (buttons, dialogs, listboxes) come from -`@corbits/react-ui` -([corbitsdev/react-ui](https://github.com/corbitsdev/react-ui)); this -package holds the workbench-specific composition and the tenancy HTTP -client on top of them. +Bench tenancy helpers over Interchange's native tenant model: classifying +memberships by kind, asking which tenant ids are workbench child tenancies, +and the shared tenancy contracts (roles, signup mode, DM naming, parent +cycle checks). Creation lives at `/new`; people management lives in +`@corbits/settings-ui`'s PeopleSection — this package no longer ships +switcher / create / members UI. ## Key modules -- `bench-switcher.tsx` — the sidebar's bench dock: trigger plus popover to - switch benches or create a new one -- `create-bench-dialog.tsx` — the new-bench flow -- `member-list.tsx` / `members-panel.tsx` / `invite-member-dialog.tsx` — - viewing, inviting, and managing bench membership -- `membership.ts` — pure helpers: slug derivation, membership display, - role labels - `tenancy-kind.ts` — classifying a membership by tenancy kind +- `api.ts` — `listWorkbenchTenantIds` HTTP client +- `membership.ts` — `isRawIdentifier` (raw platform ids must never render) - `tenancy-contracts.ts` — shared tenancy constants and validation (roles, signup mode, DM workbench naming, parent-tenant cycle checks) -- `api.ts` — the bench HTTP client: memberships, creation, members, - settings ## Running tests ``` cd packages/bench-ui && bun test ``` - -Suites render with `react-dom/server`'s `renderToStaticMarkup` rather than -a mounted DOM, so no `bunfig.toml` preload is needed here. diff --git a/packages/bench-ui/bunfig.toml b/packages/bench-ui/bunfig.toml deleted file mode 100644 index 564f98dac..000000000 --- a/packages/bench-ui/bunfig.toml +++ /dev/null @@ -1,2 +0,0 @@ -[test] -preload = ["./test/dom-environment.ts"] diff --git a/packages/bench-ui/package.json b/packages/bench-ui/package.json index 740dd8a16..526356c9a 100644 --- a/packages/bench-ui/package.json +++ b/packages/bench-ui/package.json @@ -1,7 +1,7 @@ { "name": "@corbits/bench-ui", "private": true, - "description": "Bench (tenant) management UI: the switcher, the benches page, and member/invite management, composed from the published component library over Interchange's native tenancy routes", + "description": "Bench tenancy helpers: membership kind classification, workbench-tenancy client, and shared tenancy contracts over Interchange's native tenant model", "version": "0.0.1", "license": "LGPL-2.1-or-later", "type": "module", @@ -15,19 +15,11 @@ }, "dependencies": { "@corbits/api-query": "workspace:*", - "@corbits/bench": "workspace:*", - "@corbits/react-ui": "github:corbitsdev/react-ui#3b122812a307ccb35be31386f7696020c5a84635", "@intx/types": "0.3.0", - "arktype": "catalog:", - "@corbits/icons": "workspace:*", - "react": "^19.2.0", - "react-dom": "^19.2.0" + "arktype": "catalog:" }, "devDependencies": { - "@happy-dom/global-registrator": "^20.11.2", "@types/bun": "catalog:", - "@types/react": "^19.2.2", - "@types/react-dom": "^19.2.1", "typescript": "catalog:" } } diff --git a/packages/bench-ui/src/api.ts b/packages/bench-ui/src/api.ts index 1ff8bbae2..00d08d46e 100644 --- a/packages/bench-ui/src/api.ts +++ b/packages/bench-ui/src/api.ts @@ -1,37 +1,15 @@ -// The bench surface's one seam to Interchange's native tenancy routes -// (see vendor/intx/hub-api/src/routes/tenants.ts and principals.ts). Every -// fetch the bench/* components make goes through a function here, and every -// response is parsed with an arktype schema from `@intx/types` — the one -// real wire contract, never a hand-copied second one — at the boundary. +// The one HTTP seam this package still owns: asking which of the caller's +// tenant ids are workbench child tenancies. Member/invite/create clients +// lived here while the dead switcher and MembersPanel did; those UIs are +// gone (creation is `/new`, people management is settings-ui's PeopleSection), +// so the orphan invite/create/list clients went with them. import { type } from "arktype"; import type { ArkErrors } from "arktype"; -import { - PrincipalResponse, - PrincipalSummary, - TenantResponse, - paginatedSchema, -} from "@intx/types"; +import { PrincipalSummary } from "@intx/types"; import { UnauthenticatedError } from "@corbits/api-query"; -import { getBenchSettings, patchBenchSettings } from "@corbits/bench/client"; -import type { - BenchSettingsPatch, - BenchSettingsResponse, -} from "@corbits/bench/client"; - -// Purpose and type aren't part of Interchange's native tenant shape (see -// this file's header note), so they come from `@corbits/bench`'s own -// side-table client — re-exported here rather than imported directly by -// components, so `bench-ui`'s components keep this one seam. -export { getBenchSettings, patchBenchSettings }; -export type { BenchSettingsPatch, BenchSettingsResponse }; export type BenchMembership = typeof PrincipalSummary.infer; -export type BenchMember = typeof PrincipalResponse.infer; -export type Bench = typeof TenantResponse.infer; - -const MembershipsPage = paginatedSchema(PrincipalSummary); -const MembersPage = paginatedSchema(PrincipalResponse); export class BenchApiError extends Error { constructor( @@ -79,65 +57,6 @@ async function request( return parsed; } -/** The caller's own memberships, one row per bench they belong to. Only the - * first page — a person on more than a page of benches is not a case this - * surface handles yet, matching the same simplification `apps/web`'s - * settings page already makes over this same endpoint. */ -export function listMyMemberships(): Promise { - return request("/api/me/principals", MembershipsPage).then( - (page) => page.data, - ); -} - -export type CreateBenchInput = { - readonly name: string; - readonly slug: string; - readonly parentId?: string; -}; - -/** - * Creates a bench. A `parentId` (creating a sub-workbench under an - * existing one) routes through `@workbench/access-policy`'s gated - * surface instead of the native route directly — that surface checks - * the parent's own `tenancyCreation` policy against the caller's roles - * before ever calling `POST /api/tenants` itself. A bare top-level - * bench (no `parentId`) is unaffected and still hits the native route. - */ -export function createBench(input: CreateBenchInput): Promise { - if (input.parentId !== undefined) { - return request( - `/api/tenants/${input.parentId}/access-policy/child-tenants`, - TenantResponse, - { - method: "POST", - body: JSON.stringify({ name: input.name, slug: input.slug }), - }, - ); - } - return request("/api/tenants", TenantResponse, { - method: "POST", - body: JSON.stringify(input), - }); -} - -/** Every member of one bench. Only the first page, same simplification as - * `listMyMemberships`. */ -export function listMembers(tenantId: string): Promise { - return request(`/api/tenants/${tenantId}/principals`, MembersPage).then( - (page) => page.data, - ); -} - -export function inviteMember( - tenantId: string, - email: string, -): Promise { - return request(`/api/tenants/${tenantId}/members/invite`, PrincipalResponse, { - method: "POST", - body: JSON.stringify({ email }), - }); -} - const WorkbenchTenantIds = type({ workbenchTenantIds: "string[]", }); diff --git a/packages/bench-ui/src/bench-switcher.tsx b/packages/bench-ui/src/bench-switcher.tsx deleted file mode 100644 index 98ab2f86f..000000000 --- a/packages/bench-ui/src/bench-switcher.tsx +++ /dev/null @@ -1,221 +0,0 @@ -// The sidebar's bench dock: a trigger showing the active bench name -// that opens an upward listbox popover to switch benches or create a -// new one. Anatomy follows the app-chrome workspace-switcher pattern — -// trigger (icon, truncated name, chevron), invisible click-catcher, -// `role="listbox"` panel — with every label the server-resolved bench -// name, never a tenant id. -// -// Split into stateless pieces (`BenchSwitcherTrigger`, -// `BenchSwitcherList`) plus the composed `BenchSwitcher` that owns the -// open state and the create flow, so other sidebar variants can compose -// the same parts. - -import { CaretDown, Plus } from "@corbits/icons"; -import { useState } from "react"; - -import { BenchApiError, createBench, patchBenchSettings } from "./api"; -import type { Bench, BenchMembership } from "./api"; -import { CreateBenchDialog } from "./create-bench-dialog"; -import type { BenchCreateType } from "./create-bench-dialog"; -import { deriveBenchSlug, membershipDisplay } from "./membership"; -import { BENCH_STRINGS } from "./strings"; - -export function createBenchErrorMessage(cause: unknown): string { - if (cause instanceof BenchApiError && cause.status === 409) { - return BENCH_STRINGS.createBenchConflictError; - } - return BENCH_STRINGS.createBenchError; -} - -/** Square monogram for the trigger — the first letters of up to two words. */ -export function benchMonogram(name: string | null): string { - if (name === null) return "··"; - const initials = name - .split(/[\s._-]+/) - .filter((word) => word.length > 0) - .map((word) => word.charAt(0)) - .join("") - .slice(0, 2) - .toUpperCase(); - return initials.length > 0 ? initials : "··"; -} - -export function BenchSwitcherTrigger({ - activeName, - open, - onToggle, -}: { - readonly activeName: string | null; - readonly open: boolean; - readonly onToggle: () => void; -}) { - return ( - - ); -} - -export function BenchSwitcherList({ - memberships, - activeTenantId, - onSelect, - onCreate, -}: { - readonly memberships: readonly BenchMembership[]; - readonly activeTenantId: string | null; - readonly onSelect: (tenantId: string) => void; - readonly onCreate: () => void; -}) { - return ( -
- - {memberships.map((membership) => { - const display = membershipDisplay(membership); - const active = display.tenantId === activeTenantId; - return ( - - ); - })} -
- ); -} - -export function BenchSwitcher({ - memberships, - activeTenantId, - onSelect, - onBenchCreated, -}: { - readonly memberships: readonly BenchMembership[]; - readonly activeTenantId: string | null; - readonly onSelect: (tenantId: string) => void; - readonly onBenchCreated: (bench: Bench) => void; -}) { - const [open, setOpen] = useState(false); - const [createOpen, setCreateOpen] = useState(false); - const [createSubmitting, setCreateSubmitting] = useState(false); - const [createError, setCreateError] = useState(null); - - const active = - activeTenantId === null - ? undefined - : memberships.find( - (membership) => membership.tenantId === activeTenantId, - ); - const activeName = - active !== undefined ? membershipDisplay(active).name : null; - - function handleCreate( - name: string, - purpose?: string, - benchType?: BenchCreateType, - ) { - setCreateSubmitting(true); - setCreateError(null); - createBench({ name, slug: deriveBenchSlug(name) }) - .then(async (bench) => { - // Purpose/type aren't part of the native tenant-creation route - // (see create-bench-dialog.tsx's header note), so they land via a - // follow-up PATCH once the bench itself exists. A failure here is - // swallowed on purpose: the bench was already created successfully, - // and losing the purpose/type it was given is a smaller problem - // than reporting a creation failure that didn't happen. - if (purpose !== undefined || benchType !== undefined) { - try { - const patch = - purpose !== undefined && benchType !== undefined - ? { purpose, type: benchType } - : purpose !== undefined - ? { purpose } - : benchType !== undefined - ? { type: benchType } - : {}; - await patchBenchSettings(bench.id, patch); - } catch { - // best-effort, see comment above - } - } - setCreateSubmitting(false); - setCreateOpen(false); - onBenchCreated(bench); - }) - .catch((cause: unknown) => { - setCreateSubmitting(false); - setCreateError(createBenchErrorMessage(cause)); - }); - } - - return ( -
- setOpen((value) => !value)} - /> - {open && ( - <> -
- ); -} diff --git a/packages/bench-ui/src/create-bench-dialog.tsx b/packages/bench-ui/src/create-bench-dialog.tsx deleted file mode 100644 index 075bd6f6e..000000000 --- a/packages/bench-ui/src/create-bench-dialog.tsx +++ /dev/null @@ -1,191 +0,0 @@ -// The "new workbench" affordance: type cards (global vs sub), name, purpose, -// derived slug preview, and a join-policy note that reflects operator signup -// defaults. `onCreate` still names the bench itself — purpose and type are -// not part of Interchange's native tenant-creation route, so the caller is -// expected to persist them with a follow-up call once the bench exists (see -// `BenchSwitcher.handleCreate`). Join policy storage is not on the hub yet. - -import { - Button, - Dialog, - DialogBody, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, - Input, -} from "@corbits/react-ui"; -import { useState } from "react"; - -import { canCreateBench, deriveBenchSlug } from "./membership"; -import { BENCH_STRINGS } from "./strings"; - -export type BenchCreateType = "global" | "sub"; - -export function CreateBenchDialog({ - open, - onOpenChange, - onCreate, - submitting, - error = null, - signupOpen = false, -}: { - readonly open: boolean; - readonly onOpenChange: (open: boolean) => void; - /** - * `purpose`/`benchType` are only passed when the person actually entered - * one — `purpose` when non-empty after trimming, `benchType` always - * (the type cards always have one selected, "global" by default, so a - * "not set" state doesn't exist the way it does for the free-text - * purpose field). - */ - readonly onCreate: ( - name: string, - purpose?: string, - benchType?: BenchCreateType, - ) => void; - readonly submitting: boolean; - readonly error?: string | null; - /** When true, join-policy copy reflects open signup; otherwise invites-only. */ - readonly signupOpen?: boolean; -}) { - const [name, setName] = useState(""); - const [purpose, setPurpose] = useState(""); - const [benchType, setBenchType] = useState("global"); - const slug = deriveBenchSlug(name); - const canSubmit = canCreateBench(name); - - function reset() { - setName(""); - setPurpose(""); - setBenchType("global"); - } - - function handleSubmit() { - if (!canSubmit) return; - const trimmedPurpose = purpose.trim(); - onCreate( - name.trim(), - trimmedPurpose.length > 0 ? trimmedPurpose : undefined, - benchType, - ); - } - - return ( - { - onOpenChange(next); - if (!next) reset(); - }} - > - - - {BENCH_STRINGS.createBenchDialogTitle} - - {BENCH_STRINGS.createBenchDialogDescription} - - - -
{ - event.preventDefault(); - handleSubmit(); - }} - > -
- {BENCH_STRINGS.createBenchTypeLabel} -
- - -
-
- -