From d1cb84b5968bb209c49d77165e6eaf38912f7c1f Mon Sep 17 00:00:00 2001 From: Kpa-clawbot Date: Tue, 31 Mar 2026 23:38:54 -0700 Subject: [PATCH] feat: Priority+ nav pattern for tablet viewports (768-1023px) (#345) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Priority+ Navigation Pattern for Tablet Viewports Phase 2 of responsive nav improvements for #322. ### What this does On **tablet viewports (768-1023px)**, implements the [Priority+ navigation pattern](https://css-tricks.com/the-priority-plus-navigation-pattern/): - **5 high-priority tabs** shown inline: Home, Nodes, Packets, Map, Live - **6 low-priority tabs** collapse into a "More ▾" dropdown: Channels, Traces, Observers, Analytics, Perf, Lab - The "More" button highlights when a low-priority page is active **Desktop (>=1024px)** and **mobile (<768px)** behavior is unchanged. ### Changes | File | Change | |------|--------| | `public/index.html` | Added `data-priority="high"` to 5 primary nav links; added More button + dropdown menu | | `public/style.css` | Split ≤1023px hamburger query into tablet Priority+ (768-1023px) and mobile hamburger (<768px); added More dropdown styles | | `public/app.js` | Added `closeMoreMenu()`, More button toggle, outside-click/Escape close, active state on More button | | Cache busters | Bumped in same commit | ### Accessibility - `aria-haspopup="true"` and `aria-expanded` on More button - `role="menu"` / `role="menuitem"` on dropdown - Focus moves to first item on open - Escape key closes dropdown ### Testing - All 308 existing tests pass (217 frontend-helpers + 62 packet-filter + 29 aging) - No new dependencies added - No build step changes ### Breakpoint summary | Viewport | Behavior | |----------|----------| | >= 1024px | Full horizontal nav (unchanged) | | 768-1023px | Priority+ pattern: 5 tabs + More dropdown **← NEW** | | < 768px | Hamburger drawer with all items (unchanged) | --------- Co-authored-by: Kpa-clawbot <259247574+Kpa-clawbot@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- public/app.js | 50 ++++++++++++++++++++++++++++++++- public/index.html | 70 +++++++++++++++++++++++++---------------------- public/style.css | 38 ++++++++++++++++++++++--- 3 files changed, 120 insertions(+), 38 deletions(-) diff --git a/public/app.js b/public/app.js index d4363cde..6e5749fa 100644 --- a/public/app.js +++ b/public/app.js @@ -412,6 +412,14 @@ function closeNav() { document.body.classList.remove('nav-open'); var btn = document.getElementById('hamburger'); if (btn) btn.setAttribute('aria-expanded', 'false'); + closeMoreMenu(); +} + +function closeMoreMenu() { + var menu = document.getElementById('navMoreMenu'); + var btn = document.getElementById('navMoreBtn'); + if (menu) menu.classList.remove('open'); + if (btn) btn.setAttribute('aria-expanded', 'false'); } function navigate() { @@ -448,6 +456,13 @@ function navigate() { document.querySelectorAll('.nav-link[data-route]').forEach(el => { el.classList.toggle('active', el.dataset.route === basePage); }); + // Update "More" button to show active state if a low-priority page is selected + var moreBtn = document.getElementById('navMoreBtn'); + if (moreBtn) { + var moreMenu = document.getElementById('navMoreMenu'); + var hasActiveMore = moreMenu && moreMenu.querySelector('.nav-link.active'); + moreBtn.classList.toggle('active', !!hasActiveMore); + } if (currentPage && pages[currentPage]?.destroy) { pages[currentPage].destroy(); @@ -551,8 +566,36 @@ window.addEventListener('DOMContentLoaded', () => { navLinks.querySelectorAll('.nav-link').forEach(link => { link.addEventListener('click', closeNav); }); + + // --- "More" dropdown (tablet Priority+ nav) --- + const navMoreBtn = document.getElementById('navMoreBtn'); + const navMoreMenu = document.getElementById('navMoreMenu'); + if (navMoreBtn && navMoreMenu) { + // Build More menu dynamically from non-priority nav links (DRY) + navMoreMenu.innerHTML = ''; + document.querySelectorAll('.nav-links a:not([data-priority="high"])').forEach(function(link) { + var clone = link.cloneNode(true); + clone.setAttribute('role', 'menuitem'); + clone.addEventListener('click', closeMoreMenu); + navMoreMenu.appendChild(clone); + }); + navMoreBtn.addEventListener('click', (e) => { + e.stopPropagation(); + const opening = !navMoreMenu.classList.contains('open'); + navMoreMenu.classList.toggle('open'); + navMoreBtn.setAttribute('aria-expanded', String(opening)); + if (opening) { + var firstLink = navMoreMenu.querySelector('.nav-link'); + if (firstLink) firstLink.focus(); + } + }); + } + document.addEventListener('keydown', (e) => { - if (e.key === 'Escape' && navLinks.classList.contains('open')) closeNav(); + if (e.key === 'Escape') { + if (navMoreMenu && navMoreMenu.classList.contains('open')) closeMoreMenu(); + if (navLinks.classList.contains('open')) closeNav(); + } }); document.addEventListener('click', (e) => { if (navLinks.classList.contains('open') && @@ -560,6 +603,11 @@ window.addEventListener('DOMContentLoaded', () => { !hamburger.contains(e.target)) { closeNav(); } + if (navMoreMenu && navMoreMenu.classList.contains('open') && + !navMoreMenu.contains(e.target) && + !navMoreBtn.contains(e.target)) { + closeMoreMenu(); + } }); // --- Favorites dropdown --- diff --git a/public/index.html b/public/index.html index 12b226b6..ff334314 100644 --- a/public/index.html +++ b/public/index.html @@ -22,9 +22,9 @@ - - - + + + @@ -44,18 +44,22 @@
- Home - Packets - Map - 🔴 Live + Home + Packets + Map + 🔴 Live Channels - Nodes + Nodes Traces Observers Analytics ⚡ Perf 🎵 Lab
+
+ +
+
@@ -81,30 +85,30 @@
- - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/style.css b/public/style.css index 0059ef26..426c2e9a 100644 --- a/public/style.css +++ b/public/style.css @@ -5,6 +5,7 @@ --nav-bg2: #1a1a2e; --nav-text: #ffffff; --nav-text-muted: #cbd5e1; + --nav-active-bg: rgba(74, 158, 255, 0.15); --accent: #4a9eff; --geo-filter-color: #3b82f6; --status-green: #22c55e; @@ -128,7 +129,7 @@ a:focus-visible, button:focus-visible, input:focus-visible, select:focus-visible .nav-link.active { color: var(--nav-text); border-bottom-color: transparent; - background: rgba(74, 158, 255, 0.15); + background: var(--nav-active-bg); border-radius: 6px; margin: 4px 0; padding: 10px 12px; @@ -837,6 +838,22 @@ button.ch-item.selected { background: var(--selected-bg); } /* === Hamburger (hidden on desktop) === */ .hamburger { display: none; } +/* "More" button (hidden on desktop) */ +.nav-more-wrap { display: none; position: relative; } +.nav-more-btn { display: inline-flex; } +.nav-more-menu { + display: none; position: absolute; top: calc(var(--top-nav-h, 52px) - 4px); right: 0; + background: var(--nav-bg); border: 1px solid var(--border); border-radius: 8px; + box-shadow: 0 4px 12px rgba(0,0,0,0.15); flex-direction: column; + min-width: 160px; padding: 4px 0; z-index: 1200; +} +.nav-more-menu.open { display: flex; } +.nav-more-menu .nav-link { + padding: 10px 16px; border-bottom: none; border-radius: 0; margin: 0; + white-space: nowrap; +} +.nav-more-menu .nav-link:hover { background: var(--nav-bg2); color: var(--nav-text); } +.nav-more-menu .nav-link.active { background: var(--nav-active-bg); } /* Ensure nav stays above Leaflet map */ .nav-links.open { z-index: 1100; } #map-wrap .leaflet-container { z-index: 1; } @@ -851,18 +868,31 @@ button.ch-item.selected { background: var(--selected-bg); } .map-controls { width: 180px; font-size: 12px; } } -/* === Responsive — Hamburger nav (≤1023px) === */ -@media (max-width: 1023px) { +/* === Responsive — Tablet Priority+ nav (768–1023px) === */ +@media (min-width: 768px) and (max-width: 1023px) { + .nav-links { display: flex !important; flex-direction: row; gap: 2px; } + .nav-links a:not([data-priority="high"]) { display: none; } + .nav-more-wrap { display: flex; align-items: center; } + .hamburger { display: none; } + .nav-link { padding: 14px 8px; font-size: 13px; } + .nav-links a[data-priority="high"] { order: -1; } + .nav-link.active { background: var(--nav-active-bg); border-radius: 6px; margin: 4px 0; padding: 10px 8px; } +} + +/* === Responsive — Hamburger nav (<768px) === */ +@media (max-width: 767px) { .hamburger { display: inline-flex; } + .nav-more-wrap { display: none !important; } .nav-links { display: none; position: absolute; top: 52px; left: 0; right: 0; background: var(--nav-bg); flex-direction: column; padding: 8px 0; box-shadow: 0 8px 24px rgba(0,0,0,.4); z-index: 1100; max-height: calc(100dvh - 52px); overflow-y: auto; } + .nav-links a:not([data-priority="high"]) { display: flex; } .nav-links.open { display: flex; } .nav-link { padding: 12px 20px; border-bottom: none; } - .nav-link.active { background: rgba(74,158,255,0.15); border-radius: 0; margin: 0; padding: 12px 20px; } + .nav-link.active { background: var(--nav-active-bg); border-radius: 0; margin: 0; padding: 12px 20px; } .nav-left { gap: 12px; } body.nav-open { overflow: hidden; } }