diff --git a/app/src/app/add.tsx b/app/src/app/add.tsx index ef57f79..346c387 100644 --- a/app/src/app/add.tsx +++ b/app/src/app/add.tsx @@ -9,7 +9,7 @@ import { haptic } from "../lib/haptics"; import { parseQuickAdd, type ParsedQuickAdd } from "../lib/quickadd"; import { REPEAT_LABEL, REPEATS } from "../lib/repeat"; 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 type { Repeat, Task, TaskStatus } from "../lib/types"; @@ -81,7 +81,12 @@ export default function AddTask() { }; return ( - + - - + [s.save, !ready && s.saveDisabled, pressed && ready && { opacity: 0.8 }]} + onPress={save} + disabled={!ready} + > + {ready ? "Add task" : "Needs a title, priority & due date"} @@ -146,15 +155,23 @@ export default function AddTask() { } 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: { backgroundColor: C.accent, - borderRadius: 10, - padding: 14, + borderRadius: radius.md, + paddingVertical: space.lg - 2, alignItems: "center", - marginTop: 8, - marginBottom: 24, + marginTop: space.sm, + marginBottom: space.xxl, }, - saveDisabled: { backgroundColor: C.surface }, - saveText: { color: C.text, fontSize: 16, fontWeight: "600" }, + saveDisabled: { backgroundColor: C.surface2 }, + saveText: { fontSize: 16, fontWeight: "600", color: C.onAccent }, + saveTextDisabled: { color: C.muted }, }); diff --git a/app/src/app/projects.tsx b/app/src/app/projects.tsx index 54280b7..efc2ed1 100644 --- a/app/src/app/projects.tsx +++ b/app/src/app/projects.tsx @@ -1,8 +1,11 @@ import { useRouter } from "expo-router"; import { useState } from "react"; 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 { C } from "../lib/theme"; +import { C, radius, shadow, space, type } from "../lib/theme"; import { useStore } from "../lib/useStore"; export default function Projects() { @@ -15,6 +18,7 @@ export default function Projects() { const name = draft.trim(); if (!name) return; setDraft(""); + haptic.success(); createProject({ name }); }; @@ -32,18 +36,35 @@ export default function Projects() { p.id} - renderItem={({ item }) => ( - router.push(`/project/${item.id}`)}> - - {item.name} - - {item.status} · {openTaskCount(store, item.id)} open - - - › - - )} - ListEmptyComponent={No projects yet.} + renderItem={({ item, index }) => { + const open = openTaskCount(store, item.id); + return ( + [ + s.row, + index === 0 && s.first, + index === projects.length - 1 && s.last, + pressed && { backgroundColor: C.surface2 }, + ]} + onPress={() => router.push(`/project/${item.id}`)} + > + + + {item.name} + + + {open === 0 ? "Nothing open" : `${open} open`} + + + {item.status !== "active" && } + + + ); + }} + ItemSeparatorComponent={() => } + ListEmptyComponent={No projects yet. Name one above.} + contentContainerStyle={s.content} + showsVerticalScrollIndicator={false} /> ); @@ -51,26 +72,34 @@ export default function Projects() { const s = StyleSheet.create({ screen: { flex: 1, backgroundColor: C.bg }, + content: { paddingHorizontal: space.lg, paddingBottom: 40 }, capture: { - margin: 12, - marginBottom: 4, - padding: 14, - borderRadius: 12, + marginHorizontal: space.lg, + marginTop: space.md, + marginBottom: space.lg, + paddingHorizontal: space.lg, + paddingVertical: space.md + 2, + borderRadius: radius.md, backgroundColor: C.surface, color: C.text, fontSize: 16, - borderWidth: 1, + borderWidth: StyleSheet.hairlineWidth, borderColor: C.border, }, row: { flexDirection: "row", alignItems: "center", - paddingVertical: 14, - paddingHorizontal: 16, - borderBottomWidth: StyleSheet.hairlineWidth, - borderBottomColor: C.border, + gap: space.md, + paddingVertical: space.md + 2, + paddingHorizontal: space.lg, + backgroundColor: C.surface, }, - title: { color: C.text, fontSize: 16 }, - meta: { color: C.muted, fontSize: 13, marginTop: 2 }, - empty: { color: C.muted, textAlign: "center", marginTop: 48 }, + first: { borderTopLeftRadius: radius.lg, borderTopRightRadius: radius.lg }, + last: { + borderBottomLeftRadius: radius.lg, + borderBottomRightRadius: radius.lg, + ...shadow.card, + }, + title: { ...type.row, color: C.text }, + meta: { ...type.meta, color: C.muted, marginTop: 2 }, }); diff --git a/app/src/app/today.tsx b/app/src/app/today.tsx index cf170ff..51d53e6 100644 --- a/app/src/app/today.tsx +++ b/app/src/app/today.tsx @@ -1,7 +1,8 @@ -import { SectionList, StyleSheet, Text } from "react-native"; -import TaskRow from "../components/TaskRow"; +import { SectionList, StyleSheet, View } from "react-native"; +import TaskRow, { TaskSeparator } from "../components/TaskRow"; +import { Empty, SectionHeader } from "../components/ui"; import { todayTasks } from "../lib/store"; -import { C } from "../lib/theme"; +import { C, radius, shadow, space } from "../lib/theme"; import { useStore } from "../lib/useStore"; export default function Today() { @@ -9,26 +10,48 @@ export default function Today() { const { overdue, today } = todayTasks(store); const sections = [ ...(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 ( t.id} - renderItem={({ item }) => } - renderSectionHeader={({ section }) => ( - {section.title} + // Each section is its own rounded card, so the first and last rows in a + // section round their outer corners. + renderItem={({ item, index, section }) => ( + + + )} - ListEmptyComponent={Nothing due. Clear runway.} - contentContainerStyle={{ paddingBottom: 32 }} + renderSectionHeader={({ section }) => ( + {section.title} + )} + ItemSeparatorComponent={TaskSeparator} + ListFooterComponent={empty ? Nothing due. Clear runway. : null} + contentContainerStyle={s.content} stickySectionHeadersEnabled={false} + showsVerticalScrollIndicator={false} /> ); } const s = StyleSheet.create({ - header: { fontSize: 15, fontWeight: "700", paddingHorizontal: 16, paddingTop: 18, paddingBottom: 6 }, - empty: { color: C.muted, textAlign: "center", marginTop: 48, fontSize: 15 }, + screen: { flex: 1, backgroundColor: C.bg }, + 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, + }, }); diff --git a/app/src/components/ui.tsx b/app/src/components/ui.tsx index 87bd74b..b3860cd 100644 --- a/app/src/components/ui.tsx +++ b/app/src/components/ui.tsx @@ -55,11 +55,13 @@ export function Empty({ children }: { children: React.ReactNode }) { return {children}; } -export function Field({ label, ...props }: TextInputProps & { label: string }) { +export function Field({ label, style, ...props }: TextInputProps & { label: string }) { return ( {label} - + {/* style last so callers can extend the input (e.g. multiline height) + without silently dropping the base styling. */} + ); }