@os-design/theming-tools 1.0.13 → 1.0.15

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 CHANGED
@@ -1,11 +1,12 @@
1
1
  {
2
2
  "name": "@os-design/theming-tools",
3
- "version": "1.0.13",
3
+ "version": "1.0.15",
4
4
  "license": "UNLICENSED",
5
5
  "repository": "git@gitlab.com:os-team/libs/os-design.git",
6
6
  "main": "dist/cjs/index.js",
7
7
  "module": "dist/esm/index.js",
8
8
  "types": "dist/types/index.d.ts",
9
+ "react-native": "src/index.ts",
9
10
  "exports": {
10
11
  ".": {
11
12
  "require": "./dist/cjs/index.js",
@@ -14,7 +15,14 @@
14
15
  "./package.json": "./package.json"
15
16
  },
16
17
  "files": [
17
- "dist"
18
+ "dist",
19
+ "src",
20
+ "!**/*.test.ts",
21
+ "!**/*.test.tsx",
22
+ "!**/__tests__",
23
+ "!**/*.stories.tsx",
24
+ "!**/*.stories.mdx",
25
+ "!**/*.example.tsx"
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": "0e88d3afc41e36cee61222a039ef1aa4d08115b5"
46
+ "gitHead": "e5d8409760608145d2c738aa5789d0465ae5416f"
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;