theme.go
package main
import (
"fmt"
"hash/fnv"
"strings"
)
// Hostname-derived theming: each host hashes its name to an accent color,
// so several vitrines open in browser tabs are distinguishable at a glance.
// The theme name also appears next to the site name in the nav,
// because color alone must not carry the meaning
// (colorblind users, and nearby hues look alike anyway).
// theme is one host's visual identity, computed once at startup.
type theme struct {
Name string // the seed itself, shown in the nav next to the site name
CSS []byte // ":root" accent override, appended after the stylesheet by the /-/style.css handler
}
func newTheme(seed string) theme {
hue := themeHue(seed)
return theme{
Name: seed,
CSS: []byte(fmt.Sprintf(":root { --accent: hsl(%d 70%% %d%%); }\n", hue, themeLightness(hue))),
}
}
// hostLabel extracts the theme seed from a hostname: the first dot-segment,
// so both "langoustine" and "langoustine.exe.xyz" yield "langoustine".
// (The first segment is the machine-distinct one; the tail is shared domain.)
func hostLabel(hostname string) string {
label, _, _ := strings.Cut(hostname, ".")
return label
}
// themeHue hashes a theme seed to a hue in [0, 360) via FNV-1a.
// Deterministic and pinned by tests:
// changing this function silently repaints every deployed host.
func themeHue(seed string) int {
h := fnv.New32a()
h.Write([]byte(seed))
return int(h.Sum32() % 360)
}
// themeLightness picks the accent lightness (percent) for a hue.
// The accent is used for text (h1, links) on the near-white --paper background,
// and at 70% saturation a fixed 35% lightness fails WCAG AA (4.5:1)
// for the whole yellow-green-cyan band: measured, hues 41-191 fall short,
// bottoming out around 3.0:1 at hue 60.
// Dropping that band (with a little margin) to 26% lightness
// brings the worst case over the whole wheel up to ~5.06:1 (at hue 34, just outside the band);
// all hues outside the band clear 4.5:1 at 35%.
func themeLightness(hue int) int {
if hue >= 35 && hue <= 200 {
return 26
}
return 35
}
theme.go · 2.0 KiB · a4fa43a129