From 3ad4c82b86f70305d718bdf6e9b62ee68cb3c4f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaroslav=20Bene=C5=A1?= Date: Tue, 21 Jul 2026 18:36:41 +0200 Subject: [PATCH] Fix the settings tabs, and space a form from what follows it **The Audio tab rendered nothing.** The tabs are radios plus sibling selectors, and the CSS named every tab twice -- once to highlight its label, once to show its panel. A tab added without also adding those two rules gets a label that selects nothing, which is not something anyone catches in review; it looks like a blank page. Replaced with rules that derive what they can. The active label is `input:checked + .tabs__tab`, which needs to know nothing at all. The panel is matched by position -- CSS cannot compare a radio's id with a panel's data-tab -- so the Nth radio shows the Nth panel. Both lists render in the same order and a conditional tab drops out of both at once, so they cannot drift. There is a test asserting the two orders match, including with Audio absent. **A card following a form sat flush against Save.** The "Try it" panel on the search page read as another field of the settings form. The gap belongs to the form rather than to its action row: the action row is always its form's last child, so a bottom margin there has nothing to push away from. Adds `.form-actions` and a bottom margin on a form that is a direct child of an admin page. Also says plainly in the dictation settings that a server hosting one model ignores the model field, so `whisper-1` there is a label rather than a selection. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/lembas/web/static/css/admin.css | 61 +++++++++++++++++---- src/lembas/web/templates/admin/audio.html | 7 ++- src/lembas/web/templates/admin/general.html | 2 +- src/lembas/web/templates/admin/search.html | 2 +- tests/test_audio.py | 37 +++++++++++++ 5 files changed, 94 insertions(+), 15 deletions(-) diff --git a/src/lembas/web/static/css/admin.css b/src/lembas/web/static/css/admin.css index 9b48b21..2d77d7e 100644 --- a/src/lembas/web/static/css/admin.css +++ b/src/lembas/web/static/css/admin.css @@ -72,23 +72,64 @@ .tabs__panel { display: none; } -/* Each radio activates its own label and its own panel. Written out because - CSS has no way to derive one from the other. */ -#tab-account:checked ~ label[for="tab-account"], -#tab-models:checked ~ label[for="tab-models"], -#tab-appearance:checked ~ label[for="tab-appearance"], -#tab-security:checked ~ label[for="tab-security"] { +/* The active tab's label. Each radio is immediately followed by its own label, + so this needs to know nothing about how many tabs there are or what they are + called. */ +.tabs__bar input:checked + .tabs__tab { color: var(--ink); border-bottom-color: var(--accent); } -.tabs__bar:has(#tab-account:checked) ~ .tabs__body [data-tab="tab-account"], -.tabs__bar:has(#tab-models:checked) ~ .tabs__body [data-tab="tab-models"], -.tabs__bar:has(#tab-appearance:checked) ~ .tabs__body [data-tab="tab-appearance"], -.tabs__bar:has(#tab-security:checked) ~ .tabs__body [data-tab="tab-security"] { + +/* + The active panel, matched by position. + + CSS cannot compare a radio's id with a panel's data-tab, so this used to name + every tab twice -- and a tab added without also adding its two rules here + rendered a label that selected nothing. That is not a failure anyone spots in + review; it looks like a blank page. + + Position is derivable, so it is used instead: the Nth radio shows the Nth + panel. Both lists are rendered in the same order, and a conditional tab drops + out of both at once, so they cannot drift apart. :nth-of-type counts by + element name, which is why the panels are
and the alerts above them + are
-- the alerts are not counted. + + Enumerated to eight, comfortably more than exist. A ninth tab needs one line. +*/ +.tabs__bar:has(input:nth-of-type(1):checked) ~ .tabs__body .tabs__panel:nth-of-type(1), +.tabs__bar:has(input:nth-of-type(2):checked) ~ .tabs__body .tabs__panel:nth-of-type(2), +.tabs__bar:has(input:nth-of-type(3):checked) ~ .tabs__body .tabs__panel:nth-of-type(3), +.tabs__bar:has(input:nth-of-type(4):checked) ~ .tabs__body .tabs__panel:nth-of-type(4), +.tabs__bar:has(input:nth-of-type(5):checked) ~ .tabs__body .tabs__panel:nth-of-type(5), +.tabs__bar:has(input:nth-of-type(6):checked) ~ .tabs__body .tabs__panel:nth-of-type(6), +.tabs__bar:has(input:nth-of-type(7):checked) ~ .tabs__body .tabs__panel:nth-of-type(7), +.tabs__bar:has(input:nth-of-type(8):checked) ~ .tabs__body .tabs__panel:nth-of-type(8) { display: block; } .tabs__tab:has(:focus-visible) { outline: 2px solid var(--accent); outline-offset: -2px; } +/* + A form's action row, and the space after the form it closes. + + Not a plain .btn-row: a settings form is a stack of cards, and Save has to + read as belonging to the cards above it rather than to whatever comes next. + The gap that matters is the one *after* the form -- a card following it (the + "Try it" panel on the search page, say) otherwise sits flush against Save and + looks like another field of the same form. That margin therefore belongs to + the form, not to the action row: the action row is always its form's last + child, so a bottom margin there would have nothing to push away from. +*/ +.form-actions { + display: flex; + align-items: center; + gap: var(--sp-2); + flex-wrap: wrap; + margin-top: var(--sp-5); +} + +.admin-page > form { margin-bottom: var(--sp-8); } +.admin-page > form:last-child { margin-bottom: 0; } + /* --- Cards ----------------------------------------------------------------- */ .card { background: var(--surface); diff --git a/src/lembas/web/templates/admin/audio.html b/src/lembas/web/templates/admin/audio.html index d06d865..caf500d 100644 --- a/src/lembas/web/templates/admin/audio.html +++ b/src/lembas/web/templates/admin/audio.html @@ -61,8 +61,9 @@

- Sent even to servers that only host one; a router in front of several - needs it. + A server hosting one model ignores this and uses + whatever it was started with — whisper-1 is then just a + label. It only selects anything on a server that hosts several.

@@ -179,6 +180,6 @@
-
+
{% endblock %} diff --git a/src/lembas/web/templates/admin/general.html b/src/lembas/web/templates/admin/general.html index a5884a0..83759b3 100644 --- a/src/lembas/web/templates/admin/general.html +++ b/src/lembas/web/templates/admin/general.html @@ -84,6 +84,6 @@

-
+
{% endblock %} diff --git a/src/lembas/web/templates/admin/search.html b/src/lembas/web/templates/admin/search.html index 033d9ca..c41a23d 100644 --- a/src/lembas/web/templates/admin/search.html +++ b/src/lembas/web/templates/admin/search.html @@ -128,7 +128,7 @@ -
+
diff --git a/tests/test_audio.py b/tests/test_audio.py index db6bd90..e26fae6 100644 --- a/tests/test_audio.py +++ b/tests/test_audio.py @@ -298,6 +298,43 @@ def test_the_audio_tab_appears_only_once_audio_is_configured( assert "tab-audio" in client.get("/settings").text +def _tab_lists(html: str) -> tuple[list[str], list[str]]: + import re + + return ( + re.findall(r']*name="settings-tab"[^>]*id="(tab-[a-z]+)"', html), + re.findall(r'