v0.1 public beta early access on GitHub
Home / Screens / finance-spending-analytics
Screen template

Spending Analytics Screen

Weekly spend bars and category share

pro Expo Go ✓ iosandroid gestures: tap · scroll haptics: light · selection
$ npx lumioui add finance-spending-analytics
import { SpendingAnalyticsScreen } from "@/screens/finance/spending-analytics";
Native packages installed6
@lumioui/ui@latest@lumioui/core@latestreact-native-safe-area-context@*react-native-reanimated@*react-native-svg@*nativewind@*
finance/spending-analytics.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 { Button, Glass, Icon, type IconName, List, Progress, SegmentedControl, Text } from "@lumioui/ui";
import { useHaptic, type ColorTokens } from "@lumioui/core";
import { BLOCK_STAGGER, LumioMark, Mount, Press, SEPARATOR_INSET } from "../shared";

export interface SpendBar {
  label: string;
  /** 0–1 height fraction. */
  value: number;
  current?: boolean;
}

export interface SpendCategory {
  id: string;
  name: string;
  icon: IconName;
  amount: string;
  /** Share of the period total, 0–1. */
  share: number;
  /** Soft-tint tile background class. */
  tileBg?: string;
  /** Icon tint token — pairs with `tileBg`. */
  tint?: keyof ColorTokens;
}

export interface SpendingAnalyticsScreenProps {
  period?: string;
  total?: string;
  bars?: SpendBar[];
  categories?: SpendCategory[];
  onPeriodChange?: (period: string) => void;
  onBack?: () => void;
}

const DEFAULT_BARS: SpendBar[] = [
  { label: "M", value: 0.35 },
  { label: "T", value: 0.55 },
  { label: "W", value: 0.4 },
  { label: "T", value: 0.75 },
  { label: "F", value: 0.9 },
  { label: "S", value: 0.65 },
  { label: "S", value: 0.5, current: true },
];

/* Amounts sum to the $1,806 total; shares are each amount over the total. */
const DEFAULT_CATEGORIES: SpendCategory[] = [
  { id: "s1", name: "Shopping", icon: "shopping-cart", amount: "$688", share: 0.38, tileBg: "bg-primary/15", tint: "primary" },
  { id: "s2", name: "Groceries", icon: "carrot", amount: "$514", share: 0.28, tileBg: "bg-success/15", tint: "success" },
  { id: "s3", name: "Dining", icon: "utensils", amount: "$342", share: 0.19, tileBg: "bg-warning/15", tint: "warning" },
  { id: "s4", name: "Transport", icon: "car", amount: "$146", share: 0.08, tileBg: "bg-destructive/15", tint: "destructive" },
  { id: "s5", name: "Subscriptions", icon: "repeat", amount: "$116", share: 0.06, tileBg: "bg-chart-5/15", tint: "chart5" },
];

const BAR_COLORS = ["bg-primary", "bg-success", "bg-warning", "bg-destructive"];

/** Spending analytics — week bars + per-category spend share. */
export function SpendingAnalyticsScreen({
  period = "Week",
  total = "$1,806",
  bars = DEFAULT_BARS,
  categories = DEFAULT_CATEGORIES,
  onPeriodChange,
  onBack,
}: SpendingAnalyticsScreenProps) {
  const haptics = useHaptic();
  const insets = useSafeAreaInsets();
  const [tab, setTab] = React.useState(period);

  return (
    <SafeAreaView className="flex-1 bg-[#F4F5F9] dark:bg-[#111114]" edges={["top"]}>
      <View pointerEvents="box-none" className="absolute inset-x-4 z-10" style={{ top: insets.top + 8 }}>
        <View className="flex-row items-center justify-between">
          <Glass interactive effect="regular" className="h-11 w-11 items-center justify-center rounded-full">
            <Press
              haptic="light"
              onPress={() => onBack?.()}
              accessibilityRole="button"
              accessibilityLabel="Go back"
              className="h-9 w-9 items-center justify-center"
            >
              <Icon name="chevron-left" size="md" color="foreground" />
            </Press>
          </Glass>
          <View className="h-11 w-11" />
        </View>
        <View pointerEvents="none" className="absolute inset-0 items-center justify-center">
          <View className="h-11 flex-row items-center justify-center gap-2 rounded-full pl-3.5 pr-5">
            <Glass effect="regular" className="absolute inset-0 rounded-full" />
            <LumioMark size={18} />
            <Text accessibilityRole="header" className="text-sm font-bold tracking-tight">
              Spending
            </Text>
          </View>
        </View>
      </View>

      <ScrollView
        showsVerticalScrollIndicator={false}
        contentContainerStyle={{ paddingTop: insets.top + 60, paddingBottom: insets.bottom + 32 }}
      >
        <Mount delay={BLOCK_STAGGER}>
          <View className="px-4 pb-2">
            <SegmentedControl
              value={tab}
              onValueChange={(v) => {
                haptics.selection();
                setTab(v);
                onPeriodChange?.(v);
              }}
              options={["Week", "Month", "Year"]}
            />
          </View>
        </Mount>

        {/* Total. */}
        <Mount delay={BLOCK_STAGGER * 2}>
          <View className="items-center py-2">
            <Text className="text-5xl font-bold tracking-tight">{total}</Text>
            <Text variant="small" className="text-muted-foreground">
              This {tab.toLowerCase()}
            </Text>
            <View className="mt-1 flex-row items-center gap-1">
              <Icon name="arrow-down" size="sm" color="success" />
              <Text className="text-xs text-muted-foreground">
                8% under last {tab.toLowerCase()}
              </Text>
            </View>
          </View>
        </Mount>

        {/* Bar chart — plain columns, latest day highlighted; a real
            chart lives here, not a decorative sparkline. */}
        <Mount delay={BLOCK_STAGGER * 3}>
          <View className="mx-4 mb-2 flex-row items-end justify-between gap-3 px-2 pb-1 pt-4" style={{ height: 140 }}>
            {bars.map((b, i) => (
              <View key={i} className="flex-1 items-center gap-1.5">
                <View className="w-full items-center justify-end" style={{ height: 96 }}>
                  <View
                    className={`w-full rounded-full ${
                      b.current ? "bg-[#BEF04C]" : "bg-[#1B1B26]/15"
                    }`}
                    style={{ height: Math.max(8, b.value * 96) }}
                    accessibilityElementsHidden
                  />
                </View>
                <Text className="text-xs text-muted-foreground">{b.label}</Text>
              </View>
            ))}
          </View>
        </Mount>

        {/* Insight callout — analytics earn their keep by saying something,
            not just charting it. */}
        <Mount delay={BLOCK_STAGGER * 4 - 20}>
          <View className="mx-4 mb-2 flex-row items-center gap-2.5 rounded-2xl bg-[#EDF8C4] px-4 py-3">
            <Icon name="sparkles" size="sm" color="#65A30D" />
            <Text variant="small" className="flex-1 text-xs">
              Dining is down 18% vs last month — you're pacing under budget
            </Text>
          </View>
        </Mount>

        <Mount delay={BLOCK_STAGGER * 4}>
          <List inset>
            <List.Section title="Top categories">
              {categories.map((c, ii) => (
                <React.Fragment key={c.id}>
                  {ii > 0 && <List.Separator className={SEPARATOR_INSET} />}
                  <List.Item
                    title={c.name}
                    leading={
                      <View
                        className={`h-8 w-8 items-center justify-center rounded-2xl ${c.tileBg ?? "bg-muted"}`}
                      >
                        <Icon name={c.icon} size="sm" color={c.tint ?? "foreground"} />
                      </View>
                    }
                    trailing={
                      <Text className="font-semibold">{c.amount}</Text>
                    }
                    haptic="light"
                    onPress={() => haptics.light()}
                    className="py-3"
                  />
                  <View className="px-4 pb-3 pl-[60px]">
                    <Progress
                      value={Math.round(c.share * 100)}
                      className="h-1.5 bg-muted"
                      indicatorClassName={BAR_COLORS[ii % BAR_COLORS.length]}
                    />
                  </View>
                </React.Fragment>
              ))}
            </List.Section>
          </List>
        </Mount>

        <Mount delay={BLOCK_STAGGER * 5}>
          <View className="mx-4 mt-4 flex-row items-center gap-3 rounded-2xl bg-white p-4 dark:bg-[#1A1A20]">
            <View className="h-9 w-9 items-center justify-center rounded-2xl bg-chart-5/15">
              <Icon name="calendar" size="sm" color="chart5" />
            </View>
            <View className="flex-1">
              <Text className="text-sm font-semibold">Weekend-heavy week</Text>
              <Text className="text-xs text-muted-foreground">
                Sat–Sun drove 32% of spending
              </Text>
            </View>
          </View>
        </Mount>
      </ScrollView>
    </SafeAreaView>
  );
}
SpendingAnalyticsScreen.displayName = "SpendingAnalyticsScreen";
agent prompt — paste into Claude Code, Cursor, Devin…
Recreate the "Spending Analytics Screen" screen in React Native using LumioUI.

Goal: Weekly spend bars and category share 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 finance-spending-analytics
```

Or pull just the components: `npx lumioui add button icon list progress segmented-control 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.
- imported but not rendered directly — likely used through a sibling or as a utility

### `icon` — Lucide icon set mapped to theme colors and sizes.
- `<Icon>` ×5

### `list` — Settings-style rows with separators and press feedback.
- `<List>` ×1

### `progress` — Animated fill bar with indeterminate sweep mode.
- `<Progress>` ×1

### `segmented-control` — iOS 26 segmented picker — glass capsule track with a gliding liquid indicator pill, spring motion + haptics
- `<SegmentedControl>` ×1

### `text` — Themed type scale from caption to display.
- `<Text>` ×9

## 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/finance/spending-analytics.tsx` exporting SpendingAnalyticsScreen. 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 { Button, Glass, Icon, type IconName, List, Progress, SegmentedControl, Text } from "@lumioui/ui";
import { useHaptic, type ColorTokens } from "@lumioui/core";
import { BLOCK_STAGGER, LumioMark, Mount, Press, SEPARATOR_INSET } from "../shared";

export interface SpendBar {
  label: string;
  /** 0–1 height fraction. */
  value: number;
  current?: boolean;
}

export interface SpendCategory {
  id: string;
  name: string;
  icon: IconName;
  amount: string;
  /** Share of the period total, 0–1. */
  share: number;
  /** Soft-tint tile background class. */
  tileBg?: string;
  /** Icon tint token — pairs with `tileBg`. */
  tint?: keyof ColorTokens;
}

export interface SpendingAnalyticsScreenProps {
  period?: string;
  total?: string;
  bars?: SpendBar[];
  categories?: SpendCategory[];
  onPeriodChange?: (period: string) => void;
  onBack?: () => void;
}

const DEFAULT_BARS: SpendBar[] = [
  { label: "M", value: 0.35 },
  { label: "T", value: 0.55 },
  { label: "W", value: 0.4 },
  { label: "T", value: 0.75 },
  { label: "F", value: 0.9 },
  { label: "S", value: 0.65 },
  { label: "S", value: 0.5, current: true },
];

/* Amounts sum to the $1,806 total; shares are each amount over the total. */
const DEFAULT_CATEGORIES: SpendCategory[] = [
  { id: "s1", name: "Shopping", icon: "shopping-cart", amount: "$688", share: 0.38, tileBg: "bg-primary/15", tint: "primary" },
  { id: "s2", name: "Groceries", icon: "carrot", amount: "$514", share: 0.28, tileBg: "bg-success/15", tint: "success" },
  { id: "s3", name: "Dining", icon: "utensils", amount: "$342", share: 0.19, tileBg: "bg-warning/15", tint: "warning" },
  { id: "s4", name: "Transport", icon: "car", amount: "$146", share: 0.08, tileBg: "bg-destructive/15", tint: "destructive" },
  { id: "s5", name: "Subscriptions", icon: "repeat", amount: "$116", share: 0.06, tileBg: "bg-chart-5/15", tint: "chart5" },
];

const BAR_COLORS = ["bg-primary", "bg-success", "bg-warning", "bg-destructive"];

/** Spending analytics — week bars + per-category spend share. */
export function SpendingAnalyticsScreen({
  period = "Week",
  total = "$1,806",
  bars = DEFAULT_BARS,
  categories = DEFAULT_CATEGORIES,
  onPeriodChange,
  onBack,
}: SpendingAnalyticsScreenProps) {
  const haptics = useHaptic();
  const insets = useSafeAreaInsets();
  const [tab, setTab] = React.useState(period);

  return (
    <SafeAreaView className="flex-1 bg-[#F4F5F9] dark:bg-[#111114]" edges={["top"]}>
      <View pointerEvents="box-none" className="absolute inset-x-4 z-10" style={{ top: insets.top + 8 }}>
        <View className="flex-row items-center justify-between">
          <Glass interactive effect="regular" className="h-11 w-11 items-center justify-center rounded-full">
            <Press
              haptic="light"
              onPress={() => onBack?.()}
              accessibilityRole="button"
              accessibilityLabel="Go back"
              className="h-9 w-9 items-center justify-center"
            >
              <Icon name="chevron-left" size="md" color="foreground" />
            </Press>
          </Glass>
          <View className="h-11 w-11" />
        </View>
        <View pointerEvents="none" className="absolute inset-0 items-center justify-center">
          <View className="h-11 flex-row items-center justify-center gap-2 rounded-full pl-3.5 pr-5">
            <Glass effect="regular" className="absolute inset-0 rounded-full" />
            <LumioMark size={18} />
            <Text accessibilityRole="header" className="text-sm font-bold tracking-tight">
              Spending
            </Text>
          </View>
        </View>
      </View>

      <ScrollView
        showsVerticalScrollIndicator={false}
        contentContainerStyle={{ paddingTop: insets.top + 60, paddingBottom: insets.bottom + 32 }}
      >
        <Mount delay={BLOCK_STAGGER}>
          <View className="px-4 pb-2">
            <SegmentedControl
              value={tab}
              onValueChange={(v) => {
                haptics.selection();
                setTab(v);
                onPeriodChange?.(v);
              }}
              options={["Week", "Month", "Year"]}
            />
          </View>
        </Mount>

        {/* Total. */}
        <Mount delay={BLOCK_STAGGER * 2}>
          <View className="items-center py-2">
            <Text className="text-5xl font-bold tracking-tight">{total}</Text>
            <Text variant="small" className="text-muted-foreground">
              This {tab.toLowerCase()}
            </Text>
            <View className="mt-1 flex-row items-center gap-1">
              <Icon name="arrow-down" size="sm" color="success" />
              <Text className="text-xs text-muted-foreground">
                8% under last {tab.toLowerCase()}
              </Text>
            </View>
          </View>
        </Mount>

        {/* Bar chart — plain columns, latest day highlighted; a real
            chart lives here, not a decorative sparkline. */}
        <Mount delay={BLOCK_STAGGER * 3}>
          <View className="mx-4 mb-2 flex-row items-end justify-between gap-3 px-2 pb-1 pt-4" style={{ height: 140 }}>
            {bars.map((b, i) => (
              <View key={i} className="flex-1 items-center gap-1.5">
                <View className="w-full items-center justify-end" style={{ height: 96 }}>
                  <View
                    className={`w-full rounded-full ${
                      b.current ? "bg-[#BEF04C]" : "bg-[#1B1B26]/15"
                    }`}
                    style={{ height: Math.max(8, b.value * 96) }}
                    accessibilityElementsHidden
                  />
                </View>
                <Text className="text-xs text-muted-foreground">{b.label}</Text>
              </View>
            ))}
          </View>
        </Mount>

        {/* Insight callout — analytics earn their keep by saying something,
            not just charting it. */}
        <Mount delay={BLOCK_STAGGER * 4 - 20}>
          <View className="mx-4 mb-2 flex-row items-center gap-2.5 rounded-2xl bg-[#EDF8C4] px-4 py-3">
            <Icon name="sparkles" size="sm" color="#65A30D" />
            <Text variant="small" className="flex-1 text-xs">
              Dining is down 18% vs last month — you're pacing under budget
            </Text>
          </View>
        </Mount>

        <Mount delay={BLOCK_STAGGER * 4}>
          <List inset>
            <List.Section title="Top categories">
              {categories.map((c, ii) => (
                <React.Fragment key={c.id}>
                  {ii > 0 && <List.Separator className={SEPARATOR_INSET} />}
                  <List.Item
                    title={c.name}
                    leading={
                      <View
                        className={`h-8 w-8 items-center justify-center rounded-2xl ${c.tileBg ?? "bg-muted"}`}
                      >
                        <Icon name={c.icon} size="sm" color={c.tint ?? "foreground"} />
                      </View>
                    }
                    trailing={
                      <Text className="font-semibold">{c.amount}</Text>
                    }
                    haptic="light"
                    onPress={() => haptics.light()}
                    className="py-3"
                  />
                  <View className="px-4 pb-3 pl-[60px]">
                    <Progress
                      value={Math.round(c.share * 100)}
                      className="h-1.5 bg-muted"
                      indicatorClassName={BAR_COLORS[ii % BAR_COLORS.length]}
                    />
                  </View>
                </React.Fragment>
              ))}
            </List.Section>
          </List>
        </Mount>

        <Mount delay={BLOCK_STAGGER * 5}>
          <View className="mx-4 mt-4 flex-row items-center gap-3 rounded-2xl bg-white p-4 dark:bg-[#1A1A20]">
            <View className="h-9 w-9 items-center justify-center rounded-2xl bg-chart-5/15">
              <Icon name="calendar" size="sm" color="chart5" />
            </View>
            <View className="flex-1">
              <Text className="text-sm font-semibold">Weekend-heavy week</Text>
              <Text className="text-xs text-muted-foreground">
                Sat–Sun drove 32% of spending
              </Text>
            </View>
          </View>
        </Mount>
      </ScrollView>
    </SafeAreaView>
  );
}
SpendingAnalyticsScreen.displayName = "SpendingAnalyticsScreen";
```

Screen: https://lumioui.dev/screens/finance-spending-analytics
iPhone 17 · iOS
9:41
Live preview — the real screen via Expo Web · open full ↗