feat: restructure bottom nav — kitchen/search buttons, navItems cleanup, g-k shortcuts

This commit is contained in:
Ulas Kalayci
2026-04-29 19:59:15 +02:00
parent 88778a95c9
commit 1ac2fbd2b5
+105 -31
View File
@@ -8,6 +8,7 @@ import { api, auth } from '/api.js';
import { initI18n, getLocale, t } from '/i18n.js'; import { initI18n, getLocale, t } from '/i18n.js';
import { esc } from '/utils/html.js'; import { esc } from '/utils/html.js';
import { init as initReminders, stop as stopReminders } from '/reminders.js'; import { init as initReminders, stop as stopReminders } from '/reminders.js';
import { isKitchenRoute, getLastKitchenRoute } from '/utils/kitchen-tabs.js';
// -------------------------------------------------------- // --------------------------------------------------------
// Routen-Definitionen // Routen-Definitionen
@@ -128,10 +129,10 @@ let _pendingLoginRedirect = false;
// Router // Router
// -------------------------------------------------------- // --------------------------------------------------------
const ROUTE_ORDER = ['/', '/tasks', '/calendar', '/birthdays', '/meals', '/recipes', '/shopping', const ROUTE_ORDER = ['/', '/calendar', '/tasks', '/meals', '/recipes', '/shopping',
'/notes', '/contacts', '/budget', '/documents', '/settings']; '/birthdays', '/notes', '/contacts', '/budget', '/documents', '/settings'];
const PRIMARY_NAV = 3; const PRIMARY_NAV = 2;
const DEFAULT_APP_NAME = 'Oikos'; const DEFAULT_APP_NAME = 'Oikos';
const APP_NAME_STORAGE_KEY = 'oikos-app-name'; const APP_NAME_STORAGE_KEY = 'oikos-app-name';
@@ -478,6 +479,42 @@ function renderAppShell(container) {
const bottomItems = document.createElement('div'); const bottomItems = document.createElement('div');
bottomItems.className = 'nav-bottom__items'; bottomItems.className = 'nav-bottom__items';
navItems().slice(0, PRIMARY_NAV).forEach((item) => bottomItems.appendChild(navItemEl(item))); 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'); const moreBtn = document.createElement('button');
moreBtn.className = 'nav-item nav-item--more'; moreBtn.className = 'nav-item nav-item--more';
moreBtn.id = 'more-btn'; moreBtn.id = 'more-btn';
@@ -506,21 +543,11 @@ function renderAppShell(container) {
moreSheet.setAttribute('role', 'dialog'); moreSheet.setAttribute('role', 'dialog');
moreSheet.setAttribute('aria-label', t('nav.more')); moreSheet.setAttribute('aria-label', t('nav.more'));
moreSheet.setAttribute('aria-hidden', 'true'); moreSheet.setAttribute('aria-hidden', 'true');
const searchTrigger = document.createElement('button'); const dragHandle = document.createElement('div');
searchTrigger.className = 'more-sheet__search-trigger'; dragHandle.className = 'more-sheet__handle';
searchTrigger.id = 'search-btn'; dragHandle.setAttribute('aria-hidden', 'true');
searchTrigger.setAttribute('aria-label', t('search.title')); moreSheet.insertAdjacentElement('afterbegin', dragHandle);
const searchTriggerIcon = document.createElement('i'); navItems().filter((i) => !i.sidebarOnly).slice(PRIMARY_NAV).forEach((item) => moreSheet.appendChild(moreItemEl(item)));
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 searchOverlay = document.createElement('div'); const searchOverlay = document.createElement('div');
searchOverlay.className = 'search-overlay'; searchOverlay.className = 'search-overlay';
@@ -588,7 +615,11 @@ const SHORTCUTS = [
{ key: 'g t', description: () => t('shortcuts.goTasks'), action: () => navigate('/tasks') }, { key: 'g t', description: () => t('shortcuts.goTasks'), action: () => navigate('/tasks') },
{ key: 'g c', description: () => t('shortcuts.goCal'), action: () => navigate('/calendar') }, { key: 'g c', description: () => t('shortcuts.goCal'), action: () => navigate('/calendar') },
{ key: 'g s', description: () => t('shortcuts.goShop'), action: () => navigate('/shopping') }, { 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; let _pendingKey = null;
@@ -603,8 +634,32 @@ function initKeyboardShortcuts() {
const key = e.key.toLowerCase(); 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') { if (_pendingKey === 'g' && key !== 'g') {
clearTimeout(_pendingTimer); 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; _pendingKey = null;
const combo = `g ${key}`; const combo = `g ${key}`;
const shortcut = SHORTCUTS.find((s) => s.key === combo); const shortcut = SHORTCUTS.find((s) => s.key === combo);
@@ -739,6 +794,14 @@ function initMoreSheet(container) {
backdrop.addEventListener('click', closeSheet); 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) => { sheet.querySelectorAll('[data-route]').forEach((el) => {
el.addEventListener('click', () => closeSheet()); el.addEventListener('click', () => closeSheet());
}); });
@@ -873,17 +936,19 @@ function renderSearchResults(container, data, onClose) {
function navItems() { function navItems() {
return [ return [
{ path: '/', label: t('nav.dashboard'), icon: 'layout-dashboard' }, { 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: '/calendar', label: t('nav.calendar'), icon: 'calendar' },
{ path: '/shopping', label: t('nav.shopping'), icon: 'shopping-cart' }, // More-Sheet Items:
{ path: '/meals', label: t('nav.meals'), icon: 'utensils' }, { path: '/tasks', label: t('nav.tasks'), icon: 'check-square' },
{ path: '/recipes', label: t('nav.recipes'), icon: 'book-text' },
{ path: '/birthdays', label: t('nav.birthdays'), icon: 'cake' }, { path: '/birthdays', label: t('nav.birthdays'), icon: 'cake' },
{ path: '/notes', label: t('nav.notes'), icon: 'sticky-note' }, { path: '/notes', label: t('nav.notes'), icon: 'sticky-note' },
{ path: '/contacts', label: t('nav.contacts'), icon: 'book-user' }, { path: '/contacts', label: t('nav.contacts'), icon: 'book-user' },
{ path: '/budget', label: t('nav.budget'), icon: 'wallet' }, { path: '/budget', label: t('nav.budget'), icon: 'wallet' },
{ path: '/documents', label: t('nav.documents'), icon: 'folder-lock' }, { path: '/documents', label: t('nav.documents'), icon: 'folder-lock' },
{ path: '/settings', label: t('nav.settings'), icon: 'settings' }, { 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'); const moreBtn = document.querySelector('#more-btn');
if (moreBtn) { 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 activeSecondary = secondaryItems.find((n) => n.path === path);
const inMoreSheet = !!activeSecondary; const inMoreSheet = !!activeSecondary;
@@ -1120,16 +1192,18 @@ window.addEventListener('locale-changed', () => {
navSidebarItems.replaceChildren(...navItems().map(navItemEl)); navSidebarItems.replaceChildren(...navItems().map(navItemEl));
} }
if (bottomItems) { 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); const newItems = navItems().slice(0, PRIMARY_NAV).map(navItemEl);
bottomItems.replaceChildren(...newItems, moreBtn); bottomItems.replaceChildren(...newItems, kitchenBtnEl, searchBtnEl, moreBtn);
} }
if (moreSheet) { if (moreSheet) {
const searchTrig = moreSheet.querySelector('#search-btn'); const handle = moreSheet.querySelector('.more-sheet__handle');
const searchTrigPlaceholder = searchTrig?.querySelector('.more-sheet__search-trigger-placeholder'); const newMoreItems = navItems().filter((i) => !i.sidebarOnly).slice(PRIMARY_NAV).map(moreItemEl);
if (searchTrigPlaceholder) searchTrigPlaceholder.textContent = t('search.placeholder'); moreSheet.replaceChildren(handle, ...newMoreItems);
const newMoreItems = navItems().slice(PRIMARY_NAV).map(moreItemEl);
moreSheet.replaceChildren(searchTrig, ...newMoreItems);
} }
document.querySelectorAll('[data-route]').forEach((el) => { document.querySelectorAll('[data-route]').forEach((el) => {