detached 093dc62

feat(web): proof of work solver

pow-worker.js hashes SHA-256(nonce || ":" || n) with crypto.subtle in a
same-origin dedicated worker; issue-form.js drives the counters and the
submit button. Tested against a vector shared with the Go side.
commit
093dc62c15dc28fde5f62ff0e2715cc58bac4777
author
brice · · 2026-10-19 07:42 UTC
parent
3f9aa10
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
8 files · +470 -162
M internal/adapters/web/ASSETS.md +2 -0
@@ -13,6 +13,8 @@ content-hashed names. Nothing is fetched at runtime.
1313 | `static/js/log.js` | the live log of a running job (`window.ermiteLog`, tested in Node by `jstest/log.test.mjs`) | written here | |
1414 | `static/js/files.js` | the Files view: a tree whose directories open in place and walk by keyboard (`window.ermiteTree`, tested in Node by `jstest/tree.test.mjs`), the fuzzy file finder and the palette's file hits (`window.ermiteFinder`; its core is tested in Node by `jstest/files.test.mjs`) | written here | |
1515 | `static/js/passkey.js` | passkey sign-in on `/-/login` and passkey registration on `/-/passkeys`: a delegated click handler on `[data-passkey]` that runs the WebAuthn JSON API flow (`window.ermitePasskey`; tested in Node by `jstest/passkey.test.mjs`) | written here | |
16+| `static/js/issue-form.js` | the public issue form: starts the proof-of-work worker on load, length counters, the state of the submit button (`window.ermiteIssueForm`; core tested in Node by `jstest/issue-form.test.mjs`) | written here | |
17+| `static/js/pow-worker.js` | the dedicated Worker of the form: finds the counter `n` such that SHA-256(nonce bytes \|\| ":" \|\| decimal `n`) has the required leading zero bits, with `crypto.subtle`; same encoding as `usecase/issue_submissions.go` (shared vector tested on both sides). Loaded from the same origin by `new Worker(url)`, no `blob:` | written here | |
1618 | `static/css/56-chroma.css` | syntax classes of the code view, mapped to the `--syn-*` tokens (no colour of its own) | generated by `go run scripts/chroma-css.go` from chroma's token classes | |
1719 | `static/css/*.css` | native CSS, served as one stylesheet in name order; relative `url(fonts/…)` and `url(icons/…)` become hashed URLs | written here | |
1820 | `static/fonts/atkinson-next.woff2`, `atkinson-next-italic.woff2` | Atkinson Hyperlegible Next, variable weight, latin subset (SIL OFL 1.1, `static/fonts/OFL-next.txt`) | Google Fonts | `18b2a1a39a2fa298b0ba5390aca68462669826c90925656f1c1f6796e0e1bbaf`, `4a5037bfaf6680f40147407407ec09fa42925774bde809a579283d27f9f08106` |
@@ -13,6 +13,8 @@ content-hashed names. Nothing is fetched at runtime.
13 | `static/js/log.js` | the live log of a running job (`window.ermiteLog`, tested in Node by `jstest/log.test.mjs`) | written here | |13 | `static/js/log.js` | the live log of a running job (`window.ermiteLog`, tested in Node by `jstest/log.test.mjs`) | written here | |
14 | `static/js/files.js` | the Files view: a tree whose directories open in place and walk by keyboard (`window.ermiteTree`, tested in Node by `jstest/tree.test.mjs`), the fuzzy file finder and the palette's file hits (`window.ermiteFinder`; its core is tested in Node by `jstest/files.test.mjs`) | written here | |14 | `static/js/files.js` | the Files view: a tree whose directories open in place and walk by keyboard (`window.ermiteTree`, tested in Node by `jstest/tree.test.mjs`), the fuzzy file finder and the palette's file hits (`window.ermiteFinder`; its core is tested in Node by `jstest/files.test.mjs`) | written here | |
15 | `static/js/passkey.js` | passkey sign-in on `/-/login` and passkey registration on `/-/passkeys`: a delegated click handler on `[data-passkey]` that runs the WebAuthn JSON API flow (`window.ermitePasskey`; tested in Node by `jstest/passkey.test.mjs`) | written here | |15 | `static/js/passkey.js` | passkey sign-in on `/-/login` and passkey registration on `/-/passkeys`: a delegated click handler on `[data-passkey]` that runs the WebAuthn JSON API flow (`window.ermitePasskey`; tested in Node by `jstest/passkey.test.mjs`) | written here | |
16+| `static/js/issue-form.js` | the public issue form: starts the proof-of-work worker on load, length counters, the state of the submit button (`window.ermiteIssueForm`; core tested in Node by `jstest/issue-form.test.mjs`) | written here | |
17+| `static/js/pow-worker.js` | the dedicated Worker of the form: finds the counter `n` such that SHA-256(nonce bytes \|\| ":" \|\| decimal `n`) has the required leading zero bits, with `crypto.subtle`; same encoding as `usecase/issue_submissions.go` (shared vector tested on both sides). Loaded from the same origin by `new Worker(url)`, no `blob:` | written here | |
16 | `static/css/56-chroma.css` | syntax classes of the code view, mapped to the `--syn-*` tokens (no colour of its own) | generated by `go run scripts/chroma-css.go` from chroma's token classes | |18 | `static/css/56-chroma.css` | syntax classes of the code view, mapped to the `--syn-*` tokens (no colour of its own) | generated by `go run scripts/chroma-css.go` from chroma's token classes | |
17 | `static/css/*.css` | native CSS, served as one stylesheet in name order; relative `url(fonts/…)` and `url(icons/…)` become hashed URLs | written here | |19 | `static/css/*.css` | native CSS, served as one stylesheet in name order; relative `url(fonts/…)` and `url(icons/…)` become hashed URLs | written here | |
18 | `static/fonts/atkinson-next.woff2`, `atkinson-next-italic.woff2` | Atkinson Hyperlegible Next, variable weight, latin subset (SIL OFL 1.1, `static/fonts/OFL-next.txt`) | Google Fonts | `18b2a1a39a2fa298b0ba5390aca68462669826c90925656f1c1f6796e0e1bbaf`, `4a5037bfaf6680f40147407407ec09fa42925774bde809a579283d27f9f08106` |20 | `static/fonts/atkinson-next.woff2`, `atkinson-next-italic.woff2` | Atkinson Hyperlegible Next, variable weight, latin subset (SIL OFL 1.1, `static/fonts/OFL-next.txt`) | Google Fonts | `18b2a1a39a2fa298b0ba5390aca68462669826c90925656f1c1f6796e0e1bbaf`, `4a5037bfaf6680f40147407407ec09fa42925774bde809a579283d27f9f08106` |
M internal/adapters/web/assets.go +3 -1
@@ -24,7 +24,7 @@ const StaticPrefix = "/-/static/"
2424 // Assets are the URLs of the assets a page loads. Their names carry a hash of
2525 // the content, so they can be cached for ever.
2626 type Assets struct {
27- CSS, HTMX, Live, SSE, Store, App, Files, Passkey, Log, MR, Icon string
27+ CSS, HTMX, Live, SSE, Store, App, Files, Passkey, Log, MR, IssueForm, PowWorker, Icon string
2828 // Manifest is the web app manifest (install on a phone's home screen),
2929 // AppleIcon the home screen icon iOS reads from a link instead.
3030 Manifest, AppleIcon string
@@ -65,6 +65,8 @@ var scripts = []struct {
6565 {"passkey.js", func(a *Assets, u string) { a.Passkey = u }},
6666 {"log.js", func(a *Assets, u string) { a.Log = u }},
6767 {"mr.js", func(a *Assets, u string) { a.MR = u }},
68+ {"issue-form.js", func(a *Assets, u string) { a.IssueForm = u }},
69+ {"pow-worker.js", func(a *Assets, u string) { a.PowWorker = u }},
6870 }
6971
7072 // sheetAssets are the files the stylesheet references as url(<dir>/<file>):
@@ -24,7 +24,7 @@ const StaticPrefix = "/-/static/"
24 // Assets are the URLs of the assets a page loads. Their names carry a hash of24 // Assets are the URLs of the assets a page loads. Their names carry a hash of
25 // the content, so they can be cached for ever.25 // the content, so they can be cached for ever.
26 type Assets struct {26 type Assets struct {
27- CSS, HTMX, Live, SSE, Store, App, Files, Passkey, Log, MR, Icon string27+ CSS, HTMX, Live, SSE, Store, App, Files, Passkey, Log, MR, IssueForm, PowWorker, Icon string
28 // Manifest is the web app manifest (install on a phone's home screen),28 // Manifest is the web app manifest (install on a phone's home screen),
29 // AppleIcon the home screen icon iOS reads from a link instead.29 // AppleIcon the home screen icon iOS reads from a link instead.
30 Manifest, AppleIcon string30 Manifest, AppleIcon string
@@ -65,6 +65,8 @@ var scripts = []struct {
65 {"passkey.js", func(a *Assets, u string) { a.Passkey = u }},65 {"passkey.js", func(a *Assets, u string) { a.Passkey = u }},
66 {"log.js", func(a *Assets, u string) { a.Log = u }},66 {"log.js", func(a *Assets, u string) { a.Log = u }},
67 {"mr.js", func(a *Assets, u string) { a.MR = u }},67 {"mr.js", func(a *Assets, u string) { a.MR = u }},
68+ {"issue-form.js", func(a *Assets, u string) { a.IssueForm = u }},
69+ {"pow-worker.js", func(a *Assets, u string) { a.PowWorker = u }},
68 }70 }
69 71
70 // sheetAssets are the files the stylesheet references as url(<dir>/<file>):72 // sheetAssets are the files the stylesheet references as url(<dir>/<file>):
A internal/adapters/web/jstest/issue-form.test.mjs +96 -0
@@ -0,0 +1,96 @@
1+// Tests of the public issue form: the proof-of-work solver (static/js/pow-worker.js)
2+// and the pure core of the form (static/js/issue-form.js). Run with
3+// node --test 'internal/adapters/web/jstest/*.test.mjs'
4+import assert from 'node:assert/strict';
5+import { createHash } from 'node:crypto';
6+import { test } from 'node:test';
7+
8+globalThis.window = globalThis;
9+globalThis.document = { readyState: 'complete', querySelector: () => null, querySelectorAll: () => [], addEventListener() {} };
10+await import('../static/js/pow-worker.js');
11+await import('../static/js/issue-form.js');
12+const P = globalThis.ermitePow;
13+const C = window.ermiteIssueForm.core;
14+
15+// The vector of usecase/issue_pow_vector_test.go: the smallest solution for this
16+// nonce at this difficulty, found by the server's own powHash.
17+const VECTOR = { nonce: 'AAECAwQFBgcICQoLDA0ODw', difficulty: 12, solution: '2491' };
18+
19+test('leadingZeroBits counts the zero bits at the start', () => {
20+ assert.equal(P.leadingZeroBits(Uint8Array.of(0xff)), 0);
21+ assert.equal(P.leadingZeroBits(Uint8Array.of(0x80)), 0);
22+ assert.equal(P.leadingZeroBits(Uint8Array.of(0x40)), 1);
23+ assert.equal(P.leadingZeroBits(Uint8Array.of(0x01)), 7);
24+ assert.equal(P.leadingZeroBits(Uint8Array.of(0x00, 0x80)), 8);
25+ assert.equal(P.leadingZeroBits(Uint8Array.of(0x00, 0x00, 0x1f)), 19);
26+ assert.equal(P.leadingZeroBits(Uint8Array.of(0, 0)), 16);
27+ assert.equal(P.leadingZeroBits(Uint8Array.of()), 0);
28+});
29+
30+test('b64urlDecode reads the nonce the server sends', () => {
31+ assert.deepEqual([...P.b64urlDecode(VECTOR.nonce)], [...Array(16).keys()]);
32+ assert.deepEqual([...P.b64urlDecode('-_8')], [0xfb, 0xff]);
33+});
34+
35+test('the hashed bytes are nonce || ":" || decimal solution', () => {
36+ const nonce = P.b64urlDecode(VECTOR.nonce);
37+ const got = P.input(nonce, 2491);
38+ assert.equal(got.length, 16 + 1 + 4);
39+ assert.deepEqual([...got.slice(0, 16)], [...nonce]);
40+ assert.equal(Buffer.from(got.slice(16)).toString('latin1'), ':2491');
41+});
42+
43+test('the solver finds the very solution of the Go vector', async () => {
44+ const sol = await P.solve(P.b64urlDecode(VECTOR.nonce), VECTOR.difficulty);
45+ assert.equal(sol, VECTOR.solution);
46+ const sum = createHash('sha256').update(Buffer.concat([Buffer.from(P.b64urlDecode(VECTOR.nonce)), Buffer.from(':' + sol)])).digest();
47+ assert.ok(P.leadingZeroBits(sum) >= VECTOR.difficulty);
48+ // and no smaller counter works
49+ for (let n = 0; n < Number(sol); n++) {
50+ const s = createHash('sha256').update(Buffer.concat([Buffer.from(P.b64urlDecode(VECTOR.nonce)), Buffer.from(':' + n)])).digest();
51+ assert.ok(P.leadingZeroBits(s) < VECTOR.difficulty, 'counter ' + n);
52+ }
53+});
54+
55+test('the solver is independent of the batch size and reports progress', async () => {
56+ const nonce = P.b64urlDecode(VECTOR.nonce);
57+ const seen = [];
58+ const sol = await P.solve(nonce, VECTOR.difficulty, { batch: 100, onProgress: (n) => { seen.push(n); } });
59+ assert.equal(sol, VECTOR.solution);
60+ assert.deepEqual(seen.slice(0, 2), [100, 200]);
61+ const stopped = await P.solve(nonce, 40, { batch: 10, onProgress: (n) => n >= 30 });
62+ assert.equal(stopped, null, 'stopped by the caller');
63+});
64+
65+test('counterClass: warn from 90 %, over above the maximum', () => {
66+ assert.equal(C.counterClass(0, 120), '');
67+ assert.equal(C.counterClass(107, 120), '');
68+ assert.equal(C.counterClass(108, 120), 'warn');
69+ assert.equal(C.counterClass(120, 120), 'warn');
70+ assert.equal(C.counterClass(121, 120), 'over');
71+ assert.equal(C.counterClass(9000, 10000), 'warn');
72+ assert.equal(C.counterClass(8999, 10000), '');
73+ assert.equal(C.counterClass(41, 40), 'over');
74+});
75+
76+test('length counts characters, not UTF-16 units', () => {
77+ assert.equal(C.length('abc'), 3);
78+ assert.equal(C.length('a😀b'), 3);
79+ assert.equal(C.length(''), 0);
80+});
81+
82+test('state: the button opens only when solved, waited and not over', () => {
83+ const base = { solved: false, now: 1000, minAt: 5000, sending: false, unsupported: false, failed: false, overs: false };
84+ const s = (o) => C.state({ ...base, ...o });
85+ assert.deepEqual(s({}), { disabled: true, status: 'Checking your browser… (this takes a few seconds)' });
86+ assert.equal(s({ solved: true }).disabled, true, 'solved too early');
87+ assert.equal(s({ solved: true, now: 4999 }).disabled, true);
88+ assert.equal(s({ now: 6000 }).disabled, true, 'waited but not solved');
89+ assert.equal(s({ solved: true, now: 5000 }).disabled, false);
90+ assert.equal(s({ solved: true, now: 6000, overs: true }).disabled, true, 'a field is too long');
91+ assert.equal(s({ solved: true, now: 6000, sending: true }).disabled, true, 'no double submit');
92+ assert.equal(s({ solved: true, now: 6000, sending: true }).status, 'Sending…');
93+ assert.equal(s({ unsupported: true }).status, 'Your browser cannot run the check.');
94+ assert.equal(s({ solved: true, now: 6000, unsupported: true }).disabled, true);
95+ assert.equal(s({ failed: true, solved: true, now: 6000 }).disabled, true);
96+});
@@ -0,0 +1,96 @@
1+// Tests of the public issue form: the proof-of-work solver (static/js/pow-worker.js)
2+// and the pure core of the form (static/js/issue-form.js). Run with
3+// node --test 'internal/adapters/web/jstest/*.test.mjs'
4+import assert from 'node:assert/strict';
5+import { createHash } from 'node:crypto';
6+import { test } from 'node:test';
7+
8+globalThis.window = globalThis;
9+globalThis.document = { readyState: 'complete', querySelector: () => null, querySelectorAll: () => [], addEventListener() {} };
10+await import('../static/js/pow-worker.js');
11+await import('../static/js/issue-form.js');
12+const P = globalThis.ermitePow;
13+const C = window.ermiteIssueForm.core;
14+
15+// The vector of usecase/issue_pow_vector_test.go: the smallest solution for this
16+// nonce at this difficulty, found by the server's own powHash.
17+const VECTOR = { nonce: 'AAECAwQFBgcICQoLDA0ODw', difficulty: 12, solution: '2491' };
18+
19+test('leadingZeroBits counts the zero bits at the start', () => {
20+ assert.equal(P.leadingZeroBits(Uint8Array.of(0xff)), 0);
21+ assert.equal(P.leadingZeroBits(Uint8Array.of(0x80)), 0);
22+ assert.equal(P.leadingZeroBits(Uint8Array.of(0x40)), 1);
23+ assert.equal(P.leadingZeroBits(Uint8Array.of(0x01)), 7);
24+ assert.equal(P.leadingZeroBits(Uint8Array.of(0x00, 0x80)), 8);
25+ assert.equal(P.leadingZeroBits(Uint8Array.of(0x00, 0x00, 0x1f)), 19);
26+ assert.equal(P.leadingZeroBits(Uint8Array.of(0, 0)), 16);
27+ assert.equal(P.leadingZeroBits(Uint8Array.of()), 0);
28+});
29+
30+test('b64urlDecode reads the nonce the server sends', () => {
31+ assert.deepEqual([...P.b64urlDecode(VECTOR.nonce)], [...Array(16).keys()]);
32+ assert.deepEqual([...P.b64urlDecode('-_8')], [0xfb, 0xff]);
33+});
34+
35+test('the hashed bytes are nonce || ":" || decimal solution', () => {
36+ const nonce = P.b64urlDecode(VECTOR.nonce);
37+ const got = P.input(nonce, 2491);
38+ assert.equal(got.length, 16 + 1 + 4);
39+ assert.deepEqual([...got.slice(0, 16)], [...nonce]);
40+ assert.equal(Buffer.from(got.slice(16)).toString('latin1'), ':2491');
41+});
42+
43+test('the solver finds the very solution of the Go vector', async () => {
44+ const sol = await P.solve(P.b64urlDecode(VECTOR.nonce), VECTOR.difficulty);
45+ assert.equal(sol, VECTOR.solution);
46+ const sum = createHash('sha256').update(Buffer.concat([Buffer.from(P.b64urlDecode(VECTOR.nonce)), Buffer.from(':' + sol)])).digest();
47+ assert.ok(P.leadingZeroBits(sum) >= VECTOR.difficulty);
48+ // and no smaller counter works
49+ for (let n = 0; n < Number(sol); n++) {
50+ const s = createHash('sha256').update(Buffer.concat([Buffer.from(P.b64urlDecode(VECTOR.nonce)), Buffer.from(':' + n)])).digest();
51+ assert.ok(P.leadingZeroBits(s) < VECTOR.difficulty, 'counter ' + n);
52+ }
53+});
54+
55+test('the solver is independent of the batch size and reports progress', async () => {
56+ const nonce = P.b64urlDecode(VECTOR.nonce);
57+ const seen = [];
58+ const sol = await P.solve(nonce, VECTOR.difficulty, { batch: 100, onProgress: (n) => { seen.push(n); } });
59+ assert.equal(sol, VECTOR.solution);
60+ assert.deepEqual(seen.slice(0, 2), [100, 200]);
61+ const stopped = await P.solve(nonce, 40, { batch: 10, onProgress: (n) => n >= 30 });
62+ assert.equal(stopped, null, 'stopped by the caller');
63+});
64+
65+test('counterClass: warn from 90 %, over above the maximum', () => {
66+ assert.equal(C.counterClass(0, 120), '');
67+ assert.equal(C.counterClass(107, 120), '');
68+ assert.equal(C.counterClass(108, 120), 'warn');
69+ assert.equal(C.counterClass(120, 120), 'warn');
70+ assert.equal(C.counterClass(121, 120), 'over');
71+ assert.equal(C.counterClass(9000, 10000), 'warn');
72+ assert.equal(C.counterClass(8999, 10000), '');
73+ assert.equal(C.counterClass(41, 40), 'over');
74+});
75+
76+test('length counts characters, not UTF-16 units', () => {
77+ assert.equal(C.length('abc'), 3);
78+ assert.equal(C.length('a😀b'), 3);
79+ assert.equal(C.length(''), 0);
80+});
81+
82+test('state: the button opens only when solved, waited and not over', () => {
83+ const base = { solved: false, now: 1000, minAt: 5000, sending: false, unsupported: false, failed: false, overs: false };
84+ const s = (o) => C.state({ ...base, ...o });
85+ assert.deepEqual(s({}), { disabled: true, status: 'Checking your browser… (this takes a few seconds)' });
86+ assert.equal(s({ solved: true }).disabled, true, 'solved too early');
87+ assert.equal(s({ solved: true, now: 4999 }).disabled, true);
88+ assert.equal(s({ now: 6000 }).disabled, true, 'waited but not solved');
89+ assert.equal(s({ solved: true, now: 5000 }).disabled, false);
90+ assert.equal(s({ solved: true, now: 6000, overs: true }).disabled, true, 'a field is too long');
91+ assert.equal(s({ solved: true, now: 6000, sending: true }).disabled, true, 'no double submit');
92+ assert.equal(s({ solved: true, now: 6000, sending: true }).status, 'Sending…');
93+ assert.equal(s({ unsupported: true }).status, 'Your browser cannot run the check.');
94+ assert.equal(s({ solved: true, now: 6000, unsupported: true }).disabled, true);
95+ assert.equal(s({ failed: true, solved: true, now: 6000 }).disabled, true);
96+});
M internal/adapters/web/shell.templ +3 -0
@@ -33,6 +33,9 @@ templ Shell(l Layout, rail templ.Component) {
3333 if l.MR {
3434 <script src={ l.Assets.MR } defer></script>
3535 }
36+ if l.IssueForm {
37+ <script src={ l.Assets.IssueForm } defer></script>
38+ }
3639 </head>
3740 <body
3841 data-ref={ l.RefPath }
@@ -33,6 +33,9 @@ templ Shell(l Layout, rail templ.Component) {
33 if l.MR {33 if l.MR {
34 <script src={ l.Assets.MR } defer></script>34 <script src={ l.Assets.MR } defer></script>
35 }35 }
36+ if l.IssueForm {
37+ <script src={ l.Assets.IssueForm } defer></script>
38+ }
36 </head>39 </head>
37 <body40 <body
38 data-ref={ l.RefPath }41 data-ref={ l.RefPath }
M internal/adapters/web/shell_templ.go +180 -161
@@ -239,69 +239,88 @@ func Shell(l Layout, rail templ.Component) templ.Component {
239239 return templ_7745c5c3_Err
240240 }
241241 }
242- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "</head><body data-ref=\"")
242+ if l.IssueForm {
243+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "<script src=\"")
244+ if templ_7745c5c3_Err != nil {
245+ return templ_7745c5c3_Err
246+ }
247+ var templ_7745c5c3_Var17 string
248+ templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.ResolveAttributeValue(l.Assets.IssueForm)
249+ if templ_7745c5c3_Err != nil {
250+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 37, Col: 36}
251+ }
252+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var17)
253+ if templ_7745c5c3_Err != nil {
254+ return templ_7745c5c3_Err
255+ }
256+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "\" defer></script>")
257+ if templ_7745c5c3_Err != nil {
258+ return templ_7745c5c3_Err
259+ }
260+ }
261+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "</head><body data-ref=\"")
243262 if templ_7745c5c3_Err != nil {
244263 return templ_7745c5c3_Err
245264 }
246- var templ_7745c5c3_Var17 string
247- templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.ResolveAttributeValue(l.RefPath)
265+ var templ_7745c5c3_Var18 string
266+ templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.ResolveAttributeValue(l.RefPath)
248267 if templ_7745c5c3_Err != nil {
249- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 38, Col: 23}
268+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 41, Col: 23}
250269 }
251- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var17)
270+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var18)
252271 if templ_7745c5c3_Err != nil {
253272 return templ_7745c5c3_Err
254273 }
255- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "\" data-sha=\"")
274+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "\" data-sha=\"")
256275 if templ_7745c5c3_Err != nil {
257276 return templ_7745c5c3_Err
258277 }
259- var templ_7745c5c3_Var18 string
260- templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.ResolveAttributeValue(l.SHA)
278+ var templ_7745c5c3_Var19 string
279+ templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.ResolveAttributeValue(l.SHA)
261280 if templ_7745c5c3_Err != nil {
262- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 39, Col: 19}
281+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 42, Col: 19}
263282 }
264- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var18)
283+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var19)
265284 if templ_7745c5c3_Err != nil {
266285 return templ_7745c5c3_Err
267286 }
268- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "\" data-sel=\"\" data-nav=\"\"")
287+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "\" data-sel=\"\" data-nav=\"\"")
269288 if templ_7745c5c3_Err != nil {
270289 return templ_7745c5c3_Err
271290 }
272291 if l.PathsURL != "" {
273- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, " data-paths=\"")
292+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, " data-paths=\"")
274293 if templ_7745c5c3_Err != nil {
275294 return templ_7745c5c3_Err
276295 }
277- var templ_7745c5c3_Var19 string
278- templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.ResolveAttributeValue(l.PathsURL)
296+ var templ_7745c5c3_Var20 string
297+ templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.ResolveAttributeValue(l.PathsURL)
279298 if templ_7745c5c3_Err != nil {
280- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 43, Col: 27}
299+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 46, Col: 27}
281300 }
282- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var19)
301+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var20)
283302 if templ_7745c5c3_Err != nil {
284303 return templ_7745c5c3_Err
285304 }
286- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "\" data-blob=\"")
305+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "\" data-blob=\"")
287306 if templ_7745c5c3_Err != nil {
288307 return templ_7745c5c3_Err
289308 }
290- var templ_7745c5c3_Var20 string
291- templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.ResolveAttributeValue(l.BlobBase)
309+ var templ_7745c5c3_Var21 string
310+ templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.ResolveAttributeValue(l.BlobBase)
292311 if templ_7745c5c3_Err != nil {
293- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 44, Col: 26}
312+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 47, Col: 26}
294313 }
295- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var20)
314+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var21)
296315 if templ_7745c5c3_Err != nil {
297316 return templ_7745c5c3_Err
298317 }
299- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "\"")
318+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "\"")
300319 if templ_7745c5c3_Err != nil {
301320 return templ_7745c5c3_Err
302321 }
303322 }
304- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "><a class=\"skip\" href=\"#main\">skip to content</a>")
323+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "><a class=\"skip\" href=\"#main\">skip to content</a>")
305324 if templ_7745c5c3_Err != nil {
306325 return templ_7745c5c3_Err
307326 }
@@ -309,48 +328,48 @@ func Shell(l Layout, rail templ.Component) templ.Component {
309328 if templ_7745c5c3_Err != nil {
310329 return templ_7745c5c3_Err
311330 }
312- var templ_7745c5c3_Var21 = []any{"work", templ.KV("has-rail", l.HasRail && rail != nil)}
313- templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var21...)
331+ var templ_7745c5c3_Var22 = []any{"work", templ.KV("has-rail", l.HasRail && rail != nil)}
332+ templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var22...)
314333 if templ_7745c5c3_Err != nil {
315334 return templ_7745c5c3_Err
316335 }
317- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "<div class=\"")
336+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "<div class=\"")
318337 if templ_7745c5c3_Err != nil {
319338 return templ_7745c5c3_Err
320339 }
321- var templ_7745c5c3_Var22 string
322- templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var21).String())
340+ var templ_7745c5c3_Var23 string
341+ templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var22).String())
323342 if templ_7745c5c3_Err != nil {
324343 return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 1, Col: 0}
325344 }
326- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var22)
345+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var23)
327346 if templ_7745c5c3_Err != nil {
328347 return templ_7745c5c3_Err
329348 }
330- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "\">")
349+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "\">")
331350 if templ_7745c5c3_Err != nil {
332351 return templ_7745c5c3_Err
333352 }
334353 if l.Lede != "" {
335- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "<section class=\"work-lede\" aria-label=\"Description\"><p class=\"lede\">")
354+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "<section class=\"work-lede\" aria-label=\"Description\"><p class=\"lede\">")
336355 if templ_7745c5c3_Err != nil {
337356 return templ_7745c5c3_Err
338357 }
339- var templ_7745c5c3_Var23 string
340- templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinStringErrs(l.Lede)
358+ var templ_7745c5c3_Var24 string
359+ templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinStringErrs(l.Lede)
341360 if templ_7745c5c3_Err != nil {
342- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 51, Col: 81}
361+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 54, Col: 81}
343362 }
344- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23))
363+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24))
345364 if templ_7745c5c3_Err != nil {
346365 return templ_7745c5c3_Err
347366 }
348- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "</p></section>")
367+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "</p></section>")
349368 if templ_7745c5c3_Err != nil {
350369 return templ_7745c5c3_Err
351370 }
352371 }
353- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "<main id=\"main\" tabindex=\"-1\">")
372+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "<main id=\"main\" tabindex=\"-1\">")
354373 if templ_7745c5c3_Err != nil {
355374 return templ_7745c5c3_Err
356375 }
@@ -358,12 +377,12 @@ func Shell(l Layout, rail templ.Component) templ.Component {
358377 if templ_7745c5c3_Err != nil {
359378 return templ_7745c5c3_Err
360379 }
361- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "</main>")
380+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "</main>")
362381 if templ_7745c5c3_Err != nil {
363382 return templ_7745c5c3_Err
364383 }
365384 if l.HasRail && rail != nil {
366- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "<aside class=\"rail\" aria-label=\"Context\">")
385+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "<aside class=\"rail\" aria-label=\"Context\">")
367386 if templ_7745c5c3_Err != nil {
368387 return templ_7745c5c3_Err
369388 }
@@ -371,12 +390,12 @@ func Shell(l Layout, rail templ.Component) templ.Component {
371390 if templ_7745c5c3_Err != nil {
372391 return templ_7745c5c3_Err
373392 }
374- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "</aside>")
393+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "</aside>")
375394 if templ_7745c5c3_Err != nil {
376395 return templ_7745c5c3_Err
377396 }
378397 }
379- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "</div>")
398+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "</div>")
380399 if templ_7745c5c3_Err != nil {
381400 return templ_7745c5c3_Err
382401 }
@@ -404,7 +423,7 @@ func Shell(l Layout, rail templ.Component) templ.Component {
404423 if templ_7745c5c3_Err != nil {
405424 return templ_7745c5c3_Err
406425 }
407- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "</body></html>")
426+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "</body></html>")
408427 if templ_7745c5c3_Err != nil {
409428 return templ_7745c5c3_Err
410429 }
@@ -430,324 +449,324 @@ func Header(l Layout) templ.Component {
430449 }()
431450 }
432451 ctx = templ.InitializeContext(ctx)
433- templ_7745c5c3_Var24 := templ.GetChildren(ctx)
434- if templ_7745c5c3_Var24 == nil {
435- templ_7745c5c3_Var24 = templ.NopComponent
452+ templ_7745c5c3_Var25 := templ.GetChildren(ctx)
453+ if templ_7745c5c3_Var25 == nil {
454+ templ_7745c5c3_Var25 = templ.NopComponent
436455 }
437456 ctx = templ.ClearChildren(ctx)
438- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "<header class=\"hdr\"><div class=\"hdr-top\"><a class=\"brand\" href=\"/\" data-g=\"h\" aria-label=\"home\" title=\"home (g h)\"><span class=\"brand-mark\" aria-hidden=\"true\"></span></a> ")
457+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "<header class=\"hdr\"><div class=\"hdr-top\"><a class=\"brand\" href=\"/\" data-g=\"h\" aria-label=\"home\" title=\"home (g h)\"><span class=\"brand-mark\" aria-hidden=\"true\"></span></a> ")
439458 if templ_7745c5c3_Err != nil {
440459 return templ_7745c5c3_Err
441460 }
442461 if len(l.Crumbs) > 0 {
443- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "<nav class=\"crumbs\" aria-label=\"Path\"><ol>")
462+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "<nav class=\"crumbs\" aria-label=\"Path\"><ol>")
444463 if templ_7745c5c3_Err != nil {
445464 return templ_7745c5c3_Err
446465 }
447466 for _, c := range l.Crumbs {
448- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "<li>")
467+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "<li>")
449468 if templ_7745c5c3_Err != nil {
450469 return templ_7745c5c3_Err
451470 }
452471 if c.Href != "" {
453- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "<a href=\"")
472+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "<a href=\"")
454473 if templ_7745c5c3_Err != nil {
455474 return templ_7745c5c3_Err
456475 }
457- var templ_7745c5c3_Var25 templ.SafeURL
458- templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(c.Href))
476+ var templ_7745c5c3_Var26 templ.SafeURL
477+ templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(c.Href))
459478 if templ_7745c5c3_Err != nil {
460- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 87, Col: 38}
479+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 90, Col: 38}
461480 }
462- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var25))
481+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var26))
463482 if templ_7745c5c3_Err != nil {
464483 return templ_7745c5c3_Err
465484 }
466- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "\"")
485+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "\"")
467486 if templ_7745c5c3_Err != nil {
468487 return templ_7745c5c3_Err
469488 }
470489 if c.Current {
471- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, " aria-current=\"page\"")
490+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, " aria-current=\"page\"")
472491 if templ_7745c5c3_Err != nil {
473492 return templ_7745c5c3_Err
474493 }
475494 }
476- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, ">")
495+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, ">")
477496 if templ_7745c5c3_Err != nil {
478497 return templ_7745c5c3_Err
479498 }
480- var templ_7745c5c3_Var26 string
481- templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.JoinStringErrs(c.Label)
499+ var templ_7745c5c3_Var27 string
500+ templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.JoinStringErrs(c.Label)
482501 if templ_7745c5c3_Err != nil {
483- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 91, Col: 19}
502+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 94, Col: 19}
484503 }
485- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var26))
504+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var27))
486505 if templ_7745c5c3_Err != nil {
487506 return templ_7745c5c3_Err
488507 }
489- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "</a>")
508+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "</a>")
490509 if templ_7745c5c3_Err != nil {
491510 return templ_7745c5c3_Err
492511 }
493512 } else {
494- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "<span")
513+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "<span")
495514 if templ_7745c5c3_Err != nil {
496515 return templ_7745c5c3_Err
497516 }
498517 if c.Current {
499- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, " aria-current=\"page\"")
518+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, " aria-current=\"page\"")
500519 if templ_7745c5c3_Err != nil {
501520 return templ_7745c5c3_Err
502521 }
503522 }
504- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, ">")
523+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, ">")
505524 if templ_7745c5c3_Err != nil {
506525 return templ_7745c5c3_Err
507526 }
508- var templ_7745c5c3_Var27 string
509- templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.JoinStringErrs(c.Label)
527+ var templ_7745c5c3_Var28 string
528+ templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.JoinStringErrs(c.Label)
510529 if templ_7745c5c3_Err != nil {
511- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 97, Col: 19}
530+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 100, Col: 19}
512531 }
513- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var27))
532+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var28))
514533 if templ_7745c5c3_Err != nil {
515534 return templ_7745c5c3_Err
516535 }
517- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "</span>")
536+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, "</span>")
518537 if templ_7745c5c3_Err != nil {
519538 return templ_7745c5c3_Err
520539 }
521540 }
522- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "</li>")
541+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "</li>")
523542 if templ_7745c5c3_Err != nil {
524543 return templ_7745c5c3_Err
525544 }
526545 }
527- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, "</ol></nav>")
546+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, "</ol></nav>")
528547 if templ_7745c5c3_Err != nil {
529548 return templ_7745c5c3_Err
530549 }
531550 }
532- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "<div class=\"hdr-acts\">")
551+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, "<div class=\"hdr-acts\">")
533552 if templ_7745c5c3_Err != nil {
534553 return templ_7745c5c3_Err
535554 }
536555 if l.Ref != "" {
537- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, "<a class=\"btn ref\" href=\"")
556+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, "<a class=\"btn ref\" href=\"")
538557 if templ_7745c5c3_Err != nil {
539558 return templ_7745c5c3_Err
540559 }
541- var templ_7745c5c3_Var28 templ.SafeURL
542- templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(repoHref(l.Repo, "refs")))
560+ var templ_7745c5c3_Var29 templ.SafeURL
561+ templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(repoHref(l.Repo, "refs")))
543562 if templ_7745c5c3_Err != nil {
544- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 108, Col: 52}
563+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 111, Col: 52}
545564 }
546- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var28))
565+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var29))
547566 if templ_7745c5c3_Err != nil {
548567 return templ_7745c5c3_Err
549568 }
550- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, "\" data-fire=\"refs\" aria-keyshortcuts=\"b\" title=\"switch branch or tag (b)\"><span class=\"ico i-branch\" aria-hidden=\"true\"></span> <span class=\"ref-name\">")
569+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, "\" data-fire=\"refs\" aria-keyshortcuts=\"b\" title=\"switch branch or tag (b)\"><span class=\"ico i-branch\" aria-hidden=\"true\"></span> <span class=\"ref-name\">")
551570 if templ_7745c5c3_Err != nil {
552571 return templ_7745c5c3_Err
553572 }
554- var templ_7745c5c3_Var29 string
555- templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.JoinStringErrs(l.Ref)
573+ var templ_7745c5c3_Var30 string
574+ templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinStringErrs(l.Ref)
556575 if templ_7745c5c3_Err != nil {
557- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 114, Col: 36}
576+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 117, Col: 36}
558577 }
559- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var29))
578+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var30))
560579 if templ_7745c5c3_Err != nil {
561580 return templ_7745c5c3_Err
562581 }
563- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, "</span> <span class=\"sha\">")
582+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, "</span> <span class=\"sha\">")
564583 if templ_7745c5c3_Err != nil {
565584 return templ_7745c5c3_Err
566585 }
567- var templ_7745c5c3_Var30 string
568- templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinStringErrs(l.Short)
586+ var templ_7745c5c3_Var31 string
587+ templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.JoinStringErrs(l.Short)
569588 if templ_7745c5c3_Err != nil {
570- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 115, Col: 33}
589+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 118, Col: 33}
571590 }
572- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var30))
591+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var31))
573592 if templ_7745c5c3_Err != nil {
574593 return templ_7745c5c3_Err
575594 }
576- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, "</span></a> ")
595+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, "</span></a> ")
577596 if templ_7745c5c3_Err != nil {
578597 return templ_7745c5c3_Err
579598 }
580599 }
581- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, "<button type=\"button\" class=\"btn search\" data-js aria-haspopup=\"dialog\" aria-controls=\"panel-cmd\" aria-keyshortcuts=\"Control+K :\" title=\"go to a section, ref, file or repo (ctrl k or :)\" data-fire=\"cmd\"><span class=\"ico i-search\" aria-hidden=\"true\"></span> <span class=\"search-label\">Go to…</span> <kbd aria-hidden=\"true\">ctrl k</kbd></button> ")
600+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 56, "<button type=\"button\" class=\"btn search\" data-js aria-haspopup=\"dialog\" aria-controls=\"panel-cmd\" aria-keyshortcuts=\"Control+K :\" title=\"go to a section, ref, file or repo (ctrl k or :)\" data-fire=\"cmd\"><span class=\"ico i-search\" aria-hidden=\"true\"></span> <span class=\"search-label\">Go to…</span> <kbd aria-hidden=\"true\">ctrl k</kbd></button> ")
582601 if templ_7745c5c3_Err != nil {
583602 return templ_7745c5c3_Err
584603 }
585604 if SignedIn(ctx) {
586- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, "<form class=\"owner\" method=\"post\" action=\"/-/logout\"><span class=\"owner-badge\" title=\"signed in as the owner\"><span class=\"ico i-user\" aria-hidden=\"true\"></span>")
605+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, "<form class=\"owner\" method=\"post\" action=\"/-/logout\"><span class=\"owner-badge\" title=\"signed in as the owner\"><span class=\"ico i-user\" aria-hidden=\"true\"></span>")
587606 if templ_7745c5c3_Err != nil {
588607 return templ_7745c5c3_Err
589608 }
590- var templ_7745c5c3_Var31 string
591- templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.JoinStringErrs(l.Owner)
609+ var templ_7745c5c3_Var32 string
610+ templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.JoinStringErrs(l.Owner)
592611 if templ_7745c5c3_Err != nil {
593- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 134, Col: 123}
612+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 137, Col: 123}
594613 }
595- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var31))
614+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var32))
596615 if templ_7745c5c3_Err != nil {
597616 return templ_7745c5c3_Err
598617 }
599- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 56, "</span> ")
618+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "</span> ")
600619 if templ_7745c5c3_Err != nil {
601620 return templ_7745c5c3_Err
602621 }
603622 if l.Passkeys {
604- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, "<a class=\"btn icon\" href=\"/-/passkeys\" aria-label=\"Passkeys\" title=\"passkeys: add or remove the passkeys that sign you in\"><span class=\"ico i-key\" aria-hidden=\"true\"></span></a> ")
623+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, "<a class=\"btn icon\" href=\"/-/passkeys\" aria-label=\"Passkeys\" title=\"passkeys: add or remove the passkeys that sign you in\"><span class=\"ico i-key\" aria-hidden=\"true\"></span></a> ")
605624 if templ_7745c5c3_Err != nil {
606625 return templ_7745c5c3_Err
607626 }
608627 }
609- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "<button type=\"submit\" class=\"btn icon\" aria-label=\"Sign out\" title=\"sign out of this browser\"><span class=\"ico i-log-out\" aria-hidden=\"true\"></span></button></form>")
628+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 60, "<button type=\"submit\" class=\"btn icon\" aria-label=\"Sign out\" title=\"sign out of this browser\"><span class=\"ico i-log-out\" aria-hidden=\"true\"></span></button></form>")
610629 if templ_7745c5c3_Err != nil {
611630 return templ_7745c5c3_Err
612631 }
613632 } else if l.Passkeys {
614- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, "<a class=\"btn icon\" href=\"/-/login\" aria-label=\"Sign in\" title=\"sign in with a passkey\"><span class=\"ico i-log-in\" aria-hidden=\"true\"></span></a>")
633+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 61, "<a class=\"btn icon\" href=\"/-/login\" aria-label=\"Sign in\" title=\"sign in with a passkey\"><span class=\"ico i-log-in\" aria-hidden=\"true\"></span></a>")
615634 if templ_7745c5c3_Err != nil {
616635 return templ_7745c5c3_Err
617636 }
618637 }
619- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 60, "</div></div>")
638+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 62, "</div></div>")
620639 if templ_7745c5c3_Err != nil {
621640 return templ_7745c5c3_Err
622641 }
623642 if len(l.Sections) > 0 {
624- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 61, "<nav class=\"tabs\" aria-label=\"Sections\"><ul>")
643+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 63, "<nav class=\"tabs\" aria-label=\"Sections\"><ul>")
625644 if templ_7745c5c3_Err != nil {
626645 return templ_7745c5c3_Err
627646 }
628647 for _, s := range l.Sections {
629- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 62, "<li><a href=\"")
648+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 64, "<li><a href=\"")
630649 if templ_7745c5c3_Err != nil {
631650 return templ_7745c5c3_Err
632651 }
633- var templ_7745c5c3_Var32 templ.SafeURL
634- templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(s.Href))
652+ var templ_7745c5c3_Var33 templ.SafeURL
653+ templ_7745c5c3_Var33, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(s.Href))
635654 if templ_7745c5c3_Err != nil {
636- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 151, Col: 36}
655+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 154, Col: 36}
637656 }
638- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var32))
657+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var33))
639658 if templ_7745c5c3_Err != nil {
640659 return templ_7745c5c3_Err
641660 }
642- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 63, "\" data-key=\"")
661+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 65, "\" data-key=\"")
643662 if templ_7745c5c3_Err != nil {
644663 return templ_7745c5c3_Err
645664 }
646- var templ_7745c5c3_Var33 string
647- templ_7745c5c3_Var33, templ_7745c5c3_Err = templ.ResolveAttributeValue(s.Key)
665+ var templ_7745c5c3_Var34 string
666+ templ_7745c5c3_Var34, templ_7745c5c3_Err = templ.ResolveAttributeValue(s.Key)
648667 if templ_7745c5c3_Err != nil {
649- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 152, Col: 24}
668+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 155, Col: 24}
650669 }
651- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var33)
670+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var34)
652671 if templ_7745c5c3_Err != nil {
653672 return templ_7745c5c3_Err
654673 }
655- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 64, "\" aria-keyshortcuts=\"")
674+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 66, "\" aria-keyshortcuts=\"")
656675 if templ_7745c5c3_Err != nil {
657676 return templ_7745c5c3_Err
658677 }
659- var templ_7745c5c3_Var34 string
660- templ_7745c5c3_Var34, templ_7745c5c3_Err = templ.ResolveAttributeValue(s.Key)
678+ var templ_7745c5c3_Var35 string
679+ templ_7745c5c3_Var35, templ_7745c5c3_Err = templ.ResolveAttributeValue(s.Key)
661680 if templ_7745c5c3_Err != nil {
662- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 153, Col: 33}
681+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 156, Col: 33}
663682 }
664- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var34)
683+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var35)
665684 if templ_7745c5c3_Err != nil {
666685 return templ_7745c5c3_Err
667686 }
668- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 65, "\"")
687+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 67, "\"")
669688 if templ_7745c5c3_Err != nil {
670689 return templ_7745c5c3_Err
671690 }
672691 if s.Current {
673- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 66, " aria-current=\"page\"")
692+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 68, " aria-current=\"page\"")
674693 if templ_7745c5c3_Err != nil {
675694 return templ_7745c5c3_Err
676695 }
677696 }
678- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 67, ">")
697+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 69, ">")
679698 if templ_7745c5c3_Err != nil {
680699 return templ_7745c5c3_Err
681700 }
682- var templ_7745c5c3_Var35 string
683- templ_7745c5c3_Var35, templ_7745c5c3_Err = templ.JoinStringErrs(s.Label)
701+ var templ_7745c5c3_Var36 string
702+ templ_7745c5c3_Var36, templ_7745c5c3_Err = templ.JoinStringErrs(s.Label)
684703 if templ_7745c5c3_Err != nil {
685- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 158, Col: 17}
704+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 161, Col: 17}
686705 }
687- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var35))
706+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var36))
688707 if templ_7745c5c3_Err != nil {
689708 return templ_7745c5c3_Err
690709 }
691- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 68, " ")
710+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 70, " ")
692711 if templ_7745c5c3_Err != nil {
693712 return templ_7745c5c3_Err
694713 }
695714 if s.Count != "" {
696- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 69, "<span class=\"count\">")
715+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 71, "<span class=\"count\">")
697716 if templ_7745c5c3_Err != nil {
698717 return templ_7745c5c3_Err
699718 }
700- var templ_7745c5c3_Var36 string
701- templ_7745c5c3_Var36, templ_7745c5c3_Err = templ.JoinStringErrs(s.Count)
719+ var templ_7745c5c3_Var37 string
720+ templ_7745c5c3_Var37, templ_7745c5c3_Err = templ.JoinStringErrs(s.Count)
702721 if templ_7745c5c3_Err != nil {
703- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 160, Col: 38}
722+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 163, Col: 38}
704723 }
705- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var36))
724+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var37))
706725 if templ_7745c5c3_Err != nil {
707726 return templ_7745c5c3_Err
708727 }
709- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 70, "<span class=\"sr\">")
728+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 72, "<span class=\"sr\">")
710729 if templ_7745c5c3_Err != nil {
711730 return templ_7745c5c3_Err
712731 }
713- var templ_7745c5c3_Var37 string
714- templ_7745c5c3_Var37, templ_7745c5c3_Err = templ.JoinStringErrs(" open")
732+ var templ_7745c5c3_Var38 string
733+ templ_7745c5c3_Var38, templ_7745c5c3_Err = templ.JoinStringErrs(" open")
715734 if templ_7745c5c3_Err != nil {
716- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 160, Col: 66}
735+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 163, Col: 66}
717736 }
718- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var37))
737+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var38))
719738 if templ_7745c5c3_Err != nil {
720739 return templ_7745c5c3_Err
721740 }
722- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 71, "</span></span> ")
741+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 73, "</span></span> ")
723742 if templ_7745c5c3_Err != nil {
724743 return templ_7745c5c3_Err
725744 }
726745 }
727- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 72, "<kbd aria-hidden=\"true\">")
746+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 74, "<kbd aria-hidden=\"true\">")
728747 if templ_7745c5c3_Err != nil {
729748 return templ_7745c5c3_Err
730749 }
731- var templ_7745c5c3_Var38 string
732- templ_7745c5c3_Var38, templ_7745c5c3_Err = templ.JoinStringErrs(s.Key)
750+ var templ_7745c5c3_Var39 string
751+ templ_7745c5c3_Var39, templ_7745c5c3_Err = templ.JoinStringErrs(s.Key)
733752 if templ_7745c5c3_Err != nil {
734- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 162, Col: 39}
753+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 165, Col: 39}
735754 }
736- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var38))
755+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var39))
737756 if templ_7745c5c3_Err != nil {
738757 return templ_7745c5c3_Err
739758 }
740- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 73, "</kbd></a></li>")
759+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 75, "</kbd></a></li>")
741760 if templ_7745c5c3_Err != nil {
742761 return templ_7745c5c3_Err
743762 }
744763 }
745- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 74, "</ul></nav>")
764+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 76, "</ul></nav>")
746765 if templ_7745c5c3_Err != nil {
747766 return templ_7745c5c3_Err
748767 }
749768 }
750- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 75, "</header>")
769+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 77, "</header>")
751770 if templ_7745c5c3_Err != nil {
752771 return templ_7745c5c3_Err
753772 }
@@ -773,38 +792,38 @@ func Footer(l Layout) templ.Component {
773792 }()
774793 }
775794 ctx = templ.InitializeContext(ctx)
776- templ_7745c5c3_Var39 := templ.GetChildren(ctx)
777- if templ_7745c5c3_Var39 == nil {
778- templ_7745c5c3_Var39 = templ.NopComponent
795+ templ_7745c5c3_Var40 := templ.GetChildren(ctx)
796+ if templ_7745c5c3_Var40 == nil {
797+ templ_7745c5c3_Var40 = templ.NopComponent
779798 }
780799 ctx = templ.ClearChildren(ctx)
781- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 76, "<footer class=\"foot\"><span>ermite ")
800+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 78, "<footer class=\"foot\"><span>ermite ")
782801 if templ_7745c5c3_Err != nil {
783802 return templ_7745c5c3_Err
784803 }
785- var templ_7745c5c3_Var40 string
786- templ_7745c5c3_Var40, templ_7745c5c3_Err = templ.JoinStringErrs(l.Version)
804+ var templ_7745c5c3_Var41 string
805+ templ_7745c5c3_Var41, templ_7745c5c3_Err = templ.JoinStringErrs(l.Version)
787806 if templ_7745c5c3_Err != nil {
788- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 176, Col: 26}
807+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 179, Col: 26}
789808 }
790- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var40))
809+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var41))
791810 if templ_7745c5c3_Err != nil {
792811 return templ_7745c5c3_Err
793812 }
794- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 77, "</span> <a href=\"")
813+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 79, "</span> <a href=\"")
795814 if templ_7745c5c3_Err != nil {
796815 return templ_7745c5c3_Err
797816 }
798- var templ_7745c5c3_Var41 templ.SafeURL
799- templ_7745c5c3_Var41, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(l.SourceURL))
817+ var templ_7745c5c3_Var42 templ.SafeURL
818+ templ_7745c5c3_Var42, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(l.SourceURL))
800819 if templ_7745c5c3_Err != nil {
801- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 177, Col: 38}
820+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 180, Col: 38}
802821 }
803- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var41))
822+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var42))
804823 if templ_7745c5c3_Err != nil {
805824 return templ_7745c5c3_Err
806825 }
807- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 78, "\" rel=\"noopener\" title=\"source of this version (AGPL-3.0-or-later)\">source</a> <button type=\"button\" class=\"foot-keys\" data-js data-fire=\"help\" aria-haspopup=\"dialog\" aria-controls=\"panel-help\" aria-keyshortcuts=\"?\"><kbd aria-hidden=\"true\">?</kbd> keyboard shortcuts</button></footer>")
826+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 80, "\" rel=\"noopener\" title=\"source of this version (AGPL-3.0-or-later)\">source</a> <button type=\"button\" class=\"foot-keys\" data-js data-fire=\"help\" aria-haspopup=\"dialog\" aria-controls=\"panel-help\" aria-keyshortcuts=\"?\"><kbd aria-hidden=\"true\">?</kbd> keyboard shortcuts</button></footer>")
808827 if templ_7745c5c3_Err != nil {
809828 return templ_7745c5c3_Err
810829 }
@@ -832,12 +851,12 @@ func Hint(l Layout) templ.Component {
832851 }()
833852 }
834853 ctx = templ.InitializeContext(ctx)
835- templ_7745c5c3_Var42 := templ.GetChildren(ctx)
836- if templ_7745c5c3_Var42 == nil {
837- templ_7745c5c3_Var42 = templ.NopComponent
854+ templ_7745c5c3_Var43 := templ.GetChildren(ctx)
855+ if templ_7745c5c3_Var43 == nil {
856+ templ_7745c5c3_Var43 = templ.NopComponent
838857 }
839858 ctx = templ.ClearChildren(ctx)
840- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 79, "<div class=\"hint\" role=\"status\" aria-live=\"polite\"><span class=\"hint-nav\" :text=\"document.body.dataset.nav || ''\"></span> <span class=\"hint-sel\" :text=\"document.body.dataset.sel ? document.body.dataset.sel + ' · y permalink' : ''\"></span> <span class=\"hint-flash\" :text=\"document.body.dataset.flash || ''\"></span>")
859+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 81, "<div class=\"hint\" role=\"status\" aria-live=\"polite\"><span class=\"hint-nav\" :text=\"document.body.dataset.nav || ''\"></span> <span class=\"hint-sel\" :text=\"document.body.dataset.sel ? document.body.dataset.sel + ' · y permalink' : ''\"></span> <span class=\"hint-flash\" :text=\"document.body.dataset.flash || ''\"></span>")
841860 if templ_7745c5c3_Err != nil {
842861 return templ_7745c5c3_Err
843862 }
@@ -845,7 +864,7 @@ func Hint(l Layout) templ.Component {
845864 if templ_7745c5c3_Err != nil {
846865 return templ_7745c5c3_Err
847866 }
848- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 80, "</div>")
867+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 82, "</div>")
849868 if templ_7745c5c3_Err != nil {
850869 return templ_7745c5c3_Err
851870 }
@@ -239,69 +239,88 @@ func Shell(l Layout, rail templ.Component) templ.Component {
239 return templ_7745c5c3_Err239 return templ_7745c5c3_Err
240 }240 }
241 }241 }
242- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "</head><body data-ref=\"")242+ if l.IssueForm {
243+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "<script src=\"")
244+ if templ_7745c5c3_Err != nil {
245+ return templ_7745c5c3_Err
246+ }
247+ var templ_7745c5c3_Var17 string
248+ templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.ResolveAttributeValue(l.Assets.IssueForm)
249+ if templ_7745c5c3_Err != nil {
250+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 37, Col: 36}
251+ }
252+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var17)
253+ if templ_7745c5c3_Err != nil {
254+ return templ_7745c5c3_Err
255+ }
256+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "\" defer></script>")
257+ if templ_7745c5c3_Err != nil {
258+ return templ_7745c5c3_Err
259+ }
260+ }
261+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "</head><body data-ref=\"")
243 if templ_7745c5c3_Err != nil {262 if templ_7745c5c3_Err != nil {
244 return templ_7745c5c3_Err263 return templ_7745c5c3_Err
245 }264 }
246- var templ_7745c5c3_Var17 string265+ var templ_7745c5c3_Var18 string
247- templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.ResolveAttributeValue(l.RefPath)266+ templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.ResolveAttributeValue(l.RefPath)
248 if templ_7745c5c3_Err != nil {267 if templ_7745c5c3_Err != nil {
249- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 38, Col: 23}268+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 41, Col: 23}
250 }269 }
251- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var17)270+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var18)
252 if templ_7745c5c3_Err != nil {271 if templ_7745c5c3_Err != nil {
253 return templ_7745c5c3_Err272 return templ_7745c5c3_Err
254 }273 }
255- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "\" data-sha=\"")274+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "\" data-sha=\"")
256 if templ_7745c5c3_Err != nil {275 if templ_7745c5c3_Err != nil {
257 return templ_7745c5c3_Err276 return templ_7745c5c3_Err
258 }277 }
259- var templ_7745c5c3_Var18 string278+ var templ_7745c5c3_Var19 string
260- templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.ResolveAttributeValue(l.SHA)279+ templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.ResolveAttributeValue(l.SHA)
261 if templ_7745c5c3_Err != nil {280 if templ_7745c5c3_Err != nil {
262- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 39, Col: 19}281+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 42, Col: 19}
263 }282 }
264- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var18)283+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var19)
265 if templ_7745c5c3_Err != nil {284 if templ_7745c5c3_Err != nil {
266 return templ_7745c5c3_Err285 return templ_7745c5c3_Err
267 }286 }
268- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "\" data-sel=\"\" data-nav=\"\"")287+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "\" data-sel=\"\" data-nav=\"\"")
269 if templ_7745c5c3_Err != nil {288 if templ_7745c5c3_Err != nil {
270 return templ_7745c5c3_Err289 return templ_7745c5c3_Err
271 }290 }
272 if l.PathsURL != "" {291 if l.PathsURL != "" {
273- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, " data-paths=\"")292+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, " data-paths=\"")
274 if templ_7745c5c3_Err != nil {293 if templ_7745c5c3_Err != nil {
275 return templ_7745c5c3_Err294 return templ_7745c5c3_Err
276 }295 }
277- var templ_7745c5c3_Var19 string296+ var templ_7745c5c3_Var20 string
278- templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.ResolveAttributeValue(l.PathsURL)297+ templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.ResolveAttributeValue(l.PathsURL)
279 if templ_7745c5c3_Err != nil {298 if templ_7745c5c3_Err != nil {
280- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 43, Col: 27}299+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 46, Col: 27}
281 }300 }
282- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var19)301+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var20)
283 if templ_7745c5c3_Err != nil {302 if templ_7745c5c3_Err != nil {
284 return templ_7745c5c3_Err303 return templ_7745c5c3_Err
285 }304 }
286- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "\" data-blob=\"")305+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "\" data-blob=\"")
287 if templ_7745c5c3_Err != nil {306 if templ_7745c5c3_Err != nil {
288 return templ_7745c5c3_Err307 return templ_7745c5c3_Err
289 }308 }
290- var templ_7745c5c3_Var20 string309+ var templ_7745c5c3_Var21 string
291- templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.ResolveAttributeValue(l.BlobBase)310+ templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.ResolveAttributeValue(l.BlobBase)
292 if templ_7745c5c3_Err != nil {311 if templ_7745c5c3_Err != nil {
293- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 44, Col: 26}312+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 47, Col: 26}
294 }313 }
295- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var20)314+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var21)
296 if templ_7745c5c3_Err != nil {315 if templ_7745c5c3_Err != nil {
297 return templ_7745c5c3_Err316 return templ_7745c5c3_Err
298 }317 }
299- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "\"")318+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "\"")
300 if templ_7745c5c3_Err != nil {319 if templ_7745c5c3_Err != nil {
301 return templ_7745c5c3_Err320 return templ_7745c5c3_Err
302 }321 }
303 }322 }
304- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "><a class=\"skip\" href=\"#main\">skip to content</a>")323+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "><a class=\"skip\" href=\"#main\">skip to content</a>")
305 if templ_7745c5c3_Err != nil {324 if templ_7745c5c3_Err != nil {
306 return templ_7745c5c3_Err325 return templ_7745c5c3_Err
307 }326 }
@@ -309,48 +328,48 @@ func Shell(l Layout, rail templ.Component) templ.Component {
309 if templ_7745c5c3_Err != nil {328 if templ_7745c5c3_Err != nil {
310 return templ_7745c5c3_Err329 return templ_7745c5c3_Err
311 }330 }
312- var templ_7745c5c3_Var21 = []any{"work", templ.KV("has-rail", l.HasRail && rail != nil)}331+ var templ_7745c5c3_Var22 = []any{"work", templ.KV("has-rail", l.HasRail && rail != nil)}
313- templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var21...)332+ templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var22...)
314 if templ_7745c5c3_Err != nil {333 if templ_7745c5c3_Err != nil {
315 return templ_7745c5c3_Err334 return templ_7745c5c3_Err
316 }335 }
317- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "<div class=\"")336+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "<div class=\"")
318 if templ_7745c5c3_Err != nil {337 if templ_7745c5c3_Err != nil {
319 return templ_7745c5c3_Err338 return templ_7745c5c3_Err
320 }339 }
321- var templ_7745c5c3_Var22 string340+ var templ_7745c5c3_Var23 string
322- templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var21).String())341+ templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var22).String())
323 if templ_7745c5c3_Err != nil {342 if templ_7745c5c3_Err != nil {
324 return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 1, Col: 0}343 return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 1, Col: 0}
325 }344 }
326- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var22)345+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var23)
327 if templ_7745c5c3_Err != nil {346 if templ_7745c5c3_Err != nil {
328 return templ_7745c5c3_Err347 return templ_7745c5c3_Err
329 }348 }
330- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "\">")349+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "\">")
331 if templ_7745c5c3_Err != nil {350 if templ_7745c5c3_Err != nil {
332 return templ_7745c5c3_Err351 return templ_7745c5c3_Err
333 }352 }
334 if l.Lede != "" {353 if l.Lede != "" {
335- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "<section class=\"work-lede\" aria-label=\"Description\"><p class=\"lede\">")354+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "<section class=\"work-lede\" aria-label=\"Description\"><p class=\"lede\">")
336 if templ_7745c5c3_Err != nil {355 if templ_7745c5c3_Err != nil {
337 return templ_7745c5c3_Err356 return templ_7745c5c3_Err
338 }357 }
339- var templ_7745c5c3_Var23 string358+ var templ_7745c5c3_Var24 string
340- templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinStringErrs(l.Lede)359+ templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinStringErrs(l.Lede)
341 if templ_7745c5c3_Err != nil {360 if templ_7745c5c3_Err != nil {
342- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 51, Col: 81}361+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 54, Col: 81}
343 }362 }
344- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23))363+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24))
345 if templ_7745c5c3_Err != nil {364 if templ_7745c5c3_Err != nil {
346 return templ_7745c5c3_Err365 return templ_7745c5c3_Err
347 }366 }
348- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "</p></section>")367+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "</p></section>")
349 if templ_7745c5c3_Err != nil {368 if templ_7745c5c3_Err != nil {
350 return templ_7745c5c3_Err369 return templ_7745c5c3_Err
351 }370 }
352 }371 }
353- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "<main id=\"main\" tabindex=\"-1\">")372+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "<main id=\"main\" tabindex=\"-1\">")
354 if templ_7745c5c3_Err != nil {373 if templ_7745c5c3_Err != nil {
355 return templ_7745c5c3_Err374 return templ_7745c5c3_Err
356 }375 }
@@ -358,12 +377,12 @@ func Shell(l Layout, rail templ.Component) templ.Component {
358 if templ_7745c5c3_Err != nil {377 if templ_7745c5c3_Err != nil {
359 return templ_7745c5c3_Err378 return templ_7745c5c3_Err
360 }379 }
361- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "</main>")380+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "</main>")
362 if templ_7745c5c3_Err != nil {381 if templ_7745c5c3_Err != nil {
363 return templ_7745c5c3_Err382 return templ_7745c5c3_Err
364 }383 }
365 if l.HasRail && rail != nil {384 if l.HasRail && rail != nil {
366- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "<aside class=\"rail\" aria-label=\"Context\">")385+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "<aside class=\"rail\" aria-label=\"Context\">")
367 if templ_7745c5c3_Err != nil {386 if templ_7745c5c3_Err != nil {
368 return templ_7745c5c3_Err387 return templ_7745c5c3_Err
369 }388 }
@@ -371,12 +390,12 @@ func Shell(l Layout, rail templ.Component) templ.Component {
371 if templ_7745c5c3_Err != nil {390 if templ_7745c5c3_Err != nil {
372 return templ_7745c5c3_Err391 return templ_7745c5c3_Err
373 }392 }
374- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "</aside>")393+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "</aside>")
375 if templ_7745c5c3_Err != nil {394 if templ_7745c5c3_Err != nil {
376 return templ_7745c5c3_Err395 return templ_7745c5c3_Err
377 }396 }
378 }397 }
379- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "</div>")398+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "</div>")
380 if templ_7745c5c3_Err != nil {399 if templ_7745c5c3_Err != nil {
381 return templ_7745c5c3_Err400 return templ_7745c5c3_Err
382 }401 }
@@ -404,7 +423,7 @@ func Shell(l Layout, rail templ.Component) templ.Component {
404 if templ_7745c5c3_Err != nil {423 if templ_7745c5c3_Err != nil {
405 return templ_7745c5c3_Err424 return templ_7745c5c3_Err
406 }425 }
407- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "</body></html>")426+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "</body></html>")
408 if templ_7745c5c3_Err != nil {427 if templ_7745c5c3_Err != nil {
409 return templ_7745c5c3_Err428 return templ_7745c5c3_Err
410 }429 }
@@ -430,324 +449,324 @@ func Header(l Layout) templ.Component {
430 }()449 }()
431 }450 }
432 ctx = templ.InitializeContext(ctx)451 ctx = templ.InitializeContext(ctx)
433- templ_7745c5c3_Var24 := templ.GetChildren(ctx)452+ templ_7745c5c3_Var25 := templ.GetChildren(ctx)
434- if templ_7745c5c3_Var24 == nil {453+ if templ_7745c5c3_Var25 == nil {
435- templ_7745c5c3_Var24 = templ.NopComponent454+ templ_7745c5c3_Var25 = templ.NopComponent
436 }455 }
437 ctx = templ.ClearChildren(ctx)456 ctx = templ.ClearChildren(ctx)
438- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "<header class=\"hdr\"><div class=\"hdr-top\"><a class=\"brand\" href=\"/\" data-g=\"h\" aria-label=\"home\" title=\"home (g h)\"><span class=\"brand-mark\" aria-hidden=\"true\"></span></a> ")457+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "<header class=\"hdr\"><div class=\"hdr-top\"><a class=\"brand\" href=\"/\" data-g=\"h\" aria-label=\"home\" title=\"home (g h)\"><span class=\"brand-mark\" aria-hidden=\"true\"></span></a> ")
439 if templ_7745c5c3_Err != nil {458 if templ_7745c5c3_Err != nil {
440 return templ_7745c5c3_Err459 return templ_7745c5c3_Err
441 }460 }
442 if len(l.Crumbs) > 0 {461 if len(l.Crumbs) > 0 {
443- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "<nav class=\"crumbs\" aria-label=\"Path\"><ol>")462+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "<nav class=\"crumbs\" aria-label=\"Path\"><ol>")
444 if templ_7745c5c3_Err != nil {463 if templ_7745c5c3_Err != nil {
445 return templ_7745c5c3_Err464 return templ_7745c5c3_Err
446 }465 }
447 for _, c := range l.Crumbs {466 for _, c := range l.Crumbs {
448- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "<li>")467+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "<li>")
449 if templ_7745c5c3_Err != nil {468 if templ_7745c5c3_Err != nil {
450 return templ_7745c5c3_Err469 return templ_7745c5c3_Err
451 }470 }
452 if c.Href != "" {471 if c.Href != "" {
453- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "<a href=\"")472+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "<a href=\"")
454 if templ_7745c5c3_Err != nil {473 if templ_7745c5c3_Err != nil {
455 return templ_7745c5c3_Err474 return templ_7745c5c3_Err
456 }475 }
457- var templ_7745c5c3_Var25 templ.SafeURL476+ var templ_7745c5c3_Var26 templ.SafeURL
458- templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(c.Href))477+ templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(c.Href))
459 if templ_7745c5c3_Err != nil {478 if templ_7745c5c3_Err != nil {
460- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 87, Col: 38}479+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 90, Col: 38}
461 }480 }
462- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var25))481+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var26))
463 if templ_7745c5c3_Err != nil {482 if templ_7745c5c3_Err != nil {
464 return templ_7745c5c3_Err483 return templ_7745c5c3_Err
465 }484 }
466- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "\"")485+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "\"")
467 if templ_7745c5c3_Err != nil {486 if templ_7745c5c3_Err != nil {
468 return templ_7745c5c3_Err487 return templ_7745c5c3_Err
469 }488 }
470 if c.Current {489 if c.Current {
471- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, " aria-current=\"page\"")490+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, " aria-current=\"page\"")
472 if templ_7745c5c3_Err != nil {491 if templ_7745c5c3_Err != nil {
473 return templ_7745c5c3_Err492 return templ_7745c5c3_Err
474 }493 }
475 }494 }
476- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, ">")495+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, ">")
477 if templ_7745c5c3_Err != nil {496 if templ_7745c5c3_Err != nil {
478 return templ_7745c5c3_Err497 return templ_7745c5c3_Err
479 }498 }
480- var templ_7745c5c3_Var26 string499+ var templ_7745c5c3_Var27 string
481- templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.JoinStringErrs(c.Label)500+ templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.JoinStringErrs(c.Label)
482 if templ_7745c5c3_Err != nil {501 if templ_7745c5c3_Err != nil {
483- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 91, Col: 19}502+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 94, Col: 19}
484 }503 }
485- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var26))504+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var27))
486 if templ_7745c5c3_Err != nil {505 if templ_7745c5c3_Err != nil {
487 return templ_7745c5c3_Err506 return templ_7745c5c3_Err
488 }507 }
489- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "</a>")508+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "</a>")
490 if templ_7745c5c3_Err != nil {509 if templ_7745c5c3_Err != nil {
491 return templ_7745c5c3_Err510 return templ_7745c5c3_Err
492 }511 }
493 } else {512 } else {
494- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "<span")513+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "<span")
495 if templ_7745c5c3_Err != nil {514 if templ_7745c5c3_Err != nil {
496 return templ_7745c5c3_Err515 return templ_7745c5c3_Err
497 }516 }
498 if c.Current {517 if c.Current {
499- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, " aria-current=\"page\"")518+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, " aria-current=\"page\"")
500 if templ_7745c5c3_Err != nil {519 if templ_7745c5c3_Err != nil {
501 return templ_7745c5c3_Err520 return templ_7745c5c3_Err
502 }521 }
503 }522 }
504- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, ">")523+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, ">")
505 if templ_7745c5c3_Err != nil {524 if templ_7745c5c3_Err != nil {
506 return templ_7745c5c3_Err525 return templ_7745c5c3_Err
507 }526 }
508- var templ_7745c5c3_Var27 string527+ var templ_7745c5c3_Var28 string
509- templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.JoinStringErrs(c.Label)528+ templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.JoinStringErrs(c.Label)
510 if templ_7745c5c3_Err != nil {529 if templ_7745c5c3_Err != nil {
511- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 97, Col: 19}530+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 100, Col: 19}
512 }531 }
513- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var27))532+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var28))
514 if templ_7745c5c3_Err != nil {533 if templ_7745c5c3_Err != nil {
515 return templ_7745c5c3_Err534 return templ_7745c5c3_Err
516 }535 }
517- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "</span>")536+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, "</span>")
518 if templ_7745c5c3_Err != nil {537 if templ_7745c5c3_Err != nil {
519 return templ_7745c5c3_Err538 return templ_7745c5c3_Err
520 }539 }
521 }540 }
522- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "</li>")541+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "</li>")
523 if templ_7745c5c3_Err != nil {542 if templ_7745c5c3_Err != nil {
524 return templ_7745c5c3_Err543 return templ_7745c5c3_Err
525 }544 }
526 }545 }
527- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, "</ol></nav>")546+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, "</ol></nav>")
528 if templ_7745c5c3_Err != nil {547 if templ_7745c5c3_Err != nil {
529 return templ_7745c5c3_Err548 return templ_7745c5c3_Err
530 }549 }
531 }550 }
532- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "<div class=\"hdr-acts\">")551+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, "<div class=\"hdr-acts\">")
533 if templ_7745c5c3_Err != nil {552 if templ_7745c5c3_Err != nil {
534 return templ_7745c5c3_Err553 return templ_7745c5c3_Err
535 }554 }
536 if l.Ref != "" {555 if l.Ref != "" {
537- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, "<a class=\"btn ref\" href=\"")556+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, "<a class=\"btn ref\" href=\"")
538 if templ_7745c5c3_Err != nil {557 if templ_7745c5c3_Err != nil {
539 return templ_7745c5c3_Err558 return templ_7745c5c3_Err
540 }559 }
541- var templ_7745c5c3_Var28 templ.SafeURL560+ var templ_7745c5c3_Var29 templ.SafeURL
542- templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(repoHref(l.Repo, "refs")))561+ templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(repoHref(l.Repo, "refs")))
543 if templ_7745c5c3_Err != nil {562 if templ_7745c5c3_Err != nil {
544- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 108, Col: 52}563+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 111, Col: 52}
545 }564 }
546- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var28))565+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var29))
547 if templ_7745c5c3_Err != nil {566 if templ_7745c5c3_Err != nil {
548 return templ_7745c5c3_Err567 return templ_7745c5c3_Err
549 }568 }
550- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, "\" data-fire=\"refs\" aria-keyshortcuts=\"b\" title=\"switch branch or tag (b)\"><span class=\"ico i-branch\" aria-hidden=\"true\"></span> <span class=\"ref-name\">")569+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, "\" data-fire=\"refs\" aria-keyshortcuts=\"b\" title=\"switch branch or tag (b)\"><span class=\"ico i-branch\" aria-hidden=\"true\"></span> <span class=\"ref-name\">")
551 if templ_7745c5c3_Err != nil {570 if templ_7745c5c3_Err != nil {
552 return templ_7745c5c3_Err571 return templ_7745c5c3_Err
553 }572 }
554- var templ_7745c5c3_Var29 string573+ var templ_7745c5c3_Var30 string
555- templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.JoinStringErrs(l.Ref)574+ templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinStringErrs(l.Ref)
556 if templ_7745c5c3_Err != nil {575 if templ_7745c5c3_Err != nil {
557- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 114, Col: 36}576+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 117, Col: 36}
558 }577 }
559- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var29))578+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var30))
560 if templ_7745c5c3_Err != nil {579 if templ_7745c5c3_Err != nil {
561 return templ_7745c5c3_Err580 return templ_7745c5c3_Err
562 }581 }
563- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, "</span> <span class=\"sha\">")582+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, "</span> <span class=\"sha\">")
564 if templ_7745c5c3_Err != nil {583 if templ_7745c5c3_Err != nil {
565 return templ_7745c5c3_Err584 return templ_7745c5c3_Err
566 }585 }
567- var templ_7745c5c3_Var30 string586+ var templ_7745c5c3_Var31 string
568- templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinStringErrs(l.Short)587+ templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.JoinStringErrs(l.Short)
569 if templ_7745c5c3_Err != nil {588 if templ_7745c5c3_Err != nil {
570- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 115, Col: 33}589+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 118, Col: 33}
571 }590 }
572- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var30))591+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var31))
573 if templ_7745c5c3_Err != nil {592 if templ_7745c5c3_Err != nil {
574 return templ_7745c5c3_Err593 return templ_7745c5c3_Err
575 }594 }
576- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, "</span></a> ")595+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, "</span></a> ")
577 if templ_7745c5c3_Err != nil {596 if templ_7745c5c3_Err != nil {
578 return templ_7745c5c3_Err597 return templ_7745c5c3_Err
579 }598 }
580 }599 }
581- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, "<button type=\"button\" class=\"btn search\" data-js aria-haspopup=\"dialog\" aria-controls=\"panel-cmd\" aria-keyshortcuts=\"Control+K :\" title=\"go to a section, ref, file or repo (ctrl k or :)\" data-fire=\"cmd\"><span class=\"ico i-search\" aria-hidden=\"true\"></span> <span class=\"search-label\">Go to…</span> <kbd aria-hidden=\"true\">ctrl k</kbd></button> ")600+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 56, "<button type=\"button\" class=\"btn search\" data-js aria-haspopup=\"dialog\" aria-controls=\"panel-cmd\" aria-keyshortcuts=\"Control+K :\" title=\"go to a section, ref, file or repo (ctrl k or :)\" data-fire=\"cmd\"><span class=\"ico i-search\" aria-hidden=\"true\"></span> <span class=\"search-label\">Go to…</span> <kbd aria-hidden=\"true\">ctrl k</kbd></button> ")
582 if templ_7745c5c3_Err != nil {601 if templ_7745c5c3_Err != nil {
583 return templ_7745c5c3_Err602 return templ_7745c5c3_Err
584 }603 }
585 if SignedIn(ctx) {604 if SignedIn(ctx) {
586- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, "<form class=\"owner\" method=\"post\" action=\"/-/logout\"><span class=\"owner-badge\" title=\"signed in as the owner\"><span class=\"ico i-user\" aria-hidden=\"true\"></span>")605+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, "<form class=\"owner\" method=\"post\" action=\"/-/logout\"><span class=\"owner-badge\" title=\"signed in as the owner\"><span class=\"ico i-user\" aria-hidden=\"true\"></span>")
587 if templ_7745c5c3_Err != nil {606 if templ_7745c5c3_Err != nil {
588 return templ_7745c5c3_Err607 return templ_7745c5c3_Err
589 }608 }
590- var templ_7745c5c3_Var31 string609+ var templ_7745c5c3_Var32 string
591- templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.JoinStringErrs(l.Owner)610+ templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.JoinStringErrs(l.Owner)
592 if templ_7745c5c3_Err != nil {611 if templ_7745c5c3_Err != nil {
593- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 134, Col: 123}612+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 137, Col: 123}
594 }613 }
595- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var31))614+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var32))
596 if templ_7745c5c3_Err != nil {615 if templ_7745c5c3_Err != nil {
597 return templ_7745c5c3_Err616 return templ_7745c5c3_Err
598 }617 }
599- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 56, "</span> ")618+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "</span> ")
600 if templ_7745c5c3_Err != nil {619 if templ_7745c5c3_Err != nil {
601 return templ_7745c5c3_Err620 return templ_7745c5c3_Err
602 }621 }
603 if l.Passkeys {622 if l.Passkeys {
604- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, "<a class=\"btn icon\" href=\"/-/passkeys\" aria-label=\"Passkeys\" title=\"passkeys: add or remove the passkeys that sign you in\"><span class=\"ico i-key\" aria-hidden=\"true\"></span></a> ")623+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, "<a class=\"btn icon\" href=\"/-/passkeys\" aria-label=\"Passkeys\" title=\"passkeys: add or remove the passkeys that sign you in\"><span class=\"ico i-key\" aria-hidden=\"true\"></span></a> ")
605 if templ_7745c5c3_Err != nil {624 if templ_7745c5c3_Err != nil {
606 return templ_7745c5c3_Err625 return templ_7745c5c3_Err
607 }626 }
608 }627 }
609- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "<button type=\"submit\" class=\"btn icon\" aria-label=\"Sign out\" title=\"sign out of this browser\"><span class=\"ico i-log-out\" aria-hidden=\"true\"></span></button></form>")628+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 60, "<button type=\"submit\" class=\"btn icon\" aria-label=\"Sign out\" title=\"sign out of this browser\"><span class=\"ico i-log-out\" aria-hidden=\"true\"></span></button></form>")
610 if templ_7745c5c3_Err != nil {629 if templ_7745c5c3_Err != nil {
611 return templ_7745c5c3_Err630 return templ_7745c5c3_Err
612 }631 }
613 } else if l.Passkeys {632 } else if l.Passkeys {
614- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, "<a class=\"btn icon\" href=\"/-/login\" aria-label=\"Sign in\" title=\"sign in with a passkey\"><span class=\"ico i-log-in\" aria-hidden=\"true\"></span></a>")633+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 61, "<a class=\"btn icon\" href=\"/-/login\" aria-label=\"Sign in\" title=\"sign in with a passkey\"><span class=\"ico i-log-in\" aria-hidden=\"true\"></span></a>")
615 if templ_7745c5c3_Err != nil {634 if templ_7745c5c3_Err != nil {
616 return templ_7745c5c3_Err635 return templ_7745c5c3_Err
617 }636 }
618 }637 }
619- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 60, "</div></div>")638+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 62, "</div></div>")
620 if templ_7745c5c3_Err != nil {639 if templ_7745c5c3_Err != nil {
621 return templ_7745c5c3_Err640 return templ_7745c5c3_Err
622 }641 }
623 if len(l.Sections) > 0 {642 if len(l.Sections) > 0 {
624- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 61, "<nav class=\"tabs\" aria-label=\"Sections\"><ul>")643+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 63, "<nav class=\"tabs\" aria-label=\"Sections\"><ul>")
625 if templ_7745c5c3_Err != nil {644 if templ_7745c5c3_Err != nil {
626 return templ_7745c5c3_Err645 return templ_7745c5c3_Err
627 }646 }
628 for _, s := range l.Sections {647 for _, s := range l.Sections {
629- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 62, "<li><a href=\"")648+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 64, "<li><a href=\"")
630 if templ_7745c5c3_Err != nil {649 if templ_7745c5c3_Err != nil {
631 return templ_7745c5c3_Err650 return templ_7745c5c3_Err
632 }651 }
633- var templ_7745c5c3_Var32 templ.SafeURL652+ var templ_7745c5c3_Var33 templ.SafeURL
634- templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(s.Href))653+ templ_7745c5c3_Var33, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(s.Href))
635 if templ_7745c5c3_Err != nil {654 if templ_7745c5c3_Err != nil {
636- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 151, Col: 36}655+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 154, Col: 36}
637 }656 }
638- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var32))657+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var33))
639 if templ_7745c5c3_Err != nil {658 if templ_7745c5c3_Err != nil {
640 return templ_7745c5c3_Err659 return templ_7745c5c3_Err
641 }660 }
642- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 63, "\" data-key=\"")661+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 65, "\" data-key=\"")
643 if templ_7745c5c3_Err != nil {662 if templ_7745c5c3_Err != nil {
644 return templ_7745c5c3_Err663 return templ_7745c5c3_Err
645 }664 }
646- var templ_7745c5c3_Var33 string665+ var templ_7745c5c3_Var34 string
647- templ_7745c5c3_Var33, templ_7745c5c3_Err = templ.ResolveAttributeValue(s.Key)666+ templ_7745c5c3_Var34, templ_7745c5c3_Err = templ.ResolveAttributeValue(s.Key)
648 if templ_7745c5c3_Err != nil {667 if templ_7745c5c3_Err != nil {
649- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 152, Col: 24}668+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 155, Col: 24}
650 }669 }
651- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var33)670+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var34)
652 if templ_7745c5c3_Err != nil {671 if templ_7745c5c3_Err != nil {
653 return templ_7745c5c3_Err672 return templ_7745c5c3_Err
654 }673 }
655- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 64, "\" aria-keyshortcuts=\"")674+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 66, "\" aria-keyshortcuts=\"")
656 if templ_7745c5c3_Err != nil {675 if templ_7745c5c3_Err != nil {
657 return templ_7745c5c3_Err676 return templ_7745c5c3_Err
658 }677 }
659- var templ_7745c5c3_Var34 string678+ var templ_7745c5c3_Var35 string
660- templ_7745c5c3_Var34, templ_7745c5c3_Err = templ.ResolveAttributeValue(s.Key)679+ templ_7745c5c3_Var35, templ_7745c5c3_Err = templ.ResolveAttributeValue(s.Key)
661 if templ_7745c5c3_Err != nil {680 if templ_7745c5c3_Err != nil {
662- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 153, Col: 33}681+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 156, Col: 33}
663 }682 }
664- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var34)683+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var35)
665 if templ_7745c5c3_Err != nil {684 if templ_7745c5c3_Err != nil {
666 return templ_7745c5c3_Err685 return templ_7745c5c3_Err
667 }686 }
668- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 65, "\"")687+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 67, "\"")
669 if templ_7745c5c3_Err != nil {688 if templ_7745c5c3_Err != nil {
670 return templ_7745c5c3_Err689 return templ_7745c5c3_Err
671 }690 }
672 if s.Current {691 if s.Current {
673- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 66, " aria-current=\"page\"")692+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 68, " aria-current=\"page\"")
674 if templ_7745c5c3_Err != nil {693 if templ_7745c5c3_Err != nil {
675 return templ_7745c5c3_Err694 return templ_7745c5c3_Err
676 }695 }
677 }696 }
678- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 67, ">")697+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 69, ">")
679 if templ_7745c5c3_Err != nil {698 if templ_7745c5c3_Err != nil {
680 return templ_7745c5c3_Err699 return templ_7745c5c3_Err
681 }700 }
682- var templ_7745c5c3_Var35 string701+ var templ_7745c5c3_Var36 string
683- templ_7745c5c3_Var35, templ_7745c5c3_Err = templ.JoinStringErrs(s.Label)702+ templ_7745c5c3_Var36, templ_7745c5c3_Err = templ.JoinStringErrs(s.Label)
684 if templ_7745c5c3_Err != nil {703 if templ_7745c5c3_Err != nil {
685- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 158, Col: 17}704+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 161, Col: 17}
686 }705 }
687- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var35))706+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var36))
688 if templ_7745c5c3_Err != nil {707 if templ_7745c5c3_Err != nil {
689 return templ_7745c5c3_Err708 return templ_7745c5c3_Err
690 }709 }
691- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 68, " ")710+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 70, " ")
692 if templ_7745c5c3_Err != nil {711 if templ_7745c5c3_Err != nil {
693 return templ_7745c5c3_Err712 return templ_7745c5c3_Err
694 }713 }
695 if s.Count != "" {714 if s.Count != "" {
696- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 69, "<span class=\"count\">")715+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 71, "<span class=\"count\">")
697 if templ_7745c5c3_Err != nil {716 if templ_7745c5c3_Err != nil {
698 return templ_7745c5c3_Err717 return templ_7745c5c3_Err
699 }718 }
700- var templ_7745c5c3_Var36 string719+ var templ_7745c5c3_Var37 string
701- templ_7745c5c3_Var36, templ_7745c5c3_Err = templ.JoinStringErrs(s.Count)720+ templ_7745c5c3_Var37, templ_7745c5c3_Err = templ.JoinStringErrs(s.Count)
702 if templ_7745c5c3_Err != nil {721 if templ_7745c5c3_Err != nil {
703- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 160, Col: 38}722+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 163, Col: 38}
704 }723 }
705- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var36))724+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var37))
706 if templ_7745c5c3_Err != nil {725 if templ_7745c5c3_Err != nil {
707 return templ_7745c5c3_Err726 return templ_7745c5c3_Err
708 }727 }
709- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 70, "<span class=\"sr\">")728+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 72, "<span class=\"sr\">")
710 if templ_7745c5c3_Err != nil {729 if templ_7745c5c3_Err != nil {
711 return templ_7745c5c3_Err730 return templ_7745c5c3_Err
712 }731 }
713- var templ_7745c5c3_Var37 string732+ var templ_7745c5c3_Var38 string
714- templ_7745c5c3_Var37, templ_7745c5c3_Err = templ.JoinStringErrs(" open")733+ templ_7745c5c3_Var38, templ_7745c5c3_Err = templ.JoinStringErrs(" open")
715 if templ_7745c5c3_Err != nil {734 if templ_7745c5c3_Err != nil {
716- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 160, Col: 66}735+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 163, Col: 66}
717 }736 }
718- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var37))737+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var38))
719 if templ_7745c5c3_Err != nil {738 if templ_7745c5c3_Err != nil {
720 return templ_7745c5c3_Err739 return templ_7745c5c3_Err
721 }740 }
722- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 71, "</span></span> ")741+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 73, "</span></span> ")
723 if templ_7745c5c3_Err != nil {742 if templ_7745c5c3_Err != nil {
724 return templ_7745c5c3_Err743 return templ_7745c5c3_Err
725 }744 }
726 }745 }
727- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 72, "<kbd aria-hidden=\"true\">")746+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 74, "<kbd aria-hidden=\"true\">")
728 if templ_7745c5c3_Err != nil {747 if templ_7745c5c3_Err != nil {
729 return templ_7745c5c3_Err748 return templ_7745c5c3_Err
730 }749 }
731- var templ_7745c5c3_Var38 string750+ var templ_7745c5c3_Var39 string
732- templ_7745c5c3_Var38, templ_7745c5c3_Err = templ.JoinStringErrs(s.Key)751+ templ_7745c5c3_Var39, templ_7745c5c3_Err = templ.JoinStringErrs(s.Key)
733 if templ_7745c5c3_Err != nil {752 if templ_7745c5c3_Err != nil {
734- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 162, Col: 39}753+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 165, Col: 39}
735 }754 }
736- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var38))755+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var39))
737 if templ_7745c5c3_Err != nil {756 if templ_7745c5c3_Err != nil {
738 return templ_7745c5c3_Err757 return templ_7745c5c3_Err
739 }758 }
740- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 73, "</kbd></a></li>")759+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 75, "</kbd></a></li>")
741 if templ_7745c5c3_Err != nil {760 if templ_7745c5c3_Err != nil {
742 return templ_7745c5c3_Err761 return templ_7745c5c3_Err
743 }762 }
744 }763 }
745- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 74, "</ul></nav>")764+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 76, "</ul></nav>")
746 if templ_7745c5c3_Err != nil {765 if templ_7745c5c3_Err != nil {
747 return templ_7745c5c3_Err766 return templ_7745c5c3_Err
748 }767 }
749 }768 }
750- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 75, "</header>")769+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 77, "</header>")
751 if templ_7745c5c3_Err != nil {770 if templ_7745c5c3_Err != nil {
752 return templ_7745c5c3_Err771 return templ_7745c5c3_Err
753 }772 }
@@ -773,38 +792,38 @@ func Footer(l Layout) templ.Component {
773 }()792 }()
774 }793 }
775 ctx = templ.InitializeContext(ctx)794 ctx = templ.InitializeContext(ctx)
776- templ_7745c5c3_Var39 := templ.GetChildren(ctx)795+ templ_7745c5c3_Var40 := templ.GetChildren(ctx)
777- if templ_7745c5c3_Var39 == nil {796+ if templ_7745c5c3_Var40 == nil {
778- templ_7745c5c3_Var39 = templ.NopComponent797+ templ_7745c5c3_Var40 = templ.NopComponent
779 }798 }
780 ctx = templ.ClearChildren(ctx)799 ctx = templ.ClearChildren(ctx)
781- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 76, "<footer class=\"foot\"><span>ermite ")800+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 78, "<footer class=\"foot\"><span>ermite ")
782 if templ_7745c5c3_Err != nil {801 if templ_7745c5c3_Err != nil {
783 return templ_7745c5c3_Err802 return templ_7745c5c3_Err
784 }803 }
785- var templ_7745c5c3_Var40 string804+ var templ_7745c5c3_Var41 string
786- templ_7745c5c3_Var40, templ_7745c5c3_Err = templ.JoinStringErrs(l.Version)805+ templ_7745c5c3_Var41, templ_7745c5c3_Err = templ.JoinStringErrs(l.Version)
787 if templ_7745c5c3_Err != nil {806 if templ_7745c5c3_Err != nil {
788- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 176, Col: 26}807+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 179, Col: 26}
789 }808 }
790- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var40))809+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var41))
791 if templ_7745c5c3_Err != nil {810 if templ_7745c5c3_Err != nil {
792 return templ_7745c5c3_Err811 return templ_7745c5c3_Err
793 }812 }
794- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 77, "</span> <a href=\"")813+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 79, "</span> <a href=\"")
795 if templ_7745c5c3_Err != nil {814 if templ_7745c5c3_Err != nil {
796 return templ_7745c5c3_Err815 return templ_7745c5c3_Err
797 }816 }
798- var templ_7745c5c3_Var41 templ.SafeURL817+ var templ_7745c5c3_Var42 templ.SafeURL
799- templ_7745c5c3_Var41, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(l.SourceURL))818+ templ_7745c5c3_Var42, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(l.SourceURL))
800 if templ_7745c5c3_Err != nil {819 if templ_7745c5c3_Err != nil {
801- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 177, Col: 38}820+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 180, Col: 38}
802 }821 }
803- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var41))822+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var42))
804 if templ_7745c5c3_Err != nil {823 if templ_7745c5c3_Err != nil {
805 return templ_7745c5c3_Err824 return templ_7745c5c3_Err
806 }825 }
807- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 78, "\" rel=\"noopener\" title=\"source of this version (AGPL-3.0-or-later)\">source</a> <button type=\"button\" class=\"foot-keys\" data-js data-fire=\"help\" aria-haspopup=\"dialog\" aria-controls=\"panel-help\" aria-keyshortcuts=\"?\"><kbd aria-hidden=\"true\">?</kbd> keyboard shortcuts</button></footer>")826+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 80, "\" rel=\"noopener\" title=\"source of this version (AGPL-3.0-or-later)\">source</a> <button type=\"button\" class=\"foot-keys\" data-js data-fire=\"help\" aria-haspopup=\"dialog\" aria-controls=\"panel-help\" aria-keyshortcuts=\"?\"><kbd aria-hidden=\"true\">?</kbd> keyboard shortcuts</button></footer>")
808 if templ_7745c5c3_Err != nil {827 if templ_7745c5c3_Err != nil {
809 return templ_7745c5c3_Err828 return templ_7745c5c3_Err
810 }829 }
@@ -832,12 +851,12 @@ func Hint(l Layout) templ.Component {
832 }()851 }()
833 }852 }
834 ctx = templ.InitializeContext(ctx)853 ctx = templ.InitializeContext(ctx)
835- templ_7745c5c3_Var42 := templ.GetChildren(ctx)854+ templ_7745c5c3_Var43 := templ.GetChildren(ctx)
836- if templ_7745c5c3_Var42 == nil {855+ if templ_7745c5c3_Var43 == nil {
837- templ_7745c5c3_Var42 = templ.NopComponent856+ templ_7745c5c3_Var43 = templ.NopComponent
838 }857 }
839 ctx = templ.ClearChildren(ctx)858 ctx = templ.ClearChildren(ctx)
840- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 79, "<div class=\"hint\" role=\"status\" aria-live=\"polite\"><span class=\"hint-nav\" :text=\"document.body.dataset.nav || ''\"></span> <span class=\"hint-sel\" :text=\"document.body.dataset.sel ? document.body.dataset.sel + ' · y permalink' : ''\"></span> <span class=\"hint-flash\" :text=\"document.body.dataset.flash || ''\"></span>")859+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 81, "<div class=\"hint\" role=\"status\" aria-live=\"polite\"><span class=\"hint-nav\" :text=\"document.body.dataset.nav || ''\"></span> <span class=\"hint-sel\" :text=\"document.body.dataset.sel ? document.body.dataset.sel + ' · y permalink' : ''\"></span> <span class=\"hint-flash\" :text=\"document.body.dataset.flash || ''\"></span>")
841 if templ_7745c5c3_Err != nil {860 if templ_7745c5c3_Err != nil {
842 return templ_7745c5c3_Err861 return templ_7745c5c3_Err
843 }862 }
@@ -845,7 +864,7 @@ func Hint(l Layout) templ.Component {
845 if templ_7745c5c3_Err != nil {864 if templ_7745c5c3_Err != nil {
846 return templ_7745c5c3_Err865 return templ_7745c5c3_Err
847 }866 }
848- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 80, "</div>")867+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 82, "</div>")
849 if templ_7745c5c3_Err != nil {868 if templ_7745c5c3_Err != nil {
850 return templ_7745c5c3_Err869 return templ_7745c5c3_Err
851 }870 }
A internal/adapters/web/static/js/issue-form.js +112 -0
@@ -0,0 +1,112 @@
1+// issue-form.js: the public issue form (/<repo>/issues/new for a visitor).
2+// The server sends a proof of work with the form; this script starts the
3+// worker (pow-worker.js) when the page loads, so it solves while the visitor
4+// writes, shows the length counters, and enables the button once the solution
5+// is found and the minimum fill time has passed. Without scripts the form
6+// stays hidden (the page says why).
7+//
8+// window.ermiteIssueForm.core is pure (jstest/issue-form.test.mjs).
9+window.ermiteIssueForm = (() => {
10+ const core = {
11+ // counterClass is the class of a length counter: "over" above max, "warn"
12+ // from 90 % of it, else none.
13+ counterClass(n, max) {
14+ if (n > max) return 'over';
15+ if (n * 10 >= max * 9) return 'warn';
16+ return '';
17+ },
18+ // length counts code points, like the server.
19+ length(s) { return [...String(s)].length; },
20+ // state is what the button and the status line show.
21+ // {solved, now, minAt, sending, unsupported, failed, overs}
22+ state({ solved, now, minAt, sending, unsupported, failed, overs }) {
23+ if (unsupported) return { disabled: true, status: 'Your browser cannot run the check.' };
24+ if (failed) return { disabled: true, status: 'The check failed. Reload the page.' };
25+ if (sending) return { disabled: true, status: 'Sending…' };
26+ if (!solved) return { disabled: true, status: 'Checking your browser… (this takes a few seconds)' };
27+ if (now < minAt) return { disabled: true, status: 'Almost ready…' };
28+ if (overs) return { disabled: true, status: 'A field is too long.' };
29+ return { disabled: false, status: 'Ready.' };
30+ },
31+ };
32+
33+ function init(form) {
34+ const $ = (sel) => form.querySelector(sel);
35+ const button = $('button[type="submit"]');
36+ const status = $('[data-status]');
37+ const solution = $('input[name="solution"]');
38+ const minAt = Number(form.dataset.minSubmitAt) || 0;
39+ const ctx = { solved: false, sending: false, unsupported: false, failed: false, overs: false };
40+ let timer = null;
41+
42+ function render() {
43+ const s = core.state({ ...ctx, now: Date.now(), minAt });
44+ button.disabled = s.disabled;
45+ if (status) status.textContent = s.status;
46+ clearTimeout(timer);
47+ if (ctx.solved && Date.now() < minAt && !ctx.sending) timer = setTimeout(render, minAt - Date.now() + 20);
48+ }
49+
50+ for (const out of form.querySelectorAll('output[data-for]')) {
51+ const field = form.querySelector('#' + out.dataset.for);
52+ const max = Number(out.dataset.max);
53+ const update = () => {
54+ const n = core.length(field.value);
55+ out.textContent = n + ' / ' + max;
56+ out.className = core.counterClass(n, max);
57+ ctx.overs = [...form.querySelectorAll('output[data-for]')].some((o) => o.className === 'over');
58+ render();
59+ };
60+ field.addEventListener('input', update);
61+ update();
62+ }
63+
64+ form.addEventListener('submit', (e) => {
65+ if (ctx.sending || !ctx.solved || Date.now() < minAt) { e.preventDefault(); return; }
66+ ctx.sending = true;
67+ render();
68+ });
69+
70+ form.hidden = false;
71+ form.removeAttribute('hidden');
72+
73+ if (!window.Worker || !window.crypto || !window.crypto.subtle || !form.dataset.worker) {
74+ ctx.unsupported = true;
75+ render();
76+ return;
77+ }
78+ render();
79+ let worker;
80+ try {
81+ worker = new window.Worker(form.dataset.worker);
82+ } catch (_) {
83+ ctx.unsupported = true;
84+ render();
85+ return;
86+ }
87+ worker.onmessage = (e) => {
88+ const m = e.data || {};
89+ if (m.solution !== undefined) {
90+ solution.value = m.solution;
91+ ctx.solved = true;
92+ worker.terminate();
93+ } else if (m.error === 'nocrypto') {
94+ ctx.unsupported = true;
95+ } else if (m.error) {
96+ ctx.failed = true;
97+ } else {
98+ return;
99+ }
100+ render();
101+ };
102+ worker.onerror = () => { ctx.failed = true; render(); };
103+ worker.postMessage({ nonce: form.dataset.nonce, difficulty: Number(form.dataset.difficulty) });
104+ }
105+
106+ return { core, init };
107+})();
108+
109+document.addEventListener('DOMContentLoaded', () => {
110+ const form = document.querySelector('form[data-issue-form]');
111+ if (form) window.ermiteIssueForm.init(form);
112+});
@@ -0,0 +1,112 @@
1+// issue-form.js: the public issue form (/<repo>/issues/new for a visitor).
2+// The server sends a proof of work with the form; this script starts the
3+// worker (pow-worker.js) when the page loads, so it solves while the visitor
4+// writes, shows the length counters, and enables the button once the solution
5+// is found and the minimum fill time has passed. Without scripts the form
6+// stays hidden (the page says why).
7+//
8+// window.ermiteIssueForm.core is pure (jstest/issue-form.test.mjs).
9+window.ermiteIssueForm = (() => {
10+ const core = {
11+ // counterClass is the class of a length counter: "over" above max, "warn"
12+ // from 90 % of it, else none.
13+ counterClass(n, max) {
14+ if (n > max) return 'over';
15+ if (n * 10 >= max * 9) return 'warn';
16+ return '';
17+ },
18+ // length counts code points, like the server.
19+ length(s) { return [...String(s)].length; },
20+ // state is what the button and the status line show.
21+ // {solved, now, minAt, sending, unsupported, failed, overs}
22+ state({ solved, now, minAt, sending, unsupported, failed, overs }) {
23+ if (unsupported) return { disabled: true, status: 'Your browser cannot run the check.' };
24+ if (failed) return { disabled: true, status: 'The check failed. Reload the page.' };
25+ if (sending) return { disabled: true, status: 'Sending…' };
26+ if (!solved) return { disabled: true, status: 'Checking your browser… (this takes a few seconds)' };
27+ if (now < minAt) return { disabled: true, status: 'Almost ready…' };
28+ if (overs) return { disabled: true, status: 'A field is too long.' };
29+ return { disabled: false, status: 'Ready.' };
30+ },
31+ };
32+
33+ function init(form) {
34+ const $ = (sel) => form.querySelector(sel);
35+ const button = $('button[type="submit"]');
36+ const status = $('[data-status]');
37+ const solution = $('input[name="solution"]');
38+ const minAt = Number(form.dataset.minSubmitAt) || 0;
39+ const ctx = { solved: false, sending: false, unsupported: false, failed: false, overs: false };
40+ let timer = null;
41+
42+ function render() {
43+ const s = core.state({ ...ctx, now: Date.now(), minAt });
44+ button.disabled = s.disabled;
45+ if (status) status.textContent = s.status;
46+ clearTimeout(timer);
47+ if (ctx.solved && Date.now() < minAt && !ctx.sending) timer = setTimeout(render, minAt - Date.now() + 20);
48+ }
49+
50+ for (const out of form.querySelectorAll('output[data-for]')) {
51+ const field = form.querySelector('#' + out.dataset.for);
52+ const max = Number(out.dataset.max);
53+ const update = () => {
54+ const n = core.length(field.value);
55+ out.textContent = n + ' / ' + max;
56+ out.className = core.counterClass(n, max);
57+ ctx.overs = [...form.querySelectorAll('output[data-for]')].some((o) => o.className === 'over');
58+ render();
59+ };
60+ field.addEventListener('input', update);
61+ update();
62+ }
63+
64+ form.addEventListener('submit', (e) => {
65+ if (ctx.sending || !ctx.solved || Date.now() < minAt) { e.preventDefault(); return; }
66+ ctx.sending = true;
67+ render();
68+ });
69+
70+ form.hidden = false;
71+ form.removeAttribute('hidden');
72+
73+ if (!window.Worker || !window.crypto || !window.crypto.subtle || !form.dataset.worker) {
74+ ctx.unsupported = true;
75+ render();
76+ return;
77+ }
78+ render();
79+ let worker;
80+ try {
81+ worker = new window.Worker(form.dataset.worker);
82+ } catch (_) {
83+ ctx.unsupported = true;
84+ render();
85+ return;
86+ }
87+ worker.onmessage = (e) => {
88+ const m = e.data || {};
89+ if (m.solution !== undefined) {
90+ solution.value = m.solution;
91+ ctx.solved = true;
92+ worker.terminate();
93+ } else if (m.error === 'nocrypto') {
94+ ctx.unsupported = true;
95+ } else if (m.error) {
96+ ctx.failed = true;
97+ } else {
98+ return;
99+ }
100+ render();
101+ };
102+ worker.onerror = () => { ctx.failed = true; render(); };
103+ worker.postMessage({ nonce: form.dataset.nonce, difficulty: Number(form.dataset.difficulty) });
104+ }
105+
106+ return { core, init };
107+})();
108+
109+document.addEventListener('DOMContentLoaded', () => {
110+ const form = document.querySelector('form[data-issue-form]');
111+ if (form) window.ermiteIssueForm.init(form);
112+});
A internal/adapters/web/static/js/pow-worker.js +72 -0
@@ -0,0 +1,72 @@
1+// pow-worker.js: the proof of work of the public issue form, in a dedicated
2+// Worker (started by issue-form.js). It finds the smallest decimal counter n
3+// such that SHA-256(nonce bytes || ":" || n) has at least `difficulty` leading
4+// zero bits: the very check of the server (usecase/issue_submissions.go,
5+// powHash). The nonce arrives as base64url without padding, like the server
6+// sends it. Messages in: {nonce, difficulty}. Messages out:
7+// {progress: tried} now and then, then {solution: "<decimal>"} or {error}.
8+//
9+// window-less on purpose: ermitePow is also loaded by Node for the tests
10+// (jstest/issue-form.test.mjs).
11+(() => {
12+ const BATCH = 2048;
13+ const enc = new TextEncoder();
14+
15+ // leadingZeroBits counts the zero bits at the start of the bytes.
16+ function leadingZeroBits(bytes) {
17+ let n = 0;
18+ for (const b of bytes) {
19+ if (b === 0) { n += 8; continue; }
20+ return n + Math.clz32(b) - 24;
21+ }
22+ return n;
23+ }
24+
25+ // b64urlDecode turns unpadded base64url into bytes.
26+ function b64urlDecode(s) {
27+ const bin = atob(String(s).replace(/-/g, '+').replace(/_/g, '/'));
28+ const out = new Uint8Array(bin.length);
29+ for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
30+ return out;
31+ }
32+
33+ // input is nonce || ":" || decimal(n), the bytes the server hashes.
34+ function input(nonce, n) {
35+ const tail = enc.encode(':' + n);
36+ const out = new Uint8Array(nonce.length + tail.length);
37+ out.set(nonce, 0);
38+ out.set(tail, nonce.length);
39+ return out;
40+ }
41+
42+ // solve returns the smallest solution as a decimal string. digest is
43+ // (Uint8Array) => Promise<ArrayBuffer-like>, crypto.subtle.digest by default.
44+ // Hashes are asked a batch at a time, in order, so the first hit of the first
45+ // batch that has one is the smallest. onProgress(tried) is called after each
46+ // batch; it may return true to stop (solve then returns null).
47+ async function solve(nonce, difficulty, { digest, batch = BATCH, onProgress } = {}) {
48+ const dig = digest || ((d) => globalThis.crypto.subtle.digest('SHA-256', d));
49+ for (let start = 0; ; start += batch) {
50+ const sums = await Promise.all(Array.from({ length: batch }, async (_, i) => new Uint8Array(await dig(input(nonce, start + i)))));
51+ for (let i = 0; i < batch; i++) {
52+ if (leadingZeroBits(sums[i]) >= difficulty) return String(start + i);
53+ }
54+ if (onProgress && onProgress(start + batch)) return null;
55+ }
56+ }
57+
58+ globalThis.ermitePow = { leadingZeroBits, b64urlDecode, input, solve };
59+
60+ if (typeof WorkerGlobalScope !== 'undefined' && typeof globalThis.postMessage === 'function') {
61+ globalThis.onmessage = async (e) => {
62+ try {
63+ if (!globalThis.crypto || !globalThis.crypto.subtle) { postMessage({ error: 'nocrypto' }); return; }
64+ const { nonce, difficulty } = e.data;
65+ const solution = await solve(b64urlDecode(nonce), difficulty, { onProgress: (tried) => { postMessage({ progress: tried }); } });
66+ postMessage({ solution });
67+ } catch (err) {
68+ postMessage({ error: String(err && err.message || err) });
69+ }
70+ };
71+ }
72+})();
@@ -0,0 +1,72 @@
1+// pow-worker.js: the proof of work of the public issue form, in a dedicated
2+// Worker (started by issue-form.js). It finds the smallest decimal counter n
3+// such that SHA-256(nonce bytes || ":" || n) has at least `difficulty` leading
4+// zero bits: the very check of the server (usecase/issue_submissions.go,
5+// powHash). The nonce arrives as base64url without padding, like the server
6+// sends it. Messages in: {nonce, difficulty}. Messages out:
7+// {progress: tried} now and then, then {solution: "<decimal>"} or {error}.
8+//
9+// window-less on purpose: ermitePow is also loaded by Node for the tests
10+// (jstest/issue-form.test.mjs).
11+(() => {
12+ const BATCH = 2048;
13+ const enc = new TextEncoder();
14+
15+ // leadingZeroBits counts the zero bits at the start of the bytes.
16+ function leadingZeroBits(bytes) {
17+ let n = 0;
18+ for (const b of bytes) {
19+ if (b === 0) { n += 8; continue; }
20+ return n + Math.clz32(b) - 24;
21+ }
22+ return n;
23+ }
24+
25+ // b64urlDecode turns unpadded base64url into bytes.
26+ function b64urlDecode(s) {
27+ const bin = atob(String(s).replace(/-/g, '+').replace(/_/g, '/'));
28+ const out = new Uint8Array(bin.length);
29+ for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
30+ return out;
31+ }
32+
33+ // input is nonce || ":" || decimal(n), the bytes the server hashes.
34+ function input(nonce, n) {
35+ const tail = enc.encode(':' + n);
36+ const out = new Uint8Array(nonce.length + tail.length);
37+ out.set(nonce, 0);
38+ out.set(tail, nonce.length);
39+ return out;
40+ }
41+
42+ // solve returns the smallest solution as a decimal string. digest is
43+ // (Uint8Array) => Promise<ArrayBuffer-like>, crypto.subtle.digest by default.
44+ // Hashes are asked a batch at a time, in order, so the first hit of the first
45+ // batch that has one is the smallest. onProgress(tried) is called after each
46+ // batch; it may return true to stop (solve then returns null).
47+ async function solve(nonce, difficulty, { digest, batch = BATCH, onProgress } = {}) {
48+ const dig = digest || ((d) => globalThis.crypto.subtle.digest('SHA-256', d));
49+ for (let start = 0; ; start += batch) {
50+ const sums = await Promise.all(Array.from({ length: batch }, async (_, i) => new Uint8Array(await dig(input(nonce, start + i)))));
51+ for (let i = 0; i < batch; i++) {
52+ if (leadingZeroBits(sums[i]) >= difficulty) return String(start + i);
53+ }
54+ if (onProgress && onProgress(start + batch)) return null;
55+ }
56+ }
57+
58+ globalThis.ermitePow = { leadingZeroBits, b64urlDecode, input, solve };
59+
60+ if (typeof WorkerGlobalScope !== 'undefined' && typeof globalThis.postMessage === 'function') {
61+ globalThis.onmessage = async (e) => {
62+ try {
63+ if (!globalThis.crypto || !globalThis.crypto.subtle) { postMessage({ error: 'nocrypto' }); return; }
64+ const { nonce, difficulty } = e.data;
65+ const solution = await solve(b64urlDecode(nonce), difficulty, { onProgress: (tried) => { postMessage({ progress: tried }); } });
66+ postMessage({ solution });
67+ } catch (err) {
68+ postMessage({ error: String(err && err.message || err) });
69+ }
70+ };
71+ }
72+})();
M internal/adapters/web/view.go +2 -0
@@ -69,6 +69,8 @@ type Layout struct {
6969 Passkeys bool
7070 // MR loads the script of the merge request page (mr.js).
7171 MR bool
72+ // IssueForm loads the script of the public issue form (issue-form.js).
73+ IssueForm bool
7274 }
7375
7476 // Link is a plain label + href pair.
@@ -69,6 +69,8 @@ type Layout struct {
69 Passkeys bool69 Passkeys bool
70 // MR loads the script of the merge request page (mr.js).70 // MR loads the script of the merge request page (mr.js).
71 MR bool71 MR bool
72+ // IssueForm loads the script of the public issue form (issue-form.js).
73+ IssueForm bool
72 }74 }
73 75
74 // Link is a plain label + href pair.76 // Link is a plain label + href pair.

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 43

↑↓ moveenter openesc close