Generated by Spark: Add props configuration to pages.json for dynamic component props

This commit is contained in:
2026-01-16 23:30:20 +00:00
committed by GitHub
parent 5bb96235e0
commit 838abcb618
7 changed files with 1137 additions and 116 deletions

436
CONFIG_ARCHITECTURE.md Normal file
View File

@@ -0,0 +1,436 @@
# Declarative Page Configuration System
## Overview
CodeForge uses a **declarative, JSON-driven configuration system** to manage pages, components, props, and layouts. This architecture enables rapid development without modifying core application logic.
## Architecture Diagram
```
┌─────────────────────────────────────────────────────────────┐
│ App.tsx │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Uses page-loader to: │ │
│ │ • Get enabled pages │ │
│ │ • Resolve component props dynamically │ │
│ │ • Render pages with correct configuration │ │
│ └──────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
│ imports & uses
┌─────────────────────────────────────────────────────────────┐
│ page-loader.ts (Resolution Logic) │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ • getPageConfig() │ │
│ │ • getEnabledPages(featureToggles) │ │
│ │ • resolveProps(propConfig, state, actions) │ │
│ │ • getPageShortcuts() │ │
│ └──────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
│ reads
┌─────────────────────────────────────────────────────────────┐
│ pages.json (Declarative Config) │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ { │ │
│ │ "pages": [ │ │
│ │ { │ │
│ │ "id": "models", │ │
│ │ "component": "ModelDesigner", │ │
│ │ "props": { │ │
│ │ "state": ["models"], │ │
│ │ "actions": ["onModelsChange:setModels"] │ │
│ │ } │ │
│ │ } │ │
│ │ ] │ │
│ │ } │ │
│ └──────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
│ validates
┌─────────────────────────────────────────────────────────────┐
│ validate-config.ts (Validation Logic) │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ • validatePageConfig() │ │
│ │ • Checks for duplicate IDs/shortcuts │ │
│ │ • Validates state/action keys exist │ │
│ │ • Validates resizable configurations │ │
│ └──────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
│ uses schemas
┌─────────────────────────────────────────────────────────────┐
│ page-schema.ts (Type Definitions) │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ • PropConfigSchema │ │
│ │ • ResizableConfigSchema │ │
│ │ • SimplePageConfigSchema │ │
│ │ • Zod schemas for runtime validation │ │
│ └──────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
```
## Core Files
### 1. `pages.json` - Source of Truth
The declarative configuration file that defines all pages, their components, and props.
**Location**: `/src/config/pages.json`
**Purpose**:
- Define all application pages
- Configure component props mapping
- Set keyboard shortcuts
- Configure resizable layouts
- Enable/disable features
### 2. `page-loader.ts` - Resolution Engine
The TypeScript module that reads and processes the JSON configuration.
**Location**: `/src/config/page-loader.ts`
**Key Functions**:
- `getPageConfig()` - Returns full page configuration
- `getEnabledPages(featureToggles)` - Filters pages based on toggles
- `resolveProps(propConfig, stateContext, actionContext)` - Maps JSON config to actual props
- `getPageShortcuts(featureToggles)` - Extracts keyboard shortcuts
### 3. `validate-config.ts` - Configuration Validator
Validates the pages.json configuration for errors and warnings.
**Location**: `/src/config/validate-config.ts`
**Validates**:
- Required fields (id, title, component)
- Duplicate IDs, shortcuts, or order numbers
- Valid state and action keys
- Resizable configuration correctness
- Feature toggle key validity
### 4. `page-schema.ts` - Type Definitions
Zod schemas and TypeScript types for type-safe configuration.
**Location**: `/src/config/page-schema.ts`
**Exports**:
- `PropConfigSchema` - Props configuration structure
- `ResizableConfigSchema` - Resizable layout structure
- `SimplePageConfigSchema` - Individual page structure
- TypeScript types inferred from schemas
## Data Flow
### Adding a New Page
```mermaid
graph LR
A[Edit pages.json] --> B[Add page config with props]
B --> C[page-loader reads config]
C --> D[App.tsx calls resolveProps]
D --> E[Props mapped from state/actions]
E --> F[Component rendered with props]
```
### Prop Resolution Flow
```
1. Component needs props
2. App.tsx calls getPropsForComponent(pageId)
3. getPropsForComponent finds page config
4. Calls resolveProps(page.props, stateContext, actionContext)
5. resolveProps iterates over state/action arrays
6. Maps JSON keys to actual values from contexts
7. Returns resolved props object
8. Props passed to component
```
## Configuration Examples
### Simple Page (Dashboard)
```json
{
"id": "dashboard",
"title": "Dashboard",
"icon": "ChartBar",
"component": "ProjectDashboard",
"enabled": true,
"shortcut": "ctrl+1",
"order": 1,
"props": {
"state": ["files", "models", "components"]
}
}
```
### Page with Actions (Model Designer)
```json
{
"id": "models",
"title": "Models",
"component": "ModelDesigner",
"props": {
"state": ["models"],
"actions": ["onModelsChange:setModels"]
}
}
```
### Page with Renamed Props (Flask Designer)
```json
{
"id": "flask",
"component": "FlaskDesigner",
"props": {
"state": ["config:flaskConfig"],
"actions": ["onConfigChange:setFlaskConfig"]
}
}
```
### Resizable Split-Panel Page (Code Editor)
```json
{
"id": "code",
"component": "CodeEditor",
"requiresResizable": true,
"props": {
"state": ["files", "activeFileId"],
"actions": ["onFileChange:handleFileChange"]
},
"resizableConfig": {
"leftComponent": "FileExplorer",
"leftProps": {
"state": ["files"],
"actions": ["onFileSelect:setActiveFileId"]
},
"leftPanel": {
"defaultSize": 20,
"minSize": 15,
"maxSize": 30
},
"rightPanel": {
"defaultSize": 80
}
}
}
```
## State Context Keys
Available state variables that can be referenced in `props.state`:
- `files` - Project files
- `models` - Data models
- `components` - Component definitions
- `componentTrees` - Component tree structures
- `workflows` - Workflow definitions
- `lambdas` - Lambda functions
- `theme` - Theme configuration
- `playwrightTests` - Playwright tests
- `storybookStories` - Storybook stories
- `unitTests` - Unit tests
- `flaskConfig` - Flask configuration
- `nextjsConfig` - Next.js configuration
- `npmSettings` - NPM settings
- `featureToggles` - Feature toggles
- `activeFileId` - Active file ID
## Action Context Keys
Available action functions that can be referenced in `props.actions`:
- `handleFileChange` - Update file content
- `setActiveFileId` - Set active file
- `handleFileClose` - Close file
- `handleFileAdd` - Add new file
- `setModels` - Update models
- `setComponents` - Update components
- `setComponentTrees` - Update component trees
- `setWorkflows` - Update workflows
- `setLambdas` - Update lambdas
- `setTheme` - Update theme
- `setPlaywrightTests` - Update Playwright tests
- `setStorybookStories` - Update Storybook stories
- `setUnitTests` - Update unit tests
- `setFlaskConfig` - Update Flask config
- `setNextjsConfig` - Update Next.js config
- `setNpmSettings` - Update NPM settings
- `setFeatureToggles` - Update feature toggles
## Benefits of This Architecture
### 1. **Declarative Configuration**
- All pages defined in one place
- Easy to understand at a glance
- No need to read through TypeScript code
### 2. **Separation of Concerns**
- Configuration separate from logic
- Props resolution handled systematically
- Business logic stays in components
### 3. **Type Safety**
- Zod schemas validate runtime data
- TypeScript types ensure compile-time safety
- Catch errors early in development
### 4. **Maintainability**
- Add pages without touching App.tsx logic
- Modify props without code changes
- Clear validation error messages
### 5. **Scalability**
- Easy to add new pages
- Simple to extend prop mappings
- Can add more configuration options
### 6. **Testability**
- Validate configuration independently
- Test prop resolution in isolation
- Mock configurations for testing
## Validation
Run configuration validation:
```bash
# Via TypeScript (if configured)
npm run validate-config
# Or manually import and run
import { validatePageConfig, printValidationErrors } from '@/config/validate-config'
const errors = validatePageConfig()
printValidationErrors(errors)
```
Validation checks:
- ✅ Required fields present
- ✅ No duplicate IDs or shortcuts
- ✅ Valid state/action keys
- ✅ Correct resizable config format
- ✅ Panel sizes sum to 100
- ⚠️ Warnings for missing icons or duplicate orders
## Extending the System
### Adding New State Keys
1. Add state to `useProjectState` hook
2. Add to `stateContext` in `App.tsx`
3. Add to validation in `validate-config.ts`
4. Document in this README
### Adding New Action Keys
1. Create action function in `App.tsx` or hook
2. Add to `actionContext` in `App.tsx`
3. Add to validation in `validate-config.ts`
4. Document in this README
### Adding New Configuration Options
1. Update `pages.json` with new field
2. Update `PageConfig` interface in `page-loader.ts`
3. Update Zod schema in `page-schema.ts`
4. Add validation in `validate-config.ts`
5. Implement logic in `App.tsx`
## Migration Guide
### From Hardcoded Props to JSON Config
**Before** (Hardcoded in App.tsx):
```typescript
const getPropsForComponent = (pageId: string) => {
const propsMap: Record<string, any> = {
'ModelDesigner': {
models,
onModelsChange: setModels,
},
}
return propsMap[pageId] || {}
}
```
**After** (Declarative in pages.json):
```json
{
"id": "models",
"component": "ModelDesigner",
"props": {
"state": ["models"],
"actions": ["onModelsChange:setModels"]
}
}
```
## Best Practices
1. **Keep props minimal** - Only pass what the component needs
2. **Use descriptive prop names** - Clear intent over brevity
3. **Validate configurations** - Run validation before deployment
4. **Document new features** - Update this README when extending
5. **Consistent naming** - Follow existing patterns for state/actions
6. **Group related props** - Keep state and actions organized
## Troubleshooting
### Props not reaching component?
- Check state/action key spelling in pages.json
- Verify key exists in stateContext/actionContext
- Run validation to catch errors
### Component not rendering?
- Ensure component added to componentMap in App.tsx
- Check component name matches exactly (case-sensitive)
- Verify component has default export
### Resizable panel issues?
- Ensure defaultSize values sum to 100
- Check leftComponent exists in componentMap
- Verify resizableConfig structure is complete
## Related Documentation
- [PROPS_CONFIG_GUIDE.md](./PROPS_CONFIG_GUIDE.md) - Detailed props configuration guide
- [src/config/pages.json](./src/config/pages.json) - Current page configurations
- [src/config/page-loader.ts](./src/config/page-loader.ts) - Resolution logic
- [src/config/validate-config.ts](./src/config/validate-config.ts) - Validation utilities
## Future Enhancements
Potential improvements to the configuration system:
- [ ] Hot-reload configuration changes in development
- [ ] Visual configuration editor UI
- [ ] Import/export page configurations
- [ ] Configuration presets/templates
- [ ] Computed/derived props from state
- [ ] Conditional prop mappings
- [ ] Hook injection via configuration
- [ ] Layout templates beyond resizable panels
- [ ] Permission-based page visibility
- [ ] Analytics tracking configuration

317
PROPS_CONFIG_GUIDE.md Normal file
View File

@@ -0,0 +1,317 @@
# Props Configuration Guide
## Overview
The CodeForge application now supports **dynamic component props configuration** through the `pages.json` file. This declarative approach eliminates the need to modify `App.tsx` when adding or updating component props, making the system more maintainable and scalable.
## Architecture
### Key Files
1. **`src/config/pages.json`** - Declarative page and props configuration
2. **`src/config/page-loader.ts`** - Props resolution logic and interfaces
3. **`src/App.tsx`** - Consumes the configuration and renders pages
## Props Configuration Schema
### Basic Props Structure
```json
{
"id": "example-page",
"component": "ExampleComponent",
"props": {
"state": ["stateKey1", "stateKey2"],
"actions": ["actionPropName:actionFunctionName"]
}
}
```
### Props Configuration Options
#### `state` Array
Maps application state to component props. Supports two formats:
1. **Direct mapping** (prop name = state key):
```json
"state": ["files", "models", "theme"]
```
Results in: `{ files, models, theme }`
2. **Renamed mapping** (prop name : state key):
```json
"state": ["trees:componentTrees", "config:flaskConfig"]
```
Results in: `{ trees: componentTrees, config: flaskConfig }`
#### `actions` Array
Maps action functions (handlers/setters) to component props. Uses format:
```json
"actions": ["propName:functionName"]
```
Example:
```json
"actions": [
"onModelsChange:setModels",
"onFileSelect:setActiveFileId"
]
```
### Resizable Page Configuration
For pages with split-panel layouts (e.g., File Explorer + Code Editor):
```json
{
"id": "code",
"component": "CodeEditor",
"requiresResizable": true,
"props": {
"state": ["files", "activeFileId"],
"actions": ["onFileChange:handleFileChange"]
},
"resizableConfig": {
"leftComponent": "FileExplorer",
"leftProps": {
"state": ["files", "activeFileId"],
"actions": ["onFileSelect:setActiveFileId"]
},
"leftPanel": {
"defaultSize": 20,
"minSize": 15,
"maxSize": 30
},
"rightPanel": {
"defaultSize": 80
}
}
}
```
## Available State Keys
The following state variables are available in the state context:
- `files` - Project files
- `models` - Data models
- `components` - Component definitions
- `componentTrees` - Component tree structures
- `workflows` - Workflow definitions
- `lambdas` - Lambda function definitions
- `theme` - Theme configuration
- `playwrightTests` - Playwright test definitions
- `storybookStories` - Storybook story definitions
- `unitTests` - Unit test definitions
- `flaskConfig` - Flask API configuration
- `nextjsConfig` - Next.js project configuration
- `npmSettings` - NPM package settings
- `featureToggles` - Feature toggle states
- `activeFileId` - Currently selected file ID
## Available Actions
The following action functions are available in the action context:
- `handleFileChange` - Update file content
- `setActiveFileId` - Set active file
- `handleFileClose` - Close a file
- `handleFileAdd` - Add new file
- `setModels` - Update models
- `setComponents` - Update components
- `setComponentTrees` - Update component trees
- `setWorkflows` - Update workflows
- `setLambdas` - Update lambdas
- `setTheme` - Update theme
- `setPlaywrightTests` - Update Playwright tests
- `setStorybookStories` - Update Storybook stories
- `setUnitTests` - Update unit tests
- `setFlaskConfig` - Update Flask config
- `setNextjsConfig` - Update Next.js config
- `setNpmSettings` - Update NPM settings
- `setFeatureToggles` - Update feature toggles
## Examples
### Simple Component (No Props)
```json
{
"id": "docs",
"title": "Documentation",
"component": "DocumentationView",
"props": {}
}
```
### Component with State Only
```json
{
"id": "models",
"title": "Models",
"component": "ModelDesigner",
"props": {
"state": ["models"]
}
}
```
### Component with State and Actions
```json
{
"id": "models",
"title": "Models",
"component": "ModelDesigner",
"props": {
"state": ["models"],
"actions": ["onModelsChange:setModels"]
}
}
```
### Component with Renamed Props
```json
{
"id": "flask",
"title": "Flask API",
"component": "FlaskDesigner",
"props": {
"state": ["config:flaskConfig"],
"actions": ["onConfigChange:setFlaskConfig"]
}
}
```
### Dashboard with Multiple State Props
```json
{
"id": "dashboard",
"title": "Dashboard",
"component": "ProjectDashboard",
"props": {
"state": [
"files",
"models",
"components",
"theme",
"playwrightTests",
"storybookStories",
"unitTests",
"flaskConfig"
]
}
}
```
## Adding a New Page
To add a new page with props configuration:
1. **Add the page to `pages.json`**:
```json
{
"id": "new-page",
"title": "New Feature",
"icon": "Star",
"component": "NewFeatureComponent",
"enabled": true,
"order": 21,
"props": {
"state": ["relevantState"],
"actions": ["onAction:setRelevantState"]
}
}
```
2. **Add the component to the lazy import map in `App.tsx`**:
```typescript
const componentMap: Record<string, React.LazyExoticComponent<any>> = {
// ... existing components
NewFeatureComponent: lazy(() => import('@/components/NewFeatureComponent').then(m => ({ default: m.NewFeatureComponent }))),
}
```
3. **Optionally add to feature toggles** (if applicable):
```json
{
"toggleKey": "newFeature"
}
```
That's it! No need to modify the `getPropsForComponent` function or other logic in `App.tsx`.
## Benefits
1. **Declarative Configuration** - All page configs in one place
2. **No Code Changes** - Add/modify pages without touching `App.tsx` logic
3. **Type Safety** - TypeScript interfaces ensure configuration validity
4. **Maintainability** - Easy to understand and modify page props
5. **Scalability** - Simple to add new pages and props
6. **Consistency** - Standardized prop resolution across all pages
## Migration from Old System
The old hardcoded `propsMap` in `getPropsForComponent` has been replaced with dynamic resolution using `resolveProps()`. The configuration in `pages.json` now drives all prop mapping.
### Before (Hardcoded in App.tsx)
```typescript
const propsMap: Record<string, any> = {
'ModelDesigner': {
models,
onModelsChange: setModels,
},
// ... 20+ more entries
}
```
### After (Declarative in pages.json)
```json
{
"id": "models",
"component": "ModelDesigner",
"props": {
"state": ["models"],
"actions": ["onModelsChange:setModels"]
}
}
```
## Future Enhancements
Potential extensions to the props configuration system:
1. **Computed Props** - Props derived from multiple state values
2. **Prop Transformations** - Map/filter/reduce operations on state
3. **Conditional Props** - Props based on feature toggles or user state
4. **Default Values** - Fallback values for missing state
5. **Validation Rules** - Runtime prop validation from schema
6. **Hook Configuration** - Custom hooks to inject into components
7. **Event Handlers** - Declarative event handler composition
## Troubleshooting
### Props not being passed to component
1. Check that the state/action key exists in the context objects
2. Verify the prop name mapping is correct (before:after colon)
3. Ensure the page is enabled and not filtered by feature toggles
4. Check browser console for resolution errors
### Component not rendering
1. Verify the component is added to the `componentMap` in `App.tsx`
2. Check that the component name matches exactly (case-sensitive)
3. Ensure the component export matches the import pattern
### State updates not working
1. Verify the action function name is correct
2. Check that the setter is using functional updates for array/object state
3. Ensure the action is properly mapped in the action context

View File

@@ -6,7 +6,7 @@ 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 { getPageConfig, getEnabledPages, getPageShortcuts, resolveProps } from '@/config/page-loader'
import { toast } from 'sonner'
const componentMap: Record<string, React.LazyExoticComponent<any>> = {
@@ -134,92 +134,48 @@ function App() {
}
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': {},
const page = enabledPages.find(p => p.id === pageId)
if (!page || !page.props) return {}
const stateContext = {
files,
models,
components,
componentTrees,
workflows,
lambdas,
theme,
playwrightTests,
storybookStories,
unitTests,
flaskConfig,
nextjsConfig,
npmSettings,
featureToggles,
activeFileId,
}
return propsMap[pageId] || {}
const actionContext = {
handleFileChange,
setActiveFileId,
handleFileClose,
handleFileAdd,
setModels,
setComponents,
setComponentTrees,
setWorkflows,
setLambdas,
setTheme,
setPlaywrightTests,
setStorybookStories,
setUnitTests,
setFlaskConfig,
setNextjsConfig,
setNpmSettings,
setFeatureToggles,
}
return resolveProps(page.props, stateContext, actionContext)
}
const renderPageContent = (page: any) => {
@@ -228,27 +184,78 @@ function App() {
return <LoadingFallback message={`Component ${page.component} not found`} />
}
if (page.requiresResizable && page.id === 'code') {
const FileExplorerComp = componentMap['FileExplorer']
const CodeEditorComp = componentMap['CodeEditor']
if (page.requiresResizable && page.resizableConfig) {
const config = page.resizableConfig
const LeftComponent = componentMap[config.leftComponent]
const RightComponent = Component
if (!LeftComponent) {
return <LoadingFallback message={`Component ${config.leftComponent} not found`} />
}
const stateContext = {
files,
models,
components,
componentTrees,
workflows,
lambdas,
theme,
playwrightTests,
storybookStories,
unitTests,
flaskConfig,
nextjsConfig,
npmSettings,
featureToggles,
activeFileId,
}
const actionContext = {
handleFileChange,
setActiveFileId,
handleFileClose,
handleFileAdd,
setModels,
setComponents,
setComponentTrees,
setWorkflows,
setLambdas,
setTheme,
setPlaywrightTests,
setStorybookStories,
setUnitTests,
setFlaskConfig,
setNextjsConfig,
setNpmSettings,
setFeatureToggles,
}
const leftProps = resolveProps(config.leftProps, stateContext, actionContext)
const rightProps = getPropsForComponent(page.id)
return (
<ResizablePanelGroup direction="horizontal">
<ResizablePanel defaultSize={20} minSize={15} maxSize={30}>
<Suspense fallback={<LoadingFallback message="Loading explorer..." />}>
<FileExplorerComp {...getPropsForComponent('FileExplorer')} />
<ResizablePanel
defaultSize={config.leftPanel.defaultSize}
minSize={config.leftPanel.minSize}
maxSize={config.leftPanel.maxSize}
>
<Suspense fallback={<LoadingFallback message={`Loading ${config.leftComponent.toLowerCase()}...`} />}>
<LeftComponent {...leftProps} />
</Suspense>
</ResizablePanel>
<ResizableHandle />
<ResizablePanel defaultSize={80}>
<Suspense fallback={<LoadingFallback message="Loading editor..." />}>
<CodeEditorComp {...getPropsForComponent('CodeEditor')} />
<ResizablePanel defaultSize={config.rightPanel.defaultSize}>
<Suspense fallback={<LoadingFallback message={`Loading ${page.title.toLowerCase()}...`} />}>
<RightComponent {...rightProps} />
</Suspense>
</ResizablePanel>
</ResizablePanelGroup>
)
}
const props = getPropsForComponent(page.component)
const props = getPropsForComponent(page.id)
return (
<Suspense fallback={<LoadingFallback message={`Loading ${page.title.toLowerCase()}...`} />}>
<Component {...props} />

View File

@@ -1,6 +1,24 @@
import pagesConfig from './pages.json'
import { FeatureToggles } from '@/types/project'
export interface PropConfig {
state?: string[]
actions?: string[]
}
export interface ResizableConfig {
leftComponent: string
leftProps: PropConfig
leftPanel: {
defaultSize: number
minSize: number
maxSize: number
}
rightPanel: {
defaultSize: number
}
}
export interface PageConfig {
id: string
title: string
@@ -11,6 +29,8 @@ export interface PageConfig {
shortcut?: string
order: number
requiresResizable?: boolean
props?: PropConfig
resizableConfig?: ResizableConfig
}
export interface PagesConfig {
@@ -57,3 +77,33 @@ export function getPageShortcuts(featureToggles?: FeatureToggles): Array<{
}
})
}
export function resolveProps(propConfig: PropConfig | undefined, stateContext: Record<string, any>, actionContext: Record<string, any>): Record<string, any> {
if (!propConfig) return {}
const resolvedProps: Record<string, any> = {}
if (propConfig.state) {
for (const stateKey of propConfig.state) {
const [propName, contextKey] = stateKey.includes(':')
? stateKey.split(':')
: [stateKey, stateKey]
if (stateContext[contextKey] !== undefined) {
resolvedProps[propName] = stateContext[contextKey]
}
}
}
if (propConfig.actions) {
for (const actionKey of propConfig.actions) {
const [propName, contextKey] = actionKey.split(':')
if (actionContext[contextKey]) {
resolvedProps[propName] = actionContext[contextKey]
}
}
}
return resolvedProps
}

View File

@@ -1,5 +1,41 @@
import { z } from 'zod'
export const PropConfigSchema = z.object({
state: z.array(z.string()).optional(),
actions: z.array(z.string()).optional(),
})
export const ResizablePanelConfigSchema = z.object({
defaultSize: z.number(),
minSize: z.number().optional(),
maxSize: z.number().optional(),
})
export const ResizableConfigSchema = z.object({
leftComponent: z.string(),
leftProps: PropConfigSchema,
leftPanel: ResizablePanelConfigSchema,
rightPanel: ResizablePanelConfigSchema,
})
export const SimplePageConfigSchema = z.object({
id: z.string(),
title: z.string(),
icon: z.string(),
component: z.string(),
enabled: z.boolean(),
toggleKey: z.string().optional(),
shortcut: z.string().optional(),
order: z.number(),
requiresResizable: z.boolean().optional(),
props: PropConfigSchema.optional(),
resizableConfig: ResizableConfigSchema.optional(),
})
export const SimplePagesConfigSchema = z.object({
pages: z.array(SimplePageConfigSchema),
})
export const KeyboardShortcutSchema = z.object({
key: z.string(),
ctrl: z.boolean().optional(),
@@ -45,6 +81,11 @@ export const PageRegistrySchema = z.object({
pages: z.array(PageConfigSchema),
})
export type PropConfig = z.infer<typeof PropConfigSchema>
export type ResizablePanelConfig = z.infer<typeof ResizablePanelConfigSchema>
export type ResizableConfig = z.infer<typeof ResizableConfigSchema>
export type SimplePageConfig = z.infer<typeof SimplePageConfigSchema>
export type SimplePagesConfig = z.infer<typeof SimplePagesConfigSchema>
export type KeyboardShortcut = z.infer<typeof KeyboardShortcutSchema>
export type PanelConfig = z.infer<typeof PanelConfigSchema>
export type LayoutConfig = z.infer<typeof LayoutConfigSchema>

View File

@@ -7,7 +7,10 @@
"component": "ProjectDashboard",
"enabled": true,
"shortcut": "ctrl+1",
"order": 1
"order": 1,
"props": {
"state": ["files", "models", "components", "theme", "playwrightTests", "storybookStories", "unitTests", "flaskConfig"]
}
},
{
"id": "code",
@@ -18,7 +21,26 @@
"toggleKey": "codeEditor",
"shortcut": "ctrl+2",
"order": 2,
"requiresResizable": true
"requiresResizable": true,
"props": {
"state": ["files", "activeFileId"],
"actions": ["onFileChange:handleFileChange", "onFileSelect:setActiveFileId", "onFileClose:handleFileClose"]
},
"resizableConfig": {
"leftComponent": "FileExplorer",
"leftProps": {
"state": ["files", "activeFileId"],
"actions": ["onFileSelect:setActiveFileId", "onFileAdd:handleFileAdd"]
},
"leftPanel": {
"defaultSize": 20,
"minSize": 15,
"maxSize": 30
},
"rightPanel": {
"defaultSize": 80
}
}
},
{
"id": "models",
@@ -28,7 +50,11 @@
"enabled": true,
"toggleKey": "models",
"shortcut": "ctrl+3",
"order": 3
"order": 3,
"props": {
"state": ["models"],
"actions": ["onModelsChange:setModels"]
}
},
{
"id": "components",
@@ -38,7 +64,11 @@
"enabled": true,
"toggleKey": "components",
"shortcut": "ctrl+4",
"order": 4
"order": 4,
"props": {
"state": ["components"],
"actions": ["onComponentsChange:setComponents"]
}
},
{
"id": "component-trees",
@@ -48,7 +78,11 @@
"enabled": true,
"toggleKey": "componentTrees",
"shortcut": "ctrl+5",
"order": 5
"order": 5,
"props": {
"state": ["trees:componentTrees"],
"actions": ["onTreesChange:setComponentTrees"]
}
},
{
"id": "workflows",
@@ -58,7 +92,11 @@
"enabled": true,
"toggleKey": "workflows",
"shortcut": "ctrl+6",
"order": 6
"order": 6,
"props": {
"state": ["workflows"],
"actions": ["onWorkflowsChange:setWorkflows"]
}
},
{
"id": "lambdas",
@@ -68,7 +106,11 @@
"enabled": true,
"toggleKey": "lambdas",
"shortcut": "ctrl+7",
"order": 7
"order": 7,
"props": {
"state": ["lambdas"],
"actions": ["onLambdasChange:setLambdas"]
}
},
{
"id": "styling",
@@ -78,7 +120,11 @@
"enabled": true,
"toggleKey": "styling",
"shortcut": "ctrl+8",
"order": 8
"order": 8,
"props": {
"state": ["theme"],
"actions": ["onThemeChange:setTheme"]
}
},
{
"id": "favicon",
@@ -88,7 +134,8 @@
"enabled": true,
"toggleKey": "faviconDesigner",
"shortcut": "ctrl+9",
"order": 9
"order": 9,
"props": {}
},
{
"id": "ideas",
@@ -97,7 +144,8 @@
"component": "FeatureIdeaCloud",
"enabled": true,
"toggleKey": "ideaCloud",
"order": 10
"order": 10,
"props": {}
},
{
"id": "flask",
@@ -106,7 +154,11 @@
"component": "FlaskDesigner",
"enabled": true,
"toggleKey": "flaskApi",
"order": 11
"order": 11,
"props": {
"state": ["config:flaskConfig"],
"actions": ["onConfigChange:setFlaskConfig"]
}
},
{
"id": "playwright",
@@ -115,7 +167,11 @@
"component": "PlaywrightDesigner",
"enabled": true,
"toggleKey": "playwright",
"order": 12
"order": 12,
"props": {
"state": ["tests:playwrightTests"],
"actions": ["onTestsChange:setPlaywrightTests"]
}
},
{
"id": "storybook",
@@ -124,7 +180,11 @@
"component": "StorybookDesigner",
"enabled": true,
"toggleKey": "storybook",
"order": 13
"order": 13,
"props": {
"state": ["stories:storybookStories"],
"actions": ["onStoriesChange:setStorybookStories"]
}
},
{
"id": "unit-tests",
@@ -133,7 +193,11 @@
"component": "UnitTestDesigner",
"enabled": true,
"toggleKey": "unitTests",
"order": 14
"order": 14,
"props": {
"state": ["tests:unitTests"],
"actions": ["onTestsChange:setUnitTests"]
}
},
{
"id": "errors",
@@ -142,7 +206,11 @@
"component": "ErrorPanel",
"enabled": true,
"toggleKey": "errorRepair",
"order": 15
"order": 15,
"props": {
"state": ["files"],
"actions": ["onFileChange:handleFileChange", "onFileSelect:setActiveFileId"]
}
},
{
"id": "docs",
@@ -151,7 +219,8 @@
"component": "DocumentationView",
"enabled": true,
"toggleKey": "documentation",
"order": 16
"order": 16,
"props": {}
},
{
"id": "sass",
@@ -160,7 +229,8 @@
"component": "SassStylesShowcase",
"enabled": true,
"toggleKey": "sassStyles",
"order": 17
"order": 17,
"props": {}
},
{
"id": "settings",
@@ -168,7 +238,11 @@
"icon": "Gear",
"component": "ProjectSettingsDesigner",
"enabled": true,
"order": 18
"order": 18,
"props": {
"state": ["nextjsConfig", "npmSettings"],
"actions": ["onNextjsConfigChange:setNextjsConfig", "onNpmSettingsChange:setNpmSettings"]
}
},
{
"id": "pwa",
@@ -176,7 +250,8 @@
"icon": "DeviceMobile",
"component": "PWASettings",
"enabled": true,
"order": 19
"order": 19,
"props": {}
},
{
"id": "features",
@@ -184,7 +259,11 @@
"icon": "ToggleRight",
"component": "FeatureToggleSettings",
"enabled": true,
"order": 20
"order": 20,
"props": {
"state": ["features:featureToggles"],
"actions": ["onFeaturesChange:setFeatureToggles"]
}
}
]
}

View File

@@ -13,6 +13,21 @@ export function validatePageConfig(): ValidationError[] {
const seenIds = new Set<string>()
const seenShortcuts = new Set<string>()
const seenOrders = new Set<number>()
const validStateKeys = [
'files', 'models', 'components', 'componentTrees', 'workflows',
'lambdas', 'theme', 'playwrightTests', 'storybookStories',
'unitTests', 'flaskConfig', 'nextjsConfig', 'npmSettings',
'featureToggles', 'activeFileId'
]
const validActionKeys = [
'handleFileChange', 'setActiveFileId', 'handleFileClose', 'handleFileAdd',
'setModels', 'setComponents', 'setComponentTrees', 'setWorkflows',
'setLambdas', 'setTheme', 'setPlaywrightTests', 'setStorybookStories',
'setUnitTests', 'setFlaskConfig', 'setNextjsConfig', 'setNpmSettings',
'setFeatureToggles'
]
pagesConfig.pages.forEach((page: PageConfig) => {
if (!page.id) {
@@ -125,6 +140,82 @@ export function validatePageConfig(): ValidationError[] {
})
}
}
if (page.props) {
if (page.props.state) {
page.props.state.forEach(stateKey => {
const [, contextKey] = stateKey.includes(':')
? stateKey.split(':')
: [stateKey, stateKey]
if (!validStateKeys.includes(contextKey)) {
errors.push({
page: page.id || 'Unknown',
field: 'props.state',
message: `Unknown state key: ${contextKey}. Valid keys: ${validStateKeys.join(', ')}`,
severity: 'error',
})
}
})
}
if (page.props.actions) {
page.props.actions.forEach(actionKey => {
const [, contextKey] = actionKey.split(':')
if (!contextKey) {
errors.push({
page: page.id || 'Unknown',
field: 'props.actions',
message: `Action key must use format "propName:functionName". Got: ${actionKey}`,
severity: 'error',
})
} else if (!validActionKeys.includes(contextKey)) {
errors.push({
page: page.id || 'Unknown',
field: 'props.actions',
message: `Unknown action key: ${contextKey}. Valid keys: ${validActionKeys.join(', ')}`,
severity: 'error',
})
}
})
}
}
if (page.requiresResizable) {
if (!page.resizableConfig) {
errors.push({
page: page.id || 'Unknown',
field: 'resizableConfig',
message: 'resizableConfig is required when requiresResizable is true',
severity: 'error',
})
} else {
if (!page.resizableConfig.leftComponent) {
errors.push({
page: page.id || 'Unknown',
field: 'resizableConfig.leftComponent',
message: 'leftComponent is required in resizableConfig',
severity: 'error',
})
}
const leftPanel = page.resizableConfig.leftPanel
const rightPanel = page.resizableConfig.rightPanel
if (leftPanel && rightPanel) {
const totalSize = leftPanel.defaultSize + rightPanel.defaultSize
if (totalSize !== 100) {
errors.push({
page: page.id || 'Unknown',
field: 'resizableConfig',
message: `Panel defaultSize values must sum to 100. Got: ${totalSize}`,
severity: 'warning',
})
}
}
}
}
})
return errors