diff --git a/src/components/JSONLambdaDesigner.tsx b/src/components/JSONLambdaDesigner.tsx
index ddf3da2..6e6fed5 100644
--- a/src/components/JSONLambdaDesigner.tsx
+++ b/src/components/JSONLambdaDesigner.tsx
@@ -1,16 +1,9 @@
-import { PageRenderer } from '@/lib/schema-renderer'
+import { PageRenderer } from '@/lib/json-ui/page-renderer'
import lambdaDesignerSchema from '@/config/pages/lambda-designer.json'
-import { useKV } from '@/hooks/use-kv'
-import { Component as ComponentSchema } from '@/schemas/ui-schema'
+import { PageSchema } from '@/types/json-ui'
export function JSONLambdaDesigner() {
- const [lambdas] = useKV('app-lambdas', [])
-
return (
-
+
)
}
diff --git a/src/components/organisms/JSONUIShowcase.tsx b/src/components/organisms/JSONUIShowcase.tsx
deleted file mode 100644
index 56c6b1d..0000000
--- a/src/components/organisms/JSONUIShowcase.tsx
+++ /dev/null
@@ -1,172 +0,0 @@
-import { useState, useEffect } from 'react'
-import { PageRenderer } from '@/lib/schema-renderer'
-import { useSchemaLoader } from '@/hooks/ui'
-import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
-import { Alert, AlertDescription } from '@/components/ui/alert'
-import { Button } from '@/components/ui/button'
-import { Code, FileText, Database } from '@phosphor-icons/react'
-import dashboardSchema from '@/config/schemas/json-ui-dashboard.json'
-
-interface JSONUIShowcaseProps {
- files?: any[]
- models?: any[]
- components?: any[]
-}
-
-export function JSONUIShowcase({
- files = [],
- models = [],
- components = []
-}: JSONUIShowcaseProps) {
- const [showJSON, setShowJSON] = useState(false)
- const {schema: loadedSchema, loading, error} = useSchemaLoader({
- schema: dashboardSchema as any
- })
-
- const data = {
- files: files.length > 0 ? files : [
- { name: 'App.tsx', type: 'TypeScript' },
- { name: 'index.css', type: 'CSS' },
- { name: 'schema-renderer.tsx', type: 'TypeScript' },
- { name: 'use-data-binding.ts', type: 'Hook' },
- { name: 'dashboard.json', type: 'JSON' },
- ],
- models: models.length > 0 ? models : [
- { name: 'User', fields: 5 },
- { name: 'Post', fields: 8 },
- { name: 'Comment', fields: 4 },
- ],
- components: components.length > 0 ? components : [
- { name: 'Button', type: 'atom' },
- { name: 'Card', type: 'molecule' },
- { name: 'Dashboard', type: 'organism' },
- ],
- }
-
- const functions = {
- handleClick: () => {
- console.log('Button clicked from JSON!')
- },
- }
-
- if (loading) {
- return (
-
- )
- }
-
- if (error) {
- return (
-
-
-
- Failed to load schema: {error.message}
-
-
-
- )
- }
-
- if (!loadedSchema) {
- return (
-
- )
- }
-
- return (
-
-
-
-
-
-
- JSON-Driven UI System
-
-
- Complete UI rendering from declarative JSON schemas with data bindings and event handlers
-
-
-
-
-
-
- {showJSON && (
-
- {JSON.stringify(loadedSchema, null, 2)}
-
- )}
-
-
-
-
-
-
-
-
- Schema-Driven
-
-
-
-
- UI structure defined in JSON, making it easy to modify without code changes
-
-
-
-
-
-
-
-
- Data Bindings
-
-
-
-
- Dynamic expressions in JSON connect UI to application state seamlessly
-
-
-
-
-
-
-
-
- Atomic Design
-
-
-
-
- Modular components composed from atoms to organisms following best practices
-
-
-
-
-
-
-
Rendered from JSON
-
- The content below is entirely generated from the JSON schema above, demonstrating data bindings,
- loops, and component composition.
-
-
-
-
-
- )
-}
diff --git a/src/components/organisms/index.ts b/src/components/organisms/index.ts
index f4a4e6f..3f2192c 100644
--- a/src/components/organisms/index.ts
+++ b/src/components/organisms/index.ts
@@ -12,4 +12,4 @@ export { SchemaEditorLayout } from './SchemaEditorLayout'
export { EmptyCanvasState } from './EmptyCanvasState'
export { SchemaEditorStatusBar } from './SchemaEditorStatusBar'
export { SchemaCodeViewer } from './SchemaCodeViewer'
-export { JSONUIShowcase } from './JSONUIShowcase'
+export { JSONUIShowcase } from '../JSONUIShowcase'
diff --git a/src/config/schemas/json-ui-dashboard.json b/src/config/schemas/json-ui-dashboard.json
deleted file mode 100644
index 2667312..0000000
--- a/src/config/schemas/json-ui-dashboard.json
+++ /dev/null
@@ -1,236 +0,0 @@
-{
- "id": "json-ui-dashboard",
- "title": "JSON-Driven Dashboard",
- "description": "A complete UI page rendered from JSON declarations",
- "layout": {
- "type": "flex",
- "direction": "column",
- "gap": "6",
- "className": "h-full overflow-auto p-6"
- },
- "components": [
- {
- "id": "page-header",
- "type": "div",
- "props": {
- "className": "mb-6"
- },
- "children": [
- {
- "id": "page-title",
- "type": "div",
- "props": {
- "className": "text-3xl font-bold mb-2"
- },
- "children": [
- {
- "id": "title-text",
- "type": "div",
- "props": {
- "children": "JSON-Driven UI System"
- }
- }
- ]
- },
- {
- "id": "page-description",
- "type": "div",
- "props": {
- "className": "text-muted-foreground",
- "children": "This entire page is rendered from JSON schemas with data bindings"
- }
- }
- ]
- },
- {
- "id": "stat-cards-grid",
- "type": "div",
- "props": {
- "className": "grid gap-4 md:grid-cols-2 lg:grid-cols-3"
- },
- "children": [
- {
- "id": "stat-card-1",
- "type": "Card",
- "children": [
- {
- "id": "stat-card-1-header",
- "type": "CardHeader",
- "children": [
- {
- "id": "stat-card-1-title",
- "type": "CardTitle",
- "props": {
- "className": "flex items-center gap-2"
- },
- "children": [
- {
- "id": "stat-1-text",
- "type": "div",
- "props": {
- "children": "{{files.length}} Files"
- }
- }
- ]
- }
- ]
- },
- {
- "id": "stat-card-1-content",
- "type": "CardContent",
- "children": [
- {
- "id": "stat-1-value",
- "type": "div",
- "props": {
- "className": "text-2xl font-bold",
- "children": "{{files.length}}"
- }
- }
- ]
- }
- ]
- },
- {
- "id": "stat-card-2",
- "type": "Card",
- "children": [
- {
- "id": "stat-card-2-header",
- "type": "CardHeader",
- "children": [
- {
- "id": "stat-card-2-title",
- "type": "CardTitle",
- "props": {
- "children": "{{models.length}} Models"
- }
- }
- ]
- },
- {
- "id": "stat-card-2-content",
- "type": "CardContent",
- "children": [
- {
- "id": "stat-2-value",
- "type": "div",
- "props": {
- "className": "text-2xl font-bold",
- "children": "{{models.length}}"
- }
- }
- ]
- }
- ]
- },
- {
- "id": "stat-card-3",
- "type": "Card",
- "children": [
- {
- "id": "stat-card-3-header",
- "type": "CardHeader",
- "children": [
- {
- "id": "stat-card-3-title",
- "type": "CardTitle",
- "props": {
- "children": "{{components.length}} Components"
- }
- }
- ]
- },
- {
- "id": "stat-card-3-content",
- "type": "CardContent",
- "children": [
- {
- "id": "stat-3-value",
- "type": "div",
- "props": {
- "className": "text-2xl font-bold",
- "children": "{{components.length}}"
- }
- }
- ]
- }
- ]
- }
- ]
- },
- {
- "id": "files-section",
- "type": "Card",
- "props": {
- "className": "mt-6"
- },
- "children": [
- {
- "id": "files-header",
- "type": "CardHeader",
- "children": [
- {
- "id": "files-title",
- "type": "CardTitle",
- "props": {
- "children": "Recent Files"
- }
- },
- {
- "id": "files-description",
- "type": "CardDescription",
- "props": {
- "children": "Files loaded from your project"
- }
- }
- ]
- },
- {
- "id": "files-content",
- "type": "CardContent",
- "children": [
- {
- "id": "files-list",
- "type": "div",
- "props": {
- "className": "space-y-2"
- },
- "repeat": {
- "items": "files.slice(0, 5)",
- "itemVar": "file"
- },
- "children": [
- {
- "id": "file-item",
- "type": "div",
- "props": {
- "className": "flex items-center justify-between p-2 border border-border rounded-md hover:bg-accent/50 transition-colors"
- },
- "children": [
- {
- "id": "file-name",
- "type": "div",
- "props": {
- "className": "font-mono text-sm",
- "children": "{{file.name}}"
- }
- },
- {
- "id": "file-badge",
- "type": "Badge",
- "props": {
- "variant": "outline",
- "children": "{{file.type}}"
- }
- }
- ]
- }
- ]
- }
- ]
- }
- ]
- }
- ]
-}
diff --git a/src/hooks/ui/index.ts b/src/hooks/ui/index.ts
index 3b715ed..6931aec 100644
--- a/src/hooks/ui/index.ts
+++ b/src/hooks/ui/index.ts
@@ -1,7 +1,3 @@
-export { useDataBinding } from './use-data-binding'
-export { useEventHandlers } from './use-event-handlers'
-export { useSchemaLoader } from './use-schema-loader'
-export { useComponentRegistry } from './use-component-registry'
export { useDashboardMetrics } from './use-dashboard-metrics'
export { useDashboardTips } from './use-dashboard-tips'
export { useToggle } from './use-toggle'
diff --git a/src/hooks/ui/use-component-registry.ts b/src/hooks/ui/use-component-registry.ts
deleted file mode 100644
index 6c0d309..0000000
--- a/src/hooks/ui/use-component-registry.ts
+++ /dev/null
@@ -1,37 +0,0 @@
-import { createElement, useMemo } from 'react'
-import { uiComponentRegistry, iconComponents } from '@/lib/json-ui/component-registry'
-import * as Icons from '@phosphor-icons/react'
-
-interface ComponentRegistryOptions {
- customComponents?: Record>
-}
-
-export function useComponentRegistry({ customComponents = {} }: ComponentRegistryOptions = {}) {
- const registry = useMemo(
- () => ({
- ...uiComponentRegistry,
- ...customComponents,
- }),
- [customComponents]
- )
-
- const getComponent = (type: string): React.ComponentType | null => {
- return registry[type as keyof typeof registry] || null
- }
-
- const getIcon = (iconName: string, props?: any): React.ReactElement | null => {
- const registryIcon = registry[iconName as keyof typeof registry]
- const IconComponent =
- (registryIcon && typeof registryIcon !== 'string' ? registryIcon : null) ||
- iconComponents[iconName as keyof typeof iconComponents] ||
- (Icons as any)[iconName]
- if (!IconComponent || typeof IconComponent === 'string') return null
- return createElement(IconComponent, { size: 24, weight: "duotone", ...props })
- }
-
- return {
- registry,
- getComponent,
- getIcon,
- }
-}
diff --git a/src/hooks/ui/use-data-binding.ts b/src/hooks/ui/use-data-binding.ts
deleted file mode 100644
index 3bb0157..0000000
--- a/src/hooks/ui/use-data-binding.ts
+++ /dev/null
@@ -1,74 +0,0 @@
-import { useCallback, useMemo } from 'react'
-
-interface UseDataBindingOptions {
- data: Record
- onError?: (error: Error, expression: string) => void
-}
-
-export function useDataBinding({ data, onError }: UseDataBindingOptions) {
- const resolveBinding = useCallback(
- (expression: string, fallback?: any): any => {
- if (!expression) return fallback
-
- try {
- const keys = Object.keys(data)
- const values = Object.values(data)
- const func = new Function(...keys, `"use strict"; return (${expression})`)
- return func(...values)
- } catch (error) {
- if (onError) {
- onError(error as Error, expression)
- }
- console.warn(`Failed to resolve binding: ${expression}`, error)
- return fallback
- }
- },
- [data, onError]
- )
-
- const resolveCondition = useCallback(
- (condition: string): boolean => {
- try {
- const result = resolveBinding(condition, false)
- return Boolean(result)
- } catch {
- return false
- }
- },
- [resolveBinding]
- )
-
- const resolveProps = useCallback(
- (props: Record): Record => {
- if (!props) return {}
-
- const resolved: Record = {}
-
- for (const [key, value] of Object.entries(props)) {
- if (typeof value === 'string' && value.startsWith('{{') && value.endsWith('}}')) {
- const expression = value.slice(2, -2).trim()
- resolved[key] = resolveBinding(expression)
- } else if (typeof value === 'object' && value !== null && value.type === 'binding') {
- resolved[key] = resolveBinding(value.expression, value.fallback)
- } else {
- resolved[key] = value
- }
- }
-
- return resolved
- },
- [resolveBinding]
- )
-
- const context = useMemo(
- () => ({
- resolveBinding,
- resolveCondition,
- resolveProps,
- data,
- }),
- [resolveBinding, resolveCondition, resolveProps, data]
- )
-
- return context
-}
diff --git a/src/hooks/ui/use-event-handlers.ts b/src/hooks/ui/use-event-handlers.ts
deleted file mode 100644
index 0387985..0000000
--- a/src/hooks/ui/use-event-handlers.ts
+++ /dev/null
@@ -1,63 +0,0 @@
-import { useCallback, useMemo } from 'react'
-
-interface UseEventHandlersOptions {
- functions?: Record any>
- onError?: (error: Error, functionName: string) => void
-}
-
-export function useEventHandlers({ functions = {}, onError }: UseEventHandlersOptions) {
- const createHandler = useCallback(
- (functionName: string) => {
- return (...args: any[]) => {
- const handler = functions[functionName]
-
- if (!handler) {
- const error = new Error(`Function "${functionName}" not found`)
- if (onError) {
- onError(error, functionName)
- } else {
- console.error(error)
- }
- return
- }
-
- try {
- return handler(...args)
- } catch (error) {
- if (onError) {
- onError(error as Error, functionName)
- } else {
- console.error(`Error executing function "${functionName}":`, error)
- }
- }
- }
- },
- [functions, onError]
- )
-
- const resolveEvents = useCallback(
- (events?: Record): Record any> => {
- if (!events) return {}
-
- const resolved: Record any> = {}
-
- for (const [eventName, functionName] of Object.entries(events)) {
- resolved[eventName] = createHandler(functionName)
- }
-
- return resolved
- },
- [createHandler]
- )
-
- const context = useMemo(
- () => ({
- createHandler,
- resolveEvents,
- functions,
- }),
- [createHandler, resolveEvents, functions]
- )
-
- return context
-}
diff --git a/src/hooks/ui/use-schema-loader.ts b/src/hooks/ui/use-schema-loader.ts
deleted file mode 100644
index 241ff15..0000000
--- a/src/hooks/ui/use-schema-loader.ts
+++ /dev/null
@@ -1,60 +0,0 @@
-import { useState, useCallback, useEffect } from 'react'
-import { PageSchemaType } from '@/schemas/ui-schema'
-
-interface UseSchemaLoaderOptions {
- schemaUrl?: string
- schema?: PageSchemaType
- onError?: (error: Error) => void
-}
-
-export function useSchemaLoader({ schemaUrl, schema: initialSchema, onError }: UseSchemaLoaderOptions) {
- const [schema, setSchema] = useState(initialSchema || null)
- const [loading, setLoading] = useState(!!schemaUrl && !initialSchema)
- const [error, setError] = useState(null)
-
- const loadSchema = useCallback(
- async (url: string) => {
- setLoading(true)
- setError(null)
-
- try {
- const response = await fetch(url)
- if (!response.ok) {
- throw new Error(`Failed to load schema: ${response.statusText}`)
- }
-
- const data = await response.json()
- setSchema(data)
- } catch (err) {
- const error = err instanceof Error ? err : new Error('Unknown error loading schema')
- setError(error)
- if (onError) {
- onError(error)
- }
- } finally {
- setLoading(false)
- }
- },
- [onError]
- )
-
- useEffect(() => {
- if (schemaUrl && !initialSchema) {
- loadSchema(schemaUrl)
- }
- }, [schemaUrl, initialSchema, loadSchema])
-
- const reloadSchema = useCallback(() => {
- if (schemaUrl) {
- loadSchema(schemaUrl)
- }
- }, [schemaUrl, loadSchema])
-
- return {
- schema,
- loading,
- error,
- reloadSchema,
- setSchema,
- }
-}
diff --git a/src/lib/schema-renderer.tsx b/src/lib/schema-renderer.tsx
deleted file mode 100644
index 02ed0fc..0000000
--- a/src/lib/schema-renderer.tsx
+++ /dev/null
@@ -1,179 +0,0 @@
-import { createElement, type ComponentType, type ReactNode } from 'react'
-import { cn } from '@/lib/utils'
-import { Component as ComponentSchema, Layout } from '@/schemas/ui-schema'
-import { useDataBinding, useEventHandlers, useComponentRegistry } from '@/hooks/ui'
-import { getDeprecatedComponentInfo } from '@/lib/json-ui/component-registry'
-
-interface SchemaRendererProps {
- schema: ComponentSchema
- data: Record
- functions?: Record any>
- componentRegistry?: Record>
-}
-
-interface LayoutRendererProps {
- layout: Layout
- children: ReactNode
-}
-
-const warnedDeprecatedComponents = new Set()
-
-const warnDeprecatedComponent = (schema: ComponentSchema) => {
- const deprecatedInfo = getDeprecatedComponentInfo(schema.type)
- if (!deprecatedInfo || warnedDeprecatedComponents.has(schema.type)) {
- return
- }
-
- const idSuffix = schema.id ? ` (id: ${schema.id})` : ''
- const replacementHint = deprecatedInfo.replacedBy
- ? ` Replace with "${deprecatedInfo.replacedBy}".`
- : ''
- const extraMessage = deprecatedInfo.message ? ` ${deprecatedInfo.message}` : ''
-
- console.warn(
- `[SchemaRenderer] Deprecated component "${schema.type}" detected in schema${idSuffix}.${replacementHint}${extraMessage}`
- )
- warnedDeprecatedComponents.add(schema.type)
-}
-
-function LayoutRenderer({ layout, children }: LayoutRendererProps) {
- const getLayoutClasses = () => {
- const classes: string[] = []
-
- if (layout.type === 'flex') {
- classes.push('flex')
- if (layout.direction) {
- classes.push(layout.direction === 'column' ? 'flex-col' : 'flex-row')
- }
- } else if (layout.type === 'grid') {
- classes.push('grid')
- if (layout.columns) {
- const { base = 1, sm, md, lg, xl } = layout.columns
- classes.push(`grid-cols-${base}`)
- if (sm) classes.push(`sm:grid-cols-${sm}`)
- if (md) classes.push(`md:grid-cols-${md}`)
- if (lg) classes.push(`lg:grid-cols-${lg}`)
- if (xl) classes.push(`xl:grid-cols-${xl}`)
- }
- } else if (layout.type === 'stack') {
- classes.push('flex flex-col')
- }
-
- if (layout.gap) {
- classes.push(`gap-${layout.gap}`)
- }
-
- if (layout.className) {
- classes.push(layout.className)
- }
-
- return cn(...classes)
- }
-
- return {children}
-}
-
-export function SchemaRenderer({ schema, data, functions = {}, componentRegistry }: SchemaRendererProps) {
- const { resolveCondition, resolveProps, resolveBinding } = useDataBinding({ data })
- const { resolveEvents } = useEventHandlers({ functions })
- const { getComponent, getIcon } = useComponentRegistry({ customComponents: componentRegistry })
-
- if (schema.condition && !resolveCondition(schema.condition)) {
- return null
- }
-
- if (schema.repeat) {
- const items = resolveBinding(schema.repeat.items, []) as any[]
- return (
- <>
- {items.map((item, index) => {
- const itemData = {
- ...data,
- [schema.repeat!.itemVar]: item,
- ...(schema.repeat!.indexVar ? { [schema.repeat!.indexVar]: index } : {}),
- }
- return (
-
- )
- })}
- >
- )
- }
-
- warnDeprecatedComponent(schema)
-
- const props = resolveProps(schema.props || {})
- const events = resolveEvents(schema.events)
- const combinedProps = { ...props, ...events }
-
- if (schema.binding) {
- const iconName = resolveBinding(schema.binding)
- if (typeof iconName === 'string' && schema.type === 'Icon') {
- const IconComponent = getComponent(iconName)
- if (IconComponent) {
- return createElement(IconComponent, combinedProps)
- }
- return getIcon(iconName, combinedProps)
- }
- }
-
- const Component = getComponent(schema.type)
-
- if (!Component) {
- console.warn(`Component type "${schema.type}" not found in registry`)
- return (
-
-
- Unknown component: {schema.type}
-
-
- )
- }
-
- const children = schema.children?.map((child, index) => (
-
- ))
-
- return {children}
-}
-
-interface PageRendererProps {
- schema: {
- id: string
- title?: string
- description?: string
- layout: Layout
- components: ComponentSchema[]
- }
- data: Record
- functions?: Record any>
- componentRegistry?: Record>
-}
-
-export function PageRenderer({ schema, data, functions = {}, componentRegistry }: PageRendererProps) {
- return (
-
- {schema.components.map((component) => (
-
- ))}
-
- )
-}
diff --git a/src/schemas/ui-schema.ts b/src/schemas/ui-schema.ts
deleted file mode 100644
index 490bbe4..0000000
--- a/src/schemas/ui-schema.ts
+++ /dev/null
@@ -1,57 +0,0 @@
-import { z } from 'zod'
-
-export const BindingSchema = z.object({
- type: z.literal('binding'),
- expression: z.string(),
- fallback: z.any().optional(),
-})
-
-export const ComponentSchema: z.ZodType = z.lazy(() =>
- z.object({
- id: z.string(),
- type: z.string(),
- props: z.record(z.string(), z.any()).optional(),
- children: z.array(ComponentSchema).optional(),
- binding: z.string().optional(),
- condition: z.string().optional(),
- repeat: z
- .object({
- items: z.string(),
- itemVar: z.string(),
- indexVar: z.string().optional(),
- })
- .optional(),
- events: z.record(z.string(), z.string()).optional(),
- })
-)
-
-export const LayoutSchema = z.object({
- type: z.enum(['flex', 'grid', 'stack', 'custom']),
- direction: z.enum(['row', 'column']).optional(),
- gap: z.string().optional(),
- columns: z
- .object({
- base: z.number().optional(),
- sm: z.number().optional(),
- md: z.number().optional(),
- lg: z.number().optional(),
- xl: z.number().optional(),
- })
- .optional(),
- className: z.string().optional(),
-})
-
-export const PageSchema = z.object({
- id: z.string(),
- title: z.string(),
- description: z.string().optional(),
- layout: LayoutSchema,
- components: z.array(ComponentSchema),
- dataBindings: z.array(z.string()).optional(),
- functions: z.record(z.string(), z.string()).optional(),
-})
-
-export type Binding = z.infer
-export type Component = z.infer
-export type Layout = z.infer
-export type PageSchemaType = z.infer