From f9319549973052fbc83dbbf9c13161e2631044b6 Mon Sep 17 00:00:00 2001 From: gorkem-bwl Date: Thu, 3 Sep 2026 01:14:49 -0400 Subject: [PATCH] fix(client): make row selection visible, and clear review findings Selection never showed. getRowSx set a background on the , but every paints background.paper over it, and the hover rule targets cells - so hover won and selection was invisible. Selection and hover are now both cell-level rules in DataTable, with selection first so hover layers on top, and rows carry an `is-selected` class from a new isRowSelected prop. Uptime, Docker and Infrastructure tables all had the same broken copy of this; they now share the one implementation, and any future table gets it for free. action.selected/selectedHover are defined explicitly and tinted with the brand, so a selected row reads as chosen rather than dirty. Kept low (6-11%) so the highlight does not swamp the row, especially in dark mode. From code review: - Dropped the Google Fonts @import at the top of index.css. Nothing has used Inter since the switch to Geist, and it was a render-blocking third-party request on every load - exactly what self-hosting the font was meant to avoid on air-gapped installs. - TextInput's autofill override painted background.default, which used to equal paper and no longer does, so Chrome-autofilled fields showed a grey block on white cards. Now paper. - Sidebar background moved from sx to the bgcolor prop (convention rule 1, and it was a new lint warning), and the backdropFilter dropped - the background is opaque, so the blur only cost a compositing layer. - Tab no longer bolds the selected label. That is the same reflow the ToggleButton change removed, and worse here because the indicator animates toward a target that moves as the label widens. - Corrected the StatBox gradient comment: contrastText is white in light mode but near-black in dark, so the constraint is staying near `main`, not staying dark. Claude-Session: https://claude.ai/code/session_01AbYLvwpEaZFXktHVcqrFRd --- .../Components/design-elements/StatBox.tsx | 8 ++++--- .../src/Components/design-elements/Table.tsx | 21 ++++++++++++++++++- .../src/Components/design-elements/Tabs.tsx | 5 ++++- client/src/Components/inputs/TextInput.tsx | 2 +- client/src/Components/sidebar/index.tsx | 9 ++++---- .../Components/DockerMonitorsTable.tsx | 6 +----- .../Monitors/Components/MonitorsTable.tsx | 6 +----- .../Components/UptimeMonitorsTable.tsx | 6 +----- client/src/Utils/Theme/Palette.ts | 6 ++++++ client/src/Utils/Theme/Theme.ts | 1 + client/src/index.css | 2 -- 11 files changed, 44 insertions(+), 28 deletions(-) diff --git a/client/src/Components/design-elements/StatBox.tsx b/client/src/Components/design-elements/StatBox.tsx index 9cdb9c024..b5e9c7834 100644 --- a/client/src/Components/design-elements/StatBox.tsx +++ b/client/src/Components/design-elements/StatBox.tsx @@ -23,9 +23,11 @@ export const GradientBox = ({ children, palette, sx, interactive }: GradientBox) // gradient collapses flat. Darken slightly towards the page ground instead. const paperStart = isLight ? darken(paper, 0.02) : lighten(paper, 0.08); const paperEnd = paper; - // The gradient carries white `contrastText`, so it runs between two shades - // dark enough to keep that legible. `light` is a mid-tone used for status - // dots and chart marks - too pale here - so lift from `main` instead. + // Both ends stay close to `main` so MUI's derived `contrastText` still + // applies - that is white on the dark light-mode fills and near-black on + // the lighter dark-mode ones, so a bigger lift would break one or other. + // `light` is a mid-tone used for status dots and chart marks, too pale to + // use here, so the lift comes off `main` instead. const bg = palette ? `linear-gradient(135deg, ${lighten(theme.palette[palette].main, 0.12)} 0%, ${theme.palette[palette].main} 100%)` : `linear-gradient(135deg, ${paperStart} 0%, ${paperEnd} 100%)`; diff --git a/client/src/Components/design-elements/Table.tsx b/client/src/Components/design-elements/Table.tsx index 9ef7dcee8..44eab9424 100644 --- a/client/src/Components/design-elements/Table.tsx +++ b/client/src/Components/design-elements/Table.tsx @@ -52,6 +52,8 @@ type DataTableProps = emptyViewText?: string; emptyViewPositive?: boolean; getRowSx?: (row: T) => SxProps; + /** Marks a row selected so it gets the shared selected + hover treatment. */ + isRowSelected?: (row: T) => boolean; }; export function DataTable< @@ -70,6 +72,7 @@ export function DataTable< emptyViewText, emptyViewPositive, getRowSx, + isRowSelected, }: DataTableProps) { const theme = useTheme(); const [expanded, setExpanded] = useState<(string | number) | null>(null); @@ -213,10 +216,21 @@ export function DataTable< "& .MuiTableBody-root .MuiTableRow-root:last-child .MuiTableCell-root": { borderBottom: "none", }, + // Cells paint `background.paper`, so a background set on the row + // never shows. Selection and hover both have to be applied at + // cell level, and selection has to come first so hover layers + // on top of it rather than replacing it. + "& .MuiTableBody-root .MuiTableRow-root.is-selected .MuiTableCell-root": { + backgroundColor: theme.palette.action.selected, + }, "& .MuiTableBody-root .MuiTableRow-root.is-clickable:hover .MuiTableCell-root": { backgroundColor: theme.palette.action.rowHover, }, + "& .MuiTableBody-root .MuiTableRow-root.is-selected.is-clickable:hover .MuiTableCell-root": + { + backgroundColor: theme.palette.action.selectedHover, + }, }} > @@ -241,7 +255,12 @@ export function DataTable< return ( { alignItems: "flex-start", color: theme.palette.text.secondary, "&.Mui-selected": { + // Weight stays put, as on ToggleButton: bolding the active tab + // widens it, shifting every tab to its right and dragging the + // indicator to a moving target. Colour and the indicator are + // already carrying the state. color: theme.palette.primary.main, - fontWeight: 600, }, "&:hover": { color: theme.palette.text.secondary, diff --git a/client/src/Components/inputs/TextInput.tsx b/client/src/Components/inputs/TextInput.tsx index 230e7650b..a246aaf04 100644 --- a/client/src/Components/inputs/TextInput.tsx +++ b/client/src/Components/inputs/TextInput.tsx @@ -39,7 +39,7 @@ export const TextInput = forwardRef(function T }, "& input:-webkit-autofill, & input:-webkit-autofill:hover, & input:-webkit-autofill:focus, & input:-webkit-autofill:active": { - WebkitBoxShadow: `0 0 0 100px ${theme.palette.background.default} inset !important`, + WebkitBoxShadow: `0 0 0 100px ${theme.palette.background.paper} inset !important`, WebkitTextFillColor: `${theme.palette.text.primary} !important`, caretColor: theme.palette.text.primary, transition: "background-color 5000s ease-in-out 0s", diff --git a/client/src/Components/sidebar/index.tsx b/client/src/Components/sidebar/index.tsx index 658054dbd..e9a999ebd 100644 --- a/client/src/Components/sidebar/index.tsx +++ b/client/src/Components/sidebar/index.tsx @@ -73,15 +73,14 @@ export const Sidebar = () => { gap={theme.spacing(LAYOUT.SM)} borderRight={`1px solid ${theme.palette.divider}`} width={width} + // On `paper` rather than inheriting the page ground, so the nav + // reads as a surface and the content area stays the darker + // `default`. Opaque, so the blur it used to carry is gone. + bgcolor={theme.palette.background.paper} sx={{ touchAction: "none", transition: transition, zIndex: 1000, - backdropFilter: "blur(8px)", - // Sits on `paper` rather than inheriting the page ground, so - // the nav reads as a surface and the content area behind it - // stays the slightly darker `default`. - backgroundColor: theme.palette.background.paper, }} > { navigate(`/docker/host/${row.id}`); }} - getRowSx={(row) => ({ - backgroundColor: isRowSelected(row.id) - ? theme.palette.action.selected - : "inherit", - })} + isRowSelected={(row) => isRowSelected(row.id)} /> { navigate(`/infrastructure/${row.id}`); }} - getRowSx={(row) => ({ - backgroundColor: isRowSelected(row.id) - ? theme.palette.action.selected - : "inherit", - })} + isRowSelected={(row) => isRowSelected(row.id)} /> { navigate(`/uptime/${row.id}`); }} - getRowSx={(row) => ({ - backgroundColor: isRowSelected(row.id) - ? theme.palette.action.selected - : "inherit", - })} + isRowSelected={(row) => isRowSelected(row.id)} />