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