panel-redesign: the component kit - panel.css, partials, shell, kit page
Stage 0, steps 3-5, in one commit: the three were built together (the kit
page is what exercises the partials and the shell) and do not split into
states that build on their own. Written by Sonnet, reviewed by Opus.
- static/panel.css: shared/brand.css + panel/theme.css, rule for rule.
wordmark.svg and perf-edge.svg beside it; the stamp is logo.svg.
- templates/components.html: every partial of the plan's Three layers,
with its typed input in components.go. Wrappers are open/close pairs
(box_open/box_close, body_open/body_close, record_open/record_close).
Rich text is a Text value built from escaped runs - no field is
trusted HTML. status_tag and wbr_at are template functions; copy_field
is a partial whose button panel.js wires through data-copy.
- templates/layout.html: the shell as build.py draws it, the menu paths
written out literally, visibility from a typed Shell (role, feature
flags, the user's reach); the signed-out screen as a second define.
Not in the 2.0 menu: Inbound Log / Queue / Quarantine / Filter lists
and Preflight.
- GET /server/components (global role, 404 otherwise): the mockup
components page rebuilt from fixtures (kit.go) by the real templates.
- The old panel steps aside unchanged: panel.css -> legacy.css,
layout.html -> layout_legacy.html; a page uses the new layout iff it
is off legacy_pages.txt.
- legacy_pages.txt: @kit removed - the kit is under the guards for good.
Differences from the mockup components page, for the owner:
- the head button is Help, not "Cover sheet"; two "Example" links are
dropped (no fixed URL), two point at /outbound/domains and /login;
- the page sits in the Server group, so the sibling strip shows;
- Sign out is a POST form whose button carries navbar-item: same size
and type as the links, but without Bulma's hover tint;
- a head's actions are always wrapped in div.buttons.
Evidence
- docs/assets/panel-redesign/evidence/components.png: 1440 px wide, full
height, headless Edge. Taken from the real templates served locally,
NOT from the container (no Docker on this machine); the page reads
only its own fixtures and embedded assets.
- panel/audit.js on the kit page, 1440 and 1280:
{"pages":1,"widths":[1440,1280],"textsChecked":516,"problems":[]}
- go test ./internal/web/... (with @kit removed)
ok github.com/mixeme/selfpost/internal/web
ok github.com/mixeme/selfpost/internal/web/auth
ok github.com/mixeme/selfpost/internal/web/handlers
ok github.com/mixeme/selfpost/internal/web/validate
ok github.com/mixeme/selfpost/internal/web/view
TestRoutesFollowNavigation stays skipped behind @routes (stage 1).
Roadmap 10/27.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,16 +1,32 @@
|
||||
{{define "layout.html"}}<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
{{/* The shell of every redesigned page (docs/plans/panel-redesign.md § Three
|
||||
layers): a brick navbar with the wordmark and hover menus, the stamp's
|
||||
perforated edge, the strip of sibling pages, the page, the footer. It is
|
||||
built exactly like the mockups' build.py builds it, and it is the only
|
||||
place besides components.html where sp- classes are written. A page never
|
||||
writes any of it.
|
||||
|
||||
layout.html is executed with a Frame (components.go): .Shell is what the
|
||||
shell shows, .Page is the page's own data, handed to the page's "content"
|
||||
template. Which entries are visible comes from .Shell (Engine.shell derives
|
||||
it from the role, the feature flags and the user's reach); the paths are
|
||||
written out here, so the menu can be read, and checked against the router
|
||||
(guard_routes_test.go), without running anything. */}}
|
||||
|
||||
{{/* layout_head is the head of both documents; the stylesheets, in this order,
|
||||
and the two scripts are all a page ever loads. */}}
|
||||
{{define "layout_head"}}<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{{.Title}}</title>
|
||||
<title>{{.Title}} · SelfPost</title>
|
||||
{{/* The tab icon is the stamp's small-size variant — the initials rather than
|
||||
the two-line wordmark, which stops resolving below 32px (the thresholds are
|
||||
recorded in docs/assets/selfpost-proof.html). The PNG is declared first and
|
||||
the SVG second: a browser takes the last icon whose type it understands, so
|
||||
vector wins wherever it is supported and the raster catches the rest. */}}
|
||||
the two-line wordmark, which stops resolving below 32px. The PNG is declared
|
||||
first and the SVG second: a browser takes the last icon whose type it
|
||||
understands, so vector wins wherever it is supported and the raster catches
|
||||
the rest. */}}
|
||||
<link rel="icon" href="/static/favicon.png" sizes="32x32">
|
||||
<link rel="icon" href="/static/favicon.svg" type="image/svg+xml">
|
||||
<link rel="stylesheet" href="/static/bulma.min.css">
|
||||
<link rel="stylesheet" href="/static/tabler-icons.css">
|
||||
<link rel="stylesheet" href="/static/panel.css">
|
||||
{{/* Unless told otherwise, htmx injects a stylesheet element of its own into
|
||||
the head for the request-indicator classes. The panel uses no
|
||||
@@ -20,126 +36,113 @@
|
||||
<meta name="htmx-config" content='{"includeIndicatorStyles":false}'>
|
||||
<script src="/static/htmx.min.js" defer></script>
|
||||
<script src="/static/panel.js" defer></script>
|
||||
</head>
|
||||
</head>{{end}}
|
||||
|
||||
{{/* layout.html is the signed-in document. Engine.Render uses
|
||||
layout_signed_out instead when there is no user. */}}
|
||||
{{define "layout.html"}}<!DOCTYPE html>
|
||||
<html lang="en" data-theme="light">
|
||||
{{template "layout_head" .Shell}}
|
||||
<body>
|
||||
{{if .User}}{{template "help_inputs" .}}{{end}}
|
||||
{{/* Two columns: the navigation on the left and the page beside it, centred
|
||||
together (see .shell in panel.css). The signed-out pages have no navigation
|
||||
and the shell holds their card alone. */}}
|
||||
<div class="shell">
|
||||
{{if .User}}{{template "nav" .}}{{end}}
|
||||
{{/* The page name travels onto <main> as a class so the stylesheet can size a
|
||||
page to what it holds — the signed-out pages are narrower than the rest
|
||||
(see main.page-* in panel.css). It is .Active, already carried for the
|
||||
navigation, rather than a second field every handler would have to
|
||||
remember to set. The "wide" block beside it is how a page made of data
|
||||
asks for the whole column instead of the reading measure; it is the page's
|
||||
own declaration rather than a name in the stylesheet, since .Active cannot
|
||||
tell the send log from a single delivery's page. */}}
|
||||
<main class="page-{{.Active}} {{template "wide" .}}">
|
||||
{{template "content" .}}
|
||||
{{/* Appropriate Legal Notices (AGPL-3.0): copyright, how to read the licence,
|
||||
where the Corresponding Source is, and that there is no warranty. Shown
|
||||
on every page, including login/setup — those are interactive UIs too.
|
||||
The running version stays signed-in only: it is what a backup manifest is
|
||||
checked against on restore, and must not be advertised to the internet. */}}
|
||||
<footer class="version">
|
||||
{{if .User}}SelfPost {{.Version}} · {{end}}{{.Copyright}} ·
|
||||
<a href="/license">License (AGPL-3.0)</a> ·
|
||||
<a href="{{.SourceURL}}">Source</a> ·
|
||||
No warranty
|
||||
</footer>
|
||||
{{template "nav" .Shell}}
|
||||
{{template "subnav" .Shell}}
|
||||
<main>
|
||||
<div class="container sp-page">
|
||||
{{template "content" .Page}}
|
||||
{{/* Appropriate Legal Notices (AGPL-3.0): copyright, how to read the
|
||||
licence, where the Corresponding Source is, and that there is no
|
||||
warranty. The running version is shown signed in only: it is what a
|
||||
backup manifest is checked against on restore, and must not be
|
||||
advertised to the internet. */}}
|
||||
<footer class="sp-foot">SelfPost {{.Shell.Version}} · {{.Shell.Copyright}} · <a href="/license">License (AGPL-3.0)</a> · <a href="{{.Shell.SourceURL}}">Source</a> · No warranty</footer>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
{{if .User}}{{template "help_drawer" .}}{{end}}
|
||||
</body>
|
||||
</html>{{end}}
|
||||
|
||||
{{/* nav is the panel's navigation bar. It is rendered here, from the layout, so
|
||||
every authenticated page has it without the page's own template having to
|
||||
remember to include it; .Active names the current page so it is highlighted
|
||||
instead of linking to itself. Unauthenticated pages (login, setup) carry no
|
||||
.User and get no nav. */}}
|
||||
{{define "nav"}}
|
||||
<nav class="nav">
|
||||
{{/* The order of the blocks is the order they are read in, top to bottom of
|
||||
the column: the mark, the panel's pages, and the session last. The mark
|
||||
goes to /status, the page the panel opens on — named outright rather
|
||||
than as "/", which is only a redirect to it. */}}
|
||||
<a class="brand" href="{{if .IsGlobal}}/status{{else}}/domains{{end}}"><img src="/static/logo-compact.svg" width="220" height="100" alt="SelfPost"></a>
|
||||
<div class="links">
|
||||
{{if .IsGlobal}}
|
||||
{{if eq .Active "status"}}<span aria-current="page">{{template "icon-status"}}Status</span>{{else}}<a href="/status">{{template "icon-status"}}Status</a>{{end}}
|
||||
{{end}}
|
||||
{{if eq .Active "domains"}}<span aria-current="page">{{template "icon-domains"}}Domains</span>{{else}}<a href="/domains">{{template "icon-domains"}}Domains</a>{{end}}
|
||||
{{if and .IsGlobal .InboundEnabled}}
|
||||
{{if eq .Active "inbound"}}<span aria-current="page">{{template "icon-inbound"}}Inbound</span>{{else}}<a href="/inbound">{{template "icon-inbound"}}Inbound</a>{{end}}
|
||||
{{end}}
|
||||
{{if .DMARCEnabled}}
|
||||
{{if eq .Active "dmarc"}}<span aria-current="page">{{template "icon-dmarc"}}DMARC</span>{{else}}<a href="/dmarc">{{template "icon-dmarc"}}DMARC</a>{{end}}
|
||||
{{end}}
|
||||
{{if eq .Active "help"}}<span aria-current="page">{{template "icon-help"}}Help</span>{{else}}<a href="/help">{{template "icon-help"}}Help</a>{{end}}
|
||||
{{if eq .Active "deliveries"}}<span aria-current="page">{{template "icon-deliveries"}}Deliveries</span>{{else}}<a href="/deliveries">{{template "icon-deliveries"}}Deliveries</a>{{end}}
|
||||
{{if .IsGlobal}}
|
||||
{{if eq .Active "mail_queue"}}<span aria-current="page">{{template "icon-mail-queue"}}Mail queue</span>{{else}}<a href="/mail-queue">{{template "icon-mail-queue"}}Mail queue</a>{{end}}
|
||||
{{if eq .Active "system_log"}}<span aria-current="page">{{template "icon-system-log"}}System log</span>{{else}}<a href="/system-log">{{template "icon-system-log"}}System log</a>{{end}}
|
||||
{{if eq .Active "backup"}}<span aria-current="page">{{template "icon-backup"}}Backup</span>{{else}}<a href="/backup">{{template "icon-backup"}}Backup</a>{{end}}
|
||||
{{if eq .Active "users"}}<span aria-current="page">{{template "icon-users"}}Users</span>{{else}}<a href="/users">{{template "icon-users"}}Users</a>{{end}}
|
||||
{{end}}
|
||||
{{/* layout_signed_out is the split brand / form screen of sign-in and setup:
|
||||
the stamp on the brick, the page's own form beside it, and the legal
|
||||
notice without the version. */}}
|
||||
{{define "layout_signed_out"}}<!DOCTYPE html>
|
||||
<html lang="en" data-theme="light">
|
||||
{{template "layout_head" .Shell}}
|
||||
<body>
|
||||
<div class="columns is-gapless sp-signin">
|
||||
<div class="column is-half sp-signin-brand">
|
||||
<div>
|
||||
<img src="/static/logo.svg" alt="SelfPost">
|
||||
<p>YOUR SMTP. YOUR RULES.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="session">
|
||||
<span class="session-user muted">{{template "icon-account"}}User: {{.User}}</span>
|
||||
{{if eq .Active "settings"}}<span aria-current="page">{{template "icon-settings"}}Settings</span>{{else}}<a href="/settings">{{template "icon-settings"}}Settings</a>{{end}}
|
||||
<form class="inline" method="post" action="/logout">
|
||||
<button type="submit" class="danger">{{template "icon-sign-out"}}Sign out</button>
|
||||
</form>
|
||||
<div class="column is-half sp-signin-form">
|
||||
{{template "content" .Page}}
|
||||
<p class="sp-foot">{{.Shell.Copyright}} · <a href="/license">License (AGPL-3.0)</a> · <a href="{{.Shell.SourceURL}}">Source</a> · No warranty</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>{{end}}
|
||||
|
||||
{{/* nav is the navbar and the perforated edge under it (input: Shell). A group
|
||||
with nothing in it for this user is absent; the group of the current page
|
||||
carries sp-current and its current entry is-active. */}}
|
||||
{{define "nav"}}<nav class="navbar is-primary" aria-label="main navigation">
|
||||
<div class="container">
|
||||
<div class="navbar-brand"><a class="navbar-item" href="{{.Home}}"><img src="/static/wordmark.svg" alt="SelfPost"></a></div>
|
||||
<div class="navbar-menu is-active">
|
||||
<div class="navbar-start">
|
||||
{{if .ShowOverview}} <a class="navbar-item{{if eq .Section "overview"}} sp-current{{end}}" href="/overview">Overview</a>
|
||||
{{end}}{{if .ShowOutbound}} <div class="navbar-item has-dropdown is-hoverable{{if eq .Section "outbound"}} sp-current{{end}}">
|
||||
<a class="navbar-link" href="/outbound/domains">Outbound</a>
|
||||
<div class="navbar-dropdown">
|
||||
<a class="navbar-item{{if and (eq .Section "outbound") (eq .Page "domains")}} is-active{{end}}" href="/outbound/domains"><i class="ti ti-world-upload"></i>Domains</a>
|
||||
<a class="navbar-item{{if and (eq .Section "outbound") (eq .Page "log")}} is-active{{end}}" href="/outbound/log"><i class="ti ti-list-details"></i>Log</a>
|
||||
{{if .ShowQueue}} <a class="navbar-item{{if and (eq .Section "outbound") (eq .Page "queue")}} is-active{{end}}" href="/outbound/queue"><i class="ti ti-stack-2"></i>Queue</a>
|
||||
{{end}}{{if .ShowDMARC}} <a class="navbar-item{{if and (eq .Section "outbound") (eq .Page "dmarc")}} is-active{{end}}" href="/outbound/dmarc"><i class="ti ti-report-analytics"></i>DMARC reports</a>
|
||||
{{end}} </div>
|
||||
</div>
|
||||
{{end}}{{if .ShowInbound}} <div class="navbar-item has-dropdown is-hoverable{{if eq .Section "inbound"}} sp-current{{end}}">
|
||||
<a class="navbar-link" href="/inbound/domains">Inbound</a>
|
||||
<div class="navbar-dropdown">
|
||||
<a class="navbar-item{{if and (eq .Section "inbound") (eq .Page "domains")}} is-active{{end}}" href="/inbound/domains"><i class="ti ti-world-download"></i>Domains</a>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}{{if .ShowServer}} <div class="navbar-item has-dropdown is-hoverable{{if eq .Section "server"}} sp-current{{end}}">
|
||||
<a class="navbar-link" href="/server/health">Server</a>
|
||||
<div class="navbar-dropdown">
|
||||
<a class="navbar-item{{if and (eq .Section "server") (eq .Page "health")}} is-active{{end}}" href="/server/health"><i class="ti ti-heartbeat"></i>Health</a>
|
||||
<a class="navbar-item{{if and (eq .Section "server") (eq .Page "log")}} is-active{{end}}" href="/server/log"><i class="ti ti-file-text"></i>System log</a>
|
||||
<a class="navbar-item{{if and (eq .Section "server") (eq .Page "backup")}} is-active{{end}}" href="/server/backup"><i class="ti ti-archive"></i>Backup</a>
|
||||
<a class="navbar-item{{if and (eq .Section "server") (eq .Page "users")}} is-active{{end}}" href="/server/users"><i class="ti ti-users"></i>Users</a>
|
||||
<a class="navbar-item{{if and (eq .Section "server") (eq .Page "settings")}} is-active{{end}}" href="/server/settings"><i class="ti ti-settings"></i>Settings</a>
|
||||
</div>
|
||||
</div>
|
||||
{{end}} </div>
|
||||
<div class="navbar-end">
|
||||
<div class="navbar-item has-dropdown is-hoverable{{if eq .Section "user"}} sp-current{{end}}">
|
||||
<a class="navbar-link" href="/account"><i class="ti ti-user-circle"></i>{{.User}}</a>
|
||||
<div class="navbar-dropdown is-right">
|
||||
<a class="navbar-item" href="/account"><i class="ti ti-key"></i>Account</a>
|
||||
<a class="navbar-item" href="/help"><i class="ti ti-help"></i>Help</a>
|
||||
<hr class="navbar-divider">
|
||||
{{/* Signing out is a POST (the handler answers nothing else), so it
|
||||
stays a form that works without scripts. The submit button
|
||||
carries navbar-item: Bulma resets a button to plain text, so
|
||||
it takes the dropdown's entry styling like the links around it
|
||||
(only Bulma's hover tint, which it gives to a.navbar-item
|
||||
alone, is missing). */}}
|
||||
<form method="post" action="/logout"><button type="submit" class="navbar-item"><i class="ti ti-logout"></i>Sign out</button></form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
{{end}}
|
||||
<div class="sp-perf" aria-hidden="true"></div>{{end}}
|
||||
|
||||
{{/* Whether the page takes the column whole. Empty here, so a page is held to
|
||||
the reading measure unless it says otherwise; the pages that are tables of
|
||||
data, raw log lines or side-by-side cards redefine this as the word "wide",
|
||||
which lands in <main>'s class list (see main.wide in panel.css). Each
|
||||
page's template file is parsed after this one, so its definition replaces
|
||||
the empty one. */}}
|
||||
{{define "wide"}}{{end}}
|
||||
|
||||
{{/* back_link — up-navigation on drill-down pages. Invoke with the "back"
|
||||
function, e.g. {{template "back_link" (back "/users" "Back to users")}}.
|
||||
Place it directly under the page <h1> and above the cards (see .back in
|
||||
panel.css). TestDrillDownPagesPlaceBackLinkAboveContent guards this. */}}
|
||||
{{define "back_link"}}
|
||||
<a class="back" href="{{.Href}}">← {{.Label}}</a>
|
||||
{{end}}
|
||||
|
||||
{{/* Navigation icons. Inline SVG rather than an icon font or sprite file: they
|
||||
inherit the link's colour through currentColor, cost no extra request, and
|
||||
need no exemption from the panel's "default-src 'self'" policy. Each is
|
||||
aria-hidden because the entry's own text is already the accessible name;
|
||||
the icon is a landmark for the eye, not a second label. Kept as separate
|
||||
templates so the nav above stays one readable line per page. */}}
|
||||
{{define "icon-status"}}<svg class="icon" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M1.25 8.5h2.9L6.2 3.4l3.1 9.4 1.9-4.3h3.55"/></svg>{{end}}
|
||||
{{define "icon-domains"}}<svg class="icon" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="8" cy="8" r="6.25"/><path d="M1.9 8h12.2"/><path d="M8 1.75c1.85 1.8 2.8 4 2.8 6.25S9.85 12.45 8 14.25C6.15 12.45 5.2 10.25 5.2 8S6.15 3.55 8 1.75Z"/></svg>{{end}}
|
||||
{{define "icon-inbound"}}<svg class="icon" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M2.5 9.5h11"/><path d="M8 2.75v6.2"/><path d="M5.4 6.4 8 9.05 10.6 6.4"/><path d="M3.2 12.6h9.6"/></svg>{{end}}
|
||||
{{define "icon-dmarc"}}<svg class="icon" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M2.5 4.5h11v7H2.5z"/><path d="M5 7.5h6M5 9.5h4"/></svg>{{end}}
|
||||
{{define "icon-help"}}<svg class="icon" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="8" cy="8" r="6.25"/><path d="M8 7.2V11.4"/><path d="M8 5.05v.01"/></svg>{{end}}
|
||||
{{define "icon-deliveries"}}<svg class="icon" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M14.25 1.75 1.6 6.6l5 2.05 2.05 5z"/><path d="M14.25 1.75 6.6 8.65"/></svg>{{end}}
|
||||
{{define "icon-mail-queue"}}<svg class="icon" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M1.75 9.5h3.3l1 1.75h3.9l1-1.75h3.3v3.05a1.2 1.2 0 0 1-1.2 1.2H2.95a1.2 1.2 0 0 1-1.2-1.2z"/><path d="M1.75 9.5 3.4 3.2a1.25 1.25 0 0 1 1.2-.95h6.8a1.25 1.25 0 0 1 1.2.95l1.65 6.3"/></svg>{{end}}
|
||||
{{define "icon-system-log"}}<svg class="icon" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3.75 1.75h5.1l3.4 3.4v8.05a1.05 1.05 0 0 1-1.05 1.05H3.75a1.05 1.05 0 0 1-1.05-1.05V2.8a1.05 1.05 0 0 1 1.05-1.05Z"/><path d="M8.85 1.75v3.4h3.4"/><path d="M5.35 8.6h5.3M5.35 11.1h3.5"/></svg>{{end}}
|
||||
{{/* A filing cabinet rather than a storage box: the box read as a box, while
|
||||
two drawers with handles is the picture of an archive. */}}
|
||||
{{define "icon-backup"}}<svg class="icon" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="2.75" y="1.75" width="10.5" height="12.5" rx="1.15"/><path d="M2.75 8h10.5"/><path d="M6.4 4.85h3.2M6.4 11.15h3.2"/></svg>{{end}}
|
||||
{{define "icon-account"}}<svg class="icon" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="8" cy="5.4" r="2.75"/><path d="M2.9 14.25a5.1 5.1 0 0 1 10.2 0"/></svg>{{end}}
|
||||
{{/* Two people: rear as a right-side crescent (head + shoulder) so it peeks
|
||||
from behind; front is a full silhouette matching icon-account. Heads share
|
||||
one baseline; shoulder arcs share one floor — otherwise the pair looks
|
||||
lopsided at 16 px. */}}
|
||||
{{define "icon-users"}}<svg class="icon" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M10.9 3.1a2.1 2.1 0 0 1 0 4.2"/><path d="M14.7 14.25a3.8 3.8 0 0 0-3.9-3.65"/><circle cx="5.5" cy="5.2" r="2.5"/><path d="M1.4 14.25a4.8 4.8 0 0 1 8.2 0"/></svg>{{end}}
|
||||
{{/* A gear (Feather "settings"), not a sun-with-rays: at 16 px spokes alone
|
||||
read as brightness, while the toothed rim is the picture of configuration.
|
||||
viewBox 0 0 24 matches the source path; CSS sizes the icon to 1 rem. */}}
|
||||
{{define "icon-settings"}}<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.25" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>{{end}}
|
||||
{{/* Sign out is the one icon here that marks an action rather than a page: an
|
||||
open door with the arrow leading out of it. */}}
|
||||
{{define "icon-sign-out"}}<svg class="icon" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6.1 14.25H3.65a1.15 1.15 0 0 1-1.15-1.15V2.9a1.15 1.15 0 0 1 1.15-1.15H6.1"/><path d="M10.6 11.15 13.75 8 10.6 4.85"/><path d="M13.75 8H6.35"/></svg>{{end}}
|
||||
{{/* subnav is the strip of sibling pages under the edge (input: Shell): the
|
||||
pages of the current group, the current one marked. A page outside every
|
||||
group (the user menu, the kit page's neighbours) has none. */}}
|
||||
{{define "subnav"}}{{if and .ShowOutbound (eq .Section "outbound")}}<div class="sp-subnav"><div class="container"><a href="/outbound/domains"{{if eq .Page "domains"}} aria-current="page"{{end}}><i class="ti ti-world-upload"></i>Domains</a><a href="/outbound/log"{{if eq .Page "log"}} aria-current="page"{{end}}><i class="ti ti-list-details"></i>Log</a>{{if .ShowQueue}}<a href="/outbound/queue"{{if eq .Page "queue"}} aria-current="page"{{end}}><i class="ti ti-stack-2"></i>Queue</a>{{end}}{{if .ShowDMARC}}<a href="/outbound/dmarc"{{if eq .Page "dmarc"}} aria-current="page"{{end}}><i class="ti ti-report-analytics"></i>DMARC reports</a>{{end}}</div></div>
|
||||
{{else if and .ShowInbound (eq .Section "inbound")}}<div class="sp-subnav"><div class="container"><a href="/inbound/domains"{{if eq .Page "domains"}} aria-current="page"{{end}}><i class="ti ti-world-download"></i>Domains</a></div></div>
|
||||
{{else if and .ShowServer (eq .Section "server")}}<div class="sp-subnav"><div class="container"><a href="/server/health"{{if eq .Page "health"}} aria-current="page"{{end}}><i class="ti ti-heartbeat"></i>Health</a><a href="/server/log"{{if eq .Page "log"}} aria-current="page"{{end}}><i class="ti ti-file-text"></i>System log</a><a href="/server/backup"{{if eq .Page "backup"}} aria-current="page"{{end}}><i class="ti ti-archive"></i>Backup</a><a href="/server/users"{{if eq .Page "users"}} aria-current="page"{{end}}><i class="ti ti-users"></i>Users</a><a href="/server/settings"{{if eq .Page "settings"}} aria-current="page"{{end}}><i class="ti ti-settings"></i>Settings</a></div></div>
|
||||
{{end}}{{end}}
|
||||
|
||||
Reference in New Issue
Block a user