@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.
Files changed (42) hide show
  1. package/LICENSE +21 -0
  2. package/dist/index.d.ts +94 -0
  3. package/dist/index.js +5437 -0
  4. package/package.json +80 -0
  5. package/src/Button.stories.tsx +134 -0
  6. package/src/Button.tsx +172 -0
  7. package/src/Explorer.stories.tsx +40 -0
  8. package/src/Explorer.tsx +38 -0
  9. package/src/IconButton.tsx +10 -0
  10. package/src/Layout.stories.tsx +70 -0
  11. package/src/Layout.tsx +310 -0
  12. package/src/LayoutProvider.tsx +32 -0
  13. package/src/MobileAddonsPanel.tsx +195 -0
  14. package/src/MobileMenuDrawer.tsx +90 -0
  15. package/src/Refs.tsx +82 -0
  16. package/src/Search.tsx +234 -0
  17. package/src/SearchResults.stories.tsx +102 -0
  18. package/src/SearchResults.tsx +254 -0
  19. package/src/SelectedNodeProvider.tsx +58 -0
  20. package/src/Sidebar.stories.tsx +188 -0
  21. package/src/Sidebar.tsx +131 -0
  22. package/src/StorageProvider.tsx +21 -0
  23. package/src/StorybookLogo.stories.tsx +76 -0
  24. package/src/StorybookLogo.tsx +108 -0
  25. package/src/Tree.stories.tsx +177 -0
  26. package/src/Tree.tsx +390 -0
  27. package/src/TreeNode.stories.tsx +117 -0
  28. package/src/TreeNode.tsx +154 -0
  29. package/src/assets/react-native-logo.png +0 -0
  30. package/src/constants.ts +4 -0
  31. package/src/hooks/useExpanded.ts +64 -0
  32. package/src/hooks/useLastViewed.ts +48 -0
  33. package/src/hooks/useStoreState.ts +27 -0
  34. package/src/icon/iconDataUris.tsx +365 -0
  35. package/src/index.tsx +11 -0
  36. package/src/mockdata.large.ts +25217 -0
  37. package/src/mockdata.ts +287 -0
  38. package/src/types.ts +66 -0
  39. package/src/util/StoryHash.ts +249 -0
  40. package/src/util/status.tsx +87 -0
  41. package/src/util/tree.ts +93 -0
  42. package/src/util/useStyle.ts +28 -0
package/package.json ADDED
@@ -0,0 +1,80 @@
1
+ {
2
+ "name": "@storybook/react-native-ui-lite",
3
+ "version": "9.0.0-beta.11",
4
+ "description": "lightweight ui components for react native storybook",
5
+ "keywords": [
6
+ "react",
7
+ "react-native",
8
+ "storybook"
9
+ ],
10
+ "homepage": "https://storybook.js.org/",
11
+ "bugs": {
12
+ "url": "https://github.com/storybookjs/react-native/issues"
13
+ },
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "https://github.com/storybookjs/react-native.git",
17
+ "directory": "packages/react-native-ui-lite"
18
+ },
19
+ "react-native": "src/index.tsx",
20
+ "main": "dist/index.js",
21
+ "types": "src/index.tsx",
22
+ "license": "MIT",
23
+ "files": [
24
+ "dist/**/*",
25
+ "README.md",
26
+ "*.js",
27
+ "*.d.ts",
28
+ "src/**/*"
29
+ ],
30
+ "scripts": {
31
+ "dev": "tsup --watch",
32
+ "prepare": "tsup",
33
+ "test": "jest --passWithNoTests",
34
+ "test:ci": "jest"
35
+ },
36
+ "jest": {
37
+ "modulePathIgnorePatterns": [
38
+ "dist/"
39
+ ],
40
+ "moduleFileExtensions": [
41
+ "ts",
42
+ "tsx",
43
+ "js",
44
+ "jsx",
45
+ "json",
46
+ "node"
47
+ ],
48
+ "preset": "react-native"
49
+ },
50
+ "devDependencies": {
51
+ "@types/jest": "^29.4.3",
52
+ "@types/react": "~19.0.10",
53
+ "babel-jest": "^29.7.0",
54
+ "jest": "^29.7.0",
55
+ "react-test-renderer": "^19.1.0",
56
+ "ts-dedent": "^2.2.0",
57
+ "tsup": "^7.2.0",
58
+ "typescript": "~5.8.3"
59
+ },
60
+ "dependencies": {
61
+ "@storybook/react": "9.0.0-beta.10",
62
+ "@storybook/react-native-theming": "^9.0.0-beta.11",
63
+ "fuse.js": "^7.0.0",
64
+ "memoizerific": "^1.11.3",
65
+ "polished": "^4.3.1",
66
+ "store2": "^2.14.3"
67
+ },
68
+ "peerDependencies": {
69
+ "react": "*",
70
+ "react-native": ">=0.57.0",
71
+ "storybook": "9.0.0-beta.10"
72
+ },
73
+ "engines": {
74
+ "node": ">=18.0.0"
75
+ },
76
+ "publishConfig": {
77
+ "access": "public"
78
+ },
79
+ "gitHead": "531ed8a758adc98bb436d0ccb32efcb369572ffe"
80
+ }
@@ -0,0 +1,134 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import type { ReactNode } from 'react';
3
+
4
+ import { View } from 'react-native';
5
+
6
+ import { Button } from './Button';
7
+ import { FaceHappyIcon } from './icon/iconDataUris';
8
+
9
+ const meta = {
10
+ title: 'UI/Button',
11
+ component: Button,
12
+ args: {},
13
+ } satisfies Meta<typeof Button>;
14
+
15
+ export default meta;
16
+ type Story = StoryObj<typeof meta>;
17
+
18
+ const Stack = ({ children }: { children: ReactNode }) => (
19
+ <View style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>{children}</View>
20
+ );
21
+
22
+ const Row = ({ children }: { children: ReactNode }) => (
23
+ <View style={{ display: 'flex', flexDirection: 'row', alignItems: 'center', gap: 16 }}>
24
+ {children}
25
+ </View>
26
+ );
27
+
28
+ export const Base: Story = {};
29
+
30
+ export const Variants: Story = {
31
+ render: (args) => (
32
+ <Stack>
33
+ <Row>
34
+ <Button variant="solid" text="Solid" {...args} />
35
+ <Button variant="outline" text="Outline" {...args} />
36
+ <Button variant="ghost" text="Ghost" {...args} />
37
+ </Row>
38
+ <Row>
39
+ <Button variant="solid" {...args} Icon={FaceHappyIcon} text="Solid" />
40
+ <Button variant="outline" Icon={FaceHappyIcon} text="Outline" {...args} />
41
+ <Button variant="ghost" Icon={FaceHappyIcon} text="Ghost" {...args} />
42
+ </Row>
43
+ <Row>
44
+ <Button variant="solid" padding="small" Icon={FaceHappyIcon} {...args} />
45
+ <Button variant="outline" padding="small" Icon={FaceHappyIcon} {...args} />
46
+ <Button variant="ghost" padding="small" Icon={FaceHappyIcon} {...args} />
47
+ </Row>
48
+ </Stack>
49
+ ),
50
+ };
51
+
52
+ export const Active: Story = {
53
+ args: {
54
+ active: true,
55
+ text: 'Button',
56
+ Icon: FaceHappyIcon,
57
+ },
58
+ render: (args) => (
59
+ <Row>
60
+ <Button variant="solid" {...args} />
61
+ <Button variant="outline" {...args} />
62
+ <Button variant="ghost" {...args} />
63
+ </Row>
64
+ ),
65
+ };
66
+
67
+ export const WithIcon: Story = {
68
+ args: {
69
+ Icon: FaceHappyIcon,
70
+ text: 'Button',
71
+ },
72
+ render: (args) => (
73
+ <Row>
74
+ <Button variant="solid" {...args} />
75
+ <Button variant="outline" {...args} />
76
+ <Button variant="ghost" {...args} />
77
+ </Row>
78
+ ),
79
+ };
80
+
81
+ export const IconOnly: Story = {
82
+ args: {
83
+ padding: 'small',
84
+ Icon: FaceHappyIcon,
85
+ },
86
+ render: (args) => (
87
+ <Row>
88
+ <Button variant="solid" {...args} />
89
+ <Button variant="outline" {...args} />
90
+ <Button variant="ghost" {...args} />
91
+ </Row>
92
+ ),
93
+ };
94
+
95
+ export const Sizes: Story = {
96
+ render: () => (
97
+ <Row>
98
+ <Button size="small" text="Small Button" />
99
+ <Button size="medium" text="Medium Button" />
100
+ </Row>
101
+ ),
102
+ };
103
+
104
+ export const Disabled: Story = {
105
+ args: {
106
+ disabled: true,
107
+ text: 'Disabled Button',
108
+ },
109
+ };
110
+
111
+ export const Animated: Story = {
112
+ args: {
113
+ variant: 'outline',
114
+ },
115
+ render: (args) => (
116
+ <Stack>
117
+ <Row>
118
+ <Button animation="glow" text="Button" {...args} />
119
+ <Button animation="jiggle" text="Button" {...args} />
120
+ <Button animation="rotate360" text="Button" {...args} />
121
+ </Row>
122
+ <Row>
123
+ <Button animation="glow" text="Button" Icon={FaceHappyIcon} {...args} />
124
+ <Button animation="jiggle" text="Button" Icon={FaceHappyIcon} {...args} />
125
+ <Button animation="rotate360" Icon={FaceHappyIcon} text="Button" {...args} />
126
+ </Row>
127
+ <Row>
128
+ <Button animation="glow" padding="small" Icon={FaceHappyIcon} {...args} />
129
+ <Button animation="jiggle" padding="small" Icon={FaceHappyIcon} {...args} />
130
+ <Button animation="rotate360" padding="small" Icon={FaceHappyIcon} {...args} />
131
+ </Row>
132
+ </Stack>
133
+ ),
134
+ };
package/src/Button.tsx ADDED
@@ -0,0 +1,172 @@
1
+ import { styled, useTheme } from '@storybook/react-native-theming';
2
+ import { ReactElement, forwardRef, useEffect, useMemo, useState } from 'react';
3
+ import type { TouchableOpacityProps } from 'react-native';
4
+ import { SvgProps } from 'react-native-svg';
5
+
6
+ export interface ButtonProps extends TouchableOpacityProps {
7
+ asChild?: boolean;
8
+ size?: 'small' | 'medium';
9
+ padding?: 'small' | 'medium';
10
+ variant?: 'outline' | 'solid' | 'ghost';
11
+ disabled?: boolean;
12
+ active?: boolean;
13
+ animation?: 'none' | 'rotate360' | 'glow' | 'jiggle';
14
+ text?: string;
15
+ Icon?: (props: SvgProps) => ReactElement;
16
+ }
17
+
18
+ // TODO fix this type
19
+ export const Button = forwardRef<any, ButtonProps>(
20
+ (
21
+ {
22
+ Icon,
23
+ animation = 'none',
24
+ size = 'small',
25
+ variant = 'outline',
26
+ padding = 'medium',
27
+ disabled = false,
28
+ active = false,
29
+ onPress,
30
+ children,
31
+ text,
32
+ ...props
33
+ }: ButtonProps,
34
+ ref
35
+ ) => {
36
+ // let Comp: 'button' | 'a' | typeof Slot = 'button';
37
+ // if (props.isLink) Comp = 'a';
38
+ // if (asChild) Comp = Slot;
39
+ // let localVariant = variant;
40
+ // let localSize = size;
41
+
42
+ const [isAnimating, setIsAnimating] = useState(false);
43
+
44
+ const handleClick = (event) => {
45
+ if (onPress) onPress(event);
46
+ if (animation === 'none') return;
47
+ setIsAnimating(true);
48
+ };
49
+
50
+ useEffect(() => {
51
+ const timer = setTimeout(() => {
52
+ if (isAnimating) setIsAnimating(false);
53
+ }, 1000);
54
+ return () => clearTimeout(timer);
55
+ }, [isAnimating]);
56
+
57
+ return (
58
+ <StyledButton
59
+ ref={ref}
60
+ variant={variant}
61
+ size={size}
62
+ padding={padding}
63
+ disabled={disabled}
64
+ active={active}
65
+ animating={isAnimating}
66
+ animation={animation}
67
+ onPress={handleClick}
68
+ {...props}
69
+ >
70
+ {Icon && <ButtonIcon Icon={Icon} variant={variant} active={active} />}
71
+ {text && (
72
+ <ButtonText variant={variant} active={active}>
73
+ {text}
74
+ </ButtonText>
75
+ )}
76
+ {children}
77
+ </StyledButton>
78
+ );
79
+ }
80
+ );
81
+
82
+ Button.displayName = 'Button';
83
+
84
+ const StyledButton = styled.TouchableOpacity<
85
+ ButtonProps & { animating: boolean; animation: ButtonProps['animation'] }
86
+ >(({ theme, variant, size, disabled, active, padding }) => ({
87
+ border: 0,
88
+ // cursor: disabled ? 'not-allowed' : 'pointer',
89
+ display: 'flex',
90
+ flexDirection: 'row',
91
+ gap: 6,
92
+ alignItems: 'center',
93
+ justifyContent: 'center',
94
+ overflow: 'hidden',
95
+ paddingHorizontal: (() => {
96
+ if (padding === 'small' && size === 'small') return 7;
97
+ if (padding === 'small' && size === 'medium') return 9;
98
+ if (size === 'small') return 10;
99
+ if (size === 'medium') return 12;
100
+ return 0;
101
+ })(),
102
+ paddingVertical: 0,
103
+ height: size === 'small' ? 28 : 32,
104
+ position: 'relative',
105
+ transitionProperty: 'background, box-shadow',
106
+ transitionDuration: '150ms',
107
+ transitionTimingFunction: 'ease-out',
108
+ whiteSpace: 'nowrap',
109
+ userSelect: 'none',
110
+ opacity: disabled ? 0.5 : 1,
111
+ margin: 0,
112
+
113
+ backgroundColor: (() => {
114
+ if (variant === 'solid') return theme.color.secondary;
115
+ if (variant === 'outline') return theme.button.background;
116
+ if (variant === 'ghost' && active) return theme.background.hoverable;
117
+
118
+ return 'transparent';
119
+ })(),
120
+
121
+ boxShadow: variant === 'outline' ? `${theme.button.border} 0 0 0 1px inset` : 'none',
122
+ borderRadius: theme.input.borderRadius,
123
+ // Making sure that the button never shrinks below its minimum size
124
+ flexShrink: 0,
125
+ }));
126
+
127
+ export const ButtonText = styled.Text<{
128
+ variant: ButtonProps['variant'];
129
+ active: ButtonProps['active'];
130
+ }>(({ theme, variant, active }) => ({
131
+ color: (() => {
132
+ if (variant === 'solid') return theme.color.lightest;
133
+ if (variant === 'outline') return theme.input.color;
134
+ if (variant === 'ghost' && active) return theme.color.secondary;
135
+ if (variant === 'ghost') return theme.color.mediumdark;
136
+ return theme.input.color;
137
+ })(),
138
+ flexDirection: 'row',
139
+ gap: 6,
140
+ textAlign: 'center',
141
+ fontSize: theme.typography.size.s1,
142
+ fontWeight: theme.typography.weight.bold,
143
+ }));
144
+
145
+ export const ButtonIcon = ({
146
+ Icon,
147
+ active,
148
+ variant,
149
+ }: {
150
+ Icon: (props: SvgProps) => ReactElement;
151
+ variant: ButtonProps['variant'];
152
+ active: ButtonProps['active'];
153
+ }) => {
154
+ const theme = useTheme();
155
+
156
+ const color = useMemo(() => {
157
+ if (variant === 'solid') return theme.color.lightest;
158
+ if (variant === 'outline') return theme.input.color;
159
+ if (variant === 'ghost' && active) return theme.color.secondary;
160
+ if (variant === 'ghost') return theme.color.mediumdark;
161
+ return theme.input.color;
162
+ }, [
163
+ active,
164
+ theme.color.lightest,
165
+ theme.color.mediumdark,
166
+ theme.color.secondary,
167
+ theme.input.color,
168
+ variant,
169
+ ]);
170
+
171
+ return <Icon color={color} />;
172
+ };
@@ -0,0 +1,40 @@
1
+ import { Explorer } from './Explorer';
2
+ import { mockDataset } from './mockdata';
3
+ import type { RefType } from './types';
4
+ import { View } from 'react-native';
5
+
6
+ export default {
7
+ component: Explorer,
8
+ title: 'UI/Sidebar/Explorer',
9
+ parameters: { layout: 'fullscreen', theme: 'side-by-side' },
10
+ decorators: [
11
+ (storyFn: any) => <View style={{ paddingHorizontal: 20 }}>{storyFn()}</View>,
12
+ (storyFn: any) => <View style={{ paddingHorizontal: 20 }}>{storyFn()}</View>,
13
+ ],
14
+ };
15
+
16
+ const selected = {
17
+ refId: 'storybook_internal',
18
+ storyId: 'root-1-child-a2--grandchild-a1-1',
19
+ };
20
+
21
+ const simple: Record<string, RefType> = {
22
+ storybook_internal: {
23
+ title: undefined,
24
+ id: 'storybook_internal',
25
+ url: 'iframe.html',
26
+ previewInitialized: true,
27
+ // @ts-expect-error (invalid input)
28
+ index: mockDataset.withRoot,
29
+ },
30
+ };
31
+
32
+ export const Simple = () => (
33
+ <Explorer
34
+ dataset={{ hash: simple, entries: Object.entries(simple) }}
35
+ selected={selected}
36
+ isLoading={false}
37
+ isBrowsing
38
+ setSelection={() => {}}
39
+ />
40
+ );
@@ -0,0 +1,38 @@
1
+ import type { FC } from 'react';
2
+ import React, { useRef } from 'react';
3
+ import { Ref } from './Refs';
4
+ import type { CombinedDataset, Selection } from './types';
5
+ import { View } from 'react-native';
6
+
7
+ export interface ExplorerProps {
8
+ isLoading: boolean;
9
+ isBrowsing: boolean;
10
+ dataset: CombinedDataset;
11
+ selected: Selection;
12
+ setSelection: (selection: Selection) => void;
13
+ }
14
+
15
+ export const Explorer: FC<ExplorerProps> = React.memo(function Explorer({
16
+ isLoading,
17
+ isBrowsing,
18
+ dataset,
19
+ selected,
20
+ setSelection,
21
+ }) {
22
+ const containerRef = useRef<View>(null);
23
+
24
+ return (
25
+ <View ref={containerRef} id="storybook-explorer-tree">
26
+ {dataset.entries.map(([refId, ref]) => (
27
+ <Ref
28
+ {...ref}
29
+ key={refId}
30
+ isLoading={isLoading}
31
+ isBrowsing={isBrowsing}
32
+ selectedStoryId={selected?.refId === ref.id ? selected.storyId : null}
33
+ setSelection={setSelection}
34
+ />
35
+ ))}
36
+ </View>
37
+ );
38
+ });
@@ -0,0 +1,10 @@
1
+ import { Button, ButtonProps } from './Button';
2
+ import { forwardRef } from 'react';
3
+
4
+ export const IconButton = forwardRef(
5
+ ({ padding = 'small', variant = 'ghost', ...props }: ButtonProps, ref) => {
6
+ return <Button padding={padding} variant={variant} ref={ref} {...props} />;
7
+ }
8
+ );
9
+
10
+ IconButton.displayName = 'IconButton';
@@ -0,0 +1,70 @@
1
+ // import type { Meta, StoryObj } from '@storybook/react';
2
+ // import { Layout } from './Layout';
3
+ // import { mockDataset } from './mockdata';
4
+ // import { Text, View } from 'react-native';
5
+ // import { LayoutProvider } from './LayoutProvider';
6
+
7
+ // const meta = {
8
+ // component: Layout,
9
+ // decorators: [
10
+ // (Story) => (
11
+ // <LayoutProvider>
12
+ // <Story />
13
+ // </LayoutProvider>
14
+ // ),
15
+ // ],
16
+ // } satisfies Meta<typeof Layout>;
17
+
18
+ // export default meta;
19
+
20
+ // type Story = StoryObj<typeof meta>;
21
+
22
+ // export const Basic: Story = {
23
+ // args: {
24
+ // children: <Text>Testing</Text>,
25
+ // //@ts-ignore
26
+ // storyHash: mockDataset.withRoot,
27
+ // storyId: 'emails-buildnotification--with-changes',
28
+ // },
29
+ // };
30
+
31
+ // export const OverflowSidebarExample: Story = {
32
+ // args: {
33
+ // children: (
34
+ // <View
35
+ // style={{
36
+ // width: '100%',
37
+ // backgroundColor: 'lightgreen',
38
+ // alignItems: 'flex-end',
39
+ // left: '-50%',
40
+ // }}
41
+ // >
42
+ // <Text style={{ width: '50%', textAlign: 'right' }}>
43
+ // This box should not overflow the side navigation in desktop mode
44
+ // </Text>
45
+ // </View>
46
+ // ),
47
+ // //@ts-ignore
48
+ // storyHash: mockDataset.withRoot,
49
+ // storyId: 'emails-buildnotification--with-changes',
50
+ // },
51
+ // };
52
+
53
+ // export const OverflowAddonsExample: Story = {
54
+ // args: {
55
+ // children: (
56
+ // <View
57
+ // style={{
58
+ // height: '100%',
59
+ // backgroundColor: 'lightgreen',
60
+ // bottom: '-50%',
61
+ // }}
62
+ // >
63
+ // <Text style={{ height: '50%' }}>This box should not overflow the addons panel</Text>
64
+ // </View>
65
+ // ),
66
+ // //@ts-ignore
67
+ // storyHash: mockDataset.withRoot,
68
+ // storyId: 'emails-buildnotification--with-changes',
69
+ // },
70
+ // };