From 1d79473bc6f709f06c8bfe4c7997a92c0dc4eb3f Mon Sep 17 00:00:00 2001 From: Danila Poyarkov Date: Sat, 28 Feb 2026 13:12:27 +0300 Subject: [PATCH] Pages support: multi-page documents like Figma MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Document structure: Document (root) → Pages (CANVAS) → Nodes. Each page has its own viewport (pan/zoom) and background color. - CANVAS node type for pages - SceneGraph: addPage(), getPages(), getAbsolutePosition stops at CANVAS - Store: currentPageId, switchPage(), addPage(), deletePage(), renamePage() - All operations (create, select, paste, hit test, render) scoped to current page - Per-page viewport state saved/restored on page switch - Pages list in LayersPanel with add/switch/double-click rename - .fig import creates proper pages from DOCUMENT→CANVAS hierarchy - Renderer takes pageId to render correct page's children - Unit test for pages, updated E2E tests for page-scoped graph --- .gitignore | 2 + components.d.ts | 1 + src/components/LayersPanel.vue | 42 +++++++++- src/composables/use-canvas-input.ts | 16 ++-- src/composables/use-canvas.ts | 1 + src/demo.ts | 2 +- src/engine/renderer.ts | 22 +++--- src/engine/scene-graph.ts | 24 ++++-- src/kiwi/fig-import.ts | 93 ++++++++++++++++------- src/stores/editor.ts | 114 ++++++++++++++++++++++++---- tests/e2e/layers-panel.spec.ts | 2 +- tests/engine/scene-graph.test.ts | 22 +++++- 12 files changed, 267 insertions(+), 74 deletions(-) diff --git a/.gitignore b/.gitignore index 954717375..50025f26c 100644 --- a/.gitignore +++ b/.gitignore @@ -26,3 +26,5 @@ public/canvaskit.wasm *.sln *.sw? test-results/ +# Git worktrees for parallel agent work +.worktrees/ diff --git a/components.d.ts b/components.d.ts index 91f602130..be8db16f6 100644 --- a/components.d.ts +++ b/components.d.ts @@ -32,6 +32,7 @@ declare module 'vue' { IconLucideChevronRight: typeof import('~icons/lucide/chevron-right')['default'] IconLucideEye: typeof import('~icons/lucide/eye')['default'] IconLucideEyeOff: typeof import('~icons/lucide/eye-off')['default'] + IconLucideFile: typeof import('~icons/lucide/file')['default'] IconLucideFlipHorizontal: typeof import('~icons/lucide/flip-horizontal')['default'] IconLucideFlipVertical: typeof import('~icons/lucide/flip-vertical')['default'] IconLucideRadius: typeof import('~icons/lucide/radius')['default'] diff --git a/src/components/LayersPanel.vue b/src/components/LayersPanel.vue index 928a369b0..dfd923d3d 100644 --- a/src/components/LayersPanel.vue +++ b/src/components/LayersPanel.vue @@ -48,17 +48,22 @@ function buildTree(parentId: string): LayerNode[] { })) } -const items = ref(buildTree(store.graph.rootId)) +const items = ref(buildTree(store.state.currentPageId)) const treeKey = ref(0) watch( - () => store.state.renderVersion, + [() => store.state.renderVersion, () => store.state.currentPageId], () => { - items.value = buildTree(store.graph.rootId) + items.value = buildTree(store.state.currentPageId) treeKey.value++ } ) +const pages = computed(() => { + void store.state.renderVersion + return store.graph.getPages() +}) + const expanded = ref([]) function onSelect(ev: CustomEvent) { @@ -71,6 +76,13 @@ function onSelect(ev: CustomEvent) { } } +function renamePage(pageId: string, currentName: string) { + const name = prompt('Rename page', currentName) + if (name && name !== currentName) { + store.renamePage(pageId, name) + } +} + function toggleExpand(id: string) { const idx = expanded.value.indexOf(id) if (idx >= 0) { @@ -201,6 +213,30 @@ function updateDropTarget(ev: PointerEvent) {