fix(client): sidebar selection ordering, and clear second review pass

The sidebar regression is the real fix here. NavItem paints the selected
item with action.selected but hovers unselected items with MUI's default
action.hover. Once the sidebar moved onto `paper`, selected composited to
rgb(31,38,41) - darker than the ground - while hover came out lighter at
rgb(46,46,51), so hovering any other item made it read as more selected
than the actual selection. Both states now come from the same family
(selectedHover / rowHover), so selected always reads stronger.

Also from review:

- Dropped "Inter" from the status page SANS_STACK. Its webfont went with
  the Google Fonts import, so on Windows and Linux the stack was silently
  falling through to Segoe UI / Roboto on pages users publish.
- Button focus rings now use the same dark-mode green as input focus
  rings. They were still on primary.main at 2.86:1, which is the exact
  problem primary.light was added to solve.
- Removed 13 size={16} props made redundant by the Icon default change.
  The load-bearing pins stay: size={20} on the public status page, and
  the header pins that were relying on the old default.
- Documented that alpha() replaces rather than multiplies the alpha
  channel in StatusBox, so the grid is coupled to `divider` being opaque.
- Noted on the isRowSelected JSDoc that the small-screen card branch does
  not style selection, same limitation getRowSx already had.

Claude-Session: https://claude.ai/code/session_01AbYLvwpEaZFXktHVcqrFRd
This commit is contained in:
gorkem-bwl committed 2026-09-03 01:30:02 -04:00
1 parent 3ddc6ef1cb
commit dbe0e2f4e3
8 files changed
+36 -79

No files matched your search

@@ -35,7 +35,10 @@ export const BGBox = ({ children, sx }: StatusBoxProps) => {
pointerEvents: "none",
// Deliberately far fainter than `divider` - this is texture
// behind the number, not a rule, and it must never compete
// with the box's own border.
// with the box's own border. Note alpha() *replaces* the
// alpha channel, so this relies on `divider` being an opaque
// hex; if it ever goes translucent this gets stronger, not
// weaker.
backgroundImage: `
linear-gradient(${alpha(theme.palette.divider, 0.4)} 1px, transparent 1px),
linear-gradient(90deg, ${alpha(theme.palette.divider, 0.4)} 1px, transparent 1px)
@@ -52,7 +52,11 @@ type DataTableProps<T extends { id?: string | number; _id?: string | number }> =
emptyViewText?: string;
emptyViewPositive?: boolean;
getRowSx?: (row: T) => SxProps<Theme>;
/** Marks a row selected so it gets the shared selected + hover treatment. */
/**
* Marks a row selected so it gets the shared selected + hover treatment.
* Desktop table only - the small-screen card branch does not style
* selection yet, same as `getRowSx`.
*/
isRowSelected?: (row: T) => boolean;
};
@@ -64,12 +64,7 @@ export const HeaderMonitorControls = ({
variant="contained"
color="secondary"
loading={isPosting}
startIcon={
<Icon
icon={Mail}
size={16}
/>
}
startIcon={<Icon icon={Mail} />}
onClick={async () => {
await post(`/notifications/test/all`, { monitorId: monitor.id });
}}
@@ -82,12 +77,7 @@ export const HeaderMonitorControls = ({
<Button
variant="contained"
color="secondary"
startIcon={
<Icon
icon={Bug}
size={16}
/>
}
startIcon={<Icon icon={Bug} />}
onClick={() => {
navigate(`/incidents/${monitor?.id}`);
}}
@@ -99,19 +89,7 @@ export const HeaderMonitorControls = ({
variant="contained"
color="secondary"
loading={isPosting}
startIcon={
monitor?.isActive ? (
<Icon
icon={Pause}
size={16}
/>
) : (
<Icon
icon={Play}
size={16}
/>
)
}
startIcon={monitor?.isActive ? <Icon icon={Pause} /> : <Icon icon={Play} />}
onClick={async () => {
await post(`/monitors/pause/${monitor.id}`, {});
await refetch();
@@ -124,12 +102,7 @@ export const HeaderMonitorControls = ({
<Button
variant="contained"
color="secondary"
startIcon={
<Icon
icon={Settings}
size={16}
/>
}
startIcon={<Icon icon={Settings} />}
onClick={() => navigate(`/${path}/configure/${monitor.id}`)}
>
{t("common.buttons.configure")}
@@ -176,19 +149,7 @@ export const HeaderDeleteControls = ({
variant="contained"
color="secondary"
loading={isPosting}
startIcon={
monitor?.isActive ? (
<Icon
icon={Pause}
size={16}
/>
) : (
<Icon
icon={Play}
size={16}
/>
)
}
startIcon={monitor?.isActive ? <Icon icon={Pause} /> : <Icon icon={Play} />}
onClick={async () => {
await post(`/monitors/pause/${monitor.id}`, {});
await refetch();
@@ -201,12 +162,7 @@ export const HeaderDeleteControls = ({
<Button
variant="contained"
color="error"
startIcon={
<Icon
icon={Trash}
size={16}
/>
}
startIcon={<Icon icon={Trash} />}
onClick={() => {
onDelete?.();
}}
+1 -4
View File
@@ -185,10 +185,7 @@ export const AuthFooter = ({ collapsed, accountMenuItems }: AuthFooterProps) =>
}}
sx={menuItemSx}
>
<Icon
icon={mode === "light" ? Moon : Sun}
size={16}
/>
<Icon icon={mode === "light" ? Moon : Sun} />
{mode === "light"
? t("pages.settings.form.ui.option.theme.dark")
: t("pages.settings.form.ui.option.theme.light")}
+5 -2
View File
@@ -26,10 +26,13 @@ export const NavItem = ({
? theme.palette.sidebar.accent
: theme.palette.text.secondary;
// Both states come from the same family so selected always reads stronger
// than a hovered neighbour. MUI's default action.hover is lighter than
// action.selected on a dark ground, which inverted the two.
const buttonBgColor = selected ? theme.palette.action.selected : "transparent";
const buttonBgHoverColor = selected
? theme.palette.action.selected
: theme.palette.action.hover;
? theme.palette.action.selectedHover
: theme.palette.action.rowHover;
const fontWeight = selected ? 600 : 400;
return (
<Tooltip
@@ -149,7 +149,6 @@ const SummaryIncidentItem = ({ incident }: { incident: IncidentSummaryItem }) =>
>
<Icon
icon={Globe}
size={16}
color={theme.palette.text.secondary}
/>
<Typography
@@ -221,32 +220,17 @@ export const SummaryCardStats = ({ summary }: SummaryCardStatsProps) => {
return (
<SummaryCard title={t("pages.incidents.summaryCard.incidentStats.title")}>
<SummaryItem
icon={
<Icon
icon={Bell}
size={16}
/>
}
icon={<Icon icon={Bell} />}
label={t("pages.incidents.summaryCard.incidentStats.totalIncidents")}
value={summary?.total || 0}
/>
<SummaryItem
icon={
<Icon
icon={TriangleAlert}
size={16}
/>
}
icon={<Icon icon={TriangleAlert} />}
label={t("pages.incidents.summaryCard.incidentStats.mostAffectedMonitor")}
value={mostAffected}
/>
<SummaryItem
icon={
<Icon
icon={Wrench}
size={16}
/>
}
icon={<Icon icon={Wrench} />}
label={t("pages.incidents.summaryCard.incidentStats.avgResolutionTime")}
value={
summary.total > 0
@@ -1,5 +1,8 @@
// No "Inter" here: its webfont was never self-hosted, so once the Google Fonts
// import went it resolved to a fallback anyway. Listing it only made the stack
// look intentional on machines that happen to have Inter installed.
export const SANS_STACK =
'-apple-system, BlinkMacSystemFont, "Inter", "Segoe UI", Roboto, sans-serif';
'-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';
export const BOLD_SANS_STACK = `ui-sans-serif, ${SANS_STACK}`;
+8 -1
View File
@@ -134,7 +134,14 @@ export const theme = (mode: string, palette: any) =>
styleOverrides: {
root: ({ theme }) => ({
"&.Mui-focusVisible": {
outline: `1px solid ${theme.palette.primary.main}`,
// Same green as the input focus ring - `main` is only
// 2.86:1 on the dark ground, so keyboard focus was
// barely visible there.
outline: `1px solid ${
theme.palette.mode === "dark"
? theme.palette.primary.light
: theme.palette.primary.main
}`,
outlineOffset: 2,
},
}),