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" // TOC fully visible | "expanding" // rows fading, title flying up | "editor" // editor fully visible | "collapsing"; // editor fading, title flying down interface ExpansionOrigin { top: number; // px from top of container title: string; } // Editor title top offset (below header + spacing) const EDITOR_TITLE_TOP = 68; const EXPAND_DURATION = 320; const COLLAPSE_DURATION = 320; export function NotesMode(props: NotesModeProps) { let modeEl!: HTMLDivElement; let flyingTitleEl: HTMLDivElement | undefined; let tocWrapperEl: HTMLDivElement | undefined; let editorWrapperEl: HTMLDivElement | undefined; const [phase, setPhase] = createSignal("toc"); const [activeNote, setActiveNote] = createSignal(null); const [isNewNote, setIsNewNote] = createSignal(false); const [origin, setOrigin] = createSignal(null); const [flyingTitle, setFlyingTitle] = createSignal(""); const [flyingTop, setFlyingTop] = createSignal(0); const [flyingFontSize, setFlyingFontSize] = createSignal(18); const [flyingVisible, setFlyingVisible] = createSignal(false); onMount(() => { // Direct-to-editor from global search if (props.initialNoteId) { const note = props.notes.find((n) => n.id === props.initialNoteId); if (note) { setActiveNote(note); setPhase("editor"); } } }); function vibrate(ms: number) { try { navigator.vibrate?.(ms); } catch { /* unsupported */ } } function handleNoteRowTap(note: VaultEntry, rowEl: HTMLElement) { if (phase() !== "toc") return; vibrate(12); const rect = rowEl.getBoundingClientRect(); const containerRect = modeEl.getBoundingClientRect(); const rowTop = rect.top - containerRect.top; // The title text starts about 50% vertically within the row const titleTop = rowTop + (rect.height - 18) / 2; setOrigin({ top: titleTop, title: note.title || "Untitled" }); setFlyingTitle(note.title || "Untitled"); setFlyingTop(titleTop); setFlyingFontSize(18); setFlyingVisible(true); setActiveNote(note); setIsNewNote(false); // Phase 1: show flying title at row position, fade TOC rows setPhase("expanding"); // Phase 2: animate title to editor position requestAnimationFrame(() => { requestAnimationFrame(() => { setFlyingTop(EDITOR_TITLE_TOP); setFlyingFontSize(32); }); }); // Phase 3: after animation, show editor and hide flying title setTimeout(() => { setPhase("editor"); setFlyingVisible(false); }, EXPAND_DURATION); } function handleNewNote() { if (phase() !== "toc") return; vibrate(12); setActiveNote(null); setIsNewNote(true); setOrigin(null); // Simple expand from bottom — no flying title for new note setPhase("expanding"); requestAnimationFrame(() => { setTimeout(() => { setPhase("editor"); }, 80); }); } function handleEditorBack() { if (phase() !== "editor") return; vibrate(12); const orig = origin(); if (orig) { // Animate title back to its row setFlyingTitle(activeNote()?.title || "Untitled"); setFlyingTop(EDITOR_TITLE_TOP); setFlyingFontSize(32); setFlyingVisible(true); setPhase("collapsing"); requestAnimationFrame(() => { requestAnimationFrame(() => { setFlyingTop(orig.top); setFlyingFontSize(18); }); }); setTimeout(() => { setFlyingVisible(false); setActiveNote(null); setIsNewNote(false); setOrigin(null); setPhase("toc"); }, COLLAPSE_DURATION); } else { // No animation (new note or direct open) setPhase("collapsing"); setTimeout(() => { setActiveNote(null); setIsNewNote(false); setPhase("toc"); }, 200); } } function handleSaved(note: VaultEntry) { if (!activeNote() || activeNote()!.id !== note.id) { setActiveNote(note); } if (flyingTitle() !== note.title) { setFlyingTitle(note.title || "Untitled"); } props.onNoteListChanged(); } function handleDeleteNote(note: VaultEntry) { void sendMessage({ type: "DELETE_NOTE", payload: { id: note.id } }); props.onNoteListChanged(); handleEditorBack(); } return (
{/* TOC wrapper */}
{/* Flying title — shared element between TOC and editor during animation */} {/* Editor wrapper */}
); }