Theming the React Frontend

The React frontend themes with the same semantics and the same persistence contract as the Blazor portal's <FluentDesignTheme> — so a user switching between the two frontends on one origin keeps a single preference. Everything lives in clients/react/src/theme/.

The shared contract with Blazor

Four points of deliberate compatibility (theme/themeMode.ts):

  1. Three modes"system" (default), "light", "dark" — Blazor's DesignThemeModes.
  2. Same storage key, same JSON shape. The preference persists in localStorage under the key "theme" (the Blazor portal's <FluentDesignTheme StorageName="theme"> in SiteSettingsPanel.razor), as the same JSON object the fluent-design-theme web component writes: { mode, primaryColor? }. Writing the mode preserves sibling fields a Blazor portal may have stored under the same key:
export const DEFAULT_THEME_STORAGE_KEY = "theme";

// Reads tolerate the Blazor JSON shape ({ mode, primaryColor }), a bare string
// ("dark"), or nothing/garbage (→ "system", the Blazor default).
export function readStoredThemeMode(storageKey?: string): ThemeMode;

// Writes keep { ...existing, mode } — primaryColor and friends survive.
export function writeStoredThemeMode(mode: ThemeMode, storageKey?: string): void;

// Blazor's "Reset settings" equivalent — clears the key, back to system.
export function clearStoredTheme(storageKey?: string): void;
  1. system follows the OS liveprefers-color-scheme is observed, so an OS-level switch restyles a running app without a reload.
  2. The resolved mode is mirrored onto the documentdocument.body.dataset.theme (the same body[data-theme] attribute Blazor's theme script sets) plus document.documentElement.style.colorScheme, so scrollbars and native inputs restyle too.

Design-token propagation

The Fluent design tokens (--colorNeutralBackground1, --colorBrandBackground2, …) are applied by <FluentProvider> from webLightTheme / webDarkTheme — the React v9 equivalent of Blazor's design-token stylesheet. Every control in the pack styles itself from tokens, so the whole tree restyles from the one switch; custom controls should do the same (color: "var(--colorNeutralForeground3)", never hard-coded colors).

export function resolveThemeMode(mode: ThemeMode, prefersDark: boolean): ResolvedThemeMode;
export function fluentThemeFor(resolved: ResolvedThemeMode): Theme;  // webDarkTheme | webLightTheme

useThemeMode — the hook

import { useThemeMode } from "@meshweaver/react";

function Shell() {
  const { mode, resolved, theme, setMode } = useThemeMode();
  // mode:     the user's choice — "light" | "dark" | "system"
  // resolved: what's actually on screen — "light" | "dark"
  // theme:    the Fluent theme object for <FluentProvider>
  return <FluentProvider theme={theme}>…</FluentProvider>;
}

The hook keeps every instance in sync: instances in the same document coordinate through a custom meshweaver:theme-change event (a toggle in the header and an appearance panel never disagree), and other tabs follow via the browser's storage event.

MeshAreaView calls useThemeMode internally — when you don't pin a theme prop, the view follows the persisted preference automatically. themeStorageKey overrides the storage key for apps that must not share the Blazor preference.

ThemeToggle — the switcher

theme/ThemeToggle.tsx is the React counterpart of the theme selector in the Blazor portal's site settings panel: a menu button offering Light / Dark / System with the current mode checked, persisting through useThemeMode:

import { ThemeToggle } from "@meshweaver/react";

<header>
  …
  <ThemeToggle />   {/* optional: <ThemeToggle storageKey="my-app-theme" /> */}
</header>

The clients/portal app shell places it in the header next to the user avatar — the same spot the Blazor portal exposes its theme control.

Reconnecting…
The server was updated. Reloading the page to pick up the latest version.