PaneForge statt eigener Resize-Logik — funktionierendes Drag

Eigene Resize-Implementierung komplett ersetzt durch PaneForge Library:
- Getestet, Svelte 5 kompatibel, funktioniert in WebKitGTK
- autoSaveId für automatische Persistierung in localStorage
- minSize/maxSize pro Panel in Prozent
- Kein eigener Pointer-Event-Code mehr nötig

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Eddy 2026-04-13 20:26:20 +02:00
parent f414e820e6
commit e48519fff4
2 changed files with 95 additions and 171 deletions

View file

@ -25,6 +25,7 @@
"@anthropic-ai/claude-code": "^0.2.0", "@anthropic-ai/claude-code": "^0.2.0",
"@tauri-apps/api": "^2.0.0", "@tauri-apps/api": "^2.0.0",
"@tauri-apps/plugin-shell": "^2.0.0", "@tauri-apps/plugin-shell": "^2.0.0",
"marked": "^18.0.0" "marked": "^18.0.0",
"paneforge": "^1.0.2"
} }
} }

View file

@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import { onMount, onDestroy } from 'svelte'; import { PaneGroup, Pane, PaneResizer } from 'paneforge';
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';
@ -21,118 +21,29 @@
{ id: 'agents', label: 'Agents', icon: '🤖' }, { id: 'agents', label: 'Agents', icon: '🤖' },
{ id: 'guards', label: 'Guard-Rails', icon: '🛡️' }, { id: 'guards', label: 'Guard-Rails', icon: '🛡️' },
]; ];
// ============ Resizable Panels (Vanilla JS) ============
let panelWidths = [220, 400, 400, 380];
let gridEl: HTMLDivElement;
let handleEls: HTMLDivElement[] = [];
function applyWidths() {
if (!gridEl) return;
const available = gridEl.clientWidth - 24; // 3 Handles × 8px
const total = panelWidths[0] + panelWidths[1] + panelWidths[2] + panelWidths[3];
// Letztes Panel bekommt den Restplatz (mindestens 80px)
const lastWidth = Math.max(80, panelWidths[3] + (available - total));
const cols = `${panelWidths[0]}px 8px ${panelWidths[1]}px 8px ${panelWidths[2]}px 8px ${lastWidth}px`;
gridEl.style.gridTemplateColumns = cols;
}
onMount(() => {
// Gespeicherte Breiten laden
try {
const saved = localStorage.getItem('panel-widths');
if (saved) {
const parsed = JSON.parse(saved);
if (Array.isArray(parsed) && parsed.length === 4) panelWidths = parsed;
}
} catch {}
// An Fensterbreite anpassen
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);
});
});
});
</script> </script>
<div class="layout"> <PaneGroup direction="horizontal" autoSaveId="claude-desktop-panels" class="pane-group">
<div class="grid" bind:this={gridEl}> <!-- Sessions -->
<aside class="panel"> <Pane defaultSize={15} minSize={8} maxSize={30} class="panel">
<SessionList /> <SessionList />
</aside> </Pane>
<div class="handle" bind:this={handleEls[0]}></div>
<section class="panel"> <PaneResizer class="resizer">
<div class="resizer-line"></div>
</PaneResizer>
<!-- Chat -->
<Pane defaultSize={35} minSize={15} class="panel">
<ChatPanel /> <ChatPanel />
</section> </Pane>
<div class="handle" bind:this={handleEls[1]}></div>
<section class="panel"> <PaneResizer class="resizer">
<div class="resizer-line"></div>
</PaneResizer>
<!-- Aktivität / Memory / Audit -->
<Pane defaultSize={25} minSize={10} class="panel">
<div class="panel-tabs"> <div class="panel-tabs">
{#each middleTabs as tab} {#each middleTabs as tab}
<button <button
@ -153,10 +64,14 @@
<AuditLog /> <AuditLog />
{/if} {/if}
</div> </div>
</section> </Pane>
<div class="handle" bind:this={handleEls[2]}></div>
<section class="panel"> <PaneResizer class="resizer">
<div class="resizer-line"></div>
</PaneResizer>
<!-- Agents / Guard-Rails -->
<Pane defaultSize={25} minSize={10} class="panel">
<div class="panel-tabs"> <div class="panel-tabs">
{#each rightTabs as tab} {#each rightTabs as tab}
<button <button
@ -175,44 +90,52 @@
<GuardRailsPanel /> <GuardRailsPanel />
{/if} {/if}
</div> </div>
</section> </Pane>
</div> </PaneGroup>
</div>
<style> <style>
.layout { /* PaneForge Container */
height: 100%; :global(.pane-group) {
overflow: hidden;
}
.grid {
display: grid;
grid-template-columns: 220px 8px 1fr 8px 1fr 8px 380px;
height: 100%; height: 100%;
} }
.panel { :global(.panel) {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
background: var(--bg-primary); background: var(--bg-primary);
overflow: hidden; overflow: hidden;
min-width: 0;
} }
/* Handle — eigene Grid-Spalte, Touch-Area */ /* Resizer Handle */
.handle { :global(.resizer) {
width: 8px; width: 8px;
cursor: col-resize;
background: var(--border); background: var(--border);
touch-action: none; /* Wichtig für Pointer Events */ display: flex;
user-select: none; align-items: center;
justify-content: center;
cursor: col-resize;
transition: background 0.15s ease;
} }
.handle:hover, :global(.resizer:hover),
.handle.active { :global(.resizer[data-state="drag"]) {
background: var(--accent); background: var(--accent);
} }
.resizer-line {
width: 2px;
height: 24px;
border-radius: 1px;
background: var(--text-secondary);
opacity: 0;
transition: opacity 0.15s;
}
:global(.resizer:hover) .resizer-line,
:global(.resizer[data-state="drag"]) .resizer-line {
opacity: 0.5;
}
/* Tabs */ /* Tabs */
.panel-tabs { .panel-tabs {
display: flex; display: flex;