Quadrant home screen: Next/Waiting/Someday/Projects grid replaces bottom tabs
Some checks failed
Build & Release APK / build (push) Has been cancelled

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-07 19:10:59 -07:00
parent d48fbbce37
commit 4d6031d007
9 changed files with 206 additions and 61 deletions

View File

@@ -1,36 +0,0 @@
import { Link, Tabs } from "expo-router";
import { Text } from "react-native";
import { C } from "../../lib/theme";
function icon(glyph: string) {
return ({ focused }: { focused: boolean }) => (
<Text style={{ fontSize: 20, opacity: focused ? 1 : 0.45 }}>{glyph}</Text>
);
}
export default function TabsLayout() {
return (
<Tabs
screenOptions={{
headerStyle: { backgroundColor: C.bg },
headerTintColor: C.text,
headerShadowVisible: false,
sceneStyle: { backgroundColor: C.bg },
tabBarStyle: { backgroundColor: C.bg, borderTopColor: C.border },
tabBarActiveTintColor: C.text,
tabBarInactiveTintColor: C.muted,
headerRight: () => (
<Link href="/settings" style={{ paddingHorizontal: 16, fontSize: 18 }}>
</Link>
),
}}
>
<Tabs.Screen name="index" options={{ title: "Inbox", tabBarIcon: icon("📥") }} />
<Tabs.Screen name="next" options={{ title: "Next", tabBarIcon: icon("▶️") }} />
<Tabs.Screen name="waiting" options={{ title: "Waiting", tabBarIcon: icon("⏳") }} />
<Tabs.Screen name="someday" options={{ title: "Someday", tabBarIcon: icon("💭") }} />
<Tabs.Screen name="projects" options={{ title: "Projects", tabBarIcon: icon("📁") }} />
</Tabs>
);
}

View File

@@ -1,5 +0,0 @@
import TaskListScreen from "../../components/TaskListScreen";
export default function Inbox() {
return <TaskListScreen status="inbox" capture emptyHint="Inbox zero. Capture anything above." />;
}

View File

@@ -1,5 +0,0 @@
import TaskListScreen from "../../components/TaskListScreen";
export default function Next() {
return <TaskListScreen status="next" capture emptyHint="No next actions. Clarify your inbox." />;
}

View File

@@ -1,5 +0,0 @@
import TaskListScreen from "../../components/TaskListScreen";
export default function Someday() {
return <TaskListScreen status="someday" emptyHint="No someday/maybe items." />;
}

View File

@@ -1,5 +0,0 @@
import TaskListScreen from "../../components/TaskListScreen";
export default function Waiting() {
return <TaskListScreen status="waiting" emptyHint="Not waiting on anyone." />;
}

View File

@@ -1,4 +1,4 @@
import { Stack } from "expo-router"; import { Link, Stack } from "expo-router";
import { StatusBar } from "expo-status-bar"; import { StatusBar } from "expo-status-bar";
import { C } from "../lib/theme"; import { C } from "../lib/theme";
@@ -14,7 +14,19 @@ export default function RootLayout() {
contentStyle: { backgroundColor: C.bg }, contentStyle: { backgroundColor: C.bg },
}} }}
> >
<Stack.Screen name="(tabs)" options={{ headerShown: false }} /> <Stack.Screen
name="index"
options={{
title: "GTD",
headerRight: () => (
<Link href="/settings" style={{ paddingHorizontal: 8, fontSize: 18 }}>
</Link>
),
}}
/>
<Stack.Screen name="list/[status]" />
<Stack.Screen name="projects" options={{ title: "Projects" }} />
<Stack.Screen name="task/[id]" options={{ title: "Clarify", presentation: "modal" }} /> <Stack.Screen name="task/[id]" options={{ title: "Clarify", presentation: "modal" }} />
<Stack.Screen name="project/[id]" options={{ title: "Project" }} /> <Stack.Screen name="project/[id]" options={{ title: "Project" }} />
<Stack.Screen name="settings" options={{ title: "Settings", presentation: "modal" }} /> <Stack.Screen name="settings" options={{ title: "Settings", presentation: "modal" }} />

157
app/src/app/index.tsx Normal file
View File

@@ -0,0 +1,157 @@
import { useFocusEffect, useRouter } from "expo-router";
import { useCallback, useState } from "react";
import { Pressable, StyleSheet, Text, TextInput, View } from "react-native";
import { api } from "../lib/api";
import { C } from "../lib/theme";
import type { Project, Task } from "../lib/types";
const Q = {
next: { title: "Next up", color: "#3DBF77", bg: "#15301F", border: "#1E4D33" },
waiting: { title: "Waiting for", color: "#E0A93E", bg: "#33270F", border: "#544019" },
someday: { title: "Someday", color: "#9D8FE8", bg: "#221E38", border: "#38315C" },
projects: { title: "Projects", color: "#5B9DE8", bg: "#152538", border: "#1F3D5C" },
};
function Quadrant({
kind,
count,
preview,
onPress,
}: {
kind: keyof typeof Q;
count: number;
preview: string[];
onPress: () => void;
}) {
const q = Q[kind];
return (
<Pressable
style={[s.quad, { backgroundColor: q.bg, borderColor: q.border }]}
onPress={onPress}
>
<View style={s.quadHead}>
<Text style={[s.quadTitle, { color: q.color }]}>{q.title}</Text>
<Text style={[s.quadCount, { color: q.color }]}>{count}</Text>
</View>
{preview.map((p, i) => (
<Text key={i} style={s.quadItem} numberOfLines={1}>
{p}
</Text>
))}
</Pressable>
);
}
export default function Home() {
const router = useRouter();
const [tasks, setTasks] = useState<Task[]>([]);
const [projects, setProjects] = useState<Project[]>([]);
const [draft, setDraft] = useState("");
const [error, setError] = useState<string | null>(null);
const load = useCallback(async () => {
try {
const [ts, ps] = await Promise.all([api.tasks.list(), api.projects.list()]);
setTasks(ts);
setProjects(ps.filter((p) => p.status === "active"));
setError(null);
} catch (e) {
setError(e instanceof Error ? e.message : String(e));
}
}, []);
useFocusEffect(
useCallback(() => {
load();
}, [load]),
);
const capture = async () => {
const title = draft.trim();
if (!title) return;
setDraft("");
try {
await api.tasks.create({ title });
} finally {
load();
}
};
const byStatus = (st: string) => tasks.filter((t) => t.status === st);
const inboxCount = byStatus("inbox").length;
const titles = (st: string) => byStatus(st).slice(0, 3).map((t) => t.title);
return (
<View style={s.screen}>
<TextInput
style={s.capture}
placeholder="Capture anything…"
placeholderTextColor={C.muted}
value={draft}
onChangeText={setDraft}
onSubmitEditing={capture}
submitBehavior="submit"
returnKeyType="done"
/>
<Pressable style={s.inboxPill} onPress={() => router.push("/list/inbox")}>
<Text style={s.inboxText}>📥 Inbox</Text>
<Text style={[s.inboxText, { color: inboxCount ? C.text : C.muted }]}>{inboxCount}</Text>
</Pressable>
{error && <Text style={s.error}>{error}</Text>}
<View style={s.grid}>
<View style={s.gridRow}>
<Quadrant kind="next" count={byStatus("next").length} preview={titles("next")} onPress={() => router.push("/list/next")} />
<Quadrant kind="waiting" count={byStatus("waiting").length} preview={titles("waiting")} onPress={() => router.push("/list/waiting")} />
</View>
<View style={s.gridRow}>
<Quadrant kind="someday" count={byStatus("someday").length} preview={titles("someday")} onPress={() => router.push("/list/someday")} />
<Quadrant
kind="projects"
count={projects.length}
preview={projects.slice(0, 3).map((p) => p.name)}
onPress={() => router.push("/projects")}
/>
</View>
</View>
</View>
);
}
const s = StyleSheet.create({
screen: { flex: 1, backgroundColor: C.bg, padding: 12 },
capture: {
padding: 14,
borderRadius: 12,
backgroundColor: C.surface,
color: C.text,
fontSize: 16,
borderWidth: 1,
borderColor: C.border,
},
inboxPill: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
marginTop: 10,
paddingVertical: 12,
paddingHorizontal: 16,
borderRadius: 12,
backgroundColor: C.surface,
borderWidth: 1,
borderColor: C.border,
},
inboxText: { color: C.text, fontSize: 16, fontWeight: "600" },
grid: { flex: 1, marginTop: 10, gap: 10 },
gridRow: { flex: 1, flexDirection: "row", gap: 10 },
quad: {
flex: 1,
borderRadius: 16,
borderWidth: 1,
padding: 14,
},
quadHead: { flexDirection: "row", justifyContent: "space-between", marginBottom: 8 },
quadTitle: { fontSize: 17, fontWeight: "700" },
quadCount: { fontSize: 17, fontWeight: "700" },
quadItem: { color: C.muted, fontSize: 13, marginBottom: 4 },
error: { color: C.danger, marginTop: 8 },
});

View File

@@ -0,0 +1,32 @@
import { Stack, useLocalSearchParams } from "expo-router";
import TaskListScreen from "../../components/TaskListScreen";
import type { TaskStatus } from "../../lib/types";
const TITLES: Record<string, string> = {
inbox: "Inbox",
next: "Next up",
waiting: "Waiting for",
scheduled: "Scheduled",
someday: "Someday",
done: "Done",
};
const HINTS: Record<string, string> = {
inbox: "Inbox zero. Capture from the home screen.",
next: "No next actions. Clarify your inbox.",
waiting: "Not waiting on anyone.",
scheduled: "Nothing scheduled.",
someday: "No someday/maybe items.",
done: "Nothing done yet.",
};
export default function ListByStatus() {
const { status } = useLocalSearchParams<{ status: string }>();
const st = (status in TITLES ? status : "inbox") as TaskStatus;
return (
<>
<Stack.Screen options={{ title: TITLES[st] }} />
<TaskListScreen status={st} capture={st === "inbox" || st === "next"} emptyHint={HINTS[st]} />
</>
);
}

View File

@@ -1,9 +1,9 @@
import { useFocusEffect, useRouter } from "expo-router"; import { useFocusEffect, useRouter } from "expo-router";
import { useCallback, useState } from "react"; import { useCallback, useState } from "react";
import { FlatList, Pressable, StyleSheet, Text, TextInput, View } from "react-native"; import { FlatList, Pressable, StyleSheet, Text, TextInput, View } from "react-native";
import { api } from "../../lib/api"; import { api } from "../lib/api";
import { C } from "../../lib/theme"; import { C } from "../lib/theme";
import type { Project } from "../../lib/types"; import type { Project } from "../lib/types";
export default function Projects() { export default function Projects() {
const router = useRouter(); const router = useRouter();