Navigasjon: scroll=pan, Ctrl+scroll=zoom, piltaster, viewport-plassering

Canvas: scroll=pan, Ctrl+scroll=zoom, piltaster, dblclick=100%.
BlockShell: Ctrl+scroll zoomer panel-innhold.
Nye paneler plasseres i viewport-sentrum, ikke utenfor bildet.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
vegard 2026-03-20 02:58:25 +00:00
parent 0d9837a917
commit e94c22fcb8
3 changed files with 51 additions and 12 deletions

View file

@ -73,6 +73,7 @@
let dropFeedback = $state<string>('');
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 @@
<!-- Content area (hidden when minimized) -->
{#if !minimized || isFullscreen}
<div class="blockshell-content">
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div
class="blockshell-content"
style:transform="scale({contentZoom})"
style:transform-origin="top left"
onwheel={(e) => {
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}

View file

@ -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}

View file

@ -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,
},
],
};