Files
notes-qpid-pro/app/static/js/modules/ui.js
T
Antigravity e73d3470b3 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
2026-10-06 21:28:25 +03:00

182 lines
6.1 KiB
JavaScript

// =======================================================================
// Notes Application - UI & Utilities Module (Native ES Module)
// =======================================================================
export function escapeHtml(str) {
if (!str) return '';
return String(str)
.replace(/&/g, '&')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
}
export function escapeRegex(string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
export function slugify(text) {
if (!text) return 'book';
return text.toString().toLowerCase().trim()
.replace(/[^\w\s-]/g, '')
.replace(/[\s_-]+/g, '-')
.replace(/^-+|-+$/g, '');
}
export function shuffleArray(array) {
const arr = [...array];
for (let i = arr.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[arr[i], arr[j]] = [arr[j], arr[i]];
}
return arr;
}
// -----------------------------------------------------------------------
// Toast Notifications
// -----------------------------------------------------------------------
export function showToast(message, type = 'info') {
let toastContainer = document.getElementById('notes-toast-container');
if (!toastContainer) {
toastContainer = document.createElement('div');
toastContainer.id = 'notes-toast-container';
toastContainer.style.cssText = `
position: fixed;
bottom: 24px;
right: 24px;
z-index: 10000;
display: flex;
flex-direction: column;
gap: 8px;
pointer-events: none;
`;
document.body.appendChild(toastContainer);
}
const toast = document.createElement('div');
const colors = {
success: '#10b981',
error: '#ef4444',
warning: '#f59e0b',
info: '#3b82f6'
};
const bg = colors[type] || colors.info;
toast.style.cssText = `
background: ${bg};
color: #ffffff;
padding: 10px 18px;
border-radius: 8px;
font-size: 14px;
font-weight: 500;
box-shadow: 0 4px 14px rgba(0,0,0,0.25);
opacity: 0;
transform: translateY(12px);
transition: all 0.25s ease;
pointer-events: auto;
max-width: 340px;
`;
toast.textContent = message;
toastContainer.appendChild(toast);
requestAnimationFrame(() => {
toast.style.opacity = '1';
toast.style.transform = 'translateY(0)';
});
setTimeout(() => {
toast.style.opacity = '0';
toast.style.transform = 'translateY(12px)';
setTimeout(() => toast.remove(), 250);
}, 3500);
}
// -----------------------------------------------------------------------
// Theme Management
// -----------------------------------------------------------------------
export function initTheme() {
const themeToggle = document.getElementById('theme-toggle');
const mobileThemeBtn = document.getElementById('mobile-theme-btn');
const savedTheme = localStorage.getItem('notes_theme') || 'dark';
document.documentElement.setAttribute('data-theme', savedTheme);
function toggle() {
const current = document.documentElement.getAttribute('data-theme');
const next = current === 'dark' ? 'light' : 'dark';
document.documentElement.setAttribute('data-theme', next);
localStorage.setItem('notes_theme', next);
}
if (themeToggle) {
themeToggle.addEventListener('click', toggle);
}
if (mobileThemeBtn) {
mobileThemeBtn.addEventListener('click', () => {
toggle();
showToast(`Тема переключена`, 'info');
});
}
}
// -----------------------------------------------------------------------
// Modal Open / Close Helpers
// -----------------------------------------------------------------------
export function openModal(modalElem) {
if (!modalElem) return;
modalElem.classList.remove('hidden');
}
export function closeModal(modalElem) {
if (!modalElem) return;
modalElem.classList.add('hidden');
}
// -----------------------------------------------------------------------
// Header Stat Counters
// -----------------------------------------------------------------------
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 = 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);
}
}
// -----------------------------------------------------------------------
// Audio Player Helper (Safe stub / audio stream)
// -----------------------------------------------------------------------
export function playAudio(url) {
if (!url) return;
try {
const audio = new Audio(url);
audio.play().catch(e => console.warn('Audio playback not supported or blocked:', e));
} catch (e) {
console.warn('Audio play error:', e);
}
}