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
+105
View File
@@ -0,0 +1,105 @@
// Modus-Logik (Tabs) und Kachel-Ansicht der Befehls-History.
import { beforeEach, describe, expect, it, vi } from "vitest";
import { initCommandsView, initPanelMode } from "./commands-view";
function modeSetup() {
document.body.innerHTML = `
<div id="tabs">
<button data-mode="commands">Befehle</button>
<button data-mode="draft">Dokument</button>
<button data-mode="wiki">Wiki</button>
<button data-mode="search">Suche</button>
</div>
<div id="title">Mein Titel</div>
<div id="draft-el"></div>
<div id="cc" hidden></div>
<div id="sc" hidden></div>
<div id="wc" hidden></div>`;
const el = (id: string) => document.getElementById(id)!;
const flush = vi.fn();
const mode = initPanelMode({
tabsEl: el("tabs"),
draftEls: [el("draft-el")],
commandsContent: el("cc"),
searchContent: el("sc"),
wikiContent: el("wc"),
titleEl: el("title"),
flush,
});
const tab = (m: string) =>
document.querySelector<HTMLElement>(`[data-mode="${m}"]`)!;
return { mode, flush, el, tab };
}
describe("initPanelMode", () => {
it("startet im Dokument-Modus mit aktivem Tab", () => {
const { el, tab } = modeSetup();
expect(el("draft-el").hidden).toBe(false);
expect(el("cc").hidden).toBe(true);
expect(tab("draft").classList.contains("active")).toBe(true);
});
it("wechselt Sichtbarkeit, Titel und aktive Markierung", () => {
const { mode, flush, el, tab } = modeSetup();
mode.to("commands");
expect(flush).toHaveBeenCalledOnce();
expect(el("cc").hidden).toBe(false);
expect(el("draft-el").hidden).toBe(true);
expect(el("title").textContent).toBe("Befehle");
expect(tab("commands").classList.contains("active")).toBe(true);
expect(tab("draft").classList.contains("active")).toBe(false);
mode.to("draft");
expect(el("title").textContent).toBe("Mein Titel");
});
it("Tab-Klick wechselt den Modus", () => {
const { el, tab } = modeSetup();
tab("wiki").click();
expect(el("wc").hidden).toBe(false);
expect(el("draft-el").hidden).toBe(true);
});
it("clear hebt die Auswahl auf; danach wählt to() wieder aus", () => {
const { mode, el, tab } = modeSetup();
mode.clear();
expect(document.querySelectorAll(".active").length).toBe(0);
expect(mode.current()).toBe(null);
mode.to("draft");
expect(tab("draft").classList.contains("active")).toBe(true);
expect(el("title").textContent).toBe("Mein Titel");
});
});
describe("initCommandsView", () => {
beforeEach(() => {
document.body.innerHTML = `<div id="c"></div>`;
});
const jsonl = [
JSON.stringify({ ts: 1700000000, session: true }),
JSON.stringify({
ts: 1700000100,
commands: [{ cmd: "ls -la", note: "Liste" }, { cmd: "pwd" }],
}),
].join("\n");
it("rendert Kacheln, Session-Trenner und meldet empty korrekt", () => {
const view = initCommandsView(document.getElementById("c")!, () => {});
view.set(jsonl);
expect(view.empty()).toBe(false);
expect(document.querySelectorAll(".cmd-tile").length).toBe(2);
expect(document.querySelectorAll(".cmd-sep").length).toBe(1);
expect(document.querySelector(".cmd-text")!.textContent).toBe("ls -la");
view.set("");
expect(view.empty()).toBe(true);
});
it("meldet Löschen mit Record- und Eintrags-Index", () => {
const onDelete = vi.fn();
const view = initCommandsView(document.getElementById("c")!, onDelete);
view.set(jsonl);
const dels = document.querySelectorAll<HTMLElement>(".cmd-del");
dels[1].click(); // zweite Kachel im Record 1
expect(onDelete).toHaveBeenCalledWith(1, 1);
});
});