@fragments-sdk/cli 0.14.3 → 0.15.0
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 +0 -3
- package/dist/bin.js +4290 -3754
- package/dist/bin.js.map +1 -1
- package/dist/{chunk-TXFCEDOC.js → chunk-2WXKALIG.js} +2 -2
- package/dist/{chunk-I34BC3CU.js → chunk-32LIWN2P.js} +1006 -3
- package/dist/chunk-32LIWN2P.js.map +1 -0
- package/dist/{chunk-55KERLWL.js → chunk-65WSVDV5.js} +314 -89
- package/dist/chunk-65WSVDV5.js.map +1 -0
- package/dist/chunk-7DZC4YEV.js +294 -0
- package/dist/chunk-7DZC4YEV.js.map +1 -0
- package/dist/{chunk-LOYS64QS.js → chunk-7WHVW72L.js} +230 -19
- package/dist/chunk-7WHVW72L.js.map +1 -0
- package/dist/{chunk-PJT5IZ37.js → chunk-BJE3425I.js} +19 -52
- package/dist/{chunk-PJT5IZ37.js.map → chunk-BJE3425I.js.map} +1 -1
- package/dist/{chunk-5A6X2Y73.js → chunk-CZD3AD4Q.js} +12 -11
- package/dist/chunk-CZD3AD4Q.js.map +1 -0
- package/dist/{chunk-EYXVAMEX.js → chunk-MN3TJ3D5.js} +72 -3
- package/dist/chunk-MN3TJ3D5.js.map +1 -0
- package/dist/chunk-QCN35LJU.js +630 -0
- package/dist/chunk-QCN35LJU.js.map +1 -0
- package/dist/chunk-T47OLCSF.js +36 -0
- package/dist/chunk-T47OLCSF.js.map +1 -0
- package/dist/{chunk-APTQIBS5.js → chunk-XJQ5BIWI.js} +144 -1049
- package/dist/chunk-XJQ5BIWI.js.map +1 -0
- package/dist/codebase-scanner-VOTPXRYW.js +22 -0
- package/dist/converter-JLINP7CJ.js +34 -0
- package/dist/converter-JLINP7CJ.js.map +1 -0
- package/dist/core/index.js +43 -1
- package/dist/{generate-RYWIPDN2.js → generate-A4FP5426.js} +3 -4
- package/dist/{generate-RYWIPDN2.js.map → generate-A4FP5426.js.map} +1 -1
- package/dist/govern-scan-UCBZR6D6.js +280 -0
- package/dist/govern-scan-UCBZR6D6.js.map +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +11 -11
- package/dist/{init-WRUSW7R5.js → init-HGSM35XA.js} +131 -128
- package/dist/init-HGSM35XA.js.map +1 -0
- package/dist/{init-cloud-REQ3XLHO.js → init-cloud-MQ6GRJAZ.js} +2 -2
- package/dist/mcp-bin.js +5 -36
- package/dist/mcp-bin.js.map +1 -1
- package/dist/scan-VNNKACG2.js +15 -0
- package/dist/{scan-generate-TFZVL3BT.js → scan-generate-TWRHNU5M.js} +335 -46
- package/dist/scan-generate-TWRHNU5M.js.map +1 -0
- package/dist/scanner-7LAZYPWZ.js +13 -0
- package/dist/{service-HKJ6B7P7.js → service-FHQU7YS7.js} +27 -23
- package/dist/{snapshot-C5DYIGIV.js → snapshot-KQEQ6XHL.js} +2 -2
- package/dist/{static-viewer-DUVC4UIM.js → static-viewer-63PG6FWY.js} +3 -3
- package/dist/static-viewer-63PG6FWY.js.map +1 -0
- package/dist/{test-JW7JIDFG.js → test-UQYUCZIS.js} +4 -6
- package/dist/{test-JW7JIDFG.js.map → test-UQYUCZIS.js.map} +1 -1
- package/dist/{tokens-KE73G5JC.js → tokens-6GYKDV6U.js} +6 -5
- package/dist/{tokens-KE73G5JC.js.map → tokens-6GYKDV6U.js.map} +1 -1
- package/dist/tokens-generate-VTZV5EEW.js +86 -0
- package/dist/tokens-generate-VTZV5EEW.js.map +1 -0
- package/package.json +6 -6
- package/src/bin.ts +210 -48
- package/src/build.ts +130 -6
- package/src/commands/__fixtures__/shadcn-label-wrapper/package.json +7 -0
- package/src/commands/__fixtures__/shadcn-label-wrapper/src/components/ui/label.contract.json +42 -0
- package/src/commands/__fixtures__/shadcn-label-wrapper/src/components/ui/label.tsx +11 -0
- package/src/commands/__fixtures__/shadcn-label-wrapper/src/components/ui/primitive.contract.json +20 -0
- package/src/commands/__fixtures__/shadcn-label-wrapper/src/components/ui/primitive.tsx +14 -0
- package/src/commands/__fixtures__/shadcn-label-wrapper/tsconfig.app.json +23 -0
- package/src/commands/__tests__/init.test.ts +113 -0
- package/src/commands/__tests__/scan-generate.test.ts +188 -69
- package/src/commands/__tests__/verify.test.ts +91 -0
- package/src/commands/discover.ts +151 -0
- package/src/commands/enhance.ts +3 -1
- package/src/commands/govern-scan.ts +386 -0
- package/src/commands/govern.ts +2 -2
- package/src/commands/init.ts +152 -28
- package/src/commands/inspect.ts +290 -0
- package/src/commands/migrate-contract.ts +85 -0
- package/src/commands/scan-generate.ts +438 -50
- package/src/commands/scan.ts +1 -0
- package/src/commands/setup.ts +27 -50
- package/src/commands/tokens-generate.ts +113 -0
- package/src/commands/verify.ts +195 -1
- package/src/core/__fixtures__/shadcn-input/input.tsx +7 -0
- package/src/core/__fixtures__/shadcn-input/tsconfig.json +14 -0
- package/src/core/__fixtures__/shadcn-label/label.tsx +11 -0
- package/src/core/__fixtures__/shadcn-label/primitive.tsx +14 -0
- package/src/core/__fixtures__/shadcn-label/tsconfig.json +14 -0
- package/src/core/__fixtures__/shadcn-radix-label/label.tsx +11 -0
- package/src/core/__fixtures__/shadcn-radix-label/node_modules/radix-ui/index.d.ts +12 -0
- package/src/core/__fixtures__/shadcn-radix-label/tsconfig.json +14 -0
- package/src/core/__tests__/contract-parity.test.ts +316 -0
- package/src/core/component-extractor.test.ts +39 -0
- package/src/core/component-extractor.ts +92 -1
- package/src/core/config.ts +2 -1
- package/src/core/discovery.ts +13 -2
- package/src/core/drift-verifier.ts +123 -0
- package/src/core/extractor-adapter.ts +80 -0
- package/src/mcp/__tests__/projectFields.test.ts +1 -1
- package/src/mcp/utils.ts +1 -50
- package/src/migrate/converter.ts +3 -3
- package/src/migrate/fragment-to-contract.ts +253 -0
- package/src/migrate/report.ts +1 -1
- package/src/scripts/token-benchmark.ts +121 -0
- package/src/service/__tests__/props-extractor.test.ts +94 -0
- package/src/service/__tests__/token-normalizer.test.ts +690 -0
- package/src/service/ast-utils.ts +4 -23
- package/src/service/babel-config.ts +23 -0
- package/src/service/enhance/converter.ts +61 -0
- package/src/service/enhance/props-extractor.ts +25 -8
- package/src/service/enhance/scanner.ts +5 -24
- package/src/service/snippet-validation.ts +9 -3
- package/src/service/token-normalizer.ts +510 -0
- package/src/shared/index.ts +1 -0
- package/src/shared/project-fields.ts +46 -0
- package/src/viewer/__tests__/viewer-integration.test.ts +8 -8
- package/src/viewer/preview-adapter.ts +116 -0
- package/src/viewer/style-utils.ts +27 -412
- package/src/viewer/vite-plugin.ts +2 -2
- package/dist/chunk-55KERLWL.js.map +0 -1
- package/dist/chunk-5A6X2Y73.js.map +0 -1
- package/dist/chunk-APTQIBS5.js.map +0 -1
- package/dist/chunk-EYXVAMEX.js.map +0 -1
- package/dist/chunk-I34BC3CU.js.map +0 -1
- package/dist/chunk-LOYS64QS.js.map +0 -1
- package/dist/chunk-ZKTFKHWN.js +0 -324
- package/dist/chunk-ZKTFKHWN.js.map +0 -1
- package/dist/discovery-VDANZAJ2.js +0 -28
- package/dist/init-WRUSW7R5.js.map +0 -1
- package/dist/scan-YJHQIRKG.js +0 -14
- package/dist/scan-generate-TFZVL3BT.js.map +0 -1
- package/dist/viewer-2TZS3NDL.js +0 -2730
- package/dist/viewer-2TZS3NDL.js.map +0 -1
- package/src/commands/dev.ts +0 -107
- /package/dist/{chunk-TXFCEDOC.js.map → chunk-2WXKALIG.js.map} +0 -0
- /package/dist/{discovery-VDANZAJ2.js.map → codebase-scanner-VOTPXRYW.js.map} +0 -0
- /package/dist/{init-cloud-REQ3XLHO.js.map → init-cloud-MQ6GRJAZ.js.map} +0 -0
- /package/dist/{scan-YJHQIRKG.js.map → scan-VNNKACG2.js.map} +0 -0
- /package/dist/{service-HKJ6B7P7.js.map → scanner-7LAZYPWZ.js.map} +0 -0
- /package/dist/{static-viewer-DUVC4UIM.js.map → service-FHQU7YS7.js.map} +0 -0
- /package/dist/{snapshot-C5DYIGIV.js.map → snapshot-KQEQ6XHL.js.map} +0 -0
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Preview Adapter — generates render modules from contract.json metadata.
|
|
3
|
+
*
|
|
4
|
+
* Each adapter takes a CompiledFragment sourced from a .contract.json
|
|
5
|
+
* and produces a virtual ES module string that a bundler (Vite) can serve.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { CompiledFragment } from '@fragments-sdk/core';
|
|
9
|
+
|
|
10
|
+
export interface PreviewConfig {
|
|
11
|
+
setupModule?: string;
|
|
12
|
+
wrapperModule?: string;
|
|
13
|
+
wrapperExport?: string;
|
|
14
|
+
css?: string[];
|
|
15
|
+
theme?: 'light' | 'dark';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface PreviewAdapter {
|
|
19
|
+
framework: string;
|
|
20
|
+
canHandle(fragment: CompiledFragment): boolean;
|
|
21
|
+
generateRenderModule(
|
|
22
|
+
fragment: CompiledFragment,
|
|
23
|
+
variant: string,
|
|
24
|
+
previewConfig: PreviewConfig,
|
|
25
|
+
): string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* React preview adapter — generates a virtual module that:
|
|
30
|
+
* 1. Imports the component from sourcePath/exportName
|
|
31
|
+
* 2. Wraps with providers from project config
|
|
32
|
+
* 3. Renders with args from the selected example
|
|
33
|
+
* 4. Injects CSS from config
|
|
34
|
+
*/
|
|
35
|
+
class ReactPreviewAdapter implements PreviewAdapter {
|
|
36
|
+
framework = 'react';
|
|
37
|
+
|
|
38
|
+
canHandle(fragment: CompiledFragment): boolean {
|
|
39
|
+
return (fragment.framework ?? 'react') === 'react';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
generateRenderModule(
|
|
43
|
+
fragment: CompiledFragment,
|
|
44
|
+
variant: string,
|
|
45
|
+
config: PreviewConfig,
|
|
46
|
+
): string {
|
|
47
|
+
if (!fragment.sourcePath || !fragment.exportName) {
|
|
48
|
+
throw new Error(
|
|
49
|
+
`Cannot preview ${fragment.meta.name}: missing sourcePath or exportName`,
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const example = fragment.variants.find((v) => v.name === variant) ??
|
|
54
|
+
fragment.variants[0];
|
|
55
|
+
|
|
56
|
+
const args = example?.args ?? {};
|
|
57
|
+
const argsStr = JSON.stringify(args);
|
|
58
|
+
|
|
59
|
+
const lines: string[] = [];
|
|
60
|
+
|
|
61
|
+
// Setup module (global CSS, MSW, etc.)
|
|
62
|
+
if (config.setupModule) {
|
|
63
|
+
lines.push(`import '${config.setupModule}';`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// CSS imports
|
|
67
|
+
if (config.css?.length) {
|
|
68
|
+
for (const css of config.css) {
|
|
69
|
+
lines.push(`import '${css}';`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Import React
|
|
74
|
+
lines.push(`import React from 'react';`);
|
|
75
|
+
|
|
76
|
+
// Import the component
|
|
77
|
+
lines.push(`import { ${fragment.exportName} } from '${fragment.sourcePath}';`);
|
|
78
|
+
|
|
79
|
+
// Import wrapper if configured
|
|
80
|
+
if (config.wrapperModule && config.wrapperExport) {
|
|
81
|
+
lines.push(`import { ${config.wrapperExport} } from '${config.wrapperModule}';`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Generate the render function
|
|
85
|
+
lines.push('');
|
|
86
|
+
lines.push(`const args = ${argsStr};`);
|
|
87
|
+
lines.push('');
|
|
88
|
+
lines.push('export default function PreviewRender() {');
|
|
89
|
+
|
|
90
|
+
if (config.wrapperModule && config.wrapperExport) {
|
|
91
|
+
lines.push(` return React.createElement(${config.wrapperExport}, null,`);
|
|
92
|
+
lines.push(` React.createElement(${fragment.exportName}, args)`);
|
|
93
|
+
lines.push(' );');
|
|
94
|
+
} else {
|
|
95
|
+
lines.push(` return React.createElement(${fragment.exportName}, args);`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
lines.push('}');
|
|
99
|
+
|
|
100
|
+
return lines.join('\n');
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Create a preview adapter for the given framework.
|
|
106
|
+
*/
|
|
107
|
+
export function createPreviewAdapter(
|
|
108
|
+
framework: string,
|
|
109
|
+
_config: PreviewConfig,
|
|
110
|
+
): PreviewAdapter {
|
|
111
|
+
if (framework === 'react') {
|
|
112
|
+
return new ReactPreviewAdapter();
|
|
113
|
+
}
|
|
114
|
+
// Future: Vue, Svelte adapters
|
|
115
|
+
throw new Error(`No preview adapter available for framework: ${framework}`);
|
|
116
|
+
}
|
|
@@ -1,414 +1,29 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Style comparison utilities
|
|
3
|
-
* with rendered component computed styles.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Style diff result for a single CSS property
|
|
8
|
-
*/
|
|
9
|
-
export interface StyleDiffItem {
|
|
10
|
-
/** CSS property name */
|
|
11
|
-
property: string;
|
|
12
|
-
/** Expected value from Figma */
|
|
13
|
-
figma: string;
|
|
14
|
-
/** Actual value from rendered component */
|
|
15
|
-
rendered: string;
|
|
16
|
-
/** Whether values match (within tolerance) */
|
|
17
|
-
match: boolean;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Result of comparing styles
|
|
22
|
-
*/
|
|
23
|
-
export interface StyleComparisonResult {
|
|
24
|
-
/** Whether all styles match */
|
|
25
|
-
match: boolean;
|
|
26
|
-
/** Individual property comparisons */
|
|
27
|
-
properties: StyleDiffItem[];
|
|
28
|
-
/** CSS properties from Figma design */
|
|
29
|
-
figmaStyles: Record<string, string>;
|
|
30
|
-
/** Computed CSS properties from rendered component */
|
|
31
|
-
renderedStyles: Record<string, string>;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Compare Figma CSS properties with rendered computed styles.
|
|
36
|
-
*/
|
|
37
|
-
export function compareStyles(
|
|
38
|
-
figmaStyles: Record<string, string | undefined>,
|
|
39
|
-
renderedStyles: Record<string, string>
|
|
40
|
-
): StyleComparisonResult {
|
|
41
|
-
const properties: StyleDiffItem[] = [];
|
|
42
|
-
const cleanFigmaStyles: Record<string, string> = {};
|
|
43
|
-
|
|
44
|
-
// Properties to compare
|
|
45
|
-
const propsToCompare = [
|
|
46
|
-
"backgroundColor",
|
|
47
|
-
"borderColor",
|
|
48
|
-
"borderWidth",
|
|
49
|
-
"borderRadius",
|
|
50
|
-
"fontFamily",
|
|
51
|
-
"fontSize",
|
|
52
|
-
"fontWeight",
|
|
53
|
-
"lineHeight",
|
|
54
|
-
"letterSpacing",
|
|
55
|
-
"textAlign",
|
|
56
|
-
"boxShadow",
|
|
57
|
-
"padding",
|
|
58
|
-
"gap",
|
|
59
|
-
"opacity",
|
|
60
|
-
];
|
|
61
|
-
|
|
62
|
-
for (const prop of propsToCompare) {
|
|
63
|
-
const figmaValue = figmaStyles[prop];
|
|
64
|
-
const renderedValue = renderedStyles[prop];
|
|
65
|
-
|
|
66
|
-
if (figmaValue !== undefined) {
|
|
67
|
-
cleanFigmaStyles[prop] = figmaValue;
|
|
68
|
-
|
|
69
|
-
const match = compareStyleValue(prop, figmaValue, renderedValue || "");
|
|
70
|
-
properties.push({
|
|
71
|
-
property: prop,
|
|
72
|
-
figma: figmaValue,
|
|
73
|
-
rendered: renderedValue || "(not set)",
|
|
74
|
-
match,
|
|
75
|
-
});
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
const allMatch = properties.every((p) => p.match);
|
|
80
|
-
|
|
81
|
-
return {
|
|
82
|
-
match: allMatch,
|
|
83
|
-
properties,
|
|
84
|
-
figmaStyles: cleanFigmaStyles,
|
|
85
|
-
renderedStyles,
|
|
86
|
-
};
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Compare a single style value with tolerance for color and numeric differences.
|
|
91
|
-
*/
|
|
92
|
-
export function compareStyleValue(
|
|
93
|
-
prop: string,
|
|
94
|
-
figma: string,
|
|
95
|
-
rendered: string
|
|
96
|
-
): boolean {
|
|
97
|
-
// Normalize values for comparison
|
|
98
|
-
const normalizedFigma = normalizeStyleValue(prop, figma);
|
|
99
|
-
const normalizedRendered = normalizeStyleValue(prop, rendered);
|
|
100
|
-
|
|
101
|
-
// Direct match
|
|
102
|
-
if (normalizedFigma === normalizedRendered) {
|
|
103
|
-
return true;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
// Color comparison with tolerance
|
|
107
|
-
if (prop === "backgroundColor" || prop === "borderColor") {
|
|
108
|
-
return compareColors(normalizedFigma, normalizedRendered, 5);
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
// Numeric comparison with tolerance (for pixels)
|
|
112
|
-
if (
|
|
113
|
-
["borderWidth", "borderRadius", "fontSize", "padding", "gap"].includes(prop)
|
|
114
|
-
) {
|
|
115
|
-
return compareNumericValues(normalizedFigma, normalizedRendered, 1);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
return false;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/**
|
|
122
|
-
* Normalize a style value for comparison.
|
|
123
|
-
*/
|
|
124
|
-
export function normalizeStyleValue(prop: string, value: string): string {
|
|
125
|
-
// Remove extra whitespace
|
|
126
|
-
let normalized = value.trim().replace(/\s+/g, " ");
|
|
127
|
-
|
|
128
|
-
// Normalize "none" shadow to empty
|
|
129
|
-
if (prop === "boxShadow" && normalized === "none") {
|
|
130
|
-
normalized = "";
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// Normalize rgba(0, 0, 0, 0) to "transparent"
|
|
134
|
-
if (normalized.match(/rgba\(\s*0\s*,\s*0\s*,\s*0\s*,\s*0\s*\)/)) {
|
|
135
|
-
normalized = "transparent";
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
return normalized;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/**
|
|
142
|
-
* Compare two color values with tolerance.
|
|
143
|
-
*/
|
|
144
|
-
export function compareColors(
|
|
145
|
-
color1: string,
|
|
146
|
-
color2: string,
|
|
147
|
-
tolerance: number
|
|
148
|
-
): boolean {
|
|
149
|
-
const rgb1 = parseColor(color1);
|
|
150
|
-
const rgb2 = parseColor(color2);
|
|
151
|
-
|
|
152
|
-
if (!rgb1 || !rgb2) {
|
|
153
|
-
return color1 === color2;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
return (
|
|
157
|
-
Math.abs(rgb1.r - rgb2.r) <= tolerance &&
|
|
158
|
-
Math.abs(rgb1.g - rgb2.g) <= tolerance &&
|
|
159
|
-
Math.abs(rgb1.b - rgb2.b) <= tolerance &&
|
|
160
|
-
Math.abs((rgb1.a ?? 1) - (rgb2.a ?? 1)) <= 0.05
|
|
161
|
-
);
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
/**
|
|
165
|
-
* Parse a color string to RGB values.
|
|
166
|
-
*/
|
|
167
|
-
export function parseColor(
|
|
168
|
-
color: string
|
|
169
|
-
): { r: number; g: number; b: number; a?: number } | null {
|
|
170
|
-
// Handle hex colors
|
|
171
|
-
const hexMatch = color.match(/^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i);
|
|
172
|
-
if (hexMatch) {
|
|
173
|
-
return {
|
|
174
|
-
r: parseInt(hexMatch[1], 16),
|
|
175
|
-
g: parseInt(hexMatch[2], 16),
|
|
176
|
-
b: parseInt(hexMatch[3], 16),
|
|
177
|
-
};
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
// Handle rgb/rgba
|
|
181
|
-
const rgbaMatch = color.match(
|
|
182
|
-
/rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*(?:,\s*([\d.]+))?\s*\)/
|
|
183
|
-
);
|
|
184
|
-
if (rgbaMatch) {
|
|
185
|
-
return {
|
|
186
|
-
r: parseInt(rgbaMatch[1], 10),
|
|
187
|
-
g: parseInt(rgbaMatch[2], 10),
|
|
188
|
-
b: parseInt(rgbaMatch[3], 10),
|
|
189
|
-
a: rgbaMatch[4] ? parseFloat(rgbaMatch[4]) : 1,
|
|
190
|
-
};
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
return null;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
/**
|
|
197
|
-
* Compare numeric values (e.g., "10px" vs "11px") with tolerance.
|
|
198
|
-
*/
|
|
199
|
-
export function compareNumericValues(
|
|
200
|
-
value1: string,
|
|
201
|
-
value2: string,
|
|
202
|
-
tolerance: number
|
|
203
|
-
): boolean {
|
|
204
|
-
const num1 = parseFloat(value1);
|
|
205
|
-
const num2 = parseFloat(value2);
|
|
206
|
-
|
|
207
|
-
if (isNaN(num1) || isNaN(num2)) {
|
|
208
|
-
return value1 === value2;
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
return Math.abs(num1 - num2) <= tolerance;
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
// ----- Enhanced Token-Aware Style Comparison -----
|
|
215
|
-
|
|
216
|
-
import type {
|
|
217
|
-
EnhancedStyleDiffItem,
|
|
218
|
-
TokenFix,
|
|
219
|
-
TokenUsageSummary,
|
|
220
|
-
DesignToken,
|
|
221
|
-
} from "../core/index.js";
|
|
222
|
-
|
|
223
|
-
/**
|
|
224
|
-
* Enhanced style diff result with token information
|
|
225
|
-
*/
|
|
226
|
-
export interface EnhancedStyleComparisonResult extends StyleComparisonResult {
|
|
227
|
-
/** Individual property comparisons with token info */
|
|
228
|
-
properties: EnhancedStyleDiffItem[];
|
|
229
|
-
/** Token usage summary */
|
|
230
|
-
tokenSummary?: TokenUsageSummary;
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
/**
|
|
234
|
-
* Token registry interface for style comparison
|
|
235
|
-
* (subset of TokenRegistryManager methods needed here)
|
|
236
|
-
*/
|
|
237
|
-
export interface TokenLookup {
|
|
238
|
-
findByValue(value: string, theme?: string): string[];
|
|
239
|
-
getToken(name: string): DesignToken | undefined;
|
|
240
|
-
calculateUsageSummary(
|
|
241
|
-
styleDiffs: Array<{
|
|
242
|
-
property: string;
|
|
243
|
-
figma: string;
|
|
244
|
-
rendered: string;
|
|
245
|
-
match: boolean;
|
|
246
|
-
}>,
|
|
247
|
-
theme?: string
|
|
248
|
-
): TokenUsageSummary;
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
/**
|
|
252
|
-
* Compare styles with token awareness.
|
|
2
|
+
* Style comparison utilities — thin re-export from @fragments-sdk/core.
|
|
253
3
|
*
|
|
254
|
-
*
|
|
255
|
-
*
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
"textAlign",
|
|
281
|
-
"boxShadow",
|
|
282
|
-
"padding",
|
|
283
|
-
"gap",
|
|
284
|
-
"opacity",
|
|
285
|
-
"color",
|
|
286
|
-
];
|
|
287
|
-
|
|
288
|
-
for (const prop of propsToCompare) {
|
|
289
|
-
const figmaValue = figmaStyles[prop];
|
|
290
|
-
const renderedValue = renderedStyles[prop];
|
|
291
|
-
|
|
292
|
-
if (figmaValue !== undefined) {
|
|
293
|
-
cleanFigmaStyles[prop] = figmaValue;
|
|
294
|
-
|
|
295
|
-
const match = compareStyleValue(prop, figmaValue, renderedValue || "");
|
|
296
|
-
|
|
297
|
-
// Build enhanced diff item
|
|
298
|
-
const item: EnhancedStyleDiffItem = {
|
|
299
|
-
property: prop,
|
|
300
|
-
figma: figmaValue,
|
|
301
|
-
rendered: renderedValue || "(not set)",
|
|
302
|
-
match,
|
|
303
|
-
isHardcoded: false,
|
|
304
|
-
};
|
|
305
|
-
|
|
306
|
-
// Add token information if registry is available
|
|
307
|
-
if (tokenLookup) {
|
|
308
|
-
const figmaTokens = tokenLookup.findByValue(figmaValue, theme);
|
|
309
|
-
const renderedTokens = renderedValue
|
|
310
|
-
? tokenLookup.findByValue(renderedValue, theme)
|
|
311
|
-
: [];
|
|
312
|
-
|
|
313
|
-
if (figmaTokens.length > 0) {
|
|
314
|
-
item.figmaToken = figmaTokens[0];
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
if (renderedTokens.length > 0) {
|
|
318
|
-
item.renderedToken = renderedTokens[0];
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
// Determine if this is a hardcoded value
|
|
322
|
-
// Hardcoded = Figma matches a token, but rendered doesn't use a token
|
|
323
|
-
item.isHardcoded = !!item.figmaToken && !item.renderedToken;
|
|
324
|
-
|
|
325
|
-
// Generate fix suggestion if hardcoded
|
|
326
|
-
if (item.isHardcoded && item.figmaToken) {
|
|
327
|
-
const token = tokenLookup.getToken(item.figmaToken);
|
|
328
|
-
if (token) {
|
|
329
|
-
const cssProperty = toCssProperty(prop);
|
|
330
|
-
item.suggestedFix = {
|
|
331
|
-
tokenName: item.figmaToken,
|
|
332
|
-
tokenValue: token.resolvedValue,
|
|
333
|
-
codeFix: `${cssProperty}: var(${item.figmaToken});`,
|
|
334
|
-
confidence: 0.9,
|
|
335
|
-
reason: `Figma uses token ${item.figmaToken} (${token.resolvedValue}). Replace hardcoded value with token for consistency.`,
|
|
336
|
-
};
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
properties.push(item);
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
const allMatch = properties.every((p) => p.match);
|
|
346
|
-
|
|
347
|
-
// Calculate token summary if registry available
|
|
348
|
-
let tokenSummary: TokenUsageSummary | undefined;
|
|
349
|
-
if (tokenLookup) {
|
|
350
|
-
tokenSummary = tokenLookup.calculateUsageSummary(
|
|
351
|
-
properties.map((p) => ({
|
|
352
|
-
property: p.property,
|
|
353
|
-
figma: p.figma,
|
|
354
|
-
rendered: p.rendered,
|
|
355
|
-
match: p.match,
|
|
356
|
-
})),
|
|
357
|
-
theme
|
|
358
|
-
);
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
return {
|
|
362
|
-
match: allMatch,
|
|
363
|
-
properties,
|
|
364
|
-
figmaStyles: cleanFigmaStyles,
|
|
365
|
-
renderedStyles,
|
|
366
|
-
tokenSummary,
|
|
367
|
-
};
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
/**
|
|
371
|
-
* Convert camelCase to kebab-case CSS property
|
|
372
|
-
*/
|
|
373
|
-
function toCssProperty(prop: string): string {
|
|
374
|
-
return prop.replace(/([A-Z])/g, "-$1").toLowerCase();
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
/**
|
|
378
|
-
* Format token summary for display
|
|
379
|
-
*/
|
|
380
|
-
export function formatTokenSummary(summary: TokenUsageSummary): string {
|
|
381
|
-
const lines: string[] = [];
|
|
382
|
-
|
|
383
|
-
lines.push(`Token Compliance: ${summary.compliancePercent}%`);
|
|
384
|
-
lines.push(
|
|
385
|
-
`${summary.usingTokens}/${summary.totalProperties} properties using tokens`
|
|
386
|
-
);
|
|
387
|
-
|
|
388
|
-
if (summary.hardcoded > 0) {
|
|
389
|
-
lines.push(`${summary.hardcoded} hardcoded value(s) detected`);
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
if (summary.implicitMatches > 0) {
|
|
393
|
-
lines.push(`${summary.implicitMatches} implicit match(es)`);
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
return lines.join("\n");
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
/**
|
|
400
|
-
* Get status badge for token compliance
|
|
401
|
-
*/
|
|
402
|
-
export function getComplianceBadge(
|
|
403
|
-
compliancePercent: number
|
|
404
|
-
): { label: string; color: string } {
|
|
405
|
-
if (compliancePercent >= 100) {
|
|
406
|
-
return { label: "Excellent", color: "green" };
|
|
407
|
-
} else if (compliancePercent >= 80) {
|
|
408
|
-
return { label: "Good", color: "blue" };
|
|
409
|
-
} else if (compliancePercent >= 50) {
|
|
410
|
-
return { label: "Fair", color: "yellow" };
|
|
411
|
-
} else {
|
|
412
|
-
return { label: "Poor", color: "red" };
|
|
413
|
-
}
|
|
414
|
-
}
|
|
4
|
+
* The canonical comparison engine lives in @fragments-sdk/core/style-comparison.
|
|
5
|
+
* This file exists for backwards compatibility with existing imports.
|
|
6
|
+
*/
|
|
7
|
+
export {
|
|
8
|
+
// Types
|
|
9
|
+
type StyleDiffItem,
|
|
10
|
+
type StyleComparisonResult,
|
|
11
|
+
type EnhancedStyleComparisonResult,
|
|
12
|
+
type TokenLookup,
|
|
13
|
+
type NormalizedToken,
|
|
14
|
+
type NormalizedStyleMap,
|
|
15
|
+
type StyleComparisonOptions,
|
|
16
|
+
// Functions
|
|
17
|
+
compareStyles,
|
|
18
|
+
compareStyleValue,
|
|
19
|
+
compareStylesWithTokens,
|
|
20
|
+
normalizeStyleValue,
|
|
21
|
+
compareColors,
|
|
22
|
+
parseColor,
|
|
23
|
+
compareNumericValues,
|
|
24
|
+
formatTokenSummary,
|
|
25
|
+
getComplianceBadge,
|
|
26
|
+
// Constants
|
|
27
|
+
DEFAULT_STYLE_PROPERTIES,
|
|
28
|
+
DEFAULT_ENHANCED_STYLE_PROPERTIES,
|
|
29
|
+
} from "@fragments-sdk/core";
|
|
@@ -1532,9 +1532,9 @@ export function fragmentsPlugin(options: FragmentsPluginOptions): Plugin[] {
|
|
|
1532
1532
|
return null;
|
|
1533
1533
|
},
|
|
1534
1534
|
|
|
1535
|
-
// Handle HMR for fragment files
|
|
1535
|
+
// Handle HMR for fragment and contract files
|
|
1536
1536
|
handleHotUpdate({ file, server }) {
|
|
1537
|
-
if (fragmentFileSet.has(file)) {
|
|
1537
|
+
if (fragmentFileSet.has(file) || file.endsWith('.contract.json')) {
|
|
1538
1538
|
// Invalidate the virtual fragments module
|
|
1539
1539
|
const mod = server.moduleGraph.getModuleById(VIRTUAL_FRAGMENTS_RESOLVED);
|
|
1540
1540
|
if (mod) {
|