mirror of
https://github.com/alphaXiv/OpenResearch.git
synced 2026-11-13 10:19:05 +08:00
* Keep editor selection from hiding highlighted code Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Use plain rgba editor selection, update selection tests, rebuild ui/dist Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Tidy editor selection test and note why the tint is a literal Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Lighten the dark editor selection so selected code stays readable A 32% white veil cut selected dark-theme code to ~4:1 and comments to ~3:1. At 16% they stay at 6.5:1 and 4.1:1; the test now checks selected-text contrast instead of matching the shared selection gray. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
133 lines
5.7 KiB
JavaScript
133 lines
5.7 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import { readFileSync } from "node:fs";
|
|
import test from "node:test";
|
|
|
|
const read = (file) => readFileSync(new URL(`../src/${file}`, import.meta.url), "utf8");
|
|
const base = read("base.css");
|
|
const app = read("app.css");
|
|
const theme = read("theme.css");
|
|
|
|
/** Declarations of the one top-level rule whose selector starts a line. */
|
|
function declarations(styles, selector) {
|
|
const escaped = selector.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
const matches = [...styles.matchAll(new RegExp(`^${escaped} \\{\\n([\\s\\S]*?)\\n\\}`, "gm"))];
|
|
assert.equal(matches.length, 1, `expected exactly one rule for ${selector}`);
|
|
return Object.fromEntries(
|
|
matches[0][1]
|
|
.split(";")
|
|
.map((declaration) => declaration.trim())
|
|
.filter(Boolean)
|
|
.map((declaration) => declaration.split(/:\s*/, 2)),
|
|
);
|
|
}
|
|
|
|
function themeTokens() {
|
|
const light = theme.match(/:root \{([\s\S]*?)\n\}/)?.[1];
|
|
const dark = theme.match(/:root\[data-theme="dark"\] \{([\s\S]*?)\n\}/)?.[1];
|
|
assert.ok(light !== undefined, "missing light theme tokens");
|
|
assert.ok(dark !== undefined, "missing dark theme tokens");
|
|
return [
|
|
{ name: "light", css: light },
|
|
{ name: "dark", css: dark },
|
|
];
|
|
}
|
|
|
|
function hexToken(css, name) {
|
|
const match = css.match(new RegExp(`--${name}:\\s*(#[0-9a-fA-F]{6})`));
|
|
assert.ok(match, `missing --${name}`);
|
|
return match[1];
|
|
}
|
|
|
|
const rgb = (hex) => hex.slice(1).match(/.{2}/g).map((channel) => Number.parseInt(channel, 16));
|
|
|
|
function luminance(hex) {
|
|
const channels = hex
|
|
.slice(1)
|
|
.match(/.{2}/g)
|
|
.map((channel) => Number.parseInt(channel, 16) / 255)
|
|
.map((channel) => (channel <= 0.04045 ? channel / 12.92 : ((channel + 0.055) / 1.055) ** 2.4));
|
|
assert.equal(channels.length, 3);
|
|
return 0.2126 * channels[0] + 0.7152 * channels[1] + 0.0722 * channels[2];
|
|
}
|
|
|
|
function contrast(first, second) {
|
|
const [lighter, darker] = [luminance(first), luminance(second)].sort((a, b) => b - a);
|
|
return (lighter + 0.05) / (darker + 0.05);
|
|
}
|
|
|
|
const globalSelection = () => declarations(base, "::selection");
|
|
|
|
test("text selection uses a global explicit foreground and background", () => {
|
|
assert.deepEqual(globalSelection(), { color: "var(--selection-text)", background: "var(--selection-background)" });
|
|
});
|
|
|
|
test("text selection meets contrast thresholds in both themes", () => {
|
|
for (const { name, css } of themeTokens()) {
|
|
const ratio = contrast(hexToken(css, "selection-text"), hexToken(css, "selection-background"));
|
|
const boundary = contrast(hexToken(css, "base"), hexToken(css, "selection-background"));
|
|
|
|
assert.ok(ratio >= 4.5, `${name} selected text contrast ${ratio.toFixed(2)} is below 4.5`);
|
|
assert.ok(boundary >= 1.2, `${name} selection boundary contrast ${boundary.toFixed(2)} is below 1.2`);
|
|
}
|
|
});
|
|
|
|
test("chat annotations stay separate from active selection", () => {
|
|
assert.ok(!base.includes(".chat-thread-inner ::selection"));
|
|
for (const { css } of themeTokens()) {
|
|
assert.notEqual(hexToken(css, "selection-background"), hexToken(css, "chat-annotation-highlight"));
|
|
}
|
|
});
|
|
|
|
test("editor selection tints the highlighted overlay instead of redrawing its text", () => {
|
|
assert.deepEqual(declarations(base, ".file-view-editarea::selection"), {
|
|
color: "transparent",
|
|
background: "var(--editor-selection)",
|
|
});
|
|
for (const { name, css } of themeTokens()) {
|
|
const tint = css.match(/--editor-selection: rgb\((\d+) (\d+) (\d+) \/ (\d+)%\)/);
|
|
assert.ok(tint, `${name} editor selection must be a translucent rgb() (no color-mix on macOS 11 WebKit)`);
|
|
const veil = (hex) =>
|
|
`#${rgb(hex)
|
|
.map((channel, i) => Math.round(channel + (Number(tint[i + 1]) - channel) * (Number(tint[4]) / 100)))
|
|
.map((channel) => channel.toString(16).padStart(2, "0"))
|
|
.join("")}`;
|
|
const background = hexToken(css, "base");
|
|
const selected = veil(background);
|
|
const boundary = contrast(background, selected);
|
|
assert.ok(boundary >= 1.2, `${name} editor selection boundary contrast ${boundary.toFixed(2)} is below 1.2`);
|
|
const text = contrast(veil(hexToken(css, "syntax-text")), selected);
|
|
assert.ok(text >= 4.5, `${name} selected code contrast ${text.toFixed(2)} is below 4.5`);
|
|
const comment = hexToken(css, "syntax-comment");
|
|
const ratio = contrast(veil(comment), selected) / contrast(comment, background);
|
|
assert.ok(ratio >= 0.8, `${name} selection costs comments ${((1 - ratio) * 100).toFixed(0)}% of their contrast`);
|
|
}
|
|
});
|
|
|
|
test("shimmering labels select with the same foreground as the surrounding text", () => {
|
|
const foreground = (color) => ({ color, "-webkit-text-fill-color": color });
|
|
|
|
assert.deepEqual(declarations(app, ".tool-running-shimmer::selection"), foreground(globalSelection().color));
|
|
});
|
|
|
|
test("diff selections use the readable shared foreground and background", () => {
|
|
assert.deepEqual(declarations(app, ".openresearch-diff ::selection"), {
|
|
color: "var(--selection-text)",
|
|
background: "var(--code-selection)",
|
|
});
|
|
const diff = read("components/GitDiff.tsx");
|
|
assert.ok(diff.includes("--diff-selection-text-color:var(--selection-text)"));
|
|
assert.ok(!diff.includes("--diff-selection-text-color:var(--primary)"));
|
|
assert.ok(theme.includes("--code-selection: var(--selection-background)"));
|
|
});
|
|
|
|
test("selected syntax uses the shared contrast pair even for low-contrast tokens", () => {
|
|
assert.ok(!base.includes("currentColor"));
|
|
assert.ok(!base.includes("code ::selection"));
|
|
for (const { name, css } of themeTokens()) {
|
|
const background = hexToken(css, "selection-background");
|
|
const selected = hexToken(css, "selection-text");
|
|
assert.ok(contrast(hexToken(css, "syntax-comment"), background) < 4.5);
|
|
assert.ok(contrast(selected, background) >= 4.5, `${name} selected syntax must remain readable`);
|
|
}
|
|
});
|