@arcblock/ux 2.13.26 → 2.13.28

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.
@@ -12,6 +12,7 @@ export declare function ConfigProvider({ children, theme, injectFirst, darkSchem
12
12
  export declare function useConfig(): {
13
13
  mode: import("@mui/material").PaletteMode;
14
14
  toggleMode: () => void;
15
+ changeMode: (mode: import("@mui/material").PaletteMode) => void;
15
16
  prefer?: import("../Theme").Prefer;
16
17
  locale: import("../type").Locale;
17
18
  changeLocale: (locale: import("../type").Locale) => void;
@@ -6,9 +6,10 @@ export interface RelativeTimeProps {
6
6
  withoutSuffix?: false | true;
7
7
  from?: string | number;
8
8
  to?: string | number;
9
- type?: 'relative' | 'absolute';
9
+ type?: 'relative' | 'absolute' | 'utc' | 'all';
10
10
  tz?: string;
11
11
  relativeRange?: number;
12
12
  enableTooltip?: boolean;
13
+ showUTCPrefix?: boolean;
13
14
  }
14
- export default function RelativeTime({ value, locale, withoutSuffix, from, to, type, tz, relativeRange, enableTooltip, ...rest }: RelativeTimeProps): "-" | import("react/jsx-runtime").JSX.Element;
15
+ export default function RelativeTime({ value, locale, withoutSuffix, from, to, type, tz, relativeRange, enableTooltip, showUTCPrefix, ...rest }: RelativeTimeProps): import("react/jsx-runtime").JSX.Element;
@@ -1,8 +1,9 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Tooltip } from '@mui/material';
3
+ import { useState } from 'react';
2
4
  import relativeTime from 'dayjs/plugin/relativeTime';
3
5
  import dayjs from 'dayjs';
4
6
  import 'dayjs/locale/zh-cn';
5
- import { Tooltip } from '@mui/material';
6
7
  import localizedFormat from 'dayjs/plugin/localizedFormat';
7
8
  import utc from 'dayjs/plugin/utc';
8
9
  import timezone from 'dayjs/plugin/timezone';
@@ -33,7 +34,15 @@ dayjs.updateLocale('zh-cn', {
33
34
  });
34
35
  // FIXME: @@ 此处不能真正的将 relativeTime 设置为支持中文
35
36
  setDateTool(dayjs);
36
- export default function RelativeTime({
37
+ const translations = {
38
+ en: {
39
+ utc: 'Your Timezone'
40
+ },
41
+ zh: {
42
+ utc: '你所在时区'
43
+ }
44
+ };
45
+ function useRelativeTime({
37
46
  value,
38
47
  locale = 'en',
39
48
  withoutSuffix = false,
@@ -41,17 +50,31 @@ export default function RelativeTime({
41
50
  to = '',
42
51
  type = 'relative',
43
52
  tz,
44
- relativeRange,
45
- enableTooltip = true,
46
- ...rest
53
+ relativeRange
47
54
  }) {
55
+ const date = new Date();
56
+ const timeZoneOffset = date.getTimezoneOffset();
57
+ const sign = timeZoneOffset > 0 ? '-' : '+';
58
+ const hoursOffset = Math.abs(timeZoneOffset) / 60;
59
+ const isLocalUtc = timeZoneOffset === 0;
60
+ const [isUtc, setIsUtc] = useState(isLocalUtc);
48
61
  if (!value) {
49
- return '-';
62
+ return {
63
+ innerContent: '-',
64
+ popContent: '-',
65
+ isUtc,
66
+ setIsUtc,
67
+ sign,
68
+ hoursOffset
69
+ };
50
70
  }
51
71
  const localeOption = locale === 'zh' ? 'zh-cn' : 'en-us';
52
- const datetime = dayjs(value).locale(localeOption);
72
+ let datetime = dayjs(value).locale(localeOption);
73
+ if (type === 'utc') {
74
+ datetime = dayjs(value).utc().locale(localeOption);
75
+ }
53
76
  if (tz) {
54
- datetime.tz(tz);
77
+ datetime = datetime.tz(tz);
55
78
  }
56
79
  const absoluteString = formatToDatetime(value, {
57
80
  locale: localeOption,
@@ -77,13 +100,156 @@ export default function RelativeTime({
77
100
  innerContent = absoluteString;
78
101
  popContent = relativeString;
79
102
  }
103
+ if (type === 'utc') {
104
+ if (isUtc) {
105
+ innerContent = formatToDatetime(value, {
106
+ locale: localeOption,
107
+ tz,
108
+ isUtc: true
109
+ });
110
+ popContent = formatToDatetime(value, {
111
+ locale: localeOption,
112
+ tz,
113
+ isUtc: true
114
+ });
115
+ } else {
116
+ innerContent = absoluteString;
117
+ popContent = relativeString;
118
+ }
119
+ }
120
+ return {
121
+ innerContent,
122
+ popContent,
123
+ isUtc,
124
+ setIsUtc,
125
+ sign,
126
+ hoursOffset,
127
+ relativeString,
128
+ absoluteString
129
+ };
130
+ }
131
+ function UTCChip({
132
+ locale,
133
+ isUtc,
134
+ sign,
135
+ hoursOffset,
136
+ setIsUtc,
137
+ showUTCPrefix = true
138
+ }) {
139
+ const prefix = showUTCPrefix ? `${translations[locale].utc}: ` : '';
140
+ return /*#__PURE__*/_jsx(Box, {
141
+ component: "span",
142
+ sx: {
143
+ color: 'inherit',
144
+ cursor: 'pointer',
145
+ border: '1px solid',
146
+ fontSize: '0.8rem',
147
+ borderColor: 'divider',
148
+ borderRadius: '20px',
149
+ padding: '4px 8px',
150
+ lineHeight: 1
151
+ },
152
+ onClick: () => setIsUtc(r => !r),
153
+ children: `${prefix}${isUtc ? 'UTC' : `UTC${sign}${hoursOffset}`}`
154
+ });
155
+ }
156
+ export default function RelativeTime({
157
+ value,
158
+ locale = 'en',
159
+ withoutSuffix = false,
160
+ from = '',
161
+ to = '',
162
+ type = 'relative',
163
+ tz,
164
+ relativeRange,
165
+ enableTooltip = true,
166
+ showUTCPrefix = true,
167
+ ...rest
168
+ }) {
169
+ const {
170
+ innerContent,
171
+ popContent,
172
+ isUtc,
173
+ setIsUtc,
174
+ sign,
175
+ hoursOffset,
176
+ relativeString
177
+ } = useRelativeTime({
178
+ value,
179
+ locale,
180
+ withoutSuffix,
181
+ from,
182
+ to,
183
+ type: type === 'all' ? 'utc' : type,
184
+ tz,
185
+ relativeRange
186
+ });
187
+ if (type === 'all') {
188
+ return /*#__PURE__*/_jsx(Tooltip, {
189
+ title: undefined,
190
+ placement: "top-end",
191
+ enterTouchDelay: 0,
192
+ children: /*#__PURE__*/_jsxs(Box, {
193
+ display: "flex",
194
+ alignItems: "center",
195
+ gap: 0.5,
196
+ width: "max-content",
197
+ ...rest,
198
+ children: [/*#__PURE__*/_jsx(Box, {
199
+ component: "span",
200
+ ...rest,
201
+ sx: {},
202
+ children: innerContent
203
+ }), /*#__PURE__*/_jsx(Box, {
204
+ component: "span",
205
+ sx: {
206
+ color: 'inherit'
207
+ },
208
+ children: "\xB7"
209
+ }), /*#__PURE__*/_jsx(Box, {
210
+ component: "span",
211
+ sx: {
212
+ color: 'text.secondary'
213
+ },
214
+ children: relativeString
215
+ }), /*#__PURE__*/_jsx(Box, {
216
+ component: "span",
217
+ sx: {
218
+ color: 'inherit'
219
+ },
220
+ children: "\xB7"
221
+ }), /*#__PURE__*/_jsx(UTCChip, {
222
+ locale: locale,
223
+ isUtc: isUtc,
224
+ sign: sign,
225
+ hoursOffset: hoursOffset,
226
+ setIsUtc: setIsUtc,
227
+ showUTCPrefix: showUTCPrefix
228
+ })]
229
+ })
230
+ });
231
+ }
80
232
  return /*#__PURE__*/_jsx(Tooltip, {
81
233
  title: enableTooltip ? popContent : undefined,
82
234
  placement: "top-end",
83
235
  enterTouchDelay: 0,
84
- children: /*#__PURE__*/_jsx("span", {
85
- ...rest,
86
- children: innerContent
236
+ children: /*#__PURE__*/_jsxs(Box, {
237
+ display: "flex",
238
+ alignItems: "center",
239
+ gap: 1,
240
+ width: "max-content",
241
+ children: [/*#__PURE__*/_jsx(Box, {
242
+ component: "span",
243
+ ...rest,
244
+ children: innerContent
245
+ }), type === 'utc' && /*#__PURE__*/_jsx(UTCChip, {
246
+ locale: locale,
247
+ isUtc: isUtc,
248
+ sign: sign,
249
+ hoursOffset: hoursOffset,
250
+ setIsUtc: setIsUtc,
251
+ showUTCPrefix: showUTCPrefix
252
+ })]
87
253
  })
88
254
  });
89
255
  }
package/lib/Tabs/index.js CHANGED
@@ -41,7 +41,7 @@ function CardTabs({
41
41
  },
42
42
  '.MuiTabs-flexContainer': {
43
43
  borderRadius: '100vw',
44
- backgroundColor: 'grey.200',
44
+ backgroundColor: 'grey.100',
45
45
  padding: 0.5,
46
46
  display: 'inline-flex',
47
47
  columnGap: 0.25,
@@ -60,7 +60,7 @@ function CardTabs({
60
60
  textTransform: 'capitalize',
61
61
  transition: 'background-color 0.2s ease',
62
62
  '&.Mui-selected, &:hover': {
63
- backgroundColor: 'white',
63
+ backgroundColor: 'action.selected',
64
64
  borderColor: 'grey.100',
65
65
  color: 'grey.A700'
66
66
  }
@@ -1,15 +1,18 @@
1
1
  import PropTypes from 'prop-types';
2
2
  import { PaletteMode } from '@mui/material';
3
- import { Theme } from '@mui/material/styles';
3
+ import { Theme, ThemeOptions } from '@mui/material/styles';
4
4
  /** 颜色模式上下文类型 */
5
5
  export interface ColorSchemeContextType {
6
6
  mode: PaletteMode;
7
7
  toggleMode: () => void;
8
+ changeMode: (mode: PaletteMode) => void;
8
9
  prefer?: Prefer;
9
10
  }
10
11
  export declare const ColorSchemeContext: import("react").Context<ColorSchemeContextType>;
11
12
  export declare function useColorScheme(): ColorSchemeContextType;
12
- export type UxTheme = Partial<Theme> | ((outerTheme: Partial<Theme>) => Theme);
13
+ export type UxTheme = ThemeOptions | ((parentTheme: Theme, context?: {
14
+ mode: PaletteMode;
15
+ }) => Theme);
13
16
  export type Prefer = 'light' | 'dark' | 'system';
14
17
  interface BaseThemeProviderProps {
15
18
  children?: React.ReactNode;
@@ -7,7 +7,7 @@ import StyledEngineProvider from '@mui/material/StyledEngineProvider';
7
7
  import CssBaseline from '@mui/material/CssBaseline';
8
8
  import set from 'lodash/set';
9
9
  import { BLOCKLET_THEME_PREFER_KEY } from '@blocklet/theme';
10
- import { createTheme, getDefaultThemePrefer, isTheme, lazyThemeConfig } from './theme';
10
+ import { createTheme, getDefaultThemePrefer, isTheme, isUxTheme, lazyCreateDefaultTheme } from './theme';
11
11
  const defaultTheme = createTheme();
12
12
 
13
13
  /** 颜色模式上下文类型 */
@@ -83,7 +83,7 @@ function DarkSchemeStyles({
83
83
  }
84
84
  return null;
85
85
  }
86
- /** 基础的 theme provider, 可以为 webapp/blocklet 快捷的配置好 mui theme provider */
86
+ /** 快速配置 MUI 主题 */
87
87
  function BaseThemeProvider({
88
88
  children,
89
89
  theme = defaultTheme,
@@ -110,7 +110,7 @@ function BaseThemeProvider({
110
110
  })
111
111
  );
112
112
  }
113
- /** 带颜色模式切换功能的 theme provider */
113
+ /** 配置带颜色模式切换功能的 MUI 主题 */
114
114
  function ColorSchemeProvider({
115
115
  children,
116
116
  theme: themeInput,
@@ -119,14 +119,26 @@ function ColorSchemeProvider({
119
119
  ...rest
120
120
  }) {
121
121
  const [mode, setMode] = useState(() => resolveMode(prefer));
122
+ const parentTheme = useTheme();
122
123
  const _themeInput = useMemo(() => {
123
124
  let result = {};
124
- const getThemeConfig = lazyThemeConfig(mode);
125
+ const createBaseTheme = lazyCreateDefaultTheme(mode);
125
126
  if (themeInput) {
126
127
  if (typeof themeInput === 'function') {
127
- result = {
128
- ...themeInput(getThemeConfig())
129
- };
128
+ const baseTheme = createBaseTheme();
129
+ if (isUxTheme(parentTheme)) {
130
+ result = {
131
+ ...themeInput(parentTheme, {
132
+ mode
133
+ })
134
+ };
135
+ } else {
136
+ result = {
137
+ ...themeInput(baseTheme, {
138
+ mode
139
+ })
140
+ };
141
+ }
130
142
  } else {
131
143
  result = {
132
144
  ...themeInput
@@ -136,7 +148,7 @@ function ColorSchemeProvider({
136
148
  set(result, 'palette.mode', mode);
137
149
  set(result, 'mode', mode);
138
150
  return result;
139
- }, [mode, themeInput]);
151
+ }, [mode, themeInput, parentTheme]);
140
152
  const theme = useMemo(() => {
141
153
  return createTheme({
142
154
  ..._themeInput,
@@ -150,11 +162,18 @@ function ColorSchemeProvider({
150
162
  setMode(newMode);
151
163
  localStorage.setItem(BLOCKLET_THEME_PREFER_KEY, newMode);
152
164
  }, [mode, setMode]);
165
+ const changeMode = useCallback(newMode => {
166
+ if (mode !== newMode) {
167
+ setMode(newMode);
168
+ localStorage.setItem(BLOCKLET_THEME_PREFER_KEY, newMode);
169
+ }
170
+ }, [mode, setMode]);
153
171
  const colorSchemeValue = useMemo(() => ({
154
172
  mode,
155
173
  toggleMode,
174
+ changeMode,
156
175
  prefer
157
- }), [mode, prefer, toggleMode]);
176
+ }), [mode, prefer, toggleMode, changeMode]);
158
177
  useEffect(() => {
159
178
  if (prefer) {
160
179
  setMode(resolveMode(prefer));
@@ -9,6 +9,8 @@ import '@fontsource/roboto/latin-ext-500.css';
9
9
  import '@fontsource/roboto/latin-ext-700.css';
10
10
  /** 是否是 MUI Theme 对象 */
11
11
  export declare function isTheme(obj: any): obj is Theme;
12
+ /** 是否是 UX Theme 对象 */
13
+ export declare function isUxTheme(obj: any): obj is Theme;
12
14
  export declare function collectFontFamilies(obj?: {
13
15
  fontFamily?: string;
14
16
  }, fontSet?: Set<string>): Set<string>;
@@ -19,10 +21,10 @@ export declare function getDefaultThemePrefer(meta?: {
19
21
  };
20
22
  }): PaletteMode;
21
23
  export declare function createDefaultThemeOptions(mode?: PaletteMode): ThemeOptions;
22
- export interface UserThemeOptions extends ThemeOptions {
24
+ export interface UxThemeOptions extends ThemeOptions {
23
25
  disableBlockletTheme?: boolean;
24
26
  }
25
- export declare function lazyThemeConfig(mode: PaletteMode): () => Partial<Theme>;
26
- export declare const create: (...args: Array<UserThemeOptions | ((config: Partial<Theme>) => UserThemeOptions)>) => Theme;
27
- export declare const createTheme: (...args: Array<UserThemeOptions | ((config: Partial<Theme>) => UserThemeOptions)>) => Theme;
27
+ export declare function lazyCreateDefaultTheme(mode: PaletteMode): () => Theme;
28
+ export declare const create: (...args: Array<UxThemeOptions | ((baseTheme: Theme) => UxThemeOptions)>) => Theme;
29
+ export declare const createTheme: (...args: Array<UxThemeOptions | ((baseTheme: Theme) => UxThemeOptions)>) => Theme;
28
30
  export { deepmerge };
@@ -20,6 +20,11 @@ export function isTheme(obj) {
20
20
  return obj && typeof obj === 'object' && obj.palette && typeof obj.palette.getContrastText === 'function';
21
21
  }
22
22
 
23
+ /** 是否是 UX Theme 对象 */
24
+ export function isUxTheme(obj) {
25
+ return isTheme(obj) && obj.__isUxTheme__ === true;
26
+ }
27
+
23
28
  // 收集字体配置
24
29
  export function collectFontFamilies(obj, fontSet = new Set()) {
25
30
  if (!obj || typeof obj !== 'object') return fontSet;
@@ -94,13 +99,13 @@ export function createDefaultThemeOptions(mode = 'light') {
94
99
  }
95
100
  return BLOCKLET_THEME_LIGHT;
96
101
  }
97
- // 用于获取 Blocklet Theme 配置,便于用户创建自定义主题
98
- export function lazyThemeConfig(mode) {
99
- let config = null;
102
+ export function lazyCreateDefaultTheme(mode) {
103
+ let theme = null;
100
104
  return () => {
101
- if (config) return config;
102
- config = deepmerge(createDefaultThemeOptions(mode), window.blocklet?.theme?.[mode] ?? {});
103
- return config;
105
+ if (theme) return theme;
106
+ const options = deepmerge(createDefaultThemeOptions(mode), window.blocklet?.theme?.[mode] ?? {});
107
+ theme = _createTheme(options);
108
+ return theme;
104
109
  };
105
110
  }
106
111
 
@@ -121,7 +126,7 @@ const normalizeUserThemeOptions = ({
121
126
  };
122
127
  return result;
123
128
  };
124
- const defaultUserThemeOptions = {
129
+ const defaultUxThemeOptions = {
125
130
  themeName: 'ArcBlock',
126
131
  pageWidth: 'md',
127
132
  disableBlockletTheme: false,
@@ -151,8 +156,8 @@ const defaultUserThemeOptions = {
151
156
  // https://material-ui.com/customization/default-theme/
152
157
  export const create = (...args) => {
153
158
  const defaultPrefer = getDefaultThemePrefer();
154
- const getThemeConfig = lazyThemeConfig(defaultPrefer);
155
- const userThemeOptions = args.reduce((acc, curr) => deepmerge(acc, normalizeUserThemeOptions(typeof curr === 'function' ? curr(getThemeConfig()) : curr)), normalizeUserThemeOptions(defaultUserThemeOptions));
159
+ const createBaseTheme = lazyCreateDefaultTheme(defaultPrefer);
160
+ const userThemeOptions = args.reduce((acc, curr) => deepmerge(acc, normalizeUserThemeOptions(typeof curr === 'function' ? curr(createBaseTheme()) : curr)), normalizeUserThemeOptions(defaultUxThemeOptions));
156
161
  const prefer = userThemeOptions.mode || userThemeOptions.palette?.mode || defaultPrefer;
157
162
  const blockletThemeOptions = window.blocklet?.theme?.[prefer] ?? {};
158
163
  const defaultThemeOptions = createDefaultThemeOptions(prefer);
@@ -172,6 +177,7 @@ export const create = (...args) => {
172
177
 
173
178
  // 创建主题
174
179
  const theme = _createTheme(mergedThemeOptions);
180
+ theme.__isUxTheme__ = true;
175
181
 
176
182
  // 异步加载字体
177
183
  const fonts = collectFontFamilies(theme.typography);
@@ -48,9 +48,10 @@ export declare function formatToDate(date: string | number | Date, { locale, tz
48
48
  * Ensure that the setDateTool() function is called first to set the time tool library.
49
49
  * @returns formatted date string
50
50
  */
51
- export declare function formatToDatetime(date: string | number | Date, { locale, tz }?: {
51
+ export declare function formatToDatetime(date: string | number | Date, { locale, tz, isUtc }?: {
52
52
  locale?: Locale;
53
53
  tz?: string;
54
+ isUtc?: boolean;
54
55
  }): any;
55
56
  export declare function detectWalletExtension(): any;
56
57
  export declare function openWebWallet({ webWalletUrl, action, locale, url, windowFeatures, appInfo, memberAppInfo, }: {
package/lib/Util/index.js CHANGED
@@ -189,7 +189,8 @@ export function getDateTool() {
189
189
  }
190
190
  const createDateFormatter = format => (date, {
191
191
  locale,
192
- tz
192
+ tz,
193
+ isUtc
193
194
  } = {}) => {
194
195
  if (dateTool === null) {
195
196
  throw new Error('call setDateTool to set the date tool library, such as: setDateTool(dayjs)');
@@ -201,6 +202,9 @@ const createDateFormatter = format => (date, {
201
202
  if (tz) {
202
203
  instance = instance.tz(tz);
203
204
  }
205
+ if (isUtc) {
206
+ instance = instance.utc();
207
+ }
204
208
  if (typeof locale !== 'undefined') {
205
209
  instance = instance.locale(locale);
206
210
  }
@@ -229,11 +233,13 @@ export function formatToDate(date, {
229
233
  */
230
234
  export function formatToDatetime(date, {
231
235
  locale = 'en',
232
- tz
236
+ tz,
237
+ isUtc = false
233
238
  } = {}) {
234
239
  return createDateFormatter('lll')(date, {
235
240
  locale,
236
- tz
241
+ tz,
242
+ isUtc
237
243
  });
238
244
  }
239
245
  export function detectWalletExtension() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arcblock/ux",
3
- "version": "2.13.26",
3
+ "version": "2.13.28",
4
4
  "description": "Common used react components for arcblock products",
5
5
  "keywords": [
6
6
  "react",
@@ -71,14 +71,14 @@
71
71
  "react": ">=18.2.0",
72
72
  "react-router-dom": ">=6.22.3"
73
73
  },
74
- "gitHead": "bf72ea918c9caeddeac9bcd6cb4352a415e7a29c",
74
+ "gitHead": "345a8363b59137fc5691b68e6295b87c9efb028a",
75
75
  "dependencies": {
76
76
  "@arcblock/did-motif": "^1.1.13",
77
- "@arcblock/icons": "^2.13.26",
78
- "@arcblock/nft-display": "^2.13.26",
79
- "@arcblock/react-hooks": "^2.13.26",
77
+ "@arcblock/icons": "^2.13.28",
78
+ "@arcblock/nft-display": "^2.13.28",
79
+ "@arcblock/react-hooks": "^2.13.28",
80
80
  "@babel/plugin-syntax-dynamic-import": "^7.8.3",
81
- "@blocklet/theme": "^2.13.26",
81
+ "@blocklet/theme": "^2.13.28",
82
82
  "@fontsource/roboto": "~5.1.1",
83
83
  "@fontsource/ubuntu-mono": "^5.0.18",
84
84
  "@iconify-icons/logos": "^1.2.36",
@@ -1,7 +1,8 @@
1
+ import { Box, Tooltip } from '@mui/material';
2
+ import { useState } from 'react';
1
3
  import relativeTime from 'dayjs/plugin/relativeTime';
2
4
  import dayjs from 'dayjs';
3
5
  import 'dayjs/locale/zh-cn';
4
- import { Tooltip } from '@mui/material';
5
6
  import localizedFormat from 'dayjs/plugin/localizedFormat';
6
7
  import utc from 'dayjs/plugin/utc';
7
8
  import timezone from 'dayjs/plugin/timezone';
@@ -35,19 +36,29 @@ dayjs.updateLocale('zh-cn', {
35
36
  // FIXME: @@ 此处不能真正的将 relativeTime 设置为支持中文
36
37
  setDateTool(dayjs);
37
38
 
39
+ const translations: Record<Locale, { utc: string }> = {
40
+ en: {
41
+ utc: 'Your Timezone',
42
+ },
43
+ zh: {
44
+ utc: '你所在时区',
45
+ },
46
+ };
47
+
38
48
  export interface RelativeTimeProps {
39
49
  value: string | number;
40
50
  locale?: Locale;
41
51
  withoutSuffix?: false | true;
42
52
  from?: string | number;
43
53
  to?: string | number;
44
- type?: 'relative' | 'absolute';
54
+ type?: 'relative' | 'absolute' | 'utc' | 'all';
45
55
  tz?: string;
46
56
  relativeRange?: number;
47
57
  enableTooltip?: boolean;
58
+ showUTCPrefix?: boolean;
48
59
  }
49
60
 
50
- export default function RelativeTime({
61
+ function useRelativeTime({
51
62
  value,
52
63
  locale = 'en',
53
64
  withoutSuffix = false,
@@ -56,18 +67,36 @@ export default function RelativeTime({
56
67
  type = 'relative',
57
68
  tz,
58
69
  relativeRange,
59
- enableTooltip = true,
60
- ...rest
61
- }: RelativeTimeProps) {
70
+ }: {
71
+ value: string | number;
72
+ locale?: Locale;
73
+ withoutSuffix?: boolean;
74
+ from?: string | number;
75
+ to?: string | number;
76
+ type?: 'relative' | 'absolute' | 'utc';
77
+ tz?: string;
78
+ relativeRange?: number;
79
+ }) {
80
+ const date = new Date();
81
+ const timeZoneOffset = date.getTimezoneOffset();
82
+ const sign = timeZoneOffset > 0 ? '-' : '+';
83
+ const hoursOffset = Math.abs(timeZoneOffset) / 60;
84
+ const isLocalUtc = timeZoneOffset === 0;
85
+ const [isUtc, setIsUtc] = useState(isLocalUtc);
86
+
62
87
  if (!value) {
63
- return '-';
88
+ return { innerContent: '-', popContent: '-', isUtc, setIsUtc, sign, hoursOffset };
64
89
  }
65
90
 
66
91
  const localeOption = locale === 'zh' ? 'zh-cn' : 'en-us';
67
- const datetime = dayjs(value).locale(localeOption);
92
+ let datetime = dayjs(value).locale(localeOption);
93
+
94
+ if (type === 'utc') {
95
+ datetime = dayjs(value).utc().locale(localeOption);
96
+ }
68
97
 
69
98
  if (tz) {
70
- datetime.tz(tz);
99
+ datetime = datetime.tz(tz);
71
100
  }
72
101
 
73
102
  const absoluteString = formatToDatetime(value, { locale: localeOption, tz });
@@ -97,9 +126,130 @@ export default function RelativeTime({
97
126
  popContent = relativeString;
98
127
  }
99
128
 
129
+ if (type === 'utc') {
130
+ if (isUtc) {
131
+ innerContent = formatToDatetime(value, { locale: localeOption, tz, isUtc: true });
132
+ popContent = formatToDatetime(value, { locale: localeOption, tz, isUtc: true });
133
+ } else {
134
+ innerContent = absoluteString;
135
+ popContent = relativeString;
136
+ }
137
+ }
138
+
139
+ return { innerContent, popContent, isUtc, setIsUtc, sign, hoursOffset, relativeString, absoluteString };
140
+ }
141
+
142
+ function UTCChip({
143
+ locale,
144
+ isUtc,
145
+ sign,
146
+ hoursOffset,
147
+ setIsUtc,
148
+ showUTCPrefix = true,
149
+ }: {
150
+ locale: Locale;
151
+ isUtc?: boolean;
152
+ sign: string;
153
+ hoursOffset: number;
154
+ setIsUtc: (data: any) => void;
155
+ showUTCPrefix?: boolean;
156
+ }) {
157
+ const prefix = showUTCPrefix ? `${translations[locale].utc}: ` : '';
158
+
159
+ return (
160
+ <Box
161
+ component="span"
162
+ sx={{
163
+ color: 'inherit',
164
+ cursor: 'pointer',
165
+ border: '1px solid',
166
+ fontSize: '0.8rem',
167
+ borderColor: 'divider',
168
+ borderRadius: '20px',
169
+ padding: '4px 8px',
170
+ lineHeight: 1,
171
+ }}
172
+ onClick={() => setIsUtc((r: any) => !r)}>
173
+ {`${prefix}${isUtc ? 'UTC' : `UTC${sign}${hoursOffset}`}`}
174
+ </Box>
175
+ );
176
+ }
177
+
178
+ export default function RelativeTime({
179
+ value,
180
+ locale = 'en',
181
+ withoutSuffix = false,
182
+ from = '',
183
+ to = '',
184
+ type = 'relative',
185
+ tz,
186
+ relativeRange,
187
+ enableTooltip = true,
188
+ showUTCPrefix = true,
189
+ ...rest
190
+ }: RelativeTimeProps) {
191
+ const { innerContent, popContent, isUtc, setIsUtc, sign, hoursOffset, relativeString } = useRelativeTime({
192
+ value,
193
+ locale,
194
+ withoutSuffix,
195
+ from,
196
+ to,
197
+ type: type === 'all' ? 'utc' : type,
198
+ tz,
199
+ relativeRange,
200
+ });
201
+
202
+ if (type === 'all') {
203
+ return (
204
+ <Tooltip title={undefined} placement="top-end" enterTouchDelay={0}>
205
+ <Box display="flex" alignItems="center" gap={0.5} width="max-content" {...rest}>
206
+ <Box component="span" {...rest} sx={{}}>
207
+ {innerContent}
208
+ </Box>
209
+
210
+ <Box component="span" sx={{ color: 'inherit' }}>
211
+ ·
212
+ </Box>
213
+
214
+ <Box component="span" sx={{ color: 'text.secondary' }}>
215
+ {relativeString}
216
+ </Box>
217
+
218
+ <Box component="span" sx={{ color: 'inherit' }}>
219
+ ·
220
+ </Box>
221
+
222
+ <UTCChip
223
+ locale={locale}
224
+ isUtc={isUtc}
225
+ sign={sign}
226
+ hoursOffset={hoursOffset}
227
+ setIsUtc={setIsUtc}
228
+ showUTCPrefix={showUTCPrefix}
229
+ />
230
+ </Box>
231
+ </Tooltip>
232
+ );
233
+ }
234
+
100
235
  return (
101
236
  <Tooltip title={enableTooltip ? popContent : undefined} placement="top-end" enterTouchDelay={0}>
102
- <span {...rest}>{innerContent}</span>
237
+ <Box display="flex" alignItems="center" gap={1} width="max-content">
238
+ <Box component="span" {...rest}>
239
+ {innerContent}
240
+ </Box>
241
+
242
+ {type === 'utc' && (
243
+ <UTCChip
244
+ locale={locale}
245
+ isUtc={isUtc}
246
+ sign={sign}
247
+ hoursOffset={hoursOffset}
248
+ setIsUtc={setIsUtc}
249
+ showUTCPrefix={showUTCPrefix}
250
+ />
251
+ )}
252
+ </Box>
103
253
  </Tooltip>
104
254
  );
105
255
  }
@@ -46,7 +46,7 @@ function CardTabs({ tabs, current, onChange, ...rest }: CardTabsProps) {
46
46
  },
47
47
  '.MuiTabs-flexContainer': {
48
48
  borderRadius: '100vw',
49
- backgroundColor: 'grey.200',
49
+ backgroundColor: 'grey.100',
50
50
  padding: 0.5,
51
51
  display: 'inline-flex',
52
52
  columnGap: 0.25,
@@ -65,7 +65,7 @@ function CardTabs({ tabs, current, onChange, ...rest }: CardTabsProps) {
65
65
  textTransform: 'capitalize',
66
66
  transition: 'background-color 0.2s ease',
67
67
  '&.Mui-selected, &:hover': {
68
- backgroundColor: 'white',
68
+ backgroundColor: 'action.selected',
69
69
  borderColor: 'grey.100',
70
70
  color: 'grey.A700',
71
71
  },
@@ -1,13 +1,20 @@
1
1
  import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
2
2
  import PropTypes from 'prop-types';
3
3
  import { GlobalStyles, PaletteMode } from '@mui/material';
4
- import { ThemeProvider as MuiThemeProvider, Theme, useTheme } from '@mui/material/styles';
4
+ import { ThemeProvider as MuiThemeProvider, Theme, ThemeOptions, useTheme } from '@mui/material/styles';
5
5
  import StyledEngineProvider from '@mui/material/StyledEngineProvider';
6
6
  import CssBaseline from '@mui/material/CssBaseline';
7
7
  import set from 'lodash/set';
8
8
  import { BLOCKLET_THEME_PREFER_KEY } from '@blocklet/theme';
9
9
 
10
- import { createTheme, getDefaultThemePrefer, isTheme, lazyThemeConfig, type UserThemeOptions } from './theme';
10
+ import {
11
+ createTheme,
12
+ getDefaultThemePrefer,
13
+ isTheme,
14
+ isUxTheme,
15
+ lazyCreateDefaultTheme,
16
+ type UxThemeOptions,
17
+ } from './theme';
11
18
 
12
19
  const defaultTheme = createTheme();
13
20
 
@@ -15,6 +22,7 @@ const defaultTheme = createTheme();
15
22
  export interface ColorSchemeContextType {
16
23
  mode: PaletteMode;
17
24
  toggleMode: () => void;
25
+ changeMode: (mode: PaletteMode) => void;
18
26
  prefer?: Prefer;
19
27
  }
20
28
 
@@ -102,7 +110,7 @@ function DarkSchemeStyles({ className }: { className?: string }) {
102
110
  return null;
103
111
  }
104
112
 
105
- export type UxTheme = Partial<Theme> | ((outerTheme: Partial<Theme>) => Theme);
113
+ export type UxTheme = ThemeOptions | ((parentTheme: Theme, context?: { mode: PaletteMode }) => Theme);
106
114
  export type Prefer = 'light' | 'dark' | 'system';
107
115
 
108
116
  interface BaseThemeProviderProps {
@@ -113,7 +121,7 @@ interface BaseThemeProviderProps {
113
121
  darkSchemeClass?: string;
114
122
  }
115
123
 
116
- /** 基础的 theme provider, 可以为 webapp/blocklet 快捷的配置好 mui theme provider */
124
+ /** 快速配置 MUI 主题 */
117
125
  function BaseThemeProvider({
118
126
  children,
119
127
  theme = defaultTheme,
@@ -144,7 +152,7 @@ interface ColorSchemeProviderProps extends BaseThemeProviderProps {
144
152
  disableBlockletTheme?: boolean;
145
153
  }
146
154
 
147
- /** 带颜色模式切换功能的 theme provider */
155
+ /** 配置带颜色模式切换功能的 MUI 主题 */
148
156
  function ColorSchemeProvider({
149
157
  children,
150
158
  theme: themeInput,
@@ -153,14 +161,21 @@ function ColorSchemeProvider({
153
161
  ...rest
154
162
  }: ThemeProviderProps) {
155
163
  const [mode, setMode] = useState<PaletteMode>(() => resolveMode(prefer));
164
+ const parentTheme = useTheme();
156
165
 
157
166
  const _themeInput = useMemo(() => {
158
- let result: UserThemeOptions = {};
159
- const getThemeConfig = lazyThemeConfig(mode);
167
+ let result: UxThemeOptions = {};
168
+ const createBaseTheme = lazyCreateDefaultTheme(mode);
160
169
 
161
170
  if (themeInput) {
162
171
  if (typeof themeInput === 'function') {
163
- result = { ...themeInput(getThemeConfig()) };
172
+ const baseTheme = createBaseTheme();
173
+
174
+ if (isUxTheme(parentTheme)) {
175
+ result = { ...themeInput(parentTheme, { mode }) };
176
+ } else {
177
+ result = { ...themeInput(baseTheme, { mode }) };
178
+ }
164
179
  } else {
165
180
  result = { ...themeInput };
166
181
  }
@@ -170,7 +185,7 @@ function ColorSchemeProvider({
170
185
  set(result, 'mode', mode);
171
186
 
172
187
  return result;
173
- }, [mode, themeInput]);
188
+ }, [mode, themeInput, parentTheme]);
174
189
 
175
190
  const theme = useMemo(() => {
176
191
  return createTheme({ ..._themeInput, disableBlockletTheme });
@@ -183,13 +198,24 @@ function ColorSchemeProvider({
183
198
  localStorage.setItem(BLOCKLET_THEME_PREFER_KEY, newMode);
184
199
  }, [mode, setMode]);
185
200
 
201
+ const changeMode = useCallback(
202
+ (newMode: PaletteMode) => {
203
+ if (mode !== newMode) {
204
+ setMode(newMode);
205
+ localStorage.setItem(BLOCKLET_THEME_PREFER_KEY, newMode);
206
+ }
207
+ },
208
+ [mode, setMode]
209
+ );
210
+
186
211
  const colorSchemeValue = useMemo(
187
212
  () => ({
188
213
  mode,
189
214
  toggleMode,
215
+ changeMode,
190
216
  prefer,
191
217
  }),
192
- [mode, prefer, toggleMode]
218
+ [mode, prefer, toggleMode, changeMode]
193
219
  );
194
220
 
195
221
  useEffect(() => {
@@ -20,6 +20,11 @@ export function isTheme(obj: any): obj is Theme {
20
20
  return obj && typeof obj === 'object' && obj.palette && typeof obj.palette.getContrastText === 'function';
21
21
  }
22
22
 
23
+ /** 是否是 UX Theme 对象 */
24
+ export function isUxTheme(obj: any): obj is Theme {
25
+ return isTheme(obj) && obj.__isUxTheme__ === true;
26
+ }
27
+
23
28
  // 收集字体配置
24
29
  export function collectFontFamilies(obj?: { fontFamily?: string }, fontSet: Set<string> = new Set()): Set<string> {
25
30
  if (!obj || typeof obj !== 'object') return fontSet;
@@ -107,26 +112,26 @@ export function createDefaultThemeOptions(mode: PaletteMode = 'light') {
107
112
  return BLOCKLET_THEME_LIGHT;
108
113
  }
109
114
 
110
- export interface UserThemeOptions extends ThemeOptions {
115
+ export interface UxThemeOptions extends ThemeOptions {
111
116
  disableBlockletTheme?: boolean;
112
117
  }
113
118
 
114
- // 用于获取 Blocklet Theme 配置,便于用户创建自定义主题
115
- export function lazyThemeConfig(mode: PaletteMode) {
116
- let config: Partial<Theme> | null = null;
119
+ export function lazyCreateDefaultTheme(mode: PaletteMode) {
120
+ let theme: Theme | null = null;
117
121
 
118
122
  return () => {
119
- if (config) return config;
123
+ if (theme) return theme;
120
124
 
121
- config = deepmerge(createDefaultThemeOptions(mode), window.blocklet?.theme?.[mode] ?? {}) as Partial<Theme>;
125
+ const options = deepmerge(createDefaultThemeOptions(mode), window.blocklet?.theme?.[mode] ?? {});
122
126
 
123
- return config;
127
+ theme = _createTheme(options);
128
+ return theme;
124
129
  };
125
130
  }
126
131
 
127
132
  // 主要处理 overrides
128
- const normalizeUserThemeOptions = ({ palette, components, overrides, ...rest }: UserThemeOptions) => {
129
- const result: UserThemeOptions = {
133
+ const normalizeUserThemeOptions = ({ palette, components, overrides, ...rest }: UxThemeOptions) => {
134
+ const result: UxThemeOptions = {
130
135
  palette,
131
136
  components: {
132
137
  ...overrides,
@@ -138,7 +143,7 @@ const normalizeUserThemeOptions = ({ palette, components, overrides, ...rest }:
138
143
  return result;
139
144
  };
140
145
 
141
- const defaultUserThemeOptions: UserThemeOptions = {
146
+ const defaultUxThemeOptions: UxThemeOptions = {
142
147
  themeName: 'ArcBlock',
143
148
  pageWidth: 'md',
144
149
  disableBlockletTheme: false,
@@ -166,13 +171,13 @@ const defaultUserThemeOptions: UserThemeOptions = {
166
171
  };
167
172
 
168
173
  // https://material-ui.com/customization/default-theme/
169
- export const create = (...args: Array<UserThemeOptions | ((config: Partial<Theme>) => UserThemeOptions)>) => {
174
+ export const create = (...args: Array<UxThemeOptions | ((baseTheme: Theme) => UxThemeOptions)>) => {
170
175
  const defaultPrefer = getDefaultThemePrefer();
171
- const getThemeConfig = lazyThemeConfig(defaultPrefer);
172
- const userThemeOptions = args.reduce<UserThemeOptions>(
176
+ const createBaseTheme = lazyCreateDefaultTheme(defaultPrefer);
177
+ const userThemeOptions = args.reduce<UxThemeOptions>(
173
178
  (acc, curr) =>
174
- deepmerge(acc, normalizeUserThemeOptions(typeof curr === 'function' ? curr(getThemeConfig()) : curr)),
175
- normalizeUserThemeOptions(defaultUserThemeOptions)
179
+ deepmerge(acc, normalizeUserThemeOptions(typeof curr === 'function' ? curr(createBaseTheme()) : curr)),
180
+ normalizeUserThemeOptions(defaultUxThemeOptions)
176
181
  );
177
182
  const prefer = userThemeOptions.mode || userThemeOptions.palette?.mode || defaultPrefer;
178
183
  const blockletThemeOptions = window.blocklet?.theme?.[prefer] ?? {};
@@ -195,6 +200,7 @@ export const create = (...args: Array<UserThemeOptions | ((config: Partial<Theme
195
200
 
196
201
  // 创建主题
197
202
  const theme = _createTheme(mergedThemeOptions);
203
+ theme.__isUxTheme__ = true;
198
204
 
199
205
  // 异步加载字体
200
206
  const fonts = collectFontFamilies(theme.typography);
package/src/Util/index.ts CHANGED
@@ -246,7 +246,7 @@ export function getDateTool() {
246
246
 
247
247
  const createDateFormatter =
248
248
  (format: string) =>
249
- (date: string | number | Date, { locale, tz }: { locale?: Locale; tz?: string } = {}) => {
249
+ (date: string | number | Date, { locale, tz, isUtc }: { locale?: Locale; tz?: string; isUtc?: boolean } = {}) => {
250
250
  if (dateTool === null) {
251
251
  throw new Error('call setDateTool to set the date tool library, such as: setDateTool(dayjs)');
252
252
  }
@@ -261,6 +261,10 @@ const createDateFormatter =
261
261
  instance = instance.tz(tz);
262
262
  }
263
263
 
264
+ if (isUtc) {
265
+ instance = instance.utc();
266
+ }
267
+
264
268
  if (typeof locale !== 'undefined') {
265
269
  instance = instance.locale(locale);
266
270
  }
@@ -287,9 +291,9 @@ export function formatToDate(
287
291
  */
288
292
  export function formatToDatetime(
289
293
  date: string | number | Date,
290
- { locale = 'en', tz }: { locale?: Locale; tz?: string } = {}
294
+ { locale = 'en', tz, isUtc = false }: { locale?: Locale; tz?: string; isUtc?: boolean } = {}
291
295
  ) {
292
- return createDateFormatter('lll')(date, { locale, tz });
296
+ return createDateFormatter('lll')(date, { locale, tz, isUtc });
293
297
  }
294
298
 
295
299
  export function detectWalletExtension() {