diff --git a/packages/core/src/design-jsx/props-overrides.ts b/packages/core/src/design-jsx/props-overrides.ts index 3db9bdd73..9073d44b4 100644 --- a/packages/core/src/design-jsx/props-overrides.ts +++ b/packages/core/src/design-jsx/props-overrides.ts @@ -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): Record { + const style = props.style + if (style === null || typeof style !== 'object' || Array.isArray(style)) return props + + const source = style as Record + 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, o: Partial, @@ -124,21 +158,23 @@ function applyFillSizing( } } -function applyVisualOverrides(props: Record, o: Partial): void { - const bg = props.bg ?? props.fill - if (typeof bg === 'string') { - o.fills = [colorToFill(bg)] - } +function applyFillOverride(props: Record, o: Partial): 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, o: Partial): 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, o: Partial): 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, o: Partial, o: Partial): 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 { + props = normalizeStyleProps(props) const o: Partial = {} if (props.name) o.name = props.name as string diff --git a/packages/core/src/design-jsx/render.ts b/packages/core/src/design-jsx/render.ts index 36206ff71..eaf8cf8fe 100644 --- a/packages/core/src/design-jsx/render.ts +++ b/packages/core/src/design-jsx/render.ts @@ -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 { diff --git a/packages/core/src/design-jsx/renderer.ts b/packages/core/src/design-jsx/renderer.ts index 6db214d31..4fae3683a 100644 --- a/packages/core/src/design-jsx/renderer.ts +++ b/packages/core/src/design-jsx/renderer.ts @@ -21,7 +21,22 @@ const TYPE_MAP: Partial> = { 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 } diff --git a/tests/engine/render/jsx/render-tree.test.ts b/tests/engine/render/jsx/render-tree.test.ts index cbf669a2f..312ead19f 100644 --- a/tests/engine/render/jsx/render-tree.test.ts +++ b/tests/engine/render/jsx/render-tree.test.ts @@ -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, + `
+ +
` + ) + + 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({