@beyondcorp/beyond-ui-native 0.1.2 → 0.1.3
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 +11 -5
- package/src/components/accordion/accordion.tsx +245 -0
- package/src/components/accordion/index.ts +1 -0
- package/src/components/alert/alert.tsx +118 -0
- package/src/components/alert/index.ts +1 -0
- package/src/components/avatar/avatar.tsx +125 -0
- package/src/components/avatar/index.ts +1 -0
- package/src/components/badge/badge.tsx +89 -0
- package/src/components/badge/index.ts +1 -0
- package/src/components/bottom-sheet/bottom-sheet.tsx +336 -0
- package/src/components/bottom-sheet/index.ts +1 -0
- package/src/components/button/button.tsx +172 -0
- package/src/components/button/index.ts +1 -0
- package/src/components/card/card.tsx +121 -0
- package/src/components/card/index.ts +1 -0
- package/src/components/checkbox/checkbox.tsx +100 -0
- package/src/components/checkbox/index.ts +1 -0
- package/src/components/data-table/data-table.tsx +277 -0
- package/src/components/data-table/index.ts +1 -0
- package/src/components/dialog/dialog.tsx +278 -0
- package/src/components/dialog/index.ts +1 -0
- package/src/components/input/index.ts +1 -0
- package/src/components/input/input.tsx +130 -0
- package/src/components/radio/index.ts +1 -0
- package/src/components/radio/radio.tsx +117 -0
- package/src/components/select/index.ts +1 -0
- package/src/components/select/select.tsx +375 -0
- package/src/components/separator/index.ts +1 -0
- package/src/components/separator/separator.tsx +28 -0
- package/src/components/skeleton/index.ts +1 -0
- package/src/components/skeleton/skeleton.tsx +43 -0
- package/src/components/spinner/index.ts +1 -0
- package/src/components/spinner/spinner.tsx +46 -0
- package/src/components/stats-card/index.ts +1 -0
- package/src/components/stats-card/stats-card.tsx +92 -0
- package/src/components/switch/index.ts +1 -0
- package/src/components/switch/switch.tsx +80 -0
- package/src/components/tabs/index.ts +1 -0
- package/src/components/tabs/tabs.tsx +172 -0
- package/src/components/text/heading.tsx +50 -0
- package/src/components/text/index.ts +2 -0
- package/src/components/text/text.tsx +62 -0
- package/src/components/textarea/index.ts +1 -0
- package/src/components/textarea/textarea.tsx +87 -0
- package/src/components/toast/index.ts +2 -0
- package/src/components/toast/toast.tsx +136 -0
- package/src/components/toast/types.ts +32 -0
- package/src/index.ts +42 -0
- package/src/primitives/box.tsx +25 -0
- package/src/primitives/index.ts +2 -0
- package/src/primitives/pressable.tsx +49 -0
- package/src/providers/BeyondUIProvider.tsx +100 -0
- package/src/templates/auth/ForgotPasswordTemplate.tsx +132 -0
- package/src/templates/auth/SignInTemplate.tsx +213 -0
- package/src/templates/auth/SignUpTemplate.tsx +233 -0
- package/src/templates/auth/index.ts +3 -0
- package/src/templates/dashboard/DashboardTemplate.tsx +205 -0
- package/src/templates/dashboard/index.ts +1 -0
- package/src/templates/index.ts +3 -0
- package/src/templates/settings/SettingsTemplate.tsx +226 -0
- package/src/templates/settings/index.ts +1 -0
- package/src/theme/preset.ts +45 -0
- package/src/theme/tokens.ts +144 -0
- package/src/types/nativewind-env.d.ts +26 -0
- package/src/utilities/accessibility.ts +43 -0
- package/src/utilities/cn.ts +9 -0
- package/src/utilities/index.ts +2 -0
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { View, Text, Pressable, Animated } from "react-native";
|
|
3
|
+
import { cva } from "class-variance-authority";
|
|
4
|
+
import { cn } from "../../utilities/cn";
|
|
5
|
+
import { ToastItem, ToastVariant } from "./types";
|
|
6
|
+
|
|
7
|
+
const toastVariants = cva(
|
|
8
|
+
"w-full max-w-sm rounded-xl p-4 shadow-lg border flex-row items-center justify-between mb-2",
|
|
9
|
+
{
|
|
10
|
+
variants: {
|
|
11
|
+
variant: {
|
|
12
|
+
default: "bg-white border-neutral-200 dark:bg-neutral-900 dark:border-neutral-800",
|
|
13
|
+
success: "bg-success-50 border-success-200 dark:bg-success-950/80 dark:border-success-800",
|
|
14
|
+
danger: "bg-danger-50 border-danger-200 dark:bg-danger-950/80 dark:border-danger-800",
|
|
15
|
+
warning: "bg-warning-50 border-warning-200 dark:bg-warning-950/80 dark:border-warning-800",
|
|
16
|
+
info: "bg-primary-50 border-primary-200 dark:bg-primary-950/80 dark:border-primary-800",
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
defaultVariants: {
|
|
20
|
+
variant: "default",
|
|
21
|
+
},
|
|
22
|
+
}
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
const toastTitleVariants = cva("text-sm font-semibold", {
|
|
26
|
+
variants: {
|
|
27
|
+
variant: {
|
|
28
|
+
default: "text-neutral-900 dark:text-white",
|
|
29
|
+
success: "text-success-700 dark:text-success-300",
|
|
30
|
+
danger: "text-danger-700 dark:text-danger-300",
|
|
31
|
+
warning: "text-warning-800 dark:text-warning-300",
|
|
32
|
+
info: "text-primary-700 dark:text-primary-300",
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
defaultVariants: {
|
|
36
|
+
variant: "default",
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
interface ToastBannerProps {
|
|
41
|
+
toast: ToastItem;
|
|
42
|
+
onDismiss: (id: string) => void;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function ToastBanner({ toast, onDismiss }: ToastBannerProps) {
|
|
46
|
+
const opacity = React.useRef(new Animated.Value(0)).current;
|
|
47
|
+
const translateY = React.useRef(new Animated.Value(-16)).current;
|
|
48
|
+
|
|
49
|
+
React.useEffect(() => {
|
|
50
|
+
Animated.parallel([
|
|
51
|
+
Animated.timing(opacity, {
|
|
52
|
+
toValue: 1,
|
|
53
|
+
duration: 200,
|
|
54
|
+
useNativeDriver: true,
|
|
55
|
+
}),
|
|
56
|
+
Animated.timing(translateY, {
|
|
57
|
+
toValue: 0,
|
|
58
|
+
duration: 200,
|
|
59
|
+
useNativeDriver: true,
|
|
60
|
+
}),
|
|
61
|
+
]).start();
|
|
62
|
+
|
|
63
|
+
const timer = setTimeout(() => {
|
|
64
|
+
handleDismiss();
|
|
65
|
+
}, toast.duration || 4000);
|
|
66
|
+
|
|
67
|
+
return () => clearTimeout(timer);
|
|
68
|
+
}, []);
|
|
69
|
+
|
|
70
|
+
const handleDismiss = () => {
|
|
71
|
+
Animated.parallel([
|
|
72
|
+
Animated.timing(opacity, {
|
|
73
|
+
toValue: 0,
|
|
74
|
+
duration: 200,
|
|
75
|
+
useNativeDriver: true,
|
|
76
|
+
}),
|
|
77
|
+
Animated.timing(translateY, {
|
|
78
|
+
toValue: -16,
|
|
79
|
+
duration: 200,
|
|
80
|
+
useNativeDriver: true,
|
|
81
|
+
}),
|
|
82
|
+
]).start(() => {
|
|
83
|
+
onDismiss(toast.id);
|
|
84
|
+
});
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const variant: ToastVariant = toast.variant || "default";
|
|
88
|
+
|
|
89
|
+
return (
|
|
90
|
+
<Animated.View
|
|
91
|
+
style={{
|
|
92
|
+
opacity,
|
|
93
|
+
transform: [{ translateY }],
|
|
94
|
+
}}
|
|
95
|
+
className="w-full items-center px-4"
|
|
96
|
+
>
|
|
97
|
+
<Pressable
|
|
98
|
+
onPress={handleDismiss}
|
|
99
|
+
className={cn(toastVariants({ variant }))}
|
|
100
|
+
>
|
|
101
|
+
<View className="flex-1 pr-2">
|
|
102
|
+
<Text className={cn(toastTitleVariants({ variant }))}>
|
|
103
|
+
{toast.title}
|
|
104
|
+
</Text>
|
|
105
|
+
{toast.description ? (
|
|
106
|
+
<Text className="text-xs text-neutral-600 dark:text-neutral-400 mt-0.5">
|
|
107
|
+
{toast.description}
|
|
108
|
+
</Text>
|
|
109
|
+
) : null}
|
|
110
|
+
</View>
|
|
111
|
+
<Text className="text-xs text-neutral-400">✕</Text>
|
|
112
|
+
</Pressable>
|
|
113
|
+
</Animated.View>
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function ToastContainer({
|
|
118
|
+
toasts,
|
|
119
|
+
onDismiss,
|
|
120
|
+
}: {
|
|
121
|
+
toasts: ToastItem[];
|
|
122
|
+
onDismiss: (id: string) => void;
|
|
123
|
+
}) {
|
|
124
|
+
if (toasts.length === 0) return null;
|
|
125
|
+
|
|
126
|
+
return (
|
|
127
|
+
<View
|
|
128
|
+
pointerEvents="box-none"
|
|
129
|
+
className="absolute top-12 left-0 right-0 z-50 items-center"
|
|
130
|
+
>
|
|
131
|
+
{toasts.map((toast) => (
|
|
132
|
+
<ToastBanner key={toast.id} toast={toast} onDismiss={onDismiss} />
|
|
133
|
+
))}
|
|
134
|
+
</View>
|
|
135
|
+
);
|
|
136
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
|
|
3
|
+
export type ToastVariant = "default" | "success" | "danger" | "warning" | "info";
|
|
4
|
+
|
|
5
|
+
export interface ToastItem {
|
|
6
|
+
id: string;
|
|
7
|
+
title: string;
|
|
8
|
+
description?: string;
|
|
9
|
+
variant?: ToastVariant;
|
|
10
|
+
duration?: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface ToastContextValue {
|
|
14
|
+
toasts: ToastItem[];
|
|
15
|
+
show: (toast: Omit<ToastItem, "id">) => string;
|
|
16
|
+
dismiss: (id: string) => void;
|
|
17
|
+
success: (title: string, description?: string) => string;
|
|
18
|
+
danger: (title: string, description?: string) => string;
|
|
19
|
+
error: (title: string, description?: string) => string;
|
|
20
|
+
warning: (title: string, description?: string) => string;
|
|
21
|
+
info: (title: string, description?: string) => string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const ToastContext = React.createContext<ToastContextValue | null>(null);
|
|
25
|
+
|
|
26
|
+
export function useToast(): ToastContextValue {
|
|
27
|
+
const context = React.useContext(ToastContext);
|
|
28
|
+
if (!context) {
|
|
29
|
+
throw new Error("useToast must be used within a BeyondUIProvider");
|
|
30
|
+
}
|
|
31
|
+
return context;
|
|
32
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/// <reference path="./types/nativewind-env.d.ts" />
|
|
2
|
+
|
|
3
|
+
// Primitives
|
|
4
|
+
export * from "./primitives";
|
|
5
|
+
|
|
6
|
+
// Components
|
|
7
|
+
export * from "./components/button";
|
|
8
|
+
export * from "./components/text";
|
|
9
|
+
export * from "./components/input";
|
|
10
|
+
export * from "./components/textarea";
|
|
11
|
+
export * from "./components/card";
|
|
12
|
+
export * from "./components/badge";
|
|
13
|
+
export * from "./components/avatar";
|
|
14
|
+
export * from "./components/switch";
|
|
15
|
+
export * from "./components/checkbox";
|
|
16
|
+
export * from "./components/radio";
|
|
17
|
+
export * from "./components/spinner";
|
|
18
|
+
export * from "./components/skeleton";
|
|
19
|
+
export * from "./components/alert";
|
|
20
|
+
export * from "./components/separator";
|
|
21
|
+
export * from "./components/dialog";
|
|
22
|
+
export * from "./components/tabs";
|
|
23
|
+
export * from "./components/toast";
|
|
24
|
+
export * from "./components/stats-card";
|
|
25
|
+
export * from "./components/accordion";
|
|
26
|
+
export * from "./components/bottom-sheet";
|
|
27
|
+
export * from "./components/select";
|
|
28
|
+
export * from "./components/data-table";
|
|
29
|
+
|
|
30
|
+
// Screen Templates
|
|
31
|
+
export * from "./templates";
|
|
32
|
+
|
|
33
|
+
// Providers & Context
|
|
34
|
+
export * from "./providers/BeyondUIProvider";
|
|
35
|
+
|
|
36
|
+
// Theme & Tokens
|
|
37
|
+
export * from "./theme/tokens";
|
|
38
|
+
export { default as beyondUIPreset } from "./theme/preset";
|
|
39
|
+
|
|
40
|
+
// Utilities
|
|
41
|
+
export * from "./utilities/cn";
|
|
42
|
+
export * from "./utilities/accessibility";
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { View, type ViewProps } from "react-native";
|
|
3
|
+
import { cn } from "../utilities/cn";
|
|
4
|
+
|
|
5
|
+
export interface BoxProps extends ViewProps {
|
|
6
|
+
className?: string;
|
|
7
|
+
children?: React.ReactNode;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const Box = React.forwardRef<View, BoxProps>(
|
|
11
|
+
({ className, style, children, ...props }, ref) => {
|
|
12
|
+
return (
|
|
13
|
+
<View
|
|
14
|
+
ref={ref}
|
|
15
|
+
className={cn(className)}
|
|
16
|
+
style={style}
|
|
17
|
+
{...props}
|
|
18
|
+
>
|
|
19
|
+
{children}
|
|
20
|
+
</View>
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
Box.displayName = "Box";
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import {
|
|
3
|
+
Pressable as RNPressable,
|
|
4
|
+
type PressableProps as RNPressableProps,
|
|
5
|
+
type View,
|
|
6
|
+
type GestureResponderEvent,
|
|
7
|
+
} from "react-native";
|
|
8
|
+
import { cn } from "../utilities/cn";
|
|
9
|
+
|
|
10
|
+
export interface PressableProps extends Omit<RNPressableProps, "children"> {
|
|
11
|
+
className?: string;
|
|
12
|
+
children?: React.ReactNode | ((state: { pressed: boolean }) => React.ReactNode);
|
|
13
|
+
feedback?: "opacity" | "scale" | "none";
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const Pressable = React.forwardRef<View, PressableProps>(
|
|
17
|
+
(
|
|
18
|
+
{
|
|
19
|
+
className,
|
|
20
|
+
style,
|
|
21
|
+
disabled,
|
|
22
|
+
feedback = "opacity",
|
|
23
|
+
children,
|
|
24
|
+
...props
|
|
25
|
+
},
|
|
26
|
+
ref
|
|
27
|
+
) => {
|
|
28
|
+
return (
|
|
29
|
+
<RNPressable
|
|
30
|
+
ref={ref}
|
|
31
|
+
disabled={disabled}
|
|
32
|
+
accessibilityRole="button"
|
|
33
|
+
accessibilityState={{ disabled: !!disabled }}
|
|
34
|
+
className={cn(
|
|
35
|
+
feedback === "opacity" && "active:opacity-70",
|
|
36
|
+
feedback === "scale" && "active:scale-[0.98]",
|
|
37
|
+
disabled && "opacity-50",
|
|
38
|
+
className
|
|
39
|
+
)}
|
|
40
|
+
style={style}
|
|
41
|
+
{...props}
|
|
42
|
+
>
|
|
43
|
+
{typeof children === "function" ? children : children}
|
|
44
|
+
</RNPressable>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
Pressable.displayName = "Pressable";
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { View, useColorScheme as useRNColorScheme } from "react-native";
|
|
3
|
+
import { PortalHost } from "@rn-primitives/portal";
|
|
4
|
+
import { ToastContext, ToastItem, ToastContextValue } from "../components/toast/types";
|
|
5
|
+
import { ToastContainer } from "../components/toast/toast";
|
|
6
|
+
|
|
7
|
+
export type ColorScheme = "light" | "dark" | "system";
|
|
8
|
+
|
|
9
|
+
export interface BeyondUIContextValue {
|
|
10
|
+
colorScheme: "light" | "dark";
|
|
11
|
+
setColorScheme: (scheme: ColorScheme) => void;
|
|
12
|
+
toggleColorScheme: () => void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const BeyondUIContext = React.createContext<BeyondUIContextValue>({
|
|
16
|
+
colorScheme: "light",
|
|
17
|
+
setColorScheme: () => {},
|
|
18
|
+
toggleColorScheme: () => {},
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
export function useBeyondUI() {
|
|
22
|
+
return React.useContext(BeyondUIContext);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface BeyondUIProviderProps {
|
|
26
|
+
children: React.ReactNode;
|
|
27
|
+
defaultColorScheme?: ColorScheme;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function BeyondUIProvider({
|
|
31
|
+
children,
|
|
32
|
+
defaultColorScheme = "system",
|
|
33
|
+
}: BeyondUIProviderProps) {
|
|
34
|
+
const systemScheme = useRNColorScheme() ?? "light";
|
|
35
|
+
const [selectedScheme, setSelectedScheme] = React.useState<ColorScheme>(defaultColorScheme);
|
|
36
|
+
const [toasts, setToasts] = React.useState<ToastItem[]>([]);
|
|
37
|
+
|
|
38
|
+
const activeScheme: "light" | "dark" =
|
|
39
|
+
selectedScheme === "system" ? systemScheme : selectedScheme;
|
|
40
|
+
|
|
41
|
+
const toggleColorScheme = React.useCallback(() => {
|
|
42
|
+
setSelectedScheme((prev) => {
|
|
43
|
+
const current = prev === "system" ? systemScheme : prev;
|
|
44
|
+
return current === "dark" ? "light" : "dark";
|
|
45
|
+
});
|
|
46
|
+
}, [systemScheme]);
|
|
47
|
+
|
|
48
|
+
const dismissToast = React.useCallback((id: string) => {
|
|
49
|
+
setToasts((prev) => prev.filter((t) => t.id !== id));
|
|
50
|
+
}, []);
|
|
51
|
+
|
|
52
|
+
const showToast = React.useCallback((toast: Omit<ToastItem, "id">) => {
|
|
53
|
+
const id = Math.random().toString(36).substring(2, 9);
|
|
54
|
+
setToasts((prev) => [...prev, { ...toast, id }]);
|
|
55
|
+
return id;
|
|
56
|
+
}, []);
|
|
57
|
+
|
|
58
|
+
const toastContextValue: ToastContextValue = React.useMemo(
|
|
59
|
+
() => ({
|
|
60
|
+
toasts,
|
|
61
|
+
show: showToast,
|
|
62
|
+
dismiss: dismissToast,
|
|
63
|
+
success: (title, description) =>
|
|
64
|
+
showToast({ title, description, variant: "success" }),
|
|
65
|
+
danger: (title, description) =>
|
|
66
|
+
showToast({ title, description, variant: "danger" }),
|
|
67
|
+
error: (title, description) =>
|
|
68
|
+
showToast({ title, description, variant: "danger" }),
|
|
69
|
+
warning: (title, description) =>
|
|
70
|
+
showToast({ title, description, variant: "warning" }),
|
|
71
|
+
info: (title, description) =>
|
|
72
|
+
showToast({ title, description, variant: "info" }),
|
|
73
|
+
}),
|
|
74
|
+
[toasts, showToast, dismissToast]
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
const contextValue: BeyondUIContextValue = React.useMemo(
|
|
78
|
+
() => ({
|
|
79
|
+
colorScheme: activeScheme,
|
|
80
|
+
setColorScheme: setSelectedScheme,
|
|
81
|
+
toggleColorScheme,
|
|
82
|
+
}),
|
|
83
|
+
[activeScheme, toggleColorScheme]
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
<BeyondUIContext.Provider value={contextValue}>
|
|
88
|
+
<ToastContext.Provider value={toastContextValue}>
|
|
89
|
+
<View
|
|
90
|
+
style={{ flex: 1 }}
|
|
91
|
+
className={activeScheme === "dark" ? "dark flex-1" : "flex-1"}
|
|
92
|
+
>
|
|
93
|
+
{children}
|
|
94
|
+
<PortalHost />
|
|
95
|
+
<ToastContainer toasts={toasts} onDismiss={dismissToast} />
|
|
96
|
+
</View>
|
|
97
|
+
</ToastContext.Provider>
|
|
98
|
+
</BeyondUIContext.Provider>
|
|
99
|
+
);
|
|
100
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import {
|
|
3
|
+
View,
|
|
4
|
+
ScrollView,
|
|
5
|
+
KeyboardAvoidingView,
|
|
6
|
+
Platform,
|
|
7
|
+
Pressable,
|
|
8
|
+
} from "react-native";
|
|
9
|
+
import { cn } from "../../utilities/cn";
|
|
10
|
+
import { Text } from "../../components/text/text";
|
|
11
|
+
import { Heading } from "../../components/text/heading";
|
|
12
|
+
import { Input } from "../../components/input/input";
|
|
13
|
+
import { Button } from "../../components/button/button";
|
|
14
|
+
import { Alert, AlertDescription } from "../../components/alert/alert";
|
|
15
|
+
|
|
16
|
+
export interface ForgotPasswordTemplateProps {
|
|
17
|
+
title?: string;
|
|
18
|
+
subtitle?: string;
|
|
19
|
+
onSubmit?: (data: { email: string }) => void;
|
|
20
|
+
onBackToSignIn?: () => void;
|
|
21
|
+
isLoading?: boolean;
|
|
22
|
+
isSuccess?: boolean;
|
|
23
|
+
error?: string | null;
|
|
24
|
+
className?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const ForgotPasswordTemplate: React.FC<ForgotPasswordTemplateProps> = ({
|
|
28
|
+
title = "Reset Password",
|
|
29
|
+
subtitle = "Enter your email address and we'll send you recovery instructions",
|
|
30
|
+
onSubmit,
|
|
31
|
+
onBackToSignIn,
|
|
32
|
+
isLoading = false,
|
|
33
|
+
isSuccess = false,
|
|
34
|
+
error,
|
|
35
|
+
className,
|
|
36
|
+
}) => {
|
|
37
|
+
const [email, setEmail] = React.useState("");
|
|
38
|
+
const [emailError, setEmailError] = React.useState<string | undefined>();
|
|
39
|
+
|
|
40
|
+
const handleSubmit = () => {
|
|
41
|
+
if (!email || !email.includes("@")) {
|
|
42
|
+
setEmailError("Please enter a valid email address");
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
setEmailError(undefined);
|
|
46
|
+
onSubmit?.({ email });
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<KeyboardAvoidingView
|
|
51
|
+
behavior={Platform.OS === "ios" ? "padding" : "height"}
|
|
52
|
+
style={{ flex: 1 }}
|
|
53
|
+
className={cn("flex-1 bg-background", className)}
|
|
54
|
+
>
|
|
55
|
+
<ScrollView
|
|
56
|
+
style={{ flex: 1 }}
|
|
57
|
+
contentContainerStyle={{ flexGrow: 1, justifyContent: "center" }}
|
|
58
|
+
className="px-6 py-8"
|
|
59
|
+
keyboardShouldPersistTaps="handled"
|
|
60
|
+
showsVerticalScrollIndicator={false}
|
|
61
|
+
>
|
|
62
|
+
<View className="w-full max-w-md mx-auto gap-6">
|
|
63
|
+
<View className="items-center gap-2">
|
|
64
|
+
<Heading level={2} className="text-3xl font-extrabold text-foreground text-center">
|
|
65
|
+
{title}
|
|
66
|
+
</Heading>
|
|
67
|
+
<Text className="text-muted text-center text-base">
|
|
68
|
+
{subtitle}
|
|
69
|
+
</Text>
|
|
70
|
+
</View>
|
|
71
|
+
|
|
72
|
+
{error && (
|
|
73
|
+
<Alert variant="danger">
|
|
74
|
+
<AlertDescription>{error}</AlertDescription>
|
|
75
|
+
</Alert>
|
|
76
|
+
)}
|
|
77
|
+
|
|
78
|
+
{isSuccess ? (
|
|
79
|
+
<View className="gap-4">
|
|
80
|
+
<Alert variant="success">
|
|
81
|
+
<AlertDescription>
|
|
82
|
+
Password reset link sent! Check your email inbox.
|
|
83
|
+
</AlertDescription>
|
|
84
|
+
</Alert>
|
|
85
|
+
|
|
86
|
+
{onBackToSignIn && (
|
|
87
|
+
<Button variant="primary" size="lg" onPress={onBackToSignIn}>
|
|
88
|
+
Back to Sign In
|
|
89
|
+
</Button>
|
|
90
|
+
)}
|
|
91
|
+
</View>
|
|
92
|
+
) : (
|
|
93
|
+
<View className="gap-4">
|
|
94
|
+
<Input
|
|
95
|
+
label="Email Address"
|
|
96
|
+
placeholder="you@company.com"
|
|
97
|
+
keyboardType="email-address"
|
|
98
|
+
autoCapitalize="none"
|
|
99
|
+
value={email}
|
|
100
|
+
onChangeText={(text) => {
|
|
101
|
+
setEmail(text);
|
|
102
|
+
if (emailError) setEmailError(undefined);
|
|
103
|
+
}}
|
|
104
|
+
error={emailError}
|
|
105
|
+
/>
|
|
106
|
+
|
|
107
|
+
<Button
|
|
108
|
+
variant="primary"
|
|
109
|
+
size="lg"
|
|
110
|
+
loading={isLoading}
|
|
111
|
+
onPress={handleSubmit}
|
|
112
|
+
className="mt-2"
|
|
113
|
+
>
|
|
114
|
+
Send Reset Link
|
|
115
|
+
</Button>
|
|
116
|
+
|
|
117
|
+
{onBackToSignIn && (
|
|
118
|
+
<View className="flex-row justify-center items-center gap-1.5 pt-2">
|
|
119
|
+
<Pressable onPress={onBackToSignIn}>
|
|
120
|
+
<Text className="text-sm font-semibold text-primary">
|
|
121
|
+
Back to Sign In
|
|
122
|
+
</Text>
|
|
123
|
+
</Pressable>
|
|
124
|
+
</View>
|
|
125
|
+
)}
|
|
126
|
+
</View>
|
|
127
|
+
)}
|
|
128
|
+
</View>
|
|
129
|
+
</ScrollView>
|
|
130
|
+
</KeyboardAvoidingView>
|
|
131
|
+
);
|
|
132
|
+
};
|