feat(web): reusable flash/notification banner (PRG)

Add a one-shot flash component used across the app — connect success,
disconnect, account delete, and registration — instead of per-page ad-hoc
markup. setFlash queues a short-lived HttpOnly+SameSite cookie carrying an
opaque code; takeFlash consumes it on the next full-page render (not on
HTMX fragments). flashBanner maps the code to a styled, role=status banner;
the message text lives server-side in flashMessages so the cookie never
carries free text and a forged/unknown code renders nothing.

Wire it into the list page (the PRG landing spot for connect/registration)
and set it on registration and connect-callback success. Styled with the
existing design-system tokens; header gains an Account nav link.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-03 16:52:03 +02:00
co-authored by Claude Opus 4.8
parent 17d5e8c393
commit 2fe4833434
8 changed files with 448 additions and 214 deletions
+19 -3
View File
@@ -20,7 +20,10 @@ templ Layout(title string) {
@templ.Raw(styleTag)
</head>
<body>
<header><a href="/" class="brand">Tapir</a></header>
<header>
<a href="/" class="brand">Tapir</a>
<nav class="nav"><a href="/account">Account</a></nav>
</header>
<main>
{ children... }
</main>
@@ -28,10 +31,23 @@ templ Layout(title string) {
</html>
}
// flashBanner renders a one-shot notification for a flash code (connect success/
// failure, disconnect, delete, registration). An empty or unknown code renders
// nothing, so it is safe to drop into any page unconditionally. Reused across the
// app — not per-page ad-hoc markup.
templ flashBanner(code string) {
if f, ok := flashFor(code); ok {
<div class={ "flash", "flash-" + f.Kind } role="status" aria-live="polite">{ f.Message }</div>
}
}
// ListPage is the full summary list with the filter form. HTMX swaps only the
// #summary-list region; a non-HTMX request renders the whole page.
templ ListPage(rows []store.SummaryRow, f Filter) {
// #summary-list region; a non-HTMX request renders the whole page. flash carries
// a one-shot notification (e.g. "connected", "registered") surfaced on arrival
// after a POST→redirect.
templ ListPage(rows []store.SummaryRow, f Filter, flash string) {
@Layout("Tapir — Summaries") {
@flashBanner(flash)
@filterForm(f)
<div id="summary-list">
@summaryList(rows)