1
1
"use client" ;
2
2
3
3
/* 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" ;
7
6
import type { Shadows } from "@mui/material/styles" ;
8
7
import { fr } from "./fr" ;
9
8
import { useIsDark } from "./useIsDark" ;
@@ -13,11 +12,17 @@ import { assert } from "tsafe/assert";
13
12
import { objectKeys } from "tsafe/objectKeys" ;
14
13
import { id } from "tsafe/id" ;
15
14
import { 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" ;
16
21
17
22
export function getMuiDsfrThemeOptions ( params : {
18
23
isDark : boolean ;
19
24
breakpointsValues : BreakpointsValues ;
20
- } ) : ThemeOptions {
25
+ } ) : mui . ThemeOptions {
21
26
const { isDark, breakpointsValues } = params ;
22
27
23
28
const { options, decisions } = fr . colors . getHex ( { isDark } ) ;
@@ -139,7 +144,7 @@ export function getMuiDsfrThemeOptions(params: {
139
144
} ) ( ) ;
140
145
} ) ( ) ,
141
146
"shadows" : ( ( ) => {
142
- const [ , , , , , , , , ...rest ] = createTheme ( ) . shadows ;
147
+ const [ , , , , , , , , ...rest ] = mui . createTheme ( ) . shadows ;
143
148
144
149
return id < Shadows > ( [
145
150
"none" ,
@@ -336,8 +341,8 @@ export function getMuiDsfrThemeOptions(params: {
336
341
export function createMuiDsfrTheme (
337
342
params : { isDark : boolean ; breakpointsValues : BreakpointsValues } ,
338
343
...args : object [ ]
339
- ) : MuiTheme {
340
- const muiTheme = createTheme ( getMuiDsfrThemeOptions ( params ) , ...args ) ;
344
+ ) : mui . Theme {
345
+ const muiTheme = mui . createTheme ( getMuiDsfrThemeOptions ( params ) , ...args ) ;
341
346
342
347
return muiTheme ;
343
348
}
@@ -349,9 +354,9 @@ export function createMuiDsfrThemeProvider(params: {
349
354
* It's a Theme as defined in import type { Theme } from "@mui/material/styles";
350
355
* That is to say before augmentation.
351
356
**/
352
- nonAugmentedMuiTheme : MuiTheme ;
357
+ nonAugmentedMuiTheme : mui . Theme ;
353
358
isDark : boolean ;
354
- } ) => MuiTheme ;
359
+ } ) => mui . Theme ;
355
360
} ) {
356
361
const { augmentMuiTheme, useIsDark : useIsDark_props = useIsDark } = params ;
357
362
@@ -378,7 +383,7 @@ export function createMuiDsfrThemeProvider(params: {
378
383
} ) ;
379
384
} , [ isDark , breakpointsValues ] ) ;
380
385
381
- return < MuiThemeProvider theme = { theme } > { children } </ MuiThemeProvider > ;
386
+ return < mui . ThemeProvider theme = { theme } > { children } </ mui . ThemeProvider > ;
382
387
}
383
388
384
389
return { MuiDsfrThemeProvider } ;
@@ -387,3 +392,206 @@ export function createMuiDsfrThemeProvider(params: {
387
392
export const { MuiDsfrThemeProvider } = createMuiDsfrThemeProvider ( { } ) ;
388
393
389
394
export 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