sonance-brand-mcp 1.3.110 → 1.3.111
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-vision-apply/route.ts +33 -8
- package/dist/assets/api/sonance-vision-edit/route.ts +33 -8
- package/dist/assets/dev-tools/SonanceDevTools.tsx +441 -365
- package/dist/assets/dev-tools/components/ChatHistory.tsx +141 -0
- package/dist/assets/dev-tools/components/ChatInterface.tsx +402 -294
- package/dist/assets/dev-tools/components/ChatTabBar.tsx +82 -0
- package/dist/assets/dev-tools/components/InlineDiffPreview.tsx +204 -0
- package/dist/assets/dev-tools/components/InspectorOverlay.tsx +12 -9
- package/dist/assets/dev-tools/components/PropertiesPanel.tsx +695 -0
- package/dist/assets/dev-tools/components/VisionModeBorder.tsx +16 -7
- package/dist/assets/dev-tools/constants.ts +38 -6
- package/dist/assets/dev-tools/hooks/useComputedStyles.ts +365 -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 +277 -127
- package/dist/assets/dev-tools/types.ts +51 -2
- package/dist/index.js +22 -3
- package/package.json +2 -1
|
@@ -0,0 +1,695 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React, { useState, useEffect, useCallback, useMemo } from "react";
|
|
4
|
+
import {
|
|
5
|
+
ChevronDown,
|
|
6
|
+
ChevronRight,
|
|
7
|
+
Eye,
|
|
8
|
+
EyeOff,
|
|
9
|
+
Box,
|
|
10
|
+
Type,
|
|
11
|
+
Palette,
|
|
12
|
+
Layers,
|
|
13
|
+
Grid3X3,
|
|
14
|
+
Square,
|
|
15
|
+
CircleDot,
|
|
16
|
+
Minus,
|
|
17
|
+
CornerDownRight,
|
|
18
|
+
RotateCcw,
|
|
19
|
+
Save,
|
|
20
|
+
Loader2,
|
|
21
|
+
} from "lucide-react";
|
|
22
|
+
import { cn } from "../../../lib/utils";
|
|
23
|
+
import { ComputedStyles } from "../hooks/useComputedStyles";
|
|
24
|
+
import { VisionFocusedElement } from "../types";
|
|
25
|
+
|
|
26
|
+
// ============================================
|
|
27
|
+
// TYPES
|
|
28
|
+
// ============================================
|
|
29
|
+
|
|
30
|
+
export interface PropertyEdits {
|
|
31
|
+
// Layout
|
|
32
|
+
width?: string;
|
|
33
|
+
height?: string;
|
|
34
|
+
// Appearance
|
|
35
|
+
opacity?: string;
|
|
36
|
+
borderRadius?: string;
|
|
37
|
+
// Typography
|
|
38
|
+
fontSize?: string;
|
|
39
|
+
fontWeight?: string;
|
|
40
|
+
lineHeight?: string;
|
|
41
|
+
letterSpacing?: string;
|
|
42
|
+
color?: string;
|
|
43
|
+
// Fill
|
|
44
|
+
backgroundColor?: string;
|
|
45
|
+
// Spacing
|
|
46
|
+
padding?: string;
|
|
47
|
+
margin?: string;
|
|
48
|
+
gap?: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
interface PropertiesPanelProps {
|
|
52
|
+
element: VisionFocusedElement | null;
|
|
53
|
+
styles: ComputedStyles | null;
|
|
54
|
+
onPropertyClick?: (property: string, value: string) => void;
|
|
55
|
+
onSaveChanges?: (edits: PropertyEdits, element: VisionFocusedElement) => void;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
interface SectionProps {
|
|
59
|
+
title: string;
|
|
60
|
+
icon: React.ReactNode;
|
|
61
|
+
defaultOpen?: boolean;
|
|
62
|
+
children: React.ReactNode;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ============================================
|
|
66
|
+
// SECTION COMPONENT
|
|
67
|
+
// ============================================
|
|
68
|
+
|
|
69
|
+
function Section({ title, icon, defaultOpen = true, children }: SectionProps) {
|
|
70
|
+
const [isOpen, setIsOpen] = useState(defaultOpen);
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<div className="border-b border-white/10 last:border-b-0">
|
|
74
|
+
<button
|
|
75
|
+
onClick={() => setIsOpen(!isOpen)}
|
|
76
|
+
className="w-full flex items-center justify-between px-2 py-1.5 hover:bg-white/5 transition-colors"
|
|
77
|
+
>
|
|
78
|
+
<div className="flex items-center gap-1.5 text-[11px] font-medium text-gray-200">
|
|
79
|
+
{icon}
|
|
80
|
+
<span>{title}</span>
|
|
81
|
+
</div>
|
|
82
|
+
<div className="flex items-center gap-1">
|
|
83
|
+
{isOpen ? (
|
|
84
|
+
<ChevronDown className="h-3 w-3 text-gray-500" />
|
|
85
|
+
) : (
|
|
86
|
+
<ChevronRight className="h-3 w-3 text-gray-500" />
|
|
87
|
+
)}
|
|
88
|
+
</div>
|
|
89
|
+
</button>
|
|
90
|
+
{isOpen && <div className="px-2 pb-2">{children}</div>}
|
|
91
|
+
</div>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ============================================
|
|
96
|
+
// EDITABLE PROPERTY ROW
|
|
97
|
+
// ============================================
|
|
98
|
+
|
|
99
|
+
interface EditablePropertyRowProps {
|
|
100
|
+
label: string;
|
|
101
|
+
value: string | number;
|
|
102
|
+
unit?: string;
|
|
103
|
+
color?: string;
|
|
104
|
+
editKey?: keyof PropertyEdits;
|
|
105
|
+
edits?: PropertyEdits;
|
|
106
|
+
onEdit?: (key: keyof PropertyEdits, value: string) => void;
|
|
107
|
+
readOnly?: boolean;
|
|
108
|
+
inputType?: "text" | "number" | "color";
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function EditablePropertyRow({
|
|
112
|
+
label,
|
|
113
|
+
value,
|
|
114
|
+
unit,
|
|
115
|
+
color,
|
|
116
|
+
editKey,
|
|
117
|
+
edits,
|
|
118
|
+
onEdit,
|
|
119
|
+
readOnly = false,
|
|
120
|
+
inputType = "text",
|
|
121
|
+
}: EditablePropertyRowProps) {
|
|
122
|
+
const isEditable = !readOnly && editKey && edits && onEdit;
|
|
123
|
+
const editedValue = editKey && edits ? edits[editKey] : undefined;
|
|
124
|
+
const displayValue = editedValue !== undefined ? editedValue : String(value);
|
|
125
|
+
const isEdited = editedValue !== undefined && editedValue !== String(value);
|
|
126
|
+
|
|
127
|
+
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
128
|
+
if (isEditable && editKey) {
|
|
129
|
+
onEdit(editKey, e.target.value);
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
return (
|
|
134
|
+
<div className="flex items-center justify-between py-0.5 text-[10px]">
|
|
135
|
+
<span className="text-gray-400 uppercase tracking-wide">{label}</span>
|
|
136
|
+
<div className="flex items-center gap-1">
|
|
137
|
+
{color && (
|
|
138
|
+
<div
|
|
139
|
+
className="w-3 h-3 rounded-sm border border-white/20"
|
|
140
|
+
style={{ backgroundColor: color }}
|
|
141
|
+
/>
|
|
142
|
+
)}
|
|
143
|
+
{isEditable ? (
|
|
144
|
+
<input
|
|
145
|
+
type={inputType}
|
|
146
|
+
value={displayValue}
|
|
147
|
+
onChange={handleChange}
|
|
148
|
+
className={cn(
|
|
149
|
+
"w-20 px-1.5 py-0.5 text-right font-mono text-[10px] bg-white/5 border rounded text-white focus:outline-none focus:ring-1 focus:ring-[#00A3E1]",
|
|
150
|
+
isEdited ? "border-[#00A3E1]" : "border-white/10"
|
|
151
|
+
)}
|
|
152
|
+
/>
|
|
153
|
+
) : (
|
|
154
|
+
<span className="font-mono text-white">
|
|
155
|
+
{displayValue}
|
|
156
|
+
</span>
|
|
157
|
+
)}
|
|
158
|
+
{unit && <span className="text-gray-500 ml-0.5">{unit}</span>}
|
|
159
|
+
</div>
|
|
160
|
+
</div>
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ============================================
|
|
165
|
+
// EDITABLE DIMENSION GRID
|
|
166
|
+
// ============================================
|
|
167
|
+
|
|
168
|
+
interface EditableDimensionGridProps {
|
|
169
|
+
x: number;
|
|
170
|
+
y: number;
|
|
171
|
+
width: number;
|
|
172
|
+
height: number;
|
|
173
|
+
edits: PropertyEdits;
|
|
174
|
+
onEdit: (key: keyof PropertyEdits, value: string) => void;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function EditableDimensionGrid({ x, y, width, height, edits, onEdit }: EditableDimensionGridProps) {
|
|
178
|
+
const widthEdited = edits.width !== undefined && edits.width !== `${width}px`;
|
|
179
|
+
const heightEdited = edits.height !== undefined && edits.height !== `${height}px`;
|
|
180
|
+
|
|
181
|
+
return (
|
|
182
|
+
<div className="grid grid-cols-2 gap-1">
|
|
183
|
+
{/* X - Read only */}
|
|
184
|
+
<div className="flex items-center gap-1 px-1.5 py-1 bg-white/5 rounded border border-white/10">
|
|
185
|
+
<span className="text-[9px] text-gray-500 uppercase w-3">X</span>
|
|
186
|
+
<span className="text-[10px] font-mono text-white flex-1 text-right">{x}</span>
|
|
187
|
+
</div>
|
|
188
|
+
{/* Y - Read only */}
|
|
189
|
+
<div className="flex items-center gap-1 px-1.5 py-1 bg-white/5 rounded border border-white/10">
|
|
190
|
+
<span className="text-[9px] text-gray-500 uppercase w-3">Y</span>
|
|
191
|
+
<span className="text-[10px] font-mono text-white flex-1 text-right">{y}</span>
|
|
192
|
+
</div>
|
|
193
|
+
{/* W - Editable */}
|
|
194
|
+
<div className={cn(
|
|
195
|
+
"flex items-center gap-1 px-1.5 py-1 bg-white/5 rounded border",
|
|
196
|
+
widthEdited ? "border-[#00A3E1]" : "border-white/10"
|
|
197
|
+
)}>
|
|
198
|
+
<span className="text-[9px] text-gray-500 uppercase w-3">W</span>
|
|
199
|
+
<input
|
|
200
|
+
type="text"
|
|
201
|
+
value={edits.width !== undefined ? edits.width : `${width}px`}
|
|
202
|
+
onChange={(e) => onEdit("width", e.target.value)}
|
|
203
|
+
className="text-[10px] font-mono text-white flex-1 text-right bg-transparent focus:outline-none"
|
|
204
|
+
/>
|
|
205
|
+
</div>
|
|
206
|
+
{/* H - Editable */}
|
|
207
|
+
<div className={cn(
|
|
208
|
+
"flex items-center gap-1 px-1.5 py-1 bg-white/5 rounded border",
|
|
209
|
+
heightEdited ? "border-[#00A3E1]" : "border-white/10"
|
|
210
|
+
)}>
|
|
211
|
+
<span className="text-[9px] text-gray-500 uppercase w-3">H</span>
|
|
212
|
+
<input
|
|
213
|
+
type="text"
|
|
214
|
+
value={edits.height !== undefined ? edits.height : `${height}px`}
|
|
215
|
+
onChange={(e) => onEdit("height", e.target.value)}
|
|
216
|
+
className="text-[10px] font-mono text-white flex-1 text-right bg-transparent focus:outline-none"
|
|
217
|
+
/>
|
|
218
|
+
</div>
|
|
219
|
+
</div>
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// ============================================
|
|
224
|
+
// FILL ROW (Read-only for now)
|
|
225
|
+
// ============================================
|
|
226
|
+
|
|
227
|
+
interface FillRowProps {
|
|
228
|
+
fill: { type: string; color?: string; opacity: number };
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function FillRow({ fill }: FillRowProps) {
|
|
232
|
+
const [visible, setVisible] = useState(true);
|
|
233
|
+
|
|
234
|
+
return (
|
|
235
|
+
<div className="flex items-center gap-1.5 py-0.5">
|
|
236
|
+
<button
|
|
237
|
+
onClick={() => setVisible(!visible)}
|
|
238
|
+
className="p-0.5 hover:bg-white/10 rounded"
|
|
239
|
+
>
|
|
240
|
+
{visible ? (
|
|
241
|
+
<Eye className="h-2.5 w-2.5 text-gray-500" />
|
|
242
|
+
) : (
|
|
243
|
+
<EyeOff className="h-2.5 w-2.5 text-gray-600" />
|
|
244
|
+
)}
|
|
245
|
+
</button>
|
|
246
|
+
{fill.type === "solid" && fill.color && (
|
|
247
|
+
<>
|
|
248
|
+
<div
|
|
249
|
+
className="w-4 h-4 rounded-sm border border-white/20 flex-shrink-0"
|
|
250
|
+
style={{ backgroundColor: fill.color }}
|
|
251
|
+
/>
|
|
252
|
+
<span className="text-[10px] font-mono text-gray-200 flex-1">{fill.color}</span>
|
|
253
|
+
<span className="text-[10px] text-gray-500">{Math.round(fill.opacity * 100)}%</span>
|
|
254
|
+
</>
|
|
255
|
+
)}
|
|
256
|
+
{fill.type === "gradient" && (
|
|
257
|
+
<>
|
|
258
|
+
<div className="w-4 h-4 rounded-sm border border-white/20 bg-gradient-to-r from-purple-500 to-pink-500 flex-shrink-0" />
|
|
259
|
+
<span className="text-[10px] text-gray-400 flex-1">Gradient</span>
|
|
260
|
+
</>
|
|
261
|
+
)}
|
|
262
|
+
{fill.type === "image" && (
|
|
263
|
+
<>
|
|
264
|
+
<div className="w-4 h-4 rounded-sm border border-white/20 bg-white/10 flex items-center justify-center flex-shrink-0">
|
|
265
|
+
<Layers className="h-2.5 w-2.5 text-gray-500" />
|
|
266
|
+
</div>
|
|
267
|
+
<span className="text-[10px] text-gray-400 flex-1">Image</span>
|
|
268
|
+
</>
|
|
269
|
+
)}
|
|
270
|
+
</div>
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// ============================================
|
|
275
|
+
// STROKE ROW (Read-only for now)
|
|
276
|
+
// ============================================
|
|
277
|
+
|
|
278
|
+
interface StrokeRowProps {
|
|
279
|
+
stroke: { color: string; width: string; style: string };
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function StrokeRow({ stroke }: StrokeRowProps) {
|
|
283
|
+
const [visible, setVisible] = useState(true);
|
|
284
|
+
|
|
285
|
+
return (
|
|
286
|
+
<div className="flex items-center gap-1.5 py-0.5">
|
|
287
|
+
<button
|
|
288
|
+
onClick={() => setVisible(!visible)}
|
|
289
|
+
className="p-0.5 hover:bg-white/10 rounded"
|
|
290
|
+
>
|
|
291
|
+
{visible ? (
|
|
292
|
+
<Eye className="h-2.5 w-2.5 text-gray-500" />
|
|
293
|
+
) : (
|
|
294
|
+
<EyeOff className="h-2.5 w-2.5 text-gray-600" />
|
|
295
|
+
)}
|
|
296
|
+
</button>
|
|
297
|
+
<div
|
|
298
|
+
className="w-4 h-4 rounded-sm border-2 flex-shrink-0"
|
|
299
|
+
style={{ borderColor: stroke.color }}
|
|
300
|
+
/>
|
|
301
|
+
<span className="text-[10px] font-mono text-gray-200">{stroke.color}</span>
|
|
302
|
+
<span className="text-[10px] text-gray-500">{stroke.width}</span>
|
|
303
|
+
</div>
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// ============================================
|
|
308
|
+
// MAIN PROPERTIES PANEL
|
|
309
|
+
// ============================================
|
|
310
|
+
|
|
311
|
+
export function PropertiesPanel({ element, styles, onPropertyClick, onSaveChanges }: PropertiesPanelProps) {
|
|
312
|
+
// Edit state
|
|
313
|
+
const [edits, setEdits] = useState<PropertyEdits>({});
|
|
314
|
+
const [isSaving, setIsSaving] = useState(false);
|
|
315
|
+
const [originalInlineStyles, setOriginalInlineStyles] = useState<Record<string, string>>({});
|
|
316
|
+
|
|
317
|
+
// Check if there are any changes
|
|
318
|
+
const hasChanges = useMemo(() => Object.keys(edits).length > 0, [edits]);
|
|
319
|
+
|
|
320
|
+
// Get the actual DOM element for live preview
|
|
321
|
+
const targetElement = useMemo(() => {
|
|
322
|
+
if (!element?.coordinates) return null;
|
|
323
|
+
const { x, y, width, height } = element.coordinates;
|
|
324
|
+
const centerX = x + width / 2;
|
|
325
|
+
const centerY = y + height / 2;
|
|
326
|
+
const el = document.elementFromPoint(centerX, centerY);
|
|
327
|
+
if (el?.closest('[data-sonance-devtools="true"]')) return null;
|
|
328
|
+
return el as HTMLElement | null;
|
|
329
|
+
}, [element]);
|
|
330
|
+
|
|
331
|
+
// Capture original inline styles when element changes
|
|
332
|
+
useEffect(() => {
|
|
333
|
+
if (targetElement) {
|
|
334
|
+
setOriginalInlineStyles({
|
|
335
|
+
width: targetElement.style.width,
|
|
336
|
+
height: targetElement.style.height,
|
|
337
|
+
opacity: targetElement.style.opacity,
|
|
338
|
+
borderRadius: targetElement.style.borderRadius,
|
|
339
|
+
fontSize: targetElement.style.fontSize,
|
|
340
|
+
fontWeight: targetElement.style.fontWeight,
|
|
341
|
+
lineHeight: targetElement.style.lineHeight,
|
|
342
|
+
letterSpacing: targetElement.style.letterSpacing,
|
|
343
|
+
color: targetElement.style.color,
|
|
344
|
+
backgroundColor: targetElement.style.backgroundColor,
|
|
345
|
+
padding: targetElement.style.padding,
|
|
346
|
+
margin: targetElement.style.margin,
|
|
347
|
+
gap: targetElement.style.gap,
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
// Clear edits when element changes
|
|
351
|
+
setEdits({});
|
|
352
|
+
}, [targetElement, element]);
|
|
353
|
+
|
|
354
|
+
// Apply live preview when edits change
|
|
355
|
+
useEffect(() => {
|
|
356
|
+
if (!targetElement) return;
|
|
357
|
+
|
|
358
|
+
// Apply each edited property
|
|
359
|
+
if (edits.width) targetElement.style.width = edits.width;
|
|
360
|
+
if (edits.height) targetElement.style.height = edits.height;
|
|
361
|
+
if (edits.opacity) targetElement.style.opacity = String(parseFloat(edits.opacity) / 100);
|
|
362
|
+
if (edits.borderRadius) targetElement.style.borderRadius = edits.borderRadius;
|
|
363
|
+
if (edits.fontSize) targetElement.style.fontSize = edits.fontSize;
|
|
364
|
+
if (edits.fontWeight) targetElement.style.fontWeight = edits.fontWeight;
|
|
365
|
+
if (edits.lineHeight) targetElement.style.lineHeight = edits.lineHeight;
|
|
366
|
+
if (edits.letterSpacing) targetElement.style.letterSpacing = edits.letterSpacing;
|
|
367
|
+
if (edits.color) targetElement.style.color = edits.color;
|
|
368
|
+
if (edits.backgroundColor) targetElement.style.backgroundColor = edits.backgroundColor;
|
|
369
|
+
if (edits.padding) targetElement.style.padding = edits.padding;
|
|
370
|
+
if (edits.margin) targetElement.style.margin = edits.margin;
|
|
371
|
+
if (edits.gap) targetElement.style.gap = edits.gap;
|
|
372
|
+
|
|
373
|
+
// Mark element as having preview
|
|
374
|
+
targetElement.setAttribute('data-sonance-preview', 'true');
|
|
375
|
+
}, [edits, targetElement]);
|
|
376
|
+
|
|
377
|
+
// Handle edit
|
|
378
|
+
const handleEdit = useCallback((key: keyof PropertyEdits, value: string) => {
|
|
379
|
+
setEdits(prev => ({ ...prev, [key]: value }));
|
|
380
|
+
}, []);
|
|
381
|
+
|
|
382
|
+
// Handle revert - restore original styles
|
|
383
|
+
const handleRevert = useCallback(() => {
|
|
384
|
+
if (!targetElement) return;
|
|
385
|
+
|
|
386
|
+
// Restore all original inline styles
|
|
387
|
+
Object.entries(originalInlineStyles).forEach(([key, value]) => {
|
|
388
|
+
(targetElement.style as unknown as Record<string, string>)[key] = value;
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
// Remove preview marker
|
|
392
|
+
targetElement.removeAttribute('data-sonance-preview');
|
|
393
|
+
|
|
394
|
+
// Clear edits
|
|
395
|
+
setEdits({});
|
|
396
|
+
}, [targetElement, originalInlineStyles]);
|
|
397
|
+
|
|
398
|
+
// Handle save - call API to persist changes
|
|
399
|
+
const handleSave = useCallback(async () => {
|
|
400
|
+
if (!element || !hasChanges) return;
|
|
401
|
+
|
|
402
|
+
setIsSaving(true);
|
|
403
|
+
try {
|
|
404
|
+
// Call the parent's save handler which will trigger the AI-assisted save
|
|
405
|
+
onSaveChanges?.(edits, element);
|
|
406
|
+
|
|
407
|
+
// Clear edits after initiating save (the parent handles the actual persistence)
|
|
408
|
+
setEdits({});
|
|
409
|
+
} finally {
|
|
410
|
+
setIsSaving(false);
|
|
411
|
+
}
|
|
412
|
+
}, [element, edits, hasChanges, onSaveChanges]);
|
|
413
|
+
|
|
414
|
+
// Empty state
|
|
415
|
+
if (!element || !styles) {
|
|
416
|
+
return (
|
|
417
|
+
<div className="flex flex-col items-center justify-center py-8 text-center">
|
|
418
|
+
<div className="w-10 h-10 rounded-full bg-white/10 flex items-center justify-center mb-3">
|
|
419
|
+
<Box className="h-5 w-5 text-gray-500" />
|
|
420
|
+
</div>
|
|
421
|
+
<p className="text-[11px] text-gray-400">
|
|
422
|
+
Click an element to inspect
|
|
423
|
+
</p>
|
|
424
|
+
</div>
|
|
425
|
+
);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
return (
|
|
429
|
+
<div className="text-gray-200 flex flex-col h-full">
|
|
430
|
+
<div className="flex-1 overflow-y-auto">
|
|
431
|
+
{/* Header - Element Name */}
|
|
432
|
+
<div className="px-2 py-2 border-b border-white/10 bg-white/5">
|
|
433
|
+
<div className="flex items-center gap-1.5">
|
|
434
|
+
<div className="w-5 h-5 rounded bg-purple-500/20 flex items-center justify-center">
|
|
435
|
+
<Box className="h-3 w-3 text-purple-400" />
|
|
436
|
+
</div>
|
|
437
|
+
<div className="flex-1 min-w-0">
|
|
438
|
+
<p className="text-[11px] font-semibold text-white truncate">
|
|
439
|
+
{element.name}
|
|
440
|
+
</p>
|
|
441
|
+
{element.variantId && (
|
|
442
|
+
<p className="text-[9px] font-mono text-gray-500 truncate">
|
|
443
|
+
#{element.variantId.substring(0, 8)}
|
|
444
|
+
</p>
|
|
445
|
+
)}
|
|
446
|
+
</div>
|
|
447
|
+
<span className="text-[9px] px-1.5 py-0.5 rounded bg-white/10 text-gray-400 uppercase">
|
|
448
|
+
{styles.tagName}
|
|
449
|
+
</span>
|
|
450
|
+
</div>
|
|
451
|
+
{styles.textContent && (
|
|
452
|
+
<p className="mt-1.5 text-[10px] text-gray-400 truncate px-0.5">
|
|
453
|
+
“{styles.textContent}”
|
|
454
|
+
</p>
|
|
455
|
+
)}
|
|
456
|
+
</div>
|
|
457
|
+
|
|
458
|
+
{/* Layout Section - Always shown */}
|
|
459
|
+
<Section
|
|
460
|
+
title="Layout"
|
|
461
|
+
icon={<Grid3X3 className="h-3 w-3" />}
|
|
462
|
+
defaultOpen={true}
|
|
463
|
+
>
|
|
464
|
+
<div className="space-y-2">
|
|
465
|
+
<EditableDimensionGrid
|
|
466
|
+
x={styles.geometry.x}
|
|
467
|
+
y={styles.geometry.y}
|
|
468
|
+
width={styles.geometry.width}
|
|
469
|
+
height={styles.geometry.height}
|
|
470
|
+
edits={edits}
|
|
471
|
+
onEdit={handleEdit}
|
|
472
|
+
/>
|
|
473
|
+
</div>
|
|
474
|
+
</Section>
|
|
475
|
+
|
|
476
|
+
{/* Appearance Section - Always shown */}
|
|
477
|
+
<Section
|
|
478
|
+
title="Appearance"
|
|
479
|
+
icon={<CircleDot className="h-3 w-3" />}
|
|
480
|
+
defaultOpen={true}
|
|
481
|
+
>
|
|
482
|
+
<div className="space-y-0.5">
|
|
483
|
+
<EditablePropertyRow
|
|
484
|
+
label="Opacity"
|
|
485
|
+
value={Math.round(styles.opacity)}
|
|
486
|
+
unit="%"
|
|
487
|
+
editKey="opacity"
|
|
488
|
+
edits={edits}
|
|
489
|
+
onEdit={handleEdit}
|
|
490
|
+
/>
|
|
491
|
+
<EditablePropertyRow
|
|
492
|
+
label="Radius"
|
|
493
|
+
value={styles.borderRadius}
|
|
494
|
+
editKey="borderRadius"
|
|
495
|
+
edits={edits}
|
|
496
|
+
onEdit={handleEdit}
|
|
497
|
+
/>
|
|
498
|
+
</div>
|
|
499
|
+
</Section>
|
|
500
|
+
|
|
501
|
+
{/* Typography Section - Only for text elements */}
|
|
502
|
+
{styles.typography && (
|
|
503
|
+
<Section
|
|
504
|
+
title="Typography"
|
|
505
|
+
icon={<Type className="h-3 w-3" />}
|
|
506
|
+
defaultOpen={true}
|
|
507
|
+
>
|
|
508
|
+
<div className="space-y-0.5">
|
|
509
|
+
<EditablePropertyRow
|
|
510
|
+
label="Font"
|
|
511
|
+
value={styles.typography.fontFamily}
|
|
512
|
+
readOnly
|
|
513
|
+
/>
|
|
514
|
+
<div className="grid grid-cols-2 gap-x-2">
|
|
515
|
+
<EditablePropertyRow
|
|
516
|
+
label="Size"
|
|
517
|
+
value={styles.typography.fontSize}
|
|
518
|
+
editKey="fontSize"
|
|
519
|
+
edits={edits}
|
|
520
|
+
onEdit={handleEdit}
|
|
521
|
+
/>
|
|
522
|
+
<EditablePropertyRow
|
|
523
|
+
label="Weight"
|
|
524
|
+
value={styles.typography.fontWeight}
|
|
525
|
+
editKey="fontWeight"
|
|
526
|
+
edits={edits}
|
|
527
|
+
onEdit={handleEdit}
|
|
528
|
+
/>
|
|
529
|
+
</div>
|
|
530
|
+
<EditablePropertyRow
|
|
531
|
+
label="Line H"
|
|
532
|
+
value={styles.typography.lineHeight}
|
|
533
|
+
editKey="lineHeight"
|
|
534
|
+
edits={edits}
|
|
535
|
+
onEdit={handleEdit}
|
|
536
|
+
/>
|
|
537
|
+
<EditablePropertyRow
|
|
538
|
+
label="Letter"
|
|
539
|
+
value={styles.typography.letterSpacing}
|
|
540
|
+
editKey="letterSpacing"
|
|
541
|
+
edits={edits}
|
|
542
|
+
onEdit={handleEdit}
|
|
543
|
+
/>
|
|
544
|
+
<EditablePropertyRow
|
|
545
|
+
label="Color"
|
|
546
|
+
value={styles.typography.color}
|
|
547
|
+
color={styles.typography.color !== "transparent" ? styles.typography.color : undefined}
|
|
548
|
+
editKey="color"
|
|
549
|
+
edits={edits}
|
|
550
|
+
onEdit={handleEdit}
|
|
551
|
+
/>
|
|
552
|
+
</div>
|
|
553
|
+
</Section>
|
|
554
|
+
)}
|
|
555
|
+
|
|
556
|
+
{/* Fill Section - Only show if has fills */}
|
|
557
|
+
{styles.fills.length > 0 && (
|
|
558
|
+
<Section
|
|
559
|
+
title="Fill"
|
|
560
|
+
icon={<Square className="h-3 w-3" />}
|
|
561
|
+
defaultOpen={true}
|
|
562
|
+
>
|
|
563
|
+
<div className="space-y-0.5">
|
|
564
|
+
{styles.fills.map((fill, i) => (
|
|
565
|
+
<FillRow key={i} fill={fill} />
|
|
566
|
+
))}
|
|
567
|
+
</div>
|
|
568
|
+
</Section>
|
|
569
|
+
)}
|
|
570
|
+
|
|
571
|
+
{/* Stroke Section - Only show if has strokes */}
|
|
572
|
+
{styles.strokes.length > 0 && (
|
|
573
|
+
<Section
|
|
574
|
+
title="Stroke"
|
|
575
|
+
icon={<Minus className="h-3 w-3" />}
|
|
576
|
+
defaultOpen={true}
|
|
577
|
+
>
|
|
578
|
+
<div className="space-y-0.5">
|
|
579
|
+
{styles.strokes.map((stroke, i) => (
|
|
580
|
+
<StrokeRow key={i} stroke={stroke} />
|
|
581
|
+
))}
|
|
582
|
+
</div>
|
|
583
|
+
</Section>
|
|
584
|
+
)}
|
|
585
|
+
|
|
586
|
+
{/* Effects Section - Only show if has effects */}
|
|
587
|
+
{styles.effects.length > 0 && (
|
|
588
|
+
<Section
|
|
589
|
+
title="Effects"
|
|
590
|
+
icon={<Layers className="h-3 w-3" />}
|
|
591
|
+
defaultOpen={true}
|
|
592
|
+
>
|
|
593
|
+
<div className="space-y-0.5">
|
|
594
|
+
{styles.effects.map((effect, i) => (
|
|
595
|
+
<div key={i} className="flex items-center gap-1.5 py-0.5">
|
|
596
|
+
<Eye className="h-2.5 w-2.5 text-gray-500" />
|
|
597
|
+
<span className="text-[10px] text-gray-400 capitalize">{effect.type}</span>
|
|
598
|
+
<span className="text-[9px] font-mono text-gray-500 truncate flex-1">
|
|
599
|
+
{effect.value.substring(0, 30)}...
|
|
600
|
+
</span>
|
|
601
|
+
</div>
|
|
602
|
+
))}
|
|
603
|
+
</div>
|
|
604
|
+
</Section>
|
|
605
|
+
)}
|
|
606
|
+
|
|
607
|
+
{/* Spacing Section - Always shown */}
|
|
608
|
+
<Section
|
|
609
|
+
title="Spacing"
|
|
610
|
+
icon={<CornerDownRight className="h-3 w-3" />}
|
|
611
|
+
defaultOpen={false}
|
|
612
|
+
>
|
|
613
|
+
<div className="space-y-0.5">
|
|
614
|
+
<EditablePropertyRow
|
|
615
|
+
label="Padding"
|
|
616
|
+
value={styles.padding}
|
|
617
|
+
editKey="padding"
|
|
618
|
+
edits={edits}
|
|
619
|
+
onEdit={handleEdit}
|
|
620
|
+
/>
|
|
621
|
+
<EditablePropertyRow
|
|
622
|
+
label="Margin"
|
|
623
|
+
value={styles.margin}
|
|
624
|
+
editKey="margin"
|
|
625
|
+
edits={edits}
|
|
626
|
+
onEdit={handleEdit}
|
|
627
|
+
/>
|
|
628
|
+
{styles.gap !== "normal" && (
|
|
629
|
+
<EditablePropertyRow
|
|
630
|
+
label="Gap"
|
|
631
|
+
value={styles.gap}
|
|
632
|
+
editKey="gap"
|
|
633
|
+
edits={edits}
|
|
634
|
+
onEdit={handleEdit}
|
|
635
|
+
/>
|
|
636
|
+
)}
|
|
637
|
+
</div>
|
|
638
|
+
</Section>
|
|
639
|
+
|
|
640
|
+
{/* Flexbox Section - only if flex */}
|
|
641
|
+
{(styles.display === "flex" || styles.display === "inline-flex") && (
|
|
642
|
+
<Section
|
|
643
|
+
title="Flexbox"
|
|
644
|
+
icon={<Palette className="h-3 w-3" />}
|
|
645
|
+
defaultOpen={false}
|
|
646
|
+
>
|
|
647
|
+
<div className="space-y-0.5">
|
|
648
|
+
<EditablePropertyRow
|
|
649
|
+
label="Direction"
|
|
650
|
+
value={styles.flexDirection}
|
|
651
|
+
readOnly
|
|
652
|
+
/>
|
|
653
|
+
<EditablePropertyRow
|
|
654
|
+
label="Align"
|
|
655
|
+
value={styles.alignItems}
|
|
656
|
+
readOnly
|
|
657
|
+
/>
|
|
658
|
+
<EditablePropertyRow
|
|
659
|
+
label="Justify"
|
|
660
|
+
value={styles.justifyContent}
|
|
661
|
+
readOnly
|
|
662
|
+
/>
|
|
663
|
+
</div>
|
|
664
|
+
</Section>
|
|
665
|
+
)}
|
|
666
|
+
</div>
|
|
667
|
+
|
|
668
|
+
{/* Save/Revert Footer - Only show when there are changes */}
|
|
669
|
+
{hasChanges && (
|
|
670
|
+
<div className="sticky bottom-0 px-2 py-2 bg-[#1a1a1a] border-t border-white/10 flex gap-2">
|
|
671
|
+
<button
|
|
672
|
+
onClick={handleRevert}
|
|
673
|
+
disabled={isSaving}
|
|
674
|
+
className="flex-1 flex items-center justify-center gap-1.5 px-3 py-1.5 text-[11px] font-medium rounded bg-white/10 text-gray-300 hover:bg-white/20 disabled:opacity-50 transition-colors"
|
|
675
|
+
>
|
|
676
|
+
<RotateCcw className="h-3 w-3" />
|
|
677
|
+
Revert
|
|
678
|
+
</button>
|
|
679
|
+
<button
|
|
680
|
+
onClick={handleSave}
|
|
681
|
+
disabled={isSaving}
|
|
682
|
+
className="flex-1 flex items-center justify-center gap-1.5 px-3 py-1.5 text-[11px] font-medium rounded bg-[#00A3E1] text-white hover:bg-[#00A3E1]/80 disabled:opacity-50 transition-colors"
|
|
683
|
+
>
|
|
684
|
+
{isSaving ? (
|
|
685
|
+
<Loader2 className="h-3 w-3 animate-spin" />
|
|
686
|
+
) : (
|
|
687
|
+
<Save className="h-3 w-3" />
|
|
688
|
+
)}
|
|
689
|
+
Save Changes
|
|
690
|
+
</button>
|
|
691
|
+
</div>
|
|
692
|
+
)}
|
|
693
|
+
</div>
|
|
694
|
+
);
|
|
695
|
+
}
|