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