The owner, 2026-10-16: "Вариант 1. Но меню выглядит длинной простынёй. Там
бы сделать сворачивание разделов." As the mockup of e0e30e2: the bar's
navbar-menu loses is-active, so Bulma hides it below 1024 px, and the new
touchnav partial (layout.html) takes its place in the brand - Menu, a
<details> whose panel holds Overview, each group as a fold (the current
one open, its page aria-current), and the user's group last with Account,
Help and Sign out (the same POST form as the bar's). The same Show* flags
as the bar decide what is there. panel.css takes the mockup's rules (the
touch navigation, a 1rem gutter for the page and the sibling strip below
1024 px). No script.
The cause, measured, is written in the roadmap row: every width under
1024 px had the always-open menu; "desktop site" (~980 px) added Bulma's
side-by-side columns from 769 px; Bulma's container has no side margin
below 1024 px.
Tests: the Menu marks the current group and page and the bar's menu has
no is-active (TestMenuHighlightsTheCurrentGroupEntryAndSibling), it offers
nothing the bar does not to a domain administrator (TestTouchMenuFollowsTheBar),
the shell's Sign out is counted twice on the password page;
TestPanelCSSIsTheMockupStylesheet. go vet + go test (26 packages) and make
e2e on the test server for this tree. CHANGELOG [Unreleased]; roadmap row
done.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
173 lines
15 KiB
HTML
173 lines
15 KiB
HTML
{{/* 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}} · 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 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. HTML links.html#rel-icon leaves "most appropriate" to the
|
|
browser, so measured, 2026-10-15, by which file was requested: Edge 155
|
|
(Chromium) takes the SVG, the Claude desktop app's embedded browser
|
|
(Chromium 152) the PNG, and adding sizes="any" to the SVG changes
|
|
neither. Firefox and Safari not measured (assumptions D-B16). */}}
|
|
<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
|
|
hx-indicator, and that injected element would be the one thing the
|
|
Content-Security-Policy has to make an exception for, so it
|
|
is switched off here. htmx 2.0.4, src/htmx.js: the ready handler runs
|
|
mergeMetaConfig() before insertIndicatorStyles(), which adds the element
|
|
only while includeIndicatorStyles is not false. Both scripts are defer:
|
|
they run in this order once parsing ends and before DOMContentLoaded
|
|
(HTML Living Standard, parsing.html#the-end). */}}
|
|
<meta name="htmx-config" content='{"includeIndicatorStyles":false}'>
|
|
<script src="/static/htmx.min.js" defer></script>
|
|
<script src="/static/panel.js" defer></script>
|
|
</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>
|
|
{{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>
|
|
</body>
|
|
</html>{{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="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. The bar's menu shows
|
|
from 1024 px (Bulma hides a navbar-menu without is-active below it);
|
|
below, touchnav takes its place. */}}
|
|
{{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>{{template "touchnav" .}}</div>
|
|
<div class="navbar-menu">
|
|
<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>
|
|
<a class="navbar-item{{if and (eq .Section "inbound") (eq .Page "log")}} is-active{{end}}" href="/inbound/log"><i class="ti ti-list-details"></i>Log</a>
|
|
{{if .ShowFilterLists}} <a class="navbar-item{{if and (eq .Section "inbound") (eq .Page "filter-lists")}} is-active{{end}}" href="/inbound/filter-lists"><i class="ti ti-filter"></i>Filter lists</a>
|
|
{{end}} </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, and panel.css hands it the hover tint Bulma gives to
|
|
a.navbar-item alone. */}}
|
|
<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>
|
|
<div class="sp-perf" aria-hidden="true"></div>{{end}}
|
|
|
|
{{/* touchnav is the navigation below 1024 px (input: Shell; mockup
|
|
build.py, touchnav()): Menu opens a panel over the page with the same
|
|
groups as the bar, each a fold - the current one open - and the user's
|
|
last. <details> all through, so it works without a script. */}}
|
|
{{define "touchnav"}}<details class="sp-touchnav"><summary>Menu</summary><div class="sp-touchnav-panel">
|
|
{{if .ShowOverview}}<a href="/overview"{{if eq .Section "overview"}} aria-current="page"{{end}}>Overview</a>
|
|
{{end}}{{if .ShowOutbound}}<details class="sp-touchnav-group"{{if eq .Section "outbound"}} open{{end}}><summary>Outbound</summary><a href="/outbound/domains"{{if and (eq .Section "outbound") (eq .Page "domains")}} aria-current="page"{{end}}><i class="ti ti-world-upload"></i>Domains</a><a href="/outbound/log"{{if and (eq .Section "outbound") (eq .Page "log")}} aria-current="page"{{end}}><i class="ti ti-list-details"></i>Log</a>{{if .ShowQueue}}<a href="/outbound/queue"{{if and (eq .Section "outbound") (eq .Page "queue")}} aria-current="page"{{end}}><i class="ti ti-stack-2"></i>Queue</a>{{end}}{{if .ShowDMARC}}<a href="/outbound/dmarc"{{if and (eq .Section "outbound") (eq .Page "dmarc")}} aria-current="page"{{end}}><i class="ti ti-report-analytics"></i>DMARC reports</a>{{end}}</details>
|
|
{{end}}{{if .ShowInbound}}<details class="sp-touchnav-group"{{if eq .Section "inbound"}} open{{end}}><summary>Inbound</summary><a href="/inbound/domains"{{if and (eq .Section "inbound") (eq .Page "domains")}} aria-current="page"{{end}}><i class="ti ti-world-download"></i>Domains</a><a href="/inbound/log"{{if and (eq .Section "inbound") (eq .Page "log")}} aria-current="page"{{end}}><i class="ti ti-list-details"></i>Log</a>{{if .ShowFilterLists}}<a href="/inbound/filter-lists"{{if and (eq .Section "inbound") (eq .Page "filter-lists")}} aria-current="page"{{end}}><i class="ti ti-filter"></i>Filter lists</a>{{end}}</details>
|
|
{{end}}{{if .ShowServer}}<details class="sp-touchnav-group"{{if eq .Section "server"}} open{{end}}><summary>Server</summary><a href="/server/health"{{if and (eq .Section "server") (eq .Page "health")}} aria-current="page"{{end}}><i class="ti ti-heartbeat"></i>Health</a><a href="/server/log"{{if and (eq .Section "server") (eq .Page "log")}} aria-current="page"{{end}}><i class="ti ti-file-text"></i>System log</a><a href="/server/backup"{{if and (eq .Section "server") (eq .Page "backup")}} aria-current="page"{{end}}><i class="ti ti-archive"></i>Backup</a><a href="/server/users"{{if and (eq .Section "server") (eq .Page "users")}} aria-current="page"{{end}}><i class="ti ti-users"></i>Users</a><a href="/server/settings"{{if and (eq .Section "server") (eq .Page "settings")}} aria-current="page"{{end}}><i class="ti ti-settings"></i>Settings</a></details>
|
|
{{end}}<details class="sp-touchnav-group"{{if eq .Section "user"}} open{{end}}><summary><i class="ti ti-user-circle"></i>{{.User}}</summary><a href="/account"><i class="ti ti-key"></i>Account</a><a href="/help"><i class="ti ti-help"></i>Help</a><form method="post" action="/logout"><button type="submit"><i class="ti ti-logout"></i>Sign out</button></form></details>
|
|
</div></details>{{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><a href="/inbound/log"{{if eq .Page "log"}} aria-current="page"{{end}}><i class="ti ti-list-details"></i>Log</a>{{if .ShowFilterLists}}<a href="/inbound/filter-lists"{{if eq .Page "filter-lists"}} aria-current="page"{{end}}><i class="ti ti-filter"></i>Filter lists</a>{{end}}</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}}
|