feat(client): one control height of 32px

Buttons, inputs, selects, pickers, toggles and tabs all declared 34 while
IconButton and the PageHeader action sat at 32, so anything mixing them
in a row was 2px out of alignment.

Everything now reads CONTROL_HEIGHT (32) from constants, so the number
lives in one place instead of nine.

Two content-box fixes were needed to make 32 safe, both measured in the
browser rather than assumed:

- MUI's button line-height is 1.75 - 22.75px at our 13px base - which
  with 6px padding needs 34.75px. It was already over its own box at 34
  and would have clipped at 32. Now 1.2, which leaves headroom for the
  taller scripts we ship (ar, th, vi, ja, zh).
- MUI pads the inner input 8.5px top and bottom, so an input's content
  needed 35.7px inside a 32px shell and was being cut off by
  overflow:hidden. Padding is zeroed in the theme for both the input and
  the Select's div, which centres instead.

Verified in the DOM: button, select and input all render at exactly 32
with scrollHeight equal to height, so nothing is clipped.

Claude-Session: https://claude.ai/code/session_01AbYLvwpEaZFXktHVcqrFRd
This commit is contained in:
gorkem-bwl committed 2026-09-03 01:38:51 -04:00
1 parent dbe0e2f4e3
commit de8eb53dd4
12 files changed
+49 -13

No files matched your search

@@ -6,6 +6,7 @@ import { Link as RouterLink } from "react-router-dom";
import { useTranslation } from "react-i18next";
import { useTheme } from "@mui/material/styles";
import { ChevronLeft, HelpCircle, FileText, Code } from "lucide-react";
import { CONTROL_HEIGHT } from "@/Utils/Theme/constants";
import type { ReactNode } from "react";
@@ -145,7 +146,7 @@ export const PageHeader = ({
alignItems: "center",
justifyContent: "center",
width: 36,
height: 32,
height: CONTROL_HEIGHT,
color: theme.palette.text.secondary,
borderRight:
idx < linkItems.length - 1
@@ -1,4 +1,5 @@
import { Box, Stack, useTheme } from "@mui/material";
import { CONTROL_HEIGHT } from "@/Utils/Theme/constants";
interface SkeletonCardProps {
width?: number | string | { xs?: number | string; md?: number | string };
@@ -44,7 +45,7 @@ export const SkeletonCard = ({
const blockStyle = {
width: 55,
height: 34,
height: CONTROL_HEIGHT,
borderRadius: theme.shape.borderRadius,
background: theme.palette.secondary.main,
border: `1px solid ${theme.palette.divider}`,
@@ -2,6 +2,7 @@ import MuiTabs from "@mui/material/Tabs";
import type { TabsProps } from "@mui/material/Tabs";
import { useTheme } from "@mui/material/styles";
import { typographyLevels } from "@/Utils/Theme/Palette";
import { CONTROL_HEIGHT } from "@/Utils/Theme/constants";
interface CustomTabsProps extends TabsProps {}
export const Tabs = (props: CustomTabsProps) => {
@@ -9,7 +10,7 @@ export const Tabs = (props: CustomTabsProps) => {
return (
<MuiTabs
sx={{
minHeight: 34,
minHeight: CONTROL_HEIGHT,
borderBottom: `1px solid ${theme.palette.divider}`,
"& .MuiTabs-indicator": {
backgroundColor: theme.palette.primary.main,
@@ -41,7 +42,7 @@ export const Tab = (props: CustomTabProps) => {
textTransform: "none",
fontSize: typographyLevels.m,
fontWeight: 500,
minHeight: 34,
minHeight: CONTROL_HEIGHT,
padding: theme.spacing(1, 0),
paddingBottom: 0,
minWidth: "auto",
+6 -2
View File
@@ -1,7 +1,7 @@
import Button from "@mui/material/Button";
import type { ButtonProps } from "@mui/material/Button";
import { useTheme, darken } from "@mui/material/styles";
import { HOVER } from "@/Utils/Theme/constants";
import { CONTROL_HEIGHT, HOVER } from "@/Utils/Theme/constants";
const PALETTE_COLORS = [
"primary",
@@ -43,7 +43,11 @@ export const ButtonInput = ({ sx, ...props }: ButtonProps) => {
{...props}
sx={{
textTransform: "none",
height: 34,
height: CONTROL_HEIGHT,
// MUI's button line-height is 1.75 (22.75px at 13px), which with the
// 6px padding needs 34.75px - already over its own box at 34, and
// clipping at 32. 1.2 leaves room for tall scripts too.
lineHeight: 1.2,
fontWeight: 400,
borderRadius: 2,
whiteSpace: "nowrap",
+2 -1
View File
@@ -4,6 +4,7 @@ import { typographyLevels } from "@/Utils/Theme/Palette";
import { useTheme } from "@mui/material";
import Stack from "@mui/material/Stack";
import { FieldLabel } from "@/Components/inputs/FieldLabel";
import { CONTROL_HEIGHT } from "@/Utils/Theme/constants";
interface ColorPickerProps extends MuiColorInputProps {
fieldLabel?: string;
@@ -18,7 +19,7 @@ export const ColorInput = ({ fieldLabel, required, ...props }: ColorPickerProps)
sx={{
"& .MuiOutlinedInput-root": {
borderRadius: theme.shape.borderRadius,
height: 34,
height: CONTROL_HEIGHT,
fontSize: typographyLevels.base,
},
"& .MuiOutlinedInput-notchedOutline": {
+2 -1
View File
@@ -8,6 +8,7 @@ import { FieldLabel } from "./FieldLabel";
import { Calendar } from "lucide-react";
import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
import { LocalizationProvider } from "@mui/x-date-pickers";
import { CONTROL_HEIGHT } from "@/Utils/Theme/constants";
interface DatePickerComponentProps extends Omit<DatePickerProps<Dayjs>, "label"> {
fieldLabel?: string;
@@ -55,7 +56,7 @@ export const DatePickerComponent = ({
flexDirection: "row-reverse",
},
"& input": {
minHeight: 34,
minHeight: CONTROL_HEIGHT,
p: 0,
pl: theme.spacing(3),
pr: theme.spacing(5),
+2 -1
View File
@@ -6,6 +6,7 @@ import Stack from "@mui/material/Stack";
import { FieldLabel } from "./FieldLabel";
import { ChevronDown } from "lucide-react";
import Typography from "@mui/material/Typography";
import { CONTROL_HEIGHT } from "@/Utils/Theme/constants";
interface SelectInputProps<T> extends Omit<SelectProps<T>, "label"> {
fieldLabel?: string;
@@ -71,7 +72,7 @@ const SelectInputInner = <T,>(
/>
)}
sx={{
height: "34px",
height: `${CONTROL_HEIGHT}px`,
"& .MuiSelect-select": {
display: "flex",
alignItems: "center",
+2 -2
View File
@@ -5,7 +5,7 @@ import { typographyLevels } from "@/Utils/Theme/Palette";
import { useTheme } from "@mui/material/styles";
import Stack from "@mui/material/Stack";
import { FieldLabel } from "./FieldLabel";
import { LAYOUT } from "@/Utils/Theme/constants";
import { CONTROL_HEIGHT, LAYOUT } from "@/Utils/Theme/constants";
interface TextInputProps extends Omit<TextFieldProps, "label"> {
fieldLabel?: string;
@@ -22,7 +22,7 @@ export const TextInput = forwardRef<HTMLInputElement, TextInputProps>(function T
width: "100%",
"& .MuiOutlinedInput-root": {
borderRadius: theme.shape.borderRadius,
height: props.multiline ? "auto" : 34,
height: props.multiline ? "auto" : CONTROL_HEIGHT,
fontSize: typographyLevels.base,
overflow: "hidden",
},
+2 -1
View File
@@ -7,6 +7,7 @@ import Typography from "@mui/material/Typography";
import { FieldLabel } from "./FieldLabel";
import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
import { LocalizationProvider } from "@mui/x-date-pickers";
import { CONTROL_HEIGHT } from "@/Utils/Theme/constants";
interface TimePickerComponentProps extends Omit<MobileTimePickerProps<Dayjs>, "label"> {
fieldLabel?: string;
@@ -33,7 +34,7 @@ export const TimePickerComponent = ({
sx: {
width: "fit-content",
"& input": {
minHeight: 34,
minHeight: CONTROL_HEIGHT,
p: 0,
px: theme.spacing(5),
},
@@ -4,6 +4,7 @@ import type { ToggleButtonProps } from "@mui/material/ToggleButton";
import type { ToggleButtonGroupProps } from "@mui/material/ToggleButtonGroup";
import { useTheme } from "@mui/material/styles";
import { alpha } from "@mui/material/styles";
import { CONTROL_HEIGHT } from "@/Utils/Theme/constants";
export const ToggleButtonInput = ({ sx, ...props }: ToggleButtonProps) => {
const theme = useTheme();
@@ -20,7 +21,7 @@ export const ToggleButtonInput = ({ sx, ...props }: ToggleButtonProps) => {
sx={{
px: 8,
textTransform: "none",
height: 34,
height: CONTROL_HEIGHT,
fontWeight: 400,
whiteSpace: "nowrap",
textOverflow: "ellipsis",
+17
View File
@@ -202,7 +202,24 @@ export const theme = (mode: string, palette: any) =>
// jumping to near-black. Focus: the brand, at 2px.
MuiOutlinedInput: {
styleOverrides: {
// MUI pads the inner input 8.5px top and bottom, which with an
// 18.7px line-height needs 35.7px - taller than the control
// itself, so the text was being clipped by overflow:hidden.
input: {
paddingTop: 0,
paddingBottom: 0,
height: "100%",
},
root: ({ theme }) => ({
// The Select renders a div rather than an input, so it needs
// the same treatment to sit inside the control height.
"& .MuiSelect-select": {
paddingTop: 0,
paddingBottom: 0,
display: "flex",
alignItems: "center",
minHeight: "unset",
},
"& .MuiOutlinedInput-notchedOutline": {
borderColor: theme.palette.divider,
transition: "border-color 0.15s ease",
+7
View File
@@ -18,6 +18,13 @@ export const LAYOUT = {
XXL: 16,
} as const;
/**
* Shared height for anything that can sit in a row with something else -
* buttons, inputs, selects, pickers, toggles, tabs, icon buttons. One number so
* they share a baseline instead of drifting 2px apart.
*/
export const CONTROL_HEIGHT = 32;
export const HOVER = {
DARKEN: 0.06, // This is a coefficient for darkening function
ROW: 0.025, // Overlay alpha for hoverable rows and cards