'use client'; import { useState, useCallback, useEffect, useRef } from 'react'; import { useRouter } from 'next/navigation'; interface WorkspaceSettingsFormProps { workspaceId: string; workspaceName: string; memberRole: string; } const SECTION: React.CSSProperties = { background: 'var(--card-bg)', border: '1px solid var(--card-border)', borderRadius: 14, padding: '24px 28px', marginBottom: 20, transition: 'background 0.2s, border-color 0.2s', }; const LABEL: React.CSSProperties = { display: 'block', fontSize: '0.8125rem', fontWeight: 600, color: 'var(--card-text)', marginBottom: 6, }; const INPUT: React.CSSProperties = { width: '100%', fontSize: '0.875rem', padding: '9px 12px', border: '1px solid var(--input-border)', borderRadius: 9, fontFamily: "'Inter', -apple-system, sans-serif", color: 'var(--input-text)', background: 'var(--input-bg)', boxSizing: 'border-box', }; const BTN_PRIMARY: React.CSSProperties = { display: 'inline-flex', alignItems: 'center', gap: 6, background: 'var(--btn-bg)', color: 'var(--btn-fg)', fontSize: '0.875rem', fontWeight: 600, padding: '9px 20px', borderRadius: 9, border: 'none', cursor: 'pointer', letterSpacing: '-0.01em', }; // Matches TeamRoleSchema in @originmain/origin-graph — keep in sync. type TeamRole = 'OWNER' | 'DESIGNER' | 'ENGINEER' | 'PM' | 'VIEWER'; function TeamInviteForm({ workspaceId }: { workspaceId: string }) { const [email, setEmail] = useState(''); const [role, setRole] = useState('DESIGNER'); const [status, setStatus] = useState<'idle' | 'loading' | 'done' | 'conflict' | 'error'>('idle'); const [errorMsg, setErrorMsg] = useState(''); const resetTimer = useRef | undefined>(undefined); // Clear any pending reset timers on unmount to avoid setState-after-unmount. useEffect(() => { return () => { clearTimeout(resetTimer.current); }; }, []); const submit = useCallback(async () => { const trimmed = email.trim().toLowerCase(); if (!trimmed) return; clearTimeout(resetTimer.current); setStatus('loading'); setErrorMsg(''); try { const res = await fetch(`/api/workspace/${workspaceId}/invite`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email: trimmed, role }), }); if (res.status === 409) { setStatus('conflict'); return; } if (!res.ok) { const data = await res.json() as { error?: string }; throw new Error(data.error ?? `HTTP ${res.status}`); } setStatus('done'); setEmail(''); resetTimer.current = setTimeout(() => setStatus('idle'), 3000); } catch (e) { setErrorMsg(e instanceof Error ? e.message : 'Invite failed'); setStatus('error'); resetTimer.current = setTimeout(() => setStatus('idle'), 4000); } }, [email, role, workspaceId]); const roles: TeamRole[] = ['DESIGNER', 'ENGINEER', 'PM', 'VIEWER', 'OWNER']; return (
{/* Role picker */}
{roles.map(r => ( ))}
{/* Email input + submit */}
setEmail(e.target.value)} onKeyDown={e => { if (e.key === 'Enter') void submit(); }} disabled={status === 'loading'} />
{/* Feedback */} {status === 'done' && (

✓ Member added successfully

)} {status === 'conflict' && (

User is already a member of this workspace

)} {status === 'error' && (

{errorMsg || 'Invite failed — try again'}

)}
); } export function WorkspaceSettingsForm({ workspaceId, workspaceName, memberRole }: WorkspaceSettingsFormProps) { const router = useRouter(); const isOwner = memberRole === 'OWNER'; /* ── Rename ── */ const [name, setName] = useState(workspaceName); const [renameStatus, setRenameStatus] = useState<'idle' | 'saving' | 'saved' | 'error'>('idle'); const renameTimer = useRef | undefined>(undefined); useEffect(() => { return () => { clearTimeout(renameTimer.current); }; }, []); async function saveName() { if (!name.trim() || name.trim() === workspaceName) return; clearTimeout(renameTimer.current); setRenameStatus('saving'); try { const res = await fetch(`/api/workspace/${workspaceId}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: name.trim() }), }); if (!res.ok) throw new Error('Rename failed'); setRenameStatus('saved'); router.refresh(); renameTimer.current = setTimeout(() => setRenameStatus('idle'), 2000); } catch { setRenameStatus('error'); renameTimer.current = setTimeout(() => setRenameStatus('idle'), 3000); } } /* ── IDE Token ── */ const [agentType, setAgentType] = useState<'CURSOR' | 'CLAUDE_CODE' | 'GENERIC'>('CLAUDE_CODE'); const [tokenResult, setTokenResult] = useState<{ token: string; cursorConfig: { cursorrules: string; settings: unknown }; claudeCodeConfig: { claudeMd: string; settings: unknown }; } | null>(null); const [tokenLoading, setTokenLoading] = useState(false); const [tokenError, setTokenError] = useState(''); const [copiedToken, setCopiedToken] = useState(false); async function issueToken() { setTokenLoading(true); setTokenError(''); setTokenResult(null); try { const res = await fetch(`/api/workspace/${workspaceId}/tokens`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ agentType, workspaceName }), }); if (!res.ok) throw new Error('Token issuance failed'); const data = await res.json() as typeof tokenResult; setTokenResult(data); } catch (e) { setTokenError(e instanceof Error ? e.message : 'Failed to issue token'); } finally { setTokenLoading(false); } } function copyToken() { if (!tokenResult) return; void navigator.clipboard.writeText(tokenResult.token); setCopiedToken(true); setTimeout(() => setCopiedToken(false), 2000); } const configText = tokenResult ? agentType === 'CURSOR' ? tokenResult.cursorConfig.cursorrules : tokenResult.claudeCodeConfig.claudeMd : ''; return ( <> {/* General */}

General

setName(e.target.value)} onKeyDown={e => { if (e.key === 'Enter') void saveName(); }} disabled={!isOwner} />
{renameStatus === 'error' && ( Rename failed — try again )} {!isOwner && ( Only workspace owners can rename. )}
{/* IDE Integration */}

IDE Integration

Issue a signed workspace token and copy the config snippet into your editor. Tokens expire after 30 days.

{(['CLAUDE_CODE', 'CURSOR', 'GENERIC'] as const).map(t => ( ))}
{tokenError && (

{tokenError}

)} {tokenResult && (
{/* Token display */}
{/* Config snippet */}
              {configText}
            

Paste this into your{' '} {agentType === 'CURSOR' ? ( .cursorrules ) : ( CLAUDE.md )}{' '} file to enable the Originmain MCP server in your editor.

)}
{/* Team */} {isOwner && (

Team

Add a team member using their email address. They must sign in with the same address.

)} {/* Danger zone */} {isOwner && (

Danger zone

Deleting this workspace is permanent and cannot be undone. All projects and artboards will be lost.

)} ); }