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

This commit is contained in:
2026-02-16 15:16:13 +07:00
parent 8a8a55e7b6
commit b87af004ee
3 changed files with 178 additions and 133 deletions
+40 -1
View File
@@ -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
View File
@@ -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>
+8 -9
View File
@@ -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>
)
}