Archiv-Wiki im Panel: vier Tabs, FTS5-Suche, strukturierbares Archiv
- Archiv-Home pro Projekt (archiveHome in ai-control.json); archive_panel mit Ordner, Beschreibung und Schlagwörtern (Frontmatter + Zeitstempel-Stem) - Archiv-Index (archive_index.rs): Scan mit Frontmatter, Wikilinks, Backlinks; Wiki-Seiten als strukturierte Daten (Übersicht/Tag-Seiten) - FTS5-Suche (archive_search.rs, rusqlite bundled): in-memory-Index pro Anfrage; MCP search_archive und Panel-Suchfeld (search_run) teilen die Engine - Panel: vier Puffer/Ansichten (Dokument, Befehle, Suche, Wiki) mit Tabs im Fenster-Header; gemeinsame Verdrahtung panel-wiring.ts, Kachel-CSS, Wiki-View mit Tag-Chips, Ordner-Sektionen und Backlinks - Archivieren speichert offene Bearbeitung (flush); Tab-Wechsel speichert statt zu blockieren; Fehler sichtbar als Panel-Toast - Abgelöstes Fenster: rahmenlos (Linux), eigene Kopfleiste mit Tabs und Fensterknöpfen, Theme-Kopplung über gemeinsames themes.ts - MCP-Tools show_commands, show_archive; Befehls-History mit Kachel-Löschen
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
/// Kachel-Ansicht der Archiv-Suchtreffer: rendert die Suchtreffer-Datei
|
||||
/// (search_archive im MCP-Server) als klickbare Kacheln — Klick reicht den
|
||||
/// relpath des Treffers an onOpen. Titel/Snippet/Pfad sind Fremdtext und
|
||||
/// gehen nie durch innerHTML; die `**…**`-Marker im Snippet werden per
|
||||
/// Split in <mark>-Elemente übersetzt.
|
||||
|
||||
interface Hit {
|
||||
relpath: string;
|
||||
title: string;
|
||||
snippet: string;
|
||||
}
|
||||
|
||||
interface SearchRun {
|
||||
query: string;
|
||||
tag?: string | null;
|
||||
home: string;
|
||||
hits: Hit[];
|
||||
}
|
||||
|
||||
export interface SearchView {
|
||||
set(text: string): void;
|
||||
empty(): boolean;
|
||||
}
|
||||
|
||||
export function initSearchView(
|
||||
container: HTMLElement,
|
||||
onOpen: (relpath: string) => void,
|
||||
onSearch: (query: string) => void,
|
||||
): SearchView {
|
||||
let count = 0;
|
||||
|
||||
// Suchfeld oben, Treffer darunter; das Feld bleibt über Updates hinweg stehen.
|
||||
const bar = document.createElement("div");
|
||||
bar.className = "hit-search";
|
||||
const input = document.createElement("input");
|
||||
input.type = "search";
|
||||
input.placeholder = "Archiv durchsuchen — Enter startet";
|
||||
input.addEventListener("keydown", (e) => {
|
||||
if (e.key === "Enter" && input.value.trim()) onSearch(input.value.trim());
|
||||
});
|
||||
bar.append(input);
|
||||
const results = document.createElement("div");
|
||||
container.append(bar, results);
|
||||
|
||||
function snippetEl(snippet: string): HTMLElement {
|
||||
const div = document.createElement("div");
|
||||
div.className = "hit-snippet";
|
||||
snippet.split("**").forEach((part, i) => {
|
||||
if (i % 2) {
|
||||
const m = document.createElement("mark");
|
||||
m.textContent = part;
|
||||
div.append(m);
|
||||
} else if (part) {
|
||||
div.append(document.createTextNode(part));
|
||||
}
|
||||
});
|
||||
return div;
|
||||
}
|
||||
|
||||
function render(run: SearchRun) {
|
||||
results.textContent = "";
|
||||
const head = document.createElement("div");
|
||||
head.className = "hit-head";
|
||||
const what = run.query.trim() ? `„${run.query}“` : "";
|
||||
const tag = run.tag ? `#${run.tag}` : "";
|
||||
head.textContent = `${run.hits.length} Treffer ${[what, tag].filter(Boolean).join(" · ")}`;
|
||||
results.append(head);
|
||||
for (const hit of run.hits) {
|
||||
const tile = document.createElement("div");
|
||||
tile.className = "hit-tile";
|
||||
const title = document.createElement("div");
|
||||
title.className = "hit-title";
|
||||
title.textContent = hit.title;
|
||||
const path = document.createElement("div");
|
||||
path.className = "hit-path";
|
||||
path.textContent = hit.relpath;
|
||||
tile.append(title, snippetEl(hit.snippet), path);
|
||||
tile.addEventListener("click", () => onOpen(hit.relpath));
|
||||
results.append(tile);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
set(text: string) {
|
||||
if (!text.trim()) {
|
||||
count = 0;
|
||||
results.textContent = "";
|
||||
return;
|
||||
}
|
||||
const run: SearchRun = JSON.parse(text);
|
||||
count = run.hits.length;
|
||||
if (document.activeElement !== input) input.value = run.query;
|
||||
render(run);
|
||||
},
|
||||
empty: () => count === 0,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user