diff --git a/static/app/components/animatedSentryLogo.tsx b/static/app/components/animatedSentryLogo.tsx new file mode 100644 index 00000000000000..afc089e5b5f11f --- /dev/null +++ b/static/app/components/animatedSentryLogo.tsx @@ -0,0 +1,104 @@ +import {useId} from 'react'; +import {useTheme} from '@emotion/react'; + +interface AnimatedSentryLogoProps { + /** + * Draw progress from 0 (no stroke) to 1 (fully drawn). + * Uses the stroke-dashoffset technique from the app loading screen. + */ + progress: number; + className?: string; + size?: number; +} + +const DASH_TOTAL = 1150; + +// Filled glyph (same path as index.ejs base + mask) +const SENTRY_GLYPH = + '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'; + +// Stroke paths that trace the logo outline (one group from index.ejs) +const STROKE_PATHS = [ + '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', + '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', + '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', +]; + +/** + * Sentry brand mark with stroke-draw progress, adapted from the app + * loading animation in static/index.ejs. Single color, progress-driven + * instead of the looping multicolor original. + */ +export function AnimatedSentryLogo({ + progress, + size = 72, + className, +}: AnimatedSentryLogoProps) { + const theme = useTheme(); + const id = useId(); + const gooId = `goo-layer-${id}`; + const maskId = `mask-layer-${id}`; + + const dashOffset = DASH_TOTAL * (1 - progress); + + return ( + + + + + + + + + + + + + + {/* Muted base shape */} + + + {/* Stroke-draw overlay, masked to the logo shape */} + + {STROKE_PATHS.map((d, i) => ( + + ))} + + + ); +} diff --git a/static/app/views/onboarding/onboarding.tsx b/static/app/views/onboarding/onboarding.tsx index 6bb306970484ae..73f322b5d6aab7 100644 --- a/static/app/views/onboarding/onboarding.tsx +++ b/static/app/views/onboarding/onboarding.tsx @@ -3,9 +3,10 @@ import styled from '@emotion/styled'; import {AnimatePresence, motion} from 'framer-motion'; import {Button} from '@sentry/scraps/button'; -import {Stack} from '@sentry/scraps/layout'; +import {Container, Stack} from '@sentry/scraps/layout'; import {Link} from '@sentry/scraps/link'; +import {AnimatedSentryLogo} from 'sentry/components/animatedSentryLogo'; import Hook from 'sentry/components/hook'; import {LogoSentry} from 'sentry/components/logoSentry'; import { @@ -101,6 +102,16 @@ const scmOnboardingSteps: StepDescriptor[] = [ }, ]; +/** + * The SCM steps that display the animated logo progress indicator. + * Order determines the progress level (first = 0, last = 1). + */ +const SCM_LOGO_STEPS = [ + OnboardingStepId.SCM_CONNECT, + OnboardingStepId.SCM_PLATFORM_FEATURES, + OnboardingStepId.SCM_PROJECT_DETAILS, +]; + function WelcomeVariable(props: StepProps) { const hasNewWelcomeUI = useHasNewWelcomeUI(); @@ -117,7 +128,9 @@ interface ContainerVariableProps { function ContainerVariable(props: PropsWithChildren) { const newWelcomeUIStep = props.hasNewWelcomeUI && props.id === OnboardingStepId.WELCOME; - const Component = newWelcomeUIStep ? ContainerNewWelcomeUI : Container; + const Component = newWelcomeUIStep + ? OnboardingContainerNewWelcomeUI + : OnboardingContainer; return ( @@ -302,6 +315,12 @@ export function OnboardingWithoutContext() { ); }; + const scmLogoIndex = stepObj ? SCM_LOGO_STEPS.indexOf(stepObj.id) : -1; + const scmLogoProgress = + scmLogoIndex >= 0 && SCM_LOGO_STEPS.length > 1 + ? scmLogoIndex / (SCM_LOGO_STEPS.length - 1) + : null; + // Redirect to the first step if we end up in an invalid state const isInvalidDocsStep = stepId === 'setup-docs' && !projectSlug; if (!stepObj || stepIndex === -1 || isInvalidDocsStep) { @@ -372,6 +391,11 @@ export function OnboardingWithoutContext() { )} + {scmLogoProgress !== null && ( + + + + )} {stepObj.Component && ( @@ -402,7 +426,7 @@ function Onboarding() { ); } -const ContainerNewWelcomeUI = styled('div')<{hasFooter: boolean}>` +const OnboardingContainerNewWelcomeUI = styled('div')<{hasFooter: boolean}>` flex-grow: 1; display: flex; flex-direction: column; @@ -421,7 +445,7 @@ const ContainerNewWelcomeUI = styled('div')<{hasFooter: boolean}>` } `; -const Container = styled('div')<{hasFooter: boolean}>` +const OnboardingContainer = styled('div')<{hasFooter: boolean}>` flex-grow: 1; display: flex; flex-direction: column;