v0.1 public beta early access on GitHub
Home / Screens / health-fitness-home
Screen template

Fitness Home Screen

Activity rings summary and today's workouts

pro Expo Go ✓ iosandroid gestures: tap · scroll haptics: light · selection
$ npx lumioui add health-fitness-home
import { FitnessHomeScreen } from "@/screens/health/fitness-home";
Native packages installed6
@lumioui/ui@latest@lumioui/core@latestreact-native-safe-area-context@*react-native-reanimated@*react-native-svg@*nativewind@*
health/fitness-home.tsxsource · yours to edit
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&apos;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";
agent prompt — paste into Claude Code, Cursor, Devin…
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&apos;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
iPhone 17 · iOS
9:41
Live preview — the real screen via Expo Web · open full ↗