diff --git a/json-components-registry.json b/json-components-registry.json index 6c4d15e..dd3461d 100644 --- a/json-components-registry.json +++ b/json-components-registry.json @@ -2484,12 +2484,8 @@ }, { "type": "NavigationMenu", - "name": "NavigationMenu", - "category": "navigation", - "canHaveChildren": true, - "description": "NavigationMenu component", - "status": "supported", - "source": "ui" + "source": "organisms", + "jsonCompatible": true }, { "type": "Notification", diff --git a/src/components/json-definitions/navigation-menu.json b/src/components/json-definitions/navigation-menu.json new file mode 100644 index 0000000..66ada4d --- /dev/null +++ b/src/components/json-definitions/navigation-menu.json @@ -0,0 +1,280 @@ +{ + "id": "navigation-menu-container", + "type": "Sidebar", + "props": { + "side": "left", + "collapsible": "offcanvas" + }, + "children": [ + { + "id": "navigation-header", + "type": "SidebarHeader", + "props": { + "className": "px-4 py-4 border-b" + }, + "children": [ + { + "id": "nav-title", + "type": "h2", + "props": { + "className": "text-lg font-semibold" + }, + "bindings": { + "children": { + "source": "navTitle", + "transform": "navTitle || 'Navigation'" + } + } + }, + { + "id": "nav-controls", + "type": "div", + "props": { + "className": "flex gap-2 mt-4" + }, + "children": [ + { + "id": "expand-all-button", + "type": "Button", + "props": { + "variant": "outline", + "size": "sm", + "className": "flex-1" + }, + "bindings": { + "onClick": { + "source": "menuState.expandAll", + "transform": "() => menuState.expandAll()" + }, + "children": { + "source": "expandAllLabel", + "transform": "expandAllLabel || 'Expand All'" + } + } + }, + { + "id": "collapse-all-button", + "type": "Button", + "props": { + "variant": "outline", + "size": "sm", + "className": "flex-1" + }, + "bindings": { + "onClick": { + "source": "menuState.collapseAll", + "transform": "() => menuState.collapseAll()" + }, + "children": { + "source": "collapseAllLabel", + "transform": "collapseAllLabel || 'Collapse All'" + } + } + } + ] + } + ] + }, + { + "id": "sidebar-content", + "type": "SidebarContent", + "children": [ + { + "id": "scroll-area", + "type": "ScrollArea", + "props": { + "className": "h-full px-4" + }, + "children": [ + { + "id": "groups-container", + "type": "div", + "props": { + "className": "space-y-2 py-4" + }, + "children": [ + { + "id": "groups-list", + "type": "list", + "bindings": { + "items": "navigationGroups", + "keyPath": "id" + }, + "itemTemplate": { + "type": "Collapsible", + "bindings": { + "open": { + "source": "item.id,menuState.expandedGroups", + "transform": "menuState.expandedGroups.has(item.id)" + }, + "onOpenChange": { + "source": "item.id", + "transform": "() => menuState.toggleGroup(item.id)" + } + }, + "children": [ + { + "id": "group-trigger", + "type": "CollapsibleTrigger", + "props": { + "className": "w-full flex items-center gap-2 px-2 py-2 rounded-lg hover:bg-muted transition-colors group" + }, + "children": [ + { + "id": "group-caret", + "type": "CaretDown", + "props": { + "size": 16, + "weight": "bold" + }, + "bindings": { + "className": { + "source": "menuState.expandedGroups", + "transform": "menuState.expandedGroups.has(item.id) ? 'text-muted-foreground transition-transform rotate-0' : 'text-muted-foreground transition-transform -rotate-90'" + } + } + }, + { + "id": "group-label", + "type": "h3", + "props": { + "className": "flex-1 text-left text-xs font-semibold text-muted-foreground uppercase tracking-wider" + }, + "bindings": { + "children": "item.label" + } + }, + { + "id": "group-count", + "type": "span", + "props": { + "className": "text-xs text-muted-foreground" + }, + "bindings": { + "children": { + "source": "item.items,menuState.isItemVisible", + "transform": "item.items.filter(it => menuState.isItemVisible(it)).length" + } + } + } + ] + }, + { + "id": "group-content", + "type": "CollapsibleContent", + "props": { + "className": "mt-1" + }, + "children": [ + { + "id": "items-container", + "type": "div", + "props": { + "className": "space-y-1 pl-2" + }, + "children": [ + { + "id": "items-list", + "type": "list", + "bindings": { + "items": "item.items", + "keyPath": "id" + }, + "itemTemplate": { + "type": "div", + "bindings": { + "onMouseEnter": { + "source": "item.value", + "transform": "() => menuState.handleItemHover(item.value)" + }, + "onMouseLeave": { + "source": "item.value", + "transform": "() => menuState.handleItemLeave(item.value)" + } + }, + "conditional": { + "if": "menuState.isItemVisible(item)" + }, + "children": [ + { + "id": "nav-item-button", + "type": "button", + "props": { + "type": "button" + }, + "bindings": { + "onClick": { + "source": "item.value,onTabChange", + "transform": "() => onTabChange(item.value)" + }, + "className": { + "source": "item.value,activeTab", + "transform": "activeTab === item.value ? 'w-full flex items-center gap-3 px-3 py-2 rounded-lg transition-colors bg-primary text-primary-foreground' : 'w-full flex items-center gap-3 px-3 py-2 rounded-lg transition-colors hover:bg-muted text-foreground'" + } + }, + "children": [ + { + "id": "item-icon", + "type": "IconWrapper", + "props": { + "size": "md" + }, + "bindings": { + "icon": "item.icon", + "variant": { + "source": "item.value,activeTab", + "transform": "activeTab === item.value ? 'default' : 'muted'" + } + } + }, + { + "id": "item-label", + "type": "Text", + "props": { + "className": "flex-1 text-left font-medium", + "variant": "small" + }, + "bindings": { + "children": "item.label" + } + }, + { + "id": "item-badge", + "type": "Badge", + "props": { + "className": "ml-auto" + }, + "bindings": { + "variant": { + "source": "item.id,activeTab,errorCount", + "transform": "activeTab === item.value ? 'secondary' : 'destructive'" + }, + "children": { + "source": "item.id,errorCount,item.badge", + "transform": "item.id === 'errors' ? errorCount : item.badge" + } + }, + "conditional": { + "if": "(item.id === 'errors' && errorCount > 0) || (item.id !== 'errors' && item.badge !== undefined && item.badge > 0)" + } + } + ] + } + ] + } + } + ] + } + ] + } + ] + } + } + ] + } + ] + } + ] + } + ] +} diff --git a/src/components/json-definitions/tree-list-panel.json b/src/components/json-definitions/tree-list-panel.json new file mode 100644 index 0000000..8cbf511 --- /dev/null +++ b/src/components/json-definitions/tree-list-panel.json @@ -0,0 +1,346 @@ +{ + "id": "tree-list-panel-container", + "type": "div", + "props": { + "className": "w-80 border-r border-border bg-card p-4 flex flex-col gap-4" + }, + "children": [ + { + "id": "tree-list-header", + "type": "div", + "props": { "className": "space-y-3" }, + "children": [ + { + "id": "header-flex", + "type": "div", + "props": { "className": "flex items-center justify-between" }, + "children": [ + { + "id": "header-title", + "type": "div", + "props": { "className": "flex items-center gap-2" }, + "children": [ + { + "id": "tree-icon", + "type": "TreeIcon", + "props": { "size": 20 } + }, + { + "id": "title-heading", + "type": "h2", + "props": { "className": "text-lg font-semibold" }, + "children": "Component Trees" + } + ] + }, + { + "id": "create-button", + "type": "IconButton", + "props": { + "size": "sm" + }, + "bindings": { + "onClick": { + "source": "onCreateNew", + "transform": "() => onCreateNew?.()" + } + }, + "children": [ + { + "id": "add-icon", + "type": "ActionIcon", + "props": { + "action": "add", + "size": 16 + } + } + ] + } + ] + }, + { + "id": "action-buttons", + "type": "div", + "props": { "className": "flex gap-2" }, + "children": [ + { + "id": "import-button", + "type": "Button", + "props": { + "size": "sm", + "variant": "outline", + "className": "flex-1 text-xs" + }, + "bindings": { + "onClick": { + "source": "onImportJson", + "transform": "() => onImportJson?.()" + } + }, + "children": [ + { + "id": "upload-icon", + "type": "ActionIcon", + "props": { + "action": "upload", + "size": 14 + } + }, + { + "id": "import-text", + "type": "span", + "children": "Import JSON" + } + ] + }, + { + "id": "export-button", + "type": "Button", + "props": { + "size": "sm", + "variant": "outline", + "className": "flex-1 text-xs" + }, + "bindings": { + "onClick": { + "source": "onExportJson", + "transform": "() => onExportJson?.()" + }, + "disabled": { + "source": "selectedTreeId", + "transform": "!selectedTreeId" + } + }, + "children": [ + { + "id": "download-icon", + "type": "ActionIcon", + "props": { + "action": "download", + "size": 14 + } + }, + { + "id": "export-text", + "type": "span", + "children": "Export JSON" + } + ] + } + ] + } + ] + }, + { + "id": "trees-list-wrapper", + "type": "div", + "conditional": { "if": "trees.length === 0" }, + "props": { "className": "flex-1 flex flex-col items-center justify-center" }, + "children": [ + { + "id": "empty-state", + "type": "EmptyState", + "props": { + "title": "No component trees yet", + "description": "Create your first tree to get started", + "action": { + "label": "Create First Tree" + } + }, + "bindings": { + "onAction": { + "source": "onCreateNew", + "transform": "() => onCreateNew?.()" + } + } + } + ] + }, + { + "id": "trees-scroll-area", + "type": "ScrollArea", + "props": { "className": "flex-1" }, + "conditional": { "if": "trees.length > 0" }, + "children": [ + { + "id": "trees-list", + "type": "div", + "props": { "className": "space-y-2" }, + "children": [ + { + "id": "tree-cards", + "type": "list", + "bindings": { + "items": "trees", + "keyPath": "id" + }, + "itemTemplate": { + "id": "tree-card-item", + "type": "Card", + "bindings": { + "className": { + "source": "selectedTreeId,item.id", + "transform": "item.id === selectedTreeId ? 'cursor-pointer transition-all p-4 ring-2 ring-primary bg-accent' : 'cursor-pointer transition-all p-4 hover:bg-accent/50'" + }, + "onClick": { + "source": "onTreeSelect,item.id", + "transform": "() => onTreeSelect?.(item.id)" + } + }, + "children": [ + { + "id": "card-content", + "type": "div", + "props": { "className": "space-y-3" }, + "children": [ + { + "id": "card-header", + "type": "div", + "props": { "className": "flex items-start justify-between gap-2" }, + "children": [ + { + "id": "card-title-wrapper", + "type": "div", + "props": { "className": "flex-1 min-w-0" }, + "children": [ + { + "id": "card-title", + "type": "h4", + "props": { "className": "text-sm truncate font-semibold" }, + "bindings": { "children": "item.name" } + }, + { + "id": "card-description", + "type": "p", + "props": { "className": "text-xs text-muted-foreground line-clamp-2" }, + "bindings": { "children": "item.description" }, + "conditional": { "if": "item.description" } + }, + { + "id": "card-badge", + "type": "div", + "props": { "className": "mt-2" }, + "children": [ + { + "id": "component-count-badge", + "type": "Badge", + "props": { "variant": "outline", "className": "text-xs" }, + "bindings": { + "children": { + "source": "item.rootNodes.length", + "transform": "`${item.rootNodes.length} components`" + } + } + } + ] + } + ] + } + ] + }, + { + "id": "card-actions", + "type": "div", + "props": { "className": "mt-2 flex gap-1" }, + "bindings": { + "onClick": { + "source": "none", + "transform": "(e) => e.stopPropagation()" + } + }, + "children": [ + { + "id": "edit-button", + "type": "IconButton", + "props": { + "variant": "ghost", + "size": "sm", + "title": "Edit tree" + }, + "bindings": { + "onClick": { + "source": "onTreeEdit,item", + "transform": "(e) => { e.stopPropagation(); onTreeEdit?.(item); }" + } + }, + "children": [ + { + "id": "edit-icon", + "type": "ActionIcon", + "props": { + "action": "edit", + "size": 14 + } + } + ] + }, + { + "id": "duplicate-button", + "type": "IconButton", + "props": { + "variant": "ghost", + "size": "sm", + "title": "Duplicate tree" + }, + "bindings": { + "onClick": { + "source": "onTreeDuplicate,item", + "transform": "(e) => { e.stopPropagation(); onTreeDuplicate?.(item); }" + } + }, + "children": [ + { + "id": "duplicate-icon", + "type": "ActionIcon", + "props": { + "action": "duplicate", + "size": 14 + } + } + ] + }, + { + "id": "delete-button", + "type": "IconButton", + "props": { + "variant": "ghost", + "size": "sm" + }, + "bindings": { + "onClick": { + "source": "onTreeDelete,item.id", + "transform": "(e) => { e.stopPropagation(); onTreeDelete?.(item.id); }" + }, + "disabled": { + "source": "trees.length", + "transform": "trees.length === 1" + }, + "title": { + "source": "trees.length", + "transform": "trees.length === 1 ? \"Can't delete last tree\" : \"Delete tree\"" + } + }, + "children": [ + { + "id": "delete-icon", + "type": "ActionIcon", + "props": { + "action": "delete", + "size": 14 + } + } + ] + } + ] + } + ] + } + ] + } + } + ] + } + ] + } + ] +} diff --git a/src/hooks/index.ts b/src/hooks/index.ts index f39323b..21d9ee3 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -39,3 +39,5 @@ export * from './use-accordion' export * from './use-binding-editor' export { useAppLayout } from './use-app-layout' export { useAppRouterLayout } from './use-app-router-layout' +export { useNavigationMenu } from './use-navigation-menu' +export { useDataSourceManagerState } from './use-data-source-manager-state' diff --git a/src/hooks/use-navigation-menu.ts b/src/hooks/use-navigation-menu.ts new file mode 100644 index 0000000..52cc10d --- /dev/null +++ b/src/hooks/use-navigation-menu.ts @@ -0,0 +1,90 @@ +import { useState } from 'react' +import { navigationGroups, NavigationItemData } from '@/lib/navigation-config' +import { FeatureToggles } from '@/types/project' +import { useRoutePreload } from './use-route-preload' + +interface UseNavigationMenuState { + expandedGroups: Set + toggleGroup: (groupId: string) => void + expandAll: () => void + collapseAll: () => void + isItemVisible: (item: NavigationItemData) => boolean + getVisibleItemsCount: (groupId: string) => number + getItemBadge: (item: NavigationItemData, errorCount: number) => number | undefined + handleItemHover: (value: string) => void + handleItemLeave: (value: string) => void +} + +export function useNavigationMenu(featureToggles: FeatureToggles, errorCount: number = 0): UseNavigationMenuState { + const [expandedGroups, setExpandedGroups] = useState>( + new Set(['overview', 'development', 'automation', 'design', 'backend', 'testing', 'tools']) + ) + + const { preloadRoute, cancelPreload } = useRoutePreload({ delay: 100 }) + + const isItemVisible = (item: NavigationItemData) => { + if (!item.featureKey) return true + return featureToggles[item.featureKey] + } + + const getVisibleItemsCount = (groupId: string) => { + const group = navigationGroups.find((g) => g.id === groupId) + if (!group) return 0 + return group.items.filter(isItemVisible).length + } + + const getItemBadge = (item: NavigationItemData, errorCount: number) => { + if (item.id === 'errors') return errorCount + return item.badge + } + + const toggleGroup = (groupId: string) => { + setExpandedGroups((prev) => { + const newSet = new Set(prev) + if (newSet.has(groupId)) { + newSet.delete(groupId) + } else { + newSet.add(groupId) + } + return newSet + }) + } + + const expandAll = () => { + const allGroupIds = navigationGroups + .filter((group) => + group.items.some((item) => { + if (!item.featureKey) return true + return featureToggles[item.featureKey] + }) + ) + .map((group) => group.id) + setExpandedGroups(new Set(allGroupIds)) + } + + const collapseAll = () => { + setExpandedGroups(new Set()) + } + + const handleItemHover = (value: string) => { + console.log(`[NAV] 🖱️ Hover detected on: ${value}`) + preloadRoute(value) + } + + const handleItemLeave = (value: string) => { + console.log(`[NAV] 👋 Hover left: ${value}`) + cancelPreload(value) + } + + return { + expandedGroups, + toggleGroup, + expandAll, + collapseAll, + isItemVisible, + getVisibleItemsCount, + getItemBadge, + handleItemHover, + handleItemLeave, + } +} diff --git a/src/lib/json-ui/hooks-registry.ts b/src/lib/json-ui/hooks-registry.ts index 483c3e8..018db09 100644 --- a/src/lib/json-ui/hooks-registry.ts +++ b/src/lib/json-ui/hooks-registry.ts @@ -17,6 +17,8 @@ import { useAccordion } from '@/hooks/use-accordion' import { useBindingEditor } from '@/hooks/use-binding-editor' import { useAppLayout } from '@/hooks/use-app-layout' import { useAppRouterLayout } from '@/hooks/use-app-router-layout' +import { useNavigationMenu } from '@/hooks/use-navigation-menu' +import { useDataSourceManagerState } from '@/hooks/use-data-source-manager-state' export interface HookRegistry { [key: string]: (...args: any[]) => any @@ -41,6 +43,8 @@ export const hooksRegistry: HookRegistry = { useBindingEditor, useAppLayout, useAppRouterLayout, + useNavigationMenu, + useDataSourceManagerState, // Add more hooks here as needed } diff --git a/src/lib/json-ui/interfaces/index.ts b/src/lib/json-ui/interfaces/index.ts index eee867f..6531037 100644 --- a/src/lib/json-ui/interfaces/index.ts +++ b/src/lib/json-ui/interfaces/index.ts @@ -22,7 +22,10 @@ export * from './menu' export * from './file-upload' export * from './accordion' export * from './binding-editor' +export * from './tree-list-panel' export * from './app-layout' export * from './app-router-layout' export * from './app-main-panel' export * from './app-dialogs' +export * from './data-source-manager' +export * from './navigation-menu' diff --git a/src/lib/json-ui/interfaces/navigation-menu.ts b/src/lib/json-ui/interfaces/navigation-menu.ts new file mode 100644 index 0000000..87d37bb --- /dev/null +++ b/src/lib/json-ui/interfaces/navigation-menu.ts @@ -0,0 +1,8 @@ +import type { FeatureToggles } from '@/types/project' + +export interface NavigationMenuProps { + activeTab: string + onTabChange: (tab: string) => void + featureToggles: FeatureToggles + errorCount?: number +} diff --git a/src/lib/json-ui/interfaces/tree-list-panel.ts b/src/lib/json-ui/interfaces/tree-list-panel.ts new file mode 100644 index 0000000..227d7f7 --- /dev/null +++ b/src/lib/json-ui/interfaces/tree-list-panel.ts @@ -0,0 +1,13 @@ +import type { ComponentTree } from '@/types/project' + +export interface TreeListPanelProps { + trees: ComponentTree[] + selectedTreeId: string | null + onTreeSelect: (treeId: string) => void + onTreeEdit: (tree: ComponentTree) => void + onTreeDuplicate: (tree: ComponentTree) => void + onTreeDelete: (treeId: string) => void + onCreateNew: () => void + onImportJson: () => void + onExportJson: () => void +} diff --git a/src/lib/json-ui/json-components.ts b/src/lib/json-ui/json-components.ts index 8839b51..0e44922 100644 --- a/src/lib/json-ui/json-components.ts +++ b/src/lib/json-ui/json-components.ts @@ -34,6 +34,9 @@ import type { AppRouterLayoutProps, AppMainPanelProps, AppDialogsProps, + DataSourceManagerProps, + NavigationMenuProps, + TreeListPanelProps, } from './interfaces' // Import JSON definitions @@ -63,6 +66,9 @@ import appLayoutDef from '@/components/json-definitions/app-layout.json' import appRouterLayoutDef from '@/components/json-definitions/app-router-layout.json' import appMainPanelDef from '@/components/json-definitions/app-main-panel.json' import appDialogsDef from '@/components/json-definitions/app-dialogs.json' +import navigationMenuDef from '@/components/json-definitions/navigation-menu.json' +import dataSourceManagerDef from '@/components/json-definitions/data-source-manager.json' +import treeListPanelDef from '@/components/json-definitions/tree-list-panel.json' // Create pure JSON components (no hooks) export const LoadingFallback = createJsonComponent(loadingFallbackDef) @@ -215,4 +221,24 @@ export const AppRouterLayout = createJsonComponentWithHooks(appMainPanelDef) +export const DataSourceManager = createJsonComponentWithHooks(dataSourceManagerDef, { + hooks: { + managerState: { + hookName: 'useDataSourceManagerState', + args: (props) => [props.dataSources || [], props.onChange || (() => {})] + } + } +}) + +export const NavigationMenu = createJsonComponentWithHooks(navigationMenuDef, { + hooks: { + menuState: { + hookName: 'useNavigationMenu', + args: (props) => [props.featureToggles, props.errorCount || 0] + } + } +}) + +export const TreeListPanel = createJsonComponent(treeListPanelDef) + // All components converted to pure JSON! 🎉