@nixopus/ui
Advanced tools
| 'use client'; | ||
| import * as React from 'react'; | ||
| import { Separator } from '../separator'; | ||
| import { cn } from '../../lib/utils'; | ||
| // Type definitions for optional Next.js dependencies | ||
| type NextRouter = { | ||
| push: (url: string) => void; | ||
| }; | ||
| type NextLinkProps = { | ||
| href: string; | ||
| className?: string; | ||
| children: React.ReactNode; | ||
| }; | ||
| /** | ||
| * Navigation item structure for sidebar menu | ||
| */ | ||
| export interface SidebarNavItem { | ||
| title: string; | ||
| url: string; | ||
| icon?: React.ComponentType<{ className?: string }>; | ||
| isActive?: boolean; | ||
| items?: SidebarNavItem[]; | ||
| badge?: string | number; | ||
| onClick?: (url: string) => void; | ||
| } | ||
| /** | ||
| * Breadcrumb item structure | ||
| */ | ||
| export interface BreadcrumbItem { | ||
| label: string; | ||
| href?: string; | ||
| onClick?: () => void; | ||
| } | ||
| /** | ||
| * Top bar configuration | ||
| */ | ||
| export interface TopBarConfig { | ||
| showSidebarTrigger?: boolean; | ||
| showBreadcrumbs?: boolean; | ||
| breadcrumbs?: BreadcrumbItem[]; | ||
| rightContent?: React.ReactNode; | ||
| className?: string; | ||
| } | ||
| /** | ||
| * Sidebar configuration options | ||
| */ | ||
| export interface SidebarConfig { | ||
| /** Sidebar position */ | ||
| side?: 'left' | 'right'; | ||
| /** Sidebar variant */ | ||
| variant?: 'sidebar' | 'floating' | 'inset'; | ||
| /** Collapsible behavior */ | ||
| collapsible?: 'offcanvas' | 'icon' | 'none'; | ||
| /** Default open state */ | ||
| defaultOpen?: boolean; | ||
| /** Controlled open state */ | ||
| open?: boolean; | ||
| /** Open state change handler */ | ||
| onOpenChange?: (open: boolean) => void; | ||
| /** Custom sidebar className */ | ||
| sidebarClassName?: string; | ||
| /** Custom header content */ | ||
| header?: React.ReactNode; | ||
| /** Custom footer content */ | ||
| footer?: React.ReactNode; | ||
| /** Navigation items */ | ||
| navItems?: SidebarNavItem[]; | ||
| /** Custom content (overrides navItems if provided) */ | ||
| content?: React.ReactNode; | ||
| /** Current pathname for active state detection (optional, can be provided via getCurrentPath) */ | ||
| currentPath?: string; | ||
| /** Function to get current pathname (optional, falls back to currentPath or manual isActive) */ | ||
| getCurrentPath?: () => string; | ||
| /** Router instance for navigation (optional, falls back to onClick handlers) */ | ||
| router?: NextRouter; | ||
| /** Link component for navigation (optional, falls back to anchor tags) */ | ||
| LinkComponent?: React.ComponentType<NextLinkProps>; | ||
| } | ||
| /** | ||
| * SidebarLayoutWrapper Props | ||
| */ | ||
| export interface SidebarLayoutWrapperProps { | ||
| /** Main content */ | ||
| children: React.ReactNode; | ||
| /** Whether to enable sidebar layout */ | ||
| enableSidebar?: boolean; | ||
| /** Sidebar configuration */ | ||
| sidebarConfig?: SidebarConfig; | ||
| /** Top bar configuration */ | ||
| topBarConfig?: TopBarConfig; | ||
| /** Custom top bar component (overrides topBarConfig if provided) */ | ||
| topBar?: React.ReactNode; | ||
| /** Additional className for the wrapper */ | ||
| className?: string; | ||
| /** Additional className for the inset (main content area) */ | ||
| insetClassName?: string; | ||
| /** Sidebar components - must be provided when enableSidebar is true */ | ||
| sidebarComponents?: { | ||
| Sidebar: React.ComponentType<any>; | ||
| SidebarContent: React.ComponentType<any>; | ||
| SidebarFooter: React.ComponentType<any>; | ||
| SidebarGroup: React.ComponentType<any>; | ||
| SidebarHeader: React.ComponentType<any>; | ||
| SidebarInset: React.ComponentType<any>; | ||
| SidebarMenu: React.ComponentType<any>; | ||
| SidebarMenuButton: React.ComponentType<any>; | ||
| SidebarMenuItem: React.ComponentType<any>; | ||
| SidebarMenuSub: React.ComponentType<any>; | ||
| SidebarMenuSubButton: React.ComponentType<any>; | ||
| SidebarMenuSubItem: React.ComponentType<any>; | ||
| SidebarProvider: React.ComponentType<any>; | ||
| SidebarRail: React.ComponentType<any>; | ||
| SidebarTrigger: React.ComponentType<any>; | ||
| useSidebar: () => { | ||
| state: 'expanded' | 'collapsed'; | ||
| open: boolean; | ||
| setOpen: (open: boolean) => void; | ||
| toggleSidebar: () => void; | ||
| }; | ||
| }; | ||
| /** Sidebar hover menu component (optional, for collapsed sidebar with nested items) */ | ||
| SidebarHoverMenu?: React.ComponentType<{ | ||
| items: Array<{ title: string; url: string }>; | ||
| children: React.ReactNode; | ||
| }>; | ||
| } | ||
| /** | ||
| * Internal component to render navigation items | ||
| */ | ||
| function NavItemsRenderer({ | ||
| items, | ||
| onItemClick, | ||
| sidebarComponents, | ||
| SidebarHoverMenu, | ||
| sidebarConfig | ||
| }: { | ||
| items: SidebarNavItem[]; | ||
| onItemClick?: (url: string) => void; | ||
| sidebarComponents: NonNullable<SidebarLayoutWrapperProps['sidebarComponents']>; | ||
| SidebarHoverMenu?: SidebarLayoutWrapperProps['SidebarHoverMenu']; | ||
| sidebarConfig?: SidebarConfig; | ||
| }) { | ||
| const { useSidebar, SidebarMenu, SidebarMenuItem, SidebarMenuButton, SidebarMenuSub, SidebarMenuSubButton, SidebarMenuSubItem } = sidebarComponents; | ||
| const { state } = useSidebar(); | ||
| const getCurrentPath = sidebarConfig?.getCurrentPath || (() => sidebarConfig?.currentPath || ''); | ||
| const router = sidebarConfig?.router; | ||
| const LinkComponent = sidebarConfig?.LinkComponent; | ||
| const handleClick = (url: string, onClick?: (url: string) => void) => { | ||
| if (onClick) { | ||
| onClick(url); | ||
| } else if (router) { | ||
| router.push(url); | ||
| } else if (typeof window !== 'undefined') { | ||
| window.location.href = url; | ||
| } | ||
| }; | ||
| const isItemActive = (url: string, isActive?: boolean) => { | ||
| if (isActive !== undefined) return isActive; | ||
| const currentPath = getCurrentPath(); | ||
| if (!currentPath) return false; | ||
| return currentPath === url || currentPath.startsWith(url + '/'); | ||
| }; | ||
| return ( | ||
| <SidebarMenu> | ||
| {items.map((item) => { | ||
| const hasNestedItems = (item.items?.length || 0) > 0; | ||
| const isCollapsed = state === 'collapsed'; | ||
| const itemIsActive = isItemActive(item.url, item.isActive); | ||
| const hasActiveSubItem = hasNestedItems && item.items?.some((subItem) => isItemActive(subItem.url, subItem.isActive)); | ||
| if (hasNestedItems && isCollapsed && SidebarHoverMenu) { | ||
| // For collapsed sidebar with nested items, show hover menu | ||
| const hoverMenuItems = item.items?.map((subItem) => ({ | ||
| title: subItem.title, | ||
| url: subItem.url | ||
| })) || []; | ||
| return ( | ||
| <SidebarMenuItem key={item.title}> | ||
| <SidebarHoverMenu items={hoverMenuItems}> | ||
| <SidebarMenuButton | ||
| className="cursor-pointer" | ||
| tooltip={item.title} | ||
| isActive={itemIsActive || hasActiveSubItem} | ||
| onClick={() => handleClick(item.url, item.onClick || onItemClick)} | ||
| > | ||
| {item.icon && <item.icon />} | ||
| <span>{item.title}</span> | ||
| {item.badge && ( | ||
| <span className="ml-auto rounded-full bg-primary px-2 py-0.5 text-xs font-medium"> | ||
| {item.badge} | ||
| </span> | ||
| )} | ||
| </SidebarMenuButton> | ||
| </SidebarHoverMenu> | ||
| </SidebarMenuItem> | ||
| ); | ||
| } | ||
| return ( | ||
| <SidebarMenuItem key={item.title}> | ||
| <SidebarMenuButton | ||
| className="cursor-pointer" | ||
| tooltip={item.title} | ||
| isActive={itemIsActive || hasActiveSubItem} | ||
| onClick={() => handleClick(item.url, item.onClick || onItemClick)} | ||
| > | ||
| {item.icon && <item.icon />} | ||
| <span>{item.title}</span> | ||
| {item.badge && ( | ||
| <span className="ml-auto rounded-full bg-primary px-2 py-0.5 text-xs font-medium"> | ||
| {item.badge} | ||
| </span> | ||
| )} | ||
| </SidebarMenuButton> | ||
| {hasNestedItems && !isCollapsed && item.items && ( | ||
| <SidebarMenuSub> | ||
| {item.items.map((subItem) => { | ||
| const subItemIsActive = isItemActive(subItem.url, subItem.isActive); | ||
| const linkContent = ( | ||
| <> | ||
| <span>{subItem.title}</span> | ||
| {subItem.badge && ( | ||
| <span className="ml-auto rounded-full bg-primary px-2 py-0.5 text-xs font-medium"> | ||
| {subItem.badge} | ||
| </span> | ||
| )} | ||
| </> | ||
| ); | ||
| return ( | ||
| <SidebarMenuSubItem key={subItem.title}> | ||
| <SidebarMenuSubButton | ||
| asChild={!!LinkComponent} | ||
| isActive={subItemIsActive} | ||
| onClick={() => handleClick(subItem.url, subItem.onClick || onItemClick)} | ||
| > | ||
| {LinkComponent ? ( | ||
| <LinkComponent href={subItem.url}>{linkContent}</LinkComponent> | ||
| ) : ( | ||
| <a href={subItem.url} onClick={(e) => { | ||
| e.preventDefault(); | ||
| handleClick(subItem.url, subItem.onClick || onItemClick); | ||
| }}> | ||
| {linkContent} | ||
| </a> | ||
| )} | ||
| </SidebarMenuSubButton> | ||
| </SidebarMenuSubItem> | ||
| ); | ||
| })} | ||
| </SidebarMenuSub> | ||
| )} | ||
| </SidebarMenuItem> | ||
| ); | ||
| })} | ||
| </SidebarMenu> | ||
| ); | ||
| } | ||
| /** | ||
| * Default Top Bar Component | ||
| */ | ||
| function DefaultTopBar({ | ||
| config, | ||
| sidebarComponents, | ||
| sidebarConfig | ||
| }: { | ||
| config?: TopBarConfig; | ||
| sidebarComponents: NonNullable<SidebarLayoutWrapperProps['sidebarComponents']>; | ||
| sidebarConfig?: SidebarConfig; | ||
| }) { | ||
| const { SidebarTrigger } = sidebarComponents; | ||
| const LinkComponent = sidebarConfig?.LinkComponent; | ||
| const { showSidebarTrigger = true, showBreadcrumbs = true, breadcrumbs, rightContent, className } = config || {}; | ||
| return ( | ||
| <header | ||
| className={cn( | ||
| 'flex h-16 shrink-0 items-center gap-2 transition-[width,height] ease-linear group-has-data-[collapsible=icon]/sidebar-wrapper:h-12', | ||
| className | ||
| )} | ||
| > | ||
| <div className="flex items-center gap-2 px-4 justify-between w-full"> | ||
| <div className="flex items-center gap-2"> | ||
| {showSidebarTrigger && <SidebarTrigger className="-ml-1" />} | ||
| {showSidebarTrigger && showBreadcrumbs && breadcrumbs && breadcrumbs.length > 0 && ( | ||
| <Separator orientation="vertical" className="mr-2 data-[orientation=vertical]:h-4" /> | ||
| )} | ||
| {showBreadcrumbs && breadcrumbs && breadcrumbs.length > 0 && ( | ||
| <nav className="flex items-center gap-1 text-sm"> | ||
| {breadcrumbs.map((crumb, idx) => ( | ||
| <React.Fragment key={idx}> | ||
| {crumb.href && LinkComponent ? ( | ||
| <LinkComponent | ||
| href={crumb.href} | ||
| className="text-muted-foreground hover:text-foreground transition-colors" | ||
| > | ||
| {crumb.label} | ||
| </LinkComponent> | ||
| ) : crumb.href ? ( | ||
| <a | ||
| href={crumb.href} | ||
| className="text-muted-foreground hover:text-foreground transition-colors" | ||
| > | ||
| {crumb.label} | ||
| </a> | ||
| ) : crumb.onClick ? ( | ||
| <button | ||
| onClick={crumb.onClick} | ||
| className="text-muted-foreground hover:text-foreground transition-colors" | ||
| > | ||
| {crumb.label} | ||
| </button> | ||
| ) : ( | ||
| <span className="text-foreground font-medium">{crumb.label}</span> | ||
| )} | ||
| {idx < breadcrumbs.length - 1 && <span className="text-muted-foreground mx-1">/</span>} | ||
| </React.Fragment> | ||
| ))} | ||
| </nav> | ||
| )} | ||
| </div> | ||
| {rightContent && <div className="flex items-center gap-4">{rightContent}</div>} | ||
| </div> | ||
| </header> | ||
| ); | ||
| } | ||
| /** | ||
| * SidebarLayoutWrapper - A flexible wrapper component for sidebar layouts | ||
| * | ||
| * This component provides a customizable sidebar layout system that can be: | ||
| * - Enabled or disabled per page/route | ||
| * - Configured with custom navigation items, header, footer | ||
| * - Customized with different sidebar variants and behaviors | ||
| * - Extended with custom top bar or content | ||
| * - Framework-agnostic (works with or without Next.js) | ||
| * | ||
| * @example | ||
| * ```tsx | ||
| * // With Next.js | ||
| * import { SidebarLayoutWrapper } from '@nixopus/ui'; | ||
| * import * as Sidebar from '@/components/ui/sidebar'; | ||
| * | ||
| * <SidebarLayoutWrapper | ||
| * enableSidebar={true} | ||
| * sidebarComponents={Sidebar} | ||
| * sidebarConfig={{ | ||
| * defaultOpen: true, | ||
| * navItems: [ | ||
| * { title: 'Home', url: '/', icon: Home }, | ||
| * { title: 'Projects', url: '/projects', icon: Folder } | ||
| * ] | ||
| * }} | ||
| * > | ||
| * <YourPageContent /> | ||
| * </SidebarLayoutWrapper> | ||
| * ``` | ||
| */ | ||
| export function SidebarLayoutWrapper({ | ||
| children, | ||
| enableSidebar = false, | ||
| sidebarConfig, | ||
| topBarConfig, | ||
| topBar, | ||
| className, | ||
| insetClassName, | ||
| sidebarComponents, | ||
| SidebarHoverMenu | ||
| }: SidebarLayoutWrapperProps) { | ||
| // If sidebar is disabled, just render children | ||
| if (!enableSidebar) { | ||
| return <>{children}</>; | ||
| } | ||
| // Require sidebar components when sidebar is enabled | ||
| if (!sidebarComponents) { | ||
| throw new Error( | ||
| 'SidebarLayoutWrapper: sidebarComponents prop is required when enableSidebar is true. ' + | ||
| 'Please provide the sidebar components from your sidebar implementation.' | ||
| ); | ||
| } | ||
| const { | ||
| Sidebar, | ||
| SidebarContent, | ||
| SidebarFooter, | ||
| SidebarGroup, | ||
| SidebarHeader, | ||
| SidebarInset, | ||
| SidebarProvider, | ||
| SidebarRail | ||
| } = sidebarComponents; | ||
| const { | ||
| side = 'left', | ||
| variant = 'sidebar', | ||
| collapsible = 'icon', | ||
| defaultOpen = true, | ||
| open: openProp, | ||
| onOpenChange, | ||
| sidebarClassName, | ||
| header, | ||
| footer, | ||
| navItems = [], | ||
| content | ||
| } = sidebarConfig || {}; | ||
| // Render sidebar content | ||
| const sidebarContent = content || ( | ||
| <> | ||
| {navItems.length > 0 && ( | ||
| <SidebarContent> | ||
| <SidebarGroup> | ||
| <NavItemsRenderer | ||
| items={navItems} | ||
| onItemClick={sidebarConfig?.navItems?.[0]?.onClick} | ||
| sidebarComponents={sidebarComponents} | ||
| SidebarHoverMenu={SidebarHoverMenu} | ||
| sidebarConfig={sidebarConfig} | ||
| /> | ||
| </SidebarGroup> | ||
| </SidebarContent> | ||
| )} | ||
| </> | ||
| ); | ||
| return ( | ||
| <SidebarProvider defaultOpen={defaultOpen} open={openProp} onOpenChange={onOpenChange}> | ||
| <Sidebar | ||
| side={side} | ||
| variant={variant} | ||
| collapsible={collapsible} | ||
| className={sidebarClassName} | ||
| > | ||
| {header && <SidebarHeader>{header}</SidebarHeader>} | ||
| {sidebarContent} | ||
| {footer && <SidebarFooter>{footer}</SidebarFooter>} | ||
| <SidebarRail /> | ||
| </Sidebar> | ||
| <SidebarInset className={insetClassName}> | ||
| {topBar || ( | ||
| <DefaultTopBar | ||
| config={topBarConfig} | ||
| sidebarComponents={sidebarComponents} | ||
| sidebarConfig={sidebarConfig} | ||
| /> | ||
| )} | ||
| <div className={cn('flex-1 overflow-auto', className)}>{children}</div> | ||
| </SidebarInset> | ||
| </SidebarProvider> | ||
| ); | ||
| } | ||
| export default SidebarLayoutWrapper; |
+1
-1
| { | ||
| "name": "@nixopus/ui", | ||
| "version": "0.1.28", | ||
| "version": "0.1.29", | ||
| "description": "Nixopus UI component library - Pure React components for building Nixopus applications", | ||
@@ -5,0 +5,0 @@ "main": "./dist/index.js", |
+9
-0
@@ -76,2 +76,3 @@ // Core UI Components (Pure Components) | ||
| export * from './components/wrapper/logs-table-wrapper'; | ||
| export * from './components/wrapper/sidebar-layout-wrapper'; | ||
@@ -114,2 +115,10 @@ // Default exports for pure components | ||
| } from './components/wrapper/repository-card-wrapper'; | ||
| export { SidebarLayoutWrapper } from './components/wrapper/sidebar-layout-wrapper'; | ||
| export type { | ||
| SidebarLayoutWrapperProps, | ||
| SidebarConfig, | ||
| SidebarNavItem, | ||
| TopBarConfig, | ||
| BreadcrumbItem | ||
| } from './components/wrapper/sidebar-layout-wrapper'; | ||
@@ -116,0 +125,0 @@ // Default export - TabsWrapper (for default import syntax: import TabsWrapper, { TabsWrapperList }) |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Unidentified License
LicenseSomething that seems like a license was found, but its contents could not be matched with a known license.
Unidentified License
LicenseSomething that seems like a license was found, but its contents could not be matched with a known license.
1811425
5.3%90
1.12%24127
4.55%