detached ab8e178

feat(web): UI polish, auto sign-in from the link, installable app (#46)

commit
ab8e17813f0cb8c0a644ebf460cdb8a21db87fb5
author
brice · · 2026-10-17 14:02 UTC
committer
bnema · · 2026-10-17 14:02 UTC
parent
924eaca
signature
SSH UnverifiedSHA256:VOCA5lYARHcbKDZ7GhTV45VmcjWnJ6ZQIBazj17LivA The key is unknown
refs
tag: v0.17.0
66 files · +776 -451
M README.md +2 -2
@@ -27,7 +27,7 @@ ermite serve
2727 | `ERMITE_LOG_FORMAT` | `json` | `json` or `console` |
2828 | `ERMITE_COMMITTER_NAME` | `ermite` | committer of squash merges |
2929 | `ERMITE_COMMITTER_EMAIL` | `ermite@<public host>` | committer email of squash merges |
30-| `ERMITE_OWNER` | `owner` | owner login that CI shows in `github.repository` (`<owner>/<repo>`) and `github.repository_owner`; follows the rules of repository names |
30+| `ERMITE_OWNER` | `owner` | owner login: shown in the web header when signed in, named in new passkeys, and the one CI shows in `github.repository` (`<owner>/<repo>`) and `github.repository_owner`; follows the rules of repository names |
3131 | `ERMITE_CI_LOG_RETENTION` | `90d` | how long CI runs, jobs, checks and logs are kept: `Nd` (days) or a Go duration such as `720h`, at least one hour |
3232 | `ERMITE_SECRETS_KEY` | empty | 32 random bytes in base64 (`head -c 32 /dev/urandom \| base64`) that encrypt CI secrets at rest (AES-256-GCM); keep it in the deployment secret store like `ERMITE_ADMIN_TOKEN`. Without it `ermite secret set` fails and jobs receive no secrets (variables and environments still work). The server refuses to start with another key than the one the stored secrets were sealed with; losing the key loses every secret (set them again), a backup without the key reveals none |
3333 | `ERMITE_CI_ARTIFACTS` | `1` | `0` turns the CI artifacts off: no artifact service, and artifact actions fail as on a GitHub Enterprise Server without artifacts v4 |
@@ -168,7 +168,7 @@ The cache (`actions/cache`, `setup-go`/`setup-node` `cache:`) is served by the r
168168 Two ways in, both for the owner, both giving the same session cookie for 12 hours. Neither works over plain `http` except on localhost: the cookie is `Secure`.
169169
170170 - **Passkey** (a phone, a laptop, a security key): open `/-/login` and press "Sign in with a passkey". Nothing to type, no terminal. A passkey is added once from `/-/passkeys` while signed in.
171-- **Link**: `ermite web login`, run with the admin token or a token with every scope, prints a link that works once within 60 seconds. Opening it and confirming signs the browser in (never longer than the token that asked for the link). It is also how to sign in the first time, before any passkey exists.
171+- **Link**: `ermite web login`, run with the admin token or a token with every scope, prints a link that works once within 60 seconds. Opening it signs the browser in (without scripts, click the button on the page) (never longer than the token that asked for the link). It is also how to sign in the first time, before any passkey exists.
172172
173173 Passkeys need `ERMITE_PUBLIC_URL` to be `https` (or `http://localhost`) with a **host name, not an IP address**: WebAuthn ties a passkey to that name, and a passkey registered for one name does not work on another. On `http://127.0.0.1:…` the server starts without passkeys (the log says `passkeys disabled`, the routes and links are absent, `ermite version` does not list `passkeys`). Up to 20 passkeys can be registered; each asks for user verification (a fingerprint, a face or a PIN), and the server stores only the public key. A security key that reports a signature counter is checked for cloning; synced passkeys (iCloud, Google, a password manager) report none, so they are not.
174174
@@ -27,7 +27,7 @@ ermite serve
27 | `ERMITE_LOG_FORMAT` | `json` | `json` or `console` |27 | `ERMITE_LOG_FORMAT` | `json` | `json` or `console` |
28 | `ERMITE_COMMITTER_NAME` | `ermite` | committer of squash merges |28 | `ERMITE_COMMITTER_NAME` | `ermite` | committer of squash merges |
29 | `ERMITE_COMMITTER_EMAIL` | `ermite@<public host>` | committer email of squash merges |29 | `ERMITE_COMMITTER_EMAIL` | `ermite@<public host>` | committer email of squash merges |
30-| `ERMITE_OWNER` | `owner` | owner login that CI shows in `github.repository` (`<owner>/<repo>`) and `github.repository_owner`; follows the rules of repository names |30+| `ERMITE_OWNER` | `owner` | owner login: shown in the web header when signed in, named in new passkeys, and the one CI shows in `github.repository` (`<owner>/<repo>`) and `github.repository_owner`; follows the rules of repository names |
31 | `ERMITE_CI_LOG_RETENTION` | `90d` | how long CI runs, jobs, checks and logs are kept: `Nd` (days) or a Go duration such as `720h`, at least one hour |31 | `ERMITE_CI_LOG_RETENTION` | `90d` | how long CI runs, jobs, checks and logs are kept: `Nd` (days) or a Go duration such as `720h`, at least one hour |
32 | `ERMITE_SECRETS_KEY` | empty | 32 random bytes in base64 (`head -c 32 /dev/urandom \| base64`) that encrypt CI secrets at rest (AES-256-GCM); keep it in the deployment secret store like `ERMITE_ADMIN_TOKEN`. Without it `ermite secret set` fails and jobs receive no secrets (variables and environments still work). The server refuses to start with another key than the one the stored secrets were sealed with; losing the key loses every secret (set them again), a backup without the key reveals none |32 | `ERMITE_SECRETS_KEY` | empty | 32 random bytes in base64 (`head -c 32 /dev/urandom \| base64`) that encrypt CI secrets at rest (AES-256-GCM); keep it in the deployment secret store like `ERMITE_ADMIN_TOKEN`. Without it `ermite secret set` fails and jobs receive no secrets (variables and environments still work). The server refuses to start with another key than the one the stored secrets were sealed with; losing the key loses every secret (set them again), a backup without the key reveals none |
33 | `ERMITE_CI_ARTIFACTS` | `1` | `0` turns the CI artifacts off: no artifact service, and artifact actions fail as on a GitHub Enterprise Server without artifacts v4 |33 | `ERMITE_CI_ARTIFACTS` | `1` | `0` turns the CI artifacts off: no artifact service, and artifact actions fail as on a GitHub Enterprise Server without artifacts v4 |
@@ -168,7 +168,7 @@ The cache (`actions/cache`, `setup-go`/`setup-node` `cache:`) is served by the r
168 Two ways in, both for the owner, both giving the same session cookie for 12 hours. Neither works over plain `http` except on localhost: the cookie is `Secure`.168 Two ways in, both for the owner, both giving the same session cookie for 12 hours. Neither works over plain `http` except on localhost: the cookie is `Secure`.
169 169
170 - **Passkey** (a phone, a laptop, a security key): open `/-/login` and press "Sign in with a passkey". Nothing to type, no terminal. A passkey is added once from `/-/passkeys` while signed in.170 - **Passkey** (a phone, a laptop, a security key): open `/-/login` and press "Sign in with a passkey". Nothing to type, no terminal. A passkey is added once from `/-/passkeys` while signed in.
171-- **Link**: `ermite web login`, run with the admin token or a token with every scope, prints a link that works once within 60 seconds. Opening it and confirming signs the browser in (never longer than the token that asked for the link). It is also how to sign in the first time, before any passkey exists.171+- **Link**: `ermite web login`, run with the admin token or a token with every scope, prints a link that works once within 60 seconds. Opening it signs the browser in (without scripts, click the button on the page) (never longer than the token that asked for the link). It is also how to sign in the first time, before any passkey exists.
172 172
173 Passkeys need `ERMITE_PUBLIC_URL` to be `https` (or `http://localhost`) with a **host name, not an IP address**: WebAuthn ties a passkey to that name, and a passkey registered for one name does not work on another. On `http://127.0.0.1:…` the server starts without passkeys (the log says `passkeys disabled`, the routes and links are absent, `ermite version` does not list `passkeys`). Up to 20 passkeys can be registered; each asks for user verification (a fingerprint, a face or a PIN), and the server stores only the public key. A security key that reports a signature counter is checked for cloning; synced passkeys (iCloud, Google, a password manager) report none, so they are not.173 Passkeys need `ERMITE_PUBLIC_URL` to be `https` (or `http://localhost`) with a **host name, not an IP address**: WebAuthn ties a passkey to that name, and a passkey registered for one name does not work on another. On `http://127.0.0.1:…` the server starts without passkeys (the log says `passkeys disabled`, the routes and links are absent, `ermite version` does not list `passkeys`). Up to 20 passkeys can be registered; each asks for user verification (a fingerprint, a face or a PIN), and the server stores only the public key. A security key that reports a signature counter is checked for cloning; synced passkeys (iCloud, Google, a password manager) report none, so they are not.
174 174
M internal/adapters/web/ASSETS.md +2 -1
@@ -16,7 +16,8 @@ content-hashed names. Nothing is fetched at runtime.
1616 | `static/css/*.css` | native CSS, served as one stylesheet in name order; relative `url(fonts/…)` and `url(icons/…)` become hashed URLs | written here | |
1717 | `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` |
1818 | `static/fonts/atkinson-mono.woff2` | Atkinson Hyperlegible Mono, variable weight, latin subset (SIL OFL 1.1, `static/fonts/OFL-mono.txt`) | Google Fonts | `2706b1ee4f452e744ea91f7e4908cbde9c5d35521bf5ffffc71a382a2de89613` |
19-| `static/icons/*.svg` | interface icons, used as CSS masks; `brand.svg` is a grey picture shown as is | written here | |
19+| `static/icons/*.svg` | interface icons, used as CSS masks; `brand.svg` is a grey picture shown as is. Only the icons in use are copied, one file each: `branch`, `search`, `tag`, `file`, `user`, `key`, `log-in`, `log-out` are Lucide 1.8 (`git-branch`, `search`, `tag`, `file-text`, `user-round`, `key-round`, `log-in`, `log-out`; ISC, https://lucide.dev/license) | Lucide; `brand.svg` written here | |
20+| `static/app/*.png` | icons of the installed web app (manifest, `apple-touch-icon`), rendered from `brand.svg` with resvg; the maskable and Apple icons have the picture in the safe zone on `#181818` | generated here | |
2021 | `static/rules.json` | speculation rules: prefetch the light same-origin pages on hover (`Speculation-Rules` header); the heavy ones (tree, blob, commit, commits, compare, refs, raw, MR "since") are excluded, each costs several Git reads on the server | written here | |
2122
2223 The three htmx files are the exact files the approved mockup was built with.
@@ -16,7 +16,8 @@ content-hashed names. Nothing is fetched at runtime.
16 | `static/css/*.css` | native CSS, served as one stylesheet in name order; relative `url(fonts/…)` and `url(icons/…)` become hashed URLs | written here | |16 | `static/css/*.css` | native CSS, served as one stylesheet in name order; relative `url(fonts/…)` and `url(icons/…)` become hashed URLs | written here | |
17 | `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` |17 | `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` |
18 | `static/fonts/atkinson-mono.woff2` | Atkinson Hyperlegible Mono, variable weight, latin subset (SIL OFL 1.1, `static/fonts/OFL-mono.txt`) | Google Fonts | `2706b1ee4f452e744ea91f7e4908cbde9c5d35521bf5ffffc71a382a2de89613` |18 | `static/fonts/atkinson-mono.woff2` | Atkinson Hyperlegible Mono, variable weight, latin subset (SIL OFL 1.1, `static/fonts/OFL-mono.txt`) | Google Fonts | `2706b1ee4f452e744ea91f7e4908cbde9c5d35521bf5ffffc71a382a2de89613` |
19-| `static/icons/*.svg` | interface icons, used as CSS masks; `brand.svg` is a grey picture shown as is | written here | |19+| `static/icons/*.svg` | interface icons, used as CSS masks; `brand.svg` is a grey picture shown as is. Only the icons in use are copied, one file each: `branch`, `search`, `tag`, `file`, `user`, `key`, `log-in`, `log-out` are Lucide 1.8 (`git-branch`, `search`, `tag`, `file-text`, `user-round`, `key-round`, `log-in`, `log-out`; ISC, https://lucide.dev/license) | Lucide; `brand.svg` written here | |
20+| `static/app/*.png` | icons of the installed web app (manifest, `apple-touch-icon`), rendered from `brand.svg` with resvg; the maskable and Apple icons have the picture in the safe zone on `#181818` | generated here | |
20 | `static/rules.json` | speculation rules: prefetch the light same-origin pages on hover (`Speculation-Rules` header); the heavy ones (tree, blob, commit, commits, compare, refs, raw, MR "since") are excluded, each costs several Git reads on the server | written here | |21 | `static/rules.json` | speculation rules: prefetch the light same-origin pages on hover (`Speculation-Rules` header); the heavy ones (tree, blob, commit, commits, compare, refs, raw, MR "since") are excluded, each costs several Git reads on the server | written here | |
21 22
22 The three htmx files are the exact files the approved mockup was built with.23 The three htmx files are the exact files the approved mockup was built with.
M internal/adapters/web/assets.go +68 -0
@@ -6,6 +6,7 @@ import (
66 "crypto/sha256"
77 "embed"
88 "encoding/hex"
9+ "encoding/json"
910 "errors"
1011 "io/fs"
1112 "net/http"
@@ -24,6 +25,9 @@ const StaticPrefix = "/-/static/"
2425 // the content, so they can be cached for ever.
2526 type Assets struct {
2627 CSS, HTMX, Live, SSE, App, Files, Passkey, Log, Icon string
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.
30+ Manifest, AppleIcon string
2731 // Font is the UI font, preloaded by every page.
2832 Font string
2933 // Rules is the speculation rules file named by the Speculation-Rules header.
@@ -146,9 +150,73 @@ func loadAssets() (*assetSet, error) {
146150 return nil, err
147151 }
148152 s.urls.Icon = put("img", "icon.svg", icon, "image/svg+xml")
153+
154+ if err := s.loadApp(put); err != nil {
155+ return nil, err
156+ }
149157 return s, nil
150158 }
151159
160+// appIcons are the PNG icons of the installed app (static/app), in the
161+// manifest; purpose "maskable" has the picture inside the safe zone on a
162+// full-bleed background, for launchers that crop to a circle or a squircle.
163+var appIcons = []struct{ file, sizes, purpose string }{
164+ {"icon-192.png", "192x192", "any"},
165+ {"icon-512.png", "512x512", "any"},
166+ {"icon-maskable-512.png", "512x512", "maskable"},
167+}
168+
169+// webManifest is the shape of the web app manifest: the minimum a browser
170+// asks for to offer "install" / "add to home screen".
171+type webManifest struct {
172+ Name string `json:"name"`
173+ ShortName string `json:"short_name"`
174+ StartURL string `json:"start_url"`
175+ Scope string `json:"scope"`
176+ Display string `json:"display"`
177+ BackgroundColor string `json:"background_color"`
178+ ThemeColor string `json:"theme_color"`
179+ Icons []manifestIcon `json:"icons"`
180+}
181+
182+type manifestIcon struct {
183+ Src string `json:"src"`
184+ Sizes string `json:"sizes"`
185+ Type string `json:"type"`
186+ Purpose string `json:"purpose"`
187+}
188+
189+// appColor is the dark canvas: the splash screen and the bar of the installed
190+// app (the pages follow the system theme; the manifest takes one colour).
191+const appColor = "#050505"
192+
193+// loadApp adds the app icons and the manifest, which names them by their
194+// hashed URLs.
195+func (s *assetSet) loadApp(put func(dir, name string, b []byte, typ string) string) error {
196+ m := webManifest{
197+ Name: "ermite", ShortName: "ermite", StartURL: "/", Scope: "/", Display: "standalone",
198+ BackgroundColor: appColor, ThemeColor: appColor,
199+ }
200+ for _, ic := range appIcons {
201+ b, err := staticFS.ReadFile("static/app/" + ic.file)
202+ if err != nil {
203+ return err
204+ }
205+ m.Icons = append(m.Icons, manifestIcon{Src: put("app", ic.file, b, "image/png"), Sizes: ic.sizes, Type: "image/png", Purpose: ic.purpose})
206+ }
207+ apple, err := staticFS.ReadFile("static/app/apple-touch-icon.png")
208+ if err != nil {
209+ return err
210+ }
211+ s.urls.AppleIcon = put("app", "apple-touch-icon.png", apple, "image/png")
212+ b, err := json.Marshal(m)
213+ if err != nil {
214+ return err
215+ }
216+ s.urls.Manifest = put("app", "manifest.webmanifest", b, "application/manifest+json")
217+ return nil
218+}
219+
152220 // serve answers GET /-/static/<hashed name>. A name that does not match a
153221 // current asset is a 404, so a stale hash never serves the wrong content.
154222 func (s *assetSet) serve(w http.ResponseWriter, r *http.Request) {
@@ -6,6 +6,7 @@ import (
6 "crypto/sha256"6 "crypto/sha256"
7 "embed"7 "embed"
8 "encoding/hex"8 "encoding/hex"
9+ "encoding/json"
9 "errors"10 "errors"
10 "io/fs"11 "io/fs"
11 "net/http"12 "net/http"
@@ -24,6 +25,9 @@ const StaticPrefix = "/-/static/"
24 // the content, so they can be cached for ever.25 // the content, so they can be cached for ever.
25 type Assets struct {26 type Assets struct {
26 CSS, HTMX, Live, SSE, App, Files, Passkey, Log, Icon string27 CSS, HTMX, Live, SSE, App, Files, Passkey, Log, Icon string
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.
30+ Manifest, AppleIcon string
27 // Font is the UI font, preloaded by every page.31 // Font is the UI font, preloaded by every page.
28 Font string32 Font string
29 // Rules is the speculation rules file named by the Speculation-Rules header.33 // Rules is the speculation rules file named by the Speculation-Rules header.
@@ -146,9 +150,73 @@ func loadAssets() (*assetSet, error) {
146 return nil, err150 return nil, err
147 }151 }
148 s.urls.Icon = put("img", "icon.svg", icon, "image/svg+xml")152 s.urls.Icon = put("img", "icon.svg", icon, "image/svg+xml")
153+
154+ if err := s.loadApp(put); err != nil {
155+ return nil, err
156+ }
149 return s, nil157 return s, nil
150 }158 }
151 159
160+// appIcons are the PNG icons of the installed app (static/app), in the
161+// manifest; purpose "maskable" has the picture inside the safe zone on a
162+// full-bleed background, for launchers that crop to a circle or a squircle.
163+var appIcons = []struct{ file, sizes, purpose string }{
164+ {"icon-192.png", "192x192", "any"},
165+ {"icon-512.png", "512x512", "any"},
166+ {"icon-maskable-512.png", "512x512", "maskable"},
167+}
168+
169+// webManifest is the shape of the web app manifest: the minimum a browser
170+// asks for to offer "install" / "add to home screen".
171+type webManifest struct {
172+ Name string `json:"name"`
173+ ShortName string `json:"short_name"`
174+ StartURL string `json:"start_url"`
175+ Scope string `json:"scope"`
176+ Display string `json:"display"`
177+ BackgroundColor string `json:"background_color"`
178+ ThemeColor string `json:"theme_color"`
179+ Icons []manifestIcon `json:"icons"`
180+}
181+
182+type manifestIcon struct {
183+ Src string `json:"src"`
184+ Sizes string `json:"sizes"`
185+ Type string `json:"type"`
186+ Purpose string `json:"purpose"`
187+}
188+
189+// appColor is the dark canvas: the splash screen and the bar of the installed
190+// app (the pages follow the system theme; the manifest takes one colour).
191+const appColor = "#050505"
192+
193+// loadApp adds the app icons and the manifest, which names them by their
194+// hashed URLs.
195+func (s *assetSet) loadApp(put func(dir, name string, b []byte, typ string) string) error {
196+ m := webManifest{
197+ Name: "ermite", ShortName: "ermite", StartURL: "/", Scope: "/", Display: "standalone",
198+ BackgroundColor: appColor, ThemeColor: appColor,
199+ }
200+ for _, ic := range appIcons {
201+ b, err := staticFS.ReadFile("static/app/" + ic.file)
202+ if err != nil {
203+ return err
204+ }
205+ m.Icons = append(m.Icons, manifestIcon{Src: put("app", ic.file, b, "image/png"), Sizes: ic.sizes, Type: "image/png", Purpose: ic.purpose})
206+ }
207+ apple, err := staticFS.ReadFile("static/app/apple-touch-icon.png")
208+ if err != nil {
209+ return err
210+ }
211+ s.urls.AppleIcon = put("app", "apple-touch-icon.png", apple, "image/png")
212+ b, err := json.Marshal(m)
213+ if err != nil {
214+ return err
215+ }
216+ s.urls.Manifest = put("app", "manifest.webmanifest", b, "application/manifest+json")
217+ return nil
218+}
219+
152 // serve answers GET /-/static/<hashed name>. A name that does not match a220 // serve answers GET /-/static/<hashed name>. A name that does not match a
153 // current asset is a 404, so a stale hash never serves the wrong content.221 // current asset is a 404, so a stale hash never serves the wrong content.
154 func (s *assetSet) serve(w http.ResponseWriter, r *http.Request) {222 func (s *assetSet) serve(w http.ResponseWriter, r *http.Request) {
M internal/adapters/web/fragments.templ +5 -4
@@ -82,8 +82,9 @@ templ MRNotice(text, href string) {
8282 }
8383
8484 // Live is the connection to the event stream, in the hint badge. It shows
85-// only once a connection was lost: a page that never connected, or that is
86-// live, says nothing. The events of the stream are fragments that ermiteLive
85+// only once a connection stays lost for a few seconds (ermiteLive.lost): a
86+// page that never connected, that is live, or whose stream comes back right
87+// away (a proxy closing an idle stream) says nothing. The events of the stream are fragments that ermiteLive
8788 // (ermite.js) swaps in the page. A page without a stream renders nothing.
8889 //
8990 // hx-sse retries a lost connection for ever, with a growing delay. A refusal
@@ -97,8 +98,8 @@ templ Live(l Layout) {
9798 hx-sse:connect={ l.LiveURL }
9899 hx-swap="none"
99100 :text="data.state === 'offline' ? 'live updates paused' : ''"
100- hx-on="htmx:sse:after:connection -> data.state = 'live';
101- htmx:sse:close, htmx:sse:error -> data.state = 'offline';
101+ hx-on="htmx:sse:after:connection -> ermiteLive.back();
102+ htmx:sse:close, htmx:sse:error -> ermiteLive.lost();
102103 htmx:sse:error -> ermiteLive.error(event.detail.status);
103104 htmx:sse:before:connection -> ermiteLive.gate(event);
104105 home-list, repo-last-push, refs-list, commits-head, mr-notice, ci-run, ci-job -> ermiteLive.swap(event.detail.data);
@@ -82,8 +82,9 @@ templ MRNotice(text, href string) {
82 }82 }
83 83
84 // Live is the connection to the event stream, in the hint badge. It shows84 // Live is the connection to the event stream, in the hint badge. It shows
85-// only once a connection was lost: a page that never connected, or that is85+// only once a connection stays lost for a few seconds (ermiteLive.lost): a
86-// live, says nothing. The events of the stream are fragments that ermiteLive86+// page that never connected, that is live, or whose stream comes back right
87+// away (a proxy closing an idle stream) says nothing. The events of the stream are fragments that ermiteLive
87 // (ermite.js) swaps in the page. A page without a stream renders nothing.88 // (ermite.js) swaps in the page. A page without a stream renders nothing.
88 //89 //
89 // hx-sse retries a lost connection for ever, with a growing delay. A refusal90 // hx-sse retries a lost connection for ever, with a growing delay. A refusal
@@ -97,8 +98,8 @@ templ Live(l Layout) {
97 hx-sse:connect={ l.LiveURL }98 hx-sse:connect={ l.LiveURL }
98 hx-swap="none"99 hx-swap="none"
99 :text="data.state === 'offline' ? 'live updates paused' : ''"100 :text="data.state === 'offline' ? 'live updates paused' : ''"
100- hx-on="htmx:sse:after:connection -> data.state = 'live';101+ hx-on="htmx:sse:after:connection -> ermiteLive.back();
101- htmx:sse:close, htmx:sse:error -> data.state = 'offline';102+ htmx:sse:close, htmx:sse:error -> ermiteLive.lost();
102 htmx:sse:error -> ermiteLive.error(event.detail.status);103 htmx:sse:error -> ermiteLive.error(event.detail.status);
103 htmx:sse:before:connection -> ermiteLive.gate(event);104 htmx:sse:before:connection -> ermiteLive.gate(event);
104 home-list, repo-last-push, refs-list, commits-head, mr-notice, ci-run, ci-job -> ermiteLive.swap(event.detail.data);105 home-list, repo-last-push, refs-list, commits-head, mr-notice, ci-run, ci-job -> ermiteLive.swap(event.detail.data);
M internal/adapters/web/fragments_templ.go +5 -4 generated file: collapsed
M internal/adapters/web/fragments_test.go +3 -3
@@ -58,9 +58,9 @@ func TestShellDegradesWithoutScripts(t *testing.T) {
5858 got := renderToString(t, Header(l))
5959 assert.Contains(t, got, `<a class="btn ref" href="/demo/refs" data-fire="refs"`)
6060 assert.NotContains(t, got, "hx-on:click", "no handler is inlined in the header")
61- for _, want := range []string{`class="btn search" data-js`, `class="btn icon" data-js data-fire="help"`} {
62- assert.Contains(t, got, want)
63- }
61+ assert.Contains(t, got, `class="btn search" data-js`)
62+ assert.NotContains(t, got, `data-fire="help"`, "the help button is in the footer only")
63+ assert.Contains(t, renderToString(t, Footer(l)), `class="foot-keys" data-js data-fire="help"`)
6464 }
6565
6666 // Data reaches the scripts through data-* attributes, never spliced into an
@@ -58,9 +58,9 @@ func TestShellDegradesWithoutScripts(t *testing.T) {
58 got := renderToString(t, Header(l))58 got := renderToString(t, Header(l))
59 assert.Contains(t, got, `<a class="btn ref" href="/demo/refs" data-fire="refs"`)59 assert.Contains(t, got, `<a class="btn ref" href="/demo/refs" data-fire="refs"`)
60 assert.NotContains(t, got, "hx-on:click", "no handler is inlined in the header")60 assert.NotContains(t, got, "hx-on:click", "no handler is inlined in the header")
61- for _, want := range []string{`class="btn search" data-js`, `class="btn icon" data-js data-fire="help"`} {61+ assert.Contains(t, got, `class="btn search" data-js`)
62- assert.Contains(t, got, want)62+ assert.NotContains(t, got, `data-fire="help"`, "the help button is in the footer only")
63- }63+ assert.Contains(t, renderToString(t, Footer(l)), `class="foot-keys" data-js data-fire="help"`)
64 }64 }
65 65
66 // Data reaches the scripts through data-* attributes, never spliced into an66 // Data reaches the scripts through data-* attributes, never spliced into an
A internal/adapters/web/jstest/live.test.mjs +40 -0
@@ -0,0 +1,40 @@
1+// Tests of the "live updates paused" delay of static/js/ermite.js
2+// (window.ermiteLive.lost/back) with mocked timers. Only the ermiteLive part of
3+// the file is evaluated: the rest needs a page. Run with
4+// node --test 'internal/adapters/web/jstest/*.test.mjs'
5+import assert from 'node:assert/strict';
6+import { readFileSync } from 'node:fs';
7+import { test } from 'node:test';
8+
9+const src = readFileSync(new URL('../static/js/ermite.js', import.meta.url), 'utf8');
10+const live = src.slice(src.indexOf('window.ermiteLive = '));
11+
12+function setup(t) {
13+ t.mock.timers.enable({ apis: ['setTimeout'] });
14+ const hint = { dataset: { state: 'connecting' } };
15+ globalThis.window = {};
16+ globalThis.document = { querySelector: (sel) => (sel === '.hint-live' ? hint : null) };
17+ new Function(live)();
18+ return { live: globalThis.window.ermiteLive, hint };
19+}
20+
21+test('a drop shorter than the delay says nothing', (t) => {
22+ const { live, hint } = setup(t);
23+ live.back();
24+ live.lost();
25+ t.mock.timers.tick(4000);
26+ live.back();
27+ t.mock.timers.tick(10000);
28+ assert.equal(hint.dataset.state, 'live');
29+});
30+
31+test('a drop that lasts shows offline, once, until the stream is back', (t) => {
32+ const { live, hint } = setup(t);
33+ live.lost();
34+ t.mock.timers.tick(3000);
35+ live.lost(); // a second error does not restart the delay
36+ t.mock.timers.tick(2000);
37+ assert.equal(hint.dataset.state, 'offline');
38+ live.back();
39+ assert.equal(hint.dataset.state, 'live');
40+});
@@ -0,0 +1,40 @@
1+// Tests of the "live updates paused" delay of static/js/ermite.js
2+// (window.ermiteLive.lost/back) with mocked timers. Only the ermiteLive part of
3+// the file is evaluated: the rest needs a page. Run with
4+// node --test 'internal/adapters/web/jstest/*.test.mjs'
5+import assert from 'node:assert/strict';
6+import { readFileSync } from 'node:fs';
7+import { test } from 'node:test';
8+
9+const src = readFileSync(new URL('../static/js/ermite.js', import.meta.url), 'utf8');
10+const live = src.slice(src.indexOf('window.ermiteLive = '));
11+
12+function setup(t) {
13+ t.mock.timers.enable({ apis: ['setTimeout'] });
14+ const hint = { dataset: { state: 'connecting' } };
15+ globalThis.window = {};
16+ globalThis.document = { querySelector: (sel) => (sel === '.hint-live' ? hint : null) };
17+ new Function(live)();
18+ return { live: globalThis.window.ermiteLive, hint };
19+}
20+
21+test('a drop shorter than the delay says nothing', (t) => {
22+ const { live, hint } = setup(t);
23+ live.back();
24+ live.lost();
25+ t.mock.timers.tick(4000);
26+ live.back();
27+ t.mock.timers.tick(10000);
28+ assert.equal(hint.dataset.state, 'live');
29+});
30+
31+test('a drop that lasts shows offline, once, until the stream is back', (t) => {
32+ const { live, hint } = setup(t);
33+ live.lost();
34+ t.mock.timers.tick(3000);
35+ live.lost(); // a second error does not restart the delay
36+ t.mock.timers.tick(2000);
37+ assert.equal(hint.dataset.state, 'offline');
38+ live.back();
39+ assert.equal(hint.dataset.state, 'live');
40+});
A internal/adapters/web/manifest_test.go +40 -0
@@ -0,0 +1,40 @@
1+// SPDX-License-Identifier: AGPL-3.0-or-later
2+
3+package web
4+
5+import (
6+ "encoding/json"
7+ "strings"
8+ "testing"
9+
10+ "github.com/stretchr/testify/assert"
11+ "github.com/stretchr/testify/require"
12+)
13+
14+// The web app manifest makes the site installable: every page links it, and
15+// its icons are served assets.
16+func TestWebAppManifest(t *testing.T) {
17+ t.Parallel()
18+ srv := newBareServer(t)
19+ u := srv.assets.urls
20+ man, ok := srv.assets.byName[strings.TrimPrefix(u.Manifest, StaticPrefix)]
21+ require.True(t, ok)
22+ assert.Equal(t, "application/manifest+json", man.typ)
23+
24+ var m webManifest
25+ require.NoError(t, json.Unmarshal(man.body, &m))
26+ assert.Equal(t, "standalone", m.Display)
27+ assert.Equal(t, "/", m.StartURL)
28+ require.Len(t, m.Icons, len(appIcons))
29+ for _, ic := range m.Icons {
30+ a, ok := srv.assets.byName[strings.TrimPrefix(ic.Src, StaticPrefix)]
31+ require.True(t, ok, ic.Src)
32+ assert.Equal(t, "image/png", a.typ)
33+ }
34+ _, ok = srv.assets.byName[strings.TrimPrefix(u.AppleIcon, StaticPrefix)]
35+ assert.True(t, ok)
36+
37+ page := renderToString(t, Shell(Layout{Assets: u}, nil))
38+ assert.Contains(t, page, `<link rel="manifest" href="`+u.Manifest+`">`)
39+ assert.Contains(t, page, `<link rel="apple-touch-icon" href="`+u.AppleIcon+`">`)
40+}
@@ -0,0 +1,40 @@
1+// SPDX-License-Identifier: AGPL-3.0-or-later
2+
3+package web
4+
5+import (
6+ "encoding/json"
7+ "strings"
8+ "testing"
9+
10+ "github.com/stretchr/testify/assert"
11+ "github.com/stretchr/testify/require"
12+)
13+
14+// The web app manifest makes the site installable: every page links it, and
15+// its icons are served assets.
16+func TestWebAppManifest(t *testing.T) {
17+ t.Parallel()
18+ srv := newBareServer(t)
19+ u := srv.assets.urls
20+ man, ok := srv.assets.byName[strings.TrimPrefix(u.Manifest, StaticPrefix)]
21+ require.True(t, ok)
22+ assert.Equal(t, "application/manifest+json", man.typ)
23+
24+ var m webManifest
25+ require.NoError(t, json.Unmarshal(man.body, &m))
26+ assert.Equal(t, "standalone", m.Display)
27+ assert.Equal(t, "/", m.StartURL)
28+ require.Len(t, m.Icons, len(appIcons))
29+ for _, ic := range m.Icons {
30+ a, ok := srv.assets.byName[strings.TrimPrefix(ic.Src, StaticPrefix)]
31+ require.True(t, ok, ic.Src)
32+ assert.Equal(t, "image/png", a.typ)
33+ }
34+ _, ok = srv.assets.byName[strings.TrimPrefix(u.AppleIcon, StaticPrefix)]
35+ assert.True(t, ok)
36+
37+ page := renderToString(t, Shell(Layout{Assets: u}, nil))
38+ assert.Contains(t, page, `<link rel="manifest" href="`+u.Manifest+`">`)
39+ assert.Contains(t, page, `<link rel="apple-touch-icon" href="`+u.AppleIcon+`">`)
40+}
M internal/adapters/web/pages.templ +6 -3
@@ -170,8 +170,11 @@ templ State(l Layout, p StatePage) {
170170 }
171171 }
172172
173-// Login asks the owner to confirm a login link. Opening the link consumes
174-// nothing; only this form does. It is a state page with a button.
173+// Login signs the browser in with a login link. Opening the link consumes
174+// nothing; only this form does. ermite.js submits it as soon as a browser
175+// shows the page (data-autosubmit): link previews and prefetchers run no
176+// script, so they still cannot use the link. Without scripts the owner
177+// clicks the button.
175178 templ Login(l Layout, p LoginPage) {
176179 @Shell(l, nil) {
177180 <section class="state" aria-labelledby="state-title">
@@ -179,7 +182,7 @@ templ Login(l Layout, p LoginPage) {
179182 for _, line := range p.Lines {
180183 <p class="dim">{ line }</p>
181184 }
182- <form class="state-act" method="post" action={ templ.SafeURL(p.Action) }>
185+ <form class="state-act" method="post" action={ templ.SafeURL(p.Action) } data-autosubmit>
183186 <button type="submit" class="btn primary">{ p.Button }</button>
184187 </form>
185188 <p class="state-back"><a href="/">Back to home</a></p>
@@ -170,8 +170,11 @@ templ State(l Layout, p StatePage) {
170 }170 }
171 }171 }
172 172
173-// Login asks the owner to confirm a login link. Opening the link consumes173+// Login signs the browser in with a login link. Opening the link consumes
174-// nothing; only this form does. It is a state page with a button.174+// nothing; only this form does. ermite.js submits it as soon as a browser
175+// shows the page (data-autosubmit): link previews and prefetchers run no
176+// script, so they still cannot use the link. Without scripts the owner
177+// clicks the button.
175 templ Login(l Layout, p LoginPage) {178 templ Login(l Layout, p LoginPage) {
176 @Shell(l, nil) {179 @Shell(l, nil) {
177 <section class="state" aria-labelledby="state-title">180 <section class="state" aria-labelledby="state-title">
@@ -179,7 +182,7 @@ templ Login(l Layout, p LoginPage) {
179 for _, line := range p.Lines {182 for _, line := range p.Lines {
180 <p class="dim">{ line }</p>183 <p class="dim">{ line }</p>
181 }184 }
182- <form class="state-act" method="post" action={ templ.SafeURL(p.Action) }>185+ <form class="state-act" method="post" action={ templ.SafeURL(p.Action) } data-autosubmit>
183 <button type="submit" class="btn primary">{ p.Button }</button>186 <button type="submit" class="btn primary">{ p.Button }</button>
184 </form>187 </form>
185 <p class="state-back"><a href="/">Back to home</a></p>188 <p class="state-back"><a href="/">Back to home</a></p>
M internal/adapters/web/pages_templ.go +26 -23 generated file: collapsed
M internal/adapters/web/passkeys_test.go +6 -4
@@ -484,19 +484,21 @@ func TestPasskeyLoginPage(t *testing.T) {
484484 }
485485
486486 // The header links: "Sign in" for a visitor, "Passkeys" for the owner.
487+// They are icons with an accessible name.
487488 func TestPasskeyHeaderLinks(t *testing.T) {
488489 t.Parallel()
489490 f := newPasskeyServer(t)
490491 rec := f.do(t, req{method: "GET", target: "/-/login"})
491- assert.Contains(t, rec.Body.String(), `href="/-/login"`)
492+ assert.Contains(t, rec.Body.String(), `href="/-/login" aria-label="Sign in"`)
492493 assert.NotContains(t, rec.Body.String(), `href="/-/passkeys"`)
493494 assert.Contains(t, rec.Body.String(), "passkey.", "the script is loaded")
494495
495496 f.passkeys.EXPECT().List(mock.Anything, mock.Anything).Return(nil, nil).Once()
496497 rec = f.do(t, req{method: "GET", target: "/-/passkeys", cookie: "tok"})
497- assert.Contains(t, rec.Body.String(), `href="/-/passkeys"`)
498+ assert.Contains(t, rec.Body.String(), `href="/-/passkeys" aria-label="Passkeys"`)
498499 assert.Contains(t, rec.Body.String(), `action="/-/logout"`)
499- assert.NotContains(t, rec.Body.String(), `>Sign in</a>`)
500+ assert.Contains(t, rec.Body.String(), `aria-label="Sign out"`)
501+ assert.NotContains(t, rec.Body.String(), `aria-label="Sign in"`)
500502 }
501503
502504 // Without the passkey service there is no route, no link and no script
@@ -564,7 +566,7 @@ func TestPasskeyRoutesLeaveLoginLinksAlone(t *testing.T) {
564566 f := newPasskeyServer(t)
565567 rec := f.do(t, req{method: "GET", target: "/-/login/" + nonceA})
566568 assert.Equal(t, http.StatusOK, rec.Code)
567- assert.Contains(t, rec.Body.String(), "sign in as the owner")
569+ assert.Contains(t, rec.Body.String(), "signing in as the owner")
568570 rec = f.do(t, req{method: "POST", target: "/-/login/" + nonceA, origin: forgeOrigin})
569571 assert.Equal(t, http.StatusSeeOther, rec.Code)
570572 }
@@ -484,19 +484,21 @@ func TestPasskeyLoginPage(t *testing.T) {
484 }484 }
485 485
486 // The header links: "Sign in" for a visitor, "Passkeys" for the owner.486 // The header links: "Sign in" for a visitor, "Passkeys" for the owner.
487+// They are icons with an accessible name.
487 func TestPasskeyHeaderLinks(t *testing.T) {488 func TestPasskeyHeaderLinks(t *testing.T) {
488 t.Parallel()489 t.Parallel()
489 f := newPasskeyServer(t)490 f := newPasskeyServer(t)
490 rec := f.do(t, req{method: "GET", target: "/-/login"})491 rec := f.do(t, req{method: "GET", target: "/-/login"})
491- assert.Contains(t, rec.Body.String(), `href="/-/login"`)492+ assert.Contains(t, rec.Body.String(), `href="/-/login" aria-label="Sign in"`)
492 assert.NotContains(t, rec.Body.String(), `href="/-/passkeys"`)493 assert.NotContains(t, rec.Body.String(), `href="/-/passkeys"`)
493 assert.Contains(t, rec.Body.String(), "passkey.", "the script is loaded")494 assert.Contains(t, rec.Body.String(), "passkey.", "the script is loaded")
494 495
495 f.passkeys.EXPECT().List(mock.Anything, mock.Anything).Return(nil, nil).Once()496 f.passkeys.EXPECT().List(mock.Anything, mock.Anything).Return(nil, nil).Once()
496 rec = f.do(t, req{method: "GET", target: "/-/passkeys", cookie: "tok"})497 rec = f.do(t, req{method: "GET", target: "/-/passkeys", cookie: "tok"})
497- assert.Contains(t, rec.Body.String(), `href="/-/passkeys"`)498+ assert.Contains(t, rec.Body.String(), `href="/-/passkeys" aria-label="Passkeys"`)
498 assert.Contains(t, rec.Body.String(), `action="/-/logout"`)499 assert.Contains(t, rec.Body.String(), `action="/-/logout"`)
499- assert.NotContains(t, rec.Body.String(), `>Sign in</a>`)500+ assert.Contains(t, rec.Body.String(), `aria-label="Sign out"`)
501+ assert.NotContains(t, rec.Body.String(), `aria-label="Sign in"`)
500 }502 }
501 503
502 // Without the passkey service there is no route, no link and no script504 // Without the passkey service there is no route, no link and no script
@@ -564,7 +566,7 @@ func TestPasskeyRoutesLeaveLoginLinksAlone(t *testing.T) {
564 f := newPasskeyServer(t)566 f := newPasskeyServer(t)
565 rec := f.do(t, req{method: "GET", target: "/-/login/" + nonceA})567 rec := f.do(t, req{method: "GET", target: "/-/login/" + nonceA})
566 assert.Equal(t, http.StatusOK, rec.Code)568 assert.Equal(t, http.StatusOK, rec.Code)
567- assert.Contains(t, rec.Body.String(), "sign in as the owner")569+ assert.Contains(t, rec.Body.String(), "signing in as the owner")
568 rec = f.do(t, req{method: "POST", target: "/-/login/" + nonceA, origin: forgeOrigin})570 rec = f.do(t, req{method: "POST", target: "/-/login/" + nonceA, origin: forgeOrigin})
569 assert.Equal(t, http.StatusSeeOther, rec.Code)571 assert.Equal(t, http.StatusSeeOther, rec.Code)
570 }572 }
M internal/adapters/web/session.go +8 -7
@@ -23,9 +23,10 @@ import (
2323 // The owner's session on the web (ADR 0018).
2424 //
2525 // A browser signs in by opening the one-time link `ermite web login` printed:
26-// GET /-/login/<nonce> shows a confirmation and consumes nothing, so link
27-// previews and prefetchers cannot use it; POST /-/login/<nonce> consumes the
28-// link and sets the cookie. The cookie holds a signed token; every request
26+// GET /-/login/<nonce> shows a page whose script posts the link at once and
27+// consumes nothing itself, so link previews and prefetchers, which run no
28+// script, cannot use it; POST /-/login/<nonce> consumes the link and sets the
29+// cookie. The cookie holds a signed token; every request
2930 // that carries a valid one runs as the owner, every other as anonymous.
3031 //
3132 // State changes are POST only and pass one gate (ServeHTTP): an Origin equal to
@@ -243,8 +244,8 @@ func (s *Server) secretPage(w http.ResponseWriter, r *http.Request, status int,
243244 s.write(r.Context(), w, r, status, c)
244245 }
245246
246-// loginConfirm is GET /-/login/<nonce>: it shows the confirmation and does not
247-// use the link.
247+// loginConfirm is GET /-/login/<nonce>: it shows the page that signs in (its
248+// script posts the form, see Login) and does not use the link.
248249 func (s *Server) loginConfirm(w http.ResponseWriter, r *http.Request) {
249250 nonce := r.PathValue("nonce")
250251 if !validNonce(nonce) || !s.cfg.Sessions.LoginLinkValid(nonce) {
@@ -253,8 +254,8 @@ func (s *Server) loginConfirm(w http.ResponseWriter, r *http.Request) {
253254 }
254255 l := s.stateLayout("sign in", []Crumb{{Label: "sign in", Current: true}})
255256 s.secretPage(w, r, http.StatusOK, Login(l, LoginPage{
256- Title: "sign in as the owner",
257- Lines: []string{"this browser will be signed in for 12 hours.", "only confirm if you ran `ermite web login` just now."},
257+ Title: "signing in as the owner…",
258+ Lines: []string{"this browser will be signed in for 12 hours.", "if nothing happens, click the button."},
258259 Action: loginPrefix + nonce,
259260 Button: "Sign in",
260261 }))
@@ -23,9 +23,10 @@ import (
23 // The owner's session on the web (ADR 0018).23 // The owner's session on the web (ADR 0018).
24 //24 //
25 // A browser signs in by opening the one-time link `ermite web login` printed:25 // A browser signs in by opening the one-time link `ermite web login` printed:
26-// GET /-/login/<nonce> shows a confirmation and consumes nothing, so link26+// GET /-/login/<nonce> shows a page whose script posts the link at once and
27-// previews and prefetchers cannot use it; POST /-/login/<nonce> consumes the27+// consumes nothing itself, so link previews and prefetchers, which run no
28-// link and sets the cookie. The cookie holds a signed token; every request28+// script, cannot use it; POST /-/login/<nonce> consumes the link and sets the
29+// cookie. The cookie holds a signed token; every request
29 // that carries a valid one runs as the owner, every other as anonymous.30 // that carries a valid one runs as the owner, every other as anonymous.
30 //31 //
31 // State changes are POST only and pass one gate (ServeHTTP): an Origin equal to32 // State changes are POST only and pass one gate (ServeHTTP): an Origin equal to
@@ -243,8 +244,8 @@ func (s *Server) secretPage(w http.ResponseWriter, r *http.Request, status int,
243 s.write(r.Context(), w, r, status, c)244 s.write(r.Context(), w, r, status, c)
244 }245 }
245 246
246-// loginConfirm is GET /-/login/<nonce>: it shows the confirmation and does not247+// loginConfirm is GET /-/login/<nonce>: it shows the page that signs in (its
247-// use the link.248+// script posts the form, see Login) and does not use the link.
248 func (s *Server) loginConfirm(w http.ResponseWriter, r *http.Request) {249 func (s *Server) loginConfirm(w http.ResponseWriter, r *http.Request) {
249 nonce := r.PathValue("nonce")250 nonce := r.PathValue("nonce")
250 if !validNonce(nonce) || !s.cfg.Sessions.LoginLinkValid(nonce) {251 if !validNonce(nonce) || !s.cfg.Sessions.LoginLinkValid(nonce) {
@@ -253,8 +254,8 @@ func (s *Server) loginConfirm(w http.ResponseWriter, r *http.Request) {
253 }254 }
254 l := s.stateLayout("sign in", []Crumb{{Label: "sign in", Current: true}})255 l := s.stateLayout("sign in", []Crumb{{Label: "sign in", Current: true}})
255 s.secretPage(w, r, http.StatusOK, Login(l, LoginPage{256 s.secretPage(w, r, http.StatusOK, Login(l, LoginPage{
256- Title: "sign in as the owner",257+ Title: "signing in as the owner…",
257- Lines: []string{"this browser will be signed in for 12 hours.", "only confirm if you ran `ermite web login` just now."},258+ Lines: []string{"this browser will be signed in for 12 hours.", "if nothing happens, click the button."},
258 Action: loginPrefix + nonce,259 Action: loginPrefix + nonce,
259 Button: "Sign in",260 Button: "Sign in",
260 }))261 }))
M internal/adapters/web/shell.templ +13 -10
@@ -13,7 +13,12 @@ templ Shell(l Layout, rail templ.Component) {
1313 <meta name="viewport" content="width=device-width, initial-scale=1"/>
1414 <meta name="color-scheme" content="light dark"/>
1515 <title>{ l.Title }</title>
16+ <meta name="theme-color" media="(prefers-color-scheme: light)" content="#ffffff"/>
17+ <meta name="theme-color" media="(prefers-color-scheme: dark)" content="#0c0c0c"/>
18+ <meta name="apple-mobile-web-app-title" content="ermite"/>
1619 <link rel="icon" type="image/svg+xml" href={ templ.SafeURL(l.Assets.Icon) }/>
20+ <link rel="apple-touch-icon" href={ templ.SafeURL(l.Assets.AppleIcon) }/>
21+ <link rel="manifest" href={ templ.SafeURL(l.Assets.Manifest) }/>
1722 <link rel="preload" href={ templ.SafeURL(l.Assets.Font) } as="font" type="font/woff2" crossorigin/>
1823 <link rel="stylesheet" href={ templ.SafeURL(l.Assets.CSS) }/>
1924 <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'/>
@@ -60,14 +65,13 @@ templ Shell(l Layout, rail templ.Component) {
6065 </html>
6166 }
6267
63-// Header is the page header: the instance, the path, the ref switcher, the
64-// "go to" palette and the owner; inside a repository, the tabs below.
68+// Header is the page header: the logo (home), the path, the ref switcher,
69+// the "go to" palette and the owner; inside a repository, the tabs below.
6570 templ Header(l Layout) {
6671 <header class="hdr">
6772 <div class="hdr-top">
68- <a class="brand" href="/" data-g="h" title="home (g h)">
73+ <a class="brand" href="/" data-g="h" aria-label="home" title="home (g h)">
6974 <span class="brand-mark" aria-hidden="true"></span>
70- <span class="brand-name">{ l.Instance }</span>
7175 </a>
7276 if len(l.Crumbs) > 0 {
7377 <nav class="crumbs" aria-label="Path">
@@ -121,17 +125,16 @@ templ Header(l Layout) {
121125 <span class="search-label">Go to…</span>
122126 <kbd aria-hidden="true">ctrl k</kbd>
123127 </button>
124- <button type="button" class="btn icon" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" aria-label="Keyboard shortcuts" title="keyboard shortcuts (?)">?</button>
125128 if SignedIn(ctx) {
126129 <form class="owner" method="post" action="/-/logout">
127- <span class="owner-badge" title="signed in as the owner">owner</span>
130+ <span class="owner-badge" title="signed in as the owner"><span class="ico i-user" aria-hidden="true"></span>{ l.Owner }</span>
128131 if l.Passkeys {
129- <a class="btn" href="/-/passkeys" title="add or remove the passkeys that sign you in">Passkeys</a>
132+ <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>
130133 }
131- <button type="submit" class="btn" title="sign out of this browser">Sign out</button>
134+ <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>
132135 </form>
133136 } else if l.Passkeys {
134- <a class="btn" href="/-/login" title="sign in with a passkey">Sign in</a>
137+ <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>
135138 }
136139 </div>
137140 </div>
@@ -162,7 +165,7 @@ templ Footer(l Layout) {
162165 <footer class="foot">
163166 <span>ermite { l.Version }</span>
164167 <a href={ templ.SafeURL(l.SourceURL) } rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a>
165- <span class="foot-keys"><kbd>?</kbd> keyboard shortcuts</span>
168+ <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>
166169 </footer>
167170 }
168171
@@ -13,7 +13,12 @@ templ Shell(l Layout, rail templ.Component) {
13 <meta name="viewport" content="width=device-width, initial-scale=1"/>13 <meta name="viewport" content="width=device-width, initial-scale=1"/>
14 <meta name="color-scheme" content="light dark"/>14 <meta name="color-scheme" content="light dark"/>
15 <title>{ l.Title }</title>15 <title>{ l.Title }</title>
16+ <meta name="theme-color" media="(prefers-color-scheme: light)" content="#ffffff"/>
17+ <meta name="theme-color" media="(prefers-color-scheme: dark)" content="#0c0c0c"/>
18+ <meta name="apple-mobile-web-app-title" content="ermite"/>
16 <link rel="icon" type="image/svg+xml" href={ templ.SafeURL(l.Assets.Icon) }/>19 <link rel="icon" type="image/svg+xml" href={ templ.SafeURL(l.Assets.Icon) }/>
20+ <link rel="apple-touch-icon" href={ templ.SafeURL(l.Assets.AppleIcon) }/>
21+ <link rel="manifest" href={ templ.SafeURL(l.Assets.Manifest) }/>
17 <link rel="preload" href={ templ.SafeURL(l.Assets.Font) } as="font" type="font/woff2" crossorigin/>22 <link rel="preload" href={ templ.SafeURL(l.Assets.Font) } as="font" type="font/woff2" crossorigin/>
18 <link rel="stylesheet" href={ templ.SafeURL(l.Assets.CSS) }/>23 <link rel="stylesheet" href={ templ.SafeURL(l.Assets.CSS) }/>
19 <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'/>24 <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'/>
@@ -60,14 +65,13 @@ templ Shell(l Layout, rail templ.Component) {
60 </html>65 </html>
61 }66 }
62 67
63-// Header is the page header: the instance, the path, the ref switcher, the68+// Header is the page header: the logo (home), the path, the ref switcher,
64-// "go to" palette and the owner; inside a repository, the tabs below.69+// the "go to" palette and the owner; inside a repository, the tabs below.
65 templ Header(l Layout) {70 templ Header(l Layout) {
66 <header class="hdr">71 <header class="hdr">
67 <div class="hdr-top">72 <div class="hdr-top">
68- <a class="brand" href="/" data-g="h" title="home (g h)">73+ <a class="brand" href="/" data-g="h" aria-label="home" title="home (g h)">
69 <span class="brand-mark" aria-hidden="true"></span>74 <span class="brand-mark" aria-hidden="true"></span>
70- <span class="brand-name">{ l.Instance }</span>
71 </a>75 </a>
72 if len(l.Crumbs) > 0 {76 if len(l.Crumbs) > 0 {
73 <nav class="crumbs" aria-label="Path">77 <nav class="crumbs" aria-label="Path">
@@ -121,17 +125,16 @@ templ Header(l Layout) {
121 <span class="search-label">Go to…</span>125 <span class="search-label">Go to…</span>
122 <kbd aria-hidden="true">ctrl k</kbd>126 <kbd aria-hidden="true">ctrl k</kbd>
123 </button>127 </button>
124- <button type="button" class="btn icon" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" aria-label="Keyboard shortcuts" title="keyboard shortcuts (?)">?</button>
125 if SignedIn(ctx) {128 if SignedIn(ctx) {
126 <form class="owner" method="post" action="/-/logout">129 <form class="owner" method="post" action="/-/logout">
127- <span class="owner-badge" title="signed in as the owner">owner</span>130+ <span class="owner-badge" title="signed in as the owner"><span class="ico i-user" aria-hidden="true"></span>{ l.Owner }</span>
128 if l.Passkeys {131 if l.Passkeys {
129- <a class="btn" href="/-/passkeys" title="add or remove the passkeys that sign you in">Passkeys</a>132+ <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>
130 }133 }
131- <button type="submit" class="btn" title="sign out of this browser">Sign out</button>134+ <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>
132 </form>135 </form>
133 } else if l.Passkeys {136 } else if l.Passkeys {
134- <a class="btn" href="/-/login" title="sign in with a passkey">Sign in</a>137+ <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>
135 }138 }
136 </div>139 </div>
137 </div>140 </div>
@@ -162,7 +165,7 @@ templ Footer(l Layout) {
162 <footer class="foot">165 <footer class="foot">
163 <span>ermite { l.Version }</span>166 <span>ermite { l.Version }</span>
164 <a href={ templ.SafeURL(l.SourceURL) } rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a>167 <a href={ templ.SafeURL(l.SourceURL) } rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a>
165- <span class="foot-keys"><kbd>?</kbd> keyboard shortcuts</span>168+ <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>
166 </footer>169 </footer>
167 }170 }
168 171
M internal/adapters/web/shell_templ.go +210 -184
@@ -47,79 +47,79 @@ func Shell(l Layout, rail templ.Component) templ.Component {
4747 if templ_7745c5c3_Err != nil {
4848 return templ_7745c5c3_Err
4949 }
50- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "</title><link rel=\"icon\" type=\"image/svg+xml\" href=\"")
50+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "</title><meta name=\"theme-color\" media=\"(prefers-color-scheme: light)\" content=\"#ffffff\"><meta name=\"theme-color\" media=\"(prefers-color-scheme: dark)\" content=\"#0c0c0c\"><meta name=\"apple-mobile-web-app-title\" content=\"ermite\"><link rel=\"icon\" type=\"image/svg+xml\" href=\"")
5151 if templ_7745c5c3_Err != nil {
5252 return templ_7745c5c3_Err
5353 }
5454 var templ_7745c5c3_Var3 templ.SafeURL
5555 templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(l.Assets.Icon))
5656 if templ_7745c5c3_Err != nil {
57- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 16, Col: 76}
57+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 19, Col: 76}
5858 }
5959 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
6060 if templ_7745c5c3_Err != nil {
6161 return templ_7745c5c3_Err
6262 }
63- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\"><link rel=\"preload\" href=\"")
63+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\"><link rel=\"apple-touch-icon\" href=\"")
6464 if templ_7745c5c3_Err != nil {
6565 return templ_7745c5c3_Err
6666 }
6767 var templ_7745c5c3_Var4 templ.SafeURL
68- templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(l.Assets.Font))
68+ templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(l.Assets.AppleIcon))
6969 if templ_7745c5c3_Err != nil {
70- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 17, Col: 58}
70+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 20, Col: 72}
7171 }
7272 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
7373 if templ_7745c5c3_Err != nil {
7474 return templ_7745c5c3_Err
7575 }
76- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "\" as=\"font\" type=\"font/woff2\" crossorigin><link rel=\"stylesheet\" href=\"")
76+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "\"><link rel=\"manifest\" href=\"")
7777 if templ_7745c5c3_Err != nil {
7878 return templ_7745c5c3_Err
7979 }
8080 var templ_7745c5c3_Var5 templ.SafeURL
81- templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(l.Assets.CSS))
81+ templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(l.Assets.Manifest))
8282 if templ_7745c5c3_Err != nil {
83- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 18, Col: 60}
83+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 21, Col: 63}
8484 }
8585 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
8686 if templ_7745c5c3_Err != nil {
8787 return templ_7745c5c3_Err
8888 }
89- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "\"><meta name=\"htmx-config\" content='{\"history\":false,\"includeIndicatorCSS\":false,\"extensions\":\"sse, hx-live\"}'><script src=\"")
89+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "\"><link rel=\"preload\" href=\"")
9090 if templ_7745c5c3_Err != nil {
9191 return templ_7745c5c3_Err
9292 }
93- var templ_7745c5c3_Var6 string
94- templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.ResolveAttributeValue(l.Assets.HTMX)
93+ var templ_7745c5c3_Var6 templ.SafeURL
94+ templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(l.Assets.Font))
9595 if templ_7745c5c3_Err != nil {
96- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 20, Col: 30}
96+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 22, Col: 58}
9797 }
98- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var6)
98+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
9999 if templ_7745c5c3_Err != nil {
100100 return templ_7745c5c3_Err
101101 }
102- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "\" defer></script><script src=\"")
102+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "\" as=\"font\" type=\"font/woff2\" crossorigin><link rel=\"stylesheet\" href=\"")
103103 if templ_7745c5c3_Err != nil {
104104 return templ_7745c5c3_Err
105105 }
106- var templ_7745c5c3_Var7 string
107- templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.ResolveAttributeValue(l.Assets.Live)
106+ var templ_7745c5c3_Var7 templ.SafeURL
107+ templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(l.Assets.CSS))
108108 if templ_7745c5c3_Err != nil {
109- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 21, Col: 30}
109+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 23, Col: 60}
110110 }
111- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var7)
111+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
112112 if templ_7745c5c3_Err != nil {
113113 return templ_7745c5c3_Err
114114 }
115- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "\" defer></script><script src=\"")
115+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "\"><meta name=\"htmx-config\" content='{\"history\":false,\"includeIndicatorCSS\":false,\"extensions\":\"sse, hx-live\"}'><script src=\"")
116116 if templ_7745c5c3_Err != nil {
117117 return templ_7745c5c3_Err
118118 }
119119 var templ_7745c5c3_Var8 string
120- templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.ResolveAttributeValue(l.Assets.SSE)
120+ templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.ResolveAttributeValue(l.Assets.HTMX)
121121 if templ_7745c5c3_Err != nil {
122- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 22, Col: 29}
122+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 25, Col: 30}
123123 }
124124 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var8)
125125 if templ_7745c5c3_Err != nil {
@@ -130,9 +130,9 @@ func Shell(l Layout, rail templ.Component) templ.Component {
130130 return templ_7745c5c3_Err
131131 }
132132 var templ_7745c5c3_Var9 string
133- templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.ResolveAttributeValue(l.Assets.App)
133+ templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.ResolveAttributeValue(l.Assets.Live)
134134 if templ_7745c5c3_Err != nil {
135- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 23, Col: 29}
135+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 26, Col: 30}
136136 }
137137 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var9)
138138 if templ_7745c5c3_Err != nil {
@@ -143,9 +143,9 @@ func Shell(l Layout, rail templ.Component) templ.Component {
143143 return templ_7745c5c3_Err
144144 }
145145 var templ_7745c5c3_Var10 string
146- templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.ResolveAttributeValue(l.Assets.Files)
146+ templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.ResolveAttributeValue(l.Assets.SSE)
147147 if templ_7745c5c3_Err != nil {
148- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 24, Col: 31}
148+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 27, Col: 29}
149149 }
150150 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var10)
151151 if templ_7745c5c3_Err != nil {
@@ -156,9 +156,9 @@ func Shell(l Layout, rail templ.Component) templ.Component {
156156 return templ_7745c5c3_Err
157157 }
158158 var templ_7745c5c3_Var11 string
159- templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.ResolveAttributeValue(l.Assets.Passkey)
159+ templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.ResolveAttributeValue(l.Assets.App)
160160 if templ_7745c5c3_Err != nil {
161- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 25, Col: 33}
161+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 28, Col: 29}
162162 }
163163 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var11)
164164 if templ_7745c5c3_Err != nil {
@@ -169,77 +169,103 @@ func Shell(l Layout, rail templ.Component) templ.Component {
169169 return templ_7745c5c3_Err
170170 }
171171 var templ_7745c5c3_Var12 string
172- templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.ResolveAttributeValue(l.Assets.Log)
172+ templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.ResolveAttributeValue(l.Assets.Files)
173173 if templ_7745c5c3_Err != nil {
174- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 26, Col: 29}
174+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 29, Col: 31}
175175 }
176176 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var12)
177177 if templ_7745c5c3_Err != nil {
178178 return templ_7745c5c3_Err
179179 }
180- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "\" defer></script></head><body data-ref=\"")
180+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "\" defer></script><script src=\"")
181181 if templ_7745c5c3_Err != nil {
182182 return templ_7745c5c3_Err
183183 }
184184 var templ_7745c5c3_Var13 string
185- templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.ResolveAttributeValue(l.RefPath)
185+ templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.ResolveAttributeValue(l.Assets.Passkey)
186186 if templ_7745c5c3_Err != nil {
187- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 29, Col: 23}
187+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 30, Col: 33}
188188 }
189189 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var13)
190190 if templ_7745c5c3_Err != nil {
191191 return templ_7745c5c3_Err
192192 }
193- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "\" data-sha=\"")
193+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "\" defer></script><script src=\"")
194194 if templ_7745c5c3_Err != nil {
195195 return templ_7745c5c3_Err
196196 }
197197 var templ_7745c5c3_Var14 string
198- templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.ResolveAttributeValue(l.SHA)
198+ templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.ResolveAttributeValue(l.Assets.Log)
199199 if templ_7745c5c3_Err != nil {
200- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 30, Col: 19}
200+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 31, Col: 29}
201201 }
202202 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var14)
203203 if templ_7745c5c3_Err != nil {
204204 return templ_7745c5c3_Err
205205 }
206- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "\" data-sel=\"\" data-nav=\"\"")
206+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "\" defer></script></head><body data-ref=\"")
207+ if templ_7745c5c3_Err != nil {
208+ return templ_7745c5c3_Err
209+ }
210+ var templ_7745c5c3_Var15 string
211+ templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.ResolveAttributeValue(l.RefPath)
212+ if templ_7745c5c3_Err != nil {
213+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 34, Col: 23}
214+ }
215+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var15)
216+ if templ_7745c5c3_Err != nil {
217+ return templ_7745c5c3_Err
218+ }
219+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "\" data-sha=\"")
220+ if templ_7745c5c3_Err != nil {
221+ return templ_7745c5c3_Err
222+ }
223+ var templ_7745c5c3_Var16 string
224+ templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.ResolveAttributeValue(l.SHA)
225+ if templ_7745c5c3_Err != nil {
226+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 35, Col: 19}
227+ }
228+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var16)
229+ if templ_7745c5c3_Err != nil {
230+ return templ_7745c5c3_Err
231+ }
232+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "\" data-sel=\"\" data-nav=\"\"")
207233 if templ_7745c5c3_Err != nil {
208234 return templ_7745c5c3_Err
209235 }
210236 if l.PathsURL != "" {
211- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, " data-paths=\"")
237+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, " data-paths=\"")
212238 if templ_7745c5c3_Err != nil {
213239 return templ_7745c5c3_Err
214240 }
215- var templ_7745c5c3_Var15 string
216- templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.ResolveAttributeValue(l.PathsURL)
241+ var templ_7745c5c3_Var17 string
242+ templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.ResolveAttributeValue(l.PathsURL)
217243 if templ_7745c5c3_Err != nil {
218- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 34, Col: 27}
244+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 39, Col: 27}
219245 }
220- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var15)
246+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var17)
221247 if templ_7745c5c3_Err != nil {
222248 return templ_7745c5c3_Err
223249 }
224- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "\" data-blob=\"")
250+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "\" data-blob=\"")
225251 if templ_7745c5c3_Err != nil {
226252 return templ_7745c5c3_Err
227253 }
228- var templ_7745c5c3_Var16 string
229- templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.ResolveAttributeValue(l.BlobBase)
254+ var templ_7745c5c3_Var18 string
255+ templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.ResolveAttributeValue(l.BlobBase)
230256 if templ_7745c5c3_Err != nil {
231- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 35, Col: 26}
257+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 40, Col: 26}
232258 }
233- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var16)
259+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var18)
234260 if templ_7745c5c3_Err != nil {
235261 return templ_7745c5c3_Err
236262 }
237- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "\"")
263+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "\"")
238264 if templ_7745c5c3_Err != nil {
239265 return templ_7745c5c3_Err
240266 }
241267 }
242- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "><a class=\"skip\" href=\"#main\">skip to content</a>")
268+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "><a class=\"skip\" href=\"#main\">skip to content</a>")
243269 if templ_7745c5c3_Err != nil {
244270 return templ_7745c5c3_Err
245271 }
@@ -247,48 +273,48 @@ func Shell(l Layout, rail templ.Component) templ.Component {
247273 if templ_7745c5c3_Err != nil {
248274 return templ_7745c5c3_Err
249275 }
250- var templ_7745c5c3_Var17 = []any{"work", templ.KV("has-rail", l.HasRail && rail != nil)}
251- templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var17...)
276+ var templ_7745c5c3_Var19 = []any{"work", templ.KV("has-rail", l.HasRail && rail != nil)}
277+ templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var19...)
252278 if templ_7745c5c3_Err != nil {
253279 return templ_7745c5c3_Err
254280 }
255- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "<div class=\"")
281+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "<div class=\"")
256282 if templ_7745c5c3_Err != nil {
257283 return templ_7745c5c3_Err
258284 }
259- var templ_7745c5c3_Var18 string
260- templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var17).String())
285+ var templ_7745c5c3_Var20 string
286+ templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var19).String())
261287 if templ_7745c5c3_Err != nil {
262288 return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 1, Col: 0}
263289 }
264- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var18)
290+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var20)
265291 if templ_7745c5c3_Err != nil {
266292 return templ_7745c5c3_Err
267293 }
268- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "\">")
294+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "\">")
269295 if templ_7745c5c3_Err != nil {
270296 return templ_7745c5c3_Err
271297 }
272298 if l.Lede != "" {
273- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "<section class=\"work-lede\" aria-label=\"Description\"><p class=\"lede\">")
299+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "<section class=\"work-lede\" aria-label=\"Description\"><p class=\"lede\">")
274300 if templ_7745c5c3_Err != nil {
275301 return templ_7745c5c3_Err
276302 }
277- var templ_7745c5c3_Var19 string
278- templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(l.Lede)
303+ var templ_7745c5c3_Var21 string
304+ templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(l.Lede)
279305 if templ_7745c5c3_Err != nil {
280- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 42, Col: 81}
306+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 47, Col: 81}
281307 }
282- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19))
308+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21))
283309 if templ_7745c5c3_Err != nil {
284310 return templ_7745c5c3_Err
285311 }
286- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "</p></section>")
312+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "</p></section>")
287313 if templ_7745c5c3_Err != nil {
288314 return templ_7745c5c3_Err
289315 }
290316 }
291- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "<main id=\"main\" tabindex=\"-1\">")
317+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "<main id=\"main\" tabindex=\"-1\">")
292318 if templ_7745c5c3_Err != nil {
293319 return templ_7745c5c3_Err
294320 }
@@ -296,12 +322,12 @@ func Shell(l Layout, rail templ.Component) templ.Component {
296322 if templ_7745c5c3_Err != nil {
297323 return templ_7745c5c3_Err
298324 }
299- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "</main>")
325+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "</main>")
300326 if templ_7745c5c3_Err != nil {
301327 return templ_7745c5c3_Err
302328 }
303329 if l.HasRail && rail != nil {
304- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "<aside class=\"rail\" aria-label=\"Context\">")
330+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "<aside class=\"rail\" aria-label=\"Context\">")
305331 if templ_7745c5c3_Err != nil {
306332 return templ_7745c5c3_Err
307333 }
@@ -309,12 +335,12 @@ func Shell(l Layout, rail templ.Component) templ.Component {
309335 if templ_7745c5c3_Err != nil {
310336 return templ_7745c5c3_Err
311337 }
312- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "</aside>")
338+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "</aside>")
313339 if templ_7745c5c3_Err != nil {
314340 return templ_7745c5c3_Err
315341 }
316342 }
317- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "</div>")
343+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "</div>")
318344 if templ_7745c5c3_Err != nil {
319345 return templ_7745c5c3_Err
320346 }
@@ -342,7 +368,7 @@ func Shell(l Layout, rail templ.Component) templ.Component {
342368 if templ_7745c5c3_Err != nil {
343369 return templ_7745c5c3_Err
344370 }
345- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "</body></html>")
371+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "</body></html>")
346372 if templ_7745c5c3_Err != nil {
347373 return templ_7745c5c3_Err
348374 }
@@ -350,8 +376,8 @@ func Shell(l Layout, rail templ.Component) templ.Component {
350376 })
351377 }
352378
353-// Header is the page header: the instance, the path, the ref switcher, the
354-// "go to" palette and the owner; inside a repository, the tabs below.
379+// Header is the page header: the logo (home), the path, the ref switcher,
380+// the "go to" palette and the owner; inside a repository, the tabs below.
355381 func Header(l Layout) templ.Component {
356382 return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
357383 templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
@@ -368,288 +394,288 @@ func Header(l Layout) templ.Component {
368394 }()
369395 }
370396 ctx = templ.InitializeContext(ctx)
371- templ_7745c5c3_Var20 := templ.GetChildren(ctx)
372- if templ_7745c5c3_Var20 == nil {
373- templ_7745c5c3_Var20 = templ.NopComponent
397+ templ_7745c5c3_Var22 := templ.GetChildren(ctx)
398+ if templ_7745c5c3_Var22 == nil {
399+ templ_7745c5c3_Var22 = templ.NopComponent
374400 }
375401 ctx = templ.ClearChildren(ctx)
376- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "<header class=\"hdr\"><div class=\"hdr-top\"><a class=\"brand\" href=\"/\" data-g=\"h\" title=\"home (g h)\"><span class=\"brand-mark\" aria-hidden=\"true\"></span> <span class=\"brand-name\">")
377- if templ_7745c5c3_Err != nil {
378- return templ_7745c5c3_Err
379- }
380- var templ_7745c5c3_Var21 string
381- templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(l.Instance)
382- if templ_7745c5c3_Err != nil {
383- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 70, Col: 41}
384- }
385- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21))
386- if templ_7745c5c3_Err != nil {
387- return templ_7745c5c3_Err
388- }
389- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "</span></a> ")
402+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "<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> ")
390403 if templ_7745c5c3_Err != nil {
391404 return templ_7745c5c3_Err
392405 }
393406 if len(l.Crumbs) > 0 {
394- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "<nav class=\"crumbs\" aria-label=\"Path\"><ol>")
407+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "<nav class=\"crumbs\" aria-label=\"Path\"><ol>")
395408 if templ_7745c5c3_Err != nil {
396409 return templ_7745c5c3_Err
397410 }
398411 for _, c := range l.Crumbs {
399- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "<li>")
412+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "<li>")
400413 if templ_7745c5c3_Err != nil {
401414 return templ_7745c5c3_Err
402415 }
403416 if c.Href != "" {
404- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "<a href=\"")
417+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "<a href=\"")
405418 if templ_7745c5c3_Err != nil {
406419 return templ_7745c5c3_Err
407420 }
408- var templ_7745c5c3_Var22 templ.SafeURL
409- templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(c.Href))
421+ var templ_7745c5c3_Var23 templ.SafeURL
422+ templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(c.Href))
410423 if templ_7745c5c3_Err != nil {
411- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 79, Col: 38}
424+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 83, Col: 38}
412425 }
413- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22))
426+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23))
414427 if templ_7745c5c3_Err != nil {
415428 return templ_7745c5c3_Err
416429 }
417- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "\"")
430+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "\"")
418431 if templ_7745c5c3_Err != nil {
419432 return templ_7745c5c3_Err
420433 }
421434 if c.Current {
422- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, " aria-current=\"page\"")
435+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, " aria-current=\"page\"")
423436 if templ_7745c5c3_Err != nil {
424437 return templ_7745c5c3_Err
425438 }
426439 }
427- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, ">")
440+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, ">")
428441 if templ_7745c5c3_Err != nil {
429442 return templ_7745c5c3_Err
430443 }
431- var templ_7745c5c3_Var23 string
432- templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinStringErrs(c.Label)
444+ var templ_7745c5c3_Var24 string
445+ templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinStringErrs(c.Label)
433446 if templ_7745c5c3_Err != nil {
434- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 83, Col: 19}
447+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 87, Col: 19}
435448 }
436- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23))
449+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24))
437450 if templ_7745c5c3_Err != nil {
438451 return templ_7745c5c3_Err
439452 }
440- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "</a>")
453+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "</a>")
441454 if templ_7745c5c3_Err != nil {
442455 return templ_7745c5c3_Err
443456 }
444457 } else {
445- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "<span")
458+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "<span")
446459 if templ_7745c5c3_Err != nil {
447460 return templ_7745c5c3_Err
448461 }
449462 if c.Current {
450- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, " aria-current=\"page\"")
463+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, " aria-current=\"page\"")
451464 if templ_7745c5c3_Err != nil {
452465 return templ_7745c5c3_Err
453466 }
454467 }
455- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, ">")
468+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, ">")
456469 if templ_7745c5c3_Err != nil {
457470 return templ_7745c5c3_Err
458471 }
459- var templ_7745c5c3_Var24 string
460- templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinStringErrs(c.Label)
472+ var templ_7745c5c3_Var25 string
473+ templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.JoinStringErrs(c.Label)
461474 if templ_7745c5c3_Err != nil {
462- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 89, Col: 19}
475+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 93, Col: 19}
463476 }
464- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24))
477+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var25))
465478 if templ_7745c5c3_Err != nil {
466479 return templ_7745c5c3_Err
467480 }
468- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "</span>")
481+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "</span>")
469482 if templ_7745c5c3_Err != nil {
470483 return templ_7745c5c3_Err
471484 }
472485 }
473- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "</li>")
486+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "</li>")
474487 if templ_7745c5c3_Err != nil {
475488 return templ_7745c5c3_Err
476489 }
477490 }
478- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "</ol></nav>")
491+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "</ol></nav>")
479492 if templ_7745c5c3_Err != nil {
480493 return templ_7745c5c3_Err
481494 }
482495 }
483- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "<div class=\"hdr-acts\">")
496+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "<div class=\"hdr-acts\">")
484497 if templ_7745c5c3_Err != nil {
485498 return templ_7745c5c3_Err
486499 }
487500 if l.Ref != "" {
488- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "<a class=\"btn ref\" href=\"")
501+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "<a class=\"btn ref\" href=\"")
489502 if templ_7745c5c3_Err != nil {
490503 return templ_7745c5c3_Err
491504 }
492- var templ_7745c5c3_Var25 templ.SafeURL
493- templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(repoHref(l.Repo, "refs")))
505+ var templ_7745c5c3_Var26 templ.SafeURL
506+ templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(repoHref(l.Repo, "refs")))
494507 if templ_7745c5c3_Err != nil {
495- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 100, Col: 52}
508+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 104, Col: 52}
496509 }
497- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var25))
510+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var26))
498511 if templ_7745c5c3_Err != nil {
499512 return templ_7745c5c3_Err
500513 }
501- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "\" 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\">")
514+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "\" 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\">")
502515 if templ_7745c5c3_Err != nil {
503516 return templ_7745c5c3_Err
504517 }
505- var templ_7745c5c3_Var26 string
506- templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.JoinStringErrs(l.Ref)
518+ var templ_7745c5c3_Var27 string
519+ templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.JoinStringErrs(l.Ref)
507520 if templ_7745c5c3_Err != nil {
508- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 106, Col: 36}
521+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 110, Col: 36}
509522 }
510- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var26))
523+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var27))
511524 if templ_7745c5c3_Err != nil {
512525 return templ_7745c5c3_Err
513526 }
514- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "</span> <span class=\"sha\">")
527+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, "</span> <span class=\"sha\">")
515528 if templ_7745c5c3_Err != nil {
516529 return templ_7745c5c3_Err
517530 }
518- var templ_7745c5c3_Var27 string
519- templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.JoinStringErrs(l.Short)
531+ var templ_7745c5c3_Var28 string
532+ templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.JoinStringErrs(l.Short)
520533 if templ_7745c5c3_Err != nil {
521- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 107, Col: 33}
534+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 111, Col: 33}
522535 }
523- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var27))
536+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var28))
524537 if templ_7745c5c3_Err != nil {
525538 return templ_7745c5c3_Err
526539 }
527- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, "</span></a> ")
540+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "</span></a> ")
528541 if templ_7745c5c3_Err != nil {
529542 return templ_7745c5c3_Err
530543 }
531544 }
532- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "<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> <button type=\"button\" class=\"btn icon\" data-js data-fire=\"help\" aria-haspopup=\"dialog\" aria-controls=\"panel-help\" aria-keyshortcuts=\"?\" aria-label=\"Keyboard shortcuts\" title=\"keyboard shortcuts (?)\">?</button> ")
545+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, "<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> ")
533546 if templ_7745c5c3_Err != nil {
534547 return templ_7745c5c3_Err
535548 }
536549 if SignedIn(ctx) {
537- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, "<form class=\"owner\" method=\"post\" action=\"/-/logout\"><span class=\"owner-badge\" title=\"signed in as the owner\">owner</span> ")
550+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, "<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>")
551+ if templ_7745c5c3_Err != nil {
552+ return templ_7745c5c3_Err
553+ }
554+ var templ_7745c5c3_Var29 string
555+ templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.JoinStringErrs(l.Owner)
556+ if templ_7745c5c3_Err != nil {
557+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 130, Col: 123}
558+ }
559+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var29))
560+ if templ_7745c5c3_Err != nil {
561+ return templ_7745c5c3_Err
562+ }
563+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, "</span> ")
538564 if templ_7745c5c3_Err != nil {
539565 return templ_7745c5c3_Err
540566 }
541567 if l.Passkeys {
542- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, "<a class=\"btn\" href=\"/-/passkeys\" title=\"add or remove the passkeys that sign you in\">Passkeys</a> ")
568+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, "<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> ")
543569 if templ_7745c5c3_Err != nil {
544570 return templ_7745c5c3_Err
545571 }
546572 }
547- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, "<button type=\"submit\" class=\"btn\" title=\"sign out of this browser\">Sign out</button></form>")
573+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, "<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>")
548574 if templ_7745c5c3_Err != nil {
549575 return templ_7745c5c3_Err
550576 }
551577 } else if l.Passkeys {
552- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, "<a class=\"btn\" href=\"/-/login\" title=\"sign in with a passkey\">Sign in</a>")
578+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, "<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>")
553579 if templ_7745c5c3_Err != nil {
554580 return templ_7745c5c3_Err
555581 }
556582 }
557- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, "</div></div>")
583+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 56, "</div></div>")
558584 if templ_7745c5c3_Err != nil {
559585 return templ_7745c5c3_Err
560586 }
561587 if len(l.Sections) > 0 {
562- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, "<nav class=\"tabs\" aria-label=\"Sections\"><ul>")
588+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, "<nav class=\"tabs\" aria-label=\"Sections\"><ul>")
563589 if templ_7745c5c3_Err != nil {
564590 return templ_7745c5c3_Err
565591 }
566592 for _, s := range l.Sections {
567- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 56, "<li><a href=\"")
593+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "<li><a href=\"")
568594 if templ_7745c5c3_Err != nil {
569595 return templ_7745c5c3_Err
570596 }
571- var templ_7745c5c3_Var28 templ.SafeURL
572- templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(s.Href))
597+ var templ_7745c5c3_Var30 templ.SafeURL
598+ templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(s.Href))
573599 if templ_7745c5c3_Err != nil {
574- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 144, Col: 36}
600+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 147, Col: 36}
575601 }
576- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var28))
602+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var30))
577603 if templ_7745c5c3_Err != nil {
578604 return templ_7745c5c3_Err
579605 }
580- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, "\" data-key=\"")
606+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, "\" data-key=\"")
581607 if templ_7745c5c3_Err != nil {
582608 return templ_7745c5c3_Err
583609 }
584- var templ_7745c5c3_Var29 string
585- templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.ResolveAttributeValue(s.Key)
610+ var templ_7745c5c3_Var31 string
611+ templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.ResolveAttributeValue(s.Key)
586612 if templ_7745c5c3_Err != nil {
587- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 145, Col: 24}
613+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 148, Col: 24}
588614 }
589- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var29)
615+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var31)
590616 if templ_7745c5c3_Err != nil {
591617 return templ_7745c5c3_Err
592618 }
593- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "\" aria-keyshortcuts=\"")
619+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 60, "\" aria-keyshortcuts=\"")
594620 if templ_7745c5c3_Err != nil {
595621 return templ_7745c5c3_Err
596622 }
597- var templ_7745c5c3_Var30 string
598- templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.ResolveAttributeValue(s.Key)
623+ var templ_7745c5c3_Var32 string
624+ templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.ResolveAttributeValue(s.Key)
599625 if templ_7745c5c3_Err != nil {
600- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 146, Col: 33}
626+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 149, Col: 33}
601627 }
602- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var30)
628+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var32)
603629 if templ_7745c5c3_Err != nil {
604630 return templ_7745c5c3_Err
605631 }
606- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, "\"")
632+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 61, "\"")
607633 if templ_7745c5c3_Err != nil {
608634 return templ_7745c5c3_Err
609635 }
610636 if s.Current {
611- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 60, " aria-current=\"page\"")
637+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 62, " aria-current=\"page\"")
612638 if templ_7745c5c3_Err != nil {
613639 return templ_7745c5c3_Err
614640 }
615641 }
616- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 61, ">")
642+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 63, ">")
617643 if templ_7745c5c3_Err != nil {
618644 return templ_7745c5c3_Err
619645 }
620- var templ_7745c5c3_Var31 string
621- templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.JoinStringErrs(s.Label)
646+ var templ_7745c5c3_Var33 string
647+ templ_7745c5c3_Var33, templ_7745c5c3_Err = templ.JoinStringErrs(s.Label)
622648 if templ_7745c5c3_Err != nil {
623- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 150, Col: 17}
649+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 153, Col: 17}
624650 }
625- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var31))
651+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var33))
626652 if templ_7745c5c3_Err != nil {
627653 return templ_7745c5c3_Err
628654 }
629- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 62, "<kbd aria-hidden=\"true\">")
655+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 64, "<kbd aria-hidden=\"true\">")
630656 if templ_7745c5c3_Err != nil {
631657 return templ_7745c5c3_Err
632658 }
633- var templ_7745c5c3_Var32 string
634- templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.JoinStringErrs(s.Key)
659+ var templ_7745c5c3_Var34 string
660+ templ_7745c5c3_Var34, templ_7745c5c3_Err = templ.JoinStringErrs(s.Key)
635661 if templ_7745c5c3_Err != nil {
636- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 150, Col: 50}
662+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 153, Col: 50}
637663 }
638- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var32))
664+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var34))
639665 if templ_7745c5c3_Err != nil {
640666 return templ_7745c5c3_Err
641667 }
642- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 63, "</kbd></a></li>")
668+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 65, "</kbd></a></li>")
643669 if templ_7745c5c3_Err != nil {
644670 return templ_7745c5c3_Err
645671 }
646672 }
647- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 64, "</ul></nav>")
673+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 66, "</ul></nav>")
648674 if templ_7745c5c3_Err != nil {
649675 return templ_7745c5c3_Err
650676 }
651677 }
652- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 65, "</header>")
678+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 67, "</header>")
653679 if templ_7745c5c3_Err != nil {
654680 return templ_7745c5c3_Err
655681 }
@@ -675,38 +701,38 @@ func Footer(l Layout) templ.Component {
675701 }()
676702 }
677703 ctx = templ.InitializeContext(ctx)
678- templ_7745c5c3_Var33 := templ.GetChildren(ctx)
679- if templ_7745c5c3_Var33 == nil {
680- templ_7745c5c3_Var33 = templ.NopComponent
704+ templ_7745c5c3_Var35 := templ.GetChildren(ctx)
705+ if templ_7745c5c3_Var35 == nil {
706+ templ_7745c5c3_Var35 = templ.NopComponent
681707 }
682708 ctx = templ.ClearChildren(ctx)
683- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 66, "<footer class=\"foot\"><span>ermite ")
709+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 68, "<footer class=\"foot\"><span>ermite ")
684710 if templ_7745c5c3_Err != nil {
685711 return templ_7745c5c3_Err
686712 }
687- var templ_7745c5c3_Var34 string
688- templ_7745c5c3_Var34, templ_7745c5c3_Err = templ.JoinStringErrs(l.Version)
713+ var templ_7745c5c3_Var36 string
714+ templ_7745c5c3_Var36, templ_7745c5c3_Err = templ.JoinStringErrs(l.Version)
689715 if templ_7745c5c3_Err != nil {
690- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 163, Col: 26}
716+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 166, Col: 26}
691717 }
692- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var34))
718+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var36))
693719 if templ_7745c5c3_Err != nil {
694720 return templ_7745c5c3_Err
695721 }
696- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 67, "</span> <a href=\"")
722+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 69, "</span> <a href=\"")
697723 if templ_7745c5c3_Err != nil {
698724 return templ_7745c5c3_Err
699725 }
700- var templ_7745c5c3_Var35 templ.SafeURL
701- templ_7745c5c3_Var35, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(l.SourceURL))
726+ var templ_7745c5c3_Var37 templ.SafeURL
727+ templ_7745c5c3_Var37, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(l.SourceURL))
702728 if templ_7745c5c3_Err != nil {
703- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 164, Col: 38}
729+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 167, Col: 38}
704730 }
705- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var35))
731+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var37))
706732 if templ_7745c5c3_Err != nil {
707733 return templ_7745c5c3_Err
708734 }
709- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 68, "\" rel=\"noopener\" title=\"source of this version (AGPL-3.0-or-later)\">source</a> <span class=\"foot-keys\"><kbd>?</kbd> keyboard shortcuts</span></footer>")
735+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 70, "\" 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>")
710736 if templ_7745c5c3_Err != nil {
711737 return templ_7745c5c3_Err
712738 }
@@ -734,12 +760,12 @@ func Hint(l Layout) templ.Component {
734760 }()
735761 }
736762 ctx = templ.InitializeContext(ctx)
737- templ_7745c5c3_Var36 := templ.GetChildren(ctx)
738- if templ_7745c5c3_Var36 == nil {
739- templ_7745c5c3_Var36 = templ.NopComponent
763+ templ_7745c5c3_Var38 := templ.GetChildren(ctx)
764+ if templ_7745c5c3_Var38 == nil {
765+ templ_7745c5c3_Var38 = templ.NopComponent
740766 }
741767 ctx = templ.ClearChildren(ctx)
742- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 69, "<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>")
768+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 71, "<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>")
743769 if templ_7745c5c3_Err != nil {
744770 return templ_7745c5c3_Err
745771 }
@@ -747,7 +773,7 @@ func Hint(l Layout) templ.Component {
747773 if templ_7745c5c3_Err != nil {
748774 return templ_7745c5c3_Err
749775 }
750- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 70, "</div>")
776+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 72, "</div>")
751777 if templ_7745c5c3_Err != nil {
752778 return templ_7745c5c3_Err
753779 }
@@ -47,79 +47,79 @@ func Shell(l Layout, rail templ.Component) templ.Component {
47 if templ_7745c5c3_Err != nil {47 if templ_7745c5c3_Err != nil {
48 return templ_7745c5c3_Err48 return templ_7745c5c3_Err
49 }49 }
50- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "</title><link rel=\"icon\" type=\"image/svg+xml\" href=\"")50+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "</title><meta name=\"theme-color\" media=\"(prefers-color-scheme: light)\" content=\"#ffffff\"><meta name=\"theme-color\" media=\"(prefers-color-scheme: dark)\" content=\"#0c0c0c\"><meta name=\"apple-mobile-web-app-title\" content=\"ermite\"><link rel=\"icon\" type=\"image/svg+xml\" href=\"")
51 if templ_7745c5c3_Err != nil {51 if templ_7745c5c3_Err != nil {
52 return templ_7745c5c3_Err52 return templ_7745c5c3_Err
53 }53 }
54 var templ_7745c5c3_Var3 templ.SafeURL54 var templ_7745c5c3_Var3 templ.SafeURL
55 templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(l.Assets.Icon))55 templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(l.Assets.Icon))
56 if templ_7745c5c3_Err != nil {56 if templ_7745c5c3_Err != nil {
57- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 16, Col: 76}57+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 19, Col: 76}
58 }58 }
59 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))59 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
60 if templ_7745c5c3_Err != nil {60 if templ_7745c5c3_Err != nil {
61 return templ_7745c5c3_Err61 return templ_7745c5c3_Err
62 }62 }
63- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\"><link rel=\"preload\" href=\"")63+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\"><link rel=\"apple-touch-icon\" href=\"")
64 if templ_7745c5c3_Err != nil {64 if templ_7745c5c3_Err != nil {
65 return templ_7745c5c3_Err65 return templ_7745c5c3_Err
66 }66 }
67 var templ_7745c5c3_Var4 templ.SafeURL67 var templ_7745c5c3_Var4 templ.SafeURL
68- templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(l.Assets.Font))68+ templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(l.Assets.AppleIcon))
69 if templ_7745c5c3_Err != nil {69 if templ_7745c5c3_Err != nil {
70- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 17, Col: 58}70+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 20, Col: 72}
71 }71 }
72 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))72 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
73 if templ_7745c5c3_Err != nil {73 if templ_7745c5c3_Err != nil {
74 return templ_7745c5c3_Err74 return templ_7745c5c3_Err
75 }75 }
76- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "\" as=\"font\" type=\"font/woff2\" crossorigin><link rel=\"stylesheet\" href=\"")76+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "\"><link rel=\"manifest\" href=\"")
77 if templ_7745c5c3_Err != nil {77 if templ_7745c5c3_Err != nil {
78 return templ_7745c5c3_Err78 return templ_7745c5c3_Err
79 }79 }
80 var templ_7745c5c3_Var5 templ.SafeURL80 var templ_7745c5c3_Var5 templ.SafeURL
81- templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(l.Assets.CSS))81+ templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(l.Assets.Manifest))
82 if templ_7745c5c3_Err != nil {82 if templ_7745c5c3_Err != nil {
83- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 18, Col: 60}83+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 21, Col: 63}
84 }84 }
85 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))85 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
86 if templ_7745c5c3_Err != nil {86 if templ_7745c5c3_Err != nil {
87 return templ_7745c5c3_Err87 return templ_7745c5c3_Err
88 }88 }
89- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "\"><meta name=\"htmx-config\" content='{\"history\":false,\"includeIndicatorCSS\":false,\"extensions\":\"sse, hx-live\"}'><script src=\"")89+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "\"><link rel=\"preload\" href=\"")
90 if templ_7745c5c3_Err != nil {90 if templ_7745c5c3_Err != nil {
91 return templ_7745c5c3_Err91 return templ_7745c5c3_Err
92 }92 }
93- var templ_7745c5c3_Var6 string93+ var templ_7745c5c3_Var6 templ.SafeURL
94- templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.ResolveAttributeValue(l.Assets.HTMX)94+ templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(l.Assets.Font))
95 if templ_7745c5c3_Err != nil {95 if templ_7745c5c3_Err != nil {
96- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 20, Col: 30}96+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 22, Col: 58}
97 }97 }
98- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var6)98+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
99 if templ_7745c5c3_Err != nil {99 if templ_7745c5c3_Err != nil {
100 return templ_7745c5c3_Err100 return templ_7745c5c3_Err
101 }101 }
102- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "\" defer></script><script src=\"")102+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "\" as=\"font\" type=\"font/woff2\" crossorigin><link rel=\"stylesheet\" href=\"")
103 if templ_7745c5c3_Err != nil {103 if templ_7745c5c3_Err != nil {
104 return templ_7745c5c3_Err104 return templ_7745c5c3_Err
105 }105 }
106- var templ_7745c5c3_Var7 string106+ var templ_7745c5c3_Var7 templ.SafeURL
107- templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.ResolveAttributeValue(l.Assets.Live)107+ templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(l.Assets.CSS))
108 if templ_7745c5c3_Err != nil {108 if templ_7745c5c3_Err != nil {
109- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 21, Col: 30}109+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 23, Col: 60}
110 }110 }
111- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var7)111+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
112 if templ_7745c5c3_Err != nil {112 if templ_7745c5c3_Err != nil {
113 return templ_7745c5c3_Err113 return templ_7745c5c3_Err
114 }114 }
115- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "\" defer></script><script src=\"")115+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "\"><meta name=\"htmx-config\" content='{\"history\":false,\"includeIndicatorCSS\":false,\"extensions\":\"sse, hx-live\"}'><script src=\"")
116 if templ_7745c5c3_Err != nil {116 if templ_7745c5c3_Err != nil {
117 return templ_7745c5c3_Err117 return templ_7745c5c3_Err
118 }118 }
119 var templ_7745c5c3_Var8 string119 var templ_7745c5c3_Var8 string
120- templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.ResolveAttributeValue(l.Assets.SSE)120+ templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.ResolveAttributeValue(l.Assets.HTMX)
121 if templ_7745c5c3_Err != nil {121 if templ_7745c5c3_Err != nil {
122- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 22, Col: 29}122+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 25, Col: 30}
123 }123 }
124 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var8)124 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var8)
125 if templ_7745c5c3_Err != nil {125 if templ_7745c5c3_Err != nil {
@@ -130,9 +130,9 @@ func Shell(l Layout, rail templ.Component) templ.Component {
130 return templ_7745c5c3_Err130 return templ_7745c5c3_Err
131 }131 }
132 var templ_7745c5c3_Var9 string132 var templ_7745c5c3_Var9 string
133- templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.ResolveAttributeValue(l.Assets.App)133+ templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.ResolveAttributeValue(l.Assets.Live)
134 if templ_7745c5c3_Err != nil {134 if templ_7745c5c3_Err != nil {
135- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 23, Col: 29}135+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 26, Col: 30}
136 }136 }
137 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var9)137 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var9)
138 if templ_7745c5c3_Err != nil {138 if templ_7745c5c3_Err != nil {
@@ -143,9 +143,9 @@ func Shell(l Layout, rail templ.Component) templ.Component {
143 return templ_7745c5c3_Err143 return templ_7745c5c3_Err
144 }144 }
145 var templ_7745c5c3_Var10 string145 var templ_7745c5c3_Var10 string
146- templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.ResolveAttributeValue(l.Assets.Files)146+ templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.ResolveAttributeValue(l.Assets.SSE)
147 if templ_7745c5c3_Err != nil {147 if templ_7745c5c3_Err != nil {
148- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 24, Col: 31}148+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 27, Col: 29}
149 }149 }
150 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var10)150 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var10)
151 if templ_7745c5c3_Err != nil {151 if templ_7745c5c3_Err != nil {
@@ -156,9 +156,9 @@ func Shell(l Layout, rail templ.Component) templ.Component {
156 return templ_7745c5c3_Err156 return templ_7745c5c3_Err
157 }157 }
158 var templ_7745c5c3_Var11 string158 var templ_7745c5c3_Var11 string
159- templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.ResolveAttributeValue(l.Assets.Passkey)159+ templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.ResolveAttributeValue(l.Assets.App)
160 if templ_7745c5c3_Err != nil {160 if templ_7745c5c3_Err != nil {
161- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 25, Col: 33}161+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 28, Col: 29}
162 }162 }
163 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var11)163 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var11)
164 if templ_7745c5c3_Err != nil {164 if templ_7745c5c3_Err != nil {
@@ -169,77 +169,103 @@ func Shell(l Layout, rail templ.Component) templ.Component {
169 return templ_7745c5c3_Err169 return templ_7745c5c3_Err
170 }170 }
171 var templ_7745c5c3_Var12 string171 var templ_7745c5c3_Var12 string
172- templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.ResolveAttributeValue(l.Assets.Log)172+ templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.ResolveAttributeValue(l.Assets.Files)
173 if templ_7745c5c3_Err != nil {173 if templ_7745c5c3_Err != nil {
174- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 26, Col: 29}174+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 29, Col: 31}
175 }175 }
176 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var12)176 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var12)
177 if templ_7745c5c3_Err != nil {177 if templ_7745c5c3_Err != nil {
178 return templ_7745c5c3_Err178 return templ_7745c5c3_Err
179 }179 }
180- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "\" defer></script></head><body data-ref=\"")180+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "\" defer></script><script src=\"")
181 if templ_7745c5c3_Err != nil {181 if templ_7745c5c3_Err != nil {
182 return templ_7745c5c3_Err182 return templ_7745c5c3_Err
183 }183 }
184 var templ_7745c5c3_Var13 string184 var templ_7745c5c3_Var13 string
185- templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.ResolveAttributeValue(l.RefPath)185+ templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.ResolveAttributeValue(l.Assets.Passkey)
186 if templ_7745c5c3_Err != nil {186 if templ_7745c5c3_Err != nil {
187- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 29, Col: 23}187+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 30, Col: 33}
188 }188 }
189 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var13)189 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var13)
190 if templ_7745c5c3_Err != nil {190 if templ_7745c5c3_Err != nil {
191 return templ_7745c5c3_Err191 return templ_7745c5c3_Err
192 }192 }
193- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "\" data-sha=\"")193+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "\" defer></script><script src=\"")
194 if templ_7745c5c3_Err != nil {194 if templ_7745c5c3_Err != nil {
195 return templ_7745c5c3_Err195 return templ_7745c5c3_Err
196 }196 }
197 var templ_7745c5c3_Var14 string197 var templ_7745c5c3_Var14 string
198- templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.ResolveAttributeValue(l.SHA)198+ templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.ResolveAttributeValue(l.Assets.Log)
199 if templ_7745c5c3_Err != nil {199 if templ_7745c5c3_Err != nil {
200- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 30, Col: 19}200+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 31, Col: 29}
201 }201 }
202 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var14)202 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var14)
203 if templ_7745c5c3_Err != nil {203 if templ_7745c5c3_Err != nil {
204 return templ_7745c5c3_Err204 return templ_7745c5c3_Err
205 }205 }
206- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "\" data-sel=\"\" data-nav=\"\"")206+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "\" defer></script></head><body data-ref=\"")
207+ if templ_7745c5c3_Err != nil {
208+ return templ_7745c5c3_Err
209+ }
210+ var templ_7745c5c3_Var15 string
211+ templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.ResolveAttributeValue(l.RefPath)
212+ if templ_7745c5c3_Err != nil {
213+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 34, Col: 23}
214+ }
215+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var15)
216+ if templ_7745c5c3_Err != nil {
217+ return templ_7745c5c3_Err
218+ }
219+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "\" data-sha=\"")
220+ if templ_7745c5c3_Err != nil {
221+ return templ_7745c5c3_Err
222+ }
223+ var templ_7745c5c3_Var16 string
224+ templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.ResolveAttributeValue(l.SHA)
225+ if templ_7745c5c3_Err != nil {
226+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 35, Col: 19}
227+ }
228+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var16)
229+ if templ_7745c5c3_Err != nil {
230+ return templ_7745c5c3_Err
231+ }
232+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "\" data-sel=\"\" data-nav=\"\"")
207 if templ_7745c5c3_Err != nil {233 if templ_7745c5c3_Err != nil {
208 return templ_7745c5c3_Err234 return templ_7745c5c3_Err
209 }235 }
210 if l.PathsURL != "" {236 if l.PathsURL != "" {
211- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, " data-paths=\"")237+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, " data-paths=\"")
212 if templ_7745c5c3_Err != nil {238 if templ_7745c5c3_Err != nil {
213 return templ_7745c5c3_Err239 return templ_7745c5c3_Err
214 }240 }
215- var templ_7745c5c3_Var15 string241+ var templ_7745c5c3_Var17 string
216- templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.ResolveAttributeValue(l.PathsURL)242+ templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.ResolveAttributeValue(l.PathsURL)
217 if templ_7745c5c3_Err != nil {243 if templ_7745c5c3_Err != nil {
218- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 34, Col: 27}244+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 39, Col: 27}
219 }245 }
220- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var15)246+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var17)
221 if templ_7745c5c3_Err != nil {247 if templ_7745c5c3_Err != nil {
222 return templ_7745c5c3_Err248 return templ_7745c5c3_Err
223 }249 }
224- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "\" data-blob=\"")250+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "\" data-blob=\"")
225 if templ_7745c5c3_Err != nil {251 if templ_7745c5c3_Err != nil {
226 return templ_7745c5c3_Err252 return templ_7745c5c3_Err
227 }253 }
228- var templ_7745c5c3_Var16 string254+ var templ_7745c5c3_Var18 string
229- templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.ResolveAttributeValue(l.BlobBase)255+ templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.ResolveAttributeValue(l.BlobBase)
230 if templ_7745c5c3_Err != nil {256 if templ_7745c5c3_Err != nil {
231- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 35, Col: 26}257+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 40, Col: 26}
232 }258 }
233- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var16)259+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var18)
234 if templ_7745c5c3_Err != nil {260 if templ_7745c5c3_Err != nil {
235 return templ_7745c5c3_Err261 return templ_7745c5c3_Err
236 }262 }
237- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "\"")263+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "\"")
238 if templ_7745c5c3_Err != nil {264 if templ_7745c5c3_Err != nil {
239 return templ_7745c5c3_Err265 return templ_7745c5c3_Err
240 }266 }
241 }267 }
242- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "><a class=\"skip\" href=\"#main\">skip to content</a>")268+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "><a class=\"skip\" href=\"#main\">skip to content</a>")
243 if templ_7745c5c3_Err != nil {269 if templ_7745c5c3_Err != nil {
244 return templ_7745c5c3_Err270 return templ_7745c5c3_Err
245 }271 }
@@ -247,48 +273,48 @@ func Shell(l Layout, rail templ.Component) templ.Component {
247 if templ_7745c5c3_Err != nil {273 if templ_7745c5c3_Err != nil {
248 return templ_7745c5c3_Err274 return templ_7745c5c3_Err
249 }275 }
250- var templ_7745c5c3_Var17 = []any{"work", templ.KV("has-rail", l.HasRail && rail != nil)}276+ var templ_7745c5c3_Var19 = []any{"work", templ.KV("has-rail", l.HasRail && rail != nil)}
251- templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var17...)277+ templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var19...)
252 if templ_7745c5c3_Err != nil {278 if templ_7745c5c3_Err != nil {
253 return templ_7745c5c3_Err279 return templ_7745c5c3_Err
254 }280 }
255- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "<div class=\"")281+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "<div class=\"")
256 if templ_7745c5c3_Err != nil {282 if templ_7745c5c3_Err != nil {
257 return templ_7745c5c3_Err283 return templ_7745c5c3_Err
258 }284 }
259- var templ_7745c5c3_Var18 string285+ var templ_7745c5c3_Var20 string
260- templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var17).String())286+ templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var19).String())
261 if templ_7745c5c3_Err != nil {287 if templ_7745c5c3_Err != nil {
262 return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 1, Col: 0}288 return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 1, Col: 0}
263 }289 }
264- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var18)290+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var20)
265 if templ_7745c5c3_Err != nil {291 if templ_7745c5c3_Err != nil {
266 return templ_7745c5c3_Err292 return templ_7745c5c3_Err
267 }293 }
268- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "\">")294+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "\">")
269 if templ_7745c5c3_Err != nil {295 if templ_7745c5c3_Err != nil {
270 return templ_7745c5c3_Err296 return templ_7745c5c3_Err
271 }297 }
272 if l.Lede != "" {298 if l.Lede != "" {
273- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "<section class=\"work-lede\" aria-label=\"Description\"><p class=\"lede\">")299+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "<section class=\"work-lede\" aria-label=\"Description\"><p class=\"lede\">")
274 if templ_7745c5c3_Err != nil {300 if templ_7745c5c3_Err != nil {
275 return templ_7745c5c3_Err301 return templ_7745c5c3_Err
276 }302 }
277- var templ_7745c5c3_Var19 string303+ var templ_7745c5c3_Var21 string
278- templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(l.Lede)304+ templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(l.Lede)
279 if templ_7745c5c3_Err != nil {305 if templ_7745c5c3_Err != nil {
280- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 42, Col: 81}306+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 47, Col: 81}
281 }307 }
282- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19))308+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21))
283 if templ_7745c5c3_Err != nil {309 if templ_7745c5c3_Err != nil {
284 return templ_7745c5c3_Err310 return templ_7745c5c3_Err
285 }311 }
286- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "</p></section>")312+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "</p></section>")
287 if templ_7745c5c3_Err != nil {313 if templ_7745c5c3_Err != nil {
288 return templ_7745c5c3_Err314 return templ_7745c5c3_Err
289 }315 }
290 }316 }
291- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "<main id=\"main\" tabindex=\"-1\">")317+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "<main id=\"main\" tabindex=\"-1\">")
292 if templ_7745c5c3_Err != nil {318 if templ_7745c5c3_Err != nil {
293 return templ_7745c5c3_Err319 return templ_7745c5c3_Err
294 }320 }
@@ -296,12 +322,12 @@ func Shell(l Layout, rail templ.Component) templ.Component {
296 if templ_7745c5c3_Err != nil {322 if templ_7745c5c3_Err != nil {
297 return templ_7745c5c3_Err323 return templ_7745c5c3_Err
298 }324 }
299- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "</main>")325+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "</main>")
300 if templ_7745c5c3_Err != nil {326 if templ_7745c5c3_Err != nil {
301 return templ_7745c5c3_Err327 return templ_7745c5c3_Err
302 }328 }
303 if l.HasRail && rail != nil {329 if l.HasRail && rail != nil {
304- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "<aside class=\"rail\" aria-label=\"Context\">")330+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "<aside class=\"rail\" aria-label=\"Context\">")
305 if templ_7745c5c3_Err != nil {331 if templ_7745c5c3_Err != nil {
306 return templ_7745c5c3_Err332 return templ_7745c5c3_Err
307 }333 }
@@ -309,12 +335,12 @@ func Shell(l Layout, rail templ.Component) templ.Component {
309 if templ_7745c5c3_Err != nil {335 if templ_7745c5c3_Err != nil {
310 return templ_7745c5c3_Err336 return templ_7745c5c3_Err
311 }337 }
312- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "</aside>")338+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "</aside>")
313 if templ_7745c5c3_Err != nil {339 if templ_7745c5c3_Err != nil {
314 return templ_7745c5c3_Err340 return templ_7745c5c3_Err
315 }341 }
316 }342 }
317- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "</div>")343+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "</div>")
318 if templ_7745c5c3_Err != nil {344 if templ_7745c5c3_Err != nil {
319 return templ_7745c5c3_Err345 return templ_7745c5c3_Err
320 }346 }
@@ -342,7 +368,7 @@ func Shell(l Layout, rail templ.Component) templ.Component {
342 if templ_7745c5c3_Err != nil {368 if templ_7745c5c3_Err != nil {
343 return templ_7745c5c3_Err369 return templ_7745c5c3_Err
344 }370 }
345- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "</body></html>")371+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "</body></html>")
346 if templ_7745c5c3_Err != nil {372 if templ_7745c5c3_Err != nil {
347 return templ_7745c5c3_Err373 return templ_7745c5c3_Err
348 }374 }
@@ -350,8 +376,8 @@ func Shell(l Layout, rail templ.Component) templ.Component {
350 })376 })
351 }377 }
352 378
353-// Header is the page header: the instance, the path, the ref switcher, the379+// Header is the page header: the logo (home), the path, the ref switcher,
354-// "go to" palette and the owner; inside a repository, the tabs below.380+// the "go to" palette and the owner; inside a repository, the tabs below.
355 func Header(l Layout) templ.Component {381 func Header(l Layout) templ.Component {
356 return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {382 return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
357 templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context383 templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
@@ -368,288 +394,288 @@ func Header(l Layout) templ.Component {
368 }()394 }()
369 }395 }
370 ctx = templ.InitializeContext(ctx)396 ctx = templ.InitializeContext(ctx)
371- templ_7745c5c3_Var20 := templ.GetChildren(ctx)397+ templ_7745c5c3_Var22 := templ.GetChildren(ctx)
372- if templ_7745c5c3_Var20 == nil {398+ if templ_7745c5c3_Var22 == nil {
373- templ_7745c5c3_Var20 = templ.NopComponent399+ templ_7745c5c3_Var22 = templ.NopComponent
374 }400 }
375 ctx = templ.ClearChildren(ctx)401 ctx = templ.ClearChildren(ctx)
376- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "<header class=\"hdr\"><div class=\"hdr-top\"><a class=\"brand\" href=\"/\" data-g=\"h\" title=\"home (g h)\"><span class=\"brand-mark\" aria-hidden=\"true\"></span> <span class=\"brand-name\">")402+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "<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> ")
377- if templ_7745c5c3_Err != nil {
378- return templ_7745c5c3_Err
379- }
380- var templ_7745c5c3_Var21 string
381- templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(l.Instance)
382- if templ_7745c5c3_Err != nil {
383- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 70, Col: 41}
384- }
385- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21))
386- if templ_7745c5c3_Err != nil {
387- return templ_7745c5c3_Err
388- }
389- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "</span></a> ")
390 if templ_7745c5c3_Err != nil {403 if templ_7745c5c3_Err != nil {
391 return templ_7745c5c3_Err404 return templ_7745c5c3_Err
392 }405 }
393 if len(l.Crumbs) > 0 {406 if len(l.Crumbs) > 0 {
394- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "<nav class=\"crumbs\" aria-label=\"Path\"><ol>")407+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "<nav class=\"crumbs\" aria-label=\"Path\"><ol>")
395 if templ_7745c5c3_Err != nil {408 if templ_7745c5c3_Err != nil {
396 return templ_7745c5c3_Err409 return templ_7745c5c3_Err
397 }410 }
398 for _, c := range l.Crumbs {411 for _, c := range l.Crumbs {
399- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "<li>")412+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "<li>")
400 if templ_7745c5c3_Err != nil {413 if templ_7745c5c3_Err != nil {
401 return templ_7745c5c3_Err414 return templ_7745c5c3_Err
402 }415 }
403 if c.Href != "" {416 if c.Href != "" {
404- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "<a href=\"")417+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "<a href=\"")
405 if templ_7745c5c3_Err != nil {418 if templ_7745c5c3_Err != nil {
406 return templ_7745c5c3_Err419 return templ_7745c5c3_Err
407 }420 }
408- var templ_7745c5c3_Var22 templ.SafeURL421+ var templ_7745c5c3_Var23 templ.SafeURL
409- templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(c.Href))422+ templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(c.Href))
410 if templ_7745c5c3_Err != nil {423 if templ_7745c5c3_Err != nil {
411- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 79, Col: 38}424+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 83, Col: 38}
412 }425 }
413- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22))426+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23))
414 if templ_7745c5c3_Err != nil {427 if templ_7745c5c3_Err != nil {
415 return templ_7745c5c3_Err428 return templ_7745c5c3_Err
416 }429 }
417- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "\"")430+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "\"")
418 if templ_7745c5c3_Err != nil {431 if templ_7745c5c3_Err != nil {
419 return templ_7745c5c3_Err432 return templ_7745c5c3_Err
420 }433 }
421 if c.Current {434 if c.Current {
422- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, " aria-current=\"page\"")435+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, " aria-current=\"page\"")
423 if templ_7745c5c3_Err != nil {436 if templ_7745c5c3_Err != nil {
424 return templ_7745c5c3_Err437 return templ_7745c5c3_Err
425 }438 }
426 }439 }
427- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, ">")440+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, ">")
428 if templ_7745c5c3_Err != nil {441 if templ_7745c5c3_Err != nil {
429 return templ_7745c5c3_Err442 return templ_7745c5c3_Err
430 }443 }
431- var templ_7745c5c3_Var23 string444+ var templ_7745c5c3_Var24 string
432- templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinStringErrs(c.Label)445+ templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinStringErrs(c.Label)
433 if templ_7745c5c3_Err != nil {446 if templ_7745c5c3_Err != nil {
434- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 83, Col: 19}447+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 87, Col: 19}
435 }448 }
436- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23))449+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24))
437 if templ_7745c5c3_Err != nil {450 if templ_7745c5c3_Err != nil {
438 return templ_7745c5c3_Err451 return templ_7745c5c3_Err
439 }452 }
440- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "</a>")453+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "</a>")
441 if templ_7745c5c3_Err != nil {454 if templ_7745c5c3_Err != nil {
442 return templ_7745c5c3_Err455 return templ_7745c5c3_Err
443 }456 }
444 } else {457 } else {
445- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "<span")458+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "<span")
446 if templ_7745c5c3_Err != nil {459 if templ_7745c5c3_Err != nil {
447 return templ_7745c5c3_Err460 return templ_7745c5c3_Err
448 }461 }
449 if c.Current {462 if c.Current {
450- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, " aria-current=\"page\"")463+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, " aria-current=\"page\"")
451 if templ_7745c5c3_Err != nil {464 if templ_7745c5c3_Err != nil {
452 return templ_7745c5c3_Err465 return templ_7745c5c3_Err
453 }466 }
454 }467 }
455- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, ">")468+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, ">")
456 if templ_7745c5c3_Err != nil {469 if templ_7745c5c3_Err != nil {
457 return templ_7745c5c3_Err470 return templ_7745c5c3_Err
458 }471 }
459- var templ_7745c5c3_Var24 string472+ var templ_7745c5c3_Var25 string
460- templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinStringErrs(c.Label)473+ templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.JoinStringErrs(c.Label)
461 if templ_7745c5c3_Err != nil {474 if templ_7745c5c3_Err != nil {
462- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 89, Col: 19}475+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 93, Col: 19}
463 }476 }
464- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24))477+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var25))
465 if templ_7745c5c3_Err != nil {478 if templ_7745c5c3_Err != nil {
466 return templ_7745c5c3_Err479 return templ_7745c5c3_Err
467 }480 }
468- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "</span>")481+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "</span>")
469 if templ_7745c5c3_Err != nil {482 if templ_7745c5c3_Err != nil {
470 return templ_7745c5c3_Err483 return templ_7745c5c3_Err
471 }484 }
472 }485 }
473- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "</li>")486+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "</li>")
474 if templ_7745c5c3_Err != nil {487 if templ_7745c5c3_Err != nil {
475 return templ_7745c5c3_Err488 return templ_7745c5c3_Err
476 }489 }
477 }490 }
478- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "</ol></nav>")491+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "</ol></nav>")
479 if templ_7745c5c3_Err != nil {492 if templ_7745c5c3_Err != nil {
480 return templ_7745c5c3_Err493 return templ_7745c5c3_Err
481 }494 }
482 }495 }
483- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "<div class=\"hdr-acts\">")496+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "<div class=\"hdr-acts\">")
484 if templ_7745c5c3_Err != nil {497 if templ_7745c5c3_Err != nil {
485 return templ_7745c5c3_Err498 return templ_7745c5c3_Err
486 }499 }
487 if l.Ref != "" {500 if l.Ref != "" {
488- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "<a class=\"btn ref\" href=\"")501+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "<a class=\"btn ref\" href=\"")
489 if templ_7745c5c3_Err != nil {502 if templ_7745c5c3_Err != nil {
490 return templ_7745c5c3_Err503 return templ_7745c5c3_Err
491 }504 }
492- var templ_7745c5c3_Var25 templ.SafeURL505+ var templ_7745c5c3_Var26 templ.SafeURL
493- templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(repoHref(l.Repo, "refs")))506+ templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(repoHref(l.Repo, "refs")))
494 if templ_7745c5c3_Err != nil {507 if templ_7745c5c3_Err != nil {
495- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 100, Col: 52}508+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 104, Col: 52}
496 }509 }
497- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var25))510+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var26))
498 if templ_7745c5c3_Err != nil {511 if templ_7745c5c3_Err != nil {
499 return templ_7745c5c3_Err512 return templ_7745c5c3_Err
500 }513 }
501- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "\" 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\">")514+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "\" 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\">")
502 if templ_7745c5c3_Err != nil {515 if templ_7745c5c3_Err != nil {
503 return templ_7745c5c3_Err516 return templ_7745c5c3_Err
504 }517 }
505- var templ_7745c5c3_Var26 string518+ var templ_7745c5c3_Var27 string
506- templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.JoinStringErrs(l.Ref)519+ templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.JoinStringErrs(l.Ref)
507 if templ_7745c5c3_Err != nil {520 if templ_7745c5c3_Err != nil {
508- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 106, Col: 36}521+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 110, Col: 36}
509 }522 }
510- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var26))523+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var27))
511 if templ_7745c5c3_Err != nil {524 if templ_7745c5c3_Err != nil {
512 return templ_7745c5c3_Err525 return templ_7745c5c3_Err
513 }526 }
514- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "</span> <span class=\"sha\">")527+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, "</span> <span class=\"sha\">")
515 if templ_7745c5c3_Err != nil {528 if templ_7745c5c3_Err != nil {
516 return templ_7745c5c3_Err529 return templ_7745c5c3_Err
517 }530 }
518- var templ_7745c5c3_Var27 string531+ var templ_7745c5c3_Var28 string
519- templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.JoinStringErrs(l.Short)532+ templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.JoinStringErrs(l.Short)
520 if templ_7745c5c3_Err != nil {533 if templ_7745c5c3_Err != nil {
521- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 107, Col: 33}534+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 111, Col: 33}
522 }535 }
523- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var27))536+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var28))
524 if templ_7745c5c3_Err != nil {537 if templ_7745c5c3_Err != nil {
525 return templ_7745c5c3_Err538 return templ_7745c5c3_Err
526 }539 }
527- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, "</span></a> ")540+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "</span></a> ")
528 if templ_7745c5c3_Err != nil {541 if templ_7745c5c3_Err != nil {
529 return templ_7745c5c3_Err542 return templ_7745c5c3_Err
530 }543 }
531 }544 }
532- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "<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> <button type=\"button\" class=\"btn icon\" data-js data-fire=\"help\" aria-haspopup=\"dialog\" aria-controls=\"panel-help\" aria-keyshortcuts=\"?\" aria-label=\"Keyboard shortcuts\" title=\"keyboard shortcuts (?)\">?</button> ")545+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, "<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> ")
533 if templ_7745c5c3_Err != nil {546 if templ_7745c5c3_Err != nil {
534 return templ_7745c5c3_Err547 return templ_7745c5c3_Err
535 }548 }
536 if SignedIn(ctx) {549 if SignedIn(ctx) {
537- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, "<form class=\"owner\" method=\"post\" action=\"/-/logout\"><span class=\"owner-badge\" title=\"signed in as the owner\">owner</span> ")550+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, "<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>")
551+ if templ_7745c5c3_Err != nil {
552+ return templ_7745c5c3_Err
553+ }
554+ var templ_7745c5c3_Var29 string
555+ templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.JoinStringErrs(l.Owner)
556+ if templ_7745c5c3_Err != nil {
557+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 130, Col: 123}
558+ }
559+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var29))
560+ if templ_7745c5c3_Err != nil {
561+ return templ_7745c5c3_Err
562+ }
563+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, "</span> ")
538 if templ_7745c5c3_Err != nil {564 if templ_7745c5c3_Err != nil {
539 return templ_7745c5c3_Err565 return templ_7745c5c3_Err
540 }566 }
541 if l.Passkeys {567 if l.Passkeys {
542- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, "<a class=\"btn\" href=\"/-/passkeys\" title=\"add or remove the passkeys that sign you in\">Passkeys</a> ")568+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, "<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> ")
543 if templ_7745c5c3_Err != nil {569 if templ_7745c5c3_Err != nil {
544 return templ_7745c5c3_Err570 return templ_7745c5c3_Err
545 }571 }
546 }572 }
547- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, "<button type=\"submit\" class=\"btn\" title=\"sign out of this browser\">Sign out</button></form>")573+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, "<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>")
548 if templ_7745c5c3_Err != nil {574 if templ_7745c5c3_Err != nil {
549 return templ_7745c5c3_Err575 return templ_7745c5c3_Err
550 }576 }
551 } else if l.Passkeys {577 } else if l.Passkeys {
552- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, "<a class=\"btn\" href=\"/-/login\" title=\"sign in with a passkey\">Sign in</a>")578+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, "<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>")
553 if templ_7745c5c3_Err != nil {579 if templ_7745c5c3_Err != nil {
554 return templ_7745c5c3_Err580 return templ_7745c5c3_Err
555 }581 }
556 }582 }
557- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, "</div></div>")583+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 56, "</div></div>")
558 if templ_7745c5c3_Err != nil {584 if templ_7745c5c3_Err != nil {
559 return templ_7745c5c3_Err585 return templ_7745c5c3_Err
560 }586 }
561 if len(l.Sections) > 0 {587 if len(l.Sections) > 0 {
562- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, "<nav class=\"tabs\" aria-label=\"Sections\"><ul>")588+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, "<nav class=\"tabs\" aria-label=\"Sections\"><ul>")
563 if templ_7745c5c3_Err != nil {589 if templ_7745c5c3_Err != nil {
564 return templ_7745c5c3_Err590 return templ_7745c5c3_Err
565 }591 }
566 for _, s := range l.Sections {592 for _, s := range l.Sections {
567- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 56, "<li><a href=\"")593+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "<li><a href=\"")
568 if templ_7745c5c3_Err != nil {594 if templ_7745c5c3_Err != nil {
569 return templ_7745c5c3_Err595 return templ_7745c5c3_Err
570 }596 }
571- var templ_7745c5c3_Var28 templ.SafeURL597+ var templ_7745c5c3_Var30 templ.SafeURL
572- templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(s.Href))598+ templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(s.Href))
573 if templ_7745c5c3_Err != nil {599 if templ_7745c5c3_Err != nil {
574- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 144, Col: 36}600+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 147, Col: 36}
575 }601 }
576- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var28))602+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var30))
577 if templ_7745c5c3_Err != nil {603 if templ_7745c5c3_Err != nil {
578 return templ_7745c5c3_Err604 return templ_7745c5c3_Err
579 }605 }
580- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, "\" data-key=\"")606+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, "\" data-key=\"")
581 if templ_7745c5c3_Err != nil {607 if templ_7745c5c3_Err != nil {
582 return templ_7745c5c3_Err608 return templ_7745c5c3_Err
583 }609 }
584- var templ_7745c5c3_Var29 string610+ var templ_7745c5c3_Var31 string
585- templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.ResolveAttributeValue(s.Key)611+ templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.ResolveAttributeValue(s.Key)
586 if templ_7745c5c3_Err != nil {612 if templ_7745c5c3_Err != nil {
587- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 145, Col: 24}613+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 148, Col: 24}
588 }614 }
589- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var29)615+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var31)
590 if templ_7745c5c3_Err != nil {616 if templ_7745c5c3_Err != nil {
591 return templ_7745c5c3_Err617 return templ_7745c5c3_Err
592 }618 }
593- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "\" aria-keyshortcuts=\"")619+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 60, "\" aria-keyshortcuts=\"")
594 if templ_7745c5c3_Err != nil {620 if templ_7745c5c3_Err != nil {
595 return templ_7745c5c3_Err621 return templ_7745c5c3_Err
596 }622 }
597- var templ_7745c5c3_Var30 string623+ var templ_7745c5c3_Var32 string
598- templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.ResolveAttributeValue(s.Key)624+ templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.ResolveAttributeValue(s.Key)
599 if templ_7745c5c3_Err != nil {625 if templ_7745c5c3_Err != nil {
600- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 146, Col: 33}626+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 149, Col: 33}
601 }627 }
602- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var30)628+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var32)
603 if templ_7745c5c3_Err != nil {629 if templ_7745c5c3_Err != nil {
604 return templ_7745c5c3_Err630 return templ_7745c5c3_Err
605 }631 }
606- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, "\"")632+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 61, "\"")
607 if templ_7745c5c3_Err != nil {633 if templ_7745c5c3_Err != nil {
608 return templ_7745c5c3_Err634 return templ_7745c5c3_Err
609 }635 }
610 if s.Current {636 if s.Current {
611- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 60, " aria-current=\"page\"")637+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 62, " aria-current=\"page\"")
612 if templ_7745c5c3_Err != nil {638 if templ_7745c5c3_Err != nil {
613 return templ_7745c5c3_Err639 return templ_7745c5c3_Err
614 }640 }
615 }641 }
616- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 61, ">")642+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 63, ">")
617 if templ_7745c5c3_Err != nil {643 if templ_7745c5c3_Err != nil {
618 return templ_7745c5c3_Err644 return templ_7745c5c3_Err
619 }645 }
620- var templ_7745c5c3_Var31 string646+ var templ_7745c5c3_Var33 string
621- templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.JoinStringErrs(s.Label)647+ templ_7745c5c3_Var33, templ_7745c5c3_Err = templ.JoinStringErrs(s.Label)
622 if templ_7745c5c3_Err != nil {648 if templ_7745c5c3_Err != nil {
623- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 150, Col: 17}649+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 153, Col: 17}
624 }650 }
625- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var31))651+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var33))
626 if templ_7745c5c3_Err != nil {652 if templ_7745c5c3_Err != nil {
627 return templ_7745c5c3_Err653 return templ_7745c5c3_Err
628 }654 }
629- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 62, "<kbd aria-hidden=\"true\">")655+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 64, "<kbd aria-hidden=\"true\">")
630 if templ_7745c5c3_Err != nil {656 if templ_7745c5c3_Err != nil {
631 return templ_7745c5c3_Err657 return templ_7745c5c3_Err
632 }658 }
633- var templ_7745c5c3_Var32 string659+ var templ_7745c5c3_Var34 string
634- templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.JoinStringErrs(s.Key)660+ templ_7745c5c3_Var34, templ_7745c5c3_Err = templ.JoinStringErrs(s.Key)
635 if templ_7745c5c3_Err != nil {661 if templ_7745c5c3_Err != nil {
636- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 150, Col: 50}662+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 153, Col: 50}
637 }663 }
638- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var32))664+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var34))
639 if templ_7745c5c3_Err != nil {665 if templ_7745c5c3_Err != nil {
640 return templ_7745c5c3_Err666 return templ_7745c5c3_Err
641 }667 }
642- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 63, "</kbd></a></li>")668+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 65, "</kbd></a></li>")
643 if templ_7745c5c3_Err != nil {669 if templ_7745c5c3_Err != nil {
644 return templ_7745c5c3_Err670 return templ_7745c5c3_Err
645 }671 }
646 }672 }
647- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 64, "</ul></nav>")673+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 66, "</ul></nav>")
648 if templ_7745c5c3_Err != nil {674 if templ_7745c5c3_Err != nil {
649 return templ_7745c5c3_Err675 return templ_7745c5c3_Err
650 }676 }
651 }677 }
652- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 65, "</header>")678+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 67, "</header>")
653 if templ_7745c5c3_Err != nil {679 if templ_7745c5c3_Err != nil {
654 return templ_7745c5c3_Err680 return templ_7745c5c3_Err
655 }681 }
@@ -675,38 +701,38 @@ func Footer(l Layout) templ.Component {
675 }()701 }()
676 }702 }
677 ctx = templ.InitializeContext(ctx)703 ctx = templ.InitializeContext(ctx)
678- templ_7745c5c3_Var33 := templ.GetChildren(ctx)704+ templ_7745c5c3_Var35 := templ.GetChildren(ctx)
679- if templ_7745c5c3_Var33 == nil {705+ if templ_7745c5c3_Var35 == nil {
680- templ_7745c5c3_Var33 = templ.NopComponent706+ templ_7745c5c3_Var35 = templ.NopComponent
681 }707 }
682 ctx = templ.ClearChildren(ctx)708 ctx = templ.ClearChildren(ctx)
683- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 66, "<footer class=\"foot\"><span>ermite ")709+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 68, "<footer class=\"foot\"><span>ermite ")
684 if templ_7745c5c3_Err != nil {710 if templ_7745c5c3_Err != nil {
685 return templ_7745c5c3_Err711 return templ_7745c5c3_Err
686 }712 }
687- var templ_7745c5c3_Var34 string713+ var templ_7745c5c3_Var36 string
688- templ_7745c5c3_Var34, templ_7745c5c3_Err = templ.JoinStringErrs(l.Version)714+ templ_7745c5c3_Var36, templ_7745c5c3_Err = templ.JoinStringErrs(l.Version)
689 if templ_7745c5c3_Err != nil {715 if templ_7745c5c3_Err != nil {
690- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 163, Col: 26}716+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 166, Col: 26}
691 }717 }
692- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var34))718+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var36))
693 if templ_7745c5c3_Err != nil {719 if templ_7745c5c3_Err != nil {
694 return templ_7745c5c3_Err720 return templ_7745c5c3_Err
695 }721 }
696- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 67, "</span> <a href=\"")722+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 69, "</span> <a href=\"")
697 if templ_7745c5c3_Err != nil {723 if templ_7745c5c3_Err != nil {
698 return templ_7745c5c3_Err724 return templ_7745c5c3_Err
699 }725 }
700- var templ_7745c5c3_Var35 templ.SafeURL726+ var templ_7745c5c3_Var37 templ.SafeURL
701- templ_7745c5c3_Var35, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(l.SourceURL))727+ templ_7745c5c3_Var37, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(l.SourceURL))
702 if templ_7745c5c3_Err != nil {728 if templ_7745c5c3_Err != nil {
703- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 164, Col: 38}729+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 167, Col: 38}
704 }730 }
705- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var35))731+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var37))
706 if templ_7745c5c3_Err != nil {732 if templ_7745c5c3_Err != nil {
707 return templ_7745c5c3_Err733 return templ_7745c5c3_Err
708 }734 }
709- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 68, "\" rel=\"noopener\" title=\"source of this version (AGPL-3.0-or-later)\">source</a> <span class=\"foot-keys\"><kbd>?</kbd> keyboard shortcuts</span></footer>")735+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 70, "\" 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>")
710 if templ_7745c5c3_Err != nil {736 if templ_7745c5c3_Err != nil {
711 return templ_7745c5c3_Err737 return templ_7745c5c3_Err
712 }738 }
@@ -734,12 +760,12 @@ func Hint(l Layout) templ.Component {
734 }()760 }()
735 }761 }
736 ctx = templ.InitializeContext(ctx)762 ctx = templ.InitializeContext(ctx)
737- templ_7745c5c3_Var36 := templ.GetChildren(ctx)763+ templ_7745c5c3_Var38 := templ.GetChildren(ctx)
738- if templ_7745c5c3_Var36 == nil {764+ if templ_7745c5c3_Var38 == nil {
739- templ_7745c5c3_Var36 = templ.NopComponent765+ templ_7745c5c3_Var38 = templ.NopComponent
740 }766 }
741 ctx = templ.ClearChildren(ctx)767 ctx = templ.ClearChildren(ctx)
742- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 69, "<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>")768+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 71, "<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>")
743 if templ_7745c5c3_Err != nil {769 if templ_7745c5c3_Err != nil {
744 return templ_7745c5c3_Err770 return templ_7745c5c3_Err
745 }771 }
@@ -747,7 +773,7 @@ func Hint(l Layout) templ.Component {
747 if templ_7745c5c3_Err != nil {773 if templ_7745c5c3_Err != nil {
748 return templ_7745c5c3_Err774 return templ_7745c5c3_Err
749 }775 }
750- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 70, "</div>")776+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 72, "</div>")
751 if templ_7745c5c3_Err != nil {777 if templ_7745c5c3_Err != nil {
752 return templ_7745c5c3_Err778 return templ_7745c5c3_Err
753 }779 }
A internal/adapters/web/static/app/apple-touch-icon.png +0 -0 binary file: no text diff

binary file: no text diff

A internal/adapters/web/static/app/icon-192.png +0 -0 binary file: no text diff

binary file: no text diff

A internal/adapters/web/static/app/icon-512.png +0 -0 binary file: no text diff

binary file: no text diff

A internal/adapters/web/static/app/icon-maskable-512.png +0 -0 binary file: no text diff

binary file: no text diff

M internal/adapters/web/static/css/10-base.css +3 -0
@@ -17,6 +17,9 @@
1717 /* a cross-document fade between pages, where the browser has it */
1818 @view-transition { navigation: auto; }
1919 ::view-transition-old(root), ::view-transition-new(root) { animation-duration: 120ms; }
20+ /* the snapshots fade over this layer: transparent, it shows a white canvas
21+ for a frame when the new page is a little late */
22+ ::view-transition { background: var(--bg-canvas); }
2023 @media (prefers-reduced-motion: reduce) {
2124 @view-transition { navigation: none; }
2225 }
@@ -17,6 +17,9 @@
17 /* a cross-document fade between pages, where the browser has it */17 /* a cross-document fade between pages, where the browser has it */
18 @view-transition { navigation: auto; }18 @view-transition { navigation: auto; }
19 ::view-transition-old(root), ::view-transition-new(root) { animation-duration: 120ms; }19 ::view-transition-old(root), ::view-transition-new(root) { animation-duration: 120ms; }
20+ /* the snapshots fade over this layer: transparent, it shows a white canvas
21+ for a frame when the new page is a little late */
22+ ::view-transition { background: var(--bg-canvas); }
20 @media (prefers-reduced-motion: reduce) {23 @media (prefers-reduced-motion: reduce) {
21 @view-transition { navigation: none; }24 @view-transition { navigation: none; }
22 }25 }
M internal/adapters/web/static/css/20-shell.css +15 -8
@@ -21,6 +21,10 @@
2121 .i-search { --i: url(icons/search.svg); }
2222 .i-file { --i: url(icons/file.svg); }
2323 .i-tag { --i: url(icons/tag.svg); }
24+ .i-user { --i: url(icons/user.svg); }
25+ .i-key { --i: url(icons/key.svg); }
26+ .i-log-in { --i: url(icons/log-in.svg); }
27+ .i-log-out { --i: url(icons/log-out.svg); }
2428
2529 .pill {
2630 display: inline-flex; align-items: center; height: 20px; padding: 0 var(--s2); margin-left: var(--s1);
@@ -48,9 +52,7 @@
4852 max-width: var(--page); margin: 0 auto; height: 56px; padding: 0 var(--pad);
4953 }
5054 .brand {
51- display: flex; align-items: center; gap: var(--s2); flex: none;
52- font: 700 var(--fs) / 1 var(--mono); color: var(--ink-hi); text-decoration: none;
53- & .brand-name { max-width: 22ch; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
55+ display: flex; align-items: center; flex: none; text-decoration: none;
5456 &:hover { text-decoration: none; }
5557 }
5658 /* the hermit by his fire: a grey picture in its own rounded square */
@@ -78,7 +80,10 @@
7880 }
7981 .owner {
8082 display: flex; align-items: center; gap: var(--s2);
81- & .owner-badge { font: 600 var(--fs-xs) / 1 var(--mono); color: var(--ink-dim); padding: 0 var(--s1); }
83+ & .owner-badge {
84+ display: inline-flex; align-items: center; gap: 6px; max-width: 20ch; padding: 0 var(--s1);
85+ font: 600 var(--fs-s) / 1 var(--mono); color: var(--ink-dim); overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
86+ }
8287 }
8388
8489 .tabs {
@@ -103,6 +108,9 @@
103108 flex: 1; width: 100%; max-width: var(--page); margin: 0 auto;
104109 padding: var(--s5) var(--pad) var(--s7);
105110 display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--s5); align-items: start;
111+ /* .work grows to fill the page (flex: 1): without this, the rows share the
112+ spare height and a short content pushes the rail down */
113+ align-content: start;
106114 }
107115 .work.has-rail { grid-template-columns: minmax(0, 1fr) var(--rail); }
108116 .rail { display: flex; flex-direction: column; gap: var(--s4); min-width: 0; }
@@ -156,7 +164,8 @@
156164 font-size: var(--fs-s); color: var(--ink-mute);
157165 &::before { content: ""; flex-basis: 100%; height: 1px; margin-bottom: var(--s2); background: var(--border); }
158166 & a { color: var(--ink-dim); }
159- & .foot-keys { margin-left: auto; }
167+ & .foot-keys { margin-left: auto; color: var(--ink-dim); }
168+ & .foot-keys:hover { color: var(--ink-hi); }
160169 }
161170
162171 /* hint: bottom right, only while it has something to say */
@@ -187,14 +196,12 @@
187196 @media (max-width: 720px) {
188197 :root { --pad: var(--s4); }
189198 .hdr-top { gap: var(--s3); height: 52px; }
190- .brand-name { display: none; }
191199 .crumbs li:not(:last-child) { display: none; }
192- .ref .sha, .owner-badge, .hdr-acts .icon { display: none; }
200+ .ref .sha, .owner .owner-badge { display: none; }
193201 .ref { max-width: 16ch; }
194202 .work { padding-top: var(--s4); }
195203 .rail { grid-template-columns: minmax(0, 1fr); }
196204 .tabs kbd { display: none; }
197- .foot .foot-keys { display: none; }
198205 .list .c-who, .list .c-desc, .list .c-sub, .list .c-ref, .list .c-size, .list .c-lang { display: none; }
199206 }
200207 }
@@ -21,6 +21,10 @@
21 .i-search { --i: url(icons/search.svg); }21 .i-search { --i: url(icons/search.svg); }
22 .i-file { --i: url(icons/file.svg); }22 .i-file { --i: url(icons/file.svg); }
23 .i-tag { --i: url(icons/tag.svg); }23 .i-tag { --i: url(icons/tag.svg); }
24+ .i-user { --i: url(icons/user.svg); }
25+ .i-key { --i: url(icons/key.svg); }
26+ .i-log-in { --i: url(icons/log-in.svg); }
27+ .i-log-out { --i: url(icons/log-out.svg); }
24 28
25 .pill {29 .pill {
26 display: inline-flex; align-items: center; height: 20px; padding: 0 var(--s2); margin-left: var(--s1);30 display: inline-flex; align-items: center; height: 20px; padding: 0 var(--s2); margin-left: var(--s1);
@@ -48,9 +52,7 @@
48 max-width: var(--page); margin: 0 auto; height: 56px; padding: 0 var(--pad);52 max-width: var(--page); margin: 0 auto; height: 56px; padding: 0 var(--pad);
49 }53 }
50 .brand {54 .brand {
51- display: flex; align-items: center; gap: var(--s2); flex: none;55+ display: flex; align-items: center; flex: none; text-decoration: none;
52- font: 700 var(--fs) / 1 var(--mono); color: var(--ink-hi); text-decoration: none;
53- & .brand-name { max-width: 22ch; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
54 &:hover { text-decoration: none; }56 &:hover { text-decoration: none; }
55 }57 }
56 /* the hermit by his fire: a grey picture in its own rounded square */58 /* the hermit by his fire: a grey picture in its own rounded square */
@@ -78,7 +80,10 @@
78 }80 }
79 .owner {81 .owner {
80 display: flex; align-items: center; gap: var(--s2);82 display: flex; align-items: center; gap: var(--s2);
81- & .owner-badge { font: 600 var(--fs-xs) / 1 var(--mono); color: var(--ink-dim); padding: 0 var(--s1); }83+ & .owner-badge {
84+ display: inline-flex; align-items: center; gap: 6px; max-width: 20ch; padding: 0 var(--s1);
85+ font: 600 var(--fs-s) / 1 var(--mono); color: var(--ink-dim); overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
86+ }
82 }87 }
83 88
84 .tabs {89 .tabs {
@@ -103,6 +108,9 @@
103 flex: 1; width: 100%; max-width: var(--page); margin: 0 auto;108 flex: 1; width: 100%; max-width: var(--page); margin: 0 auto;
104 padding: var(--s5) var(--pad) var(--s7);109 padding: var(--s5) var(--pad) var(--s7);
105 display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--s5); align-items: start;110 display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--s5); align-items: start;
111+ /* .work grows to fill the page (flex: 1): without this, the rows share the
112+ spare height and a short content pushes the rail down */
113+ align-content: start;
106 }114 }
107 .work.has-rail { grid-template-columns: minmax(0, 1fr) var(--rail); }115 .work.has-rail { grid-template-columns: minmax(0, 1fr) var(--rail); }
108 .rail { display: flex; flex-direction: column; gap: var(--s4); min-width: 0; }116 .rail { display: flex; flex-direction: column; gap: var(--s4); min-width: 0; }
@@ -156,7 +164,8 @@
156 font-size: var(--fs-s); color: var(--ink-mute);164 font-size: var(--fs-s); color: var(--ink-mute);
157 &::before { content: ""; flex-basis: 100%; height: 1px; margin-bottom: var(--s2); background: var(--border); }165 &::before { content: ""; flex-basis: 100%; height: 1px; margin-bottom: var(--s2); background: var(--border); }
158 & a { color: var(--ink-dim); }166 & a { color: var(--ink-dim); }
159- & .foot-keys { margin-left: auto; }167+ & .foot-keys { margin-left: auto; color: var(--ink-dim); }
168+ & .foot-keys:hover { color: var(--ink-hi); }
160 }169 }
161 170
162 /* hint: bottom right, only while it has something to say */171 /* hint: bottom right, only while it has something to say */
@@ -187,14 +196,12 @@
187 @media (max-width: 720px) {196 @media (max-width: 720px) {
188 :root { --pad: var(--s4); }197 :root { --pad: var(--s4); }
189 .hdr-top { gap: var(--s3); height: 52px; }198 .hdr-top { gap: var(--s3); height: 52px; }
190- .brand-name { display: none; }
191 .crumbs li:not(:last-child) { display: none; }199 .crumbs li:not(:last-child) { display: none; }
192- .ref .sha, .owner-badge, .hdr-acts .icon { display: none; }200+ .ref .sha, .owner .owner-badge { display: none; }
193 .ref { max-width: 16ch; }201 .ref { max-width: 16ch; }
194 .work { padding-top: var(--s4); }202 .work { padding-top: var(--s4); }
195 .rail { grid-template-columns: minmax(0, 1fr); }203 .rail { grid-template-columns: minmax(0, 1fr); }
196 .tabs kbd { display: none; }204 .tabs kbd { display: none; }
197- .foot .foot-keys { display: none; }
198 .list .c-who, .list .c-desc, .list .c-sub, .list .c-ref, .list .c-size, .list .c-lang { display: none; }205 .list .c-who, .list .c-desc, .list .c-sub, .list .c-ref, .list .c-size, .list .c-lang { display: none; }
199 }206 }
200 }207 }
M internal/adapters/web/static/icons/branch.svg +1 -1
@@ -1,1 +1,1 @@
1-<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="none" stroke="#000" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="5" cy="3.5" r="1.5"/><circle cx="5" cy="12.5" r="1.5"/><circle cx="11" cy="4.5" r="1.5"/><path d="M5 5v6M11 6c0 3.5-6 2-6 5"/></svg>⏎̸
1+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 6a9 9 0 0 0-9 9V3"/><circle cx="18" cy="6" r="3"/><circle cx="6" cy="18" r="3"/></svg>
@@ -1,1 +1,1 @@
1-<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="none" stroke="#000" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="5" cy="3.5" r="1.5"/><circle cx="5" cy="12.5" r="1.5"/><circle cx="11" cy="4.5" r="1.5"/><path d="M5 5v6M11 6c0 3.5-6 2-6 5"/></svg>⏎̸1+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 6a9 9 0 0 0-9 9V3"/><circle cx="18" cy="6" r="3"/><circle cx="6" cy="18" r="3"/></svg>
M internal/adapters/web/static/icons/file.svg +1 -1
@@ -1,1 +1,1 @@
1-<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="none" stroke="#000" stroke-width="1.5" stroke-linejoin="round" stroke-linecap="round"><path d="M3.5 1.5h6l3 3v10h-9zM9.5 1.5v3h3M6 8h4M6 11h4"/></svg>⏎̸
1+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z"/><path d="M14 2v5a1 1 0 0 0 1 1h5"/><path d="M10 9H8"/><path d="M16 13H8"/><path d="M16 17H8"/></svg>
@@ -1,1 +1,1 @@
1-<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="none" stroke="#000" stroke-width="1.5" stroke-linejoin="round" stroke-linecap="round"><path d="M3.5 1.5h6l3 3v10h-9zM9.5 1.5v3h3M6 8h4M6 11h4"/></svg>⏎̸1+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z"/><path d="M14 2v5a1 1 0 0 0 1 1h5"/><path d="M10 9H8"/><path d="M16 13H8"/><path d="M16 17H8"/></svg>
A internal/adapters/web/static/icons/key.svg +1 -0
@@ -0,0 +1,1 @@
1+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2.586 17.414A2 2 0 0 0 2 18.828V21a1 1 0 0 0 1 1h3a1 1 0 0 0 1-1v-1a1 1 0 0 1 1-1h1a1 1 0 0 0 1-1v-1a1 1 0 0 1 1-1h.172a2 2 0 0 0 1.414-.586l.814-.814a6.5 6.5 0 1 0-4-4z"/><circle cx="16.5" cy="7.5" r=".5" fill="#000"/></svg>
@@ -0,0 +1,1 @@
1+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2.586 17.414A2 2 0 0 0 2 18.828V21a1 1 0 0 0 1 1h3a1 1 0 0 0 1-1v-1a1 1 0 0 1 1-1h1a1 1 0 0 0 1-1v-1a1 1 0 0 1 1-1h.172a2 2 0 0 0 1.414-.586l.814-.814a6.5 6.5 0 1 0-4-4z"/><circle cx="16.5" cy="7.5" r=".5" fill="#000"/></svg>
A internal/adapters/web/static/icons/log-in.svg +1 -0
@@ -0,0 +1,1 @@
1+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m10 17 5-5-5-5"/><path d="M15 12H3"/><path d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4"/></svg>
@@ -0,0 +1,1 @@
1+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m10 17 5-5-5-5"/><path d="M15 12H3"/><path d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4"/></svg>
A internal/adapters/web/static/icons/log-out.svg +1 -0
@@ -0,0 +1,1 @@
1+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m16 17 5-5-5-5"/><path d="M21 12H9"/><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/></svg>
@@ -0,0 +1,1 @@
1+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m16 17 5-5-5-5"/><path d="M21 12H9"/><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/></svg>
M internal/adapters/web/static/icons/search.svg +1 -1
@@ -1,1 +1,1 @@
1-<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="none" stroke="#000" stroke-width="1.5" stroke-linecap="round"><circle cx="7" cy="7" r="4.5"/><path d="m10.5 10.5 3.5 3.5"/></svg>⏎̸
1+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m21 21-4.34-4.34"/><circle cx="11" cy="11" r="8"/></svg>
@@ -1,1 +1,1 @@
1-<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="none" stroke="#000" stroke-width="1.5" stroke-linecap="round"><circle cx="7" cy="7" r="4.5"/><path d="m10.5 10.5 3.5 3.5"/></svg>⏎̸1+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m21 21-4.34-4.34"/><circle cx="11" cy="11" r="8"/></svg>
M internal/adapters/web/static/icons/tag.svg +1 -1
@@ -1,1 +1,1 @@
1-<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="none" stroke="#000" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M2 2.5v5l6.5 6.5 5-5L7 2.5z"/><circle cx="5" cy="5.5" r="1"/></svg>
1+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12.586 2.586A2 2 0 0 0 11.172 2H4a2 2 0 0 0-2 2v7.172a2 2 0 0 0 .586 1.414l8.704 8.704a2.426 2.426 0 0 0 3.42 0l6.58-6.58a2.426 2.426 0 0 0 0-3.42z"/><circle cx="7.5" cy="7.5" r=".5" fill="#000"/></svg>
@@ -1,1 +1,1 @@
1-<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="none" stroke="#000" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M2 2.5v5l6.5 6.5 5-5L7 2.5z"/><circle cx="5" cy="5.5" r="1"/></svg>1+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12.586 2.586A2 2 0 0 0 11.172 2H4a2 2 0 0 0-2 2v7.172a2 2 0 0 0 .586 1.414l8.704 8.704a2.426 2.426 0 0 0 3.42 0l6.58-6.58a2.426 2.426 0 0 0 0-3.42z"/><circle cx="7.5" cy="7.5" r=".5" fill="#000"/></svg>
A internal/adapters/web/static/icons/user.svg +1 -0
@@ -0,0 +1,1 @@
1+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="8" r="5"/><path d="M20 21a8 8 0 0 0-16 0"/></svg>
@@ -0,0 +1,1 @@
1+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="8" r="5"/><path d="M20 21a8 8 0 0 0-16 0"/></svg>
M internal/adapters/web/static/js/ermite.js +26 -0
@@ -255,6 +255,15 @@
255255 a.setAttribute('aria-controls', 'panel-refs');
256256 }
257257
258+ // A login link signs in without a click: its page submits its form once a
259+ // browser shows it, not while it is prerendered.
260+ const auto = $('form[data-autosubmit]');
261+ if (auto) {
262+ const go = () => auto.requestSubmit();
263+ if (document.prerendering) document.addEventListener('prerenderingchange', go, { once: true });
264+ else go();
265+ }
266+
258267 // In the app shell the page does not scroll, <main> does: give it focus so arrows,
259268 // PageUp/PageDown and space scroll the content without a click first.
260269 if (!location.hash) document.getElementById('main')?.focus({ preventScroll: true });
@@ -310,7 +319,24 @@
310319 window.ermiteLive = (() => {
311320 let reloading = false;
312321 let stopped = false;
322+ // A stream that drops and comes back within LOST_AFTER (a proxy or the
323+ // server closing it, a network hiccup) is not worth a message.
324+ const LOST_AFTER = 5000;
325+ let lostTimer = 0;
326+ const setState = (s) => { const el = document.querySelector('.hint-live'); if (el) el.dataset.state = s; };
313327 return {
328+ // lost is htmx:sse:close and htmx:sse:error: "live updates paused" shows
329+ // only if the stream is still down LOST_AFTER later.
330+ lost() {
331+ if (lostTimer) return;
332+ lostTimer = setTimeout(() => { lostTimer = 0; setState('offline'); }, LOST_AFTER);
333+ },
334+ // back is htmx:sse:after:connection.
335+ back() {
336+ clearTimeout(lostTimer);
337+ lostTimer = 0;
338+ setState('live');
339+ },
314340 swap(html) {
315341 const t = document.createElement('template');
316342 t.innerHTML = html;
@@ -255,6 +255,15 @@
255 a.setAttribute('aria-controls', 'panel-refs');255 a.setAttribute('aria-controls', 'panel-refs');
256 }256 }
257 257
258+ // A login link signs in without a click: its page submits its form once a
259+ // browser shows it, not while it is prerendered.
260+ const auto = $('form[data-autosubmit]');
261+ if (auto) {
262+ const go = () => auto.requestSubmit();
263+ if (document.prerendering) document.addEventListener('prerenderingchange', go, { once: true });
264+ else go();
265+ }
266+
258 // In the app shell the page does not scroll, <main> does: give it focus so arrows,267 // In the app shell the page does not scroll, <main> does: give it focus so arrows,
259 // PageUp/PageDown and space scroll the content without a click first.268 // PageUp/PageDown and space scroll the content without a click first.
260 if (!location.hash) document.getElementById('main')?.focus({ preventScroll: true });269 if (!location.hash) document.getElementById('main')?.focus({ preventScroll: true });
@@ -310,7 +319,24 @@
310 window.ermiteLive = (() => {319 window.ermiteLive = (() => {
311 let reloading = false;320 let reloading = false;
312 let stopped = false;321 let stopped = false;
322+ // A stream that drops and comes back within LOST_AFTER (a proxy or the
323+ // server closing it, a network hiccup) is not worth a message.
324+ const LOST_AFTER = 5000;
325+ let lostTimer = 0;
326+ const setState = (s) => { const el = document.querySelector('.hint-live'); if (el) el.dataset.state = s; };
313 return {327 return {
328+ // lost is htmx:sse:close and htmx:sse:error: "live updates paused" shows
329+ // only if the stream is still down LOST_AFTER later.
330+ lost() {
331+ if (lostTimer) return;
332+ lostTimer = setTimeout(() => { lostTimer = 0; setState('offline'); }, LOST_AFTER);
333+ },
334+ // back is htmx:sse:after:connection.
335+ back() {
336+ clearTimeout(lostTimer);
337+ lostTimer = 0;
338+ setState('live');
339+ },
314 swap(html) {340 swap(html) {
315 const t = document.createElement('template');341 const t = document.createElement('template');
316 t.innerHTML = html;342 t.innerHTML = html;
M internal/adapters/web/view.go +3 -2
@@ -34,8 +34,9 @@ type CmdItem struct{ Kind, Label, Href string }
3434
3535 // Layout is the input of the page shell.
3636 type Layout struct {
37- Title string
38- Instance string
37+ Title string
38+ // Owner is the owner's name (ERMITE_OWNER), shown when signed in.
39+ Owner string
3940 Version string
4041 SourceURL string
4142 Assets Assets
@@ -34,8 +34,9 @@ type CmdItem struct{ Kind, Label, Href string }
34 34
35 // Layout is the input of the page shell.35 // Layout is the input of the page shell.
36 type Layout struct {36 type Layout struct {
37- Title string37+ Title string
38- Instance string38+ // Owner is the owner's name (ERMITE_OWNER), shown when signed in.
39+ Owner string
39 Version string40 Version string
40 SourceURL string41 SourceURL string
41 Assets Assets42 Assets Assets
M internal/adapters/web/web.go +1 -1
@@ -390,7 +390,7 @@ func (s *Server) notFound(context.Context, *http.Request) (templ.Component, int,
390390 // base is the Layout every page starts from.
391391 func (s *Server) base() Layout {
392392 return Layout{
393- Instance: s.instance,
393+ Owner: s.cfg.Owner,
394394 Version: s.cfg.Version,
395395 SourceURL: s.cfg.SourceURL,
396396 Assets: s.assets.urls,
@@ -390,7 +390,7 @@ func (s *Server) notFound(context.Context, *http.Request) (templ.Component, int,
390 // base is the Layout every page starts from.390 // base is the Layout every page starts from.
391 func (s *Server) base() Layout {391 func (s *Server) base() Layout {
392 return Layout{392 return Layout{
393- Instance: s.instance,393+ Owner: s.cfg.Owner,
394 Version: s.cfg.Version,394 Version: s.cfg.Version,
395 SourceURL: s.cfg.SourceURL,395 SourceURL: s.cfg.SourceURL,
396 Assets: s.assets.urls,396 Assets: s.assets.urls,
M internal/adapters/webauthn/webauthn.go +13 -11
@@ -19,20 +19,21 @@ import (
1919 "bnema.dev/ermite/internal/ports"
2020 )
2121
22-// ownerName names the one user in the options sent to the browser.
23-const ownerName = "owner"
24-
2522 // Verifier runs the WebAuthn ceremonies for the owner of one public URL.
2623 type Verifier struct {
2724 web *gowebauthn.WebAuthn
25+ // name names the one user in the options sent to the browser: the
26+ // owner's name (ERMITE_OWNER), which password managers show.
27+ name string
2828 }
2929
3030 var _ ports.PasskeyVerifier = (*Verifier)(nil)
3131
3232 // New builds a verifier for publicURL: the relying party ID is its host name,
3333 // the only accepted origin is its origin. It fails when the library refuses the
34-// configuration, for example for a host that is an IP address.
35-func New(publicURL string) (*Verifier, error) {
34+// configuration, for example for a host that is an IP address. ownerName is
35+// the name the browser shows for the passkey.
36+func New(publicURL, ownerName string) (*Verifier, error) {
3637 u, err := url.Parse(publicURL)
3738 if err != nil {
3839 return nil, fmt.Errorf("passkeys: parse public URL: %w", err)
@@ -57,18 +58,19 @@ func New(publicURL string) (*Verifier, error) {
5758 if err != nil {
5859 return nil, fmt.Errorf("passkeys: %w", err)
5960 }
60- return &Verifier{web: web}, nil
61+ return &Verifier{web: web, name: ownerName}, nil
6162 }
6263
6364 // owner is the single WebAuthn user.
6465 type owner struct {
66+ name string
6567 handle []byte
6668 credentials []gowebauthn.Credential
6769 }
6870
6971 func (o owner) WebAuthnID() []byte { return o.handle }
70-func (o owner) WebAuthnName() string { return ownerName }
71-func (o owner) WebAuthnDisplayName() string { return ownerName }
72+func (o owner) WebAuthnName() string { return o.name }
73+func (o owner) WebAuthnDisplayName() string { return o.name }
7274 func (o owner) WebAuthnCredentials() []gowebauthn.Credential { return o.credentials }
7375
7476 // refuse builds the error of a failed verification: an ErrUnauthenticated whose
@@ -84,7 +86,7 @@ func (v *Verifier) BeginRegistration(userHandle []byte, existing []domain.Passke
8486 for _, p := range existing {
8587 exclude = append(exclude, descriptor(p))
8688 }
87- creation, session, err := v.web.BeginRegistration(owner{handle: userHandle},
89+ creation, session, err := v.web.BeginRegistration(owner{name: v.name, handle: userHandle},
8890 gowebauthn.WithResidentKeyRequirement(protocol.ResidentKeyRequirementRequired),
8991 gowebauthn.WithExclusions(exclude),
9092 gowebauthn.WithConveyancePreference(protocol.PreferNoAttestation),
@@ -105,7 +107,7 @@ func (v *Verifier) FinishRegistration(userHandle []byte, ceremony, response []by
105107 if err != nil {
106108 return domain.Passkey{}, refuse("registration", err)
107109 }
108- cred, err := v.web.CreateCredential(owner{handle: userHandle}, session, parsed)
110+ cred, err := v.web.CreateCredential(owner{name: v.name, handle: userHandle}, session, parsed)
109111 if err != nil {
110112 return domain.Passkey{}, refuse("registration", err)
111113 }
@@ -158,7 +160,7 @@ func (v *Verifier) FinishLogin(userHandle []byte, ceremony, response []byte, loo
158160 return nil, err
159161 }
160162 stored = p
161- return owner{handle: userHandle, credentials: []gowebauthn.Credential{credential(p)}}, nil
163+ return owner{name: v.name, handle: userHandle, credentials: []gowebauthn.Credential{credential(p)}}, nil
162164 }
163165 _, cred, err := v.web.ValidatePasskeyLogin(handler, session, parsed)
164166 if lookupErr != nil {
@@ -19,20 +19,21 @@ import (
19 "bnema.dev/ermite/internal/ports"19 "bnema.dev/ermite/internal/ports"
20 )20 )
21 21
22-// ownerName names the one user in the options sent to the browser.
23-const ownerName = "owner"
24-
25 // Verifier runs the WebAuthn ceremonies for the owner of one public URL.22 // Verifier runs the WebAuthn ceremonies for the owner of one public URL.
26 type Verifier struct {23 type Verifier struct {
27 web *gowebauthn.WebAuthn24 web *gowebauthn.WebAuthn
25+ // name names the one user in the options sent to the browser: the
26+ // owner's name (ERMITE_OWNER), which password managers show.
27+ name string
28 }28 }
29 29
30 var _ ports.PasskeyVerifier = (*Verifier)(nil)30 var _ ports.PasskeyVerifier = (*Verifier)(nil)
31 31
32 // New builds a verifier for publicURL: the relying party ID is its host name,32 // New builds a verifier for publicURL: the relying party ID is its host name,
33 // the only accepted origin is its origin. It fails when the library refuses the33 // the only accepted origin is its origin. It fails when the library refuses the
34-// configuration, for example for a host that is an IP address.34+// configuration, for example for a host that is an IP address. ownerName is
35-func New(publicURL string) (*Verifier, error) {35+// the name the browser shows for the passkey.
36+func New(publicURL, ownerName string) (*Verifier, error) {
36 u, err := url.Parse(publicURL)37 u, err := url.Parse(publicURL)
37 if err != nil {38 if err != nil {
38 return nil, fmt.Errorf("passkeys: parse public URL: %w", err)39 return nil, fmt.Errorf("passkeys: parse public URL: %w", err)
@@ -57,18 +58,19 @@ func New(publicURL string) (*Verifier, error) {
57 if err != nil {58 if err != nil {
58 return nil, fmt.Errorf("passkeys: %w", err)59 return nil, fmt.Errorf("passkeys: %w", err)
59 }60 }
60- return &Verifier{web: web}, nil61+ return &Verifier{web: web, name: ownerName}, nil
61 }62 }
62 63
63 // owner is the single WebAuthn user.64 // owner is the single WebAuthn user.
64 type owner struct {65 type owner struct {
66+ name string
65 handle []byte67 handle []byte
66 credentials []gowebauthn.Credential68 credentials []gowebauthn.Credential
67 }69 }
68 70
69 func (o owner) WebAuthnID() []byte { return o.handle }71 func (o owner) WebAuthnID() []byte { return o.handle }
70-func (o owner) WebAuthnName() string { return ownerName }72+func (o owner) WebAuthnName() string { return o.name }
71-func (o owner) WebAuthnDisplayName() string { return ownerName }73+func (o owner) WebAuthnDisplayName() string { return o.name }
72 func (o owner) WebAuthnCredentials() []gowebauthn.Credential { return o.credentials }74 func (o owner) WebAuthnCredentials() []gowebauthn.Credential { return o.credentials }
73 75
74 // refuse builds the error of a failed verification: an ErrUnauthenticated whose76 // refuse builds the error of a failed verification: an ErrUnauthenticated whose
@@ -84,7 +86,7 @@ func (v *Verifier) BeginRegistration(userHandle []byte, existing []domain.Passke
84 for _, p := range existing {86 for _, p := range existing {
85 exclude = append(exclude, descriptor(p))87 exclude = append(exclude, descriptor(p))
86 }88 }
87- creation, session, err := v.web.BeginRegistration(owner{handle: userHandle},89+ creation, session, err := v.web.BeginRegistration(owner{name: v.name, handle: userHandle},
88 gowebauthn.WithResidentKeyRequirement(protocol.ResidentKeyRequirementRequired),90 gowebauthn.WithResidentKeyRequirement(protocol.ResidentKeyRequirementRequired),
89 gowebauthn.WithExclusions(exclude),91 gowebauthn.WithExclusions(exclude),
90 gowebauthn.WithConveyancePreference(protocol.PreferNoAttestation),92 gowebauthn.WithConveyancePreference(protocol.PreferNoAttestation),
@@ -105,7 +107,7 @@ func (v *Verifier) FinishRegistration(userHandle []byte, ceremony, response []by
105 if err != nil {107 if err != nil {
106 return domain.Passkey{}, refuse("registration", err)108 return domain.Passkey{}, refuse("registration", err)
107 }109 }
108- cred, err := v.web.CreateCredential(owner{handle: userHandle}, session, parsed)110+ cred, err := v.web.CreateCredential(owner{name: v.name, handle: userHandle}, session, parsed)
109 if err != nil {111 if err != nil {
110 return domain.Passkey{}, refuse("registration", err)112 return domain.Passkey{}, refuse("registration", err)
111 }113 }
@@ -158,7 +160,7 @@ func (v *Verifier) FinishLogin(userHandle []byte, ceremony, response []byte, loo
158 return nil, err160 return nil, err
159 }161 }
160 stored = p162 stored = p
161- return owner{handle: userHandle, credentials: []gowebauthn.Credential{credential(p)}}, nil163+ return owner{name: v.name, handle: userHandle, credentials: []gowebauthn.Credential{credential(p)}}, nil
162 }164 }
163 _, cred, err := v.web.ValidatePasskeyLogin(handler, session, parsed)165 _, cred, err := v.web.ValidatePasskeyLogin(handler, session, parsed)
164 if lookupErr != nil {166 if lookupErr != nil {
M internal/adapters/webauthn/webauthn_test.go +10 -10
@@ -90,7 +90,7 @@ func TestRegisterThenLogin(t *testing.T) {
9090 for name, s := range sites {
9191 t.Run(name, func(t *testing.T) {
9292 t.Parallel()
93- v, err := webauthn.New(s.url)
93+ v, err := webauthn.New(s.url, "owner")
9494 require.NoError(t, err)
9595 d := register(t, v, s, virtualwebauthn.AuthenticatorOptions{BackupEligible: true})
9696 d.cred.Counter = 5
@@ -119,7 +119,7 @@ func TestRegisterThenLogin(t *testing.T) {
119119 func TestRegistrationReportsBackupFlags(t *testing.T) {
120120 t.Parallel()
121121 s := sites["https"]
122- v, err := webauthn.New(s.url)
122+ v, err := webauthn.New(s.url, "owner")
123123 require.NoError(t, err)
124124 d := register(t, v, s, virtualwebauthn.AuthenticatorOptions{BackupEligible: true, BackupState: true,
125125 Transports: []virtualwebauthn.Transport{virtualwebauthn.TransportInternal, virtualwebauthn.TransportHybrid}})
@@ -139,7 +139,7 @@ func TestRegistrationReportsBackupFlags(t *testing.T) {
139139 func TestOptions(t *testing.T) {
140140 t.Parallel()
141141 s := sites["https"]
142- v, err := webauthn.New(s.url)
142+ v, err := webauthn.New(s.url, "owner")
143143 require.NoError(t, err)
144144
145145 existing := domain.Passkey{ID: []byte("already-registered"), PublicKey: []byte("k"), Transports: []string{"usb"}}
@@ -197,7 +197,7 @@ func TestOptions(t *testing.T) {
197197 func TestLoginRefused(t *testing.T) {
198198 t.Parallel()
199199 s := sites["localhost"]
200- v, err := webauthn.New(s.url)
200+ v, err := webauthn.New(s.url, "owner")
201201 require.NoError(t, err)
202202
203203 tests := []struct {
@@ -315,7 +315,7 @@ func setExpires(t *testing.T, ceremony []byte, at time.Time) []byte {
315315 func TestLoginStoreFailureIsNotAnUnauthenticated(t *testing.T) {
316316 t.Parallel()
317317 s := sites["localhost"]
318- v, err := webauthn.New(s.url)
318+ v, err := webauthn.New(s.url, "owner")
319319 require.NoError(t, err)
320320 d := register(t, v, s, virtualwebauthn.AuthenticatorOptions{})
321321 ceremony, options := d.assertion(t, v, s)
@@ -334,7 +334,7 @@ func TestLoginStoreFailureIsNotAnUnauthenticated(t *testing.T) {
334334 func TestCounterThatDoesNotAdvanceKeepsTheStoredOne(t *testing.T) {
335335 t.Parallel()
336336 s := sites["localhost"]
337- v, err := webauthn.New(s.url)
337+ v, err := webauthn.New(s.url, "owner")
338338 require.NoError(t, err)
339339 d := register(t, v, s, virtualwebauthn.AuthenticatorOptions{})
340340
@@ -364,7 +364,7 @@ func TestCounterThatDoesNotAdvanceKeepsTheStoredOne(t *testing.T) {
364364 func TestZeroCounterIsNotAClone(t *testing.T) {
365365 t.Parallel()
366366 s := sites["localhost"]
367- v, err := webauthn.New(s.url)
367+ v, err := webauthn.New(s.url, "owner")
368368 require.NoError(t, err)
369369 d := register(t, v, s, virtualwebauthn.AuthenticatorOptions{BackupEligible: true, BackupState: true})
370370 for range 2 {
@@ -378,7 +378,7 @@ func TestZeroCounterIsNotAClone(t *testing.T) {
378378 func TestRegistrationRefused(t *testing.T) {
379379 t.Parallel()
380380 s := sites["https"]
381- v, err := webauthn.New(s.url)
381+ v, err := webauthn.New(s.url, "owner")
382382 require.NoError(t, err)
383383
384384 tests := []struct {
@@ -436,11 +436,11 @@ func TestRegistrationRefused(t *testing.T) {
436436 func TestNew(t *testing.T) {
437437 t.Parallel()
438438 for _, ok := range []string{"https://git.example.com", "http://localhost:7341", "https://Git.Example.com:8443/"} {
439- _, err := webauthn.New(ok)
439+ _, err := webauthn.New(ok, "owner")
440440 assert.NoError(t, err, ok)
441441 }
442442 for _, bad := range []string{"http://127.0.0.1:7341", "http://[::1]:7341", "git.example.com", "ftp://git.example.com", "", "https://"} {
443- _, err := webauthn.New(bad)
443+ _, err := webauthn.New(bad, "owner")
444444 assert.Error(t, err, bad)
445445 }
446446 }
@@ -90,7 +90,7 @@ func TestRegisterThenLogin(t *testing.T) {
90 for name, s := range sites {90 for name, s := range sites {
91 t.Run(name, func(t *testing.T) {91 t.Run(name, func(t *testing.T) {
92 t.Parallel()92 t.Parallel()
93- v, err := webauthn.New(s.url)93+ v, err := webauthn.New(s.url, "owner")
94 require.NoError(t, err)94 require.NoError(t, err)
95 d := register(t, v, s, virtualwebauthn.AuthenticatorOptions{BackupEligible: true})95 d := register(t, v, s, virtualwebauthn.AuthenticatorOptions{BackupEligible: true})
96 d.cred.Counter = 596 d.cred.Counter = 5
@@ -119,7 +119,7 @@ func TestRegisterThenLogin(t *testing.T) {
119 func TestRegistrationReportsBackupFlags(t *testing.T) {119 func TestRegistrationReportsBackupFlags(t *testing.T) {
120 t.Parallel()120 t.Parallel()
121 s := sites["https"]121 s := sites["https"]
122- v, err := webauthn.New(s.url)122+ v, err := webauthn.New(s.url, "owner")
123 require.NoError(t, err)123 require.NoError(t, err)
124 d := register(t, v, s, virtualwebauthn.AuthenticatorOptions{BackupEligible: true, BackupState: true,124 d := register(t, v, s, virtualwebauthn.AuthenticatorOptions{BackupEligible: true, BackupState: true,
125 Transports: []virtualwebauthn.Transport{virtualwebauthn.TransportInternal, virtualwebauthn.TransportHybrid}})125 Transports: []virtualwebauthn.Transport{virtualwebauthn.TransportInternal, virtualwebauthn.TransportHybrid}})
@@ -139,7 +139,7 @@ func TestRegistrationReportsBackupFlags(t *testing.T) {
139 func TestOptions(t *testing.T) {139 func TestOptions(t *testing.T) {
140 t.Parallel()140 t.Parallel()
141 s := sites["https"]141 s := sites["https"]
142- v, err := webauthn.New(s.url)142+ v, err := webauthn.New(s.url, "owner")
143 require.NoError(t, err)143 require.NoError(t, err)
144 144
145 existing := domain.Passkey{ID: []byte("already-registered"), PublicKey: []byte("k"), Transports: []string{"usb"}}145 existing := domain.Passkey{ID: []byte("already-registered"), PublicKey: []byte("k"), Transports: []string{"usb"}}
@@ -197,7 +197,7 @@ func TestOptions(t *testing.T) {
197 func TestLoginRefused(t *testing.T) {197 func TestLoginRefused(t *testing.T) {
198 t.Parallel()198 t.Parallel()
199 s := sites["localhost"]199 s := sites["localhost"]
200- v, err := webauthn.New(s.url)200+ v, err := webauthn.New(s.url, "owner")
201 require.NoError(t, err)201 require.NoError(t, err)
202 202
203 tests := []struct {203 tests := []struct {
@@ -315,7 +315,7 @@ func setExpires(t *testing.T, ceremony []byte, at time.Time) []byte {
315 func TestLoginStoreFailureIsNotAnUnauthenticated(t *testing.T) {315 func TestLoginStoreFailureIsNotAnUnauthenticated(t *testing.T) {
316 t.Parallel()316 t.Parallel()
317 s := sites["localhost"]317 s := sites["localhost"]
318- v, err := webauthn.New(s.url)318+ v, err := webauthn.New(s.url, "owner")
319 require.NoError(t, err)319 require.NoError(t, err)
320 d := register(t, v, s, virtualwebauthn.AuthenticatorOptions{})320 d := register(t, v, s, virtualwebauthn.AuthenticatorOptions{})
321 ceremony, options := d.assertion(t, v, s)321 ceremony, options := d.assertion(t, v, s)
@@ -334,7 +334,7 @@ func TestLoginStoreFailureIsNotAnUnauthenticated(t *testing.T) {
334 func TestCounterThatDoesNotAdvanceKeepsTheStoredOne(t *testing.T) {334 func TestCounterThatDoesNotAdvanceKeepsTheStoredOne(t *testing.T) {
335 t.Parallel()335 t.Parallel()
336 s := sites["localhost"]336 s := sites["localhost"]
337- v, err := webauthn.New(s.url)337+ v, err := webauthn.New(s.url, "owner")
338 require.NoError(t, err)338 require.NoError(t, err)
339 d := register(t, v, s, virtualwebauthn.AuthenticatorOptions{})339 d := register(t, v, s, virtualwebauthn.AuthenticatorOptions{})
340 340
@@ -364,7 +364,7 @@ func TestCounterThatDoesNotAdvanceKeepsTheStoredOne(t *testing.T) {
364 func TestZeroCounterIsNotAClone(t *testing.T) {364 func TestZeroCounterIsNotAClone(t *testing.T) {
365 t.Parallel()365 t.Parallel()
366 s := sites["localhost"]366 s := sites["localhost"]
367- v, err := webauthn.New(s.url)367+ v, err := webauthn.New(s.url, "owner")
368 require.NoError(t, err)368 require.NoError(t, err)
369 d := register(t, v, s, virtualwebauthn.AuthenticatorOptions{BackupEligible: true, BackupState: true})369 d := register(t, v, s, virtualwebauthn.AuthenticatorOptions{BackupEligible: true, BackupState: true})
370 for range 2 {370 for range 2 {
@@ -378,7 +378,7 @@ func TestZeroCounterIsNotAClone(t *testing.T) {
378 func TestRegistrationRefused(t *testing.T) {378 func TestRegistrationRefused(t *testing.T) {
379 t.Parallel()379 t.Parallel()
380 s := sites["https"]380 s := sites["https"]
381- v, err := webauthn.New(s.url)381+ v, err := webauthn.New(s.url, "owner")
382 require.NoError(t, err)382 require.NoError(t, err)
383 383
384 tests := []struct {384 tests := []struct {
@@ -436,11 +436,11 @@ func TestRegistrationRefused(t *testing.T) {
436 func TestNew(t *testing.T) {436 func TestNew(t *testing.T) {
437 t.Parallel()437 t.Parallel()
438 for _, ok := range []string{"https://git.example.com", "http://localhost:7341", "https://Git.Example.com:8443/"} {438 for _, ok := range []string{"https://git.example.com", "http://localhost:7341", "https://Git.Example.com:8443/"} {
439- _, err := webauthn.New(ok)439+ _, err := webauthn.New(ok, "owner")
440 assert.NoError(t, err, ok)440 assert.NoError(t, err, ok)
441 }441 }
442 for _, bad := range []string{"http://127.0.0.1:7341", "http://[::1]:7341", "git.example.com", "ftp://git.example.com", "", "https://"} {442 for _, bad := range []string{"http://127.0.0.1:7341", "http://[::1]:7341", "git.example.com", "ftp://git.example.com", "", "https://"} {
443- _, err := webauthn.New(bad)443+ _, err := webauthn.New(bad, "owner")
444 assert.Error(t, err, bad)444 assert.Error(t, err, bad)
445 }445 }
446 }446 }
M internal/app/cli_web.go +4 -4
@@ -35,11 +35,11 @@ func newWebLoginCmd(env *cliEnv) *cobra.Command {
3535 cmd := &cobra.Command{
3636 Use: "login",
3737 Short: "Print a one-time link that signs your browser in",
38- Long: "Ask the server for a login link and print it on standard output. Open it in your browser and confirm:\n" +
38+ Long: "Ask the server for a login link and print it on standard output. Open it in your browser:\n" +
3939 "the browser is then signed in as the owner for 12 hours (private repositories, merge requests).\n\n" +
4040 "The link works once, for about a minute, and only until the server restarts. It is a secret: it is\n" +
41- "printed and nowhere else (not stored, not logged); do not paste it into a chat or a ticket. Opening\n" +
42- "it only shows a confirmation page; confirming it signs the browser in.\n\n" +
41+ "printed and nowhere else (not stored, not logged); do not paste it into a chat or a ticket. The page\n" +
42+ "signs the browser in by itself; link previews, which run no script, cannot use it.\n\n" +
4343 "JSON fields (--json): url, expires_at (RFC 3339 UTC).",
4444 Args: cobra.NoArgs,
4545 }
@@ -56,7 +56,7 @@ func newWebLoginCmd(env *cliEnv) *cobra.Command {
5656 if *asJSON {
5757 return writeJSON(cmd.OutOrStdout(), jsonLoginLink{URL: res.Msg.GetUrl(), ExpiresAt: jsonTime(res.Msg.GetExpiresAt())})
5858 }
59- noticef(cmd, "open this link in your browser and confirm; it works once, until %s UTC", shortTime(res.Msg.GetExpiresAt(), "shortly"))
59+ noticef(cmd, "open this link in your browser; it works once, until %s UTC", shortTime(res.Msg.GetExpiresAt(), "shortly"))
6060 _, err = fmt.Fprintln(cmd.OutOrStdout(), res.Msg.GetUrl())
6161 return err
6262 }
@@ -35,11 +35,11 @@ func newWebLoginCmd(env *cliEnv) *cobra.Command {
35 cmd := &cobra.Command{35 cmd := &cobra.Command{
36 Use: "login",36 Use: "login",
37 Short: "Print a one-time link that signs your browser in",37 Short: "Print a one-time link that signs your browser in",
38- Long: "Ask the server for a login link and print it on standard output. Open it in your browser and confirm:\n" +38+ Long: "Ask the server for a login link and print it on standard output. Open it in your browser:\n" +
39 "the browser is then signed in as the owner for 12 hours (private repositories, merge requests).\n\n" +39 "the browser is then signed in as the owner for 12 hours (private repositories, merge requests).\n\n" +
40 "The link works once, for about a minute, and only until the server restarts. It is a secret: it is\n" +40 "The link works once, for about a minute, and only until the server restarts. It is a secret: it is\n" +
41- "printed and nowhere else (not stored, not logged); do not paste it into a chat or a ticket. Opening\n" +41+ "printed and nowhere else (not stored, not logged); do not paste it into a chat or a ticket. The page\n" +
42- "it only shows a confirmation page; confirming it signs the browser in.\n\n" +42+ "signs the browser in by itself; link previews, which run no script, cannot use it.\n\n" +
43 "JSON fields (--json): url, expires_at (RFC 3339 UTC).",43 "JSON fields (--json): url, expires_at (RFC 3339 UTC).",
44 Args: cobra.NoArgs,44 Args: cobra.NoArgs,
45 }45 }
@@ -56,7 +56,7 @@ func newWebLoginCmd(env *cliEnv) *cobra.Command {
56 if *asJSON {56 if *asJSON {
57 return writeJSON(cmd.OutOrStdout(), jsonLoginLink{URL: res.Msg.GetUrl(), ExpiresAt: jsonTime(res.Msg.GetExpiresAt())})57 return writeJSON(cmd.OutOrStdout(), jsonLoginLink{URL: res.Msg.GetUrl(), ExpiresAt: jsonTime(res.Msg.GetExpiresAt())})
58 }58 }
59- noticef(cmd, "open this link in your browser and confirm; it works once, until %s UTC", shortTime(res.Msg.GetExpiresAt(), "shortly"))59+ noticef(cmd, "open this link in your browser; it works once, until %s UTC", shortTime(res.Msg.GetExpiresAt(), "shortly"))
60 _, err = fmt.Fprintln(cmd.OutOrStdout(), res.Msg.GetUrl())60 _, err = fmt.Fprintln(cmd.OutOrStdout(), res.Msg.GetUrl())
61 return err61 return err
62 }62 }
M internal/app/forge.go +1 -1
@@ -317,7 +317,7 @@ func newForge(ctx context.Context, cfg config.Config, opts options) (_ *forge, e
317317 // Owner passkeys (ADR 0026): built only when the public URL allows them.
318318 // When it does, a verifier that cannot be built is fatal.
319319 if ok, reason := passkeysAvailable(cfg.PublicURL); ok {
320- verifier, err := webauthn.New(cfg.PublicURL)
320+ verifier, err := webauthn.New(cfg.PublicURL, cfg.Owner)
321321 if err != nil {
322322 return nil, fmt.Errorf("passkeys: %w", err)
323323 }
@@ -317,7 +317,7 @@ func newForge(ctx context.Context, cfg config.Config, opts options) (_ *forge, e
317 // Owner passkeys (ADR 0026): built only when the public URL allows them.317 // Owner passkeys (ADR 0026): built only when the public URL allows them.
318 // When it does, a verifier that cannot be built is fatal.318 // When it does, a verifier that cannot be built is fatal.
319 if ok, reason := passkeysAvailable(cfg.PublicURL); ok {319 if ok, reason := passkeysAvailable(cfg.PublicURL); ok {
320- verifier, err := webauthn.New(cfg.PublicURL)320+ verifier, err := webauthn.New(cfg.PublicURL, cfg.Owner)
321 if err != nil {321 if err != nil {
322 return nil, fmt.Errorf("passkeys: %w", err)322 return nil, fmt.Errorf("passkeys: %w", err)
323 }323 }
M internal/app/testdata/web/blob-binary.html +8 -5
@@ -5,7 +5,12 @@
55 <meta name="viewport" content="width=device-width, initial-scale=1">
66 <meta name="color-scheme" content="light dark">
77 <title>data.bin · files · example.test</title>
8+<meta name="theme-color" media="(prefers-color-scheme: light)" content="#ffffff">
9+<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#0c0c0c">
10+<meta name="apple-mobile-web-app-title" content="ermite">
811 <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg">
12+<link rel="apple-touch-icon" href="/-/static/app/apple-touch-icon.HASH.png">
13+<link rel="manifest" href="/-/static/app/manifest.HASH.webmanifest">
914 <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin>
1015 <link rel="stylesheet" href="/-/static/css/ermite.HASH.css">
1116 <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'>
@@ -28,10 +33,9 @@
2833 <a class="skip" href="#main">skip to content</a>
2934 <header class="hdr">
3035 <div class="hdr-top">
31-<a class="brand" href="/" data-g="h" title="home (g h)">
36+<a class="brand" href="/" data-g="h" aria-label="home" title="home (g h)">
3237 <span class="brand-mark" aria-hidden="true">
3338 </span>
34-<span class="brand-name">example.test</span>
3539 </a>
3640 <nav class="crumbs" aria-label="Path">
3741 <ol>
@@ -62,7 +66,6 @@
6266 <span class="search-label">Go to…</span>
6367 <kbd aria-hidden="true">ctrl k</kbd>
6468 </button>
65-<button type="button" class="btn icon" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" aria-label="Keyboard shortcuts" title="keyboard shortcuts (?)">?</button>
6669 </div>
6770 </div>
6871 <nav class="tabs" aria-label="Sections">
@@ -129,8 +132,8 @@
129132 <footer class="foot">
130133 <span>ermite devel</span>
131134 <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a>
132-<span class="foot-keys">
133-<kbd>?</kbd> keyboard shortcuts</span>
135+<button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?">
136+<kbd aria-hidden="true">?</kbd> keyboard shortcuts</button>
134137 </footer>
135138 <div class="hint" role="status" aria-live="polite">
136139 <span class="hint-nav" :text="document.body.dataset.nav || ''">
@@ -5,7 +5,12 @@
5 <meta name="viewport" content="width=device-width, initial-scale=1">5 <meta name="viewport" content="width=device-width, initial-scale=1">
6 <meta name="color-scheme" content="light dark">6 <meta name="color-scheme" content="light dark">
7 <title>data.bin · files · example.test</title>7 <title>data.bin · files · example.test</title>
8+<meta name="theme-color" media="(prefers-color-scheme: light)" content="#ffffff">
9+<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#0c0c0c">
10+<meta name="apple-mobile-web-app-title" content="ermite">
8 <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg">11 <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg">
12+<link rel="apple-touch-icon" href="/-/static/app/apple-touch-icon.HASH.png">
13+<link rel="manifest" href="/-/static/app/manifest.HASH.webmanifest">
9 <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin>14 <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin>
10 <link rel="stylesheet" href="/-/static/css/ermite.HASH.css">15 <link rel="stylesheet" href="/-/static/css/ermite.HASH.css">
11 <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'>16 <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'>
@@ -28,10 +33,9 @@
28 <a class="skip" href="#main">skip to content</a>33 <a class="skip" href="#main">skip to content</a>
29 <header class="hdr">34 <header class="hdr">
30 <div class="hdr-top">35 <div class="hdr-top">
31-<a class="brand" href="/" data-g="h" title="home (g h)">36+<a class="brand" href="/" data-g="h" aria-label="home" title="home (g h)">
32 <span class="brand-mark" aria-hidden="true">37 <span class="brand-mark" aria-hidden="true">
33 </span>38 </span>
34-<span class="brand-name">example.test</span>
35 </a>39 </a>
36 <nav class="crumbs" aria-label="Path">40 <nav class="crumbs" aria-label="Path">
37 <ol>41 <ol>
@@ -62,7 +66,6 @@
62 <span class="search-label">Go to…</span>66 <span class="search-label">Go to…</span>
63 <kbd aria-hidden="true">ctrl k</kbd>67 <kbd aria-hidden="true">ctrl k</kbd>
64 </button>68 </button>
65-<button type="button" class="btn icon" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" aria-label="Keyboard shortcuts" title="keyboard shortcuts (?)">?</button>
66 </div>69 </div>
67 </div>70 </div>
68 <nav class="tabs" aria-label="Sections">71 <nav class="tabs" aria-label="Sections">
@@ -129,8 +132,8 @@
129 <footer class="foot">132 <footer class="foot">
130 <span>ermite devel</span>133 <span>ermite devel</span>
131 <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a>134 <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a>
132-<span class="foot-keys">135+<button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?">
133-<kbd>?</kbd> keyboard shortcuts</span>136+<kbd aria-hidden="true">?</kbd> keyboard shortcuts</button>
134 </footer>137 </footer>
135 <div class="hint" role="status" aria-live="polite">138 <div class="hint" role="status" aria-live="polite">
136 <span class="hint-nav" :text="document.body.dataset.nav || ''">139 <span class="hint-nav" :text="document.body.dataset.nav || ''">
M internal/app/testdata/web/blob-code.html +8 -5
@@ -5,7 +5,12 @@
55 <meta name="viewport" content="width=device-width, initial-scale=1">
66 <meta name="color-scheme" content="light dark">
77 <title>main.go · files · example.test</title>
8+<meta name="theme-color" media="(prefers-color-scheme: light)" content="#ffffff">
9+<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#0c0c0c">
10+<meta name="apple-mobile-web-app-title" content="ermite">
811 <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg">
12+<link rel="apple-touch-icon" href="/-/static/app/apple-touch-icon.HASH.png">
13+<link rel="manifest" href="/-/static/app/manifest.HASH.webmanifest">
914 <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin>
1015 <link rel="stylesheet" href="/-/static/css/ermite.HASH.css">
1116 <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'>
@@ -28,10 +33,9 @@
2833 <a class="skip" href="#main">skip to content</a>
2934 <header class="hdr">
3035 <div class="hdr-top">
31-<a class="brand" href="/" data-g="h" title="home (g h)">
36+<a class="brand" href="/" data-g="h" aria-label="home" title="home (g h)">
3237 <span class="brand-mark" aria-hidden="true">
3338 </span>
34-<span class="brand-name">example.test</span>
3539 </a>
3640 <nav class="crumbs" aria-label="Path">
3741 <ol>
@@ -62,7 +66,6 @@
6266 <span class="search-label">Go to…</span>
6367 <kbd aria-hidden="true">ctrl k</kbd>
6468 </button>
65-<button type="button" class="btn icon" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" aria-label="Keyboard shortcuts" title="keyboard shortcuts (?)">?</button>
6669 </div>
6770 </div>
6871 <nav class="tabs" aria-label="Sections">
@@ -199,8 +202,8 @@
199202 <footer class="foot">
200203 <span>ermite devel</span>
201204 <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a>
202-<span class="foot-keys">
203-<kbd>?</kbd> keyboard shortcuts</span>
205+<button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?">
206+<kbd aria-hidden="true">?</kbd> keyboard shortcuts</button>
204207 </footer>
205208 <div class="hint" role="status" aria-live="polite">
206209 <span class="hint-nav" :text="document.body.dataset.nav || ''">
@@ -5,7 +5,12 @@
5 <meta name="viewport" content="width=device-width, initial-scale=1">5 <meta name="viewport" content="width=device-width, initial-scale=1">
6 <meta name="color-scheme" content="light dark">6 <meta name="color-scheme" content="light dark">
7 <title>main.go · files · example.test</title>7 <title>main.go · files · example.test</title>
8+<meta name="theme-color" media="(prefers-color-scheme: light)" content="#ffffff">
9+<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#0c0c0c">
10+<meta name="apple-mobile-web-app-title" content="ermite">
8 <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg">11 <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg">
12+<link rel="apple-touch-icon" href="/-/static/app/apple-touch-icon.HASH.png">
13+<link rel="manifest" href="/-/static/app/manifest.HASH.webmanifest">
9 <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin>14 <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin>
10 <link rel="stylesheet" href="/-/static/css/ermite.HASH.css">15 <link rel="stylesheet" href="/-/static/css/ermite.HASH.css">
11 <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'>16 <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'>
@@ -28,10 +33,9 @@
28 <a class="skip" href="#main">skip to content</a>33 <a class="skip" href="#main">skip to content</a>
29 <header class="hdr">34 <header class="hdr">
30 <div class="hdr-top">35 <div class="hdr-top">
31-<a class="brand" href="/" data-g="h" title="home (g h)">36+<a class="brand" href="/" data-g="h" aria-label="home" title="home (g h)">
32 <span class="brand-mark" aria-hidden="true">37 <span class="brand-mark" aria-hidden="true">
33 </span>38 </span>
34-<span class="brand-name">example.test</span>
35 </a>39 </a>
36 <nav class="crumbs" aria-label="Path">40 <nav class="crumbs" aria-label="Path">
37 <ol>41 <ol>
@@ -62,7 +66,6 @@
62 <span class="search-label">Go to…</span>66 <span class="search-label">Go to…</span>
63 <kbd aria-hidden="true">ctrl k</kbd>67 <kbd aria-hidden="true">ctrl k</kbd>
64 </button>68 </button>
65-<button type="button" class="btn icon" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" aria-label="Keyboard shortcuts" title="keyboard shortcuts (?)">?</button>
66 </div>69 </div>
67 </div>70 </div>
68 <nav class="tabs" aria-label="Sections">71 <nav class="tabs" aria-label="Sections">
@@ -199,8 +202,8 @@
199 <footer class="foot">202 <footer class="foot">
200 <span>ermite devel</span>203 <span>ermite devel</span>
201 <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a>204 <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a>
202-<span class="foot-keys">205+<button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?">
203-<kbd>?</kbd> keyboard shortcuts</span>206+<kbd aria-hidden="true">?</kbd> keyboard shortcuts</button>
204 </footer>207 </footer>
205 <div class="hint" role="status" aria-live="polite">208 <div class="hint" role="status" aria-live="polite">
206 <span class="hint-nav" :text="document.body.dataset.nav || ''">209 <span class="hint-nav" :text="document.body.dataset.nav || ''">
M internal/app/testdata/web/blob-empty.html +8 -5
@@ -5,7 +5,12 @@
55 <meta name="viewport" content="width=device-width, initial-scale=1">
66 <meta name="color-scheme" content="light dark">
77 <title>empty.txt · files · example.test</title>
8+<meta name="theme-color" media="(prefers-color-scheme: light)" content="#ffffff">
9+<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#0c0c0c">
10+<meta name="apple-mobile-web-app-title" content="ermite">
811 <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg">
12+<link rel="apple-touch-icon" href="/-/static/app/apple-touch-icon.HASH.png">
13+<link rel="manifest" href="/-/static/app/manifest.HASH.webmanifest">
914 <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin>
1015 <link rel="stylesheet" href="/-/static/css/ermite.HASH.css">
1116 <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'>
@@ -28,10 +33,9 @@
2833 <a class="skip" href="#main">skip to content</a>
2934 <header class="hdr">
3035 <div class="hdr-top">
31-<a class="brand" href="/" data-g="h" title="home (g h)">
36+<a class="brand" href="/" data-g="h" aria-label="home" title="home (g h)">
3237 <span class="brand-mark" aria-hidden="true">
3338 </span>
34-<span class="brand-name">example.test</span>
3539 </a>
3640 <nav class="crumbs" aria-label="Path">
3741 <ol>
@@ -62,7 +66,6 @@
6266 <span class="search-label">Go to…</span>
6367 <kbd aria-hidden="true">ctrl k</kbd>
6468 </button>
65-<button type="button" class="btn icon" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" aria-label="Keyboard shortcuts" title="keyboard shortcuts (?)">?</button>
6669 </div>
6770 </div>
6871 <nav class="tabs" aria-label="Sections">
@@ -129,8 +132,8 @@
129132 <footer class="foot">
130133 <span>ermite devel</span>
131134 <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a>
132-<span class="foot-keys">
133-<kbd>?</kbd> keyboard shortcuts</span>
135+<button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?">
136+<kbd aria-hidden="true">?</kbd> keyboard shortcuts</button>
134137 </footer>
135138 <div class="hint" role="status" aria-live="polite">
136139 <span class="hint-nav" :text="document.body.dataset.nav || ''">
@@ -5,7 +5,12 @@
5 <meta name="viewport" content="width=device-width, initial-scale=1">5 <meta name="viewport" content="width=device-width, initial-scale=1">
6 <meta name="color-scheme" content="light dark">6 <meta name="color-scheme" content="light dark">
7 <title>empty.txt · files · example.test</title>7 <title>empty.txt · files · example.test</title>
8+<meta name="theme-color" media="(prefers-color-scheme: light)" content="#ffffff">
9+<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#0c0c0c">
10+<meta name="apple-mobile-web-app-title" content="ermite">
8 <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg">11 <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg">
12+<link rel="apple-touch-icon" href="/-/static/app/apple-touch-icon.HASH.png">
13+<link rel="manifest" href="/-/static/app/manifest.HASH.webmanifest">
9 <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin>14 <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin>
10 <link rel="stylesheet" href="/-/static/css/ermite.HASH.css">15 <link rel="stylesheet" href="/-/static/css/ermite.HASH.css">
11 <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'>16 <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'>
@@ -28,10 +33,9 @@
28 <a class="skip" href="#main">skip to content</a>33 <a class="skip" href="#main">skip to content</a>
29 <header class="hdr">34 <header class="hdr">
30 <div class="hdr-top">35 <div class="hdr-top">
31-<a class="brand" href="/" data-g="h" title="home (g h)">36+<a class="brand" href="/" data-g="h" aria-label="home" title="home (g h)">
32 <span class="brand-mark" aria-hidden="true">37 <span class="brand-mark" aria-hidden="true">
33 </span>38 </span>
34-<span class="brand-name">example.test</span>
35 </a>39 </a>
36 <nav class="crumbs" aria-label="Path">40 <nav class="crumbs" aria-label="Path">
37 <ol>41 <ol>
@@ -62,7 +66,6 @@
62 <span class="search-label">Go to…</span>66 <span class="search-label">Go to…</span>
63 <kbd aria-hidden="true">ctrl k</kbd>67 <kbd aria-hidden="true">ctrl k</kbd>
64 </button>68 </button>
65-<button type="button" class="btn icon" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" aria-label="Keyboard shortcuts" title="keyboard shortcuts (?)">?</button>
66 </div>69 </div>
67 </div>70 </div>
68 <nav class="tabs" aria-label="Sections">71 <nav class="tabs" aria-label="Sections">
@@ -129,8 +132,8 @@
129 <footer class="foot">132 <footer class="foot">
130 <span>ermite devel</span>133 <span>ermite devel</span>
131 <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a>134 <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a>
132-<span class="foot-keys">135+<button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?">
133-<kbd>?</kbd> keyboard shortcuts</span>136+<kbd aria-hidden="true">?</kbd> keyboard shortcuts</button>
134 </footer>137 </footer>
135 <div class="hint" role="status" aria-live="polite">138 <div class="hint" role="status" aria-live="polite">
136 <span class="hint-nav" :text="document.body.dataset.nav || ''">139 <span class="hint-nav" :text="document.body.dataset.nav || ''">
M internal/app/testdata/web/blob-image.html +8 -5
@@ -5,7 +5,12 @@
55 <meta name="viewport" content="width=device-width, initial-scale=1">
66 <meta name="color-scheme" content="light dark">
77 <title>pic.png · files · example.test</title>
8+<meta name="theme-color" media="(prefers-color-scheme: light)" content="#ffffff">
9+<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#0c0c0c">
10+<meta name="apple-mobile-web-app-title" content="ermite">
811 <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg">
12+<link rel="apple-touch-icon" href="/-/static/app/apple-touch-icon.HASH.png">
13+<link rel="manifest" href="/-/static/app/manifest.HASH.webmanifest">
914 <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin>
1015 <link rel="stylesheet" href="/-/static/css/ermite.HASH.css">
1116 <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'>
@@ -28,10 +33,9 @@
2833 <a class="skip" href="#main">skip to content</a>
2934 <header class="hdr">
3035 <div class="hdr-top">
31-<a class="brand" href="/" data-g="h" title="home (g h)">
36+<a class="brand" href="/" data-g="h" aria-label="home" title="home (g h)">
3237 <span class="brand-mark" aria-hidden="true">
3338 </span>
34-<span class="brand-name">example.test</span>
3539 </a>
3640 <nav class="crumbs" aria-label="Path">
3741 <ol>
@@ -62,7 +66,6 @@
6266 <span class="search-label">Go to…</span>
6367 <kbd aria-hidden="true">ctrl k</kbd>
6468 </button>
65-<button type="button" class="btn icon" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" aria-label="Keyboard shortcuts" title="keyboard shortcuts (?)">?</button>
6669 </div>
6770 </div>
6871 <nav class="tabs" aria-label="Sections">
@@ -131,8 +134,8 @@
131134 <footer class="foot">
132135 <span>ermite devel</span>
133136 <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a>
134-<span class="foot-keys">
135-<kbd>?</kbd> keyboard shortcuts</span>
137+<button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?">
138+<kbd aria-hidden="true">?</kbd> keyboard shortcuts</button>
136139 </footer>
137140 <div class="hint" role="status" aria-live="polite">
138141 <span class="hint-nav" :text="document.body.dataset.nav || ''">
@@ -5,7 +5,12 @@
5 <meta name="viewport" content="width=device-width, initial-scale=1">5 <meta name="viewport" content="width=device-width, initial-scale=1">
6 <meta name="color-scheme" content="light dark">6 <meta name="color-scheme" content="light dark">
7 <title>pic.png · files · example.test</title>7 <title>pic.png · files · example.test</title>
8+<meta name="theme-color" media="(prefers-color-scheme: light)" content="#ffffff">
9+<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#0c0c0c">
10+<meta name="apple-mobile-web-app-title" content="ermite">
8 <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg">11 <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg">
12+<link rel="apple-touch-icon" href="/-/static/app/apple-touch-icon.HASH.png">
13+<link rel="manifest" href="/-/static/app/manifest.HASH.webmanifest">
9 <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin>14 <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin>
10 <link rel="stylesheet" href="/-/static/css/ermite.HASH.css">15 <link rel="stylesheet" href="/-/static/css/ermite.HASH.css">
11 <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'>16 <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'>
@@ -28,10 +33,9 @@
28 <a class="skip" href="#main">skip to content</a>33 <a class="skip" href="#main">skip to content</a>
29 <header class="hdr">34 <header class="hdr">
30 <div class="hdr-top">35 <div class="hdr-top">
31-<a class="brand" href="/" data-g="h" title="home (g h)">36+<a class="brand" href="/" data-g="h" aria-label="home" title="home (g h)">
32 <span class="brand-mark" aria-hidden="true">37 <span class="brand-mark" aria-hidden="true">
33 </span>38 </span>
34-<span class="brand-name">example.test</span>
35 </a>39 </a>
36 <nav class="crumbs" aria-label="Path">40 <nav class="crumbs" aria-label="Path">
37 <ol>41 <ol>
@@ -62,7 +66,6 @@
62 <span class="search-label">Go to…</span>66 <span class="search-label">Go to…</span>
63 <kbd aria-hidden="true">ctrl k</kbd>67 <kbd aria-hidden="true">ctrl k</kbd>
64 </button>68 </button>
65-<button type="button" class="btn icon" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" aria-label="Keyboard shortcuts" title="keyboard shortcuts (?)">?</button>
66 </div>69 </div>
67 </div>70 </div>
68 <nav class="tabs" aria-label="Sections">71 <nav class="tabs" aria-label="Sections">
@@ -131,8 +134,8 @@
131 <footer class="foot">134 <footer class="foot">
132 <span>ermite devel</span>135 <span>ermite devel</span>
133 <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a>136 <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a>
134-<span class="foot-keys">137+<button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?">
135-<kbd>?</kbd> keyboard shortcuts</span>138+<kbd aria-hidden="true">?</kbd> keyboard shortcuts</button>
136 </footer>139 </footer>
137 <div class="hint" role="status" aria-live="polite">140 <div class="hint" role="status" aria-live="polite">
138 <span class="hint-nav" :text="document.body.dataset.nav || ''">141 <span class="hint-nav" :text="document.body.dataset.nav || ''">
M internal/app/testdata/web/blob-markdown.html +8 -5
@@ -5,7 +5,12 @@
55 <meta name="viewport" content="width=device-width, initial-scale=1">
66 <meta name="color-scheme" content="light dark">
77 <title>docs/guide.md · files · example.test</title>
8+<meta name="theme-color" media="(prefers-color-scheme: light)" content="#ffffff">
9+<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#0c0c0c">
10+<meta name="apple-mobile-web-app-title" content="ermite">
811 <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg">
12+<link rel="apple-touch-icon" href="/-/static/app/apple-touch-icon.HASH.png">
13+<link rel="manifest" href="/-/static/app/manifest.HASH.webmanifest">
914 <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin>
1015 <link rel="stylesheet" href="/-/static/css/ermite.HASH.css">
1116 <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'>
@@ -28,10 +33,9 @@
2833 <a class="skip" href="#main">skip to content</a>
2934 <header class="hdr">
3035 <div class="hdr-top">
31-<a class="brand" href="/" data-g="h" title="home (g h)">
36+<a class="brand" href="/" data-g="h" aria-label="home" title="home (g h)">
3237 <span class="brand-mark" aria-hidden="true">
3338 </span>
34-<span class="brand-name">example.test</span>
3539 </a>
3640 <nav class="crumbs" aria-label="Path">
3741 <ol>
@@ -65,7 +69,6 @@
6569 <span class="search-label">Go to…</span>
6670 <kbd aria-hidden="true">ctrl k</kbd>
6771 </button>
68-<button type="button" class="btn icon" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" aria-label="Keyboard shortcuts" title="keyboard shortcuts (?)">?</button>
6972 </div>
7073 </div>
7174 <nav class="tabs" aria-label="Sections">
@@ -196,8 +199,8 @@
196199 <footer class="foot">
197200 <span>ermite devel</span>
198201 <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a>
199-<span class="foot-keys">
200-<kbd>?</kbd> keyboard shortcuts</span>
202+<button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?">
203+<kbd aria-hidden="true">?</kbd> keyboard shortcuts</button>
201204 </footer>
202205 <div class="hint" role="status" aria-live="polite">
203206 <span class="hint-nav" :text="document.body.dataset.nav || ''">
@@ -5,7 +5,12 @@
5 <meta name="viewport" content="width=device-width, initial-scale=1">5 <meta name="viewport" content="width=device-width, initial-scale=1">
6 <meta name="color-scheme" content="light dark">6 <meta name="color-scheme" content="light dark">
7 <title>docs/guide.md · files · example.test</title>7 <title>docs/guide.md · files · example.test</title>
8+<meta name="theme-color" media="(prefers-color-scheme: light)" content="#ffffff">
9+<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#0c0c0c">
10+<meta name="apple-mobile-web-app-title" content="ermite">
8 <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg">11 <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg">
12+<link rel="apple-touch-icon" href="/-/static/app/apple-touch-icon.HASH.png">
13+<link rel="manifest" href="/-/static/app/manifest.HASH.webmanifest">
9 <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin>14 <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin>
10 <link rel="stylesheet" href="/-/static/css/ermite.HASH.css">15 <link rel="stylesheet" href="/-/static/css/ermite.HASH.css">
11 <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'>16 <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'>
@@ -28,10 +33,9 @@
28 <a class="skip" href="#main">skip to content</a>33 <a class="skip" href="#main">skip to content</a>
29 <header class="hdr">34 <header class="hdr">
30 <div class="hdr-top">35 <div class="hdr-top">
31-<a class="brand" href="/" data-g="h" title="home (g h)">36+<a class="brand" href="/" data-g="h" aria-label="home" title="home (g h)">
32 <span class="brand-mark" aria-hidden="true">37 <span class="brand-mark" aria-hidden="true">
33 </span>38 </span>
34-<span class="brand-name">example.test</span>
35 </a>39 </a>
36 <nav class="crumbs" aria-label="Path">40 <nav class="crumbs" aria-label="Path">
37 <ol>41 <ol>
@@ -65,7 +69,6 @@
65 <span class="search-label">Go to…</span>69 <span class="search-label">Go to…</span>
66 <kbd aria-hidden="true">ctrl k</kbd>70 <kbd aria-hidden="true">ctrl k</kbd>
67 </button>71 </button>
68-<button type="button" class="btn icon" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" aria-label="Keyboard shortcuts" title="keyboard shortcuts (?)">?</button>
69 </div>72 </div>
70 </div>73 </div>
71 <nav class="tabs" aria-label="Sections">74 <nav class="tabs" aria-label="Sections">
@@ -196,8 +199,8 @@
196 <footer class="foot">199 <footer class="foot">
197 <span>ermite devel</span>200 <span>ermite devel</span>
198 <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a>201 <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a>
199-<span class="foot-keys">202+<button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?">
200-<kbd>?</kbd> keyboard shortcuts</span>203+<kbd aria-hidden="true">?</kbd> keyboard shortcuts</button>
201 </footer>204 </footer>
202 <div class="hint" role="status" aria-live="polite">205 <div class="hint" role="status" aria-live="polite">
203 <span class="hint-nav" :text="document.body.dataset.nav || ''">206 <span class="hint-nav" :text="document.body.dataset.nav || ''">
M internal/app/testdata/web/blob-svg.html +8 -5
@@ -5,7 +5,12 @@
55 <meta name="viewport" content="width=device-width, initial-scale=1">
66 <meta name="color-scheme" content="light dark">
77 <title>logo.svg · files · example.test</title>
8+<meta name="theme-color" media="(prefers-color-scheme: light)" content="#ffffff">
9+<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#0c0c0c">
10+<meta name="apple-mobile-web-app-title" content="ermite">
811 <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg">
12+<link rel="apple-touch-icon" href="/-/static/app/apple-touch-icon.HASH.png">
13+<link rel="manifest" href="/-/static/app/manifest.HASH.webmanifest">
914 <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin>
1015 <link rel="stylesheet" href="/-/static/css/ermite.HASH.css">
1116 <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'>
@@ -28,10 +33,9 @@
2833 <a class="skip" href="#main">skip to content</a>
2934 <header class="hdr">
3035 <div class="hdr-top">
31-<a class="brand" href="/" data-g="h" title="home (g h)">
36+<a class="brand" href="/" data-g="h" aria-label="home" title="home (g h)">
3237 <span class="brand-mark" aria-hidden="true">
3338 </span>
34-<span class="brand-name">example.test</span>
3539 </a>
3640 <nav class="crumbs" aria-label="Path">
3741 <ol>
@@ -62,7 +66,6 @@
6266 <span class="search-label">Go to…</span>
6367 <kbd aria-hidden="true">ctrl k</kbd>
6468 </button>
65-<button type="button" class="btn icon" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" aria-label="Keyboard shortcuts" title="keyboard shortcuts (?)">?</button>
6669 </div>
6770 </div>
6871 <nav class="tabs" aria-label="Sections">
@@ -154,8 +157,8 @@
154157 <footer class="foot">
155158 <span>ermite devel</span>
156159 <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a>
157-<span class="foot-keys">
158-<kbd>?</kbd> keyboard shortcuts</span>
160+<button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?">
161+<kbd aria-hidden="true">?</kbd> keyboard shortcuts</button>
159162 </footer>
160163 <div class="hint" role="status" aria-live="polite">
161164 <span class="hint-nav" :text="document.body.dataset.nav || ''">
@@ -5,7 +5,12 @@
5 <meta name="viewport" content="width=device-width, initial-scale=1">5 <meta name="viewport" content="width=device-width, initial-scale=1">
6 <meta name="color-scheme" content="light dark">6 <meta name="color-scheme" content="light dark">
7 <title>logo.svg · files · example.test</title>7 <title>logo.svg · files · example.test</title>
8+<meta name="theme-color" media="(prefers-color-scheme: light)" content="#ffffff">
9+<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#0c0c0c">
10+<meta name="apple-mobile-web-app-title" content="ermite">
8 <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg">11 <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg">
12+<link rel="apple-touch-icon" href="/-/static/app/apple-touch-icon.HASH.png">
13+<link rel="manifest" href="/-/static/app/manifest.HASH.webmanifest">
9 <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin>14 <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin>
10 <link rel="stylesheet" href="/-/static/css/ermite.HASH.css">15 <link rel="stylesheet" href="/-/static/css/ermite.HASH.css">
11 <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'>16 <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'>
@@ -28,10 +33,9 @@
28 <a class="skip" href="#main">skip to content</a>33 <a class="skip" href="#main">skip to content</a>
29 <header class="hdr">34 <header class="hdr">
30 <div class="hdr-top">35 <div class="hdr-top">
31-<a class="brand" href="/" data-g="h" title="home (g h)">36+<a class="brand" href="/" data-g="h" aria-label="home" title="home (g h)">
32 <span class="brand-mark" aria-hidden="true">37 <span class="brand-mark" aria-hidden="true">
33 </span>38 </span>
34-<span class="brand-name">example.test</span>
35 </a>39 </a>
36 <nav class="crumbs" aria-label="Path">40 <nav class="crumbs" aria-label="Path">
37 <ol>41 <ol>
@@ -62,7 +66,6 @@
62 <span class="search-label">Go to…</span>66 <span class="search-label">Go to…</span>
63 <kbd aria-hidden="true">ctrl k</kbd>67 <kbd aria-hidden="true">ctrl k</kbd>
64 </button>68 </button>
65-<button type="button" class="btn icon" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" aria-label="Keyboard shortcuts" title="keyboard shortcuts (?)">?</button>
66 </div>69 </div>
67 </div>70 </div>
68 <nav class="tabs" aria-label="Sections">71 <nav class="tabs" aria-label="Sections">
@@ -154,8 +157,8 @@
154 <footer class="foot">157 <footer class="foot">
155 <span>ermite devel</span>158 <span>ermite devel</span>
156 <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a>159 <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a>
157-<span class="foot-keys">160+<button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?">
158-<kbd>?</kbd> keyboard shortcuts</span>161+<kbd aria-hidden="true">?</kbd> keyboard shortcuts</button>
159 </footer>162 </footer>
160 <div class="hint" role="status" aria-live="polite">163 <div class="hint" role="status" aria-live="polite">
161 <span class="hint-nav" :text="document.body.dataset.nav || ''">164 <span class="hint-nav" :text="document.body.dataset.nav || ''">
M internal/app/testdata/web/blob-text.html +8 -5
@@ -5,7 +5,12 @@
55 <meta name="viewport" content="width=device-width, initial-scale=1">
66 <meta name="color-scheme" content="light dark">
77 <title>notes.txt · files · example.test</title>
8+<meta name="theme-color" media="(prefers-color-scheme: light)" content="#ffffff">
9+<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#0c0c0c">
10+<meta name="apple-mobile-web-app-title" content="ermite">
811 <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg">
12+<link rel="apple-touch-icon" href="/-/static/app/apple-touch-icon.HASH.png">
13+<link rel="manifest" href="/-/static/app/manifest.HASH.webmanifest">
914 <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin>
1015 <link rel="stylesheet" href="/-/static/css/ermite.HASH.css">
1116 <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'>
@@ -28,10 +33,9 @@
2833 <a class="skip" href="#main">skip to content</a>
2934 <header class="hdr">
3035 <div class="hdr-top">
31-<a class="brand" href="/" data-g="h" title="home (g h)">
36+<a class="brand" href="/" data-g="h" aria-label="home" title="home (g h)">
3237 <span class="brand-mark" aria-hidden="true">
3338 </span>
34-<span class="brand-name">example.test</span>
3539 </a>
3640 <nav class="crumbs" aria-label="Path">
3741 <ol>
@@ -62,7 +66,6 @@
6266 <span class="search-label">Go to…</span>
6367 <kbd aria-hidden="true">ctrl k</kbd>
6468 </button>
65-<button type="button" class="btn icon" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" aria-label="Keyboard shortcuts" title="keyboard shortcuts (?)">?</button>
6669 </div>
6770 </div>
6871 <nav class="tabs" aria-label="Sections">
@@ -149,8 +152,8 @@
149152 <footer class="foot">
150153 <span>ermite devel</span>
151154 <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a>
152-<span class="foot-keys">
153-<kbd>?</kbd> keyboard shortcuts</span>
155+<button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?">
156+<kbd aria-hidden="true">?</kbd> keyboard shortcuts</button>
154157 </footer>
155158 <div class="hint" role="status" aria-live="polite">
156159 <span class="hint-nav" :text="document.body.dataset.nav || ''">
@@ -5,7 +5,12 @@
5 <meta name="viewport" content="width=device-width, initial-scale=1">5 <meta name="viewport" content="width=device-width, initial-scale=1">
6 <meta name="color-scheme" content="light dark">6 <meta name="color-scheme" content="light dark">
7 <title>notes.txt · files · example.test</title>7 <title>notes.txt · files · example.test</title>
8+<meta name="theme-color" media="(prefers-color-scheme: light)" content="#ffffff">
9+<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#0c0c0c">
10+<meta name="apple-mobile-web-app-title" content="ermite">
8 <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg">11 <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg">
12+<link rel="apple-touch-icon" href="/-/static/app/apple-touch-icon.HASH.png">
13+<link rel="manifest" href="/-/static/app/manifest.HASH.webmanifest">
9 <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin>14 <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin>
10 <link rel="stylesheet" href="/-/static/css/ermite.HASH.css">15 <link rel="stylesheet" href="/-/static/css/ermite.HASH.css">
11 <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'>16 <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'>
@@ -28,10 +33,9 @@
28 <a class="skip" href="#main">skip to content</a>33 <a class="skip" href="#main">skip to content</a>
29 <header class="hdr">34 <header class="hdr">
30 <div class="hdr-top">35 <div class="hdr-top">
31-<a class="brand" href="/" data-g="h" title="home (g h)">36+<a class="brand" href="/" data-g="h" aria-label="home" title="home (g h)">
32 <span class="brand-mark" aria-hidden="true">37 <span class="brand-mark" aria-hidden="true">
33 </span>38 </span>
34-<span class="brand-name">example.test</span>
35 </a>39 </a>
36 <nav class="crumbs" aria-label="Path">40 <nav class="crumbs" aria-label="Path">
37 <ol>41 <ol>
@@ -62,7 +66,6 @@
62 <span class="search-label">Go to…</span>66 <span class="search-label">Go to…</span>
63 <kbd aria-hidden="true">ctrl k</kbd>67 <kbd aria-hidden="true">ctrl k</kbd>
64 </button>68 </button>
65-<button type="button" class="btn icon" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" aria-label="Keyboard shortcuts" title="keyboard shortcuts (?)">?</button>
66 </div>69 </div>
67 </div>70 </div>
68 <nav class="tabs" aria-label="Sections">71 <nav class="tabs" aria-label="Sections">
@@ -149,8 +152,8 @@
149 <footer class="foot">152 <footer class="foot">
150 <span>ermite devel</span>153 <span>ermite devel</span>
151 <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a>154 <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a>
152-<span class="foot-keys">155+<button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?">
153-<kbd>?</kbd> keyboard shortcuts</span>156+<kbd aria-hidden="true">?</kbd> keyboard shortcuts</button>
154 </footer>157 </footer>
155 <div class="hint" role="status" aria-live="polite">158 <div class="hint" role="status" aria-live="polite">
156 <span class="hint-nav" :text="document.body.dataset.nav || ''">159 <span class="hint-nav" :text="document.body.dataset.nav || ''">
M internal/app/testdata/web/blob-too-large.html +8 -5
@@ -5,7 +5,12 @@
55 <meta name="viewport" content="width=device-width, initial-scale=1">
66 <meta name="color-scheme" content="light dark">
77 <title>big.txt · files · example.test</title>
8+<meta name="theme-color" media="(prefers-color-scheme: light)" content="#ffffff">
9+<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#0c0c0c">
10+<meta name="apple-mobile-web-app-title" content="ermite">
811 <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg">
12+<link rel="apple-touch-icon" href="/-/static/app/apple-touch-icon.HASH.png">
13+<link rel="manifest" href="/-/static/app/manifest.HASH.webmanifest">
914 <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin>
1015 <link rel="stylesheet" href="/-/static/css/ermite.HASH.css">
1116 <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'>
@@ -28,10 +33,9 @@
2833 <a class="skip" href="#main">skip to content</a>
2934 <header class="hdr">
3035 <div class="hdr-top">
31-<a class="brand" href="/" data-g="h" title="home (g h)">
36+<a class="brand" href="/" data-g="h" aria-label="home" title="home (g h)">
3237 <span class="brand-mark" aria-hidden="true">
3338 </span>
34-<span class="brand-name">example.test</span>
3539 </a>
3640 <nav class="crumbs" aria-label="Path">
3741 <ol>
@@ -62,7 +66,6 @@
6266 <span class="search-label">Go to…</span>
6367 <kbd aria-hidden="true">ctrl k</kbd>
6468 </button>
65-<button type="button" class="btn icon" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" aria-label="Keyboard shortcuts" title="keyboard shortcuts (?)">?</button>
6669 </div>
6770 </div>
6871 <nav class="tabs" aria-label="Sections">
@@ -129,8 +132,8 @@
129132 <footer class="foot">
130133 <span>ermite devel</span>
131134 <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a>
132-<span class="foot-keys">
133-<kbd>?</kbd> keyboard shortcuts</span>
135+<button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?">
136+<kbd aria-hidden="true">?</kbd> keyboard shortcuts</button>
134137 </footer>
135138 <div class="hint" role="status" aria-live="polite">
136139 <span class="hint-nav" :text="document.body.dataset.nav || ''">
@@ -5,7 +5,12 @@
5 <meta name="viewport" content="width=device-width, initial-scale=1">5 <meta name="viewport" content="width=device-width, initial-scale=1">
6 <meta name="color-scheme" content="light dark">6 <meta name="color-scheme" content="light dark">
7 <title>big.txt · files · example.test</title>7 <title>big.txt · files · example.test</title>
8+<meta name="theme-color" media="(prefers-color-scheme: light)" content="#ffffff">
9+<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#0c0c0c">
10+<meta name="apple-mobile-web-app-title" content="ermite">
8 <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg">11 <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg">
12+<link rel="apple-touch-icon" href="/-/static/app/apple-touch-icon.HASH.png">
13+<link rel="manifest" href="/-/static/app/manifest.HASH.webmanifest">
9 <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin>14 <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin>
10 <link rel="stylesheet" href="/-/static/css/ermite.HASH.css">15 <link rel="stylesheet" href="/-/static/css/ermite.HASH.css">
11 <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'>16 <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'>
@@ -28,10 +33,9 @@
28 <a class="skip" href="#main">skip to content</a>33 <a class="skip" href="#main">skip to content</a>
29 <header class="hdr">34 <header class="hdr">
30 <div class="hdr-top">35 <div class="hdr-top">
31-<a class="brand" href="/" data-g="h" title="home (g h)">36+<a class="brand" href="/" data-g="h" aria-label="home" title="home (g h)">
32 <span class="brand-mark" aria-hidden="true">37 <span class="brand-mark" aria-hidden="true">
33 </span>38 </span>
34-<span class="brand-name">example.test</span>
35 </a>39 </a>
36 <nav class="crumbs" aria-label="Path">40 <nav class="crumbs" aria-label="Path">
37 <ol>41 <ol>
@@ -62,7 +66,6 @@
62 <span class="search-label">Go to…</span>66 <span class="search-label">Go to…</span>
63 <kbd aria-hidden="true">ctrl k</kbd>67 <kbd aria-hidden="true">ctrl k</kbd>
64 </button>68 </button>
65-<button type="button" class="btn icon" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" aria-label="Keyboard shortcuts" title="keyboard shortcuts (?)">?</button>
66 </div>69 </div>
67 </div>70 </div>
68 <nav class="tabs" aria-label="Sections">71 <nav class="tabs" aria-label="Sections">
@@ -129,8 +132,8 @@
129 <footer class="foot">132 <footer class="foot">
130 <span>ermite devel</span>133 <span>ermite devel</span>
131 <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a>134 <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a>
132-<span class="foot-keys">135+<button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?">
133-<kbd>?</kbd> keyboard shortcuts</span>136+<kbd aria-hidden="true">?</kbd> keyboard shortcuts</button>
134 </footer>137 </footer>
135 <div class="hint" role="status" aria-live="polite">138 <div class="hint" role="status" aria-live="polite">
136 <span class="hint-nav" :text="document.body.dataset.nav || ''">139 <span class="hint-nav" :text="document.body.dataset.nav || ''">
M internal/app/testdata/web/blob-trojan.html +8 -5
@@ -5,7 +5,12 @@
55 <meta name="viewport" content="width=device-width, initial-scale=1">
66 <meta name="color-scheme" content="light dark">
77 <title>evil.go · files · example.test</title>
8+<meta name="theme-color" media="(prefers-color-scheme: light)" content="#ffffff">
9+<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#0c0c0c">
10+<meta name="apple-mobile-web-app-title" content="ermite">
811 <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg">
12+<link rel="apple-touch-icon" href="/-/static/app/apple-touch-icon.HASH.png">
13+<link rel="manifest" href="/-/static/app/manifest.HASH.webmanifest">
914 <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin>
1015 <link rel="stylesheet" href="/-/static/css/ermite.HASH.css">
1116 <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'>
@@ -28,10 +33,9 @@
2833 <a class="skip" href="#main">skip to content</a>
2934 <header class="hdr">
3035 <div class="hdr-top">
31-<a class="brand" href="/" data-g="h" title="home (g h)">
36+<a class="brand" href="/" data-g="h" aria-label="home" title="home (g h)">
3237 <span class="brand-mark" aria-hidden="true">
3338 </span>
34-<span class="brand-name">example.test</span>
3539 </a>
3640 <nav class="crumbs" aria-label="Path">
3741 <ol>
@@ -62,7 +66,6 @@
6266 <span class="search-label">Go to…</span>
6367 <kbd aria-hidden="true">ctrl k</kbd>
6468 </button>
65-<button type="button" class="btn icon" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" aria-label="Keyboard shortcuts" title="keyboard shortcuts (?)">?</button>
6669 </div>
6770 </div>
6871 <nav class="tabs" aria-label="Sections">
@@ -196,8 +199,8 @@
196199 <footer class="foot">
197200 <span>ermite devel</span>
198201 <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a>
199-<span class="foot-keys">
200-<kbd>?</kbd> keyboard shortcuts</span>
202+<button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?">
203+<kbd aria-hidden="true">?</kbd> keyboard shortcuts</button>
201204 </footer>
202205 <div class="hint" role="status" aria-live="polite">
203206 <span class="hint-nav" :text="document.body.dataset.nav || ''">
@@ -5,7 +5,12 @@
5 <meta name="viewport" content="width=device-width, initial-scale=1">5 <meta name="viewport" content="width=device-width, initial-scale=1">
6 <meta name="color-scheme" content="light dark">6 <meta name="color-scheme" content="light dark">
7 <title>evil.go · files · example.test</title>7 <title>evil.go · files · example.test</title>
8+<meta name="theme-color" media="(prefers-color-scheme: light)" content="#ffffff">
9+<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#0c0c0c">
10+<meta name="apple-mobile-web-app-title" content="ermite">
8 <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg">11 <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg">
12+<link rel="apple-touch-icon" href="/-/static/app/apple-touch-icon.HASH.png">
13+<link rel="manifest" href="/-/static/app/manifest.HASH.webmanifest">
9 <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin>14 <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin>
10 <link rel="stylesheet" href="/-/static/css/ermite.HASH.css">15 <link rel="stylesheet" href="/-/static/css/ermite.HASH.css">
11 <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'>16 <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'>
@@ -28,10 +33,9 @@
28 <a class="skip" href="#main">skip to content</a>33 <a class="skip" href="#main">skip to content</a>
29 <header class="hdr">34 <header class="hdr">
30 <div class="hdr-top">35 <div class="hdr-top">
31-<a class="brand" href="/" data-g="h" title="home (g h)">36+<a class="brand" href="/" data-g="h" aria-label="home" title="home (g h)">
32 <span class="brand-mark" aria-hidden="true">37 <span class="brand-mark" aria-hidden="true">
33 </span>38 </span>
34-<span class="brand-name">example.test</span>
35 </a>39 </a>
36 <nav class="crumbs" aria-label="Path">40 <nav class="crumbs" aria-label="Path">
37 <ol>41 <ol>
@@ -62,7 +66,6 @@
62 <span class="search-label">Go to…</span>66 <span class="search-label">Go to…</span>
63 <kbd aria-hidden="true">ctrl k</kbd>67 <kbd aria-hidden="true">ctrl k</kbd>
64 </button>68 </button>
65-<button type="button" class="btn icon" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" aria-label="Keyboard shortcuts" title="keyboard shortcuts (?)">?</button>
66 </div>69 </div>
67 </div>70 </div>
68 <nav class="tabs" aria-label="Sections">71 <nav class="tabs" aria-label="Sections">
@@ -196,8 +199,8 @@
196 <footer class="foot">199 <footer class="foot">
197 <span>ermite devel</span>200 <span>ermite devel</span>
198 <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a>201 <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a>
199-<span class="foot-keys">202+<button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?">
200-<kbd>?</kbd> keyboard shortcuts</span>203+<kbd aria-hidden="true">?</kbd> keyboard shortcuts</button>
201 </footer>204 </footer>
202 <div class="hint" role="status" aria-live="polite">205 <div class="hint" role="status" aria-live="polite">
203 <span class="hint-nav" :text="document.body.dataset.nav || ''">206 <span class="hint-nav" :text="document.body.dataset.nav || ''">
M internal/app/testdata/web/commit-big.html +8 -5
@@ -5,7 +5,12 @@
55 <meta name="viewport" content="width=device-width, initial-scale=1">
66 <meta name="color-scheme" content="light dark">
77 <title>SHA7 add a very large file · diffs · example.test</title>
8+<meta name="theme-color" media="(prefers-color-scheme: light)" content="#ffffff">
9+<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#0c0c0c">
10+<meta name="apple-mobile-web-app-title" content="ermite">
811 <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg">
12+<link rel="apple-touch-icon" href="/-/static/app/apple-touch-icon.HASH.png">
13+<link rel="manifest" href="/-/static/app/manifest.HASH.webmanifest">
914 <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin>
1015 <link rel="stylesheet" href="/-/static/css/ermite.HASH.css">
1116 <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'>
@@ -28,10 +33,9 @@
2833 <a class="skip" href="#main">skip to content</a>
2934 <header class="hdr">
3035 <div class="hdr-top">
31-<a class="brand" href="/" data-g="h" title="home (g h)">
36+<a class="brand" href="/" data-g="h" aria-label="home" title="home (g h)">
3237 <span class="brand-mark" aria-hidden="true">
3338 </span>
34-<span class="brand-name">example.test</span>
3539 </a>
3640 <nav class="crumbs" aria-label="Path">
3741 <ol>
@@ -59,7 +63,6 @@
5963 <span class="search-label">Go to…</span>
6064 <kbd aria-hidden="true">ctrl k</kbd>
6165 </button>
62-<button type="button" class="btn icon" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" aria-label="Keyboard shortcuts" title="keyboard shortcuts (?)">?</button>
6366 </div>
6467 </div>
6568 <nav class="tabs" aria-label="Sections">
@@ -166,8 +169,8 @@
166169 <footer class="foot">
167170 <span>ermite devel</span>
168171 <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a>
169-<span class="foot-keys">
170-<kbd>?</kbd> keyboard shortcuts</span>
172+<button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?">
173+<kbd aria-hidden="true">?</kbd> keyboard shortcuts</button>
171174 </footer>
172175 <div class="hint" role="status" aria-live="polite">
173176 <span class="hint-nav" :text="document.body.dataset.nav || ''">
@@ -5,7 +5,12 @@
5 <meta name="viewport" content="width=device-width, initial-scale=1">5 <meta name="viewport" content="width=device-width, initial-scale=1">
6 <meta name="color-scheme" content="light dark">6 <meta name="color-scheme" content="light dark">
7 <title>SHA7 add a very large file · diffs · example.test</title>7 <title>SHA7 add a very large file · diffs · example.test</title>
8+<meta name="theme-color" media="(prefers-color-scheme: light)" content="#ffffff">
9+<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#0c0c0c">
10+<meta name="apple-mobile-web-app-title" content="ermite">
8 <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg">11 <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg">
12+<link rel="apple-touch-icon" href="/-/static/app/apple-touch-icon.HASH.png">
13+<link rel="manifest" href="/-/static/app/manifest.HASH.webmanifest">
9 <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin>14 <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin>
10 <link rel="stylesheet" href="/-/static/css/ermite.HASH.css">15 <link rel="stylesheet" href="/-/static/css/ermite.HASH.css">
11 <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'>16 <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'>
@@ -28,10 +33,9 @@
28 <a class="skip" href="#main">skip to content</a>33 <a class="skip" href="#main">skip to content</a>
29 <header class="hdr">34 <header class="hdr">
30 <div class="hdr-top">35 <div class="hdr-top">
31-<a class="brand" href="/" data-g="h" title="home (g h)">36+<a class="brand" href="/" data-g="h" aria-label="home" title="home (g h)">
32 <span class="brand-mark" aria-hidden="true">37 <span class="brand-mark" aria-hidden="true">
33 </span>38 </span>
34-<span class="brand-name">example.test</span>
35 </a>39 </a>
36 <nav class="crumbs" aria-label="Path">40 <nav class="crumbs" aria-label="Path">
37 <ol>41 <ol>
@@ -59,7 +63,6 @@
59 <span class="search-label">Go to…</span>63 <span class="search-label">Go to…</span>
60 <kbd aria-hidden="true">ctrl k</kbd>64 <kbd aria-hidden="true">ctrl k</kbd>
61 </button>65 </button>
62-<button type="button" class="btn icon" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" aria-label="Keyboard shortcuts" title="keyboard shortcuts (?)">?</button>
63 </div>66 </div>
64 </div>67 </div>
65 <nav class="tabs" aria-label="Sections">68 <nav class="tabs" aria-label="Sections">
@@ -166,8 +169,8 @@
166 <footer class="foot">169 <footer class="foot">
167 <span>ermite devel</span>170 <span>ermite devel</span>
168 <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a>171 <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a>
169-<span class="foot-keys">172+<button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?">
170-<kbd>?</kbd> keyboard shortcuts</span>173+<kbd aria-hidden="true">?</kbd> keyboard shortcuts</button>
171 </footer>174 </footer>
172 <div class="hint" role="status" aria-live="polite">175 <div class="hint" role="status" aria-live="polite">
173 <span class="hint-nav" :text="document.body.dataset.nav || ''">176 <span class="hint-nav" :text="document.body.dataset.nav || ''">
M internal/app/testdata/web/commit-binary.html +8 -5
@@ -5,7 +5,12 @@
55 <meta name="viewport" content="width=device-width, initial-scale=1">
66 <meta name="color-scheme" content="light dark">
77 <title>SHA7 change a binary file · diffs · example.test</title>
8+<meta name="theme-color" media="(prefers-color-scheme: light)" content="#ffffff">
9+<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#0c0c0c">
10+<meta name="apple-mobile-web-app-title" content="ermite">
811 <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg">
12+<link rel="apple-touch-icon" href="/-/static/app/apple-touch-icon.HASH.png">
13+<link rel="manifest" href="/-/static/app/manifest.HASH.webmanifest">
914 <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin>
1015 <link rel="stylesheet" href="/-/static/css/ermite.HASH.css">
1116 <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'>
@@ -28,10 +33,9 @@
2833 <a class="skip" href="#main">skip to content</a>
2934 <header class="hdr">
3035 <div class="hdr-top">
31-<a class="brand" href="/" data-g="h" title="home (g h)">
36+<a class="brand" href="/" data-g="h" aria-label="home" title="home (g h)">
3237 <span class="brand-mark" aria-hidden="true">
3338 </span>
34-<span class="brand-name">example.test</span>
3539 </a>
3640 <nav class="crumbs" aria-label="Path">
3741 <ol>
@@ -59,7 +63,6 @@
5963 <span class="search-label">Go to…</span>
6064 <kbd aria-hidden="true">ctrl k</kbd>
6165 </button>
62-<button type="button" class="btn icon" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" aria-label="Keyboard shortcuts" title="keyboard shortcuts (?)">?</button>
6366 </div>
6467 </div>
6568 <nav class="tabs" aria-label="Sections">
@@ -161,8 +164,8 @@
161164 <footer class="foot">
162165 <span>ermite devel</span>
163166 <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a>
164-<span class="foot-keys">
165-<kbd>?</kbd> keyboard shortcuts</span>
167+<button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?">
168+<kbd aria-hidden="true">?</kbd> keyboard shortcuts</button>
166169 </footer>
167170 <div class="hint" role="status" aria-live="polite">
168171 <span class="hint-nav" :text="document.body.dataset.nav || ''">
@@ -5,7 +5,12 @@
5 <meta name="viewport" content="width=device-width, initial-scale=1">5 <meta name="viewport" content="width=device-width, initial-scale=1">
6 <meta name="color-scheme" content="light dark">6 <meta name="color-scheme" content="light dark">
7 <title>SHA7 change a binary file · diffs · example.test</title>7 <title>SHA7 change a binary file · diffs · example.test</title>
8+<meta name="theme-color" media="(prefers-color-scheme: light)" content="#ffffff">
9+<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#0c0c0c">
10+<meta name="apple-mobile-web-app-title" content="ermite">
8 <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg">11 <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg">
12+<link rel="apple-touch-icon" href="/-/static/app/apple-touch-icon.HASH.png">
13+<link rel="manifest" href="/-/static/app/manifest.HASH.webmanifest">
9 <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin>14 <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin>
10 <link rel="stylesheet" href="/-/static/css/ermite.HASH.css">15 <link rel="stylesheet" href="/-/static/css/ermite.HASH.css">
11 <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'>16 <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'>
@@ -28,10 +33,9 @@
28 <a class="skip" href="#main">skip to content</a>33 <a class="skip" href="#main">skip to content</a>
29 <header class="hdr">34 <header class="hdr">
30 <div class="hdr-top">35 <div class="hdr-top">
31-<a class="brand" href="/" data-g="h" title="home (g h)">36+<a class="brand" href="/" data-g="h" aria-label="home" title="home (g h)">
32 <span class="brand-mark" aria-hidden="true">37 <span class="brand-mark" aria-hidden="true">
33 </span>38 </span>
34-<span class="brand-name">example.test</span>
35 </a>39 </a>
36 <nav class="crumbs" aria-label="Path">40 <nav class="crumbs" aria-label="Path">
37 <ol>41 <ol>
@@ -59,7 +63,6 @@
59 <span class="search-label">Go to…</span>63 <span class="search-label">Go to…</span>
60 <kbd aria-hidden="true">ctrl k</kbd>64 <kbd aria-hidden="true">ctrl k</kbd>
61 </button>65 </button>
62-<button type="button" class="btn icon" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" aria-label="Keyboard shortcuts" title="keyboard shortcuts (?)">?</button>
63 </div>66 </div>
64 </div>67 </div>
65 <nav class="tabs" aria-label="Sections">68 <nav class="tabs" aria-label="Sections">
@@ -161,8 +164,8 @@
161 <footer class="foot">164 <footer class="foot">
162 <span>ermite devel</span>165 <span>ermite devel</span>
163 <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a>166 <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a>
164-<span class="foot-keys">167+<button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?">
165-<kbd>?</kbd> keyboard shortcuts</span>168+<kbd aria-hidden="true">?</kbd> keyboard shortcuts</button>
166 </footer>169 </footer>
167 <div class="hint" role="status" aria-live="polite">170 <div class="hint" role="status" aria-live="polite">
168 <span class="hint-nav" :text="document.body.dataset.nav || ''">171 <span class="hint-nav" :text="document.body.dataset.nav || ''">
M internal/app/testdata/web/commit-merge.html +8 -5
@@ -5,7 +5,12 @@
55 <meta name="viewport" content="width=device-width, initial-scale=1">
66 <meta name="color-scheme" content="light dark">
77 <title>SHA7 merge the side branch · diffs · example.test</title>
8+<meta name="theme-color" media="(prefers-color-scheme: light)" content="#ffffff">
9+<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#0c0c0c">
10+<meta name="apple-mobile-web-app-title" content="ermite">
811 <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg">
12+<link rel="apple-touch-icon" href="/-/static/app/apple-touch-icon.HASH.png">
13+<link rel="manifest" href="/-/static/app/manifest.HASH.webmanifest">
914 <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin>
1015 <link rel="stylesheet" href="/-/static/css/ermite.HASH.css">
1116 <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'>
@@ -28,10 +33,9 @@
2833 <a class="skip" href="#main">skip to content</a>
2934 <header class="hdr">
3035 <div class="hdr-top">
31-<a class="brand" href="/" data-g="h" title="home (g h)">
36+<a class="brand" href="/" data-g="h" aria-label="home" title="home (g h)">
3237 <span class="brand-mark" aria-hidden="true">
3338 </span>
34-<span class="brand-name">example.test</span>
3539 </a>
3640 <nav class="crumbs" aria-label="Path">
3741 <ol>
@@ -59,7 +63,6 @@
5963 <span class="search-label">Go to…</span>
6064 <kbd aria-hidden="true">ctrl k</kbd>
6165 </button>
62-<button type="button" class="btn icon" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" aria-label="Keyboard shortcuts" title="keyboard shortcuts (?)">?</button>
6366 </div>
6467 </div>
6568 <nav class="tabs" aria-label="Sections">
@@ -200,8 +203,8 @@
200203 <footer class="foot">
201204 <span>ermite devel</span>
202205 <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a>
203-<span class="foot-keys">
204-<kbd>?</kbd> keyboard shortcuts</span>
206+<button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?">
207+<kbd aria-hidden="true">?</kbd> keyboard shortcuts</button>
205208 </footer>
206209 <div class="hint" role="status" aria-live="polite">
207210 <span class="hint-nav" :text="document.body.dataset.nav || ''">
@@ -5,7 +5,12 @@
5 <meta name="viewport" content="width=device-width, initial-scale=1">5 <meta name="viewport" content="width=device-width, initial-scale=1">
6 <meta name="color-scheme" content="light dark">6 <meta name="color-scheme" content="light dark">
7 <title>SHA7 merge the side branch · diffs · example.test</title>7 <title>SHA7 merge the side branch · diffs · example.test</title>
8+<meta name="theme-color" media="(prefers-color-scheme: light)" content="#ffffff">
9+<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#0c0c0c">
10+<meta name="apple-mobile-web-app-title" content="ermite">
8 <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg">11 <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg">
12+<link rel="apple-touch-icon" href="/-/static/app/apple-touch-icon.HASH.png">
13+<link rel="manifest" href="/-/static/app/manifest.HASH.webmanifest">
9 <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin>14 <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin>
10 <link rel="stylesheet" href="/-/static/css/ermite.HASH.css">15 <link rel="stylesheet" href="/-/static/css/ermite.HASH.css">
11 <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'>16 <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'>
@@ -28,10 +33,9 @@
28 <a class="skip" href="#main">skip to content</a>33 <a class="skip" href="#main">skip to content</a>
29 <header class="hdr">34 <header class="hdr">
30 <div class="hdr-top">35 <div class="hdr-top">
31-<a class="brand" href="/" data-g="h" title="home (g h)">36+<a class="brand" href="/" data-g="h" aria-label="home" title="home (g h)">
32 <span class="brand-mark" aria-hidden="true">37 <span class="brand-mark" aria-hidden="true">
33 </span>38 </span>
34-<span class="brand-name">example.test</span>
35 </a>39 </a>
36 <nav class="crumbs" aria-label="Path">40 <nav class="crumbs" aria-label="Path">
37 <ol>41 <ol>
@@ -59,7 +63,6 @@
59 <span class="search-label">Go to…</span>63 <span class="search-label">Go to…</span>
60 <kbd aria-hidden="true">ctrl k</kbd>64 <kbd aria-hidden="true">ctrl k</kbd>
61 </button>65 </button>
62-<button type="button" class="btn icon" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" aria-label="Keyboard shortcuts" title="keyboard shortcuts (?)">?</button>
63 </div>66 </div>
64 </div>67 </div>
65 <nav class="tabs" aria-label="Sections">68 <nav class="tabs" aria-label="Sections">
@@ -200,8 +203,8 @@
200 <footer class="foot">203 <footer class="foot">
201 <span>ermite devel</span>204 <span>ermite devel</span>
202 <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a>205 <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a>
203-<span class="foot-keys">206+<button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?">
204-<kbd>?</kbd> keyboard shortcuts</span>207+<kbd aria-hidden="true">?</kbd> keyboard shortcuts</button>
205 </footer>208 </footer>
206 <div class="hint" role="status" aria-live="polite">209 <div class="hint" role="status" aria-live="polite">
207 <span class="hint-nav" :text="document.body.dataset.nav || ''">210 <span class="hint-nav" :text="document.body.dataset.nav || ''">
M internal/app/testdata/web/commit-modify-split.html +8 -5
@@ -5,7 +5,12 @@
55 <meta name="viewport" content="width=device-width, initial-scale=1">
66 <meta name="color-scheme" content="light dark">
77 <title>SHA7 modify a file and make a script executable · diffs · example.test</title>
8+<meta name="theme-color" media="(prefers-color-scheme: light)" content="#ffffff">
9+<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#0c0c0c">
10+<meta name="apple-mobile-web-app-title" content="ermite">
811 <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg">
12+<link rel="apple-touch-icon" href="/-/static/app/apple-touch-icon.HASH.png">
13+<link rel="manifest" href="/-/static/app/manifest.HASH.webmanifest">
914 <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin>
1015 <link rel="stylesheet" href="/-/static/css/ermite.HASH.css">
1116 <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'>
@@ -28,10 +33,9 @@
2833 <a class="skip" href="#main">skip to content</a>
2934 <header class="hdr">
3035 <div class="hdr-top">
31-<a class="brand" href="/" data-g="h" title="home (g h)">
36+<a class="brand" href="/" data-g="h" aria-label="home" title="home (g h)">
3237 <span class="brand-mark" aria-hidden="true">
3338 </span>
34-<span class="brand-name">example.test</span>
3539 </a>
3640 <nav class="crumbs" aria-label="Path">
3741 <ol>
@@ -59,7 +63,6 @@
5963 <span class="search-label">Go to…</span>
6064 <kbd aria-hidden="true">ctrl k</kbd>
6165 </button>
62-<button type="button" class="btn icon" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" aria-label="Keyboard shortcuts" title="keyboard shortcuts (?)">?</button>
6366 </div>
6467 </div>
6568 <nav class="tabs" aria-label="Sections">
@@ -321,8 +324,8 @@ And &lt;b&gt;html&lt;/b&gt; &amp; &#34;quotes&#34;.</pre>
321324 <footer class="foot">
322325 <span>ermite devel</span>
323326 <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a>
324-<span class="foot-keys">
325-<kbd>?</kbd> keyboard shortcuts</span>
327+<button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?">
328+<kbd aria-hidden="true">?</kbd> keyboard shortcuts</button>
326329 </footer>
327330 <div class="hint" role="status" aria-live="polite">
328331 <span class="hint-nav" :text="document.body.dataset.nav || ''">
@@ -5,7 +5,12 @@
5 <meta name="viewport" content="width=device-width, initial-scale=1">5 <meta name="viewport" content="width=device-width, initial-scale=1">
6 <meta name="color-scheme" content="light dark">6 <meta name="color-scheme" content="light dark">
7 <title>SHA7 modify a file and make a script executable · diffs · example.test</title>7 <title>SHA7 modify a file and make a script executable · diffs · example.test</title>
8+<meta name="theme-color" media="(prefers-color-scheme: light)" content="#ffffff">
9+<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#0c0c0c">
10+<meta name="apple-mobile-web-app-title" content="ermite">
8 <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg">11 <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg">
12+<link rel="apple-touch-icon" href="/-/static/app/apple-touch-icon.HASH.png">
13+<link rel="manifest" href="/-/static/app/manifest.HASH.webmanifest">
9 <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin>14 <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin>
10 <link rel="stylesheet" href="/-/static/css/ermite.HASH.css">15 <link rel="stylesheet" href="/-/static/css/ermite.HASH.css">
11 <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'>16 <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'>
@@ -28,10 +33,9 @@
28 <a class="skip" href="#main">skip to content</a>33 <a class="skip" href="#main">skip to content</a>
29 <header class="hdr">34 <header class="hdr">
30 <div class="hdr-top">35 <div class="hdr-top">
31-<a class="brand" href="/" data-g="h" title="home (g h)">36+<a class="brand" href="/" data-g="h" aria-label="home" title="home (g h)">
32 <span class="brand-mark" aria-hidden="true">37 <span class="brand-mark" aria-hidden="true">
33 </span>38 </span>
34-<span class="brand-name">example.test</span>
35 </a>39 </a>
36 <nav class="crumbs" aria-label="Path">40 <nav class="crumbs" aria-label="Path">
37 <ol>41 <ol>
@@ -59,7 +63,6 @@
59 <span class="search-label">Go to…</span>63 <span class="search-label">Go to…</span>
60 <kbd aria-hidden="true">ctrl k</kbd>64 <kbd aria-hidden="true">ctrl k</kbd>
61 </button>65 </button>
62-<button type="button" class="btn icon" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" aria-label="Keyboard shortcuts" title="keyboard shortcuts (?)">?</button>
63 </div>66 </div>
64 </div>67 </div>
65 <nav class="tabs" aria-label="Sections">68 <nav class="tabs" aria-label="Sections">
@@ -321,8 +324,8 @@ And &lt;b&gt;html&lt;/b&gt; &amp; &#34;quotes&#34;.</pre>
321 <footer class="foot">324 <footer class="foot">
322 <span>ermite devel</span>325 <span>ermite devel</span>
323 <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a>326 <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a>
324-<span class="foot-keys">327+<button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?">
325-<kbd>?</kbd> keyboard shortcuts</span>328+<kbd aria-hidden="true">?</kbd> keyboard shortcuts</button>
326 </footer>329 </footer>
327 <div class="hint" role="status" aria-live="polite">330 <div class="hint" role="status" aria-live="polite">
328 <span class="hint-nav" :text="document.body.dataset.nav || ''">331 <span class="hint-nav" :text="document.body.dataset.nav || ''">
M internal/app/testdata/web/commit-modify.html +8 -5
@@ -5,7 +5,12 @@
55 <meta name="viewport" content="width=device-width, initial-scale=1">
66 <meta name="color-scheme" content="light dark">
77 <title>SHA7 modify a file and make a script executable · diffs · example.test</title>
8+<meta name="theme-color" media="(prefers-color-scheme: light)" content="#ffffff">
9+<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#0c0c0c">
10+<meta name="apple-mobile-web-app-title" content="ermite">
811 <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg">
12+<link rel="apple-touch-icon" href="/-/static/app/apple-touch-icon.HASH.png">
13+<link rel="manifest" href="/-/static/app/manifest.HASH.webmanifest">
914 <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin>
1015 <link rel="stylesheet" href="/-/static/css/ermite.HASH.css">
1116 <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'>
@@ -28,10 +33,9 @@
2833 <a class="skip" href="#main">skip to content</a>
2934 <header class="hdr">
3035 <div class="hdr-top">
31-<a class="brand" href="/" data-g="h" title="home (g h)">
36+<a class="brand" href="/" data-g="h" aria-label="home" title="home (g h)">
3237 <span class="brand-mark" aria-hidden="true">
3338 </span>
34-<span class="brand-name">example.test</span>
3539 </a>
3640 <nav class="crumbs" aria-label="Path">
3741 <ol>
@@ -59,7 +63,6 @@
5963 <span class="search-label">Go to…</span>
6064 <kbd aria-hidden="true">ctrl k</kbd>
6165 </button>
62-<button type="button" class="btn icon" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" aria-label="Keyboard shortcuts" title="keyboard shortcuts (?)">?</button>
6366 </div>
6467 </div>
6568 <nav class="tabs" aria-label="Sections">
@@ -321,8 +324,8 @@ And &lt;b&gt;html&lt;/b&gt; &amp; &#34;quotes&#34;.</pre>
321324 <footer class="foot">
322325 <span>ermite devel</span>
323326 <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a>
324-<span class="foot-keys">
325-<kbd>?</kbd> keyboard shortcuts</span>
327+<button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?">
328+<kbd aria-hidden="true">?</kbd> keyboard shortcuts</button>
326329 </footer>
327330 <div class="hint" role="status" aria-live="polite">
328331 <span class="hint-nav" :text="document.body.dataset.nav || ''">
@@ -5,7 +5,12 @@
5 <meta name="viewport" content="width=device-width, initial-scale=1">5 <meta name="viewport" content="width=device-width, initial-scale=1">
6 <meta name="color-scheme" content="light dark">6 <meta name="color-scheme" content="light dark">
7 <title>SHA7 modify a file and make a script executable · diffs · example.test</title>7 <title>SHA7 modify a file and make a script executable · diffs · example.test</title>
8+<meta name="theme-color" media="(prefers-color-scheme: light)" content="#ffffff">
9+<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#0c0c0c">
10+<meta name="apple-mobile-web-app-title" content="ermite">
8 <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg">11 <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg">
12+<link rel="apple-touch-icon" href="/-/static/app/apple-touch-icon.HASH.png">
13+<link rel="manifest" href="/-/static/app/manifest.HASH.webmanifest">
9 <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin>14 <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin>
10 <link rel="stylesheet" href="/-/static/css/ermite.HASH.css">15 <link rel="stylesheet" href="/-/static/css/ermite.HASH.css">
11 <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'>16 <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'>
@@ -28,10 +33,9 @@
28 <a class="skip" href="#main">skip to content</a>33 <a class="skip" href="#main">skip to content</a>
29 <header class="hdr">34 <header class="hdr">
30 <div class="hdr-top">35 <div class="hdr-top">
31-<a class="brand" href="/" data-g="h" title="home (g h)">36+<a class="brand" href="/" data-g="h" aria-label="home" title="home (g h)">
32 <span class="brand-mark" aria-hidden="true">37 <span class="brand-mark" aria-hidden="true">
33 </span>38 </span>
34-<span class="brand-name">example.test</span>
35 </a>39 </a>
36 <nav class="crumbs" aria-label="Path">40 <nav class="crumbs" aria-label="Path">
37 <ol>41 <ol>
@@ -59,7 +63,6 @@
59 <span class="search-label">Go to…</span>63 <span class="search-label">Go to…</span>
60 <kbd aria-hidden="true">ctrl k</kbd>64 <kbd aria-hidden="true">ctrl k</kbd>
61 </button>65 </button>
62-<button type="button" class="btn icon" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" aria-label="Keyboard shortcuts" title="keyboard shortcuts (?)">?</button>
63 </div>66 </div>
64 </div>67 </div>
65 <nav class="tabs" aria-label="Sections">68 <nav class="tabs" aria-label="Sections">
@@ -321,8 +324,8 @@ And &lt;b&gt;html&lt;/b&gt; &amp; &#34;quotes&#34;.</pre>
321 <footer class="foot">324 <footer class="foot">
322 <span>ermite devel</span>325 <span>ermite devel</span>
323 <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a>326 <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a>
324-<span class="foot-keys">327+<button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?">
325-<kbd>?</kbd> keyboard shortcuts</span>328+<kbd aria-hidden="true">?</kbd> keyboard shortcuts</button>
326 </footer>329 </footer>
327 <div class="hint" role="status" aria-live="polite">330 <div class="hint" role="status" aria-live="polite">
328 <span class="hint-nav" :text="document.body.dataset.nav || ''">331 <span class="hint-nav" :text="document.body.dataset.nav || ''">
M internal/app/testdata/web/commit-rename.html +8 -5
@@ -5,7 +5,12 @@
55 <meta name="viewport" content="width=device-width, initial-scale=1">
66 <meta name="color-scheme" content="light dark">
77 <title>SHA7 rename a file and delete another · diffs · example.test</title>
8+<meta name="theme-color" media="(prefers-color-scheme: light)" content="#ffffff">
9+<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#0c0c0c">
10+<meta name="apple-mobile-web-app-title" content="ermite">
811 <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg">
12+<link rel="apple-touch-icon" href="/-/static/app/apple-touch-icon.HASH.png">
13+<link rel="manifest" href="/-/static/app/manifest.HASH.webmanifest">
914 <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin>
1015 <link rel="stylesheet" href="/-/static/css/ermite.HASH.css">
1116 <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'>
@@ -28,10 +33,9 @@
2833 <a class="skip" href="#main">skip to content</a>
2934 <header class="hdr">
3035 <div class="hdr-top">
31-<a class="brand" href="/" data-g="h" title="home (g h)">
36+<a class="brand" href="/" data-g="h" aria-label="home" title="home (g h)">
3237 <span class="brand-mark" aria-hidden="true">
3338 </span>
34-<span class="brand-name">example.test</span>
3539 </a>
3640 <nav class="crumbs" aria-label="Path">
3741 <ol>
@@ -59,7 +63,6 @@
5963 <span class="search-label">Go to…</span>
6064 <kbd aria-hidden="true">ctrl k</kbd>
6165 </button>
62-<button type="button" class="btn icon" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" aria-label="Keyboard shortcuts" title="keyboard shortcuts (?)">?</button>
6366 </div>
6467 </div>
6568 <nav class="tabs" aria-label="Sections">
@@ -226,8 +229,8 @@
226229 <footer class="foot">
227230 <span>ermite devel</span>
228231 <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a>
229-<span class="foot-keys">
230-<kbd>?</kbd> keyboard shortcuts</span>
232+<button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?">
233+<kbd aria-hidden="true">?</kbd> keyboard shortcuts</button>
231234 </footer>
232235 <div class="hint" role="status" aria-live="polite">
233236 <span class="hint-nav" :text="document.body.dataset.nav || ''">
@@ -5,7 +5,12 @@
5 <meta name="viewport" content="width=device-width, initial-scale=1">5 <meta name="viewport" content="width=device-width, initial-scale=1">
6 <meta name="color-scheme" content="light dark">6 <meta name="color-scheme" content="light dark">
7 <title>SHA7 rename a file and delete another · diffs · example.test</title>7 <title>SHA7 rename a file and delete another · diffs · example.test</title>
8+<meta name="theme-color" media="(prefers-color-scheme: light)" content="#ffffff">
9+<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#0c0c0c">
10+<meta name="apple-mobile-web-app-title" content="ermite">
8 <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg">11 <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg">
12+<link rel="apple-touch-icon" href="/-/static/app/apple-touch-icon.HASH.png">
13+<link rel="manifest" href="/-/static/app/manifest.HASH.webmanifest">
9 <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin>14 <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin>
10 <link rel="stylesheet" href="/-/static/css/ermite.HASH.css">15 <link rel="stylesheet" href="/-/static/css/ermite.HASH.css">
11 <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'>16 <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'>
@@ -28,10 +33,9 @@
28 <a class="skip" href="#main">skip to content</a>33 <a class="skip" href="#main">skip to content</a>
29 <header class="hdr">34 <header class="hdr">
30 <div class="hdr-top">35 <div class="hdr-top">
31-<a class="brand" href="/" data-g="h" title="home (g h)">36+<a class="brand" href="/" data-g="h" aria-label="home" title="home (g h)">
32 <span class="brand-mark" aria-hidden="true">37 <span class="brand-mark" aria-hidden="true">
33 </span>38 </span>
34-<span class="brand-name">example.test</span>
35 </a>39 </a>
36 <nav class="crumbs" aria-label="Path">40 <nav class="crumbs" aria-label="Path">
37 <ol>41 <ol>
@@ -59,7 +63,6 @@
59 <span class="search-label">Go to…</span>63 <span class="search-label">Go to…</span>
60 <kbd aria-hidden="true">ctrl k</kbd>64 <kbd aria-hidden="true">ctrl k</kbd>
61 </button>65 </button>
62-<button type="button" class="btn icon" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" aria-label="Keyboard shortcuts" title="keyboard shortcuts (?)">?</button>
63 </div>66 </div>
64 </div>67 </div>
65 <nav class="tabs" aria-label="Sections">68 <nav class="tabs" aria-label="Sections">
@@ -226,8 +229,8 @@
226 <footer class="foot">229 <footer class="foot">
227 <span>ermite devel</span>230 <span>ermite devel</span>
228 <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a>231 <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a>
229-<span class="foot-keys">232+<button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?">
230-<kbd>?</kbd> keyboard shortcuts</span>233+<kbd aria-hidden="true">?</kbd> keyboard shortcuts</button>
231 </footer>234 </footer>
232 <div class="hint" role="status" aria-live="polite">235 <div class="hint" role="status" aria-live="polite">
233 <span class="hint-nav" :text="document.body.dataset.nav || ''">236 <span class="hint-nav" :text="document.body.dataset.nav || ''">
M internal/app/testdata/web/commit-root.html +8 -5
@@ -5,7 +5,12 @@
55 <meta name="viewport" content="width=device-width, initial-scale=1">
66 <meta name="color-scheme" content="light dark">
77 <title>SHA7 root: the first files · diffs · example.test</title>
8+<meta name="theme-color" media="(prefers-color-scheme: light)" content="#ffffff">
9+<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#0c0c0c">
10+<meta name="apple-mobile-web-app-title" content="ermite">
811 <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg">
12+<link rel="apple-touch-icon" href="/-/static/app/apple-touch-icon.HASH.png">
13+<link rel="manifest" href="/-/static/app/manifest.HASH.webmanifest">
914 <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin>
1015 <link rel="stylesheet" href="/-/static/css/ermite.HASH.css">
1116 <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'>
@@ -28,10 +33,9 @@
2833 <a class="skip" href="#main">skip to content</a>
2934 <header class="hdr">
3035 <div class="hdr-top">
31-<a class="brand" href="/" data-g="h" title="home (g h)">
36+<a class="brand" href="/" data-g="h" aria-label="home" title="home (g h)">
3237 <span class="brand-mark" aria-hidden="true">
3338 </span>
34-<span class="brand-name">example.test</span>
3539 </a>
3640 <nav class="crumbs" aria-label="Path">
3741 <ol>
@@ -59,7 +63,6 @@
5963 <span class="search-label">Go to…</span>
6064 <kbd aria-hidden="true">ctrl k</kbd>
6165 </button>
62-<button type="button" class="btn icon" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" aria-label="Keyboard shortcuts" title="keyboard shortcuts (?)">?</button>
6366 </div>
6467 </div>
6568 <nav class="tabs" aria-label="Sections">
@@ -542,8 +545,8 @@
542545 <footer class="foot">
543546 <span>ermite devel</span>
544547 <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a>
545-<span class="foot-keys">
546-<kbd>?</kbd> keyboard shortcuts</span>
548+<button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?">
549+<kbd aria-hidden="true">?</kbd> keyboard shortcuts</button>
547550 </footer>
548551 <div class="hint" role="status" aria-live="polite">
549552 <span class="hint-nav" :text="document.body.dataset.nav || ''">
@@ -5,7 +5,12 @@
5 <meta name="viewport" content="width=device-width, initial-scale=1">5 <meta name="viewport" content="width=device-width, initial-scale=1">
6 <meta name="color-scheme" content="light dark">6 <meta name="color-scheme" content="light dark">
7 <title>SHA7 root: the first files · diffs · example.test</title>7 <title>SHA7 root: the first files · diffs · example.test</title>
8+<meta name="theme-color" media="(prefers-color-scheme: light)" content="#ffffff">
9+<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#0c0c0c">
10+<meta name="apple-mobile-web-app-title" content="ermite">
8 <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg">11 <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg">
12+<link rel="apple-touch-icon" href="/-/static/app/apple-touch-icon.HASH.png">
13+<link rel="manifest" href="/-/static/app/manifest.HASH.webmanifest">
9 <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin>14 <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin>
10 <link rel="stylesheet" href="/-/static/css/ermite.HASH.css">15 <link rel="stylesheet" href="/-/static/css/ermite.HASH.css">
11 <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'>16 <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'>
@@ -28,10 +33,9 @@
28 <a class="skip" href="#main">skip to content</a>33 <a class="skip" href="#main">skip to content</a>
29 <header class="hdr">34 <header class="hdr">
30 <div class="hdr-top">35 <div class="hdr-top">
31-<a class="brand" href="/" data-g="h" title="home (g h)">36+<a class="brand" href="/" data-g="h" aria-label="home" title="home (g h)">
32 <span class="brand-mark" aria-hidden="true">37 <span class="brand-mark" aria-hidden="true">
33 </span>38 </span>
34-<span class="brand-name">example.test</span>
35 </a>39 </a>
36 <nav class="crumbs" aria-label="Path">40 <nav class="crumbs" aria-label="Path">
37 <ol>41 <ol>
@@ -59,7 +63,6 @@
59 <span class="search-label">Go to…</span>63 <span class="search-label">Go to…</span>
60 <kbd aria-hidden="true">ctrl k</kbd>64 <kbd aria-hidden="true">ctrl k</kbd>
61 </button>65 </button>
62-<button type="button" class="btn icon" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" aria-label="Keyboard shortcuts" title="keyboard shortcuts (?)">?</button>
63 </div>66 </div>
64 </div>67 </div>
65 <nav class="tabs" aria-label="Sections">68 <nav class="tabs" aria-label="Sections">
@@ -542,8 +545,8 @@
542 <footer class="foot">545 <footer class="foot">
543 <span>ermite devel</span>546 <span>ermite devel</span>
544 <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a>547 <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a>
545-<span class="foot-keys">548+<button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?">
546-<kbd>?</kbd> keyboard shortcuts</span>549+<kbd aria-hidden="true">?</kbd> keyboard shortcuts</button>
547 </footer>550 </footer>
548 <div class="hint" role="status" aria-live="polite">551 <div class="hint" role="status" aria-live="polite">
549 <span class="hint-nav" :text="document.body.dataset.nav || ''">552 <span class="hint-nav" :text="document.body.dataset.nav || ''">
M internal/app/testdata/web/commit-trojan.html +8 -5
@@ -5,7 +5,12 @@
55 <meta name="viewport" content="width=device-width, initial-scale=1">
66 <meta name="color-scheme" content="light dark">
77 <title>SHA7 add evil.go · diffs · example.test</title>
8+<meta name="theme-color" media="(prefers-color-scheme: light)" content="#ffffff">
9+<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#0c0c0c">
10+<meta name="apple-mobile-web-app-title" content="ermite">
811 <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg">
12+<link rel="apple-touch-icon" href="/-/static/app/apple-touch-icon.HASH.png">
13+<link rel="manifest" href="/-/static/app/manifest.HASH.webmanifest">
914 <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin>
1015 <link rel="stylesheet" href="/-/static/css/ermite.HASH.css">
1116 <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'>
@@ -28,10 +33,9 @@
2833 <a class="skip" href="#main">skip to content</a>
2934 <header class="hdr">
3035 <div class="hdr-top">
31-<a class="brand" href="/" data-g="h" title="home (g h)">
36+<a class="brand" href="/" data-g="h" aria-label="home" title="home (g h)">
3237 <span class="brand-mark" aria-hidden="true">
3338 </span>
34-<span class="brand-name">example.test</span>
3539 </a>
3640 <nav class="crumbs" aria-label="Path">
3741 <ol>
@@ -59,7 +63,6 @@
5963 <span class="search-label">Go to…</span>
6064 <kbd aria-hidden="true">ctrl k</kbd>
6165 </button>
62-<button type="button" class="btn icon" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" aria-label="Keyboard shortcuts" title="keyboard shortcuts (?)">?</button>
6366 </div>
6467 </div>
6568 <nav class="tabs" aria-label="Sections">
@@ -335,8 +338,8 @@
335338 <footer class="foot">
336339 <span>ermite devel</span>
337340 <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a>
338-<span class="foot-keys">
339-<kbd>?</kbd> keyboard shortcuts</span>
341+<button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?">
342+<kbd aria-hidden="true">?</kbd> keyboard shortcuts</button>
340343 </footer>
341344 <div class="hint" role="status" aria-live="polite">
342345 <span class="hint-nav" :text="document.body.dataset.nav || ''">
@@ -5,7 +5,12 @@
5 <meta name="viewport" content="width=device-width, initial-scale=1">5 <meta name="viewport" content="width=device-width, initial-scale=1">
6 <meta name="color-scheme" content="light dark">6 <meta name="color-scheme" content="light dark">
7 <title>SHA7 add evil.go · diffs · example.test</title>7 <title>SHA7 add evil.go · diffs · example.test</title>
8+<meta name="theme-color" media="(prefers-color-scheme: light)" content="#ffffff">
9+<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#0c0c0c">
10+<meta name="apple-mobile-web-app-title" content="ermite">
8 <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg">11 <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg">
12+<link rel="apple-touch-icon" href="/-/static/app/apple-touch-icon.HASH.png">
13+<link rel="manifest" href="/-/static/app/manifest.HASH.webmanifest">
9 <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin>14 <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin>
10 <link rel="stylesheet" href="/-/static/css/ermite.HASH.css">15 <link rel="stylesheet" href="/-/static/css/ermite.HASH.css">
11 <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'>16 <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'>
@@ -28,10 +33,9 @@
28 <a class="skip" href="#main">skip to content</a>33 <a class="skip" href="#main">skip to content</a>
29 <header class="hdr">34 <header class="hdr">
30 <div class="hdr-top">35 <div class="hdr-top">
31-<a class="brand" href="/" data-g="h" title="home (g h)">36+<a class="brand" href="/" data-g="h" aria-label="home" title="home (g h)">
32 <span class="brand-mark" aria-hidden="true">37 <span class="brand-mark" aria-hidden="true">
33 </span>38 </span>
34-<span class="brand-name">example.test</span>
35 </a>39 </a>
36 <nav class="crumbs" aria-label="Path">40 <nav class="crumbs" aria-label="Path">
37 <ol>41 <ol>
@@ -59,7 +63,6 @@
59 <span class="search-label">Go to…</span>63 <span class="search-label">Go to…</span>
60 <kbd aria-hidden="true">ctrl k</kbd>64 <kbd aria-hidden="true">ctrl k</kbd>
61 </button>65 </button>
62-<button type="button" class="btn icon" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" aria-label="Keyboard shortcuts" title="keyboard shortcuts (?)">?</button>
63 </div>66 </div>
64 </div>67 </div>
65 <nav class="tabs" aria-label="Sections">68 <nav class="tabs" aria-label="Sections">
@@ -335,8 +338,8 @@
335 <footer class="foot">338 <footer class="foot">
336 <span>ermite devel</span>339 <span>ermite devel</span>
337 <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a>340 <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a>
338-<span class="foot-keys">341+<button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?">
339-<kbd>?</kbd> keyboard shortcuts</span>342+<kbd aria-hidden="true">?</kbd> keyboard shortcuts</button>
340 </footer>343 </footer>
341 <div class="hint" role="status" aria-live="polite">344 <div class="hint" role="status" aria-live="polite">
342 <span class="hint-nav" :text="document.body.dataset.nav || ''">345 <span class="hint-nav" :text="document.body.dataset.nav || ''">
M internal/app/testdata/web/commits-page2.html +8 -5
@@ -5,7 +5,12 @@
55 <meta name="viewport" content="width=device-width, initial-scale=1">
66 <meta name="color-scheme" content="light dark">
77 <title>commits · long · example.test</title>
8+<meta name="theme-color" media="(prefers-color-scheme: light)" content="#ffffff">
9+<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#0c0c0c">
10+<meta name="apple-mobile-web-app-title" content="ermite">
811 <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg">
12+<link rel="apple-touch-icon" href="/-/static/app/apple-touch-icon.HASH.png">
13+<link rel="manifest" href="/-/static/app/manifest.HASH.webmanifest">
914 <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin>
1015 <link rel="stylesheet" href="/-/static/css/ermite.HASH.css">
1116 <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'>
@@ -28,10 +33,9 @@
2833 <a class="skip" href="#main">skip to content</a>
2934 <header class="hdr">
3035 <div class="hdr-top">
31-<a class="brand" href="/" data-g="h" title="home (g h)">
36+<a class="brand" href="/" data-g="h" aria-label="home" title="home (g h)">
3237 <span class="brand-mark" aria-hidden="true">
3338 </span>
34-<span class="brand-name">example.test</span>
3539 </a>
3640 <nav class="crumbs" aria-label="Path">
3741 <ol>
@@ -59,7 +63,6 @@
5963 <span class="search-label">Go to…</span>
6064 <kbd aria-hidden="true">ctrl k</kbd>
6165 </button>
62-<button type="button" class="btn icon" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" aria-label="Keyboard shortcuts" title="keyboard shortcuts (?)">?</button>
6366 </div>
6467 </div>
6568 <nav class="tabs" aria-label="Sections">
@@ -160,8 +163,8 @@
160163 <footer class="foot">
161164 <span>ermite devel</span>
162165 <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a>
163-<span class="foot-keys">
164-<kbd>?</kbd> keyboard shortcuts</span>
166+<button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?">
167+<kbd aria-hidden="true">?</kbd> keyboard shortcuts</button>
165168 </footer>
166169 <div class="hint" role="status" aria-live="polite">
167170 <span class="hint-nav" :text="document.body.dataset.nav || ''">
@@ -5,7 +5,12 @@
5 <meta name="viewport" content="width=device-width, initial-scale=1">5 <meta name="viewport" content="width=device-width, initial-scale=1">
6 <meta name="color-scheme" content="light dark">6 <meta name="color-scheme" content="light dark">
7 <title>commits · long · example.test</title>7 <title>commits · long · example.test</title>
8+<meta name="theme-color" media="(prefers-color-scheme: light)" content="#ffffff">
9+<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#0c0c0c">
10+<meta name="apple-mobile-web-app-title" content="ermite">
8 <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg">11 <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg">
12+<link rel="apple-touch-icon" href="/-/static/app/apple-touch-icon.HASH.png">
13+<link rel="manifest" href="/-/static/app/manifest.HASH.webmanifest">
9 <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin>14 <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin>
10 <link rel="stylesheet" href="/-/static/css/ermite.HASH.css">15 <link rel="stylesheet" href="/-/static/css/ermite.HASH.css">
11 <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'>16 <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'>
@@ -28,10 +33,9 @@
28 <a class="skip" href="#main">skip to content</a>33 <a class="skip" href="#main">skip to content</a>
29 <header class="hdr">34 <header class="hdr">
30 <div class="hdr-top">35 <div class="hdr-top">
31-<a class="brand" href="/" data-g="h" title="home (g h)">36+<a class="brand" href="/" data-g="h" aria-label="home" title="home (g h)">
32 <span class="brand-mark" aria-hidden="true">37 <span class="brand-mark" aria-hidden="true">
33 </span>38 </span>
34-<span class="brand-name">example.test</span>
35 </a>39 </a>
36 <nav class="crumbs" aria-label="Path">40 <nav class="crumbs" aria-label="Path">
37 <ol>41 <ol>
@@ -59,7 +63,6 @@
59 <span class="search-label">Go to…</span>63 <span class="search-label">Go to…</span>
60 <kbd aria-hidden="true">ctrl k</kbd>64 <kbd aria-hidden="true">ctrl k</kbd>
61 </button>65 </button>
62-<button type="button" class="btn icon" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" aria-label="Keyboard shortcuts" title="keyboard shortcuts (?)">?</button>
63 </div>66 </div>
64 </div>67 </div>
65 <nav class="tabs" aria-label="Sections">68 <nav class="tabs" aria-label="Sections">
@@ -160,8 +163,8 @@
160 <footer class="foot">163 <footer class="foot">
161 <span>ermite devel</span>164 <span>ermite devel</span>
162 <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a>165 <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a>
163-<span class="foot-keys">166+<button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?">
164-<kbd>?</kbd> keyboard shortcuts</span>167+<kbd aria-hidden="true">?</kbd> keyboard shortcuts</button>
165 </footer>168 </footer>
166 <div class="hint" role="status" aria-live="polite">169 <div class="hint" role="status" aria-live="polite">
167 <span class="hint-nav" :text="document.body.dataset.nav || ''">170 <span class="hint-nav" :text="document.body.dataset.nav || ''">
M internal/app/testdata/web/commits-path.html +8 -5
@@ -5,7 +5,12 @@
55 <meta name="viewport" content="width=device-width, initial-scale=1">
66 <meta name="color-scheme" content="light dark">
77 <title>history of a.txt · long · example.test</title>
8+<meta name="theme-color" media="(prefers-color-scheme: light)" content="#ffffff">
9+<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#0c0c0c">
10+<meta name="apple-mobile-web-app-title" content="ermite">
811 <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg">
12+<link rel="apple-touch-icon" href="/-/static/app/apple-touch-icon.HASH.png">
13+<link rel="manifest" href="/-/static/app/manifest.HASH.webmanifest">
914 <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin>
1015 <link rel="stylesheet" href="/-/static/css/ermite.HASH.css">
1116 <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'>
@@ -28,10 +33,9 @@
2833 <a class="skip" href="#main">skip to content</a>
2934 <header class="hdr">
3035 <div class="hdr-top">
31-<a class="brand" href="/" data-g="h" title="home (g h)">
36+<a class="brand" href="/" data-g="h" aria-label="home" title="home (g h)">
3237 <span class="brand-mark" aria-hidden="true">
3338 </span>
34-<span class="brand-name">example.test</span>
3539 </a>
3640 <nav class="crumbs" aria-label="Path">
3741 <ol>
@@ -62,7 +66,6 @@
6266 <span class="search-label">Go to…</span>
6367 <kbd aria-hidden="true">ctrl k</kbd>
6468 </button>
65-<button type="button" class="btn icon" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" aria-label="Keyboard shortcuts" title="keyboard shortcuts (?)">?</button>
6669 </div>
6770 </div>
6871 <nav class="tabs" aria-label="Sections">
@@ -175,8 +178,8 @@
175178 <footer class="foot">
176179 <span>ermite devel</span>
177180 <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a>
178-<span class="foot-keys">
179-<kbd>?</kbd> keyboard shortcuts</span>
181+<button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?">
182+<kbd aria-hidden="true">?</kbd> keyboard shortcuts</button>
180183 </footer>
181184 <div class="hint" role="status" aria-live="polite">
182185 <span class="hint-nav" :text="document.body.dataset.nav || ''">
@@ -5,7 +5,12 @@
5 <meta name="viewport" content="width=device-width, initial-scale=1">5 <meta name="viewport" content="width=device-width, initial-scale=1">
6 <meta name="color-scheme" content="light dark">6 <meta name="color-scheme" content="light dark">
7 <title>history of a.txt · long · example.test</title>7 <title>history of a.txt · long · example.test</title>
8+<meta name="theme-color" media="(prefers-color-scheme: light)" content="#ffffff">
9+<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#0c0c0c">
10+<meta name="apple-mobile-web-app-title" content="ermite">
8 <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg">11 <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg">
12+<link rel="apple-touch-icon" href="/-/static/app/apple-touch-icon.HASH.png">
13+<link rel="manifest" href="/-/static/app/manifest.HASH.webmanifest">
9 <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin>14 <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin>
10 <link rel="stylesheet" href="/-/static/css/ermite.HASH.css">15 <link rel="stylesheet" href="/-/static/css/ermite.HASH.css">
11 <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'>16 <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'>
@@ -28,10 +33,9 @@
28 <a class="skip" href="#main">skip to content</a>33 <a class="skip" href="#main">skip to content</a>
29 <header class="hdr">34 <header class="hdr">
30 <div class="hdr-top">35 <div class="hdr-top">
31-<a class="brand" href="/" data-g="h" title="home (g h)">36+<a class="brand" href="/" data-g="h" aria-label="home" title="home (g h)">
32 <span class="brand-mark" aria-hidden="true">37 <span class="brand-mark" aria-hidden="true">
33 </span>38 </span>
34-<span class="brand-name">example.test</span>
35 </a>39 </a>
36 <nav class="crumbs" aria-label="Path">40 <nav class="crumbs" aria-label="Path">
37 <ol>41 <ol>
@@ -62,7 +66,6 @@
62 <span class="search-label">Go to…</span>66 <span class="search-label">Go to…</span>
63 <kbd aria-hidden="true">ctrl k</kbd>67 <kbd aria-hidden="true">ctrl k</kbd>
64 </button>68 </button>
65-<button type="button" class="btn icon" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" aria-label="Keyboard shortcuts" title="keyboard shortcuts (?)">?</button>
66 </div>69 </div>
67 </div>70 </div>
68 <nav class="tabs" aria-label="Sections">71 <nav class="tabs" aria-label="Sections">
@@ -175,8 +178,8 @@
175 <footer class="foot">178 <footer class="foot">
176 <span>ermite devel</span>179 <span>ermite devel</span>
177 <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a>180 <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a>
178-<span class="foot-keys">181+<button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?">
179-<kbd>?</kbd> keyboard shortcuts</span>182+<kbd aria-hidden="true">?</kbd> keyboard shortcuts</button>
180 </footer>183 </footer>
181 <div class="hint" role="status" aria-live="polite">184 <div class="hint" role="status" aria-live="polite">
182 <span class="hint-nav" :text="document.body.dataset.nav || ''">185 <span class="hint-nav" :text="document.body.dataset.nav || ''">
M internal/app/testdata/web/commits-root.html +10 -7
@@ -5,7 +5,12 @@
55 <meta name="viewport" content="width=device-width, initial-scale=1">
66 <meta name="color-scheme" content="light dark">
77 <title>commits · long · example.test</title>
8+<meta name="theme-color" media="(prefers-color-scheme: light)" content="#ffffff">
9+<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#0c0c0c">
10+<meta name="apple-mobile-web-app-title" content="ermite">
811 <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg">
12+<link rel="apple-touch-icon" href="/-/static/app/apple-touch-icon.HASH.png">
13+<link rel="manifest" href="/-/static/app/manifest.HASH.webmanifest">
914 <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin>
1015 <link rel="stylesheet" href="/-/static/css/ermite.HASH.css">
1116 <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'>
@@ -28,10 +33,9 @@
2833 <a class="skip" href="#main">skip to content</a>
2934 <header class="hdr">
3035 <div class="hdr-top">
31-<a class="brand" href="/" data-g="h" title="home (g h)">
36+<a class="brand" href="/" data-g="h" aria-label="home" title="home (g h)">
3237 <span class="brand-mark" aria-hidden="true">
3338 </span>
34-<span class="brand-name">example.test</span>
3539 </a>
3640 <nav class="crumbs" aria-label="Path">
3741 <ol>
@@ -59,7 +63,6 @@
5963 <span class="search-label">Go to…</span>
6064 <kbd aria-hidden="true">ctrl k</kbd>
6165 </button>
62-<button type="button" class="btn icon" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" aria-label="Keyboard shortcuts" title="keyboard shortcuts (?)">?</button>
6366 </div>
6467 </div>
6568 <nav class="tabs" aria-label="Sections">
@@ -1053,8 +1056,8 @@
10531056 <footer class="foot">
10541057 <span>ermite devel</span>
10551058 <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a>
1056-<span class="foot-keys">
1057-<kbd>?</kbd> keyboard shortcuts</span>
1059+<button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?">
1060+<kbd aria-hidden="true">?</kbd> keyboard shortcuts</button>
10581061 </footer>
10591062 <div class="hint" role="status" aria-live="polite">
10601063 <span class="hint-nav" :text="document.body.dataset.nav || ''">
@@ -1063,8 +1066,8 @@
10631066 </span>
10641067 <span class="hint-flash" :text="document.body.dataset.flash || ''">
10651068 </span>
1066-<span class="hint-live" data-state="connecting" hx-sse:connect="/-/events?topic=repo%3Along&amp;view=commits&amp;ref=main&amp;head=SHA40" hx-swap="none" :text="data.state === 'offline' ? 'live updates paused' : ''" hx-on="htmx:sse:after:connection -> data.state = 'live';
1067- htmx:sse:close, htmx:sse:error -> data.state = 'offline';
1069+<span class="hint-live" data-state="connecting" hx-sse:connect="/-/events?topic=repo%3Along&amp;view=commits&amp;ref=main&amp;head=SHA40" hx-swap="none" :text="data.state === 'offline' ? 'live updates paused' : ''" hx-on="htmx:sse:after:connection -> ermiteLive.back();
1070+ htmx:sse:close, htmx:sse:error -> ermiteLive.lost();
10681071 htmx:sse:error -> ermiteLive.error(event.detail.status);
10691072 htmx:sse:before:connection -> ermiteLive.gate(event);
10701073 home-list, repo-last-push, refs-list, commits-head, mr-notice, ci-run, ci-job -> ermiteLive.swap(event.detail.data);
@@ -5,7 +5,12 @@
5 <meta name="viewport" content="width=device-width, initial-scale=1">5 <meta name="viewport" content="width=device-width, initial-scale=1">
6 <meta name="color-scheme" content="light dark">6 <meta name="color-scheme" content="light dark">
7 <title>commits · long · example.test</title>7 <title>commits · long · example.test</title>
8+<meta name="theme-color" media="(prefers-color-scheme: light)" content="#ffffff">
9+<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#0c0c0c">
10+<meta name="apple-mobile-web-app-title" content="ermite">
8 <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg">11 <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg">
12+<link rel="apple-touch-icon" href="/-/static/app/apple-touch-icon.HASH.png">
13+<link rel="manifest" href="/-/static/app/manifest.HASH.webmanifest">
9 <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin>14 <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin>
10 <link rel="stylesheet" href="/-/static/css/ermite.HASH.css">15 <link rel="stylesheet" href="/-/static/css/ermite.HASH.css">
11 <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'>16 <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'>
@@ -28,10 +33,9 @@
28 <a class="skip" href="#main">skip to content</a>33 <a class="skip" href="#main">skip to content</a>
29 <header class="hdr">34 <header class="hdr">
30 <div class="hdr-top">35 <div class="hdr-top">
31-<a class="brand" href="/" data-g="h" title="home (g h)">36+<a class="brand" href="/" data-g="h" aria-label="home" title="home (g h)">
32 <span class="brand-mark" aria-hidden="true">37 <span class="brand-mark" aria-hidden="true">
33 </span>38 </span>
34-<span class="brand-name">example.test</span>
35 </a>39 </a>
36 <nav class="crumbs" aria-label="Path">40 <nav class="crumbs" aria-label="Path">
37 <ol>41 <ol>
@@ -59,7 +63,6 @@
59 <span class="search-label">Go to…</span>63 <span class="search-label">Go to…</span>
60 <kbd aria-hidden="true">ctrl k</kbd>64 <kbd aria-hidden="true">ctrl k</kbd>
61 </button>65 </button>
62-<button type="button" class="btn icon" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" aria-label="Keyboard shortcuts" title="keyboard shortcuts (?)">?</button>
63 </div>66 </div>
64 </div>67 </div>
65 <nav class="tabs" aria-label="Sections">68 <nav class="tabs" aria-label="Sections">
@@ -1053,8 +1056,8 @@
1053 <footer class="foot">1056 <footer class="foot">
1054 <span>ermite devel</span>1057 <span>ermite devel</span>
1055 <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a>1058 <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a>
1056-<span class="foot-keys">1059+<button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?">
1057-<kbd>?</kbd> keyboard shortcuts</span>1060+<kbd aria-hidden="true">?</kbd> keyboard shortcuts</button>
1058 </footer>1061 </footer>
1059 <div class="hint" role="status" aria-live="polite">1062 <div class="hint" role="status" aria-live="polite">
1060 <span class="hint-nav" :text="document.body.dataset.nav || ''">1063 <span class="hint-nav" :text="document.body.dataset.nav || ''">
@@ -1063,8 +1066,8 @@
1063 </span>1066 </span>
1064 <span class="hint-flash" :text="document.body.dataset.flash || ''">1067 <span class="hint-flash" :text="document.body.dataset.flash || ''">
1065 </span>1068 </span>
1066-<span class="hint-live" data-state="connecting" hx-sse:connect="/-/events?topic=repo%3Along&amp;view=commits&amp;ref=main&amp;head=SHA40" hx-swap="none" :text="data.state === 'offline' ? 'live updates paused' : ''" hx-on="htmx:sse:after:connection -> data.state = 'live';1069+<span class="hint-live" data-state="connecting" hx-sse:connect="/-/events?topic=repo%3Along&amp;view=commits&amp;ref=main&amp;head=SHA40" hx-swap="none" :text="data.state === 'offline' ? 'live updates paused' : ''" hx-on="htmx:sse:after:connection -> ermiteLive.back();
1067- htmx:sse:close, htmx:sse:error -> data.state = 'offline';1070+ htmx:sse:close, htmx:sse:error -> ermiteLive.lost();
1068 htmx:sse:error -> ermiteLive.error(event.detail.status);1071 htmx:sse:error -> ermiteLive.error(event.detail.status);
1069 htmx:sse:before:connection -> ermiteLive.gate(event);1072 htmx:sse:before:connection -> ermiteLive.gate(event);
1070 home-list, repo-last-push, refs-list, commits-head, mr-notice, ci-run, ci-job -> ermiteLive.swap(event.detail.data);1073 home-list, repo-last-push, refs-list, commits-head, mr-notice, ci-run, ci-job -> ermiteLive.swap(event.detail.data);
M internal/app/testdata/web/commits-small.html +10 -7
@@ -5,7 +5,12 @@
55 <meta name="viewport" content="width=device-width, initial-scale=1">
66 <meta name="color-scheme" content="light dark">
77 <title>commits · diffs · example.test</title>
8+<meta name="theme-color" media="(prefers-color-scheme: light)" content="#ffffff">
9+<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#0c0c0c">
10+<meta name="apple-mobile-web-app-title" content="ermite">
811 <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg">
12+<link rel="apple-touch-icon" href="/-/static/app/apple-touch-icon.HASH.png">
13+<link rel="manifest" href="/-/static/app/manifest.HASH.webmanifest">
914 <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin>
1015 <link rel="stylesheet" href="/-/static/css/ermite.HASH.css">
1116 <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'>
@@ -28,10 +33,9 @@
2833 <a class="skip" href="#main">skip to content</a>
2934 <header class="hdr">
3035 <div class="hdr-top">
31-<a class="brand" href="/" data-g="h" title="home (g h)">
36+<a class="brand" href="/" data-g="h" aria-label="home" title="home (g h)">
3237 <span class="brand-mark" aria-hidden="true">
3338 </span>
34-<span class="brand-name">example.test</span>
3539 </a>
3640 <nav class="crumbs" aria-label="Path">
3741 <ol>
@@ -59,7 +63,6 @@
5963 <span class="search-label">Go to…</span>
6064 <kbd aria-hidden="true">ctrl k</kbd>
6165 </button>
62-<button type="button" class="btn icon" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" aria-label="Keyboard shortcuts" title="keyboard shortcuts (?)">?</button>
6366 </div>
6467 </div>
6568 <nav class="tabs" aria-label="Sections">
@@ -314,8 +317,8 @@
314317 <footer class="foot">
315318 <span>ermite devel</span>
316319 <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a>
317-<span class="foot-keys">
318-<kbd>?</kbd> keyboard shortcuts</span>
320+<button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?">
321+<kbd aria-hidden="true">?</kbd> keyboard shortcuts</button>
319322 </footer>
320323 <div class="hint" role="status" aria-live="polite">
321324 <span class="hint-nav" :text="document.body.dataset.nav || ''">
@@ -324,8 +327,8 @@
324327 </span>
325328 <span class="hint-flash" :text="document.body.dataset.flash || ''">
326329 </span>
327-<span class="hint-live" data-state="connecting" hx-sse:connect="/-/events?topic=repo%3Adiffs&amp;view=commits&amp;ref=main&amp;head=SHA40" hx-swap="none" :text="data.state === 'offline' ? 'live updates paused' : ''" hx-on="htmx:sse:after:connection -> data.state = 'live';
328- htmx:sse:close, htmx:sse:error -> data.state = 'offline';
330+<span class="hint-live" data-state="connecting" hx-sse:connect="/-/events?topic=repo%3Adiffs&amp;view=commits&amp;ref=main&amp;head=SHA40" hx-swap="none" :text="data.state === 'offline' ? 'live updates paused' : ''" hx-on="htmx:sse:after:connection -> ermiteLive.back();
331+ htmx:sse:close, htmx:sse:error -> ermiteLive.lost();
329332 htmx:sse:error -> ermiteLive.error(event.detail.status);
330333 htmx:sse:before:connection -> ermiteLive.gate(event);
331334 home-list, repo-last-push, refs-list, commits-head, mr-notice, ci-run, ci-job -> ermiteLive.swap(event.detail.data);
@@ -5,7 +5,12 @@
5 <meta name="viewport" content="width=device-width, initial-scale=1">5 <meta name="viewport" content="width=device-width, initial-scale=1">
6 <meta name="color-scheme" content="light dark">6 <meta name="color-scheme" content="light dark">
7 <title>commits · diffs · example.test</title>7 <title>commits · diffs · example.test</title>
8+<meta name="theme-color" media="(prefers-color-scheme: light)" content="#ffffff">
9+<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#0c0c0c">
10+<meta name="apple-mobile-web-app-title" content="ermite">
8 <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg">11 <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg">
12+<link rel="apple-touch-icon" href="/-/static/app/apple-touch-icon.HASH.png">
13+<link rel="manifest" href="/-/static/app/manifest.HASH.webmanifest">
9 <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin>14 <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin>
10 <link rel="stylesheet" href="/-/static/css/ermite.HASH.css">15 <link rel="stylesheet" href="/-/static/css/ermite.HASH.css">
11 <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'>16 <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'>
@@ -28,10 +33,9 @@
28 <a class="skip" href="#main">skip to content</a>33 <a class="skip" href="#main">skip to content</a>
29 <header class="hdr">34 <header class="hdr">
30 <div class="hdr-top">35 <div class="hdr-top">
31-<a class="brand" href="/" data-g="h" title="home (g h)">36+<a class="brand" href="/" data-g="h" aria-label="home" title="home (g h)">
32 <span class="brand-mark" aria-hidden="true">37 <span class="brand-mark" aria-hidden="true">
33 </span>38 </span>
34-<span class="brand-name">example.test</span>
35 </a>39 </a>
36 <nav class="crumbs" aria-label="Path">40 <nav class="crumbs" aria-label="Path">
37 <ol>41 <ol>
@@ -59,7 +63,6 @@
59 <span class="search-label">Go to…</span>63 <span class="search-label">Go to…</span>
60 <kbd aria-hidden="true">ctrl k</kbd>64 <kbd aria-hidden="true">ctrl k</kbd>
61 </button>65 </button>
62-<button type="button" class="btn icon" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" aria-label="Keyboard shortcuts" title="keyboard shortcuts (?)">?</button>
63 </div>66 </div>
64 </div>67 </div>
65 <nav class="tabs" aria-label="Sections">68 <nav class="tabs" aria-label="Sections">
@@ -314,8 +317,8 @@
314 <footer class="foot">317 <footer class="foot">
315 <span>ermite devel</span>318 <span>ermite devel</span>
316 <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a>319 <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a>
317-<span class="foot-keys">320+<button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?">
318-<kbd>?</kbd> keyboard shortcuts</span>321+<kbd aria-hidden="true">?</kbd> keyboard shortcuts</button>
319 </footer>322 </footer>
320 <div class="hint" role="status" aria-live="polite">323 <div class="hint" role="status" aria-live="polite">
321 <span class="hint-nav" :text="document.body.dataset.nav || ''">324 <span class="hint-nav" :text="document.body.dataset.nav || ''">
@@ -324,8 +327,8 @@
324 </span>327 </span>
325 <span class="hint-flash" :text="document.body.dataset.flash || ''">328 <span class="hint-flash" :text="document.body.dataset.flash || ''">
326 </span>329 </span>
327-<span class="hint-live" data-state="connecting" hx-sse:connect="/-/events?topic=repo%3Adiffs&amp;view=commits&amp;ref=main&amp;head=SHA40" hx-swap="none" :text="data.state === 'offline' ? 'live updates paused' : ''" hx-on="htmx:sse:after:connection -> data.state = 'live';330+<span class="hint-live" data-state="connecting" hx-sse:connect="/-/events?topic=repo%3Adiffs&amp;view=commits&amp;ref=main&amp;head=SHA40" hx-swap="none" :text="data.state === 'offline' ? 'live updates paused' : ''" hx-on="htmx:sse:after:connection -> ermiteLive.back();
328- htmx:sse:close, htmx:sse:error -> data.state = 'offline';331+ htmx:sse:close, htmx:sse:error -> ermiteLive.lost();
329 htmx:sse:error -> ermiteLive.error(event.detail.status);332 htmx:sse:error -> ermiteLive.error(event.detail.status);
330 htmx:sse:before:connection -> ermiteLive.gate(event);333 htmx:sse:before:connection -> ermiteLive.gate(event);
331 home-list, repo-last-push, refs-list, commits-head, mr-notice, ci-run, ci-job -> ermiteLive.swap(event.detail.data);334 home-list, repo-last-push, refs-list, commits-head, mr-notice, ci-run, ci-job -> ermiteLive.swap(event.detail.data);
M internal/app/testdata/web/empty.html +8 -5
@@ -5,7 +5,12 @@
55 <meta name="viewport" content="width=device-width, initial-scale=1">
66 <meta name="color-scheme" content="light dark">
77 <title>quiet · example.test</title>
8+<meta name="theme-color" media="(prefers-color-scheme: light)" content="#ffffff">
9+<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#0c0c0c">
10+<meta name="apple-mobile-web-app-title" content="ermite">
811 <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg">
12+<link rel="apple-touch-icon" href="/-/static/app/apple-touch-icon.HASH.png">
13+<link rel="manifest" href="/-/static/app/manifest.HASH.webmanifest">
914 <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin>
1015 <link rel="stylesheet" href="/-/static/css/ermite.HASH.css">
1116 <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'>
@@ -28,10 +33,9 @@
2833 <a class="skip" href="#main">skip to content</a>
2934 <header class="hdr">
3035 <div class="hdr-top">
31-<a class="brand" href="/" data-g="h" title="home (g h)">
36+<a class="brand" href="/" data-g="h" aria-label="home" title="home (g h)">
3237 <span class="brand-mark" aria-hidden="true">
3338 </span>
34-<span class="brand-name">example.test</span>
3539 </a>
3640 <nav class="crumbs" aria-label="Path">
3741 <ol>
@@ -47,7 +51,6 @@
4751 <span class="search-label">Go to…</span>
4852 <kbd aria-hidden="true">ctrl k</kbd>
4953 </button>
50-<button type="button" class="btn icon" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" aria-label="Keyboard shortcuts" title="keyboard shortcuts (?)">?</button>
5154 </div>
5255 </div>
5356 </header>
@@ -78,8 +81,8 @@
7881 <footer class="foot">
7982 <span>ermite devel</span>
8083 <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a>
81-<span class="foot-keys">
82-<kbd>?</kbd> keyboard shortcuts</span>
84+<button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?">
85+<kbd aria-hidden="true">?</kbd> keyboard shortcuts</button>
8386 </footer>
8487 <div class="hint" role="status" aria-live="polite">
8588 <span class="hint-nav" :text="document.body.dataset.nav || ''">
@@ -5,7 +5,12 @@
5 <meta name="viewport" content="width=device-width, initial-scale=1">5 <meta name="viewport" content="width=device-width, initial-scale=1">
6 <meta name="color-scheme" content="light dark">6 <meta name="color-scheme" content="light dark">
7 <title>quiet · example.test</title>7 <title>quiet · example.test</title>
8+<meta name="theme-color" media="(prefers-color-scheme: light)" content="#ffffff">
9+<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#0c0c0c">
10+<meta name="apple-mobile-web-app-title" content="ermite">
8 <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg">11 <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg">
12+<link rel="apple-touch-icon" href="/-/static/app/apple-touch-icon.HASH.png">
13+<link rel="manifest" href="/-/static/app/manifest.HASH.webmanifest">
9 <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin>14 <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin>
10 <link rel="stylesheet" href="/-/static/css/ermite.HASH.css">15 <link rel="stylesheet" href="/-/static/css/ermite.HASH.css">
11 <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'>16 <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'>
@@ -28,10 +33,9 @@
28 <a class="skip" href="#main">skip to content</a>33 <a class="skip" href="#main">skip to content</a>
29 <header class="hdr">34 <header class="hdr">
30 <div class="hdr-top">35 <div class="hdr-top">
31-<a class="brand" href="/" data-g="h" title="home (g h)">36+<a class="brand" href="/" data-g="h" aria-label="home" title="home (g h)">
32 <span class="brand-mark" aria-hidden="true">37 <span class="brand-mark" aria-hidden="true">
33 </span>38 </span>
34-<span class="brand-name">example.test</span>
35 </a>39 </a>
36 <nav class="crumbs" aria-label="Path">40 <nav class="crumbs" aria-label="Path">
37 <ol>41 <ol>
@@ -47,7 +51,6 @@
47 <span class="search-label">Go to…</span>51 <span class="search-label">Go to…</span>
48 <kbd aria-hidden="true">ctrl k</kbd>52 <kbd aria-hidden="true">ctrl k</kbd>
49 </button>53 </button>
50-<button type="button" class="btn icon" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" aria-label="Keyboard shortcuts" title="keyboard shortcuts (?)">?</button>
51 </div>54 </div>
52 </div>55 </div>
53 </header>56 </header>
@@ -78,8 +81,8 @@
78 <footer class="foot">81 <footer class="foot">
79 <span>ermite devel</span>82 <span>ermite devel</span>
80 <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a>83 <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a>
81-<span class="foot-keys">84+<button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?">
82-<kbd>?</kbd> keyboard shortcuts</span>85+<kbd aria-hidden="true">?</kbd> keyboard shortcuts</button>
83 </footer>86 </footer>
84 <div class="hint" role="status" aria-live="polite">87 <div class="hint" role="status" aria-live="polite">
85 <span class="hint-nav" :text="document.body.dataset.nav || ''">88 <span class="hint-nav" :text="document.body.dataset.nav || ''">
M internal/app/testdata/web/home.html +10 -7
@@ -5,7 +5,12 @@
55 <meta name="viewport" content="width=device-width, initial-scale=1">
66 <meta name="color-scheme" content="light dark">
77 <title>example.test</title>
8+<meta name="theme-color" media="(prefers-color-scheme: light)" content="#ffffff">
9+<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#0c0c0c">
10+<meta name="apple-mobile-web-app-title" content="ermite">
811 <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg">
12+<link rel="apple-touch-icon" href="/-/static/app/apple-touch-icon.HASH.png">
13+<link rel="manifest" href="/-/static/app/manifest.HASH.webmanifest">
914 <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin>
1015 <link rel="stylesheet" href="/-/static/css/ermite.HASH.css">
1116 <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'>
@@ -28,10 +33,9 @@
2833 <a class="skip" href="#main">skip to content</a>
2934 <header class="hdr">
3035 <div class="hdr-top">
31-<a class="brand" href="/" data-g="h" title="home (g h)">
36+<a class="brand" href="/" data-g="h" aria-label="home" title="home (g h)">
3237 <span class="brand-mark" aria-hidden="true">
3338 </span>
34-<span class="brand-name">example.test</span>
3539 </a>
3640 <div class="hdr-acts">
3741 <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">
@@ -40,7 +44,6 @@
4044 <span class="search-label">Go to…</span>
4145 <kbd aria-hidden="true">ctrl k</kbd>
4246 </button>
43-<button type="button" class="btn icon" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" aria-label="Keyboard shortcuts" title="keyboard shortcuts (?)">?</button>
4447 </div>
4548 </div>
4649 </header>
@@ -103,8 +106,8 @@
103106 <footer class="foot">
104107 <span>ermite devel</span>
105108 <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a>
106-<span class="foot-keys">
107-<kbd>?</kbd> keyboard shortcuts</span>
109+<button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?">
110+<kbd aria-hidden="true">?</kbd> keyboard shortcuts</button>
108111 </footer>
109112 <div class="hint" role="status" aria-live="polite">
110113 <span class="hint-nav" :text="document.body.dataset.nav || ''">
@@ -113,8 +116,8 @@
113116 </span>
114117 <span class="hint-flash" :text="document.body.dataset.flash || ''">
115118 </span>
116-<span class="hint-live" data-state="connecting" hx-sse:connect="/-/events?topic=home" hx-swap="none" :text="data.state === 'offline' ? 'live updates paused' : ''" hx-on="htmx:sse:after:connection -> data.state = 'live';
117- htmx:sse:close, htmx:sse:error -> data.state = 'offline';
119+<span class="hint-live" data-state="connecting" hx-sse:connect="/-/events?topic=home" hx-swap="none" :text="data.state === 'offline' ? 'live updates paused' : ''" hx-on="htmx:sse:after:connection -> ermiteLive.back();
120+ htmx:sse:close, htmx:sse:error -> ermiteLive.lost();
118121 htmx:sse:error -> ermiteLive.error(event.detail.status);
119122 htmx:sse:before:connection -> ermiteLive.gate(event);
120123 home-list, repo-last-push, refs-list, commits-head, mr-notice, ci-run, ci-job -> ermiteLive.swap(event.detail.data);
@@ -5,7 +5,12 @@
5 <meta name="viewport" content="width=device-width, initial-scale=1">5 <meta name="viewport" content="width=device-width, initial-scale=1">
6 <meta name="color-scheme" content="light dark">6 <meta name="color-scheme" content="light dark">
7 <title>example.test</title>7 <title>example.test</title>
8+<meta name="theme-color" media="(prefers-color-scheme: light)" content="#ffffff">
9+<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#0c0c0c">
10+<meta name="apple-mobile-web-app-title" content="ermite">
8 <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg">11 <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg">
12+<link rel="apple-touch-icon" href="/-/static/app/apple-touch-icon.HASH.png">
13+<link rel="manifest" href="/-/static/app/manifest.HASH.webmanifest">
9 <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin>14 <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin>
10 <link rel="stylesheet" href="/-/static/css/ermite.HASH.css">15 <link rel="stylesheet" href="/-/static/css/ermite.HASH.css">
11 <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'>16 <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'>
@@ -28,10 +33,9 @@
28 <a class="skip" href="#main">skip to content</a>33 <a class="skip" href="#main">skip to content</a>
29 <header class="hdr">34 <header class="hdr">
30 <div class="hdr-top">35 <div class="hdr-top">
31-<a class="brand" href="/" data-g="h" title="home (g h)">36+<a class="brand" href="/" data-g="h" aria-label="home" title="home (g h)">
32 <span class="brand-mark" aria-hidden="true">37 <span class="brand-mark" aria-hidden="true">
33 </span>38 </span>
34-<span class="brand-name">example.test</span>
35 </a>39 </a>
36 <div class="hdr-acts">40 <div class="hdr-acts">
37 <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">41 <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">
@@ -40,7 +44,6 @@
40 <span class="search-label">Go to…</span>44 <span class="search-label">Go to…</span>
41 <kbd aria-hidden="true">ctrl k</kbd>45 <kbd aria-hidden="true">ctrl k</kbd>
42 </button>46 </button>
43-<button type="button" class="btn icon" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" aria-label="Keyboard shortcuts" title="keyboard shortcuts (?)">?</button>
44 </div>47 </div>
45 </div>48 </div>
46 </header>49 </header>
@@ -103,8 +106,8 @@
103 <footer class="foot">106 <footer class="foot">
104 <span>ermite devel</span>107 <span>ermite devel</span>
105 <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a>108 <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a>
106-<span class="foot-keys">109+<button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?">
107-<kbd>?</kbd> keyboard shortcuts</span>110+<kbd aria-hidden="true">?</kbd> keyboard shortcuts</button>
108 </footer>111 </footer>
109 <div class="hint" role="status" aria-live="polite">112 <div class="hint" role="status" aria-live="polite">
110 <span class="hint-nav" :text="document.body.dataset.nav || ''">113 <span class="hint-nav" :text="document.body.dataset.nav || ''">
@@ -113,8 +116,8 @@
113 </span>116 </span>
114 <span class="hint-flash" :text="document.body.dataset.flash || ''">117 <span class="hint-flash" :text="document.body.dataset.flash || ''">
115 </span>118 </span>
116-<span class="hint-live" data-state="connecting" hx-sse:connect="/-/events?topic=home" hx-swap="none" :text="data.state === 'offline' ? 'live updates paused' : ''" hx-on="htmx:sse:after:connection -> data.state = 'live';119+<span class="hint-live" data-state="connecting" hx-sse:connect="/-/events?topic=home" hx-swap="none" :text="data.state === 'offline' ? 'live updates paused' : ''" hx-on="htmx:sse:after:connection -> ermiteLive.back();
117- htmx:sse:close, htmx:sse:error -> data.state = 'offline';120+ htmx:sse:close, htmx:sse:error -> ermiteLive.lost();
118 htmx:sse:error -> ermiteLive.error(event.detail.status);121 htmx:sse:error -> ermiteLive.error(event.detail.status);
119 htmx:sse:before:connection -> ermiteLive.gate(event);122 htmx:sse:before:connection -> ermiteLive.gate(event);
120 home-list, repo-last-push, refs-list, commits-head, mr-notice, ci-run, ci-job -> ermiteLive.swap(event.detail.data);123 home-list, repo-last-push, refs-list, commits-head, mr-notice, ci-run, ci-job -> ermiteLive.swap(event.detail.data);
M internal/app/testdata/web/not-found-page.html +8 -5
@@ -5,7 +5,12 @@
55 <meta name="viewport" content="width=device-width, initial-scale=1">
66 <meta name="color-scheme" content="light dark">
77 <title>404 · example.test</title>
8+<meta name="theme-color" media="(prefers-color-scheme: light)" content="#ffffff">
9+<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#0c0c0c">
10+<meta name="apple-mobile-web-app-title" content="ermite">
811 <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg">
12+<link rel="apple-touch-icon" href="/-/static/app/apple-touch-icon.HASH.png">
13+<link rel="manifest" href="/-/static/app/manifest.HASH.webmanifest">
914 <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin>
1015 <link rel="stylesheet" href="/-/static/css/ermite.HASH.css">
1116 <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'>
@@ -28,10 +33,9 @@
2833 <a class="skip" href="#main">skip to content</a>
2934 <header class="hdr">
3035 <div class="hdr-top">
31-<a class="brand" href="/" data-g="h" title="home (g h)">
36+<a class="brand" href="/" data-g="h" aria-label="home" title="home (g h)">
3237 <span class="brand-mark" aria-hidden="true">
3338 </span>
34-<span class="brand-name">example.test</span>
3539 </a>
3640 <nav class="crumbs" aria-label="Path">
3741 <ol>
@@ -47,7 +51,6 @@
4751 <span class="search-label">Go to…</span>
4852 <kbd aria-hidden="true">ctrl k</kbd>
4953 </button>
50-<button type="button" class="btn icon" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" aria-label="Keyboard shortcuts" title="keyboard shortcuts (?)">?</button>
5154 </div>
5255 </div>
5356 </header>
@@ -67,8 +70,8 @@
6770 <footer class="foot">
6871 <span>ermite devel</span>
6972 <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a>
70-<span class="foot-keys">
71-<kbd>?</kbd> keyboard shortcuts</span>
73+<button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?">
74+<kbd aria-hidden="true">?</kbd> keyboard shortcuts</button>
7275 </footer>
7376 <div class="hint" role="status" aria-live="polite">
7477 <span class="hint-nav" :text="document.body.dataset.nav || ''">
@@ -5,7 +5,12 @@
5 <meta name="viewport" content="width=device-width, initial-scale=1">5 <meta name="viewport" content="width=device-width, initial-scale=1">
6 <meta name="color-scheme" content="light dark">6 <meta name="color-scheme" content="light dark">
7 <title>404 · example.test</title>7 <title>404 · example.test</title>
8+<meta name="theme-color" media="(prefers-color-scheme: light)" content="#ffffff">
9+<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#0c0c0c">
10+<meta name="apple-mobile-web-app-title" content="ermite">
8 <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg">11 <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg">
12+<link rel="apple-touch-icon" href="/-/static/app/apple-touch-icon.HASH.png">
13+<link rel="manifest" href="/-/static/app/manifest.HASH.webmanifest">
9 <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin>14 <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin>
10 <link rel="stylesheet" href="/-/static/css/ermite.HASH.css">15 <link rel="stylesheet" href="/-/static/css/ermite.HASH.css">
11 <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'>16 <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'>
@@ -28,10 +33,9 @@
28 <a class="skip" href="#main">skip to content</a>33 <a class="skip" href="#main">skip to content</a>
29 <header class="hdr">34 <header class="hdr">
30 <div class="hdr-top">35 <div class="hdr-top">
31-<a class="brand" href="/" data-g="h" title="home (g h)">36+<a class="brand" href="/" data-g="h" aria-label="home" title="home (g h)">
32 <span class="brand-mark" aria-hidden="true">37 <span class="brand-mark" aria-hidden="true">
33 </span>38 </span>
34-<span class="brand-name">example.test</span>
35 </a>39 </a>
36 <nav class="crumbs" aria-label="Path">40 <nav class="crumbs" aria-label="Path">
37 <ol>41 <ol>
@@ -47,7 +51,6 @@
47 <span class="search-label">Go to…</span>51 <span class="search-label">Go to…</span>
48 <kbd aria-hidden="true">ctrl k</kbd>52 <kbd aria-hidden="true">ctrl k</kbd>
49 </button>53 </button>
50-<button type="button" class="btn icon" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" aria-label="Keyboard shortcuts" title="keyboard shortcuts (?)">?</button>
51 </div>54 </div>
52 </div>55 </div>
53 </header>56 </header>
@@ -67,8 +70,8 @@
67 <footer class="foot">70 <footer class="foot">
68 <span>ermite devel</span>71 <span>ermite devel</span>
69 <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a>72 <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a>
70-<span class="foot-keys">73+<button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?">
71-<kbd>?</kbd> keyboard shortcuts</span>74+<kbd aria-hidden="true">?</kbd> keyboard shortcuts</button>
72 </footer>75 </footer>
73 <div class="hint" role="status" aria-live="polite">76 <div class="hint" role="status" aria-live="polite">
74 <span class="hint-nav" :text="document.body.dataset.nav || ''">77 <span class="hint-nav" :text="document.body.dataset.nav || ''">
M internal/app/testdata/web/not-found.html +8 -5
@@ -5,7 +5,12 @@
55 <meta name="viewport" content="width=device-width, initial-scale=1">
66 <meta name="color-scheme" content="light dark">
77 <title>404 · example.test</title>
8+<meta name="theme-color" media="(prefers-color-scheme: light)" content="#ffffff">
9+<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#0c0c0c">
10+<meta name="apple-mobile-web-app-title" content="ermite">
811 <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg">
12+<link rel="apple-touch-icon" href="/-/static/app/apple-touch-icon.HASH.png">
13+<link rel="manifest" href="/-/static/app/manifest.HASH.webmanifest">
914 <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin>
1015 <link rel="stylesheet" href="/-/static/css/ermite.HASH.css">
1116 <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'>
@@ -28,10 +33,9 @@
2833 <a class="skip" href="#main">skip to content</a>
2934 <header class="hdr">
3035 <div class="hdr-top">
31-<a class="brand" href="/" data-g="h" title="home (g h)">
36+<a class="brand" href="/" data-g="h" aria-label="home" title="home (g h)">
3237 <span class="brand-mark" aria-hidden="true">
3338 </span>
34-<span class="brand-name">example.test</span>
3539 </a>
3640 <nav class="crumbs" aria-label="Path">
3741 <ol>
@@ -47,7 +51,6 @@
4751 <span class="search-label">Go to…</span>
4852 <kbd aria-hidden="true">ctrl k</kbd>
4953 </button>
50-<button type="button" class="btn icon" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" aria-label="Keyboard shortcuts" title="keyboard shortcuts (?)">?</button>
5154 </div>
5255 </div>
5356 </header>
@@ -67,8 +70,8 @@
6770 <footer class="foot">
6871 <span>ermite devel</span>
6972 <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a>
70-<span class="foot-keys">
71-<kbd>?</kbd> keyboard shortcuts</span>
73+<button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?">
74+<kbd aria-hidden="true">?</kbd> keyboard shortcuts</button>
7275 </footer>
7376 <div class="hint" role="status" aria-live="polite">
7477 <span class="hint-nav" :text="document.body.dataset.nav || ''">
@@ -5,7 +5,12 @@
5 <meta name="viewport" content="width=device-width, initial-scale=1">5 <meta name="viewport" content="width=device-width, initial-scale=1">
6 <meta name="color-scheme" content="light dark">6 <meta name="color-scheme" content="light dark">
7 <title>404 · example.test</title>7 <title>404 · example.test</title>
8+<meta name="theme-color" media="(prefers-color-scheme: light)" content="#ffffff">
9+<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#0c0c0c">
10+<meta name="apple-mobile-web-app-title" content="ermite">
8 <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg">11 <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg">
12+<link rel="apple-touch-icon" href="/-/static/app/apple-touch-icon.HASH.png">
13+<link rel="manifest" href="/-/static/app/manifest.HASH.webmanifest">
9 <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin>14 <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin>
10 <link rel="stylesheet" href="/-/static/css/ermite.HASH.css">15 <link rel="stylesheet" href="/-/static/css/ermite.HASH.css">
11 <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'>16 <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'>
@@ -28,10 +33,9 @@
28 <a class="skip" href="#main">skip to content</a>33 <a class="skip" href="#main">skip to content</a>
29 <header class="hdr">34 <header class="hdr">
30 <div class="hdr-top">35 <div class="hdr-top">
31-<a class="brand" href="/" data-g="h" title="home (g h)">36+<a class="brand" href="/" data-g="h" aria-label="home" title="home (g h)">
32 <span class="brand-mark" aria-hidden="true">37 <span class="brand-mark" aria-hidden="true">
33 </span>38 </span>
34-<span class="brand-name">example.test</span>
35 </a>39 </a>
36 <nav class="crumbs" aria-label="Path">40 <nav class="crumbs" aria-label="Path">
37 <ol>41 <ol>
@@ -47,7 +51,6 @@
47 <span class="search-label">Go to…</span>51 <span class="search-label">Go to…</span>
48 <kbd aria-hidden="true">ctrl k</kbd>52 <kbd aria-hidden="true">ctrl k</kbd>
49 </button>53 </button>
50-<button type="button" class="btn icon" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" aria-label="Keyboard shortcuts" title="keyboard shortcuts (?)">?</button>
51 </div>54 </div>
52 </div>55 </div>
53 </header>56 </header>
@@ -67,8 +70,8 @@
67 <footer class="foot">70 <footer class="foot">
68 <span>ermite devel</span>71 <span>ermite devel</span>
69 <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a>72 <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a>
70-<span class="foot-keys">73+<button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?">
71-<kbd>?</kbd> keyboard shortcuts</span>74+<kbd aria-hidden="true">?</kbd> keyboard shortcuts</button>
72 </footer>75 </footer>
73 <div class="hint" role="status" aria-live="polite">76 <div class="hint" role="status" aria-live="polite">
74 <span class="hint-nav" :text="document.body.dataset.nav || ''">77 <span class="hint-nav" :text="document.body.dataset.nav || ''">
M internal/app/testdata/web/refs.html +10 -7
@@ -5,7 +5,12 @@
55 <meta name="viewport" content="width=device-width, initial-scale=1">
66 <meta name="color-scheme" content="light dark">
77 <title>refs · demo · example.test</title>
8+<meta name="theme-color" media="(prefers-color-scheme: light)" content="#ffffff">
9+<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#0c0c0c">
10+<meta name="apple-mobile-web-app-title" content="ermite">
811 <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg">
12+<link rel="apple-touch-icon" href="/-/static/app/apple-touch-icon.HASH.png">
13+<link rel="manifest" href="/-/static/app/manifest.HASH.webmanifest">
914 <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin>
1015 <link rel="stylesheet" href="/-/static/css/ermite.HASH.css">
1116 <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'>
@@ -28,10 +33,9 @@
2833 <a class="skip" href="#main">skip to content</a>
2934 <header class="hdr">
3035 <div class="hdr-top">
31-<a class="brand" href="/" data-g="h" title="home (g h)">
36+<a class="brand" href="/" data-g="h" aria-label="home" title="home (g h)">
3237 <span class="brand-mark" aria-hidden="true">
3338 </span>
34-<span class="brand-name">example.test</span>
3539 </a>
3640 <nav class="crumbs" aria-label="Path">
3741 <ol>
@@ -56,7 +60,6 @@
5660 <span class="search-label">Go to…</span>
5761 <kbd aria-hidden="true">ctrl k</kbd>
5862 </button>
59-<button type="button" class="btn icon" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" aria-label="Keyboard shortcuts" title="keyboard shortcuts (?)">?</button>
6063 </div>
6164 </div>
6265 <nav class="tabs" aria-label="Sections">
@@ -190,8 +193,8 @@
190193 <footer class="foot">
191194 <span>ermite devel</span>
192195 <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a>
193-<span class="foot-keys">
194-<kbd>?</kbd> keyboard shortcuts</span>
196+<button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?">
197+<kbd aria-hidden="true">?</kbd> keyboard shortcuts</button>
195198 </footer>
196199 <div class="hint" role="status" aria-live="polite">
197200 <span class="hint-nav" :text="document.body.dataset.nav || ''">
@@ -200,8 +203,8 @@
200203 </span>
201204 <span class="hint-flash" :text="document.body.dataset.flash || ''">
202205 </span>
203-<span class="hint-live" data-state="connecting" hx-sse:connect="/-/events?topic=repo%3Ademo&amp;view=refs" hx-swap="none" :text="data.state === 'offline' ? 'live updates paused' : ''" hx-on="htmx:sse:after:connection -> data.state = 'live';
204- htmx:sse:close, htmx:sse:error -> data.state = 'offline';
206+<span class="hint-live" data-state="connecting" hx-sse:connect="/-/events?topic=repo%3Ademo&amp;view=refs" hx-swap="none" :text="data.state === 'offline' ? 'live updates paused' : ''" hx-on="htmx:sse:after:connection -> ermiteLive.back();
207+ htmx:sse:close, htmx:sse:error -> ermiteLive.lost();
205208 htmx:sse:error -> ermiteLive.error(event.detail.status);
206209 htmx:sse:before:connection -> ermiteLive.gate(event);
207210 home-list, repo-last-push, refs-list, commits-head, mr-notice, ci-run, ci-job -> ermiteLive.swap(event.detail.data);
@@ -5,7 +5,12 @@
5 <meta name="viewport" content="width=device-width, initial-scale=1">5 <meta name="viewport" content="width=device-width, initial-scale=1">
6 <meta name="color-scheme" content="light dark">6 <meta name="color-scheme" content="light dark">
7 <title>refs · demo · example.test</title>7 <title>refs · demo · example.test</title>
8+<meta name="theme-color" media="(prefers-color-scheme: light)" content="#ffffff">
9+<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#0c0c0c">
10+<meta name="apple-mobile-web-app-title" content="ermite">
8 <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg">11 <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg">
12+<link rel="apple-touch-icon" href="/-/static/app/apple-touch-icon.HASH.png">
13+<link rel="manifest" href="/-/static/app/manifest.HASH.webmanifest">
9 <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin>14 <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin>
10 <link rel="stylesheet" href="/-/static/css/ermite.HASH.css">15 <link rel="stylesheet" href="/-/static/css/ermite.HASH.css">
11 <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'>16 <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'>
@@ -28,10 +33,9 @@
28 <a class="skip" href="#main">skip to content</a>33 <a class="skip" href="#main">skip to content</a>
29 <header class="hdr">34 <header class="hdr">
30 <div class="hdr-top">35 <div class="hdr-top">
31-<a class="brand" href="/" data-g="h" title="home (g h)">36+<a class="brand" href="/" data-g="h" aria-label="home" title="home (g h)">
32 <span class="brand-mark" aria-hidden="true">37 <span class="brand-mark" aria-hidden="true">
33 </span>38 </span>
34-<span class="brand-name">example.test</span>
35 </a>39 </a>
36 <nav class="crumbs" aria-label="Path">40 <nav class="crumbs" aria-label="Path">
37 <ol>41 <ol>
@@ -56,7 +60,6 @@
56 <span class="search-label">Go to…</span>60 <span class="search-label">Go to…</span>
57 <kbd aria-hidden="true">ctrl k</kbd>61 <kbd aria-hidden="true">ctrl k</kbd>
58 </button>62 </button>
59-<button type="button" class="btn icon" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" aria-label="Keyboard shortcuts" title="keyboard shortcuts (?)">?</button>
60 </div>63 </div>
61 </div>64 </div>
62 <nav class="tabs" aria-label="Sections">65 <nav class="tabs" aria-label="Sections">
@@ -190,8 +193,8 @@
190 <footer class="foot">193 <footer class="foot">
191 <span>ermite devel</span>194 <span>ermite devel</span>
192 <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a>195 <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a>
193-<span class="foot-keys">196+<button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?">
194-<kbd>?</kbd> keyboard shortcuts</span>197+<kbd aria-hidden="true">?</kbd> keyboard shortcuts</button>
195 </footer>198 </footer>
196 <div class="hint" role="status" aria-live="polite">199 <div class="hint" role="status" aria-live="polite">
197 <span class="hint-nav" :text="document.body.dataset.nav || ''">200 <span class="hint-nav" :text="document.body.dataset.nav || ''">
@@ -200,8 +203,8 @@
200 </span>203 </span>
201 <span class="hint-flash" :text="document.body.dataset.flash || ''">204 <span class="hint-flash" :text="document.body.dataset.flash || ''">
202 </span>205 </span>
203-<span class="hint-live" data-state="connecting" hx-sse:connect="/-/events?topic=repo%3Ademo&amp;view=refs" hx-swap="none" :text="data.state === 'offline' ? 'live updates paused' : ''" hx-on="htmx:sse:after:connection -> data.state = 'live';206+<span class="hint-live" data-state="connecting" hx-sse:connect="/-/events?topic=repo%3Ademo&amp;view=refs" hx-swap="none" :text="data.state === 'offline' ? 'live updates paused' : ''" hx-on="htmx:sse:after:connection -> ermiteLive.back();
204- htmx:sse:close, htmx:sse:error -> data.state = 'offline';207+ htmx:sse:close, htmx:sse:error -> ermiteLive.lost();
205 htmx:sse:error -> ermiteLive.error(event.detail.status);208 htmx:sse:error -> ermiteLive.error(event.detail.status);
206 htmx:sse:before:connection -> ermiteLive.gate(event);209 htmx:sse:before:connection -> ermiteLive.gate(event);
207 home-list, repo-last-push, refs-list, commits-head, mr-notice, ci-run, ci-job -> ermiteLive.swap(event.detail.data);210 home-list, repo-last-push, refs-list, commits-head, mr-notice, ci-run, ci-job -> ermiteLive.swap(event.detail.data);
M internal/app/testdata/web/summary.html +10 -7
@@ -5,7 +5,12 @@
55 <meta name="viewport" content="width=device-width, initial-scale=1">
66 <meta name="color-scheme" content="light dark">
77 <title>demo · example.test</title>
8+<meta name="theme-color" media="(prefers-color-scheme: light)" content="#ffffff">
9+<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#0c0c0c">
10+<meta name="apple-mobile-web-app-title" content="ermite">
811 <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg">
12+<link rel="apple-touch-icon" href="/-/static/app/apple-touch-icon.HASH.png">
13+<link rel="manifest" href="/-/static/app/manifest.HASH.webmanifest">
914 <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin>
1015 <link rel="stylesheet" href="/-/static/css/ermite.HASH.css">
1116 <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'>
@@ -28,10 +33,9 @@
2833 <a class="skip" href="#main">skip to content</a>
2934 <header class="hdr">
3035 <div class="hdr-top">
31-<a class="brand" href="/" data-g="h" title="home (g h)">
36+<a class="brand" href="/" data-g="h" aria-label="home" title="home (g h)">
3237 <span class="brand-mark" aria-hidden="true">
3338 </span>
34-<span class="brand-name">example.test</span>
3539 </a>
3640 <nav class="crumbs" aria-label="Path">
3741 <ol>
@@ -53,7 +57,6 @@
5357 <span class="search-label">Go to…</span>
5458 <kbd aria-hidden="true">ctrl k</kbd>
5559 </button>
56-<button type="button" class="btn icon" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" aria-label="Keyboard shortcuts" title="keyboard shortcuts (?)">?</button>
5760 </div>
5861 </div>
5962 <nav class="tabs" aria-label="Sections">
@@ -219,8 +222,8 @@
219222 <footer class="foot">
220223 <span>ermite devel</span>
221224 <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a>
222-<span class="foot-keys">
223-<kbd>?</kbd> keyboard shortcuts</span>
225+<button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?">
226+<kbd aria-hidden="true">?</kbd> keyboard shortcuts</button>
224227 </footer>
225228 <div class="hint" role="status" aria-live="polite">
226229 <span class="hint-nav" :text="document.body.dataset.nav || ''">
@@ -229,8 +232,8 @@
229232 </span>
230233 <span class="hint-flash" :text="document.body.dataset.flash || ''">
231234 </span>
232-<span class="hint-live" data-state="connecting" hx-sse:connect="/-/events?topic=repo%3Ademo&amp;view=summary" hx-swap="none" :text="data.state === 'offline' ? 'live updates paused' : ''" hx-on="htmx:sse:after:connection -> data.state = 'live';
233- htmx:sse:close, htmx:sse:error -> data.state = 'offline';
235+<span class="hint-live" data-state="connecting" hx-sse:connect="/-/events?topic=repo%3Ademo&amp;view=summary" hx-swap="none" :text="data.state === 'offline' ? 'live updates paused' : ''" hx-on="htmx:sse:after:connection -> ermiteLive.back();
236+ htmx:sse:close, htmx:sse:error -> ermiteLive.lost();
234237 htmx:sse:error -> ermiteLive.error(event.detail.status);
235238 htmx:sse:before:connection -> ermiteLive.gate(event);
236239 home-list, repo-last-push, refs-list, commits-head, mr-notice, ci-run, ci-job -> ermiteLive.swap(event.detail.data);
@@ -5,7 +5,12 @@
5 <meta name="viewport" content="width=device-width, initial-scale=1">5 <meta name="viewport" content="width=device-width, initial-scale=1">
6 <meta name="color-scheme" content="light dark">6 <meta name="color-scheme" content="light dark">
7 <title>demo · example.test</title>7 <title>demo · example.test</title>
8+<meta name="theme-color" media="(prefers-color-scheme: light)" content="#ffffff">
9+<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#0c0c0c">
10+<meta name="apple-mobile-web-app-title" content="ermite">
8 <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg">11 <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg">
12+<link rel="apple-touch-icon" href="/-/static/app/apple-touch-icon.HASH.png">
13+<link rel="manifest" href="/-/static/app/manifest.HASH.webmanifest">
9 <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin>14 <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin>
10 <link rel="stylesheet" href="/-/static/css/ermite.HASH.css">15 <link rel="stylesheet" href="/-/static/css/ermite.HASH.css">
11 <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'>16 <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'>
@@ -28,10 +33,9 @@
28 <a class="skip" href="#main">skip to content</a>33 <a class="skip" href="#main">skip to content</a>
29 <header class="hdr">34 <header class="hdr">
30 <div class="hdr-top">35 <div class="hdr-top">
31-<a class="brand" href="/" data-g="h" title="home (g h)">36+<a class="brand" href="/" data-g="h" aria-label="home" title="home (g h)">
32 <span class="brand-mark" aria-hidden="true">37 <span class="brand-mark" aria-hidden="true">
33 </span>38 </span>
34-<span class="brand-name">example.test</span>
35 </a>39 </a>
36 <nav class="crumbs" aria-label="Path">40 <nav class="crumbs" aria-label="Path">
37 <ol>41 <ol>
@@ -53,7 +57,6 @@
53 <span class="search-label">Go to…</span>57 <span class="search-label">Go to…</span>
54 <kbd aria-hidden="true">ctrl k</kbd>58 <kbd aria-hidden="true">ctrl k</kbd>
55 </button>59 </button>
56-<button type="button" class="btn icon" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" aria-label="Keyboard shortcuts" title="keyboard shortcuts (?)">?</button>
57 </div>60 </div>
58 </div>61 </div>
59 <nav class="tabs" aria-label="Sections">62 <nav class="tabs" aria-label="Sections">
@@ -219,8 +222,8 @@
219 <footer class="foot">222 <footer class="foot">
220 <span>ermite devel</span>223 <span>ermite devel</span>
221 <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a>224 <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a>
222-<span class="foot-keys">225+<button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?">
223-<kbd>?</kbd> keyboard shortcuts</span>226+<kbd aria-hidden="true">?</kbd> keyboard shortcuts</button>
224 </footer>227 </footer>
225 <div class="hint" role="status" aria-live="polite">228 <div class="hint" role="status" aria-live="polite">
226 <span class="hint-nav" :text="document.body.dataset.nav || ''">229 <span class="hint-nav" :text="document.body.dataset.nav || ''">
@@ -229,8 +232,8 @@
229 </span>232 </span>
230 <span class="hint-flash" :text="document.body.dataset.flash || ''">233 <span class="hint-flash" :text="document.body.dataset.flash || ''">
231 </span>234 </span>
232-<span class="hint-live" data-state="connecting" hx-sse:connect="/-/events?topic=repo%3Ademo&amp;view=summary" hx-swap="none" :text="data.state === 'offline' ? 'live updates paused' : ''" hx-on="htmx:sse:after:connection -> data.state = 'live';235+<span class="hint-live" data-state="connecting" hx-sse:connect="/-/events?topic=repo%3Ademo&amp;view=summary" hx-swap="none" :text="data.state === 'offline' ? 'live updates paused' : ''" hx-on="htmx:sse:after:connection -> ermiteLive.back();
233- htmx:sse:close, htmx:sse:error -> data.state = 'offline';236+ htmx:sse:close, htmx:sse:error -> ermiteLive.lost();
234 htmx:sse:error -> ermiteLive.error(event.detail.status);237 htmx:sse:error -> ermiteLive.error(event.detail.status);
235 htmx:sse:before:connection -> ermiteLive.gate(event);238 htmx:sse:before:connection -> ermiteLive.gate(event);
236 home-list, repo-last-push, refs-list, commits-head, mr-notice, ci-run, ci-job -> ermiteLive.swap(event.detail.data);239 home-list, repo-last-push, refs-list, commits-head, mr-notice, ci-run, ci-job -> ermiteLive.swap(event.detail.data);
M internal/app/testdata/web/tree-nested.html +8 -5
@@ -5,7 +5,12 @@
55 <meta name="viewport" content="width=device-width, initial-scale=1">
66 <meta name="color-scheme" content="light dark">
77 <title>src/lib · files · example.test</title>
8+<meta name="theme-color" media="(prefers-color-scheme: light)" content="#ffffff">
9+<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#0c0c0c">
10+<meta name="apple-mobile-web-app-title" content="ermite">
811 <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg">
12+<link rel="apple-touch-icon" href="/-/static/app/apple-touch-icon.HASH.png">
13+<link rel="manifest" href="/-/static/app/manifest.HASH.webmanifest">
914 <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin>
1015 <link rel="stylesheet" href="/-/static/css/ermite.HASH.css">
1116 <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'>
@@ -28,10 +33,9 @@
2833 <a class="skip" href="#main">skip to content</a>
2934 <header class="hdr">
3035 <div class="hdr-top">
31-<a class="brand" href="/" data-g="h" title="home (g h)">
36+<a class="brand" href="/" data-g="h" aria-label="home" title="home (g h)">
3237 <span class="brand-mark" aria-hidden="true">
3338 </span>
34-<span class="brand-name">example.test</span>
3539 </a>
3640 <nav class="crumbs" aria-label="Path">
3741 <ol>
@@ -65,7 +69,6 @@
6569 <span class="search-label">Go to…</span>
6670 <kbd aria-hidden="true">ctrl k</kbd>
6771 </button>
68-<button type="button" class="btn icon" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" aria-label="Keyboard shortcuts" title="keyboard shortcuts (?)">?</button>
6972 </div>
7073 </div>
7174 <nav class="tabs" aria-label="Sections">
@@ -149,8 +152,8 @@
149152 <footer class="foot">
150153 <span>ermite devel</span>
151154 <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a>
152-<span class="foot-keys">
153-<kbd>?</kbd> keyboard shortcuts</span>
155+<button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?">
156+<kbd aria-hidden="true">?</kbd> keyboard shortcuts</button>
154157 </footer>
155158 <div class="hint" role="status" aria-live="polite">
156159 <span class="hint-nav" :text="document.body.dataset.nav || ''">
@@ -5,7 +5,12 @@
5 <meta name="viewport" content="width=device-width, initial-scale=1">5 <meta name="viewport" content="width=device-width, initial-scale=1">
6 <meta name="color-scheme" content="light dark">6 <meta name="color-scheme" content="light dark">
7 <title>src/lib · files · example.test</title>7 <title>src/lib · files · example.test</title>
8+<meta name="theme-color" media="(prefers-color-scheme: light)" content="#ffffff">
9+<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#0c0c0c">
10+<meta name="apple-mobile-web-app-title" content="ermite">
8 <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg">11 <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg">
12+<link rel="apple-touch-icon" href="/-/static/app/apple-touch-icon.HASH.png">
13+<link rel="manifest" href="/-/static/app/manifest.HASH.webmanifest">
9 <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin>14 <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin>
10 <link rel="stylesheet" href="/-/static/css/ermite.HASH.css">15 <link rel="stylesheet" href="/-/static/css/ermite.HASH.css">
11 <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'>16 <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'>
@@ -28,10 +33,9 @@
28 <a class="skip" href="#main">skip to content</a>33 <a class="skip" href="#main">skip to content</a>
29 <header class="hdr">34 <header class="hdr">
30 <div class="hdr-top">35 <div class="hdr-top">
31-<a class="brand" href="/" data-g="h" title="home (g h)">36+<a class="brand" href="/" data-g="h" aria-label="home" title="home (g h)">
32 <span class="brand-mark" aria-hidden="true">37 <span class="brand-mark" aria-hidden="true">
33 </span>38 </span>
34-<span class="brand-name">example.test</span>
35 </a>39 </a>
36 <nav class="crumbs" aria-label="Path">40 <nav class="crumbs" aria-label="Path">
37 <ol>41 <ol>
@@ -65,7 +69,6 @@
65 <span class="search-label">Go to…</span>69 <span class="search-label">Go to…</span>
66 <kbd aria-hidden="true">ctrl k</kbd>70 <kbd aria-hidden="true">ctrl k</kbd>
67 </button>71 </button>
68-<button type="button" class="btn icon" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" aria-label="Keyboard shortcuts" title="keyboard shortcuts (?)">?</button>
69 </div>72 </div>
70 </div>73 </div>
71 <nav class="tabs" aria-label="Sections">74 <nav class="tabs" aria-label="Sections">
@@ -149,8 +152,8 @@
149 <footer class="foot">152 <footer class="foot">
150 <span>ermite devel</span>153 <span>ermite devel</span>
151 <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a>154 <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a>
152-<span class="foot-keys">155+<button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?">
153-<kbd>?</kbd> keyboard shortcuts</span>156+<kbd aria-hidden="true">?</kbd> keyboard shortcuts</button>
154 </footer>157 </footer>
155 <div class="hint" role="status" aria-live="polite">158 <div class="hint" role="status" aria-live="polite">
156 <span class="hint-nav" :text="document.body.dataset.nav || ''">159 <span class="hint-nav" :text="document.body.dataset.nav || ''">
M internal/app/testdata/web/tree-root.html +8 -5
@@ -5,7 +5,12 @@
55 <meta name="viewport" content="width=device-width, initial-scale=1">
66 <meta name="color-scheme" content="light dark">
77 <title>tree · files · example.test</title>
8+<meta name="theme-color" media="(prefers-color-scheme: light)" content="#ffffff">
9+<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#0c0c0c">
10+<meta name="apple-mobile-web-app-title" content="ermite">
811 <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg">
12+<link rel="apple-touch-icon" href="/-/static/app/apple-touch-icon.HASH.png">
13+<link rel="manifest" href="/-/static/app/manifest.HASH.webmanifest">
914 <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin>
1015 <link rel="stylesheet" href="/-/static/css/ermite.HASH.css">
1116 <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'>
@@ -28,10 +33,9 @@
2833 <a class="skip" href="#main">skip to content</a>
2934 <header class="hdr">
3035 <div class="hdr-top">
31-<a class="brand" href="/" data-g="h" title="home (g h)">
36+<a class="brand" href="/" data-g="h" aria-label="home" title="home (g h)">
3237 <span class="brand-mark" aria-hidden="true">
3338 </span>
34-<span class="brand-name">example.test</span>
3539 </a>
3640 <nav class="crumbs" aria-label="Path">
3741 <ol>
@@ -59,7 +63,6 @@
5963 <span class="search-label">Go to…</span>
6064 <kbd aria-hidden="true">ctrl k</kbd>
6165 </button>
62-<button type="button" class="btn icon" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" aria-label="Keyboard shortcuts" title="keyboard shortcuts (?)">?</button>
6366 </div>
6467 </div>
6568 <nav class="tabs" aria-label="Sections">
@@ -376,8 +379,8 @@
376379 <footer class="foot">
377380 <span>ermite devel</span>
378381 <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a>
379-<span class="foot-keys">
380-<kbd>?</kbd> keyboard shortcuts</span>
382+<button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?">
383+<kbd aria-hidden="true">?</kbd> keyboard shortcuts</button>
381384 </footer>
382385 <div class="hint" role="status" aria-live="polite">
383386 <span class="hint-nav" :text="document.body.dataset.nav || ''">
@@ -5,7 +5,12 @@
5 <meta name="viewport" content="width=device-width, initial-scale=1">5 <meta name="viewport" content="width=device-width, initial-scale=1">
6 <meta name="color-scheme" content="light dark">6 <meta name="color-scheme" content="light dark">
7 <title>tree · files · example.test</title>7 <title>tree · files · example.test</title>
8+<meta name="theme-color" media="(prefers-color-scheme: light)" content="#ffffff">
9+<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#0c0c0c">
10+<meta name="apple-mobile-web-app-title" content="ermite">
8 <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg">11 <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg">
12+<link rel="apple-touch-icon" href="/-/static/app/apple-touch-icon.HASH.png">
13+<link rel="manifest" href="/-/static/app/manifest.HASH.webmanifest">
9 <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin>14 <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin>
10 <link rel="stylesheet" href="/-/static/css/ermite.HASH.css">15 <link rel="stylesheet" href="/-/static/css/ermite.HASH.css">
11 <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'>16 <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'>
@@ -28,10 +33,9 @@
28 <a class="skip" href="#main">skip to content</a>33 <a class="skip" href="#main">skip to content</a>
29 <header class="hdr">34 <header class="hdr">
30 <div class="hdr-top">35 <div class="hdr-top">
31-<a class="brand" href="/" data-g="h" title="home (g h)">36+<a class="brand" href="/" data-g="h" aria-label="home" title="home (g h)">
32 <span class="brand-mark" aria-hidden="true">37 <span class="brand-mark" aria-hidden="true">
33 </span>38 </span>
34-<span class="brand-name">example.test</span>
35 </a>39 </a>
36 <nav class="crumbs" aria-label="Path">40 <nav class="crumbs" aria-label="Path">
37 <ol>41 <ol>
@@ -59,7 +63,6 @@
59 <span class="search-label">Go to…</span>63 <span class="search-label">Go to…</span>
60 <kbd aria-hidden="true">ctrl k</kbd>64 <kbd aria-hidden="true">ctrl k</kbd>
61 </button>65 </button>
62-<button type="button" class="btn icon" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" aria-label="Keyboard shortcuts" title="keyboard shortcuts (?)">?</button>
63 </div>66 </div>
64 </div>67 </div>
65 <nav class="tabs" aria-label="Sections">68 <nav class="tabs" aria-label="Sections">
@@ -376,8 +379,8 @@
376 <footer class="foot">379 <footer class="foot">
377 <span>ermite devel</span>380 <span>ermite devel</span>
378 <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a>381 <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a>
379-<span class="foot-keys">382+<button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?">
380-<kbd>?</kbd> keyboard shortcuts</span>383+<kbd aria-hidden="true">?</kbd> keyboard shortcuts</button>
381 </footer>384 </footer>
382 <div class="hint" role="status" aria-live="polite">385 <div class="hint" role="status" aria-live="polite">
383 <span class="hint-nav" :text="document.body.dataset.nav || ''">386 <span class="hint-nav" :text="document.body.dataset.nav || ''">
M internal/app/testdata/web/tree-slash-ref.html +8 -5
@@ -5,7 +5,12 @@
55 <meta name="viewport" content="width=device-width, initial-scale=1">
66 <meta name="color-scheme" content="light dark">
77 <title>x · files · example.test</title>
8+<meta name="theme-color" media="(prefers-color-scheme: light)" content="#ffffff">
9+<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#0c0c0c">
10+<meta name="apple-mobile-web-app-title" content="ermite">
811 <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg">
12+<link rel="apple-touch-icon" href="/-/static/app/apple-touch-icon.HASH.png">
13+<link rel="manifest" href="/-/static/app/manifest.HASH.webmanifest">
914 <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin>
1015 <link rel="stylesheet" href="/-/static/css/ermite.HASH.css">
1116 <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'>
@@ -28,10 +33,9 @@
2833 <a class="skip" href="#main">skip to content</a>
2934 <header class="hdr">
3035 <div class="hdr-top">
31-<a class="brand" href="/" data-g="h" title="home (g h)">
36+<a class="brand" href="/" data-g="h" aria-label="home" title="home (g h)">
3237 <span class="brand-mark" aria-hidden="true">
3338 </span>
34-<span class="brand-name">example.test</span>
3539 </a>
3640 <nav class="crumbs" aria-label="Path">
3741 <ol>
@@ -62,7 +66,6 @@
6266 <span class="search-label">Go to…</span>
6367 <kbd aria-hidden="true">ctrl k</kbd>
6468 </button>
65-<button type="button" class="btn icon" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" aria-label="Keyboard shortcuts" title="keyboard shortcuts (?)">?</button>
6669 </div>
6770 </div>
6871 <nav class="tabs" aria-label="Sections">
@@ -146,8 +149,8 @@
146149 <footer class="foot">
147150 <span>ermite devel</span>
148151 <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a>
149-<span class="foot-keys">
150-<kbd>?</kbd> keyboard shortcuts</span>
152+<button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?">
153+<kbd aria-hidden="true">?</kbd> keyboard shortcuts</button>
151154 </footer>
152155 <div class="hint" role="status" aria-live="polite">
153156 <span class="hint-nav" :text="document.body.dataset.nav || ''">
@@ -5,7 +5,12 @@
5 <meta name="viewport" content="width=device-width, initial-scale=1">5 <meta name="viewport" content="width=device-width, initial-scale=1">
6 <meta name="color-scheme" content="light dark">6 <meta name="color-scheme" content="light dark">
7 <title>x · files · example.test</title>7 <title>x · files · example.test</title>
8+<meta name="theme-color" media="(prefers-color-scheme: light)" content="#ffffff">
9+<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#0c0c0c">
10+<meta name="apple-mobile-web-app-title" content="ermite">
8 <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg">11 <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg">
12+<link rel="apple-touch-icon" href="/-/static/app/apple-touch-icon.HASH.png">
13+<link rel="manifest" href="/-/static/app/manifest.HASH.webmanifest">
9 <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin>14 <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin>
10 <link rel="stylesheet" href="/-/static/css/ermite.HASH.css">15 <link rel="stylesheet" href="/-/static/css/ermite.HASH.css">
11 <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'>16 <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'>
@@ -28,10 +33,9 @@
28 <a class="skip" href="#main">skip to content</a>33 <a class="skip" href="#main">skip to content</a>
29 <header class="hdr">34 <header class="hdr">
30 <div class="hdr-top">35 <div class="hdr-top">
31-<a class="brand" href="/" data-g="h" title="home (g h)">36+<a class="brand" href="/" data-g="h" aria-label="home" title="home (g h)">
32 <span class="brand-mark" aria-hidden="true">37 <span class="brand-mark" aria-hidden="true">
33 </span>38 </span>
34-<span class="brand-name">example.test</span>
35 </a>39 </a>
36 <nav class="crumbs" aria-label="Path">40 <nav class="crumbs" aria-label="Path">
37 <ol>41 <ol>
@@ -62,7 +66,6 @@
62 <span class="search-label">Go to…</span>66 <span class="search-label">Go to…</span>
63 <kbd aria-hidden="true">ctrl k</kbd>67 <kbd aria-hidden="true">ctrl k</kbd>
64 </button>68 </button>
65-<button type="button" class="btn icon" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" aria-label="Keyboard shortcuts" title="keyboard shortcuts (?)">?</button>
66 </div>69 </div>
67 </div>70 </div>
68 <nav class="tabs" aria-label="Sections">71 <nav class="tabs" aria-label="Sections">
@@ -146,8 +149,8 @@
146 <footer class="foot">149 <footer class="foot">
147 <span>ermite devel</span>150 <span>ermite devel</span>
148 <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a>151 <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a>
149-<span class="foot-keys">152+<button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?">
150-<kbd>?</kbd> keyboard shortcuts</span>153+<kbd aria-hidden="true">?</kbd> keyboard shortcuts</button>
151 </footer>154 </footer>
152 <div class="hint" role="status" aria-live="polite">155 <div class="hint" role="status" aria-live="polite">
153 <span class="hint-nav" :text="document.body.dataset.nav || ''">156 <span class="hint-nav" :text="document.body.dataset.nav || ''">
M internal/app/testdata/web/tree-subdir.html +8 -5
@@ -5,7 +5,12 @@
55 <meta name="viewport" content="width=device-width, initial-scale=1">
66 <meta name="color-scheme" content="light dark">
77 <title>docs · files · example.test</title>
8+<meta name="theme-color" media="(prefers-color-scheme: light)" content="#ffffff">
9+<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#0c0c0c">
10+<meta name="apple-mobile-web-app-title" content="ermite">
811 <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg">
12+<link rel="apple-touch-icon" href="/-/static/app/apple-touch-icon.HASH.png">
13+<link rel="manifest" href="/-/static/app/manifest.HASH.webmanifest">
914 <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin>
1015 <link rel="stylesheet" href="/-/static/css/ermite.HASH.css">
1116 <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'>
@@ -28,10 +33,9 @@
2833 <a class="skip" href="#main">skip to content</a>
2934 <header class="hdr">
3035 <div class="hdr-top">
31-<a class="brand" href="/" data-g="h" title="home (g h)">
36+<a class="brand" href="/" data-g="h" aria-label="home" title="home (g h)">
3237 <span class="brand-mark" aria-hidden="true">
3338 </span>
34-<span class="brand-name">example.test</span>
3539 </a>
3640 <nav class="crumbs" aria-label="Path">
3741 <ol>
@@ -62,7 +66,6 @@
6266 <span class="search-label">Go to…</span>
6367 <kbd aria-hidden="true">ctrl k</kbd>
6468 </button>
65-<button type="button" class="btn icon" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" aria-label="Keyboard shortcuts" title="keyboard shortcuts (?)">?</button>
6669 </div>
6770 </div>
6871 <nav class="tabs" aria-label="Sections">
@@ -171,8 +174,8 @@
171174 <footer class="foot">
172175 <span>ermite devel</span>
173176 <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a>
174-<span class="foot-keys">
175-<kbd>?</kbd> keyboard shortcuts</span>
177+<button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?">
178+<kbd aria-hidden="true">?</kbd> keyboard shortcuts</button>
176179 </footer>
177180 <div class="hint" role="status" aria-live="polite">
178181 <span class="hint-nav" :text="document.body.dataset.nav || ''">
@@ -5,7 +5,12 @@
5 <meta name="viewport" content="width=device-width, initial-scale=1">5 <meta name="viewport" content="width=device-width, initial-scale=1">
6 <meta name="color-scheme" content="light dark">6 <meta name="color-scheme" content="light dark">
7 <title>docs · files · example.test</title>7 <title>docs · files · example.test</title>
8+<meta name="theme-color" media="(prefers-color-scheme: light)" content="#ffffff">
9+<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#0c0c0c">
10+<meta name="apple-mobile-web-app-title" content="ermite">
8 <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg">11 <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg">
12+<link rel="apple-touch-icon" href="/-/static/app/apple-touch-icon.HASH.png">
13+<link rel="manifest" href="/-/static/app/manifest.HASH.webmanifest">
9 <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin>14 <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin>
10 <link rel="stylesheet" href="/-/static/css/ermite.HASH.css">15 <link rel="stylesheet" href="/-/static/css/ermite.HASH.css">
11 <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'>16 <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'>
@@ -28,10 +33,9 @@
28 <a class="skip" href="#main">skip to content</a>33 <a class="skip" href="#main">skip to content</a>
29 <header class="hdr">34 <header class="hdr">
30 <div class="hdr-top">35 <div class="hdr-top">
31-<a class="brand" href="/" data-g="h" title="home (g h)">36+<a class="brand" href="/" data-g="h" aria-label="home" title="home (g h)">
32 <span class="brand-mark" aria-hidden="true">37 <span class="brand-mark" aria-hidden="true">
33 </span>38 </span>
34-<span class="brand-name">example.test</span>
35 </a>39 </a>
36 <nav class="crumbs" aria-label="Path">40 <nav class="crumbs" aria-label="Path">
37 <ol>41 <ol>
@@ -62,7 +66,6 @@
62 <span class="search-label">Go to…</span>66 <span class="search-label">Go to…</span>
63 <kbd aria-hidden="true">ctrl k</kbd>67 <kbd aria-hidden="true">ctrl k</kbd>
64 </button>68 </button>
65-<button type="button" class="btn icon" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?" aria-label="Keyboard shortcuts" title="keyboard shortcuts (?)">?</button>
66 </div>69 </div>
67 </div>70 </div>
68 <nav class="tabs" aria-label="Sections">71 <nav class="tabs" aria-label="Sections">
@@ -171,8 +174,8 @@
171 <footer class="foot">174 <footer class="foot">
172 <span>ermite devel</span>175 <span>ermite devel</span>
173 <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a>176 <a href="http://example.test/ermite" rel="noopener" title="source of this version (AGPL-3.0-or-later)">source</a>
174-<span class="foot-keys">177+<button type="button" class="foot-keys" data-js data-fire="help" aria-haspopup="dialog" aria-controls="panel-help" aria-keyshortcuts="?">
175-<kbd>?</kbd> keyboard shortcuts</span>178+<kbd aria-hidden="true">?</kbd> keyboard shortcuts</button>
176 </footer>179 </footer>
177 <div class="hint" role="status" aria-live="polite">180 <div class="hint" role="status" aria-live="polite">
178 <span class="hint-nav" :text="document.body.dataset.nav || ''">181 <span class="hint-nav" :text="document.body.dataset.nav || ''">

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 42

↑↓ moveenter openesc close