From 0030bfb6cda8601fc4e58d617783299869142388 Mon Sep 17 00:00:00 2001 From: Sergey Kozyrenko Date: Sat, 25 Jul 2026 23:46:51 +0700 Subject: [PATCH] fix(ui): give link text its own token instead of reusing the fill colour MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --primary is a fill paired with white --primary-foreground, and as text on dark it measures 3.12:1 on --card (2.89:1 through the upload overlay's tint) — below AA. There is no lightness that clears AA in both roles: text needs L>=0.59 and white-on-fill needs L<=0.545, so the window is empty. Add --link, measured live at 6.34:1 on --card in dark and 15.59:1 in light, and point the link button variant, the table/file-manager/prompt hover links and the two drag overlays at it. The icon at flow-assistant-messages.tsx keeps --primary: it is a graphic and clears the 3:1 bar. The gate probed exactly one button variant, so the failure had been living under a waiver instead: probe every variant off the union the way the badge half already does, and drop the color-contrast waiver on the Screenshots source URL — that node is the link variant, and axe passes it now. The dark link row fails at 3.12 against the previous colour. Co-Authored-By: Claude Opus 4.8 --- frontend/e2e/routes.ts | 1 - frontend/e2e/specs/cross/contrast.spec.ts | 33 ++++++++++++++----- .../shared/file-manager/file-manager.tsx | 2 +- frontend/src/components/ui/button.tsx | 4 ++- frontend/src/components/ui/data-table.tsx | 2 +- frontend/src/components/ui/empty.tsx | 2 +- .../src/features/flows/files/flow-files.tsx | 2 +- frontend/src/pages/resources/resources.tsx | 2 +- .../src/pages/settings/settings-prompts.tsx | 4 +-- frontend/src/styles/index.css | 5 +++ 10 files changed, 40 insertions(+), 17 deletions(-) diff --git a/frontend/e2e/routes.ts b/frontend/e2e/routes.ts index a6ef2c01..b177c2f2 100644 --- a/frontend/e2e/routes.ts +++ b/frontend/e2e/routes.ts @@ -75,7 +75,6 @@ export const ROUTE_MANIFEST: RouteManifestEntry[] = [ { rule: 'aria-progressbar-name', tabs: ['Tasks'], target: /\.bg-primary\\\/20/ }, { rule: 'button-name', tabs: ['Files'], target: /tooltip-trigger.*size-8\[data-slot="button"\]/ }, { rule: 'target-size', tabs: ['Files'], target: /text-blue-400|button\[aria-label="Select / }, - { rule: 'color-contrast', tabs: ['Screenshots'], target: /\.text-primary > \.font-semibold\.truncate/ }, // The same file-manager row metadata waived on /resources — the Files tab embeds it. { rule: 'color-contrast', tabs: ['Files'], target: /text-muted-foreground\\?\/80/ }, { diff --git a/frontend/e2e/specs/cross/contrast.spec.ts b/frontend/e2e/specs/cross/contrast.spec.ts index 723d4306..40b9d0e7 100644 --- a/frontend/e2e/specs/cross/contrast.spec.ts +++ b/frontend/e2e/specs/cross/contrast.spec.ts @@ -1,6 +1,7 @@ import { readFileSync } from 'node:fs'; import type { BadgeVariant } from '@/components/ui/badge'; +import type { ButtonVariant } from '@/components/ui/button'; import { badgeVariants } from '@/components/ui/badge'; import { buttonVariants } from '@/components/ui/button'; @@ -32,7 +33,16 @@ const declaredEditorTokens = (): string[] => { return [...new Set(css.match(/--editor-[\w-]+/g) ?? [])].map((token) => token.slice(2)); }; -// Keyed off the union so a newly added variant fails to compile until it is probed. +// Keyed off the unions so a newly added variant fails to compile until it is probed. +const BUTTON_VARIANTS = Object.keys({ + default: true, + destructive: true, + ghost: true, + link: true, + outline: true, + secondary: true, +} satisfies Record) as ButtonVariant[]; + const BADGE_VARIANTS = Object.keys({ blue: true, default: true, @@ -91,19 +101,26 @@ for (const theme of THEMES) { } }); - test('the destructive button clears AA at rest and on hover', async ({ page }) => { + test('every button variant clears AA at rest and on hover', async ({ page }) => { await page.goto('/flows'); await expect(page.getByRole('row', { name: /E2E Alpha/ })).toBeVisible(); await expect(page.locator('html')).toHaveClass(theme === 'dark' ? /dark/ : /light/); - await mountContrastProbes(page, { destructive: buttonVariants({ variant: 'destructive' }) }); + await mountContrastProbes( + page, + Object.fromEntries(BUTTON_VARIANTS.map((variant) => [variant, buttonVariants({ variant })])), + ); - expect.soft(await measureContrast(page, 'destructive'), 'button at rest').toBeGreaterThanOrEqual(AA_NORMAL); + for (const variant of BUTTON_VARIANTS) { + expect + .soft(await measureContrast(page, variant), `button ${variant} at rest`) + .toBeGreaterThanOrEqual(AA_NORMAL); - await page.locator('[data-contrast="destructive"]').hover(); - expect - .soft(await measureContrast(page, 'destructive'), 'button on hover') - .toBeGreaterThanOrEqual(AA_NORMAL); + await page.locator(`[data-contrast="${variant}"]`).hover(); + expect + .soft(await measureContrast(page, variant), `button ${variant} on hover`) + .toBeGreaterThanOrEqual(AA_NORMAL); + } }); test('editor highlight tokens clear AA on the editor surface', async ({ page }) => { diff --git a/frontend/src/components/shared/file-manager/file-manager.tsx b/frontend/src/components/shared/file-manager/file-manager.tsx index 895174f5..e576a066 100644 --- a/frontend/src/components/shared/file-manager/file-manager.tsx +++ b/frontend/src/components/shared/file-manager/file-manager.tsx @@ -365,7 +365,7 @@ export function FileManager({ return (