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.
Files changed (84) hide show
  1. package/dist/assets/api/sonance-ai-edit/route.ts +30 -7
  2. package/dist/assets/api/sonance-save-image/route.ts +625 -0
  3. package/dist/assets/api/sonance-vision-apply/image-styling-detection.ts +1360 -0
  4. package/dist/assets/api/sonance-vision-apply/route.ts +1020 -64
  5. package/dist/assets/api/sonance-vision-apply/styling-detection.ts +730 -0
  6. package/dist/assets/api/sonance-vision-apply/theme-discovery.ts +1 -1
  7. package/dist/assets/api/sonance-vision-edit/route.ts +33 -8
  8. package/dist/assets/brand-system.ts +13 -12
  9. package/dist/assets/components/accordion.tsx +15 -7
  10. package/dist/assets/components/alert-dialog.tsx +35 -10
  11. package/dist/assets/components/alert.tsx +11 -10
  12. package/dist/assets/components/avatar.tsx +4 -4
  13. package/dist/assets/components/badge.tsx +16 -12
  14. package/dist/assets/components/button.stories.tsx +3 -3
  15. package/dist/assets/components/button.tsx +50 -31
  16. package/dist/assets/components/calendar.tsx +12 -8
  17. package/dist/assets/components/card.tsx +35 -29
  18. package/dist/assets/components/checkbox.tsx +9 -8
  19. package/dist/assets/components/code.tsx +19 -11
  20. package/dist/assets/components/command.tsx +32 -13
  21. package/dist/assets/components/context-menu.tsx +37 -16
  22. package/dist/assets/components/dialog.tsx +8 -5
  23. package/dist/assets/components/divider.tsx +15 -5
  24. package/dist/assets/components/drawer.tsx +4 -3
  25. package/dist/assets/components/dropdown-menu.tsx +15 -13
  26. package/dist/assets/components/hover-card.tsx +4 -1
  27. package/dist/assets/components/image.tsx +1 -1
  28. package/dist/assets/components/input.tsx +29 -14
  29. package/dist/assets/components/kbd.stories.tsx +3 -3
  30. package/dist/assets/components/kbd.tsx +29 -13
  31. package/dist/assets/components/listbox.tsx +8 -8
  32. package/dist/assets/components/menubar.tsx +50 -23
  33. package/dist/assets/components/navbar.stories.tsx +140 -13
  34. package/dist/assets/components/navbar.tsx +22 -5
  35. package/dist/assets/components/navigation-menu.tsx +28 -6
  36. package/dist/assets/components/pagination.tsx +10 -10
  37. package/dist/assets/components/popover.tsx +10 -8
  38. package/dist/assets/components/progress.tsx +6 -4
  39. package/dist/assets/components/radio-group.tsx +5 -5
  40. package/dist/assets/components/select.tsx +49 -29
  41. package/dist/assets/components/separator.tsx +3 -3
  42. package/dist/assets/components/sheet.tsx +4 -4
  43. package/dist/assets/components/sidebar.tsx +10 -10
  44. package/dist/assets/components/skeleton.tsx +13 -5
  45. package/dist/assets/components/slider.tsx +12 -10
  46. package/dist/assets/components/switch.tsx +4 -4
  47. package/dist/assets/components/table.tsx +5 -5
  48. package/dist/assets/components/tabs.tsx +8 -8
  49. package/dist/assets/components/textarea.tsx +11 -9
  50. package/dist/assets/components/toast.tsx +7 -7
  51. package/dist/assets/components/toggle.tsx +27 -7
  52. package/dist/assets/components/tooltip.tsx +10 -8
  53. package/dist/assets/components/user.tsx +8 -6
  54. package/dist/assets/dev-tools/SonanceDevTools.tsx +851 -708
  55. package/dist/assets/dev-tools/components/ApplyFirstPreview.tsx +10 -10
  56. package/dist/assets/dev-tools/components/ChatHistory.tsx +145 -0
  57. package/dist/assets/dev-tools/components/ChatInterface.tsx +444 -295
  58. package/dist/assets/dev-tools/components/ChatTabBar.tsx +82 -0
  59. package/dist/assets/dev-tools/components/DiffPreview.tsx +1 -1
  60. package/dist/assets/dev-tools/components/InlineDiffPreview.tsx +528 -0
  61. package/dist/assets/dev-tools/components/InspectorOverlay.tsx +21 -18
  62. package/dist/assets/dev-tools/components/PropertiesPanel.tsx +1345 -0
  63. package/dist/assets/dev-tools/components/ScreenshotAnnotator.tsx +1 -1
  64. package/dist/assets/dev-tools/components/SectionHighlight.tsx +1 -1
  65. package/dist/assets/dev-tools/components/VisionDiffPreview.tsx +7 -7
  66. package/dist/assets/dev-tools/components/VisionModeBorder.tsx +12 -63
  67. package/dist/assets/dev-tools/constants.ts +38 -6
  68. package/dist/assets/dev-tools/hooks/index.ts +69 -0
  69. package/dist/assets/dev-tools/hooks/useComponentDetection.ts +132 -0
  70. package/dist/assets/dev-tools/hooks/useComputedStyles.ts +471 -0
  71. package/dist/assets/dev-tools/hooks/useContentHash.ts +212 -0
  72. package/dist/assets/dev-tools/hooks/useElementScanner.ts +398 -0
  73. package/dist/assets/dev-tools/hooks/useImageDetection.ts +162 -0
  74. package/dist/assets/dev-tools/hooks/useTextDetection.ts +217 -0
  75. package/dist/assets/dev-tools/index.ts +3 -0
  76. package/dist/assets/dev-tools/panels/AnalysisPanel.tsx +32 -32
  77. package/dist/assets/dev-tools/panels/ComponentsPanel.tsx +384 -131
  78. package/dist/assets/dev-tools/panels/TextPanel.tsx +10 -10
  79. package/dist/assets/dev-tools/types.ts +93 -2
  80. package/dist/assets/globals.css +225 -9
  81. package/dist/assets/styles/brand-overrides.css +3 -2
  82. package/dist/assets/utils.ts +2 -1
  83. package/dist/index.js +22 -3
  84. package/package.json +2 -1
@@ -0,0 +1,1345 @@
1
+ "use client";
2
+
3
+ import React, { useState, useEffect, useCallback, useMemo, useRef } 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
+ Pencil,
22
+ Image as ImageIcon,
23
+ Upload,
24
+ Link,
25
+ } from "lucide-react";
26
+ import { useTheme } from "next-themes";
27
+ import { cn } from "../../../lib/utils";
28
+ import { ComputedStyles } from "../hooks/useComputedStyles";
29
+ import { VisionFocusedElement, ApplyFirstSession, ImageOverride, PublicImageAsset, LogoAsset } from "../types";
30
+
31
+ // ============================================
32
+ // TYPES
33
+ // ============================================
34
+
35
+ export interface PropertyEdits {
36
+ // Content
37
+ textContent?: string;
38
+ // Layout
39
+ width?: string;
40
+ height?: string;
41
+ // Appearance
42
+ opacity?: string;
43
+ borderRadius?: string;
44
+ // Typography
45
+ fontSize?: string;
46
+ fontWeight?: string;
47
+ lineHeight?: string;
48
+ letterSpacing?: string;
49
+ color?: string; // Legacy/universal color
50
+ colorLight?: string; // Color for light mode only
51
+ colorDark?: string; // Color for dark mode only
52
+ // Fill
53
+ backgroundColor?: string;
54
+ // Spacing
55
+ padding?: string;
56
+ margin?: string;
57
+ gap?: string;
58
+ }
59
+
60
+ interface PropertiesPanelProps {
61
+ element: VisionFocusedElement | null;
62
+ styles: ComputedStyles | null;
63
+ onPropertyClick?: (property: string, value: string) => void;
64
+ onSaveChanges?: (edits: PropertyEdits, element: VisionFocusedElement) => Promise<void>;
65
+ // Text change session props (shown in Details tab for text-only changes)
66
+ textChangeSession?: ApplyFirstSession | null;
67
+ onTextChangeAccept?: () => void;
68
+ onTextChangeRevert?: () => void;
69
+ // Image editing props
70
+ imageOverride?: ImageOverride;
71
+ onImageOverrideChange?: (override: ImageOverride) => void;
72
+ onSaveImageChanges?: () => void;
73
+ isImageSaving?: boolean;
74
+ publicImages?: PublicImageAsset[];
75
+ publicImagesByFolder?: Record<string, PublicImageAsset[]>;
76
+ logoAssets?: LogoAsset[];
77
+ logoAssetsByBrand?: Record<string, LogoAsset[]>;
78
+ onImageUpload?: (file: File) => Promise<string | null>;
79
+ }
80
+
81
+ interface SectionProps {
82
+ title: string;
83
+ icon: React.ReactNode;
84
+ defaultOpen?: boolean;
85
+ readOnly?: boolean;
86
+ children: React.ReactNode;
87
+ }
88
+
89
+ // ============================================
90
+ // SECTION COMPONENT
91
+ // ============================================
92
+
93
+ function Section({ title, icon, defaultOpen = true, readOnly = false, children }: SectionProps) {
94
+ const [isOpen, setIsOpen] = useState(defaultOpen);
95
+
96
+ return (
97
+ <div className="border-b border-gray-200 dark:border-white/10 last:border-b-0">
98
+ <button
99
+ onClick={() => setIsOpen(!isOpen)}
100
+ className="w-full flex items-center justify-between px-2 py-1.5 hover:bg-gray-100 dark:hover:bg-white/5 transition-colors"
101
+ >
102
+ <div className="flex items-center gap-1.5 text-[11px] font-medium text-gray-700 dark:text-gray-200">
103
+ {icon}
104
+ <span id="section-span-title">{title}</span>
105
+ {readOnly && (
106
+ <span id="section-span-readonly" className="text-[8px] px-1 py-0.5 rounded bg-gray-100 dark:bg-white/5 text-gray-500 uppercase tracking-wider">
107
+ read-only
108
+ </span>
109
+ )}
110
+ </div>
111
+ <div className="flex items-center gap-1">
112
+ {isOpen ? (
113
+ <ChevronDown className="h-3 w-3 text-gray-400 dark:text-gray-500" />
114
+ ) : (
115
+ <ChevronRight className="h-3 w-3 text-gray-400 dark:text-gray-500" />
116
+ )}
117
+ </div>
118
+ </button>
119
+ {isOpen && <div className="px-2 pb-2">{children}</div>}
120
+ </div>
121
+ );
122
+ }
123
+
124
+ // ============================================
125
+ // EDITABLE PROPERTY ROW
126
+ // ============================================
127
+
128
+ interface EditablePropertyRowProps {
129
+ label: string;
130
+ value: string | number;
131
+ unit?: string;
132
+ color?: string;
133
+ editKey?: keyof PropertyEdits;
134
+ edits?: PropertyEdits;
135
+ onEdit?: (key: keyof PropertyEdits, value: string) => void;
136
+ readOnly?: boolean;
137
+ inputType?: "text" | "number" | "color";
138
+ options?: { label: string; value: string }[];
139
+ }
140
+
141
+ function EditablePropertyRow({
142
+ label,
143
+ value,
144
+ unit,
145
+ color,
146
+ editKey,
147
+ edits,
148
+ onEdit,
149
+ readOnly = false,
150
+ inputType = "text",
151
+ options,
152
+ }: EditablePropertyRowProps) {
153
+ const isEditable = !readOnly && editKey && edits && onEdit;
154
+ const editedValue = editKey && edits ? edits[editKey] : undefined;
155
+ const displayValue = editedValue !== undefined ? editedValue : String(value);
156
+ const isEdited = editedValue !== undefined && editedValue !== String(value);
157
+
158
+ const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
159
+ if (isEditable && editKey) {
160
+ onEdit(editKey, e.target.value);
161
+ }
162
+ };
163
+
164
+ return (
165
+ <div className={cn(
166
+ "flex items-center justify-between py-0.5 text-[10px] group",
167
+ isEditable && "hover:bg-gray-100 dark:hover:bg-white/5 rounded px-1 -mx-1"
168
+ )}>
169
+ <span id="editable-property-row-span-label" className={cn(
170
+ "uppercase tracking-wide",
171
+ isEditable ? "text-gray-500 dark:text-gray-400" : "text-gray-400 dark:text-gray-500"
172
+ )}>{label}</span>
173
+ <div className="flex items-center gap-1">
174
+ {color && (
175
+ <div
176
+ className="w-3 h-3 rounded-sm border border-gray-300 dark:border-white/20 relative"
177
+ style={{ backgroundColor: color }}
178
+ >
179
+ {isEditable && inputType === "color" && (
180
+ <input
181
+ type="color"
182
+ value={displayValue.length === 7 ? displayValue : "#000000"}
183
+ onChange={handleChange}
184
+ className="absolute inset-0 opacity-0 w-full h-full cursor-pointer"
185
+ />
186
+ )}
187
+ </div>
188
+ )}
189
+ {isEditable ? (
190
+ options ? (
191
+ <div className="relative group/select">
192
+ <select
193
+ value={displayValue}
194
+ onChange={handleChange}
195
+ className={cn(
196
+ "w-20 px-1.5 py-0.5 text-right font-mono text-[10px] bg-gray-100 dark:bg-white/10 border rounded text-gray-800 dark:text-white focus:outline-none focus:ring-1 focus:ring-[#00A3E1] hover:border-[#00A3E1]/50 transition-colors cursor-pointer appearance-none",
197
+ isEdited ? "border-[#00A3E1]" : "border-gray-300 dark:border-white/20"
198
+ )}
199
+ >
200
+ {!options.some(o => o.value === displayValue) && (
201
+ <option value={displayValue}>{displayValue}</option>
202
+ )}
203
+ {options.map((opt) => (
204
+ <option key={opt.value} value={opt.value}>
205
+ {opt.label}
206
+ </option>
207
+ ))}
208
+ </select>
209
+ </div>
210
+ ) : (
211
+ <>
212
+ <Pencil className="h-2.5 w-2.5 text-gray-400 dark:text-gray-600 group-hover:text-[#00A3E1] transition-colors" />
213
+ <input
214
+ type={inputType === "color" ? "text" : inputType}
215
+ value={displayValue}
216
+ onChange={handleChange}
217
+ className={cn(
218
+ "w-20 px-1.5 py-0.5 text-right font-mono text-[10px] bg-gray-100 dark:bg-white/10 border rounded text-gray-800 dark:text-white focus:outline-none focus:ring-1 focus:ring-[#00A3E1] hover:border-[#00A3E1]/50 transition-colors cursor-text",
219
+ isEdited ? "border-[#00A3E1]" : "border-gray-300 dark:border-white/20"
220
+ )}
221
+ />
222
+ </>
223
+ )
224
+ ) : (
225
+ <span id="editable-property-row-span-displayvalue" className="font-mono text-gray-600 dark:text-gray-400 cursor-default">
226
+ {displayValue}
227
+ </span>
228
+ )}
229
+ {unit && <span id="editable-property-row-span-unit" className="text-gray-500 ml-0.5">{unit}</span>}
230
+ </div>
231
+ </div>
232
+ );
233
+ }
234
+
235
+ // ============================================
236
+ // EDITABLE DIMENSION GRID
237
+ // ============================================
238
+
239
+ interface EditableDimensionGridProps {
240
+ x: number;
241
+ y: number;
242
+ width: number;
243
+ height: number;
244
+ edits: PropertyEdits;
245
+ onEdit: (key: keyof PropertyEdits, value: string) => void;
246
+ }
247
+
248
+ function EditableDimensionGrid({ x, y, width, height, edits, onEdit }: EditableDimensionGridProps) {
249
+ const widthEdited = edits.width !== undefined && edits.width !== `${width}px`;
250
+ const heightEdited = edits.height !== undefined && edits.height !== `${height}px`;
251
+
252
+ return (
253
+ <div className="grid grid-cols-2 gap-1 overflow-hidden">
254
+ {/* X - Read only */}
255
+ <div className="flex items-center gap-1 px-1.5 py-1 bg-gray-50 dark:bg-white/5 rounded border border-transparent min-w-0 cursor-default">
256
+ <span id="editable-dimension-grid-span-x" className="text-[9px] text-gray-500 dark:text-gray-600 uppercase flex-shrink-0">X</span>
257
+ <span id="editable-dimension-grid-span-x" className="text-[10px] font-mono text-gray-600 dark:text-gray-400 text-right truncate flex-1 min-w-0">{x}</span>
258
+ </div>
259
+ {/* Y - Read only */}
260
+ <div className="flex items-center gap-1 px-1.5 py-1 bg-gray-50 dark:bg-white/5 rounded border border-transparent min-w-0 cursor-default">
261
+ <span id="editable-dimension-grid-span-y" className="text-[9px] text-gray-500 dark:text-gray-600 uppercase flex-shrink-0">Y</span>
262
+ <span id="editable-dimension-grid-span-y" className="text-[10px] font-mono text-gray-600 dark:text-gray-400 text-right truncate flex-1 min-w-0">{y}</span>
263
+ </div>
264
+ {/* W - Editable */}
265
+ <div className={cn(
266
+ "group flex items-center gap-1 px-1.5 py-1 bg-gray-100 dark:bg-white/10 rounded border min-w-0 hover:border-[#00A3E1]/50 transition-colors cursor-text",
267
+ widthEdited ? "border-[#00A3E1]" : "border-gray-300 dark:border-white/20"
268
+ )}>
269
+ <Pencil className="h-2 w-2 text-gray-400 dark:text-gray-600 group-hover:text-[#00A3E1] transition-colors flex-shrink-0" />
270
+ <span id="editable-dimension-grid-span-w" className="text-[9px] text-gray-500 dark:text-gray-400 uppercase flex-shrink-0">W</span>
271
+ <input
272
+ type="text"
273
+ value={edits.width !== undefined ? edits.width : `${width}px`}
274
+ onChange={(e) => onEdit("width", e.target.value)}
275
+ className="text-[10px] font-mono text-gray-800 dark:text-white text-right bg-transparent focus:outline-none w-full min-w-0 cursor-text"
276
+ />
277
+ </div>
278
+ {/* H - Editable */}
279
+ <div className={cn(
280
+ "group flex items-center gap-1 px-1.5 py-1 bg-gray-100 dark:bg-white/10 rounded border min-w-0 hover:border-[#00A3E1]/50 transition-colors cursor-text",
281
+ heightEdited ? "border-[#00A3E1]" : "border-gray-300 dark:border-white/20"
282
+ )}>
283
+ <Pencil className="h-2 w-2 text-gray-400 dark:text-gray-600 group-hover:text-[#00A3E1] transition-colors flex-shrink-0" />
284
+ <span id="editable-dimension-grid-span-h" className="text-[9px] text-gray-500 dark:text-gray-400 uppercase flex-shrink-0">H</span>
285
+ <input
286
+ type="text"
287
+ value={edits.height !== undefined ? edits.height : `${height}px`}
288
+ onChange={(e) => onEdit("height", e.target.value)}
289
+ className="text-[10px] font-mono text-gray-800 dark:text-white text-right bg-transparent focus:outline-none w-full min-w-0 cursor-text"
290
+ />
291
+ </div>
292
+ </div>
293
+ );
294
+ }
295
+
296
+ // ============================================
297
+ // FILL ROW (Read-only for now)
298
+ // ============================================
299
+
300
+ interface FillRowProps {
301
+ fill: { type: string; color?: string; opacity: number };
302
+ }
303
+
304
+ function FillRow({ fill }: FillRowProps) {
305
+ const [visible, setVisible] = useState(true);
306
+
307
+ return (
308
+ <div className="flex items-center gap-1.5 py-0.5">
309
+ <button
310
+ onClick={() => setVisible(!visible)}
311
+ className="p-0.5 hover:bg-gray-100 dark:hover:bg-white/10 rounded"
312
+ >
313
+ {visible ? (
314
+ <Eye className="h-2.5 w-2.5 text-gray-400 dark:text-gray-500" />
315
+ ) : (
316
+ <EyeOff className="h-2.5 w-2.5 text-gray-500 dark:text-gray-600" />
317
+ )}
318
+ </button>
319
+ {fill.type === "solid" && fill.color && (
320
+ <>
321
+ <div
322
+ className="w-4 h-4 rounded-sm border border-gray-300 dark:border-white/20 flex-shrink-0"
323
+ style={{ backgroundColor: fill.color }}
324
+ />
325
+ <span id="fill-row-span-fillcolor" className="text-[10px] font-mono text-gray-700 dark:text-gray-200 flex-1">{fill.color}</span>
326
+ <span id="fill-row-span-mathroundfillopacity" className="text-[10px] text-gray-500">{Math.round(fill.opacity * 100)}%</span>
327
+ </>
328
+ )}
329
+ {fill.type === "gradient" && (
330
+ <>
331
+ <div className="w-4 h-4 rounded-sm border border-gray-300 dark:border-white/20 bg-gradient-to-r from-purple-500 to-pink-500 flex-shrink-0" />
332
+ <span id="fill-row-span-gradient" className="text-[10px] text-gray-500 dark:text-gray-400 flex-1">Gradient</span>
333
+ </>
334
+ )}
335
+ {fill.type === "image" && (
336
+ <>
337
+ <div className="w-4 h-4 rounded-sm border border-gray-300 dark:border-white/20 bg-gray-100 dark:bg-white/10 flex items-center justify-center flex-shrink-0">
338
+ <Layers className="h-2.5 w-2.5 text-gray-400 dark:text-gray-500" />
339
+ </div>
340
+ <span id="fill-row-span-image" className="text-[10px] text-gray-500 dark:text-gray-400 flex-1">Image</span>
341
+ </>
342
+ )}
343
+ </div>
344
+ );
345
+ }
346
+
347
+ // ============================================
348
+ // STROKE ROW (Read-only for now)
349
+ // ============================================
350
+
351
+ interface StrokeRowProps {
352
+ stroke: { color: string; width: string; style: string };
353
+ }
354
+
355
+ function StrokeRow({ stroke }: StrokeRowProps) {
356
+ const [visible, setVisible] = useState(true);
357
+
358
+ return (
359
+ <div className="flex items-center gap-1.5 py-0.5">
360
+ <button
361
+ onClick={() => setVisible(!visible)}
362
+ className="p-0.5 hover:bg-gray-100 dark:hover:bg-white/10 rounded"
363
+ >
364
+ {visible ? (
365
+ <Eye className="h-2.5 w-2.5 text-gray-400 dark:text-gray-500" />
366
+ ) : (
367
+ <EyeOff className="h-2.5 w-2.5 text-gray-500 dark:text-gray-600" />
368
+ )}
369
+ </button>
370
+ <div
371
+ className="w-4 h-4 rounded-sm border-2 flex-shrink-0"
372
+ style={{ borderColor: stroke.color }}
373
+ />
374
+ <span id="stroke-row-span-strokecolor" className="text-[10px] font-mono text-gray-700 dark:text-gray-200">{stroke.color}</span>
375
+ <span id="stroke-row-span-strokewidth" className="text-[10px] text-gray-500">{stroke.width}</span>
376
+ </div>
377
+ );
378
+ }
379
+
380
+ // ============================================
381
+ // MAIN PROPERTIES PANEL
382
+ // ============================================
383
+
384
+ export function PropertiesPanel({
385
+ element,
386
+ styles,
387
+ onPropertyClick,
388
+ onSaveChanges,
389
+ textChangeSession,
390
+ onTextChangeAccept,
391
+ onTextChangeRevert,
392
+ imageOverride,
393
+ onImageOverrideChange,
394
+ onSaveImageChanges,
395
+ isImageSaving = false,
396
+ publicImages = [],
397
+ publicImagesByFolder = {},
398
+ logoAssets = [],
399
+ logoAssetsByBrand = {},
400
+ onImageUpload,
401
+ }: PropertiesPanelProps) {
402
+ // Theme detection for mode-specific color changes
403
+ const { resolvedTheme } = useTheme();
404
+ const currentTheme = resolvedTheme || "light";
405
+ const isDarkMode = currentTheme === "dark";
406
+
407
+ // Edit state
408
+ const [edits, setEdits] = useState<PropertyEdits>({});
409
+ const [isSaving, setIsSaving] = useState(false);
410
+ const [originalInlineStyles, setOriginalInlineStyles] = useState<Record<string, string>>({});
411
+ const [originalTextContent, setOriginalTextContent] = useState<string>("");
412
+
413
+ // Image editing state
414
+ const [customUrl, setCustomUrl] = useState("");
415
+ const [isUploading, setIsUploading] = useState(false);
416
+ const fileInputRef = useRef<HTMLInputElement>(null);
417
+
418
+ // Check if there are any changes
419
+ const hasChanges = useMemo(() => Object.keys(edits).length > 0, [edits]);
420
+
421
+ // Get the actual DOM element for live preview using multi-strategy approach
422
+ // This ensures we find the correct element (e.g., styled <span> inside <p>)
423
+ const targetElement = useMemo(() => {
424
+ if (!element) return null;
425
+
426
+ // Strategy 1: Find by element ID with styled child detection
427
+ if (element.elementId) {
428
+ const byId = document.getElementById(element.elementId);
429
+ if (byId && !byId.closest('[data-sonance-devtools="true"]')) {
430
+ // Check for styled child (e.g., span with inline color inside p)
431
+ const styledChild = byId.querySelector('[style*="color"]') as HTMLElement;
432
+ if (styledChild) {
433
+ return styledChild;
434
+ }
435
+ return byId;
436
+ }
437
+ }
438
+
439
+ // Strategy 2: Find by text content, prioritizing span elements
440
+ if (element.textContent) {
441
+ const textToFind = element.textContent.trim();
442
+ if (textToFind.length > 0) {
443
+ // Search spans first (often have inline styles)
444
+ const spanCandidates = document.querySelectorAll('span');
445
+ for (const el of spanCandidates) {
446
+ if (el.closest('[data-sonance-devtools="true"]')) continue;
447
+ const elText = el.textContent?.trim() || '';
448
+ const compareLength = Math.min(30, textToFind.length);
449
+ if (elText.startsWith(textToFind.substring(0, compareLength))) {
450
+ return el as HTMLElement;
451
+ }
452
+ }
453
+
454
+ // Fallback to other text elements
455
+ const candidates = document.querySelectorAll('p, h1, h2, h3, h4, h5, h6, label, button, a, td, th, li, div');
456
+ for (const el of candidates) {
457
+ if (el.closest('[data-sonance-devtools="true"]')) continue;
458
+ const elText = el.textContent?.trim() || '';
459
+ const compareLength = Math.min(30, textToFind.length);
460
+ if (elText.startsWith(textToFind.substring(0, compareLength))) {
461
+ // Check for styled span child
462
+ const styledSpan = el.querySelector('span[style*="color"]') as HTMLElement;
463
+ if (styledSpan) {
464
+ return styledSpan;
465
+ }
466
+ return el as HTMLElement;
467
+ }
468
+ }
469
+ }
470
+ }
471
+
472
+ // Strategy 3: Fallback to coordinates (may be stale after scroll)
473
+ if (element.coordinates) {
474
+ const { x, y, width, height } = element.coordinates;
475
+ const centerX = x + width / 2;
476
+ const centerY = y + height / 2;
477
+ const el = document.elementFromPoint(centerX, centerY);
478
+ if (el?.closest('[data-sonance-devtools="true"]')) return null;
479
+ return el as HTMLElement | null;
480
+ }
481
+
482
+ return null;
483
+ }, [element]);
484
+
485
+ // Capture original inline styles and text content when element changes
486
+ useEffect(() => {
487
+ if (targetElement) {
488
+ setOriginalInlineStyles({
489
+ width: targetElement.style.width,
490
+ height: targetElement.style.height,
491
+ opacity: targetElement.style.opacity,
492
+ borderRadius: targetElement.style.borderRadius,
493
+ fontSize: targetElement.style.fontSize,
494
+ fontWeight: targetElement.style.fontWeight,
495
+ lineHeight: targetElement.style.lineHeight,
496
+ letterSpacing: targetElement.style.letterSpacing,
497
+ color: targetElement.style.color,
498
+ backgroundColor: targetElement.style.backgroundColor,
499
+ padding: targetElement.style.padding,
500
+ margin: targetElement.style.margin,
501
+ gap: targetElement.style.gap,
502
+ });
503
+ // Capture original text content for revert
504
+ setOriginalTextContent(targetElement.textContent || "");
505
+ }
506
+ // Clear edits when element changes
507
+ setEdits({});
508
+ }, [targetElement, element]);
509
+
510
+ // Apply live preview when edits change
511
+ useEffect(() => {
512
+ if (!targetElement) return;
513
+
514
+ // Apply text content change
515
+ if (edits.textContent !== undefined) {
516
+ targetElement.textContent = edits.textContent;
517
+ }
518
+
519
+ // Apply each edited style property
520
+ if (edits.width) targetElement.style.width = edits.width;
521
+ if (edits.height) targetElement.style.height = edits.height;
522
+ if (edits.opacity) targetElement.style.opacity = String(parseFloat(edits.opacity) / 100);
523
+ if (edits.borderRadius) targetElement.style.borderRadius = edits.borderRadius;
524
+ if (edits.fontSize) targetElement.style.fontSize = edits.fontSize;
525
+ if (edits.fontWeight) targetElement.style.fontWeight = edits.fontWeight;
526
+ if (edits.lineHeight) targetElement.style.lineHeight = edits.lineHeight;
527
+ if (edits.letterSpacing) targetElement.style.letterSpacing = edits.letterSpacing;
528
+
529
+ // Apply theme-specific color: use the color for the current theme mode
530
+ // This ensures live preview shows the color being edited for the current mode
531
+ const colorToApply = isDarkMode
532
+ ? (edits.colorDark || edits.color)
533
+ : (edits.colorLight || edits.color);
534
+ if (colorToApply) targetElement.style.color = colorToApply;
535
+
536
+ if (edits.backgroundColor) targetElement.style.backgroundColor = edits.backgroundColor;
537
+ if (edits.padding) targetElement.style.padding = edits.padding;
538
+ if (edits.margin) targetElement.style.margin = edits.margin;
539
+ if (edits.gap) targetElement.style.gap = edits.gap;
540
+
541
+ // Mark element as having preview
542
+ targetElement.setAttribute('data-sonance-preview', 'true');
543
+ }, [edits, targetElement, isDarkMode]);
544
+
545
+ // Handle edit
546
+ const handleEdit = useCallback((key: keyof PropertyEdits, value: string) => {
547
+ setEdits(prev => ({ ...prev, [key]: value }));
548
+ }, []);
549
+
550
+ // Handle revert - restore original styles and text content
551
+ const handleRevert = useCallback(() => {
552
+ if (!targetElement) return;
553
+
554
+ // Restore original text content if it was modified
555
+ if (originalTextContent) {
556
+ targetElement.textContent = originalTextContent;
557
+ }
558
+
559
+ // Restore all original inline styles
560
+ Object.entries(originalInlineStyles).forEach(([key, value]) => {
561
+ (targetElement.style as unknown as Record<string, string>)[key] = value;
562
+ });
563
+
564
+ // Remove preview marker
565
+ targetElement.removeAttribute('data-sonance-preview');
566
+
567
+ // Clear edits
568
+ setEdits({});
569
+ }, [targetElement, originalInlineStyles, originalTextContent]);
570
+
571
+ // Handle save - call API to persist changes
572
+ const handleSave = useCallback(async () => {
573
+ if (!element || !hasChanges) return;
574
+
575
+ setIsSaving(true);
576
+ try {
577
+ // Call the parent's save handler and wait for it to complete
578
+ await onSaveChanges?.(edits, element);
579
+ // Only clear edits after successful save
580
+ setEdits({});
581
+ } catch (error) {
582
+ console.error("[PropertiesPanel] Save failed:", error);
583
+ // Keep edits on error so user can retry
584
+ } finally {
585
+ setIsSaving(false);
586
+ }
587
+ }, [element, edits, hasChanges, onSaveChanges]);
588
+
589
+ // Empty state
590
+ if (!element || !styles) {
591
+ return (
592
+ <div className="flex flex-col items-center justify-center py-8 text-center">
593
+ <div className="w-10 h-10 rounded-full bg-gray-100 dark:bg-white/10 flex items-center justify-center mb-3">
594
+ <Box className="h-5 w-5 text-gray-400 dark:text-gray-500" />
595
+ </div>
596
+ <p id="stroke-row-p-click-an-element-to-" className="text-[11px] text-gray-500 dark:text-gray-400">
597
+ Click an element to inspect
598
+ </p>
599
+ </div>
600
+ );
601
+ }
602
+
603
+ return (
604
+ <div className="text-gray-700 dark:text-gray-200 flex flex-col h-full">
605
+ <div className="flex-1 overflow-y-auto">
606
+ {/* Header - Element Name */}
607
+ <div className="px-2 py-2 border-b border-gray-200 dark:border-white/10 bg-gray-50 dark:bg-white/5">
608
+ <div className="flex items-center gap-1.5">
609
+ <div className="w-5 h-5 rounded bg-purple-500/20 flex items-center justify-center">
610
+ <Box className="h-3 w-3 text-purple-400" />
611
+ </div>
612
+ <div className="flex-1 min-w-0">
613
+ <p id="stroke-row-p-elementname" className="text-[11px] font-semibold text-gray-900 dark:text-white truncate">
614
+ {element.name}
615
+ </p>
616
+ {element.variantId && (
617
+ <p id="stroke-row-p-elementvariantidsubs" className="text-[9px] font-mono text-gray-500 truncate">
618
+ #{element.variantId.substring(0, 8)}
619
+ </p>
620
+ )}
621
+ </div>
622
+ <span id="stroke-row-span-stylestagname" className="text-[9px] px-1.5 py-0.5 rounded bg-gray-100 dark:bg-white/10 text-gray-500 dark:text-gray-400 uppercase">
623
+ {styles.tagName}
624
+ </span>
625
+ </div>
626
+ {styles.textContent && !edits.textContent && (
627
+ <p id="stroke-row-p-ldquostylestextconte" className="mt-1.5 text-[10px] text-gray-500 dark:text-gray-400 px-0.5">
628
+ &ldquo;{styles.textContent}&rdquo;
629
+ </p>
630
+ )}
631
+ {edits.textContent && (
632
+ <p className="mt-1.5 text-[10px] text-[#00A3E1] px-0.5">
633
+ &ldquo;{edits.textContent}&rdquo;
634
+ </p>
635
+ )}
636
+ </div>
637
+
638
+ {/* Content Section - Only for text elements */}
639
+ {styles.textContent && (
640
+ <Section
641
+ title="Content"
642
+ icon={<Type className="h-3 w-3" />}
643
+ defaultOpen={true}
644
+ >
645
+ <div className="space-y-1">
646
+ <textarea
647
+ value={edits.textContent ?? styles.textContent}
648
+ onChange={(e) => handleEdit("textContent", e.target.value)}
649
+ className={cn(
650
+ "w-full min-h-[60px] p-2 text-[10px] font-mono rounded resize-y",
651
+ "bg-gray-50 dark:bg-white/10 border text-gray-800 dark:text-white placeholder-gray-400 dark:placeholder-gray-500",
652
+ "focus:outline-none focus:ring-1 focus:ring-[#00A3E1]",
653
+ edits.textContent !== undefined && edits.textContent !== styles.textContent
654
+ ? "border-[#00A3E1]"
655
+ : "border-gray-300 dark:border-white/20"
656
+ )}
657
+ placeholder="Edit text content..."
658
+ />
659
+ {edits.textContent !== undefined && edits.textContent !== styles.textContent && (
660
+ <p className="text-[9px] text-[#00A3E1] flex items-center gap-1">
661
+ <Pencil className="h-2.5 w-2.5" />
662
+ Content modified
663
+ </p>
664
+ )}
665
+ </div>
666
+ </Section>
667
+ )}
668
+
669
+ {/* Image Section - Only for IMG elements */}
670
+ {styles.tagName.toUpperCase() === "IMG" && (
671
+ <Section
672
+ title="Image"
673
+ icon={<ImageIcon className="h-3 w-3" />}
674
+ defaultOpen={true}
675
+ >
676
+ <div className="space-y-3">
677
+ {/* Current Source Display */}
678
+ <div className="space-y-1">
679
+ <label className="text-[9px] text-gray-500 uppercase tracking-wide">Current Source</label>
680
+ <div className="text-[10px] font-mono text-gray-400 truncate bg-white/5 px-2 py-1 rounded">
681
+ {imageOverride?.src || (targetElement as HTMLImageElement)?.src || "Unknown"}
682
+ </div>
683
+ </div>
684
+
685
+ {/* No handler connected message */}
686
+ {!onImageOverrideChange && (
687
+ <div className="p-2 rounded bg-amber-500/10 border border-amber-500/30">
688
+ <p className="text-[10px] text-amber-400">
689
+ Image editing requires Vision Mode to be active. Enable Vision Mode to swap, resize, or scale images.
690
+ </p>
691
+ </div>
692
+ )}
693
+
694
+ {/* Brand Logos Dropdown */}
695
+ {Object.keys(logoAssetsByBrand).length > 0 && onImageOverrideChange && (
696
+ <div className="space-y-1">
697
+ <label className="text-[9px] text-gray-500 uppercase tracking-wide">Brand Logos</label>
698
+ <select
699
+ value={imageOverride?.src || ""}
700
+ onChange={(e) => {
701
+ if (e.target.value) {
702
+ onImageOverrideChange({ ...imageOverride, src: e.target.value });
703
+ }
704
+ }}
705
+ className={cn(
706
+ "w-full h-7 px-2 text-[10px] rounded",
707
+ "border border-white/20 bg-white/10 text-white",
708
+ "focus:outline-none focus:ring-1 focus:ring-[#00A3E1]"
709
+ )}
710
+ >
711
+ <option value="">-- Select brand logo --</option>
712
+ {Object.keys(logoAssetsByBrand).sort().map((brand) => (
713
+ <optgroup key={brand} label={brand.charAt(0).toUpperCase() + brand.slice(1)}>
714
+ {logoAssetsByBrand[brand].map((asset) => (
715
+ <option key={asset.id} value={asset.path}>
716
+ {asset.name}
717
+ </option>
718
+ ))}
719
+ </optgroup>
720
+ ))}
721
+ </select>
722
+ </div>
723
+ )}
724
+
725
+ {/* Public Folder Images Dropdown */}
726
+ {Object.keys(publicImagesByFolder).length > 0 && onImageOverrideChange && (
727
+ <div className="space-y-1">
728
+ <label className="text-[9px] text-gray-500 uppercase tracking-wide">Public Images</label>
729
+ <select
730
+ value={imageOverride?.src || ""}
731
+ onChange={(e) => {
732
+ if (e.target.value) {
733
+ onImageOverrideChange({ ...imageOverride, src: e.target.value });
734
+ }
735
+ }}
736
+ className={cn(
737
+ "w-full h-7 px-2 text-[10px] rounded",
738
+ "border border-white/20 bg-white/10 text-white",
739
+ "focus:outline-none focus:ring-1 focus:ring-[#00A3E1]"
740
+ )}
741
+ >
742
+ <option value="">-- Select from public folder --</option>
743
+ {Object.keys(publicImagesByFolder).sort().map((folder) => (
744
+ <optgroup key={folder} label={folder === "root" ? "Root" : folder}>
745
+ {publicImagesByFolder[folder].map((asset) => (
746
+ <option key={asset.id} value={asset.path}>
747
+ {asset.name}
748
+ </option>
749
+ ))}
750
+ </optgroup>
751
+ ))}
752
+ </select>
753
+ </div>
754
+ )}
755
+
756
+ {/* Custom URL Input */}
757
+ {onImageOverrideChange && (
758
+ <div className="space-y-1">
759
+ <label className="text-[9px] text-gray-500 uppercase tracking-wide flex items-center gap-1">
760
+ <Link className="h-2.5 w-2.5" />
761
+ Custom URL
762
+ </label>
763
+ <div className="flex gap-1">
764
+ <input
765
+ type="text"
766
+ value={customUrl}
767
+ onChange={(e) => setCustomUrl(e.target.value)}
768
+ placeholder="https://..."
769
+ className={cn(
770
+ "flex-1 h-7 px-2 text-[10px] font-mono rounded",
771
+ "border border-white/20 bg-white/10 text-white placeholder-gray-500",
772
+ "focus:outline-none focus:ring-1 focus:ring-[#00A3E1]"
773
+ )}
774
+ />
775
+ <button
776
+ onClick={() => {
777
+ if (customUrl) {
778
+ onImageOverrideChange({ ...imageOverride, src: customUrl });
779
+ setCustomUrl("");
780
+ }
781
+ }}
782
+ disabled={!customUrl}
783
+ className="px-2 h-7 text-[10px] font-medium rounded bg-[#00A3E1] text-white hover:bg-[#00A3E1]/80 disabled:opacity-50 disabled:cursor-not-allowed"
784
+ >
785
+ Apply
786
+ </button>
787
+ </div>
788
+ </div>
789
+ )}
790
+
791
+ {/* Upload Button */}
792
+ {onImageUpload && onImageOverrideChange && (
793
+ <div className="space-y-1">
794
+ <label className="text-[9px] text-gray-500 uppercase tracking-wide flex items-center gap-1">
795
+ <Upload className="h-2.5 w-2.5" />
796
+ Upload New Image
797
+ </label>
798
+ <input
799
+ ref={fileInputRef}
800
+ type="file"
801
+ accept="image/*"
802
+ onChange={async (e) => {
803
+ const file = e.target.files?.[0];
804
+ if (file && onImageUpload) {
805
+ setIsUploading(true);
806
+ try {
807
+ const uploadedPath = await onImageUpload(file);
808
+ if (uploadedPath) {
809
+ onImageOverrideChange({ ...imageOverride, src: uploadedPath });
810
+ }
811
+ } finally {
812
+ setIsUploading(false);
813
+ if (fileInputRef.current) {
814
+ fileInputRef.current.value = "";
815
+ }
816
+ }
817
+ }
818
+ }}
819
+ className="hidden"
820
+ />
821
+ <button
822
+ onClick={() => fileInputRef.current?.click()}
823
+ disabled={isUploading}
824
+ className={cn(
825
+ "w-full flex items-center justify-center gap-1.5 h-7 text-[10px] font-medium rounded",
826
+ "border border-dashed border-white/30 bg-white/5 text-gray-300",
827
+ "hover:border-[#00A3E1]/50 hover:bg-white/10 transition-colors",
828
+ "disabled:opacity-50 disabled:cursor-not-allowed"
829
+ )}
830
+ >
831
+ {isUploading ? (
832
+ <>
833
+ <Loader2 className="h-3 w-3 animate-spin" />
834
+ Uploading...
835
+ </>
836
+ ) : (
837
+ <>
838
+ <Upload className="h-3 w-3" />
839
+ Choose File
840
+ </>
841
+ )}
842
+ </button>
843
+ </div>
844
+ )}
845
+
846
+ {/* Dimensions */}
847
+ {onImageOverrideChange && (
848
+ <div className="grid grid-cols-2 gap-2">
849
+ <div className="space-y-1">
850
+ <label className="text-[9px] text-gray-500 uppercase tracking-wide">Width (px)</label>
851
+ <input
852
+ type="number"
853
+ min="10"
854
+ max="2000"
855
+ value={imageOverride?.width || ""}
856
+ placeholder="auto"
857
+ onChange={(e) => onImageOverrideChange({
858
+ ...imageOverride,
859
+ width: e.target.value ? parseInt(e.target.value) : undefined
860
+ })}
861
+ className={cn(
862
+ "w-full h-7 px-2 text-[10px] font-mono rounded",
863
+ "border border-white/20 bg-white/10 text-white placeholder-gray-500",
864
+ "focus:outline-none focus:ring-1 focus:ring-[#00A3E1]"
865
+ )}
866
+ />
867
+ </div>
868
+ <div className="space-y-1">
869
+ <label className="text-[9px] text-gray-500 uppercase tracking-wide">Height (px)</label>
870
+ <input
871
+ type="number"
872
+ min="10"
873
+ max="2000"
874
+ value={imageOverride?.height || ""}
875
+ placeholder="auto"
876
+ onChange={(e) => onImageOverrideChange({
877
+ ...imageOverride,
878
+ height: e.target.value ? parseInt(e.target.value) : undefined
879
+ })}
880
+ className={cn(
881
+ "w-full h-7 px-2 text-[10px] font-mono rounded",
882
+ "border border-white/20 bg-white/10 text-white placeholder-gray-500",
883
+ "focus:outline-none focus:ring-1 focus:ring-[#00A3E1]"
884
+ )}
885
+ />
886
+ </div>
887
+ </div>
888
+ )}
889
+
890
+ {/* Scale Slider */}
891
+ {onImageOverrideChange && (
892
+ <div className="space-y-1">
893
+ <label className="text-[9px] text-gray-500 uppercase tracking-wide">
894
+ Scale: {Math.round((imageOverride?.scale || 1) * 100)}%
895
+ </label>
896
+ <input
897
+ type="range"
898
+ min="25"
899
+ max="200"
900
+ value={(imageOverride?.scale || 1) * 100}
901
+ onChange={(e) => onImageOverrideChange({
902
+ ...imageOverride,
903
+ scale: parseInt(e.target.value) / 100
904
+ })}
905
+ className="w-full h-2 rounded-lg appearance-none cursor-pointer bg-white/20"
906
+ />
907
+ </div>
908
+ )}
909
+
910
+ {/* Object Fit */}
911
+ {onImageOverrideChange && (
912
+ <div className="space-y-1">
913
+ <label className="text-[9px] text-gray-500 uppercase tracking-wide">Object Fit</label>
914
+ <select
915
+ value={imageOverride?.objectFit || ""}
916
+ onChange={(e) => onImageOverrideChange({
917
+ ...imageOverride,
918
+ objectFit: e.target.value as ImageOverride["objectFit"] || undefined
919
+ })}
920
+ className={cn(
921
+ "w-full h-7 px-2 text-[10px] rounded",
922
+ "border border-white/20 bg-white/10 text-white",
923
+ "focus:outline-none focus:ring-1 focus:ring-[#00A3E1]"
924
+ )}
925
+ >
926
+ <option value="">-- Auto --</option>
927
+ <option value="cover">Cover</option>
928
+ <option value="contain">Contain</option>
929
+ <option value="fill">Fill</option>
930
+ <option value="none">None</option>
931
+ <option value="scale-down">Scale Down</option>
932
+ </select>
933
+ </div>
934
+ )}
935
+
936
+ {/* Action Buttons - Show when there are changes */}
937
+ {onImageOverrideChange && (imageOverride?.src || imageOverride?.width || imageOverride?.height ||
938
+ (imageOverride?.scale && imageOverride.scale !== 1) || imageOverride?.objectFit) && (
939
+ <div className="flex gap-2 pt-2 border-t border-white/10">
940
+ {/* Reset Button */}
941
+ <button
942
+ onClick={() => onImageOverrideChange({ reset: true })}
943
+ disabled={isImageSaving}
944
+ className={cn(
945
+ "flex-1 flex items-center justify-center gap-1.5 h-8 text-[10px] font-medium rounded",
946
+ "bg-white/10 text-gray-300 hover:bg-white/20 transition-colors",
947
+ "disabled:opacity-50 disabled:cursor-not-allowed"
948
+ )}
949
+ >
950
+ <RotateCcw className="h-3 w-3" />
951
+ Reset
952
+ </button>
953
+
954
+ {/* Save Button */}
955
+ {onSaveImageChanges && (
956
+ <button
957
+ onClick={onSaveImageChanges}
958
+ disabled={isImageSaving}
959
+ className={cn(
960
+ "flex-1 flex items-center justify-center gap-1.5 h-8 text-[10px] font-medium rounded",
961
+ "bg-[#00A3E1] text-white hover:bg-[#00A3E1]/80 transition-colors",
962
+ "disabled:opacity-50 disabled:cursor-not-allowed"
963
+ )}
964
+ >
965
+ {isImageSaving ? (
966
+ <>
967
+ <Loader2 className="h-3 w-3 animate-spin" />
968
+ Saving...
969
+ </>
970
+ ) : (
971
+ <>
972
+ <Save className="h-3 w-3" />
973
+ Save Changes
974
+ </>
975
+ )}
976
+ </button>
977
+ )}
978
+ </div>
979
+ )}
980
+ </div>
981
+ </Section>
982
+ )}
983
+
984
+ {/* Layout Section - Always shown */}
985
+ <Section
986
+ title="Layout"
987
+ icon={<Grid3X3 className="h-3 w-3" />}
988
+ defaultOpen={true}
989
+ >
990
+ <div className="space-y-2">
991
+ <EditableDimensionGrid
992
+ x={styles.geometry.x}
993
+ y={styles.geometry.y}
994
+ width={styles.geometry.width}
995
+ height={styles.geometry.height}
996
+ edits={edits}
997
+ onEdit={handleEdit}
998
+ />
999
+ </div>
1000
+ </Section>
1001
+
1002
+ {/* Appearance Section - Always shown */}
1003
+ <Section
1004
+ title="Appearance"
1005
+ icon={<CircleDot className="h-3 w-3" />}
1006
+ defaultOpen={true}
1007
+ >
1008
+ <div className="space-y-0.5">
1009
+ <EditablePropertyRow
1010
+ label="Opacity"
1011
+ value={Math.round(styles.opacity)}
1012
+ unit="%"
1013
+ editKey="opacity"
1014
+ edits={edits}
1015
+ onEdit={handleEdit}
1016
+ options={[
1017
+ { label: "0%", value: "0" },
1018
+ { label: "10%", value: "10" },
1019
+ { label: "20%", value: "20" },
1020
+ { label: "30%", value: "30" },
1021
+ { label: "40%", value: "40" },
1022
+ { label: "50%", value: "50" },
1023
+ { label: "60%", value: "60" },
1024
+ { label: "70%", value: "70" },
1025
+ { label: "80%", value: "80" },
1026
+ { label: "90%", value: "90" },
1027
+ { label: "100%", value: "100" },
1028
+ ]}
1029
+ />
1030
+ <EditablePropertyRow
1031
+ label="Radius"
1032
+ value={styles.borderRadius}
1033
+ editKey="borderRadius"
1034
+ edits={edits}
1035
+ onEdit={handleEdit}
1036
+ options={[
1037
+ { label: "None (0px)", value: "0px" },
1038
+ { label: "XS (2px)", value: "2px" },
1039
+ { label: "SM (4px)", value: "4px" },
1040
+ { label: "MD (6px)", value: "6px" },
1041
+ { label: "LG (8px)", value: "8px" },
1042
+ { label: "XL (12px)", value: "12px" },
1043
+ { label: "2XL (16px)", value: "16px" },
1044
+ { label: "3XL (24px)", value: "24px" },
1045
+ { label: "Full (9999px)", value: "9999px" },
1046
+ ]}
1047
+ />
1048
+ </div>
1049
+ </Section>
1050
+
1051
+ {/* Typography Section - Only for text elements */}
1052
+ {styles.typography && (
1053
+ <Section
1054
+ title="Typography"
1055
+ icon={<Type className="h-3 w-3" />}
1056
+ defaultOpen={true}
1057
+ >
1058
+ <div className="space-y-0.5">
1059
+ <EditablePropertyRow
1060
+ label="Font"
1061
+ value={styles.typography.fontFamily}
1062
+ readOnly
1063
+ />
1064
+ <div className="grid grid-cols-2 gap-x-2">
1065
+ <EditablePropertyRow
1066
+ label="Size"
1067
+ value={styles.typography.fontSize}
1068
+ editKey="fontSize"
1069
+ edits={edits}
1070
+ onEdit={handleEdit}
1071
+ options={[
1072
+ { label: "12px", value: "12px" },
1073
+ { label: "14px", value: "14px" },
1074
+ { label: "16px", value: "16px" },
1075
+ { label: "18px", value: "18px" },
1076
+ { label: "20px", value: "20px" },
1077
+ { label: "24px", value: "24px" },
1078
+ { label: "30px", value: "30px" },
1079
+ { label: "36px", value: "36px" },
1080
+ { label: "48px", value: "48px" },
1081
+ { label: "60px", value: "60px" },
1082
+ { label: "72px", value: "72px" },
1083
+ { label: "96px", value: "96px" },
1084
+ ]}
1085
+ />
1086
+ <EditablePropertyRow
1087
+ label="Weight"
1088
+ value={styles.typography.fontWeight}
1089
+ editKey="fontWeight"
1090
+ edits={edits}
1091
+ onEdit={handleEdit}
1092
+ options={[
1093
+ { label: "Thin (100)", value: "100" },
1094
+ { label: "Light (300)", value: "300" },
1095
+ { label: "Regular (400)", value: "400" },
1096
+ { label: "Medium (500)", value: "500" },
1097
+ { label: "Semibold (600)", value: "600" },
1098
+ { label: "Bold (700)", value: "700" },
1099
+ { label: "Black (900)", value: "900" },
1100
+ ]}
1101
+ />
1102
+ </div>
1103
+ <EditablePropertyRow
1104
+ label="Line H"
1105
+ value={styles.typography.lineHeight}
1106
+ editKey="lineHeight"
1107
+ edits={edits}
1108
+ onEdit={handleEdit}
1109
+ options={[
1110
+ { label: "None (1)", value: "1" },
1111
+ { label: "Tight (1.25)", value: "1.25" },
1112
+ { label: "Normal (1.5)", value: "1.5" },
1113
+ { label: "Relaxed (1.75)", value: "1.75" },
1114
+ { label: "Loose (2)", value: "2" },
1115
+ ]}
1116
+ />
1117
+ <EditablePropertyRow
1118
+ label="Letter"
1119
+ value={styles.typography.letterSpacing}
1120
+ editKey="letterSpacing"
1121
+ edits={edits}
1122
+ onEdit={handleEdit}
1123
+ options={[
1124
+ { label: "Tighter (-0.05em)", value: "-0.05em" },
1125
+ { label: "Tight (-0.025em)", value: "-0.025em" },
1126
+ { label: "Normal (0)", value: "0" },
1127
+ { label: "Wide (0.025em)", value: "0.025em" },
1128
+ { label: "Wider (0.05em)", value: "0.05em" },
1129
+ { label: "Widest (0.1em)", value: "0.1em" },
1130
+ ]}
1131
+ />
1132
+ <EditablePropertyRow
1133
+ label={`Color (${isDarkMode ? "dark" : "light"} mode)`}
1134
+ value={styles.typography.color}
1135
+ color={styles.typography.color !== "transparent" ? styles.typography.color : undefined}
1136
+ editKey={isDarkMode ? "colorDark" : "colorLight"}
1137
+ edits={edits}
1138
+ onEdit={handleEdit}
1139
+ inputType="color"
1140
+ />
1141
+ </div>
1142
+ </Section>
1143
+ )}
1144
+
1145
+ {/* Fill Section - Only show if has fills */}
1146
+ {styles.fills.length > 0 && (
1147
+ <Section
1148
+ title="Fill"
1149
+ icon={<Square className="h-3 w-3" />}
1150
+ defaultOpen={true}
1151
+ readOnly
1152
+ >
1153
+ <div className="space-y-0.5">
1154
+ {styles.fills.map((fill, i) => (
1155
+ <FillRow key={i} fill={fill} />
1156
+ ))}
1157
+ </div>
1158
+ </Section>
1159
+ )}
1160
+
1161
+ {/* Stroke Section - Only show if has strokes */}
1162
+ {styles.strokes.length > 0 && (
1163
+ <Section
1164
+ title="Stroke"
1165
+ icon={<Minus className="h-3 w-3" />}
1166
+ defaultOpen={true}
1167
+ readOnly
1168
+ >
1169
+ <div className="space-y-0.5">
1170
+ {styles.strokes.map((stroke, i) => (
1171
+ <StrokeRow key={i} stroke={stroke} />
1172
+ ))}
1173
+ </div>
1174
+ </Section>
1175
+ )}
1176
+
1177
+ {/* Effects Section - Only show if has effects */}
1178
+ {styles.effects.length > 0 && (
1179
+ <Section
1180
+ title="Effects"
1181
+ icon={<Layers className="h-3 w-3" />}
1182
+ defaultOpen={true}
1183
+ readOnly
1184
+ >
1185
+ <div className="space-y-0.5">
1186
+ {styles.effects.map((effect, i) => (
1187
+ <div key={i} className="flex items-center gap-1.5 py-0.5">
1188
+ <Eye className="h-2.5 w-2.5 text-gray-500" />
1189
+ <span id="section-span-effecttype" className="text-[10px] text-gray-400 capitalize">{effect.type}</span>
1190
+ <span id="section-span-effectvaluesubstring" className="text-[9px] font-mono text-gray-500 truncate flex-1">
1191
+ {effect.value.substring(0, 30)}...
1192
+ </span>
1193
+ </div>
1194
+ ))}
1195
+ </div>
1196
+ </Section>
1197
+ )}
1198
+
1199
+ {/* Spacing Section - Always shown */}
1200
+ <Section
1201
+ title="Spacing"
1202
+ icon={<CornerDownRight className="h-3 w-3" />}
1203
+ defaultOpen={false}
1204
+ >
1205
+ <div className="space-y-0.5">
1206
+ <EditablePropertyRow
1207
+ label="Padding"
1208
+ value={styles.padding}
1209
+ editKey="padding"
1210
+ edits={edits}
1211
+ onEdit={handleEdit}
1212
+ />
1213
+ <EditablePropertyRow
1214
+ label="Margin"
1215
+ value={styles.margin}
1216
+ editKey="margin"
1217
+ edits={edits}
1218
+ onEdit={handleEdit}
1219
+ />
1220
+ {styles.gap !== "normal" && (
1221
+ <EditablePropertyRow
1222
+ label="Gap"
1223
+ value={styles.gap}
1224
+ editKey="gap"
1225
+ edits={edits}
1226
+ onEdit={handleEdit}
1227
+ />
1228
+ )}
1229
+ </div>
1230
+ </Section>
1231
+
1232
+ {/* Flexbox Section - only if flex */}
1233
+ {(styles.display === "flex" || styles.display === "inline-flex") && (
1234
+ <Section
1235
+ title="Flexbox"
1236
+ icon={<Palette className="h-3 w-3" />}
1237
+ defaultOpen={false}
1238
+ readOnly
1239
+ >
1240
+ <div className="space-y-0.5">
1241
+ <EditablePropertyRow
1242
+ label="Direction"
1243
+ value={styles.flexDirection}
1244
+ readOnly
1245
+ />
1246
+ <EditablePropertyRow
1247
+ label="Align"
1248
+ value={styles.alignItems}
1249
+ readOnly
1250
+ />
1251
+ <EditablePropertyRow
1252
+ label="Justify"
1253
+ value={styles.justifyContent}
1254
+ readOnly
1255
+ />
1256
+ </div>
1257
+ </Section>
1258
+ )}
1259
+ </div>
1260
+
1261
+ {/* Save/Revert Footer - Only show when there are changes */}
1262
+ {hasChanges && (
1263
+ <div className="sticky bottom-0 px-2 py-2 bg-gray-100 dark:bg-[#1a1a1a] border-t border-gray-200 dark:border-white/10 flex gap-2">
1264
+ <button
1265
+ onClick={handleRevert}
1266
+ disabled={isSaving}
1267
+ className="flex-1 flex items-center justify-center gap-1.5 px-3 py-1.5 text-[11px] font-medium rounded bg-gray-200 dark:bg-white/10 text-gray-700 dark:text-gray-300 hover:bg-gray-300 dark:hover:bg-white/20 disabled:opacity-50 transition-colors"
1268
+ >
1269
+ <RotateCcw className="h-3 w-3" />
1270
+ Revert
1271
+ </button>
1272
+ <button
1273
+ onClick={handleSave}
1274
+ disabled={isSaving}
1275
+ 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"
1276
+ >
1277
+ {isSaving ? (
1278
+ <Loader2 className="h-3 w-3 animate-spin" />
1279
+ ) : (
1280
+ <Save className="h-3 w-3" />
1281
+ )}
1282
+ Save Changes
1283
+ </button>
1284
+ </div>
1285
+ )}
1286
+
1287
+ {/* Text Change Summary - Shows after text-only changes are saved */}
1288
+ {textChangeSession?.isTextOnlyChange && onTextChangeAccept && onTextChangeRevert && (
1289
+ <div className="sticky bottom-0 px-2 py-2 bg-gray-100 dark:bg-[#1a1a1a] border-t border-gray-200 dark:border-white/10">
1290
+ <div className="rounded-lg bg-green-500/10 border border-green-500/30 p-2 mb-2">
1291
+ <div className="flex items-center gap-2 mb-1">
1292
+ <div className="w-4 h-4 rounded-full bg-green-500/20 flex items-center justify-center">
1293
+ <svg className="w-2.5 h-2.5 text-green-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
1294
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={3} d="M5 13l4 4L19 7" />
1295
+ </svg>
1296
+ </div>
1297
+ <span className="text-[11px] font-medium text-green-400">Text Updated</span>
1298
+ </div>
1299
+ {textChangeSession.modifications[0] && (
1300
+ <>
1301
+ <p className="text-[9px] text-gray-400 font-mono truncate mb-1">
1302
+ {textChangeSession.modifications[0].filePath}
1303
+ </p>
1304
+ {textChangeSession.modifications[0].diff && (
1305
+ <div className="text-[9px] font-mono space-y-0.5">
1306
+ {textChangeSession.modifications[0].diff.split('\n').filter(line =>
1307
+ line.startsWith('-') && !line.startsWith('---')
1308
+ ).slice(0, 1).map((line, i) => (
1309
+ <p key={`old-${i}`} className="text-red-400/80 truncate">
1310
+ {line}
1311
+ </p>
1312
+ ))}
1313
+ {textChangeSession.modifications[0].diff.split('\n').filter(line =>
1314
+ line.startsWith('+') && !line.startsWith('+++')
1315
+ ).slice(0, 1).map((line, i) => (
1316
+ <p key={`new-${i}`} className="text-green-400/80 truncate">
1317
+ {line}
1318
+ </p>
1319
+ ))}
1320
+ </div>
1321
+ )}
1322
+ </>
1323
+ )}
1324
+ </div>
1325
+ <div className="flex gap-2">
1326
+ <button
1327
+ onClick={onTextChangeRevert}
1328
+ className="flex-1 flex items-center justify-center gap-1.5 px-3 py-1.5 text-[11px] font-medium rounded bg-gray-200 dark:bg-white/10 text-gray-700 dark:text-gray-300 hover:bg-gray-300 dark:hover:bg-white/20 transition-colors"
1329
+ >
1330
+ <RotateCcw className="h-3 w-3" />
1331
+ Revert
1332
+ </button>
1333
+ <button
1334
+ onClick={onTextChangeAccept}
1335
+ className="flex-1 flex items-center justify-center gap-1.5 px-3 py-1.5 text-[11px] font-medium rounded bg-green-600 text-white hover:bg-green-600/80 transition-colors"
1336
+ >
1337
+ <Save className="h-3 w-3" />
1338
+ Keep Changes
1339
+ </button>
1340
+ </div>
1341
+ </div>
1342
+ )}
1343
+ </div>
1344
+ );
1345
+ }