feat: Mobile Bottom-Navigation mit Swipe und Dot-Indikator
- 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 <noreply@anthropic.com>
This commit is contained in:
+43
-1
@@ -156,9 +156,18 @@ function renderAppShell(container) {
|
|||||||
</main>
|
</main>
|
||||||
|
|
||||||
<nav class="nav-bottom" aria-label="Navigation">
|
<nav class="nav-bottom" aria-label="Navigation">
|
||||||
<div class="nav-bottom__items" role="list">
|
<div class="nav-bottom__dots" aria-hidden="true">
|
||||||
|
<span class="nav-bottom__dot nav-bottom__dot--active"></span>
|
||||||
|
<span class="nav-bottom__dot"></span>
|
||||||
|
</div>
|
||||||
|
<div class="nav-bottom__scroll">
|
||||||
|
<div class="nav-bottom__page" role="list">
|
||||||
${navItems().slice(0, 5).map(navItemHtml).join('')}
|
${navItems().slice(0, 5).map(navItemHtml).join('')}
|
||||||
</div>
|
</div>
|
||||||
|
<div class="nav-bottom__page" role="list">
|
||||||
|
${navItems().slice(5).map(navItemHtml).join('')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div class="toast-container" id="toast-container" aria-live="assertive"></div>
|
<div class="toast-container" id="toast-container" aria-live="assertive"></div>
|
||||||
@@ -171,6 +180,36 @@ function renderAppShell(container) {
|
|||||||
navigate(el.dataset.route);
|
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() {
|
function navItems() {
|
||||||
@@ -211,6 +250,9 @@ function updateNav(path) {
|
|||||||
if (window.lucide) {
|
if (window.lucide) {
|
||||||
window.lucide.createIcons();
|
window.lucide.createIcons();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Bottom-Nav zur aktiven Seite scrollen
|
||||||
|
scrollNavToActive();
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderError(container, err) {
|
function renderError(container, err) {
|
||||||
|
|||||||
@@ -66,7 +66,7 @@
|
|||||||
* -------------------------------------------------------- */
|
* -------------------------------------------------------- */
|
||||||
.app-content {
|
.app-content {
|
||||||
flex: 1;
|
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;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,21 +86,58 @@
|
|||||||
bottom: 0;
|
bottom: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 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);
|
background-color: color-mix(in srgb, var(--color-surface) 85%, transparent);
|
||||||
border-top: 1px solid var(--color-border-subtle);
|
border-top: 1px solid var(--color-border-subtle);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
flex-direction: column;
|
||||||
z-index: var(--z-nav);
|
z-index: var(--z-nav);
|
||||||
backdrop-filter: blur(16px) saturate(180%);
|
backdrop-filter: blur(16px) saturate(180%);
|
||||||
-webkit-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;
|
display: flex;
|
||||||
width: 100%;
|
justify-content: center;
|
||||||
height: 100%;
|
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 ── */
|
/* ── Nav-Item (Bottom-Bar): Basis-State ── */
|
||||||
|
|||||||
+3
-3
@@ -12,9 +12,9 @@
|
|||||||
* API: Immer Netzwerk (kein Caching von Nutzerdaten)
|
* API: Immer Netzwerk (kein Caching von Nutzerdaten)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const SHELL_CACHE = 'oikos-shell-v11';
|
const SHELL_CACHE = 'oikos-shell-v12';
|
||||||
const PAGES_CACHE = 'oikos-pages-v11';
|
const PAGES_CACHE = 'oikos-pages-v12';
|
||||||
const ASSETS_CACHE = 'oikos-assets-v11';
|
const ASSETS_CACHE = 'oikos-assets-v12';
|
||||||
const ALL_CACHES = [SHELL_CACHE, PAGES_CACHE, ASSETS_CACHE];
|
const ALL_CACHES = [SHELL_CACHE, PAGES_CACHE, ASSETS_CACHE];
|
||||||
|
|
||||||
// App-Shell: sofort benötigt für ersten Render
|
// App-Shell: sofort benötigt für ersten Render
|
||||||
|
|||||||
Reference in New Issue
Block a user