feat: add animation for pages

This commit is contained in:
Sergey Krylov 2024-07-05 05:38:22 +03:00
parent 50c7f4f76c
commit 5313c5c134
27 changed files with 369 additions and 124 deletions

View File

@ -99,6 +99,11 @@
"**/.eslintrc.[j|t]s" "**/.eslintrc.[j|t]s"
], ],
"optionalDependencies": false "optionalDependencies": false
}],
"react/function-component-definition": [
"error", {
"namedComponents": "arrow-function",
"unnamedComponents": "arrow-function"
}] }]
} }
} }

View File

@ -36,6 +36,7 @@
"@emotion/styled": "11.11.5", "@emotion/styled": "11.11.5",
"@mui/icons-material": "^5.15.19", "@mui/icons-material": "^5.15.19",
"@mui/material": "5.15.19", "@mui/material": "5.15.19",
"@react-spring/web": "9.7.3",
"classnames": "2.5.1", "classnames": "2.5.1",
"notistack": "3.0.1", "notistack": "3.0.1",
"prop-types": "15.8.1", "prop-types": "15.8.1",

View File

@ -1,11 +1,10 @@
import CalendarMonthIcon from '@mui/icons-material/CalendarMonth'; import CalendarMonthIcon from '@mui/icons-material/CalendarMonth';
import FormatListBulletedIcon from '@mui/icons-material/FormatListBulleted'; import FormatListBulletedIcon from '@mui/icons-material/FormatListBulleted';
import PieChartIcon from '@mui/icons-material/PieChart'; import PieChartIcon from '@mui/icons-material/PieChart';
import { CircularProgress } from '@mui/material'; import CircularProgress from '@mui/material/CircularProgress';
import { Suspense } from 'react'; import { Suspense } from 'react';
import { createBrowserRouter, Navigate, RouterProvider as RRDRouterProvider } from 'react-router-dom'; import { createBrowserRouter, Navigate, RouterProvider as RRDRouterProvider } from 'react-router-dom';
// todo lazy & access
import { AnalyzePage, AnalyzePageToolbar } from 'pages/AnalyzePage'; import { AnalyzePage, AnalyzePageToolbar } from 'pages/AnalyzePage';
import { ErrorPage, ErrorPageToolbar } from 'pages/ErrorPage'; import { ErrorPage, ErrorPageToolbar } from 'pages/ErrorPage';
import { PaymentsPage, PaymentsPageToolbar } from 'pages/PaymentsPage'; import { PaymentsPage, PaymentsPageToolbar } from 'pages/PaymentsPage';

View File

@ -1,4 +1,4 @@
import { render, screen } from '@testing-library/react'; import { render, screen, waitFor } from '@testing-library/react';
import { SchedulePage, SchedulePageToolbar } from 'pages/SchedulePage'; import { SchedulePage, SchedulePageToolbar } from 'pages/SchedulePage';
@ -7,12 +7,15 @@ describe('Test SchedulePage', () => {
<SchedulePage data-testid="schedule-page" />, <SchedulePage data-testid="schedule-page" />,
); );
test('should render correctly', () => { test('should render correctly', async () => {
setup(); setup();
const pageElem = screen.getByTestId('schedule-page'); await waitFor(() => {
expect(pageElem).toBeVisible(); const pageElem = screen.getByTestId('schedule-page');
expect(pageElem).toHaveTextContent('Schedule Page'); expect(pageElem).toBeVisible();
// eslint-disable-next-line testing-library/no-wait-for-multiple-assertions
expect(pageElem).toHaveTextContent('Schedule Page');
});
}); });
}); });

View File

@ -1,8 +1,5 @@
// todo fix eslint
import type { FC, PropsWithChildren } from 'react'; import type { FC, PropsWithChildren } from 'react';
// eslint-disable-next-line react/function-component-definition
const withProvider = <T,>(Provider: FC<PropsWithChildren>) => (Component: FC<T>) => (...params: T[]) => ( const withProvider = <T,>(Provider: FC<PropsWithChildren>) => (Component: FC<T>) => (...params: T[]) => (
<Provider> <Provider>
{/* @ts-ignore */} {/* @ts-ignore */}

View File

@ -1,7 +1,7 @@
import { renderWithProvider } from '__spec__/custon-renders'; import { renderWithProvider } from '__spec__/custon-renders';
import PieChartIcon from '@mui/icons-material/PieChart'; import PieChartIcon from '@mui/icons-material/PieChart';
import { fireEvent, screen } from '@testing-library/react'; import { fireEvent, screen, waitFor } from '@testing-library/react';
import AsideMenuLayout from './AsideMenuLayout.ui'; import AsideMenuLayout from './AsideMenuLayout.ui';
@ -41,10 +41,6 @@ describe('Test AsideMenuLayout', () => {
<AsideMenuLayout <AsideMenuLayout
Content={<Content />} Content={<Content />}
data-testid="aside-menu-layout" data-testid="aside-menu-layout"
DrawerSwitcherProps={{
// @ts-ignore
'data-testid': 'drawer-switcher',
}}
pages={pages} pages={pages}
Toolbar={<Toolbar />} Toolbar={<Toolbar />}
/>, />,
@ -58,18 +54,22 @@ describe('Test AsideMenuLayout', () => {
expect(layoutElem).toBeVisible(); expect(layoutElem).toBeVisible();
}); });
test('should render toolbar', () => { test('should render toolbar', async () => {
setup(); setup();
const toolbarElem = screen.getByText('Test Toolbar'); await waitFor(() => {
expect(toolbarElem).toBeVisible(); const toolbarElem = screen.getByText('Test Toolbar');
expect(toolbarElem).toBeVisible();
});
}); });
test('should render content', () => { test('should render content', async () => {
setup(); setup();
const toolbarElem = screen.getByText('Test Content'); await waitFor(() => {
expect(toolbarElem).toBeVisible(); const toolbarElem = screen.getByText('Test Content');
expect(toolbarElem).toBeVisible();
});
}); });
test('should render navigation routes', () => { test('should render navigation routes', () => {

View File

@ -1,104 +1,137 @@
import ChevronLeftIcon from '@mui/icons-material/ChevronLeft'; import { useMediaQuery } from '@mui/material';
import MenuIcon from '@mui/icons-material/Menu';
import Box from '@mui/material/Box'; import Box from '@mui/material/Box';
import CssBaseline from '@mui/material/CssBaseline';
import Divider from '@mui/material/Divider';
import List from '@mui/material/List'; import List from '@mui/material/List';
import { useState } from 'react'; import { useTheme } from '@mui/material/styles';
import Tooltip from '@mui/material/Tooltip';
import { animated, useTransition } from '@react-spring/web';
import { useContext } from 'react';
import { useLocation } from 'react-router-dom';
import AppBar from './components/AppBar'; import { DesktopAppBar, MobileAppBar } from './components/AppBar';
import Drawer from './components/Drawer'; import { AsideMenuContext, AsideMenuProvider } from './components/AsideMenuProvider';
import DrawerHeader from './components/DrawerMenu'; import { DesktopDrawer } from './components/DesktopDrawer';
import DrawerSwitcher from './components/DrawerSwitcher'; import { MenuItem } from './components/MenuItem';
import MenuItem from './components/MenuItem/MenuItem.ui'; import { MobileDrawer } from './components/MobileDrawer';
import type { MenuItemProps } from './components/MenuItem/MenuItem.ui'; import type { MenuItemProps } from './components/MenuItem/ui/MenuItem.ui';
import type { BoxProps } from '@mui/material/Box/Box'; import type { BoxProps } from '@mui/material/Box/Box';
import type { import type { FC, ReactNode } from 'react';
FC,
ReactNode,
DetailedHTMLProps,
HTMLAttributes,
} from 'react';
const drawerWidth = 250; export const DESKTOP_DRAWER_WIDTH = 250;
export const MOBILE_DRAWER_WIDTH = 250;
type AsideMenuLayoutProps = BoxProps & { export type AsideMenuLayoutProps = BoxProps & {
pages: MenuItemProps['page'][]; pages: MenuItemProps['page'][];
Content: ReactNode; Content: ReactNode;
Toolbar?: ReactNode; Toolbar?: ReactNode;
DrawerSwitcherProps?: DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>;
}; };
const AsideMenuLayout: FC<AsideMenuLayoutProps> = (props) => { const AsideMenuLayout: FC<AsideMenuLayoutProps> = (props) => {
const { const {
pages, pages,
Content, Content,
Toolbar, Toolbar,
DrawerSwitcherProps,
...restProps ...restProps
} = props; } = props;
const [isOpen, setIsOpen] = useState(true); const { desktopDrawerOpen, setMobileDrawerOpen } = useContext(AsideMenuContext);
const theme = useTheme();
const isMobileView = useMediaQuery(theme.breakpoints.down('sm'));
const handleDrawerOpen = () => { const location = useLocation();
setIsOpen(true); const transitions = useTransition(location, {
}; from: { opacity: 0 },
const handleDrawerClose = () => { enter: { opacity: 1 },
setIsOpen(false); leave: { opacity: 0 },
}; unique: true,
reset: true,
});
const AnimatedContent = (
<>
{transitions((styles) => (
<animated.div style={styles}>
{Content}
</animated.div>
))}
</>
);
const AnimatedToolBar = (
<>
{transitions((styles) => (
<animated.div style={styles}>
{Toolbar}
</animated.div>
))}
</>
);
if (isMobileView) {
return (
<Box sx={{ display: 'flex', flexDirection: 'column' }} {...restProps}>
{
Boolean(Toolbar) && (
<MobileAppBar position="static">
<MobileDrawer width={MOBILE_DRAWER_WIDTH}>
<List>
{pages.map((page) => (
<MenuItem
key={page.path}
page={page}
onClick={() => { setMobileDrawerOpen(false); }}
/>
))}
</List>
</MobileDrawer>
{AnimatedToolBar}
</MobileAppBar>
)
}
<Box component="main" sx={{ flexGrow: 1, p: 3 }}>
{AnimatedContent}
</Box>
</Box>
);
}
return ( return (
<Box sx={{ display: 'flex' }} {...restProps}> <Box sx={{ display: 'flex' }} {...restProps}>
<CssBaseline /> <DesktopDrawer width={DESKTOP_DRAWER_WIDTH}>
<Drawer
drawerWidth={drawerWidth}
open={isOpen}
variant="permanent"
>
<DrawerHeader>
<DrawerSwitcher
open={isOpen}
onClick={isOpen ? handleDrawerClose : handleDrawerOpen}
{...DrawerSwitcherProps}
>
{ isOpen ? <ChevronLeftIcon /> : <MenuIcon /> }
</DrawerSwitcher>
</DrawerHeader>
<Divider />
<List> <List>
{pages.map((page) => ( {pages.map((page) => (
<MenuItem key={page.path} isOpen={isOpen} page={page} /> desktopDrawerOpen
))} ? (
<MenuItem key={page.path} isOpen={desktopDrawerOpen} page={page} />
)
: (
<Tooltip key={page.path} placement="right" title={page.title}>
<MenuItem isOpen={desktopDrawerOpen} page={page} />
</Tooltip>
)))}
</List> </List>
</Drawer> </DesktopDrawer>
<Box component="main" sx={{ flexGrow: 1, p: 3 }}> <Box sx={{ flexGrow: 1 }}>
{ { Boolean(Toolbar) && (
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions <DesktopAppBar position="static">
Toolbar {AnimatedToolBar}
? ( </DesktopAppBar>
<> )}
<AppBar
drawerWidth={drawerWidth}
open={isOpen}
position="fixed"
>
{Toolbar}
</AppBar>
<DrawerHeader /> <Box component="main" sx={{ p: 3 }}>
</> {AnimatedContent}
) </Box>
: null
}
{Content}
</Box> </Box>
</Box> </Box>
); );
}; };
export default AsideMenuLayout; const withProvider = (Component: FC<AsideMenuLayoutProps>) => (props: AsideMenuLayoutProps) => (
<AsideMenuProvider>
<Component {...props} />
</AsideMenuProvider>
);
export default withProvider(AsideMenuLayout);

View File

@ -1,30 +1,21 @@
import MuiAppBar from '@mui/material/AppBar'; import MuiAppBar from '@mui/material/AppBar';
import { styled } from '@mui/material/styles'; import { styled } from '@mui/material/styles';
import type { AppBarProps as MuiAppBarProps } from '@mui/material/AppBar/AppBar'; export const DesktopAppBar = styled(MuiAppBar)(
({ theme }) => ({
type AppBarProps = MuiAppBarProps & {
open?: boolean;
drawerWidth: number;
};
const AppBar = styled(MuiAppBar, { shouldForwardProp: (prop) => !['open', 'drawerWidth'].includes(prop as string) })<AppBarProps>(
({ theme, open, drawerWidth }) => ({
transition: theme.transitions.create(['width', 'margin'], { transition: theme.transitions.create(['width', 'margin'], {
easing: theme.transitions.easing.sharp, easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.leavingScreen, duration: theme.transitions.duration.leavingScreen,
}), }),
marginLeft: `calc(${theme.spacing(7)} + 1px)`, display: 'flex',
width: `calc(100% - calc(${theme.spacing(7)} + 1px))`, flexDirection: 'row',
...Boolean(open) && { alignItems: 'center',
marginLeft: drawerWidth,
width: `calc(100% - ${drawerWidth}px)`,
transition: theme.transitions.create(['width', 'margin'], {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.enteringScreen,
}),
},
}), }),
); );
export default AppBar; export const MobileAppBar = styled(MuiAppBar)(() => ({
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
padding: '0 24px',
}));

View File

@ -0,0 +1,2 @@
export { default as AsideMenuProvider } from './ui/AsideMenuProvider.ui';
export { AsideMenuContext } from './model/context';

View File

@ -0,0 +1,10 @@
import { createContext } from 'react';
export type AsideMenuContextType = {
desktopDrawerOpen: boolean;
setDesktopDrawerOpen: (isOpen: boolean) => void;
mobileDrawerOpen: boolean;
setMobileDrawerOpen: (isOpen: boolean) => void;
};
export const AsideMenuContext = createContext<AsideMenuContextType>({} as AsideMenuContextType);

View File

@ -0,0 +1,31 @@
import { useMemo, useState } from 'react';
import { AsideMenuContext, type AsideMenuContextType } from '../model/context';
import type { FC, ReactNode } from 'react';
type AsideMenuProviderProps = {
children: ReactNode;
};
const AsideMenuProvider: FC<AsideMenuProviderProps> = (props) => {
const { children } = props;
// todo desktop from default value localstorage
const [desktopDrawerOpen, setDesktopDrawerOpen] = useState(true);
const [mobileDrawerOpen, setMobileDrawerOpen] = useState(false);
const value = useMemo<AsideMenuContextType>(() => ({
desktopDrawerOpen,
setDesktopDrawerOpen,
mobileDrawerOpen,
setMobileDrawerOpen,
}), [desktopDrawerOpen, mobileDrawerOpen]);
return (
<AsideMenuContext.Provider value={value}>
{children}
</AsideMenuContext.Provider>
);
};
export default AsideMenuProvider;

View File

@ -0,0 +1,2 @@
export type { DesktopDrawerProps } from './ui/DesktopDrawer.ui';
export { default as DesktopDrawer } from './ui/DesktopDrawer.ui';

View File

@ -0,0 +1,53 @@
import ChevronLeftIcon from '@mui/icons-material/ChevronLeft';
import MenuIcon from '@mui/icons-material/Menu';
import Divider from '@mui/material/Divider';
import { useContext } from 'react';
import { AsideMenuContext } from '../../AsideMenuProvider';
import Drawer from './components/Drawer';
import DrawerHeader from './components/DrawerMenu';
import DrawerSwitcher from './components/DrawerSwitcher';
import type { FC, ReactNode } from 'react';
export type DesktopDrawerProps = {
width: number;
children: ReactNode;
};
const DesktopDrawer: FC<DesktopDrawerProps> = (props) => {
const {
width,
children,
} = props;
const { desktopDrawerOpen, setDesktopDrawerOpen } = useContext(AsideMenuContext);
const handleDrawerOpen = () => {
setDesktopDrawerOpen(true);
};
const handleDrawerClose = () => {
setDesktopDrawerOpen(false);
};
return (
<Drawer open={desktopDrawerOpen} variant="permanent" width={width}>
<DrawerHeader>
<DrawerSwitcher
data-testid="drawer-switcher"
open={desktopDrawerOpen}
onClick={desktopDrawerOpen ? handleDrawerClose : handleDrawerOpen}
>
{ desktopDrawerOpen ? <ChevronLeftIcon /> : <MenuIcon /> }
</DrawerSwitcher>
</DrawerHeader>
<Divider />
{children}
</Drawer>
);
};
export default DesktopDrawer;

View File

@ -3,11 +3,11 @@ import { type CSSObject, styled, type Theme } from '@mui/material/styles';
type DrawerProps = { type DrawerProps = {
open: boolean; open: boolean;
drawerWidth: number; width: number;
}; };
const openedMixin = (theme: Theme, { drawerWidth }: Pick<DrawerProps, 'drawerWidth'>): CSSObject => ({ const openedMixin = (theme: Theme, { width }: Pick<DrawerProps, 'width'>): CSSObject => ({
width: drawerWidth, width,
transition: theme.transitions.create('width', { transition: theme.transitions.create('width', {
easing: theme.transitions.easing.sharp, easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.enteringScreen, duration: theme.transitions.duration.enteringScreen,
@ -24,15 +24,15 @@ const closedMixin = (theme: Theme): CSSObject => ({
width: `calc(${theme.spacing(8)} + 1px)`, width: `calc(${theme.spacing(8)} + 1px)`,
}); });
const Drawer = styled(MuiDrawer, { shouldForwardProp: (prop) => !['open', 'drawerWidth'].includes(prop as string) })<DrawerProps>( const Drawer = styled(MuiDrawer, { shouldForwardProp: (prop) => !['open', 'width'].includes(prop as string) })<DrawerProps>(
({ theme, open, drawerWidth }) => ({ ({ theme, open, width }) => ({
width: drawerWidth, width,
flexShrink: 0, flexShrink: 0,
whiteSpace: 'nowrap', whiteSpace: 'nowrap',
boxSizing: 'border-box', boxSizing: 'border-box',
...open && { ...open && {
...openedMixin(theme, { drawerWidth }), ...openedMixin(theme, { width }),
'& .MuiDrawer-paper': openedMixin(theme, { drawerWidth }), '& .MuiDrawer-paper': openedMixin(theme, { width }),
}, },
...!open && { ...!open && {
...closedMixin(theme), ...closedMixin(theme),

View File

@ -0,0 +1 @@
export { default as MenuItem } from './ui/MenuItem.ui';

View File

@ -4,10 +4,11 @@ import ListItemIcon from '@mui/material/ListItemIcon';
import ListItemText from '@mui/material/ListItemText'; import ListItemText from '@mui/material/ListItemText';
import { styled, useTheme } from '@mui/material/styles'; import { styled, useTheme } from '@mui/material/styles';
import Typography from '@mui/material/Typography'; import Typography from '@mui/material/Typography';
import { forwardRef } from 'react';
import { NavLink as RRDNavLink } from 'react-router-dom'; import { NavLink as RRDNavLink } from 'react-router-dom';
import type { ListItemProps } from '@mui/material/ListItem/ListItem'; import type { ListItemProps } from '@mui/material/ListItem/ListItem';
import type { FC, ReactNode } from 'react'; import type { ReactNode } from 'react';
import type { NavLinkProps } from 'react-router-dom'; import type { NavLinkProps } from 'react-router-dom';
const NavLink = styled(RRDNavLink)(({ theme }) => ({ const NavLink = styled(RRDNavLink)(({ theme }) => ({
@ -26,7 +27,7 @@ export type MenuItemProps = ListItemProps & {
NavLinkProps?: NavLinkProps; NavLinkProps?: NavLinkProps;
}; };
const MenuItem: FC<MenuItemProps> = (props) => { const MenuItem = forwardRef<HTMLLIElement, MenuItemProps>((props, ref) => {
const { const {
page, page,
isOpen = true, isOpen = true,
@ -36,7 +37,7 @@ const MenuItem: FC<MenuItemProps> = (props) => {
const theme = useTheme(); const theme = useTheme();
return ( return (
<ListItem disablePadding {...restProps}> <ListItem ref={ref} disablePadding {...restProps}>
<NavLink to={page.path} {...NavLinkProps}> <NavLink to={page.path} {...NavLinkProps}>
{({ isActive }) => ( {({ isActive }) => (
<ListItemButton <ListItemButton
@ -72,6 +73,6 @@ const MenuItem: FC<MenuItemProps> = (props) => {
</NavLink> </NavLink>
</ListItem> </ListItem>
); );
}; });
export default MenuItem; export default MenuItem;

View File

@ -0,0 +1 @@
export { default as MobileDrawer } from './ui/MobileDrawer.ui';

View File

@ -0,0 +1,56 @@
import MenuIcon from '@mui/icons-material/Menu';
import MuiDrawer from '@mui/material/Drawer';
import IconButton from '@mui/material/IconButton';
import { useContext } from 'react';
import { AsideMenuContext } from '../../AsideMenuProvider';
import type { FC, ReactNode } from 'react';
type MobileDrawerProps = {
children: ReactNode;
width: number;
onOpen?: () => void;
onClose?: () => void;
};
const MobileDrawer: FC<MobileDrawerProps> = (props) => {
const {
children,
width,
onOpen,
onClose,
} = props;
const { mobileDrawerOpen, setMobileDrawerOpen } = useContext(AsideMenuContext);
const handleDrawerOpen = () => {
setMobileDrawerOpen(true);
onOpen?.();
};
const handleDrawerClose = () => {
setMobileDrawerOpen(false);
onClose?.();
};
return (
<>
<IconButton onClick={handleDrawerOpen}>
<MenuIcon style={{ fill: '#fff' }} />
</IconButton>
<MuiDrawer
open={mobileDrawerOpen}
sx={{ width }}
onClose={handleDrawerClose}
>
{children}
</MuiDrawer>
</>
);
};
export default MobileDrawer;

View File

@ -4496,6 +4496,64 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@react-spring/animated@npm:~9.7.3":
version: 9.7.3
resolution: "@react-spring/animated@npm:9.7.3"
dependencies:
"@react-spring/shared": "npm:~9.7.3"
"@react-spring/types": "npm:~9.7.3"
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0
checksum: 10c0/5151da4fa7da010bb2edbee05871aa7a4aea8763fe617389d17605810aa0dd817374205e5fb3930b650f4a7f25fcdf23205fdfb7365686ff75888bdfd0b39839
languageName: node
linkType: hard
"@react-spring/core@npm:~9.7.3":
version: 9.7.3
resolution: "@react-spring/core@npm:9.7.3"
dependencies:
"@react-spring/animated": "npm:~9.7.3"
"@react-spring/shared": "npm:~9.7.3"
"@react-spring/types": "npm:~9.7.3"
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0
checksum: 10c0/e28c05de8435bf2eaf8481f8acdf093909d4be9881a9a854c51dfac7c2d5562088d0fb2ce04e2f07e1b3bf621d8da3ab57bf6fedb4fdc954e3aa263bc1e393af
languageName: node
linkType: hard
"@react-spring/shared@npm:~9.7.3":
version: 9.7.3
resolution: "@react-spring/shared@npm:9.7.3"
dependencies:
"@react-spring/types": "npm:~9.7.3"
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0
checksum: 10c0/afb03ed28ccf62efa4012e531c3659999bb364d1e0eb0fb729b962f9bd21a0b772a2d98e862062c9c32c06edf72327afcc45984d9eb22fdd961706b6ddf6950d
languageName: node
linkType: hard
"@react-spring/types@npm:~9.7.3":
version: 9.7.3
resolution: "@react-spring/types@npm:9.7.3"
checksum: 10c0/d645044f3cc9ceb7c4f6c4d061aaf6660018568a1553d05638f56b3328c5f91597ee4118334abe22fc8f07f5ee02f054340170c1d52e11b3faea22888b5170d4
languageName: node
linkType: hard
"@react-spring/web@npm:9.7.3":
version: 9.7.3
resolution: "@react-spring/web@npm:9.7.3"
dependencies:
"@react-spring/animated": "npm:~9.7.3"
"@react-spring/core": "npm:~9.7.3"
"@react-spring/shared": "npm:~9.7.3"
"@react-spring/types": "npm:~9.7.3"
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
checksum: 10c0/a5b4847a2921a29a3e8ce569f4951abeb268b6e8eb230f8c49d98709216b2b3a23ba1e58628c51c9eaa0bd20d83f9d7b8f8f03df98215e933de4c66c39e17fe1
languageName: node
linkType: hard
"@remix-run/router@npm:1.16.1": "@remix-run/router@npm:1.16.1":
version: 1.16.1 version: 1.16.1
resolution: "@remix-run/router@npm:1.16.1" resolution: "@remix-run/router@npm:1.16.1"
@ -12874,6 +12932,7 @@ __metadata:
"@emotion/styled": "npm:11.11.5" "@emotion/styled": "npm:11.11.5"
"@mui/icons-material": "npm:^5.15.19" "@mui/icons-material": "npm:^5.15.19"
"@mui/material": "npm:5.15.19" "@mui/material": "npm:5.15.19"
"@react-spring/web": "npm:9.7.3"
"@testing-library/jest-dom": "npm:6.4.2" "@testing-library/jest-dom": "npm:6.4.2"
"@testing-library/react": "npm:14.3.1" "@testing-library/react": "npm:14.3.1"
"@testing-library/user-event": "npm:^14.5.2" "@testing-library/user-event": "npm:^14.5.2"