@nativetail/ui 0.0.1
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/README.md +5 -0
- package/babel.config.js +3 -0
- package/package.json +50 -0
- package/src/components/actions-sheet/index.tsx +98 -0
- package/src/components/alert-dialog/index.tsx +58 -0
- package/src/components/blur/index.tsx +15 -0
- package/src/components/bottom-sheet/index.tsx +46 -0
- package/src/components/button/index.tsx +109 -0
- package/src/components/chip/index.tsx +74 -0
- package/src/components/counter/index.tsx +101 -0
- package/src/components/dialog/index.tsx +111 -0
- package/src/components/dropdown/index.tsx +252 -0
- package/src/components/index.ts +14 -0
- package/src/components/input/floating-input.tsx +74 -0
- package/src/components/input/index.ts +2 -0
- package/src/components/input/input.tsx +41 -0
- package/src/components/progress/index.tsx +28 -0
- package/src/components/select/index.tsx +141 -0
- package/src/components/stepper/index.tsx +31 -0
- package/src/components/switch/index.tsx +72 -0
- package/src/components/toast/index.tsx +101 -0
- package/src/index.ts +1 -0
- package/tailwind.config.js +25 -0
- package/tsconfig.json +18 -0
package/README.md
ADDED
package/babel.config.js
ADDED
package/package.json
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
{
|
2
|
+
"name": "@nativetail/ui",
|
3
|
+
"version": "0.0.1",
|
4
|
+
"description": "",
|
5
|
+
"main": "src/index.ts",
|
6
|
+
"scripts": {},
|
7
|
+
"author": "Imtiaj Bin Aoual <imtiajbinaoual@gmail.com>",
|
8
|
+
"license": "MIT",
|
9
|
+
"keywords": [
|
10
|
+
"react",
|
11
|
+
"react-native",
|
12
|
+
"ui",
|
13
|
+
"nativetail ui",
|
14
|
+
"tailwind ui",
|
15
|
+
"tailwindcss-react-native",
|
16
|
+
"twrnc",
|
17
|
+
"tailwind-react-native",
|
18
|
+
"tailwind-rn",
|
19
|
+
"@nativetail/ui"
|
20
|
+
],
|
21
|
+
"repository": {
|
22
|
+
"type": "git",
|
23
|
+
"url": "https://github.com/Imtiajrex/nativetail.git"
|
24
|
+
},
|
25
|
+
"peerDependencies": {
|
26
|
+
"react-native": ">=0.63.0"
|
27
|
+
},
|
28
|
+
"dependencies": {
|
29
|
+
"@gorhom/bottom-sheet": "^5.0.0-alpha.10",
|
30
|
+
"@shopify/flash-list": "^1.7.0",
|
31
|
+
"expo-blur": "^13.0.2",
|
32
|
+
"expo-linear-gradient": "~13.0.2",
|
33
|
+
"moti": "^0.29.0",
|
34
|
+
"@nativetail/core": "*",
|
35
|
+
"react": "18.2.0",
|
36
|
+
"react-hook-form": "^7.51.0",
|
37
|
+
"react-native": "0.74.3",
|
38
|
+
"react-native-actions-sheet": "^0.9.6",
|
39
|
+
"react-native-gesture-handler": "^2.17.1",
|
40
|
+
"react-native-reanimated": "~3.10.1",
|
41
|
+
"react-native-safe-area-context": "4.10.1",
|
42
|
+
"tailwind-merge": "^2.3.0",
|
43
|
+
"zustand": "^4.5.2"
|
44
|
+
},
|
45
|
+
"devDependencies": {
|
46
|
+
"metro-react-native-babel-preset": "^0.77.0",
|
47
|
+
"tailwindcss": "^3.4.4"
|
48
|
+
},
|
49
|
+
"private": false
|
50
|
+
}
|
@@ -0,0 +1,98 @@
|
|
1
|
+
import { cn, useTw, View } from "@nativetail/core";
|
2
|
+
import { forwardRef } from "react";
|
3
|
+
import { Blur } from "../blur";
|
4
|
+
import { Button } from "../button";
|
5
|
+
import { Dialog, DialogMethods } from "../dialog";
|
6
|
+
|
7
|
+
type OptionType = {
|
8
|
+
text: string;
|
9
|
+
onPress: () => void;
|
10
|
+
className?: string;
|
11
|
+
};
|
12
|
+
export type ActionSheetProps = {
|
13
|
+
containerClassName?: string;
|
14
|
+
contentClassName?: string;
|
15
|
+
onCancel: () => void;
|
16
|
+
useBlur?: boolean;
|
17
|
+
children?: React.ReactNode;
|
18
|
+
options?: OptionType[];
|
19
|
+
};
|
20
|
+
export type ActionSheetRef = DialogMethods;
|
21
|
+
export const ActionSheet = forwardRef<DialogMethods, ActionSheetProps>(
|
22
|
+
function ActionSheet(
|
23
|
+
{
|
24
|
+
containerClassName,
|
25
|
+
contentClassName,
|
26
|
+
useBlur = true,
|
27
|
+
onCancel,
|
28
|
+
children,
|
29
|
+
options,
|
30
|
+
},
|
31
|
+
ref
|
32
|
+
) {
|
33
|
+
const tw = useTw();
|
34
|
+
|
35
|
+
return (
|
36
|
+
<Dialog
|
37
|
+
containerClassName={cn("justify-end", containerClassName)}
|
38
|
+
contentClassName={cn(
|
39
|
+
"bg-transparent border-transparent gap-2 pb-4",
|
40
|
+
contentClassName
|
41
|
+
)}
|
42
|
+
useBlur={false}
|
43
|
+
ref={ref}
|
44
|
+
>
|
45
|
+
<View className="w-full rounded-xl bg-card/95">
|
46
|
+
{useBlur && (
|
47
|
+
<Blur
|
48
|
+
style={tw`absolute top-0 flex-1 left-0 right-0 bottom-0 rounded-xl bg-card`}
|
49
|
+
/>
|
50
|
+
)}
|
51
|
+
{options?.map((action, index) => (
|
52
|
+
<ActionSheetItem
|
53
|
+
key={index}
|
54
|
+
text={action.text}
|
55
|
+
onPress={action.onPress}
|
56
|
+
className={action.className}
|
57
|
+
last={index == options.length - 1}
|
58
|
+
/>
|
59
|
+
))}
|
60
|
+
{children}
|
61
|
+
</View>
|
62
|
+
<Button
|
63
|
+
variant="link"
|
64
|
+
className="w-full active:opacity-75 opacity-100 text-blue-500 bg-card rounded-xl"
|
65
|
+
onPress={onCancel}
|
66
|
+
>
|
67
|
+
Cancel
|
68
|
+
</Button>
|
69
|
+
</Dialog>
|
70
|
+
);
|
71
|
+
}
|
72
|
+
);
|
73
|
+
|
74
|
+
const ActionSheetItem = ({
|
75
|
+
className,
|
76
|
+
text,
|
77
|
+
onPress,
|
78
|
+
last = false,
|
79
|
+
}: {
|
80
|
+
className?: string;
|
81
|
+
text: string;
|
82
|
+
onPress: () => void;
|
83
|
+
last?: boolean;
|
84
|
+
}) => {
|
85
|
+
return (
|
86
|
+
<Button
|
87
|
+
variant="link"
|
88
|
+
className={cn(
|
89
|
+
"w-full items-center opacity-100 active:opacity-50 text-[16px] h-14 text-blue-500 rounded-none border-b ",
|
90
|
+
last ? "border-transparent" : "border-muted/15",
|
91
|
+
className
|
92
|
+
)}
|
93
|
+
onPress={onPress}
|
94
|
+
>
|
95
|
+
{text}
|
96
|
+
</Button>
|
97
|
+
);
|
98
|
+
};
|
@@ -0,0 +1,58 @@
|
|
1
|
+
import { Text, View } from "@nativetail/core";
|
2
|
+
import { forwardRef } from "react";
|
3
|
+
import { Button } from "../button";
|
4
|
+
import { Dialog, DialogMethods } from "../dialog";
|
5
|
+
|
6
|
+
export type AlertDialogProps = {
|
7
|
+
containerClassName?: string;
|
8
|
+
onConfirm: () => void;
|
9
|
+
onCancel: () => void;
|
10
|
+
title?: string;
|
11
|
+
description?: string;
|
12
|
+
useBlur?: boolean;
|
13
|
+
};
|
14
|
+
export type AlertDialogRef = DialogMethods;
|
15
|
+
export const AlertDialog = forwardRef<DialogMethods, AlertDialogProps>(
|
16
|
+
function AlertDialog(
|
17
|
+
{ containerClassName, onConfirm, title, description, useBlur, onCancel },
|
18
|
+
ref
|
19
|
+
) {
|
20
|
+
return (
|
21
|
+
<Dialog
|
22
|
+
containerClassName={containerClassName}
|
23
|
+
useBlur={useBlur}
|
24
|
+
ref={ref}
|
25
|
+
>
|
26
|
+
<View className="p-4 py-6">
|
27
|
+
{title && (
|
28
|
+
<Text className="text-foreground text-lg text-center font-semibold">
|
29
|
+
{title}
|
30
|
+
</Text>
|
31
|
+
)}
|
32
|
+
{description && (
|
33
|
+
<Text className="text-muted/75 text-center w-full">
|
34
|
+
{description}
|
35
|
+
</Text>
|
36
|
+
)}
|
37
|
+
</View>
|
38
|
+
|
39
|
+
<View className="flex-row items-center border-t border-muted/15">
|
40
|
+
<Button
|
41
|
+
variant="link"
|
42
|
+
className="flex-1 active:opacity-75 text-foreground"
|
43
|
+
onPress={onCancel}
|
44
|
+
>
|
45
|
+
Cancel
|
46
|
+
</Button>
|
47
|
+
<Button
|
48
|
+
variant="link"
|
49
|
+
className="flex-1 border active:opacity-75 border-transparent rounded-none border-l-muted/15"
|
50
|
+
onPress={onConfirm}
|
51
|
+
>
|
52
|
+
Confirm
|
53
|
+
</Button>
|
54
|
+
</View>
|
55
|
+
</Dialog>
|
56
|
+
);
|
57
|
+
}
|
58
|
+
);
|
@@ -0,0 +1,15 @@
|
|
1
|
+
import { BlurView, BlurViewProps } from "expo-blur";
|
2
|
+
import { useColorScheme } from "@nativetail/core";
|
3
|
+
|
4
|
+
export type BlurProps = BlurViewProps;
|
5
|
+
export const Blur = (props: BlurProps) => {
|
6
|
+
const [colorScheme] = useColorScheme();
|
7
|
+
return (
|
8
|
+
<BlurView
|
9
|
+
intensity={50}
|
10
|
+
tint={colorScheme === "dark" ? "dark" : "light"}
|
11
|
+
experimentalBlurMethod="dimezisBlurView"
|
12
|
+
{...props}
|
13
|
+
/>
|
14
|
+
);
|
15
|
+
};
|
@@ -0,0 +1,46 @@
|
|
1
|
+
import { BlurView } from "expo-blur";
|
2
|
+
import { cn, useTw, View } from "@nativetail/core";
|
3
|
+
import { forwardRef } from "react";
|
4
|
+
import ActionSheet, {
|
5
|
+
ActionSheetProps,
|
6
|
+
ActionSheetRef,
|
7
|
+
} from "react-native-actions-sheet";
|
8
|
+
import { Blur } from "../blur";
|
9
|
+
export type BottomSheetProps = ActionSheetProps & {
|
10
|
+
containerClassName?: string;
|
11
|
+
contentClassName?: string;
|
12
|
+
indicatorClassName?: string;
|
13
|
+
useBlur?: boolean;
|
14
|
+
};
|
15
|
+
export const BottomSheet = forwardRef<ActionSheetRef, BottomSheetProps>(
|
16
|
+
function BottomSheet(
|
17
|
+
{
|
18
|
+
containerClassName,
|
19
|
+
contentClassName,
|
20
|
+
children,
|
21
|
+
useBlur,
|
22
|
+
indicatorClassName,
|
23
|
+
...props
|
24
|
+
},
|
25
|
+
ref
|
26
|
+
) {
|
27
|
+
const tw = useTw();
|
28
|
+
return (
|
29
|
+
<ActionSheet
|
30
|
+
ref={ref}
|
31
|
+
containerStyle={tw.style("bg-background/75", containerClassName)}
|
32
|
+
gestureEnabled
|
33
|
+
indicatorStyle={tw.style("bg-muted/15", indicatorClassName)}
|
34
|
+
{...props}
|
35
|
+
>
|
36
|
+
{useBlur && (
|
37
|
+
<Blur
|
38
|
+
style={tw`absolute top-0 left-0 right-0 bottom-0 rounded-xl flex-1 bg-card`}
|
39
|
+
/>
|
40
|
+
)}
|
41
|
+
<View className={cn("p-4 ", contentClassName)}>{children}</View>
|
42
|
+
</ActionSheet>
|
43
|
+
);
|
44
|
+
}
|
45
|
+
);
|
46
|
+
export { ActionSheetRef as BottomSheetRef };
|
@@ -0,0 +1,109 @@
|
|
1
|
+
import React, { useMemo } from "react";
|
2
|
+
|
3
|
+
import { MotiPressableProps } from "moti/interactions";
|
4
|
+
import {
|
5
|
+
ActivityIndicator,
|
6
|
+
cva,
|
7
|
+
mergeClasses,
|
8
|
+
Pressable,
|
9
|
+
separateClasses,
|
10
|
+
useTw,
|
11
|
+
VariantProps,
|
12
|
+
} from "@nativetail/core";
|
13
|
+
|
14
|
+
const buttonVariants = cva(
|
15
|
+
"flex-row gap-2 items-center justify-center rounded text-sm font-medium hover:opacity-90 active:opacity-80 opacity-100 select-none",
|
16
|
+
{
|
17
|
+
variants: {
|
18
|
+
variant: {
|
19
|
+
default: "bg-primary text-foreground ",
|
20
|
+
destructive: "bg-red-500 text-foreground ",
|
21
|
+
outline: "border border-primary text-foreground bg-black/0 ",
|
22
|
+
secondary: "bg-secondary text-foreground ",
|
23
|
+
ghost: "",
|
24
|
+
link: "text-primary ",
|
25
|
+
card: " bg-card ",
|
26
|
+
},
|
27
|
+
size: {
|
28
|
+
default: "h-12 px-4 py-2",
|
29
|
+
sm: "h-8 px-3 text-xs",
|
30
|
+
lg: "h-10 px-8",
|
31
|
+
icon: "h-9 w-9",
|
32
|
+
},
|
33
|
+
disabled: {
|
34
|
+
true: "opacity-80",
|
35
|
+
},
|
36
|
+
},
|
37
|
+
defaultVariants: {
|
38
|
+
variant: "default",
|
39
|
+
size: "default",
|
40
|
+
},
|
41
|
+
}
|
42
|
+
);
|
43
|
+
type ButtonProps = MotiPressableProps &
|
44
|
+
VariantProps<typeof buttonVariants> & {
|
45
|
+
text?: string;
|
46
|
+
disabled?: boolean;
|
47
|
+
isLoading?: boolean;
|
48
|
+
textClass?: string;
|
49
|
+
leftElement?: React.ReactNode;
|
50
|
+
rightElement?: React.ReactNode;
|
51
|
+
className?: string;
|
52
|
+
children?: React.ReactNode;
|
53
|
+
loadingIndicatorClassName?: string;
|
54
|
+
};
|
55
|
+
|
56
|
+
const Button = ({
|
57
|
+
text,
|
58
|
+
children,
|
59
|
+
isLoading,
|
60
|
+
className,
|
61
|
+
disabled,
|
62
|
+
variant,
|
63
|
+
leftElement,
|
64
|
+
rightElement,
|
65
|
+
size,
|
66
|
+
loadingIndicatorClassName,
|
67
|
+
|
68
|
+
...props
|
69
|
+
}: ButtonProps) => {
|
70
|
+
const tw = useTw();
|
71
|
+
|
72
|
+
const loading = isLoading;
|
73
|
+
|
74
|
+
const isDisabled = disabled || loading;
|
75
|
+
const variantClass = useMemo(
|
76
|
+
() =>
|
77
|
+
buttonVariants({
|
78
|
+
variant,
|
79
|
+
size,
|
80
|
+
className,
|
81
|
+
disabled: isDisabled,
|
82
|
+
}),
|
83
|
+
[variant, size, className, isDisabled, tw]
|
84
|
+
);
|
85
|
+
|
86
|
+
const { textClasses } = separateClasses(variantClass);
|
87
|
+
|
88
|
+
return (
|
89
|
+
<Pressable
|
90
|
+
disabled={disabled || loading}
|
91
|
+
className={variantClass}
|
92
|
+
{...props}
|
93
|
+
>
|
94
|
+
{leftElement}
|
95
|
+
{loading && (
|
96
|
+
<ActivityIndicator
|
97
|
+
className={mergeClasses(
|
98
|
+
"mr-2 h-5 w-5 text-foreground ",
|
99
|
+
textClasses,
|
100
|
+
loadingIndicatorClassName
|
101
|
+
)}
|
102
|
+
/>
|
103
|
+
)}
|
104
|
+
{children}
|
105
|
+
{rightElement}
|
106
|
+
</Pressable>
|
107
|
+
);
|
108
|
+
};
|
109
|
+
export { Button };
|
@@ -0,0 +1,74 @@
|
|
1
|
+
import React, { useMemo } from "react";
|
2
|
+
|
3
|
+
import {
|
4
|
+
cn,
|
5
|
+
cva,
|
6
|
+
separateClasses,
|
7
|
+
Text,
|
8
|
+
VariantProps,
|
9
|
+
View,
|
10
|
+
} from "@nativetail/core";
|
11
|
+
|
12
|
+
const chipVariants = cva(
|
13
|
+
"items-center justify-center self-start text-foreground rounded-full",
|
14
|
+
{
|
15
|
+
variants: {
|
16
|
+
variant: {
|
17
|
+
default: "bg-primary ",
|
18
|
+
destructive: "bg-danger ",
|
19
|
+
success: "bg-success",
|
20
|
+
outline: "border border-primary bg-background ",
|
21
|
+
secondary: "bg-secondary ",
|
22
|
+
card: " bg-card ",
|
23
|
+
},
|
24
|
+
size: {
|
25
|
+
default: "px-3 py-2 text-[16px]",
|
26
|
+
sm: " px-2 py-1 text-sm",
|
27
|
+
lg: "px-4 py-3 text-lg",
|
28
|
+
},
|
29
|
+
},
|
30
|
+
defaultVariants: {
|
31
|
+
variant: "default",
|
32
|
+
size: "default",
|
33
|
+
},
|
34
|
+
}
|
35
|
+
);
|
36
|
+
type ChipProps = VariantProps<typeof chipVariants> & {
|
37
|
+
text?: string;
|
38
|
+
textClass?: string;
|
39
|
+
leftElement?: React.ReactNode;
|
40
|
+
rightElement?: React.ReactNode;
|
41
|
+
className?: string;
|
42
|
+
};
|
43
|
+
|
44
|
+
const Chip = ({
|
45
|
+
text,
|
46
|
+
variant,
|
47
|
+
size = "sm",
|
48
|
+
className,
|
49
|
+
leftElement,
|
50
|
+
rightElement,
|
51
|
+
|
52
|
+
...props
|
53
|
+
}: ChipProps) => {
|
54
|
+
const variantClass = useMemo(
|
55
|
+
() =>
|
56
|
+
chipVariants({
|
57
|
+
variant,
|
58
|
+
size,
|
59
|
+
className: className,
|
60
|
+
}),
|
61
|
+
[variant, size, className]
|
62
|
+
);
|
63
|
+
|
64
|
+
const { textClasses } = separateClasses(variantClass);
|
65
|
+
|
66
|
+
return (
|
67
|
+
<View className={variantClass} {...props}>
|
68
|
+
{leftElement}
|
69
|
+
<Text className={cn(textClasses, "p-0 pt-0")}>{text}</Text>
|
70
|
+
{rightElement}
|
71
|
+
</View>
|
72
|
+
);
|
73
|
+
};
|
74
|
+
export { Chip };
|
@@ -0,0 +1,101 @@
|
|
1
|
+
import { TextInput, View, cn, useTw } from "@nativetail/core";
|
2
|
+
import { useCallback, useRef } from "react";
|
3
|
+
import { Iconify } from "react-native-iconify";
|
4
|
+
import { Button } from "../button";
|
5
|
+
export type CounterProps = {
|
6
|
+
value: number;
|
7
|
+
max?: number;
|
8
|
+
min?: number;
|
9
|
+
setValue: React.Dispatch<React.SetStateAction<number>>;
|
10
|
+
containerClassName?: string;
|
11
|
+
};
|
12
|
+
export function Counter({
|
13
|
+
value,
|
14
|
+
setValue,
|
15
|
+
min,
|
16
|
+
max,
|
17
|
+
containerClassName,
|
18
|
+
}: CounterProps) {
|
19
|
+
const tw = useTw();
|
20
|
+
const increment = useCallback(() => {
|
21
|
+
setValue((prev) => {
|
22
|
+
if (max && prev >= max) return;
|
23
|
+
return prev + 1;
|
24
|
+
});
|
25
|
+
}, [setValue, max]);
|
26
|
+
const decrement = useCallback(() => {
|
27
|
+
setValue((prev) => {
|
28
|
+
if (min && prev <= min) return;
|
29
|
+
return prev - 1;
|
30
|
+
});
|
31
|
+
}, [setValue, min]);
|
32
|
+
const onChangeText = useCallback(
|
33
|
+
(text) => {
|
34
|
+
const number = parseInt(text);
|
35
|
+
|
36
|
+
if (isNaN(number)) return;
|
37
|
+
setValue(number);
|
38
|
+
},
|
39
|
+
[min, max, setValue]
|
40
|
+
);
|
41
|
+
|
42
|
+
return (
|
43
|
+
<View
|
44
|
+
className={cn(
|
45
|
+
"flex-row items-center gap-2 select-none w-full",
|
46
|
+
containerClassName
|
47
|
+
)}
|
48
|
+
>
|
49
|
+
<CounterButton disabled={!!(min && value <= min)} onPress={decrement}>
|
50
|
+
<Iconify
|
51
|
+
icon="ic:round-minus"
|
52
|
+
size={15}
|
53
|
+
color={tw.color("foreground")}
|
54
|
+
/>
|
55
|
+
</CounterButton>
|
56
|
+
<View className="flex-1 h-full">
|
57
|
+
<TextInput
|
58
|
+
className="flex-1 items-center text-center justify-center bg-card rounded-xl h-full text-foreground font-medium select-none"
|
59
|
+
onChangeText={onChangeText}
|
60
|
+
value={value.toString()}
|
61
|
+
/>
|
62
|
+
</View>
|
63
|
+
<CounterButton disabled={!!(max && value >= max)} onPress={increment}>
|
64
|
+
<Iconify
|
65
|
+
icon="ic:round-plus"
|
66
|
+
size={15}
|
67
|
+
color={tw.color("foreground")}
|
68
|
+
/>
|
69
|
+
</CounterButton>
|
70
|
+
</View>
|
71
|
+
);
|
72
|
+
}
|
73
|
+
const CounterButton = ({
|
74
|
+
onPress,
|
75
|
+
children,
|
76
|
+
disabled,
|
77
|
+
}: {
|
78
|
+
onPress: () => void;
|
79
|
+
children: React.ReactNode;
|
80
|
+
disabled?: boolean;
|
81
|
+
}) => {
|
82
|
+
const functionInterval = useRef<NodeJS.Timeout>();
|
83
|
+
const onPressIn = useCallback(() => {
|
84
|
+
onPress();
|
85
|
+
functionInterval.current = setInterval(onPress, 100);
|
86
|
+
}, [onPress]);
|
87
|
+
const onPressOut = useCallback(() => {
|
88
|
+
if (functionInterval.current) clearInterval(functionInterval.current);
|
89
|
+
}, []);
|
90
|
+
|
91
|
+
return (
|
92
|
+
<Button
|
93
|
+
disabled={disabled}
|
94
|
+
onPressIn={onPressIn}
|
95
|
+
onPressOut={onPressOut}
|
96
|
+
size="sm"
|
97
|
+
>
|
98
|
+
{children}
|
99
|
+
</Button>
|
100
|
+
);
|
101
|
+
};
|
@@ -0,0 +1,111 @@
|
|
1
|
+
import { BlurView } from "expo-blur";
|
2
|
+
import { AnimatePresence } from "moti";
|
3
|
+
import { cn, mergeClasses, Pressable, useTw, View } from "@nativetail/core";
|
4
|
+
import {
|
5
|
+
forwardRef,
|
6
|
+
useCallback,
|
7
|
+
useEffect,
|
8
|
+
useImperativeHandle,
|
9
|
+
useState,
|
10
|
+
} from "react";
|
11
|
+
import { Modal } from "react-native";
|
12
|
+
import { Blur } from "../blur";
|
13
|
+
|
14
|
+
export type DialogProps = {
|
15
|
+
containerClassName?: string;
|
16
|
+
contentClassName?: string;
|
17
|
+
useBlur?: boolean;
|
18
|
+
children: React.ReactNode;
|
19
|
+
onRequestClose?: () => void;
|
20
|
+
};
|
21
|
+
let timeout: NodeJS.Timeout;
|
22
|
+
export type DialogMethods = {
|
23
|
+
show: () => void;
|
24
|
+
hide: () => void;
|
25
|
+
};
|
26
|
+
export const Dialog = forwardRef<DialogMethods, DialogProps>(function Dialog(
|
27
|
+
{
|
28
|
+
containerClassName,
|
29
|
+
contentClassName,
|
30
|
+
useBlur = true,
|
31
|
+
children,
|
32
|
+
onRequestClose,
|
33
|
+
}: DialogProps,
|
34
|
+
ref
|
35
|
+
) {
|
36
|
+
const [isOpen, setOpen] = useState(false);
|
37
|
+
|
38
|
+
const getRef = useCallback(() => {
|
39
|
+
return {
|
40
|
+
show: () => {
|
41
|
+
setOpen(true);
|
42
|
+
},
|
43
|
+
hide: () => {
|
44
|
+
setOpen(false);
|
45
|
+
},
|
46
|
+
};
|
47
|
+
}, [setOpen]);
|
48
|
+
useImperativeHandle(ref, getRef, []);
|
49
|
+
const tw = useTw();
|
50
|
+
const [modalOpen, setModalOpen] = useState(isOpen);
|
51
|
+
useEffect(() => {
|
52
|
+
if (isOpen) {
|
53
|
+
setModalOpen(true);
|
54
|
+
} else {
|
55
|
+
timeout = setTimeout(
|
56
|
+
() => {
|
57
|
+
setModalOpen(false);
|
58
|
+
},
|
59
|
+
isOpen ? 0 : 200
|
60
|
+
);
|
61
|
+
}
|
62
|
+
return () => {
|
63
|
+
if (timeout) clearTimeout(timeout);
|
64
|
+
};
|
65
|
+
}, [isOpen]);
|
66
|
+
|
67
|
+
const onDidAnimate = useCallback(() => {
|
68
|
+
if (!isOpen) {
|
69
|
+
setModalOpen(false);
|
70
|
+
}
|
71
|
+
}, [isOpen]);
|
72
|
+
|
73
|
+
return (
|
74
|
+
<Modal
|
75
|
+
visible={modalOpen}
|
76
|
+
transparent
|
77
|
+
onRequestClose={onRequestClose}
|
78
|
+
animationType="fade"
|
79
|
+
statusBarTranslucent
|
80
|
+
>
|
81
|
+
<Pressable
|
82
|
+
className={cn(
|
83
|
+
"flex-1 items-center justify-center p-4 w-full h-full bg-black/35",
|
84
|
+
containerClassName
|
85
|
+
)}
|
86
|
+
onPress={onRequestClose}
|
87
|
+
disabled={!onRequestClose}
|
88
|
+
>
|
89
|
+
<AnimatePresence exitBeforeEnter>
|
90
|
+
{isOpen && (
|
91
|
+
<View
|
92
|
+
className={mergeClasses(
|
93
|
+
"absolute overflow-hidden in:opacity-0 opacity-100 out:opacity-0 in:scale-0 scale-100 out:scale-0 z-10 bg-card/95 rounded-xl max-w-sm w-full border border-muted/10",
|
94
|
+
contentClassName
|
95
|
+
)}
|
96
|
+
onDidAnimate={onDidAnimate}
|
97
|
+
>
|
98
|
+
{useBlur && (
|
99
|
+
<Blur
|
100
|
+
style={tw`absolute top-0 left-0 right-0 bottom-0 rounded-xl flex-1 bg-card`}
|
101
|
+
/>
|
102
|
+
)}
|
103
|
+
|
104
|
+
{children}
|
105
|
+
</View>
|
106
|
+
)}
|
107
|
+
</AnimatePresence>
|
108
|
+
</Pressable>
|
109
|
+
</Modal>
|
110
|
+
);
|
111
|
+
});
|