fix(theme): preserve overlay contrast and unify active tab surfaces
- commit
- 00609e34a8b330e82cbb52d7b5e9930f902eca4e
- author
- brice · · 2026-10-05 12:01 UTC
- parent
- cfdaba6
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
4 files · +12 -3
M internal/usecase/daemon/render_pipeline.go +1 -1
| @@ -335,7 +335,7 @@ func captureOverlayLayers(state *capturedRenderState, snap *overlayRenderSnapsho | |||
| 335 | 335 | modal := promptModalFor(snap.promptModel.Title()) | |
| 336 | 336 | presentation := modal.Resolve(size) | |
| 337 | 337 | o.prompt = capturedModal{active: true, title: modal.Title, presentation: presentation, focused: true} | |
| 338 | - | o.prompt.inner = snap.promptModel.RenderStyled(rectSize(presentation.Inner), prompt.RenderStyles{Base: styles.PromptBase, Selection: styles.SurfaceActive}) | |
| 338 | + | o.prompt.inner = snap.promptModel.RenderStyled(rectSize(presentation.Inner), prompt.RenderStyles{Base: styles.PromptBase, Selection: styles.Selection}) | |
| 339 | 339 | } | |
| 340 | 340 | state.cursor.hiddenByOverlay = o.active() | |
| 341 | 341 | } | |
| @@ -335,7 +335,7 @@ func captureOverlayLayers(state *capturedRenderState, snap *overlayRenderSnapsho | |||||
| 335 | modal := promptModalFor(snap.promptModel.Title()) | 335 | modal := promptModalFor(snap.promptModel.Title()) | ||
| 336 | presentation := modal.Resolve(size) | 336 | presentation := modal.Resolve(size) | ||
| 337 | o.prompt = capturedModal{active: true, title: modal.Title, presentation: presentation, focused: true} | 337 | o.prompt = capturedModal{active: true, title: modal.Title, presentation: presentation, focused: true} | ||
| 338 | - | o.prompt.inner = snap.promptModel.RenderStyled(rectSize(presentation.Inner), prompt.RenderStyles{Base: styles.PromptBase, Selection: styles.SurfaceActive}) | 338 | + | o.prompt.inner = snap.promptModel.RenderStyled(rectSize(presentation.Inner), prompt.RenderStyles{Base: styles.PromptBase, Selection: styles.Selection}) |
| 339 | } | 339 | } | ||
| 340 | state.cursor.hiddenByOverlay = o.active() | 340 | state.cursor.hiddenByOverlay = o.active() | ||
| 341 | } | 341 | } | ||
M internal/usecase/theme/neutral_session_test.go +6 -0
| @@ -23,6 +23,12 @@ func TestNeutralSessionSurfaceHierarchy(t *testing.T) { | |||
| 23 | 23 | require.Equal(t, tt.foreground, styles.SurfaceActive.BackgroundRGB) | |
| 24 | 24 | require.Equal(t, tt.background, styles.SurfaceActive.ForegroundRGB) | |
| 25 | 25 | require.Equal(t, styles.TabActive.BackgroundRGB, styles.SurfaceActive.BackgroundRGB) | |
| 26 | + | require.Equal(t, styles.SurfaceActive.BackgroundRGB, styles.TabActiveTitle.BackgroundRGB) | |
| 27 | + | soft := Blend(tt.background, tt.foreground, neutralAccentBlend) | |
| 28 | + | for _, selection := range []renderer.Style{styles.PickerSelection, styles.Selection, styles.SearchSelection} { | |
| 29 | + | require.Equal(t, soft, selection.BackgroundRGB) | |
| 30 | + | } | |
| 31 | + | require.Equal(t, Blend(styles.SurfaceBar.BackgroundRGB, tt.foreground, 0.5), styles.MRUStyle(0, 3).BackgroundRGB) | |
| 26 | 32 | for count := 1; count <= 9; count++ { | |
| 27 | 33 | for index := range count { | |
| 28 | 34 | want := Blend(styles.SurfaceActive.BackgroundRGB, styles.SurfaceBar.BackgroundRGB, 1-mruWeight(index, count)) | |
| @@ -23,6 +23,12 @@ func TestNeutralSessionSurfaceHierarchy(t *testing.T) { | |||||
| 23 | require.Equal(t, tt.foreground, styles.SurfaceActive.BackgroundRGB) | 23 | require.Equal(t, tt.foreground, styles.SurfaceActive.BackgroundRGB) | ||
| 24 | require.Equal(t, tt.background, styles.SurfaceActive.ForegroundRGB) | 24 | require.Equal(t, tt.background, styles.SurfaceActive.ForegroundRGB) | ||
| 25 | require.Equal(t, styles.TabActive.BackgroundRGB, styles.SurfaceActive.BackgroundRGB) | 25 | require.Equal(t, styles.TabActive.BackgroundRGB, styles.SurfaceActive.BackgroundRGB) | ||
| 26 | + | require.Equal(t, styles.SurfaceActive.BackgroundRGB, styles.TabActiveTitle.BackgroundRGB) | |||
| 27 | + | soft := Blend(tt.background, tt.foreground, neutralAccentBlend) | |||
| 28 | + | for _, selection := range []renderer.Style{styles.PickerSelection, styles.Selection, styles.SearchSelection} { | |||
| 29 | + | require.Equal(t, soft, selection.BackgroundRGB) | |||
| 30 | + | } | |||
| 31 | + | require.Equal(t, Blend(styles.SurfaceBar.BackgroundRGB, tt.foreground, 0.5), styles.MRUStyle(0, 3).BackgroundRGB) | |||
| 26 | for count := 1; count <= 9; count++ { | 32 | for count := 1; count <= 9; count++ { | ||
| 27 | for index := range count { | 33 | for index := range count { | ||
| 28 | want := Blend(styles.SurfaceActive.BackgroundRGB, styles.SurfaceBar.BackgroundRGB, 1-mruWeight(index, count)) | 34 | want := Blend(styles.SurfaceActive.BackgroundRGB, styles.SurfaceBar.BackgroundRGB, 1-mruWeight(index, count)) | ||
M internal/usecase/theme/phase3_review_test.go +1 -0
| @@ -123,6 +123,7 @@ func TestLegacyAliasesPaletteOffAndForcedThemesRemainExact(t *testing.T) { | |||
| 123 | 123 | // Active neutral sessions and tab names share the history anchor. | |
| 124 | 124 | want["Accent"] = rgbSurface(tt.theme.Background, tt.theme.Foreground) | |
| 125 | 125 | want["TabNameActive"] = EmphasisStyle(rgbSurface(tt.theme.Background, tt.theme.Foreground), tt.theme) | |
| 126 | + | want["TabTitleActive"] = MutedVariantStyle(rgbSurface(tt.theme.Background, tt.theme.Foreground), tt.theme) | |
| 126 | 127 | for name, style := range want { | |
| 127 | 128 | require.Equalf(t, style, legacyAndSemanticStyles(got)[name], "%s", name) | |
| 128 | 129 | } | |
| @@ -123,6 +123,7 @@ func TestLegacyAliasesPaletteOffAndForcedThemesRemainExact(t *testing.T) { | |||||
| 123 | // Active neutral sessions and tab names share the 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["Accent"] = rgbSurface(tt.theme.Background, tt.theme.Foreground) | ||
| 125 | 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) | ||
| 126 | + | want["TabTitleActive"] = MutedVariantStyle(rgbSurface(tt.theme.Background, tt.theme.Foreground), tt.theme) | |||
| 126 | for name, style := range want { | 127 | for name, style := range want { | ||
| 127 | require.Equalf(t, style, legacyAndSemanticStyles(got)[name], "%s", name) | 128 | require.Equalf(t, style, legacyAndSemanticStyles(got)[name], "%s", name) | ||
| 128 | } | 129 | } | ||
M internal/usecase/theme/resolved_styles.go +4 -2
| @@ -117,8 +117,8 @@ func indexedForeground(style renderer.Style, slot uint8) renderer.Style { | |||
| 117 | 117 | return style | |
| 118 | 118 | } | |
| 119 | 119 | ||
| 120 | - | // neutralStyles deliberately preserves the pre-accent neutral hierarchy. It | |
| 121 | - | // is used for palette-off, forced themes, incomplete defaults, and RGB-less | |
| 120 | + | // neutralStyles uses full-strength session surfaces and softer overlay selections. | |
| 121 | + | // It is used for palette-off, forced themes, incomplete defaults, and RGB-less | |
| 122 | 122 | // indexed fallbacks; it never performs a palette slot lookup. | |
| 123 | 123 | func neutralStyles(t Theme) Styles { | |
| 124 | 124 | status := renderer.DefaultStyle() | |
| @@ -168,6 +168,8 @@ func neutralStyles(t Theme) Styles { | |||
| 168 | 168 | styles.Accent = styles.SurfaceActive | |
| 169 | 169 | styles.TabActive = EmphasisStyle(styles.SurfaceActive, t) | |
| 170 | 170 | styles.TabNameActive = styles.TabActive | |
| 171 | + | styles.TabActiveTitle = MutedVariantStyle(styles.SurfaceActive, t) | |
| 172 | + | styles.TabTitleActive = styles.TabActiveTitle | |
| 171 | 173 | // Without a palette accent the history uses the same mruWeight | |
| 172 | 174 | // placement as MRUStyle, between the active tab and the bar. | |
| 173 | 175 | for count := 1; count <= len(styles.mruStyles); count++ { | |
| @@ -117,8 +117,8 @@ func indexedForeground(style renderer.Style, slot uint8) renderer.Style { | |||||
| 117 | return style | 117 | return style | ||
| 118 | } | 118 | } | ||
| 119 | 119 | ||||
| 120 | - | // neutralStyles deliberately preserves the pre-accent neutral hierarchy. It | 120 | + | // neutralStyles uses full-strength session surfaces and softer overlay selections. |
| 121 | - | // is used for palette-off, forced themes, incomplete defaults, and RGB-less | 121 | + | // It is used for palette-off, forced themes, incomplete defaults, and RGB-less |
| 122 | // indexed fallbacks; it never performs a palette slot lookup. | 122 | // indexed fallbacks; it never performs a palette slot lookup. | ||
| 123 | func neutralStyles(t Theme) Styles { | 123 | func neutralStyles(t Theme) Styles { | ||
| 124 | status := renderer.DefaultStyle() | 124 | status := renderer.DefaultStyle() | ||
| @@ -168,6 +168,8 @@ func neutralStyles(t Theme) Styles { | |||||
| 168 | styles.Accent = styles.SurfaceActive | 168 | styles.Accent = styles.SurfaceActive | ||
| 169 | styles.TabActive = EmphasisStyle(styles.SurfaceActive, t) | 169 | styles.TabActive = EmphasisStyle(styles.SurfaceActive, t) | ||
| 170 | styles.TabNameActive = styles.TabActive | 170 | styles.TabNameActive = styles.TabActive | ||
| 171 | + | styles.TabActiveTitle = MutedVariantStyle(styles.SurfaceActive, t) | |||
| 172 | + | styles.TabTitleActive = styles.TabActiveTitle | |||
| 171 | // Without a palette accent the history uses the same mruWeight | 173 | // Without a palette accent the history uses the same mruWeight | ||
| 172 | // placement as MRUStyle, between the active tab and the bar. | 174 | // placement as MRUStyle, between the active tab and the bar. | ||
| 173 | for count := 1; count <= len(styles.mruStyles); count++ { | 175 | for count := 1; count <= len(styles.mruStyles); count++ { | ||