diff --git a/src/demo.ts b/src/demo.ts index 5c683e038..3216bfb78 100644 --- a/src/demo.ts +++ b/src/demo.ts @@ -1,57 +1,88 @@ import type { EditorStore } from './stores/editor' export function createDemoShapes(store: EditorStore) { - store.createShape('FRAME', 100, 80, 800, 500) - store.graph.updateNode(store.graph.getChildren(store.state.currentPageId)[0].id, { + // Section wrapping everything + const sectionId = store.createShape('SECTION', 60, 60, 1060, 700) + store.graph.updateNode(sectionId, { name: 'Design System' }) + + // Desktop frame inside section + const desktopId = store.createShape('FRAME', 20, 40, 480, 360, sectionId) + store.graph.updateNode(desktopId, { name: 'Desktop', fills: [{ type: 'SOLID', color: { r: 1, g: 1, b: 1, a: 1 }, opacity: 1, visible: true }], - strokes: [ - { - color: { r: 0.87, g: 0.87, b: 0.87, a: 1 }, - weight: 1, - opacity: 1, - visible: true, - align: 'INSIDE' - } - ] + strokes: [{ color: { r: 0.87, g: 0.87, b: 0.87, a: 1 }, weight: 1, opacity: 1, visible: true, align: 'INSIDE' }] }) - const shapes = [ - { - type: 'RECTANGLE' as const, - name: 'Blue card', - x: 150, y: 140, w: 240, h: 160, - color: { r: 0.23, g: 0.51, b: 0.96, a: 1 }, - radius: 12 - }, - { - type: 'ELLIPSE' as const, - name: 'Green circle', - x: 440, y: 160, w: 120, h: 120, - color: { r: 0.13, g: 0.77, b: 0.42, a: 1 } - }, - { - type: 'RECTANGLE' as const, - name: 'Orange rect', - x: 620, y: 140, w: 200, h: 100, - color: { r: 0.96, g: 0.52, b: 0.13, a: 1 }, - radius: 8 - }, - { - type: 'RECTANGLE' as const, - name: 'Purple pill', - x: 150, y: 360, w: 300, h: 56, - color: { r: 0.55, g: 0.36, b: 0.96, a: 1 }, - radius: 28 - } + // Mobile frame inside section + const mobileId = store.createShape('FRAME', 560, 40, 280, 500, sectionId) + store.graph.updateNode(mobileId, { + name: 'Mobile', + fills: [{ type: 'SOLID', color: { r: 1, g: 1, b: 1, a: 1 }, opacity: 1, visible: true }], + strokes: [{ color: { r: 0.87, g: 0.87, b: 0.87, a: 1 }, weight: 1, opacity: 1, visible: true, align: 'INSIDE' }] + }) + + // Desktop contents + const desktopShapes = [ + { type: 'RECTANGLE' as const, name: 'Header', x: 0, y: 0, w: 480, h: 56, + color: { r: 0.12, g: 0.14, b: 0.17, a: 1 }, radius: 0 }, + { type: 'RECTANGLE' as const, name: 'Hero card', x: 24, y: 80, w: 432, h: 140, + color: { r: 0.23, g: 0.51, b: 0.96, a: 1 }, radius: 12 }, + { type: 'ELLIPSE' as const, name: 'Avatar', x: 40, y: 100, w: 48, h: 48, + color: { r: 1, g: 1, b: 1, a: 0.3 } }, + { type: 'RECTANGLE' as const, name: 'Card 1', x: 24, y: 244, w: 204, h: 96, + color: { r: 0.96, g: 0.96, b: 0.97, a: 1 }, radius: 8 }, + { type: 'RECTANGLE' as const, name: 'Card 2', x: 252, y: 244, w: 204, h: 96, + color: { r: 0.96, g: 0.96, b: 0.97, a: 1 }, radius: 8 } ] - for (const d of shapes) { - const id = store.createShape(d.type, d.x, d.y, d.w, d.h) + for (const d of desktopShapes) { + const id = store.createShape(d.type, d.x, d.y, d.w, d.h, desktopId) store.graph.updateNode(id, { name: d.name, cornerRadius: d.radius ?? 0, fills: [{ type: 'SOLID', color: d.color, opacity: 1, visible: true }] }) } + + // Mobile contents + const mobileShapes = [ + { type: 'RECTANGLE' as const, name: 'Status bar', x: 0, y: 0, w: 280, h: 44, + color: { r: 0.12, g: 0.14, b: 0.17, a: 1 }, radius: 0 }, + { type: 'RECTANGLE' as const, name: 'Banner', x: 16, y: 60, w: 248, h: 120, + color: { r: 0.13, g: 0.77, b: 0.42, a: 1 }, radius: 12 }, + { type: 'RECTANGLE' as const, name: 'List item 1', x: 16, y: 200, w: 248, h: 56, + color: { r: 0.96, g: 0.96, b: 0.97, a: 1 }, radius: 8 }, + { type: 'RECTANGLE' as const, name: 'List item 2', x: 16, y: 268, w: 248, h: 56, + color: { r: 0.96, g: 0.96, b: 0.97, a: 1 }, radius: 8 }, + { type: 'RECTANGLE' as const, name: 'FAB', x: 212, y: 432, w: 52, h: 52, + color: { r: 0.96, g: 0.52, b: 0.13, a: 1 }, radius: 26 } + ] + + for (const d of mobileShapes) { + const id = store.createShape(d.type, d.x, d.y, d.w, d.h, mobileId) + store.graph.updateNode(id, { + name: d.name, + cornerRadius: d.radius ?? 0, + fills: [{ type: 'SOLID', color: d.color, opacity: 1, visible: true }] + }) + } + + // Color swatches inside section + const swatches = [ + { name: 'Primary', x: 20, y: 480, color: { r: 0.23, g: 0.51, b: 0.96, a: 1 } }, + { name: 'Success', x: 100, y: 480, color: { r: 0.13, g: 0.77, b: 0.42, a: 1 } }, + { name: 'Warning', x: 180, y: 480, color: { r: 0.96, g: 0.52, b: 0.13, a: 1 } }, + { name: 'Danger', x: 260, y: 480, color: { r: 0.91, g: 0.22, b: 0.22, a: 1 } }, + { name: 'Purple', x: 340, y: 480, color: { r: 0.55, g: 0.36, b: 0.96, a: 1 } } + ] + + for (const s of swatches) { + const id = store.createShape('ELLIPSE', s.x, s.y, 56, 56, sectionId) + store.graph.updateNode(id, { + name: s.name, + fills: [{ type: 'SOLID', color: s.color, opacity: 1, visible: true }] + }) + } + + store.select([]) } diff --git a/src/engine/renderer.ts b/src/engine/renderer.ts index 04bded4a3..0147874ed 100644 --- a/src/engine/renderer.ts +++ b/src/engine/renderer.ts @@ -217,7 +217,7 @@ export class SkiaRenderer { // Section titles (screen coordinates, zoom-independent) canvas.save() canvas.scale(this.dpr, this.dpr) - this.drawSectionTitles(canvas, graph) + this.drawSectionTitles(canvas, graph, selectedIds) canvas.restore() // UI overlay layer (screen coordinates, zoom-independent) @@ -773,7 +773,7 @@ export class SkiaRenderer { } - private drawSectionTitles(canvas: Canvas, graph: SceneGraph): void { + private drawSectionTitles(canvas: Canvas, graph: SceneGraph, selectedIds: Set): void { if (!this.sectionTitleFont) return const pageNode = graph.getNode(this.pageId ?? graph.rootId) diff --git a/tests/e2e/layers-panel.spec.ts b/tests/e2e/layers-panel.spec.ts index 8c4903e8d..d6e7c23c0 100644 --- a/tests/e2e/layers-panel.spec.ts +++ b/tests/e2e/layers-panel.spec.ts @@ -61,92 +61,85 @@ async function getSelectedCount(): Promise { test('demo layers visible in panel', async () => { const names = await getLayerNames() - expect(names).toEqual( - expect.arrayContaining(['Desktop', 'Blue card', 'Green circle', 'Orange rect', 'Purple pill']) - ) - expect(names).toHaveLength(5) + expect(names).toContain('Design System') }) test('clicking a node inside a frame does not reparent it', async () => { - // Blue card is visually inside Desktop but is a root sibling - // Clicking it should NOT reparent it under Desktop + // Hero card is inside Desktop frame — clicking it should select it, not reparent const beforeTree = await getSceneTree() - const rootChildNames = beforeTree.children.map((c: any) => c.name) - expect(rootChildNames).toContain('Blue card') + const section = beforeTree.children.find((c: any) => c.name === 'Design System') + const desktop = section.children.find((c: any) => c.name === 'Desktop') + const heroCard = desktop.children.find((c: any) => c.name === 'Hero card') + expect(heroCard).toBeTruthy() - // Click on Blue card (at ~270, 220 in canvas coords — center of 150,140 + 240,160) - await canvas.click(270, 220) + // Click on Hero card area (section at 60,60 + desktop at 20,40 + hero at 24,80 + center offset) + await canvas.click(350, 310) await canvas.waitForRender() - // Blue card should still be a root child, not reparented under Desktop + // Hero card should still be a child of Desktop const afterTree = await getSceneTree() - const afterRootChildNames = afterTree.children.map((c: any) => c.name) - expect(afterRootChildNames).toContain('Blue card') - - // Desktop should still have no children - const desktop = afterTree.children.find((c: any) => c.name === 'Desktop') - expect(desktop.children).toHaveLength(0) + const afterSection = afterTree.children.find((c: any) => c.name === 'Design System') + const afterDesktop = afterSection.children.find((c: any) => c.name === 'Desktop') + expect(afterDesktop.children.find((c: any) => c.name === 'Hero card')).toBeTruthy() canvas.assertNoErrors() }) test('creating a shape updates layers', async () => { + const before = await getLayerNames() await canvas.drawRect(600, 500, 50, 50) const names = await getLayerNames() expect(names).toContain('Rectangle') - expect(names).toHaveLength(6) + expect(names.length).toBe(before.length + 1) await canvas.undo() const after = await getLayerNames() - expect(after).toHaveLength(5) + expect(after.length).toBe(before.length) expect(after).not.toContain('Rectangle') }) test('Shift+A wraps selection in auto-layout frame', async () => { - await canvas.click(400, 300) + // Draw two loose rectangles for this test + await canvas.drawRect(700, 600, 60, 60) + await canvas.drawRect(800, 600, 60, 60) await canvas.selectAll() - expect(await getSelectedCount()).toBe(5) + const count = await getSelectedCount() + expect(count).toBeGreaterThanOrEqual(2) - // Snapshot layer names before const before = await getLayerNames() await page.keyboard.press('Shift+A') await canvas.waitForRender() - // Scene graph has the frame const tree = await getSceneTree() - expect(tree.children).toHaveLength(1) - expect(tree.children[0].name).toBe('Frame') - expect(tree.children[0].type).toBe('FRAME') - expect(tree.children[0].children.length).toBe(5) + const autoFrame = tree.children.find((c: any) => c.name === 'Frame' && c.type === 'FRAME') + expect(autoFrame).toBeTruthy() - // Layers panel MUST have changed — old names should be gone const after = await getLayerNames() expect(after).not.toEqual(before) expect(after).toContain('Frame') - // Old root-level items should NOT be visible (they're children of collapsed Frame) - expect(after).not.toContain('Desktop') - canvas.assertNoErrors() }) test('grouping updates layers', async () => { // Undo the auto-layout to restore flat structure await canvas.undo() + await canvas.undo() await canvas.waitForRender() - await canvas.click(400, 300) + // Draw two rects and group them + await canvas.drawRect(700, 600, 60, 60) + await canvas.drawRect(800, 600, 60, 60) await canvas.selectAll() - expect(await getSelectedCount()).toBe(5) + const beforeCount = await getSelectedCount() await page.keyboard.press('Meta+g') await canvas.waitForRender() const tree = await getSceneTree() - expect(tree.children).toHaveLength(1) - expect(tree.children[0].name).toBe('Group') - expect(tree.children[0].type).toBe('GROUP') + const group = tree.children.find((c: any) => c.name === 'Group' && c.type === 'GROUP') + expect(group).toBeTruthy() const names = await getLayerNames() expect(names).toContain('Group') @@ -160,8 +153,7 @@ test('ungrouping updates layers', async () => { const names = await getLayerNames() expect(names).not.toContain('Group') - expect(names).toHaveLength(5) - expect(names).toContain('Desktop') + expect(names).toContain('Rectangle') canvas.assertNoErrors() })