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 generated file: collapsed
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 41

↑↓ moveenter openesc close