v0.36.0 1fb2421

pages.templ RawHistoryFolder

1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3package web
4
5import "fmt"
6
7// Home is the repository list.
8templ Home(l Layout, p HomePage) {
9	@Shell(l, nil) {
10		<h1 class="sr">Repositories</h1>
11		<div class="home" data-layout="cards">
12			<section class="home-repos" aria-label="Repositories">
13				<div class="home-bar">
14					<div class="filter">
15						<label for="home-filter" class="sr">filter</label>
16						<input
17							id="home-filter"
18							type="search"
19							data-filter="#repo-rows > li"
20							placeholder="Filter repositories…"
21							autocomplete="off"
22							hx-on:input="ermite.filter(this)"
23						/>
24					</div>
25					<nav class="sort" aria-label="Sort repositories">
26						<span class="mute">sort</span>
27						for _, s := range p.Sorts {
28							<a
29								href={ templ.SafeURL(s.Href) }
30								if s.Mark != "" {
31									aria-current="true"
32								}
33							>
34								{ s.Label }
35								if s.Mark != "" {
36									<span aria-hidden="true">{ s.Mark }</span>
37									<span class="sr">{ ", " + s.State }</span>
38								}
39							</a>
40						}
41					</nav>
42					<div class="layout-switch" role="group" aria-label="Layout" data-js>
43						<button type="button" class="btn icon" data-set-layout="cards" aria-pressed="true" title="Cards" aria-label="Cards">
44							<span class="ico i-cards" aria-hidden="true"></span>
45						</button>
46						<button type="button" class="btn icon" data-set-layout="split" aria-pressed="false" title="Two columns" aria-label="Two columns">
47							<span class="ico i-split" aria-hidden="true"></span>
48						</button>
49						<button type="button" class="btn icon" data-set-layout="compact" aria-pressed="false" title="Compact list" aria-label="Compact list">
50							<span class="ico i-compact" aria-hidden="true"></span>
51						</button>
52					</div>
53				</div>
54				<ul class="repos" id="repo-rows" data-rows data-sort={ p.Sort } data-desc={ fmt.Sprint(p.Desc) }>
55					for _, r := range p.Rows {
56						@HomeRowView(r)
57					}
58				</ul>
59				if len(p.Rows) == 0 {
60					<p class="lede dim">no repositories yet.</p>
61				}
62			</section>
63		</div>
64	}
65}
66
67// Summary is the summary of a repository: the README reads in the main
68// column at the reading measure; what one acts on sits in the rail.
69templ Summary(l Layout, p SummaryPage) {
70	@Shell(l, summaryRail(p)) {
71		if p.Readme != nil {
72			@ReadmeCard(*p.Readme, p.Docs...)
73		}
74	}
75}
76
77// latestTagFact is the "latest tag" row of About: the newest tag (its
78// commit subject on hover), how old it is, and the count of tags, a link to
79// all of them. With too many tags to date them, the count only.
80templ latestTagFact(t LatestTag) {
81	<dt>latest tag</dt>
82	<dd class="latest-tag">
83		if t.Name != "" {
84			<a class="tag-name" href={ templ.SafeURL(t.Href) } title={ t.Subject }>{ t.Name }</a>
85			if t.WhenAt != "" {
86				{ " · " }<time datetime={ t.WhenAt } title={ t.WhenAt }>{ t.When }</time>
87			}
88			{ " · " }
89		}
90		<a class="all-tags" href={ templ.SafeURL(t.RefsHref) }>{ plural(t.Count, "tag", "tags") }</a>
91	</dd>
92}
93
94templ summaryRail(p SummaryPage) {
95	if p.Mirror != nil {
96		<section aria-labelledby="r-mirror">
97			<h2 id="r-mirror">Mirror</h2>
98			<p>⚠ Mirror { p.Mirror.State }</p>
99			if p.Mirror.Reason != "" {
100				<p>{ p.Mirror.Reason }</p>
101			}
102			<p>Run <code>{ p.Mirror.Command }</code></p>
103		</section>
104	}
105
106	<section aria-labelledby="r-clone"><h2 id="r-clone">Clone</h2>@CloneBlock(p.Clone)
107</section>
108	<section aria-labelledby="r-facts">
109		<h2 id="r-facts">About</h2>
110		<dl class="kv">
111			for _, f := range p.Facts {
112				<dt>{ f.Key }</dt>
113				@FactValue(f)
114			}
115			if lic := p.License; lic != nil {
116				<dt>license</dt>
117				<dd><a href={ templ.SafeURL(lic.Href) }>{ lic.ID }</a></dd>
118			}
119			if p.Latest != nil {
120				@latestTagFact(*p.Latest)
121			}
122			if r := p.Release; r != nil {
123				<dt>release</dt>
124				<dd>
125					<a href={ templ.SafeURL(r.Href) }>{ r.Tag }</a>{ " · " }
126					<time datetime={ r.WhenAt } title={ r.WhenAt }>{ r.When }</time>{ " · " }
127					<a href={ templ.SafeURL(r.AllHref) }>all releases</a>
128				</dd>
129			}
130		</dl>
131	</section>
132	if len(p.Recent) > 0 {
133		<section aria-labelledby="r-recent"><h2 id="r-recent">Recent commits <a href={ templ.SafeURL(p.AllHref) }>View all</a></h2>@CommitRows(p.Recent)
134</section>
135	}
136	if p.Actions != nil {
137		@RecentActionsBlock(*p.Actions)
138	}
139	if len(p.Outline) > 0 && p.Readme != nil {
140		<section aria-labelledby="r-outline"><h2 id="r-outline">On this page</h2>@Outline(p.Outline)
141</section>
142	}
143}
144
145// Refs lists the branches and tags of a repository.
146templ Refs(l Layout, p RefsPage) {
147	@Shell(l, refsRail(p)) {
148		<div class="filter">
149			<label for="ref-filter" class="sr">filter</label>
150			<input
151				id="ref-filter"
152				type="search"
153				data-filter="#ref-rows > tr"
154				placeholder="Filter branches and tags…"
155				autocomplete="off"
156				hx-on:input="ermite.filter(this)"
157			/>
158		</div>
159		<table class="list refs" aria-label="Branches and tags">
160			<thead>
161				<tr><th class="c-kind">kind</th><th class="c-name">name</th><th class="c-sha">commit</th><th class="c-sub">subject</th><th class="c-when">date</th></tr>
162			</thead>
163			@RefsBody(p)
164		</table>
165		if p.Note != "" {
166			<p class="note">{ p.Note }</p>
167		}
168	}
169}
170
171templ refRow(kind string, r RefRow) {
172	<tr data-cursor?={ r.Cursor }>
173		<td class="c-kind mute">{ kind }</td>
174		<td class="c-name">
175			<a href={ templ.SafeURL(r.Href) }>{ r.Name }</a>
176			if r.Default {
177				<span class="pill">default</span>
178			}
179		</td>
180		<td class="c-sha">
181			if r.CHref != "" {
182				<a href={ templ.SafeURL(r.CHref) }>{ r.Short }</a>
183			} else {
184				{ r.Short }
185			}
186			@CommitChecks(r.Checks)
187		</td>
188		<td class="c-sub">{ r.Subject }</td>
189		<td class="c-when dim">
190			if r.WhenAt != "" {
191				<time datetime={ r.WhenAt } title={ r.WhenAt }>{ r.When }</time>
192			}
193		</td>
194	</tr>
195}
196
197templ refsRail(p RefsPage) {
198	<section aria-labelledby="r-refs">
199		<h2 id="r-refs">Overview</h2>
200		@RefsKV(p)
201	</section>
202}
203
204// State is a 404, 500 or empty-repository page inside the normal shell.
205templ State(l Layout, p StatePage) {
206	@Shell(l, nil) {
207		<section class="state" aria-labelledby="state-title">
208			<p class="eyebrow">{ p.Code }</p>
209			<h1 id="state-title">{ p.Title }</h1>
210			for _, line := range p.Lines {
211				<p class="dim">{ line }</p>
212			}
213			if p.Clone != nil {
214				@CloneBlock(*p.Clone)
215			}
216			<p class="state-back">
217				for _, b := range p.Back {
218					<a href={ templ.SafeURL(b.Href) }>{ b.Label }</a>
219				}
220			</p>
221		</section>
222	}
223}
224
225// Login signs the browser in with a login link. Opening the link consumes
226// nothing; only this form does. ermite.js submits it as soon as a browser
227// shows the page (data-autosubmit): link previews and prefetchers run no
228// script, so they still cannot use the link. Without scripts the owner
229// clicks the button.
230templ Login(l Layout, p LoginPage) {
231	@Shell(l, nil) {
232		<section class="state" aria-labelledby="state-title">
233			<h1 id="state-title">{ p.Title }</h1>
234			for _, line := range p.Lines {
235				<p class="dim">{ line }</p>
236			}
237			<form class="state-act" method="post" action={ templ.SafeURL(p.Action) } data-autosubmit>
238				<button type="submit" class="btn primary">{ p.Button }</button>
239			</form>
240			<p class="state-back"><a href="/">Back to home</a></p>
241		</section>
242	}
243}
244
245// PasskeyLogin is the sign-in page: a button the script (passkey.js) drives.
246// Without script the button is hidden and the page says what to do instead.
247templ PasskeyLogin(l Layout, p PasskeyLoginPage) {
248	@Shell(l, nil) {
249		<section class="state" aria-labelledby="state-title">
250			<h1 id="state-title">{ p.Title }</h1>
251			<form class="state-act" data-js data-passkey-form>
252				<button type="submit" class="btn primary" data-passkey="login" data-begin={ p.Begin } data-finish={ p.Finish }>{ p.Button }</button>
253			</form>
254			<p class="dim" role="status" aria-live="polite" data-passkey-status></p>
255			for _, line := range p.Lines {
256				<p class="dim">{ line }</p>
257			}
258			<p class="state-back"><a href="/">Back to home</a></p>
259		</section>
260	}
261}
262
263// Passkeys lists the owner's passkeys, removes them and adds one.
264templ Passkeys(l Layout, p PasskeysPage) {
265	@Shell(l, nil) {
266		<h2 class="rule-h"><span>Passkeys</span><span class="count">{ itoa(len(p.Rows)) }</span></h2>
267		<p class="dim">A passkey signs you in from this device or your phone, without a terminal. Removing one signs every browser out.</p>
268		if len(p.Rows) > 0 {
269			<table class="list passkeys" aria-label="Passkeys">
270				<thead>
271					<tr>
272						<th class="c-name">Name</th>
273						<th class="c-when">Created</th>
274						<th class="c-when">Last used</th>
275						<th class="c-sync">Synced</th>
276						<th class="c-act"><span class="sr">Remove</span></th>
277					</tr>
278				</thead>
279				<tbody>
280					for _, r := range p.Rows {
281						<tr>
282							<td class="c-name">{ r.Name }</td>
283							<td class="c-when nowrap">
284								<time
285									if r.CreatedAt != "" {
286										datetime={ r.CreatedAt }
287									}
288								>{ r.Created }</time>
289							</td>
290							<td class="c-when nowrap">
291								<time
292									if r.LastUsedAt != "" {
293										datetime={ r.LastUsedAt }
294									}
295								>{ r.LastUsed }</time>
296							</td>
297							<td class="c-sync">
298								if r.Synced {
299									yes
300								} else {
301									no
302								}
303							</td>
304							<td class="c-act">
305								<form method="post" action={ templ.SafeURL(r.Remove) }>
306									<button type="submit" class="btn danger" aria-label={ "Remove passkey " + r.Name }>Remove</button>
307								</form>
308							</td>
309						</tr>
310					}
311				</tbody>
312			</table>
313		} else {
314			<p class="lede dim">no passkeys yet.</p>
315		}
316		if p.Full {
317			<p class="dim">{ itoa(p.Max) } passkeys are registered, the most there can be. Remove one to add another.</p>
318		} else {
319			<form class="passkey-add" data-js data-passkey-form>
320				<label for="passkey-name" class="sr">Name of the new passkey</label>
321				<input id="passkey-name" type="text" placeholder="Name, for example Pixel 9" autocomplete="off" data-passkey-name/>
322				<button type="submit" class="btn primary" data-passkey="register" data-begin={ p.Begin } data-finish={ p.Finish }>Add a passkey</button>
323			</form>
324		}
325		<p class="dim" role="status" aria-live="polite" data-passkey-status></p>
326	}
327}

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 4

Tags 45

↑↓ moveenter openesc close