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:
parent
59a98f9186
commit
27f57da340
113
src/demo.ts
113
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([])
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
})
|
||||
|
|
|
|||
Loading…
Reference in a new issue