diff --git a/client/src/Pages/StatusPage/Status/index.tsx b/client/src/Pages/StatusPage/Status/index.tsx index 4fbf6179a..3c0408628 100644 --- a/client/src/Pages/StatusPage/Status/index.tsx +++ b/client/src/Pages/StatusPage/Status/index.tsx @@ -142,6 +142,7 @@ const StatusPageView = () => { theme={statusPage.theme} themeMode={statusPage.themeMode} timezone={statusPage.timezone} + brandColor={statusPage.color} paintBody > {customCss && } diff --git a/client/src/Pages/StatusPage/Status/themes/StatusPageThemeProvider.tsx b/client/src/Pages/StatusPage/Status/themes/StatusPageThemeProvider.tsx index f79991c3b..4d50dd3e6 100644 --- a/client/src/Pages/StatusPage/Status/themes/StatusPageThemeProvider.tsx +++ b/client/src/Pages/StatusPage/Status/themes/StatusPageThemeProvider.tsx @@ -6,6 +6,7 @@ import { useState, type ReactNode, } from "react"; +import { alpha, darken } from "@mui/material/styles"; import { DEFAULT_STATUS_PAGE_THEME, resolveStatusPageTheme, @@ -36,6 +37,15 @@ interface Props { theme?: StatusPageTheme; themeMode?: StatusPageThemeMode; timezone?: string; + /** + * Brand color picked in the status-page appearance settings. When + * provided, it feeds `tokens.brand` / `brandStrong` / `brandSoft`, + * which theme styles use for accent surfaces (hero, active tabs, + * links). It does NOT recolor the semantic `up`/OK status color, so + * a red or orange brand cannot make the "all operational" indicator + * signal downtime. + */ + brandColor?: string; /** * When true, paint the document body with the theme background so it * covers beyond the provider's wrapper (useful on the public route @@ -54,10 +64,32 @@ interface Props { children: ReactNode; } +const HEX_COLOR = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i; + +// Resolve the brand triple that theme styles read from. A customer's +// picked color (when valid) overrides the theme's default; otherwise +// the theme's own `brand*` values win, and finally we fall back to the +// `up*` triple so unmigrated theme styles have something to consume. +const resolveBrandTokens = ( + tokens: StatusPageThemeTokens, + brandColor: string | undefined +): StatusPageThemeTokens => { + const validOverride = brandColor && HEX_COLOR.test(brandColor) ? brandColor : undefined; + const brand = validOverride ?? tokens.brand ?? tokens.up; + const brandStrong = validOverride + ? darken(validOverride, 0.2) + : (tokens.brandStrong ?? tokens.upStrong); + const brandSoft = validOverride + ? alpha(validOverride, 0.15) + : (tokens.brandSoft ?? tokens.upSoft); + return { ...tokens, brand, brandStrong, brandSoft }; +}; + export const StatusPageThemeProvider = ({ theme, themeMode, timezone, + brandColor, paintBody = false, transparent = false, children, @@ -109,7 +141,11 @@ export const StatusPageThemeProvider = ({ const resolvedMode: ResolvedMode = resolvedThemeMode === "auto" ? systemMode : resolvedThemeMode; - const tokens = themeTokens[resolvedTheme][resolvedMode]; + const baseTokens = themeTokens[resolvedTheme][resolvedMode]; + const tokens = useMemo( + () => resolveBrandTokens(baseTokens, brandColor), + [baseTokens, brandColor] + ); const value = useMemo( () => ({ diff --git a/client/src/Pages/StatusPage/Status/themes/refined/styles.ts b/client/src/Pages/StatusPage/Status/themes/refined/styles.ts index 0cf754af6..457b86ede 100644 --- a/client/src/Pages/StatusPage/Status/themes/refined/styles.ts +++ b/client/src/Pages/StatusPage/Status/themes/refined/styles.ts @@ -119,7 +119,7 @@ export const refinedStyles = ( width: 28, height: 28, borderRadius: "8px", - background: tokens.up, + background: tokens.brand, display: "grid", placeItems: "center", color: "#fff", @@ -191,16 +191,16 @@ export const refinedStyles = ( whiteSpace: "nowrap", textAlign: "center", border: 0, - background: active ? tokens.upSoft : "transparent", + background: active ? tokens.brandSoft : "transparent", fontFamily: "inherit", fontSize: 11, padding: { xs: "5px 6px", md: "5px 14px" }, cursor: "pointer", - color: active ? tokens.up : tokens.textMuted, + color: active ? tokens.brand : tokens.textMuted, borderRadius: "5px", transition: "background 0.15s ease, color 0.15s ease", fontWeight: active ? 600 : 500, - "&:hover": { color: active ? tokens.up : tokens.text }, + "&:hover": { color: active ? tokens.brand : tokens.text }, }), monitorList: { @@ -259,9 +259,9 @@ export const refinedStyles = ( }, pillHardware: { ...pillBase, - color: tokens.up, + color: tokens.brand, border: `1px solid ${tokens.border}`, - background: tokens.upSoft, + background: tokens.brandSoft, }, monitorUrl: { fontSize: 12, @@ -382,11 +382,11 @@ export const refinedStyles = ( fontSize: 12, mt: "40px", "& a": { - color: tokens.up, + color: tokens.brand, textDecoration: "underline", textUnderlineOffset: "3px", fontWeight: 600, - "&:hover": { color: tokens.upStrong || tokens.up }, + "&:hover": { color: tokens.brandStrong }, }, }, }; diff --git a/client/src/Pages/StatusPage/Status/themes/tokens.ts b/client/src/Pages/StatusPage/Status/themes/tokens.ts index fdb251c07..3ec90abde 100644 --- a/client/src/Pages/StatusPage/Status/themes/tokens.ts +++ b/client/src/Pages/StatusPage/Status/themes/tokens.ts @@ -9,6 +9,14 @@ export interface StatusPageThemeTokens { up: string; upStrong: string; upSoft: string; + // Accent used for brand-tinted surfaces (hero background, active tab + // underline, "view all" links). Distinct from `up` so a red/orange + // brand color cannot recolor the semantic OK/up status indicators. + // Themes leave these undefined by default; the provider falls back + // to `up`/`upStrong`/`upSoft` so styles have a value to consume. + brand?: string; + brandStrong?: string; + brandSoft?: string; degraded: string; degradedSoft: string; down: string;