/** * StylePanel Component * * Dynamic style controls panel that appears when an element is selected. * Renders different control groups based on the element's category. * Allows real-time editing of inline styles on the selected DOM element. */ import React, { useState, useCallback, useEffect } from 'react'; import { SelectedElement, StyleProperty, StyleGroup } from '../types/editor'; import { getStyleGroupsForCategory, parseStyleValue, formatStyleValue, rgbToHex, getFontDisplayName } from '../config/styleConfig'; import { getElementPath, getElementTextPreview } from '../utils/selector'; interface StylePanelProps { selectedElement: SelectedElement | null; onStyleChange: (property: string, value: string) => void; onDeselect: () => void; } // Styles for the panel (inline to avoid external dependencies) const panelStyles: { [key: string]: React.CSSProperties } = { container: { width: '320px', height: '100%', backgroundColor: '#1a1a1a', borderLeft: '1px solid #2a2a2a', display: 'flex', flexDirection: 'column', fontFamily: 'system-ui, -apple-system, sans-serif', fontSize: '13px', color: '#e5e5e5', overflow: 'hidden' }, header: { padding: '16px', borderBottom: '1px solid #2a2a2a', backgroundColor: '#222' }, headerTitle: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: '8px' }, tagBadge: { backgroundColor: '#2563eb', color: 'white', padding: '4px 10px', borderRadius: '4px', fontSize: '12px', fontWeight: '600', textTransform: 'uppercase' as const }, closeButton: { background: 'none', border: 'none', color: '#888', cursor: 'pointer', padding: '4px 8px', fontSize: '18px', lineHeight: 1 }, elementPath: { fontSize: '11px', color: '#666', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' as const }, content: { flex: 1, overflowY: 'auto' as const, padding: '8px 0' }, group: { marginBottom: '8px' }, groupHeader: { padding: '12px 16px 8px', fontSize: '11px', fontWeight: '600', color: '#888', textTransform: 'uppercase' as const, letterSpacing: '0.5px' }, propertyRow: { display: 'flex', alignItems: 'center', padding: '6px 16px', gap: '12px' }, propertyLabel: { flex: '0 0 90px', fontSize: '12px', color: '#aaa' }, propertyControl: { flex: 1, display: 'flex', alignItems: 'center', gap: '4px' }, input: { flex: 1, backgroundColor: '#2a2a2a', border: '1px solid #3a3a3a', borderRadius: '4px', padding: '6px 8px', color: '#e5e5e5', fontSize: '12px', outline: 'none' }, select: { flex: 1, backgroundColor: '#2a2a2a', border: '1px solid #3a3a3a', borderRadius: '4px', padding: '6px 8px', color: '#e5e5e5', fontSize: '12px', cursor: 'pointer', outline: 'none' }, colorInput: { width: '32px', height: '32px', padding: '2px', backgroundColor: '#2a2a2a', border: '1px solid #3a3a3a', borderRadius: '4px', cursor: 'pointer' }, colorHex: { flex: 1, backgroundColor: '#2a2a2a', border: '1px solid #3a3a3a', borderRadius: '4px', padding: '6px 8px', color: '#e5e5e5', fontSize: '12px', fontFamily: 'monospace' }, unitLabel: { fontSize: '11px', color: '#666', minWidth: '20px' }, emptyState: { display: 'flex', alignItems: 'center', justifyContent: 'center', height: '100%', color: '#666', textAlign: 'center' as const, padding: '40px' } }; export const StylePanel: React.FC = ({ selectedElement, onStyleChange, onDeselect }) => { // Track expanded groups const [expandedGroups, setExpandedGroups] = useState>(new Set()); // Initialize all groups as expanded when element changes useEffect(() => { if (selectedElement) { const groups = getStyleGroupsForCategory(selectedElement.category); setExpandedGroups(new Set(groups.map(g => g.title))); } }, [selectedElement?.category]); const toggleGroup = useCallback((title: string) => { setExpandedGroups(prev => { const next = new Set(prev); if (next.has(title)) { next.delete(title); } else { next.add(title); } return next; }); }, []); // Get current computed style value for a property const getStyleValue = useCallback((property: string): string => { if (!selectedElement) return ''; // First check inline style const inlineValue = selectedElement.node.style.getPropertyValue(property); if (inlineValue) return inlineValue; // Fall back to computed style const computed = selectedElement.computedStyles.getPropertyValue(property); return computed || ''; }, [selectedElement]); // Render empty state when nothing is selected if (!selectedElement) { return (
🎯
No Element Selected
Click on an element in the canvas to edit its styles
); } const styleGroups = getStyleGroupsForCategory(selectedElement.category); const elementPath = getElementPath(selectedElement.node); const textPreview = getElementTextPreview(selectedElement.node); return (
{/* Header with element info */}
{selectedElement.tagName}
')}> {elementPath.slice(-3).join(' › ')}
{textPreview && (
"{textPreview}"
)}
{/* Scrollable content area with style groups */}
{styleGroups.map(group => ( toggleGroup(group.title)} getStyleValue={getStyleValue} onStyleChange={onStyleChange} /> ))}
); }; // Individual style group component interface StyleGroupComponentProps { group: StyleGroup; isExpanded: boolean; onToggle: () => void; getStyleValue: (property: string) => string; onStyleChange: (property: string, value: string) => void; } const StyleGroupComponent: React.FC = ({ group, isExpanded, onToggle, getStyleValue, onStyleChange }) => { return (
{group.title} {isExpanded ? 'â–¼' : 'â–¶'}
{isExpanded && group.properties.map(property => ( onStyleChange(property.cssProperty, value)} /> ))}
); }; // Individual property control component interface StylePropertyControlProps { property: StyleProperty; value: string; onChange: (value: string) => void; } const StylePropertyControl: React.FC = ({ property, value, onChange }) => { // Local state for input value (allows typing without losing focus) const [localValue, setLocalValue] = useState(value); // Sync local value when external value changes useEffect(() => { setLocalValue(value); }, [value]); const handleInputChange = (newValue: string) => { setLocalValue(newValue); }; const handleInputBlur = () => { if (localValue !== value) { onChange(localValue); } }; const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === 'Enter') { onChange(localValue); } }; // Render different control types const renderControl = () => { switch (property.type) { case 'color': return ; case 'select': return ( ); case 'unit': return ( ); case 'number': return ( handleInputChange(e.target.value)} onBlur={handleInputBlur} onKeyDown={handleKeyDown} /> ); case 'slider': const sliderValue = parseStyleValue(localValue).value; return (
onChange(formatStyleValue(Number(e.target.value), property.unit || ''))} style={{ width: '100%' }} />
{sliderValue}{property.unit}
); default: return ( handleInputChange(e.target.value)} onBlur={handleInputBlur} onKeyDown={handleKeyDown} /> ); } }; return (
{renderControl()}
); }; // Color picker control interface ColorControlProps { value: string; onChange: (value: string) => void; } const ColorControl: React.FC = ({ value, onChange }) => { const hexValue = rgbToHex(value); const [localHex, setLocalHex] = useState(hexValue); useEffect(() => { setLocalHex(rgbToHex(value)); }, [value]); return ( <> { setLocalHex(e.target.value); onChange(e.target.value); }} /> setLocalHex(e.target.value)} onBlur={() => onChange(localHex)} onKeyDown={(e) => e.key === 'Enter' && onChange(localHex)} maxLength={7} /> ); }; // Unit input control (value + unit) interface UnitControlProps { value: string; unit: string; onChange: (value: string) => void; onBlur: () => void; onKeyDown: (e: React.KeyboardEvent) => void; } const UnitControl: React.FC = ({ value, unit, onChange, onBlur, onKeyDown }) => { const parsed = parseStyleValue(value); const displayValue = parsed.value || ''; const displayUnit = parsed.unit || unit; return ( <> { const numValue = e.target.value; onChange(numValue ? `${numValue}${displayUnit}` : ''); }} onBlur={onBlur} onKeyDown={onKeyDown} /> {displayUnit && ( {displayUnit} )} ); }; export default StylePanel;