feat: restructure bottom nav — kitchen/search buttons, navItems cleanup, g-k shortcuts
This commit is contained in:
+105
-31
@@ -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) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user