@os-design/theming-tools 1.0.13 → 1.0.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +11 -3
- package/src/index.ts +12 -0
- package/src/utils/color.ts +18 -0
- package/src/utils/createThemeContext.ts +40 -0
- package/src/utils/createThemeOverrider.tsx +86 -0
- package/src/utils/createThemeProvider.tsx +91 -0
- package/src/utils/createUseTheme.ts +32 -0
- package/src/utils/overrideTheme.ts +16 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@os-design/theming-tools",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.14",
|
|
4
4
|
"license": "UNLICENSED",
|
|
5
5
|
"repository": "git@gitlab.com:os-team/libs/os-design.git",
|
|
6
6
|
"main": "dist/cjs/index.js",
|
|
@@ -14,7 +14,15 @@
|
|
|
14
14
|
"./package.json": "./package.json"
|
|
15
15
|
},
|
|
16
16
|
"files": [
|
|
17
|
-
"dist"
|
|
17
|
+
"dist",
|
|
18
|
+
"src",
|
|
19
|
+
"!**/*.test.ts",
|
|
20
|
+
"!**/*.test.tsx",
|
|
21
|
+
"!**/__tests__",
|
|
22
|
+
"!**/*.stories.tsx",
|
|
23
|
+
"!**/*.stories.mdx",
|
|
24
|
+
"!**/*.example.tsx",
|
|
25
|
+
"!**/*.emotion.d.ts"
|
|
18
26
|
],
|
|
19
27
|
"sideEffects": false,
|
|
20
28
|
"scripts": {
|
|
@@ -35,5 +43,5 @@
|
|
|
35
43
|
"@emotion/react": ">=11",
|
|
36
44
|
"react": ">=18"
|
|
37
45
|
},
|
|
38
|
-
"gitHead": "
|
|
46
|
+
"gitHead": "3d6b264027712ef81a75379fe3fde3c76c3079af"
|
|
39
47
|
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export { default as clr } from './utils/color';
|
|
2
|
+
export { default as createThemeContext } from './utils/createThemeContext';
|
|
3
|
+
export { default as createThemeOverrider } from './utils/createThemeOverrider';
|
|
4
|
+
export { default as createThemeProvider } from './utils/createThemeProvider';
|
|
5
|
+
export { default as createUseTheme } from './utils/createUseTheme';
|
|
6
|
+
export { default as overrideTheme } from './utils/overrideTheme';
|
|
7
|
+
|
|
8
|
+
export * from './utils/color';
|
|
9
|
+
export * from './utils/createThemeContext';
|
|
10
|
+
export * from './utils/createThemeOverrider';
|
|
11
|
+
export * from './utils/createThemeProvider';
|
|
12
|
+
export * from './utils/createUseTheme';
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export type HSLColor = [number, number, number];
|
|
2
|
+
export type HSLAColor = [number, number, number, number];
|
|
3
|
+
export type Color = HSLColor | HSLAColor;
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Converts a color to string.
|
|
7
|
+
*/
|
|
8
|
+
const clr = (color: Color): string => {
|
|
9
|
+
if (!Array.isArray(color)) {
|
|
10
|
+
throw new Error(
|
|
11
|
+
`The color argument must be an array, but got ${typeof color}.`
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
if (color.length === 3) return `hsl(${color[0]}, ${color[1]}%, ${color[2]}%)`;
|
|
15
|
+
return `hsla(${color[0]}, ${color[1]}%, ${color[2]}%, ${color[3]})`;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export default clr;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import React, { Context } from 'react';
|
|
2
|
+
|
|
3
|
+
export interface ThemeMap<T> extends Record<string, T> {
|
|
4
|
+
light: T;
|
|
5
|
+
dark: T;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface ThemeContextProps<T, TM> {
|
|
9
|
+
/**
|
|
10
|
+
* Available themes.
|
|
11
|
+
*/
|
|
12
|
+
themes: TM;
|
|
13
|
+
/**
|
|
14
|
+
* Overrides the active theme parameters.
|
|
15
|
+
*/
|
|
16
|
+
overrides: Partial<T>;
|
|
17
|
+
/**
|
|
18
|
+
* The current active theme.
|
|
19
|
+
*/
|
|
20
|
+
activeTheme: keyof TM;
|
|
21
|
+
/**
|
|
22
|
+
* The callback that changes the active theme.
|
|
23
|
+
*/
|
|
24
|
+
setActiveTheme: (name: string) => void;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export type ThemeContext<T> = Context<ThemeContextProps<T, ThemeMap<T>>>;
|
|
28
|
+
|
|
29
|
+
const createThemeContext = <T>(): ThemeContext<T> =>
|
|
30
|
+
React.createContext<ThemeContextProps<T, ThemeMap<T>>>({
|
|
31
|
+
themes: {
|
|
32
|
+
light: {} as T,
|
|
33
|
+
dark: {} as T,
|
|
34
|
+
},
|
|
35
|
+
overrides: {},
|
|
36
|
+
activeTheme: 'light',
|
|
37
|
+
setActiveTheme: () => undefined,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
export default createThemeContext;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ThemeProvider as EmotionThemeProvider,
|
|
3
|
+
ThemeProviderProps as EmotionThemeProviderProps,
|
|
4
|
+
} from '@emotion/react';
|
|
5
|
+
import React, { useMemo } from 'react';
|
|
6
|
+
|
|
7
|
+
import { ThemeContext, ThemeMap } from './createThemeContext';
|
|
8
|
+
import { UseTheme } from './createUseTheme';
|
|
9
|
+
import overrideTheme from './overrideTheme';
|
|
10
|
+
|
|
11
|
+
export interface ThemeOverriderProps<T> {
|
|
12
|
+
/**
|
|
13
|
+
* The active theme of children.
|
|
14
|
+
* @default undefined
|
|
15
|
+
*/
|
|
16
|
+
activeTheme?: keyof ThemeMap<T>;
|
|
17
|
+
/**
|
|
18
|
+
* Overrides the active theme parameters.
|
|
19
|
+
* @default undefined
|
|
20
|
+
*/
|
|
21
|
+
overrides?: Partial<T> | ((theme: T) => Partial<T>);
|
|
22
|
+
/**
|
|
23
|
+
* The children.
|
|
24
|
+
* @default undefined
|
|
25
|
+
*/
|
|
26
|
+
children?: React.ReactNode;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type ThemeOverrider<T> = React.FC<ThemeOverriderProps<T>>;
|
|
30
|
+
|
|
31
|
+
function createThemeOverrider<T>(
|
|
32
|
+
TC: ThemeContext<T>,
|
|
33
|
+
useTheme: UseTheme<T>
|
|
34
|
+
): ThemeOverrider<T> {
|
|
35
|
+
// eslint-disable-next-line func-names
|
|
36
|
+
return function ({ activeTheme, overrides = {}, children }) {
|
|
37
|
+
const {
|
|
38
|
+
themes,
|
|
39
|
+
theme: parentTheme,
|
|
40
|
+
overrides: parentOverrides,
|
|
41
|
+
activeTheme: parentActiveTheme,
|
|
42
|
+
setActiveTheme,
|
|
43
|
+
} = useTheme();
|
|
44
|
+
|
|
45
|
+
const localActiveTheme = activeTheme || parentActiveTheme;
|
|
46
|
+
|
|
47
|
+
// Parents overrides are ignored if the active theme was specified
|
|
48
|
+
const localOverrides = useMemo<Partial<T>>(
|
|
49
|
+
() => ({
|
|
50
|
+
...(activeTheme ? {} : parentOverrides),
|
|
51
|
+
...(typeof overrides === 'function'
|
|
52
|
+
? (overrides as Function)(parentTheme)
|
|
53
|
+
: overrides),
|
|
54
|
+
}),
|
|
55
|
+
[activeTheme, parentOverrides, overrides, parentTheme]
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
// Make the local theme
|
|
59
|
+
const localTheme = useMemo<T>(
|
|
60
|
+
() => overrideTheme(themes[localActiveTheme], localOverrides),
|
|
61
|
+
[themes, localActiveTheme, localOverrides]
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
const contextValue = useMemo(
|
|
65
|
+
() => ({
|
|
66
|
+
themes,
|
|
67
|
+
overrides: localOverrides,
|
|
68
|
+
activeTheme: localActiveTheme,
|
|
69
|
+
setActiveTheme,
|
|
70
|
+
}),
|
|
71
|
+
[localActiveTheme, localOverrides, setActiveTheme, themes]
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
return (
|
|
75
|
+
<TC.Provider value={contextValue}>
|
|
76
|
+
<EmotionThemeProvider
|
|
77
|
+
theme={localTheme as EmotionThemeProviderProps['theme']}
|
|
78
|
+
>
|
|
79
|
+
{children}
|
|
80
|
+
</EmotionThemeProvider>
|
|
81
|
+
</TC.Provider>
|
|
82
|
+
);
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export default createThemeOverrider;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ThemeProvider as EmotionThemeProvider,
|
|
3
|
+
ThemeProviderProps as EmotionThemeProviderProps,
|
|
4
|
+
} from '@emotion/react';
|
|
5
|
+
import React, { useEffect, useMemo, useState } from 'react';
|
|
6
|
+
|
|
7
|
+
import { ThemeContext, ThemeMap } from './createThemeContext';
|
|
8
|
+
|
|
9
|
+
export interface ThemeProviderProps<T> {
|
|
10
|
+
/**
|
|
11
|
+
* Available themes.
|
|
12
|
+
* @default undefined
|
|
13
|
+
*/
|
|
14
|
+
themes?: ThemeMap<T>;
|
|
15
|
+
/**
|
|
16
|
+
* The theme used by default.
|
|
17
|
+
* @default light
|
|
18
|
+
*/
|
|
19
|
+
initialTheme?: 'light' | 'dark' | string;
|
|
20
|
+
/**
|
|
21
|
+
* The key in the local storage where the active theme is saved.
|
|
22
|
+
* @default theme
|
|
23
|
+
*/
|
|
24
|
+
storageKey?: string;
|
|
25
|
+
/**
|
|
26
|
+
* The children.
|
|
27
|
+
* @default undefined
|
|
28
|
+
*/
|
|
29
|
+
children?: React.ReactNode;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export type ThemeProvider<T> = React.FC<ThemeProviderProps<T>>;
|
|
33
|
+
|
|
34
|
+
export interface LocalStorage {
|
|
35
|
+
getItem: (key: string) => string | null | Promise<string | null>;
|
|
36
|
+
setItem: (key: string, value: string) => void | Promise<void>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function createThemeProvider<T>(
|
|
40
|
+
TC: ThemeContext<T>,
|
|
41
|
+
defaultThemes: ThemeMap<T>,
|
|
42
|
+
localStorage: LocalStorage = {
|
|
43
|
+
getItem: () => null,
|
|
44
|
+
setItem: () => {},
|
|
45
|
+
}
|
|
46
|
+
): ThemeProvider<T> {
|
|
47
|
+
// eslint-disable-next-line func-names
|
|
48
|
+
return function ({
|
|
49
|
+
themes = defaultThemes,
|
|
50
|
+
initialTheme = 'light',
|
|
51
|
+
storageKey = 'theme',
|
|
52
|
+
children,
|
|
53
|
+
}) {
|
|
54
|
+
const [activeTheme, setActiveTheme] = useState(
|
|
55
|
+
themes[initialTheme] ? initialTheme : 'light'
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
const getSavedTheme = async () => {
|
|
60
|
+
const saved = await localStorage.getItem(storageKey);
|
|
61
|
+
if (saved && themes[saved]) setActiveTheme(saved);
|
|
62
|
+
};
|
|
63
|
+
getSavedTheme();
|
|
64
|
+
}, [storageKey, themes]);
|
|
65
|
+
|
|
66
|
+
const contextValue = useMemo(
|
|
67
|
+
() => ({
|
|
68
|
+
themes,
|
|
69
|
+
overrides: {},
|
|
70
|
+
activeTheme,
|
|
71
|
+
setActiveTheme: (name) => {
|
|
72
|
+
setActiveTheme(name);
|
|
73
|
+
localStorage.setItem(storageKey, name);
|
|
74
|
+
},
|
|
75
|
+
}),
|
|
76
|
+
[activeTheme, storageKey, themes]
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<TC.Provider value={contextValue}>
|
|
81
|
+
<EmotionThemeProvider
|
|
82
|
+
theme={themes[activeTheme] as EmotionThemeProviderProps['theme']}
|
|
83
|
+
>
|
|
84
|
+
{children}
|
|
85
|
+
</EmotionThemeProvider>
|
|
86
|
+
</TC.Provider>
|
|
87
|
+
);
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export default createThemeProvider;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { useContext } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
ThemeContext,
|
|
4
|
+
ThemeContextProps,
|
|
5
|
+
ThemeMap,
|
|
6
|
+
} from './createThemeContext';
|
|
7
|
+
import overrideTheme from './overrideTheme';
|
|
8
|
+
|
|
9
|
+
interface UseThemeRes<T> extends ThemeContextProps<T, ThemeMap<T>> {
|
|
10
|
+
/**
|
|
11
|
+
* The object of the active theme.
|
|
12
|
+
*/
|
|
13
|
+
theme: T;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type UseTheme<T> = () => UseThemeRes<T>;
|
|
17
|
+
|
|
18
|
+
function createUseTheme<T>(TC: ThemeContext<T>): UseTheme<T> {
|
|
19
|
+
return (): UseThemeRes<T> => {
|
|
20
|
+
const context = useContext(TC);
|
|
21
|
+
|
|
22
|
+
return {
|
|
23
|
+
...context,
|
|
24
|
+
theme: overrideTheme(
|
|
25
|
+
context.themes[context.activeTheme],
|
|
26
|
+
context.overrides
|
|
27
|
+
),
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export default createUseTheme;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
const overrideTheme = <T>(theme1: T, theme2: Partial<T>): T => {
|
|
2
|
+
const res = {};
|
|
3
|
+
|
|
4
|
+
[theme1, theme2].forEach((theme) => {
|
|
5
|
+
const props = Object.keys(theme);
|
|
6
|
+
props.forEach((prop) => {
|
|
7
|
+
const descriptor = Object.getOwnPropertyDescriptor(theme, prop);
|
|
8
|
+
if (!descriptor) return;
|
|
9
|
+
Object.defineProperty(res, prop, descriptor);
|
|
10
|
+
});
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
return res as T;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export default overrideTheme;
|