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) <noreply@anthropic.com>
This commit is contained in:
Jaroslav Beneš
2026-07-21 18:36:41 +02:00
parent 7456525d19
commit a8b7b5fc14
5 changed files with 94 additions and 15 deletions
+51 -10
View File
@@ -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 <section> and the alerts above them
are <div> -- 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);
+4 -3
View File
@@ -61,8 +61,9 @@
<input class="input" id="stt-model" name="stt_model"
value="{{ values.stt_model }}" placeholder="whisper-1">
<p class="field__hint">
Sent even to servers that only host one; a router in front of several
needs it.
A server hosting one model <strong>ignores this</strong> and uses
whatever it was started with — <code>whisper-1</code> is then just a
label. It only selects anything on a server that hosts several.
</p>
</div>
<div class="field">
@@ -179,6 +180,6 @@
<div id="audio-test-tts"></div>
</section>
<div class="btn-row"><button class="btn btn--primary" type="submit">Save settings</button></div>
<div class="form-actions"><button class="btn btn--primary" type="submit">Save settings</button></div>
</form>
{% endblock %}
+1 -1
View File
@@ -84,6 +84,6 @@
</p>
</section>
<div class="btn-row"><button class="btn btn--primary" type="submit">Save settings</button></div>
<div class="form-actions"><button class="btn btn--primary" type="submit">Save settings</button></div>
</form>
{% endblock %}
+1 -1
View File
@@ -128,7 +128,7 @@
</div>
</section>
<div class="btn-row"><button class="btn btn--primary" type="submit">Save settings</button></div>
<div class="form-actions"><button class="btn btn--primary" type="submit">Save settings</button></div>
</form>
<section class="card">