-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathcss-vars-design-token.tsx
154 lines (138 loc) · 4.06 KB
/
css-vars-design-token.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
import * as React from 'react';
type Theme = 'dark' | 'light';
interface DesignToken {
[key: string]: string | number | DesignToken;
}
type CssVarsDesignTokenContextType = {
theme: Theme;
themes: Record<Theme, DesignToken>;
token: DesignToken;
setTheme: (theme: Theme) => void;
toggle: () => void;
mod: (name: string, value?: string | number) => void;
};
const CssVarsDesignTokenContext = React.createContext<
CssVarsDesignTokenContextType | undefined
>(undefined);
export function useCssVarsDesignTokenContext<Token extends DesignToken>() {
const context = React.useContext(CssVarsDesignTokenContext);
if (!context) throw new Error('no context found for CssVarsDesignToken');
return context as typeof context & { token: Token };
}
export const CssVarsDesignTokenProvider = ({
children,
themes,
theme: initial,
style,
}: React.PropsWithChildren<{
themes: CssVarsDesignTokenContextType['themes'];
theme?: Theme;
style?: React.CSSProperties;
}>) => {
const [theme, dispatchThemeSetter] = React.useState(
initial || getPreferredTheme(),
);
const [mods, setMods] = React.useState<Record<string, string | number>>({});
const token: DesignToken = React.useMemo(() => {
const clone = JSON.parse(JSON.stringify(themes[theme]));
Object.keys(mods).forEach((key) => {
const modValue = mods[key];
if (modValue !== undefined) dottedSetter(clone, key, mods[key]);
});
return clone;
}, [themes, theme, mods]);
const setTheme = React.useCallback(
(theme: Theme | 'auto') =>
dispatchThemeSetter(theme === 'auto' ? getPreferredTheme() : theme),
[dispatchThemeSetter],
);
const toggle = React.useCallback(() => {
dispatchThemeSetter(theme === 'dark' ? 'light' : 'dark');
}, [theme, dispatchThemeSetter]);
const mod = React.useCallback(
(name: string, value: string | number | undefined) => {
setMods((prev) => ({ ...prev, [name]: value }));
},
[setMods],
);
return (
<CssVarsDesignTokenContext.Provider
value={{
themes,
token,
theme,
toggle,
setTheme,
mod,
}}
>
<div
style={{
height: 'inherit',
width: 'inherit',
...toCssVars(token),
...style,
}}
>
{children}
</div>
</CssVarsDesignTokenContext.Provider>
);
};
export function toCssVars(
obj: DesignToken,
parentKey: string = '-',
): Record<string, string | number> {
return Object.keys(obj).reduce(
(acc, key) => {
const prefixedKey = parentKey ? `${parentKey}-${key}` : key;
if (typeof obj[key] === 'object') {
Object.assign(acc, toCssVars(obj[key] as DesignToken, prefixedKey));
} else {
acc[prefixedKey] = obj[key] as string | number;
}
return acc;
},
{} as Record<string, string | number>,
);
}
function dottedSetter(
object: DesignToken,
path: string,
value: number | string,
) {
const pathArray = Array.isArray(path) ? path : path.split('.');
let currentObject = object;
for (let i = 0; i < pathArray.length - 1; i++) {
const key = pathArray[i];
if (!currentObject.hasOwnProperty(key)) {
currentObject[key] = {};
}
currentObject = currentObject[key] as DesignToken;
}
currentObject[pathArray[pathArray.length - 1]] = value;
return object;
}
function dottedLookup(
object: DesignToken,
path: string,
defaultValue: number | string = undefined,
) {
const pathArray = Array.isArray(path) ? path : path.split('.');
if (!object) return defaultValue;
let currentObject: DesignToken | typeof defaultValue = object;
for (let i = 0; i < pathArray.length; i++) {
if (typeof currentObject !== 'object') return defaultValue;
currentObject = currentObject[pathArray[i]] as typeof currentObject;
if (currentObject === undefined) currentObject = defaultValue;
}
return currentObject;
}
const getPreferredTheme = () => {
const fn = window?.matchMedia;
return fn && fn('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
};
export const __internal__ = {
dottedLookup,
dottedSetter,
};