diff --git a/web_interface/static/v3/app.css b/web_interface/static/v3/app.css index a1342955..28262df1 100644 --- a/web_interface/static/v3/app.css +++ b/web_interface/static/v3/app.css @@ -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; diff --git a/web_interface/static/v3/app.js b/web_interface/static/v3/app.js index 01ed814a..a3ecfa68 100644 --- a/web_interface/static/v3/app.js +++ b/web_interface/static/v3/app.js @@ -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); +}); diff --git a/web_interface/static/v3/js/settings-search.js b/web_interface/static/v3/js/settings-search.js index 5c794a00..7619c2a0 100644 --- a/web_interface/static/v3/js/settings-search.js +++ b/web_interface/static/v3/js/settings-search.js @@ -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; } diff --git a/web_interface/templates/v3/base.html b/web_interface/templates/v3/base.html index fda97e28..adb8b732 100644 --- a/web_interface/templates/v3/base.html +++ b/web_interface/templates/v3/base.html @@ -552,6 +552,7 @@ window.app = function() { return { activeTab: isAPMode ? 'wifi' : 'overview', + mobileNavOpen: false, installedPlugins: [], init() { @@ -874,6 +875,16 @@
+ +

LED Matrix Control - v3 @@ -881,7 +892,7 @@

-
+