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:
@@ -0,0 +1,199 @@
|
||||
/// Kachel-Ansicht der Command-History: rendert die JSONL-Datei
|
||||
/// (write_commands im MCP-Server) als kopierbare Kacheln, Neuestes oben,
|
||||
/// mit Zeitmarken und Session-Trennern. Löschen einer Kachel geht als
|
||||
/// onDelete(line, entry) an den Aufrufer; der neue Stand kommt über den
|
||||
/// Watcher zurück. DOM wird per createElement gebaut — Befehle sind
|
||||
/// Fremdtext und gehen nie durch innerHTML.
|
||||
|
||||
import { writeText } from "@tauri-apps/plugin-clipboard-manager";
|
||||
|
||||
interface CommandEntry {
|
||||
cmd: string;
|
||||
note?: string;
|
||||
}
|
||||
|
||||
interface Record {
|
||||
ts: number;
|
||||
session?: boolean;
|
||||
commands?: CommandEntry[];
|
||||
}
|
||||
|
||||
export interface CommandsView {
|
||||
set(text: string): void;
|
||||
empty(): boolean;
|
||||
}
|
||||
|
||||
function fmtTime(ts: number): string {
|
||||
const d = new Date(ts * 1000);
|
||||
const hm = d.toLocaleTimeString("de", { hour: "2-digit", minute: "2-digit" });
|
||||
return d.toDateString() === new Date().toDateString()
|
||||
? hm
|
||||
: `${d.toLocaleDateString("de")} ${hm}`;
|
||||
}
|
||||
|
||||
/// Kurzes visuelles Feedback (copied/error) — der eine Flash-Helper fürs
|
||||
/// ganze Panel.
|
||||
export function flash(el: HTMLElement, cls: string, ms = 1200) {
|
||||
el.classList.add(cls);
|
||||
setTimeout(() => el.classList.remove(cls), ms);
|
||||
}
|
||||
|
||||
/// Sichtbare Fehlermeldung im Panel: kurz eingeblendete Zeile oben rechts.
|
||||
export function panelToast(msg: string) {
|
||||
const t = document.createElement("div");
|
||||
t.className = "panel-toast";
|
||||
t.textContent = msg;
|
||||
document.body.append(t);
|
||||
setTimeout(() => t.remove(), 5000);
|
||||
}
|
||||
|
||||
function copyBtn(text: () => string): HTMLButtonElement {
|
||||
const btn = document.createElement("button");
|
||||
btn.className = "panel-btn cmd-copy";
|
||||
btn.title = "Befehl kopieren";
|
||||
btn.innerHTML =
|
||||
'<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>';
|
||||
btn.addEventListener("click", async () => {
|
||||
await writeText(text());
|
||||
flash(btn, "copied");
|
||||
});
|
||||
return btn;
|
||||
}
|
||||
|
||||
function deleteBtn(onClick: () => void): HTMLButtonElement {
|
||||
const btn = document.createElement("button");
|
||||
btn.className = "panel-btn cmd-del";
|
||||
btn.title = "Aus der History entfernen";
|
||||
btn.innerHTML =
|
||||
'<svg width="14" height="14" viewBox="0 0 16 16"><path d="M2.5 4.5h11" /><path d="M5.5 4.5V3a1 1 0 0 1 1-1h3a1 1 0 0 1 1 1v1.5" /><path d="M4 4.5l.7 8.6a1 1 0 0 0 1 .9h4.6a1 1 0 0 0 1-.9l.7-8.6" /></svg>';
|
||||
btn.addEventListener("click", onClick);
|
||||
return btn;
|
||||
}
|
||||
|
||||
export function initCommandsView(
|
||||
container: HTMLElement,
|
||||
onDelete: (line: number, entry: number) => void,
|
||||
): CommandsView {
|
||||
let count = 0;
|
||||
|
||||
function render(records: Record[]) {
|
||||
container.textContent = "";
|
||||
// Neuestes oben: rückwärts durch die Records; ein Session-Marker trennt
|
||||
// die davor liegenden (älteren) Einträge ab und steht daher unter ihnen.
|
||||
// Der Record-Index entspricht der nicht-leeren JSONL-Zeile — er
|
||||
// adressiert den Eintrag beim Löschen.
|
||||
for (let i = records.length - 1; i >= 0; i--) {
|
||||
const rec = records[i];
|
||||
if (rec.session) {
|
||||
const sep = document.createElement("div");
|
||||
sep.className = "cmd-sep";
|
||||
sep.textContent = `Session · ${fmtTime(rec.ts)}`;
|
||||
container.append(sep);
|
||||
continue;
|
||||
}
|
||||
const cmds = rec.commands ?? [];
|
||||
if (!cmds.length) continue;
|
||||
const block = document.createElement("div");
|
||||
block.className = "cmd-rec";
|
||||
|
||||
const head = document.createElement("div");
|
||||
head.className = "cmd-rec-head";
|
||||
const time = document.createElement("span");
|
||||
time.textContent = fmtTime(rec.ts);
|
||||
head.append(time);
|
||||
if (cmds.length > 1) {
|
||||
const all = document.createElement("button");
|
||||
all.className = "cmd-all";
|
||||
all.textContent = "Alle kopieren";
|
||||
all.addEventListener("click", async () => {
|
||||
await writeText(cmds.map((c) => c.cmd).join("\n"));
|
||||
flash(all, "copied");
|
||||
});
|
||||
head.append(all);
|
||||
}
|
||||
block.append(head);
|
||||
|
||||
cmds.forEach((entry, j) => {
|
||||
const tile = document.createElement("div");
|
||||
tile.className = "cmd-tile";
|
||||
const body = document.createElement("div");
|
||||
body.className = "cmd-body";
|
||||
const cmd = document.createElement("div");
|
||||
cmd.className = "cmd-text";
|
||||
cmd.textContent = entry.cmd;
|
||||
body.append(cmd);
|
||||
if (entry.note) {
|
||||
const note = document.createElement("div");
|
||||
note.className = "cmd-note";
|
||||
note.textContent = entry.note;
|
||||
body.append(note);
|
||||
}
|
||||
tile.append(body, copyBtn(() => entry.cmd), deleteBtn(() => onDelete(i, j)));
|
||||
block.append(tile);
|
||||
});
|
||||
container.append(block);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
set(text: string) {
|
||||
const records: Record[] = [];
|
||||
for (const line of text.split("\n")) {
|
||||
if (!line.trim()) continue;
|
||||
records.push(JSON.parse(line));
|
||||
}
|
||||
count = records.filter((r) => r.commands?.length).length;
|
||||
render(records);
|
||||
},
|
||||
empty: () => count === 0,
|
||||
};
|
||||
}
|
||||
|
||||
export type PanelMode = "draft" | "commands" | "search" | "wiki";
|
||||
|
||||
const LABEL: { [m in PanelMode]: string } = {
|
||||
draft: "Dokument",
|
||||
commands: "Befehle",
|
||||
search: "Suche",
|
||||
wiki: "Wiki",
|
||||
};
|
||||
|
||||
/// Vier Tabs Entwurf / Befehle / Suche / Wiki: blenden Entwurfs-Inhalt samt
|
||||
/// zugehöriger Kopf-Controls gegen die jeweilige Ansicht aus. Ein Wechsel bei
|
||||
/// offener Inhalts-Bearbeitung speichert den Entwurf (flush) statt zu
|
||||
/// blockieren.
|
||||
export function initPanelMode(opts: {
|
||||
tabsEl: HTMLElement;
|
||||
draftEls: HTMLElement[];
|
||||
commandsContent: HTMLElement;
|
||||
searchContent: HTMLElement;
|
||||
wikiContent: HTMLElement;
|
||||
titleEl: HTMLElement;
|
||||
flush: () => void;
|
||||
}) {
|
||||
let mode: PanelMode = "draft";
|
||||
let draftTitle = "";
|
||||
const tabs = [...opts.tabsEl.querySelectorAll<HTMLElement>("[data-mode]")];
|
||||
|
||||
function apply() {
|
||||
opts.commandsContent.hidden = mode !== "commands";
|
||||
opts.searchContent.hidden = mode !== "search";
|
||||
opts.wikiContent.hidden = mode !== "wiki";
|
||||
for (const el of opts.draftEls) el.hidden = mode !== "draft";
|
||||
for (const t of tabs) t.classList.toggle("active", t.dataset.mode === mode);
|
||||
if (mode !== "draft") opts.titleEl.textContent = LABEL[mode];
|
||||
}
|
||||
|
||||
function to(m: PanelMode) {
|
||||
if (m === mode) return;
|
||||
opts.flush();
|
||||
if (mode === "draft") draftTitle = opts.titleEl.textContent || "Dokument";
|
||||
mode = m;
|
||||
apply();
|
||||
if (m === "draft") opts.titleEl.textContent = draftTitle;
|
||||
}
|
||||
|
||||
for (const t of tabs) t.addEventListener("click", () => to(t.dataset.mode as PanelMode));
|
||||
apply();
|
||||
return { to, current: () => mode };
|
||||
}
|
||||
@@ -0,0 +1,430 @@
|
||||
/* Kachel- und Wiki-Ansichten des Panels (Befehls-History, Archiv-Suchtreffer,
|
||||
Archiv-Wiki) — gemeinsam für das angedockte Panel (terminal.html) und das
|
||||
abgelöste Fenster (panel.html); von beiden Entry-Points importiert. */
|
||||
|
||||
#commands-content {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
padding: 12px 14px;
|
||||
}
|
||||
#commands-content::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
#commands-content::-webkit-scrollbar-thumb {
|
||||
background: #313244;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.cmd-sep {
|
||||
margin: 16px 0 8px;
|
||||
padding-top: 8px;
|
||||
border-top: 1px solid #313244;
|
||||
color: #6c7086;
|
||||
font:
|
||||
500 11px/1 -apple-system,
|
||||
system-ui,
|
||||
sans-serif;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
}
|
||||
.cmd-rec {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.cmd-rec-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
color: #6c7086;
|
||||
font:
|
||||
500 11px/1 -apple-system,
|
||||
system-ui,
|
||||
sans-serif;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
}
|
||||
.cmd-all {
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: #a6adc8;
|
||||
font: inherit;
|
||||
border-radius: 6px;
|
||||
padding: 4px 6px;
|
||||
}
|
||||
.cmd-all:hover {
|
||||
background: #313244;
|
||||
color: #cdd6f4;
|
||||
}
|
||||
.cmd-all.copied {
|
||||
color: #a6e3a1;
|
||||
}
|
||||
.cmd-del:hover {
|
||||
color: #f38ba8;
|
||||
}
|
||||
.cmd-tile {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 6px;
|
||||
background: #11111b;
|
||||
border: 1px solid #313244;
|
||||
border-radius: 8px;
|
||||
padding: 8px 6px 8px 10px;
|
||||
margin-top: 6px;
|
||||
}
|
||||
.cmd-body {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
.cmd-text {
|
||||
font-family: "JetBrains Mono", Menlo, monospace;
|
||||
font-size: 12px;
|
||||
line-height: 1.45;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
user-select: text;
|
||||
-webkit-user-select: text;
|
||||
cursor: text;
|
||||
}
|
||||
.cmd-note {
|
||||
margin-top: 4px;
|
||||
color: #a6adc8;
|
||||
font:
|
||||
400 11px/1.4 -apple-system,
|
||||
system-ui,
|
||||
sans-serif;
|
||||
}
|
||||
#search-content {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
padding: 12px 14px;
|
||||
}
|
||||
.hit-search {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.hit-search input {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
background: #11111b;
|
||||
border: 1px solid #313244;
|
||||
border-radius: 6px;
|
||||
padding: 6px 10px;
|
||||
color: #cdd6f4;
|
||||
font:
|
||||
400 12px/1.4 -apple-system,
|
||||
system-ui,
|
||||
sans-serif;
|
||||
outline: none;
|
||||
}
|
||||
.hit-search input:focus {
|
||||
border-color: #45475a;
|
||||
}
|
||||
.hit-search input::placeholder {
|
||||
color: #6c7086;
|
||||
}
|
||||
#search-content::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
#search-content::-webkit-scrollbar-thumb {
|
||||
background: #313244;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.hit-head {
|
||||
color: #6c7086;
|
||||
font:
|
||||
500 11px/1 -apple-system,
|
||||
system-ui,
|
||||
sans-serif;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
}
|
||||
.hit-tile {
|
||||
background: #11111b;
|
||||
border: 1px solid #313244;
|
||||
border-radius: 8px;
|
||||
padding: 8px 10px;
|
||||
margin-top: 6px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.hit-tile:hover {
|
||||
border-color: #45475a;
|
||||
}
|
||||
.hit-title {
|
||||
font:
|
||||
600 12px/1.4 -apple-system,
|
||||
system-ui,
|
||||
sans-serif;
|
||||
}
|
||||
.hit-snippet {
|
||||
margin-top: 4px;
|
||||
color: #a6adc8;
|
||||
font:
|
||||
400 11px/1.5 -apple-system,
|
||||
system-ui,
|
||||
sans-serif;
|
||||
}
|
||||
.hit-snippet mark {
|
||||
background: transparent;
|
||||
color: #f9e2af;
|
||||
font-weight: 600;
|
||||
}
|
||||
.hit-path {
|
||||
margin-top: 4px;
|
||||
color: #6c7086;
|
||||
font-family: "JetBrains Mono", Menlo, monospace;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
/* Wiki-Ansicht: Übersichts-/Schlagwort-Seiten und Dokumente. */
|
||||
#wiki-content {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
padding: 14px 16px;
|
||||
}
|
||||
#wiki-content::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
#wiki-content::-webkit-scrollbar-thumb {
|
||||
background: #313244;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.wiki-head {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
}
|
||||
.wiki-head-left {
|
||||
min-width: 0;
|
||||
}
|
||||
.wiki-head-title {
|
||||
color: #cdd6f4;
|
||||
font:
|
||||
600 13px/1.3 -apple-system,
|
||||
system-ui,
|
||||
sans-serif;
|
||||
}
|
||||
.wiki-head-sub {
|
||||
margin-top: 2px;
|
||||
color: #6c7086;
|
||||
font-family: "JetBrains Mono", Menlo, monospace;
|
||||
font-size: 10px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.wiki-head-right {
|
||||
color: #6c7086;
|
||||
font:
|
||||
500 11px/1 -apple-system,
|
||||
system-ui,
|
||||
sans-serif;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.wiki-chips {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
margin: 10px 0 4px;
|
||||
}
|
||||
.wiki-chip {
|
||||
border: 1px solid #313244;
|
||||
background: transparent;
|
||||
color: #a6adc8;
|
||||
border-radius: 999px;
|
||||
padding: 2px 8px;
|
||||
font:
|
||||
500 10.5px/1.5 -apple-system,
|
||||
system-ui,
|
||||
sans-serif;
|
||||
}
|
||||
.wiki-chip:hover {
|
||||
background: #313244;
|
||||
color: #cdd6f4;
|
||||
}
|
||||
.wiki-chip.active {
|
||||
background: #313244;
|
||||
border-color: #45475a;
|
||||
color: #89b4fa;
|
||||
}
|
||||
.wiki-folder {
|
||||
margin: 14px 0 2px;
|
||||
padding-top: 10px;
|
||||
border-top: 1px solid #313244;
|
||||
color: #6c7086;
|
||||
font:
|
||||
500 10px/1 -apple-system,
|
||||
system-ui,
|
||||
sans-serif;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
}
|
||||
.wiki-doc {
|
||||
padding: 7px 8px;
|
||||
margin: 4px -8px 0;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.wiki-doc:hover {
|
||||
background: #11111b;
|
||||
}
|
||||
.wiki-doc-line {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 8px;
|
||||
}
|
||||
.wiki-doc-title {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
color: #89b4fa;
|
||||
font:
|
||||
600 12px/1.4 -apple-system,
|
||||
system-ui,
|
||||
sans-serif;
|
||||
}
|
||||
.wiki-doc-date {
|
||||
color: #6c7086;
|
||||
font-family: "JetBrains Mono", Menlo, monospace;
|
||||
font-size: 10px;
|
||||
}
|
||||
.wiki-doc-desc {
|
||||
margin-top: 2px;
|
||||
color: #a6adc8;
|
||||
font:
|
||||
400 11px/1.5 -apple-system,
|
||||
system-ui,
|
||||
sans-serif;
|
||||
}
|
||||
.wiki-doc-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
.wiki-empty {
|
||||
margin-top: 18px;
|
||||
color: #a6adc8;
|
||||
font:
|
||||
400 12px/1.6 -apple-system,
|
||||
system-ui,
|
||||
sans-serif;
|
||||
}
|
||||
.wiki-empty strong {
|
||||
display: block;
|
||||
margin-bottom: 6px;
|
||||
color: #cdd6f4;
|
||||
font-weight: 600;
|
||||
}
|
||||
.wiki-doc-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.wiki-back {
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: #a6adc8;
|
||||
font:
|
||||
500 11px/1 -apple-system,
|
||||
system-ui,
|
||||
sans-serif;
|
||||
border-radius: 6px;
|
||||
padding: 4px 6px;
|
||||
}
|
||||
.wiki-back:hover {
|
||||
background: #313244;
|
||||
color: #cdd6f4;
|
||||
}
|
||||
.wiki-body {
|
||||
font-size: 12.5px;
|
||||
line-height: 1.55;
|
||||
user-select: text;
|
||||
-webkit-user-select: text;
|
||||
cursor: text;
|
||||
}
|
||||
.wiki-body h1,
|
||||
.wiki-body h2,
|
||||
.wiki-body h3 {
|
||||
line-height: 1.25;
|
||||
margin: 1em 0 0.4em;
|
||||
}
|
||||
.wiki-body h1 {
|
||||
font-size: 1.4em;
|
||||
}
|
||||
.wiki-body h2 {
|
||||
font-size: 1.2em;
|
||||
}
|
||||
.wiki-body h3 {
|
||||
font-size: 1.05em;
|
||||
}
|
||||
.wiki-body :first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
.wiki-body p,
|
||||
.wiki-body ul,
|
||||
.wiki-body ol,
|
||||
.wiki-body pre,
|
||||
.wiki-body blockquote {
|
||||
margin: 0 0 0.7em;
|
||||
}
|
||||
.wiki-body code {
|
||||
font-family: "JetBrains Mono", Menlo, monospace;
|
||||
font-size: 0.9em;
|
||||
background: #313244;
|
||||
border-radius: 4px;
|
||||
padding: 1px 5px;
|
||||
}
|
||||
.wiki-body pre {
|
||||
background: #11111b;
|
||||
border-radius: 8px;
|
||||
padding: 12px 14px;
|
||||
overflow: auto;
|
||||
}
|
||||
.wiki-body pre code {
|
||||
background: none;
|
||||
padding: 0;
|
||||
}
|
||||
.wiki-body blockquote {
|
||||
border-left: 3px solid #313244;
|
||||
padding-left: 12px;
|
||||
color: #a6adc8;
|
||||
}
|
||||
.wiki-body a {
|
||||
color: #89b4fa;
|
||||
}
|
||||
.wiki-backlinks {
|
||||
margin-top: 14px;
|
||||
padding-top: 8px;
|
||||
border-top: 1px solid #313244;
|
||||
color: #6c7086;
|
||||
font:
|
||||
500 11px/1.6 -apple-system,
|
||||
system-ui,
|
||||
sans-serif;
|
||||
}
|
||||
.wiki-backlinks a {
|
||||
color: #89b4fa;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
/* Sichtbare Fehlermeldung (panelToast), oben rechts über dem Panel-Inhalt. */
|
||||
.panel-toast {
|
||||
position: fixed;
|
||||
top: 44px;
|
||||
right: 12px;
|
||||
z-index: 10;
|
||||
max-width: 320px;
|
||||
background: #11111b;
|
||||
border: 1px solid #f38ba8;
|
||||
color: #f38ba8;
|
||||
border-radius: 8px;
|
||||
padding: 8px 12px;
|
||||
font:
|
||||
500 11px/1.5 -apple-system,
|
||||
system-ui,
|
||||
sans-serif;
|
||||
}
|
||||
+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(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
/// Gemeinsame Panel-Verdrahtung für das angedockte Panel (terminal.ts) und
|
||||
/// das abgelöste Fenster (panel.ts): Entwurfs-Ansicht, Befehls- und
|
||||
/// Such-Kacheln, Modus-Umschalter, Wikilink-Klick und die drei Update-Events.
|
||||
/// Die Element-IDs sind in beiden HTML-Dateien identisch; die Draft-Controls
|
||||
/// trägt das Markup als `.draft-only`.
|
||||
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { listen } from "@tauri-apps/api/event";
|
||||
import { initPanelView, type PanelView } from "./panel-view";
|
||||
import {
|
||||
initCommandsView,
|
||||
initPanelMode,
|
||||
panelToast,
|
||||
type CommandsView,
|
||||
type PanelMode,
|
||||
} from "./commands-view";
|
||||
import { initSearchView } from "./search-view";
|
||||
import { initWikiView } from "./wiki-view";
|
||||
import "./panel-tiles.css";
|
||||
|
||||
export interface PanelWiring {
|
||||
view: PanelView;
|
||||
cmdView: CommandsView;
|
||||
mode: { to(m: PanelMode): void; current(): PanelMode };
|
||||
/// Entwurfstext beim Start (für die Anfangs-Modus-Entscheidung).
|
||||
draft: string;
|
||||
}
|
||||
|
||||
export async function wirePanel(
|
||||
project: string,
|
||||
onIncoming?: () => void,
|
||||
): Promise<PanelWiring> {
|
||||
const titleEl = document.querySelector(".panel-title") as HTMLElement;
|
||||
const [defaultLang, draft, cmds, search, wiki] = await Promise.all([
|
||||
invoke<string>("spellcheck_lang"),
|
||||
invoke<string>("panel_read", { project }),
|
||||
invoke<string>("commands_read", { project }),
|
||||
invoke<string>("search_read", { project }),
|
||||
invoke<string>("wiki_read", { project }),
|
||||
]);
|
||||
|
||||
// Jeder Wiki-Sprung (Wikilink, Chip, Suchtreffer) geht als ein Invoke an den
|
||||
// Kern; das Ergebnis kommt über den Wiki-Puffer und `wiki-update` zurück.
|
||||
const openWiki = (name: string) => invoke("wiki_open", { project, name });
|
||||
|
||||
const view = initPanelView({
|
||||
content: document.getElementById("panel-content")!,
|
||||
copyBtn: document.getElementById("panel-copy")!,
|
||||
modeBtn: document.getElementById("panel-mode")!,
|
||||
titleEl,
|
||||
editBtn: document.getElementById("panel-title-edit")!,
|
||||
editContentBtn: document.getElementById("panel-content-edit")!,
|
||||
langSelect: document.getElementById("panel-lang") as HTMLSelectElement,
|
||||
defaultLang,
|
||||
onCommit: (text) => invoke("panel_set", { project, text }),
|
||||
onWikiLink: openWiki,
|
||||
});
|
||||
const cmdView = initCommandsView(document.getElementById("commands-content")!, (line, entry) =>
|
||||
invoke("commands_delete", { project, line, entry }),
|
||||
);
|
||||
const searchView = initSearchView(
|
||||
document.getElementById("search-content")!,
|
||||
(relpath) => openWiki(relpath.split("/").pop()!.replace(/\.md$/, "")),
|
||||
(query) =>
|
||||
void invoke("search_run", { project, query }).catch((e) =>
|
||||
panelToast(`Suche fehlgeschlagen: ${e}`),
|
||||
),
|
||||
);
|
||||
const wikiView = initWikiView(document.getElementById("wiki-content")!, openWiki);
|
||||
const mode = initPanelMode({
|
||||
tabsEl: document.getElementById("panel-tabs")!,
|
||||
draftEls: [
|
||||
document.getElementById("panel-content")!,
|
||||
...document.querySelectorAll<HTMLElement>(".draft-only"),
|
||||
],
|
||||
commandsContent: document.getElementById("commands-content")!,
|
||||
searchContent: document.getElementById("search-content")!,
|
||||
wikiContent: document.getElementById("wiki-content")!,
|
||||
titleEl,
|
||||
flush: () => void view.flush(),
|
||||
});
|
||||
|
||||
view.set(draft);
|
||||
cmdView.set(cmds);
|
||||
searchView.set(search);
|
||||
wikiView.set(wiki);
|
||||
|
||||
// Wiki-Tab bei leerem Puffer (Session-Start): Übersicht direkt laden.
|
||||
document
|
||||
.querySelector<HTMLElement>('#panel-tabs [data-mode="wiki"]')!
|
||||
.addEventListener("click", () => {
|
||||
if (wikiView.empty()) void openWiki("tag:");
|
||||
});
|
||||
|
||||
await listen<string>("panel-update", (e) => {
|
||||
view.set(e.payload);
|
||||
mode.to("draft");
|
||||
onIncoming?.();
|
||||
});
|
||||
await listen<string>("commands-update", (e) => {
|
||||
cmdView.set(e.payload);
|
||||
mode.to("commands");
|
||||
onIncoming?.();
|
||||
});
|
||||
await listen<string>("search-update", (e) => {
|
||||
searchView.set(e.payload);
|
||||
mode.to("search");
|
||||
onIncoming?.();
|
||||
});
|
||||
await listen<string>("wiki-update", (e) => {
|
||||
wikiView.set(e.payload);
|
||||
mode.to("wiki");
|
||||
onIncoming?.();
|
||||
});
|
||||
|
||||
return { view, cmdView, mode, draft };
|
||||
}
|
||||
+48
-23
@@ -2,36 +2,59 @@ 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, emit } from "@tauri-apps/api/event";
|
||||
import { initPanelView } from "./panel-view";
|
||||
import { emit } from "@tauri-apps/api/event";
|
||||
import { wirePanel } from "./panel-wiring";
|
||||
import { flash, panelToast } from "./commands-view";
|
||||
import { THEMES } from "./themes";
|
||||
|
||||
// Abgelöstes Panel-Fenster: liest den aktuellen Entwurf einmal ein und folgt
|
||||
// danach denselben `panel-update`-Events wie das angedockte Panel.
|
||||
// danach denselben Update-Events wie das angedockte Panel; startet in
|
||||
// „Befehle", wenn es (noch) keinen Entwurf gibt.
|
||||
const project = new URLSearchParams(location.search).get("project")!;
|
||||
const win = getCurrentWebviewWindow();
|
||||
|
||||
const view = initPanelView({
|
||||
content: document.getElementById("panel-content")!,
|
||||
copyBtn: document.getElementById("panel-copy")!,
|
||||
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 }),
|
||||
});
|
||||
// Dekorationsloses Fenster (Linux): Plattform-Flag + Resize-Zonen wie im
|
||||
// Terminal-Fenster; macOS behält die native Deko.
|
||||
const isMac = /Mac|Macintosh/.test(navigator.userAgent);
|
||||
document.documentElement.dataset.platform = isMac ? "mac" : "other";
|
||||
if (!isMac) {
|
||||
document
|
||||
.getElementById("win-min")!
|
||||
.addEventListener("click", () => win.minimize());
|
||||
document
|
||||
.getElementById("win-max")!
|
||||
.addEventListener("click", () => win.toggleMaximize());
|
||||
for (const g of document.querySelectorAll<HTMLElement>(".grip")) {
|
||||
g.addEventListener("mousedown", (e) => {
|
||||
e.preventDefault();
|
||||
win.startResizeDragging(g.dataset.dir as never);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
view.set(await invoke<string>("panel_read", { project }));
|
||||
// Farben ans Theme koppeln — derselbe Look wie das angedockte Panel im
|
||||
// Terminal-Fenster (die CSS-Defaults sind Mocha).
|
||||
interface Project {
|
||||
name: string;
|
||||
terminal: { theme: string | null };
|
||||
}
|
||||
const projects = await invoke<Project[]>("list_projects");
|
||||
const picked = THEMES[projects.find((p) => p.name === project)?.terminal.theme ?? "mocha"];
|
||||
document.documentElement.style.background = picked.header;
|
||||
document.body.style.background = picked.header;
|
||||
document.body.style.color = picked.xterm.foreground;
|
||||
const topbar = document.querySelector(".panel-topbar") as HTMLElement;
|
||||
topbar.style.background = picked.header;
|
||||
topbar.style.borderBottomColor = picked.border;
|
||||
const head = document.querySelector(".panel-head") as HTMLElement;
|
||||
head.style.background = picked.header;
|
||||
head.style.borderBottomColor = picked.border;
|
||||
|
||||
await listen<string>("panel-update", (e) => view.set(e.payload));
|
||||
const { view, cmdView, mode, draft } = await wirePanel(project);
|
||||
if (!draft.trim() && !cmdView.empty()) mode.to("commands");
|
||||
|
||||
// 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 () => {
|
||||
try {
|
||||
const configured = await invoke<string | null>("panel_archive_dir_cmd", {
|
||||
@@ -44,11 +67,13 @@ archiveBtn.addEventListener("click", async () => {
|
||||
if (!chosen) return;
|
||||
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 });
|
||||
flashBtn(archiveBtn, "copied");
|
||||
flash(archiveBtn, "copied", 1400);
|
||||
invoke("reveal_path_cmd", { path });
|
||||
} catch {
|
||||
flashBtn(archiveBtn, "error");
|
||||
} catch (e) {
|
||||
flash(archiveBtn, "error", 1400);
|
||||
panelToast(`Archivieren fehlgeschlagen: ${e}`);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
/// Kachel-Ansicht der Archiv-Suchtreffer: rendert die Suchtreffer-Datei
|
||||
/// (search_archive im MCP-Server) als klickbare Kacheln — Klick reicht den
|
||||
/// relpath des Treffers an onOpen. Titel/Snippet/Pfad sind Fremdtext und
|
||||
/// gehen nie durch innerHTML; die `**…**`-Marker im Snippet werden per
|
||||
/// Split in <mark>-Elemente übersetzt.
|
||||
|
||||
interface Hit {
|
||||
relpath: string;
|
||||
title: string;
|
||||
snippet: string;
|
||||
}
|
||||
|
||||
interface SearchRun {
|
||||
query: string;
|
||||
tag?: string | null;
|
||||
home: string;
|
||||
hits: Hit[];
|
||||
}
|
||||
|
||||
export interface SearchView {
|
||||
set(text: string): void;
|
||||
empty(): boolean;
|
||||
}
|
||||
|
||||
export function initSearchView(
|
||||
container: HTMLElement,
|
||||
onOpen: (relpath: string) => void,
|
||||
onSearch: (query: string) => void,
|
||||
): SearchView {
|
||||
let count = 0;
|
||||
|
||||
// Suchfeld oben, Treffer darunter; das Feld bleibt über Updates hinweg stehen.
|
||||
const bar = document.createElement("div");
|
||||
bar.className = "hit-search";
|
||||
const input = document.createElement("input");
|
||||
input.type = "search";
|
||||
input.placeholder = "Archiv durchsuchen — Enter startet";
|
||||
input.addEventListener("keydown", (e) => {
|
||||
if (e.key === "Enter" && input.value.trim()) onSearch(input.value.trim());
|
||||
});
|
||||
bar.append(input);
|
||||
const results = document.createElement("div");
|
||||
container.append(bar, results);
|
||||
|
||||
function snippetEl(snippet: string): HTMLElement {
|
||||
const div = document.createElement("div");
|
||||
div.className = "hit-snippet";
|
||||
snippet.split("**").forEach((part, i) => {
|
||||
if (i % 2) {
|
||||
const m = document.createElement("mark");
|
||||
m.textContent = part;
|
||||
div.append(m);
|
||||
} else if (part) {
|
||||
div.append(document.createTextNode(part));
|
||||
}
|
||||
});
|
||||
return div;
|
||||
}
|
||||
|
||||
function render(run: SearchRun) {
|
||||
results.textContent = "";
|
||||
const head = document.createElement("div");
|
||||
head.className = "hit-head";
|
||||
const what = run.query.trim() ? `„${run.query}“` : "";
|
||||
const tag = run.tag ? `#${run.tag}` : "";
|
||||
head.textContent = `${run.hits.length} Treffer ${[what, tag].filter(Boolean).join(" · ")}`;
|
||||
results.append(head);
|
||||
for (const hit of run.hits) {
|
||||
const tile = document.createElement("div");
|
||||
tile.className = "hit-tile";
|
||||
const title = document.createElement("div");
|
||||
title.className = "hit-title";
|
||||
title.textContent = hit.title;
|
||||
const path = document.createElement("div");
|
||||
path.className = "hit-path";
|
||||
path.textContent = hit.relpath;
|
||||
tile.append(title, snippetEl(hit.snippet), path);
|
||||
tile.addEventListener("click", () => onOpen(hit.relpath));
|
||||
results.append(tile);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
set(text: string) {
|
||||
if (!text.trim()) {
|
||||
count = 0;
|
||||
results.textContent = "";
|
||||
return;
|
||||
}
|
||||
const run: SearchRun = JSON.parse(text);
|
||||
count = run.hits.length;
|
||||
if (document.activeElement !== input) input.value = run.query;
|
||||
render(run);
|
||||
},
|
||||
empty: () => count === 0,
|
||||
};
|
||||
}
|
||||
+17
-378
@@ -10,7 +10,9 @@ import "@fontsource/jetbrains-mono/700.css";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow";
|
||||
import { listen } from "@tauri-apps/api/event";
|
||||
import { initPanelView } from "./panel-view";
|
||||
import { wirePanel } from "./panel-wiring";
|
||||
import { THEMES } from "./themes";
|
||||
import { flash, panelToast } from "./commands-view";
|
||||
|
||||
// Debug-Instrumentierung: Fehler auf der Seite anzeigen und ins Dev-Log spiegeln.
|
||||
function showError(msg: string) {
|
||||
@@ -57,362 +59,6 @@ if (!isMac) {
|
||||
// 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).
|
||||
const THEMES: Record<
|
||||
string,
|
||||
{ header: string; border: string; xterm: Record<string, string> }
|
||||
> = {
|
||||
mocha: {
|
||||
header: "#181825",
|
||||
border: "#313244",
|
||||
xterm: {
|
||||
background: "#1e1e2e",
|
||||
foreground: "#cdd6f4",
|
||||
cursor: "#f5e0dc",
|
||||
cursorAccent: "#1e1e2e",
|
||||
selectionBackground: "#585b7080",
|
||||
black: "#45475a",
|
||||
red: "#f38ba8",
|
||||
green: "#a6e3a1",
|
||||
yellow: "#f9e2af",
|
||||
blue: "#89b4fa",
|
||||
magenta: "#f5c2e7",
|
||||
cyan: "#94e2d5",
|
||||
white: "#bac2de",
|
||||
brightBlack: "#585b70",
|
||||
brightRed: "#f38ba8",
|
||||
brightGreen: "#a6e3a1",
|
||||
brightYellow: "#f9e2af",
|
||||
brightBlue: "#89b4fa",
|
||||
brightMagenta: "#f5c2e7",
|
||||
brightCyan: "#94e2d5",
|
||||
brightWhite: "#a6adc8",
|
||||
},
|
||||
},
|
||||
dracula: {
|
||||
header: "#21222c",
|
||||
border: "#44475a",
|
||||
xterm: {
|
||||
background: "#282a36",
|
||||
foreground: "#f8f8f2",
|
||||
cursor: "#f8f8f2",
|
||||
cursorAccent: "#282a36",
|
||||
selectionBackground: "#44475a80",
|
||||
black: "#21222c",
|
||||
red: "#ff5555",
|
||||
green: "#50fa7b",
|
||||
yellow: "#f1fa8c",
|
||||
blue: "#bd93f9",
|
||||
magenta: "#ff79c6",
|
||||
cyan: "#8be9fd",
|
||||
white: "#f8f8f2",
|
||||
brightBlack: "#6272a4",
|
||||
brightRed: "#ff6e6e",
|
||||
brightGreen: "#69ff94",
|
||||
brightYellow: "#ffffa5",
|
||||
brightBlue: "#d6acff",
|
||||
brightMagenta: "#ff92df",
|
||||
brightCyan: "#a4ffff",
|
||||
brightWhite: "#ffffff",
|
||||
},
|
||||
},
|
||||
"solarized-dark": {
|
||||
header: "#073642",
|
||||
border: "#586e75",
|
||||
xterm: {
|
||||
background: "#002b36",
|
||||
foreground: "#839496",
|
||||
cursor: "#839496",
|
||||
cursorAccent: "#002b36",
|
||||
selectionBackground: "#07364280",
|
||||
black: "#073642",
|
||||
red: "#dc322f",
|
||||
green: "#859900",
|
||||
yellow: "#b58900",
|
||||
blue: "#268bd2",
|
||||
magenta: "#d33682",
|
||||
cyan: "#2aa198",
|
||||
white: "#eee8d5",
|
||||
brightBlack: "#586e75",
|
||||
brightRed: "#cb4b16",
|
||||
brightGreen: "#859900",
|
||||
brightYellow: "#657b83",
|
||||
brightBlue: "#839496",
|
||||
brightMagenta: "#6c71c4",
|
||||
brightCyan: "#93a1a1",
|
||||
brightWhite: "#fdf6e3",
|
||||
},
|
||||
},
|
||||
gruvbox: {
|
||||
header: "#1d2021",
|
||||
border: "#3c3836",
|
||||
xterm: {
|
||||
background: "#282828",
|
||||
foreground: "#ebdbb2",
|
||||
cursor: "#ebdbb2",
|
||||
cursorAccent: "#282828",
|
||||
selectionBackground: "#3c383680",
|
||||
black: "#282828",
|
||||
red: "#cc241d",
|
||||
green: "#98971a",
|
||||
yellow: "#d79921",
|
||||
blue: "#458588",
|
||||
magenta: "#b16286",
|
||||
cyan: "#689d6a",
|
||||
white: "#a89984",
|
||||
brightBlack: "#928374",
|
||||
brightRed: "#fb4934",
|
||||
brightGreen: "#b8bb26",
|
||||
brightYellow: "#fabd2f",
|
||||
brightBlue: "#83a598",
|
||||
brightMagenta: "#d3869b",
|
||||
brightCyan: "#8ec07c",
|
||||
brightWhite: "#ebdbb2",
|
||||
},
|
||||
},
|
||||
"one-dark": {
|
||||
header: "#21252b",
|
||||
border: "#3e4451",
|
||||
xterm: {
|
||||
background: "#282c34",
|
||||
foreground: "#abb2bf",
|
||||
cursor: "#abb2bf",
|
||||
cursorAccent: "#282c34",
|
||||
selectionBackground: "#3e445180",
|
||||
black: "#282c34",
|
||||
red: "#e06c75",
|
||||
green: "#98c379",
|
||||
yellow: "#e5c07b",
|
||||
blue: "#61afef",
|
||||
magenta: "#c678dd",
|
||||
cyan: "#56b6c2",
|
||||
white: "#abb2bf",
|
||||
brightBlack: "#5c6370",
|
||||
brightRed: "#e06c75",
|
||||
brightGreen: "#98c379",
|
||||
brightYellow: "#e5c07b",
|
||||
brightBlue: "#61afef",
|
||||
brightMagenta: "#c678dd",
|
||||
brightCyan: "#56b6c2",
|
||||
brightWhite: "#ffffff",
|
||||
},
|
||||
},
|
||||
"solarized-light": {
|
||||
header: "#eee8d5",
|
||||
border: "#93a1a1",
|
||||
xterm: {
|
||||
background: "#fdf6e3",
|
||||
foreground: "#657b83",
|
||||
cursor: "#657b83",
|
||||
cursorAccent: "#fdf6e3",
|
||||
selectionBackground: "#eee8d5b0",
|
||||
black: "#073642",
|
||||
red: "#dc322f",
|
||||
green: "#859900",
|
||||
yellow: "#b58900",
|
||||
blue: "#268bd2",
|
||||
magenta: "#d33682",
|
||||
cyan: "#2aa198",
|
||||
white: "#eee8d5",
|
||||
brightBlack: "#002b36",
|
||||
brightRed: "#cb4b16",
|
||||
brightGreen: "#586e75",
|
||||
brightYellow: "#657b83",
|
||||
brightBlue: "#839496",
|
||||
brightMagenta: "#6c71c4",
|
||||
brightCyan: "#93a1a1",
|
||||
brightWhite: "#fdf6e3",
|
||||
},
|
||||
},
|
||||
"catppuccin-latte": {
|
||||
header: "#e6e9ef",
|
||||
border: "#ccd0da",
|
||||
xterm: {
|
||||
background: "#eff1f5",
|
||||
foreground: "#4c4f69",
|
||||
cursor: "#dc8a78",
|
||||
cursorAccent: "#eff1f5",
|
||||
selectionBackground: "#acb0be80",
|
||||
black: "#5c5f77",
|
||||
red: "#d20f39",
|
||||
green: "#40a02b",
|
||||
yellow: "#df8e1d",
|
||||
blue: "#1e66f5",
|
||||
magenta: "#ea76cb",
|
||||
cyan: "#179299",
|
||||
white: "#acb0be",
|
||||
brightBlack: "#6c6f85",
|
||||
brightRed: "#d20f39",
|
||||
brightGreen: "#40a02b",
|
||||
brightYellow: "#df8e1d",
|
||||
brightBlue: "#1e66f5",
|
||||
brightMagenta: "#ea76cb",
|
||||
brightCyan: "#179299",
|
||||
brightWhite: "#bcc0cc",
|
||||
},
|
||||
},
|
||||
"one-light": {
|
||||
header: "#eaeaeb",
|
||||
border: "#d4d4d4",
|
||||
xterm: {
|
||||
background: "#fafafa",
|
||||
foreground: "#383a42",
|
||||
cursor: "#526eff",
|
||||
cursorAccent: "#fafafa",
|
||||
selectionBackground: "#e5e5e6b0",
|
||||
black: "#383a42",
|
||||
red: "#e45649",
|
||||
green: "#50a14f",
|
||||
yellow: "#c18401",
|
||||
blue: "#4078f2",
|
||||
magenta: "#a626a4",
|
||||
cyan: "#0184bc",
|
||||
white: "#a0a1a7",
|
||||
brightBlack: "#696c77",
|
||||
brightRed: "#e45649",
|
||||
brightGreen: "#50a14f",
|
||||
brightYellow: "#c18401",
|
||||
brightBlue: "#4078f2",
|
||||
brightMagenta: "#a626a4",
|
||||
brightCyan: "#0184bc",
|
||||
brightWhite: "#ffffff",
|
||||
},
|
||||
},
|
||||
nord: {
|
||||
header: "#3b4252",
|
||||
border: "#4c566a",
|
||||
xterm: {
|
||||
background: "#2e3440",
|
||||
foreground: "#d8dee9",
|
||||
cursor: "#d8dee9",
|
||||
cursorAccent: "#2e3440",
|
||||
selectionBackground: "#434c5e80",
|
||||
black: "#3b4252",
|
||||
red: "#bf616a",
|
||||
green: "#a3be8c",
|
||||
yellow: "#ebcb8b",
|
||||
blue: "#81a1c1",
|
||||
magenta: "#b48ead",
|
||||
cyan: "#88c0d0",
|
||||
white: "#e5e9f0",
|
||||
brightBlack: "#4c566a",
|
||||
brightRed: "#bf616a",
|
||||
brightGreen: "#a3be8c",
|
||||
brightYellow: "#ebcb8b",
|
||||
brightBlue: "#81a1c1",
|
||||
brightMagenta: "#b48ead",
|
||||
brightCyan: "#8fbcbb",
|
||||
brightWhite: "#eceff4",
|
||||
},
|
||||
},
|
||||
"tokyo-night": {
|
||||
header: "#16161e",
|
||||
border: "#292e42",
|
||||
xterm: {
|
||||
background: "#1a1b26",
|
||||
foreground: "#c0caf5",
|
||||
cursor: "#c0caf5",
|
||||
cursorAccent: "#1a1b26",
|
||||
selectionBackground: "#28345780",
|
||||
black: "#15161e",
|
||||
red: "#f7768e",
|
||||
green: "#9ece6a",
|
||||
yellow: "#e0af68",
|
||||
blue: "#7aa2f7",
|
||||
magenta: "#bb9af7",
|
||||
cyan: "#7dcfff",
|
||||
white: "#a9b1d6",
|
||||
brightBlack: "#414868",
|
||||
brightRed: "#f7768e",
|
||||
brightGreen: "#9ece6a",
|
||||
brightYellow: "#e0af68",
|
||||
brightBlue: "#7aa2f7",
|
||||
brightMagenta: "#bb9af7",
|
||||
brightCyan: "#7dcfff",
|
||||
brightWhite: "#c0caf5",
|
||||
},
|
||||
},
|
||||
monokai: {
|
||||
header: "#1e1f1c",
|
||||
border: "#49483e",
|
||||
xterm: {
|
||||
background: "#272822",
|
||||
foreground: "#f8f8f2",
|
||||
cursor: "#f8f8f2",
|
||||
cursorAccent: "#272822",
|
||||
selectionBackground: "#49483e80",
|
||||
black: "#272822",
|
||||
red: "#f92672",
|
||||
green: "#a6e22e",
|
||||
yellow: "#e6db74",
|
||||
blue: "#66d9ef",
|
||||
magenta: "#ae81ff",
|
||||
cyan: "#a1efe4",
|
||||
white: "#f8f8f2",
|
||||
brightBlack: "#75715e",
|
||||
brightRed: "#f92672",
|
||||
brightGreen: "#a6e22e",
|
||||
brightYellow: "#f4bf75",
|
||||
brightBlue: "#66d9ef",
|
||||
brightMagenta: "#ae81ff",
|
||||
brightCyan: "#a1efe4",
|
||||
brightWhite: "#f9f8f5",
|
||||
},
|
||||
},
|
||||
"rose-pine": {
|
||||
header: "#1f1d2e",
|
||||
border: "#26233a",
|
||||
xterm: {
|
||||
background: "#191724",
|
||||
foreground: "#e0def4",
|
||||
cursor: "#e0def4",
|
||||
cursorAccent: "#191724",
|
||||
selectionBackground: "#403d5280",
|
||||
black: "#26233a",
|
||||
red: "#eb6f92",
|
||||
green: "#31748f",
|
||||
yellow: "#f6c177",
|
||||
blue: "#9ccfd8",
|
||||
magenta: "#c4a7e7",
|
||||
cyan: "#ebbcba",
|
||||
white: "#e0def4",
|
||||
brightBlack: "#6e6a86",
|
||||
brightRed: "#eb6f92",
|
||||
brightGreen: "#31748f",
|
||||
brightYellow: "#f6c177",
|
||||
brightBlue: "#9ccfd8",
|
||||
brightMagenta: "#c4a7e7",
|
||||
brightCyan: "#ebbcba",
|
||||
brightWhite: "#e0def4",
|
||||
},
|
||||
},
|
||||
everforest: {
|
||||
header: "#232a2e",
|
||||
border: "#475258",
|
||||
xterm: {
|
||||
background: "#2d353b",
|
||||
foreground: "#d3c6aa",
|
||||
cursor: "#d3c6aa",
|
||||
cursorAccent: "#2d353b",
|
||||
selectionBackground: "#47525880",
|
||||
black: "#475258",
|
||||
red: "#e67e80",
|
||||
green: "#a7c080",
|
||||
yellow: "#dbbc7f",
|
||||
blue: "#7fbbb3",
|
||||
magenta: "#d699b6",
|
||||
cyan: "#83c092",
|
||||
white: "#d3c6aa",
|
||||
brightBlack: "#859289",
|
||||
brightRed: "#e67e80",
|
||||
brightGreen: "#a7c080",
|
||||
brightYellow: "#dbbc7f",
|
||||
brightBlue: "#7fbbb3",
|
||||
brightMagenta: "#d699b6",
|
||||
brightCyan: "#83c092",
|
||||
brightWhite: "#d3c6aa",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
interface Project {
|
||||
name: string;
|
||||
@@ -538,7 +184,6 @@ term.focus();
|
||||
// ein eigenes Fenster abgelöst (`panel-detached`).
|
||||
const panel = document.getElementById("panel")!;
|
||||
const splitter = document.getElementById("splitter")!;
|
||||
const panelContent = document.getElementById("panel-content")!;
|
||||
|
||||
// Panelfarben ans Theme koppeln.
|
||||
panel.style.background = picked.header;
|
||||
@@ -547,18 +192,6 @@ splitter.style.background = picked.border;
|
||||
(panel.querySelector(".panel-head") as HTMLElement).style.borderBottomColor =
|
||||
picked.border;
|
||||
|
||||
const view = initPanelView({
|
||||
content: panelContent,
|
||||
copyBtn: document.getElementById("panel-copy")!,
|
||||
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 }),
|
||||
});
|
||||
|
||||
let detached = false;
|
||||
let hasContent = false;
|
||||
|
||||
@@ -573,11 +206,19 @@ function hidePanel() {
|
||||
fit.fit();
|
||||
}
|
||||
|
||||
await listen<string>("panel-update", (e) => {
|
||||
view.set(e.payload);
|
||||
// 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, () => {
|
||||
hasContent = true;
|
||||
if (!detached) showPanel();
|
||||
});
|
||||
|
||||
// Tab-Klick im Header öffnet das (angedockte) Panel, falls es zu ist.
|
||||
document.getElementById("panel-tabs")!.addEventListener("click", () => {
|
||||
hasContent = true;
|
||||
if (!detached) showPanel();
|
||||
});
|
||||
|
||||
await listen("panel-detached", () => {
|
||||
detached = true;
|
||||
hidePanel();
|
||||
@@ -599,10 +240,6 @@ 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 () => {
|
||||
try {
|
||||
const configured = await invoke<string | null>("panel_archive_dir_cmd", {
|
||||
@@ -615,11 +252,13 @@ archiveBtn.addEventListener("click", async () => {
|
||||
if (!chosen) return;
|
||||
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 });
|
||||
flashBtn(archiveBtn, "copied");
|
||||
flash(archiveBtn, "copied", 1400);
|
||||
invoke("reveal_path_cmd", { path });
|
||||
} catch (e) {
|
||||
flashBtn(archiveBtn, "error");
|
||||
flash(archiveBtn, "error", 1400);
|
||||
panelToast(`Archivieren fehlgeschlagen: ${e}`);
|
||||
invoke("term_log", { msg: `archive: ${e}` });
|
||||
}
|
||||
});
|
||||
|
||||
+359
@@ -0,0 +1,359 @@
|
||||
/// Farbschemata der Fenster: Header-/Rahmenfarbe plus xterm-Theme —
|
||||
/// genutzt vom Terminal-Fenster (Header, angedocktes Panel) und vom
|
||||
/// abgelösten Panel-Fenster (panel.ts), damit beide gleich aussehen.
|
||||
export const THEMES: Record<
|
||||
string,
|
||||
{ header: string; border: string; xterm: Record<string, string> }
|
||||
> = {
|
||||
mocha: {
|
||||
header: "#181825",
|
||||
border: "#313244",
|
||||
xterm: {
|
||||
background: "#1e1e2e",
|
||||
foreground: "#cdd6f4",
|
||||
cursor: "#f5e0dc",
|
||||
cursorAccent: "#1e1e2e",
|
||||
selectionBackground: "#585b7080",
|
||||
black: "#45475a",
|
||||
red: "#f38ba8",
|
||||
green: "#a6e3a1",
|
||||
yellow: "#f9e2af",
|
||||
blue: "#89b4fa",
|
||||
magenta: "#f5c2e7",
|
||||
cyan: "#94e2d5",
|
||||
white: "#bac2de",
|
||||
brightBlack: "#585b70",
|
||||
brightRed: "#f38ba8",
|
||||
brightGreen: "#a6e3a1",
|
||||
brightYellow: "#f9e2af",
|
||||
brightBlue: "#89b4fa",
|
||||
brightMagenta: "#f5c2e7",
|
||||
brightCyan: "#94e2d5",
|
||||
brightWhite: "#a6adc8",
|
||||
},
|
||||
},
|
||||
dracula: {
|
||||
header: "#21222c",
|
||||
border: "#44475a",
|
||||
xterm: {
|
||||
background: "#282a36",
|
||||
foreground: "#f8f8f2",
|
||||
cursor: "#f8f8f2",
|
||||
cursorAccent: "#282a36",
|
||||
selectionBackground: "#44475a80",
|
||||
black: "#21222c",
|
||||
red: "#ff5555",
|
||||
green: "#50fa7b",
|
||||
yellow: "#f1fa8c",
|
||||
blue: "#bd93f9",
|
||||
magenta: "#ff79c6",
|
||||
cyan: "#8be9fd",
|
||||
white: "#f8f8f2",
|
||||
brightBlack: "#6272a4",
|
||||
brightRed: "#ff6e6e",
|
||||
brightGreen: "#69ff94",
|
||||
brightYellow: "#ffffa5",
|
||||
brightBlue: "#d6acff",
|
||||
brightMagenta: "#ff92df",
|
||||
brightCyan: "#a4ffff",
|
||||
brightWhite: "#ffffff",
|
||||
},
|
||||
},
|
||||
"solarized-dark": {
|
||||
header: "#073642",
|
||||
border: "#586e75",
|
||||
xterm: {
|
||||
background: "#002b36",
|
||||
foreground: "#839496",
|
||||
cursor: "#839496",
|
||||
cursorAccent: "#002b36",
|
||||
selectionBackground: "#07364280",
|
||||
black: "#073642",
|
||||
red: "#dc322f",
|
||||
green: "#859900",
|
||||
yellow: "#b58900",
|
||||
blue: "#268bd2",
|
||||
magenta: "#d33682",
|
||||
cyan: "#2aa198",
|
||||
white: "#eee8d5",
|
||||
brightBlack: "#586e75",
|
||||
brightRed: "#cb4b16",
|
||||
brightGreen: "#859900",
|
||||
brightYellow: "#657b83",
|
||||
brightBlue: "#839496",
|
||||
brightMagenta: "#6c71c4",
|
||||
brightCyan: "#93a1a1",
|
||||
brightWhite: "#fdf6e3",
|
||||
},
|
||||
},
|
||||
gruvbox: {
|
||||
header: "#1d2021",
|
||||
border: "#3c3836",
|
||||
xterm: {
|
||||
background: "#282828",
|
||||
foreground: "#ebdbb2",
|
||||
cursor: "#ebdbb2",
|
||||
cursorAccent: "#282828",
|
||||
selectionBackground: "#3c383680",
|
||||
black: "#282828",
|
||||
red: "#cc241d",
|
||||
green: "#98971a",
|
||||
yellow: "#d79921",
|
||||
blue: "#458588",
|
||||
magenta: "#b16286",
|
||||
cyan: "#689d6a",
|
||||
white: "#a89984",
|
||||
brightBlack: "#928374",
|
||||
brightRed: "#fb4934",
|
||||
brightGreen: "#b8bb26",
|
||||
brightYellow: "#fabd2f",
|
||||
brightBlue: "#83a598",
|
||||
brightMagenta: "#d3869b",
|
||||
brightCyan: "#8ec07c",
|
||||
brightWhite: "#ebdbb2",
|
||||
},
|
||||
},
|
||||
"one-dark": {
|
||||
header: "#21252b",
|
||||
border: "#3e4451",
|
||||
xterm: {
|
||||
background: "#282c34",
|
||||
foreground: "#abb2bf",
|
||||
cursor: "#abb2bf",
|
||||
cursorAccent: "#282c34",
|
||||
selectionBackground: "#3e445180",
|
||||
black: "#282c34",
|
||||
red: "#e06c75",
|
||||
green: "#98c379",
|
||||
yellow: "#e5c07b",
|
||||
blue: "#61afef",
|
||||
magenta: "#c678dd",
|
||||
cyan: "#56b6c2",
|
||||
white: "#abb2bf",
|
||||
brightBlack: "#5c6370",
|
||||
brightRed: "#e06c75",
|
||||
brightGreen: "#98c379",
|
||||
brightYellow: "#e5c07b",
|
||||
brightBlue: "#61afef",
|
||||
brightMagenta: "#c678dd",
|
||||
brightCyan: "#56b6c2",
|
||||
brightWhite: "#ffffff",
|
||||
},
|
||||
},
|
||||
"solarized-light": {
|
||||
header: "#eee8d5",
|
||||
border: "#93a1a1",
|
||||
xterm: {
|
||||
background: "#fdf6e3",
|
||||
foreground: "#657b83",
|
||||
cursor: "#657b83",
|
||||
cursorAccent: "#fdf6e3",
|
||||
selectionBackground: "#eee8d5b0",
|
||||
black: "#073642",
|
||||
red: "#dc322f",
|
||||
green: "#859900",
|
||||
yellow: "#b58900",
|
||||
blue: "#268bd2",
|
||||
magenta: "#d33682",
|
||||
cyan: "#2aa198",
|
||||
white: "#eee8d5",
|
||||
brightBlack: "#002b36",
|
||||
brightRed: "#cb4b16",
|
||||
brightGreen: "#586e75",
|
||||
brightYellow: "#657b83",
|
||||
brightBlue: "#839496",
|
||||
brightMagenta: "#6c71c4",
|
||||
brightCyan: "#93a1a1",
|
||||
brightWhite: "#fdf6e3",
|
||||
},
|
||||
},
|
||||
"catppuccin-latte": {
|
||||
header: "#e6e9ef",
|
||||
border: "#ccd0da",
|
||||
xterm: {
|
||||
background: "#eff1f5",
|
||||
foreground: "#4c4f69",
|
||||
cursor: "#dc8a78",
|
||||
cursorAccent: "#eff1f5",
|
||||
selectionBackground: "#acb0be80",
|
||||
black: "#5c5f77",
|
||||
red: "#d20f39",
|
||||
green: "#40a02b",
|
||||
yellow: "#df8e1d",
|
||||
blue: "#1e66f5",
|
||||
magenta: "#ea76cb",
|
||||
cyan: "#179299",
|
||||
white: "#acb0be",
|
||||
brightBlack: "#6c6f85",
|
||||
brightRed: "#d20f39",
|
||||
brightGreen: "#40a02b",
|
||||
brightYellow: "#df8e1d",
|
||||
brightBlue: "#1e66f5",
|
||||
brightMagenta: "#ea76cb",
|
||||
brightCyan: "#179299",
|
||||
brightWhite: "#bcc0cc",
|
||||
},
|
||||
},
|
||||
"one-light": {
|
||||
header: "#eaeaeb",
|
||||
border: "#d4d4d4",
|
||||
xterm: {
|
||||
background: "#fafafa",
|
||||
foreground: "#383a42",
|
||||
cursor: "#526eff",
|
||||
cursorAccent: "#fafafa",
|
||||
selectionBackground: "#e5e5e6b0",
|
||||
black: "#383a42",
|
||||
red: "#e45649",
|
||||
green: "#50a14f",
|
||||
yellow: "#c18401",
|
||||
blue: "#4078f2",
|
||||
magenta: "#a626a4",
|
||||
cyan: "#0184bc",
|
||||
white: "#a0a1a7",
|
||||
brightBlack: "#696c77",
|
||||
brightRed: "#e45649",
|
||||
brightGreen: "#50a14f",
|
||||
brightYellow: "#c18401",
|
||||
brightBlue: "#4078f2",
|
||||
brightMagenta: "#a626a4",
|
||||
brightCyan: "#0184bc",
|
||||
brightWhite: "#ffffff",
|
||||
},
|
||||
},
|
||||
nord: {
|
||||
header: "#3b4252",
|
||||
border: "#4c566a",
|
||||
xterm: {
|
||||
background: "#2e3440",
|
||||
foreground: "#d8dee9",
|
||||
cursor: "#d8dee9",
|
||||
cursorAccent: "#2e3440",
|
||||
selectionBackground: "#434c5e80",
|
||||
black: "#3b4252",
|
||||
red: "#bf616a",
|
||||
green: "#a3be8c",
|
||||
yellow: "#ebcb8b",
|
||||
blue: "#81a1c1",
|
||||
magenta: "#b48ead",
|
||||
cyan: "#88c0d0",
|
||||
white: "#e5e9f0",
|
||||
brightBlack: "#4c566a",
|
||||
brightRed: "#bf616a",
|
||||
brightGreen: "#a3be8c",
|
||||
brightYellow: "#ebcb8b",
|
||||
brightBlue: "#81a1c1",
|
||||
brightMagenta: "#b48ead",
|
||||
brightCyan: "#8fbcbb",
|
||||
brightWhite: "#eceff4",
|
||||
},
|
||||
},
|
||||
"tokyo-night": {
|
||||
header: "#16161e",
|
||||
border: "#292e42",
|
||||
xterm: {
|
||||
background: "#1a1b26",
|
||||
foreground: "#c0caf5",
|
||||
cursor: "#c0caf5",
|
||||
cursorAccent: "#1a1b26",
|
||||
selectionBackground: "#28345780",
|
||||
black: "#15161e",
|
||||
red: "#f7768e",
|
||||
green: "#9ece6a",
|
||||
yellow: "#e0af68",
|
||||
blue: "#7aa2f7",
|
||||
magenta: "#bb9af7",
|
||||
cyan: "#7dcfff",
|
||||
white: "#a9b1d6",
|
||||
brightBlack: "#414868",
|
||||
brightRed: "#f7768e",
|
||||
brightGreen: "#9ece6a",
|
||||
brightYellow: "#e0af68",
|
||||
brightBlue: "#7aa2f7",
|
||||
brightMagenta: "#bb9af7",
|
||||
brightCyan: "#7dcfff",
|
||||
brightWhite: "#c0caf5",
|
||||
},
|
||||
},
|
||||
monokai: {
|
||||
header: "#1e1f1c",
|
||||
border: "#49483e",
|
||||
xterm: {
|
||||
background: "#272822",
|
||||
foreground: "#f8f8f2",
|
||||
cursor: "#f8f8f2",
|
||||
cursorAccent: "#272822",
|
||||
selectionBackground: "#49483e80",
|
||||
black: "#272822",
|
||||
red: "#f92672",
|
||||
green: "#a6e22e",
|
||||
yellow: "#e6db74",
|
||||
blue: "#66d9ef",
|
||||
magenta: "#ae81ff",
|
||||
cyan: "#a1efe4",
|
||||
white: "#f8f8f2",
|
||||
brightBlack: "#75715e",
|
||||
brightRed: "#f92672",
|
||||
brightGreen: "#a6e22e",
|
||||
brightYellow: "#f4bf75",
|
||||
brightBlue: "#66d9ef",
|
||||
brightMagenta: "#ae81ff",
|
||||
brightCyan: "#a1efe4",
|
||||
brightWhite: "#f9f8f5",
|
||||
},
|
||||
},
|
||||
"rose-pine": {
|
||||
header: "#1f1d2e",
|
||||
border: "#26233a",
|
||||
xterm: {
|
||||
background: "#191724",
|
||||
foreground: "#e0def4",
|
||||
cursor: "#e0def4",
|
||||
cursorAccent: "#191724",
|
||||
selectionBackground: "#403d5280",
|
||||
black: "#26233a",
|
||||
red: "#eb6f92",
|
||||
green: "#31748f",
|
||||
yellow: "#f6c177",
|
||||
blue: "#9ccfd8",
|
||||
magenta: "#c4a7e7",
|
||||
cyan: "#ebbcba",
|
||||
white: "#e0def4",
|
||||
brightBlack: "#6e6a86",
|
||||
brightRed: "#eb6f92",
|
||||
brightGreen: "#31748f",
|
||||
brightYellow: "#f6c177",
|
||||
brightBlue: "#9ccfd8",
|
||||
brightMagenta: "#c4a7e7",
|
||||
brightCyan: "#ebbcba",
|
||||
brightWhite: "#e0def4",
|
||||
},
|
||||
},
|
||||
everforest: {
|
||||
header: "#232a2e",
|
||||
border: "#475258",
|
||||
xterm: {
|
||||
background: "#2d353b",
|
||||
foreground: "#d3c6aa",
|
||||
cursor: "#d3c6aa",
|
||||
cursorAccent: "#2d353b",
|
||||
selectionBackground: "#47525880",
|
||||
black: "#475258",
|
||||
red: "#e67e80",
|
||||
green: "#a7c080",
|
||||
yellow: "#dbbc7f",
|
||||
blue: "#7fbbb3",
|
||||
magenta: "#d699b6",
|
||||
cyan: "#83c092",
|
||||
white: "#d3c6aa",
|
||||
brightBlack: "#859289",
|
||||
brightRed: "#e67e80",
|
||||
brightGreen: "#a7c080",
|
||||
brightYellow: "#dbbc7f",
|
||||
brightBlue: "#7fbbb3",
|
||||
brightMagenta: "#d699b6",
|
||||
brightCyan: "#83c092",
|
||||
brightWhite: "#d3c6aa",
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,226 @@
|
||||
/// Wiki-Ansicht des Panels: rendert den Wiki-Puffer (show_archive/wiki_open).
|
||||
/// Übersichts- und Schlagwort-Seiten kommen als strukturierte Daten (Kopfzeile,
|
||||
/// Schlagwort-Chips, Ordner-Sektionen mit Dokumentzeilen), Dokumente als
|
||||
/// gerendertes Markdown mit Backlinks. Titel, Beschreibungen und Pfade sind
|
||||
/// Fremdtext und gehen nie durch innerHTML; nur der Markdown-Rumpf läuft wie
|
||||
/// im Entwurf durch marked.
|
||||
|
||||
import { marked } from "marked";
|
||||
import { linkWikiRefs } from "./panel-view";
|
||||
|
||||
interface DocEntry {
|
||||
name: string;
|
||||
title: string;
|
||||
description?: string | null;
|
||||
tags: string[];
|
||||
date?: string | null;
|
||||
}
|
||||
|
||||
interface Folder {
|
||||
name: string;
|
||||
docs: DocEntry[];
|
||||
}
|
||||
|
||||
interface Page {
|
||||
kind: "page";
|
||||
home: string;
|
||||
tag?: string | null;
|
||||
total: number;
|
||||
tags: { name: string; count: number }[];
|
||||
folders: Folder[];
|
||||
}
|
||||
|
||||
interface DocPage {
|
||||
kind: "doc";
|
||||
home: string;
|
||||
relpath: string;
|
||||
name: string;
|
||||
title: string;
|
||||
tags: string[];
|
||||
backlinks: string[];
|
||||
markdown: string;
|
||||
}
|
||||
|
||||
export interface WikiView {
|
||||
set(text: string): void;
|
||||
/// Noch keine Seite im Puffer (Session-Start)?
|
||||
empty(): boolean;
|
||||
}
|
||||
|
||||
export function initWikiView(
|
||||
container: HTMLElement,
|
||||
onLink: (name: string) => void,
|
||||
): WikiView {
|
||||
function chip(label: string, target: string, active = false): HTMLElement {
|
||||
const b = document.createElement("button");
|
||||
b.className = "wiki-chip" + (active ? " active" : "");
|
||||
b.textContent = label;
|
||||
b.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
onLink(target);
|
||||
});
|
||||
return b;
|
||||
}
|
||||
|
||||
/// Kopfzeile einer Seite: Titel + Home-Pfad links, Dokumentzahl rechts.
|
||||
function pageHead(p: Page): HTMLElement {
|
||||
const head = document.createElement("div");
|
||||
head.className = "wiki-head";
|
||||
const left = document.createElement("div");
|
||||
left.className = "wiki-head-left";
|
||||
const title = document.createElement("div");
|
||||
title.className = "wiki-head-title";
|
||||
title.textContent = p.tag ? `#${p.tag}` : "Archiv";
|
||||
const sub = document.createElement("div");
|
||||
sub.className = "wiki-head-sub";
|
||||
sub.textContent = p.home;
|
||||
left.append(title, sub);
|
||||
const count = document.createElement("div");
|
||||
count.className = "wiki-head-right";
|
||||
count.textContent = `${p.total} ${p.total === 1 ? "Dokument" : "Dokumente"}`;
|
||||
head.append(left, count);
|
||||
return head;
|
||||
}
|
||||
|
||||
/// Schlagwort-Leiste: „Alle“ plus ein Chip pro Schlagwort mit Zähler; der
|
||||
/// aktive Filter ist markiert.
|
||||
function tagBar(p: Page): HTMLElement {
|
||||
const bar = document.createElement("div");
|
||||
bar.className = "wiki-chips";
|
||||
bar.append(chip("Alle", "tag:", !p.tag));
|
||||
for (const t of p.tags) {
|
||||
bar.append(chip(`#${t.name} ${t.count}`, `tag:${t.name}`, p.tag === t.name));
|
||||
}
|
||||
return bar;
|
||||
}
|
||||
|
||||
function docRow(doc: DocEntry): HTMLElement {
|
||||
const row = document.createElement("div");
|
||||
row.className = "wiki-doc";
|
||||
const line = document.createElement("div");
|
||||
line.className = "wiki-doc-line";
|
||||
const title = document.createElement("div");
|
||||
title.className = "wiki-doc-title";
|
||||
title.textContent = doc.title;
|
||||
line.append(title);
|
||||
if (doc.date) {
|
||||
const date = document.createElement("div");
|
||||
date.className = "wiki-doc-date";
|
||||
date.textContent = doc.date;
|
||||
line.append(date);
|
||||
}
|
||||
row.append(line);
|
||||
if (doc.description) {
|
||||
const desc = document.createElement("div");
|
||||
desc.className = "wiki-doc-desc";
|
||||
desc.textContent = doc.description;
|
||||
row.append(desc);
|
||||
}
|
||||
if (doc.tags.length) {
|
||||
const tags = document.createElement("div");
|
||||
tags.className = "wiki-doc-tags";
|
||||
for (const t of doc.tags) tags.append(chip(`#${t}`, `tag:${t}`));
|
||||
row.append(tags);
|
||||
}
|
||||
row.addEventListener("click", () => onLink(doc.name));
|
||||
return row;
|
||||
}
|
||||
|
||||
function renderPage(p: Page) {
|
||||
container.append(pageHead(p));
|
||||
if (p.tags.length) container.append(tagBar(p));
|
||||
if (p.total === 0) {
|
||||
const empty = document.createElement("div");
|
||||
empty.className = "wiki-empty";
|
||||
const line = document.createElement("strong");
|
||||
line.textContent = p.tag ? `Keine Dokumente mit #${p.tag}.` : "Das Archiv ist leer.";
|
||||
empty.append(line);
|
||||
if (!p.tag) {
|
||||
empty.append(
|
||||
"Archivieren: Archiv-Button im Entwurf oder „archiviere das“ im Chat — mit Ordner, Beschreibung und Schlagwörtern.",
|
||||
);
|
||||
}
|
||||
container.append(empty);
|
||||
return;
|
||||
}
|
||||
for (const folder of p.folders) {
|
||||
if (folder.name) {
|
||||
const eyebrow = document.createElement("div");
|
||||
eyebrow.className = "wiki-folder";
|
||||
eyebrow.textContent = `${folder.name}/`;
|
||||
container.append(eyebrow);
|
||||
}
|
||||
for (const doc of folder.docs) container.append(docRow(doc));
|
||||
}
|
||||
}
|
||||
|
||||
function renderDoc(d: DocPage) {
|
||||
const head = document.createElement("div");
|
||||
head.className = "wiki-doc-head";
|
||||
const back = document.createElement("button");
|
||||
back.className = "wiki-back";
|
||||
back.textContent = "‹ Archiv";
|
||||
back.addEventListener("click", () => onLink("tag:"));
|
||||
const path = document.createElement("div");
|
||||
path.className = "wiki-head-sub";
|
||||
path.textContent = d.relpath;
|
||||
head.append(back, path);
|
||||
container.append(head);
|
||||
if (d.tags.length) {
|
||||
const tags = document.createElement("div");
|
||||
tags.className = "wiki-chips";
|
||||
for (const t of d.tags) tags.append(chip(`#${t}`, `tag:${t}`));
|
||||
container.append(tags);
|
||||
}
|
||||
const body = document.createElement("div");
|
||||
body.className = "wiki-body";
|
||||
body.innerHTML = marked.parse(d.markdown, { async: false });
|
||||
linkWikiRefs(body, onLink);
|
||||
container.append(body);
|
||||
if (d.backlinks.length) {
|
||||
const back = document.createElement("div");
|
||||
back.className = "wiki-backlinks";
|
||||
back.append("Verweise hierher: ");
|
||||
d.backlinks.forEach((name, i) => {
|
||||
if (i) back.append(" · ");
|
||||
const a = document.createElement("a");
|
||||
a.href = "#";
|
||||
a.className = "wiki";
|
||||
a.textContent = name;
|
||||
a.addEventListener("click", (e) => {
|
||||
e.preventDefault();
|
||||
onLink(name);
|
||||
});
|
||||
back.append(a);
|
||||
});
|
||||
container.append(back);
|
||||
}
|
||||
}
|
||||
|
||||
/// Leerer Puffer: Einstieg statt leerer Fläche.
|
||||
function renderIntro() {
|
||||
const empty = document.createElement("div");
|
||||
empty.className = "wiki-empty";
|
||||
const line = document.createElement("strong");
|
||||
line.textContent = "Keine Wiki-Seite geladen.";
|
||||
empty.append(line);
|
||||
empty.append(chip("Archiv-Übersicht öffnen", "tag:"));
|
||||
container.append(empty);
|
||||
}
|
||||
|
||||
let loaded = false;
|
||||
return {
|
||||
set(text: string) {
|
||||
container.textContent = "";
|
||||
loaded = !!text.trim();
|
||||
if (!loaded) {
|
||||
renderIntro();
|
||||
return;
|
||||
}
|
||||
const data: Page | DocPage = JSON.parse(text);
|
||||
if (data.kind === "doc") renderDoc(data);
|
||||
else renderPage(data);
|
||||
},
|
||||
empty: () => !loaded,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user