diff --git a/.spark-initial-sha b/.spark-initial-sha
new file mode 100644
index 0000000..ffea877
--- /dev/null
+++ b/.spark-initial-sha
@@ -0,0 +1 @@
+HEAD
diff --git a/PRD.md b/PRD.md
new file mode 100644
index 0000000..50cf032
--- /dev/null
+++ b/PRD.md
@@ -0,0 +1,129 @@
+# Planning Guide
+
+A visual low-code platform for generating Next.js applications with Material UI styling, integrated Monaco code editor, and Prisma schema designer.
+
+**Experience Qualities**:
+1. **Empowering** - Users feel in control with both visual and code-level editing capabilities
+2. **Intuitive** - Complex application generation feels approachable through well-organized GUI tools
+3. **Professional** - Output-ready code that follows modern best practices and conventions
+
+**Complexity Level**: Complex Application (advanced functionality, likely with multiple views)
+This is a full-featured low-code IDE with multiple integrated tools (code editor, visual designers, schema builder), state management across views, and code generation capabilities that require sophisticated UI organization.
+
+## Essential Features
+
+### Monaco Code Editor Integration
+- **Functionality**: Full-featured code editor with syntax highlighting, autocomplete, and multi-file editing
+- **Purpose**: Allows direct code manipulation for users who want precise control
+- **Trigger**: Clicking on files in the file tree or switching to code view
+- **Progression**: Select file → Editor opens with syntax highlighting → Edit code → Changes auto-saved to state → Preview updates
+- **Success criteria**: Code edits persist, syntax highlighting works for JS/TS/CSS, multiple files can be open in tabs
+
+### Prisma Schema Designer
+- **Functionality**: Visual model designer for database schemas with drag-and-drop field creation
+- **Purpose**: Simplifies database modeling without requiring Prisma syntax knowledge
+- **Trigger**: Opening the Models tab
+- **Progression**: Create model → Add fields with types → Define relations → Visual graph updates → Generate Prisma schema code
+- **Success criteria**: Can create models, fields, relations; generates valid Prisma syntax; visual representation is clear
+
+### Component Tree Builder
+- **Functionality**: Hierarchical tree view for building React component structure
+- **Purpose**: Visual composition of component hierarchy without writing JSX
+- **Trigger**: Opening the Components tab
+- **Progression**: Select component type → Add to tree → Configure props → Nest children → View generated JSX → Export component
+- **Success criteria**: Can add/remove/reorder components; props are editable; generates valid React code
+
+### Style Designer
+- **Functionality**: Visual interface for Material UI theming and component styling
+- **Purpose**: Configure colors, typography, spacing without manual theme object creation
+- **Trigger**: Opening the Styling tab
+- **Progression**: Select theme property → Adjust values with controls → Preview updates live → Export theme configuration
+- **Success criteria**: Color pickers work; typography scales properly; generates valid MUI theme code
+
+### Project Generator
+- **Functionality**: Exports complete Next.js project with all configurations
+- **Purpose**: Converts visual designs into downloadable, runnable applications
+- **Trigger**: Clicking "Generate Project" or "Export Code"
+- **Progression**: User finalizes design → Clicks export → System bundles files → Downloads zip or shows code → User extracts and runs locally
+- **Success criteria**: Generated project structure is valid; includes package.json; code runs without errors
+
+## Edge Case Handling
+- **Empty Projects**: Show welcome screen with quick-start templates when no project exists
+- **Invalid Prisma Schemas**: Validate models and show inline errors before generating code
+- **Circular Component Dependencies**: Detect and warn when component tree has circular references
+- **Missing Required Props**: Highlight components with missing required Material UI props
+- **Large Files**: Implement virtual scrolling and lazy loading for large component trees and file lists
+
+## Design Direction
+The design should evoke a professional IDE environment while remaining approachable - think Visual Studio Code meets Figma. Clean panels, clear hierarchy, and purposeful use of space to avoid overwhelming users with options.
+
+## Color Selection
+
+- **Primary Color**: Deep indigo `oklch(0.45 0.15 270)` - Communicates professionalism and technical capability
+- **Secondary Colors**: Slate gray `oklch(0.35 0.02 250)` for UI chrome and panels; cool blue `oklch(0.55 0.12 240)` for secondary actions
+- **Accent Color**: Bright cyan `oklch(0.70 0.15 200)` - Highlights active elements, code selections, and important CTAs
+- **Foreground/Background Pairings**:
+ - Background (Dark slate #1a1d29 / oklch(0.14 0.02 250)): Light text (#e8eaed / oklch(0.93 0.005 250)) - Ratio 12.5:1 ✓
+ - Primary (Deep indigo oklch(0.45 0.15 270)): White text (#ffffff / oklch(1 0 0)) - Ratio 6.8:1 ✓
+ - Accent (Bright cyan oklch(0.70 0.15 200)): Dark text (#1a1d29 / oklch(0.14 0.02 250)) - Ratio 9.2:1 ✓
+ - Card (Elevated slate #23273a / oklch(0.18 0.02 250)): Light text (#e8eaed / oklch(0.93 0.005 250)) - Ratio 10.1:1 ✓
+
+## Font Selection
+Typography should balance code readability with UI clarity, using a monospace font for the editor and a clean sans-serif for the interface.
+
+- **Typographic Hierarchy**:
+ - H1 (Panel Titles): Space Grotesk Bold/20px/tight letter spacing
+ - H2 (Section Headers): Space Grotesk Medium/16px/normal letter spacing
+ - Body (UI Text): Inter Regular/14px/normal letter spacing
+ - Code (Editor): JetBrains Mono Regular/14px/normal letter spacing with ligatures
+ - Labels: Inter Medium/12px/wide letter spacing/uppercase
+
+## Animations
+Animations should feel responsive and purposeful - quick panel transitions (200ms) for switching views, smooth accordion expansions (250ms) for tree nodes, and subtle hover states (100ms) on interactive elements. Use elastic easing for drawer slides to add personality without slowing workflow.
+
+## Component Selection
+- **Components**:
+ - Tabs for main navigation between Code/Models/Components/Styling views
+ - ResizablePanels for adjustable file tree, editor, and preview panes
+ - Accordion for collapsible sections in designers
+ - ScrollArea for file lists and component trees
+ - Select dropdowns for component type and field type pickers
+ - Input fields for text properties with real-time validation
+ - Button with variants (primary for generate/export, secondary for add/create)
+ - Card for model and component visual representations
+ - Dialog for configuration modals and confirmations
+ - Badge for type indicators and status labels
+
+- **Customizations**:
+ - Custom MonacoEditor wrapper component with theme integration
+ - Custom SchemaNode component for visual Prisma model representation
+ - Custom ComponentTreeItem with drag handles and inline editing
+ - Custom ColorPicker using native color input wrapped in Popover
+
+- **States**:
+ - Buttons: Subtle glow on hover, pressed state with scale, disabled with reduced opacity
+ - Inputs: Border highlight on focus with accent color, error state with red border
+ - Tree items: Background highlight on hover, bold text when selected
+ - Panels: Shadow increase when resizing, smooth opacity transitions
+
+- **Icon Selection**:
+ - Code (code icon) for editor view
+ - Database (database icon) for models
+ - Tree (tree-structure icon) for components
+ - PaintBrush (paint-brush icon) for styling
+ - FileCode (file-code icon) for individual files
+ - Plus (plus icon) for create actions
+ - Download (download icon) for export
+
+- **Spacing**:
+ - Panel padding: p-6 (24px) for main content areas
+ - Card padding: p-4 (16px) for nested items
+ - Gap between elements: gap-4 (16px) default, gap-2 (8px) for tight groups
+ - Section margins: mb-6 (24px) between major sections
+
+- **Mobile**:
+ - Stack panels vertically instead of resizable horizontal splits
+ - Convert main tabs to a drawer menu on mobile
+ - Full-screen editor mode as default on small screens
+ - Collapse file tree into slide-out sheet
+ - Touch-friendly hit areas (min 44px) for all tree items and buttons
diff --git a/index.html b/index.html
index f62002d..14b52f1 100644
--- a/index.html
+++ b/index.html
@@ -4,9 +4,10 @@
-
+ CodeForge - Low-Code App Builder
+
diff --git a/package-lock.json b/package-lock.json
index c92f18e..e5a1a68 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -11,6 +11,7 @@
"@github/spark": ">=0.43.1 <1",
"@heroicons/react": "^2.2.0",
"@hookform/resolvers": "^4.1.3",
+ "@monaco-editor/react": "^4.7.0",
"@octokit/core": "^6.1.4",
"@phosphor-icons/react": "^2.1.7",
"@radix-ui/colors": "^3.0.0",
@@ -1076,6 +1077,29 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
+ "node_modules/@monaco-editor/loader": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.7.0.tgz",
+ "integrity": "sha512-gIwR1HrJrrx+vfyOhYmCZ0/JcWqG5kbfG7+d3f/C1LXk2EvzAbHSg3MQ5lO2sMlo9izoAZ04shohfKLVT6crVA==",
+ "license": "MIT",
+ "dependencies": {
+ "state-local": "^1.0.6"
+ }
+ },
+ "node_modules/@monaco-editor/react": {
+ "version": "4.7.0",
+ "resolved": "https://registry.npmjs.org/@monaco-editor/react/-/react-4.7.0.tgz",
+ "integrity": "sha512-cyzXQCtO47ydzxpQtCGSQGOC8Gk3ZUeBXFAxD+CWXYFo5OqZyZUonFl0DwUlTyAfRHntBfw2p3w4s9R6oe1eCA==",
+ "license": "MIT",
+ "dependencies": {
+ "@monaco-editor/loader": "^1.5.0"
+ },
+ "peerDependencies": {
+ "monaco-editor": ">= 0.25.0 < 1",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@@ -4404,6 +4428,14 @@
"@types/node": "*"
}
},
+ "node_modules/@types/trusted-types": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
+ "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
+ "license": "MIT",
+ "optional": true,
+ "peer": true
+ },
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.48.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.48.0.tgz",
@@ -5992,6 +6024,16 @@
"csstype": "^3.0.2"
}
},
+ "node_modules/dompurify": {
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.7.tgz",
+ "integrity": "sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw==",
+ "license": "(MPL-2.0 OR Apache-2.0)",
+ "peer": true,
+ "optionalDependencies": {
+ "@types/trusted-types": "^2.0.7"
+ }
+ },
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
@@ -7953,6 +7995,30 @@
"node": "*"
}
},
+ "node_modules/monaco-editor": {
+ "version": "0.55.1",
+ "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.55.1.tgz",
+ "integrity": "sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "dompurify": "3.2.7",
+ "marked": "14.0.0"
+ }
+ },
+ "node_modules/monaco-editor/node_modules/marked": {
+ "version": "14.0.0",
+ "resolved": "https://registry.npmjs.org/marked/-/marked-14.0.0.tgz",
+ "integrity": "sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ==",
+ "license": "MIT",
+ "peer": true,
+ "bin": {
+ "marked": "bin/marked.js"
+ },
+ "engines": {
+ "node": ">= 18"
+ }
+ },
"node_modules/motion-dom": {
"version": "12.23.23",
"resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.23.23.tgz",
@@ -9198,6 +9264,12 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/state-local": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/state-local/-/state-local-1.0.7.tgz",
+ "integrity": "sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==",
+ "license": "MIT"
+ },
"node_modules/statuses": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
diff --git a/package.json b/package.json
index 165aec8..6fac6a5 100644
--- a/package.json
+++ b/package.json
@@ -15,6 +15,7 @@
"@github/spark": ">=0.43.1 <1",
"@heroicons/react": "^2.2.0",
"@hookform/resolvers": "^4.1.3",
+ "@monaco-editor/react": "^4.7.0",
"@octokit/core": "^6.1.4",
"@phosphor-icons/react": "^2.1.7",
"@radix-ui/colors": "^3.0.0",
diff --git a/src/App.tsx b/src/App.tsx
index 98ef973..acbc05f 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,5 +1,251 @@
+import { useState } from 'react'
+import { useKV } from '@github/spark/hooks'
+import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
+import { Button } from '@/components/ui/button'
+import { Card } from '@/components/ui/card'
+import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from '@/components/ui/resizable'
+import { Code, Database, Tree, PaintBrush, Download, Sparkle } from '@phosphor-icons/react'
+import { ProjectFile, PrismaModel, ComponentNode, ThemeConfig } from '@/types/project'
+import { CodeEditor } from '@/components/CodeEditor'
+import { ModelDesigner } from '@/components/ModelDesigner'
+import { ComponentTreeBuilder } from '@/components/ComponentTreeBuilder'
+import { StyleDesigner } from '@/components/StyleDesigner'
+import { FileExplorer } from '@/components/FileExplorer'
+import { generateNextJSProject, generatePrismaSchema, generateMUITheme } from '@/lib/generators'
+import { toast } from 'sonner'
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogHeader,
+ DialogTitle,
+} from '@/components/ui/dialog'
+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',
+ fontFamily: 'Roboto, Arial, sans-serif',
+ fontSize: { small: 12, medium: 14, large: 20 },
+ spacing: 8,
+ borderRadius: 4,
+}
+
+const DEFAULT_FILES: ProjectFile[] = [
+ {
+ id: 'file-1',
+ name: 'page.tsx',
+ path: '/src/app/page.tsx',
+ content: `'use client'\n\nimport { ThemeProvider } from '@mui/material/styles'\nimport CssBaseline from '@mui/material/CssBaseline'\nimport { theme } from '@/theme'\nimport { Box, Typography, Button } from '@mui/material'\n\nexport default function Home() {\n return (\n \n \n \n \n Welcome to Your App\n \n \n Get Started\n \n \n \n )\n}`,
+ language: 'typescript',
+ },
+ {
+ id: 'file-2',
+ name: 'layout.tsx',
+ path: '/src/app/layout.tsx',
+ content: `export const metadata = {\n title: 'My Next.js App',\n description: 'Generated with CodeForge',\n}\n\nexport default function RootLayout({\n children,\n}: {\n children: React.ReactNode\n}) {\n return (\n \n {children}\n \n )\n}`,
+ language: 'typescript',
+ },
+]
+
function App() {
- return
+ const [files, setFiles] = useKV('project-files', DEFAULT_FILES)
+ const [models, setModels] = useKV('project-models', [])
+ const [components, setComponents] = useKV('project-components', [])
+ const [theme, setTheme] = useKV('project-theme', DEFAULT_THEME)
+ const [activeFileId, setActiveFileId] = useState((files || [])[0]?.id || null)
+ const [activeTab, setActiveTab] = useState('code')
+ const [exportDialogOpen, setExportDialogOpen] = useState(false)
+ const [generatedCode, setGeneratedCode] = useState>({})
+
+ const safeFiles = files || []
+ const safeModels = models || []
+ const safeComponents = components || []
+ const safeTheme = theme || DEFAULT_THEME
+
+ const handleFileChange = (fileId: string, content: string) => {
+ setFiles((currentFiles) =>
+ (currentFiles || []).map((f) => (f.id === fileId ? { ...f, content } : f))
+ )
+ }
+
+ const handleFileAdd = (file: ProjectFile) => {
+ setFiles((currentFiles) => [...(currentFiles || []), file])
+ setActiveFileId(file.id)
+ }
+
+ const handleFileClose = (fileId: string) => {
+ if (activeFileId === fileId) {
+ const currentIndex = safeFiles.findIndex((f) => f.id === fileId)
+ const nextFile = safeFiles[currentIndex + 1] || safeFiles[currentIndex - 1]
+ setActiveFileId(nextFile?.id || null)
+ }
+ }
+
+ const handleExportProject = () => {
+ const projectFiles = generateNextJSProject('my-nextjs-app', safeModels, safeComponents, safeTheme)
+
+ const prismaSchema = generatePrismaSchema(safeModels)
+ const themeCode = generateMUITheme(safeTheme)
+
+ const allFiles = {
+ ...projectFiles,
+ 'prisma/schema.prisma': prismaSchema,
+ 'src/theme.ts': themeCode,
+ ...safeFiles.reduce((acc, file) => {
+ acc[file.path] = file.content
+ return acc
+ }, {} as Record),
+ }
+
+ setGeneratedCode(allFiles)
+ setExportDialogOpen(true)
+ toast.success('Project files generated!')
+ }
+
+ const handleGenerateWithAI = async () => {
+ try {
+ toast.info('AI generation coming soon!')
+ } catch (error) {
+ toast.error('AI generation failed')
+ }
+ }
+
+ return (
+
+
+
+
+
+
+
+
+ Code Editor
+
+
+
+ Models
+
+
+
+ Components
+
+
+
+ Styling
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Generated Project Files
+
+ Copy these files to create your Next.js application
+
+
+
+
+ {Object.entries(generatedCode).map(([path, content]) => (
+
+
+
+ {path}
+
+ {
+ navigator.clipboard.writeText(content)
+ toast.success(`Copied ${path}`)
+ }}
+ >
+ Copy
+
+
+
+
+ ))}
+
+
+
+
+
+ )
}
export default App
\ No newline at end of file
diff --git a/src/components/CodeEditor.tsx b/src/components/CodeEditor.tsx
new file mode 100644
index 0000000..9bfb916
--- /dev/null
+++ b/src/components/CodeEditor.tsx
@@ -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 (
+
+ {openFiles.length > 0 ? (
+ <>
+
+ {openFiles.map((file) => (
+ 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'
+ }`}
+ >
+
+ {file.name}
+ {
+ e.stopPropagation()
+ onFileClose(file.id)
+ }}
+ className="hover:text-destructive"
+ >
+
+
+
+ ))}
+
+
+ {activeFile && (
+ 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,
+ }}
+ />
+ )}
+
+ >
+ ) : (
+
+
+
+
Select a file to edit
+
+
+ )}
+
+ )
+}
diff --git a/src/components/ComponentTreeBuilder.tsx b/src/components/ComponentTreeBuilder.tsx
new file mode 100644
index 0000000..49ca24d
--- /dev/null
+++ b/src/components/ComponentTreeBuilder.tsx
@@ -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(null)
+ const [expandedNodes, setExpandedNodes] = useState>(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) => {
+ 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 (
+
+
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 && (
+ {
+ e.stopPropagation()
+ toggleExpand(node.id)
+ }}
+ className="hover:text-accent"
+ >
+ {isExpanded ? : }
+
+ )}
+ {!hasChildren &&
}
+
+ {node.name}
+ {node.type}
+
+ {isExpanded &&
+ node.children.map((child) => renderTreeNode(child, level + 1))}
+
+ )
+ }
+
+ return (
+
+
+
+
+ Component Tree
+
+
+
+
+
+
+
+ {components.map((node) => renderTreeNode(node))}
+
+
+
+
+
+ {selectedNode ? (
+
+
+
Component Properties
+ deleteNode(selectedNode.id)}
+ >
+
+
+
+
+
+
+ Component Name
+
+ updateNode(selectedNode.id, { name: e.target.value })
+ }
+ />
+
+
+
+ Component Type
+
+ updateNode(selectedNode.id, { type: value })
+ }
+ >
+
+
+
+
+ {MUI_COMPONENTS.map((comp) => (
+
+ {comp}
+
+ ))}
+
+
+
+
+
+ Props (JSON)
+
+
+
addChildComponent(selectedNode.id)}>
+
+ Add Child Component
+
+
+
+ ) : (
+
+
+
+
Select a component to edit properties
+
+
+ )}
+
+
+ )
+}
diff --git a/src/components/FileExplorer.tsx b/src/components/FileExplorer.tsx
new file mode 100644
index 0000000..2d56072
--- /dev/null
+++ b/src/components/FileExplorer.tsx
@@ -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)
+
+ return (
+
+
+
+
+ Files
+
+
+
+
+
+
+
+
+
+ Add New File
+
+
+
+ File Name
+ setNewFileName(e.target.value)}
+ placeholder="example.tsx"
+ onKeyDown={(e) => {
+ if (e.key === 'Enter') handleAddFile()
+ }}
+ />
+
+
+ Language
+
+
+
+
+
+ TypeScript
+ JavaScript
+ CSS
+ JSON
+ Prisma
+
+
+
+
+ Add File
+
+
+
+
+
+
+
+ {Object.entries(groupedFiles).map(([dir, dirFiles]) => (
+
+
+
+ {dir}
+
+
+ {dirFiles.map((file) => (
+ 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'
+ }`}
+ >
+
+ {file.name}
+
+ ))}
+
+
+ ))}
+
+
+
+ )
+}
diff --git a/src/components/ModelDesigner.tsx b/src/components/ModelDesigner.tsx
new file mode 100644
index 0000000..bbd6529
--- /dev/null
+++ b/src/components/ModelDesigner.tsx
@@ -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(
+ 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) => {
+ 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) => {
+ 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 (
+
+
+
+
+
+ {models.map((model) => (
+
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'
+ }`}
+ >
+
+
+ {model.name}
+
+ {model.fields.length}
+
+ ))}
+
+
+
+
+
+ {selectedModel ? (
+
+
+
+ Model Name
+
+ updateModel(selectedModel.id, { name: e.target.value })
+ }
+ className="text-lg font-semibold"
+ />
+
+
deleteModel(selectedModel.id)}
+ >
+
+
+
+
+
+
+
Fields
+
+
+ Add Field
+
+
+
+
+
+ {selectedModel.fields.map((field) => (
+
+
+
+
+ Field Name
+
+ updateField(field.id, { name: e.target.value })
+ }
+ />
+
+
+ Type
+
+ updateField(field.id, { type: value })
+ }
+ >
+
+
+
+
+ {FIELD_TYPES.map((type) => (
+
+ {type}
+
+ ))}
+
+
+
+
+
+
+
+
+ updateField(field.id, { isRequired: checked })
+ }
+ />
+ Required
+
+
+
+ updateField(field.id, { isUnique: checked })
+ }
+ />
+ Unique
+
+
+
+ updateField(field.id, { isArray: checked })
+ }
+ />
+ Array
+
+
deleteField(field.id)}
+ className="ml-auto text-destructive hover:text-destructive"
+ >
+
+
+
+
+
+ Default Value (optional)
+
+ updateField(field.id, {
+ defaultValue: e.target.value,
+ })
+ }
+ placeholder="e.g., now(), cuid(), autoincrement()"
+ />
+
+
+
+ ))}
+
+
+
+
+ ) : (
+
+
+
+
Create a model to get started
+
+
+ )}
+
+
+ )
+}
diff --git a/src/components/StyleDesigner.tsx b/src/components/StyleDesigner.tsx
new file mode 100644
index 0000000..e978203
--- /dev/null
+++ b/src/components/StyleDesigner.tsx
@@ -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) => {
+ onThemeChange({ ...theme, ...updates })
+ }
+
+ return (
+
+
+
+
Material UI Theme Designer
+
+ Customize your application's visual theme
+
+
+
+
+
+
+ Color Palette
+
+
+
+
+
+ Typography
+
+
+ Font Family
+ updateTheme({ fontFamily: e.target.value })}
+ placeholder="Roboto, Arial, sans-serif"
+ />
+
+
+
+
+
+ Small Font Size
+
+ {theme.fontSize.small}px
+
+
+
+ updateTheme({
+ fontSize: { ...theme.fontSize, small: value },
+ })
+ }
+ min={10}
+ max={20}
+ step={1}
+ />
+
+
+
+
+ Medium Font Size
+
+ {theme.fontSize.medium}px
+
+
+
+ updateTheme({
+ fontSize: { ...theme.fontSize, medium: value },
+ })
+ }
+ min={12}
+ max={24}
+ step={1}
+ />
+
+
+
+
+ Large Font Size
+
+ {theme.fontSize.large}px
+
+
+
+ updateTheme({
+ fontSize: { ...theme.fontSize, large: value },
+ })
+ }
+ min={16}
+ max={48}
+ step={1}
+ />
+
+
+
+
+
+
+ Spacing & Shape
+
+
+
+ Base Spacing Unit
+
+ {theme.spacing}px
+
+
+
updateTheme({ spacing: value })}
+ min={4}
+ max={16}
+ step={1}
+ />
+
+ Material UI multiplies this value (e.g., spacing(2) = {theme.spacing * 2}px)
+
+
+
+
+
+ Border Radius
+
+ {theme.borderRadius}px
+
+
+
+ updateTheme({ borderRadius: value })
+ }
+ min={0}
+ max={24}
+ step={1}
+ />
+
+
+
+
+
+ Preview
+
+
+
+ Primary
+
+
+ Secondary
+
+
+ Error
+
+
+ Warning
+
+
+ Success
+
+
+
+
+ Large Text Sample
+
+
+ Medium Text Sample
+
+
+ Small Text Sample
+
+
+
+
+
+
+ )
+}
diff --git a/src/index.css b/src/index.css
index 860d6f2..11489c6 100644
--- a/src/index.css
+++ b/src/index.css
@@ -1 +1,81 @@
-/* This is where custom CSS goes */
\ No newline at end of file
+@import 'tailwindcss';
+@import "tw-animate-css";
+
+@layer base {
+ * {
+ @apply border-border
+ }
+
+ body {
+ font-family: 'Inter', sans-serif;
+ }
+
+ h1, h2, h3 {
+ font-family: 'Space Grotesk', sans-serif;
+ }
+
+ code, pre {
+ font-family: 'JetBrains Mono', monospace;
+ }
+}
+
+:root {
+ --background: oklch(0.14 0.02 250);
+ --foreground: oklch(0.93 0.005 250);
+
+ --card: oklch(0.18 0.02 250);
+ --card-foreground: oklch(0.93 0.005 250);
+
+ --popover: oklch(0.18 0.02 250);
+ --popover-foreground: oklch(0.93 0.005 250);
+
+ --primary: oklch(0.45 0.15 270);
+ --primary-foreground: oklch(1 0 0);
+
+ --secondary: oklch(0.35 0.02 250);
+ --secondary-foreground: oklch(0.93 0.005 250);
+
+ --muted: oklch(0.22 0.02 250);
+ --muted-foreground: oklch(0.65 0.01 250);
+
+ --accent: oklch(0.70 0.15 200);
+ --accent-foreground: oklch(0.14 0.02 250);
+
+ --destructive: oklch(0.55 0.22 25);
+ --destructive-foreground: oklch(1 0 0);
+
+ --border: oklch(0.28 0.02 250);
+ --input: oklch(0.28 0.02 250);
+ --ring: oklch(0.70 0.15 200);
+
+ --radius: 0.5rem;
+}
+
+@theme {
+ --color-background: var(--background);
+ --color-foreground: var(--foreground);
+ --color-card: var(--card);
+ --color-card-foreground: var(--card-foreground);
+ --color-popover: var(--popover);
+ --color-popover-foreground: var(--popover-foreground);
+ --color-primary: var(--primary);
+ --color-primary-foreground: var(--primary-foreground);
+ --color-secondary: var(--secondary);
+ --color-secondary-foreground: var(--secondary-foreground);
+ --color-muted: var(--muted);
+ --color-muted-foreground: var(--muted-foreground);
+ --color-accent: var(--accent);
+ --color-accent-foreground: var(--accent-foreground);
+ --color-destructive: var(--destructive);
+ --color-destructive-foreground: var(--destructive-foreground);
+ --color-border: var(--border);
+ --color-input: var(--input);
+ --color-ring: var(--ring);
+
+ --radius-sm: calc(var(--radius) * 0.5);
+ --radius-md: var(--radius);
+ --radius-lg: calc(var(--radius) * 1.5);
+ --radius-xl: calc(var(--radius) * 2);
+ --radius-2xl: calc(var(--radius) * 3);
+ --radius-full: 9999px;
+}
\ No newline at end of file
diff --git a/src/lib/generators.ts b/src/lib/generators.ts
new file mode 100644
index 0000000..143c153
--- /dev/null
+++ b/src/lib/generators.ts
@@ -0,0 +1,174 @@
+import { PrismaModel, ComponentNode, ThemeConfig } from '@/types/project'
+
+export function generatePrismaSchema(models: PrismaModel[]): string {
+ let schema = `generator client {\n provider = "prisma-client-js"\n}\n\n`
+ schema += `datasource db {\n provider = "postgresql"\n url = env("DATABASE_URL")\n}\n\n`
+
+ models.forEach((model) => {
+ schema += `model ${model.name} {\n`
+ model.fields.forEach((field) => {
+ let fieldLine = ` ${field.name} ${field.type}`
+ if (field.isArray) fieldLine += '[]'
+ if (field.isRequired && !field.defaultValue) fieldLine += ''
+ else if (!field.isRequired) fieldLine += '?'
+ if (field.isUnique) fieldLine += ' @unique'
+ if (field.defaultValue) fieldLine += ` @default(${field.defaultValue})`
+ schema += fieldLine + '\n'
+ })
+ schema += `}\n\n`
+ })
+
+ return schema
+}
+
+export function generateComponentCode(node: ComponentNode, indent: number = 0): string {
+ const spaces = ' '.repeat(indent)
+ const propsStr = Object.entries(node.props)
+ .map(([key, value]) => {
+ if (typeof value === 'string') return `${key}="${value}"`
+ if (typeof value === 'boolean') return value ? key : ''
+ return `${key}={${JSON.stringify(value)}}`
+ })
+ .filter(Boolean)
+ .join(' ')
+
+ if (node.children.length === 0) {
+ return `${spaces}<${node.type}${propsStr ? ' ' + propsStr : ''} />`
+ }
+
+ let code = `${spaces}<${node.type}${propsStr ? ' ' + propsStr : ''}>\n`
+ node.children.forEach((child) => {
+ code += generateComponentCode(child, indent + 1) + '\n'
+ })
+ code += `${spaces}${node.type}>`
+
+ return code
+}
+
+export function generateMUITheme(theme: ThemeConfig): string {
+ return `import { createTheme } from '@mui/material/styles';
+
+export const theme = createTheme({
+ palette: {
+ primary: {
+ main: '${theme.primaryColor}',
+ },
+ secondary: {
+ main: '${theme.secondaryColor}',
+ },
+ error: {
+ main: '${theme.errorColor}',
+ },
+ warning: {
+ main: '${theme.warningColor}',
+ },
+ success: {
+ main: '${theme.successColor}',
+ },
+ },
+ typography: {
+ fontFamily: '${theme.fontFamily}',
+ fontSize: ${theme.fontSize.medium},
+ },
+ spacing: ${theme.spacing},
+ shape: {
+ borderRadius: ${theme.borderRadius},
+ },
+});`
+}
+
+export function generateNextJSProject(
+ projectName: string,
+ models: PrismaModel[],
+ components: ComponentNode[],
+ theme: ThemeConfig
+): Record {
+ const files: Record = {}
+
+ files['package.json'] = JSON.stringify(
+ {
+ name: projectName,
+ version: '0.1.0',
+ private: true,
+ scripts: {
+ dev: 'next dev',
+ build: 'next build',
+ start: 'next start',
+ lint: 'next lint',
+ },
+ dependencies: {
+ '@mui/material': '^5.15.0',
+ '@emotion/react': '^11.11.0',
+ '@emotion/styled': '^11.11.0',
+ '@prisma/client': '^5.8.0',
+ next: '14.1.0',
+ react: '^18.2.0',
+ 'react-dom': '^18.2.0',
+ },
+ devDependencies: {
+ '@types/node': '^20',
+ '@types/react': '^18',
+ '@types/react-dom': '^18',
+ prisma: '^5.8.0',
+ typescript: '^5',
+ },
+ },
+ null,
+ 2
+ )
+
+ files['prisma/schema.prisma'] = generatePrismaSchema(models)
+
+ files['src/theme.ts'] = generateMUITheme(theme)
+
+ files['src/app/page.tsx'] = `'use client'
+
+import { ThemeProvider } from '@mui/material/styles'
+import CssBaseline from '@mui/material/CssBaseline'
+import { theme } from '@/theme'
+
+export default function Home() {
+ return (
+
+
+
+ {/* Your components here */}
+
+
+ )
+}`
+
+ files['next.config.js'] = `/** @type {import('next').NextConfig} */
+const nextConfig = {}
+
+module.exports = nextConfig`
+
+ files['.env'] = `DATABASE_URL="postgresql://user:password@localhost:5432/mydb"`
+
+ files['README.md'] = `# ${projectName}
+
+Generated with CodeForge
+
+## Getting Started
+
+1. Install dependencies:
+\`\`\`bash
+npm install
+\`\`\`
+
+2. Set up your database in .env
+
+3. Run Prisma migrations:
+\`\`\`bash
+npx prisma migrate dev
+\`\`\`
+
+4. Start the development server:
+\`\`\`bash
+npm run dev
+\`\`\`
+
+Open [http://localhost:3000](http://localhost:3000) with your browser.`
+
+ return files
+}
diff --git a/src/main.tsx b/src/main.tsx
index de0ac2e..3dcf9ba 100644
--- a/src/main.tsx
+++ b/src/main.tsx
@@ -4,6 +4,7 @@ import "@github/spark/spark"
import App from './App.tsx'
import { ErrorFallback } from './ErrorFallback.tsx'
+import { Toaster } from './components/ui/sonner.tsx'
import "./main.css"
import "./styles/theme.css"
@@ -12,5 +13,6 @@ import "./index.css"
createRoot(document.getElementById('root')!).render(
+
)
diff --git a/src/types/project.ts b/src/types/project.ts
new file mode 100644
index 0000000..ffb387d
--- /dev/null
+++ b/src/types/project.ts
@@ -0,0 +1,56 @@
+export interface ProjectFile {
+ id: string
+ name: string
+ path: string
+ content: string
+ language: string
+}
+
+export interface PrismaModel {
+ id: string
+ name: string
+ fields: PrismaField[]
+}
+
+export interface PrismaField {
+ id: string
+ name: string
+ type: string
+ isRequired: boolean
+ isUnique: boolean
+ isArray: boolean
+ defaultValue?: string
+ relation?: string
+}
+
+export interface ComponentNode {
+ id: string
+ type: string
+ props: Record
+ children: ComponentNode[]
+ name: string
+}
+
+export interface ThemeConfig {
+ primaryColor: string
+ secondaryColor: string
+ errorColor: string
+ warningColor: string
+ successColor: string
+ fontFamily: string
+ fontSize: {
+ small: number
+ medium: number
+ large: number
+ }
+ spacing: number
+ borderRadius: number
+}
+
+export interface Project {
+ name: string
+ files: ProjectFile[]
+ models: PrismaModel[]
+ components: ComponentNode[]
+ theme: ThemeConfig
+}