mirror of
https://github.com/luxfi/explore.git
synced 2026-09-24 00:28:18 +00:00
Two panels slide in the mobile nav. The main one animates `transform`. Its sibling animated `left` — which relayouts the nav subtree every frame AND registers as layout shift, because moving an element's layout box is exactly what CLS measures. Measured under a 4x CPU throttle at 390x844: CLS 0.726 with `left`, 0 with `transform`. Google calls anything above 0.25 "poor", so the menu was scoring nearly 3x poor on a tap. The rewrite is pixel-identical, not an approximation. The element is absolute + w-full inside a containing block with no padding, so `left: 0` is its static position and translateX(100% + 24px) reproduces the old offset exactly. It now mirrors its sibling: same distance, same duration, same property. Motion also moves out of this repo. The local `skeleton-shimmer` keyframe is retired in favour of the design system's loading state — per-element background-position shimmer measures 33.6fps at 1460 concurrent skeletons and 13.8fps at 3000 under the same throttle, while skeletons that hold still measure 120fps at every count. Reasoning and full numbers are in section 6c of @luxfi/ui/tokens.css. Also wraps one 174-char className. It was already over the 160 limit; no staged change had made the linter look at this file until now. Co-authored-by: Hanzo Dev <[email protected]>
130 lines
5.0 KiB
TypeScript
130 lines
5.0 KiB
TypeScript
import React, { useCallback } from 'react';
|
|
|
|
import useNavItems, { isGroupItem } from 'lib/hooks/useNavItems';
|
|
import { useColorModeValue } from 'toolkit/next/color-mode';
|
|
import IconSvg from 'ui/shared/IconSvg';
|
|
import useIsAuth from 'ui/snippets/auth/useIsAuth';
|
|
|
|
import NavigationPromoBanner from '../promoBanner/NavigationPromoBanner';
|
|
import NavLink from '../vertical/NavLink';
|
|
import NavLinkRewards from '../vertical/NavLinkRewards';
|
|
import NavLinkGroup from './NavLinkGroup';
|
|
|
|
const ANIMATION_DURATION = 300;
|
|
|
|
interface Props {
|
|
onNavLinkClick?: () => void;
|
|
isMarketplaceAppPage?: boolean;
|
|
}
|
|
|
|
const NavigationMobile = ({ onNavLinkClick, isMarketplaceAppPage }: Props) => {
|
|
const timeoutRef = React.useRef<number | null>(null);
|
|
const { mainNavItems, accountNavItems } = useNavItems();
|
|
|
|
const [ openedGroupIndex, setOpenedGroupIndex ] = React.useState(-1);
|
|
const [ isOpen, setIsOpen ] = React.useState(false);
|
|
|
|
const onGroupItemOpen = (index: number) => () => {
|
|
setOpenedGroupIndex(index);
|
|
timeoutRef.current = window.setTimeout(() => {
|
|
setIsOpen(true);
|
|
}, 100);
|
|
};
|
|
|
|
const onGroupItemClose = useCallback(() => {
|
|
setIsOpen(false);
|
|
timeoutRef.current = window.setTimeout(() => {
|
|
setOpenedGroupIndex(-1);
|
|
}, ANIMATION_DURATION);
|
|
}, []);
|
|
|
|
React.useEffect(() => {
|
|
return () => {
|
|
if (timeoutRef.current) {
|
|
clearTimeout(timeoutRef.current);
|
|
}
|
|
};
|
|
}, [ ]);
|
|
|
|
const isAuth = useIsAuth();
|
|
|
|
const iconColor = useColorModeValue('gray.600', 'gray.300');
|
|
|
|
const openedItem = mainNavItems[openedGroupIndex];
|
|
|
|
const isCollapsed = isMarketplaceAppPage ? false : undefined;
|
|
|
|
return (
|
|
<div className="relative flex flex-col grow">
|
|
<div
|
|
className="flex flex-col grow"
|
|
style={{
|
|
transform: isOpen ? 'translateX(calc(-100% - 24px))' : 'translateX(0)',
|
|
transition: `transform ${ ANIMATION_DURATION }ms ease-in-out`,
|
|
maxHeight: openedGroupIndex > -1 ? '100vh' : 'unset',
|
|
overflowY: openedGroupIndex > -1 ? 'hidden' : 'unset',
|
|
}}
|
|
>
|
|
<nav className="mt-6">
|
|
<ul className="flex flex-col w-full gap-1 items-start">
|
|
{ mainNavItems.map((item, index) => {
|
|
if (isGroupItem(item)) {
|
|
return <NavLinkGroup key={ item.text } item={ item } onClick={ onGroupItemOpen(index) } isExpanded={ isMarketplaceAppPage }/>;
|
|
} else {
|
|
return <NavLink key={ item.text } item={ item } onClick={ onNavLinkClick } isCollapsed={ isCollapsed }/>;
|
|
}
|
|
}) }
|
|
</ul>
|
|
</nav>
|
|
{ isAuth && (
|
|
<nav className="mt-3 pt-3 border-t border-[var(--color-border-divider)]">
|
|
<ul className="flex flex-col gap-1 items-start">
|
|
<NavLinkRewards onClick={ onNavLinkClick } isCollapsed={ isCollapsed }/>
|
|
{ accountNavItems.map((item) => <NavLink key={ item.text } item={ item } onClick={ onNavLinkClick } isCollapsed={ isCollapsed }/>) }
|
|
</ul>
|
|
</nav>
|
|
) }
|
|
<NavigationPromoBanner isCollapsed={ isCollapsed }/>
|
|
</div>
|
|
<div
|
|
key="sub"
|
|
className="w-full mt-6 absolute top-0"
|
|
style={{
|
|
// Mirrors the main panel above: same distance, same duration, but on
|
|
// the compositor. This used to animate `left`, which relayouts the
|
|
// whole nav subtree every frame — the one place a phone would drop
|
|
// frames on a menu tap. `left: 0` is this element's static position
|
|
// (absolute + w-full, no padding on the containing block), so
|
|
// translateX(100% + 24px) is pixel-identical to the old offset.
|
|
transform: isOpen ? 'translateX(0)' : 'translateX(calc(100% + 24px))',
|
|
transition: `transform ${ ANIMATION_DURATION }ms ease-in-out`,
|
|
}}
|
|
>
|
|
<div className="flex items-center px-2 py-2.5 w-full h-[50px] cursor-pointer mb-1" onClick={ onGroupItemClose }>
|
|
<IconSvg name="arrows/east-mini" className="w-6 h-6 mr-2" style={{ color: `var(--color-${ iconColor.replace('.', '-') })` }}/>
|
|
<span className="text-[var(--color-text-secondary)] text-sm">{ mainNavItems[openedGroupIndex]?.text }</span>
|
|
</div>
|
|
<ul className="w-full">
|
|
{ openedItem && isGroupItem(openedItem) && openedItem.subItems?.map(
|
|
(item, index) => Array.isArray(item) ? (
|
|
<ul
|
|
key={ index }
|
|
className={ 'w-full [&:not(:last-child)]:mb-2 [&:not(:last-child)]:pb-2 ' +
|
|
'[&:not(:last-child)]:border-b [&:not(:last-child)]:border-[var(--color-border-divider)]' }
|
|
>
|
|
{ item.map(subItem => <NavLink key={ subItem.text } item={ subItem } onClick={ onNavLinkClick } isCollapsed={ isCollapsed }/>) }
|
|
</ul>
|
|
) : (
|
|
<li key={ item.text } className="mb-1 list-none">
|
|
<NavLink item={ item } onClick={ onNavLinkClick } isCollapsed={ isCollapsed }/>
|
|
</li>
|
|
),
|
|
) }
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default NavigationMobile;
|