Dark map itinerary: glowing route, day pills, per-day stop list, navigate CTA
import * as React from "react";
import { useState } from "react";
import { View } from "react-native";
import { SafeAreaView, useSafeAreaInsets } from "react-native-safe-area-context";
import { Button, Chip, Glass, Icon, Text, type IconName } from "@lumioui/ui";
import { useHaptic } from "@lumioui/core";
import { GlowSpot, Mount, Press, World } from "../shared";
import Svg, { Circle, Path } from "react-native-svg";
export interface TripDay {
day: number;
title: string;
stops: { time: string; name: string; icon: IconName }[];
}
export interface MapDayScreenProps {
tripName?: string;
days?: TripDay[];
onBack?: () => void;
onNavigate?: (day: TripDay) => void;
}
const LIME = "#A3E635";
const DAYS: [TripDay, ...TripDay[]] = [
{
day: 1,
title: "Old town walk",
stops: [
{ time: "9:00", name: "Kiyomizu-dera", icon: "map-pin" },
{ time: "12:30", name: "Nishiki Market", icon: "utensils" },
{ time: "15:00", name: "Gion Corner", icon: "star" },
],
},
{
day: 2,
title: "Arashiyama",
stops: [
{ time: "8:30", name: "Bamboo Grove", icon: "map-pin" },
{ time: "11:00", name: "Tenryū-ji", icon: "star" },
{ time: "16:00", name: "Monkey Park", icon: "map-pin" },
],
},
{
day: 3,
title: "Fushimi Inari",
stops: [
{ time: "7:00", name: "Torii gates", icon: "map-pin" },
{ time: "13:00", name: "Sake District", icon: "coffee" },
],
},
{
day: 4,
title: "Day trip · Nara",
stops: [
{ time: "9:30", name: "Tōdai-ji", icon: "star" },
{ time: "12:00", name: "Deer Park", icon: "map-pin" },
],
},
];
/** Map day — dark map idiom (Trip Details reference): near-black canvas,
* a glowing route polyline with day pins, floating chrome, day pills that
* swap the stop list, and a Navigate CTA. */
export function MapDayScreen({
tripName = "Kyoto, Japan",
days = DAYS,
onBack,
onNavigate,
}: MapDayScreenProps) {
const haptics = useHaptic();
const insets = useSafeAreaInsets();
const [day, setDay] = useState(days[0] ?? DAYS[0]);
return (
<World primary="#65A30D">
<View className="flex-1 bg-neutral-950">
<GlowSpot color="#BEF04C" size={300} className="-top-20 right-[-80px]" />
<GlowSpot color="#BEF04C" size={220} className="bottom-[30%] left-[-100px]" />
{/* Faux map — dark ground, faint grid, glowing route. */}
<Svg width="100%" height="100%" style={{ position: "absolute" }}>
{[80, 160, 240, 320, 400, 480, 560].map((y) => (
<Path key={y} d={`M0 ${y} H400`} stroke="#ffffff" strokeOpacity={0.04} />
))}
{[60, 120, 180, 240, 300, 360].map((x) => (
<Path key={x} d={`M${x} 0 V800`} stroke="#ffffff" strokeOpacity={0.04} />
))}
<Path
d="M60 620 Q120 520 110 440 T190 320 T270 210 T330 110"
stroke={LIME}
strokeWidth={2.5}
strokeDasharray="1 7"
strokeLinecap="round"
fill="none"
/>
{[
{ x: 60, y: 620 },
{ x: 110, y: 440 },
{ x: 190, y: 320 },
{ x: 270, y: 210 },
{ x: 330, y: 110 },
].map((p, i) => (
<React.Fragment key={i}>
<Circle cx={p.x} cy={p.y} r={i === 0 || i === 4 ? 7 : 4} fill={LIME} />
{i === 0 || i === 4 ? (
<Circle cx={p.x} cy={p.y} r={13} stroke={LIME} strokeOpacity={0.3} fill="none" />
) : null}
</React.Fragment>
))}
</Svg>
<SafeAreaView edges={["top"]} className="flex-1">
{/* Floating chrome. */}
<Mount className="flex-row items-center justify-between px-4 pt-2">
<Glass interactive effect="clear" className="rounded-full">
<Press
onPress={() => {
haptics.light();
onBack?.();
}}
accessibilityLabel="Go back"
accessibilityRole="button"
className="h-10 w-10 items-center justify-center rounded-full"
>
<Icon name="chevron-left" size="md" color="#fff" />
</Press>
</Glass>
<Glass interactive effect="clear" className="rounded-full px-4 py-2">
<Text className="text-sm font-semibold text-white">{tripName}</Text>
{/* Day + weather — map days anchor in place and sky. */}
<Text className="text-center text-[10px] text-white/75">
Day 3 · 5 stops · 22°
</Text>
</Glass>
<Glass interactive effect="clear" className="rounded-full">
<Press
onPress={() => haptics.light()}
accessibilityLabel="Trip options"
accessibilityRole="button"
className="h-10 w-10 items-center justify-center rounded-full"
>
<Icon name="ellipsis" size="md" color="#fff" />
</Press>
</Glass>
</Mount>
<View className="flex-1" />
{/* Day pills. */}
<Mount delay={60} className="flex-row justify-center gap-2 px-4">
{days.map((d) => {
const active = d.day === day.day;
return (
<Chip
key={d.day}
variant="filter"
selected={active}
onSelectedChange={() => setDay(d)}
accessibilityLabel={`Day ${d.day}`}
className="border-0 bg-black/55 px-4 py-2"
selectedClassName="bg-[#A3E635]"
textClassName="text-white/80"
selectedTextClassName="text-black"
>
Day {d.day}
</Chip>
);
})}
</Mount>
{/* Stop card for the selected day. */}
<Mount delay={120}
className="mx-4 mt-3 rounded-3xl bg-black/70 p-5"
style={{ marginBottom: insets.bottom + 12 }}
>
<Text className="text-base font-semibold text-white">{day.title}</Text>
<View className="mt-3 gap-3">
{day.stops.map((s) => (
<View key={s.name} className="flex-row items-center gap-3">
<View className="w-10">
<Text className="text-xs text-white/50">{s.time}</Text>
</View>
<View
className="h-7 w-7 items-center justify-center rounded-full"
style={{ backgroundColor: `${LIME}26` }}
>
<Icon name={s.icon} size="sm" color={LIME} />
</View>
<Text className="flex-1 text-sm text-white">{s.name}</Text>
</View>
))}
</View>
<Button
size="lg"
haptic="medium"
onPress={() => {
haptics.medium();
onNavigate?.(day);
}}
accessibilityLabel={`Navigate day ${day.day}`}
className="mt-4 w-full rounded-2xl bg-[#A3E635]"
textClassName="text-black font-semibold"
>
Navigate
</Button>
</Mount>
</SafeAreaView>
</View>
</World>
);
}
MapDayScreen.displayName = "MapDayScreen";
Recreate the "Trip — Map Day Screen" screen in React Native using LumioUI.
Goal: Dark map itinerary: glowing route, day pills, per-day stop list, navigate CTA 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 travel-map-day
```
Or pull just the components: `npx lumioui add button chip glass icon text`
Native packages:
- @lumioui/ui@latest
- @lumioui/core@latest
- react-native-safe-area-context@*
- react-native-svg@*
- react-native-reanimated@*
- 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>` ×1
### `chip` — Compact selectable pill with press scale and ink overlay.
- `<Chip>` ×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>` ×3
### `icon` — Lucide icon set mapped to theme colors and sizes.
- `<Icon>` ×3
### `text` — Themed type scale from caption to display.
- `<Text>` ×5
## 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/travel/map-day.tsx` exporting MapDayScreen. 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 { View } from "react-native";
import { SafeAreaView, useSafeAreaInsets } from "react-native-safe-area-context";
import { Button, Chip, Glass, Icon, Text, type IconName } from "@lumioui/ui";
import { useHaptic } from "@lumioui/core";
import { GlowSpot, Mount, Press, World } from "../shared";
import Svg, { Circle, Path } from "react-native-svg";
export interface TripDay {
day: number;
title: string;
stops: { time: string; name: string; icon: IconName }[];
}
export interface MapDayScreenProps {
tripName?: string;
days?: TripDay[];
onBack?: () => void;
onNavigate?: (day: TripDay) => void;
}
const LIME = "#A3E635";
const DAYS: [TripDay, ...TripDay[]] = [
{
day: 1,
title: "Old town walk",
stops: [
{ time: "9:00", name: "Kiyomizu-dera", icon: "map-pin" },
{ time: "12:30", name: "Nishiki Market", icon: "utensils" },
{ time: "15:00", name: "Gion Corner", icon: "star" },
],
},
{
day: 2,
title: "Arashiyama",
stops: [
{ time: "8:30", name: "Bamboo Grove", icon: "map-pin" },
{ time: "11:00", name: "Tenryū-ji", icon: "star" },
{ time: "16:00", name: "Monkey Park", icon: "map-pin" },
],
},
{
day: 3,
title: "Fushimi Inari",
stops: [
{ time: "7:00", name: "Torii gates", icon: "map-pin" },
{ time: "13:00", name: "Sake District", icon: "coffee" },
],
},
{
day: 4,
title: "Day trip · Nara",
stops: [
{ time: "9:30", name: "Tōdai-ji", icon: "star" },
{ time: "12:00", name: "Deer Park", icon: "map-pin" },
],
},
];
/** Map day — dark map idiom (Trip Details reference): near-black canvas,
* a glowing route polyline with day pins, floating chrome, day pills that
* swap the stop list, and a Navigate CTA. */
export function MapDayScreen({
tripName = "Kyoto, Japan",
days = DAYS,
onBack,
onNavigate,
}: MapDayScreenProps) {
const haptics = useHaptic();
const insets = useSafeAreaInsets();
const [day, setDay] = useState(days[0] ?? DAYS[0]);
return (
<World primary="#65A30D">
<View className="flex-1 bg-neutral-950">
<GlowSpot color="#BEF04C" size={300} className="-top-20 right-[-80px]" />
<GlowSpot color="#BEF04C" size={220} className="bottom-[30%] left-[-100px]" />
{/* Faux map — dark ground, faint grid, glowing route. */}
<Svg width="100%" height="100%" style={{ position: "absolute" }}>
{[80, 160, 240, 320, 400, 480, 560].map((y) => (
<Path key={y} d={`M0 ${y} H400`} stroke="#ffffff" strokeOpacity={0.04} />
))}
{[60, 120, 180, 240, 300, 360].map((x) => (
<Path key={x} d={`M${x} 0 V800`} stroke="#ffffff" strokeOpacity={0.04} />
))}
<Path
d="M60 620 Q120 520 110 440 T190 320 T270 210 T330 110"
stroke={LIME}
strokeWidth={2.5}
strokeDasharray="1 7"
strokeLinecap="round"
fill="none"
/>
{[
{ x: 60, y: 620 },
{ x: 110, y: 440 },
{ x: 190, y: 320 },
{ x: 270, y: 210 },
{ x: 330, y: 110 },
].map((p, i) => (
<React.Fragment key={i}>
<Circle cx={p.x} cy={p.y} r={i === 0 || i === 4 ? 7 : 4} fill={LIME} />
{i === 0 || i === 4 ? (
<Circle cx={p.x} cy={p.y} r={13} stroke={LIME} strokeOpacity={0.3} fill="none" />
) : null}
</React.Fragment>
))}
</Svg>
<SafeAreaView edges={["top"]} className="flex-1">
{/* Floating chrome. */}
<Mount className="flex-row items-center justify-between px-4 pt-2">
<Glass interactive effect="clear" className="rounded-full">
<Press
onPress={() => {
haptics.light();
onBack?.();
}}
accessibilityLabel="Go back"
accessibilityRole="button"
className="h-10 w-10 items-center justify-center rounded-full"
>
<Icon name="chevron-left" size="md" color="#fff" />
</Press>
</Glass>
<Glass interactive effect="clear" className="rounded-full px-4 py-2">
<Text className="text-sm font-semibold text-white">{tripName}</Text>
{/* Day + weather — map days anchor in place and sky. */}
<Text className="text-center text-[10px] text-white/75">
Day 3 · 5 stops · 22°
</Text>
</Glass>
<Glass interactive effect="clear" className="rounded-full">
<Press
onPress={() => haptics.light()}
accessibilityLabel="Trip options"
accessibilityRole="button"
className="h-10 w-10 items-center justify-center rounded-full"
>
<Icon name="ellipsis" size="md" color="#fff" />
</Press>
</Glass>
</Mount>
<View className="flex-1" />
{/* Day pills. */}
<Mount delay={60} className="flex-row justify-center gap-2 px-4">
{days.map((d) => {
const active = d.day === day.day;
return (
<Chip
key={d.day}
variant="filter"
selected={active}
onSelectedChange={() => setDay(d)}
accessibilityLabel={`Day ${d.day}`}
className="border-0 bg-black/55 px-4 py-2"
selectedClassName="bg-[#A3E635]"
textClassName="text-white/80"
selectedTextClassName="text-black"
>
Day {d.day}
</Chip>
);
})}
</Mount>
{/* Stop card for the selected day. */}
<Mount delay={120}
className="mx-4 mt-3 rounded-3xl bg-black/70 p-5"
style={{ marginBottom: insets.bottom + 12 }}
>
<Text className="text-base font-semibold text-white">{day.title}</Text>
<View className="mt-3 gap-3">
{day.stops.map((s) => (
<View key={s.name} className="flex-row items-center gap-3">
<View className="w-10">
<Text className="text-xs text-white/50">{s.time}</Text>
</View>
<View
className="h-7 w-7 items-center justify-center rounded-full"
style={{ backgroundColor: `${LIME}26` }}
>
<Icon name={s.icon} size="sm" color={LIME} />
</View>
<Text className="flex-1 text-sm text-white">{s.name}</Text>
</View>
))}
</View>
<Button
size="lg"
haptic="medium"
onPress={() => {
haptics.medium();
onNavigate?.(day);
}}
accessibilityLabel={`Navigate day ${day.day}`}
className="mt-4 w-full rounded-2xl bg-[#A3E635]"
textClassName="text-black font-semibold"
>
Navigate
</Button>
</Mount>
</SafeAreaView>
</View>
</World>
);
}
MapDayScreen.displayName = "MapDayScreen";
```
Screen: https://lumioui.dev/screens/travel-map-day