panel-redesign: the queue in the new shell; the old design is removed
Stage 2, steps 7 and 8 in one commit: the static guard fails as soon as
the ratchet holds no page while legacy.css still exists, and the tests
of the old layout need it while that layout exists - the two steps do
not split into states that pass on their own. Written by Sonnet,
reviewed by Opus.
The queue
- out-queue, named after its mockup: today's content - the listing of
postqueue -p in a log pane, polled as before (GET
/outbound/queue/fragment) - and the "How retries work" facts. The
mockup's table of messages is the roadmap's queue-as-a-table; the
outline guard exempts this page for exactly that reason. Routes and
the global-only check are unchanged; the page has no actions and
never had.
- Inbound has no Queue entry and no route serves one.
The old design is gone
- Deleted: layout_legacy.html, legacy.css, logo-compact.svg,
mail_queue.html, mail_queue_body.html.
- view.go has one path: every page is layout.html + components.html +
its own files; kitPages, kitFragments, the map-data branch of Render,
the dict and back template functions and metaOf's map branch are
removed. handlers: pageBase removed.
- legacy_pages.txt holds its header and no entry.
- panel.js: the old copy handler (button.copy in .code-row) is
removed. A show-when rule now applies to every element it matches,
and hiding adds Bulma's is-hidden beside the hidden attribute - a
.box or a grid row has a display of its own and stayed visible.
- The user form hides both domain lists again when the role is Global
(Box.DomainPick, an attribute; data-global-role on the select).
- Tests of the old layout are deleted; what they asserted about the
shell (version only signed in, the menu follows the user's reach,
Inbound only with the feature, Help present, the current entry
marked) is asserted on the kit pages, two tests added for it.
For the owner: is-hidden is a Bulma class outside the subset of
check.py. No template writes it - the script sets it at run time to
hide a block - so no guard sees it; say if it should be on the list.
Evidence
- docs/assets/panel-redesign/evidence/out-queue.png: 1440 px wide,
headless Edge, the real template served locally with its fixture -
NOT from the container (no Docker on this machine).
- panel/audit.js on out-queue, user and backup at 1440 and 1280:
366 texts checked, problems: [].
- In a browser against the local fixture server: the encryption
fields of Backup, the manual / auto limit fields and the custom
report address of Domain settings show and hide with their control;
all 28 fixture pages answer 200 with panel.css, /static/legacy.css
answers 404.
- Not exercised: polling against a real postqueue (none on Windows).
- go test -count=1 ./internal/web/... ./cmd/panel
ok internal/web
ok internal/web/auth
ok internal/web/handlers
ok internal/web/validate
ok internal/web/view
ok cmd/panel
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,862 +0,0 @@
|
||||
/* Panel stylesheet. It lives in a file rather than in a <style> block in the
|
||||
layout so the panel's Content-Security-Policy can be a plain
|
||||
"default-src 'self'" with no inline-style exemption. Any rule
|
||||
added here must therefore stay here: an inline style="..." attribute in a
|
||||
template is blocked by that policy and silently does nothing.
|
||||
|
||||
The palette and the type below come from the mark's own approval sheet
|
||||
(docs/assets/selfpost-proof.html) — brick, warm paper, IBM Plex. Before it
|
||||
the panel was a default blue-on-cool-grey admin theme with the stamp pasted
|
||||
onto it, and the two read as different pieces of work. */
|
||||
|
||||
/* The fonts ship with the panel rather than being asked for by name. Two
|
||||
reasons, and the second is the one that matters. A named family resolves to
|
||||
whatever the operator's machine has, so the mark — Plex converted to
|
||||
outlines — would be the only Plex on the page. And ui-monospace is Consolas,
|
||||
SF Mono or DejaVu Sans Mono depending on that machine, each with its own
|
||||
advance width: the six-column send log and the mail.log tables would lay out
|
||||
differently for every operator. Served from the panel's own origin, so the
|
||||
policy above already covers them and needs no font-src of its own.
|
||||
One variable file for the sans (100–700 in 46 KB, less than the four static
|
||||
weights it replaces); mono has no variable release, so it is two files.
|
||||
swap, so a cold load shows the system stack for a frame instead of nothing. */
|
||||
@font-face {
|
||||
font-family: "IBM Plex Sans"; font-style: normal; font-weight: 100 700;
|
||||
font-display: swap; src: url("/static/ibm-plex-sans.woff2") format("woff2");
|
||||
}
|
||||
@font-face {
|
||||
font-family: "IBM Plex Mono"; font-style: normal; font-weight: 400;
|
||||
font-display: swap; src: url("/static/ibm-plex-mono-400.woff2") format("woff2");
|
||||
}
|
||||
@font-face {
|
||||
font-family: "IBM Plex Mono"; font-style: normal; font-weight: 600;
|
||||
font-display: swap; src: url("/static/ibm-plex-mono-600.woff2") format("woff2");
|
||||
}
|
||||
|
||||
/* Colour tokens. Light values live on :root; the dark media query below
|
||||
reassigns the same names rather than re-declaring every rule that uses
|
||||
them, so a rule needs !important nowhere in this file — the custom
|
||||
property already carries the right value for the active scheme. */
|
||||
:root {
|
||||
color-scheme: light dark;
|
||||
--font-sans: "IBM Plex Sans", system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
|
||||
--font-mono: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||
--bg: #f4f2ed; --fg: #12161c;
|
||||
--muted: #6b7684; /* secondary text: .muted, th, timestamps, footers */
|
||||
--card-bg: #fff;
|
||||
--border: #dedcd7; /* dividers: card, table, app, nav, code, encrypt-fields */
|
||||
--control-border: #cbc8c1; /* input, select, textarea, action buttons */
|
||||
--input-bg: #fff;
|
||||
--code-bg: #efede9;
|
||||
--surface-bg: #eae7e0; --surface-bg-hover: #e0dcd3; --surface-open-bg: #d8d3c8;
|
||||
--nav-active-bg: #ede4de;
|
||||
/* Brick, the mark's own colour, is the panel's accent — but as two values,
|
||||
not one. A filled control carries --accent-fill with white on it; a link or
|
||||
an active entry carries --accent-text, because the fill on a page
|
||||
background is a colour to sit behind text, not to be text. In the light
|
||||
scheme the two coincide. */
|
||||
--accent-fill: #7a3b2e; --accent-fill-hover: #632f25; --accent-text: #7a3b2e;
|
||||
--on-accent: #fff;
|
||||
--flash-bg: #edf6ef; --flash-border: #c2e0cc; --flash-fg: #2c6b43;
|
||||
--credential-bg: #fcf6e4; --credential-border: #e0c874;
|
||||
--danger-fill: #b42318; --danger-fill-hover: #912018;
|
||||
--danger-bg: #fbedea; --danger-border: #efccc4; --danger-fg: #b42318; --danger-bg-hover: #f7dfda;
|
||||
--st-ok-bg: #edf6ef; --st-ok-fg: #2c6b43; --st-ok-border: #c2e0cc;
|
||||
--st-warn-bg: #fbf2e2; --st-warn-fg: #8a5510; --st-warn-border: #ebd5a6;
|
||||
--st-error-bg: #fbedea; --st-error-fg: #b42318; --st-error-border: #efccc4;
|
||||
--st-unknown-bg: #efede9; --st-unknown-fg: #6b7684; --st-unknown-border: #dedcd7;
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--bg: #16181b; --fg: #e9e6e0;
|
||||
--muted: #9aa1a9;
|
||||
--card-bg: #1d2024;
|
||||
--border: #2c2f34;
|
||||
--control-border: #3a3e44;
|
||||
--input-bg: #14161a;
|
||||
--code-bg: #14161a;
|
||||
--surface-bg: #23262b; --surface-bg-hover: #2c3036; --surface-open-bg: #343941;
|
||||
--nav-active-bg: #2a1f1b;
|
||||
/* Brick as drawn is about 2:1 against this background — a link nobody can
|
||||
read. The fill lightens just enough to keep white on it above 4.5:1
|
||||
(6.9:1 as set), and the text value lightens further to clear the same bar
|
||||
as text on the page. This is where the two values earn their keep. */
|
||||
--accent-fill: #8e4535; --accent-fill-hover: #a0503e; --accent-text: #ce7b66;
|
||||
--flash-bg: #132318; --flash-border: #22452f; --flash-fg: #7fcb9b;
|
||||
--credential-bg: #26210d; --credential-border: #5e5013;
|
||||
--danger-fill: #9b2c22; --danger-fill-hover: #b0342a;
|
||||
--danger-bg: #2a1412; --danger-border: #5e2721; --danger-fg: #eb9b92; --danger-bg-hover: #381a17;
|
||||
--st-ok-bg: #132318; --st-ok-fg: #7fcb9b; --st-ok-border: #22452f;
|
||||
--st-warn-bg: #2a2109; --st-warn-fg: #e5be72; --st-warn-border: #5e4b12;
|
||||
--st-error-bg: #2a1412; --st-error-fg: #eb9b92; --st-error-border: #5e2721;
|
||||
--st-unknown-bg: #23262b; --st-unknown-fg: #9aa1a9; --st-unknown-border: #2c2f34;
|
||||
}
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
/* The scrollbar's width is reserved whether or not the page is long enough to
|
||||
need one. Without it a short page (Account) and a long one (Status) are laid
|
||||
out in viewports differing by the scrollbar's width, and everything on the
|
||||
page — the navigation column included — shifts sideways on the way between
|
||||
them. The panel is a set of pages an operator moves through, so that shift
|
||||
read as the layout jumping. */
|
||||
html { scrollbar-gutter: stable; }
|
||||
body {
|
||||
font: 400 15px/1.5 var(--font-sans);
|
||||
margin: 0; padding: 2rem 1rem; background: var(--bg); color: var(--fg);
|
||||
}
|
||||
/* The two columns — navigation on the left, page on the right — centred as a
|
||||
pair. The navigation is a fixed width and the page keeps the measure it had:
|
||||
the column is added beside the content, not taken out of it. */
|
||||
.shell { display: flex; justify-content: center; align-items: flex-start; gap: 1.75rem; }
|
||||
/* One column for every page. It used to be the reading measure, 48rem, which
|
||||
the three pages holding data widened to 64rem — and since the shell centres
|
||||
the navigation and the page as a pair, that difference moved the navigation
|
||||
column and the left edge of every card on the way between two pages. The
|
||||
column is now the wider of the two everywhere, so nothing moves; the reading
|
||||
measure is kept inside it by the rule below rather than by the column.
|
||||
No auto margins: inside the shell they would eat the free space and push the
|
||||
navigation column off to the far edge, so the centring is the shell's job
|
||||
(justify-content above). */
|
||||
main { flex: 1 1 auto; min-width: 0; width: 100%; max-width: 64rem; }
|
||||
/* 48rem is a reading measure: right for the forms and prose that make up most
|
||||
of the panel, and too narrow for the pages that are tables of data. Every
|
||||
page therefore keeps its content at that measure, centred in the column, and
|
||||
the pages made of data opt out (main.wide below) and take the column whole.
|
||||
The cap is on <main>'s children rather than on .card so a page's heading, its
|
||||
back link and the version footer travel with the cards: capping the cards
|
||||
alone would leave the heading spanning the column with the card it belongs to
|
||||
floating in the middle — the misalignment that gave login and setup their own
|
||||
narrow column (main.page-login further down). */
|
||||
main > * { max-width: 48rem; margin-left: auto; margin-right: auto; }
|
||||
/* The four monitoring pages are the ones that opt out: six columns of send
|
||||
log, raw mail.log lines that are long by nature — which at the measure left
|
||||
Subject and Status fighting over the last inch and wrapped every second log
|
||||
line — and a single delivery's page, which stands two cards side by side over
|
||||
a block of those same log lines. The class is declared by the page itself
|
||||
(the "wide" block in layout.html) rather than derived from the page name: a
|
||||
page's name is the navigation entry it belongs under, which several pages
|
||||
share, and how wide a page needs to be is a property of that page alone. */
|
||||
main.wide > * { max-width: none; }
|
||||
/* Longhands, not the margin shorthand: the shorthand would re-zero the auto
|
||||
side margins above and pin the heading to the column's edge, a measure's
|
||||
worth to the left of the card under it. */
|
||||
/* Light rather than bold, which is the sheet's own setting for the name and
|
||||
the one thing a variable font makes free. It is the page's title, not an
|
||||
alarm: the size already says which line is the heading, and at 700 it shouted
|
||||
over the card under it. */
|
||||
h1 { font-size: 1.5rem; font-weight: 300; letter-spacing: -0.01em; margin-top: 0; margin-bottom: 1rem; }
|
||||
/* The full mark, on the two pages that have no navigation bar to carry the
|
||||
compact one. It takes the column's width so its edges line up with the card
|
||||
below it, capped at that column's own 24rem; height stays automatic because
|
||||
the stamp's proportions are part of the mark and it is never scaled unevenly.
|
||||
The subtitle stops resolving below 280px, which a 320px viewport still clears
|
||||
once the body's padding is taken off. */
|
||||
.mark { display: block; width: 100%; max-width: 24rem; height: auto; margin-bottom: 1.4rem; }
|
||||
/* The radius comes down from 10px across the panel: the mark is a stamp with a
|
||||
square edge and a hairline, and a card rounded like a phone widget sat in
|
||||
front of it as a different kind of object. 6px on the containers, 5px on the
|
||||
controls inside them. */
|
||||
.card {
|
||||
background: var(--card-bg); border: 1px solid var(--border); border-radius: 6px;
|
||||
padding: 1.5rem; margin: 0 auto;
|
||||
}
|
||||
.card.narrow { max-width: 24rem; }
|
||||
/* A narrow card as a direct child of main means the page is a short form:
|
||||
every sibling in the column (heading, flash, footer) must share that 24rem
|
||||
measure, or they centre on 48rem while the card centres on 24rem and the
|
||||
left edges drift by half the difference (12rem / 192px at a 16px root —
|
||||
measured). The column itself stays 64rem so the navigation does not move;
|
||||
only the stack inside the column narrows. :has tracks the card rather than
|
||||
a per-page class, so a new signed-in page that adds .card.narrow inherits
|
||||
the alignment without a second stylesheet rule.
|
||||
login/setup already shrink main to 24rem (below); this rule is then a no-op
|
||||
relative to the parent. Do not replace that with :has alone: narrowing main
|
||||
is what centres the signed-out block on the page when there is no nav. */
|
||||
main:has(> .card.narrow) > * { max-width: 24rem; }
|
||||
label { display: block; font-weight: 600; margin: 0.9rem 0 0.3rem; }
|
||||
/* Checkbox rows (domain assignment, encrypt toggle) opt out of the block
|
||||
label and full-width input rules above: the box and its caption sit on one
|
||||
line. Without this the caption drops under a stretched checkbox. */
|
||||
label.check {
|
||||
display: flex; align-items: center; gap: 0.5rem; margin: 0.45rem 0 0; font-weight: 600;
|
||||
cursor: pointer;
|
||||
}
|
||||
label.check input { width: auto; margin: 0; padding: 0; border: 0; border-radius: 0; background: none; }
|
||||
input {
|
||||
width: 100%; padding: 0.55rem 0.7rem; font-family: inherit; font-size: 1rem;
|
||||
border: 1px solid var(--control-border); border-radius: 5px; background: var(--input-bg); color: inherit;
|
||||
}
|
||||
/* Domain assignment on the user form: a bordered group under Role, not another
|
||||
stacked field label. Legend replaces the usual label; the muted line is the
|
||||
hint that used to sit where a field caption would. */
|
||||
fieldset {
|
||||
margin: 0.9rem 0 0; padding: 0.55rem 0.85rem 0.85rem;
|
||||
border: 1px solid var(--control-border); border-radius: 5px;
|
||||
}
|
||||
fieldset legend { padding: 0 0.25rem; font-weight: 600; }
|
||||
fieldset > .muted { margin: 0.15rem 0 0.35rem; font-size: 0.85rem; font-weight: 400; }
|
||||
fieldset > label.check:first-of-type { margin-top: 0.15rem; }
|
||||
/* One vocabulary for actions. Anything that performs an action looks like 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).
|
||||
Several of these used to render as bold blue text instead — a POST wrapped
|
||||
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
|
||||
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
|
||||
<a> is left for links that read as part of a sentence or a list. */
|
||||
button, a.btn, a.danger {
|
||||
display: inline-block; margin-top: 1.2rem; padding: 0.6rem 1.1rem;
|
||||
font: inherit; font-size: 1rem; font-weight: 600; text-decoration: none;
|
||||
color: var(--on-accent); background: var(--accent-fill); border: 0; border-radius: 5px; cursor: pointer;
|
||||
}
|
||||
button:hover, a.btn:hover { background: var(--accent-fill-hover); }
|
||||
button:disabled, button.danger:disabled {
|
||||
opacity: 0.45; cursor: not-allowed;
|
||||
}
|
||||
button:disabled:hover { background: var(--accent-fill); }
|
||||
button.danger:disabled:hover { background: var(--danger-fill); }
|
||||
select:disabled {
|
||||
opacity: 0.65; cursor: not-allowed; background: var(--code-bg);
|
||||
}
|
||||
.error { color: var(--danger-fg); margin: 0.6rem 0 0; font-weight: 600; }
|
||||
.muted { color: var(--muted); }
|
||||
/* Links take the accent. The panel never set a colour here and ran on the
|
||||
browser's own blue, which passed unnoticed while the accent was itself blue;
|
||||
against brick it left every domain name and back link belonging to a palette
|
||||
the panel no longer uses. The controls that are links dressed as buttons
|
||||
(a.btn, a.danger, a.st) all carry a class and out-specify this. */
|
||||
a { color: var(--accent-text); }
|
||||
.topbar { display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 1.2rem; }
|
||||
.topbar .actions { display: flex; gap: 0.9rem; align-items: baseline; }
|
||||
/* form.inline only exists so a POST can sit next to other content without a
|
||||
form's block layout; its button is styled like any other. */
|
||||
form.inline { display: inline; margin: 0; }
|
||||
/* The signed-out pages are a single card and nothing else, and .card.narrow
|
||||
centres itself inside whatever holds it — so at the panel's usual width the
|
||||
card floated in the middle while the mark and the heading stayed at the far
|
||||
left, three alignments on a page with four elements. Narrowing the column to
|
||||
the card's own width makes the three line up and puts the block as a whole in
|
||||
the middle of the page.
|
||||
By page name and not by "the page holds a narrow card": signed-in pages that
|
||||
hold a narrow card keep the 64rem column (see main:has(> .card.narrow)
|
||||
above) so the navigation stays put; only these two pages have no nav and
|
||||
need the column itself narrowed. */
|
||||
main.page-login, main.page-setup { max-width: 24rem; }
|
||||
.card + .card { margin-top: 1.2rem; }
|
||||
.flash { background: var(--flash-bg); border: 1px solid var(--flash-border); color: var(--flash-fg); padding: 0.7rem 1rem; border-radius: 6px; margin-bottom: 1.2rem; }
|
||||
/* RateLimitErr renders as .flash.error: a validation failure, not a success
|
||||
notice. Without this rule it inherited the success surface (green) and only
|
||||
.error's text colour, so a rejected rate limit read as good news in red
|
||||
text. Same box, danger palette. */
|
||||
.flash.error { background: var(--danger-bg); border-color: var(--danger-border); color: var(--danger-fg); }
|
||||
table { width: 100%; border-collapse: collapse; }
|
||||
/* A table column is at least as wide as the longest unbreakable run inside it,
|
||||
and the panel's tables are full of runs with nothing to break on: email
|
||||
addresses, domains, queue ids. One 40-character recipient was enough to widen
|
||||
the send-log past its card and hang Status over the edge. Cells may break mid
|
||||
word, so a column can always be squeezed to the width available. */
|
||||
th, td { text-align: left; padding: 0.5rem 0.4rem; border-bottom: 1px solid var(--border); overflow-wrap: anywhere; }
|
||||
/* The exceptions: a timestamp broken across two lines is unreadable, and so is
|
||||
a send-log status — "deferred" was being hyphenless-broken into "deferre" and
|
||||
"d" whenever the row was tight. Both are short enough to never be the reason
|
||||
a row does not fit. */
|
||||
td.time, td.status { white-space: nowrap; }
|
||||
/* Column headings are set in the mono face: they are labels for machine data
|
||||
rather than prose, and the wider tracking a small monospaced capital wants
|
||||
also holds them apart from the first row of values. Same treatment on
|
||||
.fact-label, so a heading of that size reads as one thing wherever it
|
||||
appears. */
|
||||
th {
|
||||
font-family: var(--font-mono); font-size: 0.75rem; font-weight: 500;
|
||||
text-transform: uppercase; letter-spacing: 0.08em; color: var(--muted);
|
||||
}
|
||||
/* The controls at the end of a row are short and fixed — Details, Delete — and
|
||||
the cell is the last one, so it is the one the automatic layout squeezes
|
||||
first: in the send log it was breaking "Details" after "Detail". Holding it
|
||||
on one line costs the row nothing and hands the width back to the status
|
||||
column, which was wrapping single words for the same reason. */
|
||||
td.actions { text-align: right; white-space: nowrap; }
|
||||
/* Subject is the one cell whose text we do not control. Breaking mid word (the
|
||||
rule above) keeps it inside the card, but a long subject would do it by
|
||||
growing the row several lines tall, which buries the rows around it. So the
|
||||
subject is clipped to one line instead, with the whole of it in the tooltip.
|
||||
The clamp sits on an inner block box rather than the cell because max-width
|
||||
on a <td> is only advisory in the automatic table layout. */
|
||||
td.subject span {
|
||||
display: block; max-width: 18rem;
|
||||
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
||||
}
|
||||
.code { display: block; white-space: pre-wrap; word-break: break-all; font-family: var(--font-mono);
|
||||
font-size: 0.85rem; background: var(--code-bg); border: 1px solid var(--border); border-radius: 5px; padding: 0.7rem 0.8rem; margin: 0.3rem 0 0; }
|
||||
/* Semibold, not the browser's bold: a card's title sits a step under the page's
|
||||
own heading, and 700 against a 300 h1 read as the louder of the two. */
|
||||
h2 { font-size: 1.05rem; font-weight: 600; margin: 0 0 0.4rem; }
|
||||
/* The back link is a block so it takes the measure and lines up with the
|
||||
heading above it and the card below: auto side margins centre a block, but
|
||||
they do nothing for an inline-block, which would have stayed at the column's
|
||||
edge while everything around it moved in. Its own text stays at the left of
|
||||
that block — only the click target grows to the line, as it does for the
|
||||
navigation entries. */
|
||||
.back { display: block; margin-bottom: 1rem; }
|
||||
/* Build version, closing every authenticated page. Quiet on purpose: it is
|
||||
reference material, not something to read on the way past. */
|
||||
.version { margin-top: 1.6rem; text-align: right; font-size: 0.8rem; color: var(--muted); line-height: 1.45; }
|
||||
.version a { color: inherit; text-decoration: underline; text-underline-offset: 2px; }
|
||||
.version a:hover { color: var(--fg); }
|
||||
/* The signed-out column is 24rem; a right-aligned multi-link notice wraps
|
||||
into a ragged edge, so centre it there. */
|
||||
main.page-login .version, main.page-setup .version { text-align: center; }
|
||||
select, textarea {
|
||||
width: 100%; padding: 0.55rem 0.7rem; font-size: 1rem;
|
||||
border: 1px solid var(--control-border); border-radius: 5px; background: var(--input-bg); color: inherit;
|
||||
font-family: inherit;
|
||||
}
|
||||
textarea { resize: vertical; }
|
||||
button.danger, a.danger { background: var(--danger-fill); }
|
||||
button.danger:hover, a.danger:hover { background: var(--danger-fill-hover); }
|
||||
/* The disclosure toggle is an action too, so it is drawn as a button (see the
|
||||
compact rule below); it carries no marker because the pressed background
|
||||
already shows the open state. */
|
||||
.actions > label.toggle { display: inline-block; cursor: pointer; }
|
||||
/* 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>
|
||||
panels with textareas — never fit the panel's default width. The controls
|
||||
wrapped into a staircase, .code on the login cell grew into a slab as tall
|
||||
as the row, and the two text cells sat on the baseline halfway down it. One
|
||||
block per application gives the identity a line of its own and the
|
||||
controls a row of their own, at the width they actually need. */
|
||||
.apps { list-style: none; margin: 1.2rem 0 0; padding: 0; }
|
||||
.app { padding: 0.9rem 0; border-top: 1px solid var(--border); }
|
||||
.app:last-child { padding-bottom: 0; }
|
||||
.app-login { margin: 0; font-family: var(--font-mono); font-weight: 600; }
|
||||
.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; }
|
||||
/* Mode and rate limit open under the whole row rather than inside it. As
|
||||
<details> the panel sat where its summary was, so opening one cut the row of
|
||||
controls in half and pushed the rest below a block of fields — the buttons
|
||||
moved every time a panel opened or closed. The panel is therefore the last
|
||||
child of the row, and the checkbox that opens it 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-edit:checked ~ .panel-edit { display: block; }
|
||||
.app .actions > .t-edit:checked ~ .for-edit { background: var(--surface-open-bg); }
|
||||
.app .actions > .t-edit:focus-visible ~ .for-edit { outline: 2px solid var(--accent-fill); 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; }
|
||||
.app .actions > .panel .check-cols { margin-top: 0.6rem; }
|
||||
.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="auth_allowed_ips"] {
|
||||
margin-top: 0.45rem;
|
||||
min-height: 2.55rem;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.app .actions > .panel .check-col > .muted + form > textarea[name="auth_allowed_ips"] {
|
||||
field-sizing: fixed;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.credential { border-color: var(--credential-border); background: var(--credential-bg); }
|
||||
/* Panel navigation: rendered once from the layout, so it is present on every
|
||||
authenticated page without each content template having to include it. */
|
||||
/* A column down the left edge rather than a bar across the top. The six page
|
||||
entries need about 660px and the session block another 260px, against the
|
||||
738px the panel was wide, so as a bar it had to be split over two rows — and
|
||||
even then it cost the top of every page. Standing it up removes that: the
|
||||
entries share one left edge to scan down, the session sits at the foot where
|
||||
it is out of the way. Sticky, so the list stays in view while the page
|
||||
scrolls past it. The layout template lists the blocks in the order they are
|
||||
drawn, so reading and tab order follow the eye without a CSS `order`. */
|
||||
.nav {
|
||||
position: sticky; top: 2rem; align-self: flex-start;
|
||||
flex: none; width: 13.5rem;
|
||||
display: flex; flex-direction: column; align-items: stretch; gap: 0.75rem;
|
||||
/* A viewport shorter than the column would otherwise cut off whatever hangs
|
||||
below the fold — with no page scroll left to reach it, since the column is
|
||||
stuck to the viewport. */
|
||||
max-height: calc(100vh - 4rem); overflow-y: auto;
|
||||
}
|
||||
.nav .links, .nav .session { display: flex; flex-direction: column; gap: 0.1rem; }
|
||||
/* The session is the column's last block and the only one that is not
|
||||
navigation, so it is ruled off from the entries above it. */
|
||||
.nav .session { padding-top: 0.75rem; border-top: 1px solid var(--border); }
|
||||
/* The signed-in administrator's name: a label above the two controls, not one
|
||||
of them. It may be long and there is nothing to break it on, so it is allowed
|
||||
to break mid word rather than widen the column. */
|
||||
.nav .session .muted { padding: 0 0.6rem; font-size: 0.85rem; overflow-wrap: anywhere; }
|
||||
.nav .session-user {
|
||||
display: flex; align-items: center; gap: 0.5rem;
|
||||
}
|
||||
/* The mark is a link, but not one of the column's entries: it takes none of the
|
||||
padding and rounding the entry rule below applies, so its own edge lines up
|
||||
with the entries' icons rather than sitting half a step inside them. */
|
||||
.nav .brand { padding: 0; }
|
||||
/* The mark takes the column's full width, so its edges line up with the entries
|
||||
below it rather than ending halfway across — the same way .mark fills the
|
||||
card column on the pages that carry no navigation. width/height are on the
|
||||
element too, so the column reserves the right height before the SVG has
|
||||
loaded; height stays automatic because the stamp's proportions are part of
|
||||
the mark — it is never scaled unevenly. */
|
||||
.nav .brand img { display: block; width: 100%; height: auto; }
|
||||
/* Each entry pairs an icon with its label, so the entry itself is a flex row
|
||||
rather than a run of text — that is also why the entries centre their items
|
||||
instead of aligning them on the text baseline. In the column the entry fills
|
||||
the width, so the whole strip is the click target and the hover and active
|
||||
backgrounds read as one stack of bars. Account is included: it is a page like
|
||||
the others and would otherwise be the one bare word in the column. */
|
||||
/* No underline: in a row the entries were separated by space alone and the
|
||||
underline was what marked them as links, but a column of them reads as a
|
||||
list of links already, and eight underlines down the left edge is a lot of
|
||||
line for no added meaning. The hover background carries the affordance. */
|
||||
/* The entries are page text, not accented links: a column of eight brick rows
|
||||
would leave nothing for the current one to stand out from — that is what the
|
||||
active entry's colour is for. */
|
||||
.nav a, .nav [aria-current] {
|
||||
display: flex; align-items: center; gap: 0.5rem;
|
||||
padding: 0.35rem 0.6rem; border-radius: 5px; text-decoration: none;
|
||||
color: var(--fg);
|
||||
}
|
||||
.nav a:hover { background: var(--surface-bg); }
|
||||
/* The marker moved with the bar: a rule under the entry made sense in a row,
|
||||
where the entries sat side by side; standing up, the current entry is marked
|
||||
down its leading edge, which is the edge every entry shares. */
|
||||
.nav [aria-current] {
|
||||
font-weight: 600; color: var(--accent-text); background: var(--nav-active-bg);
|
||||
box-shadow: inset 2px 0 0 var(--accent-fill);
|
||||
}
|
||||
/* Sign out carries an icon too, so it needs the same row layout; its colours
|
||||
come from the compact button rule further down, and its type is put back to
|
||||
the column's beside it (see the rule after that one). */
|
||||
.nav button { display: flex; align-items: center; gap: 0.5rem; }
|
||||
/* The icons draw in the entry's own colour, so the active entry's darker text,
|
||||
a link's blue and Sign out's red all carry through without a rule apiece. */
|
||||
.nav .icon { width: 1rem; height: 1rem; flex: none; }
|
||||
/* Jumping to a card (in-page links) should not leave it touching the top edge. */
|
||||
.card[id] { scroll-margin-top: 1rem; }
|
||||
/* Below the width the two columns need (13.5rem of navigation, 1.75rem of gap
|
||||
and the 48rem measure, plus the body's padding — the measure, not the column
|
||||
it sits in: a page column short of its 64rem is only a page column that has
|
||||
not reached its cap, while a measure that no longer fits is a page that has),
|
||||
the column lies back down
|
||||
into a bar above the page — the same wrapping rows it used to be. A drawer
|
||||
behind a hamburger would save more height, but it would need script to open,
|
||||
and the panel's navigation is six entries: they fit. */
|
||||
@media (max-width: 66rem) {
|
||||
.shell { flex-direction: column; align-items: stretch; gap: 1.2rem; }
|
||||
main { margin: 0 auto; }
|
||||
.nav {
|
||||
position: static; width: auto; max-height: none; overflow-y: visible;
|
||||
flex-direction: row; flex-wrap: wrap; align-items: center; gap: 0.4rem 1.2rem;
|
||||
padding-bottom: 0.6rem; border-bottom: 1px solid var(--border);
|
||||
}
|
||||
/* Lying down there is no column for the mark to span, and at its full width
|
||||
it would take a row to itself above the entries, so it goes back to the
|
||||
compact size it had — enough to read beside them. */
|
||||
.nav .brand img { width: 110px; }
|
||||
/* Each block keeps its own group of entries together and wraps as one; the
|
||||
rules that separated the blocks vertically become the space between them. */
|
||||
.nav .links, .nav .session {
|
||||
flex-direction: row; flex-wrap: wrap; align-items: center;
|
||||
gap: 0.2rem 0.6rem; padding-top: 0; border-top: 0;
|
||||
}
|
||||
}
|
||||
/* Status badges: one vocabulary (ok/warn/error/unknown) shared by the server
|
||||
status page and the per-domain DNS checks, so a colour means the same thing
|
||||
everywhere. The class suffix is the check's own status value. */
|
||||
/* Mono and squared off rather than a bold uppercase pill: the badge carries a
|
||||
value the server produced (ok, warn, delivered, bounced), and the mono face
|
||||
is where the panel puts machine output everywhere else. Uppercasing it was
|
||||
the pill's own emphasis — dropped with the pill, since the colour already
|
||||
says how loud the badge is. */
|
||||
/* line-height 1 and the padding doing the centring, rather than the 1.5 the
|
||||
badge would inherit from the body. IBM Plex Mono sits a shade low in its em
|
||||
square, so equal padding leaves "ok" / "warn" sunk in the box (and visibly
|
||||
below the heading or label it sits next to). The bottom padding is therefore
|
||||
heavier than the top — enough to lift the word to the optical centre. A
|
||||
top-heavy pad was tried earlier and made the sink worse. Words with a
|
||||
descender (queued) hang into that bottom pad rather than growing the badge. */
|
||||
.st {
|
||||
display: inline-block; padding: 0.14rem 0.45rem 0.28rem; border-radius: 4px;
|
||||
font-family: var(--font-mono); font-size: 0.78rem; font-weight: 500; line-height: 1;
|
||||
letter-spacing: 0.02em;
|
||||
vertical-align: middle; border: 1px solid transparent;
|
||||
}
|
||||
.st-ok { background: var(--st-ok-bg); color: var(--st-ok-fg); border-color: var(--st-ok-border); }
|
||||
.st-warn { background: var(--st-warn-bg); color: var(--st-warn-fg); border-color: var(--st-warn-border); }
|
||||
.st-error { background: var(--st-error-bg); color: var(--st-error-fg); border-color: var(--st-error-border); }
|
||||
.st-unknown { background: var(--st-unknown-bg); color: var(--st-unknown-fg); border-color: var(--st-unknown-border); }
|
||||
/* In the domain list the badge is also the link to that domain's DNS section,
|
||||
so it must not pick up the link colour and underline that would fight with
|
||||
the badge's own palette. */
|
||||
a.st, a.st:hover { color: inherit; text-decoration: none; }
|
||||
a.st:hover { filter: brightness(1.08); }
|
||||
/* Usage bars on the status page's machine card. <meter> rather than a div sized
|
||||
from the reading, because the CSP forbids inline styles (see the note at the
|
||||
top of this file) and a bar's length has to travel on an attribute. The
|
||||
element grades itself from low/high/optimum, so the colour matches the
|
||||
badges' meaning without this file restating the thresholds — and a browser
|
||||
that does not render meters falls back to the percentage beside it, which is
|
||||
printed either way. */
|
||||
meter { width: 5rem; height: 0.7rem; vertical-align: middle; margin-right: 0.4rem; }
|
||||
/* The card's own two narrow columns. Cells may break mid word by default (see
|
||||
the th, td rule above), which the detail column needs and these two must not
|
||||
have: the resource names and the readings are short, and the long detail
|
||||
beside them would otherwise win the width and leave "Memory" broken across
|
||||
two lines. */
|
||||
.metric { white-space: nowrap; }
|
||||
/* Copy sits at the top of the value — right for a long DKIM key. The row's
|
||||
.code uses the compact button's vertical padding so a one-line value is the
|
||||
same height as Copy and does not leave the button hanging under an empty
|
||||
band of padding. */
|
||||
.code-row { display: flex; align-items: flex-start; gap: 0.5rem; }
|
||||
.code-row .code {
|
||||
flex: 1; min-width: 0; margin-top: 0;
|
||||
padding-top: 0.45rem; padding-bottom: 0.45rem;
|
||||
}
|
||||
/* A short field with its submit on the same row (Add domain). The button
|
||||
keeps the card's usual filled look; its top margin is for stacked forms. */
|
||||
.input-row { display: flex; align-items: stretch; gap: 0.5rem; }
|
||||
.input-row input { flex: 1; min-width: 0; }
|
||||
.input-row button { margin-top: 0; flex: none; white-space: nowrap; }
|
||||
|
||||
/* Two cards abreast, on the one page that has a pair of them worth reading
|
||||
together (a delivery's facts beside its history). auto-fit with a minimum
|
||||
rather than a media query: the pair falls into one column exactly when a
|
||||
column would drop below the width its content needs, which is also what
|
||||
happens inside the narrow column the navigation lies down at. */
|
||||
.split {
|
||||
display: grid; grid-template-columns: repeat(auto-fit, minmax(22rem, 1fr));
|
||||
gap: 1.2rem; margin-top: 1.2rem;
|
||||
}
|
||||
/* Cards use margin: 0 auto so a lone card centres in the column. Inside the
|
||||
grid those auto side margins absorb free space and shrink each card to its
|
||||
content instead of filling the 1fr track — so cancel them here, and allow
|
||||
the cell to shrink below the card's intrinsic minimum when the column is
|
||||
tight. */
|
||||
.split > .card { margin-left: 0; margin-right: 0; min-width: 0; }
|
||||
/* 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
|
||||
card, which in one column reads as an uneven stack. The blocks around the
|
||||
grid keep their spacing from it instead. */
|
||||
.split > .card + .card { margin-top: 0; }
|
||||
.split + .card { margin-top: 1.2rem; }
|
||||
.card + .split { margin-top: 1.2rem; }
|
||||
/* Configuration sits outside the polled fragment; without this it would
|
||||
touch the last .split inside #status-body (a nested .split is not a sibling). */
|
||||
#status-body + .card { margin-top: 1.2rem; }
|
||||
/* Peer checks or paired forms inside one card (domain DNS status: two rows of
|
||||
two; domain settings; application edit). Same auto-fit idea as .split, so a
|
||||
wide card gets two columns — not three — and falls to one when narrow. */
|
||||
.check-cols {
|
||||
display: grid; grid-template-columns: repeat(auto-fit, minmax(22rem, 1fr));
|
||||
gap: 1rem 1.2rem; margin-top: 1rem;
|
||||
}
|
||||
.check-col { min-width: 0; }
|
||||
.check-col > label:first-child { margin-top: 0; }
|
||||
.check-col-title {
|
||||
margin: 1rem 0 0.35rem; font-size: 1.05rem; font-weight: 600;
|
||||
}
|
||||
.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
|
||||
one column when the parent is too narrow for both. */
|
||||
.field-pair {
|
||||
display: grid; grid-template-columns: repeat(auto-fit, minmax(8rem, 1fr));
|
||||
gap: 0 1rem; margin-top: 0.45rem;
|
||||
}
|
||||
.field-pair > div { min-width: 0; }
|
||||
.field-pair label { margin-top: 0.45rem; }
|
||||
/* Hint under a field-pair sits tight above the form's submit button (the
|
||||
global button margin-top would leave a blank line). */
|
||||
.field-pair + p.muted { margin: 0.45rem 0 0; }
|
||||
.field-pair + p.muted + button { margin-top: 0.45rem; }
|
||||
/* Host / name beside a DNS Type that is always TXT — size the type column to
|
||||
that token rather than giving it half the row. Vertical padding matches
|
||||
.code-row .code so TXT is the same height as Host when Copy sits beside it. */
|
||||
.field-pair.host-type {
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
}
|
||||
.field-pair.host-type .code {
|
||||
padding-top: 0.45rem; padding-bottom: 0.45rem;
|
||||
}
|
||||
.field-type { width: max-content; }
|
||||
.field-type .code {
|
||||
width: fit-content; min-width: 2.75rem; text-align: center; box-sizing: border-box;
|
||||
}
|
||||
/* The subject heads a delivery's page, and it is the one heading in the panel
|
||||
whose text we do not control: it may be a hundred characters with nothing to
|
||||
break on. It wraps to as many lines as it needs (this is the page's name, not
|
||||
a table cell to clip) but never widens the column. */
|
||||
h1.subject { overflow-wrap: anywhere; }
|
||||
/* The line under it: sender, recipient and outcome. Wrapping rather than
|
||||
clipping, since either address may be long and both are the point. The
|
||||
vertical margins are longhands for the reason given at the h1 rule above —
|
||||
the shorthand would re-zero the auto side margins that keep a page's blocks
|
||||
centred on the measure. It sits close under the heading: the two are one
|
||||
statement of what this page is about. */
|
||||
.route {
|
||||
display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem;
|
||||
margin-top: -0.5rem; margin-bottom: 1rem;
|
||||
}
|
||||
.route .addr { font-family: var(--font-mono); font-size: 0.9rem; overflow-wrap: anywhere; }
|
||||
.route .arrow { color: var(--muted); }
|
||||
/* A card's short facts, as a grid of tiles rather than the label-over-value
|
||||
stack the rest of the panel uses. That stack is right for the pages where a
|
||||
value is a DNS record or a password to copy — one per line, full width — and
|
||||
wrong for six values of a few characters each, which down a narrow column
|
||||
would be a page of mostly empty rows. Same auto-fit as .split: as many
|
||||
columns as fit, and one when nothing else does. */
|
||||
.facts { display: grid; grid-template-columns: repeat(auto-fit, minmax(9.5rem, 1fr)); gap: 0.5rem; margin-top: 1rem; }
|
||||
.fact { min-width: 0; padding: 0.5rem 0.7rem; border-radius: 6px; background: var(--surface-bg); }
|
||||
.fact-label {
|
||||
display: block; font-family: var(--font-mono); font-size: 0.7rem; font-weight: 500;
|
||||
text-transform: uppercase; letter-spacing: 0.1em; color: var(--muted);
|
||||
}
|
||||
/* Nothing here is guaranteed to have a space in it — domains, logins and queue
|
||||
ids are all one run — so a value may break mid word rather than push its tile
|
||||
wider than the column it shares. */
|
||||
.fact-value { display: block; margin-top: 0.1rem; overflow-wrap: anywhere; }
|
||||
.fact-value.mono { font-family: var(--font-mono); font-size: 0.85rem; }
|
||||
/* Retry-policy tiles carry a short phrase (`doubling, cap about 1 hour 7
|
||||
minutes`) rather than a domain or a queue id, so they need a slightly
|
||||
wider minimum than the delivery-page facts before wrapping to one column. */
|
||||
.facts.retry-facts { grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr)); }
|
||||
/* A message's history: the steps down a line, each with the time it happened,
|
||||
the status it reached and what that means. The line is the list's own left
|
||||
border and the dots sit on top of it, so nothing has to be positioned against
|
||||
a fixed row height — a step's text is two lines or five depending on what
|
||||
there is to say about it. */
|
||||
.timeline { list-style: none; display: flex; flex-direction: column; gap: 1.1rem;
|
||||
margin: 1rem 0 0; padding: 0.2rem 0 0.2rem 1.4rem; border-left: 2px solid var(--border); }
|
||||
.event { position: relative; min-width: 0; }
|
||||
.event::before {
|
||||
content: ""; position: absolute; left: -1.85rem; top: 0.3rem;
|
||||
width: 0.65rem; height: 0.65rem; border-radius: 50%;
|
||||
background: var(--card-bg); border: 2px solid var(--control-border);
|
||||
}
|
||||
/* The dot carries the step's own verdict, in the same four colours as the badge
|
||||
beside it — so the shape of the history is readable down the line alone. */
|
||||
.event.lvl-ok::before { border-color: var(--st-ok-fg); background: var(--st-ok-bg); }
|
||||
.event.lvl-warn::before { border-color: var(--st-warn-fg); background: var(--st-warn-bg); }
|
||||
.event.lvl-error::before { border-color: var(--st-error-fg); background: var(--st-error-bg); }
|
||||
/* The step that has not happened yet — a queued message's delivery report — is
|
||||
the one hollow dot, and its text is quiet: it is what to expect, not a record
|
||||
of anything. */
|
||||
.event.pending { opacity: 0.7; }
|
||||
.event.pending::before { background: var(--card-bg); border-style: dashed; }
|
||||
.event-time { margin: 0; font-family: var(--font-mono); font-size: 0.75rem; color: var(--muted); }
|
||||
.event-title { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem; margin: 0.15rem 0 0; font-weight: 600; }
|
||||
.event-detail { margin: 0.2rem 0 0; font-size: 0.9rem; }
|
||||
/* A message's own mail.log lines, as a table of when and what. The time column
|
||||
is sized to its content — width:1% is as narrow as the automatic layout will
|
||||
take a column, which with the nowrap on td.time settles on exactly the stamp
|
||||
— so everything left over goes to the text, which is the column that needs
|
||||
it. */
|
||||
table.log { margin-top: 1rem; }
|
||||
table.log th:first-child, table.log td.time { width: 1%; }
|
||||
/* The log's own text: monospace, because it is full of addresses, response
|
||||
codes and identifiers that are compared character by character rather than
|
||||
read as words. pre-wrap keeps the spacing a server's reply was written with
|
||||
while still letting a long line wrap inside the cell instead of widening it
|
||||
(the th/td rule above allows the break). */
|
||||
table.log td.log-text {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.8rem; white-space: pre-wrap;
|
||||
}
|
||||
/* Compact outlined button: same affordance as the filled one but quiet enough
|
||||
that several can sit together without shouting — the Copy buttons beside a
|
||||
value, the controls of a table row or of an application block. Sign out
|
||||
overrides this with .danger below since signing out is a deliberate,
|
||||
singular action. .actions is the shared hook: a cell that holds controls, or
|
||||
the control row of an application. */
|
||||
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;
|
||||
border-radius: 5px; white-space: nowrap;
|
||||
background: var(--surface-bg); color: var(--accent-text); border: 1px solid var(--control-border);
|
||||
}
|
||||
button.copy:hover, .actions button:hover, .actions > label.toggle:hover,
|
||||
.actions a.danger:hover, .nav button:hover { background: var(--surface-bg-hover); }
|
||||
button.copy { flex: none; }
|
||||
.actions button.danger, .actions a.danger, .nav button.danger {
|
||||
color: var(--danger-fg); background: var(--danger-bg); border-color: var(--danger-border);
|
||||
}
|
||||
.actions button.danger:hover, .actions a.danger:hover, .nav button.danger:hover { background: var(--danger-bg-hover); }
|
||||
/* Sign out takes the compact button's colours but not its type. The rule above
|
||||
is written for controls that cluster — a row of them beside a value or in a
|
||||
table cell — where 0.8rem/600 is what keeps four of them from shouting. In
|
||||
the navigation column it has no cluster to belong to: it stands under
|
||||
Settings, among entries set 0.95rem/400, and being smaller and heavier than
|
||||
every word around it made it read as a different kind of object rather than
|
||||
as the last item of the list. Type and padding go back to the entries';
|
||||
the red, the border and the background stay, and are what say it acts. */
|
||||
.nav button {
|
||||
padding: 0.35rem 0.6rem; font-size: 0.95rem; font-weight: 400;
|
||||
}
|
||||
|
||||
/* The optional "encrypt this download" block on the backup and export forms.
|
||||
The checkbox uses the shared label.check rule above; the fields it reveals
|
||||
are indented under it to read as its consequence rather than as three more
|
||||
fields of the form. panel.js hides the inner block until the box is ticked
|
||||
(and empties it when unticked); without JavaScript everything stays visible,
|
||||
which the server handles identically. The import form reuses the same
|
||||
indented .encrypt-fields look for its password field, but reveals it by
|
||||
file extension instead of a checkbox (see panel.js). */
|
||||
.encrypt { margin-top: 1.2rem; }
|
||||
.encrypt label.check { margin: 0; }
|
||||
.encrypt-fields {
|
||||
margin-top: 1.2rem; margin-left: 1.6rem; padding-left: 0.9rem;
|
||||
border-left: 2px solid var(--border);
|
||||
}
|
||||
.encrypt .encrypt-fields { margin-top: 0; }
|
||||
.encrypt-fields label { margin-top: 0.7rem; }
|
||||
.encrypt-fields .muted { margin: 0.5rem 0 0; font-size: 0.85rem; }
|
||||
|
||||
/* Screen-reader accessible controls that drive UI state (help drawer panes). */
|
||||
.ctrl { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); }
|
||||
|
||||
.page-head {
|
||||
display: flex; flex-wrap: wrap; align-items: baseline; justify-content: space-between;
|
||||
gap: 0.5rem 1rem; margin-bottom: 1rem;
|
||||
}
|
||||
.page-head h1 { margin: 0; }
|
||||
|
||||
.card-head {
|
||||
display: flex; flex-wrap: wrap; align-items: baseline; justify-content: space-between;
|
||||
gap: 0.5rem 0.75rem; margin-bottom: 0.7rem;
|
||||
}
|
||||
.card-head h2 { margin: 0; }
|
||||
|
||||
.help-link {
|
||||
flex: none; width: 1.35rem; height: 1.35rem; display: inline-flex; align-items: center;
|
||||
justify-content: center; border-radius: 50%; border: 1px solid var(--control-border);
|
||||
color: var(--muted); text-decoration: none; font-family: var(--font-mono);
|
||||
font-size: 0.75rem; font-weight: 600; background: var(--card-bg); cursor: pointer; margin: 0;
|
||||
}
|
||||
.help-link:hover { color: var(--accent-text); border-color: var(--accent-fill); }
|
||||
|
||||
/* Help drawer — CSS checkbox/radio, no script required. */
|
||||
.help-drawer {
|
||||
display: none; position: fixed; top: 0; right: 0; bottom: 0;
|
||||
width: min(26rem, 100vw); z-index: 30;
|
||||
background: var(--card-bg); border-left: 1px solid var(--border);
|
||||
padding: 1.25rem 1.35rem 2rem; overflow-y: auto;
|
||||
}
|
||||
html:has(input[name="help"]:checked):not(:has(#help-off:checked)) .help-drawer,
|
||||
html:has(input[name="help"]:checked):not(:has(#help-off:checked)) .help-scrim { display: block; }
|
||||
.help-pane { display: none; }
|
||||
html:has(#help-index:checked) .help-pane-index,
|
||||
html:has(#help-status:checked) .help-pane-status,
|
||||
html:has(#help-password:checked) .help-pane-password,
|
||||
html:has(#help-dns:checked) .help-pane-dns,
|
||||
html:has(#help-records:checked) .help-pane-records,
|
||||
html:has(#help-dmarc:checked) .help-pane-dmarc,
|
||||
html:has(#help-connection:checked) .help-pane-connection,
|
||||
html:has(#help-apps:checked) .help-pane-apps,
|
||||
html:has(#help-domain-settings:checked) .help-pane-domain-settings,
|
||||
html:has(#help-export:checked) .help-pane-export { display: block; }
|
||||
.help-scrim {
|
||||
display: none; position: fixed; inset: 0; z-index: 25;
|
||||
background: rgba(18, 22, 28, 0.28); cursor: pointer;
|
||||
}
|
||||
.help-drawer h2 { margin-top: 1.2rem; }
|
||||
.help-drawer h2:first-of-type,
|
||||
.help-pane h2 { margin-top: 0; }
|
||||
.help-drawer .toc { list-style: none; margin: 0.7rem 0 0; padding: 0; }
|
||||
.help-drawer .toc li { margin: 0.35rem 0 0; }
|
||||
.help-drawer .toc label {
|
||||
margin: 0; font-weight: 600; color: var(--accent-text); cursor: pointer;
|
||||
}
|
||||
.help-drawer .more { margin-top: 1.2rem; font-size: 0.85rem; }
|
||||
.help-drawer .more label {
|
||||
display: inline; margin: 0; font-weight: 600; color: var(--accent-text); cursor: pointer;
|
||||
}
|
||||
.help-close {
|
||||
position: absolute; top: 0.8rem; right: 0.8rem; margin: 0; padding: 0.35rem 0.6rem;
|
||||
font-size: 0.8rem; font-weight: 600; background: var(--surface-bg); color: var(--fg);
|
||||
border: 1px solid var(--control-border); border-radius: 5px; cursor: pointer;
|
||||
}
|
||||
@media (max-width: 66rem) {
|
||||
.help-drawer { width: 100vw; }
|
||||
}
|
||||
|
||||
/* DMARC pages: paired cards side by side, same layout as .split but for two
|
||||
summary cards at the top of a page. */
|
||||
.pair {
|
||||
display: grid; grid-template-columns: repeat(auto-fit, minmax(18rem, 1fr));
|
||||
gap: 1.2rem; margin-top: 1.2rem;
|
||||
}
|
||||
.pair > .card { margin-left: 0; margin-right: 0; min-width: 0; }
|
||||
.pair > .card + .card { margin-top: 0; }
|
||||
|
||||
/* Attention card: elevated border when a DMARC report has failures. */
|
||||
.card.attn { border-color: var(--st-warn-fg); }
|
||||
|
||||
/* Responsive visibility: hide elements on narrow/wide screens. */
|
||||
.desk-only { display: revert; }
|
||||
.phone-only, .phone-list { display: none; }
|
||||
@media (max-width: 48rem) {
|
||||
.desk-only { display: none !important; }
|
||||
.phone-only, .phone-list { display: revert !important; }
|
||||
}
|
||||
.phone-list { list-style: none; margin: 0; padding: 0; }
|
||||
.phone-list li { border-bottom: 1px solid var(--border); }
|
||||
.phone-list li:last-child { border-bottom: none; }
|
||||
.phone-list a {
|
||||
display: grid; grid-template-columns: auto 1fr; gap: 0.15rem 0.6rem;
|
||||
padding: 0.7rem 0; text-decoration: none; color: inherit;
|
||||
}
|
||||
.phone-list .when { grid-column: 1; font-family: var(--font-mono); font-size: 0.75rem; color: var(--muted); }
|
||||
.phone-list .meta { grid-column: 2; font-size: 0.85rem; }
|
||||
.phone-list .subj { grid-column: 1 / -1; font-size: 0.85rem; color: var(--muted); }
|
||||
|
||||
/* Checkbox-label row inside a form (IP restriction toggle). */
|
||||
.check-row {
|
||||
display: flex; align-items: center; gap: 0.5rem;
|
||||
margin: 0.5rem 0; cursor: pointer;
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 220 100" width="220" height="100" role="img" aria-label="SelfPost">
|
||||
<!-- SelfPost · https://github.com/mixeme/selfpost · AGPL-3.0
|
||||
Wordmark set in IBM Plex Sans (OFL) and converted to outlines: the
|
||||
file needs no font installed to render as drawn. -->
|
||||
<path d="M10.20 8.00 L12.83 8.00 A2.2 2.2 0 0 0 17.23 8.00 L19.87 8.00 A2.2 2.2 0 0 0 24.27 8.00 L26.90 8.00 A2.2 2.2 0 0 0 31.30 8.00 L33.94 8.00 A2.2 2.2 0 0 0 38.34 8.00 L40.97 8.00 A2.2 2.2 0 0 0 45.37 8.00 L48.01 8.00 A2.2 2.2 0 0 0 52.41 8.00 L55.04 8.00 A2.2 2.2 0 0 0 59.44 8.00 L62.08 8.00 A2.2 2.2 0 0 0 66.48 8.00 L69.11 8.00 A2.2 2.2 0 0 0 73.51 8.00 L76.14 8.00 A2.2 2.2 0 0 0 80.54 8.00 L83.18 8.00 A2.2 2.2 0 0 0 87.58 8.00 L90.21 8.00 A2.2 2.2 0 0 0 94.61 8.00 L97.25 8.00 A2.2 2.2 0 0 0 101.65 8.00 L104.28 8.00 A2.2 2.2 0 0 0 108.68 8.00 L111.32 8.00 A2.2 2.2 0 0 0 115.72 8.00 L118.35 8.00 A2.2 2.2 0 0 0 122.75 8.00 L125.39 8.00 A2.2 2.2 0 0 0 129.79 8.00 L132.42 8.00 A2.2 2.2 0 0 0 136.82 8.00 L139.46 8.00 A2.2 2.2 0 0 0 143.86 8.00 L146.49 8.00 A2.2 2.2 0 0 0 150.89 8.00 L153.52 8.00 A2.2 2.2 0 0 0 157.92 8.00 L160.56 8.00 A2.2 2.2 0 0 0 164.96 8.00 L167.59 8.00 A2.2 2.2 0 0 0 171.99 8.00 L174.63 8.00 A2.2 2.2 0 0 0 179.03 8.00 L181.66 8.00 A2.2 2.2 0 0 0 186.06 8.00 L188.70 8.00 A2.2 2.2 0 0 0 193.10 8.00 L195.73 8.00 A2.2 2.2 0 0 0 200.13 8.00 L202.77 8.00 A2.2 2.2 0 0 0 207.17 8.00 L209.80 8.00 A2.2 2.2 0 0 0 212.00 10.20 L212.00 12.80 A2.2 2.2 0 0 0 212.00 17.20 L212.00 19.80 A2.2 2.2 0 0 0 212.00 24.20 L212.00 26.80 A2.2 2.2 0 0 0 212.00 31.20 L212.00 33.80 A2.2 2.2 0 0 0 212.00 38.20 L212.00 40.80 A2.2 2.2 0 0 0 212.00 45.20 L212.00 47.80 A2.2 2.2 0 0 0 212.00 52.20 L212.00 54.80 A2.2 2.2 0 0 0 212.00 59.20 L212.00 61.80 A2.2 2.2 0 0 0 212.00 66.20 L212.00 68.80 A2.2 2.2 0 0 0 212.00 73.20 L212.00 75.80 A2.2 2.2 0 0 0 212.00 80.20 L212.00 82.80 A2.2 2.2 0 0 0 212.00 87.20 L212.00 89.80 A2.2 2.2 0 0 0 209.80 92.00 L207.17 92.00 A2.2 2.2 0 0 0 202.77 92.00 L200.13 92.00 A2.2 2.2 0 0 0 195.73 92.00 L193.10 92.00 A2.2 2.2 0 0 0 188.70 92.00 L186.06 92.00 A2.2 2.2 0 0 0 181.66 92.00 L179.03 92.00 A2.2 2.2 0 0 0 174.63 92.00 L171.99 92.00 A2.2 2.2 0 0 0 167.59 92.00 L164.96 92.00 A2.2 2.2 0 0 0 160.56 92.00 L157.92 92.00 A2.2 2.2 0 0 0 153.52 92.00 L150.89 92.00 A2.2 2.2 0 0 0 146.49 92.00 L143.86 92.00 A2.2 2.2 0 0 0 139.46 92.00 L136.82 92.00 A2.2 2.2 0 0 0 132.42 92.00 L129.79 92.00 A2.2 2.2 0 0 0 125.39 92.00 L122.75 92.00 A2.2 2.2 0 0 0 118.35 92.00 L115.72 92.00 A2.2 2.2 0 0 0 111.32 92.00 L108.68 92.00 A2.2 2.2 0 0 0 104.28 92.00 L101.65 92.00 A2.2 2.2 0 0 0 97.25 92.00 L94.61 92.00 A2.2 2.2 0 0 0 90.21 92.00 L87.58 92.00 A2.2 2.2 0 0 0 83.18 92.00 L80.54 92.00 A2.2 2.2 0 0 0 76.14 92.00 L73.51 92.00 A2.2 2.2 0 0 0 69.11 92.00 L66.48 92.00 A2.2 2.2 0 0 0 62.08 92.00 L59.44 92.00 A2.2 2.2 0 0 0 55.04 92.00 L52.41 92.00 A2.2 2.2 0 0 0 48.01 92.00 L45.37 92.00 A2.2 2.2 0 0 0 40.97 92.00 L38.34 92.00 A2.2 2.2 0 0 0 33.94 92.00 L31.30 92.00 A2.2 2.2 0 0 0 26.90 92.00 L24.27 92.00 A2.2 2.2 0 0 0 19.87 92.00 L17.23 92.00 A2.2 2.2 0 0 0 12.83 92.00 L10.20 92.00 A2.2 2.2 0 0 0 8.00 89.80 L8.00 87.20 A2.2 2.2 0 0 0 8.00 82.80 L8.00 80.20 A2.2 2.2 0 0 0 8.00 75.80 L8.00 73.20 A2.2 2.2 0 0 0 8.00 68.80 L8.00 66.20 A2.2 2.2 0 0 0 8.00 61.80 L8.00 59.20 A2.2 2.2 0 0 0 8.00 54.80 L8.00 52.20 A2.2 2.2 0 0 0 8.00 47.80 L8.00 45.20 A2.2 2.2 0 0 0 8.00 40.80 L8.00 38.20 A2.2 2.2 0 0 0 8.00 33.80 L8.00 31.20 A2.2 2.2 0 0 0 8.00 26.80 L8.00 24.20 A2.2 2.2 0 0 0 8.00 19.80 L8.00 17.20 A2.2 2.2 0 0 0 8.00 12.80 L8.00 10.20 A2.2 2.2 0 0 0 10.20 8.00 Z" fill="#F3EDE1" stroke="#12161C" stroke-width="0.7" stroke-linejoin="round" />
|
||||
<rect x="15" y="15" width="190" height="70" fill="#7A3B2E" />
|
||||
<path d="M61.06 66.41Q58.14 66.41 56.23 65.3Q54.33 64.2 53 62.46L53.96 61.72Q55.21 63.35 56.86 64.32Q58.51 65.29 61.1 65.29Q64.05 65.29 65.67 63.89Q67.28 62.5 67.28 59.88Q67.28 58.08 66.6 57.04Q65.92 56 64.73 55.46Q63.54 54.92 62.08 54.61L59.8 54.17Q57.46 53.73 56.12 52.83Q54.77 51.92 54.18 50.68Q53.58 49.44 53.58 47.98Q53.58 45.91 54.5 44.55Q55.42 43.19 57.08 42.52Q58.75 41.86 60.96 41.86Q63.61 41.86 65.38 42.78Q67.15 43.7 68.17 45.23L67.22 45.94Q66.2 44.48 64.67 43.73Q63.14 42.98 60.89 42.98Q58.07 42.98 56.46 44.21Q54.84 45.43 54.84 47.95Q54.84 49.61 55.54 50.61Q56.23 51.62 57.42 52.16Q58.61 52.71 60.08 53.01L62.35 53.45Q64.77 53.9 66.11 54.83Q67.45 55.77 67.98 57.04Q68.51 58.32 68.51 59.78Q68.51 61.95 67.61 63.42Q66.71 64.88 65.02 65.64Q63.34 66.41 61.06 66.41Z M78.56 66.41Q76.28 66.41 74.64 65.35Q72.99 64.3 72.09 62.29Q71.18 60.29 71.18 57.36Q71.18 54.44 72.07 52.42Q72.95 50.39 74.57 49.36Q76.18 48.32 78.46 48.32Q80.57 48.32 82.17 49.37Q83.76 50.43 84.67 52.35Q85.57 54.27 85.57 56.89V57.43H72.44V58.35Q72.44 61.51 74.06 63.43Q75.67 65.35 78.6 65.35Q80.64 65.35 82.08 64.35Q83.53 63.35 84.27 61.41L85.19 61.92Q84.44 63.96 82.74 65.18Q81.04 66.41 78.56 66.41ZM78.46 49.34Q76.56 49.34 75.21 50.21Q73.87 51.07 73.16 52.66Q72.44 54.24 72.44 56.34V56.48H84.27V56.28Q84.27 54.17 83.54 52.62Q82.81 51.07 81.5 50.21Q80.19 49.34 78.46 49.34Z M93.72 66H91.17Q90.42 66 90.06 65.63Q89.7 65.25 89.7 64.57V40.84H90.89V64.95H93.72Z M97.92 66V49.78H94.83V48.73H97.92V42.57Q97.92 41.76 98.33 41.3Q98.74 40.84 99.59 40.84H102.99V41.89H99.11V48.73H102.99V49.78H99.11V66Z M109.88 66H105.39V42.27H116.07Q118.28 42.27 119.84 43.19Q121.41 44.1 122.26 45.75Q123.11 47.4 123.11 49.61Q123.11 51.82 122.26 53.47Q121.41 55.12 119.84 56.04Q118.28 56.96 116.07 56.96H109.88ZM109.88 46.18V53.08H115.66Q116.55 53.08 117.16 52.76Q117.77 52.43 118.11 51.84Q118.45 51.24 118.45 50.39V48.83Q118.45 47.95 118.11 47.37Q117.77 46.79 117.16 46.48Q116.55 46.18 115.66 46.18Z M132.75 66.41Q130.24 66.41 128.38 65.27Q126.53 64.13 125.53 62.04Q124.53 59.95 124.53 57.09Q124.53 54.24 125.53 52.18Q126.53 50.12 128.38 48.98Q130.24 47.84 132.75 47.84Q135.3 47.84 137.16 48.98Q139.01 50.12 140.01 52.18Q141.02 54.24 141.02 57.09Q141.02 59.95 140.01 62.04Q139.01 64.13 137.16 65.27Q135.3 66.41 132.75 66.41ZM132.75 62.91Q134.49 62.91 135.47 61.83Q136.46 60.76 136.46 58.76V55.46Q136.46 53.45 135.47 52.4Q134.49 51.35 132.75 51.35Q131.09 51.35 130.08 52.4Q129.08 53.45 129.08 55.46V58.76Q129.08 60.76 130.08 61.83Q131.09 62.91 132.75 62.91Z M149.47 66.41Q146.92 66.41 145.19 65.54Q143.45 64.67 142.13 63.14L144.78 60.56Q145.77 61.68 146.94 62.33Q148.11 62.97 149.64 62.97Q151.21 62.97 151.9 62.43Q152.6 61.89 152.6 60.93Q152.6 60.15 152.11 59.69Q151.61 59.23 150.42 59.06L148.66 58.83Q146.75 58.59 145.43 57.94Q144.1 57.3 143.42 56.17Q142.74 55.05 142.74 53.45Q142.74 50.87 144.59 49.36Q146.45 47.84 149.47 47.84Q151.21 47.84 152.45 48.17Q153.69 48.49 154.64 49.12Q155.59 49.75 156.41 50.63L153.86 53.18Q153.08 52.37 151.99 51.82Q150.9 51.28 149.61 51.28Q148.25 51.28 147.58 51.77Q146.92 52.26 146.92 53.11Q146.92 54 147.45 54.46Q147.98 54.92 149.23 55.12L151.04 55.36Q153.96 55.77 155.37 57.08Q156.78 58.38 156.78 60.59Q156.78 62.29 155.88 63.62Q154.98 64.95 153.33 65.68Q151.68 66.41 149.47 66.41Z M168.09 66H164.97Q162.72 66 161.55 64.83Q160.38 63.65 160.38 61.48V51.69H157.76V48.25H159.12Q160.1 48.25 160.46 47.79Q160.82 47.33 160.82 46.42V43.39H164.73V48.25H168.37V51.69H164.73V62.53H168.09Z" fill="#F6EDE2" />
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 7.1 KiB |
@@ -4,35 +4,7 @@
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
// --- Copy buttons on .code values ------------------------------------
|
||||
// Values that get carried into another interface (a DNS panel, a mail
|
||||
// client) sit in a .code-row wrapper next to a Copy button. The text is read
|
||||
// from the .code element itself, so it can never drift from what is shown.
|
||||
// navigator.clipboard needs a secure context (HTTPS or localhost); over plain
|
||||
// HTTP in development it is simply absent, in which case the value stays
|
||||
// selectable by hand.
|
||||
document.addEventListener("click", function (ev) {
|
||||
var button = ev.target.closest("button.copy");
|
||||
if (!button) {
|
||||
return;
|
||||
}
|
||||
var row = button.closest(".code-row");
|
||||
var code = row && row.querySelector(".code");
|
||||
if (!code || !navigator.clipboard) {
|
||||
return;
|
||||
}
|
||||
navigator.clipboard.writeText(code.textContent).then(function () {
|
||||
var original = button.textContent;
|
||||
button.textContent = "Copied";
|
||||
setTimeout(function () {
|
||||
button.textContent = original;
|
||||
}, 1500);
|
||||
}, function () {
|
||||
/* Clipboard refused (permissions, insecure context): leave the page be. */
|
||||
});
|
||||
});
|
||||
|
||||
// --- Copy buttons of the component kit ------------------------------
|
||||
// --- Copy buttons ---------------------------------------------------
|
||||
// copy_field joins a read-only input or textarea to a Copy button marked
|
||||
// data-copy; the two sit together in one .has-addons field, and the value is
|
||||
// read from the field itself, so it can never drift from what is shown. Where
|
||||
@@ -143,17 +115,19 @@
|
||||
if (!control.matches(rule.match)) {
|
||||
continue;
|
||||
}
|
||||
var target = form.querySelector(rule.target);
|
||||
if (!target) {
|
||||
continue;
|
||||
}
|
||||
// A block can be several elements (the user form's two lists of domains).
|
||||
var show = rule.visible(control);
|
||||
target.hidden = !show;
|
||||
if (!show && rule.clearWhenHidden) {
|
||||
target.querySelectorAll("input").forEach(function (input) {
|
||||
input.value = "";
|
||||
});
|
||||
}
|
||||
form.querySelectorAll(rule.target).forEach(function (target) {
|
||||
// hidden alone does not hide an element whose own stylesheet gives it a
|
||||
// display (Bulma's .box), so Bulma's is-hidden helper goes with it.
|
||||
target.hidden = !show;
|
||||
target.classList.toggle("is-hidden", !show);
|
||||
if (!show && rule.clearWhenHidden) {
|
||||
target.querySelectorAll("input").forEach(function (input) {
|
||||
input.value = "";
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user