mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-12 12:28:42 +00:00
refactor(core): desktop project struct (#8334)
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
export * from './confirm-modal';
|
||||
export * from './modal';
|
||||
export * from './overlay-modal';
|
||||
export * from './prompt-modal';
|
||||
|
||||
@@ -229,6 +229,7 @@ export const ModalInner = forwardRef<HTMLDivElement, ModalProps>(
|
||||
styles.modalContentWrapper,
|
||||
contentWrapperClassName
|
||||
)}
|
||||
data-mobile={BUILD_CONFIG.isMobileEdition ? '' : undefined}
|
||||
style={contentWrapperStyle}
|
||||
>
|
||||
<Dialog.Content
|
||||
|
||||
109
packages/frontend/component/src/ui/modal/prompt-modal.css.ts
Normal file
109
packages/frontend/component/src/ui/modal/prompt-modal.css.ts
Normal 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',
|
||||
}),
|
||||
};
|
||||
235
packages/frontend/component/src/ui/modal/prompt-modal.tsx
Normal file
235
packages/frontend/component/src/ui/modal/prompt-modal.tsx
Normal 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,
|
||||
};
|
||||
};
|
||||
@@ -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',
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user