Compare commits

...

19 Commits

Author SHA1 Message Date
b0aedbb0bd Add JSON UI support for progress and divider atoms 2026-01-18 11:55:56 +00:00
af03c13934 Merge pull request #117 from johndoe6345789/codex/integrate-breadcrumb-component-into-json
Add JSON Breadcrumb support (map to atoms/BreadcrumbNav)
2026-01-18 11:42:58 +00:00
4529708f76 Merge branch 'main' into codex/integrate-breadcrumb-component-into-json 2026-01-18 11:42:50 +00:00
8945c746cb Add breadcrumb to JSON component registry 2026-01-18 11:42:27 +00:00
2190be271f Merge pull request #116 from johndoe6345789/codex/convert-page-schemas.ts-to-json
Convert page schemas to JSON
2026-01-18 11:41:59 +00:00
e7fc49e53f Merge pull request #115 from johndoe6345789/codex/implement-prop-compatibility-for-components
Add JSON UI support for ErrorBadge, Notification, and StatusIcon
2026-01-18 11:41:22 +00:00
9448b8327d Merge branch 'main' into codex/implement-prop-compatibility-for-components 2026-01-18 11:41:15 +00:00
64c3b5b12b Convert page schemas to JSON 2026-01-18 11:40:46 +00:00
0d82406e5f Add JSON UI support for feedback atoms 2026-01-18 11:38:50 +00:00
233dbd2aa1 Merge pull request #114 from johndoe6345789/codex/implement-json-ui-component-integration
JSON UI: Add support for DataList / DataTable / MetricCard / Timeline
2026-01-18 11:36:19 +00:00
3fe02ed098 Add JSON UI support for data atoms 2026-01-18 11:35:57 +00:00
e7159916cb Merge pull request #113 from johndoe6345789/codex/assess-safety-of-converting-components-to-json
Add JSON component conversion task list
2026-01-18 11:32:36 +00:00
e41d08d40c Add JSON component conversion task list 2026-01-18 11:32:24 +00:00
fc209545c1 Merge pull request #112 from johndoe6345789/codex/add-checklist-for-json-components
Rephrase JSON compatibility refactoring checklist item
2026-01-18 11:21:46 +00:00
28a3851310 Rephrase refactoring checklist guidance 2026-01-18 11:19:42 +00:00
8465a9de5a Merge pull request #111 from johndoe6345789/codex/extend-json-schema-types-for-bindings
Add state-aware bindings for JSON renderer
2026-01-18 11:19:06 +00:00
d04333e565 Add state-aware bindings for JSON renderer 2026-01-18 11:18:40 +00:00
e210dd8bec Merge pull request #110 from johndoe6345789/codex/define-json-event-schema-and-implement-dispatcher
Add JSON event map handling in JSON UI renderer
2026-01-18 11:18:03 +00:00
39c57e9967 Add JSON event definitions and renderer support 2026-01-18 11:17:37 +00:00
18 changed files with 1043 additions and 102 deletions

View File

@@ -171,6 +171,24 @@ To add a new component to the JSON UI system:
4. Update `json-components-registry.json` with metadata 4. Update `json-components-registry.json` with metadata
5. Test the component in a JSON schema 5. Test the component in a JSON schema
### JSON Compatibility Checklist
Before migrating a component, confirm all required conditions are met:
- [ ] **Hooks/state are registry-safe**: hooks and internal state are acceptable when the component registry can control or expose them through JSON bindings.
- [ ] **Bindings are defined**: any required actions, event handlers, or state bindings are already supported by the JSON UI binding system.
- [ ] **Refactoring covered by PR**: JSON compatibility gaps should be resolved via refactoring as part of the same pull request.
### Step-by-Step Migration Path
Use this repeatable migration flow for planned components:
1. **Update the type union** in `src/types/json-ui.ts` to include the new component type name.
2. **Register the component** in `src/lib/json-ui/component-registry.tsx` so JSON schemas can resolve it at runtime.
3. **Define component metadata** in `src/lib/component-definitions.ts` (defaults, prop schema, and any JSON-driven constraints).
4. **Validate JSON schema usage** by rendering a sample schema that uses the new type.
5. **Update registry metadata** in `json-components-registry.json` so the CLI/listing reflects the new status.
## Migration Strategy ## Migration Strategy
Components marked as "planned" are: Components marked as "planned" are:

View File

@@ -0,0 +1,95 @@
# JSON Component Conversion Tasks
This task list captures the next steps for expanding JSON UI coverage, split between **component migrations** and **framework enablers**.
## Component Migration Tasks (Planned → Supported)
### Input Components
- [ ] **DatePicker**
- Add `DatePicker` to `ComponentType` in `src/types/json-ui.ts`.
- Register `DatePicker` in `src/lib/json-ui/component-registry.tsx`.
- Add metadata/defaults to `src/lib/component-definitions.ts`.
- Flip status to `supported` in `json-components-registry.json`.
- [ ] **FileUpload**
- Add `FileUpload` to `ComponentType` in `src/types/json-ui.ts`.
- Register `FileUpload` in `src/lib/json-ui/component-registry.tsx`.
- Add metadata/defaults to `src/lib/component-definitions.ts`.
- Flip status to `supported` in `json-components-registry.json`.
### Display Components
- [ ] **CircularProgress**
- Add `CircularProgress` to `ComponentType` in `src/types/json-ui.ts`.
- Register `CircularProgress` in `src/lib/json-ui/component-registry.tsx`.
- Add metadata/defaults to `src/lib/component-definitions.ts`.
- Flip status to `supported` in `json-components-registry.json`.
- [ ] **Divider**
- Add `Divider` to `ComponentType` in `src/types/json-ui.ts`.
- Register `Divider` in `src/lib/json-ui/component-registry.tsx`.
- Add metadata/defaults to `src/lib/component-definitions.ts`.
- Flip status to `supported` in `json-components-registry.json`.
- [ ] **ProgressBar**
- Add `ProgressBar` to `ComponentType` in `src/types/json-ui.ts`.
- Register `ProgressBar` in `src/lib/json-ui/component-registry.tsx`.
- Add metadata/defaults to `src/lib/component-definitions.ts`.
- Flip status to `supported` in `json-components-registry.json`.
### Navigation Components
- [ ] **Breadcrumb**
- Decide whether JSON should map to `BreadcrumbNav` (atoms) or `Breadcrumb` (molecules).
- Align props and bindings to a single JSON-friendly surface.
- Register a single `Breadcrumb` entry and set status to `supported` in `json-components-registry.json`.
### Feedback Components
- [ ] **ErrorBadge**
- Add `ErrorBadge` to `ComponentType` in `src/types/json-ui.ts`.
- Register `ErrorBadge` in `src/lib/json-ui/component-registry.tsx`.
- Add metadata/defaults to `src/lib/component-definitions.ts`.
- Flip status to `supported` in `json-components-registry.json`.
- [ ] **Notification**
- Add `Notification` to `ComponentType` in `src/types/json-ui.ts`.
- Register `Notification` in `src/lib/json-ui/component-registry.tsx`.
- Add metadata/defaults to `src/lib/component-definitions.ts`.
- Flip status to `supported` in `json-components-registry.json`.
- [ ] **StatusIcon**
- Add `StatusIcon` to `ComponentType` in `src/types/json-ui.ts`.
- Register `StatusIcon` in `src/lib/json-ui/component-registry.tsx`.
- Add metadata/defaults to `src/lib/component-definitions.ts`.
- Flip status to `supported` in `json-components-registry.json`.
### Data Components
- [ ] **DataList**
- Add `DataList` to `ComponentType` in `src/types/json-ui.ts`.
- Register `DataList` in `src/lib/json-ui/component-registry.tsx`.
- Add metadata/defaults to `src/lib/component-definitions.ts`.
- Flip status to `supported` in `json-components-registry.json`.
- [ ] **DataTable**
- Add `DataTable` to `ComponentType` in `src/types/json-ui.ts`.
- Register `DataTable` in `src/lib/json-ui/component-registry.tsx`.
- Add metadata/defaults to `src/lib/component-definitions.ts`.
- Flip status to `supported` in `json-components-registry.json`.
- [ ] **MetricCard**
- Add `MetricCard` to `ComponentType` in `src/types/json-ui.ts`.
- Register `MetricCard` in `src/lib/json-ui/component-registry.tsx`.
- Add metadata/defaults to `src/lib/component-definitions.ts`.
- Flip status to `supported` in `json-components-registry.json`.
- [ ] **Timeline**
- Add `Timeline` to `ComponentType` in `src/types/json-ui.ts`.
- Register `Timeline` in `src/lib/json-ui/component-registry.tsx`.
- Add metadata/defaults to `src/lib/component-definitions.ts`.
- Flip status to `supported` in `json-components-registry.json`.
## Framework Enablers
- [ ] **Event binding extensions**
- Expand event/action coverage to support richer interactions via JSON expressions.
- Confirm compatibility with existing `expression` and `valueTemplate` handling.
- [ ] **State binding system**
- Add support for stateful bindings needed by interactive components.
- Document and enforce which components require state binding.
- [ ] **JSON-friendly wrappers**
- Create wrapper components for hook-heavy/side-effect components.
- Register wrappers in the JSON registry instead of direct usage.
- [ ] **Registry normalization**
- Resolve duplicate component entries (e.g., multiple `Breadcrumb` variants) in `json-components-registry.json`.
- [ ] **Showcase schema coverage**
- Add JSON schema examples for each newly supported component to keep demos current.

View File

@@ -2,7 +2,7 @@
"$schema": "./schemas/json-components-registry-schema.json", "$schema": "./schemas/json-components-registry-schema.json",
"version": "2.0.0", "version": "2.0.0",
"description": "Registry of all components in the application", "description": "Registry of all components in the application",
"lastUpdated": "2026-01-17T22:10:22.582Z", "lastUpdated": "2026-01-18T11:30:24.191Z",
"categories": { "categories": {
"layout": "Layout and container components", "layout": "Layout and container components",
"input": "Form inputs and interactive controls", "input": "Form inputs and interactive controls",
@@ -611,7 +611,7 @@
"category": "display", "category": "display",
"canHaveChildren": false, "canHaveChildren": false,
"description": "Circular progress indicator", "description": "Circular progress indicator",
"status": "planned", "status": "supported",
"source": "atoms" "source": "atoms"
}, },
{ {
@@ -629,7 +629,7 @@
"category": "display", "category": "display",
"canHaveChildren": false, "canHaveChildren": false,
"description": "Visual section divider", "description": "Visual section divider",
"status": "planned", "status": "supported",
"source": "atoms" "source": "atoms"
}, },
{ {
@@ -719,7 +719,7 @@
"category": "display", "category": "display",
"canHaveChildren": false, "canHaveChildren": false,
"description": "Linear progress bar", "description": "Linear progress bar",
"status": "planned", "status": "supported",
"source": "atoms" "source": "atoms"
}, },
{ {
@@ -836,29 +836,10 @@
"name": "Breadcrumb", "name": "Breadcrumb",
"category": "navigation", "category": "navigation",
"canHaveChildren": false, "canHaveChildren": false,
"description": "Navigation breadcrumb trail", "description": "Navigation breadcrumb trail using the atoms/BreadcrumbNav JSON-safe items prop",
"status": "planned", "status": "supported",
"source": "atoms" "source": "atoms"
}, },
{
"type": "Breadcrumb",
"name": "Breadcrumb",
"category": "navigation",
"canHaveChildren": false,
"description": "Navigation breadcrumb trail",
"status": "json-compatible",
"source": "molecules",
"jsonCompatible": true
},
{
"type": "Breadcrumb",
"name": "Breadcrumb",
"category": "navigation",
"canHaveChildren": false,
"description": "Navigation breadcrumb trail",
"status": "planned",
"source": "ui"
},
{ {
"type": "ContextMenu", "type": "ContextMenu",
"name": "ContextMenu", "name": "ContextMenu",
@@ -1088,7 +1069,7 @@
"category": "feedback", "category": "feedback",
"canHaveChildren": false, "canHaveChildren": false,
"description": "Error state badge", "description": "Error state badge",
"status": "planned", "status": "supported",
"source": "atoms" "source": "atoms"
}, },
{ {
@@ -1164,7 +1145,7 @@
"category": "feedback", "category": "feedback",
"canHaveChildren": true, "canHaveChildren": true,
"description": "Toast notification", "description": "Toast notification",
"status": "planned", "status": "supported",
"source": "atoms" "source": "atoms"
}, },
{ {
@@ -1201,7 +1182,7 @@
"category": "feedback", "category": "feedback",
"canHaveChildren": false, "canHaveChildren": false,
"description": "Status indicator icon", "description": "Status indicator icon",
"status": "planned", "status": "supported",
"source": "atoms" "source": "atoms"
}, },
{ {
@@ -1219,7 +1200,7 @@
"category": "data", "category": "data",
"canHaveChildren": false, "canHaveChildren": false,
"description": "Styled data list", "description": "Styled data list",
"status": "planned", "status": "supported",
"source": "atoms" "source": "atoms"
}, },
{ {
@@ -1238,7 +1219,7 @@
"category": "data", "category": "data",
"canHaveChildren": false, "canHaveChildren": false,
"description": "Advanced data table with sorting and filtering", "description": "Advanced data table with sorting and filtering",
"status": "planned", "status": "supported",
"source": "atoms" "source": "atoms"
}, },
{ {
@@ -1304,7 +1285,7 @@
"category": "data", "category": "data",
"canHaveChildren": false, "canHaveChildren": false,
"description": "Metric display card", "description": "Metric display card",
"status": "planned", "status": "supported",
"source": "atoms" "source": "atoms"
}, },
{ {
@@ -1369,7 +1350,7 @@
"category": "data", "category": "data",
"canHaveChildren": false, "canHaveChildren": false,
"description": "Timeline visualization", "description": "Timeline visualization",
"status": "planned", "status": "supported",
"source": "atoms" "source": "atoms"
}, },
{ {
@@ -2043,24 +2024,24 @@
], ],
"statistics": { "statistics": {
"total": 219, "total": 219,
"supported": 150, "supported": 154,
"planned": 14, "planned": 10,
"jsonCompatible": 14, "jsonCompatible": 14,
"maybeJsonCompatible": 41, "maybeJsonCompatible": 41,
"byCategory": { "byCategory": {
"layout": 25, "layout": 25,
"input": 34, "input": 34,
"display": 31, "display": 31,
"navigation": 17, "navigation": 15,
"feedback": 23, "feedback": 23,
"data": 20, "data": 20,
"custom": 69 "custom": 69
}, },
"bySource": { "bySource": {
"atoms": 117, "atoms": 117,
"molecules": 41, "molecules": 40,
"organisms": 15, "organisms": 15,
"ui": 46 "ui": 45
} }
} }
} }

View File

@@ -3,11 +3,14 @@ import { AtomicComponentDemo } from '@/components/AtomicComponentDemo'
import { DashboardDemoPage } from '@/components/DashboardDemoPage' import { DashboardDemoPage } from '@/components/DashboardDemoPage'
import { PageRenderer } from '@/lib/json-ui/page-renderer' import { PageRenderer } from '@/lib/json-ui/page-renderer'
import { hydrateSchema } from '@/schemas/schema-loader' import { hydrateSchema } from '@/schemas/schema-loader'
import pageSchemasJson from '@/schemas/page-schemas.json'
import todoListJson from '@/schemas/todo-list.json' import todoListJson from '@/schemas/todo-list.json'
import newMoleculesShowcaseJson from '@/schemas/new-molecules-showcase.json' import newMoleculesShowcaseJson from '@/schemas/new-molecules-showcase.json'
const todoListSchema = hydrateSchema(todoListJson) const todoListSchema = hydrateSchema(todoListJson)
const newMoleculesShowcaseSchema = hydrateSchema(newMoleculesShowcaseJson) const newMoleculesShowcaseSchema = hydrateSchema(newMoleculesShowcaseJson)
const dataComponentsDemoSchema = hydrateSchema(pageSchemasJson.dataComponentsDemoSchema)
const feedbackAtomsDemoSchema = hydrateSchema(pageSchemasJson.feedbackAtomsDemoSchema)
export function JSONUIShowcasePage() { export function JSONUIShowcasePage() {
return ( return (
@@ -24,7 +27,9 @@ export function JSONUIShowcasePage() {
</div> </div>
<TabsList className="w-full justify-start"> <TabsList className="w-full justify-start">
<TabsTrigger value="atomic">Atomic Components</TabsTrigger> <TabsTrigger value="atomic">Atomic Components</TabsTrigger>
<TabsTrigger value="feedback">Feedback Atoms</TabsTrigger>
<TabsTrigger value="molecules">New Molecules</TabsTrigger> <TabsTrigger value="molecules">New Molecules</TabsTrigger>
<TabsTrigger value="data-components">Data Components</TabsTrigger>
<TabsTrigger value="dashboard">JSON Dashboard</TabsTrigger> <TabsTrigger value="dashboard">JSON Dashboard</TabsTrigger>
<TabsTrigger value="todos">JSON Todo List</TabsTrigger> <TabsTrigger value="todos">JSON Todo List</TabsTrigger>
</TabsList> </TabsList>
@@ -34,10 +39,18 @@ export function JSONUIShowcasePage() {
<TabsContent value="atomic" className="h-full m-0 data-[state=active]:block"> <TabsContent value="atomic" className="h-full m-0 data-[state=active]:block">
<AtomicComponentDemo /> <AtomicComponentDemo />
</TabsContent> </TabsContent>
<TabsContent value="feedback" className="h-full m-0 data-[state=active]:block">
<PageRenderer schema={feedbackAtomsDemoSchema} />
</TabsContent>
<TabsContent value="molecules" className="h-full m-0 data-[state=active]:block"> <TabsContent value="molecules" className="h-full m-0 data-[state=active]:block">
<PageRenderer schema={newMoleculesShowcaseSchema} /> <PageRenderer schema={newMoleculesShowcaseSchema} />
</TabsContent> </TabsContent>
<TabsContent value="data-components" className="h-full m-0 data-[state=active]:block">
<PageRenderer schema={dataComponentsDemoSchema} />
</TabsContent>
<TabsContent value="dashboard" className="h-full m-0 data-[state=active]:block"> <TabsContent value="dashboard" className="h-full m-0 data-[state=active]:block">
<DashboardDemoPage /> <DashboardDemoPage />

View File

@@ -17,19 +17,19 @@ export function BreadcrumbNav({ items, className }: BreadcrumbNavProps) {
<nav aria-label="Breadcrumb" className={cn('flex items-center gap-2', className)}> <nav aria-label="Breadcrumb" className={cn('flex items-center gap-2', className)}>
{items.map((item, index) => { {items.map((item, index) => {
const isLast = index === items.length - 1 const isLast = index === items.length - 1
const linkClassName = cn(
'text-sm transition-colors',
isLast ? 'text-foreground font-medium' : 'text-muted-foreground hover:text-foreground'
)
return ( return (
<div key={index} className="flex items-center gap-2"> <div key={index} className="flex items-center gap-2">
{item.href || item.onClick ? ( {item.href ? (
<button <a href={item.href} onClick={item.onClick} className={linkClassName}>
onClick={item.onClick} {item.label}
className={cn( </a>
'text-sm transition-colors', ) : item.onClick ? (
isLast <button onClick={item.onClick} className={linkClassName}>
? 'text-foreground font-medium'
: 'text-muted-foreground hover:text-foreground'
)}
>
{item.label} {item.label}
</button> </button>
) : ( ) : (
@@ -49,3 +49,5 @@ export function BreadcrumbNav({ items, className }: BreadcrumbNavProps) {
</nav> </nav>
) )
} }
export const Breadcrumb = BreadcrumbNav

View File

@@ -3,10 +3,11 @@ import { cn } from '@/lib/utils'
export interface DataListProps { export interface DataListProps {
items: any[] items: any[]
renderItem: (item: any, index: number) => ReactNode renderItem?: (item: any, index: number) => ReactNode
emptyMessage?: string emptyMessage?: string
className?: string className?: string
itemClassName?: string itemClassName?: string
itemKey?: string
} }
export function DataList({ export function DataList({
@@ -15,6 +16,7 @@ export function DataList({
emptyMessage = 'No items', emptyMessage = 'No items',
className, className,
itemClassName, itemClassName,
itemKey,
}: DataListProps) { }: DataListProps) {
if (items.length === 0) { if (items.length === 0) {
return ( return (
@@ -24,11 +26,28 @@ export function DataList({
) )
} }
const renderFallbackItem = (item: any) => {
if (itemKey && item && typeof item === 'object') {
const value = item[itemKey]
if (value !== undefined && value !== null) {
return typeof value === 'string' || typeof value === 'number'
? value
: JSON.stringify(value)
}
}
if (typeof item === 'string' || typeof item === 'number') {
return item
}
return JSON.stringify(item)
}
return ( return (
<div className={cn('space-y-2', className)}> <div className={cn('space-y-2', className)}>
{items.map((item, index) => ( {items.map((item, index) => (
<div key={index} className={cn('transition-colors', itemClassName)}> <div key={index} className={cn('transition-colors', itemClassName)}>
{renderItem(item, index)} {renderItem ? renderItem(item, index) : renderFallbackItem(item)}
</div> </div>
))} ))}
</div> </div>

View File

@@ -54,7 +54,7 @@ export { Timestamp } from './Timestamp'
export { ScrollArea } from './ScrollArea' export { ScrollArea } from './ScrollArea'
export { Tag } from './Tag' export { Tag } from './Tag'
export { BreadcrumbNav } from './Breadcrumb' export { Breadcrumb, BreadcrumbNav } from './Breadcrumb'
export { IconText } from './IconText' export { IconText } from './IconText'
export { TextArea } from './TextArea' export { TextArea } from './TextArea'
export { Input } from './Input' export { Input } from './Input'
@@ -118,4 +118,3 @@ export { MetricDisplay } from './MetricDisplay'
export { KeyValue } from './KeyValue' export { KeyValue } from './KeyValue'
export { EmptyMessage } from './EmptyMessage' export { EmptyMessage } from './EmptyMessage'
export { StepIndicator } from './StepIndicator' export { StepIndicator } from './StepIndicator'

View File

@@ -7,6 +7,23 @@ export interface ComponentDefinition {
icon: string icon: string
defaultProps?: Record<string, any> defaultProps?: Record<string, any>
canHaveChildren?: boolean canHaveChildren?: boolean
props?: ComponentPropDefinition[]
events?: ComponentEventDefinition[]
}
export interface ComponentPropDefinition {
name: string
type: string
description: string
required?: boolean
defaultValue?: string
options?: string[]
supportsBinding?: boolean
}
export interface ComponentEventDefinition {
name: string
description: string
} }
export const componentDefinitions: ComponentDefinition[] = [ export const componentDefinitions: ComponentDefinition[] = [
@@ -194,6 +211,101 @@ export const componentDefinitions: ComponentDefinition[] = [
icon: 'CircleNotch', icon: 'CircleNotch',
defaultProps: { value: 50 } defaultProps: { value: 50 }
}, },
{
type: 'ProgressBar',
label: 'Progress Bar',
category: 'display',
icon: 'CircleNotch',
defaultProps: { value: 50, max: 100, size: 'md', variant: 'default', showLabel: false },
props: [
{
name: 'value',
type: 'number',
description: 'Current progress value.',
required: true,
supportsBinding: true,
},
{
name: 'max',
type: 'number',
description: 'Maximum progress value.',
defaultValue: '100',
supportsBinding: true,
},
{
name: 'size',
type: 'string',
description: 'Height preset for the bar.',
defaultValue: 'md',
options: ['sm', 'md', 'lg'],
},
{
name: 'variant',
type: 'string',
description: 'Color variant for the bar.',
defaultValue: 'default',
options: ['default', 'accent', 'destructive'],
},
{
name: 'showLabel',
type: 'boolean',
description: 'Whether to render the percentage label below the bar.',
defaultValue: 'false',
},
{
name: 'className',
type: 'string',
description: 'Optional custom classes for the bar container.',
},
],
},
{
type: 'CircularProgress',
label: 'Circular Progress',
category: 'display',
icon: 'CircleNotch',
defaultProps: { value: 65, max: 100, size: 'md', showLabel: true },
props: [
{
name: 'value',
type: 'number',
description: 'Current progress value.',
required: true,
supportsBinding: true,
},
{
name: 'max',
type: 'number',
description: 'Maximum progress value.',
defaultValue: '100',
supportsBinding: true,
},
{
name: 'size',
type: 'string',
description: 'Size preset for the circular indicator.',
defaultValue: 'md',
options: ['sm', 'md', 'lg', 'xl'],
},
{
name: 'showLabel',
type: 'boolean',
description: 'Whether to show the percentage label inside the circle.',
defaultValue: 'true',
},
{
name: 'strokeWidth',
type: 'number',
description: 'Custom stroke width for the circle outline.',
supportsBinding: true,
},
{
name: 'className',
type: 'string',
description: 'Optional custom classes for the wrapper.',
},
],
},
{ {
type: 'Spinner', type: 'Spinner',
label: 'Spinner', label: 'Spinner',
@@ -215,6 +327,33 @@ export const componentDefinitions: ComponentDefinition[] = [
icon: 'Minus', icon: 'Minus',
defaultProps: {} defaultProps: {}
}, },
{
type: 'Divider',
label: 'Divider',
category: 'display',
icon: 'Minus',
defaultProps: { orientation: 'horizontal', decorative: true },
props: [
{
name: 'orientation',
type: 'string',
description: 'Layout direction for the divider.',
defaultValue: 'horizontal',
options: ['horizontal', 'vertical'],
},
{
name: 'decorative',
type: 'boolean',
description: 'Whether the divider is purely decorative.',
defaultValue: 'true',
},
{
name: 'className',
type: 'string',
description: 'Optional custom classes for spacing or styling.',
},
],
},
// Navigation Components // Navigation Components
{ {
type: 'Link', type: 'Link',
@@ -224,6 +363,19 @@ export const componentDefinitions: ComponentDefinition[] = [
canHaveChildren: true, canHaveChildren: true,
defaultProps: { href: '#', children: 'Link' } defaultProps: { href: '#', children: 'Link' }
}, },
{
type: 'Breadcrumb',
label: 'Breadcrumb',
category: 'navigation',
icon: 'CaretRight',
canHaveChildren: false,
defaultProps: {
items: [
{ label: 'Home', href: '/' },
{ label: 'Current' }
]
}
},
// Feedback Components // Feedback Components
{ {
type: 'Alert', type: 'Alert',
@@ -256,6 +408,107 @@ export const componentDefinitions: ComponentDefinition[] = [
icon: 'Circle', icon: 'Circle',
defaultProps: { status: 'active', children: 'Active' } defaultProps: { status: 'active', children: 'Active' }
}, },
{
type: 'ErrorBadge',
label: 'Error Badge',
category: 'feedback',
icon: 'WarningCircle',
defaultProps: { count: 3, variant: 'destructive', size: 'md' },
props: [
{
name: 'count',
type: 'number',
description: 'Number of errors to display. Hidden when set to 0.',
required: true,
supportsBinding: true,
},
{
name: 'variant',
type: 'string',
description: 'Visual variant for the badge.',
defaultValue: 'destructive',
options: ['default', 'destructive'],
},
{
name: 'size',
type: 'string',
description: 'Badge size.',
defaultValue: 'md',
options: ['sm', 'md'],
},
],
},
{
type: 'Notification',
label: 'Notification',
category: 'feedback',
icon: 'Info',
defaultProps: { type: 'info', title: 'Notification', message: 'Details go here.' },
props: [
{
name: 'type',
type: 'string',
description: 'Notification style variant.',
required: true,
options: ['info', 'success', 'warning', 'error'],
},
{
name: 'title',
type: 'string',
description: 'Primary notification title.',
required: true,
supportsBinding: true,
},
{
name: 'message',
type: 'string',
description: 'Optional supporting message text.',
supportsBinding: true,
},
{
name: 'className',
type: 'string',
description: 'Optional custom classes for spacing or layout tweaks.',
},
],
events: [
{
name: 'onClose',
description: 'Fires when the close button is clicked. Bind to dismiss or trigger an action.',
},
],
},
{
type: 'StatusIcon',
label: 'Status Icon',
category: 'feedback',
icon: 'CheckCircle',
defaultProps: { type: 'saved', size: 14, animate: false },
props: [
{
name: 'type',
type: 'string',
description: 'Status icon style.',
required: true,
supportsBinding: true,
options: ['saved', 'synced'],
},
{
name: 'size',
type: 'number',
description: 'Icon size in pixels.',
defaultValue: '14',
supportsBinding: true,
},
{
name: 'animate',
type: 'boolean',
description: 'Applies entry animation when true.',
defaultValue: 'false',
supportsBinding: true,
},
],
},
// Data Components // Data Components
{ {
type: 'List', type: 'List',
@@ -264,6 +517,17 @@ export const componentDefinitions: ComponentDefinition[] = [
icon: 'List', icon: 'List',
defaultProps: { items: [], emptyMessage: 'No items' } defaultProps: { items: [], emptyMessage: 'No items' }
}, },
{
type: 'DataList',
label: 'Data List',
category: 'data',
icon: 'List',
defaultProps: {
items: ['Daily summary', 'New signups', 'Pending approvals'],
emptyMessage: 'No updates',
itemClassName: 'rounded-md border border-border bg-card/50 px-4 py-2'
}
},
{ {
type: 'Table', type: 'Table',
label: 'Table', label: 'Table',
@@ -271,6 +535,25 @@ export const componentDefinitions: ComponentDefinition[] = [
icon: 'Table', icon: 'Table',
defaultProps: { data: [], columns: [] } defaultProps: { data: [], columns: [] }
}, },
{
type: 'DataTable',
label: 'Data Table',
category: 'data',
icon: 'Table',
defaultProps: {
columns: [
{ key: 'name', header: 'Name' },
{ key: 'status', header: 'Status' },
{ key: 'owner', header: 'Owner' },
],
data: [
{ name: 'Launch Plan', status: 'In Progress', owner: 'Avery' },
{ name: 'Design Review', status: 'Scheduled', owner: 'Jordan' },
{ name: 'QA Checklist', status: 'Done', owner: 'Riley' },
],
emptyMessage: 'No records available',
}
},
{ {
type: 'KeyValue', type: 'KeyValue',
label: 'Key Value', label: 'Key Value',
@@ -285,6 +568,45 @@ export const componentDefinitions: ComponentDefinition[] = [
icon: 'ChartBar', icon: 'ChartBar',
defaultProps: { title: 'Metric', value: '0' } defaultProps: { title: 'Metric', value: '0' }
}, },
{
type: 'MetricCard',
label: 'Metric Card',
category: 'data',
icon: 'ChartBar',
defaultProps: {
label: 'Active Users',
value: '1,248',
trend: { value: 12.4, direction: 'up' },
}
},
{
type: 'Timeline',
label: 'Timeline',
category: 'data',
icon: 'Clock',
defaultProps: {
items: [
{
title: 'Planning',
description: 'Finalize milestones',
timestamp: 'Mon 9:00 AM',
status: 'completed',
},
{
title: 'Execution',
description: 'Kick off delivery',
timestamp: 'Tue 11:00 AM',
status: 'current',
},
{
title: 'Review',
description: 'Collect feedback',
timestamp: 'Wed 3:00 PM',
status: 'pending',
},
],
}
},
// Custom Components // Custom Components
{ {
type: 'DataCard', type: 'DataCard',

View File

@@ -91,16 +91,49 @@ Connect UI to data sources:
### Event Handling ### Event Handling
Respond to user interactions: Respond to user interactions using a JSON event map. Each entry maps an event name to an action definition:
```json ```json
{ {
"events": { "events": {
"onClick": "save-data" "onClick": {
"action": "save-data",
"payload": {
"source": "profile"
}
}
} }
} }
``` ```
You can also pass full action arrays when needed:
```json
{
"events": {
"change": {
"actions": [
{ "id": "set-name", "type": "set-value", "target": "userName" }
]
}
}
}
```
#### Supported events
Events map directly to React handler props, so common values include:
- `click` / `onClick`
- `change` / `onChange`
- `submit` / `onSubmit`
- `focus` / `onFocus`
- `blur` / `onBlur`
- `keyDown` / `onKeyDown`
- `keyUp` / `onKeyUp`
- `mouseEnter` / `onMouseEnter`
- `mouseLeave` / `onMouseLeave`
### Looping ### Looping
Render lists from arrays: Render lists from arrays:

View File

@@ -136,10 +136,24 @@ export const shadcnComponents: UIComponentRegistry = {
AvatarImage, AvatarImage,
} }
export const atomComponents: UIComponentRegistry = buildRegistryFromNames( export const atomComponents: UIComponentRegistry = {
atomRegistryNames, ...buildRegistryFromNames(
AtomComponents as Record<string, ComponentType<any>> atomRegistryNames,
) AtomComponents as Record<string, ComponentType<any>>
),
CircularProgress: (AtomComponents as Record<string, ComponentType<any>>).CircularProgress,
Divider: (AtomComponents as Record<string, ComponentType<any>>).Divider,
DataList: (AtomComponents as Record<string, ComponentType<any>>).DataList,
DataTable: (AtomComponents as Record<string, ComponentType<any>>).DataTable,
MetricCard: (AtomComponents as Record<string, ComponentType<any>>).MetricCard,
ProgressBar: (AtomComponents as Record<string, ComponentType<any>>).ProgressBar,
Timeline: (AtomComponents as Record<string, ComponentType<any>>).Timeline,
}
const breadcrumbComponent = AtomComponents.Breadcrumb ?? AtomComponents.BreadcrumbNav
if (breadcrumbComponent) {
atomComponents.Breadcrumb = breadcrumbComponent as ComponentType<any>
}
export const moleculeComponents: UIComponentRegistry = buildRegistryFromNames( export const moleculeComponents: UIComponentRegistry = buildRegistryFromNames(
moleculeRegistryNames, moleculeRegistryNames,

View File

@@ -1,33 +1,101 @@
import { createElement, useMemo, Fragment } from 'react' import { createElement, useMemo, Fragment } from 'react'
import { UIComponent, Binding, ComponentRendererProps } from '@/types/json-ui' import { UIComponent, Binding, ComponentRendererProps, EventHandler, JSONEventDefinition, JSONEventMap } from '@/types/json-ui'
import { getUIComponent } from './component-registry' import { getUIComponent } from './component-registry'
import { resolveDataBinding, evaluateCondition } from './utils' import { resolveDataBinding, evaluateCondition } from './utils'
function resolveBinding(binding: Binding, data: Record<string, unknown>): unknown { function resolveBinding(
return resolveDataBinding(binding, data) binding: Binding,
data: Record<string, unknown>,
context: Record<string, unknown>,
state?: Record<string, unknown>
): unknown {
return resolveDataBinding(binding, data, context, { state, bindings: context })
} }
export function ComponentRenderer({ component, data, context = {}, onEvent }: ComponentRendererProps) { export function ComponentRenderer({ component, data, context = {}, state, onEvent }: ComponentRendererProps) {
const mergedData = useMemo(() => ({ ...data, ...context }), [data, context]) const mergedData = useMemo(() => ({ ...data, ...context }), [data, context])
const resolvedEventHandlers = useMemo(() => {
const normalizeEventName = (eventName: string) =>
eventName.startsWith('on') && eventName.length > 2
? `${eventName.charAt(2).toLowerCase()}${eventName.slice(3)}`
: eventName
const normalizeDefinition = (eventName: string, definition: JSONEventDefinition | string): EventHandler | null => {
if (!definition) return null
const normalizedEventName = normalizeEventName(eventName)
if (typeof definition === 'string') {
return {
event: normalizedEventName,
actions: [{ id: definition, type: 'custom' }],
}
}
if (definition.actions?.length) {
const actions = definition.payload
? definition.actions.map((action) => ({
...action,
params: action.params ?? definition.payload,
}))
: definition.actions
return {
event: normalizedEventName,
actions,
condition: definition.condition,
}
}
if (definition.action) {
return {
event: normalizedEventName,
actions: [{ id: definition.action, type: 'custom', params: definition.payload }],
condition: definition.condition,
}
}
return null
}
if (!component.events) {
return [] as EventHandler[]
}
if (Array.isArray(component.events)) {
return component.events.map((handler) => ({
...handler,
event: normalizeEventName(handler.event),
}))
}
const eventMap = component.events as JSONEventMap
return Object.entries(eventMap).flatMap(([eventName, definition]) => {
if (Array.isArray(definition)) {
return definition
.map((entry) => normalizeDefinition(eventName, entry))
.filter(Boolean) as EventHandler[]
}
const normalized = normalizeDefinition(eventName, definition)
return normalized ? [normalized] : []
})
}, [component.events])
const resolvedProps = useMemo(() => { const resolvedProps = useMemo(() => {
const resolved: Record<string, unknown> = { ...component.props } const resolved: Record<string, unknown> = { ...component.props }
if (component.bindings) { if (component.bindings) {
Object.entries(component.bindings).forEach(([propName, binding]) => { Object.entries(component.bindings).forEach(([propName, binding]) => {
resolved[propName] = resolveBinding(binding, mergedData) resolved[propName] = resolveBinding(binding, data, context, state)
}) })
} }
if (component.dataBinding) { if (component.dataBinding) {
const boundData = resolveDataBinding(component.dataBinding, mergedData) const boundData = resolveDataBinding(component.dataBinding, data, context, { state, bindings: context })
if (boundData !== undefined) { if (boundData !== undefined) {
resolved.value = boundData resolved.value = boundData
resolved.data = boundData resolved.data = boundData
} }
} }
if (component.events && onEvent) { if (resolvedEventHandlers.length > 0 && onEvent) {
component.events.forEach(handler => { resolvedEventHandlers.forEach(handler => {
resolved[`on${handler.event.charAt(0).toUpperCase()}${handler.event.slice(1)}`] = (e: unknown) => { resolved[`on${handler.event.charAt(0).toUpperCase()}${handler.event.slice(1)}`] = (e: unknown) => {
const conditionMet = !handler.condition const conditionMet = !handler.condition
|| (typeof handler.condition === 'function' || (typeof handler.condition === 'function'
@@ -51,7 +119,7 @@ export function ComponentRenderer({ component, data, context = {}, onEvent }: Co
} }
return resolved return resolved
}, [component, mergedData, onEvent]) }, [component, data, context, state, mergedData, onEvent])
const Component = getUIComponent(component.type) const Component = getUIComponent(component.type)
@@ -78,6 +146,7 @@ export function ComponentRenderer({ component, data, context = {}, onEvent }: Co
component={child} component={child}
data={data} data={data}
context={renderContext} context={renderContext}
state={state}
onEvent={onEvent} onEvent={onEvent}
/> />
)} )}
@@ -103,6 +172,7 @@ export function ComponentRenderer({ component, data, context = {}, onEvent }: Co
component={child} component={child}
data={data} data={data}
context={renderContext} context={renderContext}
state={state}
onEvent={onEvent} onEvent={onEvent}
/> />
)} )}
@@ -114,6 +184,7 @@ export function ComponentRenderer({ component, data, context = {}, onEvent }: Co
component={branch} component={branch}
data={data} data={data}
context={renderContext} context={renderContext}
state={state}
onEvent={onEvent} onEvent={onEvent}
/> />
) )
@@ -135,7 +206,7 @@ export function ComponentRenderer({ component, data, context = {}, onEvent }: Co
} }
if (component.loop) { if (component.loop) {
const items = resolveDataBinding(component.loop.source, mergedData) || [] const items = resolveDataBinding(component.loop.source, data, context, { state, bindings: context }) || []
const loopChildren = items.map((item: unknown, index: number) => { const loopChildren = items.map((item: unknown, index: number) => {
const loopContext = { const loopContext = {
...context, ...context,
@@ -153,7 +224,7 @@ export function ComponentRenderer({ component, data, context = {}, onEvent }: Co
} }
if (component.condition) { if (component.condition) {
const conditionValue = resolveBinding(component.condition, { ...data, ...loopContext }) const conditionValue = resolveBinding(component.condition, data, loopContext, state)
if (!conditionValue) { if (!conditionValue) {
return null return null
} }
@@ -177,7 +248,7 @@ export function ComponentRenderer({ component, data, context = {}, onEvent }: Co
} }
if (component.condition) { if (component.condition) {
const conditionValue = resolveBinding(component.condition, mergedData) const conditionValue = resolveBinding(component.condition, data, context, state)
if (!conditionValue) { if (!conditionValue) {
return null return null
} }

View File

@@ -2,6 +2,7 @@ import { useCallback } from 'react'
import { PageSchema } from '@/types/json-ui' import { PageSchema } from '@/types/json-ui'
import { useDataSources } from '@/hooks/data/use-data-sources' import { useDataSources } from '@/hooks/data/use-data-sources'
import { useActionExecutor } from '@/hooks/ui/use-action-executor' import { useActionExecutor } from '@/hooks/ui/use-action-executor'
import { useAppSelector } from '@/store'
import { ComponentRenderer } from './component-renderer' import { ComponentRenderer } from './component-renderer'
interface PageRendererProps { interface PageRendererProps {
@@ -11,6 +12,7 @@ interface PageRendererProps {
export function PageRenderer({ schema, onCustomAction }: PageRendererProps) { export function PageRenderer({ schema, onCustomAction }: PageRendererProps) {
const { data, updateData, updatePath } = useDataSources(schema.dataSources) const { data, updateData, updatePath } = useDataSources(schema.dataSources)
const state = useAppSelector((rootState) => rootState)
const context = { const context = {
data, data,
@@ -32,6 +34,7 @@ export function PageRenderer({ schema, onCustomAction }: PageRendererProps) {
key={component.id || index} key={component.id || index}
component={component} component={component}
data={data} data={data}
state={state}
onEvent={handleEvent} onEvent={handleEvent}
/> />
))} ))}

View File

@@ -1,5 +1,13 @@
import React, { useCallback } from 'react' import React, { useCallback } from 'react'
import type { Action, EventHandler, JSONFormRendererProps, JSONUIRendererProps, UIComponent } from './types' import type {
Action,
EventHandler,
JSONEventDefinition,
JSONEventMap,
JSONFormRendererProps,
JSONUIRendererProps,
UIComponent,
} from './types'
import { getUIComponent } from './component-registry' import { getUIComponent } from './component-registry'
import { resolveDataBinding, evaluateCondition } from './utils' import { resolveDataBinding, evaluateCondition } from './utils'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
@@ -79,35 +87,39 @@ export function JSONUIRenderer({
? eventName ? eventName
: `on${eventName.charAt(0).toUpperCase()}${eventName.slice(1)}` : `on${eventName.charAt(0).toUpperCase()}${eventName.slice(1)}`
const normalizeLegacyHandler = (eventName: string, handler: any): EventHandler | null => { const normalizeEventDefinition = (
if (!handler) return null eventName: string,
definition: JSONEventDefinition | string
): EventHandler | null => {
if (!definition) return null
const normalizedEvent = normalizeEventName(eventName)
if (typeof handler === 'string') { if (typeof definition === 'string') {
return { return {
event: normalizeEventName(eventName), event: normalizedEvent,
actions: [{ id: handler, type: 'custom' }], actions: [{ id: definition, type: 'custom' }],
} }
} }
if (handler.actions && Array.isArray(handler.actions)) { if (definition.actions && Array.isArray(definition.actions)) {
const actions = definition.payload
? definition.actions.map((action) => ({
...action,
params: action.params ?? definition.payload,
}))
: definition.actions
return { return {
event: normalizeEventName(eventName), event: normalizedEvent,
actions: handler.actions as Action[], actions,
condition: handler.condition, condition: definition.condition,
} }
} }
if (typeof handler === 'object' && handler.action) { if (definition.action) {
return { return {
event: normalizeEventName(eventName), event: normalizedEvent,
actions: [ actions: [{ id: definition.action, type: 'custom', params: definition.payload }],
{ condition: definition.condition,
id: handler.action,
type: 'custom',
target: handler.target,
params: handler.params,
},
],
} }
} }
@@ -119,11 +131,20 @@ export function JSONUIRenderer({
renderContext: Record<string, unknown> renderContext: Record<string, unknown>
) => { ) => {
const eventHandlers: EventHandler[] = Array.isArray(component.events) const eventHandlers: EventHandler[] = Array.isArray(component.events)
? component.events ? component.events.map((handler) => ({
...handler,
event: normalizeEventName(handler.event),
}))
: component.events : component.events
? Object.entries(component.events).map(([eventName, handler]) => ? Object.entries(component.events as JSONEventMap).flatMap(([eventName, handler]) => {
normalizeLegacyHandler(eventName, handler) if (Array.isArray(handler)) {
).filter(Boolean) as EventHandler[] return handler
.map((entry) => normalizeEventDefinition(eventName, entry))
.filter(Boolean) as EventHandler[]
}
const normalized = normalizeEventDefinition(eventName, handler)
return normalized ? [normalized] : []
})
: [] : []
if (eventHandlers.length > 0) { if (eventHandlers.length > 0) {

View File

@@ -48,6 +48,22 @@ export const EventHandlerSchema = z.object({
condition: z.any().optional(), condition: z.any().optional(),
}) })
export const JSONEventDefinitionSchema = z.object({
action: z.string().optional(),
actions: z.array(ActionSchema).optional(),
payload: z.record(z.string(), z.any()).optional(),
condition: z.any().optional(),
})
export const JSONEventMapSchema = z.record(
z.string(),
z.union([
z.string(),
JSONEventDefinitionSchema,
z.array(JSONEventDefinitionSchema),
])
)
export const ConditionalSchema = z.object({ export const ConditionalSchema = z.object({
if: z.string(), if: z.string(),
then: z.any().optional(), then: z.any().optional(),
@@ -69,7 +85,7 @@ export const UIComponentSchema: any = z.object({
z.string(), z.string(),
DataBindingSchema, DataBindingSchema,
]).optional(), ]).optional(),
events: z.array(EventHandlerSchema).optional(), events: z.union([z.array(EventHandlerSchema), JSONEventMapSchema]).optional(),
conditional: ConditionalSchema.optional(), conditional: ConditionalSchema.optional(),
loop: z.object({ loop: z.object({
source: z.string(), source: z.string(),
@@ -242,6 +258,8 @@ export type UIValue = z.infer<typeof UIValueSchema>
export type DataBinding = z.infer<typeof DataBindingSchema> export type DataBinding = z.infer<typeof DataBindingSchema>
export type Action = z.infer<typeof ActionSchema> export type Action = z.infer<typeof ActionSchema>
export type EventHandler = z.infer<typeof EventHandlerSchema> export type EventHandler = z.infer<typeof EventHandlerSchema>
export type JSONEventDefinition = z.infer<typeof JSONEventDefinitionSchema>
export type JSONEventMap = z.infer<typeof JSONEventMapSchema>
export type Conditional = z.infer<typeof ConditionalSchema> export type Conditional = z.infer<typeof ConditionalSchema>
export type UIComponent = z.infer<typeof UIComponentSchema> export type UIComponent = z.infer<typeof UIComponentSchema>
export type FormField = z.infer<typeof FormFieldSchema> export type FormField = z.infer<typeof FormFieldSchema>

View File

@@ -1,6 +1,6 @@
import type { Action, EventHandler, FormField, UIComponent } from './schema' import type { Action, EventHandler, FormField, JSONEventDefinition, JSONEventMap, UIComponent } from './schema'
export type { Action, EventHandler, FormField, UIComponent } export type { Action, EventHandler, FormField, JSONEventDefinition, JSONEventMap, UIComponent }
export interface JSONUIRendererProps { export interface JSONUIRendererProps {
component: UIComponent component: UIComponent

View File

@@ -1,23 +1,43 @@
type BindingTransform = string | ((data: unknown) => unknown) type BindingTransform = string | ((data: unknown) => unknown)
interface BindingSourceOptions {
state?: Record<string, any>
bindings?: Record<string, any>
}
export function resolveDataBinding( export function resolveDataBinding(
binding: string | { source: string; path?: string; transform?: BindingTransform }, binding: string | { source: string; sourceType?: 'data' | 'bindings' | 'state'; path?: string; transform?: BindingTransform },
dataMap: Record<string, any>, dataMap: Record<string, any>,
context: Record<string, any> = {}, context: Record<string, any> = {},
options: BindingSourceOptions = {},
): any { ): any {
const mergedContext = { ...dataMap, ...context } const mergedContext = { ...dataMap, ...context }
const stateSource = options.state ?? {}
const bindingsSource = options.bindings ?? context
if (typeof binding === 'string') { if (typeof binding === 'string') {
if (binding.startsWith('state.')) {
return getNestedValue(stateSource, binding.slice('state.'.length))
}
if (binding.startsWith('bindings.')) {
return getNestedValue(bindingsSource, binding.slice('bindings.'.length))
}
if (binding.includes('.')) { if (binding.includes('.')) {
return getNestedValue(mergedContext, binding) return getNestedValue(mergedContext, binding)
} }
return mergedContext[binding] return mergedContext[binding]
} }
const { source, path, transform } = binding const { source, sourceType, path, transform } = binding
const sourceContext =
sourceType === 'state'
? stateSource
: sourceType === 'bindings'
? bindingsSource
: mergedContext
const sourceValue = source.includes('.') const sourceValue = source.includes('.')
? getNestedValue(mergedContext, source) ? getNestedValue(sourceContext, source)
: mergedContext[source] : sourceContext[source]
const resolvedValue = path ? getNestedValue(sourceValue, path) : sourceValue const resolvedValue = path ? getNestedValue(sourceValue, path) : sourceValue
return applyTransform(resolvedValue, transform) return applyTransform(resolvedValue, transform)

View File

@@ -0,0 +1,295 @@
{
"stateBindingsDemoSchema": {
"id": "state-bindings-demo",
"name": "State & Bindings Demo",
"layout": {
"type": "single"
},
"dataSources": [
{
"id": "statusItems",
"type": "static",
"defaultValue": ["KV Ready", "Components Loaded", "Sync Enabled"]
}
],
"components": [
{
"id": "state-demo-root",
"type": "div",
"props": {
"className": "space-y-4 rounded-lg border border-border bg-card p-6"
},
"children": [
{
"id": "state-demo-title",
"type": "Heading",
"props": {
"className": "text-xl font-semibold",
"children": "Renderer State Binding Demo"
}
},
{
"id": "state-demo-theme",
"type": "Text",
"props": {
"className": "text-sm text-muted-foreground"
},
"bindings": {
"children": {
"sourceType": "state",
"source": "settings",
"path": "settings.theme"
}
}
},
{
"id": "state-demo-list",
"type": "div",
"props": {
"className": "space-y-2"
},
"loop": {
"source": "statusItems",
"itemVar": "statusItem"
},
"children": [
{
"id": "state-demo-list-item",
"type": "Text",
"props": {
"className": "text-sm"
},
"bindings": {
"children": {
"sourceType": "bindings",
"source": "statusItem"
}
}
}
]
}
]
}
]
},
"dataComponentsDemoSchema": {
"id": "data-components-demo",
"name": "Data Components Demo",
"layout": {
"type": "single"
},
"dataSources": [
{
"id": "metricCards",
"type": "static",
"defaultValue": [
{ "label": "Active Users", "value": 1248, "trend": { "value": 12.4, "direction": "up" } },
{ "label": "Churn Rate", "value": "3.2%", "trend": { "value": 1.1, "direction": "down" } },
{ "label": "Net Revenue", "value": "$48.3k", "trend": { "value": 6.8, "direction": "up" } }
]
},
{
"id": "tableColumns",
"type": "static",
"defaultValue": [
{ "key": "initiative", "header": "Initiative" },
{ "key": "owner", "header": "Owner" },
{ "key": "status", "header": "Status" }
]
},
{
"id": "tableRows",
"type": "static",
"defaultValue": [
{ "initiative": "Landing Page", "owner": "Avery", "status": "In Progress" },
{ "initiative": "Retention Emails", "owner": "Jordan", "status": "Review" },
{ "initiative": "Billing Update", "owner": "Riley", "status": "Done" }
]
},
{
"id": "listItems",
"type": "static",
"defaultValue": ["Prepare briefing deck", "Confirm stakeholder approvals", "Publish roadmap update"]
},
{
"id": "timelineItems",
"type": "static",
"defaultValue": [
{
"title": "Kickoff",
"description": "Align on scope and milestones",
"timestamp": "Mon 9:00 AM",
"status": "completed"
},
{
"title": "Execution",
"description": "Deliver initial workstream",
"timestamp": "Tue 11:00 AM",
"status": "current"
},
{
"title": "Review",
"description": "Stakeholder walkthrough",
"timestamp": "Thu 3:00 PM",
"status": "pending"
}
]
}
],
"components": [
{
"id": "data-components-root",
"type": "div",
"props": {
"className": "space-y-6 rounded-lg border border-border bg-card p-6"
},
"children": [
{
"id": "data-components-title",
"type": "Heading",
"props": {
"className": "text-xl font-semibold",
"children": "Data Components Showcase"
}
},
{
"id": "data-components-metrics-grid",
"type": "div",
"props": {
"className": "grid gap-4 md:grid-cols-3"
},
"loop": {
"source": "metricCards",
"itemVar": "metricCard"
},
"children": [
{
"id": "data-components-metric-card",
"type": "MetricCard",
"bindings": {
"label": { "sourceType": "bindings", "source": "metricCard", "path": "label" },
"value": { "sourceType": "bindings", "source": "metricCard", "path": "value" },
"trend": { "sourceType": "bindings", "source": "metricCard", "path": "trend" }
}
}
]
},
{
"id": "data-components-table",
"type": "DataTable",
"props": {
"className": "bg-background",
"emptyMessage": "No initiatives found"
},
"bindings": {
"columns": { "source": "tableColumns", "sourceType": "data" },
"data": { "source": "tableRows", "sourceType": "data" }
}
},
{
"id": "data-components-list",
"type": "DataList",
"props": {
"className": "space-y-3",
"itemClassName": "rounded-md border border-border bg-card/50 px-4 py-2 text-sm",
"emptyMessage": "No action items"
},
"bindings": {
"items": { "source": "listItems", "sourceType": "data" }
}
},
{
"id": "data-components-timeline",
"type": "Timeline",
"bindings": {
"items": { "source": "timelineItems", "sourceType": "data" }
}
}
]
}
]
},
"feedbackAtomsDemoSchema": {
"id": "feedback-atoms-demo",
"name": "Feedback Atoms Demo",
"layout": {
"type": "single"
},
"dataSources": [
{
"id": "circularProgressValue",
"type": "static",
"defaultValue": 72
},
{
"id": "progressBarValue",
"type": "static",
"defaultValue": 42
}
],
"components": [
{
"id": "feedback-atoms-root",
"type": "div",
"props": {
"className": "space-y-6 rounded-lg border border-border bg-card p-6"
},
"children": [
{
"id": "feedback-atoms-title",
"type": "Heading",
"props": {
"className": "text-xl font-semibold",
"children": "Feedback Atoms"
}
},
{
"id": "feedback-atoms-divider",
"type": "Divider",
"props": {
"className": "my-2"
}
},
{
"id": "feedback-atoms-progress-grid",
"type": "div",
"props": {
"className": "grid gap-6 md:grid-cols-2"
},
"children": [
{
"id": "feedback-atoms-circular-progress",
"type": "CircularProgress",
"props": {
"size": "lg",
"showLabel": true
},
"bindings": {
"value": {
"source": "circularProgressValue",
"sourceType": "data"
}
}
},
{
"id": "feedback-atoms-progress-bar",
"type": "ProgressBar",
"props": {
"variant": "accent",
"size": "lg",
"showLabel": true
},
"bindings": {
"value": {
"source": "progressBarValue",
"sourceType": "data"
}
}
}
]
}
]
}
]
}
}

View File

@@ -4,9 +4,12 @@ export type ComponentType =
| 'Input' | 'TextArea' | 'Textarea' | 'Select' | 'Checkbox' | 'Radio' | 'Switch' | 'Slider' | 'NumberInput' | 'Input' | 'TextArea' | 'Textarea' | 'Select' | 'Checkbox' | 'Radio' | 'Switch' | 'Slider' | 'NumberInput'
| 'Badge' | 'Progress' | 'Separator' | 'Tabs' | 'TabsContent' | 'TabsList' | 'TabsTrigger' | 'Dialog' | 'Badge' | 'Progress' | 'Separator' | 'Tabs' | 'TabsContent' | 'TabsList' | 'TabsTrigger' | 'Dialog'
| 'Text' | 'Heading' | 'Label' | 'List' | 'Grid' | 'Stack' | 'Flex' | 'Container' | 'Text' | 'Heading' | 'Label' | 'List' | 'Grid' | 'Stack' | 'Flex' | 'Container'
| 'Link' | 'Image' | 'Avatar' | 'Code' | 'Tag' | 'Spinner' | 'Skeleton' | 'Link' | 'Breadcrumb' | 'Image' | 'Avatar' | 'Code' | 'Tag' | 'Spinner' | 'Skeleton'
| 'Alert' | 'InfoBox' | 'EmptyState' | 'StatusBadge' | 'Alert' | 'InfoBox' | 'EmptyState' | 'StatusBadge'
| 'ErrorBadge' | 'Notification' | 'StatusIcon'
| 'CircularProgress' | 'Divider' | 'ProgressBar'
| 'Table' | 'KeyValue' | 'StatCard' | 'DataCard' | 'SearchInput' | 'ActionBar' | 'Table' | 'KeyValue' | 'StatCard' | 'DataCard' | 'SearchInput' | 'ActionBar'
| 'DataList' | 'DataTable' | 'MetricCard' | 'Timeline'
| 'AppBranding' | 'LabelWithBadge' | 'EmptyEditorState' | 'LoadingFallback' | 'LoadingState' | 'NavigationGroupHeader' | 'AppBranding' | 'LabelWithBadge' | 'EmptyEditorState' | 'LoadingFallback' | 'LoadingState' | 'NavigationGroupHeader'
export type ActionType = export type ActionType =
@@ -18,6 +21,9 @@ export type ActionType =
export type DataSourceType = export type DataSourceType =
| 'kv' | 'computed' | 'static' | 'kv' | 'computed' | 'static'
export type BindingSourceType =
| 'data' | 'bindings' | 'state'
export interface DataSource { export interface DataSource {
id: string id: string
type: DataSourceType type: DataSourceType
@@ -46,6 +52,7 @@ export interface Action {
export interface Binding { export interface Binding {
source: string source: string
sourceType?: BindingSourceType
path?: string path?: string
transform?: string | ((value: any) => any) transform?: string | ((value: any) => any)
} }
@@ -56,6 +63,15 @@ export interface EventHandler {
condition?: string | ((data: Record<string, any>) => boolean) condition?: string | ((data: Record<string, any>) => boolean)
} }
export interface JSONEventDefinition {
action?: string
actions?: Action[]
payload?: Record<string, any>
condition?: string | ((data: Record<string, any>) => boolean)
}
export type JSONEventMap = Record<string, JSONEventDefinition | JSONEventDefinition[] | string>
export interface Conditional { export interface Conditional {
if: string if: string
then?: UIComponent | (UIComponent | string)[] | string then?: UIComponent | (UIComponent | string)[] | string
@@ -76,7 +92,7 @@ export interface UIComponent {
style?: Record<string, any> style?: Record<string, any>
bindings?: Record<string, Binding> bindings?: Record<string, Binding>
dataBinding?: string | Binding dataBinding?: string | Binding
events?: EventHandler[] events?: EventHandler[] | JSONEventMap
children?: UIComponent[] | string children?: UIComponent[] | string
condition?: Binding condition?: Binding
conditional?: Conditional conditional?: Conditional
@@ -115,6 +131,7 @@ export interface ComponentRendererProps {
component: UIComponent component: UIComponent
data: Record<string, unknown> data: Record<string, unknown>
context?: Record<string, unknown> context?: Record<string, unknown>
state?: Record<string, unknown>
onEvent?: (componentId: string, handler: EventHandler, eventData: unknown) => void onEvent?: (componentId: string, handler: EventHandler, eventData: unknown) => void
} }