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 */
|
/* 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 */
|
/* Custom avatar background */
|
||||||
.avatar {
|
.avatar {
|
||||||
|
|||||||
+130
-123
@@ -233,113 +233,117 @@ function MainLayout() {
|
|||||||
|
|
||||||
const activeTab = getActiveTab();
|
const activeTab = getActiveTab();
|
||||||
|
|
||||||
|
// Fluid Vertical: вертикальный сайдбар + верхний навбар + page-wrapper
|
||||||
return (
|
return (
|
||||||
<div className="page d-flex flex-column min-vh-100">
|
<div className="page">
|
||||||
{/* Skip link для доступности */}
|
{/* Skip link для доступности */}
|
||||||
<a href="#main-content" className="skip-link">
|
<a href="#main-content" className="skip-link">
|
||||||
Перейти к основному содержанию
|
Перейти к основному содержанию
|
||||||
</a>
|
</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
|
<button
|
||||||
className="navbar-toggler d-md-none"
|
className="navbar-toggler d-md-none"
|
||||||
type="button"
|
type="button"
|
||||||
data-bs-toggle="collapse"
|
data-bs-toggle="collapse"
|
||||||
data-bs-target="#navbar-menu"
|
data-bs-target="#sidebar-menu"
|
||||||
aria-controls="navbar-menu"
|
aria-controls="sidebar-menu"
|
||||||
aria-expanded="false"
|
aria-label="Меню"
|
||||||
aria-label="Toggle navigation"
|
|
||||||
>
|
>
|
||||||
<IconMenu2 />
|
<IconMenu2 />
|
||||||
</button>
|
</button>
|
||||||
|
<div className="navbar-nav flex-row order-md-last flex-grow-1 justify-content-end">
|
||||||
{/* Основное меню */}
|
<div className="nav-item dropdown">
|
||||||
<div className="collapse navbar-collapse" id="navbar-menu">
|
<a
|
||||||
<div className="d-flex flex-column flex-md-row flex-fill align-items-stretch align-items-md-center">
|
href="#"
|
||||||
<ul className="navbar-nav">
|
className="nav-link px-2 dropdown-toggle"
|
||||||
{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"
|
|
||||||
data-bs-toggle="dropdown"
|
data-bs-toggle="dropdown"
|
||||||
onClick={(e) => e.preventDefault()}
|
onClick={(e) => e.preventDefault()}
|
||||||
>
|
>
|
||||||
<span className="badge">{lang.toUpperCase()}</span>
|
<span className="badge">{lang.toUpperCase()}</span>
|
||||||
</a>
|
</a>
|
||||||
<div className="dropdown-menu dropdown-menu-end">
|
<div className="dropdown-menu dropdown-menu-end">
|
||||||
<a
|
<a
|
||||||
href="#"
|
href="#"
|
||||||
className={`dropdown-item ${lang === 'ru' ? 'active' : ''}`}
|
className={`dropdown-item ${lang === 'ru' ? 'active' : ''}`}
|
||||||
onClick={(e) => { e.preventDefault(); setLang('ru'); }}
|
onClick={(e) => { e.preventDefault(); setLang('ru'); }}
|
||||||
>
|
>
|
||||||
Русский
|
Русский
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
href="#"
|
href="#"
|
||||||
className={`dropdown-item ${lang === 'en' ? 'active' : ''}`}
|
className={`dropdown-item ${lang === 'en' ? 'active' : ''}`}
|
||||||
onClick={(e) => { e.preventDefault(); setLang('en'); }}
|
onClick={(e) => { e.preventDefault(); setLang('en'); }}
|
||||||
>
|
>
|
||||||
@@ -347,27 +351,25 @@ function MainLayout() {
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="nav-item dropdown">
|
||||||
{/* Тема */}
|
<a
|
||||||
<div className="nav-item dropdown me-1">
|
href="#"
|
||||||
<a
|
className="nav-link px-2 dropdown-toggle"
|
||||||
href="#"
|
|
||||||
className="nav-link px-2 dropdown-toggle"
|
|
||||||
data-bs-toggle="dropdown"
|
data-bs-toggle="dropdown"
|
||||||
onClick={(e) => e.preventDefault()}
|
onClick={(e) => e.preventDefault()}
|
||||||
>
|
>
|
||||||
{theme === 'dark' ? '🌙' : '☀️'}
|
{theme === 'dark' ? '🌙' : '☀️'}
|
||||||
</a>
|
</a>
|
||||||
<div className="dropdown-menu dropdown-menu-end">
|
<div className="dropdown-menu dropdown-menu-end">
|
||||||
<a
|
<a
|
||||||
href="#"
|
href="#"
|
||||||
className={`dropdown-item ${theme === 'light' ? 'active' : ''}`}
|
className={`dropdown-item ${theme === 'light' ? 'active' : ''}`}
|
||||||
onClick={(e) => { e.preventDefault(); setTheme('light'); }}
|
onClick={(e) => { e.preventDefault(); setTheme('light'); }}
|
||||||
>
|
>
|
||||||
☀️ {t('light')}
|
☀️ {t('light')}
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
href="#"
|
href="#"
|
||||||
className={`dropdown-item ${theme === 'dark' ? 'active' : ''}`}
|
className={`dropdown-item ${theme === 'dark' ? 'active' : ''}`}
|
||||||
onClick={(e) => { e.preventDefault(); setTheme('dark'); }}
|
onClick={(e) => { e.preventDefault(); setTheme('dark'); }}
|
||||||
>
|
>
|
||||||
@@ -375,17 +377,13 @@ function MainLayout() {
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="nav-item">
|
||||||
{/* Горячие клавиши */}
|
|
||||||
<div className="nav-item me-1">
|
|
||||||
<KeyboardShortcutsButton className="nav-link px-2" />
|
<KeyboardShortcutsButton className="nav-link px-2" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Настройки */}
|
|
||||||
<div className="nav-item">
|
<div className="nav-item">
|
||||||
<a
|
<a
|
||||||
href="#"
|
href="#"
|
||||||
className="nav-link px-2"
|
className="nav-link px-2"
|
||||||
onClick={(e) => { e.preventDefault(); setSettingsOpen(true); }}
|
onClick={(e) => { e.preventDefault(); setSettingsOpen(true); }}
|
||||||
title="Настройки"
|
title="Настройки"
|
||||||
>
|
>
|
||||||
@@ -395,29 +393,38 @@ function MainLayout() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<main id="main-content" className="container-xl mt-4 flex-grow-1" tabIndex="-1">
|
|
||||||
<Routes>
|
<div className="page-wrapper">
|
||||||
<Route path="/dashboard" element={<Dashboard />} />
|
<div className="page-body">
|
||||||
<Route path="/domains" element={<DomainsNewManager />} />
|
<div className="container-fluid">
|
||||||
<Route path="/ip-ranges" element={<IPRangesManager />} />
|
<main id="main-content" tabIndex="-1">
|
||||||
<Route path="/asns" element={<ASNsNewManager />} />
|
<Routes>
|
||||||
<Route path="/communities" element={<CommunitiesManager />} />
|
<Route path="/dashboard" element={<Dashboard />} />
|
||||||
<Route path="/auto-urls" element={<AutoUrlManager />} />
|
<Route path="/domains" element={<DomainsNewManager />} />
|
||||||
<Route path="/servers" element={<ServerManager />} />
|
<Route path="/ip-ranges" element={<IPRangesManager />} />
|
||||||
<Route path="/billing" element={<BillingManager />} />
|
<Route path="/asns" element={<ASNsNewManager />} />
|
||||||
<Route path="/filters" element={<FilterManager />} />
|
<Route path="/communities" element={<CommunitiesManager />} />
|
||||||
<Route path="/network-config" element={<NetworkConfigManager />} />
|
<Route path="/auto-urls" element={<AutoUrlManager />} />
|
||||||
<Route path="/easy-switch" element={<EasySwitchManager />} />
|
<Route path="/servers" element={<ServerManager />} />
|
||||||
<Route path="/mikrotik-backups" element={<MikrotikBackupsManager />} />
|
<Route path="/billing" element={<BillingManager />} />
|
||||||
<Route path="/mikrotik-tools" element={<MikrotikTools />} />
|
<Route path="/filters" element={<FilterManager />} />
|
||||||
<Route path="/" element={<Navigate to="/dashboard" replace />} />
|
<Route path="/network-config" element={<NetworkConfigManager />} />
|
||||||
</Routes>
|
<Route path="/easy-switch" element={<EasySwitchManager />} />
|
||||||
</main>
|
<Route path="/mikrotik-backups" element={<MikrotikBackupsManager />} />
|
||||||
<footer className="footer mt-auto py-2">
|
<Route path="/mikrotik-tools" element={<MikrotikTools />} />
|
||||||
<div className="container-xl text-muted small">
|
<Route path="/" element={<Navigate to="/dashboard" replace />} />
|
||||||
Версия UI: {import.meta?.env?.VITE_APP_VERSION || 'dev'}
|
</Routes>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
</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)} />
|
<SettingsModal open={settingsOpen} onClose={() => setSettingsOpen(false)} />
|
||||||
<CommandPalette />
|
<CommandPalette />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,26 +1,25 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
|
/** Заголовок страницы в стиле Tabler (page-header + page-title + опционально actions справа) */
|
||||||
function PageHeader({ title, pretitle, actions, meta }) {
|
function PageHeader({ title, pretitle, actions, meta }) {
|
||||||
return (
|
return (
|
||||||
<div className="page-header d-print-none mb-4">
|
<div className="page-header d-print-none mb-4">
|
||||||
{/* Верхняя строка: только заголовок */}
|
|
||||||
<div className="row align-items-center">
|
<div className="row align-items-center">
|
||||||
<div className="col">
|
<div className="col">
|
||||||
<h2 className="page-title">{title}</h2>
|
|
||||||
{pretitle && (
|
{pretitle && (
|
||||||
<div className="page-pretitle mt-1">{pretitle}</div>
|
<div className="page-pretitle">{pretitle}</div>
|
||||||
)}
|
)}
|
||||||
|
<h1 className="page-title">{title}</h1>
|
||||||
{meta && (
|
{meta && (
|
||||||
<div className="text-muted small mt-1">{meta}</div>
|
<div className="text-muted small mt-1">{meta}</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
{actions && (
|
||||||
|
<div className="col-auto ms-auto d-print-none">
|
||||||
|
{actions}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{/* Отдельная строка под заголовком: действия страницы */}
|
|
||||||
{actions && (
|
|
||||||
<div className="mt-4 d-print-none">
|
|
||||||
{actions}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user