diff --git a/SCHEMA_CONVERSION_SUMMARY.md b/SCHEMA_CONVERSION_SUMMARY.md new file mode 100644 index 0000000..e25d15b --- /dev/null +++ b/SCHEMA_CONVERSION_SUMMARY.md @@ -0,0 +1,116 @@ +# Schema Conversion Summary + +## Overview +Successfully converted TypeScript schema files to JSON format with extracted compute functions. + +## Files Created + +### JSON Schemas +1. **`src/schemas/analytics-dashboard.json`** - Converted from `dashboard-schema.ts` + - Contains the analytics dashboard with user management + - Compute functions: `computeFilteredUsers`, `computeStats`, `updateFilterQuery`, `transformFilteredUsers`, `transformUserList` + +2. **`src/schemas/todo-list.json`** - Split from `page-schemas.ts` + - Todo list application schema + - Compute functions: `computeTodoStats`, `updateNewTodo`, `computeAddTodo`, `checkCanAddTodo` + +3. **`src/schemas/dashboard-simple.json`** - Split from `page-schemas.ts` + - Simple dashboard with static stats + - No compute functions (pure static data) + +4. **`src/schemas/new-molecules-showcase.json`** - Split from `page-schemas.ts` + - Showcase of new molecular components + - No compute functions (pure static data) + +### TypeScript Support Files +5. **`src/schemas/compute-functions.ts`** - Exported compute functions + - `computeFilteredUsers` - Filters users by search query + - `computeStats` - Calculates user statistics (total, active, inactive) + - `computeTodoStats` - Calculates todo statistics (total, completed, remaining) + - `computeAddTodo` - Creates new todo item + - `updateFilterQuery` - Event handler for filter input + - `updateNewTodo` - Event handler for todo input + - `checkCanAddTodo` - Condition checker for add button + - `transformFilteredUsers` - Transform function for badge display + - `transformUserList` - Transform function for rendering user cards + +6. **`src/schemas/schema-loader.ts`** - Hydration utility + - `hydrateSchema()` - Converts JSON schemas to runtime schemas + - Replaces string function identifiers with actual functions + - Handles compute functions in dataSources, events, actions, and bindings + +## Updated Files + +### Component Files +- **`src/components/DashboardDemoPage.tsx`** + - Changed from importing TS schema to importing JSON + hydration + +- **`src/components/JSONUIShowcasePage.tsx`** + - Changed from importing TS schemas to importing JSON + hydration + +### Configuration +- **`tsconfig.json`** + - Added `"resolveJsonModule": true` to enable JSON imports + +### Documentation +- **`docs/ARCHITECTURE.md`** - Updated file structure and example code +- **`docs/JSON_UI_GUIDE.md`** - Updated references to schema files +- **`docs/IMPLEMENTATION_SUMMARY.md`** - Updated file list +- **`docs/JSON_UI_ENHANCEMENT_SUMMARY.md`** - Updated schema file name + +## How It Works + +### 1. JSON Schema Format +Compute functions are represented as string identifiers in JSON: +```json +{ + "id": "stats", + "type": "computed", + "compute": "computeStats", + "dependencies": ["users"] +} +``` + +### 2. Hydration Process +The `hydrateSchema()` function replaces string identifiers with actual functions: +```typescript +import { hydrateSchema } from '@/schemas/schema-loader' +import analyticsDashboardJson from '@/schemas/analytics-dashboard.json' + +const schema = hydrateSchema(analyticsDashboardJson) +``` + +### 3. Usage in Components +```typescript +export function DashboardDemoPage() { + return +} +``` + +## Benefits + +1. **Pure JSON** - Schemas are now pure JSON files, making them easier to: + - Store in databases + - Transmit over APIs + - Edit with JSON tools + - Version control and diff + +2. **Separation of Concerns** - Logic is separated from structure: + - JSON defines the UI structure + - TypeScript contains the compute logic + - Schema loader connects them at runtime + +3. **Type Safety** - TypeScript functions remain type-safe and testable + +4. **Maintainability** - Compute functions are centralized and reusable + +## Old Files (Can be removed) +- `src/schemas/dashboard-schema.ts` (replaced by `analytics-dashboard.json`) +- `src/schemas/page-schemas.ts` (split into 3 JSON files) + +Note: Keep `src/schemas/ui-schema.ts` as it contains Zod validation schemas, not UI schemas. + +## Testing +- Build completed successfully with `npm run build` +- All TypeScript errors resolved +- JSON imports working correctly diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index b1e9e52..f182df8 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -45,7 +45,10 @@ This project demonstrates a comprehensive JSON-driven UI architecture with atomi ```typescript import { PageRenderer } from '@/lib/json-ui/page-renderer' -import { dashboardSchema } from '@/schemas/dashboard-schema' +import { hydrateSchema } from '@/schemas/schema-loader' +import analyticsDashboardJson from '@/schemas/analytics-dashboard.json' + +const dashboardSchema = hydrateSchema(analyticsDashboardJson) export function DashboardPage() { return @@ -439,8 +442,12 @@ src/ │ ├── component-renderer.tsx │ └── component-registry.tsx ├── schemas/ # JSON page schemas -│ ├── dashboard-schema.ts -│ └── page-schemas.ts +│ ├── analytics-dashboard.json +│ ├── todo-list.json +│ ├── dashboard-simple.json +│ ├── new-molecules-showcase.json +│ ├── compute-functions.ts +│ └── schema-loader.ts └── types/ └── json-ui.ts # TypeScript types ``` diff --git a/docs/IMPLEMENTATION_SUMMARY.md b/docs/IMPLEMENTATION_SUMMARY.md index 7462fb6..4bba010 100644 --- a/docs/IMPLEMENTATION_SUMMARY.md +++ b/docs/IMPLEMENTATION_SUMMARY.md @@ -163,7 +163,12 @@ src/ │ └── json-ui/ │ └── component-registry.tsx [MODIFIED] ├── schemas/ -│ └── dashboard-schema.ts [NEW] +│ ├── analytics-dashboard.json [NEW] +│ ├── todo-list.json [NEW] +│ ├── dashboard-simple.json [NEW] +│ ├── new-molecules-showcase.json [NEW] +│ ├── compute-functions.ts [NEW] +│ └── schema-loader.ts [NEW] ├── types/ │ └── json-ui.ts [MODIFIED] ├── App.simple-json-demo.tsx [NEW] diff --git a/docs/JSON_UI_ENHANCEMENT_SUMMARY.md b/docs/JSON_UI_ENHANCEMENT_SUMMARY.md index 4f3bc81..200fe01 100644 --- a/docs/JSON_UI_ENHANCEMENT_SUMMARY.md +++ b/docs/JSON_UI_ENHANCEMENT_SUMMARY.md @@ -59,7 +59,7 @@ Enhanced the JSON-driven UI system by creating additional custom hooks, atomic c ## JSON Page Schema Created -### Analytics Dashboard Schema (/src/schemas/dashboard-schema.ts) +### Analytics Dashboard Schema (/src/schemas/analytics-dashboard.json) Comprehensive JSON-driven page demonstrating: - **Data Sources**: diff --git a/docs/JSON_UI_GUIDE.md b/docs/JSON_UI_GUIDE.md index c89d4bf..de33a1e 100644 --- a/docs/JSON_UI_GUIDE.md +++ b/docs/JSON_UI_GUIDE.md @@ -524,7 +524,7 @@ events: [{ ## Example: Complete Todo App -See `/src/schemas/page-schemas.ts` for a full working example with: +See `/src/schemas/todo-list.json` for a full working example with: - KV persistence - Computed statistics - CRUD operations @@ -577,7 +577,9 @@ See `/src/schemas/page-schemas.ts` for a full working example with: ## Resources - **Type Definitions**: `/src/types/json-ui.ts` -- **Page Schemas**: `/src/schemas/page-schemas.ts` +- **JSON Schemas**: `/src/schemas/*.json` +- **Compute Functions**: `/src/schemas/compute-functions.ts` +- **Schema Loader**: `/src/schemas/schema-loader.ts` - **Custom Hooks**: `/src/hooks/data/` and `/src/hooks/ui/` - **Atomic Components**: `/src/components/atoms/` - **Component Registry**: `/src/lib/json-ui/component-registry.ts` diff --git a/src/components/DashboardDemoPage.tsx b/src/components/DashboardDemoPage.tsx index e19d349..dd1585c 100644 --- a/src/components/DashboardDemoPage.tsx +++ b/src/components/DashboardDemoPage.tsx @@ -1,5 +1,8 @@ import { PageRenderer } from '@/lib/json-ui/page-renderer' -import { dashboardSchema } from '@/schemas/dashboard-schema' +import { hydrateSchema } from '@/schemas/schema-loader' +import analyticsDashboardJson from '@/schemas/analytics-dashboard.json' + +const dashboardSchema = hydrateSchema(analyticsDashboardJson) export function DashboardDemoPage() { return diff --git a/src/components/JSONUIShowcasePage.tsx b/src/components/JSONUIShowcasePage.tsx index 6b8d7ea..2b92f00 100644 --- a/src/components/JSONUIShowcasePage.tsx +++ b/src/components/JSONUIShowcasePage.tsx @@ -2,7 +2,12 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' import { AtomicComponentDemo } from '@/components/AtomicComponentDemo' import { DashboardDemoPage } from '@/components/DashboardDemoPage' import { PageRenderer } from '@/lib/json-ui/page-renderer' -import { todoListSchema, newMoleculesShowcaseSchema } from '@/schemas/page-schemas' +import { hydrateSchema } from '@/schemas/schema-loader' +import todoListJson from '@/schemas/todo-list.json' +import newMoleculesShowcaseJson from '@/schemas/new-molecules-showcase.json' + +const todoListSchema = hydrateSchema(todoListJson) +const newMoleculesShowcaseSchema = hydrateSchema(newMoleculesShowcaseJson) export function JSONUIShowcasePage() { return ( diff --git a/src/schemas/analytics-dashboard.json b/src/schemas/analytics-dashboard.json new file mode 100644 index 0000000..441f7fb --- /dev/null +++ b/src/schemas/analytics-dashboard.json @@ -0,0 +1,256 @@ +{ + "id": "analytics-dashboard", + "name": "Analytics Dashboard", + "layout": { + "type": "single" + }, + "dataSources": [ + { + "id": "users", + "type": "kv", + "key": "dashboard-users", + "defaultValue": [ + { "id": 1, "name": "Alice Johnson", "email": "alice@example.com", "status": "active", "joined": "2024-01-15" }, + { "id": 2, "name": "Bob Smith", "email": "bob@example.com", "status": "active", "joined": "2024-02-20" }, + { "id": 3, "name": "Charlie Brown", "email": "charlie@example.com", "status": "inactive", "joined": "2023-12-10" } + ] + }, + { + "id": "filterQuery", + "type": "static", + "defaultValue": "" + }, + { + "id": "filteredUsers", + "type": "computed", + "compute": "computeFilteredUsers", + "dependencies": ["users", "filterQuery"] + }, + { + "id": "stats", + "type": "computed", + "compute": "computeStats", + "dependencies": ["users"] + } + ], + "components": [ + { + "id": "root", + "type": "div", + "props": { + "className": "h-full overflow-auto p-6 bg-gradient-to-br from-background via-background to-accent/5" + }, + "children": [ + { + "id": "header", + "type": "div", + "props": { "className": "mb-8" }, + "children": [ + { + "id": "title", + "type": "Heading", + "props": { + "className": "text-4xl font-bold mb-2 bg-gradient-to-r from-primary to-accent bg-clip-text text-transparent", + "children": "Analytics Dashboard" + } + }, + { + "id": "subtitle", + "type": "Text", + "props": { + "className": "text-muted-foreground text-lg", + "children": "Monitor your user activity and key metrics" + } + } + ] + }, + { + "id": "metrics-row", + "type": "div", + "props": { "className": "grid grid-cols-1 md:grid-cols-3 gap-6 mb-8" }, + "children": [ + { + "id": "metric-total", + "type": "Card", + "props": { "className": "bg-gradient-to-br from-primary/10 to-primary/5 border-primary/20" }, + "children": [ + { + "id": "metric-total-content", + "type": "CardContent", + "props": { "className": "pt-6" }, + "children": [ + { + "id": "metric-total-label", + "type": "div", + "props": { "className": "text-sm font-medium text-muted-foreground mb-2", "children": "Total Users" } + }, + { + "id": "metric-total-value", + "type": "div", + "props": { "className": "text-4xl font-bold text-primary" }, + "bindings": { + "children": { "source": "stats", "path": "total" } + } + }, + { + "id": "metric-total-description", + "type": "div", + "props": { "className": "text-xs text-muted-foreground mt-2", "children": "Registered accounts" } + } + ] + } + ] + }, + { + "id": "metric-active", + "type": "Card", + "props": { "className": "bg-gradient-to-br from-green-500/10 to-green-500/5 border-green-500/20" }, + "children": [ + { + "id": "metric-active-content", + "type": "CardContent", + "props": { "className": "pt-6" }, + "children": [ + { + "id": "metric-active-label", + "type": "div", + "props": { "className": "text-sm font-medium text-muted-foreground mb-2", "children": "Active Users" } + }, + { + "id": "metric-active-value", + "type": "div", + "props": { "className": "text-4xl font-bold text-green-600" }, + "bindings": { + "children": { "source": "stats", "path": "active" } + } + }, + { + "id": "metric-active-description", + "type": "div", + "props": { "className": "text-xs text-muted-foreground mt-2", "children": "Currently engaged" } + } + ] + } + ] + }, + { + "id": "metric-inactive", + "type": "Card", + "props": { "className": "bg-gradient-to-br from-orange-500/10 to-orange-500/5 border-orange-500/20" }, + "children": [ + { + "id": "metric-inactive-content", + "type": "CardContent", + "props": { "className": "pt-6" }, + "children": [ + { + "id": "metric-inactive-label", + "type": "div", + "props": { "className": "text-sm font-medium text-muted-foreground mb-2", "children": "Inactive Users" } + }, + { + "id": "metric-inactive-value", + "type": "div", + "props": { "className": "text-4xl font-bold text-orange-600" }, + "bindings": { + "children": { "source": "stats", "path": "inactive" } + } + }, + { + "id": "metric-inactive-description", + "type": "div", + "props": { "className": "text-xs text-muted-foreground mt-2", "children": "Need re-engagement" } + } + ] + } + ] + } + ] + }, + { + "id": "users-section", + "type": "Card", + "props": { "className": "bg-card/50 backdrop-blur" }, + "children": [ + { + "id": "users-header", + "type": "CardHeader", + "children": [ + { + "id": "users-title-row", + "type": "div", + "props": { "className": "flex items-center justify-between" }, + "children": [ + { + "id": "users-title", + "type": "CardTitle", + "props": { "children": "User Directory" } + }, + { + "id": "users-badge", + "type": "Badge", + "props": { "variant": "secondary" }, + "bindings": { + "children": { + "source": "filteredUsers", + "transform": "transformFilteredUsers" + } + } + } + ] + }, + { + "id": "users-description", + "type": "CardDescription", + "props": { "children": "Manage and filter your user base" } + } + ] + }, + { + "id": "users-content", + "type": "CardContent", + "children": [ + { + "id": "filter-row", + "type": "div", + "props": { "className": "mb-6" }, + "children": [ + { + "id": "filter-input", + "type": "Input", + "props": { "placeholder": "Search users by name or email..." }, + "events": [ + { + "event": "onChange", + "actions": [ + { + "id": "update-filter", + "type": "set-value", + "target": "filterQuery", + "compute": "updateFilterQuery" + } + ] + } + ] + } + ] + }, + { + "id": "users-list", + "type": "div", + "props": { "className": "space-y-4" }, + "bindings": { + "children": { + "source": "filteredUsers", + "transform": "transformUserList" + } + } + } + ] + } + ] + } + ] + } + ] +} diff --git a/src/schemas/compute-functions.ts b/src/schemas/compute-functions.ts new file mode 100644 index 0000000..f34056e --- /dev/null +++ b/src/schemas/compute-functions.ts @@ -0,0 +1,88 @@ +export const computeFilteredUsers = (data: any) => { + const query = (data.filterQuery || '').toLowerCase() + if (!query) return data.users || [] + return (data.users || []).filter((user: any) => + user.name.toLowerCase().includes(query) || + user.email.toLowerCase().includes(query) + ) +} + +export const computeStats = (data: any) => ({ + total: data.users?.length || 0, + active: data.users?.filter((u: any) => u.status === 'active').length || 0, + inactive: data.users?.filter((u: any) => u.status === 'inactive').length || 0, +}) + +export const computeTodoStats = (data: any) => ({ + total: data.todos?.length || 0, + completed: data.todos?.filter((t: any) => t.completed).length || 0, + remaining: data.todos?.filter((t: any) => !t.completed).length || 0, +}) + +export const computeAddTodo = (data: any) => ({ + id: Date.now(), + text: data.newTodo, + completed: false, +}) + +export const updateFilterQuery = (_: any, event: any) => event.target.value + +export const updateNewTodo = (data: any, event: any) => event.target.value + +export const checkCanAddTodo = (data: any) => data.newTodo?.trim().length > 0 + +export const transformFilteredUsers = (users: any[]) => `${users.length} users` + +export const transformUserList = (users: any[]) => users.map((user: any) => ({ + type: 'Card', + id: `user-${user.id}`, + props: { + className: 'bg-background/50 hover:bg-background/80 transition-colors border-l-4 border-l-primary', + }, + children: [ + { + type: 'CardContent', + id: `user-content-${user.id}`, + props: { className: 'pt-6' }, + children: [ + { + type: 'div', + id: `user-row-${user.id}`, + props: { className: 'flex items-start justify-between' }, + children: [ + { + type: 'div', + id: `user-info-${user.id}`, + props: { className: 'flex-1' }, + children: [ + { + type: 'div', + id: `user-name-${user.id}`, + props: { className: 'font-semibold text-lg mb-1', children: user.name }, + }, + { + type: 'div', + id: `user-email-${user.id}`, + props: { className: 'text-sm text-muted-foreground', children: user.email }, + }, + { + type: 'div', + id: `user-joined-${user.id}`, + props: { className: 'text-xs text-muted-foreground mt-2', children: `Joined ${user.joined}` }, + }, + ], + }, + { + type: 'Badge', + id: `user-status-${user.id}`, + props: { + variant: user.status === 'active' ? 'default' : 'secondary', + children: user.status, + }, + }, + ], + }, + ], + }, + ], +})) diff --git a/src/schemas/dashboard-simple.json b/src/schemas/dashboard-simple.json new file mode 100644 index 0000000..c7c4428 --- /dev/null +++ b/src/schemas/dashboard-simple.json @@ -0,0 +1,23 @@ +{ + "id": "dashboard", + "name": "Dashboard", + "layout": { + "type": "grid", + "columns": 2, + "gap": 4 + }, + "dataSources": [ + { + "id": "stats", + "type": "static", + "defaultValue": { + "users": 1247, + "revenue": 45230, + "orders": 892, + "conversion": 3.2 + } + } + ], + "components": [], + "globalActions": [] +} diff --git a/src/schemas/new-molecules-showcase.json b/src/schemas/new-molecules-showcase.json new file mode 100644 index 0000000..53beb9e --- /dev/null +++ b/src/schemas/new-molecules-showcase.json @@ -0,0 +1,303 @@ +{ + "id": "new-molecules-showcase", + "name": "New Molecules Showcase", + "layout": { + "type": "single" + }, + "dataSources": [ + { + "id": "itemCount", + "type": "static", + "defaultValue": 42 + }, + { + "id": "isLoading", + "type": "static", + "defaultValue": false + } + ], + "components": [ + { + "id": "root", + "type": "div", + "props": { + "className": "h-full overflow-auto p-8 bg-background" + }, + "children": [ + { + "id": "page-header", + "type": "div", + "props": { "className": "mb-8" }, + "children": [ + { + "id": "page-title", + "type": "Heading", + "props": { + "level": 1, + "className": "text-4xl font-bold mb-2", + "children": "New JSON-Compatible Molecules" + } + }, + { + "id": "page-description", + "type": "Text", + "props": { + "className": "text-muted-foreground text-lg", + "children": "Showcasing the newly added molecular components" + } + } + ] + }, + { + "id": "showcase-grid", + "type": "Grid", + "props": { "cols": 2, "gap": "lg", "className": "max-w-5xl" }, + "children": [ + { + "id": "branding-card", + "type": "Card", + "children": [ + { + "id": "branding-header", + "type": "CardHeader", + "children": [ + { + "id": "branding-title", + "type": "CardTitle", + "props": { "children": "AppBranding" } + }, + { + "id": "branding-description", + "type": "CardDescription", + "props": { "children": "Application branding with logo, title, and subtitle" } + } + ] + }, + { + "id": "branding-content", + "type": "CardContent", + "children": [ + { + "id": "branding-demo", + "type": "AppBranding", + "props": { + "title": "My Amazing App", + "subtitle": "Built with JSON-Powered Components" + } + } + ] + } + ] + }, + { + "id": "label-badge-card", + "type": "Card", + "children": [ + { + "id": "label-badge-header", + "type": "CardHeader", + "children": [ + { + "id": "label-badge-title", + "type": "CardTitle", + "props": { "children": "LabelWithBadge" } + }, + { + "id": "label-badge-description", + "type": "CardDescription", + "props": { "children": "Label with optional badge indicator" } + } + ] + }, + { + "id": "label-badge-content", + "type": "CardContent", + "props": { "className": "space-y-3" }, + "children": [ + { + "id": "label-badge-demo-1", + "type": "LabelWithBadge", + "props": { + "label": "Total Items" + }, + "bindings": { + "badge": { "source": "itemCount" } + } + }, + { + "id": "label-badge-demo-2", + "type": "LabelWithBadge", + "props": { + "label": "Warning", + "badge": "3", + "badgeVariant": "destructive" + } + }, + { + "id": "label-badge-demo-3", + "type": "LabelWithBadge", + "props": { + "label": "Success", + "badge": "New", + "badgeVariant": "default" + } + } + ] + } + ] + }, + { + "id": "empty-state-card", + "type": "Card", + "children": [ + { + "id": "empty-state-header", + "type": "CardHeader", + "children": [ + { + "id": "empty-state-title", + "type": "CardTitle", + "props": { "children": "EmptyEditorState" } + }, + { + "id": "empty-state-description", + "type": "CardDescription", + "props": { "children": "Empty state display for editor contexts" } + } + ] + }, + { + "id": "empty-state-content", + "type": "CardContent", + "props": { "className": "h-48" }, + "children": [ + { + "id": "empty-state-demo", + "type": "EmptyEditorState", + "props": {} + } + ] + } + ] + }, + { + "id": "loading-states-card", + "type": "Card", + "children": [ + { + "id": "loading-states-header", + "type": "CardHeader", + "children": [ + { + "id": "loading-states-title", + "type": "CardTitle", + "props": { "children": "Loading States" } + }, + { + "id": "loading-states-description", + "type": "CardDescription", + "props": { "children": "LoadingFallback and LoadingState components" } + } + ] + }, + { + "id": "loading-states-content", + "type": "CardContent", + "props": { "className": "space-y-4" }, + "children": [ + { + "id": "loading-fallback-wrapper", + "type": "div", + "props": { "className": "h-24 border border-border rounded-md" }, + "children": [ + { + "id": "loading-fallback-demo", + "type": "LoadingFallback", + "props": { + "message": "Loading your data..." + } + } + ] + }, + { + "id": "loading-state-demo", + "type": "LoadingState", + "props": { + "message": "Processing request...", + "size": "sm" + } + } + ] + } + ] + }, + { + "id": "nav-header-card", + "type": "Card", + "props": { "className": "col-span-2" }, + "children": [ + { + "id": "nav-header-header", + "type": "CardHeader", + "children": [ + { + "id": "nav-header-title", + "type": "CardTitle", + "props": { "children": "NavigationGroupHeader" } + }, + { + "id": "nav-header-description", + "type": "CardDescription", + "props": { "children": "Collapsible navigation group header (Note: requires Collapsible wrapper in production)" } + } + ] + }, + { + "id": "nav-header-content", + "type": "CardContent", + "children": [ + { + "id": "nav-header-demo", + "type": "NavigationGroupHeader", + "props": { + "label": "Components", + "count": 24, + "isExpanded": true + } + } + ] + } + ] + } + ] + }, + { + "id": "info-section", + "type": "Alert", + "props": { + "className": "max-w-5xl mt-8" + }, + "children": [ + { + "id": "info-title", + "type": "div", + "props": { + "className": "font-semibold mb-2", + "children": "✅ Successfully Added to JSON Registry" + } + }, + { + "id": "info-text", + "type": "div", + "props": { + "className": "text-sm", + "children": "All components shown above are now available in the JSON UI component registry and can be used in JSON schemas." + } + } + ] + } + ] + } + ], + "globalActions": [] +} diff --git a/src/schemas/schema-loader.ts b/src/schemas/schema-loader.ts new file mode 100644 index 0000000..13f3867 --- /dev/null +++ b/src/schemas/schema-loader.ts @@ -0,0 +1,89 @@ +import { PageSchema } from '@/types/json-ui' +import * as computeFunctions from './compute-functions' + +type ComputeFunctionMap = typeof computeFunctions + +export function hydrateSchema(jsonSchema: any): PageSchema { + const schema = { ...jsonSchema } + + if (schema.dataSources) { + schema.dataSources = schema.dataSources.map((ds: any) => { + if (ds.type === 'computed' && typeof ds.compute === 'string') { + const functionName = ds.compute as keyof ComputeFunctionMap + const computeFunction = computeFunctions[functionName] + if (!computeFunction) { + console.warn(`Compute function "${functionName}" not found`) + } + return { + ...ds, + compute: computeFunction || (() => null) + } + } + return ds + }) + } + + if (schema.components) { + schema.components = hydrateComponents(schema.components) + } + + return schema as PageSchema +} + +function hydrateComponents(components: any[]): any[] { + return components.map(component => { + const hydratedComponent = { ...component } + + if (component.events) { + hydratedComponent.events = component.events.map((event: any) => { + const hydratedEvent = { ...event } + + if (event.condition && typeof event.condition === 'string') { + const functionName = event.condition as keyof ComputeFunctionMap + const conditionFunction = computeFunctions[functionName] + hydratedEvent.condition = conditionFunction || (() => false) + } + + if (event.actions) { + hydratedEvent.actions = event.actions.map((action: any) => { + if (action.compute && typeof action.compute === 'string') { + const functionName = action.compute as keyof ComputeFunctionMap + const computeFunction = computeFunctions[functionName] + return { + ...action, + compute: computeFunction || (() => null) + } + } + return action + }) + } + + return hydratedEvent + }) + } + + if (component.bindings) { + const hydratedBindings: Record = {} + for (const [key, binding] of Object.entries(component.bindings)) { + const b = binding as any + if (b.transform && typeof b.transform === 'string') { + const functionName = b.transform as keyof ComputeFunctionMap + const transformFunction = computeFunctions[functionName] + hydratedBindings[key] = { + ...b, + transform: transformFunction || ((x: any) => x) + } + } else { + hydratedBindings[key] = b + } + } + hydratedComponent.bindings = hydratedBindings + } + + if (component.children) { + hydratedComponent.children = hydrateComponents(component.children) + } + + return hydratedComponent + }) +} diff --git a/src/schemas/todo-list.json b/src/schemas/todo-list.json new file mode 100644 index 0000000..bd44f7a --- /dev/null +++ b/src/schemas/todo-list.json @@ -0,0 +1,255 @@ +{ + "id": "todo-list", + "name": "Todo List", + "layout": { + "type": "single" + }, + "dataSources": [ + { + "id": "todos", + "type": "kv", + "key": "app-todos", + "defaultValue": [ + { "id": 1, "text": "Learn JSON-driven UI", "completed": true }, + { "id": 2, "text": "Build atomic components", "completed": false }, + { "id": 3, "text": "Create custom hooks", "completed": false } + ] + }, + { + "id": "newTodo", + "type": "static", + "defaultValue": "" + }, + { + "id": "stats", + "type": "computed", + "compute": "computeTodoStats", + "dependencies": ["todos"] + } + ], + "components": [ + { + "id": "root", + "type": "div", + "props": { + "className": "h-full overflow-auto p-6 bg-gradient-to-br from-background via-background to-primary/5" + }, + "children": [ + { + "id": "header", + "type": "div", + "props": { "className": "mb-6" }, + "children": [ + { + "id": "title", + "type": "Heading", + "props": { + "className": "text-4xl font-bold mb-2 bg-gradient-to-r from-primary to-accent bg-clip-text text-transparent", + "children": "Task Manager" + } + }, + { + "id": "subtitle", + "type": "Text", + "props": { + "className": "text-muted-foreground", + "children": "Built entirely from JSON schema" + } + } + ] + }, + { + "id": "stats-row", + "type": "div", + "props": { "className": "grid grid-cols-1 md:grid-cols-3 gap-4 mb-6 max-w-3xl" }, + "children": [ + { + "id": "stat-total", + "type": "Card", + "props": { "className": "bg-card/50 backdrop-blur" }, + "children": [ + { + "id": "stat-total-content", + "type": "CardContent", + "props": { "className": "pt-6" }, + "children": [ + { + "id": "stat-total-label", + "type": "div", + "props": { "className": "text-sm text-muted-foreground mb-1", "children": "Total Tasks" } + }, + { + "id": "stat-total-value", + "type": "div", + "props": { "className": "text-3xl font-bold" }, + "bindings": { + "children": { "source": "stats", "path": "total" } + } + } + ] + } + ] + }, + { + "id": "stat-completed", + "type": "Card", + "props": { "className": "bg-accent/10 backdrop-blur border-accent/20" }, + "children": [ + { + "id": "stat-completed-content", + "type": "CardContent", + "props": { "className": "pt-6" }, + "children": [ + { + "id": "stat-completed-label", + "type": "div", + "props": { "className": "text-sm text-muted-foreground mb-1", "children": "Completed" } + }, + { + "id": "stat-completed-value", + "type": "div", + "props": { "className": "text-3xl font-bold text-accent" }, + "bindings": { + "children": { "source": "stats", "path": "completed" } + } + } + ] + } + ] + }, + { + "id": "stat-remaining", + "type": "Card", + "props": { "className": "bg-primary/5 backdrop-blur border-primary/20" }, + "children": [ + { + "id": "stat-remaining-content", + "type": "CardContent", + "props": { "className": "pt-6" }, + "children": [ + { + "id": "stat-remaining-label", + "type": "div", + "props": { "className": "text-sm text-muted-foreground mb-1", "children": "Remaining" } + }, + { + "id": "stat-remaining-value", + "type": "div", + "props": { "className": "text-3xl font-bold text-primary" }, + "bindings": { + "children": { "source": "stats", "path": "remaining" } + } + } + ] + } + ] + } + ] + }, + { + "id": "main-card", + "type": "Card", + "props": { "className": "max-w-3xl" }, + "children": [ + { + "id": "card-header", + "type": "CardHeader", + "children": [ + { + "id": "card-title", + "type": "CardTitle", + "props": { "children": "Your Tasks" } + }, + { + "id": "card-description", + "type": "CardDescription", + "props": { "children": "Manage your daily tasks efficiently" } + } + ] + }, + { + "id": "card-content", + "type": "CardContent", + "props": { "className": "space-y-4" }, + "children": [ + { + "id": "input-group", + "type": "div", + "props": { "className": "flex gap-2" }, + "children": [ + { + "id": "todo-input", + "type": "Input", + "props": { + "placeholder": "What needs to be done?" + }, + "bindings": { + "value": { "source": "newTodo" } + }, + "events": [ + { + "event": "change", + "actions": [ + { + "id": "update-input", + "type": "set-value", + "target": "newTodo", + "compute": "updateNewTodo" + } + ] + } + ] + }, + { + "id": "add-button", + "type": "Button", + "props": { "children": "Add Task" }, + "events": [ + { + "event": "click", + "actions": [ + { + "id": "add-todo", + "type": "create", + "target": "todos", + "compute": "computeAddTodo" + }, + { + "id": "clear-input", + "type": "set-value", + "target": "newTodo", + "value": "" + }, + { + "id": "show-success", + "type": "show-toast", + "message": "Task added successfully!", + "variant": "success" + } + ], + "condition": "checkCanAddTodo" + } + ] + } + ] + }, + { + "id": "separator", + "type": "Separator", + "props": { "className": "my-4" } + }, + { + "id": "todo-list", + "type": "div", + "props": { "className": "space-y-2" }, + "children": [] + } + ] + } + ] + } + ] + } + ], + "globalActions": [] +} diff --git a/tsconfig.json b/tsconfig.json index 9b370cd..f98bcad 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,6 +11,7 @@ "module": "ESNext", "skipLibCheck": true, "strictNullChecks": true, + "resolveJsonModule": true, /* Bundler mode */ "moduleResolution": "bundler", "allowImportingTsExtensions": true,