11"use client" ;
22
33/* eslint-disable @typescript-eslint/no-non-null-assertion */
4- import React , { useMemo , type ReactNode } from "react" ;
5- import type { Theme as MuiTheme , ThemeOptions } from "@mui/material/styles" ;
6- import { createTheme , ThemeProvider as MuiThemeProvider } from "@mui/material/styles" ;
4+ import React , { useMemo , useEffect , createContext , useContext , type ReactNode } from "react" ;
5+ import * as mui from "@mui/material/styles" ;
76import type { Shadows } from "@mui/material/styles" ;
87import { fr } from "./fr" ;
98import { useIsDark } from "./useIsDark" ;
@@ -13,11 +12,17 @@ import { assert } from "tsafe/assert";
1312import { objectKeys } from "tsafe/objectKeys" ;
1413import { id } from "tsafe/id" ;
1514import { useBreakpointsValuesPx , type BreakpointsValues } from "./useBreakpointsValuesPx" ;
15+ import { structuredCloneButFunctions } from "./tools/structuredCloneButFunctions" ;
16+ import { deepAssign } from "./tools/deepAssign" ;
17+ import { Global , css } from "@emotion/react" ;
18+ import { getAssetUrl } from "./tools/getAssetUrl" ;
19+ import marianneFaviconSvgUrl from "@codegouvfr/react-dsfr/favicon/favicon.svg" ;
20+ import blankFaviconSvgUrl from "./assets/blank-favicon.svg" ;
1621
1722export function getMuiDsfrThemeOptions ( params : {
1823 isDark : boolean ;
1924 breakpointsValues : BreakpointsValues ;
20- } ) : ThemeOptions {
25+ } ) : mui . ThemeOptions {
2126 const { isDark, breakpointsValues } = params ;
2227
2328 const { options, decisions } = fr . colors . getHex ( { isDark } ) ;
@@ -139,7 +144,7 @@ export function getMuiDsfrThemeOptions(params: {
139144 } ) ( ) ;
140145 } ) ( ) ,
141146 "shadows" : ( ( ) => {
142- const [ , , , , , , , , ...rest ] = createTheme ( ) . shadows ;
147+ const [ , , , , , , , , ...rest ] = mui . createTheme ( ) . shadows ;
143148
144149 return id < Shadows > ( [
145150 "none" ,
@@ -336,8 +341,8 @@ export function getMuiDsfrThemeOptions(params: {
336341export function createMuiDsfrTheme (
337342 params : { isDark : boolean ; breakpointsValues : BreakpointsValues } ,
338343 ...args : object [ ]
339- ) : MuiTheme {
340- const muiTheme = createTheme ( getMuiDsfrThemeOptions ( params ) , ...args ) ;
344+ ) : mui . Theme {
345+ const muiTheme = mui . createTheme ( getMuiDsfrThemeOptions ( params ) , ...args ) ;
341346
342347 return muiTheme ;
343348}
@@ -349,9 +354,9 @@ export function createMuiDsfrThemeProvider(params: {
349354 * It's a Theme as defined in import type { Theme } from "@mui/material/styles";
350355 * That is to say before augmentation.
351356 **/
352- nonAugmentedMuiTheme : MuiTheme ;
357+ nonAugmentedMuiTheme : mui . Theme ;
353358 isDark : boolean ;
354- } ) => MuiTheme ;
359+ } ) => mui . Theme ;
355360} ) {
356361 const { augmentMuiTheme, useIsDark : useIsDark_props = useIsDark } = params ;
357362
@@ -378,7 +383,7 @@ export function createMuiDsfrThemeProvider(params: {
378383 } ) ;
379384 } , [ isDark , breakpointsValues ] ) ;
380385
381- return < MuiThemeProvider theme = { theme } > { children } </ MuiThemeProvider > ;
386+ return < mui . ThemeProvider theme = { theme } > { children } </ mui . ThemeProvider > ;
382387 }
383388
384389 return { MuiDsfrThemeProvider } ;
@@ -387,3 +392,206 @@ export function createMuiDsfrThemeProvider(params: {
387392export const { MuiDsfrThemeProvider } = createMuiDsfrThemeProvider ( { } ) ;
388393
389394export default MuiDsfrThemeProvider ;
395+
396+ export function createDsfrCustomBrandingProvider ( params : {
397+ createMuiTheme : ( params : {
398+ isDark : boolean ;
399+ /**
400+ * WARNING: The types can be lying here if you have augmented the theme.
401+ * It's a Theme as defined in `import type { Theme } from "@mui/material/styles";`
402+ * That is to say before augmentation.
403+ * Make sure to set your custom properties if any are declared at the type level.
404+ **/
405+ theme_gov : mui . Theme ;
406+ } ) => { theme : mui . Theme ; faviconUrl ?: string } ;
407+ } ) {
408+ const { createMuiTheme } = params ;
409+
410+ function useMuiTheme ( ) {
411+ const { isDark } = useIsDark ( ) ;
412+ const { breakpointsValues } = useBreakpointsValuesPx ( ) ;
413+
414+ const { theme, isGov, faviconUrl_userProvided } = useMemo ( ( ) => {
415+ const theme_gov = createMuiDsfrTheme ( { isDark, breakpointsValues } ) ;
416+
417+ // @ts -expect-error: Technic to detect if user is using the government theme
418+ theme_gov . palette . isGov = true ;
419+
420+ const { theme, faviconUrl : faviconUrl_userProvided } = createMuiTheme ( {
421+ isDark,
422+ theme_gov
423+ } ) ;
424+
425+ let isGov : boolean ;
426+
427+ // @ts -expect-error: We know what we are doing
428+ if ( theme . palette . isGov ) {
429+ isGov = true ;
430+ // @ts -expect-error: We know what we are doing
431+ delete theme . palette . isGov ;
432+ } else {
433+ isGov = false ;
434+ }
435+
436+ // NOTE: We do not allow customization of the spacing and breakpoints
437+ if ( ! isGov ) {
438+ theme . spacing = structuredCloneButFunctions ( theme_gov . spacing ) ;
439+ theme . breakpoints = structuredCloneButFunctions ( theme_gov . breakpoints ) ;
440+
441+ theme . components ??= { } ;
442+
443+ deepAssign ( {
444+ target : theme . components as any ,
445+ source : structuredCloneButFunctions ( {
446+ MuiTablePagination : theme_gov . components ! . MuiTablePagination
447+ } ) as any
448+ } ) ;
449+
450+ theme . typography = structuredCloneButFunctions (
451+ theme_gov . typography ,
452+ ( { key, value } ) => ( key !== "fontFamily" ? value : theme . typography . fontFamily )
453+ ) ;
454+ }
455+
456+ return { theme, isGov, faviconUrl_userProvided } ;
457+ } , [ isDark , breakpointsValues ] ) ;
458+
459+ return { theme, isGov, faviconUrl_userProvided } ;
460+ }
461+
462+ function useFavicon ( params : { faviconUrl : string } ) {
463+ const { faviconUrl } = params ;
464+
465+ useEffect ( ( ) => {
466+ document
467+ . querySelectorAll (
468+ 'link[rel="apple-touch-icon"], link[rel="icon"], link[rel="shortcut icon"]'
469+ )
470+ . forEach ( link => link . remove ( ) ) ;
471+
472+ const link = document . createElement ( "link" ) ;
473+ link . rel = "icon" ;
474+ link . href = faviconUrl ;
475+ link . type = ( ( ) => {
476+ if ( faviconUrl . startsWith ( "data:" ) ) {
477+ return faviconUrl . split ( "data:" ) [ 1 ] . split ( "," ) [ 0 ] ;
478+ }
479+ switch ( faviconUrl . split ( "." ) . pop ( ) ?. toLowerCase ( ) ) {
480+ case "svg" :
481+ return "image/svg+xml" ;
482+ case "png" :
483+ return "image/png" ;
484+ case "ico" :
485+ return "image/x-icon" ;
486+ default :
487+ throw new Error ( "Unsupported favicon file type" ) ;
488+ }
489+ } ) ( ) ;
490+ document . head . appendChild ( link ) ;
491+
492+ return ( ) => {
493+ link . remove ( ) ;
494+ } ;
495+ } , [ faviconUrl ] ) ;
496+ }
497+
498+ function DsfrCustomBrandingProvider ( props : { children : ReactNode } ) {
499+ const { children } = props ;
500+
501+ const { theme, isGov, faviconUrl_userProvided } = useMuiTheme ( ) ;
502+
503+ useFavicon ( {
504+ faviconUrl :
505+ faviconUrl_userProvided ??
506+ getAssetUrl ( isGov ? marianneFaviconSvgUrl : blankFaviconSvgUrl )
507+ } ) ;
508+
509+ return (
510+ < >
511+ { ! isGov && (
512+ < Global
513+ styles = { css ( {
514+ ":root" : {
515+ "--text-active-blue-france" : theme . palette . primary . main ,
516+ "--background-active-blue-france" : theme . palette . primary . main ,
517+ "--text-action-high-blue-france" : theme . palette . primary . main ,
518+ "--border-plain-blue-france" : theme . palette . primary . main ,
519+ "--border-active-blue-france" : theme . palette . primary . main ,
520+ "--text-title-grey" : theme . palette . text . primary ,
521+ "--background-action-high-blue-france" : theme . palette . primary . main ,
522+ "--border-default-grey" : theme . palette . divider ,
523+ "--border-action-high-blue-france" : theme . palette . primary . main
524+
525+ // options:
526+ /*
527+ "--blue-france-sun-113-625": theme.palette.primary.main,
528+ "--blue-france-sun-113-625-active": theme.palette.primary.light,
529+ "--blue-france-sun-113-625-hover": theme.palette.primary.dark,
530+ "--blue-france-975-sun-113": theme.palette.primary.contrastText,
531+
532+ "--blue-france-950-100": theme.palette.secondary.main,
533+ "--blue-france-950-100-active": theme.palette.secondary.light,
534+ "--blue-france-950-100-hover": theme.palette.secondary.dark,
535+ //"--blue-france-sun-113-625": theme.palette.secondary.contrastText,
536+
537+ "--grey-50-1000": theme.palette.text.primary,
538+ "--grey-200-850": theme.palette.text.secondary,
539+ "--grey-625-425": theme.palette.text.disabled,
540+
541+ "--grey-900-175": theme.palette.divider,
542+
543+ //"--grey-200-850": theme.palette.action.active,
544+ "--grey-975-100": theme.palette.action.hover,
545+ "--blue-france-925-125-active": theme.palette.action.selected,
546+ //"--grey-625-425": theme.palette.action.disabled,
547+ "--grey-925-125": theme.palette.action.disabledBackground,
548+ //"--blue-france-sun-113-625-active": theme.palette.action.focus,
549+
550+ "--grey-1000-50": theme.palette.background.default,
551+ "--grey-1000-100": theme.palette.background.paper
552+ */
553+ } ,
554+ body : {
555+ fontFamily : theme . typography . fontFamily ,
556+ fontSize : theme . typography . fontSize ,
557+ //"lineHeight": theme.typography.lineHeight,
558+
559+ color : theme . palette . text . primary ,
560+ backgroundColor : theme . palette . background . default
561+ } ,
562+ [ `.${ fr . cx ( "fr-header__logo" ) } ` ] : {
563+ display : "none"
564+ } ,
565+ [ `.${ fr . cx ( "fr-footer__brand" ) } .${ fr . cx ( "fr-logo" ) } ` ] : {
566+ display : "none"
567+ } ,
568+ [ `.${ fr . cx ( "fr-footer__content-list" ) } ` ] : {
569+ display : "none"
570+ } ,
571+ [ `.${ fr . cx ( "fr-footer__bottom-copy" ) } ` ] : {
572+ display : "none"
573+ }
574+ } ) }
575+ />
576+ ) }
577+ < context_isGov . Provider value = { isGov } >
578+ < mui . ThemeProvider theme = { theme } > { children } </ mui . ThemeProvider >
579+ </ context_isGov . Provider >
580+ </ >
581+ ) ;
582+ }
583+
584+ return { DsfrCustomBrandingProvider } ;
585+ }
586+
587+ const context_isGov = createContext < boolean | undefined > ( undefined ) ;
588+
589+ export function useIsGov ( ) {
590+ const isGov = useContext ( context_isGov ) ;
591+
592+ if ( isGov === undefined ) {
593+ throw new Error ( "useIsGov must be used within a MuiThemeProvider" ) ;
594+ }
595+
596+ return { isGov } ;
597+ }
0 commit comments