panel: stack Add an application above the list, drop the two-column row

The side-by-side .split row read badly: main is capped at 48rem, so the
applications table had to live in roughly 27rem and its actions column
squeezed four controls into it.

Put the create form directly above the list instead — the order the
domains page already uses for "Add a sending domain" above "Domains" —
and delete .split, which nothing else used. The empty-state text follows
the same page's wording.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-03 16:21:09 +03:00
parent f5f4bd4551
commit 337c8e8f0e
3 changed files with 33 additions and 45 deletions
+3 -3
View File
@@ -21,9 +21,9 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); version
They are all buttons now: filled for a card's own action, compact and
outlined where actions cluster in a table row or the nav bar. An `<a>` is
once again only used for navigation.
- panel: on the domain page, **Applications** and **Add an application** sit
side by side on a wide viewport instead of the form being stranded below the
domain rate limit; they stack again on a narrow one.
- panel: on the domain page **Add an application** now sits directly above the
**Applications** list — the same order the domains page uses for its own add
form — instead of being stranded below the domain rate limit.
- ci: hermetic container e2e suite (`test/e2e`, a separate Go module) gates
image publishing — `make e2e` locally, and `go test ./...` in `test/e2e` as
a required step in `release.yml` before a version tag's image is pushed.
-7
View File
@@ -49,13 +49,6 @@ button:hover { background: #1d4ed8; }
form.inline { display: inline; margin: 0; }
main { max-width: 48rem; }
.card + .card { margin-top: 1.2rem; }
/* Two cards side by side on a wide viewport; below 52rem the columns collapse
and the grid gap keeps the same vertical rhythm as .card + .card. The inner
margin-top is cleared because the gap already spaces the pair. */
.split { display: grid; grid-template-columns: minmax(0, 1.5fr) minmax(0, 1fr); gap: 1.2rem; align-items: start; }
.split > .card + .card { margin-top: 0; }
.card + .split, .split + .card { margin-top: 1.2rem; }
@media (max-width: 52rem) { .split { grid-template-columns: minmax(0, 1fr); } }
.flash { background: #ecfdf3; border: 1px solid #abefc6; color: #067647; padding: 0.7rem 1rem; border-radius: 8px; margin-bottom: 1.2rem; }
@media (prefers-color-scheme: dark) { .flash { background: #0d2818 !important; border-color: #1a5336 !important; color: #75d99b !important; } }
table { width: 100%; border-collapse: collapse; }
+30 -35
View File
@@ -144,11 +144,35 @@
lost, generate a new one.</p>
</div>
{{/* Applications and its create form sit side by side on a wide viewport: the
table is read far more often than the form, and pairing them saves scrolling
past the list to add one. On a narrow viewport the .split grid stacks them
again, list first. */}}
<div class="split">
{{/* Create form above the list, the same order the domains page uses for
"Add a sending domain" above "Domains". */}}
<div class="card">
<h2>Add an application</h2>
<form method="post" action="/domains/{{.Domain.ID}}/applications">
<label for="login">Login</label>
<input id="login" name="login" type="text" placeholder="prod-server"
autocomplete="off" autocapitalize="none" spellcheck="false"
value="{{.FormLogin}}" required>
<label for="mode">Address mode</label>
<select id="mode" name="mode" data-list-mode="{{.List}}">
<option value="{{.Wildcard}}" {{if eq .FormMode .Wildcard}}selected{{end}}>Any address of the domain</option>
<option value="{{.List}}" {{if eq .FormMode .List}}selected{{end}}>Specific addresses (list)</option>
</select>
<div data-addresses>
<label for="addresses">Addresses (one per line or comma-separated)</label>
<textarea id="addresses" name="addresses" rows="3"
placeholder="alerts@{{.Domain.Name}}">{{.FormAddrs}}</textarea>
</div>
{{if .Error}}<p class="error">{{.Error}}</p>{{end}}
<button type="submit">Create application</button>
</form>
<p class="muted">A strong password is generated and shown once. The login must
be unique across all domains and may contain letters, digits, '.', '-' and '_'.</p>
</div>
<div class="card">
<h2>Applications</h2>
<p class="muted">Each application is a SASL login/password an app or script
@@ -220,39 +244,10 @@
</tbody>
</table>
{{else}}
<p class="muted">No applications yet. Create the first one with the
<strong>Add an application</strong> form.</p>
<p class="muted">No applications yet. Add one above to get started.</p>
{{end}}
</div>
<div class="card">
<h2>Add an application</h2>
<form method="post" action="/domains/{{.Domain.ID}}/applications">
<label for="login">Login</label>
<input id="login" name="login" type="text" placeholder="prod-server"
autocomplete="off" autocapitalize="none" spellcheck="false"
value="{{.FormLogin}}" required>
<label for="mode">Address mode</label>
<select id="mode" name="mode" data-list-mode="{{.List}}">
<option value="{{.Wildcard}}" {{if eq .FormMode .Wildcard}}selected{{end}}>Any address of the domain</option>
<option value="{{.List}}" {{if eq .FormMode .List}}selected{{end}}>Specific addresses (list)</option>
</select>
<div data-addresses>
<label for="addresses">Addresses (one per line or comma-separated)</label>
<textarea id="addresses" name="addresses" rows="3"
placeholder="alerts@{{.Domain.Name}}">{{.FormAddrs}}</textarea>
</div>
{{if .Error}}<p class="error">{{.Error}}</p>{{end}}
<button type="submit">Create application</button>
</form>
<p class="muted">A strong password is generated and shown once. The login must
be unique across all domains and may contain letters, digits, '.', '-' and '_'.</p>
</div>
</div>
<div class="card">
<h2>Sending rate limit (domain)</h2>
<p class="muted">Optional level-2 limit (spec 7.4): cap how many messages this