297 lines
10 KiB
TypeScript
297 lines
10 KiB
TypeScript
/**
|
|
* Style Editor
|
|
* Phase 7: Layout & Template System
|
|
* Component for editing CSS styles and theme customization
|
|
*/
|
|
import React, { useState } from 'react';
|
|
import { CodeIcon, PaletteIcon, TypeIcon, SaveIcon, RefreshCwIcon } from '../../icons';
|
|
import { Card } from '../../ui/card';
|
|
import Button from '../../ui/button/Button';
|
|
import Label from '../../form/Label';
|
|
import TextArea from '../../form/input/TextArea';
|
|
import { useToast } from '../../ui/toast/ToastContainer';
|
|
|
|
export interface StyleSettings {
|
|
customCSS?: string;
|
|
colorPalette?: {
|
|
primary: string;
|
|
secondary: string;
|
|
accent: string;
|
|
background: string;
|
|
text: string;
|
|
};
|
|
typography?: {
|
|
fontFamily: string;
|
|
headingFont: string;
|
|
fontSize: string;
|
|
lineHeight: string;
|
|
};
|
|
spacing?: {
|
|
base: string;
|
|
scale: string;
|
|
};
|
|
}
|
|
|
|
interface StyleEditorProps {
|
|
styleSettings: StyleSettings;
|
|
onChange: (settings: StyleSettings) => void;
|
|
onSave?: () => void;
|
|
onReset?: () => void;
|
|
}
|
|
|
|
export default function StyleEditor({ styleSettings, onChange, onSave, onReset }: StyleEditorProps) {
|
|
const toast = useToast();
|
|
const [activeTab, setActiveTab] = useState<'css' | 'colors' | 'typography' | 'spacing'>('css');
|
|
const [customCSS, setCustomCSS] = useState(styleSettings.customCSS || '');
|
|
|
|
const updateSettings = (updates: Partial<StyleSettings>) => {
|
|
onChange({ ...styleSettings, ...updates });
|
|
};
|
|
|
|
const handleCSSChange = (value: string) => {
|
|
setCustomCSS(value);
|
|
updateSettings({ customCSS: value });
|
|
};
|
|
|
|
const handleColorChange = (key: string, value: string) => {
|
|
updateSettings({
|
|
colorPalette: {
|
|
...styleSettings.colorPalette,
|
|
[key]: value,
|
|
} as StyleSettings['colorPalette'],
|
|
});
|
|
};
|
|
|
|
const handleTypographyChange = (key: string, value: string) => {
|
|
updateSettings({
|
|
typography: {
|
|
...styleSettings.typography,
|
|
[key]: value,
|
|
} as StyleSettings['typography'],
|
|
});
|
|
};
|
|
|
|
const handleSpacingChange = (key: string, value: string) => {
|
|
updateSettings({
|
|
spacing: {
|
|
...styleSettings.spacing,
|
|
[key]: value,
|
|
} as StyleSettings['spacing'],
|
|
});
|
|
};
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<div className="flex justify-between items-center">
|
|
<div>
|
|
<h2 className="text-xl font-bold text-gray-900 dark:text-white">Style Editor</h2>
|
|
<p className="text-sm text-gray-600 dark:text-gray-400">Customize CSS, colors, typography, and spacing</p>
|
|
</div>
|
|
<div className="flex gap-2">
|
|
{onReset && (
|
|
<Button variant="outline" onClick={onReset}>
|
|
<RefreshCwIcon className="w-4 h-4 mr-2" />
|
|
Reset
|
|
</Button>
|
|
)}
|
|
{onSave && (
|
|
<Button variant="primary" onClick={onSave}>
|
|
<SaveIcon className="w-4 h-4 mr-2" />
|
|
Save Styles
|
|
</Button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Tabs */}
|
|
<div className="border-b border-gray-200 dark:border-gray-700">
|
|
<div className="flex gap-4">
|
|
<button
|
|
type="button"
|
|
onClick={() => setActiveTab('css')}
|
|
className={`px-4 py-2 font-medium border-b-2 transition-colors ${
|
|
activeTab === 'css'
|
|
? 'border-brand-500 text-brand-600 dark:text-brand-400'
|
|
: 'border-transparent text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300'
|
|
}`}
|
|
>
|
|
<CodeIcon className="w-4 h-4 inline mr-2" />
|
|
Custom CSS
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => setActiveTab('colors')}
|
|
className={`px-4 py-2 font-medium border-b-2 transition-colors ${
|
|
activeTab === 'colors'
|
|
? 'border-brand-500 text-brand-600 dark:text-brand-400'
|
|
: 'border-transparent text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300'
|
|
}`}
|
|
>
|
|
<PaletteIcon className="w-4 h-4 inline mr-2" />
|
|
Colors
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => setActiveTab('typography')}
|
|
className={`px-4 py-2 font-medium border-b-2 transition-colors ${
|
|
activeTab === 'typography'
|
|
? 'border-brand-500 text-brand-600 dark:text-brand-400'
|
|
: 'border-transparent text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300'
|
|
}`}
|
|
>
|
|
<TypeIcon className="w-4 h-4 inline mr-2" />
|
|
Typography
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => setActiveTab('spacing')}
|
|
className={`px-4 py-2 font-medium border-b-2 transition-colors ${
|
|
activeTab === 'spacing'
|
|
? 'border-brand-500 text-brand-600 dark:text-brand-400'
|
|
: 'border-transparent text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300'
|
|
}`}
|
|
>
|
|
Spacing
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Custom CSS Tab */}
|
|
{activeTab === 'css' && (
|
|
<Card className="p-6">
|
|
<div className="space-y-4">
|
|
<div>
|
|
<Label>Custom CSS</Label>
|
|
<TextArea
|
|
value={customCSS}
|
|
onChange={handleCSSChange}
|
|
rows={20}
|
|
placeholder="/* Add your custom CSS here */ .custom-class { color: #333; }"
|
|
className="mt-1 font-mono text-sm"
|
|
/>
|
|
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
|
Add custom CSS rules. These will be applied to your site.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
)}
|
|
|
|
{/* Colors Tab */}
|
|
{activeTab === 'colors' && (
|
|
<Card className="p-6">
|
|
<div className="space-y-4">
|
|
{['primary', 'secondary', 'accent', 'background', 'text'].map((colorKey) => (
|
|
<div key={colorKey}>
|
|
<Label>{colorKey.charAt(0).toUpperCase() + colorKey.slice(1)} Color</Label>
|
|
<div className="flex gap-2 mt-1">
|
|
<input
|
|
type="color"
|
|
value={styleSettings.colorPalette?.[colorKey as keyof typeof styleSettings.colorPalette] || '#000000'}
|
|
onChange={(e) => handleColorChange(colorKey, e.target.value)}
|
|
className="w-16 h-10 rounded border border-gray-300 dark:border-gray-700 cursor-pointer"
|
|
/>
|
|
<input
|
|
type="text"
|
|
value={styleSettings.colorPalette?.[colorKey as keyof typeof styleSettings.colorPalette] || ''}
|
|
onChange={(e) => handleColorChange(colorKey, e.target.value)}
|
|
placeholder="#000000"
|
|
className="flex-1 px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md dark:bg-gray-800 dark:text-white"
|
|
/>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</Card>
|
|
)}
|
|
|
|
{/* Typography Tab */}
|
|
{activeTab === 'typography' && (
|
|
<Card className="p-6">
|
|
<div className="space-y-4">
|
|
<div>
|
|
<Label>Font Family</Label>
|
|
<input
|
|
type="text"
|
|
value={styleSettings.typography?.fontFamily || ''}
|
|
onChange={(e) => handleTypographyChange('fontFamily', e.target.value)}
|
|
placeholder="Arial, sans-serif"
|
|
className="mt-1 w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md dark:bg-gray-800 dark:text-white"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<Label>Heading Font</Label>
|
|
<input
|
|
type="text"
|
|
value={styleSettings.typography?.headingFont || ''}
|
|
onChange={(e) => handleTypographyChange('headingFont', e.target.value)}
|
|
placeholder="Georgia, serif"
|
|
className="mt-1 w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md dark:bg-gray-800 dark:text-white"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<Label>Base Font Size</Label>
|
|
<input
|
|
type="text"
|
|
value={styleSettings.typography?.fontSize || ''}
|
|
onChange={(e) => handleTypographyChange('fontSize', e.target.value)}
|
|
placeholder="16px"
|
|
className="mt-1 w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md dark:bg-gray-800 dark:text-white"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<Label>Line Height</Label>
|
|
<input
|
|
type="text"
|
|
value={styleSettings.typography?.lineHeight || ''}
|
|
onChange={(e) => handleTypographyChange('lineHeight', e.target.value)}
|
|
placeholder="1.5"
|
|
className="mt-1 w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md dark:bg-gray-800 dark:text-white"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
)}
|
|
|
|
{/* Spacing Tab */}
|
|
{activeTab === 'spacing' && (
|
|
<Card className="p-6">
|
|
<div className="space-y-4">
|
|
<div>
|
|
<Label>Base Spacing Unit</Label>
|
|
<input
|
|
type="text"
|
|
value={styleSettings.spacing?.base || ''}
|
|
onChange={(e) => handleSpacingChange('base', e.target.value)}
|
|
placeholder="8px"
|
|
className="mt-1 w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md dark:bg-gray-800 dark:text-white"
|
|
/>
|
|
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
|
Base unit for spacing calculations (e.g., 8px, 1rem)
|
|
</p>
|
|
</div>
|
|
|
|
<div>
|
|
<Label>Spacing Scale</Label>
|
|
<input
|
|
type="text"
|
|
value={styleSettings.spacing?.scale || ''}
|
|
onChange={(e) => handleSpacingChange('scale', e.target.value)}
|
|
placeholder="1.5"
|
|
className="mt-1 w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md dark:bg-gray-800 dark:text-white"
|
|
/>
|
|
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
|
Multiplier for spacing scale (e.g., 1.5 for 1.5x spacing)
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|