[eric] ui: every input inherits readable colours from the theme, so no field can be black on black (ENG-281)

This commit is contained in:
ciregenz
2026-08-12 18:25:03 -07:00
parent 471c0a9dd5
commit b5f010fbb8
3 changed files with 74 additions and 0 deletions
+2
View File
@@ -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: {
@@ -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
// <TextField> 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<string, string>;
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<string, Record<string, string>>;
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<string, Record<string, string | number>>;
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<string, string>;
assert.ok(root.color && root.backgroundColor, 'override returned nothing to inherit');
});
@@ -0,0 +1,24 @@
import type { ClaudeTokens } from './claudeTokens';
// A bare <TextField> 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<string, object> {
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 },
},
};
}