From 6c6671bb7aa06c56b89bc749fb4550e07d8cbbf7 Mon Sep 17 00:00:00 2001 From: Danila Poyarkov Date: Sat, 25 Apr 2026 21:45:38 +0300 Subject: [PATCH] fix(kiwi): resolve badge avatar overrides --- .../core/src/kiwi/instance-overrides/index.ts | 9 ++- .../core/src/kiwi/instance-overrides/props.ts | 27 ++++++- .../src/kiwi/instance-overrides/resolve.ts | 77 +++++++++++++------ .../core/src/kiwi/instance-overrides/types.ts | 2 + 4 files changed, 87 insertions(+), 28 deletions(-) diff --git a/packages/core/src/kiwi/instance-overrides/index.ts b/packages/core/src/kiwi/instance-overrides/index.ts index 71aa9ea84..3aec09272 100644 --- a/packages/core/src/kiwi/instance-overrides/index.ts +++ b/packages/core/src/kiwi/instance-overrides/index.ts @@ -95,12 +95,14 @@ function buildOverrideContext( } const propDefaults = new Map() + const propNames = new Map() for (const [, nc] of changeMap) { if (!nc.componentPropDefs?.length) continue for (const def of nc.componentPropDefs) { - if (def.id && def.initialValue) { - propDefaults.set(guidToString(def.id), def.initialValue) - } + if (!def.id) continue + const id = guidToString(def.id) + if (def.initialValue) propDefaults.set(id, def.initialValue) + if (def.name) propNames.set(id, def.name) } } @@ -120,6 +122,7 @@ function buildOverrideContext( overrideKeyToGuid, nodeIdToGuid, propDefaults, + propNames, preComputedRoot: new Map(), componentIdRoot: new Map(), swappedInstances: new Set(), diff --git a/packages/core/src/kiwi/instance-overrides/props.ts b/packages/core/src/kiwi/instance-overrides/props.ts index af7794a9e..78e3d55bd 100644 --- a/packages/core/src/kiwi/instance-overrides/props.ts +++ b/packages/core/src/kiwi/instance-overrides/props.ts @@ -8,6 +8,10 @@ import type { ComponentPropValue } from './types' +function normalizePropName(value: string): string { + return value.toLowerCase().replace(/[^a-z0-9]/g, '') +} + function isEmptyPropValue(v: ComponentPropValue): boolean { return v.boolValue === undefined && v.textValue === undefined && v.guidValue === undefined } @@ -69,6 +73,25 @@ function assignmentsToValueMap( * Recursively apply prop assignments to children of a parent node. * Handles VISIBLE toggles and OVERRIDDEN_SYMBOL_ID (instance swap). */ +function fallbackRefsForChild( + ctx: OverrideContext, + childName: string, + valueByDef: Map +): ComponentPropRef[] | undefined { + const normalizedChildName = normalizePropName(childName) + const refs: ComponentPropRef[] = [] + for (const defId of valueByDef.keys()) { + const propName = ctx.propNames.get(defId) + if (propName && normalizePropName(propName) === normalizedChildName) { + refs.push({ + defID: { sessionID: Number(defId.split(':')[0]), localID: Number(defId.split(':')[1]) }, + componentPropNodeField: 'VISIBLE' + }) + } + } + return refs.length > 0 ? refs : undefined +} + function applyPropAssignments( ctx: OverrideContext, parentId: string, @@ -86,7 +109,9 @@ function applyPropAssignments( continue } - const refs = findPropRefs(ctx, child.componentId, propRefsMap) + const refs = + findPropRefs(ctx, child.componentId, propRefsMap) ?? + fallbackRefsForChild(ctx, child.name, valueByDef) if (refs) { for (const ref of refs) { if (!ref.defID) continue diff --git a/packages/core/src/kiwi/instance-overrides/resolve.ts b/packages/core/src/kiwi/instance-overrides/resolve.ts index d72e9b48c..1ebf5f958 100644 --- a/packages/core/src/kiwi/instance-overrides/resolve.ts +++ b/packages/core/src/kiwi/instance-overrides/resolve.ts @@ -2,7 +2,7 @@ import { guidToString } from '../convert' import type { SceneNode } from '../../scene-graph' import type { GUID } from '../codec' -import type { OverrideContext } from './types' +import type { InstanceNodeChange, OverrideContext } from './types' const MAX_CHAIN_DEPTH = 20 const siblingIndexCache = new WeakMap>() @@ -141,6 +141,29 @@ function sourceSiblingIndex(ctx: OverrideContext, figmaGuid: string): number | n return result } +function findNodeByNameAndType( + ctx: OverrideContext, + parentId: string, + name: string | undefined, + type: string | undefined +): string | null { + if (!name || !type) return null + let match: string | null = null + let count = 0 + const visit = (id: string) => { + if (count > 1) return + const node = ctx.graph.getNode(id) + if (!node) return + if (node.name === name && node.type === type) { + count++ + match = id + } + for (const childId of node.childIds) visit(childId) + } + visit(parentId) + return count === 1 ? match : null +} + function findNodeBySourceSiblingIndex( ctx: OverrideContext, parentId: string, @@ -195,10 +218,7 @@ export function findNodeByComponentId( if (cache.has(cacheKey)) return cache.get(cacheKey) ?? null const parent = ctx.graph.getNode(parentId) - if (!parent) { - cache.set(cacheKey, null) - return null - } + if (!parent) return null for (const childId of parent.childIds) { const child = ctx.graph.getNode(childId) @@ -238,7 +258,6 @@ export function findNodeByComponentId( return deep } } - cache.set(cacheKey, null) return null } @@ -248,6 +267,25 @@ export function findNodeByComponentId( * Each GUID in the path identifies an overrideKey → figmaGuid → graph node. * The chain walks from the instance down to the target. */ +function resolveOverrideStep( + ctx: OverrideContext, + currentId: string, + figmaGuid: string, + remapped: string | undefined, + targetNc: InstanceNodeChange | undefined +): string | null { + if (!remapped) return findNodeByNameAndType(ctx, currentId, targetNc?.name, targetNc?.type) + + const current = ctx.graph.getNode(currentId) + if (current?.componentId === remapped) return currentId + + return ( + findNodeByComponentId(ctx, currentId, remapped) ?? + findNodeBySourceSiblingIndex(ctx, currentId, remapped, figmaGuid) ?? + findNodeByNameAndType(ctx, currentId, targetNc?.name, targetNc?.type) + ) +} + export function resolveOverrideTarget( ctx: OverrideContext, instanceId: string, @@ -257,27 +295,18 @@ export function resolveOverrideTarget( for (let index = 0; index < guids.length; index++) { const key = guidToString(guids[index]) const figmaGuid = ctx.overrideKeyToGuid.get(key) ?? key - const remapped = ctx.guidToNodeId.get(figmaGuid) - if (!remapped) return null - - const current = ctx.graph.getNode(currentId) - if (current?.componentId === remapped) continue - - const found = findNodeByComponentId(ctx, currentId, remapped) - if (found) { - currentId = found + const targetNc = ctx.changeMap.get(figmaGuid) + const symbolGuid = targetNc?.symbolData?.symbolID + ? guidToString(targetNc.symbolData.symbolID) + : null + const remapped = + ctx.guidToNodeId.get(figmaGuid) ?? (symbolGuid ? ctx.guidToNodeId.get(symbolGuid) : undefined) + const resolved = resolveOverrideStep(ctx, currentId, figmaGuid, remapped, targetNc) + if (resolved) { + currentId = resolved continue } - const indexed = findNodeBySourceSiblingIndex(ctx, currentId, remapped, figmaGuid) - if (indexed) { - currentId = indexed - continue - } - - // Some .fig DSD paths include an intermediate source instance while the - // imported tree has a single wrapper clone in that slot. Descend into the - // wrapper and retry the same guid before giving up. const parent = ctx.graph.getNode(currentId) if (parent?.childIds.length === 1) { currentId = parent.childIds[0] diff --git a/packages/core/src/kiwi/instance-overrides/types.ts b/packages/core/src/kiwi/instance-overrides/types.ts index a2aed0dce..47f3df601 100644 --- a/packages/core/src/kiwi/instance-overrides/types.ts +++ b/packages/core/src/kiwi/instance-overrides/types.ts @@ -47,6 +47,7 @@ export interface ComponentPropDef { export interface InstanceNodeChange { type?: string + name?: string guid?: GUID parentIndex?: { guid?: GUID } transform?: Matrix @@ -76,6 +77,7 @@ export interface OverrideContext { overrideKeyToGuid: Map nodeIdToGuid: Map propDefaults: Map + propNames: Map preComputedRoot: Map componentIdRoot: Map swappedInstances: Set