diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-09-19 18:32:47 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-09-19 18:32:47 +0200 |
| commit | 39c58b807d5f62ee55b606ceb79f21855221a8d0 (patch) | |
| tree | 39289388df6c67ece508c8d060a203553d33475a | |
| parent | 1ec316035ce9aa656dd18a05889856bce9e3aba3 (diff) | |
| download | meshbay-39c58b807d5f62ee55b606ceb79f21855221a8d0.tar.gz | |
fix(hub): the members table stops hanging off the side of a phone
`.admin-table` column heads are `white-space: nowrap`, which meets a
translated string: "Username" is one word, "Nom d'utilisateur" is three,
uppercased with letter-spacing on top. The heads alone overflowed a 360px
window by 11px, and on Android an overflowing document takes every sticky
element with it.
11px was only what could be measured: the probe served no members, so the
table measured its headers and nothing else. With one realistic username
in it the same page overflowed by 248px, at 420px-wide windows too. The
cells get `word-break: break-word`, as `.file-name` already had.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
| -rw-r--r-- | CLAUDE.md | 14 | ||||
| -rw-r--r-- | packages/meshbay-hub/src/meshbay_hub/static/style.css | 17 | ||||
| -rwxr-xr-x | packages/meshbay-hub/tests/harness/sticky_header_probe.py | 18 |
3 files changed, 47 insertions, 2 deletions
@@ -492,6 +492,20 @@ do. Read them before writing anything that touches the same mechanism. `autofocus`, and Android's `interactive-widget` default splits the visual viewport from the layout one), which was plausible, cost a round trip, and was wrong; the screenshot showing no keyboard was already in hand. + **It came back on 2026-09-19, one table along, and the fixture hid most of + it again.** `.admin-table` overflowed a 360px window by 11px — from its + *column heads*, because `white-space: nowrap` met a translated string: + "Username" is one word and "Nom d'utilisateur" is three, uppercased with + letter-spacing on top. **A label's length is not yours to assume once it goes + through `t()`**, and a head that wraps onto two lines is legible where a + table the reader cannot reach the right of is not. The 11px was the whole of + what could be measured, because the probe served no members at all: an empty + table measures its headers and nothing else. With one realistic username in + it — no spaces to break at, a string somebody else chose — the same page + overflowed by **248px**, and at 420px-wide windows as well as 360. Same + lesson as the paragraph above, twice now: the wrapping rule belongs on every + cell that carries a name, and the fixture has to carry the names real data + has or it measures itself. - **Derived data that nothing persists is re-derived where somebody is watching.** `video_meta`, `photo_meta` and `thumbs` were durable; the audio diff --git a/packages/meshbay-hub/src/meshbay_hub/static/style.css b/packages/meshbay-hub/src/meshbay_hub/static/style.css index 81ae895..63f631a 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/style.css +++ b/packages/meshbay-hub/src/meshbay_hub/static/style.css @@ -2110,6 +2110,14 @@ button:disabled { opacity: 0.5; cursor: not-allowed; } border-collapse: collapse; font-size: 0.88em; } +/* A column head is a *translated* string, so its length is not ours to assume: + "Username" is one word in English and "Nom d'utilisateur" is three in French, + and uppercased with letter-spacing on top. Under `white-space: nowrap` the + longer one sets the column's minimum width, and the whole table hangs off the + side of a phone — which on Android takes every `position: sticky` element + with it, the fault already recorded against the Files table one table along. + A head that wraps onto two lines is legible; a table the reader cannot reach + the right of is not. */ .admin-table th { text-align: left; padding: 8px 10px; @@ -2119,12 +2127,19 @@ button:disabled { opacity: 0.5; cursor: not-allowed; } font-weight: 600; text-transform: uppercase; letter-spacing: 0.04em; - white-space: nowrap; + white-space: normal; } +/* And the same rule as `.file-name`, for the same reason: these cells carry + names somebody else chose — a username, a group name — and a name with no + spaces in it has nowhere to break unless it is told to. The heads alone cost + 11px of overflow at 360px; with a realistic username in the fixture it was + 248px, and it reached 420px-wide windows too. */ .admin-table td { padding: 8px 10px; border-bottom: 1px solid var(--border); vertical-align: middle; + word-break: break-word; + min-width: 0; } .admin-table tr:hover { background: var(--bg-raised); } .admin-table .admin-empty { diff --git a/packages/meshbay-hub/tests/harness/sticky_header_probe.py b/packages/meshbay-hub/tests/harness/sticky_header_probe.py index 6f084d6..7579bf9 100755 --- a/packages/meshbay-hub/tests/harness/sticky_header_probe.py +++ b/packages/meshbay-hub/tests/harness/sticky_header_probe.py @@ -401,7 +401,11 @@ const settle = () => new Promise((r) => .slice(0, 6) .map((el) => (el.tagName.toLowerCase() + '.' + (el.className || '')) .slice(0, 60) + ' →' - + Math.round(el.getBoundingClientRect().right)); + + Math.round(el.getBoundingClientRect().right) + // The text, because the element alone never says which + // cell it was: a column is as wide as its widest + // unwrappable string, and that string is the finding. + + ' ' + JSON.stringify((el.textContent || '').trim().slice(0, 40))); })(), innerHeight, innerWidth, // What the stylesheet was actually handed, so a wrong offset can be told @@ -487,6 +491,18 @@ class H(http.server.BaseHTTPRequestHandler): self._send(render_frame(index).encode(), "text/html; charset=utf-8") elif path == "/v1/groups/g1/nodes": self._send(b'{"nodes": [{"node_id": "n1"}]}', "application/json") + elif path == "/v1/groups/g1/members": + # Served at all because an empty table measures only its headers, + # and a member's name is a string this project did not choose. One + # of them has no spaces to break at, which is what makes a cell set + # its column's minimum width — the fault already recorded against + # the Files table, one table along. + self._send( + b'{"members": [' + b'{"user_id": "u1", "username": "moi"},' + b'{"user_id": "u2", "username":' + b' "un_utilisateur_au_nom_deraisonnablement_long_2026"}]}', + "application/json") else: asset = (STATIC / path.lstrip("/")).resolve() if not str(asset).startswith(str(STATIC)) or not asset.is_file(): |