diff --git a/packages/frontend/core/src/components/affine/setting-modal/atoms.ts b/packages/frontend/core/src/components/affine/setting-modal/atoms.ts new file mode 100644 index 0000000000..a740fac2a2 --- /dev/null +++ b/packages/frontend/core/src/components/affine/setting-modal/atoms.ts @@ -0,0 +1,3 @@ +import { atom } from 'jotai'; + +export const settingModalScrollContainerAtom = atom(null); diff --git a/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/index.tsx b/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/index.tsx index ab2a22eb7f..b3a8643088 100644 --- a/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/index.tsx +++ b/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/index.tsx @@ -190,6 +190,7 @@ const Settings = () => { return ( { @@ -70,12 +78,32 @@ const PricingCollapsible = ({ export interface PlanLayoutProps { cloud?: ReactNode; ai?: ReactNode; + aiTip?: boolean; } -export const PlanLayout = ({ cloud, ai }: PlanLayoutProps) => { +export const PlanLayout = ({ cloud, ai, aiTip }: PlanLayoutProps) => { const t = useAFFiNEI18N(); const [{ scrollAnchor }, setOpenSettingModal] = useAtom(openSettingModalAtom); const aiPricingPlanRef = useRef(null); + const aiScrollTipRef = useRef(null); + const settingModalScrollContainer = useAtomValue( + settingModalScrollContainerAtom + ); + + const updateAiTipState = useCallback(() => { + if (!aiTip) return; + const aiContainer = aiPricingPlanRef.current; + if (!settingModalScrollContainer || !aiContainer) return; + + const minVisibleHeight = 30; + + const containerRect = settingModalScrollContainer.getBoundingClientRect(); + const aiTop = aiContainer.getBoundingClientRect().top - containerRect.top; + const aiIntoView = aiTop < containerRect.height - minVisibleHeight; + if (aiIntoView) { + settingModalScrollContainer.dataset.aiVisible = ''; + } + }, [aiTip, settingModalScrollContainer]); // TODO: Need a better solution to handle this situation useLayoutEffect(() => { @@ -88,6 +116,23 @@ export const PlanLayout = ({ cloud, ai }: PlanLayoutProps) => { }); }, [scrollAnchor, setOpenSettingModal]); + useEffect(() => { + if (!settingModalScrollContainer || !aiScrollTipRef.current) return; + + settingModalScrollContainer.addEventListener('scroll', updateAiTipState); + updateAiTipState(); + return () => { + settingModalScrollContainer.removeEventListener( + 'scroll', + updateAiTipState + ); + }; + }, [settingModalScrollContainer, updateAiTipState]); + + const scrollAiIntoView = useCallback(() => { + aiPricingPlanRef.current?.scrollIntoView({ behavior: 'smooth' }); + }, []); + return (
{/* TODO: SettingHeader component shouldn't have margin itself */} @@ -98,12 +143,35 @@ export const PlanLayout = ({ cloud, ai }: PlanLayoutProps) => { {cloud} {ai ? ( <> - +
{ai}
) : null} + + {aiTip && settingModalScrollContainer + ? createPortal( +
+
+ +
Meet AFFiNE AI
+
+
NEW
+
+
+ +
, + settingModalScrollContainer, + 'aiScrollTip' + ) + : null}
); }; diff --git a/packages/frontend/core/src/components/affine/setting-modal/index.tsx b/packages/frontend/core/src/components/affine/setting-modal/index.tsx index d2bf8acd98..048ed26f90 100644 --- a/packages/frontend/core/src/components/affine/setting-modal/index.tsx +++ b/packages/frontend/core/src/components/affine/setting-modal/index.tsx @@ -16,9 +16,16 @@ import { } from '@toeverything/infra'; import { useSetAtom } from 'jotai'; import { debounce } from 'lodash-es'; -import { Suspense, useCallback, useLayoutEffect, useRef } from 'react'; +import { + Suspense, + useCallback, + useEffect, + useLayoutEffect, + useRef, +} from 'react'; import { AccountSetting } from './account-setting'; +import { settingModalScrollContainerAtom } from './atoms'; import { GeneralSetting } from './general-setting'; import { SettingSidebar } from './setting-sidebar'; import * as style from './style.css'; @@ -56,6 +63,9 @@ const SettingModalInner = ({ const modalContentRef = useRef(null); const modalContentWrapperRef = useRef(null); + const setSettingModalScrollContainer = useSetAtom( + settingModalScrollContainerAtom + ); useLayoutEffect(() => { if (!modalProps.open) return; @@ -66,13 +76,24 @@ const SettingModalInner = ({ if (!modalContentRef.current || !modalContentWrapperRef.current) return; const wrapperWidth = modalContentWrapperRef.current.offsetWidth; + const wrapperHeight = modalContentWrapperRef.current.offsetHeight; const contentWidth = modalContentRef.current.offsetWidth; - modalContentRef.current?.style.setProperty( + const wrapper = modalContentWrapperRef.current; + + wrapper?.style.setProperty( '--setting-modal-width', `${wrapperWidth}px` ); - modalContentRef.current?.style.setProperty( + wrapper?.style.setProperty( + '--setting-modal-height', + `${wrapperHeight}px` + ); + wrapper?.style.setProperty( + '--setting-modal-content-width', + `${contentWidth}px` + ); + wrapper?.style.setProperty( '--setting-modal-gap-x', `${(wrapperWidth - contentWidth) / 2}px` ); @@ -87,6 +108,13 @@ const SettingModalInner = ({ }; }, [modalProps.open]); + useEffect(() => { + setSettingModalScrollContainer(modalContentWrapperRef.current); + return () => { + setSettingModalScrollContainer(null); + }; + }, [setSettingModalScrollContainer]); + const onTabChange = useCallback( (key: ActiveTab, meta: WorkspaceMetadata | null) => { onSettingClick({ activeTab: key, workspaceMetadata: meta });