2026-03-07 08:40:16 -08:00
|
|
|
import { useState, useEffect, useRef, useMemo } from "react";
|
|
|
|
|
import { useNavigate } from "react-router-dom";
|
|
|
|
|
import { useVault } from "../App";
|
feat: v0.3 + v0.4 — frontmatter, templates, outline panel, vault cache
v0.3: Core file reading & markdown infrastructure
- Rust: read_note_with_meta, list_templates, save_attachment commands
- TypeScript: frontmatter.ts (parse/serialize YAML, extract headings)
- OutlinePanel with click-to-scroll headings + tabbed right panel
- CommandPalette: New from Template with {{title}}/{{date}} replacement
- Editor: image drag-and-drop to attachments/
- 130 lines of CSS for outline panel and right panel tabs
v0.4: File reading & caching
- Rust: VaultCache (cache.rs) with mtime-based invalidation
- Rewrote read_note, read_note_with_meta, build_graph, search_vault to use cache
- init_vault_cache (eager scan on startup), get_cache_stats commands
- Frontend LRU noteCache (capacity 20, stale-while-revalidate)
- notify crate added for filesystem watching foundation
2026-03-07 09:54:08 -08:00
|
|
|
import { listTemplates, readNote, writeNote, type NoteEntry } from "../lib/commands";
|
2026-03-07 08:40:16 -08:00
|
|
|
|
|
|
|
|
interface Command {
|
|
|
|
|
id: string;
|
|
|
|
|
label: string;
|
|
|
|
|
icon: string;
|
|
|
|
|
action: () => void;
|
feat: v0.3 + v0.4 — frontmatter, templates, outline panel, vault cache
v0.3: Core file reading & markdown infrastructure
- Rust: read_note_with_meta, list_templates, save_attachment commands
- TypeScript: frontmatter.ts (parse/serialize YAML, extract headings)
- OutlinePanel with click-to-scroll headings + tabbed right panel
- CommandPalette: New from Template with {{title}}/{{date}} replacement
- Editor: image drag-and-drop to attachments/
- 130 lines of CSS for outline panel and right panel tabs
v0.4: File reading & caching
- Rust: VaultCache (cache.rs) with mtime-based invalidation
- Rewrote read_note, read_note_with_meta, build_graph, search_vault to use cache
- init_vault_cache (eager scan on startup), get_cache_stats commands
- Frontend LRU noteCache (capacity 20, stale-while-revalidate)
- notify crate added for filesystem watching foundation
2026-03-07 09:54:08 -08:00
|
|
|
section: "notes" | "commands" | "templates";
|
2026-03-07 08:40:16 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function CommandPalette({ open, onClose }: { open: boolean; onClose: () => void }) {
|
|
|
|
|
const [query, setQuery] = useState("");
|
|
|
|
|
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
|
|
|
const inputRef = useRef<HTMLInputElement>(null);
|
|
|
|
|
const listRef = useRef<HTMLDivElement>(null);
|
|
|
|
|
const navigate = useNavigate();
|
|
|
|
|
const { notes, vaultPath, refreshNotes } = useVault();
|
feat: v0.3 + v0.4 — frontmatter, templates, outline panel, vault cache
v0.3: Core file reading & markdown infrastructure
- Rust: read_note_with_meta, list_templates, save_attachment commands
- TypeScript: frontmatter.ts (parse/serialize YAML, extract headings)
- OutlinePanel with click-to-scroll headings + tabbed right panel
- CommandPalette: New from Template with {{title}}/{{date}} replacement
- Editor: image drag-and-drop to attachments/
- 130 lines of CSS for outline panel and right panel tabs
v0.4: File reading & caching
- Rust: VaultCache (cache.rs) with mtime-based invalidation
- Rewrote read_note, read_note_with_meta, build_graph, search_vault to use cache
- init_vault_cache (eager scan on startup), get_cache_stats commands
- Frontend LRU noteCache (capacity 20, stale-while-revalidate)
- notify crate added for filesystem watching foundation
2026-03-07 09:54:08 -08:00
|
|
|
const [templates, setTemplates] = useState<string[]>([]);
|
2026-03-07 08:40:16 -08:00
|
|
|
|
|
|
|
|
// Flatten notes for search
|
|
|
|
|
const allNotes = useMemo(() => flattenEntries(notes), [notes]);
|
|
|
|
|
|
|
|
|
|
// Build command list
|
|
|
|
|
const commands: Command[] = useMemo(() => {
|
|
|
|
|
const cmds: Command[] = [
|
|
|
|
|
{ id: "cmd-new", label: "New Note", icon: "✏️", section: "commands", action: () => {
|
|
|
|
|
onClose();
|
|
|
|
|
const name = prompt("Note name:");
|
|
|
|
|
if (name?.trim()) {
|
|
|
|
|
import("../lib/commands").then(({ writeNote }) => {
|
|
|
|
|
writeNote(vaultPath, `${name.trim()}.md`, `# ${name.trim()}\n\n`).then(() => {
|
|
|
|
|
refreshNotes();
|
|
|
|
|
navigate(`/note/${encodeURIComponent(`${name.trim()}.md`)}`);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}},
|
|
|
|
|
{ id: "cmd-daily", label: "Open Daily Note", icon: "📅", section: "commands", action: () => { onClose(); navigate("/daily"); }},
|
|
|
|
|
{ id: "cmd-graph", label: "Open Graph View", icon: "🔮", section: "commands", action: () => { onClose(); navigate("/graph"); }},
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
// Add notes as commands
|
|
|
|
|
for (const note of allNotes) {
|
|
|
|
|
cmds.push({
|
|
|
|
|
id: `note-${note.path}`,
|
|
|
|
|
label: note.name,
|
|
|
|
|
icon: "📄",
|
|
|
|
|
section: "notes",
|
|
|
|
|
action: () => { onClose(); navigate(`/note/${encodeURIComponent(note.path)}`); },
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
feat: v0.3 + v0.4 — frontmatter, templates, outline panel, vault cache
v0.3: Core file reading & markdown infrastructure
- Rust: read_note_with_meta, list_templates, save_attachment commands
- TypeScript: frontmatter.ts (parse/serialize YAML, extract headings)
- OutlinePanel with click-to-scroll headings + tabbed right panel
- CommandPalette: New from Template with {{title}}/{{date}} replacement
- Editor: image drag-and-drop to attachments/
- 130 lines of CSS for outline panel and right panel tabs
v0.4: File reading & caching
- Rust: VaultCache (cache.rs) with mtime-based invalidation
- Rewrote read_note, read_note_with_meta, build_graph, search_vault to use cache
- init_vault_cache (eager scan on startup), get_cache_stats commands
- Frontend LRU noteCache (capacity 20, stale-while-revalidate)
- notify crate added for filesystem watching foundation
2026-03-07 09:54:08 -08:00
|
|
|
// Add template commands
|
|
|
|
|
for (const tmpl of templates) {
|
|
|
|
|
cmds.push({
|
|
|
|
|
id: `tmpl-${tmpl}`,
|
|
|
|
|
label: `New from: ${tmpl}`,
|
|
|
|
|
icon: "📋",
|
|
|
|
|
section: "templates",
|
|
|
|
|
action: () => {
|
|
|
|
|
onClose();
|
|
|
|
|
const name = prompt(`Note name (from ${tmpl} template):`);
|
|
|
|
|
if (name?.trim() && vaultPath) {
|
|
|
|
|
readNote(vaultPath, `_templates/${tmpl}.md`).then((content) => {
|
|
|
|
|
const today = new Date().toISOString().split("T")[0];
|
|
|
|
|
const filled = content
|
|
|
|
|
.replace(/\{\{title\}\}/gi, name.trim())
|
|
|
|
|
.replace(/\{\{date\}\}/gi, today);
|
|
|
|
|
writeNote(vaultPath, `${name.trim()}.md`, filled).then(() => {
|
|
|
|
|
refreshNotes();
|
|
|
|
|
navigate(`/note/${encodeURIComponent(`${name.trim()}.md`)}`);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-07 08:40:16 -08:00
|
|
|
return cmds;
|
feat: v0.3 + v0.4 — frontmatter, templates, outline panel, vault cache
v0.3: Core file reading & markdown infrastructure
- Rust: read_note_with_meta, list_templates, save_attachment commands
- TypeScript: frontmatter.ts (parse/serialize YAML, extract headings)
- OutlinePanel with click-to-scroll headings + tabbed right panel
- CommandPalette: New from Template with {{title}}/{{date}} replacement
- Editor: image drag-and-drop to attachments/
- 130 lines of CSS for outline panel and right panel tabs
v0.4: File reading & caching
- Rust: VaultCache (cache.rs) with mtime-based invalidation
- Rewrote read_note, read_note_with_meta, build_graph, search_vault to use cache
- init_vault_cache (eager scan on startup), get_cache_stats commands
- Frontend LRU noteCache (capacity 20, stale-while-revalidate)
- notify crate added for filesystem watching foundation
2026-03-07 09:54:08 -08:00
|
|
|
}, [allNotes, templates, navigate, onClose, vaultPath, refreshNotes]);
|
2026-03-07 08:40:16 -08:00
|
|
|
|
|
|
|
|
// Filter by query
|
|
|
|
|
const filtered = useMemo(() => {
|
|
|
|
|
if (!query.trim()) return commands;
|
|
|
|
|
const q = query.toLowerCase();
|
|
|
|
|
return commands.filter((c) => c.label.toLowerCase().includes(q));
|
|
|
|
|
}, [commands, query]);
|
|
|
|
|
|
|
|
|
|
// Reset selection on filter change
|
|
|
|
|
useEffect(() => { setSelectedIndex(0); }, [filtered]);
|
|
|
|
|
|
|
|
|
|
// Focus input on open
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (open) {
|
|
|
|
|
setQuery("");
|
|
|
|
|
setSelectedIndex(0);
|
|
|
|
|
setTimeout(() => inputRef.current?.focus(), 50);
|
feat: v0.3 + v0.4 — frontmatter, templates, outline panel, vault cache
v0.3: Core file reading & markdown infrastructure
- Rust: read_note_with_meta, list_templates, save_attachment commands
- TypeScript: frontmatter.ts (parse/serialize YAML, extract headings)
- OutlinePanel with click-to-scroll headings + tabbed right panel
- CommandPalette: New from Template with {{title}}/{{date}} replacement
- Editor: image drag-and-drop to attachments/
- 130 lines of CSS for outline panel and right panel tabs
v0.4: File reading & caching
- Rust: VaultCache (cache.rs) with mtime-based invalidation
- Rewrote read_note, read_note_with_meta, build_graph, search_vault to use cache
- init_vault_cache (eager scan on startup), get_cache_stats commands
- Frontend LRU noteCache (capacity 20, stale-while-revalidate)
- notify crate added for filesystem watching foundation
2026-03-07 09:54:08 -08:00
|
|
|
// Load templates
|
|
|
|
|
if (vaultPath) {
|
|
|
|
|
listTemplates(vaultPath).then(setTemplates).catch(() => setTemplates([]));
|
|
|
|
|
}
|
2026-03-07 08:40:16 -08:00
|
|
|
}
|
feat: v0.3 + v0.4 — frontmatter, templates, outline panel, vault cache
v0.3: Core file reading & markdown infrastructure
- Rust: read_note_with_meta, list_templates, save_attachment commands
- TypeScript: frontmatter.ts (parse/serialize YAML, extract headings)
- OutlinePanel with click-to-scroll headings + tabbed right panel
- CommandPalette: New from Template with {{title}}/{{date}} replacement
- Editor: image drag-and-drop to attachments/
- 130 lines of CSS for outline panel and right panel tabs
v0.4: File reading & caching
- Rust: VaultCache (cache.rs) with mtime-based invalidation
- Rewrote read_note, read_note_with_meta, build_graph, search_vault to use cache
- init_vault_cache (eager scan on startup), get_cache_stats commands
- Frontend LRU noteCache (capacity 20, stale-while-revalidate)
- notify crate added for filesystem watching foundation
2026-03-07 09:54:08 -08:00
|
|
|
}, [open, vaultPath]);
|
2026-03-07 08:40:16 -08:00
|
|
|
|
|
|
|
|
// Scroll selected item into view
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (listRef.current) {
|
|
|
|
|
const item = listRef.current.children[selectedIndex] as HTMLElement;
|
|
|
|
|
item?.scrollIntoView({ block: "nearest" });
|
|
|
|
|
}
|
|
|
|
|
}, [selectedIndex]);
|
|
|
|
|
|
|
|
|
|
const handleKeyDown = (e: React.KeyboardEvent) => {
|
|
|
|
|
if (e.key === "ArrowDown") {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
setSelectedIndex((i) => Math.min(i + 1, filtered.length - 1));
|
|
|
|
|
} else if (e.key === "ArrowUp") {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
setSelectedIndex((i) => Math.max(i - 1, 0));
|
|
|
|
|
} else if (e.key === "Enter") {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
filtered[selectedIndex]?.action();
|
|
|
|
|
} else if (e.key === "Escape") {
|
|
|
|
|
onClose();
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (!open) return null;
|
|
|
|
|
|
|
|
|
|
// Group by section
|
|
|
|
|
const noteResults = filtered.filter((c) => c.section === "notes");
|
|
|
|
|
const cmdResults = filtered.filter((c) => c.section === "commands");
|
feat: v0.3 + v0.4 — frontmatter, templates, outline panel, vault cache
v0.3: Core file reading & markdown infrastructure
- Rust: read_note_with_meta, list_templates, save_attachment commands
- TypeScript: frontmatter.ts (parse/serialize YAML, extract headings)
- OutlinePanel with click-to-scroll headings + tabbed right panel
- CommandPalette: New from Template with {{title}}/{{date}} replacement
- Editor: image drag-and-drop to attachments/
- 130 lines of CSS for outline panel and right panel tabs
v0.4: File reading & caching
- Rust: VaultCache (cache.rs) with mtime-based invalidation
- Rewrote read_note, read_note_with_meta, build_graph, search_vault to use cache
- init_vault_cache (eager scan on startup), get_cache_stats commands
- Frontend LRU noteCache (capacity 20, stale-while-revalidate)
- notify crate added for filesystem watching foundation
2026-03-07 09:54:08 -08:00
|
|
|
const tmplResults = filtered.filter((c) => c.section === "templates");
|
|
|
|
|
const ordered = [...cmdResults, ...tmplResults, ...noteResults];
|
2026-03-07 08:40:16 -08:00
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="palette-overlay" onClick={onClose}>
|
|
|
|
|
<div className="palette" onClick={(e) => e.stopPropagation()} onKeyDown={handleKeyDown}>
|
|
|
|
|
<div className="palette-input-wrap">
|
|
|
|
|
<svg className="palette-search-icon" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
|
|
|
<circle cx="11" cy="11" r="8" />
|
|
|
|
|
<path d="m21 21-4.35-4.35" />
|
|
|
|
|
</svg>
|
|
|
|
|
<input
|
|
|
|
|
ref={inputRef}
|
|
|
|
|
type="text"
|
|
|
|
|
className="palette-input"
|
|
|
|
|
placeholder="Search notes & commands..."
|
|
|
|
|
value={query}
|
|
|
|
|
onChange={(e) => setQuery(e.target.value)}
|
|
|
|
|
/>
|
|
|
|
|
<kbd className="palette-kbd">esc</kbd>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="palette-list" ref={listRef}>
|
|
|
|
|
{ordered.length === 0 && (
|
|
|
|
|
<div className="palette-empty">No results found</div>
|
|
|
|
|
)}
|
|
|
|
|
{cmdResults.length > 0 && (
|
|
|
|
|
<div className="palette-section-label">Commands</div>
|
|
|
|
|
)}
|
|
|
|
|
{cmdResults.map((cmd) => {
|
|
|
|
|
const globalIdx = ordered.indexOf(cmd);
|
|
|
|
|
return (
|
|
|
|
|
<button
|
|
|
|
|
key={cmd.id}
|
|
|
|
|
className={`palette-item ${globalIdx === selectedIndex ? "selected" : ""}`}
|
|
|
|
|
onClick={cmd.action}
|
|
|
|
|
onMouseEnter={() => setSelectedIndex(globalIdx)}
|
|
|
|
|
>
|
|
|
|
|
<span className="palette-item-icon">{cmd.icon}</span>
|
|
|
|
|
<span className="palette-item-label">{cmd.label}</span>
|
|
|
|
|
</button>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
{noteResults.length > 0 && (
|
|
|
|
|
<div className="palette-section-label">Notes</div>
|
|
|
|
|
)}
|
feat: v0.3 + v0.4 — frontmatter, templates, outline panel, vault cache
v0.3: Core file reading & markdown infrastructure
- Rust: read_note_with_meta, list_templates, save_attachment commands
- TypeScript: frontmatter.ts (parse/serialize YAML, extract headings)
- OutlinePanel with click-to-scroll headings + tabbed right panel
- CommandPalette: New from Template with {{title}}/{{date}} replacement
- Editor: image drag-and-drop to attachments/
- 130 lines of CSS for outline panel and right panel tabs
v0.4: File reading & caching
- Rust: VaultCache (cache.rs) with mtime-based invalidation
- Rewrote read_note, read_note_with_meta, build_graph, search_vault to use cache
- init_vault_cache (eager scan on startup), get_cache_stats commands
- Frontend LRU noteCache (capacity 20, stale-while-revalidate)
- notify crate added for filesystem watching foundation
2026-03-07 09:54:08 -08:00
|
|
|
{tmplResults.length > 0 && (
|
|
|
|
|
<div className="palette-section-label">Templates</div>
|
|
|
|
|
)}
|
|
|
|
|
{tmplResults.map((cmd) => {
|
|
|
|
|
const globalIdx = ordered.indexOf(cmd);
|
|
|
|
|
return (
|
|
|
|
|
<button
|
|
|
|
|
key={cmd.id}
|
|
|
|
|
className={`palette-item ${globalIdx === selectedIndex ? "selected" : ""}`}
|
|
|
|
|
onClick={cmd.action}
|
|
|
|
|
onMouseEnter={() => setSelectedIndex(globalIdx)}
|
|
|
|
|
>
|
|
|
|
|
<span className="palette-item-icon">{cmd.icon}</span>
|
|
|
|
|
<span className="palette-item-label">{cmd.label}</span>
|
|
|
|
|
</button>
|
|
|
|
|
);
|
|
|
|
|
})}
|
2026-03-07 08:40:16 -08:00
|
|
|
{noteResults.map((cmd) => {
|
|
|
|
|
const globalIdx = ordered.indexOf(cmd);
|
|
|
|
|
return (
|
|
|
|
|
<button
|
|
|
|
|
key={cmd.id}
|
|
|
|
|
className={`palette-item ${globalIdx === selectedIndex ? "selected" : ""}`}
|
|
|
|
|
onClick={cmd.action}
|
|
|
|
|
onMouseEnter={() => setSelectedIndex(globalIdx)}
|
|
|
|
|
>
|
|
|
|
|
<span className="palette-item-icon">{cmd.icon}</span>
|
|
|
|
|
<span className="palette-item-label">{cmd.label}</span>
|
|
|
|
|
</button>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="palette-footer">
|
|
|
|
|
<span>↑↓ navigate</span>
|
|
|
|
|
<span>↵ open</span>
|
|
|
|
|
<span>esc close</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function flattenEntries(entries: NoteEntry[]): { name: string; path: string }[] {
|
|
|
|
|
const result: { name: string; path: string }[] = [];
|
|
|
|
|
for (const e of entries) {
|
|
|
|
|
if (e.is_dir && e.children) {
|
|
|
|
|
result.push(...flattenEntries(e.children));
|
|
|
|
|
} else if (!e.is_dir) {
|
|
|
|
|
result.push({ name: e.name, path: e.path });
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return result;
|
|
|
|
|
}
|