panel: align paired domain settings and export actions
test / test (push) Has been cancelled

Subgrid Domain settings rows, match Address mode and trusted-IP controls,
and pin Export/Danger buttons to a shared baseline in split cards.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-13 00:39:55 +03:00
parent 958d07fcdd
commit 8289ed3916
3 changed files with 110 additions and 50 deletions
+3 -2
View File
@@ -11,8 +11,9 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); version
(`N messages / Ws — Settings`) and in message-limit labels; Settings uses a (`N messages / Ws — Settings`) and in message-limit labels; Settings uses a
code-row for the L1 value. Domain and application rate-limit status uses the code-row for the L1 value. Domain and application rate-limit status uses the
shared `st` badge. Trusted-IP help sits under the IP field; domain rate-limit shared `st` badge. Trusted-IP help sits under the IP field; domain rate-limit
help sits above Save; Address mode drops the duplicate field label; field-pair help sits above Save; Address mode / trusted-IP columns share muted leads and
top spacing aligns with paired DMARC labels. matched control height; Domain settings uses subgrid so DMARC and rate-limit
rows line up; paired `.split` cards pin primary actions to a shared baseline.
## [1.2.4] - 2026-08-12 ## [1.2.4] - 2026-08-12
+55 -5
View File
@@ -360,6 +360,19 @@ button.danger:hover, a.danger:hover { background: var(--danger-fill-hover); }
.app .actions > .panel .panel-buttons form { margin-top: 0; } .app .actions > .panel .panel-buttons form { margin-top: 0; }
.app .actions > .panel .check-cols { margin-top: 0.6rem; } .app .actions > .panel .check-cols { margin-top: 0.6rem; }
.app .actions > .panel .check-col > form { margin-top: 0; } .app .actions > .panel .check-col > form { margin-top: 0; }
.app .actions > .panel .check-col > .check-col-title { margin-top: 0; }
.app .actions > .panel .check-col > .muted { margin: 0; }
/* Address mode select ‖ trusted-IP field: same top offset and control height. */
.app .actions > .panel .check-col > .muted + form > select:first-of-type,
.app .actions > .panel .check-col > .muted + form > textarea[name="allowed_ips"] {
margin-top: 0.45rem;
min-height: 2.55rem;
box-sizing: border-box;
}
.app .actions > .panel .check-col > .muted + form > textarea[name="allowed_ips"] {
field-sizing: fixed;
overflow-y: auto;
}
.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. */
@@ -538,6 +551,22 @@ meter { width: 5rem; height: 0.7rem; vertical-align: middle; margin-right: 0.4re
the cell to shrink below the card's intrinsic minimum when the column is the cell to shrink below the card's intrinsic minimum when the column is
tight. */ tight. */
.split > .card { margin-left: 0; margin-right: 0; min-width: 0; } .split > .card { margin-left: 0; margin-right: 0; min-width: 0; }
/* Paired cards often differ in body length (export's encrypt checkbox vs
danger's short blurb). Stretch both in the grid row and pin the primary
action to the bottom so the buttons share a baseline. */
.split > .card {
display: flex; flex-direction: column;
}
.split > .card > form {
display: flex; flex-direction: column; flex: 1 1 auto;
}
.split > .card > form > button[type="submit"]:last-child {
margin-top: auto;
}
.split > .card > a.danger:last-child,
.split > .card > a.btn:last-child {
margin-top: auto; align-self: flex-start;
}
/* The gap already spaces the cards inside the grid; the general .card + .card /* The gap already spaces the cards inside the grid; the general .card + .card
rule would add a second gap's worth on top of it, and only to the second rule would add a second gap's worth on top of it, and only to the second
card, which in one column reads as an uneven stack. The blocks around the card, which in one column reads as an uneven stack. The blocks around the
@@ -561,16 +590,37 @@ meter { width: 5rem; height: 0.7rem; vertical-align: middle; margin-right: 0.4re
margin: 1rem 0 0.35rem; font-size: 1.05rem; font-weight: 600; margin: 1rem 0 0.35rem; font-size: 1.05rem; font-weight: 600;
} }
.card > h2 + .check-col-title { margin-top: 0.55rem; } .card > h2 + .check-col-title { margin-top: 0.55rem; }
/* Domain settings pairs DMARC rate limit: share four row tracks so titles,
leads, fields and action buttons line up across columns. */
@supports (grid-template-rows: subgrid) {
.check-cols-rows {
grid-template-rows: auto auto 1fr auto;
}
.check-cols-rows > .check-col {
display: grid;
grid-template-rows: subgrid;
grid-row: span 4;
}
}
.check-cols-rows > .check-col > .check-col-title { margin-top: 0; }
.check-cols-rows > .check-col > .muted { margin: 0; }
.check-col-fields { min-width: 0; }
.check-col-fields > form { margin: 0; }
.check-col-fields .field-pair { margin-top: 0; }
.check-col-fields .field-pair > div > label { margin-top: 0.9rem; }
.check-col-actions {
display: flex; flex-wrap: wrap; align-items: center; gap: 0.6rem 0.75rem;
margin-top: 0;
}
.check-col-actions > button { margin-top: 1.2rem; }
.check-col-actions > form.inline { margin: 0; }
.check-col-actions > form.inline > button { margin-top: 1.2rem; }
/* Two short fields on one row (rate-limit message count window). Falls to /* Two short fields on one row (rate-limit message count window). Falls to
one column when the parent is too narrow for both. */ one column when the parent is too narrow for both. */
.field-pair { .field-pair {
display: grid; grid-template-columns: repeat(auto-fit, minmax(8rem, 1fr)); display: grid; grid-template-columns: repeat(auto-fit, minmax(8rem, 1fr));
gap: 0 1rem; gap: 0 1rem; margin-top: 0.45rem;
} }
/* First control row in a check-col form: match a bare label's top margin so
paired columns (DMARC rate limit) line up their first field labels. */
form > .field-pair { margin-top: 0; }
form > .field-pair > div > label { margin-top: 0.9rem; }
.field-pair > div { min-width: 0; } .field-pair > div { min-width: 0; }
.field-pair label { margin-top: 0.45rem; } .field-pair label { margin-top: 0.45rem; }
/* Hint under a field-pair sits tight above the form's submit button (the /* Hint under a field-pair sits tight above the form's submit button (the
+52 -43
View File
@@ -329,6 +329,7 @@
<div class="check-cols"> <div class="check-cols">
<div class="check-col"> <div class="check-col">
<p class="check-col-title">Address mode</p> <p class="check-col-title">Address mode</p>
<p class="muted">Which From addresses this application may use.</p>
<form method="post" action="/applications/{{.ID}}/mode"> <form method="post" action="/applications/{{.ID}}/mode">
<select name="mode" data-list-mode="{{$.List}}" aria-label="Address mode"> <select name="mode" data-list-mode="{{$.List}}" aria-label="Address mode">
<option value="{{$.Wildcard}}" {{if eq .AddressMode $.Wildcard}}selected{{end}}>Any address of the domain</option> <option value="{{$.Wildcard}}" {{if eq .AddressMode $.Wildcard}}selected{{end}}>Any address of the domain</option>
@@ -345,12 +346,14 @@
<div class="check-col"> <div class="check-col">
<p class="check-col-title">Optional trusted-IP override <p class="check-col-title">Optional trusted-IP override
{{if .HasLimit}}<span class="st st-ok">active</span>{{else}}<span class="st st-unknown">inactive</span>{{end}}</p> {{if .HasLimit}}<span class="st st-ok">active</span>{{else}}<span class="st st-unknown">inactive</span>{{end}}</p>
<p class="muted">One per line or comma-separated.</p>
<form id="rl-{{.ID}}" method="post" action="/applications/{{.ID}}/ratelimit"> <form id="rl-{{.ID}}" method="post" action="/applications/{{.ID}}/ratelimit">
<label>Trusted client IPs (required; one per line or comma-separated)</label> <textarea name="allowed_ips" rows="1" placeholder="203.0.113.10"
<textarea name="allowed_ips" rows="2" placeholder="203.0.113.10">{{.IPsText}}</textarea> aria-label="Trusted client IPs">{{.IPsText}}</textarea>
<p class="muted">These IPs get a higher ceiling than the domain <p class="muted">These IPs get a higher ceiling than the domain
(≤ level&nbsp;1) and skip the domain check; everyone else uses (≤ level&nbsp;1) and skip the domain check; everyone else uses
{{if $.DomainHasRL}}the domain ceiling ({{$.DomainRLMaxNum}}){{else}}level&nbsp;1 only{{end}}.</p> the domain level-2 limit{{if $.DomainHasRL}} ({{$.DomainRLMaxNum}}){{end}}
if set, otherwise level&nbsp;1.</p>
<div class="field-pair"> <div class="field-pair">
<div> <div>
<label>Message limit (max {{$.L1Messages}})</label> <label>Message limit (max {{$.L1Messages}})</label>
@@ -390,58 +393,64 @@
<div class="card" id="domain-settings"> <div class="card" id="domain-settings">
<h2>Domain settings</h2> <h2>Domain settings</h2>
<div class="check-cols"> <div class="check-cols check-cols-rows">
<div class="check-col"> <div class="check-col">
<p class="check-col-title">DMARC reports</p> <p class="check-col-title">DMARC reports</p>
<p class="muted">Default comes from <a href="/settings">Settings</a>; <p class="muted">Default comes from <a href="/settings">Settings</a>;
override per domain here.</p> override per domain here.</p>
<form method="post" action="/domains/{{.Domain.ID}}/dmarc"> <div class="check-col-fields">
<label for="dmarc_rua_mode">Aggregate reports (rua=)</label> <form id="dmarc-domain" method="post" action="/domains/{{.Domain.ID}}/dmarc">
<select id="dmarc_rua_mode" name="dmarc_rua_mode" data-custom-mode="custom"> <label for="dmarc_rua_mode">Aggregate reports (rua=)</label>
<option value="inherit"{{if eq .DMARCRuaMode "inherit"}} selected{{end}}>Same as Settings{{if .ProfileDMARCEmail}} ({{.ProfileDMARCEmail}}){{end}}</option> <select id="dmarc_rua_mode" name="dmarc_rua_mode" data-custom-mode="custom">
<option value="none"{{if eq .DMARCRuaMode "none"}} selected{{end}}>No aggregate reports</option> <option value="inherit"{{if eq .DMARCRuaMode "inherit"}} selected{{end}}>Same as Settings{{if .ProfileDMARCEmail}} ({{.ProfileDMARCEmail}}){{end}}</option>
<option value="custom"{{if eq .DMARCRuaMode "custom"}} selected{{end}}>Custom address</option> <option value="none"{{if eq .DMARCRuaMode "none"}} selected{{end}}>No aggregate reports</option>
</select> <option value="custom"{{if eq .DMARCRuaMode "custom"}} selected{{end}}>Custom address</option>
</select>
<div data-custom-address> <div data-custom-address>
<label for="dmarc_rua_email">Custom report address</label> <label for="dmarc_rua_email">Custom report address</label>
<input id="dmarc_rua_email" name="dmarc_rua_email" type="email" <input id="dmarc_rua_email" name="dmarc_rua_email" type="email"
autocapitalize="none" spellcheck="false" value="{{.DMARCRuaCustom}}" autocapitalize="none" spellcheck="false" value="{{.DMARCRuaCustom}}"
placeholder="reports@your-mail-domain.com"> placeholder="reports@your-mail-domain.com">
</div> </div>
</form>
<button type="submit">Save DMARC report settings</button> </div>
</form> <div class="check-col-actions">
<button type="submit" form="dmarc-domain">Save DMARC report settings</button>
</div>
</div> </div>
<div class="check-col"> <div class="check-col">
<p class="check-col-title">Level-2 rate limit <p class="check-col-title">Level-2 rate limit
{{if .DomainHasRL}}<span class="st st-ok">active</span>{{else}}<span class="st st-unknown">inactive</span>{{end}}</p> {{if .DomainHasRL}}<span class="st st-ok">active</span>{{else}}<span class="st st-unknown">inactive</span>{{end}}</p>
<p class="muted">Level&nbsp;1 backstop: {{.L1Messages}} messages / {{.L1Window}}s — <a href="/settings#rate-limits">Settings</a>.</p> <p class="muted">Level&nbsp;1 backstop: {{.L1Messages}} messages / {{.L1Window}}s — <a href="/settings#rate-limits">Settings</a>.</p>
<div class="check-col-fields">
<form method="post" action="/domains/{{.Domain.ID}}/ratelimit"> <form id="rl-domain" method="post" action="/domains/{{.Domain.ID}}/ratelimit">
<div class="field-pair"> <div class="field-pair">
<div> <div>
<label for="d_max">Message limit (max {{.L1Messages}})</label> <label for="d_max">Message limit (max {{.L1Messages}})</label>
<input id="d_max" name="max_messages" type="number" min="1" max="{{.L1Messages}}" <input id="d_max" name="max_messages" type="number" min="1" max="{{.L1Messages}}"
value="{{.DomainRLMax}}" placeholder="{{.L1Messages}}"> value="{{.DomainRLMax}}" placeholder="{{.L1Messages}}">
</div>
<div>
<label for="d_win">Window (seconds)</label>
<input id="d_win" name="window_seconds" type="number" min="1" value="{{.DomainRLWin}}">
</div>
</div> </div>
<div> <p class="muted">Applies to every client IP on this domain. Leave the
<label for="d_win">Window (seconds)</label> message limit empty to use level&nbsp;1 only.</p>
<input id="d_win" name="window_seconds" type="number" min="1" value="{{.DomainRLWin}}"> </form>
</div> </div>
</div> <div class="check-col-actions">
<p class="muted">Applies to every client IP on this domain. Leave the <button type="submit" form="rl-domain">Save limit</button>
message limit empty to use level&nbsp;1 only.</p> {{if .DomainHasRL}}
<button type="submit">Save limit</button> <form class="inline" method="post" action="/domains/{{.Domain.ID}}/ratelimit"
</form> data-confirm="Remove the domain rate limit? Only the global level-1 limit will apply.">
{{if .DomainHasRL}} <input type="hidden" name="clear" value="1">
<form class="inline" method="post" action="/domains/{{.Domain.ID}}/ratelimit" <button type="submit" class="danger">Remove limit</button>
data-confirm="Remove the domain rate limit? Only the global level-1 limit will apply."> </form>
<input type="hidden" name="clear" value="1"> {{end}}
<button type="submit" class="danger">Remove limit</button> </div>
</form>
{{end}}
</div> </div>
</div> </div>
</div> </div>