v0.1 public beta early access on GitHub
Home / Screens / productivity-task-list
Screen template

Task List Screen

Checkable tasks with progress

pro Expo Go ✓ iosandroid gestures: tap · scroll haptics: light · selection
$ npx lumioui add productivity-task-list
import { TaskListScreen } from "@/screens/productivity/task-list";
Registry dependencies6
Native packages installed6
@lumioui/ui@latest@lumioui/core@latestreact-native-safe-area-context@*react-native-reanimated@*react-native-svg@*nativewind@*
productivity/task-list.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 { Text } from "@lumioui/ui";
import { useHaptic } from "@lumioui/core";
import { GlowSpot, MONO, Mount, Press, World } from "../shared";

export interface Task {
  id: string;
  title: string;
  detail: string;
  done?: boolean;
  flagged?: boolean;
}

export interface TaskListScreenProps {
  list?: string;
  tasks?: Task[];
  onToggle?: (id: string, done: boolean) => void;
}

const DEFAULT_TASKS: Task[] = [
  { id: "t1", title: "Review onboarding screens", detail: "today · design", flagged: true },
  { id: "t2", title: "Reply to Sarah's proposal", detail: "today · work", done: true },
  { id: "t3", title: "Buy groceries", detail: "18:00 · errands" },
  { id: "t4", title: "Call plumber re: sink", detail: "tomorrow · home" },
  { id: "t5", title: "Book dentist check-up", detail: "this week" },
  { id: "t6", title: "Send invoice to Forma", detail: "today · work" },
  { id: "t7", title: "Renew gym membership", detail: "friday · health" },
  { id: "t8", title: "Plan Lisbon itinerary", detail: "this week · travel" },
  { id: "t9", title: "Read 20 pages", detail: "nightly · personal" },
];

const GREEN = "#4ADE80";
const DIM = "#5C6B5E";
const FAINT = "#334036";

/** Text progress bar — block glyphs, the terminal convention. */
function blocks(pct: number, width = 18) {
  const fill = Math.round((pct / 100) * width);
  return "█".repeat(fill) + "░".repeat(width - fill);
}

/** Task list — a terminal. Mono face, green phosphor on near-black, ASCII
 *  checkboxes and block progress, no cards. The todo-cli idiom. */
export function TaskListScreen({
  list = "today",
  tasks = DEFAULT_TASKS,
  onToggle,
}: TaskListScreenProps) {
  const haptics = useHaptic();
  const insets = useSafeAreaInsets();
  const [doneSet, setDoneSet] = React.useState<Set<string>>(
    () => new Set(tasks.filter((t) => t.done).map((t) => t.id)),
  );

  const total = tasks.length;
  const done = doneSet.size;
  const pct = total ? (done / total) * 100 : 0;

  const toggle = (id: string) => {
    const next = !doneSet.has(id);
    haptics.selection();
    setDoneSet((s) => {
      const ns = new Set(s);
      if (next) ns.add(id);
      else ns.delete(id);
      return ns;
    });
    onToggle?.(id, next);
  };

  const dueToday = (t: Task) =>
    /today|\d/.test(t.detail.split("·")[0] ?? "");
  const today = tasks.filter(dueToday);
  const upcoming = tasks.filter((t) => !dueToday(t));

  const Section = ({ title, rows, delay }: { title: string; rows: Task[]; delay: number }) => (
    <Mount delay={delay} className="px-5 pt-6">
      <Text
        className="pb-2 text-[11px] uppercase tracking-[0.2em]"
        style={{ color: DIM, fontFamily: MONO }}
      >
        {`# ${title}`}
      </Text>
      <View className="border-t" style={{ borderColor: FAINT }}>
        {rows.map((t) => (
          <Press
            key={t.id}
            haptic="light"
            onPress={() => toggle(t.id)}
            accessibilityRole="checkbox"
            accessibilityState={{ checked: doneSet.has(t.id) }}
            accessibilityLabel={`${t.title}, ${t.detail}`}
            className="flex-row items-baseline gap-3 border-b py-3"
            style={{ borderColor: FAINT }}
          >
            <Text
              className="w-6 text-[13px]"
              style={{
                color: doneSet.has(t.id) ? GREEN : DIM,
                fontFamily: MONO,
              }}
            >
              {doneSet.has(t.id) ? "[x]" : "[ ]"}
            </Text>
            <View className="flex-1">
              <Text
                className="text-[13px]"
                style={{
                  color: doneSet.has(t.id) ? DIM : "#D9E4DB",
                  fontFamily: MONO,
                  textDecorationLine: doneSet.has(t.id) ? "line-through" : "none",
                }}
              >
                {t.title}
                {t.flagged ? "  !" : ""}
              </Text>
            </View>
            <Text
              className="text-[11px]"
              style={{ color: "#4A5A4C", fontFamily: MONO }}
            >
              {t.detail}
            </Text>
          </Press>
        ))}
      </View>
    </Mount>
  );

  return (
    <World primary={GREEN}>
    <SafeAreaView className="flex-1 bg-[#0C0F0A] dark:bg-[#0C0F0A]" edges={["top", "bottom"]}>
      <GlowSpot color="#4ADE80" size={300} className="-top-20 right-[-80px]" />
      <GlowSpot color="#4ADE80" size={220} className="bottom-[30%] left-[-100px]" />
      <ScrollView
        showsVerticalScrollIndicator={false}
        contentContainerStyle={{ paddingBottom: insets.bottom + 96 }}
      >
        {/* Shell header — the prompt line + block progress. */}
        <Mount className="px-5 pt-4">
          <Text
            className="text-[13px]"
            style={{ color: DIM, fontFamily: MONO }}
            accessibilityElementsHidden
            importantForAccessibility="no"
          >
            {"$ tasks --"}{list}
          </Text>
          <View className="mt-4 flex-row items-end justify-between">
            <Text
              className="text-[30px] font-bold tracking-tight"
              style={{ color: "#E8F0EA", fontFamily: MONO }}
              accessibilityRole="header"
            >
              {done}/{total}
            </Text>
            <Text className="text-[11px]" style={{ color: DIM, fontFamily: MONO }}>
              {Math.round(pct)}% complete
            </Text>
          </View>
          <Text
            className="mt-1 text-[13px] tracking-[2px]"
            style={{ color: GREEN, fontFamily: MONO }}
            accessibilityElementsHidden
            importantForAccessibility="no"
          >
            {blocks(pct)}
          </Text>
          {/* Uptime line — terminals always answer "how's it running". */}
          <Text
            className="mt-2 text-[11px]"
            style={{ color: DIM, fontFamily: MONO }}
          >
            {`$ streak 12d · ${today.length} queued`}
          </Text>
        </Mount>

        <Section title="due today" rows={today} delay={60} />
        {upcoming.length > 0 ? (
          <Section title="upcoming" rows={upcoming} delay={140} />
        ) : null}

        {/* Input line — the blinking-cursor affordance. */}
        <Mount delay={200} className="px-5 pt-8">
          <View className="flex-row items-center gap-2">
            <Text className="text-[13px]" style={{ color: GREEN, fontFamily: MONO }}>
              {">"}
            </Text>
            <Text className="text-[13px]" style={{ color: DIM, fontFamily: MONO }}>
              add task…
            </Text>
            <View className="h-4 w-2" style={{ backgroundColor: GREEN }} />
          </View>
        </Mount>
      </ScrollView>
    </SafeAreaView>
    </World>
  );
}
TaskListScreen.displayName = "TaskListScreen";
agent prompt — paste into Claude Code, Cursor, Devin…
Recreate the "Task List Screen" screen in React Native using LumioUI.

Goal: Checkable tasks with progress 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-task-list
```

Or pull just the components: `npx lumioui add button checkbox icon list progress 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

### `checkbox` — Animated check box with haptic tick and error state.
- imported but not rendered directly — likely used through a sibling or as a utility

### `icon` — Lucide icon set mapped to theme colors and sizes.
- imported but not rendered directly — likely used through a sibling or as a utility

### `list` — Settings-style rows with separators and press feedback.
- imported but not rendered directly — likely used through a sibling or as a utility

### `progress` — Animated fill bar with indeterminate sweep mode.
- imported but not rendered directly — likely used through a sibling or as a utility

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

## 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/task-list.tsx` exporting TaskListScreen. 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 { Text } from "@lumioui/ui";
import { useHaptic } from "@lumioui/core";
import { GlowSpot, MONO, Mount, Press, World } from "../shared";

export interface Task {
  id: string;
  title: string;
  detail: string;
  done?: boolean;
  flagged?: boolean;
}

export interface TaskListScreenProps {
  list?: string;
  tasks?: Task[];
  onToggle?: (id: string, done: boolean) => void;
}

const DEFAULT_TASKS: Task[] = [
  { id: "t1", title: "Review onboarding screens", detail: "today · design", flagged: true },
  { id: "t2", title: "Reply to Sarah's proposal", detail: "today · work", done: true },
  { id: "t3", title: "Buy groceries", detail: "18:00 · errands" },
  { id: "t4", title: "Call plumber re: sink", detail: "tomorrow · home" },
  { id: "t5", title: "Book dentist check-up", detail: "this week" },
  { id: "t6", title: "Send invoice to Forma", detail: "today · work" },
  { id: "t7", title: "Renew gym membership", detail: "friday · health" },
  { id: "t8", title: "Plan Lisbon itinerary", detail: "this week · travel" },
  { id: "t9", title: "Read 20 pages", detail: "nightly · personal" },
];

const GREEN = "#4ADE80";
const DIM = "#5C6B5E";
const FAINT = "#334036";

/** Text progress bar — block glyphs, the terminal convention. */
function blocks(pct: number, width = 18) {
  const fill = Math.round((pct / 100) * width);
  return "█".repeat(fill) + "░".repeat(width - fill);
}

/** Task list — a terminal. Mono face, green phosphor on near-black, ASCII
 *  checkboxes and block progress, no cards. The todo-cli idiom. */
export function TaskListScreen({
  list = "today",
  tasks = DEFAULT_TASKS,
  onToggle,
}: TaskListScreenProps) {
  const haptics = useHaptic();
  const insets = useSafeAreaInsets();
  const [doneSet, setDoneSet] = React.useState<Set<string>>(
    () => new Set(tasks.filter((t) => t.done).map((t) => t.id)),
  );

  const total = tasks.length;
  const done = doneSet.size;
  const pct = total ? (done / total) * 100 : 0;

  const toggle = (id: string) => {
    const next = !doneSet.has(id);
    haptics.selection();
    setDoneSet((s) => {
      const ns = new Set(s);
      if (next) ns.add(id);
      else ns.delete(id);
      return ns;
    });
    onToggle?.(id, next);
  };

  const dueToday = (t: Task) =>
    /today|\d/.test(t.detail.split("·")[0] ?? "");
  const today = tasks.filter(dueToday);
  const upcoming = tasks.filter((t) => !dueToday(t));

  const Section = ({ title, rows, delay }: { title: string; rows: Task[]; delay: number }) => (
    <Mount delay={delay} className="px-5 pt-6">
      <Text
        className="pb-2 text-[11px] uppercase tracking-[0.2em]"
        style={{ color: DIM, fontFamily: MONO }}
      >
        {`# ${title}`}
      </Text>
      <View className="border-t" style={{ borderColor: FAINT }}>
        {rows.map((t) => (
          <Press
            key={t.id}
            haptic="light"
            onPress={() => toggle(t.id)}
            accessibilityRole="checkbox"
            accessibilityState={{ checked: doneSet.has(t.id) }}
            accessibilityLabel={`${t.title}, ${t.detail}`}
            className="flex-row items-baseline gap-3 border-b py-3"
            style={{ borderColor: FAINT }}
          >
            <Text
              className="w-6 text-[13px]"
              style={{
                color: doneSet.has(t.id) ? GREEN : DIM,
                fontFamily: MONO,
              }}
            >
              {doneSet.has(t.id) ? "[x]" : "[ ]"}
            </Text>
            <View className="flex-1">
              <Text
                className="text-[13px]"
                style={{
                  color: doneSet.has(t.id) ? DIM : "#D9E4DB",
                  fontFamily: MONO,
                  textDecorationLine: doneSet.has(t.id) ? "line-through" : "none",
                }}
              >
                {t.title}
                {t.flagged ? "  !" : ""}
              </Text>
            </View>
            <Text
              className="text-[11px]"
              style={{ color: "#4A5A4C", fontFamily: MONO }}
            >
              {t.detail}
            </Text>
          </Press>
        ))}
      </View>
    </Mount>
  );

  return (
    <World primary={GREEN}>
    <SafeAreaView className="flex-1 bg-[#0C0F0A] dark:bg-[#0C0F0A]" edges={["top", "bottom"]}>
      <GlowSpot color="#4ADE80" size={300} className="-top-20 right-[-80px]" />
      <GlowSpot color="#4ADE80" size={220} className="bottom-[30%] left-[-100px]" />
      <ScrollView
        showsVerticalScrollIndicator={false}
        contentContainerStyle={{ paddingBottom: insets.bottom + 96 }}
      >
        {/* Shell header — the prompt line + block progress. */}
        <Mount className="px-5 pt-4">
          <Text
            className="text-[13px]"
            style={{ color: DIM, fontFamily: MONO }}
            accessibilityElementsHidden
            importantForAccessibility="no"
          >
            {"$ tasks --"}{list}
          </Text>
          <View className="mt-4 flex-row items-end justify-between">
            <Text
              className="text-[30px] font-bold tracking-tight"
              style={{ color: "#E8F0EA", fontFamily: MONO }}
              accessibilityRole="header"
            >
              {done}/{total}
            </Text>
            <Text className="text-[11px]" style={{ color: DIM, fontFamily: MONO }}>
              {Math.round(pct)}% complete
            </Text>
          </View>
          <Text
            className="mt-1 text-[13px] tracking-[2px]"
            style={{ color: GREEN, fontFamily: MONO }}
            accessibilityElementsHidden
            importantForAccessibility="no"
          >
            {blocks(pct)}
          </Text>
          {/* Uptime line — terminals always answer "how's it running". */}
          <Text
            className="mt-2 text-[11px]"
            style={{ color: DIM, fontFamily: MONO }}
          >
            {`$ streak 12d · ${today.length} queued`}
          </Text>
        </Mount>

        <Section title="due today" rows={today} delay={60} />
        {upcoming.length > 0 ? (
          <Section title="upcoming" rows={upcoming} delay={140} />
        ) : null}

        {/* Input line — the blinking-cursor affordance. */}
        <Mount delay={200} className="px-5 pt-8">
          <View className="flex-row items-center gap-2">
            <Text className="text-[13px]" style={{ color: GREEN, fontFamily: MONO }}>
              {">"}
            </Text>
            <Text className="text-[13px]" style={{ color: DIM, fontFamily: MONO }}>
              add task…
            </Text>
            <View className="h-4 w-2" style={{ backgroundColor: GREEN }} />
          </View>
        </Mount>
      </ScrollView>
    </SafeAreaView>
    </World>
  );
}
TaskListScreen.displayName = "TaskListScreen";
```

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