sonance-brand-mcp 1.3.110 → 1.3.112
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.
- package/dist/assets/api/sonance-ai-edit/route.ts +30 -7
- package/dist/assets/api/sonance-save-image/route.ts +625 -0
- package/dist/assets/api/sonance-vision-apply/image-styling-detection.ts +1360 -0
- package/dist/assets/api/sonance-vision-apply/route.ts +1020 -64
- package/dist/assets/api/sonance-vision-apply/styling-detection.ts +730 -0
- package/dist/assets/api/sonance-vision-apply/theme-discovery.ts +1 -1
- package/dist/assets/api/sonance-vision-edit/route.ts +33 -8
- package/dist/assets/brand-system.ts +13 -12
- package/dist/assets/components/accordion.tsx +15 -7
- package/dist/assets/components/alert-dialog.tsx +35 -10
- package/dist/assets/components/alert.tsx +11 -10
- package/dist/assets/components/avatar.tsx +4 -4
- package/dist/assets/components/badge.tsx +16 -12
- package/dist/assets/components/button.stories.tsx +3 -3
- package/dist/assets/components/button.tsx +50 -31
- package/dist/assets/components/calendar.tsx +12 -8
- package/dist/assets/components/card.tsx +35 -29
- package/dist/assets/components/checkbox.tsx +9 -8
- package/dist/assets/components/code.tsx +19 -11
- package/dist/assets/components/command.tsx +32 -13
- package/dist/assets/components/context-menu.tsx +37 -16
- package/dist/assets/components/dialog.tsx +8 -5
- package/dist/assets/components/divider.tsx +15 -5
- package/dist/assets/components/drawer.tsx +4 -3
- package/dist/assets/components/dropdown-menu.tsx +15 -13
- package/dist/assets/components/hover-card.tsx +4 -1
- package/dist/assets/components/image.tsx +1 -1
- package/dist/assets/components/input.tsx +29 -14
- package/dist/assets/components/kbd.stories.tsx +3 -3
- package/dist/assets/components/kbd.tsx +29 -13
- package/dist/assets/components/listbox.tsx +8 -8
- package/dist/assets/components/menubar.tsx +50 -23
- package/dist/assets/components/navbar.stories.tsx +140 -13
- package/dist/assets/components/navbar.tsx +22 -5
- package/dist/assets/components/navigation-menu.tsx +28 -6
- package/dist/assets/components/pagination.tsx +10 -10
- package/dist/assets/components/popover.tsx +10 -8
- package/dist/assets/components/progress.tsx +6 -4
- package/dist/assets/components/radio-group.tsx +5 -5
- package/dist/assets/components/select.tsx +49 -29
- package/dist/assets/components/separator.tsx +3 -3
- package/dist/assets/components/sheet.tsx +4 -4
- package/dist/assets/components/sidebar.tsx +10 -10
- package/dist/assets/components/skeleton.tsx +13 -5
- package/dist/assets/components/slider.tsx +12 -10
- package/dist/assets/components/switch.tsx +4 -4
- package/dist/assets/components/table.tsx +5 -5
- package/dist/assets/components/tabs.tsx +8 -8
- package/dist/assets/components/textarea.tsx +11 -9
- package/dist/assets/components/toast.tsx +7 -7
- package/dist/assets/components/toggle.tsx +27 -7
- package/dist/assets/components/tooltip.tsx +10 -8
- package/dist/assets/components/user.tsx +8 -6
- package/dist/assets/dev-tools/SonanceDevTools.tsx +851 -708
- package/dist/assets/dev-tools/components/ApplyFirstPreview.tsx +10 -10
- package/dist/assets/dev-tools/components/ChatHistory.tsx +145 -0
- package/dist/assets/dev-tools/components/ChatInterface.tsx +444 -295
- package/dist/assets/dev-tools/components/ChatTabBar.tsx +82 -0
- package/dist/assets/dev-tools/components/DiffPreview.tsx +1 -1
- package/dist/assets/dev-tools/components/InlineDiffPreview.tsx +528 -0
- package/dist/assets/dev-tools/components/InspectorOverlay.tsx +21 -18
- package/dist/assets/dev-tools/components/PropertiesPanel.tsx +1345 -0
- package/dist/assets/dev-tools/components/ScreenshotAnnotator.tsx +1 -1
- package/dist/assets/dev-tools/components/SectionHighlight.tsx +1 -1
- package/dist/assets/dev-tools/components/VisionDiffPreview.tsx +7 -7
- package/dist/assets/dev-tools/components/VisionModeBorder.tsx +12 -63
- package/dist/assets/dev-tools/constants.ts +38 -6
- package/dist/assets/dev-tools/hooks/index.ts +69 -0
- package/dist/assets/dev-tools/hooks/useComponentDetection.ts +132 -0
- package/dist/assets/dev-tools/hooks/useComputedStyles.ts +471 -0
- package/dist/assets/dev-tools/hooks/useContentHash.ts +212 -0
- package/dist/assets/dev-tools/hooks/useElementScanner.ts +398 -0
- package/dist/assets/dev-tools/hooks/useImageDetection.ts +162 -0
- package/dist/assets/dev-tools/hooks/useTextDetection.ts +217 -0
- package/dist/assets/dev-tools/index.ts +3 -0
- package/dist/assets/dev-tools/panels/AnalysisPanel.tsx +32 -32
- package/dist/assets/dev-tools/panels/ComponentsPanel.tsx +384 -131
- package/dist/assets/dev-tools/panels/TextPanel.tsx +10 -10
- package/dist/assets/dev-tools/types.ts +93 -2
- package/dist/assets/globals.css +225 -9
- package/dist/assets/styles/brand-overrides.css +3 -2
- package/dist/assets/utils.ts +2 -1
- package/dist/index.js +22 -3
- package/package.json +2 -1
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React from "react";
|
|
4
|
+
import { Plus, X, MessageSquare } from "lucide-react";
|
|
5
|
+
import { cn } from "../../../lib/utils";
|
|
6
|
+
import { ChatSession } from "../types";
|
|
7
|
+
|
|
8
|
+
export interface ChatTabBarProps {
|
|
9
|
+
sessions: ChatSession[];
|
|
10
|
+
activeSessionId: string | null;
|
|
11
|
+
onSelectSession: (sessionId: string) => void;
|
|
12
|
+
onCreateSession: () => void;
|
|
13
|
+
onCloseSession: (sessionId: string) => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function ChatTabBar({
|
|
17
|
+
sessions,
|
|
18
|
+
activeSessionId,
|
|
19
|
+
onSelectSession,
|
|
20
|
+
onCreateSession,
|
|
21
|
+
onCloseSession,
|
|
22
|
+
}: ChatTabBarProps) {
|
|
23
|
+
// Generate a short name for the session
|
|
24
|
+
const getSessionName = (session: ChatSession, index: number) => {
|
|
25
|
+
if (session.name && session.name !== "New Chat") {
|
|
26
|
+
return session.name.length > 12 ? session.name.slice(0, 12) + "..." : session.name;
|
|
27
|
+
}
|
|
28
|
+
return `Chat ${index + 1}`;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<div className="flex items-center gap-0.5 px-3 py-1 bg-transparent border-b border-white/10 overflow-x-auto scrollbar-hide">
|
|
33
|
+
{/* Session Tabs - Cursor-style underline (always on dark bg) */}
|
|
34
|
+
{sessions.map((session, index) => {
|
|
35
|
+
const isActive = session.id === activeSessionId;
|
|
36
|
+
return (
|
|
37
|
+
<div
|
|
38
|
+
key={session.id}
|
|
39
|
+
className={cn(
|
|
40
|
+
"group relative flex items-center gap-1 px-2 py-1 text-[11px] cursor-pointer transition-all duration-150 bg-transparent",
|
|
41
|
+
isActive
|
|
42
|
+
? "font-medium text-white after:absolute after:bottom-0 after:left-0.5 after:right-0.5 after:h-[2px] after:bg-[#00A3E1] after:rounded-full"
|
|
43
|
+
: "text-gray-400 hover:text-white"
|
|
44
|
+
)}
|
|
45
|
+
onClick={() => onSelectSession(session.id)}
|
|
46
|
+
>
|
|
47
|
+
<MessageSquare className="h-3 w-3 flex-shrink-0" />
|
|
48
|
+
<span id="chat-tab-bar-span-getsessionnamesessio" className="truncate max-w-[72px]">
|
|
49
|
+
{getSessionName(session, index)}
|
|
50
|
+
</span>
|
|
51
|
+
{/* Close button - only show if more than 1 session */}
|
|
52
|
+
{sessions.length > 1 && (
|
|
53
|
+
<button
|
|
54
|
+
onClick={(e) => {
|
|
55
|
+
e.stopPropagation();
|
|
56
|
+
onCloseSession(session.id);
|
|
57
|
+
}}
|
|
58
|
+
className={cn(
|
|
59
|
+
"p-0.5 rounded-sm hover:bg-white/10 transition-colors",
|
|
60
|
+
"opacity-0 group-hover:opacity-100",
|
|
61
|
+
isActive && "opacity-60 group-hover:opacity-100"
|
|
62
|
+
)}
|
|
63
|
+
>
|
|
64
|
+
<X className="h-2.5 w-2.5" />
|
|
65
|
+
</button>
|
|
66
|
+
)}
|
|
67
|
+
</div>
|
|
68
|
+
);
|
|
69
|
+
})}
|
|
70
|
+
|
|
71
|
+
{/* New Chat Button - minimal icon */}
|
|
72
|
+
<button
|
|
73
|
+
onClick={onCreateSession}
|
|
74
|
+
className="flex items-center justify-center p-1 text-gray-400 hover:text-white hover:bg-white/10 transition-colors ml-0.5"
|
|
75
|
+
title="New chat"
|
|
76
|
+
>
|
|
77
|
+
<Plus className="h-3 w-3" />
|
|
78
|
+
</button>
|
|
79
|
+
</div>
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
@@ -118,7 +118,7 @@ export function DiffPreview({
|
|
|
118
118
|
{/* Live Preview Indicator */}
|
|
119
119
|
<div className="flex items-center gap-2 p-2 rounded bg-[#00A3E1]/10 border border-[#00A3E1]/30">
|
|
120
120
|
<Eye className="h-3.5 w-3.5 text-[#00A3E1]" />
|
|
121
|
-
<span className="text-xs text-[#00A3E1]">
|
|
121
|
+
<span id="span-live-preview-active-" className="text-xs text-[#00A3E1]">
|
|
122
122
|
Live preview active - scroll to see changes on the page
|
|
123
123
|
</span>
|
|
124
124
|
</div>
|
|
@@ -0,0 +1,528 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React, { useState, useMemo } from "react";
|
|
4
|
+
import {
|
|
5
|
+
ChevronDown,
|
|
6
|
+
ChevronRight,
|
|
7
|
+
Check,
|
|
8
|
+
RotateCcw,
|
|
9
|
+
FileCode,
|
|
10
|
+
CheckCircle,
|
|
11
|
+
XCircle,
|
|
12
|
+
Loader2,
|
|
13
|
+
AlertTriangle,
|
|
14
|
+
Info,
|
|
15
|
+
RefreshCw,
|
|
16
|
+
Plus,
|
|
17
|
+
Minus,
|
|
18
|
+
Code,
|
|
19
|
+
} from "lucide-react";
|
|
20
|
+
import { cn } from "../../../lib/utils";
|
|
21
|
+
import { ChatMessageAction, ApplyFirstStatus } from "../types";
|
|
22
|
+
|
|
23
|
+
export interface InlineDiffPreviewProps {
|
|
24
|
+
action: ChatMessageAction;
|
|
25
|
+
onAccept?: () => void;
|
|
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;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function InlineDiffPreview({
|
|
192
|
+
action,
|
|
193
|
+
onAccept,
|
|
194
|
+
onRevert,
|
|
195
|
+
liveStatus,
|
|
196
|
+
isProcessing = false,
|
|
197
|
+
}: InlineDiffPreviewProps) {
|
|
198
|
+
const [expandedFiles, setExpandedFiles] = useState<Set<string>>(
|
|
199
|
+
// Expand first file by default
|
|
200
|
+
new Set(action.files?.slice(0, 1).map((f) => f.path) || [])
|
|
201
|
+
);
|
|
202
|
+
// Track which files show full diff vs summary
|
|
203
|
+
const [showFullDiff, setShowFullDiff] = useState<Set<string>>(new Set());
|
|
204
|
+
|
|
205
|
+
const toggleFile = (path: string) => {
|
|
206
|
+
setExpandedFiles((prev) => {
|
|
207
|
+
const next = new Set(prev);
|
|
208
|
+
if (next.has(path)) {
|
|
209
|
+
next.delete(path);
|
|
210
|
+
} else {
|
|
211
|
+
next.add(path);
|
|
212
|
+
}
|
|
213
|
+
return next;
|
|
214
|
+
});
|
|
215
|
+
};
|
|
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
|
+
|
|
245
|
+
const getStatusColor = () => {
|
|
246
|
+
switch (action.status) {
|
|
247
|
+
case "pending":
|
|
248
|
+
return "border-green-300 dark:border-green-600 bg-green-50 dark:bg-green-900/20";
|
|
249
|
+
case "accepted":
|
|
250
|
+
return "border-green-300 dark:border-green-600 bg-green-50 dark:bg-green-900/20";
|
|
251
|
+
case "reverted":
|
|
252
|
+
return "border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-800/50";
|
|
253
|
+
case "error":
|
|
254
|
+
return "border-red-300 dark:border-red-600 bg-red-50 dark:bg-red-900/20";
|
|
255
|
+
default:
|
|
256
|
+
return "border-border bg-background";
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
|
|
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
|
+
|
|
271
|
+
switch (action.status) {
|
|
272
|
+
case "pending":
|
|
273
|
+
return (
|
|
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
|
|
277
|
+
</span>
|
|
278
|
+
);
|
|
279
|
+
case "accepted":
|
|
280
|
+
return (
|
|
281
|
+
<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">
|
|
282
|
+
<CheckCircle className="h-3 w-3" />
|
|
283
|
+
Accepted
|
|
284
|
+
</span>
|
|
285
|
+
);
|
|
286
|
+
case "reverted":
|
|
287
|
+
return (
|
|
288
|
+
<span className="inline-flex items-center gap-1 px-2 py-0.5 text-[10px] font-medium bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-300 rounded-full">
|
|
289
|
+
<XCircle className="h-3 w-3" />
|
|
290
|
+
Reverted
|
|
291
|
+
</span>
|
|
292
|
+
);
|
|
293
|
+
case "error":
|
|
294
|
+
return (
|
|
295
|
+
<span className="inline-flex items-center gap-1 px-2 py-0.5 text-[10px] font-medium bg-red-100 dark:bg-red-900/40 text-red-700 dark:text-red-400 rounded-full">
|
|
296
|
+
<XCircle className="h-3 w-3" />
|
|
297
|
+
Error
|
|
298
|
+
</span>
|
|
299
|
+
);
|
|
300
|
+
default:
|
|
301
|
+
return null;
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
// Parse diff into lines with proper styling
|
|
306
|
+
const renderDiff = (diff: string) => {
|
|
307
|
+
const lines = diff.split("\n");
|
|
308
|
+
return (
|
|
309
|
+
<pre className="text-[11px] font-mono leading-relaxed overflow-x-auto">
|
|
310
|
+
{lines.map((line, i) => {
|
|
311
|
+
let lineClass = "text-foreground-secondary";
|
|
312
|
+
let bgClass = "";
|
|
313
|
+
|
|
314
|
+
if (line.startsWith("+") && !line.startsWith("+++")) {
|
|
315
|
+
lineClass = "text-green-700 dark:text-green-400";
|
|
316
|
+
bgClass = "bg-green-50 dark:bg-green-900/30";
|
|
317
|
+
} else if (line.startsWith("-") && !line.startsWith("---")) {
|
|
318
|
+
lineClass = "text-red-700 dark:text-red-400";
|
|
319
|
+
bgClass = "bg-red-50 dark:bg-red-900/30";
|
|
320
|
+
} else if (line.startsWith("@@")) {
|
|
321
|
+
lineClass = "text-blue-600 dark:text-blue-400";
|
|
322
|
+
bgClass = "bg-blue-50 dark:bg-blue-900/30";
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
return (
|
|
326
|
+
<div key={i} className={cn("px-2 py-0.5", bgClass)}>
|
|
327
|
+
<span id="inline-diff-preview-span-line" className={lineClass}>{line}</span>
|
|
328
|
+
</div>
|
|
329
|
+
);
|
|
330
|
+
})}
|
|
331
|
+
</pre>
|
|
332
|
+
);
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
return (
|
|
336
|
+
<div className={cn("rounded-lg border overflow-hidden", getStatusColor())}>
|
|
337
|
+
{/* Header */}
|
|
338
|
+
<div className="flex items-center justify-between px-3 py-2 border-b border-inherit bg-background/50">
|
|
339
|
+
<div className="flex items-center gap-2">
|
|
340
|
+
<FileCode className="h-4 w-4 text-foreground-secondary" />
|
|
341
|
+
<span className="text-xs font-medium text-foreground">
|
|
342
|
+
{action.files?.length || 0} file{(action.files?.length || 0) !== 1 ? "s" : ""} changed
|
|
343
|
+
</span>
|
|
344
|
+
{getStatusBadge()}
|
|
345
|
+
</div>
|
|
346
|
+
</div>
|
|
347
|
+
|
|
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't see them, click Refresh.
|
|
355
|
+
</span>
|
|
356
|
+
</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
|
+
)}
|
|
366
|
+
|
|
367
|
+
{/* File List */}
|
|
368
|
+
<div className="divide-y divide-inherit">
|
|
369
|
+
{action.files?.map((file) => {
|
|
370
|
+
const isExpanded = expandedFiles.has(file.path);
|
|
371
|
+
const isShowingFullDiff = showFullDiff.has(file.path);
|
|
372
|
+
const fileName = file.path.split("/").pop() || file.path;
|
|
373
|
+
const summary = fileSummaries[file.path] || [];
|
|
374
|
+
|
|
375
|
+
return (
|
|
376
|
+
<div key={file.path}>
|
|
377
|
+
{/* File Header */}
|
|
378
|
+
<button
|
|
379
|
+
onClick={() => toggleFile(file.path)}
|
|
380
|
+
className="w-full flex items-center gap-2 px-3 py-2 text-left hover:bg-background/50 transition-colors"
|
|
381
|
+
>
|
|
382
|
+
{isExpanded ? (
|
|
383
|
+
<ChevronDown className="h-3.5 w-3.5 text-foreground-muted" />
|
|
384
|
+
) : (
|
|
385
|
+
<ChevronRight className="h-3.5 w-3.5 text-foreground-muted" />
|
|
386
|
+
)}
|
|
387
|
+
<span className="text-[11px] font-mono text-foreground truncate">
|
|
388
|
+
{fileName}
|
|
389
|
+
</span>
|
|
390
|
+
<span className="text-[10px] text-foreground-muted truncate flex-1">
|
|
391
|
+
{file.path}
|
|
392
|
+
</span>
|
|
393
|
+
</button>
|
|
394
|
+
|
|
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>
|
|
461
|
+
</div>
|
|
462
|
+
)}
|
|
463
|
+
</div>
|
|
464
|
+
);
|
|
465
|
+
})}
|
|
466
|
+
</div>
|
|
467
|
+
|
|
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>
|
|
523
|
+
</div>
|
|
524
|
+
)}
|
|
525
|
+
</div>
|
|
526
|
+
);
|
|
527
|
+
}
|
|
528
|
+
|