diff --git a/static/app/views/onboarding/components/scmAlertFrequency.tsx b/static/app/views/onboarding/components/scmAlertFrequency.tsx
index a17e07c47b6704..8f7b04968b02e7 100644
--- a/static/app/views/onboarding/components/scmAlertFrequency.tsx
+++ b/static/app/views/onboarding/components/scmAlertFrequency.tsx
@@ -1,8 +1,9 @@
import {Input} from '@sentry/scraps/input';
-import {Container, Grid, Stack} from '@sentry/scraps/layout';
+import {Flex, Grid, Stack} from '@sentry/scraps/layout';
import {Select} from '@sentry/scraps/select';
import {Text} from '@sentry/scraps/text';
+import {IconInfo} from 'sentry/icons/iconInfo';
import {t} from 'sentry/locale';
import {ScmAlertOptionCard} from 'sentry/views/onboarding/components/scmAlertOptionCard';
import {
@@ -31,72 +32,73 @@ export function ScmAlertFrequency({
const isLaterSelected = alertSetting === RuleAction.CREATE_ALERT_LATER;
return (
-
- onFieldChange('alertSetting', RuleAction.DEFAULT_ALERT)}
- />
+
+
+ onFieldChange('alertSetting', RuleAction.DEFAULT_ALERT)}
+ />
- onFieldChange('alertSetting', RuleAction.CUSTOMIZED_ALERTS)}
- >
-
-
-
-
+ onFieldChange('alertSetting', RuleAction.CUSTOMIZED_ALERTS)}
+ >
+ {isCustomSelected && (
+
+
{t('When there are more than')}
-
-
- onFieldChange('threshold', e.target.value)}
- disabled={!isCustomSelected}
- />
-
-
-
-
+
+ onFieldChange('threshold', e.target.value)}
+ />
+
+
+
{t('a unique error in')}
-
-
-
-
-
+ )}
+
+
+ onFieldChange('alertSetting', RuleAction.CREATE_ALERT_LATER)}
+ />
+
- onFieldChange('alertSetting', RuleAction.CREATE_ALERT_LATER)}
- />
+
+
+
+ {t('You can always change alerts after project creation')}
+
+
);
}
diff --git a/static/app/views/onboarding/components/scmAlertOptionCard.tsx b/static/app/views/onboarding/components/scmAlertOptionCard.tsx
index 6b7e06ce8f9d67..93dd490615bf47 100644
--- a/static/app/views/onboarding/components/scmAlertOptionCard.tsx
+++ b/static/app/views/onboarding/components/scmAlertOptionCard.tsx
@@ -1,8 +1,11 @@
-import {Grid, Stack} from '@sentry/scraps/layout';
+import styled from '@emotion/styled';
+
+import {Container, Flex, Grid, Stack} from '@sentry/scraps/layout';
import {Radio} from '@sentry/scraps/radio';
import {Text} from '@sentry/scraps/text';
import {ScmCardButton} from 'sentry/views/onboarding/components/scmCardButton';
+import {ScmCollapsibleReveal} from 'sentry/views/onboarding/components/scmCollapsibleReveal';
import {ScmSelectableContainer} from 'sentry/views/onboarding/components/scmSelectableContainer';
interface ScmAlertOptionCardProps {
@@ -10,27 +13,70 @@ interface ScmAlertOptionCardProps {
label: string;
onSelect: () => void;
children?: React.ReactNode;
+ description?: string;
}
export function ScmAlertOptionCard({
label,
+ description,
isSelected,
onSelect,
children,
}: ScmAlertOptionCardProps) {
return (
-
-
-
-
-
-
- {label}
-
-
-
-
- {children}
-
+
+
+ {/* The padding lives on the button (not the card) so the whole header,
+ edge to edge, is part of the click target. */}
+
+
+
+
+
+
+
+
+ {label}
+
+
+ {description && (
+
+
+ {description}
+
+
+ )}
+
+
+
+ {/* Selecting the card expands its body; ScmCollapsibleReveal's height
+ tween lets cards in scmCreateProject's layout="position" group
+ reflow smoothly. */}
+
+ {children}
+
+
+
);
}
+
+// The body indents to line up under the label (header button padding + the xs
+// radio's 12px width + grid column gap) and insets its right/bottom to match
+// the header's padding, since the card itself carries none. The top gap comes
+// from the header button's own bottom padding.
+const ExpandedBody = styled('div')`
+ padding: 0 ${p => p.theme.space.lg} ${p => p.theme.space.lg};
+ padding-left: calc(${p => p.theme.space.lg} + 12px + ${p => p.theme.space.md});
+`;
diff --git a/static/app/views/onboarding/components/scmCollapsibleReveal.tsx b/static/app/views/onboarding/components/scmCollapsibleReveal.tsx
new file mode 100644
index 00000000000000..7fd39af60d3eb5
--- /dev/null
+++ b/static/app/views/onboarding/components/scmCollapsibleReveal.tsx
@@ -0,0 +1,51 @@
+import {useState} from 'react';
+import {AnimatePresence, motion} from 'framer-motion';
+
+interface ScmCollapsibleRevealProps {
+ children: React.ReactNode;
+ /** When true the content is shown; toggling tweens height and opacity. */
+ open: boolean;
+ /** Forwarded to the animated element, e.g. as an aria-controls target. */
+ id?: string;
+}
+
+/**
+ * Reveals or hides content with a height + fade tween. Shared by
+ * ScmCollapsibleSection and ScmAlertOptionCard so their expand/collapse timing
+ * stays in sync. Animating height (rather than display) lets sibling cards in a
+ * framer-motion layout="position" group reflow via normal document flow.
+ * initial={false} renders the open state without animating on mount.
+ */
+export function ScmCollapsibleReveal({open, id, children}: ScmCollapsibleRevealProps) {
+ // overflow:hidden is needed while the height tween runs so the content clips
+ // cleanly, but kept on it would also clip anything that extends past the
+ // settled bounds, e.g. a focus ring at the edge or an open select menu below.
+ // Switch to visible once open and settled, back to hidden whenever animating.
+ const [overflow, setOverflow] = useState<'hidden' | 'visible'>(
+ open ? 'visible' : 'hidden'
+ );
+
+ return (
+
+ {open && (
+ setOverflow('hidden')}
+ onAnimationComplete={() => {
+ if (open) {
+ setOverflow('visible');
+ }
+ }}
+ style={{overflow, width: '100%'}}
+ >
+ {children}
+
+ )}
+
+ );
+}
diff --git a/static/app/views/onboarding/components/scmCollapsibleSection.tsx b/static/app/views/onboarding/components/scmCollapsibleSection.tsx
index 7b8c3acc19b1e7..7bc7d60725aba2 100644
--- a/static/app/views/onboarding/components/scmCollapsibleSection.tsx
+++ b/static/app/views/onboarding/components/scmCollapsibleSection.tsx
@@ -1,12 +1,12 @@
import {useId, useState} from 'react';
import styled from '@emotion/styled';
-import {AnimatePresence, motion} from 'framer-motion';
import {Button} from '@sentry/scraps/button';
import {Flex, Stack} from '@sentry/scraps/layout';
import {Text} from '@sentry/scraps/text';
import {IconChevron} from 'sentry/icons';
+import {ScmCollapsibleReveal} from 'sentry/views/onboarding/components/scmCollapsibleReveal';
interface ScmCollapsibleSectionProps {
children: React.ReactNode;
@@ -64,21 +64,9 @@ export function ScmCollapsibleSection({
{trailing}
-
- {expanded && (
-
- {children}
-
- )}
-
+
+ {children}
+
);
}
diff --git a/static/app/views/onboarding/scmProjectDetails.spec.tsx b/static/app/views/onboarding/scmProjectDetails.spec.tsx
index 8a9ab886e2c94d..dbc0420adf53e6 100644
--- a/static/app/views/onboarding/scmProjectDetails.spec.tsx
+++ b/static/app/views/onboarding/scmProjectDetails.spec.tsx
@@ -115,8 +115,14 @@ describe('ScmProjectDetails', () => {
});
expect(await screen.findByText('High priority issues')).toBeInTheDocument();
- expect(screen.getByText('Custom')).toBeInTheDocument();
- expect(screen.getByText("I'll create my own alerts later")).toBeInTheDocument();
+ expect(
+ screen.getByText('Alert on new, regressed, and escalating issues')
+ ).toBeInTheDocument();
+ expect(screen.getByText('Custom threshold')).toBeInTheDocument();
+ expect(screen.getByText("I'll set up alerts later")).toBeInTheDocument();
+ expect(
+ screen.getByText('You can always change alerts after project creation')
+ ).toBeInTheDocument();
});
it('re-derives the fields when the host clears the form', () => {
diff --git a/static/app/views/projectInstall/scmCreateProject.tsx b/static/app/views/projectInstall/scmCreateProject.tsx
index 77ae0938075bb8..92df68b74825ae 100644
--- a/static/app/views/projectInstall/scmCreateProject.tsx
+++ b/static/app/views/projectInstall/scmCreateProject.tsx
@@ -281,7 +281,7 @@ function ScmCreateProjectWizard({initialState}: {initialState: WizardState}) {
>
{t('Create a new project')}
-
+
{t('Create a project')}
{tct(
diff --git a/tests/acceptance/test_scm_onboarding.py b/tests/acceptance/test_scm_onboarding.py
index 9680c17d866461..395f92887ff91a 100644
--- a/tests/acceptance/test_scm_onboarding.py
+++ b/tests/acceptance/test_scm_onboarding.py
@@ -744,12 +744,10 @@ def test_scm_back_from_setup_docs_active_project_alert_changed(self) -> None:
self.browser.click('[aria-label="Back"]')
self.browser.wait_until('[data-test-id="onboarding-step-scm-project-details"]')
- # Switch alerts from "High priority issues" to "create later".
- self.browser.click(
- xpath='//button[@role="radio"][contains(., "create my own alerts later")]'
- )
+ # Switch alerts from "High priority issues" to "set up later".
+ self.browser.click(xpath='//button[@role="radio"][contains(., "set up alerts later")]')
self.browser.wait_until(
- xpath='//button[@role="radio"][@aria-checked="true"][contains(., "create my own alerts later")]'
+ xpath='//button[@role="radio"][@aria-checked="true"][contains(., "set up alerts later")]'
)
self.browser.wait_until_clickable(xpath='//button[contains(., "Create project")]')
self.browser.click(xpath='//button[contains(., "Create project")]')