@fragments-sdk/cli 0.2.2

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 (259) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +106 -0
  3. package/dist/bin.d.ts +1 -0
  4. package/dist/bin.js +4783 -0
  5. package/dist/bin.js.map +1 -0
  6. package/dist/chunk-4FDQSGKX.js +786 -0
  7. package/dist/chunk-4FDQSGKX.js.map +1 -0
  8. package/dist/chunk-7H2MMGYG.js +369 -0
  9. package/dist/chunk-7H2MMGYG.js.map +1 -0
  10. package/dist/chunk-BSCG3IP7.js +619 -0
  11. package/dist/chunk-BSCG3IP7.js.map +1 -0
  12. package/dist/chunk-LY2CFFPY.js +898 -0
  13. package/dist/chunk-LY2CFFPY.js.map +1 -0
  14. package/dist/chunk-MUZ6CM66.js +6636 -0
  15. package/dist/chunk-MUZ6CM66.js.map +1 -0
  16. package/dist/chunk-OAENNG3G.js +1489 -0
  17. package/dist/chunk-OAENNG3G.js.map +1 -0
  18. package/dist/chunk-XHNKNI6J.js +235 -0
  19. package/dist/chunk-XHNKNI6J.js.map +1 -0
  20. package/dist/core-DWKLGY4N.js +68 -0
  21. package/dist/core-DWKLGY4N.js.map +1 -0
  22. package/dist/generate-4LQNJ7SX.js +249 -0
  23. package/dist/generate-4LQNJ7SX.js.map +1 -0
  24. package/dist/index.d.ts +775 -0
  25. package/dist/index.js +41 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/init-EMVI47QG.js +416 -0
  28. package/dist/init-EMVI47QG.js.map +1 -0
  29. package/dist/mcp-bin.d.ts +1 -0
  30. package/dist/mcp-bin.js +1117 -0
  31. package/dist/mcp-bin.js.map +1 -0
  32. package/dist/scan-4YPRF7FV.js +12 -0
  33. package/dist/scan-4YPRF7FV.js.map +1 -0
  34. package/dist/service-QSZMZJBJ.js +208 -0
  35. package/dist/service-QSZMZJBJ.js.map +1 -0
  36. package/dist/static-viewer-MIPGZ4Z7.js +12 -0
  37. package/dist/static-viewer-MIPGZ4Z7.js.map +1 -0
  38. package/dist/test-SQ5ZHXWU.js +1067 -0
  39. package/dist/test-SQ5ZHXWU.js.map +1 -0
  40. package/dist/tokens-HSGMYK64.js +173 -0
  41. package/dist/tokens-HSGMYK64.js.map +1 -0
  42. package/dist/viewer-YRF4SQE4.js +11101 -0
  43. package/dist/viewer-YRF4SQE4.js.map +1 -0
  44. package/package.json +107 -0
  45. package/src/ai.ts +266 -0
  46. package/src/analyze.ts +265 -0
  47. package/src/bin.ts +916 -0
  48. package/src/build.ts +248 -0
  49. package/src/commands/a11y.ts +302 -0
  50. package/src/commands/add.ts +313 -0
  51. package/src/commands/audit.ts +195 -0
  52. package/src/commands/baseline.ts +221 -0
  53. package/src/commands/build.ts +144 -0
  54. package/src/commands/compare.ts +337 -0
  55. package/src/commands/context.ts +107 -0
  56. package/src/commands/dev.ts +107 -0
  57. package/src/commands/enhance.ts +858 -0
  58. package/src/commands/generate.ts +391 -0
  59. package/src/commands/init.ts +531 -0
  60. package/src/commands/link/figma.ts +645 -0
  61. package/src/commands/link/index.ts +10 -0
  62. package/src/commands/link/storybook.ts +267 -0
  63. package/src/commands/list.ts +49 -0
  64. package/src/commands/metrics.ts +114 -0
  65. package/src/commands/reset.ts +242 -0
  66. package/src/commands/scan.ts +537 -0
  67. package/src/commands/storygen.ts +207 -0
  68. package/src/commands/tokens.ts +251 -0
  69. package/src/commands/validate.ts +93 -0
  70. package/src/commands/verify.ts +215 -0
  71. package/src/core/composition.test.ts +262 -0
  72. package/src/core/composition.ts +255 -0
  73. package/src/core/config.ts +84 -0
  74. package/src/core/constants.ts +111 -0
  75. package/src/core/context.ts +380 -0
  76. package/src/core/defineSegment.ts +137 -0
  77. package/src/core/discovery.ts +337 -0
  78. package/src/core/figma.ts +263 -0
  79. package/src/core/fragment-types.ts +214 -0
  80. package/src/core/generators/context.ts +389 -0
  81. package/src/core/generators/index.ts +23 -0
  82. package/src/core/generators/registry.ts +364 -0
  83. package/src/core/generators/typescript-extractor.ts +374 -0
  84. package/src/core/importAnalyzer.ts +217 -0
  85. package/src/core/index.ts +149 -0
  86. package/src/core/loader.ts +155 -0
  87. package/src/core/node.ts +63 -0
  88. package/src/core/parser.ts +551 -0
  89. package/src/core/previewLoader.ts +172 -0
  90. package/src/core/schema/fragment.schema.json +189 -0
  91. package/src/core/schema/registry.schema.json +137 -0
  92. package/src/core/schema.ts +182 -0
  93. package/src/core/storyAdapter.test.ts +571 -0
  94. package/src/core/storyAdapter.ts +761 -0
  95. package/src/core/token-types.ts +287 -0
  96. package/src/core/types.ts +754 -0
  97. package/src/diff.ts +323 -0
  98. package/src/index.ts +43 -0
  99. package/src/mcp/__tests__/projectFields.test.ts +130 -0
  100. package/src/mcp/bin.ts +36 -0
  101. package/src/mcp/index.ts +8 -0
  102. package/src/mcp/server.ts +1310 -0
  103. package/src/mcp/utils.ts +54 -0
  104. package/src/mcp-bin.ts +36 -0
  105. package/src/migrate/__tests__/argTypes/argTypes.test.ts +189 -0
  106. package/src/migrate/__tests__/args/args.test.ts +452 -0
  107. package/src/migrate/__tests__/meta/meta.test.ts +198 -0
  108. package/src/migrate/__tests__/stories/stories.test.ts +278 -0
  109. package/src/migrate/__tests__/utils/utils.test.ts +371 -0
  110. package/src/migrate/__tests__/values/values.test.ts +303 -0
  111. package/src/migrate/bin.ts +108 -0
  112. package/src/migrate/converter.ts +658 -0
  113. package/src/migrate/detect.ts +196 -0
  114. package/src/migrate/index.ts +45 -0
  115. package/src/migrate/migrate.ts +163 -0
  116. package/src/migrate/parser.ts +1136 -0
  117. package/src/migrate/report.ts +624 -0
  118. package/src/migrate/types.ts +169 -0
  119. package/src/screenshot.ts +249 -0
  120. package/src/service/__tests__/ast-utils.test.ts +426 -0
  121. package/src/service/__tests__/enhance-scanner.test.ts +200 -0
  122. package/src/service/__tests__/figma/figma.test.ts +652 -0
  123. package/src/service/__tests__/metrics-store.test.ts +409 -0
  124. package/src/service/__tests__/patch-generator.test.ts +186 -0
  125. package/src/service/__tests__/props-extractor.test.ts +365 -0
  126. package/src/service/__tests__/token-registry.test.ts +267 -0
  127. package/src/service/analytics.ts +659 -0
  128. package/src/service/ast-utils.ts +444 -0
  129. package/src/service/browser-pool.ts +339 -0
  130. package/src/service/capture.ts +267 -0
  131. package/src/service/diff.ts +279 -0
  132. package/src/service/enhance/aggregator.ts +489 -0
  133. package/src/service/enhance/cache.ts +275 -0
  134. package/src/service/enhance/codebase-scanner.ts +357 -0
  135. package/src/service/enhance/context-generator.ts +529 -0
  136. package/src/service/enhance/doc-extractor.ts +523 -0
  137. package/src/service/enhance/index.ts +131 -0
  138. package/src/service/enhance/props-extractor.ts +665 -0
  139. package/src/service/enhance/scanner.ts +445 -0
  140. package/src/service/enhance/storybook-parser.ts +552 -0
  141. package/src/service/enhance/types.ts +346 -0
  142. package/src/service/enhance/variant-renderer.ts +479 -0
  143. package/src/service/figma.ts +1008 -0
  144. package/src/service/index.ts +249 -0
  145. package/src/service/metrics-store.ts +333 -0
  146. package/src/service/patch-generator.ts +349 -0
  147. package/src/service/report.ts +854 -0
  148. package/src/service/storage.ts +401 -0
  149. package/src/service/token-fixes.ts +281 -0
  150. package/src/service/token-parser.ts +504 -0
  151. package/src/service/token-registry.ts +721 -0
  152. package/src/service/utils.ts +172 -0
  153. package/src/setup.ts +241 -0
  154. package/src/shared/command-wrapper.ts +81 -0
  155. package/src/shared/dev-server-client.ts +199 -0
  156. package/src/shared/index.ts +8 -0
  157. package/src/shared/segment-loader.ts +59 -0
  158. package/src/shared/types.ts +147 -0
  159. package/src/static-viewer.ts +715 -0
  160. package/src/test/discovery.ts +172 -0
  161. package/src/test/index.ts +281 -0
  162. package/src/test/reporters/console.ts +194 -0
  163. package/src/test/reporters/json.ts +190 -0
  164. package/src/test/reporters/junit.ts +186 -0
  165. package/src/test/runner.ts +598 -0
  166. package/src/test/types.ts +245 -0
  167. package/src/test/watch.ts +200 -0
  168. package/src/validators.ts +152 -0
  169. package/src/viewer/__tests__/jsx-parser.test.ts +502 -0
  170. package/src/viewer/__tests__/render-utils.test.ts +232 -0
  171. package/src/viewer/__tests__/style-utils.test.ts +404 -0
  172. package/src/viewer/bin.ts +86 -0
  173. package/src/viewer/cli/health.ts +256 -0
  174. package/src/viewer/cli/index.ts +33 -0
  175. package/src/viewer/cli/scan.ts +124 -0
  176. package/src/viewer/cli/utils.ts +174 -0
  177. package/src/viewer/components/AccessibilityPanel.tsx +1404 -0
  178. package/src/viewer/components/ActionCapture.tsx +172 -0
  179. package/src/viewer/components/ActionsPanel.tsx +371 -0
  180. package/src/viewer/components/App.tsx +638 -0
  181. package/src/viewer/components/BottomPanel.tsx +224 -0
  182. package/src/viewer/components/CodePanel.tsx +589 -0
  183. package/src/viewer/components/CommandPalette.tsx +336 -0
  184. package/src/viewer/components/ComponentGraph.tsx +394 -0
  185. package/src/viewer/components/ComponentHeader.tsx +85 -0
  186. package/src/viewer/components/ContractPanel.tsx +234 -0
  187. package/src/viewer/components/ErrorBoundary.tsx +85 -0
  188. package/src/viewer/components/FigmaEmbed.tsx +231 -0
  189. package/src/viewer/components/FragmentEditor.tsx +485 -0
  190. package/src/viewer/components/HealthDashboard.tsx +452 -0
  191. package/src/viewer/components/HmrStatusIndicator.tsx +71 -0
  192. package/src/viewer/components/Icons.tsx +417 -0
  193. package/src/viewer/components/InteractionsPanel.tsx +720 -0
  194. package/src/viewer/components/IsolatedPreviewFrame.tsx +321 -0
  195. package/src/viewer/components/IsolatedRender.tsx +111 -0
  196. package/src/viewer/components/KeyboardShortcutsHelp.tsx +89 -0
  197. package/src/viewer/components/LandingPage.tsx +441 -0
  198. package/src/viewer/components/Layout.tsx +22 -0
  199. package/src/viewer/components/LeftSidebar.tsx +391 -0
  200. package/src/viewer/components/MultiViewportPreview.tsx +429 -0
  201. package/src/viewer/components/PreviewArea.tsx +404 -0
  202. package/src/viewer/components/PreviewFrameHost.tsx +310 -0
  203. package/src/viewer/components/PreviewPane.tsx +150 -0
  204. package/src/viewer/components/PreviewToolbar.tsx +176 -0
  205. package/src/viewer/components/PropsEditor.tsx +512 -0
  206. package/src/viewer/components/PropsTable.tsx +98 -0
  207. package/src/viewer/components/RelationsSection.tsx +57 -0
  208. package/src/viewer/components/ResizablePanel.tsx +328 -0
  209. package/src/viewer/components/RightSidebar.tsx +118 -0
  210. package/src/viewer/components/ScreenshotButton.tsx +90 -0
  211. package/src/viewer/components/Sidebar.tsx +169 -0
  212. package/src/viewer/components/SkeletonLoader.tsx +156 -0
  213. package/src/viewer/components/StoryRenderer.tsx +128 -0
  214. package/src/viewer/components/ThemeProvider.tsx +96 -0
  215. package/src/viewer/components/Toast.tsx +67 -0
  216. package/src/viewer/components/TokenStylePanel.tsx +708 -0
  217. package/src/viewer/components/UsageSection.tsx +95 -0
  218. package/src/viewer/components/VariantMatrix.tsx +350 -0
  219. package/src/viewer/components/VariantRenderer.tsx +131 -0
  220. package/src/viewer/components/VariantTabs.tsx +84 -0
  221. package/src/viewer/components/ViewportSelector.tsx +165 -0
  222. package/src/viewer/components/_future/CreatePage.tsx +836 -0
  223. package/src/viewer/composition-renderer.ts +381 -0
  224. package/src/viewer/constants/index.ts +1 -0
  225. package/src/viewer/constants/ui.ts +185 -0
  226. package/src/viewer/entry.tsx +299 -0
  227. package/src/viewer/hooks/index.ts +2 -0
  228. package/src/viewer/hooks/useA11yCache.ts +383 -0
  229. package/src/viewer/hooks/useA11yService.ts +498 -0
  230. package/src/viewer/hooks/useActions.ts +138 -0
  231. package/src/viewer/hooks/useAppState.ts +124 -0
  232. package/src/viewer/hooks/useFigmaIntegration.ts +132 -0
  233. package/src/viewer/hooks/useHmrStatus.ts +109 -0
  234. package/src/viewer/hooks/useKeyboardShortcuts.ts +222 -0
  235. package/src/viewer/hooks/usePreviewBridge.ts +347 -0
  236. package/src/viewer/hooks/useScrollSpy.ts +78 -0
  237. package/src/viewer/hooks/useUrlState.ts +330 -0
  238. package/src/viewer/hooks/useViewSettings.ts +125 -0
  239. package/src/viewer/index.html +28 -0
  240. package/src/viewer/index.ts +14 -0
  241. package/src/viewer/intelligence/healthReport.ts +505 -0
  242. package/src/viewer/intelligence/styleDrift.ts +340 -0
  243. package/src/viewer/intelligence/usageScanner.ts +309 -0
  244. package/src/viewer/jsx-parser.ts +485 -0
  245. package/src/viewer/postcss.config.js +6 -0
  246. package/src/viewer/preview-frame-entry.tsx +25 -0
  247. package/src/viewer/preview-frame.html +109 -0
  248. package/src/viewer/render-template.html +68 -0
  249. package/src/viewer/render-utils.ts +170 -0
  250. package/src/viewer/server.ts +276 -0
  251. package/src/viewer/style-utils.ts +414 -0
  252. package/src/viewer/styles/globals.css +355 -0
  253. package/src/viewer/tailwind.config.js +37 -0
  254. package/src/viewer/types/a11y.ts +197 -0
  255. package/src/viewer/utils/a11y-fixes.ts +471 -0
  256. package/src/viewer/utils/actionExport.ts +372 -0
  257. package/src/viewer/utils/colorSchemes.ts +201 -0
  258. package/src/viewer/utils/detectRelationships.ts +256 -0
  259. package/src/viewer/vite-plugin.ts +2143 -0
@@ -0,0 +1,721 @@
1
+ /**
2
+ * Token Registry
3
+ *
4
+ * Manages design tokens in memory with fast lookup capabilities:
5
+ * - By name: O(1) lookup
6
+ * - By value (reverse): O(1) lookup
7
+ * - By theme: O(1) lookup
8
+ * - By category: O(1) lookup
9
+ */
10
+
11
+ import type {
12
+ DesignToken,
13
+ TokenRegistry,
14
+ TokenRegistryMeta,
15
+ TokenCategory,
16
+ TokenConfig,
17
+ TokenMatchRequest,
18
+ TokenMatchResult,
19
+ EnhancedStyleDiffItem,
20
+ TokenUsageSummary,
21
+ } from "../core/index.js";
22
+ import { parseTokenFiles, normalizeColor, hexToRgb, parseRgb } from "./token-parser.js";
23
+
24
+ /**
25
+ * Token Registry Manager
26
+ *
27
+ * Singleton-style manager for design tokens with initialization,
28
+ * lookup, and matching capabilities.
29
+ */
30
+ export class TokenRegistryManager {
31
+ private registry: TokenRegistry | null = null;
32
+ private initPromise: Promise<void> | null = null;
33
+
34
+ /**
35
+ * Initialize the registry by parsing token files
36
+ */
37
+ async initialize(
38
+ config: TokenConfig,
39
+ projectRoot: string
40
+ ): Promise<TokenRegistry> {
41
+ // If already initializing, wait for that
42
+ if (this.initPromise) {
43
+ await this.initPromise;
44
+ return this.registry!;
45
+ }
46
+
47
+ // If already initialized, return existing
48
+ if (this.registry) {
49
+ return this.registry;
50
+ }
51
+
52
+ this.initPromise = this.doInitialize(config, projectRoot);
53
+ await this.initPromise;
54
+ this.initPromise = null;
55
+
56
+ return this.registry!;
57
+ }
58
+
59
+ private async doInitialize(
60
+ config: TokenConfig,
61
+ projectRoot: string
62
+ ): Promise<void> {
63
+ const startTime = performance.now();
64
+
65
+ // Parse all token files
66
+ const result = await parseTokenFiles(config, projectRoot);
67
+
68
+ // Build the registry
69
+ const byName = new Map<string, DesignToken>();
70
+ const byValue = new Map<string, string[]>();
71
+ const byTheme = new Map<string, DesignToken[]>();
72
+ const byCategory = new Map<TokenCategory, DesignToken[]>();
73
+
74
+ let circularRefs = 0;
75
+ let unresolvedRefs = 0;
76
+
77
+ for (const token of result.tokens) {
78
+ // Index by name
79
+ byName.set(token.name, token);
80
+
81
+ // Index by resolved value (reverse lookup)
82
+ const normalizedValue = this.normalizeForLookup(token.resolvedValue);
83
+ const existingTokens = byValue.get(normalizedValue) || [];
84
+ existingTokens.push(token.name);
85
+ byValue.set(normalizedValue, existingTokens);
86
+
87
+ // Index by theme
88
+ const themeTokens = byTheme.get(token.theme) || [];
89
+ themeTokens.push(token);
90
+ byTheme.set(token.theme, themeTokens);
91
+
92
+ // Index by category
93
+ const categoryTokens = byCategory.get(token.category) || [];
94
+ categoryTokens.push(token);
95
+ byCategory.set(token.category, categoryTokens);
96
+ }
97
+
98
+ // Count issues from warnings
99
+ for (const warning of result.warnings) {
100
+ if (warning.includes("Circular")) circularRefs++;
101
+ if (warning.includes("Unresolved")) unresolvedRefs++;
102
+ }
103
+
104
+ const meta: TokenRegistryMeta = {
105
+ discoveredAt: new Date(),
106
+ sourceFiles: config.include,
107
+ totalTokens: result.tokens.length,
108
+ parseTimeMs: performance.now() - startTime,
109
+ circularRefs,
110
+ unresolvedRefs,
111
+ };
112
+
113
+ this.registry = {
114
+ byName,
115
+ byValue,
116
+ byTheme,
117
+ byCategory,
118
+ meta,
119
+ };
120
+ }
121
+
122
+ /**
123
+ * Get the current registry (throws if not initialized)
124
+ */
125
+ getRegistry(): TokenRegistry {
126
+ if (!this.registry) {
127
+ throw new Error("Token registry not initialized. Call initialize() first.");
128
+ }
129
+ return this.registry;
130
+ }
131
+
132
+ /**
133
+ * Check if registry is initialized
134
+ */
135
+ isInitialized(): boolean {
136
+ return this.registry !== null;
137
+ }
138
+
139
+ /**
140
+ * Clear the registry (for re-initialization)
141
+ */
142
+ clear(): void {
143
+ this.registry = null;
144
+ this.initPromise = null;
145
+ }
146
+
147
+ /**
148
+ * Get a token by name
149
+ */
150
+ getToken(name: string): DesignToken | undefined {
151
+ return this.registry?.byName.get(name);
152
+ }
153
+
154
+ /**
155
+ * Find tokens by resolved value (reverse lookup)
156
+ *
157
+ * This is the key feature: given a computed value like "#0051c2",
158
+ * find which design token(s) resolve to that value.
159
+ */
160
+ findByValue(value: string, theme?: string): string[] {
161
+ if (!this.registry) return [];
162
+
163
+ const normalizedValue = this.normalizeForLookup(value);
164
+ const tokenNames = this.registry.byValue.get(normalizedValue) || [];
165
+
166
+ // Filter by theme if specified
167
+ if (theme && tokenNames.length > 0) {
168
+ return tokenNames.filter((name) => {
169
+ const token = this.registry!.byName.get(name);
170
+ return token?.theme === theme || token?.theme === "default";
171
+ });
172
+ }
173
+
174
+ return tokenNames;
175
+ }
176
+
177
+ /**
178
+ * Match a value to tokens with similarity scoring
179
+ */
180
+ matchValue(request: TokenMatchRequest): TokenMatchResult {
181
+ const { value, propertyType, theme } = request;
182
+ const exactMatches: DesignToken[] = [];
183
+ const closeMatches: Array<{ token: DesignToken; similarity: number }> = [];
184
+
185
+ if (!this.registry) {
186
+ return { exactMatches, closeMatches, found: false };
187
+ }
188
+
189
+ const normalizedValue = this.normalizeForLookup(value);
190
+
191
+ // Find exact matches
192
+ const exactNames = this.registry.byValue.get(normalizedValue) || [];
193
+ for (const name of exactNames) {
194
+ const token = this.registry.byName.get(name);
195
+ if (token) {
196
+ // Filter by theme if specified
197
+ if (!theme || token.theme === theme || token.theme === "default") {
198
+ exactMatches.push(token);
199
+ }
200
+ }
201
+ }
202
+
203
+ // For colors, find close matches
204
+ if (propertyType === "color" || this.looksLikeColor(value)) {
205
+ const targetRgb = this.parseColorToRgb(value);
206
+ if (targetRgb) {
207
+ // Get all color tokens
208
+ const colorTokens = this.registry.byCategory.get("color") || [];
209
+
210
+ for (const token of colorTokens) {
211
+ // Skip if already an exact match
212
+ if (exactMatches.includes(token)) continue;
213
+
214
+ // Filter by theme
215
+ if (theme && token.theme !== theme && token.theme !== "default") {
216
+ continue;
217
+ }
218
+
219
+ const tokenRgb = this.parseColorToRgb(token.resolvedValue);
220
+ if (tokenRgb) {
221
+ const similarity = this.colorSimilarity(targetRgb, tokenRgb);
222
+ if (similarity > 0.9) {
223
+ // 90% similar
224
+ closeMatches.push({ token, similarity });
225
+ }
226
+ }
227
+ }
228
+
229
+ // Sort by similarity descending
230
+ closeMatches.sort((a, b) => b.similarity - a.similarity);
231
+ }
232
+ }
233
+
234
+ return {
235
+ exactMatches,
236
+ closeMatches: closeMatches.slice(0, 5), // Top 5 close matches
237
+ found: exactMatches.length > 0 || closeMatches.length > 0,
238
+ };
239
+ }
240
+
241
+ /**
242
+ * Get all tokens for a theme
243
+ */
244
+ getTokensByTheme(theme: string): DesignToken[] {
245
+ return this.registry?.byTheme.get(theme) || [];
246
+ }
247
+
248
+ /**
249
+ * Get all tokens in a category
250
+ */
251
+ getTokensByCategory(category: TokenCategory): DesignToken[] {
252
+ return this.registry?.byCategory.get(category) || [];
253
+ }
254
+
255
+ /**
256
+ * Get all tokens
257
+ */
258
+ getAllTokens(): DesignToken[] {
259
+ if (!this.registry) return [];
260
+ return Array.from(this.registry.byName.values());
261
+ }
262
+
263
+ /**
264
+ * Get registry metadata
265
+ */
266
+ getMeta(): TokenRegistryMeta | undefined {
267
+ return this.registry?.meta;
268
+ }
269
+
270
+ /**
271
+ * Enhance a style diff with token information
272
+ */
273
+ enhanceStyleDiff(
274
+ figmaValue: string,
275
+ renderedValue: string,
276
+ property: string,
277
+ theme = "default"
278
+ ): Partial<EnhancedStyleDiffItem> {
279
+ const figmaTokens = this.findByValue(figmaValue, theme);
280
+ const renderedTokens = this.findByValue(renderedValue, theme);
281
+
282
+ const figmaToken = figmaTokens.length > 0 ? figmaTokens[0] : undefined;
283
+ const renderedToken = renderedTokens.length > 0 ? renderedTokens[0] : undefined;
284
+
285
+ // Determine if this is a hardcoded value
286
+ // Hardcoded = Figma matches a token, but rendered doesn't use a token
287
+ const isHardcoded = !!figmaToken && !renderedToken;
288
+
289
+ let suggestedFix: EnhancedStyleDiffItem["suggestedFix"] | undefined;
290
+
291
+ if (isHardcoded && figmaToken) {
292
+ const token = this.getToken(figmaToken);
293
+ if (token) {
294
+ const cssProperty = this.toCssProperty(property);
295
+ suggestedFix = {
296
+ tokenName: figmaToken,
297
+ tokenValue: token.resolvedValue,
298
+ codeFix: `${cssProperty}: var(${figmaToken});`,
299
+ confidence: 0.9,
300
+ reason: `Figma uses token ${figmaToken} (${token.resolvedValue}). Replace hardcoded value with token for consistency.`,
301
+ };
302
+ }
303
+ }
304
+
305
+ return {
306
+ figmaToken,
307
+ renderedToken,
308
+ isHardcoded,
309
+ suggestedFix,
310
+ };
311
+ }
312
+
313
+ /**
314
+ * Calculate token usage summary for a set of style comparisons
315
+ */
316
+ calculateUsageSummary(
317
+ styleDiffs: Array<{
318
+ property: string;
319
+ figma: string;
320
+ rendered: string;
321
+ match: boolean;
322
+ }>,
323
+ theme = "default"
324
+ ): TokenUsageSummary {
325
+ const hardcodedProperties: EnhancedStyleDiffItem[] = [];
326
+ let usingTokens = 0;
327
+ let hardcoded = 0;
328
+ let implicitMatches = 0;
329
+
330
+ for (const diff of styleDiffs) {
331
+ const enhanced = this.enhanceStyleDiff(
332
+ diff.figma,
333
+ diff.rendered,
334
+ diff.property,
335
+ theme
336
+ );
337
+
338
+ if (enhanced.renderedToken) {
339
+ usingTokens++;
340
+ } else if (enhanced.isHardcoded) {
341
+ hardcoded++;
342
+ hardcodedProperties.push({
343
+ ...diff,
344
+ ...enhanced,
345
+ isHardcoded: true,
346
+ } as EnhancedStyleDiffItem);
347
+ } else if (diff.match && enhanced.figmaToken) {
348
+ // Values match and Figma uses a token, but rendered doesn't explicitly use token
349
+ implicitMatches++;
350
+ }
351
+ }
352
+
353
+ const totalProperties = styleDiffs.length;
354
+ const compliancePercent =
355
+ totalProperties > 0
356
+ ? Math.round(((usingTokens + implicitMatches) / totalProperties) * 100)
357
+ : 100;
358
+
359
+ return {
360
+ totalProperties,
361
+ usingTokens,
362
+ hardcoded,
363
+ implicitMatches,
364
+ compliancePercent,
365
+ hardcodedProperties,
366
+ };
367
+ }
368
+
369
+ /**
370
+ * Find closest matching tokens for a value that doesn't match exactly
371
+ *
372
+ * @param value - The value to find close matches for
373
+ * @param category - Optional category to filter by (color, spacing, sizing, etc.)
374
+ * @param options - Matching options
375
+ * @returns Array of close matches sorted by distance with confidence scores
376
+ */
377
+ findClosestToken(
378
+ value: string,
379
+ category?: TokenCategory,
380
+ options: {
381
+ /** Maximum numeric difference for spacing/sizing (default: 4px) */
382
+ numericThreshold?: number;
383
+ /** Maximum color difference using Delta E (default: 5) */
384
+ colorDeltaE?: number;
385
+ /** Maximum results to return (default: 5) */
386
+ limit?: number;
387
+ /** Theme to filter by */
388
+ theme?: string;
389
+ } = {}
390
+ ): Array<{ token: DesignToken; distance: number; confidence: number }> {
391
+ if (!this.registry) return [];
392
+
393
+ const {
394
+ numericThreshold = 4,
395
+ colorDeltaE = 5,
396
+ limit = 5,
397
+ theme,
398
+ } = options;
399
+
400
+ const results: Array<{ token: DesignToken; distance: number; confidence: number }> = [];
401
+
402
+ // Determine value type
403
+ const isColor = this.looksLikeColor(value);
404
+ const numericValue = this.parseNumericValue(value);
405
+ const isNumeric = numericValue !== null;
406
+
407
+ // Get tokens to search through
408
+ let candidates: DesignToken[];
409
+ if (category) {
410
+ candidates = this.registry.byCategory.get(category) || [];
411
+ } else if (isColor) {
412
+ candidates = this.registry.byCategory.get("color") || [];
413
+ } else if (isNumeric) {
414
+ // Search through spacing, sizing, and radius tokens
415
+ candidates = [
416
+ ...(this.registry.byCategory.get("spacing") || []),
417
+ ...(this.registry.byCategory.get("sizing") || []),
418
+ ...(this.registry.byCategory.get("radius") || []),
419
+ ];
420
+ } else {
421
+ // Search all tokens
422
+ candidates = Array.from(this.registry.byName.values());
423
+ }
424
+
425
+ // Filter by theme if specified
426
+ if (theme) {
427
+ candidates = candidates.filter(
428
+ (t) => t.theme === theme || t.theme === "default"
429
+ );
430
+ }
431
+
432
+ // Calculate distances
433
+ if (isColor) {
434
+ const targetRgb = this.parseColorToRgb(value);
435
+ if (targetRgb) {
436
+ for (const token of candidates) {
437
+ const tokenRgb = this.parseColorToRgb(token.resolvedValue);
438
+ if (!tokenRgb) continue;
439
+
440
+ // Calculate Delta E (CIE76 approximation using RGB)
441
+ const deltaE = this.calculateDeltaE(targetRgb, tokenRgb);
442
+
443
+ if (deltaE <= colorDeltaE) {
444
+ // Confidence inversely proportional to deltaE
445
+ const confidence = Math.max(0, 1 - deltaE / colorDeltaE);
446
+ results.push({ token, distance: deltaE, confidence });
447
+ }
448
+ }
449
+ }
450
+ } else if (isNumeric) {
451
+ for (const token of candidates) {
452
+ const tokenNumeric = this.parseNumericValue(token.resolvedValue);
453
+ if (tokenNumeric === null) continue;
454
+
455
+ const distance = Math.abs(numericValue - tokenNumeric);
456
+
457
+ if (distance <= numericThreshold) {
458
+ // Confidence inversely proportional to distance
459
+ const confidence = Math.max(0, 1 - distance / numericThreshold);
460
+ results.push({ token, distance, confidence });
461
+ }
462
+ }
463
+ }
464
+
465
+ // Sort by distance ascending (closest first)
466
+ results.sort((a, b) => a.distance - b.distance);
467
+
468
+ return results.slice(0, limit);
469
+ }
470
+
471
+ /**
472
+ * Serialize registry for JSON export
473
+ */
474
+ toJSON(): {
475
+ tokens: DesignToken[];
476
+ meta: TokenRegistryMeta;
477
+ } {
478
+ if (!this.registry) {
479
+ return { tokens: [], meta: this.createEmptyMeta() };
480
+ }
481
+
482
+ return {
483
+ tokens: Array.from(this.registry.byName.values()),
484
+ meta: this.registry.meta,
485
+ };
486
+ }
487
+
488
+ // ----- Private helpers -----
489
+
490
+ /**
491
+ * Normalize a value for consistent lookup
492
+ */
493
+ private normalizeForLookup(value: string): string {
494
+ // Lowercase
495
+ let normalized = value.toLowerCase().trim();
496
+
497
+ // Normalize color formats
498
+ if (this.looksLikeColor(normalized)) {
499
+ const rgb = this.parseColorToRgb(value);
500
+ if (rgb) {
501
+ // Convert to lowercase hex for consistent lookup
502
+ normalized = `#${[rgb.r, rgb.g, rgb.b]
503
+ .map((x) => x.toString(16).padStart(2, "0"))
504
+ .join("")}`;
505
+ }
506
+ }
507
+
508
+ // Normalize whitespace
509
+ normalized = normalized.replace(/\s+/g, " ");
510
+
511
+ return normalized;
512
+ }
513
+
514
+ /**
515
+ * Check if a value looks like a color
516
+ */
517
+ private looksLikeColor(value: string): boolean {
518
+ return (
519
+ value.startsWith("#") ||
520
+ value.startsWith("rgb") ||
521
+ value.startsWith("hsl") ||
522
+ /^(transparent|white|black|red|blue|green|yellow|orange|purple|pink|gray|grey)$/i.test(
523
+ value
524
+ )
525
+ );
526
+ }
527
+
528
+ /**
529
+ * Parse any color format to RGB
530
+ */
531
+ private parseColorToRgb(
532
+ color: string
533
+ ): { r: number; g: number; b: number } | null {
534
+ // Handle named colors
535
+ const namedColors: Record<string, { r: number; g: number; b: number }> = {
536
+ white: { r: 255, g: 255, b: 255 },
537
+ black: { r: 0, g: 0, b: 0 },
538
+ transparent: { r: 0, g: 0, b: 0 },
539
+ red: { r: 255, g: 0, b: 0 },
540
+ green: { r: 0, g: 128, b: 0 },
541
+ blue: { r: 0, g: 0, b: 255 },
542
+ };
543
+
544
+ const lower = color.toLowerCase().trim();
545
+ if (namedColors[lower]) {
546
+ return namedColors[lower];
547
+ }
548
+
549
+ // Handle hex
550
+ if (color.startsWith("#")) {
551
+ return hexToRgb(color);
552
+ }
553
+
554
+ // Handle rgb/rgba
555
+ if (color.startsWith("rgb")) {
556
+ const parsed = parseRgb(color);
557
+ if (parsed) {
558
+ return { r: parsed.r, g: parsed.g, b: parsed.b };
559
+ }
560
+ }
561
+
562
+ return null;
563
+ }
564
+
565
+ /**
566
+ * Calculate color similarity (0-1)
567
+ */
568
+ private colorSimilarity(
569
+ c1: { r: number; g: number; b: number },
570
+ c2: { r: number; g: number; b: number }
571
+ ): number {
572
+ // Use simple Euclidean distance in RGB space
573
+ const maxDist = Math.sqrt(255 ** 2 * 3);
574
+ const dist = Math.sqrt(
575
+ (c1.r - c2.r) ** 2 + (c1.g - c2.g) ** 2 + (c1.b - c2.b) ** 2
576
+ );
577
+
578
+ return 1 - dist / maxDist;
579
+ }
580
+
581
+ /**
582
+ * Convert camelCase property to kebab-case CSS property
583
+ */
584
+ private toCssProperty(prop: string): string {
585
+ return prop.replace(/([A-Z])/g, "-$1").toLowerCase();
586
+ }
587
+
588
+ /**
589
+ * Create empty metadata
590
+ */
591
+ private createEmptyMeta(): TokenRegistryMeta {
592
+ return {
593
+ discoveredAt: new Date(),
594
+ sourceFiles: [],
595
+ totalTokens: 0,
596
+ parseTimeMs: 0,
597
+ circularRefs: 0,
598
+ unresolvedRefs: 0,
599
+ };
600
+ }
601
+
602
+ /**
603
+ * Parse a numeric value from a CSS value string
604
+ * Handles px, rem, em, % and bare numbers
605
+ */
606
+ private parseNumericValue(value: string): number | null {
607
+ const trimmed = value.trim().toLowerCase();
608
+
609
+ // Match numeric values with optional units
610
+ const match = trimmed.match(/^(-?\d*\.?\d+)(px|rem|em|%)?$/);
611
+ if (!match) return null;
612
+
613
+ const num = parseFloat(match[1]);
614
+ const unit = match[2];
615
+
616
+ // Convert to px for comparison
617
+ switch (unit) {
618
+ case "rem":
619
+ return num * 16; // Assume 16px base
620
+ case "em":
621
+ return num * 16; // Approximate
622
+ case "%":
623
+ return null; // Percentages are relative, skip
624
+ case "px":
625
+ default:
626
+ return num;
627
+ }
628
+ }
629
+
630
+ /**
631
+ * Calculate Delta E (CIE76) color difference
632
+ * Uses simplified RGB to Lab approximation
633
+ * Lower values = more similar (0 = identical, >5 = noticeably different)
634
+ */
635
+ private calculateDeltaE(
636
+ c1: { r: number; g: number; b: number },
637
+ c2: { r: number; g: number; b: number }
638
+ ): number {
639
+ // Convert RGB to Lab (simplified)
640
+ const lab1 = this.rgbToLab(c1);
641
+ const lab2 = this.rgbToLab(c2);
642
+
643
+ // CIE76 Delta E formula
644
+ return Math.sqrt(
645
+ Math.pow(lab2.l - lab1.l, 2) +
646
+ Math.pow(lab2.a - lab1.a, 2) +
647
+ Math.pow(lab2.b - lab1.b, 2)
648
+ );
649
+ }
650
+
651
+ /**
652
+ * Convert RGB to Lab color space (simplified approximation)
653
+ */
654
+ private rgbToLab(rgb: { r: number; g: number; b: number }): { l: number; a: number; b: number } {
655
+ // Normalize RGB values
656
+ let r = rgb.r / 255;
657
+ let g = rgb.g / 255;
658
+ let b = rgb.b / 255;
659
+
660
+ // Apply gamma correction
661
+ r = r > 0.04045 ? Math.pow((r + 0.055) / 1.055, 2.4) : r / 12.92;
662
+ g = g > 0.04045 ? Math.pow((g + 0.055) / 1.055, 2.4) : g / 12.92;
663
+ b = b > 0.04045 ? Math.pow((b + 0.055) / 1.055, 2.4) : b / 12.92;
664
+
665
+ // Convert to XYZ
666
+ let x = (r * 0.4124564 + g * 0.3575761 + b * 0.1804375) / 0.95047;
667
+ let y = (r * 0.2126729 + g * 0.7151522 + b * 0.0721750);
668
+ let z = (r * 0.0193339 + g * 0.1191920 + b * 0.9503041) / 1.08883;
669
+
670
+ // Convert to Lab
671
+ x = x > 0.008856 ? Math.pow(x, 1/3) : (7.787 * x) + 16/116;
672
+ y = y > 0.008856 ? Math.pow(y, 1/3) : (7.787 * y) + 16/116;
673
+ z = z > 0.008856 ? Math.pow(z, 1/3) : (7.787 * z) + 16/116;
674
+
675
+ return {
676
+ l: (116 * y) - 16,
677
+ a: 500 * (x - y),
678
+ b: 200 * (y - z),
679
+ };
680
+ }
681
+ }
682
+
683
+ /**
684
+ * Shared singleton instance
685
+ */
686
+ let sharedRegistry: TokenRegistryManager | null = null;
687
+
688
+ /**
689
+ * Get or create the shared token registry
690
+ */
691
+ export function getSharedTokenRegistry(): TokenRegistryManager {
692
+ if (!sharedRegistry) {
693
+ sharedRegistry = new TokenRegistryManager();
694
+ }
695
+ return sharedRegistry;
696
+ }
697
+
698
+ /**
699
+ * Initialize the shared registry
700
+ */
701
+ export async function initializeSharedRegistry(
702
+ config: TokenConfig,
703
+ projectRoot: string
704
+ ): Promise<TokenRegistry> {
705
+ return getSharedTokenRegistry().initialize(config, projectRoot);
706
+ }
707
+
708
+ /**
709
+ * Clear the shared registry
710
+ */
711
+ export function clearSharedRegistry(): void {
712
+ sharedRegistry?.clear();
713
+ sharedRegistry = null;
714
+ }
715
+
716
+ /**
717
+ * Create a standalone registry (not shared)
718
+ */
719
+ export function createTokenRegistry(): TokenRegistryManager {
720
+ return new TokenRegistryManager();
721
+ }