From 0060c024d55b1da2d6a88d154d1729a9c4fdbfad Mon Sep 17 00:00:00 2001 From: clawbot Date: Mon, 10 Aug 2026 13:45:59 +0000 Subject: [PATCH] fix: wrap per-host status line so 320px viewport does not scroll (closes #42) The status line carried `whitespace-nowrap`, and once populated its content (`min 1ms / med 2ms / avg 3ms / max 4ms`) measured 325px inside the 270px host column at a 320px viewport. The element does not clip, so the overflow propagated to the document and the page scrolled horizontally. Drop `whitespace-nowrap` from the status line and instead make each label/value/separator group individually unbreakable, so wrapping happens only between stats and a wrapped line never starts with a separator. The line fits on one row in the 420px desktop column, so the wide layout is unchanged. The repeated class string is now a single constant. --- TODO.md | 2 ++ src/main.js | 57 +++++++++++++++++++++++++++++++++++------------------ 2 files changed, 40 insertions(+), 19 deletions(-) diff --git a/TODO.md b/TODO.md index bed8e9d..0caaa52 100644 --- a/TODO.md +++ b/TODO.md @@ -22,6 +22,8 @@ files, so merging it also closes most compliance gaps. # Completed Steps +- 2026-08-10: per-host status line wraps below the 768px breakpoint instead of + forcing horizontal page scroll at 320px - 2026-08-09: `Dockerfile.backend` reworked to the mandated Go multistage lint-stage pattern: separate `lint` stage on the hash-pinned `golangci/golangci-lint` image, `COPY --from=lint` stage dependency, diff --git a/src/main.js b/src/main.js index 3ab6bb8..aaa3cdf 100644 --- a/src/main.js +++ b/src/main.js @@ -537,6 +537,12 @@ class SparklineRenderer { // --- UI Renderer ------------------------------------------------------------- +// The per-host status line must stay wrappable: its populated content is +// wider than the host column at a 320px viewport, and `whitespace-nowrap` +// here overflows the element and forces the whole document to scroll +// horizontally. +const STATUS_TEXT_CLASS = "status-text text-xs text-right col-span-2 mt-5"; + function hostRowHTML(host, index, showPin = true) { const pinColor = host.pinned ? "text-blue-500" @@ -562,7 +568,7 @@ function hostRowHTML(host, index, showPin = true) { --- ${host.url} -
waiting...
+
waiting...
@@ -688,6 +694,26 @@ function buildUI(state) { // --- UI Updaters ------------------------------------------------------------- +// Renders `min 1ms / med 2ms / avg 3ms / max 4ms`. Each label, value and +// trailing separator is one unbreakable unit, so wrapping only ever happens +// between stats and a wrapped line never starts with a separator. +function statusStatsHTML(stats) { + return stats + .map(([label, value], i) => { + const sep = + i < stats.length - 1 + ? ` /` + : ""; + return ( + `` + + `${label} ` + + `${value}ms` + + `${sep}` + ); + }) + .join(" "); +} + function updateHostRow(host, index) { const latencyEl = document.querySelector( `.latency-value[data-host="${index}"]`, @@ -712,28 +738,22 @@ function updateHostRow(host, index) { const min = host.minLatency(); const max = host.maxLatency(); if (host.status === "online" && avg !== null) { - statusEl.innerHTML = - `min ${min}ms` + - ` / ` + - `med ${med}ms` + - ` / ` + - `avg ${avg}ms` + - ` / ` + - `max ${max}ms`; - statusEl.className = - "status-text text-xs whitespace-nowrap text-right col-span-2 mt-5"; + statusEl.innerHTML = statusStatsHTML([ + ["min", min], + ["med", med], + ["avg", avg], + ["max", max], + ]); + statusEl.className = STATUS_TEXT_CLASS; } else if (host.status === "offline") { statusEl.textContent = "unreachable"; - statusEl.className = - "status-text text-xs text-red-400 whitespace-nowrap text-right col-span-2 mt-5"; + statusEl.className = `${STATUS_TEXT_CLASS} text-red-400`; } else if (host.status === "error") { statusEl.textContent = "timeout"; - statusEl.className = - "status-text text-xs text-orange-400 whitespace-nowrap text-right col-span-2 mt-5"; + statusEl.className = `${STATUS_TEXT_CLASS} text-orange-400`; } else { statusEl.textContent = "connecting..."; - statusEl.className = - "status-text text-xs text-gray-500 whitespace-nowrap text-right col-span-2 mt-5"; + statusEl.className = `${STATUS_TEXT_CLASS} text-gray-500`; } SparklineRenderer.draw(canvas, host.history); @@ -1044,8 +1064,7 @@ function greyOutUI(state) { } if (statusEl) { statusEl.textContent = "paused"; - statusEl.className = - "status-text text-xs text-gray-500 whitespace-nowrap text-right col-span-2 mt-5"; + statusEl.className = `${STATUS_TEXT_CLASS} text-gray-500`; } // Grey out the status dot const row = document.querySelector(`.host-row[data-index="${i}"]`); -- 2.49.1