mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-11 20:08:37 +00:00
feat(core): add starAFFiNE and issueFeedback modal (#5718)
close TOV-482 https://github.com/toeverything/AFFiNE/assets/102217452/da1f74bc-4b8d-4d7f-987d-f53da98d92fe
This commit is contained in:
Binary file not shown.
@@ -1,192 +0,0 @@
|
||||
import { cssVar } from '@toeverything/theme';
|
||||
import { keyframes, style } from '@vanilla-extract/css';
|
||||
export const modalStyle = style({
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
backgroundColor: cssVar('backgroundSecondaryColor'),
|
||||
borderRadius: '16px',
|
||||
overflow: 'hidden',
|
||||
});
|
||||
export const titleContainerStyle = style({
|
||||
width: 'calc(100% - 72px)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
position: 'relative',
|
||||
height: '60px',
|
||||
overflow: 'hidden',
|
||||
});
|
||||
export const titleStyle = style({
|
||||
fontSize: cssVar('fontH6'),
|
||||
fontWeight: '600',
|
||||
marginTop: '12px',
|
||||
position: 'absolute',
|
||||
marginBottom: '12px',
|
||||
});
|
||||
const slideToLeft = keyframes({
|
||||
'0%': {
|
||||
transform: 'translateX(0)',
|
||||
opacity: 1,
|
||||
},
|
||||
'100%': {
|
||||
transform: 'translateX(-300px)',
|
||||
opacity: 0,
|
||||
},
|
||||
});
|
||||
const slideToRight = keyframes({
|
||||
'0%': {
|
||||
transform: 'translateX(0)',
|
||||
opacity: 1,
|
||||
},
|
||||
'100%': {
|
||||
transform: 'translateX(300px)',
|
||||
opacity: 0,
|
||||
},
|
||||
});
|
||||
const slideFormLeft = keyframes({
|
||||
'0%': {
|
||||
transform: 'translateX(300px)',
|
||||
opacity: 0,
|
||||
},
|
||||
'100%': {
|
||||
transform: 'translateX(0)',
|
||||
opacity: 1,
|
||||
},
|
||||
});
|
||||
const slideFormRight = keyframes({
|
||||
'0%': {
|
||||
transform: 'translateX(-300px)',
|
||||
opacity: 0,
|
||||
},
|
||||
'100%': {
|
||||
transform: 'translateX(0)',
|
||||
opacity: 1,
|
||||
},
|
||||
});
|
||||
export const formSlideToLeftStyle = style({
|
||||
animation: `${slideFormLeft} 0.3s ease-in-out forwards`,
|
||||
});
|
||||
export const formSlideToRightStyle = style({
|
||||
animation: `${slideFormRight} 0.3s ease-in-out forwards`,
|
||||
});
|
||||
export const slideToLeftStyle = style({
|
||||
animation: `${slideToLeft} 0.3s ease-in-out forwards`,
|
||||
});
|
||||
export const slideToRightStyle = style({
|
||||
animation: `${slideToRight} 0.3s ease-in-out forwards`,
|
||||
});
|
||||
export const containerStyle = style({
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
});
|
||||
export const videoContainerStyle = style({
|
||||
height: '300px',
|
||||
width: 'calc(100% - 72px)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
flexGrow: 1,
|
||||
justifyContent: 'space-between',
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
});
|
||||
export const videoSlideStyle = style({
|
||||
width: '100%',
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
});
|
||||
export const videoStyle = style({
|
||||
position: 'absolute',
|
||||
objectFit: 'fill',
|
||||
height: '300px',
|
||||
border: `1px solid ${cssVar('borderColor')}`,
|
||||
transition: 'opacity 0.5s ease-in-out',
|
||||
});
|
||||
const fadeIn = keyframes({
|
||||
'0%': {
|
||||
transform: 'translateX(300px)',
|
||||
},
|
||||
'100%': {
|
||||
transform: 'translateX(0)',
|
||||
},
|
||||
});
|
||||
export const videoActiveStyle = style({
|
||||
animation: `${fadeIn} 0.5s ease-in-out forwards`,
|
||||
opacity: 0,
|
||||
});
|
||||
export const arrowStyle = style({
|
||||
wordBreak: 'break-all',
|
||||
wordWrap: 'break-word',
|
||||
width: '36px',
|
||||
fontSize: '32px',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
height: '240px',
|
||||
flexGrow: 0.2,
|
||||
cursor: 'pointer',
|
||||
});
|
||||
export const descriptionContainerStyle = style({
|
||||
width: 'calc(100% - 112px)',
|
||||
height: '100px',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
});
|
||||
export const descriptionStyle = style({
|
||||
marginTop: '15px',
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
fontSize: cssVar('fontSm'),
|
||||
lineHeight: '18px',
|
||||
position: 'absolute',
|
||||
});
|
||||
export const tabStyle = style({
|
||||
width: '40px',
|
||||
height: '40px',
|
||||
content: '""',
|
||||
margin: '40px 10px 40px 0',
|
||||
transition: 'all 0.15s ease-in-out',
|
||||
position: 'relative',
|
||||
cursor: 'pointer',
|
||||
':hover': {
|
||||
opacity: 1,
|
||||
},
|
||||
'::after': {
|
||||
content: '""',
|
||||
position: 'absolute',
|
||||
bottom: '20px',
|
||||
left: '0',
|
||||
width: '100%',
|
||||
height: '2px',
|
||||
background: cssVar('textPrimaryColor'),
|
||||
transition: 'all 0.15s ease-in-out',
|
||||
opacity: 0.2,
|
||||
cursor: 'pointer',
|
||||
},
|
||||
});
|
||||
export const tabActiveStyle = style({
|
||||
'::after': {
|
||||
opacity: 1,
|
||||
},
|
||||
});
|
||||
export const tabContainerStyle = style({
|
||||
width: '100%',
|
||||
marginTop: '20px',
|
||||
position: 'relative',
|
||||
height: '2px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
});
|
||||
export const buttonDisableStyle = style({
|
||||
cursor: 'not-allowed',
|
||||
color: cssVar('textDisableColor'),
|
||||
});
|
||||
@@ -1 +0,0 @@
|
||||
export * from './tour-modal';
|
||||
Binary file not shown.
@@ -1,160 +0,0 @@
|
||||
/// <reference types="../../type.d.ts" />
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { ArrowLeftSmallIcon, ArrowRightSmallIcon } from '@blocksuite/icons';
|
||||
import clsx from 'clsx';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { Modal, type ModalProps } from '../../ui/modal';
|
||||
import editingVideo from './editingVideo.mp4';
|
||||
import {
|
||||
arrowStyle,
|
||||
buttonDisableStyle,
|
||||
containerStyle,
|
||||
descriptionContainerStyle,
|
||||
descriptionStyle,
|
||||
formSlideToLeftStyle,
|
||||
formSlideToRightStyle,
|
||||
modalStyle,
|
||||
slideToLeftStyle,
|
||||
slideToRightStyle,
|
||||
tabActiveStyle,
|
||||
tabContainerStyle,
|
||||
tabStyle,
|
||||
titleContainerStyle,
|
||||
titleStyle,
|
||||
videoContainerStyle,
|
||||
videoSlideStyle,
|
||||
videoStyle,
|
||||
} from './index.css';
|
||||
import switchVideo from './switchVideo.mp4';
|
||||
|
||||
export const TourModal = (props: ModalProps) => {
|
||||
const t = useAFFiNEI18N();
|
||||
const [step, setStep] = useState(-1);
|
||||
return (
|
||||
<Modal
|
||||
width={545}
|
||||
contentOptions={{
|
||||
['data-testid' as string]: 'onboarding-modal',
|
||||
style: {
|
||||
minHeight: '480px',
|
||||
padding: 0,
|
||||
},
|
||||
}}
|
||||
overlayOptions={{
|
||||
style: {
|
||||
background: 'transparent',
|
||||
},
|
||||
}}
|
||||
closeButtonOptions={{
|
||||
// @ts-expect-error - fix upstream type
|
||||
'data-testid': 'onboarding-modal-close-button',
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
<div className={modalStyle}>
|
||||
<div className={titleContainerStyle}>
|
||||
{step !== -1 && (
|
||||
<div
|
||||
className={clsx(titleStyle, {
|
||||
[slideToRightStyle]: step === 0,
|
||||
[formSlideToLeftStyle]: step === 1,
|
||||
})}
|
||||
>
|
||||
{t['com.affine.onboarding.title2']()}
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={clsx(titleStyle, {
|
||||
[slideToLeftStyle]: step === 1,
|
||||
[formSlideToRightStyle]: step === 0,
|
||||
})}
|
||||
>
|
||||
{t['com.affine.onboarding.title1']()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={containerStyle}>
|
||||
<div
|
||||
className={clsx(arrowStyle, { [buttonDisableStyle]: step !== 1 })}
|
||||
onClick={() => step === 1 && setStep(0)}
|
||||
data-testid="onboarding-modal-pre-button"
|
||||
>
|
||||
<ArrowLeftSmallIcon />
|
||||
</div>
|
||||
<div className={videoContainerStyle}>
|
||||
<div className={videoSlideStyle}>
|
||||
{step !== -1 && (
|
||||
<video
|
||||
autoPlay
|
||||
muted
|
||||
loop
|
||||
className={clsx(videoStyle, {
|
||||
[slideToRightStyle]: step === 0,
|
||||
[formSlideToLeftStyle]: step === 1,
|
||||
})}
|
||||
data-testid="onboarding-modal-editing-video"
|
||||
>
|
||||
<source src={editingVideo} type="video/mp4" />
|
||||
</video>
|
||||
)}
|
||||
<video
|
||||
autoPlay
|
||||
muted
|
||||
loop
|
||||
className={clsx(videoStyle, {
|
||||
[slideToLeftStyle]: step === 1,
|
||||
[formSlideToRightStyle]: step === 0,
|
||||
})}
|
||||
data-testid="onboarding-modal-switch-video"
|
||||
>
|
||||
<source src={switchVideo} type="video/mp4" />
|
||||
</video>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={clsx(arrowStyle, { [buttonDisableStyle]: step === 1 })}
|
||||
onClick={() => setStep(1)}
|
||||
data-testid="onboarding-modal-next-button"
|
||||
>
|
||||
<ArrowRightSmallIcon />
|
||||
</div>
|
||||
</div>
|
||||
<ul className={tabContainerStyle}>
|
||||
<li
|
||||
className={clsx(tabStyle, {
|
||||
[tabActiveStyle]: step !== 1,
|
||||
})}
|
||||
onClick={() => setStep(0)}
|
||||
></li>
|
||||
<li
|
||||
className={clsx(tabStyle, { [tabActiveStyle]: step === 1 })}
|
||||
onClick={() => setStep(1)}
|
||||
></li>
|
||||
</ul>
|
||||
<div className={descriptionContainerStyle}>
|
||||
{step !== -1 && (
|
||||
<div
|
||||
className={clsx(descriptionStyle, {
|
||||
[slideToRightStyle]: step === 0,
|
||||
[formSlideToLeftStyle]: step === 1,
|
||||
})}
|
||||
>
|
||||
{t['com.affine.onboarding.videoDescription2']()}
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={clsx(descriptionStyle, {
|
||||
[slideToLeftStyle]: step === 1,
|
||||
[formSlideToRightStyle]: step === 0,
|
||||
})}
|
||||
>
|
||||
{t['com.affine.onboarding.videoDescription1']()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default TourModal;
|
||||
@@ -1,2 +1,3 @@
|
||||
export * from './confirm-modal';
|
||||
export * from './modal';
|
||||
export * from './overlay-modal';
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Button } from '../button';
|
||||
import { Input, type InputProps } from '../input';
|
||||
import { ConfirmModal, type ConfirmModalProps } from './confirm-modal';
|
||||
import { Modal, type ModalProps } from './modal';
|
||||
import { OverlayModal, type OverlayModalProps } from './overlay-modal';
|
||||
|
||||
export default {
|
||||
title: 'UI/Modal',
|
||||
@@ -65,5 +66,38 @@ const ConfirmModalTemplate: StoryFn<ConfirmModalProps> = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const OverlayModalTemplate: StoryFn<OverlayModalProps> = () => {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button onClick={() => setOpen(true)}>Open Overlay Modal</Button>
|
||||
<OverlayModal
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
title="Modal Title"
|
||||
description="Modal description"
|
||||
confirmButtonOptions={{
|
||||
type: 'primary',
|
||||
}}
|
||||
topImage={
|
||||
<div
|
||||
style={{
|
||||
width: '400px',
|
||||
height: '300px',
|
||||
background: '#66ccff',
|
||||
opacity: 0.1,
|
||||
color: '#fff',
|
||||
}}
|
||||
></div>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const Confirm: StoryFn<ModalProps> =
|
||||
ConfirmModalTemplate.bind(undefined);
|
||||
|
||||
export const Overlay: StoryFn<ModalProps> =
|
||||
OverlayModalTemplate.bind(undefined);
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import { cssVar } from '@toeverything/theme';
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
export const title = style({
|
||||
padding: '20px 24px 8px 24px',
|
||||
fontSize: cssVar('fontH6'),
|
||||
fontFamily: cssVar('fontFamily'),
|
||||
fontWeight: '600',
|
||||
lineHeight: '26px',
|
||||
});
|
||||
|
||||
export const content = style({
|
||||
padding: '0px 24px 8px',
|
||||
fontSize: cssVar('fontBase'),
|
||||
lineHeight: '24px',
|
||||
fontWeight: 400,
|
||||
});
|
||||
|
||||
export const footer = style({
|
||||
padding: '20px 24px',
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
gap: '20px',
|
||||
});
|
||||
|
||||
export const gotItBtn = style({
|
||||
fontWeight: 500,
|
||||
});
|
||||
|
||||
export const buttonText = style({
|
||||
color: cssVar('pureWhite'),
|
||||
textDecoration: 'none',
|
||||
cursor: 'pointer',
|
||||
':visited': {
|
||||
color: cssVar('pureWhite'),
|
||||
},
|
||||
});
|
||||
102
packages/frontend/component/src/ui/modal/overlay-modal.tsx
Normal file
102
packages/frontend/component/src/ui/modal/overlay-modal.tsx
Normal file
@@ -0,0 +1,102 @@
|
||||
import { DialogTrigger } from '@radix-ui/react-dialog';
|
||||
import { cssVar } from '@toeverything/theme';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { Button, type ButtonProps } from '../button';
|
||||
import { Modal, type ModalProps } from './modal';
|
||||
import * as styles from './overlay-modal.css';
|
||||
|
||||
const defaultContentOptions: ModalProps['contentOptions'] = {
|
||||
style: {
|
||||
padding: 0,
|
||||
overflow: 'hidden',
|
||||
boxShadow: cssVar('menuShadow'),
|
||||
},
|
||||
};
|
||||
const defaultOverlayOptions: ModalProps['overlayOptions'] = {
|
||||
style: {
|
||||
background: cssVar('white80'),
|
||||
backdropFilter: 'blur(2px)',
|
||||
},
|
||||
};
|
||||
|
||||
export interface OverlayModalProps extends ModalProps {
|
||||
to?: string;
|
||||
external?: boolean;
|
||||
topImage?: React.ReactNode;
|
||||
confirmText?: string;
|
||||
confirmButtonOptions?: ButtonProps;
|
||||
onConfirm?: () => void;
|
||||
cancelText?: string;
|
||||
cancelButtonOptions?: ButtonProps;
|
||||
withoutCancelButton?: boolean;
|
||||
}
|
||||
|
||||
export const OverlayModal = memo(function OverlayModal({
|
||||
open,
|
||||
topImage,
|
||||
onOpenChange,
|
||||
title,
|
||||
description,
|
||||
onConfirm,
|
||||
to,
|
||||
external,
|
||||
confirmButtonOptions,
|
||||
cancelButtonOptions,
|
||||
withoutCancelButton,
|
||||
contentOptions = defaultContentOptions,
|
||||
overlayOptions = defaultOverlayOptions,
|
||||
// FIXME: we need i18n
|
||||
cancelText = 'Cancel',
|
||||
confirmText = 'Confirm',
|
||||
width = 400,
|
||||
}: OverlayModalProps) {
|
||||
const handleConfirm = useCallback(() => {
|
||||
onOpenChange?.(false);
|
||||
onConfirm?.();
|
||||
}, [onOpenChange, onConfirm]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
contentOptions={contentOptions}
|
||||
overlayOptions={overlayOptions}
|
||||
open={open}
|
||||
width={width}
|
||||
onOpenChange={onOpenChange}
|
||||
withoutCloseButton
|
||||
>
|
||||
{topImage}
|
||||
<div className={styles.title}>{title}</div>
|
||||
<div className={styles.content}>{description}</div>
|
||||
<div className={styles.footer}>
|
||||
{!withoutCancelButton ? (
|
||||
<DialogTrigger asChild>
|
||||
<Button {...cancelButtonOptions}>{cancelText}</Button>
|
||||
</DialogTrigger>
|
||||
) : null}
|
||||
|
||||
{to ? (
|
||||
external ? (
|
||||
//FIXME: we need a more standardized way to implement this link with other click events
|
||||
<a href={to} target="_blank" rel="noreferrer">
|
||||
<Button onClick={handleConfirm} {...confirmButtonOptions}>
|
||||
{confirmText}
|
||||
</Button>
|
||||
</a>
|
||||
) : (
|
||||
<Link to={to}>
|
||||
<Button onClick={handleConfirm} {...confirmButtonOptions}>
|
||||
{confirmText}
|
||||
</Button>
|
||||
</Link>
|
||||
)
|
||||
) : (
|
||||
<Button onClick={handleConfirm} {...confirmButtonOptions}>
|
||||
{confirmText}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user