Delivery home — address header with cart pill, search, cuisine chips, featured restaurant rail, and popular dishes with add buttons
import * as React from "react";
import { useState } from "react";
import { ScrollView, View } from "react-native";
import { SafeAreaView, useSafeAreaInsets } from "react-native-safe-area-context";
import { Badge, Card, Glass, Icon, Image, PromoBanner, SearchInput, Text } from "@lumioui/ui";
import { cn, useHaptic } from "@lumioui/core";
import { Mount, Press, SectionHead, World } from "../shared";
export interface FoodRestaurant {
id: string;
name: string;
cuisine: string;
/** Category key used by the filter chips. */
category: string;
rating: number;
eta: string;
/** "Free" renders as a success badge; anything else shows as a price. */
deliveryFee: string;
imageUrl?: string;
badge?: string;
}
export interface FoodDish {
id: string;
name: string;
restaurant: string;
price: number;
category: string;
imageUrl?: string;
}
export interface FoodHomeScreenProps {
/** Short delivery address shown in the header. */
address?: string;
categories?: string[];
restaurants?: FoodRestaurant[];
dishes?: FoodDish[];
/** Item count on the cart affordance. Hidden when 0. */
cartCount?: number;
currency?: string;
onSearch?: (query: string) => void;
onCategoryPress?: (category: string) => void;
onRestaurantPress?: (restaurant: FoodRestaurant) => void;
onDishPress?: (dish: FoodDish) => void;
onAddDish?: (dish: FoodDish) => void;
onCartPress?: () => void;
onAddressPress?: () => void;
/** Promo banner CTA — "Order now". */
onPromoPress?: () => void;
}
/** Pinned Unsplash food photography — each id visually verified to show
* the named dish, the same contract catalogImg keeps for apparel. */
const FOOD_PHOTOS = {
burger: "photo-1568901346375-23c9450c58cd",
pizza: "photo-1513104890138-7c749659a591",
sushi: "photo-1579871494447-9811cf80d66c",
tacos: "photo-1551504734-5ee1c4a1479b",
ramen: "photo-1569718212165-3a8278d5f624",
salad: "photo-1512621776951-a57141f2eefd",
bowl: "photo-1546069901-ba9599a7e63c",
coffee: "photo-1509042239860-f550ce710b93",
dessert: "photo-1551024506-0bccd828d307",
} as const;
const img = (p: keyof typeof FOOD_PHOTOS, w = 640, h = 420) =>
`https://images.unsplash.com/${FOOD_PHOTOS[p]}?w=${w}&h=${h}&fit=crop&q=70&auto=format`;
const fmt = (n: number, currency = "$") =>
`${currency}${n.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
const DEFAULT_CATEGORIES = ["All", "Pizza", "Sushi", "Burgers", "Healthy", "Mexican", "Coffee"];
/** Category → circle photo; "All" renders as an accent disc. */
const CATEGORY_PHOTOS: Record<string, string> = {
Pizza: img("pizza", 160, 160),
Sushi: img("sushi", 160, 160),
Burgers: img("burger", 160, 160),
Healthy: img("bowl", 160, 160),
Mexican: img("tacos", 160, 160),
Coffee: img("coffee", 160, 160),
};
const DEFAULT_RESTAURANTS: FoodRestaurant[] = [
{ id: "r1", name: "Nonna Rosa", cuisine: "Italian · $$", category: "Pizza", rating: 4.8, eta: "25–35 min", deliveryFee: "Free", imageUrl: img("pizza"), badge: "Popular" },
{ id: "r2", name: "Sakura Handroll Bar", cuisine: "Japanese · $$$", category: "Sushi", rating: 4.9, eta: "30–40 min", deliveryFee: "$1.99", imageUrl: img("sushi") },
{ id: "r3", name: "Smoky Stack", cuisine: "American · $", category: "Burgers", rating: 4.6, eta: "15–25 min", deliveryFee: "Free", imageUrl: img("burger"), badge: "New" },
{ id: "r4", name: "Green Ritual", cuisine: "Salads & bowls · $$", category: "Healthy", rating: 4.7, eta: "20–30 min", deliveryFee: "$0.99", imageUrl: img("bowl") },
{ id: "r5", name: "El Camino", cuisine: "Mexican · $", category: "Mexican", rating: 4.7, eta: "25–35 min", deliveryFee: "$1.49", imageUrl: img("tacos") },
{ id: "r6", name: "Kaffa Roasters", cuisine: "Coffee & brunch · $", category: "Coffee", rating: 4.9, eta: "10–20 min", deliveryFee: "Free", imageUrl: img("coffee") },
];
const DEFAULT_DISHES: FoodDish[] = [
{ id: "d1", name: "Margherita Verace", restaurant: "Nonna Rosa", price: 14.5, category: "Pizza", imageUrl: img("pizza", 240, 240) },
{ id: "d2", name: "Spicy Salmon Roll (8 pc)", restaurant: "Sakura Handroll Bar", price: 16.0, category: "Sushi", imageUrl: img("sushi", 240, 240) },
{ id: "d3", name: "Double Smash & Fries", restaurant: "Smoky Stack", price: 13.9, category: "Burgers", imageUrl: img("burger", 240, 240) },
{ id: "d4", name: "Harissa Chicken Bowl", restaurant: "Green Ritual", price: 12.75, category: "Healthy", imageUrl: img("bowl", 240, 240) },
{ id: "d5", name: "Tonkotsu Ramen", restaurant: "Menya Noodle", price: 15.4, category: "Sushi", imageUrl: img("ramen", 240, 240) },
{ id: "d6", name: "Al Pastor Trio", restaurant: "El Camino", price: 11.9, category: "Mexican", imageUrl: img("tacos", 240, 240) },
];
/** Cart affordance — secondary disc, count pill anchored to the corner. */
function CartButton({ count, onPress }: { count: number; onPress?: () => void }) {
const haptics = useHaptic();
return (
<Press
onPress={() => {
haptics.light();
onPress?.();
}}
accessibilityRole="button"
accessibilityLabel={count > 0 ? `Cart, ${count} items` : "Cart"}
hitSlop={8}
className="h-11 w-11 items-center justify-center overflow-hidden rounded-full"
>
<Glass interactive effect="clear" sheen={0.45} className="absolute inset-0 rounded-full" pointerEvents="none" />
<Icon name="shopping-cart" size="md" color="foreground" />
{count > 0 ? (
<View className="absolute -right-1 -top-1 min-w-5 items-center rounded-full bg-[#BEF04C] px-1.5 py-0.5">
<Text className="text-[10px] font-semibold text-[#1B1B26] tabular-nums">
{count}
</Text>
</View>
) : null}
</Press>
);
}
CartButton.displayName = "CartButton";
/** FoodHome — delivery home: address header with cart, search, category
* chips, a featured-restaurant rail, and a popular-dishes list. */
export function FoodHomeScreen({
address = "128 Linden St, Apt 4B",
categories = DEFAULT_CATEGORIES,
restaurants = DEFAULT_RESTAURANTS,
dishes = DEFAULT_DISHES,
cartCount = 2,
currency = "$",
onSearch,
onCategoryPress,
onRestaurantPress,
onDishPress,
onAddDish,
onCartPress,
onAddressPress,
onPromoPress,
}: FoodHomeScreenProps) {
const haptics = useHaptic();
const insets = useSafeAreaInsets();
const [query, setQuery] = useState("");
const [activeCategory, setActiveCategory] = useState(categories[0] ?? "All");
const pickCategory = (category: string) => {
setActiveCategory(category);
// Chip fires haptics.selection() internally — don't double it.
onCategoryPress?.(category);
};
const all = activeCategory === categories[0];
const visibleRestaurants = all
? restaurants
: restaurants.filter((r) => r.category === activeCategory);
const visibleDishes = all ? dishes : dishes.filter((d) => d.category === activeCategory);
return (
<World primary="#65A30D">
<SafeAreaView className="flex-1 bg-[#F4F5F9] dark:bg-[#111114]" edges={["top"]}>
{/* Address + cart header — the two things a delivery app always shows. */}
<Mount>
<View className="flex-row items-center gap-3 px-6 pb-2 pt-4">
<Press
onPress={() => {
haptics.light();
onAddressPress?.();
}}
accessibilityRole="button"
accessibilityLabel={`Deliver to ${address}, change address`}
hitSlop={8}
className="min-h-11 flex-1 flex-row items-center gap-2"
>
<Icon name="map-pin" size="md" color="#65A30D" />
<View className="flex-1">
<Text variant="muted" className="text-xs">
Deliver to
</Text>
<View className="flex-row items-center gap-1">
<Text variant="small" className="font-semibold" numberOfLines={1}>
{address}
</Text>
<Icon name="chevron-down" size="sm" color="mutedForeground" />
</View>
</View>
</Press>
<CartButton count={cartCount} onPress={onCartPress} />
</View>
</Mount>
{/* Search */}
<Mount delay={40}>
<View className="px-6 py-2">
<SearchInput
placeholder="Search dishes, restaurants…"
value={query}
debounceMs={300}
onChangeText={(text) => {
setQuery(text);
onSearch?.(text);
}}
returnKeyType="search"
accessibilityLabel="Search food"
/>
</View>
</Mount>
{/* Cuisine rail — circular food photos with a label, the DoorDash
idiom. Selected gets the accent ring instead of a filled chip. */}
<Mount delay={80}>
<ScrollView
horizontal
className="grow-0"
showsHorizontalScrollIndicator={false}
contentContainerClassName="gap-4 px-6 py-3"
keyboardShouldPersistTaps="handled"
>
{categories.map((category) => {
const selected = category === activeCategory;
const photo = CATEGORY_PHOTOS[category];
return (
<Press
key={category}
onPress={() => pickCategory(category)}
accessibilityRole="button"
accessibilityState={{ selected }}
accessibilityLabel={`Filter by ${category}`}
className="w-14 items-center gap-1.5"
>
<View
className={cn(
"h-14 w-14 overflow-hidden rounded-full",
selected ? "border-2 border-[#65A30D]" : "border border-border"
)}
style={selected ? { padding: 0 } : undefined}
>
{photo ? (
<Image
source={{ uri: photo }}
rounded="none"
className="h-full w-full"
accessibilityElementsHidden
/>
) : (
<View className="h-full w-full items-center justify-center bg-[#1B1B26]">
<Icon name="grid" size="sm" color="#BEF04C" />
</View>
)}
</View>
<Text
className={cn(
"text-xs",
selected ? "font-semibold text-[#65A30D]" : "text-muted-foreground"
)}
numberOfLines={1}
>
{category}
</Text>
</Press>
);
})}
</ScrollView>
</Mount>
<ScrollView
className="flex-1"
showsVerticalScrollIndicator={false}
contentContainerStyle={{ paddingBottom: insets.bottom + 88 }}
keyboardShouldPersistTaps="handled"
>
{/* Promo banner — warm amber gradient, the food-app signature.
Raw hex is art direction (like StoryChrome). */}
<Mount delay={100}>
<PromoBanner
className="mx-6 mb-2 mt-1"
eyebrow="Weekend deal"
title="Free delivery over $20"
subtitle="with code SUNDAY"
ctaLabel="Order now"
onCta={onPromoPress}
icon="utensils"
from="#F59E0B"
to="#DC2626"
/>
</Mount>
{/* Featured restaurants — horizontal rail of photo-led cards. */}
{/* Live-now line — food apps sell urgency and availability. */}
<Mount delay={90}>
<Text variant="muted" className="px-6 pt-1 text-xs">
{restaurants.length} places delivering to you now · avg 25–35 min
</Text>
</Mount>
{visibleRestaurants.length > 0 ? (
<Mount delay={120}>
<SectionHead title="Featured" action="See all" className="px-6 pb-3 pt-2" />
<ScrollView
horizontal
className="grow-0"
showsHorizontalScrollIndicator={false}
contentContainerClassName="gap-4 px-6"
>
{visibleRestaurants.map((restaurant) => (
<Card
variant="elevated"
key={restaurant.id}
onPress={() => onRestaurantPress?.(restaurant)}
accessibilityLabel={`${restaurant.name}, ${restaurant.cuisine}, rated ${restaurant.rating}`}
className="w-64 overflow-hidden rounded-3xl bg-white dark:bg-[#1A1A20]"
>
<View>
<Image
source={restaurant.imageUrl ? { uri: restaurant.imageUrl } : undefined}
rounded="none"
aspectRatio={16 / 10}
accessibilityLabel={`Photo of food at ${restaurant.name}`}
/>
{restaurant.badge ? (
<Badge variant="secondary" className="absolute left-3 top-3">
{restaurant.badge}
</Badge>
) : null}
</View>
<Card.Content className="gap-1 p-3">
<Text variant="small" className="font-semibold" numberOfLines={1}>
{restaurant.name}
</Text>
<Text variant="muted" className="text-xs" numberOfLines={1}>
{restaurant.cuisine}
</Text>
<View className="mt-1 flex-row items-center gap-3">
<View
className="flex-row items-center gap-1"
accessibilityLabel={`Rated ${restaurant.rating} out of 5`}
>
<Icon name="star" size="sm" color="warning" filled />
<Text variant="small" className="tabular-nums">
{restaurant.rating.toFixed(1)}
</Text>
</View>
<View className="flex-row items-center gap-1">
<Icon name="clock" size="sm" color="mutedForeground" />
<Text variant="muted" className="text-xs">
{restaurant.eta}
</Text>
</View>
{restaurant.deliveryFee === "Free" ? (
<Badge variant="success" size="sm">
Free
</Badge>
) : (
<Text variant="muted" className="text-xs tabular-nums">
{restaurant.deliveryFee}
</Text>
)}
</View>
</Card.Content>
</Card>
))}
</ScrollView>
</Mount>
) : null}
{/* Popular dishes — dense rows, thumbnail + price + add. */}
{visibleDishes.length > 0 ? (
<Mount delay={200}>
<SectionHead title="Popular near you" action="See all" className="px-6 pb-1 pt-6" />
<View className="px-6">
{visibleDishes.map((dish) => (
<Press
key={dish.id}
onPress={() => {
haptics.light();
onDishPress?.(dish);
}}
accessibilityRole="button"
accessibilityLabel={`${dish.name} from ${dish.restaurant}, ${fmt(dish.price, currency)}`}
className="min-h-11 flex-row items-center gap-3 py-3"
>
<Image
source={dish.imageUrl ? { uri: dish.imageUrl } : undefined}
rounded="xl"
className="h-14 w-14"
accessibilityLabel={`Photo of ${dish.name}`}
/>
<View className="flex-1">
<Text variant="small" numberOfLines={1}>
{dish.name}
</Text>
<Text variant="muted" className="text-xs" numberOfLines={1}>
{dish.restaurant}
</Text>
</View>
<Text variant="small" className="font-semibold tabular-nums">
{fmt(dish.price, currency)}
</Text>
<Press
onPress={() => {
haptics.medium();
onAddDish?.(dish);
}}
accessibilityRole="button"
accessibilityLabel={`Add ${dish.name} to cart`}
hitSlop={10}
className="h-11 w-11 items-center justify-center rounded-full bg-[#1B1B26]"
>
<Icon name="plus" size="md" color="#BEF04C" />
</Press>
</Press>
))}
</View>
</Mount>
) : null}
</ScrollView>
</SafeAreaView>
</World>
);
}
FoodHomeScreen.displayName = "FoodHomeScreen";
Recreate the "Food Home Screen" screen in React Native using LumioUI.
Goal: Delivery home — address header with cart pill, search, cuisine chips, featured restaurant rail, and popular dishes with add buttons Match the reference implementation's layout, behavior, and motion exactly — this is a production screen, not a mockup.
## Setup
Fastest path — install the screen plus its component deps:
```bash
npx lumioui add food-food-home
```
Or pull just the components: `npx lumioui add badge card chip icon image promo-banner search-input text`
Native packages:
- @lumioui/ui@latest
- @lumioui/core@latest
- react-native-safe-area-context@*
- react-native-reanimated@*
- react-native-svg@*
- nativewind@*
Stack assumptions: Expo SDK 54 / RN 0.81, Reanimated 4, NativeWind v4, TypeScript strict, Expo Go compatible.
## Components — how, where, when
### `badge` — Compact status pill with variants, icons and pulse option.
- `<Badge>` ×2
### `card` — Compound surface — header, title, content and footer slots.
- `<Card>` ×1
### `chip` — Compact selectable pill with press scale and ink overlay.
- imported but not rendered directly — likely used through a sibling or as a utility
### `icon` — Lucide icon set mapped to theme colors and sizes.
- `<Icon>` ×7
### `image` — Cached image with placeholder, fallback and aspect handling.
- `<Image>` ×3
### `promo-banner` — Gradient or photo promo card with eyebrow kicker, title, subtitle, and a glass CTA pill — the signature moment for home feeds.
- `<PromoBanner>` ×1
### `search-input` — Search field with clear button, debounce, focus halo — plus a glass capsule mode that floats over content like iOS 26
- `<SearchInput>` ×1
### `text` — Themed type scale from caption to display.
- `<Text>` ×13
## Conventions (non-negotiable)
- Styling via NativeWind `className` with semantic tokens only: `bg-background`, `text-foreground`, `bg-card`, `text-card-foreground`, `bg-secondary`, `bg-muted`, `text-muted-foreground`, `bg-accent`, `bg-destructive`, `border-border`. Hard-coded colors only inside media overlays (`text-white`, `bg-black/N`).
- Motion via the token presets from `@lumioui/core` (`spring.press`, `spring.surface`, `spring.glide`, `timing.fast`, `timing.slow`, …) — no bespoke spring configs or durations.
- Gate transform motion behind `useReducedMotion()`; keep opacity/color feedback, drop travel and overshoot.
- Haptics via `useHaptic()`: `light` on press, `selection` on toggle/select, `warning` on invalid input. Never import expo-haptics directly.
- Press feedback via `usePressAnimation()` (~0.97 scale), not `activeOpacity` tricks.
- a11y: `accessibilityRole` + `accessibilityLabel` on every control, touch targets ≥44pt.
- Entrance choreography: staggered `Mount` wrappers (delay 0/60/120…) so regions land in sequence instead of popping in at once.
- Wrap the screen in `SafeAreaView`; keep dark mode working via the semantic tokens.
## Deliverable
One file: `screens/food/food-home.tsx` exporting FoodHomeScreen. Preserve the props interface — the host app wires the callbacks (navigation, submit handlers) through them.
## Reference implementation
The LumioUI source for this screen — reproduce it faithfully:
```tsx
import * as React from "react";
import { useState } from "react";
import { ScrollView, View } from "react-native";
import { SafeAreaView, useSafeAreaInsets } from "react-native-safe-area-context";
import { Badge, Card, Glass, Icon, Image, PromoBanner, SearchInput, Text } from "@lumioui/ui";
import { cn, useHaptic } from "@lumioui/core";
import { Mount, Press, SectionHead, World } from "../shared";
export interface FoodRestaurant {
id: string;
name: string;
cuisine: string;
/** Category key used by the filter chips. */
category: string;
rating: number;
eta: string;
/** "Free" renders as a success badge; anything else shows as a price. */
deliveryFee: string;
imageUrl?: string;
badge?: string;
}
export interface FoodDish {
id: string;
name: string;
restaurant: string;
price: number;
category: string;
imageUrl?: string;
}
export interface FoodHomeScreenProps {
/** Short delivery address shown in the header. */
address?: string;
categories?: string[];
restaurants?: FoodRestaurant[];
dishes?: FoodDish[];
/** Item count on the cart affordance. Hidden when 0. */
cartCount?: number;
currency?: string;
onSearch?: (query: string) => void;
onCategoryPress?: (category: string) => void;
onRestaurantPress?: (restaurant: FoodRestaurant) => void;
onDishPress?: (dish: FoodDish) => void;
onAddDish?: (dish: FoodDish) => void;
onCartPress?: () => void;
onAddressPress?: () => void;
/** Promo banner CTA — "Order now". */
onPromoPress?: () => void;
}
/** Pinned Unsplash food photography — each id visually verified to show
* the named dish, the same contract catalogImg keeps for apparel. */
const FOOD_PHOTOS = {
burger: "photo-1568901346375-23c9450c58cd",
pizza: "photo-1513104890138-7c749659a591",
sushi: "photo-1579871494447-9811cf80d66c",
tacos: "photo-1551504734-5ee1c4a1479b",
ramen: "photo-1569718212165-3a8278d5f624",
salad: "photo-1512621776951-a57141f2eefd",
bowl: "photo-1546069901-ba9599a7e63c",
coffee: "photo-1509042239860-f550ce710b93",
dessert: "photo-1551024506-0bccd828d307",
} as const;
const img = (p: keyof typeof FOOD_PHOTOS, w = 640, h = 420) =>
`https://images.unsplash.com/${FOOD_PHOTOS[p]}?w=${w}&h=${h}&fit=crop&q=70&auto=format`;
const fmt = (n: number, currency = "$") =>
`${currency}${n.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
const DEFAULT_CATEGORIES = ["All", "Pizza", "Sushi", "Burgers", "Healthy", "Mexican", "Coffee"];
/** Category → circle photo; "All" renders as an accent disc. */
const CATEGORY_PHOTOS: Record<string, string> = {
Pizza: img("pizza", 160, 160),
Sushi: img("sushi", 160, 160),
Burgers: img("burger", 160, 160),
Healthy: img("bowl", 160, 160),
Mexican: img("tacos", 160, 160),
Coffee: img("coffee", 160, 160),
};
const DEFAULT_RESTAURANTS: FoodRestaurant[] = [
{ id: "r1", name: "Nonna Rosa", cuisine: "Italian · $$", category: "Pizza", rating: 4.8, eta: "25–35 min", deliveryFee: "Free", imageUrl: img("pizza"), badge: "Popular" },
{ id: "r2", name: "Sakura Handroll Bar", cuisine: "Japanese · $$$", category: "Sushi", rating: 4.9, eta: "30–40 min", deliveryFee: "$1.99", imageUrl: img("sushi") },
{ id: "r3", name: "Smoky Stack", cuisine: "American · $", category: "Burgers", rating: 4.6, eta: "15–25 min", deliveryFee: "Free", imageUrl: img("burger"), badge: "New" },
{ id: "r4", name: "Green Ritual", cuisine: "Salads & bowls · $$", category: "Healthy", rating: 4.7, eta: "20–30 min", deliveryFee: "$0.99", imageUrl: img("bowl") },
{ id: "r5", name: "El Camino", cuisine: "Mexican · $", category: "Mexican", rating: 4.7, eta: "25–35 min", deliveryFee: "$1.49", imageUrl: img("tacos") },
{ id: "r6", name: "Kaffa Roasters", cuisine: "Coffee & brunch · $", category: "Coffee", rating: 4.9, eta: "10–20 min", deliveryFee: "Free", imageUrl: img("coffee") },
];
const DEFAULT_DISHES: FoodDish[] = [
{ id: "d1", name: "Margherita Verace", restaurant: "Nonna Rosa", price: 14.5, category: "Pizza", imageUrl: img("pizza", 240, 240) },
{ id: "d2", name: "Spicy Salmon Roll (8 pc)", restaurant: "Sakura Handroll Bar", price: 16.0, category: "Sushi", imageUrl: img("sushi", 240, 240) },
{ id: "d3", name: "Double Smash & Fries", restaurant: "Smoky Stack", price: 13.9, category: "Burgers", imageUrl: img("burger", 240, 240) },
{ id: "d4", name: "Harissa Chicken Bowl", restaurant: "Green Ritual", price: 12.75, category: "Healthy", imageUrl: img("bowl", 240, 240) },
{ id: "d5", name: "Tonkotsu Ramen", restaurant: "Menya Noodle", price: 15.4, category: "Sushi", imageUrl: img("ramen", 240, 240) },
{ id: "d6", name: "Al Pastor Trio", restaurant: "El Camino", price: 11.9, category: "Mexican", imageUrl: img("tacos", 240, 240) },
];
/** Cart affordance — secondary disc, count pill anchored to the corner. */
function CartButton({ count, onPress }: { count: number; onPress?: () => void }) {
const haptics = useHaptic();
return (
<Press
onPress={() => {
haptics.light();
onPress?.();
}}
accessibilityRole="button"
accessibilityLabel={count > 0 ? `Cart, ${count} items` : "Cart"}
hitSlop={8}
className="h-11 w-11 items-center justify-center overflow-hidden rounded-full"
>
<Glass interactive effect="clear" sheen={0.45} className="absolute inset-0 rounded-full" pointerEvents="none" />
<Icon name="shopping-cart" size="md" color="foreground" />
{count > 0 ? (
<View className="absolute -right-1 -top-1 min-w-5 items-center rounded-full bg-[#BEF04C] px-1.5 py-0.5">
<Text className="text-[10px] font-semibold text-[#1B1B26] tabular-nums">
{count}
</Text>
</View>
) : null}
</Press>
);
}
CartButton.displayName = "CartButton";
/** FoodHome — delivery home: address header with cart, search, category
* chips, a featured-restaurant rail, and a popular-dishes list. */
export function FoodHomeScreen({
address = "128 Linden St, Apt 4B",
categories = DEFAULT_CATEGORIES,
restaurants = DEFAULT_RESTAURANTS,
dishes = DEFAULT_DISHES,
cartCount = 2,
currency = "$",
onSearch,
onCategoryPress,
onRestaurantPress,
onDishPress,
onAddDish,
onCartPress,
onAddressPress,
onPromoPress,
}: FoodHomeScreenProps) {
const haptics = useHaptic();
const insets = useSafeAreaInsets();
const [query, setQuery] = useState("");
const [activeCategory, setActiveCategory] = useState(categories[0] ?? "All");
const pickCategory = (category: string) => {
setActiveCategory(category);
// Chip fires haptics.selection() internally — don't double it.
onCategoryPress?.(category);
};
const all = activeCategory === categories[0];
const visibleRestaurants = all
? restaurants
: restaurants.filter((r) => r.category === activeCategory);
const visibleDishes = all ? dishes : dishes.filter((d) => d.category === activeCategory);
return (
<World primary="#65A30D">
<SafeAreaView className="flex-1 bg-[#F4F5F9] dark:bg-[#111114]" edges={["top"]}>
{/* Address + cart header — the two things a delivery app always shows. */}
<Mount>
<View className="flex-row items-center gap-3 px-6 pb-2 pt-4">
<Press
onPress={() => {
haptics.light();
onAddressPress?.();
}}
accessibilityRole="button"
accessibilityLabel={`Deliver to ${address}, change address`}
hitSlop={8}
className="min-h-11 flex-1 flex-row items-center gap-2"
>
<Icon name="map-pin" size="md" color="#65A30D" />
<View className="flex-1">
<Text variant="muted" className="text-xs">
Deliver to
</Text>
<View className="flex-row items-center gap-1">
<Text variant="small" className="font-semibold" numberOfLines={1}>
{address}
</Text>
<Icon name="chevron-down" size="sm" color="mutedForeground" />
</View>
</View>
</Press>
<CartButton count={cartCount} onPress={onCartPress} />
</View>
</Mount>
{/* Search */}
<Mount delay={40}>
<View className="px-6 py-2">
<SearchInput
placeholder="Search dishes, restaurants…"
value={query}
debounceMs={300}
onChangeText={(text) => {
setQuery(text);
onSearch?.(text);
}}
returnKeyType="search"
accessibilityLabel="Search food"
/>
</View>
</Mount>
{/* Cuisine rail — circular food photos with a label, the DoorDash
idiom. Selected gets the accent ring instead of a filled chip. */}
<Mount delay={80}>
<ScrollView
horizontal
className="grow-0"
showsHorizontalScrollIndicator={false}
contentContainerClassName="gap-4 px-6 py-3"
keyboardShouldPersistTaps="handled"
>
{categories.map((category) => {
const selected = category === activeCategory;
const photo = CATEGORY_PHOTOS[category];
return (
<Press
key={category}
onPress={() => pickCategory(category)}
accessibilityRole="button"
accessibilityState={{ selected }}
accessibilityLabel={`Filter by ${category}`}
className="w-14 items-center gap-1.5"
>
<View
className={cn(
"h-14 w-14 overflow-hidden rounded-full",
selected ? "border-2 border-[#65A30D]" : "border border-border"
)}
style={selected ? { padding: 0 } : undefined}
>
{photo ? (
<Image
source={{ uri: photo }}
rounded="none"
className="h-full w-full"
accessibilityElementsHidden
/>
) : (
<View className="h-full w-full items-center justify-center bg-[#1B1B26]">
<Icon name="grid" size="sm" color="#BEF04C" />
</View>
)}
</View>
<Text
className={cn(
"text-xs",
selected ? "font-semibold text-[#65A30D]" : "text-muted-foreground"
)}
numberOfLines={1}
>
{category}
</Text>
</Press>
);
})}
</ScrollView>
</Mount>
<ScrollView
className="flex-1"
showsVerticalScrollIndicator={false}
contentContainerStyle={{ paddingBottom: insets.bottom + 88 }}
keyboardShouldPersistTaps="handled"
>
{/* Promo banner — warm amber gradient, the food-app signature.
Raw hex is art direction (like StoryChrome). */}
<Mount delay={100}>
<PromoBanner
className="mx-6 mb-2 mt-1"
eyebrow="Weekend deal"
title="Free delivery over $20"
subtitle="with code SUNDAY"
ctaLabel="Order now"
onCta={onPromoPress}
icon="utensils"
from="#F59E0B"
to="#DC2626"
/>
</Mount>
{/* Featured restaurants — horizontal rail of photo-led cards. */}
{/* Live-now line — food apps sell urgency and availability. */}
<Mount delay={90}>
<Text variant="muted" className="px-6 pt-1 text-xs">
{restaurants.length} places delivering to you now · avg 25–35 min
</Text>
</Mount>
{visibleRestaurants.length > 0 ? (
<Mount delay={120}>
<SectionHead title="Featured" action="See all" className="px-6 pb-3 pt-2" />
<ScrollView
horizontal
className="grow-0"
showsHorizontalScrollIndicator={false}
contentContainerClassName="gap-4 px-6"
>
{visibleRestaurants.map((restaurant) => (
<Card
variant="elevated"
key={restaurant.id}
onPress={() => onRestaurantPress?.(restaurant)}
accessibilityLabel={`${restaurant.name}, ${restaurant.cuisine}, rated ${restaurant.rating}`}
className="w-64 overflow-hidden rounded-3xl bg-white dark:bg-[#1A1A20]"
>
<View>
<Image
source={restaurant.imageUrl ? { uri: restaurant.imageUrl } : undefined}
rounded="none"
aspectRatio={16 / 10}
accessibilityLabel={`Photo of food at ${restaurant.name}`}
/>
{restaurant.badge ? (
<Badge variant="secondary" className="absolute left-3 top-3">
{restaurant.badge}
</Badge>
) : null}
</View>
<Card.Content className="gap-1 p-3">
<Text variant="small" className="font-semibold" numberOfLines={1}>
{restaurant.name}
</Text>
<Text variant="muted" className="text-xs" numberOfLines={1}>
{restaurant.cuisine}
</Text>
<View className="mt-1 flex-row items-center gap-3">
<View
className="flex-row items-center gap-1"
accessibilityLabel={`Rated ${restaurant.rating} out of 5`}
>
<Icon name="star" size="sm" color="warning" filled />
<Text variant="small" className="tabular-nums">
{restaurant.rating.toFixed(1)}
</Text>
</View>
<View className="flex-row items-center gap-1">
<Icon name="clock" size="sm" color="mutedForeground" />
<Text variant="muted" className="text-xs">
{restaurant.eta}
</Text>
</View>
{restaurant.deliveryFee === "Free" ? (
<Badge variant="success" size="sm">
Free
</Badge>
) : (
<Text variant="muted" className="text-xs tabular-nums">
{restaurant.deliveryFee}
</Text>
)}
</View>
</Card.Content>
</Card>
))}
</ScrollView>
</Mount>
) : null}
{/* Popular dishes — dense rows, thumbnail + price + add. */}
{visibleDishes.length > 0 ? (
<Mount delay={200}>
<SectionHead title="Popular near you" action="See all" className="px-6 pb-1 pt-6" />
<View className="px-6">
{visibleDishes.map((dish) => (
<Press
key={dish.id}
onPress={() => {
haptics.light();
onDishPress?.(dish);
}}
accessibilityRole="button"
accessibilityLabel={`${dish.name} from ${dish.restaurant}, ${fmt(dish.price, currency)}`}
className="min-h-11 flex-row items-center gap-3 py-3"
>
<Image
source={dish.imageUrl ? { uri: dish.imageUrl } : undefined}
rounded="xl"
className="h-14 w-14"
accessibilityLabel={`Photo of ${dish.name}`}
/>
<View className="flex-1">
<Text variant="small" numberOfLines={1}>
{dish.name}
</Text>
<Text variant="muted" className="text-xs" numberOfLines={1}>
{dish.restaurant}
</Text>
</View>
<Text variant="small" className="font-semibold tabular-nums">
{fmt(dish.price, currency)}
</Text>
<Press
onPress={() => {
haptics.medium();
onAddDish?.(dish);
}}
accessibilityRole="button"
accessibilityLabel={`Add ${dish.name} to cart`}
hitSlop={10}
className="h-11 w-11 items-center justify-center rounded-full bg-[#1B1B26]"
>
<Icon name="plus" size="md" color="#BEF04C" />
</Press>
</Press>
))}
</View>
</Mount>
) : null}
</ScrollView>
</SafeAreaView>
</World>
);
}
FoodHomeScreen.displayName = "FoodHomeScreen";
```
Screen: https://lumioui.dev/screens/food-food-home