From 2072f2e9168bb52b3adb1c5bb50871d74fe8d23d Mon Sep 17 00:00:00 2001 From: Danila Poyarkov Date: Sun, 17 May 2026 22:40:30 +0300 Subject: [PATCH] fix(fig): apply text component props --- .../core/src/kiwi/instance-overrides/props.ts | 83 ++++++++++++++----- .../core/src/kiwi/instance-overrides/types.ts | 6 +- .../io/fig/import/component-props.test.ts | 70 ++++++++++++++++ 3 files changed, 136 insertions(+), 23 deletions(-) create mode 100644 tests/engine/io/fig/import/component-props.test.ts diff --git a/packages/core/src/kiwi/instance-overrides/props.ts b/packages/core/src/kiwi/instance-overrides/props.ts index e5d117a26..1cd79d785 100644 --- a/packages/core/src/kiwi/instance-overrides/props.ts +++ b/packages/core/src/kiwi/instance-overrides/props.ts @@ -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 +): 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, + modified?: Set +): 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) } } diff --git a/packages/core/src/kiwi/instance-overrides/types.ts b/packages/core/src/kiwi/instance-overrides/types.ts index 71ae24f6e..85c93c37b 100644 --- a/packages/core/src/kiwi/instance-overrides/types.ts +++ b/packages/core/src/kiwi/instance-overrides/types.ts @@ -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 } } } diff --git a/tests/engine/io/fig/import/component-props.test.ts b/tests/engine/io/fig/import/component-props.test.ts new file mode 100644 index 000000000..37fd2f2ed --- /dev/null +++ b/tests/engine/io/fig/import/component-props.test.ts @@ -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']) + }) +})