diff --git a/package.json b/package.json index 13bcb96305..7dc45c1f01 100644 --- a/package.json +++ b/package.json @@ -112,6 +112,7 @@ "lottie-react": "^2.4.1", "marked": "^17.0.1", "moize": "^6.1.7", + "motion": "^12.23.26", "murmurhash3js": "^3.0.1", "nanoid": "^5.1.6", "openai": "^5.20.3", @@ -127,6 +128,7 @@ "react-dom": "^19.2.3", "react-error-boundary": "^6.0.2", "react-gravatar": "^2.6.3", + "react-modal-sheet": "^5.2.1", "react-native-web": "^0.21.2", "react-redux": "^9.2.0", "react-signature-pad-wrapper": "^4.2.0", diff --git a/src/components/AppComponent.tsx b/src/components/AppComponent.tsx index 07bf21c271..0c10714a78 100644 --- a/src/components/AppComponent.tsx +++ b/src/components/AppComponent.tsx @@ -1,7 +1,7 @@ import { Capacitor } from '@capacitor/core' import { StatusBar, Style } from '@capacitor/status-bar' import _ from 'lodash' -import React, { FC, PropsWithChildren, useEffect, useLayoutEffect } from 'react' +import React, { FC, PropsWithChildren, useEffect, useLayoutEffect, useRef } from 'react' import { useSelector } from 'react-redux' import { WebviewBackground } from 'webview-background' import { css } from '../../styled-system/css' @@ -115,6 +115,7 @@ const AppComponent: FC = () => { const fontSize = useSelector(state => state.fontSize) const showModal = useSelector(state => state.showModal) const tutorial = useSelector(isTutorial) + const rootRef = useRef(null) useEffect(() => { WebviewBackground.changeBackgroundColor({ color: colors.bg }) @@ -171,6 +172,7 @@ const AppComponent: FC = () => { /* safeAreaTop applies for rounded screens */ paddingTop: 'safeAreaTop', })} + ref={rootRef} > @@ -215,7 +217,7 @@ const AppComponent: FC = () => { {/* NavBar must be outside MultiGestureIfTouch in order to have a higher stacking order than the Sidebar. Otherwise the user can accidentally activate the Sidebar edge swipe when trying to tap the Home icon. */} - +
diff --git a/src/components/CommandCenter/CommandCenter.tsx b/src/components/CommandCenter/CommandCenter.tsx index 35c4d000da..4069b2bb02 100644 --- a/src/components/CommandCenter/CommandCenter.tsx +++ b/src/components/CommandCenter/CommandCenter.tsx @@ -1,10 +1,11 @@ -import SwipeableDrawer from '@mui/material/SwipeableDrawer' import _ from 'lodash' +import { cubicBezier, useTransform } from 'motion/react' +import { motion } from 'motion/react' import pluralize from 'pluralize' import { FC, useCallback, useRef } from 'react' +import { Sheet, SheetProps, SheetRef, SheetTweenConfig } from 'react-modal-sheet' import { useDispatch, useSelector } from 'react-redux' import { css } from '../../../styled-system/css' -import { token } from '../../../styled-system/tokens' import { clearMulticursorsActionCreator as clearMulticursors } from '../../actions/clearMulticursors' import { toggleDropdownActionCreator as toggleDropdown } from '../../actions/toggleDropdown' import { isTouch } from '../../browser' @@ -17,10 +18,9 @@ import note from '../../commands/note' import outdent from '../../commands/outdent' import swapParent from '../../commands/swapParent' import uncategorize from '../../commands/uncategorize' +import durationsConfig from '../../durations.config' import isTutorial from '../../selectors/isTutorial' -import durations from '../../util/durations' import fastClick from '../../util/fastClick' -import FadeTransition from '../FadeTransition' import PanelCommand from './PanelCommand' import PanelCommandGroup from './PanelCommandGroup' @@ -61,31 +61,6 @@ const MultiselectMessage: FC = () => { ) } -/** Command center gradient overlay. Fades in when the Command Center opens. */ -const Overlay = () => { - const showCommandCenter = useSelector(state => state.showCommandCenter) - const ref = useRef(null) - return ( - -
- - ) -} - /** * A hidden pre-rendered overlay on mobile, used as a workaround for the * Command Center flicker caused by the overlay background only being loaded @@ -102,17 +77,43 @@ const HiddenOverlay = () => { ) } +const bezierDefinition = [0, 0, 0.2, 1] as const + +const ease = cubicBezier(...bezierDefinition) + +const tweenConfig: SheetTweenConfig = { + duration: durationsConfig.medium / 1000, + ease: bezierDefinition, +} + /** * A panel that displays the Command Center. */ -const CommandCenter = () => { +const CommandCenter = ({ mountPoint }: Pick) => { const dispatch = useDispatch() const showCommandCenter = useSelector(state => state.showCommandCenter) const isTutorialOn = useSelector(isTutorial) + const ref = useRef(null) - const onOpen = useCallback(() => { - dispatch(toggleDropdown({ dropDownType: 'commandCenter', value: true })) - }, [dispatch]) + const height = useTransform(() => { + return ref.current?.yInverted.get() ?? 0 + }) + + const blurHeight = useTransform(height, height => height + 110) + + const sheetProgress = useTransform(() => { + const y = ref.current?.yInverted.get() ?? 0 + const height = ref.current?.height ?? 0 + if (height === 0) return 0 + return Math.min(Math.max(y / height, 0), 1) + }) + + const opacity = useTransform(sheetProgress, [0, 1], [0, 1], { ease }) + + const bottom = useTransform(() => { + const y = ref.current?.y.get() ?? 0 + return -y + }) const onClose = useCallback(() => { dispatch([toggleDropdown({ dropDownType: 'commandCenter', value: false }), clearMulticursors()]) @@ -121,23 +122,67 @@ const CommandCenter = () => { if (isTouch && !isTutorialOn) { return ( <> + - + + + { maxHeight: '70%', pointerEvents: 'auto', boxShadow: 'none', - }, - }} - ModalProps={{ - disableAutoFocus: true, - disableEnforceFocus: true, - disableRestoreFocus: true, - style: { - pointerEvents: 'none', - zIndex: token('zIndex.modal'), - backgroundColor: 'transparent', - }, - }} - > -
-
-
- - -
-
+
- +
+ +
+
+
+ + + + + + + + + + +
-
- - - - - - - - - - - -
-
-
- + + + ) } diff --git a/src/components/CommandCenter/PanelCommand.tsx b/src/components/CommandCenter/PanelCommand.tsx index 05545084e4..b184de8567 100644 --- a/src/components/CommandCenter/PanelCommand.tsx +++ b/src/components/CommandCenter/PanelCommand.tsx @@ -32,6 +32,8 @@ const ActiveButtonGlowImage: FC = ({ isActive, type in={isActive} unmountOnExit nodeRef={nodeRef} + /** When `in={true}` on mount, ensures the initial opacity of the element is correct. */ + appear >
= ({ command, size }) => { : { gridColumn: 'span 1', gridTemplateColumns: 'auto', gridTemplateAreas: `"command"` }), })} > + {/* For the first fade in to work properly, ActiveButtonGlowImage must be already mounted with opacity 0. */}
{ // wait for the command center panel to appear before taking screenshot await waitForSelector('[data-testid=command-center-panel]') + // wait for the command center panel to slide in fully before taking screenshot + await sleep(durationsConfig.medium) + expect(await screenshot()).toMatchImageSnapshot() }) diff --git a/src/recipes/fadeTransition.ts b/src/recipes/fadeTransition.ts index 6608b786d5..5629400d22 100644 --- a/src/recipes/fadeTransition.ts +++ b/src/recipes/fadeTransition.ts @@ -6,7 +6,17 @@ import { defineSlotRecipe } from '@pandacss/dev' const fadeTransitionRecipe = defineSlotRecipe({ className: 'fade', - slots: ['enter', 'exit', 'exitActive', 'enterActive', 'enterDone', 'exitDone'], + slots: [ + 'enter', + 'exit', + 'exitActive', + 'enterActive', + 'enterDone', + 'exitDone', + 'appear', + 'appearActive', + 'appearDone', + ], base: { enter: { opacity: 0 }, enterActive: { opacity: 1 }, @@ -29,11 +39,6 @@ const fadeTransitionRecipe = defineSlotRecipe({ enterActive: { transition: `opacity {durations.medium} ease 0ms` }, exitActive: { transition: `opacity {durations.medium} ease 0ms` }, }, - commandCenterDrawer: { - // Easing follows that of Material UI SwipeableDrawer. - enterActive: { transition: `opacity {durations.commandCenterDrawer} cubic-bezier(0, 0, 0.2, 1) 0ms` }, - exitActive: { transition: `opacity {durations.commandCenterDrawer} cubic-bezier(0.4, 0, 0.2, 1) 0ms` }, - }, activeButtonGlowLuminosity: { enter: { opacity: 0 }, enterActive: { opacity: 0.75, transition: `opacity {durations.activeButtonGlowLuminosity} ease 0ms` }, @@ -41,6 +46,9 @@ const fadeTransitionRecipe = defineSlotRecipe({ exit: { opacity: 0.75 }, exitActive: { opacity: 0, transition: `opacity {durations.activeButtonGlowLuminosity} ease 0ms` }, exitDone: { opacity: 0 }, + appear: { opacity: 0 }, + appearActive: { opacity: 0.75, transition: `opacity {durations.activeButtonGlowLuminosity} ease 0ms` }, + appearDone: { opacity: 0.75 }, }, activeButtonGlowSaturation: { enter: { opacity: 0 }, @@ -49,6 +57,9 @@ const fadeTransitionRecipe = defineSlotRecipe({ exit: { opacity: 0.45 }, exitActive: { opacity: 0, transition: `opacity {durations.activeButtonGlowSaturation} ease 0ms` }, exitDone: { opacity: 0 }, + appear: { opacity: 0 }, + appearActive: { opacity: 0.45, transition: `opacity {durations.activeButtonGlowSaturation} ease 0ms` }, + appearDone: { opacity: 0.45 }, }, distractionFreeTyping: { enterActive: { transition: `opacity {durations.distractionFreeTyping} ease 0ms` }, diff --git a/yarn.lock b/yarn.lock index b306eea277..d2375c0329 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9941,6 +9941,7 @@ __metadata: lottie-react: "npm:^2.4.1" marked: "npm:^17.0.1" moize: "npm:^6.1.7" + motion: "npm:^12.23.26" murmurhash3js: "npm:^3.0.1" nanoid: "npm:^5.1.6" npm-run-all2: "npm:^8.0.4" @@ -9962,6 +9963,7 @@ __metadata: react-dom: "npm:^19.2.3" react-error-boundary: "npm:^6.0.2" react-gravatar: "npm:^2.6.3" + react-modal-sheet: "npm:^5.2.1" react-native-web: "npm:^0.21.2" react-redux: "npm:^9.2.0" react-signature-pad-wrapper: "npm:^4.2.0" @@ -11884,6 +11886,28 @@ __metadata: languageName: node linkType: hard +"framer-motion@npm:^12.23.26": + version: 12.23.26 + resolution: "framer-motion@npm:12.23.26" + dependencies: + motion-dom: "npm:^12.23.23" + motion-utils: "npm:^12.23.6" + tslib: "npm:^2.4.0" + peerDependencies: + "@emotion/is-prop-valid": "*" + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + "@emotion/is-prop-valid": + optional: true + react: + optional: true + react-dom: + optional: true + checksum: 10c0/7dbbc4a392b969804e04a056e3d89a55bf31572dc7f6cd79050b90616fbb84a1762e4ac4e2537c735d347bbf4ceebea126f5c090234149b12cffa3ea6c518a34 + languageName: node + linkType: hard + "fresh@npm:0.5.2": version: 0.5.2 resolution: "fresh@npm:0.5.2" @@ -16267,6 +16291,43 @@ __metadata: languageName: node linkType: hard +"motion-dom@npm:^12.23.23": + version: 12.23.23 + resolution: "motion-dom@npm:12.23.23" + dependencies: + motion-utils: "npm:^12.23.6" + checksum: 10c0/139705731085063519b88f23fcc5b1c13e15707a4ff3365da02ef9a4bf2a2d8ebed9a151c57e7f215ccd9e822365d93c16e28e619fbf25611f61dcff5ee81d75 + languageName: node + linkType: hard + +"motion-utils@npm:^12.23.6": + version: 12.23.6 + resolution: "motion-utils@npm:12.23.6" + checksum: 10c0/c058e8ba6423b3baa63e985bcc669877ee7d9579d938f5348b4e60c5ea1b4b33dd7f4877434436a4a5807f3cf00370d3fd4079a6fdd6309c5c87aa62b311a897 + languageName: node + linkType: hard + +"motion@npm:^12.23.26": + version: 12.23.26 + resolution: "motion@npm:12.23.26" + dependencies: + framer-motion: "npm:^12.23.26" + tslib: "npm:^2.4.0" + peerDependencies: + "@emotion/is-prop-valid": "*" + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + "@emotion/is-prop-valid": + optional: true + react: + optional: true + react-dom: + optional: true + checksum: 10c0/80ac9c2caaa149fb6feaad4146294b304dda52a643360063eb7a672bf7e7c8e134ec201ddcfafc0a918113004aed939ddbacf546b0e2297ea23955c78235c497 + languageName: node + linkType: hard + "mrmime@npm:^2.0.0": version: 2.0.1 resolution: "mrmime@npm:2.0.1" @@ -18599,6 +18660,18 @@ __metadata: languageName: node linkType: hard +"react-modal-sheet@npm:^5.2.1": + version: 5.2.1 + resolution: "react-modal-sheet@npm:5.2.1" + dependencies: + react-use-measure: "npm:2.1.7" + peerDependencies: + motion: ">=11" + react: ">=16" + checksum: 10c0/b622d670e83d3035b3b798564143a7f7cf34987e66c800837d6dc784a1d3afb606c7e67d89ea92bca65c73fa286a0e6bc076ce6d0eb1dc64b91d95f2e85ffe97 + languageName: node + linkType: hard + "react-native-web@npm:^0.21.2": version: 0.21.2 resolution: "react-native-web@npm:0.21.2" @@ -18734,6 +18807,19 @@ __metadata: languageName: node linkType: hard +"react-use-measure@npm:2.1.7": + version: 2.1.7 + resolution: "react-use-measure@npm:2.1.7" + peerDependencies: + react: ">=16.13" + react-dom: ">=16.13" + peerDependenciesMeta: + react-dom: + optional: true + checksum: 10c0/ff24130e6f95e853feb6892fb74af08dbc5aae3574b701169e3bc3adb392c3162f51a58ddfe39bb7337db13ae609bbec0bb51a9de8b5fae5420f9d17e1f8b542 + languageName: node + linkType: hard + "react@npm:^19.2.3": version: 19.2.3 resolution: "react@npm:19.2.3"