@nativetail/ui 0.0.6 → 0.0.8
Sign up to get free protection for your applications and to get access to all the features.
- package/package.json +3 -2
- package/src/components/alert-dialog/index.tsx +1 -1
- package/src/components/bottom-sheet/index.tsx +0 -1
- package/src/components/button/index.tsx +65 -23
- package/src/components/chip/index.tsx +1 -1
- package/src/components/counter/index.tsx +4 -3
- package/src/components/dialog/index.tsx +19 -7
- package/src/components/dropdown/index.tsx +3 -1
- package/src/components/input/floating-input.tsx +58 -15
- package/src/components/input/input.tsx +47 -9
- package/src/components/input/pin-input.tsx +130 -35
- package/src/components/select/index.tsx +2 -141
- package/src/components/select/multi-select.tsx +208 -0
- package/src/components/select/select.tsx +181 -0
- package/src/components/toast/index.tsx +121 -39
- package/src/index.ts +1 -0
- package/src/utils/component-theme.tsx +51 -0
- package/tailwind.config.js +4 -0
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "@nativetail/ui",
|
3
|
-
"version": "0.0.
|
3
|
+
"version": "0.0.8",
|
4
4
|
"description": "",
|
5
5
|
"main": "src/index.ts",
|
6
6
|
"scripts": {},
|
@@ -40,7 +40,8 @@
|
|
40
40
|
"react-native-reanimated": "~3.10.1",
|
41
41
|
"react-native-safe-area-context": "4.10.1",
|
42
42
|
"tailwind-merge": "^2.3.0",
|
43
|
-
"zustand": "^4.5.2"
|
43
|
+
"zustand": "^4.5.2",
|
44
|
+
"@hookform/resolvers": "^3.6.0"
|
44
45
|
},
|
45
46
|
"devDependencies": {
|
46
47
|
"metro-react-native-babel-preset": "^0.77.0",
|
@@ -39,7 +39,7 @@ export const AlertDialog = forwardRef<DialogMethods, AlertDialogProps>(
|
|
39
39
|
<View className="flex-row items-center border-t border-muted/15">
|
40
40
|
<Button
|
41
41
|
variant="link"
|
42
|
-
className="flex-1 active:opacity-75 text-foreground"
|
42
|
+
className="flex-1 active:opacity-75 text-foreground rounded-none"
|
43
43
|
onPress={onCancel}
|
44
44
|
>
|
45
45
|
Cancel
|
@@ -1,8 +1,9 @@
|
|
1
1
|
import React, { useMemo } from "react";
|
2
2
|
|
3
|
-
import { MotiPressableProps } from "moti/interactions";
|
4
3
|
import {
|
5
4
|
ActivityIndicator,
|
5
|
+
cn,
|
6
|
+
ConfigVariants,
|
6
7
|
cva,
|
7
8
|
mergeClasses,
|
8
9
|
Pressable,
|
@@ -10,6 +11,8 @@ import {
|
|
10
11
|
useTw,
|
11
12
|
VariantProps,
|
12
13
|
} from "@nativetail/core";
|
14
|
+
import { MotiPressableProps } from "moti/interactions";
|
15
|
+
import { useComponentTheme } from "../../utils/component-theme";
|
13
16
|
|
14
17
|
const buttonVariants = cva(
|
15
18
|
"flex-row gap-2 items-center justify-center rounded text-sm font-medium hover:opacity-90 active:opacity-80 opacity-100 select-none",
|
@@ -18,7 +21,7 @@ const buttonVariants = cva(
|
|
18
21
|
variant: {
|
19
22
|
default: "bg-primary text-foreground ",
|
20
23
|
destructive: "bg-red-500 text-foreground ",
|
21
|
-
outline: "border border-
|
24
|
+
outline: "border border-muted/15 text-foreground bg-black/0 ",
|
22
25
|
secondary: "bg-secondary text-foreground ",
|
23
26
|
ghost: "",
|
24
27
|
link: "text-primary ",
|
@@ -40,6 +43,30 @@ const buttonVariants = cva(
|
|
40
43
|
},
|
41
44
|
}
|
42
45
|
);
|
46
|
+
type VariantPropType = (
|
47
|
+
props?: ConfigVariants<{
|
48
|
+
variant: {
|
49
|
+
default: string;
|
50
|
+
destructive: string;
|
51
|
+
outline: string;
|
52
|
+
secondary: string;
|
53
|
+
ghost: string;
|
54
|
+
link: string;
|
55
|
+
card: string;
|
56
|
+
};
|
57
|
+
size: {
|
58
|
+
default: string;
|
59
|
+
sm: string;
|
60
|
+
lg: string;
|
61
|
+
icon: string;
|
62
|
+
};
|
63
|
+
disabled: {
|
64
|
+
true: string;
|
65
|
+
};
|
66
|
+
}> & {
|
67
|
+
className?: string;
|
68
|
+
}
|
69
|
+
) => string;
|
43
70
|
export type ButtonProps = MotiPressableProps &
|
44
71
|
VariantProps<typeof buttonVariants> & {
|
45
72
|
text?: string;
|
@@ -51,36 +78,51 @@ export type ButtonProps = MotiPressableProps &
|
|
51
78
|
className?: string;
|
52
79
|
children?: React.ReactNode;
|
53
80
|
loadingIndicatorClassName?: string;
|
81
|
+
variants?: VariantPropType;
|
54
82
|
};
|
55
83
|
|
56
|
-
const Button = ({
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
84
|
+
const Button = (passedProps: ButtonProps) => {
|
85
|
+
const componentTheme = useComponentTheme();
|
86
|
+
const buttonProps = componentTheme?.Button || {};
|
87
|
+
const {
|
88
|
+
text,
|
89
|
+
children,
|
90
|
+
isLoading,
|
91
|
+
disabled,
|
92
|
+
variant,
|
93
|
+
leftElement,
|
94
|
+
rightElement,
|
95
|
+
size,
|
96
|
+
className: propClassName,
|
97
|
+
loadingIndicatorClassName,
|
98
|
+
variants,
|
99
|
+
...props
|
100
|
+
} = {
|
101
|
+
...buttonProps,
|
102
|
+
...passedProps,
|
103
|
+
};
|
70
104
|
const tw = useTw();
|
105
|
+
const className = cn(buttonProps.className, passedProps.className);
|
71
106
|
|
72
107
|
const loading = isLoading;
|
73
108
|
|
74
109
|
const isDisabled = disabled || loading;
|
75
110
|
const variantClass = useMemo(
|
76
111
|
() =>
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
112
|
+
variants
|
113
|
+
? variants({
|
114
|
+
variant,
|
115
|
+
size,
|
116
|
+
className,
|
117
|
+
disabled: isDisabled,
|
118
|
+
})
|
119
|
+
: buttonVariants({
|
120
|
+
variant,
|
121
|
+
size,
|
122
|
+
className,
|
123
|
+
disabled: isDisabled,
|
124
|
+
}),
|
125
|
+
[variant, size, className, isDisabled, tw, variants]
|
84
126
|
);
|
85
127
|
|
86
128
|
const { textClasses } = separateClasses(variantClass);
|
@@ -19,13 +19,13 @@ export function Counter({
|
|
19
19
|
const tw = useTw();
|
20
20
|
const increment = useCallback(() => {
|
21
21
|
setValue((prev) => {
|
22
|
-
if (max && prev >= max) return;
|
22
|
+
if (max && prev >= max) return prev;
|
23
23
|
return prev + 1;
|
24
24
|
});
|
25
25
|
}, [setValue, max]);
|
26
26
|
const decrement = useCallback(() => {
|
27
27
|
setValue((prev) => {
|
28
|
-
if (min && prev <= min) return;
|
28
|
+
if (min && prev <= min) return prev;
|
29
29
|
return prev - 1;
|
30
30
|
});
|
31
31
|
}, [setValue, min]);
|
@@ -34,6 +34,7 @@ export function Counter({
|
|
34
34
|
const number = parseInt(text);
|
35
35
|
|
36
36
|
if (isNaN(number)) return;
|
37
|
+
if (!number) return;
|
37
38
|
setValue(number);
|
38
39
|
},
|
39
40
|
[min, max, setValue]
|
@@ -57,7 +58,7 @@ export function Counter({
|
|
57
58
|
<TextInput
|
58
59
|
className="flex-1 items-center text-center justify-center bg-card rounded-xl h-full text-foreground font-medium select-none"
|
59
60
|
onChangeText={onChangeText}
|
60
|
-
value={value
|
61
|
+
value={value?.toString?.()}
|
61
62
|
/>
|
62
63
|
</View>
|
63
64
|
<CounterButton disabled={!!(max && value >= max)} onPress={increment}>
|
@@ -17,6 +17,8 @@ export type DialogProps = {
|
|
17
17
|
useBlur?: boolean;
|
18
18
|
children: React.ReactNode;
|
19
19
|
onRequestClose?: () => void;
|
20
|
+
closable?: boolean;
|
21
|
+
backdropClassName?: string;
|
20
22
|
};
|
21
23
|
let timeout: NodeJS.Timeout;
|
22
24
|
export type DialogMethods = {
|
@@ -29,7 +31,9 @@ export const Dialog = forwardRef<DialogMethods, DialogProps>(function Dialog(
|
|
29
31
|
contentClassName,
|
30
32
|
useBlur = false,
|
31
33
|
children,
|
34
|
+
closable = true,
|
32
35
|
onRequestClose,
|
36
|
+
backdropClassName,
|
33
37
|
}: DialogProps,
|
34
38
|
ref
|
35
39
|
) {
|
@@ -70,27 +74,35 @@ export const Dialog = forwardRef<DialogMethods, DialogProps>(function Dialog(
|
|
70
74
|
}
|
71
75
|
}, [isOpen]);
|
72
76
|
|
77
|
+
const handleOnRequestClose =
|
78
|
+
onRequestClose ?? closable ? getRef().hide : undefined;
|
73
79
|
return (
|
74
80
|
<Modal
|
75
81
|
visible={modalOpen}
|
76
82
|
transparent
|
77
|
-
onRequestClose={
|
83
|
+
onRequestClose={handleOnRequestClose}
|
78
84
|
animationType="fade"
|
79
85
|
statusBarTranslucent
|
80
86
|
>
|
81
|
-
<
|
87
|
+
<View
|
82
88
|
className={cn(
|
83
|
-
"flex-1 items-center justify-center p-4 w-full h-full
|
89
|
+
"flex-1 items-center justify-center p-4 w-full h-full ",
|
84
90
|
containerClassName
|
85
91
|
)}
|
86
|
-
onPress={onRequestClose}
|
87
|
-
disabled={!onRequestClose}
|
88
92
|
>
|
93
|
+
<Pressable
|
94
|
+
onPress={handleOnRequestClose}
|
95
|
+
disabled={!closable && !onRequestClose}
|
96
|
+
className={cn(
|
97
|
+
"flex-1 bg-black/35 -z-10 w-full h-full absolute top-0 left-0",
|
98
|
+
backdropClassName
|
99
|
+
)}
|
100
|
+
/>
|
89
101
|
<AnimatePresence exitBeforeEnter>
|
90
102
|
{isOpen && (
|
91
103
|
<View
|
92
104
|
className={mergeClasses(
|
93
|
-
"absolute overflow-hidden in:opacity-0 opacity-100 out:opacity-0 in:scale-0 scale-100 out:scale-0 z-
|
105
|
+
"absolute overflow-hidden in:opacity-0 opacity-100 out:opacity-0 in:scale-0 scale-100 out:scale-0 z-15 bg-card/95 rounded-xl max-w-sm w-full border border-muted/10",
|
94
106
|
contentClassName
|
95
107
|
)}
|
96
108
|
onDidAnimate={onDidAnimate}
|
@@ -105,7 +117,7 @@ export const Dialog = forwardRef<DialogMethods, DialogProps>(function Dialog(
|
|
105
117
|
</View>
|
106
118
|
)}
|
107
119
|
</AnimatePresence>
|
108
|
-
</
|
120
|
+
</View>
|
109
121
|
</Modal>
|
110
122
|
);
|
111
123
|
});
|
@@ -215,12 +215,14 @@ const DropdownItem = ({
|
|
215
215
|
children,
|
216
216
|
last,
|
217
217
|
first,
|
218
|
+
autoClosable = true,
|
218
219
|
...props
|
219
220
|
}: {
|
220
221
|
className?: string;
|
221
222
|
last?: boolean;
|
222
223
|
children: ReactNode;
|
223
224
|
first?: boolean;
|
225
|
+
autoClosable?: boolean;
|
224
226
|
} & PressableProps) => {
|
225
227
|
const close = useDropdownContext().close;
|
226
228
|
return (
|
@@ -233,7 +235,7 @@ const DropdownItem = ({
|
|
233
235
|
)}
|
234
236
|
{...props}
|
235
237
|
onPress={() => {
|
236
|
-
close();
|
238
|
+
if (autoClosable) close();
|
237
239
|
props.onPress && props.onPress();
|
238
240
|
}}
|
239
241
|
>
|
@@ -1,24 +1,52 @@
|
|
1
|
-
import {
|
2
|
-
|
3
|
-
Text,
|
4
|
-
TextInput,
|
5
|
-
TextInputProps,
|
6
|
-
useTw,
|
7
|
-
View,
|
8
|
-
} from "@nativetail/core";
|
9
|
-
import { useCallback, useState } from "react";
|
1
|
+
import { cn, Text, TextInput, TextInputProps, View } from "@nativetail/core";
|
2
|
+
import { LegacyRef, useCallback, useState } from "react";
|
10
3
|
import ShowPassword from "./show-password";
|
4
|
+
import { Control, Controller, Path } from "react-hook-form";
|
11
5
|
|
12
|
-
type FloatingInputProps =
|
6
|
+
export type FloatingInputProps<T = Record<string, any>> = TextInputProps & {
|
13
7
|
containerClassName?: string;
|
14
|
-
label
|
8
|
+
label?: string;
|
15
9
|
error?: string;
|
16
10
|
helperText?: string;
|
17
11
|
isSecretToggleable?: boolean;
|
18
12
|
leftElement?: React.ReactNode;
|
19
13
|
rightElement?: React.ReactNode;
|
14
|
+
value?: string;
|
15
|
+
control?: Control<T, any>;
|
16
|
+
name?: Path<T>;
|
17
|
+
inputRef?: LegacyRef<typeof TextInput>;
|
18
|
+
labelClassName?: string;
|
19
|
+
activeLabelClassName?: string;
|
20
20
|
};
|
21
|
-
|
21
|
+
|
22
|
+
export const FloatingInput = <T extends Record<string, any>>({
|
23
|
+
name,
|
24
|
+
control,
|
25
|
+
...props
|
26
|
+
}: FloatingInputProps<T>) => {
|
27
|
+
if (control) {
|
28
|
+
return (
|
29
|
+
<Controller
|
30
|
+
name={name}
|
31
|
+
control={control}
|
32
|
+
render={({ field, fieldState }) => {
|
33
|
+
return (
|
34
|
+
<BaseInput
|
35
|
+
{...props}
|
36
|
+
value={field.value}
|
37
|
+
onChangeText={(text) => {
|
38
|
+
field.onChange(text);
|
39
|
+
}}
|
40
|
+
error={fieldState?.error?.message}
|
41
|
+
/>
|
42
|
+
);
|
43
|
+
}}
|
44
|
+
/>
|
45
|
+
);
|
46
|
+
}
|
47
|
+
return <BaseInput {...props} />;
|
48
|
+
};
|
49
|
+
function BaseInput({
|
22
50
|
value,
|
23
51
|
onChangeText,
|
24
52
|
containerClassName,
|
@@ -29,6 +57,8 @@ export function FloatingInput({
|
|
29
57
|
helperText,
|
30
58
|
leftElement,
|
31
59
|
rightElement,
|
60
|
+
labelClassName,
|
61
|
+
activeLabelClassName,
|
32
62
|
...props
|
33
63
|
}: FloatingInputProps) {
|
34
64
|
const [isFocused, setIsFocused] = useState(false);
|
@@ -49,7 +79,13 @@ export function FloatingInput({
|
|
49
79
|
containerClassName
|
50
80
|
)}
|
51
81
|
>
|
52
|
-
<Label
|
82
|
+
<Label
|
83
|
+
label={label}
|
84
|
+
value={value}
|
85
|
+
isFocused={isFocused}
|
86
|
+
labelClassName={labelClassName}
|
87
|
+
activeLabelClassName={activeLabelClassName}
|
88
|
+
/>
|
53
89
|
|
54
90
|
{leftElement && (
|
55
91
|
<View className="absolute left-2 bottom-2">{leftElement}</View>
|
@@ -59,6 +95,7 @@ export function FloatingInput({
|
|
59
95
|
onBlur={onBlur}
|
60
96
|
value={value}
|
61
97
|
onChangeText={onChangeText}
|
98
|
+
enablesReturnKeyAutomatically
|
62
99
|
className={cn(
|
63
100
|
"flex-1 p-3 bg-card rounded-xl absolute w-full h-full -z-5 pt-5 text-foreground text-[16px]",
|
64
101
|
className,
|
@@ -91,10 +128,14 @@ const Label = ({
|
|
91
128
|
label,
|
92
129
|
value,
|
93
130
|
isFocused,
|
131
|
+
activeLabelClassName,
|
132
|
+
labelClassName,
|
94
133
|
}: {
|
95
134
|
label?: string;
|
96
135
|
value?: string;
|
97
136
|
isFocused?: boolean;
|
137
|
+
labelClassName?: string;
|
138
|
+
activeLabelClassName?: string;
|
98
139
|
}) => {
|
99
140
|
const labelOnTop = isFocused || !!value;
|
100
141
|
|
@@ -103,8 +144,10 @@ const Label = ({
|
|
103
144
|
<Text
|
104
145
|
animated
|
105
146
|
className={cn(
|
106
|
-
"text-muted duration-75
|
107
|
-
|
147
|
+
"text-muted duration-75 translate-y-0 text-[16px]",
|
148
|
+
labelClassName,
|
149
|
+
labelOnTop ? " -translate-y-16 text-xs" : "",
|
150
|
+
labelOnTop ? activeLabelClassName : ""
|
108
151
|
)}
|
109
152
|
>
|
110
153
|
{label}
|
@@ -6,19 +6,54 @@ import {
|
|
6
6
|
useTw,
|
7
7
|
View,
|
8
8
|
} from "@nativetail/core";
|
9
|
-
import { useState } from "react";
|
9
|
+
import React, { LegacyRef, useState } from "react";
|
10
|
+
import { Control, Controller, Path } from "react-hook-form";
|
11
|
+
import { TextInput as NativeTextInput } from "react-native";
|
10
12
|
import ShowPassword from "./show-password";
|
11
13
|
|
12
|
-
type InputProps = TextInputProps & {
|
14
|
+
export type InputProps<T = Record<string, any>> = TextInputProps & {
|
13
15
|
containerClassName?: string;
|
14
|
-
label
|
16
|
+
label?: string;
|
15
17
|
error?: string;
|
16
18
|
helperText?: string;
|
17
19
|
isSecretToggleable?: boolean;
|
18
20
|
leftElement?: React.ReactNode;
|
19
21
|
rightElement?: React.ReactNode;
|
22
|
+
value?: string;
|
23
|
+
control?: Control<T, any>;
|
24
|
+
name?: Path<T>;
|
25
|
+
inputRef?: LegacyRef<NativeTextInput>;
|
20
26
|
};
|
21
|
-
|
27
|
+
|
28
|
+
export const Input = <T extends Record<string, any>>({
|
29
|
+
name,
|
30
|
+
control,
|
31
|
+
...props
|
32
|
+
}: InputProps<T>) => {
|
33
|
+
if (control) {
|
34
|
+
return (
|
35
|
+
<Controller
|
36
|
+
name={name}
|
37
|
+
control={control}
|
38
|
+
render={({ field, fieldState }) => {
|
39
|
+
return (
|
40
|
+
<BaseInput
|
41
|
+
{...props}
|
42
|
+
value={field.value}
|
43
|
+
onChangeText={(text) => {
|
44
|
+
field.onChange(text);
|
45
|
+
}}
|
46
|
+
error={fieldState.error?.message}
|
47
|
+
/>
|
48
|
+
);
|
49
|
+
}}
|
50
|
+
/>
|
51
|
+
);
|
52
|
+
}
|
53
|
+
return <BaseInput {...props} />;
|
54
|
+
};
|
55
|
+
|
56
|
+
const BaseInput = <T extends Record<string, any>>({
|
22
57
|
value,
|
23
58
|
onChangeText,
|
24
59
|
containerClassName,
|
@@ -29,8 +64,9 @@ export function Input({
|
|
29
64
|
rightElement,
|
30
65
|
helperText,
|
31
66
|
leftElement,
|
67
|
+
inputRef,
|
32
68
|
...props
|
33
|
-
}: InputProps) {
|
69
|
+
}: InputProps<T>) => {
|
34
70
|
const tw = useTw();
|
35
71
|
|
36
72
|
const [showPassword, setShowPassword] = useState(
|
@@ -38,16 +74,18 @@ export function Input({
|
|
38
74
|
);
|
39
75
|
return (
|
40
76
|
<View className={cn("w-full gap-1", containerClassName)}>
|
41
|
-
<Text className={cn("text-muted/75 duration-75 ")}>{label}</Text>
|
77
|
+
<Text className={cn("text-muted/75 duration-75 text-sm")}>{label}</Text>
|
42
78
|
|
43
79
|
<TextInput
|
44
80
|
value={value}
|
45
81
|
onChangeText={onChangeText}
|
82
|
+
ref={inputRef}
|
46
83
|
className={cn(
|
47
|
-
"p-3 bg-card rounded-lg w-full border border-muted/15 h-
|
84
|
+
"p-3 bg-card rounded-lg w-full border border-muted/15 h-12 text-foreground -z-5 text-[16px]",
|
48
85
|
className,
|
49
86
|
isSecretToggleable || rightElement ? "pr-12" : "",
|
50
|
-
leftElement ? "pl-12" : ""
|
87
|
+
leftElement ? "pl-12" : "",
|
88
|
+
error && "border-danger"
|
51
89
|
)}
|
52
90
|
placeholderTextColor={tw.color("muted")}
|
53
91
|
secureTextEntry={!showPassword}
|
@@ -72,4 +110,4 @@ export function Input({
|
|
72
110
|
)}
|
73
111
|
</View>
|
74
112
|
);
|
75
|
-
}
|
113
|
+
};
|