fix(ai): render common generated JSX aliases
This commit is contained in:
parent
227ee97448
commit
afd571341b
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
Loading…
Reference in a new issue