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:
@@ -377,24 +377,28 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 17. 🍔 Разгрузка хеддера и единая навигация через боковое меню / Drawer (Unified Navigation & Header Redesign)
|
### 17. 🍔 Разгрузка хеддера и единая навигация через боковое меню / Drawer (Unified Navigation & Header Redesign) (Completed)
|
||||||
- [ ] **Проблема перегруженного хеддера**:
|
- [x] **Проблема перегруженного хеддера полностью решена**:
|
||||||
- В шапке скопилось 8+ кнопок (`📚 Читалка`, `🎯 Тесты CEFR`, `📖 Грамматика`, `🎓 Тренажёр`, `📊 Статистика`, `📤 Экспорт`, тема, шестеренка, профиль), на экранах планшетов и ноутбуков возникает переполнение и визуальный шум.
|
- Ликвидировано нагромождение из 8+ кнопок в шапке, устранён визуальный шум и горизонтальный сдвиг на планшетах и компактных экранах.
|
||||||
- Разрозненность UX: на десктопе все кнопки в шапке, на мобильных — нижний таб-бар + модальное меню.
|
- Устранена разрозненность UX: на десктопе, планшете и смартфоне теперь действует единая система навигации.
|
||||||
- [ ] **Единый лаконичный Header (Desktop, Tablet, Mobile)**:
|
- [x] **Единый лаконичный Header (Desktop, Tablet, Mobile)**:
|
||||||
- Слева: Кнопка вызова бокового меню (Бургер `☰` / `ph-list`), кликабельный логотип `📖 Notes` (возврат в словарь).
|
- Слева: Кнопка вызова бокового меню (Бургер `☰` / `ph-list`), кликабельный логотип `📖 Notes` (возврат в словарь).
|
||||||
- Центр: Фокусная кнопка быстрого доступа к активному действию (например, `🎓 Тренажёр SRS` с живым счётчиком повторений `srs-badge`).
|
- Центр: Фокусная кнопка быстрого доступа к главному ежедневному действию (`🎓 Тренажёр SRS` с живым счётчиком повторений `srs-badge`) и компактные счетчики базы (`Всего: X`, `Сегодня: +Y`). На смартфонах счетчики автоматически скрываются, а кнопка сжимается до компактного бейджа `🎓 (N)`.
|
||||||
- Справа: Переключатель темы `🌓`, компактная плашка пользователя/аватар.
|
- Справа: Переключатель темы `🌓`, компактная плашка пользователя/аватар `👤 user` (на смартфонах отображается только иконка аватара).
|
||||||
- [ ] **Единый выдвижной Drawer (Боковое меню навигации)**:
|
- [x] **Единый выдвижной Drawer (Боковое меню навигации)**:
|
||||||
- Плавная аппаратная анимация (`transform: translateX`, `backdrop-filter: blur`).
|
- Плавная аппаратная анимация (`transform: translateX`, `backdrop-filter: blur(4px)`).
|
||||||
|
- Карточка профиля с аватаром, именем пользователя и описанием.
|
||||||
|
- Интерактивный мини-дашборд статистики (Всего в базе, Сегодня, На повторение).
|
||||||
- Логическая группировка пунктов с красивыми иконками:
|
- Логическая группировка пунктов с красивыми иконками:
|
||||||
- **Обучение & Чтение**: Словарь, Читалка, Тесты CEFR, Грамматика Мерфи, Тренажёр SRS.
|
- **Обучение & Чтение**: Словарь & Поиск, Тренажёр SRS (с живым бейджем), Читалка & Книги, Тесты CEFR (A1–B2), Грамматика Мерфи & Тетрадь.
|
||||||
- **Аналитика**: Статистика обучения и календарь активности (Heatmap).
|
- **Аналитика**: Календарь активности & Статистика (Heatmap).
|
||||||
- **Настройки & Профиль**: Экспорт словаря, Смена пароля, Переключение темы, Выход.
|
- **Настройки & Профиль**: Экспорт словаря, Сменить пароль, Переключение темы, Выход из аккаунта.
|
||||||
- Закрытие по клику вне области (оверлей), кнопке `✕`, клавише <kbd>Esc</kbd> и свайпу влево на тач-устройствах (Swipe to dismiss).
|
- Закрытие по клику вне области (оверлей), кнопке `✕`, клавише <kbd>Esc</kbd> и свайпу влево на тач-устройствах (Swipe to dismiss).
|
||||||
- Блокировка скролла фона (`overflow: hidden` на `body`) при открытом Drawer.
|
- Блокировка скролла фона (`overflow: hidden` на `body`) при открытом Drawer.
|
||||||
- [ ] **Оптимизация мобильного режима**:
|
- Подсветка активного раздела (`.active`), синхронизированная с URL-хэшем и переходами между экранами.
|
||||||
- Согласование с Bottom Tab Bar: оставить на нижней панели только 3-4 ключевых быстрых действия либо полностью перевести навигацию в чистый Drawer-паттерн без дублирования элементов.
|
- [x] **Оптимизация мобильного режима**:
|
||||||
|
- На нижней панели (Bottom Tab Bar) оставлены 4 ключевых быстрых действия: `📖 Словарь`, `🎓 Тренажёр`, `📚 Читалка`, `☰ Меню`.
|
||||||
|
- Тап по `☰ Меню` на мобильном открывает единый Drawer, полностью заменив устаревший отдельный модальный слой `mobile-menu-modal` без дублирования верстки и стилей.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
+341
-22
@@ -245,13 +245,15 @@ body {
|
|||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* App Navbar (Unified Layout) */
|
||||||
.navbar {
|
.navbar {
|
||||||
background-color: var(--bg-surface);
|
background-color: var(--bg-surface);
|
||||||
border-bottom: 1px solid var(--border-color);
|
border-bottom: 1px solid var(--border-color);
|
||||||
padding: 12px 24px;
|
padding: 8px 20px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
gap: 16px;
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 0;
|
top: 0;
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
@@ -259,10 +261,46 @@ body {
|
|||||||
box-sizing: border-box;
|
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 {
|
.nav-brand {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
transition: opacity 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-brand:hover {
|
||||||
|
opacity: 0.85;
|
||||||
}
|
}
|
||||||
|
|
||||||
.brand-icon {
|
.brand-icon {
|
||||||
@@ -286,9 +324,18 @@ body {
|
|||||||
margin-left: 4px;
|
margin-left: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.nav-center {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 14px;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.nav-stats {
|
.nav-stats {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 12px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-badge {
|
.stat-badge {
|
||||||
@@ -311,22 +358,286 @@ body {
|
|||||||
.nav-actions {
|
.nav-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 12px;
|
gap: 10px;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-profile {
|
.user-profile-btn {
|
||||||
display: flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
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 {
|
.user-badge {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: var(--text-muted);
|
color: var(--text-main);
|
||||||
font-weight: 600;
|
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);
|
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 */
|
/* Buttons */
|
||||||
@@ -2493,27 +2804,35 @@ body {
|
|||||||
padding-bottom: calc(74px + env(safe-area-inset-bottom, 0px)) !important;
|
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 {
|
.navbar {
|
||||||
padding: calc(8px + env(safe-area-inset-top, 0px)) 12px 8px 12px;
|
padding: calc(8px + env(safe-area-inset-top, 0px)) 10px 8px 10px;
|
||||||
position: sticky;
|
gap: 8px;
|
||||||
top: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.brand-sub,
|
.brand-sub,
|
||||||
.nav-center,
|
.nav-stats {
|
||||||
.nav-actions #nav-reader-btn,
|
|
||||||
.nav-actions #srs-trainer-btn,
|
|
||||||
.nav-actions #stats-open-btn,
|
|
||||||
.nav-actions #export-open-btn,
|
|
||||||
.nav-actions .user-profile {
|
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mobile-only-btn {
|
.btn-trainer .trainer-text {
|
||||||
display: inline-flex !important;
|
display: none !important;
|
||||||
align-items: center;
|
}
|
||||||
justify-content: center;
|
|
||||||
|
.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 {
|
.mobile-tab-bar {
|
||||||
|
|||||||
+131
-61
@@ -52,48 +52,37 @@
|
|||||||
<!-- Main Application Shell -->
|
<!-- Main Application Shell -->
|
||||||
<div id="app-shell" class="app-shell hidden">
|
<div id="app-shell" class="app-shell hidden">
|
||||||
<!-- Top Navigation Bar -->
|
<!-- Top Navigation Bar -->
|
||||||
<header class="navbar">
|
<header class="navbar" id="main-navbar">
|
||||||
<div class="nav-brand" id="nav-brand-home" title="На главную">
|
<div class="nav-left">
|
||||||
<span class="brand-icon">📖</span>
|
<button id="drawer-toggle-btn" class="icon-btn drawer-toggle-btn" title="Меню навигации (☰)" aria-label="Открыть меню">
|
||||||
<span class="brand-title">Notes</span>
|
<span class="hamburger-icon">☰</span>
|
||||||
<span class="brand-sub">English Lexicon</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>
|
||||||
|
|
||||||
<div class="nav-center">
|
<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">
|
<div class="nav-stats" id="nav-stats">
|
||||||
<span class="stat-badge" title="Всего слов в словаре">Всего: <strong id="stat-total">0</strong></span>
|
<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>
|
<span class="stat-badge stat-today" title="Добавлено сегодня">Сегодня: <strong id="stat-today">+0</strong></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="nav-actions">
|
<div class="nav-actions nav-right">
|
||||||
<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>
|
|
||||||
<button id="theme-toggle" class="icon-btn" title="Переключить тему">🌓</button>
|
<button id="theme-toggle" class="icon-btn" title="Переключить тему">🌓</button>
|
||||||
<button id="mobile-menu-toggle-btn" class="icon-btn mobile-only-btn" title="Меню профиля и настроек">⚙️</button>
|
<button id="nav-user-profile-btn" class="user-profile-btn" title="Меню профиля и настройки">
|
||||||
<div class="user-profile">
|
<span class="user-avatar-icon">👤</span>
|
||||||
<span id="user-display-name" class="user-badge">user</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>
|
||||||
<button id="logout-btn" class="btn btn-sm btn-outline" title="Выйти из аккаунта">Выход</button>
|
<button id="logout-btn" class="hidden" style="display: none;" title="Выйти из аккаунта"></button>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
@@ -881,7 +870,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</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">
|
<nav class="mobile-tab-bar" id="mobile-tab-bar">
|
||||||
<button class="tab-item active" data-view="dictionary-view" id="tab-nav-dict">
|
<button class="tab-item active" data-view="dictionary-view" id="tab-nav-dict">
|
||||||
<span class="tab-icon">📖</span>
|
<span class="tab-icon">📖</span>
|
||||||
@@ -896,44 +885,125 @@
|
|||||||
<span class="tab-icon">📚</span>
|
<span class="tab-icon">📚</span>
|
||||||
<span class="tab-label">Читалка</span>
|
<span class="tab-label">Читалка</span>
|
||||||
</button>
|
</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">
|
<button class="tab-item" id="tab-nav-menu">
|
||||||
<span class="tab-icon">⚙️</span>
|
<span class="tab-icon">☰</span>
|
||||||
<span class="tab-label">Меню</span>
|
<span class="tab-label">Меню</span>
|
||||||
</button>
|
</button>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Mobile Menu / Profile Sheet Modal -->
|
<!-- Navigation Drawer (Unified Side Drawer for Desktop & Mobile) -->
|
||||||
<div id="mobile-menu-modal" class="modal-backdrop hidden">
|
<div id="app-drawer-backdrop" class="drawer-backdrop hidden" aria-hidden="true"></div>
|
||||||
<div class="modal-window mobile-sheet-window">
|
<aside id="app-drawer" class="app-drawer" aria-label="Боковое меню навигации">
|
||||||
<div class="modal-header">
|
<div class="drawer-header">
|
||||||
<h3>⚙️ Профиль & Меню</h3>
|
<div class="drawer-user-card" id="drawer-user-card">
|
||||||
<button class="modal-close-btn" id="mobile-menu-close-btn">✕</button>
|
<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>
|
||||||
<div class="modal-body mobile-menu-body">
|
<button id="app-drawer-close-btn" class="icon-btn drawer-close-btn" title="Закрыть меню (Esc)" aria-label="Закрыть">✕</button>
|
||||||
<div class="mobile-user-card">
|
</div>
|
||||||
<span class="mobile-user-avatar">👤</span>
|
|
||||||
<div class="mobile-user-info">
|
<!-- Quick Stats in Drawer -->
|
||||||
<strong id="mobile-user-name">user</strong>
|
<div class="drawer-stats-summary" id="drawer-stats-summary">
|
||||||
<p class="subtitle">Личный словарь English Lexicon</p>
|
<div class="drawer-stat-item">
|
||||||
</div>
|
<span class="drawer-stat-val" id="drawer-stat-total">0</span>
|
||||||
</div>
|
<span class="drawer-stat-lbl">Слов в базе</span>
|
||||||
<div class="mobile-menu-actions">
|
</div>
|
||||||
<button id="mobile-grammar-btn" class="btn btn-outline btn-block">📖 Грамматика Мерфи & Тетрадь</button>
|
<div class="drawer-stat-item">
|
||||||
<button id="mobile-cefr-btn" class="btn btn-outline btn-block">🎯 Уровневые тесты CEFR (A1–B2)</button>
|
<span class="drawer-stat-val text-success" id="drawer-stat-today">+0</span>
|
||||||
<button id="mobile-stats-btn" class="btn btn-outline btn-block">📊 Статистика & Календарь</button>
|
<span class="drawer-stat-lbl">Сегодня</span>
|
||||||
<button id="mobile-export-btn" class="btn btn-outline btn-block">📤 Экспорт словаря</button>
|
</div>
|
||||||
<button id="mobile-pwd-btn" class="btn btn-outline btn-block">🔑 Сменить пароль</button>
|
<div class="drawer-stat-item">
|
||||||
<button id="mobile-theme-btn" class="btn btn-outline btn-block">🌓 Переключить тему</button>
|
<span class="drawer-stat-val text-warning" id="drawer-stat-due">0</span>
|
||||||
<button id="mobile-logout-btn" class="btn btn-danger btn-block">Выйти из аккаунта</button>
|
<span class="drawer-stat-lbl">Повторение</span>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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 -->
|
<!-- Analytics & Activity Heatmap Modal -->
|
||||||
<div id="stats-modal" class="modal-backdrop hidden">
|
<div id="stats-modal" class="modal-backdrop hidden">
|
||||||
|
|||||||
+179
-95
@@ -15,15 +15,45 @@ let currentUser = null;
|
|||||||
let isRouting = false;
|
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 => {
|
document.querySelectorAll('.mobile-tab-bar .tab-item').forEach(btn => {
|
||||||
btn.classList.remove('active');
|
btn.classList.remove('active');
|
||||||
});
|
});
|
||||||
if (!tabName) return;
|
if (viewName === 'dict' || viewName === 'dictionary') {
|
||||||
const target = document.getElementById(`tab-nav-${tabName}`);
|
const t = document.getElementById('tab-nav-dict');
|
||||||
if (target) target.classList.add('active');
|
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 (cefrView) cefrView.classList.add('hidden');
|
||||||
if (grammarView) grammarView.classList.add('hidden');
|
if (grammarView) grammarView.classList.add('hidden');
|
||||||
if (readerView) readerView.classList.remove('hidden');
|
if (readerView) readerView.classList.remove('hidden');
|
||||||
updateMobileActiveTab('reader');
|
updateActiveNavState('reader');
|
||||||
if (hash.includes('/book/')) {
|
if (hash.includes('/book/')) {
|
||||||
if (bookshelfContainer) bookshelfContainer.classList.add('hidden');
|
if (bookshelfContainer) bookshelfContainer.classList.add('hidden');
|
||||||
if (readerInterface) readerInterface.classList.remove('hidden');
|
if (readerInterface) readerInterface.classList.remove('hidden');
|
||||||
@@ -74,21 +104,21 @@ export function applyInitialViewPreflight() {
|
|||||||
if (cefrView) cefrView.classList.add('hidden');
|
if (cefrView) cefrView.classList.add('hidden');
|
||||||
if (grammarView) grammarView.classList.add('hidden');
|
if (grammarView) grammarView.classList.add('hidden');
|
||||||
if (srsView) srsView.classList.remove('hidden');
|
if (srsView) srsView.classList.remove('hidden');
|
||||||
updateMobileActiveTab('srs');
|
updateActiveNavState('srs');
|
||||||
} else if (hash.startsWith('#cefr')) {
|
} else if (hash.startsWith('#cefr')) {
|
||||||
if (dictionaryView) dictionaryView.classList.add('hidden');
|
if (dictionaryView) dictionaryView.classList.add('hidden');
|
||||||
if (readerView) readerView.classList.add('hidden');
|
if (readerView) readerView.classList.add('hidden');
|
||||||
if (srsView) srsView.classList.add('hidden');
|
if (srsView) srsView.classList.add('hidden');
|
||||||
if (grammarView) grammarView.classList.add('hidden');
|
if (grammarView) grammarView.classList.add('hidden');
|
||||||
if (cefrView) cefrView.classList.remove('hidden');
|
if (cefrView) cefrView.classList.remove('hidden');
|
||||||
updateMobileActiveTab('');
|
updateActiveNavState('cefr');
|
||||||
} else if (hash.startsWith('#grammar')) {
|
} else if (hash.startsWith('#grammar')) {
|
||||||
if (dictionaryView) dictionaryView.classList.add('hidden');
|
if (dictionaryView) dictionaryView.classList.add('hidden');
|
||||||
if (readerView) readerView.classList.add('hidden');
|
if (readerView) readerView.classList.add('hidden');
|
||||||
if (srsView) srsView.classList.add('hidden');
|
if (srsView) srsView.classList.add('hidden');
|
||||||
if (cefrView) cefrView.classList.add('hidden');
|
if (cefrView) cefrView.classList.add('hidden');
|
||||||
if (grammarView) grammarView.classList.remove('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 (cefrView) cefrView.classList.add('hidden');
|
||||||
if (grammarView) grammarView.classList.add('hidden');
|
if (grammarView) grammarView.classList.add('hidden');
|
||||||
if (dictionaryView) dictionaryView.classList.remove('hidden');
|
if (dictionaryView) dictionaryView.classList.remove('hidden');
|
||||||
updateMobileActiveTab('dict');
|
updateActiveNavState('dict');
|
||||||
try { localStorage.setItem('notes_active_view', 'dictionary-view'); } catch (e) {}
|
try { localStorage.setItem('notes_active_view', 'dictionary-view'); } catch (e) {}
|
||||||
loadInitialData();
|
loadInitialData();
|
||||||
}
|
}
|
||||||
@@ -133,7 +163,7 @@ export function openSrsView(tagFilter = null, updateHash = true) {
|
|||||||
if (cefrView) cefrView.classList.add('hidden');
|
if (cefrView) cefrView.classList.add('hidden');
|
||||||
if (grammarView) grammarView.classList.add('hidden');
|
if (grammarView) grammarView.classList.add('hidden');
|
||||||
if (srsView) srsView.classList.remove('hidden');
|
if (srsView) srsView.classList.remove('hidden');
|
||||||
updateMobileActiveTab('srs');
|
updateActiveNavState('srs');
|
||||||
try { localStorage.setItem('notes_active_view', 'srs-view'); } catch (e) {}
|
try { localStorage.setItem('notes_active_view', 'srs-view'); } catch (e) {}
|
||||||
startSrsTrainingSession(false, tagFilter);
|
startSrsTrainingSession(false, tagFilter);
|
||||||
}
|
}
|
||||||
@@ -162,7 +192,7 @@ export function openBookshelfView(updateHash = true) {
|
|||||||
if (readerView) readerView.classList.remove('hidden');
|
if (readerView) readerView.classList.remove('hidden');
|
||||||
if (bookshelfContainer) bookshelfContainer.classList.remove('hidden');
|
if (bookshelfContainer) bookshelfContainer.classList.remove('hidden');
|
||||||
if (readerInterface) readerInterface.classList.add('hidden');
|
if (readerInterface) readerInterface.classList.add('hidden');
|
||||||
updateMobileActiveTab('reader');
|
updateActiveNavState('reader');
|
||||||
try {
|
try {
|
||||||
localStorage.setItem('notes_active_view', 'reader-view');
|
localStorage.setItem('notes_active_view', 'reader-view');
|
||||||
localStorage.removeItem('notes_active_book_id');
|
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 (grammarView) grammarView.classList.add('hidden');
|
||||||
if (cefrView) cefrView.classList.remove('hidden');
|
if (cefrView) cefrView.classList.remove('hidden');
|
||||||
|
|
||||||
updateMobileActiveTab('');
|
updateActiveNavState('cefr');
|
||||||
try { localStorage.setItem('notes_active_view', 'cefr-view'); } catch (e) {}
|
try { localStorage.setItem('notes_active_view', 'cefr-view'); } catch (e) {}
|
||||||
|
|
||||||
if (level) {
|
if (level) {
|
||||||
@@ -231,7 +261,7 @@ export function openGrammarView(unitId = null, updateHash = true) {
|
|||||||
if (cefrView) cefrView.classList.add('hidden');
|
if (cefrView) cefrView.classList.add('hidden');
|
||||||
if (grammarView) grammarView.classList.remove('hidden');
|
if (grammarView) grammarView.classList.remove('hidden');
|
||||||
|
|
||||||
updateMobileActiveTab('');
|
updateActiveNavState('grammar');
|
||||||
try { localStorage.setItem('notes_active_view', 'grammar-view'); } catch (e) {}
|
try { localStorage.setItem('notes_active_view', 'grammar-view'); } catch (e) {}
|
||||||
|
|
||||||
if (unitId) {
|
if (unitId) {
|
||||||
@@ -349,122 +379,176 @@ export function showApp() {
|
|||||||
const loginOverlay = document.getElementById('login-overlay');
|
const loginOverlay = document.getElementById('login-overlay');
|
||||||
const appShell = document.getElementById('app-shell');
|
const appShell = document.getElementById('app-shell');
|
||||||
const userDisplayName = document.getElementById('user-display-name');
|
const userDisplayName = document.getElementById('user-display-name');
|
||||||
|
const drawerUserName = document.getElementById('drawer-user-name');
|
||||||
const mobileUserName = document.getElementById('mobile-user-name');
|
const mobileUserName = document.getElementById('mobile-user-name');
|
||||||
|
|
||||||
if (loginOverlay) loginOverlay.classList.add('hidden');
|
if (loginOverlay) loginOverlay.classList.add('hidden');
|
||||||
if (appShell) appShell.classList.remove('hidden');
|
if (appShell) appShell.classList.remove('hidden');
|
||||||
const name = currentUser ? currentUser.username : 'user';
|
const name = currentUser ? currentUser.username : 'user';
|
||||||
if (userDisplayName) userDisplayName.textContent = name;
|
if (userDisplayName) userDisplayName.textContent = name;
|
||||||
|
if (drawerUserName) drawerUserName.textContent = name;
|
||||||
if (mobileUserName) mobileUserName.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 tabDict = document.getElementById('tab-nav-dict');
|
||||||
const tabSrs = document.getElementById('tab-nav-srs');
|
const tabSrs = document.getElementById('tab-nav-srs');
|
||||||
const tabReader = document.getElementById('tab-nav-reader');
|
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');
|
// Drawer nav buttons
|
||||||
const mobileMenuToggleBtn = document.getElementById('mobile-menu-toggle-btn');
|
const drawerNavDict = document.getElementById('drawer-nav-dict');
|
||||||
const mobileMenuCloseBtn = document.getElementById('mobile-menu-close-btn');
|
const drawerNavSrs = document.getElementById('drawer-nav-srs');
|
||||||
|
const drawerNavReader = document.getElementById('drawer-nav-reader');
|
||||||
const mobileStatsBtn = document.getElementById('mobile-stats-btn');
|
const drawerNavCefr = document.getElementById('drawer-nav-cefr');
|
||||||
const mobileExportBtn = document.getElementById('mobile-export-btn');
|
const drawerNavGrammar = document.getElementById('drawer-nav-grammar');
|
||||||
const mobilePwdBtn = document.getElementById('mobile-pwd-btn');
|
const drawerNavStats = document.getElementById('drawer-nav-stats');
|
||||||
const mobileThemeBtn = document.getElementById('mobile-theme-btn');
|
const drawerNavExport = document.getElementById('drawer-nav-export');
|
||||||
const mobileLogoutBtn = document.getElementById('mobile-logout-btn');
|
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 exportModal = document.getElementById('export-modal');
|
||||||
const changePasswordModal = document.getElementById('change-password-modal');
|
const changePasswordModal = document.getElementById('change-password-modal');
|
||||||
const themeToggle = document.getElementById('theme-toggle');
|
const themeToggle = document.getElementById('theme-toggle');
|
||||||
const logoutBtn = document.getElementById('logout-btn');
|
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 (tabDict) tabDict.addEventListener('click', () => { window.location.hash = '#dictionary'; });
|
||||||
if (tabSrs) tabSrs.addEventListener('click', () => { window.location.hash = '#srs'; });
|
if (tabSrs) tabSrs.addEventListener('click', () => { window.location.hash = '#srs'; });
|
||||||
if (tabReader) tabReader.addEventListener('click', () => { window.location.hash = '#reader'; });
|
if (tabReader) tabReader.addEventListener('click', () => { window.location.hash = '#reader'; });
|
||||||
|
|
||||||
if (tabStats) {
|
// Drawer Navigation Actions
|
||||||
tabStats.addEventListener('click', () => {
|
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();
|
openStatsDashboard();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if (drawerNavExport && exportModal) {
|
||||||
function openMobileMenu() {
|
drawerNavExport.addEventListener('click', () => {
|
||||||
if (mobileMenuModal) {
|
closeDrawer();
|
||||||
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();
|
|
||||||
openModal(exportModal);
|
openModal(exportModal);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if (drawerNavPwd && changePasswordModal) {
|
||||||
if (mobilePwdBtn && changePasswordModal) {
|
drawerNavPwd.addEventListener('click', () => {
|
||||||
mobilePwdBtn.addEventListener('click', () => {
|
closeDrawer();
|
||||||
closeMobileMenu();
|
|
||||||
openModal(changePasswordModal);
|
openModal(changePasswordModal);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if (drawerNavTheme && themeToggle) {
|
||||||
if (mobileThemeBtn && themeToggle) {
|
drawerNavTheme.addEventListener('click', () => {
|
||||||
mobileThemeBtn.addEventListener('click', () => {
|
|
||||||
themeToggle.click();
|
themeToggle.click();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if (drawerNavLogout && logoutBtn) {
|
||||||
if (mobileLogoutBtn && logoutBtn) {
|
drawerNavLogout.addEventListener('click', () => {
|
||||||
mobileLogoutBtn.addEventListener('click', () => {
|
closeDrawer();
|
||||||
closeMobileMenu();
|
|
||||||
logoutBtn.click();
|
logoutBtn.click();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -605,8 +689,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
srsJumpCefrBtn.addEventListener('click', () => { window.location.hash = '#cefr'; });
|
srsJumpCefrBtn.addEventListener('click', () => { window.location.hash = '#cefr'; });
|
||||||
}
|
}
|
||||||
|
|
||||||
// 6. Mobile Tab Bar & Drawer
|
// 6. Navigation Drawer & Mobile Tab Bar
|
||||||
initMobileNavigation();
|
initNavigationDrawer();
|
||||||
|
|
||||||
// 7. Change Password Modal
|
// 7. Change Password Modal
|
||||||
initChangePasswordModal();
|
initChangePasswordModal();
|
||||||
|
|||||||
@@ -96,6 +96,7 @@ export function initDictionary() {
|
|||||||
const moveColConfirmBtn = document.getElementById('move-col-confirm-btn');
|
const moveColConfirmBtn = document.getElementById('move-col-confirm-btn');
|
||||||
|
|
||||||
const exportOpenBtn = document.getElementById('export-open-btn');
|
const exportOpenBtn = document.getElementById('export-open-btn');
|
||||||
|
const drawerNavExport = document.getElementById('drawer-nav-export');
|
||||||
const exportModal = document.getElementById('export-modal');
|
const exportModal = document.getElementById('export-modal');
|
||||||
const closeExportModalBtn = document.getElementById('close-export-modal-btn');
|
const closeExportModalBtn = document.getElementById('close-export-modal-btn');
|
||||||
const exportCancelBtn = document.getElementById('export-cancel-btn');
|
const exportCancelBtn = document.getElementById('export-cancel-btn');
|
||||||
@@ -556,6 +557,9 @@ export function initDictionary() {
|
|||||||
if (exportOpenBtn) {
|
if (exportOpenBtn) {
|
||||||
exportOpenBtn.addEventListener('click', () => openModal(exportModal));
|
exportOpenBtn.addEventListener('click', () => openModal(exportModal));
|
||||||
}
|
}
|
||||||
|
if (drawerNavExport) {
|
||||||
|
drawerNavExport.addEventListener('click', () => openModal(exportModal));
|
||||||
|
}
|
||||||
closeExportModalBtn.addEventListener('click', () => closeModal(exportModal));
|
closeExportModalBtn.addEventListener('click', () => closeModal(exportModal));
|
||||||
exportCancelBtn.addEventListener('click', () => closeModal(exportModal));
|
exportCancelBtn.addEventListener('click', () => closeModal(exportModal));
|
||||||
|
|
||||||
@@ -687,12 +691,22 @@ export function initDictionary() {
|
|||||||
if (stats) {
|
if (stats) {
|
||||||
const due = stats.due_cards || 0;
|
const due = stats.due_cards || 0;
|
||||||
const srsBadge = document.getElementById('srs-badge');
|
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');
|
const tabSrsBadge = document.getElementById('tab-srs-badge');
|
||||||
if (tabSrsBadge) {
|
if (tabSrsBadge) {
|
||||||
tabSrsBadge.textContent = due;
|
tabSrsBadge.textContent = due;
|
||||||
tabSrsBadge.classList.toggle('hidden', due === 0);
|
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) {}
|
} catch (e) {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -115,6 +115,7 @@ export function initSrs() {
|
|||||||
// Stats & Heatmap Modal
|
// Stats & Heatmap Modal
|
||||||
const statsOpenBtn = document.getElementById('stats-open-btn');
|
const statsOpenBtn = document.getElementById('stats-open-btn');
|
||||||
const mobileStatsBtn = document.getElementById('mobile-stats-btn');
|
const mobileStatsBtn = document.getElementById('mobile-stats-btn');
|
||||||
|
const drawerNavStats = document.getElementById('drawer-nav-stats');
|
||||||
const statsModal = document.getElementById('stats-modal');
|
const statsModal = document.getElementById('stats-modal');
|
||||||
const closeStatsModalBtn = document.getElementById('close-stats-modal-btn');
|
const closeStatsModalBtn = document.getElementById('close-stats-modal-btn');
|
||||||
const closeStatsBtn = document.getElementById('close-stats-btn');
|
const closeStatsBtn = document.getElementById('close-stats-btn');
|
||||||
@@ -646,6 +647,7 @@ export function initSrs() {
|
|||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
if (statsOpenBtn) statsOpenBtn.addEventListener('click', openStatsDashboard);
|
if (statsOpenBtn) statsOpenBtn.addEventListener('click', openStatsDashboard);
|
||||||
if (mobileStatsBtn) mobileStatsBtn.addEventListener('click', openStatsDashboard);
|
if (mobileStatsBtn) mobileStatsBtn.addEventListener('click', openStatsDashboard);
|
||||||
|
if (drawerNavStats) drawerNavStats.addEventListener('click', openStatsDashboard);
|
||||||
closeStatsModalBtn.addEventListener('click', () => closeModal(statsModal));
|
closeStatsModalBtn.addEventListener('click', () => closeModal(statsModal));
|
||||||
closeStatsBtn.addEventListener('click', () => closeModal(statsModal));
|
closeStatsBtn.addEventListener('click', () => closeModal(statsModal));
|
||||||
|
|
||||||
|
|||||||
@@ -139,13 +139,32 @@ export function updateHeaderStats(total = 0, today = 0, srsCount = 0) {
|
|||||||
const statTotal = document.getElementById('stat-total');
|
const statTotal = document.getElementById('stat-total');
|
||||||
const statToday = document.getElementById('stat-today');
|
const statToday = document.getElementById('stat-today');
|
||||||
const srsBadge = document.getElementById('srs-badge');
|
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 (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) {
|
if (srsBadge) {
|
||||||
srsBadge.textContent = srsCount;
|
srsBadge.textContent = srsCount;
|
||||||
srsBadge.style.display = srsCount > 0 ? 'inline-block' : 'none';
|
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
@@ -1,7 +1,7 @@
|
|||||||
// Service Worker for Notes English PWA (Stage 2)
|
// Service Worker for Notes English PWA (Stage 2)
|
||||||
// Strategy: Cache-first for static assets, Network-first for API calls.
|
// 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 OFFLINE_URL = '/';
|
||||||
|
|
||||||
const PRECACHE_URLS = [
|
const PRECACHE_URLS = [
|
||||||
|
|||||||
Reference in New Issue
Block a user