Add JSON wrapper components for hook-based UI

This commit is contained in:
2026-01-18 12:38:39 +00:00
parent bd9482b6d4
commit c345e892f9
8 changed files with 562 additions and 20 deletions

View File

@@ -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-18T12:05:00.000Z",
"lastUpdated": "2026-01-18T12:29:35.000Z",
"categories": {
"layout": "Layout and container components",
"input": "Form inputs and interactive controls",
@@ -74,9 +74,22 @@
"category": "layout",
"canHaveChildren": true,
"description": "ComponentBindingDialog component",
"status": "maybe-json-compatible",
"status": "supported",
"source": "molecules",
"jsonCompatible": true
"jsonCompatible": false,
"wrapperRequired": true,
"wrapperComponent": "ComponentBindingDialogWrapper"
},
{
"type": "ComponentBindingDialogWrapper",
"name": "ComponentBindingDialogWrapper",
"category": "layout",
"canHaveChildren": true,
"description": "JSON wrapper for component binding dialog with props-driven bindings",
"status": "json-compatible",
"source": "wrappers",
"jsonCompatible": true,
"wrapperFor": "ComponentBindingDialog"
},
{
"type": "Container",
@@ -103,9 +116,22 @@
"category": "layout",
"canHaveChildren": true,
"description": "DataSourceEditorDialog component",
"status": "maybe-json-compatible",
"status": "supported",
"source": "molecules",
"jsonCompatible": true
"jsonCompatible": false,
"wrapperRequired": true,
"wrapperComponent": "DataSourceEditorDialogWrapper"
},
{
"type": "DataSourceEditorDialogWrapper",
"name": "DataSourceEditorDialogWrapper",
"category": "layout",
"canHaveChildren": true,
"description": "JSON wrapper for data source editor dialog with props-driven fields",
"status": "json-compatible",
"source": "wrappers",
"jsonCompatible": true,
"wrapperFor": "DataSourceEditorDialog"
},
{
"type": "Dialog",
@@ -1080,7 +1106,20 @@
"description": "GitHubBuildStatus component",
"status": "supported",
"source": "molecules",
"jsonCompatible": false
"jsonCompatible": false,
"wrapperRequired": true,
"wrapperComponent": "GitHubBuildStatusWrapper"
},
{
"type": "GitHubBuildStatusWrapper",
"name": "GitHubBuildStatusWrapper",
"category": "feedback",
"canHaveChildren": false,
"description": "JSON wrapper for props-driven GitHub build status summary",
"status": "json-compatible",
"source": "wrappers",
"jsonCompatible": true,
"wrapperFor": "GitHubBuildStatus"
},
{
"type": "InfoBox",
@@ -1236,30 +1275,69 @@
"name": "LazyBarChart",
"category": "data",
"canHaveChildren": true,
"description": "Lazy-loaded Recharts bar chart with runtime library loading",
"status": "supported",
"source": "molecules",
"jsonCompatible": false,
"wrapperRequired": true,
"wrapperComponent": "LazyBarChartWrapper"
},
{
"type": "LazyBarChartWrapper",
"name": "LazyBarChartWrapper",
"category": "data",
"canHaveChildren": true,
"description": "JSON wrapper for a props-driven bar chart (no lazy hooks)",
"status": "json-compatible",
"source": "molecules",
"jsonCompatible": true
"source": "wrappers",
"jsonCompatible": true,
"wrapperFor": "LazyBarChart"
},
{
"type": "LazyD3BarChart",
"name": "LazyD3BarChart",
"category": "data",
"canHaveChildren": true,
"description": "Lazy-loaded D3 bar chart with runtime library loading",
"status": "supported",
"source": "molecules",
"jsonCompatible": false,
"wrapperRequired": true,
"wrapperComponent": "LazyD3BarChartWrapper"
},
{
"type": "LazyD3BarChartWrapper",
"name": "LazyD3BarChartWrapper",
"category": "data",
"canHaveChildren": true,
"description": "JSON wrapper for a simple SVG bar chart (no D3 hooks)",
"status": "json-compatible",
"source": "molecules",
"jsonCompatible": true
"source": "wrappers",
"jsonCompatible": true,
"wrapperFor": "LazyD3BarChart"
},
{
"type": "LazyLineChart",
"name": "LazyLineChart",
"category": "data",
"canHaveChildren": true,
"description": "Lazy-loaded Recharts line chart with runtime library loading",
"status": "supported",
"source": "molecules",
"jsonCompatible": false,
"wrapperRequired": true,
"wrapperComponent": "LazyLineChartWrapper"
},
{
"type": "LazyLineChartWrapper",
"name": "LazyLineChartWrapper",
"category": "data",
"canHaveChildren": true,
"description": "JSON wrapper for a props-driven line chart (no lazy hooks)",
"status": "json-compatible",
"source": "molecules",
"jsonCompatible": true
"source": "wrappers",
"jsonCompatible": true,
"wrapperFor": "LazyLineChart"
},
{
"type": "List",
@@ -1302,10 +1380,23 @@
"name": "SeedDataManager",
"category": "data",
"canHaveChildren": true,
"description": "Seed data management with app-level hook state",
"status": "supported",
"source": "molecules",
"jsonCompatible": false,
"wrapperRequired": true,
"wrapperComponent": "SeedDataManagerWrapper"
},
{
"type": "SeedDataManagerWrapper",
"name": "SeedDataManagerWrapper",
"category": "data",
"canHaveChildren": true,
"description": "JSON wrapper for seed data management with props-driven state",
"status": "json-compatible",
"source": "molecules",
"jsonCompatible": true
"source": "wrappers",
"jsonCompatible": true,
"wrapperFor": "SeedDataManager"
},
{
"type": "StatCard",
@@ -1565,9 +1656,22 @@
"category": "custom",
"canHaveChildren": true,
"description": "ComponentTree component",
"status": "maybe-json-compatible",
"status": "supported",
"source": "molecules",
"jsonCompatible": true
"jsonCompatible": false,
"wrapperRequired": true,
"wrapperComponent": "ComponentTreeWrapper"
},
{
"type": "ComponentTreeWrapper",
"name": "ComponentTreeWrapper",
"category": "custom",
"canHaveChildren": true,
"description": "JSON wrapper for a props-driven component tree view",
"status": "json-compatible",
"source": "wrappers",
"jsonCompatible": true,
"wrapperFor": "ComponentTree"
},
{
"type": "ComponentTreeNode",
@@ -1809,10 +1913,23 @@
"name": "SaveIndicator",
"category": "custom",
"canHaveChildren": true,
"description": "Save status indicator with hook-driven state",
"status": "supported",
"source": "molecules",
"jsonCompatible": false,
"wrapperRequired": true,
"wrapperComponent": "SaveIndicatorWrapper"
},
{
"type": "SaveIndicatorWrapper",
"name": "SaveIndicatorWrapper",
"category": "custom",
"canHaveChildren": true,
"description": "JSON wrapper for save status indicator with pure-props API",
"status": "json-compatible",
"source": "molecules",
"jsonCompatible": true
"source": "wrappers",
"jsonCompatible": true,
"wrapperFor": "SaveIndicator"
},
{
"type": "SchemaEditorCanvas",
@@ -1979,10 +2096,23 @@
"name": "StorageSettings",
"category": "custom",
"canHaveChildren": true,
"description": "Storage settings controls with hook-driven state",
"status": "supported",
"source": "molecules",
"jsonCompatible": false,
"wrapperRequired": true,
"wrapperComponent": "StorageSettingsWrapper"
},
{
"type": "StorageSettingsWrapper",
"name": "StorageSettingsWrapper",
"category": "custom",
"canHaveChildren": true,
"description": "JSON wrapper for storage settings controls with props-driven state",
"status": "json-compatible",
"source": "molecules",
"jsonCompatible": true
"source": "wrappers",
"jsonCompatible": true,
"wrapperFor": "StorageSettings"
},
{
"type": "Timestamp",

View File

@@ -21,6 +21,10 @@ import * as AtomComponents from '@/components/atoms'
import * as MoleculeComponents from '@/components/molecules'
import {
BreadcrumbWrapper,
ComponentBindingDialogWrapper,
ComponentTreeWrapper,
DataSourceEditorDialogWrapper,
GitHubBuildStatusWrapper,
LazyBarChartWrapper,
LazyD3BarChartWrapper,
LazyLineChartWrapper,
@@ -168,12 +172,27 @@ export const moleculeComponents: UIComponentRegistry = buildRegistryFromNames(
export const jsonWrapperComponents: UIComponentRegistry = {
Breadcrumb: BreadcrumbWrapper,
BreadcrumbWrapper: BreadcrumbWrapper,
SaveIndicator: SaveIndicatorWrapper,
SaveIndicatorWrapper: SaveIndicatorWrapper,
LazyBarChart: LazyBarChartWrapper,
LazyBarChartWrapper: LazyBarChartWrapper,
LazyLineChart: LazyLineChartWrapper,
LazyLineChartWrapper: LazyLineChartWrapper,
LazyD3BarChart: LazyD3BarChartWrapper,
LazyD3BarChartWrapper: LazyD3BarChartWrapper,
SeedDataManager: SeedDataManagerWrapper,
SeedDataManagerWrapper: SeedDataManagerWrapper,
StorageSettings: StorageSettingsWrapper,
StorageSettingsWrapper: StorageSettingsWrapper,
GitHubBuildStatus: GitHubBuildStatusWrapper,
GitHubBuildStatusWrapper: GitHubBuildStatusWrapper,
ComponentBindingDialog: ComponentBindingDialogWrapper,
ComponentBindingDialogWrapper: ComponentBindingDialogWrapper,
DataSourceEditorDialog: DataSourceEditorDialogWrapper,
DataSourceEditorDialogWrapper: DataSourceEditorDialogWrapper,
ComponentTree: ComponentTreeWrapper,
ComponentTreeWrapper: ComponentTreeWrapper,
}
export const iconComponents: UIComponentRegistry = {

View File

@@ -0,0 +1,75 @@
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { cn } from '@/lib/utils'
import type { ComponentBindingDialogWrapperProps } from './interfaces'
export function ComponentBindingDialogWrapper({
open = false,
title = 'Component Bindings',
description = 'Connect component props to data sources.',
componentType,
componentId,
bindings = [],
onBindingChange,
onSave,
onCancel,
onOpenChange,
className,
}: ComponentBindingDialogWrapperProps) {
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className={cn('max-w-2xl', className)}>
<DialogHeader>
<DialogTitle>{title}</DialogTitle>
<DialogDescription>{description}</DialogDescription>
</DialogHeader>
{(componentType || componentId) && (
<div className="rounded-md border border-border bg-muted/30 p-3 text-sm">
{componentType && (
<div className="flex items-center gap-2">
<span className="text-muted-foreground">Component:</span>
<span className="font-mono font-medium">{componentType}</span>
</div>
)}
{componentId && (
<div className="flex items-center gap-2">
<span className="text-muted-foreground">ID:</span>
<span className="font-mono text-xs">{componentId}</span>
</div>
)}
</div>
)}
<div className="space-y-4">
{bindings.length === 0 ? (
<p className="text-sm text-muted-foreground">No bindings configured.</p>
) : (
bindings.map((binding) => (
<div key={binding.id} className="space-y-2">
<Label htmlFor={`binding-${binding.id}`}>{binding.label}</Label>
<Input
id={`binding-${binding.id}`}
value={binding.value ?? ''}
placeholder={binding.placeholder}
onChange={(event) => onBindingChange?.(binding.id, event.target.value)}
/>
</div>
))
)}
</div>
<DialogFooter>
<Button variant="outline" onClick={onCancel}>
Cancel
</Button>
<Button onClick={onSave}>
Save
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}

View File

@@ -0,0 +1,55 @@
import { cn } from '@/lib/utils'
import type { UIComponent } from '@/types/json-ui'
import type { ComponentTreeWrapperProps } from './interfaces'
const renderTreeNodes = (
components: UIComponent[],
depth: number,
selectedId: string | null,
onSelect?: (id: string) => void
) => {
return components.map((component) => {
const hasChildren = Array.isArray(component.children) && component.children.length > 0
const isSelected = selectedId === component.id
return (
<div key={component.id}>
<button
type="button"
onClick={() => onSelect?.(component.id)}
className={cn(
'flex w-full items-center gap-2 rounded-md px-2 py-1 text-left text-sm transition-colors',
isSelected ? 'bg-accent/40 text-foreground' : 'hover:bg-muted'
)}
style={{ paddingLeft: `${depth * 16 + 8}px` }}
>
<span className="font-medium">{component.type}</span>
<span className="text-xs text-muted-foreground">{component.id}</span>
</button>
{hasChildren && (
<div className="mt-1">
{renderTreeNodes(component.children as UIComponent[], depth + 1, selectedId, onSelect)}
</div>
)}
</div>
)
})
}
export function ComponentTreeWrapper({
components = [],
selectedId = null,
emptyMessage = 'No components available.',
onSelect,
className,
}: ComponentTreeWrapperProps) {
return (
<div className={cn('space-y-2', className)}>
{components.length === 0 ? (
<p className="text-sm text-muted-foreground">{emptyMessage}</p>
) : (
<div className="space-y-1">{renderTreeNodes(components, 0, selectedId, onSelect)}</div>
)}
</div>
)
}

View File

@@ -0,0 +1,59 @@
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { cn } from '@/lib/utils'
import type { DataSourceEditorDialogWrapperProps } from './interfaces'
export function DataSourceEditorDialogWrapper({
open = false,
title = 'Data Source',
description = 'Update data source details and fields.',
fields = [],
onFieldChange,
onSave,
onCancel,
onOpenChange,
className,
}: DataSourceEditorDialogWrapperProps) {
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className={cn('max-w-2xl', className)}>
<DialogHeader>
<DialogTitle>{title}</DialogTitle>
<DialogDescription>{description}</DialogDescription>
</DialogHeader>
<div className="space-y-4">
{fields.length === 0 ? (
<p className="text-sm text-muted-foreground">No fields configured.</p>
) : (
fields.map((field) => (
<div key={field.id} className="space-y-2">
<Label htmlFor={`field-${field.id}`}>{field.label}</Label>
<Input
id={`field-${field.id}`}
value={field.value ?? ''}
placeholder={field.placeholder}
onChange={(event) => onFieldChange?.(field.id, event.target.value)}
/>
{field.helperText && (
<p className="text-xs text-muted-foreground">{field.helperText}</p>
)}
</div>
))
)}
</div>
<DialogFooter>
<Button variant="outline" onClick={onCancel}>
Cancel
</Button>
<Button onClick={onSave}>
Save
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}

View File

@@ -0,0 +1,127 @@
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { cn } from '@/lib/utils'
import {
ArrowSquareOut,
CheckCircle,
Clock,
WarningCircle,
XCircle,
} from '@phosphor-icons/react'
import type { GitHubBuildStatusWrapperProps, GitHubBuildStatusWorkflowItem } from './interfaces'
const getStatusBadge = (workflow: GitHubBuildStatusWorkflowItem) => {
if (workflow.status === 'completed') {
if (workflow.conclusion === 'success') {
return <Badge className="bg-green-500/10 text-green-600 border-green-500/20">Success</Badge>
}
if (workflow.conclusion === 'failure') {
return <Badge variant="destructive">Failed</Badge>
}
if (workflow.conclusion === 'cancelled') {
return <Badge variant="secondary">Cancelled</Badge>
}
}
return (
<Badge variant="outline" className="border-blue-500/50 text-blue-500">
Running
</Badge>
)
}
const getStatusIcon = (workflow: GitHubBuildStatusWorkflowItem) => {
if (workflow.status === 'completed') {
if (workflow.conclusion === 'success') {
return <CheckCircle size={18} className="text-green-500" weight="fill" />
}
if (workflow.conclusion === 'failure') {
return <XCircle size={18} className="text-red-500" weight="fill" />
}
if (workflow.conclusion === 'cancelled') {
return <WarningCircle size={18} className="text-yellow-500" weight="fill" />
}
}
return <Clock size={18} className="text-blue-500" weight="duotone" />
}
export function GitHubBuildStatusWrapper({
title = 'GitHub Build Status',
description = 'Latest workflow runs and status badges.',
workflows = [],
isLoading = false,
errorMessage,
emptyMessage = 'No workflows to display yet.',
footerLinkLabel = 'View on GitHub',
footerLinkUrl,
className,
}: GitHubBuildStatusWrapperProps) {
return (
<Card className={cn(className)}>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<ArrowSquareOut size={18} weight="duotone" />
{title}
</CardTitle>
<CardDescription>{description}</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
{isLoading && <p className="text-sm text-muted-foreground">Loading workflows</p>}
{!isLoading && errorMessage && (
<div className="flex items-center gap-2 text-sm text-red-500">
<WarningCircle size={16} weight="fill" />
<span>{errorMessage}</span>
</div>
)}
{!isLoading && !errorMessage && workflows.length === 0 && (
<p className="text-sm text-muted-foreground">{emptyMessage}</p>
)}
{!isLoading && !errorMessage && workflows.length > 0 && (
<div className="space-y-3">
{workflows.map((workflow) => (
<div
key={workflow.id}
className="flex items-center justify-between gap-3 rounded-lg border border-border p-3"
>
<div className="flex items-center gap-3 min-w-0">
{getStatusIcon(workflow)}
<div className="min-w-0">
<div className="flex items-center gap-2">
<p className="text-sm font-medium truncate">{workflow.name}</p>
{getStatusBadge(workflow)}
</div>
<div className="text-xs text-muted-foreground truncate">
{[workflow.branch, workflow.updatedAt, workflow.event]
.filter(Boolean)
.join(' • ')}
</div>
</div>
</div>
{workflow.url && (
<Button variant="ghost" size="sm" asChild>
<a href={workflow.url} target="_blank" rel="noopener noreferrer">
<ArrowSquareOut size={14} />
</a>
</Button>
)}
</div>
))}
</div>
)}
{footerLinkUrl && (
<Button variant="outline" size="sm" asChild className="w-full">
<a href={footerLinkUrl} target="_blank" rel="noopener noreferrer">
{footerLinkLabel}
</a>
</Button>
)}
</CardContent>
</Card>
)
}

View File

@@ -5,3 +5,7 @@ export { LazyLineChartWrapper } from './LazyLineChartWrapper'
export { LazyD3BarChartWrapper } from './LazyD3BarChartWrapper'
export { SeedDataManagerWrapper } from './SeedDataManagerWrapper'
export { StorageSettingsWrapper } from './StorageSettingsWrapper'
export { GitHubBuildStatusWrapper } from './GitHubBuildStatusWrapper'
export { ComponentBindingDialogWrapper } from './ComponentBindingDialogWrapper'
export { DataSourceEditorDialogWrapper } from './DataSourceEditorDialogWrapper'
export { ComponentTreeWrapper } from './ComponentTreeWrapper'

View File

@@ -1,4 +1,5 @@
import type { StorageBackendKey } from '@/components/storage/storageSettingsConfig'
import type { UIComponent } from '@/types/json-ui'
export interface BreadcrumbItem {
label: string
@@ -89,3 +90,75 @@ export interface StorageSettingsWrapperProps {
onExport?: () => void
onImport?: () => void
}
export interface GitHubBuildStatusWorkflowItem {
id: string
name: string
status?: string
conclusion?: string | null
branch?: string
updatedAt?: string
event?: string
url?: string
}
export interface GitHubBuildStatusWrapperProps {
title?: string
description?: string
workflows?: GitHubBuildStatusWorkflowItem[]
isLoading?: boolean
errorMessage?: string
emptyMessage?: string
footerLinkLabel?: string
footerLinkUrl?: string
className?: string
}
export interface ComponentBindingField {
id: string
label: string
value?: string
placeholder?: string
}
export interface ComponentBindingDialogWrapperProps {
open?: boolean
title?: string
description?: string
componentType?: string
componentId?: string
bindings?: ComponentBindingField[]
onBindingChange?: (id: string, value: string) => void
onSave?: () => void
onCancel?: () => void
onOpenChange?: (open: boolean) => void
className?: string
}
export interface DataSourceField {
id: string
label: string
value?: string
placeholder?: string
helperText?: string
}
export interface DataSourceEditorDialogWrapperProps {
open?: boolean
title?: string
description?: string
fields?: DataSourceField[]
onFieldChange?: (id: string, value: string) => void
onSave?: () => void
onCancel?: () => void
onOpenChange?: (open: boolean) => void
className?: string
}
export interface ComponentTreeWrapperProps {
components?: UIComponent[]
selectedId?: string | null
emptyMessage?: string
onSelect?: (id: string) => void
className?: string
}