sonance-brand-mcp 1.3.20 → 1.3.22

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.
@@ -64,26 +64,33 @@ You can see screenshots of web pages and understand their visual layout, then mo
64
64
  CRITICAL RULES
65
65
  ═══════════════════════════════════════════════════════════════════════════════
66
66
 
67
+ **FILE RULES (MOST IMPORTANT):**
68
+ 1. You may ONLY edit files that are provided in the PAGE CONTEXT section
69
+ 2. NEVER create new files - only modify existing ones shown to you
70
+ 3. The filePath in your response MUST exactly match one of the provided file paths
71
+ 4. If you cannot find the right file to edit, explain this in your response instead of creating a new file
72
+
67
73
  **ANALYSIS:**
68
- 1. CAREFULLY analyze the screenshot to understand the current visual state
69
- 2. Identify elements mentioned in the user's request
70
- 3. Understand the current styling and layout
71
- 4. Consider how changes will affect the overall design
74
+ 5. CAREFULLY analyze the screenshot to understand the current visual state
75
+ 6. Identify elements mentioned in the user's request
76
+ 7. Understand the current styling and layout
77
+ 8. Consider how changes will affect the overall design
72
78
 
73
79
  **PRESERVATION RULES:**
74
- 5. NEVER delete or remove existing content, children, or JSX elements
75
- 6. NEVER change component structure unless specifically requested
76
- 7. NEVER modify TypeScript types, imports, or exports unless necessary
77
- 8. NEVER remove data-sonance-* attributes
80
+ 9. NEVER delete or remove existing content, children, or JSX elements
81
+ 10. NEVER change component structure unless specifically requested
82
+ 11. NEVER modify TypeScript types, imports, or exports unless necessary
83
+ 12. NEVER remove data-sonance-* attributes
78
84
 
79
85
  **CHANGE RULES:**
80
- 9. Make ONLY the changes requested by the user
81
- 10. Modify the minimum amount of code necessary
82
- 11. Use semantic Tailwind classes (bg-primary, text-foreground, etc.)
83
- 12. Maintain dark mode compatibility with CSS variables
84
- 13. Keep the cn() utility for className merging
85
-
86
- **SONANCE BRAND COLORS:**
86
+ 13. Make ONLY the changes requested by the user
87
+ 14. Modify the minimum amount of code necessary
88
+ 15. Use semantic Tailwind classes (bg-primary, text-foreground, etc.)
89
+ 16. Maintain dark mode compatibility with CSS variables
90
+ 17. Keep the cn() utility for className merging
91
+ 18. CRITICAL: You MUST return the FULL file content in "modifiedContent". Do NOT use comments like "// ... existing code ..." or "// ... rest of file ...". Return every single line of code, even if unchanged.
92
+
93
+ **SONANCE BRAND COLORS:**
87
94
  - Charcoal: #333F48, #343D46 (primary)
88
95
  - Silver: #E2E2E2, #D1D1D6 (secondary)
89
96
  - IPORT Orange: #FC4C02
@@ -95,11 +102,11 @@ CRITICAL RULES
95
102
  Return ONLY a valid JSON object. Do not include any conversational text before or after the JSON.
96
103
  The JSON must include:
97
104
  - "reasoning": Brief explanation of what you see in the screenshot and your plan
98
- - "modifications": Array of file modifications, each with:
99
- - "filePath": Path to the file
100
- - "modifiedContent": Complete updated file content
101
- - "explanation": What changed in this file
102
- - "previewCSS": CSS for live preview (use [data-sonance-name="ComponentName"] selectors)
105
+ - "modifications": Array of file modifications, each with:
106
+ - "filePath": Path to the file
107
+ - "modifiedContent": Complete updated file content (MUST BE FULL CONTENT, NO TRUNCATION)
108
+ - "explanation": What changed in this file
109
+ - "previewCSS": CSS for live preview (use [data-sonance-name="ComponentName"] selectors)
103
110
  - "aggregatedPreviewCSS": Combined CSS for all changes
104
111
  - "explanation": Overall summary of changes`;
105
112
 
@@ -188,8 +195,8 @@ export async function POST(request: Request) {
188
195
  );
189
196
  }
190
197
 
191
- // Gather page context
192
- const pageContext = gatherPageContext(pageRoute, projectRoot);
198
+ // Gather page context (including focused element files via project-wide search)
199
+ const pageContext = gatherPageContext(pageRoute, projectRoot, focusedElements);
193
200
 
194
201
  // Build user message with vision
195
202
  const messageContent: Anthropic.MessageCreateParams["messages"][0]["content"] = [];
@@ -247,13 +254,19 @@ GLOBALS.CSS (relevant theme variables):
247
254
  ${pageContext.globalsCSS.substring(0, 2000)}${pageContext.globalsCSS.length > 2000 ? "\n/* ... (truncated) */" : ""}
248
255
  \`\`\`
249
256
 
257
+ VALID FILES YOU MAY EDIT:
258
+ ${pageContext.pageFile ? `- ${pageContext.pageFile}` : ""}
259
+ ${pageContext.componentSources.map((c) => `- ${c.path}`).join("\n")}
260
+
250
261
  INSTRUCTIONS:
251
262
  1. Look at the screenshot and identify elements mentioned in the user's request
252
- 2. Review the code to understand current implementation
253
- 3. Determine which files need modifications
263
+ 2. Review the provided code to understand current implementation
264
+ 3. Choose which of the VALID FILES above need modifications
254
265
  4. Generate complete modified code for each file
255
266
  5. Provide previewCSS for immediate visual feedback
256
- 6. Return as JSON in the specified format`;
267
+ 6. Return as JSON in the specified format
268
+
269
+ CRITICAL: Only use file paths from the VALID FILES list above. Do NOT create new files.`;
257
270
 
258
271
  messageContent.push({
259
272
  type: "text",
@@ -327,6 +340,34 @@ INSTRUCTIONS:
327
340
  );
328
341
  }
329
342
 
343
+ // Build list of valid file paths
344
+ const validPaths = new Set<string>();
345
+ if (pageContext.pageFile) {
346
+ validPaths.add(pageContext.pageFile);
347
+ }
348
+ for (const comp of pageContext.componentSources) {
349
+ validPaths.add(comp.path);
350
+ }
351
+
352
+ // Validate AI response - reject any file paths not in our valid list
353
+ const invalidMods = (aiResponse.modifications || []).filter(
354
+ (mod) => !validPaths.has(mod.filePath)
355
+ );
356
+
357
+ if (invalidMods.length > 0) {
358
+ console.error(
359
+ "AI attempted to create new files:",
360
+ invalidMods.map((m) => m.filePath)
361
+ );
362
+ return NextResponse.json(
363
+ {
364
+ success: false,
365
+ error: `Cannot create new files. The following paths were not found in the project: ${invalidMods.map((m) => m.filePath).join(", ")}. Please try a more specific request targeting existing components.`,
366
+ } as VisionEditResponse,
367
+ { status: 400 }
368
+ );
369
+ }
370
+
330
371
  // Read original content and generate diffs for each modification
331
372
  const modificationsWithOriginals: VisionFileModification[] = [];
332
373
  for (const mod of aiResponse.modifications || []) {
@@ -374,12 +415,77 @@ INSTRUCTIONS:
374
415
  }
375
416
  }
376
417
 
418
+ /**
419
+ * Search for component files by name across the project
420
+ * Uses similar logic to /api/sonance-find-component
421
+ */
422
+ function findComponentFileByName(
423
+ componentName: string,
424
+ projectRoot: string
425
+ ): string | null {
426
+ const normalizedName = componentName.toLowerCase().replace(/\s+/g, "-");
427
+ const baseName = normalizedName.split("-")[0];
428
+
429
+ const SEARCH_DIRS = ["src/components", "components", "src", "app"];
430
+ const EXTENSIONS = [".tsx", ".jsx", ".ts", ".js"];
431
+
432
+ function searchRecursive(
433
+ dir: string,
434
+ fileName: string,
435
+ depth = 0
436
+ ): string | null {
437
+ if (depth > 4) return null;
438
+ try {
439
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
440
+ // Check direct matches
441
+ for (const ext of EXTENSIONS) {
442
+ const match = entries.find(
443
+ (e) => e.isFile() && e.name.toLowerCase() === `${fileName}${ext}`
444
+ );
445
+ if (match) return path.join(dir, match.name);
446
+ }
447
+ // Recurse into subdirs
448
+ for (const entry of entries) {
449
+ if (
450
+ entry.isDirectory() &&
451
+ !entry.name.startsWith(".") &&
452
+ entry.name !== "node_modules"
453
+ ) {
454
+ const result = searchRecursive(
455
+ path.join(dir, entry.name),
456
+ fileName,
457
+ depth + 1
458
+ );
459
+ if (result) return result;
460
+ }
461
+ }
462
+ } catch {
463
+ /* skip unreadable */
464
+ }
465
+ return null;
466
+ }
467
+
468
+ for (const dir of SEARCH_DIRS) {
469
+ const searchDir = path.join(projectRoot, dir);
470
+ if (fs.existsSync(searchDir)) {
471
+ const result = searchRecursive(searchDir, normalizedName);
472
+ if (result) return path.relative(projectRoot, result);
473
+ if (baseName !== normalizedName) {
474
+ const baseResult = searchRecursive(searchDir, baseName);
475
+ if (baseResult) return path.relative(projectRoot, baseResult);
476
+ }
477
+ }
478
+ }
479
+ return null;
480
+ }
481
+
377
482
  /**
378
483
  * Gather context about the current page for AI analysis
379
484
  */
380
485
  function gatherPageContext(
381
486
  pageRoute: string,
382
- projectRoot: string
487
+ projectRoot: string,
488
+ focusedElements?: VisionFocusedElement[]
383
489
  ): {
384
490
  pageFile: string | null;
385
491
  pageContent: string;
@@ -412,6 +518,25 @@ function gatherPageContext(
412
518
  }
413
519
  }
414
520
 
521
+ // Also search for focused element components if provided
522
+ if (focusedElements && focusedElements.length > 0) {
523
+ for (const el of focusedElements) {
524
+ // Try to find component file by name
525
+ const foundPath = findComponentFileByName(el.name, projectRoot);
526
+ if (foundPath && !componentSources.some((c) => c.path === foundPath)) {
527
+ try {
528
+ const content = fs.readFileSync(
529
+ path.join(projectRoot, foundPath),
530
+ "utf-8"
531
+ );
532
+ componentSources.push({ path: foundPath, content });
533
+ } catch {
534
+ /* skip if unreadable */
535
+ }
536
+ }
537
+ }
538
+ }
539
+
415
540
  // Read globals.css
416
541
  let globalsCSS = "";
417
542
  const globalsPath = path.join(projectRoot, "src/app/globals.css");
@@ -11,6 +11,8 @@ interface CheckboxProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>
11
11
  description?: string;
12
12
  /** Visual state for Storybook/Figma documentation */
13
13
  state?: CheckboxState;
14
+ /** Radix UI-style callback for checked state changes */
15
+ onCheckedChange?: (checked: boolean) => void;
14
16
  }
15
17
 
16
18
  // State styles for Storybook/Figma visualization
@@ -28,17 +30,23 @@ const getStateStyles = (state?: CheckboxState) => {
28
30
  };
29
31
 
30
32
  export const Checkbox = forwardRef<HTMLInputElement, CheckboxProps>(
31
- ({ className, label, description, id, state, disabled, checked, defaultChecked, onChange, style, ...props }, ref) => {
33
+ ({ className, label, description, id, state, disabled, checked, defaultChecked, onChange, onCheckedChange, style, ...props }, ref) => {
32
34
  const uniqueId = useId();
33
35
  const inputId = id || `checkbox-${uniqueId}`;
34
36
  const isDisabled = disabled || state === "disabled";
35
37
 
36
- // Determine if we're in controlled mode
37
- const isControlled = checked !== undefined || onChange !== undefined;
38
+ // Determine if we're in controlled mode (only when checked is explicitly provided)
39
+ const isControlled = checked !== undefined;
38
40
  const isCheckedForState = state === "checked";
39
41
 
42
+ // Combined change handler that supports both native onChange and Radix-style onCheckedChange
43
+ const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
44
+ onChange?.(e);
45
+ onCheckedChange?.(e.target.checked);
46
+ };
47
+
40
48
  return (
41
- <div data-sonance-name="checkbox" className="flex items-start gap-3">
49
+ <label data-sonance-name="checkbox" className="flex items-start gap-3 cursor-pointer">
42
50
  <div className="relative flex items-center justify-center">
43
51
  <input
44
52
  type="checkbox"
@@ -46,10 +54,12 @@ export const Checkbox = forwardRef<HTMLInputElement, CheckboxProps>(
46
54
  ref={ref}
47
55
  disabled={isDisabled}
48
56
  style={style}
57
+ onChange={handleChange}
49
58
  {...(isControlled
50
- ? { checked: checked || isCheckedForState, onChange }
51
- : { defaultChecked: defaultChecked || isCheckedForState }
59
+ ? { checked: checked ?? isCheckedForState }
60
+ : { defaultChecked: defaultChecked ?? isCheckedForState }
52
61
  )}
62
+ readOnly={isControlled && !onChange && !onCheckedChange}
53
63
  className={cn(
54
64
  "peer h-5 w-5 shrink-0 appearance-none border border-border bg-input",
55
65
  "hover:border-border-hover",
@@ -59,7 +69,7 @@ export const Checkbox = forwardRef<HTMLInputElement, CheckboxProps>(
59
69
  "transition-colors duration-150",
60
70
  getStateStyles(state),
61
71
  className
62
- )} data-sonance-name="checkbox"
72
+ )}
63
73
  {...props}
64
74
  />
65
75
  <Check
@@ -72,19 +82,18 @@ export const Checkbox = forwardRef<HTMLInputElement, CheckboxProps>(
72
82
  {(label || description) && (
73
83
  <div className="flex flex-col gap-0.5">
74
84
  {label && (
75
- <label
76
- htmlFor={inputId}
77
- className="text-sm font-medium text-foreground cursor-pointer select-none"
85
+ <span
86
+ className="text-sm font-medium text-foreground select-none"
78
87
  >
79
88
  {label}
80
- </label>
89
+ </span>
81
90
  )}
82
91
  {description && (
83
92
  <p id="p-description" className="text-xs text-foreground-muted">{description}</p>
84
93
  )}
85
94
  </div>
86
95
  )}
87
- </div>
96
+ </label>
88
97
  );
89
98
  }
90
99
  );
@@ -2,7 +2,7 @@
2
2
 
3
3
  import React, { useState, useEffect, useCallback, useRef, useMemo } from "react";
4
4
  import { createPortal } from "react-dom";
5
- import { Palette, X, Copy, Check, RotateCcw, ChevronDown, Save, Loader2, AlertCircle, CheckCircle, Sun, Moon, Eye, EyeOff, Zap, Image as ImageIcon, Wand2, Scan, FileCode, Tag, Type, MousePointer, FormInput, Box, Search, Send, Sparkles, RefreshCw } from "lucide-react";
5
+ import { Palette, X, Copy, Check, RotateCcw, ChevronDown, Save, Loader2, AlertCircle, CheckCircle, Sun, Moon, Eye, EyeOff, Zap, Image as ImageIcon, Wand2, Scan, FileCode, Tag, Type, MousePointer, FormInput, Box, Search, Send, Sparkles, RefreshCw, GripHorizontal } from "lucide-react";
6
6
  import { useTheme } from "next-themes";
7
7
  import { cn } from "../../lib/utils";
8
8
  import {
@@ -118,6 +118,14 @@ export function SonanceDevTools() {
118
118
  // Track which elements were changed (for highlighting until accept/revert)
119
119
  const [changedElements, setChangedElements] = useState<VisionFocusedElement[]>([]);
120
120
 
121
+ // Drag state for movable panel
122
+ const DEVTOOLS_POSITION_KEY = "sonance-devtools-pos";
123
+ const DEFAULT_POSITION = { x: 0, y: 0 }; // Offset from default bottom-right position
124
+ const [dragPosition, setDragPosition] = useState(DEFAULT_POSITION);
125
+ const [isDragging, setIsDragging] = useState(false);
126
+ const dragOffsetRef = useRef({ x: 0, y: 0 });
127
+ const panelRef = useRef<HTMLDivElement>(null);
128
+
121
129
  // Component-specific style overrides (for scalable, project-agnostic styling)
122
130
  // Key: component type (e.g., "card", "button-primary", "card:variant123"), Value: style overrides
123
131
  const [componentOverrides, setComponentOverrides] = useState<Record<string, ComponentStyle>>({});
@@ -236,6 +244,93 @@ export function SonanceDevTools() {
236
244
  setMounted(true);
237
245
  }, []);
238
246
 
247
+ // Load drag position from localStorage on mount
248
+ useEffect(() => {
249
+ if (!mounted) return;
250
+ try {
251
+ const saved = localStorage.getItem(DEVTOOLS_POSITION_KEY);
252
+ if (saved) {
253
+ const parsed = JSON.parse(saved);
254
+ if (typeof parsed.x === "number" && typeof parsed.y === "number") {
255
+ setDragPosition(parsed);
256
+ }
257
+ }
258
+ } catch {
259
+ // Ignore parse errors
260
+ }
261
+ }, [mounted, DEVTOOLS_POSITION_KEY]);
262
+
263
+ // Drag handlers for movable panel
264
+ const headerRef = useRef<HTMLDivElement>(null);
265
+
266
+ const handleDragStart = useCallback((e: React.PointerEvent<HTMLDivElement>) => {
267
+ // Don't start drag if clicking on a button or interactive element
268
+ const target = e.target as HTMLElement;
269
+ if (target.closest('button')) return;
270
+
271
+ if (!panelRef.current || !headerRef.current) return;
272
+
273
+ // Prevent text selection during drag
274
+ e.preventDefault();
275
+
276
+ // Store the initial mouse position and current panel offset
277
+ dragOffsetRef.current = {
278
+ x: e.clientX - dragPosition.x,
279
+ y: e.clientY - dragPosition.y,
280
+ };
281
+
282
+ setIsDragging(true);
283
+ headerRef.current.setPointerCapture(e.pointerId);
284
+ }, [dragPosition]);
285
+
286
+ const handleDragMove = useCallback((e: React.PointerEvent<HTMLDivElement>) => {
287
+ if (!isDragging || !panelRef.current) return;
288
+
289
+ const viewportWidth = window.innerWidth;
290
+ const viewportHeight = window.innerHeight;
291
+ const panelWidth = panelRef.current.offsetWidth;
292
+ const panelHeight = panelRef.current.offsetHeight;
293
+
294
+ // Calculate new position directly from mouse movement
295
+ const newX = e.clientX - dragOffsetRef.current.x;
296
+ const newY = e.clientY - dragOffsetRef.current.y;
297
+
298
+ // Clamp to keep panel within viewport (with 24px padding)
299
+ const padding = 24;
300
+ const maxX = viewportWidth - panelWidth - padding;
301
+ const maxY = viewportHeight - panelHeight - padding;
302
+ const minX = -(viewportWidth - panelWidth - padding);
303
+ const minY = -(viewportHeight - panelHeight - padding);
304
+
305
+ setDragPosition({
306
+ x: Math.max(minX, Math.min(maxX, newX)),
307
+ y: Math.max(minY, Math.min(maxY, newY)),
308
+ });
309
+ }, [isDragging]);
310
+
311
+ const handleDragEnd = useCallback((e: React.PointerEvent<HTMLDivElement>) => {
312
+ if (!isDragging || !headerRef.current) return;
313
+
314
+ setIsDragging(false);
315
+ headerRef.current.releasePointerCapture(e.pointerId);
316
+
317
+ // Save position to localStorage
318
+ try {
319
+ localStorage.setItem(DEVTOOLS_POSITION_KEY, JSON.stringify(dragPosition));
320
+ } catch {
321
+ // Ignore storage errors
322
+ }
323
+ }, [isDragging, dragPosition, DEVTOOLS_POSITION_KEY]);
324
+
325
+ const handleResetPosition = useCallback(() => {
326
+ setDragPosition(DEFAULT_POSITION);
327
+ try {
328
+ localStorage.removeItem(DEVTOOLS_POSITION_KEY);
329
+ } catch {
330
+ // Ignore storage errors
331
+ }
332
+ }, [DEVTOOLS_POSITION_KEY]);
333
+
239
334
  // Inject/update component-specific preview styles whenever overrides change
240
335
  useEffect(() => {
241
336
  if (!mounted) return;
@@ -2172,8 +2267,9 @@ export function SonanceDevTools() {
2172
2267
  // Close and reset
2173
2268
  const handleClose = useCallback(() => {
2174
2269
  setIsOpen(false);
2175
- // Disable inspector so user can interact with the app
2270
+ // Disable inspector and vision mode so user can interact with the app
2176
2271
  setInspectorEnabled(false);
2272
+ setVisionModeActive(false);
2177
2273
  resetThemeFromDOM();
2178
2274
  // Reset color change tracking for next session
2179
2275
  setColorsExplicitlyChanged(false);
@@ -2201,19 +2297,37 @@ export function SonanceDevTools() {
2201
2297
 
2202
2298
  const panel = isOpen && (
2203
2299
  <div
2300
+ ref={panelRef}
2204
2301
  data-sonance-devtools="true"
2205
2302
  className={cn(
2206
2303
  "fixed bottom-6 right-6 z-[9999]",
2207
2304
  "w-[360px] max-h-[80vh]",
2208
2305
  "bg-white rounded-lg shadow-2xl border border-gray-200",
2209
2306
  "flex flex-col overflow-hidden",
2210
- "font-['Montserrat',sans-serif]"
2307
+ "font-['Montserrat',sans-serif]",
2308
+ isDragging && "select-none"
2211
2309
  )}
2212
- style={{ colorScheme: "light" }}
2310
+ style={{
2311
+ colorScheme: "light",
2312
+ transform: `translate(${dragPosition.x}px, ${dragPosition.y}px)`,
2313
+ }}
2213
2314
  >
2214
- {/* Header - Simplified */}
2215
- <div className="flex items-center justify-between px-4 py-3 border-b border-gray-200 bg-[#333F48]">
2315
+ {/* Header - Draggable */}
2316
+ <div
2317
+ ref={headerRef}
2318
+ className={cn(
2319
+ "flex items-center justify-between px-4 py-3 border-b border-gray-200 bg-[#333F48]",
2320
+ "cursor-move touch-none"
2321
+ )}
2322
+ onPointerDown={handleDragStart}
2323
+ onPointerMove={handleDragMove}
2324
+ onPointerUp={handleDragEnd}
2325
+ onPointerCancel={handleDragEnd}
2326
+ onDoubleClick={handleResetPosition}
2327
+ title="Drag to move • Double-click to reset position"
2328
+ >
2216
2329
  <div className="flex items-center gap-2">
2330
+ <GripHorizontal className="h-4 w-4 text-white/50" />
2217
2331
  <Palette className="h-5 w-5 text-[#00A3E1]" />
2218
2332
  <span id="span-sonance-devtools" className="text-sm font-semibold text-white">
2219
2333
  Sonance DevTools
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sonance-brand-mcp",
3
- "version": "1.3.20",
3
+ "version": "1.3.22",
4
4
  "description": "MCP Server for Sonance Brand Guidelines and Component Library - gives Claude instant access to brand colors, typography, and UI components.",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",