Generated by Spark: Make a React app that can code generate Next.js, Material UI and GHCR apps. It should have Monaco editor for programming actions. It needs a model designer (Prisma?). Try to keep it low code and add gui designers for stylin, component tree and other aspects of generating a react app.

This commit is contained in:
2026-01-16 00:38:18 +00:00
committed by GitHub
parent 0cc2aebec1
commit e5557fbfa9
15 changed files with 1849 additions and 3 deletions

View File

@@ -0,0 +1,85 @@
import Editor from '@monaco-editor/react'
import { Card } from '@/components/ui/card'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { ProjectFile } from '@/types/project'
import { FileCode, X } from '@phosphor-icons/react'
interface CodeEditorProps {
files: ProjectFile[]
activeFileId: string | null
onFileChange: (fileId: string, content: string) => void
onFileSelect: (fileId: string) => void
onFileClose: (fileId: string) => void
}
export function CodeEditor({
files,
activeFileId,
onFileChange,
onFileSelect,
onFileClose,
}: CodeEditorProps) {
const activeFile = files.find((f) => f.id === activeFileId)
const openFiles = files.filter((f) => f.id === activeFileId || files.length < 5)
return (
<div className="h-full flex flex-col">
{openFiles.length > 0 ? (
<>
<div className="flex items-center gap-1 bg-secondary/50 border-b border-border px-2 py-1">
{openFiles.map((file) => (
<button
key={file.id}
onClick={() => onFileSelect(file.id)}
className={`flex items-center gap-2 px-3 py-1.5 rounded text-sm transition-colors ${
file.id === activeFileId
? 'bg-card text-foreground'
: 'text-muted-foreground hover:text-foreground hover:bg-card/50'
}`}
>
<FileCode size={16} />
<span>{file.name}</span>
<button
onClick={(e) => {
e.stopPropagation()
onFileClose(file.id)
}}
className="hover:text-destructive"
>
<X size={14} />
</button>
</button>
))}
</div>
<div className="flex-1">
{activeFile && (
<Editor
height="100%"
language={activeFile.language}
value={activeFile.content}
onChange={(value) => onFileChange(activeFile.id, value || '')}
theme="vs-dark"
options={{
minimap: { enabled: false },
fontSize: 14,
fontFamily: 'JetBrains Mono, monospace',
fontLigatures: true,
lineNumbers: 'on',
scrollBeyondLastLine: false,
automaticLayout: true,
}}
/>
)}
</div>
</>
) : (
<div className="flex-1 flex items-center justify-center text-muted-foreground">
<div className="text-center">
<FileCode size={48} className="mx-auto mb-4 opacity-50" />
<p>Select a file to edit</p>
</div>
</div>
)}
</div>
)
}

View File

@@ -0,0 +1,268 @@
import { useState } from 'react'
import { ComponentNode } from '@/types/project'
import { Card } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import { ScrollArea } from '@/components/ui/scroll-area'
import { Plus, Trash, Tree, CaretRight, CaretDown } from '@phosphor-icons/react'
import { Textarea } from '@/components/ui/textarea'
interface ComponentTreeBuilderProps {
components: ComponentNode[]
onComponentsChange: (components: ComponentNode[]) => void
}
const MUI_COMPONENTS = [
'Box',
'Container',
'Grid',
'Stack',
'Paper',
'Card',
'CardContent',
'CardActions',
'Button',
'TextField',
'Typography',
'AppBar',
'Toolbar',
'List',
'ListItem',
'ListItemText',
'Divider',
'Avatar',
'Chip',
'IconButton',
]
export function ComponentTreeBuilder({
components,
onComponentsChange,
}: ComponentTreeBuilderProps) {
const [selectedNodeId, setSelectedNodeId] = useState<string | null>(null)
const [expandedNodes, setExpandedNodes] = useState<Set<string>>(new Set())
const findNodeById = (
nodes: ComponentNode[],
id: string
): ComponentNode | null => {
for (const node of nodes) {
if (node.id === id) return node
const found = findNodeById(node.children, id)
if (found) return found
}
return null
}
const selectedNode = selectedNodeId ? findNodeById(components, selectedNodeId) : null
const addRootComponent = () => {
const newNode: ComponentNode = {
id: `node-${Date.now()}`,
type: 'Box',
name: `Component${components.length + 1}`,
props: {},
children: [],
}
onComponentsChange([...components, newNode])
setSelectedNodeId(newNode.id)
}
const addChildComponent = (parentId: string) => {
const newNode: ComponentNode = {
id: `node-${Date.now()}`,
type: 'Box',
name: 'NewComponent',
props: {},
children: [],
}
const addChild = (nodes: ComponentNode[]): ComponentNode[] => {
return nodes.map((node) => {
if (node.id === parentId) {
return { ...node, children: [...node.children, newNode] }
}
return { ...node, children: addChild(node.children) }
})
}
onComponentsChange(addChild(components))
setExpandedNodes(new Set([...expandedNodes, parentId]))
setSelectedNodeId(newNode.id)
}
const deleteNode = (nodeId: string) => {
const deleteFromTree = (nodes: ComponentNode[]): ComponentNode[] => {
return nodes
.filter((node) => node.id !== nodeId)
.map((node) => ({ ...node, children: deleteFromTree(node.children) }))
}
onComponentsChange(deleteFromTree(components))
if (selectedNodeId === nodeId) {
setSelectedNodeId(null)
}
}
const updateNode = (nodeId: string, updates: Partial<ComponentNode>) => {
const updateInTree = (nodes: ComponentNode[]): ComponentNode[] => {
return nodes.map((node) => {
if (node.id === nodeId) {
return { ...node, ...updates }
}
return { ...node, children: updateInTree(node.children) }
})
}
onComponentsChange(updateInTree(components))
}
const toggleExpand = (nodeId: string) => {
const newExpanded = new Set(expandedNodes)
if (newExpanded.has(nodeId)) {
newExpanded.delete(nodeId)
} else {
newExpanded.add(nodeId)
}
setExpandedNodes(newExpanded)
}
const renderTreeNode = (node: ComponentNode, level: number = 0) => {
const isExpanded = expandedNodes.has(node.id)
const isSelected = selectedNodeId === node.id
const hasChildren = node.children.length > 0
return (
<div key={node.id}>
<button
onClick={() => setSelectedNodeId(node.id)}
className={`w-full flex items-center gap-2 px-3 py-2 rounded text-sm transition-colors ${
isSelected
? 'bg-accent text-accent-foreground'
: 'hover:bg-muted text-foreground'
}`}
style={{ paddingLeft: `${level * 20 + 12}px` }}
>
{hasChildren && (
<button
onClick={(e) => {
e.stopPropagation()
toggleExpand(node.id)
}}
className="hover:text-accent"
>
{isExpanded ? <CaretDown size={16} /> : <CaretRight size={16} />}
</button>
)}
{!hasChildren && <div className="w-4" />}
<Tree size={16} />
<span className="font-medium">{node.name}</span>
<span className="text-muted-foreground text-xs ml-auto">{node.type}</span>
</button>
{isExpanded &&
node.children.map((child) => renderTreeNode(child, level + 1))}
</div>
)
}
return (
<div className="h-full flex gap-4 p-6">
<div className="w-80 flex flex-col gap-4">
<div className="flex items-center justify-between">
<h3 className="font-semibold text-sm uppercase tracking-wide">
Component Tree
</h3>
<Button size="sm" onClick={addRootComponent} className="h-8 w-8 p-0">
<Plus size={16} />
</Button>
</div>
<ScrollArea className="flex-1 border rounded-lg">
<div className="p-2 space-y-1">
{components.map((node) => renderTreeNode(node))}
</div>
</ScrollArea>
</div>
<Card className="flex-1 p-6">
{selectedNode ? (
<div className="space-y-6">
<div className="flex items-center justify-between">
<h4 className="text-lg font-semibold">Component Properties</h4>
<Button
variant="destructive"
size="sm"
onClick={() => deleteNode(selectedNode.id)}
>
<Trash size={16} />
</Button>
</div>
<div className="grid gap-4">
<div className="space-y-2">
<Label>Component Name</Label>
<Input
value={selectedNode.name}
onChange={(e) =>
updateNode(selectedNode.id, { name: e.target.value })
}
/>
</div>
<div className="space-y-2">
<Label>Component Type</Label>
<Select
value={selectedNode.type}
onValueChange={(value) =>
updateNode(selectedNode.id, { type: value })
}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{MUI_COMPONENTS.map((comp) => (
<SelectItem key={comp} value={comp}>
{comp}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label>Props (JSON)</Label>
<Textarea
value={JSON.stringify(selectedNode.props, null, 2)}
onChange={(e) => {
try {
const props = JSON.parse(e.target.value)
updateNode(selectedNode.id, { props })
} catch (err) {
}
}}
className="font-mono text-sm h-64"
placeholder='{"variant": "contained", "color": "primary"}'
/>
</div>
<Button onClick={() => addChildComponent(selectedNode.id)}>
<Plus size={16} className="mr-2" />
Add Child Component
</Button>
</div>
</div>
) : (
<div className="h-full flex items-center justify-center text-muted-foreground">
<div className="text-center">
<Tree size={48} className="mx-auto mb-4 opacity-50" />
<p>Select a component to edit properties</p>
</div>
</div>
)}
</Card>
</div>
)
}

View File

@@ -0,0 +1,147 @@
import { useState } from 'react'
import { ProjectFile } from '@/types/project'
import { ScrollArea } from '@/components/ui/scroll-area'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { FileCode, FolderOpen, Plus, Folder } from '@phosphor-icons/react'
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@/components/ui/dialog'
import { Label } from '@/components/ui/label'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
interface FileExplorerProps {
files: ProjectFile[]
activeFileId: string | null
onFileSelect: (fileId: string) => void
onFileAdd: (file: ProjectFile) => void
}
export function FileExplorer({
files,
activeFileId,
onFileSelect,
onFileAdd,
}: FileExplorerProps) {
const [isAddDialogOpen, setIsAddDialogOpen] = useState(false)
const [newFileName, setNewFileName] = useState('')
const [newFileLanguage, setNewFileLanguage] = useState('typescript')
const handleAddFile = () => {
if (!newFileName.trim()) return
const newFile: ProjectFile = {
id: `file-${Date.now()}`,
name: newFileName,
path: `/src/${newFileName}`,
content: '',
language: newFileLanguage,
}
onFileAdd(newFile)
setNewFileName('')
setIsAddDialogOpen(false)
}
const groupedFiles = files.reduce((acc, file) => {
const dir = file.path.split('/').slice(0, -1).join('/') || '/'
if (!acc[dir]) acc[dir] = []
acc[dir].push(file)
return acc
}, {} as Record<string, ProjectFile[]>)
return (
<div className="h-full flex flex-col border-r border-border bg-card">
<div className="p-3 border-b border-border flex items-center justify-between">
<h3 className="font-semibold text-sm uppercase tracking-wide flex items-center gap-2">
<FolderOpen size={18} weight="duotone" />
Files
</h3>
<Dialog open={isAddDialogOpen} onOpenChange={setIsAddDialogOpen}>
<DialogTrigger asChild>
<Button size="sm" className="h-7 w-7 p-0">
<Plus size={14} />
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Add New File</DialogTitle>
</DialogHeader>
<div className="space-y-4">
<div className="space-y-2">
<Label>File Name</Label>
<Input
value={newFileName}
onChange={(e) => setNewFileName(e.target.value)}
placeholder="example.tsx"
onKeyDown={(e) => {
if (e.key === 'Enter') handleAddFile()
}}
/>
</div>
<div className="space-y-2">
<Label>Language</Label>
<Select
value={newFileLanguage}
onValueChange={setNewFileLanguage}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="typescript">TypeScript</SelectItem>
<SelectItem value="javascript">JavaScript</SelectItem>
<SelectItem value="css">CSS</SelectItem>
<SelectItem value="json">JSON</SelectItem>
<SelectItem value="prisma">Prisma</SelectItem>
</SelectContent>
</Select>
</div>
<Button onClick={handleAddFile} className="w-full">
Add File
</Button>
</div>
</DialogContent>
</Dialog>
</div>
<ScrollArea className="flex-1">
<div className="p-2">
{Object.entries(groupedFiles).map(([dir, dirFiles]) => (
<div key={dir} className="mb-2">
<div className="flex items-center gap-2 text-xs text-muted-foreground mb-1 px-2">
<Folder size={14} />
<span>{dir}</span>
</div>
<div className="space-y-0.5">
{dirFiles.map((file) => (
<button
key={file.id}
onClick={() => onFileSelect(file.id)}
className={`w-full flex items-center gap-2 px-3 py-2 rounded text-sm transition-colors ${
activeFileId === file.id
? 'bg-accent text-accent-foreground'
: 'hover:bg-muted text-foreground'
}`}
>
<FileCode size={16} />
<span className="truncate">{file.name}</span>
</button>
))}
</div>
</div>
))}
</div>
</ScrollArea>
</div>
)
}

View File

@@ -0,0 +1,269 @@
import { useState } from 'react'
import { PrismaModel, PrismaField } from '@/types/project'
import { Card } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import { Switch } from '@/components/ui/switch'
import { ScrollArea } from '@/components/ui/scroll-area'
import { Plus, Trash, Database } from '@phosphor-icons/react'
import { Badge } from '@/components/ui/badge'
interface ModelDesignerProps {
models: PrismaModel[]
onModelsChange: (models: PrismaModel[]) => void
}
const FIELD_TYPES = [
'String',
'Int',
'Float',
'Boolean',
'DateTime',
'Json',
'Bytes',
]
export function ModelDesigner({ models, onModelsChange }: ModelDesignerProps) {
const [selectedModelId, setSelectedModelId] = useState<string | null>(
models[0]?.id || null
)
const selectedModel = models.find((m) => m.id === selectedModelId)
const addModel = () => {
const newModel: PrismaModel = {
id: `model-${Date.now()}`,
name: `Model${models.length + 1}`,
fields: [
{
id: `field-${Date.now()}`,
name: 'id',
type: 'String',
isRequired: true,
isUnique: true,
isArray: false,
defaultValue: 'cuid()',
},
],
}
onModelsChange([...models, newModel])
setSelectedModelId(newModel.id)
}
const deleteModel = (modelId: string) => {
const newModels = models.filter((m) => m.id !== modelId)
onModelsChange(newModels)
if (selectedModelId === modelId) {
setSelectedModelId(newModels[0]?.id || null)
}
}
const updateModel = (modelId: string, updates: Partial<PrismaModel>) => {
onModelsChange(
models.map((m) => (m.id === modelId ? { ...m, ...updates } : m))
)
}
const addField = () => {
if (!selectedModel) return
const newField: PrismaField = {
id: `field-${Date.now()}`,
name: `field${selectedModel.fields.length + 1}`,
type: 'String',
isRequired: false,
isUnique: false,
isArray: false,
}
updateModel(selectedModel.id, {
fields: [...selectedModel.fields, newField],
})
}
const updateField = (fieldId: string, updates: Partial<PrismaField>) => {
if (!selectedModel) return
updateModel(selectedModel.id, {
fields: selectedModel.fields.map((f) =>
f.id === fieldId ? { ...f, ...updates } : f
),
})
}
const deleteField = (fieldId: string) => {
if (!selectedModel) return
updateModel(selectedModel.id, {
fields: selectedModel.fields.filter((f) => f.id !== fieldId),
})
}
return (
<div className="h-full flex gap-4 p-6">
<div className="w-64 flex flex-col gap-4">
<div className="flex items-center justify-between">
<h3 className="font-semibold text-sm uppercase tracking-wide">Models</h3>
<Button size="sm" onClick={addModel} className="h-8 w-8 p-0">
<Plus size={16} />
</Button>
</div>
<ScrollArea className="flex-1">
<div className="space-y-2">
{models.map((model) => (
<button
key={model.id}
onClick={() => setSelectedModelId(model.id)}
className={`w-full flex items-center justify-between p-3 rounded-lg border transition-colors ${
selectedModelId === model.id
? 'bg-accent text-accent-foreground border-accent'
: 'bg-card text-card-foreground border-border hover:border-accent/50'
}`}
>
<div className="flex items-center gap-2">
<Database size={18} weight="duotone" />
<span className="font-medium">{model.name}</span>
</div>
<Badge variant="secondary">{model.fields.length}</Badge>
</button>
))}
</div>
</ScrollArea>
</div>
<Card className="flex-1 p-6">
{selectedModel ? (
<div className="space-y-6">
<div className="flex items-center justify-between">
<div className="space-y-2 flex-1 mr-4">
<Label>Model Name</Label>
<Input
value={selectedModel.name}
onChange={(e) =>
updateModel(selectedModel.id, { name: e.target.value })
}
className="text-lg font-semibold"
/>
</div>
<Button
variant="destructive"
size="sm"
onClick={() => deleteModel(selectedModel.id)}
>
<Trash size={16} />
</Button>
</div>
<div className="space-y-4">
<div className="flex items-center justify-between">
<h4 className="font-semibold text-sm uppercase tracking-wide">Fields</h4>
<Button size="sm" onClick={addField}>
<Plus size={16} className="mr-2" />
Add Field
</Button>
</div>
<ScrollArea className="h-96">
<div className="space-y-4">
{selectedModel.fields.map((field) => (
<Card key={field.id} className="p-4 bg-secondary/30">
<div className="grid gap-4">
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label>Field Name</Label>
<Input
value={field.name}
onChange={(e) =>
updateField(field.id, { name: e.target.value })
}
/>
</div>
<div className="space-y-2">
<Label>Type</Label>
<Select
value={field.type}
onValueChange={(value) =>
updateField(field.id, { type: value })
}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{FIELD_TYPES.map((type) => (
<SelectItem key={type} value={type}>
{type}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
<div className="flex items-center gap-6">
<div className="flex items-center gap-2">
<Switch
checked={field.isRequired}
onCheckedChange={(checked) =>
updateField(field.id, { isRequired: checked })
}
/>
<Label>Required</Label>
</div>
<div className="flex items-center gap-2">
<Switch
checked={field.isUnique}
onCheckedChange={(checked) =>
updateField(field.id, { isUnique: checked })
}
/>
<Label>Unique</Label>
</div>
<div className="flex items-center gap-2">
<Switch
checked={field.isArray}
onCheckedChange={(checked) =>
updateField(field.id, { isArray: checked })
}
/>
<Label>Array</Label>
</div>
<Button
variant="ghost"
size="sm"
onClick={() => deleteField(field.id)}
className="ml-auto text-destructive hover:text-destructive"
>
<Trash size={16} />
</Button>
</div>
<div className="space-y-2">
<Label>Default Value (optional)</Label>
<Input
value={field.defaultValue || ''}
onChange={(e) =>
updateField(field.id, {
defaultValue: e.target.value,
})
}
placeholder="e.g., now(), cuid(), autoincrement()"
/>
</div>
</div>
</Card>
))}
</div>
</ScrollArea>
</div>
</div>
) : (
<div className="h-full flex items-center justify-center text-muted-foreground">
<div className="text-center">
<Database size={48} className="mx-auto mb-4 opacity-50" />
<p>Create a model to get started</p>
</div>
</div>
)}
</Card>
</div>
)
}

View File

@@ -0,0 +1,315 @@
import { ThemeConfig } 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 { PaintBrush } from '@phosphor-icons/react'
interface StyleDesignerProps {
theme: ThemeConfig
onThemeChange: (theme: ThemeConfig) => void
}
export function StyleDesigner({ theme, onThemeChange }: StyleDesignerProps) {
const updateTheme = (updates: Partial<ThemeConfig>) => {
onThemeChange({ ...theme, ...updates })
}
return (
<div className="h-full overflow-auto p-6">
<div className="max-w-4xl mx-auto space-y-6">
<div>
<h2 className="text-2xl font-bold mb-2">Material UI Theme Designer</h2>
<p className="text-muted-foreground">
Customize your application's visual theme
</p>
</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>
</Card>
<Card className="p-6">
<h3 className="text-lg font-semibold mb-4">Typography</h3>
<div className="space-y-6">
<div className="space-y-2">
<Label>Font Family</Label>
<Input
value={theme.fontFamily}
onChange={(e) => updateTheme({ fontFamily: e.target.value })}
placeholder="Roboto, Arial, sans-serif"
/>
</div>
<div className="space-y-4">
<div className="space-y-2">
<div className="flex justify-between">
<Label>Small Font Size</Label>
<span className="text-sm text-muted-foreground">
{theme.fontSize.small}px
</span>
</div>
<Slider
value={[theme.fontSize.small]}
onValueChange={([value]) =>
updateTheme({
fontSize: { ...theme.fontSize, small: value },
})
}
min={10}
max={20}
step={1}
/>
</div>
<div className="space-y-2">
<div className="flex justify-between">
<Label>Medium Font Size</Label>
<span className="text-sm text-muted-foreground">
{theme.fontSize.medium}px
</span>
</div>
<Slider
value={[theme.fontSize.medium]}
onValueChange={([value]) =>
updateTheme({
fontSize: { ...theme.fontSize, medium: value },
})
}
min={12}
max={24}
step={1}
/>
</div>
<div className="space-y-2">
<div className="flex justify-between">
<Label>Large Font Size</Label>
<span className="text-sm text-muted-foreground">
{theme.fontSize.large}px
</span>
</div>
<Slider
value={[theme.fontSize.large]}
onValueChange={([value]) =>
updateTheme({
fontSize: { ...theme.fontSize, large: value },
})
}
min={16}
max={48}
step={1}
/>
</div>
</div>
</div>
</Card>
<Card className="p-6">
<h3 className="text-lg font-semibold mb-4">Spacing & Shape</h3>
<div className="space-y-6">
<div className="space-y-2">
<div className="flex justify-between">
<Label>Base Spacing Unit</Label>
<span className="text-sm text-muted-foreground">
{theme.spacing}px
</span>
</div>
<Slider
value={[theme.spacing]}
onValueChange={([value]) => updateTheme({ spacing: value })}
min={4}
max={16}
step={1}
/>
<p className="text-xs text-muted-foreground">
Material UI multiplies this value (e.g., spacing(2) = {theme.spacing * 2}px)
</p>
</div>
<div className="space-y-2">
<div className="flex justify-between">
<Label>Border Radius</Label>
<span className="text-sm text-muted-foreground">
{theme.borderRadius}px
</span>
</div>
<Slider
value={[theme.borderRadius]}
onValueChange={([value]) =>
updateTheme({ borderRadius: value })
}
min={0}
max={24}
step={1}
/>
</div>
</div>
</Card>
<Card className="p-6 bg-gradient-to-br from-card to-muted">
<h3 className="text-lg font-semibold mb-4">Preview</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"
style={{
backgroundColor: theme.primaryColor,
borderRadius: `${theme.borderRadius}px`,
}}
>
Primary
</div>
<div
className="w-20 h-20 rounded flex items-center justify-center text-white font-semibold"
style={{
backgroundColor: theme.secondaryColor,
borderRadius: `${theme.borderRadius}px`,
}}
>
Secondary
</div>
<div
className="w-20 h-20 rounded flex items-center justify-center text-white font-semibold"
style={{
backgroundColor: theme.errorColor,
borderRadius: `${theme.borderRadius}px`,
}}
>
Error
</div>
<div
className="w-20 h-20 rounded flex items-center justify-center text-white font-semibold"
style={{
backgroundColor: theme.warningColor,
borderRadius: `${theme.borderRadius}px`,
}}
>
Warning
</div>
<div
className="w-20 h-20 rounded flex items-center justify-center text-white font-semibold"
style={{
backgroundColor: theme.successColor,
borderRadius: `${theme.borderRadius}px`,
}}
>
Success
</div>
</div>
<div
className="p-4 border"
style={{
fontFamily: theme.fontFamily,
borderRadius: `${theme.borderRadius}px`,
}}
>
<p style={{ fontSize: `${theme.fontSize.large}px` }}>
Large Text Sample
</p>
<p style={{ fontSize: `${theme.fontSize.medium}px` }}>
Medium Text Sample
</p>
<p style={{ fontSize: `${theme.fontSize.small}px` }}>
Small Text Sample
</p>
</div>
</div>
</Card>
</div>
</div>
)
}