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.
Files changed (178) hide show
  1. package/README.md +108 -25
  2. package/bin/inkbridge.mjs +354 -83
  3. package/code.js +40 -11802
  4. package/manifest.json +1 -0
  5. package/package.json +74 -23
  6. package/scanner/adapter-utils-regression.ts +159 -0
  7. package/scanner/aspect-percent-position-regression.ts +237 -0
  8. package/scanner/aspect-ratio-regression.ts +90 -0
  9. package/scanner/blob-placement-regression.ts +2 -2
  10. package/scanner/block-cache-regression.ts +195 -0
  11. package/scanner/bundle-size-regression.ts +50 -0
  12. package/scanner/child-sizing-matrix-regression.ts +303 -0
  13. package/scanner/cli.ts +342 -13
  14. package/scanner/component-scanner.ts +2108 -174
  15. package/scanner/component-sections-regression.ts +198 -0
  16. package/scanner/compound-classes-lookup-regression.ts +163 -0
  17. package/scanner/css-token-reader-regression.ts +7 -6
  18. package/scanner/css-token-reader.ts +152 -31
  19. package/scanner/cva-jsx-child-fallback-regression.ts +98 -0
  20. package/scanner/cva-master-icon-regression.ts +315 -0
  21. package/scanner/data-attr-prop-alias-regression.ts +129 -0
  22. package/scanner/explicit-size-root-regression.ts +102 -0
  23. package/scanner/font-family-extract-regression.ts +113 -0
  24. package/scanner/font-style-resolver-regression.ts +1 -1
  25. package/scanner/framework-adapter-shadcn-regression.ts +480 -0
  26. package/scanner/full-width-matrix-regression.ts +338 -0
  27. package/scanner/grid-cols-extraction-regression.ts +110 -0
  28. package/scanner/image-src-collector-regression.ts +204 -0
  29. package/scanner/inline-flex-regression.ts +235 -0
  30. package/scanner/input-range-regression.ts +217 -0
  31. package/scanner/instance-rendering-regression.ts +224 -0
  32. package/scanner/jsx-prop-unresolved-regression.ts +178 -0
  33. package/scanner/jsx-text-regression.ts +178 -0
  34. package/scanner/layout-alignment-regression.ts +108 -0
  35. package/scanner/layout-flex-regression.ts +90 -0
  36. package/scanner/layout-mode-regression.ts +71 -0
  37. package/scanner/layout-sizing-regression.ts +227 -0
  38. package/scanner/layout-spacing-regression.ts +135 -0
  39. package/scanner/local-const-className-regression.ts +331 -0
  40. package/scanner/percent-position-regression.ts +105 -0
  41. package/scanner/provider-cascade-regression.ts +224 -0
  42. package/scanner/provider-flatten-regression.ts +235 -0
  43. package/scanner/radial-gradient-regression.ts +1 -1
  44. package/scanner/render-prop-parser-regression.ts +161 -0
  45. package/scanner/ring-utility-regression.ts +153 -0
  46. package/scanner/sandbox-spread-regression.ts +125 -0
  47. package/scanner/selection-pressed-regression.ts +241 -0
  48. package/scanner/size-full-normalization-regression.ts +127 -0
  49. package/scanner/state-classification-regression.ts +175 -0
  50. package/scanner/story-diagnostics-regression.ts +216 -0
  51. package/scanner/story-dimensioning-regression.ts +298 -0
  52. package/scanner/story-render-strategy-regression.ts +205 -0
  53. package/scanner/stretch-to-parent-width-regression.ts +147 -0
  54. package/scanner/svg-fill-parent-regression.ts +98 -0
  55. package/scanner/svg-group-inheritance-regression.ts +166 -0
  56. package/scanner/svg-marker-inline-regression.ts +211 -0
  57. package/scanner/svg-marker-regression.ts +116 -0
  58. package/scanner/tailwind-parser.ts +46 -4
  59. package/scanner/text-resize-matrix-regression.ts +173 -0
  60. package/scanner/transform-math-regression.ts +1 -1
  61. package/scanner/types.ts +26 -2
  62. package/src/cache/frame-cache.ts +150 -0
  63. package/src/cache/index.ts +2 -0
  64. package/src/{component-defs.ts → components/component-defs.ts} +25 -10
  65. package/src/{component-gen.ts → components/component-gen.ts} +43 -116
  66. package/src/components/component-instance.ts +386 -0
  67. package/src/components/component-library.ts +44 -0
  68. package/src/components/component-lookup.ts +161 -0
  69. package/src/components/index.ts +7 -0
  70. package/src/components/scanner-types.ts +39 -0
  71. package/src/components/symbol-instance-policy.ts +312 -0
  72. package/src/design-system/block-cache.ts +130 -0
  73. package/src/design-system/component-sections.ts +107 -0
  74. package/src/design-system/cva-inference.ts +187 -0
  75. package/src/design-system/cva-master.ts +427 -0
  76. package/src/design-system/cva-utils.ts +29 -0
  77. package/src/design-system/design-system.ts +334 -0
  78. package/src/design-system/frame-stabilizers.ts +191 -0
  79. package/src/design-system/frame-utils.ts +46 -0
  80. package/src/design-system/generated-node.ts +84 -0
  81. package/src/design-system/icon-rendering.ts +229 -0
  82. package/src/design-system/index.ts +13 -0
  83. package/src/design-system/instance-rendering.ts +307 -0
  84. package/src/design-system/master-shared.ts +133 -0
  85. package/src/design-system/node-helpers.ts +237 -0
  86. package/src/design-system/node-variants.ts +196 -0
  87. package/src/design-system/non-cva-master.ts +104 -0
  88. package/src/design-system/portal-handling.ts +138 -0
  89. package/src/design-system/preview-builder.ts +738 -0
  90. package/src/{render-context.ts → design-system/render-context.ts} +32 -6
  91. package/src/design-system/render-prop-parser.ts +50 -0
  92. package/src/design-system/responsive-resolver.ts +180 -0
  93. package/src/design-system/selectable-state.ts +157 -0
  94. package/src/design-system/state-master.ts +267 -0
  95. package/src/design-system/state-utils.ts +15 -0
  96. package/src/design-system/story-builder-context.ts +40 -0
  97. package/src/design-system/story-builder.ts +1322 -0
  98. package/src/design-system/story-diagnostics.ts +80 -0
  99. package/src/design-system/story-dimensioning.ts +272 -0
  100. package/src/design-system/story-frames.ts +400 -0
  101. package/src/design-system/story-instance.ts +333 -0
  102. package/src/{story-layout.ts → design-system/story-layout.ts} +2 -2
  103. package/src/design-system/story-render-strategy.ts +150 -0
  104. package/src/design-system/story-tree-search.ts +110 -0
  105. package/src/design-system/symbol-fallback.ts +89 -0
  106. package/src/design-system/symbol-source.ts +172 -0
  107. package/src/design-system/table-helpers.ts +56 -0
  108. package/src/design-system/tag-predicates.ts +99 -0
  109. package/src/design-system/theme-context.ts +52 -0
  110. package/src/design-system/typography.ts +100 -0
  111. package/src/design-system/ui-builder.ts +2676 -0
  112. package/src/{clip-path-decorative.ts → effects/clip-path-decorative.ts} +11 -11
  113. package/src/effects/icon-builder.ts +1074 -0
  114. package/src/effects/index.ts +5 -0
  115. package/src/effects/portal-panel.ts +369 -0
  116. package/src/{radial-gradient.ts → effects/radial-gradient.ts} +1 -1
  117. package/src/framework-adapters/index.ts +47 -0
  118. package/src/framework-adapters/shadcn.ts +541 -0
  119. package/src/{github.ts → github/github.ts} +46 -21
  120. package/src/github/index.ts +1 -0
  121. package/src/layout/deferred-layout.ts +1556 -0
  122. package/src/layout/index.ts +24 -0
  123. package/src/layout/layout-parser.ts +375 -0
  124. package/src/{layout-utils.ts → layout/layout-utils.ts} +23 -17
  125. package/src/layout/parser/alignment.ts +54 -0
  126. package/src/layout/parser/flex.ts +59 -0
  127. package/src/layout/parser/index.ts +65 -0
  128. package/src/layout/parser/ir.ts +80 -0
  129. package/src/layout/parser/layout-mode.ts +57 -0
  130. package/src/layout/parser/sizing.ts +241 -0
  131. package/src/layout/parser/spacing-scale.ts +78 -0
  132. package/src/layout/parser/spacing.ts +134 -0
  133. package/src/layout/ring-utils.ts +120 -0
  134. package/src/layout/size-utils.ts +143 -0
  135. package/src/layout/text-resize-decision.ts +51 -0
  136. package/src/{width-solver.ts → layout/width-solver.ts} +168 -37
  137. package/src/main.ts +444 -162
  138. package/src/{config.ts → plugin/config.ts} +12 -12
  139. package/src/{dev-server.ts → plugin/dev-server.ts} +3 -3
  140. package/src/plugin/image-src-collector.ts +52 -0
  141. package/src/plugin/index.ts +3 -0
  142. package/src/plugin/packs/index.ts +2 -0
  143. package/src/{pack-provider.ts → plugin/packs/pack-provider.ts} +12 -12
  144. package/src/{packs.ts → plugin/packs/packs.ts} +22 -17
  145. package/src/render-engine-version.ts +2 -0
  146. package/src/tailwind/adapter-utils.ts +137 -0
  147. package/src/{class-utils.ts → tailwind/class-utils.ts} +33 -6
  148. package/src/tailwind/index.ts +8 -0
  149. package/src/tailwind/jsx-utils.ts +319 -0
  150. package/src/{node-ir.ts → tailwind/node-ir.ts} +208 -19
  151. package/src/{responsive-analyzer.ts → tailwind/responsive-analyzer.ts} +32 -2
  152. package/src/{state-analyzer.ts → tailwind/state-analyzer.ts} +71 -5
  153. package/src/{tailwind.ts → tailwind/tailwind.ts} +423 -674
  154. package/src/{utility-resolver.ts → tailwind/utility-resolver.ts} +27 -6
  155. package/src/{font-style-resolver.ts → text/font-style-resolver.ts} +0 -2
  156. package/src/text/index.ts +4 -0
  157. package/src/{inline-text.ts → text/inline-text.ts} +13 -13
  158. package/src/{text-builder.ts → text/text-builder.ts} +24 -7
  159. package/src/{text-line.ts → text/text-line.ts} +2 -2
  160. package/src/{change-detection.ts → tokens/change-detection.ts} +12 -12
  161. package/src/{color-resolver.ts → tokens/color-resolver.ts} +1 -6
  162. package/src/{colors.ts → tokens/colors.ts} +13 -6
  163. package/src/tokens/index.ts +6 -0
  164. package/src/{token-source.ts → tokens/token-source.ts} +4 -1
  165. package/src/{tokens.ts → tokens/tokens.ts} +116 -20
  166. package/src/{variables.ts → tokens/variables.ts} +447 -102
  167. package/templates/patch-tokens-route.ts +25 -6
  168. package/templates/scan-components-route.ts +26 -5
  169. package/ui.html +485 -37
  170. package/src/component-lookup.ts +0 -82
  171. package/src/design-system.ts +0 -59
  172. package/src/icon-builder.ts +0 -607
  173. package/src/layout-parser.ts +0 -667
  174. package/src/story-builder.ts +0 -1706
  175. package/src/ui-builder.ts +0 -1996
  176. /package/src/{image-cache.ts → cache/image-cache.ts} +0 -0
  177. /package/src/{blob-placement.ts → effects/blob-placement.ts} +0 -0
  178. /package/src/{transform-math.ts → tailwind/transform-math.ts} +0 -0
@@ -0,0 +1,427 @@
1
+ import { splitClassName, applyTailwindStylesToFrame, tailwindClassesToStyle, type TailwindStyle } from '../tailwind';
2
+ import { parseColor, bindColorVariable } from '../tokens';
3
+ import { createTextNode } from '../text';
4
+ import { getRingInfoFromClasses, markRingNode, applyRingIfPossible, enforceFixedBoxSizingAfterLayout } from '../layout';
5
+ import { MASTER_ICON_NAME_KEY } from '../components/component-instance';
6
+ import { findChildByName, getFrameHash, setFrameHash, hashString, hashDef } from '../cache';
7
+ import { RENDER_ENGINE_VERSION } from '../render-engine-version';
8
+ import { tagGeneratedNode } from './generated-node';
9
+ import { getThemeContext } from './theme-context';
10
+ import {
11
+ ENABLE_SYMBOL_MASTERS,
12
+ toFigmaVariantPropertyName,
13
+ toFigmaVariantPropertyValue,
14
+ ensureThemeComponentLibrary,
15
+ } from './master-shared';
16
+ import { buildCvaClassesWithSelection } from './cva-utils';
17
+ import { getPreviewTypography } from './typography';
18
+ import { buildCvaSymbolSourceNode } from './symbol-source';
19
+ import type { StoryBuilderContext } from './story-builder-context';
20
+
21
+ /**
22
+ * CVA-master logic: enumerating Class-Variance-Authority variant
23
+ * combinations and producing a Figma component set per design-token theme.
24
+ *
25
+ * Shared concepts live in sibling utility modules so this file can import
26
+ * its dependencies directly without callback indirection:
27
+ * - buildCvaClassesWithSelection → cva-utils.ts
28
+ * - getPreviewTypography → typography.ts
29
+ * - buildCvaSymbolSourceNode → symbol-source.ts
30
+ *
31
+ * Re-exported here for back-compat with story-builder's existing imports.
32
+ */
33
+ export { buildCvaClassesWithSelection };
34
+
35
+ export const MAX_CVA_MASTER_COMBINATIONS = 96;
36
+
37
+ export function applyStyleToFrame(frame: FrameNode, style: TailwindStyle, theme: string): void {
38
+ if (!style) return;
39
+
40
+ if (style.bg) {
41
+ const bgBound = style.bgToken && bindColorVariable(frame, style.bgToken, 'fill', theme);
42
+ if (bgBound) {
43
+ if (style.bgOpacity != null && Array.isArray(frame.fills) && frame.fills.length > 0) {
44
+ const nextFills = JSON.parse(JSON.stringify(frame.fills));
45
+ nextFills[0].opacity = style.bgOpacity;
46
+ frame.fills = nextFills;
47
+ }
48
+ } else {
49
+ const bg = parseColor(style.bg);
50
+ const bgOpacity = style.bgOpacity != null ? style.bgOpacity : (bg.a == null ? 1 : bg.a);
51
+ frame.fills = [{ type: 'SOLID', color: { r: bg.r, g: bg.g, b: bg.b }, opacity: bgOpacity }];
52
+ }
53
+ }
54
+
55
+ if (style.border) {
56
+ const borderBound = style.borderToken && bindColorVariable(frame, style.borderToken, 'stroke', theme);
57
+ if (!borderBound) {
58
+ const borderColor = parseColor(style.border);
59
+ frame.strokes = [{ type: 'SOLID', color: { r: borderColor.r, g: borderColor.g, b: borderColor.b } }];
60
+ }
61
+ frame.strokeWeight = 1;
62
+ }
63
+
64
+ if (style.opacity != null) {
65
+ frame.opacity = style.opacity;
66
+ }
67
+ }
68
+
69
+ function normalizeVariantLookupValue(rawValue: unknown): string {
70
+ return String(rawValue == null ? '' : rawValue).trim().toLowerCase();
71
+ }
72
+
73
+ function normalizeVariantOptionValue(rawValue: unknown): string {
74
+ let value = String(rawValue == null ? '' : rawValue).trim();
75
+ if (
76
+ (value.startsWith('"') && value.endsWith('"'))
77
+ || (value.startsWith("'") && value.endsWith("'"))
78
+ ) {
79
+ value = value.slice(1, -1).trim();
80
+ }
81
+ return value.toLowerCase();
82
+ }
83
+
84
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
85
+ function resolveCvaVariantValue(def: any, variantKey: string, candidate: unknown): string | null {
86
+ const values = def && def.variants && def.variants[variantKey];
87
+ if (!Array.isArray(values) || values.length === 0) return null;
88
+ const normalizedCandidate = normalizeVariantLookupValue(candidate);
89
+ if (!normalizedCandidate) return null;
90
+ for (let i = 0; i < values.length; i++) {
91
+ const option = values[i];
92
+ if (
93
+ normalizeVariantLookupValue(option) === normalizedCandidate
94
+ || normalizeVariantOptionValue(option) === normalizedCandidate
95
+ ) {
96
+ return String(option);
97
+ }
98
+ }
99
+ return null;
100
+ }
101
+
102
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
103
+ function getDefaultCvaVariantValue(def: any, variantKey: string): string | null {
104
+ const fromDefault = resolveCvaVariantValue(
105
+ def,
106
+ variantKey,
107
+ def && def.defaultVariants ? def.defaultVariants[variantKey] : undefined
108
+ );
109
+ if (fromDefault) return fromDefault;
110
+ const values = def && def.variants && def.variants[variantKey];
111
+ if (!Array.isArray(values) || values.length === 0) return null;
112
+ return String(values[0]);
113
+ }
114
+
115
+ export function isTruthyVariantValue(rawValue: unknown): boolean {
116
+ const lowered = normalizeVariantLookupValue(rawValue);
117
+ return lowered === 'true' || lowered === '1' || lowered === 'yes' || lowered === 'on';
118
+ }
119
+
120
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
121
+ function inferCvaVariantValueFromClassName(def: any, variantKey: string, className: unknown): string | null {
122
+ const classTokens = splitClassName(className == null ? '' : String(className));
123
+ if (classTokens.length === 0) return null;
124
+ const present: Record<string, boolean> = {};
125
+ for (let i = 0; i < classTokens.length; i++) {
126
+ present[classTokens[i]] = true;
127
+ }
128
+
129
+ const values = def && def.variants && def.variants[variantKey];
130
+ if (!Array.isArray(values) || values.length === 0) return null;
131
+
132
+ let bestValue: string | null = null;
133
+ let bestScore = 0;
134
+ let tied = false;
135
+
136
+ for (let i = 0; i < values.length; i++) {
137
+ const value = String(values[i]);
138
+ const valueClasses = def && def.variantClasses && def.variantClasses[variantKey] && def.variantClasses[variantKey][value];
139
+ if (!Array.isArray(valueClasses) || valueClasses.length === 0) continue;
140
+
141
+ let score = 0;
142
+ for (let j = 0; j < valueClasses.length; j++) {
143
+ const token = String(valueClasses[j] || '').trim();
144
+ if (!token) continue;
145
+ if (present[token]) score++;
146
+ }
147
+
148
+ if (score > bestScore) {
149
+ bestScore = score;
150
+ bestValue = value;
151
+ tied = false;
152
+ } else if (score > 0 && score === bestScore) {
153
+ tied = true;
154
+ }
155
+ }
156
+
157
+ if (bestScore <= 0 || tied || !bestValue) return null;
158
+ return bestValue;
159
+ }
160
+
161
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
162
+ export function getCvaSelectionFromInstance(def: any, instance: any): Record<string, string> {
163
+ const props = instance && instance.props ? instance.props : {};
164
+ const selection: Record<string, string> = {};
165
+ const variantKeys = Object.keys((def && def.variants) || {});
166
+ for (let i = 0; i < variantKeys.length; i++) {
167
+ const key = variantKeys[i];
168
+ const resolved =
169
+ resolveCvaVariantValue(def, key, props[key])
170
+ || inferCvaVariantValueFromClassName(def, key, props.className)
171
+ || getDefaultCvaVariantValue(def, key);
172
+ if (resolved) selection[key] = resolved;
173
+ }
174
+ return selection;
175
+ }
176
+
177
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
178
+ export function countCvaVariantCombinations(def: any): number {
179
+ const variantKeys = Object.keys((def && def.variants) || {});
180
+ if (variantKeys.length === 0) return 0;
181
+ let total = 1;
182
+ for (let i = 0; i < variantKeys.length; i++) {
183
+ const key = variantKeys[i];
184
+ const values = def && def.variants && def.variants[key];
185
+ if (!Array.isArray(values) || values.length === 0) return 0;
186
+ total *= values.length;
187
+ }
188
+ return total;
189
+ }
190
+
191
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
192
+ function enumerateCvaVariantSelections(def: any): Record<string, string>[] {
193
+ const variantKeys = Object.keys((def && def.variants) || {});
194
+ const out: Record<string, string>[] = [];
195
+ if (variantKeys.length === 0) return out;
196
+
197
+ function walk(index: number, current: Record<string, string>): void {
198
+ if (index >= variantKeys.length) {
199
+ out.push(Object.assign({}, current));
200
+ return;
201
+ }
202
+ const key = variantKeys[index];
203
+ const values = def && def.variants && def.variants[key];
204
+ if (!Array.isArray(values) || values.length === 0) return;
205
+ for (let i = 0; i < values.length; i++) {
206
+ current[key] = String(values[i]);
207
+ walk(index + 1, current);
208
+ }
209
+ delete current[key];
210
+ }
211
+
212
+ walk(0, {});
213
+ return out;
214
+ }
215
+
216
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
217
+ export function buildCvaMasterHash(def: any, theme: string): string {
218
+ const themeContext = getThemeContext(theme);
219
+ return hashString(
220
+ hashDef(def)
221
+ + ':'
222
+ + theme
223
+ + ':cva-master-v3:'
224
+ + RENDER_ENGINE_VERSION
225
+ + ':'
226
+ + JSON.stringify({
227
+ colorGroup: themeContext.colorGroup,
228
+ radiusGroup: themeContext.radiusGroup,
229
+ })
230
+ );
231
+ }
232
+
233
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
234
+ export function shouldCreateCvaMaster(def: any): boolean {
235
+ if (!def || def.type !== 'cva') return false;
236
+ if (def.symbolCandidate === false) return false;
237
+ const variantKeys = Object.keys((def && def.variants) || {});
238
+ if (variantKeys.length === 0) return false;
239
+ const combinations = countCvaVariantCombinations(def);
240
+ return combinations > 0 && combinations <= MAX_CVA_MASTER_COMBINATIONS;
241
+ }
242
+
243
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
244
+ export function getCvaSymbolFallbackReason(def: any, instance: any): string {
245
+ const props = instance && instance.props ? instance.props : {};
246
+ if (splitClassName(props.className).length > 0) return 'class_override';
247
+ if (def && def.symbolCandidate === false) return 'symbol_candidate_disabled';
248
+ const variantKeys = Object.keys((def && def.variants) || {});
249
+ if (variantKeys.length === 0) return 'missing_variants';
250
+ const combinations = countCvaVariantCombinations(def);
251
+ if (combinations <= 0) return 'invalid_variant_combinations';
252
+ if (combinations > MAX_CVA_MASTER_COMBINATIONS) return 'variant_combination_limit';
253
+ return 'component_set_or_instance_unavailable';
254
+ }
255
+
256
+ export function ensureCvaComponentSet(
257
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
258
+ def: any,
259
+ theme: string,
260
+ ctx: StoryBuilderContext
261
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
262
+ ): any | null {
263
+ if (!ENABLE_SYMBOL_MASTERS) return null;
264
+ if (!def || def.type !== 'cva') return null;
265
+ if (def.symbolCandidate === false) return null;
266
+
267
+ const variantKeys = Object.keys((def && def.variants) || {});
268
+ if (variantKeys.length === 0) return null;
269
+
270
+ const combinationCount = countCvaVariantCombinations(def);
271
+ if (combinationCount <= 0 || combinationCount > MAX_CVA_MASTER_COMBINATIONS) {
272
+ return null;
273
+ }
274
+
275
+ const themeLibrary = ensureThemeComponentLibrary(theme);
276
+ if (!themeLibrary) return null;
277
+
278
+ const setName = def.name + ' [' + theme + ']';
279
+ const masterHash = buildCvaMasterHash(def, theme);
280
+ const existing = findChildByName(themeLibrary, setName);
281
+ if (existing && existing.type === 'COMPONENT_SET' && getFrameHash(existing) === masterHash) {
282
+ return existing;
283
+ }
284
+ if (existing) {
285
+ existing.remove();
286
+ }
287
+
288
+ const themeContext = getThemeContext(theme);
289
+ const colorGroup = themeContext.colorGroup;
290
+ const radiusGroup = themeContext.radiusGroup;
291
+ const selections = enumerateCvaVariantSelections(def);
292
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
293
+ const components: any[] = [];
294
+
295
+ for (let i = 0; i < selections.length; i++) {
296
+ const selection = selections[i];
297
+ const classes = buildCvaClassesWithSelection(def, selection, '');
298
+
299
+ const comp = figma.createComponent();
300
+ comp.name = variantKeys
301
+ .map(function(key: string) {
302
+ return toFigmaVariantPropertyName(key) + '=' + toFigmaVariantPropertyValue(selection[key]);
303
+ })
304
+ .join(', ');
305
+ comp.primaryAxisSizingMode = 'AUTO';
306
+ comp.counterAxisSizingMode = 'AUTO';
307
+ comp.counterAxisAlignItems = 'CENTER';
308
+ comp.primaryAxisAlignItems = 'CENTER';
309
+ comp.itemSpacing = 8;
310
+ comp.fills = [];
311
+ comp.strokes = [];
312
+ // ComponentNode and FrameNode share the auto-layout / fills / strokes
313
+ // members these helpers touch. Cast deliberately at the call boundary.
314
+ ctx.applyClipBehavior(comp as unknown as FrameNode, classes);
315
+
316
+ const storyNode = buildCvaSymbolSourceNode(def, selection, theme, ctx);
317
+ if (storyNode) {
318
+ comp.layoutMode = 'VERTICAL';
319
+ comp.primaryAxisSizingMode = 'AUTO';
320
+ comp.counterAxisSizingMode = 'AUTO';
321
+ comp.counterAxisAlignItems = 'MIN';
322
+ comp.primaryAxisAlignItems = 'MIN';
323
+ comp.itemSpacing = 0;
324
+ comp.appendChild(storyNode);
325
+ // Propagate the icon name (stamped by buildCvaSymbolSourceNode) from
326
+ // the rendered sourceNode up to the master variant Component. The
327
+ // CVA-instance creator reads this in `tryCreateCvaComponentInstance`
328
+ // to decide whether an instance with element children (an icon)
329
+ // matches the master and can safely reuse it.
330
+ try {
331
+ const iconName = typeof (storyNode as SceneNode).getPluginData === 'function'
332
+ ? (storyNode as SceneNode).getPluginData(MASTER_ICON_NAME_KEY)
333
+ : '';
334
+ if (iconName) {
335
+ comp.setPluginData(MASTER_ICON_NAME_KEY, iconName);
336
+ }
337
+ } catch (_e) {
338
+ // ignore plugin-data access errors
339
+ }
340
+ // Set the inner storyNode to "Fill" horizontally so that when an
341
+ // instance of this master is resized by a stretching parent (e.g.
342
+ // shadcn DialogFooter's mobile `flex flex-col-reverse`, which gives
343
+ // children CSS `align-items: stretch`), the visible button pill
344
+ // follows the instance's new width. Equivalent to a designer
345
+ // manually switching the inner frame's W sizing to "Fill" in Figma's
346
+ // auto-layout panel. `layoutSizingHorizontal = 'FILL'` is the modern
347
+ // Figma API that the UI "Fill" toggle uses; `layoutAlign = 'STRETCH'`
348
+ // is the legacy equivalent and doesn't always propagate through HUG
349
+ // comp parents. Masters still hug at rest because the comp's
350
+ // counter-axis remains AUTO — FILL only activates width propagation
351
+ // when something pins the outer comp to an explicit size.
352
+ try {
353
+ if ('layoutSizingHorizontal' in storyNode) {
354
+ storyNode.layoutSizingHorizontal = 'FILL';
355
+ } else if ('layoutAlign' in storyNode) {
356
+ storyNode.layoutAlign = 'STRETCH';
357
+ }
358
+ } catch (_err) {
359
+ // ignore if plugin runtime rejects the assignment
360
+ }
361
+ } else {
362
+ // applyTailwindStylesToFrame / markRingNode / applyStyleToFrame all
363
+ // touch the shared frame mixins (fills, strokes, opacity, layout). Cast
364
+ // ComponentNode → FrameNode at the boundary; no `any`.
365
+ const compFrame = comp as unknown as FrameNode;
366
+ applyTailwindStylesToFrame(compFrame, classes, colorGroup, radiusGroup, theme);
367
+ const disabledVariantKey = variantKeys.find(function(key: string) {
368
+ return key.toLowerCase() === 'disabled';
369
+ });
370
+ const state = disabledVariantKey && isTruthyVariantValue(selection[disabledVariantKey])
371
+ ? 'disabled'
372
+ : 'default';
373
+ const style = tailwindClassesToStyle(classes, state, colorGroup);
374
+ applyStyleToFrame(compFrame, style, theme);
375
+
376
+ const typography = getPreviewTypography(classes);
377
+ const textColor = style.text
378
+ ? parseColor(style.text)
379
+ : (colorGroup.foreground ? parseColor(colorGroup.foreground) : { r: 0, g: 0, b: 0 });
380
+ const text = createTextNode(def.name, {
381
+ bold: typography.bold,
382
+ fontSize: typography.fontSize,
383
+ fill: textColor,
384
+ });
385
+ if (style.underline && text.textDecoration !== undefined) {
386
+ text.textDecoration = 'UNDERLINE';
387
+ }
388
+ comp.appendChild(text);
389
+ const ringInfo = getRingInfoFromClasses(classes, colorGroup);
390
+ if (ringInfo) markRingNode(compFrame, ringInfo);
391
+ }
392
+
393
+ enforceFixedBoxSizingAfterLayout(comp, classes);
394
+
395
+ // combineAsVariants expects components to exist in the document tree.
396
+ themeLibrary.appendChild(comp);
397
+ // Comp is now in the theme library and its sizing has been finalised by
398
+ // `enforceFixedBoxSizingAfterLayout` — apply the (marked) ring overlay.
399
+ applyRingIfPossible(comp as unknown as FrameNode, themeLibrary);
400
+ components.push(comp);
401
+ }
402
+
403
+ if (components.length === 0) {
404
+ return null;
405
+ }
406
+
407
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
408
+ let componentSet: any = null;
409
+ try {
410
+ componentSet = figma.combineAsVariants(components, themeLibrary);
411
+ } catch (_error) {
412
+ for (let i = 0; i < components.length; i++) {
413
+ try {
414
+ components[i].remove();
415
+ } catch (_cleanupError) {
416
+ // ignore cleanup errors
417
+ }
418
+ }
419
+ return null;
420
+ }
421
+
422
+ componentSet.name = setName;
423
+ componentSet.visible = false;
424
+ setFrameHash(componentSet, masterHash);
425
+ tagGeneratedNode(componentSet, 'cva-component-set:' + def.name + ':' + theme);
426
+ return componentSet;
427
+ }
@@ -0,0 +1,29 @@
1
+ import { splitClassName } from '../tailwind';
2
+
3
+ /**
4
+ * Pure CVA-class assembly. Takes a component def, a variant selection,
5
+ * and an optional className override; returns the resolved class list.
6
+ *
7
+ * Lifted out of cva-master.ts so symbol-source.ts can import it without
8
+ * cycling back through cva-master (which imports buildCvaSymbolSourceNode
9
+ * from symbol-source.ts).
10
+ */
11
+ export function buildCvaClassesWithSelection(
12
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
13
+ def: any,
14
+ selection: Record<string, string>,
15
+ extraClassName: unknown
16
+ ): string[] {
17
+ let classes = def && def.baseClasses ? def.baseClasses.slice() : [];
18
+ const variantKeys = Object.keys((def && def.variants) || {});
19
+ for (let i = 0; i < variantKeys.length; i++) {
20
+ const key = variantKeys[i];
21
+ const value = selection[key];
22
+ if (!value) continue;
23
+ if (def && def.variantClasses && def.variantClasses[key] && def.variantClasses[key][value]) {
24
+ classes = classes.concat(def.variantClasses[key][value]);
25
+ }
26
+ }
27
+ classes = classes.concat(splitClassName(extraClassName == null ? undefined : String(extraClassName)));
28
+ return classes;
29
+ }