feat(ui): unify header and implement navigation drawer (Task 17)

- Redesign top header into clean, uncluttered layout with hamburger drawer trigger, clickable logo, central SRS focus badge, theme toggle and profile avatar
- Implement unified responsive Navigation Drawer for both desktop and mobile with touch swipe-to-dismiss, backdrop blur, keyboard Esc support and background scroll lock
- Streamline mobile bottom tab bar to 4 core actions (Dictionary, SRS, Reader, Menu) with Menu opening the unified drawer
- Synchronize real-time SRS due badges and dictionary word statistics between header, bottom tabs, and drawer
- Eliminate obsolete separate mobile-menu-modal in favor of unified drawer
- Bump Service Worker cache version to notes-v5.7
This commit is contained in:
2026-10-06 21:28:25 +03:00
parent 0ff4465d34
commit e73d3470b3
8 changed files with 707 additions and 195 deletions
+18 -14
View File
@@ -377,24 +377,28 @@
---
### 17. 🍔 Разгрузка хеддера и единая навигация через боковое меню / Drawer (Unified Navigation & Header Redesign)
- [ ] **Проблема перегруженного хеддера**:
- В шапке скопилось 8+ кнопок (`📚 Читалка`, `🎯 Тесты CEFR`, `📖 Грамматика`, `🎓 Тренажёр`, `📊 Статистика`, `📤 Экспорт`, тема, шестеренка, профиль), на экранах планшетов и ноутбуков возникает переполнение и визуальный шум.
- Разрозненность UX: на десктопе все кнопки в шапке, на мобильных — нижний таб-бар + модальное меню.
- [ ] **Единый лаконичный Header (Desktop, Tablet, Mobile)**:
### 17. 🍔 Разгрузка хеддера и единая навигация через боковое меню / Drawer (Unified Navigation & Header Redesign) (Completed)
- [x] **Проблема перегруженного хеддера полностью решена**:
- Ликвидировано нагромождение из 8+ кнопок в шапке, устранён визуальный шум и горизонтальный сдвиг на планшетах и компактных экранах.
- Устранена разрозненность UX: на десктопе, планшете и смартфоне теперь действует единая система навигации.
- [x] **Единый лаконичный Header (Desktop, Tablet, Mobile)**:
- Слева: Кнопка вызова бокового меню (Бургер `☰` / `ph-list`), кликабельный логотип `📖 Notes` (возврат в словарь).
- Центр: Фокусная кнопка быстрого доступа к активному действию (например, `🎓 Тренажёр SRS` с живым счётчиком повторений `srs-badge`).
- Справа: Переключатель темы `🌓`, компактная плашка пользователя/аватар.
- [ ] **Единый выдвижной Drawer (Боковое меню навигации)**:
- Плавная аппаратная анимация (`transform: translateX`, `backdrop-filter: blur`).
- Центр: Фокусная кнопка быстрого доступа к главному ежедневному действию (`🎓 Тренажёр SRS` с живым счётчиком повторений `srs-badge`) и компактные счетчики базы (`Всего: X`, `Сегодня: +Y`). На смартфонах счетчики автоматически скрываются, а кнопка сжимается до компактного бейджа `🎓 (N)`.
- Справа: Переключатель темы `🌓`, компактная плашка пользователя/аватар `👤 user` (на смартфонах отображается только иконка аватара).
- [x] **Единый выдвижной Drawer (Боковое меню навигации)**:
- Плавная аппаратная анимация (`transform: translateX`, `backdrop-filter: blur(4px)`).
- Карточка профиля с аватаром, именем пользователя и описанием.
- Интерактивный мини-дашборд статистики (Всего в базе, Сегодня, На повторение).
- Логическая группировка пунктов с красивыми иконками:
- **Обучение & Чтение**: Словарь, Читалка, Тесты CEFR, Грамматика Мерфи, Тренажёр SRS.
- **Аналитика**: Статистика обучения и календарь активности (Heatmap).
- **Настройки & Профиль**: Экспорт словаря, Смена пароля, Переключение темы, Выход.
- **Обучение & Чтение**: Словарь & Поиск, Тренажёр SRS (с живым бейджем), Читалка & Книги, Тесты CEFR (A1–B2), Грамматика Мерфи & Тетрадь.
- **Аналитика**: Календарь активности & Статистика (Heatmap).
- **Настройки & Профиль**: Экспорт словаря, Сменить пароль, Переключение темы, Выход из аккаунта.
- Закрытие по клику вне области (оверлей), кнопке `✕`, клавише <kbd>Esc</kbd> и свайпу влево на тач-устройствах (Swipe to dismiss).
- Блокировка скролла фона (`overflow: hidden` на `body`) при открытом Drawer.
- [ ] **Оптимизация мобильного режима**:
- Согласование с Bottom Tab Bar: оставить на нижней панели только 3-4 ключевых быстрых действия либо полностью перевести навигацию в чистый Drawer-паттерн без дублирования элементов.
- Подсветка активного раздела (`.active`), синхронизированная с URL-хэшем и переходами между экранами.
- [x] **Оптимизация мобильного режима**:
- На нижней панели (Bottom Tab Bar) оставлены 4 ключевых быстрых действия: `📖 Словарь`, `🎓 Тренажёр`, `📚 Читалка`, `☰ Меню`.
- Тап по `☰ Меню` на мобильном открывает единый Drawer, полностью заменив устаревший отдельный модальный слой `mobile-menu-modal` без дублирования верстки и стилей.
---
+341 -22
View File
@@ -245,13 +245,15 @@ body {
min-height: 100vh;
}
/* App Navbar (Unified Layout) */
.navbar {
background-color: var(--bg-surface);
border-bottom: 1px solid var(--border-color);
padding: 12px 24px;
padding: 8px 20px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
position: sticky;
top: 0;
z-index: 100;
@@ -259,10 +261,46 @@ body {
box-sizing: border-box;
}
.nav-left {
display: flex;
align-items: center;
gap: 10px;
flex-shrink: 0;
}
.drawer-toggle-btn {
display: inline-flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
border-radius: var(--radius-sm);
background-color: transparent;
border: 1px solid transparent;
color: var(--text-main);
cursor: pointer;
font-size: 20px;
line-height: 1;
transition: var(--transition);
}
.drawer-toggle-btn:hover {
background-color: var(--bg-surface-hover);
border-color: var(--border-color);
color: var(--primary);
}
.nav-brand {
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
user-select: none;
transition: opacity 0.15s ease;
}
.nav-brand:hover {
opacity: 0.85;
}
.brand-icon {
@@ -286,9 +324,18 @@ body {
margin-left: 4px;
}
.nav-center {
display: flex;
align-items: center;
justify-content: center;
gap: 14px;
flex: 1;
min-width: 0;
}
.nav-stats {
display: flex;
gap: 12px;
gap: 8px;
}
.stat-badge {
@@ -311,22 +358,286 @@ body {
.nav-actions {
display: flex;
align-items: center;
gap: 12px;
gap: 10px;
flex-shrink: 0;
}
.user-profile {
display: flex;
.user-profile-btn {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 5px 12px;
border-radius: var(--radius-full);
background-color: var(--bg-surface-hover);
border: 1px solid var(--border-color);
color: var(--text-main);
font-size: 13px;
font-weight: 600;
cursor: pointer;
transition: var(--transition);
}
.user-profile-btn:hover {
border-color: var(--primary);
color: var(--primary);
background-color: var(--primary-dim);
}
.user-avatar-icon {
font-size: 15px;
}
.user-badge {
font-size: 13px;
color: var(--text-muted);
color: var(--text-main);
font-weight: 600;
background-color: var(--bg-surface-hover);
padding: 4px 10px;
}
/* -----------------------------------------------------------------------
Unified Navigation Drawer (Desktop & Mobile)
----------------------------------------------------------------------- */
.drawer-backdrop {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background-color: rgba(0, 0, 0, 0.65);
backdrop-filter: blur(4px);
-webkit-backdrop-filter: blur(4px);
z-index: 1999;
opacity: 0;
pointer-events: none;
transition: opacity 0.26s cubic-bezier(0.16, 1, 0.3, 1);
}
.drawer-backdrop.active {
opacity: 1;
pointer-events: auto;
}
.app-drawer {
position: fixed;
top: 0;
left: 0;
bottom: 0;
width: min(340px, 86vw);
background-color: var(--bg-surface);
border-right: 1px solid var(--border-color);
box-shadow: 4px 0 28px rgba(0, 0, 0, 0.5);
z-index: 2000;
display: flex;
flex-direction: column;
transform: translateX(-100%);
transition: transform 0.28s cubic-bezier(0.16, 1, 0.3, 1);
will-change: transform;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
}
.app-drawer.active {
transform: translateX(0);
}
body.drawer-open {
overflow: hidden !important;
}
.drawer-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: calc(16px + env(safe-area-inset-top, 0px)) 16px 14px 18px;
border-bottom: 1px solid var(--border-color);
background-color: var(--bg-surface);
position: sticky;
top: 0;
z-index: 10;
}
.drawer-user-card {
display: flex;
align-items: center;
gap: 12px;
min-width: 0;
}
.drawer-user-avatar {
width: 42px;
height: 42px;
border-radius: var(--radius-full);
background: linear-gradient(135deg, var(--primary), #6366f1);
color: #ffffff;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
flex-shrink: 0;
box-shadow: var(--shadow-sm);
}
.drawer-user-info {
display: flex;
flex-direction: column;
min-width: 0;
}
.drawer-user-name {
font-size: 15px;
font-weight: 700;
color: var(--text-main);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.drawer-user-desc {
font-size: 12px;
color: var(--text-muted);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.drawer-close-btn {
width: 34px;
height: 34px;
border-radius: var(--radius-sm);
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
color: var(--text-muted);
background: transparent;
border: none;
cursor: pointer;
transition: var(--transition);
}
.drawer-close-btn:hover {
background-color: var(--bg-surface-hover);
color: var(--text-main);
}
.drawer-stats-summary {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px;
padding: 12px 14px;
margin: 12px 14px 4px 14px;
background-color: var(--bg-main);
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
text-align: center;
}
.drawer-stat-item {
display: flex;
flex-direction: column;
gap: 2px;
}
.drawer-stat-val {
font-size: 16px;
font-weight: 800;
color: var(--text-main);
}
.drawer-stat-lbl {
font-size: 10px;
font-weight: 600;
color: var(--text-dim);
text-transform: uppercase;
letter-spacing: 0.3px;
}
.drawer-nav {
flex: 1;
padding: 8px 12px;
display: flex;
flex-direction: column;
gap: 4px;
}
.drawer-section-title {
font-size: 11px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.6px;
color: var(--text-dim);
padding: 12px 8px 4px 8px;
}
.drawer-nav-list {
list-style: none;
padding: 0;
margin: 0;
display: flex;
flex-direction: column;
gap: 3px;
}
.drawer-nav-item {
width: 100%;
display: flex;
align-items: center;
gap: 12px;
padding: 10px 12px;
border-radius: var(--radius-sm);
background: transparent;
border: 1px solid transparent;
color: var(--text-main);
font-size: 14px;
font-weight: 500;
text-align: left;
cursor: pointer;
transition: var(--transition);
text-decoration: none;
}
.drawer-nav-item:hover {
background-color: var(--bg-surface-hover);
color: var(--primary);
border-color: var(--border-color);
}
.drawer-nav-item.active {
background-color: var(--primary-dim);
color: var(--primary);
border-color: rgba(56, 189, 248, 0.3);
font-weight: 700;
}
.drawer-item-icon {
font-size: 18px;
width: 24px;
text-align: center;
flex-shrink: 0;
}
.drawer-item-label {
flex: 1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.drawer-badge {
padding: 2px 7px;
font-size: 11px;
font-weight: 700;
border-radius: var(--radius-full);
background-color: var(--primary);
color: #0f172a;
line-height: 1.2;
}
.drawer-footer {
padding: 14px 16px calc(14px + env(safe-area-inset-bottom, 0px)) 16px;
border-top: 1px solid var(--border-color);
font-size: 12px;
color: var(--text-dim);
text-align: center;
}
/* Buttons */
@@ -2493,27 +2804,35 @@ body {
padding-bottom: calc(74px + env(safe-area-inset-bottom, 0px)) !important;
}
/* 3. Header & Navigation: Clean iOS App Header */
/* 3. Header & Navigation: Unified Responsive Header */
.navbar {
padding: calc(8px + env(safe-area-inset-top, 0px)) 12px 8px 12px;
position: sticky;
top: 0;
padding: calc(8px + env(safe-area-inset-top, 0px)) 10px 8px 10px;
gap: 8px;
}
.brand-sub,
.nav-center,
.nav-actions #nav-reader-btn,
.nav-actions #srs-trainer-btn,
.nav-actions #stats-open-btn,
.nav-actions #export-open-btn,
.nav-actions .user-profile {
.nav-stats {
display: none !important;
}
.mobile-only-btn {
display: inline-flex !important;
align-items: center;
justify-content: center;
.btn-trainer .trainer-text {
display: none !important;
}
.btn-trainer {
padding: 6px 10px !important;
font-size: 13px !important;
min-height: 36px;
}
.user-profile-btn #user-display-name {
display: none !important;
}
.user-profile-btn {
padding: 6px 10px !important;
border-radius: var(--radius-sm);
min-height: 36px;
}
.mobile-tab-bar {
+126 -56
View File
@@ -52,48 +52,37 @@
<!-- Main Application Shell -->
<div id="app-shell" class="app-shell hidden">
<!-- Top Navigation Bar -->
<header class="navbar">
<div class="nav-brand" id="nav-brand-home" title="На главную">
<header class="navbar" id="main-navbar">
<div class="nav-left">
<button id="drawer-toggle-btn" class="icon-btn drawer-toggle-btn" title="Меню навигации (☰)" aria-label="Открыть меню">
<span class="hamburger-icon">☰</span>
</button>
<div class="nav-brand" id="nav-brand-home" title="На главную (Словарь)">
<span class="brand-icon">📖</span>
<span class="brand-title">Notes</span>
<span class="brand-sub">English Lexicon</span>
</div>
</div>
<div class="nav-center">
<button id="srs-trainer-btn" class="btn btn-trainer" title="Интервальный тренажер SRS (повторение слов)">
<span class="trainer-icon">🎓</span>
<span class="trainer-text">Тренажёр SRS</span>
<span id="srs-badge" class="srs-due-badge">0</span>
</button>
<div class="nav-stats" id="nav-stats">
<span class="stat-badge" title="Всего слов в словаре">Всего: <strong id="stat-total">0</strong></span>
<span class="stat-badge stat-today" title="Добавлено сегодня">Сегодня: <strong id="stat-today">+0</strong></span>
</div>
</div>
<div class="nav-actions">
<button id="nav-reader-btn" class="btn btn-outline btn-sm" title="Интерактивная читалка и библиотека книг">
<span>📚 Читалка</span>
</button>
<button id="nav-cefr-btn" class="btn btn-outline btn-sm" title="Уровневые тесты CEFR (A1, A2, B1, B2)">
<span>🎯 Тесты CEFR</span>
</button>
<button id="nav-grammar-btn" class="btn btn-outline btn-sm" title="Оффлайн-грамматика Мерфи и бумажная тетрадь">
<span>📖 Грамматика</span>
</button>
<button id="srs-trainer-btn" class="btn btn-trainer" title="Интервальный тренажер (Flashcards, 4 варианта, диктант, ввод)">
<span class="trainer-icon">🎓</span>
<span class="trainer-text">Тренажёр SRS</span>
<span id="srs-badge" class="srs-due-badge">0</span>
</button>
<button id="stats-open-btn" class="btn btn-outline btn-sm stat-open-btn" title="Календарь активности и статистика">
<span>📊 Статистика</span>
</button>
<button id="export-open-btn" class="btn btn-outline btn-sm" title="Экспорт словаря">
<span>📤 Экспорт</span>
</button>
<div class="nav-actions nav-right">
<button id="theme-toggle" class="icon-btn" title="Переключить тему">🌓</button>
<button id="mobile-menu-toggle-btn" class="icon-btn mobile-only-btn" title="Меню профиля и настроек">⚙️</button>
<div class="user-profile">
<button id="nav-user-profile-btn" class="user-profile-btn" title="Меню профиля и настройки">
<span class="user-avatar-icon">👤</span>
<span id="user-display-name" class="user-badge">user</span>
<button id="change-pwd-open-btn" class="btn btn-sm btn-outline" title="Сменить пароль">🔑 Пароль</button>
<button id="logout-btn" class="btn btn-sm btn-outline" title="Выйти из аккаунта">Выход</button>
</div>
</button>
<button id="logout-btn" class="hidden" style="display: none;" title="Выйти из аккаунта"></button>
</div>
</header>
@@ -881,7 +870,7 @@
</div>
</div>
<!-- Mobile Bottom Navigation Tab Bar (iOS style) -->
<!-- Mobile Bottom Navigation Tab Bar (iOS style, 4 core tabs) -->
<nav class="mobile-tab-bar" id="mobile-tab-bar">
<button class="tab-item active" data-view="dictionary-view" id="tab-nav-dict">
<span class="tab-icon">📖</span>
@@ -896,44 +885,125 @@
<span class="tab-icon">📚</span>
<span class="tab-label">Читалка</span>
</button>
<button class="tab-item" id="tab-nav-stats">
<span class="tab-icon">📊</span>
<span class="tab-label">Прогресс</span>
</button>
<button class="tab-item" id="tab-nav-menu">
<span class="tab-icon">⚙️</span>
<span class="tab-icon">☰</span>
<span class="tab-label">Меню</span>
</button>
</nav>
</div>
<!-- Mobile Menu / Profile Sheet Modal -->
<div id="mobile-menu-modal" class="modal-backdrop hidden">
<div class="modal-window mobile-sheet-window">
<div class="modal-header">
<h3>⚙️ Профиль & Меню</h3>
<button class="modal-close-btn" id="mobile-menu-close-btn">✕</button>
</div>
<div class="modal-body mobile-menu-body">
<div class="mobile-user-card">
<span class="mobile-user-avatar">👤</span>
<div class="mobile-user-info">
<strong id="mobile-user-name">user</strong>
<p class="subtitle">Личный словарь English Lexicon</p>
<!-- Navigation Drawer (Unified Side Drawer for Desktop & Mobile) -->
<div id="app-drawer-backdrop" class="drawer-backdrop hidden" aria-hidden="true"></div>
<aside id="app-drawer" class="app-drawer" aria-label="Боковое меню навигации">
<div class="drawer-header">
<div class="drawer-user-card" id="drawer-user-card">
<div class="drawer-user-avatar">👤</div>
<div class="drawer-user-info">
<strong id="drawer-user-name" class="drawer-user-name">user</strong>
<span class="drawer-user-desc">Личный словарь English Lexicon</span>
</div>
</div>
<div class="mobile-menu-actions">
<button id="mobile-grammar-btn" class="btn btn-outline btn-block">📖 Грамматика Мерфи & Тетрадь</button>
<button id="mobile-cefr-btn" class="btn btn-outline btn-block">🎯 Уровневые тесты CEFR (A1–B2)</button>
<button id="mobile-stats-btn" class="btn btn-outline btn-block">📊 Статистика & Календарь</button>
<button id="mobile-export-btn" class="btn btn-outline btn-block">📤 Экспорт словаря</button>
<button id="mobile-pwd-btn" class="btn btn-outline btn-block">🔑 Сменить пароль</button>
<button id="mobile-theme-btn" class="btn btn-outline btn-block">🌓 Переключить тему</button>
<button id="mobile-logout-btn" class="btn btn-danger btn-block">Выйти из аккаунта</button>
</div>
<button id="app-drawer-close-btn" class="icon-btn drawer-close-btn" title="Закрыть меню (Esc)" aria-label="Закрыть">✕</button>
</div>
<!-- Quick Stats in Drawer -->
<div class="drawer-stats-summary" id="drawer-stats-summary">
<div class="drawer-stat-item">
<span class="drawer-stat-val" id="drawer-stat-total">0</span>
<span class="drawer-stat-lbl">Слов в базе</span>
</div>
<div class="drawer-stat-item">
<span class="drawer-stat-val text-success" id="drawer-stat-today">+0</span>
<span class="drawer-stat-lbl">Сегодня</span>
</div>
<div class="drawer-stat-item">
<span class="drawer-stat-val text-warning" id="drawer-stat-due">0</span>
<span class="drawer-stat-lbl">Повторение</span>
</div>
</div>
<nav class="drawer-nav">
<!-- Section 1: Study & Reading -->
<div class="drawer-section-title">Обучение & Чтение</div>
<ul class="drawer-nav-list">
<li>
<button class="drawer-nav-item active" data-hash="#dictionary" id="drawer-nav-dict">
<span class="drawer-item-icon">📖</span>
<span class="drawer-item-label">Словарь & Поиск</span>
</button>
</li>
<li>
<button class="drawer-nav-item" data-hash="#srs" id="drawer-nav-srs">
<span class="drawer-item-icon">🎓</span>
<span class="drawer-item-label">Тренажёр SRS</span>
<span id="drawer-srs-badge" class="drawer-badge hidden">0</span>
</button>
</li>
<li>
<button class="drawer-nav-item" data-hash="#reader" id="drawer-nav-reader">
<span class="drawer-item-icon">📚</span>
<span class="drawer-item-label">Интерактивная читалка & Книги</span>
</button>
</li>
<li>
<button class="drawer-nav-item" data-hash="#cefr" id="drawer-nav-cefr">
<span class="drawer-item-icon">🎯</span>
<span class="drawer-item-label">Уровневые тесты CEFR (A1–B2)</span>
</button>
</li>
<li>
<button class="drawer-nav-item" data-hash="#grammar" id="drawer-nav-grammar">
<span class="drawer-item-icon">📘</span>
<span class="drawer-item-label">Грамматика Мерфи & Тетрадь</span>
</button>
</li>
</ul>
<!-- Section 2: Analytics & Progress -->
<div class="drawer-section-title">Аналитика</div>
<ul class="drawer-nav-list">
<li>
<button class="drawer-nav-item" id="drawer-nav-stats">
<span class="drawer-item-icon">📊</span>
<span class="drawer-item-label">Календарь активности & Статистика</span>
</button>
</li>
</ul>
<!-- Section 3: Profile & Settings -->
<div class="drawer-section-title">Настройки & Профиль</div>
<ul class="drawer-nav-list">
<li>
<button class="drawer-nav-item" id="drawer-nav-export">
<span class="drawer-item-icon">📤</span>
<span class="drawer-item-label">Экспорт словаря (JSON / CSV / TXT)</span>
</button>
</li>
<li>
<button class="drawer-nav-item" id="drawer-nav-pwd">
<span class="drawer-item-icon">🔑</span>
<span class="drawer-item-label">Сменить пароль</span>
</button>
</li>
<li>
<button class="drawer-nav-item" id="drawer-nav-theme">
<span class="drawer-item-icon">🌓</span>
<span class="drawer-item-label">Переключить тему оформления</span>
</button>
</li>
<li>
<button class="drawer-nav-item text-danger" id="drawer-nav-logout">
<span class="drawer-item-icon">🚪</span>
<span class="drawer-item-label">Выйти из аккаунта</span>
</button>
</li>
</ul>
</nav>
<div class="drawer-footer">
<span class="drawer-footer-text">study.qpid.pro • v5.7 PWA</span>
</div>
</aside>
<!-- Analytics & Activity Heatmap Modal -->
<div id="stats-modal" class="modal-backdrop hidden">
+179 -95
View File
@@ -15,15 +15,45 @@ let currentUser = null;
let isRouting = false;
// -----------------------------------------------------------------------
// Mobile Navigation Active Tab
// Unified Navigation & Drawer Active State
// -----------------------------------------------------------------------
export function updateMobileActiveTab(tabName) {
export function updateActiveNavState(viewName) {
// 1. Mobile bottom tab bar
document.querySelectorAll('.mobile-tab-bar .tab-item').forEach(btn => {
btn.classList.remove('active');
});
if (!tabName) return;
const target = document.getElementById(`tab-nav-${tabName}`);
if (target) target.classList.add('active');
if (viewName === 'dict' || viewName === 'dictionary') {
const t = document.getElementById('tab-nav-dict');
if (t) t.classList.add('active');
} else if (viewName === 'srs') {
const t = document.getElementById('tab-nav-srs');
if (t) t.classList.add('active');
} else if (viewName === 'reader') {
const t = document.getElementById('tab-nav-reader');
if (t) t.classList.add('active');
}
// 2. Drawer nav items
document.querySelectorAll('.drawer-nav-item').forEach(btn => {
btn.classList.remove('active');
});
const drawerMap = {
'dict': 'drawer-nav-dict',
'dictionary': 'drawer-nav-dict',
'srs': 'drawer-nav-srs',
'reader': 'drawer-nav-reader',
'cefr': 'drawer-nav-cefr',
'grammar': 'drawer-nav-grammar',
};
const targetId = drawerMap[viewName];
if (targetId) {
const dBtn = document.getElementById(targetId);
if (dBtn) dBtn.classList.add('active');
}
}
export function updateMobileActiveTab(tabName) {
updateActiveNavState(tabName);
}
// -----------------------------------------------------------------------
@@ -62,7 +92,7 @@ export function applyInitialViewPreflight() {
if (cefrView) cefrView.classList.add('hidden');
if (grammarView) grammarView.classList.add('hidden');
if (readerView) readerView.classList.remove('hidden');
updateMobileActiveTab('reader');
updateActiveNavState('reader');
if (hash.includes('/book/')) {
if (bookshelfContainer) bookshelfContainer.classList.add('hidden');
if (readerInterface) readerInterface.classList.remove('hidden');
@@ -74,21 +104,21 @@ export function applyInitialViewPreflight() {
if (cefrView) cefrView.classList.add('hidden');
if (grammarView) grammarView.classList.add('hidden');
if (srsView) srsView.classList.remove('hidden');
updateMobileActiveTab('srs');
updateActiveNavState('srs');
} else if (hash.startsWith('#cefr')) {
if (dictionaryView) dictionaryView.classList.add('hidden');
if (readerView) readerView.classList.add('hidden');
if (srsView) srsView.classList.add('hidden');
if (grammarView) grammarView.classList.add('hidden');
if (cefrView) cefrView.classList.remove('hidden');
updateMobileActiveTab('');
updateActiveNavState('cefr');
} else if (hash.startsWith('#grammar')) {
if (dictionaryView) dictionaryView.classList.add('hidden');
if (readerView) readerView.classList.add('hidden');
if (srsView) srsView.classList.add('hidden');
if (cefrView) cefrView.classList.add('hidden');
if (grammarView) grammarView.classList.remove('hidden');
updateMobileActiveTab('');
updateActiveNavState('grammar');
}
}
@@ -111,7 +141,7 @@ export function openDictionaryView(updateHash = true) {
if (cefrView) cefrView.classList.add('hidden');
if (grammarView) grammarView.classList.add('hidden');
if (dictionaryView) dictionaryView.classList.remove('hidden');
updateMobileActiveTab('dict');
updateActiveNavState('dict');
try { localStorage.setItem('notes_active_view', 'dictionary-view'); } catch (e) {}
loadInitialData();
}
@@ -133,7 +163,7 @@ export function openSrsView(tagFilter = null, updateHash = true) {
if (cefrView) cefrView.classList.add('hidden');
if (grammarView) grammarView.classList.add('hidden');
if (srsView) srsView.classList.remove('hidden');
updateMobileActiveTab('srs');
updateActiveNavState('srs');
try { localStorage.setItem('notes_active_view', 'srs-view'); } catch (e) {}
startSrsTrainingSession(false, tagFilter);
}
@@ -162,7 +192,7 @@ export function openBookshelfView(updateHash = true) {
if (readerView) readerView.classList.remove('hidden');
if (bookshelfContainer) bookshelfContainer.classList.remove('hidden');
if (readerInterface) readerInterface.classList.add('hidden');
updateMobileActiveTab('reader');
updateActiveNavState('reader');
try {
localStorage.setItem('notes_active_view', 'reader-view');
localStorage.removeItem('notes_active_book_id');
@@ -201,7 +231,7 @@ export function openCefrView(level = null, mode = 'all', updateHash = true) {
if (grammarView) grammarView.classList.add('hidden');
if (cefrView) cefrView.classList.remove('hidden');
updateMobileActiveTab('');
updateActiveNavState('cefr');
try { localStorage.setItem('notes_active_view', 'cefr-view'); } catch (e) {}
if (level) {
@@ -231,7 +261,7 @@ export function openGrammarView(unitId = null, updateHash = true) {
if (cefrView) cefrView.classList.add('hidden');
if (grammarView) grammarView.classList.remove('hidden');
updateMobileActiveTab('');
updateActiveNavState('grammar');
try { localStorage.setItem('notes_active_view', 'grammar-view'); } catch (e) {}
if (unitId) {
@@ -349,122 +379,176 @@ export function showApp() {
const loginOverlay = document.getElementById('login-overlay');
const appShell = document.getElementById('app-shell');
const userDisplayName = document.getElementById('user-display-name');
const drawerUserName = document.getElementById('drawer-user-name');
const mobileUserName = document.getElementById('mobile-user-name');
if (loginOverlay) loginOverlay.classList.add('hidden');
if (appShell) appShell.classList.remove('hidden');
const name = currentUser ? currentUser.username : 'user';
if (userDisplayName) userDisplayName.textContent = name;
if (drawerUserName) drawerUserName.textContent = name;
if (mobileUserName) mobileUserName.textContent = name;
}
// -----------------------------------------------------------------------
// Mobile Navigation & Drawer
// Unified Navigation Drawer Controller (Desktop & Mobile)
// -----------------------------------------------------------------------
function initMobileNavigation() {
export function initNavigationDrawer() {
const drawerBackdrop = document.getElementById('app-drawer-backdrop');
const drawer = document.getElementById('app-drawer');
const drawerToggleBtn = document.getElementById('drawer-toggle-btn');
const drawerCloseBtn = document.getElementById('app-drawer-close-btn');
const navUserProfileBtn = document.getElementById('nav-user-profile-btn');
const tabMenu = document.getElementById('tab-nav-menu');
// Bottom tab items
const tabDict = document.getElementById('tab-nav-dict');
const tabSrs = document.getElementById('tab-nav-srs');
const tabReader = document.getElementById('tab-nav-reader');
const tabStats = document.getElementById('tab-nav-stats');
const tabMenu = document.getElementById('tab-nav-menu');
const mobileMenuModal = document.getElementById('mobile-menu-modal');
const mobileMenuToggleBtn = document.getElementById('mobile-menu-toggle-btn');
const mobileMenuCloseBtn = document.getElementById('mobile-menu-close-btn');
const mobileStatsBtn = document.getElementById('mobile-stats-btn');
const mobileExportBtn = document.getElementById('mobile-export-btn');
const mobilePwdBtn = document.getElementById('mobile-pwd-btn');
const mobileThemeBtn = document.getElementById('mobile-theme-btn');
const mobileLogoutBtn = document.getElementById('mobile-logout-btn');
// Drawer nav buttons
const drawerNavDict = document.getElementById('drawer-nav-dict');
const drawerNavSrs = document.getElementById('drawer-nav-srs');
const drawerNavReader = document.getElementById('drawer-nav-reader');
const drawerNavCefr = document.getElementById('drawer-nav-cefr');
const drawerNavGrammar = document.getElementById('drawer-nav-grammar');
const drawerNavStats = document.getElementById('drawer-nav-stats');
const drawerNavExport = document.getElementById('drawer-nav-export');
const drawerNavPwd = document.getElementById('drawer-nav-pwd');
const drawerNavTheme = document.getElementById('drawer-nav-theme');
const drawerNavLogout = document.getElementById('drawer-nav-logout');
const exportModal = document.getElementById('export-modal');
const changePasswordModal = document.getElementById('change-password-modal');
const themeToggle = document.getElementById('theme-toggle');
const logoutBtn = document.getElementById('logout-btn');
function openDrawer() {
if (!drawer || !drawerBackdrop) return;
const drawerUserName = document.getElementById('drawer-user-name');
if (drawerUserName && currentUser) {
drawerUserName.textContent = currentUser.username;
}
drawerBackdrop.classList.remove('hidden');
// Force reflow for animation
void drawerBackdrop.offsetWidth;
drawerBackdrop.classList.add('active');
drawer.classList.add('active');
document.body.classList.add('drawer-open');
}
function closeDrawer() {
if (!drawer || !drawerBackdrop) return;
drawer.classList.remove('active');
drawerBackdrop.classList.remove('active');
document.body.classList.remove('drawer-open');
setTimeout(() => {
if (!drawer.classList.contains('active')) {
drawerBackdrop.classList.add('hidden');
}
}, 280);
}
// Opening Triggers
if (drawerToggleBtn) drawerToggleBtn.addEventListener('click', openDrawer);
if (navUserProfileBtn) navUserProfileBtn.addEventListener('click', openDrawer);
if (tabMenu) tabMenu.addEventListener('click', openDrawer);
// Closing Triggers
if (drawerCloseBtn) drawerCloseBtn.addEventListener('click', closeDrawer);
if (drawerBackdrop) drawerBackdrop.addEventListener('click', closeDrawer);
// Escape Key Support
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && drawer && drawer.classList.contains('active')) {
closeDrawer();
}
});
// Touch Swipe Left to Dismiss (Mobile & Tablet)
let touchStartX = 0;
let touchCurrentX = 0;
if (drawer) {
drawer.addEventListener('touchstart', (e) => {
touchStartX = e.touches[0].clientX;
touchCurrentX = touchStartX;
}, { passive: true });
drawer.addEventListener('touchmove', (e) => {
touchCurrentX = e.touches[0].clientX;
}, { passive: true });
drawer.addEventListener('touchend', () => {
if (touchStartX - touchCurrentX > 60 && touchCurrentX > 0) {
closeDrawer();
}
touchStartX = 0;
touchCurrentX = 0;
});
}
// Bottom Tab Items
if (tabDict) tabDict.addEventListener('click', () => { window.location.hash = '#dictionary'; });
if (tabSrs) tabSrs.addEventListener('click', () => { window.location.hash = '#srs'; });
if (tabReader) tabReader.addEventListener('click', () => { window.location.hash = '#reader'; });
if (tabStats) {
tabStats.addEventListener('click', () => {
// Drawer Navigation Actions
if (drawerNavDict) {
drawerNavDict.addEventListener('click', () => {
closeDrawer();
window.location.hash = '#dictionary';
});
}
if (drawerNavSrs) {
drawerNavSrs.addEventListener('click', () => {
closeDrawer();
window.location.hash = '#srs';
});
}
if (drawerNavReader) {
drawerNavReader.addEventListener('click', () => {
closeDrawer();
window.location.hash = '#reader';
});
}
if (drawerNavCefr) {
drawerNavCefr.addEventListener('click', () => {
closeDrawer();
window.location.hash = '#cefr';
});
}
if (drawerNavGrammar) {
drawerNavGrammar.addEventListener('click', () => {
closeDrawer();
window.location.hash = '#grammar';
});
}
if (drawerNavStats) {
drawerNavStats.addEventListener('click', () => {
closeDrawer();
openStatsDashboard();
});
}
function openMobileMenu() {
if (mobileMenuModal) {
const mobileUserName = document.getElementById('mobile-user-name');
if (mobileUserName && currentUser) {
mobileUserName.textContent = currentUser.username;
}
openModal(mobileMenuModal);
}
}
function closeMobileMenu() {
if (mobileMenuModal) {
closeModal(mobileMenuModal);
}
}
if (tabMenu) tabMenu.addEventListener('click', openMobileMenu);
if (mobileMenuToggleBtn) mobileMenuToggleBtn.addEventListener('click', openMobileMenu);
if (mobileMenuCloseBtn) mobileMenuCloseBtn.addEventListener('click', closeMobileMenu);
if (mobileMenuModal) {
mobileMenuModal.addEventListener('click', (e) => {
if (e.target === mobileMenuModal) closeMobileMenu();
});
}
const mobileGrammarBtn = document.getElementById('mobile-grammar-btn');
if (mobileGrammarBtn) {
mobileGrammarBtn.addEventListener('click', () => {
closeMobileMenu();
openGrammarView(null, true);
});
}
const mobileCefrBtn = document.getElementById('mobile-cefr-btn');
if (mobileCefrBtn) {
mobileCefrBtn.addEventListener('click', () => {
closeMobileMenu();
openCefrView(null, 'all', true);
});
}
if (mobileStatsBtn) {
mobileStatsBtn.addEventListener('click', () => {
closeMobileMenu();
openStatsDashboard();
});
}
if (mobileExportBtn && exportModal) {
mobileExportBtn.addEventListener('click', () => {
closeMobileMenu();
if (drawerNavExport && exportModal) {
drawerNavExport.addEventListener('click', () => {
closeDrawer();
openModal(exportModal);
});
}
if (mobilePwdBtn && changePasswordModal) {
mobilePwdBtn.addEventListener('click', () => {
closeMobileMenu();
if (drawerNavPwd && changePasswordModal) {
drawerNavPwd.addEventListener('click', () => {
closeDrawer();
openModal(changePasswordModal);
});
}
if (mobileThemeBtn && themeToggle) {
mobileThemeBtn.addEventListener('click', () => {
if (drawerNavTheme && themeToggle) {
drawerNavTheme.addEventListener('click', () => {
themeToggle.click();
});
}
if (mobileLogoutBtn && logoutBtn) {
mobileLogoutBtn.addEventListener('click', () => {
closeMobileMenu();
if (drawerNavLogout && logoutBtn) {
drawerNavLogout.addEventListener('click', () => {
closeDrawer();
logoutBtn.click();
});
}
@@ -605,8 +689,8 @@ document.addEventListener('DOMContentLoaded', () => {
srsJumpCefrBtn.addEventListener('click', () => { window.location.hash = '#cefr'; });
}
// 6. Mobile Tab Bar & Drawer
initMobileNavigation();
// 6. Navigation Drawer & Mobile Tab Bar
initNavigationDrawer();
// 7. Change Password Modal
initChangePasswordModal();
+15 -1
View File
@@ -96,6 +96,7 @@ export function initDictionary() {
const moveColConfirmBtn = document.getElementById('move-col-confirm-btn');
const exportOpenBtn = document.getElementById('export-open-btn');
const drawerNavExport = document.getElementById('drawer-nav-export');
const exportModal = document.getElementById('export-modal');
const closeExportModalBtn = document.getElementById('close-export-modal-btn');
const exportCancelBtn = document.getElementById('export-cancel-btn');
@@ -556,6 +557,9 @@ export function initDictionary() {
if (exportOpenBtn) {
exportOpenBtn.addEventListener('click', () => openModal(exportModal));
}
if (drawerNavExport) {
drawerNavExport.addEventListener('click', () => openModal(exportModal));
}
closeExportModalBtn.addEventListener('click', () => closeModal(exportModal));
exportCancelBtn.addEventListener('click', () => closeModal(exportModal));
@@ -687,12 +691,22 @@ export function initDictionary() {
if (stats) {
const due = stats.due_cards || 0;
const srsBadge = document.getElementById('srs-badge');
if (srsBadge) srsBadge.textContent = due;
if (srsBadge) {
srsBadge.textContent = due;
srsBadge.style.display = due > 0 ? 'inline-block' : 'none';
}
const tabSrsBadge = document.getElementById('tab-srs-badge');
if (tabSrsBadge) {
tabSrsBadge.textContent = due;
tabSrsBadge.classList.toggle('hidden', due === 0);
}
const drawerSrsBadge = document.getElementById('drawer-srs-badge');
if (drawerSrsBadge) {
drawerSrsBadge.textContent = due;
drawerSrsBadge.classList.toggle('hidden', due === 0);
}
const drawerStatDue = document.getElementById('drawer-stat-due');
if (drawerStatDue) drawerStatDue.textContent = due;
}
} catch (e) {}
}
+2
View File
@@ -115,6 +115,7 @@ export function initSrs() {
// Stats & Heatmap Modal
const statsOpenBtn = document.getElementById('stats-open-btn');
const mobileStatsBtn = document.getElementById('mobile-stats-btn');
const drawerNavStats = document.getElementById('drawer-nav-stats');
const statsModal = document.getElementById('stats-modal');
const closeStatsModalBtn = document.getElementById('close-stats-modal-btn');
const closeStatsBtn = document.getElementById('close-stats-btn');
@@ -646,6 +647,7 @@ export function initSrs() {
// -----------------------------------------------------------------------
if (statsOpenBtn) statsOpenBtn.addEventListener('click', openStatsDashboard);
if (mobileStatsBtn) mobileStatsBtn.addEventListener('click', openStatsDashboard);
if (drawerNavStats) drawerNavStats.addEventListener('click', openStatsDashboard);
closeStatsModalBtn.addEventListener('click', () => closeModal(statsModal));
closeStatsBtn.addEventListener('click', () => closeModal(statsModal));
+20 -1
View File
@@ -139,13 +139,32 @@ export function updateHeaderStats(total = 0, today = 0, srsCount = 0) {
const statTotal = document.getElementById('stat-total');
const statToday = document.getElementById('stat-today');
const srsBadge = document.getElementById('srs-badge');
const tabSrsBadge = document.getElementById('tab-srs-badge');
const drawerSrsBadge = document.getElementById('drawer-srs-badge');
const drawerStatTotal = document.getElementById('drawer-stat-total');
const drawerStatToday = document.getElementById('drawer-stat-today');
const drawerStatDue = document.getElementById('drawer-stat-due');
const formattedToday = today > 0 ? `+${today}` : today;
if (statTotal) statTotal.textContent = total;
if (statToday) statToday.textContent = today;
if (statToday) statToday.textContent = formattedToday;
if (drawerStatTotal) drawerStatTotal.textContent = total;
if (drawerStatToday) drawerStatToday.textContent = formattedToday;
if (drawerStatDue) drawerStatDue.textContent = srsCount;
if (srsBadge) {
srsBadge.textContent = srsCount;
srsBadge.style.display = srsCount > 0 ? 'inline-block' : 'none';
}
if (tabSrsBadge) {
tabSrsBadge.textContent = srsCount;
tabSrsBadge.classList.toggle('hidden', srsCount === 0);
}
if (drawerSrsBadge) {
drawerSrsBadge.textContent = srsCount;
drawerSrsBadge.classList.toggle('hidden', srsCount === 0);
}
}
// -----------------------------------------------------------------------
+1 -1
View File
@@ -1,7 +1,7 @@
// Service Worker for Notes English PWA (Stage 2)
// Strategy: Cache-first for static assets, Network-first for API calls.
const CACHE_NAME = 'notes-v5.6';
const CACHE_NAME = 'notes-v5.7';
const OFFLINE_URL = '/';
const PRECACHE_URLS = [