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:
parent
eb8e2ac1d7
commit
82f40b6ae2
1 changed files with 87 additions and 127 deletions
|
|
@ -1,5 +1,5 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
import SessionList from '$lib/components/SessionList.svelte';
|
||||
import ChatPanel from '$lib/components/ChatPanel.svelte';
|
||||
import ActivityPanel from '$lib/components/ActivityPanel.svelte';
|
||||
|
|
@ -22,82 +22,20 @@
|
|||
{ id: 'guards', label: 'Guard-Rails', icon: '🛡️' },
|
||||
];
|
||||
|
||||
// ============ Resizable Panels ============
|
||||
// ============ Resizable Panels (Vanilla JS) ============
|
||||
|
||||
let panelWidths = [220, 400, 400, 380];
|
||||
let container: HTMLDivElement;
|
||||
let draggingIdx: number | null = null;
|
||||
let dragStartX = 0;
|
||||
let dragStartWidths: number[] = [];
|
||||
let gridEl: HTMLDivElement;
|
||||
let handleEls: HTMLDivElement[] = [];
|
||||
|
||||
// Panel-Refs für Position-Berechnung
|
||||
let panelRefs: HTMLElement[] = [];
|
||||
|
||||
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);
|
||||
function applyWidths() {
|
||||
if (gridEl) {
|
||||
gridEl.style.gridTemplateColumns = panelWidths.map(w => `${w}px`).join(' ');
|
||||
}
|
||||
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(() => {
|
||||
// Gespeicherte Breiten laden
|
||||
try {
|
||||
const saved = localStorage.getItem('panel-widths');
|
||||
if (saved) {
|
||||
|
|
@ -107,31 +45,86 @@
|
|||
} catch {}
|
||||
|
||||
// An Fensterbreite anpassen
|
||||
requestAnimationFrame(() => {
|
||||
if (container) {
|
||||
const available = container.clientWidth;
|
||||
const total = panelWidths.reduce((a, b) => a + b, 0);
|
||||
if (total > available || total < available * 0.5) {
|
||||
const scale = available / total;
|
||||
panelWidths = panelWidths.map(w => Math.max(80, Math.round(w * scale)));
|
||||
}
|
||||
if (gridEl) {
|
||||
const available = gridEl.parentElement?.clientWidth || window.innerWidth;
|
||||
const total = panelWidths.reduce((a, b) => a + b, 0);
|
||||
if (total > available || total < available * 0.5) {
|
||||
const scale = available / total;
|
||||
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>
|
||||
|
||||
<div class="layout" bind:this={container}>
|
||||
<!-- Die 4 Panels als einfaches Grid ohne Handle-Spalten -->
|
||||
<div class="panels" style="grid-template-columns: {getGridTemplate()}">
|
||||
<div class="layout">
|
||||
<div class="grid" bind:this={gridEl}>
|
||||
<aside class="panel">
|
||||
<SessionList />
|
||||
</aside>
|
||||
<div class="handle" bind:this={handleEls[0]}></div>
|
||||
|
||||
<section class="panel">
|
||||
<ChatPanel />
|
||||
</section>
|
||||
<div class="handle" bind:this={handleEls[1]}></div>
|
||||
|
||||
<section class="panel">
|
||||
<div class="panel-tabs">
|
||||
|
|
@ -155,6 +148,7 @@
|
|||
{/if}
|
||||
</div>
|
||||
</section>
|
||||
<div class="handle" bind:this={handleEls[2]}></div>
|
||||
|
||||
<section class="panel">
|
||||
<div class="panel-tabs">
|
||||
|
|
@ -177,35 +171,18 @@
|
|||
</div>
|
||||
</section>
|
||||
</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>
|
||||
|
||||
<style>
|
||||
.layout {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.panels {
|
||||
.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%;
|
||||
}
|
||||
|
||||
|
|
@ -215,37 +192,20 @@
|
|||
background: var(--bg-primary);
|
||||
overflow: hidden;
|
||||
min-width: 0;
|
||||
border-right: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.panel:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
/* Resize Handle — absolut positioniert, 8px breit, über den Panel-Grenzen */
|
||||
.resize-handle {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
/* Handle — eigene Grid-Spalte, Touch-Area */
|
||||
.handle {
|
||||
width: 8px;
|
||||
cursor: col-resize;
|
||||
z-index: 100;
|
||||
background: transparent;
|
||||
transition: background 0.1s ease;
|
||||
background: var(--border);
|
||||
touch-action: none; /* Wichtig für Pointer Events */
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.resize-handle:hover,
|
||||
.resize-handle.active {
|
||||
.handle:hover,
|
||||
.handle.active {
|
||||
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 */
|
||||
|
|
|
|||
Loading…
Reference in a new issue