NotesMode.tsx

4.4 KB · raw

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    // If an initialNoteId was passed (from global search), open it immediately38    if (props.initialNoteId) {39      const note = props.notes.find((n) => n.id === props.initialNoteId);40      if (note) {41        // Open without expansion animation — just direct to editor42        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    // New note expands from the bottom68    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    // RAF to let the flying title render at its start position82    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    // If this was a new note, now it has an id — update active note104    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      {/* TOC — always mounted, hidden during editor phase */}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      {/* Editor — mounted only when a note is active */}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}