OK
This commit is contained in:
@@ -125,9 +125,56 @@
|
|||||||
function handleDragOver(e: DragEvent) {
|
function handleDragOver(e: DragEvent) {
|
||||||
e.preventDefault();
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:window onmouseup={handleMouseUp} onmousemove={handleMouseMove} />
|
<svelte:window onmouseup={handleMouseUp} onmousemove={handleMouseMove} onkeydown={handleKeyDown} />
|
||||||
|
|
||||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
|
|
||||||
let activeTab = $state<'html' | 'css' | 'shared' | 'app'>(mode === 'item' ? 'html' : 'shared');
|
let activeTab = $state<'html' | 'css' | 'shared' | 'app'>(mode === 'item' ? 'html' : 'shared');
|
||||||
let editMode = $state<EditMode>('normal');
|
let editMode = $state<EditMode>('normal');
|
||||||
|
let previousItemId = $state<string | null>(null);
|
||||||
|
|
||||||
function getKeymapExtension(m: EditMode) {
|
function getKeymapExtension(m: EditMode) {
|
||||||
switch (m) {
|
switch (m) {
|
||||||
@@ -152,6 +153,22 @@
|
|||||||
sharedEditor?.dispatch({ effects: keymapCompartment.reconfigure(ext) });
|
sharedEditor?.dispatch({ effects: keymapCompartment.reconfigure(ext) });
|
||||||
appEditor?.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 }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="panel">
|
<div class="panel">
|
||||||
|
|||||||
@@ -153,6 +153,7 @@
|
|||||||
|
|
||||||
function handleDoubleClick(e: MouseEvent) {
|
function handleDoubleClick(e: MouseEvent) {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
appState.select(item.id);
|
||||||
appState.focus(item.id);
|
appState.focus(item.id);
|
||||||
appState.edit(item.id);
|
appState.edit(item.id);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Upload, Download, Paintbrush, Trash2, EyeOff } from 'lucide-svelte';
|
import { Upload, Download, Paintbrush, Trash2, EyeOff, ZoomIn } from 'lucide-svelte';
|
||||||
import { exportBoard, importBoard } from './io';
|
import { exportBoard, importBoard } from './io';
|
||||||
import { state } from './state.svelte';
|
import { state } from './state.svelte';
|
||||||
import Palette from './Palette.svelte';
|
import Palette from './Palette.svelte';
|
||||||
@@ -55,6 +55,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
let zoomPercent = $derived(Math.round(state.viewport.zoom * 100));
|
let zoomPercent = $derived(Math.round(state.viewport.zoom * 100));
|
||||||
|
let coordX = $derived(Math.round(-state.viewport.x / state.viewport.zoom));
|
||||||
|
let coordY = $derived(Math.round(-state.viewport.y / state.viewport.zoom));
|
||||||
const flagKeys = ['1', '2', '3', '4', '5', '6', '7', '8', '9'];
|
const flagKeys = ['1', '2', '3', '4', '5', '6', '7', '8', '9'];
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -75,6 +77,7 @@
|
|||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
<div class="spacer"></div>
|
<div class="spacer"></div>
|
||||||
|
<span class="coords">{coordX}, {coordY}</span>
|
||||||
<div class="zoom">
|
<div class="zoom">
|
||||||
<input
|
<input
|
||||||
type="range"
|
type="range"
|
||||||
@@ -85,7 +88,10 @@
|
|||||||
oninput={handleZoom}
|
oninput={handleZoom}
|
||||||
title="Zoom"
|
title="Zoom"
|
||||||
/>
|
/>
|
||||||
<span class="zoom-label">{zoomPercent}%</span>
|
<button class="zoom-reset" onclick={() => state.resetViewport()} title="Reset view (0, 0 at 100%)">
|
||||||
|
<ZoomIn size={12} />
|
||||||
|
<span>{zoomPercent}%</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<button onclick={() => state.editGlobal(true)} title="Style"><Paintbrush size={14} /></button>
|
<button onclick={() => state.editGlobal(true)} title="Style"><Paintbrush size={14} /></button>
|
||||||
<button onclick={handleClear} title="Clear"><Trash2 size={14} /></button>
|
<button onclick={handleClear} title="Clear"><Trash2 size={14} /></button>
|
||||||
@@ -198,10 +204,21 @@
|
|||||||
background: var(--accent, #4a9eff);
|
background: var(--accent, #4a9eff);
|
||||||
}
|
}
|
||||||
|
|
||||||
.zoom-label {
|
.coords {
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
color: var(--text-dim, #666);
|
color: var(--text-dim, #666);
|
||||||
min-width: 36px;
|
display: flex;
|
||||||
text-align: right;
|
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;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -76,6 +76,7 @@ function createState() {
|
|||||||
let editingId = $state<string | null>(null);
|
let editingId = $state<string | null>(null);
|
||||||
let editingGlobal = $state<boolean>(false);
|
let editingGlobal = $state<boolean>(false);
|
||||||
let focusedId = $state<string | null>(null);
|
let focusedId = $state<string | null>(null);
|
||||||
|
let clipboard = $state<Omit<Item, 'id'> | null>(null);
|
||||||
|
|
||||||
let saveTimeout: ReturnType<typeof setTimeout> | null = null;
|
let saveTimeout: ReturnType<typeof setTimeout> | null = null;
|
||||||
let animationId: number | null = null;
|
let animationId: number | null = null;
|
||||||
@@ -201,6 +202,40 @@ function createState() {
|
|||||||
save();
|
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) {
|
function pan(dx: number, dy: number) {
|
||||||
viewport.x += dx;
|
viewport.x += dx;
|
||||||
viewport.y += dy;
|
viewport.y += dy;
|
||||||
@@ -218,6 +253,10 @@ function createState() {
|
|||||||
viewport.zoom = Math.max(0.1, Math.min(10, zoom));
|
viewport.zoom = Math.max(0.1, Math.min(10, zoom));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resetViewport() {
|
||||||
|
animateViewport(0, 0, 1);
|
||||||
|
}
|
||||||
|
|
||||||
function hasFlag(key: string): boolean {
|
function hasFlag(key: string): boolean {
|
||||||
return !!manifest.flags?.[key];
|
return !!manifest.flags?.[key];
|
||||||
}
|
}
|
||||||
@@ -343,9 +382,15 @@ function createState() {
|
|||||||
updateSharedCss,
|
updateSharedCss,
|
||||||
updateAppCss,
|
updateAppCss,
|
||||||
bringToFront,
|
bringToFront,
|
||||||
|
copyItem,
|
||||||
|
pasteItem,
|
||||||
|
duplicateItem,
|
||||||
|
sendBackward,
|
||||||
|
bringForward,
|
||||||
pan,
|
pan,
|
||||||
zoomAt,
|
zoomAt,
|
||||||
setZoom,
|
setZoom,
|
||||||
|
resetViewport,
|
||||||
hasFlag,
|
hasFlag,
|
||||||
setFlag,
|
setFlag,
|
||||||
clearFlag,
|
clearFlag,
|
||||||
|
|||||||
Reference in New Issue
Block a user