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
@@ -6,13 +6,13 @@ import {
6
6
  type ScannedThemeTokens,
7
7
  type ScannedTokenMap,
8
8
  type TokenSourceMode,
9
- } from '../src/token-source';
9
+ } from '../src/tokens/token-source';
10
10
 
11
11
  const CSS_DISCOVERY_PATHS = [
12
- 'src/app/tokens.css',
13
12
  'src/app/globals.css',
14
13
  'app/globals.css',
15
14
  'styles/globals.css',
15
+ 'src/app/tokens.css',
16
16
  ];
17
17
 
18
18
  const DEFAULT_DTCG_PATH = 'design-tokens/tokens.dtcg.json';
@@ -32,8 +32,69 @@ type TargetTokenGroup = {
32
32
  spacing: Record<string, number>;
33
33
  fontSize: Record<string, number>;
34
34
  shadows: Record<string, string>;
35
+ breakpoints: Record<string, number>;
35
36
  };
36
37
 
38
+ // The Tailwind default theme ships ~240 color shades (red-50, blue-500, …) plus
39
+ // `black` and `white`. They bloat the Figma Variables collection and the design
40
+ // tokens overview without giving designers anything actionable — semantic
41
+ // overrides like --primary, --background, etc. carry the real intent. Drop the
42
+ // raw scale; user-defined tokens that happen to share these names are an
43
+ // accepted edge case.
44
+ const TAILWIND_DEFAULT_COLOR_SCALE = /^(red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose|slate|gray|zinc|neutral|stone|mauve|olive|mist|taupe)-(50|100|200|300|400|500|600|700|800|900|950)$/;
45
+ const TAILWIND_DEFAULT_COLOR_NAMES = new Set(['black', 'white']);
46
+
47
+ function isTailwindDefaultColorKey(key: string): boolean {
48
+ return TAILWIND_DEFAULT_COLOR_SCALE.test(key) || TAILWIND_DEFAULT_COLOR_NAMES.has(key);
49
+ }
50
+
51
+ // CSS variables that appear in Tailwind's default theme but are not design
52
+ // tokens we surface (font weights, easing curves, animations, container sizes,
53
+ // tracking/leading metadata, blur scales, internal `--default-*` pointers,
54
+ // and *-shadow effect variants like inset/drop/text). Skipping these keeps the
55
+ // plugin focused on tokens designers actually edit.
56
+ const SKIP_PREFIXES = [
57
+ 'font-weight-',
58
+ 'font-feature-settings-',
59
+ 'font-variation-settings-',
60
+ 'inset-shadow-',
61
+ 'drop-shadow-',
62
+ 'text-shadow-',
63
+ 'ease-',
64
+ 'animate-',
65
+ // tw-animate-css helper variables — animation timing/duration/etc., not design tokens.
66
+ 'animation-',
67
+ // tw-animate-css percentage helpers used by keyframe utilities.
68
+ 'percentage-',
69
+ 'blur-',
70
+ 'perspective-',
71
+ 'aspect-',
72
+ 'default-',
73
+ 'tracking-',
74
+ 'leading-',
75
+ 'container-',
76
+ ];
77
+
78
+ // Single-name leftovers from Tailwind's deprecated `@theme default inline reference`
79
+ // block (no suffix). They would otherwise fall through to the catch-all and pollute
80
+ // the color group.
81
+ const SKIP_EXACT_NAMES = new Set([
82
+ 'blur',
83
+ 'drop-shadow',
84
+ 'max-width-prose',
85
+ ]);
86
+
87
+ function shouldSkipVariableName(name: string): boolean {
88
+ if (SKIP_EXACT_NAMES.has(name)) return true;
89
+ for (const prefix of SKIP_PREFIXES) {
90
+ if (name.startsWith(prefix)) return true;
91
+ }
92
+ // Tailwind also emits `--text-{size}--line-height` siblings that are calc
93
+ // expressions — they are metadata, not standalone tokens.
94
+ if (/^text-.+--line-height$/.test(name)) return true;
95
+ return false;
96
+ }
97
+
37
98
  function toDisplayPath(projectRoot: string, filePath: string): string {
38
99
  const rel = path.relative(projectRoot, filePath);
39
100
  if (!rel || rel.startsWith('..')) return filePath;
@@ -63,6 +124,37 @@ function extractImportSpecifier(params: string): string | null {
63
124
  return null;
64
125
  }
65
126
 
127
+ function resolveBareSpecifierFromNodeModules(baseFilePath: string, specifier: string): string | null {
128
+ let dir = path.dirname(baseFilePath);
129
+ while (true) {
130
+ const pkgRoot = path.join(dir, 'node_modules', specifier);
131
+ if (fs.existsSync(pkgRoot)) {
132
+ const pkgJsonPath = path.join(pkgRoot, 'package.json');
133
+ if (fs.existsSync(pkgJsonPath)) {
134
+ try {
135
+ const pkg = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf-8'));
136
+ // Prefer "style" field (CSS entry), fall back to "main" if it ends in .css.
137
+ if (typeof pkg.style === 'string' && pkg.style) {
138
+ const stylePath = path.join(pkgRoot, pkg.style);
139
+ if (fs.existsSync(stylePath)) return stylePath;
140
+ }
141
+ if (typeof pkg.main === 'string' && pkg.main && pkg.main.endsWith('.css')) {
142
+ const mainPath = path.join(pkgRoot, pkg.main);
143
+ if (fs.existsSync(mainPath)) return mainPath;
144
+ }
145
+ } catch {
146
+ // Malformed package.json — fall through to index.css.
147
+ }
148
+ }
149
+ const indexCss = path.join(pkgRoot, 'index.css');
150
+ if (fs.existsSync(indexCss)) return indexCss;
151
+ }
152
+ const parent = path.dirname(dir);
153
+ if (parent === dir) return null;
154
+ dir = parent;
155
+ }
156
+ }
157
+
66
158
  function resolveImportedCssPath(baseFilePath: string, params: string): string | null {
67
159
  const specifier = extractImportSpecifier(params);
68
160
  if (!specifier) return null;
@@ -80,10 +172,23 @@ function resolveImportedCssPath(baseFilePath: string, params: string): string |
80
172
  const direct = path.resolve(fromDir, specifier);
81
173
  if (fs.existsSync(direct)) return direct;
82
174
  if (!path.extname(direct) && fs.existsSync(direct + '.css')) return direct + '.css';
175
+
176
+ // Bare specifiers (e.g. `@import "tailwindcss"`) resolve via node_modules so
177
+ // we can pull Tailwind's default `@theme` block — breakpoints, default text
178
+ // sizes, etc. — into the scanned token map.
179
+ const isBare = !specifier.startsWith('./') && !specifier.startsWith('../') && !path.isAbsolute(specifier);
180
+ if (isBare) {
181
+ const nm = resolveBareSpecifierFromNodeModules(baseFilePath, specifier);
182
+ if (nm) return nm;
183
+ }
83
184
  return null;
84
185
  }
85
186
 
86
187
  function cssHasTokenDeclarations(cssText: string): boolean {
188
+ // Only count declarations inside :root {} or .[theme] {} rules — the same
189
+ // selectors that patchCssVariables targets. We intentionally skip @theme
190
+ // at-rules (Tailwind v4 utility mappings) because those contain var()
191
+ // references, not source values, and the patcher cannot update them.
87
192
  try {
88
193
  const root = postcss.parse(cssText);
89
194
  let found = false;
@@ -91,16 +196,9 @@ function cssHasTokenDeclarations(cssText: string): boolean {
91
196
  if (found) return;
92
197
  if (!decl.prop || !decl.prop.startsWith('--')) return;
93
198
  const parent = decl.parent;
94
- if (!parent) return;
95
- if (parent.type === 'atrule') {
96
- const at = parent as AtRule;
97
- if ((at.name || '').toLowerCase() === 'theme') found = true;
98
- return;
99
- }
100
- if (parent.type === 'rule') {
101
- const selector = (parent as Rule).selector || '';
102
- if (parseThemeSelectors(selector).length > 0) found = true;
103
- }
199
+ if (!parent || parent.type !== 'rule') return;
200
+ const selector = (parent as Rule).selector || '';
201
+ if (parseThemeSelectors(selector).length > 0) found = true;
104
202
  });
105
203
  return found;
106
204
  } catch {
@@ -108,7 +206,7 @@ function cssHasTokenDeclarations(cssText: string): boolean {
108
206
  }
109
207
  }
110
208
 
111
- function resolveCssTokenPathFromImports(filePath: string, visited: Set<string> = new Set()): string {
209
+ export function resolveCssTokenPathFromImports(filePath: string, visited: Set<string> = new Set()): string {
112
210
  const absolute = path.resolve(filePath);
113
211
  if (visited.has(absolute)) return absolute;
114
212
  visited.add(absolute);
@@ -179,12 +277,11 @@ function readCssWithImports(filePath: string, visited: Set<string> = new Set()):
179
277
 
180
278
  export function discoverCssTokenPath(projectRoot: string, explicitPath?: string): string | null {
181
279
  if (explicitPath && explicitPath.trim()) {
182
- const explicit = discoverFilePath(projectRoot, explicitPath.trim());
183
- return explicit ? resolveCssTokenPathFromImports(explicit) : null;
280
+ return discoverFilePath(projectRoot, explicitPath.trim());
184
281
  }
185
282
  for (const rel of CSS_DISCOVERY_PATHS) {
186
283
  const found = discoverFilePath(projectRoot, rel);
187
- if (found) return resolveCssTokenPathFromImports(found);
284
+ if (found) return found;
188
285
  }
189
286
  return null;
190
287
  }
@@ -235,6 +332,7 @@ function getTargetGroup(map: ScannedTokenMap, themeName: string): TargetTokenGro
235
332
  spacing: map.spacing,
236
333
  fontSize: map.fontSize,
237
334
  shadows: map.shadows,
335
+ breakpoints: map.breakpoints,
238
336
  };
239
337
  }
240
338
  const theme = ensureThemeContainer(map, themeName);
@@ -243,6 +341,7 @@ function getTargetGroup(map: ScannedTokenMap, themeName: string): TargetTokenGro
243
341
  if (!theme.spacing) theme.spacing = {};
244
342
  if (!theme.fontSize) theme.fontSize = {};
245
343
  if (!theme.shadows) theme.shadows = {};
344
+ if (!theme.breakpoints) theme.breakpoints = {};
246
345
  return {
247
346
  colors: theme.colors,
248
347
  radius: theme.radius,
@@ -250,6 +349,7 @@ function getTargetGroup(map: ScannedTokenMap, themeName: string): TargetTokenGro
250
349
  spacing: theme.spacing,
251
350
  fontSize: theme.fontSize,
252
351
  shadows: theme.shadows,
352
+ breakpoints: theme.breakpoints,
253
353
  };
254
354
  }
255
355
 
@@ -259,14 +359,22 @@ function parseThemeSelectors(selector: string): string[] {
259
359
  if (!raw) return [];
260
360
  const normalized = raw.toLowerCase();
261
361
 
362
+ // data-theme attribute approach: :root[data-theme="secondary"]
262
363
  const dataThemeMatches = normalized.matchAll(/\[data-theme\s*=\s*["']?([^"'\\\]]+)["']?\]/g);
263
364
  for (const match of dataThemeMatches) {
264
365
  const themeName = String(match[1] || '').trim();
265
366
  if (themeName) themes.add(themeName);
266
367
  }
267
368
 
268
- if (/(\.|:)(dark)\b/.test(normalized)) {
269
- themes.add('dark');
369
+ // Class-based brand themes: .secondary — skip compound .dark.X selectors
370
+ // so dark-mode overrides don't pollute the brand theme list.
371
+ const hasDarkClass = /(?:^|[\s.])dark\b/.test(normalized);
372
+ if (!hasDarkClass) {
373
+ const classMatches = normalized.matchAll(/\.([a-z][a-z0-9-]*)/g);
374
+ for (const match of classMatches) {
375
+ const name = String(match[1] || '').trim();
376
+ if (name && name !== 'dark' && name !== 'root') themes.add(name);
377
+ }
270
378
  }
271
379
 
272
380
  if (normalized.includes(':root') && themes.size === 0) {
@@ -276,18 +384,23 @@ function parseThemeSelectors(selector: string): string[] {
276
384
  return Array.from(themes);
277
385
  }
278
386
 
279
- type VariableCategory = 'color' | 'radius' | 'font' | 'spacing' | 'fontSize' | 'shadow';
387
+ type VariableCategory = 'color' | 'radius' | 'font' | 'spacing' | 'fontSize' | 'shadow' | 'breakpoint';
280
388
 
281
389
  function classifyVariable(rawName: string): { category: VariableCategory; key: string } | null {
282
390
  const name = String(rawName || '').trim().replace(/^--/, '');
283
391
  if (!name) return null;
284
392
 
393
+ if (shouldSkipVariableName(name)) return null;
394
+
285
395
  if (name === 'radius') return { category: 'radius', key: 'base' };
286
396
  if (name.startsWith('radius-')) return { category: 'radius', key: name.slice('radius-'.length) };
287
397
 
398
+ if (name.startsWith('breakpoint-')) return { category: 'breakpoint', key: name.slice('breakpoint-'.length) };
399
+
288
400
  if (name.startsWith('font-family-')) return { category: 'font', key: name.slice('font-family-'.length) };
289
401
  if (name.startsWith('font-') && !name.startsWith('font-size-')) return { category: 'font', key: name.slice('font-'.length) };
290
402
 
403
+ if (name === 'spacing') return { category: 'spacing', key: 'base' };
291
404
  if (name.startsWith('spacing-')) return { category: 'spacing', key: name.slice('spacing-'.length) };
292
405
  if (name.startsWith('text-')) return { category: 'fontSize', key: name.slice('text-'.length) };
293
406
  if (name.startsWith('font-size-')) return { category: 'fontSize', key: name.slice('font-size-'.length) };
@@ -295,7 +408,12 @@ function classifyVariable(rawName: string): { category: VariableCategory; key: s
295
408
  if (name === 'shadow') return { category: 'shadow', key: 'DEFAULT' };
296
409
  if (name.startsWith('shadow-')) return { category: 'shadow', key: name.slice('shadow-'.length) };
297
410
 
298
- if (name.startsWith('color-')) return { category: 'color', key: name.slice('color-'.length) };
411
+ if (name.startsWith('color-')) {
412
+ const key = name.slice('color-'.length);
413
+ if (isTailwindDefaultColorKey(key)) return null;
414
+ return { category: 'color', key };
415
+ }
416
+ if (isTailwindDefaultColorKey(name)) return null;
299
417
  return { category: 'color', key: name };
300
418
  }
301
419
 
@@ -323,6 +441,11 @@ function applyDeclaration(target: TargetTokenGroup, decl: Declaration): void {
323
441
  if (px != null) target.radius[classification.key] = px;
324
442
  return;
325
443
  }
444
+ if (classification.category === 'breakpoint') {
445
+ const px = parseDimensionPx(value);
446
+ if (px != null) target.breakpoints[classification.key] = px;
447
+ return;
448
+ }
326
449
  if (classification.category === 'spacing') {
327
450
  const px = parseDimensionPx(value);
328
451
  if (px != null) target.spacing[classification.key] = px;
@@ -376,7 +499,7 @@ function walkCssNodes(map: ScannedTokenMap, container: postcss.Container): void
376
499
  }
377
500
  }
378
501
 
379
- export function parseCssTokenMap(cssText: string, source: string, requestedMode: TokenSourceMode = 'auto'): ScannedTokenMap {
502
+ export function parseCssTokenMap(cssText: string, source: string, requestedMode: TokenSourceMode = 'css'): ScannedTokenMap {
380
503
  const map = createEmptyScannedTokenMap('css', source, requestedMode);
381
504
  const root = postcss.parse(cssText);
382
505
  walkCssNodes(map, root);
@@ -417,7 +540,7 @@ function applyDtcgDimensionGroup(
417
540
  function parseDtcgTokenMap(
418
541
  dtcgJson: unknown,
419
542
  source: string,
420
- requestedMode: TokenSourceMode = 'auto'
543
+ requestedMode: TokenSourceMode = 'dtcg'
421
544
  ): ScannedTokenMap {
422
545
  const map = createEmptyScannedTokenMap('dtcg', source, requestedMode);
423
546
  if (!isPlainObject(dtcgJson)) return map;
@@ -445,16 +568,10 @@ function parseDtcgTokenMap(
445
568
 
446
569
  export function readTokenSourceMap(options: ReadTokenSourceOptions): ScannedTokenMap {
447
570
  const projectRoot = path.resolve(options.projectRoot);
448
- const requestedMode = options.tokenSourceMode || 'auto';
571
+ const requestedMode: TokenSourceMode = options.tokenSourceMode === 'dtcg' ? 'dtcg' : 'css';
449
572
  const cssPath = discoverCssTokenPath(projectRoot, options.cssTokenPath);
450
573
  const dtcgPath = discoverDtcgTokenPath(projectRoot, options.dtcgTokenPath);
451
574
 
452
- if (requestedMode === 'css') {
453
- if (!cssPath) return createEmptyScannedTokenMap('embedded', 'embedded:tokens.ts', requestedMode);
454
- const cssText = readCssWithImports(cssPath);
455
- return parseCssTokenMap(cssText, toDisplayPath(projectRoot, cssPath), requestedMode);
456
- }
457
-
458
575
  if (requestedMode === 'dtcg') {
459
576
  if (!dtcgPath) return createEmptyScannedTokenMap('embedded', 'embedded:tokens.ts', requestedMode);
460
577
  const dtcgText = fs.readFileSync(dtcgPath, 'utf-8');
@@ -462,10 +579,14 @@ export function readTokenSourceMap(options: ReadTokenSourceOptions): ScannedToke
462
579
  return parseDtcgTokenMap(dtcgJson, toDisplayPath(projectRoot, dtcgPath), requestedMode);
463
580
  }
464
581
 
465
- // auto mode: CSS always wins when present.
582
+ // css mode: CSS preferred; fall back to DTCG if no CSS file found, then embedded.
466
583
  if (cssPath) {
467
584
  const cssText = readCssWithImports(cssPath);
468
- return parseCssTokenMap(cssText, toDisplayPath(projectRoot, cssPath), requestedMode);
585
+ // Use the file that actually contains the declarations as source so the
586
+ // write-back path (PR/patch) targets the right file even when globals.css
587
+ // delegates token definitions to an imported file like tokens.css.
588
+ const writeTarget = resolveCssTokenPathFromImports(cssPath);
589
+ return parseCssTokenMap(cssText, toDisplayPath(projectRoot, writeTarget), requestedMode);
469
590
  }
470
591
  if (dtcgPath) {
471
592
  const dtcgText = fs.readFileSync(dtcgPath, 'utf-8');
@@ -0,0 +1,98 @@
1
+ import assert from 'node:assert/strict';
2
+
3
+ import { cvaInstanceHasOverridingJsxChildren } from '../src/components/component-instance';
4
+
5
+ // Regression: predicate that decides whether a CVA component instance has
6
+ // per-instance JSX-element children (e.g. icons) that the symbol-instance
7
+ // path can't honour, forcing a fall back to frame rendering.
8
+ //
9
+ // History: `<ToggleGroup>` with three `<Toggle>` items, each with a different
10
+ // icon child (AlignLeft / AlignCenter / AlignRight), rendered all three with
11
+ // the same icon (Bold) — because the Toggle CVA master is built from one of
12
+ // Toggle's own stories and Figma's instance API can't swap SVG vectors per
13
+ // instance. The story-root path already had a similar guard
14
+ // (`shouldUseInstance` in story-render-strategy.ts); this is the parallel
15
+ // guard for NESTED CVA instances inside a JSX tree.
16
+
17
+ interface Case {
18
+ name: string;
19
+ props: { __jsxChildren?: unknown } | null;
20
+ expected: boolean;
21
+ }
22
+
23
+ const CASES: Case[] = [
24
+ {
25
+ name: 'no props at all',
26
+ props: null,
27
+ expected: false,
28
+ },
29
+ {
30
+ name: 'empty props',
31
+ props: {},
32
+ expected: false,
33
+ },
34
+ {
35
+ name: '__jsxChildren is not an array (defensive)',
36
+ props: { __jsxChildren: 'not-an-array' },
37
+ expected: false,
38
+ },
39
+ {
40
+ name: 'empty __jsxChildren array',
41
+ props: { __jsxChildren: [] },
42
+ expected: false,
43
+ },
44
+ {
45
+ name: 'text-only children (e.g. <Button>Click me</Button>)',
46
+ props: { __jsxChildren: [{ type: 'text', content: 'Click me' }] },
47
+ expected: false,
48
+ },
49
+ {
50
+ name: 'single element child (e.g. <Toggle><Bold /></Toggle>)',
51
+ props: {
52
+ __jsxChildren: [
53
+ { type: 'element', tagName: 'Bold', isComponent: true, props: {}, children: [] },
54
+ ],
55
+ },
56
+ expected: true,
57
+ },
58
+ {
59
+ name: 'icon + text mixed (e.g. <Button><Plus /> Add</Button>)',
60
+ props: {
61
+ __jsxChildren: [
62
+ { type: 'element', tagName: 'Plus', isComponent: true, props: {}, children: [] },
63
+ { type: 'text', content: ' Add' },
64
+ ],
65
+ },
66
+ expected: true,
67
+ },
68
+ {
69
+ name: 'multiple elements (rare; defensive)',
70
+ props: {
71
+ __jsxChildren: [
72
+ { type: 'element', tagName: 'AlignLeft', isComponent: true, props: {}, children: [] },
73
+ { type: 'element', tagName: 'AlignCenter', isComponent: true, props: {}, children: [] },
74
+ ],
75
+ },
76
+ expected: true,
77
+ },
78
+ {
79
+ name: 'null entries in array are tolerated',
80
+ props: { __jsxChildren: [null, undefined, { type: 'text', content: 'Hi' }] },
81
+ expected: false,
82
+ },
83
+ ];
84
+
85
+ let failed = 0;
86
+ for (const c of CASES) {
87
+ const actual = cvaInstanceHasOverridingJsxChildren(c.props);
88
+ if (actual !== c.expected) {
89
+ console.error(` ✗ ${c.name}: expected ${c.expected}, got ${actual}`);
90
+ failed++;
91
+ }
92
+ }
93
+
94
+ if (failed > 0) {
95
+ assert.fail(`${failed}/${CASES.length} cva-jsx-child-fallback cases failed`);
96
+ }
97
+
98
+ console.log(`cva-jsx-child-fallback-regression: PASS (${CASES.length} cases)`);