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
+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();