Refinement-Runde Panel/Wiki: Bedienung, Themes, Archiv-Wahl, Frontend-Tests
- Slug transliteriert Umlaute/ß (fuer statt f-r); aktiver Tab mit Akzent + Unterstrich; kein aktiver Tab bei zugeklapptem Panel (mode.clear) - Suche: Live-Update auch beim Löschen (<3 Zeichen: Hinweis), #tag-Filter in der Query, Treffer öffnen im Dokument-Tab (panel_load, ohne Frontmatter); Wiki-Sprung-Button im Dokument; Wiki-Fehler sichtbar als Toast - Archiv-Formular am Archiv-Button (Ordner/Beschreibung/Schlagwörter); panel_archive_cmd nimmt Meta entgegen - Wiki-Übersicht: Zuletzt-Sektion, Backlink-Zähler, Wurzel-Eyebrow - Themes: Panel-Farben über CSS-Variablen, applyTheme leitet sie aus dem jeweiligen Theme ab (auch helle); gemeinsames themes.ts - Fenster: Doppelklick maximiert; gelöstes Fenster merkt Größe/Position (Window-ACL: set-position/outer-position/inner-size ergänzt); open_panel_window async (GTK-Deadlock-Vorgabe); Panel-Fenster spiegelt JS-Fehler ins Dev-Log - Archiv pro Projekt wähl-/abwählbar (Einstellungsdialog, setzt/entfernt Permissions); require_archive_home legt den Ordner an; ohne Archiv nur Befehle+Dokument im Panel - Frontend-Tests: vitest + happy-dom, 27 Tests über Modus-, Such-, Wiki-, Editor- und Formular-Logik (npm test)
This commit is contained in:
+26
-9
@@ -1,6 +1,6 @@
|
||||
/// 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
|
||||
/// absoluten Pfad 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.
|
||||
|
||||
@@ -24,7 +24,7 @@ export interface SearchView {
|
||||
|
||||
export function initSearchView(
|
||||
container: HTMLElement,
|
||||
onOpen: (relpath: string) => void,
|
||||
onOpen: (path: string) => void,
|
||||
onSearch: (query: string) => void,
|
||||
): SearchView {
|
||||
let count = 0;
|
||||
@@ -34,16 +34,30 @@ export function initSearchView(
|
||||
bar.className = "hit-search";
|
||||
const input = document.createElement("input");
|
||||
input.type = "search";
|
||||
input.placeholder = "Archiv durchsuchen";
|
||||
input.placeholder = "Archiv durchsuchen — #tag filtert";
|
||||
// Live-Suche ab 3 Zeichen, entprellt (300 ms Debounce); endet die Eingabe
|
||||
// mitten im Wort, wird das letzte Wort als Präfix gesucht (arch → arch*).
|
||||
// Enter sucht sofort und wörtlich — für exakte FTS-Syntax (Phrasen, OR/NOT).
|
||||
// mitten im Wort, wird das letzte Wort als Präfix gesucht (arch → arch*) —
|
||||
// außer es ist ein #tag-Token. Enter sucht sofort und wörtlich — für exakte
|
||||
// FTS-Syntax (Phrasen, OR/NOT).
|
||||
let pending: number | undefined;
|
||||
input.addEventListener("input", () => {
|
||||
clearTimeout(pending);
|
||||
const q = input.value.trim();
|
||||
if (q.length < 3) return;
|
||||
const live = /[\p{L}\p{N}]$/u.test(q) ? `${q}*` : q;
|
||||
if (q.length < 3) {
|
||||
// Beim Löschen unter die Schwelle: alte Treffer weg, kurzer Hinweis.
|
||||
count = 0;
|
||||
results.textContent = "";
|
||||
if (q) {
|
||||
const head = document.createElement("div");
|
||||
head.className = "hit-head";
|
||||
head.textContent = "Mindestens 3 Zeichen — oder Enter.";
|
||||
results.append(head);
|
||||
}
|
||||
return;
|
||||
}
|
||||
const last = q.split(/\s+/).pop()!;
|
||||
const live =
|
||||
!last.startsWith("#") && /[\p{L}\p{N}]$/u.test(q) ? `${q}*` : q;
|
||||
pending = window.setTimeout(() => onSearch(live), 300);
|
||||
});
|
||||
input.addEventListener("keydown", (e) => {
|
||||
@@ -77,7 +91,10 @@ export function initSearchView(
|
||||
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(" · ")}`;
|
||||
const scope = [what, tag].filter(Boolean).join(" · ");
|
||||
head.textContent = run.hits.length
|
||||
? `${run.hits.length} Treffer ${scope}`
|
||||
: `Keine Treffer für ${scope}`;
|
||||
results.append(head);
|
||||
for (const hit of run.hits) {
|
||||
const tile = document.createElement("div");
|
||||
@@ -89,7 +106,7 @@ export function initSearchView(
|
||||
path.className = "hit-path";
|
||||
path.textContent = hit.relpath;
|
||||
tile.append(title, snippetEl(hit.snippet), path);
|
||||
tile.addEventListener("click", () => onOpen(hit.relpath));
|
||||
tile.addEventListener("click", () => onOpen(`${run.home}/${hit.relpath}`));
|
||||
results.append(tile);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user