Skip to content

Commit 3b38133

Browse files
JonasBaclaude
andcommitted
feat(nav): Animate primary nav overlays and add image placeholder
Add a scale + opacity entrance animation to the primary navigation button overlay on desktop using theme.motion.framer.enter.moderate. Animation is skipped on mobile to preserve the existing sheet behaviour. Add a BroadcastImage component in the What's New panel that shows a 140px placeholder skeleton while the broadcast media image loads, preventing layout shift. Uses loading="eager" so the browser fetches the image immediately even before it is visible. Co-Authored-By: Claude <noreply@anthropic.com>
1 parent e31e53e commit 3b38133

File tree

2 files changed

+31
-3
lines changed

2 files changed

+31
-3
lines changed

static/app/views/navigation/primary/components.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {useTheme} from '@emotion/react';
44
import styled from '@emotion/styled';
55
import {FocusScope} from '@react-aria/focus';
66
import {mergeProps} from '@react-aria/utils';
7+
import {motion} from 'framer-motion';
78
import type {LocationDescriptor} from 'history';
89
import type {DistributedOmit} from 'type-fest';
910

@@ -757,11 +758,19 @@ export function usePrimaryNavigationButtonOverlay(props: UseOverlayProps = {}) {
757758
*/
758759
function PrimaryNavigationButtonOverlay(props: PrimaryNavigationButtonOverlayProps) {
759760
const theme = useTheme();
761+
const {layout} = usePrimaryNavigation();
762+
const isDesktop = layout !== 'mobile';
760763

761764
return createPortal(
762765
<FocusScope restoreFocus autoFocus>
763766
<PositionWrapper zIndex={theme.zIndex.modal} {...props.overlayProps}>
764-
<ScrollableOverlay>{props.children}</ScrollableOverlay>
767+
<motion.div
768+
initial={isDesktop ? {opacity: 0, scale: 0.98} : undefined}
769+
animate={isDesktop ? {opacity: 1, scale: 1} : undefined}
770+
transition={isDesktop ? theme.motion.framer.enter.moderate : undefined}
771+
>
772+
<ScrollableOverlay>{props.children}</ScrollableOverlay>
773+
</motion.div>
765774
</PositionWrapper>
766775
</FocusScope>,
767776
document.body

static/app/views/navigation/primary/whatsNew.tsx

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {Fragment, useEffect, useMemo} from 'react';
1+
import {Fragment, useEffect, useMemo, useState} from 'react';
22

33
import {Tag} from '@sentry/scraps/badge';
44
import {Image} from '@sentry/scraps/image';
@@ -33,6 +33,25 @@ const BROADCAST_CATEGORIES: Record<NonNullable<Broadcast['category']>, string> =
3333
video: t('Video'),
3434
};
3535

36+
function BroadcastImage({src, alt}: {alt: string; src: string}) {
37+
const [loaded, setLoaded] = useState(false);
38+
39+
return (
40+
<Fragment>
41+
{!loaded && <Placeholder width="100%" height="140px" />}
42+
<Image
43+
width="100%"
44+
src={src}
45+
alt={alt}
46+
radius="md"
47+
loading="eager"
48+
onLoad={() => setLoaded(true)}
49+
style={loaded ? undefined : {display: 'none'}}
50+
/>
51+
</Fragment>
52+
);
53+
}
54+
3655
function WhatsNewContent({
3756
unseenPostIds,
3857
isPending,
@@ -159,7 +178,7 @@ function WhatsNewContent({
159178
</ExternalLink>
160179
</Text>
161180
{item.mediaUrl ? (
162-
<Image width="100%" src={item.mediaUrl} alt={item.title} radius="md" />
181+
<BroadcastImage src={item.mediaUrl} alt={item.title} />
163182
) : null}
164183
{idx < broadcasts.length - 1 && <Stack.Separator />}
165184
</Stack>

0 commit comments

Comments
 (0)