sonance-brand-mcp 1.3.19 → 1.3.21

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.
@@ -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/dist/index.js CHANGED
@@ -356,6 +356,12 @@ function runDevToolsInstaller() {
356
356
  const apiAssetsDir = path.join(targetDir, baseDir, "app/api/sonance-assets");
357
357
  const apiInjectIdDir = path.join(targetDir, baseDir, "app/api/sonance-inject-id");
358
358
  const apiAnalyzeDir = path.join(targetDir, baseDir, "app/api/sonance-analyze");
359
+ const apiVisionApplyDir = path.join(targetDir, baseDir, "app/api/sonance-vision-apply");
360
+ const apiVisionEditDir = path.join(targetDir, baseDir, "app/api/sonance-vision-edit");
361
+ const apiAiEditDir = path.join(targetDir, baseDir, "app/api/sonance-ai-edit");
362
+ const apiSaveColorsDir = path.join(targetDir, baseDir, "app/api/sonance-save-colors");
363
+ const apiComponentSourceDir = path.join(targetDir, baseDir, "app/api/sonance-component-source");
364
+ const apiFindComponentDir = path.join(targetDir, baseDir, "app/api/sonance-find-component");
359
365
  const themeDir = path.join(targetDir, baseDir, "theme");
360
366
  // Source resolution
361
367
  let sourceBrandSystem;
@@ -369,6 +375,12 @@ function runDevToolsInstaller() {
369
375
  let sourceApiAssets;
370
376
  let sourceApiInjectId;
371
377
  let sourceApiAnalyze;
378
+ let sourceApiVisionApply;
379
+ let sourceApiVisionEdit;
380
+ let sourceApiAiEdit;
381
+ let sourceApiSaveColors;
382
+ let sourceApiComponentSource;
383
+ let sourceApiFindComponent;
372
384
  if (IS_BUNDLED) {
373
385
  sourceBrandSystem = path.join(BUNDLED_ASSETS, "brand-system.ts");
374
386
  sourceBrandContext = path.join(BUNDLED_ASSETS, "brand-context.tsx");
@@ -381,6 +393,12 @@ function runDevToolsInstaller() {
381
393
  sourceApiAssets = path.join(BUNDLED_ASSETS, "api/sonance-assets/route.ts");
382
394
  sourceApiInjectId = path.join(BUNDLED_ASSETS, "api/sonance-inject-id/route.ts");
383
395
  sourceApiAnalyze = path.join(BUNDLED_ASSETS, "api/sonance-analyze/route.ts");
396
+ sourceApiVisionApply = path.join(BUNDLED_ASSETS, "api/sonance-vision-apply/route.ts");
397
+ sourceApiVisionEdit = path.join(BUNDLED_ASSETS, "api/sonance-vision-edit/route.ts");
398
+ sourceApiAiEdit = path.join(BUNDLED_ASSETS, "api/sonance-ai-edit/route.ts");
399
+ sourceApiSaveColors = path.join(BUNDLED_ASSETS, "api/sonance-save-colors/route.ts");
400
+ sourceApiComponentSource = path.join(BUNDLED_ASSETS, "api/sonance-component-source/route.ts");
401
+ sourceApiFindComponent = path.join(BUNDLED_ASSETS, "api/sonance-find-component/route.ts");
384
402
  }
385
403
  else {
386
404
  sourceBrandSystem = path.join(DEV_PROJECT_ROOT, "src/lib/brand-system.ts");
@@ -394,6 +412,12 @@ function runDevToolsInstaller() {
394
412
  sourceApiAssets = path.join(DEV_PROJECT_ROOT, "src/app/api/sonance-assets/route.ts");
395
413
  sourceApiInjectId = path.join(DEV_PROJECT_ROOT, "src/app/api/sonance-inject-id/route.ts");
396
414
  sourceApiAnalyze = path.join(DEV_PROJECT_ROOT, "src/app/api/sonance-analyze/route.ts");
415
+ sourceApiVisionApply = path.join(DEV_PROJECT_ROOT, "src/app/api/sonance-vision-apply/route.ts");
416
+ sourceApiVisionEdit = path.join(DEV_PROJECT_ROOT, "src/app/api/sonance-vision-edit/route.ts");
417
+ sourceApiAiEdit = path.join(DEV_PROJECT_ROOT, "src/app/api/sonance-ai-edit/route.ts");
418
+ sourceApiSaveColors = path.join(DEV_PROJECT_ROOT, "src/app/api/sonance-save-colors/route.ts");
419
+ sourceApiComponentSource = path.join(DEV_PROJECT_ROOT, "src/app/api/sonance-component-source/route.ts");
420
+ sourceApiFindComponent = path.join(DEV_PROJECT_ROOT, "src/app/api/sonance-find-component/route.ts");
397
421
  }
398
422
  // Verify sources exist
399
423
  if (!fs.existsSync(sourceBrandSystem)) {
@@ -530,6 +554,49 @@ function runDevToolsInstaller() {
530
554
  fs.copyFileSync(sourceApiAnalyze, path.join(apiAnalyzeDir, "route.ts"));
531
555
  createdFiles.push(`${pathPrefix}app/api/sonance-analyze/route.ts`);
532
556
  console.log(` ✓ Created ${pathPrefix}app/api/sonance-analyze/route.ts`);
557
+ // 10. Install Vision and AI API routes
558
+ if (!fs.existsSync(apiVisionApplyDir)) {
559
+ fs.mkdirSync(apiVisionApplyDir, { recursive: true });
560
+ }
561
+ createdDirectories.push(`${pathPrefix}app/api/sonance-vision-apply`);
562
+ fs.copyFileSync(sourceApiVisionApply, path.join(apiVisionApplyDir, "route.ts"));
563
+ createdFiles.push(`${pathPrefix}app/api/sonance-vision-apply/route.ts`);
564
+ console.log(` ✓ Created ${pathPrefix}app/api/sonance-vision-apply/route.ts`);
565
+ if (!fs.existsSync(apiVisionEditDir)) {
566
+ fs.mkdirSync(apiVisionEditDir, { recursive: true });
567
+ }
568
+ createdDirectories.push(`${pathPrefix}app/api/sonance-vision-edit`);
569
+ fs.copyFileSync(sourceApiVisionEdit, path.join(apiVisionEditDir, "route.ts"));
570
+ createdFiles.push(`${pathPrefix}app/api/sonance-vision-edit/route.ts`);
571
+ console.log(` ✓ Created ${pathPrefix}app/api/sonance-vision-edit/route.ts`);
572
+ if (!fs.existsSync(apiAiEditDir)) {
573
+ fs.mkdirSync(apiAiEditDir, { recursive: true });
574
+ }
575
+ createdDirectories.push(`${pathPrefix}app/api/sonance-ai-edit`);
576
+ fs.copyFileSync(sourceApiAiEdit, path.join(apiAiEditDir, "route.ts"));
577
+ createdFiles.push(`${pathPrefix}app/api/sonance-ai-edit/route.ts`);
578
+ console.log(` ✓ Created ${pathPrefix}app/api/sonance-ai-edit/route.ts`);
579
+ if (!fs.existsSync(apiSaveColorsDir)) {
580
+ fs.mkdirSync(apiSaveColorsDir, { recursive: true });
581
+ }
582
+ createdDirectories.push(`${pathPrefix}app/api/sonance-save-colors`);
583
+ fs.copyFileSync(sourceApiSaveColors, path.join(apiSaveColorsDir, "route.ts"));
584
+ createdFiles.push(`${pathPrefix}app/api/sonance-save-colors/route.ts`);
585
+ console.log(` ✓ Created ${pathPrefix}app/api/sonance-save-colors/route.ts`);
586
+ if (!fs.existsSync(apiComponentSourceDir)) {
587
+ fs.mkdirSync(apiComponentSourceDir, { recursive: true });
588
+ }
589
+ createdDirectories.push(`${pathPrefix}app/api/sonance-component-source`);
590
+ fs.copyFileSync(sourceApiComponentSource, path.join(apiComponentSourceDir, "route.ts"));
591
+ createdFiles.push(`${pathPrefix}app/api/sonance-component-source/route.ts`);
592
+ console.log(` ✓ Created ${pathPrefix}app/api/sonance-component-source/route.ts`);
593
+ if (!fs.existsSync(apiFindComponentDir)) {
594
+ fs.mkdirSync(apiFindComponentDir, { recursive: true });
595
+ }
596
+ createdDirectories.push(`${pathPrefix}app/api/sonance-find-component`);
597
+ fs.copyFileSync(sourceApiFindComponent, path.join(apiFindComponentDir, "route.ts"));
598
+ createdFiles.push(`${pathPrefix}app/api/sonance-find-component/route.ts`);
599
+ console.log(` ✓ Created ${pathPrefix}app/api/sonance-find-component/route.ts`);
533
600
  // 11. Install brand-overrides.css for production logo sizing
534
601
  if (!fs.existsSync(stylesDir)) {
535
602
  fs.mkdirSync(stylesDir, { recursive: true });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sonance-brand-mcp",
3
- "version": "1.3.19",
3
+ "version": "1.3.21",
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",