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 11:38:28 +00:00
committed by GitHub
parent 19a44c4010
commit 3ae07db657
28 changed files with 2227 additions and 465 deletions

View File

@@ -1,5 +1,6 @@
import { useCRUD, useSearch, useFilter } from '@/hooks/data'
import { useCRUD, useSearchFilter } from '@/hooks/data'
import { useToggle, useDialog } from '@/hooks/ui'
import { useKV } from '@github/spark/hooks'
import { Button } from '@/components/ui/button'
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card'
import { SearchInput, DataCard, ActionBar } from '@/components/molecules'
@@ -15,17 +16,15 @@ interface Task {
}
export function AtomicComponentDemo() {
const { items: tasks, create, remove } = useCRUD<Task>({
key: 'demo-tasks',
defaultValue: [
{ id: 1, title: 'Build component library', status: 'active', priority: 'high' },
{ id: 2, title: 'Write documentation', status: 'pending', priority: 'medium' },
{ id: 3, title: 'Create examples', status: 'success', priority: 'low' },
],
persist: true,
})
const [tasks, setTasks] = useKV<Task[]>('demo-tasks', [
{ id: 1, title: 'Build component library', status: 'active', priority: 'high' },
{ id: 2, title: 'Write documentation', status: 'pending', priority: 'medium' },
{ id: 3, title: 'Create examples', status: 'success', priority: 'low' },
])
const { query, setQuery, filtered } = useSearch({
const crud = useCRUD<Task>({ items: tasks, setItems: setTasks })
const { searchQuery: query, setSearchQuery: setQuery, filtered } = useSearchFilter({
items: tasks,
searchFields: ['title'],
})

View File

@@ -0,0 +1,205 @@
import { ReactNode } from 'react'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'
import { Progress } from '@/components/ui/progress'
import { StatCard } from '@/components/atoms'
import * as Icons from '@phosphor-icons/react'
import { cn } from '@/lib/utils'
export interface PageComponentConfig {
id: string
type: string
[key: string]: any
}
export interface PageLayoutConfig {
type: string
spacing?: string
sections?: PageSectionConfig[]
[key: string]: any
}
export interface PageSectionConfig {
type: string
[key: string]: any
}
export interface PageSchema {
id: string
layout: PageLayoutConfig
dashboardCards?: any[]
statCards?: any[]
[key: string]: any
}
interface ComponentRendererProps {
schema: PageSchema
data: Record<string, any>
functions?: Record<string, (...args: any[]) => any>
}
function resolveBinding(binding: string, data: Record<string, any>): any {
try {
const func = new Function(...Object.keys(data), `return ${binding}`)
return func(...Object.values(data))
} catch {
return binding
}
}
function getIcon(iconName: string, props?: any) {
const IconComponent = (Icons as any)[iconName]
if (!IconComponent) return null
return <IconComponent size={24} weight="duotone" {...props} />
}
export function JSONPageRenderer({ schema, data, functions = {} }: ComponentRendererProps) {
const renderSection = (section: PageSectionConfig, index: number): ReactNode => {
switch (section.type) {
case 'header':
return (
<div key={index}>
<h1 className="text-3xl font-bold mb-2">{section.title}</h1>
{section.description && (
<p className="text-muted-foreground">{section.description}</p>
)}
</div>
)
case 'cards':
const cards = schema[section.items as string] || []
return (
<div key={index} className={cn('space-y-' + (section.spacing || '4'))}>
{cards.map((card: any) => renderCard(card))}
</div>
)
case 'grid':
const gridItems = schema[section.items as string] || []
const { sm = 1, md = 2, lg = 3 } = section.columns || {}
return (
<div
key={index}
className={cn(
'grid gap-' + (section.gap || '4'),
`grid-cols-${sm} md:grid-cols-${md} lg:grid-cols-${lg}`
)}
>
{gridItems.map((item: any) => renderStatCard(item))}
</div>
)
default:
return null
}
}
const renderCard = (card: any): ReactNode => {
const icon = card.icon ? getIcon(card.icon) : null
if (card.type === 'gradient-card') {
const computeFn = functions[card.dataSource?.compute]
const computedData = computeFn ? computeFn(data) : {}
return (
<Card
key={card.id}
className={cn(
'bg-gradient-to-br border-primary/20',
card.gradient
)}
>
<CardHeader>
<CardTitle className="flex items-center gap-2">
{icon && <span className="text-primary">{icon}</span>}
{card.title}
</CardTitle>
<CardDescription>{card.description}</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
{card.components?.map((comp: any, idx: number) =>
renderSubComponent(comp, computedData, idx)
)}
</CardContent>
</Card>
)
}
return (
<Card key={card.id}>
<CardHeader>
<CardTitle>{card.title}</CardTitle>
</CardHeader>
<CardContent>
{card.component && renderCustomComponent(card.component, card.props || {})}
</CardContent>
</Card>
)
}
const renderSubComponent = (comp: any, dataContext: any, key: number): ReactNode => {
const value = dataContext[comp.binding]
switch (comp.type) {
case 'metric':
return (
<div key={key} className="flex items-center justify-between">
<span className={cn(
'font-bold',
comp.size === 'large' ? 'text-4xl' : 'text-2xl'
)}>
{comp.format === 'percentage' ? `${value}%` : value}
</span>
</div>
)
case 'badge':
const variant = value === 'ready' ? comp.variants?.ready : comp.variants?.inProgress
return (
<Badge key={key} variant={variant?.variant as any}>
{variant?.label}
</Badge>
)
case 'progress':
return <Progress key={key} value={value} className="h-3" />
case 'text':
return (
<p key={key} className={comp.className}>
{value}
</p>
)
default:
return null
}
}
const renderStatCard = (stat: any): ReactNode => {
const icon = stat.icon ? getIcon(stat.icon) : undefined
const value = resolveBinding(stat.dataBinding, data)
const description = `${value} ${stat.description}`
return (
<StatCard
key={stat.id}
icon={icon}
title={stat.title}
value={value}
description={description}
color={stat.color}
/>
)
}
const renderCustomComponent = (componentName: string, props: any): ReactNode => {
return <div>Custom component: {componentName}</div>
}
return (
<div className="h-full overflow-auto p-6 space-y-6">
{schema.layout.sections?.map((section, index) => renderSection(section, index))}
</div>
)
}

View File

@@ -0,0 +1,55 @@
import { JSONPageRenderer } from '@/components/JSONPageRenderer'
import dashboardSchema from '@/config/pages/dashboard.json'
import { ProjectFile, PrismaModel, ComponentNode, ThemeConfig, PlaywrightTest, StorybookStory, UnitTest, FlaskConfig, NextJsConfig } from '@/types/project'
interface ProjectDashboardProps {
files: ProjectFile[]
models: PrismaModel[]
components: ComponentNode[]
theme: ThemeConfig
playwrightTests: PlaywrightTest[]
storybookStories: StorybookStory[]
unitTests: UnitTest[]
flaskConfig: FlaskConfig
nextjsConfig: NextJsConfig
}
function calculateCompletionScore(data: any) {
const { files = [], models = [], components = [], playwrightTests = [], storybookStories = [], unitTests = [] } = data
const totalFiles = files.length
const totalModels = models.length
const totalComponents = components.length
const totalTests = playwrightTests.length + storybookStories.length + unitTests.length
let score = 0
if (totalFiles > 0) score += 30
if (totalModels > 0) score += 20
if (totalComponents > 0) score += 20
if (totalTests > 0) score += 30
const completionScore = Math.min(score, 100)
return {
completionScore,
completionStatus: completionScore >= 70 ? 'ready' : 'inProgress',
completionMessage: getCompletionMessage(completionScore)
}
}
function getCompletionMessage(score: number): string {
if (score >= 90) return 'Excellent! Your project is production-ready.'
if (score >= 70) return 'Great progress! Consider adding more tests.'
if (score >= 50) return 'Good start! Keep building features.'
return 'Just getting started. Add some components and models.'
}
export function ProjectDashboard(props: ProjectDashboardProps) {
return (
<JSONPageRenderer
schema={dashboardSchema as any}
data={props}
functions={{ calculateCompletionScore }}
/>
)
}

View File

@@ -1,36 +1,51 @@
import { ReactNode } from 'react'
import { Button } from '@/components/ui/button'
import { forwardRef } from 'react'
import { cn } from '@/lib/utils'
import { Tooltip, TooltipContent, TooltipTrigger, TooltipProvider } from '@/components/ui/tooltip'
interface ActionButtonProps {
icon?: React.ReactNode
label?: string
variant?: 'default' | 'secondary' | 'outline' | 'ghost' | 'destructive'
export interface ActionButtonProps {
icon?: ReactNode
label: string
onClick: () => void
variant?: 'default' | 'outline' | 'ghost' | 'destructive'
size?: 'default' | 'sm' | 'lg' | 'icon'
onClick?: () => void
tooltip?: string
disabled?: boolean
loading?: boolean
className?: string
}
export const ActionButton = forwardRef<HTMLButtonElement, ActionButtonProps>(
({ icon, label, variant = 'default', size = 'default', onClick, disabled, loading, className }, ref) => {
export function ActionButton({
icon,
label,
onClick,
variant = 'default',
size = 'default',
tooltip,
disabled,
className,
}: ActionButtonProps) {
const button = (
<Button
variant={variant}
size={size}
onClick={onClick}
disabled={disabled}
className={className}
>
{icon && <span className="mr-2">{icon}</span>}
{label}
</Button>
)
if (tooltip) {
return (
<Button
ref={ref}
variant={variant}
size={size}
onClick={onClick}
disabled={disabled || loading}
className={cn(className)}
>
{loading ? (
<div className="w-4 h-4 border-2 border-current border-t-transparent rounded-full animate-spin" />
) : icon}
{label && <span className={icon ? 'ml-2' : ''}>{label}</span>}
</Button>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>{button}</TooltipTrigger>
<TooltipContent>{tooltip}</TooltipContent>
</Tooltip>
</TooltipProvider>
)
}
)
ActionButton.displayName = 'ActionButton'
return button
}

View File

@@ -1,23 +1,24 @@
import { ReactNode } from 'react'
import { cn } from '@/lib/utils'
interface DataListProps<T> {
items: T[]
renderItem: (item: T, index: number) => React.ReactNode
export interface DataListProps {
items: any[]
renderItem: (item: any, index: number) => ReactNode
emptyMessage?: string
className?: string
itemClassName?: string
}
export function DataList<T>({
items,
renderItem,
emptyMessage = 'No items to display',
export function DataList({
items,
renderItem,
emptyMessage = 'No items',
className,
itemClassName
}: DataListProps<T>) {
itemClassName,
}: DataListProps) {
if (items.length === 0) {
return (
<div className="text-center py-12 text-muted-foreground">
<div className="text-center py-8 text-muted-foreground">
{emptyMessage}
</div>
)
@@ -26,7 +27,7 @@ export function DataList<T>({
return (
<div className={cn('space-y-2', className)}>
{items.map((item, index) => (
<div key={index} className={itemClassName}>
<div key={index} className={cn('transition-colors', itemClassName)}>
{renderItem(item, index)}
</div>
))}

View File

@@ -0,0 +1,48 @@
import { ReactNode } from 'react'
import { cn } from '@/lib/utils'
import { Button } from '@/components/ui/button'
export interface EmptyStateProps {
icon?: ReactNode
title: string
description?: string
action?: {
label: string
onClick: () => void
}
className?: string
}
export function EmptyState({
icon,
title,
description,
action,
className,
}: EmptyStateProps) {
return (
<div className={cn(
'flex flex-col items-center justify-center gap-4 py-12 px-6 text-center',
className
)}>
{icon && (
<div className="text-muted-foreground text-4xl opacity-50">
{icon}
</div>
)}
<div className="space-y-2">
<h3 className="text-lg font-semibold">{title}</h3>
{description && (
<p className="text-sm text-muted-foreground max-w-md">
{description}
</p>
)}
</div>
{action && (
<Button onClick={action.onClick} className="mt-2">
{action.label}
</Button>
)}
</div>
)
}

View File

@@ -0,0 +1,31 @@
import { cn } from '@/lib/utils'
export interface LoadingStateProps {
message?: string
size?: 'sm' | 'md' | 'lg'
className?: string
}
export function LoadingState({
message = 'Loading...',
size = 'md',
className
}: LoadingStateProps) {
const sizeClasses = {
sm: 'w-4 h-4 border-2',
md: 'w-8 h-8 border-3',
lg: 'w-12 h-12 border-4',
}
return (
<div className={cn('flex flex-col items-center justify-center gap-3 py-8', className)}>
<div className={cn(
'border-primary border-t-transparent rounded-full animate-spin',
sizeClasses[size]
)} />
{message && (
<p className="text-sm text-muted-foreground">{message}</p>
)}
</div>
)
}

View File

@@ -1,37 +1,50 @@
import { ReactNode } from 'react'
import { Card, CardContent } from '@/components/ui/card'
import { cn } from '@/lib/utils'
interface StatCardProps {
label: string
export interface StatCardProps {
icon?: ReactNode
title: string
value: string | number
icon?: React.ReactNode
description?: string
color?: string
trend?: {
value: number
positive: boolean
direction: 'up' | 'down'
}
className?: string
}
export function StatCard({ label, value, icon, trend, className }: StatCardProps) {
export function StatCard({
icon,
title,
value,
description,
color = 'text-primary',
trend,
className,
}: StatCardProps) {
return (
<Card className={cn('bg-card/50 backdrop-blur', className)}>
<CardContent className="pt-6">
<div className="flex items-start justify-between">
<div className="space-y-1">
<p className="text-sm text-muted-foreground">{label}</p>
<p className="text-3xl font-bold">{value}</p>
<Card className={cn('transition-all hover:shadow-lg', className)}>
<CardContent className="p-6">
<div className="flex items-start justify-between gap-4">
<div className="flex-1">
<p className="text-sm font-medium text-muted-foreground">{title}</p>
<p className="text-3xl font-bold mt-2">{value}</p>
{description && (
<p className="text-sm text-muted-foreground mt-1">{description}</p>
)}
{trend && (
<p className={cn(
'text-xs flex items-center gap-1',
trend.positive ? 'text-green-600' : 'text-red-600'
<div className={cn(
'text-sm font-medium mt-2',
trend.direction === 'up' ? 'text-green-600' : 'text-red-600'
)}>
<span>{trend.positive ? '↑' : '↓'}</span>
<span>{Math.abs(trend.value)}%</span>
</p>
{trend.direction === 'up' ? '↑' : '↓'} {Math.abs(trend.value)}%
</div>
)}
</div>
{icon && (
<div className="text-muted-foreground">
<div className={cn('text-2xl', color)}>
{icon}
</div>
)}

View File

@@ -19,4 +19,8 @@ export { List } from './List'
export { Grid } from './Grid'
export { DataSourceBadge } from './DataSourceBadge'
export { BindingIndicator } from './BindingIndicator'
export { StatCard } from './StatCard'
export { LoadingState } from './LoadingState'
export { EmptyState } from './EmptyState'