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/Badge.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

139 lines
3.4 KiB
TypeScript

import { Ionicons } from "@expo/vector-icons";
import { View, type StyleProp, type ViewStyle } from "react-native";
import { Text } from "@/components/ui/Text";
import { colors, radii } from "@/lib/theme";
type Tone = "brand" | "danger" | "warn" | "info" | "neutral" | "dark";
const tones: Record<Tone, { bg: string; fg: string }> = {
brand: { bg: colors.brandSoft, fg: colors.brandText },
danger: { bg: colors.dangerSoft, fg: colors.danger },
warn: { bg: colors.warnSoft, fg: colors.warn },
info: { bg: colors.infoSoft, fg: colors.info },
neutral: { bg: colors.surfaceAlt, fg: colors.muted },
dark: { bg: colors.ink, fg: colors.surface },
};
export function Badge({
label,
tone = "brand",
icon,
solid,
style,
}: {
label: string;
tone?: Tone;
icon?: keyof typeof Ionicons.glyphMap;
/** Filled lime treatment used for discount flags on imagery. */
solid?: boolean;
style?: StyleProp<ViewStyle>;
}) {
const skin = tones[tone];
const bg = solid ? colors.brand : skin.bg;
const fg = solid ? colors.ink : skin.fg;
return (
<View
style={[
{
flexDirection: "row",
alignItems: "center",
gap: 4,
alignSelf: "flex-start",
backgroundColor: bg,
paddingHorizontal: 8,
paddingVertical: 4,
borderRadius: radii.xs,
},
style,
]}
>
{icon ? <Ionicons name={icon} size={11} color={fg} /> : null}
<Text variant="micro" color={fg}>
{label}
</Text>
</View>
);
}
export function StockLabel({
inStock,
count,
}: {
inStock: boolean;
count?: number;
}) {
return (
<Text variant="micro" color={inStock ? colors.inStock : colors.outOfStock}>
{inStock ? `In stock${count ? ` (${count})` : ""}` : "Out of stock"}
</Text>
);
}
export function Rating({
value,
count,
showStars = true,
size = 13,
}: {
value: number;
count?: number;
showStars?: boolean;
size?: number;
}) {
return (
<View style={{ flexDirection: "row", alignItems: "center", gap: 4 }}>
{showStars ? (
<View style={{ flexDirection: "row", gap: 1 }}>
{[1, 2, 3, 4, 5].map((i) => (
<Ionicons
key={i}
name={
value >= i ? "star" : value >= i - 0.5 ? "star-half" : "star-outline"
}
size={size}
color={value >= i - 0.5 ? colors.star : colors.disabled}
/>
))}
</View>
) : (
<Ionicons name="star" size={size} color={colors.star} />
)}
<Text variant="micro" color={colors.muted}>
{value.toFixed(1)}
{count !== undefined ? ` (${count})` : ""}
</Text>
</View>
);
}
/** Compact price with optional struck-through original. */
export function Price({
value,
compareAt,
size = "md",
format,
}: {
value: number;
compareAt?: number;
size?: "sm" | "md" | "lg";
format: (n: number) => string;
}) {
return (
<View style={{ flexDirection: "row", alignItems: "baseline", gap: 6 }}>
<Text variant={size === "lg" ? "priceLg" : size === "sm" ? "label" : "price"}>
{format(value)}
</Text>
{compareAt && compareAt > value ? (
<Text
variant="caption"
color={colors.subtle}
style={{ textDecorationLine: "line-through" }}
>
{format(compareAt)}
</Text>
) : null}
</View>
);
}