fix(theme): keep stacked pane titles and symbolic icons readable
- commit
- 05565b913e55f74a54e3fe105a6e762557b51eb7
- author
- bnema · · 2026-09-04 21:43 UTC
- parent
- 28a2549
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
2 files · +45 -13
M internal/ui/theme/css.go +6 -13
| @@ -745,28 +745,21 @@ func generateStackedPaneCSS(p Palette) string { | |||
| 745 | 745 | /* The title bar is now a Box with GestureClick, not wrapped in a button */ | |
| 746 | 746 | .stacked-pane-titlebar { | |
| 747 | 747 | background-color: var(--surface-variant); | |
| 748 | + | color: var(--control-text); | |
| 748 | 749 | border-bottom: 0.0625em solid var(--border); | |
| 749 | 750 | padding: 0.25em 0.5em; | |
| 750 | 751 | min-height: 1.5em; | |
| 751 | 752 | } | |
| 752 | 753 | ||
| 753 | - | /* Clickable title bar - hover styling */ | |
| 754 | - | .stacked-pane-titlebar.stacked-pane-title-clickable { | |
| 755 | - | transition: background-color 150ms ease-in-out; | |
| 756 | - | } | |
| 757 | - | ||
| 758 | - | .stacked-pane-titlebar.stacked-pane-title-clickable:hover { | |
| 759 | - | background-color: shade(var(--surface-variant), 1.15); | |
| 760 | - | } | |
| 761 | - | ||
| 762 | - | /* Title bar content box */ | |
| 754 | + | /* Keep the background stable so --control-text retains its contrast. | |
| 755 | + | * Indicate hover with an underline and the active pane with an accent border. */ | |
| 763 | 756 | .stacked-pane-titlebar.active { | |
| 764 | - | background-color: shade(var(--surface-variant), 1.2); | |
| 765 | 757 | border-left: 0.1875em solid var(--accent); | |
| 766 | 758 | } | |
| 767 | 759 | ||
| 768 | 760 | /* Favicon image in title bar */ | |
| 769 | 761 | .stacked-pane-titlebar image { | |
| 762 | + | color: var(--control-text); | |
| 770 | 763 | min-width: 1em; | |
| 771 | 764 | min-height: 1em; | |
| 772 | 765 | margin-right: 0.375em; | |
| @@ -774,14 +767,14 @@ func generateStackedPaneCSS(p Palette) string { | |||
| 774 | 767 | ||
| 775 | 768 | /* Title text in title bar */ | |
| 776 | 769 | .stacked-pane-titlebar label { | |
| 777 | - | color: var(--text); | |
| 770 | + | color: var(--control-text); | |
| 778 | 771 | font-size: 0.75em; | |
| 779 | 772 | font-weight: 400; | |
| 780 | 773 | } | |
| 781 | 774 | ||
| 782 | 775 | /* Hover effect on title bar label */ | |
| 783 | 776 | .stacked-pane-titlebar.stacked-pane-title-clickable:hover label { | |
| 784 | - | color: var(--accent); | |
| 777 | + | text-decoration-line: underline; | |
| 785 | 778 | } | |
| 786 | 779 | ||
| 787 | 780 | /* Close button in stacked pane title bar */ | |
| @@ -745,28 +745,21 @@ func generateStackedPaneCSS(p Palette) string { | |||||
| 745 | /* The title bar is now a Box with GestureClick, not wrapped in a button */ | 745 | /* The title bar is now a Box with GestureClick, not wrapped in a button */ | ||
| 746 | .stacked-pane-titlebar { | 746 | .stacked-pane-titlebar { | ||
| 747 | background-color: var(--surface-variant); | 747 | background-color: var(--surface-variant); | ||
| 748 | + | color: var(--control-text); | |||
| 748 | border-bottom: 0.0625em solid var(--border); | 749 | border-bottom: 0.0625em solid var(--border); | ||
| 749 | padding: 0.25em 0.5em; | 750 | padding: 0.25em 0.5em; | ||
| 750 | min-height: 1.5em; | 751 | min-height: 1.5em; | ||
| 751 | } | 752 | } | ||
| 752 | 753 | ||||
| 753 | - | /* Clickable title bar - hover styling */ | 754 | + | /* Keep the background stable so --control-text retains its contrast. |
| 754 | - | .stacked-pane-titlebar.stacked-pane-title-clickable { | 755 | + | * Indicate hover with an underline and the active pane with an accent border. */ |
| 755 | - | transition: background-color 150ms ease-in-out; | |||
| 756 | - | } | |||
| 757 | - | ||||
| 758 | - | .stacked-pane-titlebar.stacked-pane-title-clickable:hover { | |||
| 759 | - | background-color: shade(var(--surface-variant), 1.15); | |||
| 760 | - | } | |||
| 761 | - | ||||
| 762 | - | /* Title bar content box */ | |||
| 763 | .stacked-pane-titlebar.active { | 756 | .stacked-pane-titlebar.active { | ||
| 764 | - | background-color: shade(var(--surface-variant), 1.2); | |||
| 765 | border-left: 0.1875em solid var(--accent); | 757 | border-left: 0.1875em solid var(--accent); | ||
| 766 | } | 758 | } | ||
| 767 | 759 | ||||
| 768 | /* Favicon image in title bar */ | 760 | /* Favicon image in title bar */ | ||
| 769 | .stacked-pane-titlebar image { | 761 | .stacked-pane-titlebar image { | ||
| 762 | + | color: var(--control-text); | |||
| 770 | min-width: 1em; | 763 | min-width: 1em; | ||
| 771 | min-height: 1em; | 764 | min-height: 1em; | ||
| 772 | margin-right: 0.375em; | 765 | margin-right: 0.375em; | ||
| @@ -774,14 +767,14 @@ func generateStackedPaneCSS(p Palette) string { | |||||
| 774 | 767 | ||||
| 775 | /* Title text in title bar */ | 768 | /* Title text in title bar */ | ||
| 776 | .stacked-pane-titlebar label { | 769 | .stacked-pane-titlebar label { | ||
| 777 | - | color: var(--text); | 770 | + | color: var(--control-text); |
| 778 | font-size: 0.75em; | 771 | font-size: 0.75em; | ||
| 779 | font-weight: 400; | 772 | font-weight: 400; | ||
| 780 | } | 773 | } | ||
| 781 | 774 | ||||
| 782 | /* Hover effect on title bar label */ | 775 | /* Hover effect on title bar label */ | ||
| 783 | .stacked-pane-titlebar.stacked-pane-title-clickable:hover label { | 776 | .stacked-pane-titlebar.stacked-pane-title-clickable:hover label { | ||
| 784 | - | color: var(--accent); | 777 | + | text-decoration-line: underline; |
| 785 | } | 778 | } | ||
| 786 | 779 | ||||
| 787 | /* Close button in stacked pane title bar */ | 780 | /* Close button in stacked pane title bar */ | ||
M internal/ui/theme/css_test.go +39 -0
| @@ -8,6 +8,45 @@ import ( | |||
| 8 | 8 | "github.com/stretchr/testify/assert" | |
| 9 | 9 | ) | |
| 10 | 10 | ||
| 11 | + | func TestGenerateCSS_StackedTitlebarUsesReadableControlText(t *testing.T) { | |
| 12 | + | tests := []struct { | |
| 13 | + | name string | |
| 14 | + | background string | |
| 15 | + | text string | |
| 16 | + | want string | |
| 17 | + | }{ | |
| 18 | + | {"dark background", "#775f79", "#3d303f", "#ffffff"}, | |
| 19 | + | {"light background", "#f0f0f0", "#dddddd", "#000000"}, | |
| 20 | + | {"readable configured text", "#282828", "#eeeeee", "#eeeeee"}, | |
| 21 | + | } | |
| 22 | + | for _, tt := range tests { | |
| 23 | + | t.Run(tt.name, func(t *testing.T) { | |
| 24 | + | palette := DefaultLightPalette() | |
| 25 | + | palette.SurfaceVariant = tt.background | |
| 26 | + | palette.Text = tt.text | |
| 27 | + | css := GenerateCSS(palette) | |
| 28 | + | ||
| 29 | + | assert.Contains(t, css, "--control-text: "+tt.want+";") | |
| 30 | + | for _, selector := range []string{ | |
| 31 | + | ".stacked-pane-titlebar", | |
| 32 | + | ".stacked-pane-titlebar label", | |
| 33 | + | ".stacked-pane-titlebar image", | |
| 34 | + | } { | |
| 35 | + | block := regexp.MustCompile(regexp.QuoteMeta(selector) + `\s*\{[^}]*\}`).FindString(css) | |
| 36 | + | assert.Contains(t, block, "color: var(--control-text);", selector) | |
| 37 | + | } | |
| 38 | + | ||
| 39 | + | // Hover and active states must not invalidate the computed contrast. | |
| 40 | + | stackedCSS := generateStackedPaneCSS(palette) | |
| 41 | + | assert.NotContains(t, stackedCSS, "shade(var(--surface-variant)") | |
| 42 | + | hover := regexp.MustCompile(`\.stacked-pane-titlebar\.stacked-pane-title-clickable:hover label\s*\{[^}]*\}`).FindString(css) | |
| 43 | + | assert.Contains(t, hover, "text-decoration-line: underline;") | |
| 44 | + | assert.NotContains(t, hover, "color:") | |
| 45 | + | assert.Contains(t, stackedCSS, "border-left: 0.1875em solid var(--accent);") | |
| 46 | + | }) | |
| 47 | + | } | |
| 48 | + | } | |
| 49 | + | ||
| 11 | 50 | func TestGenerateCSS_UsesGTKFontForNativeWidgets(t *testing.T) { | |
| 12 | 51 | css := GenerateCSSWithScaleAndFonts(DefaultDarkPalette(), 1.0, FontConfig{ | |
| 13 | 52 | SansFont: "Fira Sans", | |
| @@ -8,6 +8,45 @@ import ( | |||||
| 8 | "github.com/stretchr/testify/assert" | 8 | "github.com/stretchr/testify/assert" | ||
| 9 | ) | 9 | ) | ||
| 10 | 10 | ||||
| 11 | + | func TestGenerateCSS_StackedTitlebarUsesReadableControlText(t *testing.T) { | |||
| 12 | + | tests := []struct { | |||
| 13 | + | name string | |||
| 14 | + | background string | |||
| 15 | + | text string | |||
| 16 | + | want string | |||
| 17 | + | }{ | |||
| 18 | + | {"dark background", "#775f79", "#3d303f", "#ffffff"}, | |||
| 19 | + | {"light background", "#f0f0f0", "#dddddd", "#000000"}, | |||
| 20 | + | {"readable configured text", "#282828", "#eeeeee", "#eeeeee"}, | |||
| 21 | + | } | |||
| 22 | + | for _, tt := range tests { | |||
| 23 | + | t.Run(tt.name, func(t *testing.T) { | |||
| 24 | + | palette := DefaultLightPalette() | |||
| 25 | + | palette.SurfaceVariant = tt.background | |||
| 26 | + | palette.Text = tt.text | |||
| 27 | + | css := GenerateCSS(palette) | |||
| 28 | + | ||||
| 29 | + | assert.Contains(t, css, "--control-text: "+tt.want+";") | |||
| 30 | + | for _, selector := range []string{ | |||
| 31 | + | ".stacked-pane-titlebar", | |||
| 32 | + | ".stacked-pane-titlebar label", | |||
| 33 | + | ".stacked-pane-titlebar image", | |||
| 34 | + | } { | |||
| 35 | + | block := regexp.MustCompile(regexp.QuoteMeta(selector) + `\s*\{[^}]*\}`).FindString(css) | |||
| 36 | + | assert.Contains(t, block, "color: var(--control-text);", selector) | |||
| 37 | + | } | |||
| 38 | + | ||||
| 39 | + | // Hover and active states must not invalidate the computed contrast. | |||
| 40 | + | stackedCSS := generateStackedPaneCSS(palette) | |||
| 41 | + | assert.NotContains(t, stackedCSS, "shade(var(--surface-variant)") | |||
| 42 | + | hover := regexp.MustCompile(`\.stacked-pane-titlebar\.stacked-pane-title-clickable:hover label\s*\{[^}]*\}`).FindString(css) | |||
| 43 | + | assert.Contains(t, hover, "text-decoration-line: underline;") | |||
| 44 | + | assert.NotContains(t, hover, "color:") | |||
| 45 | + | assert.Contains(t, stackedCSS, "border-left: 0.1875em solid var(--accent);") | |||
| 46 | + | }) | |||
| 47 | + | } | |||
| 48 | + | } | |||
| 49 | + | ||||
| 11 | func TestGenerateCSS_UsesGTKFontForNativeWidgets(t *testing.T) { | 50 | func TestGenerateCSS_UsesGTKFontForNativeWidgets(t *testing.T) { | ||
| 12 | css := GenerateCSSWithScaleAndFonts(DefaultDarkPalette(), 1.0, FontConfig{ | 51 | css := GenerateCSSWithScaleAndFonts(DefaultDarkPalette(), 1.0, FontConfig{ | ||
| 13 | SansFont: "Fira Sans", | 52 | SansFont: "Fira Sans", | ||