From 715fe5840c75b14f8df48114bf8742bb5e59245e Mon Sep 17 00:00:00 2001 From: Jay Goss Date: Thu, 6 Aug 2026 16:20:35 -0500 Subject: [PATCH 1/2] fix(onboarding): Clip the SCM product section reveal The product cards mounted at their full height, so they painted over the project details, alert, and CTA sections while framer-motion animated those layout="position" siblings into place. Wrap the section in ScmCollapsibleReveal, the height-tween-behind-a-clip pattern the rest of the folder already uses. The outer reveal replaces the early `return null` so the section can tween in and out, and it carries the host's trailing divider with it. An inner reveal covers project creation, where the Products header persists and only the cards appear, so the outer one never toggles. The section Stack drops to gap="0" with the spacing moved inside the reveal as padding, since a flex gap would snap in at full size while the revealed height was still 0. Fixes VDY-150 --- .../scm/scmFeatureSelectionPanel.spec.tsx | 74 ++++++++ .../scm/scmFeatureSelectionPanel.tsx | 169 ++++++++++-------- 2 files changed, 168 insertions(+), 75 deletions(-) diff --git a/static/app/components/onboarding/scm/scmFeatureSelectionPanel.spec.tsx b/static/app/components/onboarding/scm/scmFeatureSelectionPanel.spec.tsx index 527bfba408ee..fcb4bf087ba3 100644 --- a/static/app/components/onboarding/scm/scmFeatureSelectionPanel.spec.tsx +++ b/static/app/components/onboarding/scm/scmFeatureSelectionPanel.spec.tsx @@ -16,6 +16,17 @@ const pythonPlatform: OnboardingSelectedSDK = { category: 'popular', }; +// In neither platformProductAvailability nor PLATFORM_PRODUCT_INFO, so the +// section has nothing to configure. +const platformWithoutProducts: OnboardingSelectedSDK = { + key: 'other', + name: 'Other', + language: 'other', + type: 'language', + link: 'https://docs.sentry.io/platforms/', + category: 'popular', +}; + function defaultProps(overrides: Partial> = {}) { return { analyticsFlow: 'onboarding' as const, @@ -61,4 +72,67 @@ describe('ScmFeatureSelectionPanel', () => { expect(screen.queryByText(/unlimited volume for 14 days/)).not.toBeInTheDocument(); expect(screen.queryByText('5,000 errors / mo')).not.toBeInTheDocument(); }); + + it('prompts for a platform without showing cards in project creation', async () => { + render( + , + {organization} + ); + + expect(await screen.findByText('Products')).toBeInTheDocument(); + expect( + screen.getByText('Select a platform to configure products') + ).toBeInTheDocument(); + expect(screen.queryByRole('checkbox', {name: /Tracing/})).not.toBeInTheDocument(); + }); + + it('reveals the cards once a platform with products is chosen', async () => { + const {rerender} = render( + , + {organization} + ); + + expect(await screen.findByText('Products')).toBeInTheDocument(); + expect(screen.queryByRole('checkbox', {name: /Tracing/})).not.toBeInTheDocument(); + + rerender( + + ); + + expect(await screen.findByRole('checkbox', {name: /Tracing/})).toBeInTheDocument(); + expect( + screen.queryByText('Select a platform to configure products') + ).not.toBeInTheDocument(); + }); + + it('drops the section and its trailing divider for a platform with no products', () => { + render( + Trailing divider, + })} + />, + {organization} + ); + + expect(screen.queryByText('Products')).not.toBeInTheDocument(); + expect(screen.queryByText('Trailing divider')).not.toBeInTheDocument(); + }); }); diff --git a/static/app/components/onboarding/scm/scmFeatureSelectionPanel.tsx b/static/app/components/onboarding/scm/scmFeatureSelectionPanel.tsx index 56b18ae3f722..506682147cdf 100644 --- a/static/app/components/onboarding/scm/scmFeatureSelectionPanel.tsx +++ b/static/app/components/onboarding/scm/scmFeatureSelectionPanel.tsx @@ -1,8 +1,8 @@ -import {Fragment, type ReactNode, useCallback, useMemo} from 'react'; +import {type ReactNode, useCallback, useMemo} from 'react'; import {motion} from 'framer-motion'; import {Tag} from '@sentry/scraps/badge'; -import {Flex, Stack} from '@sentry/scraps/layout'; +import {Container, Flex, Stack} from '@sentry/scraps/layout'; import {Heading, Text} from '@sentry/scraps/text'; import {ProductSolution} from 'sentry/components/onboarding/gettingStartedDoc/types'; @@ -19,6 +19,7 @@ import {trackAnalytics} from 'sentry/utils/analytics'; import {useOrganization} from 'sentry/utils/useOrganization'; import {type ScmAnalyticsFlow, scmFlowVariantParams} from './scmAnalyticsFlow'; +import {ScmCollapsibleReveal} from './scmCollapsibleReveal'; import {ScmFeatureInfoCards} from './scmFeatureInfoCards'; import {ScmFeatureSelectionCards} from './scmFeatureSelectionCards'; import { @@ -40,9 +41,10 @@ interface ScmFeatureSelectionPanelProps { selectedFeatures: ProductSolution[] | undefined; selectedPlatform: OnboardingSelectedSDK | undefined; selectedRepository: Repository | undefined; - // Optional element rendered as a sibling after the panel content (e.g. a - // divider from the host). Dropped together with the panel when there is - // nothing to show, so the host never strands an orphaned divider. + // Optional element rendered after the panel content (e.g. a divider from the + // host). Lives inside the panel's reveal, so it is dropped — and tweens away + // — together with the panel when there is nothing to show, and the host never + // strands an orphaned divider. trailing?: ReactNode; } @@ -168,81 +170,98 @@ export function ScmFeatureSelectionPanel({ ] ); - // Hide the whole section when a resolved platform has no configurable + const hasFeatureCards = featureMode !== 'none'; + + // Show the whole section unless a resolved platform has no configurable // products. Before a platform is chosen (no resolved key), keep it visible in // project creation for the select-a-platform prompt; onboarding hides both. - if (featureMode === 'none' && (isOnboarding || !!currentPlatformKey)) { - return null; - } + // Expressed as a flag rather than an early `return null` so the reveal below + // stays mounted and can tween the section in and out. AnimatePresence's + // `initial={false}` still renders the settled state on first mount, so a page + // load with a platform already resolved does not animate. + const showSection = hasFeatureCards || (!isOnboarding && !currentPlatformKey); return ( - - - - {isOnboarding ? ( - - - - {tct( - 'You’ve got [bold:unlimited volume for 14 days] to try out everything. After that, free plan volumes apply ⋅ No credit card required', - { - bold: ( - - {null} - - ), - } + + {/* The panel and the host's trailing divider are a single reveal child + now, so the gap the host's section Stack used to put between them + lives here. Only project creation passes `trailing`. */} + + + {/* gap="0" because the spacing above the cards has to tween with + them: a flex gap would snap in at full size while the revealed + height is still 0, jumping the layout by that much. The cards own + it as padding inside the reveal instead. */} + + {isOnboarding ? ( + + + + {tct( + 'You’ve got [bold:unlimited volume for 14 days] to try out everything. After that, free plan volumes apply ⋅ No credit card required', + { + bold: ( + + {null} + + ), + } + )} + + + ) : null} + + {isOnboarding ? null : ( + + {t('Products')} + {currentPlatformKey ? null : ( + } style={{minWidth: 0}}> + + {t('Select a platform to configure products')} + + )} - - - ) : null} - - {isOnboarding ? null : ( - - {t('Products')} - {currentPlatformKey ? null : ( - } style={{minWidth: 0}}> - - {t('Select a platform to configure products')} - - - )} - - )} - - {featureMode === 'toggleable' ? ( - - ) : featureMode === 'informational' ? ( - - ) : null} - - - {trailing} - + + )} + + {/* The cards mount at their full height, so without a clipped + height tween they paint over the sections below while + framer-motion is still animating those into place. */} + + + {featureMode === 'toggleable' ? ( + + ) : featureMode === 'informational' ? ( + + ) : null} + + + + + {trailing} + + ); } From 3919fbb99aedb157a961cab1ee172d92e81e9576 Mon Sep 17 00:00:00 2001 From: Jay Goss Date: Thu, 6 Aug 2026 17:12:26 -0500 Subject: [PATCH 2/2] fix(project-creation): Collapse spacing with the products section The card spaced its sections with a flex gap, which sits outside the box ScmFeatureSelectionPanel tweens. The gap survived the whole collapse and then disappeared in one step on unmount, so the last 24px of the animation looked like a jump rather than part of the tween. Give the card gap="0" and let each section carry its own paddingBottom, so the collapsing section's spacing lives inside the animated box. The divider below the section is already passed in as `trailing`, so it carries the space on both sides of itself and the panel needs no spacing of its own. Onboarding still uses a flex gap and is unchanged. --- .../scm/scmFeatureSelectionPanel.tsx | 9 ++-- .../views/projectInstall/scmCreateProject.tsx | 43 ++++++++++++------- 2 files changed, 32 insertions(+), 20 deletions(-) diff --git a/static/app/components/onboarding/scm/scmFeatureSelectionPanel.tsx b/static/app/components/onboarding/scm/scmFeatureSelectionPanel.tsx index 506682147cdf..620e842f4965 100644 --- a/static/app/components/onboarding/scm/scmFeatureSelectionPanel.tsx +++ b/static/app/components/onboarding/scm/scmFeatureSelectionPanel.tsx @@ -183,10 +183,11 @@ export function ScmFeatureSelectionPanel({ return ( - {/* The panel and the host's trailing divider are a single reveal child - now, so the gap the host's section Stack used to put between them - lives here. Only project creation passes `trailing`. */} - + {/* No gap: the section claims no spacing of its own, so everything that + tweens away on collapse is inside this box. Hosts own the rhythm — + the section above supplies the space before it, and `trailing` + carries the space around itself. */} + {/* gap="0" because the spacing above the cards has to tween with them: a flex gap would snap in at full size while the revealed diff --git a/static/app/views/projectInstall/scmCreateProject.tsx b/static/app/views/projectInstall/scmCreateProject.tsx index e41771b6eb79..3f2622502bbf 100644 --- a/static/app/views/projectInstall/scmCreateProject.tsx +++ b/static/app/views/projectInstall/scmCreateProject.tsx @@ -3,7 +3,7 @@ import {LayoutGroup, motion} from 'framer-motion'; import {Tag} from '@sentry/scraps/badge'; import {Button} from '@sentry/scraps/button'; -import {Flex, Stack} from '@sentry/scraps/layout'; +import {Container, Flex, Stack} from '@sentry/scraps/layout'; import {ExternalLink} from '@sentry/scraps/link'; import {Separator} from '@sentry/scraps/separator'; import {Heading, Text} from '@sentry/scraps/text'; @@ -236,9 +236,15 @@ function ScmCreateProjectWizard({initialState}: {initialState: WizardState}) { + {/* Section rhythm is each section's own paddingBottom rather than a + flex gap on this Stack. ScmFeatureSelectionPanel collapses to + nothing when its platform has no products, and a flex gap sits + outside the box it tweens: it would survive the whole collapse and + then vanish in one step on unmount. As padding it lives inside the + animated box and tweens away with the content. */} {t('Create a new project')} - + {t('Create a project')} {tct( @@ -262,7 +268,7 @@ function ScmCreateProjectWizard({initialState}: {initialState: WizardState}) { - + {t('Repository')} @@ -287,7 +293,7 @@ function ScmCreateProjectWizard({initialState}: {initialState: WizardState}) { /> - + - + - + - + + {/* The divider below the section is passed in so it collapses + with it. It carries the surrounding rhythm as padding for the + same reason the Stack above uses none. */} + - + } /> - + - + - + - + - + {/* Last section, so no trailing padding. */} + - + {/* Page-level CTA: disabled until a platform and project details are ready. */} @@ -370,3 +380,4 @@ function ScmCreateProjectWizard({initialState}: {initialState: WizardState}) { } const MotionStack = motion.create(Stack); +const MotionContainer = motion.create(Container);