From d92cf3de3064ad016c9fcf532d7451eb363c6873 Mon Sep 17 00:00:00 2001 From: johndoe6345789 Date: Fri, 16 Jan 2026 00:53:06 +0000 Subject: [PATCH] Generated by Spark: Theme editor allows custom colours (not part of standard spec) and multiple themes like dark and light mode. --- PRD.md | 8 +- src/App.tsx | 42 ++- src/components/StyleDesigner.tsx | 511 ++++++++++++++++++++++++------- src/lib/generators.ts | 75 ++++- src/types/project.ts | 19 +- 5 files changed, 518 insertions(+), 137 deletions(-) diff --git a/PRD.md b/PRD.md index b98dcee..76ca03f 100644 --- a/PRD.md +++ b/PRD.md @@ -41,11 +41,11 @@ This is a full-featured low-code IDE with multiple integrated tools (code editor - **Success criteria**: Can add/remove/reorder components; AI-generated components are well-structured; props are editable; generates valid React code ### Style Designer -- **Functionality**: Visual interface for Material UI theming and component styling with AI theme generation from descriptions -- **Purpose**: Configure colors, typography, spacing without manual theme object creation, with AI design assistance +- **Functionality**: Visual interface for Material UI theming with support for multiple theme variants (light/dark/custom), custom color management, and AI theme generation from descriptions +- **Purpose**: Configure colors, typography, spacing with support for unlimited theme variants and custom color palettes beyond standard specifications, with AI design assistance - **Trigger**: Opening the Styling tab -- **Progression**: Select theme property → Adjust values with controls or describe style to AI → Preview updates live → Export theme configuration -- **Success criteria**: Color pickers work; typography scales properly; AI themes match descriptions and have good contrast; generates valid MUI theme code +- **Progression**: Select or create theme variant → Adjust standard colors or add custom colors → Configure typography and spacing → Preview updates live across all variants → Switch between variants → Export theme configuration with all variants +- **Success criteria**: Color pickers work; custom colors can be added/removed; multiple theme variants persist; AI themes match descriptions and have good contrast; generates valid MUI theme code for all variants including light and dark modes ### Project Generator - **Functionality**: Exports complete Next.js project with all configurations diff --git a/src/App.tsx b/src/App.tsx index 107d106..1427f45 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -25,11 +25,43 @@ import { ScrollArea } from '@/components/ui/scroll-area' import { Textarea } from '@/components/ui/textarea' const DEFAULT_THEME: ThemeConfig = { - primaryColor: '#1976d2', - secondaryColor: '#dc004e', - errorColor: '#f44336', - warningColor: '#ff9800', - successColor: '#4caf50', + variants: [ + { + id: 'light', + name: 'Light', + colors: { + primaryColor: '#1976d2', + secondaryColor: '#dc004e', + errorColor: '#f44336', + warningColor: '#ff9800', + successColor: '#4caf50', + background: '#ffffff', + surface: '#f5f5f5', + text: '#000000', + textSecondary: '#666666', + border: '#e0e0e0', + customColors: {}, + }, + }, + { + id: 'dark', + name: 'Dark', + colors: { + primaryColor: '#90caf9', + secondaryColor: '#f48fb1', + errorColor: '#f44336', + warningColor: '#ffa726', + successColor: '#66bb6a', + background: '#121212', + surface: '#1e1e1e', + text: '#ffffff', + textSecondary: '#b0b0b0', + border: '#333333', + customColors: {}, + }, + }, + ], + activeVariantId: 'light', fontFamily: 'Roboto, Arial, sans-serif', fontSize: { small: 12, medium: 14, large: 20 }, spacing: 8, diff --git a/src/components/StyleDesigner.tsx b/src/components/StyleDesigner.tsx index af89548..69fb122 100644 --- a/src/components/StyleDesigner.tsx +++ b/src/components/StyleDesigner.tsx @@ -1,21 +1,136 @@ -import { ThemeConfig } from '@/types/project' +import { useState } from 'react' +import { ThemeConfig, ThemeVariant } from '@/types/project' import { Card } from '@/components/ui/card' import { Label } from '@/components/ui/label' import { Input } from '@/components/ui/input' import { Slider } from '@/components/ui/slider' import { Button } from '@/components/ui/button' -import { PaintBrush, Sparkle } from '@phosphor-icons/react' +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' +import { PaintBrush, Sparkle, Plus, Trash, Moon, Sun, Palette } from '@phosphor-icons/react' import { AIService } from '@/lib/ai-service' import { toast } from 'sonner' +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogFooter, +} from '@/components/ui/dialog' interface StyleDesignerProps { theme: ThemeConfig - onThemeChange: (theme: ThemeConfig) => void + onThemeChange: (theme: ThemeConfig | ((current: ThemeConfig) => ThemeConfig)) => void } export function StyleDesigner({ theme, onThemeChange }: StyleDesignerProps) { + const [newColorName, setNewColorName] = useState('') + const [newColorValue, setNewColorValue] = useState('#000000') + const [customColorDialogOpen, setCustomColorDialogOpen] = useState(false) + const [newVariantDialogOpen, setNewVariantDialogOpen] = useState(false) + const [newVariantName, setNewVariantName] = useState('') + + const activeVariant = theme.variants.find((v) => v.id === theme.activeVariantId) || theme.variants[0] + const updateTheme = (updates: Partial) => { - onThemeChange({ ...theme, ...updates }) + onThemeChange((current) => ({ ...current, ...updates })) + } + + const updateActiveVariantColors = (colorUpdates: Partial) => { + onThemeChange((current) => ({ + ...current, + variants: current.variants.map((v) => + v.id === current.activeVariantId + ? { ...v, colors: { ...v.colors, ...colorUpdates } } + : v + ), + })) + } + + const addCustomColor = () => { + if (!newColorName.trim()) { + toast.error('Please enter a color name') + return + } + + updateActiveVariantColors({ + customColors: { + ...activeVariant.colors.customColors, + [newColorName]: newColorValue, + }, + }) + + setNewColorName('') + setNewColorValue('#000000') + setCustomColorDialogOpen(false) + toast.success(`Added custom color: ${newColorName}`) + } + + const removeCustomColor = (colorName: string) => { + const { [colorName]: _, ...remainingColors } = activeVariant.colors.customColors + updateActiveVariantColors({ + customColors: remainingColors, + }) + toast.success(`Removed custom color: ${colorName}`) + } + + const addVariant = () => { + if (!newVariantName.trim()) { + toast.error('Please enter a variant name') + return + } + + const newVariant: ThemeVariant = { + id: `variant-${Date.now()}`, + name: newVariantName, + colors: { ...activeVariant.colors, customColors: {} }, + } + + onThemeChange((current) => ({ + ...current, + variants: [...current.variants, newVariant], + activeVariantId: newVariant.id, + })) + + setNewVariantName('') + setNewVariantDialogOpen(false) + toast.success(`Added theme variant: ${newVariantName}`) + } + + const deleteVariant = (variantId: string) => { + if (theme.variants.length <= 1) { + toast.error('Cannot delete the last theme variant') + return + } + + onThemeChange((current) => { + const remainingVariants = current.variants.filter((v) => v.id !== variantId) + return { + ...current, + variants: remainingVariants, + activeVariantId: current.activeVariantId === variantId ? remainingVariants[0].id : current.activeVariantId, + } + }) + + toast.success('Theme variant deleted') + } + + const duplicateVariant = (variantId: string) => { + const variantToDuplicate = theme.variants.find((v) => v.id === variantId) + if (!variantToDuplicate) return + + const newVariant: ThemeVariant = { + id: `variant-${Date.now()}`, + name: `${variantToDuplicate.name} Copy`, + colors: { ...variantToDuplicate.colors, customColors: { ...variantToDuplicate.colors.customColors } }, + } + + onThemeChange((current) => ({ + ...current, + variants: [...current.variants, newVariant], + })) + + toast.success('Theme variant duplicated') } const generateThemeWithAI = async () => { @@ -25,9 +140,9 @@ export function StyleDesigner({ theme, onThemeChange }: StyleDesignerProps) { try { toast.info('Generating theme with AI...') const generatedTheme = await AIService.generateThemeFromDescription(description) - + if (generatedTheme) { - onThemeChange({ ...theme, ...generatedTheme }) + onThemeChange((current) => ({ ...current, ...generatedTheme })) toast.success('Theme generated successfully!') } else { toast.error('AI generation failed. Please try again.') @@ -38,14 +153,39 @@ export function StyleDesigner({ theme, onThemeChange }: StyleDesignerProps) { } } + const renderColorInput = (label: string, colorKey: keyof typeof activeVariant.colors, excludeCustom = true) => { + if (excludeCustom && colorKey === 'customColors') return null + + const value = activeVariant.colors[colorKey] as string + + return ( +
+ +
+ updateActiveVariantColors({ [colorKey]: e.target.value })} + className="w-20 h-10 cursor-pointer" + /> + updateActiveVariantColors({ [colorKey]: e.target.value })} + className="flex-1 font-mono" + /> +
+
+ ) + } + return (
-
+
-

Material UI Theme Designer

+

Theme Designer

- Customize your application's visual theme + Create and customize multiple theme variants with custom colors

-

- - Color Palette -

-
-
- -
- updateTheme({ primaryColor: e.target.value })} - className="w-20 h-10 cursor-pointer" - /> - updateTheme({ primaryColor: e.target.value })} - className="flex-1" - /> -
-
- -
- -
- - updateTheme({ secondaryColor: e.target.value }) - } - className="w-20 h-10 cursor-pointer" - /> - - updateTheme({ secondaryColor: e.target.value }) - } - className="flex-1" - /> -
-
- -
- -
- updateTheme({ errorColor: e.target.value })} - className="w-20 h-10 cursor-pointer" - /> - updateTheme({ errorColor: e.target.value })} - className="flex-1" - /> -
-
- -
- -
- updateTheme({ warningColor: e.target.value })} - className="w-20 h-10 cursor-pointer" - /> - updateTheme({ warningColor: e.target.value })} - className="flex-1" - /> -
-
- -
- -
- updateTheme({ successColor: e.target.value })} - className="w-20 h-10 cursor-pointer" - /> - updateTheme({ successColor: e.target.value })} - className="flex-1" - /> -
-
+
+

+ + Theme Variants +

+
+ +
+ {theme.variants.map((variant) => ( +
+ +
+ + +
+
+ ))} +
+ + + + Standard Colors + Extended Colors + Custom Colors ({Object.keys(activeVariant.colors.customColors).length}) + + + +
+ {renderColorInput('Primary Color', 'primaryColor')} + {renderColorInput('Secondary Color', 'secondaryColor')} + {renderColorInput('Error Color', 'errorColor')} + {renderColorInput('Warning Color', 'warningColor')} + {renderColorInput('Success Color', 'successColor')} +
+
+ + +
+ {renderColorInput('Background', 'background')} + {renderColorInput('Surface', 'surface')} + {renderColorInput('Text', 'text')} + {renderColorInput('Text Secondary', 'textSecondary')} + {renderColorInput('Border', 'border')} +
+
+ + +
+

+ Add custom colors for your specific needs +

+ +
+ + {Object.keys(activeVariant.colors.customColors).length === 0 ? ( +
+ +

No custom colors yet

+ +
+ ) : ( +
+ {Object.entries(activeVariant.colors.customColors).map(([name, value]) => ( +
+
+ + +
+
+ + updateActiveVariantColors({ + customColors: { + ...activeVariant.colors.customColors, + [name]: e.target.value, + }, + }) + } + className="w-20 h-10 cursor-pointer" + /> + + updateActiveVariantColors({ + customColors: { + ...activeVariant.colors.customColors, + [name]: e.target.value, + }, + }) + } + className="flex-1 font-mono" + /> +
+
+ ))} +
+ )} +
+
@@ -269,76 +455,163 @@ export function StyleDesigner({ theme, onThemeChange }: StyleDesignerProps) {
- -

Preview

+ +

+ Preview - {activeVariant.name} Mode +

Primary
Secondary
Error
Warning
Success
+ {Object.entries(activeVariant.colors.customColors).map(([name, color]) => ( +
+ {name} +
+ ))}
-

+

Large Text Sample

-

+

Medium Text Sample

-

- Small Text Sample +

+ Small Text Sample (Secondary)

+ + + + + Add Custom Color + + Create a custom color for your theme variant + + +
+
+ + setNewColorName(e.target.value)} + placeholder="e.g., accent, highlight, brand" + /> +
+
+ +
+ setNewColorValue(e.target.value)} + className="w-20 h-10 cursor-pointer" + /> + setNewColorValue(e.target.value)} + className="flex-1 font-mono" + /> +
+
+
+ + + + +
+
+ + + + + Add Theme Variant + + Create a new theme variant based on the current one + + +
+
+ + setNewVariantName(e.target.value)} + placeholder="e.g., High Contrast, Colorblind Friendly" + /> +
+
+ + + + +
+
) } diff --git a/src/lib/generators.ts b/src/lib/generators.ts index 143c153..3e57821 100644 --- a/src/lib/generators.ts +++ b/src/lib/generators.ts @@ -46,24 +46,36 @@ export function generateComponentCode(node: ComponentNode, indent: number = 0): } export function generateMUITheme(theme: ThemeConfig): string { - return `import { createTheme } from '@mui/material/styles'; + const lightVariant = theme.variants.find((v) => v.id === 'light') || theme.variants[0] + const darkVariant = theme.variants.find((v) => v.id === 'dark') -export const theme = createTheme({ + let themeCode = `import { createTheme } from '@mui/material/styles'; + +export const lightTheme = createTheme({ palette: { + mode: 'light', primary: { - main: '${theme.primaryColor}', + main: '${lightVariant.colors.primaryColor}', }, secondary: { - main: '${theme.secondaryColor}', + main: '${lightVariant.colors.secondaryColor}', }, error: { - main: '${theme.errorColor}', + main: '${lightVariant.colors.errorColor}', }, warning: { - main: '${theme.warningColor}', + main: '${lightVariant.colors.warningColor}', }, success: { - main: '${theme.successColor}', + main: '${lightVariant.colors.successColor}', + }, + background: { + default: '${lightVariant.colors.background}', + paper: '${lightVariant.colors.surface}', + }, + text: { + primary: '${lightVariant.colors.text}', + secondary: '${lightVariant.colors.textSecondary}', }, }, typography: { @@ -74,7 +86,54 @@ export const theme = createTheme({ shape: { borderRadius: ${theme.borderRadius}, }, -});` +}); +` + + if (darkVariant) { + themeCode += ` +export const darkTheme = createTheme({ + palette: { + mode: 'dark', + primary: { + main: '${darkVariant.colors.primaryColor}', + }, + secondary: { + main: '${darkVariant.colors.secondaryColor}', + }, + error: { + main: '${darkVariant.colors.errorColor}', + }, + warning: { + main: '${darkVariant.colors.warningColor}', + }, + success: { + main: '${darkVariant.colors.successColor}', + }, + background: { + default: '${darkVariant.colors.background}', + paper: '${darkVariant.colors.surface}', + }, + text: { + primary: '${darkVariant.colors.text}', + secondary: '${darkVariant.colors.textSecondary}', + }, + }, + typography: { + fontFamily: '${theme.fontFamily}', + fontSize: ${theme.fontSize.medium}, + }, + spacing: ${theme.spacing}, + shape: { + borderRadius: ${theme.borderRadius}, + }, +}); + +export const theme = lightTheme;` + } else { + themeCode += `\nexport const theme = lightTheme;` + } + + return themeCode } export function generateNextJSProject( diff --git a/src/types/project.ts b/src/types/project.ts index ffb387d..7328e19 100644 --- a/src/types/project.ts +++ b/src/types/project.ts @@ -31,12 +31,29 @@ export interface ComponentNode { name: string } -export interface ThemeConfig { +export interface ColorPalette { primaryColor: string secondaryColor: string errorColor: string warningColor: string successColor: string + background: string + surface: string + text: string + textSecondary: string + border: string + customColors: Record +} + +export interface ThemeVariant { + id: string + name: string + colors: ColorPalette +} + +export interface ThemeConfig { + variants: ThemeVariant[] + activeVariantId: string fontFamily: string fontSize: { small: number