improved a lot of things
This commit is contained in:
@@ -4,6 +4,7 @@ import { useState, useCallback, useRef } from 'react';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { useCanvas } from '@/store/canvas';
|
||||
import { useViewport } from '@/store/viewport';
|
||||
import { useDiffs } from '@/hooks/useDiffs';
|
||||
import { LiveArtboard } from './LiveArtboard';
|
||||
import { SelectionOverlay } from './SelectionOverlay';
|
||||
import type { FiberNode } from '@originmain/renderer';
|
||||
@@ -18,11 +19,22 @@ interface ArtboardProps {
|
||||
renderUrl?: string;
|
||||
}
|
||||
|
||||
// Status color map matching the inspector
|
||||
const DIFF_STATUS_BADGE: Record<string, { color: string; bg: string; label: string }> = {
|
||||
DRAFT: { color: '#FFBA7B', bg: 'rgba(255,186,123,0.15)', label: 'draft' },
|
||||
REVIEWED: { color: '#7EB8FF', bg: 'rgba(126,184,255,0.15)', label: 'reviewed' },
|
||||
APPLIED: { color: '#10B981', bg: 'rgba(16,185,129,0.15)', label: 'applied' },
|
||||
REJECTED: { color: '#FF8080', bg: 'rgba(255,128,128,0.15)', label: 'blocked' },
|
||||
};
|
||||
|
||||
export function Artboard({ id, label, x, y, width, height, renderUrl }: ArtboardProps) {
|
||||
const { selectedArtboardId, selectArtboard, workspaceId, projectId, setArtboardLive, setFiberRoot, selectComponent } = useCanvas();
|
||||
const selected = selectedArtboardId === id;
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
// Diff status badges — fetch is cached by TanStack Query across all artboards
|
||||
const { diffs } = useDiffs(id);
|
||||
|
||||
// ── Fiber tree (from LiveArtboard) ─────────────────────────────────────────
|
||||
const [localFiberRoot, setLocalFiberRoot] = useState<FiberNode | undefined>(undefined);
|
||||
|
||||
@@ -246,6 +258,45 @@ export function Artboard({ id, label, x, y, width, height, renderUrl }: Artboard
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Diff status badge strip — bottom-right overlay, only visible when diffs exist */}
|
||||
{diffs.length > 0 && (() => {
|
||||
// Group by status and show compact chips
|
||||
const counts: Record<string, number> = {};
|
||||
for (const d of diffs) counts[d.status] = (counts[d.status] ?? 0) + 1;
|
||||
const entries = Object.entries(counts).filter(([s]) => s in DIFF_STATUS_BADGE);
|
||||
if (entries.length === 0) return null;
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute', bottom: 6, right: 6, zIndex: 10,
|
||||
display: 'flex', gap: 4, pointerEvents: 'none',
|
||||
}}
|
||||
>
|
||||
{entries.map(([status, count]) => {
|
||||
const b = DIFF_STATUS_BADGE[status]!;
|
||||
return (
|
||||
<span
|
||||
key={status}
|
||||
title={`${count} ${b.label} diff${count !== 1 ? 's' : ''}`}
|
||||
style={{
|
||||
display: 'inline-flex', alignItems: 'center', gap: 3,
|
||||
padding: '2px 5px', borderRadius: 4,
|
||||
background: b.bg, border: `1px solid ${b.color}33`,
|
||||
fontFamily: "'JetBrains Mono', ui-monospace, monospace",
|
||||
fontSize: '0.5rem', fontWeight: 600,
|
||||
color: b.color, letterSpacing: '0.03em',
|
||||
backdropFilter: 'blur(4px)',
|
||||
}}
|
||||
>
|
||||
<span style={{ width: 4, height: 4, borderRadius: '50%', background: b.color, display: 'inline-block' }} />
|
||||
{count}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
|
||||
{/* Content */}
|
||||
{renderUrl ? (
|
||||
<>
|
||||
|
||||
@@ -10,6 +10,7 @@ import { Inspector } from '../inspector/Inspector';
|
||||
import { useHistory } from '@/store/history';
|
||||
import { useCanvas, type Tool } from '@/store/canvas';
|
||||
import { useViewport } from '@/store/viewport';
|
||||
import { useTheme } from '@/store/theme';
|
||||
|
||||
interface AppChromeProps {
|
||||
workspaceId?: string;
|
||||
@@ -22,6 +23,7 @@ export function AppChrome({ workspaceId, projectId, workspaceName, projectName }
|
||||
const selectedArtboardId = useCanvas((s) => s.selectedArtboardId);
|
||||
const setContext = useCanvas((s) => s.setContext);
|
||||
const setActiveTool = useCanvas((s) => s.setActiveTool);
|
||||
const { mode: themeMode, toggle: toggleTheme } = useTheme();
|
||||
|
||||
// Push workspace/project IDs into the store so Canvas and Navigator can read them.
|
||||
useEffect(() => {
|
||||
@@ -162,6 +164,34 @@ export function AppChrome({ workspaceId, projectId, workspaceName, projectName }
|
||||
|
||||
<div style={{ flex: 1 }} />
|
||||
|
||||
{/* Theme toggle */}
|
||||
<button
|
||||
onClick={toggleTheme}
|
||||
title={`Switch to ${themeMode === 'dark' ? 'light' : 'dark'} mode`}
|
||||
style={{
|
||||
background: 'none', border: 'none', cursor: 'pointer',
|
||||
color: 'rgba(255,255,255,0.35)', padding: '4px 6px',
|
||||
display: 'flex', alignItems: 'center',
|
||||
transition: 'color 0.12s',
|
||||
fontSize: 13,
|
||||
}}
|
||||
onMouseEnter={e => (e.currentTarget.style.color = 'rgba(255,255,255,0.75)')}
|
||||
onMouseLeave={e => (e.currentTarget.style.color = 'rgba(255,255,255,0.35)')}
|
||||
>
|
||||
{themeMode === 'dark' ? (
|
||||
/* Sun icon */
|
||||
<svg width="13" height="13" viewBox="0 0 14 14" fill="none">
|
||||
<circle cx="7" cy="7" r="2.5" stroke="currentColor" strokeWidth="1.2"/>
|
||||
<path d="M7 1v1.5M7 11.5V13M1 7h1.5M11.5 7H13M2.93 2.93l1.06 1.06M10.01 10.01l1.06 1.06M2.93 11.07l1.06-1.06M10.01 3.99l1.06-1.06" stroke="currentColor" strokeWidth="1.2" strokeLinecap="round"/>
|
||||
</svg>
|
||||
) : (
|
||||
/* Moon icon */
|
||||
<svg width="13" height="13" viewBox="0 0 14 14" fill="none">
|
||||
<path d="M12 8.5A5.5 5.5 0 0 1 5.5 2a5.5 5.5 0 1 0 6.5 6.5z" stroke="currentColor" strokeWidth="1.2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
</svg>
|
||||
)}
|
||||
</button>
|
||||
|
||||
<div style={{ transform: 'scale(0.8)', transformOrigin: 'right center' }}>
|
||||
<UserButton />
|
||||
</div>
|
||||
|
||||
@@ -542,19 +542,47 @@ function HSep() {
|
||||
function DiffTab({ artboardId }: { artboardId: string | null }) {
|
||||
const { stacks } = useHistory();
|
||||
const { diffs, createDiff, isLoading } = useDiffs(artboardId);
|
||||
const [summaryStatus, setSummaryStatus] = useState<'idle' | 'summarising' | 'exporting'>('idle');
|
||||
|
||||
const artboardHistory = artboardId ? (stacks[artboardId] ?? { past: [], future: [] }) : { past: [], future: [] };
|
||||
const pendingChanges: PropChange[] = artboardHistory.past.flatMap(e => e.changes);
|
||||
const hasChanges = pendingChanges.length > 0;
|
||||
|
||||
const exportDiff = useCallback(() => {
|
||||
const exportDiff = useCallback(async () => {
|
||||
if (!artboardId || !hasChanges) return;
|
||||
createDiff.mutate({
|
||||
artboard_id: artboardId,
|
||||
changes_jsonb: { propChanges: pendingChanges, styleChanges: [] },
|
||||
summary: '',
|
||||
status: 'DRAFT',
|
||||
});
|
||||
|
||||
// 1. Generate AI summary (best-effort — fall back to empty string on failure)
|
||||
let summary = '';
|
||||
const meaningfulChanges = pendingChanges.filter(c => c.changeType !== 'unchanged');
|
||||
if (meaningfulChanges.length > 0) {
|
||||
setSummaryStatus('summarising');
|
||||
try {
|
||||
const res = await fetch('/api/ai/diff-summary', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
changesJson: JSON.stringify(meaningfulChanges),
|
||||
componentName: meaningfulChanges[0]?.key ?? 'Component',
|
||||
}),
|
||||
});
|
||||
if (res.ok) {
|
||||
const data = await res.json() as { summary?: string };
|
||||
summary = data.summary ?? '';
|
||||
}
|
||||
} catch { /* non-fatal — proceed without summary */ }
|
||||
}
|
||||
|
||||
// 2. Export diff with AI-generated summary included
|
||||
setSummaryStatus('exporting');
|
||||
createDiff.mutate(
|
||||
{
|
||||
artboard_id: artboardId,
|
||||
changes_jsonb: { propChanges: pendingChanges, styleChanges: [] },
|
||||
summary,
|
||||
status: 'DRAFT',
|
||||
},
|
||||
{ onSettled: () => setSummaryStatus('idle') },
|
||||
);
|
||||
}, [artboardId, pendingChanges, hasChanges, createDiff]);
|
||||
|
||||
if (!artboardId) {
|
||||
@@ -579,18 +607,22 @@ function DiffTab({ artboardId }: { artboardId: string | null }) {
|
||||
<DiffChangeRow key={i} change={change} />
|
||||
))}
|
||||
<button
|
||||
onClick={exportDiff}
|
||||
disabled={createDiff.isPending}
|
||||
onClick={() => void exportDiff()}
|
||||
disabled={summaryStatus !== 'idle' || createDiff.isPending}
|
||||
style={{
|
||||
marginTop: 10, width: '100%',
|
||||
fontFamily: "'JetBrains Mono', monospace", fontSize: '0.5875rem',
|
||||
background: T.accent, border: 'none', borderRadius: 6,
|
||||
color: '#fff', padding: '7px 0', cursor: createDiff.isPending ? 'wait' : 'pointer',
|
||||
letterSpacing: '0.04em', opacity: createDiff.isPending ? 0.6 : 1,
|
||||
color: '#fff', padding: '7px 0',
|
||||
cursor: (summaryStatus !== 'idle' || createDiff.isPending) ? 'wait' : 'pointer',
|
||||
letterSpacing: '0.04em',
|
||||
opacity: (summaryStatus !== 'idle' || createDiff.isPending) ? 0.6 : 1,
|
||||
transition: 'opacity 0.15s',
|
||||
}}
|
||||
>
|
||||
{createDiff.isPending ? 'Exporting…' : 'Export diff →'}
|
||||
{summaryStatus === 'summarising' ? 'Summarising…' :
|
||||
summaryStatus === 'exporting' || createDiff.isPending ? 'Exporting…' :
|
||||
'Export diff →'}
|
||||
</button>
|
||||
{createDiff.isError && (
|
||||
<span style={{ fontSize: '0.5rem', color: '#FF8080', fontFamily: 'monospace', display: 'block', marginTop: 4 }}>
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useFileTree, FileTree } from '@pierre/trees/react';
|
||||
import { themeToTreeStyles } from '@pierre/trees';
|
||||
import { SquareRegular } from '@fluentui/react-icons';
|
||||
import { useCanvas } from '@/store/canvas';
|
||||
import { useArtboards, patchArtboard } from '@/hooks/useArtboards';
|
||||
import { useArtboards, patchArtboard, createArtboardMutation } from '@/hooks/useArtboards';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
const T = {
|
||||
@@ -61,6 +61,30 @@ export function ArtboardNavigator() {
|
||||
queryClient.invalidateQueries({ queryKey: ['artboards', workspaceId, projectId ?? undefined] });
|
||||
}, [workspaceId, projectId, queryClient]);
|
||||
|
||||
const forkArtboard = useCallback(async (id: string, label: string) => {
|
||||
if (!workspaceId) return;
|
||||
const source = rawArtboards.find(ab => ab.id === id);
|
||||
if (!source) return;
|
||||
const meta = { ...(source.metadata_jsonb as Record<string, unknown>) };
|
||||
// Offset fork to the right of the original so it doesn't overlap
|
||||
const srcWidth = typeof meta['width'] === 'number' ? (meta['width'] as number) : 360;
|
||||
meta['x'] = typeof meta['x'] === 'number' ? (meta['x'] as number) + srcWidth + 40 : 40;
|
||||
try {
|
||||
await createArtboardMutation({
|
||||
workspace_id: workspaceId,
|
||||
project_id: projectId ?? null,
|
||||
name: `Fork of ${label}`,
|
||||
origin_id: source.origin_id,
|
||||
parent_artboard_id: id,
|
||||
metadata_jsonb: meta,
|
||||
});
|
||||
queryClient.invalidateQueries({ queryKey: ['artboards', workspaceId, projectId ?? undefined] });
|
||||
} catch (err) {
|
||||
console.error('[Navigator] forkArtboard failed:', err);
|
||||
window.alert('Could not fork artboard — please try again.');
|
||||
}
|
||||
}, [workspaceId, projectId, rawArtboards, queryClient]);
|
||||
|
||||
// Real graph stats derived from live fiber trees
|
||||
const totalComponents = Object.values(artboardFiberRoots).reduce(
|
||||
(acc, root) => acc + countFiberNodes(root), 0,
|
||||
@@ -112,6 +136,7 @@ export function ArtboardNavigator() {
|
||||
}
|
||||
label={ab.label}
|
||||
onRename={() => void renameArtboard(ab.id, ab.label)}
|
||||
onFork={() => void forkArtboard(ab.id, ab.label)}
|
||||
onDelete={() => void deleteArtboard(ab.id, ab.label)}
|
||||
/>
|
||||
);
|
||||
@@ -160,6 +185,7 @@ function NavRow({
|
||||
icon,
|
||||
label,
|
||||
onRename,
|
||||
onFork,
|
||||
onDelete,
|
||||
}: {
|
||||
selected?: boolean;
|
||||
@@ -168,6 +194,7 @@ function NavRow({
|
||||
icon: React.ReactNode;
|
||||
label: string;
|
||||
onRename?: () => void;
|
||||
onFork?: () => void;
|
||||
onDelete?: () => void;
|
||||
}) {
|
||||
const [hov, setHov] = useState(false);
|
||||
@@ -206,18 +233,31 @@ function NavRow({
|
||||
}} />
|
||||
)}
|
||||
|
||||
{/* Action buttons: rename + delete — shown on hover */}
|
||||
{/* Action buttons: rename + fork + delete — shown on hover */}
|
||||
{(hov || selected) && (
|
||||
<div style={{ display: 'flex', gap: 2, flexShrink: 0 }} onClick={e => e.stopPropagation()}>
|
||||
{onRename && (
|
||||
<IconBtn title="Rename" onClick={onRename}>
|
||||
{/* Pencil */}
|
||||
<svg width="10" height="10" viewBox="0 0 10 10" fill="none">
|
||||
<path d="M1 7.5L7 1.5l1.5 1.5-6 6H1V7.5z" stroke="currentColor" strokeWidth="1" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
</svg>
|
||||
</IconBtn>
|
||||
)}
|
||||
{onFork && (
|
||||
<IconBtn title="Fork" onClick={onFork}>
|
||||
{/* Branch / fork icon */}
|
||||
<svg width="10" height="10" viewBox="0 0 10 10" fill="none">
|
||||
<circle cx="2" cy="2" r="1.2" stroke="currentColor" strokeWidth="1"/>
|
||||
<circle cx="8" cy="2" r="1.2" stroke="currentColor" strokeWidth="1"/>
|
||||
<circle cx="2" cy="8" r="1.2" stroke="currentColor" strokeWidth="1"/>
|
||||
<path d="M2 3.2v1.3C2 5.4 2.6 6 3.5 6H5M8 3.2V5a1 1 0 0 1-1 1H5m0 0v2" stroke="currentColor" strokeWidth="1" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
</svg>
|
||||
</IconBtn>
|
||||
)}
|
||||
{onDelete && (
|
||||
<IconBtn title="Delete" onClick={onDelete} danger>
|
||||
{/* Trash */}
|
||||
<svg width="10" height="10" viewBox="0 0 10 10" fill="none">
|
||||
<path d="M2 2.5h6M4 2.5V1.5h2V2.5M3 2.5v6h4v-6" stroke="currentColor" strokeWidth="1" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
</svg>
|
||||
|
||||
@@ -2,49 +2,69 @@
|
||||
|
||||
import Link from 'next/link';
|
||||
import { UserButton } from '@clerk/nextjs';
|
||||
import { useTheme } from '@/store/theme';
|
||||
|
||||
interface Crumb { label: string; href?: string }
|
||||
|
||||
interface AppHeaderProps {
|
||||
breadcrumbs?: Crumb[];
|
||||
/** @deprecated Pass breadcrumbs instead */
|
||||
workspaceName?: string;
|
||||
/** @deprecated Pass breadcrumbs instead */
|
||||
workspaceId?: string;
|
||||
}
|
||||
|
||||
export function AppHeader({ breadcrumbs = [] }: AppHeaderProps) {
|
||||
export function AppHeader({ breadcrumbs = [], workspaceName, workspaceId }: AppHeaderProps) {
|
||||
const { mode, toggle } = useTheme();
|
||||
|
||||
// Back-compat: if old props are passed without breadcrumbs, synthesise them
|
||||
const crumbs: Crumb[] = breadcrumbs.length > 0
|
||||
? breadcrumbs
|
||||
: workspaceName
|
||||
? [
|
||||
{ label: 'Workspaces', href: '/workspaces' as string },
|
||||
...(workspaceId
|
||||
? [{ label: workspaceName, href: `/workspace/${workspaceId}` as string }]
|
||||
: [{ label: workspaceName }]),
|
||||
]
|
||||
: [];
|
||||
|
||||
return (
|
||||
<header style={{
|
||||
position: 'sticky', top: 0, zIndex: 100,
|
||||
height: 56,
|
||||
background: 'rgba(255,255,255,0.92)',
|
||||
background: mode === 'dark' ? 'rgba(12,12,16,0.92)' : 'rgba(255,255,255,0.92)',
|
||||
backdropFilter: 'blur(12px)',
|
||||
borderBottom: '1px solid rgba(0,0,0,0.07)',
|
||||
borderBottom: mode === 'dark' ? '1px solid rgba(255,255,255,0.07)' : '1px solid rgba(0,0,0,0.07)',
|
||||
display: 'flex', alignItems: 'center',
|
||||
padding: '0 24px',
|
||||
gap: 0,
|
||||
}}>
|
||||
{/* Logo */}
|
||||
<Link href="/workspaces" style={{ textDecoration: 'none', flexShrink: 0 }}>
|
||||
<span style={{ fontSize: '1rem', fontWeight: 700, letterSpacing: '-0.02em', color: '#0A0A0A' }}>
|
||||
<span style={{ fontSize: '1rem', fontWeight: 700, letterSpacing: '-0.02em', color: mode === 'dark' ? '#FAFAFA' : '#0A0A0A' }}>
|
||||
Origin<span style={{ color: '#0066FF' }}>main</span>
|
||||
</span>
|
||||
</Link>
|
||||
|
||||
{/* Breadcrumbs */}
|
||||
{breadcrumbs.map((crumb, i) => (
|
||||
{crumbs.map((crumb, i) => (
|
||||
<span key={i} style={{ display: 'flex', alignItems: 'center' }}>
|
||||
<span style={{ margin: '0 8px', color: '#D4D4D8', fontSize: '0.875rem' }}>/</span>
|
||||
<span style={{ margin: '0 8px', color: mode === 'dark' ? 'rgba(255,255,255,0.2)' : '#D4D4D8', fontSize: '0.875rem' }}>/</span>
|
||||
{crumb.href ? (
|
||||
<Link href={crumb.href} style={{
|
||||
fontSize: '0.875rem', fontWeight: 500,
|
||||
color: '#71717A', textDecoration: 'none',
|
||||
color: mode === 'dark' ? 'rgba(255,255,255,0.45)' : '#71717A',
|
||||
textDecoration: 'none',
|
||||
transition: 'color 0.1s',
|
||||
}}
|
||||
onMouseEnter={e => (e.currentTarget.style.color = '#0A0A0A')}
|
||||
onMouseLeave={e => (e.currentTarget.style.color = '#71717A')}
|
||||
onMouseEnter={e => (e.currentTarget.style.color = mode === 'dark' ? 'rgba(255,255,255,0.85)' : '#0A0A0A')}
|
||||
onMouseLeave={e => (e.currentTarget.style.color = mode === 'dark' ? 'rgba(255,255,255,0.45)' : '#71717A')}
|
||||
>
|
||||
{crumb.label}
|
||||
</Link>
|
||||
) : (
|
||||
<span style={{ fontSize: '0.875rem', fontWeight: 600, color: '#0A0A0A' }}>
|
||||
<span style={{ fontSize: '0.875rem', fontWeight: 600, color: mode === 'dark' ? '#FAFAFA' : '#0A0A0A' }}>
|
||||
{crumb.label}
|
||||
</span>
|
||||
)}
|
||||
@@ -53,6 +73,40 @@ export function AppHeader({ breadcrumbs = [] }: AppHeaderProps) {
|
||||
|
||||
<div style={{ flex: 1 }} />
|
||||
|
||||
{/* Theme toggle */}
|
||||
<button
|
||||
onClick={toggle}
|
||||
title={`Switch to ${mode === 'dark' ? 'light' : 'dark'} mode`}
|
||||
style={{
|
||||
background: 'none', border: 'none', cursor: 'pointer',
|
||||
color: mode === 'dark' ? 'rgba(255,255,255,0.4)' : '#A1A1AA',
|
||||
padding: '6px 8px', marginRight: 8,
|
||||
display: 'flex', alignItems: 'center', borderRadius: 6,
|
||||
transition: 'color 0.12s, background 0.12s',
|
||||
}}
|
||||
onMouseEnter={e => {
|
||||
e.currentTarget.style.color = mode === 'dark' ? 'rgba(255,255,255,0.85)' : '#0A0A0A';
|
||||
e.currentTarget.style.background = mode === 'dark' ? 'rgba(255,255,255,0.06)' : 'rgba(0,0,0,0.05)';
|
||||
}}
|
||||
onMouseLeave={e => {
|
||||
e.currentTarget.style.color = mode === 'dark' ? 'rgba(255,255,255,0.4)' : '#A1A1AA';
|
||||
e.currentTarget.style.background = 'none';
|
||||
}}
|
||||
>
|
||||
{mode === 'dark' ? (
|
||||
/* Sun */
|
||||
<svg width="15" height="15" viewBox="0 0 14 14" fill="none">
|
||||
<circle cx="7" cy="7" r="2.5" stroke="currentColor" strokeWidth="1.2"/>
|
||||
<path d="M7 1v1.5M7 11.5V13M1 7h1.5M11.5 7H13M2.93 2.93l1.06 1.06M10.01 10.01l1.06 1.06M2.93 11.07l1.06-1.06M10.01 3.99l1.06-1.06" stroke="currentColor" strokeWidth="1.2" strokeLinecap="round"/>
|
||||
</svg>
|
||||
) : (
|
||||
/* Moon */
|
||||
<svg width="15" height="15" viewBox="0 0 14 14" fill="none">
|
||||
<path d="M12 8.5A5.5 5.5 0 0 1 5.5 2a5.5 5.5 0 1 0 6.5 6.5z" stroke="currentColor" strokeWidth="1.2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
</svg>
|
||||
)}
|
||||
</button>
|
||||
|
||||
<UserButton />
|
||||
</header>
|
||||
);
|
||||
|
||||
@@ -377,10 +377,18 @@ export function WorkspaceSettingsForm({ workspaceId, workspaceName, memberRole }
|
||||
border: '1px solid rgba(239,68,68,0.35)', background: 'transparent',
|
||||
color: '#EF4444', cursor: 'pointer',
|
||||
}}
|
||||
onClick={() => {
|
||||
if (window.confirm(`Delete workspace "${workspaceName}"? This cannot be undone.`)) {
|
||||
// TODO: call DELETE /api/workspace/:id when implemented
|
||||
window.alert('Delete endpoint not yet implemented — coming soon.');
|
||||
onClick={async () => {
|
||||
if (!window.confirm(`Delete workspace "${workspaceName}"? This cannot be undone. All projects and artboards will be permanently lost.`)) return;
|
||||
try {
|
||||
const res = await fetch(`/api/workspace/${workspaceId}`, { method: 'DELETE' });
|
||||
if (!res.ok) {
|
||||
const body = await res.json().catch(() => ({})) as { error?: string };
|
||||
throw new Error(body.error ?? `Server error ${res.status}`);
|
||||
}
|
||||
router.push('/workspaces');
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : 'Unknown error';
|
||||
window.alert(`Could not delete workspace: ${msg}`);
|
||||
}
|
||||
}}
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user