sonance-brand-mcp 1.3.111 → 1.3.113

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.
Files changed (79) hide show
  1. package/dist/assets/api/sonance-save-image/route.ts +625 -0
  2. package/dist/assets/api/sonance-vision-apply/image-styling-detection.ts +1360 -0
  3. package/dist/assets/api/sonance-vision-apply/route.ts +988 -57
  4. package/dist/assets/api/sonance-vision-apply/styling-detection.ts +730 -0
  5. package/dist/assets/api/sonance-vision-apply/theme-discovery.ts +1 -1
  6. package/dist/assets/brand-system.ts +13 -12
  7. package/dist/assets/components/accordion.tsx +15 -7
  8. package/dist/assets/components/alert-dialog.tsx +35 -10
  9. package/dist/assets/components/alert.tsx +11 -10
  10. package/dist/assets/components/avatar.tsx +4 -4
  11. package/dist/assets/components/badge.tsx +16 -12
  12. package/dist/assets/components/button.stories.tsx +3 -3
  13. package/dist/assets/components/button.tsx +50 -31
  14. package/dist/assets/components/calendar.tsx +12 -8
  15. package/dist/assets/components/card.tsx +35 -29
  16. package/dist/assets/components/checkbox.tsx +9 -8
  17. package/dist/assets/components/code.tsx +19 -11
  18. package/dist/assets/components/command.tsx +32 -13
  19. package/dist/assets/components/context-menu.tsx +37 -16
  20. package/dist/assets/components/dialog.tsx +8 -5
  21. package/dist/assets/components/divider.tsx +15 -5
  22. package/dist/assets/components/drawer.tsx +4 -3
  23. package/dist/assets/components/dropdown-menu.tsx +15 -13
  24. package/dist/assets/components/hover-card.tsx +4 -1
  25. package/dist/assets/components/image.tsx +1 -1
  26. package/dist/assets/components/input.tsx +29 -14
  27. package/dist/assets/components/kbd.stories.tsx +3 -3
  28. package/dist/assets/components/kbd.tsx +29 -13
  29. package/dist/assets/components/listbox.tsx +8 -8
  30. package/dist/assets/components/menubar.tsx +50 -23
  31. package/dist/assets/components/navbar.stories.tsx +140 -13
  32. package/dist/assets/components/navbar.tsx +22 -5
  33. package/dist/assets/components/navigation-menu.tsx +28 -6
  34. package/dist/assets/components/pagination.tsx +10 -10
  35. package/dist/assets/components/popover.tsx +10 -8
  36. package/dist/assets/components/progress.tsx +6 -4
  37. package/dist/assets/components/radio-group.tsx +5 -5
  38. package/dist/assets/components/select.tsx +49 -29
  39. package/dist/assets/components/separator.tsx +3 -3
  40. package/dist/assets/components/sheet.tsx +4 -4
  41. package/dist/assets/components/sidebar.tsx +10 -10
  42. package/dist/assets/components/skeleton.tsx +13 -5
  43. package/dist/assets/components/slider.tsx +12 -10
  44. package/dist/assets/components/switch.tsx +4 -4
  45. package/dist/assets/components/table.tsx +5 -5
  46. package/dist/assets/components/tabs.tsx +8 -8
  47. package/dist/assets/components/textarea.tsx +11 -9
  48. package/dist/assets/components/toast.tsx +7 -7
  49. package/dist/assets/components/toggle.tsx +27 -7
  50. package/dist/assets/components/tooltip.tsx +10 -8
  51. package/dist/assets/components/user.tsx +8 -6
  52. package/dist/assets/dev-tools/SonanceDevTools.tsx +429 -362
  53. package/dist/assets/dev-tools/components/ApplyFirstPreview.tsx +10 -10
  54. package/dist/assets/dev-tools/components/ChatHistory.tsx +11 -7
  55. package/dist/assets/dev-tools/components/ChatInterface.tsx +61 -20
  56. package/dist/assets/dev-tools/components/ChatTabBar.tsx +1 -1
  57. package/dist/assets/dev-tools/components/DiffPreview.tsx +1 -1
  58. package/dist/assets/dev-tools/components/InlineDiffPreview.tsx +360 -36
  59. package/dist/assets/dev-tools/components/InspectorOverlay.tsx +9 -9
  60. package/dist/assets/dev-tools/components/PropertiesPanel.tsx +743 -93
  61. package/dist/assets/dev-tools/components/ScreenshotAnnotator.tsx +1 -1
  62. package/dist/assets/dev-tools/components/SectionHighlight.tsx +1 -1
  63. package/dist/assets/dev-tools/components/VisionDiffPreview.tsx +7 -7
  64. package/dist/assets/dev-tools/components/VisionModeBorder.tsx +4 -64
  65. package/dist/assets/dev-tools/hooks/index.ts +69 -0
  66. package/dist/assets/dev-tools/hooks/useComponentDetection.ts +132 -0
  67. package/dist/assets/dev-tools/hooks/useComputedStyles.ts +171 -65
  68. package/dist/assets/dev-tools/hooks/useContentHash.ts +212 -0
  69. package/dist/assets/dev-tools/hooks/useElementScanner.ts +398 -0
  70. package/dist/assets/dev-tools/hooks/useImageDetection.ts +162 -0
  71. package/dist/assets/dev-tools/hooks/useTextDetection.ts +217 -0
  72. package/dist/assets/dev-tools/panels/ComponentsPanel.tsx +160 -57
  73. package/dist/assets/dev-tools/panels/TextPanel.tsx +10 -10
  74. package/dist/assets/dev-tools/types.ts +42 -0
  75. package/dist/assets/globals.css +225 -9
  76. package/dist/assets/styles/brand-overrides.css +3 -2
  77. package/dist/assets/utils.ts +2 -1
  78. package/dist/index.js +32 -1
  79. package/package.json +1 -1
@@ -1,34 +1,206 @@
1
1
  "use client";
2
2
 
3
- import React, { useState } from "react";
3
+ import React, { useState, useMemo } from "react";
4
4
  import {
5
5
  ChevronDown,
6
6
  ChevronRight,
7
7
  Check,
8
8
  RotateCcw,
9
9
  FileCode,
10
- Clock,
11
10
  CheckCircle,
12
11
  XCircle,
12
+ Loader2,
13
+ AlertTriangle,
14
+ Info,
15
+ RefreshCw,
16
+ Plus,
17
+ Minus,
18
+ Code,
13
19
  } from "lucide-react";
14
20
  import { cn } from "../../../lib/utils";
15
- import { ChatMessageAction, ChatFileChange } from "../types";
21
+ import { ChatMessageAction, ApplyFirstStatus } from "../types";
16
22
 
17
23
  export interface InlineDiffPreviewProps {
18
24
  action: ChatMessageAction;
19
25
  onAccept?: () => void;
20
26
  onRevert?: () => void;
27
+ // Live status from parent (for HMR indicator, loading states)
28
+ liveStatus?: ApplyFirstStatus;
29
+ isProcessing?: boolean;
30
+ }
31
+
32
+ // Helper to refresh the page to see component changes
33
+ function handleRefreshPage() {
34
+ // Session is already persisted to localStorage, so it will survive the reload
35
+ window.location.reload();
36
+ }
37
+
38
+ /**
39
+ * Extract meaningful changes from a diff string
40
+ * Instead of showing full lines, extract what was actually modified
41
+ */
42
+ interface ChangeSummary {
43
+ type: 'added' | 'removed' | 'modified';
44
+ element?: string; // e.g., "Card", "Button"
45
+ change: string; // e.g., "rounded-none", "bg-blue-500"
46
+ fullLine?: string; // Original full line for tooltip
47
+ }
48
+
49
+ function extractChangeSummary(diff: string): ChangeSummary[] {
50
+ const lines = diff.split('\n');
51
+ const changes: ChangeSummary[] = [];
52
+
53
+ // Group consecutive - and + lines for comparison
54
+ const removedLines: string[] = [];
55
+ const addedLines: string[] = [];
56
+
57
+ for (const line of lines) {
58
+ if (line.startsWith('-') && !line.startsWith('---')) {
59
+ removedLines.push(line.substring(1).trim());
60
+ } else if (line.startsWith('+') && !line.startsWith('+++')) {
61
+ addedLines.push(line.substring(1).trim());
62
+ }
63
+ }
64
+
65
+ // If we have both removed and added lines, find the differences
66
+ if (removedLines.length > 0 && addedLines.length > 0) {
67
+ for (let i = 0; i < Math.max(removedLines.length, addedLines.length); i++) {
68
+ const removed = removedLines[i] || '';
69
+ const added = addedLines[i] || '';
70
+
71
+ if (removed && added) {
72
+ // Find what changed between the lines
73
+ const diff = findLineDifference(removed, added);
74
+ if (diff) {
75
+ changes.push({
76
+ type: 'modified',
77
+ element: diff.element,
78
+ change: diff.change,
79
+ fullLine: added
80
+ });
81
+ }
82
+ } else if (added && !removed) {
83
+ changes.push({
84
+ type: 'added',
85
+ change: truncateChange(added),
86
+ fullLine: added
87
+ });
88
+ } else if (removed && !added) {
89
+ changes.push({
90
+ type: 'removed',
91
+ change: truncateChange(removed),
92
+ fullLine: removed
93
+ });
94
+ }
95
+ }
96
+ } else if (addedLines.length > 0) {
97
+ // Only additions
98
+ for (const line of addedLines) {
99
+ changes.push({
100
+ type: 'added',
101
+ change: truncateChange(line),
102
+ fullLine: line
103
+ });
104
+ }
105
+ } else if (removedLines.length > 0) {
106
+ // Only removals
107
+ for (const line of removedLines) {
108
+ changes.push({
109
+ type: 'removed',
110
+ change: truncateChange(line),
111
+ fullLine: line
112
+ });
113
+ }
114
+ }
115
+
116
+ return changes;
117
+ }
118
+
119
+ /**
120
+ * Find the actual difference between two similar lines
121
+ */
122
+ function findLineDifference(removed: string, added: string): { element?: string; change: string } | null {
123
+ // Try to find className differences
124
+ const classNameRegex = /className=["']([^"']+)["']/;
125
+ const removedClass = removed.match(classNameRegex);
126
+ const addedClass = added.match(classNameRegex);
127
+
128
+ if (removedClass && addedClass) {
129
+ const removedClasses = new Set(removedClass[1].split(/\s+/));
130
+ const addedClasses = new Set(addedClass[1].split(/\s+/));
131
+
132
+ // Find added classes
133
+ const newClasses = [...addedClasses].filter(c => !removedClasses.has(c));
134
+ // Find removed classes
135
+ const deletedClasses = [...removedClasses].filter(c => !addedClasses.has(c));
136
+
137
+ if (newClasses.length > 0 || deletedClasses.length > 0) {
138
+ // Try to extract element name (e.g., <Card, <Button)
139
+ const elementMatch = added.match(/<(\w+)/);
140
+ const element = elementMatch ? elementMatch[1] : undefined;
141
+
142
+ const changes: string[] = [];
143
+ if (newClasses.length > 0) {
144
+ changes.push(`+${newClasses.join(' ')}`);
145
+ }
146
+ if (deletedClasses.length > 0) {
147
+ changes.push(`-${deletedClasses.join(' ')}`);
148
+ }
149
+
150
+ return { element, change: changes.join(', ') };
151
+ }
152
+ }
153
+
154
+ // Try to find text content differences
155
+ const textRegex = />([^<]+)</;
156
+ const removedText = removed.match(textRegex);
157
+ const addedText = added.match(textRegex);
158
+
159
+ if (removedText && addedText && removedText[1] !== addedText[1]) {
160
+ const elementMatch = added.match(/<(\w+)/);
161
+ return {
162
+ element: elementMatch ? elementMatch[1] : undefined,
163
+ change: `"${addedText[1].substring(0, 30)}${addedText[1].length > 30 ? '...' : ''}"`
164
+ };
165
+ }
166
+
167
+ // Generic difference - show a summary
168
+ if (removed !== added) {
169
+ const elementMatch = added.match(/<(\w+)/);
170
+ return {
171
+ element: elementMatch ? elementMatch[1] : undefined,
172
+ change: 'modified'
173
+ };
174
+ }
175
+
176
+ return null;
177
+ }
178
+
179
+ /**
180
+ * Truncate a change for display
181
+ */
182
+ function truncateChange(line: string): string {
183
+ // Try to extract just the meaningful part
184
+ const elementMatch = line.match(/<(\w+)/);
185
+ if (elementMatch) {
186
+ return `<${elementMatch[1]}...>`;
187
+ }
188
+ return line.length > 40 ? line.substring(0, 40) + '...' : line;
21
189
  }
22
190
 
23
191
  export function InlineDiffPreview({
24
192
  action,
25
193
  onAccept,
26
194
  onRevert,
195
+ liveStatus,
196
+ isProcessing = false,
27
197
  }: InlineDiffPreviewProps) {
28
198
  const [expandedFiles, setExpandedFiles] = useState<Set<string>>(
29
199
  // Expand first file by default
30
200
  new Set(action.files?.slice(0, 1).map((f) => f.path) || [])
31
201
  );
202
+ // Track which files show full diff vs summary
203
+ const [showFullDiff, setShowFullDiff] = useState<Set<string>>(new Set());
32
204
 
33
205
  const toggleFile = (path: string) => {
34
206
  setExpandedFiles((prev) => {
@@ -42,10 +214,38 @@ export function InlineDiffPreview({
42
214
  });
43
215
  };
44
216
 
217
+ const toggleFullDiff = (path: string) => {
218
+ setShowFullDiff((prev) => {
219
+ const next = new Set(prev);
220
+ if (next.has(path)) {
221
+ next.delete(path);
222
+ } else {
223
+ next.add(path);
224
+ }
225
+ return next;
226
+ });
227
+ };
228
+
229
+ // Pre-compute change summaries for all files
230
+ const fileSummaries = useMemo(() => {
231
+ const summaries: Record<string, ChangeSummary[]> = {};
232
+ for (const file of action.files || []) {
233
+ if (file.diff) {
234
+ summaries[file.path] = extractChangeSummary(file.diff);
235
+ }
236
+ }
237
+ return summaries;
238
+ }, [action.files]);
239
+
240
+ // Derive loading state from liveStatus or isProcessing
241
+ const isLoading = isProcessing || liveStatus === "accepting" || liveStatus === "reverting";
242
+ const isWaitingHMR = liveStatus === "waiting-hmr";
243
+ const isLive = liveStatus === "reviewing" || action.status === "pending";
244
+
45
245
  const getStatusColor = () => {
46
246
  switch (action.status) {
47
247
  case "pending":
48
- return "border-amber-300 dark:border-amber-600 bg-amber-50 dark:bg-amber-900/20";
248
+ return "border-green-300 dark:border-green-600 bg-green-50 dark:bg-green-900/20";
49
249
  case "accepted":
50
250
  return "border-green-300 dark:border-green-600 bg-green-50 dark:bg-green-900/20";
51
251
  case "reverted":
@@ -58,12 +258,22 @@ export function InlineDiffPreview({
58
258
  };
59
259
 
60
260
  const getStatusBadge = () => {
261
+ // Show HMR status first if applicable
262
+ if (isWaitingHMR) {
263
+ return (
264
+ <span className="inline-flex items-center gap-1 px-2 py-0.5 text-[10px] font-medium bg-amber-100 dark:bg-amber-900/40 text-amber-700 dark:text-amber-400 rounded-full">
265
+ <Loader2 className="h-3 w-3 animate-spin" />
266
+ Refreshing...
267
+ </span>
268
+ );
269
+ }
270
+
61
271
  switch (action.status) {
62
272
  case "pending":
63
273
  return (
64
- <span className="inline-flex items-center gap-1 px-2 py-0.5 text-[10px] font-medium bg-amber-100 dark:bg-amber-900/40 text-amber-700 dark:text-amber-400 rounded-full">
65
- <Clock className="h-3 w-3" />
66
- Pending Review
274
+ <span className="inline-flex items-center gap-1 px-2 py-0.5 text-[10px] font-medium bg-green-100 dark:bg-green-900/40 text-green-700 dark:text-green-400 rounded-full">
275
+ <Check className="h-3 w-3" />
276
+ Changes Live
67
277
  </span>
68
278
  );
69
279
  case "accepted":
@@ -114,7 +324,7 @@ export function InlineDiffPreview({
114
324
 
115
325
  return (
116
326
  <div key={i} className={cn("px-2 py-0.5", bgClass)}>
117
- <span className={lineClass}>{line}</span>
327
+ <span id="inline-diff-preview-span-line" className={lineClass}>{line}</span>
118
328
  </div>
119
329
  );
120
330
  })}
@@ -133,33 +343,34 @@ export function InlineDiffPreview({
133
343
  </span>
134
344
  {getStatusBadge()}
135
345
  </div>
346
+ </div>
136
347
 
137
- {/* Action Buttons - only show for pending */}
138
- {action.status === "pending" && (
139
- <div className="flex items-center gap-2">
140
- <button
141
- onClick={onRevert}
142
- className="inline-flex items-center gap-1 px-2.5 py-1 text-[11px] font-medium text-foreground-secondary bg-background border border-border rounded-md hover:bg-secondary transition-colors"
143
- >
144
- <RotateCcw className="h-3 w-3" />
145
- Revert
146
- </button>
147
- <button
148
- onClick={onAccept}
149
- className="inline-flex items-center gap-1 px-2.5 py-1 text-[11px] font-medium text-white bg-green-600 dark:bg-green-700 rounded-md hover:bg-green-700 dark:hover:bg-green-600 transition-colors"
150
- >
151
- <Check className="h-3 w-3" />
152
- Accept
153
- </button>
348
+ {/* Info Banner - Show when changes are live */}
349
+ {action.status === "pending" && (
350
+ <div className="flex items-start justify-between gap-2 px-3 py-2 bg-blue-50 dark:bg-blue-900/20 border-b border-inherit">
351
+ <div className="flex items-start gap-2">
352
+ <Info className="h-3.5 w-3.5 text-blue-600 dark:text-blue-400 mt-0.5 flex-shrink-0" />
353
+ <span className="text-[11px] text-blue-700 dark:text-blue-300">
354
+ <strong>Changes saved!</strong> If you don&apos;t see them, click Refresh.
355
+ </span>
154
356
  </div>
155
- )}
156
- </div>
357
+ <button
358
+ onClick={handleRefreshPage}
359
+ className="flex items-center gap-1 px-2 py-1 text-[10px] font-medium text-blue-700 dark:text-blue-300 bg-blue-100 dark:bg-blue-800/50 hover:bg-blue-200 dark:hover:bg-blue-700/50 rounded transition-colors flex-shrink-0"
360
+ >
361
+ <RefreshCw className="h-3 w-3" />
362
+ Refresh
363
+ </button>
364
+ </div>
365
+ )}
157
366
 
158
367
  {/* File List */}
159
368
  <div className="divide-y divide-inherit">
160
369
  {action.files?.map((file) => {
161
370
  const isExpanded = expandedFiles.has(file.path);
371
+ const isShowingFullDiff = showFullDiff.has(file.path);
162
372
  const fileName = file.path.split("/").pop() || file.path;
373
+ const summary = fileSummaries[file.path] || [];
163
374
 
164
375
  return (
165
376
  <div key={file.path}>
@@ -176,15 +387,77 @@ export function InlineDiffPreview({
176
387
  <span className="text-[11px] font-mono text-foreground truncate">
177
388
  {fileName}
178
389
  </span>
179
- <span className="text-[10px] text-foreground-muted truncate">
390
+ <span className="text-[10px] text-foreground-muted truncate flex-1">
180
391
  {file.path}
181
392
  </span>
182
393
  </button>
183
394
 
184
- {/* Diff Content */}
185
- {isExpanded && file.diff && (
186
- <div className="bg-background border-t border-inherit max-h-[200px] overflow-y-auto">
187
- {renderDiff(file.diff)}
395
+ {/* Change Summary (default view) */}
396
+ {isExpanded && file.diff && !isShowingFullDiff && (
397
+ <div className="bg-background border-t border-inherit px-3 py-2 space-y-1">
398
+ {summary.length > 0 ? (
399
+ <>
400
+ {summary.map((change, i) => (
401
+ <div
402
+ key={i}
403
+ className="flex items-center gap-2 text-[11px]"
404
+ title={change.fullLine}
405
+ >
406
+ {change.type === 'added' || change.type === 'modified' ? (
407
+ <Plus className="h-3 w-3 text-green-600 dark:text-green-400 flex-shrink-0" />
408
+ ) : (
409
+ <Minus className="h-3 w-3 text-red-600 dark:text-red-400 flex-shrink-0" />
410
+ )}
411
+ {change.element && (
412
+ <span className="font-mono text-blue-600 dark:text-blue-400 flex-shrink-0">
413
+ {change.element}:
414
+ </span>
415
+ )}
416
+ <span className={cn(
417
+ "font-mono",
418
+ change.type === 'removed'
419
+ ? "text-red-600 dark:text-red-400"
420
+ : "text-green-600 dark:text-green-400"
421
+ )}>
422
+ {change.change}
423
+ </span>
424
+ </div>
425
+ ))}
426
+ {/* Toggle to show full diff */}
427
+ <button
428
+ onClick={(e) => {
429
+ e.stopPropagation();
430
+ toggleFullDiff(file.path);
431
+ }}
432
+ className="flex items-center gap-1 mt-2 text-[10px] text-foreground-muted hover:text-foreground transition-colors"
433
+ >
434
+ <Code className="h-3 w-3" />
435
+ Show full diff
436
+ </button>
437
+ </>
438
+ ) : (
439
+ <span className="text-[11px] text-foreground-muted">No significant changes detected</span>
440
+ )}
441
+ </div>
442
+ )}
443
+
444
+ {/* Full Diff Content (toggle view) */}
445
+ {isExpanded && file.diff && isShowingFullDiff && (
446
+ <div className="bg-background border-t border-inherit">
447
+ <div className="overflow-x-auto">
448
+ {renderDiff(file.diff)}
449
+ </div>
450
+ {/* Toggle back to summary */}
451
+ <button
452
+ onClick={(e) => {
453
+ e.stopPropagation();
454
+ toggleFullDiff(file.path);
455
+ }}
456
+ className="flex items-center gap-1 px-3 py-2 text-[10px] text-foreground-muted hover:text-foreground transition-colors border-t border-inherit w-full"
457
+ >
458
+ <ChevronRight className="h-3 w-3" />
459
+ Show summary
460
+ </button>
188
461
  </div>
189
462
  )}
190
463
  </div>
@@ -192,10 +465,61 @@ export function InlineDiffPreview({
192
465
  })}
193
466
  </div>
194
467
 
195
- {/* Explanation */}
196
- {action.explanation && (
197
- <div className="px-3 py-2 border-t border-inherit bg-background/30">
198
- <p className="text-[11px] text-foreground-secondary italic">{action.explanation}</p>
468
+ {/* Warning about navigation - only show for pending */}
469
+ {action.status === "pending" && (
470
+ <div className="flex items-start gap-2 px-3 py-2 bg-amber-50 dark:bg-amber-900/20 border-t border-inherit">
471
+ <AlertTriangle className="h-3.5 w-3.5 text-amber-600 dark:text-amber-400 mt-0.5 flex-shrink-0" />
472
+ <span className="text-[11px] text-amber-700 dark:text-amber-300">
473
+ Navigating away will automatically revert changes. Make sure to accept or revert first.
474
+ </span>
475
+ </div>
476
+ )}
477
+
478
+ {/* Action Buttons - only show for pending */}
479
+ {action.status === "pending" && (
480
+ <div className="flex gap-2 px-3 py-2 border-t border-inherit bg-background/50">
481
+ <button
482
+ onClick={onAccept}
483
+ disabled={isLoading}
484
+ className={cn(
485
+ "flex-1 inline-flex items-center justify-center gap-1.5 px-3 py-2 text-[11px] font-medium rounded-md transition-colors",
486
+ "bg-green-600 dark:bg-green-700 text-white hover:bg-green-700 dark:hover:bg-green-600",
487
+ "disabled:opacity-50 disabled:cursor-not-allowed"
488
+ )}
489
+ >
490
+ {liveStatus === "accepting" ? (
491
+ <>
492
+ <Loader2 className="h-3.5 w-3.5 animate-spin" />
493
+ Accepting...
494
+ </>
495
+ ) : (
496
+ <>
497
+ <Check className="h-3.5 w-3.5" />
498
+ Keep Changes
499
+ </>
500
+ )}
501
+ </button>
502
+ <button
503
+ onClick={onRevert}
504
+ disabled={isLoading}
505
+ className={cn(
506
+ "flex-1 inline-flex items-center justify-center gap-1.5 px-3 py-2 text-[11px] font-medium rounded-md transition-colors",
507
+ "bg-background border border-border text-foreground-secondary hover:bg-secondary",
508
+ "disabled:opacity-50 disabled:cursor-not-allowed"
509
+ )}
510
+ >
511
+ {liveStatus === "reverting" ? (
512
+ <>
513
+ <Loader2 className="h-3.5 w-3.5 animate-spin" />
514
+ Reverting...
515
+ </>
516
+ ) : (
517
+ <>
518
+ <RotateCcw className="h-3.5 w-3.5" />
519
+ Revert
520
+ </>
521
+ )}
522
+ </button>
199
523
  </div>
200
524
  )}
201
525
  </div>
@@ -312,26 +312,26 @@ export function InspectorOverlay({
312
312
  {isChangedElement ? (
313
313
  <>
314
314
  <Check className="h-3 w-3" />
315
- <span>Changed: {el.name}</span>
316
- {el.variantId && <span className="ml-1 opacity-80 font-mono text-[9px]">#{el.variantId.substring(0, 4)}</span>}
315
+ <span id="span-changed-elname">Changed: {el.name}</span>
316
+ {el.variantId && <span id="span-elvariantidsubstring" className="ml-1 opacity-80 font-mono text-[9px]">#{el.variantId.substring(0, 4)}</span>}
317
317
  </>
318
318
  ) : previewMode && isMatchingType ? (
319
319
  <>
320
320
  <Sparkles className="h-3 w-3" />
321
- <span>Preview: {el.name}</span>
322
- {el.variantId && <span className="ml-1 opacity-80 font-mono text-[9px]">#{el.variantId.substring(0, 4)}</span>}
321
+ <span id="span-preview-elname">Preview: {el.name}</span>
322
+ {el.variantId && <span id="span-elvariantidsubstring" className="ml-1 opacity-80 font-mono text-[9px]">#{el.variantId.substring(0, 4)}</span>}
323
323
  </>
324
324
  ) : visionMode && isVisionFocused ? (
325
325
  <>
326
326
  <Eye className="h-3 w-3" />
327
- <span>Focused: {el.name}</span>
328
- {el.variantId && <span className="ml-1 opacity-80 font-mono text-[9px]">#{el.variantId.substring(0, 4)}</span>}
327
+ <span id="span-focused-elname">Focused: {el.name}</span>
328
+ {el.variantId && <span id="span-elvariantidsubstring" className="ml-1 opacity-80 font-mono text-[9px]">#{el.variantId.substring(0, 4)}</span>}
329
329
  </>
330
330
  ) : visionMode ? (
331
331
  <>
332
332
  <Eye className="h-3 w-3 opacity-60" />
333
333
  {el.name}
334
- {el.variantId && <span className="ml-1 opacity-80 font-mono text-[9px] border-l border-white/30 pl-1">#{el.variantId.substring(0, 4)}</span>}
334
+ {el.variantId && <span id="span-elvariantidsubstring" className="ml-1 opacity-80 font-mono text-[9px] border-l border-white/30 pl-1">#{el.variantId.substring(0, 4)}</span>}
335
335
  </>
336
336
  ) : (
337
337
  <>
@@ -339,8 +339,8 @@ export function InspectorOverlay({
339
339
  {el.type === "component" && <Box className="h-3 w-3" />}
340
340
  {el.type === "text" && <Type className="h-3 w-3" />}
341
341
  {el.name}
342
- {el.variantId && <span className="ml-1 opacity-80 font-mono text-[9px] border-l border-white/30 pl-1">#{el.variantId.substring(0, 4)}</span>}
343
- {isSelected && <span className="ml-1">✓</span>}
342
+ {el.variantId && <span id="span-elvariantidsubstring" className="ml-1 opacity-80 font-mono text-[9px] border-l border-white/30 pl-1">#{el.variantId.substring(0, 4)}</span>}
343
+ {isSelected && <span id="span-title" className="ml-1">✓</span>}
344
344
  </>
345
345
  )}
346
346
  </div>