Fix: Resizable Panels — Drag-Overlay verhindert Text-Selektion

- Unsichtbares Fullscreen-Overlay während Drag fängt alle Maus-Events
- document.body.style.userSelect blockiert sofort bei mousedown
- Bestehende Selektion wird beim Drag-Start gelöscht
- Handle 5px breit + unsichtbarer 15px Greifbereich (::before)
- Visueller Grip-Indikator beim Hover

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Eddy 2026-04-13 19:17:44 +02:00
parent 92353e2852
commit a0ad11b66c

View file

@ -24,7 +24,6 @@
// ============ Resizable Panels ============ // ============ Resizable Panels ============
// Breiten in Pixel (werden zu fr umgerechnet)
let panelWidths = [220, 400, 400, 380]; let panelWidths = [220, 400, 400, 380];
let container: HTMLDivElement; let container: HTMLDivElement;
let dragging: number | null = null; let dragging: number | null = null;
@ -32,10 +31,9 @@
let startWidths: number[] = []; let startWidths: number[] = [];
function getGridTemplate(): string { function getGridTemplate(): string {
// 4 Panels + 3 Handles dazwischen: panel handle panel handle panel handle panel
return panelWidths.map((w, i) => { return panelWidths.map((w, i) => {
if (i < panelWidths.length - 1) { if (i < panelWidths.length - 1) {
return `${w}px 3px`; return `${w}px 5px`;
} }
return `${w}px`; return `${w}px`;
}).join(' '); }).join(' ');
@ -43,17 +41,30 @@
function onMouseDown(index: number, event: MouseEvent) { function onMouseDown(index: number, event: MouseEvent) {
event.preventDefault(); event.preventDefault();
event.stopPropagation();
dragging = index; dragging = index;
startX = event.clientX; startX = event.clientX;
startWidths = [...panelWidths]; startWidths = [...panelWidths];
// Sofort Text-Selektion blockieren auf dem ganzen Dokument
document.body.style.userSelect = 'none';
document.body.style.webkitUserSelect = 'none';
document.body.style.cursor = 'col-resize';
// Bestehende Selektion löschen
window.getSelection()?.removeAllRanges();
window.addEventListener('mousemove', onMouseMove); window.addEventListener('mousemove', onMouseMove);
window.addEventListener('mouseup', onMouseUp); window.addEventListener('mouseup', onMouseUp);
} }
function onMouseMove(event: MouseEvent) { function onMouseMove(event: MouseEvent) {
if (dragging === null) return; if (dragging === null) return;
event.preventDefault();
const dx = event.clientX - startX; const dx = event.clientX - startX;
const minWidth = 120; const minWidth = 100;
const leftIdx = dragging; const leftIdx = dragging;
const rightIdx = dragging + 1; const rightIdx = dragging + 1;
@ -61,7 +72,6 @@
let newLeft = startWidths[leftIdx] + dx; let newLeft = startWidths[leftIdx] + dx;
let newRight = startWidths[rightIdx] - dx; let newRight = startWidths[rightIdx] - dx;
// Mindestbreiten einhalten
if (newLeft < minWidth) { if (newLeft < minWidth) {
newRight -= (minWidth - newLeft); newRight -= (minWidth - newLeft);
newLeft = minWidth; newLeft = minWidth;
@ -73,22 +83,26 @@
panelWidths[leftIdx] = Math.max(minWidth, newLeft); panelWidths[leftIdx] = Math.max(minWidth, newLeft);
panelWidths[rightIdx] = Math.max(minWidth, newRight); panelWidths[rightIdx] = Math.max(minWidth, newRight);
panelWidths = panelWidths; // Svelte reactivity panelWidths = panelWidths;
} }
function onMouseUp() { function onMouseUp() {
dragging = null; dragging = null;
// Selektion wieder erlauben
document.body.style.userSelect = '';
document.body.style.webkitUserSelect = '';
document.body.style.cursor = '';
window.removeEventListener('mousemove', onMouseMove); window.removeEventListener('mousemove', onMouseMove);
window.removeEventListener('mouseup', onMouseUp); window.removeEventListener('mouseup', onMouseUp);
// Breiten in localStorage speichern
try { try {
localStorage.setItem('panel-widths', JSON.stringify(panelWidths)); localStorage.setItem('panel-widths', JSON.stringify(panelWidths));
} catch {} } catch {}
} }
onMount(() => { onMount(() => {
// Gespeicherte Breiten laden
try { try {
const saved = localStorage.getItem('panel-widths'); const saved = localStorage.getItem('panel-widths');
if (saved) { if (saved) {
@ -99,10 +113,10 @@
} }
} catch {} } catch {}
// Initiale Breiten an Fenster anpassen wenn zu breit // Breiten an Fenster anpassen
if (container) { if (container) {
const total = panelWidths.reduce((a, b) => a + b, 0); const total = panelWidths.reduce((a, b) => a + b, 0);
const available = container.clientWidth - 9; // 3 Handles à 3px const available = container.clientWidth - 15; // 3 Handles à 5px
if (total > available) { if (total > available) {
const scale = available / total; const scale = available / total;
panelWidths = panelWidths.map(w => Math.round(w * scale)); panelWidths = panelWidths.map(w => Math.round(w * scale));
@ -118,21 +132,23 @@
style="grid-template-columns: {getGridTemplate()}" style="grid-template-columns: {getGridTemplate()}"
> >
<!-- Session-Sidebar --> <!-- Session-Sidebar -->
<aside class="panel panel-sessions"> <aside class="panel">
<SessionList /> <SessionList />
</aside> </aside>
<div class="resize-handle" on:mousedown={(e) => onMouseDown(0, e)} role="separator" aria-orientation="vertical"></div> <!-- svelte-ignore a11y_no_static_element_interactions -->
<div class="handle" on:mousedown={(e) => onMouseDown(0, e)}><div class="handle-grip"></div></div>
<!-- Chat --> <!-- Chat -->
<section class="panel panel-chat"> <section class="panel">
<ChatPanel /> <ChatPanel />
</section> </section>
<div class="resize-handle" on:mousedown={(e) => onMouseDown(1, e)} role="separator" aria-orientation="vertical"></div> <!-- svelte-ignore a11y_no_static_element_interactions -->
<div class="handle" on:mousedown={(e) => onMouseDown(1, e)}><div class="handle-grip"></div></div>
<!-- Aktivität / Memory / Audit --> <!-- Aktivität / Memory / Audit -->
<section class="panel panel-activity"> <section class="panel">
<div class="panel-tabs"> <div class="panel-tabs">
{#each middleTabs as tab} {#each middleTabs as tab}
<button <button
@ -155,10 +171,11 @@
</div> </div>
</section> </section>
<div class="resize-handle" on:mousedown={(e) => onMouseDown(2, e)} role="separator" aria-orientation="vertical"></div> <!-- svelte-ignore a11y_no_static_element_interactions -->
<div class="handle" on:mousedown={(e) => onMouseDown(2, e)}><div class="handle-grip"></div></div>
<!-- Agents / Guard-Rails --> <!-- Agents / Guard-Rails -->
<section class="panel panel-details"> <section class="panel">
<div class="panel-tabs"> <div class="panel-tabs">
{#each rightTabs as tab} {#each rightTabs as tab}
<button <button
@ -180,17 +197,19 @@
</section> </section>
</div> </div>
<!-- Unsichtbares Overlay während Drag — fängt alle Maus-Events ab -->
{#if dragging !== null}
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div class="drag-overlay" on:mousemove={onMouseMove} on:mouseup={onMouseUp}></div>
{/if}
<style> <style>
.panels { .panels {
display: grid; display: grid;
gap: 0; gap: 0;
height: 100%; height: 100%;
background: var(--bg-primary); background: var(--bg-primary);
} position: relative;
.panels.dragging {
cursor: col-resize;
user-select: none;
} }
.panel { .panel {
@ -201,20 +220,53 @@
min-width: 0; min-width: 0;
} }
/* Resize Handle */ /* Resize Handle — 5px breit, mit unsichtbarem 15px Greifbereich */
.resize-handle { .handle {
width: 3px; width: 5px;
cursor: col-resize; cursor: col-resize;
background: var(--border); background: var(--border);
transition: background 0.15s ease; position: relative;
z-index: 10; z-index: 10;
display: flex;
align-items: center;
justify-content: center;
} }
.resize-handle:hover, .handle::before {
.panels.dragging .resize-handle { content: '';
position: absolute;
top: 0;
bottom: 0;
left: -5px;
right: -5px;
z-index: 11;
}
.handle:hover {
background: var(--accent); background: var(--accent);
} }
.handle-grip {
width: 1px;
height: 30px;
background: var(--text-secondary);
border-radius: 1px;
opacity: 0;
transition: opacity 0.15s;
}
.handle:hover .handle-grip {
opacity: 0.6;
}
/* Drag-Overlay — ganzer Bildschirm, unsichtbar, fängt Maus-Events */
.drag-overlay {
position: fixed;
inset: 0;
z-index: 9999;
cursor: col-resize;
}
/* Tabs */ /* Tabs */
.panel-tabs { .panel-tabs {
display: flex; display: flex;