fix(fig): apply text component props

This commit is contained in:
Danila Poyarkov 2026-05-17 22:40:30 +03:00
parent a6050da061
commit 2072f2e916
3 changed files with 136 additions and 23 deletions

View file

@ -13,7 +13,17 @@ function normalizePropName(value: string): string {
}
function isEmptyPropValue(v: ComponentPropValue): boolean {
return v.boolValue === undefined && v.textValue === undefined && v.guidValue === undefined
return (
v.boolValue === undefined &&
v.textValue === undefined &&
v.textDataValue === undefined &&
v.guidValue === undefined
)
}
function propTextCharacters(value: ComponentPropValue): string | undefined {
if (typeof value.textValue === 'string') return value.textValue
return value.textValue?.characters ?? value.textDataValue?.characters
}
/**
@ -60,6 +70,7 @@ function resolveAssignmentValue(
if (variableValue?.symbolIdValue?.guid) return { guidValue: variableValue.symbolIdValue.guid }
if (variableValue?.boolValue !== undefined) return { boolValue: variableValue.boolValue }
if (variableValue?.textValue !== undefined) return { textValue: variableValue.textValue }
if (variableValue?.textDataValue !== undefined) return { textDataValue: variableValue.textDataValue }
return resolveDefaults ? (ctx.propDefaults.get(key) ?? assignment.value) : assignment.value
}
@ -101,6 +112,54 @@ function fallbackRefsForChild(
return refs.length > 0 ? refs : undefined
}
function applyComponentPropRef(
ctx: OverrideContext,
childId: string,
ref: ComponentPropRef,
val: ComponentPropValue,
modified?: Set<string>
): void {
const child = ctx.graph.getNode(childId)
if (!child) return
if (ref.componentPropNodeField === 'VISIBLE' && val.boolValue !== undefined) {
ctx.graph.updateNode(childId, { visible: val.boolValue })
modified?.add(childId)
return
}
if (ref.componentPropNodeField === 'TEXT_DATA') {
const text = propTextCharacters(val)
if (text === undefined || child.type !== 'TEXT') return
ctx.graph.updateNode(childId, { text })
modified?.add(childId)
return
}
if (ref.componentPropNodeField !== 'OVERRIDDEN_SYMBOL_ID') return
const swapId = propTextCharacters(val) ?? (val.guidValue ? guidToString(val.guidValue) : undefined)
if (!swapId) return
const newCompId = ctx.guidToNodeId.get(swapId)
if (!newCompId) return
repopulateInstance(ctx, childId, newCompId)
modified?.add(childId)
}
function applyChildPropRefs(
ctx: OverrideContext,
childId: string,
refs: ComponentPropRef[] | undefined,
valueByDef: Map<string, ComponentPropValue>,
modified?: Set<string>
): void {
if (!refs) return
for (const ref of refs) {
if (!ref.defID) continue
const val = valueByDef.get(guidToString(ref.defID))
if (val) applyComponentPropRef(ctx, childId, ref, val, modified)
}
}
function applyPropAssignments(
ctx: OverrideContext,
parentId: string,
@ -121,27 +180,7 @@ function applyPropAssignments(
const refs =
findPropRefs(ctx, child.componentId, propRefsMap) ??
fallbackRefsForChild(ctx, child.name, valueByDef)
if (refs) {
for (const ref of refs) {
if (!ref.defID) continue
const val = valueByDef.get(guidToString(ref.defID))
if (!val) continue
if (ref.componentPropNodeField === 'VISIBLE' && val.boolValue !== undefined) {
ctx.graph.updateNode(childId, { visible: val.boolValue })
modified?.add(childId)
} else if (ref.componentPropNodeField === 'OVERRIDDEN_SYMBOL_ID') {
const swapId = val.textValue ?? (val.guidValue ? guidToString(val.guidValue) : undefined)
if (!swapId) continue
const newCompId = ctx.guidToNodeId.get(swapId)
if (newCompId) {
repopulateInstance(ctx, childId, newCompId)
modified?.add(childId)
}
}
}
}
applyChildPropRefs(ctx, childId, refs, valueByDef, modified)
applyPropAssignments(ctx, childId, valueByDef, propRefsMap, modified)
}
}

View file

@ -19,9 +19,12 @@ export interface ComponentPropRef {
componentPropNodeField: string
}
export type ComponentPropTextValue = string | { characters?: string }
export type ComponentPropValue = {
boolValue?: boolean
textValue?: string
textValue?: ComponentPropTextValue
textDataValue?: { characters?: string }
guidValue?: GUID
}
@ -32,6 +35,7 @@ export interface ComponentPropAssignment {
value?: {
boolValue?: boolean
textValue?: string
textDataValue?: { characters?: string }
symbolIdValue?: { guid?: GUID }
}
}

View file

@ -0,0 +1,70 @@
import { describe, expect, test } from 'bun:test'
import { importNodeChanges } from '#core/kiwi/fig/import'
import type { NodeChange } from '#core/kiwi/binary/codec'
const documentGuid = { sessionID: 0, localID: 0 }
const pageGuid = { sessionID: 0, localID: 1 }
const componentGuid = { sessionID: 1, localID: 1 }
const componentTextGuid = { sessionID: 1, localID: 2 }
const instanceGuid = { sessionID: 2, localID: 1 }
const textPropGuid = { sessionID: 3, localID: 1 }
function baseTextChange(): NodeChange {
return {
guid: componentTextGuid,
phase: 'CREATED',
parentIndex: { guid: componentGuid, position: '!' },
type: 'TEXT',
name: 'Label',
size: { x: 100, y: 20 },
transform: { m00: 1, m01: 0, m02: 0, m10: 0, m11: 1, m12: 0 },
fontSize: 14,
fontName: { family: 'Inter', style: 'Regular', postscript: '' },
textData: { characters: 'Menu Item', lines: [{ lineType: 'PLAIN' }] },
componentPropRefs: [{ defID: textPropGuid, componentPropNodeField: 'TEXT_DATA' }]
}
}
describe('Figma component property import', () => {
test('applies text data component prop assignments', () => {
const nodeChanges: NodeChange[] = [
{ guid: documentGuid, phase: 'CREATED', type: 'DOCUMENT', name: 'Document' },
{ guid: pageGuid, phase: 'CREATED', parentIndex: { guid: documentGuid, position: '!' }, type: 'CANVAS', name: 'Page' },
{
guid: componentGuid,
phase: 'CREATED',
parentIndex: { guid: pageGuid, position: '!' },
type: 'SYMBOL',
name: 'Menu item',
size: { x: 100, y: 20 },
transform: { m00: 1, m01: 0, m02: 0, m10: 0, m11: 1, m12: 0 },
componentPropDefs: [
{ id: textPropGuid, name: 'label', initialValue: { textValue: 'Menu Item' } }
]
},
baseTextChange(),
{
guid: instanceGuid,
phase: 'CREATED',
parentIndex: { guid: pageGuid, position: '"' },
type: 'INSTANCE',
name: 'Menu item instance',
symbolData: { symbolID: componentGuid },
size: { x: 100, y: 20 },
transform: { m00: 1, m01: 0, m02: 0, m10: 0, m11: 1, m12: 40 },
componentPropAssignments: [
{
defID: textPropGuid,
value: { textValue: { characters: 'Profile Item' } },
varValue: { value: { textDataValue: { characters: 'Profile Item' } } }
}
]
}
]
const graph = importNodeChanges(nodeChanges, [], undefined, { populate: 'all' })
const labels = Array.from(graph.getAllNodes()).filter((node) => node.type === 'TEXT')
expect(labels.map((node) => node.text).sort()).toEqual(['Menu Item', 'Profile Item'])
})
})