Glowup
Template
Inbox
List–detail mail: search and filter chips over a ListItem list, the message beside it on a wide container and in place of it on a narrow one, an empty state and a confirmed delete.
Live preview
󰍉
Built with
14 Glowup components and the theme's tokens — no hex values, no hand-rolled controls. Each chip opens that component's reference.
HStack
VStack
The code
The whole of Inbox.tsx. It imports only React, React Native and @its/glowup-ui, so it can be pasted into an app as is.
Inbox.tsx
import { useState } from "react"; import { View } from "react-native"; import { Avatar, Badge, Box, Button, ChipGroup, ConfirmDialog, Divider, EmptyState, HStack, IconButton, ListItem, SearchBar, Typography, VStack, useTheme, } from "@its/glowup-ui"; interface Message { id: string; from: string; subject: string; preview: string; time: string; unread: boolean; starred: boolean; } const MESSAGES: Message[] = [ { id: "1", from: "Grace Hopper", subject: "Release notes for 2.4", preview: "I went through the changelog and flagged two breaking changes…", time: "09:41", unread: true, starred: true, }, { id: "2", from: "Alan Turing", subject: "Design review on Thursday", preview: "Can we move the review to 3pm? The prototype needs one more pass.", time: "08:12", unread: true, starred: false, }, { id: "3", from: "Katherine Johnson", subject: "Quarterly numbers", preview: "Attached the final report. Revenue is up 12% on last quarter.", time: "Yesterday", unread: false, starred: true, }, { id: "4", from: "Linus Torvalds", subject: "Re: build times", preview: "Caching the dependencies cut the pipeline from 14 to 6 minutes.", time: "Mon", unread: false, starred: false, }, ]; const FILTERS = [ { value: "all", label: "All" }, { value: "unread", label: "Unread", icon: "email-outline" }, { value: "starred", label: "Starred", icon: "star-outline" }, ] as const; /** * List–detail mail. Side by side on a wide container; on a narrow one the * detail replaces the list and a back button returns to it. */ export default function Inbox() { const { theme } = useTheme(); const [width, setWidth] = useState(0); const [messages, setMessages] = useState(MESSAGES); const [query, setQuery] = useState(""); const [filter, setFilter] = useState("all"); const [selectedId, setSelectedId] = useState<string | null>("1"); // On a narrow container the detail covers the list only while reading. const [reading, setReading] = useState(false); const [confirmDelete, setConfirmDelete] = useState(false); const isWide = width >= 640; const visible = messages.filter( (message) => (filter === "all" || (filter === "unread" && message.unread) || (filter === "starred" && message.starred)) && `${message.from} ${message.subject}` .toLowerCase() .includes(query.toLowerCase()), ); const selected = messages.find((message) => message.id === selectedId); const open = (id: string) => { setSelectedId(id); setReading(true); setMessages((all) => all.map((m) => (m.id === id ? { ...m, unread: false } : m)), ); }; const toggleStar = (id: string) => setMessages((all) => all.map((m) => (m.id === id ? { ...m, starred: !m.starred } : m)), ); const remove = () => { setMessages((all) => all.filter((m) => m.id !== selectedId)); setSelectedId(null); setReading(false); setConfirmDelete(false); }; const list = ( <VStack spacing="m" style={{ flex: 1 }}> <VStack spacing="s" px="m" pt="m"> <SearchBar value={query} onChangeText={setQuery} placeholder="Search mail" /> <ChipGroup required accessibilityLabel="Filter" options={[...FILTERS]} value={filter} onValueChange={setFilter} /> </VStack> {visible.length === 0 ? ( <EmptyState icon="email-search-outline" title="No messages" description="Nothing matches this search and filter." action={{ label: "Clear filters", onPress: () => { setQuery(""); setFilter("all"); }, }} /> ) : ( <View> {visible.map((message) => ( <ListItem key={message.id} onPress={() => open(message.id)} leading={<Avatar name={message.from} size={40} />} secondary={`${message.subject} — ${message.preview}`} trailing={ <HStack spacing="s" align="center"> {message.unread && <Badge size="small" />} <Typography variant="labelSmall" style={{ color: theme.colors.onSurfaceVariant }} > {message.time} </Typography> </HStack> } itemContainerStyle={ isWide && message.id === selectedId ? { backgroundColor: theme.colors.primaryContainer } : undefined } itemTextStyle={message.unread ? { fontWeight: "700" } : undefined} > {message.from} </ListItem> ))} </View> )} </VStack> ); const detail = selected ? ( <VStack spacing="l" p="l" style={{ flex: 1 }}> <HStack justify="space-between" align="center"> {!isWide ? ( <IconButton icon="arrow-left" accessibilityLabel="Back to inbox" onPress={() => setReading(false)} /> ) : ( <View /> )} <HStack spacing="xs"> <IconButton icon={selected.starred ? "star" : "star-outline"} selected={selected.starred} accessibilityLabel="Star" onPress={() => toggleStar(selected.id)} /> <IconButton icon="archive-outline" accessibilityLabel="Archive" /> <IconButton icon="delete-outline" accessibilityLabel="Delete" onPress={() => setConfirmDelete(true)} /> </HStack> </HStack> <Typography variant="headlineSmall" style={{ color: theme.colors.onSurface }} > {selected.subject} </Typography> <HStack spacing="m" align="center"> <Avatar name={selected.from} size={40} /> <VStack flex={1}> <Typography variant="titleSmall" style={{ color: theme.colors.onSurface }} > {selected.from} </Typography> <Typography variant="bodySmall" style={{ color: theme.colors.onSurfaceVariant }} > to me · {selected.time} </Typography> </VStack> </HStack> <Divider /> <Typography variant="bodyLarge" style={{ color: theme.colors.onSurface }}> {selected.preview} Let me know what you think before Friday, and I will fold your notes into the next draft. </Typography> <HStack spacing="s" wrap> <Button iconName="reply">Reply</Button> <Button mode="outlined" iconName="share-outline"> Forward </Button> </HStack> </VStack> ) : ( <EmptyState icon="email-open-outline" title="No message selected" description="Pick a conversation from the list to read it here." /> ); return ( <Box radius="extraLarge" bg="surface" borderWidth={1} borderColor="outlineVariant" style={{ overflow: "hidden", minHeight: 480 }} onLayout={(event) => setWidth(event.nativeEvent.layout.width)} > {isWide ? ( <HStack align="stretch" spacing={0} style={{ flex: 1 }}> <View style={{ width: 320 }}>{list}</View> <Divider orientation="vertical" /> {detail} </HStack> ) : reading && selected ? ( detail ) : ( list )} <ConfirmDialog visible={confirmDelete} title="Delete this message?" message="It will be moved to the bin and removed after 30 days." confirmText="Delete" onConfirm={remove} onCancel={() => setConfirmDelete(false)} /> </Box> ); }