Activity rings summary and today's workouts
import * as React from "react";
import { ScrollView, View } from "react-native";
import { SafeAreaView, useSafeAreaInsets } from "react-native-safe-area-context";
import { Avatar, Button, Card, Fab, Glass, Icon, type IconName, ProgressRing, Text } from "@lumioui/ui";
import { Mount, World } from "../shared";
import { useHaptic } from "@lumioui/core";
export interface WorkoutCard {
id: string;
name: string;
detail: string;
duration: string;
icon: IconName;
}
export interface FitnessHomeScreenProps {
name?: string;
avatarUrl?: string;
/** "Move 62% · Exercise 80% · Stand 50%" */
ringsSummary?: string;
streak?: number;
workouts?: WorkoutCard[];
onWorkoutPress?: (id: string) => void;
onRingsPress?: () => void;
}
const DEFAULT_WORKOUTS: WorkoutCard[] = [
{ id: "w1", name: "Morning run", detail: "Zone 2 · easy pace", duration: "32 min", icon: "footprints" },
{ id: "w2", name: "Upper body", detail: "Push · pull · core", duration: "45 min", icon: "dumbbell" },
{ id: "w3", name: "Evening walk", detail: "Recovery", duration: "20 min", icon: "clock" },
];
/** Per-workout soft tint — cycled so custom `workouts` lists stay varied. */
const WORKOUT_TILES = [
{ bg: "bg-warning/15", fg: "warning" },
{ bg: "bg-chart-3/15", fg: "chart3" },
{ bg: "bg-chart-2/15", fg: "chart2" },
] as const;
/** Fitness home — ring summary card, streak, today's workouts. */
export function FitnessHomeScreen({
name = "Amara",
avatarUrl = "https://i.pravatar.cc/150?img=47",
ringsSummary = "Move 62% · Exercise 80% · Stand 50%",
streak = 12,
workouts = DEFAULT_WORKOUTS,
onWorkoutPress,
onRingsPress,
}: FitnessHomeScreenProps) {
const haptics = useHaptic();
const insets = useSafeAreaInsets();
return (
<World primary="#16A34A">
<SafeAreaView className="flex-1 bg-[#F4F5F9] dark:bg-[#111114]" edges={["top"]}>
<View className="flex-row items-center gap-3 px-4 pb-3 pt-2">
<Avatar size="md" source={{ uri: avatarUrl }} fallback={name} alt={name} />
<View className="flex-1">
<Text variant="small" className="text-muted-foreground">
Friday
</Text>
<Text className="text-xl font-bold">{name}</Text>
</View>
<View className="flex-row items-center gap-1">
<Icon name="flame" size="sm" color="warning" />
<View>
<Text className="text-sm font-semibold leading-tight">{streak}</Text>
{/* day-streak label — the number alone reads as decoration. */}
<Text variant="muted" className="text-[10px] leading-tight">
day streak
</Text>
</View>
</View>
</View>
<ScrollView
showsVerticalScrollIndicator={false}
contentContainerStyle={{ paddingBottom: insets.bottom + 32 }}
>
{/* Rings card — the one hero on a fitness home. */}
<Mount>
<Card variant="elevated"
onPress={() => {
haptics.light();
onRingsPress?.();
}}
haptic="light"
className="mx-4 mb-5 rounded-3xl bg-white dark:bg-[#1A1A20]"
>
<Card.Content className="flex-row items-center gap-4 p-5">
{/* Mini concentric rings — real arcs at the same pcts as the label. */}
{(() => {
const pcts = [...ringsSummary.matchAll(/(\d+)%/g)].map((m) => Number(m[1]));
const [move = 62, exercise = 80, stand = 50] = pcts;
return (
<ProgressRing value={move} size={80} strokeWidth={7} color="destructive" label="Move ring">
<ProgressRing value={exercise} size={56} strokeWidth={5} color="success" label="Exercise ring">
<ProgressRing value={stand} size={34} strokeWidth={4} color="primary" label="Stand ring" />
</ProgressRing>
</ProgressRing>
);
})()}
<View className="flex-1 gap-1">
<Text className="text-lg font-bold">Activity</Text>
<Text variant="small" className="text-muted-foreground">
{ringsSummary}
</Text>
</View>
<Icon name="chevron-right" size="sm" color="mutedForeground" />
</Card.Content>
</Card>
</Mount>
{/* Today's plan — the first workout earns a focus card with a real
Start button; the rest stay compact rows. */}
<Mount delay={60}>
<View className="px-4 pb-2">
<Text variant="h4">Today's focus</Text>
</View>
{workouts.map((w, wi) =>
wi === 0 ? (
<Card variant="elevated"
key={w.id}
haptic="light"
className="mx-4 mb-3 rounded-3xl bg-white dark:bg-[#1A1A20]"
>
<Card.Content className="gap-3 p-5">
<View className="flex-row items-center gap-3">
<View
className={`h-12 w-12 items-center justify-center rounded-2xl ${WORKOUT_TILES[wi % WORKOUT_TILES.length]!.bg}`}
>
<Icon name={w.icon} size="md" color={WORKOUT_TILES[wi % WORKOUT_TILES.length]!.fg} />
</View>
<View className="flex-1 gap-0.5">
<Text className="text-base font-semibold">{w.name}</Text>
<Text variant="small" className="text-muted-foreground">
{w.detail} · {w.duration}
</Text>
</View>
</View>
<Button
tone="inverse"
haptic="medium"
onPress={() => onWorkoutPress?.(w.id)}
accessibilityLabel={`Start ${w.name}`}
className="bg-[#1B1B26] w-full"
>
{`Start · ${w.duration}`}
</Button>
</Card.Content>
</Card>
) : (
<Card variant="elevated"
key={w.id}
onPress={() => onWorkoutPress?.(w.id)}
haptic="light"
className="mx-4 mb-3 rounded-3xl bg-white dark:bg-[#1A1A20]"
>
<Card.Content className="flex-row items-center gap-3 p-4">
<View
className={`h-11 w-11 items-center justify-center rounded-2xl ${WORKOUT_TILES[wi % WORKOUT_TILES.length]!.bg}`}
>
<Icon name={w.icon} size="sm" color={WORKOUT_TILES[wi % WORKOUT_TILES.length]!.fg} />
</View>
<View className="flex-1 gap-0.5">
<Text className="font-semibold">{w.name}</Text>
<Text variant="small" className="text-muted-foreground">
{w.detail}
</Text>
</View>
<View className="items-end">
<Text className="text-sm font-medium">{w.duration}</Text>
</View>
</Card.Content>
</Card>
)
)}
</Mount>
{/* Week rollup — closes out the page instead of trailing off. */}
<Mount delay={120}>
<View className="px-4 pb-2 pt-5">
<Text variant="h4">This week</Text>
</View>
<View className="mx-4 overflow-hidden rounded-3xl bg-white p-5 dark:bg-[#1A1A20]">
{/* 7-day minutes chart — the shape of the week at a glance. */}
<View className="flex-row items-end justify-between pb-4">
{[38, 62, 45, 0, 78, 90, 52].map((v, i) => (
<View key={i} className="items-center gap-1">
<View
className={`w-2.5 rounded-full ${i === 5 ? "bg-[#BEF04C]" : "bg-[#1B1B26]/15"}`}
style={{ height: 8 + (v / 100) * 44 }}
/>
<Text className="text-[10px] text-muted-foreground">
{"MTWTFSS"[i]}
</Text>
</View>
))}
</View>
<View className="flex-row border-t border-border pt-4">
{[
{ value: "5", label: "sessions" },
{ value: "312", label: "minutes" },
{ value: "1,840", label: "calories" },
].map((s) => (
<View key={s.label} className="flex-1 items-center gap-1">
<Text className="text-2xl font-bold">{s.value}</Text>
<Text className="text-xs text-muted-foreground">{s.label}</Text>
</View>
))}
</View>
</View>
</Mount>
</ScrollView>
{/* Quick-log FAB — a home that can't start anything floats. */}
<Fab
icon={<Icon name="plus" size="md" color="#1B1B26" />}
label="Log"
extended
haptic="light"
onPress={() => haptics.light()}
accessibilityLabel="Log a workout"
className="bg-[#BEF04C]"
/>
</SafeAreaView>
</World>
);
}
FitnessHomeScreen.displayName = "FitnessHomeScreen";
Recreate the "Fitness Home Screen" screen in React Native using LumioUI.
Goal: Activity rings summary and today's workouts 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-fitness-home
```
Or pull just the components: `npx lumioui add avatar button card fab icon progress-ring 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
### `button` — Tactile pill button with press spring, ink wash and async loading.
- `<Button>` ×1
### `card` — Compound surface — header, title, content and footer slots.
- `<Card>` ×3
### `fab` — Liquid-glass floating action button — glass disc or filled pill, extended label mode, auto-async loading swap, mount pop
- `<Fab>` ×1 — Quick-log FAB — a home that can't start anything floats.
### `icon` — Lucide icon set mapped to theme colors and sizes.
- `<Icon>` ×5
### `progress-ring` — Circular progress ring with animated stroke arc, center slot and indeterminate spin.
- `<ProgressRing>` ×3
### `text` — Themed type scale from caption to display.
- `<Text>` ×16
## 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/fitness-home.tsx` exporting FitnessHomeScreen. 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 { ScrollView, View } from "react-native";
import { SafeAreaView, useSafeAreaInsets } from "react-native-safe-area-context";
import { Avatar, Button, Card, Fab, Glass, Icon, type IconName, ProgressRing, Text } from "@lumioui/ui";
import { Mount, World } from "../shared";
import { useHaptic } from "@lumioui/core";
export interface WorkoutCard {
id: string;
name: string;
detail: string;
duration: string;
icon: IconName;
}
export interface FitnessHomeScreenProps {
name?: string;
avatarUrl?: string;
/** "Move 62% · Exercise 80% · Stand 50%" */
ringsSummary?: string;
streak?: number;
workouts?: WorkoutCard[];
onWorkoutPress?: (id: string) => void;
onRingsPress?: () => void;
}
const DEFAULT_WORKOUTS: WorkoutCard[] = [
{ id: "w1", name: "Morning run", detail: "Zone 2 · easy pace", duration: "32 min", icon: "footprints" },
{ id: "w2", name: "Upper body", detail: "Push · pull · core", duration: "45 min", icon: "dumbbell" },
{ id: "w3", name: "Evening walk", detail: "Recovery", duration: "20 min", icon: "clock" },
];
/** Per-workout soft tint — cycled so custom `workouts` lists stay varied. */
const WORKOUT_TILES = [
{ bg: "bg-warning/15", fg: "warning" },
{ bg: "bg-chart-3/15", fg: "chart3" },
{ bg: "bg-chart-2/15", fg: "chart2" },
] as const;
/** Fitness home — ring summary card, streak, today's workouts. */
export function FitnessHomeScreen({
name = "Amara",
avatarUrl = "https://i.pravatar.cc/150?img=47",
ringsSummary = "Move 62% · Exercise 80% · Stand 50%",
streak = 12,
workouts = DEFAULT_WORKOUTS,
onWorkoutPress,
onRingsPress,
}: FitnessHomeScreenProps) {
const haptics = useHaptic();
const insets = useSafeAreaInsets();
return (
<World primary="#16A34A">
<SafeAreaView className="flex-1 bg-[#F4F5F9] dark:bg-[#111114]" edges={["top"]}>
<View className="flex-row items-center gap-3 px-4 pb-3 pt-2">
<Avatar size="md" source={{ uri: avatarUrl }} fallback={name} alt={name} />
<View className="flex-1">
<Text variant="small" className="text-muted-foreground">
Friday
</Text>
<Text className="text-xl font-bold">{name}</Text>
</View>
<View className="flex-row items-center gap-1">
<Icon name="flame" size="sm" color="warning" />
<View>
<Text className="text-sm font-semibold leading-tight">{streak}</Text>
{/* day-streak label — the number alone reads as decoration. */}
<Text variant="muted" className="text-[10px] leading-tight">
day streak
</Text>
</View>
</View>
</View>
<ScrollView
showsVerticalScrollIndicator={false}
contentContainerStyle={{ paddingBottom: insets.bottom + 32 }}
>
{/* Rings card — the one hero on a fitness home. */}
<Mount>
<Card variant="elevated"
onPress={() => {
haptics.light();
onRingsPress?.();
}}
haptic="light"
className="mx-4 mb-5 rounded-3xl bg-white dark:bg-[#1A1A20]"
>
<Card.Content className="flex-row items-center gap-4 p-5">
{/* Mini concentric rings — real arcs at the same pcts as the label. */}
{(() => {
const pcts = [...ringsSummary.matchAll(/(\d+)%/g)].map((m) => Number(m[1]));
const [move = 62, exercise = 80, stand = 50] = pcts;
return (
<ProgressRing value={move} size={80} strokeWidth={7} color="destructive" label="Move ring">
<ProgressRing value={exercise} size={56} strokeWidth={5} color="success" label="Exercise ring">
<ProgressRing value={stand} size={34} strokeWidth={4} color="primary" label="Stand ring" />
</ProgressRing>
</ProgressRing>
);
})()}
<View className="flex-1 gap-1">
<Text className="text-lg font-bold">Activity</Text>
<Text variant="small" className="text-muted-foreground">
{ringsSummary}
</Text>
</View>
<Icon name="chevron-right" size="sm" color="mutedForeground" />
</Card.Content>
</Card>
</Mount>
{/* Today's plan — the first workout earns a focus card with a real
Start button; the rest stay compact rows. */}
<Mount delay={60}>
<View className="px-4 pb-2">
<Text variant="h4">Today's focus</Text>
</View>
{workouts.map((w, wi) =>
wi === 0 ? (
<Card variant="elevated"
key={w.id}
haptic="light"
className="mx-4 mb-3 rounded-3xl bg-white dark:bg-[#1A1A20]"
>
<Card.Content className="gap-3 p-5">
<View className="flex-row items-center gap-3">
<View
className={`h-12 w-12 items-center justify-center rounded-2xl ${WORKOUT_TILES[wi % WORKOUT_TILES.length]!.bg}`}
>
<Icon name={w.icon} size="md" color={WORKOUT_TILES[wi % WORKOUT_TILES.length]!.fg} />
</View>
<View className="flex-1 gap-0.5">
<Text className="text-base font-semibold">{w.name}</Text>
<Text variant="small" className="text-muted-foreground">
{w.detail} · {w.duration}
</Text>
</View>
</View>
<Button
tone="inverse"
haptic="medium"
onPress={() => onWorkoutPress?.(w.id)}
accessibilityLabel={`Start ${w.name}`}
className="bg-[#1B1B26] w-full"
>
{`Start · ${w.duration}`}
</Button>
</Card.Content>
</Card>
) : (
<Card variant="elevated"
key={w.id}
onPress={() => onWorkoutPress?.(w.id)}
haptic="light"
className="mx-4 mb-3 rounded-3xl bg-white dark:bg-[#1A1A20]"
>
<Card.Content className="flex-row items-center gap-3 p-4">
<View
className={`h-11 w-11 items-center justify-center rounded-2xl ${WORKOUT_TILES[wi % WORKOUT_TILES.length]!.bg}`}
>
<Icon name={w.icon} size="sm" color={WORKOUT_TILES[wi % WORKOUT_TILES.length]!.fg} />
</View>
<View className="flex-1 gap-0.5">
<Text className="font-semibold">{w.name}</Text>
<Text variant="small" className="text-muted-foreground">
{w.detail}
</Text>
</View>
<View className="items-end">
<Text className="text-sm font-medium">{w.duration}</Text>
</View>
</Card.Content>
</Card>
)
)}
</Mount>
{/* Week rollup — closes out the page instead of trailing off. */}
<Mount delay={120}>
<View className="px-4 pb-2 pt-5">
<Text variant="h4">This week</Text>
</View>
<View className="mx-4 overflow-hidden rounded-3xl bg-white p-5 dark:bg-[#1A1A20]">
{/* 7-day minutes chart — the shape of the week at a glance. */}
<View className="flex-row items-end justify-between pb-4">
{[38, 62, 45, 0, 78, 90, 52].map((v, i) => (
<View key={i} className="items-center gap-1">
<View
className={`w-2.5 rounded-full ${i === 5 ? "bg-[#BEF04C]" : "bg-[#1B1B26]/15"}`}
style={{ height: 8 + (v / 100) * 44 }}
/>
<Text className="text-[10px] text-muted-foreground">
{"MTWTFSS"[i]}
</Text>
</View>
))}
</View>
<View className="flex-row border-t border-border pt-4">
{[
{ value: "5", label: "sessions" },
{ value: "312", label: "minutes" },
{ value: "1,840", label: "calories" },
].map((s) => (
<View key={s.label} className="flex-1 items-center gap-1">
<Text className="text-2xl font-bold">{s.value}</Text>
<Text className="text-xs text-muted-foreground">{s.label}</Text>
</View>
))}
</View>
</View>
</Mount>
</ScrollView>
{/* Quick-log FAB — a home that can't start anything floats. */}
<Fab
icon={<Icon name="plus" size="md" color="#1B1B26" />}
label="Log"
extended
haptic="light"
onPress={() => haptics.light()}
accessibilityLabel="Log a workout"
className="bg-[#BEF04C]"
/>
</SafeAreaView>
</World>
);
}
FitnessHomeScreen.displayName = "FitnessHomeScreen";
```
Screen: https://lumioui.dev/screens/health-fitness-home