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 7310a349c5 Add Gishen mobile app with complete demo shopping and care flows.
Ship Expo Router screens, seeded stores, and polished UX so auth, shop, checkout, prescriptions, loyalty, family, and account processes work end-to-end offline.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-07 01:21:32 +03:00

201 lines
5.4 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);
return (
<Pressable
accessibilityRole="button"
accessibilityLabel={accessibilityLabel}
onPress={onPress}
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} />
</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>;
}