mirror of
https://github.com/johndoe6345789/low-code-react-app-b.git
synced 2026-04-25 06:04:54 +00:00
Compare commits
7 Commits
codex/crea
...
codex/ensu
| Author | SHA1 | Date | |
|---|---|---|---|
| 8fd601357d | |||
| fc209545c1 | |||
| 28a3851310 | |||
| 8465a9de5a | |||
| d04333e565 | |||
| e210dd8bec | |||
| 39c57e9967 |
@@ -171,6 +171,24 @@ To add a new component to the JSON UI system:
|
||||
4. Update `json-components-registry.json` with metadata
|
||||
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
|
||||
|
||||
Components marked as "planned" are:
|
||||
|
||||
@@ -611,7 +611,7 @@
|
||||
"category": "display",
|
||||
"canHaveChildren": false,
|
||||
"description": "Circular progress indicator",
|
||||
"status": "planned",
|
||||
"status": "supported",
|
||||
"source": "atoms"
|
||||
},
|
||||
{
|
||||
@@ -629,7 +629,7 @@
|
||||
"category": "display",
|
||||
"canHaveChildren": false,
|
||||
"description": "Visual section divider",
|
||||
"status": "planned",
|
||||
"status": "supported",
|
||||
"source": "atoms"
|
||||
},
|
||||
{
|
||||
@@ -719,7 +719,7 @@
|
||||
"category": "display",
|
||||
"canHaveChildren": false,
|
||||
"description": "Linear progress bar",
|
||||
"status": "planned",
|
||||
"status": "supported",
|
||||
"source": "atoms"
|
||||
},
|
||||
{
|
||||
|
||||
@@ -6,9 +6,16 @@ export interface ComponentDefinition {
|
||||
category: 'layout' | 'input' | 'display' | 'navigation' | 'feedback' | 'data' | 'custom'
|
||||
icon: string
|
||||
defaultProps?: Record<string, any>
|
||||
propSchema?: Record<string, ComponentPropSchema>
|
||||
canHaveChildren?: boolean
|
||||
}
|
||||
|
||||
export interface ComponentPropSchema {
|
||||
type: 'string' | 'number' | 'boolean' | 'enum'
|
||||
description?: string
|
||||
options?: string[]
|
||||
}
|
||||
|
||||
export const componentDefinitions: ComponentDefinition[] = [
|
||||
// Layout Components
|
||||
{
|
||||
@@ -194,6 +201,40 @@ export const componentDefinitions: ComponentDefinition[] = [
|
||||
icon: 'CircleNotch',
|
||||
defaultProps: { value: 50 }
|
||||
},
|
||||
{
|
||||
type: 'ProgressBar',
|
||||
label: 'Progress Bar',
|
||||
category: 'display',
|
||||
icon: 'ChartBar',
|
||||
defaultProps: { value: 60, max: 100, size: 'md', variant: 'default', showLabel: false },
|
||||
propSchema: {
|
||||
value: { type: 'number', description: 'Current progress value.' },
|
||||
max: { type: 'number', description: 'Maximum progress value.' },
|
||||
size: { type: 'enum', options: ['sm', 'md', 'lg'], description: 'Height size of the bar.' },
|
||||
variant: {
|
||||
type: 'enum',
|
||||
options: ['default', 'accent', 'destructive'],
|
||||
description: 'Color variant for the progress fill.',
|
||||
},
|
||||
showLabel: { type: 'boolean', description: 'Show percentage text below the bar.' },
|
||||
className: { type: 'string', description: 'Additional class names for the container.' },
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'CircularProgress',
|
||||
label: 'Circular Progress',
|
||||
category: 'display',
|
||||
icon: 'CircleNotch',
|
||||
defaultProps: { value: 72, max: 100, size: 'md', showLabel: true },
|
||||
propSchema: {
|
||||
value: { type: 'number', description: 'Current progress value.' },
|
||||
max: { type: 'number', description: 'Maximum progress value.' },
|
||||
size: { type: 'enum', options: ['sm', 'md', 'lg', 'xl'], description: 'Rendered size.' },
|
||||
showLabel: { type: 'boolean', description: 'Show percentage label in the center.' },
|
||||
strokeWidth: { type: 'number', description: 'Override the default stroke width.' },
|
||||
className: { type: 'string', description: 'Additional class names for the wrapper.' },
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'Spinner',
|
||||
label: 'Spinner',
|
||||
@@ -215,6 +256,22 @@ export const componentDefinitions: ComponentDefinition[] = [
|
||||
icon: 'Minus',
|
||||
defaultProps: {}
|
||||
},
|
||||
{
|
||||
type: 'Divider',
|
||||
label: 'Divider',
|
||||
category: 'display',
|
||||
icon: 'Minus',
|
||||
defaultProps: { orientation: 'horizontal', decorative: true },
|
||||
propSchema: {
|
||||
orientation: {
|
||||
type: 'enum',
|
||||
options: ['horizontal', 'vertical'],
|
||||
description: 'Divider orientation.',
|
||||
},
|
||||
decorative: { type: 'boolean', description: 'Whether the divider is decorative.' },
|
||||
className: { type: 'string', description: 'Additional class names for the divider.' },
|
||||
},
|
||||
},
|
||||
// Navigation Components
|
||||
{
|
||||
type: 'Link',
|
||||
|
||||
@@ -91,16 +91,49 @@ Connect UI to data sources:
|
||||
|
||||
### 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
|
||||
{
|
||||
"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
|
||||
|
||||
Render lists from arrays:
|
||||
|
||||
@@ -136,10 +136,15 @@ export const shadcnComponents: UIComponentRegistry = {
|
||||
AvatarImage,
|
||||
}
|
||||
|
||||
export const atomComponents: UIComponentRegistry = buildRegistryFromNames(
|
||||
atomRegistryNames,
|
||||
AtomComponents as Record<string, ComponentType<any>>
|
||||
)
|
||||
export const atomComponents: UIComponentRegistry = {
|
||||
...buildRegistryFromNames(
|
||||
atomRegistryNames,
|
||||
AtomComponents as Record<string, ComponentType<any>>
|
||||
),
|
||||
CircularProgress: AtomComponents.CircularProgress,
|
||||
Divider: AtomComponents.Divider,
|
||||
ProgressBar: AtomComponents.ProgressBar,
|
||||
}
|
||||
|
||||
export const moleculeComponents: UIComponentRegistry = buildRegistryFromNames(
|
||||
moleculeRegistryNames,
|
||||
|
||||
@@ -1,33 +1,101 @@
|
||||
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 { resolveDataBinding, evaluateCondition } from './utils'
|
||||
|
||||
function resolveBinding(binding: Binding, data: Record<string, unknown>): unknown {
|
||||
return resolveDataBinding(binding, data)
|
||||
function resolveBinding(
|
||||
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 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 resolved: Record<string, unknown> = { ...component.props }
|
||||
|
||||
if (component.bindings) {
|
||||
Object.entries(component.bindings).forEach(([propName, binding]) => {
|
||||
resolved[propName] = resolveBinding(binding, mergedData)
|
||||
resolved[propName] = resolveBinding(binding, data, context, state)
|
||||
})
|
||||
}
|
||||
|
||||
if (component.dataBinding) {
|
||||
const boundData = resolveDataBinding(component.dataBinding, mergedData)
|
||||
const boundData = resolveDataBinding(component.dataBinding, data, context, { state, bindings: context })
|
||||
if (boundData !== undefined) {
|
||||
resolved.value = boundData
|
||||
resolved.data = boundData
|
||||
}
|
||||
}
|
||||
|
||||
if (component.events && onEvent) {
|
||||
component.events.forEach(handler => {
|
||||
if (resolvedEventHandlers.length > 0 && onEvent) {
|
||||
resolvedEventHandlers.forEach(handler => {
|
||||
resolved[`on${handler.event.charAt(0).toUpperCase()}${handler.event.slice(1)}`] = (e: unknown) => {
|
||||
const conditionMet = !handler.condition
|
||||
|| (typeof handler.condition === 'function'
|
||||
@@ -51,7 +119,7 @@ export function ComponentRenderer({ component, data, context = {}, onEvent }: Co
|
||||
}
|
||||
|
||||
return resolved
|
||||
}, [component, mergedData, onEvent])
|
||||
}, [component, data, context, state, mergedData, onEvent])
|
||||
|
||||
const Component = getUIComponent(component.type)
|
||||
|
||||
@@ -78,6 +146,7 @@ export function ComponentRenderer({ component, data, context = {}, onEvent }: Co
|
||||
component={child}
|
||||
data={data}
|
||||
context={renderContext}
|
||||
state={state}
|
||||
onEvent={onEvent}
|
||||
/>
|
||||
)}
|
||||
@@ -103,6 +172,7 @@ export function ComponentRenderer({ component, data, context = {}, onEvent }: Co
|
||||
component={child}
|
||||
data={data}
|
||||
context={renderContext}
|
||||
state={state}
|
||||
onEvent={onEvent}
|
||||
/>
|
||||
)}
|
||||
@@ -114,6 +184,7 @@ export function ComponentRenderer({ component, data, context = {}, onEvent }: Co
|
||||
component={branch}
|
||||
data={data}
|
||||
context={renderContext}
|
||||
state={state}
|
||||
onEvent={onEvent}
|
||||
/>
|
||||
)
|
||||
@@ -135,7 +206,7 @@ export function ComponentRenderer({ component, data, context = {}, onEvent }: Co
|
||||
}
|
||||
|
||||
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 loopContext = {
|
||||
...context,
|
||||
@@ -153,7 +224,7 @@ export function ComponentRenderer({ component, data, context = {}, onEvent }: Co
|
||||
}
|
||||
|
||||
if (component.condition) {
|
||||
const conditionValue = resolveBinding(component.condition, { ...data, ...loopContext })
|
||||
const conditionValue = resolveBinding(component.condition, data, loopContext, state)
|
||||
if (!conditionValue) {
|
||||
return null
|
||||
}
|
||||
@@ -177,7 +248,7 @@ export function ComponentRenderer({ component, data, context = {}, onEvent }: Co
|
||||
}
|
||||
|
||||
if (component.condition) {
|
||||
const conditionValue = resolveBinding(component.condition, mergedData)
|
||||
const conditionValue = resolveBinding(component.condition, data, context, state)
|
||||
if (!conditionValue) {
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useCallback } from 'react'
|
||||
import { PageSchema } from '@/types/json-ui'
|
||||
import { useDataSources } from '@/hooks/data/use-data-sources'
|
||||
import { useActionExecutor } from '@/hooks/ui/use-action-executor'
|
||||
import { useAppSelector } from '@/store'
|
||||
import { ComponentRenderer } from './component-renderer'
|
||||
|
||||
interface PageRendererProps {
|
||||
@@ -11,6 +12,7 @@ interface PageRendererProps {
|
||||
|
||||
export function PageRenderer({ schema, onCustomAction }: PageRendererProps) {
|
||||
const { data, updateData, updatePath } = useDataSources(schema.dataSources)
|
||||
const state = useAppSelector((rootState) => rootState)
|
||||
|
||||
const context = {
|
||||
data,
|
||||
@@ -32,6 +34,7 @@ export function PageRenderer({ schema, onCustomAction }: PageRendererProps) {
|
||||
key={component.id || index}
|
||||
component={component}
|
||||
data={data}
|
||||
state={state}
|
||||
onEvent={handleEvent}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
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 { resolveDataBinding, evaluateCondition } from './utils'
|
||||
import { cn } from '@/lib/utils'
|
||||
@@ -79,35 +87,39 @@ export function JSONUIRenderer({
|
||||
? eventName
|
||||
: `on${eventName.charAt(0).toUpperCase()}${eventName.slice(1)}`
|
||||
|
||||
const normalizeLegacyHandler = (eventName: string, handler: any): EventHandler | null => {
|
||||
if (!handler) return null
|
||||
const normalizeEventDefinition = (
|
||||
eventName: string,
|
||||
definition: JSONEventDefinition | string
|
||||
): EventHandler | null => {
|
||||
if (!definition) return null
|
||||
const normalizedEvent = normalizeEventName(eventName)
|
||||
|
||||
if (typeof handler === 'string') {
|
||||
if (typeof definition === 'string') {
|
||||
return {
|
||||
event: normalizeEventName(eventName),
|
||||
actions: [{ id: handler, type: 'custom' }],
|
||||
event: normalizedEvent,
|
||||
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 {
|
||||
event: normalizeEventName(eventName),
|
||||
actions: handler.actions as Action[],
|
||||
condition: handler.condition,
|
||||
event: normalizedEvent,
|
||||
actions,
|
||||
condition: definition.condition,
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof handler === 'object' && handler.action) {
|
||||
if (definition.action) {
|
||||
return {
|
||||
event: normalizeEventName(eventName),
|
||||
actions: [
|
||||
{
|
||||
id: handler.action,
|
||||
type: 'custom',
|
||||
target: handler.target,
|
||||
params: handler.params,
|
||||
},
|
||||
],
|
||||
event: normalizedEvent,
|
||||
actions: [{ id: definition.action, type: 'custom', params: definition.payload }],
|
||||
condition: definition.condition,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,11 +131,20 @@ export function JSONUIRenderer({
|
||||
renderContext: Record<string, unknown>
|
||||
) => {
|
||||
const eventHandlers: EventHandler[] = Array.isArray(component.events)
|
||||
? component.events
|
||||
? component.events.map((handler) => ({
|
||||
...handler,
|
||||
event: normalizeEventName(handler.event),
|
||||
}))
|
||||
: component.events
|
||||
? Object.entries(component.events).map(([eventName, handler]) =>
|
||||
normalizeLegacyHandler(eventName, handler)
|
||||
).filter(Boolean) as EventHandler[]
|
||||
? Object.entries(component.events as JSONEventMap).flatMap(([eventName, handler]) => {
|
||||
if (Array.isArray(handler)) {
|
||||
return handler
|
||||
.map((entry) => normalizeEventDefinition(eventName, entry))
|
||||
.filter(Boolean) as EventHandler[]
|
||||
}
|
||||
const normalized = normalizeEventDefinition(eventName, handler)
|
||||
return normalized ? [normalized] : []
|
||||
})
|
||||
: []
|
||||
|
||||
if (eventHandlers.length > 0) {
|
||||
|
||||
@@ -48,6 +48,22 @@ export const EventHandlerSchema = z.object({
|
||||
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({
|
||||
if: z.string(),
|
||||
then: z.any().optional(),
|
||||
@@ -69,7 +85,7 @@ export const UIComponentSchema: any = z.object({
|
||||
z.string(),
|
||||
DataBindingSchema,
|
||||
]).optional(),
|
||||
events: z.array(EventHandlerSchema).optional(),
|
||||
events: z.union([z.array(EventHandlerSchema), JSONEventMapSchema]).optional(),
|
||||
conditional: ConditionalSchema.optional(),
|
||||
loop: z.object({
|
||||
source: z.string(),
|
||||
@@ -242,6 +258,8 @@ export type UIValue = z.infer<typeof UIValueSchema>
|
||||
export type DataBinding = z.infer<typeof DataBindingSchema>
|
||||
export type Action = z.infer<typeof ActionSchema>
|
||||
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 UIComponent = z.infer<typeof UIComponentSchema>
|
||||
export type FormField = z.infer<typeof FormFieldSchema>
|
||||
|
||||
@@ -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 {
|
||||
component: UIComponent
|
||||
|
||||
@@ -1,23 +1,43 @@
|
||||
type BindingTransform = string | ((data: unknown) => unknown)
|
||||
|
||||
interface BindingSourceOptions {
|
||||
state?: Record<string, any>
|
||||
bindings?: Record<string, any>
|
||||
}
|
||||
|
||||
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>,
|
||||
context: Record<string, any> = {},
|
||||
options: BindingSourceOptions = {},
|
||||
): any {
|
||||
const mergedContext = { ...dataMap, ...context }
|
||||
const stateSource = options.state ?? {}
|
||||
const bindingsSource = options.bindings ?? context
|
||||
|
||||
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('.')) {
|
||||
return getNestedValue(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('.')
|
||||
? getNestedValue(mergedContext, source)
|
||||
: mergedContext[source]
|
||||
? getNestedValue(sourceContext, source)
|
||||
: sourceContext[source]
|
||||
const resolvedValue = path ? getNestedValue(sourceValue, path) : sourceValue
|
||||
|
||||
return applyTransform(resolvedValue, transform)
|
||||
|
||||
178
src/schemas/page-schemas.ts
Normal file
178
src/schemas/page-schemas.ts
Normal file
@@ -0,0 +1,178 @@
|
||||
import type { PageSchema } from '@/types/json-ui'
|
||||
|
||||
export const stateBindingsDemoSchema: PageSchema = {
|
||||
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',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
export const progressIndicatorsDemoSchema: PageSchema = {
|
||||
id: 'progress-indicators-demo',
|
||||
name: 'Progress Indicators Demo',
|
||||
layout: {
|
||||
type: 'single',
|
||||
},
|
||||
dataSources: [],
|
||||
components: [
|
||||
{
|
||||
id: 'progress-demo-root',
|
||||
type: 'div',
|
||||
props: {
|
||||
className: 'space-y-6 rounded-lg border border-border bg-card p-6',
|
||||
},
|
||||
children: [
|
||||
{
|
||||
id: 'progress-demo-title',
|
||||
type: 'Heading',
|
||||
props: {
|
||||
className: 'text-xl font-semibold',
|
||||
children: 'Progress Indicators',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'progress-demo-subtitle',
|
||||
type: 'Text',
|
||||
props: {
|
||||
className: 'text-sm text-muted-foreground',
|
||||
children: 'Circular and linear progress components with JSON-friendly props.',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'progress-demo-circular-row',
|
||||
type: 'div',
|
||||
props: {
|
||||
className: 'flex flex-wrap items-center gap-6',
|
||||
},
|
||||
children: [
|
||||
{
|
||||
id: 'progress-demo-circular-primary',
|
||||
type: 'CircularProgress',
|
||||
props: {
|
||||
value: 72,
|
||||
size: 'md',
|
||||
showLabel: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'progress-demo-circular-large',
|
||||
type: 'CircularProgress',
|
||||
props: {
|
||||
value: 45,
|
||||
size: 'lg',
|
||||
showLabel: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'progress-demo-divider',
|
||||
type: 'Divider',
|
||||
props: {
|
||||
orientation: 'horizontal',
|
||||
decorative: true,
|
||||
className: 'my-2',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'progress-demo-linear-stack',
|
||||
type: 'div',
|
||||
props: {
|
||||
className: 'space-y-4',
|
||||
},
|
||||
children: [
|
||||
{
|
||||
id: 'progress-demo-linear-accent',
|
||||
type: 'ProgressBar',
|
||||
props: {
|
||||
value: 60,
|
||||
max: 100,
|
||||
size: 'md',
|
||||
variant: 'accent',
|
||||
showLabel: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'progress-demo-linear-compact',
|
||||
type: 'ProgressBar',
|
||||
props: {
|
||||
value: 35,
|
||||
max: 100,
|
||||
size: 'sm',
|
||||
variant: 'default',
|
||||
showLabel: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -2,7 +2,7 @@ export type ComponentType =
|
||||
| 'div' | 'section' | 'article' | 'header' | 'footer' | 'main'
|
||||
| 'Button' | 'Card' | 'CardHeader' | 'CardTitle' | 'CardDescription' | 'CardContent' | 'CardFooter'
|
||||
| 'Input' | 'TextArea' | 'Textarea' | 'Select' | 'Checkbox' | 'Radio' | 'Switch' | 'Slider' | 'NumberInput'
|
||||
| 'Badge' | 'Progress' | 'Separator' | 'Tabs' | 'TabsContent' | 'TabsList' | 'TabsTrigger' | 'Dialog'
|
||||
| 'Badge' | 'CircularProgress' | 'Divider' | 'Progress' | 'ProgressBar' | 'Separator' | 'Tabs' | 'TabsContent' | 'TabsList' | 'TabsTrigger' | 'Dialog'
|
||||
| 'Text' | 'Heading' | 'Label' | 'List' | 'Grid' | 'Stack' | 'Flex' | 'Container'
|
||||
| 'Link' | 'Image' | 'Avatar' | 'Code' | 'Tag' | 'Spinner' | 'Skeleton'
|
||||
| 'Alert' | 'InfoBox' | 'EmptyState' | 'StatusBadge'
|
||||
@@ -18,6 +18,9 @@ export type ActionType =
|
||||
export type DataSourceType =
|
||||
| 'kv' | 'computed' | 'static'
|
||||
|
||||
export type BindingSourceType =
|
||||
| 'data' | 'bindings' | 'state'
|
||||
|
||||
export interface DataSource {
|
||||
id: string
|
||||
type: DataSourceType
|
||||
@@ -46,6 +49,7 @@ export interface Action {
|
||||
|
||||
export interface Binding {
|
||||
source: string
|
||||
sourceType?: BindingSourceType
|
||||
path?: string
|
||||
transform?: string | ((value: any) => any)
|
||||
}
|
||||
@@ -56,6 +60,15 @@ export interface EventHandler {
|
||||
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 {
|
||||
if: string
|
||||
then?: UIComponent | (UIComponent | string)[] | string
|
||||
@@ -76,7 +89,7 @@ export interface UIComponent {
|
||||
style?: Record<string, any>
|
||||
bindings?: Record<string, Binding>
|
||||
dataBinding?: string | Binding
|
||||
events?: EventHandler[]
|
||||
events?: EventHandler[] | JSONEventMap
|
||||
children?: UIComponent[] | string
|
||||
condition?: Binding
|
||||
conditional?: Conditional
|
||||
@@ -115,6 +128,7 @@ export interface ComponentRendererProps {
|
||||
component: UIComponent
|
||||
data: Record<string, unknown>
|
||||
context?: Record<string, unknown>
|
||||
state?: Record<string, unknown>
|
||||
onEvent?: (componentId: string, handler: EventHandler, eventData: unknown) => void
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user