vitrine

display: themes are made of colors; hue is a doorway

commit 8027399b529cf09e712cf6c4f68347419b98aca6 · parent a92ee56422 · browse this tree
Ximianth, 2026-10-30 17:27:03 UTC

NewThemeColors is the ground-level constructor (an explicit accent pair,
one color per scheme, verbatim).  AccentColors exposes the hue-to-pair
tuning, so embedders accepting a chosen hue mint colors with the same
AA guarantees as hashed seeds.  NewTheme(seed) is now just the composition
of the two.  Replaces the briefly-lived NewThemeAt from a92ee56: piping a
hue through theme construction was the wrong currency.
diff --git a/display/theme.go b/display/theme.go
index d65cc11..4677862 100644
--- a/display/theme.go
+++ b/display/theme.go
@@ -19,20 +19,31 @@ type Theme struct {
 }
 
 func NewTheme(seed string) Theme {
-	return NewThemeAt(seed, ThemeHue(seed))
+	light, dark := AccentColors(ThemeHue(seed))
+	return NewThemeColors(seed, light, dark)
 }
 
-// NewThemeAt builds a theme at an explicitly chosen hue, for embedders whose users
-// overrule the seed's draw (the hash diversifies nicely, but it cannot be argued with).
-// The per-scheme lightness tuning still applies, so a chosen hue keeps the same
-// contrast guarantees as a hashed one.
-func NewThemeAt(name string, hue int) Theme {
+// AccentColors turns a hue into the accent pair this package would wear at it:
+// one color per scheme, with the lightness tuning that keeps the accent AA-legible
+// as text on each scheme's paper.  Exported so embedders can accept a chosen hue
+// (the hash diversifies nicely, but it cannot be argued with) and still mint
+// colors with the same guarantees -- colors, not hues, are what themes are made of.
+func AccentColors(hue int) (light, dark string) {
+	return fmt.Sprintf("hsl(%d 70%% %d%%)", hue, themeLightness(hue)),
+		fmt.Sprintf("hsl(%d 70%% %d%%)", hue, themeLightnessDark)
+}
+
+// NewThemeColors builds a theme from an explicit accent pair, one color per scheme.
+// This is the ground-level constructor: every theme is colors in the end.
+// No tuning applies here -- whoever picks exact colors owns their contrast --
+// and the values land in CSS verbatim, so validating them is the caller's business.
+func NewThemeColors(name, light, dark string) Theme {
 	return Theme{
 		Name: name,
 		CSS: []byte(fmt.Sprintf(
-			":root { --accent: hsl(%d 70%% %d%%); }\n"+
-				"@media (prefers-color-scheme: dark) { :root { --accent: hsl(%d 70%% %d%%); } }\n",
-			hue, themeLightness(hue), hue, themeLightnessDark)),
+			":root { --accent: %s; }\n"+
+				"@media (prefers-color-scheme: dark) { :root { --accent: %s; } }\n",
+			light, dark)),
 	}
 }