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:
marcus.hinz
2026-07-11 14:24:16 +02:00
parent e2e9992b1c
commit 165501c8f9
6 changed files with 336 additions and 56 deletions
+69 -3
View File
@@ -76,6 +76,51 @@
.panel-btn.copied { .panel-btn.copied {
color: #a6e3a1; color: #a6e3a1;
} }
.panel-btn.error {
color: #f38ba8;
}
.panel-btn svg {
display: block;
stroke: currentColor;
stroke-width: 1.3;
fill: none;
stroke-linecap: round;
stroke-linejoin: round;
}
.panel-btn.active {
background: #313244;
color: #cdd6f4;
}
.panel-btn.changed {
color: #f9e2af;
}
.panel-editor {
flex: 1;
min-height: 0;
resize: none;
border: none;
outline: none;
padding: 16px 20px;
background: transparent;
color: inherit;
font-family: "JetBrains Mono", Menlo, monospace;
font-size: 12px;
line-height: 1.55;
}
.panel-editor[hidden] {
display: none;
}
.panel-lang {
margin-right: auto;
background: #313244;
color: #a6adc8;
border: none;
border-radius: 6px;
padding: 3px 4px;
font: inherit;
font-size: 11px;
cursor: default;
}
#panel-content { #panel-content {
flex: 1; flex: 1;
min-height: 0; min-height: 0;
@@ -159,10 +204,31 @@
<button class="panel-btn panel-edit" id="panel-title-edit" title="Titel bearbeiten"></button> <button class="panel-btn panel-edit" id="panel-title-edit" title="Titel bearbeiten"></button>
</div> </div>
<div class="panel-actions"> <div class="panel-actions">
<select class="panel-lang" id="panel-lang" title="Sprache der Rechtschreibprüfung">
<option value="de">DE</option>
<option value="en">EN</option>
<option value="en-GB">EN-GB</option>
<option value="fr">FR</option>
<option value="es">ES</option>
<option value="it">IT</option>
<option value="nl">NL</option>
</select>
<button class="panel-btn" id="panel-mode" title="Rohtext / gerendert">MD</button> <button class="panel-btn" id="panel-mode" title="Rohtext / gerendert">MD</button>
<button class="panel-btn" id="panel-copy" title="In die Zwischenablage">Kopieren</button> <button class="panel-btn" id="panel-content-edit" title="Entwurf bearbeiten (Cmd/Ctrl+Enter speichert, Esc verwirft)">
<button class="panel-btn" id="panel-archive" title="In den Archiv-Ordner speichern">Archivieren</button> <svg width="14" height="14" viewBox="0 0 16 16"><path d="M10.8 2.6 13.4 5.2 6 12.6l-3.1.5.5-3.1z" /><path d="M9.7 3.7 12.3 6.3" /></svg>
<button class="panel-btn" id="panel-dock" title="Wieder andocken">Andocken</button> </button>
<button class="panel-btn" id="panel-copy" title="In die Zwischenablage kopieren">
<svg width="14" height="14" viewBox="0 0 16 16"><rect x="5.5" y="5.5" width="8" height="8" rx="1.5" /><path d="M10.5 3.2V3A1.5 1.5 0 0 0 9 1.5H3A1.5 1.5 0 0 0 1.5 3v6A1.5 1.5 0 0 0 3 10.5h.2" /></svg>
</button>
<button class="panel-btn" id="panel-archive" title="In den Archiv-Ordner speichern">
<svg width="14" height="14" viewBox="0 0 16 16"><rect x="1.5" y="2.5" width="13" height="3.5" rx="1" /><path d="M2.8 6v6.5a1 1 0 0 0 1 1h8.4a1 1 0 0 0 1-1V6" /><path d="M6.3 9h3.4" /></svg>
</button>
<button class="panel-btn" id="panel-dock" title="Wieder andocken">
<svg width="14" height="14" viewBox="0 0 16 16"><rect x="1.5" y="2.5" width="13" height="11" rx="1.5" /><path d="M9.5 2.5v11" /><path d="M4 8h3.4M6 6.2 7.8 8 6 9.8" /></svg>
</button>
<button class="panel-btn" id="panel-close" title="Panel schließen">
<svg width="12" height="12" viewBox="0 0 12 12"><path d="M3 3l6 6M9 3l-6 6" /></svg>
</button>
</div> </div>
</div> </div>
<div id="panel-content" class="md"></div> <div id="panel-content" class="md"></div>
+68 -14
View File
@@ -88,6 +88,17 @@ pub(crate) fn project_archive_dir(project: &str) -> Option<PathBuf> {
pub(crate) fn set_project_archive_dir(project: &str, dir: &str) -> Result<(), String> { pub(crate) fn set_project_archive_dir(project: &str, dir: &str) -> Result<(), String> {
let paths = Paths::real(); let paths = Paths::real();
let expanded = expand_home(&paths, dir); let expanded = expand_home(&paths, dir);
if !expanded.is_absolute() {
return Err(format!("Archiv-Ordner muss ein absoluter Pfad sein: {}", expanded.display()));
}
// Zu weit gefasst? Ein Vorfahre des Home (inkl. Home selbst und Root) würde
// claude über additionalDirectories weiten Zugriff geben — das lehnen wir ab.
if paths.home.starts_with(&expanded) {
return Err(format!(
"Archiv-Ordner zu weit gefasst: {}. Bitte einen spezifischen Unterordner wählen.",
expanded.display()
));
}
fs::create_dir_all(&expanded).map_err(|e| format!("{}: {e}", expanded.display()))?; fs::create_dir_all(&expanded).map_err(|e| format!("{}: {e}", expanded.display()))?;
let contracted = contract_home(&paths, &expanded); let contracted = contract_home(&paths, &expanded);
let mut cfg = read_project_config_in(&paths, project)?; let mut cfg = read_project_config_in(&paths, project)?;
@@ -638,8 +649,11 @@ fn set_todo_in(paths: &Paths, name: &str, enabled: bool) -> Result<(), String> {
fs::write(&sp, raw + "\n").map_err(|e| format!("{}: {e}", sp.display())) fs::write(&sp, raw + "\n").map_err(|e| format!("{}: {e}", sp.display()))
} }
/// "~/x" relativ zum Home auflösen; alles andere unverändert. /// "~" bzw. "~/x" relativ zum Home auflösen; alles andere unverändert.
fn expand_home(paths: &Paths, p: &str) -> PathBuf { fn expand_home(paths: &Paths, p: &str) -> PathBuf {
if p == "~" {
return paths.home.clone();
}
match p.strip_prefix("~/") { match p.strip_prefix("~/") {
Some(rest) => paths.home.join(rest), Some(rest) => paths.home.join(rest),
None => PathBuf::from(p), None => PathBuf::from(p),
@@ -1460,6 +1474,14 @@ fn register_mcp_server(pool_dir: &std::path::Path) {
let Ok(exe) = std::env::current_exe() else { let Ok(exe) = std::env::current_exe() else {
return; return;
}; };
// Tool nicht deferren, sonst sieht das Modell write_panel nicht und schreibt
// den Entwurf in den Chat statt ins Panel.
let desired = serde_json::json!({
"type": "stdio",
"command": exe.to_string_lossy(),
"args": ["--mcp-panel"],
"alwaysLoad": true,
});
let cfg = pool_dir.join(".claude.json"); let cfg = pool_dir.join(".claude.json");
let mut v = fs::read_to_string(&cfg) let mut v = fs::read_to_string(&cfg)
.ok() .ok()
@@ -1473,18 +1495,13 @@ fn register_mcp_server(pool_dir: &std::path::Path) {
else { else {
return; return;
}; };
// Schon korrekt und kein Altkey -> nichts schreiben. `.claude.json` ist
// claudes Live-State; wir fassen sie nur an, wenn der Eintrag fehlt/abweicht.
if servers.get("aicontrol").is_none() && servers.get(PANEL_MCP_SERVER) == Some(&desired) {
return;
}
servers.remove("aicontrol"); // alter Key vor Umbenennung servers.remove("aicontrol"); // alter Key vor Umbenennung
servers.insert( servers.insert(PANEL_MCP_SERVER.into(), desired);
PANEL_MCP_SERVER.into(),
serde_json::json!({
"type": "stdio",
"command": exe.to_string_lossy(),
"args": ["--mcp-panel"],
// Tool nicht deferren, sonst sieht das Modell write_panel nicht und
// schreibt den Entwurf in den Chat statt ins Panel.
"alwaysLoad": true,
}),
);
if let Ok(out) = serde_json::to_string_pretty(&v) { if let Ok(out) = serde_json::to_string_pretty(&v) {
let _ = fs::write(&cfg, out + "\n"); let _ = fs::write(&cfg, out + "\n");
} }
@@ -2137,6 +2154,17 @@ fn terminal_font_size() -> u32 {
.unwrap_or(13) .unwrap_or(13)
} }
/// Standard-Sprache der Rechtschreibprüfung (settings.json: spellcheckLang),
/// Default „de". Pro Text im Panel überschreibbar.
#[tauri::command]
fn spellcheck_lang() -> String {
fs::read_to_string(Paths::real().config_dir().join(APP_SETTINGS_FILE))
.ok()
.and_then(|s| serde_json::from_str::<serde_json::Value>(&s).ok())
.and_then(|v| v["spellcheckLang"].as_str().map(str::to_string))
.unwrap_or_else(|| "de".to_string())
}
/// Setzt die Schriftgröße; erhält übrige App-settings. /// Setzt die Schriftgröße; erhält übrige App-settings.
#[tauri::command] #[tauri::command]
fn set_terminal_font_size(size: u32) -> Result<(), String> { fn set_terminal_font_size(size: u32) -> Result<(), String> {
@@ -2269,6 +2297,30 @@ fn panel_archive_cmd(project: String, dir: Option<String>) -> Result<String, Str
archive_panel_content(&project, dir.as_deref()).map(|p| p.display().to_string()) archive_panel_content(&project, dir.as_deref()).map(|p| p.display().to_string())
} }
/// Zeigt einen Pfad im Dateimanager (nach dem Archivieren „im Finder zeigen").
#[tauri::command]
fn reveal_path_cmd(path: String) {
reveal_path(std::path::Path::new(&path));
}
#[cfg(target_os = "macos")]
fn reveal_path(path: &std::path::Path) {
let _ = Command::new("open").arg("-R").arg(path).spawn();
}
#[cfg(target_os = "linux")]
fn reveal_path(path: &std::path::Path) {
let target = path.parent().unwrap_or(path);
let _ = Command::new("xdg-open").arg(target).spawn();
}
#[cfg(target_os = "windows")]
fn reveal_path(path: &std::path::Path) {
let _ = Command::new("explorer")
.arg(format!("/select,{}", path.display()))
.spawn();
}
/// D-Bus-Relay für die GNOME-Extension: Show()/Hide() steuern das Popup-Fenster. /// D-Bus-Relay für die GNOME-Extension: Show()/Hide() steuern das Popup-Fenster.
/// Die Extension ruft Show() beim Panel-Klick und positioniert das Fenster selbst. /// Die Extension ruft Show() beim Panel-Klick und positioniert das Fenster selbst.
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
@@ -2513,7 +2565,7 @@ fn terminal_builder(project: String) -> tauri::Builder<tauri::Wry> {
if let tauri::WindowEvent::Destroyed = event { if let tauri::WindowEvent::Destroyed = event {
if window.label().starts_with("panel-") { if window.label().starts_with("panel-") {
use tauri::{Emitter, Manager}; use tauri::{Emitter, Manager};
let _ = window.app_handle().emit("panel-attached", ()); let _ = window.app_handle().emit("panel-window-closed", ());
} else { } else {
terminal::close(window); terminal::close(window);
} }
@@ -2529,12 +2581,14 @@ fn terminal_builder(project: String) -> tauri::Builder<tauri::Wry> {
terminal::open_panel_window, terminal::open_panel_window,
panel_archive_dir_cmd, panel_archive_dir_cmd,
panel_archive_cmd, panel_archive_cmd,
reveal_path_cmd,
// Header im Terminal-Prozess: Projektliste, Icon und Pool-Name. // Header im Terminal-Prozess: Projektliste, Icon und Pool-Name.
list_projects, list_projects,
project_icon, project_icon,
pool_label, pool_label,
terminal_font_size, terminal_font_size,
set_terminal_font_size set_terminal_font_size,
spellcheck_lang
]) ])
} }
+92 -10
View File
@@ -58,11 +58,20 @@ export function initPanelView(opts: {
modeBtn: HTMLElement; modeBtn: HTMLElement;
titleEl?: HTMLElement; titleEl?: HTMLElement;
editBtn?: 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; onCommit?: (text: string) => void;
}): PanelView { }): PanelView {
let rawText = ""; let rawText = "";
let rendered = true; 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() { function draw() {
if (rendered) { if (rendered) {
@@ -83,12 +92,7 @@ export function initPanelView(opts: {
opts.copyBtn.addEventListener("click", async () => { opts.copyBtn.addEventListener("click", async () => {
await navigator.clipboard.writeText(rawText); await navigator.clipboard.writeText(rawText);
opts.copyBtn.classList.add("copied"); opts.copyBtn.classList.add("copied");
const label = opts.copyBtn.textContent; setTimeout(() => opts.copyBtn.classList.remove("copied"), 1200);
opts.copyBtn.textContent = "Kopiert";
setTimeout(() => {
opts.copyBtn.classList.remove("copied");
opts.copyBtn.textContent = label;
}, 1200);
}); });
// Titel-Edit: Edit-Button macht den Titel editierbar, Enter/Blur schreibt die // Titel-Edit: Edit-Button macht den Titel editierbar, Enter/Blur schreibt die
@@ -124,14 +128,92 @@ export function initPanelView(opts: {
titleEl.addEventListener("blur", commit); titleEl.addEventListener("blur", commit);
} }
draw(); // Content-Edit: Button schaltet den Inhalt auf eine Rohtext-Textarea. Speichern
return { // (Button erneut oder Cmd/Ctrl+Enter) schreibt zurück, Escape verwirft.
set(text: string) { // 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; rawText = text;
if (titleEl && titleEl.getAttribute("contenteditable") !== "true") { if (titleEl && titleEl.getAttribute("contenteditable") !== "true") {
titleEl.textContent = firstLine(text); titleEl.textContent = firstLine(text);
} }
draw(); draw();
}
draw();
return {
set(text: string) {
// 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;
}
applyText(text);
}, },
raw: () => rawText, raw: () => rawText,
}; };
+21 -15
View File
@@ -2,7 +2,7 @@ import "@fontsource/jetbrains-mono/400.css";
import "@fontsource/jetbrains-mono/500.css"; import "@fontsource/jetbrains-mono/500.css";
import { invoke } from "@tauri-apps/api/core"; import { invoke } from "@tauri-apps/api/core";
import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow"; 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"; import { initPanelView } from "./panel-view";
// Abgelöstes Panel-Fenster: liest den aktuellen Entwurf einmal ein und folgt // Abgelöstes Panel-Fenster: liest den aktuellen Entwurf einmal ein und folgt
@@ -16,6 +16,9 @@ const view = initPanelView({
modeBtn: document.getElementById("panel-mode")!, modeBtn: document.getElementById("panel-mode")!,
titleEl: document.querySelector(".panel-title") as HTMLElement, titleEl: document.querySelector(".panel-title") as HTMLElement,
editBtn: document.getElementById("panel-title-edit")!, 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 }), 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. // Archivieren: wie im angedockten Panel — Ordner nehmen oder per Dialog wählen.
const archiveBtn = document.getElementById("panel-archive")!; 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 () => { archiveBtn.addEventListener("click", async () => {
const orig = archiveBtn.textContent;
try { try {
const configured = await invoke<string | null>("panel_archive_dir_cmd", { const configured = await invoke<string | null>("panel_archive_dir_cmd", {
project, project,
@@ -38,20 +44,20 @@ archiveBtn.addEventListener("click", async () => {
if (!chosen) return; if (!chosen) return;
dir = chosen as string; dir = chosen as string;
} }
await invoke<string>("panel_archive_cmd", { project, dir }); const path = await invoke<string>("panel_archive_cmd", { project, dir });
archiveBtn.textContent = "Archiviert"; flashBtn(archiveBtn, "copied");
archiveBtn.classList.add("copied"); invoke("reveal_path_cmd", { path });
} catch { } 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 // „Andocken": angedocktes Panel wieder einblenden, dann dieses Fenster schließen.
// angedockte Panel im Terminal-Fenster erscheint wieder. document.getElementById("panel-dock")!.addEventListener("click", async () => {
document await emit("panel-attached");
.getElementById("panel-dock")! win.close();
.addEventListener("click", () => 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
View File
@@ -553,6 +553,9 @@ const view = initPanelView({
modeBtn: document.getElementById("panel-mode")!, modeBtn: document.getElementById("panel-mode")!,
titleEl: panel.querySelector(".panel-title") as HTMLElement, titleEl: panel.querySelector(".panel-title") as HTMLElement,
editBtn: document.getElementById("panel-title-edit")!, 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 }), onCommit: (text) => invoke("panel_set", { project, text }),
}); });
@@ -583,6 +586,11 @@ await listen("panel-attached", () => {
detached = false; detached = false;
if (hasContent) showPanel(); 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 document
.getElementById("panel-detach")! .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). // Archivieren: konfigurierten Ordner nehmen, sonst per Dialog wählen (setzt ihn).
const archiveBtn = document.getElementById("panel-archive")!; 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 () => { archiveBtn.addEventListener("click", async () => {
const orig = archiveBtn.textContent;
try { try {
const configured = await invoke<string | null>("panel_archive_dir_cmd", { const configured = await invoke<string | null>("panel_archive_dir_cmd", {
project, project,
@@ -604,17 +615,13 @@ archiveBtn.addEventListener("click", async () => {
if (!chosen) return; if (!chosen) return;
dir = chosen as string; dir = chosen as string;
} }
await invoke<string>("panel_archive_cmd", { project, dir }); const path = await invoke<string>("panel_archive_cmd", { project, dir });
archiveBtn.textContent = "Archiviert"; flashBtn(archiveBtn, "copied");
archiveBtn.classList.add("copied"); invoke("reveal_path_cmd", { path });
} catch (e) { } catch (e) {
archiveBtn.textContent = "Fehler"; flashBtn(archiveBtn, "error");
invoke("term_log", { msg: `archive: ${e}` }); invoke("term_log", { msg: `archive: ${e}` });
} }
setTimeout(() => {
archiveBtn.textContent = orig;
archiveBtn.classList.remove("copied");
}, 1400);
}); });
// Splitter: Panelbreite ziehen. // Splitter: Panelbreite ziehen.
+69 -4
View File
@@ -269,6 +269,51 @@
.panel-btn.copied { .panel-btn.copied {
color: #a6e3a1; color: #a6e3a1;
} }
.panel-btn.error {
color: #f38ba8;
}
.panel-btn svg {
display: block;
stroke: currentColor;
stroke-width: 1.3;
fill: none;
stroke-linecap: round;
stroke-linejoin: round;
}
.panel-btn.active {
background: #313244;
color: #cdd6f4;
}
.panel-btn.changed {
color: #f9e2af;
}
.panel-editor {
flex: 1;
min-height: 0;
resize: none;
border: none;
outline: none;
padding: 14px 16px;
background: transparent;
color: inherit;
font-family: "JetBrains Mono", Menlo, monospace;
font-size: 12px;
line-height: 1.55;
}
.panel-editor[hidden] {
display: none;
}
.panel-lang {
margin-right: auto;
background: #313244;
color: #a6adc8;
border: none;
border-radius: 6px;
padding: 3px 4px;
font: inherit;
font-size: 11px;
cursor: default;
}
#panel-content { #panel-content {
flex: 1; flex: 1;
min-height: 0; min-height: 0;
@@ -372,11 +417,31 @@
<button class="panel-btn panel-edit" id="panel-title-edit" title="Titel bearbeiten"></button> <button class="panel-btn panel-edit" id="panel-title-edit" title="Titel bearbeiten"></button>
</div> </div>
<div class="panel-actions"> <div class="panel-actions">
<select class="panel-lang" id="panel-lang" title="Sprache der Rechtschreibprüfung">
<option value="de">DE</option>
<option value="en">EN</option>
<option value="en-GB">EN-GB</option>
<option value="fr">FR</option>
<option value="es">ES</option>
<option value="it">IT</option>
<option value="nl">NL</option>
</select>
<button class="panel-btn" id="panel-mode" title="Rohtext / gerendert">MD</button> <button class="panel-btn" id="panel-mode" title="Rohtext / gerendert">MD</button>
<button class="panel-btn" id="panel-copy" title="In die Zwischenablage">Kopieren</button> <button class="panel-btn" id="panel-content-edit" title="Entwurf bearbeiten (Cmd/Ctrl+Enter speichert, Esc verwirft)">
<button class="panel-btn" id="panel-archive" title="In den Archiv-Ordner speichern">Archivieren</button> <svg width="14" height="14" viewBox="0 0 16 16"><path d="M10.8 2.6 13.4 5.2 6 12.6l-3.1.5.5-3.1z" /><path d="M9.7 3.7 12.3 6.3" /></svg>
<button class="panel-btn" id="panel-detach" title="In eigenes Fenster ablösen"></button> </button>
<button class="panel-btn" id="panel-hide" title="Panel ausblenden"></button> <button class="panel-btn" id="panel-copy" title="In die Zwischenablage kopieren">
<svg width="14" height="14" viewBox="0 0 16 16"><rect x="5.5" y="5.5" width="8" height="8" rx="1.5" /><path d="M10.5 3.2V3A1.5 1.5 0 0 0 9 1.5H3A1.5 1.5 0 0 0 1.5 3v6A1.5 1.5 0 0 0 3 10.5h.2" /></svg>
</button>
<button class="panel-btn" id="panel-archive" title="In den Archiv-Ordner speichern">
<svg width="14" height="14" viewBox="0 0 16 16"><rect x="1.5" y="2.5" width="13" height="3.5" rx="1" /><path d="M2.8 6v6.5a1 1 0 0 0 1 1h8.4a1 1 0 0 0 1-1V6" /><path d="M6.3 9h3.4" /></svg>
</button>
<button class="panel-btn" id="panel-detach" title="In eigenes Fenster ablösen">
<svg width="14" height="14" viewBox="0 0 16 16"><path d="M12.5 8.5V13a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V4.5a1 1 0 0 1 1-1h4.5" /><path d="M10 2h4v4" /><path d="M14 2 8 8" /></svg>
</button>
<button class="panel-btn" id="panel-hide" title="Panel ausblenden">
<svg width="12" height="12" viewBox="0 0 12 12"><path d="M3 3l6 6M9 3l-6 6" /></svg>
</button>
</div> </div>
</div> </div>
<div id="panel-content" class="md"></div> <div id="panel-content" class="md"></div>