1import { createSignal, onCleanup, onMount, Show } from "solid-js";2import type { VaultEntry } from "../../shared/types";3import { sendMessage } from "../../shared/bridge";4import { NotesTOC } from "./NotesTOC";5import { NotesEditor } from "./NotesEditor";67interface NotesModeProps {8 onBack: () => void;9 onNoteListChanged: () => void;10 initialNoteId?: string;11 notes: VaultEntry[];12}1314type Phase = "toc" | "expanding" | "editor" | "collapsing";1516interface ExpansionOrigin {17 noteId: string;18 top: number;19 fontSize: number;20}2122let containerEl: HTMLDivElement | null = null;2324export function NotesMode(props: NotesModeProps) {25 let modeEl!: HTMLDivElement;26 let flyingTitleEl: HTMLSpanElement | undefined;2728 const [phase, setPhase] = createSignal<Phase>("toc");29 const [activeNote, setActiveNote] = createSignal<VaultEntry | null>(null);30 const [isNewNote, setIsNewNote] = createSignal(false);31 const [origin, setOrigin] = createSignal<ExpansionOrigin | null>(null);32 const [editorVisible, setEditorVisible] = createSignal(false);3334 onMount(() => {35 containerEl = modeEl;3637 38 if (props.initialNoteId) {39 const note = props.notes.find((n) => n.id === props.initialNoteId);40 if (note) {41 42 setActiveNote(note);43 setPhase("editor");44 setEditorVisible(true);45 }46 }4748 onCleanup(() => { containerEl = null; });49 });5051 function handleNoteRowTap(note: VaultEntry, rowEl: HTMLElement) {52 if (phase() !== "toc") return;53 const rect = rowEl.getBoundingClientRect();54 const containerRect = modeEl.getBoundingClientRect();55 setOrigin({56 noteId: note.id,57 top: rect.top - containerRect.top,58 fontSize: 18,59 });60 setActiveNote(note);61 setIsNewNote(false);62 startExpansion();63 }6465 function handleNewNote() {66 if (phase() !== "toc") return;67 68 const containerRect = modeEl.getBoundingClientRect();69 setOrigin({70 noteId: "",71 top: containerRect.height - 60,72 fontSize: 14,73 });74 setActiveNote(null);75 setIsNewNote(true);76 startExpansion();77 }7879 function startExpansion() {80 setPhase("expanding");81 82 requestAnimationFrame(() => {83 requestAnimationFrame(() => {84 setEditorVisible(true);85 setPhase("editor");86 });87 });88 }8990 function handleEditorBack() {91 if (phase() !== "editor") return;92 setPhase("collapsing");93 setEditorVisible(false);94 setTimeout(() => {95 setActiveNote(null);96 setIsNewNote(false);97 setOrigin(null);98 setPhase("toc");99 }, 350);100 }101102 function handleSaved(note: VaultEntry) {103 104 if (activeNote()?.id !== note.id) {105 setActiveNote(note);106 }107 props.onNoteListChanged();108 }109110 function handleDeleteNote(note: VaultEntry) {111 void sendMessage({ type: "DELETE_NOTE", payload: { id: note.id } });112 props.onNoteListChanged();113 handleEditorBack();114 }115116 return (117 <div ref={modeEl} class="peach-notes-mode" data-phase={phase()}>118 {}119 <div120 class="peach-notes-toc-wrapper"121 classList={{122 "is-visible": phase() === "toc",123 "is-expanding": phase() === "expanding",124 "is-collapsing": phase() === "collapsing",125 }}126 aria-hidden={phase() !== "toc"}127 >128 <NotesTOC129 notes={props.notes}130 onBack={props.onBack}131 onOpenNote={handleNoteRowTap}132 onNewNote={handleNewNote}133 onRefresh={props.onNoteListChanged}134 />135 </div>136137 {}138 <Show when={activeNote() !== null || isNewNote()}>139 <div140 class="peach-notes-editor-wrapper"141 classList={{142 "is-visible": editorVisible(),143 "is-entering": phase() === "expanding",144 "is-leaving": phase() === "collapsing",145 }}146 >147 <NotesEditor148 note={activeNote()}149 onBack={handleEditorBack}150 onSaved={handleSaved}151 onDelete={handleDeleteNote}152 autoFocusTitle={isNewNote()}153 />154 </div>155 </Show>156 </div>157 );158}