EverydayTech Platform - Developer Reference
Complete Source Code Documentation - All Applications
Loading...
Searching...
No Matches
CustomThemeCreator.tsx
Go to the documentation of this file.
1"use client";
2
3import { useState, useEffect } from 'react';
4import { Icon } from '@/contexts/IconContext';
5import { Theme } from '@/lib/themes';
6
7interface CustomThemeCreatorProps {
8 onSave: (theme: Theme) => void;
9 onClose: () => void;
10 existingTheme?: Theme;
11}
12
13export default function CustomThemeCreator({ onSave, onClose, existingTheme }: CustomThemeCreatorProps) {
14 const [themeName, setThemeName] = useState(existingTheme?.name || '');
15 const [themeDescription, setThemeDescription] = useState(existingTheme?.description || '');
16 const [colors, setColors] = useState({
17 bg: existingTheme?.colors.bg || '#f1f5f8',
18 surface: existingTheme?.colors.surface || '#ffffff',
19 surface2: existingTheme?.colors.surface2 || '#f7fafc',
20 text: existingTheme?.colors.text || '#0f1724',
21 muted: existingTheme?.colors.muted || '#6b7280',
22 border: existingTheme?.colors.border || '#e6eef6',
23 brand: existingTheme?.colors.brand || '#00483d',
24 brand2: existingTheme?.colors.brand2 || '#00946b',
25 warn: existingTheme?.colors.warn || '#b26a00',
26 danger: existingTheme?.colors.danger || '#b00020',
27 success: existingTheme?.colors.success || '#00946b',
28 info: existingTheme?.colors.info || '#0066cc',
29 });
30
31 const colorFields = [
32 { key: 'bg', label: 'Background', description: 'Page background color' },
33 { key: 'surface', label: 'Surface', description: 'Card/panel background' },
34 { key: 'surface2', label: 'Surface 2', description: 'Subtle inset surface' },
35 { key: 'text', label: 'Text', description: 'Primary text color' },
36 { key: 'muted', label: 'Muted Text', description: 'Secondary text color' },
37 { key: 'border', label: 'Border', description: 'Border and divider color' },
38 { key: 'brand', label: 'Brand Primary', description: 'Main brand color' },
39 { key: 'brand2', label: 'Brand Secondary', description: 'Accent brand color' },
40 { key: 'success', label: 'Success', description: 'Success state color' },
41 { key: 'info', label: 'Info', description: 'Info state color' },
42 { key: 'warn', label: 'Warning', description: 'Warning state color' },
43 { key: 'danger', label: 'Danger', description: 'Error/danger state color' },
44 ];
45
46 const handleColorChange = (key: string, value: string) => {
47 setColors(prev => ({ ...prev, [key]: value }));
48 };
49
50 const handleSave = () => {
51 if (!themeName.trim()) {
52 alert('Please enter a theme name');
53 return;
54 }
55
56 const customTheme: Theme = {
57 id: existingTheme?.id || `custom-${Date.now()}`,
58 name: themeName,
59 description: themeDescription,
60 colors: {
61 ...colors,
62 brand2: colors.brand2.startsWith('#') ? colors.brand2 : `${colors.brand2}1f`,
63 },
64 shadows: {
65 shadow: `0 18px 48px ${hexToRgba(colors.brand, 0.15)}`,
66 shadowSm: `0 8px 20px ${hexToRgba(colors.brand, 0.1)}`,
67 },
68 radius: {
69 radius: '12px',
70 radiusSm: '8px',
71 },
72 focus: `0 0 0 4px ${hexToRgba(colors.brand, 0.2)}`,
73 };
74
75 onSave(customTheme);
76 };
77
78 const hexToRgba = (hex: string, alpha: number): string => {
79 const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
80 if (!result) return `rgba(0, 0, 0, ${alpha})`;
81
82 return `rgba(${parseInt(result[1], 16)}, ${parseInt(result[2], 16)}, ${parseInt(result[3], 16)}, ${alpha})`;
83 };
84
85 return (
86 <div className="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-50 p-4">
87 <div className="bg-surface rounded-2xl shadow-theme-2xl max-w-5xl w-full max-h-[90vh] overflow-hidden animate-slide-in">
88 {/* Header */}
89 <div className="gradient-brand text-surface px-6 py-4 flex items-center justify-between">
90 <div>
91 <h2 className="text-2xl font-bold">Custom Theme Creator</h2>
92 <p className="text-sm opacity-90">Design your own unique color theme</p>
93 </div>
94 <button
95 onClick={onClose}
96 className="hover:opacity-80 transition p-2"
97 aria-label="Close"
98 >
99 <Icon name="close" size={28} />
100 </button>
101 </div>
102
103 {/* Content */}
104 <div className="p-6 overflow-y-auto max-h-[calc(90vh-180px)]">
105 {/* Theme Info */}
106 <div className="mb-6">
107 <label className="block text-sm font-semibold text-text mb-2">
108 Theme Name *
109 </label>
110 <input
111 type="text"
112 value={themeName}
113 onChange={(e) => setThemeName(e.target.value)}
114 placeholder="My Custom Theme"
115 className="w-full px-4 py-2 border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-brand bg-surface text-text"
116 />
117 </div>
118
119 <div className="mb-6">
120 <label className="block text-sm font-semibold text-text mb-2">
121 Description
122 </label>
123 <input
124 type="text"
125 value={themeDescription}
126 onChange={(e) => setThemeDescription(e.target.value)}
127 placeholder="A beautiful custom theme for my workspace"
128 className="w-full px-4 py-2 border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-brand bg-surface text-text"
129 />
130 </div>
131
132 {/* Color Grid */}
133 <div className="mb-6">
134 <h3 className="text-lg font-bold text-text mb-4 flex items-center gap-2">
135 <Icon name="palette" size={24} />
136 Color Palette
137 </h3>
138 <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
139 {colorFields.map((field) => (
140 <div key={field.key} className="card p-4">
141 <label className="block text-sm font-semibold text-text mb-2">
142 {field.label}
143 </label>
144 <p className="text-xs text-muted mb-3">{field.description}</p>
145
146 <div className="flex gap-2">
147 {/* Color Picker */}
148 <div className="relative flex-1">
149 <input
150 type="color"
151 value={colors[field.key as keyof typeof colors]}
152 onChange={(e) => handleColorChange(field.key, e.target.value)}
153 className="w-full h-10 rounded-lg cursor-pointer border-2 border-border"
154 style={{
155 background: colors[field.key as keyof typeof colors],
156 }}
157 />
158 </div>
159
160 {/* Hex Input */}
161 <input
162 type="text"
163 value={colors[field.key as keyof typeof colors]}
164 onChange={(e) => handleColorChange(field.key, e.target.value)}
165 placeholder="#000000"
166 className="w-24 px-2 py-1 border border-border rounded-lg text-sm font-mono focus:outline-none focus:ring-2 focus:ring-brand bg-surface text-text"
167 />
168 </div>
169 </div>
170 ))}
171 </div>
172 </div>
173
174 {/* Preview */}
175 <div className="mb-6">
176 <h3 className="text-lg font-bold text-text mb-4 flex items-center gap-2">
177 <Icon name="visibility" size={24} />
178 Live Preview
179 </h3>
180 <div
181 className="rounded-xl p-6 border-2"
182 style={{
183 background: colors.bg,
184 borderColor: colors.border,
185 }}
186 >
187 {/* Preview Card */}
188 <div
189 className="rounded-lg p-6 border shadow-lg mb-4"
190 style={{
191 background: colors.surface,
192 borderColor: colors.border,
193 }}
194 >
195 <h4
196 className="text-xl font-bold mb-2"
197 style={{ color: colors.text }}
198 >
199 Sample Card
200 </h4>
201 <p
202 className="text-sm mb-4"
203 style={{ color: colors.muted }}
204 >
205 This is how your theme will look in the application
206 </p>
207
208 {/* Preview Buttons */}
209 <div className="flex gap-2 flex-wrap">
210 <button
211 className="px-4 py-2 rounded-lg font-semibold text-white"
212 style={{ background: colors.brand }}
213 >
214 Primary Button
215 </button>
216 <button
217 className="px-4 py-2 rounded-lg font-semibold text-white"
218 style={{ background: colors.success }}
219 >
220 Success
221 </button>
222 <button
223 className="px-4 py-2 rounded-lg font-semibold text-white"
224 style={{ background: colors.info }}
225 >
226 Info
227 </button>
228 <button
229 className="px-4 py-2 rounded-lg font-semibold text-white"
230 style={{ background: colors.warn }}
231 >
232 Warning
233 </button>
234 <button
235 className="px-4 py-2 rounded-lg font-semibold text-white"
236 style={{ background: colors.danger }}
237 >
238 Danger
239 </button>
240 </div>
241 </div>
242
243 {/* Preview Stats */}
244 <div className="grid grid-cols-3 gap-4">
245 {[colors.success, colors.info, colors.warn].map((color, idx) => (
246 <div
247 key={idx}
248 className="rounded-lg p-4 border"
249 style={{
250 background: colors.surface,
251 borderColor: colors.border,
252 }}
253 >
254 <div
255 className="text-sm mb-1"
256 style={{ color: colors.muted }}
257 >
258 Metric {idx + 1}
259 </div>
260 <div
261 className="text-2xl font-bold"
262 style={{ color }}
263 >
264 {(idx + 1) * 123}
265 </div>
266 </div>
267 ))}
268 </div>
269 </div>
270 </div>
271 </div>
272
273 {/* Footer */}
274 <div className="border-t border-border px-6 py-4 flex justify-end gap-3 bg-surface-2">
275 <button
276 onClick={onClose}
277 className="px-6 py-2 bg-surface-2 text-text rounded-lg hover:opacity-90 transition font-semibold border border-border"
278 >
279 Cancel
280 </button>
281 <button
282 onClick={handleSave}
283 className="btn btn-gradient px-6 py-2 rounded-lg font-semibold"
284 >
285 <Icon name="save" size={18} className="inline mr-2" />
286 Save Theme
287 </button>
288 </div>
289 </div>
290 </div>
291 );
292}