aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-09-10 12:19:30 +0200
committerChristophe Besson <cbesson@gmail.com>2026-09-10 12:19:30 +0200
commit45d63060a975473367a0f0312572b349a5c448a4 (patch)
treeef5abf161ec9d8441d793bdc96d790885035cf25
parentc2bc79f71c553f3c8e76c592715266c8469e583e (diff)
downloadmeshbay-45d63060a975473367a0f0312572b349a5c448a4.tar.gz
fix(hub): a pinned band's ring stops eating the form above it
The tab bar paints an opaque ring of page colour around itself, `--band-margin` wide, so the gap it keeps in the flow is still there once it pins. A box-shadow spread goes out on all four sides, and above the tab bar there is only whatever the element before it happened to leave: the join-code form under a group's title leaves 12px, the ring is 16px, and the form came back with the bottom 4px of its field and its button painted over — page colour at z-index 30, against content that has none to answer with. Reported as "the form is slightly cut off", which is exactly what it looks like and says nothing about a stylesheet. The same 4px went off the bottom of the "could not reach this node" banner, the other thing that stands between a group's title and its tabs. The band reserves that room itself now. `* +`, so it is the gap between two elements rather than a margin the band always carries: `.search-bar` is a first child on the Search page, and a margin-top there would collapse through the page root and take the whole page down with it. Between siblings the two margins collapse to the larger of the pair, so everywhere that already leaves enough is untouched and only what was being painted over moves. Only the two bands that pin against the navigation bar, and that is the rule rather than an economy. Written for all six it fails 22 of the sticky-header cases: the gap *between* two bands is the upper one's `--band-margin` and nothing else — the number `--chrome-h` carries and the offset the lower band pins at — so a lower band's own margin-top wins the collapse wherever it is the bigger of the two and leaves the flow layout wider than the pinned one, at every phone width in every media view. A band under another band needs no room above it anyway: what is there is a band of higher z-index, which a ring cannot paint over. Measured against the shipped GroupPage in the state that was reported — a node answering `code_required` — in Chrome: 12px of clearance under a 16px ring before, 16 against 16 after. test_sticky_band_ring.py holds the two selector lists together out of the source rather than in a browser, because what a browser shows is the 4px at one width in one of the states that happen to put something above a band, while what has to hold is which bands are in which list. docs/apps.md sends the author of a new application to that section to make its toolbar pin; this is what says the toolbar they add does not get the gap, and why. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BAJawZ25MZPBJ7TKgnVA1n
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/style.css32
-rw-r--r--packages/meshbay-hub/tests/test_sticky_band_ring.py109
2 files changed, 141 insertions, 0 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/style.css b/packages/meshbay-hub/src/meshbay_hub/static/style.css
index 774c9bc..d51e5b0 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/style.css
+++ b/packages/meshbay-hub/src/meshbay_hub/static/style.css
@@ -578,6 +578,38 @@ a:hover { text-decoration: underline; }
box-shadow: 0 0 0 var(--band-margin) var(--bg-base);
}
+/* **And the same gap above the first one.** The ring is painted on all four
+ sides, which is what the corners need — so a band has to have
+ `--band-margin` of clearance above it too, or it paints page colour over
+ whatever is there. Every band but the first has that for free: what sits
+ above it is the band it pins under, at a higher z-index, and a ring cannot
+ paint over that. The first has the page's own content above it, and nothing
+ was making the two agree — the join-code form under a group's title leaves
+ 12px, the tab bar's ring is 16px, and the form was handed back with the
+ bottom 4px of its field and its button painted over. The same 4px went off
+ the bottom of the "could not reach this node" banner, which is the other
+ thing that stands between the title and the tabs.
+
+ Only the first band, and that is the whole of the rule rather than an
+ economy: the gap *between* two bands is the upper one's `--band-margin` and
+ nothing else — it is the number `--chrome-h` carries and the offset the
+ lower one pins at. A margin-top on a lower band would collapse to the
+ larger of the pair and win wherever it was bigger, which is most widths,
+ leaving the flow a couple of pixels wider than the pinned layout. Measured,
+ in every media view at every phone width, the moment this rule was written
+ for all six.
+
+ `* +`, so this is the gap between two elements and not a margin the band
+ always carries: as a first child — the Search page's field is one — a
+ margin-top would collapse through the page root and take the whole page
+ down with it. Between siblings the two margins collapse to the larger of
+ the pair, so every place that already leaves enough is untouched, and only
+ what was being painted over moves. */
+.sticky-chrome > * + .group-tabs,
+.sticky-chrome > * + .search-bar {
+ margin-top: var(--band-margin);
+}
+
/* ── Cards ────────────────────────────────────────────────────────────────── */
.card {
diff --git a/packages/meshbay-hub/tests/test_sticky_band_ring.py b/packages/meshbay-hub/tests/test_sticky_band_ring.py
new file mode 100644
index 0000000..2d57822
--- /dev/null
+++ b/packages/meshbay-hub/tests/test_sticky_band_ring.py
@@ -0,0 +1,109 @@
+"""
+The first band's opaque ring falls on page background, never on page content.
+
+A pinned band paints a ring of page colour around itself, `--band-margin` wide,
+so the gap it keeps in the flow is still there once it pins and nothing shifts
+at the moment it does (style.css, "Sticky chrome"). The ring is drawn on all
+four sides — it has to be, or the corners a border radius leaves stay
+transparent and a row slides visibly through them.
+
+So a band needs that much clearance *above* it as well as below. Every band but
+the first has it for free: what is above it is the band it pins under, at a
+higher z-index, which a ring cannot paint over. The first band has the page's
+own content above it, and for a while nothing made the two agree — the
+join-code form under a group's title leaves 12px, the tab bar's ring is 16px,
+and the form came back with the bottom 4px of its field and its button painted
+over. Reported as "the form is slightly cut off", which is exactly what it
+looks like and says nothing about a stylesheet to whoever reports it.
+
+Both halves are asserted, and the second is the one with teeth. A margin-top on
+a *lower* band is not a harmless extra: the gap between two bands is the upper
+one's `--band-margin` and nothing else — the number `--chrome-h` carries and
+the offset the lower band pins at — and two adjoining margins collapse to the
+larger, so a lower band's own margin-top wins wherever it is bigger and leaves
+the flow a couple of pixels wider than the pinned layout. That is what writing
+this rule for all six bands did, in every media view at every phone width,
+before `test_sticky_header.py` measured it.
+
+Read out of the stylesheet rather than measured in a browser, deliberately.
+A browser shows the 4px at one width, in one of the states that happen to put
+something above a band; what has to hold is which bands are in which of two
+lists, and that is a fact about the source. `docs/apps.md` sends the author of
+a new application here to make its toolbar pin, and this is what says whether
+the toolbar they add needs the gap — it does not, and it must not have it.
+"""
+
+import re
+from pathlib import Path
+
+import pytest
+
+STYLE = (Path(__file__).resolve().parents[1] / "src" / "meshbay_hub"
+ / "static" / "style.css")
+
+pytestmark = pytest.mark.skipif(not STYLE.exists(),
+ reason="the stylesheet is not in this checkout")
+
+COMMENT = re.compile(r"/\*.*?\*/", re.S)
+RULE = re.compile(r"([^{}]+)\{([^{}]*)\}", re.S)
+# `.sticky-chrome > .group-tabs` and `.sticky-chrome > * + .group-tabs` name the
+# same band; which of them is written says which list the band is in.
+BAND = re.compile(r"^\.sticky-chrome\s*>\s*(?:\*\s*\+\s*)?(\S+)$")
+
+
+def _bands(declares) -> set[str]:
+ """The bands named by every rule whose body `declares` says yes to."""
+ source = COMMENT.sub("", STYLE.read_text(encoding="utf-8"))
+ found = set()
+ for selectors, body in RULE.findall(source):
+ if not declares(body):
+ continue
+ for selector in selectors.split(","):
+ match = BAND.match(" ".join(selector.split()))
+ if match:
+ found.add(match.group(1))
+ return found
+
+
+def _ringed() -> set[str]:
+ return _bands(lambda body: "box-shadow" in body
+ and "var(--band-margin)" in body)
+
+
+def _first_bands() -> set[str]:
+ """The bands that pin against the navigation bar rather than another band.
+
+ `top: var(--nav-h)` on its own, where every band below one of these pins at
+ a `calc()` that adds the heights above it.
+ """
+ return _bands(lambda body: re.search(
+ r"top:\s*var\(--nav-h\)\s*;", body))
+
+
+def _gapped() -> set[str]:
+ return _bands(lambda body: re.search(
+ r"margin-top:\s*var\(--band-margin\)", body))
+
+
+def test_the_band_under_the_navigation_bar_reserves_the_gap_its_ring_needs():
+ ringed, first = _ringed(), _first_bands()
+ assert ringed and first, (
+ "no band paints a ring, or none pins against the navigation bar — "
+ "either the mechanism is gone, in which case this test should be too, "
+ "or it was renamed and nothing here is being checked")
+
+ unguarded = sorted((ringed & first) - _gapped())
+ assert not unguarded, (
+ "these bands have the page's own content above them and paint a ring "
+ "of page colour on all four sides, but nothing keeps that much room "
+ "above them, so the ring lands on the content: " + ", ".join(unguarded))
+
+
+def test_no_band_under_another_band_reserves_one():
+ """The gap between two bands is the upper one's, and only the upper one's."""
+ lower = sorted(_gapped() - _first_bands())
+ assert not lower, (
+ "these bands pin under another band, so nothing can paint over them "
+ "and they need no room above — and the margin-top they have collapses "
+ "with the upper band's, taking the flow layout wider than the pinned "
+ "one wherever it is the larger of the two: " + ", ".join(lower))