sonance-brand-mcp 1.3.57 → 1.3.59

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.
@@ -1,16 +1,148 @@
1
1
  "use client";
2
2
 
3
- import React, { useState, useEffect } from "react";
4
- import { X, Zap, Loader2, Check, Undo, ChevronDown, ChevronRight, FileCode, AlertTriangle, RefreshCw, Info } from "lucide-react";
3
+ import React, { useState, useEffect, useCallback } from "react";
4
+ import { X, Zap, Loader2, Check, ChevronDown, ChevronRight, FileCode, AlertTriangle, Info, Eye, EyeOff } from "lucide-react";
5
5
  import { cn } from "../../../lib/utils";
6
6
  import { ApplyFirstSession, ApplyFirstStatus, VisionFileModification } from "../types";
7
7
 
8
+ // CSS Injection Preview ID
9
+ const PREVIEW_STYLE_ID = "sonance-preview-css";
10
+
11
+ /**
12
+ * Extract className changes from a diff
13
+ * Returns: { removed: string[], added: string[] }
14
+ */
15
+ function extractClassNameChanges(diff: string): { removed: string[]; added: string[] } {
16
+ const removed: string[] = [];
17
+ const added: string[] = [];
18
+
19
+ // Regex to find className="..." patterns
20
+ const classNameRegex = /className=["']([^"']+)["']/g;
21
+
22
+ const lines = diff.split("\n");
23
+ for (const line of lines) {
24
+ if (line.startsWith("-") && !line.startsWith("---")) {
25
+ // Removed line - extract className
26
+ let match;
27
+ while ((match = classNameRegex.exec(line)) !== null) {
28
+ removed.push(...match[1].split(/\s+/));
29
+ }
30
+ } else if (line.startsWith("+") && !line.startsWith("+++")) {
31
+ // Added line - extract className
32
+ let match;
33
+ while ((match = classNameRegex.exec(line)) !== null) {
34
+ added.push(...match[1].split(/\s+/));
35
+ }
36
+ }
37
+ }
38
+
39
+ return { removed, added };
40
+ }
41
+
42
+ /**
43
+ * Common Tailwind class to CSS mapping for preview
44
+ * Only covers the most common styling classes for quick previews
45
+ */
46
+ const TAILWIND_TO_CSS: Record<string, string> = {
47
+ // Text colors
48
+ "text-white": "color: white !important;",
49
+ "text-black": "color: black !important;",
50
+ "text-gray-900": "color: rgb(17, 24, 39) !important;",
51
+ "text-gray-700": "color: rgb(55, 65, 81) !important;",
52
+ "text-gray-500": "color: rgb(107, 114, 128) !important;",
53
+
54
+ // Sonance brand colors
55
+ "text-sonance-charcoal": "color: #333F48 !important;",
56
+ "text-sonance-blue": "color: #00D3C8 !important;",
57
+ "border-sonance-charcoal": "border-color: #333F48 !important;",
58
+ "border-sonance-blue": "border-color: #00D3C8 !important;",
59
+ "bg-sonance-charcoal": "background-color: #333F48 !important;",
60
+ "bg-sonance-blue": "background-color: #00D3C8 !important;",
61
+
62
+ // Semantic colors
63
+ "text-primary": "color: var(--primary) !important;",
64
+ "text-primary-foreground": "color: var(--primary-foreground) !important;",
65
+ "text-accent": "color: var(--accent) !important;",
66
+ "text-accent-foreground": "color: var(--accent-foreground) !important;",
67
+ "bg-primary": "background-color: hsl(var(--primary)) !important;",
68
+ "bg-accent": "background-color: hsl(var(--accent)) !important;",
69
+
70
+ // Common backgrounds
71
+ "bg-white": "background-color: white !important;",
72
+ "bg-black": "background-color: black !important;",
73
+ "bg-gray-100": "background-color: rgb(243, 244, 246) !important;",
74
+ "bg-gray-200": "background-color: rgb(229, 231, 235) !important;",
75
+
76
+ // Borders
77
+ "border-gray-200": "border-color: rgb(229, 231, 235) !important;",
78
+ "border-gray-300": "border-color: rgb(209, 213, 219) !important;",
79
+ };
80
+
81
+ /**
82
+ * Generate CSS for preview based on class changes
83
+ */
84
+ function generatePreviewCSS(modifications: VisionFileModification[]): string {
85
+ const cssRules: string[] = [];
86
+
87
+ for (const mod of modifications) {
88
+ const { added } = extractClassNameChanges(mod.diff);
89
+
90
+ // Generate CSS for added classes
91
+ for (const cls of added) {
92
+ if (TAILWIND_TO_CSS[cls]) {
93
+ // We can't know the exact selector, so we apply to elements with data-sonance-preview
94
+ cssRules.push(TAILWIND_TO_CSS[cls]);
95
+ }
96
+ }
97
+ }
98
+
99
+ if (cssRules.length === 0) return "";
100
+
101
+ // Apply to changed elements (marked by InspectorOverlay)
102
+ return `
103
+ [data-sonance-changed="true"] {
104
+ ${cssRules.join("\n ")}
105
+ outline: 2px dashed #00D3C8 !important;
106
+ outline-offset: 2px !important;
107
+ }
108
+ `;
109
+ }
110
+
111
+ /**
112
+ * Inject preview CSS into the document
113
+ */
114
+ function injectPreviewCSS(css: string): void {
115
+ // Remove existing preview styles first
116
+ removePreviewCSS();
117
+
118
+ if (!css.trim()) return;
119
+
120
+ const style = document.createElement("style");
121
+ style.id = PREVIEW_STYLE_ID;
122
+ style.textContent = css;
123
+ document.head.appendChild(style);
124
+
125
+ console.log("[Preview CSS] Injected:", css.substring(0, 200));
126
+ }
127
+
128
+ /**
129
+ * Remove preview CSS from the document
130
+ */
131
+ function removePreviewCSS(): void {
132
+ const existing = document.getElementById(PREVIEW_STYLE_ID);
133
+ if (existing) {
134
+ existing.remove();
135
+ console.log("[Preview CSS] Removed");
136
+ }
137
+ }
138
+
8
139
  export interface ApplyFirstPreviewProps {
9
140
  session: ApplyFirstSession;
10
141
  status: ApplyFirstStatus;
11
142
  onAccept: () => void;
12
143
  onRevert: () => void;
13
144
  onForceClear?: () => void; // Force clear stale sessions
145
+ onApplyPreview?: () => void; // Apply previewed changes (for preview mode)
14
146
  }
15
147
 
16
148
  function FileModificationCard({
@@ -78,7 +210,7 @@ function HMRStatusBadge({ status }: { status: ApplyFirstStatus }) {
78
210
  if (status === "waiting-hmr") {
79
211
  return (
80
212
  <span className="flex items-center gap-1 text-[10px] px-1.5 py-0.5 rounded bg-amber-100 text-amber-700">
81
- <RefreshCw className="h-3 w-3 animate-spin" />
213
+ <Loader2 className="h-3 w-3 animate-spin" />
82
214
  Refreshing...
83
215
  </span>
84
216
  );
@@ -92,6 +224,15 @@ function HMRStatusBadge({ status }: { status: ApplyFirstStatus }) {
92
224
  </span>
93
225
  );
94
226
  }
227
+
228
+ if (status === "previewing") {
229
+ return (
230
+ <span className="flex items-center gap-1 text-[10px] px-1.5 py-0.5 rounded bg-blue-100 text-blue-700">
231
+ <Eye className="h-3 w-3" />
232
+ Preview Mode
233
+ </span>
234
+ );
235
+ }
95
236
 
96
237
  return null;
97
238
  }
@@ -102,8 +243,10 @@ export function ApplyFirstPreview({
102
243
  onAccept,
103
244
  onRevert,
104
245
  onForceClear,
246
+ onApplyPreview,
105
247
  }: ApplyFirstPreviewProps) {
106
248
  const [expandedFiles, setExpandedFiles] = useState<Set<string>>(new Set());
249
+ const [cssPreviewEnabled, setCssPreviewEnabled] = useState(true);
107
250
 
108
251
  // Expand first file by default
109
252
  useEffect(() => {
@@ -111,6 +254,32 @@ export function ApplyFirstPreview({
111
254
  setExpandedFiles(new Set([session.modifications[0].filePath]));
112
255
  }
113
256
  }, [session]);
257
+
258
+ // CSS Preview injection for preview mode
259
+ useEffect(() => {
260
+ const isPreviewMode = session.isPreview === true;
261
+
262
+ if (isPreviewMode && cssPreviewEnabled) {
263
+ // Generate and inject preview CSS
264
+ const previewCSS = generatePreviewCSS(session.modifications);
265
+ if (previewCSS) {
266
+ injectPreviewCSS(previewCSS);
267
+ }
268
+ } else {
269
+ // Remove preview CSS when not in preview mode or disabled
270
+ removePreviewCSS();
271
+ }
272
+
273
+ // Cleanup on unmount
274
+ return () => {
275
+ removePreviewCSS();
276
+ };
277
+ }, [session, cssPreviewEnabled]);
278
+
279
+ // Toggle CSS preview
280
+ const toggleCssPreview = useCallback(() => {
281
+ setCssPreviewEnabled(prev => !prev);
282
+ }, []);
114
283
 
115
284
  const toggleFile = (filePath: string) => {
116
285
  setExpandedFiles((prev) => {
@@ -125,26 +294,55 @@ export function ApplyFirstPreview({
125
294
  };
126
295
 
127
296
  const fileCount = session.modifications.length;
128
- const isLoading = status === "accepting" || status === "reverting";
297
+ const isLoading = status === "accepting" || status === "reverting" || status === "applying";
129
298
  const isStaleSession = fileCount === 0 || status === "error";
299
+ const isPreviewMode = session.isPreview === true;
130
300
 
131
301
  return (
132
- <div className="space-y-3 p-3 rounded border border-green-300 bg-green-50">
302
+ <div className={cn(
303
+ "space-y-3 p-3 rounded border",
304
+ isPreviewMode
305
+ ? "border-blue-300 bg-blue-50"
306
+ : "border-green-300 bg-green-50"
307
+ )}>
133
308
  {/* Header */}
134
309
  <div className="flex items-center justify-between">
135
310
  <div className="flex items-center gap-2">
136
- <Zap className="h-4 w-4 text-green-600" />
311
+ <Zap className={cn("h-4 w-4", isPreviewMode ? "text-blue-600" : "text-green-600")} />
137
312
  <span className="text-xs font-semibold text-gray-900">
138
- Changes Applied
313
+ {isPreviewMode ? "Proposed Changes" : "Changes Applied"}
139
314
  </span>
140
- <span className="text-[10px] px-1.5 py-0.5 rounded bg-green-200 text-green-700 font-medium">
315
+ <span className={cn(
316
+ "text-[10px] px-1.5 py-0.5 rounded font-medium",
317
+ isPreviewMode
318
+ ? "bg-blue-200 text-blue-700"
319
+ : "bg-green-200 text-green-700"
320
+ )}>
141
321
  {fileCount} file{fileCount !== 1 ? "s" : ""}
142
322
  </span>
143
323
  </div>
144
- <HMRStatusBadge status={status} />
324
+ <div className="flex items-center gap-2">
325
+ {/* CSS Preview Toggle (only in preview mode) */}
326
+ {isPreviewMode && (
327
+ <button
328
+ onClick={toggleCssPreview}
329
+ className={cn(
330
+ "flex items-center gap-1 text-[10px] px-1.5 py-0.5 rounded transition-colors",
331
+ cssPreviewEnabled
332
+ ? "bg-blue-200 text-blue-700"
333
+ : "bg-gray-200 text-gray-500"
334
+ )}
335
+ title={cssPreviewEnabled ? "Hide CSS preview" : "Show CSS preview"}
336
+ >
337
+ {cssPreviewEnabled ? <Eye className="h-3 w-3" /> : <EyeOff className="h-3 w-3" />}
338
+ CSS
339
+ </button>
340
+ )}
341
+ <HMRStatusBadge status={status} />
342
+ </div>
145
343
  </div>
146
344
 
147
- {/* Info Banner - show different message for stale sessions */}
345
+ {/* Info Banner - different messages for preview vs applied vs stale */}
148
346
  {isStaleSession ? (
149
347
  <div className="flex items-start gap-2 p-2 rounded bg-amber-50 border border-amber-200">
150
348
  <AlertTriangle className="h-3.5 w-3.5 text-amber-600 mt-0.5 flex-shrink-0" />
@@ -153,6 +351,14 @@ export function ApplyFirstPreview({
153
351
  Use &quot;Force Clear&quot; to dismiss this panel.
154
352
  </span>
155
353
  </div>
354
+ ) : isPreviewMode ? (
355
+ <div className="flex items-start gap-2 p-2 rounded bg-blue-50 border border-blue-200">
356
+ <Info className="h-3.5 w-3.5 text-blue-600 mt-0.5 flex-shrink-0" />
357
+ <span className="text-xs text-blue-700">
358
+ <strong>Review the changes below.</strong> Click Accept to apply them to your files,
359
+ or Reject to discard.
360
+ </span>
361
+ </div>
156
362
  ) : (
157
363
  <div className="flex items-start gap-2 p-2 rounded bg-blue-50 border border-blue-200">
158
364
  <Info className="h-3.5 w-3.5 text-blue-600 mt-0.5 flex-shrink-0" />
@@ -175,43 +381,47 @@ export function ApplyFirstPreview({
175
381
  ))}
176
382
  </div>
177
383
 
178
- {/* Warning about navigation */}
179
- <div className="flex items-start gap-2 p-2 rounded bg-amber-50 border border-amber-200">
180
- <AlertTriangle className="h-3.5 w-3.5 text-amber-600 mt-0.5 flex-shrink-0" />
181
- <span className="text-xs text-amber-700">
182
- Navigating away will automatically revert changes. Make sure to accept or revert first.
183
- </span>
184
- </div>
384
+ {/* Warning about navigation - only show for applied mode */}
385
+ {!isPreviewMode && (
386
+ <div className="flex items-start gap-2 p-2 rounded bg-amber-50 border border-amber-200">
387
+ <AlertTriangle className="h-3.5 w-3.5 text-amber-600 mt-0.5 flex-shrink-0" />
388
+ <span className="text-xs text-amber-700">
389
+ Navigating away will automatically revert changes. Make sure to accept or revert first.
390
+ </span>
391
+ </div>
392
+ )}
185
393
 
186
394
  {/* Action Buttons */}
187
395
  <div className="flex gap-2">
188
- {/* Accept Button - hide for stale sessions */}
396
+ {/* Accept/Apply Button - behavior depends on mode */}
189
397
  {!isStaleSession && (
190
398
  <button
191
- onClick={onAccept}
399
+ onClick={isPreviewMode && onApplyPreview ? onApplyPreview : onAccept}
192
400
  disabled={isLoading}
193
401
  className={cn(
194
402
  "flex-1 flex items-center justify-center gap-2 py-2.5",
195
403
  "text-xs font-medium rounded transition-colors",
196
- "bg-green-600 text-white hover:bg-green-700",
404
+ isPreviewMode
405
+ ? "bg-blue-600 text-white hover:bg-blue-700"
406
+ : "bg-green-600 text-white hover:bg-green-700",
197
407
  "disabled:opacity-50 disabled:cursor-not-allowed"
198
408
  )}
199
409
  >
200
- {status === "accepting" ? (
410
+ {status === "accepting" || status === "applying" ? (
201
411
  <>
202
412
  <Loader2 className="h-3.5 w-3.5 animate-spin" />
203
- Accepting...
413
+ {isPreviewMode ? "Applying..." : "Accepting..."}
204
414
  </>
205
415
  ) : (
206
416
  <>
207
417
  <Check className="h-3.5 w-3.5" />
208
- Keep Changes
418
+ {isPreviewMode ? "Accept & Apply" : "Keep Changes"}
209
419
  </>
210
420
  )}
211
421
  </button>
212
422
  )}
213
423
 
214
- {/* Revert Button */}
424
+ {/* Reject/Revert Button */}
215
425
  <button
216
426
  onClick={onRevert}
217
427
  disabled={isLoading}
@@ -225,12 +435,12 @@ export function ApplyFirstPreview({
225
435
  {status === "reverting" ? (
226
436
  <>
227
437
  <Loader2 className="h-3.5 w-3.5 animate-spin" />
228
- Reverting...
438
+ {isPreviewMode ? "Discarding..." : "Reverting..."}
229
439
  </>
230
440
  ) : (
231
441
  <>
232
- <Undo className="h-3.5 w-3.5" />
233
- Revert
442
+ <X className="h-3.5 w-3.5" />
443
+ {isPreviewMode ? "Reject" : "Revert"}
234
444
  </>
235
445
  )}
236
446
  </button>
@@ -255,7 +465,7 @@ export function ApplyFirstPreview({
255
465
 
256
466
  {/* Session Info (for debugging) */}
257
467
  <div className="text-[10px] text-gray-400 font-mono">
258
- Session: {session.sessionId} | Applied: {new Date(session.appliedAt).toLocaleTimeString()}
468
+ {isPreviewMode ? "Preview" : "Session"}: {session.sessionId} | {isPreviewMode ? "Generated" : "Applied"}: {new Date(session.appliedAt).toLocaleTimeString()}
259
469
  </div>
260
470
  </div>
261
471
  );
@@ -144,6 +144,7 @@ export function ChatInterface({
144
144
  method: "POST",
145
145
  headers: { "Content-Type": "application/json" },
146
146
  body: JSON.stringify({
147
+ // Apply-First: write files immediately so HMR shows changes
147
148
  action: useApplyFirst ? "apply" : "edit",
148
149
  screenshot,
149
150
  pageRoute: window.location.pathname,
@@ -176,7 +177,7 @@ export function ChatInterface({
176
177
 
177
178
  if (data.success && data.modifications) {
178
179
  if (useApplyFirst && onApplyFirstComplete) {
179
- // Apply-First mode: files are already written
180
+ // Apply-First mode: files are already written, user can see changes via HMR
180
181
  console.log("[Apply-First] Calling onApplyFirstComplete with:", {
181
182
  sessionId: data.sessionId,
182
183
  modifications: data.modifications.map((m: { filePath: string }) => m.filePath),
@@ -50,13 +50,14 @@ export interface ComponentsPanelProps {
50
50
  visionPendingEdit?: VisionPendingEdit | null;
51
51
  onSaveVisionEdit?: () => void;
52
52
  onClearVisionPendingEdit?: () => void;
53
- // Apply-First Mode props
53
+ // Apply-First Mode props (now Preview-First for Cursor-style flow)
54
54
  applyFirstSession?: ApplyFirstSession | null;
55
55
  applyFirstStatus?: ApplyFirstStatus;
56
56
  onApplyFirstComplete?: (session: ApplyFirstSession) => void;
57
57
  onApplyFirstAccept?: () => void;
58
58
  onApplyFirstRevert?: () => void;
59
59
  onApplyFirstForceClear?: () => void;
60
+ onApplyPreview?: () => void; // Apply previewed modifications to files
60
61
  }
61
62
 
62
63
  export function ComponentsPanel({
@@ -97,6 +98,7 @@ export function ComponentsPanel({
97
98
  onApplyFirstAccept,
98
99
  onApplyFirstRevert,
99
100
  onApplyFirstForceClear,
101
+ onApplyPreview,
100
102
  }: ComponentsPanelProps) {
101
103
  // Auto-activate inspector when entering this tab
102
104
  useEffect(() => {
@@ -573,7 +575,7 @@ export function ComponentsPanel({
573
575
  />
574
576
  )}
575
577
 
576
- {/* Apply-First Mode Preview - NEW: Changes already applied */}
578
+ {/* Preview-First Mode - Shows diff, user accepts/rejects before applying */}
577
579
  {applyFirstSession && onApplyFirstAccept && onApplyFirstRevert && (
578
580
  <ApplyFirstPreview
579
581
  session={applyFirstSession}
@@ -581,6 +583,7 @@ export function ComponentsPanel({
581
583
  onAccept={onApplyFirstAccept}
582
584
  onRevert={onApplyFirstRevert}
583
585
  onForceClear={onApplyFirstForceClear}
586
+ onApplyPreview={onApplyPreview}
584
587
  />
585
588
  )}
586
589
 
@@ -255,28 +255,31 @@ export interface VisionPendingEdit {
255
255
  explanation: string;
256
256
  }
257
257
 
258
- // ---- Apply-First Mode Types (Cursor-style instant preview) ----
258
+ // ---- Preview-First Mode Types (Cursor-style) ----
259
+ // Flow: LLM generates patches -> Show diff preview -> User accepts/rejects -> Apply to files
259
260
 
260
261
  export interface ApplyFirstSession {
261
262
  sessionId: string;
262
263
  modifications: VisionFileModification[];
263
264
  appliedAt: number;
264
- status: 'applied' | 'accepted' | 'reverted';
265
+ status: 'preview' | 'applied' | 'accepted' | 'reverted';
265
266
  backupPaths: string[];
267
+ isPreview?: boolean; // True if changes are not yet written to disk
266
268
  }
267
269
 
268
270
  export type ApplyFirstStatus =
269
271
  | 'idle' // No active session
270
272
  | 'generating' // AI is generating changes
273
+ | 'previewing' // User is reviewing proposed changes (not yet applied)
271
274
  | 'applying' // Writing files to disk
272
275
  | 'waiting-hmr' // Waiting for HMR to show changes
273
- | 'reviewing' // User is reviewing actual changes
276
+ | 'reviewing' // User is reviewing actual changes (files written)
274
277
  | 'accepting' // User clicked accept, cleaning up backups
275
278
  | 'reverting' // User clicked revert, restoring backups
276
279
  | 'error'; // Something went wrong
277
280
 
278
281
  export interface ApplyFirstRequest {
279
- action: "apply" | "accept" | "revert";
282
+ action: "apply" | "preview" | "accept" | "revert";
280
283
  sessionId?: string;
281
284
  screenshot?: string;
282
285
  pageRoute?: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sonance-brand-mcp",
3
- "version": "1.3.57",
3
+ "version": "1.3.59",
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",