sonance-brand-mcp 1.3.111 → 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.
- package/dist/assets/api/sonance-save-image/route.ts +625 -0
- package/dist/assets/api/sonance-vision-apply/image-styling-detection.ts +1360 -0
- package/dist/assets/api/sonance-vision-apply/route.ts +988 -57
- package/dist/assets/api/sonance-vision-apply/styling-detection.ts +730 -0
- package/dist/assets/api/sonance-vision-apply/theme-discovery.ts +1 -1
- package/dist/assets/brand-system.ts +13 -12
- package/dist/assets/components/accordion.tsx +15 -7
- package/dist/assets/components/alert-dialog.tsx +35 -10
- package/dist/assets/components/alert.tsx +11 -10
- package/dist/assets/components/avatar.tsx +4 -4
- package/dist/assets/components/badge.tsx +16 -12
- package/dist/assets/components/button.stories.tsx +3 -3
- package/dist/assets/components/button.tsx +50 -31
- package/dist/assets/components/calendar.tsx +12 -8
- package/dist/assets/components/card.tsx +35 -29
- package/dist/assets/components/checkbox.tsx +9 -8
- package/dist/assets/components/code.tsx +19 -11
- package/dist/assets/components/command.tsx +32 -13
- package/dist/assets/components/context-menu.tsx +37 -16
- package/dist/assets/components/dialog.tsx +8 -5
- package/dist/assets/components/divider.tsx +15 -5
- package/dist/assets/components/drawer.tsx +4 -3
- package/dist/assets/components/dropdown-menu.tsx +15 -13
- package/dist/assets/components/hover-card.tsx +4 -1
- package/dist/assets/components/image.tsx +1 -1
- package/dist/assets/components/input.tsx +29 -14
- package/dist/assets/components/kbd.stories.tsx +3 -3
- package/dist/assets/components/kbd.tsx +29 -13
- package/dist/assets/components/listbox.tsx +8 -8
- package/dist/assets/components/menubar.tsx +50 -23
- package/dist/assets/components/navbar.stories.tsx +140 -13
- package/dist/assets/components/navbar.tsx +22 -5
- package/dist/assets/components/navigation-menu.tsx +28 -6
- package/dist/assets/components/pagination.tsx +10 -10
- package/dist/assets/components/popover.tsx +10 -8
- package/dist/assets/components/progress.tsx +6 -4
- package/dist/assets/components/radio-group.tsx +5 -5
- package/dist/assets/components/select.tsx +49 -29
- package/dist/assets/components/separator.tsx +3 -3
- package/dist/assets/components/sheet.tsx +4 -4
- package/dist/assets/components/sidebar.tsx +10 -10
- package/dist/assets/components/skeleton.tsx +13 -5
- package/dist/assets/components/slider.tsx +12 -10
- package/dist/assets/components/switch.tsx +4 -4
- package/dist/assets/components/table.tsx +5 -5
- package/dist/assets/components/tabs.tsx +8 -8
- package/dist/assets/components/textarea.tsx +11 -9
- package/dist/assets/components/toast.tsx +7 -7
- package/dist/assets/components/toggle.tsx +27 -7
- package/dist/assets/components/tooltip.tsx +10 -8
- package/dist/assets/components/user.tsx +8 -6
- package/dist/assets/dev-tools/SonanceDevTools.tsx +429 -362
- package/dist/assets/dev-tools/components/ApplyFirstPreview.tsx +10 -10
- package/dist/assets/dev-tools/components/ChatHistory.tsx +11 -7
- package/dist/assets/dev-tools/components/ChatInterface.tsx +61 -20
- package/dist/assets/dev-tools/components/ChatTabBar.tsx +1 -1
- package/dist/assets/dev-tools/components/DiffPreview.tsx +1 -1
- package/dist/assets/dev-tools/components/InlineDiffPreview.tsx +360 -36
- package/dist/assets/dev-tools/components/InspectorOverlay.tsx +9 -9
- package/dist/assets/dev-tools/components/PropertiesPanel.tsx +743 -93
- package/dist/assets/dev-tools/components/ScreenshotAnnotator.tsx +1 -1
- package/dist/assets/dev-tools/components/SectionHighlight.tsx +1 -1
- package/dist/assets/dev-tools/components/VisionDiffPreview.tsx +7 -7
- package/dist/assets/dev-tools/components/VisionModeBorder.tsx +4 -64
- package/dist/assets/dev-tools/hooks/index.ts +69 -0
- package/dist/assets/dev-tools/hooks/useComponentDetection.ts +132 -0
- package/dist/assets/dev-tools/hooks/useComputedStyles.ts +171 -65
- package/dist/assets/dev-tools/hooks/useContentHash.ts +212 -0
- package/dist/assets/dev-tools/hooks/useElementScanner.ts +398 -0
- package/dist/assets/dev-tools/hooks/useImageDetection.ts +162 -0
- package/dist/assets/dev-tools/hooks/useTextDetection.ts +217 -0
- package/dist/assets/dev-tools/panels/ComponentsPanel.tsx +160 -57
- package/dist/assets/dev-tools/panels/TextPanel.tsx +10 -10
- package/dist/assets/dev-tools/types.ts +42 -0
- package/dist/assets/globals.css +225 -9
- package/dist/assets/styles/brand-overrides.css +3 -2
- package/dist/assets/utils.ts +2 -1
- package/package.json +1 -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
|
+
}
|