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
+19
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 = {
@@ -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>
)
}
@@ -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>
)
}
@@ -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>
)
}
@@ -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>
)
}
+4
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'
+73
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
}