panel: put Applications and its create form side by side

On the domain page the "Add an application" card sat below the domain rate
limit, so adding a login meant scrolling past the list and back. Move it up
next to the Applications table and wrap the pair in a .split grid (1.5fr /
1fr, so the table keeps the wider column). The columns collapse to one below
52rem, list first, and the grid gap keeps the same vertical rhythm as
.card + .card.

The empty-state text said "Create one below", which is no longer where the
form is; it now names the card instead of its position.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-03 15:50:31 +03:00
parent f3bc24b638
commit a6df2ebceb
2 changed files with 42 additions and 28 deletions
+7
View File
@@ -41,6 +41,13 @@ form.inline button { background: none; color: #2563eb; padding: 0; margin: 0; fo
form.inline button:hover { text-decoration: underline; background: none; } form.inline button:hover { text-decoration: underline; background: none; }
main { max-width: 48rem; } main { max-width: 48rem; }
.card + .card { margin-top: 1.2rem; } .card + .card { margin-top: 1.2rem; }
/* Two cards side by side on a wide viewport; below 60rem 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; } .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; } } @media (prefers-color-scheme: dark) { .flash { background: #0d2818 !important; border-color: #1a5336 !important; color: #75d99b !important; } }
table { width: 100%; border-collapse: collapse; } table { width: 100%; border-collapse: collapse; }
+35 -28
View File
@@ -101,6 +101,11 @@
lost, generate a new one.</p> lost, generate a new one.</p>
</div> </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">
<div class="card"> <div class="card">
<h2>Applications</h2> <h2>Applications</h2>
<p class="muted">Each application is a SASL login/password an app or script <p class="muted">Each application is a SASL login/password an app or script
@@ -172,10 +177,39 @@
</tbody> </tbody>
</table> </table>
{{else}} {{else}}
<p class="muted">No applications yet. Create one below.</p> <p class="muted">No applications yet. Create the first one with the
<strong>Add an application</strong> form.</p>
{{end}} {{end}}
</div> </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"> <div class="card">
<h2>Sending rate limit (domain)</h2> <h2>Sending rate limit (domain)</h2>
<p class="muted">Optional level-2 limit (spec 7.4): cap how many messages this <p class="muted">Optional level-2 limit (spec 7.4): cap how many messages this
@@ -211,33 +245,6 @@
{{end}} {{end}}
</div> </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 class="card"> <div class="card">
<h2>Export domain</h2> <h2>Export domain</h2>
<p class="muted">Download this domain to move it to another SelfPost instance: <p class="muted">Download this domain to move it to another SelfPost instance: