From 889d00007dc1ddd970d71dee4c2fe11ea9874978 Mon Sep 17 00:00:00 2001 From: Stan Grams Date: Mon, 3 Aug 2026 20:37:31 +0200 Subject: [PATCH] [feat](trx-frontend-http): box the selected tab instead of underlining it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The desktop strip marked the current page with a 2px underline while the mobile bottom nav already boxed it, so one navigation model looked like two. The box now sits on both: a transparent 1px border on the base reserves it, so switching pages moves no neighbours, and hover fills a fainter version of the same shape. Tools carries it too — that button is marked active for the destinations the strip hides. Dropping the mobile rule's border-bottom:none, which only existed to cancel the old desktop underline, closes the bottom edge its active box had been missing. The smoke test checks all four edges. Signed-off-by: Stan Grams --- .../trx-frontend-http/assets/web/style.css | 28 ++++++++++++++----- .../frontend/tests/browser-smoke.mjs | 17 ++++++++++- 2 files changed, 37 insertions(+), 8 deletions(-) diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/style.css b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/style.css index 75f72541..eeced9fc 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/style.css +++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/style.css @@ -1550,19 +1550,34 @@ small { color: var(--text-muted); } .tab-bar-nav .tab .tab-icon, .tab-bar-nav .tab .tab-more-icon { display: block; } .tab-bar-nav .tab { padding: 0.5rem 0.6rem; } } +/* The selected destination is boxed, not underlined — the same treatment the + mobile bottom nav already used, so one navigation model reads the same at + every width. The transparent border is on the base so switching tabs moves + no neighbours. */ .tab { background: transparent; - border: none; - border-bottom: 2px solid transparent; - border-radius: 0; - padding: 0.5rem 0.95rem; + border: 1px solid transparent; + border-radius: var(--radius-md); + padding: 0.45rem 0.9rem; color: var(--text-muted); cursor: pointer; font-size: 0.95rem; height: auto; + transition: color var(--dur-fast) var(--ease-standard), + background-color var(--dur-fast) var(--ease-standard), + border-color var(--dur-fast) var(--ease-standard); +} +.tab.active { + border-color: color-mix(in srgb, var(--accent-green) 50%, var(--border-light)); + background: color-mix(in srgb, var(--accent-green) 12%, transparent); + color: var(--accent-text); + font-weight: 600; +} +.tab:hover:not(.active) { + color: var(--text); + background: color-mix(in srgb, var(--btn-bg) 55%, transparent); + border-color: color-mix(in srgb, var(--border-light) 40%, transparent); } -.tab.active { border-bottom-color: var(--accent-green); color: var(--accent-text); font-weight: 600; } -.tab:hover:not(.active) { color: var(--text); } /* Tab icons — hidden on desktop, shown on mobile bottom nav */ .tab-icon { display: none; @@ -3361,7 +3376,6 @@ body[data-operator-layout="broadcast"] #cw-bar-overlay { min-height: 3.2rem; padding: 0.35rem 0.1rem 0.3rem; border: 1px solid transparent; - border-bottom: none; border-radius: 0.75rem; white-space: nowrap; text-align: center; diff --git a/src/trx-client/trx-frontend/trx-frontend-http/frontend/tests/browser-smoke.mjs b/src/trx-client/trx-frontend/trx-frontend-http/frontend/tests/browser-smoke.mjs index 50f53017..ed053be4 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/frontend/tests/browser-smoke.mjs +++ b/src/trx-client/trx-frontend/trx-frontend-http/frontend/tests/browser-smoke.mjs @@ -10,7 +10,7 @@ import { fileURLToPath } from "node:url"; import { chromium } from "playwright-core"; // page.evaluate callbacks run in the browser, not in this Node process. -/* global document */ +/* global document, getComputedStyle */ const frontendDir = path.dirname(path.dirname(fileURLToPath(import.meta.url))); const webDir = path.resolve(frontendDir, "../assets/web"); @@ -213,6 +213,21 @@ try { await page.locator("#aprs-map .leaflet-pane").first().waitFor({ state: "attached" }); assert.equal(new URL(page.url()).pathname, "/map"); + // The selected destination is marked by a box on all four sides, so a rule + // that drops one edge (the mobile nav used to lose its bottom border) is a + // regression even though the tab still reads as "active". + const activeTab = await page.evaluate(() => { + const style = getComputedStyle(document.querySelector(".tab-bar-nav .tab.active")); + return ["Top", "Right", "Bottom", "Left"].map((side) => ({ + width: style.getPropertyValue(`border-${side.toLowerCase()}-width`), + color: style.getPropertyValue(`border-${side.toLowerCase()}-color`), + })); + }); + for (const edge of activeTab) { + assert.notEqual(edge.width, "0px", `active tab border: ${JSON.stringify(activeTab)}`); + assert.ok(!/rgba\(0, 0, 0, 0\)|transparent/.test(edge.color), `active tab border: ${JSON.stringify(activeTab)}`); + } + // The map is full-bleed: it breaks out of the centred .card column and // reaches both viewport edges, without pushing the page sideways. const stage = await page.evaluate(() => {