// Press ⌘K (Ctrl+K) anywhere: type a passage ("john 3:16") to go straight to it, or a few letters to // find a study, a note you wrote, a bookmark or highlight, a page, or to search the Bible's text. import { useEffect, useMemo, useRef, useState } from 'react'; import { useApp } from '../context/AppContext.js'; import { themeVars } from './reader/readerThemes.js'; import { SearchIcon } from './reader/icons.jsx'; import { formatPassage, parsePassageRef } from '../utils/passageRef.js'; import { searchMarks, searchNotes, searchProjects } from '../utils/personalSearch.js'; import { loadProjectIndex } from '../utils/bibleUtils.jsx'; import { loadProjectById } from '../utils/projectStorage.js'; export default function CommandPalette({ onClose }) { const { readerTheme, authUser, authServerDown, projectIndex, readerBookmarks, readerTextHighlights, readerColorLabels, jumpToReaderVerse, setCurrentPage, setProject, resumeProject, openNewProject, goStudies, openReadingPlans, openBibleReader, requestReaderSearch, openBackup, } = useApp(); const [query, setQuery] = useState(''); const [active, setActive] = useState(0); const listRef = useRef(null); const inputRef = useRef(null); const opener = useRef(document.activeElement); useEffect(() => { inputRef.current?.focus(); const from = opener.current; return () => { if (from instanceof HTMLElement && document.contains(from)) from.focus({ preventScroll: true }); }; }, []); const items = useMemo(() => { const out = []; const q = query.trim(); const go = (fn) => () => { onClose(); fn(); }; // 1. A passage typed in for (const r of parsePassageRef(q)) { out.push({ group: 'Go to', label: formatPassage(r), hint: 'Open in the Bible', run: go(() => { jumpToReaderVerse(r.book, r.chapter, r.verse ?? 1); openBibleReader(); }) }); } // 2. Pages and actions const pages = [ ['Studies', 'My Studies', goStudies], ['Read Bible', 'The Bible reader', openBibleReader], ['Reading Plans', 'Plans and today’s reading', openReadingPlans], ['New project', 'Start a Bible study', openNewProject], ['Settings', 'Account and preferences', () => setCurrentPage('settings')], ['Back up my data', 'Download everything as a file', openBackup], ]; const lower = q.toLowerCase(); for (const [label, hint, fn] of pages) { if (!lower || label.toLowerCase().includes(lower)) out.push({ group: lower ? 'Pages' : 'Go to', label, hint, run: go(fn) }); } if (q.length >= 2) { // 3. Studies for (const e of searchProjects(projectIndex, q)) { out.push({ group: 'Studies', label: e.title, hint: e.chapterSummary, run: go(() => resumeProject(e.id)) }); } // 4. Notes you wrote const projects = loadProjectIndex().map((e) => loadProjectById(e.id)).filter(Boolean); for (const h of searchNotes(projects, q)) { out.push({ group: 'Your notes', label: `${h.ref} — ${h.projectTitle}`, hint: h.snippet, run: go(() => { const full = loadProjectById(h.projectId); if (!full) return; setProject({ ...full, selectedChunkId: h.chunkId }); setCurrentPage('study'); }), }); } // 5. Bookmarks and highlights for (const m of searchMarks({ bookmarks: readerBookmarks, highlights: readerTextHighlights, labels: readerColorLabels ?? {} }, q)) { out.push({ group: 'Bookmarks & highlights', label: `${m.bookName} ${m.chapter}:${m.verse}`, color: m.color, hint: m.kind === 'highlight' ? `${m.label ? `${m.label}: ` : ''}${m.text ?? 'Highlight'}` : 'Bookmark', run: go(() => { jumpToReaderVerse(m.book, m.chapter, m.verse); openBibleReader(); }), }); } // 6. The Bible's own text out.push({ group: 'Search', label: `Search the Bible for “${q}”`, hint: 'Whole-Bible search in the reader', run: go(() => { requestReaderSearch(q); openBibleReader(); }) }); } return out; }, [query]); // eslint-disable-line react-hooks/exhaustive-deps useEffect(() => { setActive(0); }, [query]); useEffect(() => { listRef.current?.querySelector('[aria-selected="true"]')?.scrollIntoView?.({ block: 'nearest' }); }, [active]); const onKeyDown = (e) => { if (e.key === 'ArrowDown') { e.preventDefault(); setActive((a) => Math.min(a + 1, items.length - 1)); } else if (e.key === 'ArrowUp') { e.preventDefault(); setActive((a) => Math.max(a - 1, 0)); } else if (e.key === 'Enter') { e.preventDefault(); items[active]?.run(); } else if (e.key === 'Escape') { e.preventDefault(); e.stopPropagation(); onClose(); } else if (e.key === 'Tab') { e.preventDefault(); } // the box and the list are one control }; if (authUser === null && !authServerDown) return null; // the sign-in screen has nothing to search let lastGroup = null; return (
{ if (e.target === e.currentTarget) onClose(); }}>
setQuery(e.target.value)} onKeyDown={onKeyDown} role="combobox" aria-expanded="true" aria-controls="palette-list" aria-autocomplete="list" aria-activedescendant={items[active] ? `palette-item-${active}` : undefined} placeholder="Go to a passage, or search your studies and notes…" className="min-w-0 flex-1 bg-transparent text-base outline-none placeholder:text-[var(--r-sub)]" /> esc

↑↓ to move · Enter to open · Esc to close

); }