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>
210 lines
6.2 KiB
TypeScript
210 lines
6.2 KiB
TypeScript
import { Pressable, StyleSheet, Text, TextInput, View, type TextInputProps, type ViewStyle } from "react-native";
|
|
import { C, radius, shadow, space, type } from "../lib/theme";
|
|
|
|
/**
|
|
* Screen background + consistent gutter. Every screen sits on the grouped
|
|
* background so white cards read as raised paper.
|
|
*/
|
|
export function Screen({ children, style }: { children: React.ReactNode; style?: ViewStyle }) {
|
|
return <View style={[u.screen, style]}>{children}</View>;
|
|
}
|
|
|
|
/**
|
|
* Large in-content heading. Nav-bar titles are tiny and centred; Things puts
|
|
* the screen name in the content at display size, which is what gives each
|
|
* screen its sense of place.
|
|
*/
|
|
export function Title({ children, count }: { children: React.ReactNode; count?: number }) {
|
|
return (
|
|
<View style={u.titleRow}>
|
|
<Text style={u.title}>{children}</Text>
|
|
{count !== undefined && count > 0 && <Text style={u.titleCount}>{count}</Text>}
|
|
</View>
|
|
);
|
|
}
|
|
|
|
/** Small caps label above a group. Sentence case — SHOUTING CAPS reads dated. */
|
|
export function SectionHeader({ children, tone }: { children: React.ReactNode; tone?: string }) {
|
|
return <Text style={[u.sectionHeader, !!tone && { color: tone }]}>{children}</Text>;
|
|
}
|
|
|
|
/**
|
|
* Rounded white group. Rows inside are separated by inset hairlines rather
|
|
* than full-bleed dividers, so the card keeps a clean silhouette.
|
|
*/
|
|
export function Card({ children, style }: { children: React.ReactNode; style?: ViewStyle }) {
|
|
return <View style={[u.card, style]}>{children}</View>;
|
|
}
|
|
|
|
/** Inset hairline for use between rows inside a `Card`. */
|
|
export function Separator({ inset = 0 }: { inset?: number }) {
|
|
return <View style={[u.separator, { marginLeft: inset }]} />;
|
|
}
|
|
|
|
/** Compact count/status pill. */
|
|
export function Pill({ label, tone }: { label: string; tone?: string }) {
|
|
return (
|
|
<View style={[u.pill, !!tone && { backgroundColor: `${tone}18` }]}>
|
|
<Text style={[u.pillText, !!tone && { color: tone }]}>{label}</Text>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
/** Centred empty state. One quiet line — no illustration, no exclamation. */
|
|
export function Empty({ children }: { children: React.ReactNode }) {
|
|
return <Text style={u.empty}>{children}</Text>;
|
|
}
|
|
|
|
export function Field({ label, ...props }: TextInputProps & { label: string }) {
|
|
return (
|
|
<View style={{ marginBottom: space.lg }}>
|
|
<Text style={u.label}>{label}</Text>
|
|
<TextInput style={u.input} placeholderTextColor={C.muted} {...props} />
|
|
</View>
|
|
);
|
|
}
|
|
|
|
export function Chips<T extends string>({
|
|
label,
|
|
options,
|
|
value,
|
|
onChange,
|
|
labels,
|
|
}: {
|
|
label: string;
|
|
options: T[];
|
|
value: T | null;
|
|
onChange: (v: T) => void;
|
|
labels?: Record<string, string>;
|
|
}) {
|
|
return (
|
|
<View style={{ marginBottom: space.lg }}>
|
|
<Text style={u.label}>{label}</Text>
|
|
<View style={u.chipRow}>
|
|
{options.map((o) => {
|
|
const active = value === o;
|
|
return (
|
|
<Pressable
|
|
key={o}
|
|
style={({ pressed }) => [u.chip, active && u.chipActive, pressed && !active && u.chipPressed]}
|
|
onPress={() => onChange(o)}
|
|
>
|
|
<Text style={[u.chipText, active && u.chipTextActive]}>{labels?.[o] ?? o}</Text>
|
|
</Pressable>
|
|
);
|
|
})}
|
|
</View>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
export function Button({
|
|
title,
|
|
onPress,
|
|
danger,
|
|
secondary,
|
|
}: {
|
|
title: string;
|
|
onPress: () => void;
|
|
danger?: boolean;
|
|
secondary?: boolean;
|
|
}) {
|
|
const variant = danger ? u.buttonDanger : secondary ? u.buttonSecondary : null;
|
|
const textVariant = danger ? u.buttonTextDanger : secondary ? u.buttonTextSecondary : null;
|
|
return (
|
|
<Pressable
|
|
style={({ pressed }) => [u.button, variant, pressed && { opacity: 0.75 }]}
|
|
onPress={onPress}
|
|
>
|
|
<Text style={[u.buttonText, textVariant]}>{title}</Text>
|
|
</Pressable>
|
|
);
|
|
}
|
|
|
|
const u = StyleSheet.create({
|
|
screen: { flex: 1, backgroundColor: C.bg },
|
|
|
|
titleRow: {
|
|
flexDirection: "row",
|
|
alignItems: "center",
|
|
gap: space.md,
|
|
paddingHorizontal: space.lg,
|
|
paddingTop: space.md,
|
|
paddingBottom: space.lg,
|
|
},
|
|
title: { ...type.title, color: C.text },
|
|
titleCount: { ...type.title, color: C.muted, fontWeight: "600" },
|
|
|
|
sectionHeader: {
|
|
...type.section,
|
|
color: C.textSecondary,
|
|
paddingHorizontal: space.xs,
|
|
paddingTop: space.xl,
|
|
paddingBottom: space.sm,
|
|
},
|
|
|
|
card: {
|
|
backgroundColor: C.surface,
|
|
borderRadius: radius.lg,
|
|
borderWidth: StyleSheet.hairlineWidth,
|
|
borderColor: C.border,
|
|
overflow: "hidden",
|
|
...shadow.card,
|
|
},
|
|
separator: { height: StyleSheet.hairlineWidth, backgroundColor: C.separator },
|
|
|
|
pill: {
|
|
paddingHorizontal: space.sm,
|
|
paddingVertical: 3,
|
|
borderRadius: radius.pill,
|
|
backgroundColor: C.surface2,
|
|
},
|
|
pillText: { ...type.meta, color: C.textSecondary, fontWeight: "500" },
|
|
|
|
empty: { color: C.muted, textAlign: "center", marginTop: 56, ...type.body },
|
|
|
|
label: {
|
|
...type.section,
|
|
color: C.textSecondary,
|
|
marginBottom: space.sm,
|
|
},
|
|
input: {
|
|
backgroundColor: C.surface,
|
|
color: C.text,
|
|
borderRadius: radius.md,
|
|
borderWidth: StyleSheet.hairlineWidth,
|
|
borderColor: C.border,
|
|
paddingHorizontal: space.md,
|
|
paddingVertical: space.md,
|
|
fontSize: 16,
|
|
},
|
|
|
|
chipRow: { flexDirection: "row", flexWrap: "wrap", gap: space.sm },
|
|
chip: {
|
|
paddingVertical: space.sm,
|
|
paddingHorizontal: space.lg - 2,
|
|
borderRadius: radius.pill,
|
|
backgroundColor: C.surface,
|
|
borderWidth: StyleSheet.hairlineWidth,
|
|
borderColor: C.border,
|
|
},
|
|
chipActive: { backgroundColor: C.accentSoft, borderColor: C.accent },
|
|
chipPressed: { backgroundColor: C.surface2 },
|
|
chipText: { ...type.chip, color: C.textSecondary },
|
|
chipTextActive: { color: C.accent, fontWeight: "600" },
|
|
|
|
button: {
|
|
backgroundColor: C.accent,
|
|
borderRadius: radius.md,
|
|
paddingVertical: space.lg - 2,
|
|
alignItems: "center",
|
|
marginTop: space.sm,
|
|
marginBottom: space.md,
|
|
},
|
|
buttonDanger: { backgroundColor: "transparent", borderWidth: StyleSheet.hairlineWidth, borderColor: C.danger },
|
|
buttonSecondary: { backgroundColor: C.surface, borderWidth: StyleSheet.hairlineWidth, borderColor: C.border },
|
|
buttonText: { fontSize: 16, fontWeight: "600", color: C.onAccent },
|
|
buttonTextDanger: { color: C.danger },
|
|
buttonTextSecondary: { color: C.text },
|
|
});
|