Files
selfpost/docs/assets/panel-redesign/panel/build.py
T
mixandClaude Fable 5.1 2f7b5d95c3
test / test (push) Canceled after 0s
docs: draw the roadmap's screens in the panel design
Four screens for open roadmap items, all built from existing components:
Inbound > Filter lists (allow / deny, where the maps go) and an adjusted
Inbound > Log (client, decision, engine and reason, Allow / Deny sender)
for inbound-antispam-panel; Inbound > Quarantine, list and held message
with Release beside a danger-zone Discard, for inbound-quarantine; and
Server > Preflight, a verdict postmark over a table of checks with a
"what to do" column and a test e-mail form, for preflight.

A mockup is the target look, not a decision to build: each item keeps
its status, and quarantine's open questions stay open - the mockup only
assumes answers to them. The paths join the plan's Routes table, the
quarantine and preflight plans link their mockups, and the components
page maps the quarantine decision to a tag. 33 screens; lint clean.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-21 22:07:39 +03:00

122 lines
5.8 KiB
Python

"""Wrap every fragment in src/ with the panel shell and write it next to this file.
The mockups are static HTML, but thirty pages share one navbar; keeping that
navbar in one place is the only reason this script exists. Run it from
anywhere: python docs/assets/panel-redesign/panel/build.py
A fragment starts with one meta comment:
<!-- title: Outbound domains | section: outbound | page: domains -->
section/page pick the highlighted entries; "shell: bare" skips the navbar
(sign-in, setup).
"""
import pathlib
import re
HERE = pathlib.Path(__file__).parent
NAV = [
("overview", "Overview", "overview.html", "ti-activity-heartbeat", []),
("outbound", "Outbound", "out-domains.html", "ti-send", [
("domains", "Domains", "out-domains.html", "ti-world-upload"),
("log", "Log", "out-log.html", "ti-list-details"),
("queue", "Queue", "out-queue.html", "ti-stack-2"),
("dmarc", "DMARC reports", "dmarc.html", "ti-report-analytics"),
]),
("inbound", "Inbound", "in-domains.html", "ti-inbox", [
("domains", "Domains", "in-domains.html", "ti-world-download"),
("log", "Log", "in-log.html", "ti-list-details"),
("queue", "Queue", "in-queue.html", "ti-stack-2"),
("quarantine", "Quarantine", "in-quarantine.html", "ti-mail-pause"),
("filter-lists", "Filter lists", "in-filter-lists.html", "ti-filter"),
]),
("server", "Server", "health.html", "ti-server-2", [
("health", "Health", "health.html", "ti-heartbeat"),
("preflight", "Preflight", "preflight.html", "ti-checklist"),
("system-log", "System log", "system-log.html", "ti-file-text"),
("backup", "Backup", "backup.html", "ti-archive"),
("users", "Users", "users.html", "ti-users"),
("settings", "Settings", "settings.html", "ti-settings"),
]),
]
HEAD = """<!DOCTYPE html>
<html lang="en" data-theme="light">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{title} · SelfPost</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@1.0.4/css/bulma.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@tabler/icons-webfont@3/dist/tabler-icons.min.css">
<link rel="stylesheet" href="../shared/brand.css">
<link rel="stylesheet" href="theme.css">
</head>
"""
FOOT = ('<footer class="sp-foot">SelfPost 1.8.0 · © 2026 Mix · <a href="#">License (AGPL-3.0)</a>'
' · <a href="#">Source</a> · No warranty</footer>')
def navbar(section, page):
out = ['<nav class="navbar is-primary" aria-label="main navigation">', ' <div class="container">',
' <div class="navbar-brand"><a class="navbar-item" href="overview.html">'
'<img src="../../selfpost-stamp-compact.svg" alt="SelfPost"></a></div>',
' <div class="navbar-menu is-active">', ' <div class="navbar-start">']
for key, label, href, _icon, pages in NAV:
cur = " sp-current" if key == section else ""
if not pages:
out.append(f' <a class="navbar-item{cur}" href="{href}">{label}</a>')
continue
out.append(f' <div class="navbar-item has-dropdown is-hoverable{cur}">')
out.append(f' <a class="navbar-link" href="{href}">{label}</a>')
out.append(' <div class="navbar-dropdown">')
for pkey, plabel, phref, picon in pages:
act = " is-active" if key == section and pkey == page else ""
out.append(f' <a class="navbar-item{act}" href="{phref}"><i class="ti {picon}"></i>{plabel}</a>')
out.append(' </div>')
out.append(' </div>')
out += [' </div>', ' <div class="navbar-end">',
' <div class="navbar-item has-dropdown is-hoverable%s">' % (" sp-current" if section == "user" else ""),
' <a class="navbar-link" href="account.html"><i class="ti ti-user-circle"></i>admin</a>',
' <div class="navbar-dropdown is-right">',
' <a class="navbar-item" href="account.html"><i class="ti ti-key"></i>Account</a>',
' <a class="navbar-item" href="help.html"><i class="ti ti-help"></i>Help</a>',
' <hr class="navbar-divider">',
' <a class="navbar-item" href="login.html"><i class="ti ti-logout"></i>Sign out</a>',
' </div>', ' </div>', ' </div>', ' </div>', ' </div>', '</nav>',
'<div class="sp-perf" aria-hidden="true"></div>']
return "\n".join(out)
def subnav(section, page):
for key, _label, _href, _icon, pages in NAV:
if key == section and pages:
cur = ' aria-current="page"'
links = "".join(
f'<a href="{phref}"{cur if pkey == page else ""}><i class="ti {picon}"></i>{plabel}</a>'
for pkey, plabel, phref, picon in pages)
return f'<div class="sp-subnav"><div class="container">{links}</div></div>'
return ""
def build(path):
text = path.read_text(encoding="utf-8")
m = re.match(r"\s*<!--(.*?)-->\s*", text, re.S)
meta = dict(part.split(":", 1) for part in m.group(1).split("|"))
meta = {k.strip(): v.strip() for k, v in meta.items()}
body = text[m.end():]
html = HEAD.format(title=meta["title"])
if meta.get("shell") == "bare":
html += f"<body>\n{body}\n</body>\n</html>\n"
else:
section, page = meta.get("section", ""), meta.get("page", "")
html += (f"<body>\n{navbar(section, page)}\n{subnav(section, page)}\n<main>\n"
f' <div class="container sp-page">\n{body}\n {FOOT}\n </div>\n</main>\n</body>\n</html>\n')
(HERE / path.name).write_text(html, encoding="utf-8", newline="\n")
if __name__ == "__main__":
pages = sorted((HERE / "src").glob("*.html"))
for p in pages:
build(p)
print(f"built {len(pages)} pages")