Files
gtd/app/src/components/Icon.tsx

323 lines
10 KiB
TypeScript

// 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"
| "flag"
| "repeat"
| "tag"
| "plus"
| "close"
| "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 "flag":
return (
<View style={box}>
{/* Pole plus a pennant — reads as a priority flag at 20px where a
detailed banner outline would smear into a blob. */}
<View style={{ position: "absolute", left: size * 0.26, top: size * 0.12, width: w, height: size * 0.76, borderRadius: 1, backgroundColor: color }} />
<View
style={{
position: "absolute",
left: size * 0.26 + w,
top: size * 0.14,
width: size * 0.44,
height: size * 0.34,
borderTopRightRadius: 2,
borderBottomRightRadius: 2,
backgroundColor: color,
}}
/>
</View>
);
case "repeat":
return (
<View style={box}>
{/* Two offset bars with opposing arrowheads: a loop, without the
anti-aliasing mush a thin circular arc gets at this size. */}
<View style={{ position: "absolute", left: size * 0.2, top: size * 0.3, width: size * 0.5, height: w, backgroundColor: color }} />
<View style={{ position: "absolute", left: size * 0.62, top: size * 0.3 - size * 0.09 + w / 2, width: size * 0.18, height: size * 0.18, borderRightWidth: w, borderTopWidth: w, borderColor: color, transform: [{ rotate: "45deg" }] }} />
<View style={{ position: "absolute", left: size * 0.3, top: size * 0.66, width: size * 0.5, height: w, backgroundColor: color }} />
<View style={{ position: "absolute", left: size * 0.2, top: size * 0.66 - size * 0.09 + w / 2, width: size * 0.18, height: size * 0.18, borderLeftWidth: w, borderBottomWidth: w, borderColor: color, transform: [{ rotate: "45deg" }] }} />
</View>
);
case "tag":
return (
<View style={[box, s.center]}>
<View
style={{
width: size * 0.6,
height: size * 0.6,
borderWidth: w,
borderColor: color,
borderRadius: 3,
transform: [{ rotate: "45deg" }],
}}
/>
<View style={{ position: "absolute", left: size * 0.3, top: size * 0.3, width: w + 0.6, height: w + 0.6, borderRadius: 999, backgroundColor: color }} />
</View>
);
case "close":
return (
<View style={[box, s.center]}>
<View style={{ position: "absolute", width: size * 0.6, height: w, borderRadius: 2, backgroundColor: color, transform: [{ rotate: "45deg" }] }} />
<View style={{ position: "absolute", width: size * 0.6, height: w, borderRadius: 2, backgroundColor: color, transform: [{ rotate: "-45deg" }] }} />
</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" },
});