Improved demo: section with Desktop/Mobile frames, color swatches

- 'Design System' section containing Desktop and Mobile frames
- Desktop: header, hero card, avatar, two content cards
- Mobile: status bar, banner, list items, FAB button
- Color palette swatches (Primary, Success, Warning, Danger, Purple)
- Updated E2E tests for new demo structure
- Reverted section title pill to always use fill color (not blue)
This commit is contained in:
Danila Poyarkov 2026-02-28 14:02:46 +03:00
parent 59a98f9186
commit 27f57da340
3 changed files with 104 additions and 81 deletions

View file

@ -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([])
}

View file

@ -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<string>): void {
if (!this.sectionTitleFont) return
const pageNode = graph.getNode(this.pageId ?? graph.rootId)

View file

@ -61,92 +61,85 @@ async function getSelectedCount(): Promise<number> {
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()
})