feat: add configurable sidebar and top navigation layout modes commit

This commit is contained in:
mengstabketemaw
2026-06-19 16:17:01 +03:00
parent ddd91b6125
commit 1f27a36b90
6 changed files with 196 additions and 67 deletions

View File

@@ -1,10 +1,11 @@
import { useCallback } from 'react';
import { useCallback, useState } from 'react';
import { AppShell, rem } from '@mantine/core';
import { useDisclosure } from '@mantine/hooks';
import { Outlet, useLocation, useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { logout } from '@ema-platform/auth';
import { AppHeader } from '@ema-platform/ui';
import { AppHeader, AppSidebar } from '@ema-platform/ui';
import type { NavItem } from '@ema-platform/ui';
import {
IconLayoutDashboard,
IconUsers,
@@ -14,14 +15,6 @@ import {
import { notify } from '@ema-platform/ui';
import { SUPPORTED_LANGUAGES } from '../i18n/config';
import { useAppDispatch, useAppSelector } from '../store/hooks';
import type { Icon } from '@tabler/icons-react';
interface NavItem {
label: string;
icon: Icon;
to?: string;
soon?: boolean;
}
const NAV_ITEMS: NavItem[] = [
{ to: '/dashboard', label: 'Dashboard', icon: IconLayoutDashboard },
@@ -38,7 +31,9 @@ export function BackofficeLayout() {
const location = useLocation();
const dispatch = useAppDispatch();
const [opened, { toggle: toggleNav }] = useDisclosure();
const [collapsed, setCollapsed] = useState(false);
const user = useAppSelector((state) => state.auth.user);
const layoutMode = useAppSelector((state) => state.preferences.layoutMode);
const displayName = user?.name?.en || user?.username || '';
const initials = displayName
@@ -70,9 +65,20 @@ export function BackofficeLayout() {
}
};
const handleToggleCollapse = useCallback(() => {
setCollapsed((prev) => !prev);
}, []);
const isSidebar = layoutMode === 'sidebar';
return (
<AppShell
header={{ height: HEADER_HEIGHT }}
header={{ height: isSidebar ? 74 : HEADER_HEIGHT }}
navbar={isSidebar ? {
width: collapsed ? 72 : 264,
breakpoint: 'sm',
collapsed: { mobile: !opened },
} : undefined}
padding="lg"
>
<AppShell.Header
@@ -83,11 +89,10 @@ export function BackofficeLayout() {
flexDirection: 'column',
}}
>
{/* Top bar */}
<div style={{ height: 74, flexShrink: 0, padding: '0 32px' }}>
<AppHeader
onToggleNav={toggleNav}
onToggleSidebar={toggleNav}
onToggleSidebar={isSidebar ? handleToggleCollapse : toggleNav}
navOpened={opened}
breadcrumbs={crumbs}
onNavigate={navigate}
@@ -98,63 +103,87 @@ export function BackofficeLayout() {
/>
</div>
{/* Legacy-style tab bar — matching UM AppMenuTabs look */}
<div
{!isSidebar && (
<div
style={{
display: 'flex',
alignItems: 'center',
gap: rem(2),
padding: '0 32px',
height: 42,
borderTop: '1px solid var(--mantine-color-gray-1)',
overflowX: 'auto',
flexShrink: 0,
}}
>
{NAV_ITEMS.map((item) => {
const active = !!item.to && (location.pathname === item.to || location.pathname.startsWith(`${item.to}/`));
const ItemIcon = item.icon;
return (
<button
key={item.to}
onClick={() => go(item)}
style={{
display: 'flex',
alignItems: 'center',
gap: rem(6),
padding: '8px 16px',
border: 'none',
borderBottom: '2px solid',
borderBottomColor: active
? 'var(--mantine-color-blue-6)'
: 'transparent',
background: 'transparent',
color: active
? 'var(--mantine-color-blue-6)'
: 'var(--mantine-color-gray-6)',
fontWeight: active ? 600 : 500,
fontSize: rem(14),
whiteSpace: 'nowrap',
cursor: 'pointer',
transition: 'all 150ms ease',
height: '100%',
marginBottom: -1,
fontFamily: 'inherit',
}}
onMouseEnter={(e) => {
if (!active) e.currentTarget.style.color = 'var(--mantine-color-blue-6)';
}}
onMouseLeave={(e) => {
if (!active) e.currentTarget.style.color = 'var(--mantine-color-gray-6)';
}}
>
<ItemIcon size={18} stroke={1.6} />
<span>{item.label}</span>
</button>
);
})}
</div>
)}
</AppShell.Header>
{isSidebar && (
<AppShell.Navbar
p={0}
style={{
display: 'flex',
alignItems: 'center',
gap: rem(2),
padding: '0 32px',
height: 42,
borderTop: '1px solid var(--mantine-color-gray-1)',
overflowX: 'auto',
flexShrink: 0,
overflow: 'hidden',
transition: 'width 200ms ease',
background: 'var(--mantine-color-body)',
borderRight: '1px solid var(--mantine-color-gray-2)',
}}
>
{NAV_ITEMS.map((item) => {
const active = !!item.to && (location.pathname === item.to || location.pathname.startsWith(`${item.to}/`));
const ItemIcon = item.icon;
return (
<button
key={item.to}
onClick={() => go(item)}
style={{
display: 'flex',
alignItems: 'center',
gap: rem(6),
padding: '8px 16px',
border: 'none',
borderBottom: '2px solid',
borderBottomColor: active
? 'var(--mantine-color-blue-6)'
: 'transparent',
background: 'transparent',
color: active
? 'var(--mantine-color-blue-6)'
: 'var(--mantine-color-gray-6)',
fontWeight: active ? 600 : 500,
fontSize: rem(14),
whiteSpace: 'nowrap',
cursor: 'pointer',
transition: 'all 150ms ease',
height: '100%',
marginBottom: -1,
fontFamily: 'inherit',
}}
onMouseEnter={(e) => {
if (!active) e.currentTarget.style.color = 'var(--mantine-color-blue-6)';
}}
onMouseLeave={(e) => {
if (!active) e.currentTarget.style.color = 'var(--mantine-color-gray-6)';
}}
>
<ItemIcon size={18} stroke={1.6} />
<span>{item.label}</span>
</button>
);
})}
</div>
</AppShell.Header>
<AppSidebar
navItems={NAV_ITEMS}
collapsed={collapsed}
activePath={location.pathname}
onToggleCollapse={handleToggleCollapse}
onNavigate={go}
brandName={t('app.name')}
brandSubtitle={t('app.authority')}
/>
</AppShell.Navbar>
)}
<AppShell.Main>
<div key={location.pathname} className="ema-page-enter">
<Outlet />