v0.166.0: mobile nav off-canvas drawer + sidebar cleanup + versioned logo/favicon URLs

Mobile nav was broken — the <=768px block predated the v0.146.0 accordion and
flattened .nav-links into a horizontal overflow-x strip, clipping the accordion's
nested sub-lists. Replaced with a sticky top bar + off-canvas left drawer that
reuses the vertical sidebar (accordion untouched), plus a no-js static fallback.
Removed the sidebar customer-name span (kept on login). Added ?v={{.Version}}
cache-bust to logo/favicon URLs (Cloudflare 4h edge-cache; 0.126.1 failure mode).

Part 4 (outlined-logo constant swap) gated out per §3a: live felhom.eu main still
serves a logo.svg with live <text>/font-family; constants unchanged.

5 new tests via the real layout/CSS render; nav_accordion invariants unchanged.
This commit is contained in:
2026-07-24 13:26:22 +02:00
parent 1fd070615d
commit bf44216e79
8 changed files with 361 additions and 19 deletions
@@ -31,4 +31,5 @@
<symbol id="i-chevron-down" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6" /></symbol>
<symbol id="i-play" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 5a2 2 0 0 1 3.008-1.728l11.997 6.998a2 2 0 0 1 .003 3.458l-12 7A2 2 0 0 1 5 19z" /></symbol>
<symbol id="i-pause" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="14" y="3" width="5" height="18" rx="1" /> <rect x="5" y="3" width="5" height="18" rx="1" /></symbol>
<symbol id="i-menu" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 12h16" /> <path d="M4 6h16" /> <path d="M4 18h16" /></symbol>
</svg>{{end}}
+36 -7
View File
@@ -1,17 +1,20 @@
{{define "layout_start"}}
<!DOCTYPE html>
<html lang="hu">
<html lang="hu" class="no-js">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{.Title}} — Felhom.eu</title>
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg">
{{/* ?v= cache-bust: Cloudflare edge-caches /static/* for 4h — an unversioned URL keeps
serving the PREVIOUS release's CSS after a deploy (0.126.1 live QA hit this). The
controller version busts it automatically on every release. */}}
serving the PREVIOUS release's asset after a deploy (0.126.1 live QA hit this on CSS;
the logo/favicon share the failure mode). The controller version busts it every release. */}}
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg?v={{.Version}}">
<link rel="stylesheet" href="/static/style.css?v={{.Version}}">
<meta name="csrf-token" content="{{.CSRFToken}}">
<script>
// Progressive-enhancement hook (v0.166.0): flip no-js → js before any CSS-dependent paint so the
// mobile off-canvas drawer engages only when the drawer JS below can actually open it.
document.documentElement.className=document.documentElement.className.replace('no-js','js');
function csrfHeaders(){var el=document.querySelector('meta[name="csrf-token"]');return el?{'X-CSRF-Token':el.content}:{};}
// Inline two-step confirm (drill F-11): the trigger swaps IN PLACE to "<question> Igen/Mégse".
// Never native confirm() — it is an OS-modal that blocks browser automation — and no overlay:
@@ -62,10 +65,16 @@
</head>
<body>
{{template "icon_sprite"}}
<nav class="sidebar">
<header class="mobile-topbar">
<a href="/" class="mobile-topbar-logo"><img src="/static/felhom-logo.svg?v={{.Version}}" alt="Felhom.eu"></a>
<button type="button" class="nav-burger" aria-expanded="false" aria-controls="sidebar" aria-label="Menü">
<svg class="ico"><use href="#i-menu"/></svg>
</button>
</header>
<div class="nav-backdrop" hidden></div>
<nav class="sidebar" id="sidebar">
<div class="sidebar-header">
<img src="/static/felhom-logo.svg" alt="Felhom.eu" class="sidebar-logo">
<span class="customer-name">{{.CustomerName}}</span>
<img src="/static/felhom-logo.svg?v={{.Version}}" alt="Felhom.eu" class="sidebar-logo">
</div>
<ul class="nav-links">
<li><a href="/launcher" class="{{if eq .Page "launcher"}}active{{end}}"><svg class="ico"><use href="#i-rocket"/></svg>Indítópult</a></li>
@@ -139,6 +148,26 @@
{{define "layout_end"}}
</main>
<script>
// Mobile off-canvas drawer (v0.166.0). Reuses the existing sidebar as the drawer; the accordion
// handler in the head script is untouched and keeps working inside it. Only wires up on ≤768px
// chrome — on desktop the burger/backdrop are display:none so these listeners never fire.
(function(){
var burger=document.querySelector('.nav-burger');
var sidebar=document.getElementById('sidebar');
var backdrop=document.querySelector('.nav-backdrop');
if(!burger||!sidebar||!backdrop)return;
function setOpen(open){
sidebar.classList.toggle('is-open',open);
document.body.classList.toggle('nav-open',open);
backdrop.hidden=!open;
burger.setAttribute('aria-expanded',open?'true':'false');
}
burger.addEventListener('click',function(){setOpen(!sidebar.classList.contains('is-open'));});
backdrop.addEventListener('click',function(){setOpen(false);});
document.addEventListener('keydown',function(e){
if(e.key==='Escape'&&sidebar.classList.contains('is-open'))setOpen(false);
});
})();
document.addEventListener('click', function(e) {
if (e.target.closest('a, button, .btn, input, select, textarea, .app-row-actions, .stack-detail-actions')) return;
var card = e.target.closest('[data-href]');
+1 -1
View File
@@ -9,7 +9,7 @@
</head>
<body class="login-body">
<div class="login-card">
<img src="/static/felhom-logo.svg" alt="Felhom.eu" class="login-logo">
<img src="/static/felhom-logo.svg?v={{.Version}}" alt="Felhom.eu" class="login-logo">
<h1 class="login-title">Otthoni <span class="title-accent">vezérlőpult</span></h1>
<p class="login-subtitle">{{.CustomerName}}</p>
{{if .Flash}}<div class="alert alert-info">{{.Flash}}</div>{{end}}
+72 -10
View File
@@ -100,12 +100,6 @@ body {
display: block;
margin: 0 auto 0.5rem;
}
.customer-name {
display: block;
font-size: .85rem;
color: var(--text-2);
margin-top: .25rem;
}
.nav-links {
list-style: none;
padding: 1rem 0;
@@ -142,6 +136,26 @@ body {
.version { color: var(--text-3); }
.logout-link { color: var(--text-3); text-decoration: none; transition: color 0.2s ease; }
.logout-link:hover { color: var(--blue-bright); }
/* Mobile chrome (v0.166.0) — hidden by default so desktop (>768px) stays pixel-identical; the
≤768px block below turns the top bar on and converts the sidebar into an off-canvas drawer. */
.mobile-topbar { display: none; }
.nav-backdrop { display: none; }
.nav-burger {
display: flex;
align-items: center;
padding: .375rem;
background: none;
border: none;
color: var(--text-2);
cursor: pointer;
transition: color 0.2s ease;
}
.nav-burger:hover { color: var(--blue-bright); }
.nav-burger:focus-visible {
outline: 2px solid var(--blue-bright);
outline-offset: -2px;
}
.nav-burger .ico { width: 24px; height: 24px; }
/* Main content */
.content {
@@ -2532,10 +2546,54 @@ a.stat-card:hover {
/* Responsive */
@media(max-width: 768px) {
.sidebar { width: 100%; height: auto; position: relative; border-right: none; border-bottom: 1px solid var(--line); }
.nav-links { display: flex; padding: 0; overflow-x: auto; }
.nav-links a { padding: .5rem 1rem; white-space: nowrap; }
.nav-links a.active { border-left: none; border-bottom: 2px solid var(--blue); }
/* v0.166.0: the old horizontal .nav-links strip was deleted — it predated the v0.146.0
accordion and, because the nested sub-lists share the .nav-links class, laid them out
horizontally inside an overflow:hidden row and clipped every sub-item past the first.
Replaced by a sticky top bar + off-canvas drawer (JS) with a static-sidebar fallback (no-JS). */
.mobile-topbar {
display: flex;
align-items: center;
justify-content: space-between;
position: sticky;
top: 0;
z-index: 800;
background: var(--bg-2);
border-bottom: 1px solid var(--line);
padding: .5rem 1rem;
}
.mobile-topbar-logo img { height: 36px; width: auto; display: block; }
/* JS path: the vertical sidebar becomes an off-canvas left drawer (accordion untouched). */
.js .sidebar {
position: fixed;
top: 0;
left: 0;
height: 100vh;
height: 100dvh;
width: min(280px, 85vw);
transform: translateX(-100%);
transition: transform .2s ease;
z-index: 950;
border-right: 1px solid var(--line);
border-bottom: none;
}
.js .sidebar.is-open { transform: none; }
.js .nav-backdrop:not([hidden]) {
display: block;
position: fixed;
inset: 0;
background: rgba(0, 0, 0, .5);
z-index: 900;
}
body.nav-open { overflow: hidden; }
/* No-JS fallback: sidebar renders static inline above the content so no destination dead-ends. */
.no-js .sidebar {
position: static;
height: auto;
width: 100%;
border-right: none;
border-bottom: 1px solid var(--line);
}
.no-js .nav-burger { display: none; }
.content { margin-left: 0; padding: 1rem; }
body { flex-direction: column; }
.app-row { flex-wrap: wrap; }
@@ -2552,6 +2610,10 @@ a.stat-card:hover {
.restore-label { min-width: auto; }
.restore-select { max-width: 100%; }
}
/* v0.166.0: honor reduced-motion for the drawer slide, matching the accordion/chevron convention. */
@media (prefers-reduced-motion: reduce) {
.js .sidebar { transition: none; }
}
/* ===================================================================
Storage Init Wizard & Migration UI