From a6fc854e0b5bdd1693e6aeb2f968f3fb4ccc7850 Mon Sep 17 00:00:00 2001 From: Sergey Kozyrenko Date: Sat, 25 Jul 2026 22:36:23 +0700 Subject: [PATCH] fix(editor): keep highlight tokens readable inside code blocks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Syntax highlighting handed the code-block surface to atom-one-dark, which is #282c34 in both themes, but the `{{.Var}}` / `` view decorations kept their theme tokens — dark ink meant for a light ground. Measured on the stand: a tag inside a fence sits at 1.16:1 in light theme (a variable at 1.72), and dark had slipped to 4.05, below AA. Outside a fence both are unaffected. Re-point the two tokens at the dark ground inside `pre`, where they measure 5.77 / 5.30 in both themes. The contrast gate could not see this: it mounts probes on a synthetic --card surface and composited only the probe's immediate parent, so a span inside a transparent `code` measured against nothing. Walk to the first opaque ancestor instead, and pin the real rendered spans in the prompt-detail editor — the cassette's bash fence now carries a tag as well as a variable. Both new rows fail on the previous CSS with exactly the numbers above. Co-Authored-By: Claude Opus 4.8 --- frontend/e2e/helpers/contrast.ts | 39 ++++++++++++----- .../e2e/mocks/cassettes/settings-prompts.ts | 2 +- frontend/e2e/specs/cross/contrast.spec.ts | 42 ++++++++++++++++++- frontend/src/styles/index.css | 6 ++- 4 files changed, 76 insertions(+), 13 deletions(-) diff --git a/frontend/e2e/helpers/contrast.ts b/frontend/e2e/helpers/contrast.ts index 463f456e..918c5cad 100644 --- a/frontend/e2e/helpers/contrast.ts +++ b/frontend/e2e/helpers/contrast.ts @@ -83,11 +83,14 @@ export const mountEditorProbes = async (page: Page, probes: Record => - page.evaluate((name) => { - const element = document.querySelector(`[data-contrast="${name}"]`); + measureContrastAt(page, `[data-contrast="${probe}"]`); + +export const measureContrastAt = async (page: Page, selector: string): Promise => + page.evaluate((target) => { + const element = document.querySelector(target); if (!element?.parentElement) { - throw new Error(`contrast probe "${name}" is not mounted`); + throw new Error(`contrast target "${target}" is not mounted`); } const canvas = document.createElement('canvas'); @@ -138,15 +141,31 @@ export const measureContrast = async (page: Page, probe: string): Promise b - a); return (high + 0.05) / (low + 0.05); - }, probe); + }, selector); diff --git a/frontend/e2e/mocks/cassettes/settings-prompts.ts b/frontend/e2e/mocks/cassettes/settings-prompts.ts index b75b94b4..c9531ffb 100644 --- a/frontend/e2e/mocks/cassettes/settings-prompts.ts +++ b/frontend/e2e/mocks/cassettes/settings-prompts.ts @@ -32,7 +32,7 @@ export const RICH_PROMPT_TEMPLATE = [ '- report every finding with evidence', '', '```bash', - 'nmap -sV {{.Target}}', + 'nmap -sV {{.Target}} --script ', '```', '', '| Field | Value |', diff --git a/frontend/e2e/specs/cross/contrast.spec.ts b/frontend/e2e/specs/cross/contrast.spec.ts index 0c1756ca..723d4306 100644 --- a/frontend/e2e/specs/cross/contrast.spec.ts +++ b/frontend/e2e/specs/cross/contrast.spec.ts @@ -8,8 +8,15 @@ import { buttonVariants } from '@/components/ui/button'; import type { EditorProbe } from '../../helpers/contrast.ts'; import { expect, test } from '../../fixtures/test.ts'; -import { AA_NORMAL, measureContrast, mountContrastProbes, mountEditorProbes } from '../../helpers/contrast.ts'; +import { + AA_NORMAL, + measureContrast, + measureContrastAt, + mountContrastProbes, + mountEditorProbes, +} from '../../helpers/contrast.ts'; import { flowsCassette } from '../../mocks/cassettes/flows.ts'; +import { PROMPT_DETAIL_AGENT, promptDetailCassette } from '../../mocks/cassettes/settings-prompts.ts'; const EDITOR_PROBES = { 'editor-accent': { tag: 'a' }, @@ -113,4 +120,37 @@ for (const theme of THEMES) { } }); }); + + test.describe(`contrast in a code block (${theme})`, { tag: '@cross' }, () => { + test.use({ cassette: promptDetailCassette() }); + + if (theme === 'dark') { + test.beforeEach(async ({ page }) => { + await page.addInitScript(() => window.localStorage.setItem('theme', 'dark')); + }); + } + + test('editor highlight tokens clear AA inside a fence', async ({ page }) => { + await page.goto(`/settings/prompts/${PROMPT_DETAIL_AGENT}`); + + const fence = page.locator('.tiptap-content .ProseMirror pre'); + + await expect(fence).toBeVisible(); + await expect(page.locator('html')).toHaveClass(theme === 'dark' ? /dark/ : /light/); + + // The atom-one-dark stylesheet ships with the editor chunk. Measuring before it lands would + // put the probes on the page ground and pass on a surface no user ever sees. + await expect(fence).toHaveCSS('background-color', 'rgb(40, 44, 52)'); + + for (const token of ['variable', 'tag'] as const) { + await expect(fence.locator(`.template-${token}`).first()).toBeVisible(); + expect + .soft( + await measureContrastAt(page, `.tiptap-content .ProseMirror pre .template-${token}`), + `editor-${token} inside a code block (${theme})`, + ) + .toBeGreaterThanOrEqual(AA_NORMAL); + } + }); + }); } diff --git a/frontend/src/styles/index.css b/frontend/src/styles/index.css index 18d214ae..af302f9b 100644 --- a/frontend/src/styles/index.css +++ b/frontend/src/styles/index.css @@ -634,8 +634,12 @@ font-size: 0.875em; } -/* No background/colour here — the .hljs highlight.js theme (imported in markdown-editor.tsx) supplies them. */ +/* No background/colour here — the .hljs highlight.js theme (imported in markdown-editor.tsx) supplies them. + That surface is dark in both themes, so the highlight tokens are re-pointed at values that clear AA on it. */ .tiptap-content .ProseMirror pre { + --editor-variable: oklch(0.76 0.12 155); + --editor-tag: oklch(0.76 0.13 300); + position: relative; padding: 0.75rem 1rem; border-radius: calc(var(--radius) - 2px);