diff --git a/frontends/nextjs/src/components/ui/organisms/navigation/MenuItemList.tsx b/frontends/nextjs/src/components/ui/organisms/navigation/MenuItemList.tsx new file mode 100644 index 000000000..29b5007b5 --- /dev/null +++ b/frontends/nextjs/src/components/ui/organisms/navigation/MenuItemList.tsx @@ -0,0 +1,109 @@ +'use client' + +import { forwardRef, ReactNode, useState } from 'react' +import { + Box, + Collapse, + List, + ListItem, + ListItemButton, + ListItemIcon, + ListItemText, +} from '@mui/material' +import ExpandLess from '@mui/icons-material/ExpandLess' +import ExpandMore from '@mui/icons-material/ExpandMore' + +interface SidebarItem { + label: string + icon?: ReactNode + href?: string + onClick?: () => void + children?: SidebarItem[] + badge?: ReactNode + disabled?: boolean +} + +interface MenuItemListProps { + items: SidebarItem[] + dense?: boolean +} + +const MenuItemList = forwardRef( + ({ items, dense = false, ...props }, ref) => { + const [openItems, setOpenItems] = useState>(new Set()) + + const toggleItem = (label: string) => { + setOpenItems(prev => { + const next = new Set(prev) + if (next.has(label)) { + next.delete(label) + } else { + next.add(label) + } + return next + }) + } + + const renderItem = (item: SidebarItem, depth: number = 0) => { + const hasChildren = item.children && item.children.length > 0 + const isOpen = openItems.has(item.label) + + return ( + + + { + if (hasChildren) { + toggleItem(item.label) + } else if (item.onClick) { + item.onClick() + } + }} + disabled={item.disabled} + sx={{ + pl: 2 + depth * 2, + minHeight: dense ? 40 : 48, + }} + > + {item.icon && ( + + {item.icon} + + )} + + {item.badge && ( + + {item.badge} + + )} + {hasChildren && (isOpen ? : )} + + + {hasChildren && ( + + + {item.children!.map(child => renderItem(child, depth + 1))} + + + )} + + ) + } + + return ( + + {items.map(item => renderItem(item))} + + ) + } +) +MenuItemList.displayName = 'MenuItemList' + +export { MenuItemList } +export type { MenuItemListProps, SidebarItem } diff --git a/frontends/nextjs/src/components/ui/organisms/navigation/Sidebar.tsx b/frontends/nextjs/src/components/ui/organisms/navigation/Sidebar.tsx index bd9bdb0a8..760030fbe 100644 --- a/frontends/nextjs/src/components/ui/organisms/navigation/Sidebar.tsx +++ b/frontends/nextjs/src/components/ui/organisms/navigation/Sidebar.tsx @@ -1,37 +1,18 @@ -// TODO: Split this file (309 LOC) into smaller organisms (<150 LOC each) 'use client' -import { forwardRef, ReactNode, useState } from 'react' +import { forwardRef, ReactNode } from 'react' import { Box, Drawer, - List, - ListItem, - ListItemButton, - ListItemIcon, - ListItemText, - Collapse, - Divider, IconButton, - useTheme, useMediaQuery, - Typography, + useTheme, } from '@mui/material' import MenuIcon from '@mui/icons-material/Menu' -import ExpandLess from '@mui/icons-material/ExpandLess' -import ExpandMore from '@mui/icons-material/ExpandMore' -import ChevronLeftIcon from '@mui/icons-material/ChevronLeft' -// Types -interface SidebarItem { - label: string - icon?: ReactNode - href?: string - onClick?: () => void - children?: SidebarItem[] - badge?: ReactNode - disabled?: boolean -} +import { MenuItemList, type MenuItemListProps, type SidebarItem } from './MenuItemList' +import { SidebarHeader, type SidebarHeaderProps } from './Sidebar/Header' +import { SidebarSection, SidebarSeparator } from './Sidebar/NavSections' interface SidebarProps { children?: ReactNode @@ -42,7 +23,6 @@ interface SidebarProps { anchor?: 'left' | 'right' } -// Sidebar Root const Sidebar = forwardRef( ({ children, open = true, onClose, width = 280, variant = 'permanent', anchor = 'left', ...props }, ref) => { const theme = useTheme() @@ -76,227 +56,66 @@ const Sidebar = forwardRef( ) Sidebar.displayName = 'Sidebar' -// SidebarHeader -interface SidebarHeaderProps { - children?: ReactNode - onClose?: () => void - showCloseButton?: boolean -} - -const SidebarHeader = forwardRef( - ({ children, onClose, showCloseButton = false, ...props }, ref) => { - return ( - - {children} - {showCloseButton && onClose && ( - - - - )} - - ) - } -) -SidebarHeader.displayName = 'SidebarHeader' - -// SidebarContent interface SidebarContentProps { children: ReactNode } const SidebarContent = forwardRef( - ({ children, ...props }, ref) => { - return ( - - {children} - - ) - } + ({ children, ...props }, ref) => ( + + {children} + + ) ) SidebarContent.displayName = 'SidebarContent' -// SidebarFooter interface SidebarFooterProps { children: ReactNode } const SidebarFooter = forwardRef( - ({ children, ...props }, ref) => { - return ( - - {children} - - ) - } + ({ children, ...props }, ref) => ( + + {children} + + ) ) SidebarFooter.displayName = 'SidebarFooter' -// SidebarNav -interface SidebarNavProps { - items: SidebarItem[] - dense?: boolean -} - -const SidebarNav = forwardRef( - ({ items, dense = false, ...props }, ref) => { - const [openItems, setOpenItems] = useState>(new Set()) - - const toggleItem = (label: string) => { - setOpenItems(prev => { - const next = new Set(prev) - if (next.has(label)) { - next.delete(label) - } else { - next.add(label) - } - return next - }) - } - - const renderItem = (item: SidebarItem, depth: number = 0) => { - const hasChildren = item.children && item.children.length > 0 - const isOpen = openItems.has(item.label) - - return ( - - - { - if (hasChildren) { - toggleItem(item.label) - } else if (item.onClick) { - item.onClick() - } - }} - disabled={item.disabled} - sx={{ - pl: 2 + depth * 2, - minHeight: dense ? 40 : 48, - }} - > - {item.icon && ( - - {item.icon} - - )} - - {item.badge && ( - - {item.badge} - - )} - {hasChildren && (isOpen ? : )} - - - {hasChildren && ( - - - {item.children!.map(child => renderItem(child, depth + 1))} - - - )} - - ) - } - - return ( - - {items.map(item => renderItem(item))} - - ) - } -) -SidebarNav.displayName = 'SidebarNav' - -// SidebarSection -interface SidebarSectionProps { - title?: string - children: ReactNode -} - -const SidebarSection = forwardRef( - ({ title, children, ...props }, ref) => { - return ( - - {title && ( - - {title} - - )} - {children} - - ) - } -) -SidebarSection.displayName = 'SidebarSection' - -// SidebarSeparator -const SidebarSeparator = forwardRef>( - (props, ref) => { - return - } -) -SidebarSeparator.displayName = 'SidebarSeparator' - -// SidebarToggle - trigger to open sidebar on mobile interface SidebarToggleProps { onClick: () => void } const SidebarToggle = forwardRef( - ({ onClick, ...props }, ref) => { - return ( - - - - ) - } + ({ onClick, ...props }, ref) => ( + + + + ) ) SidebarToggle.displayName = 'SidebarToggle' +const SidebarNav = forwardRef((props, ref) => ( + +)) +SidebarNav.displayName = 'SidebarNav' + export { Sidebar, SidebarHeader, @@ -307,4 +126,4 @@ export { SidebarSeparator, SidebarToggle, } -export type { SidebarItem, SidebarProps } +export type { SidebarItem, SidebarProps, SidebarHeaderProps } diff --git a/frontends/nextjs/src/components/ui/organisms/navigation/Sidebar/Header.tsx b/frontends/nextjs/src/components/ui/organisms/navigation/Sidebar/Header.tsx new file mode 100644 index 000000000..cfe0fe23d --- /dev/null +++ b/frontends/nextjs/src/components/ui/organisms/navigation/Sidebar/Header.tsx @@ -0,0 +1,38 @@ +import { forwardRef, ReactNode } from 'react' +import { Box, IconButton } from '@mui/material' +import ChevronLeftIcon from '@mui/icons-material/ChevronLeft' + +interface SidebarHeaderProps { + children?: ReactNode + onClose?: () => void + showCloseButton?: boolean +} + +const SidebarHeader = forwardRef( + ({ children, onClose, showCloseButton = false, ...props }, ref) => { + return ( + + {children} + {showCloseButton && onClose && ( + + + + )} + + ) + } +) +SidebarHeader.displayName = 'SidebarHeader' + +export { SidebarHeader } +export type { SidebarHeaderProps } diff --git a/frontends/nextjs/src/components/ui/organisms/navigation/Sidebar/NavSections.tsx b/frontends/nextjs/src/components/ui/organisms/navigation/Sidebar/NavSections.tsx new file mode 100644 index 000000000..8e05ba355 --- /dev/null +++ b/frontends/nextjs/src/components/ui/organisms/navigation/Sidebar/NavSections.tsx @@ -0,0 +1,44 @@ +import { forwardRef, ReactNode } from 'react' +import { Box, Divider, Typography } from '@mui/material' + +interface SidebarSectionProps { + title?: string + children: ReactNode +} + +const SidebarSection = forwardRef( + ({ title, children, ...props }, ref) => { + return ( + + {title && ( + + {title} + + )} + {children} + + ) + } +) +SidebarSection.displayName = 'SidebarSection' + +const SidebarSeparator = forwardRef>( + (props, ref) => { + return + } +) +SidebarSeparator.displayName = 'SidebarSeparator' + +export { SidebarSection, SidebarSeparator } +export type { SidebarSectionProps }