fix(ui): match the editor variable colour to its panel badge, tag to violet

A used template variable read as green in the Available-variables panel (its
"used" badge) but violet in the editor highlight — the same entity, two colours.
Swap the editor variable to green and the <xml-tag> highlight to violet so it
stays distinct. A pure hue swap: lightness and chroma are untouched, and every
token still clears WCAG AA in both themes (tightest is the dark tag at 5.36).

Guard the editor palette in cross/contrast.spec.ts alongside the badges, mounting
the real .template-* rule so the shipped CSS is measured, not a copy — verified to
fail when a token is pushed under AA.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Sergey Kozyrenko
2026-07-21 18:48:15 +07:00
co-authored by Claude Opus 4.8
parent 10baac43da
commit 07a2d1fa2d
3 changed files with 61 additions and 7 deletions
+35
View File
@@ -29,6 +29,41 @@ export const mountContrastProbes = async (page: Page, probes: Record<string, str
);
};
// Editor highlight tokens are styled through the `.tiptap-content .ProseMirror .template-*`
// selector, so a flat probe span (as in mountContrastProbes) would pick up none of it and
// measure nothing. Reproduce the real ancestor chain on the shipped class. Surface is --card,
// within ~0.005 L of the editor's real `dark:bg-input/30` ground and conservative for the floor.
export const mountEditorProbes = async (page: Page, probes: Record<string, string>): Promise<void> => {
await page.evaluate(
({ entries, hostId }) => {
document.getElementById(hostId)?.remove();
const host = document.createElement('div');
host.id = hostId;
host.className = 'tiptap-content';
host.style.cssText = 'position:fixed;inset:0 auto auto 0;z-index:2147483647;padding:12px';
const surface = document.createElement('div');
surface.className = 'ProseMirror';
surface.style.cssText = 'background:var(--card);display:flex;gap:8px;padding:8px';
for (const [name, className] of entries) {
const probe = document.createElement('span');
probe.className = className;
probe.dataset.contrast = name;
probe.textContent = 'Sample';
surface.append(probe);
}
host.append(surface);
document.body.append(host);
},
{ entries: Object.entries(probes), hostId: HOST_ID },
);
};
export const measureContrast = async (page: Page, probe: string): Promise<number> =>
page.evaluate((name) => {
const element = document.querySelector<HTMLElement>(`[data-contrast="${name}"]`);
+19 -1
View File
@@ -4,9 +4,14 @@ import { badgeVariants } from '@/components/ui/badge';
import { buttonVariants } from '@/components/ui/button';
import { expect, test } from '../../fixtures/test.ts';
import { AA_NORMAL, measureContrast, mountContrastProbes } from '../../helpers/contrast.ts';
import { AA_NORMAL, measureContrast, mountContrastProbes, mountEditorProbes } from '../../helpers/contrast.ts';
import { flowsCassette } from '../../mocks/cassettes/flows.ts';
const EDITOR_TOKENS = {
'editor-tag': 'template-tag',
'editor-variable': 'template-variable',
} as const;
const BADGE_VARIANTS = [
'blue',
'default',
@@ -67,5 +72,18 @@ for (const theme of THEMES) {
.soft(await measureContrast(page, 'destructive'), 'button on hover')
.toBeGreaterThanOrEqual(AA_NORMAL);
});
test('editor highlight tokens clear AA on the editor surface', async ({ page }) => {
await page.goto('/flows');
await expect(page.getByRole('row', { name: /E2E Alpha/ })).toBeVisible();
await mountEditorProbes(page, EDITOR_TOKENS);
for (const token of Object.keys(EDITOR_TOKENS)) {
expect
.soft(await measureContrast(page, token), `${token} (${theme})`)
.toBeGreaterThanOrEqual(AA_NORMAL);
}
});
});
}
+7 -6
View File
@@ -308,11 +308,12 @@
--sidebar-ring: oklch(0.25 0.14 245);
/* Editor highlight TEXT, tuned for readable contrast on the editor surface (unlike --primary, a fill
color that fails WCAG AA as text on dark). Four distinct hues so adjacent tokens don't blur:
link = theme blue 245; variable = violet 300; tag = teal 155; inline code = red 25 on a visible chip.
link = theme blue 245; variable = green 155 (matches the panel's "used" badge); tag = violet 300;
inline code = red 25 on a visible chip.
--editor-code-bg is the opaque inline-code + code-block chip. Don't fold these back into --primary. */
--editor-accent: oklch(0.25 0.12 245);
--editor-variable: oklch(0.4 0.18 300);
--editor-tag: oklch(0.25 0.12 155);
--editor-variable: oklch(0.4 0.18 155);
--editor-tag: oklch(0.25 0.12 300);
--editor-code: oklch(0.5 0.16 25);
--editor-code-bg: oklch(0.94 0.006 245);
--font-sans: Inter, 'Inter Fallback', sans-serif;
@@ -371,8 +372,8 @@
--sidebar-border: oklch(0.3 0.04 245);
--sidebar-ring: oklch(0.5 0.16 245);
--editor-accent: oklch(0.68 0.13 245);
--editor-variable: oklch(0.72 0.12 300);
--editor-tag: oklch(0.68 0.13 155);
--editor-variable: oklch(0.72 0.12 155);
--editor-tag: oklch(0.68 0.13 300);
--editor-code: oklch(0.7 0.15 25);
--editor-code-bg: oklch(0.27 0.03 245);
--font-sans: Inter, 'Inter Fallback', sans-serif;
@@ -759,7 +760,7 @@
padding: 0 1px;
}
/* `<xml-tags>`, highlighted view-only (teal --editor-tag reads distinct from the violet variables). */
/* `<xml-tags>`, highlighted view-only. */
.tiptap-content .ProseMirror .template-tag {
color: var(--editor-tag);
background-color: color-mix(in oklab, var(--editor-tag) 9%, transparent);