@fragments-sdk/cli 0.14.3 → 0.15.1
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/{ai-client-I6MDWNYA.js → ai-client-LSLQGOMM.js} +1 -2
- package/dist/bin.js +4745 -3817
- 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-5JF26E55.js +1255 -0
- package/dist/chunk-5JF26E55.js.map +1 -0
- package/dist/{chunk-APTQIBS5.js → chunk-6SQPP47U.js} +153 -1342
- package/dist/chunk-6SQPP47U.js.map +1 -0
- package/dist/chunk-7DZC4YEV.js +294 -0
- package/dist/chunk-7DZC4YEV.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-55KERLWL.js → chunk-HQ6A6DTV.js} +1587 -1073
- package/dist/chunk-HQ6A6DTV.js.map +1 -0
- package/dist/chunk-MHIBEEW4.js +511 -0
- package/dist/chunk-MHIBEEW4.js.map +1 -0
- package/dist/{chunk-5A6X2Y73.js → chunk-ONUP6Z4W.js} +25 -13
- package/dist/chunk-ONUP6Z4W.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/codebase-scanner-MQHUZC2G.js +21 -0
- package/dist/converter-7XM3Y6NJ.js +33 -0
- package/dist/converter-7XM3Y6NJ.js.map +1 -0
- package/dist/core/index.js +43 -2
- package/dist/create-IH4R45GE.js +806 -0
- package/dist/create-IH4R45GE.js.map +1 -0
- package/dist/{generate-RYWIPDN2.js → generate-PVOLUAAC.js} +4 -6
- package/dist/{generate-RYWIPDN2.js.map → generate-PVOLUAAC.js.map} +1 -1
- package/dist/govern-scan-OYFZYOQW.js +413 -0
- package/dist/govern-scan-OYFZYOQW.js.map +1 -0
- package/dist/index.d.ts +4 -23
- package/dist/index.js +15 -14
- package/dist/index.js.map +1 -1
- package/dist/{init-WRUSW7R5.js → init-SSGUSP7Z.js} +131 -129
- package/dist/init-SSGUSP7Z.js.map +1 -0
- package/dist/{init-cloud-REQ3XLHO.js → init-cloud-3DNKPWFB.js} +30 -5
- package/dist/{init-cloud-REQ3XLHO.js.map → init-cloud-3DNKPWFB.js.map} +1 -1
- package/dist/mcp-bin.js +5 -37
- package/dist/mcp-bin.js.map +1 -1
- package/dist/node-37AUE74M.js +65 -0
- package/dist/push-contracts-WY32TFP6.js +84 -0
- package/dist/push-contracts-WY32TFP6.js.map +1 -0
- package/dist/scan-PKSYSTRR.js +15 -0
- package/dist/{scan-generate-TFZVL3BT.js → scan-generate-VY27PIOX.js} +340 -52
- package/dist/scan-generate-VY27PIOX.js.map +1 -0
- package/dist/scanner-4KZNOXAK.js +12 -0
- package/dist/{service-HKJ6B7P7.js → service-QJGWUIVL.js} +41 -30
- package/dist/{snapshot-C5DYIGIV.js → snapshot-WIJMEIFT.js} +2 -3
- package/dist/{snapshot-C5DYIGIV.js.map → snapshot-WIJMEIFT.js.map} +1 -1
- package/dist/{static-viewer-DUVC4UIM.js → static-viewer-7QIBQZRC.js} +3 -4
- package/dist/static-viewer-7QIBQZRC.js.map +1 -0
- package/dist/{test-JW7JIDFG.js → test-64Z5BKBA.js} +4 -7
- package/dist/{test-JW7JIDFG.js.map → test-64Z5BKBA.js.map} +1 -1
- package/dist/token-normalizer-TEPOVBPV.js +312 -0
- package/dist/token-normalizer-TEPOVBPV.js.map +1 -0
- package/dist/token-parser-32KOIOFN.js +22 -0
- package/dist/token-parser-32KOIOFN.js.map +1 -0
- package/dist/{tokens-KE73G5JC.js → tokens-NZWFQIAB.js} +10 -9
- package/dist/{tokens-KE73G5JC.js.map → tokens-NZWFQIAB.js.map} +1 -1
- package/dist/tokens-generate-5JQSJ27E.js +85 -0
- package/dist/tokens-generate-5JQSJ27E.js.map +1 -0
- package/dist/tokens-push-HY3KO36V.js +148 -0
- package/dist/tokens-push-HY3KO36V.js.map +1 -0
- package/package.json +8 -6
- package/src/bin.ts +300 -48
- 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__/build-freshness.test.ts +231 -0
- package/src/commands/__tests__/create.test.ts +71 -0
- package/src/commands/__tests__/drift-sync.test.ts +1 -1
- package/src/commands/__tests__/govern.test.ts +258 -0
- package/src/commands/__tests__/init.test.ts +113 -0
- package/src/commands/__tests__/scan-generate.test.ts +189 -70
- package/src/commands/__tests__/verify.test.ts +91 -0
- package/src/commands/build.ts +54 -1
- package/src/commands/context.ts +1 -1
- package/src/commands/create.ts +536 -0
- package/src/commands/discover.ts +151 -0
- package/src/commands/doctor.ts +3 -2
- package/src/commands/enhance.ts +3 -1
- package/src/commands/govern-scan.ts +565 -0
- package/src/commands/govern.ts +67 -4
- package/src/commands/init-cloud.ts +32 -4
- 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/push-contracts.ts +112 -0
- package/src/commands/scan-generate.ts +439 -51
- package/src/commands/scan.ts +14 -0
- package/src/commands/setup.ts +27 -50
- package/src/commands/sync.ts +2 -2
- package/src/commands/tokens-generate.ts +113 -0
- package/src/commands/tokens-push.ts +199 -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/__tests__/token-resolver.test.ts +1 -1
- package/src/core/component-extractor.test.ts +40 -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/index.ts +3 -3
- 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/index.ts +8 -0
- package/src/service/snippet-validation.ts +9 -3
- package/src/service/tailwind-v4-parser.ts +314 -0
- package/src/service/token-normalizer.ts +510 -0
- package/src/service/token-parser.ts +56 -0
- package/src/setup.ts +10 -39
- package/src/shared/index.ts +1 -0
- package/src/shared/project-fields.ts +46 -0
- package/src/theme/__tests__/component-contrast.test.ts +2 -2
- package/src/theme/__tests__/serializer.test.ts +1 -1
- package/src/theme/generator.ts +16 -1
- package/src/theme/schema.ts +8 -0
- package/src/theme/serializer.ts +13 -9
- package/src/theme/types.ts +8 -0
- package/src/validators.ts +1 -2
- package/src/viewer/__tests__/viewer-integration.test.ts +8 -8
- 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 +0 -626
- package/dist/chunk-EYXVAMEX.js.map +0 -1
- package/dist/chunk-I34BC3CU.js.map +0 -1
- package/dist/chunk-LOYS64QS.js +0 -2453
- package/dist/chunk-LOYS64QS.js.map +0 -1
- package/dist/chunk-Z7EY4VHE.js +0 -50
- 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/sass.node-4XJK6YBF.js +0 -130708
- package/dist/sass.node-4XJK6YBF.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/build.ts +0 -612
- package/src/commands/dev.ts +0 -107
- package/src/core/auto-props.ts +0 -464
- package/src/core/component-extractor.ts +0 -1030
- package/src/core/token-resolver.ts +0 -155
- /package/dist/{ai-client-I6MDWNYA.js.map → ai-client-LSLQGOMM.js.map} +0 -0
- /package/dist/{chunk-TXFCEDOC.js.map → chunk-2WXKALIG.js.map} +0 -0
- /package/dist/{chunk-Z7EY4VHE.js.map → codebase-scanner-MQHUZC2G.js.map} +0 -0
- /package/dist/{discovery-VDANZAJ2.js.map → node-37AUE74M.js.map} +0 -0
- /package/dist/{scan-YJHQIRKG.js.map → scan-PKSYSTRR.js.map} +0 -0
- /package/dist/{service-HKJ6B7P7.js.map → scanner-4KZNOXAK.js.map} +0 -0
- /package/dist/{static-viewer-DUVC4UIM.js.map → service-QJGWUIVL.js.map} +0 -0
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Validates WCAG 2.1 contrast ratios between CSS custom property pairings
|
|
5
5
|
* (--fui-text-*, --fui-bg-*, --fui-color-*) across all 5 neutral palettes
|
|
6
|
-
* (Stone, Ice, Earth, Sand, Fire) in both light and dark modes.
|
|
6
|
+
* (Stone, Ice, Earth, Sand, Fire, Fragments) in both light and dark modes.
|
|
7
7
|
*
|
|
8
8
|
* Token values are derived from the seed-derivation system — the same
|
|
9
9
|
* derivation functions that produce the runtime CSS custom properties.
|
|
@@ -133,7 +133,7 @@ function ratioOnComposite(fgHex: string, rgbaBg: string, baseHex: string): numbe
|
|
|
133
133
|
|
|
134
134
|
// ── Tests ────────────────────────────────────────────────────────────────────
|
|
135
135
|
|
|
136
|
-
const PALETTE_NAMES
|
|
136
|
+
const PALETTE_NAMES = Object.keys(PALETTES) as NeutralPalette[];
|
|
137
137
|
const brand = DEFAULT_SEEDS.brand;
|
|
138
138
|
|
|
139
139
|
for (const paletteName of PALETTE_NAMES) {
|
package/src/theme/generator.ts
CHANGED
|
@@ -109,6 +109,9 @@ const DARK_TOKEN_MAPPINGS = {
|
|
|
109
109
|
md: "fui-dark-shadow-md",
|
|
110
110
|
},
|
|
111
111
|
// Direct dark mode properties
|
|
112
|
+
accent: "fui-dark-color-accent",
|
|
113
|
+
accentHover: "fui-dark-color-accent-hover",
|
|
114
|
+
accentActive: "fui-dark-color-accent-active",
|
|
112
115
|
dangerBg: "fui-dark-color-danger-bg",
|
|
113
116
|
successBg: "fui-dark-color-success-bg",
|
|
114
117
|
warningBg: "fui-dark-color-warning-bg",
|
|
@@ -179,7 +182,7 @@ function generateDarkTokens(
|
|
|
179
182
|
}
|
|
180
183
|
|
|
181
184
|
// Handle direct dark mode properties
|
|
182
|
-
const directProps = ["dangerBg", "successBg", "warningBg", "infoBg", "dangerText", "successText", "warningText", "infoText", "backdrop"] as const;
|
|
185
|
+
const directProps = ["accent", "accentHover", "accentActive", "dangerBg", "successBg", "warningBg", "infoBg", "dangerText", "successText", "warningText", "infoText", "backdrop"] as const;
|
|
183
186
|
for (const prop of directProps) {
|
|
184
187
|
const value = config.dark[prop];
|
|
185
188
|
if (value !== undefined) {
|
|
@@ -450,6 +453,13 @@ export function generateScssTokens(config: ThemeConfig): string {
|
|
|
450
453
|
lines.push("");
|
|
451
454
|
}
|
|
452
455
|
|
|
456
|
+
// Density
|
|
457
|
+
if (config.density) {
|
|
458
|
+
lines.push("// Density");
|
|
459
|
+
lines.push(`$fui-density: "${config.density}" !default;`);
|
|
460
|
+
lines.push("");
|
|
461
|
+
}
|
|
462
|
+
|
|
453
463
|
// Dark mode
|
|
454
464
|
const darkTokens = generateDarkTokens(config, "scss");
|
|
455
465
|
if (darkTokens.length > 0) {
|
|
@@ -487,6 +497,11 @@ export function generateCssTokens(config: ThemeConfig): string {
|
|
|
487
497
|
lightTokens.push(...tokens);
|
|
488
498
|
}
|
|
489
499
|
|
|
500
|
+
// Density
|
|
501
|
+
if (config.density) {
|
|
502
|
+
lightTokens.push(` --fui-density: ${config.density};`);
|
|
503
|
+
}
|
|
504
|
+
|
|
490
505
|
if (lightTokens.length > 0) {
|
|
491
506
|
lines.push(":root {");
|
|
492
507
|
lines.push(...lightTokens);
|
package/src/theme/schema.ts
CHANGED
|
@@ -139,11 +139,18 @@ export const themeDarkModeSchema = z.object({
|
|
|
139
139
|
text: themeTextSchema.optional(),
|
|
140
140
|
borders: themeBordersSchema.optional(),
|
|
141
141
|
shadows: themeShadowsSchema.optional(),
|
|
142
|
+
accent: colorSchema.optional(),
|
|
143
|
+
accentHover: colorSchema.optional(),
|
|
144
|
+
accentActive: colorSchema.optional(),
|
|
142
145
|
dangerBg: colorSchema.optional(),
|
|
143
146
|
successBg: colorSchema.optional(),
|
|
144
147
|
warningBg: colorSchema.optional(),
|
|
145
148
|
infoBg: colorSchema.optional(),
|
|
146
149
|
backdrop: colorSchema.optional(),
|
|
150
|
+
dangerText: colorSchema.optional(),
|
|
151
|
+
successText: colorSchema.optional(),
|
|
152
|
+
warningText: colorSchema.optional(),
|
|
153
|
+
infoText: colorSchema.optional(),
|
|
147
154
|
}).strict();
|
|
148
155
|
|
|
149
156
|
/**
|
|
@@ -161,6 +168,7 @@ export const themeConfigSchema = z.object({
|
|
|
161
168
|
radius: themeRadiusSchema.optional(),
|
|
162
169
|
shadows: themeShadowsSchema.optional(),
|
|
163
170
|
dark: themeDarkModeSchema.optional(),
|
|
171
|
+
density: z.enum(['compact', 'default', 'relaxed']).optional(),
|
|
164
172
|
}).strict();
|
|
165
173
|
|
|
166
174
|
/**
|
package/src/theme/serializer.ts
CHANGED
|
@@ -9,7 +9,7 @@ import { deflateSync, inflateSync } from "node:zlib";
|
|
|
9
9
|
import { validateThemeConfig } from "./schema.js";
|
|
10
10
|
import type { ThemeConfig } from "./types.js";
|
|
11
11
|
|
|
12
|
-
const DEFAULT_BASE_URL = "https://
|
|
12
|
+
const DEFAULT_BASE_URL = "https://usefragments.com/create";
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* Convert a Buffer to base64url encoding (URL-safe base64)
|
|
@@ -61,22 +61,26 @@ export function compressTheme(config: ThemeConfig): string {
|
|
|
61
61
|
* @returns Theme configuration or null if invalid
|
|
62
62
|
*/
|
|
63
63
|
export function decompressTheme(encoded: string): ThemeConfig | null {
|
|
64
|
+
// Try zlib-compressed first (CLI native encoding)
|
|
64
65
|
try {
|
|
65
66
|
const buffer = fromBase64Url(encoded);
|
|
66
67
|
const decompressed = inflateSync(buffer);
|
|
67
68
|
const json = decompressed.toString("utf-8");
|
|
68
69
|
const parsed = JSON.parse(json);
|
|
70
|
+
const result = validateThemeConfig(parsed);
|
|
71
|
+
if (result.success) return result.data;
|
|
72
|
+
} catch { /* not zlib-compressed */ }
|
|
69
73
|
|
|
70
|
-
|
|
74
|
+
// Fall back to plain base64url JSON (browser/docs encoding)
|
|
75
|
+
try {
|
|
76
|
+
const buffer = fromBase64Url(encoded);
|
|
77
|
+
const json = buffer.toString("utf-8");
|
|
78
|
+
const parsed = JSON.parse(json);
|
|
71
79
|
const result = validateThemeConfig(parsed);
|
|
72
|
-
if (result.success)
|
|
73
|
-
|
|
74
|
-
}
|
|
80
|
+
if (result.success) return result.data;
|
|
81
|
+
} catch { /* invalid */ }
|
|
75
82
|
|
|
76
|
-
|
|
77
|
-
} catch {
|
|
78
|
-
return null;
|
|
79
|
-
}
|
|
83
|
+
return null;
|
|
80
84
|
}
|
|
81
85
|
|
|
82
86
|
/**
|
package/src/theme/types.ts
CHANGED
|
@@ -136,6 +136,12 @@ export interface ThemeDarkMode {
|
|
|
136
136
|
borders?: ThemeBorders;
|
|
137
137
|
/** Shadow overrides for dark mode */
|
|
138
138
|
shadows?: ThemeShadows;
|
|
139
|
+
/** Accent color for dark mode - $fui-dark-color-accent */
|
|
140
|
+
accent?: string;
|
|
141
|
+
/** Accent hover for dark mode - $fui-dark-color-accent-hover */
|
|
142
|
+
accentHover?: string;
|
|
143
|
+
/** Accent active for dark mode - $fui-dark-color-accent-active */
|
|
144
|
+
accentActive?: string;
|
|
139
145
|
/** Danger background for dark mode - $fui-dark-color-danger-bg */
|
|
140
146
|
dangerBg?: string;
|
|
141
147
|
/** Success background for dark mode - $fui-dark-color-success-bg */
|
|
@@ -185,6 +191,8 @@ export interface ThemeConfig {
|
|
|
185
191
|
shadows?: ThemeShadows;
|
|
186
192
|
/** Dark mode overrides */
|
|
187
193
|
dark?: ThemeDarkMode;
|
|
194
|
+
/** Density preset — controls spacing scale */
|
|
195
|
+
density?: 'compact' | 'default' | 'relaxed';
|
|
188
196
|
}
|
|
189
197
|
|
|
190
198
|
/**
|
package/src/validators.ts
CHANGED
|
@@ -6,8 +6,7 @@ import {
|
|
|
6
6
|
loadFragmentFile,
|
|
7
7
|
} from './core/node.js';
|
|
8
8
|
import { validateSnippetPolicy, type SnippetValidationOptions } from './service/snippet-validation.js';
|
|
9
|
-
import { createComponentExtractor, type ComponentMeta, type PropMeta } from '
|
|
10
|
-
import { resolveComponentSourcePath } from './core/auto-props.js';
|
|
9
|
+
import { createComponentExtractor, type ComponentMeta, type PropMeta, resolveComponentSourcePath } from '@fragments-sdk/extract';
|
|
11
10
|
import { parseFragmentFile } from './core/parser.js';
|
|
12
11
|
import { readFile } from 'node:fs/promises';
|
|
13
12
|
|
|
@@ -191,12 +191,12 @@ describe("discoverInstalledFragments", () => {
|
|
|
191
191
|
JSON.stringify({ name: "@acme/ui", fragments: { src: "src" } })
|
|
192
192
|
);
|
|
193
193
|
await writeFile(
|
|
194
|
-
resolve(acmeDir, "src/components/Button.
|
|
195
|
-
"
|
|
194
|
+
resolve(acmeDir, "src/components/Button.contract.json"),
|
|
195
|
+
"{}"
|
|
196
196
|
);
|
|
197
197
|
await writeFile(
|
|
198
|
-
resolve(acmeDir, "src/components/Card.
|
|
199
|
-
"
|
|
198
|
+
resolve(acmeDir, "src/components/Card.contract.json"),
|
|
199
|
+
"{}"
|
|
200
200
|
);
|
|
201
201
|
|
|
202
202
|
const someLibDir = resolve(tmpDir, "node_modules/some-lib");
|
|
@@ -206,8 +206,8 @@ describe("discoverInstalledFragments", () => {
|
|
|
206
206
|
JSON.stringify({ name: "some-lib" })
|
|
207
207
|
);
|
|
208
208
|
await writeFile(
|
|
209
|
-
resolve(someLibDir, "src/Foo.
|
|
210
|
-
"
|
|
209
|
+
resolve(someLibDir, "src/Foo.contract.json"),
|
|
210
|
+
"{}"
|
|
211
211
|
);
|
|
212
212
|
});
|
|
213
213
|
|
|
@@ -219,8 +219,8 @@ describe("discoverInstalledFragments", () => {
|
|
|
219
219
|
const results = await discoverInstalledFragments(tmpDir);
|
|
220
220
|
const paths = results.map((r) => r.relativePath).sort();
|
|
221
221
|
expect(paths).toEqual([
|
|
222
|
-
"@acme/ui/src/components/Button.
|
|
223
|
-
"@acme/ui/src/components/Card.
|
|
222
|
+
"@acme/ui/src/components/Button.contract.json",
|
|
223
|
+
"@acme/ui/src/components/Card.contract.json",
|
|
224
224
|
]);
|
|
225
225
|
});
|
|
226
226
|
|
|
@@ -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) {
|