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>
286 lines
6.7 KiB
Svelte
286 lines
6.7 KiB
Svelte
<script lang="ts">
|
|
import { onMount } from 'svelte';
|
|
import SessionList from '$lib/components/SessionList.svelte';
|
|
import ChatPanel from '$lib/components/ChatPanel.svelte';
|
|
import ActivityPanel from '$lib/components/ActivityPanel.svelte';
|
|
import AgentView from '$lib/components/AgentView.svelte';
|
|
import MemoryPanel from '$lib/components/MemoryPanel.svelte';
|
|
import AuditLog from '$lib/components/AuditLog.svelte';
|
|
import GuardRailsPanel from '$lib/components/GuardRailsPanel.svelte';
|
|
|
|
let activeMiddleTab = 'activity';
|
|
let activeRightTab = 'agents';
|
|
|
|
const middleTabs = [
|
|
{ id: 'activity', label: 'Aktivität', icon: '📋' },
|
|
{ id: 'memory', label: 'Gedächtnis', icon: '🧠' },
|
|
{ id: 'audit', label: 'Historie', icon: '📝' },
|
|
];
|
|
|
|
const rightTabs = [
|
|
{ id: 'agents', label: 'Agents', icon: '🤖' },
|
|
{ id: 'guards', label: 'Guard-Rails', icon: '🛡️' },
|
|
];
|
|
|
|
// ============ Resizable Panels ============
|
|
|
|
let panelWidths = [220, 400, 400, 380];
|
|
let container: HTMLDivElement;
|
|
let draggingIdx: number | null = null;
|
|
let dragStartX = 0;
|
|
let dragStartWidths: number[] = [];
|
|
|
|
// 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);
|
|
}
|
|
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(() => {
|
|
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
|
|
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)));
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
$: 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()}">
|
|
<aside class="panel">
|
|
<SessionList />
|
|
</aside>
|
|
|
|
<section class="panel">
|
|
<ChatPanel />
|
|
</section>
|
|
|
|
<section class="panel">
|
|
<div class="panel-tabs">
|
|
{#each middleTabs as tab}
|
|
<button
|
|
class="tab"
|
|
class:active={activeMiddleTab === tab.id}
|
|
on:click={() => activeMiddleTab = tab.id}
|
|
>
|
|
{tab.icon} {tab.label}
|
|
</button>
|
|
{/each}
|
|
</div>
|
|
<div class="panel-content">
|
|
{#if activeMiddleTab === 'activity'}
|
|
<ActivityPanel />
|
|
{:else if activeMiddleTab === 'memory'}
|
|
<MemoryPanel />
|
|
{:else if activeMiddleTab === 'audit'}
|
|
<AuditLog />
|
|
{/if}
|
|
</div>
|
|
</section>
|
|
|
|
<section class="panel">
|
|
<div class="panel-tabs">
|
|
{#each rightTabs as tab}
|
|
<button
|
|
class="tab"
|
|
class:active={activeRightTab === tab.id}
|
|
on:click={() => activeRightTab = tab.id}
|
|
>
|
|
{tab.icon} {tab.label}
|
|
</button>
|
|
{/each}
|
|
</div>
|
|
<div class="panel-content">
|
|
{#if activeRightTab === 'agents'}
|
|
<AgentView />
|
|
{:else if activeRightTab === 'guards'}
|
|
<GuardRailsPanel />
|
|
{/if}
|
|
</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 {
|
|
display: grid;
|
|
gap: 0;
|
|
height: 100%;
|
|
}
|
|
|
|
.panel {
|
|
display: flex;
|
|
flex-direction: column;
|
|
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;
|
|
width: 8px;
|
|
cursor: col-resize;
|
|
z-index: 100;
|
|
background: transparent;
|
|
transition: background 0.1s ease;
|
|
}
|
|
|
|
.resize-handle:hover,
|
|
.resize-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 */
|
|
.panel-tabs {
|
|
display: flex;
|
|
background: var(--bg-secondary);
|
|
border-bottom: 1px solid var(--border);
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.tab {
|
|
flex: 1;
|
|
padding: var(--spacing-sm);
|
|
font-size: 0.75rem;
|
|
color: var(--text-secondary);
|
|
background: transparent;
|
|
border: none;
|
|
border-bottom: 2px solid transparent;
|
|
cursor: pointer;
|
|
transition: all 0.15s ease;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.tab:hover {
|
|
color: var(--text-primary);
|
|
background: var(--bg-hover);
|
|
}
|
|
|
|
.tab.active {
|
|
color: var(--accent);
|
|
border-bottom-color: var(--accent);
|
|
}
|
|
|
|
.panel-content {
|
|
flex: 1;
|
|
overflow: auto;
|
|
}
|
|
</style>
|