Estimate: 30m · Depends on: 1.2.1, 1.2.3, 1.2.5
The user-facing React for everything 1.2 ships: workspace switcher in the top-nav, settings page at /settings/workspace (rename + leave + delete-with-double-confirmation), and the invite-acceptance landing page at /invite/accept. All three compose from the design-system primitives (Button, Input, Card, Dialog) and follow the mockups from 1.2.1.
Why one Subtask for all three UI surfaces: they share a common API client (the invite + workspace endpoints from 1.2.5) and a common visual grammar (the workspace context). Splitting them would mean three Subtasks each calling out a slightly different slice of useWorkspace() / the switcher Popover / the destructive-dialog pattern. They cluster naturally; the Subtask is sized to one focused PR review.
Top-nav minimal-then-expand pattern: this Subtask introduces a tiny app/(authed)/_components/TopNav.tsx with two slots — the workspace switcher on the left, the user menu on the right. Story 1.5 (app shell) later expands this nav with project nav, search, etc. — but composes atop the existing structure rather than replacing it. The TopNav lives in a layout file app/(authed)/layout.tsx so it appears on every authed route (the smoke /dashboard from Story 1.1.2, plus everything 1.3+ adds). The dashboard's current ad-hoc "Sign out" form becomes a user-menu item.
Switcher implementation: client component reading the user's memberships via a server-action wrapped useSWR (or equivalent cache-friendly fetch); active workspace persists via a workspace_id cookie set on switch. Switching workspaces triggers router.refresh() to revalidate server components against the new context, and clears any workspace-scoped useSWR caches.
Settings page: server component reading the active workspace via getWorkspaceContext() from 1.2.3. Renders three Card sections: Name (Input + Save server action), Members (list from findWorkspaceMemberships(workspaceId) + per-row Remove + an Invite button opening a Dialog with email Input + Send), Danger zone (Leave workspace server action with last-member guard; Delete workspace opens a Dialog matching 1.2.1's double-confirmation mockup — the user must type the workspace name to enable the destructive Button). Last-member-cannot-leave is enforced server-side AND surfaced in the UI as a disabled-with-tooltip Leave button.
Invite-acceptance page: server component at app/(authed)/invite/accept/page.tsx (NOT under (auth) — requires auth, so it's gated by the proxy.ts from Story 1.1.11). Reads ?token= from search params, calls GET /api/invites/[token] to render the workspace name + inviter; renders the "Accept invite to {Workspace}" Button. Click fires POST /api/invites/[token]/accept and redirects to /dashboard. Error states (expired, already-used, wrong-email) render full-screen with the appropriate copy from 1.2.1's mockups.
What you'll do: Create app/(authed)/layout.tsx (with the TopNav), update app/(authed)/dashboard/page.tsx to drop its inline sign-out form (now in the user menu), create app/(authed)/_components/{TopNav,WorkspaceSwitcher,UserMenu}.tsx, app/(authed)/settings/workspace/page.tsx + actions.ts (Server Actions for rename/leave/delete + send-invite), app/(authed)/invite/accept/page.tsx. Playwright E2E spec tests/e2e/workspace-flows.spec.ts covers: switcher list + switch persists across reload; rename workspace; invite + accept end-to-end (uses the file outbox from 1.1.6); leave workspace blocked when last member; delete workspace cascades.
app/(authed)/layout.tsx renders TopNav on every authed route. TopNav contains the workspace switcher (left) and the user menu (right; includes Sign out, replacing the dashboard's inline form).workspace_id cookie, calls router.refresh(), and re-renders server components with the new context./settings/workspace renders three Cards (Name, Members, Danger zone) matching 1.2.1's mockup. All three operate via Server Actions (not client-side fetches) for the form posts.updateWorkspace server action; success surfaces a transient toast (use the existing Radix toast primitive from 1.0.5.2).deleteWorkspace (server action), which deletes via Prisma with cascade. On success, redirects to whichever workspace the user has left (or to a "create your first workspace" empty state if they have none)./invite/accept renders the workspace name + inviter, plus a single Accept Button. Click calls POST /api/invites/[token]/accept, switches the active workspace cookie to the newly-joined workspace, redirects to /dashboard.tests/e2e/workspace-flows.spec.ts covers: sign-up → switcher shows the auto-created workspace → rename → invite a second user (read link from file outbox, sign up as second user, accept) → second user appears in members list → switcher now shows both workspaces → switch between them → second user leaves → first user deletes workspace → cascade verified by DB query./design/workspaces/*.png from 1.2.1 — all four mockupscomponents/ui/{Button,Input,Card,Dialog,Toast}.tsx + app/globals.css tokensapp/(authed)/dashboard/page.tsx — current smoke route to refactorlib/workspaces/{repo,context,middleware,invites}.ts from 1.2.2 / 1.2.3 / 1.2.5app/api/workspaces/* + app/api/invites/* endpoints from 1.2.4 / 1.2.5tests/e2e/_helpers/{db-reset,email-capture}.ts from 1.1.7 — the E2E helper pattern this Subtask reusesrouter.refresh() + Server Components cache invalidation