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:
+58
-8
@@ -1,8 +1,13 @@
|
||||
import { marked } from "marked";
|
||||
import { writeText } from "@tauri-apps/plugin-clipboard-manager";
|
||||
import { flash } from "./commands-view";
|
||||
|
||||
export interface PanelView {
|
||||
set(text: string): void;
|
||||
raw(): string;
|
||||
/// Schreibt eine offene Inhalts-Bearbeitung zurück (und beendet sie);
|
||||
/// aufgelöst, sobald der Entwurf gespeichert ist.
|
||||
flush(): Promise<void>;
|
||||
}
|
||||
|
||||
/// Verkabelt einen Panel-Inhaltsbereich: MD/Roh-Umschalter und Copy-Button.
|
||||
@@ -22,7 +27,7 @@ function stripInlineMd(s: string): string {
|
||||
}
|
||||
|
||||
/// Titel für den Panel-Kopf: erste Überschrift (# …) oder sonst erste
|
||||
/// nicht-leere Zeile, ohne Inline-Markdown; „Entwurf" bei leerem Text.
|
||||
/// nicht-leere Zeile, ohne Inline-Markdown; „Dokument" bei leerem Text.
|
||||
function firstLine(text: string): string {
|
||||
let fallback = "";
|
||||
for (const line of text.split("\n")) {
|
||||
@@ -32,7 +37,7 @@ function firstLine(text: string): string {
|
||||
if (t.startsWith("#") && h) return stripInlineMd(h);
|
||||
if (!fallback) fallback = t;
|
||||
}
|
||||
return fallback ? stripInlineMd(fallback) : "Entwurf";
|
||||
return fallback ? stripInlineMd(fallback) : "Dokument";
|
||||
}
|
||||
|
||||
/// Ersetzt die erste Überschrift (bzw. legt eine an) im Rohtext durch `title`.
|
||||
@@ -52,6 +57,45 @@ function setHeading(text: string, title: string): string {
|
||||
return `# ${title}\n`;
|
||||
}
|
||||
|
||||
/// Macht `[[name]]`-Wikilinks im gerendertem Markdown klickbar. Läuft über die
|
||||
/// Textknoten des DOM statt über den Rohtext, damit Vorkommen in Code-Spans
|
||||
/// und Code-Blöcken (z. B. bash `[[ -f x ]]`) unangetastet bleiben.
|
||||
export function linkWikiRefs(root: HTMLElement, onClick: (name: string) => void) {
|
||||
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
|
||||
const nodes: Text[] = [];
|
||||
for (let n = walker.nextNode(); n; n = walker.nextNode()) {
|
||||
const t = n as Text;
|
||||
if (!t.textContent?.includes("[[")) continue;
|
||||
if (t.parentElement?.closest("code, pre, a")) continue;
|
||||
nodes.push(t);
|
||||
}
|
||||
for (const node of nodes) {
|
||||
const parts = node.textContent!.split(/\[\[([^\]]+)\]\]/g);
|
||||
if (parts.length < 3) continue;
|
||||
const frag = document.createDocumentFragment();
|
||||
parts.forEach((part, i) => {
|
||||
if (i % 2) {
|
||||
// `[[ziel]]` oder `[[ziel|label]]`.
|
||||
const sep = part.indexOf("|");
|
||||
const target = sep < 0 ? part : part.slice(0, sep);
|
||||
const label = sep < 0 ? part : part.slice(sep + 1);
|
||||
const a = document.createElement("a");
|
||||
a.href = "#";
|
||||
a.className = "wiki";
|
||||
a.textContent = label;
|
||||
a.addEventListener("click", (e) => {
|
||||
e.preventDefault();
|
||||
onClick(target);
|
||||
});
|
||||
frag.append(a);
|
||||
} else if (part) {
|
||||
frag.append(part);
|
||||
}
|
||||
});
|
||||
node.replaceWith(frag);
|
||||
}
|
||||
}
|
||||
|
||||
export function initPanelView(opts: {
|
||||
content: HTMLElement;
|
||||
copyBtn: HTMLElement;
|
||||
@@ -64,7 +108,9 @@ export function initPanelView(opts: {
|
||||
defaultLang?: string;
|
||||
/// Wird mit dem neuen Rohtext aufgerufen, wenn Titel oder Inhalt geändert
|
||||
/// wurden.
|
||||
onCommit?: (text: string) => void;
|
||||
onCommit?: (text: string) => void | Promise<void>;
|
||||
/// Klick auf einen `[[name]]`-Wikilink im gerenderten Markdown.
|
||||
onWikiLink?: (name: string) => void;
|
||||
}): PanelView {
|
||||
let rawText = "";
|
||||
let rendered = true;
|
||||
@@ -72,11 +118,13 @@ export function initPanelView(opts: {
|
||||
// gepuffert statt angewandt.
|
||||
let editing = false;
|
||||
let pending: string | null = null;
|
||||
let flushEditor: () => Promise<void> = async () => {};
|
||||
|
||||
function draw() {
|
||||
if (rendered) {
|
||||
opts.content.className = "md";
|
||||
opts.content.innerHTML = marked.parse(rawText, { async: false });
|
||||
if (opts.onWikiLink) linkWikiRefs(opts.content, opts.onWikiLink);
|
||||
} else {
|
||||
opts.content.className = "raw";
|
||||
opts.content.textContent = rawText;
|
||||
@@ -90,9 +138,8 @@ export function initPanelView(opts: {
|
||||
});
|
||||
|
||||
opts.copyBtn.addEventListener("click", async () => {
|
||||
await navigator.clipboard.writeText(rawText);
|
||||
opts.copyBtn.classList.add("copied");
|
||||
setTimeout(() => opts.copyBtn.classList.remove("copied"), 1200);
|
||||
await writeText(rawText);
|
||||
flash(opts.copyBtn, "copied");
|
||||
});
|
||||
|
||||
// Titel-Edit: Edit-Button macht den Titel editierbar, Enter/Blur schreibt die
|
||||
@@ -172,11 +219,13 @@ export function initPanelView(opts: {
|
||||
editBtn.classList.add("active");
|
||||
editor.focus();
|
||||
};
|
||||
const save = () => {
|
||||
flushEditor = async () => {
|
||||
if (!editing) return;
|
||||
const v = editor.value;
|
||||
leave();
|
||||
opts.onCommit!(v); // schreibt Datei -> panel-update -> set() rendert
|
||||
await opts.onCommit!(v); // schreibt Datei -> panel-update -> set() rendert
|
||||
};
|
||||
const save = () => void flushEditor();
|
||||
const cancel = () => {
|
||||
const p = pending;
|
||||
leave();
|
||||
@@ -216,5 +265,6 @@ export function initPanelView(opts: {
|
||||
applyText(text);
|
||||
},
|
||||
raw: () => rawText,
|
||||
flush: () => flushEditor(),
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user