import { createSignal, onCleanup, onMount, Show } from "solid-js"; import type { VaultEntry } from "../../shared/types"; import { sendMessage } from "../../shared/bridge"; import { NotesTOC } from "./NotesTOC"; import { NotesEditor } from "./NotesEditor"; interface NotesModeProps { onBack: () => void; onNoteListChanged: () => void; initialNoteId?: string; notes: VaultEntry[]; } type Phase = "toc" | "expanding" | "editor" | "collapsing"; interface ExpansionOrigin { noteId: string; top: number; fontSize: number; } let containerEl: HTMLDivElement | null = null; export function NotesMode(props: NotesModeProps) { let modeEl!: HTMLDivElement; let flyingTitleEl: HTMLSpanElement | undefined; const [phase, setPhase] = createSignal("toc"); const [activeNote, setActiveNote] = createSignal(null); const [isNewNote, setIsNewNote] = createSignal(false); const [origin, setOrigin] = createSignal(null); const [editorVisible, setEditorVisible] = createSignal(false); onMount(() => { containerEl = modeEl; // If an initialNoteId was passed (from global search), open it immediately if (props.initialNoteId) { const note = props.notes.find((n) => n.id === props.initialNoteId); if (note) { // Open without expansion animation — just direct to editor setActiveNote(note); setPhase("editor"); setEditorVisible(true); } } onCleanup(() => { containerEl = null; }); }); function handleNoteRowTap(note: VaultEntry, rowEl: HTMLElement) { if (phase() !== "toc") return; const rect = rowEl.getBoundingClientRect(); const containerRect = modeEl.getBoundingClientRect(); setOrigin({ noteId: note.id, top: rect.top - containerRect.top, fontSize: 18, }); setActiveNote(note); setIsNewNote(false); startExpansion(); } function handleNewNote() { if (phase() !== "toc") return; // New note expands from the bottom const containerRect = modeEl.getBoundingClientRect(); setOrigin({ noteId: "", top: containerRect.height - 60, fontSize: 14, }); setActiveNote(null); setIsNewNote(true); startExpansion(); } function startExpansion() { setPhase("expanding"); // RAF to let the flying title render at its start position requestAnimationFrame(() => { requestAnimationFrame(() => { setEditorVisible(true); setPhase("editor"); }); }); } function handleEditorBack() { if (phase() !== "editor") return; setPhase("collapsing"); setEditorVisible(false); setTimeout(() => { setActiveNote(null); setIsNewNote(false); setOrigin(null); setPhase("toc"); }, 350); } function handleSaved(note: VaultEntry) { // If this was a new note, now it has an id — update active note if (activeNote()?.id !== note.id) { setActiveNote(note); } props.onNoteListChanged(); } function handleDeleteNote(note: VaultEntry) { void sendMessage({ type: "DELETE_NOTE", payload: { id: note.id } }); props.onNoteListChanged(); handleEditorBack(); } return (
{/* TOC — always mounted, hidden during editor phase */}
{/* Editor — mounted only when a note is active */}
); }