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({ export default function AppViewSwitcher({
currentSelection, currentSelection,
className, className,
animateSearch = true, animate = true,
}: { }: {
currentSelection?: SwitcherSelection currentSelection?: SwitcherSelection
className?: string className?: string
animateSearch?: boolean animate?: boolean
}) { }) {
const pathname = usePathname(); const pathname = usePathname();
@ -151,10 +151,10 @@ export default function AppViewSwitcher({
</Switcher> </Switcher>
{showSortControl && {showSortControl &&
<motion.div <motion.div
initial={{ opacity: 0, scale: 0.5 }} initial={animate ? { opacity: 0, scale: 0.5 } : false}
animate={{ opacity: 1, scale: 1 }} animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.5 }} exit={{ opacity: 0, scale: 0.5 }}
transition={{ duration: 0.3, ease: 'easeInOut' }} transition={{ duration: 0.2, ease: 'easeInOut' }}
className={GAP_CLASS} className={GAP_CLASS}
> >
<Switcher className="max-sm:hidden"> <Switcher className="max-sm:hidden">
@ -172,7 +172,12 @@ export default function AppViewSwitcher({
/> />
</Switcher> </Switcher>
</motion.div>} </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"> <Switcher type="borderless">
<SwitcherItem <SwitcherItem
icon={<IconSearch includeTitle={false} />} icon={<IconSearch includeTitle={false} />}

View File

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

View File

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

View File

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