fix(client): correct StatBox hover and separate nested-control hover

Addresses review findings on the hover-states change.

StatBox's hover was a no-op. `GradientBox` always paints an opaque
`linear-gradient` via the `background` shorthand, which populates
background-image and covers any `backgroundColor` beneath it, so the new hover
was invisible — a regression from the `opacity: 0.95` it replaced. The overlay
now layers onto the same gradient, and moves into `GradientBox` where `bg` is
in scope, behind an `interactive` prop.

The mobile card branch gated its cursor and hover on `onRowClick` alone, so
expandable-row tables below the md breakpoint rendered cards that expand on
click but showed no pointer and no hover — the exact inconsistency this change
set out to fix, reintroduced on mobile. Both branches now share one
`isInteractive` flag.

The row action gear used the same overlay as the row beneath it. Alpha
overlays compose, so it was not invisible as reported, but the resulting delta
was 6 levels — too faint for a nested control. Adds `HOVER.CONTROL` (0.05) and
an `action.controlHover` token for controls nested inside a hoverable row: the
gear and the pagination arrows.

Claude-Session: https://claude.ai/code/session_01AbYLvwpEaZFXktHVcqrFRd
This commit is contained in:
gorkem-bwl committed 2026-08-28 19:53:39 -04:00
1 parent 9a6602b9e1
commit 3e56dc63a9
6 files changed
+24 -18

No files matched your search

+1 -1
View File
@@ -33,7 +33,7 @@ export const ActionsMenu = ({ items }: { items: ActionMenuItem[] }) => {
onClick={handleClick}
sx={{
"&:hover": {
backgroundColor: theme.palette.action.rowHover,
backgroundColor: theme.palette.action.controlHover,
},
}}
>
@@ -11,9 +11,10 @@ import type { SxProps } from "@mui/material";
type GradientBox = React.PropsWithChildren<{
palette?: PaletteKey;
sx?: SxProps;
interactive?: boolean;
}>;
export const GradientBox = ({ children, palette, sx }: GradientBox) => {
export const GradientBox = ({ children, palette, sx, interactive }: GradientBox) => {
const theme = useTheme();
const isSmall = useMediaQuery(theme.breakpoints.down("md"));
const isLight = theme.palette.mode === "light";
@@ -30,6 +31,12 @@ export const GradientBox = ({ children, palette, sx }: GradientBox) => {
padding: `${theme.spacing(4)} ${theme.spacing(8)}`,
width: isSmall ? `100%` : `calc(25% - (3 * ${theme.spacing(8)} / 4))`,
background: bg,
...(interactive && {
cursor: "pointer",
"&:hover": {
backgroundImage: `linear-gradient(${theme.palette.action.rowHover}, ${theme.palette.action.rowHover}), ${bg}`,
},
}),
...sx,
}}
>
@@ -62,15 +69,8 @@ export const StatBox = ({
return (
<GradientBox
palette={palette}
sx={{
...(sx as object),
...(onClick
? {
cursor: "pointer",
"&:hover": { backgroundColor: theme.palette.action.rowHover },
}
: {}),
}}
interactive={Boolean(onClick)}
sx={{ ...(sx as object) }}
>
<Stack onClick={onClick}>
<Box sx={{ display: "flex", alignItems: "center", gap: theme.spacing(2) }}>
@@ -79,6 +79,7 @@ export function DataTable<
};
const isSmall = useMediaQuery(theme.breakpoints.down("md"));
const isInteractive = Boolean(onRowClick) || expandableRows;
if (data.length === 0 || headers.length === 0) {
return (
@@ -107,8 +108,8 @@ export function DataTable<
borderColor: theme.palette.divider,
borderRadius: theme.shape.borderRadius,
padding: theme.spacing(LAYOUT.XS),
cursor: onRowClick ? "pointer" : "default",
...(onRowClick && {
cursor: isInteractive ? "pointer" : "default",
...(isInteractive && {
"&:hover": {
backgroundColor: theme.palette.action.rowHover,
},
@@ -240,9 +241,9 @@ export function DataTable<
return (
<Fragment key={key}>
<TableRow
className={onRowClick || expandableRows ? "is-clickable" : undefined}
className={isInteractive ? "is-clickable" : undefined}
sx={{
cursor: onRowClick || expandableRows ? "pointer" : "default",
cursor: isInteractive ? "pointer" : "default",
...(getRowSx?.(row) as object),
}}
onClick={() => {
@@ -328,7 +329,7 @@ function TablePaginationActions(props: TablePaginationActionsProps) {
flexShrink: 0,
ml: { xs: 0, md: 2.5 },
"& .MuiIconButton-root:not(.Mui-disabled):hover": {
backgroundColor: theme.palette.action.rowHover,
backgroundColor: theme.palette.action.controlHover,
},
}}
className="table-pagination-actions"
@@ -432,7 +433,7 @@ function HasMoreTablePaginationActions(props: HasMoreTablePaginationActionsProps
flexShrink: 0,
ml: { xs: 0, md: 2.5 },
"& .MuiIconButton-root:not(.Mui-disabled):hover": {
backgroundColor: theme.palette.action.rowHover,
backgroundColor: theme.palette.action.controlHover,
},
}}
className="table-pagination-actions"
+2
View File
@@ -42,6 +42,7 @@ export const lightPalette = {
},
action: {
rowHover: alpha(colors.gray900, HOVER.ROW),
controlHover: alpha(colors.gray900, HOVER.CONTROL),
},
secondary: {
main: colors.gray200,
@@ -71,6 +72,7 @@ export const darkPalette = {
},
action: {
rowHover: alpha("#FFFFFF", HOVER.ROW),
controlHover: alpha("#FFFFFF", HOVER.CONTROL),
},
secondary: {
main: colors.gray700,
+2
View File
@@ -15,9 +15,11 @@ declare module "@mui/material/styles" {
}
interface TypeAction {
rowHover: string;
controlHover: string;
}
interface TypeActionOptions {
rowHover?: string;
controlHover?: string;
}
interface Palette {
sidebar: { accent: string };
+2 -1
View File
@@ -20,5 +20,6 @@ export const LAYOUT = {
export const HOVER = {
DARKEN: 0.06, // This is a coefficient for darkening function
ROW: 0.025, // Overlay alpha for hoverable rows, cards and icon buttons
ROW: 0.025, // Overlay alpha for hoverable rows and cards
CONTROL: 0.05, // Stronger overlay for controls nested inside a hoverable row
} as const;