diff --git a/src/components/json-definitions/component-tree.json b/src/components/json-definitions/component-tree.json
new file mode 100644
index 0000000..d17f8a0
--- /dev/null
+++ b/src/components/json-definitions/component-tree.json
@@ -0,0 +1,65 @@
+{
+ "id": "component-tree-container",
+ "type": "div",
+ "bindings": {
+ "className": {
+ "source": "className",
+ "transform": "data ? `space-y-2 ${data}` : 'space-y-2'"
+ }
+ },
+ "children": [
+ {
+ "id": "empty-message",
+ "type": "p",
+ "props": { "className": "text-sm text-muted-foreground" },
+ "bindings": { "children": "emptyMessage" },
+ "conditional": { "if": "components.length === 0" }
+ },
+ {
+ "id": "tree-nodes",
+ "type": "div",
+ "props": { "className": "space-y-1" },
+ "conditional": { "if": "components.length > 0" },
+ "children": [
+ {
+ "id": "tree-node-list",
+ "type": "list",
+ "bindings": {
+ "items": "treeData",
+ "keyPath": "component.id"
+ },
+ "itemTemplate": {
+ "type": "button",
+ "props": { "type": "button" },
+ "bindings": {
+ "onClick": {
+ "source": "onSelect,item.component.id",
+ "transform": "() => onSelect?.(item.component.id)"
+ },
+ "className": {
+ "source": "item.isSelected",
+ "transform": "item.isSelected ? 'flex w-full items-center gap-2 rounded-md px-2 py-1 text-left text-sm transition-colors bg-accent/40 text-foreground' : 'flex w-full items-center gap-2 rounded-md px-2 py-1 text-left text-sm transition-colors hover:bg-muted'"
+ },
+ "style": {
+ "source": "item.paddingLeft",
+ "transform": "{ paddingLeft: item.paddingLeft }"
+ }
+ },
+ "children": [
+ {
+ "type": "span",
+ "props": { "className": "font-medium" },
+ "bindings": { "children": "item.component.type" }
+ },
+ {
+ "type": "span",
+ "props": { "className": "text-xs text-muted-foreground" },
+ "bindings": { "children": "item.component.id" }
+ }
+ ]
+ }
+ }
+ ]
+ }
+ ]
+}
diff --git a/src/components/json-definitions/seed-data-manager.json b/src/components/json-definitions/seed-data-manager.json
new file mode 100644
index 0000000..004ec3d
--- /dev/null
+++ b/src/components/json-definitions/seed-data-manager.json
@@ -0,0 +1,217 @@
+{
+ "id": "seed-data-manager-card",
+ "type": "Card",
+ "children": [
+ {
+ "id": "card-header",
+ "type": "CardHeader",
+ "children": [
+ {
+ "id": "card-title",
+ "type": "CardTitle",
+ "props": { "className": "flex items-center gap-2" },
+ "children": [
+ {
+ "id": "title-icon",
+ "type": "PhosphorIcon",
+ "props": { "icon": "Database", "size": 24, "weight": "duotone" }
+ },
+ {
+ "id": "title-text",
+ "type": "text",
+ "bindings": { "children": "title" }
+ }
+ ]
+ },
+ {
+ "id": "card-description",
+ "type": "CardDescription",
+ "bindings": { "children": "description" }
+ }
+ ]
+ },
+ {
+ "id": "card-content",
+ "type": "CardContent",
+ "props": { "className": "flex flex-col gap-4" },
+ "children": [
+ {
+ "id": "loaded-alert",
+ "type": "Alert",
+ "conditional": { "if": "isLoaded" },
+ "children": [
+ {
+ "id": "alert-icon",
+ "type": "PhosphorIcon",
+ "props": { "icon": "CheckCircle", "className": "h-4 w-4", "weight": "fill" }
+ },
+ {
+ "id": "alert-description",
+ "type": "AlertDescription",
+ "children": [{ "type": "text", "children": "Seed data is loaded and available" }]
+ }
+ ]
+ },
+ {
+ "id": "buttons-container",
+ "type": "div",
+ "props": { "className": "flex flex-col gap-3" },
+ "children": [
+ {
+ "id": "buttons-row",
+ "type": "div",
+ "props": { "className": "flex gap-2 flex-wrap" },
+ "children": [
+ {
+ "id": "load-button",
+ "type": "Button",
+ "bindings": {
+ "onClick": "onLoadSeedData",
+ "disabled": {
+ "source": "isLoading,isLoaded",
+ "transform": "isLoading || isLoaded"
+ }
+ },
+ "props": { "variant": "default" },
+ "children": [
+ {
+ "id": "load-button-content",
+ "type": "conditional-group",
+ "conditional": { "if": "isLoading" },
+ "children": [
+ {
+ "type": "PhosphorIcon",
+ "props": { "icon": "CircleNotch", "className": "animate-spin", "size": 16 }
+ },
+ {
+ "type": "text",
+ "bindings": { "children": "loadingLabel" }
+ }
+ ],
+ "else": [
+ {
+ "type": "PhosphorIcon",
+ "props": { "icon": "Database", "size": 16, "weight": "fill" }
+ },
+ {
+ "type": "text",
+ "bindings": { "children": "loadLabel" }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "id": "reset-button",
+ "type": "Button",
+ "bindings": {
+ "onClick": "onResetSeedData",
+ "disabled": "isLoading"
+ },
+ "props": { "variant": "outline" },
+ "children": [
+ {
+ "id": "reset-button-content",
+ "type": "conditional-group",
+ "conditional": { "if": "isLoading" },
+ "children": [
+ {
+ "type": "PhosphorIcon",
+ "props": { "icon": "CircleNotch", "className": "animate-spin", "size": 16 }
+ },
+ {
+ "type": "text",
+ "bindings": { "children": "resettingLabel" }
+ }
+ ],
+ "else": [
+ {
+ "type": "PhosphorIcon",
+ "props": { "icon": "ArrowClockwise", "size": 16, "weight": "bold" }
+ },
+ {
+ "type": "text",
+ "bindings": { "children": "resetLabel" }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "id": "clear-button",
+ "type": "Button",
+ "bindings": {
+ "onClick": "onClearAllData",
+ "disabled": "isLoading"
+ },
+ "props": { "variant": "destructive" },
+ "children": [
+ {
+ "id": "clear-button-content",
+ "type": "conditional-group",
+ "conditional": { "if": "isLoading" },
+ "children": [
+ {
+ "type": "PhosphorIcon",
+ "props": { "icon": "CircleNotch", "className": "animate-spin", "size": 16 }
+ },
+ {
+ "type": "text",
+ "bindings": { "children": "clearingLabel" }
+ }
+ ],
+ "else": [
+ {
+ "type": "PhosphorIcon",
+ "props": { "icon": "Trash", "size": 16, "weight": "fill" }
+ },
+ {
+ "type": "text",
+ "bindings": { "children": "clearLabel" }
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "id": "helper-text",
+ "type": "div",
+ "props": { "className": "text-sm text-muted-foreground space-y-1" },
+ "children": [
+ {
+ "id": "load-help",
+ "type": "p",
+ "conditional": { "if": "helperText.load" },
+ "children": [
+ { "type": "strong", "children": "Load Seed Data: " },
+ { "type": "text", "bindings": { "children": "helperText.load" } }
+ ]
+ },
+ {
+ "id": "reset-help",
+ "type": "p",
+ "conditional": { "if": "helperText.reset" },
+ "children": [
+ { "type": "strong", "children": "Reset to Defaults: " },
+ { "type": "text", "bindings": { "children": "helperText.reset" } }
+ ]
+ },
+ {
+ "id": "clear-help",
+ "type": "p",
+ "conditional": { "if": "helperText.clear" },
+ "children": [
+ { "type": "strong", "children": "Clear All Data: " },
+ { "type": "text", "bindings": { "children": "helperText.clear" } }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+}
diff --git a/src/components/molecules/ComponentTreeWrapper.tsx b/src/components/molecules/ComponentTreeWrapper.tsx
deleted file mode 100644
index 36bc508..0000000
--- a/src/components/molecules/ComponentTreeWrapper.tsx
+++ /dev/null
@@ -1,55 +0,0 @@
-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 (
-
-
- {hasChildren && (
-
- {renderTreeNodes(component.children as UIComponent[], depth + 1, selectedId, onSelect)}
-
- )}
-
- )
- })
-}
-
-export function ComponentTreeWrapper({
- components = [],
- selectedId = null,
- emptyMessage = 'No components available.',
- onSelect,
- className,
-}: ComponentTreeWrapperProps) {
- return (
-
- {components.length === 0 ? (
-
{emptyMessage}
- ) : (
-
{renderTreeNodes(components, 0, selectedId, onSelect)}
- )}
-
- )
-}
diff --git a/src/components/molecules/SeedDataManagerWrapper.tsx b/src/components/molecules/SeedDataManagerWrapper.tsx
deleted file mode 100644
index 54e77f7..0000000
--- a/src/components/molecules/SeedDataManagerWrapper.tsx
+++ /dev/null
@@ -1,122 +0,0 @@
-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 (
-
-
-
-
- {title}
-
- {description}
-
-
- {isLoaded && (
-
-
- Seed data is loaded and available
-
- )}
-
-
-
-
-
-
-
-
-
-
-
- {helperText.load && (
-
- Load Seed Data: {helperText.load}
-
- )}
- {helperText.reset && (
-
- Reset to Defaults: {helperText.reset}
-
- )}
- {helperText.clear && (
-
- Clear All Data: {helperText.clear}
-
- )}
-
-
-
-
- )
-}
diff --git a/src/components/molecules/index.ts b/src/components/molecules/index.ts
index 1f8b59c..3939d8d 100644
--- a/src/components/molecules/index.ts
+++ b/src/components/molecules/index.ts
@@ -2,7 +2,6 @@ export { Breadcrumb } from './Breadcrumb'
export { CanvasRenderer } from './CanvasRenderer'
export { CodeExplanationDialog } from './CodeExplanationDialog'
export { ComponentPalette } from './ComponentPalette'
-export { ComponentTree } from './ComponentTree'
export { GitHubBuildStatus } from './GitHubBuildStatus'
export { LazyLineChart } from './LazyLineChart'
export { LazyBarChart } from './LazyBarChart'
@@ -10,8 +9,6 @@ export { LazyD3BarChart } from './LazyD3BarChart'
export { StorageSettings } from './StorageSettings'
export { NavigationGroupHeader } from './NavigationGroupHeader'
export { PropertyEditor } from './PropertyEditor'
-export { SaveIndicator } from './SaveIndicator'
-export { SeedDataManager } from './SeedDataManager'
export { ToolbarButton } from './ToolbarButton'
export { TreeFormDialog } from './TreeFormDialog'
export { SearchInput } from './SearchInput'
@@ -25,6 +22,8 @@ export {
ComponentBindingDialog,
DataSourceEditorDialog,
GitHubBuildStatus as GitHubBuildStatusJSON,
- SaveIndicator as SaveIndicatorJSON
+ SaveIndicator,
+ ComponentTree,
+ SeedDataManager
} from '@/lib/json-ui/json-components'
export { preloadMonacoEditor } from './LazyMonacoEditor'
diff --git a/src/hooks/use-component-tree.ts b/src/hooks/use-component-tree.ts
new file mode 100644
index 0000000..7b2e3ae
--- /dev/null
+++ b/src/hooks/use-component-tree.ts
@@ -0,0 +1,46 @@
+/**
+ * Hook for flattening and rendering component tree structure
+ * Converts recursive component tree to flat list with depth information
+ */
+import type { UIComponent } from '@/types/json-ui'
+
+export interface TreeNode {
+ component: UIComponent
+ depth: number
+ hasChildren: boolean
+ isSelected: boolean
+ paddingLeft: string
+}
+
+export function useComponentTree(
+ components: UIComponent[],
+ selectedId: string | null
+): TreeNode[] {
+ const flattenTree = (
+ items: UIComponent[],
+ depth: number = 0
+ ): TreeNode[] => {
+ const result: TreeNode[] = []
+
+ for (const component of items) {
+ const hasChildren = Array.isArray(component.children) && component.children.length > 0
+ const isSelected = selectedId === component.id
+
+ result.push({
+ component,
+ depth,
+ hasChildren,
+ isSelected,
+ paddingLeft: `${depth * 16 + 8}px`
+ })
+
+ if (hasChildren) {
+ result.push(...flattenTree(component.children as UIComponent[], depth + 1))
+ }
+ }
+
+ return result
+ }
+
+ return flattenTree(components)
+}
diff --git a/src/hooks/use-d3-bar-chart.ts b/src/hooks/use-d3-bar-chart.ts
new file mode 100644
index 0000000..6f7177d
--- /dev/null
+++ b/src/hooks/use-d3-bar-chart.ts
@@ -0,0 +1,72 @@
+/**
+ * Hook for calculating D3 bar chart dimensions and positions
+ */
+export interface BarChartData {
+ label: string
+ value: number
+}
+
+export interface BarPosition {
+ x: number
+ y: number
+ width: number
+ height: number
+ label: string
+ value: number
+ labelX: number
+ labelY: number
+ valueX: number
+ valueY: number
+}
+
+export interface ChartDimensions {
+ innerWidth: number
+ innerHeight: number
+ margin: { top: number; right: number; bottom: number; left: number }
+ translateX: number
+ translateY: number
+ bars: BarPosition[]
+}
+
+export function useD3BarChart(
+ data: BarChartData[],
+ width: number = 600,
+ height: number = 300
+): ChartDimensions {
+ 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
+
+ const bars: BarPosition[] = data.map((item, index) => {
+ const barHeight = maxValue ? (item.value / maxValue) * innerHeight : 0
+ const x = index * (barWidth + barGap)
+ const y = innerHeight - barHeight
+
+ return {
+ x,
+ y,
+ width: barWidth,
+ height: barHeight,
+ label: item.label,
+ value: item.value,
+ labelX: x + barWidth / 2,
+ labelY: innerHeight + 16,
+ valueX: x + barWidth / 2,
+ valueY: Math.max(y - 6, 0)
+ }
+ })
+
+ return {
+ innerWidth,
+ innerHeight,
+ margin,
+ translateX: margin.left,
+ translateY: margin.top,
+ bars
+ }
+}
diff --git a/src/hooks/use-storage-backend-info.ts b/src/hooks/use-storage-backend-info.ts
new file mode 100644
index 0000000..3877684
--- /dev/null
+++ b/src/hooks/use-storage-backend-info.ts
@@ -0,0 +1,30 @@
+/**
+ * Hook for getting storage backend icon and copy
+ */
+import type { StorageBackendKey } from '@/components/storage/storageSettingsConfig'
+import { getBackendCopy } from '@/components/storage/storageSettingsConfig'
+
+export interface BackendInfo {
+ iconName: string
+ iconWeight: string
+ moleculeLabel: string
+}
+
+export function useStorageBackendInfo(backend: StorageBackendKey | null): BackendInfo {
+ const iconMap: Record = {
+ flask: { iconName: 'Cpu', iconWeight: 'regular' },
+ indexeddb: { iconName: 'HardDrive', iconWeight: 'regular' },
+ sqlite: { iconName: 'Database', iconWeight: 'regular' },
+ sparkkv: { iconName: 'Cloud', iconWeight: 'regular' },
+ null: { iconName: 'Database', iconWeight: 'regular' }
+ }
+
+ const icon = iconMap[backend || 'null']
+ const backendCopy = getBackendCopy(backend)
+
+ return {
+ iconName: icon.iconName,
+ iconWeight: icon.iconWeight,
+ moleculeLabel: backendCopy.moleculeLabel
+ }
+}
diff --git a/src/lib/json-ui/hooks-registry.ts b/src/lib/json-ui/hooks-registry.ts
index 22ad6ab..e994459 100644
--- a/src/lib/json-ui/hooks-registry.ts
+++ b/src/lib/json-ui/hooks-registry.ts
@@ -3,6 +3,9 @@
* Allows JSON components to use custom React hooks
*/
import { useSaveIndicator } from '@/hooks/use-save-indicator'
+import { useComponentTree } from '@/hooks/use-component-tree'
+import { useStorageBackendInfo } from '@/hooks/use-storage-backend-info'
+import { useD3BarChart } from '@/hooks/use-d3-bar-chart'
export interface HookRegistry {
[key: string]: (...args: any[]) => any
@@ -13,6 +16,9 @@ export interface HookRegistry {
*/
export const hooksRegistry: HookRegistry = {
useSaveIndicator,
+ useComponentTree,
+ useStorageBackendInfo,
+ useD3BarChart,
// Add more hooks here as needed
}
diff --git a/src/lib/json-ui/json-components.ts b/src/lib/json-ui/json-components.ts
index dcfd782..92d7b12 100644
--- a/src/lib/json-ui/json-components.ts
+++ b/src/lib/json-ui/json-components.ts
@@ -29,6 +29,8 @@ import componentBindingDialogDef from '@/components/json-definitions/component-b
import dataSourceEditorDialogDef from '@/components/json-definitions/data-source-editor-dialog.json'
import githubBuildStatusDef from '@/components/json-definitions/github-build-status.json'
import saveIndicatorDef from '@/components/json-definitions/save-indicator.json'
+import componentTreeDef from '@/components/json-definitions/component-tree.json'
+import seedDataManagerDef from '@/components/json-definitions/seed-data-manager.json'
// Create pure JSON components (no hooks)
export const LoadingFallback = createJsonComponent(loadingFallbackDef)
@@ -37,6 +39,7 @@ export const PageHeaderContent = createJsonComponent(pag
export const ComponentBindingDialog = createJsonComponent(componentBindingDialogDef)
export const DataSourceEditorDialog = createJsonComponent(dataSourceEditorDialogDef)
export const GitHubBuildStatus = createJsonComponent(githubBuildStatusDef)
+export const SeedDataManager = createJsonComponent(seedDataManagerDef)
// Create JSON components with hooks
export const SaveIndicator = createJsonComponentWithHooks(saveIndicatorDef, {
@@ -48,10 +51,17 @@ export const SaveIndicator = createJsonComponentWithHooks(sa
}
})
+export const ComponentTree = createJsonComponentWithHooks(componentTreeDef, {
+ hooks: {
+ treeData: {
+ hookName: 'useComponentTree',
+ args: (props) => [props.components || [], props.selectedId || null]
+ }
+ }
+})
+
// Note: The following still need JSON definitions created:
-// - LazyBarChart (needs Recharts integration)
-// - LazyLineChart (needs Recharts integration)
-// - LazyD3BarChart (needs D3 integration)
-// - SeedDataManager (complex multi-button component)
-// - StorageSettings (complex form component)
-// - ComponentTree (needs recursive rendering support)
+// - StorageSettings (complex form with backend switching)
+// - LazyBarChart (Recharts integration)
+// - LazyLineChart (Recharts integration)
+// - LazyD3BarChart (D3 calculations - hook created, needs JSON definition)