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}