fix(client): dashboard hover controls, card widths and font sizes

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
This commit is contained in:
gorkem-bwl committed 2026-08-29 09:53:42 -04:00
1 parent 49af07009d
commit c46ead515b
15 files changed
+52 -39

No files matched your search

@@ -79,7 +79,7 @@ export const CardPicker = ({
>
<Typography
variant="eyebrow"
fontSize={typographyLevels.s}
fontSize={typographyLevels.m}
color={theme.palette.text.disabled}
>
{t(`components.sidebar.menu.${group}`, { defaultValue: group })}
@@ -97,7 +97,7 @@ export const CardRowLabel = ({
</Typography>
{secondary && (
<Typography
fontSize={typographyLevels.s}
fontSize={typographyLevels.m}
color={theme.palette.text.secondary}
noWrap
>
@@ -132,7 +132,7 @@ export const CardFigure = ({
</Typography>
{caption && (
<Typography
fontSize={typographyLevels.s}
fontSize={typographyLevels.m}
color={theme.palette.text.secondary}
>
{caption}
@@ -37,7 +37,7 @@ const StatLine = ({
gap={theme.spacing(LAYOUT.SM)}
>
<Typography
fontSize={typographyLevels.s}
fontSize={typographyLevels.m}
color={theme.palette.text.secondary}
>
{label}
@@ -96,7 +96,7 @@ export const ChecksOnScheduleCard = () => {
secondary={job.failReason}
/>
<Typography
fontSize={typographyLevels.s}
fontSize={typographyLevels.m}
color={theme.palette.error.main}
flexShrink={0}
>
@@ -159,7 +159,7 @@ export const RecentErrorsCard = () => {
action={
errors > 0 ? (
<Typography
fontSize={typographyLevels.s}
fontSize={typographyLevels.m}
color={theme.palette.error.main}
>
{t("pages.dashboard.cards.recentErrors.count", { count: errors })}
@@ -183,7 +183,7 @@ export const RecentErrorsCard = () => {
secondary={[entry.service, entry.method].filter(Boolean).join(" · ")}
/>
<Typography
fontSize={typographyLevels.s}
fontSize={typographyLevels.m}
color={
entry.level === "error"
? theme.palette.error.main
@@ -48,13 +48,13 @@ const Metric = ({ label, value }: { label: string; value: number | null }) => {
gap={theme.spacing(LAYOUT.XS)}
>
<Typography
fontSize={typographyLevels.s}
fontSize={typographyLevels.m}
color={theme.palette.text.secondary}
>
{label}
</Typography>
<Typography
fontSize={typographyLevels.s}
fontSize={typographyLevels.m}
color={color}
>
{/* A down server reports nothing — show a dash, never 0%. */}
@@ -40,7 +40,7 @@ const MetricLine = ({
gap={theme.spacing(LAYOUT.SM)}
>
<Typography
fontSize={typographyLevels.s}
fontSize={typographyLevels.m}
color={theme.palette.text.secondary}
>
{label}
@@ -92,7 +92,7 @@ export const CurrentlyDownCard = () => {
action={
problems.length > 0 ? (
<Typography
fontSize={typographyLevels.s}
fontSize={typographyLevels.m}
color={theme.palette.error.main}
>
{t("pages.dashboard.cards.currentlyDown.count", {
@@ -120,7 +120,7 @@ export const CurrentlyDownCard = () => {
secondary={monitor.url}
/>
<Typography
fontSize={typographyLevels.s}
fontSize={typographyLevels.m}
color={theme.palette.text.secondary}
flexShrink={0}
>
@@ -128,7 +128,7 @@ export const CurrentlyDownCard = () => {
</Typography>
{since && (
<Typography
fontSize={typographyLevels.s}
fontSize={typographyLevels.m}
color={theme.palette.error.main}
flexShrink={0}
minWidth={DURATION_COLUMN_WIDTH}
@@ -154,7 +154,7 @@ export const CurrentlyDownCard = () => {
* the link and activating it navigates away.
*/}
<Typography
fontSize={typographyLevels.s}
fontSize={typographyLevels.m}
color={theme.palette.text.secondary}
flexShrink={0}
minWidth={STATUS_CODE_COLUMN_WIDTH}
@@ -79,7 +79,7 @@ export const MonitorStatusCard = () => {
size="md"
/>
<Typography
fontSize={typographyLevels.s}
fontSize={typographyLevels.m}
color={theme.palette.text.secondary}
>
{t(`pages.common.monitors.status.${key}`)}
@@ -85,7 +85,7 @@ export const MonitorsByGroupCard = () => {
>
{bucket.down > 0 && (
<Typography
fontSize={typographyLevels.s}
fontSize={typographyLevels.m}
color={theme.palette.error.main}
>
{t("pages.dashboard.cards.monitorsByType.down", {
@@ -85,7 +85,7 @@ export const MonitorsByTypeCard = () => {
>
{bucket.down > 0 && (
<Typography
fontSize={typographyLevels.s}
fontSize={typographyLevels.m}
color={theme.palette.error.main}
>
{t("pages.dashboard.cards.monitorsByType.down", {
@@ -33,7 +33,7 @@ const RankingCount = ({ shown, total }: { shown: number; total: number }) => {
const { t } = useTranslation();
return (
<Typography
fontSize={typographyLevels.s}
fontSize={typographyLevels.m}
color={theme.palette.text.secondary}
>
{t("pages.dashboard.cards.ranking.topOf", { shown, total })}
@@ -44,7 +44,7 @@ export const RecentIncidentsCard = () => {
action={
data && data.totalActive > 0 ? (
<Typography
fontSize={typographyLevels.s}
fontSize={typographyLevels.m}
color={theme.palette.error.main}
>
{t("pages.dashboard.cards.recentIncidents.active", {
@@ -78,7 +78,7 @@ export const RecentIncidentsCard = () => {
secondary={formatTimestamp(incident.startTime)}
/>
<Typography
fontSize={typographyLevels.s}
fontSize={typographyLevels.m}
color={theme.palette.text.secondary}
sx={{ flexShrink: 0 }}
>
@@ -88,7 +88,7 @@ export const RecentIncidentsCard = () => {
</Typography>
{incident.resolutionType === "manual" && (
<Typography
fontSize={typographyLevels.s}
fontSize={typographyLevels.m}
color={theme.palette.warning.main}
sx={{ flexShrink: 0 }}
>
@@ -27,7 +27,7 @@ const StatLine = ({ label, value }: { label: string; value: string }) => {
gap={theme.spacing(LAYOUT.SM)}
>
<Typography
fontSize={typographyLevels.s}
fontSize={typographyLevels.m}
color={theme.palette.text.secondary}
>
{label}
@@ -218,7 +218,7 @@ export const StatusPagesCard = () => {
})}
/>
<Typography
fontSize={typographyLevels.s}
fontSize={typographyLevels.m}
color={theme.palette.text.secondary}
flexShrink={0}
>
@@ -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)}
>
<Typography
fontSize={typographyLevels.s}
fontSize={typographyLevels.m}
color={theme.palette.text.secondary}
>
{text}
+3 -1
View File
@@ -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",
];
+16 -11
View File
@@ -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,
},