@ramme-io/create-app 2.0.0 → 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.
Files changed (32) hide show
  1. package/README.md +2 -2
  2. package/index.js +1 -1
  3. package/package.json +1 -1
  4. package/template/index.html +1 -1
  5. package/template/package.json +14 -5
  6. package/template/public/_redirects +1 -0
  7. package/template/src/App.tsx +3 -2
  8. package/template/src/components/AppHeader.tsx +5 -0
  9. package/template/src/components/ScrollToTop.tsx +6 -6
  10. package/template/src/features/ai/pages/AiChat.tsx +136 -37
  11. package/template/src/features/auth/AuthContext.tsx +16 -6
  12. package/template/src/features/config/AppConfigContext.tsx +2 -2
  13. package/template/src/features/docs/pages/EdgeTelemetryDemo.tsx +149 -0
  14. package/template/src/features/onboarding/pages/AboutRamme.tsx +12 -12
  15. package/template/src/features/onboarding/pages/PrototypeGallery.tsx +8 -6
  16. package/template/src/features/onboarding/pages/RammeFeatures.tsx +18 -17
  17. package/template/src/features/onboarding/pages/RammeTutorial.tsx +20 -11
  18. package/template/src/features/onboarding/pages/Welcome.tsx +6 -6
  19. package/template/src/features/styleguide/sections/tables/TablesSection.tsx +25 -5
  20. package/template/src/features/theme/pages/ThemeCustomizerPage.tsx +344 -256
  21. package/template/src/features/theme/utils/ThemeGenerator.logic.ts +587 -0
  22. package/template/src/hooks/__tests__/useStudioHotkeys.test.ts +100 -0
  23. package/template/src/hooks/useStudioHotkeys.ts +36 -0
  24. package/template/src/index.css +91 -1
  25. package/template/src/main.tsx +44 -2
  26. package/template/src/templates/dashboard/DashboardLayout.tsx +6 -1
  27. package/template/src/templates/dashboard/dashboard.sitemap.ts +1 -1
  28. package/template/src/templates/docs/docs.sitemap.ts +8 -0
  29. package/template/src/templates/settings/SettingsLayout.tsx +13 -26
  30. package/template/src/test/setup.ts +1 -0
  31. package/template/tsconfig.app.json +1 -0
  32. 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, AITheme } from '../services/themeAgent';
5
- import { generateRandomTheme, applyThemeToDom, loadGoogleFont } from '../utils/themeUtils';
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 [currentTheme, setCurrentTheme] = useState<AITheme | null>(() => {
11
- const saved = localStorage.getItem('ramme_persisted_theme');
12
- return saved ? JSON.parse(saved) : null;
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
- if (currentTheme) {
26
- localStorage.setItem('ramme_persisted_theme', JSON.stringify(currentTheme));
27
- if (currentTheme.fonts) {
28
- loadGoogleFont(currentTheme.fonts.heading, currentTheme.fonts.body);
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
- applyThemeToDom(currentTheme.cssVars);
31
- }
32
- }, [currentTheme]);
113
+ } catch { /* ignore */ }
114
+ }, [applyGeneratedTheme]);
33
115
 
34
- const handleSaveKey = () => {
35
- localStorage.setItem('ramme_vault_gemini_key', geminiKey);
36
- setShowKeyInput(false);
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 handleReset = () => {
40
- if (window.confirm("Reset to default theme and clear saved settings?")) {
41
- localStorage.removeItem('ramme_persisted_theme');
42
- window.location.reload();
43
- }
126
+ const handleProfileGenerate = (profileId: string) => {
127
+ const theme = generateFromProfile(profileId);
128
+ setGeneratedTheme(theme);
129
+ applyGeneratedTheme(theme, isDarkMode);
44
130
  };
45
131
 
46
- const handleShare = () => {
47
- if (!currentTheme) return;
48
- const themeString = JSON.stringify(currentTheme, null, 2);
49
- navigator.clipboard.writeText(themeString);
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 handleRandomize = () => {
54
- const vars = generateRandomTheme();
55
- const randomTheme: AITheme = {
56
- name: 'Random Generated',
57
- description: 'Procedural RGB palette.',
58
- cssVars: vars
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 handleDownloadCSS = () => {
65
- if (!currentTheme) return;
66
-
67
- // 1. Build the Google Fonts import string
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
- const handleAiGenerate = async (e: React.FormEvent) => {
97
- e.preventDefault();
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
- const theme = await generateThemeFromPrompt(prompt);
104
- if (theme && typeof theme === 'object' && theme.cssVars) {
105
- document.documentElement.classList.remove('midnight', 'corporate', 'blueprint', 'dark');
106
- if (theme.fonts?.heading && theme.fonts?.body) {
107
- loadGoogleFont(theme.fonts.heading, theme.fonts.body);
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
- console.error("Theme Engine Error:", err);
114
- alert(err.message || "Snag in the orchestration.");
169
+ alert(err.message || 'Deep AI generation failed.');
115
170
  } finally {
116
- setLoading(false);
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 Engine" description="AI-powered aesthetic generator.">
122
- <div className="max-w-5xl mx-auto space-y-8 pb-20">
123
-
124
- {/* VAULT SETTINGS */}
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
- <Card className="p-4 border-[rgb(var(--app-primary-color))]/50 bg-[rgb(var(--app-primary-color))]/5 animate-in fade-in slide-in-from-top-2">
127
- <div className="flex flex-col md:flex-row items-center gap-4">
128
- <div className="flex-1 w-full">
129
- <Label className="text-[10px] font-bold uppercase mb-2 block text-[rgb(var(--app-primary-color))]">
130
- Secure Vault: Gemini API Key
131
- </Label>
132
- <Input
133
- type="password"
134
- placeholder="Paste Gemini API Key..."
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
- {/* CONTROLS */}
148
- <div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
149
- <Card className="p-6 lg:col-span-2 border-2 border-[rgb(var(--app-primary-color))]/20 bg-gradient-to-br from-[rgb(var(--app-bg-color))] to-[rgb(var(--app-muted-bg))]/30">
150
- <div className="flex items-center justify-between mb-4">
151
- <h2 className="font-bold text-[rgb(var(--app-text-color))] flex items-center gap-2 uppercase tracking-widest text-sm">
152
- <Icon name="bot" size={18} className="text-[rgb(var(--app-primary-color))]" />
153
- Vibe Coding
154
- </h2>
155
- <div className="flex gap-4">
156
- <button onClick={handleReset} className="text-[10px] uppercase font-bold text-red-500 opacity-60 hover:opacity-100">Reset</button>
157
- {!showKeyInput && (
158
- <button onClick={() => setShowKeyInput(true)} className="text-[10px] uppercase font-bold text-[rgb(var(--app-primary-color))] opacity-60 hover:opacity-100">Update Key</button>
159
- )}
160
- </div>
161
- </div>
162
- <form onSubmit={handleAiGenerate} className="flex gap-3">
163
- <Input
164
- placeholder='e.g., "Cyberpunk Neon", "Soft Matcha", "Deep Space"'
165
- value={prompt}
166
- onChange={(e) => setPrompt(e.target.value)}
167
- className="flex-1"
168
- />
169
- <Button type="submit" disabled={loading || !geminiKey} className="w-28 bg-[rgb(var(--app-primary-color))] text-[rgb(var(--app-primary-foreground-color))]">
170
- {loading ? <Icon name="loader" className="animate-spin"/> : 'Generate'}
171
- </Button>
172
- </form>
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
- {/* VISUALIZER & THEME INFO */}
183
- <div className="grid grid-cols-1 md:grid-cols-3 gap-6">
184
- <Card className="p-6 md:col-span-2">
185
- <div className="flex justify-between items-end mb-6">
186
- <h3 className="font-bold flex items-center gap-2 text-xs uppercase opacity-60 tracking-tighter">
187
- <Icon name="palette" size={14}/> Active Palette DNA
188
- </h3>
189
- <div className="flex gap-2">
190
- <Button
191
- variant="ghost"
192
- size="sm"
193
- onClick={handleShare}
194
- className="h-8 text-[10px] uppercase border border-[rgb(var(--app-border-color))]"
195
- >
196
- <Icon name="copy" size={12} className="mr-2" /> Share JSON
197
- </Button>
198
- <Button
199
- variant="ghost"
200
- size="sm"
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
- </Card>
314
+ </div>
225
315
 
226
- <Card className="p-6 border-[rgb(var(--app-primary-color))]/30">
227
- <h3 className="text-[10px] font-bold uppercase opacity-60 mb-4 tracking-widest">Model Analysis</h3>
228
- <div className="space-y-4">
229
- <div>
230
- <Label className="text-[10px] opacity-40">Theme Name</Label>
231
- <p className="text-sm font-bold text-[rgb(var(--app-primary-color))]">{currentTheme?.name || 'Standard'}</p>
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
- <Label className="text-[10px] opacity-40">Typography Pair</Label>
235
- <p className="text-xs font-medium">H: {currentTheme?.fonts?.heading || 'Default'}</p>
236
- <p className="text-xs font-medium">B: {currentTheme?.fonts?.body || 'Default'}</p>
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
- <Label className="text-[10px] opacity-40">Aesthetic Meta</Label>
240
- <p className="text-[11px] leading-relaxed italic opacity-80">{currentTheme?.description || 'The default system appearance.'}</p>
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
- </div>
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
- {/* RAW VARIABLES MANIFEST */}
268
- <Card className="p-0 overflow-hidden border-2 border-[rgb(var(--app-primary-color))]/10">
269
- <div className="bg-[rgb(var(--app-muted-bg))]/50 p-4 border-b border-[rgb(var(--app-border-color))] flex justify-between items-center">
270
- <div className="flex items-center gap-2">
271
- <Icon name="code" size={16} className="text-[rgb(var(--app-primary-color))]" />
272
- <h3 className="font-bold text-xs uppercase tracking-widest">CSS Variables Manifest</h3>
273
- </div>
274
- <Button
275
- variant="ghost"
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-[300px] overflow-y-auto custom-scrollbar">
290
- <div className="grid grid-cols-1 md:grid-cols-2 gap-x-8 gap-y-1">
291
- {currentTheme && Object.entries(currentTheme.cssVars).map(([key, value]) => (
292
- <div key={key} className="flex justify-between py-1 border-b border-black/5 last:border-0 group">
293
- <span className="text-[rgb(var(--app-primary-color))] opacity-70 group-hover:opacity-100">{key}</span>
294
- <span className="font-bold opacity-80">{value}</span>
295
- </div>
296
- ))}
297
- {!currentTheme && (
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
- </Card>
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>