diff --git a/public/router.js b/public/router.js index b2d17ef..011a0a9 100644 --- a/public/router.js +++ b/public/router.js @@ -8,6 +8,7 @@ import { api, auth } from '/api.js'; import { initI18n, getLocale, t } from '/i18n.js'; import { esc } from '/utils/html.js'; import { init as initReminders, stop as stopReminders } from '/reminders.js'; +import { isKitchenRoute, getLastKitchenRoute } from '/utils/kitchen-tabs.js'; // -------------------------------------------------------- // Routen-Definitionen @@ -128,10 +129,10 @@ let _pendingLoginRedirect = false; // Router // -------------------------------------------------------- -const ROUTE_ORDER = ['/', '/tasks', '/calendar', '/birthdays', '/meals', '/recipes', '/shopping', - '/notes', '/contacts', '/budget', '/documents', '/settings']; +const ROUTE_ORDER = ['/', '/calendar', '/tasks', '/meals', '/recipes', '/shopping', + '/birthdays', '/notes', '/contacts', '/budget', '/documents', '/settings']; -const PRIMARY_NAV = 3; +const PRIMARY_NAV = 2; const DEFAULT_APP_NAME = 'Oikos'; const APP_NAME_STORAGE_KEY = 'oikos-app-name'; @@ -478,6 +479,42 @@ function renderAppShell(container) { const bottomItems = document.createElement('div'); bottomItems.className = 'nav-bottom__items'; navItems().slice(0, PRIMARY_NAV).forEach((item) => bottomItems.appendChild(navItemEl(item))); + + const kitchenBtn = document.createElement('button'); + kitchenBtn.className = 'nav-item nav-item--kitchen'; + kitchenBtn.id = 'kitchen-btn'; + kitchenBtn.type = 'button'; + kitchenBtn.setAttribute('aria-label', t('nav.kitchen')); + kitchenBtn.setAttribute('title', t('nav.kitchen')); + const kitchenBtnIcon = document.createElement('i'); + kitchenBtnIcon.dataset.lucide = 'utensils'; + kitchenBtnIcon.className = 'nav-item__icon'; + kitchenBtnIcon.setAttribute('aria-hidden', 'true'); + const kitchenBtnLabel = document.createElement('span'); + kitchenBtnLabel.className = 'nav-item__label'; + kitchenBtnLabel.textContent = t('nav.kitchen'); + kitchenBtn.appendChild(kitchenBtnIcon); + kitchenBtn.appendChild(kitchenBtnLabel); + kitchenBtn.addEventListener('click', () => navigate(getLastKitchenRoute())); + bottomItems.appendChild(kitchenBtn); + + const searchNavBtn = document.createElement('button'); + searchNavBtn.className = 'nav-item nav-item--search'; + searchNavBtn.id = 'search-btn'; + searchNavBtn.type = 'button'; + searchNavBtn.setAttribute('aria-label', t('nav.search')); + searchNavBtn.setAttribute('title', t('nav.search')); + const searchNavIcon = document.createElement('i'); + searchNavIcon.dataset.lucide = 'search'; + searchNavIcon.className = 'nav-item__icon'; + searchNavIcon.setAttribute('aria-hidden', 'true'); + const searchNavLabel = document.createElement('span'); + searchNavLabel.className = 'nav-item__label'; + searchNavLabel.textContent = t('nav.search'); + searchNavBtn.appendChild(searchNavIcon); + searchNavBtn.appendChild(searchNavLabel); + bottomItems.appendChild(searchNavBtn); + const moreBtn = document.createElement('button'); moreBtn.className = 'nav-item nav-item--more'; moreBtn.id = 'more-btn'; @@ -506,21 +543,11 @@ function renderAppShell(container) { moreSheet.setAttribute('role', 'dialog'); moreSheet.setAttribute('aria-label', t('nav.more')); moreSheet.setAttribute('aria-hidden', 'true'); - const searchTrigger = document.createElement('button'); - searchTrigger.className = 'more-sheet__search-trigger'; - searchTrigger.id = 'search-btn'; - searchTrigger.setAttribute('aria-label', t('search.title')); - const searchTriggerIcon = document.createElement('i'); - searchTriggerIcon.dataset.lucide = 'search'; - searchTriggerIcon.className = 'more-sheet__search-trigger-icon'; - searchTriggerIcon.setAttribute('aria-hidden', 'true'); - const searchTriggerText = document.createElement('span'); - searchTriggerText.className = 'more-sheet__search-trigger-placeholder'; - searchTriggerText.textContent = t('search.placeholder'); - searchTrigger.appendChild(searchTriggerIcon); - searchTrigger.appendChild(searchTriggerText); - moreSheet.appendChild(searchTrigger); - navItems().slice(PRIMARY_NAV).forEach((item) => moreSheet.appendChild(moreItemEl(item))); + const dragHandle = document.createElement('div'); + dragHandle.className = 'more-sheet__handle'; + dragHandle.setAttribute('aria-hidden', 'true'); + moreSheet.insertAdjacentElement('afterbegin', dragHandle); + navItems().filter((i) => !i.sidebarOnly).slice(PRIMARY_NAV).forEach((item) => moreSheet.appendChild(moreItemEl(item))); const searchOverlay = document.createElement('div'); searchOverlay.className = 'search-overlay'; @@ -588,7 +615,11 @@ const SHORTCUTS = [ { key: 'g t', description: () => t('shortcuts.goTasks'), action: () => navigate('/tasks') }, { key: 'g c', description: () => t('shortcuts.goCal'), action: () => navigate('/calendar') }, { key: 'g s', description: () => t('shortcuts.goShop'), action: () => navigate('/shopping') }, - { key: 'g n', description: () => t('shortcuts.goNotes'), action: () => navigate('/notes') }, + { key: 'g n', description: () => t('shortcuts.goNotes'), action: () => navigate('/notes') }, + { key: 'g k', description: () => t('shortcuts.goKitchen'), action: () => navigate(getLastKitchenRoute()) }, + { key: 'g k m', description: () => t('shortcuts.goKitchen'), action: () => navigate('/meals') }, + { key: 'g k r', description: () => t('shortcuts.goKitchen'), action: () => navigate('/recipes') }, + { key: 'g k s', description: () => t('shortcuts.goKitchen'), action: () => navigate('/shopping') }, ]; let _pendingKey = null; @@ -603,8 +634,32 @@ function initKeyboardShortcuts() { const key = e.key.toLowerCase(); + // 3-Tasten-Chord: g k {m|r|s} + if (_pendingKey === 'g k') { + clearTimeout(_pendingTimer); + _pendingKey = null; + const chord3 = `g k ${key}`; + const s3 = SHORTCUTS.find((s) => s.key === chord3); + if (s3) { e.preventDefault(); s3.action(); return; } + // Kein 3-Chord-Match → g k selbst ausführen + const gk = SHORTCUTS.find((s) => s.key === 'g k'); + if (gk) { e.preventDefault(); gk.action(); } + return; + } + + // 2-Tasten-Chord: g {d|t|c|s|n|k} if (_pendingKey === 'g' && key !== 'g') { clearTimeout(_pendingTimer); + if (key === 'k') { + // k ist Präfix für 3-Chord — auf dritten Tastendruck warten + _pendingKey = 'g k'; + _pendingTimer = setTimeout(() => { + _pendingKey = null; + const gk = SHORTCUTS.find((s) => s.key === 'g k'); + if (gk) gk.action(); + }, 1000); + return; + } _pendingKey = null; const combo = `g ${key}`; const shortcut = SHORTCUTS.find((s) => s.key === combo); @@ -739,6 +794,14 @@ function initMoreSheet(container) { backdrop.addEventListener('click', closeSheet); + let _touchStartY = 0; + sheet.addEventListener('touchstart', (e) => { + _touchStartY = e.touches[0].clientY; + }, { passive: true }); + sheet.addEventListener('touchend', (e) => { + if (e.changedTouches[0].clientY - _touchStartY > 60) closeSheet(); + }, { passive: true }); + sheet.querySelectorAll('[data-route]').forEach((el) => { el.addEventListener('click', () => closeSheet()); }); @@ -873,17 +936,19 @@ function renderSearchResults(container, data, onClose) { function navItems() { return [ { path: '/', label: t('nav.dashboard'), icon: 'layout-dashboard' }, - { path: '/tasks', label: t('nav.tasks'), icon: 'check-square' }, { path: '/calendar', label: t('nav.calendar'), icon: 'calendar' }, - { path: '/shopping', label: t('nav.shopping'), icon: 'shopping-cart' }, - { path: '/meals', label: t('nav.meals'), icon: 'utensils' }, - { path: '/recipes', label: t('nav.recipes'), icon: 'book-text' }, + // More-Sheet Items: + { path: '/tasks', label: t('nav.tasks'), icon: 'check-square' }, { path: '/birthdays', label: t('nav.birthdays'), icon: 'cake' }, { path: '/notes', label: t('nav.notes'), icon: 'sticky-note' }, { path: '/contacts', label: t('nav.contacts'), icon: 'book-user' }, { path: '/budget', label: t('nav.budget'), icon: 'wallet' }, { path: '/documents', label: t('nav.documents'), icon: 'folder-lock' }, { path: '/settings', label: t('nav.settings'), icon: 'settings' }, + // Sidebar only (Küche-Gruppe): + { path: '/meals', label: t('nav.meals'), icon: 'utensils', sidebarOnly: true }, + { path: '/recipes', label: t('nav.recipes'), icon: 'book-text', sidebarOnly: true }, + { path: '/shopping', label: t('nav.shopping'), icon: 'shopping-cart', sidebarOnly: true }, ]; } @@ -936,9 +1001,16 @@ function updateNav(path) { } }); + const kitchenNavBtn = document.querySelector('#kitchen-btn'); + if (kitchenNavBtn) { + const isKitchen = isKitchenRoute(path); + kitchenNavBtn.classList.toggle('nav-item--active', isKitchen); + kitchenNavBtn.toggleAttribute('aria-current', isKitchen); + } + const moreBtn = document.querySelector('#more-btn'); if (moreBtn) { - const secondaryItems = navItems().slice(PRIMARY_NAV); + const secondaryItems = navItems().filter((i) => !i.sidebarOnly).slice(PRIMARY_NAV); const activeSecondary = secondaryItems.find((n) => n.path === path); const inMoreSheet = !!activeSecondary; @@ -1120,16 +1192,18 @@ window.addEventListener('locale-changed', () => { navSidebarItems.replaceChildren(...navItems().map(navItemEl)); } if (bottomItems) { - const moreBtn = bottomItems.querySelector('#more-btn'); + const kitchenBtnEl = bottomItems.querySelector('#kitchen-btn'); + const searchBtnEl = bottomItems.querySelector('#search-btn'); + const moreBtn = bottomItems.querySelector('#more-btn'); + if (kitchenBtnEl) kitchenBtnEl.querySelector('.nav-item__label').textContent = t('nav.kitchen'); + if (searchBtnEl) searchBtnEl.querySelector('.nav-item__label').textContent = t('nav.search'); const newItems = navItems().slice(0, PRIMARY_NAV).map(navItemEl); - bottomItems.replaceChildren(...newItems, moreBtn); + bottomItems.replaceChildren(...newItems, kitchenBtnEl, searchBtnEl, moreBtn); } if (moreSheet) { - const searchTrig = moreSheet.querySelector('#search-btn'); - const searchTrigPlaceholder = searchTrig?.querySelector('.more-sheet__search-trigger-placeholder'); - if (searchTrigPlaceholder) searchTrigPlaceholder.textContent = t('search.placeholder'); - const newMoreItems = navItems().slice(PRIMARY_NAV).map(moreItemEl); - moreSheet.replaceChildren(searchTrig, ...newMoreItems); + const handle = moreSheet.querySelector('.more-sheet__handle'); + const newMoreItems = navItems().filter((i) => !i.sidebarOnly).slice(PRIMARY_NAV).map(moreItemEl); + moreSheet.replaceChildren(handle, ...newMoreItems); } document.querySelectorAll('[data-route]').forEach((el) => {