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:
parent
f414e820e6
commit
e48519fff4
2 changed files with 95 additions and 171 deletions
|
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue