mirror of
https://github.com/johndoe6345789/low-code-react-app-b.git
synced 2026-04-29 16:14:55 +00:00
Generated by Spark: Load more of UI from JSON declarations
This commit is contained in:
147
src/components/JSONUIPage.tsx
Normal file
147
src/components/JSONUIPage.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
138
src/components/JSONUIShowcase.tsx
Normal file
138
src/components/JSONUIShowcase.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -3,3 +3,5 @@ export * from './molecules'
|
||||
export * from './organisms'
|
||||
export * from './TemplateSelector'
|
||||
export * from './TemplateExplorer'
|
||||
export * from './JSONUIShowcase'
|
||||
export * from './JSONUIPage'
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
119
src/config/ui-examples/README.md
Normal file
119
src/config/ui-examples/README.md
Normal 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
|
||||
437
src/config/ui-examples/dashboard.json
Normal file
437
src/config/ui-examples/dashboard.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
241
src/config/ui-examples/form.json
Normal file
241
src/config/ui-examples/form.json
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
526
src/config/ui-examples/settings.json
Normal file
526
src/config/ui-examples/settings.json
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
286
src/config/ui-examples/table.json
Normal file
286
src/config/ui-examples/table.json
Normal 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
320
src/lib/json-ui/README.md
Normal 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.
|
||||
157
src/lib/json-ui/component-registry.ts
Normal file
157
src/lib/json-ui/component-registry.ts
Normal 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
138
src/lib/json-ui/hooks.ts
Normal 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
6
src/lib/json-ui/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export * from './schema'
|
||||
export * from './renderer'
|
||||
export * from './component-registry'
|
||||
export * from './hooks'
|
||||
export * from './utils'
|
||||
export * from './validator'
|
||||
187
src/lib/json-ui/renderer.tsx
Normal file
187
src/lib/json-ui/renderer.tsx
Normal 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
203
src/lib/json-ui/schema.ts
Normal 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
61
src/lib/json-ui/utils.ts
Normal 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)}`
|
||||
}
|
||||
150
src/lib/json-ui/validator.ts
Normal file
150
src/lib/json-ui/validator.ts
Normal 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')
|
||||
}
|
||||
Reference in New Issue
Block a user