Generated by Spark: Load more of UI from JSON declarations and break up large components into atomic and create hooks as needed

This commit is contained in:
2026-01-17 12:01:34 +00:00
committed by GitHub
parent 165c8d3847
commit 944e164deb
13 changed files with 1035 additions and 25 deletions

147
src/lib/schema-renderer.tsx Normal file
View File

@@ -0,0 +1,147 @@
import { ReactNode } from 'react'
import { cn } from '@/lib/utils'
import { Component as ComponentSchema, Layout } from '@/schemas/ui-schema'
import { useDataBinding, useEventHandlers, useComponentRegistry } from '@/hooks/ui'
interface SchemaRendererProps {
schema: ComponentSchema
data: Record<string, any>
functions?: Record<string, (...args: any[]) => any>
}
interface LayoutRendererProps {
layout: Layout
children: ReactNode
}
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 <div className={getLayoutClasses()}>{children}</div>
}
export function SchemaRenderer({ schema, data, functions = {} }: SchemaRendererProps) {
const { resolveCondition, resolveProps, resolveBinding } = useDataBinding({ data })
const { resolveEvents } = useEventHandlers({ functions })
const { getComponent, getIcon } = useComponentRegistry()
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 (
<SchemaRenderer
key={index}
schema={{ ...schema, repeat: undefined }}
data={itemData}
functions={functions}
/>
)
})}
</>
)
}
const Component = getComponent(schema.type)
if (!Component) {
console.warn(`Component type "${schema.type}" not found in registry`)
return (
<div className="border-2 border-dashed border-destructive p-4 rounded-md">
<p className="text-destructive font-mono text-sm">
Unknown component: {schema.type}
</p>
</div>
)
}
const props = resolveProps(schema.props || {})
const events = resolveEvents(schema.events)
const combinedProps = { ...props, ...events }
if (schema.binding) {
const iconName = resolveBinding(schema.binding)
if (iconName && schema.type === 'Icon') {
return getIcon(iconName, props)
}
}
const children = schema.children?.map((child, index) => (
<SchemaRenderer
key={child.id || index}
schema={child}
data={data}
functions={functions}
/>
))
return <Component {...combinedProps}>{children}</Component>
}
interface PageRendererProps {
schema: {
id: string
title?: string
description?: string
layout: Layout
components: ComponentSchema[]
}
data: Record<string, any>
functions?: Record<string, (...args: any[]) => any>
}
export function PageRenderer({ schema, data, functions = {} }: PageRendererProps) {
return (
<LayoutRenderer layout={schema.layout}>
{schema.components.map((component) => (
<SchemaRenderer
key={component.id}
schema={component}
data={data}
functions={functions}
/>
))}
</LayoutRenderer>
)
}