Generated by Spark: Load more of UI from JSON declarations

This commit is contained in:
2026-01-17 10:22:57 +00:00
committed by GitHub
parent eb895f70f5
commit 1176959d4a
22 changed files with 4545 additions and 0 deletions

View File

@@ -0,0 +1,147 @@
import { useState, useEffect } from 'react'
import { JSONUIRenderer } from '@/lib/json-ui/renderer'
import { UIComponent, EventHandler, Layout } from '@/lib/json-ui/schema'
import { toast } from 'sonner'
interface JSONUIPageProps {
jsonConfig: any
}
export function JSONUIPage({ jsonConfig }: JSONUIPageProps) {
const [dataMap, setDataMap] = useState<Record<string, any>>({})
useEffect(() => {
if (jsonConfig.dataSources) {
const initialData: Record<string, any> = {}
Object.entries(jsonConfig.dataSources).forEach(([key, source]: [string, any]) => {
if (source.type === 'static') {
initialData[key] = source.config
}
})
setDataMap(initialData)
}
}, [jsonConfig])
const updateDataField = (source: string, field: string, value: any) => {
setDataMap((prev) => ({
...prev,
[source]: {
...prev[source],
[field]: value,
},
}))
}
const handleAction = (handler: EventHandler, event?: any) => {
console.log('Action triggered:', handler.action, handler.params, event)
switch (handler.action) {
case 'refresh-data':
toast.success('Data refreshed')
break
case 'create-project':
toast.info('Create project clicked')
break
case 'deploy':
toast.info('Deploy clicked')
break
case 'view-logs':
toast.info('View logs clicked')
break
case 'settings':
toast.info('Settings clicked')
break
case 'add-project':
toast.info('Add project clicked')
break
case 'view-project':
toast.info(`View project: ${handler.params?.projectId}`)
break
case 'edit-project':
toast.info(`Edit project: ${handler.params?.projectId}`)
break
case 'delete-project':
toast.error(`Delete project: ${handler.params?.projectId}`)
break
case 'update-field':
if (event?.target) {
const { name, value } = event.target
updateDataField('formData', name, value)
}
break
case 'update-checkbox':
if (handler.params?.field) {
updateDataField('formData', handler.params.field, event)
}
break
case 'submit-form':
toast.success('Form submitted!')
console.log('Form data:', dataMap.formData)
break
case 'cancel-form':
toast.info('Form cancelled')
break
case 'toggle-dark-mode':
updateDataField('settings', 'darkMode', event)
toast.success(`Dark mode ${event ? 'enabled' : 'disabled'}`)
break
case 'toggle-auto-save':
updateDataField('settings', 'autoSave', event)
toast.success(`Auto-save ${event ? 'enabled' : 'disabled'}`)
break
case 'toggle-email-notifications':
updateDataField('notifications', 'email', event)
toast.success(`Email notifications ${event ? 'enabled' : 'disabled'}`)
break
case 'toggle-push-notifications':
updateDataField('notifications', 'push', event)
toast.success(`Push notifications ${event ? 'enabled' : 'disabled'}`)
break
case 'toggle-2fa':
updateDataField('security', 'twoFactor', event)
toast.success(`Two-factor auth ${event ? 'enabled' : 'disabled'}`)
break
case 'logout-all-sessions':
toast.success('All other sessions logged out')
break
case 'save-settings':
toast.success('Settings saved successfully')
console.log('Settings:', dataMap)
break
case 'reset-settings':
toast.info('Settings reset to defaults')
break
default:
console.log('Unhandled action:', handler.action)
}
}
if (!jsonConfig.layout) {
return <div className="p-6 text-muted-foreground">No layout defined</div>
}
const layoutComponent: UIComponent = {
id: jsonConfig.layout.type || 'root-layout',
type: 'div',
className: jsonConfig.layout.className,
style: {
display: jsonConfig.layout.type === 'flex' ? 'flex' : 'block',
flexDirection: jsonConfig.layout.direction === 'column' ? 'column' : 'row',
gap: jsonConfig.layout.gap ? `${jsonConfig.layout.gap * 0.25}rem` : undefined,
padding: jsonConfig.layout.padding ? `${jsonConfig.layout.padding * 0.25}rem` : undefined,
},
children: jsonConfig.layout.children || [],
}
return (
<div className="h-full w-full overflow-auto">
<JSONUIRenderer
component={layoutComponent}
dataMap={dataMap}
onAction={handleAction}
/>
</div>
)
}

View File

@@ -0,0 +1,138 @@
import { useState } from 'react'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { JSONUIPage } from '@/components/JSONUIPage'
import { Separator } from '@/components/ui/separator'
import { Badge } from '@/components/ui/badge'
import dashboardExample from '@/config/ui-examples/dashboard.json'
import formExample from '@/config/ui-examples/form.json'
import tableExample from '@/config/ui-examples/table.json'
import settingsExample from '@/config/ui-examples/settings.json'
import { FileCode, Eye, Code, ChartBar, ListBullets, Table, Gear } from '@phosphor-icons/react'
export function JSONUIShowcase() {
const [selectedExample, setSelectedExample] = useState('dashboard')
const [showJSON, setShowJSON] = useState(false)
const examples = {
dashboard: {
name: 'Dashboard',
description: 'Complete dashboard with stats, activity feed, and quick actions',
icon: ChartBar,
config: dashboardExample,
},
form: {
name: 'Form',
description: 'Dynamic form with validation and data binding',
icon: ListBullets,
config: formExample,
},
table: {
name: 'Data Table',
description: 'Interactive table with row actions and looping',
icon: Table,
config: tableExample,
},
settings: {
name: 'Settings',
description: 'Tabbed settings panel with switches and selections',
icon: Gear,
config: settingsExample,
},
}
const currentExample = examples[selectedExample as keyof typeof examples]
return (
<div className="h-full flex flex-col bg-background">
<div className="border-b border-border bg-card px-6 py-4">
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold">JSON UI System</h1>
<p className="text-sm text-muted-foreground mt-1">
Build complex UIs from declarative JSON configurations
</p>
</div>
<Badge variant="secondary" className="font-mono">
EXPERIMENTAL
</Badge>
</div>
</div>
<div className="flex-1 overflow-hidden">
<Tabs value={selectedExample} onValueChange={setSelectedExample} className="h-full flex flex-col">
<div className="border-b border-border bg-muted/30 px-6">
<div className="flex items-center justify-between">
<TabsList className="bg-transparent border-0">
{Object.entries(examples).map(([key, example]) => {
const Icon = example.icon
return (
<TabsTrigger key={key} value={key} className="gap-2">
<Icon size={16} />
{example.name}
</TabsTrigger>
)
})}
</TabsList>
<Button
variant="outline"
size="sm"
onClick={() => setShowJSON(!showJSON)}
className="gap-2"
>
{showJSON ? <Eye size={16} /> : <Code size={16} />}
{showJSON ? 'Show Preview' : 'Show JSON'}
</Button>
</div>
</div>
<div className="flex-1 overflow-auto">
{Object.entries(examples).map(([key, example]) => (
<TabsContent key={key} value={key} className="h-full m-0">
{showJSON ? (
<div className="p-6">
<Card>
<CardHeader>
<CardTitle className="text-lg">JSON Configuration</CardTitle>
<CardDescription>
{example.description}
</CardDescription>
</CardHeader>
<CardContent>
<pre className="bg-muted p-4 rounded-lg overflow-auto text-sm max-h-[600px]">
<code>{JSON.stringify(example.config, null, 2)}</code>
</pre>
</CardContent>
</Card>
</div>
) : (
<JSONUIPage jsonConfig={example.config} />
)}
</TabsContent>
))}
</div>
</Tabs>
</div>
<div className="border-t border-border bg-card px-6 py-3">
<div className="flex items-center gap-6 text-xs text-muted-foreground">
<div className="flex items-center gap-2">
<div className="w-2 h-2 rounded-full bg-green-500" />
<span>Fully declarative - no React code needed</span>
</div>
<Separator orientation="vertical" className="h-4" />
<div className="flex items-center gap-2">
<div className="w-2 h-2 rounded-full bg-blue-500" />
<span>Data binding with automatic updates</span>
</div>
<Separator orientation="vertical" className="h-4" />
<div className="flex items-center gap-2">
<div className="w-2 h-2 rounded-full bg-purple-500" />
<span>Event handlers and actions</span>
</div>
</div>
</div>
</div>
)
}

View File

@@ -3,3 +3,5 @@ export * from './molecules'
export * from './organisms'
export * from './TemplateSelector'
export * from './TemplateExplorer'
export * from './JSONUIShowcase'
export * from './JSONUIPage'

View File

@@ -26,6 +26,7 @@ import { FeatureToggleSettings } from '@/components/FeatureToggleSettings'
import { PWASettings } from '@/components/PWASettings'
import { FaviconDesigner } from '@/components/FaviconDesigner'
import { FeatureIdeaCloud } from '@/components/FeatureIdeaCloud'
import { JSONUIShowcase } from '@/components/JSONUIShowcase'
export const ComponentRegistry: Record<string, ComponentType<any>> = {
Button,
@@ -59,6 +60,7 @@ export const ComponentRegistry: Record<string, ComponentType<any>> = {
PWASettings,
FaviconDesigner,
FeatureIdeaCloud,
JSONUIShowcase,
}
export function getComponent(name: string): ComponentType<any> | null {

View File

@@ -273,6 +273,15 @@
"state": ["features:featureToggles"],
"actions": ["onFeaturesChange:setFeatureToggles"]
}
},
{
"id": "json-ui",
"title": "JSON UI",
"icon": "Code",
"component": "JSONUIShowcase",
"enabled": true,
"order": 22,
"props": {}
}
]
}

View File

@@ -0,0 +1,119 @@
# JSON UI Examples
This directory contains example JSON configurations that demonstrate the capabilities of the JSON UI system.
## Available Examples
### 1. Dashboard (`dashboard.json`)
A complete dashboard interface featuring:
- **Stats Cards**: Display key metrics with data binding
- **Activity Feed**: Shows recent activities using list looping
- **Quick Actions**: Grid of action buttons with click handlers
- **Static Data Sources**: Demonstrates hardcoded data in JSON
**Key Features Demonstrated:**
- Component composition with Cards
- Data binding to show dynamic values
- Event handlers for user interactions
- Grid layouts with responsive classes
- Icon components integration
### 2. Form (`form.json`)
A user registration form showcasing:
- **Text Inputs**: Name, email, password fields
- **Textarea**: Multi-line bio input
- **Checkbox**: Newsletter subscription
- **Form Actions**: Submit and cancel buttons
- **Data Binding**: Two-way binding for all form fields
**Key Features Demonstrated:**
- Form field components
- Input validation attributes
- onChange event handling
- Form data management
- Label-input associations
### 3. Data Table (`table.json`)
An interactive projects table with:
- **Table Structure**: Header and body rows
- **List Looping**: Dynamic rows from array data
- **Status Badges**: Visual status indicators
- **Row Actions**: View, edit, and delete buttons per row
- **Action Parameters**: Pass row data to event handlers
**Key Features Demonstrated:**
- Table components (TableHeader, TableBody, TableRow, TableCell)
- Loop rendering with itemVar and indexVar
- Badge components for status
- Icon buttons for actions
- Event handlers with dynamic parameters
### 4. Settings (`settings.json`)
A comprehensive settings panel featuring:
- **Tabbed Interface**: General, Notifications, Security tabs
- **Switch Toggles**: Enable/disable features
- **Select Dropdown**: Language selection
- **Multiple Data Sources**: Separate sources for each tab
- **Settings Persistence**: Save and reset functionality
**Key Features Demonstrated:**
- Tabs component with multiple TabsContent
- Switch components with data binding
- Select components with options
- Separator components for visual organization
- Multiple independent data sources
## How to Use These Examples
1. **View in the UI**: Navigate to the "JSON UI" page in the application to see live previews
2. **Toggle JSON View**: Click the "Show JSON" button to see the configuration
3. **Copy and Modify**: Use these as templates for your own UI configurations
4. **Learn by Example**: Each example builds on concepts from the previous ones
## Creating Your Own
To create a new JSON UI:
1. Create a new `.json` file in this directory
2. Follow the structure from existing examples
3. Import it in `JSONUIShowcase.tsx`:
```typescript
import myExample from '@/config/ui-examples/my-example.json'
```
4. Add it to the examples object:
```typescript
myExample: {
name: 'My Example',
description: 'Description here',
icon: IconComponent,
config: myExample,
}
```
## JSON Structure Reference
Each JSON file should have:
- `id`: Unique identifier
- `title`: Display title
- `description`: Brief description
- `layout`: Root layout configuration
- `type`: Layout type (flex, grid, etc.)
- `children`: Array of child components
- `dataSources`: Data sources configuration
- `actions` (optional): Action definitions
## Best Practices
1. **Start Simple**: Begin with basic layouts before adding complexity
2. **Use Semantic IDs**: Give components meaningful, descriptive IDs
3. **Test Data First**: Start with static data sources before moving to API/KV
4. **Incremental Development**: Add features one at a time
5. **Refer to Documentation**: See `/docs/JSON-UI-SYSTEM.md` for complete reference
## Tips
- Use the existing examples as starting points
- Keep component trees shallow for better performance
- Leverage Tailwind classes for styling
- Use data binding instead of hardcoded values
- Group related settings in separate data sources

View File

@@ -0,0 +1,437 @@
{
"id": "dashboard-ui",
"title": "Dashboard",
"description": "Application dashboard with stats and recent activity",
"layout": {
"type": "flex",
"direction": "column",
"gap": "6",
"padding": "6",
"className": "h-full bg-background",
"children": [
{
"id": "header-section",
"type": "div",
"className": "flex justify-between items-center",
"children": [
{
"id": "page-title",
"type": "h1",
"className": "text-3xl font-bold",
"children": "Dashboard"
},
{
"id": "refresh-button",
"type": "Button",
"props": {
"variant": "outline",
"size": "sm"
},
"events": {
"onClick": "refresh-data"
},
"children": [
{
"id": "refresh-icon",
"type": "RefreshCw",
"props": {
"size": 16
}
}
]
}
]
},
{
"id": "stats-grid",
"type": "div",
"className": "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4",
"children": [
{
"id": "stat-card-1",
"type": "Card",
"children": [
{
"id": "stat-header-1",
"type": "CardHeader",
"children": [
{
"id": "stat-title-1",
"type": "CardTitle",
"className": "text-sm font-medium text-muted-foreground",
"children": "Total Users"
}
]
},
{
"id": "stat-content-1",
"type": "CardContent",
"children": [
{
"id": "stat-value-1",
"type": "p",
"className": "text-3xl font-bold",
"dataBinding": "stats.users",
"children": "0"
}
]
}
]
},
{
"id": "stat-card-2",
"type": "Card",
"children": [
{
"id": "stat-header-2",
"type": "CardHeader",
"children": [
{
"id": "stat-title-2",
"type": "CardTitle",
"className": "text-sm font-medium text-muted-foreground",
"children": "Active Projects"
}
]
},
{
"id": "stat-content-2",
"type": "CardContent",
"children": [
{
"id": "stat-value-2",
"type": "p",
"className": "text-3xl font-bold",
"dataBinding": "stats.projects",
"children": "0"
}
]
}
]
},
{
"id": "stat-card-3",
"type": "Card",
"children": [
{
"id": "stat-header-3",
"type": "CardHeader",
"children": [
{
"id": "stat-title-3",
"type": "CardTitle",
"className": "text-sm font-medium text-muted-foreground",
"children": "Deployments"
}
]
},
{
"id": "stat-content-3",
"type": "CardContent",
"children": [
{
"id": "stat-value-3",
"type": "p",
"className": "text-3xl font-bold",
"dataBinding": "stats.deployments",
"children": "0"
}
]
}
]
},
{
"id": "stat-card-4",
"type": "Card",
"children": [
{
"id": "stat-header-4",
"type": "CardHeader",
"children": [
{
"id": "stat-title-4",
"type": "CardTitle",
"className": "text-sm font-medium text-muted-foreground",
"children": "Success Rate"
}
]
},
{
"id": "stat-content-4",
"type": "CardContent",
"children": [
{
"id": "stat-value-4",
"type": "p",
"className": "text-3xl font-bold",
"dataBinding": "stats.successRate",
"children": "0%"
}
]
}
]
}
]
},
{
"id": "content-section",
"type": "div",
"className": "grid grid-cols-1 lg:grid-cols-2 gap-6",
"children": [
{
"id": "recent-activity-card",
"type": "Card",
"children": [
{
"id": "activity-header",
"type": "CardHeader",
"children": [
{
"id": "activity-title",
"type": "CardTitle",
"children": "Recent Activity"
},
{
"id": "activity-description",
"type": "CardDescription",
"children": "Latest updates from your projects"
}
]
},
{
"id": "activity-content",
"type": "CardContent",
"children": [
{
"id": "activity-list",
"type": "div",
"className": "space-y-4",
"loop": {
"source": "activities",
"itemVar": "activity",
"indexVar": "index"
},
"children": [
{
"id": "activity-item",
"type": "div",
"className": "flex items-start gap-3 pb-4 border-b last:border-0 last:pb-0",
"children": [
{
"id": "activity-icon",
"type": "div",
"className": "p-2 rounded-lg bg-primary/10",
"children": [
{
"id": "activity-icon-glyph",
"type": "Info",
"props": {
"size": 16
},
"className": "text-primary"
}
]
},
{
"id": "activity-details",
"type": "div",
"className": "flex-1",
"children": [
{
"id": "activity-text",
"type": "p",
"className": "text-sm font-medium",
"dataBinding": "activity.text"
},
{
"id": "activity-time",
"type": "p",
"className": "text-xs text-muted-foreground",
"dataBinding": "activity.time"
}
]
}
]
}
]
}
]
}
]
},
{
"id": "quick-actions-card",
"type": "Card",
"children": [
{
"id": "actions-header",
"type": "CardHeader",
"children": [
{
"id": "actions-title",
"type": "CardTitle",
"children": "Quick Actions"
},
{
"id": "actions-description",
"type": "CardDescription",
"children": "Common tasks and operations"
}
]
},
{
"id": "actions-content",
"type": "CardContent",
"children": [
{
"id": "actions-grid",
"type": "div",
"className": "grid grid-cols-2 gap-3",
"children": [
{
"id": "action-button-1",
"type": "Button",
"props": {
"variant": "outline"
},
"className": "h-24 flex-col gap-2",
"events": {
"onClick": "create-project"
},
"children": [
{
"id": "action-icon-1",
"type": "Plus",
"props": {
"size": 24
}
},
{
"id": "action-label-1",
"type": "span",
"className": "text-sm",
"children": "New Project"
}
]
},
{
"id": "action-button-2",
"type": "Button",
"props": {
"variant": "outline"
},
"className": "h-24 flex-col gap-2",
"events": {
"onClick": "deploy"
},
"children": [
{
"id": "action-icon-2",
"type": "Upload",
"props": {
"size": 24
}
},
{
"id": "action-label-2",
"type": "span",
"className": "text-sm",
"children": "Deploy"
}
]
},
{
"id": "action-button-3",
"type": "Button",
"props": {
"variant": "outline"
},
"className": "h-24 flex-col gap-2",
"events": {
"onClick": "view-logs"
},
"children": [
{
"id": "action-icon-3",
"type": "Eye",
"props": {
"size": 24
}
},
{
"id": "action-label-3",
"type": "span",
"className": "text-sm",
"children": "View Logs"
}
]
},
{
"id": "action-button-4",
"type": "Button",
"props": {
"variant": "outline"
},
"className": "h-24 flex-col gap-2",
"events": {
"onClick": "settings"
},
"children": [
{
"id": "action-icon-4",
"type": "Settings",
"props": {
"size": 24
}
},
{
"id": "action-label-4",
"type": "span",
"className": "text-sm",
"children": "Settings"
}
]
}
]
}
]
}
]
}
]
}
]
},
"dataSources": {
"stats": {
"type": "static",
"config": {
"users": 1234,
"projects": 45,
"deployments": 789,
"successRate": "98.5%"
}
},
"activities": {
"type": "static",
"config": [
{
"text": "Deployed project to production",
"time": "2 minutes ago"
},
{
"text": "Created new component tree",
"time": "15 minutes ago"
},
{
"text": "Updated model schema",
"time": "1 hour ago"
},
{
"text": "Ran unit tests successfully",
"time": "2 hours ago"
}
]
}
}
}

View File

@@ -0,0 +1,241 @@
{
"id": "form-builder-ui",
"title": "Form Builder",
"description": "Dynamic form example with validation",
"layout": {
"type": "flex",
"direction": "column",
"gap": "6",
"padding": "6",
"className": "h-full bg-background max-w-2xl mx-auto",
"children": [
{
"id": "form-header",
"type": "div",
"children": [
{
"id": "form-title",
"type": "h1",
"className": "text-3xl font-bold mb-2",
"children": "User Registration"
},
{
"id": "form-description",
"type": "p",
"className": "text-muted-foreground",
"children": "Create a new account by filling out the form below"
}
]
},
{
"id": "form-card",
"type": "Card",
"children": [
{
"id": "form-card-content",
"type": "CardContent",
"className": "pt-6",
"children": [
{
"id": "user-form",
"type": "div",
"className": "space-y-4",
"children": [
{
"id": "name-field",
"type": "div",
"className": "space-y-2",
"children": [
{
"id": "name-label",
"type": "Label",
"props": {
"htmlFor": "name"
},
"children": "Full Name"
},
{
"id": "name-input",
"type": "Input",
"props": {
"id": "name",
"name": "name",
"placeholder": "John Doe",
"required": true
},
"dataBinding": "formData.name",
"events": {
"onChange": "update-field"
}
}
]
},
{
"id": "email-field",
"type": "div",
"className": "space-y-2",
"children": [
{
"id": "email-label",
"type": "Label",
"props": {
"htmlFor": "email"
},
"children": "Email Address"
},
{
"id": "email-input",
"type": "Input",
"props": {
"id": "email",
"name": "email",
"type": "email",
"placeholder": "john@example.com",
"required": true
},
"dataBinding": "formData.email",
"events": {
"onChange": "update-field"
}
}
]
},
{
"id": "password-field",
"type": "div",
"className": "space-y-2",
"children": [
{
"id": "password-label",
"type": "Label",
"props": {
"htmlFor": "password"
},
"children": "Password"
},
{
"id": "password-input",
"type": "Input",
"props": {
"id": "password",
"name": "password",
"type": "password",
"placeholder": "••••••••",
"required": true
},
"dataBinding": "formData.password",
"events": {
"onChange": "update-field"
}
}
]
},
{
"id": "bio-field",
"type": "div",
"className": "space-y-2",
"children": [
{
"id": "bio-label",
"type": "Label",
"props": {
"htmlFor": "bio"
},
"children": "Bio"
},
{
"id": "bio-input",
"type": "Textarea",
"props": {
"id": "bio",
"name": "bio",
"placeholder": "Tell us about yourself...",
"rows": 4
},
"dataBinding": "formData.bio",
"events": {
"onChange": "update-field"
}
}
]
},
{
"id": "subscribe-field",
"type": "div",
"className": "flex items-center gap-2",
"children": [
{
"id": "subscribe-checkbox",
"type": "Checkbox",
"props": {
"id": "subscribe",
"name": "subscribe"
},
"dataBinding": "formData.subscribe",
"events": {
"onCheckedChange": "update-checkbox"
}
},
{
"id": "subscribe-label",
"type": "Label",
"props": {
"htmlFor": "subscribe"
},
"className": "text-sm font-normal cursor-pointer",
"children": "Subscribe to newsletter"
}
]
},
{
"id": "form-actions",
"type": "div",
"className": "flex gap-3 pt-2",
"children": [
{
"id": "submit-button",
"type": "Button",
"props": {
"type": "submit"
},
"className": "flex-1",
"events": {
"onClick": "submit-form"
},
"children": "Create Account"
},
{
"id": "cancel-button",
"type": "Button",
"props": {
"variant": "outline"
},
"className": "flex-1",
"events": {
"onClick": "cancel-form"
},
"children": "Cancel"
}
]
}
]
}
]
}
]
}
]
},
"dataSources": {
"formData": {
"type": "static",
"config": {
"name": "",
"email": "",
"password": "",
"bio": "",
"subscribe": false
}
}
}
}

View File

@@ -0,0 +1,526 @@
{
"id": "settings-ui",
"title": "Settings",
"description": "Application settings panel",
"layout": {
"type": "flex",
"direction": "column",
"gap": "6",
"padding": "6",
"className": "h-full bg-background max-w-4xl mx-auto",
"children": [
{
"id": "settings-header",
"type": "div",
"children": [
{
"id": "settings-title",
"type": "h1",
"className": "text-3xl font-bold mb-2",
"children": "Settings"
},
{
"id": "settings-description",
"type": "p",
"className": "text-muted-foreground",
"children": "Manage your application preferences and account settings"
}
]
},
{
"id": "settings-tabs",
"type": "Tabs",
"props": {
"defaultValue": "general"
},
"className": "w-full",
"children": [
{
"id": "tabs-list",
"type": "TabsList",
"className": "grid w-full grid-cols-3",
"children": [
{
"id": "tab-general",
"type": "TabsTrigger",
"props": {
"value": "general"
},
"children": "General"
},
{
"id": "tab-notifications",
"type": "TabsTrigger",
"props": {
"value": "notifications"
},
"children": "Notifications"
},
{
"id": "tab-security",
"type": "TabsTrigger",
"props": {
"value": "security"
},
"children": "Security"
}
]
},
{
"id": "tab-content-general",
"type": "TabsContent",
"props": {
"value": "general"
},
"children": [
{
"id": "general-card",
"type": "Card",
"children": [
{
"id": "general-header",
"type": "CardHeader",
"children": [
{
"id": "general-title",
"type": "CardTitle",
"children": "General Settings"
},
{
"id": "general-description",
"type": "CardDescription",
"children": "Configure basic application preferences"
}
]
},
{
"id": "general-content",
"type": "CardContent",
"className": "space-y-6",
"children": [
{
"id": "theme-setting",
"type": "div",
"className": "flex items-center justify-between",
"children": [
{
"id": "theme-info",
"type": "div",
"children": [
{
"id": "theme-label",
"type": "Label",
"className": "text-base",
"children": "Dark Mode"
},
{
"id": "theme-desc",
"type": "p",
"className": "text-sm text-muted-foreground",
"children": "Use dark theme across the application"
}
]
},
{
"id": "theme-switch",
"type": "Switch",
"dataBinding": "settings.darkMode",
"events": {
"onCheckedChange": "toggle-dark-mode"
}
}
]
},
{
"id": "divider-1",
"type": "Separator"
},
{
"id": "language-setting",
"type": "div",
"className": "flex items-center justify-between",
"children": [
{
"id": "language-info",
"type": "div",
"children": [
{
"id": "language-label",
"type": "Label",
"className": "text-base",
"children": "Language"
},
{
"id": "language-desc",
"type": "p",
"className": "text-sm text-muted-foreground",
"children": "Select your preferred language"
}
]
},
{
"id": "language-select",
"type": "Select",
"dataBinding": "settings.language",
"props": {
"defaultValue": "en"
},
"children": [
{
"id": "language-trigger",
"type": "SelectTrigger",
"className": "w-[180px]",
"children": [
{
"id": "language-value",
"type": "SelectValue",
"props": {
"placeholder": "Select language"
}
}
]
},
{
"id": "language-content",
"type": "SelectContent",
"children": [
{
"id": "lang-en",
"type": "SelectItem",
"props": {
"value": "en"
},
"children": "English"
},
{
"id": "lang-es",
"type": "SelectItem",
"props": {
"value": "es"
},
"children": "Español"
},
{
"id": "lang-fr",
"type": "SelectItem",
"props": {
"value": "fr"
},
"children": "Français"
}
]
}
]
}
]
},
{
"id": "divider-2",
"type": "Separator"
},
{
"id": "auto-save-setting",
"type": "div",
"className": "flex items-center justify-between",
"children": [
{
"id": "auto-save-info",
"type": "div",
"children": [
{
"id": "auto-save-label",
"type": "Label",
"className": "text-base",
"children": "Auto-save"
},
{
"id": "auto-save-desc",
"type": "p",
"className": "text-sm text-muted-foreground",
"children": "Automatically save changes"
}
]
},
{
"id": "auto-save-switch",
"type": "Switch",
"dataBinding": "settings.autoSave",
"events": {
"onCheckedChange": "toggle-auto-save"
}
}
]
}
]
}
]
}
]
},
{
"id": "tab-content-notifications",
"type": "TabsContent",
"props": {
"value": "notifications"
},
"children": [
{
"id": "notifications-card",
"type": "Card",
"children": [
{
"id": "notifications-header",
"type": "CardHeader",
"children": [
{
"id": "notifications-title",
"type": "CardTitle",
"children": "Notification Preferences"
},
{
"id": "notifications-description",
"type": "CardDescription",
"children": "Choose what notifications you want to receive"
}
]
},
{
"id": "notifications-content",
"type": "CardContent",
"className": "space-y-6",
"children": [
{
"id": "email-notif",
"type": "div",
"className": "flex items-center justify-between",
"children": [
{
"id": "email-notif-info",
"type": "div",
"children": [
{
"id": "email-notif-label",
"type": "Label",
"className": "text-base",
"children": "Email Notifications"
},
{
"id": "email-notif-desc",
"type": "p",
"className": "text-sm text-muted-foreground",
"children": "Receive updates via email"
}
]
},
{
"id": "email-notif-switch",
"type": "Switch",
"dataBinding": "notifications.email",
"events": {
"onCheckedChange": "toggle-email-notifications"
}
}
]
},
{
"id": "divider-3",
"type": "Separator"
},
{
"id": "push-notif",
"type": "div",
"className": "flex items-center justify-between",
"children": [
{
"id": "push-notif-info",
"type": "div",
"children": [
{
"id": "push-notif-label",
"type": "Label",
"className": "text-base",
"children": "Push Notifications"
},
{
"id": "push-notif-desc",
"type": "p",
"className": "text-sm text-muted-foreground",
"children": "Receive browser push notifications"
}
]
},
{
"id": "push-notif-switch",
"type": "Switch",
"dataBinding": "notifications.push",
"events": {
"onCheckedChange": "toggle-push-notifications"
}
}
]
}
]
}
]
}
]
},
{
"id": "tab-content-security",
"type": "TabsContent",
"props": {
"value": "security"
},
"children": [
{
"id": "security-card",
"type": "Card",
"children": [
{
"id": "security-header",
"type": "CardHeader",
"children": [
{
"id": "security-title",
"type": "CardTitle",
"children": "Security Settings"
},
{
"id": "security-description",
"type": "CardDescription",
"children": "Manage security and privacy options"
}
]
},
{
"id": "security-content",
"type": "CardContent",
"className": "space-y-6",
"children": [
{
"id": "2fa-setting",
"type": "div",
"className": "flex items-center justify-between",
"children": [
{
"id": "2fa-info",
"type": "div",
"children": [
{
"id": "2fa-label",
"type": "Label",
"className": "text-base",
"children": "Two-Factor Authentication"
},
{
"id": "2fa-desc",
"type": "p",
"className": "text-sm text-muted-foreground",
"children": "Add an extra layer of security"
}
]
},
{
"id": "2fa-switch",
"type": "Switch",
"dataBinding": "security.twoFactor",
"events": {
"onCheckedChange": "toggle-2fa"
}
}
]
},
{
"id": "divider-4",
"type": "Separator"
},
{
"id": "session-setting",
"type": "div",
"className": "space-y-3",
"children": [
{
"id": "session-label",
"type": "Label",
"className": "text-base",
"children": "Active Sessions"
},
{
"id": "session-desc",
"type": "p",
"className": "text-sm text-muted-foreground",
"children": "Manage your active login sessions"
},
{
"id": "logout-button",
"type": "Button",
"props": {
"variant": "outline"
},
"events": {
"onClick": "logout-all-sessions"
},
"children": "Logout All Other Sessions"
}
]
}
]
}
]
}
]
}
]
},
{
"id": "save-actions",
"type": "div",
"className": "flex gap-3 pt-4",
"children": [
{
"id": "save-button",
"type": "Button",
"events": {
"onClick": "save-settings"
},
"children": "Save Changes"
},
{
"id": "reset-button",
"type": "Button",
"props": {
"variant": "outline"
},
"events": {
"onClick": "reset-settings"
},
"children": "Reset to Defaults"
}
]
}
]
},
"dataSources": {
"settings": {
"type": "static",
"config": {
"darkMode": true,
"language": "en",
"autoSave": true
}
},
"notifications": {
"type": "static",
"config": {
"email": true,
"push": false
}
},
"security": {
"type": "static",
"config": {
"twoFactor": false
}
}
}
}

View File

@@ -0,0 +1,286 @@
{
"id": "table-list-ui",
"title": "Data Table",
"description": "Interactive data table with sorting and actions",
"layout": {
"type": "flex",
"direction": "column",
"gap": "6",
"padding": "6",
"className": "h-full bg-background",
"children": [
{
"id": "table-header",
"type": "div",
"className": "flex justify-between items-center",
"children": [
{
"id": "table-title",
"type": "h1",
"className": "text-3xl font-bold",
"children": "Projects"
},
{
"id": "add-button",
"type": "Button",
"props": {
"size": "sm"
},
"className": "gap-2",
"events": {
"onClick": "add-project"
},
"children": [
{
"id": "add-icon",
"type": "Plus",
"props": {
"size": 16
}
},
{
"id": "add-text",
"type": "span",
"children": "New Project"
}
]
}
]
},
{
"id": "table-card",
"type": "Card",
"children": [
{
"id": "table-content",
"type": "CardContent",
"className": "p-0",
"children": [
{
"id": "projects-table",
"type": "Table",
"children": [
{
"id": "table-header-row",
"type": "TableHeader",
"children": [
{
"id": "header-row",
"type": "TableRow",
"children": [
{
"id": "header-name",
"type": "TableHead",
"children": "Name"
},
{
"id": "header-status",
"type": "TableHead",
"children": "Status"
},
{
"id": "header-date",
"type": "TableHead",
"children": "Last Updated"
},
{
"id": "header-actions",
"type": "TableHead",
"className": "text-right",
"children": "Actions"
}
]
}
]
},
{
"id": "table-body",
"type": "TableBody",
"loop": {
"source": "projects",
"itemVar": "project",
"indexVar": "index"
},
"children": [
{
"id": "data-row",
"type": "TableRow",
"children": [
{
"id": "cell-name",
"type": "TableCell",
"className": "font-medium",
"children": [
{
"id": "project-name",
"type": "span",
"dataBinding": "project.name"
}
]
},
{
"id": "cell-status",
"type": "TableCell",
"children": [
{
"id": "status-badge",
"type": "Badge",
"props": {
"variant": "secondary"
},
"dataBinding": "project.status"
}
]
},
{
"id": "cell-date",
"type": "TableCell",
"children": [
{
"id": "project-date",
"type": "span",
"className": "text-sm text-muted-foreground",
"dataBinding": "project.lastUpdated"
}
]
},
{
"id": "cell-actions",
"type": "TableCell",
"className": "text-right",
"children": [
{
"id": "actions-container",
"type": "div",
"className": "flex gap-2 justify-end",
"children": [
{
"id": "view-button",
"type": "Button",
"props": {
"variant": "ghost",
"size": "sm"
},
"events": {
"onClick": {
"action": "view-project",
"params": {
"projectId": "project.id"
}
}
},
"children": [
{
"id": "view-icon",
"type": "Eye",
"props": {
"size": 16
}
}
]
},
{
"id": "edit-button",
"type": "Button",
"props": {
"variant": "ghost",
"size": "sm"
},
"events": {
"onClick": {
"action": "edit-project",
"params": {
"projectId": "project.id"
}
}
},
"children": [
{
"id": "edit-icon",
"type": "Edit",
"props": {
"size": 16
}
}
]
},
{
"id": "delete-button",
"type": "Button",
"props": {
"variant": "ghost",
"size": "sm"
},
"events": {
"onClick": {
"action": "delete-project",
"params": {
"projectId": "project.id"
}
}
},
"children": [
{
"id": "delete-icon",
"type": "Trash",
"props": {
"size": 16
},
"className": "text-destructive"
}
]
}
]
}
]
}
]
}
]
}
]
}
]
}
]
}
]
},
"dataSources": {
"projects": {
"type": "static",
"config": [
{
"id": "1",
"name": "E-commerce Platform",
"status": "Active",
"lastUpdated": "2 hours ago"
},
{
"id": "2",
"name": "Blog CMS",
"status": "In Progress",
"lastUpdated": "1 day ago"
},
{
"id": "3",
"name": "Analytics Dashboard",
"status": "Active",
"lastUpdated": "3 days ago"
},
{
"id": "4",
"name": "Mobile App API",
"status": "Planning",
"lastUpdated": "1 week ago"
},
{
"id": "5",
"name": "Customer Portal",
"status": "Active",
"lastUpdated": "2 weeks ago"
}
]
}
}
}

320
src/lib/json-ui/README.md Normal file
View File

@@ -0,0 +1,320 @@
# JSON UI System
A comprehensive declarative UI framework for building React interfaces from JSON configurations.
## 📁 Directory Structure
```
src/lib/json-ui/
├── index.ts # Main exports
├── schema.ts # Zod schemas for type validation
├── component-registry.ts # Component registry and lookup
├── renderer.tsx # React renderer for JSON configs
├── hooks.ts # React hooks for data management
├── utils.ts # Utility functions
└── validator.ts # Configuration validation
```
## 🚀 Quick Start
### 1. Create a JSON Configuration
```json
{
"id": "my-page",
"title": "My Page",
"layout": {
"type": "flex",
"direction": "column",
"children": [
{
"id": "greeting",
"type": "h1",
"children": "Hello World"
},
{
"id": "cta-button",
"type": "Button",
"events": {"onClick": "greet"},
"children": "Click Me"
}
]
},
"dataSources": {}
}
```
### 2. Render the Configuration
```tsx
import { JSONUIPage } from '@/components/JSONUIPage'
import config from './my-config.json'
export function MyPage() {
return <JSONUIPage jsonConfig={config} />
}
```
## 📚 Documentation
- **[Complete Guide](/docs/JSON-UI-SYSTEM.md)** - Full system documentation
- **[Quick Reference](/docs/JSON-UI-QUICK-REF.md)** - Component and syntax quick reference
- **[Migration Guide](/docs/MIGRATING-TO-JSON-UI.md)** - Convert React to JSON UI
- **[Examples README](/src/config/ui-examples/README.md)** - Example configurations
## 🎯 Core Concepts
### Components
Define UI elements using JSON:
```json
{
"id": "my-button",
"type": "Button",
"props": {"variant": "primary"},
"className": "mt-4",
"children": "Submit"
}
```
### Data Binding
Connect UI to data sources:
```json
{
"type": "p",
"dataBinding": "user.name"
}
```
### Event Handling
Respond to user interactions:
```json
{
"events": {
"onClick": "save-data"
}
}
```
### Looping
Render lists from arrays:
```json
{
"loop": {
"source": "items",
"itemVar": "item"
},
"children": [...]
}
```
### Conditionals
Show/hide based on conditions:
```json
{
"conditional": {
"if": "user.isAdmin",
"then": {...},
"else": {...}
}
}
```
## 🧩 Available Components
### Layout
- HTML primitives: `div`, `span`, `section`, `header`, etc.
### UI Components (shadcn/ui)
- `Button`, `Input`, `Textarea`, `Label`
- `Card`, `CardHeader`, `CardTitle`, `CardContent`, etc.
- `Table`, `TableHeader`, `TableBody`, `TableRow`, `TableCell`
- `Tabs`, `TabsList`, `TabsTrigger`, `TabsContent`
- `Badge`, `Separator`, `Alert`, `Switch`, `Checkbox`
- And 30+ more...
### Icons (Phosphor)
- `Plus`, `Minus`, `Edit`, `Trash`, `Eye`, `Settings`
- `User`, `Bell`, `Calendar`, `Star`, `Heart`
- And 30+ more...
## 💾 Data Sources
### Static Data
```json
{
"dataSources": {
"config": {
"type": "static",
"config": {"theme": "dark"}
}
}
}
```
### API Data
```json
{
"dataSources": {
"users": {
"type": "api",
"config": {"url": "/api/users"}
}
}
}
```
### KV Store
```json
{
"dataSources": {
"preferences": {
"type": "kv",
"config": {
"key": "user-prefs",
"defaultValue": {}
}
}
}
}
```
## 🛠️ Advanced Usage
### Custom Components
Register your own components:
```typescript
import { registerComponent } from '@/lib/json-ui'
import { MyCustomComponent } from './MyCustomComponent'
registerComponent('MyCustom', MyCustomComponent)
```
### Validation
Validate JSON configurations:
```typescript
import { validateJSONUI, prettyPrintValidation } from '@/lib/json-ui'
const result = validateJSONUI(myConfig)
console.log(prettyPrintValidation(result))
```
### Type Safety
Use TypeScript types from schemas:
```typescript
import type { UIComponent, PageUI } from '@/lib/json-ui'
const component: UIComponent = {
id: 'my-component',
type: 'Button',
children: 'Click Me'
}
```
## 📦 Exports
```typescript
// Schemas and Types
export type {
UIComponent,
Form,
Table,
Dialog,
Layout,
Tabs,
Menu,
PageUI,
DataBinding,
EventHandler
} from './schema'
// Components
export {
JSONUIRenderer,
JSONFormRenderer
} from './renderer'
export {
uiComponentRegistry,
registerComponent,
getUIComponent,
hasComponent
} from './component-registry'
// Hooks
export {
useJSONDataSource,
useJSONDataSources,
useJSONActions
} from './hooks'
// Utils
export {
resolveDataBinding,
getNestedValue,
setNestedValue,
evaluateCondition,
transformData
} from './utils'
// Validation
export {
validateJSONUI,
prettyPrintValidation
} from './validator'
```
## 🎨 Examples
See `/src/config/ui-examples/` for complete working examples:
- **dashboard.json** - Dashboard with stats and activity feed
- **form.json** - Registration form with validation
- **table.json** - Data table with row actions
- **settings.json** - Tabbed settings panel
View them live in the app under "JSON UI" tab.
## ✅ Benefits
- **Declarative**: Clear, readable configuration format
- **Type-Safe**: Validated with Zod schemas
- **Extensible**: Add custom components easily
- **Dynamic**: Load and modify UIs at runtime
- **Maintainable**: Separation of structure and logic
- **Accessible**: Non-developers can modify UIs
## ⚠️ Limitations
- Not suitable for complex state management
- Performance considerations for very large UIs
- Debugging can be more challenging
- Learning curve for the JSON schema
## 🔮 Future Enhancements
- Visual drag-and-drop UI builder
- GraphQL data source support
- Animation configurations
- Form validation schemas
- WebSocket real-time updates
- Export JSON to React code
- Template library with common patterns
## 📝 License
Part of the Spark template project.

View File

@@ -0,0 +1,157 @@
import { ComponentType } from 'react'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Textarea } from '@/components/ui/textarea'
import { Label } from '@/components/ui/label'
import { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'
import { Separator } from '@/components/ui/separator'
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
import { Switch } from '@/components/ui/switch'
import { Checkbox } from '@/components/ui/checkbox'
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'
import { Skeleton } from '@/components/ui/skeleton'
import { Progress } from '@/components/ui/progress'
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
import {
ArrowLeft, ArrowRight, Check, X, Plus, Minus, MagnifyingGlass,
Funnel, Download, Upload, PencilSimple, Trash, Eye, EyeClosed,
CaretUp, CaretDown, CaretLeft, CaretRight,
Gear, User, Bell, Envelope, Calendar, Clock, Star,
Heart, ShareNetwork, LinkSimple, Copy, FloppyDisk, ArrowClockwise, WarningCircle,
Info, Question, House, List, DotsThreeVertical, DotsThree
} from '@phosphor-icons/react'
export interface UIComponentRegistry {
[key: string]: ComponentType<any>
}
export const primitiveComponents: UIComponentRegistry = {
div: 'div' as any,
span: 'span' as any,
p: 'p' as any,
h1: 'h1' as any,
h2: 'h2' as any,
h3: 'h3' as any,
h4: 'h4' as any,
h5: 'h5' as any,
h6: 'h6' as any,
section: 'section' as any,
article: 'article' as any,
header: 'header' as any,
footer: 'footer' as any,
main: 'main' as any,
aside: 'aside' as any,
nav: 'nav' as any,
}
export const shadcnComponents: UIComponentRegistry = {
Button,
Input,
Textarea,
Label,
Card,
CardHeader,
CardTitle,
CardDescription,
CardContent,
CardFooter,
Badge,
Separator,
Alert,
AlertDescription,
AlertTitle,
Switch,
Checkbox,
RadioGroup,
RadioGroupItem,
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
Tabs,
TabsContent,
TabsList,
TabsTrigger,
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
Skeleton,
Progress,
Avatar,
AvatarFallback,
AvatarImage,
}
export const iconComponents: UIComponentRegistry = {
ArrowLeft,
ArrowRight,
Check,
X,
Plus,
Minus,
Search: MagnifyingGlass,
Filter: Funnel,
Download,
Upload,
Edit: PencilSimple,
Trash,
Eye,
EyeOff: EyeClosed,
ChevronUp: CaretUp,
ChevronDown: CaretDown,
ChevronLeft: CaretLeft,
ChevronRight: CaretRight,
Settings: Gear,
User,
Bell,
Mail: Envelope,
Calendar,
Clock,
Star,
Heart,
Share: ShareNetwork,
Link: LinkSimple,
Copy,
Save: FloppyDisk,
RefreshCw: ArrowClockwise,
AlertCircle: WarningCircle,
Info,
HelpCircle: Question,
Home: House,
Menu: List,
MoreVertical: DotsThreeVertical,
MoreHorizontal: DotsThree,
}
export const uiComponentRegistry: UIComponentRegistry = {
...primitiveComponents,
...shadcnComponents,
...iconComponents,
}
export function registerComponent(name: string, component: ComponentType<any>) {
uiComponentRegistry[name] = component
}
export function getUIComponent(type: string): ComponentType<any> | string | null {
return uiComponentRegistry[type] || null
}
export function hasComponent(type: string): boolean {
return type in uiComponentRegistry
}

138
src/lib/json-ui/hooks.ts Normal file
View File

@@ -0,0 +1,138 @@
import { useState, useEffect, useCallback } from 'react'
import { useKV } from '@github/spark/hooks'
export interface DataSourceConfig {
type: 'kv' | 'api' | 'computed' | 'static'
key?: string
url?: string
defaultValue?: any
transform?: (data: any) => any
}
export function useJSONDataSource(id: string, config: DataSourceConfig) {
const [kvValue, setKVValue] = useKV(config.key || id, config.defaultValue)
const [apiValue, setApiValue] = useState(config.defaultValue)
const [loading, setLoading] = useState(false)
const [error, setError] = useState<Error | null>(null)
const fetchAPI = useCallback(async () => {
if (config.type !== 'api' || !config.url) return
setLoading(true)
setError(null)
try {
const response = await fetch(config.url)
if (!response.ok) throw new Error(`HTTP ${response.status}`)
let data = await response.json()
if (config.transform) {
data = config.transform(data)
}
setApiValue(data)
} catch (err) {
setError(err as Error)
} finally {
setLoading(false)
}
}, [config.type, config.url, config.transform])
useEffect(() => {
if (config.type === 'api') {
fetchAPI()
}
}, [config.type, fetchAPI])
const getValue = () => {
switch (config.type) {
case 'kv':
return kvValue
case 'api':
return apiValue
case 'static':
return config.defaultValue
case 'computed':
return config.defaultValue
default:
return null
}
}
const setValue = (newValue: any) => {
switch (config.type) {
case 'kv':
setKVValue(newValue)
break
case 'api':
setApiValue(newValue)
break
default:
break
}
}
return {
value: getValue(),
setValue,
loading,
error,
refetch: fetchAPI,
}
}
export function useJSONDataSources(sources: Record<string, DataSourceConfig>) {
const [dataMap, setDataMap] = useState<Record<string, any>>({})
const [loadingMap, setLoadingMap] = useState<Record<string, boolean>>({})
const [errorMap, setErrorMap] = useState<Record<string, Error | null>>({})
const sourceIds = Object.keys(sources)
const updateData = useCallback((id: string, value: any) => {
setDataMap((prev) => ({ ...prev, [id]: value }))
}, [])
const getData = useCallback((id: string) => {
return dataMap[id]
}, [dataMap])
useEffect(() => {
sourceIds.forEach((id) => {
const config = sources[id]
if (config.type === 'static') {
updateData(id, config.defaultValue)
}
})
}, [sourceIds])
return {
dataMap,
loadingMap,
errorMap,
updateData,
getData,
}
}
export function useJSONActions() {
const [actionHandlers, setActionHandlers] = useState<Record<string, (...args: any[]) => void>>({})
const registerAction = useCallback((id: string, handler: (...args: any[]) => void) => {
setActionHandlers((prev) => ({ ...prev, [id]: handler }))
}, [])
const executeAction = useCallback((id: string, ...args: any[]) => {
const handler = actionHandlers[id]
if (handler) {
handler(...args)
} else {
console.warn(`Action handler not found: ${id}`)
}
}, [actionHandlers])
return {
registerAction,
executeAction,
}
}

6
src/lib/json-ui/index.ts Normal file
View File

@@ -0,0 +1,6 @@
export * from './schema'
export * from './renderer'
export * from './component-registry'
export * from './hooks'
export * from './utils'
export * from './validator'

View File

@@ -0,0 +1,187 @@
import React, { useCallback } from 'react'
import { UIComponent, EventHandler } from './schema'
import { getUIComponent } from './component-registry'
import { resolveDataBinding, evaluateCondition, mergeClassNames } from './utils'
import { cn } from '@/lib/utils'
export interface JSONUIRendererProps {
component: UIComponent
dataMap?: Record<string, any>
onAction?: (handler: EventHandler, event?: any) => void
context?: Record<string, any>
}
export function JSONUIRenderer({
component,
dataMap = {},
onAction,
context = {}
}: JSONUIRendererProps) {
if (component.conditional) {
const conditionMet = evaluateCondition(component.conditional.if, { ...dataMap, ...context })
if (!conditionMet) {
return component.conditional.else ? (
<JSONUIRenderer
component={component.conditional.else as UIComponent}
dataMap={dataMap}
onAction={onAction}
context={context}
/>
) : null
}
}
if (component.loop) {
const items = resolveDataBinding(component.loop.source, dataMap) || []
return (
<>
{items.map((item: any, index: number) => {
const loopContext = {
...context,
[component.loop!.itemVar]: item,
...(component.loop!.indexVar ? { [component.loop!.indexVar]: index } : {}),
}
return (
<JSONUIRenderer
key={`${component.id}-${index}`}
component={component}
dataMap={dataMap}
onAction={onAction}
context={loopContext}
/>
)
})}
</>
)
}
const Component = getUIComponent(component.type)
if (!Component) {
console.warn(`Component type "${component.type}" not found in registry`)
return null
}
const props: Record<string, any> = { ...component.props }
if (component.dataBinding) {
const boundData = resolveDataBinding(component.dataBinding, dataMap)
if (boundData !== undefined) {
props.value = boundData
props.data = boundData
}
}
if (component.events) {
Object.entries(component.events).forEach(([eventName, handler]) => {
props[eventName] = (event?: any) => {
if (onAction) {
const eventHandler = typeof handler === 'string'
? { action: handler } as EventHandler
: handler as EventHandler
onAction(eventHandler, event)
}
}
})
}
if (component.className) {
props.className = cn(props.className, component.className)
}
if (component.style) {
props.style = { ...props.style, ...component.style }
}
const renderChildren = () => {
if (!component.children) return null
if (typeof component.children === 'string') {
return component.children
}
return component.children.map((child, index) => (
<JSONUIRenderer
key={child.id || `child-${index}`}
component={child}
dataMap={dataMap}
onAction={onAction}
context={context}
/>
))
}
if (typeof Component === 'string') {
return React.createElement(Component, props, renderChildren())
}
return (
<Component {...props}>
{renderChildren()}
</Component>
)
}
export interface JSONFormRendererProps {
formData: any
fields: any[]
onSubmit: (data: any) => void
onChange?: (data: any) => void
}
export function JSONFormRenderer({ formData, fields, onSubmit, onChange }: JSONFormRendererProps) {
const handleFieldChange = useCallback((fieldName: string, value: any) => {
const newData = { ...formData, [fieldName]: value }
onChange?.(newData)
}, [formData, onChange])
const handleSubmit = useCallback((e: React.FormEvent) => {
e.preventDefault()
onSubmit(formData)
}, [formData, onSubmit])
return (
<form onSubmit={handleSubmit} className="space-y-4">
{fields.map((field) => {
const fieldComponent: UIComponent = {
id: field.id,
type: field.type === 'textarea' ? 'Textarea' : 'Input',
props: {
name: field.name,
placeholder: field.placeholder,
required: field.required,
type: field.type,
value: formData[field.name] || field.defaultValue || '',
},
events: {
onChange: {
action: 'field-change',
params: { field: field.name },
},
},
}
return (
<div key={field.id} className="space-y-2">
{field.label && (
<label htmlFor={field.name} className="text-sm font-medium">
{field.label}
{field.required && <span className="text-destructive ml-1">*</span>}
</label>
)}
<JSONUIRenderer
component={fieldComponent}
dataMap={{}}
onAction={(handler, event) => {
if (handler.action === 'field-change') {
handleFieldChange(field.name, event.target.value)
}
}}
/>
</div>
)
})}
</form>
)
}

203
src/lib/json-ui/schema.ts Normal file
View File

@@ -0,0 +1,203 @@
import { z } from 'zod'
export const UIValueSchema = z.union([
z.string(),
z.number(),
z.boolean(),
z.null(),
z.array(z.any()),
z.record(z.string(), z.any()),
])
export const DataBindingSchema = z.object({
source: z.string(),
path: z.string().optional(),
transform: z.string().optional(),
})
export const EventHandlerSchema = z.object({
action: z.string(),
target: z.string().optional(),
params: z.record(z.string(), z.any()).optional(),
})
export const ConditionalSchema = z.object({
if: z.string(),
then: z.any().optional(),
else: z.any().optional(),
})
export const UIComponentSchema: any = z.object({
id: z.string(),
type: z.string(),
props: z.record(z.string(), z.any()).optional(),
className: z.string().optional(),
style: z.record(z.string(), z.any()).optional(),
children: z.union([
z.string(),
z.array(z.lazy(() => UIComponentSchema)),
]).optional(),
dataBinding: z.union([
z.string(),
DataBindingSchema,
]).optional(),
events: z.record(z.string(), z.union([
z.string(),
EventHandlerSchema,
])).optional(),
conditional: ConditionalSchema.optional(),
loop: z.object({
source: z.string(),
itemVar: z.string(),
indexVar: z.string().optional(),
}).optional(),
})
export const FormFieldSchema = z.object({
id: z.string(),
name: z.string(),
label: z.string(),
type: z.enum(['text', 'email', 'password', 'number', 'textarea', 'select', 'checkbox', 'radio', 'date', 'file']),
placeholder: z.string().optional(),
defaultValue: z.any().optional(),
required: z.boolean().optional(),
validation: z.object({
min: z.number().optional(),
max: z.number().optional(),
pattern: z.string().optional(),
message: z.string().optional(),
}).optional(),
options: z.array(z.object({
label: z.string(),
value: z.any(),
})).optional(),
conditional: ConditionalSchema.optional(),
})
export const FormSchema = z.object({
id: z.string(),
title: z.string().optional(),
description: z.string().optional(),
fields: z.array(FormFieldSchema),
submitLabel: z.string().optional(),
cancelLabel: z.string().optional(),
onSubmit: EventHandlerSchema,
onCancel: EventHandlerSchema.optional(),
layout: z.enum(['vertical', 'horizontal', 'grid']).optional(),
})
export const TableColumnSchema = z.object({
id: z.string(),
header: z.string(),
accessor: z.string(),
type: z.enum(['text', 'number', 'date', 'badge', 'button', 'custom']).optional(),
render: z.string().optional(),
sortable: z.boolean().optional(),
filterable: z.boolean().optional(),
width: z.string().optional(),
})
export const TableSchema = z.object({
id: z.string(),
dataSource: z.string(),
columns: z.array(TableColumnSchema),
pagination: z.boolean().optional(),
pageSize: z.number().optional(),
searchable: z.boolean().optional(),
selectable: z.boolean().optional(),
actions: z.array(z.object({
id: z.string(),
label: z.string(),
icon: z.string().optional(),
handler: EventHandlerSchema,
})).optional(),
})
export const DialogSchema = z.object({
id: z.string(),
title: z.string(),
description: z.string().optional(),
content: z.union([
z.string(),
z.array(UIComponentSchema),
FormSchema,
]),
actions: z.array(z.object({
id: z.string(),
label: z.string(),
variant: z.enum(['default', 'destructive', 'outline', 'secondary', 'ghost', 'link']).optional(),
handler: EventHandlerSchema,
})).optional(),
size: z.enum(['sm', 'md', 'lg', 'xl', 'full']).optional(),
})
export const LayoutSchema = z.object({
type: z.enum(['flex', 'grid', 'stack', 'split', 'tabs']),
direction: z.enum(['row', 'column', 'horizontal', 'vertical']).optional(),
gap: z.string().optional(),
padding: z.string().optional(),
className: z.string().optional(),
children: z.array(UIComponentSchema),
})
export const TabSchema = z.object({
id: z.string(),
label: z.string(),
icon: z.string().optional(),
content: z.array(UIComponentSchema),
disabled: z.boolean().optional(),
})
export const TabsSchema = z.object({
id: z.string(),
tabs: z.array(TabSchema),
defaultTab: z.string().optional(),
orientation: z.enum(['horizontal', 'vertical']).optional(),
})
export const MenuItemSchema = z.object({
id: z.string(),
label: z.string(),
icon: z.string().optional(),
action: z.union([z.string(), EventHandlerSchema]).optional(),
disabled: z.boolean().optional(),
children: z.array(z.lazy(() => MenuItemSchema)).optional(),
})
export const MenuSchema = z.object({
id: z.string(),
items: z.array(MenuItemSchema),
orientation: z.enum(['horizontal', 'vertical']).optional(),
})
export const PageUISchema = z.object({
id: z.string(),
title: z.string(),
description: z.string().optional(),
layout: LayoutSchema,
dialogs: z.array(DialogSchema).optional(),
forms: z.array(FormSchema).optional(),
tables: z.array(TableSchema).optional(),
menus: z.array(MenuSchema).optional(),
dataSources: z.record(z.string(), z.object({
type: z.enum(['kv', 'api', 'computed', 'static']),
config: z.any(),
})).optional(),
})
export type UIValue = z.infer<typeof UIValueSchema>
export type DataBinding = z.infer<typeof DataBindingSchema>
export type EventHandler = z.infer<typeof EventHandlerSchema>
export type Conditional = z.infer<typeof ConditionalSchema>
export type UIComponent = z.infer<typeof UIComponentSchema>
export type FormField = z.infer<typeof FormFieldSchema>
export type Form = z.infer<typeof FormSchema>
export type TableColumn = z.infer<typeof TableColumnSchema>
export type Table = z.infer<typeof TableSchema>
export type Dialog = z.infer<typeof DialogSchema>
export type Layout = z.infer<typeof LayoutSchema>
export type Tab = z.infer<typeof TabSchema>
export type Tabs = z.infer<typeof TabsSchema>
export type MenuItem = z.infer<typeof MenuItemSchema>
export type Menu = z.infer<typeof MenuSchema>
export type PageUI = z.infer<typeof PageUISchema>

61
src/lib/json-ui/utils.ts Normal file
View File

@@ -0,0 +1,61 @@
export function resolveDataBinding(binding: string | { source: string; path?: string }, dataMap: Record<string, any>): any {
if (typeof binding === 'string') {
return dataMap[binding]
}
const { source, path } = binding
const data = dataMap[source]
if (!path) return data
return getNestedValue(data, path)
}
export function getNestedValue(obj: any, path: string): any {
return path.split('.').reduce((current, key) => {
return current?.[key]
}, obj)
}
export function setNestedValue(obj: any, path: string, value: any): any {
const keys = path.split('.')
const lastKey = keys.pop()!
const target = keys.reduce((current, key) => {
if (!(key in current)) {
current[key] = {}
}
return current[key]
}, obj)
target[lastKey] = value
return obj
}
export function evaluateCondition(condition: string, context: Record<string, any>): boolean {
try {
const conditionFn = new Function(...Object.keys(context), `return ${condition}`)
return Boolean(conditionFn(...Object.values(context)))
} catch (err) {
console.warn('Failed to evaluate condition:', condition, err)
return false
}
}
export function transformData(data: any, transformFn: string): any {
try {
const fn = new Function('data', `return ${transformFn}`)
return fn(data)
} catch (err) {
console.warn('Failed to transform data:', err)
return data
}
}
export function mergeClassNames(...classes: (string | undefined | null | false)[]): string {
return classes.filter(Boolean).join(' ')
}
export function generateId(prefix = 'ui'): string {
return `${prefix}-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`
}

View File

@@ -0,0 +1,150 @@
import { PageUISchema } from './schema'
import { z } from 'zod'
export interface ValidationResult {
valid: boolean
errors: Array<{
path: string
message: string
}>
warnings: Array<{
path: string
message: string
}>
}
export function validateJSONUI(config: any): ValidationResult {
const result: ValidationResult = {
valid: true,
errors: [],
warnings: [],
}
try {
PageUISchema.parse(config)
} catch (err) {
result.valid = false
if (err instanceof z.ZodError) {
result.errors = err.issues.map(e => ({
path: e.path.join('.'),
message: e.message,
}))
} else {
result.errors.push({
path: 'root',
message: String(err),
})
}
}
checkForWarnings(config, result)
return result
}
function checkForWarnings(config: any, result: ValidationResult) {
if (!config) return
if (!config.id) {
result.warnings.push({
path: 'root',
message: 'Missing id field at root level',
})
}
if (config.layout) {
checkComponentTree(config.layout, 'layout', result, new Set())
}
if (config.dataSources) {
checkDataSources(config.dataSources, result)
}
}
function checkComponentTree(
component: any,
path: string,
result: ValidationResult,
seenIds: Set<string>
) {
if (!component) return
if (!component.id) {
result.warnings.push({
path,
message: 'Component missing id field',
})
} else if (seenIds.has(component.id)) {
result.warnings.push({
path,
message: `Duplicate component id: ${component.id}`,
})
} else {
seenIds.add(component.id)
}
if (component.dataBinding && !component.dataBinding.source) {
const bindingPath = typeof component.dataBinding === 'string'
? component.dataBinding.split('.')[0]
: ''
if (bindingPath) {
result.warnings.push({
path: `${path}.${component.id}`,
message: `Data binding references '${bindingPath}' - ensure this data source exists`,
})
}
}
if (component.children) {
if (Array.isArray(component.children)) {
component.children.forEach((child: any, index: number) => {
checkComponentTree(child, `${path}.children[${index}]`, result, seenIds)
})
}
}
}
function checkDataSources(dataSources: any, result: ValidationResult) {
Object.entries(dataSources).forEach(([key, source]: [string, any]) => {
if (source.type === 'api' && !source.config?.url) {
result.warnings.push({
path: `dataSources.${key}`,
message: 'API data source missing url configuration',
})
}
if (source.type === 'kv' && !source.config?.key) {
result.warnings.push({
path: `dataSources.${key}`,
message: 'KV data source missing key configuration',
})
}
})
}
export function prettyPrintValidation(result: ValidationResult): string {
const lines: string[] = []
if (result.valid && result.warnings.length === 0) {
lines.push('✅ JSON UI configuration is valid')
return lines.join('\n')
}
if (result.errors.length > 0) {
lines.push('❌ Validation Errors:')
result.errors.forEach(error => {
lines.push(` ${error.path}: ${error.message}`)
})
lines.push('')
}
if (result.warnings.length > 0) {
lines.push('⚠️ Warnings:')
result.warnings.forEach(warning => {
lines.push(` ${warning.path}: ${warning.message}`)
})
}
return lines.join('\n')
}