mirror of
https://github.com/johndoe6345789/low-code-react-app-b.git
synced 2026-04-24 13:44:54 +00:00
Generated by Spark: Oh wow I clicked publish and it works. Can we make better use of the new declarative system?
This commit is contained in:
399
DECLARATIVE_SYSTEM.md
Normal file
399
DECLARATIVE_SYSTEM.md
Normal file
@@ -0,0 +1,399 @@
|
||||
# Declarative System Documentation
|
||||
|
||||
## Overview
|
||||
|
||||
CodeForge now uses a **declarative, JSON-driven architecture** that makes it easy to add, modify, and configure pages without touching core application code. This system reduces complexity, improves maintainability, and makes the codebase more scalable.
|
||||
|
||||
## Key Benefits
|
||||
|
||||
✅ **Add new pages by editing a JSON file** - no need to modify App.tsx
|
||||
✅ **Dynamic component loading** - components are lazy-loaded based on configuration
|
||||
✅ **Automatic keyboard shortcuts** - defined in JSON, automatically wired up
|
||||
✅ **Feature toggle integration** - pages automatically show/hide based on feature flags
|
||||
✅ **Consistent page structure** - all pages follow the same rendering pattern
|
||||
✅ **Easy to test and maintain** - configuration is separate from implementation
|
||||
|
||||
## Architecture
|
||||
|
||||
### Configuration Files
|
||||
|
||||
#### `/src/config/pages.json`
|
||||
The main configuration file that defines all pages in the application.
|
||||
|
||||
```json
|
||||
{
|
||||
"pages": [
|
||||
{
|
||||
"id": "dashboard",
|
||||
"title": "Dashboard",
|
||||
"icon": "ChartBar",
|
||||
"component": "ProjectDashboard",
|
||||
"enabled": true,
|
||||
"shortcut": "ctrl+1",
|
||||
"order": 1
|
||||
},
|
||||
{
|
||||
"id": "code",
|
||||
"title": "Code Editor",
|
||||
"icon": "Code",
|
||||
"component": "CodeEditor",
|
||||
"enabled": true,
|
||||
"toggleKey": "codeEditor",
|
||||
"shortcut": "ctrl+2",
|
||||
"order": 2,
|
||||
"requiresResizable": true
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### Page Configuration Schema
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| `id` | string | ✅ | Unique identifier for the page (used in routing) |
|
||||
| `title` | string | ✅ | Display name for the page |
|
||||
| `icon` | string | ✅ | Phosphor icon name |
|
||||
| `component` | string | ✅ | Component name (must exist in componentMap) |
|
||||
| `enabled` | boolean | ✅ | Whether the page is available |
|
||||
| `toggleKey` | string | ❌ | Feature toggle key (from FeatureToggles type) |
|
||||
| `shortcut` | string | ❌ | Keyboard shortcut (e.g., "ctrl+1", "ctrl+shift+e") |
|
||||
| `order` | number | ✅ | Display order in navigation |
|
||||
| `requiresResizable` | boolean | ❌ | Special flag for split-pane layouts |
|
||||
|
||||
### Core Functions
|
||||
|
||||
#### `getPageConfig()`
|
||||
Returns the complete page configuration.
|
||||
|
||||
```typescript
|
||||
import { getPageConfig } from '@/config/page-loader'
|
||||
|
||||
const config = getPageConfig()
|
||||
// Returns: { pages: PageConfig[] }
|
||||
```
|
||||
|
||||
#### `getEnabledPages(featureToggles)`
|
||||
Returns only pages that are enabled and pass feature toggle checks.
|
||||
|
||||
```typescript
|
||||
import { getEnabledPages } from '@/config/page-loader'
|
||||
|
||||
const enabledPages = getEnabledPages(featureToggles)
|
||||
// Returns: PageConfig[]
|
||||
```
|
||||
|
||||
#### `getPageShortcuts(featureToggles)`
|
||||
Returns keyboard shortcuts for enabled pages.
|
||||
|
||||
```typescript
|
||||
import { getPageShortcuts } from '@/config/page-loader'
|
||||
|
||||
const shortcuts = getPageShortcuts(featureToggles)
|
||||
// Returns: Array<{ key: string, ctrl?: boolean, shift?: boolean, description: string, action: string }>
|
||||
```
|
||||
|
||||
## How to Add a New Page
|
||||
|
||||
### Step 1: Create Your Component
|
||||
|
||||
```typescript
|
||||
// src/components/MyNewDesigner.tsx
|
||||
export function MyNewDesigner() {
|
||||
return (
|
||||
<div className="p-6">
|
||||
<h1>My New Designer</h1>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Step 2: Add to Component Map
|
||||
|
||||
In `src/App.tsx`, add your component to the `componentMap`:
|
||||
|
||||
```typescript
|
||||
const componentMap: Record<string, React.LazyExoticComponent<any>> = {
|
||||
// ... existing components
|
||||
MyNewDesigner: lazy(() => import('@/components/MyNewDesigner').then(m => ({ default: m.MyNewDesigner }))),
|
||||
}
|
||||
```
|
||||
|
||||
### Step 3: Add to pages.json
|
||||
|
||||
Add your page configuration to `/src/config/pages.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "my-new-page",
|
||||
"title": "My New Designer",
|
||||
"icon": "Sparkle",
|
||||
"component": "MyNewDesigner",
|
||||
"enabled": true,
|
||||
"toggleKey": "myNewFeature",
|
||||
"shortcut": "ctrl+shift+n",
|
||||
"order": 21
|
||||
}
|
||||
```
|
||||
|
||||
### Step 4: (Optional) Add Feature Toggle
|
||||
|
||||
If using a feature toggle, add it to the `FeatureToggles` type in `/src/types/project.ts`:
|
||||
|
||||
```typescript
|
||||
export interface FeatureToggles {
|
||||
// ... existing toggles
|
||||
myNewFeature: boolean
|
||||
}
|
||||
```
|
||||
|
||||
And update the default in `/src/hooks/use-project-state.ts`:
|
||||
|
||||
```typescript
|
||||
const DEFAULT_FEATURE_TOGGLES: FeatureToggles = {
|
||||
// ... existing toggles
|
||||
myNewFeature: true,
|
||||
}
|
||||
```
|
||||
|
||||
### Step 5: (Optional) Add Props Mapping
|
||||
|
||||
If your component needs props, add it to `getPropsForComponent` in `App.tsx`:
|
||||
|
||||
```typescript
|
||||
const getPropsForComponent = (pageId: string) => {
|
||||
const propsMap: Record<string, any> = {
|
||||
// ... existing mappings
|
||||
'MyNewDesigner': {
|
||||
data: someData,
|
||||
onDataChange: setSomeData,
|
||||
},
|
||||
}
|
||||
return propsMap[pageId] || {}
|
||||
}
|
||||
```
|
||||
|
||||
That's it! Your new page will now:
|
||||
- ✅ Appear in the navigation menu
|
||||
- ✅ Be accessible via the keyboard shortcut
|
||||
- ✅ Show/hide based on the feature toggle
|
||||
- ✅ Be searchable in global search
|
||||
- ✅ Follow the same rendering pattern as other pages
|
||||
|
||||
## Component Map
|
||||
|
||||
The `componentMap` in `App.tsx` is the registry of all available components:
|
||||
|
||||
```typescript
|
||||
const componentMap: Record<string, React.LazyExoticComponent<any>> = {
|
||||
ProjectDashboard: lazy(() => import('@/components/ProjectDashboard').then(m => ({ default: m.ProjectDashboard }))),
|
||||
CodeEditor: lazy(() => import('@/components/CodeEditor').then(m => ({ default: m.CodeEditor }))),
|
||||
// ... more components
|
||||
}
|
||||
```
|
||||
|
||||
All components are **lazy-loaded** for optimal performance. They only load when the user navigates to that page.
|
||||
|
||||
## Special Layouts
|
||||
|
||||
### Split-Pane Layout (Code Editor)
|
||||
|
||||
The code editor uses a special resizable split-pane layout. This is handled by the `requiresResizable` flag:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "code",
|
||||
"component": "CodeEditor",
|
||||
"requiresResizable": true
|
||||
}
|
||||
```
|
||||
|
||||
The rendering logic in `App.tsx` checks for this flag and renders the appropriate layout.
|
||||
|
||||
## Feature Toggles Integration
|
||||
|
||||
Pages can be conditionally enabled based on feature toggles:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "playwright",
|
||||
"toggleKey": "playwright",
|
||||
"enabled": true
|
||||
}
|
||||
```
|
||||
|
||||
The page will only appear if:
|
||||
1. `enabled` is `true` in the JSON
|
||||
2. `featureToggles.playwright` is `true` (or undefined)
|
||||
|
||||
Users can toggle features on/off in the **Features** page.
|
||||
|
||||
## Keyboard Shortcuts
|
||||
|
||||
Shortcuts are automatically parsed from the configuration:
|
||||
|
||||
```json
|
||||
{
|
||||
"shortcut": "ctrl+1"
|
||||
}
|
||||
```
|
||||
|
||||
Supported modifiers:
|
||||
- `ctrl` - Control key
|
||||
- `shift` - Shift key
|
||||
- `alt` - Alt key (not implemented yet, but easy to add)
|
||||
|
||||
Format: `[modifier+]key` (e.g., "ctrl+1", "ctrl+shift+e", "f")
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
The declarative system can be extended to support:
|
||||
|
||||
### 1. Dynamic Props from JSON
|
||||
```json
|
||||
{
|
||||
"component": "MyComponent",
|
||||
"props": {
|
||||
"title": "Dynamic Title",
|
||||
"showToolbar": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Layout Configuration
|
||||
```json
|
||||
{
|
||||
"layout": {
|
||||
"type": "split",
|
||||
"direction": "horizontal",
|
||||
"panels": [
|
||||
{ "component": "Sidebar", "size": 20 },
|
||||
{ "component": "MainContent", "size": 80 }
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Permission-Based Access
|
||||
```json
|
||||
{
|
||||
"permissions": ["admin", "editor"]
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Page Groups/Categories
|
||||
```json
|
||||
{
|
||||
"category": "Design Tools",
|
||||
"group": "styling"
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Page Metadata
|
||||
```json
|
||||
{
|
||||
"description": "Design and test Playwright e2e tests",
|
||||
"tags": ["testing", "automation"],
|
||||
"beta": true
|
||||
}
|
||||
```
|
||||
|
||||
## Advanced: Page Schema System
|
||||
|
||||
For even more advanced use cases, check out:
|
||||
- `/src/config/page-schema.ts` - TypeScript types for page schemas
|
||||
- `/src/components/orchestration/PageRenderer.tsx` - Generic page renderer
|
||||
- `/src/config/default-pages.json` - Alternative page configuration format
|
||||
|
||||
These files provide a more sophisticated schema-based system that can define:
|
||||
- Complex layouts (split, tabs, grid)
|
||||
- Component hierarchies
|
||||
- Action handlers
|
||||
- Context management
|
||||
|
||||
## Migration Guide
|
||||
|
||||
If you have an existing page that's hardcoded in App.tsx:
|
||||
|
||||
### Before (Hardcoded):
|
||||
```typescript
|
||||
<TabsContent value="my-page" className="h-full m-0">
|
||||
<Suspense fallback={<LoadingFallback message="Loading..." />}>
|
||||
<MyComponent prop1={data} prop2={handler} />
|
||||
</Suspense>
|
||||
</TabsContent>
|
||||
```
|
||||
|
||||
### After (Declarative):
|
||||
1. Add to `pages.json`:
|
||||
```json
|
||||
{
|
||||
"id": "my-page",
|
||||
"title": "My Page",
|
||||
"icon": "Star",
|
||||
"component": "MyComponent",
|
||||
"enabled": true,
|
||||
"order": 10
|
||||
}
|
||||
```
|
||||
|
||||
2. Add to `componentMap`:
|
||||
```typescript
|
||||
MyComponent: lazy(() => import('@/components/MyComponent').then(m => ({ default: m.MyComponent }))),
|
||||
```
|
||||
|
||||
3. Add props mapping if needed:
|
||||
```typescript
|
||||
'MyComponent': {
|
||||
prop1: data,
|
||||
prop2: handler,
|
||||
}
|
||||
```
|
||||
|
||||
4. Remove the hardcoded TabsContent - the system handles it automatically!
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Page doesn't appear in navigation
|
||||
- ✅ Check `enabled: true` in pages.json
|
||||
- ✅ Check feature toggle is enabled (if using `toggleKey`)
|
||||
- ✅ Verify component exists in `componentMap`
|
||||
- ✅ Check console for errors
|
||||
|
||||
### Component not loading
|
||||
- ✅ Verify import path in `componentMap`
|
||||
- ✅ Check component has a default export
|
||||
- ✅ Look for TypeScript errors in component file
|
||||
|
||||
### Keyboard shortcut not working
|
||||
- ✅ Verify shortcut format (e.g., "ctrl+1")
|
||||
- ✅ Check for conflicts with browser shortcuts
|
||||
- ✅ Make sure page is enabled
|
||||
|
||||
### Props not being passed
|
||||
- ✅ Add mapping in `getPropsForComponent`
|
||||
- ✅ Verify component name matches `pages.json`
|
||||
- ✅ Check prop types match component interface
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Keep pages.json organized** - Group related pages together, use consistent ordering
|
||||
2. **Use meaningful IDs** - Use kebab-case, descriptive IDs (e.g., "code-editor", not "ce")
|
||||
3. **Choose appropriate icons** - Use Phosphor icons that match the page purpose
|
||||
4. **Document feature toggles** - Add comments to FeatureToggles type
|
||||
5. **Test shortcuts** - Verify shortcuts don't conflict with browser/OS shortcuts
|
||||
6. **Lazy load everything** - Keep components in the lazy componentMap
|
||||
7. **Type your props** - Use TypeScript interfaces for component props
|
||||
8. **Keep components small** - Follow the <150 LOC guideline from refactoring
|
||||
|
||||
## Summary
|
||||
|
||||
The declarative system transforms CodeForge from a monolithic React app into a flexible, configuration-driven platform. By moving page definitions to JSON, we gain:
|
||||
|
||||
- 🚀 **Faster development** - Add pages in minutes, not hours
|
||||
- 🔧 **Easier maintenance** - Configuration is centralized and versioned
|
||||
- 📦 **Better performance** - Lazy loading reduces initial bundle size
|
||||
- 🎯 **Cleaner code** - Business logic separated from configuration
|
||||
- 🧪 **Simpler testing** - Mock configuration instead of mocking components
|
||||
|
||||
The system is designed to grow with your needs - start simple with basic page definitions, then add advanced features like layout configuration, permissions, and dynamic props as needed.
|
||||
265
EXAMPLE_NEW_PAGE.md
Normal file
265
EXAMPLE_NEW_PAGE.md
Normal file
@@ -0,0 +1,265 @@
|
||||
# Example: Adding a New "API Tester" Page
|
||||
|
||||
This example demonstrates the complete process of adding a new page to CodeForge using the declarative system.
|
||||
|
||||
## Goal
|
||||
|
||||
Add an "API Tester" page that allows users to test REST API endpoints.
|
||||
|
||||
## Step 1: Create the Component
|
||||
|
||||
Create `src/components/ApiTester.tsx`:
|
||||
|
||||
```typescript
|
||||
import { useState } from 'react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
|
||||
export function ApiTester() {
|
||||
const [method, setMethod] = useState('GET')
|
||||
const [url, setUrl] = useState('')
|
||||
const [response, setResponse] = useState('')
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
const handleTest = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const res = await fetch(url, { method })
|
||||
const data = await res.json()
|
||||
setResponse(JSON.stringify(data, null, 2))
|
||||
} catch (error) {
|
||||
setResponse(`Error: ${error}`)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col bg-background">
|
||||
<div className="flex-1 overflow-auto p-6">
|
||||
<div className="max-w-6xl mx-auto space-y-6">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold">API Tester</h1>
|
||||
<p className="text-muted-foreground mt-2">
|
||||
Test REST API endpoints and inspect responses
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Request</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="flex gap-2">
|
||||
<Select value={method} onValueChange={setMethod}>
|
||||
<SelectTrigger className="w-32">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="GET">GET</SelectItem>
|
||||
<SelectItem value="POST">POST</SelectItem>
|
||||
<SelectItem value="PUT">PUT</SelectItem>
|
||||
<SelectItem value="DELETE">DELETE</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Input
|
||||
placeholder="https://api.example.com/endpoint"
|
||||
value={url}
|
||||
onChange={(e) => setUrl(e.target.value)}
|
||||
className="flex-1"
|
||||
/>
|
||||
<Button onClick={handleTest} disabled={!url || loading}>
|
||||
{loading ? 'Testing...' : 'Test'}
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{response && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Response</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Textarea
|
||||
value={response}
|
||||
readOnly
|
||||
className="font-mono text-sm h-96"
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Step 2: Register in Component Map
|
||||
|
||||
Add to `src/App.tsx` in the `componentMap`:
|
||||
|
||||
```typescript
|
||||
const componentMap: Record<string, React.LazyExoticComponent<any>> = {
|
||||
// ... existing components
|
||||
ApiTester: lazy(() => import('@/components/ApiTester').then(m => ({ default: m.ApiTester }))),
|
||||
}
|
||||
```
|
||||
|
||||
## Step 3: Add to pages.json
|
||||
|
||||
Add to `src/config/pages.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "api-tester",
|
||||
"title": "API Tester",
|
||||
"icon": "Cloud",
|
||||
"component": "ApiTester",
|
||||
"enabled": true,
|
||||
"toggleKey": "apiTester",
|
||||
"shortcut": "ctrl+shift+a",
|
||||
"order": 21
|
||||
}
|
||||
```
|
||||
|
||||
## Step 4: Add Feature Toggle (Optional)
|
||||
|
||||
If you want the page to be toggleable, add to `src/types/project.ts`:
|
||||
|
||||
```typescript
|
||||
export interface FeatureToggles {
|
||||
// ... existing toggles
|
||||
apiTester: boolean
|
||||
}
|
||||
```
|
||||
|
||||
And add the default in `src/hooks/use-project-state.ts`:
|
||||
|
||||
```typescript
|
||||
const DEFAULT_FEATURE_TOGGLES: FeatureToggles = {
|
||||
// ... existing toggles
|
||||
apiTester: true,
|
||||
}
|
||||
```
|
||||
|
||||
## Step 5: Test It!
|
||||
|
||||
1. Start the dev server: `npm run dev`
|
||||
2. Navigate to the new page by:
|
||||
- Clicking "API Tester" in the navigation menu
|
||||
- Pressing `Ctrl+Shift+A`
|
||||
- Searching for "API Tester" in global search (`Ctrl+K`)
|
||||
|
||||
## Result
|
||||
|
||||
✅ **New page is fully integrated:**
|
||||
- Appears in navigation menu with Cloud icon
|
||||
- Accessible via keyboard shortcut (Ctrl+Shift+A)
|
||||
- Can be toggled on/off in Features page
|
||||
- Searchable in global search
|
||||
- Follows the same layout pattern as other pages
|
||||
- Lazy-loaded for optimal performance
|
||||
|
||||
## Benefits of Declarative Approach
|
||||
|
||||
**Traditional Approach (Before):**
|
||||
```typescript
|
||||
// Would require:
|
||||
// - 20+ lines of JSX in App.tsx
|
||||
// - Manual TabsContent component
|
||||
// - Hardcoded shortcut handling
|
||||
// - Manual feature toggle check
|
||||
// - Props wiring
|
||||
```
|
||||
|
||||
**Declarative Approach (After):**
|
||||
```json
|
||||
// Just 8 lines of JSON!
|
||||
{
|
||||
"id": "api-tester",
|
||||
"title": "API Tester",
|
||||
"icon": "Cloud",
|
||||
"component": "ApiTester",
|
||||
"enabled": true,
|
||||
"toggleKey": "apiTester",
|
||||
"shortcut": "ctrl+shift+a",
|
||||
"order": 21
|
||||
}
|
||||
```
|
||||
|
||||
## Advanced: With Props
|
||||
|
||||
If your component needs props from the app state, add to `getPropsForComponent` in `App.tsx`:
|
||||
|
||||
```typescript
|
||||
const getPropsForComponent = (pageId: string) => {
|
||||
const propsMap: Record<string, any> = {
|
||||
// ... existing mappings
|
||||
'ApiTester': {
|
||||
savedRequests: apiRequests,
|
||||
onSaveRequest: saveApiRequest,
|
||||
onDeleteRequest: deleteApiRequest,
|
||||
},
|
||||
}
|
||||
return propsMap[pageId] || {}
|
||||
}
|
||||
```
|
||||
|
||||
Then update your component to accept these props:
|
||||
|
||||
```typescript
|
||||
interface ApiTesterProps {
|
||||
savedRequests?: ApiRequest[]
|
||||
onSaveRequest?: (request: ApiRequest) => void
|
||||
onDeleteRequest?: (id: string) => void
|
||||
}
|
||||
|
||||
export function ApiTester({
|
||||
savedRequests = [],
|
||||
onSaveRequest,
|
||||
onDeleteRequest
|
||||
}: ApiTesterProps) {
|
||||
// Use the props
|
||||
}
|
||||
```
|
||||
|
||||
## Using the Helper Scripts
|
||||
|
||||
Generate boilerplate code automatically:
|
||||
|
||||
```bash
|
||||
# Generate all boilerplate
|
||||
npm run pages:generate ApiTester "API Tester" "Cloud" "apiTester" "ctrl+shift+a"
|
||||
|
||||
# List all pages
|
||||
npm run pages:list
|
||||
|
||||
# Validate configuration
|
||||
npm run pages:validate
|
||||
```
|
||||
|
||||
## Summary
|
||||
|
||||
With the declarative system, adding a new page requires:
|
||||
|
||||
1. ✅ Create component (1 file)
|
||||
2. ✅ Add to componentMap (1 line)
|
||||
3. ✅ Add to pages.json (8 lines)
|
||||
4. ✅ Optional: Add feature toggle (2 lines in 2 files)
|
||||
5. ✅ Optional: Add props mapping (3 lines)
|
||||
|
||||
**Total: ~15 lines of code vs. 50+ lines in the traditional approach!**
|
||||
|
||||
The system handles:
|
||||
- ✅ Navigation menu rendering
|
||||
- ✅ Keyboard shortcuts
|
||||
- ✅ Feature toggles
|
||||
- ✅ Lazy loading
|
||||
- ✅ Search integration
|
||||
- ✅ Consistent layouts
|
||||
- ✅ Props injection
|
||||
18
README.md
18
README.md
@@ -11,9 +11,12 @@ A comprehensive visual low-code platform for generating production-ready Next.js
|
||||
|
||||
## ✨ Features
|
||||
|
||||
### 🏗️ Architecture (Phase 4: Refactored ✨)
|
||||
### 🏗️ Architecture (Phase 4: Declarative System ✨)
|
||||
- **Declarative JSON-Driven Pages** - Add new pages by editing a JSON file, no code changes needed
|
||||
- **Dynamic Component Loading** - All pages are lazy-loaded based on configuration for optimal performance
|
||||
- **Automatic Keyboard Shortcuts** - Shortcuts defined in JSON and automatically wired up
|
||||
- **Feature Toggle Integration** - Pages show/hide based on feature flags without conditional rendering
|
||||
- **Comprehensive Hook Library** - 12+ custom hooks for data, UI, and form management (all <150 LOC)
|
||||
- **JSON Orchestration Engine** - Build entire pages using JSON schemas without writing React code
|
||||
- **Atomic Component Library** - All components under 150 LOC for maximum maintainability
|
||||
- **Type-Safe Everything** - Full TypeScript + Zod validation for hooks, components, and JSON schemas
|
||||
- **Centralized Configuration** - Navigation, pages, and features configured via JSON
|
||||
@@ -194,10 +197,17 @@ Build entire pages using JSON schemas without writing React code:
|
||||
- ✅ **Easy testing** - Small, focused units
|
||||
- ✅ **Rapid prototyping** - Create pages by editing JSON
|
||||
|
||||
## 🏗️ Atomic Component Architecture
|
||||
## 🏗️ Architecture Documentation
|
||||
|
||||
CodeForge also uses **Atomic Design** methodology for legacy components:
|
||||
CodeForge uses modern patterns for maintainability and extensibility:
|
||||
|
||||
### Declarative System (Primary)
|
||||
- **[DECLARATIVE_SYSTEM.md](./DECLARATIVE_SYSTEM.md)** - **⭐ START HERE** Complete guide to the JSON-driven architecture
|
||||
- Learn how to add pages by editing JSON instead of writing React code
|
||||
- Understand the component registry, keyboard shortcuts, and feature toggles
|
||||
- Includes migration guide and best practices
|
||||
|
||||
### Atomic Component Architecture (Legacy)
|
||||
- **[ATOMIC_README.md](./ATOMIC_README.md)** - Quick start guide
|
||||
- **[ATOMIC_REFACTOR_SUMMARY.md](./ATOMIC_REFACTOR_SUMMARY.md)** - Overview of the atomic structure
|
||||
- **[ATOMIC_COMPONENTS.md](./ATOMIC_COMPONENTS.md)** - Complete architecture guide
|
||||
|
||||
@@ -15,7 +15,10 @@
|
||||
"test:e2e:headed": "playwright test --headed",
|
||||
"test:e2e:smoke": "playwright test smoke.spec.ts",
|
||||
"test:e2e:debug": "playwright test --debug",
|
||||
"test:e2e:report": "playwright show-report"
|
||||
"test:e2e:report": "playwright show-report",
|
||||
"pages:list": "node scripts/list-pages.js",
|
||||
"pages:validate": "tsx src/config/validate-config.ts",
|
||||
"pages:generate": "node scripts/generate-page.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@github/spark": ">=0.43.1 <1",
|
||||
|
||||
135
scripts/generate-page.js
Normal file
135
scripts/generate-page.js
Normal file
@@ -0,0 +1,135 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Page Generator Script
|
||||
*
|
||||
* Generates boilerplate code for adding a new page to CodeForge.
|
||||
*
|
||||
* Usage:
|
||||
* node scripts/generate-page.js MyNewDesigner "My New Designer" "Sparkle"
|
||||
*
|
||||
* This will create:
|
||||
* - Component file
|
||||
* - JSON configuration snippet
|
||||
* - Props mapping snippet
|
||||
* - ComponentMap entry snippet
|
||||
*/
|
||||
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
|
||||
const args = process.argv.slice(2)
|
||||
|
||||
if (args.length < 3) {
|
||||
console.error('Usage: node scripts/generate-page.js <ComponentName> <Title> <Icon> [toggleKey] [shortcut]')
|
||||
console.error('Example: node scripts/generate-page.js MyNewDesigner "My New Designer" "Sparkle" "myNewFeature" "ctrl+shift+n"')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const [componentName, title, icon, toggleKey, shortcut] = args
|
||||
|
||||
const kebabCase = (str) => str.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase()
|
||||
const pageId = kebabCase(componentName)
|
||||
|
||||
const componentTemplate = `export function ${componentName}() {
|
||||
return (
|
||||
<div className="h-full flex flex-col bg-background">
|
||||
<div className="flex-1 overflow-auto p-6">
|
||||
<div className="max-w-6xl mx-auto space-y-6">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold">${title}</h1>
|
||||
<p className="text-muted-foreground mt-2">
|
||||
Add your description here
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="border border-border rounded-lg p-6">
|
||||
<p className="text-center text-muted-foreground">
|
||||
Start building your ${title.toLowerCase()} here
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
`
|
||||
|
||||
const nextOrder = 21
|
||||
|
||||
const pageConfigSnippet = `{
|
||||
"id": "${pageId}",
|
||||
"title": "${title}",
|
||||
"icon": "${icon}",
|
||||
"component": "${componentName}",
|
||||
"enabled": true,${toggleKey ? `\n "toggleKey": "${toggleKey}",` : ''}${shortcut ? `\n "shortcut": "${shortcut}",` : ''}
|
||||
"order": ${nextOrder}
|
||||
}`
|
||||
|
||||
const componentMapSnippet = ` ${componentName}: lazy(() => import('@/components/${componentName}').then(m => ({ default: m.${componentName} }))),`
|
||||
|
||||
const propsMapSnippet = ` '${componentName}': {
|
||||
// Add your props here
|
||||
},`
|
||||
|
||||
const featureToggleSnippet = toggleKey ? ` ${toggleKey}: boolean` : null
|
||||
|
||||
console.log('\n🎨 CodeForge Page Generator\n')
|
||||
console.log('═══════════════════════════════════════\n')
|
||||
|
||||
console.log('📁 Component file will be created at:')
|
||||
console.log(` src/components/${componentName}.tsx\n`)
|
||||
|
||||
console.log('📝 Component code:')
|
||||
console.log('───────────────────────────────────────')
|
||||
console.log(componentTemplate)
|
||||
console.log('───────────────────────────────────────\n')
|
||||
|
||||
console.log('⚙️ Add this to src/config/pages.json:')
|
||||
console.log('───────────────────────────────────────')
|
||||
console.log(pageConfigSnippet)
|
||||
console.log('───────────────────────────────────────\n')
|
||||
|
||||
console.log('🗺️ Add this to componentMap in src/App.tsx:')
|
||||
console.log('───────────────────────────────────────')
|
||||
console.log(componentMapSnippet)
|
||||
console.log('───────────────────────────────────────\n')
|
||||
|
||||
console.log('🔧 Add this to getPropsForComponent in src/App.tsx:')
|
||||
console.log('───────────────────────────────────────')
|
||||
console.log(propsMapSnippet)
|
||||
console.log('───────────────────────────────────────\n')
|
||||
|
||||
if (featureToggleSnippet) {
|
||||
console.log('🎚️ Add this to FeatureToggles in src/types/project.ts:')
|
||||
console.log('───────────────────────────────────────')
|
||||
console.log(featureToggleSnippet)
|
||||
console.log('───────────────────────────────────────\n')
|
||||
}
|
||||
|
||||
console.log('✅ Next Steps:')
|
||||
console.log(' 1. Create the component file')
|
||||
console.log(' 2. Add configuration to pages.json')
|
||||
console.log(' 3. Add component to componentMap')
|
||||
console.log(' 4. (Optional) Add props mapping')
|
||||
if (featureToggleSnippet) {
|
||||
console.log(' 5. Add feature toggle type and default value')
|
||||
}
|
||||
console.log('\n')
|
||||
|
||||
const componentPath = path.join(process.cwd(), 'src', 'components', `${componentName}.tsx`)
|
||||
|
||||
if (fs.existsSync(componentPath)) {
|
||||
console.log(`⚠️ Warning: ${componentPath} already exists. Skipping file creation.`)
|
||||
} else {
|
||||
const createFile = process.argv.includes('--create')
|
||||
|
||||
if (createFile) {
|
||||
fs.writeFileSync(componentPath, componentTemplate, 'utf8')
|
||||
console.log(`✅ Created ${componentPath}`)
|
||||
} else {
|
||||
console.log('💡 Run with --create flag to automatically create the component file')
|
||||
}
|
||||
}
|
||||
|
||||
console.log('\n')
|
||||
87
scripts/list-pages.js
Normal file
87
scripts/list-pages.js
Normal file
@@ -0,0 +1,87 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Page List Script
|
||||
*
|
||||
* Lists all pages defined in pages.json with their configuration.
|
||||
*
|
||||
* Usage:
|
||||
* node scripts/list-pages.js [--format=table|json]
|
||||
*/
|
||||
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
|
||||
const pagesJsonPath = path.join(process.cwd(), 'src', 'config', 'pages.json')
|
||||
|
||||
if (!fs.existsSync(pagesJsonPath)) {
|
||||
console.error('❌ Could not find src/config/pages.json')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const pagesConfig = JSON.parse(fs.readFileSync(pagesJsonPath, 'utf8'))
|
||||
const format = process.argv.find(arg => arg.startsWith('--format='))?.split('=')[1] || 'table'
|
||||
|
||||
if (format === 'json') {
|
||||
console.log(JSON.stringify(pagesConfig.pages, null, 2))
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
console.log('\n📋 CodeForge Pages Configuration\n')
|
||||
console.log('═══════════════════════════════════════════════════════════════════════════\n')
|
||||
|
||||
const sortedPages = [...pagesConfig.pages].sort((a, b) => a.order - b.order)
|
||||
|
||||
sortedPages.forEach((page, index) => {
|
||||
const enabled = page.enabled ? '✅' : '❌'
|
||||
const hasToggle = page.toggleKey ? `🎚️ ${page.toggleKey}` : '➖'
|
||||
const hasShortcut = page.shortcut ? `⌨️ ${page.shortcut}` : '➖'
|
||||
|
||||
console.log(`${String(index + 1).padStart(2, '0')}. ${page.title}`)
|
||||
console.log(` ID: ${page.id}`)
|
||||
console.log(` Component: ${page.component}`)
|
||||
console.log(` Icon: ${page.icon}`)
|
||||
console.log(` Enabled: ${enabled}`)
|
||||
console.log(` Toggle: ${hasToggle}`)
|
||||
console.log(` Shortcut: ${hasShortcut}`)
|
||||
console.log(` Order: ${page.order}`)
|
||||
if (page.requiresResizable) {
|
||||
console.log(` Layout: Resizable Split-Pane`)
|
||||
}
|
||||
console.log('')
|
||||
})
|
||||
|
||||
console.log('═══════════════════════════════════════════════════════════════════════════')
|
||||
console.log(`\nTotal Pages: ${pagesConfig.pages.length}`)
|
||||
console.log(`Enabled: ${pagesConfig.pages.filter(p => p.enabled).length}`)
|
||||
console.log(`With Shortcuts: ${pagesConfig.pages.filter(p => p.shortcut).length}`)
|
||||
console.log(`With Feature Toggles: ${pagesConfig.pages.filter(p => p.toggleKey).length}`)
|
||||
console.log('')
|
||||
|
||||
const shortcuts = sortedPages
|
||||
.filter(p => p.shortcut && p.enabled)
|
||||
.map(p => ` ${p.shortcut.padEnd(12)} → ${p.title}`)
|
||||
|
||||
if (shortcuts.length > 0) {
|
||||
console.log('\n⌨️ Keyboard Shortcuts\n')
|
||||
console.log('───────────────────────────────────────────────────────────────────────────')
|
||||
shortcuts.forEach(s => console.log(s))
|
||||
console.log('')
|
||||
}
|
||||
|
||||
const featureToggles = sortedPages
|
||||
.filter(p => p.toggleKey && p.enabled)
|
||||
.map(p => ` ${p.toggleKey.padEnd(20)} → ${p.title}`)
|
||||
|
||||
if (featureToggles.length > 0) {
|
||||
console.log('\n🎚️ Feature Toggles\n')
|
||||
console.log('───────────────────────────────────────────────────────────────────────────')
|
||||
featureToggles.forEach(t => console.log(t))
|
||||
console.log('')
|
||||
}
|
||||
|
||||
console.log('\n💡 Tips:')
|
||||
console.log(' • Edit src/config/pages.json to add/modify pages')
|
||||
console.log(' • Run with --format=json for JSON output')
|
||||
console.log(' • See DECLARATIVE_SYSTEM.md for full documentation')
|
||||
console.log('')
|
||||
371
src/App.tsx
371
src/App.tsx
@@ -1,4 +1,4 @@
|
||||
import { useState, lazy, Suspense } from 'react'
|
||||
import { useState, lazy, Suspense, useMemo } from 'react'
|
||||
import { Tabs, TabsContent } from '@/components/ui/tabs'
|
||||
import { AppHeader, PageHeader } from '@/components/organisms'
|
||||
import { LoadingFallback } from '@/components/molecules'
|
||||
@@ -6,29 +6,33 @@ import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from '@/componen
|
||||
import { useProjectState } from '@/hooks/use-project-state'
|
||||
import { useFileOperations } from '@/hooks/use-file-operations'
|
||||
import { useKeyboardShortcuts } from '@/hooks/use-keyboard-shortcuts'
|
||||
import { getPageConfig, getEnabledPages, getPageShortcuts } from '@/config/page-loader'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
const ProjectDashboard = lazy(() => import('@/components/ProjectDashboard').then(m => ({ default: m.ProjectDashboard })))
|
||||
const CodeEditor = lazy(() => import('@/components/CodeEditor').then(m => ({ default: m.CodeEditor })))
|
||||
const FileExplorer = lazy(() => import('@/components/FileExplorer').then(m => ({ default: m.FileExplorer })))
|
||||
const ModelDesigner = lazy(() => import('@/components/ModelDesigner').then(m => ({ default: m.ModelDesigner })))
|
||||
const ComponentTreeBuilder = lazy(() => import('@/components/ComponentTreeBuilder').then(m => ({ default: m.ComponentTreeBuilder })))
|
||||
const ComponentTreeManager = lazy(() => import('@/components/ComponentTreeManager').then(m => ({ default: m.ComponentTreeManager })))
|
||||
const WorkflowDesigner = lazy(() => import('@/components/WorkflowDesigner').then(m => ({ default: m.WorkflowDesigner })))
|
||||
const LambdaDesigner = lazy(() => import('@/components/LambdaDesigner').then(m => ({ default: m.LambdaDesigner })))
|
||||
const StyleDesigner = lazy(() => import('@/components/StyleDesigner').then(m => ({ default: m.StyleDesigner })))
|
||||
const PlaywrightDesigner = lazy(() => import('@/components/PlaywrightDesigner').then(m => ({ default: m.PlaywrightDesigner })))
|
||||
const StorybookDesigner = lazy(() => import('@/components/StorybookDesigner').then(m => ({ default: m.StorybookDesigner })))
|
||||
const UnitTestDesigner = lazy(() => import('@/components/UnitTestDesigner').then(m => ({ default: m.UnitTestDesigner })))
|
||||
const FlaskDesigner = lazy(() => import('@/components/FlaskDesigner').then(m => ({ default: m.FlaskDesigner })))
|
||||
const ProjectSettingsDesigner = lazy(() => import('@/components/ProjectSettingsDesigner').then(m => ({ default: m.ProjectSettingsDesigner })))
|
||||
const ErrorPanel = lazy(() => import('@/components/ErrorPanel').then(m => ({ default: m.ErrorPanel })))
|
||||
const DocumentationView = lazy(() => import('@/components/DocumentationView').then(m => ({ default: m.DocumentationView })))
|
||||
const SassStylesShowcase = lazy(() => import('@/components/SassStylesShowcase').then(m => ({ default: m.SassStylesShowcase })))
|
||||
const FeatureToggleSettings = lazy(() => import('@/components/FeatureToggleSettings').then(m => ({ default: m.FeatureToggleSettings })))
|
||||
const PWASettings = lazy(() => import('@/components/PWASettings').then(m => ({ default: m.PWASettings })))
|
||||
const FaviconDesigner = lazy(() => import('@/components/FaviconDesigner').then(m => ({ default: m.FaviconDesigner })))
|
||||
const FeatureIdeaCloud = lazy(() => import('@/components/FeatureIdeaCloud').then(m => ({ default: m.FeatureIdeaCloud })))
|
||||
const componentMap: Record<string, React.LazyExoticComponent<any>> = {
|
||||
ProjectDashboard: lazy(() => import('@/components/ProjectDashboard').then(m => ({ default: m.ProjectDashboard }))),
|
||||
CodeEditor: lazy(() => import('@/components/CodeEditor').then(m => ({ default: m.CodeEditor }))),
|
||||
FileExplorer: lazy(() => import('@/components/FileExplorer').then(m => ({ default: m.FileExplorer }))),
|
||||
ModelDesigner: lazy(() => import('@/components/ModelDesigner').then(m => ({ default: m.ModelDesigner }))),
|
||||
ComponentTreeBuilder: lazy(() => import('@/components/ComponentTreeBuilder').then(m => ({ default: m.ComponentTreeBuilder }))),
|
||||
ComponentTreeManager: lazy(() => import('@/components/ComponentTreeManager').then(m => ({ default: m.ComponentTreeManager }))),
|
||||
WorkflowDesigner: lazy(() => import('@/components/WorkflowDesigner').then(m => ({ default: m.WorkflowDesigner }))),
|
||||
LambdaDesigner: lazy(() => import('@/components/LambdaDesigner').then(m => ({ default: m.LambdaDesigner }))),
|
||||
StyleDesigner: lazy(() => import('@/components/StyleDesigner').then(m => ({ default: m.StyleDesigner }))),
|
||||
PlaywrightDesigner: lazy(() => import('@/components/PlaywrightDesigner').then(m => ({ default: m.PlaywrightDesigner }))),
|
||||
StorybookDesigner: lazy(() => import('@/components/StorybookDesigner').then(m => ({ default: m.StorybookDesigner }))),
|
||||
UnitTestDesigner: lazy(() => import('@/components/UnitTestDesigner').then(m => ({ default: m.UnitTestDesigner }))),
|
||||
FlaskDesigner: lazy(() => import('@/components/FlaskDesigner').then(m => ({ default: m.FlaskDesigner }))),
|
||||
ProjectSettingsDesigner: lazy(() => import('@/components/ProjectSettingsDesigner').then(m => ({ default: m.ProjectSettingsDesigner }))),
|
||||
ErrorPanel: lazy(() => import('@/components/ErrorPanel').then(m => ({ default: m.ErrorPanel }))),
|
||||
DocumentationView: lazy(() => import('@/components/DocumentationView').then(m => ({ default: m.DocumentationView }))),
|
||||
SassStylesShowcase: lazy(() => import('@/components/SassStylesShowcase').then(m => ({ default: m.SassStylesShowcase }))),
|
||||
FeatureToggleSettings: lazy(() => import('@/components/FeatureToggleSettings').then(m => ({ default: m.FeatureToggleSettings }))),
|
||||
PWASettings: lazy(() => import('@/components/PWASettings').then(m => ({ default: m.PWASettings }))),
|
||||
FaviconDesigner: lazy(() => import('@/components/FaviconDesigner').then(m => ({ default: m.FaviconDesigner }))),
|
||||
FeatureIdeaCloud: lazy(() => import('@/components/FeatureIdeaCloud').then(m => ({ default: m.FeatureIdeaCloud }))),
|
||||
}
|
||||
|
||||
const GlobalSearch = lazy(() => import('@/components/GlobalSearch').then(m => ({ default: m.GlobalSearch })))
|
||||
const KeyboardShortcutsDialog = lazy(() => import('@/components/KeyboardShortcutsDialog').then(m => ({ default: m.KeyboardShortcutsDialog })))
|
||||
const PWAInstallPrompt = lazy(() => import('@/components/PWAInstallPrompt').then(m => ({ default: m.PWAInstallPrompt })))
|
||||
@@ -77,9 +81,18 @@ function App() {
|
||||
const [lastSaved] = useState<number | null>(Date.now())
|
||||
const [errorCount] = useState(0)
|
||||
|
||||
const pageConfig = useMemo(() => getPageConfig(), [])
|
||||
const enabledPages = useMemo(() => getEnabledPages(featureToggles), [featureToggles])
|
||||
const shortcuts = useMemo(() => getPageShortcuts(featureToggles), [featureToggles])
|
||||
|
||||
useKeyboardShortcuts([
|
||||
{ key: '1', ctrl: true, description: 'Dashboard', action: () => setActiveTab('dashboard') },
|
||||
{ key: '2', ctrl: true, description: 'Code', action: () => setActiveTab('code') },
|
||||
...shortcuts.map(s => ({
|
||||
key: s.key,
|
||||
ctrl: s.ctrl,
|
||||
shift: s.shift,
|
||||
description: s.description,
|
||||
action: () => setActiveTab(s.action)
|
||||
})),
|
||||
{ key: 'k', ctrl: true, description: 'Search', action: () => setSearchOpen(true) },
|
||||
{ key: '/', ctrl: true, description: 'Shortcuts', action: () => setShortcutsOpen(true) },
|
||||
])
|
||||
@@ -120,6 +133,129 @@ function App() {
|
||||
toast.success('Project loaded')
|
||||
}
|
||||
|
||||
const getPropsForComponent = (pageId: string) => {
|
||||
const propsMap: Record<string, any> = {
|
||||
'ProjectDashboard': {
|
||||
files,
|
||||
models,
|
||||
components,
|
||||
theme,
|
||||
playwrightTests,
|
||||
storybookStories,
|
||||
unitTests,
|
||||
flaskConfig,
|
||||
},
|
||||
'CodeEditor': {
|
||||
files,
|
||||
activeFileId,
|
||||
onFileChange: handleFileChange,
|
||||
onFileSelect: setActiveFileId,
|
||||
onFileClose: handleFileClose,
|
||||
},
|
||||
'FileExplorer': {
|
||||
files,
|
||||
activeFileId,
|
||||
onFileSelect: setActiveFileId,
|
||||
onFileAdd: handleFileAdd,
|
||||
},
|
||||
'ModelDesigner': {
|
||||
models,
|
||||
onModelsChange: setModels,
|
||||
},
|
||||
'ComponentTreeBuilder': {
|
||||
components,
|
||||
onComponentsChange: setComponents,
|
||||
},
|
||||
'ComponentTreeManager': {
|
||||
trees: componentTrees,
|
||||
onTreesChange: setComponentTrees,
|
||||
},
|
||||
'WorkflowDesigner': {
|
||||
workflows,
|
||||
onWorkflowsChange: setWorkflows,
|
||||
},
|
||||
'LambdaDesigner': {
|
||||
lambdas,
|
||||
onLambdasChange: setLambdas,
|
||||
},
|
||||
'StyleDesigner': {
|
||||
theme,
|
||||
onThemeChange: setTheme,
|
||||
},
|
||||
'FlaskDesigner': {
|
||||
config: flaskConfig,
|
||||
onConfigChange: setFlaskConfig,
|
||||
},
|
||||
'PlaywrightDesigner': {
|
||||
tests: playwrightTests,
|
||||
onTestsChange: setPlaywrightTests,
|
||||
},
|
||||
'StorybookDesigner': {
|
||||
stories: storybookStories,
|
||||
onStoriesChange: setStorybookStories,
|
||||
},
|
||||
'UnitTestDesigner': {
|
||||
tests: unitTests,
|
||||
onTestsChange: setUnitTests,
|
||||
},
|
||||
'ErrorPanel': {
|
||||
files,
|
||||
onFileChange: handleFileChange,
|
||||
onFileSelect: setActiveFileId,
|
||||
},
|
||||
'ProjectSettingsDesigner': {
|
||||
nextjsConfig,
|
||||
npmSettings,
|
||||
onNextjsConfigChange: setNextjsConfig,
|
||||
onNpmSettingsChange: setNpmSettings,
|
||||
},
|
||||
'FeatureToggleSettings': {
|
||||
features: featureToggles,
|
||||
onFeaturesChange: setFeatureToggles,
|
||||
},
|
||||
'DocumentationView': {},
|
||||
'SassStylesShowcase': {},
|
||||
'PWASettings': {},
|
||||
'FaviconDesigner': {},
|
||||
'FeatureIdeaCloud': {},
|
||||
}
|
||||
return propsMap[pageId] || {}
|
||||
}
|
||||
|
||||
const renderPageContent = (page: any) => {
|
||||
const Component = componentMap[page.component]
|
||||
if (!Component) {
|
||||
return <LoadingFallback message={`Component ${page.component} not found`} />
|
||||
}
|
||||
|
||||
if (page.requiresResizable && page.id === 'code') {
|
||||
const FileExplorerComp = componentMap['FileExplorer']
|
||||
const CodeEditorComp = componentMap['CodeEditor']
|
||||
return (
|
||||
<ResizablePanelGroup direction="horizontal">
|
||||
<ResizablePanel defaultSize={20} minSize={15} maxSize={30}>
|
||||
<Suspense fallback={<LoadingFallback message="Loading explorer..." />}>
|
||||
<FileExplorerComp {...getPropsForComponent('FileExplorer')} />
|
||||
</Suspense>
|
||||
</ResizablePanel>
|
||||
<ResizableHandle />
|
||||
<ResizablePanel defaultSize={80}>
|
||||
<Suspense fallback={<LoadingFallback message="Loading editor..." />}>
|
||||
<CodeEditorComp {...getPropsForComponent('CodeEditor')} />
|
||||
</Suspense>
|
||||
</ResizablePanel>
|
||||
</ResizablePanelGroup>
|
||||
)
|
||||
}
|
||||
|
||||
const props = getPropsForComponent(page.component)
|
||||
return (
|
||||
<Suspense fallback={<LoadingFallback message={`Loading ${page.title.toLowerCase()}...`} />}>
|
||||
<Component {...props} />
|
||||
</Suspense>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h-screen flex flex-col bg-background">
|
||||
<Suspense fallback={<div className="h-1 bg-primary animate-pulse" />}>
|
||||
@@ -145,190 +281,11 @@ function App() {
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab} className="flex-1 flex flex-col">
|
||||
<PageHeader activeTab={activeTab} />
|
||||
<div className="flex-1 overflow-hidden">
|
||||
<TabsContent value="dashboard" className="h-full m-0">
|
||||
<Suspense fallback={<LoadingFallback message="Loading dashboard..." />}>
|
||||
<ProjectDashboard
|
||||
files={files}
|
||||
models={models}
|
||||
components={components}
|
||||
theme={theme}
|
||||
playwrightTests={playwrightTests}
|
||||
storybookStories={storybookStories}
|
||||
unitTests={unitTests}
|
||||
flaskConfig={flaskConfig}
|
||||
/>
|
||||
</Suspense>
|
||||
</TabsContent>
|
||||
|
||||
{featureToggles.codeEditor && (
|
||||
<TabsContent value="code" className="h-full m-0">
|
||||
<Suspense fallback={<LoadingFallback message="Loading editor..." />}>
|
||||
<ResizablePanelGroup direction="horizontal">
|
||||
<ResizablePanel defaultSize={20} minSize={15} maxSize={30}>
|
||||
<FileExplorer
|
||||
files={files}
|
||||
activeFileId={activeFileId}
|
||||
onFileSelect={setActiveFileId}
|
||||
onFileAdd={handleFileAdd}
|
||||
/>
|
||||
</ResizablePanel>
|
||||
<ResizableHandle />
|
||||
<ResizablePanel defaultSize={80}>
|
||||
<CodeEditor
|
||||
files={files}
|
||||
activeFileId={activeFileId}
|
||||
onFileChange={handleFileChange}
|
||||
onFileSelect={setActiveFileId}
|
||||
onFileClose={handleFileClose}
|
||||
/>
|
||||
</ResizablePanel>
|
||||
</ResizablePanelGroup>
|
||||
</Suspense>
|
||||
{enabledPages.map(page => (
|
||||
<TabsContent key={page.id} value={page.id} className="h-full m-0">
|
||||
{renderPageContent(page)}
|
||||
</TabsContent>
|
||||
)}
|
||||
|
||||
{featureToggles.models && (
|
||||
<TabsContent value="models" className="h-full m-0">
|
||||
<Suspense fallback={<LoadingFallback message="Loading models..." />}>
|
||||
<ModelDesigner models={models} onModelsChange={setModels} />
|
||||
</Suspense>
|
||||
</TabsContent>
|
||||
)}
|
||||
|
||||
{featureToggles.components && (
|
||||
<TabsContent value="components" className="h-full m-0">
|
||||
<Suspense fallback={<LoadingFallback message="Loading components..." />}>
|
||||
<ComponentTreeBuilder components={components} onComponentsChange={setComponents} />
|
||||
</Suspense>
|
||||
</TabsContent>
|
||||
)}
|
||||
|
||||
{featureToggles.componentTrees && (
|
||||
<TabsContent value="component-trees" className="h-full m-0">
|
||||
<Suspense fallback={<LoadingFallback message="Loading component trees..." />}>
|
||||
<ComponentTreeManager trees={componentTrees} onTreesChange={setComponentTrees} />
|
||||
</Suspense>
|
||||
</TabsContent>
|
||||
)}
|
||||
|
||||
{featureToggles.workflows && (
|
||||
<TabsContent value="workflows" className="h-full m-0">
|
||||
<Suspense fallback={<LoadingFallback message="Loading workflows..." />}>
|
||||
<WorkflowDesigner workflows={workflows} onWorkflowsChange={setWorkflows} />
|
||||
</Suspense>
|
||||
</TabsContent>
|
||||
)}
|
||||
|
||||
{featureToggles.lambdas && (
|
||||
<TabsContent value="lambdas" className="h-full m-0">
|
||||
<Suspense fallback={<LoadingFallback message="Loading lambdas..." />}>
|
||||
<LambdaDesigner lambdas={lambdas} onLambdasChange={setLambdas} />
|
||||
</Suspense>
|
||||
</TabsContent>
|
||||
)}
|
||||
|
||||
{featureToggles.styling && (
|
||||
<TabsContent value="styling" className="h-full m-0">
|
||||
<Suspense fallback={<LoadingFallback message="Loading style designer..." />}>
|
||||
<StyleDesigner theme={theme} onThemeChange={setTheme} />
|
||||
</Suspense>
|
||||
</TabsContent>
|
||||
)}
|
||||
|
||||
{featureToggles.flaskApi && (
|
||||
<TabsContent value="flask" className="h-full m-0">
|
||||
<Suspense fallback={<LoadingFallback message="Loading Flask designer..." />}>
|
||||
<FlaskDesigner config={flaskConfig} onConfigChange={setFlaskConfig} />
|
||||
</Suspense>
|
||||
</TabsContent>
|
||||
)}
|
||||
|
||||
<TabsContent value="settings" className="h-full m-0">
|
||||
<Suspense fallback={<LoadingFallback message="Loading settings..." />}>
|
||||
<ProjectSettingsDesigner
|
||||
nextjsConfig={nextjsConfig}
|
||||
npmSettings={npmSettings}
|
||||
onNextjsConfigChange={setNextjsConfig}
|
||||
onNpmSettingsChange={setNpmSettings}
|
||||
/>
|
||||
</Suspense>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="pwa" className="h-full m-0">
|
||||
<Suspense fallback={<LoadingFallback message="Loading PWA settings..." />}>
|
||||
<PWASettings />
|
||||
</Suspense>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="features" className="h-full m-0">
|
||||
<Suspense fallback={<LoadingFallback message="Loading feature toggles..." />}>
|
||||
<FeatureToggleSettings features={featureToggles} onFeaturesChange={setFeatureToggles} />
|
||||
</Suspense>
|
||||
</TabsContent>
|
||||
|
||||
{featureToggles.playwright && (
|
||||
<TabsContent value="playwright" className="h-full m-0">
|
||||
<Suspense fallback={<LoadingFallback message="Loading Playwright designer..." />}>
|
||||
<PlaywrightDesigner tests={playwrightTests} onTestsChange={setPlaywrightTests} />
|
||||
</Suspense>
|
||||
</TabsContent>
|
||||
)}
|
||||
|
||||
{featureToggles.storybook && (
|
||||
<TabsContent value="storybook" className="h-full m-0">
|
||||
<Suspense fallback={<LoadingFallback message="Loading Storybook designer..." />}>
|
||||
<StorybookDesigner stories={storybookStories} onStoriesChange={setStorybookStories} />
|
||||
</Suspense>
|
||||
</TabsContent>
|
||||
)}
|
||||
|
||||
{featureToggles.unitTests && (
|
||||
<TabsContent value="unit-tests" className="h-full m-0">
|
||||
<Suspense fallback={<LoadingFallback message="Loading unit test designer..." />}>
|
||||
<UnitTestDesigner tests={unitTests} onTestsChange={setUnitTests} />
|
||||
</Suspense>
|
||||
</TabsContent>
|
||||
)}
|
||||
|
||||
{featureToggles.errorRepair && (
|
||||
<TabsContent value="errors" className="h-full m-0">
|
||||
<Suspense fallback={<LoadingFallback message="Loading error panel..." />}>
|
||||
<ErrorPanel files={files} onFileChange={handleFileChange} onFileSelect={setActiveFileId} />
|
||||
</Suspense>
|
||||
</TabsContent>
|
||||
)}
|
||||
|
||||
{featureToggles.documentation && (
|
||||
<TabsContent value="docs" className="h-full m-0">
|
||||
<Suspense fallback={<LoadingFallback message="Loading documentation..." />}>
|
||||
<DocumentationView />
|
||||
</Suspense>
|
||||
</TabsContent>
|
||||
)}
|
||||
|
||||
{featureToggles.sassStyles && (
|
||||
<TabsContent value="sass" className="h-full m-0">
|
||||
<Suspense fallback={<LoadingFallback message="Loading Sass showcase..." />}>
|
||||
<SassStylesShowcase />
|
||||
</Suspense>
|
||||
</TabsContent>
|
||||
)}
|
||||
|
||||
{featureToggles.faviconDesigner && (
|
||||
<TabsContent value="favicon" className="h-full m-0">
|
||||
<Suspense fallback={<LoadingFallback message="Loading favicon designer..." />}>
|
||||
<FaviconDesigner />
|
||||
</Suspense>
|
||||
</TabsContent>
|
||||
)}
|
||||
|
||||
{featureToggles.ideaCloud && (
|
||||
<TabsContent value="ideas" className="h-full m-0">
|
||||
<Suspense fallback={<LoadingFallback message="Loading feature ideas..." />}>
|
||||
<FeatureIdeaCloud />
|
||||
</Suspense>
|
||||
</TabsContent>
|
||||
)}
|
||||
))}
|
||||
</div>
|
||||
</Tabs>
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import pagesConfig from './pages.json'
|
||||
import { FeatureToggles } from '@/types/project'
|
||||
|
||||
export interface PageConfig {
|
||||
id: string
|
||||
@@ -24,15 +25,15 @@ export function getPageById(id: string): PageConfig | undefined {
|
||||
return pagesConfig.pages.find(page => page.id === id)
|
||||
}
|
||||
|
||||
export function getEnabledPages(featureToggles?: Record<string, boolean>): PageConfig[] {
|
||||
export function getEnabledPages(featureToggles?: FeatureToggles): PageConfig[] {
|
||||
return pagesConfig.pages.filter(page => {
|
||||
if (!page.enabled) return false
|
||||
if (!page.toggleKey) return true
|
||||
return featureToggles?.[page.toggleKey] !== false
|
||||
return featureToggles?.[page.toggleKey as keyof FeatureToggles] !== false
|
||||
}).sort((a, b) => a.order - b.order)
|
||||
}
|
||||
|
||||
export function getPageShortcuts(featureToggles?: Record<string, boolean>): Array<{
|
||||
export function getPageShortcuts(featureToggles?: FeatureToggles): Array<{
|
||||
key: string
|
||||
ctrl?: boolean
|
||||
shift?: boolean
|
||||
|
||||
169
src/config/validate-config.ts
Normal file
169
src/config/validate-config.ts
Normal file
@@ -0,0 +1,169 @@
|
||||
import pagesConfig from './pages.json'
|
||||
import { PageConfig } from './page-loader'
|
||||
|
||||
export interface ValidationError {
|
||||
page: string
|
||||
field: string
|
||||
message: string
|
||||
severity: 'error' | 'warning'
|
||||
}
|
||||
|
||||
export function validatePageConfig(): ValidationError[] {
|
||||
const errors: ValidationError[] = []
|
||||
const seenIds = new Set<string>()
|
||||
const seenShortcuts = new Set<string>()
|
||||
const seenOrders = new Set<number>()
|
||||
|
||||
pagesConfig.pages.forEach((page: PageConfig) => {
|
||||
if (!page.id) {
|
||||
errors.push({
|
||||
page: page.title || 'Unknown',
|
||||
field: 'id',
|
||||
message: 'Page ID is required',
|
||||
severity: 'error',
|
||||
})
|
||||
} else if (seenIds.has(page.id)) {
|
||||
errors.push({
|
||||
page: page.id,
|
||||
field: 'id',
|
||||
message: `Duplicate page ID: ${page.id}`,
|
||||
severity: 'error',
|
||||
})
|
||||
} else {
|
||||
seenIds.add(page.id)
|
||||
}
|
||||
|
||||
if (!page.title) {
|
||||
errors.push({
|
||||
page: page.id || 'Unknown',
|
||||
field: 'title',
|
||||
message: 'Page title is required',
|
||||
severity: 'error',
|
||||
})
|
||||
}
|
||||
|
||||
if (!page.component) {
|
||||
errors.push({
|
||||
page: page.id || 'Unknown',
|
||||
field: 'component',
|
||||
message: 'Component name is required',
|
||||
severity: 'error',
|
||||
})
|
||||
}
|
||||
|
||||
if (!page.icon) {
|
||||
errors.push({
|
||||
page: page.id || 'Unknown',
|
||||
field: 'icon',
|
||||
message: 'Icon is required',
|
||||
severity: 'warning',
|
||||
})
|
||||
}
|
||||
|
||||
if (page.shortcut) {
|
||||
if (seenShortcuts.has(page.shortcut)) {
|
||||
errors.push({
|
||||
page: page.id || 'Unknown',
|
||||
field: 'shortcut',
|
||||
message: `Duplicate shortcut: ${page.shortcut}`,
|
||||
severity: 'warning',
|
||||
})
|
||||
} else {
|
||||
seenShortcuts.add(page.shortcut)
|
||||
}
|
||||
|
||||
const validShortcutPattern = /^(ctrl\+)?(shift\+)?(alt\+)?[a-z0-9]$/i
|
||||
if (!validShortcutPattern.test(page.shortcut)) {
|
||||
errors.push({
|
||||
page: page.id || 'Unknown',
|
||||
field: 'shortcut',
|
||||
message: `Invalid shortcut format: ${page.shortcut}. Use format like "ctrl+1" or "ctrl+shift+e"`,
|
||||
severity: 'error',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (page.order !== undefined) {
|
||||
if (seenOrders.has(page.order)) {
|
||||
errors.push({
|
||||
page: page.id || 'Unknown',
|
||||
field: 'order',
|
||||
message: `Duplicate order number: ${page.order}`,
|
||||
severity: 'warning',
|
||||
})
|
||||
} else {
|
||||
seenOrders.add(page.order)
|
||||
}
|
||||
}
|
||||
|
||||
if (page.toggleKey) {
|
||||
const validToggleKeys = [
|
||||
'codeEditor',
|
||||
'models',
|
||||
'components',
|
||||
'componentTrees',
|
||||
'workflows',
|
||||
'lambdas',
|
||||
'styling',
|
||||
'flaskApi',
|
||||
'playwright',
|
||||
'storybook',
|
||||
'unitTests',
|
||||
'errorRepair',
|
||||
'documentation',
|
||||
'sassStyles',
|
||||
'faviconDesigner',
|
||||
'ideaCloud',
|
||||
]
|
||||
|
||||
if (!validToggleKeys.includes(page.toggleKey)) {
|
||||
errors.push({
|
||||
page: page.id || 'Unknown',
|
||||
field: 'toggleKey',
|
||||
message: `Unknown toggle key: ${page.toggleKey}. Must match a key in FeatureToggles type.`,
|
||||
severity: 'error',
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return errors
|
||||
}
|
||||
|
||||
export function printValidationErrors(errors: ValidationError[]) {
|
||||
if (errors.length === 0) {
|
||||
console.log('✅ Page configuration is valid!')
|
||||
return
|
||||
}
|
||||
|
||||
const errorCount = errors.filter(e => e.severity === 'error').length
|
||||
const warningCount = errors.filter(e => e.severity === 'warning').length
|
||||
|
||||
console.log('\n📋 Page Configuration Validation Results\n')
|
||||
|
||||
if (errorCount > 0) {
|
||||
console.log(`❌ Errors: ${errorCount}`)
|
||||
errors
|
||||
.filter(e => e.severity === 'error')
|
||||
.forEach(e => {
|
||||
console.log(` • [${e.page}] ${e.field}: ${e.message}`)
|
||||
})
|
||||
}
|
||||
|
||||
if (warningCount > 0) {
|
||||
console.log(`\n⚠️ Warnings: ${warningCount}`)
|
||||
errors
|
||||
.filter(e => e.severity === 'warning')
|
||||
.forEach(e => {
|
||||
console.log(` • [${e.page}] ${e.field}: ${e.message}`)
|
||||
})
|
||||
}
|
||||
|
||||
console.log('\n')
|
||||
}
|
||||
|
||||
if (import.meta.url === `file://${process.argv[1]}`) {
|
||||
const errors = validatePageConfig()
|
||||
printValidationErrors(errors)
|
||||
process.exit(errors.filter(e => e.severity === 'error').length > 0 ? 1 : 0)
|
||||
}
|
||||
Reference in New Issue
Block a user