@fragments-sdk/core 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,321 @@
1
+ /**
2
+ * Token Parser — extracts CSS custom property declarations from SCSS/CSS files.
3
+ *
4
+ * Parses files for `--prefix-*: value;` declarations and groups them
5
+ * by SCSS comment sections (e.g., `// Typography`, `// Colors`).
6
+ * Falls back to naming-convention-based categorization when comments
7
+ * are absent.
8
+ */
9
+
10
+ export interface ParsedToken {
11
+ /** Full CSS variable name (e.g., "--fui-color-accent") */
12
+ name: string;
13
+ /** Raw value from the declaration (e.g., "#{$fui-space-4}" or "16px") */
14
+ value?: string;
15
+ /** Resolved value after SCSS variable substitution (e.g., "16px") */
16
+ resolvedValue?: string;
17
+ /** Category inferred from SCSS comment or naming convention */
18
+ category: string;
19
+ /** Description from inline comment, if any */
20
+ description?: string;
21
+ }
22
+
23
+ export interface TokenParseOutput {
24
+ /** Detected prefix (e.g., "--fui-") */
25
+ prefix: string;
26
+ /** Tokens grouped by category */
27
+ categories: Record<string, ParsedToken[]>;
28
+ /** Total number of tokens found */
29
+ total: number;
30
+ }
31
+
32
+ /**
33
+ * Category inference from naming conventions.
34
+ * Order matters — first match wins.
35
+ */
36
+ const NAMING_RULES: Array<{ pattern: RegExp; category: string }> = [
37
+ { pattern: /--\w+-font-/, category: 'typography' },
38
+ { pattern: /--\w+-line-height-/, category: 'typography' },
39
+ { pattern: /--\w+-space-/, category: 'spacing' },
40
+ { pattern: /--\w+-padding-/, category: 'spacing' },
41
+ { pattern: /--\w+-radius-/, category: 'radius' },
42
+ { pattern: /--\w+-color-/, category: 'colors' },
43
+ { pattern: /--\w+-bg-/, category: 'surfaces' },
44
+ { pattern: /--\w+-text-/, category: 'text' },
45
+ { pattern: /--\w+-border/, category: 'borders' },
46
+ { pattern: /--\w+-shadow-/, category: 'shadows' },
47
+ { pattern: /--\w+-focus-/, category: 'focus' },
48
+ { pattern: /--\w+-transition-/, category: 'transitions' },
49
+ { pattern: /--\w+-scrollbar-/, category: 'scrollbar' },
50
+ { pattern: /--\w+-z-index/, category: 'z-index' },
51
+ { pattern: /--\w+-(button|input|touch)-/, category: 'component-sizing' },
52
+ { pattern: /--\w+-appshell-/, category: 'layout' },
53
+ { pattern: /--\w+-header-/, category: 'layout' },
54
+ { pattern: /--\w+-code-/, category: 'code' },
55
+ { pattern: /--\w+-tooltip-/, category: 'tooltip' },
56
+ { pattern: /--\w+-hero-/, category: 'marketing' },
57
+ ];
58
+
59
+ /**
60
+ * Infer category from a CSS variable name using naming conventions.
61
+ */
62
+ function inferCategory(name: string): string {
63
+ for (const rule of NAMING_RULES) {
64
+ if (rule.pattern.test(name)) {
65
+ return rule.category;
66
+ }
67
+ }
68
+ return 'other';
69
+ }
70
+
71
+ /**
72
+ * Detect the most common prefix from a list of CSS variable names.
73
+ * E.g., given ["--fui-color-accent", "--fui-bg-primary"] → "--fui-"
74
+ */
75
+ function detectPrefix(names: string[]): string {
76
+ if (names.length === 0) return '--';
77
+
78
+ // Find common prefix after "--"
79
+ const stripped = names.map((n) => n.slice(2)); // remove "--"
80
+ let prefix = '';
81
+ const first = stripped[0];
82
+
83
+ for (let i = 0; i < first.length; i++) {
84
+ const ch = first[i];
85
+ if (stripped.every((s) => s[i] === ch)) {
86
+ prefix += ch;
87
+ } else {
88
+ break;
89
+ }
90
+ }
91
+
92
+ // Trim to last hyphen to get clean prefix
93
+ const lastHyphen = prefix.lastIndexOf('-');
94
+ if (lastHyphen > 0) {
95
+ prefix = prefix.slice(0, lastHyphen + 1);
96
+ }
97
+
98
+ return `--${prefix}`;
99
+ }
100
+
101
+ /**
102
+ * Normalize a SCSS comment into a category name.
103
+ * "// Typography" → "typography"
104
+ * "// Component heights" → "component-sizing"
105
+ * "// Hero/Marketing gradient" → "marketing"
106
+ */
107
+ function normalizeCategory(comment: string): string {
108
+ const text = comment
109
+ .trim()
110
+ .replace(/^\/\/\s*/, '')
111
+ .replace(/^\/\*+\s*/, '')
112
+ .replace(/\s*\*+\/$/, '')
113
+ .trim()
114
+ .toLowerCase();
115
+
116
+ // Map common comment headings to clean category names
117
+ const mappings: Record<string, string> = {
118
+ 'base configuration': 'base',
119
+ 'typography': 'typography',
120
+ 'spacing (micro)': 'spacing',
121
+ 'spacing': 'spacing',
122
+ 'density padding': 'spacing',
123
+ 'border radius': 'radius',
124
+ 'transitions': 'transitions',
125
+ 'colors': 'colors',
126
+ 'surfaces': 'surfaces',
127
+ 'text': 'text',
128
+ 'borders': 'borders',
129
+ 'shadows': 'shadows',
130
+ 'focus': 'focus',
131
+ 'scrollbar': 'scrollbar',
132
+ 'component heights': 'component-sizing',
133
+ 'appshell layout': 'layout',
134
+ 'codeblock': 'code',
135
+ 'tooltip': 'tooltip',
136
+ 'hero/marketing gradient': 'marketing',
137
+ };
138
+
139
+ return mappings[text] ?? text.replace(/\s+/g, '-');
140
+ }
141
+
142
+ /**
143
+ * Extract SCSS variable declarations ($name: value;) from file content.
144
+ * Returns a map of variable name → value.
145
+ */
146
+ function extractScssVariables(content: string): Map<string, string> {
147
+ const vars = new Map<string, string>();
148
+ // Match: $var-name: value; (handles multi-word values, stops at semicolon)
149
+ const scssVarRegex = /^\s*(\$[\w-]+)\s*:\s*(.+?)\s*(?:!default\s*)?;/gm;
150
+
151
+ let match: RegExpExecArray | null;
152
+ while ((match = scssVarRegex.exec(content)) !== null) {
153
+ const name = match[1];
154
+ const value = match[2].replace(/\s*\/\/.*$/, '').trim();
155
+ // Only store the first occurrence (canonical definition)
156
+ if (!vars.has(name)) {
157
+ vars.set(name, value);
158
+ }
159
+ }
160
+
161
+ return vars;
162
+ }
163
+
164
+ /**
165
+ * Resolve SCSS interpolations and variable references in a token value.
166
+ *
167
+ * Handles:
168
+ * - `#{$var}` → looks up $var in scssVars map
169
+ * - `$var` standalone → looks up in scssVars map
170
+ * - `var(--other-token, fallback)` → returns fallback if provided
171
+ * - Recursive resolution up to 5 levels deep
172
+ */
173
+ function resolveTokenValue(
174
+ rawValue: string,
175
+ scssVars: Map<string, string>,
176
+ cssVarValues: Map<string, string>,
177
+ depth = 0
178
+ ): string {
179
+ if (depth > 5) return rawValue; // Prevent infinite recursion
180
+
181
+ let resolved = rawValue;
182
+
183
+ // Resolve #{$var} interpolations
184
+ resolved = resolved.replace(/#\{(\$[\w-]+)\}/g, (_, varName) => {
185
+ const val = scssVars.get(varName);
186
+ return val !== undefined
187
+ ? resolveTokenValue(val, scssVars, cssVarValues, depth + 1)
188
+ : `#{${varName}}`;
189
+ });
190
+
191
+ // Resolve standalone $var references (not inside #{})
192
+ resolved = resolved.replace(/(?<![#\{])(\$[\w-]+)/g, (_, varName) => {
193
+ const val = scssVars.get(varName);
194
+ return val !== undefined
195
+ ? resolveTokenValue(val, scssVars, cssVarValues, depth + 1)
196
+ : varName;
197
+ });
198
+
199
+ // Resolve var(--token, fallback) — use the referenced token value or fallback
200
+ resolved = resolved.replace(
201
+ /var\((--[\w-]+)(?:\s*,\s*(.+?))?\)/g,
202
+ (original, tokenName, fallback) => {
203
+ const tokenVal = cssVarValues.get(tokenName);
204
+ if (tokenVal !== undefined) {
205
+ return resolveTokenValue(tokenVal, scssVars, cssVarValues, depth + 1);
206
+ }
207
+ if (fallback) {
208
+ return resolveTokenValue(fallback.trim(), scssVars, cssVarValues, depth + 1);
209
+ }
210
+ return original;
211
+ }
212
+ );
213
+
214
+ return resolved;
215
+ }
216
+
217
+ /**
218
+ * Parse a SCSS or CSS file and extract CSS custom property declarations.
219
+ *
220
+ * Handles two grouping strategies:
221
+ * 1. Comment-based: Uses `// Category` comments above groups of declarations
222
+ * 2. Naming-based: Falls back to inferring category from variable name patterns
223
+ *
224
+ * Also resolves SCSS variable interpolations (e.g., `#{$fui-space-4}` → `16px`)
225
+ * when the SCSS variable definitions are found in the same file content.
226
+ */
227
+ export function parseTokenFile(content: string, filePath: string): TokenParseOutput {
228
+ const lines = content.split('\n');
229
+ const tokens: ParsedToken[] = [];
230
+ const seenNames = new Set<string>();
231
+ let currentCategory = 'other';
232
+ let hasCommentCategories = false;
233
+
234
+ // First pass: extract SCSS variable declarations for resolution
235
+ const scssVars = extractScssVariables(content);
236
+
237
+ // Regex for CSS custom property declarations
238
+ // Matches: --name: value; (with optional SCSS interpolation)
239
+ // Captures both the variable name and its value
240
+ const varDeclRegex = /^\s*(--[\w-]+)\s*:\s*(.+?)\s*;/;
241
+ // Regex for section comments (// Category or /* Category */)
242
+ // Allow any characters after uppercase start (including / for "Hero/Marketing")
243
+ const sectionCommentRegex = /^\s*\/\/\s+([A-Z].+)$/;
244
+
245
+ for (const line of lines) {
246
+ // Check for section comment
247
+ const commentMatch = line.match(sectionCommentRegex);
248
+ if (commentMatch) {
249
+ const normalized = normalizeCategory(commentMatch[0]);
250
+ if (normalized) {
251
+ currentCategory = normalized;
252
+ hasCommentCategories = true;
253
+ }
254
+ continue;
255
+ }
256
+
257
+ // Check for CSS variable declaration
258
+ const varMatch = line.match(varDeclRegex);
259
+ if (varMatch) {
260
+ const name = varMatch[1];
261
+ const rawValue = varMatch[2];
262
+
263
+ // Deduplicate: keep only the first occurrence of each variable.
264
+ // Dark mode and high contrast blocks redefine the same variables
265
+ // with different values — we only want the canonical list.
266
+ if (seenNames.has(name)) continue;
267
+ seenNames.add(name);
268
+
269
+ // Extract inline comment if present
270
+ const inlineComment = line.match(/\/\/\s*(.+)$/);
271
+ const description = inlineComment ? inlineComment[1].trim() : undefined;
272
+
273
+ // Clean the value: strip trailing inline comments
274
+ const cleanValue = rawValue.replace(/\s*\/\/.*$/, '').trim();
275
+
276
+ tokens.push({
277
+ name,
278
+ value: cleanValue || undefined,
279
+ category: hasCommentCategories ? currentCategory : inferCategory(name),
280
+ description,
281
+ });
282
+ }
283
+ }
284
+
285
+ // Second pass: build a CSS custom property → raw value map for cross-references
286
+ const cssVarValues = new Map<string, string>();
287
+ for (const token of tokens) {
288
+ if (token.value) {
289
+ cssVarValues.set(token.name, token.value);
290
+ }
291
+ }
292
+
293
+ // Third pass: resolve SCSS interpolations and var() references
294
+ for (const token of tokens) {
295
+ if (token.value) {
296
+ const resolved = resolveTokenValue(token.value, scssVars, cssVarValues);
297
+ // Only set resolvedValue if it's different from raw and doesn't still contain unresolved refs
298
+ if (resolved !== token.value && !resolved.includes('#{') && !resolved.includes('$')) {
299
+ token.resolvedValue = resolved;
300
+ }
301
+ }
302
+ }
303
+
304
+ // Group by category
305
+ const categories: Record<string, ParsedToken[]> = {};
306
+ for (const token of tokens) {
307
+ if (!categories[token.category]) {
308
+ categories[token.category] = [];
309
+ }
310
+ categories[token.category].push(token);
311
+ }
312
+
313
+ // Detect prefix
314
+ const prefix = detectPrefix(tokens.map((t) => t.name));
315
+
316
+ return {
317
+ prefix,
318
+ categories,
319
+ total: tokens.length,
320
+ };
321
+ }
@@ -0,0 +1,287 @@
1
+ /**
2
+ * Design Token Types for Fragments
3
+ *
4
+ * These types define the structure for CSS custom property (CSS variable) discovery,
5
+ * parsing, and reverse lookup capabilities. The token system enables:
6
+ *
7
+ * 1. Automatic discovery of design tokens from CSS/SCSS files
8
+ * 2. Reverse lookup: given a computed value, find which token(s) produce it
9
+ * 3. Detection of hardcoded values vs token usage
10
+ * 4. AI-friendly fix suggestions
11
+ */
12
+
13
+ /**
14
+ * Token categories for grouping and filtering
15
+ */
16
+ export type TokenCategory =
17
+ | "color"
18
+ | "spacing"
19
+ | "typography"
20
+ | "radius"
21
+ | "shadow"
22
+ | "sizing"
23
+ | "border"
24
+ | "animation"
25
+ | "z-index"
26
+ | "other";
27
+
28
+ /**
29
+ * A single design token (CSS custom property)
30
+ */
31
+ export interface DesignToken {
32
+ /** Token name with leading dashes (e.g., "--color-primary") */
33
+ name: string;
34
+
35
+ /** Raw value as written in CSS (e.g., "var(--color-cobalt-50)") */
36
+ rawValue: string;
37
+
38
+ /** Fully resolved value (e.g., "#0051c2") */
39
+ resolvedValue: string;
40
+
41
+ /** Inferred category based on naming convention */
42
+ category: TokenCategory;
43
+
44
+ /**
45
+ * Token level in the design system hierarchy:
46
+ * - 1 = Base/primitive tokens (raw values like colors, sizes)
47
+ * - 2 = Semantic tokens (references to base tokens with meaning)
48
+ * - 3 = Component tokens (component-specific tokens)
49
+ */
50
+ level: 1 | 2 | 3;
51
+
52
+ /**
53
+ * Reference chain showing how the value was resolved
54
+ * e.g., ["--color-primary", "--color-cobalt-50"] means
55
+ * --color-primary references --color-cobalt-50
56
+ */
57
+ referenceChain: string[];
58
+
59
+ /** Source file where this token was defined */
60
+ sourceFile: string;
61
+
62
+ /** Line number in source file */
63
+ lineNumber?: number;
64
+
65
+ /** Theme this token belongs to (e.g., "default", "dark", "light") */
66
+ theme: string;
67
+
68
+ /** CSS selector where this token is defined (e.g., ":root", "[data-theme='dark']") */
69
+ selector: string;
70
+
71
+ /** Optional description from comments */
72
+ description?: string;
73
+ }
74
+
75
+ /**
76
+ * Token registry for fast lookups
77
+ */
78
+ export interface TokenRegistry {
79
+ /** Lookup by token name (e.g., "--color-primary") */
80
+ byName: Map<string, DesignToken>;
81
+
82
+ /**
83
+ * REVERSE lookup: resolved value -> token names
84
+ * Key is normalized value (e.g., "#0051c2" lowercase)
85
+ * Value is array of token names that resolve to this value
86
+ */
87
+ byValue: Map<string, string[]>;
88
+
89
+ /** Tokens grouped by theme */
90
+ byTheme: Map<string, DesignToken[]>;
91
+
92
+ /** Tokens grouped by category */
93
+ byCategory: Map<TokenCategory, DesignToken[]>;
94
+
95
+ /** Registry metadata */
96
+ meta: TokenRegistryMeta;
97
+ }
98
+
99
+ /**
100
+ * Token registry metadata
101
+ */
102
+ export interface TokenRegistryMeta {
103
+ /** When tokens were discovered */
104
+ discoveredAt: Date;
105
+
106
+ /** Source files that were parsed */
107
+ sourceFiles: string[];
108
+
109
+ /** Total number of tokens discovered */
110
+ totalTokens: number;
111
+
112
+ /** Time taken to parse (ms) */
113
+ parseTimeMs: number;
114
+
115
+ /** Number of circular references detected */
116
+ circularRefs: number;
117
+
118
+ /** Number of unresolved references */
119
+ unresolvedRefs: number;
120
+ }
121
+
122
+ /**
123
+ * Enhanced style diff item with token information
124
+ */
125
+ export interface EnhancedStyleDiffItem {
126
+ /** CSS property name (e.g., "backgroundColor") */
127
+ property: string;
128
+
129
+ /** Value from Figma design */
130
+ figma: string;
131
+
132
+ /** Value from rendered component */
133
+ rendered: string;
134
+
135
+ /** Whether values match (within tolerance) */
136
+ match: boolean;
137
+
138
+ /** Token name if Figma value matches a known token */
139
+ figmaToken?: string;
140
+
141
+ /** Token name if rendered value uses a token */
142
+ renderedToken?: string;
143
+
144
+ /**
145
+ * True if rendered value doesn't use a token but should
146
+ * (i.e., Figma uses a token but code uses hardcoded value)
147
+ */
148
+ isHardcoded: boolean;
149
+
150
+ /** Suggested fix if hardcoded */
151
+ suggestedFix?: TokenFix;
152
+ }
153
+
154
+ /**
155
+ * Token-based fix suggestion
156
+ */
157
+ export interface TokenFix {
158
+ /** Token name to use (e.g., "--color-primary") */
159
+ tokenName: string;
160
+
161
+ /** Token's resolved value */
162
+ tokenValue: string;
163
+
164
+ /** Code snippet to fix the issue */
165
+ codeFix: string;
166
+
167
+ /** Confidence score 0-1 */
168
+ confidence: number;
169
+
170
+ /** Human-readable explanation */
171
+ reason: string;
172
+ }
173
+
174
+ /**
175
+ * Configuration for token discovery
176
+ */
177
+ export interface TokenConfig {
178
+ /**
179
+ * Glob patterns for files to scan for tokens
180
+ * e.g., ["src/styles/theme.scss", "src/styles/variables.css"]
181
+ */
182
+ include: string[];
183
+
184
+ /**
185
+ * Glob patterns to exclude
186
+ * @example ["node_modules"]
187
+ */
188
+ exclude?: string[];
189
+
190
+ /**
191
+ * Map CSS selectors to theme names
192
+ * @example { ":root": "default", "[data-theme='dark']": "dark" }
193
+ */
194
+ themeSelectors?: Record<string, string>;
195
+
196
+ /** Enable token comparison in style diffs (default: true) */
197
+ enabled?: boolean;
198
+ }
199
+
200
+ /**
201
+ * Result of parsing a CSS/SCSS file for tokens
202
+ */
203
+ export interface TokenParseResult {
204
+ /** Tokens discovered in the file */
205
+ tokens: DesignToken[];
206
+
207
+ /** Errors encountered during parsing */
208
+ errors: TokenParseError[];
209
+
210
+ /** Warnings (non-fatal issues) */
211
+ warnings: string[];
212
+
213
+ /** Parse time in ms */
214
+ parseTimeMs: number;
215
+ }
216
+
217
+ /**
218
+ * Error during token parsing
219
+ */
220
+ export interface TokenParseError {
221
+ /** Error message */
222
+ message: string;
223
+
224
+ /** File where error occurred */
225
+ file: string;
226
+
227
+ /** Line number if known */
228
+ line?: number;
229
+
230
+ /** The problematic content if available */
231
+ content?: string;
232
+ }
233
+
234
+ /**
235
+ * Request to match a value to tokens
236
+ */
237
+ export interface TokenMatchRequest {
238
+ /** The value to find tokens for (e.g., "#0051c2") */
239
+ value: string;
240
+
241
+ /** Property type hint for better matching (e.g., "color") */
242
+ propertyType?: "color" | "spacing" | "typography" | "other";
243
+
244
+ /** Specific theme to search in */
245
+ theme?: string;
246
+ }
247
+
248
+ /**
249
+ * Result of token matching
250
+ */
251
+ export interface TokenMatchResult {
252
+ /** Exact matches (same resolved value) */
253
+ exactMatches: DesignToken[];
254
+
255
+ /** Close matches (similar value, useful for colors) */
256
+ closeMatches: Array<{
257
+ token: DesignToken;
258
+ /** How close the match is (0-1, 1 = exact) */
259
+ similarity: number;
260
+ }>;
261
+
262
+ /** Whether any match was found */
263
+ found: boolean;
264
+ }
265
+
266
+ /**
267
+ * Summary of token usage in a component
268
+ */
269
+ export interface TokenUsageSummary {
270
+ /** Total CSS properties checked */
271
+ totalProperties: number;
272
+
273
+ /** Properties using design tokens */
274
+ usingTokens: number;
275
+
276
+ /** Properties with hardcoded values */
277
+ hardcoded: number;
278
+
279
+ /** Properties matching but not using tokens explicitly */
280
+ implicitMatches: number;
281
+
282
+ /** Compliance percentage (usingTokens / totalProperties * 100) */
283
+ compliancePercent: number;
284
+
285
+ /** List of hardcoded properties with fix suggestions */
286
+ hardcodedProperties: EnhancedStyleDiffItem[];
287
+ }