phase: 176_mobile_settings_value_width
Build and Push Containers / build-and-push-app (push) Successful in 1m34s
Build and Push Containers / build-and-push-db (push) Successful in 14s

All documentation claims verify against the code. Everything is green — final verification complete.

## Phase 176 — Final Verification Report

- **Verified (all 3 tasks in `complete/`):** the mobile Value-column fix is in the working tree — `frontend/index.html` L1602 carries `class="settings-value-th"`; `frontend/assets/styles.css` has exactly two new rules inside the `@media (max-width: 640px)` block: `.settings-table { min-width: 720px }` and `.settings-value-cell, .settings-value-th { min-width: calc(13rem + 2rem) }`; `settings.js` + `app/` byte-unchanged (diff = index.html + styles.css + unit test only).
- **Documented deviation (flagged in task-03 report, not silent):** spec's `min-width: 13rem` on the border-box cell left the control at 176px (< the 192px A3 pin); executor corrected the cell to `calc(13rem + 2rem)` so the control (100% of content) lands at exactly 13rem ≈ 208px — the A2 lock intent preserved, unit pin + CSS comment updated to match.
- **Tests/lint/coverage (exact commands, all green):** `uv run pytest --cov=app` → **4138 passed**, `app/` coverage **99%** (>90% ✓); `uv run ruff check .` → clean; `uv run pyright` → 0 errors, 0 warnings.
- **Completion criteria:** 375px controls ≥ 192px — ✅ (E2E `test_mobile_value_inputs_are_usable_width`); card scrolls / page doesn't overflow at 375px — ✅ (`test_mobile_table_still_scrolls_and_page_does_not_overflow`); 1440px media query inert — ✅ (`test_desktop_value_width_is_unchanged`); tab order + zero external requests — ✅; phase E2E isolated — ✅ 4/4; phase 171 isolated — ✅ 5/5; phase 168 isolated — ✅ 12/12; unit pins — ✅ 17/17; commit/move — the harness commits; phase dir already in `complete/`.
- **Documentation:** README.md and AGENTS.md reviewed in full — every claim spot-checked against code (E2E ports 8123/8901, defaults `BOR_COMPRESS_MIN_BYTES=860` / `BOR_LOG_LARGE_RESPONSE_BYTES=65536`, audit budget 262144, `.cell-clip`, `.bubble:empty`, `min(70vh, 64rem)`, aria-pressed toggle, CDN integration test, `.gitignore` entries, all referenced scripts/routes) — verified consistent, no changes needed.
- **Notable:** `image-1.png` (owner's TODO screenshot, referenced by the phase overview) sits untracked in the tree — left alone; not part of the phase's commit scope.
- **Next pending phase:** `177_themed_favicon`.
This commit is contained in:
2026-10-11 13:23:04 -04:00
parent 2a5df7cb31
commit dd119d534a
24 changed files with 1329 additions and 4 deletions
+121 -2
View File
@@ -224,11 +224,15 @@ def test_settings_content_scaffold_and_feedback_regions() -> None:
assert '<caption class="visually-hidden">' in thead, (
"the caption is visually-hidden (the phase-86 table pattern)"
)
assert thead.count('<th scope="col">') == 4, (
assert thead.count('<th scope="col"') == 4, (
"the table is four columns (Setting | Value | Source | Actions)"
)
assert '<th scope="col">Setting</th>' in thead
assert '<th scope="col">Value</th>' in thead
assert '<th scope="col" class="settings-value-th">Value</th>' in thead, (
"phase 176: the Value th carries the .settings-value-th class "
"(the mobile 13rem column floor constrains the header cell "
"under auto table layout, not just the td's)"
)
assert '<th scope="col">Source</th>' in thead
assert (
'<th scope="col"><span class="visually-hidden">Actions</span></th>'
@@ -764,6 +768,121 @@ def test_settings_save_disabled_is_just_disabled_no_wait_cursor() -> None:
)
# ---------- phase 176 (task 01): the mobile Value-column floor ----------
def test_settings_mobile_value_column_floor() -> None:
"""Phase 176 (task 01; TODO.md L3, owner-locked A1/A2): on a
phone (≤640px) the Settings table's Value inputs rendered 2–3
characters wide — the Setting cells' nowrap labels (the
``.settings-table td`` rule) exhausted the table's 560px floor
and collapsed the ``min-width: 0`` Value cells. The owner's
direction: "the table is horizontally scrollable, so we can have
slightly wider input fields." Pinned here (static, no browser):
* the Value column — ``.settings-value-cell`` (the td) and
``.settings-value-th`` (the th; task 01 added the class so the
column floor constrains the header cell too, under auto table
layout) — gets a ``min-width: calc(13rem + 2rem)`` floor: the
global border-box model (``* { box-sizing: border-box }``)
makes a cell's min-width a BORDER-box, so the cell is floored
at the locked 13rem of CONTENT (≈208px ≈ 14 mono chars at the
controls' 0.85rem) plus the td's own 1rem × 2 padding — the
``.settings-control`` (``width: 100%`` of the cell's content)
thus lands at the locked 13rem (the A2 intent, "slightly
wider input fields", and the A3 pin's 12rem / 192px threshold
with a 16px margin); a bare ``13rem`` border-box would only
leave 176px of content (the control), under the A3 pin, and
* the table's mobile scroll floor is raised to ``720px`` while
the base rule keeps ``560px`` (≥641px — the desktop layout
stays byte-identical, A1/A2).
Both rules live ONLY inside the existing ``@media (max-width:
640px)`` block (A1 — mobile-only scope): the selectors occur
exactly once in the whole file, and that one occurrence sits
inside the media block (the slice is by open/close markers —
the inner rules are 2-space-indented, so the first column-zero
``}`` after the open is the block's close; no hardcoded line
range, so the pin survives an insertion above it). The 720px
floor + the ``.table-wrap`` card's ``overflow: auto`` mean the
phone scrolls the CARD, never the page (the phase-171 "no
horizontal page overflow" E2E pin holds). 720px is a bare
length literal and the cell floor is the locked 13rem of content
+ the cell's 2rem of padding — NO new CSS custom property (B3
binds palette variables; a length in the mobile block is the
idiom the neighbouring ``width: 100%`` rules already use), so
``test_styles_css_settings_block_uses_existing_tokens_only``
stays green unchanged."""
css = _read(ASSETS / "styles.css")
# --- the ≤640px block, sliced by its open/close markers.
open_marker = "@media (max-width: 640px) {"
mobile_open = css.index(open_marker)
mobile_close = css.index("\n}", mobile_open + len(open_marker))
mobile = css[mobile_open:mobile_close + 2]
# --- the Value rule: exactly ONCE in the whole file, and that
# one occurrence sits INSIDE the ≤640px block (A1: no desktop
# copy, no stray rule elsewhere).
value_selector = ".settings-value-cell, .settings-value-th"
assert css.count(value_selector) == 1, (
"the Value rule must occur exactly once in styles.css"
)
assert mobile.count(value_selector) == 1, (
"the Value rule must sit inside the ≤640px block (A1: "
"mobile-only — at ≥641px the rule is inert)"
)
value_rule = re.search(
r"\.settings-value-cell, \.settings-value-th \{([^{}]*)\}", mobile
)
assert value_rule, "the Value rule must declare the column floor"
assert "min-width: calc(13rem + 2rem)" in value_rule.group(1), (
"the Value column (th AND td) is floored at 13rem of content "
"(≈208px ≈ 14 mono chars — A2: 'slightly wider input fields') "
"+ the cell's 2rem of padding (the global border-box model — "
"a bare 13rem border-box would leave the control at 176px, "
"under the A3 192px pin)"
)
# --- the table floor: BOTH rules pinned — the base keeps 560px
# for ≥641px (and must not know 720px) while the mobile block
# raises it to 720px (the phone scrolls the card — the
# .table-wrap overflow: auto contract, never the page).
base = re.search(r"\.settings-table \{([^{}]*)\}", css)
assert base, "styles.css must style .settings-table (the base rule)"
assert "min-width: 560px" in base.group(1), (
"the base .settings-table floor stays 560px (≥641px — "
"unchanged, A1)"
)
assert "720px" not in base.group(1), (
"the base .settings-table rule must not know 720px (the "
"720px floor belongs to the mobile block only, A1)"
)
mtable = re.search(r"\.settings-table \{([^{}]*)\}", mobile)
assert mtable, (
"the ≤640px block must carry the .settings-table floor rule"
)
assert "min-width: 720px" in mtable.group(1), (
"the mobile table floor is 720px (A2: the phone scrolls "
"the card horizontally)"
)
# The 720px declaration exists exactly once in the whole file —
# and it is the mobile block's (no ≥641px copy anywhere).
assert css.count("min-width: 720px") == 1 and (
"min-width: 720px" in mobile
), "the 720px floor must exist exactly once, in the ≤640px block"
# --- the index.html static thead: the Value th carries the
# .settings-value-th class so the column floor constrains the
# header cell too (deterministic under auto table layout —
# a plain <th> would otherwise stay unconstrained).
html = _html()
table_start = html.find('class="settings-table"')
assert table_start != -1, "the settings table must exist"
thead = html[table_start: html.find("</thead>", table_start)]
assert '<th scope="col" class="settings-value-th">Value</th>' in thead, (
"the settings thead's Value th carries the settings-value-th "
"class (task 01: the column floor is deterministic under "
"auto table layout)"
)
# ---------- the four Theme-bearing pages carry the Settings link ----------