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:
marcus hinz
2026-07-19 17:08:05 +02:00
parent 81a663e020
commit 286513def8
27 changed files with 1653 additions and 190 deletions
+37 -22
View File
@@ -11,8 +11,9 @@ import { invoke } from "@tauri-apps/api/core";
import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow";
import { listen } from "@tauri-apps/api/event";
import { wirePanel } from "./panel-wiring";
import { THEMES } from "./themes";
import { applyTheme, THEMES } from "./themes";
import { flash, panelToast } from "./commands-view";
import { initArchiveForm } from "./archive-form";
// Debug-Instrumentierung: Fehler auf der Seite anzeigen und ins Dev-Log spiegeln.
function showError(msg: string) {
@@ -56,6 +57,13 @@ if (!isMac) {
}
}
// Doppelklick auf die Kopfleiste maximiert (Fenster-Konvention).
document.querySelector("header")!.addEventListener("dblclick", (e) => {
if ((e.target as HTMLElement).hasAttribute("data-tauri-drag-region")) {
win.toggleMaximize();
}
});
// Fensterhintergrund beim Öffnen kommt aus terminal.rs (theme_background);
// dort nicht gepflegte Themes fallen auf Mocha zurück (nur kurzer Moment
// bis die Webview lädt).
@@ -90,14 +98,8 @@ if (cfg?.terminal.icon) {
const picked = THEMES[cfg?.terminal.theme ?? "mocha"];
const theme = picked.xterm;
// Seitenfarben ans Theme anpassen (Defaults in terminal.html sind Mocha).
document.documentElement.style.background = theme.background;
document.body.style.background = theme.background;
document.body.style.borderColor = picked.border;
const header = document.querySelector("header") as HTMLElement;
header.style.background = picked.header;
header.style.borderBottomColor = picked.border;
header.style.color = theme.foreground;
// Seitenfarben ans Theme anpassen — alles Weitere hängt an den CSS-Variablen.
applyTheme(picked);
// Globale Schriftgröße (settings.json: terminalFontSize), ein Wert für alle.
const DEFAULT_FONT_SIZE = 13;
@@ -186,13 +188,6 @@ term.focus();
const panel = document.getElementById("panel")!;
const splitter = document.getElementById("splitter")!;
// Panelfarben ans Theme koppeln.
panel.style.background = picked.header;
panel.style.color = theme.foreground;
splitter.style.background = picked.border;
(panel.querySelector(".panel-head") as HTMLElement).style.borderBottomColor =
picked.border;
let detached = false;
let hasContent = false;
@@ -209,39 +204,52 @@ function hidePanel() {
// Gemeinsame Verdrahtung (Views, Modi, Update-Events); jedes eingehende
// Update blendet das angedockte Panel ein, solange es nicht abgelöst ist.
const { view } = await wirePanel(project, () => {
const { view, mode } = await wirePanel(project, () => {
hasContent = true;
if (!detached) showPanel();
});
// Panel startet zugeklappt — kein Tab aktiv, bis eine Ansicht geöffnet wird.
mode.clear();
// Tab-Klick im Header öffnet das (angedockte) Panel, falls es zu ist.
document.getElementById("panel-tabs")!.addEventListener("click", () => {
const headerTabs = document.getElementById("panel-tabs")!;
headerTabs.addEventListener("click", () => {
hasContent = true;
if (!detached) showPanel();
});
// Solange das Panel abgelöst ist, hat das eigene Fenster die Tabs — die im
// Header verschwinden.
await listen("panel-detached", () => {
detached = true;
headerTabs.hidden = true;
hidePanel();
mode.clear();
});
await listen("panel-attached", () => {
detached = false;
headerTabs.hidden = false;
if (hasContent) showPanel();
});
// Abgelöstes Fenster geschlossen (per ✕ oder OS): angedocktes Panel bleibt
// ausgeblendet, aber der nächste Entwurf darf es wieder einblenden.
await listen("panel-window-closed", () => {
detached = false;
headerTabs.hidden = false;
});
document
.getElementById("panel-detach")!
.addEventListener("click", () => invoke("open_panel_window", { project }));
document.getElementById("panel-hide")!.addEventListener("click", hidePanel);
document.getElementById("panel-hide")!.addEventListener("click", () => {
hidePanel();
mode.clear();
});
// Archivieren: konfigurierten Ordner nehmen, sonst per Dialog wählen (setzt ihn).
// Archivieren: Button klappt das Formular (Ordner/Beschreibung/Schlagwörter)
// auf; Abschicken wählt notfalls erst das Archiv-Home per Dialog.
const archiveBtn = document.getElementById("panel-archive")!;
archiveBtn.addEventListener("click", async () => {
const archiveForm = initArchiveForm(archiveBtn, async (meta) => {
try {
const configured = await invoke<string | null>("panel_archive_dir_cmd", {
project,
@@ -254,7 +262,13 @@ archiveBtn.addEventListener("click", async () => {
dir = chosen as string;
}
await view.flush(); // offene Bearbeitung erst speichern — archiviert, was zu sehen ist
const path = await invoke<string>("panel_archive_cmd", { project, dir });
const path = await invoke<string>("panel_archive_cmd", {
project,
dir,
folder: meta.folder ?? null,
description: meta.description ?? null,
tags: meta.tags,
});
flash(archiveBtn, "copied", 1400);
invoke("reveal_path_cmd", { path });
} catch (e) {
@@ -263,6 +277,7 @@ archiveBtn.addEventListener("click", async () => {
invoke("term_log", { msg: `archive: ${e}` });
}
});
archiveBtn.addEventListener("click", () => archiveForm.toggle());
// Splitter: Panelbreite ziehen.
splitter.addEventListener("mousedown", (e) => {