🎩 You're Invited:Meet the Socket team at Black Hat in Las Vegas, August 3-6.RSVP
Sign In

@nixopus/ui

Package Overview
Dependencies
Maintainers
1
Versions
34
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@nixopus/ui - npm Package Compare versions

Comparing version
0.1.28
to
0.1.29
+469
src/components/wrapper/sidebar-layout-wrapper.tsx
'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",

@@ -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