Меню згруповане; значки у згорнутій панелі центровані
Дванадцять пунктів у стовпчик означали читати всі дванадцять, щоб знайти один. Тепер чотири групи за питанням, з яким відкривають меню: огляд, мережа, налаштування збору, організація. Група без доступних ролі пунктів зникає разом із заголовком; у згорнутому вигляді заголовок замінює риска, а назва групи переїжджає в підказку. Зсув значків мала не розмітка (0.4 px), а смуга прокрутки — вона забирає ширину справа й тягне колонку вліво. Плюс значок сидів у коробці w-4 з паддінгом px-3, тобто з нульовою шириною вмісту, і емодзі різної ширини центрувались випадково. Тепер квадрат h-5 w-5 з flex-центруванням, смуга прокрутки схована. Зсув рівно 0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
d1c9d450f2
commit
43d1f62b29
2 changed files with 169 additions and 50 deletions
51
HISTORY.md
51
HISTORY.md
|
|
@ -2424,3 +2424,54 @@ button, [role='button'], summary {
|
|||
список правил рядки з шаблону підписані «із шаблону», кнопок правки немає
|
||||
міграції 25 на чистій БД, тести сервера з базою — зелені
|
||||
```
|
||||
|
||||
## Меню: групи і центрування
|
||||
|
||||
### Групи
|
||||
|
||||
Дванадцять пунктів у стовпчик означали читати всі дванадцять, щоб знайти
|
||||
один. Тепер вони згруповані за питанням, з яким людина відкриває меню:
|
||||
|
||||
```
|
||||
Огляд що зараз відбувається — щодня й багато разів
|
||||
Мережа що в мене є — коли додають або шукають залізо
|
||||
Налаштування збору як воно налаштоване — рідко, зазвичай при заведенні
|
||||
Організація хто має доступ
|
||||
```
|
||||
|
||||
Група, у якій ролі не доступний жоден пункт, зникає разом із
|
||||
заголовком: заголовок над порожнечею обіцяє розділ, якого немає.
|
||||
|
||||
У згорнутому вигляді заголовок замінює риска — текст туди не влазить, а
|
||||
межа між групами потрібна, інакше значки зливаються в одну стрічку.
|
||||
Назва групи переїжджає в підказку: `Мережа · Хости`.
|
||||
|
||||
### Значки не по центру
|
||||
|
||||
Виміряв — розмітка давала зсув 0.4 пікселя, тобто річ була не в ній.
|
||||
Винних виявилось двоє.
|
||||
|
||||
**Смуга прокрутки.** Вона забирає ширину справа й зсуває всю колонку
|
||||
значків уліво. У згорнутому вигляді це видно одразу, бо є з чим
|
||||
порівнювати — рівний край панелі. З групами пунктів стало більше, і
|
||||
прокрутка з випадкової стала звичною. Смугу сховано, прокрутка лишилась
|
||||
колесом і клавіатурою.
|
||||
|
||||
**Значок у вузькій коробці.** Було `w-4 text-center` з накладеним
|
||||
`md:px-3`: ширина 1rem проти паддінгів 1.5rem дає нульову ширину вмісту,
|
||||
і гліф центрується випадково — а емодзі різної ширини роблять це
|
||||
помітним на кожному другому. Тепер це квадрат `h-5 w-5` з власним
|
||||
центруванням через flex.
|
||||
|
||||
Після виправлення всі дванадцять значків і кнопка згортання дають зсув
|
||||
рівно 0 від центру панелі.
|
||||
|
||||
### Мірка, яка збрехала
|
||||
|
||||
Проміжні заміри показували ширину 60 px при класі `md:w-52` — виглядало
|
||||
як зламане правило CSS. Після чесного перезавантаження — 208 px, як і
|
||||
має бути. Це артефакт гарячої заміни модулів у vite: клас на елементі
|
||||
вже новий, а перехід ширини лишився від попереднього стану.
|
||||
|
||||
Урок той самий, що з невидимою вкладкою: перш ніж шукати винного в коді,
|
||||
перевірити, що вимірюєш те, що думаєш.
|
||||
|
|
|
|||
|
|
@ -15,19 +15,50 @@ interface NavItem {
|
|||
badge?: boolean
|
||||
}
|
||||
|
||||
const navItems: NavItem[] = [
|
||||
{ to: '/dashboard', label: 'Дашборд', icon: '▦', perm: 'dashboards:read' },
|
||||
{ to: '/map', label: 'Мапа', icon: '🗺', perm: 'maps:read' },
|
||||
{ to: '/devices', label: 'Хости', icon: '🖧', perm: 'devices:read' },
|
||||
{ to: '/groups', label: 'Групи', icon: '🗂', perm: 'devices:read' },
|
||||
{ to: '/metrics', label: 'Метрики', icon: '📈', perm: 'devices:read' },
|
||||
{ to: '/templates', label: 'Шаблони', icon: '📐', perm: 'devices:read' },
|
||||
{ to: '/credentials', label: 'Доступи', icon: '🔑', perm: 'devices:read' },
|
||||
{ to: '/configs', label: 'Конфіги', icon: '📄', perm: 'ncm:read' },
|
||||
{ to: '/alerts', label: 'Алерти', icon: '🔔', perm: 'alerts:read', badge: true },
|
||||
{ to: '/channels', label: 'Сповіщення', icon: '📨', perm: 'alerts:read' },
|
||||
{ to: '/agents', label: 'Зонди', icon: '📡', perm: 'agents:read' },
|
||||
{ to: '/team', label: 'Команда', icon: '👥', perm: 'users:read' },
|
||||
interface NavGroup {
|
||||
title: string
|
||||
items: NavItem[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Меню згруповане за питанням, з яким людина його відкриває.
|
||||
*
|
||||
* «Що зараз відбувається» — щодня й багато разів. «Що в мене є» — коли
|
||||
* додають або шукають залізо. «Як воно налаштоване» — рідко, зазвичай
|
||||
* при заведенні. Плаский список із дванадцяти пунктів змушував щоразу
|
||||
* читати всі дванадцять, щоб знайти один.
|
||||
*/
|
||||
const navGroups: NavGroup[] = [
|
||||
{
|
||||
title: 'Огляд',
|
||||
items: [
|
||||
{ to: '/dashboard', label: 'Дашборд', icon: '▦', perm: 'dashboards:read' },
|
||||
{ to: '/map', label: 'Мапа', icon: '🗺', perm: 'maps:read' },
|
||||
{ to: '/metrics', label: 'Метрики', icon: '📈', perm: 'devices:read' },
|
||||
{ to: '/alerts', label: 'Алерти', icon: '🔔', perm: 'alerts:read', badge: true },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Мережа',
|
||||
items: [
|
||||
{ to: '/devices', label: 'Хости', icon: '🖧', perm: 'devices:read' },
|
||||
{ to: '/groups', label: 'Групи', icon: '🗂', perm: 'devices:read' },
|
||||
{ to: '/configs', label: 'Конфіги', icon: '📄', perm: 'ncm:read' },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Налаштування збору',
|
||||
items: [
|
||||
{ to: '/templates', label: 'Шаблони', icon: '📐', perm: 'devices:read' },
|
||||
{ to: '/credentials', label: 'Доступи', icon: '🔑', perm: 'devices:read' },
|
||||
{ to: '/agents', label: 'Зонди', icon: '📡', perm: 'agents:read' },
|
||||
{ to: '/channels', label: 'Сповіщення', icon: '📨', perm: 'alerts:read' },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Організація',
|
||||
items: [{ to: '/team', label: 'Команда', icon: '👥', perm: 'users:read' }],
|
||||
},
|
||||
]
|
||||
|
||||
/**
|
||||
|
|
@ -65,7 +96,12 @@ export function AppShell() {
|
|||
// лишається поверх щойно відкритої сторінки.
|
||||
useEffect(() => setNavOpen(false), [location.pathname])
|
||||
|
||||
const visible = navItems.filter((i) => session.can(i.perm))
|
||||
// Група без жодного доступного пункту зникає цілком: заголовок над
|
||||
// порожнечею обіцяє розділ, якого для цієї ролі не існує.
|
||||
const groups = navGroups
|
||||
.map((g) => ({ ...g, items: g.items.filter((i) => session.can(i.perm)) }))
|
||||
.filter((g) => g.items.length > 0)
|
||||
const hasAny = groups.some((g) => g.items.length > 0)
|
||||
|
||||
return (
|
||||
<div className="flex h-[100dvh] w-screen flex-col bg-slate-950 text-slate-200">
|
||||
|
|
@ -153,48 +189,80 @@ export function AppShell() {
|
|||
)}
|
||||
|
||||
<nav
|
||||
className={`absolute inset-y-0 left-0 z-20 flex w-56 shrink-0 flex-col gap-0.5
|
||||
// Смуга прокрутки забирає ширину справа й зсуває всю колонку
|
||||
// значків уліво — у згорнутому вигляді це помітно одразу, бо
|
||||
// порівнювати є з чим: рівний край панелі. Ховаємо смугу,
|
||||
// прокрутка лишається колесом і клавіатурою.
|
||||
className={`absolute inset-y-0 left-0 z-20 flex w-56 shrink-0 flex-col
|
||||
overflow-y-auto overflow-x-hidden border-r border-slate-800
|
||||
bg-slate-900 p-2 transition-transform
|
||||
bg-slate-900 py-2 transition-transform
|
||||
[scrollbar-width:none] [&::-webkit-scrollbar]:hidden
|
||||
md:static md:z-0 md:translate-x-0 md:bg-slate-900/50
|
||||
md:transition-[width]
|
||||
${collapsed ? 'md:w-[3.75rem]' : 'md:w-52'}
|
||||
${collapsed ? 'md:w-[3.75rem] md:px-1.5' : 'px-2 md:w-52'}
|
||||
${navOpen ? 'translate-x-0' : '-translate-x-full'}`}
|
||||
>
|
||||
{visible.map((i) => (
|
||||
<NavLink
|
||||
key={i.to}
|
||||
to={i.to}
|
||||
// Підказка лише у згорнутому вигляді: поруч із видимим
|
||||
// підписом вона повторювала б його й миготіла на кожному
|
||||
// проході мишею.
|
||||
title={collapsed ? i.label : undefined}
|
||||
className={({ isActive }) =>
|
||||
`relative flex items-center gap-2.5 rounded py-2.5 text-sm md:py-2 ${
|
||||
collapsed ? 'md:justify-center md:px-0' : 'px-3'
|
||||
} ${
|
||||
isActive
|
||||
? 'bg-slate-800 font-medium text-slate-100'
|
||||
: 'text-slate-400 hover:bg-slate-800/60 hover:text-slate-200'
|
||||
}`
|
||||
}
|
||||
>
|
||||
<span className={`w-4 shrink-0 text-center opacity-80 ${collapsed ? 'md:px-3' : ''}`}>
|
||||
{i.icon}
|
||||
</span>
|
||||
<span className={`flex-1 truncate ${collapsed ? 'md:hidden' : ''}`}>{i.label}</span>
|
||||
{i.badge && alerts.counts.firing > 0 && (
|
||||
<span
|
||||
className={`rounded bg-red-900/70 px-1.5 text-[11px] tabular-nums text-red-200
|
||||
${collapsed ? 'md:absolute md:right-1 md:top-1 md:px-1' : ''}`}
|
||||
{groups.map((g, gi) => (
|
||||
<div key={g.title} className="flex flex-col gap-0.5">
|
||||
{/* Заголовок групи у згорнутому вигляді замінює риска:
|
||||
текст туди не влазить, а межа між групами потрібна —
|
||||
без неї значки зливаються в одну стрічку. */}
|
||||
{collapsed ? (
|
||||
gi > 0 && <div className="mx-1 my-1.5 hidden border-t border-slate-800 md:block" />
|
||||
) : (
|
||||
<div
|
||||
className={`px-3 pb-1 text-[10px] font-semibold uppercase tracking-wider
|
||||
text-slate-600 ${gi > 0 ? 'pt-3' : 'pt-1'}`}
|
||||
>
|
||||
{alerts.counts.firing}
|
||||
</span>
|
||||
{g.title}
|
||||
</div>
|
||||
)}
|
||||
</NavLink>
|
||||
|
||||
{g.items.map((i) => (
|
||||
<NavLink
|
||||
key={i.to}
|
||||
to={i.to}
|
||||
// Підказка лише у згорнутому вигляді: поруч із видимим
|
||||
// підписом вона повторювала б його й миготіла на кожному
|
||||
// проході мишею. Група в підказці теж не зайва — саме
|
||||
// вона зникла разом із заголовком.
|
||||
title={collapsed ? `${g.title} · ${i.label}` : undefined}
|
||||
className={({ isActive }) =>
|
||||
`relative flex items-center rounded py-2.5 text-sm md:py-2 ${
|
||||
collapsed ? 'md:justify-center md:gap-0 md:px-0' : 'gap-2.5 px-3'
|
||||
} ${
|
||||
isActive
|
||||
? 'bg-slate-800 font-medium text-slate-100'
|
||||
: 'text-slate-400 hover:bg-slate-800/60 hover:text-slate-200'
|
||||
}`
|
||||
}
|
||||
>
|
||||
{/* Квадрат зі своїм центруванням, а не текст у вузькій
|
||||
коробці: значки — емодзі різної ширини, і покластися
|
||||
на text-align означає зсув на кожному другому. */}
|
||||
<span
|
||||
className="flex h-5 w-5 shrink-0 items-center justify-center
|
||||
text-[15px] leading-none opacity-80"
|
||||
>
|
||||
{i.icon}
|
||||
</span>
|
||||
<span className={`flex-1 truncate ${collapsed ? 'md:hidden' : ''}`}>
|
||||
{i.label}
|
||||
</span>
|
||||
{i.badge && alerts.counts.firing > 0 && (
|
||||
<span
|
||||
className={`rounded bg-red-900/70 px-1.5 text-[11px] tabular-nums text-red-200
|
||||
${collapsed ? 'md:absolute md:right-0.5 md:top-0.5 md:px-1' : ''}`}
|
||||
>
|
||||
{alerts.counts.firing}
|
||||
</span>
|
||||
)}
|
||||
</NavLink>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
|
||||
{visible.length === 0 && (
|
||||
{!hasAny && (
|
||||
<p className="px-3 py-4 text-xs leading-relaxed text-slate-600">
|
||||
Вашій ролі не надано жодного розділу. Зверніться до власника організації.
|
||||
</p>
|
||||
|
|
@ -204,14 +272,14 @@ export function AppShell() {
|
|||
панелі, і шукати її перемикач в іншому кутку екрана —
|
||||
зайвий крок. На телефоні панель і так шухляда. */}
|
||||
<button
|
||||
className={`mt-auto hidden shrink-0 items-center gap-2.5 rounded py-2 text-sm
|
||||
className={`mt-4 hidden shrink-0 items-center rounded py-2 text-sm
|
||||
text-slate-500 hover:bg-slate-800/60 hover:text-slate-300 md:flex
|
||||
${collapsed ? 'md:justify-center md:px-0' : 'px-3'}`}
|
||||
${collapsed ? 'md:justify-center md:gap-0 md:px-0' : 'gap-2.5 px-3'}`}
|
||||
onClick={() => setCollapsed((v) => !v)}
|
||||
title={collapsed ? 'Розгорнути панель' : 'Згорнути панель'}
|
||||
aria-label={collapsed ? 'Розгорнути панель' : 'Згорнути панель'}
|
||||
>
|
||||
<span className={`w-4 shrink-0 text-center ${collapsed ? 'md:px-3' : ''}`}>
|
||||
<span className="flex h-5 w-5 shrink-0 items-center justify-center leading-none">
|
||||
{collapsed ? '»' : '«'}
|
||||
</span>
|
||||
<span className={collapsed ? 'md:hidden' : ''}>Згорнути</span>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue