@storybook/react-native-ui 8.0.0-alpha.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/LICENSE +21 -0
  2. package/dist/index.d.ts +492 -0
  3. package/dist/index.js +2860 -0
  4. package/package.json +85 -0
  5. package/src/Button.stories.tsx +134 -0
  6. package/src/Button.tsx +243 -0
  7. package/src/Explorer.stories.tsx +46 -0
  8. package/src/Explorer.tsx +54 -0
  9. package/src/IconButton.tsx +11 -0
  10. package/src/Layout.stories.tsx +38 -0
  11. package/src/Layout.tsx +103 -0
  12. package/src/LayoutProvider.tsx +90 -0
  13. package/src/MobileAddonsPanel.tsx +166 -0
  14. package/src/MobileMenuDrawer.tsx +75 -0
  15. package/src/Refs.tsx +142 -0
  16. package/src/Search.tsx +336 -0
  17. package/src/SearchResults.tsx +315 -0
  18. package/src/Sidebar.stories.tsx +262 -0
  19. package/src/Sidebar.tsx +200 -0
  20. package/src/Tree.stories.tsx +139 -0
  21. package/src/Tree.tsx +441 -0
  22. package/src/TreeNode.stories.tsx +122 -0
  23. package/src/TreeNode.tsx +146 -0
  24. package/src/constants.ts +4 -0
  25. package/src/icon/BottomBarToggleIcon.tsx +23 -0
  26. package/src/icon/CloseIcon.tsx +22 -0
  27. package/src/icon/CollapseAllIcon.tsx +17 -0
  28. package/src/icon/CollapseIcon.tsx +39 -0
  29. package/src/icon/ComponentIcon.tsx +14 -0
  30. package/src/icon/ExpandAllIcon.tsx +17 -0
  31. package/src/icon/FaceHappyIcon.tsx +18 -0
  32. package/src/icon/GroupIcon.tsx +14 -0
  33. package/src/icon/MenuIcon.tsx +18 -0
  34. package/src/icon/SearchIcon.tsx +17 -0
  35. package/src/icon/StoryIcon.tsx +14 -0
  36. package/src/index.tsx +9 -0
  37. package/src/mockdata.large.ts +25217 -0
  38. package/src/mockdata.ts +287 -0
  39. package/src/types.ts +78 -0
  40. package/src/useExpanded.ts +130 -0
  41. package/src/useLastViewed.ts +48 -0
  42. package/src/util/status.tsx +70 -0
  43. package/src/util/tree.ts +91 -0
@@ -0,0 +1,287 @@
1
+ import type { API_HashEntry } from '@storybook/types';
2
+
3
+ export type MockDataSet = Record<string, Record<string, Partial<API_HashEntry>>>;
4
+
5
+ export const mockDataset: MockDataSet = {
6
+ withRoot: {
7
+ 'group-1': {
8
+ type: 'group',
9
+ children: ['group-1--child-b1', 'group-1--child-b2'],
10
+ depth: 0,
11
+ id: 'group-1',
12
+ name: 'Group 1',
13
+ },
14
+ 'group-1--child-b1': {
15
+ type: 'story',
16
+ prepared: true,
17
+ id: 'group-1--child-b1',
18
+ depth: 1,
19
+ name: 'Child B1',
20
+ parent: 'group-1',
21
+ title: '',
22
+ args: {},
23
+ initialArgs: {},
24
+ importPath: './importPath.js',
25
+ },
26
+ 'group-1--child-b2': {
27
+ type: 'story',
28
+ prepared: true,
29
+ id: 'group-1--child-b2',
30
+ depth: 1,
31
+ name: 'Child B2',
32
+ parent: 'group-1',
33
+ title: '',
34
+ args: {},
35
+ initialArgs: {},
36
+ importPath: './importPath.js',
37
+ },
38
+ 'root-1': {
39
+ type: 'root',
40
+ children: ['root-1-child-a1', 'root-1-child-a2'],
41
+ depth: 0,
42
+ id: 'root-1',
43
+ name: 'Root 1',
44
+ },
45
+ 'root-1-child-a1': {
46
+ type: 'component',
47
+ id: 'root-1-child-a1',
48
+ parent: 'root-1',
49
+ depth: 1,
50
+ name: 'Child A1',
51
+ children: [],
52
+ },
53
+ 'root-1-child-a2': {
54
+ type: 'component',
55
+ id: 'root-1-child-a2',
56
+ parent: 'root-1',
57
+ name: 'Child A2',
58
+ depth: 1,
59
+ children: ['root-1-child-a2--grandchild-a1-1', 'root-1-child-a2--grandchild-a1-2'],
60
+ },
61
+ 'root-1-child-a2--grandchild-a1-1': {
62
+ type: 'story',
63
+ prepared: true,
64
+ id: 'root-1-child-a2--grandchild-a1-1',
65
+ parent: 'root-1-child-a2',
66
+ depth: 2,
67
+ name: 'GrandChild A1.1',
68
+ title: '',
69
+ args: {},
70
+ initialArgs: {},
71
+ importPath: './importPath.js',
72
+ },
73
+ 'root-1-child-a2--grandchild-a1-2': {
74
+ type: 'story',
75
+ prepared: true,
76
+ id: 'root-1-child-a2--grandchild-a1-2',
77
+ parent: 'root-1-child-a2',
78
+ depth: 2,
79
+ name: 'GrandChild A1.2',
80
+ title: '',
81
+ args: {},
82
+ initialArgs: {},
83
+ importPath: './importPath.js',
84
+ },
85
+ 'root-3': {
86
+ type: 'root',
87
+ children: ['root-3--child-a1', 'root-3-child-a2'],
88
+ depth: 0,
89
+ id: 'root-3',
90
+ name: 'Root 3',
91
+ },
92
+ 'root-3--child-a1': {
93
+ type: 'story',
94
+ prepared: true,
95
+ id: 'root-3--child-a1',
96
+ depth: 1,
97
+ name: 'Child A1',
98
+ parent: 'root-3',
99
+ title: '',
100
+ args: {},
101
+ initialArgs: {},
102
+ importPath: './importPath.js',
103
+ },
104
+ 'root-3-child-a2': {
105
+ type: 'component',
106
+ id: 'root-3-child-a2',
107
+ name: 'Child A2',
108
+ depth: 1,
109
+ children: ['root-3-child-a2--grandchild-a1-1', 'root-3-child-a2--grandchild-a1-2'],
110
+ parent: 'root-3',
111
+ },
112
+ 'root-3-child-a2--grandchild-a1-1': {
113
+ type: 'story',
114
+ prepared: true,
115
+ id: 'root-3-child-a2--grandchild-a1-1',
116
+ depth: 2,
117
+ name: 'GrandChild A1.1',
118
+ parent: 'root-3-child-a2',
119
+ title: '',
120
+ args: {},
121
+ initialArgs: {},
122
+ importPath: './importPath.js',
123
+ },
124
+ 'root-3-child-a2--grandchild-a1-2': {
125
+ type: 'story',
126
+ prepared: true,
127
+ id: 'root-3-child-a2--grandchild-a1-2',
128
+ depth: 2,
129
+ name: 'GrandChild A1.2',
130
+ parent: 'root-3-child-a2',
131
+ title: '',
132
+ args: {},
133
+ initialArgs: {},
134
+ importPath: './importPath.js',
135
+ },
136
+ },
137
+ noRoot: {
138
+ 'root-1': {
139
+ children: ['root-1-child-a1', 'root-1-child-a2'],
140
+ type: 'group',
141
+ depth: 0,
142
+ id: 'root-1',
143
+ name: 'Parent A',
144
+ },
145
+ 'group-1': {
146
+ children: ['group-1--child-b1', 'group-1--child-b2'],
147
+ type: 'component',
148
+ depth: 0,
149
+ id: 'group-1',
150
+ name: 'Parent B',
151
+ },
152
+ 'root-1-child-a1': {
153
+ id: 'root-1-child-a1',
154
+ depth: 1,
155
+ name: 'Child A1',
156
+ type: 'story',
157
+ prepared: true,
158
+ parent: 'root-1',
159
+ title: '',
160
+ args: {},
161
+ initialArgs: {},
162
+ importPath: './importPath.js',
163
+ },
164
+ 'root-1-child-a2--grandchild-a1-1': {
165
+ id: 'root-1-child-a2--grandchild-a1-1',
166
+ depth: 2,
167
+ name: 'GrandChild A1.1',
168
+ type: 'story',
169
+ prepared: true,
170
+ parent: 'root-1-child-a2',
171
+ title: '',
172
+ args: {},
173
+ initialArgs: {},
174
+ importPath: './importPath.js',
175
+ },
176
+ 'root-1-child-a2--grandchild-a1-2': {
177
+ id: 'root-1-child-a2--grandchild-a1-2',
178
+ depth: 2,
179
+ name: 'GrandChild A1.2',
180
+ type: 'story',
181
+ prepared: true,
182
+ parent: 'root-1-child-a2',
183
+ title: '',
184
+ args: {},
185
+ initialArgs: {},
186
+ importPath: './importPath.js',
187
+ },
188
+ 'root-1-child-a2': {
189
+ id: 'root-1-child-a2',
190
+ name: 'Child A2',
191
+ depth: 1,
192
+ children: ['root-1-child-a2--grandchild-a1-1', 'root-1-child-a2--grandchild-a1-2'],
193
+ type: 'component',
194
+ parent: 'root-1',
195
+ },
196
+ 'group-1--child-b1': {
197
+ id: 'group-1--child-b1',
198
+ depth: 1,
199
+ name: 'Child B1',
200
+ type: 'story',
201
+ prepared: true,
202
+ parent: 'group-1',
203
+ title: '',
204
+ args: {},
205
+ initialArgs: {},
206
+ importPath: './importPath.js',
207
+ },
208
+ 'group-1--child-b2': {
209
+ id: 'group-1--child-b2',
210
+ depth: 1,
211
+ name: 'Child B2',
212
+ type: 'story',
213
+ prepared: true,
214
+ parent: 'group-1',
215
+ title: '',
216
+ args: {},
217
+ initialArgs: {},
218
+ importPath: './importPath.js',
219
+ },
220
+ },
221
+ };
222
+
223
+ export const mockSelected = {
224
+ withRoot: {
225
+ 'root-1': false,
226
+ 'group-1': false,
227
+ 'root-1-child-a1': false,
228
+ 'root-1-child-a2--grandchild-a1-1': false,
229
+ 'root-1-child-a2--grandchild-a1-2': false,
230
+ 'root-1-child-a2': false,
231
+ 'group-1--child-b1': false,
232
+ 'group-1--child-b2': false,
233
+ 'root-3': false,
234
+ 'root-3--child-a1': false,
235
+ 'root-3-child-a2': false,
236
+ 'root-3-child-a2--grandchild-a1-1': false,
237
+ 'root-3-child-a2--grandchild-a1-2': false,
238
+ },
239
+ noRoot: {
240
+ 'root-1': false,
241
+ 'group-1': false,
242
+ 'root-1-child-a1': false,
243
+ 'root-1-child-a2--grandchild-a1-1': false,
244
+ 'root-1-child-a2--grandchild-a1-2': false,
245
+ 'root-1-child-a2': false,
246
+ 'group-1--child-b1': false,
247
+ 'group-1--child-b2': false,
248
+ },
249
+ };
250
+
251
+ export const mockExpanded = {
252
+ withRoot: {
253
+ 'root-1': true,
254
+ 'group-1': false,
255
+ 'root-1-child-a1': true,
256
+ 'root-1-child-a2--grandchild-a1-1': false,
257
+ 'root-1-child-a2--grandchild-a1-2': false,
258
+ 'root-1-child-a2': false,
259
+ 'group-1--child-b1': false,
260
+ 'group-1--child-b2': false,
261
+ 'root-3': false,
262
+ 'root-3--child-a1': false,
263
+ 'root-3-child-a2': false,
264
+ 'root-3-child-a2--grandchild-a1-1': false,
265
+ 'root-3-child-a2--grandchild-a1-2': false,
266
+ },
267
+ noRoot: {
268
+ 'root-1': true,
269
+ 'group-1': false,
270
+ 'root-1-child-a1': true,
271
+ 'root-1-child-a2--grandchild-a1-1': false,
272
+ 'root-1-child-a2--grandchild-a1-2': false,
273
+ 'root-1-child-a2': false,
274
+ 'group-1--child-b1': false,
275
+ 'group-1--child-b2': false,
276
+ },
277
+ noRootSecond: {
278
+ 'root-1': true,
279
+ 'group-1': false,
280
+ 'root-1-child-a1': true,
281
+ 'root-1-child-a2--grandchild-a1-1': true,
282
+ 'root-1-child-a2--grandchild-a1-2': true,
283
+ 'root-1-child-a2': true,
284
+ 'group-1--child-b1': false,
285
+ 'group-1--child-b2': false,
286
+ },
287
+ };
package/src/types.ts ADDED
@@ -0,0 +1,78 @@
1
+ import type { StoriesHash, State } from '@storybook/manager-api';
2
+ // import type { ControllerStateAndHelpers } from 'downshift';
3
+ import type { API_StatusState, API_StatusValue } from '@storybook/types';
4
+ import * as Fuse from 'fuse.js';
5
+ import { PressableProps } from 'react-native';
6
+
7
+ export type Refs = State['refs'];
8
+ export type RefType = Refs[keyof Refs] & { status?: API_StatusState };
9
+ export type Item = StoriesHash[keyof StoriesHash];
10
+ export type Dataset = Record<string, Item>;
11
+
12
+ export interface CombinedDataset {
13
+ hash: Refs;
14
+ entries: [string, RefType][];
15
+ }
16
+
17
+ export interface ItemRef {
18
+ itemId: string;
19
+ refId: string;
20
+ }
21
+ export interface StoryRef {
22
+ storyId: string;
23
+ refId: string;
24
+ }
25
+
26
+ export type Highlight = ItemRef | null;
27
+ export type Selection = StoryRef | null;
28
+
29
+ // export interface Match {
30
+ // value: string;
31
+ // indices: [number, number][];
32
+ // key: 'name' | 'path';
33
+ // arrayIndex: number;
34
+ // }
35
+
36
+ export function isExpandType(x: any): x is ExpandType {
37
+ return !!(x && x.showAll);
38
+ }
39
+ // export function isSearchResult(x: any): x is SearchResult {
40
+ // return !!(x && x.item);
41
+ // }
42
+ export interface ExpandType {
43
+ showAll: () => void;
44
+ totalCount: number;
45
+ moreCount: number;
46
+ }
47
+
48
+ export type SearchItem = Item & {
49
+ refId: string;
50
+ path: string[];
51
+ status?: API_StatusValue;
52
+ showAll?: () => void;
53
+ };
54
+
55
+ export type SearchResult = Fuse.FuseResult<SearchItem>;
56
+
57
+ export type SearchResultProps = SearchResult & {
58
+ icon: string;
59
+ isHighlighted: boolean;
60
+ onPress: PressableProps['onPress'];
61
+ };
62
+
63
+ // export type DownshiftItem = SearchResult | ExpandType;
64
+ export type GetSearchItemProps = (args: {
65
+ item: SearchResult;
66
+ index: number;
67
+ key: string;
68
+ }) => SearchResultProps;
69
+
70
+ export type SearchChildrenFn = (args: {
71
+ query: string;
72
+ results: SearchResult[]; // TODO fix this type
73
+ isBrowsing: boolean;
74
+ closeMenu: (cb?: () => void) => void;
75
+ // getMenuProps: ControllerStateAndHelpers<DownshiftItem>['getMenuProps'];
76
+ getItemProps: GetSearchItemProps;
77
+ highlightedIndex: number | null;
78
+ }) => React.ReactNode;
@@ -0,0 +1,130 @@
1
+ import type { StoriesHash } from '@storybook/manager-api';
2
+ import { useStorybookApi } from '@storybook/manager-api';
3
+ import { STORIES_COLLAPSE_ALL, STORIES_EXPAND_ALL } from '@storybook/core-events';
4
+
5
+ import type { Dispatch, /* MutableRefObject, */ Reducer } from 'react';
6
+ import { useCallback, useEffect, useReducer } from 'react';
7
+ // import type { Highlight } from './types';
8
+
9
+ import { /* isAncestor */ getAncestorIds /* getDescendantIds */ } from './util/tree';
10
+
11
+ export type ExpandedState = Record<string, boolean>;
12
+
13
+ export interface ExpandAction {
14
+ ids: string[];
15
+ value: boolean;
16
+ }
17
+
18
+ export interface ExpandedProps {
19
+ // containerRef: MutableRefObject<HTMLElement>;
20
+ // isBrowsing: boolean;
21
+ refId: string;
22
+ data: StoriesHash;
23
+ initialExpanded?: ExpandedState;
24
+ rootIds: string[];
25
+ // highlightedRef: MutableRefObject<Highlight>;
26
+ // setHighlightedItemId: (storyId: string) => void;
27
+ selectedStoryId: string | null;
28
+ onSelectStoryId: (storyId: string) => void;
29
+ }
30
+
31
+ const initializeExpanded = ({
32
+ // refId,
33
+ // data,
34
+ initialExpanded,
35
+ // highlightedRef,
36
+ rootIds,
37
+ }: {
38
+ refId: string;
39
+ data: StoriesHash;
40
+ initialExpanded?: ExpandedState;
41
+ // highlightedRef: MutableRefObject<Highlight>;
42
+ rootIds: string[];
43
+ }) => {
44
+ const highlightedAncestors = [];
45
+ return [...rootIds, ...highlightedAncestors].reduce<ExpandedState>(
46
+ (acc, id) => Object.assign(acc, { [id]: id in initialExpanded ? initialExpanded[id] : true }),
47
+ {}
48
+ );
49
+ };
50
+
51
+ const noop = () => {};
52
+
53
+ export const useExpanded = ({
54
+ // containerRef,
55
+ // isBrowsing,
56
+ refId,
57
+ data,
58
+ initialExpanded,
59
+ rootIds,
60
+ // highlightedRef,
61
+ // setHighlightedItemId,
62
+ // onSelectStoryId,
63
+ selectedStoryId,
64
+ }: ExpandedProps): [ExpandedState, Dispatch<ExpandAction>] => {
65
+ const api = useStorybookApi();
66
+
67
+ // Track the set of currently expanded nodes within this tree.
68
+ // Root nodes are expanded by default.
69
+ const [expanded, setExpanded] = useReducer<
70
+ Reducer<ExpandedState, ExpandAction>,
71
+ {
72
+ refId: string;
73
+ data: StoriesHash;
74
+ // highlightedRef: MutableRefObject<Highlight>;
75
+ rootIds: string[];
76
+ initialExpanded: ExpandedState;
77
+ }
78
+ >(
79
+ (state, { ids, value }) =>
80
+ ids.reduce((acc, id) => Object.assign(acc, { [id]: value }), { ...state }),
81
+ { refId, data, /* highlightedRef */ rootIds, initialExpanded },
82
+ initializeExpanded
83
+ );
84
+
85
+ // const highlightElement = useCallback(
86
+ // (element: Element) => {
87
+ // setHighlightedItemId(element.getAttribute('data-item-id'));
88
+ // // scrollIntoView(element);
89
+ // },
90
+ // [setHighlightedItemId]
91
+ // );
92
+
93
+ const updateExpanded = useCallback(({ ids, value }: ExpandAction) => {
94
+ setExpanded({ ids, value });
95
+ // if (ids.length === 1) {
96
+ // const element = containerRef.current?.querySelector(
97
+ // `[data-item-id="${ids[0]}"][data-ref-id="${refId}"]`
98
+ // );
99
+ // if (element) highlightElement(element);
100
+ // }
101
+ }, []);
102
+
103
+ // Expand the whole ancestry of the currently selected story whenever it changes.
104
+ useEffect(() => {
105
+ setExpanded({ ids: getAncestorIds(data, selectedStoryId), value: true });
106
+ }, [data, selectedStoryId]);
107
+
108
+ const collapseAll = useCallback(() => {
109
+ const ids = Object.keys(data).filter((id) => !rootIds.includes(id));
110
+ setExpanded({ ids, value: false });
111
+ }, [data, rootIds]);
112
+
113
+ const expandAll = useCallback(() => {
114
+ setExpanded({ ids: Object.keys(data), value: true });
115
+ }, [data]);
116
+
117
+ useEffect(() => {
118
+ if (!api) return noop;
119
+
120
+ api.on(STORIES_COLLAPSE_ALL, collapseAll);
121
+ api.on(STORIES_EXPAND_ALL, expandAll);
122
+
123
+ return () => {
124
+ api.off(STORIES_COLLAPSE_ALL, collapseAll);
125
+ api.off(STORIES_EXPAND_ALL, expandAll);
126
+ };
127
+ }, [api, collapseAll, expandAll]);
128
+
129
+ return [expanded, updateExpanded];
130
+ };
@@ -0,0 +1,48 @@
1
+ import debounce from 'lodash/debounce.js';
2
+ import { useCallback, useEffect, useMemo, useRef } from 'react';
3
+ import store from 'store2';
4
+
5
+ import type { Selection, StoryRef } from './types';
6
+
7
+ const save = debounce((value) => store.set('lastViewedStoryIds', value), 1000);
8
+
9
+ export const useLastViewed = (selection: Selection) => {
10
+ const initialLastViewedStoryIds = useMemo((): StoryRef[] => {
11
+ const items = store.get('lastViewedStoryIds');
12
+ if (!items || !Array.isArray(items)) return [];
13
+ if (!items.some((item) => typeof item === 'object' && item.storyId && item.refId)) return [];
14
+ return items;
15
+ }, []);
16
+
17
+ const lastViewedRef = useRef(initialLastViewedStoryIds);
18
+
19
+ const updateLastViewed = useCallback(
20
+ (story: StoryRef) => {
21
+ const items = lastViewedRef.current;
22
+ const index = items.findIndex(
23
+ ({ storyId, refId }) => storyId === story.storyId && refId === story.refId
24
+ );
25
+ if (index === 0) return;
26
+ if (index === -1) {
27
+ lastViewedRef.current = [story, ...items];
28
+ } else {
29
+ lastViewedRef.current = [story, ...items.slice(0, index), ...items.slice(index + 1)];
30
+ }
31
+ save(lastViewedRef.current);
32
+ },
33
+ [lastViewedRef]
34
+ );
35
+
36
+ useEffect(() => {
37
+ if (selection) updateLastViewed(selection);
38
+ // eslint-disable-next-line react-hooks/exhaustive-deps
39
+ }, [selection]);
40
+
41
+ return {
42
+ getLastViewed: useCallback(() => lastViewedRef.current, [lastViewedRef]),
43
+ clearLastViewed: useCallback(() => {
44
+ lastViewedRef.current = lastViewedRef.current.slice(0, 1);
45
+ save(lastViewedRef.current);
46
+ }, [lastViewedRef]),
47
+ };
48
+ };
@@ -0,0 +1,70 @@
1
+ import type { ReactElement } from 'react';
2
+ import type { API_HashEntry, API_StatusState, API_StatusValue } from '@storybook/types';
3
+
4
+ import { useTheme } from '@storybook/react-native-theming';
5
+
6
+ import { getDescendantIds } from './tree';
7
+
8
+ import Svg, { Path, SvgProps } from 'react-native-svg';
9
+
10
+ function CircleIcon({ height = 14, width = 14, color, ...props }: SvgProps) {
11
+ return (
12
+ <Svg width={width} height={height} viewBox="0 0 14 14" fill="none" {...props}>
13
+ <Path d="M14 7A7 7 0 110 7a7 7 0 0114 0z" fill={color} />
14
+ </Svg>
15
+ );
16
+ }
17
+
18
+ function SmallIcons(props: SvgProps) {
19
+ return <CircleIcon width={6} height={6} {...props} />;
20
+ }
21
+
22
+ function LoadingIcons(props: SvgProps) {
23
+ const theme = useTheme();
24
+ return (
25
+ <SmallIcons
26
+ color={theme.base === 'light' ? theme.color.mediumdark : theme.color.darker}
27
+ {...props}
28
+ />
29
+ );
30
+ }
31
+
32
+ export const statusPriority: API_StatusValue[] = ['unknown', 'pending', 'success', 'warn', 'error'];
33
+ export const statusMapping: Record<API_StatusValue, [ReactElement | null, string | null]> = {
34
+ unknown: [null, null],
35
+ pending: [<LoadingIcons key="icon" />, 'currentColor'],
36
+ success: [<SmallIcons key="icon" color="green" />, 'currentColor'],
37
+ warn: [<SmallIcons key="icon" color="orange" />, '#A15C20'],
38
+ error: [<SmallIcons key="icon" color="red" />, 'brown'],
39
+ };
40
+
41
+ export const getHighestStatus = (statuses: API_StatusValue[]): API_StatusValue => {
42
+ return statusPriority.reduce(
43
+ (acc, status) => (statuses.includes(status) ? status : acc),
44
+ 'unknown'
45
+ );
46
+ };
47
+
48
+ export function getGroupStatus(
49
+ collapsedData: {
50
+ [x: string]: Partial<API_HashEntry>;
51
+ },
52
+ status: API_StatusState
53
+ ): Record<string, API_StatusValue> {
54
+ return Object.values(collapsedData).reduce<Record<string, API_StatusValue>>((acc, item) => {
55
+ if (item.type === 'group' || item.type === 'component') {
56
+ const leafs = getDescendantIds(collapsedData as any, item.id, false)
57
+ .map((id) => collapsedData[id])
58
+ .filter((i) => i.type === 'story');
59
+
60
+ const combinedStatus = getHighestStatus(
61
+ leafs.flatMap((story) => Object.values(status?.[story.id] || {})).map((s) => s.status)
62
+ );
63
+
64
+ if (combinedStatus) {
65
+ acc[item.id] = combinedStatus;
66
+ }
67
+ }
68
+ return acc;
69
+ }, {});
70
+ }
@@ -0,0 +1,91 @@
1
+ import memoize from 'memoizerific';
2
+ import type { SyntheticEvent } from 'react';
3
+ import type { IndexHash } from '@storybook/manager-api';
4
+
5
+ import { DEFAULT_REF_ID } from '../constants';
6
+ import type { Item, RefType, Dataset, SearchItem } from '../types';
7
+
8
+ export const createId = (itemId: string, refId?: string) =>
9
+ !refId || refId === DEFAULT_REF_ID ? itemId : `${refId}_${itemId}`;
10
+
11
+ export const prevent = (e: SyntheticEvent) => {
12
+ e.preventDefault();
13
+ return false;
14
+ };
15
+
16
+ export const get = memoize(1000)((id: string, dataset: Dataset) => dataset[id]);
17
+
18
+ export const getParent = memoize(1000)((id: string, dataset: Dataset) => {
19
+ const item = get(id, dataset);
20
+ return item && item.type !== 'root' ? get(item.parent, dataset) : undefined;
21
+ });
22
+
23
+ export const getParents = memoize(1000)((id: string, dataset: Dataset): Item[] => {
24
+ const parent = getParent(id, dataset);
25
+ return parent ? [parent, ...getParents(parent.id, dataset)] : [];
26
+ });
27
+
28
+ export const getAncestorIds = memoize(1000)((data: IndexHash, id: string): string[] =>
29
+ getParents(id, data).map((item) => item.id)
30
+ );
31
+
32
+ export const getDescendantIds = memoize(1000)(
33
+ (data: IndexHash, id: string, skipLeafs: boolean): string[] => {
34
+ const entry = data[id];
35
+ const children = entry.type === 'story' || entry.type === 'docs' ? [] : entry.children;
36
+ return children.reduce((acc, childId) => {
37
+ const child = data[childId];
38
+ if (!child || (skipLeafs && (child.type === 'story' || child.type === 'docs'))) return acc;
39
+ acc.push(childId, ...getDescendantIds(data, childId, skipLeafs));
40
+ return acc;
41
+ }, []);
42
+ }
43
+ );
44
+
45
+ export function getPath(item: Item, ref: RefType): string[] {
46
+ const parent = item.type !== 'root' && item.parent ? ref.index[item.parent] : null;
47
+ if (parent) return [...getPath(parent, ref), parent.name];
48
+ return ref.id === DEFAULT_REF_ID ? [] : [ref.title || ref.id];
49
+ }
50
+
51
+ export const searchItem = (item: Item, ref: RefType): SearchItem => {
52
+ return { ...item, refId: ref.id, path: getPath(item, ref) };
53
+ };
54
+
55
+ export function cycle<T>(array: T[], index: number, delta: number): number {
56
+ let next = index + (delta % array.length);
57
+ if (next < 0) next = array.length + next;
58
+ if (next >= array.length) next -= array.length;
59
+ return next;
60
+ }
61
+
62
+ export const getStateType = (
63
+ isLoading: boolean,
64
+ isAuthRequired: boolean,
65
+ isError: boolean,
66
+ isEmpty: boolean
67
+ ) => {
68
+ switch (true) {
69
+ case isAuthRequired:
70
+ return 'auth';
71
+ case isError:
72
+ return 'error';
73
+ case isLoading:
74
+ return 'loading';
75
+ case isEmpty:
76
+ return 'empty';
77
+ default:
78
+ return 'ready';
79
+ }
80
+ };
81
+
82
+ export const isAncestor = (element?: Element, maybeAncestor?: Element): boolean => {
83
+ if (!element || !maybeAncestor) return false;
84
+ if (element === maybeAncestor) return true;
85
+ return isAncestor(element.parentElement, maybeAncestor);
86
+ };
87
+
88
+ export const removeNoiseFromName = (storyName: string) => storyName.replaceAll(/(\s|-|_)/gi, '');
89
+
90
+ export const isStoryHoistable = (storyName: string, componentName: string) =>
91
+ removeNoiseFromName(storyName) === removeNoiseFromName(componentName);