detached 7635eef

fix(theme): derive readable text for overlays and toasts (#451)

Use contrast-based text colors for the link status overlay, mode legend, and mode/level toasts instead of hardcoded white or background colors.
commit
7635eef80360bd5c023ed093ec3aeeef5a393caf
author
bnema · · 2026-09-24 05:24 UTC
committer
GitHub · · 2026-09-24 05:24 UTC
parent
677167c
signature
GPG UnverifiedB5690EEEBB952194 The key is unknown
4 files · +45 -23
M internal/ui/theme/css.go +10 -10
@@ -1380,25 +1380,25 @@ func generateToasterCSS(p Palette) string {
13801380 /* Toast level: info (default, accent color) */
13811381 .toast-info {
13821382 background-color: alpha(var(--accent), 0.9);
1383- color: var(--bg);
1383+ color: var(--accent-text);
13841384 }
13851385
13861386 /* Toast level: success */
13871387 .toast-success {
13881388 background-color: alpha(var(--success), 0.9);
1389- color: var(--bg);
1389+ color: var(--success-text);
13901390 }
13911391
13921392 /* Toast level: warning */
13931393 .toast-warning {
13941394 background-color: alpha(var(--warning), 0.9);
1395- color: var(--bg);
1395+ color: var(--warning-text);
13961396 }
13971397
13981398 /* Toast level: error */
13991399 .toast-error {
14001400 background-color: alpha(var(--destructive), 0.9);
1401- color: var(--bg);
1401+ color: var(--destructive-text);
14021402 }
14031403
14041404 /* Toast with custom styling (mode indicator toasters) */
@@ -1409,27 +1409,27 @@ func generateToasterCSS(p Palette) string {
14091409 /* Mode-specific toast colors */
14101410 .toast-pane-mode {
14111411 background-color: var(--pane-mode-color);
1412- color: #ffffff;
1412+ color: var(--pane-mode-text);
14131413 }
14141414
14151415 .toast-vim-mode {
14161416 background-color: var(--vim-mode-color);
1417- color: #ffffff;
1417+ color: var(--vim-mode-text);
14181418 }
14191419
14201420 .toast-tab-mode {
14211421 background-color: var(--tab-mode-color);
1422- color: #ffffff;
1422+ color: var(--tab-mode-text);
14231423 }
14241424
14251425 .toast-session-mode {
14261426 background-color: var(--session-mode-color);
1427- color: #ffffff;
1427+ color: var(--session-mode-text);
14281428 }
14291429
14301430 .toast-resize-mode {
14311431 background-color: var(--resize-mode-color);
1432- color: #ffffff;
1432+ color: var(--resize-mode-text);
14331433 }
14341434 `
14351435 }
@@ -1461,7 +1461,7 @@ func generateLinkStatusCSS(p Palette) string {
14611461
14621462 /* Link status label text */
14631463 .link-status label {
1464- color: var(--muted);
1464+ color: var(--control-muted);
14651465 }
14661466 `
14671467 }
@@ -1380,25 +1380,25 @@ func generateToasterCSS(p Palette) string {
1380 /* Toast level: info (default, accent color) */1380 /* Toast level: info (default, accent color) */
1381 .toast-info {1381 .toast-info {
1382 background-color: alpha(var(--accent), 0.9);1382 background-color: alpha(var(--accent), 0.9);
1383- color: var(--bg);1383+ color: var(--accent-text);
1384 }1384 }
1385 1385
1386 /* Toast level: success */1386 /* Toast level: success */
1387 .toast-success {1387 .toast-success {
1388 background-color: alpha(var(--success), 0.9);1388 background-color: alpha(var(--success), 0.9);
1389- color: var(--bg);1389+ color: var(--success-text);
1390 }1390 }
1391 1391
1392 /* Toast level: warning */1392 /* Toast level: warning */
1393 .toast-warning {1393 .toast-warning {
1394 background-color: alpha(var(--warning), 0.9);1394 background-color: alpha(var(--warning), 0.9);
1395- color: var(--bg);1395+ color: var(--warning-text);
1396 }1396 }
1397 1397
1398 /* Toast level: error */1398 /* Toast level: error */
1399 .toast-error {1399 .toast-error {
1400 background-color: alpha(var(--destructive), 0.9);1400 background-color: alpha(var(--destructive), 0.9);
1401- color: var(--bg);1401+ color: var(--destructive-text);
1402 }1402 }
1403 1403
1404 /* Toast with custom styling (mode indicator toasters) */1404 /* Toast with custom styling (mode indicator toasters) */
@@ -1409,27 +1409,27 @@ func generateToasterCSS(p Palette) string {
1409 /* Mode-specific toast colors */1409 /* Mode-specific toast colors */
1410 .toast-pane-mode {1410 .toast-pane-mode {
1411 background-color: var(--pane-mode-color);1411 background-color: var(--pane-mode-color);
1412- color: #ffffff;1412+ color: var(--pane-mode-text);
1413 }1413 }
1414 1414
1415 .toast-vim-mode {1415 .toast-vim-mode {
1416 background-color: var(--vim-mode-color);1416 background-color: var(--vim-mode-color);
1417- color: #ffffff;1417+ color: var(--vim-mode-text);
1418 }1418 }
1419 1419
1420 .toast-tab-mode {1420 .toast-tab-mode {
1421 background-color: var(--tab-mode-color);1421 background-color: var(--tab-mode-color);
1422- color: #ffffff;1422+ color: var(--tab-mode-text);
1423 }1423 }
1424 1424
1425 .toast-session-mode {1425 .toast-session-mode {
1426 background-color: var(--session-mode-color);1426 background-color: var(--session-mode-color);
1427- color: #ffffff;1427+ color: var(--session-mode-text);
1428 }1428 }
1429 1429
1430 .toast-resize-mode {1430 .toast-resize-mode {
1431 background-color: var(--resize-mode-color);1431 background-color: var(--resize-mode-color);
1432- color: #ffffff;1432+ color: var(--resize-mode-text);
1433 }1433 }
1434 `1434 `
1435 }1435 }
@@ -1461,7 +1461,7 @@ func generateLinkStatusCSS(p Palette) string {
1461 1461
1462 /* Link status label text */1462 /* Link status label text */
1463 .link-status label {1463 .link-status label {
1464- color: var(--muted);1464+ color: var(--control-muted);
1465 }1465 }
1466 `1466 `
1467 }1467 }
M internal/ui/theme/mode_legend_css.go +12 -13
@@ -5,13 +5,13 @@ import (
55 "strings"
66 )
77
8-// modeLegendColors maps legend mode classes to the palette variable of their mode.
9-var modeLegendColors = []struct{ mode, color string }{
10- {"pane", "--pane-mode-color"},
11- {"vim", "--vim-mode-color"},
12- {"resize", "--resize-mode-color"},
13- {"tab", "--tab-mode-color"},
14- {"session", "--session-mode-color"},
8+// modeLegendColors maps legend mode classes to the palette variables of their mode.
9+var modeLegendColors = []struct{ mode, color, text string }{
10+ {"pane", "--pane-mode-color", "--pane-mode-text"},
11+ {"vim", "--vim-mode-color", "--vim-mode-text"},
12+ {"resize", "--resize-mode-color", "--resize-mode-text"},
13+ {"tab", "--tab-mode-color", "--tab-mode-text"},
14+ {"session", "--session-mode-color", "--session-mode-text"},
1515 }
1616
1717 func generateModeLegendCSS() string {
@@ -31,7 +31,6 @@ func generateModeLegendCSS() string {
3131 font-family: var(--font-mono);
3232 font-size: 0.8125em;
3333 font-weight: bold;
34- color: #ffffff;
3534 padding: 0.375em 0.75em;
3635 margin-bottom: 0.375em;
3736 }
@@ -48,7 +47,7 @@ func generateModeLegendCSS() string {
4847 font-family: var(--font-mono);
4948 font-size: 0.75em;
5049 font-weight: bold;
51- color: var(--text);
50+ color: var(--control-text);
5251 background-color: var(--surface-variant);
5352 border-style: solid;
5453 border-width: 0.0625em;
@@ -57,7 +56,7 @@ func generateModeLegendCSS() string {
5756 margin-right: 0.375em;
5857 transition: background-color 120ms ease-out, color 120ms ease-out;
5958 }
60-.mode-legend-description { font-size: 0.75em; color: var(--muted); }
59+.mode-legend-description { font-size: 0.75em; color: var(--surface-muted); }
6160 .mode-legend-row.mode-legend-dim { opacity: 0.3; }
6261 @keyframes mode-legend-appear {
6362 from { opacity: 0; }
@@ -66,11 +65,11 @@ func generateModeLegendCSS() string {
6665 `)
6766 for _, m := range modeLegendColors {
6867 fmt.Fprintf(&sb, `.omnibox-container.mode-legend-panel.mode-legend-%[1]s { border-color: var(%[2]s); }
69-.mode-legend-%[1]s .mode-legend-title { background-color: var(%[2]s); }
68+.mode-legend-%[1]s .mode-legend-title { background-color: var(%[2]s); color: var(%[3]s); }
7069 .mode-legend-%[1]s .mode-legend-group-title { color: var(%[2]s); }
7170 .mode-legend-%[1]s .mode-legend-keycap { border-color: alpha(var(%[2]s), 0.6); }
72-.mode-legend-%[1]s .mode-legend-keycap.mode-legend-flash { background-color: var(%[2]s); color: #ffffff; }
73-`, m.mode, m.color)
71+.mode-legend-%[1]s .mode-legend-keycap.mode-legend-flash { background-color: var(%[2]s); color: var(%[3]s); }
72+`, m.mode, m.color, m.text)
7473 }
7574 return sb.String()
7675 }
@@ -5,13 +5,13 @@ import (
5 "strings"5 "strings"
6 )6 )
7 7
8-// modeLegendColors maps legend mode classes to the palette variable of their mode.8+// modeLegendColors maps legend mode classes to the palette variables of their mode.
9-var modeLegendColors = []struct{ mode, color string }{9+var modeLegendColors = []struct{ mode, color, text string }{
10- {"pane", "--pane-mode-color"},10+ {"pane", "--pane-mode-color", "--pane-mode-text"},
11- {"vim", "--vim-mode-color"},11+ {"vim", "--vim-mode-color", "--vim-mode-text"},
12- {"resize", "--resize-mode-color"},12+ {"resize", "--resize-mode-color", "--resize-mode-text"},
13- {"tab", "--tab-mode-color"},13+ {"tab", "--tab-mode-color", "--tab-mode-text"},
14- {"session", "--session-mode-color"},14+ {"session", "--session-mode-color", "--session-mode-text"},
15 }15 }
16 16
17 func generateModeLegendCSS() string {17 func generateModeLegendCSS() string {
@@ -31,7 +31,6 @@ func generateModeLegendCSS() string {
31 font-family: var(--font-mono);31 font-family: var(--font-mono);
32 font-size: 0.8125em;32 font-size: 0.8125em;
33 font-weight: bold;33 font-weight: bold;
34- color: #ffffff;
35 padding: 0.375em 0.75em;34 padding: 0.375em 0.75em;
36 margin-bottom: 0.375em;35 margin-bottom: 0.375em;
37 }36 }
@@ -48,7 +47,7 @@ func generateModeLegendCSS() string {
48 font-family: var(--font-mono);47 font-family: var(--font-mono);
49 font-size: 0.75em;48 font-size: 0.75em;
50 font-weight: bold;49 font-weight: bold;
51- color: var(--text);50+ color: var(--control-text);
52 background-color: var(--surface-variant);51 background-color: var(--surface-variant);
53 border-style: solid;52 border-style: solid;
54 border-width: 0.0625em;53 border-width: 0.0625em;
@@ -57,7 +56,7 @@ func generateModeLegendCSS() string {
57 margin-right: 0.375em;56 margin-right: 0.375em;
58 transition: background-color 120ms ease-out, color 120ms ease-out;57 transition: background-color 120ms ease-out, color 120ms ease-out;
59 }58 }
60-.mode-legend-description { font-size: 0.75em; color: var(--muted); }59+.mode-legend-description { font-size: 0.75em; color: var(--surface-muted); }
61 .mode-legend-row.mode-legend-dim { opacity: 0.3; }60 .mode-legend-row.mode-legend-dim { opacity: 0.3; }
62 @keyframes mode-legend-appear {61 @keyframes mode-legend-appear {
63 from { opacity: 0; }62 from { opacity: 0; }
@@ -66,11 +65,11 @@ func generateModeLegendCSS() string {
66 `)65 `)
67 for _, m := range modeLegendColors {66 for _, m := range modeLegendColors {
68 fmt.Fprintf(&sb, `.omnibox-container.mode-legend-panel.mode-legend-%[1]s { border-color: var(%[2]s); }67 fmt.Fprintf(&sb, `.omnibox-container.mode-legend-panel.mode-legend-%[1]s { border-color: var(%[2]s); }
69-.mode-legend-%[1]s .mode-legend-title { background-color: var(%[2]s); }68+.mode-legend-%[1]s .mode-legend-title { background-color: var(%[2]s); color: var(%[3]s); }
70 .mode-legend-%[1]s .mode-legend-group-title { color: var(%[2]s); }69 .mode-legend-%[1]s .mode-legend-group-title { color: var(%[2]s); }
71 .mode-legend-%[1]s .mode-legend-keycap { border-color: alpha(var(%[2]s), 0.6); }70 .mode-legend-%[1]s .mode-legend-keycap { border-color: alpha(var(%[2]s), 0.6); }
72-.mode-legend-%[1]s .mode-legend-keycap.mode-legend-flash { background-color: var(%[2]s); color: #ffffff; }71+.mode-legend-%[1]s .mode-legend-keycap.mode-legend-flash { background-color: var(%[2]s); color: var(%[3]s); }
73-`, m.mode, m.color)72+`, m.mode, m.color, m.text)
74 }73 }
75 return sb.String()74 return sb.String()
76 }75 }
M internal/ui/theme/palette.go +10 -0
@@ -53,6 +53,11 @@ func (m ModeColors) ToCSSVars() string {
5353 sb.WriteString(" --tab-mode-color: " + m.TabMode + ";\n")
5454 sb.WriteString(" --session-mode-color: " + m.SessionMode + ";\n")
5555 sb.WriteString(" --resize-mode-color: " + m.ResizeMode + ";\n")
56+ sb.WriteString(" --pane-mode-text: " + readableTextColor(m.PaneMode, "#ffffff") + ";\n")
57+ sb.WriteString(" --vim-mode-text: " + readableTextColor(m.VimMode, "#ffffff") + ";\n")
58+ sb.WriteString(" --tab-mode-text: " + readableTextColor(m.TabMode, "#ffffff") + ";\n")
59+ sb.WriteString(" --session-mode-text: " + readableTextColor(m.SessionMode, "#ffffff") + ";\n")
60+ sb.WriteString(" --resize-mode-text: " + readableTextColor(m.ResizeMode, "#ffffff") + ";\n")
5661 return sb.String()
5762 }
5863
@@ -273,6 +278,8 @@ func (p Palette) ToCSSVars() string {
273278 sb.WriteString(" --surface: " + p.Surface + ";\n")
274279 sb.WriteString(" --surface-variant: " + p.SurfaceVariant + ";\n")
275280 sb.WriteString(" --control-text: " + readableTextColor(p.SurfaceVariant, p.Text) + ";\n")
281+ sb.WriteString(" --control-muted: " + readableTextColor(p.SurfaceVariant, p.Muted) + ";\n")
282+ sb.WriteString(" --surface-muted: " + readableTextColor(p.Surface, p.Muted) + ";\n")
276283 sb.WriteString(" --text: " + p.Text + ";\n")
277284 sb.WriteString(" --muted: " + p.Muted + ";\n")
278285 sb.WriteString(" --accent: " + p.Accent + ";\n")
@@ -281,6 +288,9 @@ func (p Palette) ToCSSVars() string {
281288 sb.WriteString(" --success: " + p.Success + ";\n")
282289 sb.WriteString(" --warning: " + p.Warning + ";\n")
283290 sb.WriteString(" --destructive: " + p.Destructive + ";\n")
291+ sb.WriteString(" --success-text: " + readableTextColor(p.Success, p.Background) + ";\n")
292+ sb.WriteString(" --warning-text: " + readableTextColor(p.Warning, p.Background) + ";\n")
293+ sb.WriteString(" --destructive-text: " + readableTextColor(p.Destructive, p.Background) + ";\n")
284294 return sb.String()
285295 }
286296
@@ -53,6 +53,11 @@ func (m ModeColors) ToCSSVars() string {
53 sb.WriteString(" --tab-mode-color: " + m.TabMode + ";\n")53 sb.WriteString(" --tab-mode-color: " + m.TabMode + ";\n")
54 sb.WriteString(" --session-mode-color: " + m.SessionMode + ";\n")54 sb.WriteString(" --session-mode-color: " + m.SessionMode + ";\n")
55 sb.WriteString(" --resize-mode-color: " + m.ResizeMode + ";\n")55 sb.WriteString(" --resize-mode-color: " + m.ResizeMode + ";\n")
56+ sb.WriteString(" --pane-mode-text: " + readableTextColor(m.PaneMode, "#ffffff") + ";\n")
57+ sb.WriteString(" --vim-mode-text: " + readableTextColor(m.VimMode, "#ffffff") + ";\n")
58+ sb.WriteString(" --tab-mode-text: " + readableTextColor(m.TabMode, "#ffffff") + ";\n")
59+ sb.WriteString(" --session-mode-text: " + readableTextColor(m.SessionMode, "#ffffff") + ";\n")
60+ sb.WriteString(" --resize-mode-text: " + readableTextColor(m.ResizeMode, "#ffffff") + ";\n")
56 return sb.String()61 return sb.String()
57 }62 }
58 63
@@ -273,6 +278,8 @@ func (p Palette) ToCSSVars() string {
273 sb.WriteString(" --surface: " + p.Surface + ";\n")278 sb.WriteString(" --surface: " + p.Surface + ";\n")
274 sb.WriteString(" --surface-variant: " + p.SurfaceVariant + ";\n")279 sb.WriteString(" --surface-variant: " + p.SurfaceVariant + ";\n")
275 sb.WriteString(" --control-text: " + readableTextColor(p.SurfaceVariant, p.Text) + ";\n")280 sb.WriteString(" --control-text: " + readableTextColor(p.SurfaceVariant, p.Text) + ";\n")
281+ sb.WriteString(" --control-muted: " + readableTextColor(p.SurfaceVariant, p.Muted) + ";\n")
282+ sb.WriteString(" --surface-muted: " + readableTextColor(p.Surface, p.Muted) + ";\n")
276 sb.WriteString(" --text: " + p.Text + ";\n")283 sb.WriteString(" --text: " + p.Text + ";\n")
277 sb.WriteString(" --muted: " + p.Muted + ";\n")284 sb.WriteString(" --muted: " + p.Muted + ";\n")
278 sb.WriteString(" --accent: " + p.Accent + ";\n")285 sb.WriteString(" --accent: " + p.Accent + ";\n")
@@ -281,6 +288,9 @@ func (p Palette) ToCSSVars() string {
281 sb.WriteString(" --success: " + p.Success + ";\n")288 sb.WriteString(" --success: " + p.Success + ";\n")
282 sb.WriteString(" --warning: " + p.Warning + ";\n")289 sb.WriteString(" --warning: " + p.Warning + ";\n")
283 sb.WriteString(" --destructive: " + p.Destructive + ";\n")290 sb.WriteString(" --destructive: " + p.Destructive + ";\n")
291+ sb.WriteString(" --success-text: " + readableTextColor(p.Success, p.Background) + ";\n")
292+ sb.WriteString(" --warning-text: " + readableTextColor(p.Warning, p.Background) + ";\n")
293+ sb.WriteString(" --destructive-text: " + readableTextColor(p.Destructive, p.Background) + ";\n")
284 return sb.String()294 return sb.String()
285 }295 }
286 296
M internal/ui/theme/palette_test.go +13 -0
@@ -51,3 +51,16 @@ func TestPaletteToCSSVarsAddsReadableControlText(t *testing.T) {
5151 t.Fatalf("ToCSSVars() did not add light control text for dark input background:\n%s", css)
5252 }
5353 }
54+
55+func TestModeColorsToCSSVarsAddsReadableModeText(t *testing.T) {
56+ colors := DefaultModeColors()
57+ colors.TabMode = "#ffe066"
58+
59+ css := colors.ToCSSVars()
60+ if !strings.Contains(css, " --tab-mode-text: #000000;\n") {
61+ t.Fatalf("ToCSSVars() did not add dark text for light tab mode color:\n%s", css)
62+ }
63+ if !strings.Contains(css, " --session-mode-text: #ffffff;\n") {
64+ t.Fatalf("ToCSSVars() did not keep white text for dark session mode color:\n%s", css)
65+ }
66+}
@@ -51,3 +51,16 @@ func TestPaletteToCSSVarsAddsReadableControlText(t *testing.T) {
51 t.Fatalf("ToCSSVars() did not add light control text for dark input background:\n%s", css)51 t.Fatalf("ToCSSVars() did not add light control text for dark input background:\n%s", css)
52 }52 }
53 }53 }
54+
55+func TestModeColorsToCSSVarsAddsReadableModeText(t *testing.T) {
56+ colors := DefaultModeColors()
57+ colors.TabMode = "#ffe066"
58+
59+ css := colors.ToCSSVars()
60+ if !strings.Contains(css, " --tab-mode-text: #000000;\n") {
61+ t.Fatalf("ToCSSVars() did not add dark text for light tab mode color:\n%s", css)
62+ }
63+ if !strings.Contains(css, " --session-mode-text: #ffffff;\n") {
64+ t.Fatalf("ToCSSVars() did not keep white text for dark session mode color:\n%s", css)
65+ }
66+}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 42

↑↓ moveenter openesc close