/** * `MemoryEditor` — a fullscreen overlay for creating or editing memory notes * (L14). Rendered on top of the rest of the UI (`fixed inset-0`). * * Provides: * - Create-mode: name (slug source) + description + type selector + content * - Edit-mode: slug shown read-only (identity is immutable), description + type * + content editable, and a read-only list of resolved `[[wikilinks]]`. * * Pure presentation: all mutations are delegated to the callbacks supplied by * the parent (`MemoryPanel`). Styled with `@/shared`; no inline styles. */ import { useEffect, useState } from "react"; import type { MemoryIndexEntry, MemoryLink, MemoryType } from "@/domain"; import { Button, Input, cn } from "@/shared"; const MEMORY_TYPES: MemoryType[] = ["user", "feedback", "project", "reference"]; export interface MemoryEditorProps { /** * When set, the editor is in edit-mode for the given index entry; otherwise it * is in create-mode (all fields start empty). The slug is immutable in edit. */ entry?: MemoryIndexEntry | null; /** Loads the full content of the entry being edited (edit-mode only). */ loadContent?: (slug: string) => Promise; /** Resolves the entry's `[[wikilinks]]` to existing target slugs (edit-mode only). */ resolveLinks?: (slug: string) => Promise; /** Create-mode submit: name + description + type + content. */ onCreate: ( name: string, description: string, type: MemoryType, content: string, ) => Promise; /** Edit-mode submit: description + type + content (slug stays fixed). */ onUpdate: ( slug: string, description: string, type: MemoryType, content: string, ) => Promise; /** Called when the user cancels / closes the overlay. */ onClose: () => void; /** Whether a save operation is in flight. */ busy?: boolean; } export function MemoryEditor({ entry, loadContent, resolveLinks, onCreate, onUpdate, onClose, busy = false, }: MemoryEditorProps) { const editing = entry != null; const [name, setName] = useState(entry?.title ?? ""); const [description, setDescription] = useState(entry?.hook ?? ""); const [type, setType] = useState(entry?.type ?? "project"); const [content, setContent] = useState(""); const [links, setLinks] = useState([]); // In edit-mode, hydrate the editable content and the resolved links lazily. useEffect(() => { if (!entry) return; let cancelled = false; void (async () => { try { if (loadContent) { const loaded = await loadContent(entry.slug); if (!cancelled) setContent(loaded); } if (resolveLinks) { const resolved = await resolveLinks(entry.slug); if (!cancelled) setLinks(resolved); } } catch { // Best-effort hydration: leave fields as-is on failure. } })(); return () => { cancelled = true; }; }, [entry, loadContent, resolveLinks]); const canSave = description.trim().length > 0 && content.trim().length > 0 && !busy && (editing || name.trim().length > 0); async function handleSubmit(e: React.FormEvent) { e.preventDefault(); if (!canSave) return; if (editing) { await onUpdate(entry.slug, description.trim(), type, content); } else { await onCreate(name.trim(), description.trim(), type, content); } } const selectClass = cn( "h-9 w-full rounded-md bg-raised px-3 text-sm text-content", "border border-border outline-none transition-colors", "focus:border-primary disabled:cursor-not-allowed disabled:opacity-50", ); return (
{/* ── Header bar ── */}

{editing ? `Edit note — ${entry.title}` : "New note"}

{/* ── Form body ── */}
void handleSubmit(e)} className="flex flex-1 flex-col gap-0 overflow-hidden" > {/* ── Meta fields ── */}
{editing ? (
Slug {entry.slug}
) : (
setName(e.target.value)} disabled={busy} className="min-w-48" />
)}
{/* ── Description ── */}
setDescription(e.target.value)} disabled={busy} />
{/* ── Content ── */}