Skip to content

Commit d1662dc

Browse files
authored
ref(stories): add regions to search (#95247)
Adds regions so that we can tell where each of the stories belongs to <img width="1002" height="1120" alt="CleanShot 2025-07-10 at 12 51 37@2x" src="https://github.com/user-attachments/assets/94f396e3-a31a-4600-99eb-592cdddfbcbd" />
1 parent 9f897a6 commit d1662dc

File tree

2 files changed

+119
-38
lines changed

2 files changed

+119
-38
lines changed

static/app/stories/view/storySearch.tsx

Lines changed: 88 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type {Key} from 'react';
22
import {useCallback, useMemo, useRef, useState} from 'react';
33
import styled from '@emotion/styled';
44
import {type AriaComboBoxProps} from '@react-aria/combobox';
5-
import {Item} from '@react-stately/collections';
5+
import {Item, Section} from '@react-stately/collections';
66
import {useComboBoxState} from '@react-stately/combobox';
77
import type {CollectionChildren} from '@react-types/shared';
88

@@ -14,37 +14,93 @@ import {useSearchTokenCombobox} from 'sentry/components/searchQueryBuilder/token
1414
import {IconSearch} from 'sentry/icons';
1515
import {t} from 'sentry/locale';
1616
import type {StoryTreeNode} from 'sentry/stories/view/storyTree';
17-
import {useStoryTree} from 'sentry/stories/view/storyTree';
1817
import {space} from 'sentry/styles/space';
1918
import {fzf} from 'sentry/utils/profiling/fzf/fzf';
2019
import {useHotkeys} from 'sentry/utils/useHotkeys';
2120
import {useNavigate} from 'sentry/utils/useNavigate';
2221

23-
import {useStoryBookFiles} from './useStoriesLoader';
22+
import {useStoryBookFilesByCategory} from './storySidebar';
23+
24+
interface StorySection {
25+
key: string;
26+
label: string;
27+
options: StoryTreeNode[];
28+
}
29+
30+
function isStorySection(item: StoryTreeNode | StorySection): item is StorySection {
31+
return 'options' in item;
32+
}
2433

2534
export function StorySearch() {
2635
const inputRef = useRef<HTMLInputElement | null>(null);
27-
const files = useStoryBookFiles();
28-
const tree = useStoryTree(files, {query: '', representation: 'category', type: 'flat'});
36+
const {foundations, core, shared} = useStoryBookFilesByCategory();
37+
2938
const storiesSearchHotkeys = useMemo(() => {
3039
return [{match: '/', callback: () => inputRef.current?.focus()}];
3140
}, []);
41+
3242
useHotkeys(storiesSearchHotkeys);
3343

44+
const sectionedItems = useMemo(() => {
45+
const sections: StorySection[] = [];
46+
47+
if (foundations.length > 0) {
48+
sections.push({
49+
key: 'foundations',
50+
label: 'Foundations',
51+
options: foundations,
52+
});
53+
}
54+
55+
if (core.length > 0) {
56+
sections.push({
57+
key: 'core',
58+
label: 'Core',
59+
options: core,
60+
});
61+
}
62+
63+
if (shared.length > 0) {
64+
sections.push({
65+
key: 'shared',
66+
label: 'Shared',
67+
options: shared,
68+
});
69+
}
70+
71+
return sections;
72+
}, [foundations, core, shared]);
73+
3474
return (
3575
<SearchComboBox
3676
label={t('Search stories')}
3777
menuTrigger="focus"
3878
inputRef={inputRef}
39-
defaultItems={tree}
79+
defaultItems={sectionedItems}
4080
>
41-
{item => (
42-
<Item
43-
key={item.filesystemPath}
44-
textValue={item.label}
45-
{...({label: item.label, hideCheck: true} as any)}
46-
/>
47-
)}
81+
{item => {
82+
if (isStorySection(item)) {
83+
return (
84+
<Section key={item.key} title={<SectionTitle>{item.label}</SectionTitle>}>
85+
{item.options.map(storyItem => (
86+
<Item
87+
key={storyItem.filesystemPath}
88+
textValue={storyItem.label}
89+
{...({label: storyItem.label, hideCheck: true} as any)}
90+
/>
91+
))}
92+
</Section>
93+
);
94+
}
95+
96+
return (
97+
<Item
98+
key={item.filesystemPath}
99+
textValue={item.label}
100+
{...({label: item.label, hideCheck: true} as any)}
101+
/>
102+
);
103+
}}
48104
</SearchComboBox>
49105
);
50106
}
@@ -67,10 +123,12 @@ function SearchInput(
67123
);
68124
}
69125

126+
type SearchComboBoxItem<T extends StoryTreeNode> = T | StorySection;
127+
70128
interface SearchComboBoxProps<T extends StoryTreeNode>
71-
extends Omit<AriaComboBoxProps<T>, 'children'> {
72-
children: CollectionChildren<T>;
73-
defaultItems: T[];
129+
extends Omit<AriaComboBoxProps<SearchComboBoxItem<T>>, 'children'> {
130+
children: CollectionChildren<SearchComboBoxItem<T>>;
131+
defaultItems: Array<SearchComboBoxItem<T>>;
74132
inputRef: React.RefObject<HTMLInputElement | null>;
75133
description?: string | null;
76134
label?: string;
@@ -106,7 +164,9 @@ function SearchComboBox<T extends StoryTreeNode>(props: SearchComboBoxProps<T>)
106164
onSelectionChange: handleSelectionChange,
107165
});
108166

109-
const {inputProps, listBoxProps, labelProps} = useSearchTokenCombobox<T>(
167+
const {inputProps, listBoxProps, labelProps} = useSearchTokenCombobox<
168+
SearchComboBoxItem<T>
169+
>(
110170
{
111171
...props,
112172
inputRef,
@@ -160,4 +220,15 @@ const StyledOverlay = styled(Overlay)`
160220
width: 320px;
161221
max-height: calc(100dvh - 128px);
162222
overflow-y: auto;
223+
224+
/* Make section headers darker in this component */
225+
p[id][aria-hidden='true'] {
226+
color: ${p => p.theme.textColor};
227+
}
228+
`;
229+
230+
const SectionTitle = styled('span')`
231+
color: ${p => p.theme.textColor};
232+
font-weight: 600;
233+
text-transform: uppercase;
163234
`;

static/app/stories/view/storySidebar.tsx

Lines changed: 31 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,29 @@ import {StoryTree, useStoryTree} from './storyTree';
77
import {useStoryBookFiles} from './useStoriesLoader';
88

99
export function StorySidebar() {
10+
const {foundations, core, shared} = useStoryBookFilesByCategory();
11+
12+
return (
13+
<SidebarContainer>
14+
<ul>
15+
<li>
16+
<h3>Foundations</h3>
17+
<StoryTree nodes={foundations} />
18+
</li>
19+
<li>
20+
<h3>Components</h3>
21+
<StoryTree nodes={core} />
22+
</li>
23+
<li>
24+
<h3>Shared</h3>
25+
<StoryTree nodes={shared} />
26+
</li>
27+
</ul>
28+
</SidebarContainer>
29+
);
30+
}
31+
32+
export function useStoryBookFilesByCategory() {
1033
const files = useStoryBookFiles();
1134
const filesByOwner = useMemo(() => {
1235
const map: Record<'foundations' | 'core' | 'shared', string[]> = {
@@ -26,38 +49,25 @@ export function StorySidebar() {
2649
return map;
2750
}, [files]);
2851

29-
const foundationsTree = useStoryTree(filesByOwner.foundations, {
52+
const foundations = useStoryTree(filesByOwner.foundations, {
3053
query: '',
3154
representation: 'category',
3255
});
33-
const coreTree = useStoryTree(filesByOwner.core, {
56+
const core = useStoryTree(filesByOwner.core, {
3457
query: '',
3558
representation: 'category',
3659
type: 'flat',
3760
});
38-
const sharedTree = useStoryTree(filesByOwner.shared, {
61+
const shared = useStoryTree(filesByOwner.shared, {
3962
query: '',
4063
representation: 'category',
4164
});
4265

43-
return (
44-
<SidebarContainer>
45-
<ul>
46-
<li>
47-
<h3>Foundations</h3>
48-
<StoryTree nodes={foundationsTree} />
49-
</li>
50-
<li>
51-
<h3>Components</h3>
52-
<StoryTree nodes={coreTree} />
53-
</li>
54-
<li>
55-
<h3>Shared</h3>
56-
<StoryTree nodes={sharedTree} />
57-
</li>
58-
</ul>
59-
</SidebarContainer>
60-
);
66+
return {
67+
foundations,
68+
core,
69+
shared,
70+
};
6171
}
6272

6373
function isCoreFile(file: string) {

0 commit comments

Comments
 (0)