Resizable Panels — alle Bereiche per Drag ziehbar
- 3 Resize-Handles zwischen den 4 Panels (Session/Chat/Aktivität/Agents) - Mindestbreite 120px pro Panel - Panel-Breiten werden in localStorage gespeichert und beim Neustart geladen - Visuelles Feedback: Handle wird blau beim Hover/Drag Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
f101661016
commit
56c967b618
1 changed files with 120 additions and 41 deletions
|
|
@ -1,4 +1,5 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { onMount } 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';
|
||||||
|
|
@ -7,10 +8,7 @@
|
||||||
import AuditLog from '$lib/components/AuditLog.svelte';
|
import AuditLog from '$lib/components/AuditLog.svelte';
|
||||||
import GuardRailsPanel from '$lib/components/GuardRailsPanel.svelte';
|
import GuardRailsPanel from '$lib/components/GuardRailsPanel.svelte';
|
||||||
|
|
||||||
// Tab-State für mittleres Panel
|
|
||||||
let activeMiddleTab = 'activity';
|
let activeMiddleTab = 'activity';
|
||||||
|
|
||||||
// Tab-State für rechtes Panel
|
|
||||||
let activeRightTab = 'agents';
|
let activeRightTab = 'agents';
|
||||||
|
|
||||||
const middleTabs = [
|
const middleTabs = [
|
||||||
|
|
@ -23,20 +21,111 @@
|
||||||
{ id: 'agents', label: 'Agents', icon: '🤖' },
|
{ id: 'agents', label: 'Agents', icon: '🤖' },
|
||||||
{ id: 'guards', label: 'Guard-Rails', icon: '🛡️' },
|
{ id: 'guards', label: 'Guard-Rails', icon: '🛡️' },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// ============ Resizable Panels ============
|
||||||
|
|
||||||
|
// Breiten in Pixel (werden zu fr umgerechnet)
|
||||||
|
let panelWidths = [220, 400, 400, 380];
|
||||||
|
let container: HTMLDivElement;
|
||||||
|
let dragging: number | null = null;
|
||||||
|
let startX = 0;
|
||||||
|
let startWidths: number[] = [];
|
||||||
|
|
||||||
|
function getGridTemplate(): string {
|
||||||
|
return panelWidths.map(w => `${w}px`).join(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
function onMouseDown(index: number, event: MouseEvent) {
|
||||||
|
event.preventDefault();
|
||||||
|
dragging = index;
|
||||||
|
startX = event.clientX;
|
||||||
|
startWidths = [...panelWidths];
|
||||||
|
window.addEventListener('mousemove', onMouseMove);
|
||||||
|
window.addEventListener('mouseup', onMouseUp);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onMouseMove(event: MouseEvent) {
|
||||||
|
if (dragging === null) return;
|
||||||
|
const dx = event.clientX - startX;
|
||||||
|
const minWidth = 120;
|
||||||
|
|
||||||
|
const leftIdx = dragging;
|
||||||
|
const rightIdx = dragging + 1;
|
||||||
|
|
||||||
|
let newLeft = startWidths[leftIdx] + dx;
|
||||||
|
let newRight = startWidths[rightIdx] - dx;
|
||||||
|
|
||||||
|
// Mindestbreiten einhalten
|
||||||
|
if (newLeft < minWidth) {
|
||||||
|
newRight -= (minWidth - newLeft);
|
||||||
|
newLeft = minWidth;
|
||||||
|
}
|
||||||
|
if (newRight < minWidth) {
|
||||||
|
newLeft -= (minWidth - newRight);
|
||||||
|
newRight = minWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
panelWidths[leftIdx] = Math.max(minWidth, newLeft);
|
||||||
|
panelWidths[rightIdx] = Math.max(minWidth, newRight);
|
||||||
|
panelWidths = panelWidths; // Svelte reactivity
|
||||||
|
}
|
||||||
|
|
||||||
|
function onMouseUp() {
|
||||||
|
dragging = null;
|
||||||
|
window.removeEventListener('mousemove', onMouseMove);
|
||||||
|
window.removeEventListener('mouseup', onMouseUp);
|
||||||
|
|
||||||
|
// Breiten in localStorage speichern
|
||||||
|
try {
|
||||||
|
localStorage.setItem('panel-widths', JSON.stringify(panelWidths));
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {}
|
||||||
|
|
||||||
|
// Initiale Breiten an Fenster anpassen wenn zu breit
|
||||||
|
if (container) {
|
||||||
|
const total = panelWidths.reduce((a, b) => a + b, 0);
|
||||||
|
const available = container.clientWidth - 9; // 3 Handles à 3px
|
||||||
|
if (total > available) {
|
||||||
|
const scale = available / total;
|
||||||
|
panelWidths = panelWidths.map(w => Math.round(w * scale));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="panels">
|
<div
|
||||||
<!-- Session-Sidebar (links) -->
|
class="panels"
|
||||||
|
class:dragging={dragging !== null}
|
||||||
|
bind:this={container}
|
||||||
|
style="grid-template-columns: {getGridTemplate()}"
|
||||||
|
>
|
||||||
|
<!-- Session-Sidebar -->
|
||||||
<aside class="panel panel-sessions">
|
<aside class="panel panel-sessions">
|
||||||
<SessionList />
|
<SessionList />
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<!-- Chat (Mitte-Links) -->
|
<div class="resize-handle" on:mousedown={(e) => onMouseDown(0, e)} role="separator" aria-orientation="vertical"></div>
|
||||||
|
|
||||||
|
<!-- Chat -->
|
||||||
<section class="panel panel-chat">
|
<section class="panel panel-chat">
|
||||||
<ChatPanel />
|
<ChatPanel />
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- Aktivität / Memory / Audit (Mitte-Rechts) -->
|
<div class="resize-handle" on:mousedown={(e) => onMouseDown(1, e)} role="separator" aria-orientation="vertical"></div>
|
||||||
|
|
||||||
|
<!-- Aktivität / Memory / Audit -->
|
||||||
<section class="panel panel-activity">
|
<section class="panel panel-activity">
|
||||||
<div class="panel-tabs">
|
<div class="panel-tabs">
|
||||||
{#each middleTabs as tab}
|
{#each middleTabs as tab}
|
||||||
|
|
@ -60,7 +149,9 @@
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- Agents / Guard-Rails (rechts) -->
|
<div class="resize-handle" on:mousedown={(e) => onMouseDown(2, e)} role="separator" aria-orientation="vertical"></div>
|
||||||
|
|
||||||
|
<!-- Agents / Guard-Rails -->
|
||||||
<section class="panel panel-details">
|
<section class="panel panel-details">
|
||||||
<div class="panel-tabs">
|
<div class="panel-tabs">
|
||||||
{#each rightTabs as tab}
|
{#each rightTabs as tab}
|
||||||
|
|
@ -86,10 +177,14 @@
|
||||||
<style>
|
<style>
|
||||||
.panels {
|
.panels {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 220px 1fr 1fr 1fr;
|
gap: 0;
|
||||||
gap: 1px;
|
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background: var(--border);
|
background: var(--bg-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.panels.dragging {
|
||||||
|
cursor: col-resize;
|
||||||
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.panel {
|
.panel {
|
||||||
|
|
@ -97,12 +192,24 @@
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
background: var(--bg-primary);
|
background: var(--bg-primary);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.panel-sessions {
|
/* Resize Handle */
|
||||||
min-width: 180px;
|
.resize-handle {
|
||||||
|
width: 3px;
|
||||||
|
cursor: col-resize;
|
||||||
|
background: var(--border);
|
||||||
|
transition: background 0.15s ease;
|
||||||
|
z-index: 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.resize-handle:hover,
|
||||||
|
.panels.dragging .resize-handle {
|
||||||
|
background: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tabs */
|
||||||
.panel-tabs {
|
.panel-tabs {
|
||||||
display: flex;
|
display: flex;
|
||||||
background: var(--bg-secondary);
|
background: var(--bg-secondary);
|
||||||
|
|
@ -136,32 +243,4 @@
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Responsive */
|
|
||||||
@media (max-width: 1400px) {
|
|
||||||
.panels {
|
|
||||||
grid-template-columns: 200px 1fr 1fr;
|
|
||||||
}
|
|
||||||
.panel-details {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 1000px) {
|
|
||||||
.panels {
|
|
||||||
grid-template-columns: 180px 1fr;
|
|
||||||
}
|
|
||||||
.panel-activity {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 700px) {
|
|
||||||
.panels {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
.panel-sessions {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue