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

Dashboard — Bento Screen

Light fintech dashboard: hero numeral, 2-up metric tiles, mixed bento card grid

pro Expo Go ✓ iosandroid gestures: tap · scroll haptics: light · selection
$ npx lumioui add finance-bento
import { BentoScreen } from "@/screens/finance/bento";
Registry dependencies4
Native packages installed6
@lumioui/ui@latest@lumioui/core@latestreact-native-safe-area-context@*react-native-reanimated@*react-native-svg@*nativewind@*
finance/bento.tsxsource · yours to edit
import * as React from "react";
import { useState } from "react";
import { ScrollView, View } from "react-native";
import { SafeAreaView, useSafeAreaInsets } from "react-native-safe-area-context";
import { Avatar, Icon, type IconName, ProgressRing, Text } from "@lumioui/ui";
import { useHaptic } from "@lumioui/core";
import { type IconTone, MONO, Mount, Press, RowIcon } from "../shared";

export interface BentoMetric {
  key: string;
  label: string;
  value: string;
  icon: IconName;
  tone: IconTone;
}

export interface BentoScreenProps {
  userName?: string;
  avatarUrl?: string;
  balance?: number;
  metrics?: BentoMetric[];
}

const METRICS: BentoMetric[] = [
  { key: "positions", label: "Positions", value: "$9,204.18", icon: "zap", tone: "blue" },
  { key: "cash", label: "Cash", value: "$2,250.00", icon: "credit-card", tone: "green" },
];

const ACTIVITY = [
  { icon: "shopping-cart" as IconName, tone: "purple" as IconTone, title: "Apple Store", meta: "Today · 14:32", amount: "-$129.00" },
  { icon: "repeat" as IconName, tone: "teal" as IconTone, title: "Dividend", meta: "Yesterday", amount: "+$42.10" },
  { icon: "car" as IconName, tone: "orange" as IconTone, title: "Shell", meta: "Yesterday", amount: "-$38.42" },
];

/** Bento dashboard — light canvas, big hero numeral, compact 2-up metric
 *  tiles, then a mixed bento grid (ring card + accent card + dark card).
 *  Tiles select; balance hides. */
export function BentoScreen({
  userName = "Amara",
  avatarUrl = "https://i.pravatar.cc/150?img=9",
  balance = 13553,
  metrics = METRICS,
}: BentoScreenProps) {
  const haptics = useHaptic();
  const insets = useSafeAreaInsets();
  const [active, setActive] = useState(metrics[0]?.key ?? "");
  const [hidden, setHidden] = useState(false);

  const fmt = (n: number) => `$${n.toLocaleString("en-US", { minimumFractionDigits: 2 })}`;

  return (
    <SafeAreaView className="flex-1 bg-[#F4F5F9] dark:bg-[#111114]" edges={["top"]}>
      <ScrollView
        showsVerticalScrollIndicator={false}
        contentContainerStyle={{ paddingBottom: insets.bottom + 24 }}
      >
        <Mount className="flex-row items-center justify-between px-6 pt-2">
          <View className="flex-row items-center gap-3">
            <Avatar size="md" source={{ uri: avatarUrl }} fallback={userName} alt={userName} />
            <View>
              <Text variant="small" className="text-muted-foreground">
                {new Date().toLocaleDateString("en-US", { weekday: "long", month: "short", day: "numeric" })}
              </Text>
              <Text className="font-semibold">{userName}</Text>
            </View>
          </View>
          <Press
            onPress={() => haptics.light()}
            accessibilityLabel="Notifications"
            accessibilityRole="button"
            className="h-10 w-10 items-center justify-center rounded-full bg-white dark:bg-[#1A1A20]"
          >
            <Icon name="bell" size="md" color="foreground" />
            <View className="absolute right-2.5 top-2.5 h-2 w-2 rounded-full bg-destructive" />
          </Press>
        </Mount>

        {/* Hero figure + right-aligned label — the banking reference split. */}
        <Mount delay={60} className="mt-4 flex-row items-baseline justify-between px-6">
          <Text className="text-5xl font-bold tracking-tight" style={{ fontFamily: MONO, fontVariant: ["tabular-nums"] }}>
            {hidden ? "••••••" : fmt(balance)}
          </Text>
          <Press
            onPress={() => {
              haptics.selection();
              setHidden((h) => !h);
            }}
            accessibilityLabel={hidden ? "Show balance" : "Hide balance"}
            accessibilityRole="button"
            className="flex-row items-center gap-1"
          >
            <Text variant="small" className="text-muted-foreground">
              Balance
            </Text>
            <Icon name={hidden ? "eye" : "eye-off"} size="sm" color="mutedForeground" />
          </Press>
        </Mount>

        {/* Compact 2-up metric tiles — tap to select. */}
        <Mount delay={120} className="mt-4 flex-row gap-3 px-6">
          {metrics.map((m) => {
            const sel = active === m.key;
            return (
              <Press
                key={m.key}
                onPress={() => {
                  haptics.selection();
                  setActive(m.key);
                }}
                accessibilityRole="button"
                accessibilityState={{ selected: sel }}
                accessibilityLabel={m.label}
                className={`flex-1 rounded-2xl border p-4 dark:bg-[#1A1A20] ${
                  sel ? "border-[#BEF04C] bg-[#EDF8C4] dark:bg-[#1E2410]" : "border-transparent bg-white"
                }`}
              >
                <View className="flex-row items-center justify-between">
                  <RowIcon name={m.icon} tone={m.tone} />
                  <Icon name="chevron-right" size="sm" color="mutedForeground" />
                </View>
                <Text variant="small" className="mt-3 text-muted-foreground">
                  {m.label}
                </Text>
                <Text className="mt-0.5 text-lg font-semibold">{m.value}</Text>
              </Press>
            );
          })}
        </Mount>

        {/* Bento grid — ring card + accent card + dark card, mixed fills. */}
        <Mount delay={180} className="mt-3 flex-row gap-3 px-6">
          <View className="flex-1 items-center rounded-2xl bg-white p-4 dark:bg-[#1A1A20]">
            <ProgressRing value={31} size={84} strokeWidth={7}>
              <Text className="text-base font-semibold">31%</Text>
            </ProgressRing>
            <Text variant="small" className="mt-2 text-center text-muted-foreground">
              Tokens to buy
            </Text>
          </View>
          <View className="flex-1 gap-3">
            <View className="rounded-2xl bg-[#BEF04C] p-4">
              <Text variant="small" className="text-[#1B1B26]/70">
                Bonus received
              </Text>
              <Text className="mt-1 text-lg font-semibold text-[#1B1B26]">
                $22.42
              </Text>
            </View>
            <View className="rounded-2xl bg-[#1B1B26] p-4">
              <Text variant="small" className="text-white/60">
                Referral
              </Text>
              <Text className="mt-1 text-lg font-semibold text-white">
                $122.00
              </Text>
            </View>
          </View>
        </Mount>

        {/* Activity */}
        <Text variant="large" className="mt-6 px-6">
          Activity
        </Text>
        <View className="mx-6 mt-1 rounded-2xl bg-white px-4 dark:bg-[#1A1A20]">
          {ACTIVITY.map((a, i) => (
            <View key={a.title} className="flex-row items-center gap-3 py-3">
              <RowIcon name={a.icon} tone={a.tone} />
              <View className="flex-1">
                <Text className="font-medium">{a.title}</Text>
                <Text variant="small" className="text-muted-foreground">
                  {a.meta}
                </Text>
              </View>
              <Text
                className={`font-semibold ${a.amount.startsWith("+") ? "text-success" : ""}`}
              >
                {a.amount}
              </Text>
              {i < ACTIVITY.length - 1 ? (
                <View className="absolute bottom-0 left-0 right-0 ml-[44px] border-b border-border" />
              ) : null}
            </View>
          ))}
        </View>
      </ScrollView>
    </SafeAreaView>
  );
}
BentoScreen.displayName = "BentoScreen";
agent prompt — paste into Claude Code, Cursor, Devin…
Recreate the "Dashboard — Bento Screen" screen in React Native using LumioUI.

Goal: Light fintech dashboard: hero numeral, 2-up metric tiles, mixed bento card grid 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-bento
```

Or pull just the components: `npx lumioui add avatar 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

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

### `progress-ring` — Circular progress ring with animated stroke arc, center slot and indeterminate spin.
- `<ProgressRing>` ×1

### `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/finance/bento.tsx` exporting BentoScreen. 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, useSafeAreaInsets } from "react-native-safe-area-context";
import { Avatar, Icon, type IconName, ProgressRing, Text } from "@lumioui/ui";
import { useHaptic } from "@lumioui/core";
import { type IconTone, MONO, Mount, Press, RowIcon } from "../shared";

export interface BentoMetric {
  key: string;
  label: string;
  value: string;
  icon: IconName;
  tone: IconTone;
}

export interface BentoScreenProps {
  userName?: string;
  avatarUrl?: string;
  balance?: number;
  metrics?: BentoMetric[];
}

const METRICS: BentoMetric[] = [
  { key: "positions", label: "Positions", value: "$9,204.18", icon: "zap", tone: "blue" },
  { key: "cash", label: "Cash", value: "$2,250.00", icon: "credit-card", tone: "green" },
];

const ACTIVITY = [
  { icon: "shopping-cart" as IconName, tone: "purple" as IconTone, title: "Apple Store", meta: "Today · 14:32", amount: "-$129.00" },
  { icon: "repeat" as IconName, tone: "teal" as IconTone, title: "Dividend", meta: "Yesterday", amount: "+$42.10" },
  { icon: "car" as IconName, tone: "orange" as IconTone, title: "Shell", meta: "Yesterday", amount: "-$38.42" },
];

/** Bento dashboard — light canvas, big hero numeral, compact 2-up metric
 *  tiles, then a mixed bento grid (ring card + accent card + dark card).
 *  Tiles select; balance hides. */
export function BentoScreen({
  userName = "Amara",
  avatarUrl = "https://i.pravatar.cc/150?img=9",
  balance = 13553,
  metrics = METRICS,
}: BentoScreenProps) {
  const haptics = useHaptic();
  const insets = useSafeAreaInsets();
  const [active, setActive] = useState(metrics[0]?.key ?? "");
  const [hidden, setHidden] = useState(false);

  const fmt = (n: number) => `$${n.toLocaleString("en-US", { minimumFractionDigits: 2 })}`;

  return (
    <SafeAreaView className="flex-1 bg-[#F4F5F9] dark:bg-[#111114]" edges={["top"]}>
      <ScrollView
        showsVerticalScrollIndicator={false}
        contentContainerStyle={{ paddingBottom: insets.bottom + 24 }}
      >
        <Mount className="flex-row items-center justify-between px-6 pt-2">
          <View className="flex-row items-center gap-3">
            <Avatar size="md" source={{ uri: avatarUrl }} fallback={userName} alt={userName} />
            <View>
              <Text variant="small" className="text-muted-foreground">
                {new Date().toLocaleDateString("en-US", { weekday: "long", month: "short", day: "numeric" })}
              </Text>
              <Text className="font-semibold">{userName}</Text>
            </View>
          </View>
          <Press
            onPress={() => haptics.light()}
            accessibilityLabel="Notifications"
            accessibilityRole="button"
            className="h-10 w-10 items-center justify-center rounded-full bg-white dark:bg-[#1A1A20]"
          >
            <Icon name="bell" size="md" color="foreground" />
            <View className="absolute right-2.5 top-2.5 h-2 w-2 rounded-full bg-destructive" />
          </Press>
        </Mount>

        {/* Hero figure + right-aligned label — the banking reference split. */}
        <Mount delay={60} className="mt-4 flex-row items-baseline justify-between px-6">
          <Text className="text-5xl font-bold tracking-tight" style={{ fontFamily: MONO, fontVariant: ["tabular-nums"] }}>
            {hidden ? "••••••" : fmt(balance)}
          </Text>
          <Press
            onPress={() => {
              haptics.selection();
              setHidden((h) => !h);
            }}
            accessibilityLabel={hidden ? "Show balance" : "Hide balance"}
            accessibilityRole="button"
            className="flex-row items-center gap-1"
          >
            <Text variant="small" className="text-muted-foreground">
              Balance
            </Text>
            <Icon name={hidden ? "eye" : "eye-off"} size="sm" color="mutedForeground" />
          </Press>
        </Mount>

        {/* Compact 2-up metric tiles — tap to select. */}
        <Mount delay={120} className="mt-4 flex-row gap-3 px-6">
          {metrics.map((m) => {
            const sel = active === m.key;
            return (
              <Press
                key={m.key}
                onPress={() => {
                  haptics.selection();
                  setActive(m.key);
                }}
                accessibilityRole="button"
                accessibilityState={{ selected: sel }}
                accessibilityLabel={m.label}
                className={`flex-1 rounded-2xl border p-4 dark:bg-[#1A1A20] ${
                  sel ? "border-[#BEF04C] bg-[#EDF8C4] dark:bg-[#1E2410]" : "border-transparent bg-white"
                }`}
              >
                <View className="flex-row items-center justify-between">
                  <RowIcon name={m.icon} tone={m.tone} />
                  <Icon name="chevron-right" size="sm" color="mutedForeground" />
                </View>
                <Text variant="small" className="mt-3 text-muted-foreground">
                  {m.label}
                </Text>
                <Text className="mt-0.5 text-lg font-semibold">{m.value}</Text>
              </Press>
            );
          })}
        </Mount>

        {/* Bento grid — ring card + accent card + dark card, mixed fills. */}
        <Mount delay={180} className="mt-3 flex-row gap-3 px-6">
          <View className="flex-1 items-center rounded-2xl bg-white p-4 dark:bg-[#1A1A20]">
            <ProgressRing value={31} size={84} strokeWidth={7}>
              <Text className="text-base font-semibold">31%</Text>
            </ProgressRing>
            <Text variant="small" className="mt-2 text-center text-muted-foreground">
              Tokens to buy
            </Text>
          </View>
          <View className="flex-1 gap-3">
            <View className="rounded-2xl bg-[#BEF04C] p-4">
              <Text variant="small" className="text-[#1B1B26]/70">
                Bonus received
              </Text>
              <Text className="mt-1 text-lg font-semibold text-[#1B1B26]">
                $22.42
              </Text>
            </View>
            <View className="rounded-2xl bg-[#1B1B26] p-4">
              <Text variant="small" className="text-white/60">
                Referral
              </Text>
              <Text className="mt-1 text-lg font-semibold text-white">
                $122.00
              </Text>
            </View>
          </View>
        </Mount>

        {/* Activity */}
        <Text variant="large" className="mt-6 px-6">
          Activity
        </Text>
        <View className="mx-6 mt-1 rounded-2xl bg-white px-4 dark:bg-[#1A1A20]">
          {ACTIVITY.map((a, i) => (
            <View key={a.title} className="flex-row items-center gap-3 py-3">
              <RowIcon name={a.icon} tone={a.tone} />
              <View className="flex-1">
                <Text className="font-medium">{a.title}</Text>
                <Text variant="small" className="text-muted-foreground">
                  {a.meta}
                </Text>
              </View>
              <Text
                className={`font-semibold ${a.amount.startsWith("+") ? "text-success" : ""}`}
              >
                {a.amount}
              </Text>
              {i < ACTIVITY.length - 1 ? (
                <View className="absolute bottom-0 left-0 right-0 ml-[44px] border-b border-border" />
              ) : null}
            </View>
          ))}
        </View>
      </ScrollView>
    </SafeAreaView>
  );
}
BentoScreen.displayName = "BentoScreen";
```

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