@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
package/src/Tree.tsx ADDED
@@ -0,0 +1,441 @@
1
+ import type {
2
+ API,
3
+ ComponentEntry,
4
+ GroupEntry,
5
+ State,
6
+ StoriesHash,
7
+ StoryEntry,
8
+ } from '@storybook/manager-api';
9
+ import { useStorybookApi } from '@storybook/manager-api';
10
+ import { styled } from '@storybook/react-native-theming';
11
+ import React, { useCallback, useMemo, useRef } from 'react';
12
+ import { IconButton } from './IconButton';
13
+ import { ComponentNode, GroupNode, StoryNode } from './TreeNode';
14
+ import { CollapseIcon } from './icon/CollapseIcon';
15
+ import type { ExpandAction, ExpandedState } from './useExpanded';
16
+ import { useExpanded } from './useExpanded';
17
+ import { createId, getAncestorIds, getDescendantIds, isStoryHoistable } from './util/tree';
18
+ import { getGroupStatus, statusMapping } from './util/status';
19
+ import { Text, View } from 'react-native';
20
+ import { ExpandAllIcon } from './icon/ExpandAllIcon';
21
+ import { CollapseAllIcon } from './icon/CollapseAllIcon';
22
+ import { Item } from './types';
23
+ import { useLayout } from './LayoutProvider';
24
+
25
+ // export type Item = StoriesHash[keyof StoriesHash];
26
+
27
+ // export const createId = (itemId: string, refId?: string) =>
28
+ // !refId || refId === DEFAULT_REF_ID ? itemId : `${refId}_${itemId}`;
29
+
30
+ // export const statusPriority: API_StatusValue[] = ['unknown', 'pending', 'success', 'warn', 'error'];
31
+
32
+ // export const getHighestStatus = (statuses: API_StatusValue[]): API_StatusValue => {
33
+ // return statusPriority.reduce(
34
+ // (acc, status) => (statuses.includes(status) ? status : acc),
35
+ // 'unknown'
36
+ // );
37
+ // };
38
+
39
+ interface NodeProps {
40
+ item: Item;
41
+ refId: string;
42
+ docsMode: boolean;
43
+ isOrphan: boolean;
44
+ isDisplayed: boolean;
45
+ color: string | undefined;
46
+ isSelected: boolean;
47
+ isFullyExpanded?: boolean;
48
+ isExpanded: boolean;
49
+ setExpanded: (action: ExpandAction) => void;
50
+ setFullyExpanded?: () => void;
51
+ onSelectStoryId: (itemId: string) => void;
52
+ status: State['status'][keyof State['status']];
53
+ api: API;
54
+ }
55
+
56
+ export const Node = React.memo<NodeProps>(function Node({
57
+ item,
58
+ // status,
59
+ refId,
60
+ isOrphan,
61
+ isDisplayed,
62
+ isSelected,
63
+ isFullyExpanded,
64
+ color: _2,
65
+ setFullyExpanded,
66
+ isExpanded,
67
+ setExpanded,
68
+ onSelectStoryId,
69
+ api: _1,
70
+ }) {
71
+ const { isDesktop, isMobile, closeMobileMenu } = useLayout();
72
+
73
+ if (!isDisplayed) {
74
+ return null;
75
+ }
76
+
77
+ const id = createId(item.id, refId);
78
+
79
+ if (item.type === 'story') {
80
+ const LeafNode = StoryNode;
81
+ // const statusValue = getHighestStatus(Object.values(status || {}).map((s) => s.status));
82
+ // const [icon, textColor] = statusMapping[statusValue];
83
+
84
+ return (
85
+ <LeafNodeStyleWrapper>
86
+ <LeafNode
87
+ selected={isSelected}
88
+ // style={isSelected ? {} : { color: textColor }}
89
+ key={id}
90
+ // href={getLink(item, refId)}
91
+ id={id}
92
+ depth={isOrphan ? item.depth : item.depth - 1}
93
+ onPress={() => {
94
+ onSelectStoryId(item.id);
95
+ if (isMobile) closeMobileMenu();
96
+ }}
97
+ >
98
+ {(item.renderLabel as (i: typeof item) => React.ReactNode)?.(item) || item.name}
99
+ </LeafNode>
100
+ </LeafNodeStyleWrapper>
101
+ );
102
+ }
103
+
104
+ if (item.type === 'root') {
105
+ return (
106
+ <RootNode
107
+ key={id}
108
+ id={id}
109
+ // className="sidebar-subheading"
110
+ // data-ref-id={refId}
111
+ // data-item-id={item.id}
112
+ // data-nodetype="root"
113
+ >
114
+ <CollapseButton
115
+ // type="button"
116
+ data-action="collapse-root"
117
+ onPress={(event) => {
118
+ event.preventDefault();
119
+ setExpanded({ ids: [item.id], value: !isExpanded });
120
+ }}
121
+ aria-expanded={isExpanded}
122
+ >
123
+ <CollapseIcon isExpanded={isExpanded} />
124
+ <Text>{item.renderLabel?.(item) || item.name}</Text>
125
+ </CollapseButton>
126
+ {isExpanded && (
127
+ <IconButton
128
+ // className="sidebar-subheading-action"
129
+ aria-label={isFullyExpanded ? 'Expand' : 'Collapse'}
130
+ data-action="expand-all"
131
+ data-expanded={isFullyExpanded}
132
+ onPress={(event) => {
133
+ event.preventDefault();
134
+ setFullyExpanded();
135
+ }}
136
+ >
137
+ {isFullyExpanded ? <CollapseAllIcon /> : <ExpandAllIcon />}
138
+ </IconButton>
139
+ )}
140
+ </RootNode>
141
+ );
142
+ }
143
+
144
+ if (item.type === 'component' || item.type === 'group') {
145
+ const BranchNode = item.type === 'component' ? ComponentNode : GroupNode;
146
+ return (
147
+ <BranchNode
148
+ key={id}
149
+ id={id}
150
+ // className="sidebar-item"
151
+ // data-ref-id={refId}
152
+ // data-item-id={item.id}
153
+ // data-parent-id={item.parent}
154
+ // data-nodetype={item.type === 'component' ? 'component' : 'group'}
155
+ // data-highlightable={isDisplayed}
156
+ aria-controls={item.children && item.children[0]}
157
+ aria-expanded={isExpanded}
158
+ depth={isOrphan ? item.depth : item.depth - 1}
159
+ isComponent={item.type === 'component'}
160
+ isExpandable={item.children && item.children.length > 0}
161
+ isExpanded={isExpanded}
162
+ onPress={(event) => {
163
+ event.preventDefault();
164
+ setExpanded({ ids: [item.id], value: !isExpanded });
165
+ if (item.type === 'component' && !isExpanded && isDesktop) onSelectStoryId(item.id);
166
+ }}
167
+ >
168
+ {(item.renderLabel as (i: typeof item) => React.ReactNode)?.(item) || item.name}
169
+ </BranchNode>
170
+ );
171
+ }
172
+
173
+ return null;
174
+ });
175
+
176
+ export const LeafNodeStyleWrapper = styled.View(({ theme }) => ({
177
+ position: 'relative',
178
+ display: 'flex',
179
+ flexDirection: 'row',
180
+ justifyContent: 'space-between',
181
+ alignItems: 'center',
182
+ paddingRight: 20,
183
+ color: theme.color.defaultText,
184
+ background: 'transparent',
185
+ minHeight: 28,
186
+ borderRadius: 4,
187
+ }));
188
+
189
+ export const RootNode = styled.View(({}) => ({
190
+ display: 'flex',
191
+ flexDirection: 'row',
192
+ alignItems: 'center',
193
+ justifyContent: 'space-between',
194
+ marginTop: 16,
195
+ marginBottom: 4,
196
+ minHeight: 28,
197
+ }));
198
+
199
+ export const RootNodeText = styled.Text(({ theme }) => ({
200
+ fontSize: theme.typography.size.s1 - 1,
201
+ fontWeight: theme.typography.weight.bold,
202
+ color: theme.textMutedColor,
203
+ lineHeight: 16,
204
+ letterSpacing: 2.5,
205
+ // letterSpacing: '0.16em',
206
+ textTransform: 'uppercase',
207
+ }));
208
+
209
+ const CollapseButton = styled.TouchableOpacity(({}) => ({
210
+ display: 'flex',
211
+ flexDirection: 'row',
212
+ paddingVertical: 0,
213
+ paddingHorizontal: 8,
214
+ borderRadius: 4,
215
+ // transition: 'color 150ms, box-shadow 150ms',
216
+ gap: 6,
217
+ alignItems: 'center',
218
+ cursor: 'pointer',
219
+ height: 28,
220
+ }));
221
+
222
+ export const Tree = React.memo<{
223
+ isBrowsing: boolean;
224
+ isMain: boolean;
225
+ status?: State['status'];
226
+ refId: string;
227
+ data: StoriesHash;
228
+ docsMode: boolean;
229
+ selectedStoryId: string | null;
230
+ onSelectStoryId: (storyId: string) => void;
231
+ }>(function Tree({
232
+ // isBrowsing,
233
+ isMain,
234
+ refId,
235
+ data,
236
+ status,
237
+ docsMode,
238
+ selectedStoryId,
239
+ onSelectStoryId,
240
+ }) {
241
+ const containerRef = useRef<View>(null);
242
+ const api = useStorybookApi();
243
+
244
+ // Find top-level nodes and group them so we can hoist any orphans and expand any roots.
245
+ const [rootIds, orphanIds, initialExpanded] = useMemo(
246
+ () =>
247
+ Object.keys(data).reduce<[string[], string[], ExpandedState]>(
248
+ (acc, id) => {
249
+ const item = data[id];
250
+ if (item.type === 'root') acc[0].push(id);
251
+ else if (!item.parent) acc[1].push(id);
252
+ if (item.type === 'root' && item.startCollapsed) acc[2][id] = false;
253
+ return acc;
254
+ },
255
+ [[], [], {}]
256
+ ),
257
+ [data]
258
+ );
259
+
260
+ // Create a map of expandable descendants for each root/orphan item, which is needed later.
261
+ // Doing that here is a performance enhancement, as it avoids traversing the tree again later.
262
+ const { expandableDescendants } = useMemo(() => {
263
+ return [...orphanIds, ...rootIds].reduce(
264
+ (acc, nodeId) => {
265
+ acc.expandableDescendants[nodeId] = getDescendantIds(data, nodeId, false).filter(
266
+ (d) => !['story', 'docs'].includes(data[d].type)
267
+ );
268
+ return acc;
269
+ },
270
+ { orphansFirst: [] as string[], expandableDescendants: {} as Record<string, string[]> }
271
+ );
272
+ }, [data, rootIds, orphanIds]);
273
+
274
+ // Create a list of component IDs which should be collapsed into their (only) child.
275
+ // That is:
276
+ // - components with a single story child with the same name
277
+ // - components with only a single docs child
278
+ const singleStoryComponentIds = useMemo(() => {
279
+ return Object.keys(data).filter((id) => {
280
+ const entry = data[id];
281
+ if (entry.type !== 'component') return false;
282
+
283
+ const { children = [], name } = entry;
284
+ if (children.length !== 1) return false;
285
+
286
+ const onlyChild = data[children[0]];
287
+
288
+ if (onlyChild.type === 'docs') return true;
289
+ if (onlyChild.type === 'story') return isStoryHoistable(onlyChild.name, name);
290
+ return false;
291
+ });
292
+ }, [data]);
293
+
294
+ // Omit single-story components from the list of nodes.
295
+ const collapsedItems = useMemo(
296
+ () => Object.keys(data).filter((id) => !singleStoryComponentIds.includes(id)),
297
+ // eslint-disable-next-line react-hooks/exhaustive-deps
298
+ [singleStoryComponentIds]
299
+ );
300
+
301
+ // Rewrite the dataset to place the child story in place of the component.
302
+ const collapsedData = useMemo(() => {
303
+ return singleStoryComponentIds.reduce(
304
+ (acc, id) => {
305
+ const { children, parent, name } = data[id] as ComponentEntry;
306
+ const [childId] = children;
307
+ if (parent) {
308
+ const siblings = [...(data[parent] as GroupEntry).children];
309
+ siblings[siblings.indexOf(id)] = childId;
310
+ acc[parent] = { ...data[parent], children: siblings } as GroupEntry;
311
+ }
312
+ acc[childId] = {
313
+ ...data[childId],
314
+ name,
315
+ parent,
316
+ depth: data[childId].depth - 1,
317
+ } as StoryEntry;
318
+ return acc;
319
+ },
320
+ { ...data }
321
+ );
322
+ // eslint-disable-next-line react-hooks/exhaustive-deps
323
+ }, [data]);
324
+
325
+ const ancestry = useMemo(() => {
326
+ return collapsedItems.reduce(
327
+ (acc, id) => Object.assign(acc, { [id]: getAncestorIds(collapsedData, id) }),
328
+ {} as { [key: string]: string[] }
329
+ );
330
+ }, [collapsedItems, collapsedData]);
331
+
332
+ // Track expanded nodes, keep it in sync with props and enable keyboard shortcuts.
333
+ const [expanded, setExpanded] = useExpanded({
334
+ // containerRef,
335
+ // isBrowsing,
336
+ refId,
337
+ data: collapsedData,
338
+ initialExpanded,
339
+ rootIds,
340
+ // highlightedRef,
341
+ // setHighlightedItemId,
342
+ selectedStoryId,
343
+ onSelectStoryId,
344
+ });
345
+
346
+ const groupStatus = useMemo(() => getGroupStatus(collapsedData, status), [collapsedData, status]);
347
+
348
+ const treeItems = useMemo(() => {
349
+ return collapsedItems.map((itemId) => {
350
+ const item = collapsedData[itemId];
351
+ const id = createId(itemId, refId);
352
+
353
+ if (item.type === 'root') {
354
+ const descendants = expandableDescendants[item.id];
355
+ const isFullyExpanded = descendants.every((d: string) => expanded[d]);
356
+ return (
357
+ // @ts-expect-error (TODO)
358
+ <Root
359
+ key={id}
360
+ item={item}
361
+ refId={refId}
362
+ isOrphan={false}
363
+ isDisplayed
364
+ isSelected={selectedStoryId === itemId}
365
+ isExpanded={!!expanded[itemId]}
366
+ setExpanded={setExpanded}
367
+ isFullyExpanded={isFullyExpanded}
368
+ expandableDescendants={descendants}
369
+ onSelectStoryId={onSelectStoryId}
370
+ />
371
+ );
372
+ }
373
+
374
+ const isDisplayed = !item.parent || ancestry[itemId].every((a: string) => expanded[a]);
375
+ const color = groupStatus[itemId] ? statusMapping[groupStatus[itemId]][1] : null;
376
+
377
+ return (
378
+ <Node
379
+ api={api}
380
+ key={id}
381
+ item={item}
382
+ status={status?.[itemId]}
383
+ refId={refId}
384
+ color={color}
385
+ docsMode={docsMode}
386
+ isOrphan={orphanIds.some((oid) => itemId === oid || itemId.startsWith(`${oid}-`))}
387
+ isDisplayed={isDisplayed}
388
+ isSelected={selectedStoryId === itemId}
389
+ isExpanded={!!expanded[itemId]}
390
+ setExpanded={setExpanded}
391
+ onSelectStoryId={onSelectStoryId}
392
+ />
393
+ );
394
+ });
395
+ }, [
396
+ ancestry,
397
+ api,
398
+ collapsedData,
399
+ collapsedItems,
400
+ docsMode,
401
+ expandableDescendants,
402
+ expanded,
403
+ groupStatus,
404
+ onSelectStoryId,
405
+ orphanIds,
406
+ refId,
407
+ selectedStoryId,
408
+ setExpanded,
409
+ status,
410
+ ]);
411
+ return (
412
+ <Container ref={containerRef} hasOrphans={isMain && orphanIds.length > 0}>
413
+ {treeItems}
414
+ </Container>
415
+ );
416
+ });
417
+
418
+ const Container = styled.View<{ hasOrphans: boolean }>((props) => ({
419
+ marginTop: props.hasOrphans ? 20 : 0,
420
+ marginBottom: 20,
421
+ }));
422
+
423
+ const Root = React.memo<NodeProps & { expandableDescendants: string[] }>(function Root({
424
+ setExpanded,
425
+ isFullyExpanded,
426
+ expandableDescendants,
427
+ ...props
428
+ }) {
429
+ const setFullyExpanded = useCallback(
430
+ () => setExpanded({ ids: expandableDescendants, value: !isFullyExpanded }),
431
+ [setExpanded, isFullyExpanded, expandableDescendants]
432
+ );
433
+ return (
434
+ <Node
435
+ {...props}
436
+ setExpanded={setExpanded}
437
+ isFullyExpanded={isFullyExpanded}
438
+ setFullyExpanded={setFullyExpanded}
439
+ />
440
+ );
441
+ });
@@ -0,0 +1,122 @@
1
+ import React from 'react';
2
+ // import { HighlightStyles } from './HighlightStyles';
3
+ // import { LeafNodeStyleWrapper } from './Tree';
4
+ import type { Meta, StoryObj } from '@storybook/react';
5
+ import { ComponentNode, GroupNode, StoryNode } from './TreeNode';
6
+ import { View } from 'react-native';
7
+
8
+ // import { IconSymbols } from './IconSymbols';
9
+
10
+ const meta = {
11
+ title: 'UI/Sidebar/TreeNode',
12
+ parameters: { layout: 'fullscreen' },
13
+ component: StoryNode,
14
+ } satisfies Meta<typeof StoryNode>;
15
+ export default meta;
16
+
17
+ export const Types: StoryObj<typeof meta> = {
18
+ render: () => (
19
+ <View style={{ flex: 1 }}>
20
+ <ComponentNode>Component</ComponentNode>
21
+ <GroupNode>Group</GroupNode>
22
+ <StoryNode>Story</StoryNode>
23
+ </View>
24
+ ),
25
+ args: {
26
+ children: <></>,
27
+ },
28
+ };
29
+
30
+ export const Expandable = () => (
31
+ <>
32
+ <ComponentNode isExpandable>Collapsed component</ComponentNode>
33
+ <ComponentNode isExpandable isExpanded>
34
+ Expanded component
35
+ </ComponentNode>
36
+ <GroupNode isExpandable>Collapsed group</GroupNode>
37
+ <GroupNode isExpandable isExpanded>
38
+ Expanded group
39
+ </GroupNode>
40
+ </>
41
+ );
42
+
43
+ export const ExpandableLongName = () => (
44
+ <>
45
+ <ComponentNode isExpandable>
46
+ Collapsed component with a very very very very very very very very very very very very very
47
+ very very very very very very veryvery very very very very very very very very veryvery very
48
+ very very very very very very very veryvery very very very very very very very very veryvery
49
+ very very very very very very very very veryvery very very very very very very very very
50
+ veryvery very very very very very very very very veryvery very very very very very very very
51
+ very very long name
52
+ </ComponentNode>
53
+ <ComponentNode isExpandable isExpanded>
54
+ Expanded component with a very very very very very very very very very very very very very
55
+ very very very very very very veryvery very very very very very very very very veryvery very
56
+ very very very very very very very veryvery very very very very very very very very veryvery
57
+ very very very very very very very very veryvery very very very very very very very very
58
+ veryvery very very very very very very very very veryvery very very very very very very very
59
+ very very long name
60
+ </ComponentNode>
61
+ <GroupNode isExpandable>
62
+ Collapsed group with a very very very very very very very very very very very very very very
63
+ very very very very very veryvery very very very very very very very very veryvery very very
64
+ very very very very very very veryvery very very very very very very very very veryvery very
65
+ very very very very very very very veryvery very very very very very very very very veryvery
66
+ very very very very very very very very veryvery very very very very very very very very very
67
+ long name
68
+ </GroupNode>
69
+ <GroupNode isExpandable isExpanded>
70
+ Expanded group with a very very very very very very very very very very very very very very
71
+ very very very very very veryvery very very very very very very very very veryvery very very
72
+ very very very very very very veryvery very very very very very very very very veryvery very
73
+ very very very very very very very veryvery very very very very very very very very veryvery
74
+ very very very very very very very very veryvery very very very very very very very very very
75
+ long name
76
+ </GroupNode>
77
+ </>
78
+ );
79
+
80
+ export const Nested = () => (
81
+ <>
82
+ <GroupNode isExpandable isExpanded depth={0}>
83
+ Zero
84
+ </GroupNode>
85
+ <GroupNode isExpandable isExpanded depth={1}>
86
+ One
87
+ </GroupNode>
88
+ <StoryNode depth={2}>Two</StoryNode>
89
+ <ComponentNode isExpandable isExpanded depth={2}>
90
+ Two
91
+ </ComponentNode>
92
+ <StoryNode depth={3}>Three</StoryNode>
93
+ </>
94
+ );
95
+ export const Selection = () => (
96
+ <>
97
+ <StoryNode selected={false}>Default story</StoryNode>
98
+ <StoryNode selected>Selected story</StoryNode>
99
+ </>
100
+ );
101
+
102
+ export const SelectionWithLongName = () => (
103
+ <>
104
+ <StoryNode>
105
+ Default story with a very very very very very very very very very very very very very very
106
+ very very very very very veryvery very very very very very very very very veryvery very very
107
+ very very very very very very veryvery very very very very very very very very veryvery very
108
+ very very very very very very very veryvery very very very very very very very very veryvery
109
+ very very very very very very very very veryvery very very very very very very very very very
110
+ long name
111
+ </StoryNode>
112
+
113
+ <StoryNode selected>
114
+ Selected story with a very very very very very very very very very very very very very very
115
+ very very very very very veryvery very very very very very very very very veryvery very very
116
+ very very very very very very veryvery very very very very very very very very veryvery very
117
+ very very very very very very very veryvery very very very very very very very very veryvery
118
+ very very very very very very very very veryvery very very very very very very very very very
119
+ long name
120
+ </StoryNode>
121
+ </>
122
+ );
@@ -0,0 +1,146 @@
1
+ import { styled, useTheme } from '@storybook/react-native-theming';
2
+ import { ComponentIcon } from './icon/ComponentIcon';
3
+ import { GroupIcon } from './icon/GroupIcon';
4
+
5
+ import { StoryIcon } from './icon/StoryIcon';
6
+ import { CollapseIcon } from './icon/CollapseIcon';
7
+ import React, { ComponentProps, FC } from 'react';
8
+ import { transparentize } from 'polished';
9
+
10
+ export interface NodeProps {
11
+ children: React.ReactNode | React.ReactNode[];
12
+ isExpandable?: boolean;
13
+ isExpanded?: boolean;
14
+ }
15
+
16
+ const BranchNodeText = styled.Text<{ isSelected?: boolean }>(({ theme }) => ({
17
+ textAlign: 'left',
18
+ fontSize: theme.typography.size.s2,
19
+ flexShrink: 1,
20
+ }));
21
+
22
+ const BranchNode = styled.TouchableOpacity<{
23
+ depth?: number;
24
+ isExpandable?: boolean;
25
+ isExpanded?: boolean;
26
+ isComponent?: boolean;
27
+ isSelected?: boolean;
28
+ }>(({ depth = 0, isExpandable = false, theme }) => ({
29
+ width: '100%',
30
+ border: 'none',
31
+ cursor: 'pointer',
32
+ display: 'flex',
33
+ flexDirection: 'row',
34
+ alignItems: 'flex-start',
35
+ alignSelf: 'flex-start',
36
+ paddingLeft: (isExpandable ? 8 : 22) + depth * 18,
37
+
38
+ background: 'transparent',
39
+ minHeight: 28,
40
+ borderRadius: 4,
41
+ gap: 6,
42
+ paddingTop: 5,
43
+ paddingBottom: 4,
44
+
45
+ // will this actually do anything?
46
+ '&:hover, &:focus': {
47
+ background: transparentize(0.93, theme.color.secondary),
48
+ outline: 'none',
49
+ },
50
+ }));
51
+
52
+ const LeafNode = styled.TouchableOpacity<{ depth?: number; selected?: boolean }>(
53
+ ({ depth = 0, selected, theme }) => ({
54
+ alignSelf: 'flex-start',
55
+ cursor: 'pointer',
56
+ color: 'inherit',
57
+ display: 'flex',
58
+ gap: 6,
59
+ flexDirection: 'row',
60
+ alignItems: 'flex-start',
61
+ paddingLeft: 22 + depth * 18,
62
+ paddingTop: 5,
63
+ paddingBottom: 4,
64
+ backgroundColor: selected ? theme.color.secondary : undefined,
65
+ // flex: 1,
66
+ // not sure 👇
67
+ width: '100%',
68
+ borderRadius: 4,
69
+ paddingRight: 20,
70
+ minHeight: 28,
71
+ })
72
+ );
73
+
74
+ const LeafNodeText = styled.Text<{ depth?: number; selected?: boolean }>(({ theme, selected }) => ({
75
+ fontSize: theme.typography.size.s2,
76
+ flexShrink: 1,
77
+ color: selected ? theme.color.lightest : theme.color.defaultText,
78
+ }));
79
+
80
+ const Wrapper = styled.View({
81
+ display: 'flex',
82
+ flexDirection: 'row',
83
+ alignItems: 'center',
84
+ gap: 6,
85
+ marginTop: 2,
86
+ });
87
+
88
+ export const GroupNode: FC<
89
+ ComponentProps<typeof BranchNode> & { isExpanded?: boolean; isExpandable?: boolean }
90
+ > = React.memo(function GroupNode({
91
+ children,
92
+ isExpanded = false,
93
+ isExpandable = false,
94
+ ...props
95
+ }) {
96
+ const theme = useTheme();
97
+
98
+ return (
99
+ <BranchNode isExpandable={isExpandable} {...props}>
100
+ <Wrapper>
101
+ {isExpandable && <CollapseIcon isExpanded={isExpanded} />}
102
+ <GroupIcon
103
+ width="14"
104
+ height="14"
105
+ color={theme.base === 'dark' ? theme.color.primary : theme.color.ultraviolet}
106
+ />
107
+ </Wrapper>
108
+ <BranchNodeText>{children}</BranchNodeText>
109
+ </BranchNode>
110
+ );
111
+ });
112
+
113
+ export const ComponentNode: FC<ComponentProps<typeof BranchNode>> = React.memo(
114
+ function ComponentNode({ children, isExpanded, isExpandable, ...props }) {
115
+ const theme = useTheme();
116
+ return (
117
+ <BranchNode isExpandable={isExpandable} {...props}>
118
+ <Wrapper>
119
+ {isExpandable && <CollapseIcon isExpanded={isExpanded} />}
120
+ <ComponentIcon width={12} height={12} color={theme.color.secondary} />
121
+ </Wrapper>
122
+ <BranchNodeText>{children}</BranchNodeText>
123
+ </BranchNode>
124
+ );
125
+ }
126
+ );
127
+
128
+ export const StoryNode: FC<ComponentProps<typeof LeafNode>> = React.memo(function StoryNode({
129
+ children,
130
+ ...props
131
+ }) {
132
+ const theme = useTheme();
133
+
134
+ return (
135
+ <LeafNode {...props}>
136
+ <Wrapper>
137
+ <StoryIcon
138
+ width={14}
139
+ height={14}
140
+ color={props.selected ? theme.color.lightest : theme.color.seafoam}
141
+ />
142
+ </Wrapper>
143
+ <LeafNodeText selected={props.selected}>{children}</LeafNodeText>
144
+ </LeafNode>
145
+ );
146
+ });