Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion packages/diffs/src/editor/editor.css
Original file line number Diff line number Diff line change
Expand Up @@ -275,8 +275,14 @@
border: 0;
pointer-events: auto;

/* `contrast-color()` returns black or white against the severity fill. A
theme can set that fill through `editorWarning.foreground` and the similar
tokens, so a fixed text color can fail.
Pass one argument only. Two arguments make the declaration invalid at
computed-value time: `color` then inherits the editor foreground and
ignores the `--diffs-marker-contrast` declaration above. */
@supports (color: contrast-color(red)) {
color: contrast-color(var(--diffs-marker-bg), var(--diffs-bg));
color: contrast-color(var(--diffs-marker-bg));

@mdo mdo Aug 6, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To keep the intent here, this was meant to be a fallback. Can probably update the code comment to remove mention of two colors.

Suggested change
color: contrast-color(var(--diffs-marker-bg));
color: contrast-color(var(--diffs-marker-bg, var(--diffs-bg)));

}

[data-marker-message] {
Expand Down
83 changes: 83 additions & 0 deletions packages/diffs/test/cssContrastColorArity.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { describe, expect, test } from 'bun:test';
import { readFileSync } from 'fs';
import { resolve } from 'path';

// CSS files that ship with the package. The tests below check every
// `contrast-color()` call in them.
const STYLESHEETS = ['../src/style.css', '../src/editor/editor.css'];

// Splits a `contrast-color()` argument list into its top-level arguments. A
// comma inside `var()`, `light-dark()`, or `color-mix()` does not split.
function splitTopLevelArgs(args: string): string[] {
const parts: string[] = [];
let depth = 0;
let current = '';
for (const char of args) {
if (char === '(') {
depth += 1;
} else if (char === ')') {
depth -= 1;
} else if (char === ',' && depth === 0) {
parts.push(current.trim());
current = '';
continue;
}
current += char;
}
parts.push(current.trim());
return parts;
}

// Returns the argument list of every `contrast-color()` call in `css`. It counts
// nested parens to find the closing paren of each call.
function findContrastColorArgs(css: string): string[] {
const calls: string[] = [];
const marker = 'contrast-color(';
let index = css.indexOf(marker);
while (index !== -1) {
let depth = 1;
let cursor = index + marker.length;
while (cursor < css.length && depth > 0) {
if (css[cursor] === '(') {
depth += 1;
} else if (css[cursor] === ')') {
depth -= 1;
}
cursor += 1;
}
calls.push(css.slice(index + marker.length, cursor - 1));
index = css.indexOf(marker, cursor);
}
return calls;
}

// Regression test for white text on the yellow warning popover.
// `contrast-color()` takes one argument. A call with two arguments still parses,
// because `var()` defers the grammar check to substitution. The declaration is
// then invalid at computed-value time, so `color` inherits its value and ignores
// the `--diffs-marker-contrast` declaration. No browser test catches this:
// Playwright bundles an old Chromium without `contrast-color()`, so it runs the
// fallback branch only.
describe('contrast-color() arity in shipped CSS', () => {
for (const stylesheet of STYLESHEETS) {
test(`${stylesheet} passes one argument per call`, () => {
const css = readFileSync(resolve(__dirname, stylesheet), 'utf-8');
for (const args of findContrastColorArgs(css)) {
expect(
splitTopLevelArgs(args),
`contrast-color(${args}) must take exactly one argument`
).toHaveLength(1);
}
});
}

test('detects a two-argument call', () => {
expect(
splitTopLevelArgs(
findContrastColorArgs(
'a { color: contrast-color(var(--a), light-dark(#000, #fff)); }'
)[0]
)
).toHaveLength(2);
});
});
4 changes: 3 additions & 1 deletion packages/diffs/test/e2e/markers.pw.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,9 @@ function openScrolledMarkerNearGutter(page: Page): Promise<{
// contrast ratio between the popover's resolved text and background colors.
// Guards the marker popover fallback in browsers without contrast-color(): the
// severity fill is a theme editorX.foreground token, so its text candidate must
// remain legible in both light and dark themes.
// remain legible in both light and dark themes. Playwright bundles an old
// Chromium without contrast-color(), so this test measures the fallback branch
// only. cssContrastColorArity.test.ts covers the `@supports` branch.
async function popoverContrast(
page: Page,
token: string
Expand Down