@ramme-io/create-app 2.0.0-alpha.4 → 2.0.1
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.
- package/README.md +2 -2
- package/index.js +3 -3
- package/package.json +1 -1
- package/template/index.html +1 -1
- package/template/package.json +15 -6
- package/template/public/_redirects +1 -0
- package/template/src/App.tsx +3 -2
- package/template/src/components/AppHeader.tsx +5 -0
- package/template/src/components/ScrollToTop.tsx +6 -6
- package/template/src/features/ai/pages/AiChat.tsx +136 -37
- package/template/src/features/auth/AuthContext.tsx +16 -6
- package/template/src/features/config/AppConfigContext.tsx +2 -2
- package/template/src/features/docs/pages/EdgeTelemetryDemo.tsx +149 -0
- package/template/src/features/onboarding/pages/AboutRamme.tsx +12 -12
- package/template/src/features/onboarding/pages/PrototypeGallery.tsx +8 -6
- package/template/src/features/onboarding/pages/RammeFeatures.tsx +18 -17
- package/template/src/features/onboarding/pages/RammeTutorial.tsx +20 -11
- package/template/src/features/onboarding/pages/Welcome.tsx +6 -6
- package/template/src/features/styleguide/sections/tables/TablesSection.tsx +25 -5
- package/template/src/features/theme/pages/ThemeCustomizerPage.tsx +344 -256
- package/template/src/features/theme/utils/ThemeGenerator.logic.ts +587 -0
- package/template/src/hooks/__tests__/useStudioHotkeys.test.ts +100 -0
- package/template/src/hooks/useStudioHotkeys.ts +36 -0
- package/template/src/index.css +91 -1
- package/template/src/main.tsx +44 -2
- package/template/src/templates/dashboard/DashboardLayout.tsx +6 -1
- package/template/src/templates/dashboard/dashboard.sitemap.ts +1 -1
- package/template/src/templates/docs/docs.sitemap.ts +8 -0
- package/template/src/templates/settings/SettingsLayout.tsx +13 -26
- package/template/src/test/setup.ts +1 -0
- package/template/tsconfig.app.json +1 -0
- package/template/vite.config.ts +7 -0
|
@@ -1,313 +1,401 @@
|
|
|
1
|
-
import React, { useState, useEffect } from 'react';
|
|
1
|
+
import React, { useState, useEffect, useCallback } from 'react';
|
|
2
2
|
import { StandardPageLayout } from '@/components/layout/StandardPageLayout';
|
|
3
3
|
import { Card, Input, Button, Icon, Label } from '@ramme-io/ui';
|
|
4
|
-
import { generateThemeFromPrompt
|
|
5
|
-
import {
|
|
4
|
+
import { generateThemeFromPrompt } from '../services/themeAgent';
|
|
5
|
+
import { applyThemeToDom, loadGoogleFont } from '../utils/themeUtils';
|
|
6
|
+
import {
|
|
7
|
+
generateFromPrompt,
|
|
8
|
+
generateFromProfile,
|
|
9
|
+
generateRandom,
|
|
10
|
+
paletteToRgbVars,
|
|
11
|
+
hslToCssString,
|
|
12
|
+
AESTHETIC_PROFILES,
|
|
13
|
+
type GeneratedTheme,
|
|
14
|
+
type ThemePalette,
|
|
15
|
+
} from '../utils/ThemeGenerator.logic';
|
|
16
|
+
|
|
17
|
+
// ─── SWATCH COMPONENT ────────────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
const SWATCH_TOKENS: { label: string; key: keyof ThemePalette }[] = [
|
|
20
|
+
{ label: 'Background', key: 'background' },
|
|
21
|
+
{ label: 'Foreground', key: 'foreground' },
|
|
22
|
+
{ label: 'Primary', key: 'primary' },
|
|
23
|
+
{ label: 'Secondary', key: 'secondary' },
|
|
24
|
+
{ label: 'Accent', key: 'accent' },
|
|
25
|
+
{ label: 'Muted', key: 'muted' },
|
|
26
|
+
{ label: 'Border', key: 'border' },
|
|
27
|
+
{ label: 'Card', key: 'card' },
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
function PaletteSwatch({ palette, label }: { palette: ThemePalette; label: string }) {
|
|
31
|
+
return (
|
|
32
|
+
<div className="space-y-3">
|
|
33
|
+
<h4 className="text-[10px] font-bold uppercase tracking-widest opacity-50">{label} Palette</h4>
|
|
34
|
+
<div className="grid grid-cols-4 md:grid-cols-8 gap-2">
|
|
35
|
+
{SWATCH_TOKENS.map((token) => (
|
|
36
|
+
<div key={token.key} className="space-y-1.5">
|
|
37
|
+
<div
|
|
38
|
+
className="h-10 w-full rounded-lg border border-black/10 shadow-sm transition-all duration-500"
|
|
39
|
+
style={{ backgroundColor: hslToCssString(palette[token.key]) }}
|
|
40
|
+
/>
|
|
41
|
+
<div className="text-[9px] font-mono opacity-50 truncate text-center">{token.label}</div>
|
|
42
|
+
</div>
|
|
43
|
+
))}
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ─── PROFILE ICON MAP ────────────────────────────────────────────────────────
|
|
50
|
+
|
|
51
|
+
const PROFILE_ICONS: Record<string, string> = {
|
|
52
|
+
cyberpunk: 'zap',
|
|
53
|
+
nordic: 'snowflake',
|
|
54
|
+
earthy: 'leaf',
|
|
55
|
+
corporate: 'briefcase',
|
|
56
|
+
minimalist: 'minus-square',
|
|
57
|
+
sunset: 'sunset',
|
|
58
|
+
ocean: 'waves',
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
// ─── MAIN COMPONENT ─────────────────────────────────────────────────────────
|
|
6
62
|
|
|
7
63
|
export default function ThemeCustomizerPage() {
|
|
8
64
|
const [prompt, setPrompt] = useState('');
|
|
9
65
|
const [loading, setLoading] = useState(false);
|
|
10
|
-
const [
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
66
|
+
const [isDarkMode, setIsDarkMode] = useState(false);
|
|
67
|
+
const [generatedTheme, setGeneratedTheme] = useState<GeneratedTheme | null>(null);
|
|
68
|
+
|
|
69
|
+
// Gemini Deep AI state
|
|
15
70
|
const [geminiKey, setGeminiKey] = useState('');
|
|
16
71
|
const [showKeyInput, setShowKeyInput] = useState(false);
|
|
17
72
|
|
|
18
73
|
useEffect(() => {
|
|
19
74
|
const savedKey = localStorage.getItem('ramme_vault_gemini_key') || '';
|
|
20
75
|
setGeminiKey(savedKey);
|
|
21
|
-
if (!savedKey) setShowKeyInput(true);
|
|
22
76
|
}, []);
|
|
23
77
|
|
|
78
|
+
// Apply theme to DOM whenever generated theme or dark mode changes
|
|
79
|
+
const applyGeneratedTheme = useCallback((theme: GeneratedTheme, dark: boolean) => {
|
|
80
|
+
const palette = dark ? theme.dark : theme.light;
|
|
81
|
+
const vars = paletteToRgbVars(palette);
|
|
82
|
+
document.documentElement.classList.remove('midnight', 'corporate', 'blueprint', 'dark');
|
|
83
|
+
applyThemeToDom(vars);
|
|
84
|
+
|
|
85
|
+
// Load fonts
|
|
86
|
+
if (theme.fonts?.heading && theme.fonts?.body) {
|
|
87
|
+
loadGoogleFont(theme.fonts.heading, theme.fonts.body);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Persist
|
|
91
|
+
const persist = { theme, isDark: dark };
|
|
92
|
+
localStorage.setItem('ramme_themelab', JSON.stringify(persist));
|
|
93
|
+
localStorage.setItem('ramme_persisted_theme', JSON.stringify({
|
|
94
|
+
name: theme.name,
|
|
95
|
+
description: theme.description,
|
|
96
|
+
fonts: theme.fonts,
|
|
97
|
+
cssVars: vars,
|
|
98
|
+
}));
|
|
99
|
+
}, []);
|
|
100
|
+
|
|
101
|
+
// Restore on mount
|
|
24
102
|
useEffect(() => {
|
|
25
|
-
|
|
26
|
-
localStorage.
|
|
27
|
-
if (
|
|
28
|
-
|
|
103
|
+
try {
|
|
104
|
+
const saved = localStorage.getItem('ramme_themelab');
|
|
105
|
+
if (saved) {
|
|
106
|
+
const { theme, isDark } = JSON.parse(saved);
|
|
107
|
+
if (theme && theme.light && theme.dark) {
|
|
108
|
+
setGeneratedTheme(theme);
|
|
109
|
+
setIsDarkMode(isDark);
|
|
110
|
+
applyGeneratedTheme(theme, isDark);
|
|
111
|
+
}
|
|
29
112
|
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
}, [currentTheme]);
|
|
113
|
+
} catch { /* ignore */ }
|
|
114
|
+
}, [applyGeneratedTheme]);
|
|
33
115
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
116
|
+
// ── HANDLERS ──────────────────────────────────────────────────────────────
|
|
117
|
+
|
|
118
|
+
const handleLocalGenerate = (e: React.FormEvent) => {
|
|
119
|
+
e.preventDefault();
|
|
120
|
+
if (!prompt.trim()) return;
|
|
121
|
+
const theme = generateFromPrompt(prompt);
|
|
122
|
+
setGeneratedTheme(theme);
|
|
123
|
+
applyGeneratedTheme(theme, isDarkMode);
|
|
37
124
|
};
|
|
38
125
|
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
}
|
|
126
|
+
const handleProfileGenerate = (profileId: string) => {
|
|
127
|
+
const theme = generateFromProfile(profileId);
|
|
128
|
+
setGeneratedTheme(theme);
|
|
129
|
+
applyGeneratedTheme(theme, isDarkMode);
|
|
44
130
|
};
|
|
45
131
|
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
alert("Theme DNA copied to clipboard! You can paste this into a JSON file or share it with the team.");
|
|
132
|
+
const handleShuffle = () => {
|
|
133
|
+
const theme = generateRandom();
|
|
134
|
+
setGeneratedTheme(theme);
|
|
135
|
+
applyGeneratedTheme(theme, isDarkMode);
|
|
51
136
|
};
|
|
52
137
|
|
|
53
|
-
const
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
};
|
|
60
|
-
setCurrentTheme(randomTheme);
|
|
61
|
-
applyThemeToDom(vars);
|
|
138
|
+
const handleToggleDark = () => {
|
|
139
|
+
const next = !isDarkMode;
|
|
140
|
+
setIsDarkMode(next);
|
|
141
|
+
if (generatedTheme) {
|
|
142
|
+
applyGeneratedTheme(generatedTheme, next);
|
|
143
|
+
}
|
|
62
144
|
};
|
|
63
145
|
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
const fontImport = currentTheme.fonts
|
|
69
|
-
? `@import url('https://fonts.googleapis.com/css2?family=${currentTheme.fonts.heading.replace(/ /g, '+')}&family=${currentTheme.fonts.body.replace(/ /g, '+')}&display=swap');\n\n`
|
|
70
|
-
: '';
|
|
71
|
-
|
|
72
|
-
// 2. Build the CSS variables block
|
|
73
|
-
const cssVars = Object.entries(currentTheme.cssVars)
|
|
74
|
-
.map(([key, val]) => ` ${key}: ${val};`)
|
|
75
|
-
.join('\n');
|
|
76
|
-
|
|
77
|
-
// 3. Add font-family assignments
|
|
78
|
-
const fontVars = currentTheme.fonts
|
|
79
|
-
? ` --font-heading: '${currentTheme.fonts.heading}', sans-serif;\n --font-body: '${currentTheme.fonts.body}', sans-serif;\n`
|
|
80
|
-
: '';
|
|
81
|
-
|
|
82
|
-
const fullCSS = `${fontImport}/* Theme: ${currentTheme.name} */\n/* Description: ${currentTheme.description} */\n\n:root {\n${cssVars}\n${fontVars}}`;
|
|
83
|
-
|
|
84
|
-
// 4. Trigger the download
|
|
85
|
-
const blob = new Blob([fullCSS], { type: 'text/css' });
|
|
86
|
-
const url = URL.createObjectURL(blob);
|
|
87
|
-
const link = document.createElement('a');
|
|
88
|
-
link.href = url;
|
|
89
|
-
link.download = `${currentTheme.name.toLowerCase().replace(/\s+/g, '-')}-theme.css`;
|
|
90
|
-
document.body.appendChild(link);
|
|
91
|
-
link.click();
|
|
92
|
-
document.body.removeChild(link);
|
|
93
|
-
URL.revokeObjectURL(url);
|
|
146
|
+
const handleReset = () => {
|
|
147
|
+
localStorage.removeItem('ramme_themelab');
|
|
148
|
+
localStorage.removeItem('ramme_persisted_theme');
|
|
149
|
+
window.location.reload();
|
|
94
150
|
};
|
|
95
151
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
if (!prompt) return;
|
|
152
|
+
// Deep AI (Gemini)
|
|
153
|
+
const handleDeepAiGenerate = async () => {
|
|
154
|
+
if (!prompt.trim()) return;
|
|
99
155
|
if (!geminiKey) { setShowKeyInput(true); return; }
|
|
100
|
-
|
|
101
156
|
setLoading(true);
|
|
102
157
|
try {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
}
|
|
109
|
-
setCurrentTheme(theme);
|
|
110
|
-
applyThemeToDom(theme.cssVars);
|
|
158
|
+
const theme = await generateThemeFromPrompt(prompt);
|
|
159
|
+
if (theme?.cssVars) {
|
|
160
|
+
document.documentElement.classList.remove('midnight', 'corporate', 'blueprint', 'dark');
|
|
161
|
+
if (theme.fonts?.heading && theme.fonts?.body) {
|
|
162
|
+
loadGoogleFont(theme.fonts.heading, theme.fonts.body);
|
|
111
163
|
}
|
|
164
|
+
applyThemeToDom(theme.cssVars);
|
|
165
|
+
localStorage.setItem('ramme_persisted_theme', JSON.stringify(theme));
|
|
166
|
+
setGeneratedTheme(null); // clear local theme state
|
|
167
|
+
}
|
|
112
168
|
} catch (err: any) {
|
|
113
|
-
|
|
114
|
-
alert(err.message || "Snag in the orchestration.");
|
|
169
|
+
alert(err.message || 'Deep AI generation failed.');
|
|
115
170
|
} finally {
|
|
116
|
-
|
|
171
|
+
setLoading(false);
|
|
117
172
|
}
|
|
118
173
|
};
|
|
119
174
|
|
|
175
|
+
const handleSaveKey = () => {
|
|
176
|
+
localStorage.setItem('ramme_vault_gemini_key', geminiKey);
|
|
177
|
+
setShowKeyInput(false);
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
const handleCopyVars = () => {
|
|
181
|
+
if (!generatedTheme) return;
|
|
182
|
+
const palette = isDarkMode ? generatedTheme.dark : generatedTheme.light;
|
|
183
|
+
const vars = paletteToRgbVars(palette);
|
|
184
|
+
const css = Object.entries(vars).map(([k, v]) => `${k}: ${v};`).join('\n');
|
|
185
|
+
navigator.clipboard.writeText(`:root {\n${css}\n}`);
|
|
186
|
+
alert('CSS variables copied!');
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
const activePalette = generatedTheme ? (isDarkMode ? generatedTheme.dark : generatedTheme.light) : null;
|
|
190
|
+
|
|
191
|
+
// ── RENDER ────────────────────────────────────────────────────────────────
|
|
192
|
+
|
|
120
193
|
return (
|
|
121
|
-
<StandardPageLayout title="Theme
|
|
122
|
-
<div className="max-w-
|
|
123
|
-
|
|
124
|
-
{/*
|
|
194
|
+
<StandardPageLayout title="Theme Lab" description="Generate, randomize, and preview entire design systems instantly.">
|
|
195
|
+
<div className="max-w-6xl mx-auto space-y-8 pb-20">
|
|
196
|
+
|
|
197
|
+
{/* ── ROW 1: PROMPT + SHUFFLE ──────────────────────────────────── */}
|
|
198
|
+
<div className="grid grid-cols-1 lg:grid-cols-4 gap-4">
|
|
199
|
+
<Card className="p-5 lg:col-span-3 border-2 border-primary/20">
|
|
200
|
+
<div className="flex items-center justify-between mb-3">
|
|
201
|
+
<h2 className="font-bold flex items-center gap-2 uppercase tracking-widest text-xs">
|
|
202
|
+
<Icon name="sparkles" size={16} className="text-primary" />
|
|
203
|
+
Describe Your Vibe
|
|
204
|
+
</h2>
|
|
205
|
+
<div className="flex items-center gap-3">
|
|
206
|
+
<button onClick={handleReset} className="text-[10px] uppercase font-bold text-red-500/60 hover:text-red-500 transition-colors">Reset</button>
|
|
207
|
+
</div>
|
|
208
|
+
</div>
|
|
209
|
+
<form onSubmit={handleLocalGenerate} className="flex gap-3">
|
|
210
|
+
<Input
|
|
211
|
+
placeholder='"Moody lavender sunset", "Clean Swiss medical", "Forest cabin"...'
|
|
212
|
+
value={prompt}
|
|
213
|
+
onChange={(e) => setPrompt(e.target.value)}
|
|
214
|
+
className="flex-1"
|
|
215
|
+
/>
|
|
216
|
+
<Button type="submit" className="w-32 gap-2">
|
|
217
|
+
<Icon name="sparkles" size={14} />
|
|
218
|
+
Generate
|
|
219
|
+
</Button>
|
|
220
|
+
</form>
|
|
221
|
+
{geminiKey && (
|
|
222
|
+
<div className="mt-2 flex items-center gap-2">
|
|
223
|
+
<button
|
|
224
|
+
type="button"
|
|
225
|
+
onClick={handleDeepAiGenerate}
|
|
226
|
+
disabled={loading || !prompt.trim()}
|
|
227
|
+
className="text-[10px] uppercase font-bold text-primary/60 hover:text-primary transition-colors disabled:opacity-30"
|
|
228
|
+
>
|
|
229
|
+
{loading ? '⏳ Generating...' : '🧠 Deep AI (Gemini)'}
|
|
230
|
+
</button>
|
|
231
|
+
<button onClick={() => setShowKeyInput(!showKeyInput)} className="text-[10px] uppercase font-bold opacity-30 hover:opacity-60 transition-opacity">
|
|
232
|
+
API Key
|
|
233
|
+
</button>
|
|
234
|
+
</div>
|
|
235
|
+
)}
|
|
236
|
+
{!geminiKey && (
|
|
237
|
+
<button onClick={() => setShowKeyInput(!showKeyInput)} className="mt-2 text-[10px] uppercase font-bold opacity-30 hover:opacity-60 transition-opacity">
|
|
238
|
+
+ Add Gemini API Key for Deep AI
|
|
239
|
+
</button>
|
|
240
|
+
)}
|
|
241
|
+
</Card>
|
|
242
|
+
|
|
243
|
+
<Card
|
|
244
|
+
className="p-5 flex flex-col justify-center items-center cursor-pointer border-dashed border-2 hover:border-primary/40 hover:bg-muted/30 transition-all active:scale-[0.97]"
|
|
245
|
+
onClick={handleShuffle}
|
|
246
|
+
>
|
|
247
|
+
<Icon name="shuffle" size={28} className="mb-2 text-primary" />
|
|
248
|
+
<h3 className="font-bold text-sm">Shuffle</h3>
|
|
249
|
+
<p className="text-[10px] opacity-50 italic">Random aesthetic</p>
|
|
250
|
+
</Card>
|
|
251
|
+
</div>
|
|
252
|
+
|
|
253
|
+
{/* Gemini Key Input (collapsible) */}
|
|
125
254
|
{showKeyInput && (
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
value={geminiKey}
|
|
136
|
-
onChange={(e) => setGeminiKey(e.target.value)}
|
|
137
|
-
className="bg-[rgb(var(--app-card-bg-color))] border-[rgb(var(--app-border-color))] focus:border-[rgb(var(--app-primary-color))]"
|
|
138
|
-
/>
|
|
139
|
-
</div>
|
|
140
|
-
<Button onClick={handleSaveKey} className="bg-[rgb(var(--app-primary-color))] text-[rgb(var(--app-primary-foreground-color))] w-full md:w-auto">
|
|
141
|
-
Save to Vault
|
|
142
|
-
</Button>
|
|
143
|
-
</div>
|
|
144
|
-
</Card>
|
|
255
|
+
<Card className="p-4 border-primary/30 bg-primary/5 animate-in fade-in slide-in-from-top-2">
|
|
256
|
+
<div className="flex flex-col md:flex-row items-center gap-3">
|
|
257
|
+
<div className="flex-1 w-full">
|
|
258
|
+
<Label className="text-[10px] font-bold uppercase mb-1.5 block text-primary">Gemini API Key</Label>
|
|
259
|
+
<Input type="password" placeholder="Paste key..." value={geminiKey} onChange={(e) => setGeminiKey(e.target.value)} />
|
|
260
|
+
</div>
|
|
261
|
+
<Button onClick={handleSaveKey} size="sm">Save</Button>
|
|
262
|
+
</div>
|
|
263
|
+
</Card>
|
|
145
264
|
)}
|
|
146
265
|
|
|
147
|
-
{/*
|
|
148
|
-
<div
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
</Card>
|
|
174
|
-
|
|
175
|
-
<Card className="p-6 flex flex-col justify-center items-center cursor-pointer border-dashed border-2 hover:bg-[rgb(var(--app-muted-bg))]/50 transition-all active:scale-95" onClick={handleRandomize}>
|
|
176
|
-
<Icon name="shuffle" className="mb-2" />
|
|
177
|
-
<h3 className="font-bold text-sm">Instant Randomizer</h3>
|
|
178
|
-
<p className="text-[10px] opacity-60 italic">Roll the RGB dice</p>
|
|
179
|
-
</Card>
|
|
266
|
+
{/* ── ROW 2: AESTHETIC PROFILES ────────────────────────────────── */}
|
|
267
|
+
<div>
|
|
268
|
+
<h3 className="text-[10px] font-bold uppercase tracking-widest opacity-40 mb-3">Aesthetic Profiles</h3>
|
|
269
|
+
<div className="grid grid-cols-2 sm:grid-cols-4 lg:grid-cols-7 gap-3">
|
|
270
|
+
{AESTHETIC_PROFILES.map((profile) => (
|
|
271
|
+
<button
|
|
272
|
+
key={profile.id}
|
|
273
|
+
onClick={() => handleProfileGenerate(profile.id)}
|
|
274
|
+
className={`group p-4 rounded-xl border-2 transition-all duration-300 text-left hover:scale-[1.03] active:scale-[0.97] ${
|
|
275
|
+
generatedTheme?.profileId === profile.id
|
|
276
|
+
? 'border-primary bg-primary/10 shadow-lg shadow-primary/10'
|
|
277
|
+
: 'border-border hover:border-primary/30 bg-card'
|
|
278
|
+
}`}
|
|
279
|
+
>
|
|
280
|
+
<Icon
|
|
281
|
+
name={(PROFILE_ICONS[profile.id] || 'palette') as any}
|
|
282
|
+
size={20}
|
|
283
|
+
className={`mb-2 transition-colors ${
|
|
284
|
+
generatedTheme?.profileId === profile.id ? 'text-primary' : 'opacity-40 group-hover:opacity-80'
|
|
285
|
+
}`}
|
|
286
|
+
/>
|
|
287
|
+
<div className="font-bold text-xs">{profile.name}</div>
|
|
288
|
+
<div className="text-[9px] opacity-40 mt-0.5 leading-tight">{profile.description.slice(0, 40)}...</div>
|
|
289
|
+
</button>
|
|
290
|
+
))}
|
|
291
|
+
</div>
|
|
180
292
|
</div>
|
|
181
293
|
|
|
182
|
-
{/*
|
|
183
|
-
|
|
184
|
-
<Card className="p-6
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
onClick={handleDownloadCSS}
|
|
202
|
-
className="h-8 text-[10px] uppercase border border-[rgb(var(--app-primary-color))]/30 text-[rgb(var(--app-primary-color))] hover:bg-[rgb(var(--app-primary-color))] hover:text-white"
|
|
203
|
-
>
|
|
204
|
-
<Icon name="download" size={12} className="mr-2" /> Download .css
|
|
205
|
-
</Button>
|
|
206
|
-
</div>
|
|
207
|
-
</div>
|
|
208
|
-
|
|
209
|
-
<div className="grid grid-cols-2 md:grid-cols-6 gap-4">
|
|
210
|
-
{[
|
|
211
|
-
{ label: 'bg-color', var: '--app-bg-color' },
|
|
212
|
-
{ label: 'text-color', var: '--app-text-color' },
|
|
213
|
-
{ label: 'primary', var: '--app-primary-color' },
|
|
214
|
-
{ label: 'card-bg', var: '--app-card-bg-color' },
|
|
215
|
-
{ label: 'muted', var: '--app-muted-bg' },
|
|
216
|
-
{ label: 'border', var: '--app-border-color' },
|
|
217
|
-
].map((token) => (
|
|
218
|
-
<div key={token.var} className="space-y-2">
|
|
219
|
-
<div className="h-12 w-full rounded-md border shadow-sm transition-all duration-700" style={{ backgroundColor: `rgb(var(${token.var}))` }} />
|
|
220
|
-
<div className="text-[10px] font-mono opacity-60 truncate">{token.label}</div>
|
|
221
|
-
</div>
|
|
222
|
-
))}
|
|
294
|
+
{/* ── ROW 3: DARK MODE TOGGLE + SWATCHES ──────────────────────── */}
|
|
295
|
+
{generatedTheme && activePalette && (
|
|
296
|
+
<Card className="p-6 space-y-6">
|
|
297
|
+
<div className="flex items-center justify-between">
|
|
298
|
+
<h3 className="font-bold flex items-center gap-2 text-sm">
|
|
299
|
+
<Icon name="palette" size={16} className="text-primary" />
|
|
300
|
+
{generatedTheme.name}
|
|
301
|
+
</h3>
|
|
302
|
+
<div className="flex items-center gap-4">
|
|
303
|
+
<button onClick={handleCopyVars} className="text-[10px] uppercase font-bold opacity-40 hover:opacity-80 transition-opacity flex items-center gap-1">
|
|
304
|
+
<Icon name="copy" size={12} /> Copy CSS
|
|
305
|
+
</button>
|
|
306
|
+
<button
|
|
307
|
+
onClick={handleToggleDark}
|
|
308
|
+
className="flex items-center gap-2 px-4 py-2 rounded-full border-2 border-border hover:border-primary/40 transition-all bg-card"
|
|
309
|
+
>
|
|
310
|
+
<Icon name={isDarkMode ? 'moon' : 'sun'} size={16} className="text-primary" />
|
|
311
|
+
<span className="text-xs font-bold uppercase tracking-wider">{isDarkMode ? 'Dark' : 'Light'}</span>
|
|
312
|
+
</button>
|
|
223
313
|
</div>
|
|
224
|
-
|
|
314
|
+
</div>
|
|
225
315
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
<
|
|
316
|
+
<p className="text-xs italic opacity-50">{generatedTheme.description}</p>
|
|
317
|
+
|
|
318
|
+
{/* Font Pair */}
|
|
319
|
+
{generatedTheme.fonts && (
|
|
320
|
+
<div className="flex items-center gap-6 py-2 px-3 rounded-lg bg-muted/30 border border-border/50">
|
|
321
|
+
<div className="flex items-center gap-2">
|
|
322
|
+
<Icon name="type" size={14} className="opacity-40" />
|
|
323
|
+
<span className="text-[10px] uppercase font-bold tracking-widest opacity-40">Heading</span>
|
|
324
|
+
<span className="text-xs font-bold" style={{ fontFamily: `'${generatedTheme.fonts.heading}', sans-serif` }}>{generatedTheme.fonts.heading}</span>
|
|
325
|
+
</div>
|
|
326
|
+
<div className="h-4 w-px bg-border" />
|
|
327
|
+
<div className="flex items-center gap-2">
|
|
328
|
+
<span className="text-[10px] uppercase font-bold tracking-widest opacity-40">Body</span>
|
|
329
|
+
<span className="text-xs font-medium" style={{ fontFamily: `'${generatedTheme.fonts.body}', sans-serif` }}>{generatedTheme.fonts.body}</span>
|
|
330
|
+
</div>
|
|
232
331
|
</div>
|
|
332
|
+
)}
|
|
333
|
+
|
|
334
|
+
{/* Active Palette Swatches */}
|
|
335
|
+
<PaletteSwatch palette={activePalette} label={isDarkMode ? 'Dark' : 'Light'} />
|
|
336
|
+
|
|
337
|
+
{/* Both palettes side-by-side preview */}
|
|
338
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 pt-4 border-t border-border/50">
|
|
233
339
|
<div>
|
|
234
|
-
<
|
|
235
|
-
<
|
|
236
|
-
|
|
340
|
+
<h4 className="text-[10px] font-bold uppercase tracking-widest opacity-40 mb-3">Light Preview</h4>
|
|
341
|
+
<div
|
|
342
|
+
className="p-4 rounded-xl border transition-all duration-500"
|
|
343
|
+
style={{ backgroundColor: hslToCssString(generatedTheme.light.background), color: hslToCssString(generatedTheme.light.foreground), borderColor: hslToCssString(generatedTheme.light.border) }}
|
|
344
|
+
>
|
|
345
|
+
<div className="flex items-center gap-2 mb-3">
|
|
346
|
+
<div className="h-3 w-3 rounded-full" style={{ backgroundColor: hslToCssString(generatedTheme.light.primary) }} />
|
|
347
|
+
<span className="text-xs font-bold">Primary Button</span>
|
|
348
|
+
</div>
|
|
349
|
+
<div className="h-8 rounded-md mb-2 transition-all duration-500" style={{ backgroundColor: hslToCssString(generatedTheme.light.card), borderColor: hslToCssString(generatedTheme.light.border), borderWidth: 1 }} />
|
|
350
|
+
<div className="flex gap-2">
|
|
351
|
+
<div className="h-4 flex-1 rounded transition-all duration-500" style={{ backgroundColor: hslToCssString(generatedTheme.light.muted) }} />
|
|
352
|
+
<div className="h-4 w-12 rounded transition-all duration-500" style={{ backgroundColor: hslToCssString(generatedTheme.light.accent) }} />
|
|
353
|
+
</div>
|
|
354
|
+
</div>
|
|
237
355
|
</div>
|
|
238
356
|
<div>
|
|
239
|
-
<
|
|
240
|
-
<
|
|
357
|
+
<h4 className="text-[10px] font-bold uppercase tracking-widest opacity-40 mb-3">Dark Preview</h4>
|
|
358
|
+
<div
|
|
359
|
+
className="p-4 rounded-xl border transition-all duration-500"
|
|
360
|
+
style={{ backgroundColor: hslToCssString(generatedTheme.dark.background), color: hslToCssString(generatedTheme.dark.foreground), borderColor: hslToCssString(generatedTheme.dark.border) }}
|
|
361
|
+
>
|
|
362
|
+
<div className="flex items-center gap-2 mb-3">
|
|
363
|
+
<div className="h-3 w-3 rounded-full" style={{ backgroundColor: hslToCssString(generatedTheme.dark.primary) }} />
|
|
364
|
+
<span className="text-xs font-bold">Primary Button</span>
|
|
365
|
+
</div>
|
|
366
|
+
<div className="h-8 rounded-md mb-2 transition-all duration-500" style={{ backgroundColor: hslToCssString(generatedTheme.dark.card), borderColor: hslToCssString(generatedTheme.dark.border), borderWidth: 1 }} />
|
|
367
|
+
<div className="flex gap-2">
|
|
368
|
+
<div className="h-4 flex-1 rounded transition-all duration-500" style={{ backgroundColor: hslToCssString(generatedTheme.dark.muted) }} />
|
|
369
|
+
<div className="h-4 w-12 rounded transition-all duration-500" style={{ backgroundColor: hslToCssString(generatedTheme.dark.accent) }} />
|
|
370
|
+
</div>
|
|
371
|
+
</div>
|
|
241
372
|
</div>
|
|
242
373
|
</div>
|
|
243
374
|
</Card>
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
{/* INTEGRATION DOCS */}
|
|
247
|
-
<div className="pt-8 border-t border-[rgb(var(--app-border-color))]">
|
|
248
|
-
<h2 className="text-2xl font-bold mb-6 flex items-center gap-2 tracking-tighter">
|
|
249
|
-
<Icon name="terminal" /> Developer Handover
|
|
250
|
-
</h2>
|
|
251
|
-
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
|
|
252
|
-
<div className="space-y-3">
|
|
253
|
-
<h3 className="font-bold text-sm uppercase text-[rgb(var(--app-primary-color))] tracking-widest">Recharts Adapter</h3>
|
|
254
|
-
<div className="bg-black/90 p-4 rounded-lg border border-white/10 font-mono text-[11px] text-green-400">
|
|
255
|
-
{`<Line stroke="rgb(var(--app-chart-1))" />`}
|
|
256
|
-
</div>
|
|
257
|
-
</div>
|
|
258
|
-
<div className="space-y-3">
|
|
259
|
-
<h3 className="font-bold text-sm uppercase text-[rgb(var(--app-primary-color))] tracking-widest">Typography Engine</h3>
|
|
260
|
-
<div className="bg-black/90 p-4 rounded-lg border border-white/10 font-mono text-[11px] text-blue-400">
|
|
261
|
-
{`body { font-family: '${currentTheme?.fonts?.body || 'Inter'}'; }`}
|
|
262
|
-
</div>
|
|
263
|
-
</div>
|
|
264
|
-
</div>
|
|
265
|
-
</div>
|
|
375
|
+
)}
|
|
266
376
|
|
|
267
|
-
{/*
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
</
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
size="sm"
|
|
277
|
-
className="h-7 text-[10px] hover:bg-[rgb(var(--app-primary-color))] hover:text-[rgb(var(--app-primary-foreground-color))]"
|
|
278
|
-
onClick={() => {
|
|
279
|
-
const cssString = Object.entries(currentTheme?.cssVars || {})
|
|
280
|
-
.map(([key, val]) => `${key}: ${val};`)
|
|
281
|
-
.join('\n');
|
|
282
|
-
navigator.clipboard.writeText(cssString);
|
|
283
|
-
alert("Manifest copied!");
|
|
284
|
-
}}
|
|
285
|
-
>
|
|
286
|
-
Copy All
|
|
287
|
-
</Button>
|
|
377
|
+
{/* ── ROW 4: CSS MANIFEST ─────────────────────────────────────── */}
|
|
378
|
+
{activePalette && (
|
|
379
|
+
<Card className="p-0 overflow-hidden border-2 border-primary/10">
|
|
380
|
+
<div className="bg-muted/30 p-4 border-b border-border flex justify-between items-center">
|
|
381
|
+
<div className="flex items-center gap-2">
|
|
382
|
+
<Icon name="code" size={16} className="text-primary" />
|
|
383
|
+
<h3 className="font-bold text-xs uppercase tracking-widest">CSS Variables</h3>
|
|
384
|
+
</div>
|
|
385
|
+
<Button variant="ghost" size="sm" className="h-7 text-[10px]" onClick={handleCopyVars}>Copy All</Button>
|
|
288
386
|
</div>
|
|
289
|
-
<div className="p-4 bg-black/5 font-mono text-[11px] max-h-[
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
<p className="text-muted-foreground italic">No active DNA detected. Generate a vibe to see manifest.</p>
|
|
299
|
-
)}
|
|
300
|
-
</div>
|
|
387
|
+
<div className="p-4 bg-black/5 font-mono text-[11px] max-h-[260px] overflow-y-auto">
|
|
388
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-x-8 gap-y-1">
|
|
389
|
+
{Object.entries(paletteToRgbVars(activePalette)).map(([key, value]) => (
|
|
390
|
+
<div key={key} className="flex justify-between py-1 border-b border-black/5 last:border-0 group">
|
|
391
|
+
<span className="text-primary/70 group-hover:text-primary transition-colors">{key}</span>
|
|
392
|
+
<span className="font-bold opacity-80">{value}</span>
|
|
393
|
+
</div>
|
|
394
|
+
))}
|
|
395
|
+
</div>
|
|
301
396
|
</div>
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
{/* CSS for the custom scrollbar in this block */}
|
|
305
|
-
<style dangerouslySetInnerHTML={{ __html: `
|
|
306
|
-
.custom-scrollbar::-webkit-scrollbar { width: 4px; }
|
|
307
|
-
.custom-scrollbar::-webkit-scrollbar-track { background: transparent; }
|
|
308
|
-
.custom-scrollbar::-webkit-scrollbar-thumb { background: rgb(var(--app-primary-color), 0.2); border-radius: 10px; }
|
|
309
|
-
.custom-scrollbar::-webkit-scrollbar-thumb:hover { background: rgb(var(--app-primary-color), 0.5); }
|
|
310
|
-
`}} />
|
|
397
|
+
</Card>
|
|
398
|
+
)}
|
|
311
399
|
|
|
312
400
|
</div>
|
|
313
401
|
</StandardPageLayout>
|