diff --git a/app/src/app/add.tsx b/app/src/app/add.tsx
index 73506e0..f3ca933 100644
--- a/app/src/app/add.tsx
+++ b/app/src/app/add.tsx
@@ -1,6 +1,7 @@
import { useRouter } from "expo-router";
import { useState } from "react";
import { Pressable, ScrollView, StyleSheet, Text, View } from "react-native";
+import DateField from "../components/DateField";
import PriorityChips from "../components/PriorityChips";
import { Chips, Field } from "../components/ui";
import { haptic } from "../lib/haptics";
@@ -17,12 +18,6 @@ const BOXES: { key: TaskStatus; label: string }[] = [
const ESTIMATES = [15, 30, 45, 60, 90, 120];
-function isoToday(offset = 0): string {
- const d = new Date();
- d.setDate(d.getDate() + offset);
- return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
-}
-
export default function AddTask() {
const router = useRouter();
const store = useStore();
@@ -69,28 +64,7 @@ export default function AddTask() {
style={{ minHeight: 64, textAlignVertical: "top" }}
/>
-
- Due date
-
- {[
- { label: "Today", v: isoToday() },
- { label: "Tomorrow", v: isoToday(1) },
- { label: "Next week", v: isoToday(7) },
- ].map(({ label, v }) => (
- {
- haptic.select();
- setDue(due === v ? "" : v);
- }}
- >
- {label}
-
- ))}
-
-
-
+ setDue(v ?? "")} />
Estimated time
diff --git a/app/src/app/task/[id].tsx b/app/src/app/task/[id].tsx
index 95ff40a..8045d86 100644
--- a/app/src/app/task/[id].tsx
+++ b/app/src/app/task/[id].tsx
@@ -1,6 +1,7 @@
import { useLocalSearchParams, useRouter } from "expo-router";
import { useState } from "react";
import { ScrollView, Text } from "react-native";
+import DateField from "../../components/DateField";
import PriorityChips from "../../components/PriorityChips";
import { Button, Chips, Field } from "../../components/ui";
import { activeProjects, trashTask, updateTask } from "../../lib/store";
@@ -76,20 +77,8 @@ export default function TaskDetail() {
placeholder="Who or what?"
/>
)}
- set({ due_date: v || null })}
- placeholder="YYYY-MM-DD"
- autoCapitalize="none"
- />
- set({ defer_date: v || null })}
- placeholder="YYYY-MM-DD"
- autoCapitalize="none"
- />
+ set({ due_date: v })} clearable />
+ set({ defer_date: v })} clearable />
void }) {
+ const initial = value ? fromIso(value) : new Date();
+ const [year, setYear] = useState(initial.getFullYear());
+ const [month, setMonth] = useState(initial.getMonth());
+ const todayIso = iso(new Date());
+
+ const weeks = useMemo(() => {
+ const first = new Date(year, month, 1);
+ const start = new Date(first);
+ start.setDate(1 - ((first.getDay() + 6) % 7)); // back to Monday
+ return Array.from({ length: 6 }, (_, w) =>
+ Array.from({ length: 7 }, (_, i) => {
+ const d = new Date(start);
+ d.setDate(start.getDate() + w * 7 + i);
+ return d;
+ }),
+ );
+ }, [year, month]);
+
+ const shift = (delta: number) => {
+ haptic.select();
+ const d = new Date(year, month + delta, 1);
+ setYear(d.getFullYear());
+ setMonth(d.getMonth());
+ };
+
+ return (
+
+
+ shift(-1)} hitSlop={10} style={s.arrow}>
+ ‹
+
+
+ {MONTHS[month]} {year}
+
+ shift(1)} hitSlop={10} style={s.arrow}>
+ ›
+
+
+
+ {WEEKDAYS.map((w, i) => (
+
+ {w}
+
+ ))}
+
+ {weeks.map((week, wi) => (
+
+ {week.map((d) => {
+ const dIso = iso(d);
+ const inMonth = d.getMonth() === month;
+ const selected = dIso === value;
+ const isToday = dIso === todayIso;
+ return (
+ {
+ haptic.select();
+ onPick(dIso);
+ }}
+ >
+
+ {d.getDate()}
+
+
+ );
+ })}
+
+ ))}
+
+ );
+}
+
+export default function DateField({
+ label,
+ value,
+ onChange,
+ clearable,
+}: {
+ label: string;
+ value: string | null;
+ onChange: (v: string | null) => void;
+ clearable?: boolean;
+}) {
+ const [open, setOpen] = useState(false);
+ const today = new Date();
+ const quick = [
+ { label: "Today", v: iso(today) },
+ { label: "Tomorrow", v: iso(new Date(today.getTime() + DAY_MS)) },
+ { label: "Next week", v: iso(new Date(today.getTime() + 7 * DAY_MS)) },
+ ];
+
+ return (
+
+
+ {label}
+ {value != null && {pretty(value)}}
+
+
+ {quick.map((q) => (
+ {
+ haptic.select();
+ onChange(value === q.v && clearable ? null : q.v);
+ }}
+ >
+ {q.label}
+
+ ))}
+ {
+ haptic.select();
+ setOpen(!open);
+ }}
+ >
+ 📆 Pick…
+
+ {clearable && value != null && (
+ {
+ haptic.tap();
+ onChange(null);
+ }}
+ >
+ Clear
+
+ )}
+
+ {open && (
+ {
+ onChange(v);
+ setOpen(false);
+ }}
+ />
+ )}
+
+ );
+}
+
+const s = StyleSheet.create({
+ labelRow: { flexDirection: "row", justifyContent: "space-between", alignItems: "baseline", marginBottom: 6 },
+ label: { color: C.muted, fontSize: 13, textTransform: "uppercase", letterSpacing: 0.5 },
+ chosen: { color: C.accent, fontSize: 14, fontWeight: "600" },
+ rowWrap: { flexDirection: "row", flexWrap: "wrap", gap: 8 },
+ chip: {
+ paddingVertical: 8,
+ paddingHorizontal: 14,
+ borderRadius: 20,
+ backgroundColor: C.surface,
+ borderWidth: 1,
+ borderColor: C.border,
+ },
+ chipActive: { backgroundColor: C.surface2, borderColor: C.accent },
+ chipText: { color: C.muted, fontSize: 14 },
+ calendar: {
+ marginTop: 10,
+ backgroundColor: C.surface,
+ borderRadius: 14,
+ borderWidth: 1,
+ borderColor: C.border,
+ padding: 10,
+ },
+ calHead: { flexDirection: "row", justifyContent: "space-between", alignItems: "center", marginBottom: 6 },
+ calTitle: { color: C.text, fontSize: 15, fontWeight: "600" },
+ arrow: { paddingHorizontal: 14, paddingVertical: 4 },
+ arrowText: { color: C.accent, fontSize: 22, fontWeight: "600" },
+ week: { flexDirection: "row" },
+ weekday: { flex: 1, textAlign: "center", color: C.muted, fontSize: 11, paddingVertical: 4 },
+ day: {
+ flex: 1,
+ aspectRatio: 1.15,
+ alignItems: "center",
+ justifyContent: "center",
+ borderRadius: 999,
+ margin: 1,
+ },
+ daySelected: { backgroundColor: C.accent },
+ dayToday: { borderWidth: 1, borderColor: C.accent },
+ dayText: { color: C.text, fontSize: 14 },
+});