UI: light Things-3 design system — tokens, task rows, chips, home

Replaces the dark navy palette with a light grouped-background system:
- theme.ts: full token set (space/radius/type/shadow) + light+dark palettes
- ui.tsx: Screen/Title/SectionHeader/Card/Separator/Pill/Empty primitives
- TaskRow: rounded-square checkbox carrying priority colour, inset separators
- TaskListScreen: rows grouped into a card, drag lift, refined deferred footer
- Icon.tsx: dependency-free geometric icon set (emoji don't take a tint)
- home: large title, Next-up card, "needs attention" rows, icon tab bar
- Priority/Estimate/Date chips unified on the accent-soft active state

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Claude
2026-09-01 22:36:26 +10:00
parent 65efb632db
commit 1af86cd27c
11 changed files with 863 additions and 217 deletions

View File

@@ -5,7 +5,7 @@ import { dueMeta, friendlyDate, isDeferred } from "../lib/dates";
import { haptic } from "../lib/haptics";
import { formatEstimate, PRIORITY_COLOR } from "../lib/priority";
import { updateTask } from "../lib/store";
import { C } from "../lib/theme";
import { C, radius, space, type } from "../lib/theme";
import type { Task, TaskStatus } from "../lib/types";
export default function TaskRow({
@@ -22,6 +22,7 @@ export default function TaskRow({
onLongPress?: () => void;
}) {
const router = useRouter();
const done = task.status === "done";
// Due date is rendered separately so it can carry a tone color; the reader
// sees "due today" in amber, never does date math on an ISO string.
const due = task.due_date ? dueMeta(task.due_date) : null;
@@ -37,33 +38,42 @@ export default function TaskRow({
.filter(Boolean)
.join(" · ");
const meta = base || due;
const flag = PRIORITY_COLOR[task.priority];
const toggle = () => {
const done = task.status !== "done";
if (done) haptic.success();
const next = task.status !== "done";
if (next) haptic.success();
else haptic.tap();
updateTask(task.id, { status: done ? "done" : uncheckTo });
updateTask(task.id, { status: next ? "done" : uncheckTo });
};
return (
<Animated.View entering={FadeIn.duration(150)}>
<Pressable style={s.row} onPress={() => router.push(`/task/${task.id}`)} onLongPress={onLongPress} delayLongPress={200}>
<Pressable
style={({ pressed }) => [s.row, pressed && s.rowPressed]}
onPress={() => router.push(`/task/${task.id}`)}
onLongPress={onLongPress}
delayLongPress={200}
>
{/* Rounded square rather than a circle — the Things checkbox shape.
The border carries the priority colour, so priority is legible at a
glance without spending a whole column on a flag. */}
<Pressable
style={[s.checkbox, { borderColor: PRIORITY_COLOR[task.priority] }]}
style={[s.checkbox, { borderColor: done ? C.done : flag }, done && s.checkboxDone]}
hitSlop={10}
onPress={toggle}
>
{task.status === "done" && <View style={s.checkboxFill} />}
{done && <Text style={s.check}>✓</Text>}
</Pressable>
<View style={{ flex: 1 }}>
<Text style={s.title} numberOfLines={2}>
<Text style={[s.title, done && s.titleDone]} numberOfLines={2}>
{task.title}
</Text>
{!!meta && (
<Text style={s.meta}>
<Text style={s.meta} numberOfLines={1}>
{base}
{!!base && !!due && " · "}
{due && <Text style={{ color: dueColor }}>{due.label}</Text>}
{due && <Text style={{ color: dueColor, fontWeight: "500" }}>{due.label}</Text>}
</Text>
)}
</View>
@@ -76,21 +86,40 @@ const s = StyleSheet.create({
row: {
flexDirection: "row",
alignItems: "center",
gap: 12,
paddingVertical: 14,
paddingHorizontal: 16,
borderBottomWidth: StyleSheet.hairlineWidth,
borderBottomColor: C.border,
gap: space.md,
paddingVertical: space.md + 2,
paddingHorizontal: space.lg,
backgroundColor: C.surface,
},
rowPressed: { backgroundColor: C.surface2 },
checkbox: {
width: 22,
height: 22,
borderRadius: 11,
borderWidth: 2,
borderRadius: 7,
borderWidth: 1.5,
alignItems: "center",
justifyContent: "center",
},
checkboxFill: { width: 12, height: 12, borderRadius: 6, backgroundColor: C.done },
title: { color: C.text, fontSize: 16 },
meta: { color: C.muted, fontSize: 13, marginTop: 2 },
checkboxDone: { backgroundColor: C.done },
check: { color: "#FFFFFF", fontSize: 13, lineHeight: 16, fontWeight: "700" },
title: { ...type.row, color: C.text },
titleDone: { color: C.muted, textDecorationLine: "line-through" },
meta: { ...type.meta, color: C.muted, marginTop: 3 },
});
/** Inset hairline sized to line up under the task title, not the checkbox. */
export const TASK_ROW_INSET = space.lg + 22 + space.md;
export function TaskSeparator() {
return (
<View
style={{
height: StyleSheet.hairlineWidth,
backgroundColor: C.separator,
marginLeft: TASK_ROW_INSET,
// Keeps the hairline from showing through a card's rounded corner.
borderRadius: radius.sm,
}}
/>
);
}