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
|
@@ -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
|
-
|
|
96
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
269
|
-
|
|
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-'))
|
|
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 = '
|
|
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 = '
|
|
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
|
|
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
|
-
//
|
|
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
|
-
|
|
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)`);
|