- 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
138 lines
5.2 KiB
Go
138 lines
5.2 KiB
Go
package server
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
"git.coopcloud.tech/wiki-cafe/member-console/internal/domains"
|
|
"git.coopcloud.tech/wiki-cafe/member-console/internal/integration"
|
|
)
|
|
|
|
// Page anatomy (docs/design-system.md §6; spec page-anatomy). These are the
|
|
// values the shared parts under templates/partials/ui_*.html render from.
|
|
// Pages build them in Go, usually as methods on their page data, so a
|
|
// template never composes a title, a badge class, or an empty state itself.
|
|
|
|
// Link is a labelled destination for a header slot.
|
|
type Link struct {
|
|
Label string
|
|
URL string
|
|
// Primary marks the page's single filled action (chrome-conventions
|
|
// "One dominant primary action per screen"); every other action renders
|
|
// in the outline style.
|
|
Primary bool
|
|
}
|
|
|
|
// PageHeader is what the pageHeader part renders: the page title at the one
|
|
// size, an optional back affordance above it, an optional one-sentence lead
|
|
// below it, and one right-hand slot holding either a muted count or one
|
|
// action control.
|
|
type PageHeader struct {
|
|
Title string
|
|
Lead string
|
|
Back *Link
|
|
Count string
|
|
Action *Link
|
|
}
|
|
|
|
// Check enforces the one-slot rule at render time: html/template aborts the
|
|
// execution when a method returns a non-nil error, so a page that supplies
|
|
// both a count and an action fails to render instead of rendering both.
|
|
func (h PageHeader) Check() (string, error) {
|
|
if h.Count != "" && h.Action != nil {
|
|
return "", fmt.Errorf("page header %q: the right-hand slot holds a count or an action, not both", h.Title)
|
|
}
|
|
return "", nil
|
|
}
|
|
|
|
// SectionHeader is what the sectionHeader part (and its sectionSummary
|
|
// variant for a <details> disclosure) renders: an h2 at the one section
|
|
// size with an optional muted count or one action.
|
|
type SectionHeader struct {
|
|
Title string
|
|
Count string
|
|
Action *Link
|
|
}
|
|
|
|
// Badge is what the statusBadge part renders. Tone is one of success,
|
|
// secondary, warning, danger, info, light; Title is an optional tooltip.
|
|
type Badge struct {
|
|
Label string
|
|
Tone string
|
|
Title string
|
|
}
|
|
|
|
// WithTitle returns the badge with a tooltip.
|
|
func (b Badge) WithTitle(title string) Badge {
|
|
b.Title = title
|
|
return b
|
|
}
|
|
|
|
// badgeMap is the one place a state becomes a label and a tone. Keys are the
|
|
// state strings the handlers emit (database enums, registry states, and the
|
|
// console's own readiness words). Labels are title case. Add a state here
|
|
// before emitting it; TestStatusBadgeMapIsComplete fails on the ones it
|
|
// knows about that are missing.
|
|
var badgeMap = map[string]Badge{
|
|
// Lifecycle
|
|
"active": {Label: "Active", Tone: "success"},
|
|
"inactive": {Label: "Inactive", Tone: "secondary"},
|
|
"pending": {Label: "Pending", Tone: "warning"},
|
|
"expired": {Label: "Expired", Tone: "secondary"},
|
|
"canceled": {Label: "Canceled", Tone: "secondary"},
|
|
"released": {Label: "Released", Tone: "secondary"},
|
|
"revoked": {Label: "Revoked", Tone: "secondary"},
|
|
"superseded": {Label: "Superseded", Tone: "secondary"},
|
|
"live": {Label: "Live", Tone: "success"},
|
|
"draft": {Label: "Draft", Tone: "secondary"},
|
|
"published": {Label: "Published", Tone: "success"},
|
|
"retired": {Label: "Retired", Tone: "secondary"},
|
|
// Readiness
|
|
"configured": {Label: "Configured", Tone: "success"},
|
|
"not_configured": {Label: "Not configured", Tone: "warning"},
|
|
"verified": {Label: "Verified", Tone: "success"},
|
|
"unverified": {Label: "Unverified", Tone: "secondary"},
|
|
"servable": {Label: "Servable", Tone: "success"},
|
|
"unservable": {Label: "Unservable", Tone: "secondary"},
|
|
"seen": {Label: "Seen", Tone: "info"},
|
|
// Kinds (neutral)
|
|
domains.KindOperatorRoot: {Label: "Operator root", Tone: "light"},
|
|
domains.KindMember: {Label: "Member", Tone: "light"},
|
|
domains.KindExternal: {Label: "External", Tone: "light"},
|
|
// Payments and projection
|
|
"paid": {Label: "Paid", Tone: "success"},
|
|
"unpaid": {Label: "Unpaid", Tone: "warning"},
|
|
"overdue": {Label: "Overdue", Tone: "danger"},
|
|
"synced": {Label: "Synced", Tone: "success"},
|
|
"not_mapped": {Label: "Not mapped", Tone: "secondary"},
|
|
"sync_pending": {Label: "Sync pending", Tone: "warning"},
|
|
"sync_failed": {Label: "Sync failed", Tone: "danger"},
|
|
}
|
|
|
|
// StatusBadge maps a state to its badge. An unknown state renders as a
|
|
// secondary badge with the raw state as its label, so it is visible rather
|
|
// than styled; the map's completeness test names it.
|
|
func StatusBadge(state string) Badge {
|
|
if b, ok := badgeMap[state]; ok {
|
|
return b
|
|
}
|
|
return Badge{Label: state, Tone: "secondary"}
|
|
}
|
|
|
|
// knownStates are the state strings the handlers emit through StatusBadge,
|
|
// gathered from the packages that define them so the completeness test
|
|
// follows their constants.
|
|
var knownStates = []string{
|
|
domains.StatusPending, domains.StatusActive, domains.StatusExpired, domains.StatusCanceled, domains.StatusReleased,
|
|
domains.KindOperatorRoot, domains.KindMember, domains.KindExternal,
|
|
string(integration.StateActive),
|
|
"not_configured", "servable", "unservable", "seen",
|
|
}
|
|
|
|
// countLabel renders "1 live claim" / "3 live claims" for header slots.
|
|
func countLabel(n int, singular, plural string) string {
|
|
if n == 1 {
|
|
return fmt.Sprintf("%d %s", n, singular)
|
|
}
|
|
return fmt.Sprintf("%d %s", n, plural)
|
|
}
|