myshell-react-lib 0.1.0

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.
Files changed (151) hide show
  1. package/README.md +268 -0
  2. package/dist/assets/audio-playing.json +3657 -0
  3. package/dist/index.cjs +9654 -0
  4. package/dist/index.cjs.map +1 -0
  5. package/dist/index.d.cts +1431 -0
  6. package/dist/index.d.ts +1431 -0
  7. package/dist/index.js +8788 -0
  8. package/dist/index.js.map +1 -0
  9. package/package.json +140 -0
  10. package/src/common/assets/audio-playing.json +3657 -0
  11. package/src/common/constants/constants.ts +24 -0
  12. package/src/common/constants/types/common.ts +10 -0
  13. package/src/common/hooks/useAudioPlayer.tsx +198 -0
  14. package/src/common/hooks/useDevice.ts +26 -0
  15. package/src/common/hooks/useNativeBridge.ts +42 -0
  16. package/src/common/hooks/useNotification.tsx +179 -0
  17. package/src/common/hooks/useWindowWidth.ts +19 -0
  18. package/src/common/utils/common-helper.ts +81 -0
  19. package/src/components/ItemDemo.tsx +15 -0
  20. package/src/components/accordion.tsx +126 -0
  21. package/src/components/alert-dialog.tsx +148 -0
  22. package/src/components/alert.tsx +65 -0
  23. package/src/components/aspect-ratio.tsx +7 -0
  24. package/src/components/audio-player.tsx +58 -0
  25. package/src/components/avatar.tsx +133 -0
  26. package/src/components/badge.tsx +65 -0
  27. package/src/components/button/button.styles.ts +258 -0
  28. package/src/components/button/button.tsx +215 -0
  29. package/src/components/button/icon-button.styles.ts +101 -0
  30. package/src/components/button/icon-button.tsx +100 -0
  31. package/src/components/button/index.tsx +3 -0
  32. package/src/components/button/link-button.tsx +184 -0
  33. package/src/components/cascader.tsx +175 -0
  34. package/src/components/checkbox.tsx +135 -0
  35. package/src/components/command.tsx +155 -0
  36. package/src/components/context-menu.tsx +198 -0
  37. package/src/components/count-down.tsx +83 -0
  38. package/src/components/custom-notification.tsx +95 -0
  39. package/src/components/dialog.tsx +158 -0
  40. package/src/components/drawer.tsx +116 -0
  41. package/src/components/dropdown-menu.tsx +196 -0
  42. package/src/components/energy-progress.tsx +55 -0
  43. package/src/components/form.tsx +201 -0
  44. package/src/components/group.tsx +9 -0
  45. package/src/components/guide.tsx +243 -0
  46. package/src/components/icon.tsx +89 -0
  47. package/src/components/icons/outline/DownIcon.tsx +18 -0
  48. package/src/components/icons/outline/FilterIcon.tsx +21 -0
  49. package/src/components/icons/outline/arrow-left.tsx +16 -0
  50. package/src/components/icons/outline/arrow-up-tray.tsx +16 -0
  51. package/src/components/icons/outline/check-circle.tsx +17 -0
  52. package/src/components/icons/outline/config.tsx +42 -0
  53. package/src/components/icons/outline/pencil-square.tsx +16 -0
  54. package/src/components/icons/outline/trash.tsx +17 -0
  55. package/src/components/icons/outline/window.tsx +16 -0
  56. package/src/components/icons/outline/x-circle.tsx +17 -0
  57. package/src/components/icons/outline/x-mark.tsx +16 -0
  58. package/src/components/icons/solid/audio-playing.tsx +31 -0
  59. package/src/components/icons/solid/caret-down.tsx +14 -0
  60. package/src/components/icons/solid/code.tsx +18 -0
  61. package/src/components/icons/solid/drag.tsx +14 -0
  62. package/src/components/icons/solid/phone.tsx +23 -0
  63. package/src/components/icons/solid/rectangle-group.tsx +14 -0
  64. package/src/components/image.tsx +151 -0
  65. package/src/components/input.tsx +118 -0
  66. package/src/components/label.tsx +26 -0
  67. package/src/components/link.tsx +123 -0
  68. package/src/components/marquee/index.css +15 -0
  69. package/src/components/marquee/marquee.tsx +220 -0
  70. package/src/components/masonry.tsx +138 -0
  71. package/src/components/menubar.tsx +234 -0
  72. package/src/components/mobile/m-tooltip.tsx +34 -0
  73. package/src/components/modal.tsx +561 -0
  74. package/src/components/navigation-bar.tsx +100 -0
  75. package/src/components/number-input.tsx +143 -0
  76. package/src/components/page-content.tsx +16 -0
  77. package/src/components/popover.tsx +191 -0
  78. package/src/components/progress.tsx +80 -0
  79. package/src/components/radio-group.tsx +44 -0
  80. package/src/components/scroll-area.tsx +49 -0
  81. package/src/components/search-bar.tsx +140 -0
  82. package/src/components/secondary-navigation-bar.tsx +307 -0
  83. package/src/components/select.tsx +273 -0
  84. package/src/components/separator.tsx +31 -0
  85. package/src/components/sheet.tsx +143 -0
  86. package/src/components/skeleton.tsx +20 -0
  87. package/src/components/slider.tsx +160 -0
  88. package/src/components/spinner.tsx +48 -0
  89. package/src/components/swiper/index.module.scss +88 -0
  90. package/src/components/swiper/index.tsx +319 -0
  91. package/src/components/switch.tsx +67 -0
  92. package/src/components/tabs.tsx +325 -0
  93. package/src/components/textarea.tsx +71 -0
  94. package/src/components/toast/toast.tsx +182 -0
  95. package/src/components/toast/toaster.tsx +160 -0
  96. package/src/components/toast/use-toast.tsx +248 -0
  97. package/src/components/toggle-group.tsx +64 -0
  98. package/src/components/toggle.tsx +46 -0
  99. package/src/components/tooltip.tsx +283 -0
  100. package/src/components/typography.tsx +437 -0
  101. package/src/index.ts +66 -0
  102. package/src/lib/utils.ts +62 -0
  103. package/src/stories/Accordion.stories.tsx +64 -0
  104. package/src/stories/AccordionItem.stories.tsx +48 -0
  105. package/src/stories/Avatar.stories.ts +58 -0
  106. package/src/stories/Badge.stories.tsx +40 -0
  107. package/src/stories/BannerSwiper.stories.tsx +102 -0
  108. package/src/stories/Button.stories.tsx +543 -0
  109. package/src/stories/Checkbox.stories.tsx +161 -0
  110. package/src/stories/Configure.mdx +341 -0
  111. package/src/stories/CssProperties.mdx +30 -0
  112. package/src/stories/Description.stories.ts +70 -0
  113. package/src/stories/Display.stories.ts +64 -0
  114. package/src/stories/FeaturedSwiper.stories.tsx +6978 -0
  115. package/src/stories/GridSwiper.stories.tsx +1407 -0
  116. package/src/stories/Guide.stories.tsx +247 -0
  117. package/src/stories/Heading.stories.ts +89 -0
  118. package/src/stories/Icon.stories.ts +77 -0
  119. package/src/stories/IconButton.stories.tsx +301 -0
  120. package/src/stories/IconTextButton.stories.ts +59 -0
  121. package/src/stories/Image.stories.ts +55 -0
  122. package/src/stories/Input.stories.tsx +203 -0
  123. package/src/stories/Modal.stories.tsx +144 -0
  124. package/src/stories/NavigationBar.stories.tsx +81 -0
  125. package/src/stories/Notification.stories.tsx +276 -0
  126. package/src/stories/Popover.stories.tsx +100 -0
  127. package/src/stories/SearchBar.stories.ts +43 -0
  128. package/src/stories/SecondaryNavigationBar.stories.tsx +199 -0
  129. package/src/stories/Select.stories.tsx +107 -0
  130. package/src/stories/Separator.stories.tsx +49 -0
  131. package/src/stories/Spinner.stories.tsx +48 -0
  132. package/src/stories/SubHeading.stories.ts +64 -0
  133. package/src/stories/Swich.stories.tsx +69 -0
  134. package/src/stories/Tabs.stories.tsx +90 -0
  135. package/src/stories/Text.stories.ts +78 -0
  136. package/src/stories/Textarea.stories.tsx +155 -0
  137. package/src/stories/Toast.stories.tsx +424 -0
  138. package/src/stories/Tooltip.stories.tsx +244 -0
  139. package/src/stories/ViewAutoSwiper.stories.tsx +1408 -0
  140. package/src/styles/components-dark.scss +212 -0
  141. package/src/styles/components-light.scss +210 -0
  142. package/src/styles/design-dark.scss +330 -0
  143. package/src/styles/design-light.scss +345 -0
  144. package/src/styles/design2-dark.scss +319 -0
  145. package/src/styles/design2-light.scss +364 -0
  146. package/src/styles/font.css +19 -0
  147. package/src/styles/global.scss +251 -0
  148. package/src/styles/md-viewer.scss +155 -0
  149. package/src/styles/new-tokens.scss +255 -0
  150. package/src/styles/tokens.scss +401 -0
  151. package/src/types/scss.d.ts +24 -0
@@ -0,0 +1,24 @@
1
+ /**
2
+ * 通用白名单
3
+ */
4
+
5
+ export const NotMatchWhitelist = [
6
+ '/privacy-policy',
7
+ '/tos',
8
+ '/connect/tg',
9
+ '/browser',
10
+ '/upload',
11
+ '/shadcn-playground',
12
+ '/sc-did',
13
+ '/mainpage',
14
+ '/sitemap',
15
+ '/chatpage',
16
+ '/widgetpage',
17
+ '/postpage',
18
+ '/articlepage',
19
+ '/gallerypage',
20
+ '/botimage',
21
+ '/galleryimage',
22
+ '/robots.txt',
23
+ '/favicon.ico',
24
+ ];
@@ -0,0 +1,10 @@
1
+ /**
2
+ * 通用类型定义
3
+ */
4
+
5
+ export type HeroIcon = React.ForwardRefExoticComponent<
6
+ React.PropsWithoutRef<React.SVGProps<SVGSVGElement>> & {
7
+ title?: string;
8
+ titleId?: string;
9
+ } & React.RefAttributes<SVGSVGElement>
10
+ >;
@@ -0,0 +1,198 @@
1
+ import {
2
+ forwardRef,
3
+ useCallback,
4
+ useImperativeHandle,
5
+ useLayoutEffect,
6
+ useRef,
7
+ } from 'react';
8
+
9
+ import { useDevice } from './useDevice';
10
+
11
+ declare global {
12
+ interface Window {
13
+ webkitAudioContext: typeof AudioContext;
14
+ }
15
+ }
16
+
17
+ type AudioAPI = {
18
+ play(arrayBuffer: ArrayBuffer, start: number): void;
19
+ pause(): void;
20
+ };
21
+
22
+ let audioAPI: AudioAPI | null = null;
23
+
24
+ const getAudioAPI = (): AudioAPI => {
25
+ if (audioAPI) return audioAPI;
26
+
27
+ // make it crossbrowser, vendor prefix implement is supported from 6.14 (Released 2012-09-10)
28
+ const AudioContext = window.AudioContext || window.webkitAudioContext;
29
+ const context = new AudioContext();
30
+ const gainNode = context.createGain();
31
+ // set volume to 100%
32
+ gainNode.gain.value = 1;
33
+
34
+ let currentSourceNode: AudioBufferSourceNode | null = null;
35
+
36
+ // unlock the audio context
37
+ window.addEventListener(
38
+ 'touchstart',
39
+ () => {
40
+ // target to play an empty audio
41
+ const buffer = context.createBuffer(1, 1, 22050);
42
+ const source = context.createBufferSource();
43
+ source.buffer = buffer;
44
+ source.connect(context.destination);
45
+ source.start(0);
46
+ },
47
+ { once: true }
48
+ );
49
+
50
+ audioAPI = {
51
+ play(arrayBuffer: ArrayBuffer, start: number) {
52
+ // call slice to clone, avoid disposing the input array buffer
53
+ context.decodeAudioData(arrayBuffer.slice(0), (audioBuffer) => {
54
+ if (context.state === 'suspended') {
55
+ if (currentSourceNode) {
56
+ currentSourceNode.stop();
57
+ }
58
+
59
+ context.resume();
60
+ }
61
+
62
+ if (gainNode.gain.value === 0) {
63
+ gainNode.gain.value = 1;
64
+ }
65
+ const source = context.createBufferSource();
66
+ source.buffer = audioBuffer;
67
+ source.connect(context.destination);
68
+ currentSourceNode = source;
69
+
70
+ source.start(0, start);
71
+ });
72
+ },
73
+ pause() {
74
+ // if (context.state === 'running') {
75
+ // context.suspend();
76
+ // }
77
+ // 不能判定状态,否则在 iOS 端,若录制声音,然后再继续播放,会有两条音轨播放
78
+ context.suspend();
79
+ },
80
+ };
81
+
82
+ return audioAPI;
83
+ };
84
+
85
+ const IosAudioPlayer = forwardRef(
86
+ (
87
+ props: React.DetailedHTMLProps<
88
+ React.AudioHTMLAttributes<HTMLAudioElement>,
89
+ HTMLAudioElement
90
+ >,
91
+ ref
92
+ ) => {
93
+ const audioRef = useRef<HTMLAudioElement>(null);
94
+ const audioApi = getAudioAPI();
95
+ const loadTask = useRef<Promise<ArrayBuffer | void>>();
96
+
97
+ useLayoutEffect(() => {
98
+ return () => {
99
+ audioApi.pause();
100
+ if (audioRef.current) {
101
+ audioRef.current!.pause();
102
+ }
103
+ };
104
+ }, []);
105
+
106
+ const handlePlay = useCallback(async () => {
107
+ if (!audioRef.current?.paused) return Promise.reject();
108
+
109
+ if (props.src && loadTask.current) {
110
+ const arrayBuffer = await loadTask.current;
111
+ if (arrayBuffer) {
112
+ return audioApi.play(arrayBuffer, audioRef.current!.currentTime);
113
+ }
114
+ }
115
+
116
+ return Promise.resolve();
117
+ }, []);
118
+
119
+ const handleLoad = useCallback(async () => {
120
+ if (props.src) {
121
+ try {
122
+ loadTask.current = window
123
+ .fetch(props.src)
124
+ .then((response) => response.arrayBuffer())
125
+ .catch((error) => {
126
+ console.error(error);
127
+ });
128
+ } catch (error) {
129
+ console.error(error);
130
+ }
131
+ }
132
+ }, []);
133
+
134
+ // expose same ref interface as audio element, delegate to muted <audio /> besides real audio play implement
135
+ useImperativeHandle(
136
+ ref,
137
+ () => {
138
+ return {
139
+ play: () =>
140
+ handlePlay()
141
+ .then(() => {
142
+ console.log('audioCompPlay');
143
+
144
+ return audioRef.current!.play();
145
+ })
146
+ .catch(() => {
147
+ // playing
148
+ }),
149
+ pause: () => {
150
+ audioAPI?.pause();
151
+
152
+ return audioRef.current!.pause();
153
+ },
154
+ load: () => {
155
+ // loading the audio array buffer
156
+ handleLoad();
157
+
158
+ return audioRef.current!.load();
159
+ },
160
+ addEventListener: (
161
+ type: 'play' | 'pause',
162
+ listener: (this: HTMLAudioElement, ev: Event) => any,
163
+ options?: boolean | AddEventListenerOptions | undefined
164
+ ) => {
165
+ return audioRef.current!.addEventListener(type, listener, options);
166
+ },
167
+ get currentTime() {
168
+ return audioRef.current!.currentTime;
169
+ },
170
+ set currentTime(value) {
171
+ audioRef.current!.currentTime = value;
172
+ },
173
+ get duration() {
174
+ return audioRef.current!.duration;
175
+ },
176
+ get paused() {
177
+ return audioRef.current!.paused;
178
+ },
179
+ };
180
+ },
181
+ [handlePlay, handleLoad]
182
+ );
183
+
184
+ // iOS allows muted audio played without a user gesture, use this silent <audio /> to reflect the current audio status.
185
+ // eslint-disable-next-line react/jsx-props-no-spreading
186
+ return <audio {...props} muted ref={audioRef} />;
187
+ }
188
+ );
189
+
190
+ IosAudioPlayer.displayName = 'IosAudioPlayer';
191
+
192
+ const useAudioPlayer = (): React.ComponentType | 'audio' => {
193
+ const { isIos } = useDevice();
194
+
195
+ return !isIos ? 'audio' : IosAudioPlayer;
196
+ };
197
+
198
+ export default useAudioPlayer;
@@ -0,0 +1,26 @@
1
+ import isMobile from 'ismobilejs';
2
+ import { useState, useEffect } from 'react';
3
+
4
+ import { useWindowWidth } from './useWindowWidth';
5
+
6
+ export const useDevice = () => {
7
+ const [device, setDevice] = useState(isMobile());
8
+ const [isWeixin, setIsWeixin] = useState<boolean>(false);
9
+ useEffect(() => {
10
+ const device = isMobile();
11
+ setDevice(device);
12
+ setIsWeixin(
13
+ navigator.userAgent.toLowerCase().indexOf('micromessenger') !== -1
14
+ );
15
+ }, []);
16
+
17
+ return {
18
+ isMobile: device.phone || device.tablet,
19
+ isIos: device.apple.phone || device.apple.tablet || device.apple.ipod,
20
+ isWeixin,
21
+ };
22
+ };
23
+
24
+ export const useIsMobileByWindowWidth = () => {
25
+ return useWindowWidth() <= 768;
26
+ };
@@ -0,0 +1,42 @@
1
+ /**
2
+ * 与原生应用交互的钩子
3
+ */
4
+
5
+ import { useCallback } from 'react';
6
+ import { isIosApp } from '../utils/common-helper';
7
+
8
+ export function useNativeBridge() {
9
+ const callNative = useCallback((action: string, data?: any) => {
10
+ if (isIosApp() && window.webkit?.messageHandlers?.reactNative) {
11
+ window.webkit.messageHandlers.reactNative.postMessage({
12
+ action,
13
+ data,
14
+ });
15
+ }
16
+
17
+ // 安卓交互逻辑
18
+ if (typeof window !== 'undefined' && (window as any).ReactNativeWebView) {
19
+ (window as any).ReactNativeWebView.postMessage(
20
+ JSON.stringify({
21
+ action,
22
+ data,
23
+ })
24
+ );
25
+ }
26
+ }, []);
27
+
28
+ const getIosUrl = useCallback((path: string) => {
29
+ // 简单实现,实际可能需要更复杂的逻辑
30
+ return path;
31
+ }, []);
32
+
33
+ const nativeGoBack = useCallback(() => {
34
+ callNative('goBack');
35
+ }, [callNative]);
36
+
37
+ return {
38
+ callNative,
39
+ getIosUrl,
40
+ nativeGoBack,
41
+ };
42
+ }
@@ -0,0 +1,179 @@
1
+ import CustomNotification from '@/components/custom-notification';
2
+ import { useCallback } from 'react';
3
+ import { toast, ToastOptions } from 'react-hot-toast';
4
+
5
+ export type ToastType = 'success' | 'info' | 'warning' | 'error';
6
+
7
+ export interface CustomToasterProps {
8
+ id?: string;
9
+ type?: ToastType;
10
+ title?: string;
11
+ content: string;
12
+ isClosable?: boolean;
13
+ translateInToast?: boolean;
14
+ reason?: string;
15
+ action?: React.ReactNode;
16
+ loading?: boolean;
17
+ }
18
+
19
+ export function useNotification() {
20
+ const addToast = useCallback(
21
+ (config: CustomToasterProps, duration?: number) => {
22
+ // 如果显式指定 id 的,则同时只能有一条显示
23
+ if (config.id) {
24
+ toast.remove(config.id);
25
+ }
26
+ toast.custom(
27
+ (t: ToastOptions) => {
28
+ return (
29
+ <CustomNotification
30
+ tProps={{ ...t, duration }}
31
+ customProps={config}
32
+ />
33
+ );
34
+ },
35
+ {
36
+ id: config.id,
37
+ duration,
38
+ }
39
+ );
40
+ },
41
+ []
42
+ );
43
+
44
+ const message = useCallback(
45
+ (config: CustomToasterProps, duration?: number) =>
46
+ addToast(
47
+ {
48
+ ...config,
49
+ },
50
+ duration
51
+ ),
52
+ []
53
+ );
54
+
55
+ const success = useCallback(
56
+ (config: CustomToasterProps, duration?: number) =>
57
+ addToast(
58
+ {
59
+ ...config,
60
+ type: 'success',
61
+ },
62
+ duration
63
+ ),
64
+ []
65
+ );
66
+
67
+ const error = useCallback(
68
+ (config: CustomToasterProps, duration?: number) =>
69
+ addToast(
70
+ {
71
+ ...config,
72
+ type: 'error',
73
+ },
74
+ duration
75
+ ),
76
+ []
77
+ );
78
+
79
+ const warning = useCallback(
80
+ (config: CustomToasterProps, duration?: number) =>
81
+ addToast(
82
+ {
83
+ ...config,
84
+ type: 'warning',
85
+ },
86
+ duration
87
+ ),
88
+ []
89
+ );
90
+
91
+ const info = useCallback(
92
+ (config: CustomToasterProps, duration?: number) =>
93
+ addToast(
94
+ {
95
+ ...config,
96
+ type: 'info',
97
+ },
98
+ duration
99
+ ),
100
+ []
101
+ );
102
+
103
+ const close = useCallback((id: string) => {
104
+ toast.dismiss(id);
105
+ }, []);
106
+
107
+ return { addToast, success, error, warning, info, close, message };
108
+ }
109
+
110
+ export class Message {
111
+ private static _toast(config: CustomToasterProps, duration?: number) {
112
+ if (config.id) {
113
+ toast.remove(config.id);
114
+ }
115
+
116
+ const addToast = () => {
117
+ toast.custom(
118
+ (t: ToastOptions) => {
119
+ return (
120
+ <CustomNotification
121
+ tProps={{ ...t, duration }}
122
+ customProps={config}
123
+ />
124
+ );
125
+ },
126
+ {
127
+ id: config.id,
128
+ duration,
129
+ }
130
+ );
131
+ };
132
+
133
+ addToast();
134
+ }
135
+
136
+ static success(config: CustomToasterProps, duration?: number) {
137
+ return this._toast(
138
+ {
139
+ ...config,
140
+ type: 'success',
141
+ },
142
+ duration
143
+ );
144
+ }
145
+
146
+ static error(config: CustomToasterProps, duration?: number) {
147
+ return this._toast(
148
+ {
149
+ ...config,
150
+ type: 'error',
151
+ },
152
+ duration
153
+ );
154
+ }
155
+
156
+ static info(config: CustomToasterProps, duration?: number) {
157
+ return this._toast(
158
+ {
159
+ ...config,
160
+ type: 'info',
161
+ },
162
+ duration
163
+ );
164
+ }
165
+
166
+ static warning(config: CustomToasterProps, duration?: number) {
167
+ return this._toast(
168
+ {
169
+ ...config,
170
+ type: 'warning',
171
+ },
172
+ duration
173
+ );
174
+ }
175
+
176
+ static close(id: string) {
177
+ toast.dismiss(id);
178
+ }
179
+ }
@@ -0,0 +1,19 @@
1
+ import { useEffect, useState } from 'react';
2
+
3
+ export const useWindowWidth = () => {
4
+ const [windowWidth, setWindowWidth] = useState(0);
5
+ useEffect(() => {
6
+ const handleResize = () => {
7
+ setWindowWidth(window.innerWidth);
8
+ };
9
+ window.addEventListener('resize', handleResize);
10
+ handleResize();
11
+
12
+ // 在组件卸载时移除事件监听
13
+ return () => {
14
+ window.removeEventListener('resize', handleResize);
15
+ };
16
+ }, []);
17
+
18
+ return windowWidth;
19
+ };
@@ -0,0 +1,81 @@
1
+ export const CDN_URL = 'https://www.myshellstatic.com/';
2
+ /**
3
+ * 通用工具函数
4
+ */
5
+
6
+ /**
7
+ * 获取资源URL
8
+ */
9
+
10
+ export function getAssetsUrl(url: string | any) {
11
+ if (!isString(url) || !(url || '').trim()) {
12
+ return url;
13
+ }
14
+ if (url.startsWith('http')) {
15
+ return url;
16
+ }
17
+
18
+ return `${CDN_URL}${url}`;
19
+ }
20
+
21
+ /**
22
+ * 格式化时长
23
+ */
24
+ export function durationFormatter(seconds: number): string {
25
+ const minutes = Math.floor(seconds / 60);
26
+ const remainingSeconds = Math.floor(seconds % 60);
27
+ return `${minutes}:${remainingSeconds.toString().padStart(2, '0')}`;
28
+ }
29
+
30
+ /**
31
+ * 获取百分比
32
+ */
33
+ export function getPercent(value: number, total: number): number {
34
+ if (total === 0) return 0;
35
+ return (value / total) * 100;
36
+ }
37
+
38
+ /**
39
+ * 限制数值在指定范围内
40
+ */
41
+ export function clamp(value: number, min: number, max: number): number {
42
+ return Math.min(Math.max(value, min), max);
43
+ }
44
+
45
+ /**
46
+ * 检查是否为客户端环境
47
+ */
48
+ export function isClient(): boolean {
49
+ return typeof window !== 'undefined';
50
+ }
51
+
52
+ /**
53
+ * 为Window添加webkit属性的类型定义
54
+ */
55
+ declare global {
56
+ interface Window {
57
+ webkit?: {
58
+ messageHandlers?: {
59
+ reactNative?: {
60
+ postMessage: (message: any) => void;
61
+ };
62
+ };
63
+ };
64
+ }
65
+ }
66
+
67
+ /**
68
+ * 检查是否为iOS应用环境
69
+ */
70
+ export function isIosApp(): boolean {
71
+ return (
72
+ isClient() &&
73
+ typeof navigator !== 'undefined' &&
74
+ /iPhone|iPad|iPod/i.test(navigator.userAgent) &&
75
+ !!window.webkit
76
+ );
77
+ }
78
+
79
+ export function isString(str: any): str is string {
80
+ return typeof str === 'string' || str instanceof String;
81
+ }
@@ -0,0 +1,15 @@
1
+ 'use client';
2
+
3
+ import React from 'react';
4
+
5
+ const ItemDemo: React.FC = () => {
6
+ console.log('a');
7
+
8
+ return (
9
+ <div className="text-lg text-Colors-Text-Brand-Default">
10
+ Yes. this is ItemDemo children.
11
+ </div>
12
+ );
13
+ };
14
+
15
+ export default ItemDemo;