8aa758ae64
- Andockbares Panel im Terminal-Fenster: Markdown-Rendering, Copy-Button, normale Selektion; in eigenes Fenster ablösbar und wieder andockbar (Splitter, panel-detached/panel-attached). - MCP-stdio-Server (app --mcp-panel) mit Tool write_panel; schreibt in die Datei aus AI_CONTROL_PANEL, Watcher zieht sie ins Panel. - Provisionierung je Pool: Server-Registrierung in .claude.json (alwaysLoad), Freigabe mcp__text-panel__write_panel in settings.json, Panel-Skill; bei App-Start (alle Pools) und Pool-Anlage. Alte tee-Freigaben werden entfernt. - Capability deckt panel-*-Fenster ab.
604 lines
17 KiB
TypeScript
604 lines
17 KiB
TypeScript
import { Terminal } from "@xterm/xterm";
|
|
import { FitAddon } from "@xterm/addon-fit";
|
|
import { WebglAddon } from "@xterm/addon-webgl";
|
|
import { LigaturesAddon } from "@xterm/addon-ligatures";
|
|
import "@xterm/xterm/css/xterm.css";
|
|
import "@fontsource/jetbrains-mono/400.css";
|
|
import "@fontsource/jetbrains-mono/500.css";
|
|
import "@fontsource/jetbrains-mono/600.css";
|
|
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";
|
|
|
|
// Debug-Instrumentierung: Fehler auf der Seite anzeigen und ins Dev-Log spiegeln.
|
|
function showError(msg: string) {
|
|
const el = document.createElement("pre");
|
|
el.style.cssText =
|
|
"color:#f38ba8;padding:16px;font:12px Menlo,monospace;white-space:pre-wrap";
|
|
el.textContent = msg;
|
|
document.body.appendChild(el);
|
|
invoke("term_log", { msg });
|
|
}
|
|
window.addEventListener("error", (e) =>
|
|
showError(`error: ${e.message}\n${e.error?.stack ?? ""}`),
|
|
);
|
|
window.addEventListener("unhandledrejection", (e) =>
|
|
showError(`rejection: ${e.reason}\n${e.reason?.stack ?? ""}`),
|
|
);
|
|
|
|
const project = new URLSearchParams(location.search).get("project")!;
|
|
const win = getCurrentWebviewWindow();
|
|
|
|
// macOS nutzt die native Ampel (Overlay-Titelbar); sonst eigene Fensterknöpfe
|
|
// im Header (Linux ist dekorationslos, siehe terminal.rs).
|
|
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());
|
|
document
|
|
.getElementById("win-close")
|
|
?.addEventListener("click", () => win.close());
|
|
// Resize-Zonen: dekorationslose Fenster haben keinen nativen Rand.
|
|
for (const g of document.querySelectorAll<HTMLElement>(".grip")) {
|
|
g.addEventListener("mousedown", (e) => {
|
|
e.preventDefault();
|
|
win.startResizeDragging(g.dataset.dir as never);
|
|
});
|
|
}
|
|
}
|
|
|
|
// 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;
|
|
pool: string | null;
|
|
terminal: { theme: string | null; icon: string | null; title: string | null };
|
|
}
|
|
const projects = await invoke<Project[]>("list_projects");
|
|
const cfg = projects.find((p) => p.name === project);
|
|
if (cfg?.pool) {
|
|
// Anzeigename statt Pool-ID (Ordnername).
|
|
document.getElementById("pool")!.textContent = await invoke<string>(
|
|
"pool_label",
|
|
{ pool: cfg.pool },
|
|
);
|
|
}
|
|
document.getElementById("project-name")!.textContent =
|
|
cfg?.terminal.title ?? project;
|
|
|
|
// Projekt-Icon vor den Titel — dieselbe data-URL wie in der Projektliste.
|
|
if (cfg?.terminal.icon) {
|
|
const data = await invoke<string | null>("project_icon", { project });
|
|
if (data) {
|
|
const img = document.getElementById("project-icon") as HTMLImageElement;
|
|
img.src = data;
|
|
img.hidden = false;
|
|
}
|
|
}
|
|
|
|
const picked = THEMES[cfg?.terminal.theme ?? "mocha"];
|
|
const theme = picked.xterm;
|
|
|
|
// Seitenfarben ans Theme anpassen (Defaults in terminal.html sind Mocha).
|
|
document.documentElement.style.background = theme.background;
|
|
document.body.style.background = theme.background;
|
|
const header = document.querySelector("header") as HTMLElement;
|
|
header.style.background = picked.header;
|
|
header.style.borderBottomColor = picked.border;
|
|
header.style.color = theme.foreground;
|
|
|
|
// Globale Schriftgröße (settings.json: terminalFontSize), ein Wert für alle.
|
|
const DEFAULT_FONT_SIZE = 13;
|
|
let fontSize = await invoke<number>("terminal_font_size");
|
|
|
|
const term = new Terminal({
|
|
// Ligatur-Addon nutzt die Character-Joiner-API (Proposed API)
|
|
allowProposedApi: true,
|
|
fontFamily: '"JetBrains Mono", Menlo, monospace',
|
|
fontSize,
|
|
fontWeightBold: "600",
|
|
lineHeight: 1.0,
|
|
letterSpacing: 0,
|
|
cursorBlink: true,
|
|
cursorStyle: "bar",
|
|
scrollback: 10000,
|
|
theme,
|
|
});
|
|
const fit = new FitAddon();
|
|
term.loadAddon(fit);
|
|
|
|
// Ctrl/Cmd + +/-/0: Schriftgröße global anpassen, sofort im aktuellen Fenster.
|
|
function applyFontSize(px: number) {
|
|
fontSize = Math.max(8, Math.min(24, px));
|
|
term.options.fontSize = fontSize;
|
|
fit.fit();
|
|
invoke("set_terminal_font_size", { size: fontSize });
|
|
}
|
|
|
|
// Shift+Enter als CSI-u-Sequenz senden (Zeilenumbruch in Claude Code,
|
|
// auch bei nicht-leerer Zeile). Neben keydown muss auch keypress unter-
|
|
// drückt werden, sonst sendet xterm.js zusätzlich \r und submittet damit.
|
|
term.attachCustomKeyEventHandler((e) => {
|
|
if (e.key === "Enter" && e.shiftKey) {
|
|
if (e.type === "keydown") invoke("term_write", { data: "\x1b[13;2u" });
|
|
return false;
|
|
}
|
|
if ((e.ctrlKey || e.metaKey) && !e.altKey) {
|
|
if (e.key === "+" || e.key === "=") {
|
|
if (e.type === "keydown") applyFontSize(fontSize + 1);
|
|
return false;
|
|
}
|
|
if (e.key === "-" || e.key === "_") {
|
|
if (e.type === "keydown") applyFontSize(fontSize - 1);
|
|
return false;
|
|
}
|
|
if (e.key === "0") {
|
|
if (e.type === "keydown") applyFontSize(DEFAULT_FONT_SIZE);
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
});
|
|
|
|
// Font muss geladen sein, bevor WebGL den Glyphen-Atlas aufbaut —
|
|
// beide verwendeten Gewichte (400 normal, 600 bold).
|
|
await document.fonts.load(`${fontSize}px "JetBrains Mono"`);
|
|
await document.fonts.load(`600 ${fontSize}px "JetBrains Mono"`);
|
|
term.open(document.getElementById("term")!);
|
|
// Reihenfolge laut Addon-Doku: Ligaturen vor WebGL aktivieren.
|
|
term.loadAddon(new LigaturesAddon());
|
|
term.loadAddon(new WebglAddon());
|
|
fit.fit();
|
|
|
|
function b64ToBytes(b64: string): Uint8Array {
|
|
const bin = atob(b64);
|
|
const bytes = new Uint8Array(bin.length);
|
|
for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
|
|
return bytes;
|
|
}
|
|
|
|
await win.listen<string>("pty-output", (e) => term.write(b64ToBytes(e.payload)));
|
|
await win.listen("pty-exit", () => term.write("\r\n\x1b[90m[Prozess beendet]\x1b[0m\r\n"));
|
|
|
|
term.onData((data) => invoke("term_write", { data }));
|
|
term.onResize(({ rows, cols }) => invoke("term_resize", { rows, cols }));
|
|
window.addEventListener("resize", () => fit.fit());
|
|
|
|
await invoke("term_start", { project, rows: term.rows, cols: term.cols });
|
|
term.focus();
|
|
|
|
// --- Andockbares Entwurfs-Panel -------------------------------------------
|
|
// Der Skill schreibt Entwürfe in eine Datei; terminal.rs meldet neuen Inhalt
|
|
// per `panel-update`. Das Panel blendet sich dann ein — außer es ist gerade in
|
|
// 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;
|
|
panel.style.color = theme.foreground;
|
|
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")!,
|
|
});
|
|
|
|
let detached = false;
|
|
let hasContent = false;
|
|
|
|
function showPanel() {
|
|
panel.hidden = false;
|
|
splitter.hidden = false;
|
|
fit.fit();
|
|
}
|
|
function hidePanel() {
|
|
panel.hidden = true;
|
|
splitter.hidden = true;
|
|
fit.fit();
|
|
}
|
|
|
|
await listen<string>("panel-update", (e) => {
|
|
view.set(e.payload);
|
|
hasContent = true;
|
|
if (!detached) showPanel();
|
|
});
|
|
await listen("panel-detached", () => {
|
|
detached = true;
|
|
hidePanel();
|
|
});
|
|
await listen("panel-attached", () => {
|
|
detached = false;
|
|
if (hasContent) showPanel();
|
|
});
|
|
|
|
document
|
|
.getElementById("panel-detach")!
|
|
.addEventListener("click", () => invoke("open_panel_window", { project }));
|
|
document.getElementById("panel-hide")!.addEventListener("click", hidePanel);
|
|
|
|
// Splitter: Panelbreite ziehen.
|
|
splitter.addEventListener("mousedown", (e) => {
|
|
e.preventDefault();
|
|
const move = (ev: MouseEvent) => {
|
|
const w = Math.max(240, Math.min(window.innerWidth - 200, window.innerWidth - ev.clientX));
|
|
panel.style.width = `${w}px`;
|
|
fit.fit();
|
|
};
|
|
const up = () => {
|
|
window.removeEventListener("mousemove", move);
|
|
window.removeEventListener("mouseup", up);
|
|
};
|
|
window.addEventListener("mousemove", move);
|
|
window.addEventListener("mouseup", up);
|
|
});
|