Merge branch 'ulsklyc:main' into main
This commit is contained in:
@@ -7,6 +7,33 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.34.1] - 2026-04-29
|
||||
|
||||
### Fixed
|
||||
- Kitchen tabs bar disappeared after navigating to Shopping, because the page overwrote the container a second time after loading data
|
||||
|
||||
## [0.34.0] - 2026-04-29
|
||||
|
||||
### Added
|
||||
- Navigation: new "Küche" (Kitchen) button in the bottom bar groups Meals, Recipes and Shopping behind a single entry point with a persistent tab bar inside each sub-module
|
||||
- Navigation: new "Suche" (Search) button added to the bottom bar for one-tap access to the search overlay
|
||||
- Kitchen tabs bar: sticky segment-control (Meals / Recipes / Shopping) injected at the top of each sub-module page; remembers the last active tab via sessionStorage
|
||||
- Keyboard shortcuts: `g k` navigates to Kitchen (last tab), `g k m` → Meals, `g k r` → Recipes, `g k s` → Shopping
|
||||
- i18n: `nav.kitchen`, `nav.search` and `shortcuts.goKitchen` keys added to all 15 locale files
|
||||
|
||||
### Changed
|
||||
- Navigation: bottom bar reorganised — Dashboard, Calendar, Küche, Suche, Mehr (5 items)
|
||||
- Navigation: Meals, Recipes and Shopping removed from the More sheet; they are accessible via the Kitchen tab bar and the sidebar on desktop
|
||||
- More sheet: reduced from 3-column to 2-column grid for larger touch targets; search trigger removed
|
||||
- More sheet: drag-handle added at the top; swipe-down gesture closes the sheet
|
||||
|
||||
## [0.33.1] - 2026-04-29
|
||||
|
||||
### Changed
|
||||
- Navigation: removed the dedicated Search button from the bottom bar; the bottom bar now shows three primary module links plus the More button
|
||||
- Navigation: the More sheet now opens with a full-width pill-shaped search trigger at the top, replacing the grid-cell search item
|
||||
- Search: the search overlay input field is now positioned at the bottom of the screen (thumb zone) instead of the top
|
||||
|
||||
## [0.33.0] - 2026-04-29
|
||||
|
||||
### Added
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "oikos",
|
||||
"version": "0.33.0",
|
||||
"version": "0.34.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "oikos",
|
||||
"version": "0.33.0",
|
||||
"version": "0.34.1",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bcrypt": "^6.0.0",
|
||||
|
||||
+3
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "oikos",
|
||||
"version": "0.33.0",
|
||||
"version": "0.34.1",
|
||||
"description": "Self-hosted family planner - calendar, tasks, shopping, meal planning, budget and more. Private, open-source, no subscription.",
|
||||
"main": "server/index.js",
|
||||
"type": "module",
|
||||
@@ -20,12 +20,13 @@
|
||||
"test:ncb": "node --experimental-sqlite test-notes-contacts-budget.js",
|
||||
"test:ux-utils": "node test-ux-utils.js",
|
||||
"test:modal-utils": "node --loader ./test-browser-loader.mjs test-modal-utils.js",
|
||||
"test:kitchen-tabs": "node --loader ./test-browser-loader.mjs test-kitchen-tabs.js",
|
||||
"test:reminders": "node --experimental-sqlite test-reminders.js",
|
||||
"test:api": "node test-api.js",
|
||||
"test:setup": "node test-setup.js",
|
||||
"test:ics-parser": "node test-ics-parser.js",
|
||||
"test:ics-sub": "node --experimental-sqlite test-ics-subscription.js",
|
||||
"test": "node --experimental-sqlite test-db.js && node --experimental-sqlite test-dashboard.js && node --experimental-sqlite test-tasks.js && node --experimental-sqlite test-shopping.js && node --experimental-sqlite test-meals.js && node --experimental-sqlite test-calendar.js && node --experimental-sqlite test-notes-contacts-budget.js && npm run test:ux-utils && npm run test:modal-utils && npm run test:reminders && npm run test:api && npm run test:ics-parser && npm run test:ics-sub && npm run test:setup"
|
||||
"test": "node --experimental-sqlite test-db.js && node --experimental-sqlite test-dashboard.js && node --experimental-sqlite test-tasks.js && node --experimental-sqlite test-shopping.js && node --experimental-sqlite test-meals.js && node --experimental-sqlite test-calendar.js && node --experimental-sqlite test-notes-contacts-budget.js && npm run test:ux-utils && npm run test:modal-utils && npm run test:reminders && npm run test:api && npm run test:ics-parser && npm run test:ics-sub && npm run test:setup && npm run test:kitchen-tabs"
|
||||
},
|
||||
"dependencies": {
|
||||
"bcrypt": "^6.0.0",
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
<link rel="stylesheet" href="/styles/pwa.css" />
|
||||
<link rel="stylesheet" href="/styles/layout.css" />
|
||||
<link rel="stylesheet" href="/styles/glass.css" />
|
||||
<link rel="stylesheet" href="/styles/kitchen-tabs.css" />
|
||||
<link rel="stylesheet" href="/styles/login.css" />
|
||||
<!-- Theme: explizite Nutzer-Overrides vor CSS-Rendering anwenden (Flash-Prevention).
|
||||
System-Präferenz wird durch @media (prefers-color-scheme: dark) in tokens.css
|
||||
|
||||
@@ -47,7 +47,9 @@
|
||||
"quickActions": "الإجراءات السريعة",
|
||||
"recipes": "الوصفات",
|
||||
"more": "المزيد",
|
||||
"documents": "المستندات"
|
||||
"documents": "المستندات",
|
||||
"kitchen": "المطبخ",
|
||||
"search": "بحث"
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "لوحة التحكم",
|
||||
@@ -985,5 +987,8 @@
|
||||
"dropzoneTitle": "أفلت الملف هنا أو انقر للاختيار",
|
||||
"dropzoneHint": "اسحب ملفًا إلى هذه المنطقة أو استخدم محدد الملفات.",
|
||||
"selectedFileLabel": "المحدد: {{name}}"
|
||||
},
|
||||
"shortcuts": {
|
||||
"goKitchen": "المطبخ"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,7 +47,9 @@
|
||||
"quickActions": "Schnellaktionen",
|
||||
"more": "Mehr",
|
||||
"recipes": "Rezepte",
|
||||
"documents": "Dokumente"
|
||||
"documents": "Dokumente",
|
||||
"kitchen": "Küche",
|
||||
"search": "Suche"
|
||||
},
|
||||
"search": {
|
||||
"title": "Suche",
|
||||
@@ -961,7 +963,8 @@
|
||||
"goTasks": "Aufgaben",
|
||||
"goCal": "Kalender",
|
||||
"goShop": "Einkaufsliste",
|
||||
"goNotes": "Notizen"
|
||||
"goNotes": "Notizen",
|
||||
"goKitchen": "Küche"
|
||||
},
|
||||
"documents": {
|
||||
"title": "Dokumente",
|
||||
|
||||
@@ -47,7 +47,9 @@
|
||||
"quickActions": "Γρήγορες ενέργειες",
|
||||
"recipes": "Συνταγές",
|
||||
"more": "Περισσότερα",
|
||||
"documents": "Έγγραφα"
|
||||
"documents": "Έγγραφα",
|
||||
"kitchen": "Κουζίνα",
|
||||
"search": "Αναζήτηση"
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Επισκόπηση",
|
||||
@@ -985,5 +987,8 @@
|
||||
"dropzoneTitle": "Αφήστε το αρχείο εδώ ή κάντε κλικ για επιλογή",
|
||||
"dropzoneHint": "Σύρετε ένα αρχείο σε αυτήν την περιοχή ή χρησιμοποιήστε τον επιλογέα αρχείων.",
|
||||
"selectedFileLabel": "Επιλέχθηκε: {{name}}"
|
||||
},
|
||||
"shortcuts": {
|
||||
"goKitchen": "Κουζίνα"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,7 +47,9 @@
|
||||
"quickActions": "Quick actions",
|
||||
"recipes": "Recipes",
|
||||
"more": "More",
|
||||
"documents": "Documents"
|
||||
"documents": "Documents",
|
||||
"kitchen": "Kitchen",
|
||||
"search": "Search"
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Overview",
|
||||
@@ -931,7 +933,8 @@
|
||||
"goTasks": "Tasks",
|
||||
"goCal": "Calendar",
|
||||
"goShop": "Shopping list",
|
||||
"goNotes": "Notes"
|
||||
"goNotes": "Notes",
|
||||
"goKitchen": "Kitchen"
|
||||
},
|
||||
"emptyHint": {
|
||||
"tasks": "Tap + to create your first task. Swipe a card left to delete.",
|
||||
|
||||
@@ -47,7 +47,9 @@
|
||||
"quickActions": "Acciones rápidas",
|
||||
"recipes": "Recetas",
|
||||
"more": "Más",
|
||||
"documents": "Documentos"
|
||||
"documents": "Documentos",
|
||||
"kitchen": "Cocina",
|
||||
"search": "Buscar"
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Inicio",
|
||||
@@ -985,5 +987,8 @@
|
||||
"dropzoneTitle": "Suelta el archivo aquí o haz clic para elegir",
|
||||
"dropzoneHint": "Arrastra un archivo a esta área o usa el selector de archivos.",
|
||||
"selectedFileLabel": "Seleccionado: {{name}}"
|
||||
},
|
||||
"shortcuts": {
|
||||
"goKitchen": "Cocina"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,7 +47,9 @@
|
||||
"quickActions": "Actions rapides",
|
||||
"recipes": "Recettes",
|
||||
"more": "Plus",
|
||||
"documents": "Documents"
|
||||
"documents": "Documents",
|
||||
"kitchen": "Cuisine",
|
||||
"search": "Recherche"
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Accueil",
|
||||
@@ -985,5 +987,8 @@
|
||||
"dropzoneTitle": "Déposez le fichier ici ou cliquez pour choisir",
|
||||
"dropzoneHint": "Glissez un fichier dans cette zone ou utilisez le sélecteur.",
|
||||
"selectedFileLabel": "Sélectionné : {{name}}"
|
||||
},
|
||||
"shortcuts": {
|
||||
"goKitchen": "Cuisine"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,7 +47,9 @@
|
||||
"quickActions": "त्वरित क्रियाएं",
|
||||
"recipes": "रेसिपी",
|
||||
"more": "और",
|
||||
"documents": "दस्तावेज़"
|
||||
"documents": "दस्तावेज़",
|
||||
"kitchen": "रसोई",
|
||||
"search": "खोज"
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "डैशबोर्ड",
|
||||
@@ -985,5 +987,8 @@
|
||||
"dropzoneTitle": "फ़ाइल यहाँ छोड़ें या चुनने के लिए क्लिक करें",
|
||||
"dropzoneHint": "फ़ाइल को इस क्षेत्र में खींचें या फ़ाइल पिकर का उपयोग करें।",
|
||||
"selectedFileLabel": "चयनित: {{name}}"
|
||||
},
|
||||
"shortcuts": {
|
||||
"goKitchen": "रसोई"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,7 +47,9 @@
|
||||
"quickActions": "Azioni rapide",
|
||||
"recipes": "Ricette",
|
||||
"more": "Altro",
|
||||
"documents": "Documenti"
|
||||
"documents": "Documenti",
|
||||
"kitchen": "Cucina",
|
||||
"search": "Cerca"
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Panoramica",
|
||||
@@ -985,5 +987,8 @@
|
||||
"dropzoneTitle": "Rilascia il file qui o fai clic per scegliere",
|
||||
"dropzoneHint": "Trascina un file in quest’area oppure usa il selettore.",
|
||||
"selectedFileLabel": "Selezionato: {{name}}"
|
||||
},
|
||||
"shortcuts": {
|
||||
"goKitchen": "Cucina"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,7 +47,9 @@
|
||||
"quickActions": "クイックアクション",
|
||||
"recipes": "レシピ",
|
||||
"more": "もっと見る",
|
||||
"documents": "書類"
|
||||
"documents": "書類",
|
||||
"kitchen": "キッチン",
|
||||
"search": "検索"
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "ダッシュボード",
|
||||
@@ -985,5 +987,8 @@
|
||||
"dropzoneTitle": "ここにファイルをドロップ、またはクリックして選択",
|
||||
"dropzoneHint": "この領域にファイルをドラッグするか、ファイル選択を使用します。",
|
||||
"selectedFileLabel": "選択済み: {{name}}"
|
||||
},
|
||||
"shortcuts": {
|
||||
"goKitchen": "キッチン"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,7 +47,9 @@
|
||||
"quickActions": "Ações rápidas",
|
||||
"recipes": "Receitas",
|
||||
"more": "Mais",
|
||||
"documents": "Documentos"
|
||||
"documents": "Documentos",
|
||||
"kitchen": "Cozinha",
|
||||
"search": "Pesquisar"
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Painel",
|
||||
@@ -986,5 +988,8 @@
|
||||
"dropzoneTitle": "Solte o arquivo aqui ou clique para escolher",
|
||||
"dropzoneHint": "Arraste um arquivo para esta area, ou use o seletor de arquivos.",
|
||||
"selectedFileLabel": "Selecionado: {{name}}"
|
||||
},
|
||||
"shortcuts": {
|
||||
"goKitchen": "Cozinha"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,7 +47,9 @@
|
||||
"quickActions": "Быстрые действия",
|
||||
"recipes": "Рецепты",
|
||||
"more": "Ещё",
|
||||
"documents": "Документы"
|
||||
"documents": "Документы",
|
||||
"kitchen": "Кухня",
|
||||
"search": "Поиск"
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Обзор",
|
||||
@@ -985,5 +987,8 @@
|
||||
"dropzoneTitle": "Перетащите файл сюда или нажмите для выбора",
|
||||
"dropzoneHint": "Перетащите файл в эту область или используйте выбор файла.",
|
||||
"selectedFileLabel": "Выбрано: {{name}}"
|
||||
},
|
||||
"shortcuts": {
|
||||
"goKitchen": "Кухня"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,7 +47,9 @@
|
||||
"quickActions": "Snabba åtgärder",
|
||||
"recipes": "Recept",
|
||||
"more": "Mer",
|
||||
"documents": "Dokument"
|
||||
"documents": "Dokument",
|
||||
"kitchen": "Kök",
|
||||
"search": "Sök"
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Översikt",
|
||||
@@ -985,5 +987,8 @@
|
||||
"dropzoneTitle": "Släpp filen här eller klicka för att välja",
|
||||
"dropzoneHint": "Dra en fil till området eller använd filväljaren.",
|
||||
"selectedFileLabel": "Vald: {{name}}"
|
||||
},
|
||||
"shortcuts": {
|
||||
"goKitchen": "Kök"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,7 +47,9 @@
|
||||
"quickActions": "Hızlı işlemler",
|
||||
"recipes": "Tarifler",
|
||||
"more": "Daha Fazla",
|
||||
"documents": "Belgeler"
|
||||
"documents": "Belgeler",
|
||||
"kitchen": "Mutfak",
|
||||
"search": "Ara"
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Genel Bakış",
|
||||
@@ -985,5 +987,8 @@
|
||||
"dropzoneTitle": "Dosyayı buraya bırakın veya seçmek için tıklayın",
|
||||
"dropzoneHint": "Bir dosyayı bu alana sürükleyin veya dosya seçiciyi kullanın.",
|
||||
"selectedFileLabel": "Seçildi: {{name}}"
|
||||
},
|
||||
"shortcuts": {
|
||||
"goKitchen": "Mutfak"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,7 +47,9 @@
|
||||
"quickActions": "Швидкі дії",
|
||||
"recipes": "Рецепти",
|
||||
"more": "Більше",
|
||||
"documents": "Документи"
|
||||
"documents": "Документи",
|
||||
"kitchen": "Кухня",
|
||||
"search": "Пошук"
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Огляд",
|
||||
@@ -993,5 +995,8 @@
|
||||
"dropzoneTitle": "Перетягніть файл сюди або натисніть для вибору",
|
||||
"dropzoneHint": "Перетягніть файл у цю область або скористайтеся вибором файлу.",
|
||||
"selectedFileLabel": "Вибрано: {{name}}"
|
||||
},
|
||||
"shortcuts": {
|
||||
"goKitchen": "Кухня"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,7 +47,9 @@
|
||||
"quickActions": "快捷操作",
|
||||
"recipes": "食谱",
|
||||
"more": "更多",
|
||||
"documents": "文档"
|
||||
"documents": "文档",
|
||||
"kitchen": "厨房",
|
||||
"search": "搜索"
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "概览",
|
||||
@@ -985,5 +987,8 @@
|
||||
"dropzoneTitle": "将文件拖到此处或点击选择",
|
||||
"dropzoneHint": "将文件拖入此区域,或使用文件选择器。",
|
||||
"selectedFileLabel": "已选择:{{name}}"
|
||||
},
|
||||
"shortcuts": {
|
||||
"goKitchen": "厨房"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import { stagger } from '/utils/ux.js';
|
||||
import { t, formatDate, dateInputPlaceholder, formatDateInput, parseDateInput, isDateInputValid } from '/i18n.js';
|
||||
import { esc } from '/utils/html.js';
|
||||
import { DEFAULT_CATEGORY_NAME, categoryLabel } from '/utils/shopping-categories.js';
|
||||
import { renderKitchenTabsBar } from '/utils/kitchen-tabs.js';
|
||||
|
||||
// --------------------------------------------------------
|
||||
// Konstanten
|
||||
@@ -163,6 +164,7 @@ export async function render(container, { user }) {
|
||||
`;
|
||||
|
||||
if (window.lucide) lucide.createIcons();
|
||||
renderKitchenTabsBar(container, '/meals');
|
||||
|
||||
const today = new Date().toISOString().slice(0, 10);
|
||||
const monday = getMondayOf(today);
|
||||
|
||||
@@ -7,6 +7,7 @@ import { api } from '/api.js';
|
||||
import { t } from '/i18n.js';
|
||||
import { openModal as openSharedModal, closeModal as closeSharedModal } from '/components/modal.js';
|
||||
import { DEFAULT_CATEGORY_NAME, categoryLabel } from '/utils/shopping-categories.js';
|
||||
import { renderKitchenTabsBar } from '/utils/kitchen-tabs.js';
|
||||
|
||||
let _container = null;
|
||||
|
||||
@@ -70,6 +71,7 @@ export async function render(container) {
|
||||
|
||||
page.append(header, list, fab);
|
||||
container.replaceChildren(page);
|
||||
renderKitchenTabsBar(container, '/recipes');
|
||||
|
||||
if (window.lucide) window.lucide.createIcons();
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import { t } from '/i18n.js';
|
||||
import { esc } from '/utils/html.js';
|
||||
import { promptModal } from '/components/modal.js';
|
||||
import { DEFAULT_CATEGORY_NAME, categoryLabel } from '/utils/shopping-categories.js';
|
||||
import { renderKitchenTabsBar } from '/utils/kitchen-tabs.js';
|
||||
|
||||
// --------------------------------------------------------
|
||||
// Konstanten
|
||||
@@ -839,7 +840,6 @@ export async function render(container, { user }) {
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
try {
|
||||
await Promise.all([loadCategories(), loadLists()]);
|
||||
if (state.lists.length) {
|
||||
@@ -862,6 +862,7 @@ export async function render(container, { user }) {
|
||||
</div>
|
||||
`;
|
||||
|
||||
renderKitchenTabsBar(container, '/shopping');
|
||||
renderTabs(container);
|
||||
wireTabBar(container);
|
||||
renderListContent(container);
|
||||
|
||||
+96
-57
@@ -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,20 +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-nav-btn';
|
||||
searchNavBtn.setAttribute('aria-label', t('search.title'));
|
||||
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('search.title');
|
||||
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';
|
||||
@@ -520,20 +543,11 @@ function renderAppShell(container) {
|
||||
moreSheet.setAttribute('role', 'dialog');
|
||||
moreSheet.setAttribute('aria-label', t('nav.more'));
|
||||
moreSheet.setAttribute('aria-hidden', 'true');
|
||||
const searchBtn = document.createElement('button');
|
||||
searchBtn.className = 'more-item';
|
||||
searchBtn.id = 'search-btn';
|
||||
const searchIcon = document.createElement('i');
|
||||
searchIcon.dataset.lucide = 'search';
|
||||
searchIcon.className = 'more-item__icon';
|
||||
searchIcon.setAttribute('aria-hidden', 'true');
|
||||
const searchLabel = document.createElement('span');
|
||||
searchLabel.className = 'more-item__label';
|
||||
searchLabel.textContent = t('search.title');
|
||||
searchBtn.appendChild(searchIcon);
|
||||
searchBtn.appendChild(searchLabel);
|
||||
moreSheet.appendChild(searchBtn);
|
||||
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';
|
||||
@@ -601,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;
|
||||
@@ -616,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);
|
||||
@@ -752,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());
|
||||
});
|
||||
@@ -763,9 +813,8 @@ function initMoreSheet(container) {
|
||||
* Initialisiert die Suchfunktion (Overlay + API-Calls).
|
||||
*/
|
||||
function initSearch(container) {
|
||||
const searchBtn = container.querySelector('#search-btn');
|
||||
const searchNavBtn = container.querySelector('#search-nav-btn');
|
||||
const searchClose = container.querySelector('#search-close');
|
||||
const searchBtn = container.querySelector('#search-btn');
|
||||
const searchClose = container.querySelector('#search-close');
|
||||
const overlay = container.querySelector('#search-overlay');
|
||||
const input = container.querySelector('#search-input');
|
||||
const results = container.querySelector('#search-results');
|
||||
@@ -812,7 +861,6 @@ function initSearch(container) {
|
||||
}
|
||||
|
||||
searchBtn.addEventListener('click', openSearch);
|
||||
if (searchNavBtn) searchNavBtn.addEventListener('click', openSearch);
|
||||
searchClose.addEventListener('click', closeSearch);
|
||||
|
||||
document.addEventListener('keydown', (e) => {
|
||||
@@ -888,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 },
|
||||
];
|
||||
}
|
||||
|
||||
@@ -951,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;
|
||||
|
||||
@@ -1135,36 +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);
|
||||
// Such-Button neu erstellen (wird durch replaceChildren entfernt)
|
||||
const newSearchBtn = document.createElement('button');
|
||||
newSearchBtn.className = 'nav-item nav-item--search';
|
||||
newSearchBtn.id = 'search-nav-btn';
|
||||
newSearchBtn.setAttribute('aria-label', t('search.title'));
|
||||
const newSearchIcon = document.createElement('i');
|
||||
newSearchIcon.dataset.lucide = 'search';
|
||||
newSearchIcon.className = 'nav-item__icon';
|
||||
newSearchIcon.setAttribute('aria-hidden', 'true');
|
||||
const newSearchLbl = document.createElement('span');
|
||||
newSearchLbl.className = 'nav-item__label';
|
||||
newSearchLbl.textContent = t('search.title');
|
||||
newSearchBtn.appendChild(newSearchIcon);
|
||||
newSearchBtn.appendChild(newSearchLbl);
|
||||
bottomItems.replaceChildren(...newItems, newSearchBtn, moreBtn);
|
||||
// Event-Listener auf neuen Such-Button
|
||||
if (newSearchBtn) {
|
||||
newSearchBtn.addEventListener('click', () => {
|
||||
if (window._openSearch) window._openSearch();
|
||||
});
|
||||
}
|
||||
bottomItems.replaceChildren(...newItems, kitchenBtnEl, searchBtnEl, moreBtn);
|
||||
}
|
||||
if (moreSheet) {
|
||||
const searchBtn = moreSheet.querySelector('#search-btn');
|
||||
const searchLbl = searchBtn?.querySelector('.more-item__label');
|
||||
if (searchLbl) searchLbl.textContent = t('search.title');
|
||||
const newMoreItems = navItems().slice(PRIMARY_NAV).map(moreItemEl);
|
||||
moreSheet.replaceChildren(searchBtn, ...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) => {
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
/* Modul: Kitchen Tabs Bar
|
||||
* Sticky Segment-Control für Mahlzeiten/Rezepte/Einkauf + Sub-Modul Layout-Fixes
|
||||
*/
|
||||
|
||||
.kitchen-tabs-bar {
|
||||
display: flex;
|
||||
gap: var(--space-1);
|
||||
padding: var(--space-2) var(--space-4);
|
||||
height: var(--kitchen-tabs-height);
|
||||
box-sizing: border-box;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: var(--z-sticky);
|
||||
background-color: var(--color-bg);
|
||||
border-bottom: 1px solid var(--color-border-subtle);
|
||||
overflow-x: auto;
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.kitchen-tabs-bar::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.kitchen-tab {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--space-1);
|
||||
padding: 0 var(--space-3);
|
||||
height: 36px;
|
||||
border-radius: var(--radius-full);
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--color-text-secondary);
|
||||
font-family: inherit;
|
||||
font-size: var(--text-sm);
|
||||
font-weight: var(--font-weight-medium);
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
transition: background-color var(--transition-fast), color var(--transition-fast);
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
.kitchen-tab:active {
|
||||
transform: scale(0.96);
|
||||
transition-duration: 0.06s;
|
||||
}
|
||||
|
||||
.kitchen-tab--active {
|
||||
background-color: color-mix(in srgb, var(--active-module-accent, var(--color-accent)) 14%, transparent);
|
||||
color: var(--active-module-accent, var(--color-accent));
|
||||
}
|
||||
|
||||
.kitchen-tab__icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.kitchen-tab__label {
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
/* Mahlzeiten: sticky day-header unterhalb der Tab-Leiste */
|
||||
.has-kitchen-tabs .day-header {
|
||||
top: var(--kitchen-tabs-height);
|
||||
}
|
||||
|
||||
/* Einkauf: Viewport-Höhe um Tab-Leiste reduzieren (Mobile) */
|
||||
.has-kitchen-tabs .shopping-page {
|
||||
height: calc(
|
||||
100dvh
|
||||
- var(--nav-bottom-height)
|
||||
- var(--safe-area-inset-bottom)
|
||||
- var(--kitchen-tabs-height)
|
||||
);
|
||||
}
|
||||
|
||||
/* Einkauf: Viewport-Höhe (Desktop) */
|
||||
@media (min-width: 1024px) {
|
||||
.has-kitchen-tabs .shopping-page {
|
||||
height: calc(100dvh - var(--kitchen-tabs-height));
|
||||
}
|
||||
}
|
||||
@@ -172,7 +172,9 @@
|
||||
}
|
||||
|
||||
/* ── More-Button (Button-Basis, ansonsten wie nav-item) ── */
|
||||
.nav-item--more {
|
||||
.nav-item--more,
|
||||
.nav-item--kitchen,
|
||||
.nav-item--search {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
@@ -216,7 +218,7 @@
|
||||
padding: var(--space-4) var(--space-4) calc(var(--space-4) + var(--safe-area-inset-bottom));
|
||||
z-index: calc(var(--z-nav) + 2);
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: var(--space-3);
|
||||
transform: translateY(100%);
|
||||
transition: transform 0.25s var(--ease-out);
|
||||
@@ -226,6 +228,16 @@
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
/* ── More-Sheet Drag-Handle ── */
|
||||
.more-sheet__handle {
|
||||
grid-column: 1 / -1;
|
||||
width: 36px;
|
||||
height: 4px;
|
||||
border-radius: var(--radius-full);
|
||||
background-color: var(--color-border);
|
||||
margin: 0 auto var(--space-2);
|
||||
}
|
||||
|
||||
/* ── More-Item ── */
|
||||
.more-item {
|
||||
display: flex;
|
||||
@@ -275,7 +287,7 @@
|
||||
background-color: var(--color-surface);
|
||||
z-index: calc(var(--z-nav) + 3);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-direction: column-reverse;
|
||||
transform: translateY(100%);
|
||||
transition: transform 0.25s var(--ease-out);
|
||||
}
|
||||
@@ -288,8 +300,8 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-3);
|
||||
padding: calc(var(--space-4) + var(--safe-area-inset-top)) var(--space-4) var(--space-3);
|
||||
border-bottom: 1px solid var(--color-border-subtle);
|
||||
padding: var(--space-3) var(--space-4) calc(var(--space-4) + var(--safe-area-inset-bottom));
|
||||
border-top: 1px solid var(--color-border-subtle);
|
||||
}
|
||||
|
||||
.search-overlay__input {
|
||||
@@ -331,7 +343,8 @@
|
||||
.search-overlay__results {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: var(--space-4);
|
||||
padding: var(--space-4) var(--space-4) var(--space-2);
|
||||
padding-top: calc(var(--space-4) + var(--safe-area-inset-top));
|
||||
}
|
||||
|
||||
.search-overlay__empty {
|
||||
|
||||
@@ -351,6 +351,7 @@
|
||||
--content-max-width: 1280px;
|
||||
--content-max-width-narrow: 720px;
|
||||
--cal-hour-height: 56px;
|
||||
--kitchen-tabs-height: 56px;
|
||||
|
||||
/* --------------------------------------------------------
|
||||
* 13. Sidebar
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import { t } from '/i18n.js';
|
||||
|
||||
export const KITCHEN_ROUTES = ['/meals', '/recipes', '/shopping'];
|
||||
export const KITCHEN_STORAGE_KEY = 'oikos-kitchen-tab';
|
||||
|
||||
const TABS = () => [
|
||||
{ route: '/meals', labelKey: 'nav.meals', icon: 'utensils' },
|
||||
{ route: '/recipes', labelKey: 'nav.recipes', icon: 'book-text' },
|
||||
{ route: '/shopping', labelKey: 'nav.shopping', icon: 'shopping-cart' },
|
||||
];
|
||||
|
||||
export function getLastKitchenRoute() {
|
||||
try {
|
||||
const stored = sessionStorage.getItem(KITCHEN_STORAGE_KEY);
|
||||
return KITCHEN_ROUTES.includes(stored) ? stored : '/meals';
|
||||
} catch {
|
||||
return '/meals';
|
||||
}
|
||||
}
|
||||
|
||||
export function isKitchenRoute(path) {
|
||||
return KITCHEN_ROUTES.includes(path);
|
||||
}
|
||||
|
||||
export function renderKitchenTabsBar(container, activeRoute) {
|
||||
try {
|
||||
sessionStorage.setItem(KITCHEN_STORAGE_KEY, activeRoute);
|
||||
} catch { /* ignore */ }
|
||||
|
||||
container.classList.add('has-kitchen-tabs');
|
||||
|
||||
const bar = document.createElement('div');
|
||||
bar.className = 'kitchen-tabs-bar';
|
||||
bar.setAttribute('role', 'tablist');
|
||||
bar.setAttribute('aria-label', t('nav.kitchen'));
|
||||
|
||||
TABS().forEach(({ route, labelKey, icon }) => {
|
||||
const btn = document.createElement('button');
|
||||
btn.className = 'kitchen-tab' + (route === activeRoute ? ' kitchen-tab--active' : '');
|
||||
btn.dataset.route = route;
|
||||
btn.type = 'button';
|
||||
btn.setAttribute('role', 'tab');
|
||||
btn.setAttribute('aria-selected', route === activeRoute ? 'true' : 'false');
|
||||
|
||||
const i = document.createElement('i');
|
||||
i.dataset.lucide = icon;
|
||||
i.className = 'kitchen-tab__icon';
|
||||
i.setAttribute('aria-hidden', 'true');
|
||||
|
||||
const span = document.createElement('span');
|
||||
span.className = 'kitchen-tab__label';
|
||||
span.textContent = t(labelKey);
|
||||
|
||||
btn.appendChild(i);
|
||||
btn.appendChild(span);
|
||||
bar.appendChild(btn);
|
||||
});
|
||||
|
||||
bar.addEventListener('click', (e) => {
|
||||
const btn = e.target.closest('[data-route]');
|
||||
if (!btn || btn.dataset.route === activeRoute) return;
|
||||
window.oikos?.navigate(btn.dataset.route);
|
||||
});
|
||||
|
||||
container.insertAdjacentElement('afterbegin', bar);
|
||||
|
||||
if (window.lucide) window.lucide.createIcons({ el: bar });
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
/**
|
||||
* Tests: Kitchen-Tabs Utility (pure functions)
|
||||
* Läuft mit: node --loader ./test-browser-loader.mjs test-kitchen-tabs.js
|
||||
*/
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
|
||||
const { KITCHEN_ROUTES, KITCHEN_STORAGE_KEY, getLastKitchenRoute, isKitchenRoute } = await (async () => {
|
||||
global.window = { oikos: null };
|
||||
global.document = {
|
||||
createElement: () => ({
|
||||
className: '', dataset: {}, style: {},
|
||||
setAttribute() {}, appendChild() {},
|
||||
classList: { add() {}, toggle() {} },
|
||||
insertAdjacentElement() {},
|
||||
addEventListener() {},
|
||||
}),
|
||||
};
|
||||
const storage = {
|
||||
_d: {},
|
||||
getItem(k) { return this._d[k] ?? null; },
|
||||
setItem(k, v) { this._d[k] = v; },
|
||||
};
|
||||
global.sessionStorage = storage;
|
||||
global.t = (k) => k;
|
||||
return import('./public/utils/kitchen-tabs.js');
|
||||
})();
|
||||
|
||||
test('KITCHEN_ROUTES enthält alle drei Sub-Routen', () => {
|
||||
assert.deepEqual(KITCHEN_ROUTES, ['/meals', '/recipes', '/shopping']);
|
||||
});
|
||||
|
||||
test('KITCHEN_STORAGE_KEY ist korrekt', () => {
|
||||
assert.equal(KITCHEN_STORAGE_KEY, 'oikos-kitchen-tab');
|
||||
});
|
||||
|
||||
test('getLastKitchenRoute: Standardwert /meals wenn kein Storage-Eintrag', () => {
|
||||
global.sessionStorage._d = {};
|
||||
assert.equal(getLastKitchenRoute(), '/meals');
|
||||
});
|
||||
|
||||
test('getLastKitchenRoute: gibt gespeicherte Route zurück', () => {
|
||||
global.sessionStorage._d = { 'oikos-kitchen-tab': '/recipes' };
|
||||
assert.equal(getLastKitchenRoute(), '/recipes');
|
||||
});
|
||||
|
||||
test('getLastKitchenRoute: ignoriert ungültige gespeicherte Route', () => {
|
||||
global.sessionStorage._d = { 'oikos-kitchen-tab': '/admin' };
|
||||
assert.equal(getLastKitchenRoute(), '/meals');
|
||||
});
|
||||
|
||||
test('isKitchenRoute: erkennt Kitchen-Routen', () => {
|
||||
assert.equal(isKitchenRoute('/meals'), true);
|
||||
assert.equal(isKitchenRoute('/recipes'), true);
|
||||
assert.equal(isKitchenRoute('/shopping'), true);
|
||||
});
|
||||
|
||||
test('isKitchenRoute: lehnt Nicht-Kitchen-Routen ab', () => {
|
||||
assert.equal(isKitchenRoute('/tasks'), false);
|
||||
assert.equal(isKitchenRoute('/'), false);
|
||||
assert.equal(isKitchenRoute('/calendar'), false);
|
||||
assert.equal(isKitchenRoute(''), false);
|
||||
});
|
||||
Reference in New Issue
Block a user