iOS grouped list — profile card row, tinted-icon navigation rows, toggles, centered destructive logout, and version footer
import * as React from "react";
import { Pressable, ScrollView, View } from "react-native";
import { SafeAreaView, useSafeAreaInsets } from "react-native-safe-area-context";
import { Avatar, Glass, Icon, List, Switch, Text, type IconName } from "@lumioui/ui";
import { useHaptic } from "@lumioui/core";
import { BLOCK_STAGGER, Mount, RowIcon, SEPARATOR_INSET, type IconTone } from "../shared";
export interface SettingsUser {
name: string;
email?: string;
avatarUrl?: string;
}
/** Map of toggle key → current value. */
export type SettingsToggles = Record<string, boolean>;
export interface SettingsScreenProps {
user?: SettingsUser;
toggles?: SettingsToggles;
onToggle?: (key: string, value: boolean) => void;
onNavigate?: (route: string) => void;
onLogout?: () => void;
/** Footer line under the list — e.g. "Version 2.4.0 (148)". */
version?: string;
}
const face = (n: number) => `https://i.pravatar.cc/150?img=${n}`;
const DEFAULT_TOGGLES: SettingsToggles = {
pushNotifications: true,
emailUpdates: false,
haptics: true,
};
interface ToggleRowProps {
title: string;
icon: IconName;
tone: IconTone;
checked: boolean;
onChange: (value: boolean) => void;
}
/** Boolean row — the whole row is a toggle target, Switch is the affordance. */
function ToggleRow({ title, icon, tone, checked, onChange }: ToggleRowProps) {
return (
<List.Item
title={title}
leading={<RowIcon name={icon} tone={tone} />}
// Switch fires its own selection haptic; the row press routes through
// onChange so both paths vibrate exactly once.
haptic={false}
onPress={() => onChange(!checked)}
accessibilityState={{ checked }}
trailing={
<Switch
checked={checked}
onCheckedChange={onChange}
accessibilityLabel={title}
/>
}
/>
);
}
ToggleRow.displayName = "ToggleRow";
/** Settings — iOS grouped list: profile card, icon rows, toggles, version footer. */
export function SettingsScreen({
user = { name: "Amara Chen", email: "amara.chen@lumio.dev", avatarUrl: face(9) },
toggles = DEFAULT_TOGGLES,
onToggle,
onNavigate,
onLogout,
version = "Lumio · Version 2.4.0 (148)",
}: SettingsScreenProps) {
const haptics = useHaptic();
const insets = useSafeAreaInsets();
const [values, setValues] = React.useState(toggles);
const toggle = (key: string) => (value: boolean) => {
haptics.selection();
setValues((prev) => ({ ...prev, [key]: value }));
onToggle?.(key, value);
};
// Rows only get onPress when the host wired navigation — a row with a
// press handler always announces as a button, even if it's a no-op.
const nav = (route: string) =>
onNavigate
? () => {
haptics.light();
onNavigate(route);
}
: undefined;
return (
<SafeAreaView className="flex-1 bg-[#F4F5F9] dark:bg-[#111114]" edges={["top"]}>
{/* Floating chrome — centered title pill only; settings is a root tab. */}
<View className="absolute inset-x-4 z-10 items-center" style={{ top: insets.top + 8 }} pointerEvents="none">
<Glass
effect="regular"
className="h-11 flex-row items-center justify-center rounded-full px-5"
>
<Text accessibilityRole="header" className="text-sm font-bold tracking-tight">
Settings
</Text>
</Glass>
</View>
<ScrollView
showsVerticalScrollIndicator={false}
contentContainerStyle={{ paddingTop: insets.top + 60, paddingBottom: insets.bottom + 32 }}
>
{/* Profile identity — the ink hero card: avatar + name on dark
surface, lime Pro chip, white chevron disc. */}
<Mount delay={BLOCK_STAGGER}>
<Pressable
onPress={nav("profile")}
accessibilityRole="button"
accessibilityLabel={`${user.name}, view profile`}
disabled={!onNavigate}
className="mx-4 flex-row items-center gap-4 overflow-hidden rounded-3xl bg-[#1B1B26] p-5"
>
<Avatar
size="xl"
source={user.avatarUrl ? { uri: user.avatarUrl } : undefined}
fallback={user.name}
alt={user.name}
/>
<View className="flex-1">
<Text className="text-lg font-semibold tracking-tight text-white">{user.name}</Text>
<Text className="text-sm text-white/55">
{user.email ?? "Account and preferences"}
</Text>
<View className="mt-2 flex-row items-center gap-1.5">
<View className="flex-row items-center gap-1 rounded-full bg-[#BEF04C] px-2 py-0.5">
<Icon name="crown" size="sm" color="#1B1B26" />
<Text className="text-[11px] font-semibold text-[#1B1B26]">
Lumio Pro
</Text>
</View>
</View>
</View>
<View className="h-8 w-8 items-center justify-center rounded-full bg-white/10">
<Icon name="chevron-right" size="sm" color="#FFF" />
</View>
</Pressable>
</Mount>
<List inset>
<Mount delay={BLOCK_STAGGER * 2}>
<List.Section title="Account">
<List.Item
title="Personal information"
leading={<RowIcon name="user" tone="blue" />}
showChevron
haptic="light"
onPress={nav("account/personal")}
/>
<List.Separator className={SEPARATOR_INSET} />
<List.Item
title="Sign-in & security"
leading={<RowIcon name="shield" tone="green" />}
showChevron
haptic="light"
onPress={nav("account/security")}
/>
<List.Separator className={SEPARATOR_INSET} />
<List.Item
title="Subscription"
subtitle="Lumio Pro · Renews Oct 12"
leading={<RowIcon name="crown" tone="purple" />}
showChevron
haptic="light"
onPress={nav("account/subscription")}
/>
</List.Section>
</Mount>
<Mount delay={BLOCK_STAGGER * 3}>
<List.Section title="Preferences">
<ToggleRow
title="Push notifications"
icon="bell"
tone="red"
checked={values.pushNotifications ?? false}
onChange={toggle("pushNotifications")}
/>
<List.Separator className={SEPARATOR_INSET} />
<ToggleRow
title="Email updates"
icon="mail"
tone="teal"
checked={values.emailUpdates ?? false}
onChange={toggle("emailUpdates")}
/>
<List.Separator className={SEPARATOR_INSET} />
<ToggleRow
title="Haptic feedback"
icon="vibrate"
tone="orange"
checked={values.haptics ?? false}
onChange={toggle("haptics")}
/>
<List.Separator className={SEPARATOR_INSET} />
<List.Item
title="Appearance"
subtitle="System"
leading={<RowIcon name="moon" tone="purple" />}
showChevron
haptic="light"
onPress={nav("preferences/appearance")}
/>
</List.Section>
</Mount>
<Mount delay={BLOCK_STAGGER * 4}>
<List.Section title="Support & about">
<List.Item
title="Help center"
leading={<RowIcon name="question-circle" tone="teal" />}
showChevron
haptic="light"
onPress={nav("support/help")}
/>
<List.Separator className={SEPARATOR_INSET} />
<List.Item
title="Privacy policy"
leading={<RowIcon name="file-text" tone="gray" />}
showChevron
haptic="light"
onPress={nav("about/privacy")}
/>
<List.Separator className={SEPARATOR_INSET} />
<List.Item
title="Rate LumioUI"
leading={<RowIcon name="star" tone="orange" />}
showChevron
haptic="light"
onPress={nav("support/rate")}
/>
<List.Separator className={SEPARATOR_INSET} />
<List.Item
title="Tell a friend"
leading={<RowIcon name="share" tone="blue" />}
showChevron
haptic="light"
onPress={nav("support/share")}
/>
</List.Section>
</Mount>
{/* Destructive row — centered label is the iOS logout idiom. */}
<Mount delay={BLOCK_STAGGER * 5}>
<List.Section>
<List.Item
title="Log Out"
titleClassName="text-center font-medium text-destructive"
haptic={false}
onPress={() => {
haptics.warning();
onLogout?.();
}}
accessibilityLabel="Log out"
accessibilityHint="Ends your session on this device"
/>
</List.Section>
</Mount>
</List>
{version ? (
<Mount delay={BLOCK_STAGGER * 6}>
<View className="items-center gap-1.5 px-6 pb-2">
<Text variant="muted" className="text-center text-xs">
{version}
</Text>
{/* Release-notes link — settings footers always carry it. */}
<Text variant="small" className="font-medium text-primary">
What's new in 2.4
</Text>
</View>
</Mount>
) : null}
</ScrollView>
</SafeAreaView>
);
}
Recreate the "Settings Screen" screen in React Native using LumioUI.
Goal: iOS grouped list — profile card row, tinted-icon navigation rows, toggles, centered destructive logout, and version footer 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 core-settings
```
Or pull just the components: `npx lumioui add avatar icon list switch 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>` ×2
### `list` — Settings-style rows with separators and press feedback.
- `<List>` ×1
### `switch` — iOS-size toggle with spring thumb and haptics.
- `<Switch>` ×1
### `text` — Themed type scale from caption to display.
- `<Text>` ×6
## 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/core/settings.tsx` exporting SettingsScreen. 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 { Pressable, ScrollView, View } from "react-native";
import { SafeAreaView, useSafeAreaInsets } from "react-native-safe-area-context";
import { Avatar, Glass, Icon, List, Switch, Text, type IconName } from "@lumioui/ui";
import { useHaptic } from "@lumioui/core";
import { BLOCK_STAGGER, Mount, RowIcon, SEPARATOR_INSET, type IconTone } from "../shared";
export interface SettingsUser {
name: string;
email?: string;
avatarUrl?: string;
}
/** Map of toggle key → current value. */
export type SettingsToggles = Record<string, boolean>;
export interface SettingsScreenProps {
user?: SettingsUser;
toggles?: SettingsToggles;
onToggle?: (key: string, value: boolean) => void;
onNavigate?: (route: string) => void;
onLogout?: () => void;
/** Footer line under the list — e.g. "Version 2.4.0 (148)". */
version?: string;
}
const face = (n: number) => `https://i.pravatar.cc/150?img=${n}`;
const DEFAULT_TOGGLES: SettingsToggles = {
pushNotifications: true,
emailUpdates: false,
haptics: true,
};
interface ToggleRowProps {
title: string;
icon: IconName;
tone: IconTone;
checked: boolean;
onChange: (value: boolean) => void;
}
/** Boolean row — the whole row is a toggle target, Switch is the affordance. */
function ToggleRow({ title, icon, tone, checked, onChange }: ToggleRowProps) {
return (
<List.Item
title={title}
leading={<RowIcon name={icon} tone={tone} />}
// Switch fires its own selection haptic; the row press routes through
// onChange so both paths vibrate exactly once.
haptic={false}
onPress={() => onChange(!checked)}
accessibilityState={{ checked }}
trailing={
<Switch
checked={checked}
onCheckedChange={onChange}
accessibilityLabel={title}
/>
}
/>
);
}
ToggleRow.displayName = "ToggleRow";
/** Settings — iOS grouped list: profile card, icon rows, toggles, version footer. */
export function SettingsScreen({
user = { name: "Amara Chen", email: "amara.chen@lumio.dev", avatarUrl: face(9) },
toggles = DEFAULT_TOGGLES,
onToggle,
onNavigate,
onLogout,
version = "Lumio · Version 2.4.0 (148)",
}: SettingsScreenProps) {
const haptics = useHaptic();
const insets = useSafeAreaInsets();
const [values, setValues] = React.useState(toggles);
const toggle = (key: string) => (value: boolean) => {
haptics.selection();
setValues((prev) => ({ ...prev, [key]: value }));
onToggle?.(key, value);
};
// Rows only get onPress when the host wired navigation — a row with a
// press handler always announces as a button, even if it's a no-op.
const nav = (route: string) =>
onNavigate
? () => {
haptics.light();
onNavigate(route);
}
: undefined;
return (
<SafeAreaView className="flex-1 bg-[#F4F5F9] dark:bg-[#111114]" edges={["top"]}>
{/* Floating chrome — centered title pill only; settings is a root tab. */}
<View className="absolute inset-x-4 z-10 items-center" style={{ top: insets.top + 8 }} pointerEvents="none">
<Glass
effect="regular"
className="h-11 flex-row items-center justify-center rounded-full px-5"
>
<Text accessibilityRole="header" className="text-sm font-bold tracking-tight">
Settings
</Text>
</Glass>
</View>
<ScrollView
showsVerticalScrollIndicator={false}
contentContainerStyle={{ paddingTop: insets.top + 60, paddingBottom: insets.bottom + 32 }}
>
{/* Profile identity — the ink hero card: avatar + name on dark
surface, lime Pro chip, white chevron disc. */}
<Mount delay={BLOCK_STAGGER}>
<Pressable
onPress={nav("profile")}
accessibilityRole="button"
accessibilityLabel={`${user.name}, view profile`}
disabled={!onNavigate}
className="mx-4 flex-row items-center gap-4 overflow-hidden rounded-3xl bg-[#1B1B26] p-5"
>
<Avatar
size="xl"
source={user.avatarUrl ? { uri: user.avatarUrl } : undefined}
fallback={user.name}
alt={user.name}
/>
<View className="flex-1">
<Text className="text-lg font-semibold tracking-tight text-white">{user.name}</Text>
<Text className="text-sm text-white/55">
{user.email ?? "Account and preferences"}
</Text>
<View className="mt-2 flex-row items-center gap-1.5">
<View className="flex-row items-center gap-1 rounded-full bg-[#BEF04C] px-2 py-0.5">
<Icon name="crown" size="sm" color="#1B1B26" />
<Text className="text-[11px] font-semibold text-[#1B1B26]">
Lumio Pro
</Text>
</View>
</View>
</View>
<View className="h-8 w-8 items-center justify-center rounded-full bg-white/10">
<Icon name="chevron-right" size="sm" color="#FFF" />
</View>
</Pressable>
</Mount>
<List inset>
<Mount delay={BLOCK_STAGGER * 2}>
<List.Section title="Account">
<List.Item
title="Personal information"
leading={<RowIcon name="user" tone="blue" />}
showChevron
haptic="light"
onPress={nav("account/personal")}
/>
<List.Separator className={SEPARATOR_INSET} />
<List.Item
title="Sign-in & security"
leading={<RowIcon name="shield" tone="green" />}
showChevron
haptic="light"
onPress={nav("account/security")}
/>
<List.Separator className={SEPARATOR_INSET} />
<List.Item
title="Subscription"
subtitle="Lumio Pro · Renews Oct 12"
leading={<RowIcon name="crown" tone="purple" />}
showChevron
haptic="light"
onPress={nav("account/subscription")}
/>
</List.Section>
</Mount>
<Mount delay={BLOCK_STAGGER * 3}>
<List.Section title="Preferences">
<ToggleRow
title="Push notifications"
icon="bell"
tone="red"
checked={values.pushNotifications ?? false}
onChange={toggle("pushNotifications")}
/>
<List.Separator className={SEPARATOR_INSET} />
<ToggleRow
title="Email updates"
icon="mail"
tone="teal"
checked={values.emailUpdates ?? false}
onChange={toggle("emailUpdates")}
/>
<List.Separator className={SEPARATOR_INSET} />
<ToggleRow
title="Haptic feedback"
icon="vibrate"
tone="orange"
checked={values.haptics ?? false}
onChange={toggle("haptics")}
/>
<List.Separator className={SEPARATOR_INSET} />
<List.Item
title="Appearance"
subtitle="System"
leading={<RowIcon name="moon" tone="purple" />}
showChevron
haptic="light"
onPress={nav("preferences/appearance")}
/>
</List.Section>
</Mount>
<Mount delay={BLOCK_STAGGER * 4}>
<List.Section title="Support & about">
<List.Item
title="Help center"
leading={<RowIcon name="question-circle" tone="teal" />}
showChevron
haptic="light"
onPress={nav("support/help")}
/>
<List.Separator className={SEPARATOR_INSET} />
<List.Item
title="Privacy policy"
leading={<RowIcon name="file-text" tone="gray" />}
showChevron
haptic="light"
onPress={nav("about/privacy")}
/>
<List.Separator className={SEPARATOR_INSET} />
<List.Item
title="Rate LumioUI"
leading={<RowIcon name="star" tone="orange" />}
showChevron
haptic="light"
onPress={nav("support/rate")}
/>
<List.Separator className={SEPARATOR_INSET} />
<List.Item
title="Tell a friend"
leading={<RowIcon name="share" tone="blue" />}
showChevron
haptic="light"
onPress={nav("support/share")}
/>
</List.Section>
</Mount>
{/* Destructive row — centered label is the iOS logout idiom. */}
<Mount delay={BLOCK_STAGGER * 5}>
<List.Section>
<List.Item
title="Log Out"
titleClassName="text-center font-medium text-destructive"
haptic={false}
onPress={() => {
haptics.warning();
onLogout?.();
}}
accessibilityLabel="Log out"
accessibilityHint="Ends your session on this device"
/>
</List.Section>
</Mount>
</List>
{version ? (
<Mount delay={BLOCK_STAGGER * 6}>
<View className="items-center gap-1.5 px-6 pb-2">
<Text variant="muted" className="text-center text-xs">
{version}
</Text>
{/* Release-notes link — settings footers always carry it. */}
<Text variant="small" className="font-medium text-primary">
What's new in 2.4
</Text>
</View>
</Mount>
) : null}
</ScrollView>
</SafeAreaView>
);
}
```
Screen: https://lumioui.dev/screens/core-settings