@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.
@@ -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: string;
7
- onChangeText: (text: string) => void;
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
- export function PinInput({
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.length == length ? length - 1 : value.length;
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.length <= length) {
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
- <View className={cn(" gap-2 flex-row w-full", containerClassName)}>
49
- {Array.from({ length: length }).map((_, index) => (
50
- <Pressable
51
- key={`pininput-${index}`}
52
- className={cn(
53
- "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",
54
- pinBoxClassName,
55
- isFocused &&
56
- activeIndex === index &&
57
- "border-foreground" + " " + pinBoxFocusedClassName
58
- )}
59
- onPress={onPinBoxPress}
60
- >
61
- {value[index]}
62
- </Pressable>
63
- ))}
64
- <TextInput
65
- ref={textInputRef}
66
- value={value}
67
- onChangeText={_handleChange}
68
- onFocus={onFocus}
69
- onBlur={onBlur}
70
- className="opacity-0 scale-0 absolute"
71
- {...props}
72
- />
73
- </View>
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
- import { cn, PressableProps, Text, useTw, View } from "@nativetail/core";
2
- import { Dropdown } from "../dropdown";
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
+ );