sonance-brand-mcp 1.3.110 → 1.3.111

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