@nativetail/ui 0.0.5 → 0.0.7
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/package.json +3 -2
- package/src/components/actions-sheet/index.tsx +1 -1
- package/src/components/alert-dialog/index.tsx +1 -1
- package/src/components/bottom-sheet/index.tsx +2 -3
- 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 +20 -8
- package/src/components/dropdown/index.tsx +4 -2
- 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.7",
|
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,4 +1,3 @@
|
|
1
|
-
import { BlurView } from "expo-blur";
|
2
1
|
import { cn, useTw, View } from "@nativetail/core";
|
3
2
|
import { forwardRef } from "react";
|
4
3
|
import ActionSheet, {
|
@@ -18,7 +17,7 @@ export const BottomSheet = forwardRef<ActionSheetRef, BottomSheetProps>(
|
|
18
17
|
containerClassName,
|
19
18
|
contentClassName,
|
20
19
|
children,
|
21
|
-
useBlur,
|
20
|
+
useBlur = false,
|
22
21
|
indicatorClassName,
|
23
22
|
...props
|
24
23
|
},
|
@@ -28,7 +27,7 @@ export const BottomSheet = forwardRef<ActionSheetRef, BottomSheetProps>(
|
|
28
27
|
return (
|
29
28
|
<ActionSheet
|
30
29
|
ref={ref}
|
31
|
-
containerStyle={tw.style("bg-background/
|
30
|
+
containerStyle={tw.style("bg-background/95", containerClassName)}
|
32
31
|
gestureEnabled
|
33
32
|
indicatorStyle={tw.style("bg-muted/15", indicatorClassName)}
|
34
33
|
{...props}
|
@@ -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 = {
|
@@ -27,9 +29,11 @@ export const Dialog = forwardRef<DialogMethods, DialogProps>(function Dialog(
|
|
27
29
|
{
|
28
30
|
containerClassName,
|
29
31
|
contentClassName,
|
30
|
-
useBlur =
|
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
|
});
|
@@ -130,7 +130,7 @@ let timeout: NodeJS.Timeout;
|
|
130
130
|
const DropdownMenu = ({
|
131
131
|
className,
|
132
132
|
children,
|
133
|
-
useBlur =
|
133
|
+
useBlur = false,
|
134
134
|
}: {
|
135
135
|
className?: string;
|
136
136
|
children: ReactNode;
|
@@ -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
|
+
};
|