ux(admin): sticky table headers + collapsible nav + унификация лейблов

3 победы из аудита админ-панели за один заход:

1) STICKY TABLE HEADERS
   admin.html:142 — добавлен position:sticky; top:0; z-index:5; на <th>
   Заголовки колонок теперь остаются видны при scroll длинных таблиц
   (Users, Sessions, Shop, Gam — 100+ строк). Background фон поменян
   на opaque #E5EAF7 чтобы строки скроллились чисто за header'ом.
   Стоимость: 1 CSS-правило. Эффект: пользователи не теряют контекст
   столбцов при просмотре длинного списка.

2) COLLAPSIBLE NAV GROUPS
   admin.html:875+ — 4 группы (Аналитика, Контент, Пользователи,
   Система) вместо плоского списка 21 кнопки с просто визуальными
   сепараторами. Каждая группа сворачивается кликом по заголовку.
   Состояние per-группа в localStorage (ls_adm_g_<slug>).
   Группа «Система» (только админ) теперь объединяет shop, gam, sims,
   games, audit, errors, health — раньше они шли вперемешку с
   teacher-видимыми табами (sublog, topics, broadcast). Переместил
   sublog/broadcast в группу «Пользователи», topics в «Контент» —
   логичнее по смыслу.
   Паттерн один-в-один как у sidebar.js (где мы это сделали ранее).

3) УНИФИКАЦИЯ ЛЕЙБЛОВ
   Правило: «+ Добавить» для атомов (вопрос, тема, опция, товар),
              «+ Создать» для составных объектов (тест, задание, курс).
   Изменения:
   - admin.html:1431 — «Создать» → «Добавить» (форма темы — атом)
   - admin.html:1195 — «Новый товар» → «Добавить товар»
   - admin.js:415 — q-modal title «Новый вопрос» → «Добавить вопрос»
   - admin.js:2239 — shop-form-title «Новый товар» → «Добавить товар»
   Теперь кнопка в toolbar и заголовок модалки/формы согласованы.

Остались крупные пункты из аудита (на отдельный заход):
  - Q-modal wizard (split на 2 шага) — 🔴 высокий приоритет
  - Pagination в больших таблицах — 🟡
  - Standardized error/loading states — 🔵
This commit is contained in:
Maxim Dolgolyov
2026-05-16 19:48:31 +03:00
parent 846a3c389d
commit ffd7bac0ac
2 changed files with 135 additions and 78 deletions
+116 -74
View File
@@ -139,7 +139,8 @@
/* tables */
.table-wrap { background: var(--surface); backdrop-filter: var(--blur); border: 1px solid var(--border); border-radius: var(--r-lg); overflow: hidden; margin-bottom: 48px; box-shadow: var(--shadow); }
table { width: 100%; border-collapse: collapse; }
th { padding: 14px 20px; text-align: left; font-size: 0.82rem; font-weight: 700; color: var(--text-3); text-transform: uppercase; letter-spacing: 0.06em; border-bottom: 1px solid var(--border); background: rgba(238,242,255,0.5); }
/* Sticky header: stays visible while body scrolls (in .sb-content). Background opaque (not transparent) so rows scroll cleanly behind. */
th { padding: 14px 20px; text-align: left; font-size: 0.82rem; font-weight: 700; color: var(--text-3); text-transform: uppercase; letter-spacing: 0.06em; border-bottom: 1px solid var(--border); background: #E5EAF7; position: sticky; top: 0; z-index: 5; }
td { padding: 15px 20px; font-size: 0.94rem; border-bottom: 1px solid var(--border); vertical-align: middle; }
tr:last-child td { border-bottom: none; }
tr.clickable { cursor: pointer; transition: background var(--tr); }
@@ -196,6 +197,22 @@
}
.admin-nav-label:first-child { margin-top: 0; }
.admin-nav-sep { height: 1px; background: var(--border); margin: 8px 6px; }
/* Collapsible nav groups */
.admin-nav-group { margin-bottom: 6px; }
.admin-nav-ghdr {
width: 100%; padding: 6px 10px 4px; border: none; background: none;
display: flex; align-items: center; justify-content: space-between;
font-family: 'Manrope', system-ui, sans-serif;
font-size: 0.68rem; font-weight: 800; letter-spacing: 0.08em;
color: var(--text-3); text-transform: uppercase;
cursor: pointer; transition: color .12s, opacity .12s; opacity: .72;
}
.admin-nav-ghdr:hover { color: var(--violet); opacity: 1; }
.admin-nav-ghdr .adm-chev { width: 12px; height: 12px; transition: transform .18s; }
.admin-nav-group.collapsed .adm-chev { transform: rotate(-90deg); }
.admin-nav-body { display: flex; flex-direction: column; max-height: 1500px; overflow: hidden; transition: max-height .25s ease, opacity .18s; opacity: 1; }
.admin-nav-group.collapsed .admin-nav-body { max-height: 0; opacity: 0; pointer-events: none; }
.admin-nav-group .admin-nav-item { padding-left: 10px; }
.admin-nav-item {
display: flex; align-items: center; gap: 10px;
padding: 10px 12px; border-radius: 10px; border: none;
@@ -874,80 +891,105 @@
<div class="admin-layout">
<nav class="admin-nav" id="admin-nav">
<div class="admin-nav-label">Аналитика</div>
<button class="admin-nav-item active" data-tab="stats" onclick="switchTab(this)">
<i data-lucide="bar-chart-2" style="width:15px;height:15px"></i> Статистика
</button>
<button class="admin-nav-item" data-tab="sessions" onclick="switchTab(this)">
<i data-lucide="clock" style="width:15px;height:15px"></i> История сессий
</button>
<button class="admin-nav-item" data-tab="classroom" onclick="switchTab(this)">
<i data-lucide="video" style="width:15px;height:15px"></i> Онлайн-уроки
</button>
<div class="admin-nav-group" data-ng="analytics">
<button class="admin-nav-ghdr" onclick="toggleAdminGroup('analytics')">
<span>Аналитика</span>
<svg class="adm-chev" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg>
</button>
<div class="admin-nav-body">
<button class="admin-nav-item active" data-tab="stats" onclick="switchTab(this)">
<i data-lucide="bar-chart-2" style="width:15px;height:15px"></i> Статистика
</button>
<button class="admin-nav-item" data-tab="sessions" onclick="switchTab(this)">
<i data-lucide="clock" style="width:15px;height:15px"></i> История сессий
</button>
<button class="admin-nav-item" data-tab="classroom" onclick="switchTab(this)">
<i data-lucide="video" style="width:15px;height:15px"></i> Онлайн-уроки
</button>
</div>
</div>
<div class="admin-nav-sep"></div>
<div class="admin-nav-label">Контент</div>
<button class="admin-nav-item" data-tab="questions" onclick="switchTab(this)">
<i data-lucide="help-circle" style="width:15px;height:15px"></i> Вопросы
</button>
<button class="admin-nav-item" data-tab="tests" onclick="switchTab(this)">
<i data-lucide="clipboard-list" style="width:15px;height:15px"></i> Тесты
</button>
<button class="admin-nav-item" data-tab="assignments" onclick="switchTab(this)">
<i data-lucide="file-check" style="width:15px;height:15px"></i> Задания
</button>
<button class="admin-nav-item" data-tab="subjects" onclick="switchTab(this)" id="btn-tab-subjects" style="display:none">
<i data-lucide="book-marked" style="width:15px;height:15px"></i> Доступные тесты
</button>
<button class="admin-nav-item" data-tab="tpl" onclick="switchTab(this)" id="btn-tab-tpl" style="display:none">
<i data-lucide="copy" style="width:15px;height:15px"></i> Шаблоны
</button>
<div class="admin-nav-group" data-ng="content">
<button class="admin-nav-ghdr" onclick="toggleAdminGroup('content')">
<span>Контент</span>
<svg class="adm-chev" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg>
</button>
<div class="admin-nav-body">
<button class="admin-nav-item" data-tab="questions" onclick="switchTab(this)">
<i data-lucide="help-circle" style="width:15px;height:15px"></i> Вопросы
</button>
<button class="admin-nav-item" data-tab="tests" onclick="switchTab(this)">
<i data-lucide="clipboard-list" style="width:15px;height:15px"></i> Тесты
</button>
<button class="admin-nav-item" data-tab="assignments" onclick="switchTab(this)">
<i data-lucide="file-check" style="width:15px;height:15px"></i> Задания
</button>
<button class="admin-nav-item" data-tab="subjects" onclick="switchTab(this)" id="btn-tab-subjects" style="display:none">
<i data-lucide="book-marked" style="width:15px;height:15px"></i> Доступные тесты
</button>
<button class="admin-nav-item" data-tab="tpl" onclick="switchTab(this)" id="btn-tab-tpl" style="display:none">
<i data-lucide="copy" style="width:15px;height:15px"></i> Шаблоны
</button>
<button class="admin-nav-item" data-tab="topics" onclick="switchTab(this)">
<i data-lucide="list-tree" style="width:15px;height:15px"></i> Темы
</button>
</div>
</div>
<div class="admin-nav-sep"></div>
<div class="admin-nav-label">Пользователи</div>
<button class="admin-nav-item" data-tab="users" onclick="switchTab(this)">
<i data-lucide="users" style="width:15px;height:15px"></i> Пользователи
</button>
<button class="admin-nav-item" data-tab="permissions" onclick="switchTab(this)" id="btn-tab-permissions" style="display:none">
<i data-lucide="shield" style="width:15px;height:15px"></i> Права доступа
</button>
<button class="admin-nav-item" data-tab="avatars" onclick="switchTab(this);loadAvatarRequests()">
<i data-lucide="image" style="width:15px;height:15px"></i> Аватары
<span class="admin-badge" id="av-badge" style="display:none"></span>
</button>
<div class="admin-nav-group" data-ng="users">
<button class="admin-nav-ghdr" onclick="toggleAdminGroup('users')">
<span>Пользователи</span>
<svg class="adm-chev" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg>
</button>
<div class="admin-nav-body">
<button class="admin-nav-item" data-tab="users" onclick="switchTab(this)">
<i data-lucide="users" style="width:15px;height:15px"></i> Пользователи
</button>
<button class="admin-nav-item" data-tab="permissions" onclick="switchTab(this)" id="btn-tab-permissions" style="display:none">
<i data-lucide="shield" style="width:15px;height:15px"></i> Права доступа
</button>
<button class="admin-nav-item" data-tab="avatars" onclick="switchTab(this);loadAvatarRequests()">
<i data-lucide="image" style="width:15px;height:15px"></i> Аватары
<span class="admin-badge" id="av-badge" style="display:none"></span>
</button>
<button class="admin-nav-item" data-tab="sublog" onclick="switchTab(this)">
<i data-lucide="file-x" style="width:15px;height:15px"></i> Журнал работ
</button>
<button class="admin-nav-item" data-tab="broadcast" onclick="switchTab(this)">
<i data-lucide="megaphone" style="width:15px;height:15px"></i> Рассылка
</button>
</div>
</div>
<div class="admin-nav-sep" id="admin-nav-system-sep" style="display:none"></div>
<div class="admin-nav-label" id="admin-nav-system-label" style="display:none">Система</div>
<button class="admin-nav-item" data-tab="shop" onclick="switchTab(this)" id="btn-tab-shop" style="display:none">
<i data-lucide="shopping-bag" style="width:15px;height:15px"></i> Магазин
</button>
<button class="admin-nav-item" data-tab="gam" onclick="switchTab(this)" id="btn-tab-gam" style="display:none">
<i data-lucide="trophy" style="width:15px;height:15px"></i> Геймификация
</button>
<button class="admin-nav-item" data-tab="sims" onclick="switchTab(this)" id="btn-tab-sims" style="display:none">
<i data-lucide="atom" style="width:15px;height:15px"></i> Симуляции
</button>
<button class="admin-nav-item" data-tab="games" onclick="switchTab(this)" id="btn-tab-games" style="display:none">
<i data-lucide="gamepad-2" style="width:15px;height:15px"></i> Игры
</button>
<button class="admin-nav-item" data-tab="sublog" onclick="switchTab(this)">
<i data-lucide="file-x" style="width:15px;height:15px"></i> Журнал работ
</button>
<button class="admin-nav-item" data-tab="topics" onclick="switchTab(this)">
<i data-lucide="list-tree" style="width:15px;height:15px"></i> Темы
</button>
<button class="admin-nav-item" data-tab="broadcast" onclick="switchTab(this)">
<i data-lucide="megaphone" style="width:15px;height:15px"></i> Рассылка
</button>
<button class="admin-nav-item" data-tab="audit" onclick="switchTab(this)">
<i data-lucide="scroll-text" style="width:15px;height:15px"></i> Аудит-лог
</button>
<button class="admin-nav-item" data-tab="errors" onclick="switchTab(this)">
<i data-lucide="bug" style="width:15px;height:15px"></i> Ошибки
</button>
<button class="admin-nav-item" data-tab="health" onclick="switchTab(this)">
<i data-lucide="activity" style="width:15px;height:15px"></i> Здоровье
</button>
<div class="admin-nav-group" data-ng="system" id="admin-nav-system-group" style="display:none">
<button class="admin-nav-ghdr" onclick="toggleAdminGroup('system')">
<span>Система</span>
<svg class="adm-chev" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg>
</button>
<div class="admin-nav-body">
<button class="admin-nav-item" data-tab="shop" onclick="switchTab(this)" id="btn-tab-shop" style="display:none">
<i data-lucide="shopping-bag" style="width:15px;height:15px"></i> Магазин
</button>
<button class="admin-nav-item" data-tab="gam" onclick="switchTab(this)" id="btn-tab-gam" style="display:none">
<i data-lucide="trophy" style="width:15px;height:15px"></i> Геймификация
</button>
<button class="admin-nav-item" data-tab="sims" onclick="switchTab(this)" id="btn-tab-sims" style="display:none">
<i data-lucide="atom" style="width:15px;height:15px"></i> Симуляции
</button>
<button class="admin-nav-item" data-tab="games" onclick="switchTab(this)" id="btn-tab-games" style="display:none">
<i data-lucide="gamepad-2" style="width:15px;height:15px"></i> Игры
</button>
<button class="admin-nav-item" data-tab="audit" onclick="switchTab(this)">
<i data-lucide="scroll-text" style="width:15px;height:15px"></i> Аудит-лог
</button>
<button class="admin-nav-item" data-tab="errors" onclick="switchTab(this)">
<i data-lucide="bug" style="width:15px;height:15px"></i> Ошибки
</button>
<button class="admin-nav-item" data-tab="health" onclick="switchTab(this)">
<i data-lucide="activity" style="width:15px;height:15px"></i> Здоровье
</button>
</div>
</div>
</nav>
<div class="admin-main">
@@ -1192,7 +1234,7 @@
</div>
<div class="adm-panel" id="shop-item-form" style="display:none">
<div class="adm-panel-title" id="shop-form-title">Новый товар</div>
<div class="adm-panel-title" id="shop-form-title">Добавить товар</div>
<div class="adm-form-row">
<div class="adm-form-group" style="flex:1">
<label>Название</label>
@@ -1428,7 +1470,7 @@
<div class="adm-panel" style="padding:16px 20px">
<div class="adm-form-row" style="margin:0">
<div class="adm-form-group" style="flex:1"><label>Название</label><input type="text" id="topics-new-name" placeholder="Название темы" /></div>
<button class="adm-btn adm-btn-primary adm-btn-small" onclick="createTopic()" style="align-self:flex-end">Создать</button>
<button class="adm-btn adm-btn-primary adm-btn-small" onclick="createTopic()" style="align-self:flex-end">Добавить</button>
<button class="adm-btn adm-btn-small" style="background:var(--border-h);color:var(--text-3);align-self:flex-end" onclick="document.getElementById('topics-add-row').style.display='none'">Отмена</button>
</div>
</div>
+19 -4
View File
@@ -12,9 +12,24 @@
const el = document.getElementById(id);
if (el) el.style.display = '';
});
document.getElementById('admin-nav-system-sep').style.display = '';
document.getElementById('admin-nav-system-label').style.display = '';
const sysGroup = document.getElementById('admin-nav-system-group');
if (sysGroup) sysGroup.style.display = '';
}
/* Collapsible nav groups — state persisted in localStorage */
window.toggleAdminGroup = function (slug) {
const g = document.querySelector(`.admin-nav-group[data-ng="${slug}"]`);
if (!g) return;
const collapsed = g.classList.toggle('collapsed');
try { localStorage.setItem('ls_adm_g_' + slug, collapsed ? '1' : '0'); } catch {}
};
// Restore collapsed state on page load
document.querySelectorAll('.admin-nav-group').forEach(g => {
const slug = g.dataset.ng;
try {
if (localStorage.getItem('ls_adm_g_' + slug) === '1') g.classList.add('collapsed');
} catch {}
});
LS.showBoardIfAllowed();
LS.hideDisabledFeatures?.();
LS.notif?.init();
@@ -412,7 +427,7 @@
/* ─── Modal ─── */
function openQModal(q = null) {
editingQId = q ? q.id : null;
document.getElementById('q-modal-title').textContent = q ? `Редактировать вопрос #${q.id}` : 'Новый вопрос';
document.getElementById('q-modal-title').textContent = q ? `Редактировать вопрос #${q.id}` : 'Добавить вопрос';
const textEl = document.getElementById('qf-text');
textEl.value = q?.text || '';
updateCharCounter(textEl, 'qf-text-cnt', 500);
@@ -2236,7 +2251,7 @@
function shopAdminCreateItem() {
_shopEditId = null;
document.getElementById('shop-form-title').textContent = 'Новый товар';
document.getElementById('shop-form-title').textContent = 'Добавить товар';
document.getElementById('shop-f-name').value = '';
document.getElementById('shop-f-type').value = 'frame';
document.getElementById('shop-f-price').value = '100';