chore: release v0.42.0
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -210,6 +210,17 @@
|
||||
"statusArchived": "Archiviert",
|
||||
"archiveButton": "Aufgabe archivieren",
|
||||
"archivedToast": "Aufgabe archiviert.",
|
||||
"bulkSelect": "Mehrfachauswahl",
|
||||
"selectTask": "Aufgabe auswählen",
|
||||
"bulkSelectedCount": "{{count}} ausgewählt",
|
||||
"bulkMarkDone": "Erledigt",
|
||||
"bulkMarkOpen": "Offen",
|
||||
"bulkArchive": "Archivieren",
|
||||
"bulkDelete": "Löschen",
|
||||
"bulkDeleteConfirm": "{{count}} Aufgaben unwiderruflich löschen?",
|
||||
"bulkStatusChanged": "Status geändert.",
|
||||
"bulkArchived": "Aufgaben archiviert.",
|
||||
"bulkDeleted": "Aufgaben gelöscht.",
|
||||
"kanbanArchived": "Archiviert",
|
||||
"reminderNeedsDueDate": "Lege ein Fälligkeitsdatum fest, um Aufgabenerinnerungen zu aktivieren."
|
||||
},
|
||||
@@ -755,6 +766,10 @@
|
||||
"tabsAriaLabel": "Einstellungsbereiche",
|
||||
"sectionDesign": "Design",
|
||||
"sectionAppName": "Anwendungsname",
|
||||
"sectionModules": "Module",
|
||||
"modulesTitle": "Aktive Module",
|
||||
"modulesHint": "Deaktivierte Module verschwinden aus der Navigation. Daten bleiben erhalten und können nach Reaktivierung wieder genutzt werden.",
|
||||
"modulesSaved": "Modul-Sichtbarkeit gespeichert.",
|
||||
"sectionShopping": "Einkauf",
|
||||
"shoppingCategoriesLabel": "Einkaufskategorien",
|
||||
"shoppingCategoriesHint": "Kategorien hinzufügen, umbenennen, löschen oder sortieren.",
|
||||
|
||||
@@ -184,6 +184,17 @@
|
||||
"createdToast": "Task created.",
|
||||
"deletedToast": "Task deleted.",
|
||||
"archivedToast": "Task archived.",
|
||||
"bulkSelect": "Bulk select",
|
||||
"selectTask": "Select task",
|
||||
"bulkSelectedCount": "{{count}} selected",
|
||||
"bulkMarkDone": "Mark done",
|
||||
"bulkMarkOpen": "Mark open",
|
||||
"bulkArchive": "Archive",
|
||||
"bulkDelete": "Delete",
|
||||
"bulkDeleteConfirm": "Delete {{count}} tasks permanently?",
|
||||
"bulkStatusChanged": "Status changed.",
|
||||
"bulkArchived": "Tasks archived.",
|
||||
"bulkDeleted": "Tasks deleted.",
|
||||
"loadError": "Task could not be loaded.",
|
||||
"subtaskPrompt": "Subtask:",
|
||||
"kanbanOpen": "Open",
|
||||
@@ -749,6 +760,10 @@
|
||||
"tabsAriaLabel": "Settings sections",
|
||||
"sectionDesign": "Appearance",
|
||||
"sectionAppName": "Application name",
|
||||
"sectionModules": "Modules",
|
||||
"modulesTitle": "Active modules",
|
||||
"modulesHint": "Disabled modules disappear from the navigation. Data is preserved and reappears once a module is re-enabled.",
|
||||
"modulesSaved": "Module visibility saved.",
|
||||
"sectionShopping": "Shopping",
|
||||
"shoppingCategoriesLabel": "Shopping Categories",
|
||||
"shoppingCategoriesHint": "Add, rename, delete or reorder categories.",
|
||||
|
||||
@@ -199,7 +199,7 @@ export async function render(container, { user }) {
|
||||
let users = [];
|
||||
let googleStatus = { configured: false, connected: false, lastSync: null };
|
||||
let appleStatus = { configured: false, lastSync: null };
|
||||
let prefs = { visible_meal_types: ['breakfast', 'lunch', 'dinner', 'snack'], currency: 'EUR', date_format: 'mdy', time_format: '24h', app_name: DEFAULT_APP_NAME };
|
||||
let prefs = { visible_meal_types: ['breakfast', 'lunch', 'dinner', 'snack'], currency: 'EUR', date_format: 'mdy', time_format: '24h', app_name: DEFAULT_APP_NAME, disabled_modules: [] };
|
||||
let categories = [];
|
||||
let icsSubscriptions = [];
|
||||
let apiTokens = [];
|
||||
@@ -359,6 +359,35 @@ export async function render(container, { user }) {
|
||||
<oikos-locale-picker></oikos-locale-picker>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
${user?.role === 'admin' ? `
|
||||
<section class="settings-section">
|
||||
<h2 class="settings-section__title">${t('settings.sectionModules')}</h2>
|
||||
<div class="settings-card">
|
||||
<h3 class="settings-card__title">${t('settings.modulesTitle')}</h3>
|
||||
<p class="form-hint" style="margin-bottom:var(--space-3)">${t('settings.modulesHint')}</p>
|
||||
<div class="meal-type-toggles" id="module-toggles">
|
||||
${[
|
||||
['tasks', 'nav.tasks'],
|
||||
['calendar', 'nav.calendar'],
|
||||
['meals', 'nav.meals'],
|
||||
['recipes', 'nav.recipes'],
|
||||
['shopping', 'nav.shopping'],
|
||||
['birthdays', 'nav.birthdays'],
|
||||
['notes', 'nav.notes'],
|
||||
['contacts', 'nav.contacts'],
|
||||
['budget', 'nav.budget'],
|
||||
['documents', 'nav.documents'],
|
||||
].map(([slug, labelKey]) => `
|
||||
<label class="toggle-row">
|
||||
<input type="checkbox" value="${slug}" ${prefs.disabled_modules?.includes(slug) ? '' : 'checked'}>
|
||||
<span>${t(labelKey)}</span>
|
||||
</label>
|
||||
`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
` : ''}
|
||||
</div>
|
||||
|
||||
<!-- Panel: Mahlzeiten -->
|
||||
@@ -823,6 +852,22 @@ function bindEvents(container, user, users, categories, icsSubscriptions, apiTok
|
||||
});
|
||||
}
|
||||
|
||||
// Modul-Toggles (admin-only)
|
||||
const moduleToggles = container.querySelector('#module-toggles');
|
||||
if (moduleToggles) {
|
||||
moduleToggles.addEventListener('change', async () => {
|
||||
const disabled = [...moduleToggles.querySelectorAll('input:not(:checked)')].map((cb) => cb.value);
|
||||
try {
|
||||
const res = await api.put('/preferences', { disabled_modules: disabled });
|
||||
const saved = res?.data?.disabled_modules ?? disabled;
|
||||
window.oikos?.setDisabledModules?.(saved);
|
||||
window.oikos?.showToast(t('settings.modulesSaved'), 'success');
|
||||
} catch (err) {
|
||||
window.oikos?.showToast(err.message ?? t('common.errorGeneric'), 'danger');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Meal-Type-Toggles
|
||||
const mealToggles = container.querySelector('#meal-type-toggles');
|
||||
if (mealToggles) {
|
||||
|
||||
+135
-2
@@ -159,7 +159,7 @@ function renderSwipeRow(task, innerHtml) {
|
||||
}
|
||||
|
||||
function renderTaskCard(task, opts = {}) {
|
||||
const { expandedSubtasks = false } = opts;
|
||||
const { expandedSubtasks = false, showCheckbox = false, isChecked = false } = opts;
|
||||
const isDone = task.status === 'done';
|
||||
const progress = task.subtask_total > 0
|
||||
? Math.round((task.subtask_done / task.subtask_total) * 100)
|
||||
@@ -181,6 +181,10 @@ function renderTaskCard(task, opts = {}) {
|
||||
return `
|
||||
<div class="task-card ${isDone ? 'task-card--done' : ''}" data-task-id="${task.id}">
|
||||
<div class="task-card__main">
|
||||
${showCheckbox ? `
|
||||
<input type="checkbox" class="task-bulk-checkbox" data-task-id="${task.id}"
|
||||
${isChecked ? 'checked' : ''} aria-label="${t('tasks.selectTask')}">
|
||||
` : ''}
|
||||
<button class="task-status-btn task-status-btn--${task.status}"
|
||||
data-action="toggle-status" data-id="${task.id}" data-status="${task.status}"
|
||||
aria-label="${isDone ? t('tasks.markOpen', { title: esc(task.title) }) : t('tasks.markDone', { title: esc(task.title) })}">
|
||||
@@ -286,7 +290,10 @@ function renderTaskGroups(tasks, groupMode) {
|
||||
<span class="task-group__title">${catLabelsMap[name] ?? name}</span>
|
||||
<span class="task-group__count">${groupTasks.length}</span>
|
||||
</div>
|
||||
${sorted.map((t) => renderSwipeRow(t, renderTaskCard(t))).join('')}
|
||||
${sorted.map((t) => renderSwipeRow(t, renderTaskCard(t, {
|
||||
showCheckbox: state.bulkSelectMode,
|
||||
isChecked: state.selectedTaskIds.has(t.id),
|
||||
}))).join('')}
|
||||
</div>`;
|
||||
}).join('');
|
||||
}
|
||||
@@ -414,6 +421,8 @@ let state = {
|
||||
expandedTasks: new Set(),
|
||||
dragTaskId: null,
|
||||
filterPanelOpen: false,
|
||||
bulkSelectMode: false,
|
||||
selectedTaskIds: new Set(),
|
||||
};
|
||||
|
||||
// --------------------------------------------------------
|
||||
@@ -1504,11 +1513,21 @@ function wireViewToggle(container) {
|
||||
);
|
||||
const groupToggle = container.querySelector('#group-mode-toggle');
|
||||
if (groupToggle) groupToggle.style.display = state.viewMode === 'list' ? '' : 'none';
|
||||
const bulkSelectBtn = container.querySelector('#btn-bulk-select');
|
||||
if (bulkSelectBtn) {
|
||||
bulkSelectBtn.style.display = state.viewMode === 'list' ? '' : 'none';
|
||||
if (state.viewMode === 'kanban') {
|
||||
state.bulkSelectMode = false;
|
||||
state.selectedTaskIds.clear();
|
||||
bulkSelectBtn.classList.remove('btn--active');
|
||||
}
|
||||
}
|
||||
// Skeleton-Flash: einen Frame Render-Feedback geben, dann Ansicht aufbauen
|
||||
const listEl = container.querySelector('#task-list');
|
||||
if (listEl) listEl.style.opacity = '0.4';
|
||||
requestAnimationFrame(() => {
|
||||
renderTaskList(container);
|
||||
updateBulkActionsBar(container);
|
||||
const el = container.querySelector('#task-list');
|
||||
if (el) { el.style.transition = 'opacity 0.15s'; el.style.opacity = ''; }
|
||||
});
|
||||
@@ -1538,6 +1557,92 @@ function wireNewTaskBtn(container) {
|
||||
container.querySelector('#fab-new-task')?.addEventListener('click', handler);
|
||||
}
|
||||
|
||||
function updateBulkActionsBar(container) {
|
||||
const bar = container.querySelector('#bulk-actions-bar');
|
||||
const count = container.querySelector('#bulk-count');
|
||||
if (!bar) return;
|
||||
|
||||
const selected = state.selectedTaskIds.size;
|
||||
if (selected === 0) {
|
||||
bar.hidden = true;
|
||||
} else {
|
||||
bar.hidden = false;
|
||||
count.textContent = t('tasks.bulkSelectedCount', { count: selected });
|
||||
}
|
||||
}
|
||||
|
||||
function wireBulkSelect(container) {
|
||||
const toggleBtn = container.querySelector('#btn-bulk-select');
|
||||
if (!toggleBtn) return;
|
||||
|
||||
toggleBtn.addEventListener('click', () => {
|
||||
state.bulkSelectMode = !state.bulkSelectMode;
|
||||
if (!state.bulkSelectMode) {
|
||||
state.selectedTaskIds.clear();
|
||||
}
|
||||
toggleBtn.classList.toggle('btn--active', state.bulkSelectMode);
|
||||
loadTasks(container);
|
||||
});
|
||||
}
|
||||
|
||||
function wireBulkCheckboxes(container) {
|
||||
const listEl = container.querySelector('#task-list');
|
||||
if (!listEl) return;
|
||||
|
||||
listEl.addEventListener('change', (e) => {
|
||||
const checkbox = e.target.closest('.task-bulk-checkbox');
|
||||
if (!checkbox) return;
|
||||
|
||||
const taskId = Number(checkbox.dataset.taskId);
|
||||
if (checkbox.checked) {
|
||||
state.selectedTaskIds.add(taskId);
|
||||
} else {
|
||||
state.selectedTaskIds.delete(taskId);
|
||||
}
|
||||
updateBulkActionsBar(container);
|
||||
});
|
||||
}
|
||||
|
||||
function wireBulkActions(container) {
|
||||
const bar = container.querySelector('#bulk-actions-bar');
|
||||
if (!bar) return;
|
||||
|
||||
bar.addEventListener('click', async (e) => {
|
||||
const btn = e.target.closest('button[id^="bulk-"]');
|
||||
if (!btn) return;
|
||||
|
||||
const taskIds = [...state.selectedTaskIds];
|
||||
if (taskIds.length === 0) return;
|
||||
|
||||
const action = btn.id;
|
||||
let confirm = true;
|
||||
if (action === 'bulk-delete') {
|
||||
confirm = window.confirm(t('tasks.bulkDeleteConfirm', { count: taskIds.length }));
|
||||
}
|
||||
if (!confirm) return;
|
||||
|
||||
try {
|
||||
if (action === 'bulk-mark-done' || action === 'bulk-mark-open') {
|
||||
const status = btn.dataset.status;
|
||||
await Promise.all(taskIds.map(id => api.patch(`/tasks/${id}/status`, { status })));
|
||||
window.oikos.showToast(t('tasks.bulkStatusChanged'), 'success');
|
||||
} else if (action === 'bulk-archive') {
|
||||
await Promise.all(taskIds.map(id => api.patch(`/tasks/${id}/status`, { status: 'archived' })));
|
||||
window.oikos.showToast(t('tasks.bulkArchived'), 'success');
|
||||
} else if (action === 'bulk-delete') {
|
||||
await Promise.all(taskIds.map(id => api.delete(`/tasks/${id}`)));
|
||||
window.oikos.showToast(t('tasks.bulkDeleted'), 'success');
|
||||
}
|
||||
|
||||
state.selectedTaskIds.clear();
|
||||
updateBulkActionsBar(container);
|
||||
await loadTasks(container);
|
||||
} catch (err) {
|
||||
window.oikos.showToast(err.message ?? t('common.errorGeneric'), 'danger');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function wireTaskList(container) {
|
||||
const listEl = container.querySelector('#task-list');
|
||||
if (!listEl) return;
|
||||
@@ -1638,6 +1743,10 @@ export async function render(container, { user }) {
|
||||
<i data-lucide="columns" class="icon-md" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
<button class="btn btn--ghost btn--icon" id="btn-bulk-select" ${isKanban ? 'style="display:none"' : ''}
|
||||
title="${t('tasks.bulkSelect')}" aria-label="${t('tasks.bulkSelect')}">
|
||||
<i data-lucide="list-checks" class="icon-lg" aria-hidden="true"></i>
|
||||
</button>
|
||||
<button class="btn btn--primary" id="btn-new-task" style="gap:var(--space-1)">
|
||||
<i data-lucide="plus" class="icon-lg" aria-hidden="true"></i> ${t('tasks.newTask')}
|
||||
</button>
|
||||
@@ -1647,6 +1756,27 @@ export async function render(container, { user }) {
|
||||
<div class="tasks-body">
|
||||
<div class="tasks-filters" id="filter-bar"></div>
|
||||
<div class="filter-panel" id="filter-panel" hidden></div>
|
||||
<div class="bulk-actions-bar" id="bulk-actions-bar" hidden>
|
||||
<span class="bulk-actions-bar__count" id="bulk-count"></span>
|
||||
<div class="bulk-actions-bar__actions">
|
||||
<button class="btn btn--secondary btn--sm" id="bulk-mark-done" data-status="done">
|
||||
<i data-lucide="check" class="icon-base" aria-hidden="true"></i>
|
||||
${t('tasks.bulkMarkDone')}
|
||||
</button>
|
||||
<button class="btn btn--secondary btn--sm" id="bulk-mark-open" data-status="open">
|
||||
<i data-lucide="rotate-ccw" class="icon-base" aria-hidden="true"></i>
|
||||
${t('tasks.bulkMarkOpen')}
|
||||
</button>
|
||||
<button class="btn btn--secondary btn--sm" id="bulk-archive">
|
||||
<i data-lucide="archive" class="icon-base" aria-hidden="true"></i>
|
||||
${t('tasks.bulkArchive')}
|
||||
</button>
|
||||
<button class="btn btn--danger btn--sm" id="bulk-delete">
|
||||
<i data-lucide="trash-2" class="icon-base" aria-hidden="true"></i>
|
||||
${t('tasks.bulkDelete')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="task-list">
|
||||
${[1,2,3].map(() => `
|
||||
@@ -1691,6 +1821,9 @@ export async function render(container, { user }) {
|
||||
wireGroupToggle(container);
|
||||
wireNewTaskBtn(container);
|
||||
wireTaskList(container);
|
||||
wireBulkSelect(container);
|
||||
wireBulkCheckboxes(container);
|
||||
wireBulkActions(container);
|
||||
renderFilters(container);
|
||||
renderTaskList(container);
|
||||
|
||||
|
||||
+60
-24
@@ -121,6 +121,7 @@ let currentUser = null;
|
||||
let currentPath = null;
|
||||
let isNavigating = false;
|
||||
let _preferencesLoaded = false;
|
||||
let _disabledModules = new Set();
|
||||
// Gesetzt wenn auth:expired waehrend einer laufenden Navigation feuert.
|
||||
// Die Weiterleitung zu /login wird nach Abschluss der Navigation nachgeholt.
|
||||
let _pendingLoginRedirect = false;
|
||||
@@ -239,7 +240,15 @@ async function navigate(path, userOrPushState = true, pushState = true) {
|
||||
const basePath = path.split('?')[0];
|
||||
currentPath = basePath;
|
||||
|
||||
const route = ROUTES.find((r) => r.path === basePath) ?? ROUTES.find((r) => r.path === '/');
|
||||
let route = ROUTES.find((r) => r.path === basePath) ?? ROUTES.find((r) => r.path === '/');
|
||||
|
||||
// Modul-Guard: deaktivierte Module leiten auf Dashboard um.
|
||||
if (route.module && _disabledModules.has(route.module) && route.path !== '/') {
|
||||
currentPath = null;
|
||||
isNavigating = false;
|
||||
navigate('/');
|
||||
return;
|
||||
}
|
||||
|
||||
// Auth-Guard
|
||||
if (route.requiresAuth && !currentUser) {
|
||||
@@ -308,6 +317,9 @@ async function syncPreferencesOnce() {
|
||||
setAppName(res.data.app_name);
|
||||
updateBranding();
|
||||
}
|
||||
if (Array.isArray(res?.data?.disabled_modules)) {
|
||||
_disabledModules = new Set(res.data.disabled_modules);
|
||||
}
|
||||
} catch {
|
||||
// Non-critical. The settings page can refresh this later.
|
||||
}
|
||||
@@ -981,22 +993,32 @@ function renderSearchResults(container, data, onClose) {
|
||||
}
|
||||
|
||||
function navItems() {
|
||||
return [
|
||||
{ path: '/', label: t('nav.dashboard'), icon: 'layout-dashboard' },
|
||||
{ path: '/calendar', label: t('nav.calendar'), icon: 'calendar' },
|
||||
const all = [
|
||||
{ path: '/', label: t('nav.dashboard'), icon: 'layout-dashboard', module: 'dashboard' },
|
||||
{ path: '/calendar', label: t('nav.calendar'), icon: 'calendar', module: 'calendar' },
|
||||
// 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' },
|
||||
{ path: '/tasks', label: t('nav.tasks'), icon: 'check-square', module: 'tasks' },
|
||||
{ path: '/birthdays', label: t('nav.birthdays'), icon: 'cake', module: 'birthdays' },
|
||||
{ path: '/notes', label: t('nav.notes'), icon: 'sticky-note', module: 'notes' },
|
||||
{ path: '/contacts', label: t('nav.contacts'), icon: 'book-user', module: 'contacts' },
|
||||
{ path: '/budget', label: t('nav.budget'), icon: 'wallet', module: 'budget' },
|
||||
{ path: '/documents', label: t('nav.documents'), icon: 'folder-lock', module: 'documents' },
|
||||
{ path: '/settings', label: t('nav.settings'), icon: 'settings', module: 'settings' },
|
||||
// Kitchen-Gruppe: via Küche-Nav-Button (Bottom-Nav + Sidebar) + kitchen-tabs-bar erreichbar
|
||||
{ path: '/meals', label: t('nav.meals'), icon: 'utensils', kitchenGroup: true },
|
||||
{ path: '/recipes', label: t('nav.recipes'), icon: 'book-text', kitchenGroup: true },
|
||||
{ path: '/shopping', label: t('nav.shopping'), icon: 'shopping-cart', kitchenGroup: true },
|
||||
{ path: '/meals', label: t('nav.meals'), icon: 'utensils', module: 'meals', kitchenGroup: true },
|
||||
{ path: '/recipes', label: t('nav.recipes'), icon: 'book-text', module: 'recipes', kitchenGroup: true },
|
||||
{ path: '/shopping', label: t('nav.shopping'), icon: 'shopping-cart', module: 'shopping', kitchenGroup: true },
|
||||
];
|
||||
return all.filter((item) => !_disabledModules.has(item.module));
|
||||
}
|
||||
|
||||
function isModuleDisabled(moduleName) {
|
||||
return _disabledModules.has(moduleName);
|
||||
}
|
||||
|
||||
function setDisabledModules(modules) {
|
||||
_disabledModules = new Set(Array.isArray(modules) ? modules : []);
|
||||
rebuildNavigation();
|
||||
}
|
||||
|
||||
function navItemEl({ path, label, icon }) {
|
||||
@@ -1276,8 +1298,9 @@ window.addEventListener('auth:expired', () => {
|
||||
}
|
||||
});
|
||||
|
||||
// Sprache geändert: Navigation neu rendern damit Labels aktualisiert werden
|
||||
window.addEventListener('locale-changed', () => {
|
||||
// Navigation komplett neu rendern (z.B. nach Sprach- oder Modul-Toggle-Änderung).
|
||||
// Behält Bottom-Bar-Buttons (Kitchen, More) und More-Sheet-Handle/Suche bei.
|
||||
function rebuildNavigation({ updateLabels = true } = {}) {
|
||||
const skipLink = document.querySelector('.sr-only[href="#main-content"]');
|
||||
const navSidebar = document.querySelector('.nav-sidebar');
|
||||
const navSidebarItems = document.querySelector('.nav-sidebar__items');
|
||||
@@ -1286,10 +1309,14 @@ window.addEventListener('locale-changed', () => {
|
||||
const moreSheet = document.querySelector('#more-sheet');
|
||||
const moreBtnLabel = document.querySelector('#more-btn .nav-item__label');
|
||||
|
||||
if (skipLink) skipLink.textContent = t('common.skipToContent');
|
||||
if (navSidebar) navSidebar.setAttribute('aria-label', t('nav.main'));
|
||||
if (navBottom) navBottom.setAttribute('aria-label', t('nav.navigation'));
|
||||
if (moreBtnLabel) moreBtnLabel.textContent = t('nav.more');
|
||||
if (updateLabels) {
|
||||
if (skipLink) skipLink.textContent = t('common.skipToContent');
|
||||
if (navSidebar) navSidebar.setAttribute('aria-label', t('nav.main'));
|
||||
if (navBottom) navBottom.setAttribute('aria-label', t('nav.navigation'));
|
||||
if (moreBtnLabel) moreBtnLabel.textContent = t('nav.more');
|
||||
}
|
||||
|
||||
const kitchenVisible = ['meals', 'recipes', 'shopping'].some((m) => !_disabledModules.has(m));
|
||||
|
||||
if (navSidebarItems) {
|
||||
const sidebarEls = [];
|
||||
@@ -1297,7 +1324,7 @@ window.addEventListener('locale-changed', () => {
|
||||
.filter((item) => !item.kitchenGroup)
|
||||
.forEach((item) => {
|
||||
sidebarEls.push(navItemEl(item));
|
||||
if (item.path === '/calendar') sidebarEls.push(sidebarKitchenEl());
|
||||
if (item.path === '/calendar' && kitchenVisible) sidebarEls.push(sidebarKitchenEl());
|
||||
});
|
||||
navSidebarItems.replaceChildren(...sidebarEls);
|
||||
if (window.lucide) window.lucide.createIcons({ el: navSidebarItems });
|
||||
@@ -1305,9 +1332,13 @@ window.addEventListener('locale-changed', () => {
|
||||
if (bottomItems) {
|
||||
const kitchenBtnEl = bottomItems.querySelector('#kitchen-btn');
|
||||
const moreBtn = bottomItems.querySelector('#more-btn');
|
||||
if (kitchenBtnEl) kitchenBtnEl.querySelector('.nav-item__label').textContent = t('nav.kitchen');
|
||||
if (kitchenBtnEl) {
|
||||
kitchenBtnEl.querySelector('.nav-item__label').textContent = t('nav.kitchen');
|
||||
kitchenBtnEl.hidden = !kitchenVisible;
|
||||
}
|
||||
const newItems = navItems().slice(0, PRIMARY_NAV).map(navItemEl);
|
||||
bottomItems.replaceChildren(...newItems, kitchenBtnEl, moreBtn);
|
||||
const tail = [kitchenBtnEl, moreBtn].filter(Boolean);
|
||||
bottomItems.replaceChildren(...newItems, ...tail);
|
||||
}
|
||||
if (moreSheet) {
|
||||
const handle = moreSheet.querySelector('.more-sheet__handle');
|
||||
@@ -1330,7 +1361,10 @@ window.addEventListener('locale-changed', () => {
|
||||
|
||||
updateNav(currentPath);
|
||||
updateBranding(currentPath || '/');
|
||||
});
|
||||
}
|
||||
|
||||
// Sprache geändert: Navigation neu rendern damit Labels aktualisiert werden
|
||||
window.addEventListener('locale-changed', () => rebuildNavigation());
|
||||
|
||||
window.addEventListener('app-name-changed', () => {
|
||||
updateBranding(currentPath || '/');
|
||||
@@ -1422,6 +1456,8 @@ window.oikos = {
|
||||
showToast,
|
||||
friendlyError,
|
||||
setThemeColor,
|
||||
setDisabledModules,
|
||||
isModuleDisabled,
|
||||
applyTheme: (value) => {
|
||||
localStorage.setItem('oikos-theme', value);
|
||||
if (value === 'dark') {
|
||||
|
||||
@@ -776,3 +776,38 @@
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
/* --------------------------------------------------------
|
||||
Bulk selection
|
||||
-------------------------------------------------------- */
|
||||
|
||||
.task-bulk-checkbox {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-right: var(--space-2);
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.bulk-actions-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--space-3);
|
||||
padding: var(--space-3);
|
||||
margin-bottom: var(--space-3);
|
||||
background: var(--color-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
|
||||
.bulk-actions-bar__count {
|
||||
font-weight: 600;
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.bulk-actions-bar__actions {
|
||||
display: flex;
|
||||
gap: var(--space-2);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user