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>
120 lines
3.0 KiB
TypeScript
120 lines
3.0 KiB
TypeScript
import { useState, type ReactNode } from "react";
|
|
import { Ionicons } from "@expo/vector-icons";
|
|
import {
|
|
Pressable,
|
|
TextInput,
|
|
View,
|
|
type TextInputProps,
|
|
} from "react-native";
|
|
import { Text } from "@/components/ui/Text";
|
|
import { colors, fonts, radii } from "@/lib/theme";
|
|
|
|
type Props = TextInputProps & {
|
|
onClear?: () => void;
|
|
/** Renders as a button instead of an input (home screen → search screen). */
|
|
readOnlyPress?: () => void;
|
|
trailing?: ReactNode;
|
|
};
|
|
|
|
export function SearchField({
|
|
value,
|
|
onClear,
|
|
readOnlyPress,
|
|
trailing,
|
|
placeholder = "Search medicine, brand or symptom",
|
|
...rest
|
|
}: Props) {
|
|
const [focused, setFocused] = useState(false);
|
|
|
|
const body = (
|
|
<View
|
|
style={{
|
|
flexDirection: "row",
|
|
alignItems: "center",
|
|
gap: 10,
|
|
backgroundColor: colors.surface,
|
|
borderRadius: radii.pill,
|
|
borderWidth: 1.5,
|
|
borderColor: focused ? colors.brand : colors.line,
|
|
paddingLeft: 16,
|
|
paddingRight: trailing ? 6 : 16,
|
|
height: 52,
|
|
}}
|
|
>
|
|
<Ionicons name="search" size={18} color={focused ? colors.brandText : colors.subtle} />
|
|
{readOnlyPress ? (
|
|
<Text variant="body" color={colors.subtle} style={{ flex: 1 }} numberOfLines={1}>
|
|
{value || placeholder}
|
|
</Text>
|
|
) : (
|
|
<TextInput
|
|
value={value}
|
|
placeholder={placeholder}
|
|
placeholderTextColor={colors.subtle}
|
|
onFocus={() => setFocused(true)}
|
|
onBlur={() => setFocused(false)}
|
|
returnKeyType="search"
|
|
style={{
|
|
flex: 1,
|
|
fontFamily: fonts.regular,
|
|
fontSize: 14,
|
|
color: colors.ink,
|
|
paddingVertical: 0,
|
|
}}
|
|
{...rest}
|
|
/>
|
|
)}
|
|
{value && onClear && !readOnlyPress ? (
|
|
<Pressable onPress={onClear} hitSlop={8}>
|
|
<Ionicons name="close-circle" size={18} color={colors.subtle} />
|
|
</Pressable>
|
|
) : null}
|
|
{trailing}
|
|
</View>
|
|
);
|
|
|
|
if (readOnlyPress) {
|
|
return (
|
|
<Pressable onPress={readOnlyPress} style={({ pressed }) => ({ opacity: pressed ? 0.9 : 1 })}>
|
|
{body}
|
|
</Pressable>
|
|
);
|
|
}
|
|
|
|
return body;
|
|
}
|
|
|
|
/** Lime pill action that sits inside the search field (scan entry point). */
|
|
export function SearchAction({
|
|
icon,
|
|
label,
|
|
onPress,
|
|
}: {
|
|
icon: keyof typeof Ionicons.glyphMap;
|
|
label?: string;
|
|
onPress?: () => void;
|
|
}) {
|
|
return (
|
|
<Pressable
|
|
onPress={onPress}
|
|
style={({ pressed }) => ({
|
|
flexDirection: "row",
|
|
alignItems: "center",
|
|
gap: 6,
|
|
backgroundColor: colors.brand,
|
|
borderRadius: radii.pill,
|
|
paddingHorizontal: label ? 14 : 10,
|
|
height: 40,
|
|
opacity: pressed ? 0.85 : 1,
|
|
})}
|
|
>
|
|
<Ionicons name={icon} size={16} color={colors.ink} />
|
|
{label ? (
|
|
<Text variant="label" color={colors.ink}>
|
|
{label}
|
|
</Text>
|
|
) : null}
|
|
</Pressable>
|
|
);
|
|
}
|