fix(web): commit signature row and compact home layout (#74)
- commit
- dfd6f0b357179d346e33720bb464397fcba5b3ba
- author
- brice · · 2026-10-18 19:01 UTC
- committer
- bnema · · 2026-10-18 19:01 UTC
- parent
- df1db36
- signature
- SSH Unverified
SHA256:VOCA5lYARHcbKDZ7GhTV45VmcjWnJ6ZQIBazj17LivAThe key is unknown - refs
- tag: v0.26.1
8 files · +46 -25
M internal/adapters/web/components.templ +1 -1
| @@ -96,7 +96,7 @@ templ HomeRowView(r HomeRow) { | |||
| 96 | 96 | if r.CI != nil { | |
| 97 | 97 | <a class="repo-ci" href={ templ.SafeURL(r.CI.Href) } title={ r.CI.Title }> | |
| 98 | 98 | <span class={ "ck", "ck-" + r.CI.Kind } aria-hidden="true"></span> | |
| 99 | - | <span class="sr">CI: </span>{ r.CI.Label } | |
| 99 | + | <span class="sr">CI: </span><span class="ci-label">{ r.CI.Label }</span> | |
| 100 | 100 | </a> | |
| 101 | 101 | } | |
| 102 | 102 | </div> | |
| @@ -96,7 +96,7 @@ templ HomeRowView(r HomeRow) { | |||||
| 96 | if r.CI != nil { | 96 | if r.CI != nil { | ||
| 97 | <a class="repo-ci" href={ templ.SafeURL(r.CI.Href) } title={ r.CI.Title }> | 97 | <a class="repo-ci" href={ templ.SafeURL(r.CI.Href) } title={ r.CI.Title }> | ||
| 98 | <span class={ "ck", "ck-" + r.CI.Kind } aria-hidden="true"></span> | 98 | <span class={ "ck", "ck-" + r.CI.Kind } aria-hidden="true"></span> | ||
| 99 | - | <span class="sr">CI: </span>{ r.CI.Label } | 99 | + | <span class="sr">CI: </span><span class="ci-label">{ r.CI.Label }</span> |
| 100 | </a> | 100 | </a> | ||
| 101 | } | 101 | } | ||
| 102 | </div> | 102 | </div> | ||
M internal/adapters/web/components_templ.go +3 -3 generated file: collapsed
M internal/adapters/web/diff.templ +2 -2
| @@ -50,8 +50,8 @@ templ Commit(l Layout, p CommitPage) { | |||
| 50 | 50 | if p.Verification.KeyTitle != "" { | |
| 51 | 51 | <span class="dim">{ "· " + visibleText(p.Verification.KeyTitle) }</span> | |
| 52 | 52 | } | |
| 53 | - | if p.Sig != nil && p.Sig.Class != "sig-none" { | |
| 54 | - | <span class="sig-why dim">{ p.Sig.Title }</span> | |
| 53 | + | if p.Sig.Class != "sig-none" { | |
| 54 | + | <span class="sig-why dim">{ p.Sig.Reason }</span> | |
| 55 | 55 | } | |
| 56 | 56 | </dd> | |
| 57 | 57 | } | |
| @@ -50,8 +50,8 @@ templ Commit(l Layout, p CommitPage) { | |||||
| 50 | if p.Verification.KeyTitle != "" { | 50 | if p.Verification.KeyTitle != "" { | ||
| 51 | <span class="dim">{ "· " + visibleText(p.Verification.KeyTitle) }</span> | 51 | <span class="dim">{ "· " + visibleText(p.Verification.KeyTitle) }</span> | ||
| 52 | } | 52 | } | ||
| 53 | - | if p.Sig != nil && p.Sig.Class != "sig-none" { | 53 | + | if p.Sig.Class != "sig-none" { |
| 54 | - | <span class="sig-why dim">{ p.Sig.Title }</span> | 54 | + | <span class="sig-why dim">{ p.Sig.Reason }</span> |
| 55 | } | 55 | } | ||
| 56 | </dd> | 56 | </dd> | ||
| 57 | } | 57 | } | ||
M internal/adapters/web/diff_templ.go +3 -3 generated file: collapsed
M internal/adapters/web/home_test.go +1 -1
| @@ -212,7 +212,7 @@ func TestHomeRowCI(t *testing.T) { | |||
| 212 | 212 | } | |
| 213 | 213 | require.NotNil(t, row.CI) | |
| 214 | 214 | assert.Contains(t, out, `class="repo-ci" href="`+tt.href+`"`) | |
| 215 | - | assert.Contains(t, out, tt.label, "the state has a word, not only a mark") | |
| 215 | + | assert.Contains(t, out, `<span class="ci-label">`+tt.label+`</span>`, "the state has a word, not only a mark; compact hides it") | |
| 216 | 216 | assert.Contains(t, out, `<span class="sr">CI: </span>`) | |
| 217 | 217 | }) | |
| 218 | 218 | } | |
| @@ -212,7 +212,7 @@ func TestHomeRowCI(t *testing.T) { | |||||
| 212 | } | 212 | } | ||
| 213 | require.NotNil(t, row.CI) | 213 | require.NotNil(t, row.CI) | ||
| 214 | assert.Contains(t, out, `class="repo-ci" href="`+tt.href+`"`) | 214 | assert.Contains(t, out, `class="repo-ci" href="`+tt.href+`"`) | ||
| 215 | - | assert.Contains(t, out, tt.label, "the state has a word, not only a mark") | 215 | + | assert.Contains(t, out, `<span class="ci-label">`+tt.label+`</span>`, "the state has a word, not only a mark; compact hides it") |
| 216 | assert.Contains(t, out, `<span class="sr">CI: </span>`) | 216 | assert.Contains(t, out, `<span class="sr">CI: </span>`) | ||
| 217 | }) | 217 | }) | ||
| 218 | } | 218 | } | ||
M internal/adapters/web/signature.go +15 -5
| @@ -9,8 +9,10 @@ import ( | |||
| 9 | 9 | // SignatureBadge is the pill of a signed commit. Label is the verdict, Proto | |
| 10 | 10 | // the protocol ("SSH", "GPG", "X.509", empty when unknown), Class the style | |
| 11 | 11 | // (sig-ok, sig-warn, sig-bad, sig-none) and Title the tooltip that says why. | |
| 12 | + | // Reason says why without naming the key, for a page that shows the key next | |
| 13 | + | // to it. | |
| 12 | 14 | type SignatureBadge struct { | |
| 13 | - | Label, Proto, Class, Title string | |
| 15 | + | Label, Proto, Class, Title, Reason string | |
| 14 | 16 | } | |
| 15 | 17 | ||
| 16 | 18 | // signatureProto is the display name of a signature format. | |
| @@ -37,7 +39,7 @@ func signatureBadge(v domain.CommitVerification, long bool) *SignatureBadge { | |||
| 37 | 39 | if !long { | |
| 38 | 40 | return nil | |
| 39 | 41 | } | |
| 40 | - | return &SignatureBadge{Label: "unsigned", Class: "sig-none", Title: "This commit is not signed"} | |
| 42 | + | return &SignatureBadge{Label: "unsigned", Class: "sig-none", Title: "This commit is not signed", Reason: "Not signed"} | |
| 41 | 43 | } | |
| 42 | 44 | b := &SignatureBadge{Proto: signatureProto(v.Format)} | |
| 43 | 45 | key := keyPhrase(v) | |
| @@ -45,31 +47,39 @@ func signatureBadge(v domain.CommitVerification, long bool) *SignatureBadge { | |||
| 45 | 47 | case domain.StatusVerified: | |
| 46 | 48 | b.Label, b.Class = "Verified", "sig-ok" | |
| 47 | 49 | b.Title = "Signed with " + key + " — verified for the committer email" | |
| 50 | + | b.Reason = "Verified for the committer email" | |
| 48 | 51 | case domain.StatusUnknownKey: | |
| 49 | 52 | b.Label, b.Class = "Unverified", "sig-warn" | |
| 50 | 53 | b.Title = unknownKeyTitle(v) | |
| 54 | + | b.Reason = "The key is unknown" | |
| 51 | 55 | case domain.StatusEmailMismatch: | |
| 52 | 56 | b.Label, b.Class = "Unverified", "sig-warn" | |
| 53 | 57 | b.Title = "Signed with " + key + " — this key is not trusted for the committer email" | |
| 58 | + | b.Reason = "This key is not trusted for the committer email" | |
| 54 | 59 | case domain.StatusExpired: | |
| 55 | 60 | b.Label, b.Class = "Unverified", "sig-warn" | |
| 56 | 61 | b.Title = "Signed with " + key + " — the key or the signature expired" | |
| 62 | + | b.Reason = "The key or the signature expired" | |
| 57 | 63 | case domain.StatusRevoked: | |
| 58 | 64 | b.Label, b.Class = "Unverified", "sig-warn" | |
| 59 | 65 | b.Title = "Signed with " + key + " — the key is revoked" | |
| 66 | + | b.Reason = "The key is revoked" | |
| 60 | 67 | case domain.StatusBadSignature: | |
| 61 | 68 | b.Label, b.Class = "Bad signature", "sig-bad" | |
| 62 | - | b.Title = "The signature does not match the commit" | |
| 69 | + | b.Reason = "The signature does not match the commit" | |
| 63 | 70 | case domain.StatusUnsupported: | |
| 64 | 71 | b.Label, b.Class = "Unverified", "sig-warn" | |
| 65 | 72 | if proto := signatureProto(v.Format); proto != "" { | |
| 66 | - | b.Title = proto + " signatures are not checked" | |
| 73 | + | b.Reason = proto + " signatures are not checked" | |
| 67 | 74 | } else { | |
| 68 | - | b.Title = "This kind of signature is not checked" | |
| 75 | + | b.Reason = "This kind of signature is not checked" | |
| 69 | 76 | } | |
| 70 | 77 | default: | |
| 71 | 78 | return nil | |
| 72 | 79 | } | |
| 80 | + | if b.Title == "" { | |
| 81 | + | b.Title = b.Reason | |
| 82 | + | } | |
| 73 | 83 | return b | |
| 74 | 84 | } | |
| 75 | 85 | ||
| @@ -9,8 +9,10 @@ import ( | |||||
| 9 | // SignatureBadge is the pill of a signed commit. Label is the verdict, Proto | 9 | // SignatureBadge is the pill of a signed commit. Label is the verdict, Proto | ||
| 10 | // the protocol ("SSH", "GPG", "X.509", empty when unknown), Class the style | 10 | // the protocol ("SSH", "GPG", "X.509", empty when unknown), Class the style | ||
| 11 | // (sig-ok, sig-warn, sig-bad, sig-none) and Title the tooltip that says why. | 11 | // (sig-ok, sig-warn, sig-bad, sig-none) and Title the tooltip that says why. | ||
| 12 | + | // Reason says why without naming the key, for a page that shows the key next | |||
| 13 | + | // to it. | |||
| 12 | type SignatureBadge struct { | 14 | type SignatureBadge struct { | ||
| 13 | - | Label, Proto, Class, Title string | 15 | + | Label, Proto, Class, Title, Reason string |
| 14 | } | 16 | } | ||
| 15 | 17 | ||||
| 16 | // signatureProto is the display name of a signature format. | 18 | // signatureProto is the display name of a signature format. | ||
| @@ -37,7 +39,7 @@ func signatureBadge(v domain.CommitVerification, long bool) *SignatureBadge { | |||||
| 37 | if !long { | 39 | if !long { | ||
| 38 | return nil | 40 | return nil | ||
| 39 | } | 41 | } | ||
| 40 | - | return &SignatureBadge{Label: "unsigned", Class: "sig-none", Title: "This commit is not signed"} | 42 | + | return &SignatureBadge{Label: "unsigned", Class: "sig-none", Title: "This commit is not signed", Reason: "Not signed"} |
| 41 | } | 43 | } | ||
| 42 | b := &SignatureBadge{Proto: signatureProto(v.Format)} | 44 | b := &SignatureBadge{Proto: signatureProto(v.Format)} | ||
| 43 | key := keyPhrase(v) | 45 | key := keyPhrase(v) | ||
| @@ -45,31 +47,39 @@ func signatureBadge(v domain.CommitVerification, long bool) *SignatureBadge { | |||||
| 45 | case domain.StatusVerified: | 47 | case domain.StatusVerified: | ||
| 46 | b.Label, b.Class = "Verified", "sig-ok" | 48 | b.Label, b.Class = "Verified", "sig-ok" | ||
| 47 | b.Title = "Signed with " + key + " — verified for the committer email" | 49 | b.Title = "Signed with " + key + " — verified for the committer email" | ||
| 50 | + | b.Reason = "Verified for the committer email" | |||
| 48 | case domain.StatusUnknownKey: | 51 | case domain.StatusUnknownKey: | ||
| 49 | b.Label, b.Class = "Unverified", "sig-warn" | 52 | b.Label, b.Class = "Unverified", "sig-warn" | ||
| 50 | b.Title = unknownKeyTitle(v) | 53 | b.Title = unknownKeyTitle(v) | ||
| 54 | + | b.Reason = "The key is unknown" | |||
| 51 | case domain.StatusEmailMismatch: | 55 | case domain.StatusEmailMismatch: | ||
| 52 | b.Label, b.Class = "Unverified", "sig-warn" | 56 | b.Label, b.Class = "Unverified", "sig-warn" | ||
| 53 | b.Title = "Signed with " + key + " — this key is not trusted for the committer email" | 57 | b.Title = "Signed with " + key + " — this key is not trusted for the committer email" | ||
| 58 | + | b.Reason = "This key is not trusted for the committer email" | |||
| 54 | case domain.StatusExpired: | 59 | case domain.StatusExpired: | ||
| 55 | b.Label, b.Class = "Unverified", "sig-warn" | 60 | b.Label, b.Class = "Unverified", "sig-warn" | ||
| 56 | b.Title = "Signed with " + key + " — the key or the signature expired" | 61 | b.Title = "Signed with " + key + " — the key or the signature expired" | ||
| 62 | + | b.Reason = "The key or the signature expired" | |||
| 57 | case domain.StatusRevoked: | 63 | case domain.StatusRevoked: | ||
| 58 | b.Label, b.Class = "Unverified", "sig-warn" | 64 | b.Label, b.Class = "Unverified", "sig-warn" | ||
| 59 | b.Title = "Signed with " + key + " — the key is revoked" | 65 | b.Title = "Signed with " + key + " — the key is revoked" | ||
| 66 | + | b.Reason = "The key is revoked" | |||
| 60 | case domain.StatusBadSignature: | 67 | case domain.StatusBadSignature: | ||
| 61 | b.Label, b.Class = "Bad signature", "sig-bad" | 68 | b.Label, b.Class = "Bad signature", "sig-bad" | ||
| 62 | - | b.Title = "The signature does not match the commit" | 69 | + | b.Reason = "The signature does not match the commit" |
| 63 | case domain.StatusUnsupported: | 70 | case domain.StatusUnsupported: | ||
| 64 | b.Label, b.Class = "Unverified", "sig-warn" | 71 | b.Label, b.Class = "Unverified", "sig-warn" | ||
| 65 | if proto := signatureProto(v.Format); proto != "" { | 72 | if proto := signatureProto(v.Format); proto != "" { | ||
| 66 | - | b.Title = proto + " signatures are not checked" | 73 | + | b.Reason = proto + " signatures are not checked" |
| 67 | } else { | 74 | } else { | ||
| 68 | - | b.Title = "This kind of signature is not checked" | 75 | + | b.Reason = "This kind of signature is not checked" |
| 69 | } | 76 | } | ||
| 70 | default: | 77 | default: | ||
| 71 | return nil | 78 | return nil | ||
| 72 | } | 79 | } | ||
| 80 | + | if b.Title == "" { | |||
| 81 | + | b.Title = b.Reason | |||
| 82 | + | } | |||
| 73 | return b | 83 | return b | ||
| 74 | } | 84 | } | ||
| 75 | 85 | ||||
M internal/adapters/web/signature_test.go +2 -1
| @@ -128,7 +128,8 @@ func TestCommitPageShowsTheSignature(t *testing.T) { | |||
| 128 | 128 | got = page(verification(domain.FormatGPG, domain.StatusUnknownKey, gpgFP, "")) | |
| 129 | 129 | assert.Contains(t, got, `sig-warn`) | |
| 130 | 130 | assert.Contains(t, got, "<code>"+gpgFP+"</code>") | |
| 131 | - | assert.Contains(t, got, `<span class="sig-why dim">Signed with an unknown GPG key `+gpgFP+`</span>`, "the reason is text, not only a tooltip") | |
| 131 | + | assert.Contains(t, got, `<span class="sig-why dim">The key is unknown</span>`, "the reason is text, not only a tooltip") | |
| 132 | + | assert.Equal(t, 1, strings.Count(got, ">"+gpgFP), "the fingerprint is shown once; the pill tooltip may repeat it") | |
| 132 | 133 | ||
| 133 | 134 | got = page(verification("", domain.StatusUnsigned, "", "")) | |
| 134 | 135 | assert.Contains(t, got, "<dt>signature</dt>") | |
| @@ -128,7 +128,8 @@ func TestCommitPageShowsTheSignature(t *testing.T) { | |||||
| 128 | got = page(verification(domain.FormatGPG, domain.StatusUnknownKey, gpgFP, "")) | 128 | got = page(verification(domain.FormatGPG, domain.StatusUnknownKey, gpgFP, "")) | ||
| 129 | assert.Contains(t, got, `sig-warn`) | 129 | assert.Contains(t, got, `sig-warn`) | ||
| 130 | assert.Contains(t, got, "<code>"+gpgFP+"</code>") | 130 | assert.Contains(t, got, "<code>"+gpgFP+"</code>") | ||
| 131 | - | assert.Contains(t, got, `<span class="sig-why dim">Signed with an unknown GPG key `+gpgFP+`</span>`, "the reason is text, not only a tooltip") | 131 | + | assert.Contains(t, got, `<span class="sig-why dim">The key is unknown</span>`, "the reason is text, not only a tooltip") |
| 132 | + | assert.Equal(t, 1, strings.Count(got, ">"+gpgFP), "the fingerprint is shown once; the pill tooltip may repeat it") | |||
| 132 | 133 | ||||
| 133 | got = page(verification("", domain.StatusUnsigned, "", "")) | 134 | got = page(verification("", domain.StatusUnsigned, "", "")) | ||
| 134 | assert.Contains(t, got, "<dt>signature</dt>") | 135 | assert.Contains(t, got, "<dt>signature</dt>") | ||
M internal/adapters/web/static/css/30-list.css +19 -9
| @@ -179,19 +179,27 @@ | |||
| 179 | 179 | .home[data-layout="split"] .repos { grid-template-columns: minmax(0, 1fr); } | |
| 180 | 180 | } | |
| 181 | 181 | ||
| 182 | - | /* compact: a line per repository. Name, description, figures, CI state and | |
| 183 | - | a small pulse sit in one grid row; the commits and topics are left out. */ | |
| 182 | + | /* compact: a line per repository. Name, description, figures, CI mark and | |
| 183 | + | a small pulse sit in one grid row; the commits and topics are left out. | |
| 184 | + | The rows share the columns of the list (subgrid): the name column is as | |
| 185 | + | wide as the longest name, the figures line up, and the description takes | |
| 186 | + | what is left. */ | |
| 184 | 187 | .home[data-layout="compact"] { | |
| 185 | - | & .repos { gap: var(--s2); } | |
| 188 | + | & .repos { | |
| 189 | + | display: grid; grid-template-columns: fit-content(18rem) minmax(0, 1fr) auto auto 140px; | |
| 190 | + | gap: var(--s2) var(--s4); | |
| 191 | + | } | |
| 186 | 192 | & .repo { | |
| 187 | - | grid-template-columns: minmax(0, 2fr) minmax(0, 3fr) minmax(0, 20rem) 5.5rem 140px; | |
| 193 | + | grid-column: 1 / -1; grid-template-columns: subgrid; | |
| 188 | 194 | grid-template-areas: "name desc stats ci pulse"; | |
| 189 | - | grid-template-rows: none; column-gap: var(--s4); row-gap: 0; align-items: center; | |
| 195 | + | /* normal: the row takes the gaps of the list */ | |
| 196 | + | grid-template-rows: none; column-gap: normal; row-gap: 0; align-items: center; | |
| 190 | 197 | padding: var(--s2) var(--s4); | |
| 191 | 198 | } | |
| 192 | 199 | & .repo-main, & .repo-head { display: contents; } | |
| 193 | 200 | & .repo-topics, & .repo-commits { display: none; } | |
| 194 | - | & .repo-name { grid-area: name; flex-wrap: nowrap; gap: var(--s2); font-size: var(--fs); line-height: 24px; overflow-wrap: normal; } | |
| 201 | + | /* min-width 0: a long name is cut, it does not widen the column past its cap */ | |
| 202 | + | & .repo-name { grid-area: name; min-width: 0; flex-wrap: nowrap; gap: var(--s2); font-size: var(--fs); line-height: 24px; overflow-wrap: normal; } | |
| 195 | 203 | & .repo-name a { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } | |
| 196 | 204 | & .repo-name .pill { flex: none; } | |
| 197 | 205 | & .repo-desc { | |
| @@ -212,7 +220,9 @@ | |||
| 212 | 220 | content: ""; display: inline-block; width: 12px; height: 12px; margin-right: 3px; vertical-align: -2px; | |
| 213 | 221 | background: currentColor; mask: url(icons/tag.svg) center / contain no-repeat; | |
| 214 | 222 | } | |
| 215 | - | & .repo-ci { grid-area: ci; justify-self: start; padding-top: 0; font-size: var(--fs-xs); } | |
| 223 | + | /* the CI state is its mark; the word is in the tooltip and for screen readers */ | |
| 224 | + | & .repo-ci { grid-area: ci; justify-self: center; padding-top: 0; } | |
| 225 | + | & .repo-ci .ci-label { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; } | |
| 216 | 226 | /* the pulse: the same curve, small, with a bigger dot to stay seen */ | |
| 217 | 227 | & .repo-pulse { grid-area: pulse; width: 100%; height: 100%; min-height: 18px; margin: 0; padding: 0; } | |
| 218 | 228 | & div.repo-pulse { aspect-ratio: auto; box-sizing: border-box; } | |
| @@ -221,14 +231,14 @@ | |||
| 221 | 231 | & .pulse-dot { r: 9px; stroke: none; } | |
| 222 | 232 | } | |
| 223 | 233 | @media (max-width: 1100px) { | |
| 224 | - | .home[data-layout="compact"] .repo { grid-template-columns: minmax(0, 2fr) minmax(0, 2fr) minmax(0, 14rem) 5.5rem 96px; column-gap: var(--s3); } | |
| 234 | + | .home[data-layout="compact"] .repos { grid-template-columns: fit-content(14rem) minmax(0, 1fr) auto auto 96px; column-gap: var(--s3); } | |
| 225 | 235 | .home[data-layout="compact"] .repo-stats .rs-tag { display: none; } | |
| 226 | 236 | } | |
| 227 | 237 | /* on a phone: name and CI state on a line, the description and the figures | |
| 228 | 238 | under it; the pulse is dropped */ | |
| 229 | 239 | @media (max-width: 720px) { | |
| 230 | 240 | .home[data-layout="compact"] .repo { | |
| 231 | - | grid-template-columns: minmax(0, 1fr) auto; grid-template-areas: "name ci" "desc desc" "stats stats"; row-gap: 2px; | |
| 241 | + | grid-template-columns: minmax(0, 1fr) auto; grid-template-areas: "name ci" "desc desc" "stats stats"; column-gap: var(--s3); row-gap: 2px; | |
| 232 | 242 | } | |
| 233 | 243 | .home[data-layout="compact"] .repo-pulse { display: none; } | |
| 234 | 244 | .home[data-layout="compact"] .repo-stats .rs-tag { display: flex; } | |
| @@ -179,19 +179,27 @@ | |||||
| 179 | .home[data-layout="split"] .repos { grid-template-columns: minmax(0, 1fr); } | 179 | .home[data-layout="split"] .repos { grid-template-columns: minmax(0, 1fr); } | ||
| 180 | } | 180 | } | ||
| 181 | 181 | ||||
| 182 | - | /* compact: a line per repository. Name, description, figures, CI state and | 182 | + | /* compact: a line per repository. Name, description, figures, CI mark and |
| 183 | - | a small pulse sit in one grid row; the commits and topics are left out. */ | 183 | + | a small pulse sit in one grid row; the commits and topics are left out. |
| 184 | + | The rows share the columns of the list (subgrid): the name column is as | |||
| 185 | + | wide as the longest name, the figures line up, and the description takes | |||
| 186 | + | what is left. */ | |||
| 184 | .home[data-layout="compact"] { | 187 | .home[data-layout="compact"] { | ||
| 185 | - | & .repos { gap: var(--s2); } | 188 | + | & .repos { |
| 189 | + | display: grid; grid-template-columns: fit-content(18rem) minmax(0, 1fr) auto auto 140px; | |||
| 190 | + | gap: var(--s2) var(--s4); | |||
| 191 | + | } | |||
| 186 | & .repo { | 192 | & .repo { | ||
| 187 | - | grid-template-columns: minmax(0, 2fr) minmax(0, 3fr) minmax(0, 20rem) 5.5rem 140px; | 193 | + | grid-column: 1 / -1; grid-template-columns: subgrid; |
| 188 | grid-template-areas: "name desc stats ci pulse"; | 194 | grid-template-areas: "name desc stats ci pulse"; | ||
| 189 | - | grid-template-rows: none; column-gap: var(--s4); row-gap: 0; align-items: center; | 195 | + | /* normal: the row takes the gaps of the list */ |
| 196 | + | grid-template-rows: none; column-gap: normal; row-gap: 0; align-items: center; | |||
| 190 | padding: var(--s2) var(--s4); | 197 | padding: var(--s2) var(--s4); | ||
| 191 | } | 198 | } | ||
| 192 | & .repo-main, & .repo-head { display: contents; } | 199 | & .repo-main, & .repo-head { display: contents; } | ||
| 193 | & .repo-topics, & .repo-commits { display: none; } | 200 | & .repo-topics, & .repo-commits { display: none; } | ||
| 194 | - | & .repo-name { grid-area: name; flex-wrap: nowrap; gap: var(--s2); font-size: var(--fs); line-height: 24px; overflow-wrap: normal; } | 201 | + | /* min-width 0: a long name is cut, it does not widen the column past its cap */ |
| 202 | + | & .repo-name { grid-area: name; min-width: 0; flex-wrap: nowrap; gap: var(--s2); font-size: var(--fs); line-height: 24px; overflow-wrap: normal; } | |||
| 195 | & .repo-name a { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } | 203 | & .repo-name a { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } | ||
| 196 | & .repo-name .pill { flex: none; } | 204 | & .repo-name .pill { flex: none; } | ||
| 197 | & .repo-desc { | 205 | & .repo-desc { | ||
| @@ -212,7 +220,9 @@ | |||||
| 212 | content: ""; display: inline-block; width: 12px; height: 12px; margin-right: 3px; vertical-align: -2px; | 220 | content: ""; display: inline-block; width: 12px; height: 12px; margin-right: 3px; vertical-align: -2px; | ||
| 213 | background: currentColor; mask: url(icons/tag.svg) center / contain no-repeat; | 221 | background: currentColor; mask: url(icons/tag.svg) center / contain no-repeat; | ||
| 214 | } | 222 | } | ||
| 215 | - | & .repo-ci { grid-area: ci; justify-self: start; padding-top: 0; font-size: var(--fs-xs); } | 223 | + | /* the CI state is its mark; the word is in the tooltip and for screen readers */ |
| 224 | + | & .repo-ci { grid-area: ci; justify-self: center; padding-top: 0; } | |||
| 225 | + | & .repo-ci .ci-label { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; } | |||
| 216 | /* the pulse: the same curve, small, with a bigger dot to stay seen */ | 226 | /* the pulse: the same curve, small, with a bigger dot to stay seen */ | ||
| 217 | & .repo-pulse { grid-area: pulse; width: 100%; height: 100%; min-height: 18px; margin: 0; padding: 0; } | 227 | & .repo-pulse { grid-area: pulse; width: 100%; height: 100%; min-height: 18px; margin: 0; padding: 0; } | ||
| 218 | & div.repo-pulse { aspect-ratio: auto; box-sizing: border-box; } | 228 | & div.repo-pulse { aspect-ratio: auto; box-sizing: border-box; } | ||
| @@ -221,14 +231,14 @@ | |||||
| 221 | & .pulse-dot { r: 9px; stroke: none; } | 231 | & .pulse-dot { r: 9px; stroke: none; } | ||
| 222 | } | 232 | } | ||
| 223 | @media (max-width: 1100px) { | 233 | @media (max-width: 1100px) { | ||
| 224 | - | .home[data-layout="compact"] .repo { grid-template-columns: minmax(0, 2fr) minmax(0, 2fr) minmax(0, 14rem) 5.5rem 96px; column-gap: var(--s3); } | 234 | + | .home[data-layout="compact"] .repos { grid-template-columns: fit-content(14rem) minmax(0, 1fr) auto auto 96px; column-gap: var(--s3); } |
| 225 | .home[data-layout="compact"] .repo-stats .rs-tag { display: none; } | 235 | .home[data-layout="compact"] .repo-stats .rs-tag { display: none; } | ||
| 226 | } | 236 | } | ||
| 227 | /* on a phone: name and CI state on a line, the description and the figures | 237 | /* on a phone: name and CI state on a line, the description and the figures | ||
| 228 | under it; the pulse is dropped */ | 238 | under it; the pulse is dropped */ | ||
| 229 | @media (max-width: 720px) { | 239 | @media (max-width: 720px) { | ||
| 230 | .home[data-layout="compact"] .repo { | 240 | .home[data-layout="compact"] .repo { | ||
| 231 | - | grid-template-columns: minmax(0, 1fr) auto; grid-template-areas: "name ci" "desc desc" "stats stats"; row-gap: 2px; | 241 | + | grid-template-columns: minmax(0, 1fr) auto; grid-template-areas: "name ci" "desc desc" "stats stats"; column-gap: var(--s3); row-gap: 2px; |
| 232 | } | 242 | } | ||
| 233 | .home[data-layout="compact"] .repo-pulse { display: none; } | 243 | .home[data-layout="compact"] .repo-pulse { display: none; } | ||
| 234 | .home[data-layout="compact"] .repo-stats .rs-tag { display: flex; } | 244 | .home[data-layout="compact"] .repo-stats .rs-tag { display: flex; } | ||