@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/Layout.tsx
ADDED
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
import { SET_CURRENT_STORY } from 'storybook/internal/core-events';
|
|
2
|
+
import { addons } from 'storybook/internal/manager-api';
|
|
3
|
+
import { type API_IndexHash, type Args, type StoryContext } from 'storybook/internal/types';
|
|
4
|
+
import type { ReactRenderer } from '@storybook/react';
|
|
5
|
+
import { styled, ThemeProvider, useTheme, Theme } from '@storybook/react-native-theming';
|
|
6
|
+
import { ReactNode, useState, useCallback, useRef } from 'react';
|
|
7
|
+
import { SafeAreaView, ScrollView, Text, TouchableOpacity, View, ViewStyle } from 'react-native';
|
|
8
|
+
import { IconButton } from './IconButton';
|
|
9
|
+
import { LayoutProvider, useLayout } from './LayoutProvider';
|
|
10
|
+
import { AddonsTabs, MobileAddonsPanel, MobileAddonsPanelRef } from './MobileAddonsPanel';
|
|
11
|
+
import { MobileMenuDrawer, MobileMenuDrawerRef } from './MobileMenuDrawer';
|
|
12
|
+
import { Sidebar } from './Sidebar';
|
|
13
|
+
import { StorybookLogo } from './StorybookLogo';
|
|
14
|
+
import { DEFAULT_REF_ID } from './constants';
|
|
15
|
+
import { useStoreBooleanState } from './hooks/useStoreState';
|
|
16
|
+
import { useStyle } from './util/useStyle';
|
|
17
|
+
import { Storage, StorageProvider } from './StorageProvider';
|
|
18
|
+
import { SelectedNodeProvider } from './SelectedNodeProvider';
|
|
19
|
+
import {
|
|
20
|
+
BottomBarToggleIcon,
|
|
21
|
+
CloseFullscreenIcon,
|
|
22
|
+
FullscreenIcon,
|
|
23
|
+
MenuIcon,
|
|
24
|
+
} from './icon/iconDataUris';
|
|
25
|
+
|
|
26
|
+
const desktopLogoContainer = {
|
|
27
|
+
flexDirection: 'row',
|
|
28
|
+
alignItems: 'center',
|
|
29
|
+
paddingTop: 10,
|
|
30
|
+
paddingLeft: 16,
|
|
31
|
+
paddingBottom: 4,
|
|
32
|
+
paddingRight: 10,
|
|
33
|
+
justifyContent: 'space-between',
|
|
34
|
+
} satisfies ViewStyle;
|
|
35
|
+
|
|
36
|
+
const contentContainerStyle = { flex: 1, overflow: 'hidden' } satisfies ViewStyle;
|
|
37
|
+
|
|
38
|
+
const mobileContentStyle = { flex: 1, overflow: 'hidden' } satisfies ViewStyle;
|
|
39
|
+
|
|
40
|
+
const placeholderObject = {};
|
|
41
|
+
|
|
42
|
+
const iconFloatRightStyle = { marginLeft: 'auto' } satisfies ViewStyle;
|
|
43
|
+
|
|
44
|
+
const navButtonStyle = { flexShrink: 1 } satisfies ViewStyle;
|
|
45
|
+
|
|
46
|
+
const navButtonHitSlop = { bottom: 10, left: 10, right: 10, top: 10 };
|
|
47
|
+
|
|
48
|
+
const mobileMenuDrawerContentStyle = {
|
|
49
|
+
paddingLeft: 16,
|
|
50
|
+
paddingTop: 4,
|
|
51
|
+
paddingBottom: 4,
|
|
52
|
+
} satisfies ViewStyle;
|
|
53
|
+
|
|
54
|
+
export const LiteUI = ({
|
|
55
|
+
storage,
|
|
56
|
+
theme,
|
|
57
|
+
storyHash,
|
|
58
|
+
story,
|
|
59
|
+
children,
|
|
60
|
+
}: {
|
|
61
|
+
storage: Storage;
|
|
62
|
+
theme: Theme;
|
|
63
|
+
storyHash: API_IndexHash | undefined;
|
|
64
|
+
story?: StoryContext<ReactRenderer, Args>;
|
|
65
|
+
children: ReactNode | ReactNode[];
|
|
66
|
+
}) => (
|
|
67
|
+
<>
|
|
68
|
+
<ThemeProvider theme={theme}>
|
|
69
|
+
{/* @ts-ignore something weird with story type */}
|
|
70
|
+
<StorageProvider storage={storage}>
|
|
71
|
+
<LayoutProvider>
|
|
72
|
+
<Layout storyHash={storyHash} story={story}>
|
|
73
|
+
{children}
|
|
74
|
+
</Layout>
|
|
75
|
+
</LayoutProvider>
|
|
76
|
+
</StorageProvider>
|
|
77
|
+
</ThemeProvider>
|
|
78
|
+
</>
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
export const Layout = ({
|
|
82
|
+
storyHash,
|
|
83
|
+
story,
|
|
84
|
+
children,
|
|
85
|
+
}: {
|
|
86
|
+
storyHash: API_IndexHash | undefined;
|
|
87
|
+
story?: StoryContext<ReactRenderer, Args>;
|
|
88
|
+
children: ReactNode | ReactNode[];
|
|
89
|
+
}) => {
|
|
90
|
+
const theme = useTheme();
|
|
91
|
+
|
|
92
|
+
const { isDesktop } = useLayout();
|
|
93
|
+
|
|
94
|
+
const [desktopSidebarOpen, setDesktopSidebarOpen] = useStoreBooleanState(
|
|
95
|
+
'desktopSidebarState',
|
|
96
|
+
true
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
const [desktopAddonsPanelOpen, setDesktopAddonsPanelOpen] = useStoreBooleanState(
|
|
100
|
+
'desktopPanelState',
|
|
101
|
+
true
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
const [uiHidden, setUiHidden] = useState(false);
|
|
105
|
+
|
|
106
|
+
const desktopSidebarStyle = useStyle(
|
|
107
|
+
() => ({
|
|
108
|
+
width: desktopSidebarOpen ? 240 : undefined,
|
|
109
|
+
padding: desktopSidebarOpen ? 0 : 10,
|
|
110
|
+
borderColor: theme.appBorderColor,
|
|
111
|
+
borderRightWidth: 1,
|
|
112
|
+
}),
|
|
113
|
+
[desktopSidebarOpen, theme.appBorderColor]
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
const desktopAddonsPanelStyle = useStyle(
|
|
117
|
+
() => ({
|
|
118
|
+
height: desktopAddonsPanelOpen ? 300 : undefined,
|
|
119
|
+
borderTopWidth: 1,
|
|
120
|
+
borderColor: theme.appBorderColor,
|
|
121
|
+
paddingTop: desktopAddonsPanelOpen ? 4 : 0,
|
|
122
|
+
padding: desktopAddonsPanelOpen ? 0 : 10,
|
|
123
|
+
}),
|
|
124
|
+
[desktopAddonsPanelOpen, theme.appBorderColor]
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
const containerStyle = useStyle(() => {
|
|
128
|
+
if (isDesktop) {
|
|
129
|
+
return {
|
|
130
|
+
flex: 1,
|
|
131
|
+
backgroundColor: theme.background.content,
|
|
132
|
+
flexDirection: 'row',
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
flex: 1,
|
|
138
|
+
backgroundColor: theme.background.content,
|
|
139
|
+
};
|
|
140
|
+
}, [theme.background.content, story?.parameters?.noSafeArea, isDesktop]);
|
|
141
|
+
|
|
142
|
+
const fullScreenButtonStyle = useStyle(
|
|
143
|
+
() => ({
|
|
144
|
+
position: 'absolute',
|
|
145
|
+
bottom: uiHidden ? 56 : 16,
|
|
146
|
+
right: 16,
|
|
147
|
+
backgroundColor: theme.background.content,
|
|
148
|
+
padding: 4,
|
|
149
|
+
borderRadius: 4,
|
|
150
|
+
borderWidth: 1,
|
|
151
|
+
borderColor: theme.appBorderColor,
|
|
152
|
+
}),
|
|
153
|
+
[uiHidden, theme.background.content, theme.appBorderColor]
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
const navButtonTextStyle = useStyle(
|
|
157
|
+
() => ({
|
|
158
|
+
flexShrink: 1,
|
|
159
|
+
color: theme.barTextColor,
|
|
160
|
+
}),
|
|
161
|
+
[theme.barTextColor]
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
const mobileMenuDrawerRef = useRef<MobileMenuDrawerRef>(null);
|
|
165
|
+
const addonPanelRef = useRef<MobileAddonsPanelRef>(null);
|
|
166
|
+
|
|
167
|
+
const setSelection = useCallback(({ storyId: newStoryId }: { storyId: string }) => {
|
|
168
|
+
const channel = addons.getChannel();
|
|
169
|
+
|
|
170
|
+
channel.emit(SET_CURRENT_STORY, { storyId: newStoryId });
|
|
171
|
+
}, []);
|
|
172
|
+
|
|
173
|
+
return (
|
|
174
|
+
<SafeAreaView style={containerStyle}>
|
|
175
|
+
{isDesktop ? (
|
|
176
|
+
<View style={desktopSidebarStyle}>
|
|
177
|
+
{desktopSidebarOpen ? (
|
|
178
|
+
<ScrollView keyboardShouldPersistTaps="handled">
|
|
179
|
+
<View style={desktopLogoContainer}>
|
|
180
|
+
<StorybookLogo theme={theme} />
|
|
181
|
+
|
|
182
|
+
<IconButton onPress={() => setDesktopSidebarOpen(false)} Icon={MenuIcon} />
|
|
183
|
+
</View>
|
|
184
|
+
|
|
185
|
+
<Sidebar
|
|
186
|
+
previewInitialized
|
|
187
|
+
indexError={undefined}
|
|
188
|
+
refs={placeholderObject}
|
|
189
|
+
setSelection={setSelection}
|
|
190
|
+
status={placeholderObject}
|
|
191
|
+
index={storyHash}
|
|
192
|
+
storyId={story?.id}
|
|
193
|
+
refId={DEFAULT_REF_ID}
|
|
194
|
+
/>
|
|
195
|
+
</ScrollView>
|
|
196
|
+
) : (
|
|
197
|
+
<IconButton onPress={() => setDesktopSidebarOpen(true)} Icon={MenuIcon} />
|
|
198
|
+
)}
|
|
199
|
+
</View>
|
|
200
|
+
) : null}
|
|
201
|
+
|
|
202
|
+
<View style={mobileContentStyle}>
|
|
203
|
+
<View style={contentContainerStyle}>{children}</View>
|
|
204
|
+
|
|
205
|
+
{story?.parameters?.hideFullScreenButton || isDesktop ? null : (
|
|
206
|
+
<TouchableOpacity
|
|
207
|
+
style={fullScreenButtonStyle}
|
|
208
|
+
onPress={() => setUiHidden((prev) => !prev)}
|
|
209
|
+
>
|
|
210
|
+
{uiHidden ? (
|
|
211
|
+
<CloseFullscreenIcon color={theme.color.mediumdark} />
|
|
212
|
+
) : (
|
|
213
|
+
<FullscreenIcon color={theme.color.mediumdark} />
|
|
214
|
+
)}
|
|
215
|
+
</TouchableOpacity>
|
|
216
|
+
)}
|
|
217
|
+
|
|
218
|
+
{isDesktop ? (
|
|
219
|
+
<View style={desktopAddonsPanelStyle}>
|
|
220
|
+
{desktopAddonsPanelOpen ? (
|
|
221
|
+
<AddonsTabs storyId={story?.id} onClose={() => setDesktopAddonsPanelOpen(false)} />
|
|
222
|
+
) : (
|
|
223
|
+
<IconButton
|
|
224
|
+
style={iconFloatRightStyle}
|
|
225
|
+
onPress={() => setDesktopAddonsPanelOpen(true)}
|
|
226
|
+
Icon={BottomBarToggleIcon}
|
|
227
|
+
/>
|
|
228
|
+
)}
|
|
229
|
+
</View>
|
|
230
|
+
) : null}
|
|
231
|
+
</View>
|
|
232
|
+
|
|
233
|
+
{!uiHidden && !isDesktop ? (
|
|
234
|
+
<Container>
|
|
235
|
+
<Nav>
|
|
236
|
+
<Button
|
|
237
|
+
testID="mobile-menu-button"
|
|
238
|
+
style={navButtonStyle}
|
|
239
|
+
hitSlop={navButtonHitSlop}
|
|
240
|
+
onPress={() => mobileMenuDrawerRef.current?.setMobileMenuOpen(true)}
|
|
241
|
+
>
|
|
242
|
+
<MenuIcon color={theme.color.mediumdark} />
|
|
243
|
+
<Text style={navButtonTextStyle} numberOfLines={1}>
|
|
244
|
+
{story?.title}/{story?.name}
|
|
245
|
+
</Text>
|
|
246
|
+
</Button>
|
|
247
|
+
|
|
248
|
+
<IconButton
|
|
249
|
+
testID="mobile-addons-button"
|
|
250
|
+
onPress={() => addonPanelRef.current.setAddonsPanelOpen(true)}
|
|
251
|
+
Icon={BottomBarToggleIcon}
|
|
252
|
+
/>
|
|
253
|
+
</Nav>
|
|
254
|
+
</Container>
|
|
255
|
+
) : null}
|
|
256
|
+
|
|
257
|
+
{isDesktop ? null : (
|
|
258
|
+
<SelectedNodeProvider>
|
|
259
|
+
<MobileMenuDrawer ref={mobileMenuDrawerRef}>
|
|
260
|
+
<View style={mobileMenuDrawerContentStyle}>
|
|
261
|
+
<StorybookLogo theme={theme} />
|
|
262
|
+
</View>
|
|
263
|
+
|
|
264
|
+
<Sidebar
|
|
265
|
+
previewInitialized
|
|
266
|
+
indexError={undefined}
|
|
267
|
+
refs={placeholderObject}
|
|
268
|
+
setSelection={setSelection}
|
|
269
|
+
status={placeholderObject}
|
|
270
|
+
index={storyHash}
|
|
271
|
+
storyId={story?.id}
|
|
272
|
+
refId={DEFAULT_REF_ID}
|
|
273
|
+
/>
|
|
274
|
+
</MobileMenuDrawer>
|
|
275
|
+
</SelectedNodeProvider>
|
|
276
|
+
)}
|
|
277
|
+
|
|
278
|
+
{isDesktop ? null : <MobileAddonsPanel ref={addonPanelRef} storyId={story?.id} />}
|
|
279
|
+
</SafeAreaView>
|
|
280
|
+
);
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
const Nav = styled.View({
|
|
284
|
+
display: 'flex',
|
|
285
|
+
flexDirection: 'row',
|
|
286
|
+
alignItems: 'center',
|
|
287
|
+
justifyContent: 'space-between',
|
|
288
|
+
width: '100%',
|
|
289
|
+
height: 40,
|
|
290
|
+
paddingHorizontal: 6,
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
const Container = styled.View(({ theme }) => ({
|
|
294
|
+
alignSelf: 'flex-end',
|
|
295
|
+
width: '100%',
|
|
296
|
+
backgroundColor: theme.barBg,
|
|
297
|
+
borderTopColor: theme.appBorderColor,
|
|
298
|
+
borderTopWidth: 1,
|
|
299
|
+
}));
|
|
300
|
+
|
|
301
|
+
const Button = styled.TouchableOpacity(({ theme }) => ({
|
|
302
|
+
display: 'flex',
|
|
303
|
+
flexDirection: 'row',
|
|
304
|
+
alignItems: 'center',
|
|
305
|
+
gap: 10,
|
|
306
|
+
color: theme.color.mediumdark,
|
|
307
|
+
fontSize: theme.typography.size?.s2 - 1,
|
|
308
|
+
paddingHorizontal: 7,
|
|
309
|
+
fontWeight: theme.typography.weight.bold,
|
|
310
|
+
}));
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { FC, PropsWithChildren } from 'react';
|
|
2
|
+
import { createContext, useContext, useMemo } from 'react';
|
|
3
|
+
import { useWindowDimensions } from 'react-native';
|
|
4
|
+
import { BREAKPOINT } from './constants';
|
|
5
|
+
|
|
6
|
+
type LayoutContextType = {
|
|
7
|
+
isDesktop: boolean;
|
|
8
|
+
isMobile: boolean;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const LayoutContext = createContext<LayoutContextType>({
|
|
12
|
+
isDesktop: false,
|
|
13
|
+
isMobile: true,
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
export const LayoutProvider: FC<PropsWithChildren> = ({ children }) => {
|
|
17
|
+
const { width } = useWindowDimensions();
|
|
18
|
+
const isDesktop = width >= BREAKPOINT;
|
|
19
|
+
const isMobile = !isDesktop;
|
|
20
|
+
|
|
21
|
+
const contextValue = useMemo(
|
|
22
|
+
() => ({
|
|
23
|
+
isDesktop,
|
|
24
|
+
isMobile,
|
|
25
|
+
}),
|
|
26
|
+
[isDesktop, isMobile]
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
return <LayoutContext.Provider value={contextValue}>{children}</LayoutContext.Provider>;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export const useLayout = () => useContext(LayoutContext);
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { styled, useTheme } from '@storybook/react-native-theming';
|
|
2
|
+
import { forwardRef, useImperativeHandle, useMemo, useState } from 'react';
|
|
3
|
+
import {
|
|
4
|
+
KeyboardAvoidingView,
|
|
5
|
+
Modal,
|
|
6
|
+
Pressable,
|
|
7
|
+
SafeAreaView,
|
|
8
|
+
ScrollView,
|
|
9
|
+
StyleProp,
|
|
10
|
+
Text,
|
|
11
|
+
View,
|
|
12
|
+
ViewStyle,
|
|
13
|
+
} from 'react-native';
|
|
14
|
+
import { addons } from 'storybook/internal/manager-api';
|
|
15
|
+
import { Addon_TypesEnum } from 'storybook/internal/types';
|
|
16
|
+
import { IconButton } from './IconButton';
|
|
17
|
+
import { CloseIcon } from './icon/iconDataUris';
|
|
18
|
+
|
|
19
|
+
export interface MobileAddonsPanelRef {
|
|
20
|
+
setAddonsPanelOpen: (isOpen: boolean) => void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const MobileAddonsPanel = forwardRef<MobileAddonsPanelRef, { storyId?: string }>(
|
|
24
|
+
({ storyId }, ref) => {
|
|
25
|
+
const theme = useTheme();
|
|
26
|
+
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
|
27
|
+
|
|
28
|
+
useImperativeHandle(ref, () => ({
|
|
29
|
+
setAddonsPanelOpen: (open: boolean) => {
|
|
30
|
+
if (open) {
|
|
31
|
+
setMobileMenuOpen(true);
|
|
32
|
+
} else {
|
|
33
|
+
setMobileMenuOpen(false);
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
}));
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<Modal
|
|
40
|
+
visible={mobileMenuOpen}
|
|
41
|
+
onRequestClose={() => setMobileMenuOpen(false)}
|
|
42
|
+
transparent
|
|
43
|
+
animationType="slide"
|
|
44
|
+
>
|
|
45
|
+
<KeyboardAvoidingView behavior="height" style={{ flex: 1 }}>
|
|
46
|
+
<SafeAreaView style={{ justifyContent: 'flex-end', flex: 1 }}>
|
|
47
|
+
<View
|
|
48
|
+
style={{ flex: 1, borderBottomColor: theme.appBorderColor, borderBottomWidth: 1 }}
|
|
49
|
+
>
|
|
50
|
+
<Pressable style={{ flex: 1 }} onPress={() => setMobileMenuOpen(false)}></Pressable>
|
|
51
|
+
</View>
|
|
52
|
+
|
|
53
|
+
<View
|
|
54
|
+
style={{ height: '50%', backgroundColor: theme.background.content, paddingTop: 10 }}
|
|
55
|
+
>
|
|
56
|
+
<AddonsTabs
|
|
57
|
+
onClose={() => {
|
|
58
|
+
setMobileMenuOpen(false);
|
|
59
|
+
}}
|
|
60
|
+
storyId={storyId}
|
|
61
|
+
/>
|
|
62
|
+
</View>
|
|
63
|
+
</SafeAreaView>
|
|
64
|
+
</KeyboardAvoidingView>
|
|
65
|
+
</Modal>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
MobileAddonsPanel.displayName = 'MobileAddonsPanel';
|
|
71
|
+
|
|
72
|
+
const addonsTabsContainerStyle = {
|
|
73
|
+
flex: 1,
|
|
74
|
+
} satisfies StyleProp<ViewStyle>;
|
|
75
|
+
|
|
76
|
+
const addonsTabsStyle = {
|
|
77
|
+
flexDirection: 'row',
|
|
78
|
+
borderBottomWidth: 1,
|
|
79
|
+
borderBottomColor: 'lightgrey',
|
|
80
|
+
} satisfies StyleProp<ViewStyle>;
|
|
81
|
+
|
|
82
|
+
const addonsTabsContentContainerStyle = {
|
|
83
|
+
justifyContent: 'center',
|
|
84
|
+
} satisfies StyleProp<ViewStyle>;
|
|
85
|
+
|
|
86
|
+
const closeIconStyle = {
|
|
87
|
+
marginRight: 4,
|
|
88
|
+
marginBottom: 4,
|
|
89
|
+
alignItems: 'center',
|
|
90
|
+
justifyContent: 'center',
|
|
91
|
+
} satisfies StyleProp<ViewStyle>;
|
|
92
|
+
|
|
93
|
+
const addonsScrollStyle = {
|
|
94
|
+
flex: 1,
|
|
95
|
+
} satisfies StyleProp<ViewStyle>;
|
|
96
|
+
|
|
97
|
+
const centeredStyle = {
|
|
98
|
+
alignItems: 'center',
|
|
99
|
+
justifyContent: 'center',
|
|
100
|
+
} satisfies StyleProp<ViewStyle>;
|
|
101
|
+
|
|
102
|
+
const scrollContentContainerStyle = {
|
|
103
|
+
paddingBottom: 16,
|
|
104
|
+
} satisfies StyleProp<ViewStyle>;
|
|
105
|
+
const hitSlop = { top: 10, right: 10, bottom: 10, left: 10 };
|
|
106
|
+
|
|
107
|
+
export const AddonsTabs = ({ onClose, storyId }: { onClose?: () => void; storyId?: string }) => {
|
|
108
|
+
const panels = addons.getElements(Addon_TypesEnum.PANEL);
|
|
109
|
+
|
|
110
|
+
const [addonSelected, setAddonSelected] = useState(Object.keys(panels)[0]);
|
|
111
|
+
|
|
112
|
+
const panel = useMemo(() => {
|
|
113
|
+
if (!storyId) {
|
|
114
|
+
return (
|
|
115
|
+
<View style={centeredStyle}>
|
|
116
|
+
<Text>No Story Selected</Text>
|
|
117
|
+
</View>
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (Object.keys(panels).length === 0) {
|
|
122
|
+
return (
|
|
123
|
+
<View style={centeredStyle}>
|
|
124
|
+
<Text>No addons loaded.</Text>
|
|
125
|
+
</View>
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return panels[addonSelected].render({ active: true });
|
|
130
|
+
}, [addonSelected, panels, storyId]);
|
|
131
|
+
|
|
132
|
+
return (
|
|
133
|
+
<View style={addonsTabsContainerStyle}>
|
|
134
|
+
<View style={addonsTabsStyle}>
|
|
135
|
+
<ScrollView
|
|
136
|
+
horizontal
|
|
137
|
+
showsHorizontalScrollIndicator={false}
|
|
138
|
+
contentContainerStyle={addonsTabsContentContainerStyle}
|
|
139
|
+
>
|
|
140
|
+
{Object.values(panels).map(({ id, title }) => {
|
|
141
|
+
const resolvedTitle = typeof title === 'function' ? title({}) : title;
|
|
142
|
+
|
|
143
|
+
return (
|
|
144
|
+
<Tab
|
|
145
|
+
key={id}
|
|
146
|
+
active={id === addonSelected}
|
|
147
|
+
onPress={() => setAddonSelected(id)}
|
|
148
|
+
text={resolvedTitle}
|
|
149
|
+
/>
|
|
150
|
+
);
|
|
151
|
+
})}
|
|
152
|
+
</ScrollView>
|
|
153
|
+
|
|
154
|
+
<IconButton
|
|
155
|
+
style={closeIconStyle}
|
|
156
|
+
hitSlop={hitSlop}
|
|
157
|
+
Icon={CloseIcon}
|
|
158
|
+
onPress={() => onClose?.()}
|
|
159
|
+
/>
|
|
160
|
+
</View>
|
|
161
|
+
<ScrollView
|
|
162
|
+
style={addonsScrollStyle}
|
|
163
|
+
// keyboardShouldPersistTaps="handled"
|
|
164
|
+
contentContainerStyle={scrollContentContainerStyle}
|
|
165
|
+
>
|
|
166
|
+
{panel}
|
|
167
|
+
</ScrollView>
|
|
168
|
+
</View>
|
|
169
|
+
);
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
const Tab = ({ active, onPress, text }: { active: boolean; onPress: () => void; text: string }) => {
|
|
173
|
+
return (
|
|
174
|
+
<TabButton active={active} onPress={onPress}>
|
|
175
|
+
<TabText active={active}>{text}</TabText>
|
|
176
|
+
</TabButton>
|
|
177
|
+
);
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
const TabButton = styled.TouchableOpacity<{ active: boolean }>(({ theme, active }) => ({
|
|
181
|
+
borderBottomWidth: active ? 2 : 0,
|
|
182
|
+
borderBottomColor: active ? theme.barSelectedColor : undefined,
|
|
183
|
+
overflow: 'hidden',
|
|
184
|
+
paddingHorizontal: 15,
|
|
185
|
+
justifyContent: 'center',
|
|
186
|
+
alignItems: 'center',
|
|
187
|
+
}));
|
|
188
|
+
|
|
189
|
+
const TabText = styled.Text<{ active: boolean }>(({ theme, active }) => ({
|
|
190
|
+
color: active ? theme.barSelectedColor : theme.color.mediumdark,
|
|
191
|
+
textAlign: 'center',
|
|
192
|
+
fontWeight: 'bold',
|
|
193
|
+
fontSize: 12,
|
|
194
|
+
lineHeight: 12,
|
|
195
|
+
}));
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { useTheme } from '@storybook/react-native-theming';
|
|
2
|
+
import { forwardRef, memo, ReactNode, useImperativeHandle, useMemo, useState } from 'react';
|
|
3
|
+
import {
|
|
4
|
+
Keyboard,
|
|
5
|
+
KeyboardAvoidingView,
|
|
6
|
+
Modal,
|
|
7
|
+
Pressable,
|
|
8
|
+
SafeAreaView,
|
|
9
|
+
ScrollView,
|
|
10
|
+
StyleProp,
|
|
11
|
+
useWindowDimensions,
|
|
12
|
+
View,
|
|
13
|
+
ViewStyle,
|
|
14
|
+
} from 'react-native';
|
|
15
|
+
|
|
16
|
+
import { useSelectedNode } from './SelectedNodeProvider';
|
|
17
|
+
|
|
18
|
+
interface MobileMenuDrawerProps {
|
|
19
|
+
children: ReactNode | ReactNode[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface MobileMenuDrawerRef {
|
|
23
|
+
setMobileMenuOpen: (isOpen: boolean) => void;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const MobileMenuDrawer = memo(
|
|
27
|
+
forwardRef<MobileMenuDrawerRef, MobileMenuDrawerProps>(({ children }, ref) => {
|
|
28
|
+
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
|
29
|
+
const { scrollToSelectedNode, scrollRef } = useSelectedNode();
|
|
30
|
+
const { height } = useWindowDimensions();
|
|
31
|
+
useImperativeHandle(ref, () => ({
|
|
32
|
+
setMobileMenuOpen: (open: boolean) => {
|
|
33
|
+
if (open) {
|
|
34
|
+
scrollToSelectedNode();
|
|
35
|
+
setMobileMenuOpen(true);
|
|
36
|
+
} else {
|
|
37
|
+
Keyboard.dismiss();
|
|
38
|
+
setMobileMenuOpen(false);
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
}));
|
|
42
|
+
const theme = useTheme();
|
|
43
|
+
const bgColorStyle = useMemo(() => {
|
|
44
|
+
return {
|
|
45
|
+
marginTop: 'auto',
|
|
46
|
+
backgroundColor: theme.background.content,
|
|
47
|
+
height: height,
|
|
48
|
+
width: '100%',
|
|
49
|
+
overflow: 'hidden',
|
|
50
|
+
} satisfies StyleProp<ViewStyle>;
|
|
51
|
+
}, [height, theme.background.content]);
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<Modal
|
|
55
|
+
visible={mobileMenuOpen}
|
|
56
|
+
style={bgColorStyle}
|
|
57
|
+
animationType="slide"
|
|
58
|
+
transparent
|
|
59
|
+
onRequestClose={() => setMobileMenuOpen(false)}
|
|
60
|
+
>
|
|
61
|
+
<KeyboardAvoidingView behavior="height" style={{ flex: 1 }}>
|
|
62
|
+
<SafeAreaView style={{ justifyContent: 'flex-end', flex: 1 }}>
|
|
63
|
+
<View
|
|
64
|
+
style={{ flex: 1, borderBottomColor: theme.appBorderColor, borderBottomWidth: 1 }}
|
|
65
|
+
>
|
|
66
|
+
<Pressable style={{ flex: 1 }} onPress={() => setMobileMenuOpen(false)}></Pressable>
|
|
67
|
+
</View>
|
|
68
|
+
|
|
69
|
+
<View style={{ height: '65%' }}>
|
|
70
|
+
<ScrollView
|
|
71
|
+
ref={scrollRef}
|
|
72
|
+
keyboardShouldPersistTaps="handled"
|
|
73
|
+
style={{
|
|
74
|
+
flex: 1,
|
|
75
|
+
paddingBottom: 150,
|
|
76
|
+
paddingTop: 24,
|
|
77
|
+
alignSelf: 'flex-end',
|
|
78
|
+
width: '100%',
|
|
79
|
+
backgroundColor: theme.background.content,
|
|
80
|
+
}}
|
|
81
|
+
>
|
|
82
|
+
{children}
|
|
83
|
+
</ScrollView>
|
|
84
|
+
</View>
|
|
85
|
+
</SafeAreaView>
|
|
86
|
+
</KeyboardAvoidingView>
|
|
87
|
+
</Modal>
|
|
88
|
+
);
|
|
89
|
+
})
|
|
90
|
+
);
|
package/src/Refs.tsx
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import type { FC } from 'react';
|
|
2
|
+
import React, { useMemo, useCallback, useEffect, useState } from 'react';
|
|
3
|
+
import type { State } from 'storybook/internal/manager-api';
|
|
4
|
+
import { styled } from '@storybook/react-native-theming';
|
|
5
|
+
import { Tree } from './Tree';
|
|
6
|
+
import type { RefType } from './types';
|
|
7
|
+
import { getStateType } from './util/tree';
|
|
8
|
+
|
|
9
|
+
export interface RefProps {
|
|
10
|
+
isLoading: boolean;
|
|
11
|
+
isBrowsing: boolean;
|
|
12
|
+
selectedStoryId: string | null;
|
|
13
|
+
setSelection: (selection: { refId: string; storyId: string }) => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const Wrapper = styled.View<{ isMain: boolean }>(() => ({
|
|
17
|
+
position: 'relative',
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
export const Ref: FC<RefType & RefProps & { status?: State['status'] }> = React.memo(
|
|
21
|
+
function Ref(props) {
|
|
22
|
+
const {
|
|
23
|
+
index,
|
|
24
|
+
id: refId,
|
|
25
|
+
title = refId,
|
|
26
|
+
isLoading: isLoadingMain,
|
|
27
|
+
isBrowsing,
|
|
28
|
+
selectedStoryId,
|
|
29
|
+
loginUrl,
|
|
30
|
+
type,
|
|
31
|
+
expanded = true,
|
|
32
|
+
indexError,
|
|
33
|
+
previewInitialized,
|
|
34
|
+
setSelection,
|
|
35
|
+
} = props;
|
|
36
|
+
const length = useMemo(() => (index ? Object.keys(index).length : 0), [index]);
|
|
37
|
+
|
|
38
|
+
const isLoadingInjected =
|
|
39
|
+
(type === 'auto-inject' && !previewInitialized) || type === 'server-checked';
|
|
40
|
+
const isLoading = isLoadingMain || isLoadingInjected || type === 'unknown';
|
|
41
|
+
const isError = !!indexError;
|
|
42
|
+
const isEmpty = !isLoading && length === 0;
|
|
43
|
+
const isAuthRequired = !!loginUrl && length === 0;
|
|
44
|
+
|
|
45
|
+
const state = getStateType(isLoading, isAuthRequired, isError, isEmpty);
|
|
46
|
+
const [isExpanded, setExpanded] = useState<boolean>(expanded);
|
|
47
|
+
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
if (index && selectedStoryId && index[selectedStoryId]) {
|
|
50
|
+
setExpanded(true);
|
|
51
|
+
}
|
|
52
|
+
}, [setExpanded, index, selectedStoryId]);
|
|
53
|
+
|
|
54
|
+
const onSelectStoryId = useCallback(
|
|
55
|
+
(storyId: string) => {
|
|
56
|
+
setSelection({ refId, storyId });
|
|
57
|
+
},
|
|
58
|
+
[refId, setSelection]
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<>
|
|
63
|
+
{isExpanded && (
|
|
64
|
+
<Wrapper data-title={title} isMain={true}>
|
|
65
|
+
{state === 'ready' && (
|
|
66
|
+
<Tree
|
|
67
|
+
status={props.status}
|
|
68
|
+
isBrowsing={isBrowsing}
|
|
69
|
+
isMain={true}
|
|
70
|
+
refId={refId}
|
|
71
|
+
data={index}
|
|
72
|
+
docsMode={false}
|
|
73
|
+
selectedStoryId={selectedStoryId}
|
|
74
|
+
onSelectStoryId={onSelectStoryId}
|
|
75
|
+
/>
|
|
76
|
+
)}
|
|
77
|
+
</Wrapper>
|
|
78
|
+
)}
|
|
79
|
+
</>
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
);
|