Skip to content

feat(stories): bring back the sidebar on landing page #95882

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Jul 21, 2025
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