Merge pull request #127 from johndoe6345789/codex/create-wrapper-components-for-skipped-items

Move JSON wrapper interfaces to shared file
This commit is contained in:
2026-01-18 12:18:45 +00:00
committed by GitHub
13 changed files with 624 additions and 6 deletions

View File

@@ -1236,7 +1236,7 @@
"name": "LazyBarChart",
"category": "data",
"canHaveChildren": true,
"description": "LazyBarChart component",
"description": "JSON wrapper for a props-driven bar chart (no lazy hooks)",
"status": "json-compatible",
"source": "molecules",
"jsonCompatible": true
@@ -1246,7 +1246,7 @@
"name": "LazyD3BarChart",
"category": "data",
"canHaveChildren": true,
"description": "LazyD3BarChart component",
"description": "JSON wrapper for a simple SVG bar chart (no D3 hooks)",
"status": "json-compatible",
"source": "molecules",
"jsonCompatible": true
@@ -1256,7 +1256,7 @@
"name": "LazyLineChart",
"category": "data",
"canHaveChildren": true,
"description": "LazyLineChart component",
"description": "JSON wrapper for a props-driven line chart (no lazy hooks)",
"status": "json-compatible",
"source": "molecules",
"jsonCompatible": true
@@ -1302,7 +1302,7 @@
"name": "SeedDataManager",
"category": "data",
"canHaveChildren": true,
"description": "SeedDataManager component",
"description": "JSON wrapper for seed data management with props-driven state",
"status": "json-compatible",
"source": "molecules",
"jsonCompatible": true
@@ -1809,7 +1809,7 @@
"name": "SaveIndicator",
"category": "custom",
"canHaveChildren": true,
"description": "SaveIndicator component",
"description": "JSON wrapper for save status indicator with pure-props API",
"status": "json-compatible",
"source": "molecules",
"jsonCompatible": true
@@ -1979,7 +1979,7 @@
"name": "StorageSettings",
"category": "custom",
"canHaveChildren": true,
"description": "StorageSettings component",
"description": "JSON wrapper for storage settings controls with props-driven state",
"status": "json-compatible",
"source": "molecules",
"jsonCompatible": true

View File

@@ -375,6 +375,13 @@
}
]
},
{
"type": "SaveIndicator",
"label": "Save Indicator",
"category": "feedback",
"icon": "FloppyDisk",
"defaultProps": { "status": "saved", "label": "Saved", "showLabel": true, "animate": true }
},
{
"type": "List",
"label": "List",
@@ -472,6 +479,66 @@
]
}
},
{
"type": "LazyBarChart",
"label": "Bar Chart",
"category": "data",
"icon": "ChartBar",
"defaultProps": {
"data": [
{ "name": "Q1", "value": 400 },
{ "name": "Q2", "value": 300 },
{ "name": "Q3", "value": 500 },
{ "name": "Q4", "value": 280 }
],
"xKey": "name",
"yKey": "value",
"height": 300,
"color": "#8884d8"
}
},
{
"type": "LazyLineChart",
"label": "Line Chart",
"category": "data",
"icon": "ChartLine",
"defaultProps": {
"data": [
{ "name": "Jan", "value": 120 },
{ "name": "Feb", "value": 260 },
{ "name": "Mar", "value": 180 },
{ "name": "Apr", "value": 320 }
],
"xKey": "name",
"yKey": "value",
"height": 300,
"color": "#38bdf8"
}
},
{
"type": "LazyD3BarChart",
"label": "Simple Bar Chart",
"category": "data",
"icon": "ChartBar",
"defaultProps": {
"data": [
{ "label": "Mon", "value": 12 },
{ "label": "Tue", "value": 18 },
{ "label": "Wed", "value": 9 },
{ "label": "Thu", "value": 22 }
],
"width": 600,
"height": 300,
"color": "#22c55e"
}
},
{
"type": "SeedDataManager",
"label": "Seed Data Manager",
"category": "data",
"icon": "Database",
"defaultProps": { "isLoaded": false, "isLoading": false }
},
{
"type": "DataCard",
"label": "Data Card",
@@ -493,5 +560,19 @@
"icon": "Toolbox",
"canHaveChildren": true,
"defaultProps": { "actions": [] }
},
{
"type": "StorageSettings",
"label": "Storage Settings",
"category": "custom",
"icon": "Database",
"defaultProps": {
"backend": "indexeddb",
"flaskUrl": "http://localhost:5000",
"isLoading": false,
"isSwitching": false,
"isExporting": false,
"isImporting": false
}
}
]

View File

@@ -19,6 +19,15 @@ import { Progress } from '@/components/ui/progress'
import { Avatar as ShadcnAvatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
import * as AtomComponents from '@/components/atoms'
import * as MoleculeComponents from '@/components/molecules'
import {
BreadcrumbWrapper,
LazyBarChartWrapper,
LazyD3BarChartWrapper,
LazyLineChartWrapper,
SaveIndicatorWrapper,
SeedDataManagerWrapper,
StorageSettingsWrapper,
} from '@/lib/json-ui/wrappers'
import jsonComponentsRegistry from '../../../json-components-registry.json'
import {
ArrowLeft, ArrowRight, Check, X, Plus, Minus, MagnifyingGlass,
@@ -157,6 +166,16 @@ export const moleculeComponents: UIComponentRegistry = buildRegistryFromNames(
MoleculeComponents as Record<string, ComponentType<any>>
)
export const jsonWrapperComponents: UIComponentRegistry = {
Breadcrumb: BreadcrumbWrapper,
SaveIndicator: SaveIndicatorWrapper,
LazyBarChart: LazyBarChartWrapper,
LazyLineChart: LazyLineChartWrapper,
LazyD3BarChart: LazyD3BarChartWrapper,
SeedDataManager: SeedDataManagerWrapper,
StorageSettings: StorageSettingsWrapper,
}
export const iconComponents: UIComponentRegistry = {
ArrowLeft,
ArrowRight,
@@ -203,6 +222,7 @@ export const uiComponentRegistry: UIComponentRegistry = {
...shadcnComponents,
...atomComponents,
...moleculeComponents,
...jsonWrapperComponents,
...iconComponents,
}

View File

@@ -0,0 +1,11 @@
import { BreadcrumbNav } from '@/components/atoms'
import { cn } from '@/lib/utils'
import type { BreadcrumbWrapperProps } from './interfaces'
export function BreadcrumbWrapper({ items = [], className }: BreadcrumbWrapperProps) {
if (items.length === 0) {
return null
}
return <BreadcrumbNav items={items} className={cn('flex items-center', className)} />
}

View File

@@ -0,0 +1,31 @@
import { Bar, BarChart, CartesianGrid, Legend, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts'
import { cn } from '@/lib/utils'
import type { LazyBarChartWrapperProps } from './interfaces'
export function LazyBarChartWrapper({
data,
xKey,
yKey,
width = '100%',
height = 300,
color = '#8884d8',
showGrid = true,
showTooltip = true,
showLegend = true,
className,
}: LazyBarChartWrapperProps) {
return (
<div className={cn('w-full', className)}>
<ResponsiveContainer width={width} height={height}>
<BarChart data={data}>
{showGrid && <CartesianGrid strokeDasharray="3 3" />}
<XAxis dataKey={xKey} />
<YAxis />
{showTooltip && <Tooltip />}
{showLegend && <Legend />}
<Bar dataKey={yKey} fill={color} />
</BarChart>
</ResponsiveContainer>
</div>
)
}

View File

@@ -0,0 +1,55 @@
import { cn } from '@/lib/utils'
import type { LazyD3BarChartWrapperProps } from './interfaces'
export function LazyD3BarChartWrapper({
data,
width = 600,
height = 300,
color = '#8884d8',
className,
}: LazyD3BarChartWrapperProps) {
const margin = { top: 20, right: 20, bottom: 30, left: 40 }
const innerWidth = Math.max(width - margin.left - margin.right, 0)
const innerHeight = Math.max(height - margin.top - margin.bottom, 0)
const maxValue = Math.max(...data.map((item) => item.value), 0)
const barGap = 8
const barCount = data.length
const totalGap = barCount > 1 ? barGap * (barCount - 1) : 0
const barWidth = barCount > 0 ? Math.max((innerWidth - totalGap) / barCount, 0) : 0
return (
<svg width={width} height={height} className={cn('overflow-visible', className)}>
<g transform={`translate(${margin.left},${margin.top})`}>
{data.map((item, index) => {
const barHeight = maxValue ? (item.value / maxValue) * innerHeight : 0
const x = index * (barWidth + barGap)
const y = innerHeight - barHeight
return (
<g key={`${item.label}-${index}`}>
<rect x={x} y={y} width={barWidth} height={barHeight} fill={color} rx={2} />
<text
x={x + barWidth / 2}
y={innerHeight + 16}
textAnchor="middle"
fill="currentColor"
style={{ fontSize: 10 }}
>
{item.label}
</text>
<text
x={x + barWidth / 2}
y={Math.max(y - 6, 0)}
textAnchor="middle"
fill="currentColor"
style={{ fontSize: 10 }}
>
{item.value}
</text>
</g>
)
})}
</g>
</svg>
)
}

View File

@@ -0,0 +1,31 @@
import { CartesianGrid, Legend, Line, LineChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts'
import { cn } from '@/lib/utils'
import type { LazyLineChartWrapperProps } from './interfaces'
export function LazyLineChartWrapper({
data,
xKey,
yKey,
width = '100%',
height = 300,
color = '#8884d8',
showGrid = true,
showTooltip = true,
showLegend = true,
className,
}: LazyLineChartWrapperProps) {
return (
<div className={cn('w-full', className)}>
<ResponsiveContainer width={width} height={height}>
<LineChart data={data}>
{showGrid && <CartesianGrid strokeDasharray="3 3" />}
<XAxis dataKey={xKey} />
<YAxis />
{showTooltip && <Tooltip />}
{showLegend && <Legend />}
<Line type="monotone" dataKey={yKey} stroke={color} />
</LineChart>
</ResponsiveContainer>
</div>
)
}

View File

@@ -0,0 +1,21 @@
import { StatusIcon } from '@/components/atoms'
import { cn } from '@/lib/utils'
import type { SaveIndicatorWrapperProps } from './interfaces'
export function SaveIndicatorWrapper({
status = 'saved',
label,
showLabel = true,
animate,
className,
}: SaveIndicatorWrapperProps) {
const resolvedLabel = label ?? (status === 'saved' ? 'Saved' : 'Synced')
const shouldAnimate = animate ?? status === 'saved'
return (
<div className={cn('flex items-center gap-1.5 text-xs text-muted-foreground', className)}>
<StatusIcon type={status} animate={shouldAnimate} />
{showLabel && <span className="hidden sm:inline">{resolvedLabel}</span>}
</div>
)
}

View File

@@ -0,0 +1,122 @@
import { Alert, AlertDescription } from '@/components/ui/alert'
import { Button } from '@/components/ui/button'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { CheckCircle, CircleNotch, Database, Trash, ArrowClockwise } from '@phosphor-icons/react'
import type { SeedDataManagerWrapperProps } from './interfaces'
export function SeedDataManagerWrapper({
isLoaded = false,
isLoading = false,
title = 'Seed Data Management',
description = 'Load, reset, or clear application seed data from the database',
loadLabel = 'Load Seed Data',
loadingLabel = 'Loading...',
resetLabel = 'Reset to Defaults',
resettingLabel = 'Resetting...',
clearLabel = 'Clear All Data',
clearingLabel = 'Clearing...',
onLoadSeedData,
onResetSeedData,
onClearAllData,
helperText = {
load: 'Populates database with initial data if not already loaded',
reset: 'Overwrites all data with fresh seed data',
clear: 'Removes all data from the database (destructive action)',
},
}: SeedDataManagerWrapperProps) {
return (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Database size={24} weight="duotone" />
{title}
</CardTitle>
<CardDescription>{description}</CardDescription>
</CardHeader>
<CardContent className="flex flex-col gap-4">
{isLoaded && (
<Alert>
<CheckCircle className="h-4 w-4" weight="fill" />
<AlertDescription>Seed data is loaded and available</AlertDescription>
</Alert>
)}
<div className="flex flex-col gap-3">
<div className="flex gap-2 flex-wrap">
<Button
onClick={onLoadSeedData}
disabled={isLoading || isLoaded}
variant="default"
>
{isLoading ? (
<>
<CircleNotch className="animate-spin" size={16} />
{loadingLabel}
</>
) : (
<>
<Database size={16} weight="fill" />
{loadLabel}
</>
)}
</Button>
<Button
onClick={onResetSeedData}
disabled={isLoading}
variant="outline"
>
{isLoading ? (
<>
<CircleNotch className="animate-spin" size={16} />
{resettingLabel}
</>
) : (
<>
<ArrowClockwise size={16} weight="bold" />
{resetLabel}
</>
)}
</Button>
<Button
onClick={onClearAllData}
disabled={isLoading}
variant="destructive"
>
{isLoading ? (
<>
<CircleNotch className="animate-spin" size={16} />
{clearingLabel}
</>
) : (
<>
<Trash size={16} weight="fill" />
{clearLabel}
</>
)}
</Button>
</div>
<div className="text-sm text-muted-foreground space-y-1">
{helperText.load && (
<p>
<strong>Load Seed Data:</strong> {helperText.load}
</p>
)}
{helperText.reset && (
<p>
<strong>Reset to Defaults:</strong> {helperText.reset}
</p>
)}
{helperText.clear && (
<p>
<strong>Clear All Data:</strong> {helperText.clear}
</p>
)}
</div>
</div>
</CardContent>
</Card>
)
}

View File

@@ -0,0 +1,146 @@
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Cloud, Cpu, Database, Download, HardDrive, Upload } from '@phosphor-icons/react'
import {
getBackendCopy,
storageSettingsCopy,
} from '@/components/storage/storageSettingsConfig'
import type { StorageBackendKey } from '@/components/storage/storageSettingsConfig'
import type { StorageSettingsWrapperProps } from './interfaces'
const getBackendIcon = (backend: StorageBackendKey | null) => {
switch (backend) {
case 'flask':
return <Cpu className="w-5 h-5" />
case 'indexeddb':
return <HardDrive className="w-5 h-5" />
case 'sqlite':
return <Database className="w-5 h-5" />
case 'sparkkv':
return <Cloud className="w-5 h-5" />
default:
return <Database className="w-5 h-5" />
}
}
export function StorageSettingsWrapper({
backend = null,
isLoading = false,
flaskUrl = storageSettingsCopy.molecule.flaskUrlPlaceholder,
isSwitching = false,
onFlaskUrlChange,
onSwitchToFlask,
onSwitchToIndexedDB,
onSwitchToSQLite,
isExporting = false,
isImporting = false,
onExport,
onImport,
}: StorageSettingsWrapperProps) {
const backendCopy = getBackendCopy(backend)
return (
<div className="space-y-6">
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
{getBackendIcon(backend)}
{storageSettingsCopy.molecule.title}
</CardTitle>
<CardDescription>{storageSettingsCopy.molecule.description}</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex items-center gap-2">
<span className="text-sm text-muted-foreground">
{storageSettingsCopy.molecule.currentBackendLabel}
</span>
<Badge variant="secondary" className="flex items-center gap-1">
{getBackendIcon(backend)}
{backendCopy.moleculeLabel}
</Badge>
</div>
<div className="grid gap-4">
<div className="space-y-2">
<Label htmlFor="flask-url">{storageSettingsCopy.molecule.flaskUrlLabel}</Label>
<div className="flex gap-2">
<Input
id="flask-url"
value={flaskUrl}
onChange={(e) => onFlaskUrlChange?.(e.target.value)}
placeholder={storageSettingsCopy.molecule.flaskUrlPlaceholder}
disabled={isSwitching || isLoading}
/>
<Button
onClick={onSwitchToFlask}
disabled={isSwitching || isLoading || backend === 'flask'}
variant={backend === 'flask' ? 'secondary' : 'default'}
>
<Cpu className="w-4 h-4 mr-2" />
{backend === 'flask'
? storageSettingsCopy.molecule.buttons.flaskActive
: storageSettingsCopy.molecule.buttons.flaskUse}
</Button>
</div>
<p className="text-xs text-muted-foreground">{storageSettingsCopy.molecule.flaskHelp}</p>
</div>
<div className="flex gap-2">
<Button
onClick={onSwitchToIndexedDB}
disabled={isSwitching || isLoading || backend === 'indexeddb'}
variant={backend === 'indexeddb' ? 'secondary' : 'outline'}
className="flex-1"
>
<HardDrive className="w-4 h-4 mr-2" />
{backend === 'indexeddb'
? storageSettingsCopy.molecule.buttons.indexeddbActive
: storageSettingsCopy.molecule.buttons.indexeddbUse}
</Button>
<Button
onClick={onSwitchToSQLite}
disabled={isSwitching || isLoading || backend === 'sqlite'}
variant={backend === 'sqlite' ? 'secondary' : 'outline'}
className="flex-1"
>
<Database className="w-4 h-4 mr-2" />
{backend === 'sqlite'
? storageSettingsCopy.molecule.buttons.sqliteActive
: storageSettingsCopy.molecule.buttons.sqliteUse}
</Button>
</div>
<div className="text-xs text-muted-foreground space-y-1">
<p>{storageSettingsCopy.molecule.backendDetails.indexeddb}</p>
<p>{storageSettingsCopy.molecule.backendDetails.sqlite}</p>
<p>{storageSettingsCopy.molecule.backendDetails.flask}</p>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>{storageSettingsCopy.molecule.dataTitle}</CardTitle>
<CardDescription>{storageSettingsCopy.molecule.dataDescription}</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex gap-2">
<Button onClick={onExport} variant="outline" className="flex-1" disabled={isExporting}>
<Download className="w-4 h-4 mr-2" />
{storageSettingsCopy.molecule.buttons.export}
</Button>
<Button onClick={onImport} variant="outline" className="flex-1" disabled={isImporting}>
<Upload className="w-4 h-4 mr-2" />
{storageSettingsCopy.molecule.buttons.import}
</Button>
</div>
<p className="text-xs text-muted-foreground">{storageSettingsCopy.molecule.dataHelp}</p>
</CardContent>
</Card>
</div>
)
}

View File

@@ -0,0 +1,7 @@
export { BreadcrumbWrapper } from './BreadcrumbWrapper'
export { SaveIndicatorWrapper } from './SaveIndicatorWrapper'
export { LazyBarChartWrapper } from './LazyBarChartWrapper'
export { LazyLineChartWrapper } from './LazyLineChartWrapper'
export { LazyD3BarChartWrapper } from './LazyD3BarChartWrapper'
export { SeedDataManagerWrapper } from './SeedDataManagerWrapper'
export { StorageSettingsWrapper } from './StorageSettingsWrapper'

View File

@@ -0,0 +1,91 @@
import type { StorageBackendKey } from '@/components/storage/storageSettingsConfig'
export interface BreadcrumbItem {
label: string
href?: string
}
export interface BreadcrumbWrapperProps {
items?: BreadcrumbItem[]
className?: string
}
export type SaveIndicatorStatus = 'saved' | 'synced'
export interface SaveIndicatorWrapperProps {
status?: SaveIndicatorStatus
label?: string
showLabel?: boolean
animate?: boolean
className?: string
}
export interface LazyBarChartWrapperProps {
data: Array<Record<string, any>>
xKey: string
yKey: string
width?: number | string
height?: number
color?: string
showGrid?: boolean
showTooltip?: boolean
showLegend?: boolean
className?: string
}
export interface LazyLineChartWrapperProps {
data: Array<Record<string, any>>
xKey: string
yKey: string
width?: number | string
height?: number
color?: string
showGrid?: boolean
showTooltip?: boolean
showLegend?: boolean
className?: string
}
export interface LazyD3BarChartWrapperProps {
data: Array<{ label: string; value: number }>
width?: number
height?: number
color?: string
className?: string
}
export interface SeedDataManagerWrapperProps {
isLoaded?: boolean
isLoading?: boolean
title?: string
description?: string
loadLabel?: string
loadingLabel?: string
resetLabel?: string
resettingLabel?: string
clearLabel?: string
clearingLabel?: string
onLoadSeedData?: () => void
onResetSeedData?: () => void
onClearAllData?: () => void
helperText?: {
load?: string
reset?: string
clear?: string
}
}
export interface StorageSettingsWrapperProps {
backend?: StorageBackendKey | null
isLoading?: boolean
flaskUrl?: string
isSwitching?: boolean
onFlaskUrlChange?: (value: string) => void
onSwitchToFlask?: () => void
onSwitchToIndexedDB?: () => void
onSwitchToSQLite?: () => void
isExporting?: boolean
isImporting?: boolean
onExport?: () => void
onImport?: () => void
}

View File

@@ -11,6 +11,8 @@ export type ComponentType =
| 'ErrorBadge' | 'Notification' | 'StatusIcon'
| 'Table' | 'KeyValue' | 'StatCard' | 'DataCard' | 'SearchInput' | 'ActionBar'
| 'DataList' | 'DataTable' | 'MetricCard' | 'Timeline'
| 'LazyBarChart' | 'LazyLineChart' | 'LazyD3BarChart' | 'SeedDataManager'
| 'SaveIndicator' | 'StorageSettings'
| 'AppBranding' | 'LabelWithBadge' | 'EmptyEditorState' | 'LoadingFallback' | 'LoadingState' | 'NavigationGroupHeader'
export type ActionType =