From e536fdd2c0af5fbe2b824d2ad95f5c1d3686aca4 Mon Sep 17 00:00:00 2001 From: Niklas Ye Date: Tue, 22 Sep 2026 21:47:15 +0200 Subject: [PATCH] Replace the mobile tab bar with a hamburger menu Six tabs (Queue, On-call, Alerts, Team, Admin, Account) had already outgrown the bottom bar once: 56b8191 let it auto-size its columns to fit however many there were, but on a phone that only left each tab 55-65px wide. Squeezing further wasn't an option, so the bar is gone on mobile and a hamburger button in the topbar opens a menu instead. The menu reuses the sheet + menu-item pattern already used for the snooze and assign-to actions in incident.js, rather than a new overlay component. It lists the same sections the sidebar does, including hiding Admin for non-admins, and the triggered-incident badge that used to sit on the Queue tab icon now shows on the hamburger button. Desktop (>=900px) is untouched: the sidebar is the same markup, CSS alone hides it below 900px and shows it above, so nothing there changed behaviourally. Claude-Session: https://claude.ai/code/session_01RHPj4ggeFdEjKKfm4SHbD7 --- README.md | 8 +++--- internal/web/static/app.css | 37 +++++++++++-------------- internal/web/static/index.html | 8 +++++- internal/web/static/js/app.js | 50 ++++++++++++++++++++++++++++++++-- internal/web/static/js/ui.js | 12 ++++++++ 5 files changed, 86 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index 4f1b560..c6c02f8 100644 --- a/README.md +++ b/README.md @@ -48,10 +48,10 @@ curl -H "Authorization: Bearer $KEY" http://localhost:8080/api/users The server serves a web UI at `/`: the incident queue, each incident's alerts and timeline with every action (acknowledge, assign, snooze, note, resolve, archive), who is on call, the alert feed, and an *Account* tab for your own -password and the ntfy topic your pages go to. It is built for a phone first. On a phone it has a bottom tab bar and a sticky action -bar, it follows the system's dark mode, and it can be added to the home screen. -From 900px wide it switches to a sidebar with the queue and the incident side by -side. Statistics remain in +password and the ntfy topic your pages go to. It is built for a phone first. On a phone +it navigates through a hamburger menu and has a sticky action bar, it follows the +system's dark mode, and it can be added to the home screen. From 900px wide it switches +to a sidebar with the queue and the incident side by side. Statistics remain in [terdut-tui](https://github.com/yeniklas/terdut-tui) for now. You sign in with a username and password. Users have no password until one is diff --git a/internal/web/static/app.css b/internal/web/static/app.css index d7e790f..c6f42c6 100644 --- a/internal/web/static/app.css +++ b/internal/web/static/app.css @@ -48,7 +48,11 @@ --mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; --topbar-h: 52px; - --tabbar-h: 58px; + /* No bottom tab bar on any breakpoint any more — mobile uses the hamburger + menu in the topbar, desktop the sidebar — so this stays 0. Kept as a + variable rather than deleted since .view, .toast and .nav's own height + calc still read it. */ + --tabbar-h: 0px; --safe-top: env(safe-area-inset-top, 0px); --safe-bottom: env(safe-area-inset-bottom, 0px); } @@ -196,7 +200,13 @@ input:focus, textarea:focus { outline: none; border-color: var(--accent); box-sh -webkit-backdrop-filter: saturate(1.4) blur(12px); border-bottom: 1px solid var(--border); } -.topbar-title { font-size: 18px; font-weight: 700; letter-spacing: -0.01em; } +.topbar-left { display: flex; align-items: center; gap: 8px; min-width: 0; } +.topbar-title { + font-size: 18px; font-weight: 700; letter-spacing: -0.01em; + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; +} +#menu-btn { position: relative; } +.nav-badge.menu-btn-badge { top: 2px; left: auto; right: 2px; } .open-pill { display: inline-flex; align-items: center; gap: 6px; @@ -209,15 +219,12 @@ input:focus, textarea:focus { outline: none; border-color: var(--accent); box-sh .open-pill.has-triggered::before { background: var(--crit); } .open-pill.all-acked::before { background: var(--warn); } -/* Bottom tab bar on phones. */ +/* Hidden on phones — mobile navigates through the hamburger menu in the + topbar instead (see #menu-btn / openNavMenu in app.js). Reappears as the + left sidebar from 900px, where the desktop block below redeclares display. */ .nav { + display: none; position: fixed; left: 0; right: 0; bottom: 0; z-index: 20; - /* One column per link, however many there are. This was repeat(4, 1fr) when - there were four tabs; Team and Admin arriving pushed six items into four - columns, which on a phone is how they stopped fitting. Auto columns mean - the next tab cannot break the row either — and Admin is only rendered for - an administrator, so the count genuinely varies between viewers. */ - display: grid; grid-auto-flow: column; grid-auto-columns: 1fr; height: calc(var(--tabbar-h) + var(--safe-bottom)); padding-bottom: var(--safe-bottom); background: color-mix(in srgb, var(--surface) 92%, transparent); @@ -239,16 +246,6 @@ input:focus, textarea:focus { outline: none; border-color: var(--accent); box-sh } .nav-link svg { width: 24px; height: 24px; flex: none; fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; } -/* Narrow phones, where six tabs each get about 55-65px. Tightening is enough: - the widest label, "On-call", is about 38px at this size, so nothing has to - be hidden. The ellipsis above is the backstop if a future tab is named - something longer. */ -@media (max-width: 420px) { - .nav-link { font-size: 10px; gap: 1px; } - .nav-link svg { width: 21px; height: 21px; } - .nav-badge { left: calc(50% + 4px); min-width: 16px; height: 16px; font-size: 10px; line-height: 16px; } -} - .nav-link[aria-current="page"] { color: var(--accent); } .nav-badge { position: absolute; top: 6px; left: calc(50% + 6px); @@ -601,8 +598,6 @@ kbd { /* ---------- desktop ---------- */ @media (min-width: 900px) { - :root { --tabbar-h: 0px; } - .app { display: grid; grid-template-columns: 220px 1fr; height: 100dvh; } .nav { diff --git a/internal/web/static/index.html b/internal/web/static/index.html index 01960bb..2174927 100644 --- a/internal/web/static/index.html +++ b/internal/web/static/index.html @@ -108,7 +108,13 @@
-

Queue

+
+ +

Queue

+
diff --git a/internal/web/static/js/app.js b/internal/web/static/js/app.js index 5e89698..45d7329 100644 --- a/internal/web/static/js/app.js +++ b/internal/web/static/js/app.js @@ -30,6 +30,17 @@ const SECTIONS = { more: { title: 'Account', view: account }, }; +// The mobile hamburger menu's contents — the same sections the desktop +// sidebar's .nav-link list carries in index.html, in the same order. +const NAV_ITEMS = [ + { path: '/', section: 'queue', label: 'Queue', icon: 'queueList' }, + { path: '/oncall', section: 'oncall', label: 'On-call', icon: 'calendar' }, + { path: '/alerts', section: 'alerts', label: 'Alerts', icon: 'bell' }, + { path: '/team', section: 'team', label: 'Team', icon: 'team' }, + { path: '/admin', section: 'admin', label: 'Admin', icon: 'shield', adminOnly: true }, + { path: '/more', section: 'more', label: 'Account', icon: 'user' }, +]; + function parseRoute(pathname) { const m = pathname.match(/^\/incidents\/(\d+)\/?$/); if (m) return { section: 'queue', incident: Number(m[1]) }; @@ -132,6 +143,35 @@ function render() { updateTitle(); } +// ---------- nav menu ---------- + +// The mobile hamburger menu: same shape as the sheet-based action menus in +// incident.js (openSheet + a