fix(theme): strengthen active grayscale session contrast
- commit
- cfdaba61e8d86928bab119829a9297b4850a2903
- author
- brice · · 2026-10-05 11:58 UTC
- parent
- 93f7e39
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
5 files · +44 -5
M internal/usecase/daemon/theme_integration_test.go +2 -2
| @@ -161,12 +161,12 @@ func TestAccentLifecycleNeutralChromeByteSnapshots(t *testing.T) { | |||
| 161 | 161 | { | |
| 162 | 162 | name: "forced dark", | |
| 163 | 163 | config: domain.Config{Theme: domain.ThemeDark}, | |
| 164 | - | want: "\x1b[1;1H\x1b[0;38;2;216;216;216;48;2;47;47;47mABC\x1b[0;38;2;216;216;216;48;2;72;72;72mD\x1b[0;38;2;139;139;139mEF\x1b[0m", | |
| 164 | + | want: "\x1b[1;1H\x1b[0;38;2;216;216;216;48;2;47;47;47mABC\x1b[0;38;2;24;24;24;48;2;216;216;216mD\x1b[0;38;2;139;139;139mEF\x1b[0m", | |
| 165 | 165 | }, | |
| 166 | 166 | { | |
| 167 | 167 | name: "forced light", | |
| 168 | 168 | config: domain.Config{Theme: domain.ThemeLight}, | |
| 169 | - | want: "\x1b[1;1H\x1b[0;38;2;32;32;32;48;2;222;222;222mABC\x1b[0;38;2;32;32;32;48;2;194;194;194mD\x1b[0;38;2;118;118;118mEF\x1b[0m", | |
| 169 | + | want: "\x1b[1;1H\x1b[0;38;2;32;32;32;48;2;222;222;222mABC\x1b[0;38;2;248;248;248;48;2;32;32;32mD\x1b[0;38;2;118;118;118mEF\x1b[0m", | |
| 170 | 170 | }, | |
| 171 | 171 | } | |
| 172 | 172 | for _, tt := range tests { | |
| @@ -161,12 +161,12 @@ func TestAccentLifecycleNeutralChromeByteSnapshots(t *testing.T) { | |||||
| 161 | { | 161 | { | ||
| 162 | name: "forced dark", | 162 | name: "forced dark", | ||
| 163 | config: domain.Config{Theme: domain.ThemeDark}, | 163 | config: domain.Config{Theme: domain.ThemeDark}, | ||
| 164 | - | want: "\x1b[1;1H\x1b[0;38;2;216;216;216;48;2;47;47;47mABC\x1b[0;38;2;216;216;216;48;2;72;72;72mD\x1b[0;38;2;139;139;139mEF\x1b[0m", | 164 | + | want: "\x1b[1;1H\x1b[0;38;2;216;216;216;48;2;47;47;47mABC\x1b[0;38;2;24;24;24;48;2;216;216;216mD\x1b[0;38;2;139;139;139mEF\x1b[0m", |
| 165 | }, | 165 | }, | ||
| 166 | { | 166 | { | ||
| 167 | name: "forced light", | 167 | name: "forced light", | ||
| 168 | config: domain.Config{Theme: domain.ThemeLight}, | 168 | config: domain.Config{Theme: domain.ThemeLight}, | ||
| 169 | - | want: "\x1b[1;1H\x1b[0;38;2;32;32;32;48;2;222;222;222mABC\x1b[0;38;2;32;32;32;48;2;194;194;194mD\x1b[0;38;2;118;118;118mEF\x1b[0m", | 169 | + | want: "\x1b[1;1H\x1b[0;38;2;32;32;32;48;2;222;222;222mABC\x1b[0;38;2;248;248;248;48;2;32;32;32mD\x1b[0;38;2;118;118;118mEF\x1b[0m", |
| 170 | }, | 170 | }, | ||
| 171 | } | 171 | } | ||
| 172 | for _, tt := range tests { | 172 | for _, tt := range tests { | ||
A internal/usecase/theme/neutral_session_test.go +34 -0
| @@ -0,0 +1,34 @@ | |||
| 1 | + | package theme | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "testing" | |
| 5 | + | ||
| 6 | + | renderer "github.com/bnema/vev-vt" | |
| 7 | + | "github.com/stretchr/testify/require" | |
| 8 | + | ) | |
| 9 | + | ||
| 10 | + | func TestNeutralSessionSurfaceHierarchy(t *testing.T) { | |
| 11 | + | black := renderer.RGB{} | |
| 12 | + | white := renderer.RGB{R: 255, G: 255, B: 255} | |
| 13 | + | for _, tt := range []struct { | |
| 14 | + | name string | |
| 15 | + | foreground, background renderer.RGB | |
| 16 | + | }{ | |
| 17 | + | {name: "dark", foreground: white, background: black}, | |
| 18 | + | {name: "light", foreground: black, background: white}, | |
| 19 | + | } { | |
| 20 | + | t.Run(tt.name, func(t *testing.T) { | |
| 21 | + | theme := Theme{Known: true, Foreground: tt.foreground, Background: tt.background, HasFG: true, HasBG: true} | |
| 22 | + | styles := neutralStyles(theme) | |
| 23 | + | require.Equal(t, tt.foreground, styles.SurfaceActive.BackgroundRGB) | |
| 24 | + | require.Equal(t, tt.background, styles.SurfaceActive.ForegroundRGB) | |
| 25 | + | require.Equal(t, styles.TabActive.BackgroundRGB, styles.SurfaceActive.BackgroundRGB) | |
| 26 | + | for count := 1; count <= 9; count++ { | |
| 27 | + | for index := range count { | |
| 28 | + | want := Blend(styles.SurfaceActive.BackgroundRGB, styles.SurfaceBar.BackgroundRGB, 1-mruWeight(index, count)) | |
| 29 | + | require.Equal(t, want, styles.MRUStyle(index, count).BackgroundRGB) | |
| 30 | + | } | |
| 31 | + | } | |
| 32 | + | }) | |
| 33 | + | } | |
| 34 | + | } | |
| @@ -0,0 +1,34 @@ | |||||
| 1 | + | package theme | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "testing" | |||
| 5 | + | ||||
| 6 | + | renderer "github.com/bnema/vev-vt" | |||
| 7 | + | "github.com/stretchr/testify/require" | |||
| 8 | + | ) | |||
| 9 | + | ||||
| 10 | + | func TestNeutralSessionSurfaceHierarchy(t *testing.T) { | |||
| 11 | + | black := renderer.RGB{} | |||
| 12 | + | white := renderer.RGB{R: 255, G: 255, B: 255} | |||
| 13 | + | for _, tt := range []struct { | |||
| 14 | + | name string | |||
| 15 | + | foreground, background renderer.RGB | |||
| 16 | + | }{ | |||
| 17 | + | {name: "dark", foreground: white, background: black}, | |||
| 18 | + | {name: "light", foreground: black, background: white}, | |||
| 19 | + | } { | |||
| 20 | + | t.Run(tt.name, func(t *testing.T) { | |||
| 21 | + | theme := Theme{Known: true, Foreground: tt.foreground, Background: tt.background, HasFG: true, HasBG: true} | |||
| 22 | + | styles := neutralStyles(theme) | |||
| 23 | + | require.Equal(t, tt.foreground, styles.SurfaceActive.BackgroundRGB) | |||
| 24 | + | require.Equal(t, tt.background, styles.SurfaceActive.ForegroundRGB) | |||
| 25 | + | require.Equal(t, styles.TabActive.BackgroundRGB, styles.SurfaceActive.BackgroundRGB) | |||
| 26 | + | for count := 1; count <= 9; count++ { | |||
| 27 | + | for index := range count { | |||
| 28 | + | want := Blend(styles.SurfaceActive.BackgroundRGB, styles.SurfaceBar.BackgroundRGB, 1-mruWeight(index, count)) | |||
| 29 | + | require.Equal(t, want, styles.MRUStyle(index, count).BackgroundRGB) | |||
| 30 | + | } | |||
| 31 | + | } | |||
| 32 | + | }) | |||
| 33 | + | } | |||
| 34 | + | } | |||
M internal/usecase/theme/phase3_review_test.go +2 -1
| @@ -120,7 +120,8 @@ func TestLegacyAliasesPaletteOffAndForcedThemesRemainExact(t *testing.T) { | |||
| 120 | 120 | // PickerSeparator is now a semantic role with the SurfaceBar | |
| 121 | 121 | // background; it was never a compatibility alias. | |
| 122 | 122 | delete(want, "PickerSeparator") | |
| 123 | - | // Active neutral tab names use a high-contrast history anchor. | |
| 123 | + | // Active neutral sessions and tab names share the history anchor. | |
| 124 | + | want["Accent"] = rgbSurface(tt.theme.Background, tt.theme.Foreground) | |
| 124 | 125 | want["TabNameActive"] = EmphasisStyle(rgbSurface(tt.theme.Background, tt.theme.Foreground), tt.theme) | |
| 125 | 126 | for name, style := range want { | |
| 126 | 127 | require.Equalf(t, style, legacyAndSemanticStyles(got)[name], "%s", name) | |
| @@ -120,7 +120,8 @@ func TestLegacyAliasesPaletteOffAndForcedThemesRemainExact(t *testing.T) { | |||||
| 120 | // PickerSeparator is now a semantic role with the SurfaceBar | 120 | // PickerSeparator is now a semantic role with the SurfaceBar | ||
| 121 | // background; it was never a compatibility alias. | 121 | // background; it was never a compatibility alias. | ||
| 122 | delete(want, "PickerSeparator") | 122 | delete(want, "PickerSeparator") | ||
| 123 | - | // Active neutral tab names use a high-contrast history anchor. | 123 | + | // Active neutral sessions and tab names share the history anchor. |
| 124 | + | want["Accent"] = rgbSurface(tt.theme.Background, tt.theme.Foreground) | |||
| 124 | want["TabNameActive"] = EmphasisStyle(rgbSurface(tt.theme.Background, tt.theme.Foreground), tt.theme) | 125 | want["TabNameActive"] = EmphasisStyle(rgbSurface(tt.theme.Background, tt.theme.Foreground), tt.theme) | ||
| 125 | for name, style := range want { | 126 | for name, style := range want { | ||
| 126 | require.Equalf(t, style, legacyAndSemanticStyles(got)[name], "%s", name) | 127 | require.Equalf(t, style, legacyAndSemanticStyles(got)[name], "%s", name) | ||
M internal/usecase/theme/resolved_styles.go +5 -1
| @@ -162,7 +162,11 @@ func neutralStyles(t Theme) Styles { | |||
| 162 | 162 | styles.PaletteDesc = legacyMutedText(t) | |
| 163 | 163 | styles = withMRUStyles(styles, Ramp{SurfaceBar: styles.SurfaceBar, SurfaceRecent: styles.SurfaceRecent}) | |
| 164 | 164 | if usable(t) { | |
| 165 | - | styles.TabActive = EmphasisStyle(rgbSurface(t.Background, t.Foreground), t) | |
| 165 | + | // Sessions share the full-strength neutral endpoint with active tabs; | |
| 166 | + | // overlay selections retain their softer neutral surface. | |
| 167 | + | styles.SurfaceActive = rgbSurface(t.Background, t.Foreground) | |
| 168 | + | styles.Accent = styles.SurfaceActive | |
| 169 | + | styles.TabActive = EmphasisStyle(styles.SurfaceActive, t) | |
| 166 | 170 | styles.TabNameActive = styles.TabActive | |
| 167 | 171 | // Without a palette accent the history uses the same mruWeight | |
| 168 | 172 | // placement as MRUStyle, between the active tab and the bar. | |
| @@ -162,7 +162,11 @@ func neutralStyles(t Theme) Styles { | |||||
| 162 | styles.PaletteDesc = legacyMutedText(t) | 162 | styles.PaletteDesc = legacyMutedText(t) | ||
| 163 | styles = withMRUStyles(styles, Ramp{SurfaceBar: styles.SurfaceBar, SurfaceRecent: styles.SurfaceRecent}) | 163 | styles = withMRUStyles(styles, Ramp{SurfaceBar: styles.SurfaceBar, SurfaceRecent: styles.SurfaceRecent}) | ||
| 164 | if usable(t) { | 164 | if usable(t) { | ||
| 165 | - | styles.TabActive = EmphasisStyle(rgbSurface(t.Background, t.Foreground), t) | 165 | + | // Sessions share the full-strength neutral endpoint with active tabs; |
| 166 | + | // overlay selections retain their softer neutral surface. | |||
| 167 | + | styles.SurfaceActive = rgbSurface(t.Background, t.Foreground) | |||
| 168 | + | styles.Accent = styles.SurfaceActive | |||
| 169 | + | styles.TabActive = EmphasisStyle(styles.SurfaceActive, t) | |||
| 166 | styles.TabNameActive = styles.TabActive | 170 | styles.TabNameActive = styles.TabActive | ||
| 167 | // Without a palette accent the history uses the same mruWeight | 171 | // Without a palette accent the history uses the same mruWeight | ||
| 168 | // placement as MRUStyle, between the active tab and the bar. | 172 | // placement as MRUStyle, between the active tab and the bar. | ||
M internal/usecase/theme/theme_test.go +1 -1
| @@ -592,7 +592,7 @@ func TestStyleHelpersFallbackAndThemed(t *testing.T) { | |||
| 592 | 592 | t.Fatalf("status themed=%+v", status) | |
| 593 | 593 | } | |
| 594 | 594 | accent := AccentStyle(theme) | |
| 595 | - | if accent.Inverse || !accent.HasBackgroundRGB || accent.BackgroundRGB != (renderer.RGB{R: 58, G: 65, B: 73}) { | |
| 595 | + | if accent.Inverse || !accent.HasBackgroundRGB || accent.BackgroundRGB != theme.Foreground || accent.ForegroundRGB != theme.Background { | |
| 596 | 596 | t.Fatalf("accent themed=%+v", accent) | |
| 597 | 597 | } | |
| 598 | 598 | border := BorderStyle(theme) | |
| @@ -592,7 +592,7 @@ func TestStyleHelpersFallbackAndThemed(t *testing.T) { | |||||
| 592 | t.Fatalf("status themed=%+v", status) | 592 | t.Fatalf("status themed=%+v", status) | ||
| 593 | } | 593 | } | ||
| 594 | accent := AccentStyle(theme) | 594 | accent := AccentStyle(theme) | ||
| 595 | - | if accent.Inverse || !accent.HasBackgroundRGB || accent.BackgroundRGB != (renderer.RGB{R: 58, G: 65, B: 73}) { | 595 | + | if accent.Inverse || !accent.HasBackgroundRGB || accent.BackgroundRGB != theme.Foreground || accent.ForegroundRGB != theme.Background { |
| 596 | t.Fatalf("accent themed=%+v", accent) | 596 | t.Fatalf("accent themed=%+v", accent) | ||
| 597 | } | 597 | } | ||
| 598 | border := BorderStyle(theme) | 598 | border := BorderStyle(theme) | ||