From f9d55efa1563ad1a415d18092e3c50644ba1bdb8 Mon Sep 17 00:00:00 2001 From: istos Date: Thu, 30 Jul 2026 08:22:36 +0200 Subject: [PATCH] Make the activity log's resize grip actually resize MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The grip's handlers write #logbody's inline height, but the CSS said flex:1 — inside the column-flex #logpanel the flex algorithm sized the element from basis 0% + grow and never consulted the height property, so dragging did nothing. Worse, mouseup persisted a re-read offsetHeight (the flex-computed value), overwriting the remembered size with the status quo on every attempt. Re-couple the two: #logbody becomes flex:none so the written height is authoritative again, with max-height:60vh guarding overflow. The handlers track the clamped height they compute and mouseup persists that value; the load-time restore runs through the same clamp and writes the same property. The drawer grip writes width on the position:fixed #drawer itself, so no flex competes there — covered by a non-regression test alongside the source-level invariants in tests/test_log_resize.py. Co-Authored-By: Claude Fable 5 --- manager/core/board.html | 12 +++-- tests/test_log_resize.py | 103 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 110 insertions(+), 5 deletions(-) create mode 100644 tests/test_log_resize.py diff --git a/manager/core/board.html b/manager/core/board.html index b2b367f..04c347f 100644 --- a/manager/core/board.html +++ b/manager/core/board.html @@ -236,7 +236,7 @@ } #loghead .xfile{cursor:pointer;padding:1px 0;white-space:nowrap} #loghead .xfile:hover{color:var(--accent)} - #logbody{flex:1;min-height:0;overflow-y:auto;padding:6px 18px 12px;display:grid;gap:2px;align-content:start} + #logbody{flex:none;max-height:60vh;overflow-y:auto;padding:6px 18px 12px;display:grid;gap:2px;align-content:start} .ev{display:grid;grid-template-columns:64px 18px 1fr;align-items:baseline;gap:10px;font-size:12px;min-width:0} .ev time{font-family:var(--mono);font-size:11px;color:var(--dim)} .ev .glyph{font-family:var(--mono);font-size:11.5px;color:var(--muted);text-align:center} @@ -1804,8 +1804,10 @@ document.addEventListener('keydown', (e) => { /* the activity log: drag its grip to resize, scroll to read back */ { const grip = $('#loggrip'), body = $('#logbody'); - body.style.height = (parseInt(localStorage.getItem('bench-log-h'), 10) || - Math.round(innerHeight * 0.30)) + 'px'; + const clamp = (px) => Math.min(Math.max(px, 80), Math.round(innerHeight * 0.6)); + let h = clamp(parseInt(localStorage.getItem('bench-log-h'), 10) || + Math.round(innerHeight * 0.30)); + body.style.height = h + 'px'; let dragging = false, startY = 0, startH = 0; grip.addEventListener('mousedown', (e) => { dragging = true; startY = e.clientY; startH = body.offsetHeight; @@ -1813,13 +1815,13 @@ document.addEventListener('keydown', (e) => { }); document.addEventListener('mousemove', (e) => { if (!dragging) return; - const h = Math.min(Math.max(startH + (startY - e.clientY), 80), Math.round(innerHeight * 0.6)); + h = clamp(startH + (startY - e.clientY)); body.style.height = h + 'px'; }); document.addEventListener('mouseup', () => { if (!dragging) return; dragging = false; grip.classList.remove('dragging'); - localStorage.setItem('bench-log-h', body.offsetHeight); + localStorage.setItem('bench-log-h', h); }); body.addEventListener('scroll', () => { S.logStick = body.scrollTop + body.clientHeight >= body.scrollHeight - 8; diff --git a/tests/test_log_resize.py b/tests/test_log_resize.py new file mode 100644 index 0000000..2c73b1a --- /dev/null +++ b/tests/test_log_resize.py @@ -0,0 +1,103 @@ +"""The activity log's resize grip actually resizes (task 04). + +The grip's drag handlers wrote #logbody's inline height while the CSS said +flex:1 — so the flex algorithm sized the element and the write was dead. +Worse, mouseup persisted a re-read offsetHeight (the flex-computed value), +silently overwriting the remembered size with the status quo. + +board.html is a single file with inline JS and no frontend test runner, so +these are source-level invariants over the rule and the handler block: the +ones that, if broken, would decouple the written height from the laid-out +height again, or bring back the offsetHeight re-read that corrupted the +stored size. + + python3 -m unittest discover -s tests -v +""" + +from __future__ import annotations + +import re +import unittest +from pathlib import Path + +BOARD = Path(__file__).resolve().parents[1] / "manager" / "core" / "board.html" + + +class LogResizeTests(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.html = BOARD.read_text(encoding="utf-8") + + m = re.search(r"#logbody\{([^}]*)\}", cls.html) + assert m, "board.html lost its #logbody rule" + cls.logbody = m.group(1).replace(" ", "") + + # the whole activity-log drag block, from its banner comment to the + # drawer of the next top-level comment + m = re.search( + r"/\* the activity log:.*?\n\{(.*?)\n\}", cls.html, re.S) + assert m, "board.html lost the activity-log resize block" + cls.logjs = m.group(1) + + m = re.search(r"/\* the drawer:.*?\n\{(.*?)\n\}", cls.html, re.S) + assert m, "board.html lost the drawer resize block" + cls.drawerjs = m.group(1) + + # ── the height the handler writes is the height the layout uses ── + + def test_logbody_height_is_authoritative(self): + """flex:none (0 0 auto): the inline height the drag handlers write + sizes the element. flex:1 (basis 0% + grow) is what made the write + dead — the flex algorithm never consulted the height property.""" + self.assertIn("flex:none", self.logbody, + "#logbody must not be flex-sized; the grip writes " + "style.height and the layout must honour it") + self.assertNotIn("flex:1", self.logbody) + + def test_logbody_growth_is_guarded(self): + """With height authoritative again, a huge saved value or a shrunk + window must not overflow: CSS max-height mirrors the drag clamp's + 60vh ceiling.""" + self.assertIn("max-height:60vh", self.logbody) + + def test_drag_clamp_bounds_survive(self): + """The advertised 80px–60vh drag bounds live in the handler block.""" + self.assertRegex(self.logjs, r"Math\.max\(px,\s*80\)") + self.assertRegex(self.logjs, r"innerHeight\s*\*\s*0\.6\b") + + # ── the saved size round-trips instead of being overwritten ── + + def test_mouseup_persists_the_computed_height(self): + """mouseup must save the value the drag computed. Re-reading + offsetHeight is the old bug: while the write was dead it persisted + the flex-computed height, corrupting the stored size (856 → 156).""" + save = re.search(r"setItem\('bench-log-h',\s*([^)]*)\)", self.logjs) + self.assertIsNotNone(save, "the log block must persist bench-log-h") + self.assertNotIn("offsetHeight", save.group(1), + "persist the drag-computed value, not a re-read") + + def test_restore_uses_the_drag_channel_and_clamp(self): + """The load-time restore writes the same property the drag writes, + through the same clamp — a saved value from a taller window must + not restore beyond today's 60vh.""" + self.assertRegex( + self.logjs, r"clamp\([^)]*getItem\('bench-log-h'", + "the restore must clamp the saved height") + writes = re.findall(r"body\.style\.(\w+)\s*=", self.logjs) + self.assertTrue(writes, "the block must size #logbody") + self.assertEqual(set(writes), {"height"}, + "restore and drag must write the same property") + + # ── the drawer grip: same interaction family, must not regress ── + + def test_drawer_width_is_still_authoritative(self): + """The drawer handlers write #drawer's own width; #drawer is + position:fixed, so no flex sizing competes with the write.""" + drawer = re.search(r"#drawer\{([^}]*)\}", self.html).group(1) + self.assertIn("position:fixed", drawer.replace(" ", "")) + self.assertIn("panel.style.width", self.drawerjs) + self.assertIn("setItem('bench-drawer-w'", self.drawerjs) + + +if __name__ == "__main__": + unittest.main()