From a2adb2b94cc34f82c33c8e1630cfe3674a1e140f Mon Sep 17 00:00:00 2001 From: ulsklyc Date: Thu, 26 Mar 2026 07:02:17 +0100 Subject: [PATCH] feat: Mobile Bottom-Navigation mit Swipe und Dot-Indikator MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Alle 9 Menüpunkte jetzt auf Mobile erreichbar (2 Seiten) - Horizontaler Scroll-Snap für seitenweises Swipen - Dezente Dot-Indikatoren zeigen aktive Seite an - Automatischer Scroll zur richtigen Seite bei Navigation zu Seite-2-Items - Service Worker Cache v12 Co-Authored-By: Claude Opus 4.6 --- public/router.js | 46 ++++++++++++++++++++++++++++++++++-- public/styles/layout.css | 51 ++++++++++++++++++++++++++++++++++------ public/sw.js | 6 ++--- 3 files changed, 91 insertions(+), 12 deletions(-) diff --git a/public/router.js b/public/router.js index 1480445..324b849 100644 --- a/public/router.js +++ b/public/router.js @@ -156,8 +156,17 @@ function renderAppShell(container) { @@ -171,6 +180,36 @@ function renderAppShell(container) { navigate(el.dataset.route); }); }); + + // Bottom-Nav: Scroll-Snap + Dot-Indikator + initBottomNavSwipe(container); +} + +/** + * Initialisiert Swipe-Gesten und Dot-Indikator für die mobile Bottom-Navigation. + */ +function initBottomNavSwipe(container) { + const scroll = container.querySelector('.nav-bottom__scroll'); + const dots = container.querySelectorAll('.nav-bottom__dot'); + if (!scroll || !dots.length) return; + + // Scroll-Event: Dot-Indikator aktualisieren + scroll.addEventListener('scroll', () => { + const page = Math.round(scroll.scrollLeft / scroll.offsetWidth); + dots.forEach((d, i) => d.classList.toggle('nav-bottom__dot--active', i === page)); + }, { passive: true }); +} + +/** + * Scrollt die Bottom-Nav zur richtigen Seite, wenn ein Item auf Seite 2 aktiv ist. + */ +function scrollNavToActive() { + const scroll = document.querySelector('.nav-bottom__scroll'); + if (!scroll) return; + const secondPage = navItems().slice(5).map(n => n.path); + if (secondPage.includes(currentPath)) { + scroll.scrollTo({ left: scroll.offsetWidth, behavior: 'smooth' }); + } } function navItems() { @@ -211,6 +250,9 @@ function updateNav(path) { if (window.lucide) { window.lucide.createIcons(); } + + // Bottom-Nav zur aktiven Seite scrollen + scrollNavToActive(); } function renderError(container, err) { diff --git a/public/styles/layout.css b/public/styles/layout.css index 7a4a64d..1341a7e 100644 --- a/public/styles/layout.css +++ b/public/styles/layout.css @@ -66,7 +66,7 @@ * -------------------------------------------------------- */ .app-content { flex: 1; - padding-bottom: calc(var(--nav-height-mobile) + var(--safe-area-inset-bottom)); + padding-bottom: calc(var(--nav-height-mobile) + 12px + var(--safe-area-inset-bottom)); overflow-y: auto; } @@ -86,21 +86,58 @@ bottom: 0; left: 0; right: 0; - height: calc(var(--nav-height-mobile) + var(--safe-area-inset-bottom)); - padding-bottom: var(--safe-area-inset-bottom); background-color: color-mix(in srgb, var(--color-surface) 85%, transparent); border-top: 1px solid var(--color-border-subtle); display: flex; - align-items: center; + flex-direction: column; z-index: var(--z-nav); backdrop-filter: blur(16px) saturate(180%); -webkit-backdrop-filter: blur(16px) saturate(180%); + padding-bottom: var(--safe-area-inset-bottom); } -.nav-bottom__items { +/* ── Dot-Indikator ── */ +.nav-bottom__dots { display: flex; - width: 100%; - height: 100%; + justify-content: center; + gap: 6px; + padding: 5px 0 2px; +} + +.nav-bottom__dot { + width: 5px; + height: 5px; + border-radius: 50%; + background-color: var(--color-text-tertiary); + opacity: 0.25; + transition: opacity 0.2s ease, transform 0.2s ease; +} + +.nav-bottom__dot--active { + opacity: 0.7; + transform: scale(1.2); +} + +/* ── Scroll-Container ── */ +.nav-bottom__scroll { + display: flex; + overflow-x: auto; + scroll-snap-type: x mandatory; + -webkit-overflow-scrolling: touch; + scrollbar-width: none; /* Firefox */ + height: var(--nav-height-mobile); +} + +.nav-bottom__scroll::-webkit-scrollbar { + display: none; /* Chrome/Safari */ +} + +/* ── Einzelne Seiten ── */ +.nav-bottom__page { + display: flex; + min-width: 100%; + flex-shrink: 0; + scroll-snap-align: start; } /* ── Nav-Item (Bottom-Bar): Basis-State ── */ diff --git a/public/sw.js b/public/sw.js index 0990118..3ad0181 100644 --- a/public/sw.js +++ b/public/sw.js @@ -12,9 +12,9 @@ * API: Immer Netzwerk (kein Caching von Nutzerdaten) */ -const SHELL_CACHE = 'oikos-shell-v11'; -const PAGES_CACHE = 'oikos-pages-v11'; -const ASSETS_CACHE = 'oikos-assets-v11'; +const SHELL_CACHE = 'oikos-shell-v12'; +const PAGES_CACHE = 'oikos-pages-v12'; +const ASSETS_CACHE = 'oikos-assets-v12'; const ALL_CACHES = [SHELL_CACHE, PAGES_CACHE, ASSETS_CACHE]; // App-Shell: sofort benötigt für ersten Render