v0.17.0 ab8e178

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

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