docs: turn the accepted panel design into an enforceable contract
panel-redesign.md is rewritten for the accepted direction: three layers (Bulma as shipped, adapters, own sp- components), the page layouts, what an implementer must not do, and how that is enforced — guard tests that land before any template, a design-first CI step, and acceptance by evidence (test output plus a screenshot of every page beside its mockup, reviewed by a model that did not write the step). A components page shows every building block in every state; a screen may be built from nothing else. panel/check.py lints the mockups against the contract (class vocabulary, CSS bans on !important, id and page selectors and stray widths, page structure) and writes panel/outlines.json, the per-screen component skeleton an implementation is compared with, so a different design cannot pass as this one. Own modifier classes now carry the sp- prefix (sp-warn, sp-fail, sp-big, sp-danger-zone, ...) so the vocabulary is machine-checkable; the lint also caught a width set outside the layout in the mockup's own CSS. Agent rule .cursor/rules/panel-design.mdc; roadmap row and section updated (0/15, still candidate). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
+154
-152
@@ -1,197 +1,199 @@
|
||||
# Plan: panel-redesign
|
||||
|
||||
**Status:** candidate
|
||||
**Date:** 2026-09-08
|
||||
**Version:** `1.x` MINOR once implementation is agreed; this file is givens, not a
|
||||
build plan.
|
||||
**Date:** 2026-09-21 (replaces the 2026-09-08 givens)
|
||||
**Version:** `1.x` MINOR once agreed.
|
||||
|
||||
---
|
||||
|
||||
## What this file is
|
||||
|
||||
Source data for a visual redesign of the control panel. Mockups have not been
|
||||
drawn yet. Implementation in `internal/web` waits on those mockups and on
|
||||
roadmap status **agreed**.
|
||||
The design contract for the control panel and the plan to implement it. The
|
||||
design itself is drawn and accepted:
|
||||
|
||||
HTML click-throughs in [docs/assets/panel-ui/](../assets/panel-ui/index.html)
|
||||
are **not** the brief. They were an earlier layout experiment; new screens are
|
||||
drawn from this file's vocabulary, not copied from that folder.
|
||||
| Artefact | Role |
|
||||
|---|---|
|
||||
| [docs/assets/panel-redesign/panel/](../assets/panel-redesign/index.html) | 29 screens — **the** reference for what each page looks like |
|
||||
| [panel/components.html](../assets/panel-redesign/panel/components.html) | every component in every state — the only parts a page may be built from |
|
||||
| [panel/outlines.json](../assets/panel-redesign/panel/outlines.json) | the component skeleton of each screen, generated from the mockups |
|
||||
| [panel/check.py](../assets/panel-redesign/panel/check.py) | the contract as a program; its rules are ported to Go tests below |
|
||||
|
||||
When this file and a mockup disagree, the mockup wins and this file is fixed.
|
||||
When an implementation and a mockup disagree, the implementation is wrong.
|
||||
|
||||
The 2026-09-08 givens ("no component library, Web Awesome layout CSS only") are
|
||||
withdrawn. `docs/assets/panel-ui/` and the stage-1 directions `a/`, `b/`, `c/`
|
||||
are history, not a brief.
|
||||
|
||||
## Goal
|
||||
|
||||
Stop hand-tuning every card on every page to hide meaningless empty space.
|
||||
Operators should get a panel whose width rules are the same on Status, a
|
||||
delete-confirm, and a six-column log: a form stays at a reading measure, ops
|
||||
content uses the remaining column, and a card fills its cell.
|
||||
A panel an operator opens a few times a year and still finds their way in: the
|
||||
important things in view, details one or two clicks away, one visual language
|
||||
on every page, and no page that tunes its own width.
|
||||
|
||||
This is a visual and CSS-grammar change. Routes, POST behaviour, and htmx
|
||||
fragments stay as they are unless a later mockup proves a control must move.
|
||||
|
||||
## Pain (why)
|
||||
|
||||
[panel.css](../../internal/web/view/static/panel.css) already juggles several
|
||||
widths (`64rem` column, `48rem` measure, `main.wide`, `.card.narrow`,
|
||||
`page-login`) and two names for one pairing (`.split` everywhere, `.pair` on
|
||||
DMARC). Each new screen picks a width by eye. On a wide monitor that leaves
|
||||
voids; stretching a single input to the window is unreadable. The fix is a
|
||||
layout grammar, not another button library.
|
||||
|
||||
## Delivery constraints (fixed unless product says otherwise)
|
||||
|
||||
The panel is not an SPA. Changing any of these needs an explicit decision, not
|
||||
a mockup flourish:
|
||||
## Delivery constraints (unchanged)
|
||||
|
||||
| Constraint | Where |
|
||||
|---|---|
|
||||
| Go `html/template`, server-rendered pages | [internal/web/view/](../../internal/web/view/) |
|
||||
| Vendored htmx 2.x for polling fragments | [layout.html](../../internal/web/view/templates/layout.html), [development.md](../development.md) § Vendored front-end |
|
||||
| Go `html/template`, server-rendered; htmx 2.x for polled fragments | [internal/web/view/](../../internal/web/view/) |
|
||||
| Assets embedded (`//go:embed`), no CDN at runtime | [view.go](../../internal/web/view/view.go) |
|
||||
| CSP `default-src 'self'` — no inline script or `style=""` | [security.go](../../internal/web/security.go), `TestNoTemplateUsesInlineScriptOrStyle` |
|
||||
| State changes are native HTML POST | templates under `internal/web/view/templates/` |
|
||||
| AGPL-3.0; new front-end must be MIT-or-better and listed in NOTICE | [NOTICE](../../NOTICE), [development.md](../development.md) |
|
||||
| CSP `default-src 'self'` — no inline script, no `style=""` | [security.go](../../internal/web/security.go) |
|
||||
| State changes are native HTML POST; no script is the only way to submit | templates |
|
||||
| New front-end is MIT-or-better and listed in NOTICE | [NOTICE](../../NOTICE) |
|
||||
|
||||
JavaScript is not a product requirement. [security.md](../security.md) records
|
||||
progressive enhancement so **authorization does not depend on JS** (origin
|
||||
check, session, RBAC). `data-confirm` is deliberately not a security boundary.
|
||||
The redesign may use JS for chrome (copy, `<dialog>`). It must not make a
|
||||
script the only way to submit a form.
|
||||
Bulma was chosen partly because it needs nothing from this table relaxed: no
|
||||
JavaScript, no `data:` URIs.
|
||||
|
||||
Brand carriers today: stamp, brick, IBM Plex — [product.md](../product.md) and
|
||||
[docs/assets/selfpost-proof.html](../assets/selfpost-proof.html). New mockups
|
||||
may restyle; they must not silently drop the mark or invent a second product
|
||||
look.
|
||||
## Information architecture
|
||||
|
||||
## Screens in scope (information architecture)
|
||||
Navigation is grouped by mail direction. Page titles carry the group
|
||||
(**Outbound domains**, **Inbound log**); menu entries do not repeat it.
|
||||
|
||||
Same pages as the live panel. Visibility flags (inbound, DMARC, global vs
|
||||
domain-admin) stay as in [web.go](../../internal/web/web.go).
|
||||
| Group | Pages | Today |
|
||||
|---|---|---|
|
||||
| Overview | Overview | `/status` (verdicts only) |
|
||||
| Outbound | Domains · Log · Queue · DMARC reports | `/domains`, `/deliveries`, `/mail-queue`, `/dmarc` |
|
||||
| Inbound | Domains · Log · Queue | `/inbound`; Log and Queue are new |
|
||||
| Server | Health · System log · Backup · Users · Settings | Health is new (the tables that left `/status`) |
|
||||
| User menu | Account · Help · Sign out | Account is the credentials half of `/settings` |
|
||||
|
||||
| Kind | Pages |
|
||||
Visibility flags (global vs domain-admin, inbound, DMARC) stay as in
|
||||
[web.go](../../internal/web/web.go): a hidden page leaves no gap in the menu.
|
||||
Whether paths are renamed to match (`/outbound/log`, …) with redirects from
|
||||
the old ones is decided in checklist step 3, not by the implementer.
|
||||
|
||||
**Needs its own roadmap decision before it is built** (the mockups show the
|
||||
target; none of this is smuggled in with the restyle):
|
||||
|
||||
- Queue as a table — parse `postqueue -j` instead of printing `postqueue -p`.
|
||||
- Inbound queue — the same queue filtered by recipient domain.
|
||||
- Inbound log — rides on `inbound-antispam-panel` (`1.10.0`).
|
||||
- Server › Health — a route for the tables removed from Overview.
|
||||
|
||||
Until a row above is agreed, its page ships in the old content inside the new
|
||||
shell, and its menu entry is hidden if the page does not exist.
|
||||
|
||||
## Three layers
|
||||
|
||||
1. **Bulma, as shipped.** Vendored `bulma.min.css`, never edited. Only the
|
||||
classes listed in `check.py` (`BULMA`) are used; reaching for another is a
|
||||
design change.
|
||||
2. **Adapters** — where display logic lives once:
|
||||
`status_tag` (status → `tag is-… is-light`) as a template function, and
|
||||
`copy_field` (the read-only value with its Copy button).
|
||||
3. **Own components** — everything with an `sp-` class, styled in one
|
||||
`panel.css`, each rendered by one partial in `components.html`:
|
||||
|
||||
| Partial | What it is |
|
||||
|---|---|
|
||||
| Signed-out | login, setup |
|
||||
| Global ops | Status (plus polled fragment), mail queue, system log, backup, users |
|
||||
| Shared | domains, domain detail, deliveries, delivery, help, settings |
|
||||
| Optional | inbound (+ domain, delete), DMARC hub / domain / report |
|
||||
| Confirms / forms | domain delete, inbound delete, user create/edit/delete |
|
||||
| `page_head` | path, `<h1>`, lead, actions; optionally a `postmark` |
|
||||
| `postmark` | the round verdict stamp: ok / `sp-warn` / `sp-fail` |
|
||||
| `box`, `box_head`, `box_foot` | numbered mono head with an end slot; variants `sp-danger-zone`, `sp-credential` |
|
||||
| `health_card` | Overview only; ok / `sp-warn` / `sp-fail`; links to its detail |
|
||||
| `dns_record` | one block per record: status, what to publish, "in DNS now" only on a mismatch |
|
||||
| `facts` | label / value grid, `sp-big` for headline numbers |
|
||||
| `side_menu` | Bulma `menu` with icons and count tags; sticky |
|
||||
| `timeline`, `log_pane`, `empty_state`, `confirm_list`, `help_topic` | as on the components page |
|
||||
| form rows | `sp-row` (wide + short), `sp-pair` (two equal) |
|
||||
|
||||
## Layout layer (decided)
|
||||
The shell — brick navbar with hover menus, perforated edge, sibling-page
|
||||
strip, footer — belongs to `layout.html` alone.
|
||||
|
||||
**Hybrid.** Page-level regions are ours. In-page composition is vendored Web
|
||||
Awesome **layout CSS only** (MIT Core), not the component library and not the
|
||||
full `utilities.css` barrel.
|
||||
## Layout rules
|
||||
|
||||
Every Layout ([every-layout.dev](https://every-layout.dev/)) is the *school*
|
||||
(Stack, Cluster, Sidebar, Grid). It is a paid book; redistribution is
|
||||
forbidden. Do not vendor it, do not buy it for the image, do not put their
|
||||
custom elements in the tree. Web Awesome `layout.css` is the MIT packaging of
|
||||
those recipes (`wa-stack` ≈ Stack, `wa-cluster` ≈ Cluster, `wa-flank` ≈
|
||||
Sidebar, `wa-grid` ≈ Grid/Switcher).
|
||||
A page is exactly one of:
|
||||
|
||||
### Regions (our CSS)
|
||||
- **list** — `page_head`, then full-width boxes: the add form, then the table.
|
||||
- **detail** — `page_head`, then `columns`: `is-3` `side_menu` + `is-9` boxes.
|
||||
- **form** — `page_head`, then boxes in equal `columns`, one submit row below.
|
||||
- **signed out** — the split brand / form screen.
|
||||
|
||||
Shell: navigation column plus content that takes the rest (today's `.shell`,
|
||||
without a centred `64rem` well that leaves side voids).
|
||||
Width comes from the layout, never from the page. A narrow form does not get a
|
||||
`max-width`; it gets a column and a neighbour that explains it.
|
||||
|
||||
Inside the content column, **two modes only**. A page must not invent a third
|
||||
width:
|
||||
## The contract
|
||||
|
||||
- **`measure`** — one readable form (login, setup, delete confirm, user form).
|
||||
Cap around `42rem`, **left-aligned**. Fields fill the measure, not the
|
||||
window. Empty space to the right of a form on a wide monitor is acceptable.
|
||||
- **`fill`** — ops: Status, tables, logs, queue, DNS, paired cards. One column
|
||||
cap for all fill pages (pick a single value in mockups, e.g. ~`90rem`; do
|
||||
not keep both `48rem` and `64rem`). A card fills its grid cell.
|
||||
An implementer — human or model — **must not**:
|
||||
|
||||
There is no `main.wide` or `.card.narrow` as a width exception. A narrow card
|
||||
lives in `measure`. A wide table lives in `fill`.
|
||||
1. Write an `sp-` class, a Bulma class outside the subset, or any other class
|
||||
that the components page does not show.
|
||||
2. Add a component, a state or a layout. If a screen seems to need one, **stop
|
||||
and ask**; do not approximate it with ad-hoc markup.
|
||||
3. Put a selector in `panel.css` that names a page or an element id, uses
|
||||
`!important`, or sets a width outside the `WIDTH_OK` list.
|
||||
4. Write component markup by hand in a page template instead of calling the
|
||||
partial.
|
||||
5. Edit the vendored Bulma, or load a second CSS framework, icon set or any
|
||||
JavaScript widget.
|
||||
6. Change a mockup, `outlines.json`, `check.py` or a guard test in the same
|
||||
commit as `internal/web`, or at all without the owner's explicit
|
||||
instruction. **The guards are not the implementer's to adjust.** A failing
|
||||
guard means the implementation is wrong, not the guard.
|
||||
7. Report a step as done on the strength of a description. Done means the
|
||||
evidence below exists.
|
||||
|
||||
### In-page (vendored WA CSS)
|
||||
**Changing the design** is allowed and has one route: mockup first. Edit the
|
||||
fragment in `panel/src/`, the components page if a part changed, run
|
||||
`build.py` and `check.py --write`, commit as `design: …` with the reason, get
|
||||
it accepted — and only then touch `internal/web`.
|
||||
|
||||
Embed under `/static`, same as htmx — never a runtime CDN.
|
||||
## Enforcement
|
||||
|
||||
| Take | Do not take |
|
||||
Guard tests live in `internal/web/view` and run in `go test ./...` and CI.
|
||||
They land **before** any template is restyled (checklist step 2), written and
|
||||
reviewed by models other than the one that implements the templates.
|
||||
|
||||
| Test | Fails when |
|
||||
|---|---|
|
||||
| `utilities/layout.css` | barrel `utilities.css` |
|
||||
| `utilities/gap.css` | `themes/default.css`, `native.css` |
|
||||
| `align-items.css`, `justify-content.css`, `flex-wrap.css` if a mockup needs them | variants, prose, FOUCE, JS, icons, Pro |
|
||||
| A short `:root` stub for `--wa-space-*` only | their `layers.css` (it styles `wa-page`) |
|
||||
| `TestPanelClassVocabulary` | a template uses a class outside the Bulma subset + the `sp-` classes of `panel.css`; an `sp-` class is defined but unused, or used but absent from `components.html` |
|
||||
| `TestPanelCSSContract` | `panel.css` has `!important`, an id or page-name selector, or a width outside `WIDTH_OK` |
|
||||
| `TestTemplatesUseComponents` | a page template contains `sp-box-head`, `sp-postmark`, `sp-record`, `sp-facts`, … literally instead of `{{template "…"}}` |
|
||||
| `TestPanelOutlinesMatchMockups` | a page rendered with fixtures has a different component skeleton than `outlines.json` — a different design cannot pass as this one |
|
||||
| `TestVendoredBulmaChecksum` | `bulma.min.css` differs from the pinned SHA-256 |
|
||||
| `TestNoTemplateUsesInlineScriptOrStyle` | (exists) inline `style` / script |
|
||||
|
||||
Declare `@layer wa-utilities` locally. One spacing scale for the whole layer
|
||||
(map to today's `1.2rem` gaps if that looks right; do not keep a second scale).
|
||||
CI adds one step, `design-first`: a commit that touches
|
||||
`internal/web/view/**` must not touch `docs/assets/panel-redesign/panel/**`
|
||||
or the guard tests.
|
||||
|
||||
`.card` with slots (head / body / actions) stays ours so a `wa-grid` cell can
|
||||
stretch. Do not use `wa-card`.
|
||||
### Evidence for "done"
|
||||
|
||||
### Class vocabulary
|
||||
Every checklist step that restyles pages ends with, in the commit:
|
||||
|
||||
**Paired cards are `wa-grid`, not `wa-split`.** In Web Awesome, `wa-split` is
|
||||
space-between (title ‖ action). `wa-grid` is `auto-fit` + `--min-column-size`,
|
||||
which is today's `.split { minmax(22rem, 1fr) }`. Freeze `--min-column-size`
|
||||
once (same order as `22rem`); do not retune it per page.
|
||||
- `go test ./internal/web/...` output in the commit message body (pass);
|
||||
- a screenshot of each restyled page at 1440×900, saved next to its mockup's
|
||||
name under `docs/assets/panel-redesign/evidence/`, rendered from the running
|
||||
container with the e2e fixtures — not from the mockup;
|
||||
- a review by a model that did not write the step
|
||||
([development.md](../development.md) § Model routing): mockup and screenshot
|
||||
side by side, every difference either fixed or listed for the owner.
|
||||
|
||||
| Class | Use |
|
||||
|---|---|
|
||||
| `measure` / `fill` | page region only, never inside a card |
|
||||
| `wa-stack` | form or card body; children stretch on the block axis |
|
||||
| `wa-cluster` | button rows, tags, Copy beside a value |
|
||||
| `wa-grid` | two or more equal cards or field columns |
|
||||
| `wa-split` | card header: title ‖ help/action; label ‖ value |
|
||||
| `wa-flank` | rare: fixed + fluid (icon beside a field) |
|
||||
| `wa-gap-*` | change a gap; not a substitute for a region |
|
||||
The owner accepts a step by looking at the pairs. A step without them is not
|
||||
reviewed.
|
||||
|
||||
`wa-frame` is for media boxes; the panel has none — omit from mockups.
|
||||
## Out of scope
|
||||
|
||||
One child in `wa-grid` occupies the row (`auto-fit`, not `auto-fill`): a
|
||||
domain-admin Settings card must not leave an empty second column.
|
||||
|
||||
**Forbidden in mockups and later in templates:** a one-off `max-width` “for
|
||||
this page”; a second name for the same pairing (today `.pair` vs `.split`);
|
||||
`margin-inline: auto` on a card inside a grid (that already collapsed `.split`
|
||||
tracks).
|
||||
|
||||
**Ready when:** Status (`fill` + `wa-grid` pairs) and login (`measure` +
|
||||
`wa-stack`) can be drawn with no extra width rule. If a third screen wants a
|
||||
new `max-width`, the vocabulary is unfinished.
|
||||
|
||||
## Components (decided)
|
||||
|
||||
**No JavaScript component library.** Pico, Bootstrap JS, DaisyUI, FAST,
|
||||
Shoelace, and Web Awesome custom elements (`wa-button`, `wa-dialog`,
|
||||
`wa-drawer`, `wa-card`) are out. They do not fix empty cards; they add Lit or
|
||||
another runtime, Shadow DOM, CSP risk, and friction with htmx swaps.
|
||||
|
||||
What the live panel actually uses — and what mockups should assume:
|
||||
|
||||
| Need | Source |
|
||||
|---|---|
|
||||
| Button, input, select, textarea, table | native HTML, our CSS (brick / Plex) |
|
||||
| Card | our `.card` + slots |
|
||||
| Flash, status tint | our CSS |
|
||||
| Help / application panels | CSS disclosure (checkbox today); or `<details>` / Popover if a mockup prefers |
|
||||
| Destructive confirm | `data-confirm` / `window.confirm`, plus existing confirm pages for high blast radius |
|
||||
| Overlay, if a mockup truly needs one | HTML [`<dialog>`](https://developer.mozilla.org/docs/Web/HTML/Element/dialog), not `wa-dialog` |
|
||||
| Copy-to-clipboard | existing `panel.js` |
|
||||
|
||||
There is no combobox, date picker, tab set, tree, or chart in the panel. If a
|
||||
future mockup invents a widget native HTML cannot express, choose a library
|
||||
then, for that widget only.
|
||||
|
||||
## Out of scope for this item
|
||||
|
||||
- New panel features, routes, or copy (those are other roadmap rows).
|
||||
- Weakening CSP to accommodate a widget library.
|
||||
- Web Awesome Pro, Font Awesome Kit, a JS bundler for Lit.
|
||||
- Replacing htmx with a front-end framework.
|
||||
- Implementing `docs/assets/panel-ui/` as-is.
|
||||
|
||||
## Next (not this file's job)
|
||||
|
||||
1. Draw new mockups using only the vocabulary above.
|
||||
2. After mockups are accepted and this item is **agreed**: vendor the WA CSS
|
||||
files, NOTICE / development.md rows, then restyle templates without changing
|
||||
behaviour.
|
||||
- New features beyond the four rows listed under Information architecture.
|
||||
- Dark scheme and the narrow-screen shell: Bulma ships both; they are drawn
|
||||
and agreed as a `design:` change before anyone implements them.
|
||||
- Weakening CSP; a JS bundler; a front-end framework.
|
||||
|
||||
## Implementation checklist
|
||||
|
||||
None until mockups exist and the roadmap status is **agreed**. Do not start
|
||||
vendoring or rewriting `internal/web` from this file alone.
|
||||
No code until the roadmap status is **agreed**.
|
||||
|
||||
- [ ] Vendor Bulma 1.0.4 and the used Tabler icons (MIT) under `/static`, NOTICE and development.md rows, pinned checksum — **Haiku**
|
||||
- [ ] Guard tests from the table above, red against today's templates where expected; `design-first` CI step — **Opus**
|
||||
- [ ] Owner decision recorded here: keep today's paths or rename with redirects — **owner**
|
||||
- [ ] `panel.css` from `panel/theme.css`; `components.html` partials; `status_tag`; a `/components` page behind the global role rendering every partial — **Sonnet**
|
||||
- [ ] `layout.html`: navbar, perforated edge, sibling strip, user menu, footer; visibility flags — **Sonnet**
|
||||
- [ ] Signed-out pages: login, setup — **Sonnet**
|
||||
- [ ] Overview (verdicts) and Account / Settings split — **Sonnet**
|
||||
- [ ] Outbound: domains, domain, domain settings, application form, shown-once password, delete — **Sonnet**
|
||||
- [ ] Outbound: log, message, DMARC hub / domain / report — **Sonnet**
|
||||
- [ ] Inbound: domains, domain, delete — **Sonnet**
|
||||
- [ ] Server: system log, backup, users, user form, delete; Help — **Sonnet**
|
||||
- [ ] Remove the old `panel.css` rules and classes nothing references; vocabulary test green with no allow-list — **Sonnet**
|
||||
- [ ] Technical review of the whole against the mockups, evidence pairs complete — **Opus**
|
||||
- [ ] guide.md screenshots and wording for the new navigation — **Sonnet**
|
||||
- [ ] `go vet`, `go test ./...`, e2e suite — **Haiku**
|
||||
|
||||
Reference in New Issue
Block a user