From e6da2736fe0b974c87cdd86bab55bc11a49705cb Mon Sep 17 00:00:00 2001 From: Danila Poyarkov Date: Wed, 20 May 2026 15:02:01 +0300 Subject: [PATCH] fix(fig): sync lazy link component overrides - Carry resolved text styling and glyph data through component-property text clones - Align lazy cloned child placement and hidden chevrons with their resolved source - Cover the gold-preview dropzone link lazy import regression --- .../component-props/apply.ts | 19 +++++-- .../core/src/kiwi/instance-overrides/index.ts | 55 ++++++++++++++++++- .../fig/import/instance-regressions.test.ts | 23 +++++++- 3 files changed, 88 insertions(+), 9 deletions(-) diff --git a/packages/core/src/kiwi/instance-overrides/component-props/apply.ts b/packages/core/src/kiwi/instance-overrides/component-props/apply.ts index 7e1927f52..ebbf21f20 100644 --- a/packages/core/src/kiwi/instance-overrides/component-props/apply.ts +++ b/packages/core/src/kiwi/instance-overrides/component-props/apply.ts @@ -6,6 +6,7 @@ import type { OverrideContext } from '#core/kiwi/instance-overrides/types' import { guidToString } from '#core/kiwi/node-change/convert' +import { copyFills, copyStyleRuns } from '#core/scene-graph/copy' import { propTextCharacters } from './values' @@ -42,12 +43,18 @@ function applyTextProp( const child = ctx.graph.getNode(childId) const text = propTextCharacters(val) if (text === undefined || child?.type !== 'TEXT') return - applyPatchAndMark( - ctx, - childId, - { targetId: childId, source: 'component-prop', props: { text } }, - modified - ) + const source = child.componentId ? ctx.graph.getNode(child.componentId) : null + const props: Parameters[2]['props'] = { text } + if (source?.type === 'TEXT' && source.text === text) { + props.width = source.width + props.height = source.height + props.fills = copyFills(source.fills) + props.styleRuns = copyStyleRuns(source.styleRuns) + props.figmaDerivedTextGlyphs = source.figmaDerivedTextGlyphs + ? structuredClone(source.figmaDerivedTextGlyphs) + : undefined + } + applyPatchAndMark(ctx, childId, { targetId: childId, source: 'component-prop', props }, modified) } function applySwapProp( diff --git a/packages/core/src/kiwi/instance-overrides/index.ts b/packages/core/src/kiwi/instance-overrides/index.ts index e56a9922b..c4dc92dba 100644 --- a/packages/core/src/kiwi/instance-overrides/index.ts +++ b/packages/core/src/kiwi/instance-overrides/index.ts @@ -11,8 +11,8 @@ export type { } from './types' import { guidToString } from '#core/kiwi/node-change/convert' -import type { SceneGraph } from '#core/scene-graph' -import { copyFills } from '#core/scene-graph/copy' +import type { SceneGraph, SceneNode } from '#core/scene-graph' +import { copyFills, copyStyleRuns } from '#core/scene-graph/copy' import { applyConstraintScaling } from './constraints' import { applyDerivedSymbolData } from './derived-symbol-data' @@ -89,6 +89,55 @@ function propagateResolvedFills( } } +function propagateResolvedChildPlacementClones(graph: SceneGraph): void { + for (let pass = 0; pass < 10; pass++) { + let changed = false + for (const node of graph.getAllNodes()) { + if (node.type !== 'INSTANCE' || !node.componentId) continue + const source = graph.getNode(node.componentId) + if (!source || source.childIds.length !== node.childIds.length) continue + for (let i = 0; i < node.childIds.length; i++) { + const sourceChild = graph.getNode(source.childIds[i]) + const child = graph.getNode(node.childIds[i]) + if (!sourceChild || !child) continue + if (sourceChild.overrideKey && child.overrideKey && sourceChild.overrideKey !== child.overrideKey) { + continue + } + const updates: Partial = {} + if (!sourceChild.visible && child.visible) updates.visible = false + if (sourceChild.x !== child.x) updates.x = sourceChild.x + if (sourceChild.y !== child.y) updates.y = sourceChild.y + if (Object.keys(updates).length === 0) continue + graph.updateNode(child.id, updates) + changed = true + } + } + if (!changed) return + } +} + +function propagateResolvedTextClones(graph: SceneGraph): void { + for (let pass = 0; pass < 10; pass++) { + let changed = false + for (const node of graph.getAllNodes()) { + if (node.type !== 'TEXT' || !node.componentId) continue + const source = graph.getNode(node.componentId) + if (source?.type !== 'TEXT' || source.text !== node.text) continue + graph.updateNode(node.id, { + width: source.width, + height: source.height, + fills: copyFills(source.fills), + styleRuns: copyStyleRuns(source.styleRuns), + figmaDerivedTextGlyphs: source.figmaDerivedTextGlyphs + ? structuredClone(source.figmaDerivedTextGlyphs) + : undefined + }) + changed = true + } + if (!changed) return + } +} + function buildOverrideContext( graph: SceneGraph, changeMap: Map, @@ -213,6 +262,7 @@ export function populateAndApplyOverrides( ctx.protectedFields ) } + propagateResolvedChildPlacementClones(graph) } applyDerivedSymbolData(ctx) @@ -221,5 +271,6 @@ export function populateAndApplyOverrides( new Set([...ctx.kiwiPropertyNodes, ...overriddenNodes]), ctx.activeNodeIds ) + propagateResolvedTextClones(graph) applyConstraintScaling(ctx) } diff --git a/tests/engine/io/fig/import/instance-regressions.test.ts b/tests/engine/io/fig/import/instance-regressions.test.ts index afba5306f..8bd0e49de 100644 --- a/tests/engine/io/fig/import/instance-regressions.test.ts +++ b/tests/engine/io/fig/import/instance-regressions.test.ts @@ -123,7 +123,7 @@ describe('derived instance layout regressions', () => { } }) - test('propagates static icon color overrides with lazy first-page import', async () => { + test('propagates nested component overrides with lazy first-page import', async () => { const graph = await parseFigFile(readFixtureBytes('gold-preview.fig').buffer as ArrayBuffer, { populate: 'first-page' }) @@ -144,6 +144,27 @@ describe('derived instance layout regressions', () => { expect(vector?.strokes[0]?.color).toMatchObject({ r: 1, g: 1, b: 1, a: 1 }) } + + const dropzone = previewChild(graph, nodes, 'Drag’ n’ Drop File Uploads') + const dropzoneRoot = childNamed(graph, dropzone, '_drag-n-drop-file-upload') + const content = childNamed(graph, dropzoneRoot, 'Content') + const titleRow = childNamed(graph, content, 'Title') + const links = childNamed(graph, titleRow, 'Links') + const linkRoot = childNamed(graph, links, '_link-default') + const chevronLeft = childNamed(graph, linkRoot, 'chevron-left') + const placeholder = childNamed(graph, linkRoot, 'Placeholder') + const linkText = childNamed(graph, placeholder, 'Link') + + expect(chevronLeft?.visible).toBe(false) + expect(placeholder?.x).toBeCloseTo(0, 3) + expect(linkText?.text).toBe('browse') + expect(linkText?.fills[0]?.color).toMatchObject({ + r: 0.14509804546833038, + g: 0.38823530077934265, + b: 0.9215686321258545, + a: 1 + }) + expect(linkText?.figmaDerivedTextGlyphs?.length).toBeGreaterThan(0) }) test('preserves WYSIWYG toolbar padding', () => {