refactor(core): desktop project struct (#8334)

This commit is contained in:
EYHN
2024-11-05 11:00:33 +08:00
committed by GitHub
parent 89d09fd5e9
commit 902635e60f
343 changed files with 3846 additions and 3508 deletions

View File

@@ -1,3 +1,4 @@
export * from './confirm-modal';
export * from './modal';
export * from './overlay-modal';
export * from './prompt-modal';

View File

@@ -229,6 +229,7 @@ export const ModalInner = forwardRef<HTMLDivElement, ModalProps>(
styles.modalContentWrapper,
contentWrapperClassName
)}
data-mobile={BUILD_CONFIG.isMobileEdition ? '' : undefined}
style={contentWrapperStyle}
>
<Dialog.Content

View File

@@ -0,0 +1,109 @@
import { cssVar } from '@toeverything/theme';
import { style } from '@vanilla-extract/css';
// desktop
export const desktopStyles = {
container: style({
display: 'flex',
flexDirection: 'column',
}),
description: style({}),
header: style({}),
content: style({
height: '100%',
overflowY: 'auto',
padding: '12px 4px 20px 4px',
}),
label: style({
color: cssVar('textSecondaryColor'),
fontSize: 14,
lineHeight: '22px',
padding: '8px 0',
}),
input: style({}),
inputContainer: style({}),
footer: style({
display: 'flex',
justifyContent: 'flex-end',
alignItems: 'center',
paddingTop: '40px',
marginTop: 'auto',
gap: '20px',
selectors: {
'&.modalFooterWithChildren': {
paddingTop: '20px',
},
'&.reverse': {
flexDirection: 'row-reverse',
justifyContent: 'flex-start',
},
},
}),
action: style({}),
};
// mobile
export const mobileStyles = {
container: style({
display: 'flex',
flexDirection: 'column',
padding: '12px 0 !important',
borderRadius: 22,
}),
description: style({
padding: '11px 22px',
fontSize: 17,
fontWeight: 400,
letterSpacing: -0.43,
lineHeight: '22px',
}),
label: style({
color: cssVar('textSecondaryColor'),
fontSize: 14,
lineHeight: '22px',
padding: '8px 16px',
}),
header: style({
padding: '10px 16px',
marginBottom: '0px !important',
fontSize: 17,
fontWeight: 600,
letterSpacing: -0.43,
lineHeight: '22px',
}),
inputContainer: style({
padding: '0 16px',
}),
input: style({
height: 44,
fontSize: 17,
lineHeight: '22px',
}),
content: style({
padding: '11px 22px',
fontSize: 17,
fontWeight: 400,
letterSpacing: -0.43,
lineHeight: '22px',
}),
footer: style({
padding: '8px 16px',
display: 'flex',
flexDirection: 'column',
gap: 16,
selectors: {
'&.reverse': {
flexDirection: 'column-reverse',
},
},
}),
action: style({
width: '100%',
height: 44,
borderRadius: 8,
fontSize: 17,
fontWeight: 400,
letterSpacing: -0.43,
lineHeight: '22px',
}),
};

View File

@@ -0,0 +1,235 @@
import { DialogTrigger } from '@radix-ui/react-dialog';
import clsx from 'clsx';
import type { PropsWithChildren } from 'react';
import { createContext, useCallback, useContext, useState } from 'react';
import type { ButtonProps } from '../button';
import { Button } from '../button';
import Input, { type InputProps } from '../input';
import type { ModalProps } from './modal';
import { Modal } from './modal';
import { desktopStyles, mobileStyles } from './prompt-modal.css';
const styles = BUILD_CONFIG.isMobileEdition ? mobileStyles : desktopStyles;
export interface PromptModalProps extends ModalProps {
confirmButtonOptions?: Omit<ButtonProps, 'children'>;
onConfirm?: ((text: string) => void) | ((text: string) => Promise<void>);
onCancel?: () => void;
confirmText?: React.ReactNode;
cancelText?: React.ReactNode;
label?: React.ReactNode;
defaultValue?: string;
required?: boolean;
cancelButtonOptions?: Omit<ButtonProps, 'children'>;
inputOptions?: Omit<InputProps, 'value' | 'onChange'>;
reverseFooter?: boolean;
/**
* Auto focus on confirm button when modal opened
* @default true
*/
autoFocusConfirm?: boolean;
}
export const PromptModal = ({
children,
confirmButtonOptions,
// FIXME: we need i18n
confirmText,
cancelText = 'Cancel',
cancelButtonOptions,
reverseFooter,
onConfirm,
onCancel,
label,
required = true,
inputOptions,
defaultValue,
width = 480,
autoFocusConfirm = true,
headerClassName,
descriptionClassName,
...props
}: PromptModalProps) => {
const [value, setValue] = useState(defaultValue ?? '');
const onConfirmClick = useCallback(() => {
Promise.resolve(onConfirm?.(value))
.catch(err => {
console.error(err);
})
.finally(() => {
setValue('');
});
}, [onConfirm, value]);
const onKeyDown = useCallback(
(e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Escape') {
if (value) {
e.preventDefault();
return;
} else {
e.currentTarget.blur();
}
}
},
[value]
);
return (
<Modal
contentOptions={{
className: styles.container,
onPointerDownOutside: e => {
e.stopPropagation();
onCancel?.();
},
}}
width={width}
closeButtonOptions={{
onClick: onCancel,
}}
headerClassName={clsx(styles.header, headerClassName)}
descriptionClassName={clsx(styles.description, descriptionClassName)}
{...props}
>
<div className={styles.label}>{label}</div>
<div className={styles.inputContainer}>
<Input
value={value}
onChange={setValue}
autoFocus
className={styles.input}
onKeyDown={onKeyDown}
data-testid="prompt-modal-input"
{...inputOptions}
/>
</div>
{children ? <div className={styles.content}>{children}</div> : null}
<div
className={clsx(styles.footer, {
modalFooterWithChildren: !!children,
reverse: reverseFooter,
})}
>
<DialogTrigger asChild>
<Button
className={styles.action}
onClick={onCancel}
data-testid="prompt-modal-cancel"
{...cancelButtonOptions}
>
{cancelText}
</Button>
</DialogTrigger>
<Button
className={styles.action}
onClick={onConfirmClick}
disabled={required && !value}
data-testid="prompt-modal-confirm"
autoFocus={autoFocusConfirm}
{...confirmButtonOptions}
>
{confirmText}
</Button>
</div>
</Modal>
);
};
interface OpenPromptModalOptions {
autoClose?: boolean;
onSuccess?: () => void;
}
interface PromptModalContextProps {
modalProps: PromptModalProps;
openPromptModal: (
props?: PromptModalProps,
options?: OpenPromptModalOptions
) => void;
closePromptModal: () => void;
}
const PromptModalContext = createContext<PromptModalContextProps>({
modalProps: { open: false },
openPromptModal: () => {},
closePromptModal: () => {},
});
export const PromptModalProvider = ({ children }: PropsWithChildren) => {
const [modalProps, setModalProps] = useState<PromptModalProps>({
open: false,
});
const setLoading = useCallback((value: boolean) => {
setModalProps(prev => ({
...prev,
confirmButtonOptions: {
...prev.confirmButtonOptions,
loading: value,
},
}));
}, []);
const closePromptModal = useCallback(() => {
setModalProps({ open: false });
}, []);
const openPromptModal = useCallback(
(props?: PromptModalProps, options?: OpenPromptModalOptions) => {
const { autoClose = true, onSuccess } = options ?? {};
if (!props) {
setModalProps({ open: true });
return;
}
const { onConfirm: _onConfirm, ...otherProps } = props;
const onConfirm = (text: string) => {
setLoading(true);
return Promise.resolve(_onConfirm?.(text))
.then(() => onSuccess?.())
.catch(console.error)
.finally(() => setLoading(false))
.finally(() => autoClose && closePromptModal());
};
setModalProps({ ...otherProps, onConfirm, open: true });
},
[closePromptModal, setLoading]
);
const onOpenChange = useCallback(
(open: boolean) => {
modalProps.onOpenChange?.(open);
setModalProps(props => ({ ...props, open }));
},
[modalProps]
);
return (
<PromptModalContext.Provider
value={{
openPromptModal: openPromptModal,
closePromptModal: closePromptModal,
modalProps,
}}
>
{children}
{/* TODO(@catsjuice): multi-instance support(unnecessary for now) */}
<PromptModal {...modalProps} onOpenChange={onOpenChange} />
</PromptModalContext.Provider>
);
};
export const usePromptModal = () => {
const context = useContext(PromptModalContext);
if (!context) {
throw new Error(
'useConfirmModal must be used within a ConfirmModalProvider'
);
}
return {
openPromptModal: context.openPromptModal,
closePromptModal: context.closePromptModal,
};
};

View File

@@ -79,15 +79,12 @@ export const modalContentWrapper = style({
alignItems: 'center',
justifyContent: 'center',
zIndex: cssVar('zIndexModal'),
'@media': {
'screen and (width <= 640px)': {
// todo: adjust animation
selectors: {
'&[data-mobile]': {
alignItems: 'flex-end',
paddingBottom: 'env(safe-area-inset-bottom, 20px)',
},
},
selectors: {
'&[data-full-screen="true"]': {
padding: '0 !important',
},