Add multi-diagram management

- DiagramRepository interface redesigned around multiple diagrams:
  list()/loadById()/deleteById() replace the old single-diagram
  load()/clear(). Both implementations updated to match.
- SupabaseDiagramRepository.save() now does an explicit update-or-insert
  instead of a blind upsert, so owner_id is only ever set at creation --
  an upsert would resend it on every save and let whoever saves last
  silently reassign ownership. Not reachable yet (no collaborator UI),
  but a real landmine once diagram sharing (organized-ideas.md §8) lands,
  and cheap to avoid now.
- LocalStorageDiagramRepository now stores diagrams keyed by id (was a
  single fixed key), keeping it a genuine working fallback rather than
  a stale reference implementing an old interface.
- Store: loadInitialDiagram (renamed from loadFromStorage) opens the
  last diagram you had open (tracked in localStorage -- a UI
  preference, not app data), falling back to the most recently updated
  one, falling back to a fresh empty diagram. New actions:
  refreshDiagramList, switchToDiagram, deleteDiagram. newProject and
  importProject now persist immediately (not just via the debounced
  autosave) so a new/imported diagram shows up in the list right away;
  importProject also assigns a fresh id so it can't collide with an
  existing diagram.
- New DiagramManagerModal (list/open/delete/+New), opened from a
  "Diagrams" button in TopBar that replaces the old single-diagram
  "New" button and its now-unnecessary confirmation dialog -- nothing
  is lost by creating a new diagram anymore, since the old one stays
  saved and reachable from the list.

Verified insert/list/update(-preserves-owner)/loadById/delete against
the real local stack; RLS test suite still 23/23 after a fresh reset.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017DUU6CnxECCDeqDNYJgr5x
This commit is contained in:
2026-09-07 21:19:24 -05:00
co-authored by Claude Sonnet 5
parent 027455cc7d
commit 2f1b83e0a9
7 changed files with 284 additions and 75 deletions
@@ -0,0 +1,95 @@
import { useEffect } from 'react'
import { useProjectStore } from '../../state/projectStore'
import Modal from '../common/Modal'
function formatUpdatedAt(iso: string): string {
const date = new Date(iso)
return date.toLocaleString(undefined, { dateStyle: 'medium', timeStyle: 'short' })
}
export default function DiagramManagerModal({ onClose }: { onClose: () => void }) {
const project = useProjectStore((s) => s.project)
const diagramList = useProjectStore((s) => s.diagramList)
const refreshDiagramList = useProjectStore((s) => s.refreshDiagramList)
const newProject = useProjectStore((s) => s.newProject)
const switchToDiagram = useProjectStore((s) => s.switchToDiagram)
const deleteDiagram = useProjectStore((s) => s.deleteDiagram)
useEffect(() => {
refreshDiagramList()
}, [refreshDiagramList])
const handleOpen = async (id: string) => {
if (id === project.id) return
await switchToDiagram(id)
onClose()
}
const handleNew = async () => {
await newProject()
onClose()
}
const handleDelete = async (id: string, name: string) => {
const confirmed = window.confirm(
diagramList.length === 1
? `Delete "${name}"? This is your only diagram, so a fresh empty one will replace it.`
: `Delete "${name}"? This can't be undone.`,
)
if (!confirmed) return
await deleteDiagram(id)
}
return (
<Modal title="Diagrams" onClose={onClose} width="max-w-lg">
<div className="space-y-3">
<button
onClick={handleNew}
className="w-full rounded bg-indigo-600 py-1.5 text-xs font-medium text-white hover:bg-indigo-500"
>
+ New Diagram
</button>
<ul className="space-y-1">
{diagramList.map((d) => {
const isCurrent = d.id === project.id
return (
<li
key={d.id}
className={`group flex items-center justify-between gap-2 rounded border px-3 py-2 text-xs ${
isCurrent ? 'border-indigo-300 bg-indigo-50' : 'border-slate-200 bg-white'
}`}
>
<button
onClick={() => handleOpen(d.id)}
disabled={isCurrent}
className="min-w-0 flex-1 text-left disabled:cursor-default"
>
<div className="flex items-center gap-1.5">
<span className="truncate font-medium text-slate-700">{d.name}</span>
{isCurrent && (
<span className="shrink-0 rounded bg-indigo-100 px-1 text-[10px] font-medium text-indigo-700">
current
</span>
)}
</div>
<div className="truncate text-[10px] text-slate-400">Updated {formatUpdatedAt(d.updatedAt)}</div>
</button>
<button
onClick={() => handleDelete(d.id, d.name)}
title="Delete this diagram"
className="shrink-0 rounded px-1.5 py-0.5 text-slate-400 opacity-0 hover:bg-red-50 hover:text-red-600 group-hover:opacity-100"
>
✕
</button>
</li>
)
})}
{diagramList.length === 0 && (
<p className="py-2 text-center text-xs italic text-slate-400">No diagrams yet.</p>
)}
</ul>
</div>
</Modal>
)
}