From 838abcb61826ee995092b67ba3bffa3992e8f274 Mon Sep 17 00:00:00 2001 From: johndoe6345789 Date: Fri, 16 Jan 2026 23:30:20 +0000 Subject: [PATCH] Generated by Spark: Add props configuration to pages.json for dynamic component props --- CONFIG_ARCHITECTURE.md | 436 ++++++++++++++++++++++++++++++++++ PROPS_CONFIG_GUIDE.md | 317 ++++++++++++++++++++++++ src/App.tsx | 199 ++++++++-------- src/config/page-loader.ts | 50 ++++ src/config/page-schema.ts | 41 ++++ src/config/pages.json | 119 ++++++++-- src/config/validate-config.ts | 91 +++++++ 7 files changed, 1137 insertions(+), 116 deletions(-) create mode 100644 CONFIG_ARCHITECTURE.md create mode 100644 PROPS_CONFIG_GUIDE.md diff --git a/CONFIG_ARCHITECTURE.md b/CONFIG_ARCHITECTURE.md new file mode 100644 index 0000000..95b62da --- /dev/null +++ b/CONFIG_ARCHITECTURE.md @@ -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 = { + '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 diff --git a/PROPS_CONFIG_GUIDE.md b/PROPS_CONFIG_GUIDE.md new file mode 100644 index 0000000..a286998 --- /dev/null +++ b/PROPS_CONFIG_GUIDE.md @@ -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> = { + // ... 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 = { + '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 diff --git a/src/App.tsx b/src/App.tsx index 71516ab..bed2eec 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -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> = { @@ -134,92 +134,48 @@ function App() { } const getPropsForComponent = (pageId: string) => { - const propsMap: Record = { - '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 } - 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 + } + + 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 ( - - }> - + + }> + - - }> - + + }> + ) } - const props = getPropsForComponent(page.component) + const props = getPropsForComponent(page.id) return ( }> diff --git a/src/config/page-loader.ts b/src/config/page-loader.ts index 29fd461..7477016 100644 --- a/src/config/page-loader.ts +++ b/src/config/page-loader.ts @@ -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, actionContext: Record): Record { + if (!propConfig) return {} + + const resolvedProps: Record = {} + + 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 +} diff --git a/src/config/page-schema.ts b/src/config/page-schema.ts index 6e03fdf..ce49807 100644 --- a/src/config/page-schema.ts +++ b/src/config/page-schema.ts @@ -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 +export type ResizablePanelConfig = z.infer +export type ResizableConfig = z.infer +export type SimplePageConfig = z.infer +export type SimplePagesConfig = z.infer export type KeyboardShortcut = z.infer export type PanelConfig = z.infer export type LayoutConfig = z.infer diff --git a/src/config/pages.json b/src/config/pages.json index 8f399e1..8f78c20 100644 --- a/src/config/pages.json +++ b/src/config/pages.json @@ -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"] + } } ] } diff --git a/src/config/validate-config.ts b/src/config/validate-config.ts index cfbaaa3..63bb357 100644 --- a/src/config/validate-config.ts +++ b/src/config/validate-config.ts @@ -13,6 +13,21 @@ export function validatePageConfig(): ValidationError[] { const seenIds = new Set() const seenShortcuts = new Set() const seenOrders = new Set() + + 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