Commit b3b4b490f0
b3b4b490f0b68b6589df1a9e18f145cc28c1923c
parent: 846b17ebe7
Verified · cmc ci/build: success
cmc <hello@cleberg.net> · 2026-08-26T23:02:50Z
web: scope each syntax palette to its color scheme
The light palette was emitted unscoped and the dark one only overrode
the tokens it names, so every token github-dark omits kept a light-theme
colour on a black ground — NameAttribute measured 2.97:1 against an
added line. Scoped, those tokens inherit the wrapper colour instead.
e2e/diffweb_test.go
+13 −1
| @@ -88,9 +88,21 @@ func TestDiffRendering(t *testing.T) { |
| 88 | 88 | if !strings.Contains(body, `<td class="src chroma">`) { |
| 89 | 89 | t.Error("diff cell is not a chroma wrapper, so token classes go unstyled") |
| 90 | 90 | } |
| 91 | | if !strings.Contains(stylesheet(t, inst), ".chroma .k") { |
| 91 | css := stylesheet(t, inst) |
| 92 | if !strings.Contains(css, ".chroma .k") { |
| 92 | 93 | t.Error("stylesheet has no chroma token rules to match") |
| 93 | 94 | } |
| 95 | // Each palette is scoped to its own scheme. Unscoped, the light one |
| 96 | // leaks into dark mode for every token the dark palette does not name. |
| 97 | if !strings.Contains(css, "@media (prefers-color-scheme: light)") || |
| 98 | !strings.Contains(css, "@media (prefers-color-scheme: dark)") { |
| 99 | t.Error("chroma palettes are not both scheme-scoped") |
| 100 | } |
| 101 | if i := strings.Index(css, ".chroma .na"); i >= 0 { |
| 102 | if j := strings.LastIndex(css[:i], "prefers-color-scheme"); j < 0 { |
| 103 | t.Error("a chroma token rule sits outside any scheme scope") |
| 104 | } |
| 105 | } |
| 94 | 106 | // The +/- markers are CSS, so a copied selection is real source. |
| 95 | 107 | if strings.Contains(body, `<td class="src">+`) { |
| 96 | 108 | t.Error("diff markers are in the markup, not the stylesheet") |
internal/httpd/web.go
+8 −4
| @@ -829,13 +829,17 @@ func highlight(filePath string, data []byte) template.HTML { |
| 829 | 829 | return template.HTML(buf.String()) |
| 830 | 830 | } |
| 831 | 831 | |
| 832 | | // chromaCSS is both syntax palettes: light by default, dark under the same |
| 833 | | // media query the rest of the stylesheet uses. The site's --code-bg stays |
| 834 | | // the background either way. |
| 832 | // chromaCSS is both syntax palettes, each scoped to the scheme it is for. |
| 833 | // The light one cannot be left unscoped: the two palettes do not name the |
| 834 | // same token set, and every token github-dark omits would keep its |
| 835 | // light-theme colour on a black ground — NameAttribute landed at 2.97:1. |
| 836 | // Scoped, an unnamed token inherits the wrapper's colour instead, which is |
| 837 | // readable in both. The site's --code-bg stays the background either way. |
| 835 | 838 | var chromaCSS = func() []byte { |
| 836 | 839 | var buf bytes.Buffer |
| 840 | buf.WriteString("@media (prefers-color-scheme: light) {\n") |
| 837 | 841 | chromaFormatter.WriteCSS(&buf, styles.Get("friendly")) |
| 838 | | buf.WriteString("\n@media (prefers-color-scheme: dark) {\n") |
| 842 | buf.WriteString("}\n@media (prefers-color-scheme: dark) {\n") |
| 839 | 843 | chromaFormatter.WriteCSS(&buf, styles.Get("github-dark")) |
| 840 | 844 | buf.WriteString("}\n.chroma, .bg { background: transparent !important; }\n") |
| 841 | 845 | return buf.Bytes() |