123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875 |
- import {Fragment, useEffect, useState} from 'react';
- import styled from '@emotion/styled';
- import * as Sentry from '@sentry/react';
- import emptyStateImg from 'sentry-images/spot/performance-empty-state.svg';
- import emptyTraceImg from 'sentry-images/spot/performance-empty-trace.svg';
- import tourAlert from 'sentry-images/spot/performance-tour-alert.svg';
- import tourCorrelate from 'sentry-images/spot/performance-tour-correlate.svg';
- import tourMetrics from 'sentry-images/spot/performance-tour-metrics.svg';
- import tourTrace from 'sentry-images/spot/performance-tour-trace.svg';
- import {
- addErrorMessage,
- addLoadingMessage,
- clearIndicators,
- } from 'sentry/actionCreators/indicator';
- import type {Client} from 'sentry/api';
- import UnsupportedAlert from 'sentry/components/alerts/unsupportedAlert';
- import {Button, LinkButton} from 'sentry/components/button';
- import ButtonBar from 'sentry/components/buttonBar';
- import {GuidedSteps} from 'sentry/components/guidedSteps/guidedSteps';
- import LoadingIndicator from 'sentry/components/loadingIndicator';
- import type {TourStep} from 'sentry/components/modals/featureTourModal';
- import FeatureTourModal, {
- TourImage,
- TourText,
- } from 'sentry/components/modals/featureTourModal';
- import {AuthTokenGeneratorProvider} from 'sentry/components/onboarding/gettingStartedDoc/authTokenGenerator';
- import {OnboardingCodeSnippet} from 'sentry/components/onboarding/gettingStartedDoc/onboardingCodeSnippet';
- import {
- type Configuration,
- TabbedCodeSnippet,
- } from 'sentry/components/onboarding/gettingStartedDoc/step';
- import {
- type DocsParams,
- ProductSolution,
- } from 'sentry/components/onboarding/gettingStartedDoc/types';
- import {useLoadGettingStarted} from 'sentry/components/onboarding/gettingStartedDoc/utils/useLoadGettingStarted';
- import LegacyOnboardingPanel from 'sentry/components/onboardingPanel';
- import Panel from 'sentry/components/panels/panel';
- import PanelBody from 'sentry/components/panels/panelBody';
- import {filterProjects} from 'sentry/components/performanceOnboarding/utils';
- import {SidebarPanelKey} from 'sentry/components/sidebar/types';
- import {
- withoutPerformanceSupport,
- withPerformanceOnboarding,
- } from 'sentry/data/platformCategories';
- import platforms, {otherPlatform} from 'sentry/data/platforms';
- import {t, tct} from 'sentry/locale';
- import ConfigStore from 'sentry/stores/configStore';
- import SidebarPanelStore from 'sentry/stores/sidebarPanelStore';
- import {useLegacyStore} from 'sentry/stores/useLegacyStore';
- import pulsingIndicatorStyles from 'sentry/styles/pulsingIndicator';
- import {space} from 'sentry/styles/space';
- import type {Organization} from 'sentry/types/organization';
- import type {Project} from 'sentry/types/project';
- import {trackAnalytics} from 'sentry/utils/analytics';
- import {browserHistory} from 'sentry/utils/browserHistory';
- import {generateLinkToEventInTraceView} from 'sentry/utils/discover/urls';
- import EventWaiter from 'sentry/utils/eventWaiter';
- import useApi from 'sentry/utils/useApi';
- import {useLocation} from 'sentry/utils/useLocation';
- import useProjects from 'sentry/utils/useProjects';
- import {traceAnalytics} from './newTraceDetails/traceAnalytics';
- const performanceSetupUrl =
- 'https://docs.sentry.io/performance-monitoring/getting-started/';
- const docsLink = (
- <LinkButton external href={performanceSetupUrl}>
- {t('Setup')}
- </LinkButton>
- );
- export const PERFORMANCE_TOUR_STEPS: TourStep[] = [
- {
- title: t('Track Application Metrics'),
- image: <TourImage src={tourMetrics} />,
- body: (
- <TourText>
- {t(
- 'Monitor your slowest pageloads and APIs to see which users are having the worst time.'
- )}
- </TourText>
- ),
- actions: docsLink,
- },
- {
- title: t('Correlate Errors and Traces'),
- image: <TourImage src={tourCorrelate} />,
- body: (
- <TourText>
- {t(
- 'See what errors occurred within a transaction and the impact of those errors.'
- )}
- </TourText>
- ),
- actions: docsLink,
- },
- {
- title: t('Watch and Alert'),
- image: <TourImage src={tourAlert} />,
- body: (
- <TourText>
- {t(
- 'Highlight mission-critical pages and APIs and set latency alerts to notify you before things go wrong.'
- )}
- </TourText>
- ),
- actions: docsLink,
- },
- {
- title: t('Trace Across Systems'),
- image: <TourImage src={tourTrace} />,
- body: (
- <TourText>
- {t(
- "Follow a trace from a user's session and drill down to identify any bottlenecks that occur."
- )}
- </TourText>
- ),
- },
- ];
- type SampleButtonProps = {
- api: Client;
- errorMessage: React.ReactNode;
- loadingMessage: React.ReactNode;
- organization: Organization;
- project: Project;
- triggerText: React.ReactNode;
- };
- function SampleButton({
- triggerText,
- loadingMessage,
- errorMessage,
- project,
- organization,
- api,
- }: SampleButtonProps) {
- const location = useLocation();
- return (
- <Button
- data-test-id="create-sample-transaction-btn"
- onClick={async () => {
- trackAnalytics('performance_views.create_sample_transaction', {
- platform: project.platform,
- organization,
- });
- addLoadingMessage(loadingMessage, {
- duration: 15000,
- });
- const url = `/projects/${organization.slug}/${project.slug}/create-sample-transaction/`;
- try {
- const eventData = await api.requestPromise(url, {method: 'POST'});
- const traceSlug = eventData.contexts?.trace?.trace_id ?? '';
- browserHistory.push(
- generateLinkToEventInTraceView({
- eventId: eventData.eventID,
- location,
- projectSlug: project.slug,
- organization,
- timestamp: eventData.endTimestamp,
- traceSlug,
- demo: `${project.slug}:${eventData.eventID}`,
- })
- );
- clearIndicators();
- } catch (error) {
- Sentry.withScope(scope => {
- scope.setExtra('error', error);
- Sentry.captureException(new Error('Failed to create sample event'));
- });
- clearIndicators();
- addErrorMessage(errorMessage);
- return;
- }
- }}
- >
- {triggerText}
- </Button>
- );
- }
- type OnboardingProps = {
- organization: Organization;
- project: Project;
- };
- export function LegacyOnboarding({organization, project}: OnboardingProps) {
- const api = useApi();
- const {projects} = useProjects();
- const location = useLocation();
- const {projectsForOnboarding} = filterProjects(projects);
- const showOnboardingChecklist = organization.features.includes(
- 'performance-onboarding-checklist'
- );
- useEffect(() => {
- if (
- showOnboardingChecklist &&
- location.hash === '#performance-sidequest' &&
- projectsForOnboarding.some(p => p.id === project.id)
- ) {
- SidebarPanelStore.activatePanel(SidebarPanelKey.PERFORMANCE_ONBOARDING);
- }
- }, [location.hash, projectsForOnboarding, project.id, showOnboardingChecklist]);
- function handleAdvance(step: number, duration: number) {
- trackAnalytics('performance_views.tour.advance', {
- step,
- duration,
- organization,
- });
- }
- function handleClose(step: number, duration: number) {
- trackAnalytics('performance_views.tour.close', {
- step,
- duration,
- organization,
- });
- }
- const currentPlatform = project.platform;
- const hasPerformanceOnboarding = currentPlatform
- ? withPerformanceOnboarding.has(currentPlatform)
- : false;
- const noPerformanceSupport =
- currentPlatform && withoutPerformanceSupport.has(currentPlatform);
- let setupButton = (
- <LinkButton
- priority="primary"
- href="https://docs.sentry.io/performance-monitoring/getting-started/"
- external
- >
- {t('Start Setup')}
- </LinkButton>
- );
- if (hasPerformanceOnboarding && showOnboardingChecklist) {
- setupButton = (
- <Button
- priority="primary"
- onClick={event => {
- event.preventDefault();
- window.location.hash = 'performance-sidequest';
- SidebarPanelStore.activatePanel(SidebarPanelKey.PERFORMANCE_ONBOARDING);
- }}
- >
- {t('Set Up Tracing')}
- </Button>
- );
- }
- return (
- <Fragment>
- {noPerformanceSupport && (
- <UnsupportedAlert projectSlug={project.slug} featureName="Performance" />
- )}
- <LegacyOnboardingPanel image={<PerfImage src={emptyStateImg} />}>
- <h3>{t('Pinpoint problems')}</h3>
- <p>
- {t(
- 'Something seem slow? Track down transactions to connect the dots between 10-second page loads and poor-performing API calls or slow database queries.'
- )}
- </p>
- <ButtonList gap={1}>
- {setupButton}
- <SampleButton
- triggerText={t('View Sample Transaction')}
- loadingMessage={t('Processing sample transaction...')}
- errorMessage={t('Failed to create sample transaction')}
- organization={organization}
- project={project}
- api={api}
- />
- </ButtonList>
- <FeatureTourModal
- steps={PERFORMANCE_TOUR_STEPS}
- onAdvance={handleAdvance}
- onCloseModal={handleClose}
- doneUrl={performanceSetupUrl}
- doneText={t('Start Setup')}
- >
- {({showModal}) => (
- <Button
- priority="link"
- onClick={() => {
- trackAnalytics('performance_views.tour.start', {organization});
- showModal();
- }}
- >
- {t('Take a Tour')}
- </Button>
- )}
- </FeatureTourModal>
- </LegacyOnboardingPanel>
- </Fragment>
- );
- }
- const PerfImage = styled('img')`
- @media (min-width: ${p => p.theme.breakpoints.small}) {
- max-width: unset;
- user-select: none;
- position: absolute;
- top: 75px;
- bottom: 0;
- width: 450px;
- margin-top: auto;
- margin-bottom: auto;
- }
- @media (min-width: ${p => p.theme.breakpoints.medium}) {
- width: 480px;
- }
- @media (min-width: ${p => p.theme.breakpoints.large}) {
- width: 600px;
- }
- `;
- const ButtonList = styled(ButtonBar)`
- grid-template-columns: repeat(auto-fit, minmax(130px, max-content));
- margin-bottom: 16px;
- `;
- function WaitingIndicator({
- api,
- organization,
- project,
- }: {
- api: Client;
- organization: Organization;
- project: Project;
- }) {
- const [received, setReceived] = useState<boolean>(false);
- return (
- <EventWaiter
- api={api}
- organization={organization}
- project={project}
- eventType="transaction"
- onIssueReceived={() => {
- setReceived(true);
- }}
- >
- {() => (received ? <EventReceivedIndicator /> : <EventWaitingIndicator />)}
- </EventWaiter>
- );
- }
- type ConfigurationStepProps = {
- api: Client;
- configuration: Configuration;
- organization: Organization;
- project: Project;
- showWaitingIndicator: boolean;
- stepKey: string;
- title: React.ReactNode;
- };
- function ConfigurationStep({
- stepKey,
- title,
- api,
- organization,
- project,
- configuration,
- showWaitingIndicator,
- }: ConfigurationStepProps) {
- return (
- <GuidedSteps.Step stepKey={stepKey} title={title}>
- <div>
- <div>
- <DescriptionWrapper>{configuration.description}</DescriptionWrapper>
- <CodeSnippetWrapper>
- {configuration.code ? (
- Array.isArray(configuration.code) ? (
- <TabbedCodeSnippet tabs={configuration.code} />
- ) : (
- <OnboardingCodeSnippet language={configuration.language}>
- {configuration.code}
- </OnboardingCodeSnippet>
- )
- ) : null}
- </CodeSnippetWrapper>
- <CodeSnippetWrapper>
- {configuration.configurations && configuration.configurations.length > 0 ? (
- Array.isArray(configuration.configurations[0]!.code) ? (
- <TabbedCodeSnippet tabs={configuration.configurations[0]!.code} />
- ) : null
- ) : null}
- </CodeSnippetWrapper>
- <DescriptionWrapper>{configuration.additionalInfo}</DescriptionWrapper>
- {showWaitingIndicator ? (
- <WaitingIndicator api={api} organization={organization} project={project} />
- ) : null}
- </div>
- <GuidedSteps.ButtonWrapper>
- <GuidedSteps.BackButton size="md" />
- <GuidedSteps.NextButton size="md" />
- </GuidedSteps.ButtonWrapper>
- </div>
- </GuidedSteps.Step>
- );
- }
- function OnboardingPanel({
- project,
- children,
- }: {
- children: React.ReactNode;
- project: Project;
- }) {
- return (
- <Panel>
- <PanelBody>
- <AuthTokenGeneratorProvider projectSlug={project?.slug}>
- <div>
- <HeaderWrapper>
- <HeaderText>
- <Title>{t('Query for Traces, Get Answers')}</Title>
- <SubTitle>
- {t(
- 'You can query and aggregate spans to create metrics that help you debug busted API calls, slow image loads, or any other metrics you’d like to track.'
- )}
- </SubTitle>
- <BulletList>
- <li>
- {t(
- 'Find traces tied to a user complaint and pinpoint exactly what broke'
- )}
- </li>
- <li>
- {t(
- 'Debug persistent issues by investigating API payloads, cache sizes, user tokens, and more'
- )}
- </li>
- <li>
- {t(
- 'Track any span attribute as a metric to catch slowdowns before they escalate'
- )}
- </li>
- </BulletList>
- </HeaderText>
- <Image src={emptyTraceImg} />
- </HeaderWrapper>
- <Divider />
- <Body>
- <Setup>{children}</Setup>
- <Preview>
- <BodyTitle>{t('Preview a Sentry Trace')}</BodyTitle>
- <Arcade
- src="https://demo.arcade.software/BPVB65UiYCxixEw8bnmj?embed"
- loading="lazy"
- allowFullScreen
- />
- </Preview>
- </Body>
- </div>
- </AuthTokenGeneratorProvider>
- </PanelBody>
- </Panel>
- );
- }
- export function Onboarding({organization, project}: OnboardingProps) {
- const api = useApi();
- const {isSelfHosted, urlPrefix} = useLegacyStore(ConfigStore);
- const [received, setReceived] = useState<boolean>(false);
- const showNewUi = organization.features.includes('tracing-onboarding-new-ui');
- const currentPlatform = project.platform
- ? platforms.find(p => p.id === project.platform)
- : undefined;
- const {isLoading, docs, dsn, projectKeyId} = useLoadGettingStarted({
- platform: currentPlatform || otherPlatform,
- orgSlug: organization.slug,
- projSlug: project.slug,
- productType: 'performance',
- });
- const doesNotSupportPerformance = project.platform
- ? withoutPerformanceSupport.has(project.platform)
- : false;
- useEffect(() => {
- if (isLoading || !currentPlatform || !dsn || !projectKeyId) {
- return;
- }
- traceAnalytics.trackTracingOnboarding(
- organization,
- currentPlatform.id,
- !doesNotSupportPerformance,
- withPerformanceOnboarding.has(currentPlatform.id)
- );
- }, [
- currentPlatform,
- isLoading,
- dsn,
- projectKeyId,
- organization,
- doesNotSupportPerformance,
- ]);
- if (!showNewUi) {
- return <LegacyOnboarding organization={organization} project={project} />;
- }
- const performanceDocs = docs?.performanceOnboarding;
- if (isLoading) {
- return <LoadingIndicator />;
- }
- if (doesNotSupportPerformance) {
- return (
- <OnboardingPanel project={project}>
- <div>
- {tct(
- 'Fiddlesticks. Performance isn’t available for your [platform] project yet but we’re definitely still working on it. Stay tuned.',
- {platform: currentPlatform?.name || project.slug}
- )}
- </div>
- <br />
- <div>
- <LinkButton
- size="sm"
- href="https://docs.sentry.io/platforms/"
- external
- onClick={() => {
- traceAnalytics.trackPlatformDocsViewed(
- organization,
- currentPlatform?.id ?? project.platform ?? 'unknown'
- );
- }}
- >
- {t('Go to Documentation')}
- </LinkButton>
- </div>
- </OnboardingPanel>
- );
- }
- if (!currentPlatform || !performanceDocs || !dsn || !projectKeyId) {
- return (
- <OnboardingPanel project={project}>
- <div>
- {tct(
- 'Fiddlesticks. The tracing onboarding checklist isn’t available for your [project] project yet, but for now, go to Sentry docs for installation details.',
- {project: project.slug}
- )}
- </div>
- <br />
- <div>
- <LinkButton
- size="sm"
- href="https://docs.sentry.io/product/performance/getting-started/"
- external
- onClick={() => {
- traceAnalytics.trackPerformanceSetupDocsViewed(
- organization,
- currentPlatform?.id ?? project.platform ?? 'unknown'
- );
- }}
- >
- {t('Go to Documentation')}
- </LinkButton>
- </div>
- </OnboardingPanel>
- );
- }
- const docParams: DocsParams<any> = {
- api,
- projectKeyId,
- dsn,
- organization,
- platformKey: project.platform || 'other',
- projectId: project.id,
- projectSlug: project.slug,
- isFeedbackSelected: false,
- isPerformanceSelected: true,
- isProfilingSelected: false,
- isReplaySelected: false,
- sourcePackageRegistries: {
- isLoading: false,
- data: undefined,
- },
- platformOptions: [ProductSolution.PERFORMANCE_MONITORING],
- newOrg: false,
- feedbackOptions: {},
- urlPrefix,
- isSelfHosted,
- };
- const installStep = performanceDocs.install(docParams)[0]!;
- const configureStep = performanceDocs.configure(docParams)[0]!;
- const [sentryConfiguration, addingDistributedTracing] = configureStep.configurations!;
- const verifyStep = performanceDocs.verify(docParams)[0]!;
- const hasVerifyStep = !!(verifyStep.configurations || verifyStep.description);
- const eventWaitingIndicator = (
- <EventWaiter
- api={api}
- organization={organization}
- project={project}
- eventType="transaction"
- onIssueReceived={() => {
- setReceived(true);
- }}
- >
- {() => (received ? <EventReceivedIndicator /> : <EventWaitingIndicator />)}
- </EventWaiter>
- );
- return (
- <OnboardingPanel project={project}>
- <BodyTitle>{t('Set up the Sentry SDK')}</BodyTitle>
- <GuidedSteps>
- <GuidedSteps.Step stepKey="install-sentry" title={t('Install Sentry')}>
- <div>
- <div>
- <DescriptionWrapper>{installStep.description}</DescriptionWrapper>
- {installStep.configurations?.map((configuration, index) => (
- <div key={index}>
- <DescriptionWrapper>{configuration.description}</DescriptionWrapper>
- <CodeSnippetWrapper>
- {configuration.code ? (
- Array.isArray(configuration.code) ? (
- <TabbedCodeSnippet tabs={configuration.code} />
- ) : (
- <OnboardingCodeSnippet language={configuration.language}>
- {configuration.code}
- </OnboardingCodeSnippet>
- )
- ) : null}
- </CodeSnippetWrapper>
- </div>
- ))}
- {!configureStep.configurations && !verifyStep.configurations
- ? eventWaitingIndicator
- : null}
- </div>
- <GuidedSteps.ButtonWrapper>
- <GuidedSteps.BackButton size="md" />
- <GuidedSteps.NextButton size="md" />
- </GuidedSteps.ButtonWrapper>
- </div>
- </GuidedSteps.Step>
- {sentryConfiguration ? (
- <ConfigurationStep
- stepKey={'configure-sentry'}
- title={t('Configure Sentry')}
- configuration={sentryConfiguration}
- api={api}
- organization={organization}
- project={project}
- showWaitingIndicator={!hasVerifyStep}
- />
- ) : null}
- {addingDistributedTracing ? (
- <ConfigurationStep
- stepKey={'add-distributed-tracing'}
- title={tct('Add Distributed Tracing [optional:(Optional)]', {
- optional: <OptionalText />,
- })}
- configuration={addingDistributedTracing}
- api={api}
- organization={organization}
- project={project}
- showWaitingIndicator={!hasVerifyStep}
- />
- ) : null}
- {verifyStep.configurations || verifyStep.description ? (
- <GuidedSteps.Step stepKey="verify-sentry" title={t('Verify')}>
- <div>
- <DescriptionWrapper>{verifyStep.description}</DescriptionWrapper>
- {verifyStep.configurations?.map((configuration, index) => (
- <div key={index}>
- <DescriptionWrapper>{configuration.description}</DescriptionWrapper>
- <CodeSnippetWrapper>
- {configuration.code ? (
- Array.isArray(configuration.code) ? (
- <TabbedCodeSnippet tabs={configuration.code} />
- ) : (
- <OnboardingCodeSnippet language={configuration.language}>
- {configuration.code}
- </OnboardingCodeSnippet>
- )
- ) : null}
- </CodeSnippetWrapper>
- </div>
- ))}
- {eventWaitingIndicator}
- </div>
- <GuidedSteps.ButtonWrapper>
- <GuidedSteps.BackButton size="md" />
- <SampleButton
- triggerText={t('Take me to an example')}
- loadingMessage={t('Processing sample trace...')}
- errorMessage={t('Failed to create sample trace')}
- organization={organization}
- project={project}
- api={api}
- />
- </GuidedSteps.ButtonWrapper>
- </GuidedSteps.Step>
- ) : (
- <Fragment />
- )}
- </GuidedSteps>
- </OnboardingPanel>
- );
- }
- const EventWaitingIndicator = styled((p: React.HTMLAttributes<HTMLDivElement>) => (
- <div {...p}>
- {t("Waiting for this project's first trace")}
- <PulsingIndicator />
- </div>
- ))`
- display: flex;
- align-items: center;
- position: relative;
- z-index: 10;
- flex-grow: 1;
- font-size: ${p => p.theme.fontSizeMedium};
- color: ${p => p.theme.pink400};
- `;
- const PulsingIndicator = styled('div')`
- ${pulsingIndicatorStyles};
- margin-left: ${space(1)};
- `;
- const OptionalText = styled('span')`
- color: ${p => p.theme.purple300};
- font-weight: ${p => p.theme.fontWeightNormal};
- `;
- const EventReceivedIndicator = styled((p: React.HTMLAttributes<HTMLDivElement>) => (
- <div {...p}>
- {'🎉 '}
- {t("We've received this project's first trace!")}
- </div>
- ))`
- display: flex;
- align-items: center;
- flex-grow: 1;
- font-size: ${p => p.theme.fontSizeMedium};
- color: ${p => p.theme.successText};
- `;
- const SubTitle = styled('div')`
- margin-bottom: ${space(1)};
- `;
- const Title = styled('div')`
- font-size: 26px;
- font-weight: ${p => p.theme.fontWeightBold};
- `;
- const BulletList = styled('ul')`
- list-style-type: disc;
- padding-left: 20px;
- margin-bottom: ${space(2)};
- li {
- margin-bottom: ${space(1)};
- }
- `;
- const HeaderWrapper = styled('div')`
- display: flex;
- justify-content: space-between;
- gap: ${space(3)};
- border-radius: ${p => p.theme.borderRadius};
- padding: ${space(4)};
- `;
- const HeaderText = styled('div')`
- flex: 0.65;
- @media (max-width: ${p => p.theme.breakpoints.small}) {
- flex: 1;
- }
- `;
- const BodyTitle = styled('div')`
- font-size: ${p => p.theme.fontSizeExtraLarge};
- font-weight: ${p => p.theme.fontWeightBold};
- margin-bottom: ${space(1)};
- `;
- const Setup = styled('div')`
- padding: ${space(4)};
- &:after {
- content: '';
- position: absolute;
- right: 50%;
- top: 2.5%;
- height: 95%;
- border-right: 1px ${p => p.theme.border} solid;
- }
- `;
- const Preview = styled('div')`
- padding: ${space(4)};
- `;
- const Body = styled('div')`
- display: grid;
- position: relative;
- grid-auto-columns: minmax(0, 1fr);
- grid-auto-flow: column;
- h4 {
- margin-bottom: 0;
- }
- `;
- const Image = styled('img')`
- display: block;
- pointer-events: none;
- height: 120px;
- overflow: hidden;
- @media (max-width: ${p => p.theme.breakpoints.small}) {
- display: none;
- }
- `;
- const Divider = styled('hr')`
- height: 1px;
- width: 95%;
- background: ${p => p.theme.border};
- border: none;
- margin-top: 0;
- margin-bottom: 0;
- `;
- const Arcade = styled('iframe')`
- width: 750px;
- max-width: 100%;
- margin-top: ${space(3)};
- height: 522px;
- border: 0;
- `;
- const CodeSnippetWrapper = styled('div')`
- margin-bottom: ${space(2)};
- `;
- const DescriptionWrapper = styled('div')`
- margin-bottom: ${space(1)};
- code {
- color: ${p => p.theme.pink400};
- }
- `;
|