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