Finesse navbar search animation

This commit is contained in:
Sam Becker 2025-06-30 09:28:27 -05:00
parent 05b8a9c9f0
commit a2745068e8
4 changed files with 26 additions and 12 deletions

View File

@ -33,11 +33,11 @@ const GAP_CLASS = 'mr-1.5 sm:mr-2';
export default function AppViewSwitcher({
currentSelection,
className,
animateSearch = true,
animate = true,
}: {
currentSelection?: SwitcherSelection
className?: string
animateSearch?: boolean
animate?: boolean
}) {
const pathname = usePathname();
@ -151,10 +151,10 @@ export default function AppViewSwitcher({
</Switcher>
{showSortControl &&
<motion.div
initial={{ opacity: 0, scale: 0.5 }}
initial={animate ? { opacity: 0, scale: 0.5 } : false}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.5 }}
transition={{ duration: 0.3, ease: 'easeInOut' }}
transition={{ duration: 0.2, ease: 'easeInOut' }}
className={GAP_CLASS}
>
<Switcher className="max-sm:hidden">
@ -172,7 +172,12 @@ export default function AppViewSwitcher({
/>
</Switcher>
</motion.div>}
<motion.div layout={animateSearch}>
<motion.div
// Conditional key necessary to halt/resume layout animations
key={animate ? 'search' : 'search-no-animate'}
layout={animate}
transition={{ duration: 0.2, ease: 'easeInOut' }}
>
<Switcher type="borderless">
<SwitcherItem
icon={<IconSearch includeTitle={false} />}

View File

@ -20,6 +20,7 @@ import {
} from './config';
import { useRef } from 'react';
import useStickyNav from './useStickyNav';
import { useAppState } from '@/state/AppState';
const NAV_HEIGHT_CLASS = NAV_CAPTION
? 'min-h-[4rem] sm:min-h-[5rem]'
@ -37,6 +38,10 @@ export default function Nav({
const pathname = usePathname();
const showNav = !isPathSignIn(pathname);
const {
hasLoadedWithAnimations,
} = useAppState();
const {
classNameStickyContainer,
classNameStickyNav,
@ -86,7 +91,7 @@ export default function Nav({
<AppViewSwitcher
currentSelection={switcherSelectionForPath()}
className="translate-x-[-1px]"
animateSearch={isNavVisible}
animate={hasLoadedWithAnimations && isNavVisible}
/>
<div className={clsx(
'grow text-right min-w-0',

View File

@ -19,7 +19,7 @@ export type AppStateContextType = {
// CORE
previousPathname?: string
hasLoaded?: boolean
setHasLoaded?: Dispatch<SetStateAction<boolean>>
hasLoadedWithAnimations?: boolean
swrTimestamp?: number
invalidateSwr?: () => void
nextPhotoAnimation?: AnimationConfig

View File

@ -50,6 +50,8 @@ export default function AppStateProvider({
// CORE
const [hasLoaded, setHasLoaded] =
useState(false);
const [hasLoadedWithAnimations, setHasLoadedWithAnimations] =
useState(false);
const [swrTimestamp, setSwrTimestamp] =
useState(Date.now());
const [nextPhotoAnimation, _setNextPhotoAnimation] =
@ -112,8 +114,13 @@ export default function AppStateProvider({
useState(false);
useEffect(() => {
setHasLoaded?.(true);
setHasLoaded(true);
storeTimezoneCookie();
setUserEmailEager(getAuthEmailCookie());
const timeout = setTimeout(() => {
setHasLoadedWithAnimations(true);
}, 1000);
return () => clearTimeout(timeout);
}, []);
const invalidateSwr = useCallback(() => setSwrTimestamp(Date.now()), []);
@ -128,9 +135,6 @@ export default function AppStateProvider({
error: authError,
isLoading: isCheckingAuth,
} = useSWR('getAuth', getAuthAction);
useEffect(() => {
setUserEmailEager(getAuthEmailCookie());
}, []);
useEffect(() => {
if (auth === null || authError) {
setUserEmail(undefined);
@ -207,7 +211,7 @@ export default function AppStateProvider({
// CORE
previousPathname,
hasLoaded,
setHasLoaded,
hasLoadedWithAnimations,
swrTimestamp,
invalidateSwr,
nextPhotoAnimation,