v0.22.0 4c072b1

pages.templ RawHistoryFolder

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

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 2

Tags 45

↑↓ moveenter openesc close