(value: T) => {
4 | const ref = React.useRef(value);
5 | ref.current = value;
6 |
7 | return ref;
8 | };
9 |
10 | export default useLatest;
11 |
--------------------------------------------------------------------------------
/packages/hooks/useLockFn.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const useLockFn = (fn: (...args: P) => Promise) => {
4 | const lockRef = React.useRef(false);
5 |
6 | return React.useCallback(
7 | async (...args: P) => {
8 | if (lockRef.current) return;
9 |
10 | lockRef.current = true;
11 |
12 | try {
13 | const ret = await fn(...args);
14 | lockRef.current = false;
15 | return ret;
16 | } catch (e) {
17 | lockRef.current = false;
18 | throw e;
19 | }
20 | },
21 | [fn]
22 | );
23 | };
24 |
25 | export default useLockFn;
26 |
--------------------------------------------------------------------------------
/packages/hooks/useMemoizedFn.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import useLatest from '@/hooks/useLatest';
4 |
5 | const useMemoizedFn = (fn: (...args: unknown[]) => void) => {
6 | const latestfnRef = useLatest(fn);
7 |
8 | const memoizedFn = React.useRef((...args: unknown[]) => {
9 | latestfnRef.current?.(...args);
10 | });
11 |
12 | return memoizedFn.current;
13 | };
14 |
15 | export default useMemoizedFn;
16 |
--------------------------------------------------------------------------------
/packages/hooks/useMount.tsx:
--------------------------------------------------------------------------------
1 | import useEffectOnce from '@/hooks/useEffectOnce';
2 |
3 | const useMount = (fn: () => void) => {
4 | useEffectOnce(() => {
5 | fn();
6 | });
7 | };
8 |
9 | export default useMount;
10 |
--------------------------------------------------------------------------------
/packages/hooks/useReadLocalStorage.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | type Value = T | null;
4 |
5 | const useReadLocalStorage = (key: string): Value => {
6 | const readValue = React.useCallback((): Value => {
7 | if (typeof window === 'undefined') {
8 | return null;
9 | }
10 |
11 | try {
12 | const item = window.localStorage.getItem(key);
13 | return item ? (JSON.parse(item) as T) : null;
14 | } catch (err) {
15 | console.warn(`Error reading localStorage key "${key}":`, err);
16 | return null;
17 | }
18 | }, [key]);
19 |
20 | const [storedValue, setStoredValue] = React.useState>(readValue);
21 |
22 | const handleStorageChange = React.useCallback(
23 | (event: StorageEvent) => {
24 | if (event.key !== key) {
25 | return;
26 | }
27 | setStoredValue(readValue());
28 | },
29 | [key, readValue]
30 | );
31 |
32 | React.useEffect(() => {
33 | window.addEventListener('storage', handleStorageChange);
34 |
35 | return () => {
36 | window.removeEventListener('storage', handleStorageChange);
37 | };
38 | }, [handleStorageChange]);
39 |
40 | return storedValue;
41 | };
42 |
43 | export default useReadLocalStorage;
44 |
--------------------------------------------------------------------------------
/packages/hooks/useResizeObserver.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import useIsomorphicLayoutEffect from '@/hooks/useIsomorphicLayoutEffect';
4 |
5 | const useResizeObserver = (callback: (target: T) => void, targetRef: React.RefObject) => {
6 | useIsomorphicLayoutEffect(() => {
7 | const element = targetRef.current;
8 | if (!element) return;
9 |
10 | if (window.ResizeObserver) {
11 | const observer = new ResizeObserver(() => {
12 | callback(element);
13 | });
14 | observer.observe(element);
15 | return () => {
16 | observer.disconnect();
17 | };
18 | }
19 | callback(element);
20 |
21 | return () => null;
22 | }, []);
23 | };
24 |
25 | export default useResizeObserver;
26 |
--------------------------------------------------------------------------------
/packages/hooks/useScrollLock.tsx:
--------------------------------------------------------------------------------
1 | import useIsomorphicLayoutEffect from '@/hooks/useIsomorphicLayoutEffect';
2 |
3 | const useScrollLock = (visible: boolean) => {
4 | useIsomorphicLayoutEffect(() => {
5 | if (!visible) return;
6 |
7 | const el = document.getElementsByTagName('html')[0];
8 | el.style.overflow = 'hidden';
9 |
10 | return () => {
11 | el.style.overflow = '';
12 | };
13 | }, [visible]);
14 | };
15 |
16 | export default useScrollLock;
17 |
--------------------------------------------------------------------------------
/packages/hooks/useThrottleFn.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import useLatest from '@/hooks/useLatest';
3 | import useUnmount from '@/hooks/useUnmount';
4 |
5 | const useThrottleFn = (fn: (...args: any[]) => any, ms: number) => {
6 | const timerRef = React.useRef>();
7 | const fnRef = useLatest(fn);
8 |
9 | const timeoutCallback = React.useCallback(() => {
10 | fnRef.current();
11 | timerRef.current = undefined;
12 | }, []);
13 |
14 | React.useEffect(() => {
15 | if (!timerRef.current) {
16 | timerRef.current = setTimeout(timeoutCallback, ms);
17 | }
18 | }, [ms, timeoutCallback]);
19 |
20 | useUnmount(() => {
21 | timerRef.current && clearTimeout(timerRef.current);
22 | });
23 |
24 | return {};
25 | };
26 |
27 | export default useThrottleFn;
28 |
--------------------------------------------------------------------------------
/packages/hooks/useUnmount.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import useEffectOnce from '@/hooks/useEffectOnce';
3 |
4 | const useUnmount = (fn: () => any) => {
5 | const fnRef = React.useRef(fn);
6 |
7 | fnRef.current = fn;
8 |
9 | useEffectOnce(() => () => fnRef.current());
10 | };
11 |
12 | export default useUnmount;
13 |
--------------------------------------------------------------------------------
/packages/hooks/useUpdateEffect.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const useUpdateEffect = (callback: React.EffectCallback, deep?: React.DependencyList) => {
4 | const isMounted = React.useRef(false);
5 |
6 | React.useEffect(() => {
7 | return () => {
8 | isMounted.current = false;
9 | };
10 | }, []);
11 |
12 | React.useEffect(() => {
13 | if (!isMounted.current) {
14 | isMounted.current = true;
15 | } else {
16 | callback();
17 | }
18 | // eslint-disable-next-line react-hooks/exhaustive-deps
19 | }, deep);
20 | };
21 |
22 | export default useUpdateEffect;
23 |
--------------------------------------------------------------------------------
/packages/hooks/useUpdateIsomorphicLayoutEffect.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import useIsomorphicLayoutEffect from '@/hooks/useIsomorphicLayoutEffect';
4 |
5 | const useUpdateIsomorphicLayoutEffect = (callback: React.EffectCallback, deep?: React.DependencyList) => {
6 | const isMounted = React.useRef(false);
7 |
8 | useIsomorphicLayoutEffect(() => {
9 | return () => {
10 | isMounted.current = false;
11 | };
12 | }, []);
13 |
14 | useIsomorphicLayoutEffect(() => {
15 | if (!isMounted.current) {
16 | isMounted.current = true;
17 | } else {
18 | callback();
19 | }
20 | }, deep);
21 | };
22 |
23 | export default useUpdateIsomorphicLayoutEffect;
24 |
--------------------------------------------------------------------------------
/packages/image/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import useIntersectionObserver from '@/hooks/useIntersectionObserver';
3 |
4 | export interface ImageProps {
5 | /** 图片地址 */
6 | src: string;
7 | /** 图片描述 */
8 | alt?: string;
9 | /** 图片宽度 */
10 | width?: number | string;
11 | /** 图片高度 */
12 | height?: number | string;
13 | /** 加载时的占位图地址 */
14 | loading?: string;
15 | style?: React.CSSProperties;
16 | /** 是否懒加载 */
17 | lazy?: boolean;
18 | /** 图片填充模式 */
19 | fit?: 'contain' | 'cover' | 'fill' | 'scale-down';
20 | className?: string;
21 | /** 图片点击事件 */
22 | onClick?: (event: React.MouseEvent) => void;
23 | /** 图片加载失败时回调 */
24 | onError?: (event: React.SyntheticEvent) => void;
25 | /** 图片加载完成时回调 */
26 | onLoad?: (event: React.SyntheticEvent) => void;
27 | }
28 |
29 | const Image: React.FC = (props) => {
30 | const imageRef = React.useRef(null);
31 | const observerEntry = useIntersectionObserver(imageRef, { freezeOnceVisible: true });
32 |
33 | return (
34 |
47 | );
48 | };
49 |
50 | Image.defaultProps = {
51 | alt: '',
52 | width: '100%',
53 | height: '100%',
54 | lazy: false,
55 | fit: 'fill',
56 | loading:
57 | 'data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mM8/x8AAqMB0Fk+W34AAAAASUVORK5CYII=',
58 | };
59 |
60 | Image.displayName = 'Image';
61 |
62 | export default Image;
63 |
--------------------------------------------------------------------------------
/packages/index.tsx:
--------------------------------------------------------------------------------
1 | import './styles/index.scss';
2 |
3 | export { default as Button } from '@/button';
4 | export type { ButtonProps } from '@/button';
5 |
6 | export { default as Mask } from '@/mask';
7 | export type { MaskProps } from '@/mask';
8 |
9 | export { default as Popup } from '@/popup';
10 | export type { PopupProps } from '@/popup';
11 |
12 | export { default as Toast } from '@/toast';
13 | export type { ToastProps, ToastShowProps } from '@/toast';
14 |
15 | export { default as SpinnerLoading } from '@/spinner-loading';
16 | export type { SpinnerLoadingProps } from '@/spinner-loading';
17 |
18 | export { default as NavBar } from '@/nav-bar';
19 | export type { NavBarProps } from '@/nav-bar';
20 |
21 | export { default as Card } from '@/card';
22 | export type { CardProps } from '@/card';
23 |
24 | export { default as Image } from '@/image';
25 | export type { ImageProps } from '@/image';
26 |
27 | export { default as Countdown } from '@/countdown';
28 | export type { CountdownProps } from '@/countdown';
29 |
30 | export { default as PullToRefresh } from '@/pull-to-refresh';
31 | export type { PullToRefreshProps } from '@/pull-to-refresh';
32 |
33 | export { default as Space } from '@/space';
34 | export type { SpaceProps } from '@/space';
35 |
36 | export { default as Tabs } from '@/tabs';
37 | export type { TabsProps, TabProps } from '@/tabs';
38 |
39 | export { default as Swiper } from '@/swiper';
40 | export type { SwiperProps, SwiperItemProps, SwiperRef } from '@/swiper';
41 |
42 | export { default as Grid } from '@/grid';
43 | export type { GridProps, GridItemProps } from '@/grid';
44 |
45 | export type { ErrorBlockProps } from '@/error-block';
46 | export { default as ErrorBlock } from '@/error-block';
47 |
48 | export type { InputProps, InputRef } from '@/input';
49 | export { default as Input } from '@/input';
50 |
51 | export type { SidebarProps } from '@/sidebar';
52 | export { default as Sidebar } from '@/sidebar';
53 |
54 | export type { SearchBarProps, SearchBarRef } from '@/search-bar';
55 | export { default as SearchBar } from '@/search-bar';
56 |
57 | export type { ActionSheetProps, Action } from '@/action-sheet';
58 | export { default as ActionSheet } from '@/action-sheet';
59 |
60 | export type { InfiniteScrollProps } from '@/infinite-scroll';
61 | export { default as InfiniteScroll } from '@/infinite-scroll';
62 |
63 | export type { CellGroupProps, CellProps } from '@/cell';
64 | export { default as Cell } from '@/cell';
65 |
66 | export type { EllipsisProps } from '@/ellipsis';
67 | export { default as Ellipsis } from '@/ellipsis';
68 |
69 | export type { SelectorProps } from '@/selector';
70 | export { default as Selector } from '@/selector';
71 |
72 | export type { SliderProps, SliderRef } from '@/slider';
73 | export { default as Slider } from '@/slider';
74 |
75 | export type { DialogProps, DialogAlertProps } from '@/dialog';
76 | export { default as Dialog } from '@/dialog';
77 |
78 | export type { DividerProps } from '@/divider';
79 | export { default as Divider } from '@/divider';
80 |
81 | export { default as hooks } from '@/hooks';
82 |
--------------------------------------------------------------------------------
/packages/infinite-scroll/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Loading from '@/spinner-loading';
3 |
4 | import useIntersectionObserver from '@/hooks/useIntersectionObserver';
5 | import useLockFn from '@/hooks/useLockFn';
6 |
7 | import './styles/index.scss';
8 |
9 | export interface InfiniteScrollProps {
10 | /** 是否加载更多 */
11 | hasMore: boolean;
12 | /** 加载数据方法 */
13 | loadMore: () => Promise;
14 | /** 自定义底部样式 */
15 | footer?: React.ReactNode;
16 | children: React.ReactNode;
17 | }
18 |
19 | const classPrefix = `ygm-infinite-scroll`;
20 |
21 | const InfiniteScroll: React.FC = React.memo((props) => {
22 | const doLoadMore = useLockFn(() => props.loadMore());
23 |
24 | const intersectionEleRef = React.useRef(null);
25 |
26 | const observerEntry = useIntersectionObserver(intersectionEleRef, {});
27 |
28 | const check = React.useCallback(async () => {
29 | if (!observerEntry?.isIntersecting) return;
30 | if (!props.hasMore) return;
31 |
32 | await doLoadMore();
33 | }, [doLoadMore, observerEntry?.isIntersecting, props.hasMore]);
34 |
35 | React.useEffect(() => {
36 | check();
37 | }, [check]);
38 |
39 | return (
40 |
41 | {props.children}
42 |
43 |
44 | {props.footer && props.footer}
45 | {!props.footer && (props.hasMore ? : '')}
46 |
47 |
48 | );
49 | });
50 |
51 | InfiniteScroll.displayName = 'InfiniteScroll';
52 |
53 | export default InfiniteScroll;
54 |
--------------------------------------------------------------------------------
/packages/infinite-scroll/styles/index.scss:
--------------------------------------------------------------------------------
1 | .ygm-infinite-scroll {
2 | position: 'relative';
3 | &-load {
4 | display: flex;
5 | justify-content: center;
6 | align-items: center;
7 | padding: 10px;
8 | font-size: var(--ygm-font-size-m);
9 | color: var(--ygm-color-weak);
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/packages/input/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import cx from 'classnames';
3 |
4 | import { CloseCircleFill } from 'antd-mobile-icons';
5 |
6 | import './styles/index.scss';
7 |
8 | type TStyle = Partial>;
9 |
10 | export interface InputRef {
11 | clear: () => void;
12 | focus: () => void;
13 | blur: () => void;
14 | setValue: (val: string) => void;
15 | }
16 |
17 | export interface InputProps {
18 | id?: string;
19 | value?: string;
20 | placeholder?: string;
21 | className?: string;
22 | /** 是否显示清除icon */
23 | clearable?: boolean;
24 | style?: React.CSSProperties & TStyle;
25 | autoFocus?: boolean;
26 | disabled?: boolean;
27 | readOnly?: boolean;
28 | maxLength?: number;
29 | minLength?: number;
30 | max?: number;
31 | min?: number;
32 | pattern?: string;
33 | name?: string;
34 | autoComplete?: 'on' | 'off';
35 | autoCapitalize?: 'on' | 'off';
36 | autoCorrect?: 'on' | 'off';
37 | inputMode?: 'none' | 'text' | 'decimal' | 'numeric' | 'tel' | 'search' | 'email' | 'url';
38 | type?: React.HTMLInputTypeAttribute;
39 | onKeyDown?: React.KeyboardEventHandler;
40 | onKeyUp?: React.KeyboardEventHandler;
41 | onCompositionStart?: React.CompositionEventHandler;
42 | onCompositionEnd?: React.CompositionEventHandler;
43 | onClick?: React.MouseEventHandler;
44 | onEnterPress?: (e: React.KeyboardEvent) => void;
45 | onChange?: (val: string) => void;
46 | onClear?: () => void;
47 | onFocus?: (e: React.FocusEvent) => void;
48 | onBlur?: (e: React.FocusEvent) => void;
49 | }
50 |
51 | const classPrefix = `ygm-input`;
52 |
53 | const Input = React.forwardRef((props, ref) => {
54 | const [value, setValue] = React.useState(props.value!);
55 | const nativeInputRef = React.useRef(null);
56 |
57 | React.useImperativeHandle(ref, () => ({
58 | clear: () => {
59 | setValue('');
60 | },
61 | focus: () => {
62 | nativeInputRef.current?.focus();
63 | },
64 | blur: () => {
65 | nativeInputRef.current?.blur();
66 | },
67 | setValue: (val: string) => {
68 | setValue(val);
69 | },
70 | }));
71 |
72 | const handleKeydown = React.useCallback(
73 | (e: React.KeyboardEvent) => {
74 | if (props.onEnterPress && e.code === 'Enter') {
75 | props.onEnterPress(e);
76 | }
77 | props.onKeyDown?.(e);
78 | },
79 | [props]
80 | );
81 |
82 | const showClearable = React.useMemo(() => {
83 | if (!props.clearable || !value || props.readOnly) return false;
84 | return true;
85 | }, [props.clearable, props.readOnly, value]);
86 |
87 | return (
88 |
89 |
{
113 | setValue(e.target.value);
114 | props.onChange?.(e.target.value);
115 | }}
116 | onFocus={props.onFocus}
117 | onBlur={props.onBlur}
118 | />
119 |
120 | {showClearable && (
121 |
{
124 | e.preventDefault();
125 | }}
126 | onClick={() => {
127 | setValue('');
128 | props.onClear?.();
129 | }}
130 | >
131 |
132 |
133 | )}
134 |
135 | );
136 | });
137 |
138 | Input.defaultProps = {
139 | autoComplete: 'off',
140 | autoCapitalize: 'off',
141 | autoCorrect: 'off',
142 | value: '',
143 | id: 'ygm-input',
144 | type: 'text',
145 | };
146 |
147 | Input.displayName = 'Input';
148 |
149 | export default Input;
150 |
--------------------------------------------------------------------------------
/packages/input/styles/index.scss:
--------------------------------------------------------------------------------
1 | $class-prefix-input: 'ygm-input';
2 |
3 | .#{$class-prefix-input} {
4 | --color: var(--ygm-color-text);
5 | --placeholder-color: var(--ygm-color-light);
6 |
7 | --text-align: left;
8 | --background-color: transparent;
9 | --font-size: var(--ygm-font-size-m);
10 |
11 | display: flex;
12 | justify-content: flex-start;
13 | align-items: center;
14 |
15 | width: 100%;
16 | min-height: 24px;
17 | background-color: var(--background-color);
18 |
19 | &-disabled {
20 | opacity: 0.4;
21 | }
22 |
23 | &-element {
24 | flex: auto;
25 | display: inline-block;
26 | box-sizing: border-box;
27 | width: 100%;
28 | max-width: 100%;
29 | max-height: 100%;
30 | padding: 0;
31 | margin: 0;
32 | color: var(--color);
33 | font-size: var(--font-size);
34 | line-height: 1.5;
35 | background: transparent;
36 | border: 0;
37 | outline: none;
38 | appearance: none;
39 | text-align: var(--text-align);
40 |
41 | &::placeholder {
42 | color: var(--placeholder-color);
43 | font-family: inherit;
44 | }
45 |
46 | &:-webkit-autofill {
47 | background-color: transparent;
48 | }
49 |
50 | &:read-only {
51 | cursor: default;
52 | }
53 |
54 | &:invalid {
55 | box-shadow: none;
56 | }
57 |
58 | &::-ms-clear {
59 | display: none;
60 | }
61 | &::-webkit-search-cancel-button {
62 | display: none;
63 | }
64 | &::-webkit-search-decoration {
65 | display: none;
66 | }
67 | &:disabled {
68 | opacity: 1;
69 | }
70 |
71 | // for ios
72 | &[type='date'],
73 | &[type='time'],
74 | &[type='datetime-local'] {
75 | min-height: 1.5em;
76 | }
77 |
78 | // for safari
79 | &[type='search'] {
80 | -webkit-appearance: none;
81 | }
82 |
83 | &[readonly] {
84 | pointer-events: none;
85 | }
86 | }
87 |
88 | &-clear {
89 | flex: none;
90 | margin-left: 8px;
91 | color: var(--ygm-color-light);
92 | &:active {
93 | color: var(--ygm-color-weak);
94 | }
95 | padding: 4px;
96 | cursor: pointer;
97 | .antd-mobile-icon {
98 | display: block;
99 | font-size: var(--ygm-font-size-l);
100 | }
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/packages/mask/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useSpring, animated } from '@react-spring/web';
3 |
4 | import useIsomorphicLayoutEffect from '@/hooks/useIsomorphicLayoutEffect';
5 | import useScrollLock from '@/hooks/useScrollLock';
6 |
7 | import './styles/index.scss';
8 |
9 | export interface MaskProps {
10 | /** 是否可见 */
11 | visible: boolean;
12 | /** 点击蒙层触发回调 */
13 | onMaskClick?: (event: React.MouseEvent) => void;
14 | style?: React.CSSProperties & Partial>;
15 | }
16 |
17 | const classPrefix = 'ygm-mask';
18 |
19 | const Mask: React.FC = (props) => {
20 | const [active, setActive] = React.useState(props.visible);
21 |
22 | useScrollLock(props.visible);
23 |
24 | const onMask = React.useCallback(
25 | (e: React.MouseEvent) => {
26 | e.stopPropagation();
27 | props.onMaskClick?.(e);
28 | },
29 | [props.onMaskClick]
30 | );
31 |
32 | const { opacity } = useSpring({
33 | opacity: props.visible ? 1 : 0,
34 | config: {
35 | tension: 250,
36 | friction: 30,
37 | clamp: true,
38 | },
39 | onRest: () => {
40 | setActive(props.visible);
41 | },
42 | });
43 |
44 | useIsomorphicLayoutEffect(() => {
45 | if (props.visible) {
46 | setActive(true);
47 | }
48 | }, [props.visible]);
49 |
50 | return (
51 |
56 | );
57 | };
58 |
59 | export default Mask;
60 |
61 | Mask.displayName = 'Mask';
62 |
--------------------------------------------------------------------------------
/packages/mask/styles/index.scss:
--------------------------------------------------------------------------------
1 | $class-prefix-mask: 'ygm-mask';
2 |
3 | .#{$class-prefix-mask} {
4 | --z-index: 999;
5 | --background: rgba(0, 0, 0, 0.55);
6 |
7 | position: fixed;
8 | top: 0;
9 | left: 0;
10 | z-index: var(--z-index);
11 | width: 100%;
12 | height: 100%;
13 | display: block;
14 | background: var(--background);
15 | overflow: hidden;
16 | }
17 |
--------------------------------------------------------------------------------
/packages/nav-bar/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { LeftOutline } from 'antd-mobile-icons';
4 |
5 | import './styles/index.scss';
6 |
7 | export interface NavBarProps {
8 | /** 点击返回区域后的回调 */
9 | onBack?: () => void;
10 | /** 右侧内容 */
11 | right?: React.ReactNode;
12 | /** 中间内容 */
13 | children?: React.ReactNode;
14 | /** 是否显示返回区域的箭头 */
15 | leftArrow?: boolean;
16 | /** 返回区域文字 */
17 | leftText?: string;
18 | /** 样式 */
19 | style?: React.CSSProperties & Partial>;
20 | }
21 |
22 | const classPrefix = 'ygm-nav-bar';
23 |
24 | const NavBar: React.FC = (props) => {
25 | return (
26 |
27 |
28 | {props.leftArrow && (
29 |
30 |
31 |
32 | )}
33 |
{props.leftText}
34 |
35 |
{props.children}
36 |
{props.right}
37 |
38 | );
39 | };
40 |
41 | NavBar.defaultProps = {
42 | leftText: '',
43 | leftArrow: true,
44 | };
45 |
46 | NavBar.displayName = 'NavBar';
47 |
48 | export default NavBar;
49 |
--------------------------------------------------------------------------------
/packages/nav-bar/styles/index.scss:
--------------------------------------------------------------------------------
1 | $class-prefix-nav-bar: 'ygm-nav-bar';
2 |
3 | .#{$class-prefix-nav-bar} {
4 | --nav-bar-height: 45px;
5 | --border-bottom: none;
6 |
7 | height: var(--nav-bar-height);
8 | border-bottom: var(--border-bottom);
9 | padding: 0 var(--ygm-padding-l);
10 | white-space: nowrap;
11 | display: flex;
12 | align-items: center;
13 | background-color: var(--ygm-color-background);
14 | box-sizing: border-box;
15 |
16 | &-left,
17 | &-right {
18 | flex: 1;
19 | }
20 |
21 | &-title {
22 | flex: auto;
23 | text-align: center;
24 | overflow: hidden;
25 | text-overflow: ellipsis;
26 | font-size: var(--ygm-font-size-l);
27 | }
28 |
29 | &-left {
30 | font-size: var(--ygm-font-size-m);
31 | display: flex;
32 | justify-content: flex-start;
33 | align-items: center;
34 |
35 | &-icon {
36 | font-size: var(--ygm-font-size-xl);
37 | margin-right: 4px;
38 | }
39 | }
40 |
41 | &-right {
42 | text-align: right;
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/packages/popup/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import cx from 'classnames';
3 |
4 | // 它基于弹簧物理原理实现,他的核心理念就是
5 | // 使我们元素的动画轨迹和真实世界更接近
6 | import { useSpring, animated } from '@react-spring/web';
7 |
8 | import useIsomorphicLayoutEffect from '@/hooks/useIsomorphicLayoutEffect';
9 | import useScrollLock from '@/hooks/useScrollLock';
10 |
11 | import Mask from '@/mask';
12 |
13 | import './styles/index.scss';
14 |
15 | export interface PopupProps {
16 | /** 指定弹出的位置 */
17 | position?: 'left' | 'top' | 'bottom' | 'right';
18 | /** 内容区域style属性 */
19 | style?: React.CSSProperties;
20 | /** 内容区域类名 */
21 | className?: string;
22 | /** 是否可见 */
23 | visible: boolean;
24 | children?: React.ReactNode;
25 | /** 是否展示蒙层 */
26 | mask?: boolean;
27 | /** 点击蒙层回调 */
28 | onMaskClick?: (event: React.MouseEvent) => void;
29 | /** 显示后回调 */
30 | afterShow?: () => void;
31 | /** 关闭后回调 */
32 | afterClose?: () => void;
33 | }
34 |
35 | const classPrefix = 'ygm-popup';
36 |
37 | const Popup: React.FC = (props) => {
38 | const [active, setActive] = React.useState(props.visible);
39 |
40 | useScrollLock(props.visible);
41 |
42 | const { percent } = useSpring({
43 | percent: props.visible ? 0 : 100,
44 | config: {
45 | // 精确度
46 | precision: 0.1,
47 | // 弹簧质量,mass的值越大,动画执行的速度也会随着执行的时间变得越变越快
48 | mass: 0.4,
49 | // 弹簧张力
50 | tension: 300,
51 | // 表示摩擦力和阻力
52 | friction: 30,
53 | },
54 | onRest: () => {
55 | setActive(props.visible);
56 | if (props.visible) {
57 | props.afterClose?.();
58 | } else {
59 | props.afterClose?.();
60 | }
61 | },
62 | });
63 |
64 | useIsomorphicLayoutEffect(() => {
65 | if (props.visible) {
66 | setActive(true);
67 | }
68 | }, [props.visible]);
69 |
70 | return (
71 |
72 | {props.mask &&
}
73 |
74 |
{
79 | if (props.position === 'bottom') {
80 | return `translate(0, ${v}%)`;
81 | }
82 | if (props.position === 'left') {
83 | return `translate(-${v}%, 0)`;
84 | }
85 | if (props.position === 'right') {
86 | return `translate(${v}%, 0)`;
87 | }
88 | if (props.position === 'top') {
89 | return `translate(0, -${v}%)`;
90 | }
91 | return 'none';
92 | }),
93 | }}
94 | >
95 | {props.children}
96 |
97 |
98 | );
99 | };
100 |
101 | Popup.defaultProps = {
102 | visible: false,
103 | position: 'left',
104 | mask: true,
105 | };
106 |
107 | export default Popup;
108 |
109 | Popup.displayName = 'Popup';
110 |
--------------------------------------------------------------------------------
/packages/popup/styles/index.scss:
--------------------------------------------------------------------------------
1 | .ygm-popup {
2 | --z-index: 1000;
3 |
4 | z-index: var(--z-index);
5 | position: fixed;
6 | background-color: var(--ygm-color-background);
7 |
8 | &-body {
9 | position: fixed;
10 | background-color: var(--ygm-color-white);
11 | z-index: calc(var(--z-index) + 10);
12 | }
13 |
14 | &-left {
15 | height: 100%;
16 | top: 0;
17 | left: 0;
18 | }
19 |
20 | &-bottom {
21 | width: 100%;
22 | bottom: 0;
23 | left: 0;
24 | }
25 | &-top {
26 | width: 100%;
27 | top: 0;
28 | left: 0;
29 | }
30 |
31 | &-right {
32 | height: 100%;
33 | top: 0;
34 | right: 0;
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/packages/pull-to-refresh/constants.ts:
--------------------------------------------------------------------------------
1 | import { TPullStatus, TPullKey } from './types';
2 |
3 | export const PULL_STATUS: Record = {
4 | PULLING: 'pulling',
5 | CAN_RELEASE: 'canRelease',
6 | REFRESHING: 'refreshing',
7 | COMPLETE: 'complete',
8 | };
9 |
10 | export const DEFUALT_DURATION = 300;
11 |
12 | export const FRICTION = 0.3;
13 |
--------------------------------------------------------------------------------
/packages/pull-to-refresh/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import SpinnerLoading from '@/spinner-loading';
3 |
4 | import { getScrollParent, getScrollTop, sleep } from './utils';
5 | import { TPullStatus } from './types';
6 | import { PULL_STATUS, DEFUALT_DURATION, FRICTION } from './constants';
7 |
8 | import './styles/index.scss';
9 |
10 | export interface PullToRefreshProps {
11 | children: React.ReactNode;
12 | pullingText?: React.ReactNode;
13 | canReleaseText?: React.ReactNode;
14 | refreshingText?: React.ReactNode;
15 | completeText?: React.ReactNode;
16 | headHeight?: number;
17 | threshold?: number;
18 | completeDelay?: number;
19 | onRefresh: () => Promise;
20 | }
21 |
22 | const PullToRefresh: React.FC = React.memo((props) => {
23 | const [status, setStatus] = React.useState(PULL_STATUS.PULLING);
24 | const [pullDistance, setPullDistance] = React.useState(0);
25 | const [duration, setDuration] = React.useState(DEFUALT_DURATION);
26 |
27 | const containerRef = React.useRef(null);
28 | const touchStartY = React.useRef(0);
29 | const isDragging = React.useRef(false);
30 |
31 | const trackStyle = React.useMemo(() => {
32 | return {
33 | transitionDuration: `${duration}ms`,
34 | transform: `translate3d(0,${pullDistance}px,0)`,
35 | };
36 | }, [duration, pullDistance]);
37 |
38 | const isTouchable = React.useMemo(() => {
39 | return status !== PULL_STATUS.REFRESHING && status !== PULL_STATUS.COMPLETE;
40 | }, [status]);
41 |
42 | const renderStatusText = React.useCallback(() => {
43 | if (status === PULL_STATUS.PULLING) return props.pullingText;
44 | else if (status === PULL_STATUS.CAN_RELEASE) return props.canReleaseText;
45 | else if (status === PULL_STATUS.REFRESHING) return props.refreshingText;
46 | return props.completeText;
47 | }, [props.canReleaseText, props.completeText, props.pullingText, props.refreshingText, status]);
48 |
49 | const onTouchEnd = React.useCallback(async () => {
50 | if (!isDragging.current && !isTouchable) return;
51 | isDragging.current = false;
52 | setDuration(DEFUALT_DURATION);
53 | if (status === PULL_STATUS.CAN_RELEASE) {
54 | setStatus(PULL_STATUS.REFRESHING);
55 | setPullDistance(props.headHeight!);
56 | try {
57 | await props.onRefresh();
58 | setStatus(PULL_STATUS.COMPLETE);
59 | } catch (e) {
60 | setPullDistance(0);
61 | setStatus(PULL_STATUS.PULLING);
62 | throw e;
63 | }
64 |
65 | if (props.completeDelay! > 0) {
66 | await sleep(props.completeDelay!);
67 | }
68 | }
69 | setPullDistance(0);
70 | setStatus(PULL_STATUS.PULLING);
71 | }, [isTouchable, status, props]);
72 |
73 | const onTouchMove = React.useCallback(
74 | (e: TouchEvent) => {
75 | if (!isDragging.current && !isTouchable) return;
76 | const currentY = e.changedTouches[0].clientY;
77 | const diff = (currentY - touchStartY.current) * FRICTION;
78 |
79 | if (diff <= 0) return;
80 |
81 | if (diff > props.threshold!) {
82 | setStatus(PULL_STATUS.CAN_RELEASE);
83 | } else {
84 | setStatus(PULL_STATUS.PULLING);
85 | }
86 | setPullDistance(diff);
87 | },
88 | [isTouchable, props.threshold]
89 | );
90 |
91 | const onTouchStart = React.useCallback(
92 | (e: TouchEvent) => {
93 | if (!isTouchable) return;
94 | const scrollParent = getScrollParent(e.target as Element);
95 |
96 | const scrollTop = getScrollTop(scrollParent as Element);
97 | if (scrollTop === 0) {
98 | setDuration(0);
99 | touchStartY.current = e.changedTouches[0].clientY;
100 | isDragging.current = true;
101 | }
102 | },
103 | [isTouchable]
104 | );
105 |
106 | React.useEffect(() => {
107 | const element = containerRef.current;
108 | if (!element) return;
109 |
110 | element.addEventListener('touchstart', onTouchStart);
111 | element.addEventListener('touchmove', onTouchMove);
112 | element.addEventListener('touchend', onTouchEnd);
113 |
114 | return () => {
115 | element.removeEventListener('touchstart', onTouchStart);
116 | element.removeEventListener('touchmove', onTouchMove);
117 | element.removeEventListener('touchend', onTouchEnd);
118 | };
119 | }, [onTouchEnd, onTouchMove, onTouchStart]);
120 |
121 | return (
122 |
123 |
124 |
125 | {renderStatusText()}
126 |
127 |
{props.children}
128 |
129 |
130 | );
131 | });
132 |
133 | PullToRefresh.defaultProps = {
134 | pullingText: '下拉刷新',
135 | canReleaseText: '释放立即刷新',
136 | refreshingText: ,
137 | completeText: '刷新成功',
138 | headHeight: 30,
139 | threshold: 50,
140 | completeDelay: 500,
141 | };
142 |
143 | PullToRefresh.displayName = 'PullToRefresh';
144 |
145 | export default PullToRefresh;
146 |
--------------------------------------------------------------------------------
/packages/pull-to-refresh/styles/index.scss:
--------------------------------------------------------------------------------
1 | .ygm-pull-to-refresh {
2 | overflow: hidden;
3 | user-select: none;
4 | &-head {
5 | position: relative;
6 | height: 100%;
7 | transition-property: transform;
8 |
9 | &-content {
10 | font-size: var(--ygm-font-size-s);
11 | overflow: hidden;
12 | position: absolute;
13 | left: 0;
14 | width: 100%;
15 | color: var(--ygm-color-weak);
16 | display: flex;
17 | justify-content: center;
18 | align-items: center;
19 | transform: translateY(-100%);
20 | }
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/packages/pull-to-refresh/types.ts:
--------------------------------------------------------------------------------
1 | export type TPullStatus = 'pulling' | 'canRelease' | 'refreshing' | 'complete';
2 | export type TPullKey = 'PULLING' | 'CAN_RELEASE' | 'REFRESHING' | 'COMPLETE';
3 |
--------------------------------------------------------------------------------
/packages/pull-to-refresh/utils.ts:
--------------------------------------------------------------------------------
1 | const inBrowser = typeof window !== 'undefined';
2 |
3 | const defaultRoot = inBrowser ? window : undefined;
4 |
5 | type ScrollElement = HTMLElement | Window;
6 |
7 | const overflowStylePatterns = ['scroll', 'auto'];
8 |
9 | const isElement = (node: Element) => {
10 | const ELEMENT_NODE_TYPE = 1;
11 | return node.tagName !== 'HTML' && node.tagName !== 'BODY' && node.nodeType === ELEMENT_NODE_TYPE;
12 | };
13 |
14 | export const getScrollParent = (el: Element, root: ScrollElement | undefined = defaultRoot) => {
15 | let node = el;
16 |
17 | while (node && node !== root && isElement(node)) {
18 | const { overflowY } = window.getComputedStyle(node);
19 | if (overflowStylePatterns.includes(overflowY) && node.scrollHeight > node.clientHeight) {
20 | return node;
21 | }
22 | node = node.parentNode as Element;
23 | }
24 |
25 | return root;
26 | };
27 |
28 | export const getScrollTop = (element: Window | Element) => {
29 | const top = 'scrollTop' in element ? element.scrollTop : element.scrollY;
30 |
31 | // iOS scroll bounce cause minus scrollTop
32 | return Math.max(top, 0);
33 | };
34 |
35 | export const sleep = (time: number) =>
36 | new Promise((resolve) => {
37 | window.setTimeout(resolve, time);
38 | });
39 |
--------------------------------------------------------------------------------
/packages/search-bar/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Input, { InputRef } from '@/input';
3 |
4 | import { SearchOutline } from 'antd-mobile-icons';
5 |
6 | import './styles/index.scss';
7 |
8 | const classPrefix = `ygm-search-bar`;
9 |
10 | type TStyle = Partial<
11 | Record<'--color' | '--background' | '--search-background' | '--border-radius' | '--placeholder-color', string>
12 | >;
13 |
14 | export type SearchBarRef = InputRef;
15 |
16 | export interface SearchBarProps {
17 | /** 输入内容 */
18 | value?: string;
19 | /** 提示文本 */
20 | placeholder?: string;
21 | /** 搜索框前缀图标 */
22 | icon?: React.ReactNode;
23 | /** 输入的最大字符数 */
24 | maxLength?: number;
25 | /** 是否显示清除图标,可点击清除文本框 */
26 | clearable?: boolean;
27 | /** 禁止输入 */
28 | disabled?: boolean;
29 | style?: React.CSSProperties & TStyle;
30 | /** 取消按钮文案 */
31 | cancelText?: string;
32 | /** 是否显示取消按钮 */
33 | showCancel?: boolean;
34 | /** 点击取消按钮时触发事件 */
35 | onCancel?: () => void;
36 | /** 输入框回车键触发事件 */
37 | onSearch?: (val: string) => void;
38 | /** 输入框内容变化时触发事件 */
39 | onChange?: (val: string) => void;
40 | /** 点击清除图标时触发事件 */
41 | onClear?: () => void;
42 | }
43 |
44 | const SearchBar = React.forwardRef((props, ref) => {
45 | const [value, setValue] = React.useState(props.value!);
46 | const composingRef = React.useRef(false);
47 | const inputRef = React.useRef(null);
48 |
49 | React.useImperativeHandle(ref, () => ({
50 | clear: () => inputRef.current?.clear(),
51 | focus: () => inputRef.current?.focus(),
52 | blur: () => inputRef.current?.blur(),
53 | setValue: (val: string) => inputRef.current?.setValue(val),
54 | }));
55 |
56 | const onChange = (value: string) => {
57 | setValue(value);
58 | props.onChange?.(value);
59 | };
60 |
61 | const onEnterPress = () => {
62 | // 在拼音输入法输入汉字时,避免enter键的搜索触发
63 | if (!composingRef.current) {
64 | inputRef.current?.blur();
65 | props.onSearch?.(value);
66 | }
67 | };
68 |
69 | return (
70 |
71 |
72 |
{props.icon}
73 |
{
88 | composingRef.current = true;
89 | }}
90 | onCompositionEnd={() => {
91 | composingRef.current = false;
92 | }}
93 | />
94 |
95 | {props.showCancel && (
96 |
97 | {props.cancelText}
98 |
99 | )}
100 |
101 | );
102 | });
103 |
104 | SearchBar.defaultProps = {
105 | value: '',
106 | icon: ,
107 | clearable: true,
108 | cancelText: '取消',
109 | };
110 |
111 | SearchBar.displayName = 'SearchBar';
112 |
113 | export default SearchBar;
114 |
--------------------------------------------------------------------------------
/packages/search-bar/styles/index.scss:
--------------------------------------------------------------------------------
1 | $class-prefix-search-bar: 'ygm-search-bar';
2 |
3 | .#{$class-prefix-search-bar} {
4 | --height: 32px;
5 | --padding-left: var(--ygm-padding-s);
6 | --background: var(--ygm-color-background);
7 | --search-background: var(--ygm-color-box);
8 | --border-radius: var(--ygm-radius-xs);
9 |
10 | display: flex;
11 | align-items: center;
12 | justify-content: center;
13 | box-sizing: border-box;
14 | padding: 10px 16px;
15 | background-color: var(--background);
16 |
17 | &-content {
18 | align-items: center;
19 | justify-content: center;
20 | display: flex;
21 | flex: 1;
22 | padding: 5px 12px 5px;
23 | background-color: var(--search-background);
24 | border-radius: var(--border-radius);
25 |
26 | &-icon {
27 | flex: none;
28 | color: var(--ygm-color-weak);
29 | font-size: var(--ygm-font-size-l);
30 | margin-right: 8px;
31 | }
32 |
33 | &-input {
34 | flex: 1;
35 | }
36 |
37 | &-cancel {
38 | padding-left: var(--ygm-padding-s);
39 | color: var(--ygm-color-text);
40 | font-size: var(--ygm-font-size-m);
41 | line-height: 34px;
42 | cursor: pointer;
43 | user-select: none;
44 | }
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/packages/selector/CheckMark.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const CheckMark = React.memo(() => {
4 | return (
5 |
18 | );
19 | });
20 |
21 | export default CheckMark;
22 |
--------------------------------------------------------------------------------
/packages/selector/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import cx from 'classnames';
3 |
4 | import Space from '@/space';
5 | import Grid from '@/grid';
6 |
7 | import CheckMark from '@/selector/CheckMark';
8 |
9 | import './styles/index.scss';
10 |
11 | export interface SelectorOption {
12 | label: React.ReactNode;
13 | value: V;
14 | description?: React.ReactNode;
15 | disabled?: boolean;
16 | }
17 |
18 | export interface SelectorProps {
19 | options: SelectorOption[];
20 | value?: V[];
21 | /** 布局列数 */
22 | columns?: number;
23 | /** label间距 */
24 | gap?: number | string | [number | string, number | string];
25 | /** 是否支持多选 */
26 | multiple?: boolean;
27 | showCheckMark?: boolean;
28 | onChange?: (v: V[], items: SelectorOption[]) => void;
29 | /** 自定义样式 */
30 | style?: React.CSSProperties &
31 | Partial<
32 | Record<
33 | | '--color'
34 | | '--checked-color'
35 | | '--text-color'
36 | | '--checked-text-color'
37 | | '--border'
38 | | '--checked-border'
39 | | '--border-radius'
40 | | '--padding',
41 | string
42 | >
43 | >;
44 | }
45 |
46 | type SelectorValue = string | number;
47 |
48 | const classPrefix = `ygm-selector`;
49 |
50 | const Selector = (props: SelectorProps) => {
51 | const [value, setValue] = React.useState(props.value!);
52 |
53 | const items = props.options.map((option) => {
54 | const active = value.includes(option.value);
55 | const disabled = !!option.disabled;
56 | const className = cx(`${classPrefix}-item`, {
57 | [`${classPrefix}-item-active`]: active,
58 | [`${classPrefix}-item-disabled`]: disabled,
59 | });
60 |
61 | const onClick = () => {
62 | if (disabled) return;
63 | if (props.multiple) {
64 | const val = active ? value.filter((v) => v !== option.value) : [...value, option.value];
65 | const selectorOptions = props.options.filter((option) => val.includes(option.value));
66 | setValue(val);
67 | props.onChange?.(val, selectorOptions);
68 | } else {
69 | const val = active ? [] : [option.value];
70 | setValue(val);
71 | props.onChange?.(val, [option]);
72 | }
73 | };
74 |
75 | return (
76 |
77 | {option.label}
78 | {option.description &&
{option.description}
}
79 |
80 | {active && props.showCheckMark && (
81 |
82 |
83 |
84 | )}
85 |
86 | );
87 | });
88 |
89 | return (
90 |
91 | {!props.columns ? (
92 |
93 | {items}
94 |
95 | ) : (
96 |
97 | {items}
98 |
99 | )}
100 |
101 | );
102 | };
103 |
104 | Selector.displayName = 'Selector';
105 |
106 | Selector.defaultProps = {
107 | value: [],
108 | gap: 8,
109 | showCheckMark: true,
110 | };
111 | export default Selector;
112 |
--------------------------------------------------------------------------------
/packages/selector/styles/index.scss:
--------------------------------------------------------------------------------
1 | $class-prefix-selector: 'ygm-selector';
2 |
3 | .#{$class-prefix-selector} {
4 | --color: #f5f5f5;
5 | --checked-color: #e7f1ff;
6 | --text-color: var(--ygm-color-text);
7 | --checked-text-color: var(--ygm-color-primary);
8 | --padding: 8px 16px;
9 | --border-radius: 2px;
10 | --border: none;
11 | --checked-border: none;
12 |
13 | overflow: hidden;
14 | font-size: var(--ygm-font-size-m);
15 | user-select: none;
16 | line-height: 1.4;
17 |
18 | &-item {
19 | position: relative;
20 | padding: var(--padding);
21 | background-color: var(--color);
22 | border: var(--border);
23 | border-radius: var(--border-radius);
24 | color: var(--text-color);
25 | display: inline-block;
26 | text-align: center;
27 | overflow: hidden;
28 | vertical-align: top;
29 | cursor: pointer;
30 | opacity: 1;
31 |
32 | &-active {
33 | color: var(--checked-text-color);
34 | background-color: var(--checked-color);
35 | border: var(--checked-border);
36 | }
37 |
38 | &-disabled {
39 | cursor: not-allowed;
40 | opacity: 0.4;
41 | }
42 |
43 | &-description {
44 | font-size: var(--ygm-font-size-s);
45 | color: var(--ygm-color-weak);
46 | }
47 |
48 | .#{$class-prefix-selector}-check-mark {
49 | position: absolute;
50 | right: 0;
51 | bottom: 0;
52 | width: 0;
53 | height: 0;
54 | border-top: solid 8px transparent;
55 | border-bottom: solid 8px var(--ygm-color-primary);
56 | border-left: solid 10px transparent;
57 | border-right: solid 10px var(--ygm-color-primary);
58 |
59 | > svg {
60 | position: absolute;
61 | left: 0;
62 | top: 0;
63 | height: 6px;
64 | width: 8px;
65 | }
66 | }
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/packages/sidebar/index.tsx:
--------------------------------------------------------------------------------
1 | import InternalSidebar from '@/sidebar/sidebar';
2 | import SidebarItem from '@/sidebar/sidebar-item';
3 |
4 | export type { SidebarProps } from '@/sidebar/sidebar';
5 | export type { SidebarItemProps } from '@/sidebar/sidebar-item';
6 |
7 | type InternalSidebarType = typeof InternalSidebar;
8 |
9 | export interface SidebarInterface extends InternalSidebarType {
10 | Item: typeof SidebarItem;
11 | }
12 |
13 | const Sidebar = InternalSidebar as SidebarInterface;
14 |
15 | Sidebar.Item = SidebarItem;
16 |
17 | export default Sidebar;
18 |
--------------------------------------------------------------------------------
/packages/sidebar/sidebar-item.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export interface SidebarItemProps {
4 | key: string;
5 | title: React.ReactNode;
6 | children: React.ReactNode;
7 | }
8 |
9 | const SidebarItem: React.FC = (props) => {
10 | return props.children ? (props.children as React.ReactElement) : null;
11 | };
12 |
13 | SidebarItem.displayName = 'SidebarItem';
14 |
15 | export default SidebarItem;
16 |
--------------------------------------------------------------------------------
/packages/sidebar/sidebar.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import cx from 'classnames';
3 |
4 | import SidebarItem from '@/sidebar/sidebar-item';
5 | import { traverseReactNode } from '@/utils/traverse-react-node';
6 |
7 | import './styles/index.scss';
8 |
9 | export interface SidebarProps {
10 | /** 当前激活side item面板的key */
11 | activeKey: string;
12 | /** 点击side item切换后回调 */
13 | onChange?: (key: string) => void;
14 | children?: React.ReactNode;
15 | /** 基本样式 */
16 | style?: React.CSSProperties &
17 | Partial<
18 | Record<'--width' | '--height' | '--background-color' | '--content-padding' | '--sidebar-item-padding', string>
19 | >;
20 | }
21 |
22 | const classPrefix = `ygm-sidebar`;
23 |
24 | const Sidebar: React.FC = React.memo((props) => {
25 | const [activeKey, setActiveKey] = React.useState(props.activeKey);
26 |
27 | const items: React.ReactElement>[] = [];
28 |
29 | traverseReactNode(props.children, (child) => {
30 | if (!React.isValidElement(child)) return;
31 | if (!child.key) return;
32 | items.push(child);
33 | });
34 |
35 | const onSetActive = (e: React.MouseEvent) => {
36 | const key = (e.target as HTMLElement).dataset['key'];
37 | setActiveKey(key as string);
38 | props.onChange?.(key as string);
39 | };
40 |
41 | return (
42 |
43 |
44 | {items.map((item) => {
45 | const active = item.key === activeKey;
46 | return (
47 |
55 |
56 | {item.props.title}
57 |
58 |
59 | );
60 | })}
61 |
62 |
63 |
64 | {items.map((item) => (
65 |
70 | {item.props.children}
71 |
72 | ))}
73 |
74 |
75 | );
76 | });
77 |
78 | Sidebar.displayName = 'Sidebar';
79 |
80 | export default Sidebar;
81 |
--------------------------------------------------------------------------------
/packages/sidebar/styles/index.scss:
--------------------------------------------------------------------------------
1 | $class-prefix-sidebar: 'ygm-sidebar';
2 |
3 | .#{$class-prefix-sidebar} {
4 | --height: 100%;
5 | --width: 80px;
6 | --content-padding: 0;
7 | --sidebar-item-padding: 16px 5px;
8 | --item-border-radius: var(--ygm-radius-m);
9 | --background-color: var(--ygm-color-box);
10 |
11 | height: var(--height);
12 | font-size: var(--ygm-font-size-m);
13 | display: flex;
14 |
15 | &-items {
16 | width: var(--width);
17 | box-sizing: border-box;
18 | overflow-y: auto;
19 | background-color: var(--background-color);
20 | }
21 |
22 | &-content {
23 | flex: 1;
24 | overflow: hidden;
25 | overflow-y: auto;
26 | background-color: var(--ygm-color-white);
27 | padding: var(--content-padding);
28 | }
29 |
30 | &-item {
31 | width: 100%;
32 | box-sizing: border-box;
33 | padding: var(--sidebar-item-padding);
34 | text-align: center;
35 | position: relative;
36 | cursor: pointer;
37 |
38 | &-active {
39 | background-color: var(--ygm-color-white);
40 | position: relative;
41 | }
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/packages/slider/index.tsx:
--------------------------------------------------------------------------------
1 | import Slider from '@/slider/slider';
2 |
3 | export type { SliderProps, SliderRef } from '@/slider/slider';
4 |
5 | export default Slider;
6 |
--------------------------------------------------------------------------------
/packages/slider/slider.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import cx from 'classnames';
3 |
4 | import Thumb from '@/slider/thumb';
5 |
6 | import { getValueByScope } from '@/utils/utils';
7 |
8 | import './styles/slider.scss';
9 |
10 | export interface SliderRef {
11 | setValue: (value: number) => void;
12 | }
13 |
14 | export interface SliderProps {
15 | min?: number;
16 | max?: number;
17 | value?: number;
18 | step?: number;
19 | disabled?: boolean;
20 | onChange?: (value: number) => void;
21 | onChangeAfter?: (value: number) => void;
22 | style?: React.CSSProperties &
23 | Partial>;
24 | }
25 |
26 | const classPrefix = 'ygm-slider';
27 |
28 | const Slider = React.forwardRef((props, ref) => {
29 | const [sliderValue, setSliderValue] = React.useState(getValueByScope(props.value!, props.min!, props.max!));
30 |
31 | const trackRef = React.useRef(null);
32 |
33 | React.useImperativeHandle(ref, () => ({
34 | setValue: (val: number) => {
35 | setSliderValue(getValueByScope(val, props.min!, props.max!));
36 | },
37 | }));
38 |
39 | // 滚动条值范围
40 | const scope = props.max! - props.min!;
41 | // 计算滚动的百分比
42 | const fillSize = `${((sliderValue - props.min!) * 100) / scope}%`;
43 |
44 | const getValueByPosition = (position: number) => {
45 | const newPosition = getValueByScope(position, props.min!, props.max!);
46 | // 除以step得到可以移动的步数取整,再乘以步数得到真实的value值
47 | const value = Math.round(newPosition / props.step!) * props.step!;
48 |
49 | return value;
50 | };
51 |
52 | const onTrack = (e: React.MouseEvent) => {
53 | e.stopPropagation();
54 | const track = trackRef.current;
55 | if (props.disabled || !track) return;
56 |
57 | const rect = track.getBoundingClientRect();
58 | // 滚动条总长度
59 | const sliderWidth = rect.width;
60 | // 滚动条跟视口的距离
61 | const sliderOffsetLeft = rect.left;
62 | // 滚动距离
63 | const delta = e.clientX - sliderOffsetLeft;
64 | // 占总长度百分比 * 范围长度得到真实的position值
65 | const position = props.min! + (delta / sliderWidth) * scope;
66 |
67 | const targetValue = getValueByPosition(position);
68 |
69 | setSliderValue(targetValue);
70 | props.onChangeAfter?.(targetValue);
71 | };
72 |
73 | const onDrag = (position: number) => {
74 | const targetValue = getValueByPosition(position);
75 | setSliderValue(targetValue);
76 | props.onChange?.(targetValue);
77 | };
78 |
79 | const onEnd = (position: number) => {
80 | const targetValue = getValueByPosition(position);
81 | props.onChangeAfter?.(targetValue);
82 | };
83 |
84 | return (
85 |
107 | );
108 | });
109 |
110 | Slider.defaultProps = {
111 | min: 0,
112 | max: 100,
113 | step: 1,
114 | disabled: false,
115 | value: 0,
116 | };
117 |
118 | Slider.displayName = 'Slider';
119 |
120 | export default Slider;
121 |
--------------------------------------------------------------------------------
/packages/slider/styles/slider.scss:
--------------------------------------------------------------------------------
1 | $class-prefix-slider: 'ygm-slider';
2 |
3 | .#{$class-prefix-slider} {
4 | --slider-bar-fill-color: var(--ygm-color-primary);
5 | --slider-bar-height: 2px;
6 | --slider-background-color: #ebedf0;
7 | --slider-border-radius: var(--ygm-radius-xs);
8 |
9 | position: relative;
10 | width: 100%;
11 | height: var(--slider-bar-height);
12 | background: var(--slider-background-color);
13 | border-radius: var(--slider-border-radius);
14 |
15 | &::before {
16 | position: absolute;
17 | top: calc(var(--ygm-padding-s) * -1);
18 | right: 0;
19 | bottom: calc(var(--ygm-padding-s) * -1);
20 | left: 0;
21 | content: '';
22 | cursor: grab;
23 | }
24 |
25 | &-fill {
26 | position: absolute;
27 | left: 0;
28 | z-index: 1;
29 | height: var(--slider-bar-height);
30 | border-radius: var(--slider-border-radius);
31 | background-color: var(--slider-bar-fill-color);
32 | }
33 |
34 | &-disabled {
35 | opacity: 0.4;
36 |
37 | .#{$class-prefix-slider}-thumb {
38 | cursor: not-allowed;
39 | }
40 | &.#{$class-prefix-slider}::before {
41 | cursor: not-allowed;
42 | }
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/packages/slider/styles/thumb.scss:
--------------------------------------------------------------------------------
1 | $class-prefix-slider-thumb: 'ygm-slider-thumb';
2 |
3 | .#{$class-prefix-slider-thumb} {
4 | touch-action: none;
5 | position: absolute;
6 | z-index: 2;
7 | border-radius: 50%;
8 | top: 50%;
9 | transform: translate(-50%, -50%);
10 | cursor: grab;
11 |
12 | &-button {
13 | width: 24px;
14 | height: 24px;
15 | background: var(--ygm-color-background);
16 | border-radius: 50%;
17 | box-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/packages/slider/thumb.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import './styles/thumb.scss';
4 |
5 | interface ThumbProps {
6 | value: number;
7 | min: number;
8 | max: number;
9 | disabled: boolean;
10 | trackRef: React.RefObject;
11 | onDrag: (value: number) => void;
12 | onChangeAfter: (value: number) => void;
13 | }
14 |
15 | const classPrefix = 'ygm-slider-thumb';
16 |
17 | const Thumb: React.FC = (props) => {
18 | const prevValue = React.useRef(0);
19 |
20 | const startX = React.useRef(0);
21 | const endX = React.useRef(0);
22 |
23 | const currentPosition = `${((props.value - props.min) / (props.max - props.min)) * 100}%`;
24 |
25 | const onTouchStart = (e: React.TouchEvent) => {
26 | if (props.disabled) return;
27 |
28 | prevValue.current = props.value;
29 | startX.current = e.touches[0].clientX;
30 | };
31 |
32 | const onTouchMove = (e: React.TouchEvent) => {
33 | const trackElement = props.trackRef.current;
34 | if (!trackElement || props.disabled) return;
35 |
36 | const deltaX = e.touches[0].clientX - startX.current;
37 | const total = trackElement.offsetWidth;
38 |
39 | // 移动距离:总长度 = 移动的实际距离 :实际距离
40 | const position = (deltaX / total) * (props.max - props.min);
41 | const finalPosition = position + prevValue.current;
42 | endX.current = finalPosition;
43 | props.onDrag(finalPosition);
44 | };
45 |
46 | const onTouchEnd = () => {
47 | props?.onChangeAfter(endX.current);
48 | };
49 |
50 | return (
51 |
64 | );
65 | };
66 |
67 | Thumb.displayName = 'Thumb';
68 |
69 | export default Thumb;
70 |
--------------------------------------------------------------------------------
/packages/space/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import cx from 'classnames';
3 |
4 | import './styles/index.scss';
5 |
6 | export interface SpaceProps {
7 | /** 间距方向 */
8 | direction?: 'horizontal' | 'vertical';
9 | /** 交叉轴对齐方式 */
10 | align?: 'start' | 'end' | 'center' | 'baseline';
11 | /** 主轴对齐方式 */
12 | justify?: 'start' | 'end' | 'center' | 'between' | 'around' | 'evenly' | 'stretch';
13 | /** 是否自动换行,仅在 horizontal 时有效 */
14 | wrap?: boolean;
15 | /** 是否渲染为块级元素 */
16 | block?: boolean;
17 | /** 间距大小,设为数组时则分别设置垂直方向和水平方向的间距大小 */
18 | gap?: number | string | [number | string, number | string];
19 | /** 元素点击事件 */
20 | onClick?: (event: React.MouseEvent) => void;
21 | children?: React.ReactNode;
22 | }
23 |
24 | const classPrefix = `ygm-space`;
25 |
26 | const formatGap = (gap: string | number) => (typeof gap === 'number' ? `${gap}px` : gap);
27 |
28 | const Space: React.FC = (props) => {
29 | const style = React.useMemo(() => {
30 | if (props.gap) {
31 | if (Array.isArray(props.gap)) {
32 | const [gapH, gapV] = props.gap;
33 | return {
34 | '--gap-vertical': formatGap(gapV),
35 | '--gap-horizontal': formatGap(gapH),
36 | };
37 | }
38 | return { '--gap': formatGap(props.gap) };
39 | }
40 | return {};
41 | }, [props.gap]);
42 |
43 | return (
44 |
55 | {React.Children.map(props.children, (child) => {
56 | return child !== null && child !== undefined &&
{child}
;
57 | })}
58 |
59 | );
60 | };
61 |
62 | Space.defaultProps = {
63 | direction: 'horizontal',
64 | block: true,
65 | };
66 |
67 | Space.displayName = 'Space';
68 |
69 | export default Space;
70 |
--------------------------------------------------------------------------------
/packages/space/styles/index.scss:
--------------------------------------------------------------------------------
1 | $class-prefix-space: 'ygm-space';
2 |
3 | .#{$class-prefix-space} {
4 | --gap: 8px;
5 | --gap-vertical: var(--gap);
6 | --gap-horizontal: var(--gap);
7 |
8 | display: inline-flex;
9 |
10 | &-horizontal {
11 | flex-direction: row;
12 |
13 | > .#{$class-prefix-space}-item {
14 | margin-right: var(--gap-horizontal);
15 |
16 | &:last-child {
17 | margin-right: 0;
18 | }
19 | }
20 | &.#{$class-prefix-space}-wrap {
21 | flex-wrap: wrap;
22 | margin-bottom: calc(var(--gap-vertical) * -1);
23 | > .#{$class-prefix-space}-item {
24 | padding-bottom: var(--gap-vertical);
25 | }
26 | }
27 | }
28 |
29 | &-vertical {
30 | flex-direction: column;
31 |
32 | > .#{$class-prefix-space}-item {
33 | margin-bottom: var(--gap-vertical);
34 |
35 | &:last-child {
36 | margin-bottom: 0;
37 | }
38 | }
39 | }
40 |
41 | &.#{$class-prefix-space}-block {
42 | display: flex;
43 | }
44 |
45 | &-align {
46 | &-center {
47 | align-items: center;
48 | }
49 | &-start {
50 | align-items: flex-start;
51 | }
52 | &-end {
53 | align-items: flex-end;
54 | }
55 | &-baseline {
56 | align-items: baseline;
57 | }
58 | }
59 |
60 | &-justify {
61 | &-center {
62 | justify-content: center;
63 | }
64 | &-start {
65 | justify-content: flex-start;
66 | }
67 | &-end {
68 | justify-content: flex-end;
69 | }
70 | &-between {
71 | justify-content: space-between;
72 | }
73 | &-around {
74 | justify-content: space-around;
75 | }
76 | &-evenly {
77 | justify-content: space-evenly;
78 | }
79 | &-stretch {
80 | justify-content: stretch;
81 | }
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/packages/spinner-loading/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import cx from 'classnames';
3 |
4 | import './styles/index.scss';
5 |
6 | export interface SpinnerLoadingProps {
7 | type?: 'spinner';
8 | color?: 'default' | 'primary' | 'white' | string;
9 | size?: number;
10 | }
11 |
12 | const colorRecord: Record = {
13 | default: true,
14 | primary: true,
15 | white: true,
16 | };
17 |
18 | const SpinnerLoading: React.FC = React.memo((props) => {
19 | return (
20 |
26 | );
27 | });
28 |
29 | SpinnerLoading.defaultProps = {
30 | color: 'default',
31 | size: 32,
32 | type: 'spinner',
33 | };
34 |
35 | export default SpinnerLoading;
36 |
37 | SpinnerLoading.displayName = 'SpinnerLoading';
38 |
--------------------------------------------------------------------------------
/packages/spinner-loading/styles/index.scss:
--------------------------------------------------------------------------------
1 | .ygm-spinner {
2 | &-loading {
3 | border-top: 1px solid;
4 | border-right: 1px solid rgba(0, 0, 0, 0);
5 | border-bottom: 1px solid rgba(0, 0, 0, 0);
6 | border-left: 1px solid;
7 | border-radius: 50%;
8 | z-index: 1001;
9 | animation: spinner 0.8s infinite linear;
10 | }
11 |
12 | &-loading-color-default {
13 | border-top-color: var(--ygm-color-weak);
14 | border-left-color: var(--ygm-color-weak);
15 | }
16 |
17 | &-loading-color-primary {
18 | border-top-color: var(--ygm-color-primary);
19 | border-left-color: var(--ygm-color-primary);
20 | }
21 |
22 | &-loading-color-white {
23 | border-top-color: var(--ygm-color-white);
24 | border-left-color: var(--ygm-color-white);
25 | }
26 | }
27 |
28 | @keyframes spinner {
29 | 0% {
30 | transform: rotate(0deg);
31 | }
32 | 100% {
33 | transform: rotate(360deg);
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/packages/styles/base.scss:
--------------------------------------------------------------------------------
1 | @import './variable.scss';
2 |
3 | body {
4 | color: var(--ygm-color-text);
5 | font-size: var(--ygm-font-size-m);
6 | font-family: var(--ygm-font-family);
7 | }
8 |
9 | a,
10 | button {
11 | cursor: pointer;
12 | }
13 |
14 | a {
15 | color: var(--ygm-color-primary);
16 | transition: opacity ease-in-out 0.2s;
17 | }
18 | a:active {
19 | opacity: 0.8;
20 | }
21 |
--------------------------------------------------------------------------------
/packages/styles/index.scss:
--------------------------------------------------------------------------------
1 | @import 'reset';
2 | @import 'base';
3 |
--------------------------------------------------------------------------------
/packages/styles/reset.scss:
--------------------------------------------------------------------------------
1 | html,
2 | body,
3 | div,
4 | span,
5 | applet,
6 | object,
7 | iframe,
8 | h1,
9 | h2,
10 | h3,
11 | h4,
12 | h5,
13 | h6,
14 | p,
15 | blockquote,
16 | pre,
17 | a,
18 | abbr,
19 | acronym,
20 | address,
21 | big,
22 | cite,
23 | code,
24 | del,
25 | dfn,
26 | em,
27 | img,
28 | ins,
29 | kbd,
30 | q,
31 | s,
32 | samp,
33 | small,
34 | strike,
35 | strong,
36 | sub,
37 | sup,
38 | tt,
39 | var,
40 | b,
41 | u,
42 | i,
43 | center,
44 | dl,
45 | dt,
46 | dd,
47 | ol,
48 | ul,
49 | li,
50 | fieldset,
51 | form,
52 | label,
53 | legend,
54 | table,
55 | caption,
56 | tbody,
57 | tfoot,
58 | thead,
59 | tr,
60 | th,
61 | td,
62 | article,
63 | aside,
64 | canvas,
65 | details,
66 | embed,
67 | figure,
68 | figcaption,
69 | footer,
70 | header,
71 | hgroup,
72 | menu,
73 | nav,
74 | output,
75 | ruby,
76 | section,
77 | summary,
78 | time,
79 | mark,
80 | audio,
81 | video {
82 | margin: 0;
83 | padding: 0;
84 | border: 0;
85 | font-size: 100%;
86 | font: inherit;
87 | vertical-align: baseline;
88 | }
89 | /* HTML5 display-role reset for older browsers */
90 | article,
91 | aside,
92 | details,
93 | figcaption,
94 | figure,
95 | footer,
96 | header,
97 | hgroup,
98 | menu,
99 | nav,
100 | section {
101 | display: block;
102 | }
103 | body {
104 | line-height: 1;
105 | }
106 | ol,
107 | ul {
108 | list-style: none;
109 | }
110 | blockquote,
111 | q {
112 | quotes: none;
113 | }
114 | blockquote:before,
115 | blockquote:after,
116 | q:before,
117 | q:after {
118 | content: '';
119 | content: none;
120 | }
121 | table {
122 | border-collapse: collapse;
123 | border-spacing: 0;
124 | }
125 |
--------------------------------------------------------------------------------
/packages/styles/variable.scss:
--------------------------------------------------------------------------------
1 | :root {
2 | // Color
3 | --ygm-color-primary: #1989fa;
4 | --ygm-color-success: #00b578;
5 | --ygm-color-warning: #ff8f1f;
6 | --ygm-color-danger: #ff3141;
7 |
8 | --ygm-color-white: #ffffff;
9 | --ygm-color-text: #333333;
10 | --ygm-color-weak: #999999;
11 | --ygm-color-light: #cccccc;
12 | --ygm-color-border: #eeeeee;
13 | --ygm-color-background: #ffffff;
14 | --ygm-color-box: #f5f5f5;
15 |
16 | // Padding
17 | --ygm-padding-xs: 4px;
18 | --ygm-padding-s: 8px;
19 | --ygm-padding-m: 12px;
20 | --ygm-padding-l: 16px;
21 | --ygm-padding-xl: 20px;
22 | --ygm-padding-xxl: 24px;
23 |
24 | // Border-radius
25 | --ygm-radius-xs: 4px;
26 | --ygm-radius-s: 6px;
27 | --ygm-radius-m: 8px;
28 | --ygm-radius-l: 10px;
29 | --ygm-radius-xl: 12px;
30 | --ygm-radius-xxl: 14px;
31 | --ygm-radius-xxxl: 16px;
32 |
33 | // Font
34 | --ygm-font-size-xs: 10px;
35 | --ygm-font-size-s: 12px;
36 | --ygm-font-size-m: 14px;
37 | --ygm-font-size-l: 16px;
38 | --ygm-font-size-xl: 18px;
39 | --ygm-font-size-xxl: 20px;
40 | --ygm-font-size-xxxl: 22px;
41 |
42 | --ygm-font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', Helvetica, Segoe UI, Arial, Roboto,
43 | 'PingFang SC', 'miui', 'Hiragino Sans GB', 'Microsoft Yahei', sans-serif;
44 | }
45 |
--------------------------------------------------------------------------------
/packages/swiper/index.tsx:
--------------------------------------------------------------------------------
1 | import InternalSwiper from './swiper';
2 | import SwiperItem from './swiper-item';
3 |
4 | export type { SwiperProps, SwiperRef } from './swiper';
5 | export type { SwiperItemProps } from './swiper-item';
6 |
7 | type InternalSwiperType = typeof InternalSwiper;
8 |
9 | export interface SwiperInterface extends InternalSwiperType {
10 | Item: typeof SwiperItem;
11 | }
12 |
13 | const Swiper = InternalSwiper as SwiperInterface;
14 |
15 | Swiper.Item = SwiperItem;
16 |
17 | export default Swiper;
18 |
--------------------------------------------------------------------------------
/packages/swiper/styles/swiper-item.scss:
--------------------------------------------------------------------------------
1 | .ygm-swiper-item {
2 | display: block;
3 | width: 100%;
4 | height: 100%;
5 | white-space: normal;
6 | }
7 |
--------------------------------------------------------------------------------
/packages/swiper/styles/swiper-page-indicator.scss:
--------------------------------------------------------------------------------
1 | .ygm-swiper-page-indicator {
2 | display: flex;
3 | width: auto;
4 |
5 | &-dot {
6 | width: 5px;
7 | height: 5px;
8 | border-radius: 50%;
9 | background-color: var(--ygm-color-weak);
10 | margin-right: 5px;
11 |
12 | &:last-child {
13 | margin-right: 0;
14 | }
15 |
16 | &-active {
17 | width: 13px;
18 | height: 5px;
19 | border-radius: 2px;
20 | background-color: var(--ygm-color-primary);
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/packages/swiper/styles/swiper.scss:
--------------------------------------------------------------------------------
1 | .ygm-swiper {
2 | --height: auto;
3 | --width: 100%;
4 | --border-radius: 0;
5 | --track-padding: 0;
6 |
7 | width: var(--width);
8 | height: var(--height);
9 | position: relative;
10 | touch-action: pan-y;
11 | border-radius: var(--border-radius);
12 | overflow: hidden;
13 | z-index: 0;
14 |
15 | &-track {
16 | width: 100%;
17 | height: 100%;
18 | position: relative;
19 | flex-wrap: nowrap;
20 | display: flex;
21 | overflow: hidden;
22 | box-sizing: border-box;
23 | padding: var(--track-padding);
24 |
25 | &-inner {
26 | width: 100%;
27 | height: 100%;
28 | position: relative;
29 | flex-wrap: nowrap;
30 | display: flex;
31 | overflow: hidden;
32 | }
33 | }
34 |
35 | &-slide {
36 | width: 100%;
37 | position: relative;
38 | display: block;
39 | flex-shrink: 0;
40 | white-space: unset;
41 | }
42 |
43 | &-indicator {
44 | position: absolute;
45 | bottom: 6px;
46 | left: 50%;
47 | transform: translateX(-50%);
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/packages/swiper/swiper-item.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import './styles/swiper-item.scss';
4 |
5 | export interface SwiperItemProps {
6 | onClick?: (e: React.MouseEvent) => void;
7 | children?: React.ReactNode;
8 | }
9 |
10 | const SwiperItem: React.FC = React.memo((props) => {
11 | return (
12 |
13 | {props.children}
14 |
15 | );
16 | });
17 |
18 | SwiperItem.displayName = 'SwiperItem';
19 |
20 | export default SwiperItem;
21 |
--------------------------------------------------------------------------------
/packages/swiper/swiper-page-indicator.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import cx from 'classnames';
3 |
4 | import './styles/swiper-page-indicator.scss';
5 |
6 | export interface SwiperPageIndicatorProps {
7 | current: number;
8 | total: number;
9 | indicatorClassName?: string;
10 | }
11 |
12 | const classPrefix = 'ygm-swiper-page-indicator';
13 |
14 | const SwiperPageIndicator: React.FC = React.memo((props) => {
15 | const dots: React.ReactElement[] = React.useMemo(() => {
16 | return Array(props.total)
17 | .fill(0)
18 | .map((_, index) => (
19 |
25 | ));
26 | }, [props]);
27 |
28 | return {dots}
;
29 | });
30 |
31 | SwiperPageIndicator.displayName = 'SwiperPageIndicator';
32 |
33 | export default SwiperPageIndicator;
34 |
--------------------------------------------------------------------------------
/packages/swiper/utils.ts:
--------------------------------------------------------------------------------
1 | export const modulus = (value: number, division: number) => {
2 | const remainder = value % division;
3 | return remainder < 0 ? remainder + division : remainder;
4 | };
5 |
--------------------------------------------------------------------------------
/packages/tabs/index.tsx:
--------------------------------------------------------------------------------
1 | import InternalTabs from '@/tabs/tabs';
2 | import Tab from '@/tabs/tab';
3 |
4 | export type { TabsProps } from '@/tabs/tabs';
5 | export type { TabProps } from '@/tabs/tab';
6 |
7 | type InternalTabsType = typeof InternalTabs;
8 |
9 | export interface TabsInterface extends InternalTabsType {
10 | Tab: typeof Tab;
11 | }
12 |
13 | const Tabs = InternalTabs as TabsInterface;
14 |
15 | Tabs.Tab = Tab;
16 |
17 | export default Tabs;
18 |
--------------------------------------------------------------------------------
/packages/tabs/styles/index.scss:
--------------------------------------------------------------------------------
1 | $class-prefix-tabs: 'ygm-tabs';
2 |
3 | .#{$class-prefix-tabs} {
4 | position: relative;
5 | user-select: none;
6 |
7 | &-tab-list {
8 | display: flex;
9 | flex-wrap: nowrap;
10 | justify-content: flex-start;
11 | align-items: center;
12 | position: relative;
13 | overflow-x: scroll;
14 | scrollbar-width: none;
15 | &::-webkit-scrollbar {
16 | display: none;
17 | }
18 |
19 | &-card {
20 | .#{$class-prefix-tabs}-tab-active {
21 | background: var(--ygm-color-primary);
22 |
23 | .#{$class-prefix-tabs}-tab-title {
24 | color: var(--ygm-color-white);
25 | }
26 | }
27 | }
28 |
29 | &-card {
30 | background-color: var(--ygm-color-border);
31 | .#{$class-prefix-tabs}-tab-title {
32 | color: var(--ygm-color-weak);
33 | }
34 | }
35 | }
36 |
37 | &-tab {
38 | flex: auto;
39 | padding: 0 12px;
40 | box-sizing: border-box;
41 |
42 | &-title {
43 | margin: auto;
44 | font-size: var(--ygm-font-size-m);
45 | white-space: nowrap;
46 | width: min-content;
47 | position: relative;
48 | padding: 8px 0 10px;
49 | }
50 |
51 | &-active {
52 | .#{$class-prefix-tabs}-tab-title {
53 | color: var(--ygm-color-primary);
54 | }
55 | }
56 | }
57 |
58 | &-tab-line {
59 | position: absolute;
60 | bottom: 0;
61 | height: 2px;
62 | background: var(--ygm-color-primary);
63 | border-radius: 2px;
64 | }
65 |
66 | &-content {
67 | padding: var(--ygm-padding-m);
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/packages/tabs/tab.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export interface TabProps {
4 | key: string;
5 | title: string;
6 | children?: React.ReactNode;
7 | }
8 |
9 | const Tab: React.FC = (props) => {
10 | return props.children ? (props.children as React.ReactElement) : null;
11 | };
12 |
13 | Tab.displayName = 'Tab';
14 |
15 | export default Tab;
16 |
--------------------------------------------------------------------------------
/packages/tabs/tabs.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import cx from 'classnames';
3 |
4 | import Tab from '@/tabs/tab';
5 | import useIsomorphicLayoutEffect from '@/hooks/useIsomorphicLayoutEffect';
6 | import useUpdateIsomorphicLayoutEffect from '@/hooks/useUpdateIsomorphicLayoutEffect';
7 |
8 | import { traverseReactNode } from '@/utils/traverse-react-node';
9 |
10 | import './styles/index.scss';
11 |
12 | export interface TabsProps {
13 | /** 当前激活tab面板的key */
14 | activeKey: string;
15 | children?: React.ReactNode;
16 | /** 是否显示tab下划线 */
17 | showTabLine?: boolean;
18 | /** tab展示形式 */
19 | type?: 'line' | 'card';
20 | /** 点击tab切换后回调 */
21 | onChange?: (key: string) => void;
22 | /** 激活的tab样式 */
23 | tabActiveClassName?: string;
24 | /** tab列表样式 */
25 | tabListClassName?: string;
26 | /** tab内容样式 */
27 | tabContentClassName?: string;
28 | }
29 |
30 | const classPrefix = 'ygm-tabs';
31 |
32 | const Tabs: React.FC = (props) => {
33 | const [activeKey, setActiveKey] = React.useState(props.activeKey);
34 | const [activeLineStyle, setActiveLineStyle] = React.useState({
35 | width: 0,
36 | transform: `translate3d(0px, 0px, 0px)`,
37 | transitionDuration: '0',
38 | });
39 | const tabListRef = React.useRef(null);
40 |
41 | const keyToIndexRecord: Record = React.useMemo(() => ({}), []);
42 | const panes: React.ReactElement>[] = [];
43 |
44 | traverseReactNode(props.children, (child) => {
45 | if (!React.isValidElement(child)) return;
46 | if (!child.key) return;
47 | const length = panes.push(child);
48 | keyToIndexRecord[child.key] = length - 1;
49 | });
50 |
51 | const onTab = React.useCallback(
52 | (e: React.MouseEvent) => {
53 | const key = (e.target as HTMLElement).dataset['key'] as string;
54 | setActiveKey(key);
55 | props?.onChange?.(key);
56 | },
57 | [props?.onChange]
58 | );
59 |
60 | const calculateLineWidth = React.useCallback(
61 | (immediate = false) => {
62 | if (!props.showTabLine) return;
63 | const tabListEle = tabListRef.current;
64 | if (!tabListEle) return;
65 | const activeIndex = keyToIndexRecord[activeKey];
66 | const activeTabWrapper = tabListRef.current.children.item(activeIndex + 1) as HTMLDivElement;
67 | const activeTab = activeTabWrapper.children.item(0) as HTMLDivElement;
68 | const activeTabWidth = activeTab.offsetWidth;
69 | const activeTabLeft = activeTab.offsetLeft;
70 | const width = activeTabWidth;
71 | const x = activeTabLeft;
72 |
73 | setActiveLineStyle({
74 | width,
75 | transform: `translate3d(${x}px, 0px, 0px)`,
76 | transitionDuration: immediate ? '0ms' : '300ms',
77 | });
78 | },
79 | [activeKey, keyToIndexRecord, props.showTabLine]
80 | );
81 |
82 | useIsomorphicLayoutEffect(() => {
83 | calculateLineWidth(true);
84 | }, []);
85 |
86 | useUpdateIsomorphicLayoutEffect(() => {
87 | calculateLineWidth();
88 | }, [calculateLineWidth]);
89 |
90 | React.useEffect(() => {
91 | window.addEventListener('resize', () => calculateLineWidth(true));
92 |
93 | return () => window.removeEventListener('resize', () => calculateLineWidth(true));
94 | }, [calculateLineWidth]);
95 |
96 | return (
97 |
98 |
104 | {props.showTabLine && (
105 |
111 | )}
112 | {panes.map((item) => (
113 |
121 |
122 | {item.props.title}
123 |
124 |
125 | ))}
126 |
127 |
128 | {panes.map(
129 | (child) =>
130 | child.props.children && (
131 |
136 | {child}
137 |
138 | )
139 | )}
140 |
141 | );
142 | };
143 |
144 | Tabs.defaultProps = {
145 | showTabLine: true,
146 | type: 'line',
147 | };
148 |
149 | Tabs.displayName = 'Tabs';
150 |
151 | export default Tabs;
152 |
--------------------------------------------------------------------------------
/packages/toast/index.tsx:
--------------------------------------------------------------------------------
1 | export type { ToastShowProps } from './methods';
2 |
3 | import { show } from './methods';
4 |
5 | export interface ToastProps {
6 | show: typeof show;
7 | }
8 |
9 | const Toast = {
10 | show,
11 | };
12 |
13 | export default Toast;
14 |
--------------------------------------------------------------------------------
/packages/toast/methods.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom/client';
3 |
4 | import { default as Toast, ToastProps } from './toast';
5 |
6 | export type ToastShowProps = ToastProps;
7 |
8 | export const show = (p: ToastShowProps | string) => {
9 | const props = typeof p === 'string' ? { content: p } : p;
10 |
11 | const container = document.createElement('div');
12 | document.body.appendChild(container);
13 |
14 | const root = ReactDOM.createRoot(container);
15 |
16 | const unmount = () => {
17 | document.body.removeChild(container);
18 | root.unmount();
19 | };
20 |
21 | root.render();
22 | };
23 |
--------------------------------------------------------------------------------
/packages/toast/styles/index.scss:
--------------------------------------------------------------------------------
1 | .ygm-toast {
2 | position: fixed;
3 | top: 0;
4 | left: 0;
5 | width: 100%;
6 | height: 100%;
7 | user-select: none;
8 | z-index: 1001;
9 |
10 | &-main {
11 | display: inline-block;
12 | position: relative;
13 | top: 50%;
14 | left: 50%;
15 | transform: translate(-50%, -50%);
16 | width: auto;
17 | min-width: 96px;
18 | max-width: 70%;
19 | max-height: 70%;
20 | overflow: auto;
21 | color: var(--ygm-color-white);
22 | word-break: break-all;
23 | background-color: rgba(0, 0, 0, 0.7);
24 | border-radius: var(--ygm-radius-m);
25 | pointer-events: all;
26 | font-size: var(--ygm-font-size-m);
27 | line-height: 1.5;
28 | box-sizing: border-box;
29 |
30 | &-icon {
31 | padding: 30px 35px;
32 | }
33 |
34 | &-text {
35 | padding: var(--ygm-padding-m);
36 | }
37 | }
38 |
39 | &-text {
40 | text-align: center;
41 | }
42 |
43 | &-icon {
44 | text-align: center;
45 | margin-bottom: 8px;
46 | font-size: 36px;
47 | line-height: 1;
48 | display: flex;
49 | justify-content: center;
50 | align-items: center;
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/packages/toast/toast.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import cx from 'classnames';
3 | import { CheckOutline, CloseOutline } from 'antd-mobile-icons';
4 |
5 | import SpinnerLoading from '@/spinner-loading';
6 |
7 | import './styles/index.scss';
8 |
9 | export interface ToastProps {
10 | /** 提示持续时间 */
11 | duration?: number;
12 | /** Toast文本内容 */
13 | content: React.ReactNode;
14 | /** Toast关闭后的回调 */
15 | afterClose?: () => void;
16 | /** 卸载当前Toast的DOM */
17 | unmount?: () => void;
18 | /** Toast图标 */
19 | icon?: 'success' | 'fail' | 'loading' | React.ReactNode;
20 | }
21 |
22 | const classPrefix = 'ygm-toast';
23 |
24 | const Toast: React.FC = React.memo(({ icon, duration, content, afterClose, unmount }) => {
25 | const [_, setVisible] = React.useState(true);
26 |
27 | const iconElement = React.useMemo(() => {
28 | if (icon === null || icon === undefined) return null;
29 | switch (icon) {
30 | case 'success':
31 | return ;
32 | case 'fail':
33 | return ;
34 | case 'loading':
35 | return ;
36 | default:
37 | return icon;
38 | }
39 | }, [icon]);
40 |
41 | React.useEffect(() => {
42 | const timer = window.setTimeout(() => {
43 | setVisible(false);
44 | unmount?.();
45 | }, duration);
46 |
47 | return () => clearTimeout(timer);
48 | }, [duration, unmount]);
49 |
50 | React.useEffect(() => {
51 | return () => {
52 | afterClose?.();
53 | };
54 | }, [afterClose]);
55 |
56 | return (
57 |
58 |
59 | {iconElement &&
{iconElement}
}
60 |
{content}
61 |
62 |
63 | );
64 | });
65 |
66 | Toast.defaultProps = {
67 | duration: 2000,
68 | };
69 |
70 | Toast.displayName = 'Toast';
71 |
72 | export default Toast;
73 |
--------------------------------------------------------------------------------
/packages/utils/event.ts:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export const getTouchEventData = (
4 | e: TouchEvent | MouseEvent | React.TouchEvent | React.MouseEvent
5 | ) => {
6 | return 'changedTouches' in e ? e.changedTouches[0] : e;
7 | };
8 |
--------------------------------------------------------------------------------
/packages/utils/render-imperatively.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { renderToBody } from '@/utils/render';
3 |
4 | export interface ElementProps {
5 | visible?: boolean;
6 | onClose?: () => void;
7 | afterClose?: () => void;
8 | }
9 |
10 | const renderImperatively = (element: React.ReactElement) => {
11 | const Wraper = () => {
12 | const [visible, setVisible] = React.useState(false);
13 |
14 | const onClose = () => {
15 | element.props?.onClose?.();
16 | setVisible(false);
17 | };
18 |
19 | const afterClose = () => {
20 | unmount();
21 | };
22 |
23 | React.useEffect(() => {
24 | setVisible(true);
25 | }, []);
26 |
27 | return React.cloneElement(element, { ...element.props, visible, onClose, afterClose });
28 | };
29 |
30 | const unmount = renderToBody();
31 | };
32 |
33 | export default renderImperatively;
34 |
--------------------------------------------------------------------------------
/packages/utils/render.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom/client';
3 |
4 | export const render = (element: React.ReactElement, container: HTMLElement) => {
5 | const root = ReactDOM.createRoot(container);
6 | root.render(element);
7 |
8 | const unmount = () => {
9 | document.body.removeChild(container);
10 | root.unmount();
11 | };
12 |
13 | return unmount;
14 | };
15 |
16 | export const renderToBody = (element: React.ReactElement) => {
17 | const container = document.createElement('div');
18 | document.body.appendChild(container);
19 |
20 | const unmount = render(element, container);
21 |
22 | return unmount;
23 | };
24 |
--------------------------------------------------------------------------------
/packages/utils/scroll.ts:
--------------------------------------------------------------------------------
1 | const inBrowser = typeof window !== 'undefined';
2 |
3 | const defaultRoot = inBrowser ? window : undefined;
4 |
5 | type ScrollElement = HTMLElement | Window;
6 |
7 | const overflowStylePatterns = ['scroll', 'auto'];
8 |
9 | const isElement = (node: HTMLElement) => {
10 | const ELEMENT_NODE_TYPE = 1;
11 | return node.tagName !== 'HTML' && node.tagName !== 'BODY' && node.nodeType === ELEMENT_NODE_TYPE;
12 | };
13 |
14 | export const getScrollParent = (el: HTMLElement, root: ScrollElement | undefined = defaultRoot) => {
15 | let node = el;
16 |
17 | while (node && node !== root && isElement(node)) {
18 | const { overflowY } = window.getComputedStyle(node);
19 | if (overflowStylePatterns.includes(overflowY) && node.scrollHeight > node.clientHeight) {
20 | return node;
21 | }
22 | node = node.parentNode as HTMLElement;
23 | }
24 |
25 | return root;
26 | };
27 |
28 | export const getScrollTop = (element: Window | Element) => {
29 | const top = 'scrollTop' in element ? element.scrollTop : element.scrollY;
30 |
31 | // iOS scroll bounce cause minus scrollTop
32 | return Math.max(top, 0);
33 | };
34 |
35 | export const sleep = (time: number) =>
36 | new Promise((resolve) => {
37 | window.setTimeout(resolve, time);
38 | });
39 |
--------------------------------------------------------------------------------
/packages/utils/traverse-react-node.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { isFragment } from 'react-is';
3 |
4 | export const traverseReactNode = (children: React.ReactNode, fn: (child: React.ReactNode, index: number) => void) => {
5 | let i = 0;
6 | const handle = (target: React.ReactNode) => {
7 | React.Children.forEach(target, (child) => {
8 | if (!isFragment(child)) {
9 | fn(child, i);
10 | i++;
11 | } else {
12 | handle(child.props.children);
13 | }
14 | });
15 | };
16 |
17 | handle(children);
18 | };
19 |
--------------------------------------------------------------------------------
/packages/utils/utils.ts:
--------------------------------------------------------------------------------
1 | export const getValueByScope = (value: number, min: number, max: number): number => {
2 | let newValue = Math.max(value, min);
3 | newValue = Math.min(newValue, max);
4 | return newValue;
5 | };
6 |
--------------------------------------------------------------------------------
/packages/utils/validate.ts:
--------------------------------------------------------------------------------
1 | export function isPromise(obj: unknown): obj is Promise {
2 | return !!obj && typeof obj === 'object' && typeof (obj as any).then === 'function';
3 | }
4 |
--------------------------------------------------------------------------------
/scripts/build.js:
--------------------------------------------------------------------------------
1 | const log = require('./utils/log');
2 | const babel = require('./utils/babel');
3 |
4 | async function build() {
5 | const cwd = process.cwd();
6 |
7 | const bundleOpts = {
8 | entry: 'packages/index.tsx',
9 | };
10 |
11 | log('Build cjs with babel');
12 | await babel({ cwd, type: 'cjs', bundleOpts });
13 |
14 | log('Build esm with babel');
15 | await babel({ cwd, type: 'esm', importLibToEs: true, bundleOpts });
16 | }
17 |
18 | build();
19 |
--------------------------------------------------------------------------------
/scripts/utils/babel.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs');
2 | const path = require('path');
3 | const babel = require('@babel/core');
4 | const chalk = require('chalk');
5 | const gulpIf = require('gulp-if');
6 | const gulpAlias = require('gulp-ts-alias');
7 | const gulpTs = require('gulp-typescript');
8 | const gulpStyle = require('gulp-style-aliases');
9 | const gulpSass = require('gulp-sass')(require('sass'));
10 | const slash = require('slash2');
11 | const rimraf = require('rimraf');
12 | const through = require('through2');
13 | const vfs = require('vinyl-fs');
14 | const signale = require('signale');
15 | const getBabelConfig = require('./getBabelConfig');
16 | const log = require('./log');
17 |
18 | module.exports = function (opts) {
19 | const { cwd, type } = opts;
20 |
21 | const srcPath = path.join(cwd, 'packages');
22 | const targetDir = type === 'esm' ? 'es' : 'lib';
23 | const targetPath = path.join(cwd, targetDir);
24 |
25 | log(chalk.gray(`Clean ${targetDir} directory`));
26 | rimraf.sync(targetPath);
27 |
28 | const tsConfigPath = path.join(cwd, 'tsconfig.json');
29 |
30 | const tsConfig = JSON.parse(fs.readFileSync(tsConfigPath, 'utf-8')).compilerOptions || {};
31 | function isTsFile(path) {
32 | return /\.tsx?$/.test(path) && !path.endsWith('.d.ts');
33 | }
34 |
35 | function isStyleFile(path) {
36 | return /\._?(css|scss)?$/.test(path);
37 | }
38 |
39 | function isTransform(path) {
40 | const babelTransformRegexp = /\.(t|j)sx?$/;
41 | return babelTransformRegexp.test(path) && !path.endsWith('.d.ts');
42 | }
43 |
44 | function transform(opts) {
45 | const { file, type } = opts;
46 | const babelOptions = getBabelConfig(type);
47 | const relFile = slash(file.path).replace(`${cwd}/`, '');
48 | log(`Transform to ${type} for ${chalk['yellow'](relFile)}`);
49 |
50 | return babel.transform(file.contents, {
51 | ...babelOptions,
52 | filename: file.path,
53 | }).code;
54 | }
55 |
56 | function createStream(src) {
57 | return vfs
58 | .src(src, {
59 | allowEmpty: true,
60 | base: srcPath,
61 | })
62 | .pipe(gulpIf((f) => isTsFile(f.path), gulpAlias({ configuration: tsConfig })))
63 | .pipe(gulpIf((f) => isStyleFile(f.path), gulpAlias({ configuration: tsConfig })))
64 | .pipe(
65 | gulpIf(
66 | (f) => isStyleFile(f.path),
67 | gulpStyle({
68 | '@': 'packages',
69 | })
70 | )
71 | )
72 | .pipe(gulpIf((f) => isStyleFile(f.path), gulpSass()))
73 | .pipe(
74 | gulpIf(
75 | (f) => isTransform(f.path),
76 | through.obj((file, env, cb) => {
77 | try {
78 | file.contents = Buffer.from(
79 | transform({
80 | file,
81 | type,
82 | })
83 | );
84 | // .jsx -> .js
85 | file.path = file.path.replace(path.extname(file.path), '.js');
86 | cb(null, file);
87 | } catch (e) {
88 | signale.error(`Compiled faild: ${file.path}`);
89 | console.log(e);
90 | cb(null);
91 | }
92 | })
93 | )
94 | )
95 | .pipe(vfs.dest(targetPath));
96 | }
97 |
98 | function createTypeStream(src) {
99 | return vfs
100 | .src(src, {
101 | allowEmpty: true,
102 | base: srcPath,
103 | })
104 | .pipe(gulpIf((f) => isTsFile(f.path), gulpAlias({ configuration: tsConfig })))
105 | .pipe(gulpIf((f) => isTsFile(f.path), gulpTs({ ...tsConfig, files: [path.join(cwd, 'typings.d.ts')] })))
106 | .pipe(vfs.dest(targetPath));
107 | }
108 |
109 | const patterns = [path.join(srcPath, '**/*')];
110 |
111 | return new Promise((resolve) => {
112 | createTypeStream(patterns).on('end', resolve);
113 | }).then(
114 | () =>
115 | new Promise((resolve) => {
116 | createStream(patterns).on('end', resolve);
117 | })
118 | );
119 | };
120 |
--------------------------------------------------------------------------------
/scripts/utils/getBabelConfig.js:
--------------------------------------------------------------------------------
1 | function getBabelConfig(type) {
2 | return {
3 | presets: [
4 | [
5 | '@babel/preset-env',
6 | {
7 | modules: type === 'esm' ? false : 'commonjs',
8 | targets: {
9 | browsers: ['> 1%', 'last 2 versions', 'not dead'],
10 | },
11 | },
12 | ],
13 | '@babel/preset-typescript',
14 | '@babel/preset-react',
15 | ],
16 | plugins: [['@babel/plugin-transform-runtime', { corejs: 3 }]],
17 | };
18 | }
19 |
20 | module.exports = getBabelConfig;
21 |
--------------------------------------------------------------------------------
/scripts/utils/log.js:
--------------------------------------------------------------------------------
1 | const randomColor = require('./randomColor');
2 |
3 | module.exports = function (msg) {
4 | console.log(`${randomColor('@yg/react-mobile-ui')} ${msg}`);
5 | };
6 |
--------------------------------------------------------------------------------
/scripts/utils/randomColor.js:
--------------------------------------------------------------------------------
1 | const chalk = require('chalk');
2 |
3 | const colors = [
4 | 'red',
5 | 'green',
6 | 'yellow',
7 | 'blue',
8 | 'magenta',
9 | 'cyan',
10 | 'gray',
11 | 'redBright',
12 | 'greenBright',
13 | 'yellowBright',
14 | 'blueBright',
15 | 'magentaBright',
16 | 'cyanBright',
17 | ];
18 |
19 | let index = 0;
20 | const cache = {};
21 |
22 | module.exports = function (pkg) {
23 | if (!cache[pkg]) {
24 | const color = colors[index];
25 | let str = chalk[color].bold(pkg);
26 | cache[pkg] = str;
27 | if (index === colors.length - 1) {
28 | index = 0;
29 | } else {
30 | index += 1;
31 | }
32 | }
33 | return cache[pkg];
34 | };
35 |
--------------------------------------------------------------------------------
/stories/action-sheet/index.scss:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taoyage/react-mobile-ui/e0d17002078e43a0fdfc75407f78b3e937e4127d/stories/action-sheet/index.scss
--------------------------------------------------------------------------------
/stories/action-sheet/index.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { Meta } from '@storybook/react';
4 |
5 | import ActionSheet, { Action } from '@/action-sheet';
6 | import Button from '@/button';
7 | import Space from '@/space';
8 | import Toast from '@/toast';
9 |
10 | import DemoWrap from '../../demos/demo-wrap';
11 | import DemoBlock from '../../demos/demo-block';
12 |
13 | const ActionSheetStory: Meta = {
14 | title: '反馈/ActionSheet 动作面板',
15 | component: ActionSheet,
16 | };
17 |
18 | const actions: Action[] = [
19 | { name: '选项1', key: 'option1' },
20 | { name: '选项2', key: 'option2' },
21 | { name: '选项3', key: 'option3' },
22 | ];
23 |
24 | const actions1: Action[] = [
25 | { name: '选项一', key: 'option1' },
26 | { name: '选项二', key: 'option2' },
27 | { name: '选项三', description: '描述信息', key: 'option3' },
28 | ];
29 |
30 | const actions2: Action[] = [
31 | { name: '选项一', key: 'option1', color: '#ee0a24' },
32 | { name: '选项二', key: 'option2', disabled: true },
33 | { name: '选项三', description: '描述信息', key: 'option3' },
34 | ];
35 |
36 | export const Basic = () => {
37 | const [visible1, setVisible1] = React.useState(false);
38 | const [visible2, setVisible2] = React.useState(false);
39 | const [visible3, setVisible3] = React.useState(false);
40 | const [visible4, setVisible4] = React.useState(false);
41 | const [visible5, setVisible5] = React.useState(false);
42 |
43 | return (
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 | {/* 基本用法 */}
62 | setVisible1(false)} />
63 |
64 | {/* 展示取消按钮 */}
65 | setVisible2(false)} cancelText="取消" />
66 |
67 | {/* 展示描述信息 */}
68 | setVisible3(false)}
72 | cancelText="取消"
73 | description="这是一段描述信息"
74 | />
75 |
76 | {/* 选项状态 */}
77 | setVisible4(false)} cancelText="取消" />
78 |
79 | {/* 事件处理 */}
80 | {
84 | Toast.show(`点击了${action.name}`);
85 | }}
86 | onClose={() => {
87 | setVisible5(false);
88 | Toast.show('动作面板已关闭');
89 | }}
90 | cancelText="取消"
91 | />
92 |
93 | );
94 | };
95 |
96 | Basic.storyName = '基本用法';
97 |
98 | export default ActionSheetStory;
99 |
--------------------------------------------------------------------------------
/stories/button/index.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { Meta } from '@storybook/react';
4 |
5 | import Button from '@/button';
6 | import Space from '@/space';
7 |
8 | import DemoWrap from '../../demos/demo-wrap';
9 | import DemoBlock from '../../demos/demo-block';
10 |
11 | const ButtonStory: Meta = {
12 | title: '通用/Button 按钮',
13 | component: Button,
14 | };
15 |
16 | export const Basic = () => {
17 | return (
18 |
19 |
20 |
21 |
24 |
27 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
47 |
48 |
49 |
50 |
51 |
54 |
57 |
60 |
61 |
62 |
63 |
64 |
65 |
68 |
71 |
74 |
75 |
76 |
77 | );
78 | };
79 |
80 | Basic.storyName = '基本用法';
81 |
82 | export default ButtonStory;
83 |
--------------------------------------------------------------------------------
/stories/card/index.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { Meta } from '@storybook/react';
4 | import { RightOutline } from 'antd-mobile-icons';
5 |
6 | import Card from '@/card';
7 |
8 | import DemoWrap from '../../demos/demo-wrap';
9 | import DemoBlock from '../../demos/demo-block';
10 |
11 | const CardStory: Meta = {
12 | title: '信息展示/Card 卡片',
13 | component: Card,
14 | };
15 |
16 | export const Basic = () => {
17 | return (
18 |
19 |
20 | 内容
21 |
22 |
23 |
24 |
25 |
26 |
27 | 内容
28 |
29 |
30 |
31 | }>
32 |
35 |
38 |
41 |
42 |
43 |
44 | );
45 | };
46 |
47 | Basic.storyName = '基本用法';
48 |
49 | export default CardStory;
50 |
--------------------------------------------------------------------------------
/stories/cell/index.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Meta } from '@storybook/react';
3 | import { UnorderedListOutline, PayCircleOutline } from 'antd-mobile-icons';
4 |
5 | import Cell from '@/cell';
6 | import Space from '@/space';
7 |
8 | import DemoWrap from '../../demos/demo-wrap';
9 |
10 | const CellStory: Meta = {
11 | title: '信息展示/Cell 单元格',
12 | component: Cell,
13 | subcomponents: { 'Cell.Group': Cell.Group },
14 | };
15 |
16 | export const Basic = () => {
17 | return (
18 |
19 |
20 |
21 | 内容 |
22 |
23 | 内容
24 | |
25 |
26 |
27 |
28 | 内容 |
29 |
30 | 内容
31 | |
32 |
33 |
34 |
35 | | }>
36 | 内容
37 |
38 | | } />
39 |
40 |
41 |
42 |
43 | 内容
44 | |
45 | |
46 |
47 |
48 |
49 | );
50 | };
51 |
52 | Basic.storyName = '基本用法';
53 |
54 | export default CellStory;
55 |
--------------------------------------------------------------------------------
/stories/countdown/index.scss:
--------------------------------------------------------------------------------
1 | .demo-countdown-num {
2 | color: #fff;
3 | width: 16px;
4 | height: 16px;
5 | border-radius: 2px;
6 | display: flex;
7 | align-items: center;
8 | justify-content: center;
9 | box-sizing: border-box;
10 | background: #ee0a24;
11 | padding: 10px;
12 | }
13 |
14 | .demo-countdown-symbol {
15 | color: #ee0a24;
16 | height: 14px;
17 | margin: 0 2px;
18 | vertical-align: middle;
19 | display: inline-block;
20 | }
21 |
--------------------------------------------------------------------------------
/stories/countdown/index.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Meta } from '@storybook/react';
3 |
4 | import Countdown from '@/countdown';
5 |
6 | import DemoWrap from '../../demos/demo-wrap';
7 | import DemoBlock from '../../demos/demo-block';
8 |
9 | import './index.scss';
10 |
11 | const CountdownStory: Meta = {
12 | title: '信息展示/Countdown 倒计时',
13 | component: Countdown,
14 | };
15 |
16 | export const Basic = () => {
17 | return (
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
34 |
35 |
36 | );
37 | };
38 |
39 | Basic.storyName = '基本用法';
40 |
41 | export default CountdownStory;
42 |
--------------------------------------------------------------------------------
/stories/dialog/index.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Meta } from '@storybook/react';
3 |
4 | import Dialog from '@/dialog';
5 | import Button from '@/button';
6 | import Space from '@/space';
7 |
8 | import DemoWrap from '../../demos/demo-wrap';
9 | import DemoBlock from '../../demos/demo-block';
10 | import { sleep } from '@/pull-to-refresh/utils';
11 |
12 | const DialogStory: Meta = {
13 | title: '反馈/Dialog 弹出框',
14 | component: Dialog,
15 | };
16 |
17 | export const Basic = () => {
18 | const [visible1, setVisible1] = React.useState(false);
19 |
20 | return (
21 |
22 |
23 |
24 |
25 |
30 |
31 |
32 |
33 |
34 |
35 |
48 |
49 |
50 |
51 |
52 |
53 |
72 |
73 | );
74 | };
75 |
76 | Basic.storyName = '基本用法';
77 |
78 | export default DialogStory;
79 |
--------------------------------------------------------------------------------
/stories/divider/index.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { Meta } from '@storybook/react';
4 |
5 | import Divider from '@/divider';
6 |
7 | import DemoWrap from '../../demos/demo-wrap';
8 | import DemoBlock from '../../demos/demo-block';
9 |
10 | const GridStory: Meta = {
11 | title: '布局/Divider 分割线',
12 | component: Divider,
13 | };
14 |
15 | export const Basic = () => {
16 | return (
17 |
18 |
19 |
20 |
21 |
22 |
23 | 文字
24 |
25 |
26 |
27 | 左侧内容位置
28 | 右侧内容位置
29 |
30 |
31 |
32 | 虚线Divider
33 |
34 |
35 |
36 |
37 | 自定义样式
38 |
39 |
40 |
41 |
42 | <>
43 | Text
44 |
45 | Link
46 |
47 | Link
48 | >
49 |
50 |
51 | );
52 | };
53 |
54 | Basic.storyName = '基本用法';
55 |
56 | export default GridStory;
57 |
--------------------------------------------------------------------------------
/stories/ellipsis/index.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Meta } from '@storybook/react';
3 |
4 | import Ellipsis from '@/ellipsis';
5 |
6 | import DemoWrap from '../../demos/demo-wrap';
7 | import DemoBlock from '../../demos/demo-block';
8 |
9 | const EllipsisStory: Meta = {
10 | title: '信息展示/Ellipsis 文本省略',
11 | component: Ellipsis,
12 | };
13 |
14 | export const Basic = () => {
15 | return (
16 |
17 |
18 |
19 |
20 |
21 |
22 |
26 |
27 |
28 |
29 |
34 |
35 |
36 |
37 |
41 |
42 |
43 | );
44 | };
45 |
46 | Basic.storyName = '基本用法';
47 |
48 | export default EllipsisStory;
49 |
--------------------------------------------------------------------------------
/stories/error-block/index.scss:
--------------------------------------------------------------------------------
1 | .demo-block-content {
2 | .ygm-error-block {
3 | height: 100%;
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/stories/error-block/index.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Meta } from '@storybook/react';
3 |
4 | import ErrorBlock from '@/error-block';
5 |
6 | import DemoWrap from '../../demos/demo-wrap';
7 | import DemoBlock from '../../demos/demo-block';
8 |
9 | import './index.scss';
10 |
11 | const ErrorStory: Meta = {
12 | title: '反馈/ErrorBlock 异常',
13 | component: ErrorBlock,
14 | };
15 |
16 | export const Basic = () => {
17 | return (
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 | );
32 | };
33 |
34 | Basic.storyName = '基本用法';
35 |
36 | export default ErrorStory;
37 |
--------------------------------------------------------------------------------
/stories/grid/index.scss:
--------------------------------------------------------------------------------
1 | .grid-demo-item-block {
2 | border: solid 1px #999999;
3 | background: var(--ygm-color-box);
4 | text-align: center;
5 | color: #999999;
6 | height: 100%;
7 | }
8 |
--------------------------------------------------------------------------------
/stories/grid/index.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { Meta } from '@storybook/react';
4 |
5 | import Grid from '@/grid';
6 |
7 | import DemoWrap from '../../demos/demo-wrap';
8 | import DemoBlock from '../../demos/demo-block';
9 |
10 | import './index.scss';
11 |
12 | const GridStory: Meta = {
13 | title: '布局/Grid 栅格',
14 | component: Grid,
15 | };
16 |
17 | export const Basic = () => {
18 | return (
19 |
20 |
21 |
22 |
23 | A
24 |
25 |
26 | B
27 |
28 |
29 | C
30 |
31 |
32 | D
33 |
34 |
35 | E
36 |
37 |
38 |
39 |
40 |
41 |
42 | A
43 |
44 |
45 | B
46 |
47 |
48 | C
49 |
50 |
51 | D
52 |
53 |
54 | E
55 |
56 |
57 |
58 |
59 | );
60 | };
61 |
62 | Basic.storyName = '基本用法';
63 |
64 | export default GridStory;
65 |
--------------------------------------------------------------------------------
/stories/image/img.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taoyage/react-mobile-ui/e0d17002078e43a0fdfc75407f78b3e937e4127d/stories/image/img.png
--------------------------------------------------------------------------------
/stories/image/index.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { Meta } from '@storybook/react';
4 |
5 | import Image from '@/image';
6 | import Space from '@/space';
7 |
8 | import DemoWrap from '../../demos/demo-wrap';
9 | import DemoBlock from '../../demos/demo-block';
10 |
11 | import demoImg from './img.png';
12 |
13 | const ImageStory: Meta = {
14 | title: '信息展示/Image 图片',
15 | component: Image,
16 | };
17 |
18 | export const Basic = () => {
19 | return (
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 | );
47 | };
48 |
49 | Basic.storyName = '基本用法';
50 |
51 | export default ImageStory;
52 |
--------------------------------------------------------------------------------
/stories/infinite-scroll/index.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Meta } from '@storybook/react';
3 |
4 | import { List } from 'antd-mobile';
5 |
6 | import InfiniteScroll from '@/infinite-scroll';
7 | import Space from '@/space';
8 |
9 | import DemoWrap from '../../demos/demo-wrap';
10 | import DemoBlock from '../../demos/demo-block';
11 |
12 | const ErrorStory: Meta = {
13 | title: '信息展示/infiniteScroll 无限滚动',
14 | component: InfiniteScroll,
15 | };
16 |
17 | const mockData = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q'];
18 |
19 | function sleep(ms: number): any {
20 | // eslint-disable-next-line no-promise-executor-return
21 | return new Promise((resolve) => window.setTimeout(resolve, ms));
22 | }
23 |
24 | export const Basic = () => {
25 | const [data1, setData1] = React.useState(mockData);
26 | const [data2, setData2] = React.useState(mockData);
27 | const [hasMore1, setHasMore1] = React.useState(true);
28 | const [hasMore2, setHasMore2] = React.useState(true);
29 |
30 | const loadMore1 = async () => {
31 | await sleep(3000);
32 | setData1((val) => [...val, ...mockData]);
33 | if (data1.length >= 68) {
34 | setHasMore1(false);
35 | }
36 | };
37 |
38 | const loadMore2 = async () => {
39 | await sleep(3000);
40 | setData2((val) => [...val, ...mockData]);
41 | if (data2.length >= 68) {
42 | setHasMore2(false);
43 | }
44 | };
45 |
46 | return (
47 |
48 |
49 |
50 |
51 |
52 |
53 | {data1.map((item, index) => (
54 | {item}
55 | ))}
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 | 加载中 : --- 没有更多 ---}
67 | >
68 |
69 | {data2.map((item, index) => (
70 | {item}
71 | ))}
72 |
73 |
74 |
75 |
76 |
77 |
78 | );
79 | };
80 |
81 | Basic.storyName = '基本用法';
82 |
83 | export default ErrorStory;
84 |
--------------------------------------------------------------------------------
/stories/input/index.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Meta } from '@storybook/react';
3 |
4 | import Input from '@/input';
5 |
6 | import DemoWrap from '../../demos/demo-wrap';
7 | import DemoBlock from '../../demos/demo-block';
8 |
9 | const InputStory: Meta = {
10 | title: '信息录入/Input 输入框',
11 | component: Input,
12 | };
13 |
14 | export const Basic = () => {
15 | return (
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 | );
34 | };
35 |
36 | Basic.storyName = '基本用法';
37 |
38 | export default InputStory;
39 |
--------------------------------------------------------------------------------
/stories/mask/index.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Meta } from '@storybook/react';
3 |
4 | import Mask from '@/mask';
5 | import Button from '@/button';
6 |
7 | import DemoWrap from '../../demos/demo-wrap';
8 | import DemoBlock from '../../demos/demo-block';
9 |
10 | const MaskStory: Meta = {
11 | title: '反馈/Mask 遮罩层',
12 | component: Mask,
13 | };
14 |
15 | export const Basic = () => {
16 | const [visible1, setVisible1] = React.useState(false);
17 | const [visible2, setVisible2] = React.useState(false);
18 |
19 | return (
20 |
21 |
22 |
23 | setVisible1(false)} />
24 |
25 |
26 |
27 |
28 | setVisible2(false)}
32 | />
33 |
34 |
35 | );
36 | };
37 |
38 | Basic.storyName = '基本用法';
39 |
40 | export default MaskStory;
41 |
--------------------------------------------------------------------------------
/stories/nav-bar/index.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Meta } from '@storybook/react';
3 | import { SearchOutline } from 'antd-mobile-icons';
4 |
5 | import NavBar from '@/nav-bar';
6 | import Toast from '@/toast';
7 |
8 | import DemoWrap from '../../demos/demo-wrap';
9 | import DemoBlock from '../../demos/demo-block';
10 |
11 | const NavBarStory: Meta = {
12 | title: '导航/NavBar 导航栏',
13 | component: NavBar,
14 | };
15 |
16 | export const Basic = () => {
17 | const onBack = () => {
18 | Toast.show('back');
19 | };
20 |
21 | return (
22 |
23 |
24 | 标题
25 |
26 |
27 |
28 | 标题
29 |
30 |
31 |
32 | }>
33 | 标题
34 |
35 |
36 |
37 | );
38 | };
39 |
40 | Basic.storyName = '基本用法';
41 |
42 | export default NavBarStory;
43 |
--------------------------------------------------------------------------------
/stories/popup/index.scss:
--------------------------------------------------------------------------------
1 | .popup-demo {
2 | .ygm-button {
3 | margin-right: 20px;
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/stories/popup/index.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Meta } from '@storybook/react';
3 |
4 | import Popup from '@/popup';
5 | import Button from '@/button';
6 |
7 | import './index.scss';
8 |
9 | const ToastStory: Meta = {
10 | title: '反馈/Popup 弹出层',
11 | component: Popup,
12 | };
13 |
14 | export const Basic = () => {
15 | const [visible1, setVisible1] = React.useState(false);
16 | const [visible2, setVisible2] = React.useState(false);
17 | const [visible3, setVisible3] = React.useState(false);
18 | const [visible4, setVisible4] = React.useState(false);
19 |
20 | return (
21 |
22 |
23 |
24 |
25 |
26 |
27 |
setVisible1(false)} style={{ height: '30vh' }} />
28 | setVisible2(false)} style={{ height: '30vh' }} />
29 | setVisible3(false)} style={{ width: '30vh' }} />
30 | setVisible4(false)} style={{ width: '30vh' }} />
31 |
32 | );
33 | };
34 |
35 | Basic.storyName = '基本用法';
36 |
37 | export default ToastStory;
38 |
--------------------------------------------------------------------------------
/stories/pull-to-refresh/index.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Meta } from '@storybook/react';
3 |
4 | import DemoWrap from '../../demos/demo-wrap';
5 | import DemoBlock from '../../demos/demo-block';
6 |
7 | import PullToRefresh from '@/pull-to-refresh';
8 |
9 | const CountdownStory: Meta = {
10 | title: '反馈/PullToRefresh 下拉刷新',
11 | component: PullToRefresh,
12 | };
13 |
14 | const list = new Array(20).fill(1);
15 |
16 | export const Basic = () => {
17 | const onRefresh = () => {
18 | return new Promise((resolve) => {
19 | setTimeout(resolve, 3000);
20 | });
21 | };
22 |
23 | return (
24 |
25 |
26 |
27 | {list.map((_, index) => (
28 | list-{index}
29 | ))}
30 |
31 |
32 |
33 | );
34 | };
35 |
36 | Basic.storyName = '基本用法';
37 |
38 | export default CountdownStory;
39 |
--------------------------------------------------------------------------------
/stories/search-bar/index.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Meta } from '@storybook/react';
3 |
4 | import SearchBar from '@/search-bar';
5 | import Toast from '@/toast';
6 |
7 | import DemoWrap from '../../demos/demo-wrap';
8 | import DemoBlock from '../../demos/demo-block';
9 |
10 | const SearchBarStories: Meta = {
11 | title: '信息录入/SearchBar 搜索栏',
12 | component: SearchBar,
13 | };
14 |
15 | export const Basic = () => {
16 | const ref = React.useRef(null);
17 | const onClear = () => {
18 | Toast.show('清除');
19 | };
20 |
21 | const onSearch = () => {
22 | Toast.show('搜索');
23 | };
24 |
25 | return (
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
49 |
50 |
51 | );
52 | };
53 |
54 | Basic.storyName = '基本用法';
55 |
56 | export default SearchBarStories;
57 |
--------------------------------------------------------------------------------
/stories/selector/index.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Meta } from '@storybook/react';
3 |
4 | import Selector from '@/selector';
5 |
6 | import DemoWrap from '../../demos/demo-wrap';
7 | import DemoBlock from '../../demos/demo-block';
8 |
9 | const SidebarStory: Meta = {
10 | title: '信息录入/Selector 选择组',
11 | component: Selector,
12 | };
13 |
14 | const options = [
15 | {
16 | label: '选项一',
17 | value: '1',
18 | },
19 | {
20 | label: '选项二',
21 | value: '2',
22 | },
23 | {
24 | label: '选项三',
25 | value: '3',
26 | },
27 | ];
28 |
29 | const options1 = [
30 | {
31 | label: '选项一',
32 | value: '1',
33 | disabled: true,
34 | },
35 | {
36 | label: '选项二',
37 | value: '2',
38 | },
39 | {
40 | label: '选项三',
41 | value: '3',
42 | },
43 | ];
44 |
45 | const options2 = [
46 | {
47 | label: '选项一',
48 | value: '1',
49 | description: '描述一',
50 | },
51 | {
52 | label: '选项二',
53 | value: '2',
54 | description: '描述二',
55 | },
56 | ];
57 |
58 | export const Basic = () => {
59 | return (
60 |
61 |
62 |
63 |
64 |
65 |
66 | {
71 | console.log(value);
72 | console.log(selectorOptions);
73 | }}
74 | />
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
100 |
101 |
102 | );
103 | };
104 |
105 | Basic.storyName = '基本用法';
106 |
107 | export default SidebarStory;
108 |
--------------------------------------------------------------------------------
/stories/sidebar/index.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Meta } from '@storybook/react';
3 |
4 | import Sidebar from '@/sidebar';
5 |
6 | import DemoWrap from '../../demos/demo-wrap';
7 | import DemoBlock from '../../demos/demo-block';
8 |
9 | const SidebarStory: Meta = {
10 | title: '导航/Sidebar 侧边导航',
11 | component: Sidebar,
12 | };
13 |
14 | export const Basic = () => {
15 | return (
16 |
17 |
18 |
19 |
20 | 1
21 |
22 |
23 | 2
24 |
25 |
26 | 3
27 |
28 |
29 | 4
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 | 1
38 |
39 |
40 | 2
41 |
42 |
43 | 3
44 |
45 |
46 | 4
47 |
48 |
49 |
50 |
51 | );
52 | };
53 |
54 | Basic.storyName = '基本用法';
55 |
56 | export default SidebarStory;
57 |
--------------------------------------------------------------------------------
/stories/slider/index.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Meta } from '@storybook/react';
3 |
4 | import Slider from '@/slider';
5 |
6 | import DemoWrap from '../../demos/demo-wrap';
7 | import DemoBlock from '../../demos/demo-block';
8 |
9 | const SliderStory: Meta = {
10 | title: '信息录入/Slider 滑动条',
11 | component: Slider,
12 | };
13 |
14 | export const Basic = () => {
15 | const onChange = (value: number) => {
16 | console.log(value);
17 | };
18 |
19 | const onChangeAfter = (value: number) => {
20 | console.log(value);
21 | };
22 |
23 | return (
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
41 |
42 |
43 | );
44 | };
45 |
46 | Basic.storyName = '基本用法';
47 |
48 | export default SliderStory;
49 |
--------------------------------------------------------------------------------
/stories/space/index.scss:
--------------------------------------------------------------------------------
1 | .space-demo-wrap {
2 | font-family: arial;
3 | }
4 |
--------------------------------------------------------------------------------
/stories/space/index.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Meta } from '@storybook/react';
3 |
4 | import Space from '@/space';
5 | import Button from '@/button';
6 |
7 | import DemoWrap from '../../demos/demo-wrap';
8 | import DemoBlock from '../../demos/demo-block';
9 |
10 | import './index.scss';
11 |
12 | const LoadingStory: Meta = {
13 | title: '布局/Space 间距',
14 | component: Space,
15 | };
16 |
17 | export const Basic = () => {
18 | return (
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
69 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
81 |
84 |
85 |
86 |
87 | );
88 | };
89 |
90 | Basic.storyName = '基本用法';
91 |
92 | export default LoadingStory;
93 |
--------------------------------------------------------------------------------
/stories/spinner-loading/index.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Meta } from '@storybook/react';
3 |
4 | import SpinnerLoading from '@/spinner-loading';
5 | import Space from '@/space';
6 |
7 | import DemoWrap from '../../demos/demo-wrap';
8 | import DemoBlock from '../../demos/demo-block';
9 |
10 | const LoadingStory: Meta = {
11 | title: '反馈/Loading 加载中',
12 | component: SpinnerLoading,
13 | };
14 |
15 | export const Basic = () => {
16 | return (
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 | );
37 | };
38 |
39 | Basic.storyName = '基本用法';
40 |
41 | export default LoadingStory;
42 |
--------------------------------------------------------------------------------
/stories/swiper/index.scss:
--------------------------------------------------------------------------------
1 | .swiper-demo {
2 | &-content {
3 | height: 120px;
4 | color: #ffffff;
5 | display: flex;
6 | justify-content: center;
7 | align-items: center;
8 | font-size: 48px;
9 | user-select: none;
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/stories/swiper/index.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Meta } from '@storybook/react';
3 |
4 | import Swiper, { SwiperRef } from '@/swiper';
5 | import Button from '@/button';
6 | import Space from '@/space';
7 |
8 | import DemoWrap from '../../demos/demo-wrap';
9 | import DemoBlock from '../../demos/demo-block';
10 |
11 | import './index.scss';
12 |
13 | const SwiperStory: Meta = {
14 | title: '信息展示/Swiper 轮播图',
15 | component: Swiper,
16 | subcomponents: { 'Swiper.Item': Swiper.Item },
17 | };
18 |
19 | const colors = ['#ace0ff', '#bcffbd', '#e4fabd', '#ffcfac'];
20 |
21 | export const Basic = () => {
22 | const swiperRef = React.useRef(null);
23 |
24 | return (
25 |
26 |
27 |
28 | {colors.map((color, index) => (
29 |
30 |
31 | {index + 1}
32 |
33 |
34 | ))}
35 |
36 |
37 |
38 |
39 |
40 | {colors.map((color, index) => (
41 |
42 |
43 | {index + 1}
44 |
45 |
46 | ))}
47 |
48 |
49 |
50 |
51 |
52 | {colors.map((color, index) => (
53 |
54 |
55 | {index + 1}
56 |
57 |
58 | ))}
59 |
60 |
61 |
62 |
63 |
64 | {colors.map((color, index) => (
65 |
66 |
67 | {index + 1}
68 |
69 |
70 | ))}
71 |
72 |
73 |
74 |
75 |
76 | {colors.map((color, index) => (
77 |
78 |
79 | {index + 1}
80 |
81 |
82 | ))}
83 |
84 |
85 |
92 |
99 |
100 |
101 |
102 | );
103 | };
104 |
105 | Basic.storyName = '基本用法';
106 |
107 | export default SwiperStory;
108 |
--------------------------------------------------------------------------------
/stories/tabs/index.scss:
--------------------------------------------------------------------------------
1 | .tabs-demo-list {
2 | border-radius: 15px;
3 | }
4 |
5 | .tabs-demo-active {
6 | border-radius: 15px;
7 | }
8 |
--------------------------------------------------------------------------------
/stories/tabs/index.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Meta } from '@storybook/react';
3 |
4 | import Tabs from '@/tabs';
5 |
6 | import DemoWrap from '../../demos/demo-wrap';
7 | import DemoBlock from '../../demos/demo-block';
8 |
9 | import './index.scss';
10 |
11 | const TabsStory: Meta = {
12 | title: '导航/ Tabs 标签页',
13 | component: Tabs,
14 | };
15 |
16 | export const Basic = () => (
17 |
18 |
19 |
20 |
21 | 内容1
22 |
23 |
24 | 内容2
25 |
26 |
27 | 内容3
28 |
29 |
30 |
31 |
32 |
33 |
40 |
41 | 内容1
42 |
43 |
44 | 内容2
45 |
46 |
47 | 内容3
48 |
49 |
50 |
51 |
52 | );
53 |
54 | Basic.storyName = '基础用法';
55 |
56 | export default TabsStory;
57 |
--------------------------------------------------------------------------------
/stories/toast/index.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Meta } from '@storybook/react';
3 |
4 | import Toast from '@/toast';
5 | import ToastComponent from '@/toast/toast';
6 | import Button from '@/button';
7 |
8 | import Space from '@/space';
9 |
10 | import DemoWrap from '../../demos/demo-wrap';
11 | import DemoBlock from '../../demos/demo-block';
12 |
13 | const ToastStory: Meta = {
14 | title: '反馈/ Toast 轻提示',
15 | component: ToastComponent,
16 | };
17 |
18 | export const Basic = () => (
19 |
20 |
21 |
33 |
34 |
35 |
36 |
37 |
51 |
52 |
66 |
67 |
81 |
82 |
83 |
84 | );
85 |
86 | Basic.storyName = '基础用法';
87 |
88 | export default ToastStory;
89 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@taoyage/configs/shared-tsconfig",
3 | "compilerOptions": {
4 | "baseUrl": ".",
5 | "skipLibCheck": true,
6 | "declaration": true,
7 | "esModuleInterop": true,
8 | "jsx": "react",
9 | "outDir": "lib",
10 | "paths": {
11 | "@/*": ["packages/*"]
12 | },
13 | "rootDir": "."
14 | },
15 | "include": ["packages", "stories", "demos"],
16 | "exclude": ["node_modules"],
17 | "files": ["./typings.d.ts"]
18 | }
19 |
--------------------------------------------------------------------------------
/typings.d.ts:
--------------------------------------------------------------------------------
1 | declare module '*.png';
2 |
--------------------------------------------------------------------------------