Browse Source

Merge pull request #10839 from growilabs/imprv/staff-credit

imprv: Staff credit
Yuki Takei 2 weeks ago
parent
commit
837f238fd9

+ 1 - 2
apps/app/package.json

@@ -197,6 +197,7 @@
     "mongoose-gridfs": "^1.3.0",
     "mongoose-gridfs": "^1.3.0",
     "mongoose-paginate-v2": "^1.3.9",
     "mongoose-paginate-v2": "^1.3.9",
     "mongoose-unique-validator": "^2.0.3",
     "mongoose-unique-validator": "^2.0.3",
+    "motion": "^12.35.0",
     "multer": "~1.4.0",
     "multer": "~1.4.0",
     "multer-autoreap": "^1.0.3",
     "multer-autoreap": "^1.0.3",
     "mustache": "^4.2.0",
     "mustache": "^4.2.0",
@@ -240,7 +241,6 @@
     "react-input-autosize": "^3.0.0",
     "react-input-autosize": "^3.0.0",
     "react-markdown": "^9.0.1",
     "react-markdown": "^9.0.1",
     "react-multiline-clamp": "^2.0.0",
     "react-multiline-clamp": "^2.0.0",
-    "react-scroll": "^1.8.7",
     "react-stickynode": "^4.1.1",
     "react-stickynode": "^4.1.1",
     "react-syntax-highlighter": "^16.1.0",
     "react-syntax-highlighter": "^16.1.0",
     "react-toastify": "^9.1.3",
     "react-toastify": "^9.1.3",
@@ -321,7 +321,6 @@
     "@types/react": "^18.2.14",
     "@types/react": "^18.2.14",
     "@types/react-dom": "^18.2.6",
     "@types/react-dom": "^18.2.6",
     "@types/react-input-autosize": "^2.2.4",
     "@types/react-input-autosize": "^2.2.4",
-    "@types/react-scroll": "^1.8.4",
     "@types/react-stickynode": "^4.0.3",
     "@types/react-stickynode": "^4.0.3",
     "@types/supertest": "^6.0.3",
     "@types/supertest": "^6.0.3",
     "@types/testing-library__dom": "^7.5.0",
     "@types/testing-library__dom": "^7.5.0",

+ 28 - 28
apps/app/src/client/components/ContentLinkButtons.tsx

@@ -1,7 +1,8 @@
 import React, { type JSX } from 'react';
 import React, { type JSX } from 'react';
 import { type IUserHasId, USER_STATUS } from '@growi/core';
 import { type IUserHasId, USER_STATUS } from '@growi/core';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
-import { Link as ScrollLink } from 'react-scroll';
+
+import { scrollToElement } from '~/client/util/smooth-scroll';
 
 
 import {
 import {
   BOOKMARKS_LIST_ID,
   BOOKMARKS_LIST_ID,
@@ -12,15 +13,14 @@ import {
 const BookMarkLinkButton = React.memo(() => {
 const BookMarkLinkButton = React.memo(() => {
   const { t } = useTranslation();
   const { t } = useTranslation();
   return (
   return (
-    <ScrollLink to={BOOKMARKS_LIST_ID} offset={-120}>
-      <button
-        type="button"
-        className="btn btn-sm btn-outline-neutral-secondary rounded-pill d-flex align-items-center w-100 px-3"
-      >
-        <span className="material-symbols-outlined p-0 me-2">bookmark</span>
-        <span>{t('user_home_page.bookmarks')}</span>
-      </button>
-    </ScrollLink>
+    <button
+      type="button"
+      className="btn btn-sm btn-outline-neutral-secondary rounded-pill d-flex align-items-center w-100 px-3"
+      onClick={() => scrollToElement(BOOKMARKS_LIST_ID, { offset: -120 })}
+    >
+      <span className="material-symbols-outlined p-0 me-2">bookmark</span>
+      <span>{t('user_home_page.bookmarks')}</span>
+    </button>
   );
   );
 });
 });
 
 
@@ -29,15 +29,16 @@ BookMarkLinkButton.displayName = 'BookMarkLinkButton';
 const RecentlyCreatedLinkButton = React.memo(() => {
 const RecentlyCreatedLinkButton = React.memo(() => {
   const { t } = useTranslation();
   const { t } = useTranslation();
   return (
   return (
-    <ScrollLink to={RECENTLY_CREATED_LIST_ID} offset={-120}>
-      <button
-        type="button"
-        className="btn btn-sm btn-outline-neutral-secondary rounded-pill d-flex align-items-center w-100 px-3"
-      >
-        <span className="growi-custom-icons mx-2 ">recently_created</span>
-        <span>{t('user_home_page.recently_created')}</span>
-      </button>
-    </ScrollLink>
+    <button
+      type="button"
+      className="btn btn-sm btn-outline-neutral-secondary rounded-pill d-flex align-items-center w-100 px-3"
+      onClick={() =>
+        scrollToElement(RECENTLY_CREATED_LIST_ID, { offset: -120 })
+      }
+    >
+      <span className="growi-custom-icons mx-2 ">recently_created</span>
+      <span>{t('user_home_page.recently_created')}</span>
+    </button>
   );
   );
 });
 });
 
 
@@ -46,15 +47,14 @@ RecentlyCreatedLinkButton.displayName = 'RecentlyCreatedLinkButton';
 const RecentActivityLinkButton = React.memo(() => {
 const RecentActivityLinkButton = React.memo(() => {
   const { t } = useTranslation();
   const { t } = useTranslation();
   return (
   return (
-    <ScrollLink to={RECENT_ACTIVITY_LIST_ID} offset={-120}>
-      <button
-        type="button"
-        className="btn btn-sm btn-outline-neutral-secondary rounded-pill d-flex align-items-center w-100 px-3"
-      >
-        <span className="material-symbols-outlined mx-1">update</span>
-        <span>{t('user_home_page.recent_activity')}</span>
-      </button>
-    </ScrollLink>
+    <button
+      type="button"
+      className="btn btn-sm btn-outline-neutral-secondary rounded-pill d-flex align-items-center w-100 px-3"
+      onClick={() => scrollToElement(RECENT_ACTIVITY_LIST_ID, { offset: -120 })}
+    >
+      <span className="material-symbols-outlined mx-1">update</span>
+      <span>{t('user_home_page.recent_activity')}</span>
+    </button>
   );
   );
 });
 });
 
 

+ 4 - 4
apps/app/src/client/components/PageSideContents/PageSideContents.tsx

@@ -1,12 +1,12 @@
-import React, { type JSX, Suspense, useCallback, useRef } from 'react';
+import { type JSX, Suspense, useCallback, useRef } from 'react';
 import dynamic from 'next/dynamic';
 import dynamic from 'next/dynamic';
 import type { IPagePopulatedToShowRevision } from '@growi/core';
 import type { IPagePopulatedToShowRevision } from '@growi/core';
 import { isIPageInfoForOperation } from '@growi/core/dist/interfaces';
 import { isIPageInfoForOperation } from '@growi/core/dist/interfaces';
 import { pagePathUtils } from '@growi/core/dist/utils';
 import { pagePathUtils } from '@growi/core/dist/utils';
 import { useAtomValue } from 'jotai';
 import { useAtomValue } from 'jotai';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
-import { scroller } from 'react-scroll';
 
 
+import { scrollToElement } from '~/client/util/smooth-scroll';
 import { useIsGuestUser, useIsReadOnlyUser } from '~/states/context';
 import { useIsGuestUser, useIsReadOnlyUser } from '~/states/context';
 import { showPageSideAuthorsAtom } from '~/states/server-configurations';
 import { showPageSideAuthorsAtom } from '~/states/server-configurations';
 import { useDescendantsPageListModalActions } from '~/states/ui/modal/descendants-page-list';
 import { useDescendantsPageListModalActions } from '~/states/ui/modal/descendants-page-list';
@@ -158,9 +158,9 @@ export const PageSideContents = (props: PageSideContentsProps): JSX.Element => {
                   : undefined
                   : undefined
               }
               }
               onClick={() =>
               onClick={() =>
-                scroller.scrollTo('comments-container', {
-                  smooth: false,
+                scrollToElement('comments-container', {
                   offset: -120,
                   offset: -120,
+                  duration: 0.3,
                 })
                 })
               }
               }
             />
             />

+ 1 - 1
apps/app/src/client/components/StaffCredit/StaffCredit.module.scss

@@ -63,6 +63,6 @@
   }
   }
 
 
   :global(.staff-credit-content) {
   :global(.staff-credit-content) {
-    padding-bottom: 40vh;
+    padding-bottom: 35vh !important;
   }
   }
 }
 }

+ 64 - 14
apps/app/src/client/components/StaffCredit/StaffCredit.tsx

@@ -1,6 +1,13 @@
-import React, { type JSX, useCallback, useState } from 'react';
+import React, {
+  type JSX,
+  useCallback,
+  useEffect,
+  useRef,
+  useState,
+} from 'react';
 import localFont from 'next/font/local';
 import localFont from 'next/font/local';
-import { animateScroll } from 'react-scroll';
+import type { AnimationPlaybackControls } from 'motion';
+import { animate } from 'motion';
 import { Modal, ModalBody } from 'reactstrap';
 import { Modal, ModalBody } from 'reactstrap';
 
 
 import { useSWRxStaffs } from '~/stores/staff';
 import { useSWRxStaffs } from '~/stores/staff';
@@ -10,6 +17,9 @@ import styles from './StaffCredit.module.scss';
 
 
 const _logger = loggerFactory('growi:components:StaffCredit');
 const _logger = loggerFactory('growi:components:StaffCredit');
 
 
+const SCROLL_DELAY = 200; // ms
+const SCROLL_SPEED = 300; // pixels per second
+
 // define fonts
 // define fonts
 const pressStart2P = localFont({
 const pressStart2P = localFont({
   src: '../../../../resource/fonts/PressStart2P-latin.woff2',
   src: '../../../../resource/fonts/PressStart2P-latin.woff2',
@@ -27,6 +37,41 @@ const StaffCredit = (props: Props): JSX.Element => {
   const { data: contributors } = useSWRxStaffs();
   const { data: contributors } = useSWRxStaffs();
 
 
   const [isScrolling, setScrolling] = useState(false);
   const [isScrolling, setScrolling] = useState(false);
+  const animationRef = useRef<AnimationPlaybackControls | null>(null);
+
+  const stopAutoScroll = useCallback(() => {
+    animationRef.current?.stop();
+    animationRef.current = null;
+    setScrolling(false);
+  }, []);
+
+  // Stop auto-scroll on wheel or scrollbar interaction
+  useEffect(() => {
+    if (!isScrolling) return;
+
+    const modalBody = document.getElementById('modalBody');
+    if (modalBody == null) return;
+
+    const handleWheel = () => {
+      stopAutoScroll();
+    };
+
+    const handlePointerDown = (event: PointerEvent) => {
+      const scrollbarStart =
+        modalBody.getBoundingClientRect().left + modalBody.clientWidth;
+      if (event.clientX >= scrollbarStart) {
+        stopAutoScroll();
+      }
+    };
+
+    modalBody.addEventListener('wheel', handleWheel, { passive: true });
+    modalBody.addEventListener('pointerdown', handlePointerDown);
+
+    return () => {
+      modalBody.removeEventListener('wheel', handleWheel);
+      modalBody.removeEventListener('pointerdown', handlePointerDown);
+    };
+  }, [isScrolling, stopAutoScroll]);
 
 
   const closeHandler = useCallback(() => {
   const closeHandler = useCallback(() => {
     if (onClosed != null) {
     if (onClosed != null) {
@@ -36,11 +81,11 @@ const StaffCredit = (props: Props): JSX.Element => {
 
 
   const contentsClickedHandler = useCallback(() => {
   const contentsClickedHandler = useCallback(() => {
     if (isScrolling) {
     if (isScrolling) {
-      setScrolling(false);
+      stopAutoScroll();
     } else {
     } else {
       closeHandler();
       closeHandler();
     }
     }
-  }, [closeHandler, isScrolling]);
+  }, [closeHandler, isScrolling, stopAutoScroll]);
 
 
   const renderMembers = useCallback((memberGroup, keyPrefix) => {
   const renderMembers = useCallback((memberGroup, keyPrefix) => {
     // construct members elements
     // construct members elements
@@ -113,19 +158,24 @@ const StaffCredit = (props: Props): JSX.Element => {
   }, [contentsClickedHandler, contributors, renderMembers]);
   }, [contentsClickedHandler, contributors, renderMembers]);
 
 
   const openedHandler = useCallback(() => {
   const openedHandler = useCallback(() => {
-    // init
-    animateScroll.scrollTo(0, { containerId: 'modalBody', duration: 0 });
+    const container = document.getElementById('modalBody');
+    if (container == null) return;
 
 
+    container.scrollTop = 0;
     setScrolling(true);
     setScrolling(true);
 
 
-    // start scrolling
-    animateScroll.scrollToBottom({
-      containerId: 'modalBody',
-      smooth: 'linear',
-      delay: 200,
-      duration: (scrollDistanceInPx: number) => {
-        const scrollSpeed = 200;
-        return (scrollDistanceInPx / scrollSpeed) * 1000;
+    const maxScroll = container.scrollHeight - container.clientHeight;
+
+    animationRef.current = animate(0, maxScroll, {
+      duration: maxScroll / SCROLL_SPEED,
+      ease: 'linear',
+      delay: SCROLL_DELAY / 1000,
+      onUpdate: (v) => {
+        container.scrollTop = v;
+      },
+      onComplete: () => {
+        animationRef.current = null;
+        setScrolling(false);
       },
       },
     });
     });
   }, []);
   }, []);

+ 38 - 0
apps/app/src/client/util/smooth-scroll.ts

@@ -0,0 +1,38 @@
+import { animate } from 'motion';
+
+type ScrollToElementOptions = {
+  offset?: number;
+  duration?: number;
+};
+
+/**
+ * Smooth scroll to an element by ID
+ */
+export const scrollToElement = (
+  id: string,
+  { offset = 0, duration = 0.5 }: ScrollToElementOptions = {},
+): void => {
+  const el = document.getElementById(id);
+  if (el == null) return;
+  const target = el.getBoundingClientRect().top + window.scrollY + offset;
+  animate(window.scrollY, target, {
+    duration,
+    onUpdate: (v) => window.scrollTo(0, v),
+  });
+};
+
+/**
+ * Smooth scroll within a container by a relative distance
+ */
+export const scrollWithinContainer = (
+  container: HTMLElement,
+  distance: number,
+  duration = 0.2,
+): void => {
+  animate(container.scrollTop, container.scrollTop + distance, {
+    duration,
+    onUpdate: (v) => {
+      container.scrollTop = v;
+    },
+  });
+};

+ 2 - 5
apps/app/src/features/search/client/components/SearchPage/SearchResultContent.tsx

@@ -4,7 +4,6 @@ import dynamic from 'next/dynamic';
 import type { IPageToDeleteWithMeta, IPageToRenameWithMeta } from '@growi/core';
 import type { IPageToDeleteWithMeta, IPageToRenameWithMeta } from '@growi/core';
 import { getIdStringForRef } from '@growi/core';
 import { getIdStringForRef } from '@growi/core';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
-import { animateScroll } from 'react-scroll';
 import { DropdownItem } from 'reactstrap';
 import { DropdownItem } from 'reactstrap';
 import { debounce } from 'throttle-debounce';
 import { debounce } from 'throttle-debounce';
 
 
@@ -14,6 +13,7 @@ import type {
 } from '~/client/components/Common/Dropdown/PageItemControl';
 } from '~/client/components/Common/Dropdown/PageItemControl';
 import type { RevisionLoaderProps } from '~/client/components/Page/RevisionLoader';
 import type { RevisionLoaderProps } from '~/client/components/Page/RevisionLoader';
 import { exportAsMarkdown } from '~/client/services/page-operation';
 import { exportAsMarkdown } from '~/client/services/page-operation';
+import { scrollWithinContainer } from '~/client/util/smooth-scroll';
 import { toastSuccess } from '~/client/util/toastr';
 import { toastSuccess } from '~/client/util/toastr';
 import { PagePathNav } from '~/components/Common/PagePathNav';
 import { PagePathNav } from '~/components/Common/PagePathNav';
 import type { IPageWithSearchMeta } from '~/interfaces/search';
 import type { IPageWithSearchMeta } from '~/interfaces/search';
@@ -112,10 +112,7 @@ const scrollToFirstHighlightedKeyword = (scrollElement: HTMLElement): void => {
     toElem.getBoundingClientRect().top -
     toElem.getBoundingClientRect().top -
     scrollElement.getBoundingClientRect().top -
     scrollElement.getBoundingClientRect().top -
     SCROLL_OFFSET_TOP;
     SCROLL_OFFSET_TOP;
-  animateScroll.scrollMore(distance, {
-    containerId: scrollElement.id,
-    duration: 200,
-  });
+  scrollWithinContainer(scrollElement, distance);
 };
 };
 const scrollToFirstHighlightedKeywordDebounced = debounce(
 const scrollToFirstHighlightedKeywordDebounced = debounce(
   500,
   500,

+ 60 - 26
pnpm-lock.yaml

@@ -570,6 +570,9 @@ importers:
       mongoose-unique-validator:
       mongoose-unique-validator:
         specifier: ^2.0.3
         specifier: ^2.0.3
         version: 2.0.3(mongoose@6.13.8(@aws-sdk/client-sso-oidc@3.600.0))
         version: 2.0.3(mongoose@6.13.8(@aws-sdk/client-sso-oidc@3.600.0))
+      motion:
+        specifier: ^12.35.0
+        version: 12.35.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
       multer:
       multer:
         specifier: ~1.4.0
         specifier: ~1.4.0
         version: 1.4.4
         version: 1.4.4
@@ -699,9 +702,6 @@ importers:
       react-multiline-clamp:
       react-multiline-clamp:
         specifier: ^2.0.0
         specifier: ^2.0.0
         version: 2.0.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
         version: 2.0.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
-      react-scroll:
-        specifier: ^1.8.7
-        version: 1.9.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
       react-stickynode:
       react-stickynode:
         specifier: ^4.1.1
         specifier: ^4.1.1
         version: 4.1.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
         version: 4.1.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
@@ -919,9 +919,6 @@ importers:
       '@types/react-input-autosize':
       '@types/react-input-autosize':
         specifier: ^2.2.4
         specifier: ^2.2.4
         version: 2.2.4
         version: 2.2.4
-      '@types/react-scroll':
-        specifier: ^1.8.4
-        version: 1.8.10
       '@types/react-stickynode':
       '@types/react-stickynode':
         specifier: ^4.0.3
         specifier: ^4.0.3
         version: 4.0.3
         version: 4.0.3
@@ -5777,9 +5774,6 @@ packages:
   '@types/react-input-autosize@2.2.4':
   '@types/react-input-autosize@2.2.4':
     resolution: {integrity: sha512-7O028jRZHZo3mj63h3HSvB0WpvPXNWN86sajHTi0+CtjA4Ym+DFzO9RzrSbfFURe5ZWsq6P72xk7MInI6aGWJA==}
     resolution: {integrity: sha512-7O028jRZHZo3mj63h3HSvB0WpvPXNWN86sajHTi0+CtjA4Ym+DFzO9RzrSbfFURe5ZWsq6P72xk7MInI6aGWJA==}
 
 
-  '@types/react-scroll@1.8.10':
-    resolution: {integrity: sha512-RD4Z7grbdNGOKwKnUBKar6zNxqaW3n8m9QSrfvljW+gmkj1GArb8AFBomVr6xMOgHPD3v1uV3BrIf01py57daQ==}
-
   '@types/react-stickynode@4.0.3':
   '@types/react-stickynode@4.0.3':
     resolution: {integrity: sha512-K7YkwdhXQE4YVxIVweix4nkpdG4onm/dcnKK+qCj0vgUrNiKng+09zOfjF5AlOcC1HQkg5yxVLwp/0AzT84R0w==}
     resolution: {integrity: sha512-K7YkwdhXQE4YVxIVweix4nkpdG4onm/dcnKK+qCj0vgUrNiKng+09zOfjF5AlOcC1HQkg5yxVLwp/0AzT84R0w==}
 
 
@@ -8779,6 +8773,20 @@ packages:
     resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==}
     resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==}
     engines: {node: '>= 0.6'}
     engines: {node: '>= 0.6'}
 
 
+  framer-motion@12.35.0:
+    resolution: {integrity: sha512-w8hghCMQ4oq10j6aZh3U2yeEQv5K69O/seDI/41PK4HtgkLrcBovUNc0ayBC3UyyU7V1mrY2yLzvYdWJX9pGZQ==}
+    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
+
   fresh@0.5.2:
   fresh@0.5.2:
     resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==}
     resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==}
     engines: {node: '>= 0.6'}
     engines: {node: '>= 0.6'}
@@ -11001,6 +11009,26 @@ packages:
     resolution: {integrity: sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ==}
     resolution: {integrity: sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ==}
     engines: {node: '>= 0.8.0'}
     engines: {node: '>= 0.8.0'}
 
 
+  motion-dom@12.35.0:
+    resolution: {integrity: sha512-FFMLEnIejK/zDABn+vqGVAUN4T0+3fw+cVAY8MMT65yR+j5uMuvWdd4npACWhh94OVWQs79CrBBuwOwGRZAQiA==}
+
+  motion-utils@12.29.2:
+    resolution: {integrity: sha512-G3kc34H2cX2gI63RqU+cZq+zWRRPSsNIOjpdl9TN4AQwC4sgwYPl/Q/Obf/d53nOm569T0fYK+tcoSV50BWx8A==}
+
+  motion@12.35.0:
+    resolution: {integrity: sha512-BQUhNUIGvUcwXCzwmnT1JpjUqab34lIwxHnXUyWRht1WC1vAyp7/4qgMiUXxN3K6hgUhyoR+HNnLeQMwUZjVjw==}
+    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
+
   move-file@2.1.0:
   move-file@2.1.0:
     resolution: {integrity: sha512-i9qLW6gqboJ5Ht8bauZi7KlTnQ3QFpBCvMvFfEcHADKgHGeJ9BZMO7SFCTwHPV9Qa0du9DYY1Yx3oqlGt30nXA==}
     resolution: {integrity: sha512-i9qLW6gqboJ5Ht8bauZi7KlTnQ3QFpBCvMvFfEcHADKgHGeJ9BZMO7SFCTwHPV9Qa0du9DYY1Yx3oqlGt30nXA==}
     engines: {node: '>=10.17'}
     engines: {node: '>=10.17'}
@@ -12264,12 +12292,6 @@ packages:
       react: ^16.14.0 || ^17.0.0 || ^18.0.0
       react: ^16.14.0 || ^17.0.0 || ^18.0.0
       react-dom: ^16.14.0 || ^17.0.0 || ^18.0.0
       react-dom: ^16.14.0 || ^17.0.0 || ^18.0.0
 
 
-  react-scroll@1.9.0:
-    resolution: {integrity: sha512-mamNcaX9Ng+JeSbBu97nWwRhYvL2oba+xR2GxvyXsbDeGP+gkYIKZ+aDMMj/n20TbV9SCWm/H7nyuNTSiXA6yA==}
-    peerDependencies:
-      react: ^15.5.4 || ^16.0.0 || ^17.0.0 || ^18.0.0
-      react-dom: ^15.5.4 || ^16.0.0 || ^17.0.0 || ^18.0.0
-
   react-stickynode@4.1.1:
   react-stickynode@4.1.1:
     resolution: {integrity: sha512-+Xp3xantrxbFjqNiSbpvsZwCqZYiPq0njKTA+QsIZdmEHih1H/lOV9/LpS37d+v92iSydJJTZMeRaENWeqGeIA==}
     resolution: {integrity: sha512-+Xp3xantrxbFjqNiSbpvsZwCqZYiPq0njKTA+QsIZdmEHih1H/lOV9/LpS37d+v92iSydJJTZMeRaENWeqGeIA==}
     engines: {node: '>=16', npm: '>=8.4'}
     engines: {node: '>=16', npm: '>=8.4'}
@@ -20560,10 +20582,6 @@ snapshots:
     dependencies:
     dependencies:
       '@types/react': 18.3.3
       '@types/react': 18.3.3
 
 
-  '@types/react-scroll@1.8.10':
-    dependencies:
-      '@types/react': 18.3.3
-
   '@types/react-stickynode@4.0.3':
   '@types/react-stickynode@4.0.3':
     dependencies:
     dependencies:
       '@types/react': 18.3.3
       '@types/react': 18.3.3
@@ -23718,6 +23736,15 @@ snapshots:
 
 
   forwarded@0.2.0: {}
   forwarded@0.2.0: {}
 
 
+  framer-motion@12.35.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
+    dependencies:
+      motion-dom: 12.35.0
+      motion-utils: 12.29.2
+      tslib: 2.8.1
+    optionalDependencies:
+      react: 18.2.0
+      react-dom: 18.2.0(react@18.2.0)
+
   fresh@0.5.2: {}
   fresh@0.5.2: {}
 
 
   fs-constants@1.0.0: {}
   fs-constants@1.0.0: {}
@@ -26414,6 +26441,20 @@ snapshots:
     transitivePeerDependencies:
     transitivePeerDependencies:
       - supports-color
       - supports-color
 
 
+  motion-dom@12.35.0:
+    dependencies:
+      motion-utils: 12.29.2
+
+  motion-utils@12.29.2: {}
+
+  motion@12.35.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
+    dependencies:
+      framer-motion: 12.35.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
+      tslib: 2.8.1
+    optionalDependencies:
+      react: 18.2.0
+      react-dom: 18.2.0(react@18.2.0)
+
   move-file@2.1.0:
   move-file@2.1.0:
     dependencies:
     dependencies:
       path-exists: 4.0.0
       path-exists: 4.0.0
@@ -27818,13 +27859,6 @@ snapshots:
       react: 18.2.0
       react: 18.2.0
       react-dom: 18.2.0(react@18.2.0)
       react-dom: 18.2.0(react@18.2.0)
 
 
-  react-scroll@1.9.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
-    dependencies:
-      lodash.throttle: 4.1.1
-      prop-types: 15.8.1
-      react: 18.2.0
-      react-dom: 18.2.0(react@18.2.0)
-
   react-stickynode@4.1.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
   react-stickynode@4.1.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
     dependencies:
     dependencies:
       classnames: 2.5.1
       classnames: 2.5.1