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:
260
app/src/components/Icon.tsx
Normal file
260
app/src/components/Icon.tsx
Normal file
@@ -0,0 +1,260 @@
|
||||
// A tiny geometric icon set drawn from plain Views.
|
||||
//
|
||||
// The app ships no vector-icon dependency and emoji render differently on
|
||||
// every platform (and can't take the accent colour), so these are built from
|
||||
// borders and rectangles instead: monochrome, tintable, pixel-identical on
|
||||
// Android and web, and zero bytes of new dependency.
|
||||
import { StyleSheet, View } from "react-native";
|
||||
import { C } from "../lib/theme";
|
||||
|
||||
export type IconName =
|
||||
| "today"
|
||||
| "next"
|
||||
| "waiting"
|
||||
| "someday"
|
||||
| "projects"
|
||||
| "review"
|
||||
| "settings"
|
||||
| "plus"
|
||||
| "chevron";
|
||||
|
||||
type Props = {
|
||||
name: IconName;
|
||||
size?: number;
|
||||
color?: string;
|
||||
/** Used to carve the crescent out of the "someday" moon. */
|
||||
bg?: string;
|
||||
};
|
||||
|
||||
export default function Icon({ name, size = 22, color = C.textSecondary, bg = C.bg }: Props) {
|
||||
const box = { width: size, height: size };
|
||||
const w = 1.7; // one consistent stroke weight across the whole set
|
||||
|
||||
switch (name) {
|
||||
case "today":
|
||||
return (
|
||||
<View style={[box, s.center]}>
|
||||
<View
|
||||
style={{
|
||||
width: size * 0.86,
|
||||
height: size * 0.82,
|
||||
borderWidth: w,
|
||||
borderColor: color,
|
||||
borderRadius: 5,
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
<View style={{ height: size * 0.2, backgroundColor: color }} />
|
||||
<View style={[s.center, { flex: 1 }]}>
|
||||
<View
|
||||
style={{ width: w * 2, height: w * 2, borderRadius: 999, backgroundColor: color }}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
|
||||
case "next":
|
||||
return (
|
||||
<View style={[box, s.center]}>
|
||||
<View
|
||||
style={{
|
||||
width: 0,
|
||||
height: 0,
|
||||
borderTopWidth: size * 0.32,
|
||||
borderBottomWidth: size * 0.32,
|
||||
borderLeftWidth: size * 0.52,
|
||||
borderTopColor: "transparent",
|
||||
borderBottomColor: "transparent",
|
||||
borderLeftColor: color,
|
||||
marginLeft: size * 0.12,
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
|
||||
case "waiting":
|
||||
return (
|
||||
<View style={[box, s.center]}>
|
||||
<View
|
||||
style={{
|
||||
width: size * 0.84,
|
||||
height: size * 0.84,
|
||||
borderRadius: 999,
|
||||
borderWidth: w,
|
||||
borderColor: color,
|
||||
}}
|
||||
>
|
||||
{/* hour hand, then minute hand — anchored at the dial centre */}
|
||||
<View
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: size * 0.42 - w / 2,
|
||||
top: size * 0.2,
|
||||
width: w,
|
||||
height: size * 0.22,
|
||||
backgroundColor: color,
|
||||
borderRadius: w,
|
||||
}}
|
||||
/>
|
||||
<View
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: size * 0.42 - w / 2,
|
||||
top: size * 0.42 - w / 2,
|
||||
width: size * 0.24,
|
||||
height: w,
|
||||
backgroundColor: color,
|
||||
borderRadius: w,
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
|
||||
case "someday":
|
||||
return (
|
||||
<View style={[box, s.center]}>
|
||||
<View
|
||||
style={{
|
||||
width: size * 0.84,
|
||||
height: size * 0.84,
|
||||
borderRadius: 999,
|
||||
backgroundColor: color,
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
position: "absolute",
|
||||
right: -size * 0.22,
|
||||
top: -size * 0.26,
|
||||
width: size * 0.84,
|
||||
height: size * 0.84,
|
||||
borderRadius: 999,
|
||||
backgroundColor: bg,
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
|
||||
case "projects":
|
||||
return (
|
||||
<View style={[box, s.center]}>
|
||||
<View style={{ width: size * 0.86, height: size * 0.78 }}>
|
||||
<View
|
||||
style={{
|
||||
width: size * 0.4,
|
||||
height: size * 0.16,
|
||||
borderTopLeftRadius: 3,
|
||||
borderTopRightRadius: 3,
|
||||
backgroundColor: color,
|
||||
}}
|
||||
/>
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
borderWidth: w,
|
||||
borderColor: color,
|
||||
borderRadius: 5,
|
||||
borderTopLeftRadius: 0,
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
|
||||
case "review":
|
||||
return (
|
||||
<View style={[box, s.center]}>
|
||||
{/* an open ring plus an arrowhead reads as "cycle" without an SVG path */}
|
||||
<View
|
||||
style={{
|
||||
width: size * 0.82,
|
||||
height: size * 0.82,
|
||||
borderRadius: 999,
|
||||
borderWidth: w,
|
||||
borderColor: color,
|
||||
borderTopColor: "transparent",
|
||||
transform: [{ rotate: "45deg" }],
|
||||
}}
|
||||
/>
|
||||
<View
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: size * 0.06,
|
||||
right: size * 0.12,
|
||||
width: 0,
|
||||
height: 0,
|
||||
borderLeftWidth: size * 0.16,
|
||||
borderRightWidth: size * 0.16,
|
||||
borderBottomWidth: size * 0.2,
|
||||
borderLeftColor: "transparent",
|
||||
borderRightColor: "transparent",
|
||||
borderBottomColor: color,
|
||||
transform: [{ rotate: "115deg" }],
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
|
||||
case "settings":
|
||||
// Sliders, not a cog: three strokes with offset knobs stays legible at 20px.
|
||||
return (
|
||||
<View style={[box, s.center]}>
|
||||
<View style={{ width: size * 0.88, gap: size * 0.16 }}>
|
||||
{[0.62, 0.3, 0.48].map((knob, i) => (
|
||||
<View key={i} style={s.sliderRow}>
|
||||
<View style={{ flex: 1, height: w, backgroundColor: color, borderRadius: w }} />
|
||||
<View
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: `${knob * 100}%`,
|
||||
width: w * 3,
|
||||
height: w * 3,
|
||||
marginLeft: -w * 1.5,
|
||||
marginTop: -w,
|
||||
borderRadius: 999,
|
||||
borderWidth: w,
|
||||
borderColor: color,
|
||||
backgroundColor: bg,
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
|
||||
case "plus":
|
||||
return (
|
||||
<View style={[box, s.center]}>
|
||||
<View style={{ position: "absolute", width: size * 0.7, height: w + 0.5, borderRadius: 2, backgroundColor: color }} />
|
||||
<View style={{ position: "absolute", width: w + 0.5, height: size * 0.7, borderRadius: 2, backgroundColor: color }} />
|
||||
</View>
|
||||
);
|
||||
|
||||
case "chevron":
|
||||
return (
|
||||
<View style={[box, s.center]}>
|
||||
<View
|
||||
style={{
|
||||
width: size * 0.34,
|
||||
height: size * 0.34,
|
||||
borderRightWidth: w,
|
||||
borderTopWidth: w,
|
||||
borderColor: color,
|
||||
transform: [{ rotate: "45deg" }],
|
||||
marginLeft: -size * 0.06,
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const s = StyleSheet.create({
|
||||
center: { alignItems: "center", justifyContent: "center" },
|
||||
sliderRow: { justifyContent: "center" },
|
||||
});
|
||||
Reference in New Issue
Block a user