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:
@@ -1,5 +1,52 @@
|
||||
## Changelog
|
||||
|
||||
### v0.166.0 — Mobile nav drawer + sidebar cleanup + versioned logo/favicon URLs (2026-07-24)
|
||||
|
||||
No agent coupling; MinAgent unchanged. Templates (layout/login/icons) + CSS + one login-handler data
|
||||
key + tests only — no backend logic, no routes, no settings, no dependency changes. Desktop (>768px)
|
||||
is unchanged.
|
||||
|
||||
**Mobile navigation rework (Option A — off-canvas drawer).** The `@media(max-width:768px)` block
|
||||
predated the v0.146.0 accordion: it flattened `.nav-links` into a horizontal `overflow-x` strip, and
|
||||
because the accordion's nested sub-lists share the `.nav-links` class, sub-items laid out horizontally
|
||||
inside an `overflow:hidden` grid row — everything past the first sub-item was clipped. The strip is
|
||||
**deleted** (not patched — Option C was rejected) and replaced by:
|
||||
|
||||
- a sticky **top bar** (`.mobile-topbar`, logo → `/`, single hamburger `.nav-burger` with
|
||||
`aria-expanded`/`aria-controls="sidebar"`, new `#i-menu` icon);
|
||||
- the existing vertical sidebar reused as an **off-canvas left drawer** (`.js .sidebar`,
|
||||
`transform:translateX(-100%)`→`is-open`), a `.nav-backdrop`, body scroll-lock (`body.nav-open`);
|
||||
drawer JS toggles on burger, closes on backdrop click or Escape. **The accordion handler is
|
||||
untouched** — it works identically inside the drawer (all sub-items stack vertically, nothing clipped).
|
||||
- a **no-JS fallback**: `<html class="no-js">` (swapped to `js` by an early head script); when JS is
|
||||
off the sidebar renders static inline above the content and the burger is hidden, so no destination
|
||||
dead-ends.
|
||||
- z-index ladder: top bar 800 < backdrop 900 < drawer 950 < `.modal-overlay` 1000 (modals stay on top);
|
||||
`height:100dvh`; drawer transition disabled under `prefers-reduced-motion`.
|
||||
|
||||
**Sidebar customer-name removed.** The `<span class="customer-name">` and its dead CSS rule are gone
|
||||
from the sidebar header (logo only). `{{.CustomerName}}` stays in base data and on the **login page**
|
||||
subtitle (identifies the box owner).
|
||||
|
||||
**Cache-bust on logo/favicon.** `/static/felhom-logo.svg` and `/static/favicon.svg` now carry
|
||||
`?v={{.Version}}` (sidebar logo, head favicon, login logo) — Cloudflare edge-caches `/static/*` for 4h,
|
||||
so unversioned asset URLs kept serving the previous release's copy after a deploy (the 0.126.1 CSS
|
||||
failure mode). `renderLogin` now passes `Version`.
|
||||
|
||||
**Part 4 (outlined-logo swap) GATED OUT — not shipped.** The §3a precondition failed: live felhom.eu
|
||||
`main` (`be9edb44`) still serves a `website/assets/logo.svg` with live `<text>`/`font-family` (the
|
||||
text-outlined master is Viktor's manual Inkscape push, still pending). The `FelhomLogoSVG` /
|
||||
`FelhomFaviconSVG` constants are therefore **unchanged** and still contain live `<text>` — the wordmark
|
||||
renders in a fallback font under `<img>` secure static mode until the outlined asset lands and Part 4
|
||||
ships. Only the `?v=` cache-bust portion of the logo work is in this release.
|
||||
|
||||
- Tests (5 new, all through the real layout/CSS): topbar+drawer markup, CSS strip-removed/drawer-present
|
||||
(scoped to the 768px block), sidebar-no-customer-name, login-customer-name-kept, versioned asset URLs.
|
||||
RED-proofs recorded pre-change (strip present, no drawer, customer-name present, no `?v=`, and the
|
||||
gated logo-constant proof). `nav_accordion_test.go` invariants pass unchanged. Focus-trap on the
|
||||
drawer deliberately omitted (page navigations reset state). Android drawer feel + desktop
|
||||
pixel-parity are Viktor's visual acceptance step.
|
||||
|
||||
### v0.165.1 — Native "Megosztás…" button in the share modal (Web Share API) (2026-07-24)
|
||||
|
||||
No agent coupling; MinAgent unchanged. Template JS + tests only — no backend, no routes, no
|
||||
|
||||
+22
-1
@@ -7,7 +7,28 @@
|
||||
>
|
||||
> Ask Claude Code: "Please update CONTEXT.md with what we did today"
|
||||
|
||||
Last updated: 2026-07-24 (v0.165.1 — native "Megosztás…" button in the share modal via the Web Share API)
|
||||
Last updated: 2026-07-24 (v0.166.0 — mobile nav drawer + sidebar cleanup + versioned logo/favicon URLs)
|
||||
|
||||
> **2026-07-24 — v0.166.0 (mobile nav = off-canvas drawer; sidebar cleanup; ?v= on logo/favicon).**
|
||||
> 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 (they share the
|
||||
> `.nav-links` class). **Decision: mobile nav = a sticky top bar + off-canvas left drawer that REUSES
|
||||
> the vertical sidebar (Option A).** The accordion handler is untouched and works inside the drawer;
|
||||
> a `no-js` html-class fallback renders the sidebar static inline so nothing dead-ends without JS.
|
||||
> Options B (separate mobile menu) and C (exclude nested lists from the strip) were rejected. z-index
|
||||
> ladder topbar 800 < backdrop 900 < drawer 950 < modal 1000; `100dvh`; reduced-motion disables the
|
||||
> slide; focus-trap deliberately omitted (navigations reset state). **Sidebar customer-name removed**
|
||||
> (logo only); `{{.CustomerName}}` stays in base data + login subtitle. **Logo policy decision: the
|
||||
> wordmark must be OUTLINED paths, never live `<text>`** — under `<img>` secure static mode only
|
||||
> locally-installed fonts resolve, so `font-family` in the SVG renders a fallback font everywhere.
|
||||
> **Part 4 (swap `FelhomLogoSVG`/`FelhomFaviconSVG` to the outlined master) is GATED OUT** — §3a check
|
||||
> against live felhom.eu `main` (`be9edb44`) found `website/assets/logo.svg` still has `<text>`/
|
||||
> `font-family`; the outlined master is Viktor's manual Inkscape push, still pending. Only the `?v=`
|
||||
> cache-bust (logo/favicon/login-logo, Cloudflare 4h edge-cache — the 0.126.1 failure mode) shipped
|
||||
> from the logo work. Follow-up: when Viktor pushes the outlined asset, ship Part 4 (swap constants +
|
||||
> clean the favicon's vestigial `<text>` nodes). Separately, the website + hub still serve their own
|
||||
> non-outlined logo copies — propagation is a distinct follow-up.
|
||||
|
||||
|
||||
> **2026-07-24 — v0.165.1 (native "Megosztás…" in the share modal, Web Share API).** The share modal
|
||||
> gains a feature-detected `navigator.share` button (OS share sheet → Messenger/WhatsApp/email),
|
||||
|
||||
@@ -327,6 +327,7 @@ func (s *Server) renderLogin(w http.ResponseWriter, errorMsg, flashMsg string) {
|
||||
data := map[string]interface{}{
|
||||
"Title": "Bejelentkezés",
|
||||
"CustomerName": s.cfg.Customer.Name,
|
||||
"Version": s.version, // logo ?v= cache-bust (v0.166.0)
|
||||
"Error": errorMsg,
|
||||
"Flash": flashMsg,
|
||||
}
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// v0.166.0 mobile navigation rework. The old @media(max-width:768px) block predated the v0.146.0
|
||||
// accordion: it flattened .nav-links into a horizontal, overflow-x strip, which clipped the
|
||||
// accordion's nested sub-lists (they share the .nav-links class). This suite pins the replacement:
|
||||
// a hamburger + off-canvas drawer that REUSES the existing vertical sidebar, plus a no-JS static
|
||||
// fallback so navigation never dead-ends. Desktop (>768px) is untouched — the accordion invariants
|
||||
// stay in nav_accordion_test.go and must pass unchanged.
|
||||
|
||||
// mediaBlock768 returns the full brace-balanced body of the @media(max-width: 768px) rule so CSS
|
||||
// pins are scoped to the mobile block rather than matching an unrelated rule elsewhere in the sheet.
|
||||
func mediaBlock768(t *testing.T, css string) string {
|
||||
t.Helper()
|
||||
marker := "@media(max-width: 768px)"
|
||||
i := strings.Index(css, marker)
|
||||
if i < 0 {
|
||||
t.Fatalf("no %q media block found — the mobile breakpoint moved or was renamed", marker)
|
||||
}
|
||||
open := strings.IndexByte(css[i:], '{')
|
||||
if open < 0 {
|
||||
t.Fatal("no opening brace after the 768px media marker")
|
||||
}
|
||||
start := i + open
|
||||
depth := 0
|
||||
for j := start; j < len(css); j++ {
|
||||
switch css[j] {
|
||||
case '{':
|
||||
depth++
|
||||
case '}':
|
||||
depth--
|
||||
if depth == 0 {
|
||||
return css[start : j+1]
|
||||
}
|
||||
}
|
||||
}
|
||||
t.Fatal("unbalanced braces in the 768px media block")
|
||||
return ""
|
||||
}
|
||||
|
||||
// Group B — CSS pins (Scenario A/B/C).
|
||||
//
|
||||
// Red-proof: run against the pre-edit style.css and the strip-absence assertions FAIL — the old
|
||||
// block still carries `.nav-links { display:flex; ... overflow-x:auto }`.
|
||||
func TestMobileCSS_StripRemovedDrawerPresent(t *testing.T) {
|
||||
data, err := StyleCSS()
|
||||
if err != nil {
|
||||
t.Fatalf("StyleCSS(): %v", err)
|
||||
}
|
||||
css := string(data)
|
||||
block := mediaBlock768(t, css)
|
||||
|
||||
// The horizontal strip that clipped the accordion is gone from the mobile block.
|
||||
if strings.Contains(block, "overflow-x") {
|
||||
t.Error("768px block still contains an overflow-x rule — the horizontal .nav-links strip was not removed")
|
||||
}
|
||||
if strings.Contains(block, ".nav-links {") || strings.Contains(block, ".nav-links{") {
|
||||
t.Error("768px block still flattens .nav-links — the strip pattern must be deleted, not patched")
|
||||
}
|
||||
|
||||
// The off-canvas drawer + no-JS fallback are present in the mobile block.
|
||||
for _, want := range []string{
|
||||
".js .sidebar", // JS path: sidebar becomes an off-canvas drawer
|
||||
"translateX(-100%)", // parked off-canvas until opened
|
||||
".no-js .sidebar", // no-JS path: sidebar renders static inline
|
||||
"body.nav-open", // scroll lock while the drawer is open
|
||||
".mobile-topbar", // the sticky top bar is styled inside the mobile block
|
||||
} {
|
||||
if !strings.Contains(block, want) {
|
||||
t.Errorf("768px block missing %q — the drawer/fallback rules are incomplete", want)
|
||||
}
|
||||
}
|
||||
|
||||
// The desktop defaults that keep >768px pixel-identical live OUTSIDE the media query.
|
||||
for _, want := range []string{".mobile-topbar", ".nav-backdrop", ".nav-burger"} {
|
||||
if !strings.Contains(css, want) {
|
||||
t.Errorf("stylesheet missing base rule for %q", want)
|
||||
}
|
||||
}
|
||||
if !strings.Contains(css, ".mobile-topbar { display: none") &&
|
||||
!strings.Contains(css, ".mobile-topbar{display:none") {
|
||||
t.Error("no desktop-default `.mobile-topbar { display:none }` — the top bar would leak onto desktop")
|
||||
}
|
||||
}
|
||||
|
||||
// Group A — drawer markup (Scenario A/C), asserted through the real layout render.
|
||||
//
|
||||
// Red-proof: remove aria-controls from the burger button in layout.html → the aria-controls
|
||||
// assertion FAILS. (Recorded in REPORT.)
|
||||
func TestMobileNav_TopbarAndDrawerMarkup(t *testing.T) {
|
||||
html := renderNavFor(t, "dashboard")
|
||||
|
||||
// Progressive-enhancement hook: the html element starts as no-js and the head script swaps it.
|
||||
if !strings.Contains(html, `<html lang="hu" class="no-js">`) {
|
||||
t.Error("<html> element does not carry class=\"no-js\" — the no-JS CSS fallback would never engage")
|
||||
}
|
||||
if !strings.Contains(html, `replace('no-js','js')`) {
|
||||
t.Error("head script does not swap no-js → js")
|
||||
}
|
||||
|
||||
// Sticky top bar with a single burger toggle wired to the sidebar.
|
||||
if !strings.Contains(html, `class="mobile-topbar"`) {
|
||||
t.Error("no .mobile-topbar element rendered")
|
||||
}
|
||||
if !strings.Contains(html, `class="nav-burger"`) {
|
||||
t.Error("no .nav-burger button rendered")
|
||||
}
|
||||
if !strings.Contains(html, `aria-expanded="false"`) {
|
||||
t.Error("burger missing aria-expanded=\"false\" initial state")
|
||||
}
|
||||
if !strings.Contains(html, `aria-controls="sidebar"`) {
|
||||
t.Error("burger missing aria-controls=\"sidebar\" — the button is not associated with the drawer")
|
||||
}
|
||||
|
||||
// The sidebar is the drawer target, and the backdrop starts hidden.
|
||||
if !strings.Contains(html, `id="sidebar"`) {
|
||||
t.Error("sidebar has no id=\"sidebar\" — aria-controls points at nothing")
|
||||
}
|
||||
if !strings.Contains(html, `class="nav-backdrop"`) {
|
||||
t.Error("no .nav-backdrop element rendered")
|
||||
}
|
||||
// The backdrop must default hidden (both the attribute and its element must be present together).
|
||||
if !strings.Contains(html, `class="nav-backdrop" hidden`) {
|
||||
t.Error("nav-backdrop is not initially hidden")
|
||||
}
|
||||
}
|
||||
|
||||
// Group D — customer name (Scenario D).
|
||||
//
|
||||
// Red-proof: re-add <span class="customer-name">{{.CustomerName}}</span> to the sidebar header →
|
||||
// TestSidebar_NoCustomerName FAILS.
|
||||
func TestSidebar_NoCustomerName(t *testing.T) {
|
||||
html := renderNavFor(t, "dashboard")
|
||||
if strings.Contains(html, "customer-name") {
|
||||
t.Error("sidebar still renders the customer-name element — it must live only on the login page")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLogin_CustomerNameKept(t *testing.T) {
|
||||
s := testServer(t)
|
||||
s.loadTemplates()
|
||||
var buf bytes.Buffer
|
||||
data := map[string]interface{}{
|
||||
"Title": "Bejelentkezés",
|
||||
"CustomerName": "Teszt Ügyfél",
|
||||
"Version": "9.9.9",
|
||||
}
|
||||
if err := s.tmpl.ExecuteTemplate(&buf, "login", data); err != nil {
|
||||
t.Fatalf("render login: %v", err)
|
||||
}
|
||||
out := buf.String()
|
||||
// The login page still identifies whose box this is.
|
||||
if !strings.Contains(out, "Teszt Ügyfél") {
|
||||
t.Error("login page dropped the CustomerName subtitle — it identifies the box owner and must stay")
|
||||
}
|
||||
if !strings.Contains(out, `class="login-subtitle"`) {
|
||||
t.Error("login-subtitle element missing")
|
||||
}
|
||||
}
|
||||
|
||||
// Group E — versioned asset URLs (Scenario E). The logo-constant no-live-text test ships WITH
|
||||
// Part 4 (the outlined-asset swap), which is gated on Viktor pushing the outlined master; it is
|
||||
// intentionally not part of this suite while the constants still carry live <text>.
|
||||
//
|
||||
// Red-proof: run against the pre-edit layout.html → both assertions FAIL (the /static/ URLs lack
|
||||
// the ?v= cache-bust). Cloudflare edge-caches /static/* for 4h, so an unversioned logo/favicon keeps
|
||||
// serving the previous release after a deploy (same failure mode as the 0.126.1 CSS incident).
|
||||
func TestLayout_VersionedAssetURLs(t *testing.T) {
|
||||
html := renderNavFor(t, "dashboard")
|
||||
if !strings.Contains(html, "/static/felhom-logo.svg?v=") {
|
||||
t.Error("sidebar logo URL is not cache-busted with ?v= — a deploy would keep serving the stale edge copy")
|
||||
}
|
||||
if !strings.Contains(html, "/static/favicon.svg?v=") {
|
||||
t.Error("favicon URL is not cache-busted with ?v=")
|
||||
}
|
||||
}
|
||||
@@ -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}}
|
||||
|
||||
@@ -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]');
|
||||
|
||||
@@ -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}}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user