UI: restyle Today, Add and Projects onto the new tokens

Also fixes Field silently dropping its base input style when a caller
passed their own (the multiline Description box was unstyled).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Claude
2026-09-01 22:37:26 +10:00
parent 1af86cd27c
commit dff9142e2e
4 changed files with 122 additions and 51 deletions

View File

@@ -9,7 +9,7 @@ import { haptic } from "../lib/haptics";
import { parseQuickAdd, type ParsedQuickAdd } from "../lib/quickadd"; import { parseQuickAdd, type ParsedQuickAdd } from "../lib/quickadd";
import { REPEAT_LABEL, REPEATS } from "../lib/repeat"; import { REPEAT_LABEL, REPEATS } from "../lib/repeat";
import { activeProjects, createTask } from "../lib/store"; import { activeProjects, createTask } from "../lib/store";
import { C } from "../lib/theme"; import { C, radius, space, type } from "../lib/theme";
import { useStore } from "../lib/useStore"; import { useStore } from "../lib/useStore";
import type { Repeat, Task, TaskStatus } from "../lib/types"; import type { Repeat, Task, TaskStatus } from "../lib/types";
@@ -81,7 +81,12 @@ export default function AddTask() {
}; };
return ( return (
<ScrollView style={{ flex: 1, backgroundColor: C.bg }} contentContainerStyle={{ padding: 16 }}> <ScrollView
style={{ flex: 1, backgroundColor: C.bg }}
contentContainerStyle={s.content}
keyboardShouldPersistTaps="handled"
showsVerticalScrollIndicator={false}
>
<Field <Field
label="Task" label="Task"
value={raw} value={raw}
@@ -134,10 +139,14 @@ export default function AddTask() {
onChangeText={setNotes} onChangeText={setNotes}
placeholder="Details, links, notes…" placeholder="Details, links, notes…"
multiline multiline
style={{ minHeight: 64, textAlignVertical: "top" }} style={{ minHeight: 88, textAlignVertical: "top" }}
/> />
<Pressable style={[s.save, !ready && s.saveDisabled]} onPress={save} disabled={!ready}> <Pressable
<Text style={[s.saveText, !ready && { color: C.muted }]}> style={({ pressed }) => [s.save, !ready && s.saveDisabled, pressed && ready && { opacity: 0.8 }]}
onPress={save}
disabled={!ready}
>
<Text style={[s.saveText, !ready && s.saveTextDisabled]}>
{ready ? "Add task" : "Needs a title, priority & due date"} {ready ? "Add task" : "Needs a title, priority & due date"}
</Text> </Text>
</Pressable> </Pressable>
@@ -146,15 +155,23 @@ export default function AddTask() {
} }
const s = StyleSheet.create({ const s = StyleSheet.create({
parsedHint: { color: C.accent, fontSize: 13, marginTop: -8, marginBottom: 12 }, content: { padding: space.lg, paddingBottom: space.xxl },
parsedHint: {
...type.meta,
color: C.accent,
marginTop: -space.md,
marginBottom: space.md,
fontWeight: "500",
},
save: { save: {
backgroundColor: C.accent, backgroundColor: C.accent,
borderRadius: 10, borderRadius: radius.md,
padding: 14, paddingVertical: space.lg - 2,
alignItems: "center", alignItems: "center",
marginTop: 8, marginTop: space.sm,
marginBottom: 24, marginBottom: space.xxl,
}, },
saveDisabled: { backgroundColor: C.surface }, saveDisabled: { backgroundColor: C.surface2 },
saveText: { color: C.text, fontSize: 16, fontWeight: "600" }, saveText: { fontSize: 16, fontWeight: "600", color: C.onAccent },
saveTextDisabled: { color: C.muted },
}); });

View File

@@ -1,8 +1,11 @@
import { useRouter } from "expo-router"; import { useRouter } from "expo-router";
import { useState } from "react"; import { useState } from "react";
import { FlatList, Pressable, StyleSheet, Text, TextInput, View } from "react-native"; import { FlatList, Pressable, StyleSheet, Text, TextInput, View } from "react-native";
import Icon from "../components/Icon";
import { Empty, Pill, Separator } from "../components/ui";
import { haptic } from "../lib/haptics";
import { activeProjects, createProject, openTaskCount } from "../lib/store"; import { activeProjects, createProject, openTaskCount } from "../lib/store";
import { C } from "../lib/theme"; import { C, radius, shadow, space, type } from "../lib/theme";
import { useStore } from "../lib/useStore"; import { useStore } from "../lib/useStore";
export default function Projects() { export default function Projects() {
@@ -15,6 +18,7 @@ export default function Projects() {
const name = draft.trim(); const name = draft.trim();
if (!name) return; if (!name) return;
setDraft(""); setDraft("");
haptic.success();
createProject({ name }); createProject({ name });
}; };
@@ -32,18 +36,35 @@ export default function Projects() {
<FlatList <FlatList
data={projects} data={projects}
keyExtractor={(p) => p.id} keyExtractor={(p) => p.id}
renderItem={({ item }) => ( renderItem={({ item, index }) => {
<Pressable style={s.row} onPress={() => router.push(`/project/${item.id}`)}> const open = openTaskCount(store, item.id);
return (
<Pressable
style={({ pressed }) => [
s.row,
index === 0 && s.first,
index === projects.length - 1 && s.last,
pressed && { backgroundColor: C.surface2 },
]}
onPress={() => router.push(`/project/${item.id}`)}
>
<View style={{ flex: 1 }}> <View style={{ flex: 1 }}>
<Text style={s.title}>{item.name}</Text> <Text style={s.title} numberOfLines={1}>
{item.name}
</Text>
<Text style={s.meta}> <Text style={s.meta}>
{item.status} · {openTaskCount(store, item.id)} open {open === 0 ? "Nothing open" : `${open} open`}
</Text> </Text>
</View> </View>
<Text style={{ color: C.muted }}>›</Text> {item.status !== "active" && <Pill label={item.status} />}
<Icon name="chevron" size={16} color={C.muted} />
</Pressable> </Pressable>
)} );
ListEmptyComponent={<Text style={s.empty}>No projects yet.</Text>} }}
ItemSeparatorComponent={() => <Separator inset={space.lg} />}
ListEmptyComponent={<Empty>No projects yet. Name one above.</Empty>}
contentContainerStyle={s.content}
showsVerticalScrollIndicator={false}
/> />
</View> </View>
); );
@@ -51,26 +72,34 @@ export default function Projects() {
const s = StyleSheet.create({ const s = StyleSheet.create({
screen: { flex: 1, backgroundColor: C.bg }, screen: { flex: 1, backgroundColor: C.bg },
content: { paddingHorizontal: space.lg, paddingBottom: 40 },
capture: { capture: {
margin: 12, marginHorizontal: space.lg,
marginBottom: 4, marginTop: space.md,
padding: 14, marginBottom: space.lg,
borderRadius: 12, paddingHorizontal: space.lg,
paddingVertical: space.md + 2,
borderRadius: radius.md,
backgroundColor: C.surface, backgroundColor: C.surface,
color: C.text, color: C.text,
fontSize: 16, fontSize: 16,
borderWidth: 1, borderWidth: StyleSheet.hairlineWidth,
borderColor: C.border, borderColor: C.border,
}, },
row: { row: {
flexDirection: "row", flexDirection: "row",
alignItems: "center", alignItems: "center",
paddingVertical: 14, gap: space.md,
paddingHorizontal: 16, paddingVertical: space.md + 2,
borderBottomWidth: StyleSheet.hairlineWidth, paddingHorizontal: space.lg,
borderBottomColor: C.border, backgroundColor: C.surface,
}, },
title: { color: C.text, fontSize: 16 }, first: { borderTopLeftRadius: radius.lg, borderTopRightRadius: radius.lg },
meta: { color: C.muted, fontSize: 13, marginTop: 2 }, last: {
empty: { color: C.muted, textAlign: "center", marginTop: 48 }, borderBottomLeftRadius: radius.lg,
borderBottomRightRadius: radius.lg,
...shadow.card,
},
title: { ...type.row, color: C.text },
meta: { ...type.meta, color: C.muted, marginTop: 2 },
}); });

View File

@@ -1,7 +1,8 @@
import { SectionList, StyleSheet, Text } from "react-native"; import { SectionList, StyleSheet, View } from "react-native";
import TaskRow from "../components/TaskRow"; import TaskRow, { TaskSeparator } from "../components/TaskRow";
import { Empty, SectionHeader } from "../components/ui";
import { todayTasks } from "../lib/store"; import { todayTasks } from "../lib/store";
import { C } from "../lib/theme"; import { C, radius, shadow, space } from "../lib/theme";
import { useStore } from "../lib/useStore"; import { useStore } from "../lib/useStore";
export default function Today() { export default function Today() {
@@ -9,26 +10,48 @@ export default function Today() {
const { overdue, today } = todayTasks(store); const { overdue, today } = todayTasks(store);
const sections = [ const sections = [
...(overdue.length ? [{ title: "Overdue", data: overdue, color: C.danger }] : []), ...(overdue.length ? [{ title: "Overdue", data: overdue, color: C.danger }] : []),
{ title: "Today", data: today, color: C.text }, { title: "Today", data: today, color: C.textSecondary },
]; ];
const empty = overdue.length === 0 && today.length === 0;
return ( return (
<SectionList <SectionList
style={{ flex: 1, backgroundColor: C.bg }} style={s.screen}
sections={sections} sections={sections}
keyExtractor={(t) => t.id} keyExtractor={(t) => t.id}
renderItem={({ item }) => <TaskRow task={item} showStatus />} // Each section is its own rounded card, so the first and last rows in a
renderSectionHeader={({ section }) => ( // section round their outer corners.
<Text style={[s.header, { color: section.color }]}>{section.title}</Text> renderItem={({ item, index, section }) => (
<View
style={[
s.row,
index === 0 && s.first,
index === section.data.length - 1 && s.last,
]}
>
<TaskRow task={item} showStatus />
</View>
)} )}
ListEmptyComponent={<Text style={s.empty}>Nothing due. Clear runway.</Text>} renderSectionHeader={({ section }) => (
contentContainerStyle={{ paddingBottom: 32 }} <SectionHeader tone={section.color}>{section.title}</SectionHeader>
)}
ItemSeparatorComponent={TaskSeparator}
ListFooterComponent={empty ? <Empty>Nothing due. Clear runway.</Empty> : null}
contentContainerStyle={s.content}
stickySectionHeadersEnabled={false} stickySectionHeadersEnabled={false}
showsVerticalScrollIndicator={false}
/> />
); );
} }
const s = StyleSheet.create({ const s = StyleSheet.create({
header: { fontSize: 15, fontWeight: "700", paddingHorizontal: 16, paddingTop: 18, paddingBottom: 6 }, screen: { flex: 1, backgroundColor: C.bg },
empty: { color: C.muted, textAlign: "center", marginTop: 48, fontSize: 15 }, content: { paddingHorizontal: space.lg, paddingBottom: 40 },
row: { backgroundColor: C.surface },
first: { borderTopLeftRadius: radius.lg, borderTopRightRadius: radius.lg },
last: {
borderBottomLeftRadius: radius.lg,
borderBottomRightRadius: radius.lg,
...shadow.card,
},
}); });

View File

@@ -55,11 +55,13 @@ export function Empty({ children }: { children: React.ReactNode }) {
return <Text style={u.empty}>{children}</Text>; return <Text style={u.empty}>{children}</Text>;
} }
export function Field({ label, ...props }: TextInputProps & { label: string }) { export function Field({ label, style, ...props }: TextInputProps & { label: string }) {
return ( return (
<View style={{ marginBottom: space.lg }}> <View style={{ marginBottom: space.lg }}>
<Text style={u.label}>{label}</Text> <Text style={u.label}>{label}</Text>
<TextInput style={u.input} placeholderTextColor={C.muted} {...props} /> {/* style last so callers can extend the input (e.g. multiline height)
without silently dropping the base styling. */}
<TextInput style={[u.input, style]} placeholderTextColor={C.muted} {...props} />
</View> </View>
); );
} }