style(web): move the row-count select into the card header

It was sitting in a strip below the heading with a "Show" label and a "newest
first" hint — three lines of chrome for one control. The select now lives at the
right end of the card's own header bar (`margin-left:auto` inside the flex h2),
compact and case-normal so it reads as a control and does not grow the bar.
Label and hint are gone; the header text names the card, the values speak for
themselves, and screen readers keep the `aria-label`.
This commit is contained in:
2026-07-25 15:43:17 +02:00
parent fe4d749a90
commit 61c4972a88
4 changed files with 30 additions and 23 deletions
+3 -3
View File
@@ -295,9 +295,9 @@ Gitea at most one call per 10s window; the VM columns are re-read on every
refresh. refresh.
Rows are ordered **newest first** (a queued run, which has no start time yet, Rows are ordered **newest first** (a queued run, which has no start time yet,
sorts above everything; ties break on Gitea's run id). The **Show** selector in sorts above everything; ties break on Gitea's run id). The row-count dropdown at
the card's header picks how many to list — 10 / 25 / 50 / 100, default from the right end of the card's header bar picks how many to list — 10 / 25 / 50 /
`[web].executions_limit` (10). The choice rides on the card's own polling URL 100, default from `[web].executions_limit` (10). The choice rides on the card's own polling URL
(`/executions?limit=N`), so it survives every 5s refresh with no client-side (`/executions?limit=N`), so it survives every 5s refresh with no client-side
state; a value set in the config outside those four is offered as an extra state; a value set in the config outside those four is offered as an extra
option rather than dropped. option rather than dropped.
+12 -10
View File
@@ -281,17 +281,19 @@ dd code, dd .mono { overflow-wrap: anywhere; }
/* A row of buttons (e.g. Purge / Empty all) that wraps on narrow cards. */ /* A row of buttons (e.g. Purge / Empty all) that wraps on narrow cards. */
.btn-row { display: flex; flex-wrap: wrap; gap: 0.5rem; } .btn-row { display: flex; flex-wrap: wrap; gap: 0.5rem; }
/* Small control strip under a card's heading (Executions row count): right /* A control living inside a card's header bar (Executions row count), pushed to
* aligned so it reads as a view option, not as part of the data below. */ * the bar's right end. Compact and case-normal so it reads as a control rather
.card-toolbar { * than part of the uppercase heading text, and short enough not to grow the
display: flex; * bar's height. */
flex-wrap: wrap; .head-select {
align-items: center; margin-left: auto;
justify-content: flex-end; padding: 0.1rem 0.35rem;
gap: 0.5rem; font-size: 0.8rem;
margin: -0.35rem 0 0.7rem; line-height: 1.2;
text-transform: none;
letter-spacing: normal;
color: var(--text);
} }
.card-toolbar select { padding: 0.2rem 0.4rem; }
/* Per-template expandable backup list (Templates panel). */ /* Per-template expandable backup list (Templates panel). */
.backups-details > summary { .backups-details > summary {
@@ -11,15 +11,18 @@
scrolled out of the window (or that no run ever claimed) keeps its row and scrolled out of the window (or that no run ever claimed) keeps its row and
its Remove button. its Remove button.
The row-count selector re-fetches the card with ``?limit=N``, and the card it The row-count selector sits in the card's own header bar (right end) and
renders carries that same limit on its own polling URL -- that is what makes re-fetches the card with ``?limit=N``; the card it renders carries that same
the choice survive the 5s refresh without any client-side state. limit on its polling URL, which is what makes the choice survive the 5s
refresh without any client-side state. It carries no visible label -- the
header text names the card and the values are self-evident -- so screen
readers get ``aria-label`` instead.
#} #}
<section class="card card--wide" id="executions-card" hx-get="/executions?limit={{ limit }}" hx-trigger="every 5s, ci:refresh from:body" hx-swap="outerHTML"> <section class="card card--wide" id="executions-card" hx-get="/executions?limit={{ limit }}" hx-trigger="every 5s, ci:refresh from:body" hx-swap="outerHTML">
<h2><svg class="icon" aria-hidden="true"><use href="#i-play"></use></svg>Executions</h2> <h2>
<div class="card-toolbar"> <svg class="icon" aria-hidden="true"><use href="#i-play"></use></svg>Executions
<label for="executions-limit">Show</label> <select id="executions-limit" name="limit" class="head-select"
<select id="executions-limit" name="limit" aria-label="Number of executions to show" aria-label="Number of executions to show"
hx-get="/executions" hx-target="#executions-card" hx-swap="outerHTML" hx-trigger="change"> hx-get="/executions" hx-target="#executions-card" hx-swap="outerHTML" hx-trigger="change">
{% for choice in limit_choices %} {% for choice in limit_choices %}
<option value="{{ choice }}" {{ 'selected' if choice == limit else '' }}>{{ choice }}</option> <option value="{{ choice }}" {{ 'selected' if choice == limit else '' }}>{{ choice }}</option>
@@ -28,8 +31,7 @@
<option value="{{ limit }}" selected>{{ limit }}</option> <option value="{{ limit }}" selected>{{ limit }}</option>
{% endif %} {% endif %}
</select> </select>
<span class="field-hint">newest first</span> </h2>
</div>
{% if not backend_available %} {% if not backend_available %}
<p class="notice notice-warn" role="alert">Backend unavailable (vmrun not found on this host).</p> <p class="notice notice-warn" role="alert">Backend unavailable (vmrun not found on this host).</p>
{% endif %} {% endif %}
+4 -1
View File
@@ -385,7 +385,10 @@ def test_default_limit_is_ten(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -
assert limits == [10] assert limits == [10]
assert '<option value="10" selected>10</option>' in resp.text assert '<option value="10" selected>10</option>' in resp.text
assert "newest first" in resp.text # The selector lives inside the card's header bar, not in a strip below it.
head = resp.text.split("</h2>", 1)[0]
assert "head-select" in head
assert "<label" not in resp.text
def test_selector_offers_the_choices_and_marks_the_current_one( def test_selector_offers_the_choices_and_marks_the_current_one(