inkbridge 0.1.0-beta.2 → 0.1.0-beta.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +108 -25
- package/bin/inkbridge.mjs +354 -83
- package/code.js +40 -11802
- package/manifest.json +1 -0
- package/package.json +74 -23
- package/scanner/adapter-utils-regression.ts +159 -0
- package/scanner/aspect-percent-position-regression.ts +237 -0
- package/scanner/aspect-ratio-regression.ts +90 -0
- package/scanner/blob-placement-regression.ts +2 -2
- package/scanner/block-cache-regression.ts +195 -0
- package/scanner/bundle-size-regression.ts +50 -0
- package/scanner/child-sizing-matrix-regression.ts +303 -0
- package/scanner/cli.ts +342 -13
- package/scanner/component-scanner.ts +2108 -174
- package/scanner/component-sections-regression.ts +198 -0
- package/scanner/compound-classes-lookup-regression.ts +163 -0
- package/scanner/css-token-reader-regression.ts +7 -6
- package/scanner/css-token-reader.ts +152 -31
- package/scanner/cva-jsx-child-fallback-regression.ts +98 -0
- package/scanner/cva-master-icon-regression.ts +315 -0
- package/scanner/data-attr-prop-alias-regression.ts +129 -0
- package/scanner/explicit-size-root-regression.ts +102 -0
- package/scanner/font-family-extract-regression.ts +113 -0
- package/scanner/font-style-resolver-regression.ts +1 -1
- package/scanner/framework-adapter-shadcn-regression.ts +480 -0
- package/scanner/full-width-matrix-regression.ts +338 -0
- package/scanner/grid-cols-extraction-regression.ts +110 -0
- package/scanner/image-src-collector-regression.ts +204 -0
- package/scanner/inline-flex-regression.ts +235 -0
- package/scanner/input-range-regression.ts +217 -0
- package/scanner/instance-rendering-regression.ts +224 -0
- package/scanner/jsx-prop-unresolved-regression.ts +178 -0
- package/scanner/jsx-text-regression.ts +178 -0
- package/scanner/layout-alignment-regression.ts +108 -0
- package/scanner/layout-flex-regression.ts +90 -0
- package/scanner/layout-mode-regression.ts +71 -0
- package/scanner/layout-sizing-regression.ts +227 -0
- package/scanner/layout-spacing-regression.ts +135 -0
- package/scanner/local-const-className-regression.ts +331 -0
- package/scanner/percent-position-regression.ts +105 -0
- package/scanner/provider-cascade-regression.ts +224 -0
- package/scanner/provider-flatten-regression.ts +235 -0
- package/scanner/radial-gradient-regression.ts +1 -1
- package/scanner/render-prop-parser-regression.ts +161 -0
- package/scanner/ring-utility-regression.ts +153 -0
- package/scanner/sandbox-spread-regression.ts +125 -0
- package/scanner/selection-pressed-regression.ts +241 -0
- package/scanner/size-full-normalization-regression.ts +127 -0
- package/scanner/state-classification-regression.ts +175 -0
- package/scanner/story-diagnostics-regression.ts +216 -0
- package/scanner/story-dimensioning-regression.ts +298 -0
- package/scanner/story-render-strategy-regression.ts +205 -0
- package/scanner/stretch-to-parent-width-regression.ts +147 -0
- package/scanner/svg-fill-parent-regression.ts +98 -0
- package/scanner/svg-group-inheritance-regression.ts +166 -0
- package/scanner/svg-marker-inline-regression.ts +211 -0
- package/scanner/svg-marker-regression.ts +116 -0
- package/scanner/tailwind-parser.ts +46 -4
- package/scanner/text-resize-matrix-regression.ts +173 -0
- package/scanner/transform-math-regression.ts +1 -1
- package/scanner/types.ts +26 -2
- package/src/cache/frame-cache.ts +150 -0
- package/src/cache/index.ts +2 -0
- package/src/{component-defs.ts → components/component-defs.ts} +25 -10
- package/src/{component-gen.ts → components/component-gen.ts} +43 -116
- package/src/components/component-instance.ts +386 -0
- package/src/components/component-library.ts +44 -0
- package/src/components/component-lookup.ts +161 -0
- package/src/components/index.ts +7 -0
- package/src/components/scanner-types.ts +39 -0
- package/src/components/symbol-instance-policy.ts +312 -0
- package/src/design-system/block-cache.ts +130 -0
- package/src/design-system/component-sections.ts +107 -0
- package/src/design-system/cva-inference.ts +187 -0
- package/src/design-system/cva-master.ts +427 -0
- package/src/design-system/cva-utils.ts +29 -0
- package/src/design-system/design-system.ts +334 -0
- package/src/design-system/frame-stabilizers.ts +191 -0
- package/src/design-system/frame-utils.ts +46 -0
- package/src/design-system/generated-node.ts +84 -0
- package/src/design-system/icon-rendering.ts +229 -0
- package/src/design-system/index.ts +13 -0
- package/src/design-system/instance-rendering.ts +307 -0
- package/src/design-system/master-shared.ts +133 -0
- package/src/design-system/node-helpers.ts +237 -0
- package/src/design-system/node-variants.ts +196 -0
- package/src/design-system/non-cva-master.ts +104 -0
- package/src/design-system/portal-handling.ts +138 -0
- package/src/design-system/preview-builder.ts +738 -0
- package/src/{render-context.ts → design-system/render-context.ts} +32 -6
- package/src/design-system/render-prop-parser.ts +50 -0
- package/src/design-system/responsive-resolver.ts +180 -0
- package/src/design-system/selectable-state.ts +157 -0
- package/src/design-system/state-master.ts +267 -0
- package/src/design-system/state-utils.ts +15 -0
- package/src/design-system/story-builder-context.ts +40 -0
- package/src/design-system/story-builder.ts +1322 -0
- package/src/design-system/story-diagnostics.ts +80 -0
- package/src/design-system/story-dimensioning.ts +272 -0
- package/src/design-system/story-frames.ts +400 -0
- package/src/design-system/story-instance.ts +333 -0
- package/src/{story-layout.ts → design-system/story-layout.ts} +2 -2
- package/src/design-system/story-render-strategy.ts +150 -0
- package/src/design-system/story-tree-search.ts +110 -0
- package/src/design-system/symbol-fallback.ts +89 -0
- package/src/design-system/symbol-source.ts +172 -0
- package/src/design-system/table-helpers.ts +56 -0
- package/src/design-system/tag-predicates.ts +99 -0
- package/src/design-system/theme-context.ts +52 -0
- package/src/design-system/typography.ts +100 -0
- package/src/design-system/ui-builder.ts +2676 -0
- package/src/{clip-path-decorative.ts → effects/clip-path-decorative.ts} +11 -11
- package/src/effects/icon-builder.ts +1074 -0
- package/src/effects/index.ts +5 -0
- package/src/effects/portal-panel.ts +369 -0
- package/src/{radial-gradient.ts → effects/radial-gradient.ts} +1 -1
- package/src/framework-adapters/index.ts +47 -0
- package/src/framework-adapters/shadcn.ts +541 -0
- package/src/{github.ts → github/github.ts} +46 -21
- package/src/github/index.ts +1 -0
- package/src/layout/deferred-layout.ts +1556 -0
- package/src/layout/index.ts +24 -0
- package/src/layout/layout-parser.ts +375 -0
- package/src/{layout-utils.ts → layout/layout-utils.ts} +23 -17
- package/src/layout/parser/alignment.ts +54 -0
- package/src/layout/parser/flex.ts +59 -0
- package/src/layout/parser/index.ts +65 -0
- package/src/layout/parser/ir.ts +80 -0
- package/src/layout/parser/layout-mode.ts +57 -0
- package/src/layout/parser/sizing.ts +241 -0
- package/src/layout/parser/spacing-scale.ts +78 -0
- package/src/layout/parser/spacing.ts +134 -0
- package/src/layout/ring-utils.ts +120 -0
- package/src/layout/size-utils.ts +143 -0
- package/src/layout/text-resize-decision.ts +51 -0
- package/src/{width-solver.ts → layout/width-solver.ts} +168 -37
- package/src/main.ts +444 -162
- package/src/{config.ts → plugin/config.ts} +12 -12
- package/src/{dev-server.ts → plugin/dev-server.ts} +3 -3
- package/src/plugin/image-src-collector.ts +52 -0
- package/src/plugin/index.ts +3 -0
- package/src/plugin/packs/index.ts +2 -0
- package/src/{pack-provider.ts → plugin/packs/pack-provider.ts} +12 -12
- package/src/{packs.ts → plugin/packs/packs.ts} +22 -17
- package/src/render-engine-version.ts +2 -0
- package/src/tailwind/adapter-utils.ts +137 -0
- package/src/{class-utils.ts → tailwind/class-utils.ts} +33 -6
- package/src/tailwind/index.ts +8 -0
- package/src/tailwind/jsx-utils.ts +319 -0
- package/src/{node-ir.ts → tailwind/node-ir.ts} +208 -19
- package/src/{responsive-analyzer.ts → tailwind/responsive-analyzer.ts} +32 -2
- package/src/{state-analyzer.ts → tailwind/state-analyzer.ts} +71 -5
- package/src/{tailwind.ts → tailwind/tailwind.ts} +423 -674
- package/src/{utility-resolver.ts → tailwind/utility-resolver.ts} +27 -6
- package/src/{font-style-resolver.ts → text/font-style-resolver.ts} +0 -2
- package/src/text/index.ts +4 -0
- package/src/{inline-text.ts → text/inline-text.ts} +13 -13
- package/src/{text-builder.ts → text/text-builder.ts} +24 -7
- package/src/{text-line.ts → text/text-line.ts} +2 -2
- package/src/{change-detection.ts → tokens/change-detection.ts} +12 -12
- package/src/{color-resolver.ts → tokens/color-resolver.ts} +1 -6
- package/src/{colors.ts → tokens/colors.ts} +13 -6
- package/src/tokens/index.ts +6 -0
- package/src/{token-source.ts → tokens/token-source.ts} +4 -1
- package/src/{tokens.ts → tokens/tokens.ts} +116 -20
- package/src/{variables.ts → tokens/variables.ts} +447 -102
- package/templates/patch-tokens-route.ts +25 -6
- package/templates/scan-components-route.ts +26 -5
- package/ui.html +485 -37
- package/src/component-lookup.ts +0 -82
- package/src/design-system.ts +0 -59
- package/src/icon-builder.ts +0 -607
- package/src/layout-parser.ts +0 -667
- package/src/story-builder.ts +0 -1706
- package/src/ui-builder.ts +0 -1996
- /package/src/{image-cache.ts → cache/image-cache.ts} +0 -0
- /package/src/{blob-placement.ts → effects/blob-placement.ts} +0 -0
- /package/src/{transform-math.ts → tailwind/transform-math.ts} +0 -0
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import { TOKENS, COMPONENT_DEFS, getThemeColors, getThemeRadius } from '
|
|
2
|
-
import { parseColor, debug } from '
|
|
1
|
+
import { TOKENS, COMPONENT_DEFS, getThemeColors, getThemeRadius } from '../tokens';
|
|
2
|
+
import { parseColor, debug } from '../tokens';
|
|
3
3
|
import { getComponentDef } from './component-defs';
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
|
|
4
|
+
import type { ComponentAnalysis } from '../../scanner/types';
|
|
5
|
+
import type { ComponentDef } from './scanner-types';
|
|
6
|
+
import { tailwindClassesToStyle, applyTailwindStylesToFrame } from '../tailwind';
|
|
7
|
+
import { bindColorVariable, pxFromSizeToken } from '../tokens';
|
|
8
|
+
import { createTextNode } from '../text';
|
|
9
|
+
import { extractStatesFromClasses, mergeStatesWithDefinition, type StateInfo } from '../tailwind';
|
|
10
|
+
import { getRingInfoFromClasses, markRingNode, applyRingIfPossible } from '../layout';
|
|
11
11
|
|
|
12
12
|
function getStateEntry(states: StateInfo[], name: string): StateInfo | null {
|
|
13
13
|
for (let i = 0; i < states.length; i++) {
|
|
@@ -35,100 +35,7 @@ function buildStateClasses(states: StateInfo[], name: string): string[] {
|
|
|
35
35
|
return baseClasses.concat(entry.classes);
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
function
|
|
39
|
-
if (utility === 'ring') return 3;
|
|
40
|
-
if (!utility.startsWith('ring-')) return null;
|
|
41
|
-
const token = utility.substring(5);
|
|
42
|
-
if (token === 'inset' || token.startsWith('offset-')) return null;
|
|
43
|
-
if (token.startsWith('[')) {
|
|
44
|
-
const arbitrary = extractArbitraryValue(utility);
|
|
45
|
-
if (!arbitrary) return null;
|
|
46
|
-
return parseLength(arbitrary);
|
|
47
|
-
}
|
|
48
|
-
const num = parseFloat(token);
|
|
49
|
-
if (!Number.isNaN(num) && String(num) === token) return num;
|
|
50
|
-
return null;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function parseRingColor(utility: string, colorGroup: Record<string, string>): { r: number; g: number; b: number; a?: number } | null {
|
|
54
|
-
if (!utility.startsWith('ring-')) return null;
|
|
55
|
-
const token = utility.substring(5);
|
|
56
|
-
if (token === 'inset' || token.startsWith('offset-')) return null;
|
|
57
|
-
if (token.startsWith('[')) return null;
|
|
58
|
-
const num = parseFloat(token);
|
|
59
|
-
if (!Number.isNaN(num) && String(num) === token) return null;
|
|
60
|
-
const resolved = colorGroup[token];
|
|
61
|
-
if (!resolved) return null;
|
|
62
|
-
return parseColor(resolved);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function getRingInfoFromClasses(classes: string[], colorGroup: Record<string, string>): RingInfo | null {
|
|
66
|
-
let width: number | null = null;
|
|
67
|
-
let color: { r: number; g: number; b: number; a?: number } | null = null;
|
|
68
|
-
|
|
69
|
-
for (let i = 0; i < classes.length; i++) {
|
|
70
|
-
const cls = classes[i];
|
|
71
|
-
const nextWidth = parseRingWidth(cls);
|
|
72
|
-
if (nextWidth != null) width = nextWidth;
|
|
73
|
-
const nextColor = parseRingColor(cls, colorGroup);
|
|
74
|
-
if (nextColor) color = nextColor;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
if (width == null && color == null) return null;
|
|
78
|
-
if (width == null) width = 3;
|
|
79
|
-
if (!color) {
|
|
80
|
-
const fallback = colorGroup.ring || colorGroup.primary;
|
|
81
|
-
if (!fallback) return null;
|
|
82
|
-
color = parseColor(fallback);
|
|
83
|
-
}
|
|
84
|
-
if (!width || width <= 0) return null;
|
|
85
|
-
return { width, color };
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
function hasVisibleFill(node: FrameNode): boolean {
|
|
89
|
-
const fills = (node as any).fills;
|
|
90
|
-
if (!Array.isArray(fills)) return false;
|
|
91
|
-
for (let i = 0; i < fills.length; i++) {
|
|
92
|
-
const fill = fills[i];
|
|
93
|
-
if (!fill || fill.visible === false) continue;
|
|
94
|
-
if (fill.opacity != null && fill.opacity <= 0) continue;
|
|
95
|
-
return true;
|
|
96
|
-
}
|
|
97
|
-
return false;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
function applyRingEffect(node: FrameNode, classes: string[], colorGroup: Record<string, string>): void {
|
|
101
|
-
const ring = getRingInfoFromClasses(classes, colorGroup);
|
|
102
|
-
if (!ring) return;
|
|
103
|
-
const useSpread = (node as any).clipsContent === true && hasVisibleFill(node);
|
|
104
|
-
if (!useSpread) {
|
|
105
|
-
const strokeWeight = typeof node.strokeWeight === 'number' ? node.strokeWeight : 0;
|
|
106
|
-
node.strokes = [{
|
|
107
|
-
type: 'SOLID',
|
|
108
|
-
color: { r: ring.color.r, g: ring.color.g, b: ring.color.b },
|
|
109
|
-
opacity: ring.color.a == null ? 1 : ring.color.a,
|
|
110
|
-
}];
|
|
111
|
-
node.strokeWeight = Math.max(strokeWeight, ring.width);
|
|
112
|
-
return;
|
|
113
|
-
}
|
|
114
|
-
const effect: DropShadowEffect = {
|
|
115
|
-
type: 'DROP_SHADOW',
|
|
116
|
-
color: { r: ring.color.r, g: ring.color.g, b: ring.color.b, a: ring.color.a == null ? 1 : ring.color.a },
|
|
117
|
-
offset: { x: 0, y: 0 },
|
|
118
|
-
radius: 0,
|
|
119
|
-
spread: ring.width,
|
|
120
|
-
visible: true,
|
|
121
|
-
blendMode: 'NORMAL'
|
|
122
|
-
};
|
|
123
|
-
const effects: Effect[] = [];
|
|
124
|
-
if (node.effects && node.effects.length > 0) {
|
|
125
|
-
for (let i = 0; i < node.effects.length; i++) effects.push(node.effects[i]);
|
|
126
|
-
}
|
|
127
|
-
effects.push(effect);
|
|
128
|
-
node.effects = effects;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
export function createCVAComponentSet(parent: any, def: any, theme: string): any {
|
|
38
|
+
export function createCVAComponentSet(parent: FrameNode | PageNode, def: ComponentDef, theme: string): SceneNode | null {
|
|
132
39
|
const colorGroup = getThemeColors(TOKENS, theme);
|
|
133
40
|
const radiusGroup = getThemeRadius(TOKENS, theme);
|
|
134
41
|
|
|
@@ -210,7 +117,13 @@ export function createCVAComponentSet(parent: any, def: any, theme: string): any
|
|
|
210
117
|
// Apply background - try variable binding first, fall back to raw color
|
|
211
118
|
if (style.bg) {
|
|
212
119
|
const bgBound = style.bgToken && bindColorVariable(comp, style.bgToken, 'fill', theme);
|
|
213
|
-
if (
|
|
120
|
+
if (bgBound) {
|
|
121
|
+
if (style.bgOpacity != null && Array.isArray(comp.fills) && comp.fills.length > 0) {
|
|
122
|
+
const nextFills = JSON.parse(JSON.stringify(comp.fills));
|
|
123
|
+
nextFills[0].opacity = style.bgOpacity;
|
|
124
|
+
comp.fills = nextFills;
|
|
125
|
+
}
|
|
126
|
+
} else {
|
|
214
127
|
const bg = parseColor(style.bg);
|
|
215
128
|
const bgOpacity = style.bgOpacity != null ? style.bgOpacity : (bg.a == null ? 1 : bg.a);
|
|
216
129
|
comp.fills = [{ type: 'SOLID', color: { r: bg.r, g: bg.g, b: bg.b }, opacity: bgOpacity }];
|
|
@@ -242,8 +155,14 @@ export function createCVAComponentSet(parent: any, def: any, theme: string): any
|
|
|
242
155
|
}
|
|
243
156
|
|
|
244
157
|
comp.appendChild(text);
|
|
245
|
-
|
|
158
|
+
const ringInfo = getRingInfoFromClasses(stateClasses, colorGroup);
|
|
159
|
+
if (ringInfo) markRingNode(comp, ringInfo);
|
|
246
160
|
stateRow.appendChild(comp);
|
|
161
|
+
// Comp's children are in place and it's now in the parent's auto-layout
|
|
162
|
+
// — apply ring (no-op if not marked). `applyRingIfPossible` locks the
|
|
163
|
+
// comp's sizing modes during the overlay append so inflation is
|
|
164
|
+
// impossible.
|
|
165
|
+
applyRingIfPossible(comp, stateRow);
|
|
247
166
|
}
|
|
248
167
|
|
|
249
168
|
variantSection.appendChild(stateRow);
|
|
@@ -254,7 +173,7 @@ export function createCVAComponentSet(parent: any, def: any, theme: string): any
|
|
|
254
173
|
return container;
|
|
255
174
|
}
|
|
256
175
|
|
|
257
|
-
export function createStateComponentSet(parent:
|
|
176
|
+
export function createStateComponentSet(parent: FrameNode | PageNode, def: ComponentDef, theme: string): SceneNode | null {
|
|
258
177
|
const colorGroup = getThemeColors(TOKENS, theme);
|
|
259
178
|
const radiusGroup = getThemeRadius(TOKENS, theme);
|
|
260
179
|
|
|
@@ -304,7 +223,13 @@ export function createStateComponentSet(parent: any, def: any, theme: string): a
|
|
|
304
223
|
|
|
305
224
|
if (style.bg) {
|
|
306
225
|
const bgBound = style.bgToken && bindColorVariable(comp, style.bgToken, 'fill', theme);
|
|
307
|
-
if (
|
|
226
|
+
if (bgBound) {
|
|
227
|
+
if (style.bgOpacity != null && Array.isArray(comp.fills) && comp.fills.length > 0) {
|
|
228
|
+
const nextFills = JSON.parse(JSON.stringify(comp.fills));
|
|
229
|
+
nextFills[0].opacity = style.bgOpacity;
|
|
230
|
+
comp.fills = nextFills;
|
|
231
|
+
}
|
|
232
|
+
} else {
|
|
308
233
|
const bg = parseColor(style.bg);
|
|
309
234
|
const bgOpacity = style.bgOpacity != null ? style.bgOpacity : (bg.a == null ? 1 : bg.a);
|
|
310
235
|
comp.fills = [{ type: 'SOLID', color: { r: bg.r, g: bg.g, b: bg.b }, opacity: bgOpacity }];
|
|
@@ -349,9 +274,11 @@ export function createStateComponentSet(parent: any, def: any, theme: string): a
|
|
|
349
274
|
fill: { r: placeholderColor.r, g: placeholderColor.g, b: placeholderColor.b }
|
|
350
275
|
});
|
|
351
276
|
comp.appendChild(placeholder);
|
|
352
|
-
|
|
277
|
+
const ringInfo = getRingInfoFromClasses(stateClasses, colorGroup);
|
|
278
|
+
if (ringInfo) markRingNode(comp, ringInfo);
|
|
353
279
|
|
|
354
280
|
stateRow.appendChild(comp);
|
|
281
|
+
applyRingIfPossible(comp, stateRow);
|
|
355
282
|
}
|
|
356
283
|
|
|
357
284
|
container.appendChild(stateRow);
|
|
@@ -359,7 +286,7 @@ export function createStateComponentSet(parent: any, def: any, theme: string): a
|
|
|
359
286
|
return container;
|
|
360
287
|
}
|
|
361
288
|
|
|
362
|
-
export function createCompoundComponent(parent:
|
|
289
|
+
export function createCompoundComponent(parent: FrameNode | PageNode, def: ComponentDef, theme: string): SceneNode | null {
|
|
363
290
|
const colorGroup = getThemeColors(TOKENS, theme);
|
|
364
291
|
const radiusGroup = getThemeRadius(TOKENS, theme);
|
|
365
292
|
|
|
@@ -372,7 +299,7 @@ export function createCompoundComponent(parent: any, def: any, theme: string): a
|
|
|
372
299
|
container.strokes = [];
|
|
373
300
|
|
|
374
301
|
// Find the main container sub-component
|
|
375
|
-
let mainSub:
|
|
302
|
+
let mainSub: ComponentDef = null;
|
|
376
303
|
const subComponents = def.subComponents || [];
|
|
377
304
|
for (let i = 0; i < subComponents.length; i++) {
|
|
378
305
|
if (subComponents[i].slot === 'container' || subComponents[i].name === def.name) {
|
|
@@ -391,7 +318,7 @@ export function createCompoundComponent(parent: any, def: any, theme: string): a
|
|
|
391
318
|
container.primaryAxisSizingMode = 'AUTO';
|
|
392
319
|
|
|
393
320
|
// Helper to find a sub-component by slot
|
|
394
|
-
function findSub(slot: string):
|
|
321
|
+
function findSub(slot: string): ComponentDef {
|
|
395
322
|
for (let j = 0; j < subComponents.length; j++) {
|
|
396
323
|
if (subComponents[j].slot === slot) return subComponents[j];
|
|
397
324
|
}
|
|
@@ -491,7 +418,7 @@ export function createCompoundComponent(parent: any, def: any, theme: string): a
|
|
|
491
418
|
return container;
|
|
492
419
|
}
|
|
493
420
|
|
|
494
|
-
export function createSimpleComponent(parent:
|
|
421
|
+
export function createSimpleComponent(parent: FrameNode | PageNode, def: ComponentDef, theme: string): SceneNode | null {
|
|
495
422
|
const colorGroup = getThemeColors(TOKENS, theme);
|
|
496
423
|
const radiusGroup = getThemeRadius(TOKENS, theme);
|
|
497
424
|
|
|
@@ -512,7 +439,7 @@ export function createSimpleComponent(parent: any, def: any, theme: string): any
|
|
|
512
439
|
return frame;
|
|
513
440
|
}
|
|
514
441
|
|
|
515
|
-
export function createComponentFromDef(parent:
|
|
442
|
+
export function createComponentFromDef(parent: FrameNode | PageNode, componentName: string, theme: string, _options?: { types?: string[]; exclude?: string[] }): SceneNode | null {
|
|
516
443
|
const def = getComponentDef(componentName);
|
|
517
444
|
if (!def) {
|
|
518
445
|
debug('Component not found in COMPONENT_DEFS:', componentName);
|
|
@@ -531,18 +458,18 @@ export function createComponentFromDef(parent: any, componentName: string, theme
|
|
|
531
458
|
case 'simple':
|
|
532
459
|
return createSimpleComponent(parent, def, theme);
|
|
533
460
|
default:
|
|
534
|
-
debug('Unknown component type:', def.type);
|
|
461
|
+
debug('Unknown component type:', (def as ComponentAnalysis).type);
|
|
535
462
|
return null;
|
|
536
463
|
}
|
|
537
464
|
}
|
|
538
465
|
|
|
539
|
-
export function createAllComponentsFromDefs(parent:
|
|
466
|
+
export function createAllComponentsFromDefs(parent: FrameNode | PageNode, theme: string, options?: { types?: string[]; exclude?: string[] }): SceneNode[] {
|
|
540
467
|
options = options || {};
|
|
541
468
|
const allowedTypes = options.types || ['cva', 'compound', 'state', 'simple'];
|
|
542
469
|
const excludeNames = options.exclude || [];
|
|
543
470
|
|
|
544
471
|
const components = COMPONENT_DEFS && COMPONENT_DEFS.components ? COMPONENT_DEFS.components : [];
|
|
545
|
-
const created:
|
|
472
|
+
const created: SceneNode[] = [];
|
|
546
473
|
|
|
547
474
|
for (let i = 0; i < components.length; i++) {
|
|
548
475
|
const def = components[i];
|
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
import type { BackendCtx, ComponentDef, ComponentInstance, ComponentStory } from './scanner-types';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Whether a CVA instance carries non-text JSX children that the symbol-instance
|
|
5
|
+
* path can't honour. Figma's component-property API can override TEXT and
|
|
6
|
+
* boolean/instance-swap properties on an instance — but it can't swap arbitrary
|
|
7
|
+
* SVG vectors. So when the consumer writes
|
|
8
|
+
* `<Toggle><AlignLeft /></Toggle>` inside a `<ToggleGroup>`, the Toggle master
|
|
9
|
+
* (built from Toggle's own stories) has a fixed icon, and every cloned instance
|
|
10
|
+
* inherits it. Result: all 3 ToggleGroup items render the same icon.
|
|
11
|
+
*
|
|
12
|
+
* The plugin's symmetric guard at the STORY root (`shouldUseInstance` in
|
|
13
|
+
* story-render-strategy.ts) already falls back to JSX-tree rendering when the
|
|
14
|
+
* root instance has `__jsxChildren`. This is the parallel guard for NESTED
|
|
15
|
+
* CVA instances inside a JSX tree (e.g. ToggleGroup items, icon-bearing
|
|
16
|
+
* Buttons inside another component's story). Returning null causes the JSX-
|
|
17
|
+
* tree renderer to build a frame for the instance instead, with its own
|
|
18
|
+
* per-instance children preserved.
|
|
19
|
+
*
|
|
20
|
+
* Text-only `__jsxChildren` (e.g. `<Button>Click me</Button>`) is NOT a
|
|
21
|
+
* reason to fall back — the text-override path handles it correctly. We
|
|
22
|
+
* only fall back when there's at least one element-type child.
|
|
23
|
+
*/
|
|
24
|
+
export function cvaInstanceHasOverridingJsxChildren(props: { __jsxChildren?: unknown } | null | undefined): boolean {
|
|
25
|
+
if (!props) return false;
|
|
26
|
+
const children = props.__jsxChildren;
|
|
27
|
+
if (!Array.isArray(children)) return false;
|
|
28
|
+
for (let i = 0; i < children.length; i++) {
|
|
29
|
+
const c = children[i];
|
|
30
|
+
if (c && typeof c === 'object' && (c as { type?: string }).type === 'element') return true;
|
|
31
|
+
}
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* State props that, when truthy on a CVA instance, require frame-render
|
|
37
|
+
* fall-back instead of a symbol-instance reuse. The reason: Figma's
|
|
38
|
+
* component instance API can't per-instance-activate data-attribute CSS
|
|
39
|
+
* variants. So even if the icon matches the master, an instance with
|
|
40
|
+
* `defaultPressed=true` wouldn't get the master's pressed styling unless
|
|
41
|
+
* the master itself was built from a pressed story — which is non-
|
|
42
|
+
* deterministic and brittle. Frame rendering lets the variant engine
|
|
43
|
+
* activate `data-[pressed]:*` / `data-[checked]:*` per-instance correctly.
|
|
44
|
+
*
|
|
45
|
+
* The set mirrors DATA_ATTR_PROP_ALIASES in node-variants.ts. Only the
|
|
46
|
+
* "user explicitly wants this state" props (defaultX / X) are included —
|
|
47
|
+
* not the `aria-*` mirrors, which are accessibility metadata and don't
|
|
48
|
+
* by themselves indicate a visual state requested by the consumer.
|
|
49
|
+
*/
|
|
50
|
+
const STATE_OVERRIDING_PROP_NAMES = [
|
|
51
|
+
'defaultPressed',
|
|
52
|
+
'pressed',
|
|
53
|
+
'defaultChecked',
|
|
54
|
+
'checked',
|
|
55
|
+
'disabled',
|
|
56
|
+
'defaultOpen',
|
|
57
|
+
'open',
|
|
58
|
+
'selected',
|
|
59
|
+
] as const;
|
|
60
|
+
|
|
61
|
+
function isStateTruthy(value: unknown): boolean {
|
|
62
|
+
if (value === true) return true;
|
|
63
|
+
if (typeof value === 'string') {
|
|
64
|
+
const lower = value.trim().toLowerCase();
|
|
65
|
+
return lower === 'true' || lower === 'on' || lower === '';
|
|
66
|
+
}
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Whether a CVA instance carries any truthy state-conveying prop that
|
|
72
|
+
* the master-instance path can't honour. See STATE_OVERRIDING_PROP_NAMES.
|
|
73
|
+
*
|
|
74
|
+
* `disabled=false` etc. return false (the prop is set but to a falsy
|
|
75
|
+
* value, which doesn't change the visual). Only truthy values trigger
|
|
76
|
+
* fall-back.
|
|
77
|
+
*/
|
|
78
|
+
export function cvaInstanceHasOverridingState(
|
|
79
|
+
props: Record<string, unknown> | null | undefined
|
|
80
|
+
): boolean {
|
|
81
|
+
if (!props) return false;
|
|
82
|
+
for (let i = 0; i < STATE_OVERRIDING_PROP_NAMES.length; i++) {
|
|
83
|
+
if (isStateTruthy(props[STATE_OVERRIDING_PROP_NAMES[i]])) return true;
|
|
84
|
+
}
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Extract the tagName of the first JSX-element child of an instance.
|
|
90
|
+
* Used by the smart fall-back in `tryCreateCvaComponentInstance` to decide
|
|
91
|
+
* whether the instance's icon matches the master's recorded icon.
|
|
92
|
+
*
|
|
93
|
+
* Returns null when there's no element child (text-only or empty).
|
|
94
|
+
*/
|
|
95
|
+
export function getFirstElementJsxChildTagName(jsxChildren: unknown): string | null {
|
|
96
|
+
if (!Array.isArray(jsxChildren)) return null;
|
|
97
|
+
for (let i = 0; i < jsxChildren.length; i++) {
|
|
98
|
+
const c = jsxChildren[i];
|
|
99
|
+
if (c && typeof c === 'object' && (c as { type?: string }).type === 'element') {
|
|
100
|
+
const tag = (c as { tagName?: string }).tagName;
|
|
101
|
+
if (typeof tag === 'string' && tag.length > 0) return tag;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* The plugin-data key under which `cva-master` records the icon name a
|
|
109
|
+
* CVA variant master was built with. `tryCreateCvaComponentInstance` reads
|
|
110
|
+
* it to decide whether an instance with element children can safely reuse
|
|
111
|
+
* the master (icons match) or must fall back to frame rendering
|
|
112
|
+
* (icons differ).
|
|
113
|
+
*/
|
|
114
|
+
export const MASTER_ICON_NAME_KEY = 'inkbridge:master-icon-name';
|
|
115
|
+
|
|
116
|
+
function copyVariantFillsToInstance(componentSet: SceneNode | null, instanceNode: InstanceNode | null, properties: Record<string, string>): void {
|
|
117
|
+
if (!componentSet || !('children' in componentSet) || !Array.isArray(componentSet.children)) return;
|
|
118
|
+
if (!instanceNode) return;
|
|
119
|
+
const keys = Object.keys(properties);
|
|
120
|
+
if (keys.length === 0) return;
|
|
121
|
+
|
|
122
|
+
// Match variant name: `Key1=Value1, Key2=Value2`. Split on ', ' and compare
|
|
123
|
+
// as an unordered set so order differences don't break the lookup.
|
|
124
|
+
const targetTokens: Record<string, string> = {};
|
|
125
|
+
for (let i = 0; i < keys.length; i++) {
|
|
126
|
+
targetTokens[keys[i]] = properties[keys[i]];
|
|
127
|
+
}
|
|
128
|
+
let match: ComponentNode | null = null;
|
|
129
|
+
const children = componentSet.children;
|
|
130
|
+
for (let i = 0; i < children.length; i++) {
|
|
131
|
+
const child = children[i];
|
|
132
|
+
if (!child || typeof child.name !== 'string') continue;
|
|
133
|
+
const parts = child.name.split(', ');
|
|
134
|
+
const tokens: Record<string, string> = {};
|
|
135
|
+
for (let j = 0; j < parts.length; j++) {
|
|
136
|
+
const eq = parts[j].indexOf('=');
|
|
137
|
+
if (eq > 0) tokens[parts[j].slice(0, eq)] = parts[j].slice(eq + 1);
|
|
138
|
+
}
|
|
139
|
+
let allMatch = true;
|
|
140
|
+
for (let j = 0; j < keys.length; j++) {
|
|
141
|
+
if (tokens[keys[j]] !== targetTokens[keys[j]]) { allMatch = false; break; }
|
|
142
|
+
}
|
|
143
|
+
if (allMatch && child.type === 'COMPONENT') { match = child; break; }
|
|
144
|
+
}
|
|
145
|
+
if (!match) return;
|
|
146
|
+
if (!Array.isArray(match.fills) || match.fills.length === 0) return;
|
|
147
|
+
|
|
148
|
+
try {
|
|
149
|
+
instanceNode.fills = JSON.parse(JSON.stringify(match.fills));
|
|
150
|
+
} catch (_error) {
|
|
151
|
+
// Leave instance fills as-is if the override fails.
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export type InstanceBackend = {
|
|
156
|
+
enableSymbolMasters: boolean;
|
|
157
|
+
splitClassName: (value: string | undefined) => string[];
|
|
158
|
+
ensureCvaComponentSet: (def: ComponentDef, theme: string, ctx: BackendCtx) => SceneNode | null;
|
|
159
|
+
ensureStateComponentSet: (def: ComponentDef, theme: string, ctx: BackendCtx) => SceneNode | null;
|
|
160
|
+
ensureNonCvaComponentMaster: (def: ComponentDef, theme: string, ctx: BackendCtx) => SceneNode | null;
|
|
161
|
+
getInstantiableComponent: (node: SceneNode) => ComponentNode | null;
|
|
162
|
+
getCvaSelectionFromInstance: (def: ComponentDef, instance: ComponentInstance) => Record<string, string>;
|
|
163
|
+
toFigmaVariantPropertyName: (key: string) => string;
|
|
164
|
+
toFigmaVariantPropertyValue: (value: string) => string;
|
|
165
|
+
getStateVariantForInstance: (def: ComponentDef, instance: ComponentInstance) => string;
|
|
166
|
+
isCheckedInstance: (instance: ComponentInstance) => boolean;
|
|
167
|
+
hasCheckedStateVariant: (def: ComponentDef) => boolean;
|
|
168
|
+
getInstanceTextOverride: (def: ComponentDef, instance: ComponentInstance) => string | null;
|
|
169
|
+
applyTextOverrideToInstance: (instanceNode: InstanceNode, value: string) => boolean;
|
|
170
|
+
applyCompoundTextOverridesToInstance: (def: ComponentDef, story: ComponentStory, instance: ComponentInstance, instanceNode: InstanceNode) => {
|
|
171
|
+
bySlot: Record<string, string>;
|
|
172
|
+
bySubcomponent: Record<string, string>;
|
|
173
|
+
};
|
|
174
|
+
analyzeSymbolPolicy: (def: ComponentDef, instance: ComponentInstance) => {
|
|
175
|
+
reason?: string | null;
|
|
176
|
+
ignoredProps: string[];
|
|
177
|
+
slotPropMappings: Record<string, string>;
|
|
178
|
+
};
|
|
179
|
+
setGeneratedSymbolDebugData: (node: SceneNode, data: Record<string, unknown>) => void;
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
export function tryCreateCvaComponentInstance(
|
|
183
|
+
def: ComponentDef,
|
|
184
|
+
instance: ComponentInstance,
|
|
185
|
+
theme: string,
|
|
186
|
+
ctx: BackendCtx,
|
|
187
|
+
backend: InstanceBackend
|
|
188
|
+
): InstanceNode | null {
|
|
189
|
+
if (!backend.enableSymbolMasters) return null;
|
|
190
|
+
const props = instance && instance.props ? instance.props : {};
|
|
191
|
+
if (backend.splitClassName(props.className).length > 0) {
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const variantKeys = Object.keys((def && def.variants) || {});
|
|
196
|
+
if (variantKeys.length === 0) return null;
|
|
197
|
+
|
|
198
|
+
const componentSet = backend.ensureCvaComponentSet(def, theme, ctx);
|
|
199
|
+
if (!componentSet) return null;
|
|
200
|
+
|
|
201
|
+
const sourceComponent = backend.getInstantiableComponent(componentSet);
|
|
202
|
+
if (!sourceComponent) return null;
|
|
203
|
+
|
|
204
|
+
// Smart fall-back: when the instance has overriding JSX-element children
|
|
205
|
+
// (e.g. an icon child like `<Bold />`), the symbol-instance path can only
|
|
206
|
+
// produce a faithful render if the master happens to have been built with
|
|
207
|
+
// the SAME icon — Figma's instance API can't per-instance-swap SVG
|
|
208
|
+
// vectors. So compare the master's recorded icon name (stamped by
|
|
209
|
+
// cva-master when the variant was built) against the instance's first
|
|
210
|
+
// element-child tagName:
|
|
211
|
+
// - match → master would render the same icon → proceed (symbol instance)
|
|
212
|
+
// - differ → master would render the WRONG icon → return null (caller
|
|
213
|
+
// falls through to JSX-tree frame rendering so the actual
|
|
214
|
+
// instance icon is preserved)
|
|
215
|
+
// For text-only children (e.g. `<Button>Click me</Button>`), the text-
|
|
216
|
+
// override path handles per-instance content and this guard is a no-op.
|
|
217
|
+
if (cvaInstanceHasOverridingJsxChildren(props)) {
|
|
218
|
+
const masterIconName = typeof sourceComponent.getPluginData === 'function'
|
|
219
|
+
? sourceComponent.getPluginData(MASTER_ICON_NAME_KEY)
|
|
220
|
+
: '';
|
|
221
|
+
const instanceIconName = getFirstElementJsxChildTagName(props.__jsxChildren);
|
|
222
|
+
if (!masterIconName || !instanceIconName || masterIconName !== instanceIconName) {
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Secondary fall-back: even when the icon matches, an instance carrying
|
|
228
|
+
// a truthy state prop (`defaultPressed`, `defaultChecked`, etc.) can't
|
|
229
|
+
// be expressed as a symbol-instance reuse — the master was built once
|
|
230
|
+
// from one story whose state is fixed in its baked-in classes, and
|
|
231
|
+
// Figma's instance API doesn't apply per-instance `data-[pressed]:*`
|
|
232
|
+
// resolution. Fall back to frame rendering so the variant engine
|
|
233
|
+
// (DATA_ATTR_PROP_ALIASES in node-variants.ts) activates the right
|
|
234
|
+
// state styling at render time.
|
|
235
|
+
if (cvaInstanceHasOverridingState(props)) {
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
let figmaInstance: InstanceNode | null = null;
|
|
240
|
+
try {
|
|
241
|
+
figmaInstance = sourceComponent.createInstance();
|
|
242
|
+
} catch (_error) {
|
|
243
|
+
return null;
|
|
244
|
+
}
|
|
245
|
+
if (!figmaInstance) return null;
|
|
246
|
+
|
|
247
|
+
const selection = backend.getCvaSelectionFromInstance(def, instance);
|
|
248
|
+
const properties: Record<string, string> = {};
|
|
249
|
+
for (let i = 0; i < variantKeys.length; i++) {
|
|
250
|
+
const key = variantKeys[i];
|
|
251
|
+
if (!selection[key]) continue;
|
|
252
|
+
properties[backend.toFigmaVariantPropertyName(key)] = backend.toFigmaVariantPropertyValue(selection[key]);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (Object.keys(properties).length > 0) {
|
|
256
|
+
try {
|
|
257
|
+
figmaInstance.setProperties(properties);
|
|
258
|
+
} catch (_error) {
|
|
259
|
+
// Keep the default instance when property mapping fails.
|
|
260
|
+
}
|
|
261
|
+
// Figma's variant swap inherits boundVariables.color from the target variant
|
|
262
|
+
// but drops paint.opacity — so for tokens with a Tailwind alpha modifier
|
|
263
|
+
// (e.g. bg-primary/10), the instance renders at full opacity. Copy the
|
|
264
|
+
// matching variant master's fills onto the instance to restore opacity.
|
|
265
|
+
copyVariantFillsToInstance(componentSet, figmaInstance, properties);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const textOverrides: Record<string, string> = {};
|
|
269
|
+
const textOverride = backend.getInstanceTextOverride(def, instance);
|
|
270
|
+
if (textOverride) {
|
|
271
|
+
const applied = backend.applyTextOverrideToInstance(figmaInstance, textOverride);
|
|
272
|
+
if (applied) {
|
|
273
|
+
textOverrides.primary = textOverride;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
backend.setGeneratedSymbolDebugData(figmaInstance, {
|
|
278
|
+
decision: 'cva-instance',
|
|
279
|
+
ignoredProps: [],
|
|
280
|
+
slotPropMappings: {},
|
|
281
|
+
textOverrides,
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
return figmaInstance;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
export function tryCreateNonCvaComponentInstance(
|
|
288
|
+
def: ComponentDef,
|
|
289
|
+
instance: ComponentInstance,
|
|
290
|
+
theme: string,
|
|
291
|
+
ctx: BackendCtx,
|
|
292
|
+
backend: InstanceBackend,
|
|
293
|
+
story?: ComponentStory
|
|
294
|
+
): InstanceNode | null {
|
|
295
|
+
if (!backend.enableSymbolMasters) return null;
|
|
296
|
+
if (!def || def.type === 'cva') return null;
|
|
297
|
+
if (def.symbolCandidate === false) return null;
|
|
298
|
+
const props = instance && instance.props ? instance.props : {};
|
|
299
|
+
|
|
300
|
+
if (def.type === 'state' && backend.splitClassName(props.className).length === 0) {
|
|
301
|
+
const stateSet = backend.ensureStateComponentSet(def, theme, ctx);
|
|
302
|
+
const sourceComponent = stateSet ? backend.getInstantiableComponent(stateSet) : null;
|
|
303
|
+
if (sourceComponent) {
|
|
304
|
+
try {
|
|
305
|
+
const figmaInstance = sourceComponent.createInstance();
|
|
306
|
+
const requestedState = backend.getStateVariantForInstance(def, instance);
|
|
307
|
+
const requestedChecked = backend.isCheckedInstance(instance);
|
|
308
|
+
try {
|
|
309
|
+
const stateProps: Record<string, string> = {
|
|
310
|
+
[backend.toFigmaVariantPropertyName('state')]: backend.toFigmaVariantPropertyValue(requestedState),
|
|
311
|
+
};
|
|
312
|
+
if (backend.hasCheckedStateVariant(def)) {
|
|
313
|
+
stateProps[backend.toFigmaVariantPropertyName('checked')] =
|
|
314
|
+
backend.toFigmaVariantPropertyValue(String(requestedChecked));
|
|
315
|
+
}
|
|
316
|
+
figmaInstance.setProperties(stateProps);
|
|
317
|
+
} catch (_setPropertiesError) {
|
|
318
|
+
// Keep default instance when mapping fails.
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const textOverrides: Record<string, string> = {};
|
|
322
|
+
// Checked state controls (e.g. checkbox/switch) should not absorb story labels
|
|
323
|
+
// into the symbol itself; labels belong to surrounding layout.
|
|
324
|
+
if (!backend.hasCheckedStateVariant(def)) {
|
|
325
|
+
const textOverride = backend.getInstanceTextOverride(def, instance);
|
|
326
|
+
if (textOverride) {
|
|
327
|
+
const applied = backend.applyTextOverrideToInstance(figmaInstance, textOverride);
|
|
328
|
+
if (applied) textOverrides.primary = textOverride;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
backend.setGeneratedSymbolDebugData(figmaInstance, {
|
|
332
|
+
decision: 'state-instance',
|
|
333
|
+
ignoredProps: [],
|
|
334
|
+
slotPropMappings: {},
|
|
335
|
+
textOverrides,
|
|
336
|
+
});
|
|
337
|
+
return figmaInstance;
|
|
338
|
+
} catch (_error) {
|
|
339
|
+
// Fall back to raw rendering below.
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const policy = backend.analyzeSymbolPolicy(def, instance);
|
|
345
|
+
if (policy.reason) return null;
|
|
346
|
+
|
|
347
|
+
const master = backend.ensureNonCvaComponentMaster(def, theme, ctx);
|
|
348
|
+
if (!master || master.type !== 'COMPONENT') return null;
|
|
349
|
+
|
|
350
|
+
try {
|
|
351
|
+
const figmaInstance = master.createInstance();
|
|
352
|
+
const textOverrides: Record<string, string> = {};
|
|
353
|
+
if (def.type === 'compound' && story) {
|
|
354
|
+
const applied = backend.applyCompoundTextOverridesToInstance(def, story, instance, figmaInstance);
|
|
355
|
+
const appliedSlots = Object.keys(applied.bySlot);
|
|
356
|
+
if (appliedSlots.length > 0) {
|
|
357
|
+
for (let i = 0; i < appliedSlots.length; i++) {
|
|
358
|
+
const key = appliedSlots[i];
|
|
359
|
+
textOverrides[key] = applied.bySlot[key];
|
|
360
|
+
}
|
|
361
|
+
} else {
|
|
362
|
+
const appliedSubs = Object.keys(applied.bySubcomponent);
|
|
363
|
+
for (let i = 0; i < appliedSubs.length; i++) {
|
|
364
|
+
const key = appliedSubs[i];
|
|
365
|
+
textOverrides[key] = applied.bySubcomponent[key];
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
} else {
|
|
369
|
+
const textOverride = backend.getInstanceTextOverride(def, instance);
|
|
370
|
+
if (textOverride) {
|
|
371
|
+
const applied = backend.applyTextOverrideToInstance(figmaInstance, textOverride);
|
|
372
|
+
if (applied) textOverrides.primary = textOverride;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
backend.setGeneratedSymbolDebugData(figmaInstance, {
|
|
377
|
+
decision: 'instance',
|
|
378
|
+
ignoredProps: policy.ignoredProps,
|
|
379
|
+
slotPropMappings: policy.slotPropMappings,
|
|
380
|
+
textOverrides,
|
|
381
|
+
});
|
|
382
|
+
return figmaInstance;
|
|
383
|
+
} catch (_error) {
|
|
384
|
+
return null;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { findChildByName } from '../cache';
|
|
2
|
+
|
|
3
|
+
export const DESIGN_SYSTEM_PAGE_NAME = 'Design System';
|
|
4
|
+
export const COMPONENT_LIBRARY_ROOT_NAME = '__Inkbridge Component Library';
|
|
5
|
+
|
|
6
|
+
export function getInstantiableComponent(node: BaseNode | null): ComponentNode | null {
|
|
7
|
+
if (!node) return null;
|
|
8
|
+
if (node.type === 'COMPONENT') return node;
|
|
9
|
+
if (node.type === 'COMPONENT_SET') {
|
|
10
|
+
if (node.defaultVariant) return node.defaultVariant;
|
|
11
|
+
for (const candidate of node.children) {
|
|
12
|
+
if (candidate.type === 'COMPONENT') return candidate;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function findExistingComponentLibraryRoot(): FrameNode | null {
|
|
19
|
+
const pages = figma.root && Array.isArray(figma.root.children) ? figma.root.children : [];
|
|
20
|
+
for (let i = 0; i < pages.length; i++) {
|
|
21
|
+
const page = pages[i];
|
|
22
|
+
if (!page || page.type !== 'PAGE' || !Array.isArray(page.children)) {
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
const root = findChildByName(page, COMPONENT_LIBRARY_ROOT_NAME);
|
|
26
|
+
if (root && root.type === 'FRAME') return root;
|
|
27
|
+
}
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// findChildByName returns `any` at the boundary; the caller type-narrows.
|
|
32
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
33
|
+
export function findExistingThemeComponentLibrary(theme: string): any | null {
|
|
34
|
+
const root = findExistingComponentLibraryRoot();
|
|
35
|
+
if (!root) return null;
|
|
36
|
+
return findChildByName(root, String(theme || 'primary') + ' Masters');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
40
|
+
export function findExistingThemedComponentNode(theme: string, name: string): any | null {
|
|
41
|
+
const themeLibrary = findExistingThemeComponentLibrary(theme);
|
|
42
|
+
if (!themeLibrary) return null;
|
|
43
|
+
return findChildByName(themeLibrary, name);
|
|
44
|
+
}
|