Skip to content

Commit 83fc0e2

Browse files
committed
Expose colors as CSS variables
1 parent 482f27f commit 83fc0e2

File tree

10 files changed

+182
-57
lines changed

10 files changed

+182
-57
lines changed

scripts/build/cssToTs/colorDecisions.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -253,9 +253,9 @@ export function generateGetColorDecisionsTsCode(rawCssCode: string): string {
253253
});
254254

255255
return [
256-
`export function getColorDecisions(`,
256+
`export function getColorDecisions<Format extends "css var" | "hex">(`,
257257
` params: {`,
258-
` colorOptions: ColorOptions;`,
258+
` colorOptions: ColorOptions<Format>;`,
259259
` }`,
260260
`) {`,
261261
``,

scripts/build/cssToTs/colorOptions.ts

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -381,7 +381,7 @@ export const parseColorOptions = memoize((rawCssCode: string): ColorOption[] =>
381381
.filter(exclude(undefined));
382382
});
383383

384-
export function generateGetColorOptionsTsCode(rawCssCode: string) {
384+
export function generateGetColorOptionsHexTsCode(rawCssCode: string) {
385385
const colorOptions = parseColorOptions(rawCssCode);
386386

387387
const obj: any = {};
@@ -425,7 +425,7 @@ export function generateGetColorOptionsTsCode(rawCssCode: string) {
425425
});
426426

427427
return [
428-
`export function getColorOptions(`,
428+
`export function getColorOptionsHex(`,
429429
` params: {`,
430430
` isDark: boolean;`,
431431
` }`,
@@ -449,6 +449,46 @@ export function generateGetColorOptionsTsCode(rawCssCode: string) {
449449
].join("\n");
450450
}
451451

452+
export function generateColorOptionsTsCode(rawCssCode: string) {
453+
const colorOptions = parseColorOptions(rawCssCode);
454+
455+
const obj: any = {};
456+
457+
colorOptions.forEach(colorOption => {
458+
const value = `var(${colorOption.colorOptionName})`;
459+
460+
function req(obj: any, path: readonly string[]): void {
461+
const [propertyName, ...pathRest] = path;
462+
463+
if (pathRest.length === 0) {
464+
obj[propertyName] = value;
465+
return;
466+
}
467+
468+
if (obj[propertyName] === undefined) {
469+
obj[propertyName] = {};
470+
}
471+
472+
req(obj[propertyName], pathRest);
473+
}
474+
475+
req(obj, colorOption.themePath);
476+
});
477+
478+
return [
479+
``,
480+
`export const colorOptions= {`,
481+
JSON.stringify(obj, null, 2)
482+
.replace(/^{\n/, "")
483+
.replace(/\n}$/, "")
484+
.split("\n")
485+
.map(line => line.replace(/^[ ]{2}/, ""))
486+
.map(line => ` ${line}`)
487+
.join("\n"),
488+
`} as const;`
489+
].join("\n");
490+
}
491+
452492
function toConsistentColor(color: string) {
453493
if (!color.startsWith("#")) {
454494
return color;

scripts/build/cssToTs/cssToTs.ts

Lines changed: 49 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { generateBreakpointsTsCode } from "./breakpoints";
22
import { generateGetColorDecisionsTsCode } from "./colorDecisions";
3-
import { generateGetColorOptionsTsCode } from "./colorOptions";
3+
import { generateGetColorOptionsHexTsCode, generateColorOptionsTsCode } from "./colorOptions";
44
import { getProjectRoot } from "../../../src/bin/tools/getProjectRoot";
55
import { generateTypographyTsCode } from "./typography";
66
import { generateSpacingTsCode } from "./spacing";
@@ -26,17 +26,32 @@ export function cssToTs(params: {
2626
)}, please don't edit.`
2727
].join("\n");
2828

29-
const targetOptionFilePath = pathJoin(generatedDirPath, "getColorOptions.ts");
29+
const targetGetColorOptionsHexFilePath = pathJoin(generatedDirPath, "getColorOptionsHex.ts");
3030

3131
fs.writeFileSync(
32-
targetOptionFilePath,
32+
targetGetColorOptionsHexFilePath,
33+
Buffer.from(
34+
[warningMessage, ``, generateGetColorOptionsHexTsCode(rawDsfrCssCode), ``].join("\n"),
35+
"utf8"
36+
)
37+
);
38+
39+
const targetColorOptionsFilePath = pathJoin(generatedDirPath, "colorOptions.ts");
40+
41+
fs.writeFileSync(
42+
targetColorOptionsFilePath,
3343
Buffer.from(
3444
[
3545
warningMessage,
3646
``,
37-
generateGetColorOptionsTsCode(rawDsfrCssCode),
47+
`import type { getColorOptionsHex } from "./${pathBasename(
48+
targetGetColorOptionsHexFilePath
49+
).replace(/\.ts$/, "")}";`,
3850
``,
39-
`export type ColorOptions = ReturnType<typeof getColorOptions>;`,
51+
generateColorOptionsTsCode(rawDsfrCssCode),
52+
``,
53+
`export type ColorOptions<Format extends "css var" | "hex"= "css var"> = `,
54+
` Format extends "css var" ? typeof colorOptions : ReturnType<typeof getColorOptionsHex>;`,
4055
``
4156
].join("\n"),
4257
"utf8"
@@ -48,14 +63,37 @@ export function cssToTs(params: {
4863
Buffer.from(
4964
[
5065
warningMessage,
51-
`import type { ColorOptions } from "./${pathBasename(targetOptionFilePath).replace(
52-
/\.ts$/,
53-
""
54-
)}";`,
66+
`import type { ColorOptions } from "./${pathBasename(
67+
targetColorOptionsFilePath
68+
).replace(/\.ts$/, "")}";`,
69+
``,
70+
generateGetColorDecisionsTsCode(rawDsfrCssCode).replace(
71+
"export function getColorDecisions",
72+
"function getColorDecisions_noReturnType"
73+
),
74+
``,
75+
`type IsHex<T> = T extends \`#\${string}\` ? T : never;`,
76+
``,
77+
`type OnlyHex<T> = {`,
78+
` [K in keyof T]: T[K] extends string ? IsHex<T[K]> : OnlyHex<T[K]>`,
79+
`};`,
80+
``,
81+
`type IsCssVar<T> = T extends \`var(--\${string})\` ? T : never;`,
82+
`type OnlyCssVar<T> = {`,
83+
` [K in keyof T]: T[K] extends string ? IsCssVar<T[K]> : OnlyCssVar<T[K]>`,
84+
`};`,
85+
``,
86+
`export type ColorDecisions<Format extends "css var" | "hex" = "css var"> =`,
87+
` Format extends "css var" ? OnlyCssVar<ReturnType<typeof getColorDecisions_noReturnType>> :`,
88+
` OnlyHex<ReturnType<typeof getColorDecisions_noReturnType>>;`,
5589
``,
56-
generateGetColorDecisionsTsCode(rawDsfrCssCode),
90+
`export function getColorDecisions<Format extends "css var" | "hex">(params: {`,
91+
` colorOptions: ColorOptions<Format>;`,
5792
``,
58-
`export type ColorDecisions = ReturnType<typeof getColorDecisions>;`,
93+
`}): ColorDecisions<Format> {`,
94+
` // ${"@"}ts-expect-error: We are intentionally sacrificing internal type safety for a more accurate type annotation.`,
95+
` return getColorDecisions_noReturnType(params);`,
96+
`}`,
5997
``
6098
].join("\n"),
6199
"utf8"

src/fr/colors.ts

Lines changed: 33 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,38 @@
1-
import { getColorOptions } from "./generatedFromCss/getColorOptions";
2-
import type { ColorOptions } from "./generatedFromCss/getColorOptions";
3-
import { getColorDecisions } from "./generatedFromCss/getColorDecisions";
4-
import type { ColorDecisions } from "./generatedFromCss/getColorDecisions";
5-
import { memoize } from "../tools/memoize";
1+
import { colorOptions, type ColorOptions } from "./generatedFromCss/colorOptions";
2+
import { getColorOptionsHex } from "./generatedFromCss/getColorOptionsHex";
3+
import { getColorDecisions, type ColorDecisions } from "./generatedFromCss/getColorDecisions";
64

7-
export type ColorTheme = {
8-
isDark: boolean;
9-
decisions: ColorDecisions;
10-
options: ColorOptions;
5+
export type Colors = {
6+
options: ColorOptions<"css var">;
7+
decisions: ColorDecisions<"css var">;
8+
getHex: (params: { isDark: boolean }) => {
9+
isDark: boolean;
10+
options: ColorOptions<"hex">;
11+
decisions: ColorDecisions<"hex">;
12+
};
1113
};
1214

13-
export const getColors = memoize(
14-
(isDark: boolean): ColorTheme => {
15-
const options = getColorOptions({ isDark });
15+
export const colors: Colors = {
16+
"options": colorOptions,
17+
"decisions": getColorDecisions({ colorOptions }),
18+
"getHex": (() => {
19+
const getHex: Colors["getHex"] = ({ isDark }) => {
20+
const options = getColorOptionsHex({ isDark });
1621

17-
return {
18-
isDark,
19-
options,
20-
"decisions": getColorDecisions({ "colorOptions": options })
22+
const decisions = getColorDecisions({ colorOptions });
23+
24+
return {
25+
isDark,
26+
options,
27+
decisions
28+
};
29+
};
30+
31+
const cache: Record<"light" | "dark", ReturnType<Colors["getHex"]> | undefined> = {
32+
"light": undefined,
33+
"dark": undefined
2134
};
22-
},
23-
{ "max": 1 }
24-
);
35+
36+
return ({ isDark }) => (cache[isDark ? "dark" : "light"] ??= getHex({ isDark }));
37+
})()
38+
};

src/fr/index.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,17 @@ import { spacing } from "./spacing";
55
export type { SpacingToken } from "./spacing";
66
import { cx } from "./cx";
77
export type { FrCxArg } from "./cx";
8-
export type { ColorTheme } from "./colors";
9-
import { getColors } from "./colors";
8+
export type { Colors } from "./colors";
9+
export type { ColorOptions } from "./generatedFromCss/colorOptions";
10+
export type { ColorDecisions } from "./generatedFromCss/getColorDecisions";
11+
import { colors } from "./colors";
1012
export type { FrClassName, FrIconClassName, RiIconClassName } from "./generatedFromCss/classNames";
1113
import { typography } from "./generatedFromCss/typography";
1214

1315
export const fr = {
1416
breakpoints,
1517
spacing,
1618
cx,
17-
getColors,
19+
colors,
1820
typography
1921
};

src/mui.tsx

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,7 @@ import React, { useMemo, type ReactNode } from "react";
55
import type { Theme as MuiTheme, ThemeOptions } from "@mui/material/styles";
66
import { createTheme, ThemeProvider as MuiThemeProvider } from "@mui/material/styles";
77
import type { Shadows } from "@mui/material/styles";
8-
import { getColors } from "./fr/colors";
9-
import type { ColorTheme } from "./fr/colors";
8+
import { fr } from "./fr";
109
import { useIsDark } from "./useIsDark";
1110
import { typography } from "./fr/generatedFromCss/typography";
1211
import { spacingTokenByValue } from "./fr/generatedFromCss/spacing";
@@ -21,7 +20,7 @@ export function getMuiDsfrThemeOptions(params: {
2120
}): ThemeOptions {
2221
const { isDark, breakpointsValues } = params;
2322

24-
const { options, decisions } = getColors(isDark);
23+
const { options, decisions } = fr.colors.getHex({ isDark });
2524

2625
return {
2726
"shape": {
@@ -179,7 +178,8 @@ export function getMuiDsfrThemeOptions(params: {
179178
"MuiStepIcon": {
180179
"styleOverrides": {
181180
"text": {
182-
"fill": getColors(true).decisions.text.title.grey.default
181+
"fill": fr.colors.getHex({ "isDark": true }).decisions.text.title.grey
182+
.default
183183
}
184184
}
185185
},
@@ -274,7 +274,7 @@ export function createMuiDsfrThemeProvider(params: {
274274
* That is to say before augmentation.
275275
**/
276276
nonAugmentedMuiTheme: MuiTheme;
277-
frColorTheme: ColorTheme;
277+
isDark: boolean;
278278
}) => MuiTheme;
279279
}) {
280280
const { augmentMuiTheme, useIsDark: useIsDark_props = useIsDark } = params;
@@ -297,8 +297,8 @@ export function createMuiDsfrThemeProvider(params: {
297297
return augmentMuiTheme === undefined
298298
? nonAugmentedMuiTheme
299299
: augmentMuiTheme({
300-
"frColorTheme": getColors(isDark),
301-
nonAugmentedMuiTheme
300+
nonAugmentedMuiTheme,
301+
isDark
302302
});
303303
}, [isDark]);
304304

src/next-pagesdir.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import AppleTouchIcon from "./dsfr/favicon/apple-touch-icon.png";
1515
import FaviconSvg from "./dsfr/favicon/favicon.svg";
1616
import FaviconIco from "./dsfr/favicon/favicon.ico";
1717
import { getAssetUrl } from "./tools/getAssetUrl";
18-
import { getColors } from "./fr/colors";
18+
import { fr } from "./fr";
1919
import { start } from "./start";
2020
import type { RegisterLink, RegisteredLinkProps } from "./link";
2121
import { setLink } from "./link";
@@ -171,7 +171,8 @@ export function createNextDsfrIntegrationApi(
171171
<meta
172172
name="theme-color"
173173
content={
174-
getColors(isDark).decisions.background.default.grey.default
174+
fr.colors.getHex({ isDark }).decisions.background.default
175+
.grey.default
175176
}
176177
/>
177178
</>

src/useColors.ts

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,41 @@
11
"use client";
22

33
import { useIsDark } from "./useIsDark";
4-
import { getColors } from "./fr/colors";
5-
import type { ColorTheme } from "./fr/colors";
4+
import { fr } from "./fr";
65

7-
export type { ColorTheme };
8-
9-
export function useColors(): ColorTheme {
6+
/** @deprecated: A hook is no longer required to get the colors.
7+
*
8+
* Before you would do:
9+
* ```ts
10+
* import { useColors } from "@codegouvfr/react-dsfr/useColors";
11+
* // ...
12+
* const theme = useColors();
13+
* // ...
14+
* theme.decisions.background.default.grey.default
15+
* ```
16+
* Now you should do:
17+
* ```ts
18+
* import { fr } from "@codegouvfr/react-dsfr";
19+
* // ...
20+
* fr.colors.decisions.background.default.grey.default
21+
* ```
22+
* We don't need a hook anymore as the the colors are expressed as CSS variables and thus don't need to be
23+
* switched at runtime when the user changes the dark mode.
24+
*
25+
* If however you need the colors in the HEX format you can do:
26+
*
27+
* ```ts
28+
* import { fr } from "@codegouvfr/react-dsfr";
29+
* import { useIsDark } from "@codegouvfr/react-dsfr/useIsDark";
30+
* // ...
31+
* const { isDark } = useIsDark();
32+
* const theme = fr.colors.getHex({ isDark });
33+
* // ...
34+
* theme.decisions.background.default.grey.default
35+
* ```
36+
**/
37+
export function useColors() {
1038
const { isDark } = useIsDark();
1139

12-
return getColors(isDark);
40+
return fr.colors.getHex({ isDark });
1341
}

src/useIsDark/client.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { assert } from "tsafe/assert";
22
import { createStatefulObservable, useRerenderOnChange } from "../tools/StatefulObservable";
33
import { useConstCallback } from "../tools/powerhooks/useConstCallback";
4-
import { getColors } from "../fr/colors";
4+
import { fr } from "../fr";
55
import { data_fr_scheme, data_fr_theme, rootColorSchemeStyleTagId } from "./constants";
66

77
export type ColorScheme = "light" | "dark";
@@ -248,7 +248,9 @@ export function startClientSideIsDarkLogic(params: {
248248

249249
element.name = name;
250250

251-
element.content = getColors(isDark).decisions.background.default.grey.default;
251+
element.content = fr.colors.getHex({
252+
isDark
253+
}).decisions.background.default.grey.default;
252254

253255
document.head.appendChild(element);
254256
};

src/useIsDark/scriptToRunAsap.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { ColorScheme } from "./client";
22
import { data_fr_scheme, data_fr_theme, rootColorSchemeStyleTagId } from "./constants";
3-
import { getColors } from "../fr/colors";
3+
import { fr } from "../fr";
44

55
export const getScriptToRunAsap = (defaultColorScheme: ColorScheme | "system") => `
66
{
@@ -93,8 +93,8 @@ export const getScriptToRunAsap = (defaultColorScheme: ColorScheme | "system") =
9393
element.name = name;
9494
9595
element.content = isDark ? "${
96-
getColors(true).decisions.background.default.grey.default
97-
}" : "${getColors(false).decisions.background.default.grey.default}";
96+
fr.colors.getHex({ "isDark": true }).decisions.background.default.grey.default
97+
}" : "${fr.colors.getHex({ "isDark": false }).decisions.background.default.grey.default}";
9898
9999
document.head.appendChild(element);
100100

0 commit comments

Comments
 (0)