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 =15 | "toc" 16 | "expanding" 17 | "editor" 18 | "collapsing"; 1920interface ExpansionOrigin {21 top: number; 22 title: string;23}242526const EDITOR_TITLE_TOP = 68;27const EXPAND_DURATION = 320;28const COLLAPSE_DURATION = 320;2930export function NotesMode(props: NotesModeProps) {31 let modeEl!: HTMLDivElement;32 let flyingTitleEl: HTMLDivElement | undefined;33 let tocWrapperEl: HTMLDivElement | undefined;34 let editorWrapperEl: HTMLDivElement | undefined;3536 const [phase, setPhase] = createSignal<Phase>("toc");37 const [activeNote, setActiveNote] = createSignal<VaultEntry | null>(null);38 const [isNewNote, setIsNewNote] = createSignal(false);39 const [origin, setOrigin] = createSignal<ExpansionOrigin | null>(null);40 const [flyingTitle, setFlyingTitle] = createSignal<string>("");41 const [flyingTop, setFlyingTop] = createSignal<number>(0);42 const [flyingFontSize, setFlyingFontSize] = createSignal<number>(18);43 const [flyingVisible, setFlyingVisible] = createSignal(false);4445 onMount(() => {46 47 if (props.initialNoteId) {48 const note = props.notes.find((n) => n.id === props.initialNoteId);49 if (note) {50 setActiveNote(note);51 setPhase("editor");52 }53 }54 });5556 function vibrate(ms: number) {57 try { navigator.vibrate?.(ms); } catch { }58 }5960 function handleNoteRowTap(note: VaultEntry, rowEl: HTMLElement) {61 if (phase() !== "toc") return;62 vibrate(12);6364 const rect = rowEl.getBoundingClientRect();65 const containerRect = modeEl.getBoundingClientRect();66 const rowTop = rect.top - containerRect.top;67 68 const titleTop = rowTop + (rect.height - 18) / 2;6970 setOrigin({ top: titleTop, title: note.title || "Untitled" });71 setFlyingTitle(note.title || "Untitled");72 setFlyingTop(titleTop);73 setFlyingFontSize(18);74 setFlyingVisible(true);75 setActiveNote(note);76 setIsNewNote(false);7778 79 setPhase("expanding");8081 82 requestAnimationFrame(() => {83 requestAnimationFrame(() => {84 setFlyingTop(EDITOR_TITLE_TOP);85 setFlyingFontSize(32);86 });87 });8889 90 setTimeout(() => {91 setPhase("editor");92 setFlyingVisible(false);93 }, EXPAND_DURATION);94 }9596 function handleNewNote() {97 if (phase() !== "toc") return;98 vibrate(12);99 setActiveNote(null);100 setIsNewNote(true);101 setOrigin(null);102 103 setPhase("expanding");104 requestAnimationFrame(() => {105 setTimeout(() => {106 setPhase("editor");107 }, 80);108 });109 }110111 function handleEditorBack() {112 if (phase() !== "editor") return;113 vibrate(12);114115 const orig = origin();116 if (orig) {117 118 setFlyingTitle(activeNote()?.title || "Untitled");119 setFlyingTop(EDITOR_TITLE_TOP);120 setFlyingFontSize(32);121 setFlyingVisible(true);122 setPhase("collapsing");123124 requestAnimationFrame(() => {125 requestAnimationFrame(() => {126 setFlyingTop(orig.top);127 setFlyingFontSize(18);128 });129 });130131 setTimeout(() => {132 setFlyingVisible(false);133 setActiveNote(null);134 setIsNewNote(false);135 setOrigin(null);136 setPhase("toc");137 }, COLLAPSE_DURATION);138 } else {139 140 setPhase("collapsing");141 setTimeout(() => {142 setActiveNote(null);143 setIsNewNote(false);144 setPhase("toc");145 }, 200);146 }147 }148149 function handleSaved(note: VaultEntry) {150 if (!activeNote() || activeNote()!.id !== note.id) {151 setActiveNote(note);152 }153 if (flyingTitle() !== note.title) {154 setFlyingTitle(note.title || "Untitled");155 }156 props.onNoteListChanged();157 }158159 function handleDeleteNote(note: VaultEntry) {160 void sendMessage({ type: "DELETE_NOTE", payload: { id: note.id } });161 props.onNoteListChanged();162 handleEditorBack();163 }164165 return (166 <div ref={modeEl} class="peach-notes-mode" data-phase={phase()}>167 {}168 <div169 ref={tocWrapperEl}170 class="peach-notes-toc-wrapper"171 classList={{172 "is-toc": phase() === "toc",173 "is-expanding": phase() === "expanding",174 "is-collapsing": phase() === "collapsing",175 }}176 aria-hidden={phase() !== "toc"}177 >178 <NotesTOC179 notes={props.notes}180 onBack={props.onBack}181 onOpenNote={handleNoteRowTap}182 onNewNote={handleNewNote}183 onRefresh={props.onNoteListChanged}184 expandingNoteId={phase() === "expanding" || phase() === "collapsing" ? (activeNote()?.id ?? null) : null}185 />186 </div>187188 {}189 <Show when={flyingVisible()}>190 <div191 ref={flyingTitleEl}192 class="peach-note-expanding-title"193 style={{194 top: `${flyingTop()}px`,195 transform: `scale(${flyingFontSize() / 32})`,196 }}197 aria-hidden="true"198 >199 {flyingTitle()}200 </div>201 </Show>202203 {}204 <Show when={activeNote() !== null || isNewNote()}>205 <div206 ref={editorWrapperEl}207 class="peach-notes-editor-wrapper"208 classList={{209 "is-visible": phase() === "editor",210 "is-entering": phase() === "expanding",211 "is-leaving": phase() === "collapsing",212 }}213 >214 <NotesEditor215 note={activeNote()}216 onBack={handleEditorBack}217 onSaved={handleSaved}218 onDelete={handleDeleteNote}219 autoFocusTitle={isNewNote()}220 />221 </div>222 </Show>223 </div>224 );225}