From 32d17eae9dad33bfa5015926a5e0b2e6e2e5e10a Mon Sep 17 00:00:00 2001 From: JonasBa Date: Sat, 12 Jul 2025 11:25:25 -0400 Subject: [PATCH 1/3] stories: implement programmatic pagination detection --- static/app/components/core/button/index.mdx | 6 -- static/app/stories/view/storyFooter.tsx | 73 ++++++++++++++++++--- static/app/stories/view/storySidebar.tsx | 7 +- 3 files changed, 71 insertions(+), 15 deletions(-) diff --git a/static/app/components/core/button/index.mdx b/static/app/components/core/button/index.mdx index 14eb2ae6f4ad65..43ed9ce0e03ae3 100644 --- a/static/app/components/core/button/index.mdx +++ b/static/app/components/core/button/index.mdx @@ -11,12 +11,6 @@ resources: WCAG 2.4.7: https://www.w3.org/TR/WCAG22/#focus-visible WCAG 2.5.8: https://www.w3.org/TR/WCAG22/#target-size-minimum WAI-ARIA Button Practices: https://www.w3.org/WAI/ARIA/apg/patterns/button/ -next: - link: '/stories/?name=app%2Fcomponents%2Fcore%2Fbutton%2FbuttonBar.stories.tsx' - label: ButtonBar -prev: - link: '?name=app%2Fcomponents%2Fcore%2Fbadge%2Ftag.stories.tsx' - label: Tag --- import {Button} from 'sentry/components/core/button'; diff --git a/static/app/stories/view/storyFooter.tsx b/static/app/stories/view/storyFooter.tsx index c1eed2787d7c8c..8854f886233bbf 100644 --- a/static/app/stories/view/storyFooter.tsx +++ b/static/app/stories/view/storyFooter.tsx @@ -1,33 +1,90 @@ import styled from '@emotion/styled'; +import qs from 'query-string'; import {LinkButton} from 'sentry/components/core/button/linkButton'; import {Flex} from 'sentry/components/core/layout'; import {IconArrow} from 'sentry/icons'; +import {useStoryBookFilesByCategory} from 'sentry/stories/view/storySidebar'; +import type {StoryTreeNode} from 'sentry/stories/view/storyTree'; +import type {StoryDescriptor} from 'sentry/stories/view/useStoriesLoader'; import {useStory} from 'sentry/stories/view/useStory'; import {space} from 'sentry/styles/space'; +import {useLocation} from 'sentry/utils/useLocation'; export function StoryFooter() { + const location = useLocation(); + const {story} = useStory(); - if (!story.filename.endsWith('.mdx')) return null; - const {prev, next} = story.exports.frontmatter ?? {}; + const categories = useStoryBookFilesByCategory(); + const pagination = findPreviousAndNextStory(story, categories); + + const prevLocationDescriptor = qs.stringify({ + ...location.query, + name: pagination?.prev?.story.filesystemPath, + }); + const nextLocationDescriptor = qs.stringify({ + ...location.query, + name: pagination?.next?.story.filesystemPath, + }); + return ( - {prev && ( - }> + {pagination?.prev && ( + } + > Previous - {prev.label} + {pagination.prev.story.label} )} - {next && ( - }> + {pagination?.next && ( + } + > Next - {next.label} + {pagination.next.story.label} )} ); } +function findPreviousAndNextStory( + story: StoryDescriptor, + categories: ReturnType +): { + next: {category: string; story: StoryTreeNode} | undefined; + prev: {category: string; story: StoryTreeNode} | undefined; +} | null { + let prev: {category: string; story: StoryTreeNode} | undefined; + let next: {category: string; story: StoryTreeNode} | undefined; + + const stories: Array<{category: string; story: StoryTreeNode}> = []; + + // Flatten into a single list so we don't have to deal with overflowing index + // categories and can simplify the search procedure. + for (const key in categories) { + const category = categories[key as keyof typeof categories]; + for (const s of category) { + stories.push({category: key, story: s}); + } + } + + for (let i = 0; i < stories.length; i++) { + const s = stories[i]!; + if (s.story.filesystemPath === story.filename) { + prev = stories[i - 1]; + next = stories[i + 1]; + return {prev, next}; + } + } + + return null; +} + const Card = styled(LinkButton)` display: flex; flex-direction: column; diff --git a/static/app/stories/view/storySidebar.tsx b/static/app/stories/view/storySidebar.tsx index b1349a02a64399..a2d3f4d236ed94 100644 --- a/static/app/stories/view/storySidebar.tsx +++ b/static/app/stories/view/storySidebar.tsx @@ -3,6 +3,7 @@ import styled from '@emotion/styled'; import {space} from 'sentry/styles/space'; +import type {StoryTreeNode} from './storyTree'; import {StoryTree, useStoryTree} from './storyTree'; import {useStoryBookFiles} from './useStoriesLoader'; @@ -29,9 +30,13 @@ export function StorySidebar() { ); } -export function useStoryBookFilesByCategory() { +export function useStoryBookFilesByCategory(): Record< + 'foundations' | 'core' | 'shared', + StoryTreeNode[] +> { const files = useStoryBookFiles(); const filesByOwner = useMemo(() => { + // The order of keys here is important and used by the pagination in storyFooter const map: Record<'foundations' | 'core' | 'shared', string[]> = { foundations: [], core: [], From 687bfcca0d93038a295c01f6151b2701fc6d0e71 Mon Sep 17 00:00:00 2001 From: JonasBa Date: Sat, 12 Jul 2025 11:25:25 -0400 Subject: [PATCH 2/3] stories: implement programmatic pagination detection --- static/app/stories/view/useStoriesLoader.tsx | 4 ---- 1 file changed, 4 deletions(-) diff --git a/static/app/stories/view/useStoriesLoader.tsx b/static/app/stories/view/useStoriesLoader.tsx index d41e66a947f080..b779c11190a959 100644 --- a/static/app/stories/view/useStoriesLoader.tsx +++ b/static/app/stories/view/useStoriesLoader.tsx @@ -12,16 +12,12 @@ export interface StoryResources { js?: string; } -type FrontmatterPagination = boolean | {label: string; link: string}; - interface MDXStoryDescriptor { exports: { default: React.ComponentType | any; frontmatter?: { description: string; title: string; - next?: FrontmatterPagination; - prev?: FrontmatterPagination; resources?: StoryResources; source?: string; types?: string; From 5516d57efbbdfb300ad3d478a158f7659fb5203b Mon Sep 17 00:00:00 2001 From: JonasBa Date: Mon, 14 Jul 2025 09:07:03 -0400 Subject: [PATCH 3/3] stories: simplify loop --- static/app/stories/view/storyFooter.tsx | 30 +++++++++---------------- 1 file changed, 10 insertions(+), 20 deletions(-) diff --git a/static/app/stories/view/storyFooter.tsx b/static/app/stories/view/storyFooter.tsx index 8854f886233bbf..796c7836af2ba6 100644 --- a/static/app/stories/view/storyFooter.tsx +++ b/static/app/stories/view/storyFooter.tsx @@ -59,30 +59,20 @@ function findPreviousAndNextStory( next: {category: string; story: StoryTreeNode} | undefined; prev: {category: string; story: StoryTreeNode} | undefined; } | null { - let prev: {category: string; story: StoryTreeNode} | undefined; - let next: {category: string; story: StoryTreeNode} | undefined; - - const stories: Array<{category: string; story: StoryTreeNode}> = []; + const stories = Object.entries(categories).flatMap(([key, category]) => + category.map(s => ({category: key, story: s})) + ); - // Flatten into a single list so we don't have to deal with overflowing index - // categories and can simplify the search procedure. - for (const key in categories) { - const category = categories[key as keyof typeof categories]; - for (const s of category) { - stories.push({category: key, story: s}); - } - } + const currentIndex = stories.findIndex(s => s.story.filesystemPath === story.filename); - for (let i = 0; i < stories.length; i++) { - const s = stories[i]!; - if (s.story.filesystemPath === story.filename) { - prev = stories[i - 1]; - next = stories[i + 1]; - return {prev, next}; - } + if (currentIndex === -1) { + return null; } - return null; + return { + prev: stories[currentIndex - 1] ?? undefined, + next: stories[currentIndex + 1] ?? undefined, + }; } const Card = styled(LinkButton)`