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)