@nativetail/ui 0.0.8 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- package/package.json +5 -5
- package/src/components/bottom-sheet/index.tsx +2 -2
- package/src/components/form-builder/index.tsx +141 -0
- package/src/components/index.ts +1 -0
- package/src/components/input/index.ts +1 -0
- package/src/components/input/input.tsx +85 -23
- package/src/components/input/phone-input.tsx +242 -0
- package/src/components/select/multi-select.tsx +8 -8
- package/src/components/select/select.tsx +1 -1
- package/src/components/toast/index.tsx +33 -3
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "@nativetail/ui",
|
3
|
-
"version": "0.0
|
3
|
+
"version": "0.1.0",
|
4
4
|
"description": "",
|
5
5
|
"main": "src/index.ts",
|
6
6
|
"scripts": {},
|
@@ -26,12 +26,13 @@
|
|
26
26
|
"react-native": ">=0.63.0"
|
27
27
|
},
|
28
28
|
"dependencies": {
|
29
|
-
"@
|
29
|
+
"@hookform/resolvers": "^3.6.0",
|
30
|
+
"@nativetail/core": "^0.0.2",
|
30
31
|
"@shopify/flash-list": "^1.7.0",
|
32
|
+
"countries-list": "^3.1.0",
|
31
33
|
"expo-blur": "^13.0.2",
|
32
34
|
"expo-linear-gradient": "~13.0.2",
|
33
35
|
"moti": "^0.29.0",
|
34
|
-
"@nativetail/core": "*",
|
35
36
|
"react": "18.2.0",
|
36
37
|
"react-hook-form": "^7.51.0",
|
37
38
|
"react-native": "0.74.3",
|
@@ -40,8 +41,7 @@
|
|
40
41
|
"react-native-reanimated": "~3.10.1",
|
41
42
|
"react-native-safe-area-context": "4.10.1",
|
42
43
|
"tailwind-merge": "^2.3.0",
|
43
|
-
"zustand": "^4.5.2"
|
44
|
-
"@hookform/resolvers": "^3.6.0"
|
44
|
+
"zustand": "^4.5.2"
|
45
45
|
},
|
46
46
|
"devDependencies": {
|
47
47
|
"metro-react-native-babel-preset": "^0.77.0",
|
@@ -11,6 +11,7 @@ export type BottomSheetProps = ActionSheetProps & {
|
|
11
11
|
indicatorClassName?: string;
|
12
12
|
useBlur?: boolean;
|
13
13
|
};
|
14
|
+
export type BottomSheetRef = ActionSheetRef;
|
14
15
|
export const BottomSheet = forwardRef<ActionSheetRef, BottomSheetProps>(
|
15
16
|
function BottomSheet(
|
16
17
|
{
|
@@ -27,7 +28,7 @@ export const BottomSheet = forwardRef<ActionSheetRef, BottomSheetProps>(
|
|
27
28
|
return (
|
28
29
|
<ActionSheet
|
29
30
|
ref={ref}
|
30
|
-
containerStyle={tw.style("bg-background
|
31
|
+
containerStyle={tw.style("bg-background", containerClassName)}
|
31
32
|
gestureEnabled
|
32
33
|
indicatorStyle={tw.style("bg-muted/15", indicatorClassName)}
|
33
34
|
{...props}
|
@@ -42,4 +43,3 @@ export const BottomSheet = forwardRef<ActionSheetRef, BottomSheetProps>(
|
|
42
43
|
);
|
43
44
|
}
|
44
45
|
);
|
45
|
-
export { ActionSheetRef as BottomSheetRef };
|
@@ -0,0 +1,141 @@
|
|
1
|
+
import { zodResolver } from "@hookform/resolvers/zod";
|
2
|
+
import { cn, TextInputProps, View } from "@nativetail/core";
|
3
|
+
import React, { ComponentPropsWithoutRef } from "react";
|
4
|
+
import { Control, DefaultValues, useForm } from "react-hook-form";
|
5
|
+
import { z } from "zod";
|
6
|
+
import { Button } from "../button";
|
7
|
+
import {
|
8
|
+
FloatingInput,
|
9
|
+
FloatingInputProps,
|
10
|
+
Input,
|
11
|
+
InputProps,
|
12
|
+
PhoneInput,
|
13
|
+
PinInput,
|
14
|
+
PinInputProps,
|
15
|
+
} from "../input";
|
16
|
+
import { MultiSelect, MultiSelectProps, Select, SelectProps } from "../select";
|
17
|
+
|
18
|
+
export type FormBuilderProps<
|
19
|
+
T extends z.ZodRawShape,
|
20
|
+
IValues = z.infer<z.ZodObject<T>>,
|
21
|
+
> = {
|
22
|
+
schema: z.ZodObject<T>;
|
23
|
+
inputs?: Partial<
|
24
|
+
Record<
|
25
|
+
keyof T,
|
26
|
+
{
|
27
|
+
render?: (props: {
|
28
|
+
control: Control<IValues, any>;
|
29
|
+
name: keyof T;
|
30
|
+
}) => React.ReactElement;
|
31
|
+
} & InputType
|
32
|
+
>
|
33
|
+
>;
|
34
|
+
containerClassname?: string;
|
35
|
+
submitButtonProps?: ComponentPropsWithoutRef<typeof Button>;
|
36
|
+
onSubmit?: (values: IValues) => void;
|
37
|
+
onError?: (values: Partial<Record<keyof T, any>>) => void;
|
38
|
+
isSubmitting?: boolean;
|
39
|
+
defaultValues?: DefaultValues<IValues>;
|
40
|
+
};
|
41
|
+
export function FormBuilder<T extends z.ZodRawShape>({
|
42
|
+
schema,
|
43
|
+
inputs,
|
44
|
+
containerClassname,
|
45
|
+
submitButtonProps,
|
46
|
+
onSubmit,
|
47
|
+
onError,
|
48
|
+
isSubmitting,
|
49
|
+
defaultValues,
|
50
|
+
}: FormBuilderProps<T>) {
|
51
|
+
const shape = schema.shape;
|
52
|
+
const keys = Object.keys(shape);
|
53
|
+
type FormSchemaType = z.infer<typeof schema>;
|
54
|
+
const form = useForm<FormSchemaType>({
|
55
|
+
resolver: zodResolver(schema),
|
56
|
+
defaultValues: defaultValues,
|
57
|
+
});
|
58
|
+
return (
|
59
|
+
<View className={cn("gap-2", containerClassname)}>
|
60
|
+
{keys.map((inputKey) => {
|
61
|
+
const Input = inputs[inputKey];
|
62
|
+
const Render = Input.render;
|
63
|
+
if (!Render) {
|
64
|
+
return (
|
65
|
+
<InputComponent
|
66
|
+
control={form.control}
|
67
|
+
name={inputKey}
|
68
|
+
{...Input}
|
69
|
+
key={inputKey}
|
70
|
+
/>
|
71
|
+
);
|
72
|
+
}
|
73
|
+
return <Render control={form.control} name={inputKey} key={inputKey} />;
|
74
|
+
})}
|
75
|
+
<Button
|
76
|
+
className="w-full"
|
77
|
+
children={"Submit"}
|
78
|
+
{...submitButtonProps}
|
79
|
+
onPress={form.handleSubmit(onSubmit, onError)}
|
80
|
+
isLoading={isSubmitting}
|
81
|
+
/>
|
82
|
+
</View>
|
83
|
+
);
|
84
|
+
}
|
85
|
+
const InputComponent = ({
|
86
|
+
control,
|
87
|
+
name,
|
88
|
+
props,
|
89
|
+
type,
|
90
|
+
}: {
|
91
|
+
control: Control<any>;
|
92
|
+
name: string;
|
93
|
+
} & InputType) => {
|
94
|
+
switch (type) {
|
95
|
+
case "text":
|
96
|
+
return <Input control={control} name={name} {...props} />;
|
97
|
+
case "floating":
|
98
|
+
return <FloatingInput control={control} name={name} {...props} />;
|
99
|
+
case "select":
|
100
|
+
return <Select control={control} name={name} {...props} />;
|
101
|
+
case "multi-select":
|
102
|
+
return <MultiSelect control={control} name={name} {...props} />;
|
103
|
+
case "pin":
|
104
|
+
return <PinInput control={control} name={name} {...props} />;
|
105
|
+
case "phone":
|
106
|
+
return <PhoneInput control={control} name={name} {...props} />;
|
107
|
+
}
|
108
|
+
return null;
|
109
|
+
};
|
110
|
+
|
111
|
+
type TextInputType = {
|
112
|
+
type: "text";
|
113
|
+
props: InputProps;
|
114
|
+
};
|
115
|
+
type FloatingInputType = {
|
116
|
+
type: "floating";
|
117
|
+
props: FloatingInputProps;
|
118
|
+
};
|
119
|
+
type SelectInputType = {
|
120
|
+
type: "select";
|
121
|
+
props: SelectProps;
|
122
|
+
};
|
123
|
+
type MultiSelectInputType = {
|
124
|
+
type: "multi-select";
|
125
|
+
props: MultiSelectProps;
|
126
|
+
};
|
127
|
+
type PinInputType = {
|
128
|
+
type: "pin";
|
129
|
+
props: PinInputProps;
|
130
|
+
};
|
131
|
+
type PhoneInputType = {
|
132
|
+
type: "phone";
|
133
|
+
props: TextInputProps;
|
134
|
+
};
|
135
|
+
type InputType =
|
136
|
+
| TextInputType
|
137
|
+
| FloatingInputType
|
138
|
+
| SelectInputType
|
139
|
+
| MultiSelectInputType
|
140
|
+
| PinInputType
|
141
|
+
| PhoneInputType;
|
package/src/components/index.ts
CHANGED
@@ -1,12 +1,14 @@
|
|
1
1
|
import {
|
2
2
|
cn,
|
3
|
+
Pressable,
|
3
4
|
Text,
|
4
5
|
TextInput,
|
5
6
|
TextInputProps,
|
6
7
|
useTw,
|
7
8
|
View,
|
8
9
|
} from "@nativetail/core";
|
9
|
-
import
|
10
|
+
import { MotiView } from "moti";
|
11
|
+
import React, { createRef, useState } from "react";
|
10
12
|
import { Control, Controller, Path } from "react-hook-form";
|
11
13
|
import { TextInput as NativeTextInput } from "react-native";
|
12
14
|
import ShowPassword from "./show-password";
|
@@ -22,7 +24,9 @@ export type InputProps<T = Record<string, any>> = TextInputProps & {
|
|
22
24
|
value?: string;
|
23
25
|
control?: Control<T, any>;
|
24
26
|
name?: Path<T>;
|
25
|
-
inputRef?:
|
27
|
+
inputRef?: React.RefObject<NativeTextInput>;
|
28
|
+
inputContainerClassName?: string;
|
29
|
+
formatter?: (value: string) => string;
|
26
30
|
};
|
27
31
|
|
28
32
|
export const Input = <T extends Record<string, any>>({
|
@@ -64,7 +68,9 @@ const BaseInput = <T extends Record<string, any>>({
|
|
64
68
|
rightElement,
|
65
69
|
helperText,
|
66
70
|
leftElement,
|
67
|
-
inputRef,
|
71
|
+
inputRef = createRef<NativeTextInput>(),
|
72
|
+
inputContainerClassName,
|
73
|
+
formatter,
|
68
74
|
...props
|
69
75
|
}: InputProps<T>) => {
|
70
76
|
const tw = useTw();
|
@@ -72,34 +78,65 @@ const BaseInput = <T extends Record<string, any>>({
|
|
72
78
|
const [showPassword, setShowPassword] = useState(
|
73
79
|
isSecretToggleable ? false : true
|
74
80
|
);
|
81
|
+
const [isFocused, setIsFocused] = useState(false);
|
75
82
|
return (
|
76
83
|
<View className={cn("w-full gap-1", containerClassName)}>
|
77
84
|
<Text className={cn("text-muted/75 duration-75 text-sm")}>{label}</Text>
|
78
85
|
|
79
|
-
<
|
80
|
-
value={value}
|
81
|
-
onChangeText={onChangeText}
|
82
|
-
ref={inputRef}
|
86
|
+
<View
|
83
87
|
className={cn(
|
84
|
-
"
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
88
|
+
" bg-card flex-row h-12 rounded-lg w-full border border-muted/15 ",
|
89
|
+
inputContainerClassName,
|
90
|
+
error && "border-danger",
|
91
|
+
isFocused && "border-foreground"
|
92
|
+
)}
|
93
|
+
>
|
94
|
+
{leftElement}
|
95
|
+
<TextInput
|
96
|
+
value={value}
|
97
|
+
onChangeText={onChangeText}
|
98
|
+
ref={inputRef}
|
99
|
+
className={cn(
|
100
|
+
" text-foreground p-2 rounded-lg flex-1 text-[16px]",
|
101
|
+
formatter && "opacity-0 scale-0 flex-0 absolute",
|
102
|
+
className
|
103
|
+
)}
|
104
|
+
placeholderTextColor={tw.color("muted")}
|
105
|
+
secureTextEntry={!showPassword}
|
106
|
+
{...props}
|
107
|
+
onFocus={(e) => {
|
108
|
+
setIsFocused(true);
|
109
|
+
props?.onFocus?.(e);
|
110
|
+
}}
|
111
|
+
onBlur={(e) => {
|
112
|
+
setIsFocused(false);
|
113
|
+
props?.onBlur?.(e);
|
114
|
+
}}
|
115
|
+
/>
|
116
|
+
{formatter && (
|
117
|
+
<Pressable
|
118
|
+
className={cn(
|
119
|
+
" text-foreground p-2 flex-row rounded-lg text-left items-center h-full flex-1 text-[16px]",
|
120
|
+
className
|
121
|
+
)}
|
122
|
+
onPress={() => {
|
123
|
+
inputRef?.current?.focus();
|
124
|
+
}}
|
125
|
+
>
|
126
|
+
{formatter(value)}
|
127
|
+
{isFocused && <InputBlink />}
|
128
|
+
{!value && (
|
129
|
+
<Text className={cn("text-muted/75 absolute left-0 p-2")}>
|
130
|
+
{props.placeholder}
|
131
|
+
</Text>
|
132
|
+
)}
|
133
|
+
</Pressable>
|
89
134
|
)}
|
90
|
-
placeholderTextColor={tw.color("muted")}
|
91
|
-
secureTextEntry={!showPassword}
|
92
|
-
{...props}
|
93
|
-
/>
|
94
|
-
{helperText && <Text className="text-muted text-sm">{helperText}</Text>}
|
95
135
|
|
96
|
-
|
97
|
-
|
98
|
-
|
136
|
+
{rightElement}
|
137
|
+
</View>
|
138
|
+
{helperText && <Text className="text-muted text-sm">{helperText}</Text>}
|
99
139
|
|
100
|
-
{rightElement && (
|
101
|
-
<View className="absolute right-2 bottom-2">{rightElement}</View>
|
102
|
-
)}
|
103
140
|
{error && <Text className="text-danger text-sm">{error}</Text>}
|
104
141
|
|
105
142
|
{isSecretToggleable && (
|
@@ -111,3 +148,28 @@ const BaseInput = <T extends Record<string, any>>({
|
|
111
148
|
</View>
|
112
149
|
);
|
113
150
|
};
|
151
|
+
|
152
|
+
const InputBlink = () => {
|
153
|
+
const tw = useTw();
|
154
|
+
return (
|
155
|
+
<MotiView
|
156
|
+
style={{
|
157
|
+
width: 0.1,
|
158
|
+
height: "90%",
|
159
|
+
opacity: 1,
|
160
|
+
marginHorizontal: 0.8,
|
161
|
+
backgroundColor: tw.color("foreground/85"),
|
162
|
+
}}
|
163
|
+
from={{
|
164
|
+
opacity: 0,
|
165
|
+
}}
|
166
|
+
animate={{
|
167
|
+
opacity: 1,
|
168
|
+
}}
|
169
|
+
transition={{
|
170
|
+
duration: 650,
|
171
|
+
loop: true,
|
172
|
+
}}
|
173
|
+
/>
|
174
|
+
);
|
175
|
+
};
|
@@ -0,0 +1,242 @@
|
|
1
|
+
import { cn, Pressable, Text, View } from "@nativetail/core";
|
2
|
+
import { memo, useCallback, useMemo, useRef, useState } from "react";
|
3
|
+
import { Controller } from "react-hook-form";
|
4
|
+
import { FlashList, FlatList } from "react-native-actions-sheet";
|
5
|
+
import { Iconify } from "react-native-iconify";
|
6
|
+
import { BottomSheet, BottomSheetRef } from "../bottom-sheet";
|
7
|
+
import { Input, InputProps } from "./input";
|
8
|
+
|
9
|
+
import { countries, ICountry } from "countries-list";
|
10
|
+
import { FloatingInput } from "./floating-input";
|
11
|
+
type CountryType = ICountry & {
|
12
|
+
code: string;
|
13
|
+
};
|
14
|
+
export const PhoneInput = <T extends Record<string, any>>({
|
15
|
+
name,
|
16
|
+
control,
|
17
|
+
...props
|
18
|
+
}: InputProps<T>) => {
|
19
|
+
if (control) {
|
20
|
+
return (
|
21
|
+
<Controller
|
22
|
+
name={name}
|
23
|
+
control={control}
|
24
|
+
render={({ field, fieldState }) => {
|
25
|
+
return (
|
26
|
+
<BaseInput
|
27
|
+
{...props}
|
28
|
+
value={field.value}
|
29
|
+
onChangeText={(text) => {
|
30
|
+
field.onChange(text);
|
31
|
+
}}
|
32
|
+
error={fieldState.error?.message}
|
33
|
+
/>
|
34
|
+
);
|
35
|
+
}}
|
36
|
+
/>
|
37
|
+
);
|
38
|
+
}
|
39
|
+
return <BaseInput {...props} />;
|
40
|
+
};
|
41
|
+
|
42
|
+
const BaseInput = <T extends Record<string, any>>(props: InputProps<T>) => {
|
43
|
+
const [selectedCountry, setSelectedCountry] = useState<CountryType>({
|
44
|
+
...countries.US,
|
45
|
+
code: "US",
|
46
|
+
});
|
47
|
+
return (
|
48
|
+
<Input
|
49
|
+
{...props}
|
50
|
+
leftElement={
|
51
|
+
<SelectCountry
|
52
|
+
selectedCountry={selectedCountry}
|
53
|
+
setSelectedCountry={setSelectedCountry}
|
54
|
+
/>
|
55
|
+
}
|
56
|
+
placeholder="(123) 456-7890"
|
57
|
+
value={props.value}
|
58
|
+
formatter={(value) => {
|
59
|
+
// Remove all non-numeric characters
|
60
|
+
const cleaned = ("" + value).replace(/\D/g, "");
|
61
|
+
|
62
|
+
// Format the cleaned number
|
63
|
+
const match = cleaned.match(/^(\d{3})(\d{3})(\d{4})$/);
|
64
|
+
if (match) {
|
65
|
+
return `(${match[1]}) ${match[2]}-${match[3]}`;
|
66
|
+
}
|
67
|
+
|
68
|
+
return value;
|
69
|
+
}}
|
70
|
+
onChangeText={(text) => {
|
71
|
+
props.onChangeText?.(text);
|
72
|
+
}}
|
73
|
+
/>
|
74
|
+
);
|
75
|
+
};
|
76
|
+
|
77
|
+
const SelectCountry = ({
|
78
|
+
selectedCountry,
|
79
|
+
setSelectedCountry,
|
80
|
+
}: {
|
81
|
+
selectedCountry: CountryType;
|
82
|
+
setSelectedCountry: (country: CountryType) => void;
|
83
|
+
}) => {
|
84
|
+
const sheetRef = useRef<BottomSheetRef>(null);
|
85
|
+
const flag = useMemo(
|
86
|
+
() => selectedCountry && getFlagEmoji(selectedCountry.code),
|
87
|
+
[selectedCountry]
|
88
|
+
);
|
89
|
+
return (
|
90
|
+
<>
|
91
|
+
<Pressable
|
92
|
+
className=" p-2 h-full items-center justify-center flex-row gap-0.5 active:opacity-75 opacity-100 border-r border-muted/15 pr-1"
|
93
|
+
onPress={() => {
|
94
|
+
sheetRef.current?.show();
|
95
|
+
}}
|
96
|
+
>
|
97
|
+
<Text>
|
98
|
+
<Text className="mr-1">{flag}</Text>+{selectedCountry.phone?.[0]}
|
99
|
+
</Text>
|
100
|
+
<Iconify icon="mingcute:down-line" size={24} color="gray" />
|
101
|
+
</Pressable>
|
102
|
+
<CountryBottomSheet
|
103
|
+
selectedCountry={selectedCountry}
|
104
|
+
setSelectedCountry={setSelectedCountry}
|
105
|
+
sheetRef={sheetRef}
|
106
|
+
/>
|
107
|
+
</>
|
108
|
+
);
|
109
|
+
};
|
110
|
+
|
111
|
+
const CountryBottomSheet = ({
|
112
|
+
selectedCountry,
|
113
|
+
setSelectedCountry,
|
114
|
+
sheetRef,
|
115
|
+
}: {
|
116
|
+
selectedCountry: CountryType;
|
117
|
+
setSelectedCountry: (code: CountryType) => void;
|
118
|
+
sheetRef: React.RefObject<BottomSheetRef>;
|
119
|
+
}) => {
|
120
|
+
const [search, setSearch] = useState("");
|
121
|
+
const [open, setOpen] = useState(false);
|
122
|
+
const allCountries = useMemo(
|
123
|
+
() =>
|
124
|
+
Object.entries(countries)
|
125
|
+
.map(([code, country]) => {
|
126
|
+
return {
|
127
|
+
code,
|
128
|
+
name: country.name,
|
129
|
+
...country,
|
130
|
+
};
|
131
|
+
})
|
132
|
+
.filter((country) =>
|
133
|
+
country.name?.toLowerCase()?.includes(search?.toLowerCase())
|
134
|
+
),
|
135
|
+
[search]
|
136
|
+
);
|
137
|
+
return (
|
138
|
+
<BottomSheet
|
139
|
+
ref={sheetRef}
|
140
|
+
onOpen={() => setOpen(true)}
|
141
|
+
onClose={() => setOpen(false)}
|
142
|
+
contentClassName=""
|
143
|
+
>
|
144
|
+
<View className="max-w-2xl mx-auto w-full max-h-[90%]">
|
145
|
+
{open && (
|
146
|
+
<CountryList
|
147
|
+
countries={allCountries}
|
148
|
+
search={search}
|
149
|
+
setSearch={setSearch}
|
150
|
+
selectedCountry={selectedCountry}
|
151
|
+
setSelectedCountry={setSelectedCountry}
|
152
|
+
close={() => {
|
153
|
+
sheetRef.current?.hide();
|
154
|
+
}}
|
155
|
+
/>
|
156
|
+
)}
|
157
|
+
</View>
|
158
|
+
</BottomSheet>
|
159
|
+
);
|
160
|
+
};
|
161
|
+
const CountryList = memo(
|
162
|
+
({
|
163
|
+
countries,
|
164
|
+
search,
|
165
|
+
setSearch,
|
166
|
+
selectedCountry,
|
167
|
+
setSelectedCountry,
|
168
|
+
close,
|
169
|
+
}: {
|
170
|
+
countries: CountryType[];
|
171
|
+
search: string;
|
172
|
+
setSearch: (search: string) => void;
|
173
|
+
|
174
|
+
selectedCountry: CountryType;
|
175
|
+
setSelectedCountry: (code: CountryType) => void;
|
176
|
+
close: () => void;
|
177
|
+
}) => {
|
178
|
+
const renderItem = useCallback(
|
179
|
+
({
|
180
|
+
item,
|
181
|
+
}: {
|
182
|
+
item: ICountry & {
|
183
|
+
code: string;
|
184
|
+
};
|
185
|
+
}) => {
|
186
|
+
const isActive = selectedCountry.code === item.code;
|
187
|
+
return (
|
188
|
+
<Pressable
|
189
|
+
onPress={() => {
|
190
|
+
setSelectedCountry(item);
|
191
|
+
close();
|
192
|
+
}}
|
193
|
+
className={cn(
|
194
|
+
"p-3 bg-card rounded-xl border h-12 border-muted/15 flex-row gap-4 w-full",
|
195
|
+
isActive && "bg-primary/10 border-primary"
|
196
|
+
)}
|
197
|
+
containerClassName="w-full"
|
198
|
+
>
|
199
|
+
{getFlagEmoji(item.code)}
|
200
|
+
<Text>+{item.phone?.[0]}</Text>
|
201
|
+
<Text className="font-medium">{item.name}</Text>
|
202
|
+
</Pressable>
|
203
|
+
);
|
204
|
+
},
|
205
|
+
[selectedCountry, setSelectedCountry, close]
|
206
|
+
);
|
207
|
+
|
208
|
+
return (
|
209
|
+
<View className="overflow-x-hidden h-full">
|
210
|
+
<>
|
211
|
+
<Text className="text-center text-lg font-medium mb-2">
|
212
|
+
Select Country
|
213
|
+
</Text>
|
214
|
+
<FloatingInput
|
215
|
+
value={search}
|
216
|
+
onChangeText={setSearch}
|
217
|
+
label="Search Country"
|
218
|
+
containerClassName="mb-2"
|
219
|
+
/>
|
220
|
+
</>
|
221
|
+
<View>
|
222
|
+
<FlashList
|
223
|
+
data={countries.slice(0, 15)}
|
224
|
+
renderItem={renderItem}
|
225
|
+
estimatedItemSize={100}
|
226
|
+
showsVerticalScrollIndicator
|
227
|
+
showsHorizontalScrollIndicator={false}
|
228
|
+
ItemSeparatorComponent={() => <View className="h-1" />}
|
229
|
+
keyExtractor={(item) => item.code}
|
230
|
+
/>
|
231
|
+
</View>
|
232
|
+
</View>
|
233
|
+
);
|
234
|
+
}
|
235
|
+
);
|
236
|
+
function getFlagEmoji(countryCode) {
|
237
|
+
const codePoints = countryCode
|
238
|
+
.toUpperCase()
|
239
|
+
.split("")
|
240
|
+
.map((char) => 127397 + char.charCodeAt());
|
241
|
+
return String.fromCodePoint(...codePoints);
|
242
|
+
}
|
@@ -11,7 +11,7 @@ import { memo, useCallback, useMemo } from "react";
|
|
11
11
|
import { Iconify } from "react-native-iconify";
|
12
12
|
import { Control, Controller, Path } from "react-hook-form";
|
13
13
|
|
14
|
-
type
|
14
|
+
export type MultiSelectProps<T = Record<string, any>> = PressableProps & {
|
15
15
|
containerClassName?: string;
|
16
16
|
label?: string;
|
17
17
|
error?: string;
|
@@ -31,7 +31,7 @@ export const MultiSelect = <T extends Record<string, any>>({
|
|
31
31
|
name,
|
32
32
|
control,
|
33
33
|
...props
|
34
|
-
}:
|
34
|
+
}: MultiSelectProps<T>) => {
|
35
35
|
if (control) {
|
36
36
|
return (
|
37
37
|
<Controller
|
@@ -65,7 +65,7 @@ function BaseSelect<T extends Record<string, any>>({
|
|
65
65
|
placeholder,
|
66
66
|
options,
|
67
67
|
...props
|
68
|
-
}:
|
68
|
+
}: MultiSelectProps<T>) {
|
69
69
|
const tw = useTw();
|
70
70
|
const renderOptions = useCallback(() => {
|
71
71
|
return options.map((option, index) => (
|
@@ -112,11 +112,11 @@ const SelectTrigger = memo(
|
|
112
112
|
error,
|
113
113
|
...props
|
114
114
|
}: PressableProps & {
|
115
|
-
options:
|
116
|
-
value:
|
117
|
-
onChange:
|
118
|
-
placeholder:
|
119
|
-
error:
|
115
|
+
options: MultiSelectProps<T>["options"];
|
116
|
+
value: MultiSelectProps<T>["value"];
|
117
|
+
onChange: MultiSelectProps<T>["onChange"];
|
118
|
+
placeholder: MultiSelectProps<T>["placeholder"];
|
119
|
+
error: MultiSelectProps<T>["error"];
|
120
120
|
}) => {
|
121
121
|
const selectedOptions = useMemo(
|
122
122
|
() => options.filter((option) => value?.includes?.(option.value)),
|
@@ -4,7 +4,7 @@ import { memo, useCallback, useMemo } from "react";
|
|
4
4
|
import { Iconify } from "react-native-iconify";
|
5
5
|
import { Control, Controller, Path } from "react-hook-form";
|
6
6
|
|
7
|
-
type SelectProps<T
|
7
|
+
export type SelectProps<T = Record<string, any>> = PressableProps & {
|
8
8
|
containerClassName?: string;
|
9
9
|
label?: string;
|
10
10
|
error?: string;
|
@@ -1,6 +1,7 @@
|
|
1
1
|
import { cn, Pressable, Text, useTw, View } from "@nativetail/core";
|
2
2
|
import { AnimatePresence } from "moti";
|
3
|
-
import { useEffect, useState } from "react";
|
3
|
+
import { useCallback, useEffect, useState } from "react";
|
4
|
+
import { Modal } from "react-native";
|
4
5
|
import { Iconify } from "react-native-iconify";
|
5
6
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
6
7
|
import { create } from "zustand";
|
@@ -19,6 +20,8 @@ type ToastType = {
|
|
19
20
|
|
20
21
|
containerClassName?: string;
|
21
22
|
type?: "success" | "danger" | "info" | "warning";
|
23
|
+
modal?: boolean;
|
24
|
+
icon?: React.ReactNode;
|
22
25
|
};
|
23
26
|
type InsertToastType = Omit<ToastType, "id">;
|
24
27
|
type ToastStore = {
|
@@ -57,7 +60,6 @@ export function Toaster() {
|
|
57
60
|
const bottomToasts = toasts.filter((toast) =>
|
58
61
|
toast.position.includes("bottom")
|
59
62
|
);
|
60
|
-
|
61
63
|
const safeInsets = useSafeAreaInsets();
|
62
64
|
return (
|
63
65
|
<AnimatePresence exitBeforeEnter presenceAffectsLayout>
|
@@ -93,6 +95,34 @@ const Toast = (
|
|
93
95
|
toast: ToastType & {
|
94
96
|
index: number;
|
95
97
|
}
|
98
|
+
) => {
|
99
|
+
const safeInsets = useSafeAreaInsets();
|
100
|
+
const [visible, setVisible] = useState(true);
|
101
|
+
const close = useCallback(() => {
|
102
|
+
setVisible(false);
|
103
|
+
}, [setVisible]);
|
104
|
+
if (toast.modal)
|
105
|
+
return (
|
106
|
+
<Modal visible={visible} onRequestClose={close} transparent>
|
107
|
+
<Pressable
|
108
|
+
className={cn(
|
109
|
+
"absolute w-full h-full bg-black/15 left-0 justify-start z-50 gap-2",
|
110
|
+
toast.position.includes("top")
|
111
|
+
? `pt-[${safeInsets.top + 10}px]`
|
112
|
+
: `pb-[${safeInsets.bottom + 10}px]`
|
113
|
+
)}
|
114
|
+
onPress={close}
|
115
|
+
>
|
116
|
+
<BaseToast {...toast} />
|
117
|
+
</Pressable>
|
118
|
+
</Modal>
|
119
|
+
);
|
120
|
+
return <BaseToast {...toast} />;
|
121
|
+
};
|
122
|
+
const BaseToast = (
|
123
|
+
toast: ToastType & {
|
124
|
+
index: number;
|
125
|
+
}
|
96
126
|
) => {
|
97
127
|
const [open, setOpen] = useState(true);
|
98
128
|
const tw = useTw();
|
@@ -136,7 +166,7 @@ const Toast = (
|
|
136
166
|
),
|
137
167
|
};
|
138
168
|
|
139
|
-
const Icon = Icons[toast.type];
|
169
|
+
const Icon = toast.icon || Icons[toast.type];
|
140
170
|
const horizontalPositions = {
|
141
171
|
center: "items-center",
|
142
172
|
left: "items-start",
|