Skip to content

Commit 88ba368

Browse files
authored
feat(onboarding): Add animated Sentry logo to SCM onboarding steps (#112386)
Add a progress-driven Sentry brand mark to the SCM onboarding flow. The logo uses the same stroke-dashoffset animation technique as the app loading screen (`static/index.ejs`) — interior stroke paths with a goo filter and alpha mask that merge into a solid fill — but with a single accent color and fill level controlled by step progression instead of the looping multicolor original. The component lives above `AnimatePresence` in the onboarding container so a single persistent instance coordinates the animation smoothly when navigating forward and back between steps (connect = 0%, platform = 50%, project details = 100%). Refs VDY-66
1 parent 7f9d5f6 commit 88ba368

File tree

2 files changed

+132
-4
lines changed

2 files changed

+132
-4
lines changed
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import {useId} from 'react';
2+
import {useTheme} from '@emotion/react';
3+
4+
interface AnimatedSentryLogoProps {
5+
/**
6+
* Draw progress from 0 (no stroke) to 1 (fully drawn).
7+
* Uses the stroke-dashoffset technique from the app loading screen.
8+
*/
9+
progress: number;
10+
className?: string;
11+
size?: number;
12+
}
13+
14+
const DASH_TOTAL = 1150;
15+
16+
// Filled glyph (same path as index.ejs base + mask)
17+
const SENTRY_GLYPH =
18+
'M223.687 85.56a28.02 28.02 0 0 0-24-13.559 28.02 28.02 0 0 0-24 13.559l-39.48 67.62a193.26 193.26 0 0 1 106.5 159.96h-27.72a166.07 166.07 0 0 0-92.76-136.32L85.687 240a95.52 95.52 0 0 1 55.38 73.02h-63.66a4.55 4.55 0 0 1-3.659-2.325 4.56 4.56 0 0 1-.06-4.335l17.64-30a64.4 64.4 0 0 0-20.16-11.4l-17.46 30a27.24 27.24 0 0 0 2.023 30.435 27.2 27.2 0 0 0 8.116 7.005 27.96 27.96 0 0 0 13.56 3.6h87.18a116.41 116.41 0 0 0-48-103.86l13.86-24a143.22 143.22 0 0 1 61.8 127.86h73.86a215.28 215.28 0 0 0-98.46-190.8l28.02-48a4.62 4.62 0 0 1 6.3-1.62c3.18 1.74 121.74 208.62 123.96 211.02a4.562 4.562 0 0 1-4.08 6.78h-28.56q.54 11.46 0 22.86h28.68a27.54 27.54 0 0 0 27.72-27.66 26.94 26.94 0 0 0-3.72-13.68z';
19+
20+
// Stroke paths that trace the logo outline (one group from index.ejs)
21+
const STROKE_PATHS = [
22+
'm82.8 253.5-12.7 22-12.5 21.2-.2.3v.1l-.2.2v.3a24 24 0 00.3 22.4l.3.6A24 24 0 0076.4 332h87l-3.2-21.8A115 115 0 00112 233l17.6-30.4a147 147 0 0166.8 112l1.3 17.5h65.6l-1.3-20.2a212 212 0 00-99.3-165.8l29.6-50.8a9 9 0 013.3-3.1 9 9 0 0112.1 3.1l122.2 209.2h.1a8 8 0 011 4v.2a8.5 8.5 0 01-8.6 8.5h-38.9',
23+
'M92.5 268.7 88 276.6 71.4 305v.1a8 8 0 000 7.2l.2.4a8 8 0 006 3.4h67.3l-.6-3.4A99 99 0 0097.2 242.5l-6.8-4 33.4-57.8 6.9 4a163 163 0 0181.6 128.8l.2 2.7h33.7L246 313A196 196 0 00147.8 155.7l-7-4L178.6 87v-.1a25 25 0 0133-9.1l.8.3a25 25 0 019 8.9h.1v.2L343.7 296.4a24 24 0 013.3 12v.4a24.5 24.5 0 01-24.6 24.4H291.5',
24+
'M85.7 264.5l-8.7 15-12.5 21.3-.1.2-.1.3A15.6 15.6 0 0076.9 324h77.2l-1.8-12.6a107 107 0 00-51-75.8l25.5-44.1A155 155 0 01204.3 314l.8 10.1h49.7l-.8-11.7A204 204 0 00151.8 148.9l33.6-57.7a17 17 0 0129.2 0L336.8 300.4a16 16 0 012.2 8.2 16.5 16.5 0 01-16.6 16.6H291.5',
25+
];
26+
27+
/**
28+
* Sentry brand mark with stroke-draw progress, adapted from the app
29+
* loading animation in static/index.ejs. Single color, progress-driven
30+
* instead of the looping multicolor original.
31+
*/
32+
export function AnimatedSentryLogo({
33+
progress,
34+
size = 72,
35+
className,
36+
}: AnimatedSentryLogoProps) {
37+
const theme = useTheme();
38+
const id = useId();
39+
const gooId = `goo-layer-${id}`;
40+
const maskId = `mask-layer-${id}`;
41+
42+
const dashOffset = DASH_TOTAL * (1 - progress);
43+
44+
return (
45+
<svg
46+
width={size}
47+
height={size}
48+
viewBox="0 0 400 400"
49+
fill="none"
50+
xmlns="http://www.w3.org/2000/svg"
51+
className={className}
52+
role="img"
53+
aria-label="Sentry"
54+
>
55+
<defs>
56+
<filter id={gooId}>
57+
<feGaussianBlur in="SourceGraphic" stdDeviation="8" result="blur" />
58+
<feColorMatrix
59+
in="blur"
60+
type="matrix"
61+
values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 36 -4"
62+
result="goo"
63+
/>
64+
<feComposite in="SourceGraphic" in2="goo" operator="atop" />
65+
</filter>
66+
</defs>
67+
68+
<mask
69+
id={maskId}
70+
width="300"
71+
height="265"
72+
x="50"
73+
y="72"
74+
maskUnits="userSpaceOnUse"
75+
style={{maskType: 'alpha'}}
76+
>
77+
<path fill={theme.tokens.content.accent} d={SENTRY_GLYPH} />
78+
</mask>
79+
80+
{/* Muted base shape */}
81+
<path fill={theme.tokens.graphics.neutral.muted} d={SENTRY_GLYPH} />
82+
83+
{/* Stroke-draw overlay, masked to the logo shape */}
84+
<g
85+
mask={`url(#${maskId})`}
86+
filter={`url(#${gooId})`}
87+
stroke={theme.tokens.content.accent}
88+
strokeWidth="5"
89+
strokeLinecap="round"
90+
strokeLinejoin="round"
91+
>
92+
{STROKE_PATHS.map((d, i) => (
93+
<path
94+
key={i}
95+
d={d}
96+
strokeDasharray={DASH_TOTAL}
97+
strokeDashoffset={dashOffset}
98+
style={{transition: 'stroke-dashoffset 600ms ease-out'}}
99+
/>
100+
))}
101+
</g>
102+
</svg>
103+
);
104+
}

static/app/views/onboarding/onboarding.tsx

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@ import styled from '@emotion/styled';
33
import {AnimatePresence, motion} from 'framer-motion';
44

55
import {Button} from '@sentry/scraps/button';
6-
import {Stack} from '@sentry/scraps/layout';
6+
import {Container, Stack} from '@sentry/scraps/layout';
77
import {Link} from '@sentry/scraps/link';
88

9+
import {AnimatedSentryLogo} from 'sentry/components/animatedSentryLogo';
910
import Hook from 'sentry/components/hook';
1011
import {LogoSentry} from 'sentry/components/logoSentry';
1112
import {
@@ -100,6 +101,16 @@ const scmOnboardingSteps: StepDescriptor[] = [
100101
},
101102
];
102103

104+
/**
105+
* The SCM steps that display the animated logo progress indicator.
106+
* Order determines the progress level (first = 0, last = 1).
107+
*/
108+
const SCM_LOGO_STEPS = [
109+
OnboardingStepId.SCM_CONNECT,
110+
OnboardingStepId.SCM_PLATFORM_FEATURES,
111+
OnboardingStepId.SCM_PROJECT_DETAILS,
112+
];
113+
103114
function WelcomeVariable(props: StepProps) {
104115
const hasNewWelcomeUI = useHasNewWelcomeUI();
105116

@@ -116,7 +127,9 @@ interface ContainerVariableProps {
116127

117128
function ContainerVariable(props: PropsWithChildren<ContainerVariableProps>) {
118129
const newWelcomeUIStep = props.hasNewWelcomeUI && props.id === OnboardingStepId.WELCOME;
119-
const Component = newWelcomeUIStep ? ContainerNewWelcomeUI : Container;
130+
const Component = newWelcomeUIStep
131+
? OnboardingContainerNewWelcomeUI
132+
: OnboardingContainer;
120133

121134
return (
122135
<Component hasFooter={props.hasFooter || newWelcomeUIStep}>
@@ -301,6 +314,12 @@ export function OnboardingWithoutContext() {
301314
);
302315
};
303316

317+
const scmLogoIndex = stepObj ? SCM_LOGO_STEPS.indexOf(stepObj.id) : -1;
318+
const scmLogoProgress =
319+
scmLogoIndex >= 0 && SCM_LOGO_STEPS.length > 1
320+
? scmLogoIndex / (SCM_LOGO_STEPS.length - 1)
321+
: null;
322+
304323
// Redirect to the first step if we end up in an invalid state
305324
const isInvalidDocsStep = stepId === 'setup-docs' && !projectSlug;
306325
if (!stepObj || stepIndex === -1 || isInvalidDocsStep) {
@@ -370,6 +389,11 @@ export function OnboardingWithoutContext() {
370389
</Button>
371390
</BackMotionDiv>
372391
)}
392+
{scmLogoProgress !== null && (
393+
<Container alignSelf="center">
394+
<AnimatedSentryLogo progress={scmLogoProgress} />
395+
</Container>
396+
)}
373397
<AnimatePresence mode="wait" onExitComplete={updateAnimationState}>
374398
<OnboardingStepVariable id={stepObj.id} hasNewWelcomeUI={hasNewWelcomeUI}>
375399
{stepObj.Component && (
@@ -400,7 +424,7 @@ function Onboarding() {
400424
);
401425
}
402426

403-
const ContainerNewWelcomeUI = styled('div')<{hasFooter: boolean}>`
427+
const OnboardingContainerNewWelcomeUI = styled('div')<{hasFooter: boolean}>`
404428
flex-grow: 1;
405429
display: flex;
406430
flex-direction: column;
@@ -419,7 +443,7 @@ const ContainerNewWelcomeUI = styled('div')<{hasFooter: boolean}>`
419443
}
420444
`;
421445

422-
const Container = styled('div')<{hasFooter: boolean}>`
446+
const OnboardingContainer = styled('div')<{hasFooter: boolean}>`
423447
flex-grow: 1;
424448
display: flex;
425449
flex-direction: column;

0 commit comments

Comments
 (0)