From b5f010fbb833ff60a9c7ea4ef8e3cf707b96a2b8 Mon Sep 17 00:00:00 2001 From: ciregenz Date: Wed, 12 Aug 2026 18:25:03 -0700 Subject: [PATCH] [eric] ui: every input inherits readable colours from the theme, so no field can be black on black (ENG-281) --- frontend/src/app/Main.tsx | 2 + .../src/shared/styles/inputOverrides.test.ts | 48 +++++++++++++++++++ frontend/src/shared/styles/inputOverrides.ts | 24 ++++++++++ 3 files changed, 74 insertions(+) create mode 100644 frontend/src/shared/styles/inputOverrides.test.ts create mode 100644 frontend/src/shared/styles/inputOverrides.ts diff --git a/frontend/src/app/Main.tsx b/frontend/src/app/Main.tsx index f229bb6c..68511b0c 100644 --- a/frontend/src/app/Main.tsx +++ b/frontend/src/app/Main.tsx @@ -75,6 +75,7 @@ import { useInteractionHeartbeat } from '@/shared/hooks/useInteractionHeartbeat' import { ThemeProvider, useThemeMode, useThemeAccent, useClaudeTokens } from '@/shared/styles/ThemeContext'; import { ClaudeTokens } from '@/shared/styles/claudeTokens'; import { alertStyleOverrides } from '@/shared/styles/alertOverrides'; +import { inputStyleOverrides } from '@/shared/styles/inputOverrides'; function buildMuiTheme(c: ClaudeTokens, mode: 'light' | 'dark') { return createTheme({ @@ -185,6 +186,7 @@ function buildMuiTheme(c: ClaudeTokens, mode: 'light' | 'dark') { }, }, MuiAlert: { styleOverrides: alertStyleOverrides(c) }, + MuiOutlinedInput: { styleOverrides: inputStyleOverrides(c) }, MuiTooltip: { styleOverrides: { tooltip: { diff --git a/frontend/src/shared/styles/inputOverrides.test.ts b/frontend/src/shared/styles/inputOverrides.test.ts new file mode 100644 index 00000000..36c6dd18 --- /dev/null +++ b/frontend/src/shared/styles/inputOverrides.test.ts @@ -0,0 +1,48 @@ +// Run: node --test (via frontend/scripts/run-tests.mjs) +// +// ENG-281: the question flow's "Other..." box rendered black text on a black field, because a bare +// uses MUI's palette rather than ours. The defence is at the theme, so the assertion +// that matters is not "this field is styled" but "text and background can never be the same value". +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import { inputStyleOverrides } from './inputOverrides.ts'; +import type { ClaudeTokens } from './claudeTokens'; + +// Two grounds, because the bug only showed on one of them and a light-only fix would have "passed". +const dark = { + text: { primary: '#ECECEC', tertiary: '#8A8A8A', ghost: '#5A5A5A' }, + bg: { surface: '#1E1E1E' }, + border: { medium: '#333', strong: '#555' }, + accent: { primary: '#D97757' }, +} as unknown as ClaudeTokens; + +const light = { + text: { primary: '#141414', tertiary: '#767676', ghost: '#A0A0A0' }, + bg: { surface: '#FFFFFF' }, + border: { medium: '#DDD', strong: '#BBB' }, + accent: { primary: '#D97757' }, +} as unknown as ClaudeTokens; + +for (const [name, tokens] of [['dark', dark], ['light', light]] as const) { + test(`${name}: typed text is never the same colour as the field it sits on`, () => { + const root = inputStyleOverrides(tokens).root as Record; + assert.notEqual(root.color, root.backgroundColor, 'text and field background are identical'); + }); + + test(`${name}: the inner input and textarea inherit the readable colour, not MUI's`, () => { + const root = inputStyleOverrides(tokens).root as Record>; + assert.equal(root['& input, & textarea'].color, (tokens as unknown as { text: { primary: string } }).text.primary); + }); + + test(`${name}: the placeholder is readable and not left on inherited opacity`, () => { + const root = inputStyleOverrides(tokens).root as Record>; + const ph = root['& input::placeholder, & textarea::placeholder']; + assert.equal(ph.opacity, 1, 'MUI dims placeholders via opacity on an already-wrong colour'); + assert.notEqual(ph.color, (tokens as unknown as { bg: { surface: string } }).bg.surface); + }); +} + +test('the override actually sets a colour at all, so a future empty return fails here', () => { + const root = inputStyleOverrides(dark).root as Record; + assert.ok(root.color && root.backgroundColor, 'override returned nothing to inherit'); +}); diff --git a/frontend/src/shared/styles/inputOverrides.ts b/frontend/src/shared/styles/inputOverrides.ts new file mode 100644 index 00000000..f59f7568 --- /dev/null +++ b/frontend/src/shared/styles/inputOverrides.ts @@ -0,0 +1,24 @@ +import type { ClaudeTokens } from './claudeTokens'; + +// A bare renders with MUI's own palette, not ours. On a dark surface that is black text +// on a black field: you cannot see what you are typing (ENG-281, hit on the question flow's +// "Other..." box). Fixing the one call site would leave every future bare field able to do it again, +// so the colours are pinned at the theme and every input in the app inherits them. + +export function inputStyleOverrides(c: ClaudeTokens): Record { + return { + root: { + color: c.text.primary, + backgroundColor: c.bg.surface, + borderRadius: 10, + '& input, & textarea': { color: c.text.primary }, + // Placeholders need their own rule: MUI paints them via opacity on the inherited colour, which + // lands invisible when the inherited colour was already wrong. + '& input::placeholder, & textarea::placeholder': { color: c.text.tertiary, opacity: 1 }, + '& fieldset': { borderColor: c.border.medium }, + '&:hover fieldset': { borderColor: c.border.strong }, + '&.Mui-focused fieldset': { borderColor: c.accent.primary }, + '&.Mui-disabled': { color: c.text.ghost }, + }, + }; +}