mirror of
https://github.com/johndoe6345789/low-code-react-app-b.git
synced 2026-04-24 13:44:54 +00:00
Generated by Spark: Theme editor allows custom colours (not part of standard spec) and multiple themes like dark and light mode.
This commit is contained in:
8
PRD.md
8
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
|
||||
|
||||
42
src/App.tsx
42
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,
|
||||
|
||||
@@ -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<ThemeConfig>) => {
|
||||
onThemeChange({ ...theme, ...updates })
|
||||
onThemeChange((current) => ({ ...current, ...updates }))
|
||||
}
|
||||
|
||||
const updateActiveVariantColors = (colorUpdates: Partial<typeof activeVariant.colors>) => {
|
||||
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 (
|
||||
<div className="space-y-2">
|
||||
<Label>{label}</Label>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
type="color"
|
||||
value={value}
|
||||
onChange={(e) => updateActiveVariantColors({ [colorKey]: e.target.value })}
|
||||
className="w-20 h-10 cursor-pointer"
|
||||
/>
|
||||
<Input
|
||||
value={value}
|
||||
onChange={(e) => updateActiveVariantColors({ [colorKey]: e.target.value })}
|
||||
className="flex-1 font-mono"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h-full overflow-auto p-6">
|
||||
<div className="max-w-4xl mx-auto space-y-6">
|
||||
<div className="max-w-5xl mx-auto space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold mb-2">Material UI Theme Designer</h2>
|
||||
<h2 className="text-2xl font-bold mb-2">Theme Designer</h2>
|
||||
<p className="text-muted-foreground">
|
||||
Customize your application's visual theme
|
||||
Create and customize multiple theme variants with custom colors
|
||||
</p>
|
||||
</div>
|
||||
<Button onClick={generateThemeWithAI} variant="outline">
|
||||
@@ -55,100 +195,146 @@ export function StyleDesigner({ theme, onThemeChange }: StyleDesignerProps) {
|
||||
</div>
|
||||
|
||||
<Card className="p-6">
|
||||
<h3 className="text-lg font-semibold mb-4 flex items-center gap-2">
|
||||
<PaintBrush size={20} weight="duotone" />
|
||||
Color Palette
|
||||
</h3>
|
||||
<div className="grid grid-cols-2 gap-6">
|
||||
<div className="space-y-2">
|
||||
<Label>Primary Color</Label>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
type="color"
|
||||
value={theme.primaryColor}
|
||||
onChange={(e) => updateTheme({ primaryColor: e.target.value })}
|
||||
className="w-20 h-10 cursor-pointer"
|
||||
/>
|
||||
<Input
|
||||
value={theme.primaryColor}
|
||||
onChange={(e) => updateTheme({ primaryColor: e.target.value })}
|
||||
className="flex-1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>Secondary Color</Label>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
type="color"
|
||||
value={theme.secondaryColor}
|
||||
onChange={(e) =>
|
||||
updateTheme({ secondaryColor: e.target.value })
|
||||
}
|
||||
className="w-20 h-10 cursor-pointer"
|
||||
/>
|
||||
<Input
|
||||
value={theme.secondaryColor}
|
||||
onChange={(e) =>
|
||||
updateTheme({ secondaryColor: e.target.value })
|
||||
}
|
||||
className="flex-1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>Error Color</Label>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
type="color"
|
||||
value={theme.errorColor}
|
||||
onChange={(e) => updateTheme({ errorColor: e.target.value })}
|
||||
className="w-20 h-10 cursor-pointer"
|
||||
/>
|
||||
<Input
|
||||
value={theme.errorColor}
|
||||
onChange={(e) => updateTheme({ errorColor: e.target.value })}
|
||||
className="flex-1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>Warning Color</Label>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
type="color"
|
||||
value={theme.warningColor}
|
||||
onChange={(e) => updateTheme({ warningColor: e.target.value })}
|
||||
className="w-20 h-10 cursor-pointer"
|
||||
/>
|
||||
<Input
|
||||
value={theme.warningColor}
|
||||
onChange={(e) => updateTheme({ warningColor: e.target.value })}
|
||||
className="flex-1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>Success Color</Label>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
type="color"
|
||||
value={theme.successColor}
|
||||
onChange={(e) => updateTheme({ successColor: e.target.value })}
|
||||
className="w-20 h-10 cursor-pointer"
|
||||
/>
|
||||
<Input
|
||||
value={theme.successColor}
|
||||
onChange={(e) => updateTheme({ successColor: e.target.value })}
|
||||
className="flex-1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-lg font-semibold flex items-center gap-2">
|
||||
<Palette size={20} weight="duotone" />
|
||||
Theme Variants
|
||||
</h3>
|
||||
<Button onClick={() => setNewVariantDialogOpen(true)} size="sm">
|
||||
<Plus size={16} className="mr-2" />
|
||||
Add Variant
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-2 mb-6">
|
||||
{theme.variants.map((variant) => (
|
||||
<div key={variant.id} className="flex items-center gap-2 group">
|
||||
<Button
|
||||
variant={theme.activeVariantId === variant.id ? 'default' : 'outline'}
|
||||
onClick={() => updateTheme({ activeVariantId: variant.id })}
|
||||
className="gap-2"
|
||||
>
|
||||
{variant.name === 'Light' && <Sun size={16} weight="duotone" />}
|
||||
{variant.name === 'Dark' && <Moon size={16} weight="duotone" />}
|
||||
{variant.name}
|
||||
</Button>
|
||||
<div className="opacity-0 group-hover:opacity-100 transition-opacity flex gap-1">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => duplicateVariant(variant.id)}
|
||||
title="Duplicate"
|
||||
>
|
||||
<Plus size={14} />
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => deleteVariant(variant.id)}
|
||||
disabled={theme.variants.length <= 1}
|
||||
title="Delete"
|
||||
>
|
||||
<Trash size={14} />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<Tabs defaultValue="standard" className="w-full">
|
||||
<TabsList>
|
||||
<TabsTrigger value="standard">Standard Colors</TabsTrigger>
|
||||
<TabsTrigger value="extended">Extended Colors</TabsTrigger>
|
||||
<TabsTrigger value="custom">Custom Colors ({Object.keys(activeVariant.colors.customColors).length})</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="standard" className="space-y-6 mt-6">
|
||||
<div className="grid grid-cols-2 gap-6">
|
||||
{renderColorInput('Primary Color', 'primaryColor')}
|
||||
{renderColorInput('Secondary Color', 'secondaryColor')}
|
||||
{renderColorInput('Error Color', 'errorColor')}
|
||||
{renderColorInput('Warning Color', 'warningColor')}
|
||||
{renderColorInput('Success Color', 'successColor')}
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="extended" className="space-y-6 mt-6">
|
||||
<div className="grid grid-cols-2 gap-6">
|
||||
{renderColorInput('Background', 'background')}
|
||||
{renderColorInput('Surface', 'surface')}
|
||||
{renderColorInput('Text', 'text')}
|
||||
{renderColorInput('Text Secondary', 'textSecondary')}
|
||||
{renderColorInput('Border', 'border')}
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="custom" className="space-y-6 mt-6">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Add custom colors for your specific needs
|
||||
</p>
|
||||
<Button onClick={() => setCustomColorDialogOpen(true)} size="sm">
|
||||
<Plus size={16} className="mr-2" />
|
||||
Add Custom Color
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{Object.keys(activeVariant.colors.customColors).length === 0 ? (
|
||||
<div className="text-center py-12 border-2 border-dashed border-border rounded-lg">
|
||||
<Palette size={48} className="mx-auto mb-4 text-muted-foreground" weight="duotone" />
|
||||
<p className="text-muted-foreground mb-4">No custom colors yet</p>
|
||||
<Button onClick={() => setCustomColorDialogOpen(true)} variant="outline">
|
||||
<Plus size={16} className="mr-2" />
|
||||
Add Your First Custom Color
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-2 gap-6">
|
||||
{Object.entries(activeVariant.colors.customColors).map(([name, value]) => (
|
||||
<div key={name} className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="capitalize">{name}</Label>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => removeCustomColor(name)}
|
||||
>
|
||||
<Trash size={14} />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
type="color"
|
||||
value={value}
|
||||
onChange={(e) =>
|
||||
updateActiveVariantColors({
|
||||
customColors: {
|
||||
...activeVariant.colors.customColors,
|
||||
[name]: e.target.value,
|
||||
},
|
||||
})
|
||||
}
|
||||
className="w-20 h-10 cursor-pointer"
|
||||
/>
|
||||
<Input
|
||||
value={value}
|
||||
onChange={(e) =>
|
||||
updateActiveVariantColors({
|
||||
customColors: {
|
||||
...activeVariant.colors.customColors,
|
||||
[name]: e.target.value,
|
||||
},
|
||||
})
|
||||
}
|
||||
className="flex-1 font-mono"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</Card>
|
||||
|
||||
<Card className="p-6">
|
||||
@@ -269,76 +455,163 @@ export function StyleDesigner({ theme, onThemeChange }: StyleDesignerProps) {
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-6 bg-gradient-to-br from-card to-muted">
|
||||
<h3 className="text-lg font-semibold mb-4">Preview</h3>
|
||||
<Card className="p-6" style={{ backgroundColor: activeVariant.colors.background }}>
|
||||
<h3 className="text-lg font-semibold mb-4" style={{ color: activeVariant.colors.text }}>
|
||||
Preview - {activeVariant.name} Mode
|
||||
</h3>
|
||||
<div className="space-y-4">
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
<div
|
||||
className="w-20 h-20 rounded flex items-center justify-center text-white font-semibold"
|
||||
className="w-24 h-24 rounded flex flex-col items-center justify-center text-white font-semibold text-sm"
|
||||
style={{
|
||||
backgroundColor: theme.primaryColor,
|
||||
backgroundColor: activeVariant.colors.primaryColor,
|
||||
borderRadius: `${theme.borderRadius}px`,
|
||||
}}
|
||||
>
|
||||
Primary
|
||||
</div>
|
||||
<div
|
||||
className="w-20 h-20 rounded flex items-center justify-center text-white font-semibold"
|
||||
className="w-24 h-24 rounded flex flex-col items-center justify-center text-white font-semibold text-sm"
|
||||
style={{
|
||||
backgroundColor: theme.secondaryColor,
|
||||
backgroundColor: activeVariant.colors.secondaryColor,
|
||||
borderRadius: `${theme.borderRadius}px`,
|
||||
}}
|
||||
>
|
||||
Secondary
|
||||
</div>
|
||||
<div
|
||||
className="w-20 h-20 rounded flex items-center justify-center text-white font-semibold"
|
||||
className="w-24 h-24 rounded flex flex-col items-center justify-center text-white font-semibold text-sm"
|
||||
style={{
|
||||
backgroundColor: theme.errorColor,
|
||||
backgroundColor: activeVariant.colors.errorColor,
|
||||
borderRadius: `${theme.borderRadius}px`,
|
||||
}}
|
||||
>
|
||||
Error
|
||||
</div>
|
||||
<div
|
||||
className="w-20 h-20 rounded flex items-center justify-center text-white font-semibold"
|
||||
className="w-24 h-24 rounded flex flex-col items-center justify-center text-white font-semibold text-sm"
|
||||
style={{
|
||||
backgroundColor: theme.warningColor,
|
||||
backgroundColor: activeVariant.colors.warningColor,
|
||||
borderRadius: `${theme.borderRadius}px`,
|
||||
}}
|
||||
>
|
||||
Warning
|
||||
</div>
|
||||
<div
|
||||
className="w-20 h-20 rounded flex items-center justify-center text-white font-semibold"
|
||||
className="w-24 h-24 rounded flex flex-col items-center justify-center text-white font-semibold text-sm"
|
||||
style={{
|
||||
backgroundColor: theme.successColor,
|
||||
backgroundColor: activeVariant.colors.successColor,
|
||||
borderRadius: `${theme.borderRadius}px`,
|
||||
}}
|
||||
>
|
||||
Success
|
||||
</div>
|
||||
{Object.entries(activeVariant.colors.customColors).map(([name, color]) => (
|
||||
<div
|
||||
key={name}
|
||||
className="w-24 h-24 rounded flex flex-col items-center justify-center text-white font-semibold text-sm capitalize"
|
||||
style={{
|
||||
backgroundColor: color,
|
||||
borderRadius: `${theme.borderRadius}px`,
|
||||
}}
|
||||
>
|
||||
{name}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div
|
||||
className="p-4 border"
|
||||
className="p-6"
|
||||
style={{
|
||||
fontFamily: theme.fontFamily,
|
||||
borderRadius: `${theme.borderRadius}px`,
|
||||
backgroundColor: activeVariant.colors.surface,
|
||||
color: activeVariant.colors.text,
|
||||
border: `1px solid ${activeVariant.colors.border}`,
|
||||
}}
|
||||
>
|
||||
<p style={{ fontSize: `${theme.fontSize.large}px` }}>
|
||||
<p style={{ fontSize: `${theme.fontSize.large}px`, marginBottom: '8px' }}>
|
||||
Large Text Sample
|
||||
</p>
|
||||
<p style={{ fontSize: `${theme.fontSize.medium}px` }}>
|
||||
<p style={{ fontSize: `${theme.fontSize.medium}px`, marginBottom: '8px' }}>
|
||||
Medium Text Sample
|
||||
</p>
|
||||
<p style={{ fontSize: `${theme.fontSize.small}px` }}>
|
||||
Small Text Sample
|
||||
<p style={{ fontSize: `${theme.fontSize.small}px`, color: activeVariant.colors.textSecondary }}>
|
||||
Small Text Sample (Secondary)
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<Dialog open={customColorDialogOpen} onOpenChange={setCustomColorDialogOpen}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Add Custom Color</DialogTitle>
|
||||
<DialogDescription>
|
||||
Create a custom color for your theme variant
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4 py-4">
|
||||
<div className="space-y-2">
|
||||
<Label>Color Name</Label>
|
||||
<Input
|
||||
value={newColorName}
|
||||
onChange={(e) => setNewColorName(e.target.value)}
|
||||
placeholder="e.g., accent, highlight, brand"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>Color Value</Label>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
type="color"
|
||||
value={newColorValue}
|
||||
onChange={(e) => setNewColorValue(e.target.value)}
|
||||
className="w-20 h-10 cursor-pointer"
|
||||
/>
|
||||
<Input
|
||||
value={newColorValue}
|
||||
onChange={(e) => setNewColorValue(e.target.value)}
|
||||
className="flex-1 font-mono"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setCustomColorDialogOpen(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={addCustomColor}>Add Color</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Dialog open={newVariantDialogOpen} onOpenChange={setNewVariantDialogOpen}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Add Theme Variant</DialogTitle>
|
||||
<DialogDescription>
|
||||
Create a new theme variant based on the current one
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4 py-4">
|
||||
<div className="space-y-2">
|
||||
<Label>Variant Name</Label>
|
||||
<Input
|
||||
value={newVariantName}
|
||||
onChange={(e) => setNewVariantName(e.target.value)}
|
||||
placeholder="e.g., High Contrast, Colorblind Friendly"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setNewVariantDialogOpen(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={addVariant}>Add Variant</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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<string, string>
|
||||
}
|
||||
|
||||
export interface ThemeVariant {
|
||||
id: string
|
||||
name: string
|
||||
colors: ColorPalette
|
||||
}
|
||||
|
||||
export interface ThemeConfig {
|
||||
variants: ThemeVariant[]
|
||||
activeVariantId: string
|
||||
fontFamily: string
|
||||
fontSize: {
|
||||
small: number
|
||||
|
||||
Reference in New Issue
Block a user