@nativetail/ui 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
});
|