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