@arcblock/ux 2.12.47 → 2.12.49

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.
@@ -0,0 +1,37 @@
1
+ import { ReactNode } from 'react';
2
+ import type { ThemeOptions } from '@mui/material/styles';
3
+ import { LocaleProviderProps } from '../Locale/context';
4
+ import { ThemeProviderProps } from '../Theme/theme-provider';
5
+ import { ThemeMode } from '../type';
6
+ export interface ConfigContextType {
7
+ mode: ThemeMode;
8
+ themeOptions: ThemeOptions;
9
+ toggleMode: () => void;
10
+ }
11
+ export declare function isThemeRecord(theme: ThemeOptions | Record<ThemeMode, ThemeOptions>): theme is Record<ThemeMode, ThemeOptions>;
12
+ export interface ConfigProviderProps extends Omit<LocaleProviderProps, 'translations'>, Omit<ThemeProviderProps, 'theme'> {
13
+ children: ReactNode;
14
+ prefer?: ThemeMode;
15
+ theme?: ThemeOptions | Record<ThemeMode, ThemeOptions>;
16
+ translations?: Record<string, any>;
17
+ }
18
+ /**
19
+ * 集中化配置
20
+ */
21
+ export declare function ConfigProvider({ children, prefer, theme: themeOptions, injectFirst, locale, fallbackLocale, translations, languages, onLoadingTranslation, }: ConfigProviderProps): import("react/jsx-runtime").JSX.Element;
22
+ export declare namespace ConfigProvider {
23
+ var useConfig: typeof import("./config-provider").useConfig;
24
+ }
25
+ export declare function useConfig(): {
26
+ mode: ThemeMode;
27
+ themeOptions: ThemeOptions;
28
+ toggleMode: () => void;
29
+ locale: import("../type").Locale;
30
+ changeLocale: (locale: import("../type").Locale) => void;
31
+ t: (key: string, data?: Record<string, any>) => string;
32
+ languages: {
33
+ code: string;
34
+ name: string;
35
+ }[];
36
+ theme: import("@mui/material/styles").Theme;
37
+ };
@@ -0,0 +1,93 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { createContext, useContext, useMemo, useState, useCallback } from 'react';
3
+ import useMediaQuery from '@mui/material/useMediaQuery';
4
+ import set from 'lodash/set';
5
+ import { LocaleProvider, useLocaleContext } from '../Locale/context';
6
+ import ThemeProvider from '../Theme/theme-provider';
7
+ import { createTheme, useTheme } from '../Theme';
8
+ const ConfigContext = /*#__PURE__*/createContext({});
9
+ const preferThemeModeKey = 'blocklet_theme_prefer';
10
+ export function isThemeRecord(theme) {
11
+ return 'light' in theme || 'dark' in theme;
12
+ }
13
+ /**
14
+ * 集中化配置
15
+ */
16
+ export function ConfigProvider({
17
+ children,
18
+ // theme config
19
+ prefer,
20
+ theme: themeOptions,
21
+ injectFirst,
22
+ // locale config
23
+ locale,
24
+ fallbackLocale,
25
+ translations = {},
26
+ languages,
27
+ onLoadingTranslation
28
+ }) {
29
+ const prefersDarkMode = useMediaQuery('(prefers-color-scheme: dark)');
30
+ const [mode, setMode] = useState(() => {
31
+ const preferMode = localStorage.getItem(preferThemeModeKey);
32
+ if (preferMode && (preferMode === 'light' || preferMode === 'dark')) {
33
+ return preferMode;
34
+ }
35
+ return prefer || (prefersDarkMode ? 'dark' : 'light');
36
+ });
37
+ const _themeOptions = useMemo(() => {
38
+ let result = {};
39
+ if (themeOptions) {
40
+ if (isThemeRecord(themeOptions)) {
41
+ result = themeOptions[mode] ? {
42
+ ...themeOptions[mode]
43
+ } : {};
44
+ } else {
45
+ result = {
46
+ ...themeOptions
47
+ };
48
+ }
49
+ }
50
+ set(result, 'palette.mode', mode);
51
+ set(result, 'mode', mode);
52
+ return result;
53
+ }, [mode, themeOptions]);
54
+ const theme = useMemo(() => {
55
+ return createTheme(_themeOptions);
56
+ }, [_themeOptions]);
57
+ const toggleMode = useCallback(() => {
58
+ const newMode = mode === 'light' ? 'dark' : 'light';
59
+ setMode(newMode);
60
+ localStorage.setItem(preferThemeModeKey, newMode);
61
+ }, [mode, setMode]);
62
+ const config = useMemo(() => ({
63
+ mode,
64
+ themeOptions: _themeOptions,
65
+ toggleMode
66
+ }), [mode, _themeOptions, toggleMode]);
67
+ return /*#__PURE__*/_jsx(ConfigContext.Provider, {
68
+ value: config,
69
+ children: /*#__PURE__*/_jsx(LocaleProvider, {
70
+ locale: locale,
71
+ fallbackLocale: fallbackLocale,
72
+ translations: translations,
73
+ onLoadingTranslation: onLoadingTranslation,
74
+ languages: languages,
75
+ children: /*#__PURE__*/_jsx(ThemeProvider, {
76
+ theme: theme,
77
+ injectFirst: injectFirst,
78
+ children: children
79
+ })
80
+ })
81
+ });
82
+ }
83
+ export function useConfig() {
84
+ const theme = useTheme();
85
+ const localeCtx = useLocaleContext();
86
+ const configCtx = useContext(ConfigContext);
87
+ return {
88
+ theme,
89
+ ...localeCtx,
90
+ ...configCtx
91
+ };
92
+ }
93
+ ConfigProvider.useConfig = useConfig;
@@ -0,0 +1 @@
1
+ export default function ThemeModeToggle(): import("react/jsx-runtime").JSX.Element | null;
@@ -0,0 +1,22 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { IconButton } from '@mui/material';
3
+ import LightModeOutlinedIcon from '@mui/icons-material/LightModeOutlined';
4
+ import Brightness2OutlinedIcon from '@mui/icons-material/Brightness2Outlined';
5
+ import { useConfig } from './config-provider';
6
+ export default function ThemeModeToggle() {
7
+ const {
8
+ mode,
9
+ toggleMode
10
+ } = useConfig();
11
+ if (!toggleMode) {
12
+ if (process.env.NODE_ENV !== 'production') {
13
+ console.warn('Please ensure the component is wrapped with ConfigProvider context');
14
+ }
15
+ return null;
16
+ }
17
+ return /*#__PURE__*/_jsx(IconButton, {
18
+ onClick: toggleMode,
19
+ color: "inherit",
20
+ children: mode === 'light' ? /*#__PURE__*/_jsx(LightModeOutlinedIcon, {}) : /*#__PURE__*/_jsx(Brightness2OutlinedIcon, {})
21
+ });
22
+ }
@@ -4,7 +4,7 @@ declare const getLocale: (languages?: {
4
4
  code: string;
5
5
  }[]) => string;
6
6
  declare const setLocale: (locale: Locale) => void;
7
- interface LocaleProviderProps {
7
+ export interface LocaleProviderProps {
8
8
  children: ReactNode;
9
9
  locale?: Locale;
10
10
  fallbackLocale?: Locale;
@@ -1,13 +1,14 @@
1
1
  import PropTypes from 'prop-types';
2
2
  import { Theme } from '@mui/material/styles';
3
+ export interface ThemeProviderProps {
4
+ children?: React.ReactNode;
5
+ theme: Theme;
6
+ injectFirst?: boolean;
7
+ }
3
8
  /**
4
9
  * 默认的 theme provider, 可以为 webapp/blocklet 快捷的配置好 mui theme provider
5
10
  */
6
- declare function ThemeProvider({ children, theme, injectFirst, }: {
7
- children?: React.ReactNode;
8
- theme: Theme;
9
- injectFirst: boolean;
10
- }): import("react/jsx-runtime").JSX.Element;
11
+ declare function ThemeProvider({ children, theme, injectFirst }: ThemeProviderProps): import("react/jsx-runtime").JSX.Element;
11
12
  declare namespace ThemeProvider {
12
13
  var propTypes: {
13
14
  children: PropTypes.Requireable<any>;
@@ -5,7 +5,6 @@ import StyledEngineProvider from '@mui/material/StyledEngineProvider';
5
5
  import CssBaseline from '@mui/material/CssBaseline';
6
6
  import { createTheme } from './theme';
7
7
  const defaultTheme = createTheme();
8
-
9
8
  /**
10
9
  * 默认的 theme provider, 可以为 webapp/blocklet 快捷的配置好 mui theme provider
11
10
  */
@@ -1,5 +1,5 @@
1
- import { Components, type Theme, type ThemeOptions } from '@mui/material/styles';
2
- import { Typography, TypographyOptions } from '@mui/material/styles/createTypography';
1
+ import { Components, type ThemeOptions } from '@mui/material/styles';
2
+ import type { Typography } from '@mui/material/styles/createTypography';
3
3
  import '@fontsource/inter/latin-300.css';
4
4
  import '@fontsource/inter/latin-400.css';
5
5
  import '@fontsource/inter/latin-500.css';
@@ -8,9 +8,10 @@ import '@fontsource/inter/latin-ext-300.css';
8
8
  import '@fontsource/inter/latin-ext-400.css';
9
9
  import '@fontsource/inter/latin-ext-500.css';
10
10
  import '@fontsource/inter/latin-ext-700.css';
11
+ import { ThemeMode } from '../type';
11
12
  declare module '@mui/material/styles' {
12
13
  interface Theme {
13
- mode?: string;
14
+ mode?: ThemeMode;
14
15
  themeName?: string;
15
16
  pageWidth?: string;
16
17
  colors?: Record<string, string>;
@@ -23,24 +24,26 @@ declare module '@mui/material/styles' {
23
24
  };
24
25
  }
25
26
  interface ThemeOptions {
26
- mode?: string;
27
27
  themeName?: string;
28
+ mode?: ThemeMode;
28
29
  pageWidth?: string;
29
30
  colors?: Record<string, string>;
31
+ /** @deprecated 请使用 components */
32
+ overrides?: Components<Omit<Theme, 'components'>>;
30
33
  }
31
34
  interface TypeText {
32
35
  hint: string;
33
36
  }
34
37
  }
35
- export declare const create: ({ mode, pageWidth, typography, overrides, palette, components, ...rest }?: {
36
- mode?: string;
37
- pageWidth?: string;
38
- typography?: TypographyOptions;
39
- overrides?: Components<Omit<Theme, "components">>;
40
- } & ThemeOptions) => Theme;
41
- export declare const createTheme: ({ mode, pageWidth, typography, overrides, palette, components, ...rest }?: {
42
- mode?: string;
43
- pageWidth?: string;
44
- typography?: TypographyOptions;
45
- overrides?: Components<Omit<Theme, "components">>;
46
- } & ThemeOptions) => Theme;
38
+ declare module '@mui/material/styles/createTypography' {
39
+ interface TypographyOptions {
40
+ useNextVariants?: boolean;
41
+ color?: Record<string, string>;
42
+ button?: {
43
+ fontWeight?: number;
44
+ };
45
+ }
46
+ }
47
+ export declare function createDefaultThemeOptions(mode?: ThemeMode): ThemeOptions;
48
+ export declare const create: ({ mode, pageWidth, overrides, palette, components, ...rest }?: ThemeOptions) => import("@mui/material/styles").Theme;
49
+ export declare const createTheme: ({ mode, pageWidth, overrides, palette, components, ...rest }?: ThemeOptions) => import("@mui/material/styles").Theme;
@@ -1,6 +1,7 @@
1
1
  /* eslint-disable no-shadow */
2
2
  // https://app.zeplin.io/styleguide/5d1436f1e97c2156f49c0725/colors
3
3
  import { createTheme as _createTheme, responsiveFontSizes } from '@mui/material/styles';
4
+ import { deepmerge } from '@mui/utils';
4
5
  // 为了避免加载全量的字体导致打包后体积太大,目前只选择了 latin 语系的字体
5
6
  import '@fontsource/inter/latin-300.css';
6
7
  import '@fontsource/inter/latin-400.css';
@@ -11,49 +12,29 @@ import '@fontsource/inter/latin-ext-400.css';
11
12
  import '@fontsource/inter/latin-ext-500.css';
12
13
  import '@fontsource/inter/latin-ext-700.css';
13
14
  import colors from '../Colors';
15
+ import { cleanedObj } from '../Util';
14
16
 
15
17
  // 扩展 Theme
16
18
 
19
+ // 扩展 TypographyOptions
20
+
17
21
  const muiDarkTheme = _createTheme({
18
22
  palette: {
19
23
  mode: 'dark'
20
24
  }
21
25
  });
22
-
23
- // https://material-ui.com/customization/default-theme/
24
- export const create = ({
25
- mode = 'light',
26
- pageWidth = 'md',
27
- typography,
28
- /** @deprecated 使用 components 替代 */
29
- overrides,
30
- // original theme options
31
- palette,
32
- components,
33
- ...rest
34
- } = {}) => {
35
- // palette 考虑 light & dark mode, dark mode 需要持续完善
36
- // - 能配合 ColorModeContext 使用
37
- // - 为 dark mode 系统的设计整个 palette, 不要单个 color 设置
38
- const _palette = mode === 'light' ? Object.assign({
39
- ...colors,
40
- background: {
41
- paper: colors.common.white,
42
- default: colors.background.default
43
- },
44
- mode
45
- }, palette || {}) : Object.assign({
46
- ...muiDarkTheme.palette,
47
- background: {
48
- paper: colors.grey[900],
49
- default: colors.grey[900]
26
+ const DEFAULT_FONT_FAMILY = ['Inter', 'Avenir', '-apple-system', 'BlinkMacSystemFont', '"Segoe UI"', 'Roboto', '"Helvetica Neue"', 'Arial', 'sans-serif', '"Apple Color Emoji"', '"Segoe UI Emoji"', '"Segoe UI Symbol"'].join(',');
27
+ export function createDefaultThemeOptions(mode = 'light') {
28
+ const result = {
29
+ palette: {
30
+ mode,
31
+ ...colors,
32
+ background: {
33
+ paper: colors.common.white,
34
+ default: colors.background.default
35
+ }
50
36
  },
51
- mode
52
- }, palette || {});
53
- const theme = _createTheme({
54
- themeName: 'ArcBlock',
55
- palette: _palette,
56
- typography: Object.assign({
37
+ typography: {
57
38
  useNextVariants: true,
58
39
  color: {
59
40
  // 此处 #222222 必须硬编码, layout/sidebar.js -> Icon/image 加载图片时 color 会影响加载路径
@@ -63,12 +44,12 @@ export const create = ({
63
44
  main: mode === 'light' ? '#222222' : colors.common.white,
64
45
  gray: mode === 'light' ? colors.grey[500] : colors.grey[300]
65
46
  },
66
- fontFamily: ['Inter', 'Avenir', '-apple-system', 'BlinkMacSystemFont', '"Segoe UI"', 'Roboto', '"Helvetica Neue"', 'Arial', 'sans-serif', '"Apple Color Emoji"', '"Segoe UI Emoji"', '"Segoe UI Symbol"'].join(','),
47
+ fontFamily: DEFAULT_FONT_FAMILY,
67
48
  // button 默认使用粗体
68
49
  button: {
69
50
  fontWeight: 700
70
51
  }
71
- }, typography),
52
+ },
72
53
  components: {
73
54
  MuiButton: {
74
55
  styleOverrides: {
@@ -111,14 +92,45 @@ export const create = ({
111
92
  color: mode === 'light' ? colors.grey[900] : colors.grey[300]
112
93
  }
113
94
  }
114
- },
95
+ }
96
+ }
97
+ };
98
+ // 深色主题
99
+ if (mode === 'dark') {
100
+ result.palette = {
101
+ ...muiDarkTheme.palette,
102
+ background: {
103
+ paper: colors.grey[900],
104
+ default: colors.grey[900]
105
+ }
106
+ };
107
+ }
108
+ return result;
109
+ }
110
+
111
+ // https://material-ui.com/customization/default-theme/
112
+ export const create = ({
113
+ mode = 'light',
114
+ pageWidth = 'md',
115
+ overrides,
116
+ // original theme options
117
+ palette,
118
+ components,
119
+ ...rest
120
+ } = {}) => {
121
+ const userThemeOptions = {
122
+ themeName: 'ArcBlock',
123
+ palette: {
124
+ ...palette,
125
+ mode
126
+ },
127
+ components: {
115
128
  ...overrides,
116
129
  ...components
117
130
  },
131
+ // @TODO 考虑使用 theme.shape.pageWidth
118
132
  pageWidth,
119
- /**
120
- * @deprecated 应使用 theme.palette 中的颜色
121
- */
133
+ // @TODO 考虑使用 theme.palette.common
122
134
  colors: {
123
135
  white: '#FFFFFF',
124
136
  dark: '#4A707C',
@@ -139,9 +151,29 @@ export const create = ({
139
151
  danger: '#D0021B',
140
152
  lightGrey: '#BCBCBC'
141
153
  },
154
+ // @deprecated use theme.palette.mode
142
155
  mode,
143
156
  ...rest
144
- });
157
+ };
158
+ // Blocklet Server 后台配置的全局主题
159
+ const blockletThemeOptions = window.blocklet?.theme?.[mode] ?? {};
160
+ // choose mode
161
+ const defaultThemeOptions = createDefaultThemeOptions(mode);
162
+ // 同官方合并行为
163
+ const mergedThemeOptions = deepmerge(deepmerge(defaultThemeOptions, cleanedObj(blockletThemeOptions)), cleanedObj(userThemeOptions));
164
+ const theme = _createTheme(mergedThemeOptions);
165
+
166
+ /**
167
+ * 响应式字体,配置后,theme.typography 会变为下面的结构
168
+ * {
169
+ * "h1": {
170
+ * "fontSize": "3rem",
171
+ * "@media (min-width:600px)": {
172
+ * "fontSize": "3.3125rem"
173
+ * }
174
+ * }
175
+ * }
176
+ */
145
177
  const enhanced = responsiveFontSizes(theme, {
146
178
  breakpoints: ['xs', 'sm', 'md', 'lg'],
147
179
  disableAlign: false,
@@ -87,4 +87,5 @@ export declare const getTranslation: (translations: TranslationsObject, locale:
87
87
  defaultValue?: string;
88
88
  }) => string;
89
89
  export declare const lazyRetry: (fn: () => Promise<any>) => import("react").LazyExoticComponent<import("react").ComponentType<any>>;
90
+ export declare const cleanedObj: (obj: object) => import("lodash").Dictionary<any>;
90
91
  export {};
package/lib/Util/index.js CHANGED
@@ -3,6 +3,7 @@ import { lazy } from 'react';
3
3
  import padStart from 'lodash/padStart';
4
4
  import { getDIDMotifInfo, colors } from '@arcblock/did-motif';
5
5
  import isNil from 'lodash/isNil';
6
+ import omitBy from 'lodash/omitBy';
6
7
  import pRetry from 'p-retry';
7
8
  import { DID_PREFIX, BLOCKLET_SERVICE_PATH_PREFIX } from './constant';
8
9
  let dateTool = null;
@@ -376,4 +377,7 @@ export const lazyRetry = fn => /*#__PURE__*/lazy(() => pRetry(async () => {
376
377
  // 只需要重试两次,加上原本的一次,总共三次
377
378
  {
378
379
  retries: 2
379
- }));
380
+ }));
381
+ export const cleanedObj = obj => {
382
+ return omitBy(obj, isNil);
383
+ };
package/lib/type.d.ts CHANGED
@@ -4,6 +4,7 @@ import type { LiteralUnion } from 'type-fest';
4
4
  export type $TSFixMe = any;
5
5
  export type Translations = Record<string, Record<string, any>>;
6
6
  export type Locale = LiteralUnion<'en' | 'zh', string>;
7
+ export type ThemeMode = 'light' | 'dark';
7
8
 
8
9
  // TODO: 以下为 blocklet 应用专属的全局对象类型,可以更加具体
9
10
  export type User = Record<string, any>;
@@ -25,7 +26,7 @@ export type Blocklet = {
25
26
  version: string;
26
27
  mode: string;
27
28
  tenantMode: 'single' | 'multiple';
28
- theme: Theme;
29
+ theme: Record<ThemeMode, Theme>;
29
30
  navigation: $TSFixMe[];
30
31
  preferences: Record<string, any>;
31
32
  languages: {
@@ -1,8 +1,10 @@
1
+ import { Breakpoint } from '@mui/material';
1
2
  import { type PaletteOptions } from '@mui/material/styles/createPalette';
2
3
  import { type TypographyOptions } from '@mui/material/styles/createTypography';
4
+ import { ThemeMode } from '../type';
3
5
  declare function withTheme<P extends object>(Component: React.ComponentType<P>, { mode, pageWidth, palette, typography, }?: {
4
- mode?: string;
5
- pageWidth?: string;
6
+ mode?: ThemeMode;
7
+ pageWidth?: Breakpoint;
6
8
  palette?: PaletteOptions;
7
9
  typography?: TypographyOptions;
8
10
  }): (props: P) => import("react/jsx-runtime").JSX.Element;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arcblock/ux",
3
- "version": "2.12.47",
3
+ "version": "2.12.49",
4
4
  "description": "Common used react components for arcblock products",
5
5
  "keywords": [
6
6
  "react",
@@ -65,15 +65,16 @@
65
65
  "@emotion/styled": "^11.10.4",
66
66
  "@mui/icons-material": ">=5.15.0",
67
67
  "@mui/material": ">=5.15.0",
68
+ "@mui/utils": ">=5.15.0",
68
69
  "react": ">=18.2.0",
69
70
  "react-router-dom": ">=6.22.3"
70
71
  },
71
- "gitHead": "f8b62d37754f4254c8f2961eb778ca2ce71264ee",
72
+ "gitHead": "ffebc9c5df6d576f08c3538b4db75d7ec8cf00be",
72
73
  "dependencies": {
73
74
  "@arcblock/did-motif": "^1.1.13",
74
- "@arcblock/icons": "^2.12.47",
75
- "@arcblock/nft-display": "^2.12.47",
76
- "@arcblock/react-hooks": "^2.12.47",
75
+ "@arcblock/icons": "^2.12.49",
76
+ "@arcblock/nft-display": "^2.12.49",
77
+ "@arcblock/react-hooks": "^2.12.49",
77
78
  "@babel/plugin-syntax-dynamic-import": "^7.8.3",
78
79
  "@fontsource/inter": "^5.0.16",
79
80
  "@fontsource/ubuntu-mono": "^5.0.18",
@@ -0,0 +1,123 @@
1
+ import { createContext, useContext, ReactNode, useMemo, useState, useCallback } from 'react';
2
+ import type { ThemeOptions } from '@mui/material/styles';
3
+ import useMediaQuery from '@mui/material/useMediaQuery';
4
+ import set from 'lodash/set';
5
+ import { LocaleProvider, LocaleProviderProps, useLocaleContext } from '../Locale/context';
6
+ import ThemeProvider, { ThemeProviderProps } from '../Theme/theme-provider';
7
+ import { createTheme, useTheme } from '../Theme';
8
+ import { ThemeMode } from '../type';
9
+
10
+ export interface ConfigContextType {
11
+ mode: ThemeMode;
12
+ themeOptions: ThemeOptions;
13
+ toggleMode: () => void;
14
+ }
15
+
16
+ const ConfigContext = createContext<ConfigContextType>({} as ConfigContextType);
17
+ const preferThemeModeKey = 'blocklet_theme_prefer';
18
+
19
+ export function isThemeRecord(
20
+ theme: ThemeOptions | Record<ThemeMode, ThemeOptions>
21
+ ): theme is Record<ThemeMode, ThemeOptions> {
22
+ return 'light' in theme || 'dark' in theme;
23
+ }
24
+
25
+ export interface ConfigProviderProps
26
+ extends Omit<LocaleProviderProps, 'translations'>,
27
+ Omit<ThemeProviderProps, 'theme'> {
28
+ children: ReactNode;
29
+ prefer?: ThemeMode;
30
+ theme?: ThemeOptions | Record<ThemeMode, ThemeOptions>;
31
+ translations?: Record<string, any>;
32
+ }
33
+
34
+ /**
35
+ * 集中化配置
36
+ */
37
+ export function ConfigProvider({
38
+ children,
39
+ // theme config
40
+ prefer,
41
+ theme: themeOptions,
42
+ injectFirst,
43
+ // locale config
44
+ locale,
45
+ fallbackLocale,
46
+ translations = {},
47
+ languages,
48
+ onLoadingTranslation,
49
+ }: ConfigProviderProps) {
50
+ const prefersDarkMode = useMediaQuery('(prefers-color-scheme: dark)');
51
+ const [mode, setMode] = useState<ThemeMode>(() => {
52
+ const preferMode = localStorage.getItem(preferThemeModeKey) as ThemeMode;
53
+ if (preferMode && (preferMode === 'light' || preferMode === 'dark')) {
54
+ return preferMode;
55
+ }
56
+ return prefer || (prefersDarkMode ? 'dark' : 'light');
57
+ });
58
+
59
+ const _themeOptions = useMemo(() => {
60
+ let result: ThemeOptions = {};
61
+
62
+ if (themeOptions) {
63
+ if (isThemeRecord(themeOptions)) {
64
+ result = themeOptions[mode] ? { ...themeOptions[mode] } : {};
65
+ } else {
66
+ result = { ...themeOptions };
67
+ }
68
+ }
69
+
70
+ set(result, 'palette.mode', mode);
71
+ set(result, 'mode', mode);
72
+
73
+ return result;
74
+ }, [mode, themeOptions]);
75
+
76
+ const theme = useMemo(() => {
77
+ return createTheme(_themeOptions);
78
+ }, [_themeOptions]);
79
+
80
+ const toggleMode = useCallback(() => {
81
+ const newMode = mode === 'light' ? 'dark' : 'light';
82
+ setMode(newMode);
83
+ localStorage.setItem(preferThemeModeKey, newMode);
84
+ }, [mode, setMode]);
85
+
86
+ const config = useMemo(
87
+ () => ({
88
+ mode,
89
+ themeOptions: _themeOptions,
90
+ toggleMode,
91
+ }),
92
+ [mode, _themeOptions, toggleMode]
93
+ );
94
+
95
+ return (
96
+ <ConfigContext.Provider value={config}>
97
+ <LocaleProvider
98
+ locale={locale}
99
+ fallbackLocale={fallbackLocale}
100
+ translations={translations}
101
+ onLoadingTranslation={onLoadingTranslation}
102
+ languages={languages}>
103
+ <ThemeProvider theme={theme} injectFirst={injectFirst}>
104
+ {children}
105
+ </ThemeProvider>
106
+ </LocaleProvider>
107
+ </ConfigContext.Provider>
108
+ );
109
+ }
110
+
111
+ export function useConfig() {
112
+ const theme = useTheme();
113
+ const localeCtx = useLocaleContext();
114
+ const configCtx = useContext(ConfigContext);
115
+
116
+ return {
117
+ theme,
118
+ ...localeCtx,
119
+ ...configCtx,
120
+ };
121
+ }
122
+
123
+ ConfigProvider.useConfig = useConfig;
@@ -0,0 +1,22 @@
1
+ import { IconButton } from '@mui/material';
2
+ import LightModeOutlinedIcon from '@mui/icons-material/LightModeOutlined';
3
+ import Brightness2OutlinedIcon from '@mui/icons-material/Brightness2Outlined';
4
+ import { useConfig } from './config-provider';
5
+
6
+ export default function ThemeModeToggle() {
7
+ const { mode, toggleMode } = useConfig();
8
+
9
+ if (!toggleMode) {
10
+ if (process.env.NODE_ENV !== 'production') {
11
+ console.warn('Please ensure the component is wrapped with ConfigProvider context');
12
+ }
13
+
14
+ return null;
15
+ }
16
+
17
+ return (
18
+ <IconButton onClick={toggleMode} color="inherit">
19
+ {mode === 'light' ? <LightModeOutlinedIcon /> : <Brightness2OutlinedIcon />}
20
+ </IconButton>
21
+ );
22
+ }
@@ -62,7 +62,7 @@ const getLanguages = (arg?: { code: string; name: string }[]): { code: string; n
62
62
  ];
63
63
  };
64
64
 
65
- interface LocaleProviderProps {
65
+ export interface LocaleProviderProps {
66
66
  children: ReactNode;
67
67
  locale?: Locale;
68
68
  fallbackLocale?: Locale;
@@ -6,18 +6,16 @@ import { createTheme } from './theme';
6
6
 
7
7
  const defaultTheme = createTheme();
8
8
 
9
+ export interface ThemeProviderProps {
10
+ children?: React.ReactNode;
11
+ theme: Theme;
12
+ injectFirst?: boolean;
13
+ }
14
+
9
15
  /**
10
16
  * 默认的 theme provider, 可以为 webapp/blocklet 快捷的配置好 mui theme provider
11
17
  */
12
- export default function ThemeProvider({
13
- children,
14
- theme,
15
- injectFirst,
16
- }: {
17
- children?: React.ReactNode;
18
- theme: Theme;
19
- injectFirst: boolean;
20
- }) {
18
+ export default function ThemeProvider({ children, theme, injectFirst }: ThemeProviderProps) {
21
19
  return (
22
20
  // injectFirst 会影响 makeStyles 自定义样式和 mui styles 覆盖问题
23
21
  <StyledEngineProvider injectFirst={injectFirst}>
@@ -1,14 +1,8 @@
1
1
  /* eslint-disable no-shadow */
2
2
  // https://app.zeplin.io/styleguide/5d1436f1e97c2156f49c0725/colors
3
- import {
4
- createTheme as _createTheme,
5
- Components,
6
- PaletteOptions,
7
- responsiveFontSizes,
8
- type Theme,
9
- type ThemeOptions,
10
- } from '@mui/material/styles';
11
- import { Typography, TypographyOptions } from '@mui/material/styles/createTypography';
3
+ import { createTheme as _createTheme, Components, responsiveFontSizes, type ThemeOptions } from '@mui/material/styles';
4
+ import { deepmerge } from '@mui/utils';
5
+ import type { Typography } from '@mui/material/styles/createTypography';
12
6
  // 为了避免加载全量的字体导致打包后体积太大,目前只选择了 latin 语系的字体
13
7
  import '@fontsource/inter/latin-300.css';
14
8
  import '@fontsource/inter/latin-400.css';
@@ -19,11 +13,13 @@ import '@fontsource/inter/latin-ext-400.css';
19
13
  import '@fontsource/inter/latin-ext-500.css';
20
14
  import '@fontsource/inter/latin-ext-700.css';
21
15
  import colors from '../Colors';
16
+ import { ThemeMode } from '../type';
17
+ import { cleanedObj } from '../Util';
22
18
 
23
19
  // 扩展 Theme
24
20
  declare module '@mui/material/styles' {
25
21
  interface Theme {
26
- mode?: string;
22
+ mode?: ThemeMode;
27
23
  themeName?: string;
28
24
  pageWidth?: string;
29
25
  colors?: Record<string, string>;
@@ -36,99 +32,71 @@ declare module '@mui/material/styles' {
36
32
  };
37
33
  }
38
34
  interface ThemeOptions {
39
- mode?: string;
40
35
  themeName?: string;
36
+ mode?: ThemeMode;
41
37
  pageWidth?: string;
42
38
  colors?: Record<string, string>;
39
+ /** @deprecated 请使用 components */
40
+ overrides?: Components<Omit<Theme, 'components'>>;
43
41
  }
44
-
45
42
  interface TypeText {
46
43
  hint: string;
47
44
  }
48
45
  }
46
+ // 扩展 TypographyOptions
47
+ declare module '@mui/material/styles/createTypography' {
48
+ interface TypographyOptions {
49
+ useNextVariants?: boolean;
50
+ color?: Record<string, string>;
51
+ button?: {
52
+ fontWeight?: number;
53
+ };
54
+ }
55
+ }
49
56
 
50
57
  const muiDarkTheme = _createTheme({ palette: { mode: 'dark' } });
51
58
 
52
- // https://material-ui.com/customization/default-theme/
53
- export const create = ({
54
- mode = 'light',
55
- pageWidth = 'md',
56
- typography,
57
- /** @deprecated 使用 components 替代 */
58
- overrides,
59
- // original theme options
60
- palette,
61
- components,
62
- ...rest
63
- }: {
64
- mode?: string;
65
- pageWidth?: string;
66
- typography?: TypographyOptions;
67
- overrides?: Components<Omit<Theme, 'components'>>;
68
- } & ThemeOptions = {}) => {
69
- // palette 考虑 light & dark mode, dark mode 需要持续完善
70
- // - 能配合 ColorModeContext 使用
71
- // - 为 dark mode 系统的设计整个 palette, 不要单个 color 设置
72
- const _palette: PaletteOptions =
73
- mode === 'light'
74
- ? Object.assign(
75
- {
76
- ...colors,
77
- background: {
78
- paper: colors.common.white,
79
- default: colors.background.default,
80
- },
81
- mode,
82
- },
83
- palette || {}
84
- )
85
- : Object.assign(
86
- {
87
- ...muiDarkTheme.palette,
88
- background: {
89
- paper: colors.grey[900],
90
- default: colors.grey[900],
91
- },
92
- mode,
93
- },
94
- palette || {}
95
- );
59
+ const DEFAULT_FONT_FAMILY = [
60
+ 'Inter',
61
+ 'Avenir',
62
+ '-apple-system',
63
+ 'BlinkMacSystemFont',
64
+ '"Segoe UI"',
65
+ 'Roboto',
66
+ '"Helvetica Neue"',
67
+ 'Arial',
68
+ 'sans-serif',
69
+ '"Apple Color Emoji"',
70
+ '"Segoe UI Emoji"',
71
+ '"Segoe UI Symbol"',
72
+ ].join(',');
96
73
 
97
- const theme = _createTheme({
98
- themeName: 'ArcBlock',
99
- palette: _palette,
100
- typography: Object.assign(
101
- {
102
- useNextVariants: true,
103
- color: {
104
- // 此处 #222222 必须硬编码, layout/sidebar.js -> Icon/image 加载图片时 color 会影响加载路径
105
- // TODO: 此处硬编码的色值后面需要改为 colors.grey[900],
106
- // 或者如果可以的话直接删掉 typography#color, 文本颜色建议使用 theme.palette.text 中的色值?
107
- // layout 组件建议重构, sidebar 中建议使用 icon 替换 img (#366)
108
- main: mode === 'light' ? '#222222' : colors.common.white,
109
- gray: mode === 'light' ? colors.grey[500] : colors.grey[300],
110
- },
111
- fontFamily: [
112
- 'Inter',
113
- 'Avenir',
114
- '-apple-system',
115
- 'BlinkMacSystemFont',
116
- '"Segoe UI"',
117
- 'Roboto',
118
- '"Helvetica Neue"',
119
- 'Arial',
120
- 'sans-serif',
121
- '"Apple Color Emoji"',
122
- '"Segoe UI Emoji"',
123
- '"Segoe UI Symbol"',
124
- ].join(','),
125
- // button 默认使用粗体
126
- button: {
127
- fontWeight: 700,
128
- },
74
+ export function createDefaultThemeOptions(mode: ThemeMode = 'light'): ThemeOptions {
75
+ const result: ThemeOptions = {
76
+ palette: {
77
+ mode,
78
+ ...colors,
79
+ background: {
80
+ paper: colors.common.white,
81
+ default: colors.background.default,
82
+ },
83
+ },
84
+ typography: {
85
+ useNextVariants: true,
86
+ color: {
87
+ // 此处 #222222 必须硬编码, layout/sidebar.js -> Icon/image 加载图片时 color 会影响加载路径
88
+ // TODO: 此处硬编码的色值后面需要改为 colors.grey[900],
89
+ // 或者如果可以的话直接删掉 typography#color, 文本颜色建议使用 theme.palette.text 中的色值?
90
+ // layout 组件建议重构, sidebar 中建议使用 icon 替换 img (#366)
91
+ main: mode === 'light' ? '#222222' : colors.common.white,
92
+ gray: mode === 'light' ? colors.grey[500] : colors.grey[300],
93
+ },
94
+ fontFamily: DEFAULT_FONT_FAMILY,
95
+ // button 默认使用粗体
96
+ button: {
97
+ fontWeight: 700,
129
98
  },
130
- typography
131
- ),
99
+ },
132
100
  components: {
133
101
  MuiButton: {
134
102
  styleOverrides: {
@@ -172,13 +140,45 @@ export const create = ({
172
140
  },
173
141
  },
174
142
  },
143
+ },
144
+ };
145
+ // 深色主题
146
+ if (mode === 'dark') {
147
+ result.palette = {
148
+ ...muiDarkTheme.palette,
149
+ background: {
150
+ paper: colors.grey[900],
151
+ default: colors.grey[900],
152
+ },
153
+ };
154
+ }
155
+
156
+ return result;
157
+ }
158
+
159
+ // https://material-ui.com/customization/default-theme/
160
+ export const create = ({
161
+ mode = 'light',
162
+ pageWidth = 'md',
163
+ overrides,
164
+ // original theme options
165
+ palette,
166
+ components,
167
+ ...rest
168
+ }: ThemeOptions = {}) => {
169
+ const userThemeOptions: ThemeOptions = {
170
+ themeName: 'ArcBlock',
171
+ palette: {
172
+ ...palette,
173
+ mode,
174
+ },
175
+ components: {
175
176
  ...overrides,
176
177
  ...components,
177
178
  },
179
+ // @TODO 考虑使用 theme.shape.pageWidth
178
180
  pageWidth,
179
- /**
180
- * @deprecated 应使用 theme.palette 中的颜色
181
- */
181
+ // @TODO 考虑使用 theme.palette.common
182
182
  colors: {
183
183
  white: '#FFFFFF',
184
184
  dark: '#4A707C',
@@ -199,10 +199,33 @@ export const create = ({
199
199
  danger: '#D0021B',
200
200
  lightGrey: '#BCBCBC',
201
201
  },
202
+ // @deprecated use theme.palette.mode
202
203
  mode,
203
204
  ...rest,
204
- });
205
+ };
206
+ // Blocklet Server 后台配置的全局主题
207
+ const blockletThemeOptions = window.blocklet?.theme?.[mode] ?? {};
208
+ // choose mode
209
+ const defaultThemeOptions = createDefaultThemeOptions(mode);
210
+ // 同官方合并行为
211
+ const mergedThemeOptions = deepmerge(
212
+ deepmerge(defaultThemeOptions, cleanedObj(blockletThemeOptions)),
213
+ cleanedObj(userThemeOptions)
214
+ );
215
+
216
+ const theme = _createTheme(mergedThemeOptions);
205
217
 
218
+ /**
219
+ * 响应式字体,配置后,theme.typography 会变为下面的结构
220
+ * {
221
+ * "h1": {
222
+ * "fontSize": "3rem",
223
+ * "@media (min-width:600px)": {
224
+ * "fontSize": "3.3125rem"
225
+ * }
226
+ * }
227
+ * }
228
+ */
206
229
  const enhanced = responsiveFontSizes(theme, {
207
230
  breakpoints: ['xs', 'sm', 'md', 'lg'],
208
231
  disableAlign: false,
package/src/Util/index.ts CHANGED
@@ -3,6 +3,7 @@ import { lazy } from 'react';
3
3
  import padStart from 'lodash/padStart';
4
4
  import { getDIDMotifInfo, colors } from '@arcblock/did-motif';
5
5
  import isNil from 'lodash/isNil';
6
+ import omitBy from 'lodash/omitBy';
6
7
  import pRetry from 'p-retry';
7
8
  import { DID_PREFIX, BLOCKLET_SERVICE_PATH_PREFIX } from './constant';
8
9
  import type { $TSFixMe, Locale } from '../type';
@@ -480,3 +481,7 @@ export const lazyRetry = (fn: () => Promise<any>) =>
480
481
  { retries: 2 }
481
482
  )
482
483
  );
484
+
485
+ export const cleanedObj = (obj: object) => {
486
+ return omitBy(obj, isNil);
487
+ };
package/src/type.d.ts CHANGED
@@ -4,6 +4,7 @@ import type { LiteralUnion } from 'type-fest';
4
4
  export type $TSFixMe = any;
5
5
  export type Translations = Record<string, Record<string, any>>;
6
6
  export type Locale = LiteralUnion<'en' | 'zh', string>;
7
+ export type ThemeMode = 'light' | 'dark';
7
8
 
8
9
  // TODO: 以下为 blocklet 应用专属的全局对象类型,可以更加具体
9
10
  export type User = Record<string, any>;
@@ -25,7 +26,7 @@ export type Blocklet = {
25
26
  version: string;
26
27
  mode: string;
27
28
  tenantMode: 'single' | 'multiple';
28
- theme: Theme;
29
+ theme: Record<ThemeMode, Theme>;
29
30
  navigation: $TSFixMe[];
30
31
  preferences: Record<string, any>;
31
32
  languages: {
@@ -1,10 +1,12 @@
1
1
  import { useEffect } from 'react';
2
2
  import { Global, css } from '@emotion/react';
3
3
  import CssBaseline from '@mui/material/CssBaseline';
4
+ import { Breakpoint } from '@mui/material';
4
5
  import { type PaletteOptions } from '@mui/material/styles/createPalette';
5
6
  import { type TypographyOptions } from '@mui/material/styles/createTypography';
6
7
 
7
8
  import { createTheme, ThemeProvider } from '../Theme';
9
+ import { ThemeMode } from '../type';
8
10
 
9
11
  function withTheme<P extends object>(
10
12
  Component: React.ComponentType<P>,
@@ -13,7 +15,7 @@ function withTheme<P extends object>(
13
15
  pageWidth = 'md',
14
16
  palette,
15
17
  typography,
16
- }: { mode?: string; pageWidth?: string; palette?: PaletteOptions; typography?: TypographyOptions } = {}
18
+ }: { mode?: ThemeMode; pageWidth?: Breakpoint; palette?: PaletteOptions; typography?: TypographyOptions } = {}
17
19
  ) {
18
20
  const theme = createTheme({ mode, pageWidth, palette, typography });
19
21