Skip to content
136 changes: 89 additions & 47 deletions static/app/stories/view/index.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import {Fragment, type PropsWithChildren} from 'react';
import {css, Global, useTheme} from '@emotion/react';
import styled from '@emotion/styled';

import {Alert} from 'sentry/components/core/alert';
Expand All @@ -12,6 +14,7 @@ import RouteAnalyticsContextProvider from 'sentry/views/routeAnalyticsContextPro
import {StoryLanding} from './landing';
import {StoryExports} from './storyExports';
import {StoryHeader} from './storyHeader';
import {useStoryDarkModeTheme} from './useStoriesDarkMode';
import {useStoriesLoader} from './useStoriesLoader';

export default function Stories() {
Expand All @@ -25,18 +28,11 @@ function isLandingPage(location: ReturnType<typeof useLocation>) {

function StoriesLanding() {
return (
<RouteAnalyticsContextProvider>
<OrganizationContainer>
<Layout style={{gridTemplateColumns: 'auto'}}>
<HeaderContainer>
<StoryHeader />
</HeaderContainer>
<StoryMainContainer style={{gridColumn: '1 / -1'}}>
<StoryLanding />
</StoryMainContainer>
</Layout>
</OrganizationContainer>
</RouteAnalyticsContextProvider>
<StoriesLayout>
<StoryMainContainer>
<StoryLanding />
</StoryMainContainer>
</StoriesLayout>
);
}

Expand All @@ -47,44 +43,90 @@ function StoryDetail() {
const story = useStoriesLoader({files});

return (
<RouteAnalyticsContextProvider>
<OrganizationContainer>
<Layout>
<HeaderContainer>
<StoryHeader />
</HeaderContainer>

<StorySidebar />

{story.isLoading ? (
<VerticalScroll>
<LoadingIndicator />
</VerticalScroll>
) : story.isError ? (
<VerticalScroll>
<Alert.Container>
<Alert type="error" showIcon>
<strong>{story.error.name}:</strong> {story.error.message}
</Alert>
</Alert.Container>
</VerticalScroll>
) : story.isSuccess ? (
<StoryMainContainer>
{story.data.map(s => {
return <StoryExports key={s.filename} story={s} />;
})}
</StoryMainContainer>
) : (
<VerticalScroll>
<strong>The file you selected does not export a story.</strong>
</VerticalScroll>
)}
</Layout>
</OrganizationContainer>
</RouteAnalyticsContextProvider>
<StoriesLayout>
{story.isLoading ? (
<VerticalScroll>
<LoadingIndicator />
</VerticalScroll>
) : story.isError ? (
<VerticalScroll>
<Alert.Container>
<Alert type="error" showIcon>
<strong>{story.error.name}:</strong> {story.error.message}
</Alert>
</Alert.Container>
</VerticalScroll>
) : story.isSuccess ? (
<StoryMainContainer>
{story.data.map(s => {
return <StoryExports key={s.filename} story={s} />;
})}
</StoryMainContainer>
) : (
<VerticalScroll>
<strong>The file you selected does not export a story.</strong>
</VerticalScroll>
)}
</StoriesLayout>
);
}

function StoriesLayout(props: PropsWithChildren) {
return (
<Fragment>
<GlobalStoryStyles />
<RouteAnalyticsContextProvider>
<OrganizationContainer>
<Layout>
<HeaderContainer>
<StoryHeader />
</HeaderContainer>

<StorySidebar />

{props.children}
</Layout>
</OrganizationContainer>
</RouteAnalyticsContextProvider>
</Fragment>
);
}

function GlobalStoryStyles() {
const theme = useTheme();
const darkTheme = useStoryDarkModeTheme();
const location = useLocation();
const isIndex = isLandingPage(location);
const styles = css`
/* match body background with header story styles */
body {
background-color: ${isIndex
? darkTheme.tokens.background.secondary
: theme.tokens.background.secondary};
}
/* fixed position color block to match overscroll color to story background */
body::after {
content: '';
display: block;
position: fixed;
inset: 0;
top: unset;
background-color: ${theme.tokens.background.primary};
height: 50vh;
z-index: -1;
pointer-events: none;
}
/* adjust position of global .messages-container element */
.messages-container {
margin-top: 52px;
margin-left: 256px;
z-index: ${theme.zIndex.header};
background: ${theme.tokens.background.primary};
}
`;
return <Global key="stories" styles={styles} />;
}

const Layout = styled('div')`
background: ${p => p.theme.tokens.background.primary};
--stories-grid-space: 0;
Expand Down
65 changes: 25 additions & 40 deletions static/app/stories/view/landing/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type {PropsWithChildren} from 'react';
import {Fragment, useMemo} from 'react';
import {ThemeProvider, useTheme} from '@emotion/react';
import {Fragment} from 'react';
import {useTheme} from '@emotion/react';
import styled from '@emotion/styled';

import performanceWaitingForSpan from 'sentry-images/spot/performance-waiting-for-span.svg';
Expand All @@ -11,12 +11,8 @@ import {LinkButton} from 'sentry/components/core/button/linkButton';
import {Flex} from 'sentry/components/core/layout';
import {Link} from 'sentry/components/core/link';
import {IconOpen} from 'sentry/icons';
import {StoryDarkModeProvider} from 'sentry/stories/view/useStoriesDarkMode';
import {space} from 'sentry/styles/space';
import type {Theme} from 'sentry/utils/theme';
// we need the hero to always use values from the dark theme
// eslint-disable-next-line no-restricted-imports
import {darkTheme} from 'sentry/utils/theme';
import {DO_NOT_USE_darkChonkTheme} from 'sentry/utils/theme/theme.chonk';

import {Colors, Icons, Typography} from './figures';

Expand Down Expand Up @@ -49,7 +45,7 @@ const frontmatter = {
export function StoryLanding() {
return (
<Fragment>
<AlwaysDarkThemeProvider>
<StoryDarkModeProvider>
<Hero>
<Container>
<Flex direction="column" gap={space(3)}>
Expand All @@ -74,7 +70,7 @@ export function StoryLanding() {
/>
</Container>
</Hero>
</AlwaysDarkThemeProvider>
</StoryDarkModeProvider>

<Container>
<Flex as="section" direction="column" gap={space(4)} flex={1}>
Expand Down Expand Up @@ -132,17 +128,6 @@ function Border() {
);
}

function AlwaysDarkThemeProvider(props: PropsWithChildren) {
const theme = useTheme();

const localThemeValue = useMemo(
() => (theme.isChonk ? DO_NOT_USE_darkChonkTheme : darkTheme),
[theme]
);

return <ThemeProvider theme={localThemeValue as Theme}>{props.children}</ThemeProvider>;
}

const TitleEmphasis = styled('em')`
font-style: normal;
display: inline-block;
Expand All @@ -151,13 +136,13 @@ const TitleEmphasis = styled('em')`
`;

const Hero = styled('div')`
width: 100vw;
padding: 48px 16px;
padding: 48px 0;
gap: ${space(4)};
display: flex;
align-items: center;
background: ${p => p.theme.tokens.background.tertiary};
background: ${p => p.theme.tokens.background.secondary};
color: ${p => p.theme.tokens.content.primary};
border-bottom: 1px solid ${p => p.theme.tokens.border.primary};

h1 {
font-size: 36px;
Expand All @@ -174,15 +159,13 @@ const Hero = styled('div')`
min-width: 320px;
height: auto;
}

@media (min-width: ${p => p.theme.breakpoints.md}) {
padding: 48px 92px;
}
`;

const Container = styled('div')`
max-width: 1134px;
width: calc(100vw - 32px);
max-width: 1080px;
width: 100%;
flex-grow: 1;
flex-shrink: 1;
margin-inline: auto;
display: flex;
flex-direction: column;
Expand All @@ -198,13 +181,9 @@ const Container = styled('div')`
`;

const CardGrid = styled('div')`
display: grid;
grid-template-columns: minmax(0, 1fr);
display: flex;
flex-flow: row wrap;
gap: ${space(2)};

@media (min-width: ${p => p.theme.breakpoints.md}) {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
`;

interface CardProps {
Expand All @@ -224,12 +203,13 @@ const CardLink = styled(Link)`
color: ${p => p.theme.tokens.content.primary};
display: flex;
flex-direction: column;
width: 100%;
height: 256px;
flex-grow: 1;
width: calc(100% * 3 / 5);
aspect-ratio: 2/1;
padding: ${space(2)};
border: 1px solid ${p => p.theme.tokens.border.muted};
border-radius: ${p => p.theme.borderRadius};
transition: initial 80ms ease-out;
transition: all 80ms ease-out;
transition-property: background-color, color, border-color;

&:hover,
Expand All @@ -245,15 +225,20 @@ const CardLink = styled(Link)`
max-width: 509px;
max-height: 170px;
}

@media screen and (min-width: ${p => p.theme.breakpoints.md}) {
max-width: calc(50% - 32px);
}
`;

const CardTitle = styled('span')`
margin: 0;
margin-top: ${space(1)};
margin-top: auto;
margin-bottom: ${space(2)};
padding: ${space(1)} ${space(2)};
width: 100%;
height: 24px;
font-size: 24px;
padding: ${space(1)} ${space(2)};
font-weight: ${p => p.theme.fontWeight.bold};
color: currentColor;
`;
Expand Down
32 changes: 32 additions & 0 deletions static/app/stories/view/useStoriesDarkMode.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import type {PropsWithChildren} from 'react';
import {type Theme, ThemeProvider, useTheme} from '@emotion/react';

// these utils are for stories that have forced dark mode
// which is a very specific sanctioned use case
// eslint-disable-next-line no-restricted-imports
import {darkTheme} from 'sentry/utils/theme';
import {DO_NOT_USE_darkChonkTheme} from 'sentry/utils/theme/theme.chonk';

/**
* Access the raw values from the dark theme
*
* ⚠️ DO NOT USE OUTSIDE OF STORIES
*/
export const useStoryDarkModeTheme = (): Theme => {
const theme = useTheme();
if (theme.isChonk) {
return DO_NOT_USE_darkChonkTheme as any;
}
return darkTheme;
};

/**
* Forces all `children` to be rendered in dark mode,
* regardless of user preferences.
*
* ⚠️ DO NOT USE OUTSIDE OF STORIES
*/
export function StoryDarkModeProvider(props: PropsWithChildren) {
const theme = useStoryDarkModeTheme();
return <ThemeProvider theme={theme}>{props.children}</ThemeProvider>;
}
Loading