From 2458ff434ae2a7ad585f3dabba728e8271988849 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 24 Aug 2026 20:23:32 +0000 Subject: [PATCH] board theme: make wrapping the page default so no board scrolls sideways MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On a phone the Mind and Memory dashboards held the viewport while the other organ boards scrolled right — the Heart board worst, with reason text running off the screen edge. The cause was that wrapping was declared per component. The shared sheet put `overflow-wrap:anywhere` on `.task p` and `table.recent td` only, so the two pages built entirely from those two shapes were fine, and every element an organ adds itself — the Heart's `.reasons` list, a details block, a footer — had no wrap at all. One run URL in a Heart red reason was enough: measured in a headless Chromium at a 375px viewport, that page's document was 575px wide, 200px of it off-screen. Move the guard to `body`, where `overflow-wrap` is inherited by markup this module has never seen, and bound the things that have no wrap opportunity to take (`img,svg,table{max-width:100%}`, `pre{overflow-x:auto}`). The two component rules that carried their own copy drop it: one place, not seven. Every board imports this sheet live from PyAutoBrain (Heart, Hands and the umbrella board resolve it through their sibling checkout), so the fix lands on the whole family without touching those repos. Re-measured after the change: Heart 375/375 at both 390px and 320px, including a stress render with long workspace script paths and the nowrap name column; Brain and Mind unchanged at 375/375 and 305/305. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01VhpLPmmoSFAtVpppG8azVA --- board/AGENTS.md | 8 ++++++++ board/_theme.py | 17 ++++++++++++++--- tests/test_board_theme.py | 32 ++++++++++++++++++++++++++++++++ 3 files changed, 54 insertions(+), 3 deletions(-) diff --git a/board/AGENTS.md b/board/AGENTS.md index aa988ef..25a6357 100644 --- a/board/AGENTS.md +++ b/board/AGENTS.md @@ -75,6 +75,14 @@ name in its accent, the logo's tagline underneath. Below the hero the page goes back to being a plain readable document, because these are lists people scan on a phone before breakfast. +*A phone before breakfast* is a size, not a mood: the sheet makes wrapping the +page **default** (`overflow-wrap` on `body`, inherited), so a run URL or a +dotted test id in markup the theme has never seen — an organ's own reasons +list, details block or footer — cannot push the page sideways. A board that +needs a column on one line (`white-space:nowrap`) still may; what it must not +do is re-declare the wrap per component, which is how the family drifted into +one page that scrolled and one that did not. + Colour is information, never decoration: the accent is organ identity; pills are row facets, toned so that only the *exception* is coloured (`supervised` is 9 of 10 prompts in the Mind, so it stays neutral — tinting it would paint diff --git a/board/_theme.py b/board/_theme.py index b922340..2ffa22f 100644 --- a/board/_theme.py +++ b/board/_theme.py @@ -299,9 +299,20 @@ def mark(key): --bad:#f85149;--accent:%(ink_dark)s;--tint:%(ink_dark)s1f; --edge:%(ink_dark)s47}} *{box-sizing:border-box} +/* Wrapping is the page DEFAULT, not a per-component opt-in. These boards are + read on phones, and every one of them prints run URLs, dotted test ids and + long file paths — a single unbreakable token in any element a renderer adds + itself (a reasons list, a footer, a details block) spills past the right + edge and gives the WHOLE page a horizontal scroll. `overflow-wrap` is + inherited, so setting it here covers markup this module has never seen. + The three max-width/overflow rules do the same job for the things that + cannot be wrapped: an image, a table, a code block. */ body{margin:0 auto;max-width:44rem;padding:0 1rem 4rem;background:var(--bg); color:var(--fg);font:16px/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI", - Helvetica,Arial,sans-serif;-webkit-text-size-adjust:100%%} + Helvetica,Arial,sans-serif;-webkit-text-size-adjust:100%%; + overflow-wrap:anywhere} +img,svg,table{max-width:100%%} +pre{overflow-x:auto} a{color:var(--accent);text-decoration:none} a:hover{text-decoration:underline} .muted{color:var(--muted)} @@ -353,7 +364,7 @@ def mark(key): .task{display:flex;gap:.6rem;align-items:flex-start;padding:.45rem .35rem; margin:0 -.35rem;border-bottom:1px solid var(--line);border-radius:7px} .task:hover{background:var(--tint)} -.task p{margin:.25rem 0 0;flex:1;overflow-wrap:anywhere} +.task p{margin:.25rem 0 0;flex:1} button.copy{flex:0 0 auto;width:2.6rem;height:2.6rem;font-size:1.1rem; border:1px solid var(--line);border-radius:9px;background:var(--btn); cursor:pointer;color:var(--fg);transition:border-color .12s,color .12s} @@ -402,7 +413,7 @@ def mark(key): /* --- tables ------------------------------------------------------------ */ table.recent{width:100%%;border-collapse:collapse;font-size:.95em} table.recent td{border-bottom:1px solid var(--line); - padding:.45rem .4rem .45rem 0;vertical-align:top;overflow-wrap:anywhere} + padding:.45rem .4rem .45rem 0;vertical-align:top} table.recent tr:hover td{background:var(--tint)} table.recent td.when{white-space:nowrap;color:var(--muted); font-variant-numeric:tabular-nums} diff --git a/tests/test_board_theme.py b/tests/test_board_theme.py index 6276b9c..4406f48 100644 --- a/tests/test_board_theme.py +++ b/tests/test_board_theme.py @@ -174,3 +174,35 @@ def test_boards_footer_skips_self_and_tags_each_sibling(): def test_stats_render_pairs_and_vanish_when_empty(): assert _theme.stats() == "" assert "3In flight" in _theme.stats((3, "In flight")) + + +# --- the phone invariant: nothing may push the page sideways --------------- +# These boards are read on a phone before breakfast. A single unbreakable +# token — a run URL, a dotted test id, a workspace script path — used to +# spill past the right edge and give the WHOLE page a horizontal scroll, +# because wrapping was declared per component (`.task p`, `table.recent td`) +# and every organ's own markup (a reasons list, a details block, a footer) +# missed out. The guard belongs on `body`, where it is inherited by markup +# this module has never seen. + + +def test_wrapping_is_the_page_default_not_a_per_component_opt_in(): + css = _theme.css("mind") + body = re.search(r"^body\{(.*?)\}", css, re.S | re.M).group(1) + assert "overflow-wrap:anywhere" in body.replace("\n ", "") + + +def test_the_things_that_cannot_wrap_are_bounded_instead(): + # An image, a table or a code block has no soft wrap opportunity to take; + # each is held inside the column or given its own scroller. + css = _theme.css("mind") + assert "img,svg,table{max-width:100%}" in css + assert "pre{overflow-x:auto}" in css + + +def test_no_component_rule_re_declares_the_wrap(): + """One place, not seven — a component that sets its own wrap is a rule + that will be forgotten by the next board that adds a list.""" + css = re.sub(r"/\*.*?\*/", "", _theme.css("mind"), flags=re.S) + body_rule = re.search(r"^body\{.*?\}", css, re.S | re.M).group(0) + assert css.replace(body_rule, "").count("overflow-wrap") == 0