From 2b3ea33884651929e1ab3d66e7e1576560c7ea5b Mon Sep 17 00:00:00 2001 From: Giorgio Gilestro Date: Thu, 28 May 2026 18:36:37 +0200 Subject: [PATCH] mobile: hamburger drawer (right-side slide-out) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ≤480px gets a hamburger button in the topbar and a fixed slide-out panel from the right edge (width min(82vw, 320px)). The topbar keeps only brand + tone toggle + hamburger visible; nav and the header-right widgets (theme, lang, user menu, version meta) move into the drawer. Markup change: nav and .header-right are now wrapped in .mobile-drawer, which is display:contents on desktop (no layout effect) and a fixed translateX panel on mobile. The user-menu dropdown chip hides on mobile and its links surface flat inside the drawer. JS: ~50 lines of vanilla. Tap hamburger / backdrop / ESC / swipe- right-on-drawer all close. Clicking a nav link inside the drawer closes it after the navigation kicks off so the panel doesn't linger on the next page. CSS: per-file @media block at the bottom of layout.css per the agreed-upon organisation. --- app/static/css/layout.css | 176 ++++++++++++++++++++++++++++++++++++++ app/templates/base.html | 161 ++++++++++++++++++++++++---------- 2 files changed, 292 insertions(+), 45 deletions(-) diff --git a/app/static/css/layout.css b/app/static/css/layout.css index c4293b6..2a66ccc 100644 --- a/app/static/css/layout.css +++ b/app/static/css/layout.css @@ -51,7 +51,46 @@ a:hover { text-decoration: underline; } .app-header nav a.active { color: var(--text); } .app-header .meta { color: var(--muted); font-size: 11px; } +/* On desktop the mobile-drawer wrapper has no layout effect — its + * children (nav, header-right) flow as if it weren't there. On mobile + * the @media block at the bottom converts it to a fixed slide-out. */ +.mobile-drawer { display: contents; } + .app-header .header-right { display: flex; align-items: center; gap: 14px; } + +/* Hamburger button — only visible at ≤480px (rule in the mobile block). + * Three thin bars; uses the same border/muted treatment as the other + * header buttons so the visual rhythm matches. */ +.drawer-toggle { + display: none; + background: transparent; + border: 1px solid var(--border); + cursor: pointer; + padding: 6px 8px; + width: 36px; + height: 32px; + flex-direction: column; + justify-content: space-between; + align-items: stretch; +} +.drawer-toggle:hover { border-color: var(--accent); } +.drawer-toggle__bar { + display: block; + height: 2px; + background: var(--muted); + width: 100%; +} +.drawer-toggle:hover .drawer-toggle__bar { background: var(--accent); } + +.drawer-backdrop { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.4); + z-index: 90; + opacity: 0; + transition: opacity 120ms ease-out; +} +body.drawer-open .drawer-backdrop { opacity: 1; } .theme-toggle { background: transparent; border: 1px solid var(--border); @@ -183,3 +222,140 @@ a:hover { text-decoration: underline; } ::-webkit-scrollbar-track { background: var(--bg); } ::-webkit-scrollbar-thumb { background: var(--dim); border-radius: 0; } ::-webkit-scrollbar-thumb:hover { background: var(--muted); } + + +/* --- Mobile (≤480px) -------------------------------------------------- */ + +@media (max-width: 480px) { + /* Tighten the topbar so the brand, tone toggle and hamburger all fit + on a 360px phone. Drop the letter-spacing because at 11px tracking + eats horizontal space the brand cannot spare. */ + .app-header { + padding: 8px 12px; + gap: 8px; + letter-spacing: 0.04em; + } + .app-header .brand { + font-size: 12px; + /* Shrink the leading glyph but don't remove it — keeps brand identity. */ + } + .beta-chip { display: none; } + + /* Show the hamburger; the rest of the header widgets collapse into + the drawer (the .mobile-drawer block below). */ + .drawer-toggle { display: flex; margin-left: auto; } + + /* Keep the tone toggle visible but trim it: just N / I letters so it + fits next to brand + hamburger. */ + .tone-toggle--header { + font-size: 9.5px; + } + .tone-toggle--header button { + padding: 4px 7px; + } + + /* The drawer wrapper: full-height slide-out from the right. The + content inside (nav + header-right) becomes a vertical stack + with comfortable touch targets. */ + .mobile-drawer { + display: flex; + flex-direction: column; + gap: 0; + position: fixed; + top: 0; + right: 0; + bottom: 0; + width: min(82vw, 320px); + background: var(--surface); + border-left: 1px solid var(--border); + box-shadow: -2px 0 12px rgba(0, 0, 0, 0.18); + transform: translateX(100%); + transition: transform 180ms ease-out; + z-index: 100; + overflow-y: auto; + padding: 56px 18px 24px; + text-transform: none; + letter-spacing: 0.02em; + } + body.drawer-open .mobile-drawer { transform: translateX(0); } + + /* Vertical nav inside the drawer — links become big-tap rows, no + leading margin like the desktop horizontal nav. */ + .mobile-drawer nav { display: flex; flex-direction: column; } + .mobile-drawer nav a { + margin-left: 0; + padding: 12px 4px; + border-bottom: 1px solid var(--border); + font-size: 14px; + text-transform: uppercase; + letter-spacing: 0.06em; + } + .mobile-drawer nav a.active { + color: var(--accent); + border-left: 2px solid var(--accent); + padding-left: 10px; + } + + /* header-right widgets vertically stacked inside the drawer. */ + .mobile-drawer .header-right { + flex-direction: column; + align-items: stretch; + gap: 14px; + margin-top: 20px; + } + .mobile-drawer .theme-toggle, + .mobile-drawer .lang-toggle { + width: 100%; + justify-content: center; + } + .mobile-drawer .lang-toggle { display: inline-flex; } + .mobile-drawer .lang-toggle button, + .mobile-drawer .theme-toggle { padding: 10px; font-size: 11.5px; } + + /* The user-menu's dropdown becomes redundant inside the drawer — + surface its links flat as a list, and hide the chip button. */ + .mobile-drawer .user-menu { width: 100%; } + .mobile-drawer .user-chip { display: none; } + .mobile-drawer .user-menu__panel { + display: block !important; /* override the hidden attribute */ + position: static; + border: 0; + padding: 0; + margin-top: 4px; + } + .mobile-drawer .user-menu__panel[hidden] { display: block !important; } + .mobile-drawer .user-menu__item { + display: block; + padding: 10px 4px; + border-bottom: 1px solid var(--border); + font-size: 13px; + text-transform: uppercase; + letter-spacing: 0.06em; + } + .mobile-drawer .meta { + margin-top: auto; + padding-top: 18px; + text-align: center; + opacity: 0.7; + } + + /* The drawer container itself sits above the topbar in z-stacking; + we still want the close button accessible while it's open, so push + a close target into the top-right corner of the drawer via a + repurposed pseudo-element. (Simpler than adding new markup.) */ + .mobile-drawer::before { + content: "✕"; + position: absolute; + top: 14px; + right: 18px; + font-size: 18px; + color: var(--muted); + cursor: pointer; + pointer-events: none; /* tap handled by the backdrop / hamburger */ + } + + /* Body-level layout: tighten main padding too — saves another 16px + of horizontal real estate which the indicator table and chat + bubbles all benefit from. */ + .app-main { padding: 10px 8px; gap: 10px; } +} diff --git a/app/templates/base.html b/app/templates/base.html index fd15361..ccbcb08 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -207,57 +207,78 @@
{{ BRAND_NAME }} {% if BETA_MODE %}BETA{% endif %} - -
-
- - -
- - {% set cu = request.state.current_user if request.state and request.state.current_user is defined else None %} - {% if cu and cu.user %} -
- - -
- {% endif %} - {% if cu and (cu.user or cu.is_admin) %} -
- + +
+ + {# Mobile hamburger — shown only at ≤480px via CSS. #} + + + {# Wrapper: display:contents on desktop (zero layout effect), fixed + slide-out panel on mobile. Holds nav + header-right widgets. #} +
+ +
+ - - {% endif %} - v0.1 · UTC
+ {# Drawer backdrop. Hidden by default; CSS shows it when + body.drawer-open is set. Click closes the drawer. #} + + + +
{% block main %}{% endblock %}