Mobile nav: replace bottom icon bar with burger menu for consistency

Matches the slide-in sidebar + top-bar burger pattern used by other apps (maintenance, cashup) instead of the bottom tab bar.
This commit is contained in:
jtricerolph 2026-07-23 17:43:13 +00:00
parent 69b4c50e14
commit baba818693
2 changed files with 61 additions and 45 deletions

View file

@ -1,5 +1,5 @@
import { useState } from 'react' import { useState } from 'react'
import { DollarSign, CalendarDays, TrendingUp, BarChart3, Wallet, Settings } from 'lucide-react' import { DollarSign, CalendarDays, TrendingUp, BarChart3, Wallet, Settings, Menu } from 'lucide-react'
import AuthGate, { useAuth } from './components/AuthGate' import AuthGate, { useAuth } from './components/AuthGate'
import { UpdateBanner } from './components/UpdateBanner' import { UpdateBanner } from './components/UpdateBanner'
import { useVersionCheck } from './hooks/useVersionCheck' import { useVersionCheck } from './hooks/useVersionCheck'
@ -25,13 +25,19 @@ const NAV: { id: Page; label: string; icon: React.ElementType; cap?: string }[]
function Shell() { function Shell() {
const { user } = useAuth() const { user } = useAuth()
const [page, setPage] = useState<Page>('weekly') const [page, setPage] = useState<Page>('weekly')
const [menuOpen, setMenuOpen] = useState(false)
const hotelName = import.meta.env.VITE_HOTEL_NAME || 'Hotel' const hotelName = import.meta.env.VITE_HOTEL_NAME || 'Hotel'
const visibleNav = NAV.filter(n => !n.cap || can(user, n.cap)) const visibleNav = NAV.filter(n => !n.cap || can(user, n.cap))
function selectPage(id: Page) {
setPage(id)
setMenuOpen(false)
}
return ( return (
<div className="app-shell"> <div className={`app-shell${menuOpen ? ' menu-open' : ''}`}>
<nav className="sidebar"> <nav className="sidebar">
<div className="sidebar-logo"> <div className="sidebar-logo">
<DollarSign size={16} strokeWidth={1.75} /> <DollarSign size={16} strokeWidth={1.75} />
@ -42,7 +48,7 @@ function Shell() {
<div <div
key={n.id} key={n.id}
className={`nav-item${page === n.id ? ' active' : ''}`} className={`nav-item${page === n.id ? ' active' : ''}`}
onClick={() => setPage(n.id)} onClick={() => selectPage(n.id)}
> >
<n.icon size={16} strokeWidth={1.75} /> <n.icon size={16} strokeWidth={1.75} />
{n.label} {n.label}
@ -54,6 +60,16 @@ function Shell() {
</div> </div>
</nav> </nav>
{menuOpen && <div className="menu-backdrop" onClick={() => setMenuOpen(false)} />}
<header className="top-bar">
<button className="top-bar-burger" onClick={() => setMenuOpen(o => !o)}>
<Menu size={20} strokeWidth={1.75} />
</button>
<DollarSign size={18} strokeWidth={1.75} color="var(--gold)" />
<span className="top-bar-title">Wage Costs</span>
</header>
<main className="content"> <main className="content">
{page === 'weekly' && <Weekly />} {page === 'weekly' && <Weekly />}
{page === 'monthly' && <Monthly />} {page === 'monthly' && <Monthly />}
@ -62,19 +78,6 @@ function Shell() {
{page === 'budgets' && <Budgets />} {page === 'budgets' && <Budgets />}
{page === 'settings' && <SettingsPage />} {page === 'settings' && <SettingsPage />}
</main> </main>
<nav className="bottom-nav">
{visibleNav.map(n => (
<div
key={n.id}
className={`bottom-nav-item${page === n.id ? ' active' : ''}`}
onClick={() => setPage(n.id)}
>
<n.icon size={18} strokeWidth={1.75} />
<span>{n.label}</span>
</div>
))}
</nav>
</div> </div>
) )
} }

View file

@ -402,46 +402,59 @@ input[type="text"]:focus {
font-size: 14px; font-size: 14px;
} }
/* ── Bottom nav (mobile only) ───────────────────────────────────── */ /* ── Top bar + burger (mobile only) ────────────────────────────── */
.bottom-nav { .top-bar {
display: none; display: none;
position: fixed; height: var(--topbar-h);
bottom: 0; left: 0; right: 0;
background: var(--navy); background: var(--navy);
border-top: 1px solid rgba(255,255,255,0.1); color: #fff;
z-index: 200;
height: 58px;
justify-content: space-around;
align-items: stretch;
}
.bottom-nav-item {
display: flex;
flex-direction: column;
align-items: center; align-items: center;
justify-content: center; padding: 0 12px;
gap: 3px; gap: 10px;
color: rgba(255,255,255,0.45); flex-shrink: 0;
font-size: 10px;
cursor: pointer;
flex: 1;
min-width: 0;
padding: 4px 2px;
transition: color 0.15s;
user-select: none;
} }
.bottom-nav-item.active { color: var(--gold); } .top-bar-title {
.bottom-nav-item:hover { color: rgba(255,255,255,0.85); } flex: 1;
font-size: 15px;
font-weight: 600;
color: var(--gold);
}
.top-bar-burger {
background: none;
border: none;
color: #fff;
cursor: pointer;
display: flex;
align-items: center;
padding: 4px;
flex-shrink: 0;
}
.menu-backdrop {
position: fixed;
inset: 0;
background: rgba(0,0,0,0.5);
z-index: 199;
}
/* ── Mobile breakpoint ──────────────────────────────────────────── */ /* ── Mobile breakpoint ──────────────────────────────────────────── */
@media (max-width: 640px) { @media (max-width: 640px) {
.sidebar { display: none; } .app-shell { flex-direction: column; }
.bottom-nav { display: flex; } .top-bar { display: flex; }
.sidebar {
position: fixed;
top: 0; left: 0; bottom: 0;
z-index: 200;
transform: translateX(calc(-1 * var(--sidebar-w)));
transition: transform 0.25s ease;
}
.app-shell.menu-open .sidebar { transform: translateX(0); }
.content { .content {
padding: 12px; padding: 12px;
padding-bottom: 68px; /* clear bottom nav */
} }
.card { .card {