sonance-brand-mcp 1.3.111 → 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 (78) hide show
  1. package/dist/assets/api/sonance-save-image/route.ts +625 -0
  2. package/dist/assets/api/sonance-vision-apply/image-styling-detection.ts +1360 -0
  3. package/dist/assets/api/sonance-vision-apply/route.ts +988 -57
  4. package/dist/assets/api/sonance-vision-apply/styling-detection.ts +730 -0
  5. package/dist/assets/api/sonance-vision-apply/theme-discovery.ts +1 -1
  6. package/dist/assets/brand-system.ts +13 -12
  7. package/dist/assets/components/accordion.tsx +15 -7
  8. package/dist/assets/components/alert-dialog.tsx +35 -10
  9. package/dist/assets/components/alert.tsx +11 -10
  10. package/dist/assets/components/avatar.tsx +4 -4
  11. package/dist/assets/components/badge.tsx +16 -12
  12. package/dist/assets/components/button.stories.tsx +3 -3
  13. package/dist/assets/components/button.tsx +50 -31
  14. package/dist/assets/components/calendar.tsx +12 -8
  15. package/dist/assets/components/card.tsx +35 -29
  16. package/dist/assets/components/checkbox.tsx +9 -8
  17. package/dist/assets/components/code.tsx +19 -11
  18. package/dist/assets/components/command.tsx +32 -13
  19. package/dist/assets/components/context-menu.tsx +37 -16
  20. package/dist/assets/components/dialog.tsx +8 -5
  21. package/dist/assets/components/divider.tsx +15 -5
  22. package/dist/assets/components/drawer.tsx +4 -3
  23. package/dist/assets/components/dropdown-menu.tsx +15 -13
  24. package/dist/assets/components/hover-card.tsx +4 -1
  25. package/dist/assets/components/image.tsx +1 -1
  26. package/dist/assets/components/input.tsx +29 -14
  27. package/dist/assets/components/kbd.stories.tsx +3 -3
  28. package/dist/assets/components/kbd.tsx +29 -13
  29. package/dist/assets/components/listbox.tsx +8 -8
  30. package/dist/assets/components/menubar.tsx +50 -23
  31. package/dist/assets/components/navbar.stories.tsx +140 -13
  32. package/dist/assets/components/navbar.tsx +22 -5
  33. package/dist/assets/components/navigation-menu.tsx +28 -6
  34. package/dist/assets/components/pagination.tsx +10 -10
  35. package/dist/assets/components/popover.tsx +10 -8
  36. package/dist/assets/components/progress.tsx +6 -4
  37. package/dist/assets/components/radio-group.tsx +5 -5
  38. package/dist/assets/components/select.tsx +49 -29
  39. package/dist/assets/components/separator.tsx +3 -3
  40. package/dist/assets/components/sheet.tsx +4 -4
  41. package/dist/assets/components/sidebar.tsx +10 -10
  42. package/dist/assets/components/skeleton.tsx +13 -5
  43. package/dist/assets/components/slider.tsx +12 -10
  44. package/dist/assets/components/switch.tsx +4 -4
  45. package/dist/assets/components/table.tsx +5 -5
  46. package/dist/assets/components/tabs.tsx +8 -8
  47. package/dist/assets/components/textarea.tsx +11 -9
  48. package/dist/assets/components/toast.tsx +7 -7
  49. package/dist/assets/components/toggle.tsx +27 -7
  50. package/dist/assets/components/tooltip.tsx +10 -8
  51. package/dist/assets/components/user.tsx +8 -6
  52. package/dist/assets/dev-tools/SonanceDevTools.tsx +429 -362
  53. package/dist/assets/dev-tools/components/ApplyFirstPreview.tsx +10 -10
  54. package/dist/assets/dev-tools/components/ChatHistory.tsx +11 -7
  55. package/dist/assets/dev-tools/components/ChatInterface.tsx +61 -20
  56. package/dist/assets/dev-tools/components/ChatTabBar.tsx +1 -1
  57. package/dist/assets/dev-tools/components/DiffPreview.tsx +1 -1
  58. package/dist/assets/dev-tools/components/InlineDiffPreview.tsx +360 -36
  59. package/dist/assets/dev-tools/components/InspectorOverlay.tsx +9 -9
  60. package/dist/assets/dev-tools/components/PropertiesPanel.tsx +743 -93
  61. package/dist/assets/dev-tools/components/ScreenshotAnnotator.tsx +1 -1
  62. package/dist/assets/dev-tools/components/SectionHighlight.tsx +1 -1
  63. package/dist/assets/dev-tools/components/VisionDiffPreview.tsx +7 -7
  64. package/dist/assets/dev-tools/components/VisionModeBorder.tsx +4 -64
  65. package/dist/assets/dev-tools/hooks/index.ts +69 -0
  66. package/dist/assets/dev-tools/hooks/useComponentDetection.ts +132 -0
  67. package/dist/assets/dev-tools/hooks/useComputedStyles.ts +171 -65
  68. package/dist/assets/dev-tools/hooks/useContentHash.ts +212 -0
  69. package/dist/assets/dev-tools/hooks/useElementScanner.ts +398 -0
  70. package/dist/assets/dev-tools/hooks/useImageDetection.ts +162 -0
  71. package/dist/assets/dev-tools/hooks/useTextDetection.ts +217 -0
  72. package/dist/assets/dev-tools/panels/ComponentsPanel.tsx +160 -57
  73. package/dist/assets/dev-tools/panels/TextPanel.tsx +10 -10
  74. package/dist/assets/dev-tools/types.ts +42 -0
  75. package/dist/assets/globals.css +225 -9
  76. package/dist/assets/styles/brand-overrides.css +3 -2
  77. package/dist/assets/utils.ts +2 -1
  78. package/package.json +1 -1
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
 
3
- import React, { useState, useEffect, useCallback, useMemo } from "react";
3
+ import React, { useState, useEffect, useCallback, useMemo, useRef } from "react";
4
4
  import {
5
5
  ChevronDown,
6
6
  ChevronRight,
@@ -18,16 +18,23 @@ import {
18
18
  RotateCcw,
19
19
  Save,
20
20
  Loader2,
21
+ Pencil,
22
+ Image as ImageIcon,
23
+ Upload,
24
+ Link,
21
25
  } from "lucide-react";
26
+ import { useTheme } from "next-themes";
22
27
  import { cn } from "../../../lib/utils";
23
28
  import { ComputedStyles } from "../hooks/useComputedStyles";
24
- import { VisionFocusedElement } from "../types";
29
+ import { VisionFocusedElement, ApplyFirstSession, ImageOverride, PublicImageAsset, LogoAsset } from "../types";
25
30
 
26
31
  // ============================================
27
32
  // TYPES
28
33
  // ============================================
29
34
 
30
35
  export interface PropertyEdits {
36
+ // Content
37
+ textContent?: string;
31
38
  // Layout
32
39
  width?: string;
33
40
  height?: string;
@@ -39,7 +46,9 @@ export interface PropertyEdits {
39
46
  fontWeight?: string;
40
47
  lineHeight?: string;
41
48
  letterSpacing?: string;
42
- color?: string;
49
+ color?: string; // Legacy/universal color
50
+ colorLight?: string; // Color for light mode only
51
+ colorDark?: string; // Color for dark mode only
43
52
  // Fill
44
53
  backgroundColor?: string;
45
54
  // Spacing
@@ -52,13 +61,28 @@ interface PropertiesPanelProps {
52
61
  element: VisionFocusedElement | null;
53
62
  styles: ComputedStyles | null;
54
63
  onPropertyClick?: (property: string, value: string) => void;
55
- onSaveChanges?: (edits: PropertyEdits, element: VisionFocusedElement) => 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>;
56
79
  }
57
80
 
58
81
  interface SectionProps {
59
82
  title: string;
60
83
  icon: React.ReactNode;
61
84
  defaultOpen?: boolean;
85
+ readOnly?: boolean;
62
86
  children: React.ReactNode;
63
87
  }
64
88
 
@@ -66,24 +90,29 @@ interface SectionProps {
66
90
  // SECTION COMPONENT
67
91
  // ============================================
68
92
 
69
- function Section({ title, icon, defaultOpen = true, children }: SectionProps) {
93
+ function Section({ title, icon, defaultOpen = true, readOnly = false, children }: SectionProps) {
70
94
  const [isOpen, setIsOpen] = useState(defaultOpen);
71
95
 
72
96
  return (
73
- <div className="border-b border-white/10 last:border-b-0">
97
+ <div className="border-b border-gray-200 dark:border-white/10 last:border-b-0">
74
98
  <button
75
99
  onClick={() => setIsOpen(!isOpen)}
76
- className="w-full flex items-center justify-between px-2 py-1.5 hover:bg-white/5 transition-colors"
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"
77
101
  >
78
- <div className="flex items-center gap-1.5 text-[11px] font-medium text-gray-200">
102
+ <div className="flex items-center gap-1.5 text-[11px] font-medium text-gray-700 dark:text-gray-200">
79
103
  {icon}
80
- <span>{title}</span>
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
+ )}
81
110
  </div>
82
111
  <div className="flex items-center gap-1">
83
112
  {isOpen ? (
84
- <ChevronDown className="h-3 w-3 text-gray-500" />
113
+ <ChevronDown className="h-3 w-3 text-gray-400 dark:text-gray-500" />
85
114
  ) : (
86
- <ChevronRight className="h-3 w-3 text-gray-500" />
115
+ <ChevronRight className="h-3 w-3 text-gray-400 dark:text-gray-500" />
87
116
  )}
88
117
  </div>
89
118
  </button>
@@ -106,6 +135,7 @@ interface EditablePropertyRowProps {
106
135
  onEdit?: (key: keyof PropertyEdits, value: string) => void;
107
136
  readOnly?: boolean;
108
137
  inputType?: "text" | "number" | "color";
138
+ options?: { label: string; value: string }[];
109
139
  }
110
140
 
111
141
  function EditablePropertyRow({
@@ -118,44 +148,85 @@ function EditablePropertyRow({
118
148
  onEdit,
119
149
  readOnly = false,
120
150
  inputType = "text",
151
+ options,
121
152
  }: EditablePropertyRowProps) {
122
153
  const isEditable = !readOnly && editKey && edits && onEdit;
123
154
  const editedValue = editKey && edits ? edits[editKey] : undefined;
124
155
  const displayValue = editedValue !== undefined ? editedValue : String(value);
125
156
  const isEdited = editedValue !== undefined && editedValue !== String(value);
126
157
 
127
- const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
158
+ const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
128
159
  if (isEditable && editKey) {
129
160
  onEdit(editKey, e.target.value);
130
161
  }
131
162
  };
132
163
 
133
164
  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>
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>
136
173
  <div className="flex items-center gap-1">
137
174
  {color && (
138
175
  <div
139
- className="w-3 h-3 rounded-sm border border-white/20"
176
+ className="w-3 h-3 rounded-sm border border-gray-300 dark:border-white/20 relative"
140
177
  style={{ backgroundColor: color }}
141
- />
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>
142
188
  )}
143
189
  {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
- />
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
+ )
153
224
  ) : (
154
- <span className="font-mono text-white">
225
+ <span id="editable-property-row-span-displayvalue" className="font-mono text-gray-600 dark:text-gray-400 cursor-default">
155
226
  {displayValue}
156
227
  </span>
157
228
  )}
158
- {unit && <span className="text-gray-500 ml-0.5">{unit}</span>}
229
+ {unit && <span id="editable-property-row-span-unit" className="text-gray-500 ml-0.5">{unit}</span>}
159
230
  </div>
160
231
  </div>
161
232
  );
@@ -179,41 +250,43 @@ function EditableDimensionGrid({ x, y, width, height, edits, onEdit }: EditableD
179
250
  const heightEdited = edits.height !== undefined && edits.height !== `${height}px`;
180
251
 
181
252
  return (
182
- <div className="grid grid-cols-2 gap-1">
253
+ <div className="grid grid-cols-2 gap-1 overflow-hidden">
183
254
  {/* 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>
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>
187
258
  </div>
188
259
  {/* 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>
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>
192
263
  </div>
193
264
  {/* W - Editable */}
194
265
  <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"
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"
197
268
  )}>
198
- <span className="text-[9px] text-gray-500 uppercase w-3">W</span>
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>
199
271
  <input
200
272
  type="text"
201
273
  value={edits.width !== undefined ? edits.width : `${width}px`}
202
274
  onChange={(e) => onEdit("width", e.target.value)}
203
- className="text-[10px] font-mono text-white flex-1 text-right bg-transparent focus:outline-none"
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"
204
276
  />
205
277
  </div>
206
278
  {/* H - Editable */}
207
279
  <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"
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"
210
282
  )}>
211
- <span className="text-[9px] text-gray-500 uppercase w-3">H</span>
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>
212
285
  <input
213
286
  type="text"
214
287
  value={edits.height !== undefined ? edits.height : `${height}px`}
215
288
  onChange={(e) => onEdit("height", e.target.value)}
216
- className="text-[10px] font-mono text-white flex-1 text-right bg-transparent focus:outline-none"
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"
217
290
  />
218
291
  </div>
219
292
  </div>
@@ -235,36 +308,36 @@ function FillRow({ fill }: FillRowProps) {
235
308
  <div className="flex items-center gap-1.5 py-0.5">
236
309
  <button
237
310
  onClick={() => setVisible(!visible)}
238
- className="p-0.5 hover:bg-white/10 rounded"
311
+ className="p-0.5 hover:bg-gray-100 dark:hover:bg-white/10 rounded"
239
312
  >
240
313
  {visible ? (
241
- <Eye className="h-2.5 w-2.5 text-gray-500" />
314
+ <Eye className="h-2.5 w-2.5 text-gray-400 dark:text-gray-500" />
242
315
  ) : (
243
- <EyeOff className="h-2.5 w-2.5 text-gray-600" />
316
+ <EyeOff className="h-2.5 w-2.5 text-gray-500 dark:text-gray-600" />
244
317
  )}
245
318
  </button>
246
319
  {fill.type === "solid" && fill.color && (
247
320
  <>
248
321
  <div
249
- className="w-4 h-4 rounded-sm border border-white/20 flex-shrink-0"
322
+ className="w-4 h-4 rounded-sm border border-gray-300 dark:border-white/20 flex-shrink-0"
250
323
  style={{ backgroundColor: fill.color }}
251
324
  />
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>
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>
254
327
  </>
255
328
  )}
256
329
  {fill.type === "gradient" && (
257
330
  <>
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>
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>
260
333
  </>
261
334
  )}
262
335
  {fill.type === "image" && (
263
336
  <>
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" />
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" />
266
339
  </div>
267
- <span className="text-[10px] text-gray-400 flex-1">Image</span>
340
+ <span id="fill-row-span-image" className="text-[10px] text-gray-500 dark:text-gray-400 flex-1">Image</span>
268
341
  </>
269
342
  )}
270
343
  </div>
@@ -286,20 +359,20 @@ function StrokeRow({ stroke }: StrokeRowProps) {
286
359
  <div className="flex items-center gap-1.5 py-0.5">
287
360
  <button
288
361
  onClick={() => setVisible(!visible)}
289
- className="p-0.5 hover:bg-white/10 rounded"
362
+ className="p-0.5 hover:bg-gray-100 dark:hover:bg-white/10 rounded"
290
363
  >
291
364
  {visible ? (
292
- <Eye className="h-2.5 w-2.5 text-gray-500" />
365
+ <Eye className="h-2.5 w-2.5 text-gray-400 dark:text-gray-500" />
293
366
  ) : (
294
- <EyeOff className="h-2.5 w-2.5 text-gray-600" />
367
+ <EyeOff className="h-2.5 w-2.5 text-gray-500 dark:text-gray-600" />
295
368
  )}
296
369
  </button>
297
370
  <div
298
371
  className="w-4 h-4 rounded-sm border-2 flex-shrink-0"
299
372
  style={{ borderColor: stroke.color }}
300
373
  />
301
- <span className="text-[10px] font-mono text-gray-200">{stroke.color}</span>
302
- <span className="text-[10px] text-gray-500">{stroke.width}</span>
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>
303
376
  </div>
304
377
  );
305
378
  }
@@ -308,27 +381,108 @@ function StrokeRow({ stroke }: StrokeRowProps) {
308
381
  // MAIN PROPERTIES PANEL
309
382
  // ============================================
310
383
 
311
- export function PropertiesPanel({ element, styles, onPropertyClick, onSaveChanges }: PropertiesPanelProps) {
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
+
312
407
  // Edit state
313
408
  const [edits, setEdits] = useState<PropertyEdits>({});
314
409
  const [isSaving, setIsSaving] = useState(false);
315
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);
316
417
 
317
418
  // Check if there are any changes
318
419
  const hasChanges = useMemo(() => Object.keys(edits).length > 0, [edits]);
319
420
 
320
- // Get the actual DOM element for live preview
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>)
321
423
  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;
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;
329
483
  }, [element]);
330
484
 
331
- // Capture original inline styles when element changes
485
+ // Capture original inline styles and text content when element changes
332
486
  useEffect(() => {
333
487
  if (targetElement) {
334
488
  setOriginalInlineStyles({
@@ -346,6 +500,8 @@ export function PropertiesPanel({ element, styles, onPropertyClick, onSaveChange
346
500
  margin: targetElement.style.margin,
347
501
  gap: targetElement.style.gap,
348
502
  });
503
+ // Capture original text content for revert
504
+ setOriginalTextContent(targetElement.textContent || "");
349
505
  }
350
506
  // Clear edits when element changes
351
507
  setEdits({});
@@ -355,7 +511,12 @@ export function PropertiesPanel({ element, styles, onPropertyClick, onSaveChange
355
511
  useEffect(() => {
356
512
  if (!targetElement) return;
357
513
 
358
- // Apply each edited property
514
+ // Apply text content change
515
+ if (edits.textContent !== undefined) {
516
+ targetElement.textContent = edits.textContent;
517
+ }
518
+
519
+ // Apply each edited style property
359
520
  if (edits.width) targetElement.style.width = edits.width;
360
521
  if (edits.height) targetElement.style.height = edits.height;
361
522
  if (edits.opacity) targetElement.style.opacity = String(parseFloat(edits.opacity) / 100);
@@ -364,7 +525,14 @@ export function PropertiesPanel({ element, styles, onPropertyClick, onSaveChange
364
525
  if (edits.fontWeight) targetElement.style.fontWeight = edits.fontWeight;
365
526
  if (edits.lineHeight) targetElement.style.lineHeight = edits.lineHeight;
366
527
  if (edits.letterSpacing) targetElement.style.letterSpacing = edits.letterSpacing;
367
- if (edits.color) targetElement.style.color = edits.color;
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
+
368
536
  if (edits.backgroundColor) targetElement.style.backgroundColor = edits.backgroundColor;
369
537
  if (edits.padding) targetElement.style.padding = edits.padding;
370
538
  if (edits.margin) targetElement.style.margin = edits.margin;
@@ -372,17 +540,22 @@ export function PropertiesPanel({ element, styles, onPropertyClick, onSaveChange
372
540
 
373
541
  // Mark element as having preview
374
542
  targetElement.setAttribute('data-sonance-preview', 'true');
375
- }, [edits, targetElement]);
543
+ }, [edits, targetElement, isDarkMode]);
376
544
 
377
545
  // Handle edit
378
546
  const handleEdit = useCallback((key: keyof PropertyEdits, value: string) => {
379
547
  setEdits(prev => ({ ...prev, [key]: value }));
380
548
  }, []);
381
549
 
382
- // Handle revert - restore original styles
550
+ // Handle revert - restore original styles and text content
383
551
  const handleRevert = useCallback(() => {
384
552
  if (!targetElement) return;
385
553
 
554
+ // Restore original text content if it was modified
555
+ if (originalTextContent) {
556
+ targetElement.textContent = originalTextContent;
557
+ }
558
+
386
559
  // Restore all original inline styles
387
560
  Object.entries(originalInlineStyles).forEach(([key, value]) => {
388
561
  (targetElement.style as unknown as Record<string, string>)[key] = value;
@@ -393,7 +566,7 @@ export function PropertiesPanel({ element, styles, onPropertyClick, onSaveChange
393
566
 
394
567
  // Clear edits
395
568
  setEdits({});
396
- }, [targetElement, originalInlineStyles]);
569
+ }, [targetElement, originalInlineStyles, originalTextContent]);
397
570
 
398
571
  // Handle save - call API to persist changes
399
572
  const handleSave = useCallback(async () => {
@@ -401,11 +574,13 @@ export function PropertiesPanel({ element, styles, onPropertyClick, onSaveChange
401
574
 
402
575
  setIsSaving(true);
403
576
  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)
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
408
580
  setEdits({});
581
+ } catch (error) {
582
+ console.error("[PropertiesPanel] Save failed:", error);
583
+ // Keep edits on error so user can retry
409
584
  } finally {
410
585
  setIsSaving(false);
411
586
  }
@@ -415,10 +590,10 @@ export function PropertiesPanel({ element, styles, onPropertyClick, onSaveChange
415
590
  if (!element || !styles) {
416
591
  return (
417
592
  <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" />
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" />
420
595
  </div>
421
- <p className="text-[11px] text-gray-400">
596
+ <p id="stroke-row-p-click-an-element-to-" className="text-[11px] text-gray-500 dark:text-gray-400">
422
597
  Click an element to inspect
423
598
  </p>
424
599
  </div>
@@ -426,35 +601,386 @@ export function PropertiesPanel({ element, styles, onPropertyClick, onSaveChange
426
601
  }
427
602
 
428
603
  return (
429
- <div className="text-gray-200 flex flex-col h-full">
604
+ <div className="text-gray-700 dark:text-gray-200 flex flex-col h-full">
430
605
  <div className="flex-1 overflow-y-auto">
431
606
  {/* Header - Element Name */}
432
- <div className="px-2 py-2 border-b border-white/10 bg-white/5">
607
+ <div className="px-2 py-2 border-b border-gray-200 dark:border-white/10 bg-gray-50 dark:bg-white/5">
433
608
  <div className="flex items-center gap-1.5">
434
609
  <div className="w-5 h-5 rounded bg-purple-500/20 flex items-center justify-center">
435
610
  <Box className="h-3 w-3 text-purple-400" />
436
611
  </div>
437
612
  <div className="flex-1 min-w-0">
438
- <p className="text-[11px] font-semibold text-white truncate">
613
+ <p id="stroke-row-p-elementname" className="text-[11px] font-semibold text-gray-900 dark:text-white truncate">
439
614
  {element.name}
440
615
  </p>
441
616
  {element.variantId && (
442
- <p className="text-[9px] font-mono text-gray-500 truncate">
617
+ <p id="stroke-row-p-elementvariantidsubs" className="text-[9px] font-mono text-gray-500 truncate">
443
618
  #{element.variantId.substring(0, 8)}
444
619
  </p>
445
620
  )}
446
621
  </div>
447
- <span className="text-[9px] px-1.5 py-0.5 rounded bg-white/10 text-gray-400 uppercase">
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">
448
623
  {styles.tagName}
449
624
  </span>
450
625
  </div>
451
- {styles.textContent && (
452
- <p className="mt-1.5 text-[10px] text-gray-400 truncate px-0.5">
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">
453
628
  &ldquo;{styles.textContent}&rdquo;
454
629
  </p>
455
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
+ )}
456
636
  </div>
457
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
+
458
984
  {/* Layout Section - Always shown */}
459
985
  <Section
460
986
  title="Layout"
@@ -487,6 +1013,19 @@ export function PropertiesPanel({ element, styles, onPropertyClick, onSaveChange
487
1013
  editKey="opacity"
488
1014
  edits={edits}
489
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
+ ]}
490
1029
  />
491
1030
  <EditablePropertyRow
492
1031
  label="Radius"
@@ -494,6 +1033,17 @@ export function PropertiesPanel({ element, styles, onPropertyClick, onSaveChange
494
1033
  editKey="borderRadius"
495
1034
  edits={edits}
496
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
+ ]}
497
1047
  />
498
1048
  </div>
499
1049
  </Section>
@@ -518,6 +1068,20 @@ export function PropertiesPanel({ element, styles, onPropertyClick, onSaveChange
518
1068
  editKey="fontSize"
519
1069
  edits={edits}
520
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
+ ]}
521
1085
  />
522
1086
  <EditablePropertyRow
523
1087
  label="Weight"
@@ -525,6 +1089,15 @@ export function PropertiesPanel({ element, styles, onPropertyClick, onSaveChange
525
1089
  editKey="fontWeight"
526
1090
  edits={edits}
527
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
+ ]}
528
1101
  />
529
1102
  </div>
530
1103
  <EditablePropertyRow
@@ -533,6 +1106,13 @@ export function PropertiesPanel({ element, styles, onPropertyClick, onSaveChange
533
1106
  editKey="lineHeight"
534
1107
  edits={edits}
535
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
+ ]}
536
1116
  />
537
1117
  <EditablePropertyRow
538
1118
  label="Letter"
@@ -540,14 +1120,23 @@ export function PropertiesPanel({ element, styles, onPropertyClick, onSaveChange
540
1120
  editKey="letterSpacing"
541
1121
  edits={edits}
542
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
+ ]}
543
1131
  />
544
1132
  <EditablePropertyRow
545
- label="Color"
1133
+ label={`Color (${isDarkMode ? "dark" : "light"} mode)`}
546
1134
  value={styles.typography.color}
547
1135
  color={styles.typography.color !== "transparent" ? styles.typography.color : undefined}
548
- editKey="color"
1136
+ editKey={isDarkMode ? "colorDark" : "colorLight"}
549
1137
  edits={edits}
550
1138
  onEdit={handleEdit}
1139
+ inputType="color"
551
1140
  />
552
1141
  </div>
553
1142
  </Section>
@@ -559,6 +1148,7 @@ export function PropertiesPanel({ element, styles, onPropertyClick, onSaveChange
559
1148
  title="Fill"
560
1149
  icon={<Square className="h-3 w-3" />}
561
1150
  defaultOpen={true}
1151
+ readOnly
562
1152
  >
563
1153
  <div className="space-y-0.5">
564
1154
  {styles.fills.map((fill, i) => (
@@ -574,6 +1164,7 @@ export function PropertiesPanel({ element, styles, onPropertyClick, onSaveChange
574
1164
  title="Stroke"
575
1165
  icon={<Minus className="h-3 w-3" />}
576
1166
  defaultOpen={true}
1167
+ readOnly
577
1168
  >
578
1169
  <div className="space-y-0.5">
579
1170
  {styles.strokes.map((stroke, i) => (
@@ -589,13 +1180,14 @@ export function PropertiesPanel({ element, styles, onPropertyClick, onSaveChange
589
1180
  title="Effects"
590
1181
  icon={<Layers className="h-3 w-3" />}
591
1182
  defaultOpen={true}
1183
+ readOnly
592
1184
  >
593
1185
  <div className="space-y-0.5">
594
1186
  {styles.effects.map((effect, i) => (
595
1187
  <div key={i} className="flex items-center gap-1.5 py-0.5">
596
1188
  <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">
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">
599
1191
  {effect.value.substring(0, 30)}...
600
1192
  </span>
601
1193
  </div>
@@ -643,6 +1235,7 @@ export function PropertiesPanel({ element, styles, onPropertyClick, onSaveChange
643
1235
  title="Flexbox"
644
1236
  icon={<Palette className="h-3 w-3" />}
645
1237
  defaultOpen={false}
1238
+ readOnly
646
1239
  >
647
1240
  <div className="space-y-0.5">
648
1241
  <EditablePropertyRow
@@ -667,11 +1260,11 @@ export function PropertiesPanel({ element, styles, onPropertyClick, onSaveChange
667
1260
 
668
1261
  {/* Save/Revert Footer - Only show when there are changes */}
669
1262
  {hasChanges && (
670
- <div className="sticky bottom-0 px-2 py-2 bg-[#1a1a1a] border-t border-white/10 flex gap-2">
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">
671
1264
  <button
672
1265
  onClick={handleRevert}
673
1266
  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"
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"
675
1268
  >
676
1269
  <RotateCcw className="h-3 w-3" />
677
1270
  Revert
@@ -690,6 +1283,63 @@ export function PropertiesPanel({ element, styles, onPropertyClick, onSaveChange
690
1283
  </button>
691
1284
  </div>
692
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
+ )}
693
1343
  </div>
694
1344
  );
695
1345
  }