mirror of
https://github.com/vxcontrol/pentagi.git
synced 2026-08-26 04:56:36 +00:00
fix(editor): keep highlight tokens readable inside code blocks
Syntax highlighting handed the code-block surface to atom-one-dark, which is
#282c34 in both themes, but the `{{.Var}}` / `<tag>` 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 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
a7c0c19b53
commit
a6fc854e0b
@@ -83,11 +83,14 @@ export const mountEditorProbes = async (page: Page, probes: Record<string, Edito
|
||||
};
|
||||
|
||||
export const measureContrast = async (page: Page, probe: string): Promise<number> =>
|
||||
page.evaluate((name) => {
|
||||
const element = document.querySelector<HTMLElement>(`[data-contrast="${name}"]`);
|
||||
measureContrastAt(page, `[data-contrast="${probe}"]`);
|
||||
|
||||
export const measureContrastAt = async (page: Page, selector: string): Promise<number> =>
|
||||
page.evaluate((target) => {
|
||||
const element = document.querySelector<HTMLElement>(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<number
|
||||
return 0.2126 * channel(r) + 0.7152 * channel(g) + 0.0722 * channel(b);
|
||||
};
|
||||
|
||||
// The text goes on top of the same stack, not on its own: `paint` starts from transparent,
|
||||
// where a translucent colour keeps its opaque base and reads as a contrast it does not have.
|
||||
const ground = [
|
||||
getComputedStyle(element.parentElement).backgroundColor,
|
||||
getComputedStyle(element).backgroundColor,
|
||||
];
|
||||
// Walk to the first opaque ancestor, not just the parent: a probe inside a code block sits on
|
||||
// `pre.hljs` through a transparent `code`, and stopping at the parent would measure it against
|
||||
// nothing. `paint` starts from transparent, so a translucent layer with no opaque base under it
|
||||
// keeps its own colour and reads as a contrast it does not have.
|
||||
const stack: string[] = [];
|
||||
|
||||
for (let node: HTMLElement | null = element.parentElement; node; node = node.parentElement) {
|
||||
const background = getComputedStyle(node).backgroundColor;
|
||||
const alpha = Number(background.match(/rgba?\([^)]*,\s*([\d.]+)\s*\)/)?.[1] ?? '1');
|
||||
|
||||
if (alpha === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
stack.unshift(background);
|
||||
|
||||
if (alpha === 1) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const ground = [...stack, getComputedStyle(element).backgroundColor];
|
||||
const surface = paint(...ground);
|
||||
const label = paint(...ground, getComputedStyle(element).color);
|
||||
const [high = 0, low = 0] = [luminance(label), luminance(surface)].sort((a, b) => b - a);
|
||||
|
||||
return (high + 0.05) / (low + 0.05);
|
||||
}, probe);
|
||||
}, selector);
|
||||
|
||||
@@ -32,7 +32,7 @@ export const RICH_PROMPT_TEMPLATE = [
|
||||
'- report every finding with evidence',
|
||||
'',
|
||||
'```bash',
|
||||
'nmap -sV {{.Target}}',
|
||||
'nmap -sV {{.Target}} --script <default>',
|
||||
'```',
|
||||
'',
|
||||
'| Field | Value |',
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user