From 8d1c2e4eb57815ac3ac5969d489876da5dca1523 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 31 May 2026 03:30:45 +0000 Subject: [PATCH] feat: hamburger menu for mobile nav MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On screens ≤640px the tab row collapses; a hamburger button (Material Symbols menu/close icon) appears in the nav-right area. Tapping it opens a fixed full-width dropdown below the header with full-height tap targets and a left accent border on the active item. Tapping a tab or anywhere outside closes the menu. Desktop layout is unchanged — nav-tabs uses display:contents so the buttons remain direct flex children of nav. https://claude.ai/code/session_01JuRTR5Xjx8emQsyerBgGU7 --- static/app.js | 21 +++++++++++++++ static/index.html | 29 +++++++++++--------- static/style.css | 69 +++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 107 insertions(+), 12 deletions(-) diff --git a/static/app.js b/static/app.js index 9755b5a..0c0cb30 100644 --- a/static/app.js +++ b/static/app.js @@ -93,6 +93,23 @@ async function startApp() { document.querySelector('[data-view="bar"]').classList.add('hidden'); } + // Hamburger menu toggle + const hamburger = document.getElementById('hamburger'); + const navTabs = document.getElementById('navTabs'); + hamburger.addEventListener('click', e => { + e.stopPropagation(); + const open = navTabs.classList.toggle('open'); + hamburger.setAttribute('aria-expanded', open); + hamburger.querySelector('.material-symbols-outlined').textContent = open ? 'close' : 'menu'; + }); + document.addEventListener('click', e => { + if (navTabs.classList.contains('open') && !navTabs.contains(e.target)) { + navTabs.classList.remove('open'); + hamburger.setAttribute('aria-expanded', 'false'); + hamburger.querySelector('.material-symbols-outlined').textContent = 'menu'; + } + }); + // Nav tab switching document.querySelectorAll('.nav-btn').forEach(btn => { btn.addEventListener('click', () => { @@ -101,6 +118,10 @@ async function startApp() { document.querySelectorAll('.view').forEach(v => v.classList.add('hidden')); document.getElementById('view-' + btn.dataset.view).classList.remove('hidden'); if (btn.dataset.view === 'admin') loadAdminView(); + // Close mobile menu after selection + navTabs.classList.remove('open'); + hamburger.setAttribute('aria-expanded', 'false'); + hamburger.querySelector('.material-symbols-outlined').textContent = 'menu'; }); }); diff --git a/static/index.html b/static/index.html index cd88a73..b3a5248 100644 --- a/static/index.html +++ b/static/index.html @@ -32,20 +32,25 @@ diff --git a/static/style.css b/static/style.css index 7fc1e9f..03c0934 100644 --- a/static/style.css +++ b/static/style.css @@ -43,6 +43,9 @@ nav { z-index: 100; } +/* On desktop the wrapper is invisible — buttons behave as direct nav children */ +.nav-tabs { display: contents; } + .brand { font-size: 1rem; font-weight: 600; @@ -504,3 +507,69 @@ select { font-size: 1rem; padding: 10px 0; } + +/* ---- Hamburger button (hidden on desktop) ---- */ +.hamburger { + display: none; + align-items: center; + justify-content: center; + background: transparent; + border: 1px solid rgba(240,246,252,.3); + border-radius: var(--radius); + color: var(--nav-text); + width: 34px; + height: 30px; + cursor: pointer; + padding: 0; + transition: background .1s; +} +.hamburger:hover { background: rgba(177,186,196,.15); } +.hamburger .material-symbols-outlined { font-size: 20px; line-height: 1; } + +/* ---- Mobile nav ---- */ +@media (max-width: 640px) { + /* Hide desktop tabs, show hamburger */ + .nav-tabs { + display: none; + position: fixed; + top: 48px; + left: 0; + right: 0; + background: var(--nav-bg); + flex-direction: column; + padding: 6px 0 10px; + border-bottom: 1px solid rgba(240,246,252,.12); + box-shadow: 0 8px 16px rgba(0,0,0,.35); + z-index: 99; + } + .nav-tabs.open { display: flex; } + + .hamburger { display: inline-flex; } + + /* Hide username text to save space */ + .nav-user { display: none; } + + /* Mobile tab items: full-width rows with left accent border */ + .nav-btn, .nav-link { + align-self: auto; + width: 100%; + padding: 13px 20px; + font-size: .9375rem; + border-bottom: none; + border-left: 3px solid transparent; + border-radius: 0; + justify-content: flex-start; + gap: 12px; + } + .nav-btn:hover, .nav-link:hover { + border-bottom-color: transparent; + border-left-color: rgba(240,246,252,.35); + background: rgba(177,186,196,.12); + } + .nav-btn.active, .nav-link.active { + border-bottom-color: transparent; + border-left-color: #fff; + background: rgba(177,186,196,.12); + } + .nav-icon { font-size: 20px; } +}