Celebration beat: confetti drift, trophy glass disc, payout pills, milestone track filling toward next level
import * as React from "react";
import { View } from "react-native";
import { SafeAreaView } from "react-native-safe-area-context";
import { Button, Glass, Text } from "@lumioui/ui";
import { useHaptic } from "@lumioui/core";
import { Drift, HeroGradient, Mount, Press, SERIF, StickyBottomBar } from "../shared";
const CONFETTI = [
{ emoji: "🎉", cls: "-left-2 top-16", size: "text-[48px]", rotate: -14 },
{ emoji: "✨", cls: "right-6 top-10", size: "text-[40px]", rotate: 10 },
{ emoji: "💎", cls: "-right-3 top-44", size: "text-[52px]", rotate: 8 },
{ emoji: "⚡", cls: "left-4 top-72", size: "text-[44px]", rotate: -6 },
{ emoji: "🏅", cls: "right-14 top-[340px]", size: "text-[36px]", rotate: 14 },
{ emoji: "🎊", cls: "left-10 bottom-56", size: "text-[50px]", rotate: 0 },
{ emoji: "🪙", cls: "right-4 bottom-48", size: "text-[42px]", rotate: -12 },
] as const;
const XP_NOW = 135;
const XP_MAX = 150;
const MILESTONES = [30, 60, 90, 120, 150];
/** Quest complete — the celebration beat after a day's quest resolves:
* confetti drift over the sky world, trophy disc, payout pills, and the
* milestone track showing the XP this win added. Sibling to habit-quest. */
export function QuestCompleteScreen({
onClaim,
onShare,
}: {
onClaim?: () => void;
onShare?: () => void;
}) {
const haptics = useHaptic();
return (
<View className="flex-1 bg-[#A8D9F2]">
<HeroGradient from="#A8D9F2" to="#EAF6FD" />
{CONFETTI.map((o, i) => (
<Drift key={o.emoji} emoji={o.emoji} cls={o.cls} size={o.size} rotate={o.rotate} index={i} />
))}
<SafeAreaView className="flex-1" edges={["top", "bottom"]}>
<View className="flex-1 items-center justify-center gap-6 px-8">
<Mount className="items-center gap-6">
{/* Trophy disc — frosted glass over the confetti. */}
<View className="h-28 w-28 items-center justify-center overflow-hidden rounded-full">
<Glass effect="regular" sheen={0.6} className="absolute inset-0 rounded-full" pointerEvents="none" />
<View className="absolute inset-2 rounded-full border-2 border-white/50" pointerEvents="none" />
<Text className="text-[52px]">🏆</Text>
</View>
<View className="items-center">
<Text className="text-center text-[34px] font-bold leading-tight tracking-tight text-[#0B3B5C]">
Quest{" "}
<Text
style={{ fontFamily: SERIF, fontStyle: "italic" }}
className="text-[34px] font-bold text-[#0B3B5C]"
>
complete
</Text>
</Text>
<Text className="mt-2 text-center text-[15px] leading-6 text-[#0B3B5C]/60">
Morning run logged — you're on a{"\n"}4-day streak, James.
</Text>
</View>
{/* Payout pills. */}
<View className="flex-row gap-2">
{[
{ emoji: "⚡", label: "+40 XP" },
{ emoji: "💎", label: "+12" },
{ emoji: "🔥", label: "Day 4" },
].map((p) => (
<View key={p.label} className="overflow-hidden rounded-full">
<Glass effect="regular" className="absolute inset-0 rounded-full" pointerEvents="none" />
<View className="flex-row items-center gap-1.5 px-3.5 py-2">
<Text>{p.emoji}</Text>
<Text className="text-[13px] font-bold text-[#0B3B5C]">{p.label}</Text>
</View>
</View>
))}
</View>
</Mount>
{/* Milestone track — the XP just earned filling toward Level 5. */}
<Mount delay={140} className="w-full">
<View className="overflow-hidden rounded-3xl">
<Glass effect="regular" tintColor="#D8F1DC" className="absolute inset-0 rounded-3xl" pointerEvents="none" />
<View className="gap-3 p-4">
<View className="flex-row items-center justify-between">
<Text className="text-[13px] font-bold text-[#14532D]">👑 Level 5 — almost</Text>
<Text className="text-[12px] font-semibold text-[#14532D]/60">⚡ {XP_NOW}/{XP_MAX} XP</Text>
</View>
<View className="relative h-8 justify-center">
<View className="h-1.5 rounded-full bg-[#15803D]/15" />
<View
className="absolute h-1.5 rounded-full bg-[#22C55E]"
style={{ width: `${(XP_NOW / XP_MAX) * 96}%` }}
/>
<View className="absolute inset-x-0 flex-row justify-between">
{MILESTONES.map((m) => (
<View
key={m}
className="h-8 w-8 items-center justify-center rounded-full"
style={{
backgroundColor: m <= XP_NOW ? "#22C55E" : "#DCF5E3",
}}
>
<Text className="text-[11px]">{m <= XP_NOW ? "✓" : "🏆"}</Text>
</View>
))}
</View>
</View>
<Text className="text-[12px] text-[#14532D]/60">
15 XP to the 🏆 milestone — one more quest does it.
</Text>
</View>
</View>
</Mount>
</View>
<StickyBottomBar className="px-6">
<Button
size="lg"
className="w-full bg-[#0B3B5C]"
textClassName="text-white"
onPress={() => {
haptics.medium();
onClaim?.();
}}
accessibilityLabel="Claim rewards"
>
Claim rewards
</Button>
<Press
onPress={() => {
haptics.selection();
onShare?.();
}}
accessibilityRole="button"
accessibilityLabel="Share result"
className="mt-1 items-center py-2"
>
<Text className="text-[14px] font-semibold text-[#0B3B5C]/70">Share streak</Text>
</Press>
</StickyBottomBar>
</SafeAreaView>
</View>
);
}
QuestCompleteScreen.displayName = "QuestCompleteScreen";
Recreate the "Quest Complete Screen" screen in React Native using LumioUI.
Goal: Celebration beat: confetti drift, trophy glass disc, payout pills, milestone track filling toward next level 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 health-quest-complete
```
Or pull just the components: `npx lumioui add button glass 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>` ×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
### `text` — Themed type scale from caption to display.
- `<Text>` ×11
## 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/health/quest-complete.tsx` exporting QuestCompleteScreen. 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 { View } from "react-native";
import { SafeAreaView } from "react-native-safe-area-context";
import { Button, Glass, Text } from "@lumioui/ui";
import { useHaptic } from "@lumioui/core";
import { Drift, HeroGradient, Mount, Press, SERIF, StickyBottomBar } from "../shared";
const CONFETTI = [
{ emoji: "🎉", cls: "-left-2 top-16", size: "text-[48px]", rotate: -14 },
{ emoji: "✨", cls: "right-6 top-10", size: "text-[40px]", rotate: 10 },
{ emoji: "💎", cls: "-right-3 top-44", size: "text-[52px]", rotate: 8 },
{ emoji: "⚡", cls: "left-4 top-72", size: "text-[44px]", rotate: -6 },
{ emoji: "🏅", cls: "right-14 top-[340px]", size: "text-[36px]", rotate: 14 },
{ emoji: "🎊", cls: "left-10 bottom-56", size: "text-[50px]", rotate: 0 },
{ emoji: "🪙", cls: "right-4 bottom-48", size: "text-[42px]", rotate: -12 },
] as const;
const XP_NOW = 135;
const XP_MAX = 150;
const MILESTONES = [30, 60, 90, 120, 150];
/** Quest complete — the celebration beat after a day's quest resolves:
* confetti drift over the sky world, trophy disc, payout pills, and the
* milestone track showing the XP this win added. Sibling to habit-quest. */
export function QuestCompleteScreen({
onClaim,
onShare,
}: {
onClaim?: () => void;
onShare?: () => void;
}) {
const haptics = useHaptic();
return (
<View className="flex-1 bg-[#A8D9F2]">
<HeroGradient from="#A8D9F2" to="#EAF6FD" />
{CONFETTI.map((o, i) => (
<Drift key={o.emoji} emoji={o.emoji} cls={o.cls} size={o.size} rotate={o.rotate} index={i} />
))}
<SafeAreaView className="flex-1" edges={["top", "bottom"]}>
<View className="flex-1 items-center justify-center gap-6 px-8">
<Mount className="items-center gap-6">
{/* Trophy disc — frosted glass over the confetti. */}
<View className="h-28 w-28 items-center justify-center overflow-hidden rounded-full">
<Glass effect="regular" sheen={0.6} className="absolute inset-0 rounded-full" pointerEvents="none" />
<View className="absolute inset-2 rounded-full border-2 border-white/50" pointerEvents="none" />
<Text className="text-[52px]">🏆</Text>
</View>
<View className="items-center">
<Text className="text-center text-[34px] font-bold leading-tight tracking-tight text-[#0B3B5C]">
Quest{" "}
<Text
style={{ fontFamily: SERIF, fontStyle: "italic" }}
className="text-[34px] font-bold text-[#0B3B5C]"
>
complete
</Text>
</Text>
<Text className="mt-2 text-center text-[15px] leading-6 text-[#0B3B5C]/60">
Morning run logged — you're on a{"\n"}4-day streak, James.
</Text>
</View>
{/* Payout pills. */}
<View className="flex-row gap-2">
{[
{ emoji: "⚡", label: "+40 XP" },
{ emoji: "💎", label: "+12" },
{ emoji: "🔥", label: "Day 4" },
].map((p) => (
<View key={p.label} className="overflow-hidden rounded-full">
<Glass effect="regular" className="absolute inset-0 rounded-full" pointerEvents="none" />
<View className="flex-row items-center gap-1.5 px-3.5 py-2">
<Text>{p.emoji}</Text>
<Text className="text-[13px] font-bold text-[#0B3B5C]">{p.label}</Text>
</View>
</View>
))}
</View>
</Mount>
{/* Milestone track — the XP just earned filling toward Level 5. */}
<Mount delay={140} className="w-full">
<View className="overflow-hidden rounded-3xl">
<Glass effect="regular" tintColor="#D8F1DC" className="absolute inset-0 rounded-3xl" pointerEvents="none" />
<View className="gap-3 p-4">
<View className="flex-row items-center justify-between">
<Text className="text-[13px] font-bold text-[#14532D]">👑 Level 5 — almost</Text>
<Text className="text-[12px] font-semibold text-[#14532D]/60">⚡ {XP_NOW}/{XP_MAX} XP</Text>
</View>
<View className="relative h-8 justify-center">
<View className="h-1.5 rounded-full bg-[#15803D]/15" />
<View
className="absolute h-1.5 rounded-full bg-[#22C55E]"
style={{ width: `${(XP_NOW / XP_MAX) * 96}%` }}
/>
<View className="absolute inset-x-0 flex-row justify-between">
{MILESTONES.map((m) => (
<View
key={m}
className="h-8 w-8 items-center justify-center rounded-full"
style={{
backgroundColor: m <= XP_NOW ? "#22C55E" : "#DCF5E3",
}}
>
<Text className="text-[11px]">{m <= XP_NOW ? "✓" : "🏆"}</Text>
</View>
))}
</View>
</View>
<Text className="text-[12px] text-[#14532D]/60">
15 XP to the 🏆 milestone — one more quest does it.
</Text>
</View>
</View>
</Mount>
</View>
<StickyBottomBar className="px-6">
<Button
size="lg"
className="w-full bg-[#0B3B5C]"
textClassName="text-white"
onPress={() => {
haptics.medium();
onClaim?.();
}}
accessibilityLabel="Claim rewards"
>
Claim rewards
</Button>
<Press
onPress={() => {
haptics.selection();
onShare?.();
}}
accessibilityRole="button"
accessibilityLabel="Share result"
className="mt-1 items-center py-2"
>
<Text className="text-[14px] font-semibold text-[#0B3B5C]/70">Share streak</Text>
</Press>
</StickyBottomBar>
</SafeAreaView>
</View>
);
}
QuestCompleteScreen.displayName = "QuestCompleteScreen";
```
Screen: https://lumioui.dev/screens/health-quest-complete