Fullfører oppgave 19.5: Paneler kan minimeres til kompakt header

Dobbeltklikk på panel-header toggler minimert tilstand. Minimerte
paneler viser kun header (ikon + tittel), skjuler innhold og
resize-handles. Posisjon, bredde og full høyde bevares i layout
og gjenopprettes ved nytt dobbeltklikk. Tilstanden persisteres
i edge metadata sammen med resten av workspace-layouten.

Endringer:
- PanelLayout: nytt `minimized?`-felt
- BlockShellEvents: nytt `onMinimizeChange`-event
- BlockShell: minimized-prop, dblclick-handler, minimize-knapp,
  skjuler content/resize når minimert
- Workspace-side: håndterer minimize-state, oppdaterer canvas-
  objekthøyde til PANEL_HEADER_HEIGHT når minimert

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
vegard 2026-03-18 07:43:19 +00:00
parent 3db0e0f9fe
commit 4e9481edf3
5 changed files with 73 additions and 14 deletions

View file

@ -40,6 +40,8 @@
receiver?: BlockReceiver;
/** Whether to show close button */
closable?: boolean;
/** Whether panel is minimized (collapsed to header only) */
minimized?: boolean;
/** Extra CSS class */
class?: string;
/** Panel content */
@ -54,12 +56,14 @@
constraints = DEFAULT_CONSTRAINTS,
receiver,
closable = true,
minimized = false,
class: extraClass = '',
children,
onClose,
onDragMove,
onResize,
onFullscreenChange,
onMinimizeChange,
onDrop,
}: Props = $props();
@ -127,6 +131,15 @@
onFullscreenChange?.(isFullscreen);
}
// --- Minimize toggle (double-click header) ---
let hasDragged = false;
function handleHeaderDblClick() {
if (hasDragged) return;
if (isFullscreen) return;
onMinimizeChange?.(!minimized);
}
// --- Drag (repositioning via header) ---
function handleDragStart(e: PointerEvent) {
// Only left button, not on buttons
@ -134,6 +147,7 @@
if ((e.target as HTMLElement).closest('button')) return;
isDragging = true;
hasDragged = false;
dragStartX = e.clientX;
dragStartY = e.clientY;
@ -146,6 +160,9 @@
if (!isDragging) return;
const dx = e.clientX - dragStartX;
const dy = e.clientY - dragStartY;
if (Math.abs(dx) > 3 || Math.abs(dy) > 3) {
hasDragged = true;
}
dragStartX = e.clientX;
dragStartY = e.clientY;
onDragMove?.(dx, dy);
@ -289,12 +306,13 @@
bind:this={containerEl}
class="blockshell {extraClass}"
class:blockshell-fullscreen={isFullscreen}
class:blockshell-minimized={minimized && !isFullscreen}
class:blockshell-dragging={isDragging}
class:blockshell-resizing={isResizing}
class:blockshell-drop-compatible={dropZoneState === 'compatible'}
class:blockshell-drop-incompatible={dropZoneState === 'incompatible'}
style:width={isFullscreen ? undefined : `${clampedWidth}px`}
style:height={isFullscreen ? undefined : `${clampedHeight}px`}
style:height={isFullscreen ? undefined : minimized ? undefined : `${clampedHeight}px`}
ondragenter={handleDragEnter}
ondragover={handleDragOver}
ondragleave={handleDragLeave}
@ -309,6 +327,7 @@
onpointerdown={handleDragStart}
onpointermove={handleDragMove}
onpointerup={handleDragEnd}
ondblclick={handleHeaderDblClick}
>
<div class="blockshell-header-title">
{#if icon}
@ -318,6 +337,16 @@
</div>
<div class="blockshell-controls">
{#if !isFullscreen}
<button
class="blockshell-btn"
onclick={() => onMinimizeChange?.(!minimized)}
title={minimized ? 'Gjenopprett' : 'Minimer'}
aria-label={minimized ? 'Gjenopprett panel' : 'Minimer panel'}
>
{minimized ? '▢' : '▬'}
</button>
{/if}
<button
class="blockshell-btn"
onclick={toggleFullscreen}
@ -339,14 +368,16 @@
</div>
</div>
<!-- Content area -->
<div class="blockshell-content">
{#if children}
{@render children()}
{:else}
<p class="blockshell-empty">Ingen innhold ennå.</p>
{/if}
</div>
<!-- Content area (hidden when minimized) -->
{#if !minimized || isFullscreen}
<div class="blockshell-content">
{#if children}
{@render children()}
{:else}
<p class="blockshell-empty">Ingen innhold ennå.</p>
{/if}
</div>
{/if}
<!-- Drop zone overlay -->
{#if dropZoneState !== 'idle'}
@ -361,8 +392,8 @@
</div>
{/if}
<!-- Resize handles (hidden in fullscreen and on mobile) -->
{#if !isFullscreen && !isMobile}
<!-- Resize handles (hidden in fullscreen, mobile, and minimized) -->
{#if !isFullscreen && !isMobile && !minimized}
{#each resizeDirections as dir}
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div
@ -397,6 +428,15 @@
border: none;
}
.blockshell-minimized {
height: auto !important;
min-height: 0;
}
.blockshell-minimized .blockshell-header {
border-bottom: none;
}
.blockshell-dragging {
opacity: 0.95;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);

View file

@ -56,6 +56,8 @@ export interface BlockShellEvents {
onResize?: (width: number, height: number) => void;
/** Panel fullscreen state changed */
onFullscreenChange?: (isFullscreen: boolean) => void;
/** Panel minimize state changed (double-click header to toggle) */
onMinimizeChange?: (isMinimized: boolean) => void;
/** Drop received on this panel */
onDrop?: (payload: DragPayload) => void;
}

View file

@ -9,6 +9,9 @@
* Ref: docs/retninger/arbeidsflaten.md § "Tre lag"
*/
/** Height of the BlockShell header — used as minimized panel height */
export const PANEL_HEADER_HEIGHT = 36;
/** Position and size of a single panel on the canvas */
export interface PanelLayout {
/** Trait name this panel represents */
@ -21,6 +24,8 @@ export interface PanelLayout {
width: number;
/** Panel height */
height: number;
/** Whether panel is minimized (collapsed to header only) */
minimized?: boolean;
}
/** Full workspace layout state */

View file

@ -16,6 +16,7 @@
getPanelInfo,
generateDefaultLayout,
resolveLayout,
PANEL_HEADER_HEIGHT,
} from '$lib/workspace/types.js';
// Context header
@ -139,7 +140,7 @@
x: p.x,
y: p.y,
width: p.width,
height: p.height,
height: p.minimized ? PANEL_HEADER_HEIGHT : p.height,
}))
);
@ -197,6 +198,16 @@
persistLayout();
}
/** Handle panel minimize/restore toggle */
function handlePanelMinimize(trait: string, isMinimized: boolean) {
const idx = layout.panels.findIndex(p => p.trait === trait);
if (idx >= 0) {
layout.panels[idx] = { ...layout.panels[idx], minimized: isMinimized };
layout = { ...layout };
persistLayout();
}
}
/** Handle adding a new panel from the tool menu */
function handleAddPanel(trait: string) {
// Don't add duplicate panels
@ -369,8 +380,10 @@
icon={info.icon}
width={panel?.width ?? obj.width}
height={panel?.height ?? obj.height}
minimized={panel?.minimized ?? false}
onResize={(w, h) => handlePanelResize(trait, w, h)}
onClose={() => handlePanelClose(trait)}
onMinimizeChange={(m) => handlePanelMinimize(trait, m)}
>
{#if knownTraits.has(trait)}
{#if trait === 'editor'}

View file

@ -216,8 +216,7 @@ Ref: `docs/retninger/arbeidsflaten.md`, `docs/features/canvas_primitiv.md`
- [x] 19.2 BlockShell wrapper-komponent: header med tittel + fullskjerm/resize/lukk-knapper, drag-handles for repositionering, resize-handles, drop-sone rendering (highlight ved drag-over). Responsivt (min-size, max-size).
- [x] 19.3 Arbeidsflaten layout: skriv om `/collection/[id]` fra vertikal stack til Canvas + BlockShell. Last brukerens lagrede arrangement eller bruk defaults fra samlingens traits. Persist arrangement i bruker-edge metadata. Desktop: spatial canvas, mobil: stacked/tabs. Ref: `docs/retninger/arbeidsflaten.md` § "Tre lag".
- [x] 19.4 Kontekst-header: header tilhører flaten, viser gjeldende node som nedtrekksmeny/kontekst-velger. Mest brukte noder øverst (frekvens/recency), søkbart. Verktøymeny for å instansiere nye paneler. Ref: `docs/retninger/arbeidsflaten.md` § "Kontekst-header".
- [~] 19.5 Snarveier: paneler kan minimeres til kompakt ikon/fane. Dobbeltklikk → minimer/gjenopprett. Bevarer posisjon og størrelse. Ref: `docs/retninger/arbeidsflaten.md` § "Snarveier".
> Påbegynt: 2026-03-18T07:39
- [x] 19.5 Snarveier: paneler kan minimeres til kompakt ikon/fane. Dobbeltklikk → minimer/gjenopprett. Bevarer posisjon og størrelse. Ref: `docs/retninger/arbeidsflaten.md` § "Snarveier".
- [ ] 19.6 Personlig flate: brukerens standard arbeidsflate (node_kind: 'workspace'). Vises når ikke koblet til en annen node. Persistent layout.
## Fase 20: Universell overføring + panelrework