@nativetail/ui 0.0.1

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.
@@ -0,0 +1,252 @@
1
+ import { BlurView } from "expo-blur";
2
+ import { AnimatePresence } from "moti";
3
+ import {
4
+ mergeClasses,
5
+ Pressable,
6
+ PressableProps,
7
+ useTw,
8
+ View,
9
+ } from "@nativetail/core";
10
+ import React, {
11
+ ReactNode,
12
+ useCallback,
13
+ useEffect,
14
+ useRef,
15
+ useState,
16
+ } from "react";
17
+ import {
18
+ Dimensions,
19
+ I18nManager,
20
+ Modal,
21
+ View as NativeView,
22
+ StatusBar,
23
+ } from "react-native";
24
+ import { Blur } from "../blur";
25
+
26
+ type PositionType = {
27
+ width: number;
28
+ height: number;
29
+ top: number;
30
+ left: number;
31
+ bottom: number;
32
+ };
33
+ type DropdownState = {
34
+ isOpen: boolean;
35
+ toggle: () => void;
36
+ close: () => void;
37
+ open: () => void;
38
+ position: PositionType | null;
39
+ setPosition: (position: PositionType | null) => void;
40
+ };
41
+ const DropdownContext = React.createContext<DropdownState | null>(null);
42
+ const useDropdownContext = () => {
43
+ const context = React.useContext(DropdownContext);
44
+ if (!context) {
45
+ throw new Error("useDropdownContext must be used within a DropdownRoot");
46
+ }
47
+ return context;
48
+ };
49
+
50
+ const DropdownRoot = ({
51
+ className,
52
+ children,
53
+ }: {
54
+ className?: string;
55
+ children: ReactNode;
56
+ }) => {
57
+ const [open, setOpen] = useState(false);
58
+ const [position, setPosition] = useState<PositionType | null>(null);
59
+ return (
60
+ <DropdownContext.Provider
61
+ value={{
62
+ isOpen: open,
63
+ toggle: () => setOpen(!open),
64
+ close: () => setOpen(false),
65
+ open: () => setOpen(true),
66
+ position,
67
+ setPosition,
68
+ }}
69
+ >
70
+ <View className={className} animated>
71
+ {children}
72
+ </View>
73
+ </DropdownContext.Provider>
74
+ );
75
+ };
76
+ const DropdownTrigger = ({
77
+ className,
78
+ children,
79
+ containerClassName,
80
+ }: {
81
+ className?: string;
82
+ children: ReactNode;
83
+ containerClassName?: string;
84
+ }) => {
85
+ const toggle = useDropdownContext().toggle;
86
+ const ref = useRef<NativeView>(null);
87
+
88
+ const statusBarHeight: number = StatusBar.currentHeight || 0;
89
+ const setPosition = useDropdownContext().setPosition;
90
+
91
+ const { width: W, height: H } = Dimensions.get("window");
92
+ const _measure = useCallback(() => {
93
+ if (ref && ref?.current) {
94
+ ref.current.measureInWindow((pageX, pageY, width, height) => {
95
+ let isFull = false;
96
+
97
+ const top = isFull ? 20 : height + pageY + 2;
98
+ const bottom = H - top + height;
99
+ const left = I18nManager.isRTL ? W - width - pageX : pageX;
100
+
101
+ setPosition({
102
+ width: Math.floor(width),
103
+ top: Math.floor(top + statusBarHeight),
104
+ bottom: Math.floor(bottom - statusBarHeight),
105
+ left: Math.floor(left),
106
+ height: Math.floor(height),
107
+ });
108
+ });
109
+ }
110
+ }, [H, W]);
111
+ const handlePress = useCallback(() => {
112
+ if (__DEV__) {
113
+ _measure();
114
+ }
115
+ toggle();
116
+ }, [toggle, _measure]);
117
+ return (
118
+ <Pressable
119
+ className={className}
120
+ onPress={handlePress}
121
+ containerClassName={containerClassName}
122
+ onLayout={_measure}
123
+ ref={ref}
124
+ >
125
+ {children}
126
+ </Pressable>
127
+ );
128
+ };
129
+ let timeout: NodeJS.Timeout;
130
+ const DropdownMenu = ({
131
+ className,
132
+ children,
133
+ useBlur = true,
134
+ }: {
135
+ className?: string;
136
+ children: ReactNode;
137
+ useBlur?: boolean;
138
+ }) => {
139
+ const { isOpen, close } = useDropdownContext();
140
+ const position = useDropdownContext().position;
141
+ const left = position?.left || 0;
142
+ const top = position?.top || 0;
143
+ const menuX = left;
144
+ const menuY = top;
145
+ const [modalOpen, setModalOpen] = useState(isOpen);
146
+ useEffect(() => {
147
+ if (isOpen) {
148
+ setModalOpen(true);
149
+ } else {
150
+ timeout = setTimeout(
151
+ () => {
152
+ setModalOpen(false);
153
+ },
154
+ isOpen ? 0 : 200
155
+ );
156
+ }
157
+ return () => {
158
+ if (timeout) clearTimeout(timeout);
159
+ };
160
+ }, [isOpen]);
161
+
162
+ const onDidAnimate = useCallback(() => {
163
+ if (!isOpen) {
164
+ setModalOpen(false);
165
+ }
166
+ }, [isOpen]);
167
+ const tw = useTw();
168
+ const renderChildren = useCallback(() => {
169
+ return React.Children.map(children, (child, index) => {
170
+ return React.cloneElement(child as any, {
171
+ key: index,
172
+ last: index === React.Children.count(children) - 1,
173
+ first: index === 0,
174
+ });
175
+ });
176
+ }, [children]);
177
+ return (
178
+ <Modal
179
+ visible={modalOpen}
180
+ transparent
181
+ onRequestClose={close}
182
+ statusBarTranslucent
183
+ >
184
+ <Pressable className="flex-1 " onPress={close}>
185
+ <AnimatePresence exitBeforeEnter>
186
+ {isOpen && (
187
+ <View
188
+ className={mergeClasses(
189
+ "absolute in:scale-0 scale-100 out:scale-0 overflow-hidden z-10 bg-card/95 rounded-xl max-w-xs w-full border border-muted/15",
190
+ className
191
+ )}
192
+ onDidAnimate={onDidAnimate}
193
+ style={{
194
+ top: menuY,
195
+ left: menuX,
196
+ transformOrigin: "top left",
197
+ }}
198
+ animated
199
+ print
200
+ id={"1"}
201
+ >
202
+ {useBlur && (
203
+ <Blur style={tw`absolute top-0 left-0 rounded-xl flex-1 `} />
204
+ )}
205
+ {renderChildren()}
206
+ </View>
207
+ )}
208
+ </AnimatePresence>
209
+ </Pressable>
210
+ </Modal>
211
+ );
212
+ };
213
+ const DropdownItem = ({
214
+ className,
215
+ children,
216
+ last,
217
+ first,
218
+ ...props
219
+ }: {
220
+ className?: string;
221
+ last?: boolean;
222
+ children: ReactNode;
223
+ first?: boolean;
224
+ } & PressableProps) => {
225
+ const close = useDropdownContext().close;
226
+ return (
227
+ <Pressable
228
+ className={mergeClasses(
229
+ "w-full text-[16px] font-medium bg-card/15 active:bg-card py-2.5 px-4 flex-row items-center justify-between border-b border-b-transparent text-foreground",
230
+ first ? "rounded-t-xl" : "",
231
+ last ? "rounded-b-xl" : "border-b-muted/15",
232
+ className
233
+ )}
234
+ {...props}
235
+ onPress={() => {
236
+ close();
237
+ props.onPress && props.onPress();
238
+ }}
239
+ >
240
+ {children}
241
+ </Pressable>
242
+ );
243
+ };
244
+
245
+ const Dropdown = {
246
+ Root: DropdownRoot,
247
+ Trigger: DropdownTrigger,
248
+ Menu: DropdownMenu,
249
+ Item: DropdownItem,
250
+ };
251
+
252
+ export { Dropdown };
@@ -0,0 +1,14 @@
1
+ export * from "./dropdown";
2
+ export * from "./button";
3
+ export * from "./input";
4
+ export * from "./switch";
5
+ export * from "./toast";
6
+ export * from "./select";
7
+ export * from "./dialog";
8
+ export * from "./alert-dialog";
9
+ export * from "./bottom-sheet";
10
+ export * from "./actions-sheet";
11
+ export * from "./chip";
12
+ export * from "./blur";
13
+ export * from "./progress";
14
+ export * from "./counter";
@@ -0,0 +1,74 @@
1
+ import { cn, Text, TextInput, TextInputProps, View } from "@nativetail/core";
2
+ import { useCallback, useState } from "react";
3
+ import { create } from "zustand";
4
+
5
+ type FloatingInputProps = TextInputProps & {
6
+ containerClassName?: string;
7
+ label: string;
8
+ error?: string;
9
+ helperText?: string;
10
+ };
11
+ const useFocusSate = create<{
12
+ isFocused: boolean;
13
+ setIsFocused: (isFocused: boolean) => void;
14
+ }>((set) => ({
15
+ isFocused: false,
16
+ setIsFocused: (isFocused: boolean) => set({ isFocused }),
17
+ }));
18
+ export function FloatingInput({
19
+ value,
20
+ onChangeText,
21
+ containerClassName,
22
+ label,
23
+ error,
24
+ className,
25
+ ...props
26
+ }: FloatingInputProps) {
27
+ const onFocus = useCallback(() => {
28
+ useFocusSate.getState().setIsFocused(true);
29
+ }, []);
30
+ const onBlur = useCallback(() => {
31
+ useFocusSate.getState().setIsFocused(false);
32
+ }, []);
33
+ return (
34
+ <View
35
+ className={cn(
36
+ "w-full rounded-xl h-16 overflow-hidden border border-muted/15",
37
+ containerClassName
38
+ )}
39
+ >
40
+ <Label label={label} value={value} />
41
+
42
+ <TextInput
43
+ onFocus={onFocus}
44
+ onBlur={onBlur}
45
+ value={value}
46
+ onChangeText={onChangeText}
47
+ className={cn(
48
+ "flex-1 p-3 bg-card rounded-xl absolute w-full h-full -z-5 pt-5 text-foreground text-[16px]",
49
+ className
50
+ )}
51
+ {...props}
52
+ />
53
+ </View>
54
+ );
55
+ }
56
+
57
+ const Label = ({ label, value }: { label?: string; value?: string }) => {
58
+ const isFocused = useFocusSate((state) => state.isFocused);
59
+ const labelOnTop = isFocused || !!value;
60
+
61
+ return (
62
+ <View className="flex-1 p-3 justify-center" pointerEvents="none">
63
+ <Text
64
+ animated
65
+ className={cn(
66
+ "text-muted duration-75 ",
67
+ labelOnTop ? " -translate-y-16 text-xs" : "translate-y-0 text-[16px]"
68
+ )}
69
+ >
70
+ {label}
71
+ </Text>
72
+ </View>
73
+ );
74
+ };
@@ -0,0 +1,2 @@
1
+ export * from "./floating-input";
2
+ export * from "./input";
@@ -0,0 +1,41 @@
1
+ import {
2
+ cn,
3
+ Text,
4
+ TextInput,
5
+ TextInputProps,
6
+ useTw,
7
+ View,
8
+ } from "@nativetail/core";
9
+
10
+ type InputProps = TextInputProps & {
11
+ containerClassName?: string;
12
+ label: string;
13
+ error?: string;
14
+ helperText?: string;
15
+ };
16
+ export function Input({
17
+ value,
18
+ onChangeText,
19
+ containerClassName,
20
+ label,
21
+ error,
22
+ className,
23
+ ...props
24
+ }: InputProps) {
25
+ const tw = useTw();
26
+ return (
27
+ <View className={cn("w-full gap-1", containerClassName)}>
28
+ <Text className={cn("text-muted/75 duration-75 ")}>{label}</Text>
29
+ <TextInput
30
+ value={value}
31
+ onChangeText={onChangeText}
32
+ className={cn(
33
+ "p-3 bg-card rounded-lg w-full border border-muted/15 h-14 text-foreground -z-5 text-[16px]",
34
+ className
35
+ )}
36
+ placeholderTextColor={tw.color("muted")}
37
+ {...props}
38
+ />
39
+ </View>
40
+ );
41
+ }
@@ -0,0 +1,28 @@
1
+ import { View, cn } from "@nativetail/core";
2
+ import React, { useMemo } from "react";
3
+ export type ProgressProps = {
4
+ containerClassName?: string;
5
+ progress: number;
6
+ max: number;
7
+ };
8
+ export function Progress({ containerClassName, progress, max }: ProgressProps) {
9
+ const percent = useMemo(() => {
10
+ return Math.round((progress / max) * 100);
11
+ }, [progress, max]);
12
+ return (
13
+ <View
14
+ className={cn(
15
+ "flex-row items-center rounded-full h-4 overflow-hidden bg-primary/15 border border-primary/35 ",
16
+ containerClassName
17
+ )}
18
+ >
19
+ <View
20
+ className={cn(
21
+ "w-full h-full bg-primary rounded-full",
22
+ `w-[${percent}%]`
23
+ )}
24
+ animated
25
+ />
26
+ </View>
27
+ );
28
+ }
@@ -0,0 +1,141 @@
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
+ );
@@ -0,0 +1,31 @@
1
+ import { cn, View } from "@nativetail/core";
2
+ import { useCallback } from "react";
3
+
4
+ export type StepperProps = {
5
+ steps: string[];
6
+ activeStepIndex: number;
7
+ setActiveStepIndex?: (index: number) => void;
8
+ };
9
+ export const Stepper = (props: StepperProps) => {
10
+ const renderSteps = useCallback(() => {
11
+ return props.steps.map((step, index) => {
12
+ const isActive = index === props.activeStepIndex;
13
+ const isCompleted = index < props.activeStepIndex;
14
+ return (
15
+ <View key={index} className="flex-row items-center">
16
+ <View
17
+ className={cn("w-4 h-4 rounded-full", {
18
+ "bg-primary": isActive,
19
+ "bg-gray-300": !isActive && !isCompleted,
20
+ "bg-success": isCompleted,
21
+ })}
22
+ />
23
+ {index < props.steps.length - 1 && (
24
+ <View className="h-0.5 bg-gray-300 w-4" />
25
+ )}
26
+ </View>
27
+ );
28
+ });
29
+ }, [props.steps, props.activeStepIndex]);
30
+ return <View className="flex-row items-center">{renderSteps()}</View>;
31
+ };
@@ -0,0 +1,72 @@
1
+ import {
2
+ cn,
3
+ cva,
4
+ Pressable,
5
+ useTw,
6
+ VariantProps,
7
+ View,
8
+ } from "@nativetail/core";
9
+
10
+ const switchVariants = cva(
11
+ "rounded-full bg-card justify-center border p-0.5 items-start",
12
+ {
13
+ variants: {
14
+ size: {
15
+ small: "w-12 h-6",
16
+ medium: "w-16 h-8",
17
+ large: "w-24 h-12",
18
+ },
19
+ },
20
+ defaultVariants: {
21
+ size: "small",
22
+ },
23
+ }
24
+ );
25
+ type SwitchProps = VariantProps<typeof switchVariants> & {
26
+ checked: boolean;
27
+ onChange: (checked: boolean) => void;
28
+ containerClassName?: string;
29
+ containerActiveClass?: string;
30
+ indicatorClassName?: string;
31
+ };
32
+ export function Switch({
33
+ checked,
34
+ onChange,
35
+ containerClassName,
36
+ size,
37
+ indicatorClassName,
38
+ containerActiveClass,
39
+ ...props
40
+ }: SwitchProps) {
41
+ const className = switchVariants({
42
+ className: containerClassName,
43
+ size: size,
44
+ });
45
+ const tw = useTw();
46
+ const style = tw.style(className);
47
+ const containerWidth = Number(style.width) || 0;
48
+ const indicatorWidth = containerWidth * 0.48;
49
+ const x = `translate-x-${!checked ? 1 : indicatorWidth}`;
50
+ return (
51
+ <Pressable
52
+ className={cn(
53
+ className,
54
+ checked ? "bg-primary/35 " + containerActiveClass : ""
55
+ )}
56
+ aria-checked={checked}
57
+ accessibilityRole="switch"
58
+ aria-label="Switch"
59
+ {...props}
60
+ onPress={() => onChange(!checked)}
61
+ >
62
+ <View
63
+ className={cn(
64
+ `rounded-full bg-primary aspect-square h-full`,
65
+ indicatorClassName,
66
+ x
67
+ )}
68
+ animated
69
+ />
70
+ </Pressable>
71
+ );
72
+ }