220 lines
6.9 KiB
TypeScript
220 lines
6.9 KiB
TypeScript
import React, { useMemo } from 'react';
|
|
import type { CSSProperties } from 'react';
|
|
import { theme } from '../theme/index';
|
|
import icLauncher from '../assets/ic_launcher_foreground.png';
|
|
|
|
export interface SidebarNavItem {
|
|
id: string;
|
|
label: string;
|
|
icon: React.ReactNode;
|
|
badge?: number;
|
|
}
|
|
|
|
interface SidebarNavProps {
|
|
items: SidebarNavItem[];
|
|
footerItems?: SidebarNavItem[];
|
|
activeId: string;
|
|
collapsed?: boolean;
|
|
onSelect: (id: string) => void;
|
|
onToggleCollapse?: (collapsed: boolean) => void;
|
|
variant?: 'desktop' | 'mobile';
|
|
}
|
|
|
|
const SidebarNav: React.FC<SidebarNavProps> = ({
|
|
items,
|
|
footerItems,
|
|
activeId,
|
|
collapsed = false,
|
|
onSelect,
|
|
onToggleCollapse,
|
|
variant = 'desktop',
|
|
}) => {
|
|
const containerStyles: CSSProperties = useMemo(
|
|
() => ({
|
|
width: collapsed ? '92px' : '280px',
|
|
transition: 'width 240ms ease',
|
|
background: 'linear-gradient(180deg, rgba(4, 24, 21, 0.98) 0%, rgba(6, 35, 31, 0.95) 100%)',
|
|
borderRight: variant === 'desktop' ? `1px solid rgba(203, 169, 81, 0.15)` : undefined,
|
|
padding: collapsed ? '28px 12px 24px' : '28px 20px 24px',
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
gap: theme.spacing.md,
|
|
height: variant === 'desktop' ? '100vh' : '100%',
|
|
position: variant === 'desktop' ? 'sticky' : 'relative',
|
|
top: variant === 'desktop' ? 0 : undefined,
|
|
backdropFilter: 'blur(16px)',
|
|
boxShadow: variant === 'mobile' ? theme.shadows.xl : 'none',
|
|
zIndex: variant === 'mobile' ? theme.zIndex.modal : theme.zIndex.sticky,
|
|
}),
|
|
[collapsed, variant],
|
|
);
|
|
|
|
const headerStyles: CSSProperties = useMemo(
|
|
() => ({
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
gap: theme.spacing.md,
|
|
paddingBottom: theme.spacing.lg,
|
|
borderBottom: `1px solid rgba(203, 169, 81, 0.12)`,
|
|
}),
|
|
[],
|
|
);
|
|
|
|
const logoStyles: CSSProperties = {
|
|
width: '48px',
|
|
height: '48px',
|
|
borderRadius: theme.borderRadius.sm,
|
|
backgroundColor: 'rgba(203, 169, 81, 0.12)',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
boxShadow: 'inset 0 0 0 1px rgba(203, 169, 81, 0.2)',
|
|
};
|
|
|
|
const logoTextStyles: CSSProperties = {
|
|
...theme.typography.h4,
|
|
color: theme.colors.textPrimary,
|
|
};
|
|
|
|
const navListStyles: CSSProperties = useMemo(
|
|
() => ({
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
gap: '2px',
|
|
flex: 1,
|
|
overflowY: 'auto',
|
|
paddingRight: collapsed ? 0 : '8px',
|
|
marginTop: theme.spacing.sm,
|
|
}),
|
|
[collapsed],
|
|
);
|
|
|
|
const footerStyles: CSSProperties = {
|
|
marginTop: 'auto',
|
|
paddingTop: theme.spacing.lg,
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
gap: '2px',
|
|
};
|
|
|
|
const dividerStyles: CSSProperties = {
|
|
height: '1px',
|
|
background: 'linear-gradient(90deg, transparent 0%, rgba(203, 169, 81, 0.3) 50%, transparent 100%)',
|
|
marginBottom: theme.spacing.sm,
|
|
};
|
|
|
|
return (
|
|
<aside style={containerStyles}>
|
|
<div style={headerStyles}>
|
|
<div style={logoStyles}>
|
|
<img
|
|
src={icLauncher}
|
|
alt="Pennyzo"
|
|
style={{ width: '60%', height: '60%', objectFit: 'contain' }}
|
|
/>
|
|
</div>
|
|
{!collapsed && <span style={logoTextStyles}>Pennyzo</span>}
|
|
{variant === 'desktop' && onToggleCollapse && (
|
|
<button
|
|
onClick={() => onToggleCollapse(!collapsed)}
|
|
aria-label={collapsed ? 'Expand navigation' : 'Collapse navigation'}
|
|
style={{
|
|
marginLeft: 'auto',
|
|
width: '36px',
|
|
height: '36px',
|
|
borderRadius: theme.borderRadius.md,
|
|
border: `1px solid rgba(203, 169, 81, 0.25)`,
|
|
backgroundColor: 'rgba(203, 169, 81, 0.08)',
|
|
color: theme.colors.highlight,
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
cursor: 'pointer',
|
|
transition: 'all 180ms ease',
|
|
fontSize: '18px',
|
|
fontWeight: 'bold',
|
|
}}
|
|
>
|
|
{collapsed ? '»' : '«'}
|
|
</button>
|
|
)}
|
|
</div>
|
|
|
|
{/* Decorative divider - works in both collapsed and expanded states */}
|
|
<div style={{
|
|
height: '1px',
|
|
background: 'linear-gradient(90deg, transparent 0%, rgba(203, 169, 81, 0.3) 50%, transparent 100%)',
|
|
marginTop: theme.spacing.md,
|
|
marginBottom: theme.spacing.sm,
|
|
}} />
|
|
|
|
<nav
|
|
style={navListStyles}
|
|
className="custom-scrollbar pennyzo-scroll"
|
|
>
|
|
{items.map((item) => {
|
|
const isActive = activeId === item.id;
|
|
return (
|
|
<button
|
|
key={item.id}
|
|
type="button"
|
|
onClick={() => onSelect(item.id)}
|
|
className={`nav-item ${isActive ? 'nav-item-active' : 'nav-item-inactive'} pennyzo-transition`}
|
|
aria-label={item.label}
|
|
title={collapsed ? item.label : undefined}
|
|
>
|
|
{isActive && <div className="nav-item-indicator" />}
|
|
<div className={`nav-icon ${isActive ? 'nav-icon-active' : 'nav-icon-inactive'}`}>
|
|
{item.icon}
|
|
</div>
|
|
{!collapsed && (
|
|
<div style={{ flex: 1, display: 'flex', alignItems: 'center', gap: theme.spacing.sm }}>
|
|
<span className={`nav-label ${isActive ? 'nav-label-active' : 'nav-label-inactive'}`}>
|
|
{item.label}
|
|
</span>
|
|
{item.badge && item.badge > 0 && (
|
|
<span className="pennyzo-badge">{item.badge > 99 ? '99+' : item.badge}</span>
|
|
)}
|
|
</div>
|
|
)}
|
|
</button>
|
|
);
|
|
})}
|
|
</nav>
|
|
|
|
{footerItems && footerItems.length > 0 && (
|
|
<div style={footerStyles}>
|
|
{/* Footer divider */}
|
|
<div style={dividerStyles} />
|
|
|
|
{footerItems.map((item) => {
|
|
const isActive = activeId === item.id;
|
|
return (
|
|
<button
|
|
key={item.id}
|
|
type="button"
|
|
onClick={() => onSelect(item.id)}
|
|
className={`nav-item ${isActive ? 'nav-item-active' : 'nav-item-inactive'} pennyzo-transition`}
|
|
aria-label={item.label}
|
|
title={collapsed ? item.label : undefined}
|
|
>
|
|
{isActive && <div className="nav-item-indicator" />}
|
|
<div className={`nav-icon ${isActive ? 'nav-icon-active' : 'nav-icon-inactive'}`}>
|
|
{item.icon}
|
|
</div>
|
|
{!collapsed && (
|
|
<span className={`nav-label ${isActive ? 'nav-label-active' : 'nav-label-inactive'}`}>
|
|
{item.label}
|
|
</span>
|
|
)}
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
</aside>
|
|
);
|
|
};
|
|
|
|
export default SidebarNav;
|