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 { 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          <button75            class="peach-notes-header-btn peach-notes-new-btn-icon"76            onClick={props.onNewNote}77            aria-label="New note"78            title="New note"79          >+</button>80          <button class="peach-notes-header-btn peach-notes-search-btn" onClick={showSearch} aria-label="Search">81            <svg width="15" height="15" viewBox="0 0 15 15" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round">82              <circle cx="6.5" cy="6.5" r="4.25" />83              <path d="M10 10L13 13" />84            </svg>85          </button>86        </Show>87      </div>8889      {/* Note list */}90      <div class="peach-notes-list-container">91        <Show92          when={filteredNotes().length > 0}93          fallback={94            <div class="peach-notes-empty">95              <p class="peach-notes-empty-text">96                {searchQuery() ? "No notes match." : "No notes yet.\nStart writing."}97              </p>98            </div>99          }100        >101          <For each={filteredNotes()}>102            {(note) => {103              // Permanent index from the unfiltered list — never changes during search104              const permanentIndex = () => props.notes.findIndex((n) => n.id === note.id);105              const expandingIdx = () => props.notes.findIndex((n) => n.id === props.expandingNoteId);106              const expanding = () => props.expandingNoteId && props.expandingNoteId !== note.id;107              const expandingNote = () => props.expandingNoteId === note.id;108              return (109                <NoteRow110                  note={note}111                  index={permanentIndex()}112                  searchQuery={searchQuery()}113                  onTap={(n, el) => props.onOpenNote(n, el)}114                  onDelete={handleDelete}115                  onArchive={handleArchive}116                  fadingClass={117                    expanding()118                      ? (permanentIndex() < expandingIdx()119                          ? "is-expanding-away is-expanding-away-up"120                          : "is-expanding-away is-expanding-away-down")121                      : expandingNote()122                        ? "is-expanding-target"123                        : undefined124                  }125                />126              );127            }}128          </For>129        </Show>130      </div>131    </div>132  );133}