Merge pull request #387 from bnema/fix/noctalia-dark-input-text
fix(theme): keep dark input text readable
5 files · +111 -6
M Makefile +3 -2
| @@ -14,7 +14,7 @@ MAIN_PATH=./cmd/dumber | |||
| 14 | 14 | DIST_DIR=dist | |
| 15 | 15 | LOCAL_BIN_DIR?=$(HOME)/.local/bin | |
| 16 | 16 | TOOL_BIN_DIR?=$(shell go env GOPATH)/bin | |
| 17 | - | GOLANGCI_LINT_VERSION?=v2.12.2 | |
| 17 | + | GOLANGCI_LINT_VERSION?=v2.13.2 | |
| 18 | 18 | STATICCHECK_VERSION?=v0.7.0 | |
| 19 | 19 | ||
| 20 | 20 | # Detect number of CPU cores for parallel compilation | |
| @@ -165,7 +165,8 @@ install-golangci-lint: ## Install pinned golangci-lint | |||
| 165 | 165 | # Build the pinned version with the active Go toolchain instead of using the | |
| 166 | 166 | # upstream install.sh helper, which currently misidentifies release assets now | |
| 167 | 167 | # that golangci-lint publishes sibling .sbom.json files for each tarball. | |
| 168 | - | GOBIN=$(TOOL_BIN_DIR) go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@$(GOLANGCI_LINT_VERSION) | |
| 168 | + | # Run outside this module so Go resolves the requested tool version independently. | |
| 169 | + | cd "$${TMPDIR:-/tmp}" && GOBIN=$(TOOL_BIN_DIR) go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@$(GOLANGCI_LINT_VERSION) | |
| 169 | 170 | ||
| 170 | 171 | install-staticcheck: ## Install pinned Staticcheck with the active Go toolchain | |
| 171 | 172 | @echo "Installing staticcheck $(STATICCHECK_VERSION)..." | |
| @@ -14,7 +14,7 @@ MAIN_PATH=./cmd/dumber | |||||
| 14 | DIST_DIR=dist | 14 | DIST_DIR=dist | ||
| 15 | LOCAL_BIN_DIR?=$(HOME)/.local/bin | 15 | LOCAL_BIN_DIR?=$(HOME)/.local/bin | ||
| 16 | TOOL_BIN_DIR?=$(shell go env GOPATH)/bin | 16 | TOOL_BIN_DIR?=$(shell go env GOPATH)/bin | ||
| 17 | - | GOLANGCI_LINT_VERSION?=v2.12.2 | 17 | + | GOLANGCI_LINT_VERSION?=v2.13.2 |
| 18 | STATICCHECK_VERSION?=v0.7.0 | 18 | STATICCHECK_VERSION?=v0.7.0 | ||
| 19 | 19 | ||||
| 20 | # Detect number of CPU cores for parallel compilation | 20 | # Detect number of CPU cores for parallel compilation | ||
| @@ -165,7 +165,8 @@ install-golangci-lint: ## Install pinned golangci-lint | |||||
| 165 | # Build the pinned version with the active Go toolchain instead of using the | 165 | # Build the pinned version with the active Go toolchain instead of using the | ||
| 166 | # upstream install.sh helper, which currently misidentifies release assets now | 166 | # upstream install.sh helper, which currently misidentifies release assets now | ||
| 167 | # that golangci-lint publishes sibling .sbom.json files for each tarball. | 167 | # that golangci-lint publishes sibling .sbom.json files for each tarball. | ||
| 168 | - | GOBIN=$(TOOL_BIN_DIR) go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@$(GOLANGCI_LINT_VERSION) | 168 | + | # Run outside this module so Go resolves the requested tool version independently. |
| 169 | + | cd "$${TMPDIR:-/tmp}" && GOBIN=$(TOOL_BIN_DIR) go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@$(GOLANGCI_LINT_VERSION) | |||
| 169 | 170 | ||||
| 170 | install-staticcheck: ## Install pinned Staticcheck with the active Go toolchain | 171 | install-staticcheck: ## Install pinned Staticcheck with the active Go toolchain | ||
| 171 | @echo "Installing staticcheck $(STATICCHECK_VERSION)..." | 172 | @echo "Installing staticcheck $(STATICCHECK_VERSION)..." | ||
M internal/ui/theme/css.go +2 -2
| @@ -287,7 +287,7 @@ entry.omnibox-entry, | |||
| 287 | 287 | entry.omnibox-entry > text { | |
| 288 | 288 | background-color: var(--surface-variant); | |
| 289 | 289 | background-image: none; | |
| 290 | - | color: var(--text); | |
| 290 | + | color: var(--control-text); | |
| 291 | 291 | caret-color: var(--accent); | |
| 292 | 292 | } | |
| 293 | 293 | ||
| @@ -504,7 +504,7 @@ entry.omnibox-entry.omnibox-entry-bang-active:focus-visible > text { | |||
| 504 | 504 | /* Ghost text autocomplete overlay - positioned to align with entry text */ | |
| 505 | 505 | .omnibox-ghost { | |
| 506 | 506 | font-size: 1em; | |
| 507 | - | color: var(--muted); | |
| 507 | + | color: var(--control-text); | |
| 508 | 508 | margin-top: -0.0625em; /* Nudge up 1px to align with entry text baseline */ | |
| 509 | 509 | background-color: transparent; | |
| 510 | 510 | } | |
| @@ -287,7 +287,7 @@ entry.omnibox-entry, | |||||
| 287 | entry.omnibox-entry > text { | 287 | entry.omnibox-entry > text { | ||
| 288 | background-color: var(--surface-variant); | 288 | background-color: var(--surface-variant); | ||
| 289 | background-image: none; | 289 | background-image: none; | ||
| 290 | - | color: var(--text); | 290 | + | color: var(--control-text); |
| 291 | caret-color: var(--accent); | 291 | caret-color: var(--accent); | ||
| 292 | } | 292 | } | ||
| 293 | 293 | ||||
| @@ -504,7 +504,7 @@ entry.omnibox-entry.omnibox-entry-bang-active:focus-visible > text { | |||||
| 504 | /* Ghost text autocomplete overlay - positioned to align with entry text */ | 504 | /* Ghost text autocomplete overlay - positioned to align with entry text */ | ||
| 505 | .omnibox-ghost { | 505 | .omnibox-ghost { | ||
| 506 | font-size: 1em; | 506 | font-size: 1em; | ||
| 507 | - | color: var(--muted); | 507 | + | color: var(--control-text); |
| 508 | margin-top: -0.0625em; /* Nudge up 1px to align with entry text baseline */ | 508 | margin-top: -0.0625em; /* Nudge up 1px to align with entry text baseline */ | ||
| 509 | background-color: transparent; | 509 | background-color: transparent; | ||
| 510 | } | 510 | } | ||
M internal/ui/theme/css_test.go +5 -2
| @@ -118,10 +118,13 @@ func TestGenerateCSS_OmniboxSearchEntryUsesControlSurface(t *testing.T) { | |||
| 118 | 118 | if !containerRe.MatchString(css) { | |
| 119 | 119 | t.Fatalf("expected omnibox container to use the header surface color") | |
| 120 | 120 | } | |
| 121 | - | entryRe := regexp.MustCompile(`(?s)entry\.omnibox-entry\s*,\s*entry\.omnibox-entry\s*>\s*text\s*\{[^}]*background-color:\s*var\(--surface-variant\);[^}]*background-image:\s*none;`) | |
| 121 | + | entryRe := regexp.MustCompile(`(?s)entry\.omnibox-entry\s*,\s*entry\.omnibox-entry\s*>\s*text\s*\{[^}]*background-color:\s*var\(--surface-variant\);[^}]*background-image:\s*none;[^}]*color:\s*var\(--control-text\);`) | |
| 122 | 122 | if !entryRe.MatchString(css) { | |
| 123 | - | t.Fatalf("expected omnibox entry and its text node to use the control surface fill") | |
| 123 | + | t.Fatalf("expected omnibox entry and its text node to use the control surface fill and readable text") | |
| 124 | 124 | } | |
| 125 | + | ghostBlock := cssRuleBlock(t, css, ".omnibox-ghost") | |
| 126 | + | assert.Contains(t, ghostBlock, "color: var(--control-text);") | |
| 127 | + | assert.NotContains(t, ghostBlock, "alpha(var(--control-text)") | |
| 125 | 128 | focusedEntryRe := regexp.MustCompile(`(?s)entry\.omnibox-entry:focus[^\{]*,\s*entry\.omnibox-entry:focus-within[^\{]*,\s*entry\.omnibox-entry:focus-visible[^\{]*,\s*entry\.omnibox-entry:focus\s*>\s*text[^\{]*,\s*entry\.omnibox-entry:focus-within\s*>\s*text[^\{]*,\s*entry\.omnibox-entry:focus-visible\s*>\s*text\s*\{[^}]*background-color:\s*shade\(var\(--surface-variant\),\s*1\.08\);`) | |
| 126 | 129 | if !focusedEntryRe.MatchString(css) { | |
| 127 | 130 | t.Fatalf("expected focused omnibox entry text node to keep a distinct control-surface fill") | |
| @@ -118,10 +118,13 @@ func TestGenerateCSS_OmniboxSearchEntryUsesControlSurface(t *testing.T) { | |||||
| 118 | if !containerRe.MatchString(css) { | 118 | if !containerRe.MatchString(css) { | ||
| 119 | t.Fatalf("expected omnibox container to use the header surface color") | 119 | t.Fatalf("expected omnibox container to use the header surface color") | ||
| 120 | } | 120 | } | ||
| 121 | - | entryRe := regexp.MustCompile(`(?s)entry\.omnibox-entry\s*,\s*entry\.omnibox-entry\s*>\s*text\s*\{[^}]*background-color:\s*var\(--surface-variant\);[^}]*background-image:\s*none;`) | 121 | + | entryRe := regexp.MustCompile(`(?s)entry\.omnibox-entry\s*,\s*entry\.omnibox-entry\s*>\s*text\s*\{[^}]*background-color:\s*var\(--surface-variant\);[^}]*background-image:\s*none;[^}]*color:\s*var\(--control-text\);`) |
| 122 | if !entryRe.MatchString(css) { | 122 | if !entryRe.MatchString(css) { | ||
| 123 | - | t.Fatalf("expected omnibox entry and its text node to use the control surface fill") | 123 | + | t.Fatalf("expected omnibox entry and its text node to use the control surface fill and readable text") |
| 124 | } | 124 | } | ||
| 125 | + | ghostBlock := cssRuleBlock(t, css, ".omnibox-ghost") | |||
| 126 | + | assert.Contains(t, ghostBlock, "color: var(--control-text);") | |||
| 127 | + | assert.NotContains(t, ghostBlock, "alpha(var(--control-text)") | |||
| 125 | focusedEntryRe := regexp.MustCompile(`(?s)entry\.omnibox-entry:focus[^\{]*,\s*entry\.omnibox-entry:focus-within[^\{]*,\s*entry\.omnibox-entry:focus-visible[^\{]*,\s*entry\.omnibox-entry:focus\s*>\s*text[^\{]*,\s*entry\.omnibox-entry:focus-within\s*>\s*text[^\{]*,\s*entry\.omnibox-entry:focus-visible\s*>\s*text\s*\{[^}]*background-color:\s*shade\(var\(--surface-variant\),\s*1\.08\);`) | 128 | focusedEntryRe := regexp.MustCompile(`(?s)entry\.omnibox-entry:focus[^\{]*,\s*entry\.omnibox-entry:focus-within[^\{]*,\s*entry\.omnibox-entry:focus-visible[^\{]*,\s*entry\.omnibox-entry:focus\s*>\s*text[^\{]*,\s*entry\.omnibox-entry:focus-within\s*>\s*text[^\{]*,\s*entry\.omnibox-entry:focus-visible\s*>\s*text\s*\{[^}]*background-color:\s*shade\(var\(--surface-variant\),\s*1\.08\);`) | ||
| 126 | if !focusedEntryRe.MatchString(css) { | 129 | if !focusedEntryRe.MatchString(css) { | ||
| 127 | t.Fatalf("expected focused omnibox entry text node to keep a distinct control-surface fill") | 130 | t.Fatalf("expected focused omnibox entry text node to keep a distinct control-surface fill") | ||
M internal/ui/theme/palette.go +48 -0
| @@ -3,6 +3,7 @@ package theme | |||
| 3 | 3 | ||
| 4 | 4 | import ( | |
| 5 | 5 | "fmt" | |
| 6 | + | "math" | |
| 6 | 7 | "regexp" | |
| 7 | 8 | "strings" | |
| 8 | 9 | ||
| @@ -267,6 +268,7 @@ func (p Palette) ToCSSVars() string { | |||
| 267 | 268 | sb.WriteString(" --bg: " + p.Background + ";\n") | |
| 268 | 269 | sb.WriteString(" --surface: " + p.Surface + ";\n") | |
| 269 | 270 | sb.WriteString(" --surface-variant: " + p.SurfaceVariant + ";\n") | |
| 271 | + | sb.WriteString(" --control-text: " + readableTextColor(p.SurfaceVariant, p.Text) + ";\n") | |
| 270 | 272 | sb.WriteString(" --text: " + p.Text + ";\n") | |
| 271 | 273 | sb.WriteString(" --muted: " + p.Muted + ";\n") | |
| 272 | 274 | sb.WriteString(" --accent: " + p.Accent + ";\n") | |
| @@ -277,6 +279,52 @@ func (p Palette) ToCSSVars() string { | |||
| 277 | 279 | return sb.String() | |
| 278 | 280 | } | |
| 279 | 281 | ||
| 282 | + | const ( | |
| 283 | + | minimumReadableContrast = 4.5 | |
| 284 | + | contrastLuminanceOffset = 0.05 | |
| 285 | + | sRGBLinearThreshold = 0.04045 | |
| 286 | + | sRGBLinearDivisor = 12.92 | |
| 287 | + | sRGBGammaOffset = 0.055 | |
| 288 | + | sRGBGammaScale = 1.055 | |
| 289 | + | sRGBGammaExponent = 2.4 | |
| 290 | + | redLuminanceWeight = 0.2126 | |
| 291 | + | greenLuminanceWeight = 0.7152 | |
| 292 | + | blueLuminanceWeight = 0.0722 | |
| 293 | + | ) | |
| 294 | + | ||
| 295 | + | func readableTextColor(background, preferred string) string { | |
| 296 | + | if contrastRatio(background, preferred) >= minimumReadableContrast { | |
| 297 | + | return preferred | |
| 298 | + | } | |
| 299 | + | if contrastRatio(background, "#ffffff") >= contrastRatio(background, "#000000") { | |
| 300 | + | return "#ffffff" | |
| 301 | + | } | |
| 302 | + | return "#000000" | |
| 303 | + | } | |
| 304 | + | ||
| 305 | + | func contrastRatio(first, second string) float64 { | |
| 306 | + | firstLuminance := relativeLuminance(first) | |
| 307 | + | secondLuminance := relativeLuminance(second) | |
| 308 | + | if firstLuminance < secondLuminance { | |
| 309 | + | firstLuminance, secondLuminance = secondLuminance, firstLuminance | |
| 310 | + | } | |
| 311 | + | return (firstLuminance + contrastLuminanceOffset) / (secondLuminance + contrastLuminanceOffset) | |
| 312 | + | } | |
| 313 | + | ||
| 314 | + | func relativeLuminance(color string) float64 { | |
| 315 | + | r, g, b, _ := HexToRGBA(color) | |
| 316 | + | linearize := func(channel float32) float64 { | |
| 317 | + | value := float64(channel) | |
| 318 | + | if value <= sRGBLinearThreshold { | |
| 319 | + | return value / sRGBLinearDivisor | |
| 320 | + | } | |
| 321 | + | return math.Pow((value+sRGBGammaOffset)/sRGBGammaScale, sRGBGammaExponent) | |
| 322 | + | } | |
| 323 | + | return redLuminanceWeight*linearize(r) + | |
| 324 | + | greenLuminanceWeight*linearize(g) + | |
| 325 | + | blueLuminanceWeight*linearize(b) | |
| 326 | + | } | |
| 327 | + | ||
| 280 | 328 | // ToWebCSSVars generates CSS custom properties for web UI (Tailwind compatibility). | |
| 281 | 329 | // Maps GTK palette names to Tailwind variable names used in the WebUI homepage. | |
| 282 | 330 | func (p Palette) ToWebCSSVars() string { | |
| @@ -3,6 +3,7 @@ package theme | |||||
| 3 | 3 | ||||
| 4 | import ( | 4 | import ( | ||
| 5 | "fmt" | 5 | "fmt" | ||
| 6 | + | "math" | |||
| 6 | "regexp" | 7 | "regexp" | ||
| 7 | "strings" | 8 | "strings" | ||
| 8 | 9 | ||||
| @@ -267,6 +268,7 @@ func (p Palette) ToCSSVars() string { | |||||
| 267 | sb.WriteString(" --bg: " + p.Background + ";\n") | 268 | sb.WriteString(" --bg: " + p.Background + ";\n") | ||
| 268 | sb.WriteString(" --surface: " + p.Surface + ";\n") | 269 | sb.WriteString(" --surface: " + p.Surface + ";\n") | ||
| 269 | sb.WriteString(" --surface-variant: " + p.SurfaceVariant + ";\n") | 270 | sb.WriteString(" --surface-variant: " + p.SurfaceVariant + ";\n") | ||
| 271 | + | sb.WriteString(" --control-text: " + readableTextColor(p.SurfaceVariant, p.Text) + ";\n") | |||
| 270 | sb.WriteString(" --text: " + p.Text + ";\n") | 272 | sb.WriteString(" --text: " + p.Text + ";\n") | ||
| 271 | sb.WriteString(" --muted: " + p.Muted + ";\n") | 273 | sb.WriteString(" --muted: " + p.Muted + ";\n") | ||
| 272 | sb.WriteString(" --accent: " + p.Accent + ";\n") | 274 | sb.WriteString(" --accent: " + p.Accent + ";\n") | ||
| @@ -277,6 +279,52 @@ func (p Palette) ToCSSVars() string { | |||||
| 277 | return sb.String() | 279 | return sb.String() | ||
| 278 | } | 280 | } | ||
| 279 | 281 | ||||
| 282 | + | const ( | |||
| 283 | + | minimumReadableContrast = 4.5 | |||
| 284 | + | contrastLuminanceOffset = 0.05 | |||
| 285 | + | sRGBLinearThreshold = 0.04045 | |||
| 286 | + | sRGBLinearDivisor = 12.92 | |||
| 287 | + | sRGBGammaOffset = 0.055 | |||
| 288 | + | sRGBGammaScale = 1.055 | |||
| 289 | + | sRGBGammaExponent = 2.4 | |||
| 290 | + | redLuminanceWeight = 0.2126 | |||
| 291 | + | greenLuminanceWeight = 0.7152 | |||
| 292 | + | blueLuminanceWeight = 0.0722 | |||
| 293 | + | ) | |||
| 294 | + | ||||
| 295 | + | func readableTextColor(background, preferred string) string { | |||
| 296 | + | if contrastRatio(background, preferred) >= minimumReadableContrast { | |||
| 297 | + | return preferred | |||
| 298 | + | } | |||
| 299 | + | if contrastRatio(background, "#ffffff") >= contrastRatio(background, "#000000") { | |||
| 300 | + | return "#ffffff" | |||
| 301 | + | } | |||
| 302 | + | return "#000000" | |||
| 303 | + | } | |||
| 304 | + | ||||
| 305 | + | func contrastRatio(first, second string) float64 { | |||
| 306 | + | firstLuminance := relativeLuminance(first) | |||
| 307 | + | secondLuminance := relativeLuminance(second) | |||
| 308 | + | if firstLuminance < secondLuminance { | |||
| 309 | + | firstLuminance, secondLuminance = secondLuminance, firstLuminance | |||
| 310 | + | } | |||
| 311 | + | return (firstLuminance + contrastLuminanceOffset) / (secondLuminance + contrastLuminanceOffset) | |||
| 312 | + | } | |||
| 313 | + | ||||
| 314 | + | func relativeLuminance(color string) float64 { | |||
| 315 | + | r, g, b, _ := HexToRGBA(color) | |||
| 316 | + | linearize := func(channel float32) float64 { | |||
| 317 | + | value := float64(channel) | |||
| 318 | + | if value <= sRGBLinearThreshold { | |||
| 319 | + | return value / sRGBLinearDivisor | |||
| 320 | + | } | |||
| 321 | + | return math.Pow((value+sRGBGammaOffset)/sRGBGammaScale, sRGBGammaExponent) | |||
| 322 | + | } | |||
| 323 | + | return redLuminanceWeight*linearize(r) + | |||
| 324 | + | greenLuminanceWeight*linearize(g) + | |||
| 325 | + | blueLuminanceWeight*linearize(b) | |||
| 326 | + | } | |||
| 327 | + | ||||
| 280 | // ToWebCSSVars generates CSS custom properties for web UI (Tailwind compatibility). | 328 | // ToWebCSSVars generates CSS custom properties for web UI (Tailwind compatibility). | ||
| 281 | // Maps GTK palette names to Tailwind variable names used in the WebUI homepage. | 329 | // Maps GTK palette names to Tailwind variable names used in the WebUI homepage. | ||
| 282 | func (p Palette) ToWebCSSVars() string { | 330 | func (p Palette) ToWebCSSVars() string { | ||
A internal/ui/theme/palette_test.go +53 -0
| @@ -0,0 +1,53 @@ | |||
| 1 | + | package theme | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "strings" | |
| 5 | + | "testing" | |
| 6 | + | ) | |
| 7 | + | ||
| 8 | + | func TestReadableTextColor(t *testing.T) { | |
| 9 | + | tests := []struct { | |
| 10 | + | name string | |
| 11 | + | background string | |
| 12 | + | preferred string | |
| 13 | + | want string | |
| 14 | + | }{ | |
| 15 | + | { | |
| 16 | + | name: "keeps readable palette text", | |
| 17 | + | background: "#282828", | |
| 18 | + | preferred: "#ffffff", | |
| 19 | + | want: "#ffffff", | |
| 20 | + | }, | |
| 21 | + | { | |
| 22 | + | name: "uses light text on dark control", | |
| 23 | + | background: "#775f79", | |
| 24 | + | preferred: "#3d303f", | |
| 25 | + | want: "#ffffff", | |
| 26 | + | }, | |
| 27 | + | { | |
| 28 | + | name: "uses dark text on light control", | |
| 29 | + | background: "#f0f0f0", | |
| 30 | + | preferred: "#dddddd", | |
| 31 | + | want: "#000000", | |
| 32 | + | }, | |
| 33 | + | } | |
| 34 | + | ||
| 35 | + | for _, tt := range tests { | |
| 36 | + | t.Run(tt.name, func(t *testing.T) { | |
| 37 | + | if got := readableTextColor(tt.background, tt.preferred); got != tt.want { | |
| 38 | + | t.Fatalf("readableTextColor(%q, %q) = %q, want %q", tt.background, tt.preferred, got, tt.want) | |
| 39 | + | } | |
| 40 | + | }) | |
| 41 | + | } | |
| 42 | + | } | |
| 43 | + | ||
| 44 | + | func TestPaletteToCSSVarsAddsReadableControlText(t *testing.T) { | |
| 45 | + | palette := DefaultLightPalette() | |
| 46 | + | palette.SurfaceVariant = "#775f79" | |
| 47 | + | palette.Text = "#3d303f" | |
| 48 | + | ||
| 49 | + | css := palette.ToCSSVars() | |
| 50 | + | if !strings.Contains(css, " --control-text: #ffffff;\n") { | |
| 51 | + | t.Fatalf("ToCSSVars() did not add light control text for dark input background:\n%s", css) | |
| 52 | + | } | |
| 53 | + | } | |
| @@ -0,0 +1,53 @@ | |||||
| 1 | + | package theme | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "strings" | |||
| 5 | + | "testing" | |||
| 6 | + | ) | |||
| 7 | + | ||||
| 8 | + | func TestReadableTextColor(t *testing.T) { | |||
| 9 | + | tests := []struct { | |||
| 10 | + | name string | |||
| 11 | + | background string | |||
| 12 | + | preferred string | |||
| 13 | + | want string | |||
| 14 | + | }{ | |||
| 15 | + | { | |||
| 16 | + | name: "keeps readable palette text", | |||
| 17 | + | background: "#282828", | |||
| 18 | + | preferred: "#ffffff", | |||
| 19 | + | want: "#ffffff", | |||
| 20 | + | }, | |||
| 21 | + | { | |||
| 22 | + | name: "uses light text on dark control", | |||
| 23 | + | background: "#775f79", | |||
| 24 | + | preferred: "#3d303f", | |||
| 25 | + | want: "#ffffff", | |||
| 26 | + | }, | |||
| 27 | + | { | |||
| 28 | + | name: "uses dark text on light control", | |||
| 29 | + | background: "#f0f0f0", | |||
| 30 | + | preferred: "#dddddd", | |||
| 31 | + | want: "#000000", | |||
| 32 | + | }, | |||
| 33 | + | } | |||
| 34 | + | ||||
| 35 | + | for _, tt := range tests { | |||
| 36 | + | t.Run(tt.name, func(t *testing.T) { | |||
| 37 | + | if got := readableTextColor(tt.background, tt.preferred); got != tt.want { | |||
| 38 | + | t.Fatalf("readableTextColor(%q, %q) = %q, want %q", tt.background, tt.preferred, got, tt.want) | |||
| 39 | + | } | |||
| 40 | + | }) | |||
| 41 | + | } | |||
| 42 | + | } | |||
| 43 | + | ||||
| 44 | + | func TestPaletteToCSSVarsAddsReadableControlText(t *testing.T) { | |||
| 45 | + | palette := DefaultLightPalette() | |||
| 46 | + | palette.SurfaceVariant = "#775f79" | |||
| 47 | + | palette.Text = "#3d303f" | |||
| 48 | + | ||||
| 49 | + | css := palette.ToCSSVars() | |||
| 50 | + | if !strings.Contains(css, " --control-text: #ffffff;\n") { | |||
| 51 | + | t.Fatalf("ToCSSVars() did not add light control text for dark input background:\n%s", css) | |||
| 52 | + | } | |||
| 53 | + | } | |||