@nativetail/ui 0.0.5 → 0.0.7
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/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
@@ -1,18 +1,50 @@
|
|
1
|
-
import { cn, Pressable, TextInput, View } from "@nativetail/core";
|
2
|
-
import { useCallback, useRef, useState } from "react";
|
1
|
+
import { cn, Pressable, Text, TextInput, View } from "@nativetail/core";
|
2
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
3
|
+
import { Control, Controller, Path } from "react-hook-form";
|
3
4
|
import { TextInput as NativeTextInput } from "react-native";
|
4
5
|
|
5
|
-
export type PinInputProps = {
|
6
|
-
value
|
7
|
-
onChangeText
|
6
|
+
export type PinInputProps<T = Record<string, any>> = {
|
7
|
+
value?: string;
|
8
|
+
onChangeText?: (text: string) => void;
|
8
9
|
length: number;
|
9
10
|
pinBoxClassName?: string;
|
10
11
|
pinBoxFocusedClassName?: string;
|
11
12
|
containerClassName?: string;
|
12
13
|
error?: string;
|
13
14
|
helperText?: string;
|
15
|
+
secureTextEntry?: boolean;
|
16
|
+
pinHideTime?: number;
|
17
|
+
control?: Control<T, any>;
|
18
|
+
name?: Path<T>;
|
19
|
+
};
|
20
|
+
export const PinInput = <T extends Record<string, any>>({
|
21
|
+
name,
|
22
|
+
control,
|
23
|
+
...props
|
24
|
+
}: PinInputProps<T>) => {
|
25
|
+
if (control) {
|
26
|
+
return (
|
27
|
+
<Controller
|
28
|
+
name={name}
|
29
|
+
control={control}
|
30
|
+
render={({ field, fieldState }) => {
|
31
|
+
return (
|
32
|
+
<BaseInput
|
33
|
+
{...props}
|
34
|
+
value={field.value}
|
35
|
+
onChangeText={(text) => {
|
36
|
+
field.onChange(text);
|
37
|
+
}}
|
38
|
+
error={fieldState.error?.message}
|
39
|
+
/>
|
40
|
+
);
|
41
|
+
}}
|
42
|
+
/>
|
43
|
+
);
|
44
|
+
}
|
45
|
+
return <BaseInput {...props} />;
|
14
46
|
};
|
15
|
-
|
47
|
+
function BaseInput<T extends Record<string, any>>({
|
16
48
|
value,
|
17
49
|
onChangeText,
|
18
50
|
containerClassName,
|
@@ -21,10 +53,12 @@ export function PinInput({
|
|
21
53
|
pinBoxFocusedClassName,
|
22
54
|
helperText,
|
23
55
|
length,
|
56
|
+
secureTextEntry,
|
57
|
+
pinHideTime = 300,
|
24
58
|
...props
|
25
|
-
}: PinInputProps) {
|
59
|
+
}: PinInputProps<T>) {
|
26
60
|
const [isFocused, setIsFocused] = useState(false);
|
27
|
-
const activeIndex = value
|
61
|
+
const activeIndex = value?.length == length ? length - 1 : value?.length;
|
28
62
|
const textInputRef = useRef<NativeTextInput>();
|
29
63
|
const onFocus = useCallback(() => {
|
30
64
|
setIsFocused(true);
|
@@ -34,7 +68,7 @@ export function PinInput({
|
|
34
68
|
}, [setIsFocused]);
|
35
69
|
const _handleChange = useCallback(
|
36
70
|
(text: string) => {
|
37
|
-
if (text
|
71
|
+
if (text?.length <= length) {
|
38
72
|
onChangeText(text);
|
39
73
|
}
|
40
74
|
},
|
@@ -45,31 +79,92 @@ export function PinInput({
|
|
45
79
|
textInputRef.current?.focus();
|
46
80
|
}, [textInputRef, setIsFocused]);
|
47
81
|
return (
|
48
|
-
|
49
|
-
{
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
82
|
+
<>
|
83
|
+
<View className={cn(" gap-2 flex-row w-full", containerClassName)}>
|
84
|
+
{Array.from({ length: length }).map((_, index) => (
|
85
|
+
<PinBox
|
86
|
+
key={`pininput-${index}`}
|
87
|
+
pinBoxClassName={pinBoxClassName}
|
88
|
+
isFocused={isFocused}
|
89
|
+
activeIndex={activeIndex}
|
90
|
+
index={index}
|
91
|
+
pinBoxFocusedClassName={pinBoxFocusedClassName}
|
92
|
+
onPinBoxPress={onPinBoxPress}
|
93
|
+
secureTextEntry={secureTextEntry}
|
94
|
+
value={value}
|
95
|
+
pinHideTime={pinHideTime}
|
96
|
+
/>
|
97
|
+
))}
|
98
|
+
<TextInput
|
99
|
+
ref={textInputRef}
|
100
|
+
value={value}
|
101
|
+
onChangeText={_handleChange}
|
102
|
+
onFocus={onFocus}
|
103
|
+
onBlur={onBlur}
|
104
|
+
className="opacity-0 scale-0 absolute"
|
105
|
+
{...props}
|
106
|
+
/>
|
107
|
+
</View>
|
108
|
+
{error && <Text className="text-sm text-danger">{error}</Text>}
|
109
|
+
</>
|
74
110
|
);
|
75
111
|
}
|
112
|
+
|
113
|
+
const PinBox = ({
|
114
|
+
pinBoxClassName,
|
115
|
+
activeIndex,
|
116
|
+
index,
|
117
|
+
onPinBoxPress,
|
118
|
+
value,
|
119
|
+
isFocused,
|
120
|
+
pinBoxFocusedClassName,
|
121
|
+
secureTextEntry,
|
122
|
+
pinHideTime,
|
123
|
+
}: {
|
124
|
+
pinBoxClassName?: string;
|
125
|
+
isFocused?: boolean;
|
126
|
+
activeIndex: number;
|
127
|
+
index: number;
|
128
|
+
pinBoxFocusedClassName?: string;
|
129
|
+
onPinBoxPress: () => void;
|
130
|
+
value: string;
|
131
|
+
secureTextEntry?: boolean;
|
132
|
+
pinHideTime?: number;
|
133
|
+
}) => {
|
134
|
+
const pinValue = value?.[index];
|
135
|
+
const isActive = activeIndex === index;
|
136
|
+
const [hide, setHide] = useState(false);
|
137
|
+
const timeoutRef = useRef<NodeJS.Timeout>(null);
|
138
|
+
useEffect(() => {
|
139
|
+
if (!secureTextEntry) return;
|
140
|
+
if (pinValue) {
|
141
|
+
timeoutRef.current = setTimeout(() => {
|
142
|
+
setHide(true);
|
143
|
+
}, pinHideTime);
|
144
|
+
} else {
|
145
|
+
setHide(false);
|
146
|
+
}
|
147
|
+
return () => {
|
148
|
+
if (timeoutRef.current) clearTimeout(timeoutRef.current);
|
149
|
+
};
|
150
|
+
}, [secureTextEntry, pinValue, pinHideTime]);
|
151
|
+
return (
|
152
|
+
<Pressable
|
153
|
+
className={cn(
|
154
|
+
"p-2 bg-card rounded-lg items-center justify-center w-full font-medium aspect-sqaure flex-1 border border-muted/15 h-16 text-foreground text-[16px] text-center",
|
155
|
+
pinBoxClassName,
|
156
|
+
isFocused &&
|
157
|
+
isActive &&
|
158
|
+
"border-foreground" + " " + pinBoxFocusedClassName
|
159
|
+
)}
|
160
|
+
onPress={onPinBoxPress}
|
161
|
+
onFocus={onPinBoxPress}
|
162
|
+
>
|
163
|
+
{hide ? (
|
164
|
+
<View className="w-2 h-2 rounded-full bg-foreground" />
|
165
|
+
) : (
|
166
|
+
pinValue
|
167
|
+
)}
|
168
|
+
</Pressable>
|
169
|
+
);
|
170
|
+
};
|
@@ -1,141 +1,2 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
import { memo, useCallback, useMemo } from "react";
|
4
|
-
import { Iconify } from "react-native-iconify";
|
5
|
-
|
6
|
-
type SelectProps = PressableProps & {
|
7
|
-
containerClassName?: string;
|
8
|
-
label: string;
|
9
|
-
error?: string;
|
10
|
-
helperText?: string;
|
11
|
-
value: string;
|
12
|
-
onChange: (value: string) => void;
|
13
|
-
placeholder?: string;
|
14
|
-
options: {
|
15
|
-
label: string;
|
16
|
-
value: string;
|
17
|
-
icon?: React.ReactNode;
|
18
|
-
}[];
|
19
|
-
};
|
20
|
-
export function Select({
|
21
|
-
containerClassName,
|
22
|
-
label,
|
23
|
-
error,
|
24
|
-
className,
|
25
|
-
value,
|
26
|
-
onChange,
|
27
|
-
helperText,
|
28
|
-
placeholder,
|
29
|
-
options,
|
30
|
-
...props
|
31
|
-
}: SelectProps) {
|
32
|
-
const tw = useTw();
|
33
|
-
const renderOptions = useCallback(() => {
|
34
|
-
return options.map((option, index) => (
|
35
|
-
<SelectItem
|
36
|
-
label={option.label}
|
37
|
-
value={option.value}
|
38
|
-
icon={option.icon}
|
39
|
-
onChange={onChange}
|
40
|
-
isActive={value === option.value}
|
41
|
-
key={option.value}
|
42
|
-
first={index === 0}
|
43
|
-
last={index === options.length - 1}
|
44
|
-
/>
|
45
|
-
));
|
46
|
-
}, [options, value, onChange, tw]);
|
47
|
-
return (
|
48
|
-
<View className={cn("w-full gap-1", containerClassName)}>
|
49
|
-
<Text className={cn("text-muted/75 duration-75 ")}>{label}</Text>
|
50
|
-
|
51
|
-
<Dropdown.Root>
|
52
|
-
<SelectTrigger
|
53
|
-
options={options}
|
54
|
-
value={value}
|
55
|
-
placeholder={placeholder}
|
56
|
-
{...props}
|
57
|
-
/>
|
58
|
-
<Dropdown.Menu>{renderOptions()}</Dropdown.Menu>
|
59
|
-
</Dropdown.Root>
|
60
|
-
</View>
|
61
|
-
);
|
62
|
-
}
|
63
|
-
const SelectTrigger = memo(
|
64
|
-
({
|
65
|
-
options,
|
66
|
-
className,
|
67
|
-
value,
|
68
|
-
placeholder,
|
69
|
-
...props
|
70
|
-
}: Partial<SelectProps>) => {
|
71
|
-
const selectedOption = useMemo(
|
72
|
-
() => options.find((option) => option.value === value),
|
73
|
-
[value]
|
74
|
-
);
|
75
|
-
const tw = useTw();
|
76
|
-
return (
|
77
|
-
<Dropdown.Trigger
|
78
|
-
className={cn(
|
79
|
-
"p-3 bg-card rounded-lg w-full border flex-row items-center justify-between border-muted/15 h-14 text-foreground -z-5 text-[16px]",
|
80
|
-
className
|
81
|
-
)}
|
82
|
-
{...props}
|
83
|
-
>
|
84
|
-
{selectedOption && (
|
85
|
-
<Text className="text-foreground">{selectedOption.label}</Text>
|
86
|
-
)}
|
87
|
-
{!selectedOption && placeholder && (
|
88
|
-
<Text className="text-muted">{placeholder}</Text>
|
89
|
-
)}
|
90
|
-
<Iconify
|
91
|
-
icon="solar:alt-arrow-down-outline"
|
92
|
-
size={20}
|
93
|
-
color={tw.color("foreground")}
|
94
|
-
/>
|
95
|
-
</Dropdown.Trigger>
|
96
|
-
);
|
97
|
-
}
|
98
|
-
);
|
99
|
-
|
100
|
-
const SelectItem = memo(
|
101
|
-
({
|
102
|
-
label,
|
103
|
-
value,
|
104
|
-
icon,
|
105
|
-
onChange,
|
106
|
-
isActive,
|
107
|
-
first,
|
108
|
-
last,
|
109
|
-
}: {
|
110
|
-
label: string;
|
111
|
-
value: string;
|
112
|
-
icon?: React.ReactNode;
|
113
|
-
|
114
|
-
onChange: (value: string) => void;
|
115
|
-
isActive?: boolean;
|
116
|
-
first?: boolean;
|
117
|
-
last?: boolean;
|
118
|
-
}) => {
|
119
|
-
const tw = useTw();
|
120
|
-
return (
|
121
|
-
<Dropdown.Item
|
122
|
-
key={value}
|
123
|
-
onPress={() => onChange(isActive ? "" : value)}
|
124
|
-
first={first}
|
125
|
-
last={last}
|
126
|
-
>
|
127
|
-
<View className="flex-row items-center gap-2">
|
128
|
-
<Text className="text-sm text-foreground">{label}</Text>
|
129
|
-
{icon}
|
130
|
-
</View>
|
131
|
-
{isActive && (
|
132
|
-
<Iconify
|
133
|
-
icon="lucide:check"
|
134
|
-
size={16}
|
135
|
-
color={tw.color("foreground")}
|
136
|
-
/>
|
137
|
-
)}
|
138
|
-
</Dropdown.Item>
|
139
|
-
);
|
140
|
-
}
|
141
|
-
);
|
1
|
+
export * from "./select";
|
2
|
+
export * from "./multi-select";
|
@@ -0,0 +1,208 @@
|
|
1
|
+
import {
|
2
|
+
cn,
|
3
|
+
Pressable,
|
4
|
+
PressableProps,
|
5
|
+
Text,
|
6
|
+
useTw,
|
7
|
+
View,
|
8
|
+
} from "@nativetail/core";
|
9
|
+
import { Dropdown } from "../dropdown";
|
10
|
+
import { memo, useCallback, useMemo } from "react";
|
11
|
+
import { Iconify } from "react-native-iconify";
|
12
|
+
import { Control, Controller, Path } from "react-hook-form";
|
13
|
+
|
14
|
+
type SelectProps<T extends Record<string, any>> = PressableProps & {
|
15
|
+
containerClassName?: string;
|
16
|
+
label?: string;
|
17
|
+
error?: string;
|
18
|
+
helperText?: string;
|
19
|
+
value?: string[];
|
20
|
+
onChange?: (value: string[]) => void;
|
21
|
+
placeholder?: string;
|
22
|
+
options: {
|
23
|
+
label: string;
|
24
|
+
value: string;
|
25
|
+
icon?: React.ReactNode;
|
26
|
+
}[];
|
27
|
+
control?: Control<T, any>;
|
28
|
+
name?: Path<T>;
|
29
|
+
};
|
30
|
+
export const MultiSelect = <T extends Record<string, any>>({
|
31
|
+
name,
|
32
|
+
control,
|
33
|
+
...props
|
34
|
+
}: SelectProps<T>) => {
|
35
|
+
if (control) {
|
36
|
+
return (
|
37
|
+
<Controller
|
38
|
+
name={name}
|
39
|
+
control={control}
|
40
|
+
render={({ field, fieldState }) => {
|
41
|
+
return (
|
42
|
+
<BaseSelect
|
43
|
+
{...props}
|
44
|
+
value={field.value}
|
45
|
+
onChange={(text) => {
|
46
|
+
field.onChange(text);
|
47
|
+
}}
|
48
|
+
error={fieldState.error?.message}
|
49
|
+
/>
|
50
|
+
);
|
51
|
+
}}
|
52
|
+
/>
|
53
|
+
);
|
54
|
+
}
|
55
|
+
return <BaseSelect {...props} />;
|
56
|
+
};
|
57
|
+
function BaseSelect<T extends Record<string, any>>({
|
58
|
+
containerClassName,
|
59
|
+
label,
|
60
|
+
error,
|
61
|
+
className,
|
62
|
+
value,
|
63
|
+
onChange,
|
64
|
+
helperText,
|
65
|
+
placeholder,
|
66
|
+
options,
|
67
|
+
...props
|
68
|
+
}: SelectProps<T>) {
|
69
|
+
const tw = useTw();
|
70
|
+
const renderOptions = useCallback(() => {
|
71
|
+
return options.map((option, index) => (
|
72
|
+
<SelectItem
|
73
|
+
label={option.label}
|
74
|
+
value={option.value}
|
75
|
+
icon={option.icon}
|
76
|
+
onChange={(val) => {
|
77
|
+
if (!val) onChange(value.filter((v) => v != option.value));
|
78
|
+
else onChange([...(value || []), val]);
|
79
|
+
}}
|
80
|
+
isActive={value?.includes?.(option.value)}
|
81
|
+
key={option.value}
|
82
|
+
first={index === 0}
|
83
|
+
last={index === options.length - 1}
|
84
|
+
/>
|
85
|
+
));
|
86
|
+
}, [options, value, onChange, tw]);
|
87
|
+
return (
|
88
|
+
<View className={cn("w-full gap-1", containerClassName)}>
|
89
|
+
<Text className={cn("text-muted/75 duration-75 ")}>{label}</Text>
|
90
|
+
|
91
|
+
<Dropdown.Root>
|
92
|
+
<SelectTrigger
|
93
|
+
options={options}
|
94
|
+
value={value}
|
95
|
+
placeholder={placeholder}
|
96
|
+
onChange={onChange}
|
97
|
+
error={error}
|
98
|
+
{...props}
|
99
|
+
/>
|
100
|
+
<Dropdown.Menu>{renderOptions()}</Dropdown.Menu>
|
101
|
+
</Dropdown.Root>
|
102
|
+
</View>
|
103
|
+
);
|
104
|
+
}
|
105
|
+
const SelectTrigger = memo(
|
106
|
+
<T extends Record<string, any>>({
|
107
|
+
options,
|
108
|
+
className,
|
109
|
+
value,
|
110
|
+
placeholder,
|
111
|
+
onChange,
|
112
|
+
error,
|
113
|
+
...props
|
114
|
+
}: PressableProps & {
|
115
|
+
options: SelectProps<T>["options"];
|
116
|
+
value: SelectProps<T>["value"];
|
117
|
+
onChange: SelectProps<T>["onChange"];
|
118
|
+
placeholder: SelectProps<T>["placeholder"];
|
119
|
+
error: SelectProps<T>["error"];
|
120
|
+
}) => {
|
121
|
+
const selectedOptions = useMemo(
|
122
|
+
() => options.filter((option) => value?.includes?.(option.value)),
|
123
|
+
[value]
|
124
|
+
);
|
125
|
+
const tw = useTw();
|
126
|
+
return (
|
127
|
+
<>
|
128
|
+
<Dropdown.Trigger
|
129
|
+
className={cn(
|
130
|
+
"p-3 bg-card rounded-lg w-full border flex-row items-center justify-between border-muted/15 h-14 text-foreground -z-5 text-[16px]",
|
131
|
+
className
|
132
|
+
)}
|
133
|
+
{...props}
|
134
|
+
>
|
135
|
+
{selectedOptions.length > 0 && (
|
136
|
+
<View className="flex-row gap-2 flex-wrap">
|
137
|
+
{selectedOptions.map((option) => (
|
138
|
+
<Pressable
|
139
|
+
className="bg-muted/8 rounded-2xl text-sm px-2 py-1"
|
140
|
+
key={option.value}
|
141
|
+
onPress={() => {
|
142
|
+
onChange(value.filter((val) => val != option.value));
|
143
|
+
}}
|
144
|
+
>
|
145
|
+
{option.label}
|
146
|
+
</Pressable>
|
147
|
+
))}
|
148
|
+
</View>
|
149
|
+
)}
|
150
|
+
{selectedOptions.length == 0 && placeholder && (
|
151
|
+
<Text className="text-muted">{placeholder}</Text>
|
152
|
+
)}
|
153
|
+
<Iconify
|
154
|
+
icon="solar:alt-arrow-down-outline"
|
155
|
+
size={20}
|
156
|
+
color={tw.color("foreground")}
|
157
|
+
/>
|
158
|
+
</Dropdown.Trigger>
|
159
|
+
|
160
|
+
{error && <Text className="text-sm text-danger">{error}</Text>}
|
161
|
+
</>
|
162
|
+
);
|
163
|
+
}
|
164
|
+
);
|
165
|
+
|
166
|
+
const SelectItem = memo(
|
167
|
+
({
|
168
|
+
label,
|
169
|
+
value,
|
170
|
+
icon,
|
171
|
+
onChange,
|
172
|
+
isActive,
|
173
|
+
first,
|
174
|
+
last,
|
175
|
+
}: {
|
176
|
+
label: string;
|
177
|
+
value: string;
|
178
|
+
icon?: React.ReactNode;
|
179
|
+
|
180
|
+
onChange: (value: string) => void;
|
181
|
+
isActive?: boolean;
|
182
|
+
first?: boolean;
|
183
|
+
last?: boolean;
|
184
|
+
}) => {
|
185
|
+
const tw = useTw();
|
186
|
+
return (
|
187
|
+
<Dropdown.Item
|
188
|
+
key={value}
|
189
|
+
onPress={() => onChange(isActive ? "" : value)}
|
190
|
+
first={first}
|
191
|
+
last={last}
|
192
|
+
autoClosable={false}
|
193
|
+
>
|
194
|
+
<View className="flex-row items-center gap-2">
|
195
|
+
<Text className="text-sm text-foreground">{label}</Text>
|
196
|
+
{icon}
|
197
|
+
</View>
|
198
|
+
{isActive && (
|
199
|
+
<Iconify
|
200
|
+
icon="lucide:check"
|
201
|
+
size={16}
|
202
|
+
color={tw.color("foreground")}
|
203
|
+
/>
|
204
|
+
)}
|
205
|
+
</Dropdown.Item>
|
206
|
+
);
|
207
|
+
}
|
208
|
+
);
|