"use client"; import { AlertTriangleIcon, ArrowLeftIcon, BellIcon, CheckIcon, CopyIcon, CreditCardIcon, DownloadIcon, EllipsisIcon, EyeIcon, EyeOffIcon, KeyRoundIcon, MailIcon, PaletteIcon, PlusIcon, SettingsIcon, ShieldIcon, Trash2Icon, UserIcon, UsersIcon, } from "lucide-react"; import { AnimatePresence, animate, motion, useMotionValue, useReducedMotion, useTransform } from "motion/react"; import { type ReactNode, useEffect, useId, useRef, useState } from "react"; import { cn } from "@/lib/utils"; import { Alert, AlertAction, AlertDescription, AlertTitle } from "@/components/ui/alert"; import { AlertDialog, AlertDialogClose, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogPopup, AlertDialogTitle, } from "@/components/ui/alert-dialog"; import { Avatar, AvatarFallback } from "@/components/ui/avatar"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { InputGroup, InputGroupAddon, InputGroupInput, InputGroupText } from "@/components/ui/input-group"; import { Label } from "@/components/ui/label"; import { Menu, MenuItem, MenuPopup, MenuSeparator, MenuTrigger } from "@/components/ui/menu"; import { Select, SelectItem, SelectPopup, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Switch } from "@/components/ui/switch"; function useReducedMotionAfterMount() { const prefers = useReducedMotion(); const [mounted, setMounted] = useState(false); useEffect(() => setMounted(true), []); return mounted && Boolean(prefers); } export type SettingsSectionId = | "profile" | "appearance" | "notifications" | "security" | "general" | "members" | "billing" | "developer"; export type SettingsTab = "workspace" | "members" | "keys" | "billing"; export type MemberRole = "owner" | "admin" | "member" | "viewer"; export type KeyScope = "read" | "write" | "admin"; export interface SettingsMember { id: string; name: string; email: string; role: MemberRole; lastActive: string; invited?: boolean; } export interface SettingsApiKey { id: string; name: string; secret: string; scope: KeyScope; created: string; lastUsed: string; } export interface SettingsUsage { label: string; used: number; limit: number; projected?: number; unit?: string; } export interface SettingsInvoice { id: string; date: string; amount: number; status: "paid" | "due" | "failed"; } export interface SettingsPlan { id: string; name: string; price: number; blurb: string; } export interface SettingsCard { brand?: string; last4: string; expires: string; holder: string; email?: string; } export interface SettingsAndPlansProps { className?: string; defaultTab?: SettingsTab; workspace?: { name: string; slug: string; region: string }; members?: SettingsMember[]; apiKeys?: SettingsApiKey[]; plans?: SettingsPlan[]; currentPlan?: string; renewsOn?: string; usage?: SettingsUsage[]; invoices?: SettingsInvoice[]; card?: SettingsCard; cycleDaysLeft?: number; overagePer1k?: number; domain?: string; onNavigate?: (section: SettingsSectionId) => void; } const spring = { type: "spring", stiffness: 420, damping: 40 } as const; const easeOut = [0.23, 1, 0.32, 1] as const; const press = "transition-[scale,background-color,box-shadow] duration-150 ease-out motion-safe:active:scale-[0.97]"; const navGroups: { label: string; items: { id: SettingsSectionId; label: string; icon: ReactNode }[] }[] = [ { label: "Account", items: [ { id: "profile", label: "Profile", icon: }, { id: "appearance", label: "Appearance", icon: }, { id: "notifications", label: "Notifications", icon: }, { id: "security", label: "Security", icon: }, ], }, { label: "Workspace", items: [ { id: "general", label: "General", icon: }, { id: "members", label: "Members & roles", icon: }, { id: "billing", label: "Billing", icon: }, ], }, { label: "Developer", items: [{ id: "developer", label: "API keys & webhooks", icon: }], }, ]; const tabForNav: Partial> = { general: "workspace", members: "members", billing: "billing", developer: "keys", }; const navForTab: Record = { workspace: "general", members: "members", billing: "billing", keys: "developer", }; const tabOrder: SettingsTab[] = ["workspace", "members", "keys", "billing"]; const tabCopy: Record = { workspace: { title: "General", description: "Your workspace's name, address and where its data lives." }, members: { title: "Members", description: "Who can get in, and what they can touch." }, keys: { title: "API keys", description: "Secret keys for scripts, CI and server-side calls." }, billing: { title: "Billing", description: "Your plan, this cycle's usage, how you pay and past invoices." }, }; const regions = [ { value: "us-east", label: "US East · Virginia" }, { value: "eu-central", label: "EU Central · Frankfurt" }, { value: "ap-northeast", label: "AP Northeast · Tokyo" }, ]; const roleItems: { value: MemberRole; label: string; hint: string }[] = [ { value: "owner", label: "Owner", hint: "Billing and deleting the workspace" }, { value: "admin", label: "Admin", hint: "Members, keys and settings" }, { value: "member", label: "Member", hint: "Create and edit projects" }, { value: "viewer", label: "Viewer", hint: "Read-only access" }, ]; const scopeItems: { value: KeyScope; label: string; hint: string }[] = [ { value: "read", label: "Read", hint: "List and fetch" }, { value: "write", label: "Read & write", hint: "Create runs, edit data" }, { value: "admin", label: "Admin", hint: "Everything, incl. members" }, ]; const defaultMembers: SettingsMember[] = [ { id: "m1", name: "Ada Lindqvist", email: "ada@ridgeline.app", role: "owner", lastActive: "Now" }, { id: "m2", name: "Theo Marsh", email: "theo@ridgeline.app", role: "admin", lastActive: "12 min ago" }, { id: "m3", name: "Priya Raman", email: "priya@ridgeline.app", role: "member", lastActive: "2 h ago" }, { id: "m4", name: "Jonah Feld", email: "jonah@ridgeline.app", role: "member", lastActive: "Yesterday" }, { id: "m5", name: "Sara Whitlock", email: "sara@ridgeline.app", role: "viewer", lastActive: "4 days ago" }, ]; const defaultKeys: SettingsApiKey[] = [ { id: "k1", name: "Production server", secret: "rl_test_7Hq2vN9xRmT4bW8cYp3sKd1f", scope: "write", created: "Aug 02", lastUsed: "3 min ago" }, { id: "k2", name: "CI deploys", secret: "rl_test_Zt5uA0eJ6gLq9rXn2VbM8hC4", scope: "read", created: "Sep 18", lastUsed: "1 h ago" }, { id: "k3", name: "Analytics export", secret: "rl_test_Pw3kS7yD1oF5iQ0zU6aE9jB2", scope: "read", created: "Oct 01", lastUsed: "Never" }, ]; const defaultPlans: SettingsPlan[] = [ { id: "starter", name: "Starter", price: 0, blurb: "3 seats, 2k runs" }, { id: "team", name: "Team", price: 24, blurb: "10 seats, 25k runs" }, { id: "scale", name: "Scale", price: 59, blurb: "Unlimited seats, 250k runs, SSO" }, ]; const defaultUsage: SettingsUsage[] = [ { label: "Agent runs", used: 18420, limit: 25000, projected: 26300 }, { label: "Seats", used: 5, limit: 10 }, { label: "Storage", used: 43.6, limit: 50, unit: "GB" }, ]; const defaultInvoices: SettingsInvoice[] = [ { id: "INV-2026-0010", date: "Oct 14, 2026", amount: 120, status: "due" }, { id: "INV-2026-0009", date: "Sep 14, 2026", amount: 120, status: "paid" }, { id: "INV-2026-0008", date: "Aug 14, 2026", amount: 96, status: "failed" }, { id: "INV-2026-0007", date: "Jul 14, 2026", amount: 96, status: "paid" }, ]; const avatarHues = [ "from-violet-400 to-indigo-500", "from-sky-300 to-blue-500", "from-teal-300 to-emerald-500", "from-amber-300 to-orange-500", "from-rose-300 to-pink-500", "from-indigo-400 to-violet-700", ]; const invoiceBadge: Record = { paid: { label: "Paid", variant: "success" }, due: { label: "Due", variant: "warning" }, failed: { label: "Failed", variant: "error" }, }; function initials(name: string) { return name .split(/\s+/) .filter(Boolean) .map((p) => p[0]) .join("") .slice(0, 2) .toUpperCase(); } function mask(secret: string) { return `${secret.slice(0, 8)}…${secret.slice(-4)}`; } function makeSecret() { const chars = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz0123456789"; let out = "rl_test_"; for (let i = 0; i < 24; i++) out += chars[Math.floor(Math.random() * chars.length)]; return out; } const formatInt = (n: number) => Math.round(n).toLocaleString("en-US"); const formatMoney = (n: number) => `$${Math.round(n).toLocaleString("en-US")}`; const formatCents = (n: number) => `$${n.toFixed(2)}`; function RollingNumber({ value, format = formatInt }: { value: number; format?: (n: number) => string }) { const reduce = useReducedMotionAfterMount(); const mv = useMotionValue(value); const text = useTransform(mv, format); useEffect(() => { if (reduce) { mv.set(value); return; } const controls = animate(mv, value, { duration: 0.45, ease: easeOut }); return () => controls.stop(); }, [value, reduce, mv]); return {text}; } function Collapse({ show, children }: { show: boolean; children: ReactNode }) { const reduce = useReducedMotionAfterMount(); return ( {show && ( {children} )} ); } function Swap({ id, children, className }: { id: string; children: ReactNode; className?: string }) { const reduce = useReducedMotionAfterMount(); return ( {children} ); } function Section({ title, description, action, children, }: { title: ReactNode; description?: ReactNode; action?: ReactNode; children: ReactNode; }) { return (

{title}

{description &&

{description}

}
{action}
{children}
); } function Rows({ children, className }: { children: ReactNode; className?: string }) { return
{children}
; } function Row({ label, help, htmlFor, children, wide, }: { label: ReactNode; help?: ReactNode; htmlFor?: string; children?: ReactNode; wide?: boolean; }) { return (
{htmlFor ? ( ) : (

{label}

)} {help &&
{help}
}
{children &&
{children}
}
); } function TableHeadRow({ className, children }: { className: string; children: ReactNode }) { return (
{children}
); } function RidgelineShell({ active, onNavigate, children, className, }: { active: SettingsSectionId; onNavigate?: (section: SettingsSectionId) => void; children: ReactNode; className?: string; }) { const pillsRef = useRef(null); useEffect(() => { const scroller = pillsRef.current; const el = scroller?.querySelector(`[data-section="${active}"]`); if (!scroller || !el || scroller.scrollWidth <= scroller.clientWidth) return; scroller.scrollLeft = el.offsetLeft - 16; }, [active]); return (
Settings
{children}
); } type WorkspaceDraft = { name: string; slug: string; region: string }; function workspaceErrors(draft: WorkspaceDraft) { return { name: draft.name.trim() ? null : "Give your workspace a name.", slug: /^[a-z0-9](?:[a-z0-9-]{1,30})[a-z0-9]$/.test(draft.slug) ? null : "Use 3–32 letters, numbers or dashes.", }; } function FieldError({ id, children }: { id: string; children: string | null }) { return (

{children}

); } function WorkspaceMark({ name, className }: { name: string; className?: string }) { const reduce = useReducedMotionAfterMount(); const text = initials(name || "?"); return ( {text} ); } function WorkspacePanel({ draft, setDraft, savedSlug, domain, }: { draft: WorkspaceDraft; setDraft: (fn: (d: WorkspaceDraft) => WorkspaceDraft) => void; savedSlug: string; domain: string; }) { const [prefs, setPrefs] = useState({ agents: true, sso: false, digest: true }); const [confirmOpen, setConfirmOpen] = useState(false); const [typed, setTyped] = useState(""); const [scheduled, setScheduled] = useState(false); const errors = workspaceErrors(draft); const ids = useId(); return ( <>
setDraft((d) => ({ ...d, name: e.target.value }))} value={draft.name} /> {errors.name}
{domain}/ setDraft((d) => ({ ...d, slug: e.target.value.toLowerCase().replace(/[^a-z0-9-]/g, "-") }))} value={draft.slug} /> {errors.slug}
{( [ ["agents", "Members can create agents", "Otherwise only admins can publish new agents."], ["sso", "Require single sign-on", "Everyone signs in through your identity provider."], ["digest", "Weekly digest", "A Monday summary of runs, spend and failures."], ] as const ).map(([key, label, hint]) => ( ))}
{scheduled ? ( Scheduled for Nov 3. Everyone keeps access until then. ) : ( "Removes projects, runs and keys after a 7-day grace period." )} } label="Delete workspace" > {scheduled ? ( ) : ( )}
Delete {draft.name || "this workspace"}? Projects, runs and API keys are removed after 7 days. You can cancel any time before then.
setTyped(e.target.value)} value={typed} />
}>Cancel setScheduled(true)} variant="destructive" />} > Delete workspace
); } function RoleSelect({ member, onRole }: { member: SettingsMember; onRole: (id: string, role: MemberRole) => void }) { if (member.role === "owner") return Owner; return ( ); } const memberGrid = "sm:grid-cols-[minmax(0,1fr)_8rem_6.5rem_2rem]"; function MembersPanel({ initial, seatLimit }: { initial: SettingsMember[]; seatLimit: number }) { const [members, setMembers] = useState(() => initial.map((m, i) => ({ ...m, hue: avatarHues[i % avatarHues.length] }))); const [email, setEmail] = useState(""); const [role, setRole] = useState("member"); const [error, setError] = useState(null); const [sending, setSending] = useState(null); const [notice, setNotice] = useState(null); const [removing, setRemoving] = useState(null); const reduce = useReducedMotionAfterMount(); const hintId = useId(); const full = members.length >= seatLimit; useEffect(() => { if (!sending) return; const address = sending; const t = setTimeout(() => { const name = address .split("@")[0] .split(/[._-]/) .map((p) => p.charAt(0).toUpperCase() + p.slice(1)) .join(" "); setMembers((m) => [ ...m, { id: `m-${address}`, name, email: address, role, lastActive: "Invite sent", invited: true, hue: avatarHues[m.length % avatarHues.length] }, ]); setSending(null); setNotice(`Invite sent to ${address}.`); setEmail(""); }, 700); return () => clearTimeout(t); }, [sending, role]); useEffect(() => { if (!notice) return; const t = setTimeout(() => setNotice(null), 2600); return () => clearTimeout(t); }, [notice]); const invite = () => { const address = email.trim().toLowerCase(); if (!address) return setError("Enter an email address to send an invite."); if (!/^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(address)) return setError("That doesn't look like a full email address."); if (members.some((m) => m.email.toLowerCase() === address)) return setError(`${address} is already in this workspace.`); if (full) return setError(`All ${seatLimit} seats are in use. Remove someone or upgrade to add more.`); setError(null); setNotice(null); setSending(address); }; const hint = error ? { key: `e-${error}`, tone: "text-destructive-foreground", text: error } : notice ? { key: `n-${notice}`, tone: "text-success-foreground", text: notice } : { key: "idle", tone: "text-muted-foreground", text: "Invites expire after 7 days. Resend them from the list." }; const setRoleFor = (id: string, next: MemberRole) => setMembers((all) => all.map((x) => (x.id === id ? { ...x, role: next } : x))); return ( <>
of {seatLimit} {" "} seats used. } title="Invite people" >
{ e.preventDefault(); invite(); }} > { setEmail(e.target.value); if (error) setError(null); }} placeholder="name@company.com" type="email" value={email} />
{hint.text}
Member Role Last active Actions {members.map((m) => (
{initials(m.name)}

{m.name} {m.invited && ( Invited )}

{m.email}

{m.lastActive}
{m.role === "owner" ? ( ) : ( }> {m.invited && setNotice(`Invite resent to ${m.email}.`)}>Resend invite} { navigator.clipboard?.writeText(m.email).catch(() => {}); setNotice(`Copied ${m.email}.`); }} > Copy email setRemoving(m)} variant="destructive"> {m.invited ? "Revoke invite" : "Remove from workspace"} )}
))}
!open && setRemoving(null)} open={Boolean(removing)}> {removing?.invited ? `Revoke the invite for ${removing.email}?` : `Remove ${removing?.name}?`} {removing?.invited ? "The link in their email stops working straight away." : "They lose access straight away and their seat frees up. Projects they made stay put."} }>Cancel { if (!removing) return; const target = removing; setMembers((all) => all.filter((x) => x.id !== target.id)); setNotice(target.invited ? `Invite for ${target.email} revoked.` : `${target.name} removed.`); }} variant="destructive" /> } > {removing?.invited ? "Revoke invite" : "Remove"} ); } const keyGrid = "sm:grid-cols-[minmax(0,1fr)_6.5rem_6.5rem_2rem]"; function CopyButton({ text, label = "Copy" }: { text: string; label?: string }) { const [copied, setCopied] = useState(false); useEffect(() => { if (!copied) return; const t = setTimeout(() => setCopied(false), 1500); return () => clearTimeout(t); }, [copied]); return ( ); } function KeysPanel({ initial }: { initial: SettingsApiKey[] }) { const [keys, setKeys] = useState(initial); const [creating, setCreating] = useState(false); const [name, setName] = useState(""); const [scope, setScope] = useState("read"); const [fresh, setFresh] = useState(null); const [revealed, setRevealed] = useState([]); const [revoking, setRevoking] = useState(null); const reduce = useReducedMotionAfterMount(); const ids = useId(); const create = () => { if (!name.trim()) return; const key: SettingsApiKey = { id: `k-${keys.length}-${name.trim()}`, name: name.trim(), secret: makeSecret(), scope, created: "Just now", lastUsed: "Never", }; setFresh(key); setKeys((k) => [key, ...k]); setCreating(false); setName(""); setScope("read"); }; return ( <>
setCreating(true)} size="sm"> Create key } description="Keys act as the workspace. Keep them out of client-side code." title="Secret keys" >
e.key === "Escape" && setCreating(false)} onSubmit={(e) => { e.preventDefault(); create(); }} >
setName(e.target.value)} placeholder="e.g. Staging worker" value={name} />
Scope
{scopeItems.map((s) => { const on = scope === s.value; return ( ); })}
{fresh && (

{fresh.name} is ready. Copy it now: you won't see it again.

{fresh.secret}
)}
Name Scope Last used Actions {keys.map((k) => { const show = revealed.includes(k.id) && k.id !== fresh?.id; return (

{k.name}

{show ? k.secret : mask(k.secret)}
{scopeItems.find((s) => s.value === k.scope)?.label} {/^never$/i.test(k.lastUsed) ? "Never used" : `Used ${k.lastUsed}`}
{k.lastUsed}
}> setRevealed((r) => (r.includes(k.id) ? r.filter((x) => x !== k.id) : [...r, k.id]))} > {show ? : } {show ? "Hide key" : "Reveal key"} navigator.clipboard?.writeText(k.secret).catch(() => {})}> Copy key setRevoking(k)} variant="destructive"> Revoke
); })}
{keys.length === 0 && (

No keys yet

Create one for your server or CI.

)}
!open && setRevoking(null)} open={Boolean(revoking)}> Revoke {revoking?.name}? Anything still using this key starts getting 401s immediately. This can't be undone. }>Keep key { if (!revoking) return; const target = revoking; setKeys((all) => all.filter((x) => x.id !== target.id)); if (fresh?.id === target.id) setFresh(null); }} variant="destructive" /> } > Revoke key ); } function UsageRow({ item, shown }: { item: SettingsUsage; shown: boolean }) { const reduce = useReducedMotionAfterMount(); const pct = Math.min(100, (item.used / item.limit) * 100); const projPct = item.projected ? Math.min(100, (item.projected / item.limit) * 100) : 0; const over = (item.projected ?? 0) > item.limit; const fmt = item.unit ? (n: number) => n.toFixed(1) : formatInt; const fill = { duration: reduce ? 0 : 0.6, ease: easeOut }; return (
{item.label} {" "} / {item.limit.toLocaleString("en-US")} {item.unit ? ` ${item.unit}` : ""}
{projPct > pct && ( )} = 85 ? "bg-warning" : "bg-primary")} initial={false} style={{ width: `${pct}%` }} transition={fill} />
); } function BillingPanel({ active, plans, current, renewsOn, seats, usage, invoices: initialInvoices, card, daysLeft, overagePer1k, }: { active: boolean; plans: SettingsPlan[]; current: string; renewsOn: string; seats: number; usage: SettingsUsage[]; invoices: SettingsInvoice[]; card: SettingsCard; daysLeft: number; overagePer1k: number; }) { const [planId, setPlanId] = useState(current); const [picking, setPicking] = useState(false); const [choice, setChoice] = useState(current); const [switching, setSwitching] = useState(false); const [invoices, setInvoices] = useState(initialInvoices); const [retrying, setRetrying] = useState(false); const [recovered, setRecovered] = useState(null); const [hasOpened, setHasOpened] = useState(active); const reduce = useReducedMotionAfterMount(); const pickerId = useId(); const plan = plans.find((p) => p.id === planId) ?? plans[0]; const failed = invoices.find((i) => i.status === "failed"); const runs = usage.find((u) => u.projected !== undefined); const overRuns = runs?.projected ? Math.max(0, runs.projected - runs.limit) : 0; useEffect(() => { if (active) setHasOpened(true); }, [active]); useEffect(() => { if (!switching) return; const t = setTimeout(() => { setPlanId(choice); setSwitching(false); setPicking(false); }, 650); return () => clearTimeout(t); }, [switching, choice]); useEffect(() => { if (!retrying || !failed) return; const t = setTimeout(() => { setInvoices((all) => all.map((i) => (i.id === failed.id ? { ...i, status: "paid" } : i))); setRecovered(failed); setRetrying(false); }, 900); return () => clearTimeout(t); }, [retrying, failed]); useEffect(() => { if (!recovered) return; const t = setTimeout(() => setRecovered(null), 2800); return () => clearTimeout(t); }, [recovered]); const banner = failed ? { key: "failed" as const, invoice: failed } : recovered ? { key: "recovered" as const, invoice: recovered } : null; return ( <> {banner && (
{banner.key === "failed" ? : } {banner.key === "failed" ? `The ${banner.invoice.date.replace(/, \d{4}$/, "")} payment didn't go through` : "Payment received"}

{banner.key === "failed" ? ( <> {formatCents(banner.invoice.amount)} on the card ending{" "} {card.last4} was declined. ) : ( <> {banner.invoice.id} is paid. A receipt is on its way to {card.email ?? "your billing email"}. )}

{banner.key === "failed" && ( )}
)}

{plan.name} Current

per seat / month · {plan.blurb}

{!picking && ( )}
{plans.map((p) => { const on = choice === p.id; return ( ); })}
{seats} seats × , charged {renewsOn} } label="Next invoice" >
Resets {renewsOn}, {daysLeft} days from now. } title="Usage this cycle" > {usage.map((u) => ( ))} {runs?.projected !== undefined && (

Projected by {renewsOn}

{formatInt(runs.projected)} runs

Estimated overage

0 && "text-warning-foreground")}> {formatCents((overRuns / 1000) * overagePer1k)} at ${overagePer1k} / 1k

)}

{card.brand ?? "Card"} ending {card.last4}

Expires {card.expires} · {card.holder}

{card.email && ( )}
Invoice Date Status Amount Download {invoices.length === 0 && (

No invoices yet. The first one arrives on {renewsOn}.

)} {invoices.map((inv) => (

{inv.id}

{inv.date}

{inv.date} {invoiceBadge[inv.status].label} {formatCents(inv.amount)}
))}
); } export function SettingsAndPlans({ className, defaultTab = "workspace", workspace = { name: "Ridgeline", slug: "ridgeline", region: "eu-central" }, members = defaultMembers, apiKeys = defaultKeys, plans = defaultPlans, currentPlan = "team", renewsOn = "Nov 14", usage = defaultUsage, invoices = defaultInvoices, cycleDaysLeft = 18, overagePer1k = 4, card = { brand: "Visa", last4: "4417", expires: "09/28", holder: "Ada Lindqvist", email: "billing@ridgeline.app" }, domain = "ridgeline.app", onNavigate, }: SettingsAndPlansProps) { const [tab, setTab] = useState(defaultTab); const [saved, setSaved] = useState(workspace); const [draft, setDraft] = useState(workspace); const [saving, setSaving] = useState(false); const [justSaved, setJustSaved] = useState(false); const scrollRef = useRef(null); const reduce = useReducedMotionAfterMount(); const seatLimit = usage.find((u) => u.label === "Seats")?.limit ?? 10; const copy = tabCopy[tab]; const errors = workspaceErrors(draft); const blocker = errors.name ?? errors.slug; const dirty = draft.name !== saved.name || draft.slug !== saved.slug || draft.region !== saved.region; const barVisible = tab === "workspace" && (dirty || justSaved); useEffect(() => { if (!saving) return; const t = setTimeout(() => { setSaving(false); setSaved(draft); setJustSaved(true); }, 600); return () => clearTimeout(t); }, [saving, draft]); useEffect(() => { if (!justSaved) return; const t = setTimeout(() => setJustSaved(false), 1600); return () => clearTimeout(t); }, [justSaved]); const select = (id: SettingsSectionId) => { onNavigate?.(id); const next = tabForNav[id]; if (!next || next === tab) return; setTab(next); scrollRef.current?.parentElement?.scrollTo({ top: 0 }); }; return (

{copy.title}

{copy.description}

{tabOrder.map((t) => ( {t === "workspace" && } {t === "members" && } {t === "keys" && } {t === "billing" && ( )} ))}
{barVisible && (
{justSaved && !dirty ? ( Workspace saved ) : ( Unsaved changes You have unsaved changes {blocker && !saving && {blocker}} )}
)}
); }