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:35:17 +00:00
committed by GitHub
parent 55114937a7
commit 9fde2a100c
9 changed files with 689 additions and 0 deletions

View File

@@ -0,0 +1,33 @@
import { ReactNode } from 'react'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { IconRenderer } from './IconRenderer'
interface DataCardProps {
title: string
description?: string
icon?: string
gradient?: string
children: ReactNode
className?: string
}
export function DataCard({ title, description, icon, gradient, children, className }: DataCardProps) {
return (
<Card className={`${gradient ? `bg-gradient-to-br ${gradient} border-primary/20` : ''} ${className || ''}`}>
<CardHeader>
<CardTitle className="flex items-center gap-2">
{icon && (
<span className="text-primary">
<IconRenderer name={icon} />
</span>
)}
{title}
</CardTitle>
{description && <CardDescription>{description}</CardDescription>}
</CardHeader>
<CardContent>
{children}
</CardContent>
</Card>
)
}

View File

@@ -0,0 +1,21 @@
import { ReactNode } from 'react'
import { ComponentSchema } from '@/types/json-ui'
import * as Icons from '@phosphor-icons/react'
import { cn } from '@/lib/utils'
interface IconRendererProps {
name: string
size?: number
weight?: 'thin' | 'light' | 'regular' | 'bold' | 'fill' | 'duotone'
className?: string
}
export function IconRenderer({ name, size = 24, weight = 'duotone', className }: IconRendererProps) {
const IconComponent = (Icons as any)[name]
if (!IconComponent) {
return null
}
return <IconComponent size={size} weight={weight} className={className} />
}

View File

@@ -0,0 +1,2 @@
export { IconRenderer } from './IconRenderer'
export { DataCard } from './DataCard'

View File

@@ -0,0 +1,2 @@
export { useJSONRenderer } from './use-json-renderer'
export { useDataSources } from './use-data-sources'

View File

@@ -0,0 +1,73 @@
import { useState, useEffect, useMemo, useCallback } from 'react'
import { DataSource } from '@/types/json-ui'
export function useDataSources(dataSources: DataSource[]) {
const [data, setData] = useState<Record<string, any>>({})
const [loading, setLoading] = useState(true)
const staticSources = useMemo(
() => dataSources.filter((ds) => ds.type === 'static'),
[dataSources]
)
const computedSources = useMemo(
() => dataSources.filter((ds) => ds.type === 'computed'),
[dataSources]
)
useEffect(() => {
const loadData = async () => {
const initialData: Record<string, any> = {}
for (const ds of dataSources) {
if (ds.type === 'kv' && ds.key) {
try {
const value = await spark.kv.get(ds.key)
initialData[ds.id] = value !== undefined ? value : ds.defaultValue
} catch {
initialData[ds.id] = ds.defaultValue
}
} else if (ds.type === 'static') {
initialData[ds.id] = ds.defaultValue
}
}
setData(initialData)
setLoading(false)
}
loadData()
}, [dataSources])
const updateDataSource = useCallback(async (id: string, value: any) => {
setData((prev) => ({ ...prev, [id]: value }))
const kvSource = dataSources.find((ds) => ds.id === id && ds.type === 'kv')
if (kvSource && kvSource.key) {
await spark.kv.set(kvSource.key, value)
}
}, [dataSources])
const computedData = useMemo(() => {
const result: Record<string, any> = {}
computedSources.forEach((ds) => {
if (ds.compute && typeof ds.compute === 'function') {
result[ds.id] = ds.compute(data)
}
})
return result
}, [computedSources, data])
const allData = useMemo(
() => ({ ...data, ...computedData }),
[data, computedData]
)
return {
data: allData,
loading,
updateDataSource,
}
}

View File

@@ -0,0 +1,45 @@
import { useMemo } from 'react'
import { ComponentSchema } from '@/types/json-ui'
export function useJSONRenderer() {
const resolveBinding = useMemo(() => {
return (binding: string, data: Record<string, any>): any => {
if (!binding) return undefined
try {
const func = new Function(...Object.keys(data), `return ${binding}`)
return func(...Object.values(data))
} catch {
return binding
}
}
}, [])
const resolveValue = useMemo(() => {
return (value: any, data: Record<string, any>): any => {
if (typeof value === 'string' && value.startsWith('{{') && value.endsWith('}}')) {
const binding = value.slice(2, -2).trim()
return resolveBinding(binding, data)
}
return value
}
}, [resolveBinding])
const resolveProps = useMemo(() => {
return (props: Record<string, any>, data: Record<string, any>): Record<string, any> => {
const resolved: Record<string, any> = {}
for (const [key, value] of Object.entries(props)) {
resolved[key] = resolveValue(value, data)
}
return resolved
}
}, [resolveValue])
return {
resolveBinding,
resolveValue,
resolveProps,
}
}