refactor(App): update layout and styling for vertical sidebar navigation and page header, enhancing user experience and accessibility
Publish Fast Tabler Docker image / build-and-push-fast (push) Failing after 1m32s
Publish Fast Tabler Docker image / build-and-push-fast (push) Failing after 1m32s
This commit is contained in:
+40
-1
@@ -1,5 +1,44 @@
|
||||
/* Custom styles for S3 Lists Manager */
|
||||
/* Most styles are now provided by @tabler/core */
|
||||
/* Layout: Tabler Fluid Vertical (https://preview.tabler.io/layout-fluid-vertical.html) */
|
||||
|
||||
/* --- Fluid Vertical: page structure --- */
|
||||
.page {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* Sidebar: nav-link with icon (Tabler vertical) */
|
||||
.navbar-vertical .nav-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
.navbar-vertical .nav-link-icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.navbar-vertical .nav-link-icon .icon {
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
}
|
||||
/* Sidebar dropdown: inline submenu (no popup) */
|
||||
.navbar-vertical .nav-item.dropdown .dropdown-menu {
|
||||
position: static;
|
||||
float: none;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
background: transparent;
|
||||
padding: 0 0 0.5rem 0;
|
||||
margin: 0;
|
||||
}
|
||||
.navbar-vertical .nav-item.dropdown .dropdown-item {
|
||||
padding-left: calc(0.5rem + 1.25rem + 0.5rem);
|
||||
padding-top: 0.35rem;
|
||||
padding-bottom: 0.35rem;
|
||||
}
|
||||
|
||||
/* Custom avatar background */
|
||||
.avatar {
|
||||
|
||||
+130
-123
@@ -233,113 +233,117 @@ function MainLayout() {
|
||||
|
||||
const activeTab = getActiveTab();
|
||||
|
||||
// Fluid Vertical: вертикальный сайдбар + верхний навбар + page-wrapper
|
||||
return (
|
||||
<div className="page d-flex flex-column min-vh-100">
|
||||
<div className="page">
|
||||
{/* Skip link для доступности */}
|
||||
<a href="#main-content" className="skip-link">
|
||||
Перейти к основному содержанию
|
||||
</a>
|
||||
|
||||
{/* Верхнее меню Tabler Admin */}
|
||||
<header className="navbar navbar-expand-md navbar-light d-print-none" style={{boxShadow: '0 1px 0 0 #e9ecef'}}>
|
||||
<div className="container-xl">
|
||||
{/* Логотип */}
|
||||
<div className="navbar-brand navbar-brand-autodark d-none-navbar-horizontal pe-0 pe-md-3">
|
||||
<Link to="/" className="navbar-brand d-flex align-items-center">
|
||||
<IconBrandTabler className="navbar-brand-icon me-2" />
|
||||
<span className="fw-bold">S3 Lists Manager</span>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Кнопка бургера для мобильных */}
|
||||
{/* Левая вертикальная навигация (Tabler navbar-vertical) */}
|
||||
<header className="navbar navbar-vertical navbar-expand-md d-print-none">
|
||||
<div className="container-fluid">
|
||||
<Link to="/" className="navbar-brand navbar-brand-autodark">
|
||||
<IconBrandTabler className="navbar-brand-icon" />
|
||||
<span className="fw-bold">S3 Lists Manager</span>
|
||||
</Link>
|
||||
<div className="navbar-nav flex-row d-md-none">
|
||||
<button
|
||||
className="navbar-toggler"
|
||||
type="button"
|
||||
data-bs-toggle="collapse"
|
||||
data-bs-target="#sidebar-menu"
|
||||
aria-controls="sidebar-menu"
|
||||
aria-expanded="false"
|
||||
aria-label="Открыть меню"
|
||||
>
|
||||
<IconMenu2 />
|
||||
</button>
|
||||
</div>
|
||||
<div className="collapse navbar-collapse" id="sidebar-menu">
|
||||
<ul className="navbar-nav pt-lg-3">
|
||||
{navCategories.map(category => (
|
||||
<li key={category.id} className={`nav-item${category.items ? ' dropdown' : ''}${(category.single && activeTab === category.id) || (category.items && category.items.some(i => activeTab === i.id)) ? ' active' : ''}`}>
|
||||
{category.single ? (
|
||||
<Link
|
||||
to={category.path}
|
||||
className="nav-link"
|
||||
>
|
||||
<span className="nav-link-icon">
|
||||
<category.icon className="icon" />
|
||||
</span>
|
||||
<span className="nav-link-title">{category.title}</span>
|
||||
</Link>
|
||||
) : (
|
||||
<>
|
||||
<a
|
||||
className="nav-link dropdown-toggle"
|
||||
href="#"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
toggleDropdown(category.id);
|
||||
}}
|
||||
aria-expanded={dropdownStates[category.id]}
|
||||
>
|
||||
<span className="nav-link-icon">
|
||||
<category.icon className="icon" />
|
||||
</span>
|
||||
<span className="nav-link-title">{category.title}</span>
|
||||
</a>
|
||||
<div className={`dropdown-menu${dropdownStates[category.id] ? ' show' : ''}`}>
|
||||
{category.items.map(item => (
|
||||
<Link
|
||||
key={item.id}
|
||||
to={item.path}
|
||||
className={`dropdown-item${activeTab === item.id ? ' active' : ''}`}
|
||||
>
|
||||
{item.title}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Верхний навбар (как в Tabler fluid vertical) */}
|
||||
<header className="navbar navbar-expand-md d-print-none">
|
||||
<div className="container-fluid">
|
||||
<button
|
||||
className="navbar-toggler d-md-none"
|
||||
type="button"
|
||||
data-bs-toggle="collapse"
|
||||
data-bs-target="#navbar-menu"
|
||||
aria-controls="navbar-menu"
|
||||
aria-expanded="false"
|
||||
aria-label="Toggle navigation"
|
||||
data-bs-target="#sidebar-menu"
|
||||
aria-controls="sidebar-menu"
|
||||
aria-label="Меню"
|
||||
>
|
||||
<IconMenu2 />
|
||||
</button>
|
||||
|
||||
{/* Основное меню */}
|
||||
<div className="collapse navbar-collapse" id="navbar-menu">
|
||||
<div className="d-flex flex-column flex-md-row flex-fill align-items-stretch align-items-md-center">
|
||||
<ul className="navbar-nav">
|
||||
{navCategories.map(category => (
|
||||
<li key={category.id} className="nav-item dropdown">
|
||||
{category.single ? (
|
||||
<Link
|
||||
to={category.path}
|
||||
className={`nav-link${activeTab === category.id ? ' active' : ''}`}
|
||||
>
|
||||
<category.icon className="icon icon-tabler" />
|
||||
<span className="nav-link-title">{category.title}</span>
|
||||
</Link>
|
||||
) : (
|
||||
<>
|
||||
<a
|
||||
className={`nav-link dropdown-toggle${activeTab.startsWith(category.id) ? ' active' : ''}`}
|
||||
href="#"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
toggleDropdown(category.id);
|
||||
}}
|
||||
>
|
||||
<category.icon className="icon icon-tabler" />
|
||||
<span className="nav-link-title">{category.title}</span>
|
||||
</a>
|
||||
{dropdownStates[category.id] && (
|
||||
<div className="dropdown-menu show" onClick={(e) => e.stopPropagation()}>
|
||||
<div className="dropdown-menu-columns">
|
||||
<div className="dropdown-menu-column">
|
||||
{category.items.map(item => (
|
||||
<Link
|
||||
key={item.id}
|
||||
to={item.path}
|
||||
className={`dropdown-item${activeTab === item.id ? ' active' : ''}`}
|
||||
>
|
||||
<item.icon className="icon icon-tabler" />
|
||||
{item.title}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Правая часть навбара: язык, тема и действия */}
|
||||
<div className="navbar-nav flex-row order-md-last">
|
||||
{/* Язык */}
|
||||
<div className="nav-item dropdown me-1">
|
||||
<a
|
||||
href="#"
|
||||
className="nav-link px-2 dropdown-toggle"
|
||||
<div className="navbar-nav flex-row order-md-last flex-grow-1 justify-content-end">
|
||||
<div className="nav-item dropdown">
|
||||
<a
|
||||
href="#"
|
||||
className="nav-link px-2 dropdown-toggle"
|
||||
data-bs-toggle="dropdown"
|
||||
onClick={(e) => e.preventDefault()}
|
||||
>
|
||||
<span className="badge">{lang.toUpperCase()}</span>
|
||||
</a>
|
||||
<div className="dropdown-menu dropdown-menu-end">
|
||||
<a
|
||||
href="#"
|
||||
<a
|
||||
href="#"
|
||||
className={`dropdown-item ${lang === 'ru' ? 'active' : ''}`}
|
||||
onClick={(e) => { e.preventDefault(); setLang('ru'); }}
|
||||
>
|
||||
Русский
|
||||
</a>
|
||||
<a
|
||||
href="#"
|
||||
<a
|
||||
href="#"
|
||||
className={`dropdown-item ${lang === 'en' ? 'active' : ''}`}
|
||||
onClick={(e) => { e.preventDefault(); setLang('en'); }}
|
||||
>
|
||||
@@ -347,27 +351,25 @@ function MainLayout() {
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Тема */}
|
||||
<div className="nav-item dropdown me-1">
|
||||
<a
|
||||
href="#"
|
||||
className="nav-link px-2 dropdown-toggle"
|
||||
<div className="nav-item dropdown">
|
||||
<a
|
||||
href="#"
|
||||
className="nav-link px-2 dropdown-toggle"
|
||||
data-bs-toggle="dropdown"
|
||||
onClick={(e) => e.preventDefault()}
|
||||
>
|
||||
{theme === 'dark' ? '🌙' : '☀️'}
|
||||
</a>
|
||||
<div className="dropdown-menu dropdown-menu-end">
|
||||
<a
|
||||
href="#"
|
||||
<a
|
||||
href="#"
|
||||
className={`dropdown-item ${theme === 'light' ? 'active' : ''}`}
|
||||
onClick={(e) => { e.preventDefault(); setTheme('light'); }}
|
||||
>
|
||||
☀️ {t('light')}
|
||||
</a>
|
||||
<a
|
||||
href="#"
|
||||
<a
|
||||
href="#"
|
||||
className={`dropdown-item ${theme === 'dark' ? 'active' : ''}`}
|
||||
onClick={(e) => { e.preventDefault(); setTheme('dark'); }}
|
||||
>
|
||||
@@ -375,17 +377,13 @@ function MainLayout() {
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Горячие клавиши */}
|
||||
<div className="nav-item me-1">
|
||||
<div className="nav-item">
|
||||
<KeyboardShortcutsButton className="nav-link px-2" />
|
||||
</div>
|
||||
|
||||
{/* Настройки */}
|
||||
<div className="nav-item">
|
||||
<a
|
||||
href="#"
|
||||
className="nav-link px-2"
|
||||
<a
|
||||
href="#"
|
||||
className="nav-link px-2"
|
||||
onClick={(e) => { e.preventDefault(); setSettingsOpen(true); }}
|
||||
title="Настройки"
|
||||
>
|
||||
@@ -395,29 +393,38 @@ function MainLayout() {
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<main id="main-content" className="container-xl mt-4 flex-grow-1" tabIndex="-1">
|
||||
<Routes>
|
||||
<Route path="/dashboard" element={<Dashboard />} />
|
||||
<Route path="/domains" element={<DomainsNewManager />} />
|
||||
<Route path="/ip-ranges" element={<IPRangesManager />} />
|
||||
<Route path="/asns" element={<ASNsNewManager />} />
|
||||
<Route path="/communities" element={<CommunitiesManager />} />
|
||||
<Route path="/auto-urls" element={<AutoUrlManager />} />
|
||||
<Route path="/servers" element={<ServerManager />} />
|
||||
<Route path="/billing" element={<BillingManager />} />
|
||||
<Route path="/filters" element={<FilterManager />} />
|
||||
<Route path="/network-config" element={<NetworkConfigManager />} />
|
||||
<Route path="/easy-switch" element={<EasySwitchManager />} />
|
||||
<Route path="/mikrotik-backups" element={<MikrotikBackupsManager />} />
|
||||
<Route path="/mikrotik-tools" element={<MikrotikTools />} />
|
||||
<Route path="/" element={<Navigate to="/dashboard" replace />} />
|
||||
</Routes>
|
||||
</main>
|
||||
<footer className="footer mt-auto py-2">
|
||||
<div className="container-xl text-muted small">
|
||||
Версия UI: {import.meta?.env?.VITE_APP_VERSION || 'dev'}
|
||||
|
||||
<div className="page-wrapper">
|
||||
<div className="page-body">
|
||||
<div className="container-fluid">
|
||||
<main id="main-content" tabIndex="-1">
|
||||
<Routes>
|
||||
<Route path="/dashboard" element={<Dashboard />} />
|
||||
<Route path="/domains" element={<DomainsNewManager />} />
|
||||
<Route path="/ip-ranges" element={<IPRangesManager />} />
|
||||
<Route path="/asns" element={<ASNsNewManager />} />
|
||||
<Route path="/communities" element={<CommunitiesManager />} />
|
||||
<Route path="/auto-urls" element={<AutoUrlManager />} />
|
||||
<Route path="/servers" element={<ServerManager />} />
|
||||
<Route path="/billing" element={<BillingManager />} />
|
||||
<Route path="/filters" element={<FilterManager />} />
|
||||
<Route path="/network-config" element={<NetworkConfigManager />} />
|
||||
<Route path="/easy-switch" element={<EasySwitchManager />} />
|
||||
<Route path="/mikrotik-backups" element={<MikrotikBackupsManager />} />
|
||||
<Route path="/mikrotik-tools" element={<MikrotikTools />} />
|
||||
<Route path="/" element={<Navigate to="/dashboard" replace />} />
|
||||
</Routes>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
<footer className="footer footer-transparent d-print-none">
|
||||
<div className="container-fluid">
|
||||
<ul className="list-inline list-inline-dots mb-0 text-muted small">
|
||||
<li className="list-inline-item">Версия UI: {import.meta?.env?.VITE_APP_VERSION || 'dev'}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
<SettingsModal open={settingsOpen} onClose={() => setSettingsOpen(false)} />
|
||||
<CommandPalette />
|
||||
</div>
|
||||
|
||||
@@ -1,26 +1,25 @@
|
||||
import React from 'react'
|
||||
|
||||
/** Заголовок страницы в стиле Tabler (page-header + page-title + опционально actions справа) */
|
||||
function PageHeader({ title, pretitle, actions, meta }) {
|
||||
return (
|
||||
<div className="page-header d-print-none mb-4">
|
||||
{/* Верхняя строка: только заголовок */}
|
||||
<div className="row align-items-center">
|
||||
<div className="col">
|
||||
<h2 className="page-title">{title}</h2>
|
||||
{pretitle && (
|
||||
<div className="page-pretitle mt-1">{pretitle}</div>
|
||||
<div className="page-pretitle">{pretitle}</div>
|
||||
)}
|
||||
<h1 className="page-title">{title}</h1>
|
||||
{meta && (
|
||||
<div className="text-muted small mt-1">{meta}</div>
|
||||
)}
|
||||
</div>
|
||||
{actions && (
|
||||
<div className="col-auto ms-auto d-print-none">
|
||||
{actions}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{/* Отдельная строка под заголовком: действия страницы */}
|
||||
{actions && (
|
||||
<div className="mt-4 d-print-none">
|
||||
{actions}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user