fix(panel): open an application's panels under its row of controls
test / test (push) Has been cancelled
test / test (push) Has been cancelled
Mode and rate limit were <details>, so each opened where its own toggle sat and split the row of four controls in two, pushing New password and Delete below a block of fields. The toggle is now a hidden checkbox with its label drawn as the button and the panel is the last child of the row, so the controls keep their places and the fields are laid out beneath all of them. Pure CSS, so it still works with JavaScript blocked, as the disclosure did. The panel buttons take back the form spacing the compact row style zeroes out, and Save limit and Remove limit share one row: two posts mean two forms, so the first button is bound to its form by the form attribute instead of sitting inside it. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -45,6 +45,19 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); version
|
|||||||
wrapping rows it used to be — no drawer and no hamburger, since six entries
|
wrapping rows it used to be — no drawer and no hamburger, since six entries
|
||||||
fit. The markup now lists the blocks in the order they are drawn, so the tab
|
fit. The markup now lists the blocks in the order they are drawn, so the tab
|
||||||
order follows the eye instead of starting at Sign out.
|
order follows the eye instead of starting at Sign out.
|
||||||
|
- An application's mode and rate-limit fields open under its row of controls
|
||||||
|
instead of inside it. Both panels were `<details>`, so each opened where its
|
||||||
|
own toggle sat and cut the row of four in half, pushing New password and
|
||||||
|
Delete below a block of fields — the buttons moved every time a panel was
|
||||||
|
opened or closed. The toggle is now a hidden checkbox with its label drawn as
|
||||||
|
the button, and the panel is the last child of the row, so the four controls
|
||||||
|
keep their places and what a panel reveals is laid out beneath all of them.
|
||||||
|
It stays keyboard-reachable and, being pure CSS, still works with JavaScript
|
||||||
|
blocked, as the disclosure did. Inside a panel the submit buttons take the
|
||||||
|
ordinary form spacing back from the compact row style that was leaving them
|
||||||
|
flush against the field above, and Save limit and Remove limit — two posts,
|
||||||
|
hence two forms — share one row, the first button bound to its form by the
|
||||||
|
`form` attribute rather than by sitting inside it.
|
||||||
|
|
||||||
## [0.5.0] - 2026-08-06
|
## [0.5.0] - 2026-08-06
|
||||||
|
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ input {
|
|||||||
button: a <button>, or an <a> carrying .btn/.danger where the action is a
|
button: a <button>, or an <a> carrying .btn/.danger where the action is a
|
||||||
plain navigation (the delete confirmation page, the full queue view).
|
plain navigation (the delete confirmation page, the full queue view).
|
||||||
Several of these used to render as bold blue text instead — a POST wrapped
|
Several of these used to render as bold blue text instead — a POST wrapped
|
||||||
in form.inline, a <details> toggle, the delete links — which read as links
|
in form.inline, a disclosure toggle, the delete links — which read as links
|
||||||
and left two appearances for the same kind of control. They all get the
|
and left two appearances for the same kind of control. They all get the
|
||||||
button look now: filled for a card's own action, and the compact outlined
|
button look now: filled for a card's own action, and the compact outlined
|
||||||
variant further down where actions cluster (table rows, the nav bar). Bare
|
variant further down where actions cluster (table rows, the nav bar). Bare
|
||||||
@@ -154,10 +154,9 @@ textarea { resize: vertical; }
|
|||||||
button.danger, a.danger { background: #b42318; }
|
button.danger, a.danger { background: #b42318; }
|
||||||
button.danger:hover, a.danger:hover { background: #912018; }
|
button.danger:hover, a.danger:hover { background: #912018; }
|
||||||
/* The disclosure toggle is an action too, so it is drawn as a button (see the
|
/* The disclosure toggle is an action too, so it is drawn as a button (see the
|
||||||
compact rule below); the marker is dropped because the pressed background
|
compact rule below); it carries no marker because the pressed background
|
||||||
already shows the open state. */
|
already shows the open state. */
|
||||||
.actions summary { display: inline-block; list-style: none; cursor: pointer; }
|
.actions > label.toggle { display: inline-block; cursor: pointer; }
|
||||||
.actions summary::-webkit-details-marker { display: none; }
|
|
||||||
/* Applications are a list of blocks, not table rows. As a table it fell apart:
|
/* Applications are a list of blocks, not table rows. As a table it fell apart:
|
||||||
four columns of which the last held six controls — two of them <details>
|
four columns of which the last held six controls — two of them <details>
|
||||||
panels with textareas — never fit the panel's default width. The controls
|
panels with textareas — never fit the panel's default width. The controls
|
||||||
@@ -171,10 +170,41 @@ button.danger:hover, a.danger:hover { background: #912018; }
|
|||||||
.app-login { margin: 0; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-weight: 600; }
|
.app-login { margin: 0; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-weight: 600; }
|
||||||
.app-addr { margin: 0.15rem 0 0; word-break: break-all; }
|
.app-addr { margin: 0.15rem 0 0; word-break: break-all; }
|
||||||
.app .actions { display: flex; flex-wrap: wrap; gap: 0.4rem; margin-top: 0.7rem; }
|
.app .actions { display: flex; flex-wrap: wrap; gap: 0.4rem; margin-top: 0.7rem; }
|
||||||
/* An open panel claims a row to itself: its textareas and number inputs want
|
/* Mode and rate limit open under the whole row rather than inside it. As
|
||||||
the block's full width, not the width of the summary that opened them. */
|
<details> the panel sat where its summary was, so opening one cut the row of
|
||||||
.app .actions > details[open] { flex: 1 0 100%; }
|
four controls in half and pushed the rest below a block of fields — the
|
||||||
details form { margin-top: 0.6rem; }
|
buttons moved every time a panel opened or closed. The panels are therefore
|
||||||
|
the last children of the row, and the checkbox that opens each one is
|
||||||
|
visually hidden earlier in the row with its label drawn as the button. The
|
||||||
|
checkbox stays in the tab order and keeps its focus ring on the label, so it
|
||||||
|
works from the keyboard, and being pure CSS it also works with JavaScript
|
||||||
|
blocked, as <details> did. */
|
||||||
|
.app .actions > .panel-toggle {
|
||||||
|
position: absolute; width: 1px; height: 1px; margin: 0; opacity: 0; pointer-events: none;
|
||||||
|
}
|
||||||
|
.app .actions > .panel { display: none; flex: 1 0 100%; }
|
||||||
|
.app .actions > .t-mode:checked ~ .panel-mode,
|
||||||
|
.app .actions > .t-limit:checked ~ .panel-limit { display: block; }
|
||||||
|
.app .actions > .t-mode:checked ~ .for-mode,
|
||||||
|
.app .actions > .t-limit:checked ~ .for-limit { background: var(--surface-open-bg); }
|
||||||
|
.app .actions > .t-mode:focus-visible ~ .for-mode,
|
||||||
|
.app .actions > .t-limit:focus-visible ~ .for-limit { outline: 2px solid #2563eb; outline-offset: 2px; }
|
||||||
|
.panel form { margin-top: 0.6rem; }
|
||||||
|
/* A panel's own submit is a form button, not one of the controls in the row
|
||||||
|
above, so it takes back the spacing the compact .actions rule zeroes out:
|
||||||
|
without it Save sits flush against the field it saves and against the
|
||||||
|
Remove button under it, reading as one stack of edges. */
|
||||||
|
.app .actions > .panel button { margin-top: 0.9rem; }
|
||||||
|
/* Where a panel has two of them (save the limit, remove it) they share one row
|
||||||
|
under the fields, which the row itself spaces off instead of each button.
|
||||||
|
The buttons align on their tops rather than stretching: one of the two is
|
||||||
|
wrapped in a form, and that wrapper's margin would otherwise make the row
|
||||||
|
taller than a button and stretch the unwrapped one to match it. */
|
||||||
|
.app .actions > .panel .panel-buttons {
|
||||||
|
display: flex; flex-wrap: wrap; align-items: flex-start; gap: 0.4rem; margin-top: 0.9rem;
|
||||||
|
}
|
||||||
|
.app .actions > .panel .panel-buttons button,
|
||||||
|
.app .actions > .panel .panel-buttons form { margin-top: 0; }
|
||||||
.credential { border-color: var(--credential-border); background: var(--credential-bg); }
|
.credential { border-color: var(--credential-border); background: var(--credential-bg); }
|
||||||
/* Panel navigation: rendered once from the layout, so it is present on every
|
/* Panel navigation: rendered once from the layout, so it is present on every
|
||||||
authenticated page without each content template having to include it. */
|
authenticated page without each content template having to include it. */
|
||||||
@@ -321,15 +351,14 @@ meter { width: 5rem; height: 0.7rem; vertical-align: middle; margin-right: 0.4re
|
|||||||
overrides this with .danger below since signing out is a deliberate,
|
overrides this with .danger below since signing out is a deliberate,
|
||||||
singular action. .actions is the shared hook: a cell that holds controls, or
|
singular action. .actions is the shared hook: a cell that holds controls, or
|
||||||
the control row of an application. */
|
the control row of an application. */
|
||||||
button.copy, .actions button, .actions summary, .actions a.danger, .nav button {
|
button.copy, .actions button, .actions > label.toggle, .actions a.danger, .nav button {
|
||||||
margin: 0; padding: 0.45rem 0.7rem; font-size: 0.8rem; font-weight: 600;
|
margin: 0; padding: 0.45rem 0.7rem; font-size: 0.8rem; font-weight: 600;
|
||||||
border-radius: 6px; white-space: nowrap;
|
border-radius: 6px; white-space: nowrap;
|
||||||
background: var(--surface-bg); color: #2563eb; border: 1px solid var(--control-border);
|
background: var(--surface-bg); color: #2563eb; border: 1px solid var(--control-border);
|
||||||
}
|
}
|
||||||
button.copy:hover, .actions button:hover, .actions summary:hover,
|
button.copy:hover, .actions button:hover, .actions > label.toggle:hover,
|
||||||
.actions a.danger:hover, .nav button:hover { background: var(--surface-bg-hover); }
|
.actions a.danger:hover, .nav button:hover { background: var(--surface-bg-hover); }
|
||||||
button.copy { flex: none; margin-top: 0.3rem; }
|
button.copy { flex: none; margin-top: 0.3rem; }
|
||||||
.actions details[open] > summary { background: var(--surface-open-bg); }
|
|
||||||
.actions button.danger, .actions a.danger, .nav button.danger {
|
.actions button.danger, .actions a.danger, .nav button.danger {
|
||||||
color: var(--danger-fg); background: var(--danger-bg); border-color: var(--danger-border);
|
color: var(--danger-fg); background: var(--danger-bg); border-color: var(--danger-border);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -188,9 +188,26 @@
|
|||||||
{{if eq .AddressMode $.Wildcard}}Any address of the domain — *@{{$.Domain.Name}}
|
{{if eq .AddressMode $.Wildcard}}Any address of the domain — *@{{$.Domain.Name}}
|
||||||
{{else}}Fixed list — {{range $i, $a := .Addresses}}{{if $i}}, {{end}}{{$a}}{{end}}{{end}}
|
{{else}}Fixed list — {{range $i, $a := .Addresses}}{{if $i}}, {{end}}{{$a}}{{end}}{{end}}
|
||||||
</p>
|
</p>
|
||||||
|
<!-- The two panels are opened by a checkbox and a label rather than by
|
||||||
|
<details>, so that the controls stay a single row of buttons and
|
||||||
|
what a panel reveals is laid out under all four of them instead of
|
||||||
|
splitting the row in two. The checkbox precedes both its label and
|
||||||
|
its panel, which is what lets the CSS alone show them (no script
|
||||||
|
involved) — see .panel-toggle in panel.css. -->
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<details>
|
<input class="panel-toggle t-mode" id="mode-{{.ID}}" type="checkbox">
|
||||||
<summary>Edit mode</summary>
|
<label class="toggle for-mode" for="mode-{{.ID}}">Edit mode</label>
|
||||||
|
<input class="panel-toggle t-limit" id="limit-{{.ID}}" type="checkbox">
|
||||||
|
<label class="toggle for-limit" for="limit-{{.ID}}">Rate limit{{if .HasLimit}} (active){{end}}</label>
|
||||||
|
<form class="inline" method="post" action="/applications/{{.ID}}/password"
|
||||||
|
data-confirm="Regenerate the password for {{.Login}}? The current password stops working immediately.">
|
||||||
|
<button type="submit">New password</button>
|
||||||
|
</form>
|
||||||
|
<form class="inline" method="post" action="/applications/{{.ID}}/delete"
|
||||||
|
data-confirm="Delete application {{.Login}}? Its credentials stop working immediately.">
|
||||||
|
<button type="submit" class="danger">Delete</button>
|
||||||
|
</form>
|
||||||
|
<div class="panel panel-mode">
|
||||||
<form method="post" action="/applications/{{.ID}}/mode">
|
<form method="post" action="/applications/{{.ID}}/mode">
|
||||||
<label>Address mode</label>
|
<label>Address mode</label>
|
||||||
<select name="mode" data-list-mode="{{$.List}}">
|
<select name="mode" data-list-mode="{{$.List}}">
|
||||||
@@ -204,18 +221,23 @@
|
|||||||
</div>
|
</div>
|
||||||
<button type="submit">Save mode</button>
|
<button type="submit">Save mode</button>
|
||||||
</form>
|
</form>
|
||||||
</details>
|
</div>
|
||||||
<details>
|
<div class="panel panel-limit">
|
||||||
<summary>Rate limit{{if .HasLimit}} (active){{end}}</summary>
|
<form id="rl-{{.ID}}" method="post" action="/applications/{{.ID}}/ratelimit">
|
||||||
<form method="post" action="/applications/{{.ID}}/ratelimit">
|
|
||||||
<label>Expected client IPs (one per line or comma-separated)</label>
|
<label>Expected client IPs (one per line or comma-separated)</label>
|
||||||
<textarea name="allowed_ips" rows="2" placeholder="203.0.113.10">{{.IPsText}}</textarea>
|
<textarea name="allowed_ips" rows="2" placeholder="203.0.113.10">{{.IPsText}}</textarea>
|
||||||
<label>Message limit</label>
|
<label>Message limit</label>
|
||||||
<input name="max_messages" type="number" min="1" value="{{.MaxText}}" placeholder="500">
|
<input name="max_messages" type="number" min="1" value="{{.MaxText}}" placeholder="500">
|
||||||
<label>Window (seconds)</label>
|
<label>Window (seconds)</label>
|
||||||
<input name="window_seconds" type="number" min="1" value="{{.WindowVal}}">
|
<input name="window_seconds" type="number" min="1" value="{{.WindowVal}}">
|
||||||
<button type="submit">Save limit</button>
|
|
||||||
</form>
|
</form>
|
||||||
|
<!-- Saving and removing the limit are two posts, so they are two
|
||||||
|
forms and could not share a line while the Save button sat
|
||||||
|
inside the block form above. It is bound to that form by id
|
||||||
|
instead (the form attribute, no script involved), which lets
|
||||||
|
both buttons stand together in one row under the fields. -->
|
||||||
|
<div class="panel-buttons">
|
||||||
|
<button type="submit" form="rl-{{.ID}}">Save limit</button>
|
||||||
{{if .HasLimit}}
|
{{if .HasLimit}}
|
||||||
<form class="inline" method="post" action="/applications/{{.ID}}/ratelimit"
|
<form class="inline" method="post" action="/applications/{{.ID}}/ratelimit"
|
||||||
data-confirm="Remove the rate limit for {{.Login}}? Only the global level-1 limit will apply.">
|
data-confirm="Remove the rate limit for {{.Login}}? Only the global level-1 limit will apply.">
|
||||||
@@ -223,15 +245,8 @@
|
|||||||
<button type="submit" class="danger">Remove limit</button>
|
<button type="submit" class="danger">Remove limit</button>
|
||||||
</form>
|
</form>
|
||||||
{{end}}
|
{{end}}
|
||||||
</details>
|
</div>
|
||||||
<form class="inline" method="post" action="/applications/{{.ID}}/password"
|
</div>
|
||||||
data-confirm="Regenerate the password for {{.Login}}? The current password stops working immediately.">
|
|
||||||
<button type="submit">New password</button>
|
|
||||||
</form>
|
|
||||||
<form class="inline" method="post" action="/applications/{{.ID}}/delete"
|
|
||||||
data-confirm="Delete application {{.Login}}? Its credentials stop working immediately.">
|
|
||||||
<button type="submit" class="danger">Delete</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|||||||
Reference in New Issue
Block a user