sonance-brand-mcp 1.3.110 → 1.3.112

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 (84) hide show
  1. package/dist/assets/api/sonance-ai-edit/route.ts +30 -7
  2. package/dist/assets/api/sonance-save-image/route.ts +625 -0
  3. package/dist/assets/api/sonance-vision-apply/image-styling-detection.ts +1360 -0
  4. package/dist/assets/api/sonance-vision-apply/route.ts +1020 -64
  5. package/dist/assets/api/sonance-vision-apply/styling-detection.ts +730 -0
  6. package/dist/assets/api/sonance-vision-apply/theme-discovery.ts +1 -1
  7. package/dist/assets/api/sonance-vision-edit/route.ts +33 -8
  8. package/dist/assets/brand-system.ts +13 -12
  9. package/dist/assets/components/accordion.tsx +15 -7
  10. package/dist/assets/components/alert-dialog.tsx +35 -10
  11. package/dist/assets/components/alert.tsx +11 -10
  12. package/dist/assets/components/avatar.tsx +4 -4
  13. package/dist/assets/components/badge.tsx +16 -12
  14. package/dist/assets/components/button.stories.tsx +3 -3
  15. package/dist/assets/components/button.tsx +50 -31
  16. package/dist/assets/components/calendar.tsx +12 -8
  17. package/dist/assets/components/card.tsx +35 -29
  18. package/dist/assets/components/checkbox.tsx +9 -8
  19. package/dist/assets/components/code.tsx +19 -11
  20. package/dist/assets/components/command.tsx +32 -13
  21. package/dist/assets/components/context-menu.tsx +37 -16
  22. package/dist/assets/components/dialog.tsx +8 -5
  23. package/dist/assets/components/divider.tsx +15 -5
  24. package/dist/assets/components/drawer.tsx +4 -3
  25. package/dist/assets/components/dropdown-menu.tsx +15 -13
  26. package/dist/assets/components/hover-card.tsx +4 -1
  27. package/dist/assets/components/image.tsx +1 -1
  28. package/dist/assets/components/input.tsx +29 -14
  29. package/dist/assets/components/kbd.stories.tsx +3 -3
  30. package/dist/assets/components/kbd.tsx +29 -13
  31. package/dist/assets/components/listbox.tsx +8 -8
  32. package/dist/assets/components/menubar.tsx +50 -23
  33. package/dist/assets/components/navbar.stories.tsx +140 -13
  34. package/dist/assets/components/navbar.tsx +22 -5
  35. package/dist/assets/components/navigation-menu.tsx +28 -6
  36. package/dist/assets/components/pagination.tsx +10 -10
  37. package/dist/assets/components/popover.tsx +10 -8
  38. package/dist/assets/components/progress.tsx +6 -4
  39. package/dist/assets/components/radio-group.tsx +5 -5
  40. package/dist/assets/components/select.tsx +49 -29
  41. package/dist/assets/components/separator.tsx +3 -3
  42. package/dist/assets/components/sheet.tsx +4 -4
  43. package/dist/assets/components/sidebar.tsx +10 -10
  44. package/dist/assets/components/skeleton.tsx +13 -5
  45. package/dist/assets/components/slider.tsx +12 -10
  46. package/dist/assets/components/switch.tsx +4 -4
  47. package/dist/assets/components/table.tsx +5 -5
  48. package/dist/assets/components/tabs.tsx +8 -8
  49. package/dist/assets/components/textarea.tsx +11 -9
  50. package/dist/assets/components/toast.tsx +7 -7
  51. package/dist/assets/components/toggle.tsx +27 -7
  52. package/dist/assets/components/tooltip.tsx +10 -8
  53. package/dist/assets/components/user.tsx +8 -6
  54. package/dist/assets/dev-tools/SonanceDevTools.tsx +851 -708
  55. package/dist/assets/dev-tools/components/ApplyFirstPreview.tsx +10 -10
  56. package/dist/assets/dev-tools/components/ChatHistory.tsx +145 -0
  57. package/dist/assets/dev-tools/components/ChatInterface.tsx +444 -295
  58. package/dist/assets/dev-tools/components/ChatTabBar.tsx +82 -0
  59. package/dist/assets/dev-tools/components/DiffPreview.tsx +1 -1
  60. package/dist/assets/dev-tools/components/InlineDiffPreview.tsx +528 -0
  61. package/dist/assets/dev-tools/components/InspectorOverlay.tsx +21 -18
  62. package/dist/assets/dev-tools/components/PropertiesPanel.tsx +1345 -0
  63. package/dist/assets/dev-tools/components/ScreenshotAnnotator.tsx +1 -1
  64. package/dist/assets/dev-tools/components/SectionHighlight.tsx +1 -1
  65. package/dist/assets/dev-tools/components/VisionDiffPreview.tsx +7 -7
  66. package/dist/assets/dev-tools/components/VisionModeBorder.tsx +12 -63
  67. package/dist/assets/dev-tools/constants.ts +38 -6
  68. package/dist/assets/dev-tools/hooks/index.ts +69 -0
  69. package/dist/assets/dev-tools/hooks/useComponentDetection.ts +132 -0
  70. package/dist/assets/dev-tools/hooks/useComputedStyles.ts +471 -0
  71. package/dist/assets/dev-tools/hooks/useContentHash.ts +212 -0
  72. package/dist/assets/dev-tools/hooks/useElementScanner.ts +398 -0
  73. package/dist/assets/dev-tools/hooks/useImageDetection.ts +162 -0
  74. package/dist/assets/dev-tools/hooks/useTextDetection.ts +217 -0
  75. package/dist/assets/dev-tools/index.ts +3 -0
  76. package/dist/assets/dev-tools/panels/AnalysisPanel.tsx +32 -32
  77. package/dist/assets/dev-tools/panels/ComponentsPanel.tsx +384 -131
  78. package/dist/assets/dev-tools/panels/TextPanel.tsx +10 -10
  79. package/dist/assets/dev-tools/types.ts +93 -2
  80. package/dist/assets/globals.css +225 -9
  81. package/dist/assets/styles/brand-overrides.css +3 -2
  82. package/dist/assets/utils.ts +2 -1
  83. package/dist/index.js +22 -3
  84. package/package.json +2 -1
@@ -0,0 +1,1360 @@
1
+ /**
2
+ * Image Styling Detection System
3
+ *
4
+ * Intelligently detects how images are styled in a codebase and determines
5
+ * the appropriate strategy for applying changes.
6
+ *
7
+ * Supports:
8
+ * - CSS Variables (var(--logo-scale))
9
+ * - Inline styles (style={{ transform: scale(...) }})
10
+ * - Tailwind utilities (scale-90, w-24, h-auto)
11
+ * - CSS classes with static values
12
+ * - Config files (brand-system.ts, theme.ts)
13
+ * - Styled-components/Emotion
14
+ */
15
+
16
+ import * as fs from 'fs';
17
+ import * as path from 'path';
18
+
19
+ export type ImageStylingType =
20
+ | 'css-variable'
21
+ | 'inline-style'
22
+ | 'tailwind'
23
+ | 'css-class'
24
+ | 'styled-component'
25
+ | 'config-consumed'
26
+ | 'config-unused'
27
+ | 'next-image'
28
+ | 'unknown';
29
+
30
+ export type SaveStrategy =
31
+ | 'css-file'
32
+ | 'component-inline'
33
+ | 'tailwind-class'
34
+ | 'config-file'
35
+ | 'ai-assisted';
36
+
37
+ export interface ImageStylingPattern {
38
+ type: ImageStylingType;
39
+ strategy: SaveStrategy;
40
+ confidence: 'high' | 'medium' | 'low';
41
+
42
+ // Where the style is defined
43
+ sourceFile?: string;
44
+ lineNumber?: number;
45
+
46
+ // The specific property/variable name
47
+ cssVariable?: string;
48
+ configKey?: string;
49
+
50
+ // For Tailwind detection
51
+ existingClasses?: string[];
52
+
53
+ // Details about what was detected
54
+ details: string;
55
+ }
56
+
57
+ export interface ImageContext {
58
+ imageSrc: string;
59
+ elementId?: string;
60
+ className?: string;
61
+ inlineStyle?: string;
62
+ parentComponent?: string;
63
+ altText?: string;
64
+ /** Data attributes from the image element */
65
+ dataAttributes?: Record<string, string>;
66
+ /** The page route where the image was clicked */
67
+ pageRoute?: string;
68
+ /** The current brand context from useBrand() - preferred over image path detection */
69
+ currentBrand?: string;
70
+ }
71
+
72
+ interface DetectionResult {
73
+ pattern: ImageStylingPattern;
74
+ relatedFiles: string[];
75
+ }
76
+
77
+ interface PreciseLocation {
78
+ filePath: string;
79
+ lineNumber: number;
80
+ elementCode: string;
81
+ matchedBy: 'id' | 'data-attribute' | 'alt-text' | 'src-path';
82
+ confidence: 'high' | 'medium';
83
+ }
84
+
85
+ interface DiscoveredCSSVariable {
86
+ variableName: string;
87
+ file: string;
88
+ lineNumber: number;
89
+ prefix: string; // e.g., "sonance", "theme", "app"
90
+ suffix: string; // e.g., "logo-scale", "scale", "size"
91
+ }
92
+
93
+ /**
94
+ * Dynamically extract the CSS variable pattern from element code
95
+ * Returns { prefix, suffix, fullVariable } without hardcoding any specific names
96
+ */
97
+ function extractCSSVariablePattern(elementCode: string): {
98
+ prefix: string | null;
99
+ suffix: string;
100
+ fullVariable: string | null;
101
+ isDynamic: boolean;
102
+ } {
103
+ // Check if it's a dynamic template literal: var(--${something}-suffix)
104
+ const isDynamic = elementCode.includes('${') && elementCode.includes('var(--');
105
+
106
+ if (isDynamic) {
107
+ // Extract suffix from dynamic pattern: var(--${brand}-logo-scale, 1)
108
+ // Match everything after the closing } and before , or )
109
+ const dynamicSuffixMatch = elementCode.match(/var\(--\$\{[^}]+\}-([a-zA-Z0-9-]+)/);
110
+ const suffix = dynamicSuffixMatch?.[1] || 'scale';
111
+
112
+ return {
113
+ prefix: null, // Will be determined from context
114
+ suffix,
115
+ fullVariable: null, // Will be constructed with brand
116
+ isDynamic: true
117
+ };
118
+ }
119
+
120
+ // Static pattern: var(--prefix-suffix) or var(--single-name)
121
+ const staticMatch = elementCode.match(/var\(--([a-zA-Z0-9]+(?:-[a-zA-Z0-9]+)*)\s*[,)]/);
122
+
123
+ if (staticMatch) {
124
+ const fullVariable = `--${staticMatch[1]}`;
125
+ const parts = staticMatch[1].split('-');
126
+
127
+ if (parts.length >= 2) {
128
+ // Has prefix-suffix structure
129
+ const prefix = parts[0];
130
+ const suffix = parts.slice(1).join('-');
131
+ return { prefix, suffix, fullVariable, isDynamic: false };
132
+ } else {
133
+ // Single word variable
134
+ return { prefix: null, suffix: parts[0], fullVariable, isDynamic: false };
135
+ }
136
+ }
137
+
138
+ return { prefix: null, suffix: 'scale', fullVariable: null, isDynamic: false };
139
+ }
140
+
141
+ /**
142
+ * Discover CSS files that define variables matching a pattern
143
+ * Searches common CSS locations without hardcoding specific file paths
144
+ */
145
+ async function discoverCSSFileForVariable(
146
+ projectRoot: string,
147
+ variableName: string
148
+ ): Promise<{ file: string; lineNumber: number } | null> {
149
+ // Search in common CSS locations (no hardcoded specific files)
150
+ const searchDirs = ['src/styles', 'src/app', 'styles', 'app', 'src', 'public'];
151
+
152
+ for (const dir of searchDirs) {
153
+ const dirPath = path.join(projectRoot, dir);
154
+ if (!fs.existsSync(dirPath)) continue;
155
+
156
+ // Recursively search for CSS files
157
+ const cssFiles = findCSSFilesRecursive(dirPath);
158
+
159
+ for (const cssFile of cssFiles) {
160
+ try {
161
+ const content = fs.readFileSync(cssFile, 'utf-8');
162
+ const lines = content.split('\n');
163
+
164
+ // Look for the variable definition (with : after the name)
165
+ for (let i = 0; i < lines.length; i++) {
166
+ if (lines[i].includes(`${variableName}:`) || lines[i].includes(`${variableName} :`)) {
167
+ const relativePath = path.relative(projectRoot, cssFile);
168
+ console.log('[CSS Discovery] Found variable definition:', {
169
+ variable: variableName,
170
+ file: relativePath,
171
+ line: i + 1
172
+ });
173
+ return { file: relativePath, lineNumber: i + 1 };
174
+ }
175
+ }
176
+ } catch {
177
+ // Skip files that can't be read
178
+ }
179
+ }
180
+ }
181
+
182
+ // If not found, try to find any file with similar variable patterns
183
+ // This helps discover where CSS variables are typically defined in this project
184
+ for (const dir of searchDirs) {
185
+ const dirPath = path.join(projectRoot, dir);
186
+ if (!fs.existsSync(dirPath)) continue;
187
+
188
+ const cssFiles = findCSSFilesRecursive(dirPath);
189
+
190
+ for (const cssFile of cssFiles) {
191
+ try {
192
+ const content = fs.readFileSync(cssFile, 'utf-8');
193
+ // Look for :root block with CSS variables
194
+ if (content.includes(':root') && content.includes('--') && content.includes('scale')) {
195
+ const relativePath = path.relative(projectRoot, cssFile);
196
+ console.log('[CSS Discovery] Found likely CSS variables file:', relativePath);
197
+ return { file: relativePath, lineNumber: 1 };
198
+ }
199
+ } catch {
200
+ // Skip files that can't be read
201
+ }
202
+ }
203
+ }
204
+
205
+ return null;
206
+ }
207
+
208
+ /**
209
+ * Recursively find all CSS files in a directory
210
+ */
211
+ function findCSSFilesRecursive(dir: string, maxDepth = 4, currentDepth = 0): string[] {
212
+ if (currentDepth > maxDepth) return [];
213
+
214
+ const cssFiles: string[] = [];
215
+
216
+ try {
217
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
218
+
219
+ for (const entry of entries) {
220
+ const fullPath = path.join(dir, entry.name);
221
+
222
+ if (entry.isDirectory() && !entry.name.startsWith('.') && entry.name !== 'node_modules') {
223
+ cssFiles.push(...findCSSFilesRecursive(fullPath, maxDepth, currentDepth + 1));
224
+ } else if (entry.isFile() && entry.name.endsWith('.css')) {
225
+ cssFiles.push(fullPath);
226
+ }
227
+ }
228
+ } catch {
229
+ // Directory not readable
230
+ }
231
+
232
+ return cssFiles;
233
+ }
234
+
235
+ /**
236
+ * Extract brand/prefix from context or by analyzing the element code
237
+ * No hardcoded brand names - discovers dynamically
238
+ */
239
+ function determineBrandPrefix(
240
+ imageContext: ImageContext,
241
+ elementCode: string,
242
+ projectRoot: string
243
+ ): string {
244
+ // Priority 1: Use currentBrand from React context (most reliable)
245
+ if (imageContext.currentBrand) {
246
+ console.log('[Brand Detection] Using context brand:', imageContext.currentBrand);
247
+ return imageContext.currentBrand;
248
+ }
249
+
250
+ // Priority 2: Extract from static CSS variable in element code
251
+ const staticVarMatch = elementCode.match(/var\(--([a-zA-Z0-9]+)-/);
252
+ if (staticVarMatch && !staticVarMatch[0].includes('${')) {
253
+ console.log('[Brand Detection] Extracted from static variable:', staticVarMatch[1]);
254
+ return staticVarMatch[1];
255
+ }
256
+
257
+ // Priority 3: Try to find the first defined brand variable in CSS files
258
+ const discoveredBrand = discoverFirstBrandFromCSS(projectRoot);
259
+ if (discoveredBrand) {
260
+ console.log('[Brand Detection] Discovered from CSS:', discoveredBrand);
261
+ return discoveredBrand;
262
+ }
263
+
264
+ // Fallback: Use a generic default
265
+ console.log('[Brand Detection] Using default: "app"');
266
+ return 'app';
267
+ }
268
+
269
+ /**
270
+ * Discover the first brand prefix from CSS variable definitions
271
+ */
272
+ function discoverFirstBrandFromCSS(projectRoot: string): string | null {
273
+ const searchDirs = ['src/styles', 'src/app', 'styles'];
274
+
275
+ for (const dir of searchDirs) {
276
+ const dirPath = path.join(projectRoot, dir);
277
+ if (!fs.existsSync(dirPath)) continue;
278
+
279
+ const cssFiles = findCSSFilesRecursive(dirPath, 2);
280
+
281
+ for (const cssFile of cssFiles) {
282
+ try {
283
+ const content = fs.readFileSync(cssFile, 'utf-8');
284
+ // Look for patterns like --something-logo-scale: or --something-scale:
285
+ const brandMatch = content.match(/--([a-zA-Z0-9]+)-(logo-)?scale\s*:/);
286
+ if (brandMatch) {
287
+ return brandMatch[1];
288
+ }
289
+ } catch {
290
+ // Skip
291
+ }
292
+ }
293
+ }
294
+
295
+ return null;
296
+ }
297
+
298
+ /**
299
+ * Find the first CSS file that contains CSS variable definitions
300
+ * Used as a fallback when specific variable file not found
301
+ */
302
+ async function findFirstCSSVariablesFile(projectRoot: string): Promise<string> {
303
+ const searchDirs = ['src/styles', 'src/app', 'styles', 'app'];
304
+
305
+ for (const dir of searchDirs) {
306
+ const dirPath = path.join(projectRoot, dir);
307
+ if (!fs.existsSync(dirPath)) continue;
308
+
309
+ const cssFiles = findCSSFilesRecursive(dirPath, 2);
310
+
311
+ for (const cssFile of cssFiles) {
312
+ try {
313
+ const content = fs.readFileSync(cssFile, 'utf-8');
314
+ // Look for :root with CSS variables
315
+ if (content.includes(':root') && content.includes('--')) {
316
+ return path.relative(projectRoot, cssFile);
317
+ }
318
+ } catch {
319
+ // Skip
320
+ }
321
+ }
322
+ }
323
+
324
+ // Ultimate fallback - return a sensible default path
325
+ // but don't hardcode specific brand files
326
+ if (fs.existsSync(path.join(projectRoot, 'src/styles'))) {
327
+ return 'src/styles/variables.css';
328
+ }
329
+ if (fs.existsSync(path.join(projectRoot, 'src/app'))) {
330
+ return 'src/app/globals.css';
331
+ }
332
+ return 'styles/globals.css';
333
+ }
334
+
335
+ interface JSXElementBounds {
336
+ start: number;
337
+ end: number;
338
+ tagName: string;
339
+ }
340
+
341
+ /**
342
+ * Find the complete bounds of a JSX element containing a specific line
343
+ * Scans backward to find opening tag and forward to find closing tag
344
+ */
345
+ function findJSXElementBounds(lines: string[], matchLineIndex: number): JSXElementBounds {
346
+ let startLine = matchLineIndex;
347
+ let endLine = matchLineIndex;
348
+ let tagName = 'Image'; // Default
349
+
350
+ // Scan backward to find the opening tag (<Image or <img)
351
+ for (let i = matchLineIndex; i >= 0; i--) {
352
+ const line = lines[i];
353
+ const imageMatch = line.match(/<(Image|img)\b/i);
354
+ if (imageMatch) {
355
+ startLine = i;
356
+ tagName = imageMatch[1];
357
+ break;
358
+ }
359
+ // Don't go past a closing tag of a different element
360
+ if (line.includes('/>') || line.match(/<\/\w+>/)) {
361
+ // Check if this is the same line as our start
362
+ if (i < matchLineIndex) break;
363
+ }
364
+ }
365
+
366
+ // Scan forward to find the closing /> or </Image> or </img>
367
+ let bracketDepth = 0;
368
+ let foundOpeningBracket = false;
369
+
370
+ for (let i = startLine; i < lines.length; i++) {
371
+ const line = lines[i];
372
+
373
+ // Count opening braces/brackets for JSX expressions
374
+ for (const char of line) {
375
+ if (char === '{') bracketDepth++;
376
+ if (char === '}') bracketDepth--;
377
+ }
378
+
379
+ // Check for self-closing tag: />
380
+ if (line.includes('/>') && bracketDepth === 0) {
381
+ endLine = i;
382
+ break;
383
+ }
384
+
385
+ // Check for closing tag: </Image> or </img>
386
+ const closingMatch = line.match(new RegExp(`</${tagName}>`, 'i'));
387
+ if (closingMatch && bracketDepth === 0) {
388
+ endLine = i;
389
+ break;
390
+ }
391
+
392
+ // Track if we've seen the opening angle bracket
393
+ if (line.includes(`<${tagName}`)) {
394
+ foundOpeningBracket = true;
395
+ }
396
+
397
+ // Safety: don't scan more than 50 lines past the match
398
+ if (i - startLine > 50) {
399
+ endLine = Math.min(startLine + 20, lines.length - 1);
400
+ break;
401
+ }
402
+ }
403
+
404
+ // Ensure we have at least a few lines of context
405
+ if (endLine === startLine) {
406
+ endLine = Math.min(startLine + 10, lines.length - 1);
407
+ }
408
+
409
+ console.log('[JSX Bounds] Found element bounds:', {
410
+ tagName,
411
+ startLine: startLine + 1,
412
+ endLine: endLine + 1,
413
+ totalLines: endLine - startLine + 1
414
+ });
415
+
416
+ return { start: startLine, end: endLine, tagName };
417
+ }
418
+
419
+ /**
420
+ * Find the EXACT location of an image element in source code
421
+ * Uses ID, data attributes, alt text, and src path for precise matching
422
+ */
423
+ function findPreciseImageLocation(
424
+ imageContext: ImageContext,
425
+ componentFiles: { path: string; content: string }[]
426
+ ): PreciseLocation | null {
427
+
428
+ // Priority 1: Match by element ID (most precise)
429
+ if (imageContext.elementId) {
430
+ const idPatterns = [
431
+ new RegExp(`id=["'\`]${escapeRegex(imageContext.elementId)}["'\`]`),
432
+ new RegExp(`id=\\{["'\`]${escapeRegex(imageContext.elementId)}["'\`]\\}`),
433
+ ];
434
+
435
+ for (const file of componentFiles) {
436
+ const lines = file.content.split('\n');
437
+ for (let i = 0; i < lines.length; i++) {
438
+ for (const pattern of idPatterns) {
439
+ if (pattern.test(lines[i])) {
440
+ // Use findJSXElementBounds to get the complete element
441
+ const bounds = findJSXElementBounds(lines, i);
442
+ const elementCode = lines.slice(bounds.start, bounds.end + 1).join('\n');
443
+
444
+ // Verify this is an image element
445
+ if (elementCode.includes('<Image') || elementCode.includes('<img')) {
446
+ console.log('[Precise Location] Found by element ID:', {
447
+ id: imageContext.elementId,
448
+ file: file.path,
449
+ line: bounds.start + 1,
450
+ elementLines: bounds.end - bounds.start + 1
451
+ });
452
+ return {
453
+ filePath: file.path,
454
+ lineNumber: bounds.start + 1,
455
+ elementCode,
456
+ matchedBy: 'id',
457
+ confidence: 'high'
458
+ };
459
+ }
460
+ }
461
+ }
462
+ }
463
+ }
464
+ }
465
+
466
+ // Priority 2: Match by data attributes (e.g., data-sonance-logo-id)
467
+ if (imageContext.dataAttributes) {
468
+ for (const [key, value] of Object.entries(imageContext.dataAttributes)) {
469
+ if (!value) continue;
470
+
471
+ const dataPattern = new RegExp(`data-${escapeRegex(key)}=["'\`]${escapeRegex(value)}["'\`]`);
472
+
473
+ for (const file of componentFiles) {
474
+ const lines = file.content.split('\n');
475
+ for (let i = 0; i < lines.length; i++) {
476
+ if (dataPattern.test(lines[i])) {
477
+ // Use findJSXElementBounds to get the complete element
478
+ const bounds = findJSXElementBounds(lines, i);
479
+ const elementCode = lines.slice(bounds.start, bounds.end + 1).join('\n');
480
+
481
+ if (elementCode.includes('<Image') || elementCode.includes('<img')) {
482
+ console.log('[Precise Location] Found by data attribute:', {
483
+ attr: `data-${key}="${value}"`,
484
+ file: file.path,
485
+ line: bounds.start + 1,
486
+ elementLines: bounds.end - bounds.start + 1
487
+ });
488
+ return {
489
+ filePath: file.path,
490
+ lineNumber: bounds.start + 1,
491
+ elementCode,
492
+ matchedBy: 'data-attribute',
493
+ confidence: 'high'
494
+ };
495
+ }
496
+ }
497
+ }
498
+ }
499
+ }
500
+ }
501
+
502
+ // Priority 3: Match by exact alt text (good for unique logos)
503
+ if (imageContext.altText && imageContext.altText.length > 3) {
504
+ const altPatterns = [
505
+ new RegExp(`alt=["'\`]${escapeRegex(imageContext.altText)}["'\`]`),
506
+ new RegExp(`alt=\\{["'\`]${escapeRegex(imageContext.altText)}["'\`]\\}`),
507
+ ];
508
+
509
+ const matches: Array<{ file: string; line: number; context: string; bounds: JSXElementBounds }> = [];
510
+
511
+ for (const file of componentFiles) {
512
+ const lines = file.content.split('\n');
513
+ for (let i = 0; i < lines.length; i++) {
514
+ for (const pattern of altPatterns) {
515
+ if (pattern.test(lines[i])) {
516
+ // Use findJSXElementBounds to get the complete element
517
+ const bounds = findJSXElementBounds(lines, i);
518
+ const elementCode = lines.slice(bounds.start, bounds.end + 1).join('\n');
519
+
520
+ if (elementCode.includes('<Image') || elementCode.includes('<img')) {
521
+ matches.push({ file: file.path, line: bounds.start + 1, context: elementCode, bounds });
522
+ }
523
+ }
524
+ }
525
+ }
526
+ }
527
+
528
+ // If only one match, it's precise
529
+ if (matches.length === 1) {
530
+ console.log('[Precise Location] Found by unique alt text:', {
531
+ alt: imageContext.altText,
532
+ file: matches[0].file,
533
+ line: matches[0].line,
534
+ elementLines: matches[0].bounds.end - matches[0].bounds.start + 1
535
+ });
536
+ return {
537
+ filePath: matches[0].file,
538
+ lineNumber: matches[0].line,
539
+ elementCode: matches[0].context,
540
+ matchedBy: 'alt-text',
541
+ confidence: 'high'
542
+ };
543
+ } else if (matches.length > 1) {
544
+ // Multiple matches - try to narrow down by page route
545
+ if (imageContext.pageRoute) {
546
+ const routeMatch = matches.find(m => {
547
+ const route = imageContext.pageRoute || '/';
548
+ return m.file.includes(`/app${route}`) ||
549
+ m.file.includes(`/pages${route}`) ||
550
+ (route === '/' && (m.file.includes('/app/page') || m.file.includes('/pages/index')));
551
+ });
552
+ if (routeMatch) {
553
+ console.log('[Precise Location] Found by alt text + page route:', {
554
+ alt: imageContext.altText,
555
+ route: imageContext.pageRoute,
556
+ file: routeMatch.file,
557
+ line: routeMatch.line,
558
+ elementLines: routeMatch.bounds.end - routeMatch.bounds.start + 1
559
+ });
560
+ return {
561
+ filePath: routeMatch.file,
562
+ lineNumber: routeMatch.line,
563
+ elementCode: routeMatch.context,
564
+ matchedBy: 'alt-text',
565
+ confidence: 'medium'
566
+ };
567
+ }
568
+ }
569
+ }
570
+ }
571
+
572
+ // Priority 4: Match by exact src path (less precise but useful)
573
+ if (imageContext.imageSrc) {
574
+ const fileName = imageContext.imageSrc.split('/').pop() || '';
575
+ const srcPatterns = [
576
+ new RegExp(`src=["'\`]${escapeRegex(imageContext.imageSrc)}["'\`]`),
577
+ // Also check for the filename only (for dynamic sources)
578
+ ...(fileName.length > 3 ? [new RegExp(`["'\`]${escapeRegex(fileName)}["'\`]`)] : []),
579
+ ];
580
+
581
+ const matches: Array<{ file: string; line: number; context: string; bounds: JSXElementBounds }> = [];
582
+
583
+ for (const file of componentFiles) {
584
+ const lines = file.content.split('\n');
585
+ for (let i = 0; i < lines.length; i++) {
586
+ for (const pattern of srcPatterns) {
587
+ if (pattern.test(lines[i])) {
588
+ // Use findJSXElementBounds to get the complete element
589
+ const bounds = findJSXElementBounds(lines, i);
590
+ const elementCode = lines.slice(bounds.start, bounds.end + 1).join('\n');
591
+
592
+ // Must be an image element, not just any file containing the path
593
+ if (elementCode.includes('<Image') || elementCode.includes('<img')) {
594
+ matches.push({ file: file.path, line: bounds.start + 1, context: elementCode, bounds });
595
+ }
596
+ }
597
+ }
598
+ }
599
+ }
600
+
601
+ if (matches.length === 1) {
602
+ console.log('[Precise Location] Found by src path:', {
603
+ src: imageContext.imageSrc,
604
+ file: matches[0].file,
605
+ line: matches[0].line,
606
+ elementLines: matches[0].bounds.end - matches[0].bounds.start + 1
607
+ });
608
+ return {
609
+ filePath: matches[0].file,
610
+ lineNumber: matches[0].line,
611
+ elementCode: matches[0].context,
612
+ matchedBy: 'src-path',
613
+ confidence: 'medium'
614
+ };
615
+ }
616
+ }
617
+
618
+ return null;
619
+ }
620
+
621
+ function escapeRegex(str: string): string {
622
+ return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
623
+ }
624
+
625
+ /**
626
+ * Main detection function - analyzes how an image is styled in the codebase
627
+ */
628
+ export async function detectImageStylingPattern(
629
+ projectRoot: string,
630
+ imageContext: ImageContext,
631
+ componentFiles: { path: string; content: string }[]
632
+ ): Promise<DetectionResult> {
633
+ const results: ImageStylingPattern[] = [];
634
+ const relatedFiles: string[] = [];
635
+
636
+ console.log('[Image Detection] Starting detection for:', {
637
+ src: imageContext.imageSrc,
638
+ alt: imageContext.altText,
639
+ elementId: imageContext.elementId,
640
+ pageRoute: imageContext.pageRoute,
641
+ });
642
+
643
+ // STEP 1: Try to find the PRECISE location of this image element
644
+ const preciseLocation = findPreciseImageLocation(imageContext, componentFiles);
645
+
646
+ if (preciseLocation) {
647
+ console.log('[Image Detection] Found precise location:', preciseLocation);
648
+ console.log('[Image Detection] Element code captured:', preciseLocation.elementCode.substring(0, 500));
649
+ relatedFiles.push(preciseLocation.filePath);
650
+
651
+ // Analyze the precise location for styling pattern
652
+ const file = componentFiles.find(f => f.path === preciseLocation.filePath);
653
+ if (file) {
654
+ // Check what styling method is used in this specific element
655
+ const elementCode = preciseLocation.elementCode;
656
+
657
+ // PRIORITY 1: Check for CSS variable patterns (including dynamic template literals)
658
+ const cssVarPatterns = [
659
+ // Static: var(--sonance-logo-scale)
660
+ /var\(--([a-zA-Z0-9-]+)-?(logo-scale|scale|size)/i,
661
+ // Dynamic template literal: var(--${brand}-logo-scale) or var(--${currentBrand}-logo-scale)
662
+ /var\(--\$\{[^}]+\}-(logo-scale|scale|size)/i,
663
+ // Template with scale: scale(var(--${...}))
664
+ /scale\(var\(--\$\{[^}]+\}/i,
665
+ // Any var(-- with scale nearby (using [\s\S] instead of . for multiline)
666
+ /style=\{\{[\s\S]*?var\(--[\s\S]*?scale/i,
667
+ ];
668
+
669
+ for (const pattern of cssVarPatterns) {
670
+ if (pattern.test(elementCode)) {
671
+ console.log('[Image Detection] Found CSS variable pattern in element');
672
+
673
+ // DYNAMIC EXTRACTION: No hardcoded values
674
+ // Extract variable pattern from element code
675
+ const extracted = extractCSSVariablePattern(elementCode);
676
+
677
+ // Determine brand using intelligent discovery (no hardcoded brand names)
678
+ const brand = determineBrandPrefix(imageContext, elementCode, projectRoot);
679
+
680
+ // Construct the CSS variable name
681
+ let cssVariable: string;
682
+ if (extracted.isDynamic) {
683
+ // Dynamic template literal: construct with discovered brand
684
+ cssVariable = `--${brand}-${extracted.suffix}`;
685
+ } else if (extracted.fullVariable) {
686
+ // Static variable: use the exact name from the code
687
+ cssVariable = extracted.fullVariable;
688
+ } else {
689
+ // Fallback: construct from brand and suffix
690
+ cssVariable = `--${brand}-${extracted.suffix}`;
691
+ }
692
+
693
+ // DYNAMIC CSS FILE DISCOVERY: No hardcoded paths
694
+ const discoveredFile = await discoverCSSFileForVariable(projectRoot, cssVariable);
695
+ const sourceFile = discoveredFile?.file || await findFirstCSSVariablesFile(projectRoot);
696
+
697
+ console.log('[Image Detection] CSS Variable detection (dynamic):', {
698
+ isDynamic: extracted.isDynamic,
699
+ extractedPrefix: extracted.prefix,
700
+ extractedSuffix: extracted.suffix,
701
+ determinedBrand: brand,
702
+ cssVariable,
703
+ sourceFile
704
+ });
705
+
706
+ return {
707
+ pattern: {
708
+ type: 'css-variable',
709
+ strategy: 'css-file',
710
+ confidence: 'high',
711
+ sourceFile,
712
+ cssVariable,
713
+ lineNumber: preciseLocation.lineNumber,
714
+ details: `Found ${extracted.isDynamic ? 'dynamic' : 'static'} CSS variable pattern. Will update ${cssVariable} in ${sourceFile}.`
715
+ },
716
+ relatedFiles: [preciseLocation.filePath, sourceFile]
717
+ };
718
+ }
719
+ }
720
+
721
+ // PRIORITY 2: Check for Tailwind scale class
722
+ if (elementCode.match(/className=.*scale-\d+/)) {
723
+ const scaleMatch = elementCode.match(/scale-(\d+)/);
724
+ return {
725
+ pattern: {
726
+ type: 'tailwind',
727
+ strategy: 'tailwind-class',
728
+ confidence: 'high',
729
+ sourceFile: preciseLocation.filePath,
730
+ lineNumber: preciseLocation.lineNumber,
731
+ existingClasses: scaleMatch ? [`scale-${scaleMatch[1]}`] : [],
732
+ details: `Found Tailwind scale class at ${preciseLocation.filePath}:${preciseLocation.lineNumber}`
733
+ },
734
+ relatedFiles: [preciseLocation.filePath]
735
+ };
736
+ }
737
+
738
+ // PRIORITY 3: Check for inline style transform (without CSS variable)
739
+ if (elementCode.includes('style=') && elementCode.includes('transform') && !elementCode.includes('var(--')) {
740
+ return {
741
+ pattern: {
742
+ type: 'inline-style',
743
+ strategy: 'component-inline',
744
+ confidence: 'high',
745
+ sourceFile: preciseLocation.filePath,
746
+ lineNumber: preciseLocation.lineNumber,
747
+ details: `Found inline style at ${preciseLocation.filePath}:${preciseLocation.lineNumber}`
748
+ },
749
+ relatedFiles: [preciseLocation.filePath]
750
+ };
751
+ }
752
+
753
+ // PRIORITY 4: Check for Next.js Image with width/height props (lowest priority for scaling)
754
+ if (elementCode.includes('<Image') && (elementCode.includes('width=') || elementCode.includes('height='))) {
755
+ return {
756
+ pattern: {
757
+ type: 'next-image',
758
+ strategy: 'component-inline',
759
+ confidence: 'high',
760
+ sourceFile: preciseLocation.filePath,
761
+ lineNumber: preciseLocation.lineNumber,
762
+ details: `Found Next.js Image at ${preciseLocation.filePath}:${preciseLocation.lineNumber}`
763
+ },
764
+ relatedFiles: [preciseLocation.filePath]
765
+ };
766
+ }
767
+ }
768
+ }
769
+
770
+ // STEP 2: Fallback to heuristic detection if precise location not found
771
+ console.log('[Image Detection] No precise location, falling back to heuristic detection');
772
+
773
+ // Identify files that RENDER the image (have <Image> or <img> tags)
774
+ // vs files that just REFERENCE the path (config files)
775
+ const renderingFiles = componentFiles.filter(file => {
776
+ const hasImageTag = file.content.includes('<Image') || file.content.includes('<img');
777
+ const hasImagePath = file.content.includes(imageContext.imageSrc) ||
778
+ (imageContext.altText && file.content.includes(imageContext.altText));
779
+ // Must have both an image tag AND reference the image somehow
780
+ // OR reference via a variable (like brandLogos)
781
+ return hasImageTag && (hasImagePath || file.content.includes('brandLogos') || file.content.includes('logos.'));
782
+ });
783
+
784
+ console.log('[Image Detection] Found rendering files:', renderingFiles.map(f => f.path));
785
+
786
+ // 1. Check for CSS Variables pattern (HIGHEST PRIORITY for logos)
787
+ const cssVarResult = await detectCSSVariablePattern(projectRoot, imageContext, componentFiles);
788
+ if (cssVarResult) {
789
+ // Boost confidence if found in a rendering file
790
+ if (renderingFiles.some(f => cssVarResult.pattern.details?.includes(f.path))) {
791
+ cssVarResult.pattern.confidence = 'high';
792
+ }
793
+ results.push(cssVarResult.pattern);
794
+ relatedFiles.push(...cssVarResult.files);
795
+ }
796
+
797
+ // 2. Check for Next.js Image component pattern in RENDERING files only
798
+ const nextImageResult = detectNextImagePattern(imageContext, renderingFiles.length > 0 ? renderingFiles : componentFiles);
799
+ if (nextImageResult) {
800
+ results.push(nextImageResult);
801
+ }
802
+
803
+ // 3. Check for Tailwind pattern
804
+ const tailwindResult = detectTailwindPattern(imageContext, renderingFiles.length > 0 ? renderingFiles : componentFiles);
805
+ if (tailwindResult) {
806
+ results.push(tailwindResult);
807
+ }
808
+
809
+ // 4. Check for inline style pattern in RENDERING files only
810
+ const inlineResult = detectInlineStylePattern(imageContext, renderingFiles.length > 0 ? renderingFiles : componentFiles);
811
+ if (inlineResult) {
812
+ results.push(inlineResult);
813
+ }
814
+
815
+ // 5. Check for config file pattern (LOWEST PRIORITY - only if nothing else found)
816
+ if (results.length === 0) {
817
+ const configResult = await detectConfigFilePattern(projectRoot, imageContext, componentFiles);
818
+ if (configResult) {
819
+ results.push(configResult.pattern);
820
+ relatedFiles.push(...configResult.files);
821
+ }
822
+ }
823
+
824
+ // Determine if this is a brand logo (should prefer CSS variables)
825
+ const isBrandLogo = imageContext.imageSrc.toLowerCase().includes('/logos/') ||
826
+ imageContext.imageSrc.toLowerCase().includes('logo') ||
827
+ imageContext.altText?.toLowerCase().includes('logo');
828
+
829
+ // Sort by confidence, type priority, and strategy priority
830
+ // Priority order per plan:
831
+ // 1. CSS Variable (css-file) - highest priority for scale changes
832
+ // 2. Tailwind (tailwind-class) - second highest
833
+ // 3. Inline style (component-inline with inline-style type)
834
+ // 4. Next.js Image (component-inline with next-image type)
835
+ // 5. Config file (config-file)
836
+ // 6. AI-assisted (fallback)
837
+
838
+ const sortedResults = results.sort((a, b) => {
839
+ // For brand logos, prioritize CSS variables (they control global logo scaling)
840
+ if (isBrandLogo && a.type === 'css-variable' && a.cssVariable?.includes('logo-scale')) {
841
+ return -1; // CSS variable for logos always wins
842
+ }
843
+ if (isBrandLogo && b.type === 'css-variable' && b.cssVariable?.includes('logo-scale')) {
844
+ return 1;
845
+ }
846
+
847
+ // Type priority (more specific than strategy)
848
+ const typeOrder: Record<ImageStylingType, number> = {
849
+ 'css-variable': 7,
850
+ 'tailwind': 6,
851
+ 'inline-style': 5,
852
+ 'next-image': 4,
853
+ 'config-consumed': 3,
854
+ 'config-unused': 2,
855
+ 'styled-component': 2,
856
+ 'css-class': 2,
857
+ 'unknown': 1
858
+ };
859
+
860
+ const confidenceOrder = { high: 3, medium: 2, low: 1 };
861
+
862
+ // First sort by confidence
863
+ const confDiff = confidenceOrder[b.confidence] - confidenceOrder[a.confidence];
864
+ if (confDiff !== 0) return confDiff;
865
+
866
+ // Then by type priority
867
+ return typeOrder[b.type] - typeOrder[a.type];
868
+ });
869
+
870
+ console.log('[Image Detection] Sorted results:', sortedResults.map(r => ({
871
+ type: r.type,
872
+ strategy: r.strategy,
873
+ confidence: r.confidence,
874
+ file: r.sourceFile
875
+ })));
876
+
877
+ if (sortedResults.length > 0) {
878
+ return {
879
+ pattern: sortedResults[0],
880
+ relatedFiles: [...new Set(relatedFiles)]
881
+ };
882
+ }
883
+
884
+ // Fallback to AI-assisted
885
+ return {
886
+ pattern: {
887
+ type: 'unknown',
888
+ strategy: 'ai-assisted',
889
+ confidence: 'low',
890
+ details: 'Could not detect a specific pattern, will use AI-assisted modification'
891
+ },
892
+ relatedFiles: []
893
+ };
894
+ }
895
+
896
+ /**
897
+ * Detect CSS Variable pattern
898
+ * Looks for: transform: scale(var(--logo-scale))
899
+ * Must find the variable in the file that ACTUALLY RENDERS the image
900
+ */
901
+ async function detectCSSVariablePattern(
902
+ projectRoot: string,
903
+ imageContext: ImageContext,
904
+ componentFiles: { path: string; content: string }[]
905
+ ): Promise<{ pattern: ImageStylingPattern; files: string[] } | null> {
906
+
907
+ // Extract the image filename for matching
908
+ const imageName = imageContext.imageSrc.split('/').pop()?.replace(/\.[^.]+$/, '') || '';
909
+
910
+ // First, find files that actually render this specific image
911
+ const filesRenderingImage = componentFiles.filter(file => {
912
+ // Check if file contains the image source or image name
913
+ return file.content.includes(imageContext.imageSrc) ||
914
+ (imageName && file.content.includes(imageName)) ||
915
+ (imageContext.altText && file.content.includes(imageContext.altText));
916
+ });
917
+
918
+ // Specific patterns for logo/image scale CSS variables
919
+ const logoScalePatterns = [
920
+ // Pattern: scale(var(--brand-logo-scale, 1))
921
+ /scale\(var\(--([a-zA-Z0-9-]+)-logo-scale/i,
922
+ // Pattern: var(--brand-logo-scale)
923
+ /var\(--([a-zA-Z0-9-]+)-logo-scale/i,
924
+ // Pattern: transform: scale(var(--something))
925
+ /transform:\s*[`'"]?scale\(var\(--([a-zA-Z0-9-]+)\)/i,
926
+ ];
927
+
928
+ // Search in files that render this image for CSS variable usage
929
+ for (const file of filesRenderingImage) {
930
+ for (const pattern of logoScalePatterns) {
931
+ const match = file.content.match(pattern);
932
+ if (match) {
933
+ // Found CSS variable in a file that renders this image
934
+ const varPrefix = match[1];
935
+
936
+ // Construct the full variable name
937
+ let variableName: string;
938
+ if (match[0].includes('logo-scale')) {
939
+ variableName = `--${varPrefix}-logo-scale`;
940
+ } else {
941
+ variableName = `--${varPrefix}`;
942
+ }
943
+
944
+ // Search for CSS file that defines this variable
945
+ const cssFiles = await findCSSFilesDefiningVariable(projectRoot, variableName);
946
+
947
+ return {
948
+ pattern: {
949
+ type: 'css-variable',
950
+ strategy: 'css-file',
951
+ confidence: 'high',
952
+ sourceFile: cssFiles[0] || 'src/styles/brand-overrides.css',
953
+ cssVariable: variableName,
954
+ details: `Found CSS variable ${variableName} in ${file.path} (renders this image)`
955
+ },
956
+ files: cssFiles
957
+ };
958
+ }
959
+ }
960
+ }
961
+
962
+ // Check ALL rendering files for dynamic brand variable pattern (sidebar.tsx pattern)
963
+ // This catches: scale(var(--${currentBrand}-logo-scale, 1))
964
+ for (const file of filesRenderingImage) {
965
+ // Look for dynamic brand variable pattern - escape the $ properly
966
+ const dynamicPatterns = [
967
+ /scale\(var\(--\$\{[^}]+\}-logo-scale/, // Template literal: --${brand}-logo-scale
968
+ /--\w+-logo-scale/, // Static: --sonance-logo-scale
969
+ ];
970
+
971
+ for (const dynamicPattern of dynamicPatterns) {
972
+ if (dynamicPattern.test(file.content)) {
973
+ // This file uses brand-based logo scale variables
974
+ // Use intelligent brand detection (no hardcoded names)
975
+ const brand = determineBrandPrefix(imageContext, file.content, projectRoot);
976
+
977
+ // Extract the actual suffix from the pattern found
978
+ const suffixMatch = file.content.match(/var\(--[^)]*?-([a-zA-Z0-9-]+)\s*[,)]/);
979
+ const suffix = suffixMatch?.[1] || 'scale';
980
+
981
+ const variableName = `--${brand}-${suffix}`;
982
+ const cssFiles = await findCSSFilesDefiningVariable(projectRoot, variableName);
983
+
984
+ console.log('[CSS Detection] Found dynamic scale pattern:', {
985
+ file: file.path,
986
+ pattern: dynamicPattern.source,
987
+ brand,
988
+ suffix,
989
+ variableName,
990
+ cssFiles
991
+ });
992
+
993
+ return {
994
+ pattern: {
995
+ type: 'css-variable',
996
+ strategy: 'css-file',
997
+ confidence: 'high',
998
+ sourceFile: cssFiles[0] || 'src/styles/brand-overrides.css',
999
+ cssVariable: variableName,
1000
+ details: `Found brand logo-scale CSS variable ${variableName} in ${file.path}`
1001
+ },
1002
+ files: cssFiles
1003
+ };
1004
+ }
1005
+ }
1006
+ }
1007
+
1008
+ // Also check if the codebase has a pattern of CSS variables for images
1009
+ const globalCssPath = path.join(projectRoot, 'src', 'app', 'globals.css');
1010
+ const brandOverridesPath = path.join(projectRoot, 'src', 'styles', 'brand-overrides.css');
1011
+
1012
+ for (const cssPath of [brandOverridesPath, globalCssPath]) {
1013
+ if (fs.existsSync(cssPath)) {
1014
+ const content = fs.readFileSync(cssPath, 'utf-8');
1015
+ // Check for any scale-related CSS variables (no hardcoded suffixes)
1016
+ if (content.match(/--[a-zA-Z0-9]+-(?:logo-)?scale\s*:/)) {
1017
+ // Use intelligent brand detection (no hardcoded names)
1018
+ const brand = determineBrandPrefix(imageContext, content, projectRoot);
1019
+
1020
+ // Extract the actual suffix pattern from the CSS file
1021
+ const suffixMatch = content.match(/--[a-zA-Z0-9]+-([a-zA-Z0-9-]*scale[a-zA-Z0-9-]*)\s*:/);
1022
+ const suffix = suffixMatch?.[1] || 'scale';
1023
+
1024
+ const variableName = `--${brand}-${suffix}`;
1025
+ const relativePath = cssPath.replace(projectRoot + '/', '');
1026
+
1027
+ return {
1028
+ pattern: {
1029
+ type: 'css-variable',
1030
+ strategy: 'css-file',
1031
+ confidence: 'medium',
1032
+ sourceFile: relativePath,
1033
+ cssVariable: variableName,
1034
+ details: `Found scale CSS variables in ${relativePath}. Using ${variableName} based on detected patterns.`
1035
+ },
1036
+ files: [cssPath.replace(projectRoot + '/', '')]
1037
+ };
1038
+ }
1039
+ }
1040
+ }
1041
+
1042
+ return null;
1043
+ }
1044
+
1045
+ /**
1046
+ * Detect Tailwind pattern
1047
+ * Looks for: scale-90, w-24, h-auto, etc.
1048
+ */
1049
+ function detectTailwindPattern(
1050
+ imageContext: ImageContext,
1051
+ componentFiles: { path: string; content: string }[]
1052
+ ): ImageStylingPattern | null {
1053
+ const tailwindScalePattern = /\bscale-(\d+)\b/;
1054
+ const tailwindSizePatterns = [
1055
+ /\bw-(\d+|auto|full)\b/,
1056
+ /\bh-(\d+|auto|full)\b/,
1057
+ /\bmax-w-(\d+|full|screen)\b/,
1058
+ /\bmax-h-(\d+|full|screen)\b/,
1059
+ ];
1060
+
1061
+ // Check className from the image element
1062
+ if (imageContext.className) {
1063
+ const scaleMatch = imageContext.className.match(tailwindScalePattern);
1064
+ const hasSizeClasses = tailwindSizePatterns.some(p => p.test(imageContext.className || ''));
1065
+
1066
+ if (scaleMatch || hasSizeClasses) {
1067
+ // Find the file containing this image
1068
+ for (const file of componentFiles) {
1069
+ if (file.content.includes(imageContext.imageSrc) ||
1070
+ (imageContext.altText && file.content.includes(imageContext.altText))) {
1071
+ return {
1072
+ type: 'tailwind',
1073
+ strategy: 'tailwind-class',
1074
+ confidence: 'high',
1075
+ sourceFile: file.path,
1076
+ existingClasses: imageContext.className?.split(' ').filter(c =>
1077
+ tailwindScalePattern.test(c) || tailwindSizePatterns.some(p => p.test(c))
1078
+ ),
1079
+ details: `Found Tailwind sizing classes: ${imageContext.className}`
1080
+ };
1081
+ }
1082
+ }
1083
+ }
1084
+ }
1085
+
1086
+ // Check if project uses Tailwind
1087
+ const hasTailwind = componentFiles.some(f =>
1088
+ f.content.includes('className=') &&
1089
+ (f.content.includes('scale-') || f.content.includes(' w-') || f.content.includes(' h-'))
1090
+ );
1091
+
1092
+ if (hasTailwind) {
1093
+ return {
1094
+ type: 'tailwind',
1095
+ strategy: 'tailwind-class',
1096
+ confidence: 'low',
1097
+ details: 'Project uses Tailwind, can apply scale via class'
1098
+ };
1099
+ }
1100
+
1101
+ return null;
1102
+ }
1103
+
1104
+ /**
1105
+ * Detect inline style pattern
1106
+ * Looks for: style={{ transform: 'scale(0.9)' }}
1107
+ * Only in files that actually RENDER images (not config files)
1108
+ */
1109
+ function detectInlineStylePattern(
1110
+ imageContext: ImageContext,
1111
+ componentFiles: { path: string; content: string }[]
1112
+ ): ImageStylingPattern | null {
1113
+ const inlineStylePatterns = [
1114
+ /style=\{\{[^}]*transform:[^}]*scale\(/,
1115
+ /style=\{\{[^}]*width:[^}]*/,
1116
+ /style=\{\{[^}]*height:[^}]*/,
1117
+ ];
1118
+
1119
+ for (const file of componentFiles) {
1120
+ // Skip config/system files that don't render images
1121
+ if (file.path.includes('brand-system') ||
1122
+ file.path.includes('config') ||
1123
+ file.path.includes('/lib/') && !file.path.includes('components')) {
1124
+ continue;
1125
+ }
1126
+
1127
+ // Must have an actual image tag to be a rendering file
1128
+ const hasImageTag = file.content.includes('<Image') || file.content.includes('<img');
1129
+ if (!hasImageTag) continue;
1130
+
1131
+ // Check if this file might render the image (via direct src or variable)
1132
+ const mightRenderImage = file.content.includes(imageContext.imageSrc) ||
1133
+ (imageContext.altText && file.content.includes(imageContext.altText)) ||
1134
+ file.content.includes('brandLogos') ||
1135
+ file.content.includes('logos.');
1136
+
1137
+ if (mightRenderImage) {
1138
+ for (const pattern of inlineStylePatterns) {
1139
+ if (pattern.test(file.content)) {
1140
+ // Find the line number of the image element
1141
+ const lines = file.content.split('\n');
1142
+ let lineNumber = 0;
1143
+ for (let i = 0; i < lines.length; i++) {
1144
+ if ((lines[i].includes('<Image') || lines[i].includes('<img')) &&
1145
+ (lines[i].includes('style=') ||
1146
+ (i + 5 < lines.length && lines.slice(i, i + 5).join('').includes('style=')))) {
1147
+ lineNumber = i + 1;
1148
+ break;
1149
+ }
1150
+ }
1151
+
1152
+ return {
1153
+ type: 'inline-style',
1154
+ strategy: 'component-inline',
1155
+ confidence: 'high',
1156
+ sourceFile: file.path,
1157
+ lineNumber,
1158
+ details: `Found inline style with transform/dimensions in ${file.path}`
1159
+ };
1160
+ }
1161
+ }
1162
+ }
1163
+ }
1164
+
1165
+ return null;
1166
+ }
1167
+
1168
+ /**
1169
+ * Detect config file pattern
1170
+ * Looks for: brandLogos, imageConfig, logoSizes, etc.
1171
+ */
1172
+ async function detectConfigFilePattern(
1173
+ projectRoot: string,
1174
+ imageContext: ImageContext,
1175
+ componentFiles: { path: string; content: string }[]
1176
+ ): Promise<{ pattern: ImageStylingPattern; files: string[] } | null> {
1177
+ const configPatterns = [
1178
+ { pattern: /export const (brandLogos|logoConfig|logos)/i, type: 'logo-config' },
1179
+ { pattern: /export const (imageConfig|images|imageSizes)/i, type: 'image-config' },
1180
+ { pattern: /export const (logoSizes|logoDimensions)/i, type: 'logo-sizes' },
1181
+ { pattern: /export const (theme|themeConfig)/i, type: 'theme' },
1182
+ ];
1183
+
1184
+ const configFiles: string[] = [];
1185
+
1186
+ // Extract filename from image src for matching
1187
+ const imageName = imageContext.imageSrc.split('/').pop()?.replace(/\.[^.]+$/, '') || '';
1188
+
1189
+ for (const file of componentFiles) {
1190
+ // Check if this is a config file
1191
+ for (const { pattern, type } of configPatterns) {
1192
+ if (pattern.test(file.content)) {
1193
+ // Check if the image is referenced in this config
1194
+ if (file.content.includes(imageContext.imageSrc) ||
1195
+ (imageName && file.content.includes(imageName))) {
1196
+
1197
+ // Determine if the config is actually consumed
1198
+ const configName = file.content.match(pattern)?.[1] || '';
1199
+ const isConsumed = componentFiles.some(f =>
1200
+ f.path !== file.path &&
1201
+ (f.content.includes(`import { ${configName}`) ||
1202
+ f.content.includes(`import {${configName}`) ||
1203
+ f.content.includes(`${configName}.`) ||
1204
+ f.content.includes(`${configName}[`))
1205
+ );
1206
+
1207
+ configFiles.push(file.path);
1208
+
1209
+ // Check if there's dimension/scale config being used
1210
+ const hasSizeConfig = file.content.includes('scale') ||
1211
+ file.content.includes('width') ||
1212
+ file.content.includes('height');
1213
+
1214
+ return {
1215
+ pattern: {
1216
+ type: isConsumed ? 'config-consumed' : 'config-unused',
1217
+ strategy: isConsumed ? 'config-file' : 'ai-assisted',
1218
+ confidence: isConsumed && hasSizeConfig ? 'high' : 'medium',
1219
+ sourceFile: file.path,
1220
+ configKey: imageName,
1221
+ details: `Found ${type} in ${file.path}. Config is ${isConsumed ? 'consumed' : 'NOT consumed'} by components.`
1222
+ },
1223
+ files: configFiles
1224
+ };
1225
+ }
1226
+ }
1227
+ }
1228
+ }
1229
+
1230
+ return null;
1231
+ }
1232
+
1233
+ /**
1234
+ * Detect Next.js Image component pattern
1235
+ * Looks for: <Image width={} height={} />
1236
+ */
1237
+ function detectNextImagePattern(
1238
+ imageContext: ImageContext,
1239
+ componentFiles: { path: string; content: string }[]
1240
+ ): ImageStylingPattern | null {
1241
+ const nextImagePatterns = [
1242
+ /<Image[^>]*\s+width=/,
1243
+ /from ['"]next\/image['"]/,
1244
+ /import Image from ['"]next\/image['"]/,
1245
+ ];
1246
+
1247
+ for (const file of componentFiles) {
1248
+ const hasNextImage = nextImagePatterns.some(p => p.test(file.content));
1249
+ const containsImage = file.content.includes(imageContext.imageSrc) ||
1250
+ (imageContext.altText && file.content.includes(imageContext.altText));
1251
+
1252
+ if (hasNextImage && containsImage) {
1253
+ // Find the specific Image element
1254
+ const lines = file.content.split('\n');
1255
+ let lineNumber = 0;
1256
+
1257
+ for (let i = 0; i < lines.length; i++) {
1258
+ if (lines[i].includes(imageContext.imageSrc) ||
1259
+ (imageContext.altText && lines[i].includes(imageContext.altText))) {
1260
+ lineNumber = i + 1;
1261
+ break;
1262
+ }
1263
+ }
1264
+
1265
+ return {
1266
+ type: 'next-image',
1267
+ strategy: 'component-inline',
1268
+ confidence: 'high',
1269
+ sourceFile: file.path,
1270
+ lineNumber,
1271
+ details: `Found Next.js Image component in ${file.path}. Modify width/height props or add scale transform.`
1272
+ };
1273
+ }
1274
+ }
1275
+
1276
+ return null;
1277
+ }
1278
+
1279
+ /**
1280
+ * Find CSS files that define a specific variable
1281
+ */
1282
+ async function findCSSFilesDefiningVariable(
1283
+ projectRoot: string,
1284
+ variableName: string
1285
+ ): Promise<string[]> {
1286
+ const cssLocations = [
1287
+ 'src/styles',
1288
+ 'src/app',
1289
+ 'styles',
1290
+ 'app',
1291
+ ];
1292
+
1293
+ const foundFiles: string[] = [];
1294
+
1295
+ for (const location of cssLocations) {
1296
+ const dirPath = path.join(projectRoot, location);
1297
+ if (!fs.existsSync(dirPath)) continue;
1298
+
1299
+ const files = fs.readdirSync(dirPath);
1300
+ for (const file of files) {
1301
+ if (!file.endsWith('.css')) continue;
1302
+
1303
+ const filePath = path.join(dirPath, file);
1304
+ const content = fs.readFileSync(filePath, 'utf-8');
1305
+
1306
+ // Check if this file defines the variable
1307
+ if (content.includes(variableName + ':') || content.includes(variableName + ' :')) {
1308
+ foundFiles.push(path.join(location, file));
1309
+ }
1310
+ }
1311
+ }
1312
+
1313
+ return foundFiles;
1314
+ }
1315
+
1316
+ /**
1317
+ * Build save instructions based on detected pattern
1318
+ */
1319
+ export function buildSaveInstructions(pattern: ImageStylingPattern, override: {
1320
+ scale?: number;
1321
+ width?: number;
1322
+ height?: number;
1323
+ src?: string;
1324
+ }): string {
1325
+ switch (pattern.strategy) {
1326
+ case 'css-file':
1327
+ return `Update CSS file "${pattern.sourceFile}":
1328
+ - Set ${pattern.cssVariable || '--logo-scale'}: ${override.scale || 1}
1329
+ ${override.width ? `- Set width variable: ${override.width}px` : ''}
1330
+ ${override.height ? `- Set height variable: ${override.height}px` : ''}`;
1331
+
1332
+ case 'tailwind-class':
1333
+ const newScale = override.scale ? Math.round(override.scale * 100) : 100;
1334
+ return `Update Tailwind classes in "${pattern.sourceFile}":
1335
+ - ${pattern.existingClasses?.includes(`scale-${newScale}`) ? 'Keep' : 'Replace'} scale class with scale-${newScale}
1336
+ ${override.width ? `- Add/update width class` : ''}
1337
+ ${override.height ? `- Add/update height class` : ''}`;
1338
+
1339
+ case 'component-inline':
1340
+ return `Update component in "${pattern.sourceFile}" at line ${pattern.lineNumber}:
1341
+ - Set transform: scale(${override.scale || 1})
1342
+ ${override.width ? `- Set width: ${override.width}px` : ''}
1343
+ ${override.height ? `- Set height: ${override.height}px` : ''}`;
1344
+
1345
+ case 'config-file':
1346
+ return `Update config in "${pattern.sourceFile}":
1347
+ - Key: ${pattern.configKey}
1348
+ - Set scale: ${override.scale || 1}
1349
+ ${override.width ? `- Set width: ${override.width}` : ''}
1350
+ ${override.height ? `- Set height: ${override.height}` : ''}`;
1351
+
1352
+ case 'ai-assisted':
1353
+ default:
1354
+ return `Use AI to intelligently apply changes:
1355
+ - Scale: ${override.scale || 1}
1356
+ ${override.width ? `- Width: ${override.width}px` : ''}
1357
+ ${override.height ? `- Height: ${override.height}px` : ''}
1358
+ ${override.src ? `- Source: ${override.src}` : ''}`;
1359
+ }
1360
+ }