Full-bleed dish hero with floating back and favorite, rating and delivery stat row, offer strip, sectioned menu with add buttons, sticky cart bar
import * as React from "react";
import { useState } from "react";
import { Pressable, ScrollView, View } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { Badge, Button, Glass, Icon, Image, Separator, Text } from "@lumioui/ui";
import { useHaptic } from "@lumioui/core";
import { BLOCK_STAGGER, Display, Mount, StickyBottomBar, World } from "../shared";
export interface MenuItem {
id: string;
name: string;
description?: string;
price: number;
imageUrl?: string;
badge?: string;
}
export interface MenuSection {
title: string;
items: MenuItem[];
}
export interface RestaurantDetail {
name: string;
cuisine: string;
/** "$$" — rendered beside the cuisine. */
priceLevel?: string;
rating: number;
reviewCount?: number;
eta: string;
deliveryFee: string;
distance?: string;
imageUrl?: string;
/** Promo strip under the header — "20% off orders over $30". */
offer?: string;
}
export interface RestaurantDetailScreenProps {
restaurant?: RestaurantDetail;
sections?: MenuSection[];
/** Cart state powering the sticky bar — hidden when empty. */
cartCount?: number;
cartTotal?: number;
currency?: string;
onBack?: () => void;
onToggleFavorite?: (restaurant: RestaurantDetail, favorited: boolean) => void;
onAddItem?: (item: MenuItem) => void;
onItemPress?: (item: MenuItem) => void;
onViewCart?: () => void;
}
/** Pinned Unsplash food photography — each id visually verified to show
* the named dish. */
const FOOD_PHOTOS = {
burger: "photo-1568901346375-23c9450c58cd",
pizza: "photo-1513104890138-7c749659a591",
ramen: "photo-1569718212165-3a8278d5f624",
salad: "photo-1512621776951-a57141f2eefd",
pasta: "photo-1621996346565-e3dbc646d9a9",
dessert: "photo-1551024506-0bccd828d307",
steak: "photo-1504674900247-0877df9cc836",
interior: "photo-1517248135467-4c7edcad34c4",
} as const;
const img = (p: keyof typeof FOOD_PHOTOS, w = 320, h = 320) =>
`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_RESTAURANT: RestaurantDetail = {
name: "Nonna Rosa",
cuisine: "Italian · Wood-fired",
priceLevel: "$$",
rating: 4.8,
reviewCount: 1240,
eta: "25–35 min",
deliveryFee: "Free delivery",
distance: "1.2 mi",
imageUrl: img("interior", 900, 675),
offer: "20% off orders over $30",
};
const DEFAULT_MENU: MenuSection[] = [
{
title: "Popular",
items: [
{ id: "m1", name: "Margherita Verace", description: "San Marzano tomato, fior di latte, basil, cold-pressed olive oil", price: 14.5, imageUrl: img("pizza"), badge: "Most ordered" },
{ id: "m2", name: "Burrata e Prosciutto", description: "Creamy burrata, 18-month prosciutto, grilled sourdough", price: 16.0, imageUrl: img("salad") },
{ id: "m3", name: "Tiramisù della Casa", description: "Espresso-soaked savoiardi, mascarpone, cocoa", price: 8.5, imageUrl: img("dessert") },
],
},
{
title: "Mains",
items: [
{ id: "m4", name: "Rigatoni alla Vodka", description: "Tomato-vodka cream, pecorino, torn basil", price: 17.5, imageUrl: img("pasta") },
{ id: "m5", name: "Pollo al Limone", description: "Half chicken, charred lemon, rosemary jus", price: 21.0, imageUrl: img("steak") },
{ id: "m6", name: "Tonkotsu Ramen", description: "18-hour pork broth, chashu, ajitama, nori", price: 15.4, imageUrl: img("ramen") },
],
},
];
function Stars({ rating }: { rating: number }) {
return (
<View className="flex-row items-center gap-0.5" accessibilityElementsHidden importantForAccessibility="no">
{[0, 1, 2, 3, 4].map((i) => (
<Icon
key={i}
name="star"
size="sm"
color={i < Math.round(rating) ? "warning" : "mutedForeground"}
filled={i < Math.round(rating)}
/>
))}
</View>
);
}
Stars.displayName = "Stars";
/** One delivery promise — icon + bold value + muted label. */
function Stat({
icon,
value,
label,
}: {
icon: "clock" | "truck" | "map-pin";
value: string;
label: string;
}) {
return (
<View className="flex-1 flex-row items-center gap-2">
<Icon name={icon} size="sm" color="mutedForeground" />
<View>
<Text variant="small" className="font-semibold">
{value}
</Text>
<Text variant="muted" className="text-xs">
{label}
</Text>
</View>
</View>
);
}
Stat.displayName = "Stat";
/** RestaurantDetail — full-bleed dish hero, rating + logistics stat row,
* offer strip, sectioned menu with add buttons, sticky cart bar. */
export function RestaurantDetailScreen({
restaurant = DEFAULT_RESTAURANT,
sections = DEFAULT_MENU,
cartCount = 3,
cartTotal = 46.0,
currency = "$",
onBack,
onToggleFavorite,
onAddItem,
onItemPress,
onViewCart,
}: RestaurantDetailScreenProps) {
const haptics = useHaptic();
const insets = useSafeAreaInsets();
const [favorited, setFavorited] = useState(false);
return (
<World primary="#65A30D">
<View className="flex-1 bg-[#F4F5F9] dark:bg-[#111114]">
<ScrollView
className="flex-1"
showsVerticalScrollIndicator={false}
contentContainerStyle={{ paddingBottom: insets.bottom + 96 }}
// "never" keeps the hero edge-to-edge under the status bar;
// floating controls position themselves with insets.top.
contentInsetAdjustmentBehavior="never"
>
<View>
<Image
source={restaurant.imageUrl ? { uri: restaurant.imageUrl } : undefined}
rounded="none"
aspectRatio={4 / 3}
className="w-full"
accessibilityLabel={`Food at ${restaurant.name}`}
/>
{onBack ? (
<Glass interactive effect="regular" style={{ top: insets.top + 8 }} className="absolute left-4 rounded-full">
<Pressable
onPress={() => {
haptics.light();
onBack();
}}
accessibilityRole="button"
accessibilityLabel="Go back"
hitSlop={12}
className="h-10 w-10 items-center justify-center rounded-full"
>
<Icon name="chevron-left" size="md" color="foreground" />
</Pressable>
</Glass>
) : null}
<Glass interactive effect="regular" style={{ top: insets.top + 8 }} className="absolute right-4 rounded-full">
<Pressable
onPress={() => {
haptics.selection();
setFavorited((f) => !f);
onToggleFavorite?.(restaurant, !favorited);
}}
accessibilityRole="button"
accessibilityLabel={favorited ? `Remove ${restaurant.name} from favorites` : `Save ${restaurant.name} to favorites`}
accessibilityState={{ selected: favorited }}
hitSlop={12}
className="h-10 w-10 items-center justify-center rounded-full"
>
<Icon name="heart" size="md" color={favorited ? "destructive" : "foreground"} filled={favorited} />
</Pressable>
</Glass>
</View>
{/* Content sheet overlaps the hero edge — the DoorDash/Uber Eats
idiom: grabber on top, canvas-colored sheet with rounded top. */}
<View className="-mt-7 gap-6 rounded-t-[28px] bg-[#F4F5F9] px-6 pt-3 dark:bg-[#111114]">
<View className="mx-auto h-1 w-10 rounded-full bg-foreground/15" />
{/* Name / cuisine / rating / logistics */}
<Mount>
<View className="gap-1.5">
<Display className="text-3xl">{restaurant.name}</Display>
<Text variant="muted">
{restaurant.cuisine}
{restaurant.priceLevel ? ` · ${restaurant.priceLevel}` : ""}
</Text>
<View
className="flex-row items-center gap-2 pt-1"
accessibilityLabel={`Rated ${restaurant.rating} out of 5${
restaurant.reviewCount != null ? `, ${restaurant.reviewCount} reviews` : ""
}`}
>
<Stars rating={restaurant.rating} />
<Text variant="muted" className="tabular-nums">
{restaurant.rating.toFixed(1)}
{restaurant.reviewCount != null
? ` (${restaurant.reviewCount.toLocaleString()})`
: ""}
</Text>
</View>
<View className="flex-row gap-2 pt-3">
<Stat icon="clock" value={restaurant.eta} label="Delivery time" />
<Stat icon="truck" value={restaurant.deliveryFee} label="Fee" />
{restaurant.distance ? (
<Stat icon="map-pin" value={restaurant.distance} label="Distance" />
) : null}
</View>
{/* Open-hours truth — the line riders check before ordering. */}
<View className="flex-row items-center gap-1.5 pt-2">
<View className="h-1.5 w-1.5 rounded-full bg-success" />
<Text variant="muted" className="text-xs">
Open now · kitchen closes 11:00 PM
</Text>
</View>
</View>
</Mount>
{/* Coupon rail — dashed accent pills, the promo-card idiom from
the reference shots. */}
{restaurant.offer ? (
<Mount delay={BLOCK_STAGGER}>
<ScrollView
horizontal
className="grow-0 -mx-6"
showsHorizontalScrollIndicator={false}
contentContainerClassName="gap-2 px-6"
>
{[restaurant.offer, "Free delivery this week"].map((offer) => (
<View
key={offer}
className="flex-row items-center gap-1.5 rounded-xl border border-dashed border-[#65A30D] bg-[#EDF8C4] px-3 py-2"
>
<Icon name="tag" size="sm" color="#65A30D" />
<Text variant="small" className="font-medium">
{offer}
</Text>
</View>
))}
</ScrollView>
</Mount>
) : null}
{/* Menu sections — dish rows with photo + add affordance. */}
{sections.map((section, s) => (
<Mount key={section.title} delay={BLOCK_STAGGER * (s + 2)}>
<View className="gap-1">
<Text variant="small" className="pb-2 font-semibold">
{section.title}
</Text>
<View className="overflow-hidden rounded-3xl bg-white p-4 dark:bg-[#1A1A20]">
{section.items.map((item, i) => (
<View key={item.id}>
{i > 0 ? <Separator className="my-3" /> : null}
<Pressable
onPress={() => {
haptics.light();
onItemPress?.(item);
}}
accessibilityRole="button"
accessibilityLabel={`${item.name}, ${fmt(item.price, currency)}`}
className="min-h-11 flex-row gap-4 py-1"
>
<View className="flex-1 gap-1">
<View className="flex-row items-center gap-2">
<Text variant="small" className="font-semibold" numberOfLines={1}>
{item.name}
</Text>
{item.badge ? (
<Badge variant="secondary" size="sm">
{item.badge}
</Badge>
) : null}
</View>
{item.description ? (
<Text variant="muted" className="text-xs leading-4" numberOfLines={2}>
{item.description}
</Text>
) : null}
<Text variant="small" className="tabular-nums">
{fmt(item.price, currency)}
</Text>
</View>
<View>
<Image
source={item.imageUrl ? { uri: item.imageUrl } : undefined}
rounded="xl"
className="h-20 w-20"
accessibilityLabel={`Photo of ${item.name}`}
/>
<Pressable
onPress={() => {
haptics.medium();
onAddItem?.(item);
}}
accessibilityRole="button"
accessibilityLabel={`Add ${item.name} to cart`}
hitSlop={10}
className="absolute -bottom-1.5 -right-1.5 h-9 w-9 items-center justify-center rounded-full bg-[#1B1B26]"
>
<Icon name="plus" size="sm" color="#BEF04C" />
</Pressable>
</View>
</Pressable>
</View>
))}
</View>
</View>
</Mount>
))}
</View>
</ScrollView>
{cartCount > 0 ? (
<StickyBottomBar>
<Button
tone="inverse"
haptic="medium"
onPress={() => onViewCart?.()}
accessibilityLabel={`View cart, ${cartCount} items, total ${fmt(cartTotal, currency)}`}
icon={<Icon name="shopping-cart" size="sm" color="#BEF04C" />}
className="bg-[#1B1B26]">
View cart · {cartCount} · {fmt(cartTotal, currency)}
</Button>
</StickyBottomBar>
) : null}
</View>
</World>
);
}
RestaurantDetailScreen.displayName = "RestaurantDetailScreen";
Recreate the "Restaurant Detail Screen" screen in React Native using LumioUI.
Goal: Full-bleed dish hero with floating back and favorite, rating and delivery stat row, offer strip, sectioned menu with add buttons, sticky cart bar 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-restaurant-detail
```
Or pull just the components: `npx lumioui add badge button glass icon image separator 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>` ×1
### `button` — Tactile pill button with press spring, ink wash and async loading.
- `<Button>` ×1
### `glass` — iOS 26 liquid-glass surface — real glass via expo-glass-effect (Expo Go) or @callstack/liquid-glass (dev builds), SVG refraction + specular rim on web, frosted fallback elsewhere; GlassContainer melts adjacent panes into one blob
- `<Glass>` ×2
### `icon` — Lucide icon set mapped to theme colors and sizes.
- `<Icon>` ×7
### `image` — Cached image with placeholder, fallback and aspect handling.
- `<Image>` ×2
### `separator` — Hairline rule with orientation and spacing control.
- `<Separator>` ×1
### `text` — Themed type scale from caption to display.
- `<Text>` ×10
## 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/restaurant-detail.tsx` exporting RestaurantDetailScreen. 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 { Pressable, ScrollView, View } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { Badge, Button, Glass, Icon, Image, Separator, Text } from "@lumioui/ui";
import { useHaptic } from "@lumioui/core";
import { BLOCK_STAGGER, Display, Mount, StickyBottomBar, World } from "../shared";
export interface MenuItem {
id: string;
name: string;
description?: string;
price: number;
imageUrl?: string;
badge?: string;
}
export interface MenuSection {
title: string;
items: MenuItem[];
}
export interface RestaurantDetail {
name: string;
cuisine: string;
/** "$$" — rendered beside the cuisine. */
priceLevel?: string;
rating: number;
reviewCount?: number;
eta: string;
deliveryFee: string;
distance?: string;
imageUrl?: string;
/** Promo strip under the header — "20% off orders over $30". */
offer?: string;
}
export interface RestaurantDetailScreenProps {
restaurant?: RestaurantDetail;
sections?: MenuSection[];
/** Cart state powering the sticky bar — hidden when empty. */
cartCount?: number;
cartTotal?: number;
currency?: string;
onBack?: () => void;
onToggleFavorite?: (restaurant: RestaurantDetail, favorited: boolean) => void;
onAddItem?: (item: MenuItem) => void;
onItemPress?: (item: MenuItem) => void;
onViewCart?: () => void;
}
/** Pinned Unsplash food photography — each id visually verified to show
* the named dish. */
const FOOD_PHOTOS = {
burger: "photo-1568901346375-23c9450c58cd",
pizza: "photo-1513104890138-7c749659a591",
ramen: "photo-1569718212165-3a8278d5f624",
salad: "photo-1512621776951-a57141f2eefd",
pasta: "photo-1621996346565-e3dbc646d9a9",
dessert: "photo-1551024506-0bccd828d307",
steak: "photo-1504674900247-0877df9cc836",
interior: "photo-1517248135467-4c7edcad34c4",
} as const;
const img = (p: keyof typeof FOOD_PHOTOS, w = 320, h = 320) =>
`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_RESTAURANT: RestaurantDetail = {
name: "Nonna Rosa",
cuisine: "Italian · Wood-fired",
priceLevel: "$$",
rating: 4.8,
reviewCount: 1240,
eta: "25–35 min",
deliveryFee: "Free delivery",
distance: "1.2 mi",
imageUrl: img("interior", 900, 675),
offer: "20% off orders over $30",
};
const DEFAULT_MENU: MenuSection[] = [
{
title: "Popular",
items: [
{ id: "m1", name: "Margherita Verace", description: "San Marzano tomato, fior di latte, basil, cold-pressed olive oil", price: 14.5, imageUrl: img("pizza"), badge: "Most ordered" },
{ id: "m2", name: "Burrata e Prosciutto", description: "Creamy burrata, 18-month prosciutto, grilled sourdough", price: 16.0, imageUrl: img("salad") },
{ id: "m3", name: "Tiramisù della Casa", description: "Espresso-soaked savoiardi, mascarpone, cocoa", price: 8.5, imageUrl: img("dessert") },
],
},
{
title: "Mains",
items: [
{ id: "m4", name: "Rigatoni alla Vodka", description: "Tomato-vodka cream, pecorino, torn basil", price: 17.5, imageUrl: img("pasta") },
{ id: "m5", name: "Pollo al Limone", description: "Half chicken, charred lemon, rosemary jus", price: 21.0, imageUrl: img("steak") },
{ id: "m6", name: "Tonkotsu Ramen", description: "18-hour pork broth, chashu, ajitama, nori", price: 15.4, imageUrl: img("ramen") },
],
},
];
function Stars({ rating }: { rating: number }) {
return (
<View className="flex-row items-center gap-0.5" accessibilityElementsHidden importantForAccessibility="no">
{[0, 1, 2, 3, 4].map((i) => (
<Icon
key={i}
name="star"
size="sm"
color={i < Math.round(rating) ? "warning" : "mutedForeground"}
filled={i < Math.round(rating)}
/>
))}
</View>
);
}
Stars.displayName = "Stars";
/** One delivery promise — icon + bold value + muted label. */
function Stat({
icon,
value,
label,
}: {
icon: "clock" | "truck" | "map-pin";
value: string;
label: string;
}) {
return (
<View className="flex-1 flex-row items-center gap-2">
<Icon name={icon} size="sm" color="mutedForeground" />
<View>
<Text variant="small" className="font-semibold">
{value}
</Text>
<Text variant="muted" className="text-xs">
{label}
</Text>
</View>
</View>
);
}
Stat.displayName = "Stat";
/** RestaurantDetail — full-bleed dish hero, rating + logistics stat row,
* offer strip, sectioned menu with add buttons, sticky cart bar. */
export function RestaurantDetailScreen({
restaurant = DEFAULT_RESTAURANT,
sections = DEFAULT_MENU,
cartCount = 3,
cartTotal = 46.0,
currency = "$",
onBack,
onToggleFavorite,
onAddItem,
onItemPress,
onViewCart,
}: RestaurantDetailScreenProps) {
const haptics = useHaptic();
const insets = useSafeAreaInsets();
const [favorited, setFavorited] = useState(false);
return (
<World primary="#65A30D">
<View className="flex-1 bg-[#F4F5F9] dark:bg-[#111114]">
<ScrollView
className="flex-1"
showsVerticalScrollIndicator={false}
contentContainerStyle={{ paddingBottom: insets.bottom + 96 }}
// "never" keeps the hero edge-to-edge under the status bar;
// floating controls position themselves with insets.top.
contentInsetAdjustmentBehavior="never"
>
<View>
<Image
source={restaurant.imageUrl ? { uri: restaurant.imageUrl } : undefined}
rounded="none"
aspectRatio={4 / 3}
className="w-full"
accessibilityLabel={`Food at ${restaurant.name}`}
/>
{onBack ? (
<Glass interactive effect="regular" style={{ top: insets.top + 8 }} className="absolute left-4 rounded-full">
<Pressable
onPress={() => {
haptics.light();
onBack();
}}
accessibilityRole="button"
accessibilityLabel="Go back"
hitSlop={12}
className="h-10 w-10 items-center justify-center rounded-full"
>
<Icon name="chevron-left" size="md" color="foreground" />
</Pressable>
</Glass>
) : null}
<Glass interactive effect="regular" style={{ top: insets.top + 8 }} className="absolute right-4 rounded-full">
<Pressable
onPress={() => {
haptics.selection();
setFavorited((f) => !f);
onToggleFavorite?.(restaurant, !favorited);
}}
accessibilityRole="button"
accessibilityLabel={favorited ? `Remove ${restaurant.name} from favorites` : `Save ${restaurant.name} to favorites`}
accessibilityState={{ selected: favorited }}
hitSlop={12}
className="h-10 w-10 items-center justify-center rounded-full"
>
<Icon name="heart" size="md" color={favorited ? "destructive" : "foreground"} filled={favorited} />
</Pressable>
</Glass>
</View>
{/* Content sheet overlaps the hero edge — the DoorDash/Uber Eats
idiom: grabber on top, canvas-colored sheet with rounded top. */}
<View className="-mt-7 gap-6 rounded-t-[28px] bg-[#F4F5F9] px-6 pt-3 dark:bg-[#111114]">
<View className="mx-auto h-1 w-10 rounded-full bg-foreground/15" />
{/* Name / cuisine / rating / logistics */}
<Mount>
<View className="gap-1.5">
<Display className="text-3xl">{restaurant.name}</Display>
<Text variant="muted">
{restaurant.cuisine}
{restaurant.priceLevel ? ` · ${restaurant.priceLevel}` : ""}
</Text>
<View
className="flex-row items-center gap-2 pt-1"
accessibilityLabel={`Rated ${restaurant.rating} out of 5${
restaurant.reviewCount != null ? `, ${restaurant.reviewCount} reviews` : ""
}`}
>
<Stars rating={restaurant.rating} />
<Text variant="muted" className="tabular-nums">
{restaurant.rating.toFixed(1)}
{restaurant.reviewCount != null
? ` (${restaurant.reviewCount.toLocaleString()})`
: ""}
</Text>
</View>
<View className="flex-row gap-2 pt-3">
<Stat icon="clock" value={restaurant.eta} label="Delivery time" />
<Stat icon="truck" value={restaurant.deliveryFee} label="Fee" />
{restaurant.distance ? (
<Stat icon="map-pin" value={restaurant.distance} label="Distance" />
) : null}
</View>
{/* Open-hours truth — the line riders check before ordering. */}
<View className="flex-row items-center gap-1.5 pt-2">
<View className="h-1.5 w-1.5 rounded-full bg-success" />
<Text variant="muted" className="text-xs">
Open now · kitchen closes 11:00 PM
</Text>
</View>
</View>
</Mount>
{/* Coupon rail — dashed accent pills, the promo-card idiom from
the reference shots. */}
{restaurant.offer ? (
<Mount delay={BLOCK_STAGGER}>
<ScrollView
horizontal
className="grow-0 -mx-6"
showsHorizontalScrollIndicator={false}
contentContainerClassName="gap-2 px-6"
>
{[restaurant.offer, "Free delivery this week"].map((offer) => (
<View
key={offer}
className="flex-row items-center gap-1.5 rounded-xl border border-dashed border-[#65A30D] bg-[#EDF8C4] px-3 py-2"
>
<Icon name="tag" size="sm" color="#65A30D" />
<Text variant="small" className="font-medium">
{offer}
</Text>
</View>
))}
</ScrollView>
</Mount>
) : null}
{/* Menu sections — dish rows with photo + add affordance. */}
{sections.map((section, s) => (
<Mount key={section.title} delay={BLOCK_STAGGER * (s + 2)}>
<View className="gap-1">
<Text variant="small" className="pb-2 font-semibold">
{section.title}
</Text>
<View className="overflow-hidden rounded-3xl bg-white p-4 dark:bg-[#1A1A20]">
{section.items.map((item, i) => (
<View key={item.id}>
{i > 0 ? <Separator className="my-3" /> : null}
<Pressable
onPress={() => {
haptics.light();
onItemPress?.(item);
}}
accessibilityRole="button"
accessibilityLabel={`${item.name}, ${fmt(item.price, currency)}`}
className="min-h-11 flex-row gap-4 py-1"
>
<View className="flex-1 gap-1">
<View className="flex-row items-center gap-2">
<Text variant="small" className="font-semibold" numberOfLines={1}>
{item.name}
</Text>
{item.badge ? (
<Badge variant="secondary" size="sm">
{item.badge}
</Badge>
) : null}
</View>
{item.description ? (
<Text variant="muted" className="text-xs leading-4" numberOfLines={2}>
{item.description}
</Text>
) : null}
<Text variant="small" className="tabular-nums">
{fmt(item.price, currency)}
</Text>
</View>
<View>
<Image
source={item.imageUrl ? { uri: item.imageUrl } : undefined}
rounded="xl"
className="h-20 w-20"
accessibilityLabel={`Photo of ${item.name}`}
/>
<Pressable
onPress={() => {
haptics.medium();
onAddItem?.(item);
}}
accessibilityRole="button"
accessibilityLabel={`Add ${item.name} to cart`}
hitSlop={10}
className="absolute -bottom-1.5 -right-1.5 h-9 w-9 items-center justify-center rounded-full bg-[#1B1B26]"
>
<Icon name="plus" size="sm" color="#BEF04C" />
</Pressable>
</View>
</Pressable>
</View>
))}
</View>
</View>
</Mount>
))}
</View>
</ScrollView>
{cartCount > 0 ? (
<StickyBottomBar>
<Button
tone="inverse"
haptic="medium"
onPress={() => onViewCart?.()}
accessibilityLabel={`View cart, ${cartCount} items, total ${fmt(cartTotal, currency)}`}
icon={<Icon name="shopping-cart" size="sm" color="#BEF04C" />}
className="bg-[#1B1B26]">
View cart · {cartCount} · {fmt(cartTotal, currency)}
</Button>
</StickyBottomBar>
) : null}
</View>
</World>
);
}
RestaurantDetailScreen.displayName = "RestaurantDetailScreen";
```
Screen: https://lumioui.dev/screens/food-restaurant-detail