refactor(status-page): convert bold, modern and editorial themes to MUI sx

Finishes the migration started for refined. Each theme now consumes the same pattern:

- A typed *Styles interface that names every entry the page consumes (no `as Function` casts at the call site)
- A *Styles factory that takes (tokens, isDark) and returns sx recipes derived from existing tokens.ts plus the shared toneColor/toneSoft helpers in shared/overallStatus
- A useMemo around the factory call so MUI's sx processor sees referential equality across renders that don't change tokens
- modern.css's keyframes (pulse + staggered fade-in + reduced-motion guard) ported via @mui/system keyframes inside the modern styles factory
- bold.css's hero glow + conic logo + pseudo-elements ported as inline sx via "&::before" / "& > *" selectors
- editorial.css's serif typography + hairline dividers + flat rectangular badges ported as plain sx with the serif/sans family stacks named at module scope

Drop the four .css files (~1789 lines) along with the className= attributes and the import "./*.css" lines in each StatusPage component.

The shared themed components (ThemedHeatmap, ThemedHistogram, ThemedInfrastructure) lose their legacy classPrefix branch entirely now that no theme uses it. The dual-API gate is gone; there is one path.

Verified: tsc --noEmit clean, prettier clean.

Note: theme-overrides.css remains for the legacy non-themed status page components (StatusBar, MonitorsList, HeaderStatusPageControls). That's separate scope from the four-theme work the reviewer flagged; can be migrated in a follow-up.
This commit is contained in:
gorkem-bwl committed 2026-04-27 18:39:42 -04:00
1 parent b95d8886b9
commit c6c50063d8
12 files changed
+1442 -1616

No files matched your search

@@ -1,4 +1,4 @@
import { useState } from "react";
import { useMemo, useState } from "react";
import Box from "@mui/material/Box";
import Stack from "@mui/material/Stack";
import { useTranslation } from "react-i18next";
@@ -14,7 +14,8 @@ import {
resolveOverallStatus,
statusBadgeKey,
} from "../shared/overallStatus";
import "./bold.css";
import { useStatusPageTheme } from "../StatusPageThemeProvider";
import { boldStyles } from "./styles";
type StatusPageMonitor = Monitor & { checks?: Monitor["recentChecks"] };
@@ -23,85 +24,99 @@ interface Props {
monitors: StatusPageMonitor[];
}
const PREFIX = "bo";
export const BoldStatusPage = ({ statusPage, monitors }: Props) => {
const { t } = useTranslation();
const { tokens, mode } = useStatusPageTheme();
const styles = useMemo(() => boldStyles(tokens, mode === "dark"), [tokens, mode]);
const [chartMode, setChartMode] = useState<"heatmap" | "histogram">("heatmap");
// Bold uses a dedicated check-disc treatment next to the title; its
// icon is scaled differently but the semantic resolution is shared.
const overall = resolveOverallStatus(monitors, t, { iconSize: 18 });
const logoSrc = statusPage.logo?.data
? `data:${statusPage.logo.contentType};base64,${statusPage.logo.data}`
: null;
return (
<Box className={`${PREFIX}-page`}>
<Box sx={styles.page}>
<Box
component="header"
className={`${PREFIX}-top`}
sx={styles.top}
>
<Box className={`${PREFIX}-brand`}>
<Box sx={styles.brand}>
{logoSrc ? (
<Box
component="img"
src={logoSrc}
alt={statusPage.companyName}
className={`${PREFIX}-logo-img`}
sx={styles.logoImg}
/>
) : (
<Box className={`${PREFIX}-logo-conic`}>
{monoFirstChar(statusPage.companyName)}
</Box>
<Box sx={styles.logoConic}>{monoFirstChar(statusPage.companyName)}</Box>
)}
<span className={`${PREFIX}-company`}>{statusPage.companyName}</span>
<Box
component="span"
sx={styles.company}
>
{statusPage.companyName}
</Box>
</Box>
</Box>
<Box className={`${PREFIX}-hero ${PREFIX}-tone-${overall.tone}`}>
<h1 className={`${PREFIX}-hero-title`}>
<span className={`${PREFIX}-hero-check`}>{overall.icon}</span>
<Box sx={styles.hero}>
<Box
component="h1"
sx={styles.heroTitle}
>
<Box
component="span"
sx={styles.heroCheck(overall.tone)}
>
{overall.icon}
</Box>
{overall.message}
</h1>
<p className={`${PREFIX}-hero-sub`}>
</Box>
<Box
component="p"
sx={styles.heroSub}
>
{t("pages.statusPages.statusBar.monitoringSummary", {
count: monitors.length,
})}
</p>
</Box>
</Box>
{statusPage.showCharts && (
<Box className={`${PREFIX}-chart-switch-wrap`}>
<Box sx={styles.chartSwitchWrap}>
<Box
className={`${PREFIX}-chart-switch`}
sx={styles.chartSwitch}
role="radiogroup"
>
<button
<Box
component="button"
type="button"
role="radio"
aria-checked={chartMode === "heatmap"}
className={chartMode === "heatmap" ? `${PREFIX}-active` : ""}
onClick={() => setChartMode("heatmap")}
sx={styles.chartSwitchButton(chartMode === "heatmap")}
>
{t("pages.statusPages.monitorsList.chartTypeHeatmap")}
</button>
<button
</Box>
<Box
component="button"
type="button"
role="radio"
aria-checked={chartMode === "histogram"}
className={chartMode === "histogram" ? `${PREFIX}-active` : ""}
onClick={() => setChartMode("histogram")}
sx={styles.chartSwitchButton(chartMode === "histogram")}
>
{t("pages.statusPages.monitorsList.chartTypeHistogram")}
</button>
</Box>
</Box>
</Box>
)}
<Stack
component="ul"
className={`${PREFIX}-monitor-list`}
sx={styles.monitorList}
>
{monitors.map((monitor) => {
const isHardware = monitor.type === "hardware";
@@ -113,48 +128,65 @@ export const BoldStatusPage = ({ statusPage, monitors }: Props) => {
<Box
component="li"
key={monitor.id}
className={`${PREFIX}-card ${PREFIX}-card-${monitor.status}`}
sx={styles.card}
>
<Box className={`${PREFIX}-card-row`}>
<Box className={`${PREFIX}-card-left`}>
<Box className={`${PREFIX}-monitor-name`}>{monitor.name}</Box>
<Box className={`${PREFIX}-monitor-meta`}>
<span
className={`${PREFIX}-pill ${PREFIX}-pill-${isHardware ? "hardware" : "default"}`}
<Box sx={styles.cardRow}>
<Box sx={styles.cardLeft}>
<Box sx={styles.monitorName}>{monitor.name}</Box>
<Box sx={styles.monitorMeta}>
<Box
component="span"
sx={isHardware ? styles.pillHardware : styles.pill}
>
{getMonitorTypeLabel(monitor.type, t)}
</span>
</Box>
{monitor.url && (
<span
className={`${PREFIX}-monitor-url`}
<Box
component="span"
sx={styles.monitorUrl}
title={monitor.url}
>
{monitor.url}
</span>
</Box>
)}
</Box>
</Box>
<span className={`${PREFIX}-badge ${PREFIX}-badge-${badgeTone}`}>
<Box
component="span"
sx={styles.badge(badgeTone)}
>
{t(statusBadgeKey[monitor.status])}
</span>
</Box>
</Box>
{showInfra && (
<ThemedInfrastructure
monitor={monitor}
classPrefix={PREFIX}
sxApi={{
containerSx: styles.infra,
emptySx: styles.infraEmpty,
gaugeSx: styles.gauge,
gaugeLabelSx: styles.gaugeLabel,
gaugeValueSx: styles.gaugeValue,
gaugeBarSx: styles.gaugeBar,
gaugeFillSx: styles.gaugeFill,
gaugeSubSx: styles.gaugeSub,
}}
/>
)}
{showChart &&
(chartMode === "heatmap" ? (
<ThemedHeatmap
checks={monitor.recentChecks ?? []}
classPrefix={PREFIX}
containerSx={styles.heatmap}
cellSx={styles.heatmapCell}
/>
) : (
<ThemedHistogram
checks={monitor.recentChecks ?? []}
classPrefix={PREFIX}
containerSx={styles.histogram}
barSx={styles.bar}
statsSx={styles.chartStats}
/>
))}
</Box>
@@ -164,7 +196,7 @@ export const BoldStatusPage = ({ statusPage, monitors }: Props) => {
<Box
component="footer"
className={`${PREFIX}-footer`}
sx={styles.footer}
>
{t("pages.statusPages.footer.poweredBy")}{" "}
<a
@@ -1,497 +0,0 @@
/*
* Bold theme — scoped to [data-status-theme="bold"].
* Port of /Users/gorkemcetin/Desktop/checkmate-status-mockups/03-bold-dark.html.
* Signature traits: conic-gradient logo, dual-radial-glow body, large
* 34px hero title with a check-disc inline, glowing chart cells/bars,
* 18px-radius cards with staggered fade-in and accent-green hover.
*/
[data-status-theme="bold"]:not([data-status-transparent]) {
display: flex;
flex-direction: column;
min-height: 100vh;
background:
radial-gradient(
900px 500px at 15% -10%,
color-mix(in srgb, var(--sp-up) 4%, transparent),
transparent 60%
),
radial-gradient(
900px 500px at 95% 10%,
color-mix(in srgb, var(--sp-up-strong) 3%, transparent),
transparent 60%
),
var(--sp-bg);
}
[data-status-theme="bold"][data-status-mode="dark"]:not([data-status-transparent]) {
background:
radial-gradient(
900px 500px at 15% -10%,
color-mix(in srgb, var(--sp-up) 8%, transparent),
transparent 60%
),
radial-gradient(
900px 500px at 95% 10%,
color-mix(in srgb, var(--sp-up-strong) 9%, transparent),
transparent 60%
),
var(--sp-bg);
}
[data-status-theme="bold"] .bo-page {
flex: 1 0 auto;
max-width: 1040px;
width: 100%;
margin: 0 auto;
padding: 56px 24px 96px;
font:
14px/1.55 ui-sans-serif,
-apple-system,
BlinkMacSystemFont,
"Inter",
"Segoe UI",
Roboto,
sans-serif;
color: var(--sp-text);
-webkit-font-smoothing: antialiased;
}
/* --- header / conic logo --- */
[data-status-theme="bold"] .bo-top {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 36px;
}
[data-status-theme="bold"] .bo-brand {
display: flex;
align-items: center;
gap: 14px;
font-weight: 700;
font-size: 16px;
letter-spacing: -0.01em;
color: var(--sp-text);
}
[data-status-theme="bold"] .bo-logo-conic {
width: 36px;
height: 36px;
border-radius: 12px;
background: conic-gradient(
from 140deg,
var(--sp-up),
var(--sp-up-strong),
#0f766e,
var(--sp-up)
);
display: grid;
place-items: center;
color: #05070a;
font-weight: 900;
font-size: 14px;
box-shadow:
0 8px 24px color-mix(in srgb, var(--sp-up) 30%, transparent),
inset 0 1px 0 rgba(255, 255, 255, 0.2);
}
[data-status-theme="bold"] .bo-logo-img {
max-height: 36px;
max-width: 140px;
object-fit: contain;
}
/* --- hero: gradient overlay + dual radial glow + big title with check disc --- */
[data-status-theme="bold"] .bo-hero {
position: relative;
border-radius: 22px;
padding: 36px 36px 32px;
margin-bottom: 28px;
background:
linear-gradient(
135deg,
color-mix(in srgb, var(--sp-up) 8%, transparent),
transparent 60%
),
var(--sp-surface);
border: 1px solid var(--sp-border);
overflow: hidden;
}
[data-status-theme="bold"][data-status-mode="dark"] .bo-hero {
background:
linear-gradient(
135deg,
color-mix(in srgb, var(--sp-up) 16%, transparent),
transparent 60%
),
var(--sp-surface);
}
[data-status-theme="bold"] .bo-hero::before {
content: "";
position: absolute;
inset: 0;
background:
radial-gradient(
400px 200px at 20% 30%,
color-mix(in srgb, var(--sp-up) 14%, transparent),
transparent 60%
),
radial-gradient(
300px 200px at 90% 80%,
color-mix(in srgb, var(--sp-up-strong) 10%, transparent),
transparent 60%
);
pointer-events: none;
opacity: 0.8;
z-index: 0;
}
[data-status-theme="bold"][data-status-mode="dark"] .bo-hero::before {
background:
radial-gradient(
400px 200px at 20% 30%,
color-mix(in srgb, var(--sp-up) 32%, transparent),
transparent 60%
),
radial-gradient(
300px 200px at 90% 80%,
color-mix(in srgb, var(--sp-up-strong) 22%, transparent),
transparent 60%
);
mix-blend-mode: screen;
opacity: 0.8;
}
[data-status-theme="bold"] .bo-hero > * {
position: relative;
z-index: 1;
}
[data-status-theme="bold"] .bo-hero-title {
margin: 0;
font-size: 34px;
font-weight: 800;
letter-spacing: -0.02em;
line-height: 1.1;
color: var(--sp-text);
display: flex;
align-items: center;
gap: 10px;
}
[data-status-theme="bold"] .bo-hero-check {
display: inline-grid;
place-items: center;
width: 32px;
height: 32px;
border-radius: 50%;
background: var(--sp-up);
color: #05070a;
flex-shrink: 0;
box-shadow: 0 0 0 6px var(--sp-up-soft);
}
[data-status-theme="bold"] .bo-hero.bo-tone-warn .bo-hero-check {
background: var(--sp-warn);
box-shadow: 0 0 0 6px var(--sp-warn-soft);
}
[data-status-theme="bold"] .bo-hero.bo-tone-down .bo-hero-check {
background: var(--sp-down);
color: #fff;
box-shadow: 0 0 0 6px var(--sp-down-soft);
}
[data-status-theme="bold"] .bo-hero-sub {
margin: 10px 0 0;
color: var(--sp-text-muted);
font-size: 14px;
}
/* --- chart toggle --- */
[data-status-theme="bold"] .bo-chart-switch-wrap {
display: flex;
justify-content: flex-end;
margin-bottom: 16px;
}
[data-status-theme="bold"] .bo-chart-switch {
display: inline-flex;
border: 1px solid var(--sp-border);
border-radius: 999px;
background: var(--sp-surface);
padding: 3px;
gap: 2px;
}
[data-status-theme="bold"] .bo-chart-switch > button {
border: 0;
background: transparent;
font: inherit;
font-size: 11px;
padding: 8px 18px;
cursor: pointer;
color: var(--sp-text-muted);
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.12em;
transition:
background 0.2s,
color 0.2s;
border-radius: 999px;
}
[data-status-theme="bold"] .bo-chart-switch > button:hover {
color: var(--sp-text);
}
[data-status-theme="bold"] .bo-chart-switch > button.bo-active {
color: var(--sp-up);
background: var(--sp-up-soft);
}
/* --- cards: 18px radius, staggered fade-in, accent-green border on hover --- */
[data-status-theme="bold"] .bo-monitor-list {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 14px;
}
[data-status-theme="bold"] .bo-card {
background: var(--sp-surface);
border: 1px solid var(--sp-border);
border-radius: 18px;
overflow: hidden;
transition:
transform 0.2s cubic-bezier(0.2, 0.8, 0.2, 1),
border-color 0.2s;
}
[data-status-theme="bold"] .bo-card:hover {
transform: translateY(-3px);
border-color: color-mix(in srgb, var(--sp-up) 40%, var(--sp-border));
}
[data-status-theme="bold"] .bo-card-row {
display: grid;
grid-template-columns: 1fr auto;
align-items: center;
gap: 24px;
padding: 22px 28px;
}
[data-status-theme="bold"] .bo-card-left {
min-width: 0;
}
[data-status-theme="bold"] .bo-monitor-name {
font-weight: 700;
font-size: 17px;
letter-spacing: -0.01em;
color: var(--sp-text);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
[data-status-theme="bold"] .bo-monitor-meta {
display: flex;
gap: 10px;
align-items: center;
margin-top: 6px;
flex-wrap: wrap;
}
[data-status-theme="bold"] .bo-pill {
font-size: 10px;
text-transform: uppercase;
letter-spacing: 0.1em;
color: var(--sp-text-muted);
background: color-mix(in srgb, var(--sp-surface) 80%, var(--sp-bg));
border: 1px solid var(--sp-border);
padding: 3px 10px;
border-radius: 999px;
font-weight: 700;
}
[data-status-theme="bold"] .bo-pill-hardware {
color: var(--sp-up);
border-color: color-mix(in srgb, var(--sp-up) 40%, transparent);
background: var(--sp-up-soft);
}
[data-status-theme="bold"] .bo-monitor-url {
font-size: 12px;
color: var(--sp-text-muted);
font-family: ui-monospace, Menlo, monospace;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 300px;
}
/* --- badge: glowing dot --- */
[data-status-theme="bold"] .bo-badge {
font-size: 11px;
font-weight: 800;
padding: 6px 14px;
border-radius: 999px;
display: inline-flex;
align-items: center;
gap: 7px;
text-transform: uppercase;
letter-spacing: 0.08em;
white-space: nowrap;
}
[data-status-theme="bold"] .bo-badge::before {
content: "";
width: 6px;
height: 6px;
border-radius: 50%;
background: currentColor;
box-shadow: 0 0 8px currentColor;
}
[data-status-theme="bold"] .bo-badge-up {
background: var(--sp-up-soft);
color: var(--sp-up);
}
[data-status-theme="bold"] .bo-badge-warn {
background: var(--sp-warn-soft);
color: var(--sp-warn);
}
[data-status-theme="bold"] .bo-badge-down {
background: var(--sp-down-soft);
color: var(--sp-down);
}
/* --- heatmap: glowing cells --- */
[data-status-theme="bold"] .bo-heatmap {
padding: 0 28px 22px;
display: grid;
grid-template-columns: repeat(25, 1fr);
gap: 3px;
height: 48px;
}
[data-status-theme="bold"] .bo-heatmap-cell {
border-radius: 3px;
background: var(--sp-up);
transition: transform 0.15s;
box-shadow: 0 0 6px color-mix(in srgb, var(--sp-up) 18%, transparent);
}
[data-status-theme="bold"] .bo-heatmap-cell:hover {
transform: scaleY(1.2);
}
[data-status-theme="bold"] .bo-heatmap-cell.bo-fast {
background: var(--sp-up);
}
[data-status-theme="bold"] .bo-heatmap-cell.bo-med {
background: color-mix(in srgb, var(--sp-up) 70%, #ffffff 30%);
}
[data-status-theme="bold"] .bo-heatmap-cell.bo-slow {
background: var(--sp-warn);
box-shadow: 0 0 6px color-mix(in srgb, var(--sp-warn) 20%, transparent);
}
[data-status-theme="bold"] .bo-heatmap-cell.bo-down {
background: var(--sp-down);
box-shadow: 0 0 6px color-mix(in srgb, var(--sp-down) 20%, transparent);
}
[data-status-theme="bold"] .bo-heatmap-cell.bo-empty {
background: var(--sp-border);
box-shadow: none;
opacity: 0.4;
}
/* --- histogram: glowing gradient bars --- */
[data-status-theme="bold"] .bo-histogram {
padding: 0 28px;
display: grid;
grid-template-columns: repeat(25, 1fr);
gap: 3px;
align-items: flex-end;
height: 48px;
}
[data-status-theme="bold"] .bo-bar {
background: var(--sp-up);
border-radius: 3px;
min-height: 4px;
}
[data-status-theme="bold"] .bo-bar.bo-down {
background: var(--sp-down);
}
[data-status-theme="bold"] .bo-bar.bo-empty {
background: var(--sp-border);
box-shadow: none;
opacity: 0.4;
}
[data-status-theme="bold"] .bo-chart-stats {
padding: 0 28px 22px;
font-size: 11px;
color: var(--sp-text-muted);
font-variant-numeric: tabular-nums;
font-weight: 700;
letter-spacing: 0.05em;
}
/* --- gauges: larger, bolder, 16px radius --- */
[data-status-theme="bold"] .bo-infra {
padding: 18px 28px 26px;
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16px;
}
@media (max-width: 720px) {
[data-status-theme="bold"] .bo-infra {
grid-template-columns: 1fr;
}
}
[data-status-theme="bold"] .bo-infra-empty {
padding: 18px 28px 26px;
color: var(--sp-text-muted);
font-size: 13px;
}
[data-status-theme="bold"] .bo-gauge {
border: 1px solid var(--sp-border);
border-radius: 16px;
padding: 16px 18px;
background: color-mix(in srgb, var(--sp-surface) 80%, var(--sp-bg));
}
[data-status-theme="bold"] .bo-gauge-label {
font-size: 10px;
color: var(--sp-text-muted);
text-transform: uppercase;
letter-spacing: 0.12em;
font-weight: 700;
}
[data-status-theme="bold"] .bo-gauge-value {
font-size: 30px;
font-weight: 800;
letter-spacing: -0.02em;
margin-top: 6px;
font-variant-numeric: tabular-nums;
color: var(--sp-text);
}
[data-status-theme="bold"] .bo-gauge-bar {
height: 6px;
background: var(--sp-border);
border-radius: 3px;
overflow: hidden;
margin-top: 12px;
}
[data-status-theme="bold"] .bo-gauge-fill {
display: block;
height: 100%;
background: linear-gradient(
90deg,
var(--sp-up),
color-mix(in srgb, var(--sp-up) 70%, #ffffff 30%)
);
border-radius: 3px;
transition: width 0.9s cubic-bezier(0.2, 0.8, 0.2, 1);
}
[data-status-theme="bold"] .bo-gauge-fill.bo-warm {
background: linear-gradient(90deg, var(--sp-warn), #fbbf24);
}
[data-status-theme="bold"] .bo-gauge-fill.bo-hot {
background: linear-gradient(90deg, var(--sp-down), #fb7185);
}
[data-status-theme="bold"] .bo-gauge-sub {
font-size: 11px;
color: var(--sp-text-muted);
margin-top: 10px;
font-variant-numeric: tabular-nums;
}
/* --- footer --- */
[data-status-theme="bold"] .bo-footer {
text-align: center;
color: var(--sp-text-muted);
font-size: 12px;
margin-top: 52px;
}
[data-status-theme="bold"] .bo-footer a {
color: var(--sp-up);
text-decoration: underline;
text-underline-offset: 3px;
font-weight: 800;
}
@@ -0,0 +1,408 @@
import type { SxProps, Theme } from "@mui/material/styles";
import type { StatusPageThemeTokens } from "../tokens";
import { type OverallTone, toneColor, toneSoft } from "../shared/overallStatus";
export type BoldHeatCell = "fast" | "med" | "slow" | "down" | "empty";
export type BoldBarKind = "up" | "down" | "empty";
export type BoldGaugeFill = "ok" | "warm" | "hot";
export interface BoldStyles {
page: SxProps<Theme>;
top: SxProps<Theme>;
brand: SxProps<Theme>;
logoConic: SxProps<Theme>;
logoImg: SxProps<Theme>;
company: SxProps<Theme>;
hero: SxProps<Theme>;
heroTitle: SxProps<Theme>;
heroCheck: (tone: OverallTone) => SxProps<Theme>;
heroSub: SxProps<Theme>;
chartSwitchWrap: SxProps<Theme>;
chartSwitch: SxProps<Theme>;
chartSwitchButton: (active: boolean) => SxProps<Theme>;
monitorList: SxProps<Theme>;
card: SxProps<Theme>;
cardRow: SxProps<Theme>;
cardLeft: SxProps<Theme>;
monitorName: SxProps<Theme>;
monitorMeta: SxProps<Theme>;
pill: SxProps<Theme>;
pillHardware: SxProps<Theme>;
monitorUrl: SxProps<Theme>;
badge: (tone: OverallTone) => SxProps<Theme>;
heatmap: SxProps<Theme>;
heatmapCell: (kind: BoldHeatCell) => SxProps<Theme>;
histogram: SxProps<Theme>;
bar: (kind: BoldBarKind, heightPct: number) => SxProps<Theme>;
chartStats: SxProps<Theme>;
infra: SxProps<Theme>;
infraEmpty: SxProps<Theme>;
gauge: SxProps<Theme>;
gaugeLabel: SxProps<Theme>;
gaugeValue: SxProps<Theme>;
gaugeBar: SxProps<Theme>;
gaugeFill: (level: BoldGaugeFill, widthPct: number) => SxProps<Theme>;
gaugeSub: SxProps<Theme>;
footer: SxProps<Theme>;
}
const sansFontStack =
'ui-sans-serif, -apple-system, BlinkMacSystemFont, "Inter", "Segoe UI", Roboto, sans-serif';
export const boldStyles = (
tokens: StatusPageThemeTokens,
isDark: boolean
): BoldStyles => {
const heatCellBg: Record<BoldHeatCell, string> = {
fast: tokens.up,
med: `color-mix(in srgb, ${tokens.up} 70%, #ffffff 30%)`,
slow: tokens.warn,
down: tokens.down,
empty: tokens.border,
};
const heatCellShadow: Record<BoldHeatCell, string> = {
fast: `0 0 6px color-mix(in srgb, ${tokens.up} 18%, transparent)`,
med: `0 0 6px color-mix(in srgb, ${tokens.up} 18%, transparent)`,
slow: `0 0 6px color-mix(in srgb, ${tokens.warn} 20%, transparent)`,
down: `0 0 6px color-mix(in srgb, ${tokens.down} 20%, transparent)`,
empty: "none",
};
const barBg: Record<BoldBarKind, string> = {
up: tokens.up,
down: tokens.down,
empty: tokens.border,
};
const gaugeFillBg: Record<BoldGaugeFill, string> = {
ok: `linear-gradient(90deg, ${tokens.up}, color-mix(in srgb, ${tokens.up} 70%, #ffffff 30%))`,
warm: `linear-gradient(90deg, ${tokens.warn}, #fbbf24)`,
hot: `linear-gradient(90deg, ${tokens.down}, #fb7185)`,
};
const heroBgLight = `linear-gradient(135deg, color-mix(in srgb, ${tokens.up} 8%, transparent), transparent 60%), ${tokens.surface}`;
const heroBgDark = `linear-gradient(135deg, color-mix(in srgb, ${tokens.up} 16%, transparent), transparent 60%), ${tokens.surface}`;
const heroGlowLight = `radial-gradient(400px 200px at 20% 30%, color-mix(in srgb, ${tokens.up} 14%, transparent), transparent 60%), radial-gradient(300px 200px at 90% 80%, color-mix(in srgb, ${tokens.upStrong} 10%, transparent), transparent 60%)`;
const heroGlowDark = `radial-gradient(400px 200px at 20% 30%, color-mix(in srgb, ${tokens.up} 32%, transparent), transparent 60%), radial-gradient(300px 200px at 90% 80%, color-mix(in srgb, ${tokens.upStrong} 22%, transparent), transparent 60%)`;
return {
page: {
flex: "1 0 auto",
maxWidth: 1040,
width: "100%",
mx: "auto",
p: "56px 24px 96px",
fontFamily: sansFontStack,
fontSize: 14,
lineHeight: 1.55,
color: tokens.text,
WebkitFontSmoothing: "antialiased",
},
top: {
display: "flex",
alignItems: "center",
justifyContent: "space-between",
mb: "36px",
},
brand: {
display: "flex",
alignItems: "center",
gap: "14px",
fontWeight: 700,
fontSize: 16,
letterSpacing: "-0.01em",
color: tokens.text,
},
logoConic: {
width: 36,
height: 36,
borderRadius: "12px",
background: `conic-gradient(from 140deg, ${tokens.up}, ${tokens.upStrong}, #0f766e, ${tokens.up})`,
display: "grid",
placeItems: "center",
color: "#05070a",
fontWeight: 900,
fontSize: 14,
boxShadow: `0 8px 24px color-mix(in srgb, ${tokens.up} 30%, transparent), inset 0 1px 0 rgba(255, 255, 255, 0.2)`,
},
logoImg: { maxHeight: 36, maxWidth: 140, objectFit: "contain" },
company: {},
hero: {
position: "relative",
borderRadius: "22px",
padding: "36px 36px 32px",
mb: "28px",
background: isDark ? heroBgDark : heroBgLight,
border: `1px solid ${tokens.border}`,
overflow: "hidden",
"&::before": {
content: '""',
position: "absolute",
inset: 0,
background: isDark ? heroGlowDark : heroGlowLight,
pointerEvents: "none",
opacity: 0.8,
zIndex: 0,
...(isDark ? { mixBlendMode: "screen" } : {}),
},
"& > *": { position: "relative", zIndex: 1 },
},
heroTitle: {
m: 0,
fontSize: 34,
fontWeight: 800,
letterSpacing: "-0.02em",
lineHeight: 1.1,
color: tokens.text,
display: "flex",
alignItems: "center",
gap: "10px",
},
heroCheck: (tone) => ({
display: "inline-grid",
placeItems: "center",
width: 32,
height: 32,
borderRadius: "50%",
background: toneColor(tone, tokens),
color: tone === "down" ? "#fff" : "#05070a",
flexShrink: 0,
boxShadow: `0 0 0 6px ${toneSoft(tone, tokens)}`,
}),
heroSub: { mt: "10px", mb: 0, color: tokens.textMuted, fontSize: 14 },
chartSwitchWrap: { display: "flex", justifyContent: "flex-end", mb: "16px" },
chartSwitch: {
display: "inline-flex",
border: `1px solid ${tokens.border}`,
borderRadius: "999px",
background: tokens.surface,
p: "3px",
gap: "2px",
},
chartSwitchButton: (active) => ({
border: 0,
background: active ? tokens.upSoft : "transparent",
fontFamily: "inherit",
fontSize: 11,
padding: "8px 18px",
cursor: "pointer",
color: active ? tokens.up : tokens.textMuted,
fontWeight: 700,
textTransform: "uppercase",
letterSpacing: "0.12em",
transition: "background 0.2s, color 0.2s",
borderRadius: "999px",
"&:hover": { color: active ? tokens.up : tokens.text },
}),
monitorList: {
listStyle: "none",
m: 0,
p: 0,
display: "flex",
flexDirection: "column",
gap: "14px",
},
card: {
background: tokens.surface,
border: `1px solid ${tokens.border}`,
borderRadius: "18px",
overflow: "hidden",
transition: "transform 0.2s cubic-bezier(0.2, 0.8, 0.2, 1), border-color 0.2s",
"&:hover": {
transform: "translateY(-3px)",
borderColor: `color-mix(in srgb, ${tokens.up} 40%, ${tokens.border})`,
},
},
cardRow: {
display: "grid",
gridTemplateColumns: "1fr auto",
alignItems: "center",
gap: "24px",
p: "22px 28px",
},
cardLeft: { minWidth: 0 },
monitorName: {
fontWeight: 700,
fontSize: 17,
letterSpacing: "-0.01em",
color: tokens.text,
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
},
monitorMeta: {
display: "flex",
gap: "10px",
alignItems: "center",
mt: "6px",
flexWrap: "wrap",
},
pill: {
fontSize: 10,
textTransform: "uppercase",
letterSpacing: "0.1em",
color: tokens.textMuted,
background: `color-mix(in srgb, ${tokens.surface} 80%, ${tokens.bg})`,
border: `1px solid ${tokens.border}`,
padding: "3px 10px",
borderRadius: "999px",
fontWeight: 700,
},
pillHardware: {
fontSize: 10,
textTransform: "uppercase",
letterSpacing: "0.1em",
padding: "3px 10px",
borderRadius: "999px",
fontWeight: 700,
color: tokens.up,
borderColor: `color-mix(in srgb, ${tokens.up} 40%, transparent)`,
border: `1px solid color-mix(in srgb, ${tokens.up} 40%, transparent)`,
background: tokens.upSoft,
},
monitorUrl: {
fontSize: 12,
color: tokens.textMuted,
fontFamily: "ui-monospace, Menlo, monospace",
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
maxWidth: 300,
},
badge: (tone) => ({
fontSize: 11,
fontWeight: 800,
padding: "6px 14px",
borderRadius: "999px",
display: "inline-flex",
alignItems: "center",
gap: "7px",
textTransform: "uppercase",
letterSpacing: "0.08em",
whiteSpace: "nowrap",
background: toneSoft(tone, tokens),
color: toneColor(tone, tokens),
"&::before": {
content: '""',
width: 6,
height: 6,
borderRadius: "50%",
background: "currentColor",
boxShadow: "0 0 8px currentColor",
},
}),
heatmap: {
padding: "0 28px 22px",
display: "grid",
gridTemplateColumns: "repeat(25, 1fr)",
gap: "3px",
height: 48,
},
heatmapCell: (kind) => ({
borderRadius: "3px",
background: heatCellBg[kind],
boxShadow: heatCellShadow[kind],
opacity: kind === "empty" ? 0.4 : 1,
transition: "transform 0.15s",
"&:hover": { transform: "scaleY(1.2)" },
}),
histogram: {
padding: "0 28px",
display: "grid",
gridTemplateColumns: "repeat(25, 1fr)",
gap: "3px",
alignItems: "flex-end",
height: 48,
},
bar: (kind, heightPct) => ({
background: barBg[kind],
borderRadius: "3px",
minHeight: 4,
opacity: kind === "empty" ? 0.4 : 1,
height: `${heightPct}%`,
}),
chartStats: {
padding: "0 28px 22px",
fontSize: 11,
color: tokens.textMuted,
fontVariantNumeric: "tabular-nums",
fontWeight: 700,
letterSpacing: "0.05em",
},
infra: {
padding: "18px 28px 26px",
display: "grid",
gridTemplateColumns: { xs: "1fr", md: "repeat(3, 1fr)" },
gap: "16px",
},
infraEmpty: {
padding: "18px 28px 26px",
color: tokens.textMuted,
fontSize: 13,
},
gauge: {
border: `1px solid ${tokens.border}`,
borderRadius: "16px",
p: "16px 18px",
background: `color-mix(in srgb, ${tokens.surface} 80%, ${tokens.bg})`,
},
gaugeLabel: {
fontSize: 10,
color: tokens.textMuted,
textTransform: "uppercase",
letterSpacing: "0.12em",
fontWeight: 700,
},
gaugeValue: {
fontSize: 30,
fontWeight: 800,
letterSpacing: "-0.02em",
mt: "6px",
fontVariantNumeric: "tabular-nums",
color: tokens.text,
},
gaugeBar: {
height: 6,
background: tokens.border,
borderRadius: "3px",
overflow: "hidden",
mt: "12px",
},
gaugeFill: (level, widthPct) => ({
display: "block",
height: "100%",
background: gaugeFillBg[level],
borderRadius: "3px",
transition: "width 0.9s cubic-bezier(0.2, 0.8, 0.2, 1)",
width: `${Math.max(0, Math.min(100, widthPct))}%`,
}),
gaugeSub: {
fontSize: 11,
color: tokens.textMuted,
mt: "10px",
fontVariantNumeric: "tabular-nums",
},
footer: {
textAlign: "center",
color: tokens.textMuted,
fontSize: 12,
mt: "52px",
"& a": {
color: tokens.up,
textDecoration: "underline",
textUnderlineOffset: "3px",
fontWeight: 800,
},
},
};
};
@@ -13,7 +13,8 @@ import {
resolveOverallStatus,
statusBadgeKey,
} from "../shared/overallStatus";
import "./editorial.css";
import { useStatusPageTheme } from "../StatusPageThemeProvider";
import { editorialStyles } from "./styles";
type StatusPageMonitor = Monitor & { checks?: Monitor["recentChecks"] };
@@ -22,14 +23,12 @@ interface Props {
monitors: StatusPageMonitor[];
}
const PREFIX = "ed";
export const EditorialStatusPage = ({ statusPage, monitors }: Props) => {
const { t } = useTranslation();
const { tokens, mode } = useStatusPageTheme();
const styles = useMemo(() => editorialStyles(tokens, mode === "dark"), [tokens, mode]);
const [chartMode, setChartMode] = useState<"heatmap" | "histogram">("heatmap");
// Stable date across re-renders; the page polls every 10s and we don't
// want the dateline to jitter.
const todayLabel = useMemo(
() =>
new Date().toLocaleDateString(undefined, {
@@ -48,69 +47,88 @@ export const EditorialStatusPage = ({ statusPage, monitors }: Props) => {
: null;
return (
<Box className={`${PREFIX}-page`}>
<Box sx={styles.page}>
<Box
component="header"
className={`${PREFIX}-top`}
sx={styles.top}
>
<Box className={`${PREFIX}-brand-wrap`}>
<Box sx={styles.brandWrap}>
{logoSrc && (
<Box
component="img"
src={logoSrc}
alt={statusPage.companyName}
className={`${PREFIX}-logo-img`}
sx={styles.logoImg}
/>
)}
<h2 className={`${PREFIX}-brand-eyebrow`}>{statusPage.companyName}</h2>
<h1 className={`${PREFIX}-brand-title`}>
<Box
component="h2"
sx={styles.brandEyebrow}
>
{statusPage.companyName}
</Box>
<Box
component="h1"
sx={styles.brandTitle}
>
{t("pages.statusPages.editorial.reportTitle")}
</h1>
</Box>
</Box>
</Box>
<p className={`${PREFIX}-status-line ${PREFIX}-tone-${overall.tone}`}>
<span className={`${PREFIX}-status-dot`} />
<Box
component="p"
sx={styles.statusLine}
>
<Box
component="span"
sx={styles.statusDot(overall.tone)}
/>
{overall.message}
</p>
<p className={`${PREFIX}-dateline`}>
</Box>
<Box
component="p"
sx={styles.dateline}
>
{todayLabel} ·{" "}
{t("pages.statusPages.statusBar.monitoringSummary", {
count: monitors.length,
})}
</p>
</Box>
{statusPage.showCharts && (
<Box className={`${PREFIX}-chart-switch-wrap`}>
<Box sx={styles.chartSwitchWrap}>
<Box
className={`${PREFIX}-chart-switch`}
sx={styles.chartSwitch}
role="radiogroup"
>
<button
<Box
component="button"
type="button"
role="radio"
aria-checked={chartMode === "heatmap"}
className={chartMode === "heatmap" ? `${PREFIX}-active` : ""}
onClick={() => setChartMode("heatmap")}
sx={styles.chartSwitchButton(chartMode === "heatmap", true)}
>
{t("pages.statusPages.monitorsList.chartTypeHeatmap")}
</button>
<button
</Box>
<Box
component="button"
type="button"
role="radio"
aria-checked={chartMode === "histogram"}
className={chartMode === "histogram" ? `${PREFIX}-active` : ""}
onClick={() => setChartMode("histogram")}
sx={styles.chartSwitchButton(chartMode === "histogram", false)}
>
{t("pages.statusPages.monitorsList.chartTypeHistogram")}
</button>
</Box>
</Box>
</Box>
)}
<Stack
component="ul"
className={`${PREFIX}-monitor-list`}
sx={styles.monitorList}
>
{monitors.map((monitor) => {
const isHardware = monitor.type === "hardware";
@@ -122,48 +140,65 @@ export const EditorialStatusPage = ({ statusPage, monitors }: Props) => {
<Box
component="li"
key={monitor.id}
className={`${PREFIX}-card ${PREFIX}-card-${monitor.status}`}
sx={styles.card}
>
<Box className={`${PREFIX}-card-row`}>
<Box className={`${PREFIX}-card-left`}>
<Box className={`${PREFIX}-monitor-name`}>{monitor.name}</Box>
<Box className={`${PREFIX}-monitor-meta`}>
<span
className={`${PREFIX}-pill ${PREFIX}-pill-${isHardware ? "hardware" : "default"}`}
<Box sx={styles.cardRow}>
<Box sx={styles.cardLeft}>
<Box sx={styles.monitorName}>{monitor.name}</Box>
<Box sx={styles.monitorMeta}>
<Box
component="span"
sx={isHardware ? styles.pillHardware : styles.pill}
>
{getMonitorTypeLabel(monitor.type, t)}
</span>
</Box>
{monitor.url && (
<span
className={`${PREFIX}-monitor-url`}
<Box
component="span"
sx={styles.monitorUrl}
title={monitor.url}
>
{monitor.url}
</span>
</Box>
)}
</Box>
</Box>
<span className={`${PREFIX}-badge ${PREFIX}-badge-${badgeTone}`}>
<Box
component="span"
sx={styles.badge(badgeTone)}
>
{t(statusBadgeKey[monitor.status])}
</span>
</Box>
</Box>
{showInfra && (
<ThemedInfrastructure
monitor={monitor}
classPrefix={PREFIX}
sxApi={{
containerSx: styles.infra,
emptySx: styles.infraEmpty,
gaugeSx: styles.gauge,
gaugeLabelSx: styles.gaugeLabel,
gaugeValueSx: styles.gaugeValue,
gaugeBarSx: styles.gaugeBar,
gaugeFillSx: styles.gaugeFill,
gaugeSubSx: styles.gaugeSub,
}}
/>
)}
{showChart &&
(chartMode === "heatmap" ? (
<ThemedHeatmap
checks={monitor.recentChecks ?? []}
classPrefix={PREFIX}
containerSx={styles.heatmap}
cellSx={styles.heatmapCell}
/>
) : (
<ThemedHistogram
checks={monitor.recentChecks ?? []}
classPrefix={PREFIX}
containerSx={styles.histogram}
barSx={styles.bar}
statsSx={styles.chartStats}
/>
))}
</Box>
@@ -173,7 +208,7 @@ export const EditorialStatusPage = ({ statusPage, monitors }: Props) => {
<Box
component="footer"
className={`${PREFIX}-footer`}
sx={styles.footer}
>
{t("pages.statusPages.footer.poweredBy")}{" "}
<a
@@ -1,353 +0,0 @@
/*
* Editorial theme — scoped to [data-status-theme="editorial"].
* Port of /Users/gorkemcetin/Desktop/checkmate-status-mockups/04-editorial.html.
* Signature traits: serif body, narrow 760px column, newspaper-style
* header with eyebrow + big "Service Report" title, flat rectangular
* badges (no border-radius), hairline dividers instead of cards,
* dateline below status, serif gauge values.
*/
[data-status-theme="editorial"]:not([data-status-transparent]) {
display: flex;
flex-direction: column;
min-height: 100vh;
background: var(--sp-bg);
}
[data-status-theme="editorial"] .ed-page {
flex: 1 0 auto;
max-width: 760px;
width: 100%;
margin: 0 auto;
padding: 64px 24px 96px;
font:
15px/1.6 Georgia,
"Iowan Old Style",
"Palatino Linotype",
serif;
color: var(--sp-text);
-webkit-font-smoothing: antialiased;
}
/* --- header: eyebrow caps + serif h1, 2px underline under whole block --- */
[data-status-theme="editorial"] .ed-top {
display: flex;
align-items: flex-end;
justify-content: space-between;
margin-bottom: 48px;
border-bottom: 2px solid var(--sp-text);
padding-bottom: 18px;
}
[data-status-theme="editorial"] .ed-brand-wrap {
display: flex;
flex-direction: column;
}
[data-status-theme="editorial"] .ed-logo-img {
max-height: 48px;
max-width: 160px;
object-fit: contain;
margin-bottom: 8px;
}
[data-status-theme="editorial"] .ed-brand-eyebrow {
margin: 0;
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.3em;
font-weight: 700;
color: var(--sp-text-muted);
font-family: -apple-system, "Helvetica Neue", Arial, sans-serif;
}
[data-status-theme="editorial"] .ed-brand-title {
margin: 6px 0 0;
font-size: 40px;
font-weight: 700;
letter-spacing: -0.02em;
line-height: 1;
color: var(--sp-text);
}
/* --- status line: single sentence, not a card --- */
[data-status-theme="editorial"] .ed-status-line {
font-size: 22px;
font-weight: 400;
margin: 0 0 8px;
letter-spacing: -0.01em;
line-height: 1.35;
color: var(--sp-text);
font-family: Georgia, "Iowan Old Style", "Palatino Linotype", serif;
}
[data-status-theme="editorial"] .ed-status-dot {
display: inline-block;
width: 10px;
height: 10px;
border-radius: 50%;
background: var(--sp-up);
margin-right: 10px;
vertical-align: middle;
}
[data-status-theme="editorial"] .ed-status-line.ed-tone-warn .ed-status-dot {
background: var(--sp-warn);
}
[data-status-theme="editorial"] .ed-status-line.ed-tone-down .ed-status-dot {
background: var(--sp-down);
}
[data-status-theme="editorial"] .ed-dateline {
font-family: -apple-system, "Helvetica Neue", Arial, sans-serif;
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.12em;
color: var(--sp-text-muted);
margin: 0 0 56px;
}
/* --- chart switch: rectangular, dark fill on active (no radius) --- */
[data-status-theme="editorial"] .ed-chart-switch-wrap {
display: flex;
justify-content: flex-end;
margin-bottom: 20px;
font-family: -apple-system, "Helvetica Neue", Arial, sans-serif;
}
[data-status-theme="editorial"] .ed-chart-switch {
display: inline-flex;
border: 1px solid var(--sp-text);
}
[data-status-theme="editorial"] .ed-chart-switch > button {
font: inherit;
font-size: 10px;
text-transform: uppercase;
letter-spacing: 0.2em;
padding: 6px 14px;
border: 0;
background: transparent;
color: var(--sp-text);
cursor: pointer;
font-weight: 700;
}
[data-status-theme="editorial"] .ed-chart-switch > button + button {
border-left: 1px solid var(--sp-text);
}
[data-status-theme="editorial"] .ed-chart-switch > button.ed-active {
background: var(--sp-text);
color: var(--sp-bg);
}
/* --- monitor cards: transparent rows with bottom hairline --- */
[data-status-theme="editorial"] .ed-monitor-list {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
}
[data-status-theme="editorial"] .ed-card {
background: transparent;
border: 0;
border-bottom: 1px solid var(--sp-border);
margin-bottom: 0;
padding: 20px 0;
}
[data-status-theme="editorial"] .ed-card:last-of-type {
border-bottom: 0;
}
[data-status-theme="editorial"] .ed-card-row {
display: grid;
grid-template-columns: 1fr auto;
align-items: baseline;
gap: 20px;
}
[data-status-theme="editorial"] .ed-card-left {
min-width: 0;
}
[data-status-theme="editorial"] .ed-monitor-name {
font-size: 20px;
font-weight: 700;
letter-spacing: -0.015em;
color: var(--sp-text);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
[data-status-theme="editorial"] .ed-monitor-meta {
font-family: -apple-system, "Helvetica Neue", Arial, sans-serif;
font-size: 12px;
color: var(--sp-text-muted);
margin-top: 6px;
display: flex;
gap: 12px;
flex-wrap: wrap;
align-items: center;
}
[data-status-theme="editorial"] .ed-pill {
text-transform: uppercase;
letter-spacing: 0.12em;
font-weight: 700;
color: var(--sp-text-muted);
}
[data-status-theme="editorial"] .ed-pill-hardware {
color: var(--sp-up);
}
[data-status-theme="editorial"] .ed-monitor-url {
font-family: ui-monospace, Menlo, monospace;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 280px;
font-size: 12px;
}
/* --- badge: rectangular flat label --- */
[data-status-theme="editorial"] .ed-badge {
font-family: -apple-system, "Helvetica Neue", Arial, sans-serif;
font-size: 10px;
font-weight: 700;
padding: 4px 10px;
border-radius: 0;
text-transform: uppercase;
letter-spacing: 0.15em;
white-space: nowrap;
}
[data-status-theme="editorial"] .ed-badge-up {
background: var(--sp-up-soft);
color: var(--sp-up);
}
[data-status-theme="editorial"] .ed-badge-warn {
background: var(--sp-warn-soft);
color: var(--sp-warn);
}
[data-status-theme="editorial"] .ed-badge-down {
background: var(--sp-down-soft);
color: var(--sp-down);
}
/* --- charts: flat rectangles, no glow --- */
[data-status-theme="editorial"] .ed-heatmap {
margin-top: 16px;
display: grid;
grid-template-columns: repeat(25, 1fr);
gap: 2px;
height: 40px;
}
[data-status-theme="editorial"] .ed-heatmap-cell {
background: var(--sp-up);
}
[data-status-theme="editorial"] .ed-heatmap-cell.ed-fast {
background: var(--sp-up);
}
[data-status-theme="editorial"] .ed-heatmap-cell.ed-med {
background: color-mix(in srgb, var(--sp-up) 60%, var(--sp-bg));
}
[data-status-theme="editorial"] .ed-heatmap-cell.ed-slow {
background: var(--sp-warn);
}
[data-status-theme="editorial"] .ed-heatmap-cell.ed-down {
background: var(--sp-down);
}
[data-status-theme="editorial"] .ed-heatmap-cell.ed-empty {
background: var(--sp-border);
opacity: 0.5;
}
[data-status-theme="editorial"] .ed-histogram {
margin-top: 16px;
display: grid;
grid-template-columns: repeat(25, 1fr);
gap: 2px;
align-items: flex-end;
height: 40px;
}
[data-status-theme="editorial"] .ed-bar {
background: var(--sp-up);
min-height: 3px;
}
[data-status-theme="editorial"] .ed-bar.ed-down {
background: var(--sp-down);
}
[data-status-theme="editorial"] .ed-bar.ed-empty {
background: var(--sp-border);
opacity: 0.5;
}
[data-status-theme="editorial"] .ed-chart-stats {
font-family: -apple-system, "Helvetica Neue", Arial, sans-serif;
font-size: 11px;
color: var(--sp-text-muted);
font-variant-numeric: tabular-nums;
text-transform: uppercase;
letter-spacing: 0.1em;
}
/* --- gauges: 3-column with serif value + 2px underline bar --- */
[data-status-theme="editorial"] .ed-infra {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 24px;
margin-top: 20px;
font-family: -apple-system, "Helvetica Neue", Arial, sans-serif;
}
@media (max-width: 720px) {
[data-status-theme="editorial"] .ed-infra {
grid-template-columns: 1fr;
}
}
[data-status-theme="editorial"] .ed-infra-empty {
margin-top: 20px;
color: var(--sp-text-muted);
font-size: 13px;
}
[data-status-theme="editorial"] .ed-gauge-label {
font-size: 10px;
color: var(--sp-text-muted);
text-transform: uppercase;
letter-spacing: 0.18em;
font-weight: 700;
}
[data-status-theme="editorial"] .ed-gauge-value {
font-size: 28px;
font-weight: 700;
letter-spacing: -0.02em;
margin-top: 4px;
font-variant-numeric: tabular-nums;
font-family: Georgia, serif;
color: var(--sp-text);
}
[data-status-theme="editorial"] .ed-gauge-bar {
height: 2px;
background: var(--sp-border);
margin-top: 10px;
}
[data-status-theme="editorial"] .ed-gauge-fill {
display: block;
height: 100%;
background: var(--sp-up);
transition: width 0.8s;
}
[data-status-theme="editorial"] .ed-gauge-fill.ed-warm {
background: var(--sp-warn);
}
[data-status-theme="editorial"] .ed-gauge-fill.ed-hot {
background: var(--sp-down);
}
[data-status-theme="editorial"] .ed-gauge-sub {
font-size: 11px;
color: var(--sp-text-muted);
margin-top: 8px;
font-variant-numeric: tabular-nums;
}
/* --- footer: tiny uppercase tracked + underlined link --- */
[data-status-theme="editorial"] .ed-footer {
text-align: center;
color: var(--sp-text-muted);
font-size: 11px;
margin-top: 56px;
text-transform: uppercase;
letter-spacing: 0.2em;
font-family: -apple-system, "Helvetica Neue", Arial, sans-serif;
}
[data-status-theme="editorial"] .ed-footer a {
color: var(--sp-text);
text-decoration: none;
border-bottom: 1px solid var(--sp-text);
padding-bottom: 2px;
font-weight: 700;
}
@@ -0,0 +1,335 @@
import type { SxProps, Theme } from "@mui/material/styles";
import type { StatusPageThemeTokens } from "../tokens";
import { type OverallTone, toneColor, toneSoft } from "../shared/overallStatus";
export type EditorialHeatCell = "fast" | "med" | "slow" | "down" | "empty";
export type EditorialBarKind = "up" | "down" | "empty";
export type EditorialGaugeFill = "ok" | "warm" | "hot";
export interface EditorialStyles {
page: SxProps<Theme>;
top: SxProps<Theme>;
brandWrap: SxProps<Theme>;
logoImg: SxProps<Theme>;
brandEyebrow: SxProps<Theme>;
brandTitle: SxProps<Theme>;
statusLine: SxProps<Theme>;
statusDot: (tone: OverallTone) => SxProps<Theme>;
dateline: SxProps<Theme>;
chartSwitchWrap: SxProps<Theme>;
chartSwitch: SxProps<Theme>;
chartSwitchButton: (active: boolean, isFirst: boolean) => SxProps<Theme>;
monitorList: SxProps<Theme>;
card: SxProps<Theme>;
cardRow: SxProps<Theme>;
cardLeft: SxProps<Theme>;
monitorName: SxProps<Theme>;
monitorMeta: SxProps<Theme>;
pill: SxProps<Theme>;
pillHardware: SxProps<Theme>;
monitorUrl: SxProps<Theme>;
badge: (tone: OverallTone) => SxProps<Theme>;
heatmap: SxProps<Theme>;
heatmapCell: (kind: EditorialHeatCell) => SxProps<Theme>;
histogram: SxProps<Theme>;
bar: (kind: EditorialBarKind, heightPct: number) => SxProps<Theme>;
chartStats: SxProps<Theme>;
infra: SxProps<Theme>;
infraEmpty: SxProps<Theme>;
gauge: SxProps<Theme>;
gaugeLabel: SxProps<Theme>;
gaugeValue: SxProps<Theme>;
gaugeBar: SxProps<Theme>;
gaugeFill: (level: EditorialGaugeFill, widthPct: number) => SxProps<Theme>;
gaugeSub: SxProps<Theme>;
footer: SxProps<Theme>;
}
const serifStack = 'Georgia, "Iowan Old Style", "Palatino Linotype", serif';
const sansStack = '-apple-system, "Helvetica Neue", Arial, sans-serif';
export const editorialStyles = (
tokens: StatusPageThemeTokens,
_isDark: boolean
): EditorialStyles => {
const heatCellBg: Record<EditorialHeatCell, string> = {
fast: tokens.up,
med: `color-mix(in srgb, ${tokens.up} 60%, ${tokens.bg})`,
slow: tokens.warn,
down: tokens.down,
empty: tokens.border,
};
const barBg: Record<EditorialBarKind, string> = {
up: tokens.up,
down: tokens.down,
empty: tokens.border,
};
const gaugeFillBg: Record<EditorialGaugeFill, string> = {
ok: tokens.up,
warm: tokens.warn,
hot: tokens.down,
};
return {
page: {
flex: "1 0 auto",
maxWidth: 760,
width: "100%",
mx: "auto",
p: "64px 24px 96px",
fontFamily: serifStack,
fontSize: 15,
lineHeight: 1.6,
color: tokens.text,
WebkitFontSmoothing: "antialiased",
},
top: {
display: "flex",
alignItems: "flex-end",
justifyContent: "space-between",
mb: "48px",
borderBottom: `2px solid ${tokens.text}`,
pb: "18px",
},
brandWrap: { display: "flex", flexDirection: "column" },
logoImg: { maxHeight: 48, maxWidth: 160, objectFit: "contain", mb: "8px" },
brandEyebrow: {
m: 0,
fontSize: 11,
textTransform: "uppercase",
letterSpacing: "0.3em",
fontWeight: 700,
color: tokens.textMuted,
fontFamily: sansStack,
},
brandTitle: {
mt: "6px",
mb: 0,
fontSize: 40,
fontWeight: 700,
letterSpacing: "-0.02em",
lineHeight: 1,
color: tokens.text,
},
statusLine: {
fontSize: 22,
fontWeight: 400,
m: 0,
mb: "8px",
letterSpacing: "-0.01em",
lineHeight: 1.35,
color: tokens.text,
fontFamily: serifStack,
},
statusDot: (tone) => ({
display: "inline-block",
width: 10,
height: 10,
borderRadius: "50%",
background: toneColor(tone, tokens),
mr: "10px",
verticalAlign: "middle",
}),
dateline: {
fontFamily: sansStack,
fontSize: 11,
textTransform: "uppercase",
letterSpacing: "0.12em",
color: tokens.textMuted,
m: 0,
mb: "56px",
},
chartSwitchWrap: {
display: "flex",
justifyContent: "flex-end",
mb: "20px",
fontFamily: sansStack,
},
chartSwitch: { display: "inline-flex", border: `1px solid ${tokens.text}` },
chartSwitchButton: (active, isFirst) => ({
fontFamily: "inherit",
fontSize: 10,
textTransform: "uppercase",
letterSpacing: "0.2em",
padding: "6px 14px",
border: 0,
background: active ? tokens.text : "transparent",
color: active ? tokens.bg : tokens.text,
cursor: "pointer",
fontWeight: 700,
...(isFirst ? {} : { borderLeft: `1px solid ${tokens.text}` }),
}),
monitorList: {
listStyle: "none",
m: 0,
p: 0,
display: "flex",
flexDirection: "column",
},
card: {
background: "transparent",
border: 0,
borderBottom: `1px solid ${tokens.border}`,
mb: 0,
py: "20px",
"&:last-of-type": { borderBottom: 0 },
},
cardRow: {
display: "grid",
gridTemplateColumns: "1fr auto",
alignItems: "baseline",
gap: "20px",
},
cardLeft: { minWidth: 0 },
monitorName: {
fontSize: 20,
fontWeight: 700,
letterSpacing: "-0.015em",
color: tokens.text,
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
},
monitorMeta: {
fontFamily: sansStack,
fontSize: 12,
color: tokens.textMuted,
mt: "6px",
display: "flex",
gap: "12px",
flexWrap: "wrap",
alignItems: "center",
},
pill: {
textTransform: "uppercase",
letterSpacing: "0.12em",
fontWeight: 700,
color: tokens.textMuted,
},
pillHardware: {
textTransform: "uppercase",
letterSpacing: "0.12em",
fontWeight: 700,
color: tokens.up,
},
monitorUrl: {
fontFamily: "ui-monospace, Menlo, monospace",
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
maxWidth: 280,
fontSize: 12,
},
badge: (tone) => ({
fontFamily: sansStack,
fontSize: 10,
fontWeight: 700,
padding: "4px 10px",
borderRadius: 0,
textTransform: "uppercase",
letterSpacing: "0.15em",
whiteSpace: "nowrap",
background: toneSoft(tone, tokens),
color: toneColor(tone, tokens),
}),
heatmap: {
mt: "16px",
display: "grid",
gridTemplateColumns: "repeat(25, 1fr)",
gap: "2px",
height: 40,
},
heatmapCell: (kind) => ({
background: heatCellBg[kind],
opacity: kind === "empty" ? 0.5 : 1,
}),
histogram: {
mt: "16px",
display: "grid",
gridTemplateColumns: "repeat(25, 1fr)",
gap: "2px",
alignItems: "flex-end",
height: 40,
},
bar: (kind, heightPct) => ({
background: barBg[kind],
minHeight: 3,
opacity: kind === "empty" ? 0.5 : 1,
height: `${heightPct}%`,
}),
chartStats: {
fontFamily: sansStack,
fontSize: 11,
color: tokens.textMuted,
fontVariantNumeric: "tabular-nums",
textTransform: "uppercase",
letterSpacing: "0.1em",
},
infra: {
display: "grid",
gridTemplateColumns: { xs: "1fr", md: "repeat(3, 1fr)" },
gap: "24px",
mt: "20px",
fontFamily: sansStack,
},
infraEmpty: { mt: "20px", color: tokens.textMuted, fontSize: 13 },
gauge: {},
gaugeLabel: {
fontSize: 10,
color: tokens.textMuted,
textTransform: "uppercase",
letterSpacing: "0.18em",
fontWeight: 700,
},
gaugeValue: {
fontSize: 28,
fontWeight: 700,
letterSpacing: "-0.02em",
mt: "4px",
fontVariantNumeric: "tabular-nums",
fontFamily: serifStack,
color: tokens.text,
},
gaugeBar: { height: 2, background: tokens.border, mt: "10px" },
gaugeFill: (level, widthPct) => ({
display: "block",
height: "100%",
background: gaugeFillBg[level],
transition: "width 0.8s",
width: `${Math.max(0, Math.min(100, widthPct))}%`,
}),
gaugeSub: {
fontSize: 11,
color: tokens.textMuted,
mt: "8px",
fontVariantNumeric: "tabular-nums",
},
footer: {
textAlign: "center",
color: tokens.textMuted,
fontSize: 11,
mt: "56px",
textTransform: "uppercase",
letterSpacing: "0.2em",
fontFamily: sansStack,
"& a": {
color: tokens.text,
textDecoration: "none",
borderBottom: `1px solid ${tokens.text}`,
pb: "2px",
fontWeight: 700,
},
},
};
};
@@ -1,4 +1,4 @@
import { useState } from "react";
import { useMemo, useState } from "react";
import Box from "@mui/material/Box";
import Stack from "@mui/material/Stack";
import { useTranslation } from "react-i18next";
@@ -14,7 +14,8 @@ import {
resolveOverallStatus,
statusBadgeKey,
} from "../shared/overallStatus";
import "./modern.css";
import { useStatusPageTheme } from "../StatusPageThemeProvider";
import { modernStyles } from "./styles";
type StatusPageMonitor = Monitor & { checks?: Monitor["recentChecks"] };
@@ -23,10 +24,10 @@ interface Props {
monitors: StatusPageMonitor[];
}
const PREFIX = "mod";
export const ModernStatusPage = ({ statusPage, monitors }: Props) => {
const { t } = useTranslation();
const { tokens, mode } = useStatusPageTheme();
const styles = useMemo(() => modernStyles(tokens, mode === "dark"), [tokens, mode]);
const [chartMode, setChartMode] = useState<"heatmap" | "histogram">("heatmap");
const overall = resolveOverallStatus(monitors, t, { iconSize: 20 });
@@ -35,72 +36,85 @@ export const ModernStatusPage = ({ statusPage, monitors }: Props) => {
: null;
return (
<Box className={`${PREFIX}-page`}>
<Box sx={styles.page}>
<Box
component="header"
className={`${PREFIX}-top`}
sx={styles.top}
>
<Box className={`${PREFIX}-brand`}>
<Box sx={styles.brand}>
{logoSrc ? (
<Box
component="img"
src={logoSrc}
alt={statusPage.companyName}
className={`${PREFIX}-logo-img`}
sx={styles.logoImg}
/>
) : (
<Box className={`${PREFIX}-logo-grad`}>
{monoFirstChar(statusPage.companyName)}
</Box>
<Box sx={styles.logoGrad}>{monoFirstChar(statusPage.companyName)}</Box>
)}
<span className={`${PREFIX}-company`}>{statusPage.companyName}</span>
<Box
component="span"
sx={styles.company}
>
{statusPage.companyName}
</Box>
</Box>
</Box>
<Box className={`${PREFIX}-hero ${PREFIX}-tone-${overall.tone}`}>
<Box className={`${PREFIX}-pulse`} />
<Box className={`${PREFIX}-status-copy`}>
<h1 className={`${PREFIX}-hero-title`}>{overall.message}</h1>
<p className={`${PREFIX}-hero-sub`}>
<Box sx={styles.hero}>
<Box sx={styles.pulse(overall.tone)} />
<Box sx={styles.statusCopy}>
<Box
component="h1"
sx={styles.heroTitle}
>
{overall.message}
</Box>
<Box
component="p"
sx={styles.heroSub}
>
{t("pages.statusPages.statusBar.monitoringSummary", {
count: monitors.length,
})}
</p>
</Box>
</Box>
<Box className={`${PREFIX}-hero-icon`}>{overall.icon}</Box>
<Box sx={styles.heroIcon(overall.tone)}>{overall.icon}</Box>
</Box>
{statusPage.showCharts && (
<Box className={`${PREFIX}-chart-switch-wrap`}>
<Box sx={styles.chartSwitchWrap}>
<Box
className={`${PREFIX}-chart-switch`}
sx={styles.chartSwitch}
role="radiogroup"
>
<button
<Box
component="button"
type="button"
role="radio"
aria-checked={chartMode === "heatmap"}
className={chartMode === "heatmap" ? `${PREFIX}-active` : ""}
onClick={() => setChartMode("heatmap")}
sx={styles.chartSwitchButton(chartMode === "heatmap")}
>
{t("pages.statusPages.monitorsList.chartTypeHeatmap")}
</button>
<button
</Box>
<Box
component="button"
type="button"
role="radio"
aria-checked={chartMode === "histogram"}
className={chartMode === "histogram" ? `${PREFIX}-active` : ""}
onClick={() => setChartMode("histogram")}
sx={styles.chartSwitchButton(chartMode === "histogram")}
>
{t("pages.statusPages.monitorsList.chartTypeHistogram")}
</button>
</Box>
</Box>
</Box>
)}
<Stack
component="ul"
className={`${PREFIX}-monitor-list`}
sx={styles.monitorList}
>
{monitors.map((monitor) => {
const isHardware = monitor.type === "hardware";
@@ -112,48 +126,65 @@ export const ModernStatusPage = ({ statusPage, monitors }: Props) => {
<Box
component="li"
key={monitor.id}
className={`${PREFIX}-card ${PREFIX}-card-${monitor.status}`}
sx={styles.card}
>
<Box className={`${PREFIX}-card-row`}>
<Box className={`${PREFIX}-card-left`}>
<Box className={`${PREFIX}-monitor-name`}>{monitor.name}</Box>
<Box className={`${PREFIX}-monitor-meta`}>
<span
className={`${PREFIX}-pill ${PREFIX}-pill-${isHardware ? "hardware" : "default"}`}
<Box sx={styles.cardRow}>
<Box sx={styles.cardLeft}>
<Box sx={styles.monitorName}>{monitor.name}</Box>
<Box sx={styles.monitorMeta}>
<Box
component="span"
sx={isHardware ? styles.pillHardware : styles.pill}
>
{getMonitorTypeLabel(monitor.type, t)}
</span>
</Box>
{monitor.url && (
<span
className={`${PREFIX}-monitor-url`}
<Box
component="span"
sx={styles.monitorUrl}
title={monitor.url}
>
{monitor.url}
</span>
</Box>
)}
</Box>
</Box>
<span className={`${PREFIX}-badge ${PREFIX}-badge-${badgeTone}`}>
<Box
component="span"
sx={styles.badge(badgeTone)}
>
{t(statusBadgeKey[monitor.status])}
</span>
</Box>
</Box>
{showInfra && (
<ThemedInfrastructure
monitor={monitor}
classPrefix={PREFIX}
sxApi={{
containerSx: styles.infra,
emptySx: styles.infraEmpty,
gaugeSx: styles.gauge,
gaugeLabelSx: styles.gaugeLabel,
gaugeValueSx: styles.gaugeValue,
gaugeBarSx: styles.gaugeBar,
gaugeFillSx: styles.gaugeFill,
gaugeSubSx: styles.gaugeSub,
}}
/>
)}
{showChart &&
(chartMode === "heatmap" ? (
<ThemedHeatmap
checks={monitor.recentChecks ?? []}
classPrefix={PREFIX}
containerSx={styles.heatmap}
cellSx={styles.heatmapCell}
/>
) : (
<ThemedHistogram
checks={monitor.recentChecks ?? []}
classPrefix={PREFIX}
containerSx={styles.histogram}
barSx={styles.bar}
statsSx={styles.chartStats}
/>
))}
</Box>
@@ -163,7 +194,7 @@ export const ModernStatusPage = ({ statusPage, monitors }: Props) => {
<Box
component="footer"
className={`${PREFIX}-footer`}
sx={styles.footer}
>
{t("pages.statusPages.footer.poweredBy")}{" "}
<a
@@ -1,519 +0,0 @@
/*
* Modern theme — scoped to [data-status-theme="modern"].
* Port of /Users/gorkemcetin/Desktop/checkmate-status-mockups/02-modern.html.
* Signature traits: gradient logo, pulsing status dot, gradient border +
* radial glow on the hero, 16px rounded cards with strong shadow and
* staggered fade-in on mount, gradient-filled chart bars, 4-column
* gauge row with gradient backgrounds.
*/
[data-status-theme="modern"]:not([data-status-transparent]) {
display: flex;
flex-direction: column;
min-height: 100vh;
background: var(--sp-bg);
}
[data-status-theme="modern"] .mod-page {
flex: 1 0 auto;
max-width: 980px;
width: 100%;
margin: 0 auto;
padding: 56px 20px 80px;
font:
14px/1.5 -apple-system,
BlinkMacSystemFont,
"Inter",
"Segoe UI",
Roboto,
sans-serif;
color: var(--sp-text);
-webkit-font-smoothing: antialiased;
}
/* --- header / brand --- */
[data-status-theme="modern"] .mod-top {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 32px;
}
[data-status-theme="modern"] .mod-brand {
display: flex;
align-items: center;
gap: 12px;
font-weight: 700;
font-size: 15px;
letter-spacing: -0.01em;
color: var(--sp-text);
}
[data-status-theme="modern"] .mod-logo-grad {
width: 32px;
height: 32px;
border-radius: 10px;
background: linear-gradient(135deg, var(--sp-up), var(--sp-up-strong));
display: grid;
place-items: center;
color: #fff;
font-weight: 700;
font-size: 14px;
box-shadow: 0 4px 10px color-mix(in srgb, var(--sp-up) 40%, transparent);
}
[data-status-theme="modern"] .mod-logo-img {
max-height: 32px;
max-width: 120px;
object-fit: contain;
}
/* --- hero: clean flat card with pulsing dot (the signature motion) --- */
[data-status-theme="modern"] .mod-hero {
position: relative;
border-radius: 20px;
padding: 28px 32px;
margin-bottom: 24px;
display: flex;
align-items: center;
gap: 20px;
background: var(--sp-surface);
border: 1px solid var(--sp-border);
box-shadow:
0 2px 4px rgba(16, 24, 40, 0.04),
0 12px 32px rgba(16, 24, 40, 0.06);
}
[data-status-theme="modern"][data-status-mode="dark"] .mod-hero {
box-shadow:
0 2px 6px rgba(0, 0, 0, 0.5),
0 20px 40px rgba(0, 0, 0, 0.4);
}
[data-status-theme="modern"] .mod-pulse {
position: relative;
width: 14px;
height: 14px;
border-radius: 50%;
background: var(--sp-up);
flex-shrink: 0;
}
[data-status-theme="modern"] .mod-hero.mod-tone-warn .mod-pulse {
background: var(--sp-warn);
}
[data-status-theme="modern"] .mod-hero.mod-tone-down .mod-pulse {
background: var(--sp-down);
}
[data-status-theme="modern"] .mod-pulse::before,
[data-status-theme="modern"] .mod-pulse::after {
content: "";
position: absolute;
inset: 0;
border-radius: 50%;
background: currentColor;
opacity: 0.5;
animation: mod-pulse 2s ease-out infinite;
}
[data-status-theme="modern"] .mod-pulse::after {
animation-delay: 1s;
}
[data-status-theme="modern"] .mod-pulse {
color: var(--sp-up);
}
[data-status-theme="modern"] .mod-hero.mod-tone-warn .mod-pulse {
color: var(--sp-warn);
}
[data-status-theme="modern"] .mod-hero.mod-tone-down .mod-pulse {
color: var(--sp-down);
}
@keyframes mod-pulse {
0% {
transform: scale(1);
opacity: 0.5;
}
100% {
transform: scale(2.4);
opacity: 0;
}
}
[data-status-theme="modern"] .mod-status-copy {
flex: 1;
min-width: 0;
}
[data-status-theme="modern"] .mod-hero-title {
margin: 0 0 4px;
font-size: 22px;
font-weight: 700;
letter-spacing: -0.02em;
color: var(--sp-text);
}
[data-status-theme="modern"] .mod-hero-sub {
margin: 0;
color: var(--sp-text-muted);
font-size: 13px;
}
[data-status-theme="modern"] .mod-hero-icon {
color: var(--sp-text-muted);
display: flex;
align-items: center;
}
[data-status-theme="modern"] .mod-hero.mod-tone-up .mod-hero-icon {
color: var(--sp-up);
}
[data-status-theme="modern"] .mod-hero.mod-tone-warn .mod-hero-icon {
color: var(--sp-warn);
}
[data-status-theme="modern"] .mod-hero.mod-tone-down .mod-hero-icon {
color: var(--sp-down);
}
/* --- chart toggle (rounded segmented control) --- */
[data-status-theme="modern"] .mod-chart-switch-wrap {
display: flex;
justify-content: flex-end;
margin-bottom: 14px;
}
[data-status-theme="modern"] .mod-chart-switch {
display: inline-flex;
border: 1px solid var(--sp-border);
border-radius: 999px;
background: var(--sp-surface);
padding: 3px;
gap: 2px;
}
[data-status-theme="modern"] .mod-chart-switch > button {
border: 0;
background: transparent;
font: inherit;
font-size: 11px;
padding: 6px 16px;
cursor: pointer;
color: var(--sp-text-muted);
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.08em;
transition:
background 0.2s,
color 0.2s;
border-radius: 999px;
}
[data-status-theme="modern"] .mod-chart-switch > button:hover {
color: var(--sp-text);
}
[data-status-theme="modern"] .mod-chart-switch > button.mod-active {
color: var(--sp-up);
background: var(--sp-up-soft);
}
/* --- cards: 16px radius, strong shadow, staggered fade-in, hover lift --- */
[data-status-theme="modern"] .mod-monitor-list {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 14px;
}
[data-status-theme="modern"] .mod-card {
background: var(--sp-surface);
border: 1px solid var(--sp-border);
border-radius: 16px;
box-shadow:
0 2px 4px rgba(16, 24, 40, 0.04),
0 12px 32px rgba(16, 24, 40, 0.06);
overflow: hidden;
transition:
transform 0.2s cubic-bezier(0.2, 0.8, 0.2, 1),
box-shadow 0.2s;
opacity: 0;
transform: translateY(8px);
animation: mod-fadein 0.5s forwards;
}
[data-status-theme="modern"][data-status-mode="dark"] .mod-card {
box-shadow:
0 2px 6px rgba(0, 0, 0, 0.5),
0 20px 40px rgba(0, 0, 0, 0.4);
}
[data-status-theme="modern"] .mod-card:nth-child(1) {
animation-delay: 0.05s;
}
[data-status-theme="modern"] .mod-card:nth-child(2) {
animation-delay: 0.1s;
}
[data-status-theme="modern"] .mod-card:nth-child(3) {
animation-delay: 0.15s;
}
[data-status-theme="modern"] .mod-card:nth-child(4) {
animation-delay: 0.2s;
}
[data-status-theme="modern"] .mod-card:nth-child(5) {
animation-delay: 0.25s;
}
[data-status-theme="modern"] .mod-card:nth-child(n + 6) {
animation-delay: 0.3s;
}
@keyframes mod-fadein {
to {
opacity: 1;
transform: translateY(0);
}
}
[data-status-theme="modern"] .mod-card:hover {
transform: translateY(-2px);
box-shadow:
0 4px 8px rgba(16, 24, 40, 0.05),
0 16px 40px rgba(16, 24, 40, 0.08);
}
[data-status-theme="modern"] .mod-card-row {
display: grid;
grid-template-columns: 1fr auto;
align-items: center;
gap: 20px;
padding: 18px 24px;
}
[data-status-theme="modern"] .mod-card-left {
min-width: 0;
}
[data-status-theme="modern"] .mod-monitor-name {
font-weight: 600;
font-size: 15px;
letter-spacing: -0.005em;
color: var(--sp-text);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
[data-status-theme="modern"] .mod-monitor-meta {
display: flex;
gap: 10px;
align-items: center;
margin-top: 6px;
flex-wrap: wrap;
}
[data-status-theme="modern"] .mod-pill {
font-size: 10px;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--sp-text-muted);
background: var(--sp-bg);
padding: 3px 9px;
border-radius: 999px;
font-weight: 600;
}
[data-status-theme="modern"] .mod-pill-hardware {
color: #2563eb;
background: rgba(37, 99, 235, 0.1);
}
[data-status-theme="modern"][data-status-mode="dark"] .mod-pill-hardware {
color: #60a5fa;
background: rgba(96, 165, 250, 0.15);
}
[data-status-theme="modern"] .mod-monitor-url {
font-size: 12px;
color: var(--sp-text-muted);
font-family: ui-monospace, Menlo, monospace;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 280px;
}
/* --- badge: with inner dot --- */
[data-status-theme="modern"] .mod-badge {
font-size: 11px;
font-weight: 700;
padding: 5px 12px;
border-radius: 999px;
display: inline-flex;
align-items: center;
gap: 6px;
white-space: nowrap;
}
[data-status-theme="modern"] .mod-badge::before {
content: "";
width: 6px;
height: 6px;
border-radius: 50%;
background: currentColor;
}
[data-status-theme="modern"] .mod-badge-up {
background: var(--sp-up-soft);
color: var(--sp-up);
}
[data-status-theme="modern"] .mod-badge-warn {
background: var(--sp-warn-soft);
color: var(--sp-warn);
}
[data-status-theme="modern"] .mod-badge-down {
background: var(--sp-down-soft);
color: var(--sp-down);
}
/* --- heatmap with gradient cells --- */
[data-status-theme="modern"] .mod-heatmap {
padding: 0 24px 20px;
display: grid;
grid-template-columns: repeat(25, 1fr);
gap: 3px;
height: 46px;
}
[data-status-theme="modern"] .mod-heatmap-cell {
border-radius: 3px;
background: var(--sp-up);
transition: transform 0.15s;
}
[data-status-theme="modern"] .mod-heatmap-cell:hover {
transform: scaleY(1.2);
}
[data-status-theme="modern"] .mod-heatmap-cell.mod-fast {
background: linear-gradient(180deg, var(--sp-up), var(--sp-up-strong));
}
[data-status-theme="modern"] .mod-heatmap-cell.mod-med {
background: linear-gradient(
180deg,
color-mix(in srgb, var(--sp-up) 70%, #ffffff 30%),
var(--sp-up)
);
}
[data-status-theme="modern"] .mod-heatmap-cell.mod-slow {
background: linear-gradient(180deg, var(--sp-warn), var(--sp-degraded));
}
[data-status-theme="modern"] .mod-heatmap-cell.mod-down {
background: linear-gradient(
180deg,
var(--sp-down),
color-mix(in srgb, var(--sp-down) 70%, #000000 30%)
);
}
[data-status-theme="modern"] .mod-heatmap-cell.mod-empty {
background: var(--sp-border);
opacity: 0.4;
}
/* --- histogram with gradient bars --- */
[data-status-theme="modern"] .mod-histogram {
padding: 0 24px;
display: grid;
grid-template-columns: repeat(25, 1fr);
gap: 3px;
align-items: flex-end;
height: 46px;
}
[data-status-theme="modern"] .mod-bar {
background: var(--sp-up);
border-radius: 3px;
min-height: 4px;
transition: transform 0.15s;
}
[data-status-theme="modern"] .mod-bar:hover {
transform: scaleY(1.08);
}
[data-status-theme="modern"] .mod-bar.mod-down {
background: var(--sp-down);
}
[data-status-theme="modern"] .mod-bar.mod-empty {
background: var(--sp-border);
opacity: 0.4;
}
[data-status-theme="modern"] .mod-chart-stats {
padding: 0 24px 20px;
font-size: 11px;
color: var(--sp-text-muted);
font-variant-numeric: tabular-nums;
font-weight: 600;
}
/* --- infra gauges: 3-col (4 if temp present), gradient surfaces --- */
[data-status-theme="modern"] .mod-infra {
padding: 16px 24px 22px;
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 14px;
}
@media (max-width: 720px) {
[data-status-theme="modern"] .mod-infra {
grid-template-columns: 1fr;
}
}
[data-status-theme="modern"] .mod-infra-empty {
padding: 16px 24px 22px;
color: var(--sp-text-muted);
font-size: 13px;
}
[data-status-theme="modern"] .mod-gauge {
border: 1px solid var(--sp-border);
border-radius: 14px;
padding: 14px 16px;
background: linear-gradient(135deg, var(--sp-bg), var(--sp-surface));
}
[data-status-theme="modern"] .mod-gauge-label {
font-size: 10px;
color: var(--sp-text-muted);
text-transform: uppercase;
letter-spacing: 0.1em;
font-weight: 700;
}
[data-status-theme="modern"] .mod-gauge-value {
font-size: 26px;
font-weight: 700;
letter-spacing: -0.02em;
margin-top: 4px;
font-variant-numeric: tabular-nums;
color: var(--sp-text);
}
[data-status-theme="modern"] .mod-gauge-bar {
height: 6px;
background: var(--sp-border);
border-radius: 3px;
overflow: hidden;
margin-top: 10px;
}
[data-status-theme="modern"] .mod-gauge-fill {
display: block;
height: 100%;
background: linear-gradient(90deg, var(--sp-up), var(--sp-up-strong));
border-radius: 3px;
transition: width 0.8s cubic-bezier(0.2, 0.8, 0.2, 1);
}
[data-status-theme="modern"] .mod-gauge-fill.mod-warm {
background: linear-gradient(90deg, var(--sp-warn), var(--sp-degraded));
}
[data-status-theme="modern"] .mod-gauge-fill.mod-hot {
background: linear-gradient(
90deg,
var(--sp-down),
color-mix(in srgb, var(--sp-down) 70%, #000000 30%)
);
}
[data-status-theme="modern"] .mod-gauge-sub {
font-size: 11px;
color: var(--sp-text-muted);
margin-top: 8px;
font-variant-numeric: tabular-nums;
}
/* --- footer --- */
[data-status-theme="modern"] .mod-footer {
text-align: center;
color: var(--sp-text-muted);
font-size: 12px;
margin-top: 48px;
}
[data-status-theme="modern"] .mod-footer a {
color: var(--sp-up);
text-decoration: underline;
text-underline-offset: 3px;
font-weight: 700;
}
[data-status-theme="modern"] .mod-footer a:hover {
color: var(--sp-up-strong);
}
/* Reduced motion */
@media (prefers-reduced-motion: reduce) {
[data-status-theme="modern"] .mod-card {
animation: none;
opacity: 1;
transform: none;
}
[data-status-theme="modern"] .mod-pulse::before,
[data-status-theme="modern"] .mod-pulse::after {
animation: none;
}
}
@@ -0,0 +1,431 @@
import type { SxProps, Theme } from "@mui/material/styles";
import { keyframes } from "@mui/system";
import type { StatusPageThemeTokens } from "../tokens";
import { type OverallTone, toneColor, toneSoft } from "../shared/overallStatus";
export type ModernHeatCell = "fast" | "med" | "slow" | "down" | "empty";
export type ModernBarKind = "up" | "down" | "empty";
export type ModernGaugeFill = "ok" | "warm" | "hot";
export interface ModernStyles {
page: SxProps<Theme>;
top: SxProps<Theme>;
brand: SxProps<Theme>;
logoGrad: SxProps<Theme>;
logoImg: SxProps<Theme>;
company: SxProps<Theme>;
hero: SxProps<Theme>;
pulse: (tone: OverallTone) => SxProps<Theme>;
statusCopy: SxProps<Theme>;
heroTitle: SxProps<Theme>;
heroSub: SxProps<Theme>;
heroIcon: (tone: OverallTone) => SxProps<Theme>;
chartSwitchWrap: SxProps<Theme>;
chartSwitch: SxProps<Theme>;
chartSwitchButton: (active: boolean) => SxProps<Theme>;
monitorList: SxProps<Theme>;
card: SxProps<Theme>;
cardRow: SxProps<Theme>;
cardLeft: SxProps<Theme>;
monitorName: SxProps<Theme>;
monitorMeta: SxProps<Theme>;
pill: SxProps<Theme>;
pillHardware: SxProps<Theme>;
monitorUrl: SxProps<Theme>;
badge: (tone: OverallTone) => SxProps<Theme>;
heatmap: SxProps<Theme>;
heatmapCell: (kind: ModernHeatCell) => SxProps<Theme>;
histogram: SxProps<Theme>;
bar: (kind: ModernBarKind, heightPct: number) => SxProps<Theme>;
chartStats: SxProps<Theme>;
infra: SxProps<Theme>;
infraEmpty: SxProps<Theme>;
gauge: SxProps<Theme>;
gaugeLabel: SxProps<Theme>;
gaugeValue: SxProps<Theme>;
gaugeBar: SxProps<Theme>;
gaugeFill: (level: ModernGaugeFill, widthPct: number) => SxProps<Theme>;
gaugeSub: SxProps<Theme>;
footer: SxProps<Theme>;
}
const sansFontStack =
'-apple-system, BlinkMacSystemFont, "Inter", "Segoe UI", Roboto, sans-serif';
const lightShadow =
"0 2px 4px rgba(16, 24, 40, 0.04), 0 12px 32px rgba(16, 24, 40, 0.06)";
const darkShadow = "0 2px 6px rgba(0, 0, 0, 0.5), 0 20px 40px rgba(0, 0, 0, 0.4)";
const lightShadowHover =
"0 4px 8px rgba(16, 24, 40, 0.05), 0 16px 40px rgba(16, 24, 40, 0.08)";
const pulseKeyframes = keyframes`
0% { transform: scale(1); opacity: 0.5; }
100% { transform: scale(2.4); opacity: 0; }
`;
const fadeInKeyframes = keyframes`
to { opacity: 1; transform: translateY(0); }
`;
export const modernStyles = (
tokens: StatusPageThemeTokens,
isDark: boolean
): ModernStyles => {
const cardShadow = isDark ? darkShadow : lightShadow;
const heatCellBg: Record<ModernHeatCell, string> = {
fast: `linear-gradient(180deg, ${tokens.up}, ${tokens.upStrong})`,
med: `linear-gradient(180deg, color-mix(in srgb, ${tokens.up} 70%, #ffffff 30%), ${tokens.up})`,
slow: `linear-gradient(180deg, ${tokens.warn}, ${tokens.degraded})`,
down: `linear-gradient(180deg, ${tokens.down}, color-mix(in srgb, ${tokens.down} 70%, #000000 30%))`,
empty: tokens.border,
};
const barBg: Record<ModernBarKind, string> = {
up: tokens.up,
down: tokens.down,
empty: tokens.border,
};
const gaugeFillBg: Record<ModernGaugeFill, string> = {
ok: `linear-gradient(90deg, ${tokens.up}, ${tokens.upStrong})`,
warm: `linear-gradient(90deg, ${tokens.warn}, ${tokens.degraded})`,
hot: `linear-gradient(90deg, ${tokens.down}, color-mix(in srgb, ${tokens.down} 70%, #000000 30%))`,
};
return {
page: {
flex: "1 0 auto",
maxWidth: 980,
width: "100%",
mx: "auto",
p: "56px 20px 80px",
fontFamily: sansFontStack,
fontSize: 14,
lineHeight: 1.5,
color: tokens.text,
WebkitFontSmoothing: "antialiased",
},
top: {
display: "flex",
alignItems: "center",
justifyContent: "space-between",
mb: "32px",
},
brand: {
display: "flex",
alignItems: "center",
gap: "12px",
fontWeight: 700,
fontSize: 15,
letterSpacing: "-0.01em",
color: tokens.text,
},
logoGrad: {
width: 32,
height: 32,
borderRadius: "10px",
background: `linear-gradient(135deg, ${tokens.up}, ${tokens.upStrong})`,
display: "grid",
placeItems: "center",
color: "#fff",
fontWeight: 700,
fontSize: 14,
boxShadow: `0 4px 10px color-mix(in srgb, ${tokens.up} 40%, transparent)`,
},
logoImg: { maxHeight: 32, maxWidth: 120, objectFit: "contain" },
company: {},
hero: {
position: "relative",
borderRadius: "20px",
padding: "28px 32px",
mb: "24px",
display: "flex",
alignItems: "center",
gap: "20px",
background: tokens.surface,
border: `1px solid ${tokens.border}`,
boxShadow: cardShadow,
},
pulse: (tone) => {
const c = toneColor(tone, tokens);
return {
position: "relative",
width: 14,
height: 14,
borderRadius: "50%",
background: c,
color: c,
flexShrink: 0,
"&::before, &::after": {
content: '""',
position: "absolute",
inset: 0,
borderRadius: "50%",
background: "currentColor",
opacity: 0.5,
animation: `${pulseKeyframes} 2s ease-out infinite`,
},
"&::after": { animationDelay: "1s" },
"@media (prefers-reduced-motion: reduce)": {
"&::before, &::after": { animation: "none" },
},
};
},
statusCopy: { flex: 1, minWidth: 0 },
heroTitle: {
m: 0,
mb: "4px",
fontSize: 22,
fontWeight: 700,
letterSpacing: "-0.02em",
color: tokens.text,
},
heroSub: { m: 0, color: tokens.textMuted, fontSize: 13 },
heroIcon: (tone) => ({
color: toneColor(tone, tokens),
display: "flex",
alignItems: "center",
}),
chartSwitchWrap: { display: "flex", justifyContent: "flex-end", mb: "14px" },
chartSwitch: {
display: "inline-flex",
border: `1px solid ${tokens.border}`,
borderRadius: "999px",
background: tokens.surface,
p: "3px",
gap: "2px",
},
chartSwitchButton: (active) => ({
border: 0,
background: active ? tokens.upSoft : "transparent",
fontFamily: "inherit",
fontSize: 11,
padding: "6px 16px",
cursor: "pointer",
color: active ? tokens.up : tokens.textMuted,
fontWeight: 600,
textTransform: "uppercase",
letterSpacing: "0.08em",
transition: "background 0.2s, color 0.2s",
borderRadius: "999px",
"&:hover": { color: active ? tokens.up : tokens.text },
}),
monitorList: {
listStyle: "none",
m: 0,
p: 0,
display: "flex",
flexDirection: "column",
gap: "14px",
},
card: {
background: tokens.surface,
border: `1px solid ${tokens.border}`,
borderRadius: "16px",
boxShadow: cardShadow,
overflow: "hidden",
transition: "transform 0.2s cubic-bezier(0.2, 0.8, 0.2, 1), box-shadow 0.2s",
opacity: 0,
transform: "translateY(8px)",
animation: `${fadeInKeyframes} 0.5s forwards`,
"&:nth-of-type(1)": { animationDelay: "0.05s" },
"&:nth-of-type(2)": { animationDelay: "0.1s" },
"&:nth-of-type(3)": { animationDelay: "0.15s" },
"&:nth-of-type(4)": { animationDelay: "0.2s" },
"&:nth-of-type(5)": { animationDelay: "0.25s" },
"&:nth-of-type(n+6)": { animationDelay: "0.3s" },
"&:hover": { transform: "translateY(-2px)", boxShadow: lightShadowHover },
"@media (prefers-reduced-motion: reduce)": {
animation: "none",
opacity: 1,
transform: "none",
},
},
cardRow: {
display: "grid",
gridTemplateColumns: "1fr auto",
alignItems: "center",
gap: "20px",
p: "18px 24px",
},
cardLeft: { minWidth: 0 },
monitorName: {
fontWeight: 600,
fontSize: 15,
letterSpacing: "-0.005em",
color: tokens.text,
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
},
monitorMeta: {
display: "flex",
gap: "10px",
alignItems: "center",
mt: "6px",
flexWrap: "wrap",
},
pill: {
fontSize: 10,
textTransform: "uppercase",
letterSpacing: "0.08em",
color: tokens.textMuted,
background: tokens.bg,
padding: "3px 9px",
borderRadius: "999px",
fontWeight: 600,
},
pillHardware: {
fontSize: 10,
textTransform: "uppercase",
letterSpacing: "0.08em",
padding: "3px 9px",
borderRadius: "999px",
fontWeight: 600,
color: isDark ? "#60a5fa" : "#2563eb",
background: isDark ? "rgba(96, 165, 250, 0.15)" : "rgba(37, 99, 235, 0.1)",
},
monitorUrl: {
fontSize: 12,
color: tokens.textMuted,
fontFamily: "ui-monospace, Menlo, monospace",
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
maxWidth: 280,
},
badge: (tone) => ({
fontSize: 11,
fontWeight: 700,
padding: "5px 12px",
borderRadius: "999px",
display: "inline-flex",
alignItems: "center",
gap: "6px",
whiteSpace: "nowrap",
background: toneSoft(tone, tokens),
color: toneColor(tone, tokens),
"&::before": {
content: '""',
width: 6,
height: 6,
borderRadius: "50%",
background: "currentColor",
},
}),
heatmap: {
padding: "0 24px 20px",
display: "grid",
gridTemplateColumns: "repeat(25, 1fr)",
gap: "3px",
height: 46,
},
heatmapCell: (kind) => ({
borderRadius: "3px",
background: heatCellBg[kind],
opacity: kind === "empty" ? 0.4 : 1,
transition: "transform 0.15s",
"&:hover": { transform: "scaleY(1.2)" },
}),
histogram: {
padding: "0 24px",
display: "grid",
gridTemplateColumns: "repeat(25, 1fr)",
gap: "3px",
alignItems: "flex-end",
height: 46,
},
bar: (kind, heightPct) => ({
background: barBg[kind],
borderRadius: "3px",
minHeight: 4,
opacity: kind === "empty" ? 0.4 : 1,
height: `${heightPct}%`,
transition: "transform 0.15s",
"&:hover": { transform: "scaleY(1.08)" },
}),
chartStats: {
padding: "0 24px 20px",
fontSize: 11,
color: tokens.textMuted,
fontVariantNumeric: "tabular-nums",
fontWeight: 600,
},
infra: {
padding: "16px 24px 22px",
display: "grid",
gridTemplateColumns: { xs: "1fr", md: "repeat(3, 1fr)" },
gap: "14px",
},
infraEmpty: {
padding: "16px 24px 22px",
color: tokens.textMuted,
fontSize: 13,
},
gauge: {
border: `1px solid ${tokens.border}`,
borderRadius: "14px",
p: "14px 16px",
background: `linear-gradient(135deg, ${tokens.bg}, ${tokens.surface})`,
},
gaugeLabel: {
fontSize: 10,
color: tokens.textMuted,
textTransform: "uppercase",
letterSpacing: "0.1em",
fontWeight: 700,
},
gaugeValue: {
fontSize: 26,
fontWeight: 700,
letterSpacing: "-0.02em",
mt: "4px",
fontVariantNumeric: "tabular-nums",
color: tokens.text,
},
gaugeBar: {
height: 6,
background: tokens.border,
borderRadius: "3px",
overflow: "hidden",
mt: "10px",
},
gaugeFill: (level, widthPct) => ({
display: "block",
height: "100%",
background: gaugeFillBg[level],
borderRadius: "3px",
transition: "width 0.8s cubic-bezier(0.2, 0.8, 0.2, 1)",
width: `${Math.max(0, Math.min(100, widthPct))}%`,
}),
gaugeSub: {
fontSize: 11,
color: tokens.textMuted,
mt: "8px",
fontVariantNumeric: "tabular-nums",
},
footer: {
textAlign: "center",
color: tokens.textMuted,
fontSize: 12,
mt: "48px",
"& a": {
color: tokens.up,
textDecoration: "underline",
textUnderlineOffset: "3px",
fontWeight: 700,
"&:hover": { color: tokens.upStrong },
},
},
};
};
@@ -13,11 +13,8 @@ export type HeatCellKind = "fast" | "med" | "slow" | "down" | "empty";
interface Props {
checks: CheckSnapshot[];
/** sx-based API — preferred. Themes pass their own container + cell recipes. */
containerSx?: SxProps<Theme>;
cellSx?: (kind: HeatCellKind) => SxProps<Theme>;
/** Legacy className-based API — kept for unmigrated themes. Will be removed once all themes move to sx. */
classPrefix?: string;
containerSx: SxProps<Theme>;
cellSx: (kind: HeatCellKind) => SxProps<Theme>;
}
const classify = (check: CheckSnapshot): Exclude<HeatCellKind, "empty"> => {
@@ -28,9 +25,8 @@ const classify = (check: CheckSnapshot): Exclude<HeatCellKind, "empty"> => {
return "fast";
};
export const ThemedHeatmap = ({ checks, containerSx, cellSx, classPrefix }: Props) => {
export const ThemedHeatmap = ({ checks, containerSx, cellSx }: Props) => {
const { t } = useTranslation();
const useSxApi = Boolean(containerSx && cellSx);
const source = checks.slice(0, CELLS).reverse();
const padded: (CheckSnapshot | null)[] = [
@@ -38,18 +34,9 @@ export const ThemedHeatmap = ({ checks, containerSx, cellSx, classPrefix }: Prop
...source,
];
const containerProps = useSxApi
? { sx: containerSx }
: { className: `${classPrefix}-heatmap` };
const cellProps = (kind: HeatCellKind) =>
useSxApi
? { sx: cellSx!(kind) }
: { className: `${classPrefix}-heatmap-cell ${classPrefix}-${kind}` };
return (
<Box
{...containerProps}
sx={containerSx}
role="img"
aria-label={t("pages.statusPages.monitorsList.chart.heatmapAria")}
>
@@ -58,7 +45,7 @@ export const ThemedHeatmap = ({ checks, containerSx, cellSx, classPrefix }: Prop
return (
<Box
key={`empty-${i}`}
{...cellProps("empty")}
sx={cellSx("empty")}
/>
);
}
@@ -89,7 +76,7 @@ export const ThemedHeatmap = ({ checks, containerSx, cellSx, classPrefix }: Prop
placement="top"
>
<Box
{...cellProps(kind)}
sx={cellSx(kind)}
aria-label={`${check.responseTime} ms, ${check.status ? "up" : "down"}`}
/>
</Tooltip>
@@ -15,12 +15,9 @@ export type BarKind = "up" | "down" | "empty";
interface Props {
checks: CheckSnapshot[];
/** sx-based API — preferred. */
containerSx?: SxProps<Theme>;
barSx?: (kind: BarKind, heightPct: number) => SxProps<Theme>;
statsSx?: SxProps<Theme>;
/** Legacy className-based API. Will be removed once all themes move to sx. */
classPrefix?: string;
containerSx: SxProps<Theme>;
barSx: (kind: BarKind, heightPct: number) => SxProps<Theme>;
statsSx: SxProps<Theme>;
statsGap?: number;
}
@@ -32,11 +29,9 @@ export const ThemedHistogram = ({
containerSx,
barSx,
statsSx,
classPrefix,
statsGap = 1,
}: Props) => {
const { t } = useTranslation();
const useSxApi = Boolean(containerSx && barSx && statsSx);
const { padded, max, avg, peak } = useMemo(() => {
const source = checks.slice(0, CELLS).reverse();
@@ -52,29 +47,15 @@ export const ThemedHistogram = ({
return { padded: out, max: maxRt, avg: avgRt, peak: valid.length ? maxRt : 0 };
}, [checks]);
const containerProps = useSxApi
? { sx: containerSx }
: { className: `${classPrefix}-histogram` };
const statsProps = useSxApi
? { sx: statsSx }
: { className: `${classPrefix}-chart-stats` };
return (
<Stack gap={statsGap}>
<Box {...containerProps}>
<Box sx={containerSx}>
{padded.map((check, i) => {
if (!check) {
return useSxApi ? (
return (
<Box
key={`empty-${i}`}
sx={barSx!("empty", MIN_HEIGHT_PCT)}
/>
) : (
<Box
key={`empty-${i}`}
className={`${classPrefix}-bar ${classPrefix}-empty`}
style={{ height: `${MIN_HEIGHT_PCT}%` }}
sx={barSx("empty", MIN_HEIGHT_PCT)}
/>
);
}
@@ -107,18 +88,10 @@ export const ThemedHistogram = ({
arrow
placement="top"
>
{useSxApi ? (
<Box
sx={barSx!(tone(check), height)}
aria-label={`${check.responseTime} ms`}
/>
) : (
<Box
className={`${classPrefix}-bar ${classPrefix}-${tone(check)}`}
style={{ height: `${height}%` }}
aria-label={`${check.responseTime} ms`}
/>
)}
<Box
sx={barSx(tone(check), height)}
aria-label={`${check.responseTime} ms`}
/>
</Tooltip>
);
})}
@@ -126,7 +99,7 @@ export const ThemedHistogram = ({
<Stack
direction="row"
justifyContent="space-between"
{...statsProps}
sx={statsSx}
>
<span>{t("pages.statusPages.monitorsList.chart.avg", { value: avg })}</span>
<span>{t("pages.statusPages.monitorsList.chart.max", { value: peak })}</span>
@@ -6,7 +6,7 @@ import type { Monitor } from "@/Types/Monitor";
export type GaugeFillLevel = "ok" | "warm" | "hot";
interface InfraSx {
export interface InfraSx {
containerSx: SxProps<Theme>;
emptySx: SxProps<Theme>;
gaugeSx: SxProps<Theme>;
@@ -19,10 +19,7 @@ interface InfraSx {
interface Props {
monitor: Monitor & { checks?: Monitor["recentChecks"] };
/** sx-based API — preferred. */
sxApi?: InfraSx;
/** Legacy className-based API. Will be removed once all themes move to sx. */
classPrefix?: string;
sxApi: InfraSx;
}
interface Gauge {
@@ -37,19 +34,13 @@ const PCT = 100;
const heatLevel = (value: number): GaugeFillLevel =>
value > 85 ? "hot" : value > 70 ? "warm" : "ok";
export const ThemedInfrastructure = ({ monitor, sxApi, classPrefix }: Props) => {
export const ThemedInfrastructure = ({ monitor, sxApi }: Props) => {
const { t } = useTranslation();
const useSxApi = Boolean(sxApi);
const latest = monitor.recentChecks?.[0] ?? monitor.checks?.[0];
const renderEmpty = () => {
const message = t("pages.statusPages.monitorsList.noData");
return useSxApi ? (
<Box sx={sxApi!.emptySx}>{message}</Box>
) : (
<Box className={`${classPrefix}-infra-empty`}>{message}</Box>
);
};
const renderEmpty = () => (
<Box sx={sxApi.emptySx}>{t("pages.statusPages.monitorsList.noData")}</Box>
);
if (!latest) return renderEmpty();
@@ -94,49 +85,21 @@ export const ThemedInfrastructure = ({ monitor, sxApi, classPrefix }: Props) =>
if (gauges.length === 0) return renderEmpty();
if (useSxApi) {
const s = sxApi!;
return (
<Box sx={s.containerSx}>
{gauges.map((g) => (
<Box
key={g.key}
sx={s.gaugeSx}
>
<Box sx={s.gaugeLabelSx}>{g.label}</Box>
<Box sx={s.gaugeValueSx}>{g.value.toFixed(0)}%</Box>
<Box sx={s.gaugeBarSx}>
<Box sx={s.gaugeFillSx(heatLevel(g.value), Math.min(100, g.value))} />
</Box>
<Box sx={s.gaugeSubSx}>{g.sub}</Box>
</Box>
))}
</Box>
);
}
return (
<Box className={`${classPrefix}-infra`}>
{gauges.map((g) => {
const level = heatLevel(g.value);
const heatClass = level === "ok" ? "" : `${classPrefix}-${level}`;
return (
<Box
key={g.key}
className={`${classPrefix}-gauge`}
>
<Box className={`${classPrefix}-gauge-label`}>{g.label}</Box>
<Box className={`${classPrefix}-gauge-value`}>{g.value.toFixed(0)}%</Box>
<Box className={`${classPrefix}-gauge-bar`}>
<Box
className={`${classPrefix}-gauge-fill ${heatClass}`}
style={{ width: `${Math.min(100, g.value)}%` }}
/>
</Box>
<Box className={`${classPrefix}-gauge-sub`}>{g.sub}</Box>
<Box sx={sxApi.containerSx}>
{gauges.map((g) => (
<Box
key={g.key}
sx={sxApi.gaugeSx}
>
<Box sx={sxApi.gaugeLabelSx}>{g.label}</Box>
<Box sx={sxApi.gaugeValueSx}>{g.value.toFixed(0)}%</Box>
<Box sx={sxApi.gaugeBarSx}>
<Box sx={sxApi.gaugeFillSx(heatLevel(g.value), Math.min(100, g.value))} />
</Box>
);
})}
<Box sx={sxApi.gaugeSubSx}>{g.sub}</Box>
</Box>
))}
</Box>
);
};