From 9ad6cae2491c8d8b297c92974de303b0d3010d78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Forment?= Date: Mon, 1 Dec 2025 18:04:56 +0100 Subject: [PATCH] OK --- src/lib/Canvas.svelte | 49 ++++++++++++++++++++++++++++++++++++++++- src/lib/Editor.svelte | 17 ++++++++++++++ src/lib/Item.svelte | 1 + src/lib/Toolbar.svelte | 27 ++++++++++++++++++----- src/lib/state.svelte.ts | 45 +++++++++++++++++++++++++++++++++++++ 5 files changed, 133 insertions(+), 6 deletions(-) diff --git a/src/lib/Canvas.svelte b/src/lib/Canvas.svelte index 224e91b..b357d2c 100644 --- a/src/lib/Canvas.svelte +++ b/src/lib/Canvas.svelte @@ -125,9 +125,56 @@ function handleDragOver(e: DragEvent) { e.preventDefault(); } + + function handleKeyDown(e: KeyboardEvent) { + if (state.editingId || state.editingGlobal) return; + if (state.focusedId) return; + const tag = (e.target as HTMLElement)?.tagName; + if (tag === 'INPUT' || tag === 'TEXTAREA') return; + if ((e.target as HTMLElement)?.isContentEditable) return; + + const mod = e.metaKey || e.ctrlKey; + const rect = container.getBoundingClientRect(); + const centerX = (rect.width / 2 - state.viewport.x) / state.viewport.zoom; + const centerY = (rect.height / 2 - state.viewport.y) / state.viewport.zoom; + + if (mod && e.key === 'c' && state.selectedId) { + e.preventDefault(); + state.copyItem(state.selectedId); + } else if (mod && e.key === 'v') { + e.preventDefault(); + const newId = state.pasteItem(centerX, centerY); + if (newId) state.select(newId); + } else if (mod && e.key === 'd' && state.selectedId) { + e.preventDefault(); + const newId = state.duplicateItem(state.selectedId, centerX, centerY); + if (newId) state.select(newId); + } else if (e.key === 'Escape') { + state.select(null); + state.focus(null); + } else if (e.key === '[' && state.selectedId) { + e.preventDefault(); + state.sendBackward(state.selectedId); + } else if (e.key === ']' && state.selectedId) { + e.preventDefault(); + state.bringForward(state.selectedId); + } else if (e.key.startsWith('Arrow') && state.selectedId) { + e.preventDefault(); + const step = e.shiftKey ? 10 : 1; + const item = state.getItem(state.selectedId); + if (!item) return; + const dx = e.key === 'ArrowLeft' ? -step : e.key === 'ArrowRight' ? step : 0; + const dy = e.key === 'ArrowUp' ? -step : e.key === 'ArrowDown' ? step : 0; + state.updateItem(state.selectedId, { x: item.x + dx, y: item.y + dy }); + } else if (e.key === 'e' && state.selectedId && !mod) { + e.preventDefault(); + state.focus(state.selectedId); + state.edit(state.selectedId); + } + } - +
(mode === 'item' ? 'html' : 'shared'); let editMode = $state('normal'); + let previousItemId = $state(null); function getKeymapExtension(m: EditMode) { switch (m) { @@ -152,6 +153,22 @@ sharedEditor?.dispatch({ effects: keymapCompartment.reconfigure(ext) }); appEditor?.dispatch({ effects: keymapCompartment.reconfigure(ext) }); }); + + $effect(() => { + if (mode === 'item' && item && item.id !== previousItemId) { + previousItemId = item.id; + if (htmlEditor) { + htmlEditor.dispatch({ + changes: { from: 0, to: htmlEditor.state.doc.length, insert: item.html } + }); + } + if (cssEditor) { + cssEditor.dispatch({ + changes: { from: 0, to: cssEditor.state.doc.length, insert: item.css } + }); + } + } + });
diff --git a/src/lib/Item.svelte b/src/lib/Item.svelte index 5fcc7dd..323f061 100644 --- a/src/lib/Item.svelte +++ b/src/lib/Item.svelte @@ -153,6 +153,7 @@ function handleDoubleClick(e: MouseEvent) { e.stopPropagation(); + appState.select(item.id); appState.focus(item.id); appState.edit(item.id); } diff --git a/src/lib/Toolbar.svelte b/src/lib/Toolbar.svelte index 9e8b84b..8d631a8 100644 --- a/src/lib/Toolbar.svelte +++ b/src/lib/Toolbar.svelte @@ -1,5 +1,5 @@ @@ -75,6 +77,7 @@ {/each}
+ {coordX}, {coordY}
- {zoomPercent}% +
@@ -198,10 +204,21 @@ background: var(--accent, #4a9eff); } - .zoom-label { + .coords { font-size: 11px; color: var(--text-dim, #666); - min-width: 36px; - text-align: right; + display: flex; + align-items: center; + padding: 0 8px; + font-variant-numeric: tabular-nums; + } + + .zoom-reset { + display: flex; + align-items: center; + gap: 4px; + padding: 2px 6px; + font-size: 11px; + min-width: 52px; } diff --git a/src/lib/state.svelte.ts b/src/lib/state.svelte.ts index 392ab50..d0b332e 100644 --- a/src/lib/state.svelte.ts +++ b/src/lib/state.svelte.ts @@ -76,6 +76,7 @@ function createState() { let editingId = $state(null); let editingGlobal = $state(false); let focusedId = $state(null); + let clipboard = $state | null>(null); let saveTimeout: ReturnType | null = null; let animationId: number | null = null; @@ -201,6 +202,40 @@ function createState() { save(); } + function copyItem(id: string) { + const item = manifest.items.find((i) => i.id === id); + if (!item) return; + const { id: _, ...rest } = item; + clipboard = rest; + } + + function pasteItem(x: number, y: number): string | null { + if (!clipboard) return null; + const id = crypto.randomUUID(); + const newItem = { ...clipboard, id, x, y, zIndex: maxZIndex + 1 }; + addItem(newItem); + return id; + } + + function duplicateItem(id: string, x: number, y: number): string | null { + copyItem(id); + return pasteItem(x, y); + } + + function sendBackward(id: string) { + const item = manifest.items.find((i) => i.id === id); + if (!item || item.zIndex <= 0) return; + item.zIndex -= 1; + save(); + } + + function bringForward(id: string) { + const item = manifest.items.find((i) => i.id === id); + if (!item) return; + item.zIndex += 1; + save(); + } + function pan(dx: number, dy: number) { viewport.x += dx; viewport.y += dy; @@ -218,6 +253,10 @@ function createState() { viewport.zoom = Math.max(0.1, Math.min(10, zoom)); } + function resetViewport() { + animateViewport(0, 0, 1); + } + function hasFlag(key: string): boolean { return !!manifest.flags?.[key]; } @@ -343,9 +382,15 @@ function createState() { updateSharedCss, updateAppCss, bringToFront, + copyItem, + pasteItem, + duplicateItem, + sendBackward, + bringForward, pan, zoomAt, setZoom, + resetViewport, hasFlag, setFlag, clearFlag,