Skip to content

Commit fca5b20

Browse files
committedDec 24, 2024
theme-config
1 parent 56cb6c4 commit fca5b20

14 files changed

+1435
-2
lines changed
 

‎README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
> The [shadcn ui kit](https://ui.shadcn.com/) bindings for [react-declarative](https://github.com/react-declarative/react-declarative/)
44
5-
![screenshot](./screenshot.png)
5+
![screencast](./screencast.gif)
66

77
## Getting started
88

‎bun.lockb

1.05 KB
Binary file not shown.

‎package.json

+5-1
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,11 @@
4343
"react-hook-form": "7.54.2",
4444
"tailwind-merge": "2.5.5",
4545
"tailwindcss-animate": "1.0.7",
46-
"zod": "3.24.1"
46+
47+
"zod": "3.24.1",
48+
"remeda": "1.33.0",
49+
"colord": "2.9.3",
50+
"jotai": "2.11.0"
4751
},
4852
"devDependencies": {
4953
"@emotion/react": "11.10.4",

‎screencast.gif

1.54 MB
Loading

‎src/assets/RandomTheme.tsx

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { createTheme, Fab, ThemeProvider } from "@mui/material";
2+
3+
import Casino from "@mui/icons-material/Casino";
4+
import { randomThemeConfig } from "@/assets/random-theme-config";
5+
import { useSetThemeConfig } from "@/hooks/use-theme-config";
6+
7+
const theme = createTheme();
8+
9+
export const RandomTheme = () => {
10+
const setThemeConfig = useSetThemeConfig();
11+
return (
12+
<ThemeProvider theme={theme}>
13+
<Fab
14+
sx={{ position: "fixed", bottom: 16, right: 16 }}
15+
onClick={() =>
16+
setThemeConfig((prevTheme) => {
17+
let newTheme = randomThemeConfig();
18+
while (prevTheme === newTheme) {
19+
newTheme = randomThemeConfig();
20+
}
21+
return newTheme;
22+
})
23+
}
24+
color="primary"
25+
aria-label="add"
26+
>
27+
<Casino />
28+
</Fab>
29+
</ThemeProvider>
30+
);
31+
};
32+
33+
export default RandomTheme;

‎src/assets/ThemeProvider.tsx

+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
2+
import { useMemo } from "react";
3+
4+
import { themeToStyles } from "@/assets/theme-to-styles";
5+
import { useThemeConfig, useThemeDark } from "@/hooks/use-theme-config";
6+
7+
// This weird approach is necessary to also style the portaled components
8+
export const ThemeStyleSheet = () => {
9+
const config = useThemeConfig();
10+
const dark = useThemeDark();
11+
12+
const style = useMemo(() => {
13+
14+
if (!config) {
15+
return "";
16+
}
17+
18+
const styles = {
19+
light: themeToStyles(config.light),
20+
dark: themeToStyles(config.dark),
21+
};
22+
23+
const lightStyles = Object.entries(styles.light)
24+
.map(([key, value]) => `${key}: ${value};`)
25+
.join("\n");
26+
27+
const darkStyles = Object.entries(styles.dark)
28+
.map(([key, value]) => `${key}: ${value};`)
29+
.join("\n");
30+
31+
if (dark) {
32+
return `
33+
body {
34+
${darkStyles}
35+
}`;
36+
}
37+
38+
return `
39+
body {
40+
${lightStyles}
41+
}`;
42+
}, [config]);
43+
44+
return <style dangerouslySetInnerHTML={{ __html: style }} />;
45+
};

‎src/assets/create-theme-config.ts

+468
Large diffs are not rendered by default.

‎src/assets/hsl-to-variable-value.ts

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { type Hsl } from "@/assets/theme-config";
2+
3+
export const hslToVariableValue = (hsl: Hsl) => {
4+
return `${hsl.h} ${hsl.s}% ${hsl.l}%`;
5+
};
6+
7+
export const hslToCssValue = (hsl: Hsl) => {
8+
return `hsl(${hslToVariableValue(hsl)})`;
9+
};

‎src/assets/random-theme-config.ts

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import themes from "@/assets/themes";
2+
import { ThemeConfig } from "./theme-config";
3+
4+
export const randomThemeConfig = () => themes[Math.floor(Math.random() * themes.length)] as unknown as ThemeConfig;

‎src/assets/theme-config.ts

+59
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { z } from "zod";
2+
3+
const HslSchema = z.object({
4+
h: z.number(),
5+
s: z.number(),
6+
l: z.number(),
7+
});
8+
9+
export type Hsl = z.infer<typeof HslSchema>;
10+
11+
export const chartKeys = [
12+
"chart-1",
13+
"chart-2",
14+
"chart-3",
15+
"chart-4",
16+
"chart-5",
17+
] as const;
18+
19+
export type ChartKeys = (typeof chartKeys)[number];
20+
21+
const chartSchemas = chartKeys.reduce(
22+
(acc, key) => {
23+
acc[key] = HslSchema;
24+
return acc;
25+
},
26+
{} as Record<ChartKeys, typeof HslSchema>,
27+
);
28+
29+
export const ThemeSchema = z.object({
30+
background: HslSchema,
31+
foreground: HslSchema,
32+
card: HslSchema,
33+
cardForeground: HslSchema,
34+
popover: HslSchema,
35+
popoverForeground: HslSchema,
36+
primary: HslSchema,
37+
primaryForeground: HslSchema,
38+
secondary: HslSchema,
39+
secondaryForeground: HslSchema,
40+
muted: HslSchema,
41+
mutedForeground: HslSchema,
42+
accent: HslSchema,
43+
accentForeground: HslSchema,
44+
destructive: HslSchema,
45+
destructiveForeground: HslSchema,
46+
border: HslSchema,
47+
input: HslSchema,
48+
ring: HslSchema,
49+
...chartSchemas,
50+
});
51+
52+
export type Theme = z.infer<typeof ThemeSchema>;
53+
54+
export const ThemeConfigSchema = z.object({
55+
light: ThemeSchema,
56+
dark: ThemeSchema,
57+
});
58+
59+
export type ThemeConfig = z.infer<typeof ThemeConfigSchema>;

‎src/assets/theme-to-styles.ts

+132
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
import { hslToVariableValue } from "@/assets/hsl-to-variable-value";
2+
import { backfillCharts } from "@/assets/create-theme-config";
3+
import { chartKeys, type Hsl, type Theme } from "@/assets/theme-config";
4+
5+
import { fromPairs, invert, keys } from "remeda";
6+
7+
const variables: Record<keyof Theme, string> = {
8+
background: "background",
9+
foreground: "foreground",
10+
muted: "muted",
11+
mutedForeground: "muted-foreground",
12+
popover: "popover",
13+
popoverForeground: "popover-foreground",
14+
card: "card",
15+
cardForeground: "card-foreground",
16+
border: "border",
17+
input: "input",
18+
primary: "primary",
19+
primaryForeground: "primary-foreground",
20+
secondary: "secondary",
21+
secondaryForeground: "secondary-foreground",
22+
accent: "accent",
23+
accentForeground: "accent-foreground",
24+
destructive: "destructive",
25+
destructiveForeground: "destructive-foreground",
26+
ring: "ring",
27+
"chart-1": "chart-1",
28+
"chart-2": "chart-2",
29+
"chart-3": "chart-3",
30+
"chart-4": "chart-4",
31+
"chart-5": "chart-5",
32+
};
33+
34+
export const themeToStyles = (theme: Theme) => {
35+
const order = keys.strict(variables);
36+
37+
const themeWithCharts = backfillCharts(theme, [...chartKeys]);
38+
39+
const ordered = order.map((key) => {
40+
return [
41+
`--${variables[key]}`,
42+
hslToVariableValue(themeWithCharts[key]),
43+
] as const;
44+
});
45+
46+
return fromPairs.strict(ordered);
47+
};
48+
49+
export const cssToTheme = (styles: string) => {
50+
const lines = styles.split("\n");
51+
52+
const invertedVariables = invert(variables);
53+
54+
const lightThemeEntries: Array<[keyof Theme, Hsl]> = [];
55+
const darkThemeEntries: Array<[keyof Theme, Hsl]> = [];
56+
57+
let errors = 0;
58+
59+
let isDark = false;
60+
61+
for (const line of lines) {
62+
if (line.includes(".dark")) {
63+
isDark = true;
64+
}
65+
66+
if (line.includes("}")) {
67+
isDark = false;
68+
}
69+
70+
const trimmed = line.trim();
71+
72+
if (trimmed.startsWith("--")) {
73+
const [variable, value] = trimmed.split(":");
74+
75+
if (!variable) {
76+
errors++;
77+
continue;
78+
}
79+
80+
const themeKey = invertedVariables[variable.replace("--", "")];
81+
82+
if (!themeKey) continue;
83+
84+
if (!value) {
85+
errors++;
86+
continue;
87+
}
88+
89+
const hsl = value.trim().replace(";", "").replaceAll("%", "").split(" ");
90+
91+
if (hsl.length !== 3) {
92+
errors++;
93+
continue;
94+
}
95+
96+
const [h, s, l] = hsl;
97+
98+
if (!h || !s || !l) {
99+
errors++;
100+
continue;
101+
}
102+
103+
const hAsNumber = Number(h);
104+
const sAsNumber = Number(s);
105+
const lAsNumber = Number(l);
106+
107+
if (isNaN(hAsNumber) || isNaN(sAsNumber) || isNaN(lAsNumber)) {
108+
errors++;
109+
continue;
110+
}
111+
112+
const hslColor: Hsl = {
113+
h: hAsNumber,
114+
s: sAsNumber,
115+
l: lAsNumber,
116+
};
117+
118+
if (isDark) {
119+
darkThemeEntries.push([themeKey, hslColor]);
120+
continue;
121+
}
122+
123+
lightThemeEntries.push([themeKey, hslColor]);
124+
}
125+
}
126+
127+
return {
128+
light: fromPairs.strict(lightThemeEntries),
129+
dark: fromPairs.strict(darkThemeEntries),
130+
errors,
131+
};
132+
};

‎src/assets/themes.ts

+639
Large diffs are not rendered by default.

‎src/hooks/use-theme-config.ts

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
"use client";
2+
3+
import { type ThemeConfig } from "@/assets/theme-config";
4+
5+
import { atom, useAtomValue, useSetAtom } from "jotai";
6+
7+
export const themeConfigAtom = atom<ThemeConfig>(null as unknown as ThemeConfig);
8+
9+
export const themeDarkAtom = atom<boolean>(false);
10+
11+
export const useThemeConfig = () => {
12+
return useAtomValue(themeConfigAtom);
13+
};
14+
15+
export const useThemeDark = () => {
16+
return useAtomValue(themeDarkAtom);
17+
};
18+
19+
export const useSetThemeConfig = () => {
20+
const setThemeConfig = useSetAtom(themeConfigAtom);
21+
const setThemeDark = useSetAtom(themeDarkAtom);
22+
23+
const set = (
24+
value: ThemeConfig | ((v: ThemeConfig) => ThemeConfig),
25+
) => {
26+
setThemeConfig(value);
27+
setThemeDark((dark) => !dark);
28+
};
29+
30+
return set;
31+
};
32+
33+
export const useActiveTheme = () => {
34+
const config = useThemeConfig();
35+
return config["light"];
36+
};

‎src/main.tsx

+4
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,16 @@ import App from "./App";
33
import "./index.css";
44
import { ModalManagerProvider, ModalProvider } from "react-declarative";
55
import { SlotFactory } from "./components/SlotFactory";
6+
import { ThemeStyleSheet } from "./assets/ThemeProvider";
7+
import RandomTheme from "./assets/RandomTheme";
68

79
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
810
<SlotFactory>
911
<ModalProvider>
1012
<ModalManagerProvider>
13+
<ThemeStyleSheet />
1114
<App />
15+
<RandomTheme />
1216
</ModalManagerProvider>
1317
</ModalProvider>
1418
</SlotFactory>

0 commit comments

Comments
 (0)
Please sign in to comment.