This repository has been archived on 2026-08-11. You can view files and clone it, but cannot push or open issues or pull requests.
Gishen-Mob/components/ui/Button.tsx
kirukib 5391780b9b Ship Rx Care tab, notifications inbox, and guest pickup codes.
Replace the profile tab with prescription management (inline reminders and family invite), add a shared tab top bar, separate notifications from orders, and support any-branch guest checkout with GSH codes.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-07 15:35:51 +03:00

209 lines
5.6 KiB
TypeScript

import type { ReactNode } from "react";
import { Ionicons } from "@expo/vector-icons";
import {
ActivityIndicator,
Pressable,
View,
type PressableProps,
type StyleProp,
type ViewStyle,
} from "react-native";
import { Text } from "@/components/ui/Text";
import { colors, radii, shadows } from "@/lib/theme";
type Variant = "primary" | "brand" | "dark" | "outline" | "ghost" | "soft" | "danger";
type Size = "sm" | "md" | "lg";
type Props = Omit<PressableProps, "style"> & {
title: string;
variant?: Variant;
size?: Size;
icon?: keyof typeof Ionicons.glyphMap;
iconRight?: keyof typeof Ionicons.glyphMap;
loading?: boolean;
fullWidth?: boolean;
elevated?: boolean;
style?: StyleProp<ViewStyle>;
};
const skins: Record<Variant, { bg: string; fg: string; border: string }> = {
// Lime fill with ink text: on-brand and far more legible than white-on-lime.
primary: { bg: colors.brand, fg: colors.ink, border: "transparent" },
brand: { bg: colors.brand, fg: colors.ink, border: "transparent" },
dark: { bg: colors.ink, fg: colors.surface, border: "transparent" },
outline: { bg: "transparent", fg: colors.ink, border: colors.line },
ghost: { bg: "transparent", fg: colors.ink, border: "transparent" },
soft: { bg: colors.brandSoft, fg: colors.brandText, border: "transparent" },
danger: { bg: colors.dangerSoft, fg: colors.danger, border: "transparent" },
};
const sizing: Record<Size, { py: number; px: number; gap: number; icon: number }> = {
sm: { py: 9, px: 14, gap: 6, icon: 15 },
md: { py: 13, px: 18, gap: 8, icon: 17 },
lg: { py: 16, px: 22, gap: 9, icon: 19 },
};
export function Button({
title,
variant = "primary",
size = "lg",
icon,
iconRight,
loading,
disabled,
fullWidth = true,
elevated,
style,
...rest
}: Props) {
const skin = skins[variant];
const dims = sizing[size];
const inactive = disabled || loading;
return (
<Pressable
disabled={inactive}
style={({ pressed }) => [
{
backgroundColor: inactive ? colors.surfaceAlt : skin.bg,
borderColor: skin.border,
borderWidth: variant === "outline" ? 1.5 : 0,
borderRadius: radii.pill,
paddingVertical: dims.py,
paddingHorizontal: dims.px,
flexDirection: "row",
alignItems: "center",
justifyContent: "center",
gap: dims.gap,
alignSelf: fullWidth ? "stretch" : "flex-start",
opacity: pressed ? 0.88 : 1,
transform: [{ scale: pressed ? 0.99 : 1 }],
},
elevated && !inactive && shadows.sm,
style,
]}
{...rest}
>
{loading ? (
<ActivityIndicator size="small" color={inactive ? colors.subtle : skin.fg} />
) : (
<>
{icon ? (
<Ionicons
name={icon}
size={dims.icon}
color={inactive ? colors.subtle : skin.fg}
/>
) : null}
<Text
variant={size === "sm" ? "label" : "h3"}
color={inactive ? colors.subtle : skin.fg}
>
{title}
</Text>
{iconRight ? (
<Ionicons
name={iconRight}
size={dims.icon}
color={inactive ? colors.subtle : skin.fg}
/>
) : null}
</>
)}
</Pressable>
);
}
/** Circular icon button used in headers and cards. */
export function IconButton({
icon,
onPress,
size = 40,
tone = "surface",
color,
accessibilityLabel,
}: {
icon: keyof typeof Ionicons.glyphMap;
onPress?: () => void;
size?: number;
tone?: "surface" | "brand" | "dark" | "plain";
color?: string;
accessibilityLabel?: string;
}) {
const bg =
tone === "brand"
? colors.brand
: tone === "dark"
? colors.ink
: tone === "plain"
? "transparent"
: colors.surface;
const fg = color ?? (tone === "dark" ? colors.surface : colors.ink);
// Ionicons arrows/chevrons sit optically left; nudge for visual centering.
const isBackIcon = icon === "arrow-back" || icon === "chevron-back";
return (
<Pressable
accessibilityRole="button"
accessibilityLabel={accessibilityLabel}
onPress={onPress}
hitSlop={6}
style={({ pressed }) => [
{
width: size,
height: size,
borderRadius: size / 2,
backgroundColor: bg,
alignItems: "center",
justifyContent: "center",
opacity: pressed ? 0.7 : 1,
},
tone === "surface" && { borderWidth: 1, borderColor: colors.line },
]}
>
<Ionicons
name={icon}
size={size * 0.46}
color={fg}
style={isBackIcon ? { marginLeft: -1 } : undefined}
/>
</Pressable>
);
}
/** Small round "+" affordance on product cards. */
export function AddButton({
onPress,
disabled,
size = 34,
}: {
onPress?: () => void;
disabled?: boolean;
size?: number;
}) {
return (
<Pressable
accessibilityRole="button"
accessibilityLabel="Add to cart"
disabled={disabled}
onPress={onPress}
hitSlop={8}
style={({ pressed }) => ({
width: size,
height: size,
borderRadius: size / 2,
backgroundColor: disabled ? colors.surfaceAlt : colors.brand,
alignItems: "center",
justifyContent: "center",
opacity: pressed ? 0.8 : 1,
})}
>
<Ionicons name="add" size={size * 0.55} color={disabled ? colors.subtle : colors.ink} />
</Pressable>
);
}
export function ButtonRow({ children }: { children: ReactNode }) {
return <View style={{ flexDirection: "row", gap: 10 }}>{children}</View>;
}