fix(mcp): improve agent-rendered JSX
This commit is contained in:
parent
fed6fa620a
commit
779e0e493f
|
|
@ -29,6 +29,19 @@ const TEXT_ALIGN_MAP: Record<string, SceneNode['textAlignHorizontal']> = {
|
|||
justified: 'JUSTIFIED'
|
||||
}
|
||||
|
||||
const TEXT_VERTICAL_ALIGN_MAP: Record<string, SceneNode['textAlignVertical']> = {
|
||||
top: 'TOP',
|
||||
center: 'CENTER',
|
||||
bottom: 'BOTTOM'
|
||||
}
|
||||
|
||||
const TEXT_ALIGN_ALIAS_MAP: Record<string, SceneNode['textAlignHorizontal']> = {
|
||||
...TEXT_ALIGN_MAP,
|
||||
left_align: 'LEFT',
|
||||
center_align: 'CENTER',
|
||||
right_align: 'RIGHT'
|
||||
}
|
||||
|
||||
const TEXT_AUTO_RESIZE_MAP: Record<string, SceneNode['textAutoResize']> = {
|
||||
none: 'NONE',
|
||||
width: 'WIDTH_AND_HEIGHT',
|
||||
|
|
@ -141,13 +154,18 @@ function applyVisualOverrides(props: Record<string, unknown>, o: Partial<SceneNo
|
|||
if (props.cornerSmoothing !== undefined) o.cornerSmoothing = props.cornerSmoothing as number
|
||||
|
||||
if (props.opacity !== undefined) o.opacity = props.opacity as number
|
||||
if (props.rotate !== undefined) o.rotation = props.rotate as number
|
||||
applyTransformOverrides(props, o)
|
||||
if (props.blendMode !== undefined) {
|
||||
o.blendMode = (props.blendMode as string).toUpperCase() as SceneNode['blendMode']
|
||||
}
|
||||
if (props.overflow === 'hidden') o.clipsContent = true
|
||||
}
|
||||
|
||||
function applyTransformOverrides(props: Record<string, unknown>, o: Partial<SceneNode>): void {
|
||||
const rotation = props.rotate ?? props.rotation
|
||||
if (rotation !== undefined) o.rotation = rotation as number
|
||||
}
|
||||
|
||||
function applyPaddingOverrides(props: Record<string, unknown>, o: Partial<SceneNode>): void {
|
||||
const p = props.p ?? props.padding
|
||||
if (typeof p === 'number') {
|
||||
|
|
@ -173,7 +191,14 @@ function applyPaddingOverrides(props: Record<string, unknown>, o: Partial<SceneN
|
|||
}
|
||||
|
||||
const PADDING_KEYS = ['p', 'padding', 'px', 'py', 'pt', 'pr', 'pb', 'pl'] as const
|
||||
const AUTO_LAYOUT_TRIGGER_KEYS = [...PADDING_KEYS, 'justify', 'items'] as const
|
||||
const AUTO_LAYOUT_TRIGGER_KEYS = [
|
||||
...PADDING_KEYS,
|
||||
'justify',
|
||||
'justifyContent',
|
||||
'items',
|
||||
'align',
|
||||
'alignItems'
|
||||
] as const
|
||||
|
||||
function hasAutoLayoutTriggerProps(props: Record<string, unknown>): boolean {
|
||||
return AUTO_LAYOUT_TRIGGER_KEYS.some((k) => props[k] !== undefined)
|
||||
|
|
@ -272,6 +297,20 @@ function applyAutoLayoutSizing(
|
|||
if (counterDim === 'hug') o.counterAxisSizing = 'HUG'
|
||||
}
|
||||
|
||||
function applyLayoutAlignmentOverrides(
|
||||
props: Record<string, unknown>,
|
||||
o: Partial<SceneNode>
|
||||
): void {
|
||||
const justify = props.justify ?? props.justifyContent
|
||||
if (justify) {
|
||||
o.primaryAxisAlign = ALIGN_MAP[justify as string] ?? 'MIN'
|
||||
}
|
||||
const items = props.items ?? props.align ?? props.alignItems
|
||||
if (items) {
|
||||
o.counterAxisAlign = COUNTER_ALIGN_MAP[items as string] ?? 'MIN'
|
||||
}
|
||||
}
|
||||
|
||||
function shouldEnableAutoLayout(props: Record<string, unknown>, isText: boolean): boolean {
|
||||
if (props.flex !== undefined) return true
|
||||
if (!isText && hasAutoLayoutTriggerProps(props)) return true
|
||||
|
|
@ -311,12 +350,7 @@ function applyLayoutOverrides(
|
|||
if (props.rowGap !== undefined) o.counterAxisSpacing = props.rowGap as number
|
||||
}
|
||||
|
||||
if (props.justify) {
|
||||
o.primaryAxisAlign = ALIGN_MAP[props.justify as string] ?? 'MIN'
|
||||
}
|
||||
if (props.items) {
|
||||
o.counterAxisAlign = COUNTER_ALIGN_MAP[props.items as string] ?? 'MIN'
|
||||
}
|
||||
applyLayoutAlignmentOverrides(props, o)
|
||||
|
||||
applyPaddingOverrides(props, o)
|
||||
|
||||
|
|
@ -358,8 +392,21 @@ function applyTextStyleOverrides(props: Record<string, unknown>, o: Partial<Scen
|
|||
o.textTruncation = 'ENDING'
|
||||
}
|
||||
|
||||
if (props.textAlign) {
|
||||
o.textAlignHorizontal = TEXT_ALIGN_MAP[props.textAlign as string] ?? 'LEFT'
|
||||
applyTextAlignmentOverrides(props, o)
|
||||
}
|
||||
|
||||
function applyTextAlignmentOverrides(
|
||||
props: Record<string, unknown>,
|
||||
o: Partial<SceneNode>
|
||||
): void {
|
||||
const textAlign = props.textAlign ?? props.textAlignHorizontal ?? props.textHorizontalAlignment
|
||||
if (typeof textAlign === 'string') {
|
||||
o.textAlignHorizontal = TEXT_ALIGN_ALIAS_MAP[textAlign.toLowerCase()] ?? 'LEFT'
|
||||
}
|
||||
|
||||
const textAlignVertical = props.textAlignVertical ?? props.textVerticalAlignment
|
||||
if (typeof textAlignVertical === 'string') {
|
||||
o.textAlignVertical = TEXT_VERTICAL_ALIGN_MAP[textAlignVertical.toLowerCase()] ?? 'TOP'
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,14 +4,128 @@ import type { SceneGraph } from '#core/scene-graph'
|
|||
|
||||
import * as React from './mini-react'
|
||||
import { renderTree, type RenderResult } from './renderer'
|
||||
import { resolveToTree } from './tree'
|
||||
import { isTreeNode, resolveToTree, type TreeNode } from './tree'
|
||||
|
||||
/**
|
||||
* Build a component function from a JSX string using sucrase.
|
||||
* Works in both Node/Bun and the browser (no native bindings).
|
||||
*/
|
||||
const SUPPORTED_PROPS = new Set([
|
||||
'name',
|
||||
'key',
|
||||
'flex',
|
||||
'flow',
|
||||
'dir',
|
||||
'gap',
|
||||
'wrap',
|
||||
'rowGap',
|
||||
'columnGap',
|
||||
'justify',
|
||||
'justifyContent',
|
||||
'items',
|
||||
'align',
|
||||
'alignItems',
|
||||
'grow',
|
||||
'w',
|
||||
'h',
|
||||
'width',
|
||||
'height',
|
||||
'minW',
|
||||
'maxW',
|
||||
'minH',
|
||||
'maxH',
|
||||
'x',
|
||||
'y',
|
||||
'top',
|
||||
'left',
|
||||
'position',
|
||||
'p',
|
||||
'padding',
|
||||
'px',
|
||||
'py',
|
||||
'pt',
|
||||
'pr',
|
||||
'pb',
|
||||
'pl',
|
||||
'bg',
|
||||
'fill',
|
||||
'stroke',
|
||||
'strokeWidth',
|
||||
'strokeAlign',
|
||||
'strokeDash',
|
||||
'rounded',
|
||||
'roundedTL',
|
||||
'roundedTR',
|
||||
'roundedBL',
|
||||
'roundedBR',
|
||||
'cornerRadius',
|
||||
'cornerSmoothing',
|
||||
'opacity',
|
||||
'blendMode',
|
||||
'rotate',
|
||||
'rotation',
|
||||
'overflow',
|
||||
'shadow',
|
||||
'blur',
|
||||
'size',
|
||||
'fontSize',
|
||||
'font',
|
||||
'fontFamily',
|
||||
'weight',
|
||||
'fontWeight',
|
||||
'color',
|
||||
'text',
|
||||
'characters',
|
||||
'textAlign',
|
||||
'textAlignHorizontal',
|
||||
'textHorizontalAlignment',
|
||||
'textAlignVertical',
|
||||
'textVerticalAlignment',
|
||||
'textAutoResize',
|
||||
'lineHeight',
|
||||
'letterSpacing',
|
||||
'textDecoration',
|
||||
'textCase',
|
||||
'maxLines',
|
||||
'truncate',
|
||||
'grid',
|
||||
'columns',
|
||||
'rows',
|
||||
'colStart',
|
||||
'rowStart',
|
||||
'col',
|
||||
'row',
|
||||
'colSpan',
|
||||
'rowSpan',
|
||||
'points',
|
||||
'pointCount',
|
||||
'innerRadius',
|
||||
'label'
|
||||
])
|
||||
|
||||
function stripHtmlComments(jsxString: string): string {
|
||||
return jsxString.replace(/<!--[\s\S]*?-->/g, '')
|
||||
}
|
||||
|
||||
function unsupportedPropWarnings(tree: TreeNode): string[] {
|
||||
const warnings: string[] = []
|
||||
collectUnsupportedPropWarnings(tree, warnings)
|
||||
return warnings
|
||||
}
|
||||
|
||||
function collectUnsupportedPropWarnings(tree: TreeNode, warnings: string[]): void {
|
||||
for (const key of Object.keys(tree.props)) {
|
||||
if (!SUPPORTED_PROPS.has(key)) {
|
||||
warnings.push(`Unsupported prop "${key}" on <${tree.type}> is ignored.`)
|
||||
}
|
||||
}
|
||||
for (const child of tree.children) {
|
||||
if (isTreeNode(child)) collectUnsupportedPropWarnings(child, warnings)
|
||||
}
|
||||
}
|
||||
|
||||
export function buildComponent(jsxString: string): React.ComponentType {
|
||||
const trimmed = jsxString.trim()
|
||||
const trimmed = stripHtmlComments(jsxString).trim()
|
||||
|
||||
const aliases = `
|
||||
const __h = React.createElement
|
||||
|
|
@ -63,6 +177,8 @@ export async function renderJSX(
|
|||
throw new Error('JSX must return a Figma element (Frame, Text, etc)')
|
||||
}
|
||||
|
||||
const warnings = unsupportedPropWarnings(tree)
|
||||
|
||||
if (tree.type === '' && tree.children.length > 0) {
|
||||
const results: RenderResult[] = []
|
||||
for (const child of tree.children) {
|
||||
|
|
@ -72,10 +188,13 @@ export async function renderJSX(
|
|||
if (results.length === 0) {
|
||||
throw new Error('JSX must return a Figma element (Frame, Text, etc)')
|
||||
}
|
||||
if (warnings.length > 0) results[0].warnings = warnings
|
||||
return results
|
||||
}
|
||||
|
||||
return [await renderTree(graph, tree, options)]
|
||||
const result = await renderTree(graph, tree, options)
|
||||
if (warnings.length > 0) result.warnings = warnings
|
||||
return [result]
|
||||
}
|
||||
|
||||
export { renderTree as renderTreeNode }
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ export interface RenderResult {
|
|||
name: string
|
||||
type: NodeType
|
||||
childIds: string[]
|
||||
warnings?: string[]
|
||||
}
|
||||
|
||||
export async function renderTree(
|
||||
|
|
@ -102,8 +103,10 @@ async function renderNode(graph: SceneGraph, tree: TreeNode, parentId: string):
|
|||
const overrides = propsToOverrides(tree.props, isText, parentLayout)
|
||||
|
||||
if (isText) {
|
||||
const textContent = tree.children.filter((c): c is string => typeof c === 'string').join('')
|
||||
if (textContent) overrides.text = textContent
|
||||
const childText = tree.children.filter((c): c is string => typeof c === 'string').join('')
|
||||
const propText = tree.props.text ?? tree.props.characters
|
||||
if (childText) overrides.text = childText
|
||||
else if (typeof propText === 'string') overrides.text = propText
|
||||
}
|
||||
|
||||
const node = graph.createNode(nodeType, parentId, overrides)
|
||||
|
|
|
|||
|
|
@ -89,7 +89,10 @@ export type StyleProps = {
|
|||
wrap?: boolean
|
||||
rowGap?: number
|
||||
justify?: 'start' | 'end' | 'center' | 'between'
|
||||
justifyContent?: 'start' | 'end' | 'center' | 'between'
|
||||
items?: 'start' | 'end' | 'center' | 'stretch'
|
||||
align?: 'start' | 'end' | 'center' | 'stretch'
|
||||
alignItems?: 'start' | 'end' | 'center' | 'stretch'
|
||||
grow?: number
|
||||
|
||||
w?: number | 'fill' | 'hug'
|
||||
|
|
@ -125,6 +128,7 @@ export type StyleProps = {
|
|||
opacity?: number
|
||||
blendMode?: string
|
||||
rotate?: number
|
||||
rotation?: number
|
||||
overflow?: 'hidden' | 'visible'
|
||||
shadow?: string
|
||||
blur?: number
|
||||
|
|
@ -136,7 +140,13 @@ export type StyleProps = {
|
|||
weight?: number | 'bold' | 'medium' | 'normal'
|
||||
fontWeight?: number | 'bold' | 'medium' | 'normal'
|
||||
color?: string
|
||||
text?: string
|
||||
characters?: string
|
||||
textAlign?: 'left' | 'center' | 'right' | 'justified'
|
||||
textAlignHorizontal?: 'LEFT' | 'CENTER' | 'RIGHT' | 'JUSTIFIED'
|
||||
textHorizontalAlignment?: 'LEFT' | 'CENTER' | 'RIGHT' | 'JUSTIFIED'
|
||||
textAlignVertical?: 'TOP' | 'CENTER' | 'BOTTOM'
|
||||
textVerticalAlignment?: 'TOP' | 'CENTER' | 'BOTTOM'
|
||||
textAutoResize?: 'none' | 'width' | 'height'
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -28,7 +28,21 @@ export function nodeProxyToJSON(
|
|||
if (n.cornerRadius > 0) obj.cornerRadius = n.cornerRadius
|
||||
if (!n.visible) obj.visible = false
|
||||
if (n.text) obj.characters = n.text
|
||||
if (n.type === 'TEXT') obj.textDirection = n.textDirection
|
||||
if (n.type === 'TEXT') {
|
||||
obj.fontFamily = n.fontFamily
|
||||
obj.fontSize = n.fontSize
|
||||
obj.fontWeight = n.fontWeight
|
||||
obj.italic = n.italic
|
||||
obj.textAlignHorizontal = n.textAlignHorizontal
|
||||
obj.textAlignVertical = n.textAlignVertical
|
||||
obj.textAutoResize = n.textAutoResize
|
||||
obj.textDirection = n.textDirection
|
||||
if (n.lineHeight != null) obj.lineHeight = n.lineHeight
|
||||
if (n.letterSpacing !== 0) obj.letterSpacing = n.letterSpacing
|
||||
if (n.textCase !== 'ORIGINAL') obj.textCase = n.textCase
|
||||
if (n.textDecoration !== 'NONE') obj.textDecoration = n.textDecoration
|
||||
if (n.maxLines != null) obj.maxLines = n.maxLines
|
||||
}
|
||||
if (n.layoutMode !== 'NONE') {
|
||||
obj.layoutMode = n.layoutMode
|
||||
obj.layoutDirection = n.layoutDirection
|
||||
|
|
|
|||
|
|
@ -55,6 +55,7 @@ export const render = defineTool({
|
|||
name: result.name,
|
||||
type: result.type,
|
||||
children: result.childIds,
|
||||
...(result.warnings ? { warnings: result.warnings } : {}),
|
||||
...(results.length > 1
|
||||
? {
|
||||
siblings: results
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ export const nodeReplaceWith = defineTool({
|
|||
id: result.id,
|
||||
name: result.name,
|
||||
type: result.type,
|
||||
...(result.warnings ? { warnings: result.warnings } : {}),
|
||||
children: results
|
||||
.slice(1)
|
||||
.map((child) => ({ id: child.id, name: child.name, type: child.type }))
|
||||
|
|
|
|||
|
|
@ -4,12 +4,12 @@ import * as v from 'valibot'
|
|||
|
||||
import { computeAllLayouts } from '@open-pencil/core/layout'
|
||||
import type { SceneNode } from '@open-pencil/core/scene-graph'
|
||||
import { fontManager } from '@open-pencil/core/text'
|
||||
import { CORE_TOOLS, toolsToAI } from '@open-pencil/core/tools'
|
||||
import type { StepBudget, ToolLogEntry } from '@open-pencil/core/tools'
|
||||
|
||||
import { makeFigmaFromStore } from '@/app/automation/bridge/figma-factory'
|
||||
import { getActiveEditorStore } from '@/app/editor/active-store'
|
||||
import { ensureGraphFonts } from '@/app/editor/fonts'
|
||||
import type { EditorStore } from '@/app/editor/active-store'
|
||||
|
||||
export const MAX_AGENT_STEPS = 50
|
||||
|
|
@ -99,20 +99,7 @@ export function createAITools(store: EditorStore) {
|
|||
if (def.mutates) {
|
||||
const pageId = store.state.currentPageId
|
||||
const pageNode = store.graph.getNode(pageId)
|
||||
if (pageNode) {
|
||||
const fontKeys = fontManager.collectFontKeys(store.graph, pageNode.childIds)
|
||||
const missing = fontKeys.filter(([family]) => !fontManager.isLoaded(family))
|
||||
if (missing.length > 0) {
|
||||
const results = await Promise.all(
|
||||
missing.map(([family, style]) => fontManager.loadFont(family, style))
|
||||
)
|
||||
if (results.some((r) => r !== null)) {
|
||||
for (const [, node] of store.graph.nodes) {
|
||||
if (node.type === 'TEXT' && node.textPicture) node.textPicture = null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (pageNode) await ensureGraphFonts(store.graph, pageNode.childIds)
|
||||
computeAllLayouts(store.graph, pageId)
|
||||
store.requestRender()
|
||||
if (beforeSnapshot) {
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { computeAllLayouts } from '@open-pencil/core/layout'
|
|||
import { ALL_TOOLS } from '@open-pencil/core/tools'
|
||||
|
||||
import type { EditorStore } from '@/app/editor/active-store'
|
||||
import { ensureGraphFonts } from '@/app/editor/fonts'
|
||||
|
||||
type FigmaFactory = () => FigmaAPI
|
||||
|
||||
|
|
@ -18,6 +19,7 @@ export function createAutomationToolHandler(makeFigma: FigmaFactory) {
|
|||
x: toolArgs.x as number | undefined,
|
||||
y: toolArgs.y as number | undefined
|
||||
})
|
||||
await ensureGraphFonts(store.graph, [result.id])
|
||||
computeAllLayouts(store.graph, store.state.currentPageId)
|
||||
store.requestRender()
|
||||
store.flashNodes([result.id])
|
||||
|
|
@ -46,6 +48,8 @@ export function createAutomationToolHandler(makeFigma: FigmaFactory) {
|
|||
}
|
||||
|
||||
if (def.mutates) {
|
||||
const pageNode = store.graph.getNode(store.state.currentPageId)
|
||||
if (pageNode) await ensureGraphFonts(store.graph, pageNode.childIds)
|
||||
computeAllLayouts(store.graph, store.state.currentPageId)
|
||||
store.requestRender()
|
||||
store.flashNodes(extractNodeIds(result))
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { useLocalStorage } from '@vueuse/core'
|
||||
|
||||
import type { SceneGraph } from '@open-pencil/core/scene-graph'
|
||||
import {
|
||||
fontManager,
|
||||
styleToWeight,
|
||||
|
|
@ -113,6 +114,23 @@ export async function listFonts(): Promise<TauriFontFamily[]> {
|
|||
return []
|
||||
}
|
||||
|
||||
export async function ensureGraphFonts(graph: SceneGraph, nodeIds: string[]): Promise<boolean> {
|
||||
const fontKeys = fontManager.collectFontKeys(graph, nodeIds)
|
||||
const missing = fontKeys.filter(([family, style]) => !fontManager.isStyleLoaded(family, style))
|
||||
if (missing.length === 0) return false
|
||||
|
||||
const results = await Promise.all(missing.map(([family, style]) => loadFont(family, style)))
|
||||
const loaded = results.some((result) => result !== null)
|
||||
if (loaded) clearTextPictures(graph)
|
||||
return loaded
|
||||
}
|
||||
|
||||
function clearTextPictures(graph: SceneGraph): void {
|
||||
for (const [, node] of graph.nodes) {
|
||||
if (node.type === 'TEXT') node.textPicture = null
|
||||
}
|
||||
}
|
||||
|
||||
export async function loadFont(family: string, style = 'Regular'): Promise<ArrayBuffer | null> {
|
||||
configureTauriFontCache()
|
||||
if (isTauri()) {
|
||||
|
|
|
|||
|
|
@ -322,4 +322,43 @@ describe('renderJSX (string → scene graph)', () => {
|
|||
expect(node.x).toBe(100)
|
||||
expect(node.y).toBe(200)
|
||||
})
|
||||
|
||||
it('accepts rotation as an alias for rotate', async () => {
|
||||
const g = makeSceneGraph()
|
||||
const [result] = await renderJSX(g, '<Rectangle name="Rotated" w={50} h={50} rotation={15} />')
|
||||
const node = getNodeOrThrow(g, result.id)
|
||||
|
||||
expect(node.rotation).toBe(15)
|
||||
})
|
||||
|
||||
it('strips HTML comments before JSX parsing', async () => {
|
||||
const g = makeSceneGraph()
|
||||
const [result] = await renderJSX(
|
||||
g,
|
||||
'<Frame name="Comments" w={50} h={50}><!-- generated note --><Text color="#000">Ok</Text></Frame>'
|
||||
)
|
||||
const node = getNodeOrThrow(g, result.id)
|
||||
|
||||
expect(node.name).toBe('Comments')
|
||||
expect(node.childIds.length).toBe(1)
|
||||
})
|
||||
|
||||
it('warns about unsupported props', async () => {
|
||||
const g = makeSceneGraph()
|
||||
const [result] = await renderJSX(g, '<Frame name="Warn" w={50} h={50} mt={8} />')
|
||||
|
||||
expect(result.warnings).toEqual(['Unsupported prop "mt" on <frame> is ignored.'])
|
||||
})
|
||||
|
||||
it('accepts CSS-style layout aliases', async () => {
|
||||
const g = makeSceneGraph()
|
||||
const [result] = await renderJSX(
|
||||
g,
|
||||
'<Frame name="Aliases" w={200} h={100} flex="row" justifyContent="center" alignItems="center"><Rectangle w={20} h={20} /></Frame>'
|
||||
)
|
||||
const node = getNodeOrThrow(g, result.id)
|
||||
|
||||
expect(node.primaryAxisAlign).toBe('CENTER')
|
||||
expect(node.counterAxisAlign).toBe('CENTER')
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -61,6 +61,36 @@ describe('text props round-trip', () => {
|
|||
expect(jsx).toContain('truncate')
|
||||
})
|
||||
|
||||
it('accepts Figma-style text aliases', async () => {
|
||||
const g = makeSceneGraph()
|
||||
const [result] = await renderJSX(
|
||||
g,
|
||||
'<Text text="Alias text" fontSize={24} fontFamily="Inter" fontWeight={700} fill="#123456" textHorizontalAlignment="CENTER" textVerticalAlignment="CENTER" />'
|
||||
)
|
||||
const n = getNodeOrThrow(g, result.id)
|
||||
expect(n.text).toBe('Alias text')
|
||||
expect(n.fontSize).toBe(24)
|
||||
expect(n.fontFamily).toBe('Inter')
|
||||
expect(n.fontWeight).toBe(700)
|
||||
expect(n.textAlignHorizontal).toBe('CENTER')
|
||||
expect(n.textAlignVertical).toBe('CENTER')
|
||||
expect(n.fills[0]?.type).toBe('SOLID')
|
||||
})
|
||||
|
||||
it('accepts characters as a text content alias', async () => {
|
||||
const g = makeSceneGraph()
|
||||
const [result] = await renderJSX(g, '<Text characters="Characters text" color="#000" />')
|
||||
const n = getNodeOrThrow(g, result.id)
|
||||
expect(n.text).toBe('Characters text')
|
||||
})
|
||||
|
||||
it('prefers text children over text prop alias', async () => {
|
||||
const g = makeSceneGraph()
|
||||
const [result] = await renderJSX(g, '<Text text="Prop text" color="#000">Child text</Text>')
|
||||
const n = getNodeOrThrow(g, result.id)
|
||||
expect(n.text).toBe('Child text')
|
||||
})
|
||||
|
||||
it('defaults omit text props', async () => {
|
||||
const g = makeSceneGraph()
|
||||
const [result] = await renderJSX(g, '<Text color="#000">Plain</Text>')
|
||||
|
|
|
|||
|
|
@ -67,4 +67,30 @@ describe('render', () => {
|
|||
expect(result.type).toBe('FRAME')
|
||||
expect(result.children.length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
test('returns JSX warnings', async () => {
|
||||
const { figma } = setupToolTest()
|
||||
const tool = getTool('render')
|
||||
const result = (await tool.execute(figma, {
|
||||
jsx: '<Frame name="Card" w={200} h={100} mt={8} />'
|
||||
})) as ToolResult
|
||||
expect(result.warnings).toEqual(['Unsupported prop "mt" on <frame> is ignored.'])
|
||||
})
|
||||
|
||||
test('get_node exposes text style fields', async () => {
|
||||
const { figma } = setupToolTest()
|
||||
const render = getTool('render')
|
||||
const card = (await render.execute(figma, {
|
||||
jsx: '<Frame name="Card" w={200} h={100}><Text name="Title" size={24} weight={700} font="Inter" color="#111" textAlign="center">Hello</Text></Frame>'
|
||||
})) as ToolResult
|
||||
const textId = (card.children as string[])[0]
|
||||
const getNode = getTool('get_node')
|
||||
const result = getNode.execute(figma, { id: textId, depth: 0 }) as ToolResult
|
||||
|
||||
expect(result.characters).toBe('Hello')
|
||||
expect(result.fontFamily).toBe('Inter')
|
||||
expect(result.fontSize).toBe(24)
|
||||
expect(result.fontWeight).toBe(700)
|
||||
expect(result.textAlignHorizontal).toBe('CENTER')
|
||||
})
|
||||
})
|
||||
|
|
|
|||
Loading…
Reference in a new issue