Tier 1 — Core API fixes: - updateNodeWithUndo now calls requestRender internally, eliminating 15+ redundant requestRender() calls across 6 property panel components - New editor/variables.ts: 7 undo-able variable CRUD operations (renameCollection, addCollection, removeCollection, addVariable, removeVariable, renameVariable, updateVariableValue) - New editor/alignment.ts: alignNodes, flipNodes, rotateNodes — moves bounding box geometry out of PositionSection.vue Tier 2 — Composable cleanup: - Merged use-node-props + use-multi-props into single useNodeProps() with MIXED sentinel, eliminating duplicate computed refs - Added toolCursor() utility — replaces if/else chain in EditorCanvas - Renamed use-toast.ts → toast.ts (singleton module, not a composable) Tier 3 — use-canvas-input.ts split (1497 → 817 lines): - input/types.ts (88) — DragState variants, HandlePosition, TOOL_TO_NODE - input/geometry.ts (170) — hit testing, handle positions, rotation cursor - input/pan-zoom.ts (266) — wheel, touch pinch, Safari gestures - input/resize.ts (121) — constrainToAspectRatio, applyResize - input/auto-layout.ts (103) — insert indicator computation All 955 tests pass, 0 lint errors, 1.48% duplication.
95 lines
3 KiB
Vue
95 lines
3 KiB
Vue
<script setup lang="ts">
|
|
import { ref, computed, watch } from 'vue'
|
|
|
|
import { useCanvas } from '@/composables/use-canvas'
|
|
import { useCanvasDrop } from '@/composables/use-canvas-drop'
|
|
import { useCanvasInput } from '@/composables/use-canvas-input'
|
|
import { useCollabInjected } from '@/composables/use-collab'
|
|
import { useTextEdit } from '@/composables/use-text-edit'
|
|
import { useEditorStore } from '@/stores/editor'
|
|
import { toolCursor } from '@open-pencil/vue'
|
|
import CanvasContextMenu from './CanvasContextMenu.vue'
|
|
|
|
const store = useEditorStore()
|
|
const collab = useCollabInjected()
|
|
const canvasRef = ref<HTMLCanvasElement | null>(null)
|
|
|
|
const { hitTestSectionTitle, hitTestComponentLabel, hitTestFrameTitle } = useCanvas(
|
|
canvasRef,
|
|
store
|
|
)
|
|
const { cursorOverride } = useCanvasInput(
|
|
canvasRef,
|
|
store,
|
|
hitTestSectionTitle,
|
|
hitTestComponentLabel,
|
|
hitTestFrameTitle,
|
|
(cx, cy) => collab?.updateCursor(cx, cy, store.state.currentPageId)
|
|
)
|
|
|
|
useTextEdit(canvasRef, store)
|
|
const { isDraggingOver } = useCanvasDrop(canvasRef, store)
|
|
|
|
watch(
|
|
() => [...store.state.selectedIds],
|
|
(ids) => collab?.updateSelection(ids)
|
|
)
|
|
|
|
const cursor = computed(() => toolCursor(store.state.activeTool, cursorOverride.value))
|
|
</script>
|
|
|
|
<template>
|
|
<CanvasContextMenu>
|
|
<div
|
|
data-test-id="canvas-area"
|
|
class="canvas-area relative min-h-0 min-w-0 flex-1 overflow-hidden"
|
|
>
|
|
<canvas
|
|
ref="canvasRef"
|
|
data-test-id="canvas-element"
|
|
:style="{ cursor }"
|
|
class="block size-full touch-none"
|
|
/>
|
|
<Transition
|
|
enter-active-class="transition-opacity duration-150"
|
|
enter-from-class="opacity-0"
|
|
leave-active-class="transition-opacity duration-150"
|
|
leave-to-class="opacity-0"
|
|
>
|
|
<div
|
|
v-if="isDraggingOver"
|
|
class="pointer-events-none absolute inset-0 z-40 border-2 border-dashed border-accent/60 bg-accent/5"
|
|
/>
|
|
</Transition>
|
|
<Transition leave-active-class="transition-opacity duration-300" leave-to-class="opacity-0">
|
|
<div
|
|
v-if="store.state.loading"
|
|
data-test-id="canvas-loading"
|
|
class="absolute inset-0 z-50 flex items-center justify-center bg-canvas"
|
|
>
|
|
<svg
|
|
class="size-8 text-white opacity-40"
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
stroke-width="1.5"
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
>
|
|
<path
|
|
d="m15.232 5.232 3.536 3.536m-2.036-5.036a2.5 2.5 0 0 1 3.536 3.536L6.5 21.036H3v-3.572L16.732 3.732Z"
|
|
/>
|
|
</svg>
|
|
<div
|
|
class="absolute bottom-1/2 left-1/2 h-0.5 w-25 -translate-x-1/2 translate-y-10 overflow-hidden rounded-full bg-white/8"
|
|
>
|
|
<div
|
|
class="h-full w-2/5 animate-[slide_1s_ease-in-out_infinite] rounded-full bg-white/25"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</Transition>
|
|
</div>
|
|
</CanvasContextMenu>
|
|
</template>
|