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.
- 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 +988 -57
- 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/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 +429 -362
- package/dist/assets/dev-tools/components/ApplyFirstPreview.tsx +10 -10
- package/dist/assets/dev-tools/components/ChatHistory.tsx +11 -7
- package/dist/assets/dev-tools/components/ChatInterface.tsx +61 -20
- package/dist/assets/dev-tools/components/ChatTabBar.tsx +1 -1
- package/dist/assets/dev-tools/components/DiffPreview.tsx +1 -1
- package/dist/assets/dev-tools/components/InlineDiffPreview.tsx +360 -36
- package/dist/assets/dev-tools/components/InspectorOverlay.tsx +9 -9
- package/dist/assets/dev-tools/components/PropertiesPanel.tsx +743 -93
- 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 +4 -64
- 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 +171 -65
- 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/panels/ComponentsPanel.tsx +160 -57
- package/dist/assets/dev-tools/panels/TextPanel.tsx +10 -10
- package/dist/assets/dev-tools/types.ts +42 -0
- package/dist/assets/globals.css +225 -9
- package/dist/assets/styles/brand-overrides.css +3 -2
- package/dist/assets/utils.ts +2 -1
- 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=
|
|
135
|
-
|
|
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
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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-
|
|
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-
|
|
185
|
-
<span className="text-[9px] text-gray-500 uppercase
|
|
186
|
-
<span className="text-[10px] font-mono text-
|
|
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-
|
|
190
|
-
<span className="text-[9px] text-gray-500 uppercase
|
|
191
|
-
<span className="text-[10px] font-mono text-
|
|
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/
|
|
196
|
-
widthEdited ? "border-[#00A3E1]" : "border-white/
|
|
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
|
-
<
|
|
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-
|
|
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/
|
|
209
|
-
heightEdited ? "border-[#00A3E1]" : "border-white/
|
|
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
|
-
<
|
|
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-
|
|
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({
|
|
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
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
“{styles.textContent}”
|
|
454
629
|
</p>
|
|
455
630
|
)}
|
|
631
|
+
{edits.textContent && (
|
|
632
|
+
<p className="mt-1.5 text-[10px] text-[#00A3E1] px-0.5">
|
|
633
|
+
“{edits.textContent}”
|
|
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=
|
|
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="
|
|
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
|
}
|