aboutsummaryrefslogtreecommitdiffstats
path: root/apps/mobile/components/ui/Button.tsx
diff options
context:
space:
mode:
authorMohamed Bassem <me@mbassem.com>2025-08-26 15:47:05 +0300
committerGitHub <noreply@github.com>2025-08-26 13:47:05 +0100
commited86f7ef012fb558fe8a8974e1e162ce75cbfd15 (patch)
treea3470b0e1a01aede90b75bc61eeba2545e51fe83 /apps/mobile/components/ui/Button.tsx
parentec56ea33b5e37d02e87e480da305038a5ce7de49 (diff)
downloadkarakeep-ed86f7ef012fb558fe8a8974e1e162ce75cbfd15.tar.zst
feat(mobile): Retheme the mobile app (#1872)
* Add nativewindui * migrate to nativewindui text * Replace buttons with nativewindui buttons * Use nativewindui search input * fix the divider color * More changes * fix manage tag icon * fix styling of bookmark card * fix ios compilation * fix search clear * fix tag pill border color * Store theme setting in app settings * fix setting color appearance * fix coloring of search input * fix following system theme * add a save button to info * fix the grey colors on android * fix icon active tint color * drop the use of TextField
Diffstat (limited to 'apps/mobile/components/ui/Button.tsx')
-rw-r--r--apps/mobile/components/ui/Button.tsx239
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 };