Files
Checkmate/client/src/Pages/StatusPage/Create/Components/ThemePicker.tsx
T
gorkem-bwl 944ee74a1d feat(status-page): four selectable themes for the public status page
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.
2026-04-23 22:51:40 -04:00

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>
);
};