diff --git a/frontend/src/lib/components/blockshell/BlockShell.svelte b/frontend/src/lib/components/blockshell/BlockShell.svelte index e9086c5..d8ff2f9 100644 --- a/frontend/src/lib/components/blockshell/BlockShell.svelte +++ b/frontend/src/lib/components/blockshell/BlockShell.svelte @@ -73,6 +73,7 @@ let dropFeedback = $state(''); let isResizing = $state(false); let isDragging = $state(false); + let contentZoom = $state(1.0); let containerEl: HTMLDivElement | undefined = $state(); let containerWidth = $state(0); @@ -422,7 +423,20 @@ {#if !minimized || isFullscreen} -
+ +
{ + if (e.ctrlKey || e.metaKey) { + e.preventDefault(); + e.stopPropagation(); + const factor = e.deltaY > 0 ? 0.95 : 1.05; + contentZoom = Math.min(3, Math.max(0.3, contentZoom * factor)); + } + }} + > {#if children} {@render children()} {:else} diff --git a/frontend/src/lib/components/canvas/Canvas.svelte b/frontend/src/lib/components/canvas/Canvas.svelte index d18ac0b..c55e7c5 100644 --- a/frontend/src/lib/components/canvas/Canvas.svelte +++ b/frontend/src/lib/components/canvas/Canvas.svelte @@ -132,6 +132,12 @@ grid = { ...grid, enabled: !grid.enabled }; onGridChange?.(grid.enabled); } + // Piltaster = pan + const PAN_STEP = 50; + if (e.code === 'ArrowUp') { camera = { ...camera, y: camera.y + PAN_STEP }; e.preventDefault(); } + if (e.code === 'ArrowDown') { camera = { ...camera, y: camera.y - PAN_STEP }; e.preventDefault(); } + if (e.code === 'ArrowLeft') { camera = { ...camera, x: camera.x + PAN_STEP }; e.preventDefault(); } + if (e.code === 'ArrowRight') { camera = { ...camera, x: camera.x - PAN_STEP }; e.preventDefault(); } } function onKeyUp(e: KeyboardEvent) { if (e.code === 'Space') { @@ -146,11 +152,24 @@ }; }); - // --- Zoom --- + // --- Scroll: Ctrl = zoom, uten Ctrl = pan --- function handleWheel(e: WheelEvent) { + // Ikke fang scroll inne i paneler (la innhold scrolle) + if ((e.target as HTMLElement).closest('[data-canvas-object-id]')) return; + e.preventDefault(); - const zoomFactor = e.deltaY > 0 ? 0.92 : 1.08; - zoomAt(e.clientX, e.clientY, zoomFactor); + if (e.ctrlKey || e.metaKey) { + // Ctrl+scroll = zoom + const zoomFactor = e.deltaY > 0 ? 0.92 : 1.08; + zoomAt(e.clientX, e.clientY, zoomFactor); + } else { + // Scroll = pan + camera = { + ...camera, + x: camera.x - e.deltaX - (e.shiftKey ? e.deltaY : 0), + y: camera.y - (e.shiftKey ? 0 : e.deltaY), + }; + } } function zoomAt(screenX: number, screenY: number, factor: number) { @@ -459,7 +478,7 @@ ondblclick={(e) => { if (!(e.target as HTMLElement).closest('[data-canvas-object-id]') && !(e.target as HTMLElement).closest('.canvas-toolbar')) { - zoomToFit(); + camera = { ...camera, zoom: 1.0 }; } }} ontouchstart={handleTouchStart} diff --git a/frontend/src/routes/+page.svelte b/frontend/src/routes/+page.svelte index 8a827e6..bde329a 100644 --- a/frontend/src/routes/+page.svelte +++ b/frontend/src/routes/+page.svelte @@ -230,18 +230,24 @@ if (layout.panels.some(p => p.trait === trait)) return; const info = getPanelInfo(trait); const remembered = panelSizes[trait]; - const maxY = layout.panels.length > 0 - ? Math.max(...layout.panels.map(p => p.y + p.height)) - : 0; + const w = remembered?.width ?? info.defaultWidth; + const h = remembered?.height ?? info.defaultHeight; + + // Plasser i viewport-sentrum (hensyn til kamera pan/zoom) + const vpW = typeof window !== 'undefined' ? window.innerWidth : 1024; + const vpH = typeof window !== 'undefined' ? window.innerHeight - 44 : 700; // minus header + const centerX = (vpW / 2 - savedCamera.x) / savedCamera.zoom - w / 2; + const centerY = (vpH / 2 - savedCamera.y) / savedCamera.zoom - h / 2; + layout = { panels: [ ...layout.panels, { trait, - x: 30, - y: maxY + 30, - width: remembered?.width ?? info.defaultWidth, - height: remembered?.height ?? info.defaultHeight, + x: Math.round(centerX), + y: Math.round(centerY), + width: w, + height: h, }, ], };