NotesTOC.tsx

3.6 KB · raw

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}1415export function NotesTOC(props: NotesTOCProps) {16  const [searchVisible, setSearchVisible] = createSignal(false);17  const [searchQuery, setSearchQuery] = createSignal("");1819  let searchInputRef!: HTMLInputElement;2021  function filteredNotes() {22    const q = searchQuery().trim().toLowerCase();23    if (!q) return props.notes;24    return props.notes.filter((n) =>25      (n.title ?? "").toLowerCase().includes(q) ||26      (n.notes ?? "").toLowerCase().includes(q)27    );28  }2930  function showSearch() {31    setSearchVisible(true);32    setTimeout(() => searchInputRef?.focus(), 50);33  }3435  function clearSearch() {36    setSearchQuery("");37    setSearchVisible(false);38  }3940  async function handleDelete(note: VaultEntry) {41    await sendMessage({ type: "DELETE_NOTE", payload: { id: note.id } });42    props.onRefresh();43  }4445  async function handleArchive(note: VaultEntry) {46    // Archive = soft delete with a tag — for now just delete to trash47    await sendMessage({ type: "DELETE_ENTRY", payload: { id: note.id } });48    props.onRefresh();49  }5051  return (52    <div class="peach-notes-toc">53      <NoteRunes mode="toc" />5455      {/* Header */}56      <div class="peach-notes-header">57        <Show58          when={!searchVisible()}59          fallback={60            <>61              <input62                ref={searchInputRef}63                class="peach-notes-search-input"64                placeholder="search notes…"65                value={searchQuery()}66                onInput={(e) => setSearchQuery((e.target as HTMLInputElement).value)}67                aria-label="Search notes"68              />69              <button class="peach-notes-header-btn" onClick={clearSearch} aria-label="Clear search">✕</button>70            </>71          }72        >73          <button class="peach-notes-back" onClick={props.onBack} aria-label="Back to vault">‹</button>74          <span class="peach-notes-header-title">notes.</span>75          <button class="peach-notes-header-btn" onClick={showSearch} aria-label="Search">76            <svg width="15" height="15" viewBox="0 0 15 15" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round">77              <circle cx="6.5" cy="6.5" r="4.25" />78              <path d="M10 10L13 13" />79            </svg>80          </button>81        </Show>82      </div>8384      {/* Note list */}85      <div class="peach-notes-list-container">86        <Show87          when={filteredNotes().length > 0}88          fallback={89            <div class="peach-notes-empty">90              <p class="peach-notes-empty-text">91                {searchQuery() ? "No notes match." : "No notes yet.\nStart writing."}92              </p>93            </div>94          }95        >96          <For each={filteredNotes()}>97            {(note, i) => (98              <NoteRow99                note={note}100                index={i()}101                searchQuery={searchQuery()}102                onTap={(n, el) => props.onOpenNote(n, el)}103                onDelete={handleDelete}104                onArchive={handleArchive}105              />106            )}107          </For>108        </Show>109110        <button class="peach-notes-new-btn" onClick={props.onNewNote}>111          + new note112        </button>113      </div>114    </div>115  );116}