detached 5041353

shell.go RawHistoryFolder

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}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 42

↑↓ moveenter openesc close