Files
member-console/internal/server/compose.go
T
cgalo5758 408fa6f5a6 Add page anatomy parts and UI quality gate
- Add shared ui_*.html parts (pageHeader, sectionHeader, statusBadge,
  emptyState) parsed into every template set
- Add anatomy lint rules with a shrinking allowlist and screen-coverage
  check
- Add make screens capture harness with contact sheets and baseline diff
- Compose member and FedWiki regions server-side so pages arrive
  complete
- Rebuild Domains and Integrations on the parts as pilots
2026-08-30 04:05:31 -05:00

92 lines
3.1 KiB
Go

package server
import (
"fmt"
"html/template"
"log/slog"
"net/http"
"net/http/httptest"
"net/url"
)
// Server-side composition (spec page-anatomy "A page arrives complete";
// docs/design-system.md §6). A page region that also exists as an HTMX
// partial, so a mutation can swap it back in, is rendered into the page by
// the server on first paint through an in-process request to the partial's
// own route. The browser never fetches first-paint content after load: one
// response, no spinner, and the partial stays the single source of the
// region's markup.
// Include renders a GET route in-process, with the caller's request context
// (session, request ID) and headers, and returns its HTML fragment. Core
// builds one against its router and hands it to integrations through Deps
// so their card bodies compose the same way.
type Include func(r *http.Request, path string) (template.HTML, error)
// NewInclude returns an Include that dispatches through router. The router
// is the mux the partial routes are registered on; the middleware stack sits
// outside it, so an include never re-authenticates: the session already
// rides the context it is given.
func NewInclude(router http.Handler) Include {
return func(r *http.Request, path string) (template.HTML, error) {
u, err := url.Parse(path)
if err != nil {
return "", fmt.Errorf("include %s: %w", path, err)
}
req := r.Clone(r.Context())
req.Method = http.MethodGet
req.URL = u
req.RequestURI = path
req.Body = http.NoBody
req.ContentLength = 0
req.Header.Set("HX-Request", "true")
rec := httptest.NewRecorder()
router.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
return "", fmt.Errorf("include %s: status %d", path, rec.Code)
}
return template.HTML(rec.Body.String()), nil
}
}
// IncludeOr renders the fragment, or an inline notice naming what could not
// be loaded, so a failing region never fails the page and never renders
// blank. A nil Include (a test set without a router) renders nothing.
func IncludeOr(inc Include, logger *slog.Logger, r *http.Request, path, what string) template.HTML {
if inc == nil {
return ""
}
out, err := inc(r, path)
if err != nil {
if logger != nil {
logger.Error("page region failed to compose", slog.String("path", path), slog.Any("error", err))
}
return template.HTML(`<div class="alert alert-danger" role="alert">Could not load ` + template.HTMLEscapeString(what) + `.</div>`)
}
return out
}
// DashboardCardView is a declared card with its body composed for this
// request. PartialPath stays the swap source for the card's RefreshEvent.
type DashboardCardView struct {
DashboardCard
Body template.HTML
}
// ProductsPageData is the /products page: the shell plus its three regions,
// composed from their partial routes.
type ProductsPageData struct {
Shell Shell
CSRFToken string
Entitlements template.HTML
Plans template.HTML
Addons template.HTML
}
// BillingPageData is the /billing page: the shell plus the invoices region.
type BillingPageData struct {
Shell Shell
CSRFToken string
Invoices template.HTML
}