Modal new-task sheet: title input, colored priority chips, assignee — adds to the list behind
import * as React from "react";
import { useState } from "react";
import { Pressable, ScrollView, View } from "react-native";
import { SafeAreaView, useSafeAreaInsets } from "react-native-safe-area-context";
import { Avatar, Button, Checkbox, Chip, Icon, Input, Text } from "@lumioui/ui";
import { Mount, World } from "../shared";
import { cn, useHaptic } from "@lumioui/core";
export interface AddTaskSheetTask {
id: string;
title: string;
meta: string;
done?: boolean;
}
export interface AddTaskSheetScreenProps {
tasks?: AddTaskSheetTask[];
onAddTask?: (title: string, priority: string) => void;
onClose?: () => void;
}
const DEFAULT_TASKS: AddTaskSheetTask[] = [
{ id: "1", title: "Review onboarding copy", meta: "Design · Today" },
{ id: "2", title: "Ship registry build", meta: "Infra · Tomorrow", done: true },
{ id: "3", title: "Reply to Marco", meta: "People · Fri" },
];
const PRIORITIES = [
{ key: "Low", chip: "bg-success/15", text: "text-success", activeChip: "bg-success", activeText: "text-white" },
{ key: "Medium", chip: "bg-warning/15", text: "text-warning", activeChip: "bg-warning", activeText: "text-white" },
{ key: "High", chip: "bg-destructive/15", text: "text-destructive", activeChip: "bg-destructive", activeText: "text-white" },
] as const;
/** Add-task sheet — modal over a dimmed task list (Dribbble "Add a Task"
* idiom): grabber handle, title input, colored priority chips, assignee
* row, and a CTA that actually appends the task to the list behind. */
export function AddTaskSheetScreen({
tasks = DEFAULT_TASKS,
onAddTask,
onClose,
}: AddTaskSheetScreenProps) {
const haptics = useHaptic();
const insets = useSafeAreaInsets();
const [items, setItems] = useState<AddTaskSheetTask[]>(tasks);
const [open, setOpen] = useState(true);
const [title, setTitle] = useState("Schedule design review");
const [priority, setPriority] = useState<(typeof PRIORITIES)[number]["key"]>("Medium");
const [due, setDue] = useState("Today");
const toggle = (id: string) => {
haptics.selection();
setItems((prev) => prev.map((t) => (t.id === id ? { ...t, done: !t.done } : t)));
};
const addTask = () => {
haptics.medium();
setItems((prev) => [
{ id: `new-${Date.now()}`, title: title.trim() || "Untitled task", meta: `${priority} · ${due}` },
...prev,
]);
onAddTask?.(title, priority);
setOpen(false);
};
return (
<World primary="#65A30D">
<SafeAreaView className="flex-1 bg-[#F4F5F9] dark:bg-[#111114]" edges={["top", "bottom"]}>
{/* Behind the sheet: the task list the new item lands on. */}
<View className="flex-row items-center justify-between px-6 pb-2 pt-2">
<Text variant="h3">Today</Text>
<Button
variant="secondary"
size="sm"
haptic="light"
onPress={() => setOpen(true)}
accessibilityLabel="Add task"
>
+ Add task
</Button>
</View>
<ScrollView contentContainerStyle={{ paddingHorizontal: 24, paddingBottom: 24 }}>
<Mount className="overflow-hidden rounded-3xl bg-white px-4 dark:bg-[#1A1A20]">
{items.map((t, i) => (
<Pressable
key={t.id}
onPress={() => toggle(t.id)}
accessibilityRole="checkbox"
accessibilityState={{ checked: !!t.done }}
accessibilityLabel={t.title}
className={cn(
"flex-row items-center gap-3 py-4",
i < items.length - 1 && "border-b border-border"
)}
>
<Checkbox checked={!!t.done} onCheckedChange={() => toggle(t.id)} />
<View className="flex-1">
<Text
className={
t.done ? "font-medium text-muted-foreground line-through" : "font-medium"
}
>
{t.title}
</Text>
<Text variant="small" className="text-muted-foreground">
{t.meta}
</Text>
</View>
</Pressable>
))}
</Mount>
</ScrollView>
{open ? (
<Mount delay={60} className="absolute inset-0">
<Pressable
className="absolute inset-0 bg-black/50"
onPress={() => {
haptics.light();
setOpen(false);
onClose?.();
}}
accessibilityLabel="Dismiss sheet"
accessibilityRole="button"
/>
{/* Sheet — hand-rolled so the screen stays Expo-Go safe. */}
<View
className="absolute bottom-0 left-0 right-0 rounded-t-3xl bg-card px-6 pb-6 pt-3"
style={{ paddingBottom: insets.bottom + 24 }}
>
<View className="mb-3 h-1 w-10 self-center rounded-full bg-muted" />
<View className="flex-row items-center justify-between">
<Text variant="h3">New task</Text>
<Button
variant="ghost"
size="icon"
haptic="light"
onPress={() => setOpen(false)}
accessibilityLabel="Close"
>
<Icon name="x" size="md" color="mutedForeground" />
</Button>
</View>
<Input
glass
value={title}
onChangeText={setTitle}
placeholder="Task name"
accessibilityLabel="Task name"
className="mt-2"
/>
{/* NL hint — quick-add sheets teach the magic syntax. */}
<Text variant="muted" className="mt-1.5 text-[11px]">
Try "Lunch with Priya Friday 1pm" — we'll pick up the date
</Text>
{/* Due-date quick chips — Todoist's quick-add signature. */}
<View className="mt-3 flex-row flex-wrap gap-2">
{(["Today", "Tomorrow", "Weekend", "Next week"] as const).map((d, di) => {
const active = due === d;
return (
<Chip
key={d}
variant="filter"
selected={active}
onSelectedChange={() => setDue(d)}
accessibilityLabel={`Due ${d}`}
icon={
<Icon
name={di === 0 ? "sun" : di === 1 ? "clock" : di === 2 ? "coffee" : "calendar"}
size="sm"
color={active ? "#65A30D" : "mutedForeground"}
/>
}
className="border-0 bg-muted px-3 py-1.5"
selectedClassName="bg-[#65A30D]/15 border-0"
selectedTextClassName="text-[#65A30D]"
>
{d}
</Chip>
);
})}
</View>
<Text variant="small" className="mb-2 mt-4 text-muted-foreground">
Priority
</Text>
<View className="flex-row gap-2">
{PRIORITIES.map((p) => {
const active = priority === p.key;
return (
<Chip
key={p.key}
variant="filter"
selected={active}
onSelectedChange={() => setPriority(p.key)}
accessibilityLabel={`${p.key} priority`}
className={cn("border-0 px-4 py-2", p.chip)}
selectedClassName={p.activeChip}
textClassName={p.text}
selectedTextClassName={p.activeText}
>
{p.key}
</Chip>
);
})}
</View>
<View className="mt-4 flex-row items-center justify-between rounded-2xl bg-muted px-4 py-3">
<Text className="font-medium">Assign to</Text>
<View className="flex-row items-center gap-2">
<Avatar size="sm" source={{ uri: "https://i.pravatar.cc/150?img=9" }} fallback="A" alt="Amara" />
<Text variant="small" className="text-muted-foreground">
Amara
</Text>
<Icon name="chevron-right" size="sm" color="mutedForeground" />
</View>
</View>
<Button
size="lg"
haptic="medium"
onPress={addTask}
accessibilityLabel="Add task"
className="bg-[#65A30D] mt-5 w-full"
>
Add task
</Button>
</View>
</Mount>
) : null}
</SafeAreaView>
</World>
);
}
AddTaskSheetScreen.displayName = "AddTaskSheetScreen";
Recreate the "Add Task Sheet Screen" screen in React Native using LumioUI.
Goal: Modal new-task sheet: title input, colored priority chips, assignee — adds to the list behind 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 productivity-add-task-sheet
```
Or pull just the components: `npx lumioui add avatar button checkbox chip icon input 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>` ×3
### `checkbox` — Animated check box with haptic tick and error state.
- `<Checkbox>` ×1
### `chip` — Compact selectable pill with press scale and ink overlay.
- `<Chip>` ×2
### `icon` — Lucide icon set mapped to theme colors and sizes.
- `<Icon>` ×3
### `input` — Labeled field with focus ring, error shake and success check.
- `<Input>` ×1
### `text` — Themed type scale from caption to display.
- `<Text>` ×8
## 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/productivity/add-task-sheet.tsx` exporting AddTaskSheetScreen. 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 { Pressable, ScrollView, View } from "react-native";
import { SafeAreaView, useSafeAreaInsets } from "react-native-safe-area-context";
import { Avatar, Button, Checkbox, Chip, Icon, Input, Text } from "@lumioui/ui";
import { Mount, World } from "../shared";
import { cn, useHaptic } from "@lumioui/core";
export interface AddTaskSheetTask {
id: string;
title: string;
meta: string;
done?: boolean;
}
export interface AddTaskSheetScreenProps {
tasks?: AddTaskSheetTask[];
onAddTask?: (title: string, priority: string) => void;
onClose?: () => void;
}
const DEFAULT_TASKS: AddTaskSheetTask[] = [
{ id: "1", title: "Review onboarding copy", meta: "Design · Today" },
{ id: "2", title: "Ship registry build", meta: "Infra · Tomorrow", done: true },
{ id: "3", title: "Reply to Marco", meta: "People · Fri" },
];
const PRIORITIES = [
{ key: "Low", chip: "bg-success/15", text: "text-success", activeChip: "bg-success", activeText: "text-white" },
{ key: "Medium", chip: "bg-warning/15", text: "text-warning", activeChip: "bg-warning", activeText: "text-white" },
{ key: "High", chip: "bg-destructive/15", text: "text-destructive", activeChip: "bg-destructive", activeText: "text-white" },
] as const;
/** Add-task sheet — modal over a dimmed task list (Dribbble "Add a Task"
* idiom): grabber handle, title input, colored priority chips, assignee
* row, and a CTA that actually appends the task to the list behind. */
export function AddTaskSheetScreen({
tasks = DEFAULT_TASKS,
onAddTask,
onClose,
}: AddTaskSheetScreenProps) {
const haptics = useHaptic();
const insets = useSafeAreaInsets();
const [items, setItems] = useState<AddTaskSheetTask[]>(tasks);
const [open, setOpen] = useState(true);
const [title, setTitle] = useState("Schedule design review");
const [priority, setPriority] = useState<(typeof PRIORITIES)[number]["key"]>("Medium");
const [due, setDue] = useState("Today");
const toggle = (id: string) => {
haptics.selection();
setItems((prev) => prev.map((t) => (t.id === id ? { ...t, done: !t.done } : t)));
};
const addTask = () => {
haptics.medium();
setItems((prev) => [
{ id: `new-${Date.now()}`, title: title.trim() || "Untitled task", meta: `${priority} · ${due}` },
...prev,
]);
onAddTask?.(title, priority);
setOpen(false);
};
return (
<World primary="#65A30D">
<SafeAreaView className="flex-1 bg-[#F4F5F9] dark:bg-[#111114]" edges={["top", "bottom"]}>
{/* Behind the sheet: the task list the new item lands on. */}
<View className="flex-row items-center justify-between px-6 pb-2 pt-2">
<Text variant="h3">Today</Text>
<Button
variant="secondary"
size="sm"
haptic="light"
onPress={() => setOpen(true)}
accessibilityLabel="Add task"
>
+ Add task
</Button>
</View>
<ScrollView contentContainerStyle={{ paddingHorizontal: 24, paddingBottom: 24 }}>
<Mount className="overflow-hidden rounded-3xl bg-white px-4 dark:bg-[#1A1A20]">
{items.map((t, i) => (
<Pressable
key={t.id}
onPress={() => toggle(t.id)}
accessibilityRole="checkbox"
accessibilityState={{ checked: !!t.done }}
accessibilityLabel={t.title}
className={cn(
"flex-row items-center gap-3 py-4",
i < items.length - 1 && "border-b border-border"
)}
>
<Checkbox checked={!!t.done} onCheckedChange={() => toggle(t.id)} />
<View className="flex-1">
<Text
className={
t.done ? "font-medium text-muted-foreground line-through" : "font-medium"
}
>
{t.title}
</Text>
<Text variant="small" className="text-muted-foreground">
{t.meta}
</Text>
</View>
</Pressable>
))}
</Mount>
</ScrollView>
{open ? (
<Mount delay={60} className="absolute inset-0">
<Pressable
className="absolute inset-0 bg-black/50"
onPress={() => {
haptics.light();
setOpen(false);
onClose?.();
}}
accessibilityLabel="Dismiss sheet"
accessibilityRole="button"
/>
{/* Sheet — hand-rolled so the screen stays Expo-Go safe. */}
<View
className="absolute bottom-0 left-0 right-0 rounded-t-3xl bg-card px-6 pb-6 pt-3"
style={{ paddingBottom: insets.bottom + 24 }}
>
<View className="mb-3 h-1 w-10 self-center rounded-full bg-muted" />
<View className="flex-row items-center justify-between">
<Text variant="h3">New task</Text>
<Button
variant="ghost"
size="icon"
haptic="light"
onPress={() => setOpen(false)}
accessibilityLabel="Close"
>
<Icon name="x" size="md" color="mutedForeground" />
</Button>
</View>
<Input
glass
value={title}
onChangeText={setTitle}
placeholder="Task name"
accessibilityLabel="Task name"
className="mt-2"
/>
{/* NL hint — quick-add sheets teach the magic syntax. */}
<Text variant="muted" className="mt-1.5 text-[11px]">
Try "Lunch with Priya Friday 1pm" — we'll pick up the date
</Text>
{/* Due-date quick chips — Todoist's quick-add signature. */}
<View className="mt-3 flex-row flex-wrap gap-2">
{(["Today", "Tomorrow", "Weekend", "Next week"] as const).map((d, di) => {
const active = due === d;
return (
<Chip
key={d}
variant="filter"
selected={active}
onSelectedChange={() => setDue(d)}
accessibilityLabel={`Due ${d}`}
icon={
<Icon
name={di === 0 ? "sun" : di === 1 ? "clock" : di === 2 ? "coffee" : "calendar"}
size="sm"
color={active ? "#65A30D" : "mutedForeground"}
/>
}
className="border-0 bg-muted px-3 py-1.5"
selectedClassName="bg-[#65A30D]/15 border-0"
selectedTextClassName="text-[#65A30D]"
>
{d}
</Chip>
);
})}
</View>
<Text variant="small" className="mb-2 mt-4 text-muted-foreground">
Priority
</Text>
<View className="flex-row gap-2">
{PRIORITIES.map((p) => {
const active = priority === p.key;
return (
<Chip
key={p.key}
variant="filter"
selected={active}
onSelectedChange={() => setPriority(p.key)}
accessibilityLabel={`${p.key} priority`}
className={cn("border-0 px-4 py-2", p.chip)}
selectedClassName={p.activeChip}
textClassName={p.text}
selectedTextClassName={p.activeText}
>
{p.key}
</Chip>
);
})}
</View>
<View className="mt-4 flex-row items-center justify-between rounded-2xl bg-muted px-4 py-3">
<Text className="font-medium">Assign to</Text>
<View className="flex-row items-center gap-2">
<Avatar size="sm" source={{ uri: "https://i.pravatar.cc/150?img=9" }} fallback="A" alt="Amara" />
<Text variant="small" className="text-muted-foreground">
Amara
</Text>
<Icon name="chevron-right" size="sm" color="mutedForeground" />
</View>
</View>
<Button
size="lg"
haptic="medium"
onPress={addTask}
accessibilityLabel="Add task"
className="bg-[#65A30D] mt-5 w-full"
>
Add task
</Button>
</View>
</Mount>
) : null}
</SafeAreaView>
</World>
);
}
AddTaskSheetScreen.displayName = "AddTaskSheetScreen";
```
Screen: https://lumioui.dev/screens/productivity-add-task-sheet