diff --git a/CHANGELOG.md b/CHANGELOG.md index f8da91d..a5b528d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,18 @@ document is not part of the release. ### Added +- Panel redesign, stage 1: three directions drawn on the three key screens + (Overview, an outbound domain, sign-in) under + [docs/assets/panel-redesign/](docs/assets/panel-redesign/index.html) — + **A** Pico CSS ("Blank"), **B** Tabler ("Console"), **C** Bulma ("Counter"), + with a cover sheet comparing size, JavaScript, CSP fit and own CSS. All three + share a navigation grouped by mail direction (Overview · Outbound · Inbound · + Server, with a symmetric Log and Queue on both sides), one block per DNS + record instead of separate "status" and "records to publish" cards, and + applications as a table. Mockups only; nothing in `internal/web` changes. + The brief behind them supersedes the "no component library" decision in + `docs/plans/panel-redesign.md`; that file is rewritten once a direction is + accepted. - Roadmap item and givens for a panel visual redesign ([docs/plans/panel-redesign.md](docs/plans/panel-redesign.md)) — layout grammar (`measure` / `fill` plus Web Awesome layout CSS), no JS component diff --git a/docs/assets/panel-redesign/a/domain.html b/docs/assets/panel-redesign/a/domain.html new file mode 100644 index 0000000..8c9bb97 --- /dev/null +++ b/docs/assets/panel-redesign/a/domain.html @@ -0,0 +1,164 @@ + + + + + +example.org · Outbound domains · SelfPost + + + + + + + +
+
+ +
+
+
+
+ Domains + Log + Queue + DMARC reports +
+
+ +
+
+
+

Outbound domains /

+

example.org

+
+ 1 284 messages in 30 days · peak 96 msg/h · average 1.8 msg/h +
+ +
+
01

Connection

The same for every domain
+
+
+
Server
+
mail.example.org
+
+
+
Ports
+
465 — SSL/TLS (implicit)
587 — STARTTLS (submission)
+
+
+
+ +
+
+ 02

DNS records

+ Checked 3 min ago +
+
+ +
+

DKIM

publishedNot a secret · selector sp2026
+
+
Host
sp2026._domainkey.example.org
+
Type
TXT
+
Value
v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAx3kqV0mJd8e2f1Yb7Qw5uT9sLr4nHc6pZaXo1vKe0yWm2gBt8RjN5dCq3hUf7lPs9aEi4oMz6xTb1kGv0nYw…IDAQAB
+
+
+ +
+

SPF

mismatchMerge into the existing record — never publish a second one
+
+
Host
example.org
+
Type
TXT
+
Value
v=spf1 ip4:203.0.113.25 ~all
+
In DNS now
v=spf1 include:_spf.google.com ~all
+
+

The published record does not authorize 203.0.113.25. Add ip4:203.0.113.25 before ~all.

+
+ +
+

DMARC

publishedReport address from Settings
+
+
Host
_dmarc.example.org
+
Type
TXT
+
Value
v=DMARC1; p=none; rua=mailto:dmarc@mail.example.org
+
+

p=none does not affect delivery. Tighten to quarantine, then reject, once the reports look clean.

+
+
+ +
+
+ 03

Applications

+ SASL logins that may send as this domain + Add application +
+ + + + + + + + + + + + + + + + + + +
LoginMay send as30 daysLimits
*@example.org1 102 msg · peak 96/h200 / hEditNew passwordDelete
alerts@example.org, noc@example.org182 msg · peak 14/hdomain 2 IPsEditNew passwordDelete
+
+ +
+
04

Domain settings

Rarely changed
+
+ DMARC reportsSame as Settings — dmarc@mail.example.orgdefault +

The form opens here.

+
+
+ Rate limitAuto ×3 — 288 messages / hour, recalculated yesterdayactive +

The form opens here.

+
+
+ ExportDKIM key and applications as one file, optionally encrypted +

The form opens here.

+
+
+ Delete domainRemoves the DKIM key and both applications +

The confirmation opens here.

+
+
+ + +
+ + + diff --git a/docs/assets/panel-redesign/a/login.html b/docs/assets/panel-redesign/a/login.html new file mode 100644 index 0000000..30d06be --- /dev/null +++ b/docs/assets/panel-redesign/a/login.html @@ -0,0 +1,37 @@ + + + + + +Sign in · SelfPost + + + + + + +
+
+ SelfPost +
+
→

Sign in

+
+
+ + + + + +
+
+
+

YOUR SMTP. YOUR RULES.

+
+
+ + + + + diff --git a/docs/assets/panel-redesign/a/status.html b/docs/assets/panel-redesign/a/status.html new file mode 100644 index 0000000..da61c4f --- /dev/null +++ b/docs/assets/panel-redesign/a/status.html @@ -0,0 +1,136 @@ + + + + + +Overview · SelfPost + + + + + + + +
+
+ +
+
+ +
+
+
+

mail.example.org

+

Overview

+
+ Updated 14:02:31 UTC · refreshes every 10 s +
+ +
+
SELFPOSTWARN1 OF 6 CHECKS
+
+

Mail is flowing. One thing needs attention.

+

The TLS certificate expires in 12 days and has not renewed yet. Everything else is healthy.

+
+
+ +
+
01

Checks

Open a row for its detail
+ +
+ Processes5 of 5 runningok +
+ + + + + + +
ProgramStateDetail
postfixrunningpid 41, uptime 18 days
opendkimrunningpid 39, uptime 18 days
+
+
+ +
+ TLS certificateExpires in 12 days — renewal has not runwarn +
+
+
Expires
2026-10-03 09:14 UTC
+
Names
mail.example.org
+
+

Renewal normally runs 30 days before expiry. Check that port 80 is reachable and look for acme lines in the system log.

+
+
+ +
+ MachineCPU 12 % · memory 41 % · ↓ 18 kB/s ↑ 44 kB/sok +
+ + + + + + + +
ResourceUsageDetail
CPU12 %2 cores, load 0.21
Memory41 %812 MB of 1.9 GB
Network↓ 18 kB/s · ↑ 44 kB/seth0: 2.1 GB in, 6.4 GB out
+
+
+ +
+ QueuesOutbound empty · inbound emptyok +

No messages are waiting. Open the outbound queue or the inbound queue.

+
+ +
+ Milter sockets2 of 2 answeringok +
+ + + + + + +
MilterStateDetail
opendkimokinet:127.0.0.1:8891
journal-milterokunix:/run/selfpost/journal.sock
+
+
+ +
+ Hostname and reverse DNSmail.example.org ⇄ 203.0.113.25ok +
+
+
Hostname
mail.example.org
+
Lookup
mail.example.org → 203.0.113.25
203.0.113.25 → mail.example.org
+
+
+
+
+
+ + +
+ + + diff --git a/docs/assets/panel-redesign/a/theme.css b/docs/assets/panel-redesign/a/theme.css new file mode 100644 index 0000000..e5dffaa --- /dev/null +++ b/docs/assets/panel-redesign/a/theme.css @@ -0,0 +1,206 @@ +/* Direction A — "Blank" (the postal form). Pico CSS v2 carries every generic + control (type, buttons, inputs, tables, details, dropdown); this file maps + the brand onto Pico's variables and adds only what is SelfPost's own: + the two-row header, the sheet, numbered form sections, the postmark and + the DNS record block. */ + +/* :root[...] rather than [data-theme=light] alone: Pico declares its light + palette on :root:not([data-theme=dark]), which a bare attribute selector + loses to. */ +:root[data-theme=light] { + --pico-font-family-sans-serif: var(--sp-sans); + --pico-font-family-monospace: var(--sp-mono); + --pico-font-family: var(--sp-sans); + --pico-border-radius: 2px; + --pico-spacing: .9rem; + --pico-typography-spacing-vertical: .9rem; + --pico-form-element-spacing-vertical: .5rem; + --pico-form-element-spacing-horizontal: .75rem; + + --pico-background-color: var(--sp-paper); + --pico-color: var(--sp-ink); + --pico-muted-color: var(--sp-muted); + --pico-muted-border-color: var(--sp-rule); + --pico-h1-color: var(--sp-ink); --pico-h2-color: var(--sp-ink); + --pico-h3-color: var(--sp-ink); --pico-h4-color: var(--sp-ink); + + --pico-primary: var(--sp-brick); + --pico-primary-background: var(--sp-brick); + --pico-primary-border: var(--sp-brick); + --pico-primary-underline: rgb(122 59 46 / .35); + --pico-primary-hover: var(--sp-brick-dark); + --pico-primary-hover-background: var(--sp-brick-dark); + --pico-primary-hover-border: var(--sp-brick-dark); + --pico-primary-hover-underline: var(--sp-brick-dark); + --pico-primary-focus: rgb(122 59 46 / .25); + --pico-primary-inverse: #fff; + + --pico-secondary: var(--sp-ink-2); + --pico-secondary-background: var(--sp-ink-2); + --pico-secondary-border: var(--sp-ink-2); + --pico-secondary-hover: var(--sp-ink); + --pico-secondary-hover-background: var(--sp-ink); + --pico-secondary-hover-border: var(--sp-ink); + --pico-secondary-focus: rgb(18 22 28 / .2); + --pico-secondary-inverse: #fff; + + --pico-form-element-background-color: #fff; + --pico-form-element-border-color: var(--sp-rule-strong); + --pico-form-element-active-border-color: var(--sp-brick); + --pico-form-element-focus-color: rgb(122 59 46 / .25); + --pico-table-border-color: var(--sp-rule); + --pico-code-background-color: var(--sp-well); + --pico-code-color: var(--sp-ink); + --pico-dropdown-background-color: #fff; + --pico-dropdown-border-color: var(--sp-rule); + --pico-dropdown-hover-background-color: var(--sp-brick-tint); +} + +/* Pico grows the root font with the viewport (up to 131%). An ops panel wants + one size everywhere. */ +:root { font-size: 15px; } +body { min-height: 100vh; display: flex; flex-direction: column; } +h1 { font-size: 1.7rem; font-weight: 600; margin: 0; } +small, .sp-muted { color: var(--sp-muted); } +code, .sp-mono { font-family: var(--sp-mono); font-size: .88em; } + +/* ---- header: sections on the first row, the section's pages on the second */ +.sp-wrap { width: 100%; max-width: 72rem; margin-inline: auto; padding-inline: 1.5rem; } +.sp-bar { background: var(--sp-sheet); border-bottom: 1px solid var(--sp-rule); } +.sp-bar nav { align-items: center; } +.sp-bar nav li { padding-block: .45rem; } +.sp-brand img { height: 46px; width: auto; display: block; } +.sp-sections a { + font-family: var(--sp-mono); font-size: .8rem; font-weight: 600; + letter-spacing: .09em; text-transform: uppercase; text-decoration: none; + color: var(--sp-muted); padding: .5rem .8rem; margin: 0; border-radius: 0; +} +.sp-sections a:hover { color: var(--sp-ink); } +.sp-sections a[aria-current] { color: var(--sp-brick); box-shadow: inset 0 -3px 0 var(--sp-brick); } +.sp-user summary { font-size: .9rem; } +.sp-pages { background: var(--sp-paper); border-bottom: 1px solid var(--sp-rule); } +.sp-pages .sp-wrap { display: flex; gap: 1.6rem; } +.sp-pages a { color: var(--sp-muted); text-decoration: none; padding: .6rem 0; font-size: .95rem; } +.sp-pages a:hover { color: var(--sp-ink); } +.sp-pages a[aria-current] { color: var(--sp-ink); font-weight: 600; box-shadow: inset 0 -2px 0 var(--sp-ink); } + +/* ---- the sheet: one column, one width, every page */ +main.sp-wrap { flex: 1; padding-block: 1.8rem 2.5rem; } +.sp-head { display: flex; align-items: flex-end; justify-content: space-between; gap: 1rem; flex-wrap: wrap; margin-bottom: 1.4rem; } +.sp-head .sp-kicker { font-family: var(--sp-mono); font-size: .75rem; letter-spacing: .1em; text-transform: uppercase; color: var(--sp-muted); margin: 0 0 .15rem; } +.sp-head .sp-kicker a { color: inherit; } +.sp-head-actions { display: flex; gap: .5rem; align-items: center; } +.sp-head-actions > * { margin: 0; } +.sp-sheet { background: var(--sp-sheet); border: 1px solid var(--sp-rule); margin-bottom: 1.4rem; } +.sp-sheet > header { + display: flex; align-items: baseline; gap: .8rem; padding: .7rem 1.2rem; + border-bottom: 1px solid var(--sp-rule); +} +.sp-sheet > header .sp-no { font-family: var(--sp-mono); font-size: .75rem; color: var(--sp-brick); font-weight: 600; } +.sp-sheet > header h2 { + font-family: var(--sp-mono); font-size: .8rem; font-weight: 600; letter-spacing: .1em; + text-transform: uppercase; margin: 0; flex: 1; +} +.sp-sheet > header small { font-size: .85rem; } +.sp-sheet > .sp-body { padding: 1.1rem 1.2rem; } +.sp-sheet > .sp-body > :last-child { margin-bottom: 0; } +.sp-foot { color: var(--sp-muted); font-size: .8rem; border-top: 1px solid var(--sp-rule); padding-block: 1rem; } +.sp-foot a { color: inherit; } + +/* ---- status tag and the postmark */ +.sp-tag { + display: inline-block; font-family: var(--sp-mono); font-size: .72rem; font-weight: 600; + letter-spacing: .08em; text-transform: uppercase; padding: .1rem .5rem; + border: 1px solid var(--sp-idle-rule); background: var(--sp-idle-bg); color: var(--sp-idle); + border-radius: 2px; white-space: nowrap; text-decoration: none; +} +.sp-tag.ok { color: var(--sp-ok); background: var(--sp-ok-bg); border-color: var(--sp-ok-rule); } +.sp-tag.warn { color: var(--sp-warn); background: var(--sp-warn-bg); border-color: var(--sp-warn-rule); } +.sp-tag.fail { color: var(--sp-fail); background: var(--sp-fail-bg); border-color: var(--sp-fail-rule); } + +.sp-verdict { display: flex; align-items: center; gap: 1.5rem; padding: 1.3rem 1.4rem; } +.sp-verdict h2 { margin: 0 0 .2rem; font-size: 1.35rem; font-weight: 600; } +.sp-verdict p { margin: 0; color: var(--sp-muted); } +.sp-postmark { + flex: none; width: 5.6rem; height: 5.6rem; border-radius: 50%; + border: 2px solid currentColor; outline: 1px solid currentColor; outline-offset: 3px; + display: grid; place-content: center; text-align: center; transform: rotate(-9deg); + font-family: var(--sp-mono); font-weight: 600; letter-spacing: .08em; line-height: 1.15; + color: var(--sp-ok); +} +.sp-postmark.warn { color: var(--sp-warn); } +.sp-postmark.fail { color: var(--sp-fail); } +.sp-postmark b { font-size: 1.25rem; display: block; } +.sp-postmark span { font-size: .55rem; display: block; } + +/* ---- ledger: one row per check, the row opens into its detail */ +.sp-ledger details { margin: 0; padding: 0; border-bottom: 1px solid var(--sp-rule); } +.sp-ledger details:last-child { border-bottom: 0; } +.sp-ledger summary { + display: grid; grid-template-columns: 13rem 1fr auto 1.2rem; gap: 1rem; align-items: center; + padding: .75rem 1.2rem; margin: 0; color: var(--sp-ink); +} +.sp-ledger summary::after { grid-column: 4; margin: 0; float: none; } +.sp-ledger summary:hover { background: var(--sp-paper); } +.sp-ledger summary strong { font-weight: 600; } +.sp-ledger summary span.sp-muted { font-size: .93rem; } +.sp-ledger .sp-detail { padding: .2rem 1.2rem 1.1rem 15.2rem; } +.sp-ledger .sp-detail table { margin: 0; font-size: .93rem; } +.sp-ledger .sp-detail > :last-child { margin-bottom: 0; } +.sp-ledger meter { width: 9rem; height: .7rem; vertical-align: middle; margin-right: .5rem; } + +/* ---- label / value / copy: the DNS record block and connection settings */ +.sp-record { padding: 1.1rem 1.2rem; border-bottom: 1px solid var(--sp-rule); } +.sp-record:last-child { border-bottom: 0; } +.sp-record-head { display: flex; align-items: center; gap: .7rem; margin-bottom: .7rem; } +.sp-record-head h3 { margin: 0; font-size: 1.05rem; font-weight: 600; } +.sp-record-head small { margin-left: auto; } +.sp-kv { display: grid; grid-template-columns: 6.5rem 1fr; gap: .45rem 1rem; align-items: start; margin: 0; } +.sp-kv dt { + font-family: var(--sp-mono); font-size: .72rem; letter-spacing: .08em; text-transform: uppercase; + color: var(--sp-muted); padding-top: .5rem; margin: 0; +} +.sp-kv dd { margin: 0; display: flex; gap: .4rem; align-items: stretch; min-width: 0; } +.sp-value { + flex: 1; min-width: 0; font-family: var(--sp-mono); font-size: .86rem; line-height: 1.5; + background: var(--sp-well); border: 1px solid var(--sp-rule); padding: .4rem .65rem; + overflow-wrap: anywhere; border-radius: 2px; +} +.sp-value.plain { background: none; border-color: transparent; padding-left: 0; font-family: var(--sp-sans); font-size: .95rem; } +.sp-copy { + flex: none; align-self: flex-start; height: 2.15rem; margin: 0; padding: 0 .7rem; font-size: .8rem; + --pico-background-color: #fff; --pico-border-color: var(--sp-rule-strong); --pico-color: var(--sp-ink-2); +} +.sp-copy:hover { --pico-background-color: var(--sp-paper); --pico-border-color: var(--sp-ink-2); --pico-color: var(--sp-ink); } +.sp-note { margin: .7rem 0 0 7.5rem; font-size: .9rem; color: var(--sp-muted); } +.sp-note.fail { color: var(--sp-fail); } +.sp-cols { display: grid; grid-template-columns: repeat(auto-fit, minmax(20rem, 1fr)); gap: 0 2.4rem; } + +/* ---- applications */ +.sp-apps { margin: 0; font-size: .95rem; } +.sp-apps td, .sp-apps th { padding-inline: 1.2rem .4rem; vertical-align: middle; } +.sp-apps th { font-family: var(--sp-mono); font-size: .72rem; letter-spacing: .08em; text-transform: uppercase; color: var(--sp-muted); font-weight: 400; } +.sp-apps td:last-child { text-align: right; padding-right: 1.2rem; white-space: nowrap; } +.sp-apps .sp-login { font-family: var(--sp-mono); font-weight: 600; } +.sp-link-btn { font-size: .9rem; margin-left: .9rem; } +.sp-link-btn.danger { color: var(--sp-fail); --pico-underline: rgb(180 35 24 / .35); text-decoration-color: rgb(180 35 24 / .35); } +.sp-btn-sm { padding: .35rem .8rem; font-size: .9rem; margin: 0; width: auto; } + +/* ---- sign-in */ +.sp-signin { flex: 1; display: grid; place-content: center; padding: 2rem 1.5rem; } +.sp-signin-card { width: 23rem; max-width: 100%; } +.sp-signin-card img { display: block; width: 15rem; height: auto; margin: 0 auto 1.6rem; } +.sp-signin-card .sp-body { padding: 1.4rem 1.4rem 1.2rem; } +.sp-signin-card button { margin-bottom: 0; } +.sp-tagline { text-align: center; font-family: var(--sp-mono); font-size: .75rem; letter-spacing: .16em; color: var(--sp-muted); margin-top: 1.2rem; } + +@media (max-width: 48rem) { + .sp-ledger summary { grid-template-columns: 1fr auto 1.2rem; } + .sp-ledger summary span.sp-muted { grid-column: 1 / -1; grid-row: 2; } + .sp-ledger summary::after { grid-column: 3; } + .sp-ledger .sp-detail { padding-left: 1.2rem; } + .sp-kv { grid-template-columns: 1fr; gap: .15rem; } + .sp-kv dt { padding-top: .5rem; } + .sp-note { margin-left: 0; } + .sp-pages .sp-wrap { overflow-x: auto; white-space: nowrap; } +} diff --git a/docs/assets/panel-redesign/b/domain.html b/docs/assets/panel-redesign/b/domain.html new file mode 100644 index 0000000..c5053bd --- /dev/null +++ b/docs/assets/panel-redesign/b/domain.html @@ -0,0 +1,212 @@ + + + + + +example.org · Outbound domains · SelfPost + + + + + + +
+ + + +
+ + +
+
+
+ +
+
+

DNS records

Checked 3 min ago
+ +
+
+

DKIM

published + Not a secret · selector sp2026 +
+
+
+ +
+
+
+ + +
+
+ +
+
+
+
+ +
+
+

SPF

mismatch + Merge into the existing record — never publish a second one +
+
+
+ +
+
+
+ + +
+
+ +
+
+
+ + +
The published record does not authorize 203.0.113.25. Add ip4:203.0.113.25 before ~all.
+
+
+
+ +
+
+

DMARC

published + Report address from Settings +
+
+
+ +
+
+
+ + +
+
+ +
+
+
+
+
+
+ +
+
+

Connection

+
+
+
Server
mail.example.org
+
Ports
465 SSL/TLS
587 STARTTLS
+
+

The same for every domain. Authenticate with an application login.

+
+
+ +
+

Sent, 30 days

+
+
+
Messages
1 284
+
Peak
96 /h
+
Average
1.8 /h
+
+
+
+ + +
+ +
+
+

Applications

SASL logins that may send as this domain
+
+ + + + + + + + + + + + + + +
LoginMay send as30 daysLimits
prod-server*@example.org1 102 msg · peak 96/h200 / h
alertsalerts@example.org, noc@example.org182 msg · peak 14/hdomain 2 IPs
+
+
+
+ +
+
+
+ + +
+
+ + diff --git a/docs/assets/panel-redesign/b/login.html b/docs/assets/panel-redesign/b/login.html new file mode 100644 index 0000000..b85d14a --- /dev/null +++ b/docs/assets/panel-redesign/b/login.html @@ -0,0 +1,36 @@ + + + + + +Sign in · SelfPost + + + + + +
+
+
+
+
+

Sign in

+
+
+ + +
+
+ + +
+ +
+
+
+
YOUR SMTP. YOUR RULES.
+
© 2026 Mix · License (AGPL-3.0) · Source · No warranty
+
+
+ + diff --git a/docs/assets/panel-redesign/b/status.html b/docs/assets/panel-redesign/b/status.html new file mode 100644 index 0000000..9573fc2 --- /dev/null +++ b/docs/assets/panel-redesign/b/status.html @@ -0,0 +1,168 @@ + + + + + +Overview · SelfPost + + + + + + +
+ + + +
+ + +
+
+ + + +
+
Machine
CPU 12 % · mem 41 %
OK
+
Processes
5 of 5 running
OK
+
Queues
Both empty
OK
+
TLS certificate
Expires in 12 days
Warn
+
Milter sockets
2 of 2 answering
OK
+
Reverse DNS
Forward = reverse
OK
+
+ +
+
+
+

Machine

ok
+
+ + + + + + + +
ResourceUsageDetail
CPU12 %2 cores, load 0.21
Memory41 %812 MB of 1.9 GB
Network↓ 18 kB/s · ↑ 44 kB/seth0: 2.1 GB in, 6.4 GB out
+
+
+
+ +
+
+

Processes

ok
+
+ + + + + + + + + +
ProgramStateDetail
postfixrunningpid 41, uptime 18 days
opendkimrunningpid 39, uptime 18 days
journal-milterrunningpid 40, uptime 18 days
panelrunningpid 44, uptime 18 days
dmarc-ingestrunningpid 47, uptime 18 days
+
+
+
+ +
+
+

TLS certificate

warn
+
+
+
Expires
2026-10-03 09:14 UTC
+
Names
mail.example.org
+
+

Renewal normally runs 30 days before expiry. Check port 80 and the system log.

+
+
+
+ +
+
+

Milter sockets

ok
+ + + + + + +
MilterState
opendkim
inet:127.0.0.1:8891
ok
journal-milter
unix:/run/selfpost/journal.sock
ok
+
+
+ +
+
+

Hostname and reverse DNS

ok
+
+
+
Hostname
mail.example.org
+
Lookup
→ 203.0.113.25
← mail.example.org
+
+
+ +
+
+
+ +
+
+ + +
+
+ + diff --git a/docs/assets/panel-redesign/b/theme.css b/docs/assets/panel-redesign/b/theme.css new file mode 100644 index 0000000..8a5a59a --- /dev/null +++ b/docs/assets/panel-redesign/b/theme.css @@ -0,0 +1,80 @@ +/* Direction B — "Console". Tabler (Bootstrap 5 admin kit) carries the shell, + cards, tables, badges, input groups and the sign-in page as shipped; this + file only re-points Tabler's variables at the brand and adds the two things + Tabler has no word for: a sidebar group label and the DNS record block. */ + +:root, [data-bs-theme=light] { + --tblr-font-sans-serif: var(--sp-sans); + --tblr-font-monospace: var(--sp-mono); + --tblr-body-bg: var(--sp-paper); + --tblr-body-color: var(--sp-ink); + --tblr-secondary: var(--sp-muted); + --tblr-border-color: var(--sp-rule); + --tblr-border-radius: 3px; + --tblr-border-radius-lg: 4px; + + --tblr-primary: #7a3b2e; + --tblr-primary-rgb: 122, 59, 46; + --tblr-link-color: #7a3b2e; + --tblr-link-color-rgb: 122, 59, 46; + --tblr-link-hover-color: #632f25; + --tblr-link-hover-color-rgb: 99, 47, 37; + + --tblr-green: #2c6b43; --tblr-green-rgb: 44, 107, 67; + --tblr-yellow: #b5700f; --tblr-yellow-rgb: 181, 112, 15; + --tblr-red: #b42318; --tblr-red-rgb: 180, 35, 24; + --tblr-success: #2c6b43; --tblr-success-rgb: 44, 107, 67; + --tblr-warning: #b5700f; --tblr-warning-rgb: 181, 112, 15; + --tblr-danger: #b42318; --tblr-danger-rgb: 180, 35, 24; +} +body { font-feature-settings: normal; } +.btn-primary { --tblr-btn-hover-bg: #632f25; --tblr-btn-active-bg: #632f25; } +code, .font-monospace { font-family: var(--sp-mono); } + +/* ---- sidebar: ink, with the stamp on its own paper */ +.navbar-vertical { + --tblr-navbar-bg: var(--sp-ink); background: var(--sp-ink); + --tblr-navbar-color: #aab2bd; --tblr-navbar-hover-color: #fff; --tblr-navbar-active-color: #fff; + --tblr-navbar-border-color: transparent; +} +.navbar-vertical .navbar-brand { padding: 1.1rem 0 .9rem; justify-content: flex-start; } +.navbar-vertical .navbar-brand img { height: 52px; width: auto; } +.navbar-vertical .nav-link { padding-block: .42rem; } +.navbar-vertical .nav-link-icon { font-size: 1.15rem; } +.navbar-vertical .nav-item.active > .nav-link { + background: rgb(255 255 255 / .07); box-shadow: inset 3px 0 0 #c4705e; color: #fff; +} +.sp-nav-label { + font-family: var(--sp-mono); font-size: .66rem; font-weight: 600; letter-spacing: .14em; + text-transform: uppercase; color: #6f7a88; padding: 1.1rem 1.5rem .3rem; +} +.sp-nav-foot { margin-top: auto; padding-bottom: .8rem; border-top: 1px solid rgb(255 255 255 / .08); padding-top: .6rem; } +.sp-nav-user { color: #6f7a88; font-size: .8rem; padding: .3rem 1.5rem; } + +/* ---- page */ +.page-pretitle { font-family: var(--sp-mono); letter-spacing: .1em; } +.card-title { font-weight: 600; } +.table thead th { font-family: var(--sp-mono); letter-spacing: .08em; } +.sp-tile .card-body { display: flex; align-items: center; gap: .8rem; padding: .85rem 1rem; } +.sp-tile .sp-tile-icon { font-size: 1.5rem; color: var(--sp-muted); } +.sp-tile .fw-semibold { line-height: 1.2; } +.sp-tile .status { margin-left: auto; } +progress.sp-bar { + appearance: none; -webkit-appearance: none; width: 8rem; height: .4rem; border: 0; + border-radius: 1rem; overflow: hidden; vertical-align: middle; margin-right: .6rem; background: var(--sp-well); +} +progress.sp-bar::-webkit-progress-bar { background: var(--sp-well); } +progress.sp-bar::-webkit-progress-value { background: var(--tblr-green); } +progress.sp-bar::-moz-progress-bar { background: var(--tblr-green); } + +/* ---- the DNS record block: Tabler's read-only input group, plus a status */ +.sp-record + .sp-record { border-top: 1px solid var(--sp-rule); } +.sp-record .form-control[readonly], .sp-record textarea[readonly] { + font-family: var(--sp-mono); font-size: .8rem; background: var(--sp-well); +} +.sp-record textarea { resize: none; } +.sp-record .form-label { font-size: .75rem; color: var(--sp-muted); margin-bottom: .25rem; } + +.sp-foot { color: var(--sp-muted); font-size: .8rem; } +.sp-signin-mark { width: 15rem; height: auto; } +.sp-tagline { font-family: var(--sp-mono); font-size: .72rem; letter-spacing: .16em; color: var(--sp-muted); } diff --git a/docs/assets/panel-redesign/c/domain.html b/docs/assets/panel-redesign/c/domain.html new file mode 100644 index 0000000..7059e5c --- /dev/null +++ b/docs/assets/panel-redesign/c/domain.html @@ -0,0 +1,200 @@ + + + + + +example.org · Outbound domains · SelfPost + + + + + + + + +
+
+
+
+
+

Outbound domains /

+

example.org

+
+
+
+
+
+ Add application +
+
+
+ +
+
+ +
+ +
+
+

DNS records

Checked 3 min ago
+ +
+

DKIM

publishedNot a secret · selector sp2026
+
+
+ +
+
+
+
+
+ +
+
+
+ +
+

SPF

mismatchMerge into the existing record — never publish a second one
+
+
+ +
+
+
+
+
+ +
+
+
+ +
+

The published record does not authorize 203.0.113.25. Add ip4:203.0.113.25 before ~all.

+
+
+ +
+

DMARC

publishedReport address from Settings
+
+
+ +
+
+
+
+
+ +
+
+
+
+ +
+

Applications

SASL logins that may send as this domain
+ + + + + + + + + + + + + + +
LoginMay send as30 daysLimits
prod-server*@example.org1 102 msg · peak 96/h200 / h
alertsalerts@example.org, noc@example.org182 msg · peak 14/hdomain 2 IPs
+
+ +
+
+
+

Connection

Same for every domain
+

mail.example.org

+

465 SSL/TLS · 587 STARTTLS

+
+
+
+
+

Sent, 30 days

+
+
1 284messages
+
96peak / h
+
1.8average / h
+
+
+
+
+
+
+ + +
+
+ + + diff --git a/docs/assets/panel-redesign/c/login.html b/docs/assets/panel-redesign/c/login.html new file mode 100644 index 0000000..4574b43 --- /dev/null +++ b/docs/assets/panel-redesign/c/login.html @@ -0,0 +1,38 @@ + + + + + +Sign in · SelfPost + + + + + + +
+
+
+ SelfPost +

YOUR SMTP. YOUR RULES.

+
+
+
+
+

Sign in

+
+ +
+
+
+ +
+
+
+
+

© 2026 Mix · License (AGPL-3.0) · Source · No warranty

+
+
+ + + diff --git a/docs/assets/panel-redesign/c/status.html b/docs/assets/panel-redesign/c/status.html new file mode 100644 index 0000000..a3a42ff --- /dev/null +++ b/docs/assets/panel-redesign/c/status.html @@ -0,0 +1,117 @@ + + + + + +Overview · SelfPost + + + + + + + + +
+
+
+
+
+

mail.example.org

+

Overview

+
+
+
Updated 14:02:31 UTC · refreshes every 10 s
+
+ + + +
+
+
+

Outbound domains

+ + + + + + + +
DomainDNSApps30 days
example.orgDKIM SPF DMARC21 284 msg
shop.example.orgDKIM SPF DMARC118 920 msg
notify.acme.ioDKIM SPF DMARC3402 msg
+
+
+
+
+

Inbound domains

+ + + + + + +
DomainMXUpstream
lists.example.orgMXmx.internal:25
acme.ioMX10.0.4.12:2525
+
+
+
+ + +
+
+ + + diff --git a/docs/assets/panel-redesign/c/theme.css b/docs/assets/panel-redesign/c/theme.css new file mode 100644 index 0000000..f41edce --- /dev/null +++ b/docs/assets/panel-redesign/c/theme.css @@ -0,0 +1,98 @@ +/* Direction C — "Counter". Bulma v1 (pure CSS, no JavaScript, no data: URIs) + carries the navbar with hover dropdowns, columns, boxes, tables, tags, + the side menu and input-with-addon fields. This file points Bulma's HSL + variables at the brand and adds the health strip and the record block. */ + +:root { + --bulma-family-primary: var(--sp-sans); + --bulma-family-secondary: var(--sp-sans); + --bulma-family-code: var(--sp-mono); + --bulma-primary-h: 10deg; --bulma-primary-s: 45%; --bulma-primary-l: 33%; + --bulma-link-h: 10deg; --bulma-link-s: 45%; --bulma-link-l: 33%; + --bulma-success-h: 138deg; --bulma-success-s: 42%; --bulma-success-l: 30%; + --bulma-warning-h: 36deg; --bulma-warning-s: 85%; --bulma-warning-l: 55%; + --bulma-danger-h: 4deg; --bulma-danger-s: 76%; --bulma-danger-l: 40%; + --bulma-radius: 6px; --bulma-radius-large: 10px; + --bulma-body-size: 15px; + /* Bulma derives link text and the on-primary colour from lightness steps; + at the brick's 33 % both land off-brand, so they are pinned. */ + --bulma-link-text: var(--sp-brick); + --bulma-link-text-hover: var(--sp-brick-dark); +} +.button.is-primary, .button.is-primary:hover { color: #fff; } +a strong { color: inherit; } +html { background: var(--sp-paper); } +body { background: var(--sp-paper); color: var(--sp-ink); min-height: 100vh; display: flex; flex-direction: column; } +main { flex: 1; } +code { background: var(--sp-well); color: var(--sp-ink); border-radius: 3px; padding: .1em .35em; font-size: .85em; } + +/* ---- navbar: brick, the stamp on it, hover dropdowns */ +.navbar.is-primary { --bulma-navbar-height: 4rem; } +.navbar-brand img { max-height: 3rem; height: 3rem; } +.navbar.is-primary .navbar-item, .navbar.is-primary .navbar-link { color: var(--sp-on-brick); font-weight: 500; } +.navbar.is-primary .navbar-link::after { border-color: var(--sp-on-brick); } +.navbar.is-primary .navbar-start > .navbar-item.is-active, +.navbar.is-primary .navbar-start > .has-dropdown.sp-current > .navbar-link { box-shadow: inset 0 -3px 0 var(--sp-on-brick); } +.navbar-dropdown { border-top: 0; box-shadow: 0 8px 24px rgb(18 22 28 / .14); } +.navbar.is-primary .navbar-dropdown .navbar-item { color: var(--sp-ink); font-weight: 400; } +.navbar.is-primary .navbar-dropdown .navbar-item.is-active { color: var(--sp-brick); font-weight: 600; background: var(--sp-brick-tint); } + +/* ---- page */ +.sp-page { padding-block: 2rem 2.5rem; } +.sp-kicker { font-family: var(--sp-mono); font-size: .75rem; letter-spacing: .1em; text-transform: uppercase; color: var(--sp-muted); } +.sp-kicker a { color: inherit; } +.title { color: var(--sp-ink); font-weight: 600; } +.box { box-shadow: none; border: 1px solid var(--sp-rule); } +.box > .sp-box-head { display: flex; align-items: center; gap: .75rem; margin-bottom: 1rem; } +.box > .sp-box-head h2 { font-size: 1.1rem; font-weight: 600; margin: 0; } +.box > .sp-box-head .sp-end { margin-left: auto; color: var(--sp-muted); font-size: .85rem; display: flex; gap: .6rem; align-items: center; } +.table { background: transparent; } +.table thead th { font-family: var(--sp-mono); font-size: .7rem; font-weight: 400; letter-spacing: .08em; text-transform: uppercase; color: var(--sp-muted); } +.table td { vertical-align: middle; } +.tag { font-family: var(--sp-mono); font-weight: 600; letter-spacing: .04em; } +.sp-muted { color: var(--sp-muted); } +.sp-mono { font-family: var(--sp-mono); } + +/* ---- health strip */ +.sp-health { display: grid; grid-template-columns: repeat(auto-fit, minmax(9.5rem, 1fr)); gap: 0; padding: 0; overflow: hidden; } +.sp-health > a { + display: block; padding: 1rem 1.2rem; color: var(--sp-ink); border-right: 1px solid var(--sp-rule); + box-shadow: inset 0 3px 0 var(--sp-ok); +} +.sp-health > a:last-child { border-right: 0; } +.sp-health > a:hover { background: var(--sp-paper); } +.sp-health > a.warn { box-shadow: inset 0 3px 0 #d9962b; background: var(--sp-warn-bg); } +.sp-health > a.fail { box-shadow: inset 0 3px 0 var(--sp-fail); background: var(--sp-fail-bg); } +.sp-health .sp-h-name { font-family: var(--sp-mono); font-size: .68rem; letter-spacing: .08em; text-transform: uppercase; color: var(--sp-muted); } +.sp-health .sp-h-value { font-size: 1.15rem; font-weight: 600; line-height: 1.3; } +.sp-health .sp-h-sub { font-size: .8rem; color: var(--sp-muted); } + +/* ---- domain page */ +.sp-side { position: sticky; top: 1.5rem; } +.menu-label { font-family: var(--sp-mono); letter-spacing: .1em; } +.menu-list a { display: flex; align-items: center; gap: .5rem; } +.menu-list a .tag { margin-left: auto; } +.sp-record + .sp-record { border-top: 1px solid var(--sp-rule); margin-top: 1.25rem; padding-top: 1.25rem; } +.sp-record-head { display: flex; align-items: center; gap: .6rem; margin-bottom: .75rem; } +.sp-record-head h3 { font-size: 1rem; font-weight: 600; margin: 0; } +.sp-record-head small { margin-left: auto; color: var(--sp-muted); } +.sp-record .label { font-size: .75rem; font-weight: 400; color: var(--sp-muted); margin-bottom: .25rem; } +.sp-record .input[readonly], .sp-record .textarea[readonly] { + font-family: var(--sp-mono); font-size: .82rem; background: var(--sp-well); box-shadow: none; +} +.sp-record .textarea { resize: none; min-height: 0; } +.sp-record .field:not(:last-child) { margin-bottom: .6rem; } +.sp-row { display: grid; grid-template-columns: 1fr 7rem; gap: .75rem; } +.sp-stats { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1rem; } +.sp-stats b { display: block; font-size: 1.5rem; font-weight: 600; line-height: 1.2; } + +/* ---- sign-in */ +.sp-signin { min-height: 100vh; margin: 0; } +.sp-signin-brand { background: var(--sp-brick); display: grid; place-content: center; text-align: center; padding: 3rem; } +.sp-signin-brand img { width: 20rem; max-width: 100%; height: auto; } +.sp-signin-brand p { font-family: var(--sp-mono); letter-spacing: .18em; font-size: .8rem; color: var(--sp-on-brick); margin-top: 1.6rem; } +.sp-signin-form { display: flex; flex-direction: column; justify-content: center; padding: 3rem; } +.sp-signin-form form { width: 21rem; max-width: 100%; margin-inline: auto; } +.sp-foot { color: var(--sp-muted); font-size: .8rem; padding-block: 1.2rem; } +.sp-foot a { color: inherit; text-decoration: underline; } +.sp-signin-form .sp-foot { text-align: center; margin-top: 2.5rem; } diff --git a/docs/assets/panel-redesign/index.css b/docs/assets/panel-redesign/index.css new file mode 100644 index 0000000..71c0058 --- /dev/null +++ b/docs/assets/panel-redesign/index.css @@ -0,0 +1,22 @@ +/* Cover sheet for the three directions. No library on purpose: it must not + look like any one of them. */ +body { margin: 0; background: var(--sp-paper); color: var(--sp-ink); font: 16px/1.55 var(--sp-sans); } +main { max-width: 68rem; margin: 0 auto; padding: 3rem 1.5rem 4rem; } +h1 { font-size: 2rem; font-weight: 600; margin: .2rem 0 1rem; text-wrap: balance; } +h2 { font-size: 1.15rem; font-weight: 600; margin: 2.6rem 0 .8rem; } +h3 { font-size: 1.4rem; font-weight: 600; margin: .1rem 0 .6rem; } +.kicker { font-family: var(--sp-mono); font-size: .75rem; letter-spacing: .1em; text-transform: uppercase; color: var(--sp-brick); margin: 0; } +.lead { max-width: 46rem; color: var(--sp-ink-2); } +ul { padding-left: 1.2rem; max-width: 52rem; } +li { margin-bottom: .5rem; } +a { color: var(--sp-brick); } +code { font-family: var(--sp-mono); font-size: .85em; background: var(--sp-well); padding: .1em .35em; border-radius: 3px; } +.cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(18rem, 1fr)); gap: 1.2rem; } +.cards section { background: var(--sp-sheet); border: 1px solid var(--sp-rule); border-top: 3px solid var(--sp-brick); padding: 1.3rem 1.4rem; display: flex; flex-direction: column; } +.cards p { margin: 0 0 1rem; } +.links { margin-top: auto !important; margin-bottom: 0 !important; display: flex; gap: 1.1rem; font-weight: 600; } +.scroll { overflow-x: auto; } +table { border-collapse: collapse; width: 100%; background: var(--sp-sheet); border: 1px solid var(--sp-rule); font-size: .93rem; } +th, td { text-align: left; vertical-align: top; padding: .6rem .9rem; border-bottom: 1px solid var(--sp-rule); } +thead th { font-family: var(--sp-mono); font-size: .72rem; letter-spacing: .08em; text-transform: uppercase; color: var(--sp-muted); font-weight: 400; } +tbody th { font-weight: 600; white-space: nowrap; } diff --git a/docs/assets/panel-redesign/index.html b/docs/assets/panel-redesign/index.html new file mode 100644 index 0000000..d5579a1 --- /dev/null +++ b/docs/assets/panel-redesign/index.html @@ -0,0 +1,89 @@ + + + + + +Panel redesign · three directions + + + + +
+

SelfPost · panel redesign · stage 1

+

Three directions, three key screens

+

Overview, an outbound domain and sign-in, drawn three ways. Pick one (or a mix); + the remaining screens are then drawn in that direction only. Serve this folder from the repository + root — the pages load the panel's own fonts and mark by relative path.

+ +

Shared by all three

+ + +

The directions

+
+
+

A · Pico CSS 2.1

+

Blank

+

The panel as a postal form: one centred sheet, numbered sections, mono field labels, a postmark for the + verdict. Overview is a ledger — one row per check, the row opens into its detail. Sections on the first + header row, the section's pages on the second.

+ +
+
+

B · Tabler 1.5 (Bootstrap 5)

+

Console

+

The classic admin kit: ink sidebar with every page one click away, status tiles, cards and tables in a + 12-column grid, read-only input groups with Copy. Almost nothing is ours; it also looks the most like + every other admin panel.

+ +
+
+

C · Bulma 1.0

+

Counter

+

Brick top bar with CSS-only hover menus. Overview is the whole service on one screen: a health strip over + the outbound and inbound domain lists. The domain page gets a sticky side menu. Softer, rounder, the + boldest use of the brand colour.

+ +
+
+ +

What each one costs

+
+ + + + + + + + + + + + + + + + +
A · PicoB · TablerC · Bulma
LicenceMITMITMIT
Vendored CSS (minified)83 KB694 KB678 KB
JavaScriptnonenone for these screens; Bootstrap bundle only if dropdowns / modals are wantednone
Build stepnonenonenone (a Sass build could trim it; not needed)
CSP default-src 'self'needs img-src 'self' data: — 14 data: SVGs (select chevron, checkbox tick)needs img-src 'self' data: — 28 data: SVGsworks as is — no data: URIs
Our own CSS in the mockup≈ 200 lines≈ 80 lines≈ 100 lines
Markup stylesemantic HTML, few classes — closest to today's templatesclass-heavy Bootstrap markupclass-based, moderate
Own componentssheet, ledger row, postmark, record blocksidebar group label, record blockhealth strip, record block
Recognisablemostleastmiddle
+
+ +

Open points

+ +
+ + diff --git a/docs/assets/panel-redesign/shared/brand.css b/docs/assets/panel-redesign/shared/brand.css new file mode 100644 index 0000000..6264a16 --- /dev/null +++ b/docs/assets/panel-redesign/shared/brand.css @@ -0,0 +1,48 @@ +/* Brand layer shared by the three redesign directions: the panel's own fonts + and the palette of the mark (docs/assets/selfpost-proof.html). Each + direction maps these tokens onto its library's variables in its own + theme.css — nothing here knows about Pico, Tabler or Bulma. + + Mockup note: no inline style="" and no inline