NotesTOC.tsx

4.5 KB · raw

1import { createSignal, For, Show } from "solid-js";2import type { VaultEntry } from "../../shared/types";3import { sendMessage } from "../../shared/bridge";4import { NoteRow } from "../components/NoteRow";56interface NotesTOCProps {7  notes: VaultEntry[];8  onBack: () => void;9  onOpenNote: (note: VaultEntry, rowEl: HTMLElement) => void;10  onNewNote: () => void;11  onRefresh: () => void;12  /** Note being expanded — other rows should fade away */13  expandingNoteId?: string | null;14}1516export function NotesTOC(props: NotesTOCProps) {17  const [searchVisible, setSearchVisible] = createSignal(false);18  const [searchQuery, setSearchQuery] = createSignal("");1920  let searchInputRef!: HTMLInputElement;2122  function filteredNotes() {23    const q = searchQuery().trim().toLowerCase();24    if (!q) return props.notes;25    return props.notes.filter((n) =>26      (n.title ?? "").toLowerCase().includes(q) ||27      (n.notes ?? "").toLowerCase().includes(q)28    );29  }3031  function showSearch() {32    setSearchVisible(true);33    setTimeout(() => searchInputRef?.focus(), 50);34  }3536  function clearSearch() {37    setSearchQuery("");38    setSearchVisible(false);39  }4041  async function handleDelete(note: VaultEntry) {42    await sendMessage({ type: "DELETE_NOTE", payload: { id: note.id } });43    props.onRefresh();44  }4546  async function handleArchive(note: VaultEntry) {47    // Archive = soft delete with a tag — for now just delete to trash48    await sendMessage({ type: "DELETE_ENTRY", payload: { id: note.id } });49    props.onRefresh();50  }5152  return (53    <div class="peach-notes-toc">54      {/* Header */}55      <div class="peach-notes-header">56        <Show57          when={!searchVisible()}58          fallback={59            <>60              <input61                ref={searchInputRef}62                class="peach-notes-search-input"63                placeholder="search notes…"64                value={searchQuery()}65                onInput={(e) => setSearchQuery((e.target as HTMLInputElement).value)}66                aria-label="Search notes"67              />68              <button class="peach-notes-header-btn" onClick={clearSearch} aria-label="Clear search">✕</button>69            </>70          }71        >72          <button class="peach-notes-back" onClick={props.onBack} aria-label="Back to vault">‹</button>73          <span class="peach-notes-header-title">notes.</span>74          <button class="peach-notes-header-btn" onClick={showSearch} aria-label="Search">75            <svg width="15" height="15" viewBox="0 0 15 15" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round">76              <circle cx="6.5" cy="6.5" r="4.25" />77              <path d="M10 10L13 13" />78            </svg>79          </button>80        </Show>81      </div>8283      {/* Note list */}84      <div class="peach-notes-list-container">85        <Show86          when={filteredNotes().length > 0}87          fallback={88            <div class="peach-notes-empty">89              <p class="peach-notes-empty-text">90                {searchQuery() ? "No notes match." : "No notes yet.\nStart writing."}91              </p>92            </div>93          }94        >95          <For each={filteredNotes()}>96            {(note) => {97              // Permanent index from the unfiltered list — never changes during search98              const permanentIndex = () => props.notes.findIndex((n) => n.id === note.id);99              const expandingIdx = () => props.notes.findIndex((n) => n.id === props.expandingNoteId);100              const expanding = () => props.expandingNoteId && props.expandingNoteId !== note.id;101              const expandingNote = () => props.expandingNoteId === note.id;102              return (103                <NoteRow104                  note={note}105                  index={permanentIndex()}106                  searchQuery={searchQuery()}107                  onTap={(n, el) => props.onOpenNote(n, el)}108                  onDelete={handleDelete}109                  onArchive={handleArchive}110                  fadingClass={111                    expanding()112                      ? (permanentIndex() < expandingIdx()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}