feat(dom-css): improve CSS export parity
This commit is contained in:
parent
c1499b1745
commit
41c00a437f
|
|
@ -72,15 +72,16 @@ For DesignDOM output without creating a scene graph, use `htmlToDesignDocument()
|
|||
|
||||
## Tailwind pipeline
|
||||
|
||||
Tailwind classes flow through Tailwind's own compiler, then through the CSS runtime:
|
||||
Tailwind classes flow through Tailwind's own compiler, then through the CSS runtime. Prefer the browser runtime when a document is available so custom properties, `calc()`, modern colors, and browser-default behavior come from native `getComputedStyle()`:
|
||||
|
||||
```ts
|
||||
import { tailwindHTMLToSceneGraph } from '@open-pencil/dom-css'
|
||||
import { createBrowserCSSRuntime, tailwindHTMLToSceneGraph } from '@open-pencil/dom-css'
|
||||
|
||||
const classes = ['flex', 'w-80', 'p-6', 'rounded-xl', 'bg-white']
|
||||
const graph = await tailwindHTMLToSceneGraph(
|
||||
`<article class="${classes.join(' ')}">OpenPencil</article>`,
|
||||
classes
|
||||
classes,
|
||||
{ runtime: createBrowserCSSRuntime({ sandbox: 'iframe' }) }
|
||||
)
|
||||
```
|
||||
|
||||
|
|
@ -91,7 +92,7 @@ const graph = await tailwindHTMLToSceneGraph(
|
|||
- DOM-shaped `DesignDocument` / `DesignElement` types
|
||||
- Browser-backed runtime adapter for native HTML parsing, serialization, and computed-style extraction
|
||||
- Headless runtime adapter with `parse5` HTML parsing and CSSOM-backed style computation for basic selectors, nested CSSOM rules, cascade order, inheritance, common shorthands, and simple custom-property/calc values
|
||||
- SceneGraph ⇄ DesignDOM conversion for simple HTML/CSS-shaped layouts
|
||||
- SceneGraph ⇄ DesignDOM conversion for simple HTML/CSS-shaped layouts, including flex alignment, logical padding, independent side borders, constraints, clipping, opacity, typography, and shadows
|
||||
- Tailwind v4 compiler adapter
|
||||
- Browser oracle fixtures for CSS variables, `calc()`, modern color output, and Tailwind utility output
|
||||
|
||||
|
|
|
|||
|
|
@ -33,9 +33,15 @@ export function colorToFillFromCSS(value: string | undefined): Fill[] {
|
|||
return [{ type: 'SOLID', color, opacity: color.a, visible: true }]
|
||||
}
|
||||
|
||||
export function strokeToCSS(stroke: Stroke | undefined): string | undefined {
|
||||
export function strokeColorToCSS(stroke: Stroke | undefined): string | undefined {
|
||||
if (!stroke?.visible) return undefined
|
||||
return `${stroke.weight}px solid ${colorToCSS({ ...stroke.color, a: stroke.opacity })}`
|
||||
return colorToCSS({ ...stroke.color, a: stroke.opacity })
|
||||
}
|
||||
|
||||
export function strokeToCSS(stroke: Stroke | undefined): string | undefined {
|
||||
const color = strokeColorToCSS(stroke)
|
||||
if (!color || !stroke) return undefined
|
||||
return `${stroke.weight}px solid ${color}`
|
||||
}
|
||||
|
||||
export function colorToStrokeFromCSS(
|
||||
|
|
|
|||
|
|
@ -2,7 +2,13 @@ import { colorToCSS } from '@open-pencil/core/color'
|
|||
import { BLACK } from '@open-pencil/core/constants'
|
||||
import type { SceneGraph, SceneNode } from '@open-pencil/core/scene-graph'
|
||||
|
||||
import { dropShadowToCSS, fillToCSS, sceneNodeSizeStyle, strokeToCSS } from './css-values'
|
||||
import {
|
||||
dropShadowToCSS,
|
||||
fillToCSS,
|
||||
sceneNodeSizeStyle,
|
||||
strokeColorToCSS,
|
||||
strokeToCSS
|
||||
} from './css-values'
|
||||
import type { DesignDocument, DesignNode, DesignStyleDeclaration } from './types'
|
||||
|
||||
export interface SceneGraphToDesignOptions {
|
||||
|
|
@ -52,19 +58,46 @@ function addCornerRadii(style: DesignStyleDeclaration, node: SceneNode): void {
|
|||
}
|
||||
|
||||
function addStroke(style: DesignStyleDeclaration, node: SceneNode): void {
|
||||
const stroke = strokeToCSS(node.strokes[0])
|
||||
if (!stroke) return
|
||||
const stroke = node.strokes[0]
|
||||
const border = strokeToCSS(stroke)
|
||||
if (!border) return
|
||||
if (!node.independentStrokeWeights) {
|
||||
style.border = stroke
|
||||
style.border = border
|
||||
return
|
||||
}
|
||||
|
||||
const color = strokeColorToCSS(stroke) ?? 'currentColor'
|
||||
style['border-style'] = 'solid'
|
||||
style['border-color'] = stroke.split(' solid ').at(1) ?? 'currentColor'
|
||||
if (node.borderTopWeight > 0) style['border-top-width'] = `${node.borderTopWeight}px`
|
||||
if (node.borderRightWeight > 0) style['border-right-width'] = `${node.borderRightWeight}px`
|
||||
if (node.borderBottomWeight > 0) style['border-bottom-width'] = `${node.borderBottomWeight}px`
|
||||
if (node.borderLeftWeight > 0) style['border-left-width'] = `${node.borderLeftWeight}px`
|
||||
style['border-color'] = color
|
||||
style['border-top-width'] = `${node.borderTopWeight}px`
|
||||
style['border-right-width'] = `${node.borderRightWeight}px`
|
||||
style['border-bottom-width'] = `${node.borderBottomWeight}px`
|
||||
style['border-left-width'] = `${node.borderLeftWeight}px`
|
||||
}
|
||||
|
||||
function addPadding(style: DesignStyleDeclaration, node: SceneNode): void {
|
||||
const { paddingTop, paddingRight, paddingBottom, paddingLeft } = node
|
||||
if (paddingTop === 0 && paddingRight === 0 && paddingBottom === 0 && paddingLeft === 0) return
|
||||
|
||||
if (
|
||||
paddingTop === paddingRight &&
|
||||
paddingRight === paddingBottom &&
|
||||
paddingBottom === paddingLeft
|
||||
) {
|
||||
style.padding = `${paddingTop}px`
|
||||
return
|
||||
}
|
||||
|
||||
if (paddingTop === paddingBottom && paddingRight === paddingLeft) {
|
||||
if (paddingTop > 0) style['padding-block'] = `${paddingTop}px`
|
||||
if (paddingRight > 0) style['padding-inline'] = `${paddingRight}px`
|
||||
return
|
||||
}
|
||||
|
||||
if (paddingTop > 0) style['padding-top'] = `${paddingTop}px`
|
||||
if (paddingRight > 0) style['padding-right'] = `${paddingRight}px`
|
||||
if (paddingBottom > 0) style['padding-bottom'] = `${paddingBottom}px`
|
||||
if (paddingLeft > 0) style['padding-left'] = `${paddingLeft}px`
|
||||
}
|
||||
|
||||
function styleFromSceneNode(node: SceneNode): DesignStyleDeclaration {
|
||||
|
|
@ -87,10 +120,7 @@ function styleFromSceneNode(node: SceneNode): DesignStyleDeclaration {
|
|||
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`
|
||||
if (node.paddingBottom > 0) style['padding-bottom'] = `${node.paddingBottom}px`
|
||||
if (node.paddingLeft > 0) style['padding-left'] = `${node.paddingLeft}px`
|
||||
addPadding(style, node)
|
||||
}
|
||||
|
||||
return style
|
||||
|
|
@ -107,6 +137,9 @@ function styleFromTextNode(node: SceneNode): DesignStyleDeclaration {
|
|||
if (node.letterSpacing !== 0) style['letter-spacing'] = `${node.letterSpacing}px`
|
||||
if (node.textAlignHorizontal !== 'LEFT')
|
||||
style['text-align'] = node.textAlignHorizontal.toLowerCase()
|
||||
if (node.opacity < 1) style.opacity = String(node.opacity)
|
||||
const shadow = dropShadowToCSS(node.effects[0])
|
||||
if (shadow) style['text-shadow'] = shadow
|
||||
if (node.textDecoration !== 'NONE') {
|
||||
style['text-decoration-line'] =
|
||||
node.textDecoration === 'UNDERLINE' ? 'underline' : 'line-through'
|
||||
|
|
|
|||
|
|
@ -74,15 +74,18 @@ function firstStrokeColor(style: DesignStyleDeclaration) {
|
|||
)
|
||||
}
|
||||
|
||||
function firstStrokeWeight(style: DesignStyleDeclaration) {
|
||||
return firstCSSNumber(
|
||||
style,
|
||||
'border-width',
|
||||
'border-top-width',
|
||||
'border-right-width',
|
||||
'border-bottom-width',
|
||||
'border-left-width'
|
||||
)
|
||||
function strokeWeightFromStyle(style: DesignStyleDeclaration) {
|
||||
const borderWidth = firstCSSNumber(style, 'border-width')
|
||||
if (borderWidth !== null) return borderWidth
|
||||
|
||||
const sideWeights = [
|
||||
firstCSSNumber(style, 'border-top-width'),
|
||||
firstCSSNumber(style, 'border-right-width'),
|
||||
firstCSSNumber(style, 'border-bottom-width'),
|
||||
firstCSSNumber(style, 'border-left-width')
|
||||
].filter((weight): weight is number => weight !== null)
|
||||
if (sideWeights.length === 0) return null
|
||||
return Math.max(...sideWeights)
|
||||
}
|
||||
|
||||
function setBorderWeights(node: SceneNode, style: DesignStyleDeclaration, stroke: Stroke): void {
|
||||
|
|
@ -153,7 +156,7 @@ function applyElementStyle(node: SceneNode, style: DesignStyleDeclaration): void
|
|||
|
||||
const strokes = colorToStrokeFromCSS(
|
||||
firstStrokeColor(style),
|
||||
firstStrokeWeight(style)?.toString()
|
||||
strokeWeightFromStyle(style)?.toString()
|
||||
)
|
||||
if (strokes.length > 0) {
|
||||
node.strokes = strokes
|
||||
|
|
@ -197,6 +200,12 @@ function applyTextStyle(node: SceneNode, style: DesignStyleDeclaration): void {
|
|||
const letterSpacing = parseCSSNumber(pickStyle(style, 'letter-spacing'))
|
||||
if (letterSpacing !== null) node.letterSpacing = letterSpacing
|
||||
|
||||
const opacity = parseCSSNumber(pickStyle(style, 'opacity'))
|
||||
if (opacity !== null) node.opacity = opacity
|
||||
|
||||
const effects = dropShadowFromCSS(pickStyle(style, 'text-shadow'))
|
||||
if (effects.length > 0) node.effects = effects
|
||||
|
||||
const fontFamily = pickStyle(style, 'font-family')
|
||||
if (fontFamily)
|
||||
node.fontFamily = fontFamily.split(',')[0]?.replaceAll('"', '').trim() || node.fontFamily
|
||||
|
|
@ -247,7 +256,14 @@ function hasBoxStyle(style: DesignStyleDeclaration): boolean {
|
|||
'padding-right',
|
||||
'padding-bottom',
|
||||
'padding-left',
|
||||
'width'
|
||||
'padding-block',
|
||||
'padding-inline',
|
||||
'width',
|
||||
'min-width',
|
||||
'max-width',
|
||||
'min-height',
|
||||
'max-height',
|
||||
'overflow'
|
||||
].some((property) => pickStyle(style, property) !== undefined)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
import { describe, expect, it } from 'bun:test'
|
||||
|
||||
import type { SceneGraph, SceneNode } from '@open-pencil/core/scene-graph'
|
||||
|
||||
import type { DesignElement } from '../src/index'
|
||||
import {
|
||||
createHeadlessCSSRuntime,
|
||||
designDocumentToSceneGraph,
|
||||
|
|
@ -8,7 +11,128 @@ import {
|
|||
sceneGraphToDesignDocument,
|
||||
serializeHTML
|
||||
} from '../src/index'
|
||||
import { cardCSS, cardHTML, fixtureCSS, fixtureHTML } from './helpers'
|
||||
import { TEST_COLORS, cardCSS, cardHTML, fixtureCSS, fixtureHTML } from './helpers'
|
||||
|
||||
function expectFrame(node: SceneNode | undefined) {
|
||||
expect(node?.type).toBe('FRAME')
|
||||
if (node?.type !== 'FRAME') throw new Error('Expected frame node')
|
||||
return node
|
||||
}
|
||||
|
||||
function expectText(node: SceneNode | undefined) {
|
||||
expect(node?.type).toBe('TEXT')
|
||||
if (node?.type !== 'TEXT') throw new Error('Expected text node')
|
||||
return node
|
||||
}
|
||||
|
||||
function createStyleRoundTripGraph() {
|
||||
return designDocumentToSceneGraph({
|
||||
type: 'document',
|
||||
children: [
|
||||
{
|
||||
type: 'element',
|
||||
tagName: 'section',
|
||||
attrs: { class: 'panel' },
|
||||
computedStyle: {
|
||||
display: 'flex',
|
||||
'flex-direction': 'column',
|
||||
width: '240px',
|
||||
height: '120px',
|
||||
'padding-block': '8px',
|
||||
'padding-inline': '12px',
|
||||
'border-color': TEST_COLORS.slate200,
|
||||
'border-top-width': '0px',
|
||||
'border-right-width': '2px',
|
||||
'border-bottom-width': '0px',
|
||||
'border-left-width': '4px',
|
||||
opacity: '0.75',
|
||||
'box-shadow': `0px 8px 24px 0px ${TEST_COLORS.slateShadow}`
|
||||
},
|
||||
children: [
|
||||
{
|
||||
type: 'element',
|
||||
tagName: 'h1',
|
||||
attrs: {},
|
||||
computedStyle: {
|
||||
color: TEST_COLORS.slate950,
|
||||
'font-family': 'Inter, sans-serif',
|
||||
'font-size': '18px',
|
||||
'font-weight': '700',
|
||||
'line-height': '24px',
|
||||
'letter-spacing': '0.2px',
|
||||
'text-align': 'center',
|
||||
opacity: '0.5',
|
||||
'text-shadow': `0px 1px 2px 0px ${TEST_COLORS.slateShadow}`
|
||||
},
|
||||
children: [{ type: 'text', text: 'Round trip' }]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
function expectStyleRoundTripPanel(graph: SceneGraph) {
|
||||
const page = graph.getPages()[0]
|
||||
const panel = expectFrame(page ? graph.getChildren(page.id)[0] : undefined)
|
||||
expect(panel.paddingTop).toBe(8)
|
||||
expect(panel.paddingRight).toBe(12)
|
||||
expect(panel.paddingBottom).toBe(8)
|
||||
expect(panel.paddingLeft).toBe(12)
|
||||
expect(panel.borderTopWeight).toBe(0)
|
||||
expect(panel.borderRightWeight).toBe(2)
|
||||
expect(panel.borderBottomWeight).toBe(0)
|
||||
expect(panel.borderLeftWeight).toBe(4)
|
||||
expect(panel.opacity).toBe(0.75)
|
||||
expect(panel.effects[0]?.type).toBe('DROP_SHADOW')
|
||||
return panel
|
||||
}
|
||||
|
||||
function expectStyleRoundTripText(graph: SceneGraph, panel: SceneNode) {
|
||||
const heading = expectText(graph.getChildren(panel.id)[0])
|
||||
expect(heading.fontSize).toBe(18)
|
||||
expect(heading.fontWeight).toBe(700)
|
||||
expect(heading.lineHeight).toBe(24)
|
||||
expect(heading.letterSpacing).toBe(0.2)
|
||||
expect(heading.textAlignHorizontal).toBe('CENTER')
|
||||
expect(heading.opacity).toBe(0.5)
|
||||
expect(heading.effects[0]?.type).toBe('DROP_SHADOW')
|
||||
}
|
||||
|
||||
function expectRoundTripPanelStyle(element: DesignElement) {
|
||||
expect(element.inlineStyle?.['padding-block']).toBe('8px')
|
||||
expect(element.inlineStyle?.['padding-inline']).toBe('12px')
|
||||
expect(element.inlineStyle?.['border-top-width']).toBe('0px')
|
||||
expect(element.inlineStyle?.['border-left-width']).toBe('4px')
|
||||
expect(element.inlineStyle?.opacity).toBe('0.75')
|
||||
expect(element.inlineStyle?.['box-shadow']).toContain('24px')
|
||||
}
|
||||
|
||||
function expectRoundTripHeadingStyle(element: DesignElement) {
|
||||
expect(element.inlineStyle?.['font-size']).toBe('18px')
|
||||
expect(element.inlineStyle?.['font-weight']).toBe('700')
|
||||
expect(element.inlineStyle?.['line-height']).toBe('24px')
|
||||
expect(element.inlineStyle?.['letter-spacing']).toBe('0.2px')
|
||||
expect(element.inlineStyle?.['text-align']).toBe('center')
|
||||
expect(element.inlineStyle?.opacity).toBe('0.5')
|
||||
expect(element.inlineStyle?.['text-shadow']).toContain('2px')
|
||||
}
|
||||
|
||||
function expectStyleRoundTripHTML(graph: SceneGraph) {
|
||||
const roundTrip = sceneGraphToDesignDocument(graph)
|
||||
const root = roundTrip.children[0]
|
||||
expect(root?.type).toBe('element')
|
||||
if (root?.type !== 'element') throw new Error('Expected root element')
|
||||
const roundTripPanel = root.children[0]
|
||||
expect(roundTripPanel?.type).toBe('element')
|
||||
if (roundTripPanel?.type !== 'element') throw new Error('Expected panel element')
|
||||
expectRoundTripPanelStyle(roundTripPanel)
|
||||
|
||||
const roundTripHeading = roundTripPanel.children[0]
|
||||
expect(roundTripHeading?.type).toBe('element')
|
||||
if (roundTripHeading?.type !== 'element') throw new Error('Expected heading element')
|
||||
expectRoundTripHeadingStyle(roundTripHeading)
|
||||
}
|
||||
|
||||
describe('@open-pencil/dom-css conversion', () => {
|
||||
it('converts HTML and CSS to DesignDOM with one API call', async () => {
|
||||
|
|
@ -92,4 +216,11 @@ describe('@open-pencil/dom-css conversion', () => {
|
|||
expect(card.fills[0]?.type).toBe('SOLID')
|
||||
expect(card.strokes[0]?.weight).toBe(1)
|
||||
})
|
||||
|
||||
it('round-trips logical padding, side borders, opacity, and text style fields', () => {
|
||||
const graph = createStyleRoundTripGraph()
|
||||
const panel = expectStyleRoundTripPanel(graph)
|
||||
expectStyleRoundTripText(graph, panel)
|
||||
expectStyleRoundTripHTML(graph)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { describe, expect, it } from 'bun:test'
|
||||
|
||||
import type { SceneNode } from '@open-pencil/core/scene-graph'
|
||||
import type { SceneGraph, SceneNode } from '@open-pencil/core/scene-graph'
|
||||
import type { DesignElement } from '@open-pencil/dom-css'
|
||||
import {
|
||||
compileTailwindCSS,
|
||||
createHeadlessCSSRuntime,
|
||||
|
|
@ -13,6 +14,7 @@ import {
|
|||
} from '@open-pencil/dom-css'
|
||||
|
||||
import {
|
||||
DOM_CSS_COLORS,
|
||||
computedCardDocument,
|
||||
cssCardCSS,
|
||||
cssCardHTML,
|
||||
|
|
@ -30,6 +32,114 @@ function expectFrame(node: SceneNode | undefined) {
|
|||
return node
|
||||
}
|
||||
|
||||
function createStyleRoundTripGraph() {
|
||||
return designDocumentToSceneGraph({
|
||||
type: 'document',
|
||||
children: [
|
||||
{
|
||||
type: 'element',
|
||||
tagName: 'section',
|
||||
attrs: { class: 'panel' },
|
||||
computedStyle: {
|
||||
display: 'flex',
|
||||
'flex-direction': 'column',
|
||||
width: '240px',
|
||||
height: '120px',
|
||||
'padding-block': '8px',
|
||||
'padding-inline': '12px',
|
||||
'border-color': DOM_CSS_COLORS.slate200,
|
||||
'border-top-width': '0px',
|
||||
'border-right-width': '2px',
|
||||
'border-bottom-width': '0px',
|
||||
'border-left-width': '4px',
|
||||
opacity: '0.75',
|
||||
'box-shadow': `0px 8px 24px 0px ${DOM_CSS_COLORS.slateShadow}`
|
||||
},
|
||||
children: [
|
||||
{
|
||||
type: 'element',
|
||||
tagName: 'h1',
|
||||
attrs: {},
|
||||
computedStyle: {
|
||||
color: DOM_CSS_COLORS.slate950,
|
||||
'font-family': 'Inter, sans-serif',
|
||||
'font-size': '18px',
|
||||
'font-weight': '700',
|
||||
'line-height': '24px',
|
||||
'letter-spacing': '0.2px',
|
||||
'text-align': 'center',
|
||||
opacity: '0.5',
|
||||
'text-shadow': `0px 1px 2px 0px ${DOM_CSS_COLORS.slateShadow}`
|
||||
},
|
||||
children: [{ type: 'text', text: 'Round trip' }]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
function expectStyleRoundTripPanel(graph: SceneGraph) {
|
||||
const page = graph.getPages()[0]
|
||||
const panel = expectFrame(page ? graph.getChildren(page.id)[0] : undefined)
|
||||
expect(panel.paddingTop).toBe(8)
|
||||
expect(panel.paddingRight).toBe(12)
|
||||
expect(panel.borderTopWeight).toBe(0)
|
||||
expect(panel.borderRightWeight).toBe(2)
|
||||
expect(panel.borderLeftWeight).toBe(4)
|
||||
expect(panel.opacity).toBe(0.75)
|
||||
expect(panel.effects[0]?.type).toBe('DROP_SHADOW')
|
||||
return panel
|
||||
}
|
||||
|
||||
function expectStyleRoundTripText(graph: SceneGraph, panel: SceneNode) {
|
||||
const heading = graph.getChildren(panel.id)[0]
|
||||
expect(heading?.type).toBe('TEXT')
|
||||
if (heading?.type !== 'TEXT') throw new Error('Expected text node')
|
||||
expect(heading.fontSize).toBe(18)
|
||||
expect(heading.fontWeight).toBe(700)
|
||||
expect(heading.lineHeight).toBe(24)
|
||||
expect(heading.letterSpacing).toBe(0.2)
|
||||
expect(heading.textAlignHorizontal).toBe('CENTER')
|
||||
expect(heading.opacity).toBe(0.5)
|
||||
expect(heading.effects[0]?.type).toBe('DROP_SHADOW')
|
||||
}
|
||||
|
||||
function expectRoundTripPanelStyle(element: DesignElement) {
|
||||
expect(element.inlineStyle?.['padding-block']).toBe('8px')
|
||||
expect(element.inlineStyle?.['padding-inline']).toBe('12px')
|
||||
expect(element.inlineStyle?.['border-top-width']).toBe('0px')
|
||||
expect(element.inlineStyle?.['border-left-width']).toBe('4px')
|
||||
expect(element.inlineStyle?.opacity).toBe('0.75')
|
||||
expect(element.inlineStyle?.['box-shadow']).toContain('24px')
|
||||
}
|
||||
|
||||
function expectRoundTripHeadingStyle(element: DesignElement) {
|
||||
expect(element.inlineStyle?.['font-size']).toBe('18px')
|
||||
expect(element.inlineStyle?.['font-weight']).toBe('700')
|
||||
expect(element.inlineStyle?.['line-height']).toBe('24px')
|
||||
expect(element.inlineStyle?.['letter-spacing']).toBe('0.2px')
|
||||
expect(element.inlineStyle?.['text-align']).toBe('center')
|
||||
expect(element.inlineStyle?.opacity).toBe('0.5')
|
||||
expect(element.inlineStyle?.['text-shadow']).toContain('2px')
|
||||
}
|
||||
|
||||
function expectStyleRoundTripHTML(graph: SceneGraph) {
|
||||
const roundTrip = sceneGraphToDesignDocument(graph)
|
||||
const root = roundTrip.children[0]
|
||||
expect(root?.type).toBe('element')
|
||||
if (root?.type !== 'element') throw new Error('Expected root element')
|
||||
const roundTripPanel = root.children[0]
|
||||
expect(roundTripPanel?.type).toBe('element')
|
||||
if (roundTripPanel?.type !== 'element') throw new Error('Expected panel element')
|
||||
expectRoundTripPanelStyle(roundTripPanel)
|
||||
|
||||
const roundTripHeading = roundTripPanel.children[0]
|
||||
expect(roundTripHeading?.type).toBe('element')
|
||||
if (roundTripHeading?.type !== 'element') throw new Error('Expected heading element')
|
||||
expectRoundTripHeadingStyle(roundTripHeading)
|
||||
}
|
||||
|
||||
describe('@open-pencil/dom-css conversion', () => {
|
||||
it('converts HTML and CSS to DesignDOM with one API call', async () => {
|
||||
const document = await htmlToDesignDocument(cssCardHTML, {
|
||||
|
|
@ -188,10 +298,18 @@ describe('@open-pencil/dom-css conversion', () => {
|
|||
if (roundTripPanel?.type !== 'element') return
|
||||
expect(roundTripPanel.inlineStyle?.overflow).toBe('hidden')
|
||||
expect(roundTripPanel.inlineStyle?.['min-width']).toBe('240px')
|
||||
expect(roundTripPanel.inlineStyle?.['border-top-width']).toBe('1px')
|
||||
expect(roundTripPanel.inlineStyle?.['border-left-width']).toBe('4px')
|
||||
expect(roundTripPanel.inlineStyle?.['border-bottom-left-radius']).toBe('16px')
|
||||
})
|
||||
|
||||
it('round-trips logical padding, opacity, and text shadows', () => {
|
||||
const graph = createStyleRoundTripGraph()
|
||||
const panel = expectStyleRoundTripPanel(graph)
|
||||
expectStyleRoundTripText(graph, panel)
|
||||
expectStyleRoundTripHTML(graph)
|
||||
})
|
||||
|
||||
it('maps CSS flex alignment into scene graph auto-layout alignment', () => {
|
||||
const graph = designDocumentToSceneGraph({
|
||||
type: 'document',
|
||||
|
|
|
|||
Loading…
Reference in a new issue