1package systemviews
2
3import (
4 "fmt"
5 "html"
6 "strings"
7
8 "github.com/a-h/templ"
9)
10
11type kvPair struct {
12 Label string
13 Value string
14}
15
16// renderedPage holds the data needed by renderAppFrame to produce the shell
17// fragment. Route views populate this; the app frame wraps it.
18type renderedPage struct {
19 route Route
20 title string
21 subtitle string
22 subtitleDetail string
23 body string
24}
25
26// renderAppFrame produces a self-contained HTML fragment (no <html>, <head>,
27// or <body>) that wraps route content in the shared shell. page.body is
28// inserted with templ.Raw, so callers must only pass trusted pre-rendered HTML.
29func renderAppFrame(page renderedPage, theme shellTheme) string {
30 return mustRenderComponent(appFrameComponent(page, theme, templ.Raw(page.body)))
31}
32
33func pageDocumentTitle(page renderedPage) string {
34 if title := strings.TrimSpace(page.title); title != "" {
35 return title
36 }
37 if subtitle := strings.TrimSpace(page.subtitle); subtitle != "" {
38 return subtitle
39 }
40 return routeSubtitle(page.route)
41}
42
43func appRootClass(theme shellTheme) string {
44 themeClass := theme.RootClass
45 if themeClass == "" {
46 themeClass = "sv-dark"
47 }
48 return "sv-app " + themeClass
49}
50
51func sectionClass(className string) string {
52 classes := "sv-section"
53 if strings.TrimSpace(className) != "" {
54 classes += " " + strings.TrimSpace(className)
55 }
56 return classes
57}
58
59func alertClass(kind string) string {
60 classes := "sv-alert"
61 switch kind {
62 case "error":
63 return classes + " sv-alert-error"
64 case "success":
65 return classes + " sv-alert-success"
66 default:
67 return classes
68 }
69}
70
71func alertRole(kind string) string {
72 if kind == "error" {
73 return "alert"
74 }
75 return "status"
76}
77
78func buttonClass(extra string) string {
79 extra = strings.TrimSpace(extra)
80 if extra == "" {
81 return "sv-button"
82 }
83 return "sv-button " + extra
84}
85
86func listHTML(rows, emptyMessage string) string {
87 if strings.TrimSpace(rows) == "" {
88 return emptyStateHTML(emptyMessage)
89 }
90
91 return fmt.Sprintf(`<ul class="sv-list">%s</ul>`, rows)
92}
93
94// listRowHTML inserts trusted, pre-sanitized raw HTML inside a list item.
95func linkHTML(href, label string) string {
96 return fmt.Sprintf(`<a class=%q href=%q>%s</a>`, "sv-link", html.EscapeString(sanitizeHref(href)), html.EscapeString(label))
97}
98
99func kvHTML(rows []kvPair) string {
100 var items strings.Builder
101 for _, row := range rows {
102 if strings.TrimSpace(row.Label) == "" && strings.TrimSpace(row.Value) == "" {
103 continue
104 }
105 items.WriteString(kvRowHTML(row.Label, row.Value))
106 }
107 if items.Len() == 0 {
108 return emptyStateHTML("No configuration details")
109 }
110
111 return fmt.Sprintf(`<dl class="sv-kv">%s</dl>`, items.String())
112}
113
114func kvRowHTML(label, value string) string {
115 return fmt.Sprintf(`<div class=%q><dt>%s</dt><dd>%s</dd></div>`, "sv-kv-row", html.EscapeString(label), html.EscapeString(value))
116}
117
118// sectionHTML wraps content in a styled section with a heading. inner is
119// passed through as raw HTML, so callers must provide trusted markup.
120func sectionHTML(class, title, inner string) string {
121 return fmt.Sprintf(`<section class=%q><h2>%s</h2>%s</section>`,
122 sectionClass(class),
123 html.EscapeString(title),
124 inner,
125 )
126}
127
128// emptyStateHTML returns a standard empty-state placeholder.
129func emptyStateHTML(message string) string {
130 return fmt.Sprintf(`<div class="sv-empty"><p>%s</p></div>`, html.EscapeString(message))
131}
132
133func errorStateHTML(message string) string {
134 return fmt.Sprintf(
135 `<div class="sv-error" role="alert"><p>Could not load this system view.</p><p class="sv-meta">%s</p></div>`,
136 html.EscapeString(message),
137 )
138}