sonance-brand-mcp 1.3.110 → 1.3.112
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/api/sonance-ai-edit/route.ts +30 -7
- package/dist/assets/api/sonance-save-image/route.ts +625 -0
- package/dist/assets/api/sonance-vision-apply/image-styling-detection.ts +1360 -0
- package/dist/assets/api/sonance-vision-apply/route.ts +1020 -64
- package/dist/assets/api/sonance-vision-apply/styling-detection.ts +730 -0
- package/dist/assets/api/sonance-vision-apply/theme-discovery.ts +1 -1
- package/dist/assets/api/sonance-vision-edit/route.ts +33 -8
- package/dist/assets/brand-system.ts +13 -12
- package/dist/assets/components/accordion.tsx +15 -7
- package/dist/assets/components/alert-dialog.tsx +35 -10
- package/dist/assets/components/alert.tsx +11 -10
- package/dist/assets/components/avatar.tsx +4 -4
- package/dist/assets/components/badge.tsx +16 -12
- package/dist/assets/components/button.stories.tsx +3 -3
- package/dist/assets/components/button.tsx +50 -31
- package/dist/assets/components/calendar.tsx +12 -8
- package/dist/assets/components/card.tsx +35 -29
- package/dist/assets/components/checkbox.tsx +9 -8
- package/dist/assets/components/code.tsx +19 -11
- package/dist/assets/components/command.tsx +32 -13
- package/dist/assets/components/context-menu.tsx +37 -16
- package/dist/assets/components/dialog.tsx +8 -5
- package/dist/assets/components/divider.tsx +15 -5
- package/dist/assets/components/drawer.tsx +4 -3
- package/dist/assets/components/dropdown-menu.tsx +15 -13
- package/dist/assets/components/hover-card.tsx +4 -1
- package/dist/assets/components/image.tsx +1 -1
- package/dist/assets/components/input.tsx +29 -14
- package/dist/assets/components/kbd.stories.tsx +3 -3
- package/dist/assets/components/kbd.tsx +29 -13
- package/dist/assets/components/listbox.tsx +8 -8
- package/dist/assets/components/menubar.tsx +50 -23
- package/dist/assets/components/navbar.stories.tsx +140 -13
- package/dist/assets/components/navbar.tsx +22 -5
- package/dist/assets/components/navigation-menu.tsx +28 -6
- package/dist/assets/components/pagination.tsx +10 -10
- package/dist/assets/components/popover.tsx +10 -8
- package/dist/assets/components/progress.tsx +6 -4
- package/dist/assets/components/radio-group.tsx +5 -5
- package/dist/assets/components/select.tsx +49 -29
- package/dist/assets/components/separator.tsx +3 -3
- package/dist/assets/components/sheet.tsx +4 -4
- package/dist/assets/components/sidebar.tsx +10 -10
- package/dist/assets/components/skeleton.tsx +13 -5
- package/dist/assets/components/slider.tsx +12 -10
- package/dist/assets/components/switch.tsx +4 -4
- package/dist/assets/components/table.tsx +5 -5
- package/dist/assets/components/tabs.tsx +8 -8
- package/dist/assets/components/textarea.tsx +11 -9
- package/dist/assets/components/toast.tsx +7 -7
- package/dist/assets/components/toggle.tsx +27 -7
- package/dist/assets/components/tooltip.tsx +10 -8
- package/dist/assets/components/user.tsx +8 -6
- package/dist/assets/dev-tools/SonanceDevTools.tsx +851 -708
- package/dist/assets/dev-tools/components/ApplyFirstPreview.tsx +10 -10
- package/dist/assets/dev-tools/components/ChatHistory.tsx +145 -0
- package/dist/assets/dev-tools/components/ChatInterface.tsx +444 -295
- package/dist/assets/dev-tools/components/ChatTabBar.tsx +82 -0
- package/dist/assets/dev-tools/components/DiffPreview.tsx +1 -1
- package/dist/assets/dev-tools/components/InlineDiffPreview.tsx +528 -0
- package/dist/assets/dev-tools/components/InspectorOverlay.tsx +21 -18
- package/dist/assets/dev-tools/components/PropertiesPanel.tsx +1345 -0
- package/dist/assets/dev-tools/components/ScreenshotAnnotator.tsx +1 -1
- package/dist/assets/dev-tools/components/SectionHighlight.tsx +1 -1
- package/dist/assets/dev-tools/components/VisionDiffPreview.tsx +7 -7
- package/dist/assets/dev-tools/components/VisionModeBorder.tsx +12 -63
- package/dist/assets/dev-tools/constants.ts +38 -6
- package/dist/assets/dev-tools/hooks/index.ts +69 -0
- package/dist/assets/dev-tools/hooks/useComponentDetection.ts +132 -0
- package/dist/assets/dev-tools/hooks/useComputedStyles.ts +471 -0
- package/dist/assets/dev-tools/hooks/useContentHash.ts +212 -0
- package/dist/assets/dev-tools/hooks/useElementScanner.ts +398 -0
- package/dist/assets/dev-tools/hooks/useImageDetection.ts +162 -0
- package/dist/assets/dev-tools/hooks/useTextDetection.ts +217 -0
- package/dist/assets/dev-tools/index.ts +3 -0
- package/dist/assets/dev-tools/panels/AnalysisPanel.tsx +32 -32
- package/dist/assets/dev-tools/panels/ComponentsPanel.tsx +384 -131
- package/dist/assets/dev-tools/panels/TextPanel.tsx +10 -10
- package/dist/assets/dev-tools/types.ts +93 -2
- package/dist/assets/globals.css +225 -9
- package/dist/assets/styles/brand-overrides.css +3 -2
- package/dist/assets/utils.ts +2 -1
- package/dist/index.js +22 -3
- package/package.json +2 -1
|
@@ -0,0 +1,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
|
+
“{styles.textContent}”
|
|
629
|
+
</p>
|
|
630
|
+
)}
|
|
631
|
+
{edits.textContent && (
|
|
632
|
+
<p className="mt-1.5 text-[10px] text-[#00A3E1] px-0.5">
|
|
633
|
+
“{edits.textContent}”
|
|
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
|
+
}
|