revert(demo): restore the visual-features showcase demo
Roll back the Pulse music-app demo to the prior standalone showcase (Components / App Preview / Effects) which cleanly demonstrates gradients, drop/inner shadows, blend modes, masks, and corner smoothing. Also removed the Pulse JSX showcase file and restored the matching demo test.
This commit is contained in:
parent
49fa36ec12
commit
bc73b15f0c
|
|
@ -1,31 +1,20 @@
|
|||
import { BLACK } from '@open-pencil/core/constants'
|
||||
import type { Color, Fill, GradientStop, Stroke } from '@open-pencil/scene-graph'
|
||||
|
||||
/** A single restrained product palette — modern, not a rainbow. */
|
||||
export const DEMO_COLORS = {
|
||||
white: { r: 1, g: 1, b: 1, a: 1 },
|
||||
black: BLACK,
|
||||
// surfaces
|
||||
bg: { r: 0.97, g: 0.97, b: 0.98, a: 1 },
|
||||
surface: { r: 1, g: 1, b: 1, a: 1 },
|
||||
surfaceSunken: { r: 0.96, g: 0.96, b: 0.98, a: 1 },
|
||||
// text
|
||||
textPrimary: { r: 0.09, g: 0.09, b: 0.11, a: 1 },
|
||||
textSecondary: { r: 0.45, g: 0.46, b: 0.5, a: 1 },
|
||||
textTertiary: { r: 0.66, g: 0.67, b: 0.71, a: 1 },
|
||||
// border
|
||||
border: { r: 0.9, g: 0.9, b: 0.93, a: 1 },
|
||||
borderStrong: { r: 0.84, g: 0.84, b: 0.88, a: 1 },
|
||||
// one accent + supporting semantics
|
||||
accent: { r: 0.36, g: 0.4, b: 0.96, a: 1 },
|
||||
accentSoft: { r: 0.93, g: 0.93, b: 1, a: 1 },
|
||||
accentText: { r: 0.28, g: 0.32, b: 0.9, a: 1 },
|
||||
success: { r: 0.13, g: 0.72, b: 0.42, a: 1 },
|
||||
successSoft: { r: 0.9, g: 0.98, b: 0.93, a: 1 },
|
||||
danger: { r: 0.92, g: 0.26, b: 0.28, a: 1 },
|
||||
dangerSoft: { r: 1, g: 0.93, b: 0.93, a: 1 },
|
||||
warning: { r: 0.96, g: 0.62, b: 0.1, a: 1 },
|
||||
warningSoft: { r: 1, g: 0.96, b: 0.88, a: 1 }
|
||||
gray50: { r: 0.98, g: 0.98, b: 0.98, a: 1 },
|
||||
gray100: { r: 0.96, g: 0.96, b: 0.97, a: 1 },
|
||||
gray200: { r: 0.9, g: 0.9, b: 0.92, a: 1 },
|
||||
gray500: { r: 0.55, g: 0.55, b: 0.58, a: 1 },
|
||||
blue: { r: 0.23, g: 0.51, b: 0.96, a: 1 },
|
||||
indigo: { r: 0.38, g: 0.35, b: 0.95, a: 1 },
|
||||
purple: { r: 0.59, g: 0.28, b: 0.96, a: 1 },
|
||||
green: { r: 0.13, g: 0.77, b: 0.42, a: 1 },
|
||||
orange: { r: 0.96, g: 0.52, b: 0.13, a: 1 },
|
||||
red: { r: 0.91, g: 0.22, b: 0.22, a: 1 },
|
||||
teal: { r: 0.08, g: 0.73, b: 0.73, a: 1 }
|
||||
} satisfies Record<string, Color>
|
||||
|
||||
export function solid(color: Color, opacity = 1): Fill {
|
||||
|
|
@ -46,7 +35,3 @@ export function gradient(stops: GradientStop[]): Fill {
|
|||
export function thinStroke(color: Color): Stroke[] {
|
||||
return [{ color, weight: 1, opacity: 1, visible: true, align: 'INSIDE' as const }]
|
||||
}
|
||||
|
||||
export function noStroke(): Stroke[] {
|
||||
return []
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,47 +1,34 @@
|
|||
import { computeAllLayouts } from '@open-pencil/core/layout'
|
||||
import { renderJSX } from '@open-pencil/core/design-jsx'
|
||||
|
||||
import { DEMO_COLORS, solid } from '@/app/demo/colors'
|
||||
import { SHOWCASE_JSX } from '@/app/demo/jsx/showcase'
|
||||
import { createAppPreviewSection } from '@/app/demo/sections/app-preview'
|
||||
import { createComponentsSection } from '@/app/demo/sections/components'
|
||||
import { createEffectsSection } from '@/app/demo/sections/effects'
|
||||
import { createStandaloneShapes } from '@/app/demo/sections/standalone'
|
||||
import { createDemoVariables } from '@/app/demo/sections/variables'
|
||||
import type { EditorStore } from '@/app/editor/session'
|
||||
|
||||
export async function createDemoShapes(store: EditorStore) {
|
||||
export function createDemoShapes(store: EditorStore) {
|
||||
const { graph } = store
|
||||
|
||||
const comps = createComponentsSection(store)
|
||||
computeAllLayouts(graph)
|
||||
const app = createAppPreviewSection(store, comps)
|
||||
await renderJSX(graph, SHOWCASE_JSX)
|
||||
const { btnCompId, badgeCompId } = createComponentsSection(store)
|
||||
computeAllLayouts(store.graph)
|
||||
const { sidebarId, headerId, frameId, headerTitle, chartTitle } = createAppPreviewSection(
|
||||
store,
|
||||
btnCompId,
|
||||
badgeCompId
|
||||
)
|
||||
|
||||
createStandaloneShapes(store)
|
||||
createDemoVariables(store)
|
||||
createEffectsSection(store)
|
||||
|
||||
// Theme the screen through variables so editing one re-themes the demo.
|
||||
graph.bindVariable(app.sidebar, 'fills/0/color', 'var-bg')
|
||||
graph.bindVariable(app.headerTitle, 'fills/0/color', 'var-text-primary')
|
||||
graph.bindVariable(app.chartTitle, 'fills/0/color', 'var-text-primary')
|
||||
|
||||
computeAllLayouts(graph)
|
||||
|
||||
// Apply badge tones after layout so component sync doesn't revert them.
|
||||
const toneColors = {
|
||||
success: [DEMO_COLORS.successSoft, DEMO_COLORS.success],
|
||||
accent: [DEMO_COLORS.accentSoft, DEMO_COLORS.accent],
|
||||
danger: [DEMO_COLORS.dangerSoft, DEMO_COLORS.danger]
|
||||
} as const
|
||||
for (const b of app.statBadges) {
|
||||
const [soft, strong] = toneColors[b.tone]
|
||||
const badge = graph.getNode(b.id)
|
||||
if (!badge) continue
|
||||
// Record the label fill as an override so component sync preserves it.
|
||||
graph.updateNode(b.labelId, { fills: [solid(strong)] })
|
||||
graph.updateNode(b.id, {
|
||||
fills: [solid(soft)],
|
||||
overrides: { ...badge.overrides, [`${b.labelId}:fills`]: [solid(strong)] }
|
||||
})
|
||||
}
|
||||
graph.bindVariable(sidebarId, 'fills/0/color', 'var-bg')
|
||||
graph.bindVariable(headerId, 'fills/0/color', 'var-bg')
|
||||
graph.bindVariable(frameId, 'fills/0/color', 'var-bg-secondary')
|
||||
graph.bindVariable(headerTitle, 'fills/0/color', 'var-text-primary')
|
||||
graph.bindVariable(chartTitle, 'fills/0/color', 'var-text-primary')
|
||||
|
||||
computeAllLayouts(store.graph)
|
||||
store.clearSelection()
|
||||
void store.loadFontsForNodes(graph.getPages().flatMap((page) => page.childIds)).then(() => {
|
||||
store.requestRender()
|
||||
|
|
|
|||
47
src/app/demo/effects.ts
Normal file
47
src/app/demo/effects.ts
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
import { TRANSPARENT } from '@open-pencil/core/constants'
|
||||
import type { Color, Effect } from '@open-pencil/scene-graph'
|
||||
|
||||
export function dropShadow(
|
||||
ox = 0,
|
||||
oy = 4,
|
||||
radius = 8,
|
||||
spread = 0,
|
||||
color: Color = { r: 0, g: 0, b: 0, a: 0.25 }
|
||||
): Effect {
|
||||
return {
|
||||
type: 'DROP_SHADOW',
|
||||
color,
|
||||
offset: { x: ox, y: oy },
|
||||
radius,
|
||||
spread,
|
||||
visible: true
|
||||
}
|
||||
}
|
||||
|
||||
export function innerShadow(
|
||||
ox = 0,
|
||||
oy = 2,
|
||||
radius = 4,
|
||||
spread = 0,
|
||||
color: Color = { r: 0, g: 0, b: 0, a: 0.2 }
|
||||
): Effect {
|
||||
return {
|
||||
type: 'INNER_SHADOW',
|
||||
color,
|
||||
offset: { x: ox, y: oy },
|
||||
radius,
|
||||
spread,
|
||||
visible: true
|
||||
}
|
||||
}
|
||||
|
||||
export function blurEffect(type: Effect['type'], radius: number): Effect {
|
||||
return {
|
||||
type,
|
||||
color: TRANSPARENT,
|
||||
offset: { x: 0, y: 0 },
|
||||
radius,
|
||||
spread: 0,
|
||||
visible: true
|
||||
}
|
||||
}
|
||||
|
|
@ -1,94 +0,0 @@
|
|||
/**
|
||||
* The showcase page: a music player app authored entirely in JSX. Exercises
|
||||
* masks, gradients, drop shadows, a glassmorphic player bar, blend-mode
|
||||
* overlays, corner smoothing, vector icons, outlined buttons, and
|
||||
* component/instance — all as natural parts of one design.
|
||||
*/
|
||||
export const SHOWCASE_JSX = `
|
||||
<Section name="App — Player" x={1560} y={60} w={560} h={760} bg="#12121A">
|
||||
|
||||
{/* Hero: gradient cover masked into a smooth-rounded card, with a
|
||||
blend-mode legibility overlay and masked listener avatars. */}
|
||||
<Frame name="Hero" x={32} y={32} w={496} h={300} rounded={20} cornerSmoothing={0.6}
|
||||
overflow="hidden" effects={[dropShadow({ x: 0, y: 12, radius: 32, color: '"#4C1A8066"' })]}
|
||||
fill={linearGradient([['#734FF2', 0], ['#F2598C', 1]])}>
|
||||
<Rectangle name="Overlay" x={0} y={160} w={496} h={140} blendMode="multiply"
|
||||
fill={linearGradient([['"#00000000"', 0], ['"#00000099"', 1]])} />
|
||||
<Group name="Listeners" x={24} y={246}>
|
||||
<Ellipse mask w={36} h={36} stroke="#fff" strokeWidth={2}
|
||||
fill={linearGradient([['#F2B366', 0], ['#B3834A', 1]])} />
|
||||
<Rectangle w={36} h={36} fill={linearGradient([['#F2B366', 0], ['#B3834A', 1]])} />
|
||||
<Ellipse mask x={28} w={36} h={36} stroke="#fff" strokeWidth={2}
|
||||
fill={linearGradient([['#66CCF2', 0], ['#3A8AB3', 1]])} />
|
||||
<Rectangle x={28} w={36} h={36} fill={linearGradient([['#66CCF2', 0], ['#3A8AB3', 1]])} />
|
||||
<Ellipse mask x={56} w={36} h={36} stroke="#fff" strokeWidth={2}
|
||||
fill={linearGradient([['#B3E67F', 0], ['#7AB34D', 1]])} />
|
||||
<Rectangle x={56} w={36} h={36} fill={linearGradient([['#B3E67F', 0], ['#7AB34D', 1]])} />
|
||||
</Group>
|
||||
<Text name="Track" x={108} y={252} size={20} weight="bold" color="#FFFFFF">Midnight Reverie</Text>
|
||||
<Text name="Artist" x={108} y={278} size={13} color=""#FFFFFFBF"">Aurora Bloom</Text>
|
||||
</Frame>
|
||||
|
||||
{/* Up-next: album art masked into rounded squares. */}
|
||||
<Text name="Label" x={32} y={356} size={14} weight="bold" color="#FFFFFF">Up next</Text>
|
||||
<Frame name="List" x={32} y={384} w={496} flex="col" gap={8}>
|
||||
<Frame name="Track" w={496} h={56} rounded={12} cornerSmoothing={0.5} bg="#21212E"
|
||||
flex="row" items="center" gap={12} pl={8} pr={12}>
|
||||
<Frame name="Art" w={40} h={40} rounded={9} overflow="hidden"
|
||||
fill={linearGradient([['#4D99F2', 0], ['#994DE6', 1]])}
|
||||
effects={[dropShadow({ x: 0, y: 2, radius: 6, color: '"#0000004C"' })]} />
|
||||
<Frame name="Meta" flex="col" gap={2}>
|
||||
<Text name="Title" size={14} weight="medium" color="#FFFFFF">Glass Cities</Text>
|
||||
<Text name="Artist" size={12} color="#A6A6B8">Nocturne</Text>
|
||||
</Frame>
|
||||
<svg name="Like" x={440} y={16} viewBox="0 0 24 24" w={22} h={22} color="#FA6685">
|
||||
<path d="M12 21s-7-4.5-9.5-9C0.5 8 2 4 6 4c2.5 0 4 1.5 6 3 2-1.5 3.5-3 6-3 4 0 5.5 4 3.5 8-2.5 4.5-9.5 9-9.5 9z"/>
|
||||
</svg>
|
||||
</Frame>
|
||||
<Frame name="Track" w={496} h={56} rounded={12} cornerSmoothing={0.5} bg="#21212E"
|
||||
flex="row" items="center" gap={12} pl={8} pr={12}>
|
||||
<Frame name="Art" w={40} h={40} rounded={9} overflow="hidden"
|
||||
fill={linearGradient([['#33D9B3', 0], ['#3380F2', 1]])}
|
||||
effects={[dropShadow({ x: 0, y: 2, radius: 6, color: '"#0000004C"' })]} />
|
||||
<Frame name="Meta" flex="col" gap={2}>
|
||||
<Text name="Title" size={14} weight="medium" color="#FFFFFF">Slow Waves</Text>
|
||||
<Text name="Artist" size={12} color="#A6A6B8">Tidal Form</Text>
|
||||
</Frame>
|
||||
<svg name="Like" x={440} y={16} viewBox="0 0 24 24" w={22} h={22} color="#66667A">
|
||||
<path d="M12 21s-7-4.5-9.5-9C0.5 8 2 4 6 4c2.5 0 4 1.5 6 3 2-1.5 3.5-3 6-3 4 0 5.5 4 3.5 8-2.5 4.5-9.5 9-9.5 9z"/>
|
||||
</svg>
|
||||
</Frame>
|
||||
<Frame name="Track" w={496} h={56} rounded={12} cornerSmoothing={0.5} bg="#21212E"
|
||||
flex="row" items="center" gap={12} pl={8} pr={12}>
|
||||
<Frame name="Art" w={40} h={40} rounded={9} overflow="hidden"
|
||||
fill={linearGradient([['#FA804D', 0], ['#E64080', 1]])}
|
||||
effects={[dropShadow({ x: 0, y: 2, radius: 6, color: '"#0000004C"' })]} />
|
||||
<Frame name="Meta" flex="col" gap={2}>
|
||||
<Text name="Title" size={14} weight="medium" color="#FFFFFF">Ember Days</Text>
|
||||
<Text name="Artist" size={12} color="#A6A6B8">Solstice</Text>
|
||||
</Frame>
|
||||
<svg name="Like" x={440} y={16} viewBox="0 0 24 24" w={22} h={22} color="#66667A">
|
||||
<path d="M12 21s-7-4.5-9.5-9C0.5 8 2 4 6 4c2.5 0 4 1.5 6 3 2-1.5 3.5-3 6-3 4 0 5.5 4 3.5 8-2.5 4.5-9.5 9-9.5 9z"/>
|
||||
</svg>
|
||||
</Frame>
|
||||
</Frame>
|
||||
|
||||
{/* Player bar: glassmorphism (background blur + translucent fill). */}
|
||||
<Frame name="Player Bar" x={32} y={640} w={496} h={88} rounded={18} cornerSmoothing={0.6}
|
||||
fill=""#29293DD9""
|
||||
effects={[backgroundBlur(16), dropShadow({ x: 0, y: 8, radius: 24, color: '"#00000066"' })]}
|
||||
flex="col" gap={12} pt={14} pb={14} pl={20} pr={20}>
|
||||
<Frame name="Progress Track" w={456} h={4} rounded={2} bg="#4D4D66">
|
||||
<Rectangle name="Progress Fill" w={300} h={4} rounded={2}
|
||||
fill={linearGradient([['#8066FA', 0], ['#F26699', 1]])} />
|
||||
</Frame>
|
||||
<Frame name="Controls" w={456} h={40} flex="row" justify="center" items="center" gap={28}>
|
||||
<Polygon name="Back" w={18} h={18} rotation={270} fill="#CCCCD9" pointCount={3} />
|
||||
<Ellipse name="Play" w={40} h={40}
|
||||
fill={linearGradient([['#8066FA', 0], ['#F26699', 1]])}
|
||||
effects={[dropShadow({ x: 0, y: 4, radius: 12, color: '"#6633CC80"' })]} />
|
||||
<Polygon name="Forward" w={18} h={18} rotation={90} fill="#CCCCD9" pointCount={3} />
|
||||
</Frame>
|
||||
</Frame>
|
||||
</Section>
|
||||
`
|
||||
|
|
@ -1,314 +1,191 @@
|
|||
import type { Color } from '@open-pencil/scene-graph'
|
||||
|
||||
import { DEMO_COLORS, gradient, solid, thinStroke } from '@/app/demo/colors'
|
||||
import type { EditorStore } from '@/app/editor/session'
|
||||
|
||||
interface DemoComponents {
|
||||
btnPrimaryComp: string
|
||||
btnSecondaryComp: string
|
||||
badgeComp: string
|
||||
avatarComp: string
|
||||
navItemComp: string
|
||||
toggleComp: string
|
||||
}
|
||||
|
||||
type StatTone = 'success' | 'accent' | 'danger'
|
||||
|
||||
const STAT_TONE_COLORS: Record<StatTone, { foreground: Color; background: Color }> = {
|
||||
success: { foreground: DEMO_COLORS.success, background: DEMO_COLORS.successSoft },
|
||||
accent: { foreground: DEMO_COLORS.accent, background: DEMO_COLORS.accentSoft },
|
||||
danger: { foreground: DEMO_COLORS.danger, background: DEMO_COLORS.dangerSoft }
|
||||
}
|
||||
|
||||
/**
|
||||
* One cohesive product screen assembled from INSTANCEs of the component
|
||||
* library, laid out with real auto-layout, and themed via bound variables.
|
||||
* Resizing the root frame demonstrates live reflow; toggling a variable
|
||||
* re-themes the whole screen.
|
||||
*/
|
||||
export function createAppPreviewSection(store: EditorStore, comps: DemoComponents) {
|
||||
export function createAppPreviewSection(
|
||||
store: EditorStore,
|
||||
btnCompId: string,
|
||||
badgeCompId: string
|
||||
) {
|
||||
const { graph } = store
|
||||
|
||||
const sectionId = store.createShape('SECTION', 760, 60, 720, 760)
|
||||
graph.updateNode(sectionId, {
|
||||
name: 'App — Analytics',
|
||||
fills: [solid(DEMO_COLORS.bg)]
|
||||
const appSectionId = store.createShape('SECTION', 1040, 60, 560, 540)
|
||||
graph.updateNode(appSectionId, { name: 'App Preview' })
|
||||
|
||||
const frameId = store.createShape('FRAME', 20, 48, 520, 460, appSectionId)
|
||||
graph.updateNode(frameId, {
|
||||
name: 'Dashboard',
|
||||
fills: [solid(DEMO_COLORS.gray50)],
|
||||
strokes: thinStroke(DEMO_COLORS.gray200),
|
||||
clipsContent: true
|
||||
})
|
||||
|
||||
// ── Sidebar ───────────────────────────────────────────────────────
|
||||
const sidebar = store.createShape('FRAME', 24, 24, 180, 712, sectionId)
|
||||
graph.updateNode(sidebar, {
|
||||
const sidebarId = store.createShape('RECTANGLE', 0, 0, 56, 460, frameId)
|
||||
graph.updateNode(sidebarId, {
|
||||
name: 'Sidebar',
|
||||
cornerRadius: 16,
|
||||
fills: [solid(DEMO_COLORS.surface)],
|
||||
strokes: thinStroke(DEMO_COLORS.border),
|
||||
layoutMode: 'VERTICAL',
|
||||
primaryAxisSizing: 'FIXED',
|
||||
counterAxisSizing: 'FIXED',
|
||||
itemSpacing: 4,
|
||||
paddingTop: 16,
|
||||
paddingBottom: 16,
|
||||
paddingLeft: 12,
|
||||
paddingRight: 12
|
||||
fills: [solid(DEMO_COLORS.white)],
|
||||
strokes: thinStroke(DEMO_COLORS.gray200)
|
||||
})
|
||||
|
||||
const logo = store.createShape('FRAME', 0, 0, 156, 32, sidebar)
|
||||
graph.updateNode(logo, {
|
||||
name: 'Logo',
|
||||
fills: [],
|
||||
layoutMode: 'HORIZONTAL',
|
||||
counterAxisAlign: 'CENTER',
|
||||
itemSpacing: 8,
|
||||
paddingLeft: 8
|
||||
})
|
||||
const logoMark = store.createShape('ELLIPSE', 0, 0, 18, 18, logo)
|
||||
graph.updateNode(logoMark, { name: 'Mark', fills: [solid(DEMO_COLORS.accent)] })
|
||||
const logoText = store.createShape('TEXT', 0, 0, 90, 18, logo)
|
||||
graph.updateNode(logoText, {
|
||||
name: 'Wordmark',
|
||||
text: 'Pulse',
|
||||
fontSize: 15,
|
||||
fontWeight: 700,
|
||||
textAutoResize: 'WIDTH_AND_HEIGHT',
|
||||
fills: [solid(DEMO_COLORS.textPrimary)]
|
||||
})
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const dotId = store.createShape('ELLIPSE', 18, 20 + i * 40, 20, 20, frameId)
|
||||
graph.updateNode(dotId, {
|
||||
name: `Nav ${i + 1}`,
|
||||
fills: [solid(i === 0 ? DEMO_COLORS.blue : DEMO_COLORS.gray200)]
|
||||
})
|
||||
}
|
||||
|
||||
const navLabels = ['Overview', 'Analytics', 'Customers', 'Reports', 'Settings']
|
||||
navLabels.forEach((label, i) => {
|
||||
const inst = graph.createInstance(comps.navItemComp, sidebar)
|
||||
if (inst) {
|
||||
graph.updateNode(inst.id, { name: `Nav / ${label}` })
|
||||
const labelChild = inst.childIds
|
||||
.map((cid) => graph.getNode(cid))
|
||||
.find((n) => n?.type === 'TEXT')
|
||||
if (labelChild) {
|
||||
// Set the text and record an override so component sync doesn't revert it.
|
||||
graph.updateNode(labelChild.id, { text: label })
|
||||
graph.updateNode(inst.id, {
|
||||
overrides: { ...inst.overrides, [`${labelChild.id}:text`]: label }
|
||||
})
|
||||
}
|
||||
if (i === 0) {
|
||||
const dotChild = inst.childIds
|
||||
.map((cid) => graph.getNode(cid))
|
||||
.find((n) => n?.type === 'ELLIPSE')
|
||||
if (dotChild) graph.updateNode(dotChild.id, { fills: [solid(DEMO_COLORS.accent)] })
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// ── Main content (auto-layout column) ─────────────────────────────
|
||||
const main = store.createShape('FRAME', 224, 24, 472, 712, sectionId)
|
||||
graph.updateNode(main, {
|
||||
name: 'Content',
|
||||
fills: [],
|
||||
layoutMode: 'VERTICAL',
|
||||
primaryAxisSizing: 'FIXED',
|
||||
counterAxisSizing: 'FIXED',
|
||||
itemSpacing: 16
|
||||
})
|
||||
|
||||
// Header
|
||||
const header = store.createShape('FRAME', 0, 0, 472, 40, main)
|
||||
graph.updateNode(header, {
|
||||
const headerId = store.createShape('FRAME', 56, 0, 464, 52, frameId)
|
||||
graph.updateNode(headerId, {
|
||||
name: 'Header',
|
||||
fills: [],
|
||||
fills: [solid(DEMO_COLORS.white)],
|
||||
strokes: thinStroke(DEMO_COLORS.gray200),
|
||||
layoutMode: 'HORIZONTAL',
|
||||
primaryAxisSizing: 'FIXED',
|
||||
counterAxisSizing: 'FIXED',
|
||||
counterAxisAlign: 'CENTER',
|
||||
primaryAxisAlign: 'SPACE_BETWEEN',
|
||||
counterAxisAlign: 'CENTER'
|
||||
paddingLeft: 20,
|
||||
paddingRight: 20,
|
||||
itemSpacing: 8
|
||||
})
|
||||
const headerTitleWrap = store.createShape('FRAME', 0, 0, 200, 40, header)
|
||||
graph.updateNode(headerTitleWrap, {
|
||||
name: 'Title',
|
||||
fills: [],
|
||||
layoutMode: 'VERTICAL',
|
||||
itemSpacing: 2
|
||||
})
|
||||
const headerTitle = store.createShape('TEXT', 0, 0, 140, 22, headerTitleWrap)
|
||||
const headerTitle = store.createShape('TEXT', 0, 0, 120, 20, headerId)
|
||||
graph.updateNode(headerTitle, {
|
||||
name: 'Heading',
|
||||
text: 'Analytics overview',
|
||||
fontSize: 18,
|
||||
fontWeight: 700,
|
||||
textAutoResize: 'WIDTH_AND_HEIGHT',
|
||||
fills: [solid(DEMO_COLORS.textPrimary)]
|
||||
name: 'Page Title',
|
||||
text: 'Dashboard',
|
||||
fontSize: 16,
|
||||
fontWeight: 600,
|
||||
textAutoResize: 'WIDTH_AND_HEIGHT' as const,
|
||||
fills: [solid(DEMO_COLORS.black)]
|
||||
})
|
||||
const headerSub = store.createShape('TEXT', 0, 0, 200, 16, headerTitleWrap)
|
||||
graph.updateNode(headerSub, {
|
||||
name: 'Subheading',
|
||||
text: 'Last 30 days',
|
||||
fontSize: 12,
|
||||
fontWeight: 400,
|
||||
textAutoResize: 'WIDTH_AND_HEIGHT',
|
||||
fills: [solid(DEMO_COLORS.textSecondary)]
|
||||
})
|
||||
graph.createInstance(comps.avatarComp, header)
|
||||
graph.createInstance(comps.btnPrimaryComp, header)
|
||||
|
||||
// Stat row (auto-layout, fills width)
|
||||
const statRow = store.createShape('FRAME', 0, 0, 472, 96, main)
|
||||
graph.updateNode(statRow, {
|
||||
name: 'Stat Row',
|
||||
fills: [],
|
||||
layoutMode: 'HORIZONTAL',
|
||||
primaryAxisSizing: 'FIXED',
|
||||
counterAxisSizing: 'FIXED',
|
||||
itemSpacing: 12
|
||||
})
|
||||
const badgeInstance = graph.createInstance(badgeCompId, headerId)
|
||||
if (badgeInstance) graph.updateNode(badgeInstance.id, { x: 264, y: 15 })
|
||||
const buttonInstance = graph.createInstance(btnCompId, headerId)
|
||||
if (buttonInstance) graph.updateNode(buttonInstance.id, { x: 324, y: 6 })
|
||||
|
||||
const stats = [
|
||||
{ title: 'Revenue', value: '$12,480', trend: '+14%', tone: 'success' as const },
|
||||
{ title: 'Active users', value: '3,842', trend: '+8%', tone: 'accent' as const },
|
||||
{ title: 'Churn', value: '1.9%', trend: '-3%', tone: 'danger' as const }
|
||||
{ title: 'Revenue', value: '$12,480', badge: '+14%', color: DEMO_COLORS.green },
|
||||
{ title: 'Users', value: '3,842', badge: '+8%', color: DEMO_COLORS.blue },
|
||||
{ title: 'Orders', value: '1,249', badge: '-3%', color: DEMO_COLORS.red }
|
||||
]
|
||||
|
||||
const statBadges: StatBadge[] = []
|
||||
stats.forEach((s) => {
|
||||
const card = store.createShape('FRAME', 0, 0, 148, 96, statRow)
|
||||
graph.updateNode(card, {
|
||||
name: `Stat / ${s.title}`,
|
||||
cornerRadius: 12,
|
||||
fills: [solid(DEMO_COLORS.surface)],
|
||||
strokes: thinStroke(DEMO_COLORS.border),
|
||||
for (let i = 0; i < stats.length; i++) {
|
||||
const s = stats[i]
|
||||
const cx = 76 + i * 152
|
||||
const cId = store.createShape('FRAME', cx, 72, 140, 88, frameId)
|
||||
graph.updateNode(cId, {
|
||||
name: s.title,
|
||||
cornerRadius: 10,
|
||||
fills: [solid(DEMO_COLORS.white)],
|
||||
strokes: thinStroke(DEMO_COLORS.gray200),
|
||||
layoutMode: 'VERTICAL',
|
||||
primaryAxisSizing: 'FIXED',
|
||||
counterAxisSizing: 'FILL',
|
||||
itemSpacing: 6,
|
||||
counterAxisSizing: 'FIXED',
|
||||
itemSpacing: 4,
|
||||
paddingTop: 14,
|
||||
paddingBottom: 14,
|
||||
paddingLeft: 16,
|
||||
paddingRight: 16
|
||||
})
|
||||
const label = store.createShape('TEXT', 0, 0, 100, 14, card)
|
||||
graph.updateNode(label, {
|
||||
const labelId = store.createShape('TEXT', 0, 0, 108, 14, cId)
|
||||
graph.updateNode(labelId, {
|
||||
name: 'Label',
|
||||
text: s.title,
|
||||
fontSize: 12,
|
||||
fontSize: 11,
|
||||
fontWeight: 500,
|
||||
textAutoResize: 'HEIGHT',
|
||||
layoutAlignSelf: 'STRETCH',
|
||||
fills: [solid(DEMO_COLORS.textSecondary)]
|
||||
fills: [solid(DEMO_COLORS.gray500)]
|
||||
})
|
||||
const value = store.createShape('TEXT', 0, 0, 100, 26, card)
|
||||
graph.updateNode(value, {
|
||||
const valId = store.createShape('TEXT', 0, 0, 108, 24, cId)
|
||||
graph.updateNode(valId, {
|
||||
name: 'Value',
|
||||
text: s.value,
|
||||
fontSize: 22,
|
||||
fontWeight: 700,
|
||||
textAutoResize: 'HEIGHT',
|
||||
layoutAlignSelf: 'STRETCH',
|
||||
fills: [solid(DEMO_COLORS.textPrimary)]
|
||||
fills: [solid(DEMO_COLORS.black)]
|
||||
})
|
||||
const badge = graph.createInstance(comps.badgeComp, card)
|
||||
if (badge) {
|
||||
const tone = STAT_TONE_COLORS[s.tone]
|
||||
const bLabel = badge.childIds.map((cid) => graph.getNode(cid)).find((n) => n?.type === 'TEXT')
|
||||
if (bLabel) {
|
||||
const overrides: Record<string, unknown> = { ...badge.overrides }
|
||||
graph.updateNode(bLabel.id, { text: s.trend, fills: [solid(tone.foreground)] })
|
||||
overrides[`${bLabel.id}:text`] = s.trend
|
||||
overrides[`${bLabel.id}:fills`] = [solid(tone.foreground)]
|
||||
graph.updateNode(badge.id, { fills: [solid(tone.background)], overrides })
|
||||
statBadges.push({ id: badge.id, labelId: bLabel.id, tone: s.tone })
|
||||
}
|
||||
}
|
||||
})
|
||||
const bId = store.createShape('TEXT', 0, 0, 108, 14, cId)
|
||||
graph.updateNode(bId, {
|
||||
name: 'Badge',
|
||||
text: s.badge,
|
||||
fontSize: 11,
|
||||
fontWeight: 600,
|
||||
textAutoResize: 'HEIGHT',
|
||||
layoutAlignSelf: 'STRETCH',
|
||||
fills: [solid(s.color)]
|
||||
})
|
||||
}
|
||||
|
||||
// Chart card
|
||||
const chart = store.createShape('FRAME', 0, 0, 472, 260, main)
|
||||
graph.updateNode(chart, {
|
||||
const chartBg = store.createShape('FRAME', 76, 180, 424, 200, frameId)
|
||||
graph.updateNode(chartBg, {
|
||||
name: 'Chart',
|
||||
cornerRadius: 12,
|
||||
fills: [solid(DEMO_COLORS.surface)],
|
||||
strokes: thinStroke(DEMO_COLORS.border),
|
||||
layoutMode: 'VERTICAL',
|
||||
primaryAxisSizing: 'FIXED',
|
||||
counterAxisSizing: 'FIXED',
|
||||
paddingTop: 16,
|
||||
paddingBottom: 16,
|
||||
paddingLeft: 16,
|
||||
paddingRight: 16,
|
||||
itemSpacing: 12
|
||||
cornerRadius: 10,
|
||||
fills: [solid(DEMO_COLORS.white)],
|
||||
strokes: thinStroke(DEMO_COLORS.gray200)
|
||||
})
|
||||
const chartHeader = store.createShape('FRAME', 0, 0, 440, 20, chart)
|
||||
graph.updateNode(chartHeader, {
|
||||
name: 'Chart Header',
|
||||
fills: [],
|
||||
layoutMode: 'HORIZONTAL',
|
||||
primaryAxisAlign: 'SPACE_BETWEEN',
|
||||
counterAxisAlign: 'CENTER'
|
||||
})
|
||||
const chartTitle = store.createShape('TEXT', 0, 0, 140, 18, chartHeader)
|
||||
const chartTitle = store.createShape('TEXT', 16, 16, 120, 18, chartBg)
|
||||
graph.updateNode(chartTitle, {
|
||||
name: 'Title',
|
||||
name: 'Chart Title',
|
||||
text: 'Revenue over time',
|
||||
fontSize: 13,
|
||||
fontWeight: 600,
|
||||
textAutoResize: 'WIDTH_AND_HEIGHT',
|
||||
fills: [solid(DEMO_COLORS.textPrimary)]
|
||||
fills: [solid(DEMO_COLORS.black)]
|
||||
})
|
||||
graph.createInstance(comps.toggleComp, chartHeader)
|
||||
|
||||
const bars = store.createShape('FRAME', 0, 0, 440, 190, chart)
|
||||
graph.updateNode(bars, {
|
||||
name: 'Bars',
|
||||
fills: [],
|
||||
layoutMode: 'HORIZONTAL',
|
||||
primaryAxisSizing: 'FIXED',
|
||||
counterAxisSizing: 'FIXED',
|
||||
primaryAxisAlign: 'SPACE_BETWEEN',
|
||||
counterAxisAlign: 'MAX',
|
||||
paddingTop: 8
|
||||
})
|
||||
const barHeights = [70, 105, 88, 128, 96, 148, 112, 84, 132, 158, 120, 92]
|
||||
barHeights.forEach((h, i) => {
|
||||
const bar = store.createShape('RECTANGLE', 0, 0, 22, h, bars)
|
||||
graph.updateNode(bar, {
|
||||
const barHeights = [60, 90, 72, 110, 95, 130, 100, 80, 120, 140, 115, 88]
|
||||
const barW = 24
|
||||
const barGap = 10
|
||||
const barStartX = 16
|
||||
const barBaseY = 180
|
||||
|
||||
for (let i = 0; i < barHeights.length; i++) {
|
||||
const h = barHeights[i]
|
||||
const bx = barStartX + i * (barW + barGap)
|
||||
const by = barBaseY - h
|
||||
const barId = store.createShape('RECTANGLE', bx, by, barW, h, chartBg)
|
||||
graph.updateNode(barId, {
|
||||
name: `Bar ${i + 1}`,
|
||||
cornerRadius: 5,
|
||||
cornerRadius: 4,
|
||||
fills: [
|
||||
gradient([
|
||||
{ color: DEMO_COLORS.accent, position: 0 },
|
||||
{ color: { r: 0.55, g: 0.5, b: 0.98, a: 1 }, position: 1 }
|
||||
{ color: DEMO_COLORS.blue, position: 0 },
|
||||
{ color: DEMO_COLORS.indigo, position: 1 }
|
||||
])
|
||||
]
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// Feature callout
|
||||
const callout = store.createShape('FRAME', 0, 0, 472, 64, main)
|
||||
graph.updateNode(callout, {
|
||||
name: 'Callout',
|
||||
cornerRadius: 12,
|
||||
fills: [solid(DEMO_COLORS.accentSoft)],
|
||||
const tableId = store.createShape('FRAME', 76, 400, 424, 40, frameId)
|
||||
graph.updateNode(tableId, {
|
||||
name: 'Table Header',
|
||||
fills: [solid(DEMO_COLORS.white)],
|
||||
strokes: thinStroke(DEMO_COLORS.gray200),
|
||||
layoutMode: 'HORIZONTAL',
|
||||
primaryAxisSizing: 'FIXED',
|
||||
counterAxisSizing: 'FIXED',
|
||||
counterAxisSizing: 'HUG',
|
||||
primaryAxisAlign: 'SPACE_BETWEEN',
|
||||
counterAxisAlign: 'CENTER',
|
||||
itemSpacing: 12,
|
||||
paddingTop: 10,
|
||||
paddingBottom: 10,
|
||||
paddingLeft: 16,
|
||||
paddingRight: 16
|
||||
})
|
||||
const calloutDot = store.createShape('ELLIPSE', 0, 0, 24, 24, callout)
|
||||
graph.updateNode(calloutDot, { name: 'Icon', fills: [solid(DEMO_COLORS.accent)] })
|
||||
const calloutText = store.createShape('TEXT', 0, 0, 340, 32, callout)
|
||||
graph.updateNode(calloutText, {
|
||||
name: 'Text',
|
||||
text: 'This screen is built from components, auto-layout, and variables. Edit a variable to re-theme it.',
|
||||
fontSize: 12,
|
||||
fontWeight: 500,
|
||||
textAutoResize: 'HEIGHT',
|
||||
layoutAlignSelf: 'STRETCH',
|
||||
fills: [solid(DEMO_COLORS.accentText)]
|
||||
paddingRight: 16,
|
||||
itemSpacing: 16
|
||||
})
|
||||
const cols = ['Name', 'Status', 'Amount', 'Date']
|
||||
for (const col of cols) {
|
||||
const colId = store.createShape('TEXT', 0, 0, 80, 16, tableId)
|
||||
graph.updateNode(colId, {
|
||||
name: col,
|
||||
text: col,
|
||||
fontSize: 12,
|
||||
fontWeight: 600,
|
||||
textAutoResize: 'WIDTH_AND_HEIGHT',
|
||||
fills: [solid(DEMO_COLORS.gray500)]
|
||||
})
|
||||
}
|
||||
|
||||
return { sectionId, sidebar, main, headerTitle, chartTitle, statBadges }
|
||||
}
|
||||
|
||||
interface StatBadge {
|
||||
id: string
|
||||
labelId: string
|
||||
tone: StatTone
|
||||
return { sidebarId, headerId, frameId, headerTitle, chartTitle }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,191 +1,253 @@
|
|||
import { DEMO_COLORS, solid } from '@/app/demo/colors'
|
||||
import { DEMO_COLORS, gradient, solid, thinStroke } from '@/app/demo/colors'
|
||||
import { makeComponent } from '@/app/demo/helpers'
|
||||
import type { EditorStore } from '@/app/editor/session'
|
||||
|
||||
/**
|
||||
* A small but real component library: Button (primary/secondary), Badge,
|
||||
* Avatar, Nav item, and Toggle — all as actual COMPONENTs. The app screen is
|
||||
* then assembled from INSTANCEs so the demo exercises the component → instance
|
||||
* → override workflow.
|
||||
*/
|
||||
export function createComponentsSection(store: EditorStore) {
|
||||
const { graph } = store
|
||||
|
||||
const sectionId = store.createShape('SECTION', 60, 60, 660, 300)
|
||||
graph.updateNode(sectionId, {
|
||||
name: 'Component Library',
|
||||
fills: [solid(DEMO_COLORS.surface)]
|
||||
const compSectionId = store.createShape('SECTION', 60, 60, 920, 540)
|
||||
graph.updateNode(compSectionId, { name: 'Components' })
|
||||
|
||||
const btnId = store.createShape('FRAME', 32, 76, 120, 40, compSectionId)
|
||||
graph.updateNode(btnId, {
|
||||
name: 'Button/Primary',
|
||||
fills: [],
|
||||
strokes: [],
|
||||
clipsContent: false
|
||||
})
|
||||
|
||||
const title = store.createShape('TEXT', 24, 20, 300, 20, sectionId)
|
||||
graph.updateNode(title, {
|
||||
name: 'Title',
|
||||
text: 'Components — the app below is built from these',
|
||||
fontSize: 12,
|
||||
fontWeight: 600,
|
||||
fills: [solid(DEMO_COLORS.textTertiary)],
|
||||
textAutoResize: 'WIDTH_AND_HEIGHT'
|
||||
})
|
||||
|
||||
function caption(text: string, x: number, y: number) {
|
||||
const c = store.createShape('TEXT', x, y, 140, 14, sectionId)
|
||||
graph.updateNode(c, {
|
||||
name: 'Caption',
|
||||
text,
|
||||
fontSize: 10,
|
||||
fontWeight: 500,
|
||||
textAutoResize: 'WIDTH_AND_HEIGHT',
|
||||
fills: [solid(DEMO_COLORS.textTertiary)]
|
||||
})
|
||||
}
|
||||
|
||||
caption('Button / Primary', 24, 52)
|
||||
caption('Button / Secondary', 156, 52)
|
||||
caption('Badge', 296, 52)
|
||||
caption('Avatar', 388, 52)
|
||||
caption('Nav item', 24, 148)
|
||||
caption('Toggle', 196, 148)
|
||||
|
||||
// ── Button / Primary ──────────────────────────────────────────────
|
||||
const btnPrimary = store.createShape('FRAME', 24, 72, 116, 36, sectionId)
|
||||
graph.updateNode(btnPrimary, {
|
||||
name: 'Button / Primary',
|
||||
const btnSurfaceId = store.createShape('FRAME', 0, 0, 120, 40, btnId)
|
||||
graph.updateNode(btnSurfaceId, {
|
||||
name: 'Surface',
|
||||
cornerRadius: 8,
|
||||
fills: [solid(DEMO_COLORS.accent)],
|
||||
fills: [solid(DEMO_COLORS.blue)],
|
||||
layoutMode: 'HORIZONTAL',
|
||||
primaryAxisSizing: 'HUG',
|
||||
counterAxisSizing: 'HUG',
|
||||
primaryAxisAlign: 'CENTER',
|
||||
counterAxisAlign: 'CENTER',
|
||||
paddingTop: 8,
|
||||
paddingBottom: 8,
|
||||
paddingLeft: 16,
|
||||
paddingRight: 16
|
||||
paddingTop: 10,
|
||||
paddingBottom: 10,
|
||||
paddingLeft: 20,
|
||||
paddingRight: 20
|
||||
})
|
||||
const btnPrimaryLabel = store.createShape('TEXT', 0, 0, 84, 20, btnPrimary)
|
||||
graph.updateNode(btnPrimaryLabel, {
|
||||
const btnTextId = store.createShape('TEXT', 0, 0, 80, 20, btnSurfaceId)
|
||||
graph.updateNode(btnTextId, {
|
||||
name: 'Label',
|
||||
text: 'Get started',
|
||||
fontSize: 13,
|
||||
text: 'Get Started',
|
||||
fontSize: 14,
|
||||
fontWeight: 600,
|
||||
textAutoResize: 'WIDTH_AND_HEIGHT',
|
||||
fills: [solid(DEMO_COLORS.white)]
|
||||
})
|
||||
const btnPrimaryComp = makeComponent(store, [btnPrimary])
|
||||
const btnCompId = makeComponent(store, [btnId])
|
||||
|
||||
// ── Button / Secondary ────────────────────────────────────────────
|
||||
const btnSecondary = store.createShape('FRAME', 156, 72, 104, 36, sectionId)
|
||||
graph.updateNode(btnSecondary, {
|
||||
name: 'Button / Secondary',
|
||||
const btn2Id = store.createShape('FRAME', 216, 76, 100, 40, compSectionId)
|
||||
graph.updateNode(btn2Id, {
|
||||
name: 'Button/Secondary',
|
||||
fills: [],
|
||||
strokes: [],
|
||||
clipsContent: false
|
||||
})
|
||||
const btn2SurfaceId = store.createShape('FRAME', 0, 0, 100, 40, btn2Id)
|
||||
graph.updateNode(btn2SurfaceId, {
|
||||
name: 'Surface',
|
||||
cornerRadius: 8,
|
||||
fills: [solid(DEMO_COLORS.surfaceSunken)],
|
||||
fills: [solid(DEMO_COLORS.white)],
|
||||
strokes: thinStroke(DEMO_COLORS.gray200),
|
||||
layoutMode: 'HORIZONTAL',
|
||||
primaryAxisSizing: 'HUG',
|
||||
counterAxisSizing: 'HUG',
|
||||
primaryAxisAlign: 'CENTER',
|
||||
counterAxisAlign: 'CENTER',
|
||||
paddingTop: 8,
|
||||
paddingBottom: 8,
|
||||
paddingLeft: 16,
|
||||
paddingRight: 16
|
||||
paddingTop: 10,
|
||||
paddingBottom: 10,
|
||||
paddingLeft: 20,
|
||||
paddingRight: 20
|
||||
})
|
||||
const btnSecondaryLabel = store.createShape('TEXT', 0, 0, 72, 20, btnSecondary)
|
||||
graph.updateNode(btnSecondaryLabel, {
|
||||
const btn2TextId = store.createShape('TEXT', 0, 0, 60, 20, btn2SurfaceId)
|
||||
graph.updateNode(btn2TextId, {
|
||||
name: 'Label',
|
||||
text: 'Cancel',
|
||||
fontSize: 13,
|
||||
fontWeight: 600,
|
||||
fontSize: 14,
|
||||
fontWeight: 500,
|
||||
textAutoResize: 'WIDTH_AND_HEIGHT',
|
||||
fills: [solid(DEMO_COLORS.textPrimary)]
|
||||
fills: [solid(DEMO_COLORS.black)]
|
||||
})
|
||||
const btnSecondaryComp = makeComponent(store, [btnSecondary])
|
||||
const btn2CompId = makeComponent(store, [btn2Id])
|
||||
|
||||
// ── Badge / Success ───────────────────────────────────────────────
|
||||
const badge = store.createShape('FRAME', 296, 72, 72, 24, sectionId)
|
||||
graph.updateNode(badge, {
|
||||
name: 'Badge / Success',
|
||||
cornerRadius: 12,
|
||||
fills: [solid(DEMO_COLORS.successSoft)],
|
||||
store.select([btnCompId, btn2CompId])
|
||||
store.createComponentSetFromComponents()
|
||||
const buttonSetId = [...store.state.selectedIds][0]
|
||||
graph.updateNode(buttonSetId, { x: 32, y: 44, width: 400, height: 136, fills: [] })
|
||||
graph.updateNode(btnCompId, { x: 40, y: 64 })
|
||||
graph.updateNode(btn2CompId, { x: 224, y: 64 })
|
||||
|
||||
const chipId = store.createShape('FRAME', 500, 72, 80, 28, compSectionId)
|
||||
graph.updateNode(chipId, {
|
||||
name: 'Tag',
|
||||
cornerRadius: 14,
|
||||
fills: [solid({ r: 0.93, g: 0.94, b: 1, a: 1 })],
|
||||
layoutMode: 'HORIZONTAL',
|
||||
primaryAxisSizing: 'HUG',
|
||||
counterAxisSizing: 'HUG',
|
||||
primaryAxisAlign: 'CENTER',
|
||||
counterAxisAlign: 'CENTER',
|
||||
paddingTop: 2,
|
||||
paddingBottom: 2,
|
||||
paddingTop: 4,
|
||||
paddingBottom: 4,
|
||||
paddingLeft: 12,
|
||||
paddingRight: 12
|
||||
})
|
||||
const chipTextId = store.createShape('TEXT', 0, 0, 56, 16, chipId)
|
||||
graph.updateNode(chipTextId, {
|
||||
name: 'Label',
|
||||
text: 'Design',
|
||||
fontSize: 12,
|
||||
fontWeight: 500,
|
||||
textAutoResize: 'WIDTH_AND_HEIGHT',
|
||||
fills: [solid(DEMO_COLORS.indigo)]
|
||||
})
|
||||
makeComponent(store, [chipId])
|
||||
|
||||
const avatarId = store.createShape('ELLIPSE', 640, 68, 40, 40, compSectionId)
|
||||
graph.updateNode(avatarId, {
|
||||
name: 'Avatar',
|
||||
fills: [
|
||||
gradient([
|
||||
{ color: DEMO_COLORS.purple, position: 0 },
|
||||
{ color: DEMO_COLORS.blue, position: 1 }
|
||||
])
|
||||
]
|
||||
})
|
||||
const avatarCompId = makeComponent(store, [avatarId])
|
||||
graph.updateNode(avatarCompId, { name: 'Avatar' })
|
||||
|
||||
const cardId = store.createShape('FRAME', 32, 216, 280, 160, compSectionId)
|
||||
graph.updateNode(cardId, {
|
||||
name: 'Card',
|
||||
cornerRadius: 12,
|
||||
fills: [solid(DEMO_COLORS.white)],
|
||||
strokes: thinStroke(DEMO_COLORS.gray200),
|
||||
layoutMode: 'VERTICAL',
|
||||
primaryAxisSizing: 'FIXED',
|
||||
counterAxisSizing: 'FIXED',
|
||||
itemSpacing: 8,
|
||||
paddingTop: 20,
|
||||
paddingBottom: 20,
|
||||
paddingLeft: 20,
|
||||
paddingRight: 20
|
||||
})
|
||||
const cardTitleId = store.createShape('TEXT', 0, 0, 240, 22, cardId)
|
||||
graph.updateNode(cardTitleId, {
|
||||
name: 'Title',
|
||||
text: 'Analytics Overview',
|
||||
fontSize: 16,
|
||||
fontWeight: 600,
|
||||
textAutoResize: 'HEIGHT',
|
||||
layoutAlignSelf: 'STRETCH',
|
||||
fills: [solid(DEMO_COLORS.black)]
|
||||
})
|
||||
const cardDescId = store.createShape('TEXT', 0, 0, 240, 36, cardId)
|
||||
graph.updateNode(cardDescId, {
|
||||
name: 'Description',
|
||||
text: 'Track your key metrics and performance indicators in real time.',
|
||||
fontSize: 13,
|
||||
fontWeight: 400,
|
||||
textAutoResize: 'HEIGHT',
|
||||
layoutAlignSelf: 'STRETCH',
|
||||
fills: [solid(DEMO_COLORS.gray500)]
|
||||
})
|
||||
const cardBarBg = store.createShape('RECTANGLE', 0, 0, 240, 8, cardId)
|
||||
graph.updateNode(cardBarBg, {
|
||||
name: 'Progress BG',
|
||||
cornerRadius: 4,
|
||||
fills: [solid(DEMO_COLORS.gray100)]
|
||||
})
|
||||
const cardBar = store.createShape('RECTANGLE', 0, 0, 168, 8, cardId)
|
||||
graph.updateNode(cardBar, {
|
||||
name: 'Progress',
|
||||
cornerRadius: 4,
|
||||
fills: [
|
||||
gradient([
|
||||
{ color: DEMO_COLORS.blue, position: 0 },
|
||||
{ color: DEMO_COLORS.teal, position: 1 }
|
||||
])
|
||||
]
|
||||
})
|
||||
makeComponent(store, [cardId])
|
||||
|
||||
const inputId = store.createShape('FRAME', 344, 216, 240, 40, compSectionId)
|
||||
graph.updateNode(inputId, {
|
||||
name: 'Input',
|
||||
cornerRadius: 8,
|
||||
fills: [solid(DEMO_COLORS.white)],
|
||||
strokes: thinStroke(DEMO_COLORS.gray200),
|
||||
layoutMode: 'HORIZONTAL',
|
||||
primaryAxisSizing: 'FIXED',
|
||||
counterAxisSizing: 'HUG',
|
||||
primaryAxisAlign: 'MIN',
|
||||
counterAxisAlign: 'CENTER',
|
||||
paddingTop: 10,
|
||||
paddingBottom: 10,
|
||||
paddingLeft: 12,
|
||||
paddingRight: 12
|
||||
})
|
||||
const inputPlaceholder = store.createShape('TEXT', 0, 0, 200, 18, inputId)
|
||||
graph.updateNode(inputPlaceholder, {
|
||||
name: 'Placeholder',
|
||||
text: 'Search...',
|
||||
fontSize: 14,
|
||||
fontWeight: 400,
|
||||
textAutoResize: 'HEIGHT',
|
||||
layoutAlignSelf: 'STRETCH',
|
||||
fills: [solid(DEMO_COLORS.gray500)]
|
||||
})
|
||||
makeComponent(store, [inputId])
|
||||
|
||||
const badgeId = store.createShape('FRAME', 344, 284, 48, 24, compSectionId)
|
||||
graph.updateNode(badgeId, {
|
||||
name: 'Badge',
|
||||
cornerRadius: 12,
|
||||
fills: [solid({ r: 0.93, g: 1, b: 0.95, a: 1 })],
|
||||
layoutMode: 'HORIZONTAL',
|
||||
primaryAxisSizing: 'HUG',
|
||||
counterAxisSizing: 'HUG',
|
||||
primaryAxisAlign: 'CENTER',
|
||||
counterAxisAlign: 'CENTER',
|
||||
itemSpacing: 4,
|
||||
paddingTop: 4,
|
||||
paddingBottom: 4,
|
||||
paddingLeft: 8,
|
||||
paddingRight: 8
|
||||
})
|
||||
const badgeLabel = store.createShape('TEXT', 0, 0, 56, 16, badge)
|
||||
graph.updateNode(badgeLabel, {
|
||||
const badgeDot = store.createShape('ELLIPSE', 0, 0, 6, 6, badgeId)
|
||||
graph.updateNode(badgeDot, {
|
||||
name: 'Dot',
|
||||
fills: [solid(DEMO_COLORS.green)]
|
||||
})
|
||||
const badgeText = store.createShape('TEXT', 0, 0, 28, 14, badgeId)
|
||||
graph.updateNode(badgeText, {
|
||||
name: 'Label',
|
||||
text: '+14%',
|
||||
text: 'Live',
|
||||
fontSize: 11,
|
||||
fontWeight: 600,
|
||||
textAutoResize: 'WIDTH_AND_HEIGHT',
|
||||
fills: [solid(DEMO_COLORS.success)]
|
||||
fills: [solid(DEMO_COLORS.green)]
|
||||
})
|
||||
const badgeComp = makeComponent(store, [badge])
|
||||
const badgeCompId = makeComponent(store, [badgeId])
|
||||
|
||||
// ── Avatar ────────────────────────────────────────────────────────
|
||||
const avatar = store.createShape('ELLIPSE', 388, 72, 32, 32, sectionId)
|
||||
graph.updateNode(avatar, {
|
||||
name: 'Avatar',
|
||||
fills: [solid(DEMO_COLORS.accentSoft)]
|
||||
})
|
||||
const avatarComp = makeComponent(store, [avatar])
|
||||
|
||||
// ── Nav item ──────────────────────────────────────────────────────
|
||||
const navItem = store.createShape('FRAME', 24, 168, 148, 32, sectionId)
|
||||
graph.updateNode(navItem, {
|
||||
name: 'Nav Item',
|
||||
cornerRadius: 6,
|
||||
fills: [solid(DEMO_COLORS.surfaceSunken)],
|
||||
layoutMode: 'HORIZONTAL',
|
||||
primaryAxisSizing: 'FIXED',
|
||||
counterAxisSizing: 'FIXED',
|
||||
counterAxisAlign: 'CENTER',
|
||||
itemSpacing: 10,
|
||||
paddingLeft: 10,
|
||||
paddingRight: 10
|
||||
})
|
||||
const navDot = store.createShape('ELLIPSE', 0, 0, 14, 14, navItem)
|
||||
graph.updateNode(navDot, {
|
||||
name: 'Icon',
|
||||
fills: [solid(DEMO_COLORS.accent)]
|
||||
})
|
||||
const navLabel = store.createShape('TEXT', 0, 0, 80, 16, navItem)
|
||||
graph.updateNode(navLabel, {
|
||||
name: 'Label',
|
||||
text: 'Overview',
|
||||
fontSize: 13,
|
||||
fontWeight: 500,
|
||||
textAutoResize: 'WIDTH_AND_HEIGHT',
|
||||
fills: [solid(DEMO_COLORS.textPrimary)]
|
||||
})
|
||||
const navItemComp = makeComponent(store, [navItem])
|
||||
|
||||
// ── Toggle ────────────────────────────────────────────────────────
|
||||
const toggle = store.createShape('FRAME', 196, 172, 36, 20, sectionId)
|
||||
graph.updateNode(toggle, {
|
||||
name: 'Toggle',
|
||||
cornerRadius: 10,
|
||||
fills: [solid(DEMO_COLORS.accent)]
|
||||
})
|
||||
const toggleKnob = store.createShape('ELLIPSE', 18, 2, 16, 16, toggle)
|
||||
graph.updateNode(toggleKnob, {
|
||||
name: 'Knob',
|
||||
fills: [solid(DEMO_COLORS.white)]
|
||||
})
|
||||
const toggleComp = makeComponent(store, [toggle])
|
||||
|
||||
return {
|
||||
sectionId,
|
||||
btnPrimaryComp,
|
||||
btnSecondaryComp,
|
||||
badgeComp,
|
||||
avatarComp,
|
||||
navItemComp,
|
||||
toggleComp
|
||||
const swatches = [
|
||||
{ name: 'Blue', color: DEMO_COLORS.blue, x: 32 },
|
||||
{ name: 'Indigo', color: DEMO_COLORS.indigo, x: 88 },
|
||||
{ name: 'Purple', color: DEMO_COLORS.purple, x: 144 },
|
||||
{ name: 'Green', color: DEMO_COLORS.green, x: 200 },
|
||||
{ name: 'Teal', color: DEMO_COLORS.teal, x: 256 },
|
||||
{ name: 'Orange', color: DEMO_COLORS.orange, x: 312 },
|
||||
{ name: 'Red', color: DEMO_COLORS.red, x: 368 }
|
||||
]
|
||||
for (const swatch of swatches) {
|
||||
const id = store.createShape('ELLIPSE', swatch.x, 460, 44, 44, compSectionId)
|
||||
graph.updateNode(id, { name: swatch.name, fills: [solid(swatch.color)] })
|
||||
}
|
||||
|
||||
return { btnCompId, badgeCompId }
|
||||
}
|
||||
|
|
|
|||
429
src/app/demo/sections/effects.ts
Normal file
429
src/app/demo/sections/effects.ts
Normal file
|
|
@ -0,0 +1,429 @@
|
|||
import { DEMO_COLORS, gradient, solid, thinStroke } from '@/app/demo/colors'
|
||||
import { blurEffect, dropShadow, innerShadow } from '@/app/demo/effects'
|
||||
import type { EditorStore } from '@/app/editor/session'
|
||||
|
||||
export function createEffectsSection(store: EditorStore) {
|
||||
const { graph } = store
|
||||
|
||||
const effectsSectionId = store.createShape('SECTION', 60, 840, 920, 780)
|
||||
graph.updateNode(effectsSectionId, {
|
||||
name: 'Effects',
|
||||
fills: [solid(DEMO_COLORS.white)]
|
||||
})
|
||||
|
||||
const shadowLabel = store.createShape('TEXT', 32, 48, 200, 20, effectsSectionId)
|
||||
graph.updateNode(shadowLabel, {
|
||||
name: 'Label',
|
||||
text: 'Drop Shadow',
|
||||
fontSize: 13,
|
||||
fontWeight: 600,
|
||||
fills: [solid(DEMO_COLORS.gray500)]
|
||||
})
|
||||
|
||||
const shadowCard1 = store.createShape('FRAME', 32, 76, 160, 100, effectsSectionId)
|
||||
graph.updateNode(shadowCard1, {
|
||||
name: 'Subtle Shadow',
|
||||
cornerRadius: 12,
|
||||
fills: [solid(DEMO_COLORS.white)],
|
||||
effects: [dropShadow(0, 2, 8, 0, { r: 0, g: 0, b: 0, a: 0.08 })]
|
||||
})
|
||||
const s1Text = store.createShape('TEXT', 16, 40, 128, 20, shadowCard1)
|
||||
graph.updateNode(s1Text, {
|
||||
name: 'Label',
|
||||
text: 'Subtle',
|
||||
fontSize: 13,
|
||||
fontWeight: 500,
|
||||
fills: [solid(DEMO_COLORS.gray500)]
|
||||
})
|
||||
|
||||
const shadowCard2 = store.createShape('FRAME', 220, 76, 160, 100, effectsSectionId)
|
||||
graph.updateNode(shadowCard2, {
|
||||
name: 'Medium Shadow',
|
||||
cornerRadius: 12,
|
||||
fills: [solid(DEMO_COLORS.white)],
|
||||
effects: [dropShadow(0, 4, 16, 0, { r: 0, g: 0, b: 0, a: 0.15 })]
|
||||
})
|
||||
const s2Text = store.createShape('TEXT', 16, 40, 128, 20, shadowCard2)
|
||||
graph.updateNode(s2Text, {
|
||||
name: 'Label',
|
||||
text: 'Medium',
|
||||
fontSize: 13,
|
||||
fontWeight: 500,
|
||||
fills: [solid(DEMO_COLORS.gray500)]
|
||||
})
|
||||
|
||||
const shadowCard3 = store.createShape('FRAME', 408, 76, 160, 100, effectsSectionId)
|
||||
graph.updateNode(shadowCard3, {
|
||||
name: 'Heavy Shadow',
|
||||
cornerRadius: 12,
|
||||
fills: [solid(DEMO_COLORS.white)],
|
||||
effects: [dropShadow(0, 8, 24, 0, { r: 0, g: 0, b: 0, a: 0.2 })]
|
||||
})
|
||||
const s3Text = store.createShape('TEXT', 16, 40, 128, 20, shadowCard3)
|
||||
graph.updateNode(s3Text, {
|
||||
name: 'Label',
|
||||
text: 'Heavy',
|
||||
fontSize: 13,
|
||||
fontWeight: 500,
|
||||
fills: [solid(DEMO_COLORS.gray500)]
|
||||
})
|
||||
|
||||
const shadowCard4 = store.createShape('FRAME', 596, 76, 160, 100, effectsSectionId)
|
||||
graph.updateNode(shadowCard4, {
|
||||
name: 'Spread Shadow',
|
||||
cornerRadius: 12,
|
||||
fills: [solid(DEMO_COLORS.white)],
|
||||
effects: [dropShadow(0, 4, 12, 8, { r: 0.23, g: 0.51, b: 0.96, a: 0.3 })]
|
||||
})
|
||||
const s4Text = store.createShape('TEXT', 16, 40, 128, 20, shadowCard4)
|
||||
graph.updateNode(s4Text, {
|
||||
name: 'Label',
|
||||
text: 'With Spread',
|
||||
fontSize: 13,
|
||||
fontWeight: 500,
|
||||
fills: [solid(DEMO_COLORS.blue)]
|
||||
})
|
||||
|
||||
const innerLabel = store.createShape('TEXT', 32, 208, 200, 20, effectsSectionId)
|
||||
graph.updateNode(innerLabel, {
|
||||
name: 'Label',
|
||||
text: 'Inner Shadow',
|
||||
fontSize: 13,
|
||||
fontWeight: 600,
|
||||
fills: [solid(DEMO_COLORS.gray500)]
|
||||
})
|
||||
|
||||
const innerCard1 = store.createShape('FRAME', 32, 236, 160, 100, effectsSectionId)
|
||||
graph.updateNode(innerCard1, {
|
||||
name: 'Inset Light',
|
||||
cornerRadius: 12,
|
||||
fills: [solid(DEMO_COLORS.gray100)],
|
||||
effects: [innerShadow(0, 2, 6, 0, { r: 0, g: 0, b: 0, a: 0.12 })]
|
||||
})
|
||||
const i1Text = store.createShape('TEXT', 16, 40, 128, 20, innerCard1)
|
||||
graph.updateNode(i1Text, {
|
||||
name: 'Label',
|
||||
text: 'Inset',
|
||||
fontSize: 13,
|
||||
fontWeight: 500,
|
||||
fills: [solid(DEMO_COLORS.gray500)]
|
||||
})
|
||||
|
||||
const innerCard2 = store.createShape('FRAME', 220, 236, 160, 100, effectsSectionId)
|
||||
graph.updateNode(innerCard2, {
|
||||
name: 'Inset with Spread',
|
||||
cornerRadius: 12,
|
||||
fills: [solid(DEMO_COLORS.gray100)],
|
||||
effects: [innerShadow(0, 2, 8, 4, { r: 0, g: 0, b: 0, a: 0.15 })]
|
||||
})
|
||||
const i2Text = store.createShape('TEXT', 16, 40, 128, 20, innerCard2)
|
||||
graph.updateNode(i2Text, {
|
||||
name: 'Label',
|
||||
text: 'Inset + Spread',
|
||||
fontSize: 13,
|
||||
fontWeight: 500,
|
||||
fills: [solid(DEMO_COLORS.gray500)]
|
||||
})
|
||||
|
||||
const innerEllipse = store.createShape('ELLIPSE', 408, 236, 100, 100, effectsSectionId)
|
||||
graph.updateNode(innerEllipse, {
|
||||
name: 'Inset Circle',
|
||||
fills: [
|
||||
gradient([
|
||||
{ color: { r: 0.93, g: 0.94, b: 1, a: 1 }, position: 0 },
|
||||
{ color: { r: 0.85, g: 0.86, b: 0.95, a: 1 }, position: 1 }
|
||||
])
|
||||
],
|
||||
effects: [innerShadow(0, 4, 12, 0, { r: 0.38, g: 0.35, b: 0.95, a: 0.3 })]
|
||||
})
|
||||
|
||||
const comboCard = store.createShape('FRAME', 536, 236, 160, 100, effectsSectionId)
|
||||
graph.updateNode(comboCard, {
|
||||
name: 'Combined',
|
||||
cornerRadius: 12,
|
||||
fills: [solid(DEMO_COLORS.white)],
|
||||
effects: [
|
||||
dropShadow(0, 4, 16, 0, { r: 0, g: 0, b: 0, a: 0.12 }),
|
||||
innerShadow(0, 1, 2, 0, { r: 0, g: 0, b: 0, a: 0.06 })
|
||||
]
|
||||
})
|
||||
const comboText = store.createShape('TEXT', 16, 40, 128, 20, comboCard)
|
||||
graph.updateNode(comboText, {
|
||||
name: 'Label',
|
||||
text: 'Drop + Inner',
|
||||
fontSize: 13,
|
||||
fontWeight: 500,
|
||||
fills: [solid(DEMO_COLORS.gray500)]
|
||||
})
|
||||
|
||||
const textFxLabel = store.createShape('TEXT', 32, 368, 200, 20, effectsSectionId)
|
||||
graph.updateNode(textFxLabel, {
|
||||
name: 'Label',
|
||||
text: 'Text Shadow & Blur',
|
||||
fontSize: 13,
|
||||
fontWeight: 600,
|
||||
fills: [solid(DEMO_COLORS.gray500)]
|
||||
})
|
||||
|
||||
const textShadow = store.createShape('TEXT', 32, 400, 200, 36, effectsSectionId)
|
||||
graph.updateNode(textShadow, {
|
||||
name: 'Text Shadow',
|
||||
text: 'Glyph Shadow',
|
||||
fontSize: 28,
|
||||
fontWeight: 700,
|
||||
fills: [solid(DEMO_COLORS.black)],
|
||||
effects: [dropShadow(2, 2, 4, 0, { r: 0.23, g: 0.51, b: 0.96, a: 0.5 })]
|
||||
})
|
||||
|
||||
const textInner = store.createShape('TEXT', 260, 400, 200, 36, effectsSectionId)
|
||||
graph.updateNode(textInner, {
|
||||
name: 'Text Inner',
|
||||
text: 'Inner Glow',
|
||||
fontSize: 28,
|
||||
fontWeight: 700,
|
||||
fills: [solid(DEMO_COLORS.indigo)],
|
||||
effects: [innerShadow(0, -1, 3, 0, { r: 1, g: 1, b: 1, a: 0.6 })]
|
||||
})
|
||||
|
||||
const blurRect = store.createShape('RECTANGLE', 500, 392, 120, 50, effectsSectionId)
|
||||
graph.updateNode(blurRect, {
|
||||
name: 'Layer Blur',
|
||||
cornerRadius: 10,
|
||||
fills: [
|
||||
gradient([
|
||||
{ color: DEMO_COLORS.purple, position: 0 },
|
||||
{ color: DEMO_COLORS.blue, position: 1 }
|
||||
])
|
||||
],
|
||||
effects: [blurEffect('LAYER_BLUR', 4)]
|
||||
})
|
||||
|
||||
const glassBg = store.createShape('FRAME', 652, 368, 200, 80, effectsSectionId)
|
||||
graph.updateNode(glassBg, {
|
||||
name: 'Glass Card',
|
||||
cornerRadius: 16,
|
||||
fills: [solid({ r: 1, g: 1, b: 1, a: 0.3 })],
|
||||
strokes: thinStroke({ r: 1, g: 1, b: 1, a: 0.4 }),
|
||||
effects: [blurEffect('BACKGROUND_BLUR', 16)]
|
||||
})
|
||||
const glassText = store.createShape('TEXT', 20, 30, 160, 20, glassBg)
|
||||
graph.updateNode(glassText, {
|
||||
name: 'Label',
|
||||
text: 'Glassmorphism',
|
||||
fontSize: 14,
|
||||
fontWeight: 600,
|
||||
fills: [solid(DEMO_COLORS.black)]
|
||||
})
|
||||
|
||||
const blendMaskLabel = store.createShape('TEXT', 32, 488, 240, 20, effectsSectionId)
|
||||
graph.updateNode(blendMaskLabel, {
|
||||
name: 'Label',
|
||||
text: 'Blend modes & masks',
|
||||
fontSize: 13,
|
||||
fontWeight: 600,
|
||||
fills: [solid(DEMO_COLORS.gray500)]
|
||||
})
|
||||
|
||||
const blendCard = store.createShape('FRAME', 32, 520, 256, 92, effectsSectionId)
|
||||
graph.updateNode(blendCard, {
|
||||
name: 'Blend Mode Stack',
|
||||
cornerRadius: 16,
|
||||
fills: [solid(DEMO_COLORS.gray50)],
|
||||
strokes: thinStroke(DEMO_COLORS.gray200)
|
||||
})
|
||||
const blendBase = store.createShape('ELLIPSE', 24, 18, 72, 72, blendCard)
|
||||
graph.updateNode(blendBase, {
|
||||
name: 'Multiply Blue',
|
||||
fills: [solid({ r: 0.16, g: 0.46, b: 1, a: 0.8 })],
|
||||
blendMode: 'MULTIPLY'
|
||||
})
|
||||
const blendMiddle = store.createShape('ELLIPSE', 70, 18, 72, 72, blendCard)
|
||||
graph.updateNode(blendMiddle, {
|
||||
name: 'Multiply Red',
|
||||
fills: [solid({ r: 1, g: 0.22, b: 0.34, a: 0.8 })],
|
||||
blendMode: 'MULTIPLY'
|
||||
})
|
||||
const blendTop = store.createShape('ELLIPSE', 116, 18, 72, 72, blendCard)
|
||||
graph.updateNode(blendTop, {
|
||||
name: 'Screen Gold',
|
||||
fills: [solid({ r: 1, g: 0.72, b: 0.16, a: 0.72 })],
|
||||
blendMode: 'SCREEN'
|
||||
})
|
||||
const blendText = store.createShape('TEXT', 168, 32, 72, 32, blendCard)
|
||||
graph.updateNode(blendText, {
|
||||
name: 'Label',
|
||||
text: 'Multiply\n+ Screen',
|
||||
fontSize: 12,
|
||||
fontWeight: 600,
|
||||
lineHeight: 16,
|
||||
fills: [solid(DEMO_COLORS.gray500)]
|
||||
})
|
||||
|
||||
const fillBlendCard = store.createShape('FRAME', 312, 520, 204, 92, effectsSectionId)
|
||||
graph.updateNode(fillBlendCard, {
|
||||
name: 'Fill Blend Card',
|
||||
cornerRadius: 16,
|
||||
fills: [
|
||||
gradient([
|
||||
{ color: DEMO_COLORS.purple, position: 0 },
|
||||
{ color: DEMO_COLORS.blue, position: 1 }
|
||||
]),
|
||||
{ ...solid(DEMO_COLORS.orange, 0.7), blendMode: 'OVERLAY' }
|
||||
],
|
||||
effects: [dropShadow(0, 6, 18, 0, { r: 0.23, g: 0.51, b: 0.96, a: 0.2 })]
|
||||
})
|
||||
const fillBlendText = store.createShape('TEXT', 20, 32, 164, 24, fillBlendCard)
|
||||
graph.updateNode(fillBlendText, {
|
||||
name: 'Label',
|
||||
text: 'Overlay fill',
|
||||
fontSize: 16,
|
||||
fontWeight: 700,
|
||||
fills: [solid(DEMO_COLORS.white)]
|
||||
})
|
||||
|
||||
const maskCard = store.createShape('FRAME', 540, 520, 300, 92, effectsSectionId)
|
||||
graph.updateNode(maskCard, {
|
||||
name: 'Mask Stack Card',
|
||||
cornerRadius: 16,
|
||||
fills: [solid(DEMO_COLORS.gray50)],
|
||||
strokes: thinStroke(DEMO_COLORS.gray200),
|
||||
clipsContent: true
|
||||
})
|
||||
const maskBg = store.createShape('RECTANGLE', 0, 0, 300, 92, maskCard)
|
||||
graph.updateNode(maskBg, {
|
||||
name: 'Backdrop',
|
||||
fills: [solid({ r: 0.08, g: 0.1, b: 0.18, a: 1 })]
|
||||
})
|
||||
const maskText = store.createShape('TEXT', 104, 26, 172, 36, maskCard)
|
||||
graph.updateNode(maskText, {
|
||||
name: 'Label',
|
||||
text: 'Sibling mask stack\nfrom imported files',
|
||||
fontSize: 13,
|
||||
fontWeight: 600,
|
||||
lineHeight: 17,
|
||||
fills: [solid(DEMO_COLORS.white)]
|
||||
})
|
||||
const maskShape = store.createShape('ELLIPSE', 22, 16, 60, 60, maskCard)
|
||||
graph.updateNode(maskShape, {
|
||||
name: 'Avatar Mask',
|
||||
fills: [solid(DEMO_COLORS.white)],
|
||||
isMask: true,
|
||||
maskType: 'ALPHA'
|
||||
})
|
||||
const stripeA = store.createShape('RECTANGLE', 14, 12, 84, 28, maskCard)
|
||||
graph.updateNode(stripeA, {
|
||||
name: 'Masked Stripe A',
|
||||
rotation: -20,
|
||||
fills: [solid(DEMO_COLORS.blue)]
|
||||
})
|
||||
const stripeB = store.createShape('RECTANGLE', 14, 36, 84, 28, maskCard)
|
||||
graph.updateNode(stripeB, {
|
||||
name: 'Masked Stripe B',
|
||||
rotation: -20,
|
||||
fills: [solid(DEMO_COLORS.purple)]
|
||||
})
|
||||
const stripeC = store.createShape('RECTANGLE', 14, 60, 84, 28, maskCard)
|
||||
graph.updateNode(stripeC, {
|
||||
name: 'Masked Stripe C',
|
||||
rotation: -20,
|
||||
fills: [solid(DEMO_COLORS.teal)]
|
||||
})
|
||||
|
||||
const smoothingLabel = store.createShape('TEXT', 32, 644, 280, 20, effectsSectionId)
|
||||
graph.updateNode(smoothingLabel, {
|
||||
name: 'Label',
|
||||
text: 'Corner smoothing & effect blends',
|
||||
fontSize: 13,
|
||||
fontWeight: 600,
|
||||
fills: [solid(DEMO_COLORS.gray500)]
|
||||
})
|
||||
|
||||
const smoothingCard = store.createShape('FRAME', 32, 676, 256, 104, effectsSectionId)
|
||||
graph.updateNode(smoothingCard, {
|
||||
name: 'Smooth Corner Comparison',
|
||||
cornerRadius: 16,
|
||||
fills: [solid(DEMO_COLORS.gray50)],
|
||||
strokes: thinStroke(DEMO_COLORS.gray200)
|
||||
})
|
||||
const regularCorner = store.createShape('RECTANGLE', 24, 18, 58, 58, smoothingCard)
|
||||
graph.updateNode(regularCorner, {
|
||||
name: 'Regular Radius',
|
||||
cornerRadius: 18,
|
||||
fills: [solid(DEMO_COLORS.blue)]
|
||||
})
|
||||
const smoothCorner = store.createShape('RECTANGLE', 102, 18, 58, 58, smoothingCard)
|
||||
graph.updateNode(smoothCorner, {
|
||||
name: 'Smoothed Radius',
|
||||
cornerRadius: 18,
|
||||
cornerSmoothing: 0.9,
|
||||
fills: [solid(DEMO_COLORS.purple)]
|
||||
})
|
||||
const cornerLabel = store.createShape('TEXT', 176, 26, 56, 36, smoothingCard)
|
||||
graph.updateNode(cornerLabel, {
|
||||
name: 'Label',
|
||||
text: 'Radius\n+ smooth',
|
||||
fontSize: 12,
|
||||
fontWeight: 600,
|
||||
lineHeight: 16,
|
||||
fills: [solid(DEMO_COLORS.gray500)]
|
||||
})
|
||||
|
||||
const independentCard = store.createShape('FRAME', 312, 676, 204, 104, effectsSectionId)
|
||||
graph.updateNode(independentCard, {
|
||||
name: 'Independent Smooth Corners',
|
||||
cornerRadius: 28,
|
||||
cornerSmoothing: 1,
|
||||
independentCorners: true,
|
||||
topLeftRadius: 36,
|
||||
topRightRadius: 10,
|
||||
bottomRightRadius: 36,
|
||||
bottomLeftRadius: 10,
|
||||
fills: [
|
||||
gradient([
|
||||
{ color: DEMO_COLORS.teal, position: 0 },
|
||||
{ color: DEMO_COLORS.blue, position: 1 }
|
||||
])
|
||||
],
|
||||
effects: [dropShadow(0, 8, 20, 0, { r: 0.08, g: 0.73, b: 0.73, a: 0.24 })]
|
||||
})
|
||||
const independentText = store.createShape('TEXT', 20, 34, 164, 24, independentCard)
|
||||
graph.updateNode(independentText, {
|
||||
name: 'Label',
|
||||
text: 'Independent corners',
|
||||
fontSize: 15,
|
||||
fontWeight: 700,
|
||||
fills: [solid(DEMO_COLORS.white)]
|
||||
})
|
||||
|
||||
const effectBlendCard = store.createShape('FRAME', 540, 676, 300, 104, effectsSectionId)
|
||||
graph.updateNode(effectBlendCard, {
|
||||
name: 'Effect Blend Card',
|
||||
cornerRadius: 18,
|
||||
fills: [solid({ r: 0.08, g: 0.1, b: 0.18, a: 1 })],
|
||||
clipsContent: true
|
||||
})
|
||||
const glowBase = store.createShape('ELLIPSE', 22, 14, 76, 76, effectBlendCard)
|
||||
graph.updateNode(glowBase, {
|
||||
name: 'Glow Base',
|
||||
fills: [solid({ r: 0.24, g: 0.46, b: 1, a: 1 })]
|
||||
})
|
||||
const glowShape = store.createShape('RECTANGLE', 70, 22, 96, 60, effectBlendCard)
|
||||
graph.updateNode(glowShape, {
|
||||
name: 'Screen Shadow Blend',
|
||||
cornerRadius: 22,
|
||||
cornerSmoothing: 0.85,
|
||||
fills: [solid({ r: 0.58, g: 0.27, b: 0.95, a: 0.7 })],
|
||||
effects: [
|
||||
{ ...dropShadow(0, 0, 28, 0, { r: 0.56, g: 0.33, b: 1, a: 0.72 }), blendMode: 'SCREEN' }
|
||||
]
|
||||
})
|
||||
const effectBlendText = store.createShape('TEXT', 184, 34, 88, 34, effectBlendCard)
|
||||
graph.updateNode(effectBlendText, {
|
||||
name: 'Label',
|
||||
text: 'Screen\nshadow',
|
||||
fontSize: 13,
|
||||
fontWeight: 700,
|
||||
lineHeight: 17,
|
||||
fills: [solid(DEMO_COLORS.white)]
|
||||
})
|
||||
}
|
||||
173
src/app/demo/sections/standalone.ts
Normal file
173
src/app/demo/sections/standalone.ts
Normal file
|
|
@ -0,0 +1,173 @@
|
|||
import { DEMO_COLORS, gradient, solid, thinStroke } from '@/app/demo/colors'
|
||||
import type { EditorStore } from '@/app/editor/session'
|
||||
|
||||
export function createStandaloneShapes(store: EditorStore) {
|
||||
const { graph } = store
|
||||
|
||||
const grad1 = store.createShape('FRAME', 60, 660, 180, 120)
|
||||
graph.updateNode(grad1, {
|
||||
name: 'Gradient Card',
|
||||
cornerRadius: 16,
|
||||
fills: [
|
||||
gradient([
|
||||
{ color: { r: 0.99, g: 0.37, b: 0.33, a: 1 }, position: 0 },
|
||||
{ color: { r: 0.93, g: 0.18, b: 0.65, a: 1 }, position: 0.5 },
|
||||
{ color: { r: 0.55, g: 0.28, b: 0.96, a: 1 }, position: 1 }
|
||||
])
|
||||
]
|
||||
})
|
||||
const gradText = store.createShape('TEXT', 20, 80, 140, 20, grad1)
|
||||
graph.updateNode(gradText, {
|
||||
name: 'Label',
|
||||
text: 'Linear Gradient',
|
||||
fontSize: 14,
|
||||
fontWeight: 600,
|
||||
fills: [solid(DEMO_COLORS.white)]
|
||||
})
|
||||
|
||||
const grad2 = store.createShape('FRAME', 260, 660, 180, 120)
|
||||
graph.updateNode(grad2, {
|
||||
name: 'Ocean',
|
||||
cornerRadius: 16,
|
||||
fills: [
|
||||
gradient([
|
||||
{ color: { r: 0.04, g: 0.82, b: 0.67, a: 1 }, position: 0 },
|
||||
{ color: { r: 0.13, g: 0.45, b: 0.96, a: 1 }, position: 1 }
|
||||
])
|
||||
]
|
||||
})
|
||||
const grad2Text = store.createShape('TEXT', 20, 80, 140, 20, grad2)
|
||||
graph.updateNode(grad2Text, {
|
||||
name: 'Label',
|
||||
text: 'Ocean Breeze',
|
||||
fontSize: 14,
|
||||
fontWeight: 600,
|
||||
fills: [solid(DEMO_COLORS.white)]
|
||||
})
|
||||
|
||||
const grad3 = store.createShape('FRAME', 460, 660, 180, 120)
|
||||
graph.updateNode(grad3, {
|
||||
name: 'Sunset',
|
||||
cornerRadius: 16,
|
||||
fills: [
|
||||
gradient([
|
||||
{ color: { r: 1, g: 0.6, b: 0.2, a: 1 }, position: 0 },
|
||||
{ color: { r: 0.96, g: 0.26, b: 0.21, a: 1 }, position: 1 }
|
||||
])
|
||||
]
|
||||
})
|
||||
const grad3Text = store.createShape('TEXT', 20, 80, 140, 20, grad3)
|
||||
graph.updateNode(grad3Text, {
|
||||
name: 'Label',
|
||||
text: 'Warm Sunset',
|
||||
fontSize: 14,
|
||||
fontWeight: 600,
|
||||
fills: [solid(DEMO_COLORS.white)]
|
||||
})
|
||||
|
||||
const typoFrame = store.createShape('FRAME', 700, 660, 320, 210)
|
||||
graph.updateNode(typoFrame, {
|
||||
name: 'Typography, OpenType & Decorations',
|
||||
cornerRadius: 12,
|
||||
fills: [solid(DEMO_COLORS.white)],
|
||||
strokes: thinStroke(DEMO_COLORS.gray200),
|
||||
layoutMode: 'VERTICAL',
|
||||
primaryAxisSizing: 'FIXED',
|
||||
counterAxisSizing: 'FIXED',
|
||||
itemSpacing: 6,
|
||||
paddingTop: 16,
|
||||
paddingBottom: 16,
|
||||
paddingLeft: 20,
|
||||
paddingRight: 20
|
||||
})
|
||||
const typoItems = [
|
||||
{ text: 'Heading', size: 24, weight: 700 },
|
||||
{ text: 'Subheading', size: 16, weight: 600 },
|
||||
{ text: 'Body text — The quick brown fox jumps.', size: 13, weight: 400 },
|
||||
{
|
||||
text: 'Ligatures off — office affine',
|
||||
size: 13,
|
||||
weight: 500,
|
||||
features: [{ tag: 'LIGA', enabled: false }]
|
||||
},
|
||||
{
|
||||
text: 'Raw OT tags — DLIG on · KERN off',
|
||||
size: 11,
|
||||
weight: 500,
|
||||
features: [
|
||||
{ tag: 'DLIG', enabled: true },
|
||||
{ tag: 'KERN', enabled: false }
|
||||
]
|
||||
},
|
||||
{
|
||||
text: 'Wavy underline from imported .fig',
|
||||
size: 13,
|
||||
weight: 500,
|
||||
decoration: 'UNDERLINE' as const,
|
||||
decorationStyle: 'WAVY' as const,
|
||||
decorationFills: [solid(DEMO_COLORS.red)],
|
||||
decorationThickness: 1.6
|
||||
},
|
||||
{
|
||||
text: 'Dotted underline + custom thickness',
|
||||
size: 13,
|
||||
weight: 500,
|
||||
decoration: 'UNDERLINE' as const,
|
||||
decorationStyle: 'DOTTED' as const,
|
||||
decorationFills: [solid(DEMO_COLORS.blue)],
|
||||
decorationThickness: 2
|
||||
}
|
||||
]
|
||||
for (const t of typoItems) {
|
||||
const tid = store.createShape('TEXT', 0, 0, 280, t.size + 4, typoFrame)
|
||||
graph.updateNode(tid, {
|
||||
name: t.text.split(' ')[0],
|
||||
text: t.text,
|
||||
fontSize: t.size,
|
||||
fontWeight: t.weight,
|
||||
fontFeatures: t.features ?? [],
|
||||
textDecoration: t.decoration ?? 'NONE',
|
||||
textDecorationStyle: t.decorationStyle ?? 'SOLID',
|
||||
textDecorationFills: t.decorationFills ?? [],
|
||||
textDecorationThickness: t.decorationThickness ?? null,
|
||||
textAutoResize: 'HEIGHT',
|
||||
layoutAlignSelf: 'STRETCH',
|
||||
fills: [solid(DEMO_COLORS.black)]
|
||||
})
|
||||
}
|
||||
|
||||
const shapes = [
|
||||
{
|
||||
type: 'ELLIPSE' as const,
|
||||
x: 1060,
|
||||
fill: gradient([
|
||||
{ color: DEMO_COLORS.purple, position: 0 },
|
||||
{ color: DEMO_COLORS.blue, position: 1 }
|
||||
])
|
||||
},
|
||||
{
|
||||
type: 'RECTANGLE' as const,
|
||||
x: 1160,
|
||||
fill: gradient([
|
||||
{ color: DEMO_COLORS.green, position: 0 },
|
||||
{ color: DEMO_COLORS.teal, position: 1 }
|
||||
])
|
||||
},
|
||||
{
|
||||
type: 'ELLIPSE' as const,
|
||||
x: 1260,
|
||||
fill: gradient([
|
||||
{ color: DEMO_COLORS.orange, position: 0 },
|
||||
{ color: DEMO_COLORS.red, position: 1 }
|
||||
])
|
||||
}
|
||||
]
|
||||
for (const s of shapes) {
|
||||
const id = store.createShape(s.type, s.x, 680, 80, 80)
|
||||
graph.updateNode(id, {
|
||||
name: s.type === 'ELLIPSE' ? 'Circle' : 'Square',
|
||||
cornerRadius: s.type === 'RECTANGLE' ? 16 : 0,
|
||||
fills: [s.fill]
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -33,40 +33,40 @@ export function createDemoVariables(store: EditorStore) {
|
|||
{
|
||||
id: 'var-gray-50',
|
||||
name: 'Gray/50',
|
||||
light: DEMO_COLORS.bg,
|
||||
light: DEMO_COLORS.gray50,
|
||||
dark: { r: 0.1, g: 0.1, b: 0.11, a: 1 }
|
||||
},
|
||||
{
|
||||
id: 'var-gray-100',
|
||||
name: 'Gray/100',
|
||||
light: DEMO_COLORS.surfaceSunken,
|
||||
light: DEMO_COLORS.gray100,
|
||||
dark: { r: 0.14, g: 0.14, b: 0.16, a: 1 }
|
||||
},
|
||||
{
|
||||
id: 'var-gray-200',
|
||||
name: 'Gray/200',
|
||||
light: DEMO_COLORS.border,
|
||||
light: DEMO_COLORS.gray200,
|
||||
dark: { r: 0.22, g: 0.22, b: 0.24, a: 1 }
|
||||
},
|
||||
{
|
||||
id: 'var-gray-500',
|
||||
name: 'Gray/500',
|
||||
light: DEMO_COLORS.textSecondary,
|
||||
light: DEMO_COLORS.gray500,
|
||||
dark: { r: 0.65, g: 0.65, b: 0.68, a: 1 }
|
||||
},
|
||||
{
|
||||
id: 'var-blue',
|
||||
name: 'Blue',
|
||||
light: DEMO_COLORS.accent,
|
||||
light: DEMO_COLORS.blue,
|
||||
dark: { r: 0.33, g: 0.61, b: 1, a: 1 }
|
||||
},
|
||||
{
|
||||
id: 'var-green',
|
||||
name: 'Green',
|
||||
light: DEMO_COLORS.success,
|
||||
light: DEMO_COLORS.green,
|
||||
dark: { r: 0.23, g: 0.87, b: 0.52, a: 1 }
|
||||
},
|
||||
{ id: 'var-red', name: 'Red', light: DEMO_COLORS.danger, dark: { r: 1, g: 0.32, b: 0.32, a: 1 } }
|
||||
{ id: 'var-red', name: 'Red', light: DEMO_COLORS.red, dark: { r: 1, g: 0.32, b: 0.32, a: 1 } }
|
||||
]
|
||||
|
||||
for (const c of primitiveColors) {
|
||||
|
|
|
|||
|
|
@ -1,20 +1,36 @@
|
|||
import { describe, expect, test } from 'bun:test'
|
||||
|
||||
import { createDemoShapes } from '@/app/demo/document'
|
||||
import { createStandaloneShapes } from '@/app/demo/sections/standalone'
|
||||
import { createEditorStore } from '@/app/editor/session'
|
||||
|
||||
describe('demo document', () => {
|
||||
test('builds the component library and analytics showcase', () => {
|
||||
test('showcases imported OpenType and text decoration features', () => {
|
||||
const store = createEditorStore()
|
||||
|
||||
createDemoShapes(store)
|
||||
createStandaloneShapes(store)
|
||||
|
||||
const nodes = [...store.graph.getAllNodes()]
|
||||
expect(nodes.some((node) => node.name === 'Component Library')).toBe(true)
|
||||
expect(nodes.some((node) => node.name === 'App — Analytics')).toBe(true)
|
||||
expect(nodes.some((node) => node.name === 'Chart')).toBe(true)
|
||||
expect(nodes.filter((node) => node.type === 'COMPONENT').length).toBeGreaterThanOrEqual(6)
|
||||
expect(nodes.filter((node) => node.type === 'INSTANCE').length).toBeGreaterThan(6)
|
||||
expect(store.graph.variables.size).toBeGreaterThan(0)
|
||||
const ligatures = nodes.find((node) => node.name === 'Ligatures')
|
||||
const rawTags = nodes.find((node) => node.name === 'Raw')
|
||||
const wavy = nodes.find((node) => node.name === 'Wavy')
|
||||
const dotted = nodes.find((node) => node.name === 'Dotted')
|
||||
|
||||
expect(ligatures?.fontFeatures).toEqual([{ tag: 'LIGA', enabled: false }])
|
||||
expect(rawTags?.fontFeatures).toEqual([
|
||||
{ tag: 'DLIG', enabled: true },
|
||||
{ tag: 'KERN', enabled: false }
|
||||
])
|
||||
expect(wavy).toMatchObject({
|
||||
textDecoration: 'UNDERLINE',
|
||||
textDecorationStyle: 'WAVY',
|
||||
textDecorationThickness: 1.6
|
||||
})
|
||||
expect(wavy?.textDecorationFills[0]?.type).toBe('SOLID')
|
||||
expect(dotted).toMatchObject({
|
||||
textDecoration: 'UNDERLINE',
|
||||
textDecorationStyle: 'DOTTED',
|
||||
textDecorationThickness: 2
|
||||
})
|
||||
expect(dotted?.textDecorationFills[0]?.type).toBe('SOLID')
|
||||
})
|
||||
})
|
||||
|
|
|
|||
Loading…
Reference in a new issue