import { useState, useCallback, useMemo } from 'react'; import { useEditorStore } from '../../store/editorStore'; import type { CssRule, CssProperty } from '@shared/types'; import { Plus, Trash2, Palette, FileType, Globe } from 'lucide-react'; import { v4 as uuidv4 } from 'uuid'; function collectSelectorsFromElements(elements: any[]): string[] { const selectors = new Set(); const walk = (els: any[]) => { for (const el of els) { if (el.props?.id) selectors.add(`#${el.props.id}`); if (el.tagName) selectors.add(el.tagName); el.classes?.forEach((c: string) => { if (c) selectors.add(`.${c}`); }); if (el.children) walk(el.children); } }; walk(elements); return Array.from(selectors).sort(); } function rulesToCssText(rules: CssRule[]): string { return rules .filter((r: CssRule) => r.enabled) .map((r: CssRule) => { const props = r.properties .filter((p: CssProperty) => p.name && p.value) .map((p: CssProperty) => { const important = p.important ? ' !important' : ''; return ` ${p.name}: ${p.value}${important};`; }) .join('\n'); if (!props) return ''; const media = r.mediaQuery ? `\n@media ${r.mediaQuery} {\n` : ''; const closeMedia = r.mediaQuery ? '\n}' : ''; return `${media}${r.selector} {\n${props}\n}${closeMedia}`; }) .filter(Boolean) .join('\n\n'); } export default function CssRulePanel() { const { activeFileId, fileTree, elementRegistry, cssRules, setCssRules, updateFile, } = useEditorStore(); const activeFile = useMemo( () => activeFileId ? fileTree.find(f => f.id === activeFileId) : null, [activeFileId, fileTree] ); // Get elements from linked HTML files const linkedElementSelectors = useMemo(() => { if (!activeFileId) return []; // Find HTML files that link to this CSS file, or CSS files linked FROM HTML const linkingHtmlFiles = fileTree.filter( f => f.type === 'html' && (f.linkedFiles || []).includes(activeFileId) ); const selectors = new Set(); for (const hf of linkingHtmlFiles) { const els = elementRegistry[hf.id] || []; collectSelectorsFromElements(els).forEach(s => selectors.add(s)); } // Also add selectors from all HTML files as fallback const allHtmlFiles = fileTree.filter(f => f.type === 'html'); for (const hf of allHtmlFiles) { const els = elementRegistry[hf.id] || []; collectSelectorsFromElements(els).forEach(s => selectors.add(s)); } return Array.from(selectors).sort(); }, [activeFileId, fileTree, elementRegistry]); const rules = useMemo(() => { if (!activeFileId) return []; return cssRules[activeFileId] || []; }, [activeFileId, cssRules]); const [expandedRule, setExpandedRule] = useState(null); const addRule = useCallback(() => { if (!activeFileId) return; const newRule: CssRule = { id: uuidv4(), selector: linkedElementSelectors[0] || 'body', type: 'class', properties: [{ id: uuidv4(), name: 'color', value: '#333', important: false, type: 'standard', unit: '' }], enabled: true, }; setCssRules(activeFileId, [...rules, newRule]); setExpandedRule(newRule.id); }, [activeFileId, linkedElementSelectors, rules, setCssRules]); const updateRule = useCallback((ruleId: string, updates: Partial) => { if (!activeFileId) return; setCssRules(activeFileId, rules.map((r: CssRule) => r.id === ruleId ? { ...r, ...updates } : r)); }, [activeFileId, rules, setCssRules]); const removeRule = useCallback((ruleId: string) => { if (!activeFileId) return; setCssRules(activeFileId, rules.filter((r: CssRule) => r.id !== ruleId)); }, [activeFileId, rules, setCssRules]); const addProperty = useCallback((ruleId: string) => { const rule = rules.find((r: CssRule) => r.id === ruleId); if (!rule) return; const newProp: CssProperty = { id: uuidv4(), name: '', value: '', important: false, type: 'standard', unit: '' }; updateRule(ruleId, { properties: [...rule.properties, newProp] }); }, [rules, updateRule]); const updateProperty = useCallback((ruleId: string, propId: string, updates: Partial) => { const rule = rules.find((r: CssRule) => r.id === ruleId); if (!rule) return; updateRule(ruleId, { properties: rule.properties.map((p: CssProperty) => p.id === propId ? { ...p, ...updates } : p), }); }, [rules, updateRule]); const removeProperty = useCallback((ruleId: string, propId: string) => { const rule = rules.find((r: CssRule) => r.id === ruleId); if (!rule) return; updateRule(ruleId, { properties: rule.properties.filter((p: CssProperty) => p.id !== propId) }); }, [rules, updateRule]); // Save CSS rules to file content const applyToFile = useCallback(() => { if (!activeFileId || !activeFile) return; const cssText = rulesToCssText(rules); updateFile(activeFileId, { content: cssText }); }, [activeFileId, activeFile, rules, updateFile]); if (!activeFile || activeFile.type !== 'css') { return (
Select a CSS file to manage styles
); } return (

CSS Rules

{linkedElementSelectors.length > 0 && (
Elements: {linkedElementSelectors.slice(0, 8).map(sel => ( ))} {linkedElementSelectors.length > 8 && ( +{linkedElementSelectors.length - 8} more )}
)}
{rules.length === 0 ? (

No CSS rules yet

Click + to add a rule

) : ( rules.map(rule => (
{/* Rule header */}
setExpandedRule(expandedRule === rule.id ? null : rule.id)}> updateRule(rule.id, { selector: e.target.value })} onClick={(e) => e.stopPropagation()} className="flex-1 bg-transparent text-xs font-mono text-primary-300 outline-none" placeholder=".my-class" /> {rule.properties.filter((p: CssProperty) => p.name && p.value).length} props
{/* Rule properties */} {expandedRule === rule.id && (
{linkedElementSelectors.map(sel => ( ))}
{rule.properties.map((prop: CssProperty) => (
updateProperty(rule.id, prop.id, { name: e.target.value })} className="w-24 bg-surface-800 text-[10px] text-surface-200 rounded px-1.5 py-0.5 border border-surface-700 outline-none font-mono" placeholder="property" /> : updateProperty(rule.id, prop.id, { value: e.target.value })} className="flex-1 bg-surface-800 text-[10px] text-surface-200 rounded px-1.5 py-0.5 border border-surface-700 outline-none font-mono" placeholder="value" />
))}
)}
)) )}
); }