mirror of
https://github.com/Tria-plc/emaui.git
synced 2026-09-08 05:25:43 +00:00
merge design protal and backoffice
This commit is contained in:
196
libs/ui/src/lib/layout/AppSidebar.tsx
Normal file
196
libs/ui/src/lib/layout/AppSidebar.tsx
Normal file
@@ -0,0 +1,196 @@
|
||||
import {
|
||||
AppShell,
|
||||
NavLink,
|
||||
ScrollArea,
|
||||
Stack,
|
||||
Text,
|
||||
Tooltip,
|
||||
UnstyledButton,
|
||||
rem,
|
||||
} from '@mantine/core';
|
||||
import {
|
||||
IconChevronLeft,
|
||||
IconChevronRight,
|
||||
} from '@tabler/icons-react';
|
||||
import type { Icon } from '@tabler/icons-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { BrandMark } from '@ema-platform/auth';
|
||||
|
||||
export interface NavItem {
|
||||
label: string;
|
||||
icon: Icon;
|
||||
to?: string;
|
||||
soon?: boolean;
|
||||
}
|
||||
|
||||
interface AppSidebarProps {
|
||||
navItems: NavItem[];
|
||||
collapsed: boolean;
|
||||
activePath: string;
|
||||
onToggleCollapse: () => void;
|
||||
onNavigate: (item: NavItem) => void;
|
||||
brandName: string;
|
||||
brandSubtitle: string;
|
||||
}
|
||||
|
||||
export function AppSidebar({
|
||||
navItems,
|
||||
collapsed,
|
||||
activePath,
|
||||
onToggleCollapse,
|
||||
onNavigate,
|
||||
brandName,
|
||||
brandSubtitle,
|
||||
}: AppSidebarProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const activeNavItem = (item: NavItem) =>
|
||||
!!item.to &&
|
||||
(activePath === item.to || activePath.startsWith(`${item.to}/`));
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Brand header */}
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: rem(12),
|
||||
height: 74,
|
||||
padding: collapsed ? '0 12px' : '0 20px',
|
||||
borderBottom: '1px solid var(--mantine-color-gray-2)',
|
||||
justifyContent: collapsed ? 'center' : 'flex-start',
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
<BrandMark size={32} />
|
||||
{!collapsed && (
|
||||
<div style={{ minWidth: 0 }}>
|
||||
<Text
|
||||
fw={800}
|
||||
size="xs"
|
||||
style={{
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.08em',
|
||||
lineHeight: 1.2,
|
||||
color: 'var(--mantine-color-primary-7)',
|
||||
}}
|
||||
>
|
||||
{brandName}
|
||||
</Text>
|
||||
<Text
|
||||
size="xs"
|
||||
c="dimmed"
|
||||
style={{
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.05em',
|
||||
fontSize: rem(10),
|
||||
lineHeight: 1.3,
|
||||
}}
|
||||
>
|
||||
{brandSubtitle}
|
||||
</Text>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Navigation items */}
|
||||
<AppShell.Section grow component={ScrollArea} p="md">
|
||||
<Stack gap={4}>
|
||||
{navItems.map((item) => {
|
||||
const ItemIcon = item.icon;
|
||||
const active = activeNavItem(item);
|
||||
|
||||
if (collapsed) {
|
||||
return (
|
||||
<Tooltip key={item.label} label={item.label} position="right" withArrow>
|
||||
<UnstyledButton
|
||||
onClick={() => onNavigate(item)}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
width: '100%',
|
||||
height: rem(40),
|
||||
borderRadius: rem(10),
|
||||
color: active ? 'var(--mantine-color-blue-6)' : undefined,
|
||||
backgroundColor: active ? 'var(--mantine-color-blue-light)' : undefined,
|
||||
}}
|
||||
>
|
||||
<ItemIcon size={20} stroke={1.6} />
|
||||
</UnstyledButton>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<NavLink
|
||||
key={item.label}
|
||||
active={active}
|
||||
label={item.label}
|
||||
leftSection={<ItemIcon size={19} stroke={1.6} />}
|
||||
onClick={() => onNavigate(item)}
|
||||
variant="light"
|
||||
styles={{ root: { borderRadius: rem(10) }, label: { fontWeight: 500 } }}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Stack>
|
||||
</AppShell.Section>
|
||||
|
||||
{/* Collapse toggle */}
|
||||
<div
|
||||
style={{
|
||||
padding: rem(8),
|
||||
borderTop: '1px solid var(--mantine-color-gray-2)',
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
<Tooltip
|
||||
label={collapsed ? t('nav.expandSidebar', 'Expand sidebar') : t('nav.collapseSidebar', 'Collapse sidebar')}
|
||||
position="right"
|
||||
withArrow
|
||||
disabled={!collapsed}
|
||||
>
|
||||
<UnstyledButton
|
||||
onClick={onToggleCollapse}
|
||||
visibleFrom="sm"
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: collapsed ? 'center' : 'flex-start',
|
||||
gap: rem(10),
|
||||
width: '100%',
|
||||
padding: '10px 12px',
|
||||
borderRadius: rem(12),
|
||||
color: 'var(--mantine-color-gray-5)',
|
||||
transition: 'all 150ms ease',
|
||||
cursor: 'pointer',
|
||||
border: 'none',
|
||||
background: 'transparent',
|
||||
fontSize: rem(13),
|
||||
fontWeight: 500,
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.background = 'var(--mantine-color-gray-0)';
|
||||
e.currentTarget.style.color = 'var(--mantine-color-gray-7)';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.background = 'transparent';
|
||||
e.currentTarget.style.color = 'var(--mantine-color-gray-5)';
|
||||
}}
|
||||
>
|
||||
{collapsed ? (
|
||||
<IconChevronRight size={18} stroke={1.6} />
|
||||
) : (
|
||||
<>
|
||||
<IconChevronLeft size={18} stroke={1.6} />
|
||||
<span>{t('nav.collapseSidebar', 'Collapse')}</span>
|
||||
</>
|
||||
)}
|
||||
</UnstyledButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user