Commit 9d96712a69

9d96712a6988b5d376162e4f31c9ee3525e1ca9a

parent: 6fdbe81e72

Verified · cmc

cmc <hello@cleberg.net> · 2026-09-18 02:29 UTC

web: stretch fix on shot images, opaque chip ground, dark-token gap surfaced

The screenshot <img> carries height="900" so display:block; width:100%
without height:auto stretched it below 1280px. The chip/badge background
mixed against transparent, so its contrast depended on the row behind it;
mixing against canvas instead makes the ground opaque, and
TestTokenContrast now measures that ground (mixHex helper) for ok, bad,
warn, done, neutral and link in both schemes. parseTokens no longer
copies a light-only colour into the dark map — a token missing from dark
is now caught by the existing missing-token loop. TestMainWidthClass's
comment described rendering pages; it checks template source text.

Ref #218
internal/web/static/style.css +2 −2
@@ -1225,7 +1225,7 @@ table.difftable td.ln a.cmt:focus { color: var(--link); text-decoration: underli
12251225 line-height: 1.5;
12261226 color: var(--chip);
12271227 border: 1px solid color-mix(in srgb, var(--chip) 40%, transparent);
1228 background: color-mix(in srgb, var(--chip) 10%, transparent);
1228 background: color-mix(in srgb, var(--chip) 10%, var(--canvas));
12291229 white-space: nowrap;
12301230 vertical-align: middle;
12311231}
@@ -1380,7 +1380,7 @@ details.refmenu .refdrop a.allrefs {
13801380.lede { font-size: var(--fs-4); color: var(--muted); line-height: 1.5; max-width: 40rem; margin: 0 0 var(--sp-5); }
13811381pre.quickstart { margin: 0 0 var(--sp-4); }
13821382.shot { border: 1px solid var(--line); border-radius: var(--r-card); overflow: hidden; margin: var(--sp-5) 0; }
1383.shot img { display: block; width: 100%; }
1383.shot img { display: block; width: 100%; height: auto; }
13841384.facets { display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--sp-5); margin: var(--sp-5) 0; }
13851385.facets h2 { font-size: var(--fs-3); margin: 0 0 var(--sp-1); }
13861386.facets p { color: var(--muted); font-size: var(--fs-2); margin: 0; }
internal/web/tokens_test.go +23 −5
@@ -1,6 +1,7 @@
11package web
22
33import (
4 "fmt"
45 "math"
56 "regexp"
67 "strconv"
@@ -45,11 +46,8 @@ func parseTokens(css []byte) (light, dark map[string]string) {
4546 }
4647 light = parse(lm[1])
4748 dark = parse(dm[1])
48 for k, v := range light {
49 if _, ok := dark[k]; !ok && strings.HasPrefix(v, "#") {
50 dark[k] = v // a light-only colour is a bug; keep it visible below
51 }
52 }
49 // A token missing from the dark block is a bug, not a fallback to the
50 // light value: the missing-token loop in TestTokenContrast reports it.
5351 return light, dark
5452}
5553
@@ -78,6 +76,18 @@ func contrastHex(a, b string) float64 {
7876 return (la + 0.05) / (lb + 0.05)
7977}
8078
79// mixHex blends two hex colours per channel, t of a and 1-t of b, matching
80// CSS's color-mix(in srgb, a <t*100>%, b).
81func mixHex(a, b string, t float64) string {
82 a, b = strings.TrimPrefix(a, "#"), strings.TrimPrefix(b, "#")
83 channel := func(ca, cb string) string {
84 na, _ := strconv.ParseUint(ca, 16, 8)
85 nb, _ := strconv.ParseUint(cb, 16, 8)
86 return fmt.Sprintf("%02x", int(math.Round(t*float64(na)+(1-t)*float64(nb))))
87 }
88 return "#" + channel(a[0:2], b[0:2]) + channel(a[2:4], b[2:4]) + channel(a[4:6], b[4:6])
89}
90
8191// TestTokenContrast is the contract for the colour tokens in style.css:
8292// every text colour clears WCAG AA on every ground it lands on, in both
8393// schemes. The hex values in the stylesheet are free to move as long as
@@ -120,6 +130,14 @@ func TestTokenContrast(t *testing.T) {
120130 t.Errorf("%s: --%s (%s) on --%s (%s) is %.2f:1, want >= %.1f", name, c.fg, scheme[c.fg], c.bg, scheme[c.bg], got, c.floor)
121131 }
122132 }
133 // The chip/badge ground is opaque (color-mix against canvas, not
134 // transparent), so its ratio does not depend on the row behind it.
135 for _, chip := range []string{"ok", "bad", "warn", "done", "neutral", "link"} {
136 ground := mixHex(scheme[chip], scheme["canvas"], 0.1)
137 if got := contrastHex(scheme[chip], ground); got < 4.5 {
138 t.Errorf("%s: chip --%s (%s) on its ground %s is %.2f:1, want >= 4.5", name, chip, scheme[chip], ground, got)
139 }
140 }
123141 // The surface ladder must be visible: canvas, surface and inset
124142 // are three grounds, not one.
125143 lc, ls, _ := luminanceHex(scheme["canvas"]), luminanceHex(scheme["surface"]), luminanceHex(scheme["inset"])
internal/web/web_test.go +3 −3
@@ -84,9 +84,9 @@ func TestWhenNamesTheZone(t *testing.T) {
8484 }
8585}
8686
87// TestMainWidthClass renders every page against an empty struct and
88// checks main carries exactly one width class, and that the pages the
89// spec calls wide or bounded say so.
87// TestMainWidthClass checks each template's source for the width define
88// the spec assigns it (wide or bounded), and that a reading page defines
89// none.
9090func TestMainWidthClass(t *testing.T) {
9191 wide := map[string]bool{"tree.html": true, "blob.html": true, "blame.html": true, "log.html": true, "commit.html": true, "compare.html": true, "builds.html": true, "build.html": true, "search.html": true, "globalsearch.html": true}
9292 bounded := map[string]bool{"landing.html": true, "login.html": true, "register.html": true, "registered.html": true, "new.html": true, "issuenew.html": true, "mrnew.html": true, "settings.html": true, "account.html": true, "admin.html": true, "edit.html": true, "snippetnew.html": true, "privacy.html": true, "404.html": true}