@nativetail/ui 0.0.8 → 0.1.0
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 +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",
|