A crowd you can find, and a phone 65px too narrow
Two reports against 1.6.0 and 1.7.0, both correct. The crowd worked end to end and was, in practice, not there: the picker was behind the ⋯ menu of a chat that already existed, and the switch was a card on the Agents page, which made it read as an agent-chat feature. The picker is now a button in the composer toolbar on both screens that include it, and on the new-chat screen the choice rides along with the first message, so a chat can start as a crowd instead of having to be converted into one. The instance switch has its own page. The width bug was the suggestion cards, exactly as reported. `.suggestions` rendered 455px inside a 366px column, and the tree's standing rule applied on its own made it worse -- 428px to 455px. A grid item carries `min-width: auto`, which is a min-content floor, and a floor beats `width: 100%`; the floor is measured while the percentage is indefinite, so `min(100%, …)` alone sends the track to a card's max-content. Both halves now go on all four auto-fit grids, and a test refuses either alone. It survived four releases of narrow-width checking because the harness never rendered that screen: `TestClient(app)` runs no lifespan outside a `with` block, so the startup-seeded cards were missing from every shot ever taken of it. And its overflow check skipped anything inside a scroller -- right for a table in its own scroller, blind to the scroller itself, which `overflow-y: auto` makes scroll sideways too. Both fixed; it now names the box and the child to blame. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
"""A grid that reflows can still overflow a phone, and twice it has.
|
||||
|
||||
`repeat(auto-fit, minmax(13rem, 1fr))` puts two 208px cards side by side on a
|
||||
390px screen: `auto-fit` decides how many columns fit, and a track whose minimum
|
||||
is a fixed length never gives that minimum up. The tree's standing rule is to
|
||||
write `minmax(min(100%, 13rem), 1fr)` instead, so the *track* yields rather than
|
||||
the viewport.
|
||||
|
||||
Half of the rule is not the rule. `.suggestions` had `width: 100%` and got the
|
||||
`min(100%, …)` track, and still rendered 455px wide inside a 366px column -- it
|
||||
is a grid item, so it carries `min-width: auto`, which for a grid item means a
|
||||
min-content floor, and a floor beats `width`. Worse, the floor is measured while
|
||||
the percentage is indefinite, so the track falls back to a card's max-content:
|
||||
adding `min(100%, …)` on its own moved the overflow from 428px to 455px.
|
||||
|
||||
So both halves are asserted here, on every auto-fit grid in the stylesheets. A
|
||||
stylesheet cannot see whether a given grid is a flex item on some page today or
|
||||
becomes one next week, and `min-width: 0` costs nothing where it is not needed.
|
||||
|
||||
Found on a phone, on the one screen the screenshot harness had never actually
|
||||
rendered -- `scripts/shoot.py` builds its client without a lifespan, so the
|
||||
suggestion cards were absent from every shot ever taken of the new-chat screen.
|
||||
That is fixed there; this file is the cheap half that runs in the suite.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
import lembas
|
||||
|
||||
CSS = Path(lembas.__file__).parent / "web/static/css"
|
||||
|
||||
# `[^{}]*` cannot cross a brace, so an `@media` prelude never matches and the
|
||||
# rules nested inside it do.
|
||||
RULE = re.compile(r"([^{}]*)\{([^{}]*)\}")
|
||||
COMMENT = re.compile(r"/\*.*?\*/", re.S)
|
||||
|
||||
|
||||
def _auto_grids() -> list[tuple[str, str, dict[str, str]]]:
|
||||
found = []
|
||||
for path in sorted(CSS.glob("*.css")):
|
||||
text = COMMENT.sub("", path.read_text(encoding="utf-8"))
|
||||
for prelude, body in RULE.findall(text):
|
||||
if "auto-fit" not in body and "auto-fill" not in body:
|
||||
continue
|
||||
declarations = {
|
||||
part.partition(":")[0].strip(): part.partition(":")[2].strip()
|
||||
for part in body.split(";")
|
||||
if ":" in part
|
||||
}
|
||||
found.append((path.name, prelude.strip(), declarations))
|
||||
return found
|
||||
|
||||
|
||||
def test_the_stylesheets_still_have_auto_fit_grids_to_check():
|
||||
# Otherwise the two tests below pass by finding nothing, which is how a
|
||||
# coverage test quietly stops covering anything.
|
||||
assert len(_auto_grids()) >= 4
|
||||
|
||||
|
||||
def test_every_auto_fit_track_can_give_up_its_minimum():
|
||||
offenders = [
|
||||
f"{name} {selector}"
|
||||
for name, selector, declarations in _auto_grids()
|
||||
for value in [declarations.get("grid-template-columns", "")]
|
||||
if "minmax(" in value and "min(100%" not in value
|
||||
]
|
||||
assert not offenders, (
|
||||
"an auto-fit track with a fixed minimum overflows a phone; write "
|
||||
f"minmax(min(100%, X), 1fr): {offenders}"
|
||||
)
|
||||
|
||||
|
||||
def test_every_auto_fit_grid_drops_its_automatic_minimum_size():
|
||||
offenders = [
|
||||
f"{name} {selector}"
|
||||
for name, selector, declarations in _auto_grids()
|
||||
if declarations.get("min-width") != "0"
|
||||
]
|
||||
assert not offenders, (
|
||||
"a grid item's `min-width: auto` is a min-content floor and beats "
|
||||
f"`width`, so `min(100%, …)` alone does not save it: {offenders}"
|
||||
)
|
||||
Reference in New Issue
Block a user