diff --git a/apps/electron/tests/basic.spec.ts b/apps/electron/tests/basic.spec.ts index 5336cea91d..c7bde31b83 100644 --- a/apps/electron/tests/basic.spec.ts +++ b/apps/electron/tests/basic.spec.ts @@ -16,6 +16,9 @@ test.beforeEach(async () => { colorScheme: 'light', }); page = await electronApp.firstWindow(); + await page.getByTestId('onboarding-modal-close-button').click({ + delay: 100, + }); // cleanup page data await page.evaluate(() => localStorage.clear()); }); @@ -76,3 +79,21 @@ test('affine cloud disabled', async () => { state: 'visible', }); }); +test('affine onboarding button', async () => { + await page.getByTestId('help-island').click(); + await page.getByTestId('easy-guide').click(); + const onboardingModal = page.locator('[data-testid=onboarding-modal]'); + expect(await onboardingModal.isVisible()).toEqual(true); + const switchVideo = page.locator( + '[data-testid=onboarding-modal-switch-video]' + ); + expect(await switchVideo.isVisible()).toEqual(true); + await page.getByTestId('onboarding-modal-next-button').click(); + const editingVideo = page.locator( + '[data-testid=onboarding-modal-editing-video]' + ); + expect(await editingVideo.isVisible()).toEqual(true); + await page.getByTestId('onboarding-modal-ok-button').click(); + + expect(await onboardingModal.isVisible()).toEqual(false); +}); diff --git a/apps/web/public/editingVideo.mp4 b/apps/web/public/editingVideo.mp4 new file mode 100644 index 0000000000..12fa935e4f Binary files /dev/null and b/apps/web/public/editingVideo.mp4 differ diff --git a/apps/web/public/switchVideo.mp4 b/apps/web/public/switchVideo.mp4 new file mode 100644 index 0000000000..14452bf78f Binary files /dev/null and b/apps/web/public/switchVideo.mp4 differ diff --git a/apps/web/src/atoms/guide.ts b/apps/web/src/atoms/guide.ts index 9b872b3e42..305438b8ef 100644 --- a/apps/web/src/atoms/guide.ts +++ b/apps/web/src/atoms/guide.ts @@ -52,3 +52,18 @@ export const guideChangeLogAtom = atom< })); } ); +export const guideOnboardingAtom = atom< + Guide['onBoarding'], + [open: boolean], + void +>( + get => { + return get(guidePrimitiveAtom).onBoarding; + }, + (_, set, open) => { + set(guidePrimitiveAtom, tips => ({ + ...tips, + onBoarding: open, + })); + } +); diff --git a/apps/web/src/atoms/index.ts b/apps/web/src/atoms/index.ts index 176563fd6a..950476f9b5 100644 --- a/apps/web/src/atoms/index.ts +++ b/apps/web/src/atoms/index.ts @@ -77,6 +77,7 @@ export const currentEditorAtom = rootCurrentEditorAtom; export const openWorkspacesModalAtom = atom(false); export const openCreateWorkspaceModalAtom = atom(false); export const openQuickSearchModalAtom = atom(false); +export const openOnboardingModalAtom = atom(false); export const openDisableCloudAlertModalAtom = atom(false); diff --git a/apps/web/src/components/blocksuite/workspace-header/header-right-items/EditorOptionMenu.tsx b/apps/web/src/components/blocksuite/workspace-header/header-right-items/EditorOptionMenu.tsx index fba8e276b2..d30745b67e 100644 --- a/apps/web/src/components/blocksuite/workspace-header/header-right-items/EditorOptionMenu.tsx +++ b/apps/web/src/components/blocksuite/workspace-header/header-right-items/EditorOptionMenu.tsx @@ -45,7 +45,7 @@ const CommonMenu = () => { diff --git a/apps/web/src/components/blocksuite/workspace-header/styles.ts b/apps/web/src/components/blocksuite/workspace-header/styles.ts index d2f5c12561..cca9462387 100644 --- a/apps/web/src/components/blocksuite/workspace-header/styles.ts +++ b/apps/web/src/components/blocksuite/workspace-header/styles.ts @@ -15,7 +15,7 @@ export const StyledHeaderContainer = styled('div')<{ top: 0, background: 'var(--affine-background-primary-color)', WebkitAppRegion: 'drag', - zIndex: 1, + zIndex: 'var(--affine-z-index-popover)', '@media (max-width: 768px)': { '&[data-open="true"]': { WebkitAppRegion: 'no-drag', diff --git a/apps/web/src/components/pure/OnboardingModal.tsx b/apps/web/src/components/pure/OnboardingModal.tsx new file mode 100644 index 0000000000..9accbb2066 --- /dev/null +++ b/apps/web/src/components/pure/OnboardingModal.tsx @@ -0,0 +1,45 @@ +import { TourModal } from '@affine/component/tour-modal'; +import { useAtom } from 'jotai'; +import { useCallback, useEffect, useMemo } from 'react'; + +import { openOnboardingModalAtom } from '../../atoms'; +import { guideOnboardingAtom } from '../../atoms/guide'; + +type OnboardingModalProps = { + onClose: () => void; + open: boolean; +}; + +const getHelperGuide = (): { onBoarding: boolean } | null => { + const helperGuide = localStorage.getItem('helper-guide'); + if (helperGuide) { + return JSON.parse(helperGuide); + } + return null; +}; + +export const OnboardingModal: React.FC = ({ + open, + onClose, +}) => { + const [, setShowOnboarding] = useAtom(guideOnboardingAtom); + const [, setOpenOnboarding] = useAtom(openOnboardingModalAtom); + const onCloseTourModal = useCallback(() => { + setShowOnboarding(false); + onClose(); + }, [onClose, setShowOnboarding]); + + const shouldShow = useMemo(() => { + const helperGuide = getHelperGuide(); + return helperGuide?.onBoarding ?? true; + }, []); + + useEffect(() => { + if (shouldShow) { + setOpenOnboarding(true); + } + }, [shouldShow, setOpenOnboarding]); + return ; +}; + +export default OnboardingModal; diff --git a/apps/web/src/components/pure/help-island/index.tsx b/apps/web/src/components/pure/help-island/index.tsx index befb63f350..dda4f22631 100644 --- a/apps/web/src/components/pure/help-island/index.tsx +++ b/apps/web/src/components/pure/help-island/index.tsx @@ -1,8 +1,11 @@ import { MuiFade, Tooltip } from '@affine/component'; +import { getEnvironment } from '@affine/env'; import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { CloseIcon, NewIcon } from '@blocksuite/icons'; +import { useAtom } from 'jotai'; import { lazy, Suspense, useState } from 'react'; +import { openOnboardingModalAtom } from '../../../atoms'; import { ShortcutsModal } from '../shortcuts-modal'; import { ContactIcon, HelpIcon, KeyboardIcon } from './Icons'; import { @@ -11,19 +14,25 @@ import { StyledIsland, StyledTriggerWrapper, } from './style'; - +const env = getEnvironment(); const ContactModal = lazy(() => import('@affine/component/contact-modal').then(({ ContactModal }) => ({ default: ContactModal, })) ); - -export type IslandItemNames = 'whatNew' | 'contact' | 'shortcuts'; +const DEFAULT_SHOW_LIST: IslandItemNames[] = [ + 'whatNew', + 'contact', + 'shortcuts', +]; +const DESKTOP_SHOW_LIST: IslandItemNames[] = [...DEFAULT_SHOW_LIST, 'guide']; +export type IslandItemNames = 'whatNew' | 'contact' | 'shortcuts' | 'guide'; export const HelpIsland = ({ - showList = ['whatNew', 'contact', 'shortcuts'], + showList = env.isDesktop ? DESKTOP_SHOW_LIST : DEFAULT_SHOW_LIST, }: { showList?: IslandItemNames[]; }) => { + const [, setOpenOnboarding] = useAtom(openOnboardingModalAtom); const [spread, setShowSpread] = useState(false); // const { triggerShortcutsModal, triggerContactModal } = useModal(); // const blockHub = useGlobalState(store => store.blockHub); @@ -98,6 +107,19 @@ export const HelpIsland = ({ )} + {showList.includes('guide') && ( + + { + setShowSpread(false); + setOpenOnboarding(true); + }} + > + + + + )} diff --git a/apps/web/src/providers/ModalProvider.tsx b/apps/web/src/providers/ModalProvider.tsx index 4172292ab2..31a276ba0c 100644 --- a/apps/web/src/providers/ModalProvider.tsx +++ b/apps/web/src/providers/ModalProvider.tsx @@ -1,3 +1,4 @@ +import { getEnvironment } from '@affine/env'; import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom'; import { arrayMove } from '@dnd-kit/sortable'; import { useAtom, useAtomValue, useSetAtom } from 'jotai'; @@ -9,6 +10,7 @@ import { currentWorkspaceIdAtom, openCreateWorkspaceModalAtom, openDisableCloudAlertModalAtom, + openOnboardingModalAtom, openWorkspacesModalAtom, } from '../atoms'; import { useAffineLogIn } from '../hooks/affine/use-affine-log-in'; @@ -37,6 +39,11 @@ const TmpDisableAffineCloudModal = lazy(() => }) ) ); +const OnboardingModalAtom = lazy(() => + import('../components/pure/OnboardingModal').then(module => ({ + default: module.OnboardingModal, + })) +); export function Modals() { const [openWorkspacesModal, setOpenWorkspacesModal] = useAtom( @@ -49,6 +56,9 @@ export function Modals() { const [openDisableCloudAlertModal, setOpenDisableCloudAlertModal] = useAtom( openDisableCloudAlertModalAtom ); + const [openOnboardingModal, setOpenOnboardingModal] = useAtom( + openOnboardingModalAtom + ); const router = useRouter(); const { jumpToSubPath } = useRouterHelper(router); @@ -59,7 +69,10 @@ export function Modals() { const [, setCurrentWorkspace] = useCurrentWorkspace(); const { createLocalWorkspace } = useAppHelper(); const [transitioning, transition] = useTransition(); - + const env = getEnvironment(); + const onCloseOnboardingModal = useCallback(() => { + setOpenOnboardingModal(false); + }, [setOpenOnboardingModal]); return ( <> @@ -70,6 +83,15 @@ export function Modals() { }, [setOpenDisableCloudAlertModal])} /> + {env.isDesktop && ( + + + + )} + void; +}; + +export const TourModal: FC = ({ open, onClose }) => { + const [step, setStep] = useState(0); + const handleClose = () => { + setStep(0); + onClose(); + }; + return ( + + + + {step === 0 && ( +
+
Hyper merged whiteboard and docs
+
+ +
+
+ +
+
+ )} + {step === 1 && ( +
+
+ Intuitive & robust block-based editing +
+
+ +
+
+ +
+
+ )} +
+
+ ); +}; + +export default TourModal; diff --git a/packages/component/src/components/tour-modal/index.css.ts b/packages/component/src/components/tour-modal/index.css.ts new file mode 100644 index 0000000000..10020b11b7 --- /dev/null +++ b/packages/component/src/components/tour-modal/index.css.ts @@ -0,0 +1,44 @@ +import { style } from '@vanilla-extract/css'; + +export const modalStyle = style({ + width: '100%', + height: '100%', + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + position: 'relative', + padding: '12px 36px', + backgroundColor: 'var(--affine-white)', + borderRadius: '12px', + boxShadow: 'var(--affine-popover-shadow)', +}); +export const titleStyle = style({ + fontSize: 'var(--affine-font-h6)', + fontWeight: '600', +}); +export const videoContainerStyle = style({ + paddingTop: '15px', + width: '100%', +}); +export const videoStyle = style({ + objectFit: 'fill', + height: '300px', + width: '100%', +}); +export const buttonContainerStyle = style({ + marginTop: '15px', + width: '100%', + display: 'flex', + justifyContent: 'flex-end', +}); +export const buttonStyle = style({ + borderRadius: '8px', + backgroundColor: 'var(--affine-primary-color)', + color: 'var(--affine-white)', + height: '32px', + padding: '4 20px', + ':hover': { + backgroundColor: 'var(--affine-primary-color)', + color: 'var(--affine-text-primary-color)', + }, +}); diff --git a/packages/component/src/components/tour-modal/index.ts b/packages/component/src/components/tour-modal/index.ts new file mode 100644 index 0000000000..fd3c9c318d --- /dev/null +++ b/packages/component/src/components/tour-modal/index.ts @@ -0,0 +1 @@ +export * from './TourModal'; diff --git a/packages/component/src/stories/OnboardingModal.stories.tsx b/packages/component/src/stories/OnboardingModal.stories.tsx new file mode 100644 index 0000000000..e3d158b428 --- /dev/null +++ b/packages/component/src/stories/OnboardingModal.stories.tsx @@ -0,0 +1,16 @@ +/* deepscan-disable USELESS_ARROW_FUNC_BIND */ +import type { StoryFn } from '@storybook/react'; + +import { TourModal } from '../components/tour-modal'; + +export default { + title: 'AFFiNE/TourModal', + component: TourModal, +}; + +export const Basic: StoryFn = () => { + return {}} />; +}; +Basic.args = { + logoSrc: '/imgs/affine-text-logo.png', +};