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
This commit is contained in:
Danila Poyarkov 2026-05-20 15:02:01 +03:00
parent 11495ee2ce
commit e6da2736fe
3 changed files with 88 additions and 9 deletions

View file

@ -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<typeof applyPatchAndMark>[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(

View file

@ -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<SceneNode> = {}
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<string, InstanceNodeChange>,
@ -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)
}

View file

@ -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', () => {