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,