mirror of
https://github.com/johndoe6345789/low-code-react-app-b.git
synced 2026-04-25 06:04:54 +00:00
Compare commits
19 Commits
codex/crea
...
codex/add-
| Author | SHA1 | Date | |
|---|---|---|---|
| b0aedbb0bd | |||
| af03c13934 | |||
| 4529708f76 | |||
| 8945c746cb | |||
| 2190be271f | |||
| e7fc49e53f | |||
| 9448b8327d | |||
| 64c3b5b12b | |||
| 0d82406e5f | |||
| 233dbd2aa1 | |||
| 3fe02ed098 | |||
| e7159916cb | |||
| e41d08d40c | |||
| 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:
|
||||
|
||||
95
docs/JSON_COMPONENT_CONVERSION_TASKS.md
Normal file
95
docs/JSON_COMPONENT_CONVERSION_TASKS.md
Normal 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.
|
||||
@@ -2,7 +2,7 @@
|
||||
"$schema": "./schemas/json-components-registry-schema.json",
|
||||
"version": "2.0.0",
|
||||
"description": "Registry of all components in the application",
|
||||
"lastUpdated": "2026-01-17T22:10:22.582Z",
|
||||
"lastUpdated": "2026-01-18T11:30:24.191Z",
|
||||
"categories": {
|
||||
"layout": "Layout and container components",
|
||||
"input": "Form inputs and interactive controls",
|
||||
@@ -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"
|
||||
},
|
||||
{
|
||||
@@ -836,29 +836,10 @@
|
||||
"name": "Breadcrumb",
|
||||
"category": "navigation",
|
||||
"canHaveChildren": false,
|
||||
"description": "Navigation breadcrumb trail",
|
||||
"status": "planned",
|
||||
"description": "Navigation breadcrumb trail using the atoms/BreadcrumbNav JSON-safe items prop",
|
||||
"status": "supported",
|
||||
"source": "atoms"
|
||||
},
|
||||
{
|
||||
"type": "Breadcrumb",
|
||||
"name": "Breadcrumb",
|
||||
"category": "navigation",
|
||||
"canHaveChildren": false,
|
||||
"description": "Navigation breadcrumb trail",
|
||||
"status": "json-compatible",
|
||||
"source": "json-ui-wrappers",
|
||||
"jsonCompatible": true
|
||||
},
|
||||
{
|
||||
"type": "Breadcrumb",
|
||||
"name": "Breadcrumb",
|
||||
"category": "navigation",
|
||||
"canHaveChildren": false,
|
||||
"description": "Navigation breadcrumb trail",
|
||||
"status": "planned",
|
||||
"source": "ui"
|
||||
},
|
||||
{
|
||||
"type": "ContextMenu",
|
||||
"name": "ContextMenu",
|
||||
@@ -1088,7 +1069,7 @@
|
||||
"category": "feedback",
|
||||
"canHaveChildren": false,
|
||||
"description": "Error state badge",
|
||||
"status": "planned",
|
||||
"status": "supported",
|
||||
"source": "atoms"
|
||||
},
|
||||
{
|
||||
@@ -1164,7 +1145,7 @@
|
||||
"category": "feedback",
|
||||
"canHaveChildren": true,
|
||||
"description": "Toast notification",
|
||||
"status": "planned",
|
||||
"status": "supported",
|
||||
"source": "atoms"
|
||||
},
|
||||
{
|
||||
@@ -1201,7 +1182,7 @@
|
||||
"category": "feedback",
|
||||
"canHaveChildren": false,
|
||||
"description": "Status indicator icon",
|
||||
"status": "planned",
|
||||
"status": "supported",
|
||||
"source": "atoms"
|
||||
},
|
||||
{
|
||||
@@ -1219,7 +1200,7 @@
|
||||
"category": "data",
|
||||
"canHaveChildren": false,
|
||||
"description": "Styled data list",
|
||||
"status": "planned",
|
||||
"status": "supported",
|
||||
"source": "atoms"
|
||||
},
|
||||
{
|
||||
@@ -1238,7 +1219,7 @@
|
||||
"category": "data",
|
||||
"canHaveChildren": false,
|
||||
"description": "Advanced data table with sorting and filtering",
|
||||
"status": "planned",
|
||||
"status": "supported",
|
||||
"source": "atoms"
|
||||
},
|
||||
{
|
||||
@@ -1257,7 +1238,7 @@
|
||||
"canHaveChildren": true,
|
||||
"description": "LazyBarChart component",
|
||||
"status": "json-compatible",
|
||||
"source": "json-ui-wrappers",
|
||||
"source": "molecules",
|
||||
"jsonCompatible": true
|
||||
},
|
||||
{
|
||||
@@ -1267,7 +1248,7 @@
|
||||
"canHaveChildren": true,
|
||||
"description": "LazyD3BarChart component",
|
||||
"status": "json-compatible",
|
||||
"source": "json-ui-wrappers",
|
||||
"source": "molecules",
|
||||
"jsonCompatible": true
|
||||
},
|
||||
{
|
||||
@@ -1277,7 +1258,7 @@
|
||||
"canHaveChildren": true,
|
||||
"description": "LazyLineChart component",
|
||||
"status": "json-compatible",
|
||||
"source": "json-ui-wrappers",
|
||||
"source": "molecules",
|
||||
"jsonCompatible": true
|
||||
},
|
||||
{
|
||||
@@ -1304,7 +1285,7 @@
|
||||
"category": "data",
|
||||
"canHaveChildren": false,
|
||||
"description": "Metric display card",
|
||||
"status": "planned",
|
||||
"status": "supported",
|
||||
"source": "atoms"
|
||||
},
|
||||
{
|
||||
@@ -1319,11 +1300,11 @@
|
||||
{
|
||||
"type": "SeedDataManager",
|
||||
"name": "SeedDataManager",
|
||||
"category": "custom",
|
||||
"category": "data",
|
||||
"canHaveChildren": true,
|
||||
"description": "SeedDataManager component",
|
||||
"status": "json-compatible",
|
||||
"source": "json-ui-wrappers",
|
||||
"source": "molecules",
|
||||
"jsonCompatible": true
|
||||
},
|
||||
{
|
||||
@@ -1369,7 +1350,7 @@
|
||||
"category": "data",
|
||||
"canHaveChildren": false,
|
||||
"description": "Timeline visualization",
|
||||
"status": "planned",
|
||||
"status": "supported",
|
||||
"source": "atoms"
|
||||
},
|
||||
{
|
||||
@@ -1826,11 +1807,11 @@
|
||||
{
|
||||
"type": "SaveIndicator",
|
||||
"name": "SaveIndicator",
|
||||
"category": "feedback",
|
||||
"category": "custom",
|
||||
"canHaveChildren": true,
|
||||
"description": "SaveIndicator component",
|
||||
"status": "json-compatible",
|
||||
"source": "json-ui-wrappers",
|
||||
"source": "molecules",
|
||||
"jsonCompatible": true
|
||||
},
|
||||
{
|
||||
@@ -2000,7 +1981,7 @@
|
||||
"canHaveChildren": true,
|
||||
"description": "StorageSettings component",
|
||||
"status": "json-compatible",
|
||||
"source": "json-ui-wrappers",
|
||||
"source": "molecules",
|
||||
"jsonCompatible": true
|
||||
},
|
||||
{
|
||||
@@ -2043,24 +2024,24 @@
|
||||
],
|
||||
"statistics": {
|
||||
"total": 219,
|
||||
"supported": 150,
|
||||
"planned": 14,
|
||||
"supported": 154,
|
||||
"planned": 10,
|
||||
"jsonCompatible": 14,
|
||||
"maybeJsonCompatible": 41,
|
||||
"byCategory": {
|
||||
"layout": 25,
|
||||
"input": 34,
|
||||
"display": 31,
|
||||
"navigation": 17,
|
||||
"navigation": 15,
|
||||
"feedback": 23,
|
||||
"data": 20,
|
||||
"custom": 69
|
||||
},
|
||||
"bySource": {
|
||||
"atoms": 117,
|
||||
"molecules": 41,
|
||||
"molecules": 40,
|
||||
"organisms": 15,
|
||||
"ui": 46
|
||||
"ui": 45
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,11 +3,14 @@ import { AtomicComponentDemo } from '@/components/AtomicComponentDemo'
|
||||
import { DashboardDemoPage } from '@/components/DashboardDemoPage'
|
||||
import { PageRenderer } from '@/lib/json-ui/page-renderer'
|
||||
import { hydrateSchema } from '@/schemas/schema-loader'
|
||||
import pageSchemasJson from '@/schemas/page-schemas.json'
|
||||
import todoListJson from '@/schemas/todo-list.json'
|
||||
import newMoleculesShowcaseJson from '@/schemas/new-molecules-showcase.json'
|
||||
|
||||
const todoListSchema = hydrateSchema(todoListJson)
|
||||
const newMoleculesShowcaseSchema = hydrateSchema(newMoleculesShowcaseJson)
|
||||
const dataComponentsDemoSchema = hydrateSchema(pageSchemasJson.dataComponentsDemoSchema)
|
||||
const feedbackAtomsDemoSchema = hydrateSchema(pageSchemasJson.feedbackAtomsDemoSchema)
|
||||
|
||||
export function JSONUIShowcasePage() {
|
||||
return (
|
||||
@@ -24,7 +27,9 @@ export function JSONUIShowcasePage() {
|
||||
</div>
|
||||
<TabsList className="w-full justify-start">
|
||||
<TabsTrigger value="atomic">Atomic Components</TabsTrigger>
|
||||
<TabsTrigger value="feedback">Feedback Atoms</TabsTrigger>
|
||||
<TabsTrigger value="molecules">New Molecules</TabsTrigger>
|
||||
<TabsTrigger value="data-components">Data Components</TabsTrigger>
|
||||
<TabsTrigger value="dashboard">JSON Dashboard</TabsTrigger>
|
||||
<TabsTrigger value="todos">JSON Todo List</TabsTrigger>
|
||||
</TabsList>
|
||||
@@ -34,10 +39,18 @@ export function JSONUIShowcasePage() {
|
||||
<TabsContent value="atomic" className="h-full m-0 data-[state=active]:block">
|
||||
<AtomicComponentDemo />
|
||||
</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">
|
||||
<PageRenderer schema={newMoleculesShowcaseSchema} />
|
||||
</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">
|
||||
<DashboardDemoPage />
|
||||
|
||||
@@ -17,19 +17,19 @@ export function BreadcrumbNav({ items, className }: BreadcrumbNavProps) {
|
||||
<nav aria-label="Breadcrumb" className={cn('flex items-center gap-2', className)}>
|
||||
{items.map((item, index) => {
|
||||
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 (
|
||||
<div key={index} className="flex items-center gap-2">
|
||||
{item.href || item.onClick ? (
|
||||
<button
|
||||
onClick={item.onClick}
|
||||
className={cn(
|
||||
'text-sm transition-colors',
|
||||
isLast
|
||||
? 'text-foreground font-medium'
|
||||
: 'text-muted-foreground hover:text-foreground'
|
||||
)}
|
||||
>
|
||||
{item.href ? (
|
||||
<a href={item.href} onClick={item.onClick} className={linkClassName}>
|
||||
{item.label}
|
||||
</a>
|
||||
) : item.onClick ? (
|
||||
<button onClick={item.onClick} className={linkClassName}>
|
||||
{item.label}
|
||||
</button>
|
||||
) : (
|
||||
@@ -49,3 +49,5 @@ export function BreadcrumbNav({ items, className }: BreadcrumbNavProps) {
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
|
||||
export const Breadcrumb = BreadcrumbNav
|
||||
|
||||
@@ -3,10 +3,11 @@ import { cn } from '@/lib/utils'
|
||||
|
||||
export interface DataListProps {
|
||||
items: any[]
|
||||
renderItem: (item: any, index: number) => ReactNode
|
||||
renderItem?: (item: any, index: number) => ReactNode
|
||||
emptyMessage?: string
|
||||
className?: string
|
||||
itemClassName?: string
|
||||
itemKey?: string
|
||||
}
|
||||
|
||||
export function DataList({
|
||||
@@ -15,6 +16,7 @@ export function DataList({
|
||||
emptyMessage = 'No items',
|
||||
className,
|
||||
itemClassName,
|
||||
itemKey,
|
||||
}: DataListProps) {
|
||||
if (items.length === 0) {
|
||||
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 (
|
||||
<div className={cn('space-y-2', className)}>
|
||||
{items.map((item, index) => (
|
||||
<div key={index} className={cn('transition-colors', itemClassName)}>
|
||||
{renderItem(item, index)}
|
||||
{renderItem ? renderItem(item, index) : renderFallbackItem(item)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -54,7 +54,7 @@ export { Timestamp } from './Timestamp'
|
||||
export { ScrollArea } from './ScrollArea'
|
||||
|
||||
export { Tag } from './Tag'
|
||||
export { BreadcrumbNav } from './Breadcrumb'
|
||||
export { Breadcrumb, BreadcrumbNav } from './Breadcrumb'
|
||||
export { IconText } from './IconText'
|
||||
export { TextArea } from './TextArea'
|
||||
export { Input } from './Input'
|
||||
@@ -118,4 +118,3 @@ export { MetricDisplay } from './MetricDisplay'
|
||||
export { KeyValue } from './KeyValue'
|
||||
export { EmptyMessage } from './EmptyMessage'
|
||||
export { StepIndicator } from './StepIndicator'
|
||||
|
||||
|
||||
@@ -7,6 +7,23 @@ export interface ComponentDefinition {
|
||||
icon: string
|
||||
defaultProps?: Record<string, any>
|
||||
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[] = [
|
||||
@@ -194,6 +211,101 @@ export const componentDefinitions: ComponentDefinition[] = [
|
||||
icon: 'CircleNotch',
|
||||
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',
|
||||
label: 'Spinner',
|
||||
@@ -215,6 +327,33 @@ export const componentDefinitions: ComponentDefinition[] = [
|
||||
icon: 'Minus',
|
||||
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
|
||||
{
|
||||
type: 'Link',
|
||||
@@ -228,14 +367,14 @@ export const componentDefinitions: ComponentDefinition[] = [
|
||||
type: 'Breadcrumb',
|
||||
label: 'Breadcrumb',
|
||||
category: 'navigation',
|
||||
icon: 'Path',
|
||||
icon: 'CaretRight',
|
||||
canHaveChildren: false,
|
||||
defaultProps: {
|
||||
items: [
|
||||
{ label: 'Home', href: '/' },
|
||||
{ label: 'Section', href: '/section' },
|
||||
{ label: 'Current Page' },
|
||||
],
|
||||
},
|
||||
{ label: 'Current' }
|
||||
]
|
||||
}
|
||||
},
|
||||
// Feedback Components
|
||||
{
|
||||
@@ -270,11 +409,105 @@ export const componentDefinitions: ComponentDefinition[] = [
|
||||
defaultProps: { status: 'active', children: 'Active' }
|
||||
},
|
||||
{
|
||||
type: 'SaveIndicator',
|
||||
label: 'Save Indicator',
|
||||
type: 'ErrorBadge',
|
||||
label: 'Error Badge',
|
||||
category: 'feedback',
|
||||
icon: 'FloppyDisk',
|
||||
defaultProps: { status: 'saved', label: 'Saved' }
|
||||
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
|
||||
{
|
||||
@@ -284,6 +517,17 @@ export const componentDefinitions: ComponentDefinition[] = [
|
||||
icon: 'List',
|
||||
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',
|
||||
label: 'Table',
|
||||
@@ -291,6 +535,25 @@ export const componentDefinitions: ComponentDefinition[] = [
|
||||
icon: 'Table',
|
||||
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',
|
||||
label: 'Key Value',
|
||||
@@ -306,44 +569,43 @@ export const componentDefinitions: ComponentDefinition[] = [
|
||||
defaultProps: { title: 'Metric', value: '0' }
|
||||
},
|
||||
{
|
||||
type: 'LazyBarChart',
|
||||
label: 'Bar Chart',
|
||||
type: 'MetricCard',
|
||||
label: 'Metric Card',
|
||||
category: 'data',
|
||||
icon: 'ChartBar',
|
||||
defaultProps: {
|
||||
data: [
|
||||
{ label: 'Jan', value: 30 },
|
||||
{ label: 'Feb', value: 45 },
|
||||
],
|
||||
xKey: 'label',
|
||||
yKey: 'value',
|
||||
},
|
||||
label: 'Active Users',
|
||||
value: '1,248',
|
||||
trend: { value: 12.4, direction: 'up' },
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'LazyLineChart',
|
||||
label: 'Line Chart',
|
||||
type: 'Timeline',
|
||||
label: 'Timeline',
|
||||
category: 'data',
|
||||
icon: 'ChartLine',
|
||||
icon: 'Clock',
|
||||
defaultProps: {
|
||||
data: [
|
||||
{ label: 'Jan', value: 10 },
|
||||
{ label: 'Feb', value: 25 },
|
||||
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',
|
||||
},
|
||||
],
|
||||
xKey: 'label',
|
||||
yKey: 'value',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'LazyD3BarChart',
|
||||
label: 'D3 Bar Chart',
|
||||
category: 'data',
|
||||
icon: 'ChartBar',
|
||||
defaultProps: {
|
||||
data: [
|
||||
{ label: 'A', value: 12 },
|
||||
{ label: 'B', value: 18 },
|
||||
],
|
||||
},
|
||||
}
|
||||
},
|
||||
// Custom Components
|
||||
{
|
||||
@@ -368,23 +630,6 @@ export const componentDefinitions: ComponentDefinition[] = [
|
||||
canHaveChildren: true,
|
||||
defaultProps: { actions: [] }
|
||||
},
|
||||
{
|
||||
type: 'SeedDataManager',
|
||||
label: 'Seed Data Manager',
|
||||
category: 'custom',
|
||||
icon: 'Database',
|
||||
defaultProps: { isLoaded: false, isLoading: false }
|
||||
},
|
||||
{
|
||||
type: 'StorageSettings',
|
||||
label: 'Storage Settings',
|
||||
category: 'custom',
|
||||
icon: 'Gear',
|
||||
defaultProps: {
|
||||
backend: 'indexeddb',
|
||||
flaskUrl: 'http://localhost:5001',
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
export function getCategoryComponents(category: string): ComponentDefinition[] {
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -19,13 +19,6 @@ import { Progress } from '@/components/ui/progress'
|
||||
import { Avatar as ShadcnAvatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
||||
import * as AtomComponents from '@/components/atoms'
|
||||
import * as MoleculeComponents from '@/components/molecules'
|
||||
import { Breadcrumb } from './wrappers/Breadcrumb'
|
||||
import { SaveIndicator } from './wrappers/SaveIndicator'
|
||||
import { LazyBarChart } from './wrappers/LazyBarChart'
|
||||
import { LazyLineChart } from './wrappers/LazyLineChart'
|
||||
import { LazyD3BarChart } from './wrappers/LazyD3BarChart'
|
||||
import { SeedDataManager } from './wrappers/SeedDataManager'
|
||||
import { StorageSettings } from './wrappers/StorageSettings'
|
||||
import jsonComponentsRegistry from '../../../json-components-registry.json'
|
||||
import {
|
||||
ArrowLeft, ArrowRight, Check, X, Plus, Minus, MagnifyingGlass,
|
||||
@@ -75,10 +68,6 @@ const moleculeRegistryNames = jsonRegistryEntries
|
||||
.filter((entry) => entry.source === 'molecules')
|
||||
.map((entry) => entry.export ?? entry.name ?? entry.type)
|
||||
.filter((name): name is string => Boolean(name))
|
||||
const wrapperRegistryNames = jsonRegistryEntries
|
||||
.filter((entry) => entry.source === 'json-ui-wrappers')
|
||||
.map((entry) => entry.export ?? entry.name ?? entry.type)
|
||||
.filter((name): name is string => Boolean(name))
|
||||
|
||||
export const primitiveComponents: UIComponentRegistry = {
|
||||
div: 'div' as any,
|
||||
@@ -147,29 +136,30 @@ 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 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(
|
||||
moleculeRegistryNames,
|
||||
MoleculeComponents as Record<string, ComponentType<any>>
|
||||
)
|
||||
|
||||
export const wrapperComponents: UIComponentRegistry = buildRegistryFromNames(
|
||||
wrapperRegistryNames,
|
||||
{
|
||||
Breadcrumb,
|
||||
SaveIndicator,
|
||||
LazyBarChart,
|
||||
LazyLineChart,
|
||||
LazyD3BarChart,
|
||||
SeedDataManager,
|
||||
StorageSettings,
|
||||
} as Record<string, ComponentType<any>>
|
||||
)
|
||||
|
||||
export const iconComponents: UIComponentRegistry = {
|
||||
ArrowLeft,
|
||||
ArrowRight,
|
||||
@@ -216,7 +206,6 @@ export const uiComponentRegistry: UIComponentRegistry = {
|
||||
...shadcnComponents,
|
||||
...atomComponents,
|
||||
...moleculeComponents,
|
||||
...wrapperComponents,
|
||||
...iconComponents,
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
@@ -147,9 +168,6 @@ export function JSONUIRenderer({
|
||||
|
||||
if (component.bindings) {
|
||||
Object.entries(component.bindings).forEach(([propName, binding]) => {
|
||||
if (propName === 'children') {
|
||||
return
|
||||
}
|
||||
props[propName] = resolveDataBinding(binding, dataMap, renderContext)
|
||||
})
|
||||
}
|
||||
@@ -203,17 +221,14 @@ export function JSONUIRenderer({
|
||||
|
||||
const props = resolveProps(renderContext)
|
||||
applyEventHandlers(props, renderContext)
|
||||
const boundChildren = component.bindings?.children
|
||||
? resolveDataBinding(component.bindings.children, dataMap, renderContext)
|
||||
: component.children
|
||||
|
||||
if (typeof Component === 'string') {
|
||||
return React.createElement(Component, props, renderChildren(boundChildren, renderContext))
|
||||
return React.createElement(Component, props, renderChildren(component.children, renderContext))
|
||||
}
|
||||
|
||||
return (
|
||||
<Component {...props}>
|
||||
{renderChildren(boundChildren, renderContext)}
|
||||
{renderChildren(component.children, renderContext)}
|
||||
</Component>
|
||||
)
|
||||
}
|
||||
@@ -237,10 +252,7 @@ export function JSONUIRenderer({
|
||||
...(component.loop!.indexVar ? { [component.loop!.indexVar]: index } : {}),
|
||||
}
|
||||
|
||||
const loopChildrenBinding = component.bindings?.children
|
||||
? resolveDataBinding(component.bindings.children, dataMap, loopContext)
|
||||
: component.children
|
||||
let content = renderChildren(loopChildrenBinding, loopContext)
|
||||
let content = renderChildren(component.children, loopContext)
|
||||
|
||||
if (component.conditional) {
|
||||
const conditionMet = evaluateCondition(component.conditional.if, { ...dataMap, ...loopContext })
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
import { JSONUIRenderer } from '@/lib/json-ui'
|
||||
import type { UIComponent } from '@/lib/json-ui/types'
|
||||
import breadcrumbDefinition from './definitions/breadcrumb.json'
|
||||
|
||||
export interface BreadcrumbItem {
|
||||
label: string
|
||||
href?: string
|
||||
onClick?: () => void
|
||||
}
|
||||
|
||||
export interface BreadcrumbProps {
|
||||
items: BreadcrumbItem[]
|
||||
className?: string
|
||||
}
|
||||
|
||||
const breadcrumbComponent = breadcrumbDefinition as UIComponent
|
||||
|
||||
export function Breadcrumb({ items, className }: BreadcrumbProps) {
|
||||
if (!items?.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<JSONUIRenderer
|
||||
component={breadcrumbComponent}
|
||||
dataMap={{ items, className }}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
import {
|
||||
Bar,
|
||||
BarChart,
|
||||
CartesianGrid,
|
||||
Legend,
|
||||
ResponsiveContainer,
|
||||
Tooltip,
|
||||
XAxis,
|
||||
YAxis,
|
||||
} from 'recharts'
|
||||
|
||||
export interface LazyBarChartProps {
|
||||
data: Array<Record<string, any>>
|
||||
xKey: string
|
||||
yKey: string
|
||||
width?: number | string
|
||||
height?: number
|
||||
color?: string
|
||||
showLegend?: boolean
|
||||
showGrid?: boolean
|
||||
}
|
||||
|
||||
export function LazyBarChart({
|
||||
data,
|
||||
xKey,
|
||||
yKey,
|
||||
width = 600,
|
||||
height = 300,
|
||||
color = '#8884d8',
|
||||
showLegend = true,
|
||||
showGrid = true,
|
||||
}: LazyBarChartProps) {
|
||||
if (!data?.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<ResponsiveContainer width={width} height={height}>
|
||||
<BarChart data={data}>
|
||||
{showGrid && <CartesianGrid strokeDasharray="3 3" />}
|
||||
<XAxis dataKey={xKey} />
|
||||
<YAxis />
|
||||
<Tooltip />
|
||||
{showLegend && <Legend />}
|
||||
<Bar dataKey={yKey} fill={color} />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
)
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
import { max, scaleBand, scaleLinear } from 'd3'
|
||||
|
||||
export interface LazyD3BarChartProps {
|
||||
data: Array<{ label: string; value: number }>
|
||||
width?: number
|
||||
height?: number
|
||||
color?: string
|
||||
showAxes?: boolean
|
||||
showGrid?: boolean
|
||||
}
|
||||
|
||||
export function LazyD3BarChart({
|
||||
data,
|
||||
width = 600,
|
||||
height = 300,
|
||||
color = '#8884d8',
|
||||
showAxes = true,
|
||||
showGrid = true,
|
||||
}: LazyD3BarChartProps) {
|
||||
if (!data?.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
const margin = { top: 20, right: 20, bottom: 30, left: 40 }
|
||||
const chartWidth = Math.max(width - margin.left - margin.right, 0)
|
||||
const chartHeight = Math.max(height - margin.top - margin.bottom, 0)
|
||||
|
||||
const maxValue = max(data, (d) => d.value) ?? 0
|
||||
const xScale = scaleBand()
|
||||
.domain(data.map((d) => d.label))
|
||||
.range([0, chartWidth])
|
||||
.padding(0.1)
|
||||
|
||||
const yScale = scaleLinear()
|
||||
.domain([0, maxValue])
|
||||
.nice()
|
||||
.range([chartHeight, 0])
|
||||
|
||||
const yTicks = yScale.ticks(4)
|
||||
|
||||
return (
|
||||
<svg width={width} height={height}>
|
||||
<g transform={`translate(${margin.left},${margin.top})`}>
|
||||
{showGrid &&
|
||||
yTicks.map((tick) => (
|
||||
<line
|
||||
key={`grid-${tick}`}
|
||||
x1={0}
|
||||
x2={chartWidth}
|
||||
y1={yScale(tick)}
|
||||
y2={yScale(tick)}
|
||||
stroke="currentColor"
|
||||
opacity={0.1}
|
||||
/>
|
||||
))}
|
||||
|
||||
{data.map((entry) => (
|
||||
<rect
|
||||
key={entry.label}
|
||||
x={xScale(entry.label) ?? 0}
|
||||
y={yScale(entry.value)}
|
||||
width={xScale.bandwidth()}
|
||||
height={chartHeight - yScale(entry.value)}
|
||||
fill={color}
|
||||
/>
|
||||
))}
|
||||
|
||||
{showAxes && (
|
||||
<>
|
||||
<line x1={0} x2={chartWidth} y1={chartHeight} y2={chartHeight} stroke="currentColor" />
|
||||
<line x1={0} x2={0} y1={0} y2={chartHeight} stroke="currentColor" />
|
||||
{yTicks.map((tick) => (
|
||||
<g key={`tick-${tick}`} transform={`translate(0,${yScale(tick)})`}>
|
||||
<line x1={-4} x2={0} y1={0} y2={0} stroke="currentColor" />
|
||||
<text x={-8} y={4} textAnchor="end" className="text-[10px] fill-muted-foreground">
|
||||
{tick}
|
||||
</text>
|
||||
</g>
|
||||
))}
|
||||
{data.map((entry) => (
|
||||
<text
|
||||
key={`label-${entry.label}`}
|
||||
x={(xScale(entry.label) ?? 0) + xScale.bandwidth() / 2}
|
||||
y={chartHeight + 16}
|
||||
textAnchor="middle"
|
||||
className="text-[10px] fill-muted-foreground"
|
||||
>
|
||||
{entry.label}
|
||||
</text>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</g>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
import {
|
||||
CartesianGrid,
|
||||
Legend,
|
||||
Line,
|
||||
LineChart,
|
||||
ResponsiveContainer,
|
||||
Tooltip,
|
||||
XAxis,
|
||||
YAxis,
|
||||
} from 'recharts'
|
||||
|
||||
export interface LazyLineChartProps {
|
||||
data: Array<Record<string, any>>
|
||||
xKey: string
|
||||
yKey: string
|
||||
width?: number | string
|
||||
height?: number
|
||||
color?: string
|
||||
showLegend?: boolean
|
||||
showGrid?: boolean
|
||||
}
|
||||
|
||||
export function LazyLineChart({
|
||||
data,
|
||||
xKey,
|
||||
yKey,
|
||||
width = 600,
|
||||
height = 300,
|
||||
color = '#8884d8',
|
||||
showLegend = true,
|
||||
showGrid = true,
|
||||
}: LazyLineChartProps) {
|
||||
if (!data?.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<ResponsiveContainer width={width} height={height}>
|
||||
<LineChart data={data}>
|
||||
{showGrid && <CartesianGrid strokeDasharray="3 3" />}
|
||||
<XAxis dataKey={xKey} />
|
||||
<YAxis />
|
||||
<Tooltip />
|
||||
{showLegend && <Legend />}
|
||||
<Line type="monotone" dataKey={yKey} stroke={color} />
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
)
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
import { JSONUIRenderer } from '@/lib/json-ui'
|
||||
import type { UIComponent } from '@/lib/json-ui/types'
|
||||
import saveIndicatorDefinition from './definitions/save-indicator.json'
|
||||
|
||||
export interface SaveIndicatorProps {
|
||||
status?: 'saved' | 'synced'
|
||||
label?: string
|
||||
showLabel?: boolean
|
||||
animate?: boolean
|
||||
className?: string
|
||||
}
|
||||
|
||||
const saveIndicatorComponent = saveIndicatorDefinition as UIComponent
|
||||
|
||||
export function SaveIndicator({
|
||||
status = 'saved',
|
||||
label,
|
||||
showLabel = true,
|
||||
animate,
|
||||
className,
|
||||
}: SaveIndicatorProps) {
|
||||
if (!status) {
|
||||
return null
|
||||
}
|
||||
|
||||
const resolvedLabel = label ?? (status === 'saved' ? 'Saved' : 'Synced')
|
||||
const shouldAnimate = animate ?? status === 'saved'
|
||||
|
||||
return (
|
||||
<JSONUIRenderer
|
||||
component={saveIndicatorComponent}
|
||||
dataMap={{
|
||||
status,
|
||||
label: resolvedLabel,
|
||||
showLabel,
|
||||
animate: shouldAnimate,
|
||||
className,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -1,108 +0,0 @@
|
||||
import { JSONUIRenderer } from '@/lib/json-ui'
|
||||
import type { UIComponent } from '@/lib/json-ui/types'
|
||||
import seedDataManagerDefinition from './definitions/seed-data-manager.json'
|
||||
|
||||
interface SeedDataManagerCopy {
|
||||
title: string
|
||||
description: string
|
||||
statusLoaded: string
|
||||
buttons: {
|
||||
load: string
|
||||
reset: string
|
||||
clear: string
|
||||
loadingLoad: string
|
||||
loadingReset: string
|
||||
loadingClear: string
|
||||
}
|
||||
help: {
|
||||
load: string
|
||||
reset: string
|
||||
clear: string
|
||||
}
|
||||
}
|
||||
|
||||
const defaultCopy: SeedDataManagerCopy = {
|
||||
title: 'Seed Data Management',
|
||||
description: 'Load, reset, or clear application seed data from the database',
|
||||
statusLoaded: 'Seed data is loaded and available',
|
||||
buttons: {
|
||||
load: 'Load Seed Data',
|
||||
reset: 'Reset to Defaults',
|
||||
clear: 'Clear All Data',
|
||||
loadingLoad: 'Loading...',
|
||||
loadingReset: 'Resetting...',
|
||||
loadingClear: 'Clearing...',
|
||||
},
|
||||
help: {
|
||||
load: 'Populates database with initial data if not already loaded',
|
||||
reset: 'Overwrites all data with fresh seed data',
|
||||
clear: 'Removes all data from the database (destructive action)',
|
||||
},
|
||||
}
|
||||
|
||||
export interface SeedDataManagerProps {
|
||||
isLoaded?: boolean
|
||||
isLoading?: boolean
|
||||
onLoadSeedData?: () => void
|
||||
onResetSeedData?: () => void
|
||||
onClearAllData?: () => void
|
||||
copy?: Partial<SeedDataManagerCopy>
|
||||
}
|
||||
|
||||
const seedDataManagerComponent = seedDataManagerDefinition as UIComponent
|
||||
|
||||
export function SeedDataManager({
|
||||
isLoaded = false,
|
||||
isLoading = false,
|
||||
onLoadSeedData,
|
||||
onResetSeedData,
|
||||
onClearAllData,
|
||||
copy,
|
||||
}: SeedDataManagerProps) {
|
||||
const resolvedCopy: SeedDataManagerCopy = {
|
||||
...defaultCopy,
|
||||
...copy,
|
||||
buttons: {
|
||||
...defaultCopy.buttons,
|
||||
...copy?.buttons,
|
||||
},
|
||||
help: {
|
||||
...defaultCopy.help,
|
||||
...copy?.help,
|
||||
},
|
||||
}
|
||||
|
||||
const loadDisabled = !onLoadSeedData || isLoading || isLoaded
|
||||
const resetDisabled = !onResetSeedData || isLoading
|
||||
const clearDisabled = !onClearAllData || isLoading
|
||||
|
||||
return (
|
||||
<JSONUIRenderer
|
||||
component={seedDataManagerComponent}
|
||||
dataMap={{
|
||||
title: resolvedCopy.title,
|
||||
description: resolvedCopy.description,
|
||||
statusLoaded: resolvedCopy.statusLoaded,
|
||||
onLoadSeedData,
|
||||
onResetSeedData,
|
||||
onClearAllData,
|
||||
isLoaded,
|
||||
loadDisabled,
|
||||
resetDisabled,
|
||||
clearDisabled,
|
||||
loadButtonText: isLoading
|
||||
? resolvedCopy.buttons.loadingLoad
|
||||
: resolvedCopy.buttons.load,
|
||||
resetButtonText: isLoading
|
||||
? resolvedCopy.buttons.loadingReset
|
||||
: resolvedCopy.buttons.reset,
|
||||
clearButtonText: isLoading
|
||||
? resolvedCopy.buttons.loadingClear
|
||||
: resolvedCopy.buttons.clear,
|
||||
helpLoad: `Load Seed Data: ${resolvedCopy.help.load}`,
|
||||
helpReset: `Reset to Defaults: ${resolvedCopy.help.reset}`,
|
||||
helpClear: `Clear All Data: ${resolvedCopy.help.clear}`,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -1,117 +0,0 @@
|
||||
import type { ChangeEvent } from 'react'
|
||||
import { JSONUIRenderer } from '@/lib/json-ui'
|
||||
import type { UIComponent } from '@/lib/json-ui/types'
|
||||
import {
|
||||
storageSettingsCopy,
|
||||
getBackendCopy,
|
||||
type StorageBackendKey,
|
||||
} from '@/components/storage/storageSettingsConfig'
|
||||
import storageSettingsDefinition from './definitions/storage-settings.json'
|
||||
|
||||
const defaultCopy = storageSettingsCopy.molecule
|
||||
|
||||
type StorageSettingsCopy = typeof defaultCopy
|
||||
|
||||
export interface StorageSettingsProps {
|
||||
backend: StorageBackendKey | null
|
||||
isLoading?: boolean
|
||||
flaskUrl?: string
|
||||
isSwitching?: boolean
|
||||
onFlaskUrlChange?: (value: string) => void
|
||||
onSwitchToFlask?: () => void
|
||||
onSwitchToIndexedDB?: () => void
|
||||
onSwitchToSQLite?: () => void
|
||||
isExporting?: boolean
|
||||
isImporting?: boolean
|
||||
onExport?: () => void
|
||||
onImport?: () => void
|
||||
copy?: Partial<StorageSettingsCopy>
|
||||
}
|
||||
|
||||
const storageSettingsComponent = storageSettingsDefinition as UIComponent
|
||||
|
||||
export function StorageSettings({
|
||||
backend,
|
||||
isLoading = false,
|
||||
flaskUrl = defaultCopy.flaskUrlPlaceholder,
|
||||
isSwitching = false,
|
||||
onFlaskUrlChange,
|
||||
onSwitchToFlask,
|
||||
onSwitchToIndexedDB,
|
||||
onSwitchToSQLite,
|
||||
isExporting = false,
|
||||
isImporting = false,
|
||||
onExport,
|
||||
onImport,
|
||||
copy,
|
||||
}: StorageSettingsProps) {
|
||||
const resolvedCopy: StorageSettingsCopy = {
|
||||
...defaultCopy,
|
||||
...copy,
|
||||
buttons: {
|
||||
...defaultCopy.buttons,
|
||||
...copy?.buttons,
|
||||
},
|
||||
backendDetails: {
|
||||
...defaultCopy.backendDetails,
|
||||
...copy?.backendDetails,
|
||||
},
|
||||
}
|
||||
|
||||
const backendCopy = getBackendCopy(backend)
|
||||
|
||||
const handleFlaskUrlChange = onFlaskUrlChange
|
||||
? (event: ChangeEvent<HTMLInputElement>) => onFlaskUrlChange(event.target.value)
|
||||
: undefined
|
||||
|
||||
return (
|
||||
<JSONUIRenderer
|
||||
component={storageSettingsComponent}
|
||||
dataMap={{
|
||||
title: resolvedCopy.title,
|
||||
description: resolvedCopy.description,
|
||||
currentBackendLabel: resolvedCopy.currentBackendLabel,
|
||||
backendBadge: backendCopy.moleculeLabel,
|
||||
flaskUrlLabel: resolvedCopy.flaskUrlLabel,
|
||||
flaskUrlPlaceholder: resolvedCopy.flaskUrlPlaceholder,
|
||||
flaskHelp: resolvedCopy.flaskHelp,
|
||||
flaskDetails: resolvedCopy.backendDetails.flask,
|
||||
indexedDbDetails: resolvedCopy.backendDetails.indexeddb,
|
||||
sqliteDetails: resolvedCopy.backendDetails.sqlite,
|
||||
flaskUrl,
|
||||
onFlaskUrlChange: handleFlaskUrlChange,
|
||||
flaskUrlDisabled: isSwitching || isLoading,
|
||||
onSwitchToFlask,
|
||||
flaskButtonDisabled: !onSwitchToFlask || isSwitching || isLoading || backend === 'flask',
|
||||
flaskButtonVariant: backend === 'flask' ? 'secondary' : 'default',
|
||||
flaskButtonLabel:
|
||||
backend === 'flask'
|
||||
? resolvedCopy.buttons.flaskActive
|
||||
: resolvedCopy.buttons.flaskUse,
|
||||
onSwitchToIndexedDB,
|
||||
indexedDbDisabled: !onSwitchToIndexedDB || isSwitching || isLoading || backend === 'indexeddb',
|
||||
indexedDbVariant: backend === 'indexeddb' ? 'secondary' : 'outline',
|
||||
indexedDbLabel:
|
||||
backend === 'indexeddb'
|
||||
? resolvedCopy.buttons.indexeddbActive
|
||||
: resolvedCopy.buttons.indexeddbUse,
|
||||
onSwitchToSQLite,
|
||||
sqliteDisabled: !onSwitchToSQLite || isSwitching || isLoading || backend === 'sqlite',
|
||||
sqliteVariant: backend === 'sqlite' ? 'secondary' : 'outline',
|
||||
sqliteLabel:
|
||||
backend === 'sqlite'
|
||||
? resolvedCopy.buttons.sqliteActive
|
||||
: resolvedCopy.buttons.sqliteUse,
|
||||
dataTitle: resolvedCopy.dataTitle,
|
||||
dataDescription: resolvedCopy.dataDescription,
|
||||
dataHelp: resolvedCopy.dataHelp,
|
||||
onExport,
|
||||
exportDisabled: !onExport || isExporting,
|
||||
exportLabel: resolvedCopy.buttons.export,
|
||||
onImport,
|
||||
importDisabled: !onImport || isImporting,
|
||||
importLabel: resolvedCopy.buttons.import,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -1,106 +0,0 @@
|
||||
{
|
||||
"id": "breadcrumb-root",
|
||||
"type": "nav",
|
||||
"className": "overflow-x-auto",
|
||||
"props": {
|
||||
"aria-label": "Breadcrumb"
|
||||
},
|
||||
"bindings": {
|
||||
"className": {
|
||||
"source": "className"
|
||||
}
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "breadcrumb-row",
|
||||
"type": "div",
|
||||
"className": "flex items-center gap-2 text-sm",
|
||||
"loop": {
|
||||
"source": "items",
|
||||
"itemVar": "item",
|
||||
"indexVar": "itemIndex"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "breadcrumb-link-last",
|
||||
"type": "Link",
|
||||
"conditional": {
|
||||
"if": "(item.href || item.onClick) && itemIndex === items.length - 1"
|
||||
},
|
||||
"props": {
|
||||
"variant": "default",
|
||||
"className": "text-foreground font-medium"
|
||||
},
|
||||
"bindings": {
|
||||
"href": {
|
||||
"source": "item.href"
|
||||
},
|
||||
"onClick": {
|
||||
"source": "item.onClick"
|
||||
},
|
||||
"children": {
|
||||
"source": "item.label"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "breadcrumb-link",
|
||||
"type": "Link",
|
||||
"conditional": {
|
||||
"if": "(item.href || item.onClick) && itemIndex < items.length - 1"
|
||||
},
|
||||
"props": {
|
||||
"variant": "muted",
|
||||
"className": "text-muted-foreground hover:text-foreground"
|
||||
},
|
||||
"bindings": {
|
||||
"href": {
|
||||
"source": "item.href"
|
||||
},
|
||||
"onClick": {
|
||||
"source": "item.onClick"
|
||||
},
|
||||
"children": {
|
||||
"source": "item.label"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "breadcrumb-text-last",
|
||||
"type": "span",
|
||||
"conditional": {
|
||||
"if": "!item.href && !item.onClick && itemIndex === items.length - 1"
|
||||
},
|
||||
"className": "text-foreground font-medium",
|
||||
"bindings": {
|
||||
"children": {
|
||||
"source": "item.label"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "breadcrumb-text",
|
||||
"type": "span",
|
||||
"conditional": {
|
||||
"if": "!item.href && !item.onClick && itemIndex < items.length - 1"
|
||||
},
|
||||
"className": "text-muted-foreground",
|
||||
"bindings": {
|
||||
"children": {
|
||||
"source": "item.label"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "breadcrumb-separator",
|
||||
"type": "span",
|
||||
"conditional": {
|
||||
"if": "itemIndex < items.length - 1"
|
||||
},
|
||||
"className": "text-muted-foreground",
|
||||
"children": "/"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
{
|
||||
"id": "save-indicator-root",
|
||||
"type": "div",
|
||||
"className": "flex items-center gap-1.5 text-xs text-muted-foreground",
|
||||
"bindings": {
|
||||
"className": {
|
||||
"source": "className"
|
||||
}
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "save-indicator-icon",
|
||||
"type": "StatusIcon",
|
||||
"bindings": {
|
||||
"type": {
|
||||
"source": "status"
|
||||
},
|
||||
"animate": {
|
||||
"source": "animate"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "save-indicator-label",
|
||||
"type": "span",
|
||||
"conditional": {
|
||||
"if": "showLabel"
|
||||
},
|
||||
"bindings": {
|
||||
"children": {
|
||||
"source": "label"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,157 +0,0 @@
|
||||
{
|
||||
"id": "seed-data-manager",
|
||||
"type": "Card",
|
||||
"children": [
|
||||
{
|
||||
"id": "seed-data-header",
|
||||
"type": "CardHeader",
|
||||
"children": [
|
||||
{
|
||||
"id": "seed-data-title",
|
||||
"type": "CardTitle",
|
||||
"bindings": {
|
||||
"children": {
|
||||
"source": "title"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "seed-data-description",
|
||||
"type": "CardDescription",
|
||||
"bindings": {
|
||||
"children": {
|
||||
"source": "description"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "seed-data-content",
|
||||
"type": "CardContent",
|
||||
"className": "flex flex-col gap-4",
|
||||
"children": [
|
||||
{
|
||||
"id": "seed-data-alert",
|
||||
"type": "Alert",
|
||||
"conditional": {
|
||||
"if": "isLoaded"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "seed-data-alert-description",
|
||||
"type": "AlertDescription",
|
||||
"bindings": {
|
||||
"children": {
|
||||
"source": "statusLoaded"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "seed-data-actions-block",
|
||||
"type": "div",
|
||||
"className": "flex flex-col gap-3",
|
||||
"children": [
|
||||
{
|
||||
"id": "seed-data-actions",
|
||||
"type": "div",
|
||||
"className": "flex gap-2 flex-wrap",
|
||||
"children": [
|
||||
{
|
||||
"id": "seed-data-load",
|
||||
"type": "Button",
|
||||
"props": {
|
||||
"variant": "default"
|
||||
},
|
||||
"bindings": {
|
||||
"onClick": {
|
||||
"source": "onLoadSeedData"
|
||||
},
|
||||
"disabled": {
|
||||
"source": "loadDisabled"
|
||||
},
|
||||
"children": {
|
||||
"source": "loadButtonText"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "seed-data-reset",
|
||||
"type": "Button",
|
||||
"props": {
|
||||
"variant": "outline"
|
||||
},
|
||||
"bindings": {
|
||||
"onClick": {
|
||||
"source": "onResetSeedData"
|
||||
},
|
||||
"disabled": {
|
||||
"source": "resetDisabled"
|
||||
},
|
||||
"children": {
|
||||
"source": "resetButtonText"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "seed-data-clear",
|
||||
"type": "Button",
|
||||
"props": {
|
||||
"variant": "destructive"
|
||||
},
|
||||
"bindings": {
|
||||
"onClick": {
|
||||
"source": "onClearAllData"
|
||||
},
|
||||
"disabled": {
|
||||
"source": "clearDisabled"
|
||||
},
|
||||
"children": {
|
||||
"source": "clearButtonText"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "seed-data-help",
|
||||
"type": "div",
|
||||
"className": "text-sm text-muted-foreground space-y-1",
|
||||
"children": [
|
||||
{
|
||||
"id": "seed-data-help-load",
|
||||
"type": "p",
|
||||
"bindings": {
|
||||
"children": {
|
||||
"source": "helpLoad"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "seed-data-help-reset",
|
||||
"type": "p",
|
||||
"bindings": {
|
||||
"children": {
|
||||
"source": "helpReset"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "seed-data-help-clear",
|
||||
"type": "p",
|
||||
"bindings": {
|
||||
"children": {
|
||||
"source": "helpClear"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,331 +0,0 @@
|
||||
{
|
||||
"id": "storage-settings-root",
|
||||
"type": "div",
|
||||
"className": "space-y-6",
|
||||
"children": [
|
||||
{
|
||||
"id": "storage-backend-card",
|
||||
"type": "Card",
|
||||
"children": [
|
||||
{
|
||||
"id": "storage-backend-header",
|
||||
"type": "CardHeader",
|
||||
"children": [
|
||||
{
|
||||
"id": "storage-backend-title",
|
||||
"type": "CardTitle",
|
||||
"bindings": {
|
||||
"children": {
|
||||
"source": "title"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "storage-backend-description",
|
||||
"type": "CardDescription",
|
||||
"bindings": {
|
||||
"children": {
|
||||
"source": "description"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "storage-backend-content",
|
||||
"type": "CardContent",
|
||||
"className": "space-y-4",
|
||||
"children": [
|
||||
{
|
||||
"id": "storage-backend-current",
|
||||
"type": "div",
|
||||
"className": "flex items-center gap-2",
|
||||
"children": [
|
||||
{
|
||||
"id": "storage-backend-current-label",
|
||||
"type": "span",
|
||||
"className": "text-sm text-muted-foreground",
|
||||
"bindings": {
|
||||
"children": {
|
||||
"source": "currentBackendLabel"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "storage-backend-current-badge",
|
||||
"type": "Badge",
|
||||
"props": {
|
||||
"variant": "secondary",
|
||||
"className": "flex items-center gap-1"
|
||||
},
|
||||
"bindings": {
|
||||
"children": {
|
||||
"source": "backendBadge"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "storage-backend-form",
|
||||
"type": "div",
|
||||
"className": "grid gap-4",
|
||||
"children": [
|
||||
{
|
||||
"id": "storage-backend-flask",
|
||||
"type": "div",
|
||||
"className": "space-y-2",
|
||||
"children": [
|
||||
{
|
||||
"id": "storage-backend-flask-label",
|
||||
"type": "Label",
|
||||
"props": {
|
||||
"htmlFor": "flask-url"
|
||||
},
|
||||
"bindings": {
|
||||
"children": {
|
||||
"source": "flaskUrlLabel"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "storage-backend-flask-row",
|
||||
"type": "div",
|
||||
"className": "flex gap-2",
|
||||
"children": [
|
||||
{
|
||||
"id": "storage-backend-flask-input",
|
||||
"type": "Input",
|
||||
"bindings": {
|
||||
"value": {
|
||||
"source": "flaskUrl"
|
||||
},
|
||||
"onChange": {
|
||||
"source": "onFlaskUrlChange"
|
||||
},
|
||||
"disabled": {
|
||||
"source": "flaskUrlDisabled"
|
||||
},
|
||||
"placeholder": {
|
||||
"source": "flaskUrlPlaceholder"
|
||||
}
|
||||
},
|
||||
"props": {
|
||||
"id": "flask-url"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "storage-backend-flask-button",
|
||||
"type": "Button",
|
||||
"bindings": {
|
||||
"onClick": {
|
||||
"source": "onSwitchToFlask"
|
||||
},
|
||||
"disabled": {
|
||||
"source": "flaskButtonDisabled"
|
||||
},
|
||||
"variant": {
|
||||
"source": "flaskButtonVariant"
|
||||
},
|
||||
"children": {
|
||||
"source": "flaskButtonLabel"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "storage-backend-flask-help",
|
||||
"type": "p",
|
||||
"className": "text-xs text-muted-foreground",
|
||||
"bindings": {
|
||||
"children": {
|
||||
"source": "flaskHelp"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "storage-backend-options",
|
||||
"type": "div",
|
||||
"className": "flex gap-2",
|
||||
"children": [
|
||||
{
|
||||
"id": "storage-backend-indexeddb",
|
||||
"type": "Button",
|
||||
"props": {
|
||||
"className": "flex-1"
|
||||
},
|
||||
"bindings": {
|
||||
"onClick": {
|
||||
"source": "onSwitchToIndexedDB"
|
||||
},
|
||||
"disabled": {
|
||||
"source": "indexedDbDisabled"
|
||||
},
|
||||
"variant": {
|
||||
"source": "indexedDbVariant"
|
||||
},
|
||||
"children": {
|
||||
"source": "indexedDbLabel"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "storage-backend-sqlite",
|
||||
"type": "Button",
|
||||
"props": {
|
||||
"className": "flex-1"
|
||||
},
|
||||
"bindings": {
|
||||
"onClick": {
|
||||
"source": "onSwitchToSQLite"
|
||||
},
|
||||
"disabled": {
|
||||
"source": "sqliteDisabled"
|
||||
},
|
||||
"variant": {
|
||||
"source": "sqliteVariant"
|
||||
},
|
||||
"children": {
|
||||
"source": "sqliteLabel"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "storage-backend-details",
|
||||
"type": "div",
|
||||
"className": "text-xs text-muted-foreground space-y-1",
|
||||
"children": [
|
||||
{
|
||||
"id": "storage-backend-details-indexeddb",
|
||||
"type": "p",
|
||||
"bindings": {
|
||||
"children": {
|
||||
"source": "indexedDbDetails"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "storage-backend-details-sqlite",
|
||||
"type": "p",
|
||||
"bindings": {
|
||||
"children": {
|
||||
"source": "sqliteDetails"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "storage-backend-details-flask",
|
||||
"type": "p",
|
||||
"bindings": {
|
||||
"children": {
|
||||
"source": "flaskDetails"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "storage-data-card",
|
||||
"type": "Card",
|
||||
"children": [
|
||||
{
|
||||
"id": "storage-data-header",
|
||||
"type": "CardHeader",
|
||||
"children": [
|
||||
{
|
||||
"id": "storage-data-title",
|
||||
"type": "CardTitle",
|
||||
"bindings": {
|
||||
"children": {
|
||||
"source": "dataTitle"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "storage-data-description",
|
||||
"type": "CardDescription",
|
||||
"bindings": {
|
||||
"children": {
|
||||
"source": "dataDescription"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "storage-data-content",
|
||||
"type": "CardContent",
|
||||
"className": "space-y-4",
|
||||
"children": [
|
||||
{
|
||||
"id": "storage-data-actions",
|
||||
"type": "div",
|
||||
"className": "flex gap-2",
|
||||
"children": [
|
||||
{
|
||||
"id": "storage-data-export",
|
||||
"type": "Button",
|
||||
"props": {
|
||||
"variant": "outline",
|
||||
"className": "flex-1"
|
||||
},
|
||||
"bindings": {
|
||||
"onClick": {
|
||||
"source": "onExport"
|
||||
},
|
||||
"disabled": {
|
||||
"source": "exportDisabled"
|
||||
},
|
||||
"children": {
|
||||
"source": "exportLabel"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "storage-data-import",
|
||||
"type": "Button",
|
||||
"props": {
|
||||
"variant": "outline",
|
||||
"className": "flex-1"
|
||||
},
|
||||
"bindings": {
|
||||
"onClick": {
|
||||
"source": "onImport"
|
||||
},
|
||||
"disabled": {
|
||||
"source": "importDisabled"
|
||||
},
|
||||
"children": {
|
||||
"source": "importLabel"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "storage-data-help",
|
||||
"type": "p",
|
||||
"className": "text-xs text-muted-foreground",
|
||||
"bindings": {
|
||||
"children": {
|
||||
"source": "dataHelp"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
295
src/schemas/page-schemas.json
Normal file
295
src/schemas/page-schemas.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -4,11 +4,13 @@ export type ComponentType =
|
||||
| 'Input' | 'TextArea' | 'Textarea' | 'Select' | 'Checkbox' | 'Radio' | 'Switch' | 'Slider' | 'NumberInput'
|
||||
| 'Badge' | 'Progress' | 'Separator' | 'Tabs' | 'TabsContent' | 'TabsList' | 'TabsTrigger' | 'Dialog'
|
||||
| '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'
|
||||
| 'ErrorBadge' | 'Notification' | 'StatusIcon'
|
||||
| 'CircularProgress' | 'Divider' | 'ProgressBar'
|
||||
| 'Table' | 'KeyValue' | 'StatCard' | 'DataCard' | 'SearchInput' | 'ActionBar'
|
||||
| 'DataList' | 'DataTable' | 'MetricCard' | 'Timeline'
|
||||
| 'AppBranding' | 'LabelWithBadge' | 'EmptyEditorState' | 'LoadingFallback' | 'LoadingState' | 'NavigationGroupHeader'
|
||||
| 'Breadcrumb' | 'SaveIndicator' | 'LazyBarChart' | 'LazyD3BarChart' | 'LazyLineChart' | 'SeedDataManager' | 'StorageSettings'
|
||||
|
||||
export type ActionType =
|
||||
| 'create' | 'update' | 'delete' | 'navigate'
|
||||
@@ -19,6 +21,9 @@ export type ActionType =
|
||||
export type DataSourceType =
|
||||
| 'kv' | 'computed' | 'static'
|
||||
|
||||
export type BindingSourceType =
|
||||
| 'data' | 'bindings' | 'state'
|
||||
|
||||
export interface DataSource {
|
||||
id: string
|
||||
type: DataSourceType
|
||||
@@ -47,6 +52,7 @@ export interface Action {
|
||||
|
||||
export interface Binding {
|
||||
source: string
|
||||
sourceType?: BindingSourceType
|
||||
path?: string
|
||||
transform?: string | ((value: any) => any)
|
||||
}
|
||||
@@ -57,6 +63,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
|
||||
@@ -77,7 +92,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
|
||||
@@ -116,6 +131,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