/** * FA Design System — MUI Theme * * Maps design tokens to MUI's theme structure. All values come from the * token pipeline: Token JSON → Style Dictionary → generated/tokens.js → here. * * To update colours/typography/spacing: edit tokens/ JSON, run `npm run build:tokens`, * then update references here if token names changed. */ import { createTheme } from '@mui/material/styles'; import type { CSSProperties } from 'react'; import * as t from './generated/tokens.js'; /* ---------- Types ---------- */ type StyleWithMedia = CSSProperties & { [key: `@media ${string}`]: CSSProperties; }; /* ---------- Custom typography variant declarations ---------- */ declare module '@mui/material/styles' { interface TypographyVariants { displayHero: StyleWithMedia; display1: StyleWithMedia; display2: StyleWithMedia; display3: StyleWithMedia; displaySm: StyleWithMedia; bodyLg: CSSProperties; bodyXs: CSSProperties; labelLg: CSSProperties; label: CSSProperties; labelSm: CSSProperties; captionSm: CSSProperties; overlineSm: CSSProperties; } interface TypographyVariantsOptions { displayHero?: StyleWithMedia; display1?: StyleWithMedia; display2?: StyleWithMedia; display3?: StyleWithMedia; displaySm?: StyleWithMedia; bodyLg?: CSSProperties; bodyXs?: CSSProperties; labelLg?: CSSProperties; label?: CSSProperties; labelSm?: CSSProperties; captionSm?: CSSProperties; overlineSm?: CSSProperties; } } declare module '@mui/material/Typography' { interface TypographyPropsVariantOverrides { displayHero: true; display1: true; display2: true; display3: true; displaySm: true; bodyLg: true; bodyXs: true; labelLg: true; label: true; labelSm: true; captionSm: true; overlineSm: true; // Disable old aliases display: false; bodyLarge: false; } } /* ---------- Custom Button declarations ---------- */ declare module '@mui/material/Button' { interface ButtonPropsSizeOverrides { xs: true; } interface ButtonPropsVariantOverrides { soft: true; } } /* ---------- Theme ---------- */ const MOBILE = '@media (max-width:600px)'; export const theme = createTheme({ palette: { primary: { main: t.ColorInteractiveDefault, dark: t.ColorInteractiveActive, light: t.ColorBrand400, contrastText: t.ColorTextInverse, }, secondary: { main: t.ColorNeutral600, dark: t.ColorNeutral700, light: t.ColorNeutral300, contrastText: t.ColorTextInverse, }, error: { main: t.ColorFeedbackError, light: t.ColorRed400, dark: t.ColorRed800, contrastText: t.ColorTextInverse, }, warning: { main: t.ColorFeedbackWarning, light: t.ColorAmber400, dark: t.ColorAmber800, contrastText: t.ColorTextInverse, }, success: { main: t.ColorFeedbackSuccess, light: t.ColorGreen400, dark: t.ColorGreen800, contrastText: t.ColorTextInverse, }, info: { main: t.ColorFeedbackInfo, light: t.ColorBlue400, dark: t.ColorBlue800, contrastText: t.ColorTextInverse, }, text: { primary: t.ColorTextPrimary, secondary: t.ColorTextSecondary, disabled: t.ColorTextDisabled, }, background: { default: t.ColorSurfaceDefault, paper: t.ColorSurfaceRaised, }, divider: t.ColorBorderDefault, action: { disabled: t.ColorTextDisabled, disabledBackground: t.ColorInteractiveDisabled, }, }, typography: { fontFamily: t.FontFamilyBody, /* ── Display (Noto Serif SC, Regular 400) ── */ displayHero: { fontFamily: t.FontFamilyDisplay, fontSize: t.TypographyDisplayHeroFontSize, fontWeight: t.TypographyDisplayHeroFontWeight, lineHeight: t.TypographyDisplayHeroLineHeight, letterSpacing: t.TypographyDisplayHeroLetterSpacing, [MOBILE]: { fontSize: t.TypographyDisplayHeroFontSizeMobile }, }, display1: { fontFamily: t.FontFamilyDisplay, fontSize: t.TypographyDisplay1FontSize, fontWeight: t.TypographyDisplay1FontWeight, lineHeight: t.TypographyDisplay1LineHeight, letterSpacing: t.TypographyDisplay1LetterSpacing, [MOBILE]: { fontSize: t.TypographyDisplay1FontSizeMobile }, }, display2: { fontFamily: t.FontFamilyDisplay, fontSize: t.TypographyDisplay2FontSize, fontWeight: t.TypographyDisplay2FontWeight, lineHeight: t.TypographyDisplay2LineHeight, letterSpacing: t.TypographyDisplay2LetterSpacing, [MOBILE]: { fontSize: t.TypographyDisplay2FontSizeMobile }, }, display3: { fontFamily: t.FontFamilyDisplay, fontSize: t.TypographyDisplay3FontSize, fontWeight: t.TypographyDisplay3FontWeight, lineHeight: t.TypographyDisplay3LineHeight, letterSpacing: t.TypographyDisplay3LetterSpacing, [MOBILE]: { fontSize: t.TypographyDisplay3FontSizeMobile }, }, displaySm: { fontFamily: t.FontFamilyDisplay, fontSize: t.TypographyDisplaySmFontSize, fontWeight: t.TypographyDisplaySmFontWeight, lineHeight: t.TypographyDisplaySmLineHeight, letterSpacing: t.TypographyDisplaySmLetterSpacing, [MOBILE]: { fontSize: t.TypographyDisplaySmFontSizeMobile }, }, /* ── Headings (Montserrat, Bold 700) ── */ h1: { fontFamily: t.FontFamilyBody, fontSize: t.TypographyH1FontSize, fontWeight: t.TypographyH1FontWeight, lineHeight: t.TypographyH1LineHeight, letterSpacing: t.TypographyH1LetterSpacing, [MOBILE]: { fontSize: t.TypographyH1FontSizeMobile }, }, h2: { fontFamily: t.FontFamilyBody, fontSize: t.TypographyH2FontSize, fontWeight: t.TypographyH2FontWeight, lineHeight: t.TypographyH2LineHeight, letterSpacing: t.TypographyH2LetterSpacing, [MOBILE]: { fontSize: t.TypographyH2FontSizeMobile }, }, h3: { fontFamily: t.FontFamilyBody, fontSize: t.TypographyH3FontSize, fontWeight: t.TypographyH3FontWeight, lineHeight: t.TypographyH3LineHeight, letterSpacing: t.TypographyH3LetterSpacing, [MOBILE]: { fontSize: t.TypographyH3FontSizeMobile }, }, h4: { fontFamily: t.FontFamilyBody, fontSize: t.TypographyH4FontSize, fontWeight: t.TypographyH4FontWeight, lineHeight: t.TypographyH4LineHeight, letterSpacing: t.TypographyH4LetterSpacing, [MOBILE]: { fontSize: t.TypographyH4FontSizeMobile }, }, h5: { fontFamily: t.FontFamilyBody, fontSize: t.TypographyH5FontSize, fontWeight: t.TypographyH5FontWeight, lineHeight: t.TypographyH5LineHeight, letterSpacing: t.TypographyH5LetterSpacing, [MOBILE]: { fontSize: t.TypographyH5FontSizeMobile }, }, h6: { fontFamily: t.FontFamilyBody, fontSize: t.TypographyH6FontSize, fontWeight: t.TypographyH6FontWeight, lineHeight: t.TypographyH6LineHeight, letterSpacing: t.TypographyH6LetterSpacing, [MOBILE]: { fontSize: t.TypographyH6FontSizeMobile }, }, /* ── Body (Montserrat, Medium 500) ── */ bodyLg: { fontFamily: t.FontFamilyBody, fontSize: t.TypographyBodyLgFontSize, fontWeight: t.TypographyBodyLgFontWeight, lineHeight: t.TypographyBodyLgLineHeight, letterSpacing: t.TypographyBodyLgLetterSpacing, }, body1: { fontSize: t.TypographyBodyFontSize, fontWeight: t.TypographyBodyFontWeight, lineHeight: t.TypographyBodyLineHeight, letterSpacing: t.TypographyBodyLetterSpacing, }, body2: { fontSize: t.TypographyBodySmFontSize, fontWeight: t.TypographyBodySmFontWeight, lineHeight: t.TypographyBodySmLineHeight, letterSpacing: t.TypographyBodySmLetterSpacing, }, bodyXs: { fontFamily: t.FontFamilyBody, fontSize: t.TypographyBodyXsFontSize, fontWeight: t.TypographyBodyXsFontWeight, lineHeight: t.TypographyBodyXsLineHeight, letterSpacing: t.TypographyBodyXsLetterSpacing, }, /* ── Subtitle (maps to body variants for MUI compat) ── */ subtitle1: { fontSize: t.TypographyBodyLgFontSize, fontWeight: t.TypographyBodyLgFontWeight, lineHeight: t.TypographyBodyLgLineHeight, }, subtitle2: { fontSize: t.TypographyLabelFontSize, fontWeight: t.TypographyLabelFontWeight, lineHeight: t.TypographyLabelLineHeight, letterSpacing: t.TypographyLabelLetterSpacing, }, /* ── Label (Montserrat, Medium 500) ── */ labelLg: { fontFamily: t.FontFamilyBody, fontSize: t.TypographyLabelLgFontSize, fontWeight: t.TypographyLabelLgFontWeight, lineHeight: t.TypographyLabelLgLineHeight, letterSpacing: t.TypographyLabelLgLetterSpacing, }, label: { fontFamily: t.FontFamilyBody, fontSize: t.TypographyLabelFontSize, fontWeight: t.TypographyLabelFontWeight, lineHeight: t.TypographyLabelLineHeight, letterSpacing: t.TypographyLabelLetterSpacing, }, labelSm: { fontFamily: t.FontFamilyBody, fontSize: t.TypographyLabelSmFontSize, fontWeight: t.TypographyLabelSmFontWeight, lineHeight: t.TypographyLabelSmLineHeight, letterSpacing: t.TypographyLabelSmLetterSpacing, }, /* ── Caption (Montserrat, Regular 400) ── */ caption: { fontSize: t.TypographyCaptionFontSize, fontWeight: t.TypographyCaptionFontWeight, lineHeight: t.TypographyCaptionLineHeight, letterSpacing: t.TypographyCaptionLetterSpacing, }, captionSm: { fontFamily: t.FontFamilyBody, fontSize: t.TypographyCaptionSmFontSize, fontWeight: t.TypographyCaptionSmFontWeight, lineHeight: t.TypographyCaptionSmLineHeight, letterSpacing: t.TypographyCaptionSmLetterSpacing, }, /* ── Overline (Montserrat, SemiBold 600, uppercase) ── */ overline: { fontSize: t.TypographyOverlineFontSize, fontWeight: t.TypographyOverlineFontWeight, lineHeight: t.TypographyOverlineLineHeight, letterSpacing: t.TypographyOverlineLetterSpacing, textTransform: 'uppercase' as const, }, overlineSm: { fontFamily: t.FontFamilyBody, fontSize: t.TypographyOverlineSmFontSize, fontWeight: t.TypographyOverlineSmFontWeight, lineHeight: t.TypographyOverlineSmLineHeight, letterSpacing: t.TypographyOverlineSmLetterSpacing, textTransform: 'uppercase' as const, }, /* ── Button text ── */ button: { fontWeight: 600, letterSpacing: '0.02em', textTransform: 'none' as const, }, }, spacing: 4, shape: { borderRadius: parseInt(t.BorderRadiusMd, 10), }, components: { MuiButton: { defaultProps: { disableElevation: true, }, styleOverrides: { root: { borderRadius: parseInt(t.ButtonBorderRadiusDefault, 10), textTransform: 'none' as const, fontWeight: 600, letterSpacing: '0.02em', transition: 'background-color 150ms ease-in-out, border-color 150ms ease-in-out, box-shadow 150ms ease-in-out', '&:focus-visible': { outline: `2px solid ${t.ColorInteractiveFocus}`, outlineOffset: '2px', }, }, sizeSmall: { minHeight: parseInt(t.ButtonHeightSm, 10), padding: `${t.ButtonPaddingYSm} ${t.ButtonPaddingXSm}`, fontSize: t.ButtonFontSizeSm, '& .MuiButton-startIcon': { marginRight: t.ButtonIconGapSm }, '& .MuiButton-endIcon': { marginLeft: t.ButtonIconGapSm }, '& .MuiButton-startIcon > *:nth-of-type(1), & .MuiButton-endIcon > *:nth-of-type(1)': { fontSize: t.ButtonIconSizeSm, }, }, sizeMedium: { minHeight: parseInt(t.ButtonHeightMd, 10), padding: `${t.ButtonPaddingYMd} ${t.ButtonPaddingXMd}`, fontSize: t.ButtonFontSizeMd, '& .MuiButton-startIcon': { marginRight: t.ButtonIconGapMd }, '& .MuiButton-endIcon': { marginLeft: t.ButtonIconGapMd }, '& .MuiButton-startIcon > *:nth-of-type(1), & .MuiButton-endIcon > *:nth-of-type(1)': { fontSize: t.ButtonIconSizeMd, }, }, sizeLarge: { minHeight: parseInt(t.ButtonHeightLg, 10), padding: `${t.ButtonPaddingYLg} ${t.ButtonPaddingXLg}`, fontSize: t.ButtonFontSizeLg, '& .MuiButton-startIcon': { marginRight: t.ButtonIconGapLg }, '& .MuiButton-endIcon': { marginLeft: t.ButtonIconGapLg }, '& .MuiButton-startIcon > *:nth-of-type(1), & .MuiButton-endIcon > *:nth-of-type(1)': { fontSize: t.ButtonIconSizeLg, }, }, containedPrimary: { '&:hover': { backgroundColor: t.ColorInteractiveHover }, '&:active': { backgroundColor: t.ColorInteractiveActive }, }, containedSecondary: { '&:hover': { backgroundColor: t.ColorNeutral700 }, }, outlinedPrimary: { borderColor: t.ColorInteractiveDefault, color: t.ColorInteractiveDefault, '&:hover': { borderColor: t.ColorInteractiveHover, color: t.ColorInteractiveHover, backgroundColor: t.ColorBrand100, }, '&:active': { borderColor: t.ColorInteractiveActive, color: t.ColorInteractiveActive, }, }, outlinedSecondary: { borderColor: t.ColorNeutral400, color: t.ColorNeutral600, '&:hover': { borderColor: t.ColorNeutral600, color: t.ColorNeutral700, backgroundColor: t.ColorNeutral200, }, }, textPrimary: { color: t.ColorInteractiveDefault, '&:hover': { color: t.ColorInteractiveHover, backgroundColor: t.ColorBrand100, }, '&:active': { color: t.ColorInteractiveActive, }, }, textSecondary: { color: t.ColorNeutral600, '&:hover': { color: t.ColorNeutral700, backgroundColor: t.ColorNeutral200, }, }, }, variants: [ { props: { size: 'xs' as const }, style: { minHeight: parseInt(t.ButtonHeightXs, 10), padding: `${t.ButtonPaddingYXs} ${t.ButtonPaddingXXs}`, fontSize: t.ButtonFontSizeXs, '& .MuiButton-startIcon': { marginRight: t.ButtonIconGapXs }, '& .MuiButton-endIcon': { marginLeft: t.ButtonIconGapXs }, '& .MuiButton-startIcon > *:nth-of-type(1), & .MuiButton-endIcon > *:nth-of-type(1)': { fontSize: t.ButtonIconSizeXs, }, }, }, { props: { variant: 'soft' as const, color: 'primary' as const }, style: { backgroundColor: t.ColorBrand200, color: t.ColorInteractiveHover, '&:hover': { backgroundColor: t.ColorBrand300 }, '&:active': { backgroundColor: t.ColorBrand400 }, }, }, { props: { variant: 'soft' as const, color: 'secondary' as const }, style: { backgroundColor: t.ColorNeutral200, color: t.ColorNeutral700, '&:hover': { backgroundColor: t.ColorNeutral300 }, '&:active': { backgroundColor: t.ColorNeutral400, color: t.ColorNeutral700 }, }, }, ], }, MuiCard: { styleOverrides: { root: { borderRadius: parseInt(t.CardBorderRadiusDefault, 10), backgroundColor: t.CardBackgroundDefault, boxShadow: t.CardShadowDefault, // Reserve 2px border on ALL cards (transparent for elevated, coloured for outlined). // Prevents layout shift when toggling selected state. border: '2px solid transparent', transition: 'box-shadow 150ms ease-in-out, border-color 150ms ease-in-out, background-color 150ms ease-in-out', }, }, variants: [ { props: { variant: 'outlined' }, style: { boxShadow: 'none', borderColor: t.CardBorderDefault, }, }, ], }, MuiOutlinedInput: { styleOverrides: { root: { borderRadius: parseInt(t.InputBorderRadiusDefault, 10), transition: 'box-shadow 150ms ease-in-out', // Default (medium) height minHeight: parseInt(t.InputHeightMd, 10), // Small size '&.MuiInputBase-sizeSmall': { minHeight: parseInt(t.InputHeightSm, 10), }, // Default border '& .MuiOutlinedInput-notchedOutline': { borderColor: t.ColorNeutral300, transition: 'border-color 150ms ease-in-out', }, // Hover — darker border (skip when focused, error, or disabled) '&:hover:not(.Mui-focused):not(.Mui-error):not(.Mui-disabled) .MuiOutlinedInput-notchedOutline': { borderColor: t.ColorNeutral400, }, // Focus — brand gold border + double ring (white gap + brand ring) '&.Mui-focused .MuiOutlinedInput-notchedOutline': { borderColor: t.ColorBrand500, borderWidth: '1px', }, '&.Mui-focused': { boxShadow: `0 0 0 3px ${t.ColorWhite}, 0 0 0 5px ${t.ColorBrand500}`, }, // Error border '&.Mui-error .MuiOutlinedInput-notchedOutline': { borderColor: t.ColorFeedbackError, borderWidth: '1px', }, // Error + focused — error-coloured ring '&.Mui-error.Mui-focused': { boxShadow: `0 0 0 3px ${t.ColorWhite}, 0 0 0 5px ${t.ColorFeedbackError}`, }, // Disabled — muted background '&.Mui-disabled': { backgroundColor: t.ColorNeutral200, '& .MuiOutlinedInput-notchedOutline': { borderColor: t.ColorNeutral300, }, }, // Adornment icon sizing '& .MuiInputAdornment-root': { color: t.ColorNeutral400, '& .MuiSvgIcon-root': { fontSize: t.InputIconSizeDefault, }, }, }, input: { padding: `${t.InputPaddingYMd} ${t.InputPaddingXDefault}`, fontSize: t.InputFontSizeDefault, color: t.ColorTextPrimary, '&::placeholder': { color: t.ColorNeutral400, opacity: 1, }, // Small size padding '.MuiInputBase-sizeSmall &': { padding: `${t.InputPaddingYSm} ${t.InputPaddingXDefault}`, }, }, notchedOutline: { // Reset top offset — MUI defaults to -5px for floating label space. // We use external labels, so the border should be flush with the input. top: 0, // Hide the notch legend — we use external labels '& legend': { display: 'none', }, }, multiline: { padding: 0, '& .MuiOutlinedInput-input': { padding: `${t.InputPaddingYMd} ${t.InputPaddingXDefault}`, }, }, }, }, MuiChip: { defaultProps: { size: 'medium', }, styleOverrides: { root: { borderRadius: parseInt(t.ChipBorderRadiusDefault, 10), fontWeight: 500, letterSpacing: '0.01em', transition: 'background-color 150ms ease-in-out, border-color 150ms ease-in-out, box-shadow 150ms ease-in-out', '&:focus-visible': { outline: `2px solid ${t.ColorInteractiveFocus}`, outlineOffset: '2px', }, }, sizeMedium: { height: parseInt(t.ChipHeightMd, 10), fontSize: t.ChipFontSizeMd, '& .MuiChip-icon': { fontSize: t.ChipIconSizeMd, marginLeft: t.ChipPaddingXMd }, '& .MuiChip-deleteIcon': { fontSize: t.ChipDeleteIconSizeMd, marginRight: t.ChipPaddingXMd }, }, sizeSmall: { height: parseInt(t.ChipHeightSm, 10), fontSize: t.ChipFontSizeSm, '& .MuiChip-icon': { fontSize: t.ChipIconSizeSm, marginLeft: t.ChipPaddingXSm }, '& .MuiChip-deleteIcon': { fontSize: t.ChipDeleteIconSizeSm, marginRight: t.ChipPaddingXSm }, }, filled: { '&.MuiChip-colorDefault': { backgroundColor: t.ColorNeutral200, color: t.ColorNeutral700, '&:hover': { backgroundColor: t.ColorNeutral300 }, '& .MuiChip-deleteIcon': { color: t.ColorNeutral500, '&:hover': { color: t.ColorNeutral700 } }, }, '&.MuiChip-colorPrimary': { backgroundColor: t.ColorBrand200, color: t.ColorBrand700, '&:hover': { backgroundColor: t.ColorBrand300 }, '& .MuiChip-deleteIcon': { color: t.ColorBrand400, '&:hover': { color: t.ColorBrand700 } }, }, }, outlined: { '&.MuiChip-colorDefault': { borderColor: t.ColorNeutral300, color: t.ColorNeutral700, '&:hover': { backgroundColor: t.ColorNeutral100, borderColor: t.ColorNeutral400 }, '& .MuiChip-deleteIcon': { color: t.ColorNeutral400, '&:hover': { color: t.ColorNeutral700 } }, }, '&.MuiChip-colorPrimary': { borderColor: t.ColorBrand400, color: t.ColorBrand700, '&:hover': { backgroundColor: t.ColorBrand100, borderColor: t.ColorBrand500 }, '& .MuiChip-deleteIcon': { color: t.ColorBrand400, '&:hover': { color: t.ColorBrand700 } }, }, }, }, }, }, }); export default theme;