fix(theme): keep overlay backgrounds neutral
- commit
- 9a739995e5a5c22c167fcdda59ec6a631b0fc6e3
- author
- brice · · 2026-07-21 14:44 UTC
- parent
- 0d707da
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
11 files · +82 -54
M internal/usecase/daemon/notices_overlay_test.go +27 -0
| @@ -11,8 +11,35 @@ import ( | |||
| 11 | 11 | ||
| 12 | 12 | "github.com/bnema/vev/internal/domain" | |
| 13 | 13 | "github.com/bnema/vev/internal/ports" | |
| 14 | + | "github.com/bnema/vev/internal/usecase/notices" | |
| 15 | + | themeui "github.com/bnema/vev/internal/usecase/theme" | |
| 16 | + | "github.com/bnema/vev/pkg/renderer" | |
| 14 | 17 | ) | |
| 15 | 18 | ||
| 19 | + | func TestCaptureNoticesOverlayUsesTerminalBackgroundOutsideSelection(t *testing.T) { | |
| 20 | + | terminal := renderer.DefaultStyle() | |
| 21 | + | inactive := renderer.Style{HasBackgroundRGB: true, BackgroundRGB: renderer.RGB{R: 10, G: 20, B: 30}} | |
| 22 | + | selection := renderer.Style{HasBackgroundRGB: true, BackgroundRGB: renderer.RGB{R: 40, G: 50, B: 60}} | |
| 23 | + | styles := themeui.Styles{ | |
| 24 | + | PickerBase: terminal, SurfaceInactive: inactive, PickerSelection: selection, | |
| 25 | + | PickerSelectionName: selection, PickerSelectionMuted: selection, | |
| 26 | + | PickerDescription: terminal, | |
| 27 | + | } | |
| 28 | + | model := notices.New([]domain.Notification{ | |
| 29 | + | {Code: domain.NoticePaneSpawn, Message: "selected", Time: time.Unix(2, 0)}, | |
| 30 | + | {Code: domain.NoticeTabSpawn, Message: "inactive", Time: time.Unix(1, 0)}, | |
| 31 | + | }, time.Unix(3, 0)) | |
| 32 | + | state := capturedRenderState{styles: styles, layout: capturedTabLayout{area: domain.Rect{Width: 100, Height: 38}}} | |
| 33 | + | snap := &overlayRenderSnapshot{noticesOverlayActive: true, noticesOverlayModel: model} | |
| 34 | + | ||
| 35 | + | captureOverlayLayers(&state, snap, domain.PaletteConfig{}) | |
| 36 | + | ||
| 37 | + | inner := state.overlays.noticesOverlay.inner | |
| 38 | + | require.True(t, inner.At(inner.Width-1, 1).Style.Equal(terminal), "inactive row keeps the terminal background") | |
| 39 | + | require.True(t, inner.At(inner.Width-1, 2).Style.Equal(terminal), "unused interior keeps the terminal background") | |
| 40 | + | require.True(t, inner.At(inner.Width-1, 0).Style.Equal(selection), "selected row keeps the accent background") | |
| 41 | + | } | |
| 42 | + | ||
| 16 | 43 | func TestNoticesOpenViaPaletteCommandShowsModalWithHistory(t *testing.T) { | |
| 17 | 44 | p, release := newBlockingPTY(t) | |
| 18 | 45 | d, sess, ac, sends := newManualSessionWithPTYs(t, p) | |
| @@ -11,8 +11,35 @@ import ( | |||||
| 11 | 11 | ||||
| 12 | "github.com/bnema/vev/internal/domain" | 12 | "github.com/bnema/vev/internal/domain" | ||
| 13 | "github.com/bnema/vev/internal/ports" | 13 | "github.com/bnema/vev/internal/ports" | ||
| 14 | + | "github.com/bnema/vev/internal/usecase/notices" | |||
| 15 | + | themeui "github.com/bnema/vev/internal/usecase/theme" | |||
| 16 | + | "github.com/bnema/vev/pkg/renderer" | |||
| 14 | ) | 17 | ) | ||
| 15 | 18 | ||||
| 19 | + | func TestCaptureNoticesOverlayUsesTerminalBackgroundOutsideSelection(t *testing.T) { | |||
| 20 | + | terminal := renderer.DefaultStyle() | |||
| 21 | + | inactive := renderer.Style{HasBackgroundRGB: true, BackgroundRGB: renderer.RGB{R: 10, G: 20, B: 30}} | |||
| 22 | + | selection := renderer.Style{HasBackgroundRGB: true, BackgroundRGB: renderer.RGB{R: 40, G: 50, B: 60}} | |||
| 23 | + | styles := themeui.Styles{ | |||
| 24 | + | PickerBase: terminal, SurfaceInactive: inactive, PickerSelection: selection, | |||
| 25 | + | PickerSelectionName: selection, PickerSelectionMuted: selection, | |||
| 26 | + | PickerDescription: terminal, | |||
| 27 | + | } | |||
| 28 | + | model := notices.New([]domain.Notification{ | |||
| 29 | + | {Code: domain.NoticePaneSpawn, Message: "selected", Time: time.Unix(2, 0)}, | |||
| 30 | + | {Code: domain.NoticeTabSpawn, Message: "inactive", Time: time.Unix(1, 0)}, | |||
| 31 | + | }, time.Unix(3, 0)) | |||
| 32 | + | state := capturedRenderState{styles: styles, layout: capturedTabLayout{area: domain.Rect{Width: 100, Height: 38}}} | |||
| 33 | + | snap := &overlayRenderSnapshot{noticesOverlayActive: true, noticesOverlayModel: model} | |||
| 34 | + | ||||
| 35 | + | captureOverlayLayers(&state, snap, domain.PaletteConfig{}) | |||
| 36 | + | ||||
| 37 | + | inner := state.overlays.noticesOverlay.inner | |||
| 38 | + | require.True(t, inner.At(inner.Width-1, 1).Style.Equal(terminal), "inactive row keeps the terminal background") | |||
| 39 | + | require.True(t, inner.At(inner.Width-1, 2).Style.Equal(terminal), "unused interior keeps the terminal background") | |||
| 40 | + | require.True(t, inner.At(inner.Width-1, 0).Style.Equal(selection), "selected row keeps the accent background") | |||
| 41 | + | } | |||
| 42 | + | ||||
| 16 | func TestNoticesOpenViaPaletteCommandShowsModalWithHistory(t *testing.T) { | 43 | func TestNoticesOpenViaPaletteCommandShowsModalWithHistory(t *testing.T) { | ||
| 17 | p, release := newBlockingPTY(t) | 44 | p, release := newBlockingPTY(t) | ||
| 18 | d, sess, ac, sends := newManualSessionWithPTYs(t, p) | 45 | d, sess, ac, sends := newManualSessionWithPTYs(t, p) | ||
M internal/usecase/daemon/palette.go +1 -1
| @@ -520,6 +520,6 @@ func composePaletteClientFrame(model *palette.Model, base renderer.Frame, cfg do | |||
| 520 | 520 | styleSet := resolveStyles(styles) | |
| 521 | 521 | modal := paletteModalFor(domain.Size{Cols: base.Width, Rows: base.Height}, cfg) | |
| 522 | 522 | return composeModalClientFrame(base, modal, styleSet, func(size domain.Size) renderer.Frame { | |
| 523 | - | return model.Render(size, palette.RenderOptions{Styles: palette.RenderStyles{Base: styleSet.PickerBase, Row: styleSet.SurfaceInactive, Selection: styleSet.PickerSelection, Description: styleSet.PickerDescription, SelectionDescription: styleSet.PickerSelectionMuted}, Guidance: guidance}) | |
| 523 | + | return model.Render(size, palette.RenderOptions{Styles: palette.RenderStyles{Base: styleSet.PickerBase, Row: styleSet.PickerBase, Selection: styleSet.PickerSelection, Description: styleSet.PickerDescription, SelectionDescription: styleSet.PickerSelectionMuted}, Guidance: guidance}) | |
| 524 | 524 | }) | |
| 525 | 525 | } | |
| @@ -520,6 +520,6 @@ func composePaletteClientFrame(model *palette.Model, base renderer.Frame, cfg do | |||||
| 520 | styleSet := resolveStyles(styles) | 520 | styleSet := resolveStyles(styles) | ||
| 521 | modal := paletteModalFor(domain.Size{Cols: base.Width, Rows: base.Height}, cfg) | 521 | modal := paletteModalFor(domain.Size{Cols: base.Width, Rows: base.Height}, cfg) | ||
| 522 | return composeModalClientFrame(base, modal, styleSet, func(size domain.Size) renderer.Frame { | 522 | return composeModalClientFrame(base, modal, styleSet, func(size domain.Size) renderer.Frame { | ||
| 523 | - | return model.Render(size, palette.RenderOptions{Styles: palette.RenderStyles{Base: styleSet.PickerBase, Row: styleSet.SurfaceInactive, Selection: styleSet.PickerSelection, Description: styleSet.PickerDescription, SelectionDescription: styleSet.PickerSelectionMuted}, Guidance: guidance}) | 523 | + | return model.Render(size, palette.RenderOptions{Styles: palette.RenderStyles{Base: styleSet.PickerBase, Row: styleSet.PickerBase, Selection: styleSet.PickerSelection, Description: styleSet.PickerDescription, SelectionDescription: styleSet.PickerSelectionMuted}, Guidance: guidance}) |
| 524 | }) | 524 | }) | ||
| 525 | } | 525 | } | ||
M internal/usecase/daemon/palette_test.go +5 -4
| @@ -74,13 +74,14 @@ func TestCaptureOverlayLayersPreservesPaletteDescriptionSurfaceAcrossFallbacks(t | |||
| 74 | 74 | captureOverlayLayers(&state, snap, domain.PaletteConfig{}) | |
| 75 | 75 | ||
| 76 | 76 | inactive := state.overlays.palette.inner.At(4, 1).Style | |
| 77 | - | require.Equal(t, state.styles.SurfaceInactive.Background, inactive.Background) | |
| 78 | - | require.Equal(t, state.styles.SurfaceInactive.HasBackgroundRGB, inactive.HasBackgroundRGB) | |
| 79 | - | require.Equal(t, state.styles.SurfaceInactive.BackgroundRGB, inactive.BackgroundRGB) | |
| 77 | + | require.Equal(t, state.styles.PickerDescription.Foreground, inactive.Foreground) | |
| 78 | + | require.Equal(t, state.styles.PickerDescription.HasForegroundRGB, inactive.HasForegroundRGB) | |
| 79 | + | require.Equal(t, state.styles.PickerDescription.ForegroundRGB, inactive.ForegroundRGB) | |
| 80 | + | require.False(t, inactive.HasBackgroundRGB) | |
| 80 | 81 | if tt.indexed { | |
| 81 | 82 | require.Equal(t, 2, inactive.Foreground) | |
| 82 | - | require.False(t, inactive.HasBackgroundRGB) | |
| 83 | 83 | } | |
| 84 | + | require.True(t, state.overlays.palette.inner.At(31, 1).Style.Equal(state.styles.PickerBase), "inactive row filler keeps the terminal background") | |
| 84 | 85 | ||
| 85 | 86 | selected := state.overlays.palette.inner.At(4, 2).Style | |
| 86 | 87 | require.True(t, selected.Equal(state.styles.PickerSelectionMuted)) | |
| @@ -74,13 +74,14 @@ func TestCaptureOverlayLayersPreservesPaletteDescriptionSurfaceAcrossFallbacks(t | |||||
| 74 | captureOverlayLayers(&state, snap, domain.PaletteConfig{}) | 74 | captureOverlayLayers(&state, snap, domain.PaletteConfig{}) | ||
| 75 | 75 | ||||
| 76 | inactive := state.overlays.palette.inner.At(4, 1).Style | 76 | inactive := state.overlays.palette.inner.At(4, 1).Style | ||
| 77 | - | require.Equal(t, state.styles.SurfaceInactive.Background, inactive.Background) | 77 | + | require.Equal(t, state.styles.PickerDescription.Foreground, inactive.Foreground) |
| 78 | - | require.Equal(t, state.styles.SurfaceInactive.HasBackgroundRGB, inactive.HasBackgroundRGB) | 78 | + | require.Equal(t, state.styles.PickerDescription.HasForegroundRGB, inactive.HasForegroundRGB) |
| 79 | - | require.Equal(t, state.styles.SurfaceInactive.BackgroundRGB, inactive.BackgroundRGB) | 79 | + | require.Equal(t, state.styles.PickerDescription.ForegroundRGB, inactive.ForegroundRGB) |
| 80 | + | require.False(t, inactive.HasBackgroundRGB) | |||
| 80 | if tt.indexed { | 81 | if tt.indexed { | ||
| 81 | require.Equal(t, 2, inactive.Foreground) | 82 | require.Equal(t, 2, inactive.Foreground) | ||
| 82 | - | require.False(t, inactive.HasBackgroundRGB) | |||
| 83 | } | 83 | } | ||
| 84 | + | require.True(t, state.overlays.palette.inner.At(31, 1).Style.Equal(state.styles.PickerBase), "inactive row filler keeps the terminal background") | |||
| 84 | 85 | ||||
| 85 | selected := state.overlays.palette.inner.At(4, 2).Style | 86 | selected := state.overlays.palette.inner.At(4, 2).Style | ||
| 86 | require.True(t, selected.Equal(state.styles.PickerSelectionMuted)) | 87 | require.True(t, selected.Equal(state.styles.PickerSelectionMuted)) | ||
M internal/usecase/daemon/picker_test.go +5 -10
| @@ -717,28 +717,23 @@ func TestCaptureOverlayLayersPreservesPickerSemanticSurfacesAcrossFallbacks(t *t | |||
| 717 | 717 | ||
| 718 | 718 | captureOverlayLayers(&state, snap, domain.PaletteConfig{}) | |
| 719 | 719 | ||
| 720 | - | require.Equal(t, state.styles.SurfaceInactive.Background, state.styles.PickerDescription.Background) | |
| 721 | - | require.Equal(t, state.styles.SurfaceInactive.HasBackgroundRGB, state.styles.PickerDescription.HasBackgroundRGB) | |
| 722 | - | require.Equal(t, state.styles.SurfaceInactive.BackgroundRGB, state.styles.PickerDescription.BackgroundRGB) | |
| 723 | - | require.Equal(t, state.styles.SurfaceBar.Background, state.styles.PickerSeparator.Background) | |
| 724 | - | require.Equal(t, state.styles.SurfaceBar.HasBackgroundRGB, state.styles.PickerSeparator.HasBackgroundRGB) | |
| 725 | - | require.Equal(t, state.styles.SurfaceBar.BackgroundRGB, state.styles.PickerSeparator.BackgroundRGB) | |
| 720 | + | require.False(t, state.styles.PickerDescription.HasBackgroundRGB) | |
| 721 | + | require.False(t, state.styles.PickerSeparator.HasBackgroundRGB) | |
| 726 | 722 | if tt.indexed { | |
| 727 | 723 | require.Equal(t, 2, state.styles.PickerDescription.Foreground) | |
| 728 | 724 | require.Equal(t, 2, state.styles.PickerSeparator.Foreground) | |
| 729 | - | require.False(t, state.styles.PickerDescription.HasBackgroundRGB) | |
| 730 | - | require.False(t, state.styles.PickerSeparator.HasBackgroundRGB) | |
| 731 | 725 | } | |
| 732 | 726 | ||
| 733 | 727 | inner := state.overlays.picker.inner | |
| 734 | 728 | layout := picker.ChooseLayout(domain.Size{Cols: inner.Width, Rows: inner.Height}) | |
| 735 | 729 | require.Equal(t, picker.LayoutHorizontal, layout.Mode) | |
| 736 | - | require.True(t, inner.At(5, 3).Style.Equal(state.styles.PickerDescription), "description text retains the inactive row surface") | |
| 730 | + | require.True(t, inner.At(5, 3).Style.Equal(state.styles.PickerDescription), "description text keeps a muted foreground on the terminal background") | |
| 731 | + | require.True(t, inner.At(layout.List.Width-1, 3).Style.Equal(state.styles.PickerBase), "inactive row filler keeps the terminal background") | |
| 737 | 732 | for y := layout.Separator.Y; y < layout.Separator.Y+layout.Separator.Height; y++ { | |
| 738 | 733 | for x := layout.Separator.X; x < layout.Separator.X+layout.Separator.Width; x++ { | |
| 739 | 734 | cell := inner.At(x, y) | |
| 740 | 735 | require.Equal(t, '│', cell.Rune) | |
| 741 | - | require.True(t, cell.Style.Equal(state.styles.PickerSeparator), "separator retains the bar surface") | |
| 736 | + | require.True(t, cell.Style.Equal(state.styles.PickerSeparator), "separator keeps a foreground-only style on the terminal background") | |
| 742 | 737 | } | |
| 743 | 738 | } | |
| 744 | 739 | }) | |
| @@ -717,28 +717,23 @@ func TestCaptureOverlayLayersPreservesPickerSemanticSurfacesAcrossFallbacks(t *t | |||||
| 717 | 717 | ||||
| 718 | captureOverlayLayers(&state, snap, domain.PaletteConfig{}) | 718 | captureOverlayLayers(&state, snap, domain.PaletteConfig{}) | ||
| 719 | 719 | ||||
| 720 | - | require.Equal(t, state.styles.SurfaceInactive.Background, state.styles.PickerDescription.Background) | 720 | + | require.False(t, state.styles.PickerDescription.HasBackgroundRGB) |
| 721 | - | require.Equal(t, state.styles.SurfaceInactive.HasBackgroundRGB, state.styles.PickerDescription.HasBackgroundRGB) | 721 | + | require.False(t, state.styles.PickerSeparator.HasBackgroundRGB) |
| 722 | - | require.Equal(t, state.styles.SurfaceInactive.BackgroundRGB, state.styles.PickerDescription.BackgroundRGB) | |||
| 723 | - | require.Equal(t, state.styles.SurfaceBar.Background, state.styles.PickerSeparator.Background) | |||
| 724 | - | require.Equal(t, state.styles.SurfaceBar.HasBackgroundRGB, state.styles.PickerSeparator.HasBackgroundRGB) | |||
| 725 | - | require.Equal(t, state.styles.SurfaceBar.BackgroundRGB, state.styles.PickerSeparator.BackgroundRGB) | |||
| 726 | if tt.indexed { | 722 | if tt.indexed { | ||
| 727 | require.Equal(t, 2, state.styles.PickerDescription.Foreground) | 723 | require.Equal(t, 2, state.styles.PickerDescription.Foreground) | ||
| 728 | require.Equal(t, 2, state.styles.PickerSeparator.Foreground) | 724 | require.Equal(t, 2, state.styles.PickerSeparator.Foreground) | ||
| 729 | - | require.False(t, state.styles.PickerDescription.HasBackgroundRGB) | |||
| 730 | - | require.False(t, state.styles.PickerSeparator.HasBackgroundRGB) | |||
| 731 | } | 725 | } | ||
| 732 | 726 | ||||
| 733 | inner := state.overlays.picker.inner | 727 | inner := state.overlays.picker.inner | ||
| 734 | layout := picker.ChooseLayout(domain.Size{Cols: inner.Width, Rows: inner.Height}) | 728 | layout := picker.ChooseLayout(domain.Size{Cols: inner.Width, Rows: inner.Height}) | ||
| 735 | require.Equal(t, picker.LayoutHorizontal, layout.Mode) | 729 | require.Equal(t, picker.LayoutHorizontal, layout.Mode) | ||
| 736 | - | require.True(t, inner.At(5, 3).Style.Equal(state.styles.PickerDescription), "description text retains the inactive row surface") | 730 | + | require.True(t, inner.At(5, 3).Style.Equal(state.styles.PickerDescription), "description text keeps a muted foreground on the terminal background") |
| 731 | + | require.True(t, inner.At(layout.List.Width-1, 3).Style.Equal(state.styles.PickerBase), "inactive row filler keeps the terminal background") | |||
| 737 | for y := layout.Separator.Y; y < layout.Separator.Y+layout.Separator.Height; y++ { | 732 | for y := layout.Separator.Y; y < layout.Separator.Y+layout.Separator.Height; y++ { | ||
| 738 | for x := layout.Separator.X; x < layout.Separator.X+layout.Separator.Width; x++ { | 733 | for x := layout.Separator.X; x < layout.Separator.X+layout.Separator.Width; x++ { | ||
| 739 | cell := inner.At(x, y) | 734 | cell := inner.At(x, y) | ||
| 740 | require.Equal(t, '│', cell.Rune) | 735 | require.Equal(t, '│', cell.Rune) | ||
| 741 | - | require.True(t, cell.Style.Equal(state.styles.PickerSeparator), "separator retains the bar surface") | 736 | + | require.True(t, cell.Style.Equal(state.styles.PickerSeparator), "separator keeps a foreground-only style on the terminal background") |
| 742 | } | 737 | } | ||
| 743 | } | 738 | } | ||
| 744 | }) | 739 | }) | ||
M internal/usecase/daemon/render_pipeline.go +3 -3
| @@ -229,13 +229,13 @@ func captureOverlayLayers(state *capturedRenderState, snap *overlayRenderSnapsho | |||
| 229 | 229 | if snap.pickerActive && snap.pickerModel != nil { | |
| 230 | 230 | o.picker.modal = pickerModal | |
| 231 | 231 | o.picker.focused = true | |
| 232 | - | renderStyles := picker.RenderStyles{Background: styles.PickerBase, Selection: styles.PickerSelection, SelectionName: styles.PickerSelectionName, SelectionMuted: styles.PickerSelectionMuted, Name: styles.SurfaceInactive, Detail: styles.PickerDescription, Base: styles.SurfaceInactive, Separator: styles.PickerSeparator} | |
| 232 | + | renderStyles := picker.RenderStyles{Background: styles.PickerBase, Selection: styles.PickerSelection, SelectionName: styles.PickerSelectionName, SelectionMuted: styles.PickerSelectionMuted, Name: styles.PickerName, Detail: styles.PickerDescription, Base: styles.PickerBase, Separator: styles.PickerSeparator} | |
| 233 | 233 | o.picker.inner = snap.pickerModel.Render(rectSize(pickerModal.Inner(size)), state.preview, renderStyles) | |
| 234 | 234 | } | |
| 235 | 235 | if snap.noticesOverlayActive && snap.noticesOverlayModel != nil { | |
| 236 | 236 | o.noticesOverlay.modal = noticesModal | |
| 237 | 237 | o.noticesOverlay.focused = true | |
| 238 | - | renderStyles := notices.RenderStyles{Background: styles.PickerBase, Base: styles.SurfaceInactive, Selection: styles.PickerSelection, Text: styles.SurfaceInactive, SelectionText: styles.PickerSelectionName, Muted: styles.PickerDescription, SelectionMuted: styles.PickerSelectionMuted} | |
| 238 | + | renderStyles := notices.RenderStyles{Background: styles.PickerBase, Base: styles.PickerBase, Selection: styles.PickerSelection, Text: styles.PickerBase, SelectionText: styles.PickerSelectionName, Muted: styles.PickerDescription, SelectionMuted: styles.PickerSelectionMuted} | |
| 239 | 239 | o.noticesOverlay.inner = snap.noticesOverlayModel.Render(rectSize(noticesModal.Inner(size)), renderStyles) | |
| 240 | 240 | } | |
| 241 | 241 | if snap.paletteActive && snap.paletteModel != nil { | |
| @@ -246,7 +246,7 @@ func captureOverlayLayers(state *capturedRenderState, snap *overlayRenderSnapsho | |||
| 246 | 246 | guidance = snap.paletteHints.Feedback | |
| 247 | 247 | } | |
| 248 | 248 | o.paletteGuidance = snap.paletteFeedback | |
| 249 | - | o.palette.inner = snap.paletteModel.Render(rectSize(o.palette.modal.Inner(size)), palette.RenderOptions{Styles: palette.RenderStyles{Base: styles.PickerBase, Row: styles.SurfaceInactive, Selection: styles.PickerSelection, Description: styles.PickerDescription, SelectionDescription: styles.PickerSelectionMuted}, Guidance: guidance, Feedback: snap.paletteFeedback}) | |
| 249 | + | o.palette.inner = snap.paletteModel.Render(rectSize(o.palette.modal.Inner(size)), palette.RenderOptions{Styles: palette.RenderStyles{Base: styles.PickerBase, Row: styles.PickerBase, Selection: styles.PickerSelection, Description: styles.PickerDescription, SelectionDescription: styles.PickerSelectionMuted}, Guidance: guidance, Feedback: snap.paletteFeedback}) | |
| 250 | 250 | } | |
| 251 | 251 | if snap.promptActive && snap.promptModel != nil { | |
| 252 | 252 | o.prompt.modal = promptModalFor(snap.promptModel.Title()) | |
| @@ -229,13 +229,13 @@ func captureOverlayLayers(state *capturedRenderState, snap *overlayRenderSnapsho | |||||
| 229 | if snap.pickerActive && snap.pickerModel != nil { | 229 | if snap.pickerActive && snap.pickerModel != nil { | ||
| 230 | o.picker.modal = pickerModal | 230 | o.picker.modal = pickerModal | ||
| 231 | o.picker.focused = true | 231 | o.picker.focused = true | ||
| 232 | - | renderStyles := picker.RenderStyles{Background: styles.PickerBase, Selection: styles.PickerSelection, SelectionName: styles.PickerSelectionName, SelectionMuted: styles.PickerSelectionMuted, Name: styles.SurfaceInactive, Detail: styles.PickerDescription, Base: styles.SurfaceInactive, Separator: styles.PickerSeparator} | 232 | + | renderStyles := picker.RenderStyles{Background: styles.PickerBase, Selection: styles.PickerSelection, SelectionName: styles.PickerSelectionName, SelectionMuted: styles.PickerSelectionMuted, Name: styles.PickerName, Detail: styles.PickerDescription, Base: styles.PickerBase, Separator: styles.PickerSeparator} |
| 233 | o.picker.inner = snap.pickerModel.Render(rectSize(pickerModal.Inner(size)), state.preview, renderStyles) | 233 | o.picker.inner = snap.pickerModel.Render(rectSize(pickerModal.Inner(size)), state.preview, renderStyles) | ||
| 234 | } | 234 | } | ||
| 235 | if snap.noticesOverlayActive && snap.noticesOverlayModel != nil { | 235 | if snap.noticesOverlayActive && snap.noticesOverlayModel != nil { | ||
| 236 | o.noticesOverlay.modal = noticesModal | 236 | o.noticesOverlay.modal = noticesModal | ||
| 237 | o.noticesOverlay.focused = true | 237 | o.noticesOverlay.focused = true | ||
| 238 | - | renderStyles := notices.RenderStyles{Background: styles.PickerBase, Base: styles.SurfaceInactive, Selection: styles.PickerSelection, Text: styles.SurfaceInactive, SelectionText: styles.PickerSelectionName, Muted: styles.PickerDescription, SelectionMuted: styles.PickerSelectionMuted} | 238 | + | renderStyles := notices.RenderStyles{Background: styles.PickerBase, Base: styles.PickerBase, Selection: styles.PickerSelection, Text: styles.PickerBase, SelectionText: styles.PickerSelectionName, Muted: styles.PickerDescription, SelectionMuted: styles.PickerSelectionMuted} |
| 239 | o.noticesOverlay.inner = snap.noticesOverlayModel.Render(rectSize(noticesModal.Inner(size)), renderStyles) | 239 | o.noticesOverlay.inner = snap.noticesOverlayModel.Render(rectSize(noticesModal.Inner(size)), renderStyles) | ||
| 240 | } | 240 | } | ||
| 241 | if snap.paletteActive && snap.paletteModel != nil { | 241 | if snap.paletteActive && snap.paletteModel != nil { | ||
| @@ -246,7 +246,7 @@ func captureOverlayLayers(state *capturedRenderState, snap *overlayRenderSnapsho | |||||
| 246 | guidance = snap.paletteHints.Feedback | 246 | guidance = snap.paletteHints.Feedback | ||
| 247 | } | 247 | } | ||
| 248 | o.paletteGuidance = snap.paletteFeedback | 248 | o.paletteGuidance = snap.paletteFeedback | ||
| 249 | - | o.palette.inner = snap.paletteModel.Render(rectSize(o.palette.modal.Inner(size)), palette.RenderOptions{Styles: palette.RenderStyles{Base: styles.PickerBase, Row: styles.SurfaceInactive, Selection: styles.PickerSelection, Description: styles.PickerDescription, SelectionDescription: styles.PickerSelectionMuted}, Guidance: guidance, Feedback: snap.paletteFeedback}) | 249 | + | o.palette.inner = snap.paletteModel.Render(rectSize(o.palette.modal.Inner(size)), palette.RenderOptions{Styles: palette.RenderStyles{Base: styles.PickerBase, Row: styles.PickerBase, Selection: styles.PickerSelection, Description: styles.PickerDescription, SelectionDescription: styles.PickerSelectionMuted}, Guidance: guidance, Feedback: snap.paletteFeedback}) |
| 250 | } | 250 | } | ||
| 251 | if snap.promptActive && snap.promptModel != nil { | 251 | if snap.promptActive && snap.promptModel != nil { | ||
| 252 | o.prompt.modal = promptModalFor(snap.promptModel.Title()) | 252 | o.prompt.modal = promptModalFor(snap.promptModel.Title()) | ||
M internal/usecase/daemon/render_pipeline_test.go +3 -1
| @@ -321,13 +321,15 @@ func TestComposeCapturedOverlaysUsesCachedModalRoles(t *testing.T) { | |||
| 321 | 321 | } | |
| 322 | 322 | ||
| 323 | 323 | func TestNoticeStylesFromMapsWarnToDedicatedRoleDistinctFromInfo(t *testing.T) { | |
| 324 | + | terminal := renderer.DefaultStyle() | |
| 324 | 325 | muted := renderer.Style{Foreground: 1} | |
| 325 | 326 | active := renderer.Style{Foreground: 2} | |
| 326 | 327 | warn := renderer.Style{Foreground: 3} | |
| 327 | - | styles := themeui.Styles{BorderMuted: muted, BorderActive: active, BorderWarn: warn} | |
| 328 | + | styles := themeui.Styles{PickerBase: terminal, BorderMuted: muted, BorderActive: active, BorderWarn: warn} | |
| 328 | 329 | ||
| 329 | 330 | got := noticeStylesFrom(styles) | |
| 330 | 331 | ||
| 332 | + | require.True(t, got.Text.Equal(terminal), "toast content keeps the terminal background") | |
| 331 | 333 | require.Equal(t, active, got.BoxError) | |
| 332 | 334 | require.Equal(t, muted, got.BoxInfo) | |
| 333 | 335 | // BoxWarn must use the dedicated BorderWarn role, not fall back to the | |
| @@ -321,13 +321,15 @@ func TestComposeCapturedOverlaysUsesCachedModalRoles(t *testing.T) { | |||||
| 321 | } | 321 | } | ||
| 322 | 322 | ||||
| 323 | func TestNoticeStylesFromMapsWarnToDedicatedRoleDistinctFromInfo(t *testing.T) { | 323 | func TestNoticeStylesFromMapsWarnToDedicatedRoleDistinctFromInfo(t *testing.T) { | ||
| 324 | + | terminal := renderer.DefaultStyle() | |||
| 324 | muted := renderer.Style{Foreground: 1} | 325 | muted := renderer.Style{Foreground: 1} | ||
| 325 | active := renderer.Style{Foreground: 2} | 326 | active := renderer.Style{Foreground: 2} | ||
| 326 | warn := renderer.Style{Foreground: 3} | 327 | warn := renderer.Style{Foreground: 3} | ||
| 327 | - | styles := themeui.Styles{BorderMuted: muted, BorderActive: active, BorderWarn: warn} | 328 | + | styles := themeui.Styles{PickerBase: terminal, BorderMuted: muted, BorderActive: active, BorderWarn: warn} |
| 328 | 329 | ||||
| 329 | got := noticeStylesFrom(styles) | 330 | got := noticeStylesFrom(styles) | ||
| 330 | 331 | ||||
| 332 | + | require.True(t, got.Text.Equal(terminal), "toast content keeps the terminal background") | |||
| 331 | require.Equal(t, active, got.BoxError) | 333 | require.Equal(t, active, got.BoxError) | ||
| 332 | require.Equal(t, muted, got.BoxInfo) | 334 | require.Equal(t, muted, got.BoxInfo) | ||
| 333 | // BoxWarn must use the dedicated BorderWarn role, not fall back to the | 335 | // BoxWarn must use the dedicated BorderWarn role, not fall back to the | ||
M internal/usecase/picker/model_test.go +7 -12
| @@ -350,7 +350,7 @@ func TestRenderStylesFillBackgroundRowsAndSelection(t *testing.T) { | |||
| 350 | 350 | require.True(t, frame.At(19, 1).Style.Equal(selection), "selected row owns active surface") | |
| 351 | 351 | } | |
| 352 | 352 | ||
| 353 | - | func TestPickerDetailKeepsInactiveSurfaceAcrossAccentFallbacks(t *testing.T) { | |
| 353 | + | func TestPickerRowsKeepTerminalBackgroundAcrossAccentFallbacks(t *testing.T) { | |
| 354 | 354 | palette := [16]renderer.RGB{} | |
| 355 | 355 | palette[2] = renderer.RGB{R: 10, G: 230, B: 120} | |
| 356 | 356 | palette[10] = palette[2] | |
| @@ -387,27 +387,22 @@ func TestPickerDetailKeepsInactiveSurfaceAcrossAccentFallbacks(t *testing.T) { | |||
| 387 | 387 | for _, tt := range tests { | |
| 388 | 388 | t.Run(tt.name, func(t *testing.T) { | |
| 389 | 389 | styles := themeui.Resolve(tt.theme, tt.policy).Styles | |
| 390 | - | require.Equal(t, styles.SurfaceInactive.Background, styles.PickerDescription.Background) | |
| 391 | - | require.Equal(t, styles.SurfaceInactive.HasBackgroundRGB, styles.PickerDescription.HasBackgroundRGB) | |
| 392 | - | require.Equal(t, styles.SurfaceInactive.BackgroundRGB, styles.PickerDescription.BackgroundRGB) | |
| 393 | - | require.Equal(t, styles.SurfaceBar.Background, styles.PickerSeparator.Background) | |
| 394 | - | require.Equal(t, styles.SurfaceBar.HasBackgroundRGB, styles.PickerSeparator.HasBackgroundRGB) | |
| 395 | - | require.Equal(t, styles.SurfaceBar.BackgroundRGB, styles.PickerSeparator.BackgroundRGB) | |
| 390 | + | require.True(t, styles.PickerBase.Equal(renderer.DefaultStyle())) | |
| 391 | + | require.False(t, styles.PickerDescription.HasBackgroundRGB) | |
| 392 | + | require.False(t, styles.PickerSeparator.HasBackgroundRGB) | |
| 396 | 393 | if tt.name == "indexed only" { | |
| 397 | 394 | require.Equal(t, 2, styles.PickerDescription.Foreground) | |
| 398 | 395 | require.Equal(t, 2, styles.PickerSeparator.Foreground) | |
| 399 | - | require.False(t, styles.PickerDescription.HasBackgroundRGB) | |
| 400 | - | require.False(t, styles.PickerSeparator.HasBackgroundRGB) | |
| 401 | 396 | } | |
| 402 | 397 | ||
| 403 | 398 | frame := model.Render(domain.Size{Cols: 32, Rows: 4}, Preview{}, RenderStyles{ | |
| 404 | - | Background: styles.PickerBase, Base: styles.SurfaceInactive, Name: styles.SurfaceInactive, | |
| 399 | + | Background: styles.PickerBase, Base: styles.PickerBase, Name: styles.PickerName, | |
| 405 | 400 | Detail: styles.PickerDescription, Selection: styles.PickerSelection, | |
| 406 | 401 | SelectionName: styles.PickerSelection, SelectionMuted: styles.PickerSelection, | |
| 407 | 402 | Separator: styles.PickerSeparator, | |
| 408 | 403 | }) | |
| 409 | - | require.True(t, frame.At(5, 3).Style.Equal(styles.PickerDescription), "description text must retain its contrast-derived foreground and inactive background") | |
| 410 | - | require.True(t, frame.At(31, 3).Style.Equal(styles.SurfaceInactive), "unused row cells must retain the inactive surface") | |
| 404 | + | require.True(t, frame.At(5, 3).Style.Equal(styles.PickerDescription), "description text keeps a contrast-derived foreground without a background tint") | |
| 405 | + | require.True(t, frame.At(31, 3).Style.Equal(styles.PickerBase), "unused row cells retain the terminal background") | |
| 411 | 406 | }) | |
| 412 | 407 | } | |
| 413 | 408 | } | |
| @@ -350,7 +350,7 @@ func TestRenderStylesFillBackgroundRowsAndSelection(t *testing.T) { | |||||
| 350 | require.True(t, frame.At(19, 1).Style.Equal(selection), "selected row owns active surface") | 350 | require.True(t, frame.At(19, 1).Style.Equal(selection), "selected row owns active surface") | ||
| 351 | } | 351 | } | ||
| 352 | 352 | ||||
| 353 | - | func TestPickerDetailKeepsInactiveSurfaceAcrossAccentFallbacks(t *testing.T) { | 353 | + | func TestPickerRowsKeepTerminalBackgroundAcrossAccentFallbacks(t *testing.T) { |
| 354 | palette := [16]renderer.RGB{} | 354 | palette := [16]renderer.RGB{} | ||
| 355 | palette[2] = renderer.RGB{R: 10, G: 230, B: 120} | 355 | palette[2] = renderer.RGB{R: 10, G: 230, B: 120} | ||
| 356 | palette[10] = palette[2] | 356 | palette[10] = palette[2] | ||
| @@ -387,27 +387,22 @@ func TestPickerDetailKeepsInactiveSurfaceAcrossAccentFallbacks(t *testing.T) { | |||||
| 387 | for _, tt := range tests { | 387 | for _, tt := range tests { | ||
| 388 | t.Run(tt.name, func(t *testing.T) { | 388 | t.Run(tt.name, func(t *testing.T) { | ||
| 389 | styles := themeui.Resolve(tt.theme, tt.policy).Styles | 389 | styles := themeui.Resolve(tt.theme, tt.policy).Styles | ||
| 390 | - | require.Equal(t, styles.SurfaceInactive.Background, styles.PickerDescription.Background) | 390 | + | require.True(t, styles.PickerBase.Equal(renderer.DefaultStyle())) |
| 391 | - | require.Equal(t, styles.SurfaceInactive.HasBackgroundRGB, styles.PickerDescription.HasBackgroundRGB) | 391 | + | require.False(t, styles.PickerDescription.HasBackgroundRGB) |
| 392 | - | require.Equal(t, styles.SurfaceInactive.BackgroundRGB, styles.PickerDescription.BackgroundRGB) | 392 | + | require.False(t, styles.PickerSeparator.HasBackgroundRGB) |
| 393 | - | require.Equal(t, styles.SurfaceBar.Background, styles.PickerSeparator.Background) | |||
| 394 | - | require.Equal(t, styles.SurfaceBar.HasBackgroundRGB, styles.PickerSeparator.HasBackgroundRGB) | |||
| 395 | - | require.Equal(t, styles.SurfaceBar.BackgroundRGB, styles.PickerSeparator.BackgroundRGB) | |||
| 396 | if tt.name == "indexed only" { | 393 | if tt.name == "indexed only" { | ||
| 397 | require.Equal(t, 2, styles.PickerDescription.Foreground) | 394 | require.Equal(t, 2, styles.PickerDescription.Foreground) | ||
| 398 | require.Equal(t, 2, styles.PickerSeparator.Foreground) | 395 | require.Equal(t, 2, styles.PickerSeparator.Foreground) | ||
| 399 | - | require.False(t, styles.PickerDescription.HasBackgroundRGB) | |||
| 400 | - | require.False(t, styles.PickerSeparator.HasBackgroundRGB) | |||
| 401 | } | 396 | } | ||
| 402 | 397 | ||||
| 403 | frame := model.Render(domain.Size{Cols: 32, Rows: 4}, Preview{}, RenderStyles{ | 398 | frame := model.Render(domain.Size{Cols: 32, Rows: 4}, Preview{}, RenderStyles{ | ||
| 404 | - | Background: styles.PickerBase, Base: styles.SurfaceInactive, Name: styles.SurfaceInactive, | 399 | + | Background: styles.PickerBase, Base: styles.PickerBase, Name: styles.PickerName, |
| 405 | Detail: styles.PickerDescription, Selection: styles.PickerSelection, | 400 | Detail: styles.PickerDescription, Selection: styles.PickerSelection, | ||
| 406 | SelectionName: styles.PickerSelection, SelectionMuted: styles.PickerSelection, | 401 | SelectionName: styles.PickerSelection, SelectionMuted: styles.PickerSelection, | ||
| 407 | Separator: styles.PickerSeparator, | 402 | Separator: styles.PickerSeparator, | ||
| 408 | }) | 403 | }) | ||
| 409 | - | require.True(t, frame.At(5, 3).Style.Equal(styles.PickerDescription), "description text must retain its contrast-derived foreground and inactive background") | 404 | + | require.True(t, frame.At(5, 3).Style.Equal(styles.PickerDescription), "description text keeps a contrast-derived foreground without a background tint") |
| 410 | - | require.True(t, frame.At(31, 3).Style.Equal(styles.SurfaceInactive), "unused row cells must retain the inactive surface") | 405 | + | require.True(t, frame.At(31, 3).Style.Equal(styles.PickerBase), "unused row cells retain the terminal background") |
| 411 | }) | 406 | }) | ||
| 412 | } | 407 | } | ||
| 413 | } | 408 | } | ||
M internal/usecase/theme/phase3_review_test.go +3 -2
| @@ -94,10 +94,11 @@ func TestPickerSeparatorUsesSecondaryTextContrast(t *testing.T) { | |||
| 94 | 94 | theme.PaletteKnown = 1<<2 | 1<<10 | |
| 95 | 95 | resolved := Resolve(theme, domain.ThemeAccent{Mode: domain.ThemeAccentAuto}) | |
| 96 | 96 | ||
| 97 | - | want, ok := secondaryText(resolved.Styles.PickerBase.ForegroundRGB, resolved.Styles.PickerBase.BackgroundRGB) | |
| 97 | + | want, ok := secondaryText(theme.Foreground, theme.Background) | |
| 98 | 98 | require.True(t, ok) | |
| 99 | 99 | require.Equal(t, want, resolved.Styles.PickerSeparator.ForegroundRGB) | |
| 100 | - | require.GreaterOrEqual(t, ContrastRatio(resolved.Styles.PickerSeparator.ForegroundRGB, resolved.Styles.PickerBase.BackgroundRGB), normalTextContrast) | |
| 100 | + | require.False(t, resolved.Styles.PickerSeparator.HasBackgroundRGB) | |
| 101 | + | require.GreaterOrEqual(t, ContrastRatio(resolved.Styles.PickerSeparator.ForegroundRGB, theme.Background), normalTextContrast) | |
| 101 | 102 | } | |
| 102 | 103 | ||
| 103 | 104 | func TestLegacyAliasesPaletteOffAndForcedThemesRemainExact(t *testing.T) { | |
| @@ -94,10 +94,11 @@ func TestPickerSeparatorUsesSecondaryTextContrast(t *testing.T) { | |||||
| 94 | theme.PaletteKnown = 1<<2 | 1<<10 | 94 | theme.PaletteKnown = 1<<2 | 1<<10 | ||
| 95 | resolved := Resolve(theme, domain.ThemeAccent{Mode: domain.ThemeAccentAuto}) | 95 | resolved := Resolve(theme, domain.ThemeAccent{Mode: domain.ThemeAccentAuto}) | ||
| 96 | 96 | ||||
| 97 | - | want, ok := secondaryText(resolved.Styles.PickerBase.ForegroundRGB, resolved.Styles.PickerBase.BackgroundRGB) | 97 | + | want, ok := secondaryText(theme.Foreground, theme.Background) |
| 98 | require.True(t, ok) | 98 | require.True(t, ok) | ||
| 99 | require.Equal(t, want, resolved.Styles.PickerSeparator.ForegroundRGB) | 99 | require.Equal(t, want, resolved.Styles.PickerSeparator.ForegroundRGB) | ||
| 100 | - | require.GreaterOrEqual(t, ContrastRatio(resolved.Styles.PickerSeparator.ForegroundRGB, resolved.Styles.PickerBase.BackgroundRGB), normalTextContrast) | 100 | + | require.False(t, resolved.Styles.PickerSeparator.HasBackgroundRGB) |
| 101 | + | require.GreaterOrEqual(t, ContrastRatio(resolved.Styles.PickerSeparator.ForegroundRGB, theme.Background), normalTextContrast) | |||
| 101 | } | 102 | } | ||
| 102 | 103 | ||||
| 103 | func TestLegacyAliasesPaletteOffAndForcedThemesRemainExact(t *testing.T) { | 104 | func TestLegacyAliasesPaletteOffAndForcedThemesRemainExact(t *testing.T) { | ||
M internal/usecase/theme/ramp_test.go +4 -1
| @@ -145,7 +145,10 @@ func TestResolveBuildsCompleteStylesFromOneAccent(t *testing.T) { | |||
| 145 | 145 | require.Equal(t, foregroundStyle(Blend(theme.Foreground, theme.Background, 0.40)), resolved.Styles.NeutralBorder) | |
| 146 | 146 | require.True(t, resolved.Styles.TabActive.Bold) | |
| 147 | 147 | require.Equal(t, resolved.Styles.SurfaceRecent, resolved.Styles.MRURecent) | |
| 148 | - | require.Equal(t, resolved.Styles.SurfaceBar, resolved.Styles.PickerBase) | |
| 148 | + | require.Equal(t, renderer.DefaultStyle(), resolved.Styles.PickerBase) | |
| 149 | + | require.Equal(t, renderer.DefaultStyle(), resolved.Styles.PromptBase) | |
| 150 | + | require.False(t, resolved.Styles.PickerDescription.HasBackgroundRGB) | |
| 151 | + | require.False(t, resolved.Styles.PickerSeparator.HasBackgroundRGB) | |
| 149 | 152 | require.Equal(t, resolved.Styles.SurfaceActive.BackgroundRGB, resolved.Styles.PickerSelection.BackgroundRGB) | |
| 150 | 153 | require.True(t, resolved.Styles.PickerSelection.Bold) | |
| 151 | 154 | } | |
| @@ -145,7 +145,10 @@ func TestResolveBuildsCompleteStylesFromOneAccent(t *testing.T) { | |||||
| 145 | require.Equal(t, foregroundStyle(Blend(theme.Foreground, theme.Background, 0.40)), resolved.Styles.NeutralBorder) | 145 | require.Equal(t, foregroundStyle(Blend(theme.Foreground, theme.Background, 0.40)), resolved.Styles.NeutralBorder) | ||
| 146 | require.True(t, resolved.Styles.TabActive.Bold) | 146 | require.True(t, resolved.Styles.TabActive.Bold) | ||
| 147 | require.Equal(t, resolved.Styles.SurfaceRecent, resolved.Styles.MRURecent) | 147 | require.Equal(t, resolved.Styles.SurfaceRecent, resolved.Styles.MRURecent) | ||
| 148 | - | require.Equal(t, resolved.Styles.SurfaceBar, resolved.Styles.PickerBase) | 148 | + | require.Equal(t, renderer.DefaultStyle(), resolved.Styles.PickerBase) |
| 149 | + | require.Equal(t, renderer.DefaultStyle(), resolved.Styles.PromptBase) | |||
| 150 | + | require.False(t, resolved.Styles.PickerDescription.HasBackgroundRGB) | |||
| 151 | + | require.False(t, resolved.Styles.PickerSeparator.HasBackgroundRGB) | |||
| 149 | require.Equal(t, resolved.Styles.SurfaceActive.BackgroundRGB, resolved.Styles.PickerSelection.BackgroundRGB) | 152 | require.Equal(t, resolved.Styles.SurfaceActive.BackgroundRGB, resolved.Styles.PickerSelection.BackgroundRGB) | ||
| 150 | require.True(t, resolved.Styles.PickerSelection.Bold) | 153 | require.True(t, resolved.Styles.PickerSelection.Bold) | ||
| 151 | } | 154 | } | ||
M internal/usecase/theme/resolved_styles.go +23 -19
| @@ -36,8 +36,7 @@ func stylesFromRamp(t Theme, accent Accent, ramp Ramp) Styles { | |||
| 36 | 36 | ||
| 37 | 37 | inactiveTitle := secondarySurface(ramp.SurfaceInactive) | |
| 38 | 38 | activeTitle := secondarySurface(ramp.SurfaceActive) | |
| 39 | - | inactiveDescription := secondarySurface(ramp.SurfaceInactive) | |
| 40 | - | barDescription := secondarySurface(ramp.SurfaceBar) | |
| 39 | + | overlayDescription := overlaySecondaryText(t) | |
| 41 | 40 | styles := Styles{ | |
| 42 | 41 | SurfaceBar: ramp.SurfaceBar, | |
| 43 | 42 | SurfaceInactive: ramp.SurfaceInactive, | |
| @@ -54,19 +53,30 @@ func stylesFromRamp(t Theme, accent Accent, ramp Ramp) Styles { | |||
| 54 | 53 | TabActiveTitle: activeTitle, | |
| 55 | 54 | MRURecent: ramp.SurfaceRecent, | |
| 56 | 55 | ||
| 57 | - | PickerBase: ramp.SurfaceBar, | |
| 56 | + | PickerBase: renderer.DefaultStyle(), | |
| 58 | 57 | PickerSelection: EmphasisStyle(ramp.SurfaceActive, t), | |
| 59 | - | PickerDescription: inactiveDescription, | |
| 60 | - | // Separators are secondary text on the picker base surface, not | |
| 58 | + | PickerDescription: overlayDescription, | |
| 59 | + | // Separators are secondary text on the terminal background, not | |
| 61 | 60 | // non-text borders; they therefore require the normal 4.5:1 contrast. | |
| 62 | - | PickerSeparator: barDescription, | |
| 63 | - | PromptBase: ramp.SurfaceBar, | |
| 61 | + | PickerSeparator: overlayDescription, | |
| 62 | + | PromptBase: renderer.DefaultStyle(), | |
| 64 | 63 | CopyStatus: ramp.SurfaceBar, | |
| 65 | 64 | SearchSelection: EmphasisStyle(ramp.SurfaceActive, t), | |
| 66 | 65 | } | |
| 67 | 66 | return withMRUStyles(withLegacyAliases(styles, t), ramp) | |
| 68 | 67 | } | |
| 69 | 68 | ||
| 69 | + | func overlaySecondaryText(t Theme) renderer.Style { | |
| 70 | + | if !usable(t) { | |
| 71 | + | return renderer.DefaultStyle() | |
| 72 | + | } | |
| 73 | + | foreground, ok := secondaryText(t.Foreground, t.Background) | |
| 74 | + | if !ok { | |
| 75 | + | return foregroundStyle(t.Foreground) | |
| 76 | + | } | |
| 77 | + | return foregroundStyle(foreground) | |
| 78 | + | } | |
| 79 | + | ||
| 70 | 80 | func secondarySurface(base renderer.Style) renderer.Style { | |
| 71 | 81 | if !base.HasForegroundRGB || !base.HasBackgroundRGB { | |
| 72 | 82 | return base | |
| @@ -113,18 +123,12 @@ func neutralStyles(t Theme) Styles { | |||
| 113 | 123 | status := renderer.DefaultStyle() | |
| 114 | 124 | accent := inverseStyle() | |
| 115 | 125 | border := renderer.DefaultStyle() | |
| 116 | - | inactiveDescription := status | |
| 117 | - | barDescription := status | |
| 126 | + | overlayDescription := renderer.DefaultStyle() | |
| 118 | 127 | if usable(t) { | |
| 119 | 128 | status = rgbSurface(t.Foreground, Blend(t.Background, t.Foreground, 0.12)) | |
| 120 | 129 | accent = rgbSurface(t.Foreground, Blend(t.Background, t.Foreground, neutralAccentBlend)) | |
| 121 | 130 | border = neutralBorder(t) | |
| 122 | - | // Preserve the legacy neutral muted foreground while retaining the | |
| 123 | - | // semantic row/bar background beneath description and separator cells. | |
| 124 | - | inactiveDescription = status | |
| 125 | - | inactiveDescription.ForegroundRGB = Blend(t.Foreground, t.Background, 0.45) | |
| 126 | - | barDescription = status | |
| 127 | - | barDescription.ForegroundRGB = Blend(t.Foreground, t.Background, 0.45) | |
| 131 | + | overlayDescription = overlaySecondaryText(t) | |
| 128 | 132 | } | |
| 129 | 133 | styles := Styles{ | |
| 130 | 134 | SurfaceBar: status, | |
| @@ -142,11 +146,11 @@ func neutralStyles(t Theme) Styles { | |||
| 142 | 146 | TabActiveTitle: MutedVariantStyle(accent, t), | |
| 143 | 147 | MRURecent: status, | |
| 144 | 148 | ||
| 145 | - | PickerBase: status, | |
| 149 | + | PickerBase: renderer.DefaultStyle(), | |
| 146 | 150 | PickerSelection: EmphasisStyle(accent, t), | |
| 147 | - | PickerDescription: inactiveDescription, | |
| 148 | - | PickerSeparator: barDescription, | |
| 149 | - | PromptBase: status, | |
| 151 | + | PickerDescription: overlayDescription, | |
| 152 | + | PickerSeparator: overlayDescription, | |
| 153 | + | PromptBase: renderer.DefaultStyle(), | |
| 150 | 154 | CopyStatus: accent, | |
| 151 | 155 | SearchSelection: accent, | |
| 152 | 156 | } | |
| @@ -36,8 +36,7 @@ func stylesFromRamp(t Theme, accent Accent, ramp Ramp) Styles { | |||||
| 36 | 36 | ||||
| 37 | inactiveTitle := secondarySurface(ramp.SurfaceInactive) | 37 | inactiveTitle := secondarySurface(ramp.SurfaceInactive) | ||
| 38 | activeTitle := secondarySurface(ramp.SurfaceActive) | 38 | activeTitle := secondarySurface(ramp.SurfaceActive) | ||
| 39 | - | inactiveDescription := secondarySurface(ramp.SurfaceInactive) | 39 | + | overlayDescription := overlaySecondaryText(t) |
| 40 | - | barDescription := secondarySurface(ramp.SurfaceBar) | |||
| 41 | styles := Styles{ | 40 | styles := Styles{ | ||
| 42 | SurfaceBar: ramp.SurfaceBar, | 41 | SurfaceBar: ramp.SurfaceBar, | ||
| 43 | SurfaceInactive: ramp.SurfaceInactive, | 42 | SurfaceInactive: ramp.SurfaceInactive, | ||
| @@ -54,19 +53,30 @@ func stylesFromRamp(t Theme, accent Accent, ramp Ramp) Styles { | |||||
| 54 | TabActiveTitle: activeTitle, | 53 | TabActiveTitle: activeTitle, | ||
| 55 | MRURecent: ramp.SurfaceRecent, | 54 | MRURecent: ramp.SurfaceRecent, | ||
| 56 | 55 | ||||
| 57 | - | PickerBase: ramp.SurfaceBar, | 56 | + | PickerBase: renderer.DefaultStyle(), |
| 58 | PickerSelection: EmphasisStyle(ramp.SurfaceActive, t), | 57 | PickerSelection: EmphasisStyle(ramp.SurfaceActive, t), | ||
| 59 | - | PickerDescription: inactiveDescription, | 58 | + | PickerDescription: overlayDescription, |
| 60 | - | // Separators are secondary text on the picker base surface, not | 59 | + | // Separators are secondary text on the terminal background, not |
| 61 | // non-text borders; they therefore require the normal 4.5:1 contrast. | 60 | // non-text borders; they therefore require the normal 4.5:1 contrast. | ||
| 62 | - | PickerSeparator: barDescription, | 61 | + | PickerSeparator: overlayDescription, |
| 63 | - | PromptBase: ramp.SurfaceBar, | 62 | + | PromptBase: renderer.DefaultStyle(), |
| 64 | CopyStatus: ramp.SurfaceBar, | 63 | CopyStatus: ramp.SurfaceBar, | ||
| 65 | SearchSelection: EmphasisStyle(ramp.SurfaceActive, t), | 64 | SearchSelection: EmphasisStyle(ramp.SurfaceActive, t), | ||
| 66 | } | 65 | } | ||
| 67 | return withMRUStyles(withLegacyAliases(styles, t), ramp) | 66 | return withMRUStyles(withLegacyAliases(styles, t), ramp) | ||
| 68 | } | 67 | } | ||
| 69 | 68 | ||||
| 69 | + | func overlaySecondaryText(t Theme) renderer.Style { | |||
| 70 | + | if !usable(t) { | |||
| 71 | + | return renderer.DefaultStyle() | |||
| 72 | + | } | |||
| 73 | + | foreground, ok := secondaryText(t.Foreground, t.Background) | |||
| 74 | + | if !ok { | |||
| 75 | + | return foregroundStyle(t.Foreground) | |||
| 76 | + | } | |||
| 77 | + | return foregroundStyle(foreground) | |||
| 78 | + | } | |||
| 79 | + | ||||
| 70 | func secondarySurface(base renderer.Style) renderer.Style { | 80 | func secondarySurface(base renderer.Style) renderer.Style { | ||
| 71 | if !base.HasForegroundRGB || !base.HasBackgroundRGB { | 81 | if !base.HasForegroundRGB || !base.HasBackgroundRGB { | ||
| 72 | return base | 82 | return base | ||
| @@ -113,18 +123,12 @@ func neutralStyles(t Theme) Styles { | |||||
| 113 | status := renderer.DefaultStyle() | 123 | status := renderer.DefaultStyle() | ||
| 114 | accent := inverseStyle() | 124 | accent := inverseStyle() | ||
| 115 | border := renderer.DefaultStyle() | 125 | border := renderer.DefaultStyle() | ||
| 116 | - | inactiveDescription := status | 126 | + | overlayDescription := renderer.DefaultStyle() |
| 117 | - | barDescription := status | |||
| 118 | if usable(t) { | 127 | if usable(t) { | ||
| 119 | status = rgbSurface(t.Foreground, Blend(t.Background, t.Foreground, 0.12)) | 128 | status = rgbSurface(t.Foreground, Blend(t.Background, t.Foreground, 0.12)) | ||
| 120 | accent = rgbSurface(t.Foreground, Blend(t.Background, t.Foreground, neutralAccentBlend)) | 129 | accent = rgbSurface(t.Foreground, Blend(t.Background, t.Foreground, neutralAccentBlend)) | ||
| 121 | border = neutralBorder(t) | 130 | border = neutralBorder(t) | ||
| 122 | - | // Preserve the legacy neutral muted foreground while retaining the | 131 | + | overlayDescription = overlaySecondaryText(t) |
| 123 | - | // semantic row/bar background beneath description and separator cells. | |||
| 124 | - | inactiveDescription = status | |||
| 125 | - | inactiveDescription.ForegroundRGB = Blend(t.Foreground, t.Background, 0.45) | |||
| 126 | - | barDescription = status | |||
| 127 | - | barDescription.ForegroundRGB = Blend(t.Foreground, t.Background, 0.45) | |||
| 128 | } | 132 | } | ||
| 129 | styles := Styles{ | 133 | styles := Styles{ | ||
| 130 | SurfaceBar: status, | 134 | SurfaceBar: status, | ||
| @@ -142,11 +146,11 @@ func neutralStyles(t Theme) Styles { | |||||
| 142 | TabActiveTitle: MutedVariantStyle(accent, t), | 146 | TabActiveTitle: MutedVariantStyle(accent, t), | ||
| 143 | MRURecent: status, | 147 | MRURecent: status, | ||
| 144 | 148 | ||||
| 145 | - | PickerBase: status, | 149 | + | PickerBase: renderer.DefaultStyle(), |
| 146 | PickerSelection: EmphasisStyle(accent, t), | 150 | PickerSelection: EmphasisStyle(accent, t), | ||
| 147 | - | PickerDescription: inactiveDescription, | 151 | + | PickerDescription: overlayDescription, |
| 148 | - | PickerSeparator: barDescription, | 152 | + | PickerSeparator: overlayDescription, |
| 149 | - | PromptBase: status, | 153 | + | PromptBase: renderer.DefaultStyle(), |
| 150 | CopyStatus: accent, | 154 | CopyStatus: accent, | ||
| 151 | SearchSelection: accent, | 155 | SearchSelection: accent, | ||
| 152 | } | 156 | } | ||
M internal/usecase/theme/theme.go +1 -1
| @@ -367,7 +367,7 @@ func BorderStyle(t Theme) renderer.Style { | |||
| 367 | 367 | } | |
| 368 | 368 | ||
| 369 | 369 | func MutedTextStyle(t Theme) renderer.Style { | |
| 370 | - | return neutralStyles(t).PickerDescription | |
| 370 | + | return legacyMutedText(t) | |
| 371 | 371 | } | |
| 372 | 372 | ||
| 373 | 373 | // mutedVariantBlend is the fraction MutedVariantStyle blends a style's own | |
| @@ -367,7 +367,7 @@ func BorderStyle(t Theme) renderer.Style { | |||||
| 367 | } | 367 | } | ||
| 368 | 368 | ||||
| 369 | func MutedTextStyle(t Theme) renderer.Style { | 369 | func MutedTextStyle(t Theme) renderer.Style { | ||
| 370 | - | return neutralStyles(t).PickerDescription | 370 | + | return legacyMutedText(t) |
| 371 | } | 371 | } | ||
| 372 | 372 | ||||
| 373 | // mutedVariantBlend is the fraction MutedVariantStyle blends a style's own | 373 | // mutedVariantBlend is the fraction MutedVariantStyle blends a style's own | ||