Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions board/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
17 changes: 14 additions & 3 deletions board/_theme.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)}
Expand Down Expand Up @@ -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}
Expand Down Expand Up @@ -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}
Expand Down
32 changes: 32 additions & 0 deletions tests/test_board_theme.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 "<b>3</b><span>In flight</span>" 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
Loading