Panel: Clobber-Schutz, Content-Editor, Archiv-Reveal/Guard, Rechtschreib-Sprache
- register_mcp_server schreibt .claude.json nur bei fehlendem/abweichendem Eintrag (claudes Live-State nicht unnötig ueberschreiben). - Content-Editor im Panel (Rohtext-Textarea): Cmd/Ctrl+Enter speichert via panel_set, Esc verwirft; eingehende Updates werden waehrend der Bearbeitung gepuffert (Button "geaendert"), Speichern gewinnt. - Archivieren zeigt die Datei danach im Dateimanager (reveal_path_cmd, cfg pro OS). Breadth-Guard: Archiv-Ordner darf kein Home-Vorfahre sein, muss absolut sein; expand_home loest bare ~ auf. - Rechtschreibpruefung: Default in settings.json (spellcheckLang "de"), pro Text per Selector ueberschreibbar.
This commit is contained in:
+93
-11
@@ -58,11 +58,20 @@ export function initPanelView(opts: {
|
||||
modeBtn: HTMLElement;
|
||||
titleEl?: HTMLElement;
|
||||
editBtn?: HTMLElement;
|
||||
/// Wird mit dem neuen Rohtext aufgerufen, wenn der Titel geändert wurde.
|
||||
editContentBtn?: HTMLElement;
|
||||
langSelect?: HTMLSelectElement;
|
||||
/// Standard-Sprache der Rechtschreibprüfung (aus den App-Settings).
|
||||
defaultLang?: string;
|
||||
/// Wird mit dem neuen Rohtext aufgerufen, wenn Titel oder Inhalt geändert
|
||||
/// wurden.
|
||||
onCommit?: (text: string) => void;
|
||||
}): PanelView {
|
||||
let rawText = "";
|
||||
let rendered = true;
|
||||
// Content-Edit-Zustand: während des Editierens werden eingehende Updates
|
||||
// gepuffert statt angewandt.
|
||||
let editing = false;
|
||||
let pending: string | null = null;
|
||||
|
||||
function draw() {
|
||||
if (rendered) {
|
||||
@@ -83,12 +92,7 @@ export function initPanelView(opts: {
|
||||
opts.copyBtn.addEventListener("click", async () => {
|
||||
await navigator.clipboard.writeText(rawText);
|
||||
opts.copyBtn.classList.add("copied");
|
||||
const label = opts.copyBtn.textContent;
|
||||
opts.copyBtn.textContent = "Kopiert";
|
||||
setTimeout(() => {
|
||||
opts.copyBtn.classList.remove("copied");
|
||||
opts.copyBtn.textContent = label;
|
||||
}, 1200);
|
||||
setTimeout(() => opts.copyBtn.classList.remove("copied"), 1200);
|
||||
});
|
||||
|
||||
// Titel-Edit: Edit-Button macht den Titel editierbar, Enter/Blur schreibt die
|
||||
@@ -124,14 +128,92 @@ export function initPanelView(opts: {
|
||||
titleEl.addEventListener("blur", commit);
|
||||
}
|
||||
|
||||
// Content-Edit: Button schaltet den Inhalt auf eine Rohtext-Textarea. Speichern
|
||||
// (Button erneut oder Cmd/Ctrl+Enter) schreibt zurück, Escape verwirft.
|
||||
// Eingehende Updates während des Editierens werden gepuffert.
|
||||
if (opts.editContentBtn && opts.onCommit) {
|
||||
const editBtn = opts.editContentBtn;
|
||||
const editor = document.createElement("textarea");
|
||||
editor.className = "panel-editor";
|
||||
editor.spellcheck = true;
|
||||
editor.hidden = true;
|
||||
opts.content.after(editor);
|
||||
|
||||
// Sprache der Rechtschreibprüfung: Default aus den Settings, per Selector
|
||||
// pro Text überschreibbar.
|
||||
let lang = opts.defaultLang || "de";
|
||||
const sel = opts.langSelect;
|
||||
if (sel) {
|
||||
if ([...sel.options].some((o) => o.value === lang)) sel.value = lang;
|
||||
else lang = sel.value;
|
||||
sel.addEventListener("change", () => {
|
||||
lang = sel.value;
|
||||
editor.lang = lang;
|
||||
// Neuprüfung erzwingen.
|
||||
editor.spellcheck = false;
|
||||
editor.spellcheck = true;
|
||||
if (!editor.hidden) editor.focus();
|
||||
});
|
||||
}
|
||||
editor.lang = lang;
|
||||
|
||||
const leave = () => {
|
||||
editing = false;
|
||||
editor.hidden = true;
|
||||
opts.content.hidden = false;
|
||||
editBtn.classList.remove("active", "changed");
|
||||
};
|
||||
const enter = () => {
|
||||
editing = true;
|
||||
pending = null;
|
||||
editor.value = rawText;
|
||||
opts.content.hidden = true;
|
||||
editor.hidden = false;
|
||||
editBtn.classList.add("active");
|
||||
editor.focus();
|
||||
};
|
||||
const save = () => {
|
||||
const v = editor.value;
|
||||
leave();
|
||||
opts.onCommit!(v); // schreibt Datei -> panel-update -> set() rendert
|
||||
};
|
||||
const cancel = () => {
|
||||
const p = pending;
|
||||
leave();
|
||||
if (p !== null) applyText(p);
|
||||
else draw();
|
||||
};
|
||||
editBtn.addEventListener("click", () => (editing ? save() : enter()));
|
||||
editor.addEventListener("keydown", (e) => {
|
||||
if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
|
||||
e.preventDefault();
|
||||
save();
|
||||
} else if (e.key === "Escape") {
|
||||
e.preventDefault();
|
||||
cancel();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function applyText(text: string) {
|
||||
rawText = text;
|
||||
if (titleEl && titleEl.getAttribute("contenteditable") !== "true") {
|
||||
titleEl.textContent = firstLine(text);
|
||||
}
|
||||
draw();
|
||||
}
|
||||
|
||||
draw();
|
||||
return {
|
||||
set(text: string) {
|
||||
rawText = text;
|
||||
if (titleEl && titleEl.getAttribute("contenteditable") !== "true") {
|
||||
titleEl.textContent = firstLine(text);
|
||||
// Während einer Inhalts-Bearbeitung nicht überschreiben — puffern und den
|
||||
// Edit-Button als „geändert" markieren.
|
||||
if (editing) {
|
||||
pending = text;
|
||||
opts.editContentBtn?.classList.add("changed");
|
||||
return;
|
||||
}
|
||||
draw();
|
||||
applyText(text);
|
||||
},
|
||||
raw: () => rawText,
|
||||
};
|
||||
|
||||
+21
-15
@@ -2,7 +2,7 @@ import "@fontsource/jetbrains-mono/400.css";
|
||||
import "@fontsource/jetbrains-mono/500.css";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow";
|
||||
import { listen } from "@tauri-apps/api/event";
|
||||
import { listen, emit } from "@tauri-apps/api/event";
|
||||
import { initPanelView } from "./panel-view";
|
||||
|
||||
// Abgelöstes Panel-Fenster: liest den aktuellen Entwurf einmal ein und folgt
|
||||
@@ -16,6 +16,9 @@ const view = initPanelView({
|
||||
modeBtn: document.getElementById("panel-mode")!,
|
||||
titleEl: document.querySelector(".panel-title") as HTMLElement,
|
||||
editBtn: document.getElementById("panel-title-edit")!,
|
||||
editContentBtn: document.getElementById("panel-content-edit")!,
|
||||
langSelect: document.getElementById("panel-lang") as HTMLSelectElement,
|
||||
defaultLang: await invoke<string>("spellcheck_lang"),
|
||||
onCommit: (text) => invoke("panel_set", { project, text }),
|
||||
});
|
||||
|
||||
@@ -25,8 +28,11 @@ await listen<string>("panel-update", (e) => view.set(e.payload));
|
||||
|
||||
// Archivieren: wie im angedockten Panel — Ordner nehmen oder per Dialog wählen.
|
||||
const archiveBtn = document.getElementById("panel-archive")!;
|
||||
function flashBtn(btn: HTMLElement, cls: string) {
|
||||
btn.classList.add(cls);
|
||||
setTimeout(() => btn.classList.remove(cls), 1400);
|
||||
}
|
||||
archiveBtn.addEventListener("click", async () => {
|
||||
const orig = archiveBtn.textContent;
|
||||
try {
|
||||
const configured = await invoke<string | null>("panel_archive_dir_cmd", {
|
||||
project,
|
||||
@@ -38,20 +44,20 @@ archiveBtn.addEventListener("click", async () => {
|
||||
if (!chosen) return;
|
||||
dir = chosen as string;
|
||||
}
|
||||
await invoke<string>("panel_archive_cmd", { project, dir });
|
||||
archiveBtn.textContent = "Archiviert";
|
||||
archiveBtn.classList.add("copied");
|
||||
const path = await invoke<string>("panel_archive_cmd", { project, dir });
|
||||
flashBtn(archiveBtn, "copied");
|
||||
invoke("reveal_path_cmd", { path });
|
||||
} catch {
|
||||
archiveBtn.textContent = "Fehler";
|
||||
flashBtn(archiveBtn, "error");
|
||||
}
|
||||
setTimeout(() => {
|
||||
archiveBtn.textContent = orig;
|
||||
archiveBtn.classList.remove("copied");
|
||||
}, 1400);
|
||||
});
|
||||
|
||||
// „Andocken": Fenster schließen — terminal.rs meldet `panel-attached`, das
|
||||
// angedockte Panel im Terminal-Fenster erscheint wieder.
|
||||
document
|
||||
.getElementById("panel-dock")!
|
||||
.addEventListener("click", () => win.close());
|
||||
// „Andocken": angedocktes Panel wieder einblenden, dann dieses Fenster schließen.
|
||||
document.getElementById("panel-dock")!.addEventListener("click", async () => {
|
||||
await emit("panel-attached");
|
||||
win.close();
|
||||
});
|
||||
|
||||
// „Schließen": Fenster zu, ohne wieder anzudocken (Panel bleibt aus, bis ein
|
||||
// neuer Entwurf kommt).
|
||||
document.getElementById("panel-close")!.addEventListener("click", () => win.close());
|
||||
|
||||
+16
-9
@@ -553,6 +553,9 @@ const view = initPanelView({
|
||||
modeBtn: document.getElementById("panel-mode")!,
|
||||
titleEl: panel.querySelector(".panel-title") as HTMLElement,
|
||||
editBtn: document.getElementById("panel-title-edit")!,
|
||||
editContentBtn: document.getElementById("panel-content-edit")!,
|
||||
langSelect: document.getElementById("panel-lang") as HTMLSelectElement,
|
||||
defaultLang: await invoke<string>("spellcheck_lang"),
|
||||
onCommit: (text) => invoke("panel_set", { project, text }),
|
||||
});
|
||||
|
||||
@@ -583,6 +586,11 @@ await listen("panel-attached", () => {
|
||||
detached = 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;
|
||||
});
|
||||
|
||||
document
|
||||
.getElementById("panel-detach")!
|
||||
@@ -591,8 +599,11 @@ document.getElementById("panel-hide")!.addEventListener("click", hidePanel);
|
||||
|
||||
// Archivieren: konfigurierten Ordner nehmen, sonst per Dialog wählen (setzt ihn).
|
||||
const archiveBtn = document.getElementById("panel-archive")!;
|
||||
function flashBtn(btn: HTMLElement, cls: string) {
|
||||
btn.classList.add(cls);
|
||||
setTimeout(() => btn.classList.remove(cls), 1400);
|
||||
}
|
||||
archiveBtn.addEventListener("click", async () => {
|
||||
const orig = archiveBtn.textContent;
|
||||
try {
|
||||
const configured = await invoke<string | null>("panel_archive_dir_cmd", {
|
||||
project,
|
||||
@@ -604,17 +615,13 @@ archiveBtn.addEventListener("click", async () => {
|
||||
if (!chosen) return;
|
||||
dir = chosen as string;
|
||||
}
|
||||
await invoke<string>("panel_archive_cmd", { project, dir });
|
||||
archiveBtn.textContent = "Archiviert";
|
||||
archiveBtn.classList.add("copied");
|
||||
const path = await invoke<string>("panel_archive_cmd", { project, dir });
|
||||
flashBtn(archiveBtn, "copied");
|
||||
invoke("reveal_path_cmd", { path });
|
||||
} catch (e) {
|
||||
archiveBtn.textContent = "Fehler";
|
||||
flashBtn(archiveBtn, "error");
|
||||
invoke("term_log", { msg: `archive: ${e}` });
|
||||
}
|
||||
setTimeout(() => {
|
||||
archiveBtn.textContent = orig;
|
||||
archiveBtn.classList.remove("copied");
|
||||
}, 1400);
|
||||
});
|
||||
|
||||
// Splitter: Panelbreite ziehen.
|
||||
|
||||
Reference in New Issue
Block a user