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

View file

@ -9,6 +9,9 @@
* Ref: docs/retninger/arbeidsflaten.md § "Tre lag" * 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 */ /** Position and size of a single panel on the canvas */
export interface PanelLayout { export interface PanelLayout {
/** Trait name this panel represents */ /** Trait name this panel represents */
@ -21,6 +24,8 @@ export interface PanelLayout {
width: number; width: number;
/** Panel height */ /** Panel height */
height: number; height: number;
/** Whether panel is minimized (collapsed to header only) */
minimized?: boolean;
} }
/** Full workspace layout state */ /** Full workspace layout state */

View file

@ -16,6 +16,7 @@
getPanelInfo, getPanelInfo,
generateDefaultLayout, generateDefaultLayout,
resolveLayout, resolveLayout,
PANEL_HEADER_HEIGHT,
} from '$lib/workspace/types.js'; } from '$lib/workspace/types.js';
// Context header // Context header
@ -139,7 +140,7 @@
x: p.x, x: p.x,
y: p.y, y: p.y,
width: p.width, width: p.width,
height: p.height, height: p.minimized ? PANEL_HEADER_HEIGHT : p.height,
})) }))
); );
@ -197,6 +198,16 @@
persistLayout(); 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 */ /** Handle adding a new panel from the tool menu */
function handleAddPanel(trait: string) { function handleAddPanel(trait: string) {
// Don't add duplicate panels // Don't add duplicate panels
@ -369,8 +380,10 @@
icon={info.icon} icon={info.icon}
width={panel?.width ?? obj.width} width={panel?.width ?? obj.width}
height={panel?.height ?? obj.height} height={panel?.height ?? obj.height}
minimized={panel?.minimized ?? false}
onResize={(w, h) => handlePanelResize(trait, w, h)} onResize={(w, h) => handlePanelResize(trait, w, h)}
onClose={() => handlePanelClose(trait)} onClose={() => handlePanelClose(trait)}
onMinimizeChange={(m) => handlePanelMinimize(trait, m)}
> >
{#if knownTraits.has(trait)} {#if knownTraits.has(trait)}
{#if trait === 'editor'} {#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.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.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". - [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". - [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".
> Påbegynt: 2026-03-18T07:39
- [ ] 19.6 Personlig flate: brukerens standard arbeidsflate (node_kind: 'workspace'). Vises når ikke koblet til en annen node. Persistent layout. - [ ] 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 ## Fase 20: Universell overføring + panelrework