diff --git a/docs/README.md b/docs/README.md index 0c40a86..31ab72b 100644 --- a/docs/README.md +++ b/docs/README.md @@ -29,9 +29,11 @@ ## JSON-Driven UI - [JSON_PAGES_GUIDE.md](./JSON_PAGES_GUIDE.md) - Building pages from JSON configuration +- [json-routing.md](./json-routing.md) - **JSON-based routing and root route configuration** - [JSON_UI_GUIDE.md](../JSON_UI_GUIDE.md) - Original JSON UI documentation ### Page Schemas +- `/src/config/pages.json` - **Master routing configuration (includes root `/` route)** - `/src/config/pages/dashboard.json` - Dashboard page configuration - More schemas can be added for other pages diff --git a/docs/json-routing.md b/docs/json-routing.md new file mode 100644 index 0000000..7f72c59 --- /dev/null +++ b/docs/json-routing.md @@ -0,0 +1,184 @@ +# JSON-Driven Routing Configuration + +This application uses a JSON-based configuration system to define routes and pages declaratively. + +## Overview + +All routes are defined in `/src/config/pages.json` and automatically loaded by the router system. + +## Root Route Configuration + +The `/` (root) route is determined by the page marked with `"isRoot": true` in the pages.json configuration: + +```json +{ + "id": "home", + "title": "Home", + "icon": "House", + "component": "ProjectDashboard", + "enabled": true, + "isRoot": true, + "order": 0, + "props": { + "state": ["files", "models", "components", "theme", ...], + } +} +``` + +## Page Configuration Schema + +Each page in `pages.json` supports the following properties: + +### Required Properties + +- `id` (string): Unique identifier used in the URL path (`/dashboard`, `/code`, etc.) +- `title` (string): Display name shown in navigation +- `icon` (string): Phosphor icon name for navigation UI +- `component` (string): Name of the React component to render (must be registered in ComponentRegistry) +- `enabled` (boolean): Whether the page is active +- `order` (number): Sort order for navigation menu + +### Optional Properties + +- `isRoot` (boolean): If true, this page will be rendered at the `/` route +- `toggleKey` (string): Feature toggle key that controls page visibility +- `shortcut` (string): Keyboard shortcut (e.g., "ctrl+1", "ctrl+shift+s") +- `requiresResizable` (boolean): Whether the page uses a resizable split layout +- `props` (object): Component props configuration (see Props Configuration) +- `resizableConfig` (object): Split panel layout configuration (see Resizable Layout) + +## Props Configuration + +Props can be automatically resolved from app state and actions: + +```json +"props": { + "state": ["files", "models", "activeFileId"], + "actions": ["onFileChange:handleFileChange", "onFileSelect:setActiveFileId"] +} +``` + +### State Props + +State props are mapped from the global state context: + +- Simple mapping: `"files"` → `{ files: stateContext.files }` +- Renamed mapping: `"trees:componentTrees"` → `{ trees: stateContext.componentTrees }` + +### Action Props + +Action props are mapped from the action context using `propName:actionName` format: + +- `"onFileChange:handleFileChange"` → `{ onFileChange: actionContext.handleFileChange }` + +## Resizable Layout Configuration + +Pages with split panels use the `requiresResizable` flag and `resizableConfig`: + +```json +{ + "id": "code", + "requiresResizable": true, + "resizableConfig": { + "leftComponent": "FileExplorer", + "leftProps": { + "state": ["files", "activeFileId"], + "actions": ["onFileSelect:setActiveFileId"] + }, + "leftPanel": { + "defaultSize": 20, + "minSize": 15, + "maxSize": 30 + }, + "rightPanel": { + "defaultSize": 80 + } + } +} +``` + +## Feature Toggles + +Pages can be conditionally shown based on feature toggles: + +```json +{ + "id": "models-json", + "toggleKey": "modelsJSON", + "enabled": true +} +``` + +The page will only appear if `featureToggles.modelsJSON !== false`. + +## Keyboard Shortcuts + +Define shortcuts in `ctrl+key` or `ctrl+shift+key` format: + +```json +{ + "id": "dashboard", + "shortcut": "ctrl+1" +} +``` + +Shortcuts automatically navigate to the page when pressed. + +## Route Resolution Flow + +1. **Load Configuration**: `pages.json` is loaded by `getPageConfig()` +2. **Filter Enabled Pages**: Only pages with `enabled: true` and matching feature toggles +3. **Sort by Order**: Pages are sorted by the `order` field +4. **Create Routes**: Each page becomes a route object with: + - Path: `/${page.id}` or `/` if `isRoot: true` + - Element: Lazy-loaded component with resolved props + - Layout: Standard or resizable based on configuration +5. **Add Fallback**: Wildcard route redirects to root + +## Adding a New Page + +1. Add entry to `src/config/pages.json`: +```json +{ + "id": "my-page", + "title": "My Page", + "icon": "Star", + "component": "MyPageComponent", + "enabled": true, + "order": 100, + "props": { + "state": ["someData"], + "actions": ["onSave:handleSave"] + } +} +``` + +2. Register component in `src/lib/component-registry.ts` +3. The route is automatically created at `/my-page` + +## Console Logging + +The routing system includes extensive console logging prefixed with `[ROUTES]` and `[CONFIG]`: + +- `[ROUTES] 🏗️` - Route creation started +- `[ROUTES] 📄` - Pages loaded +- `[ROUTES] 🏠` - Root page identified +- `[ROUTES] 📝` - Individual route configured +- `[CONFIG] 🔍` - Config queries +- `[CONFIG] ✅` - Successful resolution + +## Example: Changing the Root Page + +To make a different page appear at `/`: + +1. Remove `"isRoot": true` from the current root page (if any) +2. Add `"isRoot": true` to your desired page in `pages.json` +3. The change takes effect immediately on reload + +## Related Files + +- `/src/config/pages.json` - Page definitions +- `/src/config/page-loader.ts` - Configuration loading and prop resolution +- `/src/router/routes.tsx` - Route creation from config +- `/src/router/RouterProvider.tsx` - Router setup +- `/src/lib/component-registry.ts` - Component registration diff --git a/src/config/page-loader.ts b/src/config/page-loader.ts index c0890bc..f3bc453 100644 --- a/src/config/page-loader.ts +++ b/src/config/page-loader.ts @@ -25,6 +25,7 @@ export interface PageConfig { icon: string component: string enabled: boolean + isRoot?: boolean toggleKey?: string shortcut?: string order: number diff --git a/src/config/pages.json b/src/config/pages.json index aedca27..d5dce4c 100644 --- a/src/config/pages.json +++ b/src/config/pages.json @@ -1,5 +1,17 @@ { "pages": [ + { + "id": "home", + "title": "Home", + "icon": "House", + "component": "ProjectDashboard", + "enabled": true, + "isRoot": true, + "order": 0, + "props": { + "state": ["files", "models", "components", "theme", "playwrightTests", "storybookStories", "unitTests", "flaskConfig", "nextjsConfig"] + } + }, { "id": "dashboard", "title": "Dashboard", diff --git a/src/router/routes.tsx b/src/router/routes.tsx index 22d34de..ff76b37 100644 --- a/src/router/routes.tsx +++ b/src/router/routes.tsx @@ -76,46 +76,64 @@ export function createRoutes( const enabledPages = getEnabledPages(featureToggles) console.log('[ROUTES] 📄 Enabled pages:', enabledPages.map(p => p.id).join(', ')) - const routes: RouteObject[] = enabledPages.map(page => { - console.log('[ROUTES] 📝 Configuring route for page:', page.id) - - const props = page.props - ? resolveProps(page.props, stateContext, actionContext) - : {} + const rootPage = enabledPages.find(p => p.isRoot) + console.log('[ROUTES] 🏠 Root page:', rootPage?.id || 'none (will use dashboard)') - if (page.requiresResizable && page.resizableConfig) { - console.log('[ROUTES] 🔀 Page requires resizable layout:', page.id) - const config = page.resizableConfig - const leftProps = resolveProps(config.leftProps, stateContext, actionContext) + const routes: RouteObject[] = enabledPages + .filter(p => !p.isRoot) + .map(page => { + console.log('[ROUTES] 📝 Configuring route for page:', page.id) + + const props = page.props + ? resolveProps(page.props, stateContext, actionContext) + : {} + + if (page.requiresResizable && page.resizableConfig) { + console.log('[ROUTES] 🔀 Page requires resizable layout:', page.id) + const config = page.resizableConfig + const leftProps = resolveProps(config.leftProps, stateContext, actionContext) + + return { + path: `/${page.id}`, + element: ( + + ) + } + } return { path: `/${page.id}`, - element: ( - - ) + element: } - } + }) - return { - path: `/${page.id}`, - element: - } - }) - - routes.push({ - path: '/', - element: - }) + if (rootPage) { + console.log('[ROUTES] ✅ Adding root route from JSON config:', rootPage.component) + const props = rootPage.props + ? resolveProps(rootPage.props, stateContext, actionContext) + : {} + + routes.push({ + path: '/', + element: + }) + } else { + console.log('[ROUTES] ⚠️ No root page in config, redirecting to /dashboard') + routes.push({ + path: '/', + element: + }) + } routes.push({ path: '*', - element: + element: }) console.log('[ROUTES] ✅ Routes created:', routes.length, 'routes')