@storybook/react-native-ui-lite 10.2.0 → 10.2.2-alpha.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@storybook/react-native-ui-lite",
3
- "version": "10.2.0",
3
+ "version": "10.2.2-alpha.0",
4
4
  "description": "lightweight ui components for react native storybook",
5
5
  "keywords": [
6
6
  "react",
@@ -39,10 +39,11 @@
39
39
  },
40
40
  "dependencies": {
41
41
  "@gorhom/portal": "^1.0.14",
42
- "@storybook/react": "10.2.0-beta.1",
43
- "@storybook/react-native-theming": "^10.2.0",
44
- "@storybook/react-native-ui-common": "^10.2.0",
45
- "fuse.js": "^7.1.0",
42
+ "@legendapp/list": "3.0.0-beta.31",
43
+ "@nozbe/microfuzz": "^1.0.0",
44
+ "@storybook/react": "^10.2.2",
45
+ "@storybook/react-native-theming": "^10.2.2-alpha.0",
46
+ "@storybook/react-native-ui-common": "^10.2.2-alpha.0",
46
47
  "polished": "^4.3.1",
47
48
  "react-native-safe-area-context": "^5"
48
49
  },
@@ -57,5 +58,5 @@
57
58
  "publishConfig": {
58
59
  "access": "public"
59
60
  },
60
- "gitHead": "eca4a572e630d6536b62685bdd62114994c7515d"
61
+ "gitHead": "5e99a2f9de73aa3b88e6517f1016e933cac67c19"
61
62
  }
package/src/Explorer.tsx CHANGED
@@ -2,7 +2,9 @@ import type { FC } from 'react';
2
2
  import React, { useRef } from 'react';
3
3
  import { Ref } from './Refs';
4
4
  import type { CombinedDataset, Selection } from '@storybook/react-native-ui-common';
5
- import { View } from 'react-native';
5
+ import { View, ViewStyle } from 'react-native';
6
+
7
+ const containerStyle: ViewStyle = { flex: 1 };
6
8
 
7
9
  export interface ExplorerProps {
8
10
  isLoading: boolean;
@@ -22,7 +24,7 @@ export const Explorer: FC<ExplorerProps> = React.memo(function Explorer({
22
24
  const containerRef = useRef<View>(null);
23
25
 
24
26
  return (
25
- <View ref={containerRef} id="storybook-explorer-tree">
27
+ <View ref={containerRef} style={containerStyle}>
26
28
  {dataset.entries.map(([refId, ref]) => (
27
29
  <Ref
28
30
  {...ref}
package/src/Layout.tsx CHANGED
@@ -11,7 +11,7 @@ import {
11
11
  useStyle,
12
12
  } from '@storybook/react-native-ui-common';
13
13
  import { ReactElement, ReactNode, useCallback, useLayoutEffect, useRef, useState } from 'react';
14
- import { ScrollView, Text, TouchableOpacity, View, ViewStyle } from 'react-native';
14
+ import { Text, TouchableOpacity, View, ViewStyle } from 'react-native';
15
15
  import { SafeAreaProvider, useSafeAreaInsets } from 'react-native-safe-area-context';
16
16
  import { SET_CURRENT_STORY } from 'storybook/internal/core-events';
17
17
  import type { Args, StoryContext } from 'storybook/internal/csf';
@@ -48,30 +48,35 @@ const placeholderObject = {};
48
48
 
49
49
  const iconFloatRightStyle = { marginLeft: 'auto' } satisfies ViewStyle;
50
50
 
51
- const navButtonStyle = { flexShrink: 1 } satisfies ViewStyle;
51
+ const navButtonStyle = { flex: 1 } satisfies ViewStyle;
52
52
 
53
- const navButtonHitSlop = { bottom: 10, left: 10, right: 10, top: 10 };
53
+ const navButtonHitSlop = { bottom: 10, left: 10, top: 10 };
54
+
55
+ const addonButtonHitSlop = { bottom: 10, left: 10, right: 10, top: 10 };
54
56
 
55
57
  const mobileMenuDrawerContentStyle = {
56
58
  paddingLeft: 16,
57
- paddingTop: 4,
58
59
  paddingBottom: 4,
59
60
  } satisfies ViewStyle;
60
61
 
62
+ const flexStyle = { flex: 1 } satisfies ViewStyle;
63
+
61
64
  export const LiteUI: SBUI = ({ storage, theme, storyHash, story, children }): ReactElement => (
62
- <SafeAreaProvider style={{ flex: 1 }}>
63
- <ThemeProvider theme={theme}>
64
- <StorageProvider storage={storage}>
65
- <LayoutProvider>
66
- <PortalProvider shouldAddRootHost={false}>
67
- <Layout storyHash={storyHash} story={story}>
68
- {children}
69
- </Layout>
70
- <PortalHost name="storybook-lite-ui-root" />
71
- </PortalProvider>
72
- </LayoutProvider>
73
- </StorageProvider>
74
- </ThemeProvider>
65
+ <SafeAreaProvider style={flexStyle}>
66
+ <SelectedNodeProvider>
67
+ <ThemeProvider theme={theme}>
68
+ <StorageProvider storage={storage}>
69
+ <LayoutProvider>
70
+ <PortalProvider shouldAddRootHost={false}>
71
+ <Layout storyHash={storyHash} story={story}>
72
+ {children}
73
+ </Layout>
74
+ <PortalHost name="storybook-lite-ui-root" />
75
+ </PortalProvider>
76
+ </LayoutProvider>
77
+ </StorageProvider>
78
+ </ThemeProvider>
79
+ </SelectedNodeProvider>
75
80
  </SafeAreaProvider>
76
81
  );
77
82
 
@@ -186,24 +191,26 @@ export const Layout = ({
186
191
  {isDesktop ? (
187
192
  <View style={desktopSidebarStyle}>
188
193
  {desktopSidebarOpen ? (
189
- <ScrollView keyboardShouldPersistTaps="handled">
194
+ <>
190
195
  <View style={desktopLogoContainer}>
191
196
  <StorybookLogo theme={theme} />
192
197
 
193
198
  <IconButton onPress={() => setDesktopSidebarOpen(false)} Icon={MenuIcon} />
194
199
  </View>
195
200
 
196
- <Sidebar
197
- previewInitialized
198
- indexError={undefined}
199
- refs={placeholderObject}
200
- setSelection={setSelection}
201
- status={placeholderObject}
202
- index={storyHash}
203
- storyId={story?.id}
204
- refId={DEFAULT_REF_ID}
205
- />
206
- </ScrollView>
201
+ <View style={flexStyle}>
202
+ <Sidebar
203
+ previewInitialized
204
+ indexError={undefined}
205
+ refs={placeholderObject}
206
+ setSelection={setSelection}
207
+ status={placeholderObject}
208
+ index={storyHash}
209
+ storyId={story?.id}
210
+ refId={DEFAULT_REF_ID}
211
+ />
212
+ </View>
213
+ </>
207
214
  ) : (
208
215
  <IconButton onPress={() => setDesktopSidebarOpen(true)} Icon={MenuIcon} />
209
216
  )}
@@ -216,6 +223,7 @@ export const Layout = ({
216
223
  {story?.parameters?.hideFullScreenButton || isDesktop ? null : (
217
224
  <TouchableOpacity
218
225
  style={fullScreenButtonStyle}
226
+ hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
219
227
  onPress={() => setUiHidden((prev) => !prev)}
220
228
  >
221
229
  {uiHidden ? (
@@ -258,6 +266,7 @@ export const Layout = ({
258
266
 
259
267
  <IconButton
260
268
  testID="mobile-addons-button"
269
+ hitSlop={addonButtonHitSlop}
261
270
  onPress={() => addonPanelRef.current.setAddonsPanelOpen(true)}
262
271
  Icon={BottomBarToggleIcon}
263
272
  />
@@ -266,24 +275,22 @@ export const Layout = ({
266
275
  ) : null}
267
276
 
268
277
  {isDesktop ? null : (
269
- <SelectedNodeProvider>
270
- <MobileMenuDrawer ref={mobileMenuDrawerRef}>
271
- <View style={mobileMenuDrawerContentStyle}>
272
- <StorybookLogo theme={theme} />
273
- </View>
274
-
275
- <Sidebar
276
- previewInitialized
277
- indexError={undefined}
278
- refs={placeholderObject}
279
- setSelection={setSelection}
280
- status={placeholderObject}
281
- index={storyHash}
282
- storyId={story?.id}
283
- refId={DEFAULT_REF_ID}
284
- />
285
- </MobileMenuDrawer>
286
- </SelectedNodeProvider>
278
+ <MobileMenuDrawer ref={mobileMenuDrawerRef}>
279
+ <View style={mobileMenuDrawerContentStyle}>
280
+ <StorybookLogo theme={theme} />
281
+ </View>
282
+
283
+ <Sidebar
284
+ previewInitialized
285
+ indexError={undefined}
286
+ refs={placeholderObject}
287
+ setSelection={setSelection}
288
+ status={placeholderObject}
289
+ index={storyHash}
290
+ storyId={story?.id}
291
+ refId={DEFAULT_REF_ID}
292
+ />
293
+ </MobileMenuDrawer>
287
294
  )}
288
295
 
289
296
  {isDesktop ? null : <MobileAddonsPanel ref={addonPanelRef} storyId={story?.id} />}
@@ -1,30 +1,43 @@
1
+ import { Portal } from '@gorhom/portal';
1
2
  import { useTheme } from '@storybook/react-native-theming';
2
3
  import {
3
4
  forwardRef,
4
5
  memo,
5
6
  ReactNode,
7
+ useCallback,
6
8
  useEffect,
7
9
  useImperativeHandle,
8
10
  useMemo,
9
- useRef,
10
11
  useState,
11
12
  } from 'react';
12
13
  import {
13
14
  Animated,
15
+ Easing,
14
16
  Keyboard,
15
17
  useWindowDimensions,
16
- Modal,
17
18
  PanResponder,
18
- PanResponderInstance,
19
19
  Pressable,
20
- ScrollView,
21
20
  View,
21
+ ViewStyle,
22
22
  KeyboardEventListener,
23
23
  Platform,
24
24
  } from 'react-native';
25
+
25
26
  import { useSelectedNode } from './SelectedNodeProvider';
26
27
  import useAnimatedValue from './useAnimatedValue';
27
28
 
29
+ const flexStyle: ViewStyle = { flex: 1 };
30
+
31
+ const portalContainerStyle: ViewStyle = {
32
+ flex: 1,
33
+ position: 'absolute',
34
+ top: 0,
35
+ left: 0,
36
+ right: 0,
37
+ bottom: 0,
38
+ zIndex: 1000,
39
+ };
40
+
28
41
  interface MobileMenuDrawerProps {
29
42
  children: ReactNode | ReactNode[];
30
43
  }
@@ -45,6 +58,7 @@ export const useAnimatedModalHeight = () => {
45
58
  Animated.timing(animatedHeight, {
46
59
  toValue: maxModalHeight,
47
60
  duration,
61
+ easing: Easing.out(Easing.quad),
48
62
  useNativeDriver: false,
49
63
  }).start();
50
64
 
@@ -52,6 +66,7 @@ export const useAnimatedModalHeight = () => {
52
66
  Animated.timing(animatedHeight, {
53
67
  toValue: modalHeight,
54
68
  duration,
69
+ easing: Easing.out(Easing.quad),
55
70
  useNativeDriver: false,
56
71
  }).start();
57
72
 
@@ -96,53 +111,90 @@ export const useAnimatedModalHeight = () => {
96
111
 
97
112
  export const MobileMenuDrawer = memo(
98
113
  forwardRef<MobileMenuDrawerRef, MobileMenuDrawerProps>(({ children }, ref) => {
99
- const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
100
- const { scrollToSelectedNode, scrollRef } = useSelectedNode();
114
+ // const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
115
+ const [isVisible, setIsVisible] = useState(false);
116
+ const { scrollCallback } = useSelectedNode();
101
117
  const theme = useTheme();
118
+ const { height } = useWindowDimensions();
102
119
  const animatedHeight = useAnimatedModalHeight();
103
120
 
121
+ // Slide animation for drawer entrance/exit
122
+ const slideAnim = useAnimatedValue(height);
123
+
104
124
  // Create a reference for the drag handle animation
105
125
  const dragY = useAnimatedValue(0);
106
126
 
127
+ const openDrawer = useCallback(() => {
128
+ dragY.setValue(0);
129
+ slideAnim.setValue(height);
130
+ setIsVisible(true);
131
+ // setMobileMenuOpen(true);
132
+ Animated.timing(slideAnim, {
133
+ toValue: 0,
134
+ duration: 300,
135
+ easing: Easing.out(Easing.quad),
136
+ useNativeDriver: true,
137
+ }).start(({ finished }) => {
138
+ if (finished) {
139
+ // go to the selected story and don't animate
140
+ scrollCallback({ animated: false, id: undefined });
141
+ }
142
+ });
143
+ }, [dragY, height, scrollCallback, slideAnim]);
144
+
145
+ const closeDrawer = useCallback(() => {
146
+ Keyboard.dismiss();
147
+ // setMobileMenuOpen(false);
148
+ Animated.timing(slideAnim, {
149
+ toValue: height,
150
+ duration: 300,
151
+ easing: Easing.in(Easing.quad),
152
+ useNativeDriver: true,
153
+ }).start(({ finished }) => {
154
+ if (finished) {
155
+ setIsVisible(false);
156
+ }
157
+ });
158
+ }, [height, slideAnim]);
159
+
107
160
  // Create the pan responder for handling drag gestures
108
- const panResponder = useRef<PanResponderInstance>(
109
- PanResponder.create({
110
- onStartShouldSetPanResponder: () => true,
111
- onMoveShouldSetPanResponder: (_, gestureState) => {
112
- // Only capture downward dragging motions
113
- return gestureState.dy > 0;
114
- },
115
- onPanResponderMove: (_, gestureState) => {
116
- // Update dragY based on the gesture
117
- if (gestureState.dy > 0) {
118
- dragY.setValue(gestureState.dy);
119
- }
120
- },
121
- onPanResponderRelease: (_, gestureState) => {
122
- // If dragged down enough, close the drawer
123
- if (gestureState.dy > 50) {
124
- Keyboard.dismiss();
125
- setMobileMenuOpen(false);
126
- }
127
- // Reset the drag position
128
- Animated.timing(dragY, {
129
- toValue: 0,
130
- duration: 300,
131
- useNativeDriver: true,
132
- }).start();
133
- },
134
- })
135
- ).current;
161
+ const panResponder = useMemo(
162
+ () =>
163
+ PanResponder.create({
164
+ onStartShouldSetPanResponder: () => true,
165
+ onMoveShouldSetPanResponder: (_, gestureState) => {
166
+ // Only capture downward dragging motions
167
+ return gestureState.dy > 0;
168
+ },
169
+ onPanResponderMove: (_, gestureState) => {
170
+ // Update dragY based on the gesture
171
+ if (gestureState.dy > 0) {
172
+ dragY.setValue(gestureState.dy);
173
+ }
174
+ },
175
+ onPanResponderRelease: (_, gestureState) => {
176
+ if (gestureState.dy > 50) {
177
+ closeDrawer();
178
+ } else {
179
+ // Only snap back if not closing
180
+ Animated.timing(dragY, {
181
+ toValue: 0,
182
+ duration: 300,
183
+ easing: Easing.out(Easing.quad),
184
+ useNativeDriver: true,
185
+ }).start();
186
+ }
187
+ },
188
+ }),
189
+ [closeDrawer, dragY]
190
+ );
136
191
 
137
192
  useImperativeHandle(ref, () => ({
138
193
  setMobileMenuOpen: (open: boolean) => {
139
194
  if (open) {
140
- dragY.setValue(0);
141
- scrollToSelectedNode();
142
- setMobileMenuOpen(true);
195
+ openDrawer();
143
196
  } else {
144
- Keyboard.dismiss();
145
- setMobileMenuOpen(false);
197
+ closeDrawer();
146
198
  }
147
199
  },
148
200
  }));
@@ -154,68 +206,70 @@ export const MobileMenuDrawer = memo(
154
206
  height: 5,
155
207
  backgroundColor: theme.color.mediumdark,
156
208
  borderRadius: 2.5,
157
- alignSelf: 'center' as const, // TypeScript needs this to recognize 'center' as a valid FlexAlignType
158
- marginVertical: 8,
209
+ alignSelf: 'center' as const,
159
210
  }),
160
211
  [theme.color.mediumdark]
161
212
  );
162
213
 
214
+ const drawerContainerStyle = useMemo(
215
+ () =>
216
+ ({
217
+ flex: 1,
218
+ borderTopColor: theme.appBorderColor,
219
+ borderTopWidth: 1,
220
+ borderStyle: 'solid' as const,
221
+ backgroundColor: theme.background.content,
222
+ elevation: 8,
223
+ boxShadow: `0 16px 32px 0 ${theme.color.border}`,
224
+ }) satisfies ViewStyle,
225
+ [theme.appBorderColor, theme.background.content, theme.color.border]
226
+ );
227
+
228
+ const dragHandleWrapperStyle = useMemo(
229
+ () => ({
230
+ alignItems: 'center' as const,
231
+ justifyContent: 'center' as const,
232
+ paddingBottom: 16,
233
+ paddingTop: 10,
234
+ backgroundColor: theme.background.content,
235
+ }),
236
+ [theme.background.content]
237
+ );
238
+
239
+ const childrenWrapperStyle = useMemo(
240
+ () => ({
241
+ flex: 1,
242
+ backgroundColor: theme.background.content,
243
+ }),
244
+ [theme.background.content]
245
+ );
246
+
163
247
  return (
164
- <Modal
165
- visible={mobileMenuOpen}
166
- animationType="slide"
167
- transparent
168
- statusBarTranslucent
169
- onRequestClose={() => setMobileMenuOpen(false)}
170
- >
171
- <Animated.View style={{ flex: 1 }}>
172
- <View style={{ flex: 1 }}>
173
- <Pressable style={{ flex: 1 }} onPress={() => setMobileMenuOpen(false)}></Pressable>
248
+ <Portal hostName="storybook-lite-ui-root">
249
+ <Animated.View
250
+ style={[portalContainerStyle, { transform: [{ translateY: slideAnim }] }]}
251
+ pointerEvents={isVisible ? 'auto' : 'none'}
252
+ >
253
+ <View style={flexStyle}>
254
+ <Pressable style={flexStyle} onPress={closeDrawer} />
174
255
  </View>
175
256
 
176
- <Animated.View style={{ height: animatedHeight }}>
177
- <Animated.View
178
- style={[
179
- {
180
- flex: 1,
181
- borderTopColor: theme.appBorderColor,
182
- borderTopWidth: 1,
183
- borderStyle: 'solid',
184
- backgroundColor: theme.background.content,
185
- elevation: 8,
186
- },
187
- { transform: [{ translateY: dragY }] },
188
- ]}
189
- >
257
+ <Animated.View
258
+ style={{
259
+ height: animatedHeight,
260
+ }}
261
+ >
262
+ <Animated.View style={[drawerContainerStyle, { transform: [{ translateY: dragY }] }]}>
190
263
  {/* Drag handle */}
191
- <View
192
- {...panResponder.panHandlers}
193
- style={{
194
- alignItems: 'center',
195
- justifyContent: 'center',
196
- backgroundColor: theme.background.content,
197
- }}
198
- >
264
+ <View {...panResponder.panHandlers} style={dragHandleWrapperStyle}>
199
265
  <View style={handleStyle} />
200
266
  </View>
201
267
 
202
- <ScrollView
203
- ref={scrollRef}
204
- keyboardShouldPersistTaps="handled"
205
- style={{
206
- flex: 1,
207
- paddingBottom: 150,
208
- alignSelf: 'flex-end',
209
- width: '100%',
210
- backgroundColor: theme.background.content,
211
- }}
212
- >
213
- {children}
214
- </ScrollView>
268
+ <View style={childrenWrapperStyle}>{children}</View>
215
269
  </Animated.View>
216
270
  </Animated.View>
217
271
  </Animated.View>
218
- </Modal>
272
+ </Portal>
219
273
  );
220
274
  })
221
275
  );
package/src/Refs.tsx CHANGED
@@ -14,7 +14,7 @@ export interface RefProps {
14
14
  }
15
15
 
16
16
  const Wrapper = styled.View<{ isMain: boolean }>(() => ({
17
- position: 'relative',
17
+ flex: 1,
18
18
  }));
19
19
 
20
20
  export const Ref: FC<RefType & RefProps & { status?: State['status'] }> = React.memo(