From c46ead515bf13d0b8ea93fa74ff8fb75f53fdf10 Mon Sep 17 00:00:00 2001 From: gorkem-bwl Date: Sat, 29 Aug 2026 09:53:42 -0400 Subject: [PATCH] fix(client): dashboard hover controls, card widths and font sizes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reorder controls stayed visible after a click. The reveal used `:focus-within`, and clicking a button leaves it focused, so that card's controls stayed pinned open long after the pointer left — Failed checks being wherever focus happened to land. Now `:has(:focus-visible)`, which matches keyboard focus only, so a mouse click no longer pins them. Widths: Currently down was full-width and Recent incidents 8-wide, both with more whitespace than content. Currently down carries six columns per row so it is the densest card, but it does not need all twelve; it is now 8. Recent incidents carries four and is now 4, as are Recent errors. Declaring a narrower width is not enough on its own: a card left alone in a row gets stretched to fill it, so Currently down at 8 still rendered at 12 while it was the only card in its row. Registry and default order are now sequenced so widths pair into full rows — 8 beside 4, 6 beside 6 — which is what actually makes the narrow cards render narrow. Verified across the full catalogue, the non-admin subset and the defaults: every mid-page row fills exactly 12 with no gaps. Fonts: every secondary line used typographyLevels.s, which is 11px — body2, a de-emphasis size. The theme's body1 is 13px, so all 24 uses are now typographyLevels.m. Nothing in a card renders below body size any more; only the large figures differ, at xxl. Claude-Session: https://claude.ai/code/session_01AbYLvwpEaZFXktHVcqrFRd --- .../Pages/Dashboard/Components/CardPicker.tsx | 2 +- .../Dashboard/Components/CardPrimitives.tsx | 4 +-- .../Dashboard/Components/Cards/AdminCards.tsx | 8 +++--- .../Components/Cards/BusiestServersCard.tsx | 4 +-- .../Components/Cards/CheckmateServerCard.tsx | 2 +- .../Components/Cards/CurrentlyDownCard.tsx | 8 +++--- .../Components/Cards/MonitorStatusCard.tsx | 2 +- .../Components/Cards/MonitorsByGroupCard.tsx | 2 +- .../Components/Cards/MonitorsByTypeCard.tsx | 2 +- .../Components/Cards/RankingCards.tsx | 2 +- .../Components/Cards/RecentIncidentsCard.tsx | 6 ++--- .../Components/Cards/SummaryCards.tsx | 4 +-- .../Dashboard/Components/DashboardCard.tsx | 14 +++++++--- client/src/Pages/Dashboard/cards.ts | 4 ++- client/src/Pages/Dashboard/registry.ts | 27 +++++++++++-------- 15 files changed, 52 insertions(+), 39 deletions(-) diff --git a/client/src/Pages/Dashboard/Components/CardPicker.tsx b/client/src/Pages/Dashboard/Components/CardPicker.tsx index ac5508135..e60653cac 100644 --- a/client/src/Pages/Dashboard/Components/CardPicker.tsx +++ b/client/src/Pages/Dashboard/Components/CardPicker.tsx @@ -79,7 +79,7 @@ export const CardPicker = ({ > {t(`components.sidebar.menu.${group}`, { defaultValue: group })} diff --git a/client/src/Pages/Dashboard/Components/CardPrimitives.tsx b/client/src/Pages/Dashboard/Components/CardPrimitives.tsx index 952cd875c..f12952a7c 100644 --- a/client/src/Pages/Dashboard/Components/CardPrimitives.tsx +++ b/client/src/Pages/Dashboard/Components/CardPrimitives.tsx @@ -97,7 +97,7 @@ export const CardRowLabel = ({ {secondary && ( @@ -132,7 +132,7 @@ export const CardFigure = ({ {caption && ( {caption} diff --git a/client/src/Pages/Dashboard/Components/Cards/AdminCards.tsx b/client/src/Pages/Dashboard/Components/Cards/AdminCards.tsx index 632a0b612..da00bad05 100644 --- a/client/src/Pages/Dashboard/Components/Cards/AdminCards.tsx +++ b/client/src/Pages/Dashboard/Components/Cards/AdminCards.tsx @@ -37,7 +37,7 @@ const StatLine = ({ gap={theme.spacing(LAYOUT.SM)} > {label} @@ -96,7 +96,7 @@ export const ChecksOnScheduleCard = () => { secondary={job.failReason} /> @@ -159,7 +159,7 @@ export const RecentErrorsCard = () => { action={ errors > 0 ? ( {t("pages.dashboard.cards.recentErrors.count", { count: errors })} @@ -183,7 +183,7 @@ export const RecentErrorsCard = () => { secondary={[entry.service, entry.method].filter(Boolean).join(" · ")} /> { gap={theme.spacing(LAYOUT.XS)} > {label} {/* A down server reports nothing — show a dash, never 0%. */} diff --git a/client/src/Pages/Dashboard/Components/Cards/CheckmateServerCard.tsx b/client/src/Pages/Dashboard/Components/Cards/CheckmateServerCard.tsx index b7716f7c9..cdc7b8b2d 100644 --- a/client/src/Pages/Dashboard/Components/Cards/CheckmateServerCard.tsx +++ b/client/src/Pages/Dashboard/Components/Cards/CheckmateServerCard.tsx @@ -40,7 +40,7 @@ const MetricLine = ({ gap={theme.spacing(LAYOUT.SM)} > {label} diff --git a/client/src/Pages/Dashboard/Components/Cards/CurrentlyDownCard.tsx b/client/src/Pages/Dashboard/Components/Cards/CurrentlyDownCard.tsx index 21dca5153..5075e1c2e 100644 --- a/client/src/Pages/Dashboard/Components/Cards/CurrentlyDownCard.tsx +++ b/client/src/Pages/Dashboard/Components/Cards/CurrentlyDownCard.tsx @@ -92,7 +92,7 @@ export const CurrentlyDownCard = () => { action={ problems.length > 0 ? ( {t("pages.dashboard.cards.currentlyDown.count", { @@ -120,7 +120,7 @@ export const CurrentlyDownCard = () => { secondary={monitor.url} /> @@ -128,7 +128,7 @@ export const CurrentlyDownCard = () => { {since && ( { * the link and activating it navigates away. */} { size="md" /> {t(`pages.common.monitors.status.${key}`)} diff --git a/client/src/Pages/Dashboard/Components/Cards/MonitorsByGroupCard.tsx b/client/src/Pages/Dashboard/Components/Cards/MonitorsByGroupCard.tsx index 841b90c47..215771cf3 100644 --- a/client/src/Pages/Dashboard/Components/Cards/MonitorsByGroupCard.tsx +++ b/client/src/Pages/Dashboard/Components/Cards/MonitorsByGroupCard.tsx @@ -85,7 +85,7 @@ export const MonitorsByGroupCard = () => { > {bucket.down > 0 && ( {t("pages.dashboard.cards.monitorsByType.down", { diff --git a/client/src/Pages/Dashboard/Components/Cards/MonitorsByTypeCard.tsx b/client/src/Pages/Dashboard/Components/Cards/MonitorsByTypeCard.tsx index 1893edcb0..a36c87d0b 100644 --- a/client/src/Pages/Dashboard/Components/Cards/MonitorsByTypeCard.tsx +++ b/client/src/Pages/Dashboard/Components/Cards/MonitorsByTypeCard.tsx @@ -85,7 +85,7 @@ export const MonitorsByTypeCard = () => { > {bucket.down > 0 && ( {t("pages.dashboard.cards.monitorsByType.down", { diff --git a/client/src/Pages/Dashboard/Components/Cards/RankingCards.tsx b/client/src/Pages/Dashboard/Components/Cards/RankingCards.tsx index e76171287..d405c5392 100644 --- a/client/src/Pages/Dashboard/Components/Cards/RankingCards.tsx +++ b/client/src/Pages/Dashboard/Components/Cards/RankingCards.tsx @@ -33,7 +33,7 @@ const RankingCount = ({ shown, total }: { shown: number; total: number }) => { const { t } = useTranslation(); return ( {t("pages.dashboard.cards.ranking.topOf", { shown, total })} diff --git a/client/src/Pages/Dashboard/Components/Cards/RecentIncidentsCard.tsx b/client/src/Pages/Dashboard/Components/Cards/RecentIncidentsCard.tsx index c1ebf1b2a..448e02bfd 100644 --- a/client/src/Pages/Dashboard/Components/Cards/RecentIncidentsCard.tsx +++ b/client/src/Pages/Dashboard/Components/Cards/RecentIncidentsCard.tsx @@ -44,7 +44,7 @@ export const RecentIncidentsCard = () => { action={ data && data.totalActive > 0 ? ( {t("pages.dashboard.cards.recentIncidents.active", { @@ -78,7 +78,7 @@ export const RecentIncidentsCard = () => { secondary={formatTimestamp(incident.startTime)} /> @@ -88,7 +88,7 @@ export const RecentIncidentsCard = () => { {incident.resolutionType === "manual" && ( diff --git a/client/src/Pages/Dashboard/Components/Cards/SummaryCards.tsx b/client/src/Pages/Dashboard/Components/Cards/SummaryCards.tsx index c3e763ea2..8d5b49fad 100644 --- a/client/src/Pages/Dashboard/Components/Cards/SummaryCards.tsx +++ b/client/src/Pages/Dashboard/Components/Cards/SummaryCards.tsx @@ -27,7 +27,7 @@ const StatLine = ({ label, value }: { label: string; value: string }) => { gap={theme.spacing(LAYOUT.SM)} > {label} @@ -218,7 +218,7 @@ export const StatusPagesCard = () => { })} /> diff --git a/client/src/Pages/Dashboard/Components/DashboardCard.tsx b/client/src/Pages/Dashboard/Components/DashboardCard.tsx index 6b9af8455..9a1ad6407 100644 --- a/client/src/Pages/Dashboard/Components/DashboardCard.tsx +++ b/client/src/Pages/Dashboard/Components/DashboardCard.tsx @@ -52,9 +52,15 @@ export const DashboardCard = ({ flexDirection: "column", height: "100%", overflow: "hidden", - // The header controls stay invisible until the card is hovered or - // something inside it takes focus, so a settled dashboard is quiet. - "&:hover .dashboard-card-slot, &:focus-within .dashboard-card-slot": { + // The header controls stay invisible until the card is hovered, so a + // settled dashboard is quiet. + // + // `:has(:focus-visible)` rather than `:focus-within`: a mouse click + // leaves the button focused, and :focus-within would then pin that + // card's controls open long after the pointer had left. focus-visible + // only matches keyboard focus, which is the case that actually needs + // the controls revealed. + "&:hover .dashboard-card-slot, &:has(:focus-visible) .dashboard-card-slot": { opacity: 1, pointerEvents: "auto", }, @@ -158,7 +164,7 @@ export const CardMessage = ({ text, action }: { text: string; action?: ReactNode gap={theme.spacing(LAYOUT.SM)} > {text} diff --git a/client/src/Pages/Dashboard/cards.ts b/client/src/Pages/Dashboard/cards.ts index 33177b9b5..f16a61407 100644 --- a/client/src/Pages/Dashboard/cards.ts +++ b/client/src/Pages/Dashboard/cards.ts @@ -61,12 +61,14 @@ export interface CardDefinition { // The cards a fresh dashboard starts with. Chosen so a brand new install still // shows something real: Checkmate server works with zero monitors. +// Ordered so the declared widths pair into full rows: a card left alone in a +// row is stretched to fill it, so 8- and 4-wide cards are kept adjacent. export const DEFAULT_CARD_IDS: CardId[] = [ "monitorStatus", "currentlyDown", "failedChecks", - "recentIncidents", "monitorsByType", + "recentIncidents", "checkmateServer", ]; diff --git a/client/src/Pages/Dashboard/registry.ts b/client/src/Pages/Dashboard/registry.ts index 893f6b0be..b8a87ea27 100644 --- a/client/src/Pages/Dashboard/registry.ts +++ b/client/src/Pages/Dashboard/registry.ts @@ -24,7 +24,10 @@ import type { CardDefinition, CardId } from "./cards"; // Declared width is the minimum number of columns a card needs. The grid may // stretch the last card of a short row; nothing here is a user setting. // -// Order is the order the picker lists them in, grouped by sidebar section. +// Order is the render order, and the order the picker lists them in. Cards are +// sequenced so their declared widths pair into full rows: a card left alone in +// a row gets stretched to fill it, so an 8-wide card is kept next to a 4-wide +// one rather than being widened back to 12. export const CARD_REGISTRY: CardDefinition[] = [ { id: "monitorStatus", @@ -37,9 +40,18 @@ export const CARD_REGISTRY: CardDefinition[] = [ id: "currentlyDown", key: "currentlyDown", group: "uptime", - width: 12, + // Six columns per row — the densest card — but it does not need the + // full width, and 8 leaves room for a 4-wide card beside it. + width: 8, component: CurrentlyDownCard, }, + { + id: "failedChecks", + key: "failedChecks", + group: "checks", + width: 4, + component: FailedChecksCard, + }, { id: "slowestMonitors", key: "slowestMonitors", @@ -79,7 +91,7 @@ export const CARD_REGISTRY: CardDefinition[] = [ id: "recentIncidents", key: "recentIncidents", group: "incidents", - width: 8, + width: 4, component: RecentIncidentsCard, }, { @@ -110,13 +122,6 @@ export const CARD_REGISTRY: CardDefinition[] = [ width: 4, component: StatusPagesCard, }, - { - id: "failedChecks", - key: "failedChecks", - group: "checks", - width: 4, - component: FailedChecksCard, - }, { id: "checkmateServer", key: "checkmateServer", @@ -137,7 +142,7 @@ export const CARD_REGISTRY: CardDefinition[] = [ id: "recentErrors", key: "recentErrors", group: "logs", - width: 6, + width: 4, adminOnly: true, component: RecentErrorsCard, },