feat(dom-css): map flex alignment

This commit is contained in:
Danila Poyarkov 2026-06-02 19:53:16 +03:00
parent 101a047e4d
commit f43c50338b
3 changed files with 91 additions and 1 deletions

View file

@ -16,6 +16,21 @@ function nodeChildren(graph: SceneGraph, node: SceneNode): SceneNode[] {
.filter((child): child is SceneNode => child !== undefined)
}
function justifyContentToCSS(value: SceneNode['primaryAxisAlign']): string | undefined {
if (value === 'CENTER') return 'center'
if (value === 'MAX') return 'flex-end'
if (value === 'SPACE_BETWEEN') return 'space-between'
return undefined
}
function alignItemsToCSS(value: SceneNode['counterAxisAlign']): string | undefined {
if (value === 'CENTER') return 'center'
if (value === 'MAX') return 'flex-end'
if (value === 'STRETCH') return 'stretch'
if (value === 'BASELINE') return 'baseline'
return undefined
}
function styleFromSceneNode(node: SceneNode): DesignStyleDeclaration {
const style = sceneNodeSizeStyle(node)
const fill = fillToCSS(node.fills[0])
@ -30,6 +45,10 @@ function styleFromSceneNode(node: SceneNode): DesignStyleDeclaration {
if (node.layoutMode !== 'NONE') {
style.display = 'flex'
style['flex-direction'] = node.layoutMode === 'HORIZONTAL' ? 'row' : 'column'
const justifyContent = justifyContentToCSS(node.primaryAxisAlign)
const alignItems = alignItemsToCSS(node.counterAxisAlign)
if (justifyContent) style['justify-content'] = justifyContent
if (alignItems) style['align-items'] = alignItems
if (node.itemSpacing > 0) style.gap = `${node.itemSpacing}px`
if (node.paddingTop > 0) style['padding-top'] = `${node.paddingTop}px`
if (node.paddingRight > 0) style['padding-right'] = `${node.paddingRight}px`

View file

@ -56,6 +56,21 @@ function setNodeBox(node: SceneNode, style: DesignStyleDeclaration): void {
if (height !== null) node.height = height
}
function primaryAxisAlignFromCSS(value: string | undefined): SceneNode['primaryAxisAlign'] {
if (value === 'center') return 'CENTER'
if (value === 'end' || value === 'flex-end') return 'MAX'
if (value === 'space-between') return 'SPACE_BETWEEN'
return 'MIN'
}
function counterAxisAlignFromCSS(value: string | undefined): SceneNode['counterAxisAlign'] {
if (value === 'center') return 'CENTER'
if (value === 'end' || value === 'flex-end') return 'MAX'
if (value === 'stretch') return 'STRETCH'
if (value === 'baseline') return 'BASELINE'
return 'MIN'
}
function applyElementStyle(node: SceneNode, style: DesignStyleDeclaration): void {
setNodeBox(node, style)
@ -77,8 +92,11 @@ function applyElementStyle(node: SceneNode, style: DesignStyleDeclaration): void
const cornerRadius = firstCSSNumber(style, 'border-radius')
if (cornerRadius !== null) node.cornerRadius = cornerRadius
if (pickStyle(style, 'display') === 'flex') {
const display = pickStyle(style, 'display')
if (display === 'flex' || display === 'inline-flex') {
node.layoutMode = pickStyle(style, 'flex-direction') === 'column' ? 'VERTICAL' : 'HORIZONTAL'
node.primaryAxisAlign = primaryAxisAlignFromCSS(pickStyle(style, 'justify-content'))
node.counterAxisAlign = counterAxisAlignFromCSS(pickStyle(style, 'align-items'))
node.itemSpacing = firstCSSNumber(style, 'gap', 'column-gap', 'row-gap') ?? 0
node.paddingTop = firstCSSNumber(style, 'padding-top', 'padding') ?? 0
node.paddingRight = firstCSSNumber(style, 'padding-right', 'padding') ?? 0

View file

@ -71,6 +71,59 @@ describe('@open-pencil/dom-css conversion', () => {
expect(html).toContain('box-shadow')
})
it('maps CSS flex alignment into scene graph auto-layout alignment', () => {
const graph = designDocumentToSceneGraph({
type: 'document',
children: [
{
type: 'element',
tagName: 'div',
attrs: { class: 'toolbar' },
computedStyle: {
display: 'inline-flex',
'align-items': 'center',
'justify-content': 'space-between',
gap: '8px',
width: '240px',
height: '40px'
},
children: [
{
type: 'element',
tagName: 'span',
attrs: {},
children: [{ type: 'text', text: 'File' }]
},
{
type: 'element',
tagName: 'span',
attrs: {},
children: [{ type: 'text', text: 'Edit' }]
}
]
}
]
})
const page = graph.getPages()[0]
const toolbar = page ? graph.getChildren(page.id)[0] : undefined
expect(toolbar?.type).toBe('FRAME')
if (toolbar?.type !== 'FRAME') return
expect(toolbar.layoutMode).toBe('HORIZONTAL')
expect(toolbar.primaryAxisAlign).toBe('SPACE_BETWEEN')
expect(toolbar.counterAxisAlign).toBe('CENTER')
const roundTrip = sceneGraphToDesignDocument(graph)
const root = roundTrip.children[0]
expect(root?.type).toBe('element')
if (root?.type !== 'element') return
const roundTripToolbar = root.children[0]
expect(roundTripToolbar?.type).toBe('element')
if (roundTripToolbar?.type !== 'element') return
expect(roundTripToolbar.inlineStyle?.['justify-content']).toBe('space-between')
expect(roundTripToolbar.inlineStyle?.['align-items']).toBe('center')
})
it('projects a Tailwind card through generated CSS into a scene graph', async () => {
const runtime = createHeadlessCSSRuntime()
const classes = [...tailwindCardClasses]