fix(client): one border story for every input

Inputs disagreed on both hover and focus. The theme's MuiOutlinedInput
hovered to text.primary - near-black in light mode - while Select,
TextInput and DatePicker each overrode hover back to `divider`, which is
the resting colour, so those had no hover cue at all. Four behaviours
across four components.

Now defined once in the theme and inherited by every outlined input:

  rest   divider           1.55:1 light / 1.73:1 dark
  hover  text.disabled     3.64:1 light / 4.66:1 dark
  focus  primary, 2px      5.93:1 light / 6.36:1 dark
  error  error.main

The per-component overrides in Select, TextInput and DatePicker are gone,
so there is one place to change this next time.

Focus in dark mode takes primary.light. The brand green is only 2.86:1 on
the dark ground, which made a focused input dimmer than a hovered one.
`main` is deliberately left alone: it is the fill for contained buttons,
and lightening it would flip their label to dark text across 31 sites.

Claude-Session: https://claude.ai/code/session_01AbYLvwpEaZFXktHVcqrFRd
This commit is contained in:
gorkem-bwl committed 2026-09-03 01:27:42 -04:00
1 parent ac94ae0fdd
commit 3ddc6ef1cb
5 files changed
+26 -16

No files matched your search

+3 -1
View File
@@ -66,7 +66,9 @@ export const DatePickerComponent = ({
},
"&:not(:has(.Mui-disabled)):not(:has(.Mui-error)) .MuiOutlinedInput-root:not(:has(input:focus)):hover fieldset":
{
borderColor: error ? theme.palette.error.main : theme.palette.divider,
borderColor: error
? theme.palette.error.main
: theme.palette.text.disabled,
},
},
},
-4
View File
@@ -83,10 +83,6 @@ const SelectInputInner = <T,>(
},
"& .MuiOutlinedInput-notchedOutline": {
borderRadius: theme.shape.borderRadius,
borderColor: theme.palette.divider,
},
"&:hover .MuiOutlinedInput-notchedOutline": {
borderColor: theme.palette.divider,
},
...props.sx,
}}
@@ -26,12 +26,6 @@ export const TextInput = forwardRef<HTMLInputElement, TextInputProps>(function T
fontSize: typographyLevels.base,
overflow: "hidden",
},
"& .MuiOutlinedInput-notchedOutline": {
borderColor: theme.palette.divider,
},
"&:hover .MuiOutlinedInput-notchedOutline": {
borderColor: theme.palette.divider,
},
"& .MuiFormHelperText-root": {
marginLeft: 0,
marginRight: 0,
+5
View File
@@ -112,6 +112,11 @@ export const lightPalette = {
export const darkPalette = {
primary: {
main: colors.brandGreen,
// The dark ground needs a lighter green for anything drawn as a line
// rather than a fill - focus rings especially, which were dimmer than
// their own hover state. Leaving `main` alone keeps contained buttons
// white-on-green instead of flipping them to dark text.
light: colors.brandGreenLight,
},
action: {
rowHover: alpha("#FFFFFF", HOVER.ROW),
+18 -5
View File
@@ -189,17 +189,30 @@ export const theme = (mode: string, palette: any) =>
},
},
// One border story for every outlined input - text fields, selects,
// autocompletes, date and colour pickers all route through this.
// Resting: divider. Hover: a step darker, so there is a cue without
// jumping to near-black. Focus: the brand, at 2px.
MuiOutlinedInput: {
styleOverrides: {
root: ({ theme }) => ({
"&:hover .MuiOutlinedInput-notchedOutline": {
"& .MuiOutlinedInput-notchedOutline": {
borderColor: theme.palette.divider,
transition: "border-color 0.15s ease",
},
"&:hover:not(.Mui-focused):not(.Mui-disabled) .MuiOutlinedInput-notchedOutline":
{
borderColor: theme.palette.text.disabled,
},
"&.Mui-focused .MuiOutlinedInput-notchedOutline": {
borderColor:
theme.palette.mode === "dark"
? "rgba(255, 255, 255, 0.23)"
: theme.palette.text.primary,
? theme.palette.primary.light
: theme.palette.primary.main,
borderWidth: 2,
},
"&.Mui-focused .MuiOutlinedInput-notchedOutline": {
borderColor: palette.primary.main,
"&.Mui-error .MuiOutlinedInput-notchedOutline": {
borderColor: theme.palette.error.main,
},
}),
},