Fix: Resize-Handles als absolute Overlays statt Grid-Spalten

Komplett neuer Ansatz: Handles sind absolut positionierte 8px-Elemente
über den Panel-Grenzen statt schmale Grid-Spalten. Behebt das Problem
dass WebKitGTK Mouse-Events auf engen Grid-Zellen nicht registriert.

- Handles position:absolute über kumulierten Panel-Breiten
- 8px breit, transparent, wird blau beim Hover
- Drag-Overlay (position:fixed) fängt Events während Drag
- document.addEventListener statt window für bessere Kompatibilität

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

View file

@ -26,80 +26,75 @@
let panelWidths = [220, 400, 400, 380]; let panelWidths = [220, 400, 400, 380];
let container: HTMLDivElement; let container: HTMLDivElement;
let dragging: number | null = null; let draggingIdx: number | null = null;
let startX = 0; let dragStartX = 0;
let startWidths: number[] = []; let dragStartWidths: number[] = [];
// Panel-Refs für Position-Berechnung
let panelRefs: HTMLElement[] = [];
function getGridTemplate(): string { function getGridTemplate(): string {
return panelWidths.map((w, i) => { return panelWidths.map(w => `${w}px`).join(' ');
if (i < panelWidths.length - 1) {
return `${w}px 5px`;
}
return `${w}px`;
}).join(' ');
} }
function onMouseDown(index: number, event: MouseEvent) { // Handle-Positionen berechnen (kumulierte Breiten)
function getHandlePositions(): number[] {
const positions: number[] = [];
let cumulative = 0;
for (let i = 0; i < panelWidths.length - 1; i++) {
cumulative += panelWidths[i];
positions.push(cumulative);
}
return positions;
}
function startDrag(index: number, event: MouseEvent) {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
dragging = index; draggingIdx = index;
startX = event.clientX; dragStartX = event.clientX;
startWidths = [...panelWidths]; dragStartWidths = [...panelWidths];
// Sofort Text-Selektion blockieren auf dem ganzen Dokument // Text-Selektion sofort blockieren
document.body.style.userSelect = 'none'; document.body.style.userSelect = 'none';
document.body.style.webkitUserSelect = 'none';
document.body.style.cursor = 'col-resize'; document.body.style.cursor = 'col-resize';
// Bestehende Selektion löschen
window.getSelection()?.removeAllRanges(); window.getSelection()?.removeAllRanges();
window.addEventListener('mousemove', onMouseMove); document.addEventListener('mousemove', onDrag);
window.addEventListener('mouseup', onMouseUp); document.addEventListener('mouseup', stopDrag);
} }
function onMouseMove(event: MouseEvent) { function onDrag(event: MouseEvent) {
if (dragging === null) return; if (draggingIdx === null) return;
event.preventDefault(); event.preventDefault();
const dx = event.clientX - startX; const dx = event.clientX - dragStartX;
const minWidth = 100; const min = 80;
const leftIdx = dragging; const li = draggingIdx;
const rightIdx = dragging + 1; const ri = draggingIdx + 1;
let newLeft = startWidths[leftIdx] + dx; let newL = dragStartWidths[li] + dx;
let newRight = startWidths[rightIdx] - dx; let newR = dragStartWidths[ri] - dx;
if (newLeft < minWidth) { // Clamp
newRight -= (minWidth - newLeft); if (newL < min) { newR -= (min - newL); newL = min; }
newLeft = minWidth; if (newR < min) { newL -= (min - newR); newR = min; }
}
if (newRight < minWidth) {
newLeft -= (minWidth - newRight);
newRight = minWidth;
}
panelWidths[leftIdx] = Math.max(minWidth, newLeft); panelWidths[li] = Math.max(min, newL);
panelWidths[rightIdx] = Math.max(minWidth, newRight); panelWidths[ri] = Math.max(min, newR);
panelWidths = panelWidths; panelWidths = panelWidths;
} }
function onMouseUp() { function stopDrag() {
dragging = null; draggingIdx = null;
// Selektion wieder erlauben
document.body.style.userSelect = ''; document.body.style.userSelect = '';
document.body.style.webkitUserSelect = '';
document.body.style.cursor = ''; document.body.style.cursor = '';
document.removeEventListener('mousemove', onDrag);
document.removeEventListener('mouseup', stopDrag);
window.removeEventListener('mousemove', onMouseMove); try { localStorage.setItem('panel-widths', JSON.stringify(panelWidths)); } catch {}
window.removeEventListener('mouseup', onMouseUp);
try {
localStorage.setItem('panel-widths', JSON.stringify(panelWidths));
} catch {}
} }
onMount(() => { onMount(() => {
@ -107,47 +102,37 @@
const saved = localStorage.getItem('panel-widths'); const saved = localStorage.getItem('panel-widths');
if (saved) { if (saved) {
const parsed = JSON.parse(saved); const parsed = JSON.parse(saved);
if (Array.isArray(parsed) && parsed.length === 4) { if (Array.isArray(parsed) && parsed.length === 4) panelWidths = parsed;
panelWidths = parsed;
}
} }
} catch {} } catch {}
// Breiten an Fenster anpassen // An Fensterbreite anpassen
requestAnimationFrame(() => {
if (container) { if (container) {
const available = container.clientWidth;
const total = panelWidths.reduce((a, b) => a + b, 0); const total = panelWidths.reduce((a, b) => a + b, 0);
const available = container.clientWidth - 15; // 3 Handles à 5px if (total > available || total < available * 0.5) {
if (total > available) {
const scale = available / total; const scale = available / total;
panelWidths = panelWidths.map(w => Math.round(w * scale)); panelWidths = panelWidths.map(w => Math.max(80, Math.round(w * scale)));
} }
} }
}); });
});
$: handlePositions = getHandlePositions();
</script> </script>
<div <div class="layout" bind:this={container}>
class="panels" <!-- Die 4 Panels als einfaches Grid ohne Handle-Spalten -->
class:dragging={dragging !== null} <div class="panels" style="grid-template-columns: {getGridTemplate()}">
bind:this={container}
style="grid-template-columns: {getGridTemplate()}"
>
<!-- Session-Sidebar -->
<aside class="panel"> <aside class="panel">
<SessionList /> <SessionList />
</aside> </aside>
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div class="handle" on:mousedown={(e) => onMouseDown(0, e)}><div class="handle-grip"></div></div>
<!-- Chat -->
<section class="panel"> <section class="panel">
<ChatPanel /> <ChatPanel />
</section> </section>
<!-- 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 -->
<section class="panel"> <section class="panel">
<div class="panel-tabs"> <div class="panel-tabs">
{#each middleTabs as tab} {#each middleTabs as tab}
@ -171,10 +156,6 @@
</div> </div>
</section> </section>
<!-- 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 -->
<section class="panel"> <section class="panel">
<div class="panel-tabs"> <div class="panel-tabs">
{#each rightTabs as tab} {#each rightTabs as tab}
@ -197,19 +178,35 @@
</section> </section>
</div> </div>
<!-- Unsichtbares Overlay während Drag — fängt alle Maus-Events ab --> <!-- Resize-Handles als absolut positionierte Elemente ÜBER den Panels -->
{#if dragging !== null} {#each handlePositions as pos, i}
<!-- svelte-ignore a11y_no_static_element_interactions --> <!-- svelte-ignore a11y_no_static_element_interactions -->
<div class="drag-overlay" on:mousemove={onMouseMove} on:mouseup={onMouseUp}></div> <div
class="resize-handle"
class:active={draggingIdx === i}
style="left: {pos - 4}px"
on:mousedown={(e) => startDrag(i, e)}
></div>
{/each}
<!-- Drag-Overlay -->
{#if draggingIdx !== null}
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div class="drag-overlay" on:mousemove={onDrag} on:mouseup={stopDrag}></div>
{/if} {/if}
</div>
<style> <style>
.layout {
position: relative;
height: 100%;
overflow: hidden;
}
.panels { .panels {
display: grid; display: grid;
gap: 0; gap: 0;
height: 100%; height: 100%;
background: var(--bg-primary);
position: relative;
} }
.panel { .panel {
@ -218,48 +215,32 @@
background: var(--bg-primary); background: var(--bg-primary);
overflow: hidden; overflow: hidden;
min-width: 0; min-width: 0;
border-right: 1px solid var(--border);
} }
/* Resize Handle — 5px breit, mit unsichtbarem 15px Greifbereich */ .panel:last-child {
.handle { border-right: none;
width: 5px;
cursor: col-resize;
background: var(--border);
position: relative;
z-index: 10;
display: flex;
align-items: center;
justify-content: center;
} }
.handle::before { /* Resize Handle — absolut positioniert, 8px breit, über den Panel-Grenzen */
content: ''; .resize-handle {
position: absolute; position: absolute;
top: 0; top: 0;
bottom: 0; bottom: 0;
left: -5px; width: 8px;
right: -5px; cursor: col-resize;
z-index: 11; z-index: 100;
background: transparent;
transition: background 0.1s ease;
} }
.handle:hover { .resize-handle:hover,
.resize-handle.active {
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; opacity: 0.6;
} }
/* Drag-Overlay — ganzer Bildschirm, unsichtbar, fängt Maus-Events */ /* Drag-Overlay — fängt ALLE Maus-Events während Drag */
.drag-overlay { .drag-overlay {
position: fixed; position: fixed;
inset: 0; inset: 0;
@ -272,11 +253,12 @@
display: flex; display: flex;
background: var(--bg-secondary); background: var(--bg-secondary);
border-bottom: 1px solid var(--border); border-bottom: 1px solid var(--border);
flex-shrink: 0;
} }
.tab { .tab {
flex: 1; flex: 1;
padding: var(--spacing-sm) var(--spacing-sm); padding: var(--spacing-sm);
font-size: 0.75rem; font-size: 0.75rem;
color: var(--text-secondary); color: var(--text-secondary);
background: transparent; background: transparent;