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>
149 lines
10 KiB
HTML
149 lines
10 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. */}}
|
|
<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. */}}
|
|
<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. */}}
|
|
{{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>
|
|
<div class="sp-perf" aria-hidden="true"></div>{{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}}
|