diff --git a/.storybook/ThemeSwapper.tsx b/.storybook/ThemeSwapper.tsx index efd541cc..2d194855 100644 --- a/.storybook/ThemeSwapper.tsx +++ b/.storybook/ThemeSwapper.tsx @@ -21,12 +21,14 @@ export const TextDark = "Mode: Dark"; const ThemeSwapper = ({ context, children }: ThemeSwapperProps) => { const { mode, setMode } = useColorScheme(); - //if( !mode ) return useEffect(() => { const selectedThemeMode = context.globals.themeMode || TextLight; - setMode(selectedThemeMode == TextLight ? "light" : "dark"); - }, [context.globals.themeMode]); + const nextMode = selectedThemeMode === TextLight ? "light" : "dark"; + + setMode(nextMode); + document.documentElement.setAttribute("data-mode", nextMode); + }, [context.globals.themeMode, setMode]); return (
@@ -36,4 +38,4 @@ const ThemeSwapper = ({ context, children }: ThemeSwapperProps) => { }; export { ThemeSwapper }; -export type { Context }; +export type { Context }; \ No newline at end of file diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx index 4de2bdc4..821b5212 100644 --- a/.storybook/preview.tsx +++ b/.storybook/preview.tsx @@ -1,15 +1,53 @@ -import React from "react"; +import React, { useLayoutEffect } from "react"; import { CssBaseline } from "@mui/material"; import type { Preview } from "@storybook/react"; - import { ThemeProvider } from "../src"; -import { GenericTheme, DiamondTheme } from "../src"; - +import { + GenericTheme, + DiamondTheme, + DiamondDSTheme, + DiamondDSThemeDark, +} from "../src"; import { Context, ThemeSwapper, TextLight, TextDark } from "./ThemeSwapper"; const TextThemeBase = "Theme: Generic"; const TextThemeDiamond = "Theme: Diamond"; +const TextThemeDiamondDS = "Theme: DiamondDS"; + +function resolveTheme(selectedTheme: string, mode: "light" | "dark") { + switch (selectedTheme) { + case TextThemeBase: + return GenericTheme; + case TextThemeDiamondDS: + return mode === "dark" ? DiamondDSThemeDark : DiamondDSTheme; + case TextThemeDiamond: + default: + return DiamondTheme; + } +} + +function ApplyModeToPreviewDoc({ + mode, + doc, +}: { + mode: "light" | "dark"; + doc: Document; +}) { + useLayoutEffect(() => { + const root = doc.documentElement; // + root.setAttribute("data-mode", mode); + + // Optional: keep class too if your CSS supports it + root.classList.toggle("dark", mode === "dark"); + root.classList.toggle("light", mode === "light"); + + root.style.colorScheme = mode; + }, [mode, doc]); + + return null; +} + export const decorators = [ (StoriesWithPadding: React.FC) => { return ( @@ -28,12 +66,16 @@ export const decorators = [ (StoriesWithThemeProvider: React.FC, context: Context) => { const selectedTheme = context.globals.theme || TextThemeBase; const selectedThemeMode = context.globals.themeMode || TextLight; + const mode = selectedThemeMode === TextLight ? "light" : "dark"; + // ensure we target the preview iframe document + const doc: Document = context?.canvasElement?.ownerDocument ?? document; return ( + @@ -48,7 +90,7 @@ const preview: Preview = { toolbar: { title: "Theme", icon: "cog", - items: [TextThemeBase, TextThemeDiamond], + items: [TextThemeBase, TextThemeDiamond, TextThemeDiamondDS], dynamicTitle: true, }, }, @@ -63,8 +105,8 @@ const preview: Preview = { }, }, initialGlobals: { - theme: "Theme: Diamond", - themeMode: "Mode: Light", + theme: TextThemeDiamondDS, + themeMode: TextLight, }, parameters: { controls: { @@ -75,18 +117,6 @@ const preview: Preview = { }, backgrounds: { disable: true }, layout: "fullscreen", - options: { - storySort: { - order: [ - "Introduction", - "Components", - "Theme", - "Theme/Logos", - "Theme/Colours", - "Helpers", - ], - }, - }, }, argTypes: { linkComponent: { diff --git a/src/index.ts b/src/index.ts index 9b8dda18..386c9447 100644 --- a/src/index.ts +++ b/src/index.ts @@ -25,7 +25,7 @@ export * from "./components/helpers/jsonforms"; // themes export * from "./themes/BaseTheme"; export * from "./themes/DiamondTheme"; -export * from "./themes/DiamondOldTheme"; +export * from "./themes/DiamondDSTheme"; export * from "./themes/GenericTheme"; export * from "./themes/ThemeProvider"; export * from "./themes/ThemeManager"; diff --git a/src/styles/diamondDS/diamond-ds-roles.css b/src/styles/diamondDS/diamond-ds-roles.css new file mode 100644 index 00000000..ed198f98 --- /dev/null +++ b/src/styles/diamondDS/diamond-ds-roles.css @@ -0,0 +1,430 @@ +:root, .light, +html[data-mode="light"] { + /* Neutral */ + + --ds-background: #F6F6F9; + --ds-background-channel: 246 246 249; + + --ds-surface: #ffffff; + --ds-surface-channel: 255 255 255; + + --ds-surface-container: #eef1f5; + --ds-surface-container-high: #e6e9f0; + --ds-surface-disabled: rgba(0, 0, 0, 0.08); + + --ds-on-surface: #1a1c23; + --ds-on-surface-variant: #5e6473; + --ds-on-surface-disabled: rgba(0, 0, 0, 0.36); + --ds-action-disabled: rgba(0, 0, 0, 0.30); + --ds-on-solid: #ffffff; + + --ds-on-surface-channel: 26 28 35; + --ds-on-surface-variant-channel: 80 85 99; + + --ds-placeholder: #8a90a0; + --ds-placeholder-focus: #505563; + + --ds-border-subtle: #dde1e8; + --ds-border: #bcc2cd; + --ds-border-emphasis: #a5acb8; + + /* Overlay */ + --ds-overlay-hover: rgba(0, 0, 0, 0.08); + --ds-overlay-hover-solid: rgba(0, 0, 0, 0.16); + --ds-overlay-selected: rgba(0, 0, 0, 0.25); + --ds-overlay-focus: rgba(0, 0, 0, 0.10); + + /* Primary (Indigo-Blue) */ + --ds-primary: #2a4db8; + --ds-on-primary: #ffffff; + --ds-primary-emphasis: #1f3d96; + --ds-primary-accent: #6a86e4; + --ds-primary-container: #e5ebff; + --ds-on-primary-container: #1a2f6b; + --ds-primary-solid: #3f63c9; + --ds-on-primary-solid: #ffffff; + + --ds-on-primary-channel: 255 255 255; + --ds-primary-mainChannel: 42 77 184; + --ds-primary-lightChannel: 106 134 228; + --ds-primary-darkChannel: 31 61 150; + + /* Secondary (Teal) */ + --ds-secondary: #007b84; + --ds-on-secondary: #ffffff; + --ds-secondary-emphasis: #005f67; + --ds-secondary-accent: #27adb7; + --ds-secondary-container: #ddf3f5; + --ds-on-secondary-container: #00474d; + --ds-secondary-solid: #0a858e; + --ds-on-secondary-solid: #ffffff; + + --ds-on-secondary-channel: 255 255 255; + --ds-secondary-mainChannel: 0 123 132; + --ds-secondary-lightChannel: 39 173 183; + --ds-secondary-darkChannel: 0 95 103; + + /* Tertiary (Violet) */ + --ds-tertiary: #8c0070; + --ds-on-tertiary: #ffffff; + --ds-tertiary-emphasis: #6c0057; + --ds-tertiary-accent: #c735a8; + --ds-tertiary-container: #f8e2f2; + --ds-on-tertiary-container: #4f003f; + --ds-tertiary-solid: #b8329b; + --ds-on-tertiary-solid: #ffffff; + + --ds-on-tertiary-channel: 255 255 255; + --ds-tertiary-mainChannel: 140 0 112; + --ds-tertiary-lightChannel: 199 53 168; + --ds-tertiary-darkChannel: 108 0 87; + + /* Brand (Diamond Blue) */ + --ds-brand: #202945; + --ds-on-brand: #ffffff; + --ds-brand-emphasis: #171f35; + --ds-brand-accent: #6a86db; + --ds-brand-container: #e4e8f4; + --ds-on-brand-container: #202945; + --ds-brand-solid: #2f3b63; + --ds-on-brand-solid: #ffffff; + + --ds-brand-fixed: #202945; + --ds-brand-fixed-dim: #586084; + --ds-on-brand-fixed: #ffffff; + + --ds-on-brand-channel: 255 255 255; + --ds-brand-mainChannel: 32 41 69; + --ds-brand-lightChannel: 106 134 219; + --ds-brand-darkChannel: 23 31 53; + + /* Danger (Red) */ + --ds-danger: #b42318; + --ds-on-danger: #ffffff; + --ds-danger-emphasis: #912018; + --ds-danger-accent: #d94f45; + --ds-danger-container: #fde7e5; + --ds-on-danger-container: #6a1b15; + --ds-danger-solid: #d63c41; + --ds-on-danger-solid: #ffffff; + + --ds-on-danger-channel: 255 255 255; + --ds-danger-mainChannel: 180 35 24; + --ds-danger-lightChannel: 217 79 69; + --ds-danger-darkChannel: 145 32 24; + + /* Warning (Orange) */ + --ds-warning: #c96a04; + --ds-on-warning: #ffffff; + --ds-warning-emphasis: #a95703; + --ds-warning-accent: #e98a15; + --ds-warning-container: #fef0df; + --ds-on-warning-container: #6f3200; + --ds-warning-solid: #e97b12; + --ds-on-warning-solid: #ffffff; + + --ds-on-warning-channel: 255 255 255; + --ds-warning-mainChannel: 201 106 4; + --ds-warning-lightChannel: 233 138 21; + --ds-warning-darkChannel: 169 87 3; + + /* Success (Green) */ + --ds-success: #187a2f; + --ds-on-success: #ffffff; + --ds-success-emphasis: #146125; + --ds-success-accent: #2FB344; + --ds-success-container: #e3f4e7; + --ds-on-success-container: #124d22; + --ds-success-solid: #1B8834; + --ds-on-success-solid: #ffffff; + + --ds-on-success-channel: 255 255 255; + --ds-success-mainChannel: 24 122 47; + --ds-success-lightChannel: 47 154 73; + --ds-success-darkChannel: 20 97 37; + + /* Info (Blue) */ + --ds-info: #355ec9; + --ds-on-info: #ffffff; + --ds-info-emphasis: #2a4ea7; + --ds-info-accent: #6f8fe8; + --ds-info-container: #e9efff; + --ds-on-info-container: #1f3b85; + --ds-info-solid: #4d72dd; + --ds-on-info-solid: #ffffff; + + --ds-on-info-channel: 255 255 255; + --ds-info-mainChannel: 53 94 201; + --ds-info-lightChannel: 111 143 232; + --ds-info-darkChannel: 42 78 167; + + /* Highlight */ + --ds-highlight: #d4a900; + --ds-on-highlight: #1a1c23; + --ds-highlight-emphasis: #b89300; + --ds-highlight-accent: #ffd84d; + --ds-highlight-container: #fff4cc; + --ds-on-highlight-container: #6b5500; + --ds-highlight-solid: #b89300; + --ds-on-highlight-solid: #ffffff; + + --ds-on-highlight-channel: 26 28 35; + --ds-highlight-mainChannel: 212 169 0; + --ds-highlight-lightChannel: 255 216 77; + --ds-highlight-darkChannel: 184 147 0; + + /* Focus */ + --ds-focus-ring: var(--ds-primary-accent); + --ds-focus-ring-width: 2px; + --ds-focus-ring-offset: 2px; + + --ds-focus-ring-primary: var(--ds-primary-accent); + --ds-focus-ring-secondary: var(--ds-secondary-accent); + --ds-focus-ring-danger: var(--ds-danger-accent); + --ds-focus-ring-warning: var(--ds-warning-accent); + --ds-focus-ring-success: var(--ds-success-accent); + --ds-focus-ring-info: var(--ds-info-accent); + --ds-focus-ring-brand: var(--ds-brand-accent); + --ds-focus-ring-highlight: var(--ds-highlight-accent); +} + +.dark, +html[data-mode="dark"] { + /* Neutral */ + + --ds-background: #0e1017; + --ds-background-channel: 14 16 23; + + --ds-surface: #161820; + --ds-surface-channel: 22 24 32; + + --ds-surface-container: #222632; + --ds-surface-container-high: #2c3140; + --ds-surface-disabled: rgba(255, 255, 255, 0.14); + + --ds-on-surface: #e8eaf0; + --ds-on-surface-variant: #b6bcc9; + --ds-on-surface-disabled: rgba(255, 255, 255, 0.36); + --ds-action-disabled: rgba(255, 255, 255, 0.30); + --ds-on-solid: #ffffff; + + --ds-on-surface-channel: 232 234 240; + --ds-on-surface-variant-channel: 182 188 201; + + --ds-placeholder: #7a8191; + --ds-placeholder-focus: #b6bcc9; + + --ds-border-subtle: #3a3f4c; + --ds-border: #505664; + --ds-border-emphasis: #7c8394; + + /* Overlay */ + --ds-overlay-hover: rgba(255, 255, 255, 0.16); + --ds-overlay-hover-solid: rgba(255, 255, 255, 0.16); + --ds-overlay-selected: rgba(255, 255, 255, 0.12); + --ds-overlay-focus: rgba(255, 255, 255, 0.12); + + /* Primary */ + --ds-primary: #8aa7ff; + --ds-on-primary: #0b1638; + --ds-primary-emphasis: #c4d4ff; + --ds-primary-accent: #a5bcff; + --ds-primary-container: #1b2c5f; + --ds-on-primary-container: #e8eeff; + --ds-primary-solid: #3f63c9; + --ds-on-primary-solid: #ffffff; + + --ds-on-primary-channel: 11 22 56; + --ds-primary-mainChannel: 138 167 255; + --ds-primary-lightChannel: 196 212 255; + --ds-primary-darkChannel: 165 188 255; + + /* Secondary */ + --ds-secondary: #58d6de; + --ds-on-secondary: #002529; + --ds-secondary-emphasis: #9af0f3; + --ds-secondary-accent: #7be4ea; + --ds-secondary-container: #0d3338; + --ds-on-secondary-container: #ccf7f9; + --ds-secondary-solid: #0a858e; + --ds-on-secondary-solid: #ffffff; + + --ds-on-secondary-channel: 0 37 41; + --ds-secondary-mainChannel: 88 214 222; + --ds-secondary-lightChannel: 154 240 243; + --ds-secondary-darkChannel: 123 228 234; + + /* Tertiary */ + --ds-tertiary: #e587d1; + --ds-on-tertiary: #2a0022; + --ds-tertiary-emphasis: #f7bfeb; + --ds-tertiary-accent: #efa5e0; + --ds-tertiary-container: #381232; + --ds-on-tertiary-container: #f9d8f1; + --ds-tertiary-solid: #b8329b; + --ds-on-tertiary-solid: #ffffff; + + --ds-on-tertiary-channel: 42 0 34; + --ds-tertiary-mainChannel: 229 135 209; + --ds-tertiary-lightChannel: 247 191 235; + --ds-tertiary-darkChannel: 239 165 224; + + /* Brand */ + --ds-brand: #aabdff; + --ds-on-brand: #0d1530; + --ds-brand-emphasis: #d7e1ff; + --ds-brand-accent: #c4d2ff; + --ds-brand-container: #202945; + --ds-on-brand-container: #e3e8f7; + --ds-brand-solid: #3a4a78; + --ds-on-brand-solid: #ffffff; + + --ds-brand-fixed: #202945; + --ds-brand-fixed-dim : #586084; + --ds-on-brand-fixed: #ffffff; + + --ds-on-brand-channel: 13 21 48; + --ds-brand-mainChannel: 170 189 255; + --ds-brand-lightChannel: 215 225 255; + --ds-brand-darkChannel: 196 210 255; + + /* Danger */ + --ds-danger: #ff9088; + --ds-on-danger: #2f0907; + --ds-danger-emphasis: #ffc7c2; + --ds-danger-accent: #ffb0aa; + --ds-danger-container: #3a1613; + --ds-on-danger-container: #ffd9d6; + --ds-danger-solid: #d63c41; + --ds-on-danger-solid: #ffffff; + + --ds-on-danger-channel: 47 9 7; + --ds-danger-mainChannel: 255 144 136; + --ds-danger-lightChannel: 255 199 194; + --ds-danger-darkChannel: 255 176 170; + + /* Warning */ + --ds-warning: #ffb067; + --ds-on-warning: #311700; + --ds-warning-emphasis: #ffd9b0; + --ds-warning-accent: #ffc68a; + --ds-warning-container: #382006; + --ds-on-warning-container: #ffe4c8; + --ds-warning-solid: #f07a13; + --ds-on-warning-solid: #ffffff; + + --ds-on-warning-channel: 49 23 0; + --ds-warning-mainChannel: 255 176 103; + --ds-warning-lightChannel: 255 217 176; + --ds-warning-darkChannel: 255 198 138; + + /* Success */ + --ds-success: #6fd88a; + --ds-on-success: #08210f; + --ds-success-emphasis: #b3f0c0; + --ds-success-accent: #8ae5a2; + --ds-success-container: #10341a; + --ds-on-success-container: #d2f7da; + --ds-success-solid: #23913C; + --ds-on-success-solid: #ffffff; + + --ds-on-success-channel: 8 33 15; + --ds-success-mainChannel: 111 216 138; + --ds-success-lightChannel: 179 240 192; + --ds-success-darkChannel: 138 229 162; + + /* Info */ + --ds-info: #9fb7ff; + --ds-on-info: #101936; + --ds-info-emphasis: #d5e0ff; + --ds-info-accent: #bccdff; + --ds-info-container: #1b2b57; + --ds-on-info-container: #dce4ff; + --ds-info-solid: #4d72dd; + --ds-on-info-solid: #ffffff; + + --ds-on-info-channel: 16 25 54; + --ds-info-mainChannel: 159 183 255; + --ds-info-lightChannel: 213 224 255; + --ds-info-darkChannel: 188 205 255; + + /* Highlight */ + --ds-highlight: #ffd84d; + --ds-on-highlight: #2a2100; + --ds-highlight-emphasis: #fff1b8; + --ds-highlight-accent: #ffeaa0; + --ds-highlight-container: #4b3a05; + --ds-on-highlight-container: #fff4c7; + --ds-highlight-solid: #D4A900; + --ds-on-highlight-solid: #1A1C23; + + --ds-on-highlight-channel: 26 28 35; + --ds-highlight-mainChannel: 255 226 122; + --ds-highlight-lightChannel: 255 241 184; + --ds-highlight-darkChannel: 255 234 160; +} + + +/* Elavation colors + +0: base paper, dialogs on clean surface +1–3: cards, panels, raised sections +4–8: menus, popovers, floating UI +9–16: more obviously separated overlays +17–24: rare, maximum lift + +Figma references: +LIGHT +elevation-0 = #FFFFFF +elevation-1 = #FDFDFE +elevation-2 = #FAFBFC +elevation-3 = #F8F9FB +elevation-4 = #F7F9FB +elevation-5 = #F6F8FA +elevation-6 = #F5F7F9 +elevation-7 = #F4F6F8 +elevation-8 = #F3F5F7 +elevation-9 = #F3F5F7 +elevation-10 = #F2F4F7 +elevation-11 = #F2F4F7 +elevation-12 = #F1F3F6 +elevation-13 = #F1F3F6 +elevation-14 = #F1F3F6 +elevation-15 = #F0F2F5 +elevation-16 = #F0F2F5 +elevation-17 = #F0F2F5 +elevation-18 = #EFF1F4 +elevation-19 = #EFF1F4 +elevation-20 = #EEF1F5 +elevation-21 = #EEF1F5 +elevation-22 = #EEF1F5 +elevation-23 = #EEF1F5 +elevation-24 = #EEF1F5 + +DARK +elevation-0 = #161820 +elevation-1 = #181B23 +elevation-2 = #191C25 +elevation-3 = #1A1E27 +elevation-4 = #1B1F28 +elevation-5 = #1C202A +elevation-6 = #1E222C +elevation-7 = #1F232D +elevation-8 = #20242F +elevation-9 = #20242F +elevation-10 = #212631 +elevation-11 = #212631 +elevation-12 = #222632 +elevation-13 = #222632 +elevation-14 = #222632 +elevation-15 = #242935 +elevation-16 = #242935 +elevation-17 = #252A37 +elevation-18 = #262C39 +elevation-19 = #262C39 +elevation-20 = #28303C +elevation-21 = #28303C +elevation-22 = #2A3140 +elevation-23 = #2A3140 +elevation-24 = #2C3140 +*/ \ No newline at end of file diff --git a/src/themes/DiamondDSTheme.ts b/src/themes/DiamondDSTheme.ts new file mode 100644 index 00000000..86bc583a --- /dev/null +++ b/src/themes/DiamondDSTheme.ts @@ -0,0 +1,1113 @@ +import "../styles/diamondDS/diamond-ds-roles.css"; + +import type {} from "@mui/material/themeCssVarsAugmentation"; +import { createTheme } from "@mui/material/styles"; +import type { Theme } from "@mui/material/styles"; +import type { CSSObject } from "@mui/material/styles"; + +import type { ButtonProps } from "@mui/material/Button"; +import type { ChipProps } from "@mui/material/Chip"; +import type { CheckboxProps } from "@mui/material/Checkbox"; +import type { RadioProps } from "@mui/material/Radio"; +import type { OutlinedInputProps } from "@mui/material/OutlinedInput"; +import type { TabProps } from "@mui/material/Tab"; +import type { AlertProps } from "@mui/material/Alert"; +import type { LinearProgressProps } from "@mui/material/LinearProgress"; +import type { CircularProgressProps } from "@mui/material/CircularProgress"; + +import { mergeThemeOptions } from "./ThemeManager"; + +import logoImageLight from "../public/diamond/logo-light.svg"; +import logoImageDark from "../public/diamond/logo-dark.svg"; +import logoShort from "../public/diamond/logo-short.svg"; + +type OverrideArgs = { + ownerState: OwnerState; + theme: Theme; +}; + +type ThemeOnlyArgs = { theme: Theme }; + +type IntentColour = + | "primary" + | "secondary" + | "error" + | "warning" + | "info" + | "success"; + +type ExtendedPaletteColor = { + light?: string; + main?: string; + dark?: string; + contrastText?: string; + mainChannel?: string; + lightChannel?: string; + darkChannel?: string; + container?: string; + onContainer?: string; + solid?: string; + onSolid?: string; +}; + +declare module "@mui/material/styles" { + interface TypeBackground { + default: string; + paper: string; + } + + interface TypeText { + placeholder?: string; + placeholderFocus?: string; + onSolid?: string; + primaryChannel?: string; + secondaryChannel?: string; + } + interface TypeTextOptions { + primary?: string; + secondary?: string; + disabled?: string; + placeholder?: string; + placeholderFocus?: string; + primaryChannel?: string; + secondaryChannel?: string; + } + interface Palette { + brand?: PaletteColor; + borders: { + subtle: string; + base: string; + emphasis: string; + }; + surface: { + subtle: string; + strong: string; + }; + } + + interface PaletteOptions { + brand?: SimplePaletteColorOptions; + borders?: { + subtle?: string; + base?: string; + emphasis?: string; + }; + surface?: { + subtle?: string; + strong?: string; + }; + } + + interface PaletteColor { + mainChannel?: string; + lightChannel?: string; + darkChannel?: string; + contrastTextChannel?: string; + container?: string; + onContainer?: string; + solid?: string; + onSolid?: string; + } + + interface SimplePaletteColorOptions { + mainChannel?: string; + lightChannel?: string; + darkChannel?: string; + contrastTextChannel?: string; + container?: string; + onContainer?: string; + solid?: string; + onSolid?: string; + } +} + +export type DSMode = "light" | "dark"; + +// --- Helpers --- + +const getIntentPalette = ( + theme: Theme, + colour: IntentColour +): ExtendedPaletteColor => { + const varsPalette = (theme as any).vars?.palette?.[colour] as + | ExtendedPaletteColor + | undefined; + const fallbackPalette = (theme.palette as any)[colour] as + | ExtendedPaletteColor + | undefined; + + if (process.env.NODE_ENV !== "production" && !fallbackPalette) { + console.warn( + `[DiamondDS] getIntentPalette: colour "${colour}" not found in palette` + ); + } + + return { + ...fallbackPalette, + ...varsPalette, + }; +}; + +const getFocusToken = (colour?: IntentColour) => { + if (!colour) return "var(--ds-focus-ring)"; + return `var(--ds-focus-ring-${colour === "error" ? "danger" : colour})`; +}; + +const getFocusOutline = (token?: string): CSSObject => ({ + "&.Mui-focusVisible": { + outline: "var(--ds-focus-ring-width) solid", + outlineColor: token ?? "var(--ds-focus-ring)", + outlineOffset: "var(--ds-focus-ring-offset)", + }, +}); + +const getOverlayInset = (token = "var(--ds-overlay-hover)") => + `inset 0 0 0 9999px ${token}`; + +// --- Theme factory --- + +export const createDiamondTheme = (mode: DSMode): Theme => { + const DiamondDSThemeOptions = mergeThemeOptions({ + typography: { + fontFamily: [ + "Inter Variable", + "Inter", + "system-ui", + "-apple-system", + '"Segoe UI"', + "Roboto", + "Helvetica", + "Arial", + "sans-serif", + ].join(","), + }, + + logos: { + normal: { + src: + mode === "dark" ? (logoImageDark ?? logoImageLight) : logoImageLight, + srcDark: logoImageDark ?? logoImageLight, + alt: "Diamond Light Source Logo", + width: "100", + }, + short: { + src: logoShort, + alt: "Diamond Light Source Logo", + width: "35", + }, + }, + + palette: { + mode, + + action: { + hover: "var(--ds-overlay-hover)", + selected: "var(--ds-overlay-selected)", + focus: "var(--ds-overlay-focus)", + disabled: "var(--ds-on-surface-disabled)", + disabledBackground: "var(--ds-surface-disabled)", + + hoverOpacity: 0.04, + selectedOpacity: 0.08, + disabledOpacity: 0.1, + focusOpacity: 0.1, + }, + + text: { + primary: "var(--ds-on-surface)", + secondary: "var(--ds-on-surface-variant)", + onSolid: "var(--ds-on-solid)", + disabled: "var(--ds-on-surface-disabled)", + placeholder: "var(--ds-placeholder)", + placeholderFocus: "var(--ds-placeholder-focus)", + + primaryChannel: "var(--ds-on-surface-channel)", + secondaryChannel: "var(--ds-on-surface-variant-channel)", + }, + + background: { + default: "rgb(var(--ds-background-channel))", + paper: "rgb(var(--ds-surface-channel))", + }, + + divider: "var(--ds-border-subtle)", + + borders: { + subtle: "var(--ds-border-subtle)", + base: "var(--ds-border)", + emphasis: "var(--ds-border-emphasis)", + }, + + surface: { + subtle: "var(--ds-surface-container)", + strong: "var(--ds-surface-container-high)", + }, + + primary: { + light: "var(--ds-primary-accent)", + main: "var(--ds-primary)", + dark: "var(--ds-primary-emphasis)", + contrastText: "var(--ds-on-primary)", + container: "var(--ds-primary-container)", + onContainer: "var(--ds-on-primary-container)", + solid: "var(--ds-primary-solid)", + onSolid: "var(--ds-on-primary-solid)", + + contrastTextChannel: "var(--ds-on-primary-channel)", + mainChannel: "var(--ds-primary-mainChannel)", + lightChannel: "var(--ds-primary-lightChannel)", + darkChannel: "var(--ds-primary-darkChannel)", + }, + + secondary: { + light: "var(--ds-secondary-accent)", + main: "var(--ds-secondary)", + dark: "var(--ds-secondary-emphasis)", + contrastText: "var(--ds-on-secondary)", + container: "var(--ds-secondary-container)", + onContainer: "var(--ds-on-secondary-container)", + solid: "var(--ds-secondary-solid)", + onSolid: "var(--ds-on-secondary-solid)", + + contrastTextChannel: "var(--ds-on-secondary-channel)", + mainChannel: "var(--ds-secondary-mainChannel)", + lightChannel: "var(--ds-secondary-lightChannel)", + darkChannel: "var(--ds-secondary-darkChannel)", + }, + + brand: { + light: "var(--ds-brand-accent)", + main: "var(--ds-brand)", + dark: "var(--ds-brand-emphasis)", + contrastText: "var(--ds-on-brand)", + container: "var(--ds-brand-container)", + onContainer: "var(--ds-on-brand-container)", + solid: "var(--ds-brand-solid)", + onSolid: "var(--ds-on-brand-solid)", + + contrastTextChannel: "var(--ds-on-brand-channel)", + mainChannel: "var(--ds-brand-mainChannel)", + lightChannel: "var(--ds-brand-lightChannel)", + darkChannel: "var(--ds-brand-darkChannel)", + }, + + error: /* Danger */ { + light: "var(--ds-danger-accent)", + main: "var(--ds-danger)", + dark: "var(--ds-danger-emphasis)", + contrastText: "var(--ds-on-danger)", + container: "var(--ds-danger-container)", + onContainer: "var(--ds-on-danger-container)", + solid: "var(--ds-danger-solid)", + onSolid: "var(--ds-on-danger-solid)", + + contrastTextChannel: "var(--ds-on-danger-channel)", + mainChannel: "var(--ds-danger-mainChannel)", + lightChannel: "var(--ds-danger-lightChannel)", + darkChannel: "var(--ds-danger-darkChannel)", + }, + + warning: { + light: "var(--ds-warning-accent)", + main: "var(--ds-warning)", + dark: "var(--ds-warning-emphasis)", + contrastText: "var(--ds-on-warning)", + container: "var(--ds-warning-container)", + onContainer: "var(--ds-on-warning-container)", + solid: "var(--ds-warning-solid)", + onSolid: "var(--ds-on-warning-solid)", + + contrastTextChannel: "var(--ds-on-warning-channel)", + mainChannel: "var(--ds-warning-mainChannel)", + lightChannel: "var(--ds-warning-lightChannel)", + darkChannel: "var(--ds-warning-darkChannel)", + }, + + success: { + light: "var(--ds-success-accent)", + main: "var(--ds-success)", + dark: "var(--ds-success-emphasis)", + contrastText: "var(--ds-on-success)", + container: "var(--ds-success-container)", + onContainer: "var(--ds-on-success-container)", + solid: "var(--ds-success-solid)", + onSolid: "var(--ds-on-success-solid)", + + contrastTextChannel: "var(--ds-on-success-channel)", + mainChannel: "var(--ds-success-mainChannel)", + lightChannel: "var(--ds-success-lightChannel)", + darkChannel: "var(--ds-success-darkChannel)", + }, + + info: { + light: "var(--ds-info-accent)", + main: "var(--ds-info)", + dark: "var(--ds-info-emphasis)", + contrastText: "var(--ds-on-info)", + container: "var(--ds-info-container)", + onContainer: "var(--ds-on-info-container)", + solid: "var(--ds-info-solid)", + onSolid: "var(--ds-on-info-solid)", + + contrastTextChannel: "var(--ds-on-info-channel)", + mainChannel: "var(--ds-info-mainChannel)", + lightChannel: "var(--ds-info-lightChannel)", + darkChannel: "var(--ds-info-darkChannel)", + }, + + grey: { + 50: "#F8F8FA", + 100: "#EEF1F5", + 200: "#E6E9F0", + 300: "#DDE1E8", + 400: "#BCC2CD", + 500: "#A5ACB8", + 600: "#8A90A0", + 700: "#505563", + 800: "#2C3140", + 900: "#1A1C23", + }, + }, + + components: { + MuiPaper: { + styleOverrides: { + root: { + backgroundImage: 'none', + }, + }, + }, + + MuiButtonBase: { + defaultProps: { + disableRipple: true, + disableTouchRipple: true, + focusRipple: false, + }, + }, + + MuiButton: { + defaultProps: { + disableFocusRipple: true, + }, + styleOverrides: { + root: ({ + ownerState, + theme, + }: { + ownerState: ButtonProps; + theme: Theme; + }): CSSObject => { + const base: CSSObject = { + textTransform: "none", + boxShadow: "none", + }; + + const variant = ownerState.variant ?? "text"; + const rawColour = ownerState.color ?? "primary"; + + if (rawColour === "inherit") { + return { + ...base, + ...getFocusOutline(), + }; + } + + const colour = rawColour as IntentColour; + const p = getIntentPalette(theme, colour); + const focusToken = getFocusToken(colour); + const subtle = p.container; + const onSubtle = p.onContainer; + + if (variant === "contained") { + return { + ...base, + ...getFocusOutline(focusToken), + backgroundColor: p.solid ?? p.main, + color: p.onSolid ?? "var(--ds-on-solid)", + "&:hover": { + backgroundColor: p.solid ?? p.main, + boxShadow: getOverlayInset("var(--ds-overlay-hover-solid)"), + }, + "&:active": { + backgroundColor: p.solid ?? p.main, + boxShadow: getOverlayInset("var(--ds-overlay-selected)"), + }, + "&.Mui-focusVisible": { + outline: "var(--ds-focus-ring-width) solid", + outlineColor: focusToken, + outlineOffset: "var(--ds-focus-ring-offset)", + boxShadow: getOverlayInset("var(--ds-overlay-focus)"), + }, + "&.Mui-disabled": { + opacity: 1, + backgroundColor: "var(--ds-surface-disabled)", + color: "var(--ds-on-surface-disabled)", + boxShadow: "none", + }, + }; + } + + if (variant === "outlined") { + return { + ...base, + ...getFocusOutline(focusToken), + + color: onSubtle, + backgroundColor: subtle, + + "&:hover": { + backgroundColor: subtle, + boxShadow: getOverlayInset(), + }, + + "&:active": { + backgroundColor: subtle, + boxShadow: getOverlayInset("var(--ds-overlay-selected)"), + }, + + "&.Mui-disabled": { + opacity: 1, + backgroundColor: "transparent", + color: "var(--ds-on-surface-disabled)", + boxShadow: "none", + }, + }; + } + + if (variant === "text") { + return { + ...base, + ...getFocusOutline(focusToken), + color: p.main, + "&:hover": { + backgroundColor: subtle, + boxShadow: getOverlayInset(), + }, + }; + } + + return { + ...base, + ...getFocusOutline(focusToken), + }; + }, + }, + }, + + MuiIconButton: { + defaultProps: { + disableRipple: true, + disableFocusRipple: true, + }, + styleOverrides: { + root: ({ + ownerState, + theme, + }: { + ownerState: { color?: "inherit" | IntentColour | "default" }; + theme: Theme; + }): CSSObject => { + const rawColour = ownerState.color ?? "default"; + + if (rawColour === "inherit" || rawColour === "default") { + return { + "&:hover": { + boxShadow: getOverlayInset(), + }, + ...getFocusOutline(), + }; + } + + const colour = rawColour as IntentColour; + const p = getIntentPalette(theme, colour); + const subtle = p.container; + const focusToken = getFocusToken(colour); + + return { + color: p.main, + "&:hover": { + backgroundColor: subtle, + boxShadow: getOverlayInset(), + }, + ...getFocusOutline(focusToken), + }; + }, + }, + }, + + MuiToggleButton: { + styleOverrides: { + root: ({ theme }) => ({ + textTransform: "none", + border: `1px solid ${theme.palette.borders.base}`, + "&:hover": { + borderColor: theme.palette.borders.emphasis, + }, + }), + }, + }, + + MuiChip: { + styleOverrides: { + root: ({ + ownerState, + theme, + }: { + ownerState: ChipProps; + theme: Theme; + }): CSSObject => { + const base: CSSObject = { + "& .MuiChip-icon": { + color: "currentColor", + }, + }; + + const rawColour = ownerState.color ?? "default"; + const isDefault = rawColour === "default"; + const isOutlined = ownerState.variant === "outlined"; + const isInteractive = !!(ownerState.clickable || ownerState.onDelete); + + if (isDefault) { + return { + ...base, + ...(isInteractive ? getFocusOutline() : {}), + + color: "var(--ds-on-surface)", + borderColor: "var(--ds-border)", + backgroundColor: "var(--ds-surface-container-high)", + + ...(isInteractive && { + "&:hover": { + backgroundColor: "var(--ds-surface-container-high)", + boxShadow: getOverlayInset(), + }, + + "&:active": { + backgroundColor: "var(--ds-surface-container-high)", + boxShadow: getOverlayInset("var(--ds-overlay-selected)"), + }, + + "&&.MuiChip-clickable.Mui-focusVisible, &&.MuiChip-deletable.Mui-focusVisible": + { + backgroundColor: "var(--ds-surface-container-high)", + boxShadow: getOverlayInset("var(--ds-overlay-focus)"), + }, + + "&&.MuiChip-clickable.Mui-focusVisible:hover, &&.MuiChip-deletable.Mui-focusVisible:hover": + { + backgroundColor: "var(--ds-surface-container-high)", + boxShadow: getOverlayInset("var(--ds-overlay-focus)"), + }, + }), + }; + } + + const colour = rawColour as IntentColour; + const p = getIntentPalette(theme, colour); + const focusToken = getFocusToken(colour); + + if (isOutlined) { + const subtle = p.container; + const onSubtle = p.onContainer; + + return { + ...base, + ...(isInteractive ? getFocusOutline(focusToken) : {}), + + color: onSubtle, + borderColor: p.light, + backgroundColor: subtle, + + ...(isInteractive && { + "&:hover": { + backgroundColor: subtle, + borderColor: p.light, + boxShadow: getOverlayInset(), + }, + + "&:active": { + backgroundColor: subtle, + borderColor: p.light, + boxShadow: getOverlayInset("var(--ds-overlay-selected)"), + }, + + "&&.MuiChip-clickable.Mui-focusVisible, &&.MuiChip-deletable.Mui-focusVisible": + { + backgroundColor: subtle, + borderColor: p.light, + boxShadow: getOverlayInset("var(--ds-overlay-focus)"), + }, + + "&&.MuiChip-clickable.Mui-focusVisible:hover, &&.MuiChip-deletable.Mui-focusVisible:hover": + { + backgroundColor: subtle, + borderColor: p.light, + boxShadow: getOverlayInset("var(--ds-overlay-focus)"), + }, + }), + }; + } + + const solid = p.solid ?? p.main; + + return { + ...base, + ...(isInteractive ? getFocusOutline(focusToken) : {}), + + color: p.onSolid ?? "var(--ds-on-solid)", + backgroundColor: solid, + + ...(isInteractive && { + "&:hover": { + backgroundColor: solid, + boxShadow: getOverlayInset("var(--ds-overlay-hover-solid)"), + }, + + "&:active": { + backgroundColor: solid, + boxShadow: getOverlayInset("var(--ds-overlay-selected)"), + }, + + "&&.MuiChip-clickable.Mui-focusVisible, &&.MuiChip-deletable.Mui-focusVisible": + { + backgroundColor: solid, + boxShadow: getOverlayInset("var(--ds-overlay-focus)"), + }, + + "&&.MuiChip-clickable.Mui-focusVisible:hover, &&.MuiChip-deletable.Mui-focusVisible:hover": + { + backgroundColor: solid, + boxShadow: getOverlayInset("var(--ds-overlay-focus)"), + }, + }), + }; + }, + }, + }, + + MuiInputBase: { + styleOverrides: { + input: ({ theme }: ThemeOnlyArgs) => ({ + "&::placeholder": { + color: theme.palette.text.placeholder, + opacity: 1, + }, + "&::-webkit-input-placeholder": { + color: theme.palette.text.placeholder, + opacity: 1, + }, + "&::-moz-placeholder": { + color: theme.palette.text.placeholder, + opacity: 1, + }, + "&:focus::placeholder": { + color: theme.palette.text.placeholderFocus, + }, + "&:focus::-webkit-input-placeholder": { + color: theme.palette.text.placeholderFocus, + opacity: 1, + }, + "&:focus::-moz-placeholder": { + color: theme.palette.text.placeholderFocus, + opacity: 1, + }, + }), + + root: ({ theme }: ThemeOnlyArgs) => ({ + "&.Mui-error input::placeholder, &.Mui-error input::-webkit-input-placeholder, &.Mui-error input::-moz-placeholder": + { + color: theme.palette.error.light, + opacity: 1, + }, + "&.Mui-disabled input::placeholder, &.Mui-disabled input::-webkit-input-placeholder, &.Mui-disabled input::-moz-placeholder": + { + color: theme.palette.text.disabled, + opacity: 1, + }, + }), + }, + }, + + MuiOutlinedInput: { + styleOverrides: { + root: ({ ownerState, theme }: OverrideArgs) => { + const colour = (ownerState.color ?? "primary") as IntentColour; + const p = getIntentPalette(theme, colour); + const focusToken = getFocusToken(colour); + + return { + "& .MuiOutlinedInput-notchedOutline": { + borderColor: theme.palette.borders.base, + }, + + "&:hover:not(.Mui-disabled):not(.Mui-error):not(.Mui-focused) .MuiOutlinedInput-notchedOutline": + { + borderColor: theme.palette.borders.emphasis, + }, + + "&.Mui-focused:not(.Mui-disabled):not(.Mui-error) .MuiOutlinedInput-notchedOutline": + { + borderColor: p.light, + borderWidth: 2, + }, + + "&.Mui-focused:hover:not(.Mui-disabled):not(.Mui-error) .MuiOutlinedInput-notchedOutline": + { + borderColor: p.light, + borderWidth: 2, + }, + + "&.Mui-error .MuiOutlinedInput-notchedOutline": { + borderColor: theme.palette.error.light, + }, + + "&.Mui-error:hover:not(.Mui-disabled):not(.Mui-focused) .MuiOutlinedInput-notchedOutline": + { + borderColor: theme.palette.error.light, + }, + + "&.Mui-error.Mui-focused .MuiOutlinedInput-notchedOutline": { + borderColor: theme.palette.error.light, + borderWidth: 2, + }, + + "&.Mui-focusVisible": { + outline: "var(--ds-focus-ring-width) solid", + outlineColor: focusToken, + outlineOffset: "var(--ds-focus-ring-offset)", + }, + + "&.Mui-disabled .MuiOutlinedInput-notchedOutline": { + borderColor: "var(--ds-border-subtle)", + }, + }; + }, + }, + }, + + MuiInputLabel: { + styleOverrides: { + root: ({ theme }: ThemeOnlyArgs) => ({ + "&:not(.MuiInputLabel-shrink)": { + color: theme.palette.text.secondary, + }, + + "&.Mui-disabled:not(.MuiInputLabel-shrink)": { + color: theme.palette.text.disabled, + }, + + "&.Mui-focused": { + color: theme.palette.primary.main, + }, + + "&.Mui-focused.MuiFormLabel-colorSecondary": { + color: theme.palette.secondary.main, + }, + "&.Mui-focused.MuiFormLabel-colorSuccess": { + color: theme.palette.success.main, + }, + "&.Mui-focused.MuiFormLabel-colorWarning": { + color: theme.palette.warning.main, + }, + "&.Mui-focused.MuiFormLabel-colorError": { + color: theme.palette.error.main, + }, + "&.Mui-focused.MuiFormLabel-colorInfo": { + color: theme.palette.info.main, + }, + + "&.Mui-focused.Mui-error": { + color: theme.palette.error.main, + }, + + "&.Mui-disabled": { + color: theme.palette.text.disabled, + }, + }), + }, + }, + + MuiTab: { + styleOverrides: { + root: ({ theme }: OverrideArgs): CSSObject => ({ + textTransform: "none", + color: theme.palette.text.secondary, + fontWeight: 500, + minHeight: 44, + + "&:hover": { + color: theme.palette.text.primary, + boxShadow: getOverlayInset(), + }, + + "&.Mui-selected": { + color: theme.palette.primary.main, + fontWeight: 600, + }, + + "&.Mui-disabled": { + color: theme.palette.text.disabled, + }, + + "&.Mui-focusVisible, &:focus-visible": { + outline: "var(--ds-focus-ring-width) solid var(--ds-focus-ring)", + outlineOffset: "-2px", + }, + }), + }, + }, + + MuiAlert: { + styleOverrides: { + root: ({ + ownerState, + theme, + }: { + ownerState: AlertProps; + theme: Theme; + }): CSSObject => { + const severity = (ownerState.severity ?? "success") as IntentColour; + const mappedSeverity = severity === "error" ? "error" : severity; + const p = getIntentPalette(theme, mappedSeverity); + const subtle = p.container; + const onSubtle = p.onContainer; + + const common: CSSObject = { + borderRadius: 8, + alignItems: "flex-start", + "& .MuiAlert-icon": { + color: "currentColor", + opacity: 1, + }, + "& .MuiAlert-action": { + color: "inherit", + "& .MuiIconButton-root:hover": { + boxShadow: getOverlayInset(), + }, + }, + }; + + if (ownerState.variant === "filled") { + return { + ...common, + backgroundColor: p.solid ?? p.main, + color: p.onSolid ?? "var(--ds-on-solid)", + }; + } + + if (ownerState.variant === "outlined") { + return { + ...common, + backgroundColor: subtle, + color: onSubtle, + border: `1px solid ${p.light}`, + }; + } + + return { + ...common, + backgroundColor: subtle, + color: onSubtle, + border: "1px solid var(--ds-border-subtle)", + }; + }, + }, + }, + + MuiLinearProgress: { + styleOverrides: { + root: { + height: 6, + borderRadius: 999, + overflow: "hidden", + backgroundColor: "var(--ds-surface-container-high)", + }, + + bar: ({ + ownerState, + theme, + }: OverrideArgs): CSSObject => { + const colour = (ownerState.color ?? "primary") as IntentColour; + const p = getIntentPalette(theme, colour); + + return { + backgroundColor: p.main, + }; + }, + }, + }, + + MuiCircularProgress: { + styleOverrides: { + root: ({ + ownerState, + theme, + }: OverrideArgs): CSSObject => { + const colour = (ownerState.color ?? "primary") as IntentColour; + const p = getIntentPalette(theme, colour); + + return { + color: p.main, + }; + }, + }, + }, + + MuiSkeleton: { + styleOverrides: { + root: { + backgroundColor: "var(--ds-surface-container-high)", + }, + + wave: { + backgroundColor: "var(--ds-surface-container-high)", + position: "relative", + overflow: "hidden", + + "&::after": { + content: '""', + position: "absolute", + inset: 0, + transform: "translateX(-100%)", + backgroundImage: + "linear-gradient(90deg, transparent, var(--ds-overlay-hover), transparent)", + }, + }, + }, + }, + + MuiSnackbar: { + styleOverrides: { + root: { + "& .MuiSnackbarContent-root, & .MuiAlert-root": { + minWidth: 320, + maxWidth: 560, + }, + }, + }, + }, + + MuiSnackbarContent: { + styleOverrides: { + root: { + backgroundColor: "var(--ds-surface-container)", + color: "var(--ds-on-surface)", + border: "1px solid var(--ds-border-subtle)", + borderRadius: 8, + }, + + message: { + padding: "8px 0", + }, + + action: { + color: "inherit", + + "& .MuiIconButton-root:hover": { + boxShadow: getOverlayInset(), + }, + }, + }, + }, + + MuiCheckbox: { + defaultProps: { + disableRipple: true, + }, + styleOverrides: { + root: ({ + ownerState, + theme, + }: { + ownerState: CheckboxProps; + theme: Theme; + }): CSSObject => { + const rawColour = ownerState.color ?? "primary"; + const isDefault = rawColour === "default"; + const p = !isDefault + ? getIntentPalette(theme, rawColour as IntentColour) + : null; + const focusToken = !isDefault + ? getFocusToken(rawColour as IntentColour) + : undefined; + + return { + color: "var(--ds-on-surface-variant)", + borderRadius: 8, + + "&:hover": { + backgroundColor: "var(--ds-overlay-hover)", + }, + + ...getFocusOutline(focusToken), + + "&.Mui-checked": { + color: isDefault ? "var(--ds-on-surface)" : p?.main, + }, + + "&.MuiCheckbox-indeterminate": { + color: isDefault ? "var(--ds-on-surface)" : p?.main, + }, + + "&.Mui-disabled": { + color: "var(--ds-action-disabled)", + }, + }; + }, + }, + }, + + MuiRadio: { + defaultProps: { + disableRipple: true, + }, + styleOverrides: { + root: ({ + ownerState, + theme, + }: { + ownerState: RadioProps; + theme: Theme; + }): CSSObject => { + const rawColour = ownerState.color ?? "primary"; + const isDefault = rawColour === "default"; + const colour = rawColour as IntentColour; + + const p = !isDefault ? getIntentPalette(theme, colour) : null; + const focusToken = !isDefault ? getFocusToken(colour) : undefined; + + return { + color: "var(--ds-on-surface-variant)", + borderRadius: "50%", + + "&:hover": { + backgroundColor: "var(--ds-overlay-hover)", + }, + + ...getFocusOutline(focusToken), + + "&.Mui-checked": { + color: isDefault ? "var(--ds-on-surface)" : p?.main, + }, + + "&.Mui-disabled": { + color: "var(--ds-action-disabled)", + }, + }; + }, + }, + }, + + }, + }); + + return createTheme(DiamondDSThemeOptions); +}; + +// Convenience exports — derive from the factory so they stay in sync. +export const DiamondDSTheme = createDiamondTheme("light"); +export const DiamondDSThemeDark = createDiamondTheme("dark"); + +// Keep the old export name as an alias for backwards compatibility. +export const createMuiTheme = createDiamondTheme; \ No newline at end of file diff --git a/src/themes/ThemeProvider.tsx b/src/themes/ThemeProvider.tsx index a4badc84..56d306b1 100644 --- a/src/themes/ThemeProvider.tsx +++ b/src/themes/ThemeProvider.tsx @@ -1,26 +1,37 @@ -import { ThemeProvider as MuiThemeProvider } from "@mui/material/styles"; +import React, { useLayoutEffect, useMemo } from "react"; import { CssBaseline } from "@mui/material"; -import { GenericTheme } from "./GenericTheme"; -import { ThemeProviderProps as MuiThemeProviderProps } from "@mui/material/styles"; +import { ThemeProvider as MuiThemeProvider } from "@mui/material/styles"; +import type { ThemeProviderProps as MuiThemeProviderProps } from "@mui/material/styles"; +import { createMuiTheme } from "./DiamondDSTheme"; +import type { DSMode } from "./DiamondDSTheme"; interface ThemeProviderProps extends Partial { baseline?: boolean; + mode?: DSMode; // 'light' | 'dark' (adding 'system' for future use) } -const ThemeProvider = function ({ +export function ThemeProvider({ children, - theme = GenericTheme, baseline = true, defaultMode = "system", + mode = "light", // default to light mode (for now) ...props }: ThemeProviderProps) { + useLayoutEffect(() => { + const root = document.documentElement; + + root.setAttribute("data-mode", mode); + root.classList.toggle("dark", mode === "dark"); + root.classList.toggle("light", mode === "light"); + root.style.colorScheme = mode; + }, [mode]); + + const theme = useMemo(() => createMuiTheme(mode), [mode]); + return ( {baseline && } {children} ); -}; - -export { ThemeProvider }; -export type { ThemeProviderProps }; +}