Add login / signin / reset password pages

pull/4/head
Anthony RICHARD 5 months ago
parent 361038f2f2
commit c41f48db92

@ -0,0 +1,58 @@
import BackButton from "./components/BackButton";
import React from "react";
import { Card } from "./components/ui/card";
import Screen from "./components/Screen";
import { Heading } from "./components/ui/heading";
import { AntDesign } from "@expo/vector-icons";
import { VStack } from "./components/ui/vstack";
import { Text } from "./components/ui/text";
import { Button, ButtonIcon, ButtonText } from "./components/ui/button";
import { LockIcon } from "./components/ui/icon";
function truncateEmail(email: string) {
const splitedEmail = email.split("@");
let hiddenPart = splitedEmail[0][0];
for (let i = 1; i < splitedEmail[0].length - 1; i++) {
hiddenPart += "⋆";
}
return hiddenPart + splitedEmail[0].slice(-1) + "@" + splitedEmail[1];
}
type props = { email?: string };
export default function CodeSentPage({ email }: props) {
return (
<Screen>
<BackButton icon="close" link={"/LoginPage"} action="primary" />
<Card className="rounded-3xl mt-16">
<VStack space="lg">
<AntDesign
className="bg-green-100 p-4 rounded-3xl h-16 w-16 self-center"
name="check"
size={30}
color={"green"}
/>
<Heading className="text-center" size="3xl">
Code envoyé !
</Heading>
<VStack space="md">
<Text className="text-center" size="xl">
Nous t'avons envoyé le code de vérification à
</Text>
<Text className="text-center" size="2xl" bold={true}>
{truncateEmail(email ?? "test@Optifit.com")}
</Text>
<Text className="text-center" size="xl">
Clique sur renvoyer si tu n'as pas reçu dici quelques secondes !
🔥
</Text>
</VStack>
<Button size="xl" action="secondary">
<ButtonIcon as={LockIcon} />
<ButtonText>Renvoyer le code</ButtonText>
</Button>
</VStack>
</Card>
</Screen>
);
}

@ -0,0 +1,67 @@
import { Text } from "./components/ui/text";
import { VStack } from "./components/ui/vstack";
import { Button, ButtonGroup } from "./components/ui/button";
import { Feather } from "@expo/vector-icons";
import { HStack } from "./components/ui/hstack";
import React from "react";
import { FeatherIconNames } from "@expo/vector-icons/build/Feather";
import { Box } from "./components/ui/box";
import { Heading } from "./components/ui/heading";
import { Link } from "expo-router";
import { LinkText } from "./components/ui/link";
import LoginForm from "./components/form/LoginForm";
import Screen from "./components/Screen";
const socialNetworkButtons: ISocialNetworkButtons[] = [
{ icon: "instagram" },
{ icon: "facebook" },
{ icon: "linkedin" },
];
interface ISocialNetworkButtons {
icon: FeatherIconNames;
}
export default function LoginPage() {
return (
<Screen>
<Box className="h-full justify-center">
<VStack space="2xl">
<VStack space="sm">
<Heading className="text-center" size="3xl">
Connexion à Optifit
</Heading>
<Text size="lg" className="text-center">
Personnalise ton expérience du sport avec Optifit, ton nouveau
coach IA.
</Text>
</VStack>
<LoginForm />
<ButtonGroup className="justify-center" flexDirection="row">
{socialNetworkButtons.map((socialNetworkButton) => (
<Button
key={socialNetworkButton.icon}
size="xl"
variant="outline"
action="primary"
>
<Feather name={socialNetworkButton.icon} size={27} />
</Button>
))}
</ButtonGroup>
<VStack>
<HStack className="justify-center items-center" space="xs">
<Text>Tu n'as pas encore de compte ?</Text>
<Link href="/SigninPage">
<LinkText bold={true}>Inscris-toi !</LinkText>
</Link>
</HStack>
<Link className="text-center" href="/ResetPasswordPage">
<LinkText bold={true}>Mot de passe oublié ?</LinkText>
</Link>
</VStack>
</VStack>
</Box>
</Screen>
);
}

@ -0,0 +1,65 @@
import { Text } from "./components/ui/text";
import {
Button,
ButtonGroup,
ButtonIcon,
ButtonText,
} from "./components/ui/button";
import { VStack } from "./components/ui/vstack";
import React from "react";
import {
ArrowRightIcon,
LockIcon,
MailIcon,
MessageCircleIcon,
} from "./components/ui/icon";
import BackButton from "./components/BackButton";
import Screen from "./components/Screen";
import { Heading } from "./components/ui/heading";
import { Link } from "expo-router";
const resetButtons: IResetButton[] = [
{ icon: MailIcon, text: "Envoyer par email" },
{ icon: LockIcon, text: "Envoyer par 2FA" },
{ icon: MessageCircleIcon, text: "Envoyer par SMS" },
];
interface IResetButton {
icon: React.ElementType;
text: string;
}
export default function ResetPasswordPage() {
return (
<Screen>
<BackButton link={"/LoginPage"} />
<VStack space="2xl">
<VStack space="sm">
<Heading className="text-center" size="3xl">
Réinitialisation de ton mot de passe
</Heading>
<Text size="lg" className="text-center">
Selectionne une méthode pour recevoir ton code temporaire
</Text>
</VStack>
<ButtonGroup space="xl">
{resetButtons.map((resetButton) => (
<Link href={"/CodeSentPage"} asChild>
<Button
className="justify-between"
key={resetButton.text}
size="2xl"
action="primary"
variant="outline"
>
<ButtonIcon as={resetButton.icon} />
<ButtonText>{resetButton.text}</ButtonText>
<ButtonIcon as={ArrowRightIcon} />
</Button>
</Link>
))}
</ButtonGroup>
</VStack>
</Screen>
);
}

@ -0,0 +1,36 @@
import { Text } from "./components/ui/text";
import { VStack } from "./components/ui/vstack";
import { Box } from "./components/ui/box";
import { HStack } from "./components/ui/hstack";
import React from "react";
import SigninForm from "./components/form/SigninForm";
import Screen from "./components/Screen";
import { Heading } from "./components/ui/heading";
import { Link } from "expo-router";
import { LinkText } from "./components/ui/link";
export default function SigninPage() {
return (
<Screen>
<Box className="h-full justify-center">
<VStack space="2xl">
<VStack space="sm">
<Heading className="text-center" size="2xl">
Inscris-toi gratuitement
</Heading>
<Text size="lg" className="text-center">
Génère un programme personnalisé en quelques clics et 1 minute !
</Text>
</VStack>
<SigninForm />
<HStack className="justify-center items-center" space="xs">
<Text>Tu as déjà un compte ?</Text>
<Link href="/LoginPage">
<LinkText bold={true}>Connectes-toi !</LinkText>
</Link>
</HStack>
</VStack>
</Box>
</Screen>
);
}

@ -1,13 +1,14 @@
import { Slot } from "expo-router";
import "../global.css";
import {GluestackUIProvider} from "@/app/components/ui/gluestack-ui-provider";
import { GluestackUIProvider } from "./components/ui/gluestack-ui-provider";
import React from "react";
import NavBar from "@/app/components/NavBar";
export default function RootLayout() {
return (
<GluestackUIProvider>
<NavBar/>
</GluestackUIProvider>
);
return (
<GluestackUIProvider>
<NavBar />
<Slot />
</GluestackUIProvider>
);
}

@ -0,0 +1,24 @@
import { AntDesign } from "@expo/vector-icons";
import { Button, ButtonActions } from "./ui/button";
import { AntDesignIconNames } from "@expo/vector-icons/build/AntDesign";
import { Href, Link } from "expo-router";
type props = {
icon?: AntDesignIconNames;
link: Href;
action?: ButtonActions;
};
export default function BackButton({ icon, link, action }: props) {
return (
<Button className="h-16 w-16 mb-4" action={action ?? "secondary"}>
<Link href={link}>
<AntDesign
name={icon ?? "arrowleft"}
size={30}
color={action == "primary" ? "white" : "black"}
/>
</Link>
</Button>
);
}

@ -0,0 +1,8 @@
import { PropsWithChildren } from "react";
import { Box } from "./ui/box";
type props = PropsWithChildren;
export default function Screen({ children }: props) {
return <Box className={"h-full p-4"}>{children}</Box>;
}

@ -0,0 +1,19 @@
import {
FormControlError,
FormControlErrorIcon,
FormControlErrorText,
} from "../ui/form-control";
import { AlertCircleIcon } from "../ui/icon";
type props = { text: string };
export default function FormError({ text }: props) {
return (
<FormControlError className="mb-2 border-[1px] border-red-500 p-4 rounded-3xl bg-red-100 gap-2">
<FormControlErrorIcon as={AlertCircleIcon} />
<FormControlErrorText className="text-black font-bold">
{text}
</FormControlErrorText>
</FormControlError>
);
}

@ -0,0 +1,58 @@
import { Button, ButtonText, ButtonIcon } from "../ui/button";
import {
FormControl,
FormControlLabel,
FormControlLabelText,
} from "../ui/form-control";
import { MailIcon, ArrowRightIcon } from "../ui/icon";
import { Input, InputIcon, InputField } from "../ui/input";
import { VStack } from "../ui/vstack";
import React from "react";
import PasswordField from "./PasswordField";
import FormError from "./FormError";
export default function LoginForm() {
const REQUIRED_ERROR = "Au moins un des champs requis est vide !";
const [emailValue, setEmailValue] = React.useState("");
const [isEmailInvalid, setIsEmailInvalid] = React.useState(false);
const [passwordValue, setPasswordValue] = React.useState("");
const [isPasswordInvalid, setIsPasswordInvalid] = React.useState(false);
const [isFormInvalid, setIsFormInvalid] = React.useState(false);
const handleSubmit = () => {
//check for empty fields
setIsEmailInvalid(emailValue == "");
setIsPasswordInvalid(passwordValue == "");
setIsFormInvalid(isEmailInvalid || isPasswordInvalid);
};
return (
<FormControl className="gap-4" isInvalid={isFormInvalid}>
<FormControl isInvalid={isEmailInvalid}>
<FormControlLabel>
<FormControlLabelText bold={true}>Adresse mail</FormControlLabelText>
</FormControlLabel>
<Input variant="outline" size="xl">
<InputIcon color="black" as={MailIcon} />
<InputField
value={emailValue}
onChangeText={setEmailValue}
placeholder="Test@optifit.com"
/>
</Input>
</FormControl>
<FormControl isInvalid={isPasswordInvalid}>
<FormControlLabel>
<FormControlLabelText bold={true}>Mot de passe</FormControlLabelText>
</FormControlLabel>
<PasswordField value={passwordValue} onChangeText={setPasswordValue} />
</FormControl>
<VStack>
<FormError text={REQUIRED_ERROR} />
<Button size="xl" onPress={handleSubmit}>
<ButtonText>Se connecter</ButtonText>
<ButtonIcon as={ArrowRightIcon} />
</Button>
</VStack>
</FormControl>
);
}

@ -0,0 +1,31 @@
import React from "react";
import { LockIcon, EyeIcon, EyeOffIcon } from "../ui/icon";
import { Input, InputIcon, InputField, InputSlot } from "../ui/input";
import { VariantProps } from "@gluestack-ui/nativewind-utils";
type props = React.ComponentProps<typeof Input> &
VariantProps<typeof InputField>;
export default function PasswordField({ value, onChangeText }: props) {
const [showPassword, setShowPassword] = React.useState(false);
const handleState = () => {
setShowPassword((showState) => {
return !showState;
});
};
return (
<Input variant="outline" size="xl">
<InputIcon color="black" as={LockIcon} />
<InputField
value={value}
onChangeText={onChangeText}
type={showPassword ? "text" : "password"}
placeholder="⋆⋆⋆⋆⋆⋆⋆⋆⋆⋆⋆⋆"
/>
<InputSlot className="pr-3" onPress={handleState}>
<InputIcon as={showPassword ? EyeIcon : EyeOffIcon} />
</InputSlot>
</Input>
);
}

@ -0,0 +1,125 @@
import { Button, ButtonText, ButtonIcon } from "../ui/button";
import {
FormControl,
FormControlLabel,
FormControlLabelText,
} from "../ui/form-control";
import { MailIcon, ArrowRightIcon } from "../ui/icon";
import { Input, InputIcon, InputField } from "../ui/input";
import { VStack } from "../ui/vstack";
import React from "react";
import FormError from "./FormError";
import PasswordField from "./PasswordField";
export default function SigninForm() {
const REQUIRED = "Au moins un des champs requis est vide !";
const INVALID_EMAIL = "Adresse mail invalide !";
// TODO Définir ce qu'est un mdp valide
const INVALID_PASSWORD = "Mot de passe invalide !";
const NOT_MATCHING_PASSWORD = "Les mots de passe ne correspondent pas !";
const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
const [emailValue, setEmailValue] = React.useState("");
const [isEmailInvalid, setIsEmailInvalid] = React.useState(false);
const [passwordValue, setPasswordValue] = React.useState("");
const [isPasswordInvalid, setIsPasswordInvalid] = React.useState(false);
const [passwordConfirmValue, setPasswordConfirmValue] = React.useState("");
const [isPasswordConfirmInvalid, setIsPasswordConfirmInvalid] =
React.useState(false);
const [isFormInvalid, setIsFormInvalid] = React.useState(false);
const [formError, setFormError] = React.useState("");
function validateForm() {
setFormError("");
setIsFormInvalid(false);
}
function invalidateForm(error: string) {
setFormError(error);
setIsFormInvalid(true);
}
const handleSubmit = () => {
//check for valid email block
if (emailValue != "") {
if (emailRegex.test(emailValue)) {
setIsEmailInvalid(false);
} else {
setIsEmailInvalid(true);
invalidateForm(INVALID_EMAIL);
return;
}
} else {
setIsEmailInvalid(true);
invalidateForm(REQUIRED);
return;
}
//TODO : check for valid password block
if (passwordValue != "") {
setIsPasswordInvalid(false);
} else {
setIsPasswordInvalid(true);
invalidateForm(REQUIRED);
return;
}
//check for valid password confirmation
if (passwordConfirmValue != "") {
if (passwordConfirmValue == passwordValue) {
setIsPasswordConfirmInvalid(false);
validateForm();
} else {
setIsPasswordConfirmInvalid(true);
invalidateForm(NOT_MATCHING_PASSWORD);
return;
}
} else {
setIsPasswordConfirmInvalid(true);
invalidateForm(REQUIRED);
return;
}
};
return (
<FormControl className="gap-4" isInvalid={isFormInvalid}>
<FormControl isInvalid={isEmailInvalid}>
<FormControlLabel>
<FormControlLabelText bold={true}>Adresse mail</FormControlLabelText>
</FormControlLabel>
<Input variant="outline" size="xl">
<InputIcon color="black" as={MailIcon} />
<InputField
value={emailValue}
onChangeText={setEmailValue}
placeholder="Test@optifit.com"
/>
</Input>
</FormControl>
<FormControl isInvalid={isPasswordInvalid}>
<FormControlLabel>
<FormControlLabelText bold={true}>Mot de passe</FormControlLabelText>
</FormControlLabel>
<PasswordField value={passwordValue} onChangeText={setPasswordValue} />
</FormControl>
<FormControl isInvalid={isPasswordConfirmInvalid}>
<FormControlLabel>
<FormControlLabelText bold={true}>
Confirmation du mot de passe
</FormControlLabelText>
</FormControlLabel>
<PasswordField
value={passwordConfirmValue}
onChangeText={setPasswordConfirmValue}
/>
</FormControl>
<VStack>
<FormError text={formError} />
<Button size="xl" onPress={handleSubmit}>
<ButtonText>S'inscrire</ButtonText>
<ButtonIcon as={ArrowRightIcon} />
</Button>
</VStack>
</FormControl>
);
}

@ -1,8 +1,8 @@
import React from 'react';
import { View, ViewProps } from 'react-native';
import React from "react";
import { View, ViewProps } from "react-native";
import type { VariantProps } from '@gluestack-ui/nativewind-utils';
import { boxStyle } from './styles';
import type { VariantProps } from "@gluestack-ui/nativewind-utils";
import { boxStyle } from "./styles";
type IBoxProps = ViewProps &
VariantProps<typeof boxStyle> & { className?: string };
@ -15,5 +15,5 @@ const Box = React.forwardRef<React.ElementRef<typeof View>, IBoxProps>(
}
);
Box.displayName = 'Box';
Box.displayName = "Box";
export { Box };

@ -1,17 +1,17 @@
'use client';
import React from 'react';
import { createButton } from '@gluestack-ui/button';
import { tva } from '@gluestack-ui/nativewind-utils/tva';
"use client";
import React from "react";
import { createButton } from "@gluestack-ui/button";
import { tva } from "@gluestack-ui/nativewind-utils/tva";
import {
withStyleContext,
useStyleContext,
} from '@gluestack-ui/nativewind-utils/withStyleContext';
import { cssInterop } from 'nativewind';
import { ActivityIndicator, Pressable, Text, View } from 'react-native';
import type { VariantProps } from '@gluestack-ui/nativewind-utils';
import { PrimitiveIcon, UIIcon } from '@gluestack-ui/icon';
} from "@gluestack-ui/nativewind-utils/withStyleContext";
import { cssInterop } from "nativewind";
import { ActivityIndicator, Pressable, Text, View } from "react-native";
import type { VariantProps } from "@gluestack-ui/nativewind-utils";
import { PrimitiveIcon, UIIcon } from "@gluestack-ui/icon";
const SCOPE = 'BUTTON';
const SCOPE = "BUTTON";
const Root = withStyleContext(Pressable, SCOPE);
@ -25,262 +25,268 @@ const UIButton = createButton({
cssInterop(PrimitiveIcon, {
className: {
target: 'style',
target: "style",
nativeStyleToProp: {
height: true,
width: true,
fill: true,
color: 'classNameColor',
color: "classNameColor",
stroke: true,
},
},
});
const buttonStyle = tva({
base: 'group/button rounded bg-primary-500 flex-row items-center justify-center data-[focus-visible=true]:web:outline-none data-[focus-visible=true]:web:ring-2 data-[disabled=true]:opacity-40 gap-2',
base: "group/button rounded-3xl bg-primary-500 flex-row items-center justify-center data-[focus-visible=true]:web:outline-none data-[focus-visible=true]:web:ring-2 data-[disabled=true]:opacity-40 gap-2",
variants: {
action: {
primary:
'bg-primary-500 data-[hover=true]:bg-primary-600 data-[active=true]:bg-primary-700 border-primary-300 data-[hover=true]:border-primary-400 data-[active=true]:border-primary-500 data-[focus-visible=true]:web:ring-indicator-info',
"bg-primary-500 data-[hover=true]:bg-primary-600 data-[active=true]:bg-primary-700 border-primary-300 data-[hover=true]:border-primary-400 data-[active=true]:border-primary-500 data-[focus-visible=true]:web:ring-indicator-info",
secondary:
'bg-secondary-500 border-secondary-300 data-[hover=true]:bg-secondary-600 data-[hover=true]:border-secondary-400 data-[active=true]:bg-secondary-700 data-[active=true]:border-secondary-700 data-[focus-visible=true]:web:ring-indicator-info',
"bg-secondary-500 border-secondary-300 data-[hover=true]:bg-secondary-600 data-[hover=true]:border-secondary-400 data-[active=true]:bg-secondary-700 data-[active=true]:border-secondary-700 data-[focus-visible=true]:web:ring-indicator-info",
positive:
'bg-success-500 border-success-300 data-[hover=true]:bg-success-600 data-[hover=true]:border-success-400 data-[active=true]:bg-success-700 data-[active=true]:border-success-500 data-[focus-visible=true]:web:ring-indicator-info',
"bg-success-500 border-success-300 data-[hover=true]:bg-success-600 data-[hover=true]:border-success-400 data-[active=true]:bg-success-700 data-[active=true]:border-success-500 data-[focus-visible=true]:web:ring-indicator-info",
negative:
'bg-error-500 border-error-300 data-[hover=true]:bg-error-600 data-[hover=true]:border-error-400 data-[active=true]:bg-error-700 data-[active=true]:border-error-500 data-[focus-visible=true]:web:ring-indicator-info',
"bg-error-500 border-error-300 data-[hover=true]:bg-error-600 data-[hover=true]:border-error-400 data-[active=true]:bg-error-700 data-[active=true]:border-error-500 data-[focus-visible=true]:web:ring-indicator-info",
default:
'bg-transparent data-[hover=true]:bg-background-50 data-[active=true]:bg-transparent',
"bg-transparent data-[hover=true]:bg-background-50 data-[active=true]:bg-transparent",
},
variant: {
link: 'px-0',
link: "px-0",
outline:
'bg-transparent border data-[hover=true]:bg-background-50 data-[active=true]:bg-transparent',
solid: '',
"bg-transparent border data-[hover=true]:bg-background-50 data-[active=true]:bg-transparent",
solid: "",
},
size: {
xs: 'px-3.5 h-8',
sm: 'px-4 h-9',
md: 'px-5 h-10',
lg: 'px-6 h-11',
xl: 'px-7 h-12',
xs: "h-8",
sm: "h-9",
md: "h-10",
lg: "h-11",
xl: "p-4 h-16",
"2xl": "p-6 h-20",
"3xl": "p-8 h-24",
},
},
compoundVariants: [
{
action: 'primary',
variant: 'link',
action: "primary",
variant: "link",
class:
'px-0 bg-transparent data-[hover=true]:bg-transparent data-[active=true]:bg-transparent',
"px-0 bg-transparent data-[hover=true]:bg-transparent data-[active=true]:bg-transparent",
},
{
action: 'secondary',
variant: 'link',
action: "secondary",
variant: "link",
class:
'px-0 bg-transparent data-[hover=true]:bg-transparent data-[active=true]:bg-transparent',
"px-0 bg-transparent data-[hover=true]:bg-transparent data-[active=true]:bg-transparent",
},
{
action: 'positive',
variant: 'link',
action: "positive",
variant: "link",
class:
'px-0 bg-transparent data-[hover=true]:bg-transparent data-[active=true]:bg-transparent',
"px-0 bg-transparent data-[hover=true]:bg-transparent data-[active=true]:bg-transparent",
},
{
action: 'negative',
variant: 'link',
action: "negative",
variant: "link",
class:
'px-0 bg-transparent data-[hover=true]:bg-transparent data-[active=true]:bg-transparent',
"px-0 bg-transparent data-[hover=true]:bg-transparent data-[active=true]:bg-transparent",
},
{
action: 'primary',
variant: 'outline',
action: "primary",
variant: "outline",
class:
'bg-transparent data-[hover=true]:bg-background-50 data-[active=true]:bg-transparent',
"bg-transparent data-[hover=true]:bg-background-50 data-[active=true]:bg-transparent",
},
{
action: 'secondary',
variant: 'outline',
action: "secondary",
variant: "outline",
class:
'bg-transparent data-[hover=true]:bg-background-50 data-[active=true]:bg-transparent',
"bg-transparent data-[hover=true]:bg-background-50 data-[active=true]:bg-transparent",
},
{
action: 'positive',
variant: 'outline',
action: "positive",
variant: "outline",
class:
'bg-transparent data-[hover=true]:bg-background-50 data-[active=true]:bg-transparent',
"bg-transparent data-[hover=true]:bg-background-50 data-[active=true]:bg-transparent",
},
{
action: 'negative',
variant: 'outline',
action: "negative",
variant: "outline",
class:
'bg-transparent data-[hover=true]:bg-background-50 data-[active=true]:bg-transparent',
"bg-transparent data-[hover=true]:bg-background-50 data-[active=true]:bg-transparent",
},
],
});
const buttonTextStyle = tva({
base: 'text-typography-0 font-semibold web:select-none',
base: "text-typography-0 font-semibold web:select-none",
parentVariants: {
action: {
primary:
'text-primary-600 data-[hover=true]:text-primary-600 data-[active=true]:text-primary-700',
"text-primary-600 data-[hover=true]:text-primary-600 data-[active=true]:text-primary-700",
secondary:
'text-typography-500 data-[hover=true]:text-typography-600 data-[active=true]:text-typography-700',
"text-typography-500 data-[hover=true]:text-typography-600 data-[active=true]:text-typography-700",
positive:
'text-success-600 data-[hover=true]:text-success-600 data-[active=true]:text-success-700',
"text-success-600 data-[hover=true]:text-success-600 data-[active=true]:text-success-700",
negative:
'text-error-600 data-[hover=true]:text-error-600 data-[active=true]:text-error-700',
"text-error-600 data-[hover=true]:text-error-600 data-[active=true]:text-error-700",
},
variant: {
link: 'data-[hover=true]:underline data-[active=true]:underline',
outline: '',
link: "underline data-[hover=true]:underline data-[active=true]:underline text-orange-400",
outline: "",
solid:
'text-typography-0 data-[hover=true]:text-typography-0 data-[active=true]:text-typography-0',
"text-typography-0 data-[hover=true]:text-typography-0 data-[active=true]:text-typography-0",
},
size: {
xs: 'text-xs',
sm: 'text-sm',
md: 'text-base',
lg: 'text-lg',
xl: 'text-xl',
xs: "text-xs",
sm: "text-sm",
md: "text-base",
lg: "text-lg",
xl: "text-xl",
"2xl": "text-2xl",
"3xl": "text-3xl",
},
},
parentCompoundVariants: [
{
variant: 'solid',
action: 'primary',
variant: "solid",
action: "primary",
class:
'text-typography-0 data-[hover=true]:text-typography-0 data-[active=true]:text-typography-0',
"text-typography-0 data-[hover=true]:text-typography-0 data-[active=true]:text-typography-0",
},
{
variant: 'solid',
action: 'secondary',
variant: "solid",
action: "secondary",
class:
'text-typography-800 data-[hover=true]:text-typography-800 data-[active=true]:text-typography-800',
"text-typography-800 data-[hover=true]:text-typography-800 data-[active=true]:text-typography-800",
},
{
variant: 'solid',
action: 'positive',
variant: "solid",
action: "positive",
class:
'text-typography-0 data-[hover=true]:text-typography-0 data-[active=true]:text-typography-0',
"text-typography-0 data-[hover=true]:text-typography-0 data-[active=true]:text-typography-0",
},
{
variant: 'solid',
action: 'negative',
variant: "solid",
action: "negative",
class:
'text-typography-0 data-[hover=true]:text-typography-0 data-[active=true]:text-typography-0',
"text-typography-0 data-[hover=true]:text-typography-0 data-[active=true]:text-typography-0",
},
{
variant: 'outline',
action: 'primary',
variant: "outline",
action: "primary",
class:
'text-primary-500 data-[hover=true]:text-primary-500 data-[active=true]:text-primary-500',
"text-primary-500 data-[hover=true]:text-primary-500 data-[active=true]:text-primary-500",
},
{
variant: 'outline',
action: 'secondary',
variant: "outline",
action: "secondary",
class:
'text-typography-500 data-[hover=true]:text-primary-600 data-[active=true]:text-typography-700',
"text-typography-500 data-[hover=true]:text-primary-600 data-[active=true]:text-typography-700",
},
{
variant: 'outline',
action: 'positive',
variant: "outline",
action: "positive",
class:
'text-primary-500 data-[hover=true]:text-primary-500 data-[active=true]:text-primary-500',
"text-primary-500 data-[hover=true]:text-primary-500 data-[active=true]:text-primary-500",
},
{
variant: 'outline',
action: 'negative',
variant: "outline",
action: "negative",
class:
'text-primary-500 data-[hover=true]:text-primary-500 data-[active=true]:text-primary-500',
"text-primary-500 data-[hover=true]:text-primary-500 data-[active=true]:text-primary-500",
},
],
});
const buttonIconStyle = tva({
base: 'fill-none',
base: "fill-none",
parentVariants: {
variant: {
link: 'data-[hover=true]:underline data-[active=true]:underline',
outline: '',
link: "data-[hover=true]:underline data-[active=true]:underline",
outline: "",
solid:
'text-typography-0 data-[hover=true]:text-typography-0 data-[active=true]:text-typography-0',
"text-typography-0 data-[hover=true]:text-typography-0 data-[active=true]:text-typography-0",
},
size: {
xs: 'h-3.5 w-3.5',
sm: 'h-4 w-4',
md: 'h-[18px] w-[18px]',
lg: 'h-[18px] w-[18px]',
xl: 'h-5 w-5',
xs: "h-3.5 w-3.5",
sm: "h-4 w-4",
md: "h-[18px] w-[18px]",
lg: "h-[18px] w-[18px]",
xl: "h-5 w-5",
"2xl": "h-8 w-8",
"3xl": "h-10 w-10",
},
action: {
primary:
'text-primary-600 data-[hover=true]:text-primary-600 data-[active=true]:text-primary-700',
"text-primary-600 data-[hover=true]:text-primary-600 data-[active=true]:text-primary-700",
secondary:
'text-typography-500 data-[hover=true]:text-typography-600 data-[active=true]:text-typography-700',
"text-typography-500 data-[hover=true]:text-typography-600 data-[active=true]:text-typography-700",
positive:
'text-success-600 data-[hover=true]:text-success-600 data-[active=true]:text-success-700',
"text-success-600 data-[hover=true]:text-success-600 data-[active=true]:text-success-700",
negative:
'text-error-600 data-[hover=true]:text-error-600 data-[active=true]:text-error-700',
"text-error-600 data-[hover=true]:text-error-600 data-[active=true]:text-error-700",
},
},
parentCompoundVariants: [
{
variant: 'solid',
action: 'primary',
variant: "solid",
action: "primary",
class:
'text-typography-0 data-[hover=true]:text-typography-0 data-[active=true]:text-typography-0',
"text-typography-0 data-[hover=true]:text-typography-0 data-[active=true]:text-typography-0",
},
{
variant: 'solid',
action: 'secondary',
variant: "solid",
action: "secondary",
class:
'text-typography-800 data-[hover=true]:text-typography-800 data-[active=true]:text-typography-800',
"text-typography-800 data-[hover=true]:text-typography-800 data-[active=true]:text-typography-800",
},
{
variant: 'solid',
action: 'positive',
variant: "solid",
action: "positive",
class:
'text-typography-0 data-[hover=true]:text-typography-0 data-[active=true]:text-typography-0',
"text-typography-0 data-[hover=true]:text-typography-0 data-[active=true]:text-typography-0",
},
{
variant: 'solid',
action: 'negative',
variant: "solid",
action: "negative",
class:
'text-typography-0 data-[hover=true]:text-typography-0 data-[active=true]:text-typography-0',
"text-typography-0 data-[hover=true]:text-typography-0 data-[active=true]:text-typography-0",
},
],
});
const buttonGroupStyle = tva({
base: '',
base: "",
variants: {
space: {
'xs': 'gap-1',
'sm': 'gap-2',
'md': 'gap-3',
'lg': 'gap-4',
'xl': 'gap-5',
'2xl': 'gap-6',
'3xl': 'gap-7',
'4xl': 'gap-8',
xs: "gap-1",
sm: "gap-2",
md: "gap-3",
lg: "gap-4",
xl: "gap-5",
"2xl": "gap-6",
"3xl": "gap-7",
"4xl": "gap-8",
},
isAttached: {
true: 'gap-0',
true: "gap-0",
},
flexDirection: {
'row': 'flex-row',
'column': 'flex-col',
'row-reverse': 'flex-row-reverse',
'column-reverse': 'flex-col-reverse',
row: "flex-row",
column: "flex-col",
"row-reverse": "flex-row-reverse",
"column-reverse": "flex-col-reverse",
},
},
});
type IButtonProps = Omit<
React.ComponentPropsWithoutRef<typeof UIButton>,
'context'
"context"
> &
VariantProps<typeof buttonStyle> & { className?: string };
@ -289,7 +295,7 @@ const Button = React.forwardRef<
IButtonProps
>(
(
{ className, variant = 'solid', size = 'md', action = 'primary', ...props },
{ className, variant = "solid", size = "md", action = "primary", ...props },
ref
) => {
return (
@ -355,7 +361,7 @@ const ButtonIcon = React.forwardRef<
action: parentAction,
} = useStyleContext(SCOPE);
if (typeof size === 'number') {
if (typeof size === "number") {
return (
<UIButton.Icon
ref={ref}
@ -403,9 +409,9 @@ const ButtonGroup = React.forwardRef<
(
{
className,
space = 'md',
space = "md",
isAttached = false,
flexDirection = 'column',
flexDirection = "column",
...props
},
ref
@ -425,10 +431,11 @@ const ButtonGroup = React.forwardRef<
}
);
Button.displayName = 'Button';
ButtonText.displayName = 'ButtonText';
ButtonSpinner.displayName = 'ButtonSpinner';
ButtonIcon.displayName = 'ButtonIcon';
ButtonGroup.displayName = 'ButtonGroup';
Button.displayName = "Button";
ButtonText.displayName = "ButtonText";
ButtonSpinner.displayName = "ButtonSpinner";
ButtonIcon.displayName = "ButtonIcon";
ButtonGroup.displayName = "ButtonGroup";
export { Button, ButtonText, ButtonSpinner, ButtonIcon, ButtonGroup };
export type ButtonActions = keyof typeof buttonStyle.variants.action;

@ -0,0 +1,23 @@
import React from 'react';
import type { VariantProps } from '@gluestack-ui/nativewind-utils';
import { View, ViewProps } from 'react-native';
import { cardStyle } from './styles';
type ICardProps = ViewProps &
VariantProps<typeof cardStyle> & { className?: string };
const Card = React.forwardRef<React.ElementRef<typeof View>, ICardProps>(
({ className, size = 'md', variant = 'elevated', ...props }, ref) => {
return (
<View
className={cardStyle({ size, variant, class: className })}
{...props}
ref={ref}
/>
);
}
);
Card.displayName = 'Card';
export { Card };

@ -0,0 +1,22 @@
import React from 'react';
import { cardStyle } from './styles';
import type { VariantProps } from '@gluestack-ui/nativewind-utils';
type ICardProps = React.ComponentPropsWithoutRef<'div'> &
VariantProps<typeof cardStyle>;
const Card = React.forwardRef<HTMLDivElement, ICardProps>(
({ className, size = 'md', variant = 'elevated', ...props }, ref) => {
return (
<div
className={cardStyle({ size, variant, class: className })}
{...props}
ref={ref}
/>
);
}
);
Card.displayName = 'Card';
export { Card };

@ -0,0 +1,20 @@
import { tva } from '@gluestack-ui/nativewind-utils/tva';
import { isWeb } from '@gluestack-ui/nativewind-utils/IsWeb';
const baseStyle = isWeb ? 'flex flex-col relative z-0' : '';
export const cardStyle = tva({
base: baseStyle,
variants: {
size: {
sm: 'p-3 rounded',
md: 'p-4 rounded-md',
lg: 'p-6 rounded-xl',
},
variant: {
elevated: 'bg-background-0',
outline: 'border border-outline-200 ',
ghost: 'rounded-none',
filled: 'bg-background-50',
},
},
});

@ -0,0 +1,468 @@
'use client';
import { Text, View } from 'react-native';
import React from 'react';
import { createFormControl } from '@gluestack-ui/form-control';
import { tva } from '@gluestack-ui/nativewind-utils/tva';
import {
withStyleContext,
useStyleContext,
} from '@gluestack-ui/nativewind-utils/withStyleContext';
import { cssInterop } from 'nativewind';
import type { VariantProps } from '@gluestack-ui/nativewind-utils';
import { PrimitiveIcon, UIIcon } from '@gluestack-ui/icon';
const SCOPE = 'FORM_CONTROL';
const formControlStyle = tva({
base: 'flex flex-col',
variants: {
size: {
sm: '',
md: '',
lg: '',
},
},
});
const formControlErrorIconStyle = tva({
base: 'text-error-700 fill-none',
variants: {
size: {
'2xs': 'h-3 w-3',
'xs': 'h-3.5 w-3.5',
'sm': 'h-4 w-4',
'md': 'h-[18px] w-[18px]',
'lg': 'h-5 w-5',
'xl': 'h-6 w-6',
},
},
});
const formControlErrorStyle = tva({
base: 'flex flex-row justify-start items-center mt-1 gap-1',
});
const formControlErrorTextStyle = tva({
base: 'text-error-700',
variants: {
isTruncated: {
true: 'web:truncate',
},
bold: {
true: 'font-bold',
},
underline: {
true: 'underline',
},
strikeThrough: {
true: 'line-through',
},
size: {
'2xs': 'text-2xs',
'xs': 'text-xs',
'sm': 'text-sm',
'md': 'text-base',
'lg': 'text-lg',
'xl': 'text-xl',
'2xl': 'text-2xl',
'3xl': 'text-3xl',
'4xl': 'text-4xl',
'5xl': 'text-5xl',
'6xl': 'text-6xl',
},
sub: {
true: 'text-xs',
},
italic: {
true: 'italic',
},
highlight: {
true: 'bg-yellow-500',
},
},
});
const formControlHelperStyle = tva({
base: 'flex flex-row justify-start items-center mt-1',
});
const formControlHelperTextStyle = tva({
base: 'text-typography-500',
variants: {
isTruncated: {
true: 'web:truncate',
},
bold: {
true: 'font-bold',
},
underline: {
true: 'underline',
},
strikeThrough: {
true: 'line-through',
},
size: {
'2xs': 'text-2xs',
'xs': 'text-xs',
'sm': 'text-xs',
'md': 'text-sm',
'lg': 'text-base',
'xl': 'text-xl',
'2xl': 'text-2xl',
'3xl': 'text-3xl',
'4xl': 'text-4xl',
'5xl': 'text-5xl',
'6xl': 'text-6xl',
},
sub: {
true: 'text-xs',
},
italic: {
true: 'italic',
},
highlight: {
true: 'bg-yellow-500',
},
},
});
const formControlLabelStyle = tva({
base: 'flex flex-row justify-start items-center mb-1',
});
const formControlLabelTextStyle = tva({
base: 'font-medium text-typography-900',
variants: {
isTruncated: {
true: 'web:truncate',
},
bold: {
true: 'font-bold',
},
underline: {
true: 'underline',
},
strikeThrough: {
true: 'line-through',
},
size: {
'2xs': 'text-2xs',
'xs': 'text-xs',
'sm': 'text-sm',
'md': 'text-base',
'lg': 'text-lg',
'xl': 'text-xl',
'2xl': 'text-2xl',
'3xl': 'text-3xl',
'4xl': 'text-4xl',
'5xl': 'text-5xl',
'6xl': 'text-6xl',
},
sub: {
true: 'text-xs',
},
italic: {
true: 'italic',
},
highlight: {
true: 'bg-yellow-500',
},
},
});
const formControlLabelAstrickStyle = tva({
base: 'font-medium text-typography-900',
variants: {
isTruncated: {
true: 'web:truncate',
},
bold: {
true: 'font-bold',
},
underline: {
true: 'underline',
},
strikeThrough: {
true: 'line-through',
},
size: {
'2xs': 'text-2xs',
'xs': 'text-xs',
'sm': 'text-sm',
'md': 'text-base',
'lg': 'text-lg',
'xl': 'text-xl',
'2xl': 'text-2xl',
'3xl': 'text-3xl',
'4xl': 'text-4xl',
'5xl': 'text-5xl',
'6xl': 'text-6xl',
},
sub: {
true: 'text-xs',
},
italic: {
true: 'italic',
},
highlight: {
true: 'bg-yellow-500',
},
},
});
type IFormControlLabelAstrickProps = React.ComponentPropsWithoutRef<
typeof Text
> &
VariantProps<typeof formControlLabelAstrickStyle>;
const FormControlLabelAstrick = React.forwardRef<
React.ElementRef<typeof Text>,
IFormControlLabelAstrickProps
>(({ className, ...props }, ref) => {
const { size: parentSize } = useStyleContext(SCOPE);
return (
<Text
ref={ref}
className={formControlLabelAstrickStyle({
parentVariants: { size: parentSize },
class: className,
})}
{...props}
/>
);
});
export const UIFormControl = createFormControl({
Root: withStyleContext(View, SCOPE),
Error: View,
ErrorText: Text,
ErrorIcon: UIIcon,
Label: View,
LabelText: Text,
LabelAstrick: FormControlLabelAstrick,
Helper: View,
HelperText: Text,
});
cssInterop(PrimitiveIcon, {
className: {
target: 'style',
nativeStyleToProp: {
height: true,
width: true,
fill: true,
color: true,
stroke: true,
},
},
});
type IFormControlProps = React.ComponentProps<typeof UIFormControl> &
VariantProps<typeof formControlStyle>;
const FormControl = React.forwardRef<
React.ElementRef<typeof UIFormControl>,
IFormControlProps
>(({ className, size = 'md', ...props }, ref) => {
return (
<UIFormControl
ref={ref}
className={formControlStyle({ size, class: className })}
{...props}
context={{ size }}
/>
);
});
type IFormControlErrorProps = React.ComponentProps<typeof UIFormControl.Error> &
VariantProps<typeof formControlErrorStyle>;
const FormControlError = React.forwardRef<
React.ElementRef<typeof UIFormControl.Error>,
IFormControlErrorProps
>(({ className, ...props }, ref) => {
return (
<UIFormControl.Error
ref={ref}
className={formControlErrorStyle({ class: className })}
{...props}
/>
);
});
type IFormControlErrorTextProps = React.ComponentProps<
typeof UIFormControl.Error.Text
> &
VariantProps<typeof formControlErrorTextStyle>;
const FormControlErrorText = React.forwardRef<
React.ElementRef<typeof UIFormControl.Error.Text>,
IFormControlErrorTextProps
>(({ className, size, ...props }, ref) => {
const { size: parentSize } = useStyleContext(SCOPE);
return (
<UIFormControl.Error.Text
className={formControlErrorTextStyle({
parentVariants: { size: parentSize },
size,
class: className,
})}
ref={ref}
{...props}
/>
);
});
type IFormControlErrorIconProps = React.ComponentProps<
typeof UIFormControl.Error.Icon
> &
VariantProps<typeof formControlErrorIconStyle> & {
height?: number;
width?: number;
};
const FormControlErrorIcon = React.forwardRef<
React.ElementRef<typeof UIFormControl.Error.Icon>,
IFormControlErrorIconProps
>(({ className, size, ...props }, ref) => {
const { size: parentSize } = useStyleContext(SCOPE);
if (typeof size === 'number') {
return (
<UIFormControl.Error.Icon
ref={ref}
{...props}
className={formControlErrorIconStyle({ class: className })}
size={size}
/>
);
} else if (
(props.height !== undefined || props.width !== undefined) &&
size === undefined
) {
return (
<UIFormControl.Error.Icon
ref={ref}
{...props}
className={formControlErrorIconStyle({ class: className })}
/>
);
}
return (
<UIFormControl.Error.Icon
className={formControlErrorIconStyle({
parentVariants: { size: parentSize },
size,
class: className,
})}
{...props}
/>
);
});
type IFormControlLabelProps = React.ComponentProps<typeof UIFormControl.Label> &
VariantProps<typeof formControlLabelStyle>;
const FormControlLabel = React.forwardRef<
React.ElementRef<typeof UIFormControl.Label>,
IFormControlLabelProps
>(({ className, ...props }, ref) => {
return (
<UIFormControl.Label
ref={ref}
className={formControlLabelStyle({ class: className })}
{...props}
/>
);
});
type IFormControlLabelTextProps = React.ComponentProps<
typeof UIFormControl.Label.Text
> &
VariantProps<typeof formControlLabelTextStyle>;
const FormControlLabelText = React.forwardRef<
React.ElementRef<typeof UIFormControl.Label.Text>,
IFormControlLabelTextProps
>(({ className, size, ...props }, ref) => {
const { size: parentSize } = useStyleContext(SCOPE);
return (
<UIFormControl.Label.Text
className={formControlLabelTextStyle({
parentVariants: { size: parentSize },
size,
class: className,
})}
ref={ref}
{...props}
/>
);
});
type IFormControlHelperProps = React.ComponentProps<
typeof UIFormControl.Helper
> &
VariantProps<typeof formControlHelperStyle>;
const FormControlHelper = React.forwardRef<
React.ElementRef<typeof UIFormControl.Helper>,
IFormControlHelperProps
>(({ className, ...props }, ref) => {
return (
<UIFormControl.Helper
ref={ref}
className={formControlHelperStyle({
class: className,
})}
{...props}
/>
);
});
type IFormControlHelperTextProps = React.ComponentProps<
typeof UIFormControl.Helper.Text
> &
VariantProps<typeof formControlHelperTextStyle>;
const FormControlHelperText = React.forwardRef<
React.ElementRef<typeof UIFormControl.Helper.Text>,
IFormControlHelperTextProps
>(({ className, size, ...props }, ref) => {
const { size: parentSize } = useStyleContext(SCOPE);
return (
<UIFormControl.Helper.Text
className={formControlHelperTextStyle({
parentVariants: { size: parentSize },
size,
class: className,
})}
ref={ref}
{...props}
/>
);
});
FormControl.displayName = 'FormControl';
FormControlError.displayName = 'FormControlError';
FormControlErrorText.displayName = 'FormControlErrorText';
FormControlErrorIcon.displayName = 'FormControlErrorIcon';
FormControlLabel.displayName = 'FormControlLabel';
FormControlLabelText.displayName = 'FormControlLabelText';
FormControlLabelAstrick.displayName = 'FormControlLabelAstrick';
FormControlHelper.displayName = 'FormControlHelper';
FormControlHelperText.displayName = 'FormControlHelperText';
export {
FormControl,
FormControlError,
FormControlErrorText,
FormControlErrorIcon,
FormControlLabel,
FormControlLabelText,
FormControlLabelAstrick,
FormControlHelper,
FormControlHelperText,
};

@ -0,0 +1,219 @@
import React, { forwardRef, memo } from 'react';
import { H1, H2, H3, H4, H5, H6 } from '@expo/html-elements';
import { headingStyle } from './styles';
import type { VariantProps } from '@gluestack-ui/nativewind-utils';
import { cssInterop } from 'nativewind';
type IHeadingProps = VariantProps<typeof headingStyle> &
React.ComponentPropsWithoutRef<typeof H1> & {
as?: React.ElementType;
};
cssInterop(H1, { className: 'style' });
cssInterop(H2, { className: 'style' });
cssInterop(H3, { className: 'style' });
cssInterop(H4, { className: 'style' });
cssInterop(H5, { className: 'style' });
cssInterop(H6, { className: 'style' });
const MappedHeading = memo(
forwardRef<React.ElementRef<typeof H1>, IHeadingProps>(
(
{
size,
className,
isTruncated,
bold,
underline,
strikeThrough,
sub,
italic,
highlight,
...props
},
ref
) => {
switch (size) {
case '5xl':
case '4xl':
case '3xl':
return (
<H1
className={headingStyle({
size,
isTruncated,
bold,
underline,
strikeThrough,
sub,
italic,
highlight,
class: className,
})}
{...props}
// @ts-expect-error
ref={ref}
/>
);
case '2xl':
return (
<H2
className={headingStyle({
size,
isTruncated,
bold,
underline,
strikeThrough,
sub,
italic,
highlight,
class: className,
})}
{...props}
// @ts-expect-error
ref={ref}
/>
);
case 'xl':
return (
<H3
className={headingStyle({
size,
isTruncated,
bold,
underline,
strikeThrough,
sub,
italic,
highlight,
class: className,
})}
{...props}
// @ts-expect-error
ref={ref}
/>
);
case 'lg':
return (
<H4
className={headingStyle({
size,
isTruncated,
bold,
underline,
strikeThrough,
sub,
italic,
highlight,
class: className,
})}
{...props}
// @ts-expect-error
ref={ref}
/>
);
case 'md':
return (
<H5
className={headingStyle({
size,
isTruncated,
bold,
underline,
strikeThrough,
sub,
italic,
highlight,
class: className,
})}
{...props}
// @ts-expect-error
ref={ref}
/>
);
case 'sm':
case 'xs':
return (
<H6
className={headingStyle({
size,
isTruncated,
bold,
underline,
strikeThrough,
sub,
italic,
highlight,
class: className,
})}
{...props}
// @ts-expect-error
ref={ref}
/>
);
default:
return (
<H4
className={headingStyle({
size,
isTruncated,
bold,
underline,
strikeThrough,
sub,
italic,
highlight,
class: className,
})}
{...props}
// @ts-expect-error
ref={ref}
/>
);
}
}
)
);
const Heading = memo(
forwardRef<React.ElementRef<typeof H1>, IHeadingProps>(
({ className, size = 'lg', as: AsComp, ...props }, ref) => {
const {
isTruncated,
bold,
underline,
strikeThrough,
sub,
italic,
highlight,
} = props;
if (AsComp) {
return (
<AsComp
className={headingStyle({
size,
isTruncated,
bold,
underline,
strikeThrough,
sub,
italic,
highlight,
class: className,
})}
{...props}
/>
);
}
return (
<MappedHeading className={className} size={size} ref={ref} {...props} />
);
}
)
);
Heading.displayName = 'Heading';
export { Heading };

@ -0,0 +1,203 @@
import React, { forwardRef, memo } from 'react';
import { headingStyle } from './styles';
import type { VariantProps } from '@gluestack-ui/nativewind-utils';
type IHeadingProps = VariantProps<typeof headingStyle> &
React.ComponentPropsWithoutRef<'h1'> & {
as?: React.ElementType;
};
const MappedHeading = memo(
forwardRef<HTMLHeadingElement, IHeadingProps>(
(
{
size,
className,
isTruncated,
bold,
underline,
strikeThrough,
sub,
italic,
highlight,
...props
},
ref
) => {
switch (size) {
case '5xl':
case '4xl':
case '3xl':
return (
<h1
className={headingStyle({
size,
isTruncated,
bold,
underline,
strikeThrough,
sub,
italic,
highlight,
class: className,
})}
{...props}
ref={ref}
/>
);
case '2xl':
return (
<h2
className={headingStyle({
size,
isTruncated,
bold,
underline,
strikeThrough,
sub,
italic,
highlight,
class: className,
})}
{...props}
ref={ref}
/>
);
case 'xl':
return (
<h3
className={headingStyle({
size,
isTruncated,
bold,
underline,
strikeThrough,
sub,
italic,
highlight,
class: className,
})}
{...props}
ref={ref}
/>
);
case 'lg':
return (
<h4
className={headingStyle({
size,
isTruncated,
bold,
underline,
strikeThrough,
sub,
italic,
highlight,
class: className,
})}
{...props}
ref={ref}
/>
);
case 'md':
return (
<h5
className={headingStyle({
size,
isTruncated,
bold,
underline,
strikeThrough,
sub,
italic,
highlight,
class: className,
})}
{...props}
ref={ref}
/>
);
case 'sm':
case 'xs':
return (
<h6
className={headingStyle({
size,
isTruncated,
bold,
underline,
strikeThrough,
sub,
italic,
highlight,
class: className,
})}
{...props}
ref={ref}
/>
);
default:
return (
<h4
className={headingStyle({
size,
isTruncated,
bold,
underline,
strikeThrough,
sub,
italic,
highlight,
class: className,
})}
{...props}
ref={ref}
/>
);
}
}
)
);
const Heading = memo(
forwardRef<HTMLHeadingElement, IHeadingProps>(
({ className, size = 'lg', as: AsComp, ...props }, ref) => {
const {
isTruncated,
bold,
underline,
strikeThrough,
sub,
italic,
highlight,
} = props;
if (AsComp) {
return (
<AsComp
className={headingStyle({
size,
isTruncated,
bold,
underline,
strikeThrough,
sub,
italic,
highlight,
class: className,
})}
{...props}
ref={ref}
/>
);
}
return (
<MappedHeading className={className} size={size} ref={ref} {...props} />
);
}
)
);
Heading.displayName = 'Heading';
export { Heading };

@ -0,0 +1,43 @@
import { tva } from "@gluestack-ui/nativewind-utils/tva";
import { isWeb } from "@gluestack-ui/nativewind-utils/IsWeb";
const baseStyle = isWeb
? "font-sans tracking-sm bg-transparent border-0 box-border display-inline list-none margin-0 padding-0 position-relative text-start no-underline whitespace-pre-wrap word-wrap-break-word"
: "";
export const headingStyle = tva({
base: `text-typography-900 font-bold font-heading tracking-sm my-0 ${baseStyle}`,
variants: {
isTruncated: {
true: "truncate",
},
bold: {
true: "font-bold",
},
underline: {
true: "underline",
},
strikeThrough: {
true: "line-through",
},
sub: {
true: "text-xs",
},
italic: {
true: "italic",
},
highlight: {
true: "bg-yellow-500",
},
size: {
"5xl": "text-6xl",
"4xl": "text-5xl",
"3xl": "text-4xl",
"2xl": "text-3xl",
xl: "text-2xl",
lg: "text-xl",
md: "text-lg",
sm: "text-base",
xs: "text-sm",
},
},
});

@ -0,0 +1,214 @@
"use client";
import React from "react";
import { createInput } from "@gluestack-ui/input";
import { View, Pressable, TextInput } from "react-native";
import { tva } from "@gluestack-ui/nativewind-utils/tva";
import {
withStyleContext,
useStyleContext,
} from "@gluestack-ui/nativewind-utils/withStyleContext";
import { cssInterop } from "nativewind";
import type { VariantProps } from "@gluestack-ui/nativewind-utils";
import { PrimitiveIcon, UIIcon } from "@gluestack-ui/icon";
const SCOPE = "INPUT";
const UIInput = createInput({
Root: withStyleContext(View, SCOPE),
Icon: UIIcon,
Slot: Pressable,
Input: TextInput,
});
cssInterop(PrimitiveIcon, {
className: {
target: "style",
nativeStyleToProp: {
height: true,
width: true,
fill: true,
color: "classNameColor",
stroke: true,
},
},
});
const inputStyle = tva({
base: "border-background-300 flex-row overflow-hidden content-center data-[hover=true]:border-outline-400 data-[focus=true]:border-orange-400 data-[focus=true]:hover:border-primary-700 data-[disabled=true]:opacity-40 data-[disabled=true]:hover:border-background-300 items-center",
variants: {
size: {
xl: "px-4 h-16",
lg: "px-4 h-11",
md: "px-4 h-10",
sm: "px-4 h-9",
},
variant: {
underlined:
"rounded-none border-b data-[invalid=true]:border-b-2 data-[invalid=true]:border-error-700 data-[invalid=true]:hover:border-error-700 data-[invalid=true]:data-[focus=true]:border-error-700 data-[invalid=true]:data-[focus=true]:hover:border-error-700 data-[invalid=true]:data-[disabled=true]:hover:border-error-700",
outline:
"rounded-3xl border data-[invalid=true]:border-error-700 data-[invalid=true]:hover:border-error-700 data-[invalid=true]:data-[focus=true]:border-error-700 data-[invalid=true]:data-[focus=true]:hover:border-error-700 data-[invalid=true]:data-[disabled=true]:hover:border-error-700 data-[focus=true]:web:ring-1 data-[focus=true]:web:ring-inset data-[focus=true]:web:ring-indicator-primary data-[invalid=true]:web:ring-1 data-[invalid=true]:web:ring-inset data-[invalid=true]:web:ring-indicator-error data-[invalid=true]:data-[focus=true]:hover:web:ring-1 data-[invalid=true]:data-[focus=true]:hover:web:ring-inset data-[invalid=true]:data-[focus=true]:hover:web:ring-indicator-error data-[invalid=true]:data-[disabled=true]:hover:web:ring-1 data-[invalid=true]:data-[disabled=true]:hover:web:ring-inset data-[invalid=true]:data-[disabled=true]:hover:web:ring-indicator-error",
rounded:
"rounded-full border data-[invalid=true]:border-error-700 data-[invalid=true]:hover:border-error-700 data-[invalid=true]:data-[focus=true]:border-error-700 data-[invalid=true]:data-[focus=true]:hover:border-error-700 data-[invalid=true]:data-[disabled=true]:hover:border-error-700 data-[focus=true]:web:ring-1 data-[focus=true]:web:ring-inset data-[focus=true]:web:ring-indicator-primary data-[invalid=true]:web:ring-1 data-[invalid=true]:web:ring-inset data-[invalid=true]:web:ring-indicator-error data-[invalid=true]:data-[focus=true]:hover:web:ring-1 data-[invalid=true]:data-[focus=true]:hover:web:ring-inset data-[invalid=true]:data-[focus=true]:hover:web:ring-indicator-error data-[invalid=true]:data-[disabled=true]:hover:web:ring-1 data-[invalid=true]:data-[disabled=true]:hover:web:ring-inset data-[invalid=true]:data-[disabled=true]:hover:web:ring-indicator-error",
},
},
});
const inputIconStyle = tva({
base: "justify-center items-center text-typography-400 fill-none",
parentVariants: {
size: {
"2xs": "h-3 w-3",
xs: "h-3.5 w-3.5",
sm: "h-4 w-4",
md: "h-[18px] w-[18px]",
lg: "h-5 w-5",
xl: "h-6 w-6",
},
},
});
const inputSlotStyle = tva({
base: "justify-center items-center web:disabled:cursor-not-allowed",
});
const inputFieldStyle = tva({
base: "flex-1 text-typography-900 py-0 px-3 placeholder:text-typography-500 h-full ios:leading-[0px] web:cursor-text web:data-[disabled=true]:cursor-not-allowed",
parentVariants: {
variant: {
underlined: "web:outline-0 web:outline-none px-0",
outline: "web:outline-0 web:outline-none",
rounded: "web:outline-0 web:outline-none px-4",
},
size: {
"2xs": "text-2xs",
xs: "text-xs",
sm: "text-sm",
md: "text-base",
lg: "text-lg",
xl: "text-xl",
"2xl": "text-2xl",
"3xl": "text-3xl",
"4xl": "text-4xl",
"5xl": "text-5xl",
"6xl": "text-6xl",
},
},
});
type IInputProps = React.ComponentProps<typeof UIInput> &
VariantProps<typeof inputStyle> & { className?: string };
const Input = React.forwardRef<React.ElementRef<typeof UIInput>, IInputProps>(
({ className, variant = "outline", size = "md", ...props }, ref) => {
return (
<UIInput
ref={ref}
{...props}
className={inputStyle({ variant, size, class: className })}
context={{ variant, size }}
/>
);
}
);
type IInputIconProps = React.ComponentProps<typeof UIInput.Icon> &
VariantProps<typeof inputIconStyle> & {
className?: string;
height?: number;
width?: number;
};
const InputIcon = React.forwardRef<
React.ElementRef<typeof UIInput.Icon>,
IInputIconProps
>(({ className, size, ...props }, ref) => {
const { size: parentSize } = useStyleContext(SCOPE);
if (typeof size === "number") {
return (
<UIInput.Icon
ref={ref}
{...props}
className={inputIconStyle({ class: className })}
size={size}
/>
);
} else if (
(props.height !== undefined || props.width !== undefined) &&
size === undefined
) {
return (
<UIInput.Icon
ref={ref}
{...props}
className={inputIconStyle({ class: className })}
/>
);
}
return (
<UIInput.Icon
ref={ref}
{...props}
className={inputIconStyle({
parentVariants: {
size: parentSize,
},
class: className,
})}
/>
);
});
type IInputSlotProps = React.ComponentProps<typeof UIInput.Slot> &
VariantProps<typeof inputSlotStyle> & { className?: string };
const InputSlot = React.forwardRef<
React.ElementRef<typeof UIInput.Slot>,
IInputSlotProps
>(({ className, ...props }, ref) => {
return (
<UIInput.Slot
ref={ref}
{...props}
className={inputSlotStyle({
class: className,
})}
/>
);
});
type IInputFieldProps = React.ComponentProps<typeof UIInput.Input> &
VariantProps<typeof inputFieldStyle> & { className?: string };
const InputField = React.forwardRef<
React.ElementRef<typeof UIInput.Input>,
IInputFieldProps
>(({ className, ...props }, ref) => {
const { variant: parentVariant, size: parentSize } = useStyleContext(SCOPE);
return (
<UIInput.Input
ref={ref}
{...props}
className={inputFieldStyle({
parentVariants: {
variant: parentVariant,
size: parentSize,
},
class: className,
})}
/>
);
});
Input.displayName = "Input";
InputIcon.displayName = "InputIcon";
InputSlot.displayName = "InputSlot";
InputField.displayName = "InputField";
export { Input, InputField, InputIcon, InputSlot };

@ -0,0 +1,102 @@
"use client";
import { createLink } from "@gluestack-ui/link";
import { Pressable } from "react-native";
import { Text } from "react-native";
import { tva } from "@gluestack-ui/nativewind-utils/tva";
import { withStyleContext } from "@gluestack-ui/nativewind-utils/withStyleContext";
import { cssInterop } from "nativewind";
import type { VariantProps } from "@gluestack-ui/nativewind-utils";
import React from "react";
export const UILink = createLink({
Root: withStyleContext(Pressable),
Text: Text,
});
cssInterop(UILink, { className: "style" });
cssInterop(UILink.Text, { className: "style" });
const linkStyle = tva({
base: "group/link web:outline-0 data-[disabled=true]:web:cursor-not-allowed data-[focus-visible=true]:web:ring-2 data-[focus-visible=true]:web:ring-indicator-primary data-[focus-visible=true]:web:outline-0 data-[disabled=true]:opacity-4 ",
});
const linkTextStyle = tva({
base: "underline text-orange-400 data-[hover=true]:text-orange-400 data-[hover=true]:no-underline data-[active=true]:text-orange-400 font-normal font-body web:font-sans web:tracking-sm web:my-0 web:bg-transparent web:border-0 web:box-border web:display-inline web:list-none web:margin-0 web:padding-0 web:position-relative web:text-start web:whitespace-pre-wrap web:word-wrap-break-word",
variants: {
isTruncated: {
true: "web:truncate",
},
bold: {
true: "font-bold",
},
underline: {
true: "underline",
},
strikeThrough: {
true: "line-through",
},
size: {
"2xs": "text-2xs",
xs: "text-xs",
sm: "text-sm",
md: "text-base",
lg: "text-lg",
xl: "text-xl",
"2xl": "text-2xl",
"3xl": "text-3xl",
"4xl": "text-4xl",
"5xl": "text-5xl",
"6xl": "text-6xl",
},
sub: {
true: "text-xs",
},
italic: {
true: "italic",
},
highlight: {
true: "bg-yellow-500",
},
},
});
type ILinkProps = React.ComponentProps<typeof UILink> &
VariantProps<typeof linkStyle> & { className?: string };
const Link = React.forwardRef<React.ElementRef<typeof UILink>, ILinkProps>(
({ className, ...props }, ref) => {
return (
<UILink
ref={ref}
{...props}
className={linkStyle({ class: className })}
/>
);
}
);
type ILinkTextProps = React.ComponentProps<typeof UILink.Text> &
VariantProps<typeof linkTextStyle> & { className?: string };
const LinkText = React.forwardRef<
React.ElementRef<typeof UILink.Text>,
ILinkTextProps
>(({ className, size = "md", ...props }, ref) => {
return (
<UILink.Text
ref={ref}
{...props}
className={linkTextStyle({
class: className,
size,
})}
/>
);
});
Link.displayName = "Link";
LinkText.displayName = "LinkText";
export { Link, LinkText };

@ -1,21 +1,12 @@
import {SafeAreaView, Text, View} from "react-native";
import Navigation from "@/src/navigation/navigation";
import HomeScreen from "@/app/HomeScreen";
import { SafeAreaView, View } from "react-native";
import LoginPage from "./LoginPage";
export default function Index() {
return (
<View>
<App/>
<SafeAreaView>
<LoginPage />
</SafeAreaView>
</View>
);
}
function App() {
return (
<View>
<SafeAreaView>
</SafeAreaView>
</View>
);
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 277 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

@ -0,0 +1,8 @@
import * as React from "react";
import Svg, { Path, SvgProps } from "react-native-svg";
const Muscle = (props: SvgProps) => (
<Svg width="800px" height="800px" viewBox="0 0 24 24" {...props}>
<Path d="M9.14,16.77S8,13.17,10.09,11A14.12,14.12,0,0,1,13,9.13a4.78,4.78,0,1,1,5.61,4.7c-1.83,2.77-5.83,7.71-11.33,7.71C4.36,21.54,1.5,13,1.5,9.13V4.48A2.26,2.26,0,0,1,3.64,2.23c1.73-.09,4,0,4.54,1.17C9,5.11,7.23,8.18,5.32,8.18c0,1.5,1.83,4.76,3.49,6.56" />
</Svg>
);
export default Muscle;

1348
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -15,11 +15,15 @@
"preset": "jest-expo"
},
"dependencies": {
"@expo/html-elements": "^0.4.2",
"@expo/vector-icons": "^14.0.2",
"@gluestack-ui/avatar": "^0.1.18",
"@gluestack-ui/button": "^1.0.8",
"@gluestack-ui/checkbox": "^0.1.33",
"@gluestack-ui/form-control": "^0.1.19",
"@gluestack-ui/icon": "^0.1.25",
"@gluestack-ui/input": "^0.1.32",
"@gluestack-ui/link": "^0.1.23",
"@gluestack-ui/nativewind-utils": "^1.0.26",
"@gluestack-ui/overlay": "^0.1.16",
"@gluestack-ui/toast": "^1.0.8",
@ -27,40 +31,43 @@
"@react-navigation/native": "^7.0.14",
"@react-navigation/native-stack": "^7.2.0",
"@react-navigation/stack": "^7.1.1",
"expo": "~52.0.24",
"expo-blur": "~14.0.1",
"expo-constants": "~17.0.3",
"expo-font": "~13.0.2",
"expo-haptics": "~14.0.0",
"expo-linking": "~7.0.3",
"expo": "^52.0.24",
"expo-asset": "^11.0.2",
"expo-blur": "^14.0.1",
"expo-constants": "^17.0.3",
"expo-font": "^13.0.2",
"expo-haptics": "^14.0.0",
"expo-linking": "^7.0.3",
"expo-router": "~4.0.16",
"expo-splash-screen": "~0.29.19",
"expo-status-bar": "~2.0.0",
"expo-symbols": "~0.2.0",
"expo-system-ui": "~4.0.6",
"expo-web-browser": "~14.0.1",
"expo-splash-screen": "^0.29.19",
"expo-status-bar": "^2.0.0",
"expo-symbols": "^0.2.0",
"expo-system-ui": "^4.0.6",
"expo-web-browser": "^14.0.1",
"nativewind": "^4.1.23",
"react": "18.3.1",
"react-dom": "18.3.1",
"react-native": "0.76.5",
"react-native-gesture-handler": "~2.20.2",
"react-native-reanimated": "~3.16.1",
"react-native-safe-area-context": "^4.14.1",
"react-native-screens": "~4.4.0",
"react-native-svg": "^15.11.0",
"react-native": "0.76.6",
"react-native-gesture-handler": "^2.20.2",
"react-native-reanimated": "^3.16.1",
"react-native-safe-area-context": "^4.12.0",
"react-native-screens": "^4.4.0",
"react-native-svg": "^15.8.0",
"react-native-vector-icons": "^10.2.0",
"react-native-web": "~0.19.13",
"react-native-web": "^0.19.13",
"react-native-webview": "13.12.5",
"tailwindcss": "^3.4.17"
},
"devDependencies": {
"@babel/core": "^7.25.2",
"@types/jest": "^29.5.12",
"@types/react": "~18.3.12",
"@types/react-test-renderer": "^18.3.0",
"@types/react": "^18.3.12",
"@types/react-test-renderer": "^19.0.0",
"@types/validator": "^13.12.2",
"jest": "^29.2.1",
"jest-expo": "~52.0.2",
"react-test-renderer": "18.3.1",
"jest-expo": "^52.0.2",
"react-native-svg-transformer": "^1.5.0",
"react-test-renderer": "19.0.0",
"typescript": "^5.3.3"
},
"private": true

@ -11,6 +11,7 @@
"**/*.tsx",
".expo/types/**/*.ts",
"expo-env.d.ts",
"nativewind-env.d.ts"
"nativewind-env.d.ts",
"app/index.tsx"
]
}

Loading…
Cancel
Save