1import { createSignal, For, Show } from "solid-js";2import type { VaultEntry } from "../../shared/types";3import { sendMessage } from "../../shared/bridge";4import { NoteRunes } from "../components/NoteRunes";5import { NoteRow } from "../components/NoteRow";67interface NotesTOCProps {8 notes: VaultEntry[];9 onBack: () => void;10 onOpenNote: (note: VaultEntry, rowEl: HTMLElement) => void;11 onNewNote: () => void;12 onRefresh: () => void;13 14 expandingNoteId?: string | null;15}1617export function NotesTOC(props: NotesTOCProps) {18 const [searchVisible, setSearchVisible] = createSignal(false);19 const [searchQuery, setSearchQuery] = createSignal("");2021 let searchInputRef!: HTMLInputElement;2223 function filteredNotes() {24 const q = searchQuery().trim().toLowerCase();25 if (!q) return props.notes;26 return props.notes.filter((n) =>27 (n.title ?? "").toLowerCase().includes(q) ||28 (n.notes ?? "").toLowerCase().includes(q)29 );30 }3132 function showSearch() {33 setSearchVisible(true);34 setTimeout(() => searchInputRef?.focus(), 50);35 }3637 function clearSearch() {38 setSearchQuery("");39 setSearchVisible(false);40 }4142 async function handleDelete(note: VaultEntry) {43 await sendMessage({ type: "DELETE_NOTE", payload: { id: note.id } });44 props.onRefresh();45 }4647 async function handleArchive(note: VaultEntry) {48 49 await sendMessage({ type: "DELETE_ENTRY", payload: { id: note.id } });50 props.onRefresh();51 }5253 return (54 <div class="peach-notes-toc">55 <NoteRunes mode="toc" />5657 {}58 <div class="peach-notes-header">59 <Show60 when={!searchVisible()}61 fallback={62 <>63 <input64 ref={searchInputRef}65 class="peach-notes-search-input"66 placeholder="search notes…"67 value={searchQuery()}68 onInput={(e) => setSearchQuery((e.target as HTMLInputElement).value)}69 aria-label="Search notes"70 />71 <button class="peach-notes-header-btn" onClick={clearSearch} aria-label="Clear search">✕</button>72 </>73 }74 >75 <button class="peach-notes-back" onClick={props.onBack} aria-label="Back to vault">‹</button>76 <span class="peach-notes-header-title">notes.</span>77 <button class="peach-notes-header-btn" onClick={showSearch} aria-label="Search">78 <svg width="15" height="15" viewBox="0 0 15 15" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round">79 <circle cx="6.5" cy="6.5" r="4.25" />80 <path d="M10 10L13 13" />81 </svg>82 </button>83 </Show>84 </div>8586 {}87 <div class="peach-notes-list-container">88 <Show89 when={filteredNotes().length > 0}90 fallback={91 <div class="peach-notes-empty">92 <p class="peach-notes-empty-text">93 {searchQuery() ? "No notes match." : "No notes yet.\nStart writing."}94 </p>95 </div>96 }97 >98 <For each={filteredNotes()}>99 {(note, i) => {100 const expanding = () => props.expandingNoteId && props.expandingNoteId !== note.id;101 const expandingNote = () => props.expandingNoteId === note.id;102 return (103 <NoteRow104 note={note}105 index={i()}106 searchQuery={searchQuery()}107 onTap={(n, el) => props.onOpenNote(n, el)}108 onDelete={handleDelete}109 onArchive={handleArchive}110 fadingClass={111 expanding()112 ? (i() < (filteredNotes().findIndex(n => n.id === props.expandingNoteId))113 ? "is-expanding-away is-expanding-away-up"114 : "is-expanding-away is-expanding-away-down")115 : expandingNote()116 ? "is-expanding-target"117 : undefined118 }119 />120 );121 }}122 </For>123 </Show>124125 <button class="peach-notes-new-btn" onClick={props.onNewNote}>126 + new note127 </button>128 </div>129 </div>130 );131}