FEAT-7: AppShell 'Mehr' overflow nav (phone: 4 tabs + Mehr sheet; desktop: all 7) + de.nav.more
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,24 +1,40 @@
|
|||||||
|
import { useState } from 'react'
|
||||||
import { NavLink, Outlet } from 'react-router-dom'
|
import { NavLink, Outlet } from 'react-router-dom'
|
||||||
import { de } from '../strings/de'
|
import { de } from '../strings/de'
|
||||||
|
import './appShell.css'
|
||||||
|
|
||||||
const navItems = [
|
interface NavItem {
|
||||||
|
to: string
|
||||||
|
label: string
|
||||||
|
icon: string
|
||||||
|
end?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
// Primary items are always visible in the phone bottom bar; secondary items
|
||||||
|
// move into the "Mehr" sheet on phones and appear inline in the desktop sidebar.
|
||||||
|
const PRIMARY: NavItem[] = [
|
||||||
{ to: '/', label: de.nav.home, icon: '🏠', end: true },
|
{ to: '/', label: de.nav.home, icon: '🏠', end: true },
|
||||||
{ to: '/rennmaeuse', label: de.nav.gerbils, icon: '🐭', end: false },
|
{ to: '/rennmaeuse', label: de.nav.gerbils, icon: '🐭' },
|
||||||
{ to: '/wuerfe', label: de.nav.litters, icon: '🍼', end: false },
|
{ to: '/wuerfe', label: de.nav.litters, icon: '🍼' },
|
||||||
{ to: '/becken', label: de.nav.enclosures, icon: '🛁', end: false },
|
{ to: '/genetik', label: de.nav.genetics, icon: '🧬' },
|
||||||
{ to: '/kontakte', label: de.nav.contacts, icon: '📇', end: false },
|
|
||||||
{ to: '/genetik', label: de.nav.genetics, icon: '🧬', end: false },
|
|
||||||
// FEAT-7: vorerst 7. Tab; „Mehr“-Overflow für das Smartphone ist mit Kevin
|
|
||||||
// abgestimmt (FEAT-7-Konversation) und ersetzt diesen Eintrag dann.
|
|
||||||
{ to: '/statistik', label: de.nav.statistics, icon: '📊', end: false },
|
|
||||||
]
|
]
|
||||||
|
const SECONDARY: NavItem[] = [
|
||||||
|
{ to: '/becken', label: de.nav.enclosures, icon: '🛁' },
|
||||||
|
{ to: '/kontakte', label: de.nav.contacts, icon: '📇' },
|
||||||
|
{ to: '/statistik', label: de.nav.statistics, icon: '📊' },
|
||||||
|
]
|
||||||
|
|
||||||
|
const linkClass = ({ isActive }: { isActive: boolean }) =>
|
||||||
|
isActive ? 'nav-link nav-link--active' : 'nav-link'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* App-Grundgerüst, mobile-first:
|
* App-Grundgerüst, mobile-first:
|
||||||
* - Smartphone: Kopfzeile oben, Tab-Leiste unten
|
* - Smartphone: Kopfzeile oben, Tab-Leiste unten (4 Haupt-Tabs + „Mehr“).
|
||||||
* - Laptop (ab 768px): Seitenleiste links, Inhalt rechts
|
* - Laptop (ab 768px): Seitenleiste links mit allen Einträgen, kein „Mehr“.
|
||||||
*/
|
*/
|
||||||
export default function AppShell() {
|
export default function AppShell() {
|
||||||
|
const [moreOpen, setMoreOpen] = useState(false)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="app-shell">
|
<div className="app-shell">
|
||||||
<header className="app-header">
|
<header className="app-header">
|
||||||
@@ -27,21 +43,37 @@ export default function AppShell() {
|
|||||||
</header>
|
</header>
|
||||||
|
|
||||||
<nav className="app-nav" aria-label={de.nav.mainNavigation}>
|
<nav className="app-nav" aria-label={de.nav.mainNavigation}>
|
||||||
{navItems.map((item) => (
|
<div className="nav-primary">
|
||||||
<NavLink
|
{PRIMARY.map((item) => (
|
||||||
key={item.to}
|
<NavLink key={item.to} to={item.to} end={item.end} className={linkClass}>
|
||||||
to={item.to}
|
<span className="nav-icon" aria-hidden="true">{item.icon}</span>
|
||||||
end={item.end}
|
<span className="nav-label">{item.label}</span>
|
||||||
className={({ isActive }) =>
|
</NavLink>
|
||||||
isActive ? 'nav-link nav-link--active' : 'nav-link'
|
))}
|
||||||
}
|
<button
|
||||||
|
type="button"
|
||||||
|
className={moreOpen ? 'nav-link nav-more-btn nav-link--active' : 'nav-link nav-more-btn'}
|
||||||
|
aria-expanded={moreOpen}
|
||||||
|
onClick={() => setMoreOpen((o) => !o)}
|
||||||
>
|
>
|
||||||
<span className="nav-icon" aria-hidden="true">
|
<span className="nav-icon" aria-hidden="true">☰</span>
|
||||||
{item.icon}
|
<span className="nav-label">{de.nav.more}</span>
|
||||||
</span>
|
</button>
|
||||||
<span className="nav-label">{item.label}</span>
|
</div>
|
||||||
</NavLink>
|
|
||||||
))}
|
<div className={moreOpen ? 'nav-secondary nav-secondary--open' : 'nav-secondary'}>
|
||||||
|
{SECONDARY.map((item) => (
|
||||||
|
<NavLink
|
||||||
|
key={item.to}
|
||||||
|
to={item.to}
|
||||||
|
className={linkClass}
|
||||||
|
onClick={() => setMoreOpen(false)}
|
||||||
|
>
|
||||||
|
<span className="nav-icon" aria-hidden="true">{item.icon}</span>
|
||||||
|
<span className="nav-label">{item.label}</span>
|
||||||
|
</NavLink>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<main className="app-main">
|
<main className="app-main">
|
||||||
|
|||||||
67
gerbil-manager-web/src/components/appShell.css
Normal file
67
gerbil-manager-web/src/components/appShell.css
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
/*
|
||||||
|
* AppShell navigation overflow ("Mehr") — co-located with the component to keep
|
||||||
|
* index.css contention-free (shared-file rule). Overrides the base .app-nav
|
||||||
|
* layout from index.css; loaded after it, so these rules win.
|
||||||
|
*
|
||||||
|
* Phone: bottom bar shows 4 primary tabs + a "Mehr" button; the secondary items
|
||||||
|
* live in a sheet that opens ABOVE the bar. Laptop (>=768px): full sidebar, all
|
||||||
|
* items inline, no "Mehr" button.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* --- Phone (base) --- */
|
||||||
|
.app-nav {
|
||||||
|
/* column-reverse so .nav-secondary (2nd in DOM) renders ABOVE the bar */
|
||||||
|
flex-direction: column-reverse;
|
||||||
|
justify-content: flex-start;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-primary {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-around;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-more-btn {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
font: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-secondary {
|
||||||
|
display: none;
|
||||||
|
flex-direction: column;
|
||||||
|
background: var(--color-surface);
|
||||||
|
border-bottom: 1px solid var(--color-border);
|
||||||
|
box-shadow: 0 -4px 12px rgba(0, 0, 0, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-secondary--open {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Laptop / Tablet (>=768px): full sidebar, no overflow --- */
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
.app-nav {
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 1rem 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-primary {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-more-btn {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-secondary {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.25rem;
|
||||||
|
background: none;
|
||||||
|
border-bottom: none;
|
||||||
|
box-shadow: none;
|
||||||
|
margin-top: 0.25rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,6 +19,8 @@ export const de = {
|
|||||||
contacts: 'Kontakte',
|
contacts: 'Kontakte',
|
||||||
// FEAT-7 (Kelly): Statistik
|
// FEAT-7 (Kelly): Statistik
|
||||||
statistics: 'Statistik',
|
statistics: 'Statistik',
|
||||||
|
// FEAT-7 (Kevin): „Mehr“-Overflow auf dem Smartphone
|
||||||
|
more: 'Mehr',
|
||||||
openMenu: 'Menü öffnen',
|
openMenu: 'Menü öffnen',
|
||||||
closeMenu: 'Menü schließen',
|
closeMenu: 'Menü schließen',
|
||||||
mainNavigation: 'Hauptnavigation',
|
mainNavigation: 'Hauptnavigation',
|
||||||
|
|||||||
Reference in New Issue
Block a user