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:
@@ -1,9 +1,63 @@
|
||||
import { Pressable, StyleSheet, Text, TextInput, View, type TextInputProps } from "react-native";
|
||||
import { C } from "../lib/theme";
|
||||
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: 14 }}>
|
||||
<View style={{ marginBottom: space.lg }}>
|
||||
<Text style={u.label}>{label}</Text>
|
||||
<TextInput style={u.input} placeholderTextColor={C.muted} {...props} />
|
||||
</View>
|
||||
@@ -24,18 +78,21 @@ export function Chips<T extends string>({
|
||||
labels?: Record<string, string>;
|
||||
}) {
|
||||
return (
|
||||
<View style={{ marginBottom: 14 }}>
|
||||
<View style={{ marginBottom: space.lg }}>
|
||||
<Text style={u.label}>{label}</Text>
|
||||
<View style={u.chipRow}>
|
||||
{options.map((o) => (
|
||||
<Pressable
|
||||
key={o}
|
||||
style={[u.chip, value === o && u.chipActive]}
|
||||
onPress={() => onChange(o)}
|
||||
>
|
||||
<Text style={[u.chipText, value === o && { color: C.text }]}>{labels?.[o] ?? o}</Text>
|
||||
</Pressable>
|
||||
))}
|
||||
{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>
|
||||
);
|
||||
@@ -45,50 +102,108 @@ 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={[u.button, danger && { backgroundColor: "transparent", borderColor: C.danger, borderWidth: 1 }]}
|
||||
style={({ pressed }) => [u.button, variant, pressed && { opacity: 0.75 }]}
|
||||
onPress={onPress}
|
||||
>
|
||||
<Text style={[u.buttonText, danger && { color: C.danger }]}>{title}</Text>
|
||||
<Text style={[u.buttonText, textVariant]}>{title}</Text>
|
||||
</Pressable>
|
||||
);
|
||||
}
|
||||
|
||||
const u = StyleSheet.create({
|
||||
label: { color: C.muted, fontSize: 13, marginBottom: 6, textTransform: "uppercase", letterSpacing: 0.5 },
|
||||
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: 10,
|
||||
borderWidth: 1,
|
||||
borderRadius: radius.md,
|
||||
borderWidth: StyleSheet.hairlineWidth,
|
||||
borderColor: C.border,
|
||||
padding: 12,
|
||||
paddingHorizontal: space.md,
|
||||
paddingVertical: space.md,
|
||||
fontSize: 16,
|
||||
},
|
||||
chipRow: { flexDirection: "row", flexWrap: "wrap", gap: 8 },
|
||||
|
||||
chipRow: { flexDirection: "row", flexWrap: "wrap", gap: space.sm },
|
||||
chip: {
|
||||
paddingVertical: 8,
|
||||
paddingHorizontal: 14,
|
||||
borderRadius: 20,
|
||||
paddingVertical: space.sm,
|
||||
paddingHorizontal: space.lg - 2,
|
||||
borderRadius: radius.pill,
|
||||
backgroundColor: C.surface,
|
||||
borderWidth: 1,
|
||||
borderWidth: StyleSheet.hairlineWidth,
|
||||
borderColor: C.border,
|
||||
},
|
||||
chipActive: { backgroundColor: C.surface2, borderColor: C.accent },
|
||||
chipText: { color: C.muted, fontSize: 14 },
|
||||
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: 10,
|
||||
padding: 14,
|
||||
borderRadius: radius.md,
|
||||
paddingVertical: space.lg - 2,
|
||||
alignItems: "center",
|
||||
marginTop: 8,
|
||||
marginBottom: 12,
|
||||
marginTop: space.sm,
|
||||
marginBottom: space.md,
|
||||
},
|
||||
buttonText: { color: C.text, fontSize: 16, fontWeight: "600" },
|
||||
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 },
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user