Delivery order review — delivery/pickup switch, dish rows with steppers and notes, address card, utensils toggle, promo field, fee summary, sticky place-order bar
import * as React from "react";
import { useState } from "react";
import { FlatList, Pressable, View } from "react-native";
import { SafeAreaView, useSafeAreaInsets } from "react-native-safe-area-context";
import { Button, Card, Checkbox, EmptyState, Glass, Icon, Image, Input, SegmentedControl, Separator, Stepper, Text } from "@lumioui/ui";
import { useHaptic } from "@lumioui/core";
import { LumioMark, Mount, Press, ROW_STAGGER, ROW_STAGGER_CAP, StickyBottomBar, World } from "../shared";
export interface FoodCartItem {
id: string;
name: string;
/** Customizations — "Extra basil · No onion". */
notes?: string;
price: number;
quantity: number;
imageUrl?: string;
}
export interface FoodCartScreenProps {
items?: FoodCartItem[];
restaurant?: string;
/** Delivery address — shown only while fulfillment is "Delivery". */
address?: string;
currency?: string;
deliveryFee?: number;
serviceFee?: number;
/** Free delivery above this subtotal; deliveryFee applies otherwise. */
freeDeliveryThreshold?: number;
fulfillment?: "Delivery" | "Pickup";
onFulfillmentChange?: (mode: "Delivery" | "Pickup") => void;
onUpdateQty?: (item: FoodCartItem, quantity: number) => void;
onRemove?: (item: FoodCartItem) => void;
onApplyPromo?: (code: string) => void;
onBack?: () => void;
onPlaceOrder?: () => void;
onChangeAddress?: () => void;
onBrowse?: () => void;
}
/** Pinned Unsplash food photography — each id visually verified to show
* the named dish. */
const FOOD_PHOTOS = {
pizza: "photo-1513104890138-7c749659a591",
sushi: "photo-1579871494447-9811cf80d66c",
dessert: "photo-1551024506-0bccd828d307",
} as const;
const img = (p: keyof typeof FOOD_PHOTOS) =>
`https://images.unsplash.com/${FOOD_PHOTOS[p]}?w=240&h=240&fit=crop&q=70&auto=format`;
const fmt = (n: number, currency = "$") =>
`${currency}${n.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
const DEFAULT_ITEMS: FoodCartItem[] = [
{ id: "1", name: "Margherita Verace", notes: "Extra basil", price: 14.5, quantity: 1, imageUrl: img("pizza") },
{ id: "2", name: "Spicy Salmon Roll (8 pc)", notes: "No wasabi", price: 16.0, quantity: 2, imageUrl: img("sushi") },
{ id: "3", name: "Tiramisù della Casa", price: 8.5, quantity: 1, imageUrl: img("dessert") },
];
function SummaryRow({
label,
value,
bold,
accent,
}: {
label: string;
value: string;
bold?: boolean;
/** Success-green value — used for the promo discount. */
accent?: boolean;
}) {
return (
<View className="flex-row items-center justify-between py-1">
<Text variant={bold ? "large" : "muted"}>{label}</Text>
<Text
variant={bold ? "large" : "small"}
className={accent ? "font-semibold tabular-nums text-success" : "tabular-nums"}
>
{value}
</Text>
</View>
);
}
SummaryRow.displayName = "SummaryRow";
/** FoodCart — order review for delivery: fulfillment switch, dish rows
* with steppers and notes, address card, utensils toggle, promo,
* fee summary, sticky place-order bar. */
export function FoodCartScreen({
items = DEFAULT_ITEMS,
restaurant = "Nonna Rosa",
address = "128 Linden St, Apt 4B",
currency = "$",
deliveryFee = 1.99,
serviceFee = 2.74,
freeDeliveryThreshold = 40,
fulfillment = "Delivery",
onFulfillmentChange,
onUpdateQty,
onRemove,
onApplyPromo,
onBack,
onPlaceOrder,
onChangeAddress,
onBrowse,
}: FoodCartScreenProps) {
const haptics = useHaptic();
const insets = useSafeAreaInsets();
const [mode, setMode] = useState<"Delivery" | "Pickup">(fulfillment);
const [utensils, setUtensils] = useState(false);
const [promo, setPromo] = useState("");
const [appliedPromo, setAppliedPromo] = useState<string | null>(null);
const subtotal = items.reduce((sum, i) => sum + i.price * i.quantity, 0);
const delivery =
mode === "Pickup" || items.length === 0 || subtotal >= freeDeliveryThreshold
? 0
: deliveryFee;
const discount = appliedPromo ? Math.round(subtotal * 0.1 * 100) / 100 : 0;
const total = subtotal - discount + delivery + (items.length === 0 ? 0 : serviceFee);
const itemCount = items.reduce((sum, i) => sum + i.quantity, 0);
return (
<World primary="#65A30D">
<SafeAreaView className="flex-1 bg-[#F4F5F9] dark:bg-[#111114]" edges={["top"]}>
<View pointerEvents="box-none" className="absolute inset-x-4 z-10" style={{ top: insets.top + 8 }}>
<View className="flex-row items-center justify-between">
<Glass interactive effect="regular" className="h-11 w-11 items-center justify-center rounded-full">
<Press
haptic="light"
onPress={() => onBack?.()}
accessibilityRole="button"
accessibilityLabel="Go back"
className="h-9 w-9 items-center justify-center"
>
<Icon name="chevron-left" size="md" color="foreground" />
</Press>
</Glass>
<View className="h-11 w-11" />
</View>
<View pointerEvents="none" className="absolute inset-0 items-center justify-center">
<View className="h-11 flex-row items-center justify-center gap-2 rounded-full pl-3.5 pr-5">
<Glass effect="regular" className="absolute inset-0 rounded-full" />
<LumioMark size={18} />
<Text accessibilityRole="header" className="text-sm font-bold tracking-tight">
Your order{itemCount > 0 ? ` · ${itemCount}` : ""}
</Text>
</View>
</View>
</View>
<View className="px-6 pb-2" style={{ paddingTop: insets.top + 60 }}>
{/* Threshold nudge — the line that drives basket size. */}
<Text variant="muted" className="pb-1 text-xs">
{restaurant}
{subtotal < freeDeliveryThreshold && items.length > 0
? ` · $${(freeDeliveryThreshold - subtotal).toFixed(2)} away from free delivery`
: ""}
</Text>
</View>
<View className="px-6 pb-4">
<SegmentedControl
options={["Delivery", "Pickup"]}
value={mode}
onValueChange={(v) => {
const next = v as "Delivery" | "Pickup";
setMode(next);
onFulfillmentChange?.(next);
}}
accessibilityLabel="Fulfillment method"
/>
</View>
{items.length === 0 ? (
<View className="flex-1">
<EmptyState
title="Your cart is empty"
description="Dishes you add will appear here."
action={
onBrowse ? (
<Button variant="secondary" onPress={onBrowse}>
Browse restaurants
</Button>
) : undefined
}
className="flex-1"
/>
</View>
) : (
<>
<FlatList
data={items}
keyExtractor={(item) => item.id}
className="flex-1"
contentContainerClassName="px-6 pb-44"
contentInsetAdjustmentBehavior="automatic"
showsVerticalScrollIndicator={false}
ListHeaderComponent={
mode === "Delivery" ? (
<Card variant="elevated" className="mb-4 rounded-3xl bg-white dark:bg-[#1A1A20]">
<Card.Content className="flex-row items-center gap-3 p-4">
<View className="h-9 w-9 items-center justify-center rounded-lg bg-[#EDF8C4]">
<Icon name="map-pin" size="sm" color="#65A30D" />
</View>
<View className="flex-1">
<Text variant="small" className="font-semibold">
{address}
</Text>
<Text variant="muted" className="text-xs">
Leave at door
</Text>
</View>
{onChangeAddress ? (
<Pressable
onPress={() => {
haptics.light();
onChangeAddress();
}}
accessibilityRole="button"
accessibilityLabel="Change delivery address"
hitSlop={12}
className="min-h-11 justify-center"
>
<Text variant="small" className="font-medium text-[#65A30D]">
Change
</Text>
</Pressable>
) : null}
</Card.Content>
</Card>
) : null
}
ItemSeparatorComponent={() => <View className="h-5" />}
renderItem={({ item, index }) => (
<Mount delay={Math.min(index * ROW_STAGGER, ROW_STAGGER_CAP)}>
<View className="flex-row gap-4">
<Image
source={item.imageUrl ? { uri: item.imageUrl } : undefined}
rounded="lg"
className="h-16 w-16"
accessibilityLabel={`Photo of ${item.name}`}
/>
<View className="flex-1">
<View className="flex-row items-start justify-between">
<View className="flex-1 gap-0.5 pr-2">
<Text variant="small" numberOfLines={1}>
{item.name}
</Text>
{item.notes ? (
<Text variant="muted" className="text-xs" numberOfLines={1}>
{item.notes}
</Text>
) : null}
</View>
<Pressable
onPress={() => {
haptics.warning();
onRemove?.(item);
}}
accessibilityRole="button"
accessibilityLabel={`Remove ${item.name} from order`}
hitSlop={12}
className="h-11 w-11 items-center justify-center"
>
<Icon name="trash" size="sm" color="mutedForeground" />
</Pressable>
</View>
<View className="flex-row items-center justify-between pt-1.5">
<Stepper
value={item.quantity}
min={0}
onValueChange={(qty) =>
qty === 0 ? onRemove?.(item) : onUpdateQty?.(item, qty)
}
accessibilityLabel={`Quantity for ${item.name}`}
className="border-transparent bg-muted/60"
/>
<Text
variant="small"
className="font-semibold tabular-nums"
accessibilityLabel={`${item.name} line total ${fmt(item.price * item.quantity, currency)}`}
>
{fmt(item.price * item.quantity, currency)}
</Text>
</View>
</View>
</View>
</Mount>
)}
ListFooterComponent={
<View className="gap-4 pt-4">
<Checkbox
checked={utensils}
onCheckedChange={setUtensils}
label="Include utensils & napkins"
icon="utensils"
accessibilityLabel="Include utensils and napkins"
/>
{appliedPromo ? (
<View className="flex-row items-center justify-between rounded-2xl bg-success/10 py-1 pl-4">
<View className="flex-1 flex-row items-center gap-2">
<Icon name="check-circle" size="sm" color="success" />
<Text variant="small">
Promo{" "}
<Text variant="small" className="font-semibold">
{appliedPromo}
</Text>{" "}
applied — 10% off
</Text>
</View>
<Pressable
onPress={() => {
haptics.light();
setAppliedPromo(null);
setPromo("");
}}
accessibilityRole="button"
accessibilityLabel={`Remove promo code ${appliedPromo}`}
hitSlop={12}
className="h-11 w-11 items-center justify-center"
>
<Icon name="x" size="sm" color="mutedForeground" />
</Pressable>
</View>
) : (
<View className="flex-row gap-2">
<View className="flex-1">
<Input
placeholder="Promo code"
value={promo}
onChangeText={setPromo}
autoCapitalize="characters"
accessibilityLabel="Promo code"
/>
</View>
<Button
variant="secondary"
disabled={promo.trim().length === 0}
onPress={() => {
const code = promo.trim();
haptics.success();
setAppliedPromo(code);
onApplyPromo?.(code);
}}
>
Apply
</Button>
</View>
)}
<Card variant="elevated" className="gap-0 rounded-3xl bg-white p-4 dark:bg-[#1A1A20]">
<SummaryRow label="Subtotal" value={fmt(subtotal, currency)} />
{discount > 0 ? (
<SummaryRow
label={`Promo (${appliedPromo})`}
value={`-${fmt(discount, currency)}`}
accent
/>
) : null}
<SummaryRow
label={mode === "Pickup" ? "Pickup" : "Delivery"}
value={delivery === 0 ? "Free" : fmt(delivery, currency)}
/>
<SummaryRow label="Service fee" value={fmt(serviceFee, currency)} />
<Separator className="my-2" />
<SummaryRow label="Total" value={fmt(total, currency)} bold />
</Card>
</View>
}
/>
<StickyBottomBar>
<View className="rounded-3xl bg-white p-3 shadow-sm dark:bg-[#1A1A20]">
<Button
tone="inverse"
haptic="heavy"
onPress={() => onPlaceOrder?.()}
accessibilityLabel={`Place order, total ${fmt(total, currency)}`}
className="bg-[#1B1B26]"
>
Place order · {fmt(total, currency)}
</Button>
</View>
</StickyBottomBar>
</>
)}
</SafeAreaView>
</World>
);
}
FoodCartScreen.displayName = "FoodCartScreen";
Recreate the "Food Cart Screen" screen in React Native using LumioUI.
Goal: Delivery order review — delivery/pickup switch, dish rows with steppers and notes, address card, utensils toggle, promo field, fee summary, sticky place-order 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-food-cart
```
Or pull just the components: `npx lumioui add button card checkbox empty-state icon image input segmented-control separator stepper 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
### `button` — Tactile pill button with press spring, ink wash and async loading.
- `<Button>` ×3
### `card` — Compound surface — header, title, content and footer slots.
- `<Card>` ×2
### `checkbox` — Animated check box with haptic tick and error state.
- `<Checkbox>` ×1
### `empty-state` — Illustrated placeholder with title, copy and action slot.
- `<EmptyState>` ×1
### `icon` — Lucide icon set mapped to theme colors and sizes.
- `<Icon>` ×5
### `image` — Cached image with placeholder, fallback and aspect handling.
- `<Image>` ×1
### `input` — Labeled field with focus ring, error shake and success check.
- `<Input>` ×1
### `segmented-control` — iOS 26 segmented picker — glass capsule track with a gliding liquid indicator pill, spring motion + haptics
- `<SegmentedControl>` ×1
### `separator` — Hairline rule with orientation and spacing control.
- `<Separator>` ×1
### `stepper` — Plus/minus quantity control with hold-to-repeat.
- `<Stepper>` ×1
### `text` — Themed type scale from caption to display.
- `<Text>` ×12
## 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-cart.tsx` exporting FoodCartScreen. 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 { FlatList, Pressable, View } from "react-native";
import { SafeAreaView, useSafeAreaInsets } from "react-native-safe-area-context";
import { Button, Card, Checkbox, EmptyState, Glass, Icon, Image, Input, SegmentedControl, Separator, Stepper, Text } from "@lumioui/ui";
import { useHaptic } from "@lumioui/core";
import { LumioMark, Mount, Press, ROW_STAGGER, ROW_STAGGER_CAP, StickyBottomBar, World } from "../shared";
export interface FoodCartItem {
id: string;
name: string;
/** Customizations — "Extra basil · No onion". */
notes?: string;
price: number;
quantity: number;
imageUrl?: string;
}
export interface FoodCartScreenProps {
items?: FoodCartItem[];
restaurant?: string;
/** Delivery address — shown only while fulfillment is "Delivery". */
address?: string;
currency?: string;
deliveryFee?: number;
serviceFee?: number;
/** Free delivery above this subtotal; deliveryFee applies otherwise. */
freeDeliveryThreshold?: number;
fulfillment?: "Delivery" | "Pickup";
onFulfillmentChange?: (mode: "Delivery" | "Pickup") => void;
onUpdateQty?: (item: FoodCartItem, quantity: number) => void;
onRemove?: (item: FoodCartItem) => void;
onApplyPromo?: (code: string) => void;
onBack?: () => void;
onPlaceOrder?: () => void;
onChangeAddress?: () => void;
onBrowse?: () => void;
}
/** Pinned Unsplash food photography — each id visually verified to show
* the named dish. */
const FOOD_PHOTOS = {
pizza: "photo-1513104890138-7c749659a591",
sushi: "photo-1579871494447-9811cf80d66c",
dessert: "photo-1551024506-0bccd828d307",
} as const;
const img = (p: keyof typeof FOOD_PHOTOS) =>
`https://images.unsplash.com/${FOOD_PHOTOS[p]}?w=240&h=240&fit=crop&q=70&auto=format`;
const fmt = (n: number, currency = "$") =>
`${currency}${n.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
const DEFAULT_ITEMS: FoodCartItem[] = [
{ id: "1", name: "Margherita Verace", notes: "Extra basil", price: 14.5, quantity: 1, imageUrl: img("pizza") },
{ id: "2", name: "Spicy Salmon Roll (8 pc)", notes: "No wasabi", price: 16.0, quantity: 2, imageUrl: img("sushi") },
{ id: "3", name: "Tiramisù della Casa", price: 8.5, quantity: 1, imageUrl: img("dessert") },
];
function SummaryRow({
label,
value,
bold,
accent,
}: {
label: string;
value: string;
bold?: boolean;
/** Success-green value — used for the promo discount. */
accent?: boolean;
}) {
return (
<View className="flex-row items-center justify-between py-1">
<Text variant={bold ? "large" : "muted"}>{label}</Text>
<Text
variant={bold ? "large" : "small"}
className={accent ? "font-semibold tabular-nums text-success" : "tabular-nums"}
>
{value}
</Text>
</View>
);
}
SummaryRow.displayName = "SummaryRow";
/** FoodCart — order review for delivery: fulfillment switch, dish rows
* with steppers and notes, address card, utensils toggle, promo,
* fee summary, sticky place-order bar. */
export function FoodCartScreen({
items = DEFAULT_ITEMS,
restaurant = "Nonna Rosa",
address = "128 Linden St, Apt 4B",
currency = "$",
deliveryFee = 1.99,
serviceFee = 2.74,
freeDeliveryThreshold = 40,
fulfillment = "Delivery",
onFulfillmentChange,
onUpdateQty,
onRemove,
onApplyPromo,
onBack,
onPlaceOrder,
onChangeAddress,
onBrowse,
}: FoodCartScreenProps) {
const haptics = useHaptic();
const insets = useSafeAreaInsets();
const [mode, setMode] = useState<"Delivery" | "Pickup">(fulfillment);
const [utensils, setUtensils] = useState(false);
const [promo, setPromo] = useState("");
const [appliedPromo, setAppliedPromo] = useState<string | null>(null);
const subtotal = items.reduce((sum, i) => sum + i.price * i.quantity, 0);
const delivery =
mode === "Pickup" || items.length === 0 || subtotal >= freeDeliveryThreshold
? 0
: deliveryFee;
const discount = appliedPromo ? Math.round(subtotal * 0.1 * 100) / 100 : 0;
const total = subtotal - discount + delivery + (items.length === 0 ? 0 : serviceFee);
const itemCount = items.reduce((sum, i) => sum + i.quantity, 0);
return (
<World primary="#65A30D">
<SafeAreaView className="flex-1 bg-[#F4F5F9] dark:bg-[#111114]" edges={["top"]}>
<View pointerEvents="box-none" className="absolute inset-x-4 z-10" style={{ top: insets.top + 8 }}>
<View className="flex-row items-center justify-between">
<Glass interactive effect="regular" className="h-11 w-11 items-center justify-center rounded-full">
<Press
haptic="light"
onPress={() => onBack?.()}
accessibilityRole="button"
accessibilityLabel="Go back"
className="h-9 w-9 items-center justify-center"
>
<Icon name="chevron-left" size="md" color="foreground" />
</Press>
</Glass>
<View className="h-11 w-11" />
</View>
<View pointerEvents="none" className="absolute inset-0 items-center justify-center">
<View className="h-11 flex-row items-center justify-center gap-2 rounded-full pl-3.5 pr-5">
<Glass effect="regular" className="absolute inset-0 rounded-full" />
<LumioMark size={18} />
<Text accessibilityRole="header" className="text-sm font-bold tracking-tight">
Your order{itemCount > 0 ? ` · ${itemCount}` : ""}
</Text>
</View>
</View>
</View>
<View className="px-6 pb-2" style={{ paddingTop: insets.top + 60 }}>
{/* Threshold nudge — the line that drives basket size. */}
<Text variant="muted" className="pb-1 text-xs">
{restaurant}
{subtotal < freeDeliveryThreshold && items.length > 0
? ` · $${(freeDeliveryThreshold - subtotal).toFixed(2)} away from free delivery`
: ""}
</Text>
</View>
<View className="px-6 pb-4">
<SegmentedControl
options={["Delivery", "Pickup"]}
value={mode}
onValueChange={(v) => {
const next = v as "Delivery" | "Pickup";
setMode(next);
onFulfillmentChange?.(next);
}}
accessibilityLabel="Fulfillment method"
/>
</View>
{items.length === 0 ? (
<View className="flex-1">
<EmptyState
title="Your cart is empty"
description="Dishes you add will appear here."
action={
onBrowse ? (
<Button variant="secondary" onPress={onBrowse}>
Browse restaurants
</Button>
) : undefined
}
className="flex-1"
/>
</View>
) : (
<>
<FlatList
data={items}
keyExtractor={(item) => item.id}
className="flex-1"
contentContainerClassName="px-6 pb-44"
contentInsetAdjustmentBehavior="automatic"
showsVerticalScrollIndicator={false}
ListHeaderComponent={
mode === "Delivery" ? (
<Card variant="elevated" className="mb-4 rounded-3xl bg-white dark:bg-[#1A1A20]">
<Card.Content className="flex-row items-center gap-3 p-4">
<View className="h-9 w-9 items-center justify-center rounded-lg bg-[#EDF8C4]">
<Icon name="map-pin" size="sm" color="#65A30D" />
</View>
<View className="flex-1">
<Text variant="small" className="font-semibold">
{address}
</Text>
<Text variant="muted" className="text-xs">
Leave at door
</Text>
</View>
{onChangeAddress ? (
<Pressable
onPress={() => {
haptics.light();
onChangeAddress();
}}
accessibilityRole="button"
accessibilityLabel="Change delivery address"
hitSlop={12}
className="min-h-11 justify-center"
>
<Text variant="small" className="font-medium text-[#65A30D]">
Change
</Text>
</Pressable>
) : null}
</Card.Content>
</Card>
) : null
}
ItemSeparatorComponent={() => <View className="h-5" />}
renderItem={({ item, index }) => (
<Mount delay={Math.min(index * ROW_STAGGER, ROW_STAGGER_CAP)}>
<View className="flex-row gap-4">
<Image
source={item.imageUrl ? { uri: item.imageUrl } : undefined}
rounded="lg"
className="h-16 w-16"
accessibilityLabel={`Photo of ${item.name}`}
/>
<View className="flex-1">
<View className="flex-row items-start justify-between">
<View className="flex-1 gap-0.5 pr-2">
<Text variant="small" numberOfLines={1}>
{item.name}
</Text>
{item.notes ? (
<Text variant="muted" className="text-xs" numberOfLines={1}>
{item.notes}
</Text>
) : null}
</View>
<Pressable
onPress={() => {
haptics.warning();
onRemove?.(item);
}}
accessibilityRole="button"
accessibilityLabel={`Remove ${item.name} from order`}
hitSlop={12}
className="h-11 w-11 items-center justify-center"
>
<Icon name="trash" size="sm" color="mutedForeground" />
</Pressable>
</View>
<View className="flex-row items-center justify-between pt-1.5">
<Stepper
value={item.quantity}
min={0}
onValueChange={(qty) =>
qty === 0 ? onRemove?.(item) : onUpdateQty?.(item, qty)
}
accessibilityLabel={`Quantity for ${item.name}`}
className="border-transparent bg-muted/60"
/>
<Text
variant="small"
className="font-semibold tabular-nums"
accessibilityLabel={`${item.name} line total ${fmt(item.price * item.quantity, currency)}`}
>
{fmt(item.price * item.quantity, currency)}
</Text>
</View>
</View>
</View>
</Mount>
)}
ListFooterComponent={
<View className="gap-4 pt-4">
<Checkbox
checked={utensils}
onCheckedChange={setUtensils}
label="Include utensils & napkins"
icon="utensils"
accessibilityLabel="Include utensils and napkins"
/>
{appliedPromo ? (
<View className="flex-row items-center justify-between rounded-2xl bg-success/10 py-1 pl-4">
<View className="flex-1 flex-row items-center gap-2">
<Icon name="check-circle" size="sm" color="success" />
<Text variant="small">
Promo{" "}
<Text variant="small" className="font-semibold">
{appliedPromo}
</Text>{" "}
applied — 10% off
</Text>
</View>
<Pressable
onPress={() => {
haptics.light();
setAppliedPromo(null);
setPromo("");
}}
accessibilityRole="button"
accessibilityLabel={`Remove promo code ${appliedPromo}`}
hitSlop={12}
className="h-11 w-11 items-center justify-center"
>
<Icon name="x" size="sm" color="mutedForeground" />
</Pressable>
</View>
) : (
<View className="flex-row gap-2">
<View className="flex-1">
<Input
placeholder="Promo code"
value={promo}
onChangeText={setPromo}
autoCapitalize="characters"
accessibilityLabel="Promo code"
/>
</View>
<Button
variant="secondary"
disabled={promo.trim().length === 0}
onPress={() => {
const code = promo.trim();
haptics.success();
setAppliedPromo(code);
onApplyPromo?.(code);
}}
>
Apply
</Button>
</View>
)}
<Card variant="elevated" className="gap-0 rounded-3xl bg-white p-4 dark:bg-[#1A1A20]">
<SummaryRow label="Subtotal" value={fmt(subtotal, currency)} />
{discount > 0 ? (
<SummaryRow
label={`Promo (${appliedPromo})`}
value={`-${fmt(discount, currency)}`}
accent
/>
) : null}
<SummaryRow
label={mode === "Pickup" ? "Pickup" : "Delivery"}
value={delivery === 0 ? "Free" : fmt(delivery, currency)}
/>
<SummaryRow label="Service fee" value={fmt(serviceFee, currency)} />
<Separator className="my-2" />
<SummaryRow label="Total" value={fmt(total, currency)} bold />
</Card>
</View>
}
/>
<StickyBottomBar>
<View className="rounded-3xl bg-white p-3 shadow-sm dark:bg-[#1A1A20]">
<Button
tone="inverse"
haptic="heavy"
onPress={() => onPlaceOrder?.()}
accessibilityLabel={`Place order, total ${fmt(total, currency)}`}
className="bg-[#1B1B26]"
>
Place order · {fmt(total, currency)}
</Button>
</View>
</StickyBottomBar>
</>
)}
</SafeAreaView>
</World>
);
}
FoodCartScreen.displayName = "FoodCartScreen";
```
Screen: https://lumioui.dev/screens/food-food-cart