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 asks the owner to confirm a login link. Opening the link consumes
174// nothing; only this form does. It is a state page with a button.
175templ Login(l Layout, p LoginPage) {
176 @Shell(l, nil) {
177 <section class="state" aria-labelledby="state-title">
178 <h1 id="state-title">{ p.Title }</h1>
179 for _, line := range p.Lines {
180 <p class="dim">{ line }</p>
181 }
182 <form class="state-act" method="post" action={ templ.SafeURL(p.Action) }>
183 <button type="submit" class="btn primary">{ p.Button }</button>
184 </form>
185 <p class="state-back"><a href="/">Back to home</a></p>
186 </section>
187 }
188}
189
190// PasskeyLogin is the sign-in page: a button the script (passkey.js) drives.
191// Without script the button is hidden and the page says what to do instead.
192templ PasskeyLogin(l Layout, p PasskeyLoginPage) {
193 @Shell(l, nil) {
194 <section class="state" aria-labelledby="state-title">
195 <h1 id="state-title">{ p.Title }</h1>
196 <form class="state-act" data-js data-passkey-form>
197 <button type="submit" class="btn primary" data-passkey="login" data-begin={ p.Begin } data-finish={ p.Finish }>{ p.Button }</button>
198 </form>
199 <p class="dim" role="status" aria-live="polite" data-passkey-status></p>
200 for _, line := range p.Lines {
201 <p class="dim">{ line }</p>
202 }
203 <p class="state-back"><a href="/">Back to home</a></p>
204 </section>
205 }
206}
207
208// Passkeys lists the owner's passkeys, removes them and adds one.
209templ Passkeys(l Layout, p PasskeysPage) {
210 @Shell(l, nil) {
211 <h2 class="rule-h"><span>Passkeys</span><span class="count">{ itoa(len(p.Rows)) }</span></h2>
212 <p class="dim">A passkey signs you in from this device or your phone, without a terminal. Removing one signs every browser out.</p>
213 if len(p.Rows) > 0 {
214 <table class="list passkeys" aria-label="Passkeys">
215 <thead>
216 <tr>
217 <th class="cur"></th>
218 <th class="c-name">Name</th>
219 <th class="c-when">Created</th>
220 <th class="c-when">Last used</th>
221 <th class="c-sync">Synced</th>
222 <th class="c-act"><span class="sr">Remove</span></th>
223 </tr>
224 </thead>
225 <tbody>
226 for _, r := range p.Rows {
227 <tr>
228 <td class="cur" aria-hidden="true"></td>
229 <td class="c-name">{ r.Name }</td>
230 <td class="c-when nowrap">
231 <time
232 if r.CreatedAt != "" {
233 datetime={ r.CreatedAt }
234 }
235 >{ r.Created }</time>
236 </td>
237 <td class="c-when nowrap">
238 <time
239 if r.LastUsedAt != "" {
240 datetime={ r.LastUsedAt }
241 }
242 >{ r.LastUsed }</time>
243 </td>
244 <td class="c-sync">
245 if r.Synced {
246 yes
247 } else {
248 no
249 }
250 </td>
251 <td class="c-act">
252 <form method="post" action={ templ.SafeURL(r.Remove) }>
253 <button type="submit" class="btn danger" aria-label={ "Remove passkey " + r.Name }>Remove</button>
254 </form>
255 </td>
256 </tr>
257 }
258 </tbody>
259 </table>
260 } else {
261 <p class="lede dim">no passkeys yet.</p>
262 }
263 if p.Full {
264 <p class="dim">{ itoa(p.Max) } passkeys are registered, the most there can be. Remove one to add another.</p>
265 } else {
266 <form class="passkey-add" data-js data-passkey-form>
267 <label for="passkey-name" class="sr">Name of the new passkey</label>
268 <input id="passkey-name" type="text" placeholder="Name, for example Pixel 9" autocomplete="off" data-passkey-name/>
269 <button type="submit" class="btn primary" data-passkey="register" data-begin={ p.Begin } data-finish={ p.Finish }>Add a passkey</button>
270 </form>
271 }
272 <p class="dim" role="status" aria-live="polite" data-passkey-status></p>
273 }
274}