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