Files
member-console/internal/web/uiparts.go
T
cgalo5758 8c85983758 Align operator overview with page anatomy parts
Align the landing surface with the shared parts: sectionHeader for the
regions, the new readout part for every headline count, flush lists for
the System sheet, chevron-marked linked cards, and a boxed paginated
activity feed. Add the raw-section-title lint rule and classify the
remaining hand-written titles; archive the overview-consistency change.
2026-09-06 19:50:36 -05:00

101 lines
4.8 KiB
Go

// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Commercial
// SPDX-FileCopyrightText: 2025-2026 Christian Galo
package web
import (
"html/template"
"io/fs"
"git.coopcloud.tech/wiki-cafe/member-console/internal/config"
"git.coopcloud.tech/wiki-cafe/member-console/internal/embeds"
"git.coopcloud.tech/wiki-cafe/member-console/internal/forms"
)
// SurfaceRoot is the location trail's first crumb: the surface a page
// belongs to (design D18 "The location trail on every page, rooted at the
// surface"). The two values live here, below every template-owning package,
// so the console's own sets (internal/server) and the integration-owned
// operator sets (fedwiki, discourse) register the same root and the trail
// reads "Operator / …" on every operator page whoever renders it.
type SurfaceRoot struct {
Label string
URL string
}
// OperatorSurfaceRoot is the operator surface's root crumb.
func OperatorSurfaceRoot() SurfaceRoot { return SurfaceRoot{Label: "Operator", URL: "/operator"} }
// MemberSurfaceRoot is the member surface's root crumb.
func MemberSurfaceRoot() SurfaceRoot { return SurfaceRoot{Label: "Home", URL: "/"} }
// ParseUIPartials parses the shared application shell
// (templates/partials/shell_*.html) and the page-anatomy parts
// (templates/partials/ui_*.html) into t, and installs the template
// functions they need. Every template set that renders a page or a partial
// calls it once, so the parts are defined by name (pageHeader,
// sectionHeader, statusBadge, emptyState, readout, helpIcon,
// shell_topbar.html, shell_rail_member.html) in every set and no set keeps
// its own list
// (docs/design-system.md §6 "Page anatomy"; spec page-anatomy "Every page
// is built from the shared parts").
//
// surfaceRoot renders no root crumb by default: this is the value every
// caller gets unless it registers its own (design D18 "The location trail
// on every page, rooted at the surface"). html/template resolves a
// function call against the template set's current function map at
// execution time, not at parse time, so a caller may call t.Funcs again
// with its own "surfaceRoot" — the operator surface's or the member
// surface's — any time before it executes, whether that happens before or
// after this call; the last registration wins for every template sharing
// the set, including ones already parsed. Callers that never override it
// (most test-only template sets) simply render no root crumb, the same as
// today.
func ParseUIPartials(t *template.Template) (*template.Template, error) {
sub, err := fs.Sub(embeds.Templates, "templates")
if err != nil {
return nil, err
}
t = t.Funcs(template.FuncMap{
"deploymentName": config.DeploymentName,
"supportURL": config.SupportURL,
"surfaceRoot": func() any { return nil },
// helpIcon has one implementation everywhere (design D23), unlike
// surfaceRoot: it builds the {Label, Text} value the helpIcon part
// renders from. A page-owning template set that itself calls
// {{ template "helpIcon" (helpIcon ...) }} (most do, once
// converted) must still register its own copy before parsing that
// template, the same reason deploymentName/supportURL are
// registered at those call sites too — this copy only reaches
// templates parsed here, by this call, or later.
"helpIcon": func(label, text string) struct{ Label, Text string } {
return struct{ Label, Text string }{Label: label, Text: text}
},
// shellConfirmForm renders the one shared confirm dialog
// (shell_confirm_modal.html, which this call also parses): a
// zero-argument function, not a per-page data field, because the
// dialog needs nothing from the page that opens it (task 5.6). It
// reads the registry rather than a direct reference to keep this
// package, which internal/server imports, from importing back:
// internal/server/shell_forms.go registers "shell.confirm" at its
// own package's init, always before any template executes.
"shellConfirmForm": func() forms.FormView {
spec, ok := forms.Lookup("shell.confirm")
if !ok {
return forms.FormView{}
}
return forms.Render(spec, forms.Binding{Mode: forms.ModeUnbound})
},
})
// The list scaffold (operator_list_controls.html: listControls,
// listPager, listNoMatch, listSearch) rides along: the operator shell's
// landing branch pages its activity feed through listPager
// (overview-consistency D5), and html/template's escape analysis
// demands every referenced template exist in a set even on branches a
// page never takes, so every set that renders operator.html needs the
// scaffold whether or not its own body lists records (the integrations'
// operator pages regressed this way once before, discourse-integration
// findings #8). The scaffold uses no functions, so it parses in any set.
return t.ParseFS(sub, "partials/shell_*.html", "partials/ui_*.html", "partials/operator_list_controls.html")
}