NotesTOC.tsx

4.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  /** Note being expanded — other rows should fade away */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    // Archive = soft delete with a tag — for now just delete to trash49    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      {/* Header */}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      {/* Note list */}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) => {100              // Permanent index from the unfiltered list — never changes during search101              const permanentIndex = () => props.notes.findIndex((n) => n.id === note.id);102              const expandingIdx = () => props.notes.findIndex((n) => n.id === props.expandingNoteId);103              const expanding = () => props.expandingNoteId && props.expandingNoteId !== note.id;104              const expandingNote = () => props.expandingNoteId === note.id;105              return (106                <NoteRow107                  note={note}108                  index={permanentIndex()}109                  searchQuery={searchQuery()}110                  onTap={(n, el) => props.onOpenNote(n, el)}111                  onDelete={handleDelete}112                  onArchive={handleArchive}113                  fadingClass={114                    expanding()115                      ? (permanentIndex() < expandingIdx()116                          ? "is-expanding-away is-expanding-away-up"117                          : "is-expanding-away is-expanding-away-down")118                      : expandingNote()119                        ? "is-expanding-target"120                        : undefined121                  }122                />123              );124            }}125          </For>126        </Show>127128        <button class="peach-notes-new-btn" onClick={props.onNewNote}>129          + new note130        </button>131      </div>132    </div>133  );134}