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:
@@ -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,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user