From c6c50063d848914b2a7bd0831e65d0f6def2dd3e Mon Sep 17 00:00:00 2001 From: gorkem-bwl Date: Mon, 27 Apr 2026 18:39:42 -0400 Subject: [PATCH] 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. --- .../Status/themes/bold/BoldStatusPage.tsx | 124 +++-- .../StatusPage/Status/themes/bold/bold.css | 497 ----------------- .../StatusPage/Status/themes/bold/styles.ts | 408 ++++++++++++++ .../themes/editorial/EditorialStatusPage.tsx | 121 ++-- .../Status/themes/editorial/editorial.css | 353 ------------ .../Status/themes/editorial/styles.ts | 335 +++++++++++ .../Status/themes/modern/ModernStatusPage.tsx | 121 ++-- .../Status/themes/modern/modern.css | 519 ------------------ .../StatusPage/Status/themes/modern/styles.ts | 431 +++++++++++++++ .../Status/themes/shared/ThemedHeatmap.tsx | 25 +- .../Status/themes/shared/ThemedHistogram.tsx | 49 +- .../themes/shared/ThemedInfrastructure.tsx | 75 +-- 12 files changed, 1442 insertions(+), 1616 deletions(-) delete mode 100644 client/src/Pages/StatusPage/Status/themes/bold/bold.css create mode 100644 client/src/Pages/StatusPage/Status/themes/bold/styles.ts delete mode 100644 client/src/Pages/StatusPage/Status/themes/editorial/editorial.css create mode 100644 client/src/Pages/StatusPage/Status/themes/editorial/styles.ts delete mode 100644 client/src/Pages/StatusPage/Status/themes/modern/modern.css create mode 100644 client/src/Pages/StatusPage/Status/themes/modern/styles.ts diff --git a/client/src/Pages/StatusPage/Status/themes/bold/BoldStatusPage.tsx b/client/src/Pages/StatusPage/Status/themes/bold/BoldStatusPage.tsx index ad777dbff..feb17d917 100644 --- a/client/src/Pages/StatusPage/Status/themes/bold/BoldStatusPage.tsx +++ b/client/src/Pages/StatusPage/Status/themes/bold/BoldStatusPage.tsx @@ -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 ( - + - + {logoSrc ? ( ) : ( - - {monoFirstChar(statusPage.companyName)} - + {monoFirstChar(statusPage.companyName)} )} - {statusPage.companyName} + + {statusPage.companyName} + - -

- {overall.icon} + + + + {overall.icon} + {overall.message} -

-

+ + {t("pages.statusPages.statusBar.monitoringSummary", { count: monitors.length, })} -

+
{statusPage.showCharts && ( - + - - + )} {monitors.map((monitor) => { const isHardware = monitor.type === "hardware"; @@ -113,48 +128,65 @@ export const BoldStatusPage = ({ statusPage, monitors }: Props) => { - - - {monitor.name} - - + + {monitor.name} + + {getMonitorTypeLabel(monitor.type, t)} - + {monitor.url && ( - {monitor.url} - + )} - + {t(statusBadgeKey[monitor.status])} - +
{showInfra && ( )} {showChart && (chartMode === "heatmap" ? ( ) : ( ))}
@@ -164,7 +196,7 @@ export const BoldStatusPage = ({ statusPage, monitors }: Props) => { {t("pages.statusPages.footer.poweredBy")}{" "} * { - 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; -} diff --git a/client/src/Pages/StatusPage/Status/themes/bold/styles.ts b/client/src/Pages/StatusPage/Status/themes/bold/styles.ts new file mode 100644 index 000000000..50f8c9cf0 --- /dev/null +++ b/client/src/Pages/StatusPage/Status/themes/bold/styles.ts @@ -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; + top: SxProps; + brand: SxProps; + logoConic: SxProps; + logoImg: SxProps; + company: SxProps; + hero: SxProps; + heroTitle: SxProps; + heroCheck: (tone: OverallTone) => SxProps; + heroSub: SxProps; + chartSwitchWrap: SxProps; + chartSwitch: SxProps; + chartSwitchButton: (active: boolean) => SxProps; + monitorList: SxProps; + card: SxProps; + cardRow: SxProps; + cardLeft: SxProps; + monitorName: SxProps; + monitorMeta: SxProps; + pill: SxProps; + pillHardware: SxProps; + monitorUrl: SxProps; + badge: (tone: OverallTone) => SxProps; + heatmap: SxProps; + heatmapCell: (kind: BoldHeatCell) => SxProps; + histogram: SxProps; + bar: (kind: BoldBarKind, heightPct: number) => SxProps; + chartStats: SxProps; + infra: SxProps; + infraEmpty: SxProps; + gauge: SxProps; + gaugeLabel: SxProps; + gaugeValue: SxProps; + gaugeBar: SxProps; + gaugeFill: (level: BoldGaugeFill, widthPct: number) => SxProps; + gaugeSub: SxProps; + footer: SxProps; +} + +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 = { + 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 = { + 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 = { + up: tokens.up, + down: tokens.down, + empty: tokens.border, + }; + + const gaugeFillBg: Record = { + 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, + }, + }, + }; +}; diff --git a/client/src/Pages/StatusPage/Status/themes/editorial/EditorialStatusPage.tsx b/client/src/Pages/StatusPage/Status/themes/editorial/EditorialStatusPage.tsx index 306ae2bc0..e6c57a5bf 100644 --- a/client/src/Pages/StatusPage/Status/themes/editorial/EditorialStatusPage.tsx +++ b/client/src/Pages/StatusPage/Status/themes/editorial/EditorialStatusPage.tsx @@ -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 ( - + - + {logoSrc && ( )} -

{statusPage.companyName}

-

+ + {statusPage.companyName} + + {t("pages.statusPages.editorial.reportTitle")} -

+
-

- + + {overall.message} -

-

+ + {todayLabel} ·{" "} {t("pages.statusPages.statusBar.monitoringSummary", { count: monitors.length, })} -

+
{statusPage.showCharts && ( - + - - + )} {monitors.map((monitor) => { const isHardware = monitor.type === "hardware"; @@ -122,48 +140,65 @@ export const EditorialStatusPage = ({ statusPage, monitors }: Props) => { - - - {monitor.name} - - + + {monitor.name} + + {getMonitorTypeLabel(monitor.type, t)} - + {monitor.url && ( - {monitor.url} - + )} - + {t(statusBadgeKey[monitor.status])} - +
{showInfra && ( )} {showChart && (chartMode === "heatmap" ? ( ) : ( ))}
@@ -173,7 +208,7 @@ export const EditorialStatusPage = ({ statusPage, monitors }: Props) => { {t("pages.statusPages.footer.poweredBy")}{" "} 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; -} diff --git a/client/src/Pages/StatusPage/Status/themes/editorial/styles.ts b/client/src/Pages/StatusPage/Status/themes/editorial/styles.ts new file mode 100644 index 000000000..c3948577e --- /dev/null +++ b/client/src/Pages/StatusPage/Status/themes/editorial/styles.ts @@ -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; + top: SxProps; + brandWrap: SxProps; + logoImg: SxProps; + brandEyebrow: SxProps; + brandTitle: SxProps; + statusLine: SxProps; + statusDot: (tone: OverallTone) => SxProps; + dateline: SxProps; + chartSwitchWrap: SxProps; + chartSwitch: SxProps; + chartSwitchButton: (active: boolean, isFirst: boolean) => SxProps; + monitorList: SxProps; + card: SxProps; + cardRow: SxProps; + cardLeft: SxProps; + monitorName: SxProps; + monitorMeta: SxProps; + pill: SxProps; + pillHardware: SxProps; + monitorUrl: SxProps; + badge: (tone: OverallTone) => SxProps; + heatmap: SxProps; + heatmapCell: (kind: EditorialHeatCell) => SxProps; + histogram: SxProps; + bar: (kind: EditorialBarKind, heightPct: number) => SxProps; + chartStats: SxProps; + infra: SxProps; + infraEmpty: SxProps; + gauge: SxProps; + gaugeLabel: SxProps; + gaugeValue: SxProps; + gaugeBar: SxProps; + gaugeFill: (level: EditorialGaugeFill, widthPct: number) => SxProps; + gaugeSub: SxProps; + footer: SxProps; +} + +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 = { + 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 = { + up: tokens.up, + down: tokens.down, + empty: tokens.border, + }; + + const gaugeFillBg: Record = { + 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, + }, + }, + }; +}; diff --git a/client/src/Pages/StatusPage/Status/themes/modern/ModernStatusPage.tsx b/client/src/Pages/StatusPage/Status/themes/modern/ModernStatusPage.tsx index f853d90d6..6d2046c0f 100644 --- a/client/src/Pages/StatusPage/Status/themes/modern/ModernStatusPage.tsx +++ b/client/src/Pages/StatusPage/Status/themes/modern/ModernStatusPage.tsx @@ -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 ( - + - + {logoSrc ? ( ) : ( - - {monoFirstChar(statusPage.companyName)} - + {monoFirstChar(statusPage.companyName)} )} - {statusPage.companyName} + + {statusPage.companyName} + - - - -

{overall.message}

-

+ + + + + {overall.message} + + {t("pages.statusPages.statusBar.monitoringSummary", { count: monitors.length, })} -

+
- {overall.icon} + {overall.icon}
{statusPage.showCharts && ( - + - - + )} {monitors.map((monitor) => { const isHardware = monitor.type === "hardware"; @@ -112,48 +126,65 @@ export const ModernStatusPage = ({ statusPage, monitors }: Props) => { - - - {monitor.name} - - + + {monitor.name} + + {getMonitorTypeLabel(monitor.type, t)} - + {monitor.url && ( - {monitor.url} - + )} - + {t(statusBadgeKey[monitor.status])} - +
{showInfra && ( )} {showChart && (chartMode === "heatmap" ? ( ) : ( ))}
@@ -163,7 +194,7 @@ export const ModernStatusPage = ({ statusPage, monitors }: Props) => { {t("pages.statusPages.footer.poweredBy")}{" "}
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; - } -} diff --git a/client/src/Pages/StatusPage/Status/themes/modern/styles.ts b/client/src/Pages/StatusPage/Status/themes/modern/styles.ts new file mode 100644 index 000000000..915b26b6a --- /dev/null +++ b/client/src/Pages/StatusPage/Status/themes/modern/styles.ts @@ -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; + top: SxProps; + brand: SxProps; + logoGrad: SxProps; + logoImg: SxProps; + company: SxProps; + hero: SxProps; + pulse: (tone: OverallTone) => SxProps; + statusCopy: SxProps; + heroTitle: SxProps; + heroSub: SxProps; + heroIcon: (tone: OverallTone) => SxProps; + chartSwitchWrap: SxProps; + chartSwitch: SxProps; + chartSwitchButton: (active: boolean) => SxProps; + monitorList: SxProps; + card: SxProps; + cardRow: SxProps; + cardLeft: SxProps; + monitorName: SxProps; + monitorMeta: SxProps; + pill: SxProps; + pillHardware: SxProps; + monitorUrl: SxProps; + badge: (tone: OverallTone) => SxProps; + heatmap: SxProps; + heatmapCell: (kind: ModernHeatCell) => SxProps; + histogram: SxProps; + bar: (kind: ModernBarKind, heightPct: number) => SxProps; + chartStats: SxProps; + infra: SxProps; + infraEmpty: SxProps; + gauge: SxProps; + gaugeLabel: SxProps; + gaugeValue: SxProps; + gaugeBar: SxProps; + gaugeFill: (level: ModernGaugeFill, widthPct: number) => SxProps; + gaugeSub: SxProps; + footer: SxProps; +} + +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 = { + 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 = { + up: tokens.up, + down: tokens.down, + empty: tokens.border, + }; + + const gaugeFillBg: Record = { + 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 }, + }, + }, + }; +}; diff --git a/client/src/Pages/StatusPage/Status/themes/shared/ThemedHeatmap.tsx b/client/src/Pages/StatusPage/Status/themes/shared/ThemedHeatmap.tsx index ebd275ddc..30510611b 100644 --- a/client/src/Pages/StatusPage/Status/themes/shared/ThemedHeatmap.tsx +++ b/client/src/Pages/StatusPage/Status/themes/shared/ThemedHeatmap.tsx @@ -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; - cellSx?: (kind: HeatCellKind) => SxProps; - /** Legacy className-based API — kept for unmigrated themes. Will be removed once all themes move to sx. */ - classPrefix?: string; + containerSx: SxProps; + cellSx: (kind: HeatCellKind) => SxProps; } const classify = (check: CheckSnapshot): Exclude => { @@ -28,9 +25,8 @@ const classify = (check: CheckSnapshot): Exclude => { 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 ( @@ -58,7 +45,7 @@ export const ThemedHeatmap = ({ checks, containerSx, cellSx, classPrefix }: Prop return ( ); } @@ -89,7 +76,7 @@ export const ThemedHeatmap = ({ checks, containerSx, cellSx, classPrefix }: Prop placement="top" > diff --git a/client/src/Pages/StatusPage/Status/themes/shared/ThemedHistogram.tsx b/client/src/Pages/StatusPage/Status/themes/shared/ThemedHistogram.tsx index af73095df..8af4ffebf 100644 --- a/client/src/Pages/StatusPage/Status/themes/shared/ThemedHistogram.tsx +++ b/client/src/Pages/StatusPage/Status/themes/shared/ThemedHistogram.tsx @@ -15,12 +15,9 @@ export type BarKind = "up" | "down" | "empty"; interface Props { checks: CheckSnapshot[]; - /** sx-based API — preferred. */ - containerSx?: SxProps; - barSx?: (kind: BarKind, heightPct: number) => SxProps; - statsSx?: SxProps; - /** Legacy className-based API. Will be removed once all themes move to sx. */ - classPrefix?: string; + containerSx: SxProps; + barSx: (kind: BarKind, heightPct: number) => SxProps; + statsSx: SxProps; 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 ( - + {padded.map((check, i) => { if (!check) { - return useSxApi ? ( + return ( - ) : ( - ); } @@ -107,18 +88,10 @@ export const ThemedHistogram = ({ arrow placement="top" > - {useSxApi ? ( - - ) : ( - - )} + ); })} @@ -126,7 +99,7 @@ export const ThemedHistogram = ({ {t("pages.statusPages.monitorsList.chart.avg", { value: avg })} {t("pages.statusPages.monitorsList.chart.max", { value: peak })} diff --git a/client/src/Pages/StatusPage/Status/themes/shared/ThemedInfrastructure.tsx b/client/src/Pages/StatusPage/Status/themes/shared/ThemedInfrastructure.tsx index 8c642a344..01804a322 100644 --- a/client/src/Pages/StatusPage/Status/themes/shared/ThemedInfrastructure.tsx +++ b/client/src/Pages/StatusPage/Status/themes/shared/ThemedInfrastructure.tsx @@ -6,7 +6,7 @@ import type { Monitor } from "@/Types/Monitor"; export type GaugeFillLevel = "ok" | "warm" | "hot"; -interface InfraSx { +export interface InfraSx { containerSx: SxProps; emptySx: SxProps; gaugeSx: SxProps; @@ -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 ? ( - {message} - ) : ( - {message} - ); - }; + const renderEmpty = () => ( + {t("pages.statusPages.monitorsList.noData")} + ); 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 ( - - {gauges.map((g) => ( - - {g.label} - {g.value.toFixed(0)}% - - - - {g.sub} - - ))} - - ); - } - return ( - - {gauges.map((g) => { - const level = heatLevel(g.value); - const heatClass = level === "ok" ? "" : `${classPrefix}-${level}`; - return ( - - {g.label} - {g.value.toFixed(0)}% - - - - {g.sub} + + {gauges.map((g) => ( + + {g.label} + {g.value.toFixed(0)}% + + - ); - })} + {g.sub} + + ))} ); };