fix(ai): render common generated JSX aliases

This commit is contained in:
Danila Poyarkov 2026-05-24 12:45:42 +03:00
parent 227ee97448
commit afd571341b
4 changed files with 115 additions and 16 deletions

View file

@ -70,6 +70,40 @@ function parseStroke(value: string, width: number): Stroke {
}
}
function numberFromPx(value: unknown): number | undefined {
if (typeof value === 'number') return value
if (typeof value !== 'string') return undefined
const trimmed = value.trim()
if (!trimmed.endsWith('px')) return undefined
const parsed = Number.parseFloat(trimmed.slice(0, -2))
return Number.isFinite(parsed) ? parsed : undefined
}
function normalizeStyleProps(props: Record<string, unknown>): Record<string, unknown> {
const style = props.style
if (style === null || typeof style !== 'object' || Array.isArray(style)) return props
const source = style as Record<string, unknown>
const normalized = { ...props }
const copyIfUnset = (from: string, to: string, convert?: (value: unknown) => unknown): void => {
if (normalized[to] !== undefined || source[from] === undefined) return
normalized[to] = convert ? convert(source[from]) : source[from]
}
copyIfUnset('background', 'bg')
copyIfUnset('backgroundColor', 'bg')
copyIfUnset('color', 'color')
copyIfUnset('borderColor', 'stroke')
copyIfUnset('borderWidth', 'strokeWidth', numberFromPx)
copyIfUnset('borderRadius', 'rounded', numberFromPx)
copyIfUnset('fontSize', 'fontSize', numberFromPx)
copyIfUnset('fontWeight', 'fontWeight')
copyIfUnset('width', 'width', numberFromPx)
copyIfUnset('height', 'height', numberFromPx)
copyIfUnset('opacity', 'opacity')
return normalized
}
export function applySizeOverrides(
props: Record<string, unknown>,
o: Partial<SceneNode>,
@ -124,21 +158,23 @@ function applyFillSizing(
}
}
function applyVisualOverrides(props: Record<string, unknown>, o: Partial<SceneNode>): void {
const bg = props.bg ?? props.fill
if (typeof bg === 'string') {
o.fills = [colorToFill(bg)]
}
function applyFillOverride(props: Record<string, unknown>, o: Partial<SceneNode>): void {
const bg = props.bg ?? props.fill ?? props.background ?? props.backgroundColor
if (typeof bg === 'string') o.fills = [colorToFill(bg)]
}
if (typeof props.stroke === 'string') {
const strokeWidth = (props.strokeWidth as number | undefined) ?? 1
o.strokes = [parseStroke(props.stroke, strokeWidth)]
}
function applyStrokeOverride(props: Record<string, unknown>, o: Partial<SceneNode>): void {
const stroke = props.stroke ?? props.border ?? props.borderColor
if (typeof stroke !== 'string') return
const strokeWidth =
(props.strokeWidth as number | undefined) ?? (props.borderWidth as number | undefined) ?? 1
o.strokes = [parseStroke(stroke, strokeWidth)]
}
function applyCornerOverrides(props: Record<string, unknown>, o: Partial<SceneNode>): void {
const rounded = props.rounded ?? props.cornerRadius ?? props.borderRadius
if (typeof rounded === 'number') o.cornerRadius = rounded
const rounded = props.rounded ?? props.cornerRadius
if (typeof rounded === 'number') {
o.cornerRadius = rounded
}
if (
props.roundedTL !== undefined ||
props.roundedTR !== undefined ||
@ -151,7 +187,14 @@ function applyVisualOverrides(props: Record<string, unknown>, o: Partial<SceneNo
if (props.roundedBL !== undefined) o.bottomLeftRadius = props.roundedBL as number
if (props.roundedBR !== undefined) o.bottomRightRadius = props.roundedBR as number
}
if (props.cornerSmoothing !== undefined) o.cornerSmoothing = props.cornerSmoothing as number
}
function applyVisualOverrides(props: Record<string, unknown>, o: Partial<SceneNode>): void {
applyFillOverride(props, o)
applyStrokeOverride(props, o)
applyCornerOverrides(props, o)
if (props.opacity !== undefined) o.opacity = props.opacity as number
applyTransformOverrides(props, o)
@ -483,6 +526,7 @@ export function propsToOverrides(
isText: boolean,
parentLayout: SceneNode['layoutMode']
): Partial<SceneNode> {
props = normalizeStyleProps(props)
const o: Partial<SceneNode> = {}
if (props.name) o.name = props.name as string

View file

@ -49,11 +49,17 @@ const SUPPORTED_PROPS = new Set([
'pl',
'bg',
'fill',
'background',
'backgroundColor',
'stroke',
'border',
'borderColor',
'strokeWidth',
'borderWidth',
'strokeAlign',
'strokeDash',
'rounded',
'borderRadius',
'roundedTL',
'roundedTR',
'roundedBL',
@ -76,6 +82,9 @@ const SUPPORTED_PROPS = new Set([
'color',
'text',
'characters',
'content',
'value',
'title',
'textAlign',
'textAlignHorizontal',
'textHorizontalAlignment',
@ -100,7 +109,8 @@ const SUPPORTED_PROPS = new Set([
'points',
'pointCount',
'innerRadius',
'label'
'label',
'style'
])
function stripHtmlComments(jsxString: string): string {

View file

@ -21,7 +21,22 @@ const TYPE_MAP: Partial<Record<string, NodeType>> = {
vector: 'VECTOR',
group: 'GROUP',
section: 'SECTION',
component: 'COMPONENT'
component: 'COMPONENT',
div: 'FRAME',
main: 'FRAME',
header: 'FRAME',
footer: 'FRAME',
nav: 'FRAME',
article: 'FRAME',
aside: 'FRAME',
span: 'TEXT',
p: 'TEXT',
h1: 'TEXT',
h2: 'TEXT',
h3: 'TEXT',
h4: 'TEXT',
h5: 'TEXT',
h6: 'TEXT'
}
interface RenderOptions {
@ -104,7 +119,13 @@ async function renderNode(graph: SceneGraph, tree: TreeNode, parentId: string):
if (isText) {
const childText = tree.children.filter((c): c is string => typeof c === 'string').join('')
const propText = tree.props.text ?? tree.props.characters
const propText =
tree.props.text ??
tree.props.characters ??
tree.props.content ??
tree.props.label ??
tree.props.value ??
tree.props.title
if (childText) overrides.text = childText
else if (typeof propText === 'string') overrides.text = propText
}

View file

@ -52,6 +52,30 @@ describe('renderTree', () => {
expect(node.fills.length).toBe(1)
})
it('renders common React aliases that models often produce', async () => {
const g = makeSceneGraph()
const result = await renderJSX(
g,
`<div name="Card" style={{ width: 320, height: 120, backgroundColor: '#2563EB', borderRadius: 16 }}>
<Text name="Heading" content="Visible content" style={{ color: '#FFFFFF', fontSize: 20, fontWeight: 700 }} />
</div>`
)
const card = getNodeOrThrow(g, result[0].id)
expect(card.type).toBe('FRAME')
expect(card.width).toBe(320)
expect(card.height).toBe(120)
expect(card.cornerRadius).toBe(16)
expect(card.fills.length).toBe(1)
const heading = getNodeOrThrow(g, childIdAt(card, 0))
expect(heading.type).toBe('TEXT')
expect(heading.text).toBe('Visible content')
expect(heading.fontSize).toBe(20)
expect(heading.fontWeight).toBe(700)
expect(heading.fills.length).toBe(1)
})
it('renders nested structure', async () => {
const g = makeSceneGraph()
const tree = Frame({