Fix: Resizable Panels via Pointer Events + setPointerCapture

Komplett auf Vanilla JS Pointer Events umgestellt:
- setPointerCapture() fängt ALLE Events am Handle-Element (kein Overlay nötig)
- touch-action: none auf Handles (WebKitGTK Kompatibilität)
- addEventListener in onMount statt Svelte on: Syntax
- Handles als sichtbare 8px Grid-Spalten (nicht absolute)

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

View file

@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import { onMount } from 'svelte'; import { onMount, onDestroy } from 'svelte';
import SessionList from '$lib/components/SessionList.svelte'; import SessionList from '$lib/components/SessionList.svelte';
import ChatPanel from '$lib/components/ChatPanel.svelte'; import ChatPanel from '$lib/components/ChatPanel.svelte';
import ActivityPanel from '$lib/components/ActivityPanel.svelte'; import ActivityPanel from '$lib/components/ActivityPanel.svelte';
@ -22,82 +22,20 @@
{ id: 'guards', label: 'Guard-Rails', icon: '🛡️' }, { id: 'guards', label: 'Guard-Rails', icon: '🛡️' },
]; ];
// ============ Resizable Panels ============ // ============ Resizable Panels (Vanilla JS) ============
let panelWidths = [220, 400, 400, 380]; let panelWidths = [220, 400, 400, 380];
let container: HTMLDivElement; let gridEl: HTMLDivElement;
let draggingIdx: number | null = null; let handleEls: HTMLDivElement[] = [];
let dragStartX = 0;
let dragStartWidths: number[] = [];
// Panel-Refs für Position-Berechnung function applyWidths() {
let panelRefs: HTMLElement[] = []; if (gridEl) {
gridEl.style.gridTemplateColumns = panelWidths.map(w => `${w}px`).join(' ');
function getGridTemplate(): string {
return panelWidths.map(w => `${w}px`).join(' ');
}
// 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.stopPropagation();
draggingIdx = index;
dragStartX = event.clientX;
dragStartWidths = [...panelWidths];
// Text-Selektion sofort blockieren
document.body.style.userSelect = 'none';
document.body.style.cursor = 'col-resize';
window.getSelection()?.removeAllRanges();
document.addEventListener('mousemove', onDrag);
document.addEventListener('mouseup', stopDrag);
}
function onDrag(event: MouseEvent) {
if (draggingIdx === null) return;
event.preventDefault();
const dx = event.clientX - dragStartX;
const min = 80;
const li = draggingIdx;
const ri = draggingIdx + 1;
let newL = dragStartWidths[li] + dx;
let newR = dragStartWidths[ri] - dx;
// Clamp
if (newL < min) { newR -= (min - newL); newL = min; }
if (newR < min) { newL -= (min - newR); newR = min; }
panelWidths[li] = Math.max(min, newL);
panelWidths[ri] = Math.max(min, newR);
panelWidths = panelWidths;
}
function stopDrag() {
draggingIdx = null;
document.body.style.userSelect = '';
document.body.style.cursor = '';
document.removeEventListener('mousemove', onDrag);
document.removeEventListener('mouseup', stopDrag);
try { localStorage.setItem('panel-widths', JSON.stringify(panelWidths)); } catch {}
} }
onMount(() => { onMount(() => {
// Gespeicherte Breiten laden
try { try {
const saved = localStorage.getItem('panel-widths'); const saved = localStorage.getItem('panel-widths');
if (saved) { if (saved) {
@ -107,31 +45,86 @@
} catch {} } catch {}
// An Fensterbreite anpassen // An Fensterbreite anpassen
requestAnimationFrame(() => { if (gridEl) {
if (container) { const available = gridEl.parentElement?.clientWidth || window.innerWidth;
const available = container.clientWidth; const total = panelWidths.reduce((a, b) => a + b, 0);
const total = panelWidths.reduce((a, b) => a + b, 0); if (total > available || total < available * 0.5) {
if (total > available || total < available * 0.5) { const scale = available / total;
const scale = available / total; panelWidths = panelWidths.map(w => Math.max(80, Math.round(w * scale)));
panelWidths = panelWidths.map(w => Math.max(80, Math.round(w * scale)));
}
} }
}
applyWidths();
// Event-Handler per Vanilla JS an jedes Handle-Element binden
handleEls.forEach((el, idx) => {
if (!el) return;
el.addEventListener('pointerdown', (e: PointerEvent) => {
e.preventDefault();
e.stopPropagation();
const startX = e.clientX;
const startWidths = [...panelWidths];
const leftIdx = idx;
const rightIdx = idx + 1;
// Pointer capture — alle Events gehen an dieses Element
el.setPointerCapture(e.pointerId);
el.classList.add('active');
document.body.style.userSelect = 'none';
document.body.style.cursor = 'col-resize';
window.getSelection()?.removeAllRanges();
function onMove(ev: PointerEvent) {
ev.preventDefault();
const dx = ev.clientX - startX;
const min = 80;
let newL = startWidths[leftIdx] + dx;
let newR = startWidths[rightIdx] - dx;
if (newL < min) { newR -= (min - newL); newL = min; }
if (newR < min) { newL -= (min - newR); newR = min; }
panelWidths[leftIdx] = Math.max(min, newL);
panelWidths[rightIdx] = Math.max(min, newR);
applyWidths();
}
function onUp(ev: PointerEvent) {
el.releasePointerCapture(ev.pointerId);
el.classList.remove('active');
document.body.style.userSelect = '';
document.body.style.cursor = '';
el.removeEventListener('pointermove', onMove);
el.removeEventListener('pointerup', onUp);
el.removeEventListener('pointercancel', onUp);
try { localStorage.setItem('panel-widths', JSON.stringify(panelWidths)); } catch {}
}
el.addEventListener('pointermove', onMove);
el.addEventListener('pointerup', onUp);
el.addEventListener('pointercancel', onUp);
});
}); });
}); });
$: handlePositions = getHandlePositions();
</script> </script>
<div class="layout" bind:this={container}> <div class="layout">
<!-- Die 4 Panels als einfaches Grid ohne Handle-Spalten --> <div class="grid" bind:this={gridEl}>
<div class="panels" style="grid-template-columns: {getGridTemplate()}">
<aside class="panel"> <aside class="panel">
<SessionList /> <SessionList />
</aside> </aside>
<div class="handle" bind:this={handleEls[0]}></div>
<section class="panel"> <section class="panel">
<ChatPanel /> <ChatPanel />
</section> </section>
<div class="handle" bind:this={handleEls[1]}></div>
<section class="panel"> <section class="panel">
<div class="panel-tabs"> <div class="panel-tabs">
@ -155,6 +148,7 @@
{/if} {/if}
</div> </div>
</section> </section>
<div class="handle" bind:this={handleEls[2]}></div>
<section class="panel"> <section class="panel">
<div class="panel-tabs"> <div class="panel-tabs">
@ -177,35 +171,18 @@
</div> </div>
</section> </section>
</div> </div>
<!-- Resize-Handles als absolut positionierte Elemente ÜBER den Panels -->
{#each handlePositions as pos, i}
<!-- svelte-ignore a11y_no_static_element_interactions -->
<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}
</div> </div>
<style> <style>
.layout { .layout {
position: relative;
height: 100%; height: 100%;
overflow: hidden; overflow: hidden;
} }
.panels { .grid {
display: grid; display: grid;
gap: 0; /* 4 Panels + 3 Handles = 7 Spalten, Handles werden via JS positioniert */
grid-template-columns: 220px 8px 400px 8px 400px 8px 380px;
height: 100%; height: 100%;
} }
@ -215,37 +192,20 @@
background: var(--bg-primary); background: var(--bg-primary);
overflow: hidden; overflow: hidden;
min-width: 0; min-width: 0;
border-right: 1px solid var(--border);
} }
.panel:last-child { /* Handle — eigene Grid-Spalte, Touch-Area */
border-right: none; .handle {
}
/* Resize Handle — absolut positioniert, 8px breit, über den Panel-Grenzen */
.resize-handle {
position: absolute;
top: 0;
bottom: 0;
width: 8px; width: 8px;
cursor: col-resize; cursor: col-resize;
z-index: 100; background: var(--border);
background: transparent; touch-action: none; /* Wichtig für Pointer Events */
transition: background 0.1s ease; user-select: none;
} }
.resize-handle:hover, .handle:hover,
.resize-handle.active { .handle.active {
background: var(--accent); background: var(--accent);
opacity: 0.6;
}
/* Drag-Overlay — fängt ALLE Maus-Events während Drag */
.drag-overlay {
position: fixed;
inset: 0;
z-index: 9999;
cursor: col-resize;
} }
/* Tabs */ /* Tabs */