feat(dom-css): map flex alignment
This commit is contained in:
parent
101a047e4d
commit
f43c50338b
|
|
@ -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`
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
Loading…
Reference in a new issue