diff options
Diffstat (limited to 'apps/mobile/components/ui/Button.tsx')
| -rw-r--r-- | apps/mobile/components/ui/Button.tsx | 239 |
1 files changed, 179 insertions, 60 deletions
diff --git a/apps/mobile/components/ui/Button.tsx b/apps/mobile/components/ui/Button.tsx index 0f3b4ab3..312c3129 100644 --- a/apps/mobile/components/ui/Button.tsx +++ b/apps/mobile/components/ui/Button.tsx @@ -1,81 +1,200 @@ import type { VariantProps } from "class-variance-authority"; -import { Text, TouchableOpacity } from "react-native"; +import * as React from "react"; +import { + Platform, + Pressable, + PressableProps, + View, + ViewStyle, +} from "react-native"; +import { TextClassContext } from "@/components/ui/Text"; +import { useColorScheme } from "@/lib/useColorScheme"; import { cn } from "@/lib/utils"; +import { COLORS } from "@/theme/colors"; +import * as Slot from "@rn-primitives/slot"; import { cva } from "class-variance-authority"; -const buttonVariants = cva( - "flex flex-row items-center justify-center rounded-md", - { - variants: { - variant: { - default: "bg-primary", - secondary: "bg-secondary", - destructive: "bg-destructive", - ghost: "bg-slate-700", - link: "text-primary underline-offset-4", - }, - size: { - default: "h-10 px-4", - sm: "h-8 px-2", - lg: "h-12 px-8", - }, +const buttonVariants = cva("flex-row items-center justify-center gap-2", { + variants: { + variant: { + primary: "ios:active:opacity-80 bg-primary", + secondary: + "ios:border-primary ios:active:bg-primary/5 border border-foreground/40", + tonal: + "ios:bg-primary/10 dark:ios:bg-primary/10 ios:active:bg-primary/15 bg-primary/15 dark:bg-primary/30", + plain: "ios:active:opacity-70", + destructive: + "ios:bg-destructive border border-destructive/5 bg-destructive/80", }, - defaultVariants: { - variant: "default", - size: "default", + size: { + none: "", + sm: "rounded-full px-2.5 py-1", + md: "ios:rounded-lg ios:py-1.5 ios:px-3.5 rounded-full px-5 py-2", + lg: "ios:py-2 gap-2 rounded-xl px-5 py-2.5", + icon: "ios:rounded-lg h-10 w-10 rounded-full", }, }, -); + defaultVariants: { + variant: "primary", + size: "md", + }, +}); + +const androidRootVariants = cva("overflow-hidden", { + variants: { + size: { + none: "", + icon: "rounded-full", + sm: "rounded-full", + md: "rounded-full", + lg: "rounded-xl", + }, + }, + defaultVariants: { + size: "md", + }, +}); -const buttonTextVariants = cva("text-center font-medium", { +const buttonTextVariants = cva("font-medium", { variants: { variant: { - default: "text-primary-foreground", - secondary: "text-secondary-foreground", - destructive: "text-destructive-foreground", - ghost: "text-primary-foreground", - link: "text-primary-foreground underline", + primary: "text-white", + secondary: "ios:text-primary text-foreground", + tonal: "ios:text-primary text-foreground", + plain: "text-foreground", + destructive: "text-white", }, size: { - default: "text-base", - sm: "text-sm", - lg: "text-xl", + none: "", + icon: "", + sm: "text-[15px] leading-5", + md: "text-[17px] leading-7", + lg: "text-[17px] leading-7", }, }, defaultVariants: { - variant: "default", - size: "default", + variant: "primary", + size: "md", }, }); -interface ButtonProps - extends React.ComponentPropsWithoutRef<typeof TouchableOpacity>, - VariantProps<typeof buttonVariants> { - label: string; - labelClasses?: string; +function convertToRGBA(rgb: string, opacity: number): string { + const rgbValues = rgb.match(/\d+/g); + if (!rgbValues || rgbValues.length !== 3) { + throw new Error("Invalid RGB color format"); + } + const red = parseInt(rgbValues[0], 10); + const green = parseInt(rgbValues[1], 10); + const blue = parseInt(rgbValues[2], 10); + if (opacity < 0 || opacity > 1) { + throw new Error("Opacity must be a number between 0 and 1"); + } + return `rgba(${red},${green},${blue},${opacity})`; } -function Button({ - label, - labelClasses, - className, - variant, - size, - ...props -}: ButtonProps) { - return ( - <TouchableOpacity - className={cn(buttonVariants({ variant, size, className }))} - {...props} - > - <Text - className={cn( - buttonTextVariants({ variant, size, className: labelClasses }), - )} - > - {label} - </Text> - </TouchableOpacity> - ); + +const ANDROID_RIPPLE = { + dark: { + primary: { + color: convertToRGBA(COLORS.dark.grey3, 0.4), + borderless: false, + }, + secondary: { + color: convertToRGBA(COLORS.dark.grey5, 0.8), + borderless: false, + }, + plain: { color: convertToRGBA(COLORS.dark.grey5, 0.8), borderless: false }, + tonal: { color: convertToRGBA(COLORS.dark.grey5, 0.8), borderless: false }, + destructive: { + color: convertToRGBA(COLORS.dark.destructive, 0.8), + borderless: false, + }, + }, + light: { + primary: { + color: convertToRGBA(COLORS.light.grey4, 0.4), + borderless: false, + }, + secondary: { + color: convertToRGBA(COLORS.light.grey5, 0.4), + borderless: false, + }, + plain: { color: convertToRGBA(COLORS.light.grey5, 0.4), borderless: false }, + tonal: { color: convertToRGBA(COLORS.light.grey6, 0.4), borderless: false }, + destructive: { + color: convertToRGBA(COLORS.light.destructive, 0.4), + borderless: false, + }, + }, +}; + +// Add as class when possible: https://github.com/marklawlor/nativewind/issues/522 +const BORDER_CURVE: ViewStyle = { + borderCurve: "continuous", +}; + +type ButtonVariantProps = Omit< + VariantProps<typeof buttonVariants>, + "variant" +> & { + variant?: Exclude<VariantProps<typeof buttonVariants>["variant"], null>; +}; + +interface AndroidOnlyButtonProps { + /** + * ANDROID ONLY: The class name of root responsible for hidding the ripple overflow. + */ + androidRootClassName?: string; } -export { Button, buttonVariants, buttonTextVariants }; +type ButtonProps = PressableProps & ButtonVariantProps & AndroidOnlyButtonProps; + +const Root = Platform.OS === "android" ? View : Slot.Pressable; + +const Button = React.forwardRef< + React.ElementRef<typeof Pressable>, + ButtonProps +>( + ( + { + className, + variant = "primary", + size, + style = BORDER_CURVE, + androidRootClassName, + ...props + }, + ref, + ) => { + const { colorScheme } = useColorScheme(); + + return ( + <TextClassContext.Provider value={buttonTextVariants({ variant, size })}> + <Root + className={Platform.select({ + ios: androidRootClassName, + default: androidRootVariants({ + size, + className: androidRootClassName, + }), + })} + > + <Pressable + className={cn( + props.disabled && "opacity-50", + buttonVariants({ variant, size, className }), + )} + ref={ref} + style={style} + android_ripple={ANDROID_RIPPLE[colorScheme][variant]} + {...props} + /> + </Root> + </TextClassContext.Provider> + ); + }, +); + +Button.displayName = "Button"; + +export { Button, buttonTextVariants, buttonVariants }; +export type { ButtonProps }; |
