feat(web): mobile navigation drawer + responsive CSS gap fixes

Phones previously got the desktop layout squeezed: ~12 system tabs plus one
tab per installed plugin wrapped into many rows of small pill buttons, and
the header's settings-search and system-stats widgets were dropped entirely
(hidden below their breakpoints, never relocated).

- Off-canvas nav drawer below md: the existing nav markup (system tab row +
  #plugin-tabs-row, including dynamically injected plugin tabs) is wrapped in
  a #site-nav container that CSS repositions into a slide-in drawer on small
  screens. Same DOM nodes, same @click handlers, nothing duplicated. Tabs
  become full-width rows with 44px+ touch targets. A hamburger button
  (md:hidden) in the header and a backdrop toggle the new mobileNavOpen
  Alpine state (added to both app() definitions, mirroring activeTab).
  Clicking any tab, a search result, or the backdrop closes the drawer.
  At md+ hard CSS guards make all drawer styles inert - desktop renders
  exactly as before.
- Header widgets relocated, not hidden: placeHeaderWidgets() in app.js moves
  the #settings-search-wrap and #system-stats nodes (same elements, listeners
  intact - both are looked up by id from SSE/search code, so they must never
  be duplicated) into the drawer below md and back into the header above it,
  via a matchMedia listener.
- Fixed 13 breakpoint utility classes that templates referenced but app.css
  never defined (sm:block, sm:grid-cols-2, sm:text-sm, md:block, md:w-auto,
  lg:block, lg:flex, lg:w-64, xl:grid-cols-2/3, 2xl:grid-cols-2/3/4). This
  was a live bug: 'hidden sm:block' on the search box and 'hidden lg:flex'
  on the stats meant BOTH were invisible at every screen width. Audit method
  (repeatable): diff classes used in templates vs defined in app.css.
- Mobile modal sizing: one global rule caps .modal-content at 95vw/90vh with
  internal scroll below 640px - covers every modal without per-template
  changes.
- Horizontal-scroll affordance: pure-CSS edge-fade shadows on
  .overflow-x-auto containers (scrolling-shadows technique), plus larger
  in-table touch targets below md.

Validation: breakpoint used-vs-defined audit now returns zero gaps; Jinja
parse of base.html passes; all changes to desktop behavior are additive
(new utilities) or scoped inside max-width media queries.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KEZK1P1Q1fu5pcuVrkrCFZ
This commit is contained in:
ChuckBuilds
2026-07-16 10:20:06 -04:00
co-authored by Claude Sonnet 5
parent d2b49c4ff2
commit 1ae3b7fa24
4 changed files with 208 additions and 3 deletions
+29 -2
View File
@@ -552,6 +552,7 @@
window.app = function() {
return {
activeTab: isAPMode ? 'wifi' : 'overview',
mobileNavOpen: false,
installedPlugins: [],
init() {
@@ -874,6 +875,16 @@
<div class="mx-auto px-4 sm:px-6 lg:px-8 xl:px-12 2xl:px-16" style="max-width: 100%;">
<div class="flex justify-between items-center h-16">
<div class="flex items-center">
<!-- Mobile nav toggle (hamburger) — hidden at md and up -->
<button type="button"
class="theme-toggle-btn p-2 rounded-md mr-2 md:hidden"
@click="mobileNavOpen = !mobileNavOpen"
:aria-expanded="mobileNavOpen ? 'true' : 'false'"
aria-controls="site-nav"
aria-label="Open navigation menu"
title="Menu">
<i class="fas fa-bars" aria-hidden="true"></i>
</button>
<h1 class="text-xl font-bold text-gray-900">
<i class="fas fa-tv text-blue-600 mr-2"></i>
LED Matrix Control - v3
@@ -881,7 +892,7 @@
</div>
<!-- Connection status and theme toggle -->
<div class="flex items-center space-x-4">
<div id="header-widgets" class="flex items-center space-x-4">
<!-- Global settings search -->
<div class="relative hidden sm:block" id="settings-search-wrap">
<input id="settings-search"
@@ -919,7 +930,7 @@
</div>
<!-- System stats (populated via SSE) -->
<div class="hidden lg:flex items-center space-x-4 text-sm text-gray-600 xl:space-x-6 2xl:space-x-8">
<div id="system-stats" class="hidden lg:flex items-center space-x-4 text-sm text-gray-600 xl:space-x-6 2xl:space-x-8">
<span id="cpu-stat" class="flex items-center space-x-1">
<i class="fas fa-microchip"></i>
<span>--%</span>
@@ -993,6 +1004,19 @@
<!-- Main content -->
<main class="mx-auto px-4 sm:px-6 lg:px-8 xl:px-12 2xl:px-16 py-8" style="max-width: 100%;">
<!-- Backdrop shown behind the mobile nav drawer (no-op at md and up) -->
<div class="nav-backdrop" x-show="mobileNavOpen" x-transition.opacity
@click="mobileNavOpen = false" aria-hidden="true" style="display: none;"></div>
<!-- Navigation: static tab rows at md and up; off-canvas drawer below md.
The drawer reuses the exact same tab buttons/handlers — CSS repositions
this container, nothing is duplicated. Clicking any tab closes the drawer. -->
<div id="site-nav" class="site-nav" :class="mobileNavOpen ? 'open' : ''"
@click="if ($event.target.closest('.nav-tab')) mobileNavOpen = false">
<!-- Below md, the header's settings-search and system-stats widgets are
moved (not copied) into this slot by placeHeaderWidgets() in app.js -->
<div id="drawer-widgets"></div>
<!-- Navigation tabs -->
<nav class="mb-8">
<!-- First row - System tabs -->
@@ -1073,6 +1097,8 @@
</nav>
</div>
</nav>
</div> <!-- /#site-nav -->
<!-- Tab content -->
<div id="tab-content" class="space-y-6">
@@ -1695,6 +1721,7 @@
window.location.hostname.startsWith('192.168.4.');
return isAPMode ? 'wifi' : 'overview';
})(),
mobileNavOpen: false,
installedPlugins: [],
init() {