Gamified habit home: streak card + week strip, XP milestone track, quest rows with check-ring payouts, gem pill
import * as React from "react";
import { useState } from "react";
import { ScrollView, View } from "react-native";
import { SafeAreaView } from "react-native-safe-area-context";
import { Avatar, Glass, Icon, Text } from "@lumioui/ui";
import { cn, useHaptic } from "@lumioui/core";
import { HeroGradient, Mount, Press, World } from "../shared";
interface Quest {
id: string;
emoji: string;
tile: string;
title: string;
detail: string;
xp: number;
gems?: number;
}
export interface HabitQuestScreenProps {
quests?: Quest[];
onToggleQuest?: (id: string, done: boolean) => void;
onAvatar?: () => void;
}
const QUESTS: Quest[] = [
{ id: "1", emoji: "🧴", tile: "bg-[#E1F0FF]", title: "Morning Skincare Routine", detail: "Cleanser, moisturizer and shampoo", xp: 25, gems: 2 },
{ id: "2", emoji: "💪", tile: "bg-[#FFE4D6]", title: "Push Day Workout", detail: "Complete today's push workout", xp: 30, gems: 3 },
{ id: "3", emoji: "📖", tile: "bg-[#F0E7FF]", title: "Read 10 Pages", detail: "Wind down with one chapter", xp: 20 },
{ id: "4", emoji: "🥗", tile: "bg-[#E2F6E4]", title: "Log Lunch", detail: "Keep the streak honest", xp: 15, gems: 1 },
{ id: "5", emoji: "🚶", tile: "bg-[#FFF3C9]", title: "Evening Walk", detail: "6,000 steps before dinner", xp: 25 },
];
const WEEK = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
const DATES = [9, 10, 11, 12, 13, 14, 15];
const DONE = 3; // Mon–Wed checked, Thu is today
const MILESTONES = [30, 60, 90, 120, 150];
const XP = 95;
/** Playful backdrop doodles — sparse, edge-parked, big enough to read as an
* illustrated world behind the frosted cards. */
const DOODLES = [
{ emoji: "🌳", cls: "-left-3 top-24", size: "text-[52px]" },
{ emoji: "☁️", cls: "right-6 top-16", size: "text-[44px]" },
{ emoji: "🏡", cls: "-right-2 top-56", size: "text-[48px]" },
{ emoji: "🦕", cls: "left-4 bottom-24", size: "text-[50px]" },
{ emoji: "⛅", cls: "right-16 bottom-12", size: "text-[40px]" },
] as const;
/** Habit quest home — gamified streak idiom: greeting + gem pill, a streak
* card with week strip, an XP level card with milestone track, and today's
* quests whose check rings pay out rewards. */
export function HabitQuestScreen({ quests = QUESTS, onToggleQuest, onAvatar }: HabitQuestScreenProps) {
const haptics = useHaptic();
const [done, setDone] = useState<Record<string, boolean>>({ "1": true });
const doneCount = quests.filter((q) => done[q.id]).length;
const toggle = (id: string) => {
haptics.selection();
const next = !done[id];
setDone((d) => ({ ...d, [id]: next }));
onToggleQuest?.(id, next);
};
return (
<World primary="#16A34A">
<View className="flex-1 bg-[#CDE9FA] dark:bg-[#0E1B26]">
<HeroGradient from="#A8D9F2" to="#E9F5FC" />
{DOODLES.map((d) => (
<View key={d.emoji} pointerEvents="none" className={cn("absolute", d.cls)}>
<Text className={d.size}>{d.emoji}</Text>
</View>
))}
<SafeAreaView className="flex-1" edges={["top"]}>
{/* Greeting + wallet — the currency pill is the app's purse. */}
<View className="flex-row items-center px-5 pb-1 pt-2">
<View>
<Text variant="muted" className="text-sm">Hi, James</Text>
<Text variant="h1" className="tracking-tight">Welcome Back</Text>
</View>
<View className="ml-auto flex-row items-center gap-2.5">
<View className="h-9 flex-row items-center gap-1.5 overflow-hidden rounded-full px-3">
<Glass effect="regular" sheen={0.5} className="absolute inset-0 rounded-full" pointerEvents="none" />
<Text className="text-sm">💎</Text>
<Text className="text-sm font-bold tabular-nums">235</Text>
</View>
<Press onPress={onAvatar} accessibilityRole="button" accessibilityLabel="Open profile" className="rounded-full">
<Avatar fallback="JB" size="md" />
</Press>
</View>
</View>
<ScrollView showsVerticalScrollIndicator={false} contentContainerClassName="gap-4 px-5 pb-10 pt-3">
{/* Current streak — the number IS the product. */}
<Mount className="overflow-hidden rounded-3xl p-5">
<Glass effect="regular" sheen={0.5} className="absolute inset-0 rounded-3xl" pointerEvents="none" />
<View className="flex-row items-start justify-between">
<View>
<Text variant="muted" className="text-xs font-semibold uppercase tracking-[0.14em]">
Current streak
</Text>
<Text className="mt-1 text-3xl font-bold tracking-tight">3 Days</Text>
</View>
<Text className="text-4xl">🏅</Text>
</View>
<View className="mt-4 flex-row justify-between">
{WEEK.map((d, i) => {
const checked = i < DONE;
const today = i === DONE;
return (
<View key={d} className="w-9 items-center gap-1.5">
<Text variant="muted" className="text-xs">{d}</Text>
{checked ? (
<View className="h-6 w-6 items-center justify-center rounded-full bg-[#22C55E]/20">
<Icon name="check" size="sm" color="primary" />
</View>
) : (
<Text
className={cn(
"h-6 text-base font-semibold leading-6 tabular-nums",
today ? "text-[#15803D]" : "text-muted-foreground"
)}
>
{DATES[i]}
</Text>
)}
{today ? <View className="h-1 w-1 rounded-full bg-[#15803D]" /> : <View className="h-1" />}
</View>
);
})}
</View>
</Mount>
{/* Level — milestone track with checkpoint badges. */}
<Mount delay={70} className="overflow-hidden rounded-3xl p-5">
<Glass effect="regular" tintColor="#D8F1DC" sheen={0.4} className="absolute inset-0 rounded-3xl" pointerEvents="none" />
<View className="flex-row items-center justify-between">
<View className="flex-row items-center gap-2">
<Text className="text-lg">👑</Text>
<Text className="text-lg font-bold">Level 4</Text>
</View>
<Text className="text-sm font-semibold tabular-nums text-[#15803D]">⚡ {XP}/150 XP</Text>
</View>
<View className="mt-6">
<View className="relative h-7 justify-center">
<View className="absolute inset-x-2 top-1/2 h-1.5 -translate-y-1/2 rounded-full bg-[#15803D]/15" />
<View
className="absolute left-2 top-1/2 h-1.5 -translate-y-1/2 rounded-full bg-[#22C55E]"
style={{ width: `${(XP / 150) * 96}%` }}
/>
<View className="flex-row justify-between">
{MILESTONES.map((m) => {
const reached = XP >= m;
return (
<View
key={m}
className={cn(
"h-7 w-7 items-center justify-center rounded-full",
reached ? "bg-[#22C55E]" : "bg-[#15803D]/10"
)}
>
{reached ? (
<Icon name="check" size="sm" color="primaryForeground" />
) : (
<Text className="text-xs">🏆</Text>
)}
</View>
);
})}
</View>
</View>
<View className="mt-1.5 flex-row justify-between">
{MILESTONES.map((m) => (
<Text key={m} variant="muted" className="w-7 text-center text-[10px] tabular-nums">
{m}
</Text>
))}
</View>
</View>
</Mount>
{/* Today's quest — rings pay out XP and gems on tap. */}
<Mount delay={140} className="overflow-hidden rounded-3xl">
<Glass effect="regular" sheen={0.5} className="absolute inset-0 rounded-3xl" pointerEvents="none" />
<View className="flex-row items-center justify-between p-5 pb-2">
<View>
<Text className="text-lg font-bold tracking-tight">Today's Quest</Text>
<Text variant="muted" className="mt-0.5 text-xs">Quests reset at midnight</Text>
</View>
<View className="rounded-full bg-[#15803D]/10 px-2.5 py-1">
<Text className="text-xs font-semibold tabular-nums text-[#15803D]">
{doneCount}/{quests.length} completed
</Text>
</View>
</View>
<View className="gap-1 px-3 pb-3">
{quests.map((q) => {
const checked = !!done[q.id];
return (
<Press
key={q.id}
onPress={() => toggle(q.id)}
accessibilityRole="button"
accessibilityLabel={`${q.title}, ${q.xp} XP${checked ? ", done" : ""}`}
accessibilityState={{ checked }}
className={cn("flex-row items-center gap-3 rounded-2xl p-3", checked && "bg-[#22C55E]/10")}
>
<View className={cn("h-11 w-11 items-center justify-center rounded-2xl", q.tile)}>
<Text className="text-xl">{q.emoji}</Text>
</View>
<View className="flex-1">
<Text className={cn("font-semibold", checked && "line-through opacity-60")}>{q.title}</Text>
<Text variant="muted" className="text-xs" numberOfLines={1}>
{q.detail}
</Text>
<View className="mt-1 flex-row items-center gap-2">
<Text className="text-xs font-semibold text-[#15803D]">⚡ +{q.xp} XP</Text>
{q.gems ? <Text className="text-xs font-semibold text-[#7C3AED]">💎 +{q.gems}</Text> : null}
</View>
</View>
<View
className={cn(
"h-7 w-7 items-center justify-center rounded-full border-2",
checked ? "border-[#22C55E] bg-[#22C55E]" : "border-[#15803D]/25 bg-transparent"
)}
>
{checked ? <Icon name="check" size="sm" color="primaryForeground" /> : null}
</View>
</Press>
);
})}
</View>
</Mount>
</ScrollView>
</SafeAreaView>
</View>
</World>
);
}
HabitQuestScreen.displayName = "HabitQuestScreen";
Recreate the "Habit Quest Screen" screen in React Native using LumioUI.
Goal: Gamified habit home: streak card + week strip, XP milestone track, quest rows with check-ring payouts, gem pill 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-habit-quest
```
Or pull just the components: `npx lumioui add avatar glass icon 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
### `avatar` — Circular user image with fallback initials and status dot.
- `<Avatar>` ×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>` ×4
### `icon` — Lucide icon set mapped to theme colors and sizes.
- `<Icon>` ×3
### `text` — Themed type scale from caption to display.
- `<Text>` ×23
## 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/habit-quest.tsx` exporting HabitQuestScreen. 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 } from "react-native-safe-area-context";
import { Avatar, Glass, Icon, Text } from "@lumioui/ui";
import { cn, useHaptic } from "@lumioui/core";
import { HeroGradient, Mount, Press, World } from "../shared";
interface Quest {
id: string;
emoji: string;
tile: string;
title: string;
detail: string;
xp: number;
gems?: number;
}
export interface HabitQuestScreenProps {
quests?: Quest[];
onToggleQuest?: (id: string, done: boolean) => void;
onAvatar?: () => void;
}
const QUESTS: Quest[] = [
{ id: "1", emoji: "🧴", tile: "bg-[#E1F0FF]", title: "Morning Skincare Routine", detail: "Cleanser, moisturizer and shampoo", xp: 25, gems: 2 },
{ id: "2", emoji: "💪", tile: "bg-[#FFE4D6]", title: "Push Day Workout", detail: "Complete today's push workout", xp: 30, gems: 3 },
{ id: "3", emoji: "📖", tile: "bg-[#F0E7FF]", title: "Read 10 Pages", detail: "Wind down with one chapter", xp: 20 },
{ id: "4", emoji: "🥗", tile: "bg-[#E2F6E4]", title: "Log Lunch", detail: "Keep the streak honest", xp: 15, gems: 1 },
{ id: "5", emoji: "🚶", tile: "bg-[#FFF3C9]", title: "Evening Walk", detail: "6,000 steps before dinner", xp: 25 },
];
const WEEK = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
const DATES = [9, 10, 11, 12, 13, 14, 15];
const DONE = 3; // Mon–Wed checked, Thu is today
const MILESTONES = [30, 60, 90, 120, 150];
const XP = 95;
/** Playful backdrop doodles — sparse, edge-parked, big enough to read as an
* illustrated world behind the frosted cards. */
const DOODLES = [
{ emoji: "🌳", cls: "-left-3 top-24", size: "text-[52px]" },
{ emoji: "☁️", cls: "right-6 top-16", size: "text-[44px]" },
{ emoji: "🏡", cls: "-right-2 top-56", size: "text-[48px]" },
{ emoji: "🦕", cls: "left-4 bottom-24", size: "text-[50px]" },
{ emoji: "⛅", cls: "right-16 bottom-12", size: "text-[40px]" },
] as const;
/** Habit quest home — gamified streak idiom: greeting + gem pill, a streak
* card with week strip, an XP level card with milestone track, and today's
* quests whose check rings pay out rewards. */
export function HabitQuestScreen({ quests = QUESTS, onToggleQuest, onAvatar }: HabitQuestScreenProps) {
const haptics = useHaptic();
const [done, setDone] = useState<Record<string, boolean>>({ "1": true });
const doneCount = quests.filter((q) => done[q.id]).length;
const toggle = (id: string) => {
haptics.selection();
const next = !done[id];
setDone((d) => ({ ...d, [id]: next }));
onToggleQuest?.(id, next);
};
return (
<World primary="#16A34A">
<View className="flex-1 bg-[#CDE9FA] dark:bg-[#0E1B26]">
<HeroGradient from="#A8D9F2" to="#E9F5FC" />
{DOODLES.map((d) => (
<View key={d.emoji} pointerEvents="none" className={cn("absolute", d.cls)}>
<Text className={d.size}>{d.emoji}</Text>
</View>
))}
<SafeAreaView className="flex-1" edges={["top"]}>
{/* Greeting + wallet — the currency pill is the app's purse. */}
<View className="flex-row items-center px-5 pb-1 pt-2">
<View>
<Text variant="muted" className="text-sm">Hi, James</Text>
<Text variant="h1" className="tracking-tight">Welcome Back</Text>
</View>
<View className="ml-auto flex-row items-center gap-2.5">
<View className="h-9 flex-row items-center gap-1.5 overflow-hidden rounded-full px-3">
<Glass effect="regular" sheen={0.5} className="absolute inset-0 rounded-full" pointerEvents="none" />
<Text className="text-sm">💎</Text>
<Text className="text-sm font-bold tabular-nums">235</Text>
</View>
<Press onPress={onAvatar} accessibilityRole="button" accessibilityLabel="Open profile" className="rounded-full">
<Avatar fallback="JB" size="md" />
</Press>
</View>
</View>
<ScrollView showsVerticalScrollIndicator={false} contentContainerClassName="gap-4 px-5 pb-10 pt-3">
{/* Current streak — the number IS the product. */}
<Mount className="overflow-hidden rounded-3xl p-5">
<Glass effect="regular" sheen={0.5} className="absolute inset-0 rounded-3xl" pointerEvents="none" />
<View className="flex-row items-start justify-between">
<View>
<Text variant="muted" className="text-xs font-semibold uppercase tracking-[0.14em]">
Current streak
</Text>
<Text className="mt-1 text-3xl font-bold tracking-tight">3 Days</Text>
</View>
<Text className="text-4xl">🏅</Text>
</View>
<View className="mt-4 flex-row justify-between">
{WEEK.map((d, i) => {
const checked = i < DONE;
const today = i === DONE;
return (
<View key={d} className="w-9 items-center gap-1.5">
<Text variant="muted" className="text-xs">{d}</Text>
{checked ? (
<View className="h-6 w-6 items-center justify-center rounded-full bg-[#22C55E]/20">
<Icon name="check" size="sm" color="primary" />
</View>
) : (
<Text
className={cn(
"h-6 text-base font-semibold leading-6 tabular-nums",
today ? "text-[#15803D]" : "text-muted-foreground"
)}
>
{DATES[i]}
</Text>
)}
{today ? <View className="h-1 w-1 rounded-full bg-[#15803D]" /> : <View className="h-1" />}
</View>
);
})}
</View>
</Mount>
{/* Level — milestone track with checkpoint badges. */}
<Mount delay={70} className="overflow-hidden rounded-3xl p-5">
<Glass effect="regular" tintColor="#D8F1DC" sheen={0.4} className="absolute inset-0 rounded-3xl" pointerEvents="none" />
<View className="flex-row items-center justify-between">
<View className="flex-row items-center gap-2">
<Text className="text-lg">👑</Text>
<Text className="text-lg font-bold">Level 4</Text>
</View>
<Text className="text-sm font-semibold tabular-nums text-[#15803D]">⚡ {XP}/150 XP</Text>
</View>
<View className="mt-6">
<View className="relative h-7 justify-center">
<View className="absolute inset-x-2 top-1/2 h-1.5 -translate-y-1/2 rounded-full bg-[#15803D]/15" />
<View
className="absolute left-2 top-1/2 h-1.5 -translate-y-1/2 rounded-full bg-[#22C55E]"
style={{ width: `${(XP / 150) * 96}%` }}
/>
<View className="flex-row justify-between">
{MILESTONES.map((m) => {
const reached = XP >= m;
return (
<View
key={m}
className={cn(
"h-7 w-7 items-center justify-center rounded-full",
reached ? "bg-[#22C55E]" : "bg-[#15803D]/10"
)}
>
{reached ? (
<Icon name="check" size="sm" color="primaryForeground" />
) : (
<Text className="text-xs">🏆</Text>
)}
</View>
);
})}
</View>
</View>
<View className="mt-1.5 flex-row justify-between">
{MILESTONES.map((m) => (
<Text key={m} variant="muted" className="w-7 text-center text-[10px] tabular-nums">
{m}
</Text>
))}
</View>
</View>
</Mount>
{/* Today's quest — rings pay out XP and gems on tap. */}
<Mount delay={140} className="overflow-hidden rounded-3xl">
<Glass effect="regular" sheen={0.5} className="absolute inset-0 rounded-3xl" pointerEvents="none" />
<View className="flex-row items-center justify-between p-5 pb-2">
<View>
<Text className="text-lg font-bold tracking-tight">Today's Quest</Text>
<Text variant="muted" className="mt-0.5 text-xs">Quests reset at midnight</Text>
</View>
<View className="rounded-full bg-[#15803D]/10 px-2.5 py-1">
<Text className="text-xs font-semibold tabular-nums text-[#15803D]">
{doneCount}/{quests.length} completed
</Text>
</View>
</View>
<View className="gap-1 px-3 pb-3">
{quests.map((q) => {
const checked = !!done[q.id];
return (
<Press
key={q.id}
onPress={() => toggle(q.id)}
accessibilityRole="button"
accessibilityLabel={`${q.title}, ${q.xp} XP${checked ? ", done" : ""}`}
accessibilityState={{ checked }}
className={cn("flex-row items-center gap-3 rounded-2xl p-3", checked && "bg-[#22C55E]/10")}
>
<View className={cn("h-11 w-11 items-center justify-center rounded-2xl", q.tile)}>
<Text className="text-xl">{q.emoji}</Text>
</View>
<View className="flex-1">
<Text className={cn("font-semibold", checked && "line-through opacity-60")}>{q.title}</Text>
<Text variant="muted" className="text-xs" numberOfLines={1}>
{q.detail}
</Text>
<View className="mt-1 flex-row items-center gap-2">
<Text className="text-xs font-semibold text-[#15803D]">⚡ +{q.xp} XP</Text>
{q.gems ? <Text className="text-xs font-semibold text-[#7C3AED]">💎 +{q.gems}</Text> : null}
</View>
</View>
<View
className={cn(
"h-7 w-7 items-center justify-center rounded-full border-2",
checked ? "border-[#22C55E] bg-[#22C55E]" : "border-[#15803D]/25 bg-transparent"
)}
>
{checked ? <Icon name="check" size="sm" color="primaryForeground" /> : null}
</View>
</Press>
);
})}
</View>
</Mount>
</ScrollView>
</SafeAreaView>
</View>
</World>
);
}
HabitQuestScreen.displayName = "HabitQuestScreen";
```
Screen: https://lumioui.dev/screens/health-habit-quest