@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,504 @@
1
+ /**
2
+ * Token Parser
3
+ *
4
+ * Parses CSS/SCSS files to extract CSS custom properties (design tokens).
5
+ * Handles:
6
+ * - CSS custom property declarations (--token-name: value)
7
+ * - Reference resolution (var(--other-token))
8
+ * - Theme detection via selectors
9
+ * - Category inference from naming conventions
10
+ */
11
+
12
+ import { readFile } from "node:fs/promises";
13
+ import { resolve, relative } from "node:path";
14
+ import fastGlob from "fast-glob";
15
+ import type {
16
+ DesignToken,
17
+ TokenCategory,
18
+ TokenConfig,
19
+ TokenParseResult,
20
+ TokenParseError,
21
+ } from "../core/index.js";
22
+
23
+ /**
24
+ * Pattern to match CSS custom property declarations
25
+ * Captures: [full match, property name, value]
26
+ * Example: "--color-primary: var(--color-cobalt-50);"
27
+ */
28
+ const TOKEN_DECLARATION_PATTERN =
29
+ /--([a-zA-Z0-9_-]+)\s*:\s*([^;]+);/g;
30
+
31
+ /**
32
+ * Pattern to match var() references
33
+ * Captures: [full match, token name, fallback value?]
34
+ * Example: "var(--color-cobalt-50)" or "var(--color-primary, #fff)"
35
+ */
36
+ const VAR_REFERENCE_PATTERN =
37
+ /var\(\s*--([a-zA-Z0-9_-]+)(?:\s*,\s*([^)]+))?\s*\)/g;
38
+
39
+ /**
40
+ * Pattern to match CSS selectors (to detect theme blocks)
41
+ */
42
+ const SELECTOR_PATTERN = /([^{]+)\s*\{/g;
43
+
44
+ /**
45
+ * Category inference patterns - maps naming conventions to categories
46
+ */
47
+ const CATEGORY_PATTERNS: Array<{
48
+ pattern: RegExp;
49
+ category: TokenCategory;
50
+ }> = [
51
+ { pattern: /color|bg|background|border-color|fill|stroke/i, category: "color" },
52
+ { pattern: /spacing|margin|padding|gap|space|inset/i, category: "spacing" },
53
+ { pattern: /font|text|line-height|letter-spacing|typography/i, category: "typography" },
54
+ { pattern: /radius|rounded|corner/i, category: "radius" },
55
+ { pattern: /shadow|elevation/i, category: "shadow" },
56
+ { pattern: /size|width|height|min|max/i, category: "sizing" },
57
+ { pattern: /border(?!-color)|stroke-width|outline/i, category: "border" },
58
+ { pattern: /animation|transition|duration|timing|delay/i, category: "animation" },
59
+ { pattern: /z-index|layer|stack/i, category: "z-index" },
60
+ ];
61
+
62
+ /**
63
+ * Parse a single CSS/SCSS file for tokens
64
+ */
65
+ export async function parseTokenFile(
66
+ filePath: string,
67
+ themeSelectors: Record<string, string> = { ":root": "default" },
68
+ projectRoot?: string
69
+ ): Promise<TokenParseResult> {
70
+ const startTime = performance.now();
71
+ const tokens: DesignToken[] = [];
72
+ const errors: TokenParseError[] = [];
73
+ const warnings: string[] = [];
74
+
75
+ try {
76
+ const content = await readFile(filePath, "utf-8");
77
+ const relativePath = projectRoot
78
+ ? relative(projectRoot, filePath)
79
+ : filePath;
80
+
81
+ // Track which tokens we've seen for reference resolution
82
+ const tokensByName = new Map<string, { rawValue: string; line?: number }>();
83
+
84
+ // First pass: collect all token declarations
85
+ let lineNumber = 1;
86
+ const lines = content.split("\n");
87
+
88
+ // Track current selector/theme context
89
+ let currentSelector = ":root";
90
+ let braceDepth = 0;
91
+ const selectorStack: string[] = [];
92
+
93
+ for (let i = 0; i < lines.length; i++) {
94
+ const line = lines[i];
95
+ lineNumber = i + 1;
96
+
97
+ // Track brace depth for selector context
98
+ const openBraces = (line.match(/\{/g) || []).length;
99
+ const closeBraces = (line.match(/\}/g) || []).length;
100
+
101
+ // Check for selector at start of line (simple heuristic)
102
+ if (openBraces > closeBraces) {
103
+ // Entering a new block
104
+ const selectorMatch = line.match(/^\s*([^{]+)\s*\{/);
105
+ if (selectorMatch) {
106
+ selectorStack.push(selectorMatch[1].trim());
107
+ currentSelector = selectorStack[selectorStack.length - 1];
108
+ }
109
+ braceDepth += openBraces - closeBraces;
110
+ } else if (closeBraces > openBraces) {
111
+ braceDepth -= closeBraces - openBraces;
112
+ if (braceDepth >= 0 && selectorStack.length > 0) {
113
+ selectorStack.pop();
114
+ currentSelector = selectorStack.length > 0
115
+ ? selectorStack[selectorStack.length - 1]
116
+ : ":root";
117
+ }
118
+ }
119
+
120
+ // Find token declarations in this line
121
+ const tokenMatches = [...line.matchAll(TOKEN_DECLARATION_PATTERN)];
122
+ for (const match of tokenMatches) {
123
+ const [, name, rawValue] = match;
124
+ const fullName = `--${name}`;
125
+ tokensByName.set(fullName, { rawValue: rawValue.trim(), line: lineNumber });
126
+ }
127
+ }
128
+
129
+ // Second pass: resolve references and build token objects
130
+ for (const [name, { rawValue, line }] of tokensByName) {
131
+ // Find the selector context for this token
132
+ // Re-parse to get correct selector (simplified - uses last found)
133
+ const selector = findSelectorForLine(content, line || 1);
134
+ const theme = themeSelectors[selector] || "default";
135
+
136
+ // Resolve the value
137
+ const { resolvedValue, chain, hasCircular, unresolvedRef } = resolveValue(
138
+ rawValue,
139
+ tokensByName
140
+ );
141
+
142
+ if (hasCircular) {
143
+ warnings.push(
144
+ `Circular reference detected for ${name} at line ${line}`
145
+ );
146
+ }
147
+
148
+ if (unresolvedRef) {
149
+ warnings.push(
150
+ `Unresolved reference in ${name}: ${unresolvedRef}`
151
+ );
152
+ }
153
+
154
+ // Infer category from name
155
+ const category = inferCategory(name);
156
+
157
+ // Infer token level
158
+ const level = inferLevel(name, rawValue, chain);
159
+
160
+ // Extract description from preceding comment (if any)
161
+ const description = extractDescription(content, line || 1);
162
+
163
+ tokens.push({
164
+ name,
165
+ rawValue,
166
+ resolvedValue,
167
+ category,
168
+ level,
169
+ referenceChain: chain,
170
+ sourceFile: relativePath,
171
+ lineNumber: line,
172
+ theme,
173
+ selector,
174
+ description,
175
+ });
176
+ }
177
+ } catch (error) {
178
+ errors.push({
179
+ message: error instanceof Error ? error.message : "Unknown error",
180
+ file: filePath,
181
+ });
182
+ }
183
+
184
+ return {
185
+ tokens,
186
+ errors,
187
+ warnings,
188
+ parseTimeMs: performance.now() - startTime,
189
+ };
190
+ }
191
+
192
+ /**
193
+ * Parse multiple files based on config
194
+ */
195
+ export async function parseTokenFiles(
196
+ config: TokenConfig,
197
+ projectRoot: string
198
+ ): Promise<TokenParseResult> {
199
+ const startTime = performance.now();
200
+ const allTokens: DesignToken[] = [];
201
+ const allErrors: TokenParseError[] = [];
202
+ const allWarnings: string[] = [];
203
+
204
+ // Discover files
205
+ const files = await fastGlob(config.include, {
206
+ cwd: projectRoot,
207
+ ignore: config.exclude || ["**/node_modules/**"],
208
+ absolute: true,
209
+ });
210
+
211
+ if (files.length === 0) {
212
+ allWarnings.push(
213
+ `No token files found matching: ${config.include.join(", ")}`
214
+ );
215
+ }
216
+
217
+ // Parse each file
218
+ for (const file of files) {
219
+ const result = await parseTokenFile(
220
+ file,
221
+ config.themeSelectors,
222
+ projectRoot
223
+ );
224
+
225
+ allTokens.push(...result.tokens);
226
+ allErrors.push(...result.errors);
227
+ allWarnings.push(...result.warnings);
228
+ }
229
+
230
+ return {
231
+ tokens: allTokens,
232
+ errors: allErrors,
233
+ warnings: allWarnings,
234
+ parseTimeMs: performance.now() - startTime,
235
+ };
236
+ }
237
+
238
+ /**
239
+ * Resolve a token value, following var() references
240
+ */
241
+ function resolveValue(
242
+ rawValue: string,
243
+ tokensByName: Map<string, { rawValue: string; line?: number }>,
244
+ visited = new Set<string>()
245
+ ): {
246
+ resolvedValue: string;
247
+ chain: string[];
248
+ hasCircular: boolean;
249
+ unresolvedRef?: string;
250
+ } {
251
+ const chain: string[] = [];
252
+ let current = rawValue;
253
+ let hasCircular = false;
254
+ let unresolvedRef: string | undefined;
255
+
256
+ // Maximum iterations to prevent infinite loops
257
+ const maxIterations = 20;
258
+ let iterations = 0;
259
+
260
+ while (iterations < maxIterations) {
261
+ iterations++;
262
+
263
+ // Check for var() reference
264
+ const varMatch = current.match(/var\(\s*--([a-zA-Z0-9_-]+)(?:\s*,\s*([^)]+))?\s*\)/);
265
+
266
+ if (!varMatch) {
267
+ // No more var() references, we have the resolved value
268
+ break;
269
+ }
270
+
271
+ const [, refName, fallback] = varMatch;
272
+ const fullRefName = `--${refName}`;
273
+
274
+ // Check for circular reference
275
+ if (visited.has(fullRefName)) {
276
+ hasCircular = true;
277
+ break;
278
+ }
279
+
280
+ visited.add(fullRefName);
281
+ chain.push(fullRefName);
282
+
283
+ // Look up the referenced token
284
+ const refToken = tokensByName.get(fullRefName);
285
+
286
+ if (refToken) {
287
+ // Replace var() with the referenced value
288
+ current = current.replace(
289
+ varMatch[0],
290
+ refToken.rawValue
291
+ );
292
+ } else if (fallback) {
293
+ // Use fallback value
294
+ current = current.replace(varMatch[0], fallback.trim());
295
+ } else {
296
+ // Unresolved reference
297
+ unresolvedRef = fullRefName;
298
+ break;
299
+ }
300
+ }
301
+
302
+ return {
303
+ resolvedValue: normalizeValue(current.trim()),
304
+ chain,
305
+ hasCircular,
306
+ unresolvedRef,
307
+ };
308
+ }
309
+
310
+ /**
311
+ * Normalize a CSS value for consistent comparison
312
+ */
313
+ function normalizeValue(value: string): string {
314
+ // Lowercase hex colors
315
+ value = value.replace(/#[0-9a-fA-F]+/g, (match) => match.toLowerCase());
316
+
317
+ // Normalize whitespace
318
+ value = value.replace(/\s+/g, " ").trim();
319
+
320
+ // Normalize rgb/rgba spacing
321
+ value = value.replace(
322
+ /rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*(?:,\s*([\d.]+))?\s*\)/g,
323
+ (_, r, g, b, a) => a !== undefined ? `rgba(${r}, ${g}, ${b}, ${a})` : `rgb(${r}, ${g}, ${b})`
324
+ );
325
+
326
+ return value;
327
+ }
328
+
329
+ /**
330
+ * Infer token category from name
331
+ */
332
+ function inferCategory(name: string): TokenCategory {
333
+ const lowerName = name.toLowerCase();
334
+
335
+ for (const { pattern, category } of CATEGORY_PATTERNS) {
336
+ if (pattern.test(lowerName)) {
337
+ return category;
338
+ }
339
+ }
340
+
341
+ return "other";
342
+ }
343
+
344
+ /**
345
+ * Infer token level (1=base, 2=semantic, 3=component)
346
+ */
347
+ function inferLevel(
348
+ name: string,
349
+ rawValue: string,
350
+ referenceChain: string[]
351
+ ): 1 | 2 | 3 {
352
+ const lowerName = name.toLowerCase();
353
+
354
+ // Component-specific tokens (often contain component names)
355
+ if (
356
+ /btn|button|input|card|modal|dialog|menu|nav|header|footer|table|form/i.test(
357
+ lowerName
358
+ )
359
+ ) {
360
+ return 3;
361
+ }
362
+
363
+ // Semantic tokens (references other tokens)
364
+ if (referenceChain.length > 0) {
365
+ return 2;
366
+ }
367
+
368
+ // Base tokens (raw values like hex colors, numbers)
369
+ if (
370
+ rawValue.match(/^#[0-9a-fA-F]+$/) ||
371
+ rawValue.match(/^\d+(\.\d+)?(px|rem|em|%|vh|vw)?$/)
372
+ ) {
373
+ return 1;
374
+ }
375
+
376
+ // Default to semantic if unclear
377
+ return 2;
378
+ }
379
+
380
+ /**
381
+ * Find the CSS selector that contains a given line
382
+ */
383
+ function findSelectorForLine(content: string, targetLine: number): string {
384
+ const lines = content.split("\n");
385
+ let currentSelector = ":root";
386
+ let braceDepth = 0;
387
+
388
+ for (let i = 0; i < Math.min(targetLine, lines.length); i++) {
389
+ const line = lines[i];
390
+
391
+ // Check for selector at start of block
392
+ const selectorMatch = line.match(/^\s*([^{]+)\s*\{/);
393
+ if (selectorMatch) {
394
+ const selector = selectorMatch[1].trim();
395
+ // Only update if entering a new block
396
+ if ((line.match(/\{/g) || []).length > (line.match(/\}/g) || []).length) {
397
+ currentSelector = selector;
398
+ }
399
+ }
400
+
401
+ // Track brace depth
402
+ braceDepth += (line.match(/\{/g) || []).length;
403
+ braceDepth -= (line.match(/\}/g) || []).length;
404
+
405
+ // If we close all braces, reset to root
406
+ if (braceDepth === 0) {
407
+ currentSelector = ":root";
408
+ }
409
+ }
410
+
411
+ return currentSelector;
412
+ }
413
+
414
+ /**
415
+ * Extract description from comment preceding the token
416
+ */
417
+ function extractDescription(content: string, line: number): string | undefined {
418
+ const lines = content.split("\n");
419
+
420
+ // Look at the line before
421
+ if (line <= 1) return undefined;
422
+
423
+ const prevLine = lines[line - 2]?.trim();
424
+
425
+ // Check for single-line comment
426
+ const singleLineMatch = prevLine?.match(/\/\/\s*(.+)$/);
427
+ if (singleLineMatch) {
428
+ return singleLineMatch[1].trim();
429
+ }
430
+
431
+ // Check for multi-line comment ending
432
+ const multiLineMatch = prevLine?.match(/\*\s*(.+)\s*\*\//);
433
+ if (multiLineMatch) {
434
+ return multiLineMatch[1].trim();
435
+ }
436
+
437
+ // Check for simple /* comment */
438
+ const inlineMatch = prevLine?.match(/\/\*\s*(.+)\s*\*\//);
439
+ if (inlineMatch) {
440
+ return inlineMatch[1].trim();
441
+ }
442
+
443
+ return undefined;
444
+ }
445
+
446
+ /**
447
+ * Convert a hex color to RGB
448
+ */
449
+ export function hexToRgb(hex: string): { r: number; g: number; b: number } | null {
450
+ const match = hex.match(/^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i);
451
+ if (!match) return null;
452
+
453
+ return {
454
+ r: parseInt(match[1], 16),
455
+ g: parseInt(match[2], 16),
456
+ b: parseInt(match[3], 16),
457
+ };
458
+ }
459
+
460
+ /**
461
+ * Convert RGB to hex
462
+ */
463
+ export function rgbToHex(r: number, g: number, b: number): string {
464
+ return `#${[r, g, b]
465
+ .map((x) => Math.round(x).toString(16).padStart(2, "0"))
466
+ .join("")}`;
467
+ }
468
+
469
+ /**
470
+ * Parse an RGB/RGBA string
471
+ */
472
+ export function parseRgb(
473
+ color: string
474
+ ): { r: number; g: number; b: number; a?: number } | null {
475
+ const match = color.match(
476
+ /rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*(?:,\s*([\d.]+))?\s*\)/
477
+ );
478
+ if (!match) return null;
479
+
480
+ return {
481
+ r: parseInt(match[1], 10),
482
+ g: parseInt(match[2], 10),
483
+ b: parseInt(match[3], 10),
484
+ a: match[4] ? parseFloat(match[4]) : undefined,
485
+ };
486
+ }
487
+
488
+ /**
489
+ * Normalize a color value to lowercase hex
490
+ */
491
+ export function normalizeColor(color: string): string {
492
+ // Already hex
493
+ if (color.startsWith("#")) {
494
+ return color.toLowerCase();
495
+ }
496
+
497
+ // RGB/RGBA to hex
498
+ const rgb = parseRgb(color);
499
+ if (rgb) {
500
+ return rgbToHex(rgb.r, rgb.g, rgb.b);
501
+ }
502
+
503
+ return color.toLowerCase();
504
+ }