mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-08-01 16:58:06 +00:00
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:
co-authored by
Claude Sonnet 5
parent
d2b49c4ff2
commit
1ae3b7fa24
@@ -413,6 +413,9 @@ a, button, input, select, textarea {
|
||||
/* Responsive breakpoints */
|
||||
@media (min-width: 640px) {
|
||||
.sm\:px-6 { padding-left: 1.5rem; padding-right: 1.5rem; }
|
||||
.sm\:block { display: block; }
|
||||
.sm\:grid-cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
.sm\:text-sm { font-size: 0.875rem; line-height: 1.25rem; }
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
@@ -421,6 +424,8 @@ a, button, input, select, textarea {
|
||||
.md\:grid-cols-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
|
||||
.md\:flex { display: flex; }
|
||||
.md\:hidden { display: none; }
|
||||
.md\:block { display: block; }
|
||||
.md\:w-auto { width: auto; }
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
@@ -431,9 +436,14 @@ a, button, input, select, textarea {
|
||||
.lg\:px-8 { padding-left: 2rem; padding-right: 2rem; }
|
||||
.lg\:gap-x-3 { column-gap: 0.75rem; }
|
||||
.lg\:gap-x-6 { column-gap: 1.5rem; }
|
||||
.lg\:block { display: block; }
|
||||
.lg\:flex { display: flex; }
|
||||
.lg\:w-64 { width: 16rem; }
|
||||
}
|
||||
|
||||
@media (min-width: 1280px) {
|
||||
.xl\:grid-cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
.xl\:grid-cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
||||
.xl\:grid-cols-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
|
||||
.xl\:grid-cols-5 { grid-template-columns: repeat(5, minmax(0, 1fr)); }
|
||||
.xl\:grid-cols-6 { grid-template-columns: repeat(6, minmax(0, 1fr)); }
|
||||
@@ -446,6 +456,9 @@ a, button, input, select, textarea {
|
||||
}
|
||||
|
||||
@media (min-width: 1536px) {
|
||||
.2xl\:grid-cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
.2xl\:grid-cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
||||
.2xl\:grid-cols-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
|
||||
.2xl\:grid-cols-5 { grid-template-columns: repeat(5, minmax(0, 1fr)); }
|
||||
.2xl\:grid-cols-6 { grid-template-columns: repeat(6, minmax(0, 1fr)); }
|
||||
.2xl\:grid-cols-7 { grid-template-columns: repeat(7, minmax(0, 1fr)); }
|
||||
@@ -456,6 +469,129 @@ a, button, input, select, textarea {
|
||||
.2xl\:space-x-8 > * + * { margin-left: 2rem; }
|
||||
}
|
||||
|
||||
/* ===== Mobile navigation drawer =====
|
||||
Below md the #site-nav wrapper becomes an off-canvas drawer; at md and up
|
||||
none of these rules apply and the nav renders exactly as before. */
|
||||
@media (max-width: 767.98px) {
|
||||
.site-nav {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
z-index: 60;
|
||||
width: min(85vw, 320px);
|
||||
background-color: var(--color-surface);
|
||||
border-right: 1px solid var(--color-border);
|
||||
transform: translateX(-100%);
|
||||
transition: transform 0.25s ease;
|
||||
overflow-y: auto;
|
||||
padding: 1rem;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
.site-nav.open {
|
||||
transform: translateX(0);
|
||||
box-shadow: 0 0 24px rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
/* Tabs become full-width rows with >=44px touch targets */
|
||||
.site-nav .nav-tab {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
text-align: left;
|
||||
padding: 0.75rem 1rem;
|
||||
min-height: 44px;
|
||||
}
|
||||
.site-nav nav.-mb-px {
|
||||
display: block;
|
||||
}
|
||||
.nav-backdrop {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 55;
|
||||
background-color: rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
/* Header widgets relocated into the drawer (see placeHeaderWidgets in
|
||||
app.js). The originals carry `hidden`/breakpoint classes tuned for the
|
||||
header, so re-enable them explicitly in the drawer context. */
|
||||
#drawer-widgets #settings-search-wrap {
|
||||
display: block !important;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
#drawer-widgets #settings-search-wrap input {
|
||||
width: 100%;
|
||||
}
|
||||
#drawer-widgets #settings-search-results {
|
||||
position: static;
|
||||
width: 100%;
|
||||
max-height: 50vh;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
#drawer-widgets #system-stats {
|
||||
display: flex !important;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
/* Larger touch targets inside horizontally scrolling tables */
|
||||
.overflow-x-auto table button {
|
||||
min-width: 44px;
|
||||
min-height: 44px;
|
||||
}
|
||||
.overflow-x-auto table input:not([type="checkbox"]),
|
||||
.overflow-x-auto table select {
|
||||
min-height: 40px;
|
||||
}
|
||||
.overflow-x-auto table input[type="checkbox"] {
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
/* Hard guards: even if mobileNavOpen was left true when the viewport
|
||||
crossed the breakpoint, the drawer/backdrop must render as plain
|
||||
in-flow nav on desktop. */
|
||||
.site-nav {
|
||||
position: static;
|
||||
transform: none;
|
||||
width: auto;
|
||||
padding: 0;
|
||||
border-right: none;
|
||||
box-shadow: none;
|
||||
background-color: transparent;
|
||||
overflow-y: visible;
|
||||
}
|
||||
.nav-backdrop {
|
||||
display: none !important;
|
||||
}
|
||||
#drawer-widgets {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Mobile modal sizing: every .modal-content dialog fits the viewport with
|
||||
internal scrolling instead of overflowing it. */
|
||||
@media (max-width: 640px) {
|
||||
.modal-content {
|
||||
width: 95vw !important;
|
||||
max-width: 95vw !important;
|
||||
max-height: 90vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
|
||||
/* Edge-fade hint that a container scrolls horizontally (pure CSS,
|
||||
Lea Verou scrolling-shadows technique — backgrounds sit behind content). */
|
||||
.overflow-x-auto {
|
||||
background:
|
||||
linear-gradient(90deg, var(--color-surface) 30%, rgba(255, 255, 255, 0)) left / 24px 100%,
|
||||
linear-gradient(270deg, var(--color-surface) 30%, rgba(255, 255, 255, 0)) right / 24px 100%,
|
||||
radial-gradient(farthest-side at 0 50%, rgba(0, 0, 0, 0.18), rgba(0, 0, 0, 0)) left / 12px 100%,
|
||||
radial-gradient(farthest-side at 100% 50%, rgba(0, 0, 0, 0.18), rgba(0, 0, 0, 0)) right / 12px 100%;
|
||||
background-repeat: no-repeat;
|
||||
background-attachment: local, local, scroll, scroll;
|
||||
}
|
||||
|
||||
/* HTMX loading states */
|
||||
.htmx-request .loading {
|
||||
display: inline-block;
|
||||
|
||||
@@ -273,3 +273,38 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
}, 100);
|
||||
});
|
||||
});
|
||||
|
||||
// ===== Mobile nav: header-widget relocation =====
|
||||
// Below the md breakpoint the settings-search box and system-stats block are
|
||||
// MOVED (same DOM nodes, listeners intact) from the header into the nav
|
||||
// drawer's #drawer-widgets slot; at md and up they move back. Single-instance
|
||||
// constraint: settings-search.js and the SSE stats updater both look these
|
||||
// elements up by id, so they must never be duplicated.
|
||||
window.placeHeaderWidgets = function() {
|
||||
const drawer = document.getElementById('drawer-widgets');
|
||||
const header = document.getElementById('header-widgets');
|
||||
const search = document.getElementById('settings-search-wrap');
|
||||
const stats = document.getElementById('system-stats');
|
||||
if (!drawer || !header) return;
|
||||
|
||||
const desktop = window.matchMedia('(min-width: 768px)').matches;
|
||||
if (desktop) {
|
||||
// Restore original header order: search before the theme toggle,
|
||||
// stats as the last item.
|
||||
const themeToggle = document.getElementById('theme-toggle');
|
||||
if (search && search.parentElement !== header) {
|
||||
header.insertBefore(search, themeToggle || null);
|
||||
}
|
||||
if (stats && stats.parentElement !== header) {
|
||||
header.appendChild(stats);
|
||||
}
|
||||
} else {
|
||||
if (search && search.parentElement !== drawer) drawer.appendChild(search);
|
||||
if (stats && stats.parentElement !== drawer) drawer.appendChild(stats);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
window.placeHeaderWidgets();
|
||||
window.matchMedia('(min-width: 768px)').addEventListener('change', window.placeHeaderWidgets);
|
||||
});
|
||||
|
||||
@@ -173,7 +173,14 @@
|
||||
|
||||
function setActiveTab(tab) {
|
||||
var data = getAppData();
|
||||
if (data) { data.activeTab = tab; return true; }
|
||||
if (data) {
|
||||
data.activeTab = tab;
|
||||
// Navigating from a search result should also dismiss the mobile
|
||||
// nav drawer (harmless no-op on desktop, where the drawer CSS
|
||||
// doesn't apply).
|
||||
if ('mobileNavOpen' in data) data.mobileNavOpen = false;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user