@particle-network/ui-react 0.4.0-beta.0 → 0.4.0-beta.2

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.
@@ -1,3 +1,5 @@
1
1
  export * from './theme-data';
2
2
  export * from './theme-switch';
3
+ export * from './use-color-scheme';
3
4
  export * from './use-theme';
5
+ export * from './use-theme-color';
@@ -1,3 +1,5 @@
1
1
  export * from "./theme-data.js";
2
2
  export * from "./theme-switch.js";
3
+ export * from "./use-color-scheme.js";
3
4
  export * from "./use-theme.js";
5
+ export * from "./use-theme-color.js";
@@ -1,9 +1,10 @@
1
- export type ThemeKey = 'ux-purple-gold-dark' | 'ux-purple-gold-light' | 'ux-green-red-dark' | 'ux-green-red-light' | 'ux-green-red-soft-dark' | 'ux-green-red-soft-light' | 'gmgn-dark' | 'axiom-dark' | 'hyperliquid-dark' | 'phantom-dark' | 'binance-dark' | 'bullx-dark';
1
+ export type ThemeId = 'ux-purple-gold-dark' | 'ux-purple-gold-light' | 'ux-green-red-dark' | 'ux-green-red-light' | 'ux-green-red-soft-dark' | 'ux-green-red-soft-light' | 'gmgn-dark' | 'axiom-dark' | 'hyperliquid-dark' | 'phantom-dark' | 'binance-dark' | 'bullx-dark';
2
+ export type ColorScheme = 'dark' | 'light';
2
3
  export interface ThemeItemType {
3
- key: ThemeKey;
4
+ id: ThemeId;
4
5
  zhName: string;
5
6
  enName: string;
6
- colorScheme: 'dark' | 'light';
7
+ colorScheme: ColorScheme;
7
8
  colorVariables: {
8
9
  foreground: string;
9
10
  secondary: string;
@@ -25,4 +26,4 @@ export interface ThemeItemType {
25
26
  };
26
27
  }
27
28
  export declare const themeData: ThemeItemType[];
28
- export declare const themeKeys: ThemeKey[];
29
+ export declare const themeKeys: ThemeId[];
@@ -1,6 +1,6 @@
1
1
  const themeData = [
2
2
  {
3
- key: 'ux-purple-gold-dark',
3
+ id: 'ux-purple-gold-dark',
4
4
  zhName: 'UX 紫金 Dark',
5
5
  enName: 'UX Purple/Gold Dark',
6
6
  colorScheme: 'dark',
@@ -25,7 +25,7 @@ const themeData = [
25
25
  }
26
26
  },
27
27
  {
28
- key: 'ux-purple-gold-light',
28
+ id: 'ux-purple-gold-light',
29
29
  zhName: 'UX 紫金 Light',
30
30
  enName: 'UX Purple/Gold Light',
31
31
  colorScheme: 'light',
@@ -50,7 +50,7 @@ const themeData = [
50
50
  }
51
51
  },
52
52
  {
53
- key: 'ux-green-red-dark',
53
+ id: 'ux-green-red-dark',
54
54
  zhName: 'UX 绿红 Dark',
55
55
  enName: 'UX Green/Red Dark',
56
56
  colorScheme: 'dark',
@@ -75,7 +75,7 @@ const themeData = [
75
75
  }
76
76
  },
77
77
  {
78
- key: 'ux-green-red-light',
78
+ id: 'ux-green-red-light',
79
79
  zhName: 'UX 绿红 Light',
80
80
  enName: 'UX Green/Red Light',
81
81
  colorScheme: 'light',
@@ -100,7 +100,7 @@ const themeData = [
100
100
  }
101
101
  },
102
102
  {
103
- key: 'ux-green-red-soft-dark',
103
+ id: 'ux-green-red-soft-dark',
104
104
  zhName: 'UX 柔和 Dark',
105
105
  enName: 'UX Soft Dark',
106
106
  colorScheme: 'dark',
@@ -125,7 +125,7 @@ const themeData = [
125
125
  }
126
126
  },
127
127
  {
128
- key: 'ux-green-red-soft-light',
128
+ id: 'ux-green-red-soft-light',
129
129
  zhName: 'UX 柔和 Light',
130
130
  enName: 'UX Soft Light',
131
131
  colorScheme: 'light',
@@ -150,7 +150,7 @@ const themeData = [
150
150
  }
151
151
  },
152
152
  {
153
- key: 'gmgn-dark',
153
+ id: 'gmgn-dark',
154
154
  zhName: 'GMGN',
155
155
  enName: 'GMGN',
156
156
  colorScheme: 'dark',
@@ -175,7 +175,7 @@ const themeData = [
175
175
  }
176
176
  },
177
177
  {
178
- key: 'axiom-dark',
178
+ id: 'axiom-dark',
179
179
  zhName: 'AXIOM',
180
180
  enName: 'AXIOM',
181
181
  colorScheme: 'dark',
@@ -200,7 +200,7 @@ const themeData = [
200
200
  }
201
201
  },
202
202
  {
203
- key: 'hyperliquid-dark',
203
+ id: 'hyperliquid-dark',
204
204
  zhName: 'Hyperliquid',
205
205
  enName: 'Hyperliquid',
206
206
  colorScheme: 'dark',
@@ -225,7 +225,7 @@ const themeData = [
225
225
  }
226
226
  },
227
227
  {
228
- key: 'phantom-dark',
228
+ id: 'phantom-dark',
229
229
  zhName: 'Phantom',
230
230
  enName: 'Phantom',
231
231
  colorScheme: 'dark',
@@ -250,7 +250,7 @@ const themeData = [
250
250
  }
251
251
  },
252
252
  {
253
- key: 'binance-dark',
253
+ id: 'binance-dark',
254
254
  zhName: 'Binance',
255
255
  enName: 'Binance',
256
256
  colorScheme: 'dark',
@@ -275,7 +275,7 @@ const themeData = [
275
275
  }
276
276
  },
277
277
  {
278
- key: 'bullx-dark',
278
+ id: 'bullx-dark',
279
279
  zhName: "Bullx's",
280
280
  enName: "Bullx's",
281
281
  colorScheme: 'dark',
@@ -300,5 +300,5 @@ const themeData = [
300
300
  }
301
301
  }
302
302
  ];
303
- const themeKeys = themeData.map((theme)=>theme.key);
303
+ const themeKeys = themeData.map((theme)=>theme.id);
304
304
  export { themeData, themeKeys };
@@ -1,6 +1,6 @@
1
1
  import React from 'react';
2
2
  import type { ThemeItemType } from './theme-data';
3
- export interface ThemeItemProps extends Pick<ThemeItemType, 'zhName' | 'enName' | 'colorVariables'> {
3
+ export interface ThemeItemProps extends ThemeItemType {
4
4
  isSelected: boolean;
5
5
  onClick: () => void;
6
6
  }
@@ -1,20 +1,18 @@
1
1
  import { jsx, jsxs } from "react/jsx-runtime";
2
2
  import "react";
3
+ import { cn } from "@heroui/theme";
3
4
  import { useLang } from "../../hooks/index.js";
4
5
  import { VStack } from "../layout/index.js";
5
6
  import { Text } from "../typography/Text.js";
6
- const ThemeItem = ({ zhName, enName, colorVariables, isSelected, onClick })=>{
7
+ const ThemeItem = ({ id, zhName, enName, isSelected, onClick })=>{
7
8
  const lang = useLang();
8
9
  return /*#__PURE__*/ jsxs(VStack, {
9
10
  center: true,
10
- className: "cursor-pointer hover:scale-105 transition-all duration-300",
11
+ className: cn('cursor-pointer hover:scale-105 transition-all duration-300', id),
11
12
  onClick: onClick,
12
13
  children: [
13
14
  /*#__PURE__*/ jsx("div", {
14
- className: "rounded-small border-1.5",
15
- style: {
16
- borderColor: isSelected ? colorVariables.primary : 'transparent'
17
- },
15
+ className: cn('rounded-small border-1.5', isSelected ? 'border-primary' : 'border-transparent'),
18
16
  children: /*#__PURE__*/ jsxs("svg", {
19
17
  xmlns: "http://www.w3.org/2000/svg",
20
18
  width: "180",
@@ -29,7 +27,8 @@ const ThemeItem = ({ zhName, enName, colorVariables, isSelected, onClick })=>{
29
27
  width: "116.19",
30
28
  height: "67",
31
29
  rx: "5",
32
- fill: colorVariables.primary
30
+ fill: "currentColor",
31
+ className: "text-primary"
33
32
  }),
34
33
  /*#__PURE__*/ jsx("mask", {
35
34
  id: "mask0_40928_218196",
@@ -47,7 +46,8 @@ const ThemeItem = ({ zhName, enName, colorVariables, isSelected, onClick })=>{
47
46
  width: "116.19",
48
47
  height: "67",
49
48
  rx: "5",
50
- fill: colorVariables.background
49
+ fill: "currentColor",
50
+ className: "text-background"
51
51
  })
52
52
  }),
53
53
  /*#__PURE__*/ jsx("g", {
@@ -58,7 +58,8 @@ const ThemeItem = ({ zhName, enName, colorVariables, isSelected, onClick })=>{
58
58
  width: "116.19",
59
59
  height: "67",
60
60
  rx: "6",
61
- fill: colorVariables.background
61
+ fill: "currentColor",
62
+ className: "text-background"
62
63
  })
63
64
  }),
64
65
  /*#__PURE__*/ jsx("rect", {
@@ -66,7 +67,8 @@ const ThemeItem = ({ zhName, enName, colorVariables, isSelected, onClick })=>{
66
67
  y: "31",
67
68
  width: "100",
68
69
  height: "1",
69
- fill: colorVariables.divider
70
+ fill: "currentColor",
71
+ className: "text-divider"
70
72
  }),
71
73
  /*#__PURE__*/ jsx("rect", {
72
74
  x: "54.082",
@@ -74,7 +76,8 @@ const ThemeItem = ({ zhName, enName, colorVariables, isSelected, onClick })=>{
74
76
  width: "30.0917",
75
77
  height: "6.01835",
76
78
  rx: "3.00917",
77
- fill: colorVariables.foreground
79
+ fill: "currentColor",
80
+ className: "text-foreground"
78
81
  }),
79
82
  /*#__PURE__*/ jsx("rect", {
80
83
  x: "54.082",
@@ -82,7 +85,8 @@ const ThemeItem = ({ zhName, enName, colorVariables, isSelected, onClick })=>{
82
85
  width: "19.5596",
83
86
  height: "6.01835",
84
87
  rx: "3.00917",
85
- fill: colorVariables.bullish
88
+ fill: "currentColor",
89
+ className: "text-bullish"
86
90
  }),
87
91
  /*#__PURE__*/ jsx("rect", {
88
92
  x: "75.8994",
@@ -90,7 +94,8 @@ const ThemeItem = ({ zhName, enName, colorVariables, isSelected, onClick })=>{
90
94
  width: "19.5596",
91
95
  height: "6.01835",
92
96
  rx: "3.00917",
93
- fill: colorVariables.bearish
97
+ fill: "currentColor",
98
+ className: "text-bearish"
94
99
  }),
95
100
  /*#__PURE__*/ jsx("rect", {
96
101
  x: "86.4316",
@@ -98,7 +103,8 @@ const ThemeItem = ({ zhName, enName, colorVariables, isSelected, onClick })=>{
98
103
  width: "6.01835",
99
104
  height: "6.01835",
100
105
  rx: "3.00917",
101
- fill: colorVariables.secondary
106
+ fill: "currentColor",
107
+ className: "text-secondary"
102
108
  }),
103
109
  /*#__PURE__*/ jsx("rect", {
104
110
  x: "94.707",
@@ -106,7 +112,8 @@ const ThemeItem = ({ zhName, enName, colorVariables, isSelected, onClick })=>{
106
112
  width: "6.01835",
107
113
  height: "6.01835",
108
114
  rx: "3.00917",
109
- fill: colorVariables.secondary
115
+ fill: "currentColor",
116
+ className: "text-secondary"
110
117
  }),
111
118
  /*#__PURE__*/ jsx("rect", {
112
119
  x: "102.981",
@@ -114,7 +121,8 @@ const ThemeItem = ({ zhName, enName, colorVariables, isSelected, onClick })=>{
114
121
  width: "6.01835",
115
122
  height: "6.01835",
116
123
  rx: "3.00917",
117
- fill: colorVariables.secondary
124
+ fill: "currentColor",
125
+ className: "text-secondary"
118
126
  }),
119
127
  /*#__PURE__*/ jsx("rect", {
120
128
  x: "27",
@@ -122,7 +130,8 @@ const ThemeItem = ({ zhName, enName, colorVariables, isSelected, onClick })=>{
122
130
  width: "22.5688",
123
131
  height: "22.5688",
124
132
  rx: "11.2844",
125
- fill: colorVariables.primary
133
+ fill: "currentColor",
134
+ className: "text-primary"
126
135
  })
127
136
  ]
128
137
  })
@@ -1,4 +1,7 @@
1
1
  import React from 'react';
2
+ import type { DrawerProps } from '@heroui/drawer';
3
+ export type UXThemeSwitchDrawerProps = Omit<DrawerProps, 'children'>;
4
+ export declare const UXThemeSwitchDrawer: React.FC<UXThemeSwitchDrawerProps>;
2
5
  export interface UXThemeSwitchProps {
3
6
  children?: (onOpen: () => void) => React.ReactNode;
4
7
  }
@@ -2,22 +2,22 @@ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
2
2
  import "react";
3
3
  import { Drawer, DrawerBody, DrawerContent, DrawerFooter, DrawerHeader } from "@heroui/drawer";
4
4
  import { useDisclosure } from "@heroui/use-disclosure";
5
- import ThemeSwitchIcon from "@particle-network/icons/web/ThemeSwitchIcon";
5
+ import { ThemeSwitchIcon } from "@particle-network/icons/web";
6
6
  import { useI18n } from "../../hooks/index.js";
7
7
  import { Flex, HStack, VStack } from "../layout/index.js";
8
8
  import { Text } from "../typography/Text.js";
9
9
  import { UXButton } from "../UXButton/index.js";
10
10
  import { UXDivider } from "../UXDivider/index.js";
11
11
  import { UXInput } from "../UXInput/index.js";
12
+ import { UXSpinner } from "../UXSpinner/index.js";
12
13
  import { themeData } from "./theme-data.js";
13
14
  import { ThemeItem } from "./theme-item.js";
14
15
  import { useTheme } from "./use-theme.js";
15
- const UXThemeSwitch = ({ children })=>{
16
+ const UXThemeSwitchDrawer = ({ isOpen, onClose, onOpenChange, ...props })=>{
16
17
  const i18n = useI18n();
17
- const { isOpen, onOpen, onClose, onOpenChange } = useDisclosure();
18
- const { selectedTheme, setSelectedTheme, savedTheme, setSavedTheme, selectedFontLink, setSelectedFontLink, savedFontLink, setSavedFontLink } = useTheme();
18
+ const { selectedTheme, setSelectedTheme, savedTheme, setSavedTheme, selectedFontLink, setSelectedFontLink, savedFontLink, setSavedFontLink, fontName, fontLoadStatus, fontLoadError } = useTheme();
19
19
  const handleOpenChange = (_isOpen)=>{
20
- onOpenChange();
20
+ onOpenChange?.(_isOpen);
21
21
  if (!_isOpen) {
22
22
  setSelectedTheme(savedTheme);
23
23
  setSelectedFontLink(savedFontLink);
@@ -26,12 +26,106 @@ const UXThemeSwitch = ({ children })=>{
26
26
  const handleApplyTheme = ()=>{
27
27
  setSavedTheme(selectedTheme);
28
28
  setSavedFontLink(selectedFontLink);
29
- onClose();
29
+ onClose?.();
30
30
  };
31
31
  const handleReset = ()=>{
32
32
  setSelectedTheme(savedTheme);
33
33
  setSelectedFontLink(savedFontLink);
34
34
  };
35
+ return /*#__PURE__*/ jsx(Drawer, {
36
+ isOpen: isOpen,
37
+ backdrop: "transparent",
38
+ onOpenChange: handleOpenChange,
39
+ ...props,
40
+ children: /*#__PURE__*/ jsxs(DrawerContent, {
41
+ children: [
42
+ /*#__PURE__*/ jsx(DrawerHeader, {
43
+ className: "flex flex-col gap-1",
44
+ children: i18n.theme.title
45
+ }),
46
+ /*#__PURE__*/ jsxs(DrawerBody, {
47
+ children: [
48
+ /*#__PURE__*/ jsx(Flex, {
49
+ wrap: true,
50
+ justify: "between",
51
+ className: "gap-y-5",
52
+ children: themeData.map((theme)=>/*#__PURE__*/ jsx(ThemeItem, {
53
+ isSelected: selectedTheme.id === theme.id,
54
+ onClick: ()=>setSelectedTheme(theme),
55
+ ...theme
56
+ }, theme.id))
57
+ }),
58
+ /*#__PURE__*/ jsx(UXDivider, {
59
+ className: "my-lg"
60
+ }),
61
+ /*#__PURE__*/ jsxs(VStack, {
62
+ gap: "md",
63
+ children: [
64
+ /*#__PURE__*/ jsx(Text, {
65
+ body1Bold: true,
66
+ children: i18n.theme.font.title
67
+ }),
68
+ /*#__PURE__*/ jsx(UXInput, {
69
+ isClearable: true,
70
+ startContent: 'loading' === fontLoadStatus && /*#__PURE__*/ jsx(UXSpinner, {
71
+ size: 18
72
+ }),
73
+ isInvalid: 'error' === fontLoadStatus,
74
+ className: "w-full",
75
+ placeholder: i18n.theme.font.placeholder,
76
+ value: selectedFontLink,
77
+ description: /*#__PURE__*/ jsxs(Fragment, {
78
+ children: [
79
+ 'success' === fontLoadStatus && /*#__PURE__*/ jsxs(Text, {
80
+ body3Bold: true,
81
+ color: "success",
82
+ children: [
83
+ i18n.theme.font.success,
84
+ fontName
85
+ ]
86
+ }),
87
+ 'error' === fontLoadStatus && fontLoadError && /*#__PURE__*/ jsx(Text, {
88
+ body3Bold: true,
89
+ color: "danger",
90
+ children: fontLoadError
91
+ })
92
+ ]
93
+ }),
94
+ onValueChange: setSelectedFontLink
95
+ })
96
+ ]
97
+ })
98
+ ]
99
+ }),
100
+ /*#__PURE__*/ jsx(DrawerFooter, {
101
+ className: "mb-8",
102
+ children: /*#__PURE__*/ jsxs(HStack, {
103
+ fullWidth: true,
104
+ gap: "lg",
105
+ children: [
106
+ /*#__PURE__*/ jsx(UXButton, {
107
+ fullWidth: true,
108
+ size: "lg",
109
+ onPress: handleReset,
110
+ children: i18n.theme.reset
111
+ }),
112
+ /*#__PURE__*/ jsx(UXButton, {
113
+ fullWidth: true,
114
+ isDisabled: 'error' === fontLoadStatus,
115
+ size: "lg",
116
+ color: "primary",
117
+ onPress: handleApplyTheme,
118
+ children: i18n.theme.apply
119
+ })
120
+ ]
121
+ })
122
+ })
123
+ ]
124
+ })
125
+ });
126
+ };
127
+ const UXThemeSwitch = ({ children })=>{
128
+ const { isOpen, onOpen, onClose, onOpenChange } = useDisclosure();
35
129
  const renderChildren = ()=>{
36
130
  if (children) return children(onOpen);
37
131
  return /*#__PURE__*/ jsx(UXButton, {
@@ -44,78 +138,12 @@ const UXThemeSwitch = ({ children })=>{
44
138
  return /*#__PURE__*/ jsxs(Fragment, {
45
139
  children: [
46
140
  renderChildren(),
47
- /*#__PURE__*/ jsx(Drawer, {
141
+ /*#__PURE__*/ jsx(UXThemeSwitchDrawer, {
48
142
  isOpen: isOpen,
49
- backdrop: "transparent",
50
- onOpenChange: handleOpenChange,
51
- children: /*#__PURE__*/ jsxs(DrawerContent, {
52
- children: [
53
- /*#__PURE__*/ jsx(DrawerHeader, {
54
- className: "flex flex-col gap-1",
55
- children: i18n.theme.title
56
- }),
57
- /*#__PURE__*/ jsxs(DrawerBody, {
58
- children: [
59
- /*#__PURE__*/ jsx(Flex, {
60
- wrap: true,
61
- justify: "between",
62
- className: "gap-y-5",
63
- children: themeData.map((theme)=>/*#__PURE__*/ jsx(ThemeItem, {
64
- isSelected: selectedTheme.key === theme.key,
65
- zhName: theme.zhName,
66
- enName: theme.enName,
67
- colorVariables: theme.colorVariables,
68
- onClick: ()=>setSelectedTheme(theme)
69
- }, theme.key))
70
- }),
71
- /*#__PURE__*/ jsx(UXDivider, {
72
- className: "my-lg"
73
- }),
74
- /*#__PURE__*/ jsxs(VStack, {
75
- gap: "md",
76
- className: "w-full",
77
- children: [
78
- /*#__PURE__*/ jsx(Text, {
79
- className: "text-sm font-medium",
80
- children: i18n.theme.font.title
81
- }),
82
- /*#__PURE__*/ jsx(UXInput, {
83
- isClearable: true,
84
- className: "w-full",
85
- placeholder: i18n.theme.font.placeholder,
86
- value: selectedFontLink,
87
- onValueChange: setSelectedFontLink
88
- })
89
- ]
90
- })
91
- ]
92
- }),
93
- /*#__PURE__*/ jsx(DrawerFooter, {
94
- className: "mb-8",
95
- children: /*#__PURE__*/ jsxs(HStack, {
96
- fullWidth: true,
97
- gap: "lg",
98
- children: [
99
- /*#__PURE__*/ jsx(UXButton, {
100
- fullWidth: true,
101
- size: "lg",
102
- onPress: handleReset,
103
- children: i18n.theme.reset
104
- }),
105
- /*#__PURE__*/ jsx(UXButton, {
106
- fullWidth: true,
107
- size: "lg",
108
- color: "primary",
109
- onPress: handleApplyTheme,
110
- children: i18n.theme.apply
111
- })
112
- ]
113
- })
114
- })
115
- ]
116
- })
143
+ onClose: onClose,
144
+ onOpenChange: onOpenChange
117
145
  })
118
146
  ]
119
147
  });
120
148
  };
121
- export { UXThemeSwitch };
149
+ export { UXThemeSwitch, UXThemeSwitchDrawer };
@@ -0,0 +1,5 @@
1
+ export declare const useColorScheme: () => {
2
+ colorScheme: import("./theme-data").ColorScheme;
3
+ isDark: boolean;
4
+ isLight: boolean;
5
+ };
@@ -0,0 +1,11 @@
1
+ import { useThemeStore } from "./use-theme-store.js";
2
+ const useColorScheme = ()=>{
3
+ const { selectedTheme } = useThemeStore();
4
+ const { colorScheme } = selectedTheme;
5
+ return {
6
+ colorScheme,
7
+ isDark: 'dark' === colorScheme,
8
+ isLight: 'light' === colorScheme
9
+ };
10
+ };
11
+ export { useColorScheme };
@@ -0,0 +1,19 @@
1
+ export declare const useThemeColor: () => {
2
+ foreground: string;
3
+ secondary: string;
4
+ tertiary: string;
5
+ primary: string;
6
+ success: string;
7
+ danger: string;
8
+ alert: string;
9
+ warning: string;
10
+ gold: string;
11
+ background: string;
12
+ 'background-200': string;
13
+ 'background-300': string;
14
+ 'background-400': string;
15
+ overlay: string;
16
+ divider: string;
17
+ bullish: string;
18
+ bearish: string;
19
+ };
@@ -0,0 +1,6 @@
1
+ import { useThemeStore } from "./use-theme-store.js";
2
+ const useThemeColor = ()=>{
3
+ const { selectedTheme } = useThemeStore();
4
+ return selectedTheme.colorVariables;
5
+ };
6
+ export { useThemeColor };
@@ -1,4 +1,5 @@
1
1
  import { type ThemeItemType } from './theme-data';
2
+ export type FontLoadStatus = 'idle' | 'loading' | 'success' | 'error';
2
3
  interface State {
3
4
  /**
4
5
  * 临时选中的 theme(用于预览)
@@ -16,12 +17,27 @@ interface State {
16
17
  * 保存的字体链接
17
18
  */
18
19
  savedFontLink: string;
20
+ /**
21
+ * 应用的字体名称
22
+ */
23
+ fontName: string;
24
+ /**
25
+ * 字体加载状态
26
+ */
27
+ fontLoadStatus: FontLoadStatus;
28
+ /**
29
+ * 字体加载错误信息
30
+ */
31
+ fontLoadError: string | null;
19
32
  }
20
33
  interface Actions {
21
34
  setSelectedTheme: (theme: ThemeItemType) => void;
22
35
  setSavedTheme: (theme: ThemeItemType) => void;
23
36
  setSelectedFontLink: (link: string) => void;
24
37
  setSavedFontLink: (link: string) => void;
38
+ setFontLoadStatus: (status: FontLoadStatus) => void;
39
+ setFontLoadError: (error: string | null) => void;
40
+ setFontName: (name: string) => void;
25
41
  }
26
42
  type ThemeStore = State & Actions;
27
43
  export declare const useThemeStore: import("zustand").UseBoundStore<Omit<import("zustand").StoreApi<ThemeStore>, "setState" | "persist"> & {
@@ -6,6 +6,9 @@ const useThemeStore = create()(persist((set)=>({
6
6
  savedTheme: themeData["0"],
7
7
  selectedFontLink: '',
8
8
  savedFontLink: '',
9
+ fontName: '',
10
+ fontLoadStatus: 'idle',
11
+ fontLoadError: null,
9
12
  setSelectedTheme: (theme)=>set({
10
13
  selectedTheme: theme
11
14
  }),
@@ -17,9 +20,25 @@ const useThemeStore = create()(persist((set)=>({
17
20
  }),
18
21
  setSavedFontLink: (link)=>set({
19
22
  savedFontLink: link
23
+ }),
24
+ setFontLoadStatus: (status)=>set({
25
+ fontLoadStatus: status
26
+ }),
27
+ setFontLoadError: (error)=>set({
28
+ fontLoadError: error
29
+ }),
30
+ setFontName: (name)=>set({
31
+ fontName: name
20
32
  })
21
33
  }), {
22
34
  name: 'ux-theme',
23
- storage: createJSONStorage(()=>'undefined' != typeof window ? window.localStorage : {})
35
+ storage: createJSONStorage(()=>'undefined' != typeof window ? window.localStorage : {}),
36
+ partialize: (state)=>({
37
+ selectedTheme: state.selectedTheme,
38
+ savedTheme: state.savedTheme,
39
+ fontName: state.fontName,
40
+ selectedFontLink: state.selectedFontLink,
41
+ savedFontLink: state.savedFontLink
42
+ })
24
43
  }));
25
44
  export { useThemeStore };
@@ -1,4 +1,5 @@
1
1
  import { type ThemeItemType } from './theme-data';
2
+ import { type FontLoadStatus } from './use-theme-store';
2
3
  /**
3
4
  * UX 主题管理 Hook
4
5
  */
@@ -11,4 +12,7 @@ export declare const useTheme: () => {
11
12
  setSelectedFontLink: (link: string) => void;
12
13
  savedFontLink: string;
13
14
  setSavedFontLink: (link: string) => void;
15
+ fontLoadStatus: FontLoadStatus;
16
+ fontLoadError: string | null;
17
+ fontName: string;
14
18
  };
@@ -1,4 +1,5 @@
1
1
  import { useEffect } from "react";
2
+ import { useDebounceFn } from "ahooks";
2
3
  import { themeKeys } from "./theme-data.js";
3
4
  import { useThemeStore } from "./use-theme-store.js";
4
5
  const DEFAULT_FONT_FAMILY = 'ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji';
@@ -10,7 +11,7 @@ const applyTheme = (theme)=>{
10
11
  themeKeys.forEach((key)=>{
11
12
  root.classList.remove(key);
12
13
  });
13
- root.classList.add(theme.key);
14
+ root.classList.add(theme.id);
14
15
  };
15
16
  const extractFontFamilyFromLink = (link)=>{
16
17
  if (!link) return null;
@@ -24,38 +25,70 @@ const extractFontFamilyFromLink = (link)=>{
24
25
  return null;
25
26
  }
26
27
  };
27
- const loadGoogleFont = (link)=>{
28
+ const loadGoogleFont = (link, options)=>{
29
+ const { setFontLoadStatus, setFontLoadError, setFontName } = options;
28
30
  if ('undefined' == typeof window) return;
29
31
  const existingLink = document.getElementById('ux-google-font-link');
30
32
  if (existingLink) existingLink.remove();
31
33
  if (!link?.trim()) {
32
34
  document.documentElement.style.setProperty('--ux-font-family', DEFAULT_FONT_FAMILY);
33
35
  document.body.style.fontFamily = DEFAULT_FONT_FAMILY;
36
+ setFontLoadStatus('idle');
37
+ setFontLoadError(null);
38
+ setFontName('');
34
39
  return;
35
40
  }
41
+ setFontLoadStatus('loading');
42
+ setFontLoadError(null);
36
43
  const linkElement = document.createElement('link');
37
44
  linkElement.id = 'ux-google-font-link';
38
45
  linkElement.rel = 'stylesheet';
39
46
  linkElement.href = link;
47
+ linkElement.onload = ()=>{
48
+ setFontLoadStatus('success');
49
+ setFontLoadError(null);
50
+ const fontFamily = extractFontFamilyFromLink(link);
51
+ if (fontFamily) {
52
+ document.documentElement.style.setProperty('--ux-font-family', `"${fontFamily}", ${DEFAULT_FONT_FAMILY}`);
53
+ document.body.style.fontFamily = `"${fontFamily}", ${DEFAULT_FONT_FAMILY}`;
54
+ setFontName(fontFamily);
55
+ }
56
+ };
57
+ linkElement.onerror = ()=>{
58
+ const errorMessage = '字体加载失败,请检查链接是否正确';
59
+ setFontLoadStatus('error');
60
+ setFontLoadError(errorMessage);
61
+ setFontName('');
62
+ document.documentElement.style.setProperty('--ux-font-family', DEFAULT_FONT_FAMILY);
63
+ document.body.style.fontFamily = DEFAULT_FONT_FAMILY;
64
+ };
40
65
  document.head.appendChild(linkElement);
41
- const fontFamily = extractFontFamilyFromLink(link);
42
- if (fontFamily) {
43
- document.documentElement.style.setProperty('--ux-font-family', `"${fontFamily}", ${DEFAULT_FONT_FAMILY}`);
44
- document.body.style.fontFamily = `"${fontFamily}", ${DEFAULT_FONT_FAMILY}`;
45
- }
46
66
  };
47
67
  const useTheme = ()=>{
48
68
  const selectedTheme = useThemeStore((state)=>state.selectedTheme);
49
69
  const savedTheme = useThemeStore((state)=>state.savedTheme);
50
70
  const selectedFontLink = useThemeStore((state)=>state.selectedFontLink);
51
71
  const savedFontLink = useThemeStore((state)=>state.savedFontLink);
72
+ const fontName = useThemeStore((state)=>state.fontName);
73
+ const fontLoadStatus = useThemeStore((state)=>state.fontLoadStatus);
74
+ const fontLoadError = useThemeStore((state)=>state.fontLoadError);
52
75
  const storeSetSelectedTheme = useThemeStore((state)=>state.setSelectedTheme);
53
76
  const storeSetSavedTheme = useThemeStore((state)=>state.setSavedTheme);
54
77
  const storeSetSelectedFontLink = useThemeStore((state)=>state.setSelectedFontLink);
55
78
  const storeSetSavedFontLink = useThemeStore((state)=>state.setSavedFontLink);
79
+ const storeSetFontLoadStatus = useThemeStore((state)=>state.setFontLoadStatus);
80
+ const storeSetFontLoadError = useThemeStore((state)=>state.setFontLoadError);
81
+ const storeSetFontName = useThemeStore((state)=>state.setFontName);
82
+ const { run: debouncedLoadGoogleFont } = useDebounceFn((link)=>loadGoogleFont(link, {
83
+ setFontLoadStatus: storeSetFontLoadStatus,
84
+ setFontLoadError: storeSetFontLoadError,
85
+ setFontName: storeSetFontName
86
+ }), {
87
+ wait: 500
88
+ });
56
89
  useEffect(()=>{
57
90
  applyTheme(savedTheme);
58
- loadGoogleFont(savedFontLink);
91
+ debouncedLoadGoogleFont(savedFontLink);
59
92
  }, [
60
93
  savedTheme,
61
94
  savedFontLink
@@ -70,11 +103,11 @@ const useTheme = ()=>{
70
103
  };
71
104
  const setSelectedFontLink = (link)=>{
72
105
  storeSetSelectedFontLink(link);
73
- loadGoogleFont(link);
106
+ debouncedLoadGoogleFont(link);
74
107
  };
75
108
  const setSavedFontLink = (link)=>{
76
109
  storeSetSavedFontLink(link);
77
- loadGoogleFont(link);
110
+ debouncedLoadGoogleFont(link);
78
111
  };
79
112
  return {
80
113
  selectedTheme,
@@ -84,7 +117,10 @@ const useTheme = ()=>{
84
117
  selectedFontLink,
85
118
  setSelectedFontLink,
86
119
  savedFontLink,
87
- setSavedFontLink
120
+ setSavedFontLink,
121
+ fontLoadStatus,
122
+ fontLoadError,
123
+ fontName
88
124
  };
89
125
  };
90
126
  export { useTheme };
@@ -15,6 +15,9 @@ export declare const useI18n: () => {
15
15
  font: {
16
16
  title: string;
17
17
  placeholder: string;
18
+ loading: string;
19
+ success: string;
20
+ error: string;
18
21
  };
19
22
  };
20
23
  };
@@ -15,7 +15,10 @@ const en = {
15
15
  apply: 'Apply Theme',
16
16
  font: {
17
17
  title: 'Custom Font',
18
- placeholder: 'Enter Google Fonts link'
18
+ placeholder: 'Enter Google Fonts URL or custom font URL',
19
+ loading: 'Loading font...',
20
+ success: 'Font loaded: ',
21
+ error: 'Failed to load font'
19
22
  }
20
23
  }
21
24
  };
@@ -35,7 +38,10 @@ const zh = {
35
38
  apply: '应用主题',
36
39
  font: {
37
40
  title: '自定义字体',
38
- placeholder: '输入 Google Fonts 链接'
41
+ placeholder: '输入 Google Fonts 或自定义字体链接',
42
+ loading: '正在加载字体...',
43
+ success: '字体加载成功:',
44
+ error: '字体加载失败'
39
45
  }
40
46
  }
41
47
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@particle-network/ui-react",
3
- "version": "0.4.0-beta.0",
3
+ "version": "0.4.0-beta.2",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
@@ -50,8 +50,8 @@
50
50
  "ahooks": "^3.9.4",
51
51
  "copy-to-clipboard": "^3.3.3",
52
52
  "zustand": "^5.0.8",
53
- "@particle-network/icons": "0.3.1",
54
- "@particle-network/ui-shared": "0.2.0"
53
+ "@particle-network/icons": "0.4.0-beta.0",
54
+ "@particle-network/ui-shared": "0.3.0-beta.0"
55
55
  },
56
56
  "scripts": {
57
57
  "build": "rslib build",