@storybook/react-native-ui-common 9.0.0-beta.15
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 +182 -0
- package/dist/index.js +812 -0
- package/package.json +81 -0
- package/src/Button.stories.tsx +133 -0
- package/src/Button.tsx +171 -0
- package/src/IconButton.tsx +10 -0
- package/src/LayoutProvider.tsx +32 -0
- package/src/StorageProvider.tsx +21 -0
- package/src/assets/react-native-logo.png +0 -0
- package/src/constants.ts +4 -0
- package/src/hooks/index.ts +3 -0
- package/src/hooks/useExpanded.ts +64 -0
- package/src/hooks/useLastViewed.ts +35 -0
- package/src/hooks/useStoreState.ts +27 -0
- package/src/index.tsx +7 -0
- package/src/types.ts +66 -0
- package/src/util/StoryHash.ts +365 -0
- package/src/util/index.ts +3 -0
- package/src/util/tree.ts +93 -0
- package/src/util/useStyle.ts +28 -0
package/package.json
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@storybook/react-native-ui-common",
|
|
3
|
+
"version": "9.0.0-beta.15",
|
|
4
|
+
"description": "common 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-common"
|
|
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
|
+
"tsup": "^7.2.0",
|
|
57
|
+
"typescript": "~5.8.3"
|
|
58
|
+
},
|
|
59
|
+
"dependencies": {
|
|
60
|
+
"@storybook/react": "9.0.0-beta.10",
|
|
61
|
+
"@storybook/react-native-theming": "^9.0.0-beta.15",
|
|
62
|
+
"es-toolkit": "^1.38.0",
|
|
63
|
+
"fuse.js": "^7.0.0",
|
|
64
|
+
"memoizerific": "^1.11.3",
|
|
65
|
+
"polished": "^4.3.1",
|
|
66
|
+
"store2": "^2.14.3",
|
|
67
|
+
"ts-dedent": "^2.2.0"
|
|
68
|
+
},
|
|
69
|
+
"peerDependencies": {
|
|
70
|
+
"react": "*",
|
|
71
|
+
"react-native": ">=0.57.0",
|
|
72
|
+
"storybook": "9.0.0-beta.10"
|
|
73
|
+
},
|
|
74
|
+
"engines": {
|
|
75
|
+
"node": ">=18.0.0"
|
|
76
|
+
},
|
|
77
|
+
"publishConfig": {
|
|
78
|
+
"access": "public"
|
|
79
|
+
},
|
|
80
|
+
"gitHead": "905adcca4ea272933c7f0fa16f10dda201da9a5f"
|
|
81
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
2
|
+
import type { ReactNode } from 'react';
|
|
3
|
+
|
|
4
|
+
import { Image, View } from 'react-native';
|
|
5
|
+
|
|
6
|
+
import { Button } from './Button';
|
|
7
|
+
|
|
8
|
+
const meta = {
|
|
9
|
+
title: 'UI/Button',
|
|
10
|
+
component: Button,
|
|
11
|
+
args: {},
|
|
12
|
+
} satisfies Meta<typeof Button>;
|
|
13
|
+
|
|
14
|
+
export default meta;
|
|
15
|
+
type Story = StoryObj<typeof meta>;
|
|
16
|
+
|
|
17
|
+
const Stack = ({ children }: { children: ReactNode }) => (
|
|
18
|
+
<View style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>{children}</View>
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
const Row = ({ children }: { children: ReactNode }) => (
|
|
22
|
+
<View style={{ display: 'flex', flexDirection: 'row', alignItems: 'center', gap: 16 }}>
|
|
23
|
+
{children}
|
|
24
|
+
</View>
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
export const Base: Story = {};
|
|
28
|
+
const Icon = () => <Image source={require('./assets/react-native-logo.png')} />;
|
|
29
|
+
export const Variants: Story = {
|
|
30
|
+
render: (args) => (
|
|
31
|
+
<Stack>
|
|
32
|
+
<Row>
|
|
33
|
+
<Button variant="solid" text="Solid" {...args} />
|
|
34
|
+
<Button variant="outline" text="Outline" {...args} />
|
|
35
|
+
<Button variant="ghost" text="Ghost" {...args} />
|
|
36
|
+
</Row>
|
|
37
|
+
<Row>
|
|
38
|
+
<Button variant="solid" {...args} Icon={Icon} text="Solid" />
|
|
39
|
+
<Button variant="outline" Icon={Icon} text="Outline" {...args} />
|
|
40
|
+
<Button variant="ghost" Icon={Icon} text="Ghost" {...args} />
|
|
41
|
+
</Row>
|
|
42
|
+
<Row>
|
|
43
|
+
<Button variant="solid" padding="small" Icon={Icon} {...args} />
|
|
44
|
+
<Button variant="outline" padding="small" Icon={Icon} {...args} />
|
|
45
|
+
<Button variant="ghost" padding="small" Icon={Icon} {...args} />
|
|
46
|
+
</Row>
|
|
47
|
+
</Stack>
|
|
48
|
+
),
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export const Active: Story = {
|
|
52
|
+
args: {
|
|
53
|
+
active: true,
|
|
54
|
+
text: 'Button',
|
|
55
|
+
Icon,
|
|
56
|
+
},
|
|
57
|
+
render: (args) => (
|
|
58
|
+
<Row>
|
|
59
|
+
<Button variant="solid" {...args} />
|
|
60
|
+
<Button variant="outline" {...args} />
|
|
61
|
+
<Button variant="ghost" {...args} />
|
|
62
|
+
</Row>
|
|
63
|
+
),
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export const WithIcon: Story = {
|
|
67
|
+
args: {
|
|
68
|
+
Icon,
|
|
69
|
+
text: 'Button',
|
|
70
|
+
},
|
|
71
|
+
render: (args) => (
|
|
72
|
+
<Row>
|
|
73
|
+
<Button variant="solid" {...args} />
|
|
74
|
+
<Button variant="outline" {...args} />
|
|
75
|
+
<Button variant="ghost" {...args} />
|
|
76
|
+
</Row>
|
|
77
|
+
),
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export const IconOnly: Story = {
|
|
81
|
+
args: {
|
|
82
|
+
padding: 'small',
|
|
83
|
+
Icon,
|
|
84
|
+
},
|
|
85
|
+
render: (args) => (
|
|
86
|
+
<Row>
|
|
87
|
+
<Button variant="solid" {...args} />
|
|
88
|
+
<Button variant="outline" {...args} />
|
|
89
|
+
<Button variant="ghost" {...args} />
|
|
90
|
+
</Row>
|
|
91
|
+
),
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
export const Sizes: Story = {
|
|
95
|
+
render: () => (
|
|
96
|
+
<Row>
|
|
97
|
+
<Button size="small" text="Small Button" />
|
|
98
|
+
<Button size="medium" text="Medium Button" />
|
|
99
|
+
</Row>
|
|
100
|
+
),
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
export const Disabled: Story = {
|
|
104
|
+
args: {
|
|
105
|
+
disabled: true,
|
|
106
|
+
text: 'Disabled Button',
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
export const Animated: Story = {
|
|
111
|
+
args: {
|
|
112
|
+
variant: 'outline',
|
|
113
|
+
},
|
|
114
|
+
render: (args) => (
|
|
115
|
+
<Stack>
|
|
116
|
+
<Row>
|
|
117
|
+
<Button animation="glow" text="Button" {...args} />
|
|
118
|
+
<Button animation="jiggle" text="Button" {...args} />
|
|
119
|
+
<Button animation="rotate360" text="Button" {...args} />
|
|
120
|
+
</Row>
|
|
121
|
+
<Row>
|
|
122
|
+
<Button animation="glow" text="Button" Icon={Icon} {...args} />
|
|
123
|
+
<Button animation="jiggle" text="Button" Icon={Icon} {...args} />
|
|
124
|
+
<Button animation="rotate360" Icon={Icon} text="Button" {...args} />
|
|
125
|
+
</Row>
|
|
126
|
+
<Row>
|
|
127
|
+
<Button animation="glow" padding="small" Icon={Icon} {...args} />
|
|
128
|
+
<Button animation="jiggle" padding="small" Icon={Icon} {...args} />
|
|
129
|
+
<Button animation="rotate360" padding="small" Icon={Icon} {...args} />
|
|
130
|
+
</Row>
|
|
131
|
+
</Stack>
|
|
132
|
+
),
|
|
133
|
+
};
|
package/src/Button.tsx
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
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
|
+
|
|
5
|
+
export interface ButtonProps extends TouchableOpacityProps {
|
|
6
|
+
asChild?: boolean;
|
|
7
|
+
size?: 'small' | 'medium';
|
|
8
|
+
padding?: 'small' | 'medium';
|
|
9
|
+
variant?: 'outline' | 'solid' | 'ghost';
|
|
10
|
+
disabled?: boolean;
|
|
11
|
+
active?: boolean;
|
|
12
|
+
animation?: 'none' | 'rotate360' | 'glow' | 'jiggle';
|
|
13
|
+
text?: string;
|
|
14
|
+
Icon?: (props: { color: string }) => ReactElement;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// TODO fix this type
|
|
18
|
+
export const Button = forwardRef<any, ButtonProps>(
|
|
19
|
+
(
|
|
20
|
+
{
|
|
21
|
+
Icon,
|
|
22
|
+
animation = 'none',
|
|
23
|
+
size = 'small',
|
|
24
|
+
variant = 'outline',
|
|
25
|
+
padding = 'medium',
|
|
26
|
+
disabled = false,
|
|
27
|
+
active = false,
|
|
28
|
+
onPress,
|
|
29
|
+
children,
|
|
30
|
+
text,
|
|
31
|
+
...props
|
|
32
|
+
}: ButtonProps,
|
|
33
|
+
ref
|
|
34
|
+
) => {
|
|
35
|
+
// let Comp: 'button' | 'a' | typeof Slot = 'button';
|
|
36
|
+
// if (props.isLink) Comp = 'a';
|
|
37
|
+
// if (asChild) Comp = Slot;
|
|
38
|
+
// let localVariant = variant;
|
|
39
|
+
// let localSize = size;
|
|
40
|
+
|
|
41
|
+
const [isAnimating, setIsAnimating] = useState(false);
|
|
42
|
+
|
|
43
|
+
const handleClick = (event) => {
|
|
44
|
+
if (onPress) onPress(event);
|
|
45
|
+
if (animation === 'none') return;
|
|
46
|
+
setIsAnimating(true);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
const timer = setTimeout(() => {
|
|
51
|
+
if (isAnimating) setIsAnimating(false);
|
|
52
|
+
}, 1000);
|
|
53
|
+
return () => clearTimeout(timer);
|
|
54
|
+
}, [isAnimating]);
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<StyledButton
|
|
58
|
+
ref={ref}
|
|
59
|
+
variant={variant}
|
|
60
|
+
size={size}
|
|
61
|
+
padding={padding}
|
|
62
|
+
disabled={disabled}
|
|
63
|
+
active={active}
|
|
64
|
+
animating={isAnimating}
|
|
65
|
+
animation={animation}
|
|
66
|
+
onPress={handleClick}
|
|
67
|
+
{...props}
|
|
68
|
+
>
|
|
69
|
+
{Icon && <ButtonIcon Icon={Icon} variant={variant} active={active} />}
|
|
70
|
+
{text && (
|
|
71
|
+
<ButtonText variant={variant} active={active}>
|
|
72
|
+
{text}
|
|
73
|
+
</ButtonText>
|
|
74
|
+
)}
|
|
75
|
+
{children}
|
|
76
|
+
</StyledButton>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
Button.displayName = 'Button';
|
|
82
|
+
|
|
83
|
+
const StyledButton = styled.TouchableOpacity<
|
|
84
|
+
ButtonProps & { animating: boolean; animation: ButtonProps['animation'] }
|
|
85
|
+
>(({ theme, variant, size, disabled, active, padding }) => ({
|
|
86
|
+
border: 0,
|
|
87
|
+
// cursor: disabled ? 'not-allowed' : 'pointer',
|
|
88
|
+
display: 'flex',
|
|
89
|
+
flexDirection: 'row',
|
|
90
|
+
gap: 6,
|
|
91
|
+
alignItems: 'center',
|
|
92
|
+
justifyContent: 'center',
|
|
93
|
+
overflow: 'hidden',
|
|
94
|
+
paddingHorizontal: (() => {
|
|
95
|
+
if (padding === 'small' && size === 'small') return 7;
|
|
96
|
+
if (padding === 'small' && size === 'medium') return 9;
|
|
97
|
+
if (size === 'small') return 10;
|
|
98
|
+
if (size === 'medium') return 12;
|
|
99
|
+
return 0;
|
|
100
|
+
})(),
|
|
101
|
+
paddingVertical: 0,
|
|
102
|
+
height: size === 'small' ? 28 : 32,
|
|
103
|
+
position: 'relative',
|
|
104
|
+
transitionProperty: 'background, box-shadow',
|
|
105
|
+
transitionDuration: '150ms',
|
|
106
|
+
transitionTimingFunction: 'ease-out',
|
|
107
|
+
whiteSpace: 'nowrap',
|
|
108
|
+
userSelect: 'none',
|
|
109
|
+
opacity: disabled ? 0.5 : 1,
|
|
110
|
+
margin: 0,
|
|
111
|
+
|
|
112
|
+
backgroundColor: (() => {
|
|
113
|
+
if (variant === 'solid') return theme.color.secondary;
|
|
114
|
+
if (variant === 'outline') return theme.button.background;
|
|
115
|
+
if (variant === 'ghost' && active) return theme.background.hoverable;
|
|
116
|
+
|
|
117
|
+
return 'transparent';
|
|
118
|
+
})(),
|
|
119
|
+
|
|
120
|
+
boxShadow: variant === 'outline' ? `${theme.button.border} 0 0 0 1px inset` : 'none',
|
|
121
|
+
borderRadius: theme.input.borderRadius,
|
|
122
|
+
// Making sure that the button never shrinks below its minimum size
|
|
123
|
+
flexShrink: 0,
|
|
124
|
+
}));
|
|
125
|
+
|
|
126
|
+
export const ButtonText = styled.Text<{
|
|
127
|
+
variant: ButtonProps['variant'];
|
|
128
|
+
active: ButtonProps['active'];
|
|
129
|
+
}>(({ theme, variant, active }) => ({
|
|
130
|
+
color: (() => {
|
|
131
|
+
if (variant === 'solid') return theme.color.lightest;
|
|
132
|
+
if (variant === 'outline') return theme.input.color;
|
|
133
|
+
if (variant === 'ghost' && active) return theme.color.secondary;
|
|
134
|
+
if (variant === 'ghost') return theme.color.mediumdark;
|
|
135
|
+
return theme.input.color;
|
|
136
|
+
})(),
|
|
137
|
+
flexDirection: 'row',
|
|
138
|
+
gap: 6,
|
|
139
|
+
textAlign: 'center',
|
|
140
|
+
fontSize: theme.typography.size.s1,
|
|
141
|
+
fontWeight: theme.typography.weight.bold,
|
|
142
|
+
}));
|
|
143
|
+
|
|
144
|
+
export const ButtonIcon = ({
|
|
145
|
+
Icon,
|
|
146
|
+
active,
|
|
147
|
+
variant,
|
|
148
|
+
}: {
|
|
149
|
+
Icon: (props: { color: string }) => ReactElement;
|
|
150
|
+
variant: ButtonProps['variant'];
|
|
151
|
+
active: ButtonProps['active'];
|
|
152
|
+
}) => {
|
|
153
|
+
const theme = useTheme();
|
|
154
|
+
|
|
155
|
+
const color = useMemo(() => {
|
|
156
|
+
if (variant === 'solid') return theme.color.lightest;
|
|
157
|
+
if (variant === 'outline') return theme.input.color;
|
|
158
|
+
if (variant === 'ghost' && active) return theme.color.secondary;
|
|
159
|
+
if (variant === 'ghost') return theme.color.mediumdark;
|
|
160
|
+
return theme.input.color;
|
|
161
|
+
}, [
|
|
162
|
+
active,
|
|
163
|
+
theme.color.lightest,
|
|
164
|
+
theme.color.mediumdark,
|
|
165
|
+
theme.color.secondary,
|
|
166
|
+
theme.input.color,
|
|
167
|
+
variant,
|
|
168
|
+
]);
|
|
169
|
+
|
|
170
|
+
return <Icon color={color} />;
|
|
171
|
+
};
|
|
@@ -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,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,21 @@
|
|
|
1
|
+
import type { FC, PropsWithChildren } from 'react';
|
|
2
|
+
import { createContext, useContext } from 'react';
|
|
3
|
+
|
|
4
|
+
export interface Storage {
|
|
5
|
+
getItem: (key: string) => Promise<string | null>;
|
|
6
|
+
setItem: (key: string, value: string) => Promise<void>;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const StorageContext = createContext<Storage>({
|
|
10
|
+
getItem: async () => null,
|
|
11
|
+
setItem: async () => {},
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
export const StorageProvider: FC<PropsWithChildren<{ storage: Storage }>> = ({
|
|
15
|
+
storage,
|
|
16
|
+
children,
|
|
17
|
+
}) => {
|
|
18
|
+
return <StorageContext.Provider value={storage}>{children}</StorageContext.Provider>;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const useStorage = () => useContext(StorageContext);
|
|
Binary file
|
package/src/constants.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { StoriesHash } from 'storybook/internal/manager-api';
|
|
2
|
+
import type { Dispatch } from 'react';
|
|
3
|
+
import { useCallback, useEffect, useReducer } from 'react';
|
|
4
|
+
import { getAncestorIds } from '../util/tree';
|
|
5
|
+
|
|
6
|
+
export type ExpandedState = Record<string, boolean>;
|
|
7
|
+
|
|
8
|
+
export interface ExpandAction {
|
|
9
|
+
ids: string[];
|
|
10
|
+
value: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface ExpandedProps {
|
|
14
|
+
refId: string;
|
|
15
|
+
data: StoriesHash;
|
|
16
|
+
initialExpanded?: ExpandedState;
|
|
17
|
+
rootIds: string[];
|
|
18
|
+
selectedStoryId: string | null;
|
|
19
|
+
onSelectStoryId: (storyId: string) => void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const initializeExpanded = ({
|
|
23
|
+
initialExpanded,
|
|
24
|
+
rootIds,
|
|
25
|
+
}: {
|
|
26
|
+
refId: string;
|
|
27
|
+
data: StoriesHash;
|
|
28
|
+
initialExpanded?: ExpandedState;
|
|
29
|
+
rootIds: string[];
|
|
30
|
+
}) => {
|
|
31
|
+
const highlightedAncestors = [];
|
|
32
|
+
return [...rootIds, ...highlightedAncestors].reduce<ExpandedState>(
|
|
33
|
+
(acc, id) => Object.assign(acc, { [id]: id in initialExpanded ? initialExpanded[id] : true }),
|
|
34
|
+
{}
|
|
35
|
+
);
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export const useExpanded = ({
|
|
39
|
+
refId,
|
|
40
|
+
data,
|
|
41
|
+
initialExpanded,
|
|
42
|
+
rootIds,
|
|
43
|
+
selectedStoryId,
|
|
44
|
+
}: ExpandedProps): [ExpandedState, Dispatch<ExpandAction>] => {
|
|
45
|
+
// Track the set of currently expanded nodes within this tree.
|
|
46
|
+
// Root nodes are expanded by default.
|
|
47
|
+
const [expanded, setExpanded] = useReducer(
|
|
48
|
+
(state: ExpandedState, { ids, value }: ExpandAction) =>
|
|
49
|
+
ids.reduce((acc, id) => Object.assign(acc, { [id]: value }), { ...state }),
|
|
50
|
+
{ refId, data, rootIds, initialExpanded },
|
|
51
|
+
initializeExpanded
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
const updateExpanded = useCallback(({ ids, value }: ExpandAction) => {
|
|
55
|
+
setExpanded({ ids, value });
|
|
56
|
+
}, []);
|
|
57
|
+
|
|
58
|
+
// Expand the whole ancestry of the currently selected story whenever it changes.
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
setExpanded({ ids: getAncestorIds(data, selectedStoryId), value: true });
|
|
61
|
+
}, [data, selectedStoryId]);
|
|
62
|
+
|
|
63
|
+
return [expanded, updateExpanded];
|
|
64
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef } from 'react';
|
|
2
|
+
|
|
3
|
+
import type { Selection, StoryRef } from '../types';
|
|
4
|
+
|
|
5
|
+
export const useLastViewed = (selection: Selection) => {
|
|
6
|
+
const lastViewedRef = useRef([]);
|
|
7
|
+
|
|
8
|
+
const updateLastViewed = useCallback(
|
|
9
|
+
(story: StoryRef) => {
|
|
10
|
+
const items = lastViewedRef.current;
|
|
11
|
+
const index = items.findIndex(
|
|
12
|
+
({ storyId, refId }) => storyId === story.storyId && refId === story.refId
|
|
13
|
+
);
|
|
14
|
+
if (index === 0) return;
|
|
15
|
+
if (index === -1) {
|
|
16
|
+
lastViewedRef.current = [story, ...items];
|
|
17
|
+
} else {
|
|
18
|
+
lastViewedRef.current = [story, ...items.slice(0, index), ...items.slice(index + 1)];
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
[lastViewedRef]
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
if (selection) updateLastViewed(selection);
|
|
26
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
27
|
+
}, [selection]);
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
getLastViewed: useCallback(() => lastViewedRef.current, [lastViewedRef]),
|
|
31
|
+
clearLastViewed: useCallback(() => {
|
|
32
|
+
lastViewedRef.current = lastViewedRef.current.slice(0, 1);
|
|
33
|
+
}, [lastViewedRef]),
|
|
34
|
+
};
|
|
35
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
import { useStorage } from '../StorageProvider';
|
|
3
|
+
|
|
4
|
+
export const useStoreBooleanState = (
|
|
5
|
+
key: string,
|
|
6
|
+
defaultValue: boolean
|
|
7
|
+
): ReturnType<typeof useState<boolean>> => {
|
|
8
|
+
const storage = useStorage();
|
|
9
|
+
|
|
10
|
+
const [val, setVal] = useState<boolean>(defaultValue);
|
|
11
|
+
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
storage.getItem(key).then((newVal) => {
|
|
14
|
+
if (newVal === null || newVal === undefined) {
|
|
15
|
+
setVal(defaultValue);
|
|
16
|
+
} else {
|
|
17
|
+
setVal(newVal === 'true');
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
}, [key, storage, defaultValue]);
|
|
21
|
+
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
storage.setItem(key, val.toString());
|
|
24
|
+
}, [key, storage, val]);
|
|
25
|
+
|
|
26
|
+
return [val, setVal];
|
|
27
|
+
};
|
package/src/index.tsx
ADDED
package/src/types.ts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { StoriesHash, State } from 'storybook/internal/manager-api';
|
|
2
|
+
import type { StatusValue, StatusesByStoryIdAndTypeId } from 'storybook/internal/types';
|
|
3
|
+
import * as Fuse from 'fuse.js';
|
|
4
|
+
import { PressableProps } from 'react-native';
|
|
5
|
+
|
|
6
|
+
export type Refs = State['refs'];
|
|
7
|
+
export type RefType = Refs[keyof Refs] & { allStatuses?: StatusesByStoryIdAndTypeId };
|
|
8
|
+
export type Item = StoriesHash[keyof StoriesHash];
|
|
9
|
+
export type Dataset = Record<string, Item>;
|
|
10
|
+
|
|
11
|
+
export interface CombinedDataset {
|
|
12
|
+
hash: Refs;
|
|
13
|
+
entries: [string, RefType][];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface ItemRef {
|
|
17
|
+
itemId: string;
|
|
18
|
+
refId: string;
|
|
19
|
+
}
|
|
20
|
+
export interface StoryRef {
|
|
21
|
+
storyId: string;
|
|
22
|
+
refId: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type Highlight = ItemRef | null;
|
|
26
|
+
export type Selection = StoryRef | null;
|
|
27
|
+
|
|
28
|
+
export function isExpandType(x: any): x is ExpandType {
|
|
29
|
+
return !!(x && x.showAll);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface ExpandType {
|
|
33
|
+
showAll: () => void;
|
|
34
|
+
totalCount: number;
|
|
35
|
+
moreCount: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export type SearchItem = Item & {
|
|
39
|
+
refId: string;
|
|
40
|
+
path: string[];
|
|
41
|
+
status?: StatusValue;
|
|
42
|
+
showAll?: () => void;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export type SearchResult = Fuse.FuseResult<SearchItem>;
|
|
46
|
+
|
|
47
|
+
export type SearchResultProps = SearchResult & {
|
|
48
|
+
icon: string;
|
|
49
|
+
isHighlighted: boolean;
|
|
50
|
+
onPress: PressableProps['onPress'];
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export type GetSearchItemProps = (args: {
|
|
54
|
+
item: SearchResult;
|
|
55
|
+
index: number;
|
|
56
|
+
key: string;
|
|
57
|
+
}) => SearchResultProps;
|
|
58
|
+
|
|
59
|
+
export type SearchChildrenFn = (args: {
|
|
60
|
+
query: string;
|
|
61
|
+
results: SearchResult[]; // TODO fix this type
|
|
62
|
+
isBrowsing: boolean;
|
|
63
|
+
closeMenu: (cb?: () => void) => void;
|
|
64
|
+
getItemProps: GetSearchItemProps;
|
|
65
|
+
highlightedIndex: number | null;
|
|
66
|
+
}) => React.ReactNode;
|