@nativetail/ui 0.0.6 → 0.0.8

Sign up to get free protection for your applications and to get access to all the features.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nativetail/ui",
3
- "version": "0.0.6",
3
+ "version": "0.0.8",
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, {
@@ -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-primary text-foreground bg-black/0 ",
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
- text,
58
- children,
59
- isLoading,
60
- className,
61
- disabled,
62
- variant,
63
- leftElement,
64
- rightElement,
65
- size,
66
- loadingIndicatorClassName,
67
-
68
- ...props
69
- }: ButtonProps) => {
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
- buttonVariants({
78
- variant,
79
- size,
80
- className,
81
- disabled: isDisabled,
82
- }),
83
- [variant, size, className, isDisabled, tw]
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);
@@ -33,7 +33,7 @@ const chipVariants = cva(
33
33
  },
34
34
  }
35
35
  );
36
- type ChipProps = VariantProps<typeof chipVariants> & {
36
+ export type ChipProps = VariantProps<typeof chipVariants> & {
37
37
  text?: string;
38
38
  textClass?: string;
39
39
  leftElement?: React.ReactNode;
@@ -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.toString()}
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 = {
@@ -29,7 +31,9 @@ export const Dialog = forwardRef<DialogMethods, DialogProps>(function Dialog(
29
31
  contentClassName,
30
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={onRequestClose}
83
+ onRequestClose={handleOnRequestClose}
78
84
  animationType="fade"
79
85
  statusBarTranslucent
80
86
  >
81
- <Pressable
87
+ <View
82
88
  className={cn(
83
- "flex-1 items-center justify-center p-4 w-full h-full bg-black/35",
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-10 bg-card/95 rounded-xl max-w-sm w-full border border-muted/10",
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
- </Pressable>
120
+ </View>
109
121
  </Modal>
110
122
  );
111
123
  });
@@ -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
- cn,
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 = Omit<TextInputProps, "placeholder"> & {
6
+ export type FloatingInputProps<T = Record<string, any>> = TextInputProps & {
13
7
  containerClassName?: string;
14
- label: string;
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
- export function FloatingInput({
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 label={label} value={value} isFocused={isFocused} />
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
- labelOnTop ? " -translate-y-16 text-xs" : "translate-y-0 text-[16px]"
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: string;
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
- export function Input({
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-14 text-foreground -z-5 text-[16px]",
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
+ };