@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.
Files changed (181) hide show
  1. package/README.md +0 -3
  2. package/dist/{ai-client-I6MDWNYA.js → ai-client-LSLQGOMM.js} +1 -2
  3. package/dist/bin.js +4745 -3817
  4. package/dist/bin.js.map +1 -1
  5. package/dist/{chunk-TXFCEDOC.js → chunk-2WXKALIG.js} +2 -2
  6. package/dist/{chunk-I34BC3CU.js → chunk-32LIWN2P.js} +1006 -3
  7. package/dist/chunk-32LIWN2P.js.map +1 -0
  8. package/dist/chunk-5JF26E55.js +1255 -0
  9. package/dist/chunk-5JF26E55.js.map +1 -0
  10. package/dist/{chunk-APTQIBS5.js → chunk-6SQPP47U.js} +153 -1342
  11. package/dist/chunk-6SQPP47U.js.map +1 -0
  12. package/dist/chunk-7DZC4YEV.js +294 -0
  13. package/dist/chunk-7DZC4YEV.js.map +1 -0
  14. package/dist/{chunk-PJT5IZ37.js → chunk-BJE3425I.js} +19 -52
  15. package/dist/{chunk-PJT5IZ37.js.map → chunk-BJE3425I.js.map} +1 -1
  16. package/dist/{chunk-55KERLWL.js → chunk-HQ6A6DTV.js} +1587 -1073
  17. package/dist/chunk-HQ6A6DTV.js.map +1 -0
  18. package/dist/chunk-MHIBEEW4.js +511 -0
  19. package/dist/chunk-MHIBEEW4.js.map +1 -0
  20. package/dist/{chunk-5A6X2Y73.js → chunk-ONUP6Z4W.js} +25 -13
  21. package/dist/chunk-ONUP6Z4W.js.map +1 -0
  22. package/dist/chunk-QCN35LJU.js +630 -0
  23. package/dist/chunk-QCN35LJU.js.map +1 -0
  24. package/dist/chunk-T47OLCSF.js +36 -0
  25. package/dist/chunk-T47OLCSF.js.map +1 -0
  26. package/dist/codebase-scanner-MQHUZC2G.js +21 -0
  27. package/dist/converter-7XM3Y6NJ.js +33 -0
  28. package/dist/converter-7XM3Y6NJ.js.map +1 -0
  29. package/dist/core/index.js +43 -2
  30. package/dist/create-IH4R45GE.js +806 -0
  31. package/dist/create-IH4R45GE.js.map +1 -0
  32. package/dist/{generate-RYWIPDN2.js → generate-PVOLUAAC.js} +4 -6
  33. package/dist/{generate-RYWIPDN2.js.map → generate-PVOLUAAC.js.map} +1 -1
  34. package/dist/govern-scan-OYFZYOQW.js +413 -0
  35. package/dist/govern-scan-OYFZYOQW.js.map +1 -0
  36. package/dist/index.d.ts +4 -23
  37. package/dist/index.js +15 -14
  38. package/dist/index.js.map +1 -1
  39. package/dist/{init-WRUSW7R5.js → init-SSGUSP7Z.js} +131 -129
  40. package/dist/init-SSGUSP7Z.js.map +1 -0
  41. package/dist/{init-cloud-REQ3XLHO.js → init-cloud-3DNKPWFB.js} +30 -5
  42. package/dist/{init-cloud-REQ3XLHO.js.map → init-cloud-3DNKPWFB.js.map} +1 -1
  43. package/dist/mcp-bin.js +5 -37
  44. package/dist/mcp-bin.js.map +1 -1
  45. package/dist/node-37AUE74M.js +65 -0
  46. package/dist/push-contracts-WY32TFP6.js +84 -0
  47. package/dist/push-contracts-WY32TFP6.js.map +1 -0
  48. package/dist/scan-PKSYSTRR.js +15 -0
  49. package/dist/{scan-generate-TFZVL3BT.js → scan-generate-VY27PIOX.js} +340 -52
  50. package/dist/scan-generate-VY27PIOX.js.map +1 -0
  51. package/dist/scanner-4KZNOXAK.js +12 -0
  52. package/dist/{service-HKJ6B7P7.js → service-QJGWUIVL.js} +41 -30
  53. package/dist/{snapshot-C5DYIGIV.js → snapshot-WIJMEIFT.js} +2 -3
  54. package/dist/{snapshot-C5DYIGIV.js.map → snapshot-WIJMEIFT.js.map} +1 -1
  55. package/dist/{static-viewer-DUVC4UIM.js → static-viewer-7QIBQZRC.js} +3 -4
  56. package/dist/static-viewer-7QIBQZRC.js.map +1 -0
  57. package/dist/{test-JW7JIDFG.js → test-64Z5BKBA.js} +4 -7
  58. package/dist/{test-JW7JIDFG.js.map → test-64Z5BKBA.js.map} +1 -1
  59. package/dist/token-normalizer-TEPOVBPV.js +312 -0
  60. package/dist/token-normalizer-TEPOVBPV.js.map +1 -0
  61. package/dist/token-parser-32KOIOFN.js +22 -0
  62. package/dist/token-parser-32KOIOFN.js.map +1 -0
  63. package/dist/{tokens-KE73G5JC.js → tokens-NZWFQIAB.js} +10 -9
  64. package/dist/{tokens-KE73G5JC.js.map → tokens-NZWFQIAB.js.map} +1 -1
  65. package/dist/tokens-generate-5JQSJ27E.js +85 -0
  66. package/dist/tokens-generate-5JQSJ27E.js.map +1 -0
  67. package/dist/tokens-push-HY3KO36V.js +148 -0
  68. package/dist/tokens-push-HY3KO36V.js.map +1 -0
  69. package/package.json +8 -6
  70. package/src/bin.ts +300 -48
  71. package/src/commands/__fixtures__/shadcn-label-wrapper/package.json +7 -0
  72. package/src/commands/__fixtures__/shadcn-label-wrapper/src/components/ui/label.contract.json +42 -0
  73. package/src/commands/__fixtures__/shadcn-label-wrapper/src/components/ui/label.tsx +11 -0
  74. package/src/commands/__fixtures__/shadcn-label-wrapper/src/components/ui/primitive.contract.json +20 -0
  75. package/src/commands/__fixtures__/shadcn-label-wrapper/src/components/ui/primitive.tsx +14 -0
  76. package/src/commands/__fixtures__/shadcn-label-wrapper/tsconfig.app.json +23 -0
  77. package/src/commands/__tests__/build-freshness.test.ts +231 -0
  78. package/src/commands/__tests__/create.test.ts +71 -0
  79. package/src/commands/__tests__/drift-sync.test.ts +1 -1
  80. package/src/commands/__tests__/govern.test.ts +258 -0
  81. package/src/commands/__tests__/init.test.ts +113 -0
  82. package/src/commands/__tests__/scan-generate.test.ts +189 -70
  83. package/src/commands/__tests__/verify.test.ts +91 -0
  84. package/src/commands/build.ts +54 -1
  85. package/src/commands/context.ts +1 -1
  86. package/src/commands/create.ts +536 -0
  87. package/src/commands/discover.ts +151 -0
  88. package/src/commands/doctor.ts +3 -2
  89. package/src/commands/enhance.ts +3 -1
  90. package/src/commands/govern-scan.ts +565 -0
  91. package/src/commands/govern.ts +67 -4
  92. package/src/commands/init-cloud.ts +32 -4
  93. package/src/commands/init.ts +152 -28
  94. package/src/commands/inspect.ts +290 -0
  95. package/src/commands/migrate-contract.ts +85 -0
  96. package/src/commands/push-contracts.ts +112 -0
  97. package/src/commands/scan-generate.ts +439 -51
  98. package/src/commands/scan.ts +14 -0
  99. package/src/commands/setup.ts +27 -50
  100. package/src/commands/sync.ts +2 -2
  101. package/src/commands/tokens-generate.ts +113 -0
  102. package/src/commands/tokens-push.ts +199 -0
  103. package/src/commands/verify.ts +195 -1
  104. package/src/core/__fixtures__/shadcn-input/input.tsx +7 -0
  105. package/src/core/__fixtures__/shadcn-input/tsconfig.json +14 -0
  106. package/src/core/__fixtures__/shadcn-label/label.tsx +11 -0
  107. package/src/core/__fixtures__/shadcn-label/primitive.tsx +14 -0
  108. package/src/core/__fixtures__/shadcn-label/tsconfig.json +14 -0
  109. package/src/core/__fixtures__/shadcn-radix-label/label.tsx +11 -0
  110. package/src/core/__fixtures__/shadcn-radix-label/node_modules/radix-ui/index.d.ts +12 -0
  111. package/src/core/__fixtures__/shadcn-radix-label/tsconfig.json +14 -0
  112. package/src/core/__tests__/contract-parity.test.ts +316 -0
  113. package/src/core/__tests__/token-resolver.test.ts +1 -1
  114. package/src/core/component-extractor.test.ts +40 -1
  115. package/src/core/config.ts +2 -1
  116. package/src/core/discovery.ts +13 -2
  117. package/src/core/drift-verifier.ts +123 -0
  118. package/src/core/extractor-adapter.ts +80 -0
  119. package/src/index.ts +3 -3
  120. package/src/mcp/__tests__/projectFields.test.ts +1 -1
  121. package/src/mcp/utils.ts +1 -50
  122. package/src/migrate/converter.ts +3 -3
  123. package/src/migrate/fragment-to-contract.ts +253 -0
  124. package/src/migrate/report.ts +1 -1
  125. package/src/scripts/token-benchmark.ts +121 -0
  126. package/src/service/__tests__/props-extractor.test.ts +94 -0
  127. package/src/service/__tests__/token-normalizer.test.ts +690 -0
  128. package/src/service/ast-utils.ts +4 -23
  129. package/src/service/babel-config.ts +23 -0
  130. package/src/service/enhance/converter.ts +61 -0
  131. package/src/service/enhance/props-extractor.ts +25 -8
  132. package/src/service/enhance/scanner.ts +5 -24
  133. package/src/service/index.ts +8 -0
  134. package/src/service/snippet-validation.ts +9 -3
  135. package/src/service/tailwind-v4-parser.ts +314 -0
  136. package/src/service/token-normalizer.ts +510 -0
  137. package/src/service/token-parser.ts +56 -0
  138. package/src/setup.ts +10 -39
  139. package/src/shared/index.ts +1 -0
  140. package/src/shared/project-fields.ts +46 -0
  141. package/src/theme/__tests__/component-contrast.test.ts +2 -2
  142. package/src/theme/__tests__/serializer.test.ts +1 -1
  143. package/src/theme/generator.ts +16 -1
  144. package/src/theme/schema.ts +8 -0
  145. package/src/theme/serializer.ts +13 -9
  146. package/src/theme/types.ts +8 -0
  147. package/src/validators.ts +1 -2
  148. package/src/viewer/__tests__/viewer-integration.test.ts +8 -8
  149. package/src/viewer/style-utils.ts +27 -412
  150. package/src/viewer/vite-plugin.ts +2 -2
  151. package/dist/chunk-55KERLWL.js.map +0 -1
  152. package/dist/chunk-5A6X2Y73.js.map +0 -1
  153. package/dist/chunk-APTQIBS5.js.map +0 -1
  154. package/dist/chunk-EYXVAMEX.js +0 -626
  155. package/dist/chunk-EYXVAMEX.js.map +0 -1
  156. package/dist/chunk-I34BC3CU.js.map +0 -1
  157. package/dist/chunk-LOYS64QS.js +0 -2453
  158. package/dist/chunk-LOYS64QS.js.map +0 -1
  159. package/dist/chunk-Z7EY4VHE.js +0 -50
  160. package/dist/chunk-ZKTFKHWN.js +0 -324
  161. package/dist/chunk-ZKTFKHWN.js.map +0 -1
  162. package/dist/discovery-VDANZAJ2.js +0 -28
  163. package/dist/init-WRUSW7R5.js.map +0 -1
  164. package/dist/sass.node-4XJK6YBF.js +0 -130708
  165. package/dist/sass.node-4XJK6YBF.js.map +0 -1
  166. package/dist/scan-YJHQIRKG.js +0 -14
  167. package/dist/scan-generate-TFZVL3BT.js.map +0 -1
  168. package/dist/viewer-2TZS3NDL.js +0 -2730
  169. package/dist/viewer-2TZS3NDL.js.map +0 -1
  170. package/src/build.ts +0 -612
  171. package/src/commands/dev.ts +0 -107
  172. package/src/core/auto-props.ts +0 -464
  173. package/src/core/component-extractor.ts +0 -1030
  174. package/src/core/token-resolver.ts +0 -155
  175. /package/dist/{ai-client-I6MDWNYA.js.map → ai-client-LSLQGOMM.js.map} +0 -0
  176. /package/dist/{chunk-TXFCEDOC.js.map → chunk-2WXKALIG.js.map} +0 -0
  177. /package/dist/{chunk-Z7EY4VHE.js.map → codebase-scanner-MQHUZC2G.js.map} +0 -0
  178. /package/dist/{discovery-VDANZAJ2.js.map → node-37AUE74M.js.map} +0 -0
  179. /package/dist/{scan-YJHQIRKG.js.map → scan-PKSYSTRR.js.map} +0 -0
  180. /package/dist/{service-HKJ6B7P7.js.map → scanner-4KZNOXAK.js.map} +0 -0
  181. /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: NeutralPalette[] = ["stone", "ice", "earth", "sand", "fire"];
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) {
@@ -282,7 +282,7 @@ describe("Theme Serializer", () => {
282
282
  };
283
283
 
284
284
  const url = encodeThemeToUrl(theme);
285
- expect(url).toMatch(/^https:\/\/fragments\.dev\/init\?preset=/);
285
+ expect(url).toMatch(/^https:\/\/usefragments\.com\/create\?preset=/);
286
286
  });
287
287
  });
288
288
 
@@ -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);
@@ -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
  /**
@@ -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://fragments.dev/init";
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
- // Validate the parsed object
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
- return result.data;
74
- }
80
+ if (result.success) return result.data;
81
+ } catch { /* invalid */ }
75
82
 
76
- return null;
77
- } catch {
78
- return null;
79
- }
83
+ return null;
80
84
  }
81
85
 
82
86
  /**
@@ -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 './core/component-extractor.js';
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.fragment.tsx"),
195
- "export default {};"
194
+ resolve(acmeDir, "src/components/Button.contract.json"),
195
+ "{}"
196
196
  );
197
197
  await writeFile(
198
- resolve(acmeDir, "src/components/Card.fragment.tsx"),
199
- "export default {};"
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.fragment.tsx"),
210
- "export default {};"
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.fragment.tsx",
223
- "@acme/ui/src/components/Card.fragment.tsx",
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 for comparing Figma design properties
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
- * This enhanced version:
255
- * 1. Performs normal style comparison
256
- * 2. Identifies which values match design tokens
257
- * 3. Flags hardcoded values that should use tokens
258
- * 4. Generates fix suggestions
259
- */
260
- export function compareStylesWithTokens(
261
- figmaStyles: Record<string, string | undefined>,
262
- renderedStyles: Record<string, string>,
263
- tokenLookup?: TokenLookup,
264
- theme = "default"
265
- ): EnhancedStyleComparisonResult {
266
- const properties: EnhancedStyleDiffItem[] = [];
267
- const cleanFigmaStyles: Record<string, string> = {};
268
-
269
- // Properties to compare
270
- const propsToCompare = [
271
- "backgroundColor",
272
- "borderColor",
273
- "borderWidth",
274
- "borderRadius",
275
- "fontFamily",
276
- "fontSize",
277
- "fontWeight",
278
- "lineHeight",
279
- "letterSpacing",
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) {