From 624f3ab763bc0be496fd4db0e7c3eb33a75659b7 Mon Sep 17 00:00:00 2001 From: Ulas Date: Mon, 30 Mar 2026 16:43:05 +0200 Subject: [PATCH 01/18] style: unify card padding to 16px across all modules --- public/styles/budget.css | 2 +- public/styles/contacts.css | 2 +- public/styles/meals.css | 2 +- public/styles/tasks.css | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/public/styles/budget.css b/public/styles/budget.css index 0fd918a..6233920 100644 --- a/public/styles/budget.css +++ b/public/styles/budget.css @@ -198,7 +198,7 @@ display: flex; align-items: center; gap: var(--space-3); - padding: var(--space-2) var(--space-4); + padding: var(--space-4); border-bottom: 1px solid var(--color-border-subtle); cursor: pointer; transition: background-color var(--transition-fast); diff --git a/public/styles/contacts.css b/public/styles/contacts.css index 02067d7..eb9e5da 100644 --- a/public/styles/contacts.css +++ b/public/styles/contacts.css @@ -140,7 +140,7 @@ display: flex; align-items: center; gap: var(--space-3); - padding: var(--space-3) var(--space-4); + padding: var(--space-4); border-bottom: 1px solid var(--color-border-subtle); cursor: pointer; transition: background-color var(--transition-fast); diff --git a/public/styles/meals.css b/public/styles/meals.css index 8e57235..88461e1 100644 --- a/public/styles/meals.css +++ b/public/styles/meals.css @@ -180,7 +180,7 @@ flex-direction: column; align-items: flex-start; text-align: left; - padding: var(--space-1) var(--space-2) var(--space-2); + padding: var(--space-4); cursor: pointer; } diff --git a/public/styles/tasks.css b/public/styles/tasks.css index 9629864..f1e70fc 100644 --- a/public/styles/tasks.css +++ b/public/styles/tasks.css @@ -248,7 +248,7 @@ display: flex; align-items: flex-start; gap: var(--space-3); - padding: var(--space-3); + padding: var(--space-4); } /* Status-Checkbox */ From 194728bbe9ecea8192d6d86ca7312c59e6c81a44 Mon Sep 17 00:00:00 2001 From: Ulas Date: Mon, 30 Mar 2026 16:55:33 +0200 Subject: [PATCH 02/18] style: tie FAB colors to per-module accent tokens Co-Authored-By: Claude Sonnet 4.6 --- public/styles/budget.css | 5 +++++ public/styles/contacts.css | 5 +++++ public/styles/dashboard.css | 11 +++++++++-- public/styles/layout.css | 7 ++++--- public/styles/meals.css | 5 +++++ public/styles/notes.css | 5 +++++ public/styles/shopping.css | 5 +++++ public/styles/tasks.css | 5 +++++ 8 files changed, 43 insertions(+), 5 deletions(-) diff --git a/public/styles/budget.css b/public/styles/budget.css index 6233920..3958ad0 100644 --- a/public/styles/budget.css +++ b/public/styles/budget.css @@ -4,6 +4,11 @@ * Abhängigkeiten: tokens.css, layout.css */ +/* -------------------------------------------------------- + * Modul-Akzent + * -------------------------------------------------------- */ +.budget-page { --module-accent: var(--module-budget); } + /* -------------------------------------------------------- * Seiten-Layout * -------------------------------------------------------- */ diff --git a/public/styles/contacts.css b/public/styles/contacts.css index eb9e5da..85e78c6 100644 --- a/public/styles/contacts.css +++ b/public/styles/contacts.css @@ -4,6 +4,11 @@ * Abhängigkeiten: tokens.css, layout.css */ +/* -------------------------------------------------------- + * Modul-Akzent + * -------------------------------------------------------- */ +.contacts-page { --module-accent: var(--module-contacts); } + /* -------------------------------------------------------- * Seiten-Layout * -------------------------------------------------------- */ diff --git a/public/styles/dashboard.css b/public/styles/dashboard.css index 1600c0b..9a10f04 100644 --- a/public/styles/dashboard.css +++ b/public/styles/dashboard.css @@ -4,6 +4,13 @@ * Abhängigkeiten: tokens.css, layout.css */ +/* -------------------------------------------------------- + * Modul-Akzent + * -------------------------------------------------------- */ +.dashboard { + --module-accent: var(--module-dashboard); +} + /* -------------------------------------------------------- * Dashboard-Layout * -------------------------------------------------------- */ @@ -761,7 +768,7 @@ width: 52px; height: 52px; border-radius: var(--radius-full); - background-color: var(--color-accent); + background-color: var(--module-accent, var(--color-accent)); color: #ffffff; box-shadow: var(--shadow-lg); display: flex; @@ -783,7 +790,7 @@ } .fab-main:hover { - background-color: var(--color-accent-hover); + background-color: color-mix(in srgb, var(--module-accent, var(--color-accent)) 85%, black); } .fab-main:active { diff --git a/public/styles/layout.css b/public/styles/layout.css index cff0133..33c5eec 100644 --- a/public/styles/layout.css +++ b/public/styles/layout.css @@ -192,7 +192,7 @@ width: 52px; height: 52px; border-radius: var(--radius-full); - background-color: var(--color-accent); + background-color: var(--module-accent, var(--color-accent)); color: #ffffff; box-shadow: var(--shadow-lg); display: flex; @@ -206,7 +206,7 @@ } .page-fab:hover { - background-color: var(--color-accent-hover); + background-color: color-mix(in srgb, var(--module-accent, var(--color-accent)) 85%, black); } .page-fab:active { @@ -774,7 +774,7 @@ width: 52px; height: 52px; border-radius: var(--radius-full); - background-color: var(--color-accent); + background-color: var(--module-accent, var(--color-btn-primary)); color: #ffffff; box-shadow: var(--shadow-lg); display: flex; @@ -787,6 +787,7 @@ } .fab:hover { + background-color: color-mix(in srgb, var(--module-accent, var(--color-btn-primary)) 85%, black); transform: scale(1.05); } diff --git a/public/styles/meals.css b/public/styles/meals.css index 88461e1..0821bab 100644 --- a/public/styles/meals.css +++ b/public/styles/meals.css @@ -4,6 +4,11 @@ * Abhängigkeiten: tokens.css, layout.css */ +/* -------------------------------------------------------- + * Modul-Akzent + * -------------------------------------------------------- */ +.meals-page { --module-accent: var(--module-meals); } + /* -------------------------------------------------------- * Seiten-Layout * -------------------------------------------------------- */ diff --git a/public/styles/notes.css b/public/styles/notes.css index baf8e44..f507830 100644 --- a/public/styles/notes.css +++ b/public/styles/notes.css @@ -4,6 +4,11 @@ * Abhängigkeiten: tokens.css, layout.css */ +/* -------------------------------------------------------- + * Modul-Akzent + * -------------------------------------------------------- */ +.notes-page { --module-accent: var(--module-notes); } + /* -------------------------------------------------------- * Seiten-Layout * -------------------------------------------------------- */ diff --git a/public/styles/shopping.css b/public/styles/shopping.css index 1292d2e..4e43851 100644 --- a/public/styles/shopping.css +++ b/public/styles/shopping.css @@ -4,6 +4,11 @@ * Abhängigkeiten: tokens.css, layout.css */ +/* -------------------------------------------------------- + * Modul-Akzent + * -------------------------------------------------------- */ +.shopping-page { --module-accent: var(--module-shopping); } + /* -------------------------------------------------------- * Seiten-Layout * -------------------------------------------------------- */ diff --git a/public/styles/tasks.css b/public/styles/tasks.css index f1e70fc..c905ae7 100644 --- a/public/styles/tasks.css +++ b/public/styles/tasks.css @@ -4,6 +4,11 @@ * Abhängigkeiten: tokens.css, layout.css */ +/* -------------------------------------------------------- + * Modul-Akzent + * -------------------------------------------------------- */ +.tasks-page { --module-accent: var(--module-tasks); } + /* -------------------------------------------------------- * Seiten-Layout * -------------------------------------------------------- */ From bc3f855fa9c0c713a7157ece61032e38fb306cf0 Mon Sep 17 00:00:00 2001 From: Ulas Date: Mon, 30 Mar 2026 17:08:49 +0200 Subject: [PATCH 03/18] feat: directional slide-x page transitions in router Co-Authored-By: Claude Sonnet 4.6 --- public/router.js | 27 ++++++++++++++++++---- public/styles/layout.css | 49 ++++++++++++++++++++++++++++++++-------- 2 files changed, 61 insertions(+), 15 deletions(-) diff --git a/public/router.js b/public/router.js index 1c4bf49..b373f42 100644 --- a/public/router.js +++ b/public/router.js @@ -85,6 +85,16 @@ let currentPath = null; // Router // -------------------------------------------------------- +const ROUTE_ORDER = ['/', '/tasks', '/shopping', '/meals', '/calendar', + '/notes', '/contacts', '/budget', '/settings']; + +function getDirection(fromPath, toPath) { + const fromIdx = ROUTE_ORDER.indexOf(fromPath ?? '/'); + const toIdx = ROUTE_ORDER.indexOf(toPath); + if (fromIdx === -1 || toIdx === -1 || fromPath === toPath) return 'right'; + return toIdx > fromIdx ? 'right' : 'left'; +} + /** * Navigiert zu einem Pfad und rendert die entsprechende Seite. * @param {string} path @@ -100,6 +110,8 @@ async function navigate(path, userOrPushState = true, pushState = true) { pushState = userOrPushState; } + // Alten Pfad merken, bevor currentPath aktualisiert wird — für Richtungsberechnung + const previousPath = currentPath; currentPath = path; const route = ROUTES.find((r) => r.path === path) ?? ROUTES.find((r) => r.path === '/'); @@ -126,7 +138,7 @@ async function navigate(path, userOrPushState = true, pushState = true) { history.pushState({ path }, '', path); } - await renderPage(route); + await renderPage(route, previousPath); updateNav(path); updateThemeColorForRoute(route); } @@ -134,8 +146,9 @@ async function navigate(path, userOrPushState = true, pushState = true) { /** * Lädt und rendert eine Seite dynamisch. * @param {{ path: string, page: string }} route + * @param {string|null} previousPath - Pfad vor der Navigation (für Richtungsberechnung) */ -async function renderPage(route) { +async function renderPage(route, previousPath = null) { const app = document.getElementById('app'); const loading = document.getElementById('app-loading'); @@ -158,18 +171,22 @@ async function renderPage(route) { const content = document.getElementById('page-content') || app; + // Richtung bestimmen (previousPath ist der alte Pfad vor der Navigation) + const direction = getDirection(previousPath, route.path); + const outClass = direction === 'right' ? 'page-transition--out-left' : 'page-transition--out-right'; + const inClass = direction === 'right' ? 'page-transition--in-right' : 'page-transition--in-left'; + // Alte Seite kurz ausfaden, falls vorhanden const oldPage = content.querySelector('.page-transition'); if (oldPage) { - oldPage.classList.add('page-transition--out'); + oldPage.classList.add(outClass); await new Promise(r => setTimeout(r, 120)); } // Seiten-Wrapper bereits jetzt in den DOM einfügen, damit // document.getElementById() in render() die richtigen Elemente findet. const pageWrapper = document.createElement('div'); - pageWrapper.className = 'page-transition'; - pageWrapper.style.animation = 'page-in 0.2s ease forwards'; + pageWrapper.className = `page-transition ${inClass}`; content.replaceChildren(pageWrapper); await module.render(pageWrapper, { user: currentUser }); diff --git a/public/styles/layout.css b/public/styles/layout.css index 33c5eec..4b3782f 100644 --- a/public/styles/layout.css +++ b/public/styles/layout.css @@ -44,22 +44,51 @@ } /* -------------------------------------------------------- - * Seiten-Übergangs-Animation + * Seiten-Übergangs-Animation (direktional) * -------------------------------------------------------- */ -@keyframes page-in { - from { opacity: 0; transform: translateY(4px); } - to { opacity: 1; transform: translateY(0); } +@keyframes page-slide-in-right { + from { opacity: 0; transform: translateX(20px); } + to { opacity: 1; transform: translateX(0); } +} +@keyframes page-slide-in-left { + from { opacity: 0; transform: translateX(-20px); } + to { opacity: 1; transform: translateX(0); } +} +@keyframes page-out-left { + from { opacity: 1; transform: translateX(0); } + to { opacity: 0; transform: translateX(-20px); } +} +@keyframes page-out-right { + from { opacity: 1; transform: translateX(0); } + to { opacity: 0; transform: translateX(20px); } } -@keyframes page-out { - from { opacity: 1; } - to { opacity: 0; } +.page-transition--in-right { + animation: page-slide-in-right 0.2s var(--ease-out) forwards; } - -.page-transition--out { - animation: page-out 0.12s ease forwards; +.page-transition--in-left { + animation: page-slide-in-left 0.2s var(--ease-out) forwards; +} +.page-transition--out-left { + animation: page-out-left 0.12s ease forwards; pointer-events: none; } +.page-transition--out-right { + animation: page-out-right 0.12s ease forwards; + pointer-events: none; +} + +@media (prefers-reduced-motion: reduce) { + .page-transition--in-right, + .page-transition--in-left { + animation: none; + opacity: 1; + } + .page-transition--out-left, + .page-transition--out-right { + animation: none; + } +} /* -------------------------------------------------------- * Layout: Mobile (Standard, < 1024px) From 20792e9894a6534864284ed0fbd0df6e68bd3325 Mon Sep 17 00:00:00 2001 From: Ulas Date: Mon, 30 Mar 2026 17:11:53 +0200 Subject: [PATCH 04/18] fix: sync ROUTE_ORDER with nav order, guard against navigation race condition Co-Authored-By: Claude Sonnet 4.6 --- public/router.js | 72 +++++++++++++++++++++++++++--------------------- 1 file changed, 41 insertions(+), 31 deletions(-) diff --git a/public/router.js b/public/router.js index b373f42..19ff1ea 100644 --- a/public/router.js +++ b/public/router.js @@ -80,12 +80,13 @@ async function importPage(pagePath) { // -------------------------------------------------------- let currentUser = null; let currentPath = null; +let isNavigating = false; // -------------------------------------------------------- // Router // -------------------------------------------------------- -const ROUTE_ORDER = ['/', '/tasks', '/shopping', '/meals', '/calendar', +const ROUTE_ORDER = ['/', '/tasks', '/calendar', '/meals', '/shopping', '/notes', '/contacts', '/budget', '/settings']; function getDirection(fromPath, toPath) { @@ -103,44 +104,53 @@ function getDirection(fromPath, toPath) { * @param {boolean} pushState - false beim initialen Load und popstate */ async function navigate(path, userOrPushState = true, pushState = true) { - // Überlastung: navigate(path, user) nach Login vs navigate(path, false) beim Init - if (typeof userOrPushState === 'object' && userOrPushState !== null) { - currentUser = userOrPushState; - } else { - pushState = userOrPushState; - } + if (isNavigating) return; + isNavigating = true; - // Alten Pfad merken, bevor currentPath aktualisiert wird — für Richtungsberechnung - const previousPath = currentPath; - currentPath = path; + try { + // Überlastung: navigate(path, user) nach Login vs navigate(path, false) beim Init + if (typeof userOrPushState === 'object' && userOrPushState !== null) { + currentUser = userOrPushState; + } else { + pushState = userOrPushState; + } - const route = ROUTES.find((r) => r.path === path) ?? ROUTES.find((r) => r.path === '/'); + // Alten Pfad merken, bevor currentPath aktualisiert wird — für Richtungsberechnung + const previousPath = currentPath; + currentPath = path; - // Auth-Guard - if (route.requiresAuth && !currentUser) { - try { - const result = await auth.me(); - currentUser = result.user; - } catch { - currentPath = null; // Reset damit navigate('/login') nicht geblockt wird - navigate('/login'); + const route = ROUTES.find((r) => r.path === path) ?? ROUTES.find((r) => r.path === '/'); + + // Auth-Guard + if (route.requiresAuth && !currentUser) { + try { + const result = await auth.me(); + currentUser = result.user; + } catch { + currentPath = null; // Reset damit navigate('/login') nicht geblockt wird + isNavigating = false; + navigate('/login'); + return; + } + } + + if (!route.requiresAuth && currentUser && path === '/login') { + currentPath = null; + isNavigating = false; + navigate('/'); return; } - } - if (!route.requiresAuth && currentUser && path === '/login') { - currentPath = null; - navigate('/'); - return; - } + if (pushState) { + history.pushState({ path }, '', path); + } - if (pushState) { - history.pushState({ path }, '', path); + await renderPage(route, previousPath); + updateNav(path); + updateThemeColorForRoute(route); + } finally { + isNavigating = false; } - - await renderPage(route, previousPath); - updateNav(path); - updateThemeColorForRoute(route); } /** From f4eb567219ef7df1e51e17138ec6dbd9aafad5c9 Mon Sep 17 00:00:00 2001 From: Ulas Date: Mon, 30 Mar 2026 17:14:15 +0200 Subject: [PATCH 05/18] feat: add stagger() and vibrate() UX utilities with tests Co-Authored-By: Claude Sonnet 4.6 --- package.json | 3 ++- public/utils/ux.js | 42 +++++++++++++++++++++++++++++++++++++++ test-ux-utils.js | 49 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 93 insertions(+), 1 deletion(-) create mode 100644 public/utils/ux.js create mode 100644 test-ux-utils.js diff --git a/package.json b/package.json index 6a47635..8637035 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,8 @@ "test:meals": "node --experimental-sqlite test-meals.js", "test:calendar": "node --experimental-sqlite test-calendar.js", "test:ncb": "node --experimental-sqlite test-notes-contacts-budget.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" + "test:ux-utils": "node --experimental-vm-modules test-ux-utils.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" }, "dependencies": { "bcrypt": "^5.1.1", diff --git a/public/utils/ux.js b/public/utils/ux.js new file mode 100644 index 0000000..bd0c704 --- /dev/null +++ b/public/utils/ux.js @@ -0,0 +1,42 @@ +/** + * Modul: UX Utilities + * Zweck: Wiederverwendbare Animationshelfer (Stagger, Vibration) + * Abhängigkeiten: keine + */ + +/** + * Gestaffeltes Einblenden einer NodeList oder eines Arrays von Elementen. + * Maximal MAX_STAGGER Elemente werden verzögert, der Rest sofort eingeblendet. + * + * @param {NodeList|Element[]} elements + * @param {Object} [opts] + * @param {number} [opts.delay=30] — ms zwischen jedem Element + * @param {number} [opts.duration=180] — ms pro Element + * @param {number} [opts.max=5] — Maximale Anzahl gestaffelter Elemente + */ +export function stagger(elements, { delay = 30, duration = 180, max = 5 } = {}) { + if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return; + const els = Array.from(elements); + els.forEach((el, i) => { + const itemDelay = i < max ? i * delay : max * delay; + el.style.opacity = '0'; + el.style.transform = 'translateY(8px)'; + el.style.transition = `opacity ${duration}ms ease, transform ${duration}ms ease`; + setTimeout(() => { + el.style.opacity = '1'; + el.style.transform = 'translateY(0)'; + }, itemDelay); + }); +} + +/** + * Vibrationsmuster abspielen, wenn die API verfügbar ist und + * keine reduzierte Bewegung gewünscht wird. + * + * @param {number|number[]} pattern — ms oder [an, aus, an, ...]-Array + */ +export function vibrate(pattern) { + if (!navigator.vibrate) return; + if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return; + navigator.vibrate(pattern); +} diff --git a/test-ux-utils.js b/test-ux-utils.js new file mode 100644 index 0000000..00cc991 --- /dev/null +++ b/test-ux-utils.js @@ -0,0 +1,49 @@ +/** + * Tests: UX Utilities (stagger, vibrate) + * Läuft im Node-Kontext — kein DOM verfügbar, daher nur Pure-Logic-Tests. + */ +import { test } from 'node:test'; +import assert from 'node:assert/strict'; + +// Minimales Window/Navigator-Mock für Node +const { stagger, vibrate } = await (async () => { + // stagger braucht window.matchMedia — wir mocken es + global.window = { + matchMedia: () => ({ matches: false }), + }; + // navigator ist in Node ein getter-only property — über defineProperty überschreiben + Object.defineProperty(global, 'navigator', { + value: { vibrate: null }, + writable: true, + configurable: true, + }); + return import('./public/utils/ux.js'); +})(); + +test('stagger: setzt opacity:0 auf alle Elemente', () => { + const els = [{ style: {} }, { style: {} }, { style: {} }]; + stagger(els, { delay: 0, duration: 0 }); + assert.equal(els[0].style.opacity, '0'); + assert.equal(els[1].style.opacity, '0'); + assert.equal(els[2].style.opacity, '0'); +}); + +test('stagger: tut nichts bei prefers-reduced-motion', () => { + global.window.matchMedia = () => ({ matches: true }); + const els = [{ style: {} }]; + stagger(els); + assert.equal(els[0].style.opacity, undefined); // unverändert + global.window.matchMedia = () => ({ matches: false }); // reset +}); + +test('vibrate: tut nichts wenn API nicht vorhanden', () => { + Object.defineProperty(global, 'navigator', { value: { vibrate: null }, writable: true, configurable: true }); + assert.doesNotThrow(() => vibrate(10)); +}); + +test('vibrate: ruft navigator.vibrate auf wenn vorhanden', () => { + let called = null; + Object.defineProperty(global, 'navigator', { value: { vibrate: (p) => { called = p; } }, writable: true, configurable: true }); + vibrate(15); + assert.equal(called, 15); +}); From b2327375b87b66a2d2a0fc272ed16d835cb81bc0 Mon Sep 17 00:00:00 2001 From: Ulas Date: Mon, 30 Mar 2026 17:16:20 +0200 Subject: [PATCH 06/18] fix: remove unnecessary --experimental-vm-modules flag from test:ux-utils --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 8637035..fa533d8 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "test:meals": "node --experimental-sqlite test-meals.js", "test:calendar": "node --experimental-sqlite test-calendar.js", "test:ncb": "node --experimental-sqlite test-notes-contacts-budget.js", - "test:ux-utils": "node --experimental-vm-modules test-ux-utils.js", + "test:ux-utils": "node test-ux-utils.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" }, "dependencies": { From bc6e759b796a31fed5e2cb43ac41d5b368cc115c Mon Sep 17 00:00:00 2001 From: Ulas Date: Mon, 30 Mar 2026 17:19:33 +0200 Subject: [PATCH 07/18] feat: staggered fade-in for list items across all modules Co-Authored-By: Claude Sonnet 4.6 --- public/pages/budget.js | 2 ++ public/pages/calendar.js | 3 +++ public/pages/contacts.js | 2 ++ public/pages/meals.js | 2 ++ public/pages/notes.js | 2 ++ public/pages/shopping.js | 3 +++ public/pages/tasks.js | 2 ++ 7 files changed, 16 insertions(+) diff --git a/public/pages/budget.js b/public/pages/budget.js index d03b5bd..98f7115 100644 --- a/public/pages/budget.js +++ b/public/pages/budget.js @@ -7,6 +7,7 @@ import { api } from '/api.js'; import { openModal as openSharedModal, closeModal } from '/components/modal.js'; +import { stagger } from '/utils/ux.js'; // -------------------------------------------------------- // Konstanten @@ -202,6 +203,7 @@ function renderBody() { `; if (window.lucide) lucide.createIcons(); + stagger(_container.querySelectorAll('.budget-entry')); _container.querySelector('#budget-list')?.addEventListener('click', async (e) => { const delBtn = e.target.closest('[data-action="delete"]'); diff --git a/public/pages/calendar.js b/public/pages/calendar.js index b04e82f..dc64735 100644 --- a/public/pages/calendar.js +++ b/public/pages/calendar.js @@ -7,6 +7,7 @@ import { api } from '/api.js'; import { renderRRuleFields, bindRRuleEvents, getRRuleValues } from '/rrule-ui.js'; import { openModal as openSharedModal, closeModal } from '/components/modal.js'; +import { stagger } from '/utils/ux.js'; // -------------------------------------------------------- // Konstanten @@ -597,6 +598,8 @@ function renderAgendaView(container) { `; + stagger(container.querySelectorAll('.agenda-event')); + container.querySelector('#agenda-view').addEventListener('click', (e) => { const evEl = e.target.closest('.agenda-event'); if (evEl) { diff --git a/public/pages/contacts.js b/public/pages/contacts.js index aa7996b..5ca0890 100644 --- a/public/pages/contacts.js +++ b/public/pages/contacts.js @@ -6,6 +6,7 @@ import { api } from '/api.js'; import { openModal as openSharedModal, closeModal } from '/components/modal.js'; +import { stagger } from '/utils/ux.js'; // -------------------------------------------------------- // Konstanten @@ -159,6 +160,7 @@ function renderList() { `).join(''); if (window.lucide) lucide.createIcons(); + stagger(container.querySelectorAll('.contact-item')); // Event-Delegation container.addEventListener('click', async (e) => { diff --git a/public/pages/meals.js b/public/pages/meals.js index e69c895..410d7e4 100644 --- a/public/pages/meals.js +++ b/public/pages/meals.js @@ -6,6 +6,7 @@ import { api } from '/api.js'; import { openModal as openSharedModal, closeModal as closeSharedModal } from '/components/modal.js'; +import { stagger } from '/utils/ux.js'; // -------------------------------------------------------- // Konstanten @@ -162,6 +163,7 @@ function renderWeekGrid() { }).join(''); if (window.lucide) lucide.createIcons(); + stagger(grid.querySelectorAll('.meal-card')); wireGrid(grid); } diff --git a/public/pages/notes.js b/public/pages/notes.js index 9006335..d101af4 100644 --- a/public/pages/notes.js +++ b/public/pages/notes.js @@ -6,6 +6,7 @@ import { api } from '/api.js'; import { openModal as openSharedModal, closeModal } from '/components/modal.js'; +import { stagger } from '/utils/ux.js'; // -------------------------------------------------------- // Konstanten @@ -99,6 +100,7 @@ function renderGrid() { grid.innerHTML = state.notes.map((n) => renderNoteCard(n)).join(''); if (window.lucide) lucide.createIcons(); + stagger(grid.querySelectorAll('.note-card')); grid.addEventListener('click', async (e) => { // Pin diff --git a/public/pages/shopping.js b/public/pages/shopping.js index eb95e93..ac100f8 100644 --- a/public/pages/shopping.js +++ b/public/pages/shopping.js @@ -5,6 +5,7 @@ */ import { api } from '/api.js'; +import { stagger } from '/utils/ux.js'; // -------------------------------------------------------- // Konstanten @@ -151,6 +152,7 @@ function renderListContent(container) { `; if (window.lucide) window.lucide.createIcons(); + stagger(content.querySelectorAll('.shopping-item')); wireAutocomplete(container); wireQuickAdd(container); } @@ -311,6 +313,7 @@ function updateItemsList(container) { if (listEl) { listEl.innerHTML = renderItems(); if (window.lucide) window.lucide.createIcons(); + stagger(listEl.querySelectorAll('.shopping-item')); } // clear-checked Button aktualisieren const checkedCount = state.items.filter((i) => i.is_checked).length; diff --git a/public/pages/tasks.js b/public/pages/tasks.js index 8d9a158..b9e0fef 100644 --- a/public/pages/tasks.js +++ b/public/pages/tasks.js @@ -7,6 +7,7 @@ import { api } from '/api.js'; import { renderRRuleFields, bindRRuleEvents, getRRuleValues } from '/rrule-ui.js'; import { openModal as openSharedModal, closeModal } from '/components/modal.js'; +import { stagger } from '/utils/ux.js'; // -------------------------------------------------------- // Konstanten @@ -605,6 +606,7 @@ function renderTaskList(container) { if (!listEl) return; listEl.innerHTML = renderTaskGroups(state.tasks, state.groupMode); if (window.lucide) window.lucide.createIcons(); + stagger(listEl.querySelectorAll('.swipe-row, .kanban-card')); updateOverdueBadge(); wireSwipeGestures(container); } From eb0ac95e1d41ccb7bb9b9be48b2ec4207caac3a1 Mon Sep 17 00:00:00 2001 From: Ulas Date: Mon, 30 Mar 2026 17:21:53 +0200 Subject: [PATCH 08/18] fix: scope stagger selector to #budget-list in budget.js --- public/pages/budget.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/pages/budget.js b/public/pages/budget.js index 98f7115..c51c664 100644 --- a/public/pages/budget.js +++ b/public/pages/budget.js @@ -203,7 +203,7 @@ function renderBody() { `; if (window.lucide) lucide.createIcons(); - stagger(_container.querySelectorAll('.budget-entry')); + stagger(_container.querySelector('#budget-list')?.querySelectorAll('.budget-entry') ?? []); _container.querySelector('#budget-list')?.addEventListener('click', async (e) => { const delBtn = e.target.closest('[data-action="delete"]'); From 0eab480a0e10a0482d976e02f78f4f67316211e2 Mon Sep 17 00:00:00 2001 From: Ulas Date: Mon, 30 Mar 2026 17:25:13 +0200 Subject: [PATCH 09/18] style: unify all empty states to shared .empty-state class across all modules Co-Authored-By: Claude Sonnet 4.6 --- public/pages/budget.js | 11 +++++++---- public/pages/contacts.js | 12 +++++++++--- public/pages/meals.js | 5 ++++- public/pages/notes.js | 14 ++++++++++---- public/pages/shopping.js | 11 +++++++---- public/pages/tasks.js | 11 +++++++---- public/styles/budget.css | 13 ------------- public/styles/contacts.css | 13 ------------- public/styles/layout.css | 8 ++++++++ public/styles/notes.css | 20 -------------------- public/styles/shopping.css | 30 ------------------------------ public/styles/tasks.css | 25 ------------------------- 12 files changed, 52 insertions(+), 121 deletions(-) diff --git a/public/pages/budget.js b/public/pages/budget.js index c51c664..4fa5972 100644 --- a/public/pages/budget.js +++ b/public/pages/budget.js @@ -241,10 +241,13 @@ function renderCategoryBars(byCategory) { function renderEntries() { if (!state.entries.length) { - return `
- -
Keine Einträge
-
Noch keine Transaktionen für diesen Monat.
+ return `
+ +
Keine Einträge diesen Monat
+
Budget-Einträge über den + Button hinzufügen.
`; } diff --git a/public/pages/contacts.js b/public/pages/contacts.js index 5ca0890..e8fc55d 100644 --- a/public/pages/contacts.js +++ b/public/pages/contacts.js @@ -134,9 +134,15 @@ function renderList() { if (!contacts.length) { container.innerHTML = ` -
- -
Keine Kontakte gefunden
+
+ +
Noch keine Kontakte
+
Neue Kontakte über den + Button hinzufügen.
`; if (window.lucide) lucide.createIcons(); diff --git a/public/pages/meals.js b/public/pages/meals.js index 410d7e4..ae0061f 100644 --- a/public/pages/meals.js +++ b/public/pages/meals.js @@ -172,8 +172,11 @@ function renderSlot(date, type, mealsForDay) { if (!meal) { return ` -
+
${type.label}
+
+
Kein Essen geplant
+