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:
parent
3db0e0f9fe
commit
4e9481edf3
5 changed files with 73 additions and 14 deletions
|
|
@ -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,14 +368,16 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Content area -->
|
<!-- Content area (hidden when minimized) -->
|
||||||
<div class="blockshell-content">
|
{#if !minimized || isFullscreen}
|
||||||
{#if children}
|
<div class="blockshell-content">
|
||||||
{@render children()}
|
{#if children}
|
||||||
{:else}
|
{@render children()}
|
||||||
<p class="blockshell-empty">Ingen innhold ennå.</p>
|
{:else}
|
||||||
{/if}
|
<p class="blockshell-empty">Ingen innhold ennå.</p>
|
||||||
</div>
|
{/if}
|
||||||
|
</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);
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 */
|
||||||
|
|
|
||||||
|
|
@ -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'}
|
||||||
|
|
|
||||||
3
tasks.md
3
tasks.md
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue