Adds four fully-designed visual themes for the public status page (refined | modern | bold | editorial) with a light/dark mode selector, admin preview in a mock browser frame, and a shared theme provider that keeps the component tree unified while letting each theme own its visual identity. ## Selectable themes - Refined — clean, flat cards, soft shadows, colored status dot. - Modern — flat surfaces with a pulsing status dot and staggered fade-in cards. - Bold — conic-gradient logo, 34px hero title with a check disc, subtle dual-radial glow on dark mode, accent-green hover border. - Editorial — serif headings, narrow newspaper column, 'Service Report' eyebrow + title, flat rectangular badges and dateline. ## Architecture - Single `StatusPageThemeProvider` hosts CSS custom-property tokens per theme and resolves light/dark based on admin-forced mode, falling back to `prefers-color-scheme` with a matchMedia listener. - Each theme owns only its `*StatusPage.tsx` + scoped CSS file. The heatmap, histogram, and infrastructure renderers are shared across all four themes (parameterized by a `classPrefix`) to avoid the 12-file duplication the initial draft had. - Renderer lookup table in `Status/index.tsx` replaces a 4-way ternary chain; unknown themes fall through to the legacy renderer. - `resolveOverallStatus`, `statusBadgeKey`, `monoFirstChar`, and `monitorBadgeTone` are extracted to `themes/shared/overallStatus.tsx`. ## Admin UX - `ThemePicker` uses the project's `RadioWithDescription` and `Select` components — one radio card per theme plus an Auto/ Light-only/Dark-only mode dropdown. - `BrowserFrame` wraps the preview in the admin panel with a mock chrome (traffic lights + URL bar) that adopts the status page's theme tokens. Edge-to-edge fill with no gray bleed. - Public route renders the theme directly on the viewport with the document body painted to the theme background, reverted on unmount. ## Performance & polish - Histogram's derived stats (padded array, max, avg, peak) are memoized on `checks`; `max` is reused for `peak` instead of computing twice. - Editorial's dateline is wrapped in `useMemo` so the 10s poll doesn't rerender "April 23, 2026" as a changing string. - Provider's `setSystemMode` reconciliation uses the updater form to avoid an extra render when the value is unchanged. - `monoFirstChar` no longer uses a non-null assertion. ## Form plumbing - `useStatusPageForm` defaults include `theme` + `themeMode`. - `Validation/statusPage.ts` accepts both as optional zod enums. - `Pages/StatusPage/Create/index.tsx` appends both to FormData on create and update. ## CLAUDE.md - Dev port note: local client runs on 10001 (5173 is used by a different local project); server `CLIENT_HOST` is set to match.
67 lines
1.7 KiB
TypeScript
67 lines
1.7 KiB
TypeScript
import { useTranslation } from "react-i18next";
|
|
import Stack from "@mui/material/Stack";
|
|
import RadioGroup from "@mui/material/RadioGroup";
|
|
import MenuItem from "@mui/material/MenuItem";
|
|
import { useTheme } from "@mui/material/styles";
|
|
import { RadioWithDescription, Select } from "@/Components/inputs";
|
|
import {
|
|
STATUS_PAGE_THEMES,
|
|
STATUS_PAGE_THEME_MODES,
|
|
type StatusPageTheme,
|
|
type StatusPageThemeMode,
|
|
} from "@/Types/StatusPage";
|
|
|
|
interface Props {
|
|
theme: StatusPageTheme;
|
|
themeMode: StatusPageThemeMode;
|
|
onThemeChange: (value: StatusPageTheme) => void;
|
|
onThemeModeChange: (value: StatusPageThemeMode) => void;
|
|
}
|
|
|
|
export const ThemePicker = ({
|
|
theme,
|
|
themeMode,
|
|
onThemeChange,
|
|
onThemeModeChange,
|
|
}: Props) => {
|
|
const { t } = useTranslation();
|
|
const muiTheme = useTheme();
|
|
|
|
return (
|
|
<Stack spacing={muiTheme.spacing(6)}>
|
|
<RadioGroup
|
|
value={theme}
|
|
onChange={(_, value) => onThemeChange(value as StatusPageTheme)}
|
|
>
|
|
<Stack spacing={muiTheme.spacing(4)}>
|
|
{STATUS_PAGE_THEMES.map((option) => (
|
|
<RadioWithDescription
|
|
key={option}
|
|
value={option}
|
|
label={t(`pages.statusPages.form.theme.options.${option}.name`)}
|
|
description={t(
|
|
`pages.statusPages.form.theme.options.${option}.description`
|
|
)}
|
|
/>
|
|
))}
|
|
</Stack>
|
|
</RadioGroup>
|
|
|
|
<Select
|
|
fieldLabel={t("pages.statusPages.form.themeMode.label")}
|
|
value={themeMode}
|
|
onChange={(e) => onThemeModeChange(e.target.value as StatusPageThemeMode)}
|
|
>
|
|
{STATUS_PAGE_THEME_MODES.map((mode) => (
|
|
<MenuItem
|
|
key={mode}
|
|
value={mode}
|
|
>
|
|
{t(`pages.statusPages.form.themeMode.${mode}`)}
|
|
</MenuItem>
|
|
))}
|
|
</Select>
|
|
</Stack>
|
|
);
|
|
};
|