GTD Task Inbox
A capture-first inbox, daily focus view, and upcoming timeline — the three-view core of a GTD workflow. Used in OrgFlo.
Overview
Three cooperating views follow David Allen's capture-clarify-organise workflow:
Inbox
Zero-friction capture. Tasks with no project land here for later triage.
Today
Tasks due today or overdue. Drives daily focus without manual priority tags.
Upcoming
Future tasks grouped by date. Surfaces commitments before they become urgent.
Data is modelled as Area → Project → Task. The inbox is defined by projectID == nil — no separate flag required.
Inbox Preview
Quick-capture bar
A persistent bottom toolbar hosts the capture field. Pressing Return inserts an OFTask with no project. The field clears instantly — the task appears at the top of the list.
Triage flow
- → Tap a task to open the detail panel (NavigationSplitView on iPad/macOS)
- → Assign an Area, Project, and due date to move it out of the inbox
- → Inbox count decrements in real time — zero shows an "All clear" empty state
Code
SwiftUI tabs show InboxView/TodayView (iOS) and the OFTask SwiftData model (macOS tab). React and HTML tabs show equivalent web implementations.
"use client";
import { useState, useId } from "react";
interface Task {
id: string;
title: string;
dueDate?: string;
projectId?: string;
completed: boolean;
}
// React GTD inbox — local state version; swap useState for a server action / Zustand store.
export function TaskInbox() {
const [tasks, setTasks] = useState<Task[]>([
{ id: "1", title: "Research competitor pricing", completed: false },
{ id: "2", title: "Draft Q3 retro agenda", completed: false },
{ id: "3", title: "Follow up with contractor", dueDate: new Date().toISOString(), completed: false },
]);
const [draft, setDraft] = useState("");
// Inbox = tasks with no project assignment
const inbox = tasks.filter((t) => !t.projectId && !t.completed);
const capture = () => {
if (!draft.trim()) return;
setTasks((prev) => [...prev, { id: crypto.randomUUID(), title: draft.trim(), completed: false }]);
setDraft("");
};
const toggle = (id: string) =>
setTasks((prev) => prev.map((t) => (t.id === id ? { ...t, completed: !t.completed } : t)));
return (
<div style={{ maxWidth: 400, fontFamily: "system-ui, sans-serif" }}>
{/* Header */}
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", padding: "14px 0 10px" }}>
<h2 style={{ fontSize: 20, fontWeight: 700, margin: 0 }}>Inbox</h2>
<span style={{ fontSize: 12, fontWeight: 600, padding: "2px 10px", borderRadius: 99, background: "rgba(52,168,101,0.15)", color: "#34a865" }}>
{inbox.length} items
</span>
</div>
{/* Task list */}
<ul style={{ listStyle: "none", padding: 0, margin: 0, borderRadius: 12, overflow: "hidden", border: "1px solid var(--border, #333)" }}>
{inbox.length === 0 && (
<li style={{ padding: "24px 16px", textAlign: "center", color: "#888", fontSize: 14 }}>All clear ✓</li>
)}
{inbox.map((task, i) => (
<li
key={task.id}
style={{
display: "flex", alignItems: "flex-start", gap: 12, padding: "12px 16px",
borderBottom: i < inbox.length - 1 ? "1px solid var(--border-subtle, #222)" : undefined,
}}
>
<button
onClick={() => toggle(task.id)}
aria-label={task.completed ? "Mark incomplete" : "Mark complete"}
style={{ width: 20, height: 20, borderRadius: "50%", border: "2px solid #555", background: "transparent", cursor: "pointer", marginTop: 2, flexShrink: 0 }}
/>
<div style={{ flex: 1 }}>
<div style={{ fontSize: 14, color: "var(--text-primary, #f2f2f6)" }}>{task.title}</div>
{task.dueDate && (
<div style={{ fontSize: 11, color: new Date(task.dueDate) < new Date() ? "#f87171" : "#888", marginTop: 2 }}>
Due {new Date(task.dueDate).toLocaleDateString()}
</div>
)}
</div>
</li>
))}
</ul>
{/* Quick-capture bar */}
<div style={{ marginTop: 8, display: "flex", gap: 8 }}>
<input
value={draft}
onChange={(e) => setDraft(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && capture()}
placeholder="Capture…"
style={{ flex: 1, padding: "8px 12px", borderRadius: 8, border: "1px solid #333", background: "transparent", color: "inherit", fontSize: 14 }}
/>
{draft && (
<button onClick={capture} style={{ padding: "8px 14px", borderRadius: 8, background: "#34a865", color: "#fff", border: "none", cursor: "pointer", fontWeight: 600, fontSize: 14 }}>
Add
</button>
)}
</div>
</div>
);
}Sync & Persistence
| Layer | Responsibility |
|---|---|
| SwiftData store | On-device persistence; @Query predicates define inbox / today / upcoming filters |
| SyncService | Batches changed object IDs, diffs to CloudKit private database, last-write-wins on modifiedAt |
| WCSession | Pushes habit summary to watchOS companion; receives completion events |
Accessibility
- →Completion toggle uses
.contentTransition(.symbolEffect(.replace)); VoiceOver announces via.accessibilityValue - →Overdue dates render red and also carry
.accessibilityLabel("Overdue, date")so colour is not the only signal - →Capture field uses
.submitLabel(.done)— keyboard stays up for rapid multi-item entry