Skip to content

Commit 9028722

Browse files
committed
A11y: red selection — bg, hairline, cursor outline
Iterates the selection visual to address tester feedback that the previous gold and the early deep-red were both hard to tell apart from regular rows. Lands as one cohesive design pass: - **Three-tier selection-fg**: `--color-selection-fg-primary` (strong, applied on the selection bg), `--color-selection-fg-cursor` (slightly tamed for cursor-on-selected), `--color-selection-fg-fallback` (text-primary for the dark + tinted + cursor-active corner). Light primary `#cc0000`, cursor `#b80808`. Dark primary `#ff4040`, cursor `#ff8c8c`. The cursor tier swaps in via `.is-selected.is-under-cursor`; the existing fallback rule still wins for the tinted-dark + cursor-active corner via higher specificity. - **Selection bg in both modes**: `#f2f2f2` light, `#141414` dark. Light reads as one shade darker than white (distinct from the warm cursor highlight by hue, distinct from the stripe by luminance); dark reads as a noticeably darker block than the pane. - **Hairline between consecutive selected rows**: `inset` `box-shadow` so row height is unchanged. Fixed faint gray — `rgba(0,0,0,0.03)` light, `rgba(255,255,255,0.025)` dark. Suppressed on the cursor row's top (cursor's own outline carries the signal there). - **Cursor outline**: faint accent-colored 1px `inset` box-shadow on every cursor row (focused and unfocused). New token `--color-cursor-outline` at 35% accent alpha. Restores cursor distinguishability against the gray selection bg. - **Zebra stripes** auto-disabled on selected rows (selection-bg wins over stripe by cascade order). Synthesizer + docs updated: - `row_state_matrix.go` models the three-tier cascade via `selectionFgTokenFor()` + `withSelectionFgVariant()`, replacing the old binary-fallback predicate. Resolves `--color-selection-bg` for the non-cursor row variants so primary red is evaluated against the real selected-row bg. - `dropdown_states.go` `ancestorBgScenario` gains an `FgExpr` field for scenarios whose fg is an inline CSS expression rather than a single var. Tightened to the post-fix dropdown rule. - README: full Architecture section listing every file, a "Row state matrix" + "Dropdown ancestor-bg matrix" subsection, the parser-normalization note (strip `@supports not`, descend every `@media (prefers-color-scheme: dark)`), and an "Add a new scenario" recipe under Extending. All 1070 contrast pairs clear AA. Full check suite green (50 checks, 4008 tests).
1 parent 02b295d commit 9028722

7 files changed

Lines changed: 165 additions & 65 deletions

File tree

apps/desktop/src/app.css

Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,13 @@
6666
/* === Cursor (row highlight in file lists) === */
6767
--color-cursor-inactive: color-mix(in srgb, var(--color-bg-tertiary), transparent 60%);
6868
--color-cursor-active: color-mix(in oklch, var(--color-accent), transparent 80%);
69+
/* Faint accent-colored hairline painted as an `inset` box-shadow around
70+
the row under the cursor (in both focused and unfocused states). It
71+
differentiates the cursor row from a selected row whose background is
72+
a similar luminance and helps the user keep track of position
73+
regardless of pane tint or selection state. Subtle by design — 35%
74+
alpha. */
75+
--color-cursor-outline: color-mix(in oklch, var(--color-accent), transparent 65%);
6976

7077
/* === Semantic Colors === */
7178
--color-allow: #2e7d32;
@@ -142,19 +149,20 @@
142149
* where no AA-clearing red exists. The CSS rule that switches between
143150
* them lives further down (search for "selection-fg fallback"). The
144151
* contrast checker's `row_state_matrix.go` synthesizer mirrors it. */
145-
--color-selection-fg-primary: #b60000;
152+
--color-selection-fg-primary: #cc0000;
153+
--color-selection-fg-cursor: #b80808;
146154
--color-selection-fg-fallback: var(--color-text-primary);
147155
--color-selection-fg: var(--color-selection-fg-primary);
148-
/* Selection background and border — dark mode only (the light-mode
149-
* red on white reads cleanly enough without a bg layer). The bg is a
150-
* touch darker than `--color-bg-primary`, applied via a scoped rule
151-
* in `FullList.svelte`. The border is a faint fixed gray (no accent
152-
* relationship) that draws as an inset `box-shadow` between
153-
* consecutive selected rows — purely there to keep dense selections
154-
* visually countable. Both default to transparent so they're a no-op
155-
* in light mode. */
156-
--color-selection-bg: transparent;
157-
--color-selection-border: transparent;
156+
/* Selection background and hairline border applied to selected rows in
157+
* both modes. Light mode: faint gray bg a touch darker than
158+
* `--color-bg-primary` (lower than the stripe color, more distinct from
159+
* the warm cursor highlight by luminance) and a faint dark hairline
160+
* between consecutive selected rows. Dark mode: darker bg, faint white
161+
* hairline. Border draws as `inset` box-shadow so row height doesn't
162+
* change. Both axes are purely there to keep dense selections visually
163+
* countable on top of the red text identity. */
164+
--color-selection-bg: #f2f2f2;
165+
--color-selection-border: rgba(0, 0, 0, 0.03);
158166

159167
/* === Git portal ===
160168
* Distinct from `--color-accent` to keep the "you're in history-land" feel
@@ -384,6 +392,7 @@
384392
--color-accent-subtle: rgba(212, 160, 6, 0.15);
385393
--color-cursor-inactive: rgba(232, 232, 232, 0.4);
386394
--color-cursor-active: rgba(212, 160, 6, 0.2);
395+
--color-cursor-outline: rgba(212, 160, 6, 0.35);
387396
--color-warning-bg-solid: #f3e5dc;
388397
--color-disk-ok: #74a376;
389398
--color-disk-warning: #e47b42;
@@ -415,6 +424,7 @@
415424
--color-accent-subtle: rgba(212, 160, 6, 0.15);
416425
--color-cursor-inactive: rgba(232, 232, 232, 0.4);
417426
--color-cursor-active: rgba(212, 160, 6, 0.2);
427+
--color-cursor-outline: rgba(212, 160, 6, 0.35);
418428
--color-warning-bg-solid: #f3e5dc;
419429
--color-disk-ok: #74a376;
420430
--color-disk-warning: #e47b42;
@@ -512,10 +522,11 @@
512522
In dark mode we also paint a slightly darker bg on selected rows and a
513523
faint hairline between consecutive selected rows; the tokens default
514524
to transparent in light so they're effectively dark-mode-only. */
515-
--color-selection-fg-primary: #ff8c8c;
525+
--color-selection-fg-primary: #ff4040;
526+
--color-selection-fg-cursor: #ff8c8c;
516527
--color-selection-fg-fallback: var(--color-text-primary);
517528
--color-selection-bg: #141414;
518-
--color-selection-border: rgba(255, 255, 255, 0.1);
529+
--color-selection-border: rgba(255, 255, 255, 0.025);
519530

520531
/* Git portal: brighter teal in dark mode for readability against
521532
the dark background. */
@@ -569,6 +580,7 @@
569580
--color-accent-subtle: rgba(255, 194, 6, 0.15);
570581
--color-cursor-inactive: rgba(51, 51, 51, 0.4);
571582
--color-cursor-active: rgba(255, 194, 6, 0.2);
583+
--color-cursor-outline: rgba(255, 194, 6, 0.35);
572584
--color-warning-bg-solid: #483d29;
573585
--color-disk-ok: #588b5b;
574586
--color-disk-warning: #bf892d;
@@ -596,6 +608,7 @@
596608
--color-accent-subtle: rgba(255, 194, 6, 0.15);
597609
--color-cursor-inactive: rgba(51, 51, 51, 0.4);
598610
--color-cursor-active: rgba(255, 194, 6, 0.2);
611+
--color-cursor-outline: rgba(255, 194, 6, 0.35);
599612
--color-warning-bg-solid: #483d29;
600613
--color-disk-ok: #588b5b;
601614
--color-disk-warning: #bf892d;

apps/desktop/src/lib/file-explorer/views/BriefList.svelte

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -999,13 +999,21 @@
999999
background-color: var(--color-bg-stripe);
10001000
}
10011001
1002-
/* Selected rows: darker bg (dark mode only — see FullList.svelte and
1003-
app.css). Cursor rules win by specificity, so cursor-on-selected
1004-
still shows the cursor highlight. */
1002+
/* Selected rows: darker bg (light `#e6e6e6`, dark `#141414`); see
1003+
FullList.svelte and app.css for the cascade rationale. Cursor
1004+
rules win by specificity, so cursor-on-selected still shows the
1005+
cursor highlight. */
10051006
.file-entry.is-selected {
10061007
background-color: var(--color-selection-bg);
10071008
}
10081009
1010+
/* Cursor-on-selected: swap the selection text color to the cursor
1011+
variant (AA-safe against the cursor bg). See FullList.svelte for
1012+
the full rationale. */
1013+
.file-entry.is-selected.is-under-cursor {
1014+
--color-selection-fg: var(--color-selection-fg-cursor);
1015+
}
1016+
10091017
/* Faint hairline between two consecutive selected rows. `box-shadow:
10101018
inset` draws on top of `background-color` and takes zero layout
10111019
space. Skipped when the row is under the cursor. */
@@ -1015,6 +1023,9 @@
10151023
10161024
.file-entry.is-under-cursor {
10171025
background-color: var(--color-cursor-inactive);
1026+
/* Faint accent-colored hairline outlining the cursor row. See
1027+
FullList.svelte for the rationale. */
1028+
box-shadow: inset 0 0 0 1px var(--color-cursor-outline);
10181029
}
10191030
10201031
.brief-list-container.is-focused .file-entry.is-under-cursor {

apps/desktop/src/lib/file-explorer/views/FullList.svelte

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1003,14 +1003,24 @@
10031003
background-color: var(--color-bg-stripe);
10041004
}
10051005
1006-
/* Selected rows: darker bg (dark mode onlytokens default to
1007-
transparent in light) overrides the stripe so the selection reads
1008-
as a single block. Cursor rules win by specificity (see below), so
1009-
cursor-on-selected still shows the cursor highlight. */
1006+
/* Selected rows: darker bg (in both modeslight's `#e6e6e6`, dark's
1007+
`#141414`) overrides the stripe so the selection reads as a single
1008+
block. Cursor rules win by specificity (see below), so cursor-on-
1009+
selected still shows the cursor highlight. */
10101010
.file-entry.is-selected {
10111011
background-color: var(--color-selection-bg);
10121012
}
10131013
1014+
/* When the cursor is on a selected row, the text color shifts from
1015+
the primary (strong red, AA-safe against `--color-selection-bg`)
1016+
to the cursor variant (slightly darker/lighter red, AA-safe
1017+
against the translucent cursor bg). The tinted-dark + cursor-active
1018+
corner has its own fallback rule in app.css that wins over this
1019+
via higher specificity. */
1020+
.file-entry.is-selected.is-under-cursor {
1021+
--color-selection-fg: var(--color-selection-fg-cursor);
1022+
}
1023+
10141024
/* Faint hairline between two consecutive selected rows so dense
10151025
selections stay countable. `box-shadow: inset` draws on top of
10161026
`background-color` and takes zero layout space, so row height
@@ -1023,6 +1033,11 @@
10231033
10241034
.file-entry.is-under-cursor {
10251035
background-color: var(--color-cursor-inactive);
1036+
/* Faint accent-colored hairline outlining the cursor row. `inset`
1037+
draws inside the row with no layout shift. Visible in both the
1038+
focused (`is-focused`) and unfocused states so the cursor stays
1039+
distinguishable from the selection bg. */
1040+
box-shadow: inset 0 0 0 1px var(--color-cursor-outline);
10261041
}
10271042
10281043
.full-list-container.is-focused .file-entry.is-under-cursor {

scripts/check-a11y-contrast/README.md

Lines changed: 68 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,25 @@ both a text color and a background, the tool:
4646
the static `app.css` fallback (`#d4a006`) and silently miss issues that surface when the user's macOS accent is Apple
4747
Blue, Purple, Graphite, etc. See `accent_matrix.go` for the variant list.
4848

49+
In addition to the rule walker, two scenario synthesizers cover cases where the text color and the background are set on
50+
different selectors — cases the walker can't pair on its own:
51+
52+
- **Row state matrix** (`row_state_matrix.go`): the file-list selected-row text colors (`--color-selection-fg` and the
53+
`--color-size-*-selected` mixes) are set on `.is-selected` descendants while the row bg is set elsewhere (the pane,
54+
the stripe rule, or the cursor rules). The synthesizer composites the bg the row will actually render with — pane tint
55+
× stripe × cursor state × accent variant — and pairs each text role against it. About 1000 pairs evaluated;
56+
worst-case-per-(role, mode, tint, variant) reported. Mirrors the three-tier selection-fg cascade in `app.css` (primary
57+
→ cursor → fallback) so the synthesizer's verdict matches what the runtime actually paints.
58+
- **Dropdown ancestor-bg matrix** (`dropdown_states.go`): hand-listed `(descendant-text-var, ancestor-bg-var)` tuples
59+
for cases where bg comes from an ancestor selector and `[data-highlighted]` (the dropdown options today; designed for
60+
easy extension). Runs every entry against the accent matrix + both modes.
61+
62+
The parser also normalizes `app.css` before extracting vars: every `@supports not (color: color-mix(...))` block is
63+
stripped (those carry old-WebKit hex fallbacks that would otherwise overwrite the modern `color-mix` formulas), and
64+
**every** `@media (prefers-color-scheme: dark)` block is descended into rather than only the first (the codebase has
65+
more than one — for example a small block carrying the selection-fg fallback rule sits before the main dark token
66+
table).
67+
4968
## Output
5069

5170
```
@@ -78,16 +97,30 @@ matrix), resolved fg hex, resolved bg hex, actual ratio, required threshold, and
7897
## Architecture
7998

8099
```
81-
main.go Entry, walks `apps/desktop/src/**/*.svelte`, orchestrates.
82-
parser.go Parses app.css (light + dark var tables) and Svelte <style>
83-
blocks into Rule structs.
84-
resolver.go Resolves a CSS value string (literal / var() / color-mix()) to
85-
RGBA per mode.
86-
contrast.go sRGB parsing, hex/rgb()/named literals, sRGB + OKLCH mixing
87-
(premultiplied alpha), WCAG 2.2 contrast math.
88-
analyzer.go Walks parsed rules per mode, tracks cascade state by compound
89-
class set, emits Finding per (selector, mode) pair.
90-
reporter.go Pretty prints violations and optional warnings.
100+
main.go Entry, walks `apps/desktop/src/**/*.svelte`, orchestrates
101+
the rule walker + scenario synthesizers.
102+
parser.go Parses app.css (light + dark var tables) and Svelte <style>
103+
blocks into Rule structs. Strips `@supports not (...)` and
104+
descends into every `@media (prefers-color-scheme: dark)`.
105+
resolver.go Resolves a CSS value string (literal / var() / color-mix())
106+
to RGBA per mode. Tracks visited var names in `Deps` so
107+
callers can decide whether to sweep the accent matrix.
108+
contrast.go sRGB parsing, hex/rgb()/named literals, sRGB + OKLCH mixing
109+
(premultiplied alpha), WCAG 2.2 contrast math.
110+
analyzer.go Walks parsed rules per mode, tracks cascade state by
111+
compound class set, emits Finding per (selector, mode) pair.
112+
Uses `evaluateAt` to run worst-case across accent variants
113+
when the pair is accent-sensitive.
114+
reporter.go Pretty prints violations and optional warnings.
115+
accent_matrix.go Runtime accent variants (the 8 macOS system accents +
116+
Cmdr gold) and the per-variant VarTable override.
117+
size_tiers.go `.size-*` utility classes × known container bgs, since
118+
they're only `color:` rules with no paired bg.
119+
row_state_matrix.go Selected-row text × (pane tint × stripe × cursor state ×
120+
accent variant) bg composition. Models the three-tier
121+
`--color-selection-fg` cascade.
122+
dropdown_states.go Hand-listed (descendant-text-var, ancestor-bg-var) tuples
123+
for "secondary text on accent-bg" cases.
91124
```
92125

93126
Tests:
@@ -96,6 +129,16 @@ Tests:
96129
- `resolver_test.go`: var() fallbacks, nested color-mix, dark overrides.
97130
- `parser_test.go`: app.css + Svelte parsing, selector extraction.
98131
- `analyzer_test.go`: cascade inheritance, known false-positive cases.
132+
- `accent_matrix_test.go`: variant sweep + per-variant resolution.
133+
134+
Diagnostic helpers (skipped by default; gated on env vars):
135+
136+
- `before_after_test.go` (`CMDR_PRINT_BEFORE_AFTER=1`): prints a curated before/after comparison table for the
137+
selected-row text-role × bg matrix. Useful when iterating on selection colors to see how a change affects every
138+
meaningful combination.
139+
- `diff_test.go` (`CMDR_PRINT_SELECTION_DIFF=1`, `CMDR_PRINT_RED_CANDIDATES=1`): prints the contrast ratio between the
140+
selected-row text color and the unselected-row text color across modes + accents (differentiation, not the AA-vs-bg
141+
question), plus a sweep of candidate red hexes against the worst- case bgs for picking a new selection color.
99142

100143
## Extending
101144

@@ -117,6 +160,21 @@ WCAG AA (current) uses 4.5:1 / 3:1. For AAA, change the constants in `analyzer.e
117160
Not built yet. If the team decides certain findings are acceptable (marketing badges, subtle hover tints), add a JSON
118161
allowlist alongside this README and filter in `main.go` before calling `Report`.
119162

163+
### Add a new "text from one selector, bg from another" scenario
164+
165+
When the rule walker can't pair a text role with its actual bg (the bg lives on an ancestor or sibling selector that the
166+
walker doesn't traverse), reach for one of the synthesizers:
167+
168+
- For file-list selected-row text colors, add a new entry to `rowSelectedTextRoles` in `row_state_matrix.go`.
169+
- For "secondary text on accent-bg" cases like dropdown options, append a new entry to `dropdownScenarios` in
170+
`dropdown_states.go`.
171+
- For a one-off pair (specific descendant + specific ancestor bg) that's not in either bucket, mirror the
172+
`dropdown_states.go` pattern in a new `<thing>_states.go` and wire it into `main.go` alongside the other
173+
`Analyzer.AnalyzeXxx` calls.
174+
175+
If the scenario depends on the active accent, you don't need to do anything special — the synthesizers iterate
176+
`AccentVariants` automatically.
177+
120178
## Known trade-offs
121179

122180
- No support for `light-dark()`. If Cmdr adopts that token pattern later, add parsing in resolver.go.

scripts/check-a11y-contrast/before_after_test.go

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package main
22

33
import (
44
"fmt"
5+
"maps"
56
"os"
67
"path/filepath"
78
"sort"
@@ -168,12 +169,8 @@ func passStr(p bool) string {
168169

169170
func cloneVars(v *VarTable) *VarTable {
170171
out := NewVarTable()
171-
for k, val := range v.Light {
172-
out.Light[k] = val
173-
}
174-
for k, val := range v.Dark {
175-
out.Dark[k] = val
176-
}
172+
maps.Copy(out.Light, v.Light)
173+
maps.Copy(out.Dark, v.Dark)
177174
return out
178175
}
179176

@@ -255,8 +252,8 @@ func evalScenario(base *VarTable, sc beforeAfterScenario, applyFallback bool) (F
255252
vars = withAccentOverride(vars, AccentVariant{Name: "yellow", Light: "#ffc601", Dark: "#ffc601"})
256253
}
257254
}
258-
if applyFallback && shouldUseSelectionFgFallback(sc.mode, sc.tint, sc.variant) {
259-
vars = withSelectionFgFallback(vars)
255+
if applyFallback {
256+
vars = withSelectionFgVariant(vars, selectionFgTokenFor(sc.mode, sc.tint, sc.variant))
260257
}
261258
bg, ok := resolveRowBg(vars, sc.mode, sc.tint, sc.variant)
262259
if !ok {

scripts/check-a11y-contrast/diff_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -143,8 +143,8 @@ func selectionDiffRatio(vars *VarTable, mode Mode, tint paneTintHue, variant str
143143
if !accent.IsDefault {
144144
work = withAccentOverride(work, accent)
145145
}
146-
if applyFallback && shouldUseSelectionFgFallback(mode, tint, variant) {
147-
work = withSelectionFgFallback(work)
146+
if applyFallback {
147+
work = withSelectionFgVariant(work, selectionFgTokenFor(mode, tint, variant))
148148
}
149149
fg, _ := resolveTextRole(work, mode, "color-selection-fg")
150150
textPrimary, _ := resolveTextRole(work, mode, "color-text-primary")

scripts/check-a11y-contrast/row_state_matrix.go

Lines changed: 31 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -260,13 +260,10 @@ func (a *Analyzer) evalRowCellForAccent(
260260
if !accent.IsDefault {
261261
vars = withAccentOverride(a.Vars, accent)
262262
}
263-
// Mirror the `selection-fg fallback` rule in `app.css`: in dark mode,
264-
// when the pane is tinted AND the row is cursor-active + selected,
265-
// `--color-selection-fg` swaps to `--color-selection-fg-fallback`.
266-
// The resolver doesn't model rule-level CSS overrides; apply by hand.
267-
if shouldUseSelectionFgFallback(mode, tint, variant) {
268-
vars = withSelectionFgFallback(vars)
269-
}
263+
// Mirror the three-tier `--color-selection-fg` cascade from `app.css`
264+
// + `FullList.svelte`. The resolver doesn't model rule-level CSS
265+
// overrides, so apply the right tier by hand per scenario.
266+
vars = withSelectionFgVariant(vars, selectionFgTokenFor(mode, tint, variant))
270267
bg, ok := resolveRowBg(vars, mode, tint, variant)
271268
if !ok {
272269
return 0
@@ -318,30 +315,39 @@ func evalRowText(
318315
}, true
319316
}
320317

321-
// shouldUseSelectionFgFallback mirrors the `app.css` rule that swaps
322-
// `--color-selection-fg` to its fallback. Keep this predicate in sync with
323-
// the CSS selector (see the "selection-fg fallback" block in `app.css`).
318+
// selectionFgTokenFor picks which `--color-selection-fg-*` variant applies
319+
// to a given scenario, mirroring the three-tier cascade in `app.css` +
320+
// `FullList.svelte`. Keep this in sync with the CSS rules: the order from
321+
// least- to most-specific is primary → cursor → fallback.
324322
//
325-
// Today the rule triggers when ALL of these hold:
326-
// - dark mode (light mode's primary value is dark enough to clear AA on
327-
// every tinted bg without help)
328-
// - the pane has a tint applied
329-
// - the row is the focused cursor-active row (`.is-under-cursor` +
330-
// focused container, in our matrix terms: `variant == "cursor-active"`).
331-
func shouldUseSelectionFgFallback(mode Mode, tint paneTintHue, variant string) bool {
332-
return mode == ModeDark && tint.VarName != "" && variant == "cursor-active"
323+
// The cascade today:
324+
// - default: `--color-selection-fg-primary` (the strong red, AA-safe vs
325+
// `--color-selection-bg`).
326+
// - row is under cursor: `--color-selection-fg-cursor` (a hair darker
327+
// red in light, lighter in dark, AA-safe vs the translucent cursor bg).
328+
// - dark + tinted pane + cursor-active: `--color-selection-fg-fallback`
329+
// (= `--color-text-primary`), because no red variant clears AA there.
330+
func selectionFgTokenFor(mode Mode, tint paneTintHue, variant string) string {
331+
if mode == ModeDark && tint.VarName != "" && variant == "cursor-active" {
332+
return "color-selection-fg-fallback"
333+
}
334+
if variant == "cursor-active" || variant == "cursor-inactive" {
335+
return "color-selection-fg-cursor"
336+
}
337+
return "color-selection-fg-primary"
333338
}
334339

335-
// withSelectionFgFallback returns a VarTable derived from v with
336-
// `--color-selection-fg` pointing at `--color-selection-fg-fallback` so the
337-
// `--color-size-*-selected` mixes (which reference `--color-selection-fg`)
338-
// pick up the swap automatically.
339-
func withSelectionFgFallback(v *VarTable) *VarTable {
340+
// withSelectionFgVariant returns a VarTable derived from v with
341+
// `--color-selection-fg` pointing at the named variant token. Both modes
342+
// get the override so the `--color-size-*-selected` mixes (which reference
343+
// `--color-selection-fg`) pick up the right tier automatically.
344+
func withSelectionFgVariant(v *VarTable, tokenName string) *VarTable {
340345
out := NewVarTable()
341346
maps.Copy(out.Light, v.Light)
342347
maps.Copy(out.Dark, v.Dark)
343-
out.Light["color-selection-fg"] = "var(--color-selection-fg-fallback)"
344-
out.Dark["color-selection-fg"] = "var(--color-selection-fg-fallback)"
348+
expr := fmt.Sprintf("var(--%s)", tokenName)
349+
out.Light["color-selection-fg"] = expr
350+
out.Dark["color-selection-fg"] = expr
345351
return out
346352
}
347353

0 commit comments

Comments
 (0)