Phase 2: document CRUD + auto-save

Backend internal/docs: chi sub-router (list/create/get/update/delete)
mounted at /api/docs, scoped to the local user. Create uses RETURNING;
update is a COALESCE partial-update so rename and full editor save share
one PUT. JSON 404/400 errors; handlers_test.go walks the lifecycle.

Frontend: api/client.ts, useAutoSave (1.5s debounce + saveNow flush
before doc switch), EditorCore (Tiptap StarterKit/Underline/TextAlign/
Placeholder/CharacterCount) + Toolbar, DocList/DocListItem, StatusBar,
and an App.tsx that orchestrates load/select/create/delete with
optimistic sidebar patching. content + content_text + word_count are
emitted together on every edit. .petal-prose styling (Lora body, Nunito
headings).

Verified: tsc clean, vite build, go build, full CRUD smoke test incl.
CJK title round-trip and SPA serve.

Claude-Session: https://claude.ai/code/session_016Yr6jELuRc7hyzYLccQKZd
This commit is contained in:
prosolis
2026-06-25 20:30:39 -07:00
parent 9c98e97030
commit 5e00cdce88
13 changed files with 1006 additions and 42 deletions

View File

@@ -0,0 +1,49 @@
import type { DocSummary } from '../../api/client'
import { DocListItem } from './DocListItem'
interface Props {
docs: DocSummary[]
selectedId: string | null
onSelect: (id: string) => void
onCreate: () => void
onDelete: (id: string) => void
}
// DocList is the 260px sidebar: the document browser plus a New button.
export function DocList({ docs, selectedId, onSelect, onCreate, onDelete }: Props) {
return (
<aside
className="flex w-[260px] shrink-0 flex-col gap-2 p-3"
style={{ borderRight: '1px solid var(--color-border)' }}
>
<button
type="button"
onClick={onCreate}
className="flex items-center justify-center gap-2 py-2.5 text-sm font-bold text-white"
style={{ borderRadius: 'var(--radius-pill)', background: 'var(--color-accent)' }}
onMouseEnter={(e) => (e.currentTarget.style.background = 'var(--color-accent-hover)')}
onMouseLeave={(e) => (e.currentTarget.style.background = 'var(--color-accent)')}
>
<span className="text-base leading-none"></span> New Doc
</button>
<div className="flex flex-1 flex-col gap-0.5 overflow-y-auto">
{docs.length === 0 ? (
<p className="px-3 py-6 text-center text-xs" style={{ color: 'var(--color-muted)' }}>
No documents yet.
</p>
) : (
docs.map((doc) => (
<DocListItem
key={doc.id}
doc={doc}
active={doc.id === selectedId}
onSelect={() => onSelect(doc.id)}
onDelete={() => onDelete(doc.id)}
/>
))
)}
</div>
</aside>
)
}