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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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.
|
|
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
|
|
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
|
-
<
|
|
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
|
|
51
|
-
: { defaultChecked: defaultChecked
|
|
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
|
-
)}
|
|
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
|
-
<
|
|
76
|
-
|
|
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
|
-
</
|
|
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
|
-
</
|
|
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={{
|
|
2310
|
+
style={{
|
|
2311
|
+
colorScheme: "light",
|
|
2312
|
+
transform: `translate(${dragPosition.x}px, ${dragPosition.y}px)`,
|
|
2313
|
+
}}
|
|
2213
2314
|
>
|
|
2214
|
-
{/* Header -
|
|
2215
|
-
<div
|
|
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.
|
|
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",
|