From 743b6f5eac758425db868d26ebabb5dabfbefcd7 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Wed, 7 Oct 2026 23:00:28 -0400 Subject: [PATCH] fix(web): error text uses error-fg on every surface, not only on tints (#3150) Raw error red fails AA as text even with no tint behind it: in dark mode it measures 3.63:1 on obsidian, 3.23 on iron and 2.64 on slate, against 4.5. error-fg (the house formula, 50% toward parchment) measures 5.30 at worst across both modes. - All 19 text-error uses become text-error-fg: the "Couldn't load" messages on the admin pages, the integrations form errors, the flag popover, and the error toast's text. The toast keeps its error border, since a border is a graphic with a 3:1 floor. - check-tint-contrast flags raw error text anywhere (text-error, class:text-error, color: var(--fs-error)) and leaves borders and outlines alone. Run against the files before the swap, it finds all 19. Co-Authored-By: Claude Opus 5.5 --- web/scripts/check-tint-contrast.js | 23 ++++++++++++++++++- web/scripts/check-tint-contrast.test.js | 16 ++++++++++++- web/src/lib/components/FlagPopover.svelte | 2 +- web/src/lib/components/ToastHost.svelte | 2 +- web/src/routes/admin/diagnostics/+page.svelte | 2 +- web/src/routes/admin/duplicates/+page.svelte | 2 +- .../routes/admin/integrations/+page.svelte | 16 ++++++------- .../routes/admin/missing-files/+page.svelte | 2 +- .../routes/admin/playback-errors/+page.svelte | 2 +- web/src/routes/admin/quarantine/+page.svelte | 4 ++-- web/src/routes/admin/requests/+page.svelte | 2 +- web/src/routes/admin/users/+page.svelte | 4 ++-- 12 files changed, 56 insertions(+), 21 deletions(-) diff --git a/web/scripts/check-tint-contrast.js b/web/scripts/check-tint-contrast.js index 10ac4e73..a86abafc 100644 --- a/web/scripts/check-tint-contrast.js +++ b/web/scripts/check-tint-contrast.js @@ -64,6 +64,27 @@ export function findCss(source) { return found; } +// Hues that fail AA as text even on a plain surface, so the raw hue is never +// text at all: error measures 2.64:1 on slate and 3.23 on iron in dark mode, +// against 4.5; error-fg measures 5.30 at worst. A border or outline in the +// hue is a graphic with a 3:1 floor and stays allowed. +export const TEXT_NEVER_RAW = ['error']; + +/** Raw text-X classes and color: var(--fs-X) for the hues above. */ +export function findRawText(source) { + const found = []; + for (const hue of TEXT_NEVER_RAW) { + const patterns = [ + [new RegExp(`(?<=^|[\\s'"\`:])text-${hue}(?=$|[\\s'"\`=])`, 'g'), `text-${hue}-fg`], + [new RegExp(`(? { }); }); + describe('raw error text, on any surface', () => { + test('finds text-error, class:text-error and color: var(--fs-error)', () => { + expect(findRawText(`

x

`)).toHaveLength(1); + expect(findRawText(`
`)).toHaveLength(1); + expect(findRawText(`.msg { color: var(--fs-error); }`)[0].fix).toBe('var(--fs-error-fg)'); + }); + + test('passes error-fg, and error as a border or outline', () => { + expect(findRawText(`

x

`)).toEqual([]); + expect(findRawText(`
`)).toEqual([]); + expect(findRawText(`.a { border-color: var(--fs-error); outline-color: var(--fs-error); }`)).toEqual([]); + }); + }); + describe('CSS', () => { const tinted = (color) => `.pill { background: color-mix(in srgb, var(--fs-warning) 15%, transparent); ${color} }`; diff --git a/web/src/lib/components/FlagPopover.svelte b/web/src/lib/components/FlagPopover.svelte index a3a75db6..ebccc63a 100644 --- a/web/src/lib/components/FlagPopover.svelte +++ b/web/src/lib/components/FlagPopover.svelte @@ -79,7 +79,7 @@ > {#if error} -

Couldn't save flag — {error}

+

Couldn't save flag — {error}

{/if}
diff --git a/web/src/routes/admin/diagnostics/+page.svelte b/web/src/routes/admin/diagnostics/+page.svelte index 7224421b..bd7c487f 100644 --- a/web/src/routes/admin/diagnostics/+page.svelte +++ b/web/src/routes/admin/diagnostics/+page.svelte @@ -325,7 +325,7 @@ {#if diagQuery.isError} -

Couldn't load: {errMessage(diagQuery.error)}

+

Couldn't load: {errMessage(diagQuery.error)}

{:else if diagQuery.isPending}

Loading…

{:else if rows.length === 0} diff --git a/web/src/routes/admin/duplicates/+page.svelte b/web/src/routes/admin/duplicates/+page.svelte index b401b1ba..5cf7a768 100644 --- a/web/src/routes/admin/duplicates/+page.svelte +++ b/web/src/routes/admin/duplicates/+page.svelte @@ -188,7 +188,7 @@ {#if query.isPending}

Loading duplicates…

{:else if query.isError} -

Couldn't load the duplicates report.

+

Couldn't load the duplicates report.

{:else if groups.length === 0} diff --git a/web/src/routes/admin/integrations/+page.svelte b/web/src/routes/admin/integrations/+page.svelte index 9d6bae2d..f98096b1 100644 --- a/web/src/routes/admin/integrations/+page.svelte +++ b/web/src/routes/admin/integrations/+page.svelte @@ -463,7 +463,7 @@ {/each} {#if testListErrors.quality_profiles} -

+

Couldn't fetch quality profiles — {testListErrors.quality_profiles}

{/if} @@ -481,7 +481,7 @@ {/each} {#if testListErrors.metadata_profiles} -

+

Couldn't fetch metadata profiles — {testListErrors.metadata_profiles}

{/if} @@ -501,7 +501,7 @@ {/each} {#if testListErrors.root_folders} -

+

Couldn't fetch root folders — {testListErrors.root_folders}

{/if} @@ -513,11 +513,11 @@ Connected — Lidarr {testResult.version}

{:else} -

Connection failed — {testResult.error}

+

Connection failed — {testResult.error}

{/if} {/if} {#if saveError} -

Save failed — {saveError}

+

Save failed — {saveError}

{/if}
@@ -625,7 +625,7 @@ OK{coverTestResults[provider.id].duration_ms ? ` (${coverTestResults[provider.id].duration_ms}ms)` : ''}

{:else} -

+

Failed — {coverTestResults[provider.id].error}

{/if} @@ -722,7 +722,7 @@ OK{tagTestResults[provider.id].duration_ms ? ` (${tagTestResults[provider.id].duration_ms}ms)` : ''}

{:else} -

+

Failed — {tagTestResults[provider.id].error}

{/if} @@ -837,7 +837,7 @@ class="mt-3 w-full rounded-md border border-border bg-background px-3 py-2 font-mono text-sm text-text-primary placeholder:text-text-muted focus:outline-none focus:ring-2 focus:ring-accent" /> {#if disconnectError} -

Disconnect failed — {disconnectError}

+

Disconnect failed — {disconnectError}

{/if}