@codegouvfr/react-dsfr
Version:
French State Design System React integration library
463 lines (458 loc) • 22.1 kB
JavaScript
"use client";
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import React, { useMemo, useEffect } from "react";
import * as mui from "@mui/material/styles";
import { fr } from "../fr";
import { useIsDark } from "../useIsDark";
import { typography } from "../fr/generatedFromCss/typography";
import { spacingTokenByValue } from "../fr/generatedFromCss/spacing";
import { assert } from "tsafe/assert";
import { objectKeys } from "tsafe/objectKeys";
import { id } from "tsafe/id";
import { useBreakpointsValuesPx } from "../useBreakpointsValuesPx";
import { structuredCloneButFunctions } from "../tools/structuredCloneButFunctions";
import { deepAssign } from "../tools/deepAssign";
import { Global, css } from "@emotion/react";
import { getAssetUrl } from "../tools/getAssetUrl";
import { IsGovProvider } from "./useIsGov";
import { DisplayArtworkWhiteLabelProvider } from "../Display/Artwork/ArtworkWhiteLabel/DisplayArtworkWhiteLabelProvider";
import marianneFaviconSvgUrl from "../dsfr/favicon/favicon.svg";
import blankFaviconSvgUrl from "../assets/blank-favicon.svg";
export function getMuiDsfrThemeOptions(params) {
const { isDark, breakpointsValues } = params;
const { options, decisions } = fr.colors.getHex({ isDark });
return {
"shape": {
"borderRadius": 0
},
"breakpoints": {
//NOTE: We would use "unit": breakpointsValuesUnit but only "px" works.
"unit": "px",
"values": breakpointsValues
},
"palette": {
"mode": isDark ? "dark" : "light",
"primary": {
"main": options.blueFrance.sun113_625.default,
"light": options.blueFrance.sun113_625.active,
"dark": options.blueFrance.sun113_625.hover,
"contrastText": options.blueFrance._975sun113.default
},
"secondary": {
"main": options.blueFrance._950_100.default,
"light": options.blueFrance._950_100.active,
"dark": options.blueFrance._950_100.hover,
"contrastText": options.blueFrance.sun113_625.default
},
"error": {
"light": options.error._425_625.active,
"main": options.error._425_625.default,
"dark": options.error._425_625.hover,
"contrastText": options.grey._1000_50.default
},
"warning": {
"light": options.warning._425_625.default,
"main": options.warning._425_625.default,
"dark": options.warning._425_625.hover,
"contrastText": options.grey._1000_50.default
},
"info": {
"light": options.info._425_625.active,
"main": options.info._425_625.default,
"dark": options.info._425_625.hover,
"contrastText": options.grey._1000_50.default
},
"success": {
"light": options.success._425_625.active,
"main": options.success._425_625.default,
"dark": options.success._425_625.hover,
"contrastText": options.grey._1000_50.default
},
"text": {
"primary": options.grey._50_1000.default,
"secondary": options.grey._200_850.default,
"disabled": options.grey._625_425.default
},
"divider": options.grey._900_175.default,
"action": {
"active": options.grey._200_850.default,
"hover": options.grey._975_100.default,
"selected": options.blueFrance._925_125.active,
"disabled": options.grey._625_425.default,
"disabledBackground": options.grey._925_125.default,
"focus": options.blueFrance.sun113_625.active
},
"background": {
"default": options.grey._1000_50.default,
"paper": options.grey._1000_100.default
}
},
"typography": (() => {
const getBySelector = (selector) => {
const variant = typography.find(variant => variant.selector === selector);
assert(variant !== undefined);
const style = Object.assign({}, variant.style);
//TODO: Investigate why we need to do that.
delete style.margin;
return style;
};
return {
"fontFamily": '"Marianne", arial, sans-serif',
"h1": getBySelector("h1"),
"h2": getBySelector("h2"),
"h3": getBySelector("h3"),
"h4": getBySelector("h4"),
"h5": getBySelector("h5"),
"h6": getBySelector("h6"),
//"subtitle1":
//"subtitle2":
"body1": getBySelector("p")
//"body2": {},
//"caption": {},
//"button": {},
//"overline": {}
};
})(),
"spacing": (() => {
const values = [];
//NOTE: The properties are declared sorted in the object.
objectKeys(spacingTokenByValue).forEach(key => {
if (key.endsWith("w")) {
values.push(spacingTokenByValue[key]);
}
});
return (abs) => typeof abs === "string"
? abs
: abs === 0
? 0
: (() => {
const value = values[abs - 1];
return value === undefined ? abs : value;
})();
})(),
"shadows": (() => {
const [, , , , , , , , ...rest] = mui.createTheme().shadows;
return id([
"none",
/** ButtonBar shadow */
"0px 6px 10px 0px rgba(0,0,0,0.07)",
/** Explorer items */
"0px 4px 4px 0px rgba(0,0,0,0.1)",
/** LeftBar */
"6px 0px 16px 0px rgba(0,0,0,0.15)",
/** AccountTab default */
"4px 0px 10px 0px rgba(0,0,0,0.07)",
/** AccountTab active */
"-4px 0px 10px 0px rgba(0,0,0,0.07)",
/** Card over */
"0px 6px 10px 0px rgba(0,0,0,0.14)",
/** Dialog **/
"0px 8px 10px -7px rgba(0,0,0,0.07)",
...rest
]);
})(),
"components": Object.assign({ "MuiButton": {
"styleOverrides": {
"root": {
"textTransform": "unset"
}
}
}, "MuiSvgIcon": {
"styleOverrides": {
"colorPrimary": ({ theme }) => ({
"color": theme.palette.text.primary
})
}
}, "MuiStepIcon": {
"styleOverrides": {
"text": {
"fill": fr.colors.getHex({ "isDark": true }).decisions.text.title.grey
.default
}
}
}, "MuiTablePagination": {
"styleOverrides": {
"displayedRows": {
//Fixes: https://user-images.githubusercontent.com/6702424/206063347-65e7d13c-3dea-410c-a0e0-51cf214deba0.png
"margin": "unset"
},
"selectLabel": {
//Fixes: https://github.com/codegouvfr/react-dsfr/assets/6702424/678a7f69-d4e8-4897-85f0-65c605b46900
"margin": "unset"
}
}
}, "MuiTypography": {
"styleOverrides": {
"root": {
//Fixes double underline: https://user-images.githubusercontent.com/6702424/206064575-4f036145-ff40-47db-aabd-560f29136e71.png
"backgroundImage": "unset"
}
}
}, "MuiAutocomplete": {
"styleOverrides": {
"listbox": {
"padding": 0
},
"option": {
"padding": `${fr.spacing("2w")} !important`,
"&.Mui-focused": {
"backgroundColor": decisions.background.open.blueFrance.default + " !important"
},
"&.Mui-focusVisible": {
"backgroundColor": decisions.background.open.blueFrance.default + " !important"
}
}
}
} }, (() => {
const nonTypedMuiComponents = {
"MuiDataGrid": {
"styleOverrides": {
"root": (() => {
const set = new WeakSet();
const borderNone = {
"border": "none"
};
return (params) => {
const { ownerState } = params;
if (ownerState === undefined) {
return borderNone;
}
if (ownerState.getRowClassName === undefined ||
!set.has(ownerState.getRowClassName)) {
const originalGetRowClassName = ownerState.getRowClassName;
ownerState.getRowClassName = params => {
const { indexRelativeToCurrentPage } = params;
const parityClassName = indexRelativeToCurrentPage % 2 === 0
? "even"
: "odd";
const className = originalGetRowClassName === null || originalGetRowClassName === void 0 ? void 0 : originalGetRowClassName(params);
return className === undefined
? parityClassName
: `${parityClassName} ${className}`;
};
set.add(ownerState.getRowClassName);
}
return borderNone;
};
})(),
"columnHeaders": {
"backgroundColor": decisions.background.contrast.grey.default,
"&&": {
"borderColor": decisions.border.plain.grey.default,
"borderPosition": "bottom",
"borderWidth": 2
}
},
"row": () => {
const hoveredAndSelected = {
"&.Mui-hovered": {
"backgroundColor": fr.colors.decisions.background.contrast.grey.hover
},
"&.Mui-selected": {
"backgroundColor": fr.colors.decisions.background.contrast.grey.active
}
};
return {
"&.even": Object.assign({ "backgroundColor": decisions.background.contrast.grey.default, "&:hover": {
"backgroundColor": decisions.background.contrast.grey.hover
} }, hoveredAndSelected),
"&.odd": Object.assign({ "backgroundColor": decisions.background.alt.grey.default, "&:hover": {
"backgroundColor": decisions.background.alt.grey.hover
} }, hoveredAndSelected)
};
},
"columnSeparator": {
"display": "none"
}
}
}
};
return nonTypedMuiComponents;
})())
};
}
/**
*Generate a theme base on the options received.
*
* @param params — Dark or light mode.
*
* @param args — Deep merge the arguments with the about to be returned theme.
*
* @returns — A complete, ready-to-use mui theme object.
*/
export function createMuiDsfrTheme(params, ...args) {
const muiTheme = mui.createTheme(getMuiDsfrThemeOptions(params), ...args);
return muiTheme;
}
export function createMuiDsfrThemeProvider(params) {
const { augmentMuiTheme, useIsDark: useIsDark_props = useIsDark } = params;
function MuiDsfrThemeProvider(props) {
const { children } = props;
const { isDark } = useIsDark_props();
const { breakpointsValues } = useBreakpointsValuesPx();
const theme = useMemo(() => {
const nonAugmentedMuiTheme = createMuiDsfrTheme({ isDark, breakpointsValues });
return augmentMuiTheme === undefined
? nonAugmentedMuiTheme
: augmentMuiTheme({
nonAugmentedMuiTheme,
isDark
});
}, [isDark, breakpointsValues]);
return React.createElement(mui.ThemeProvider, { theme: theme }, children);
}
return { MuiDsfrThemeProvider };
}
export const { MuiDsfrThemeProvider } = createMuiDsfrThemeProvider({});
export default MuiDsfrThemeProvider;
export function createDsfrCustomBrandingProvider(params) {
const { createMuiTheme } = params;
function useMuiTheme() {
const { isDark } = useIsDark();
const { breakpointsValues } = useBreakpointsValuesPx();
const { theme, isGov, faviconUrl_userProvided } = useMemo(() => {
var _a;
const theme_gov = createMuiDsfrTheme({ isDark, breakpointsValues });
// @ts-expect-error: Technic to detect if user is using the government theme
theme_gov.palette.isGov = true;
const { theme, faviconUrl: faviconUrl_userProvided } = createMuiTheme({
isDark,
theme_gov
});
let isGov;
// @ts-expect-error: We know what we are doing
if (theme.palette.isGov) {
isGov = true;
// @ts-expect-error: We know what we are doing
delete theme.palette.isGov;
}
else {
isGov = false;
}
// NOTE: We do not allow customization of the spacing and breakpoints
if (!isGov) {
theme.spacing = structuredCloneButFunctions(theme_gov.spacing);
theme.breakpoints = structuredCloneButFunctions(theme_gov.breakpoints);
(_a = theme.components) !== null && _a !== void 0 ? _a : (theme.components = {});
deepAssign({
target: theme.components,
source: structuredCloneButFunctions({
MuiTablePagination: theme_gov.components.MuiTablePagination
})
});
theme.typography = structuredCloneButFunctions(theme_gov.typography, ({ key, value }) => (key !== "fontFamily" ? value : theme.typography.fontFamily));
}
return { theme, isGov, faviconUrl_userProvided };
}, [isDark, breakpointsValues]);
return { theme, isGov, faviconUrl_userProvided };
}
function useFavicon(params) {
const { faviconUrl } = params;
useEffect(() => {
document
.querySelectorAll('link[rel="apple-touch-icon"], link[rel="icon"], link[rel="shortcut icon"]')
.forEach(link => link.remove());
const link = document.createElement("link");
link.rel = "icon";
link.href = faviconUrl;
link.type = (() => {
var _a;
if (faviconUrl.startsWith("data:")) {
return faviconUrl.split("data:")[1].split(",")[0];
}
switch ((_a = faviconUrl.split(".").pop()) === null || _a === void 0 ? void 0 : _a.toLowerCase()) {
case "svg":
return "image/svg+xml";
case "png":
return "image/png";
case "ico":
return "image/x-icon";
default:
throw new Error("Unsupported favicon file type");
}
})();
document.head.appendChild(link);
return () => {
link.remove();
};
}, [faviconUrl]);
}
function DsfrCustomBrandingProvider(props) {
const { children } = props;
const { theme, isGov, faviconUrl_userProvided } = useMuiTheme();
useFavicon({
faviconUrl: faviconUrl_userProvided !== null && faviconUrl_userProvided !== void 0 ? faviconUrl_userProvided : getAssetUrl(isGov ? marianneFaviconSvgUrl : blankFaviconSvgUrl)
});
return (React.createElement(React.Fragment, null,
!isGov && (React.createElement(Global, { styles: css({
":root": {
"--text-active-blue-france": theme.palette.primary.main,
"--background-active-blue-france": theme.palette.primary.main,
"--text-action-high-blue-france": theme.palette.primary.main,
"--border-plain-blue-france": theme.palette.primary.main,
"--border-active-blue-france": theme.palette.primary.main,
"--text-title-grey": theme.palette.text.primary,
"--background-action-high-blue-france": theme.palette.primary.main,
"--border-default-grey": theme.palette.divider,
"--border-action-high-blue-france": theme.palette.primary.main,
"--border-default-blue-france": theme.palette.primary.main,
"--text-inverted-blue-france": theme.palette.primary.contrastText,
"--background-action-high-blue-france-hover": theme.palette.primary.dark,
"--background-action-high-blue-france-active": mui.darken(theme.palette.primary.main, 0.24)
// options:
/*
"--blue-france-sun-113-625": theme.palette.primary.main,
"--blue-france-sun-113-625-active": theme.palette.primary.light,
"--blue-france-sun-113-625-hover": theme.palette.primary.dark,
"--blue-france-975-sun-113": theme.palette.primary.contrastText,
"--blue-france-950-100": theme.palette.secondary.main,
"--blue-france-950-100-active": theme.palette.secondary.light,
"--blue-france-950-100-hover": theme.palette.secondary.dark,
//"--blue-france-sun-113-625": theme.palette.secondary.contrastText,
"--grey-50-1000": theme.palette.text.primary,
"--grey-200-850": theme.palette.text.secondary,
"--grey-625-425": theme.palette.text.disabled,
"--grey-900-175": theme.palette.divider,
//"--grey-200-850": theme.palette.action.active,
"--grey-975-100": theme.palette.action.hover,
"--blue-france-925-125-active": theme.palette.action.selected,
//"--grey-625-425": theme.palette.action.disabled,
"--grey-925-125": theme.palette.action.disabledBackground,
//"--blue-france-sun-113-625-active": theme.palette.action.focus,
"--grey-1000-50": theme.palette.background.default,
"--grey-1000-100": theme.palette.background.paper
*/
},
body: {
fontFamily: theme.typography.fontFamily,
fontSize: theme.typography.fontSize,
//"lineHeight": theme.typography.lineHeight,
color: theme.palette.text.primary,
backgroundColor: theme.palette.background.default
},
[`.${fr.cx("fr-header__logo")}`]: {
display: "none"
},
[`.${fr.cx("fr-footer__brand")} .${fr.cx("fr-logo")}`]: {
display: "none"
},
[`.${fr.cx("fr-footer__content-list")}`]: {
display: "none"
},
[`.${fr.cx("fr-footer__bottom-copy")}`]: {
display: "none"
},
[`.${fr.cx("fr-input")}, .${fr.cx("fr-select")}`]: {
"&&&": {
borderTopLeftRadius: `0px`,
borderTopRightRadius: `0px`
}
}
}) })),
React.createElement(IsGovProvider, { isGov: isGov },
React.createElement(mui.ThemeProvider, { theme: theme },
React.createElement(DisplayArtworkWhiteLabelProvider, null, children)))));
}
return { DsfrCustomBrandingProvider };
}
//# sourceMappingURL=mui.js.map