frontend: introduce category chips
It'll be used for antoerh patch down the line with the triggers
Parents:
76614c01 file(s) changed
- frontend/components/ui/CategoryChips.tsx +96 -0
frontend/components/ui/CategoryChips.tsx
@@ -0,0 +1,96 @@
1 + import {
2 + TouchableOpacity,
3 + Text,
4 + View,
5 + StyleSheet,
6 + } from 'react-native';
7 + import { Colors } from '@/constants/theme';
8 +
9 + export type CategoryOption<T extends string = string> = {
10 + label: string;
11 + value: T;
12 + icon: React.ReactNode;
13 + };
14 +
15 + type Props<T extends string = string> = {
16 + options: CategoryOption<T>[];
17 + active: T | null;
18 + onChange: (value: T) => void;
19 + columns?: number;
20 + };
21 +
22 + export function CategoryChips<T extends string = string>({
23 + options,
24 + active,
25 + onChange,
26 + columns = 2,
27 + }: Props<T>) {
28 + return (
29 + <View style={styles.grid}>
30 + {options.map((option) => {
31 + const isActive = option.value === active;
32 + return (
33 + <TouchableOpacity
34 + key={option.value}
35 + onPress={() => onChange(option.value)}
36 + activeOpacity={0.7}
37 + style={[
38 + styles.chip,
39 + { width: `${100 / columns - 2}%` as any },
40 + isActive && styles.chipActive,
41 + ]}
42 + >
43 + <View style={styles.chipInner}>
44 + <View style={styles.iconWrapper}>{option.icon}</View>
45 + <Text style={[styles.label, isActive && styles.labelActive]}>
46 + {option.label}
47 + </Text>
48 + </View>
49 + </TouchableOpacity>
50 + );
51 + })}
52 + </View>
53 + );
54 + }
55 +
56 + const styles = StyleSheet.create({
57 + grid: {
58 + flexDirection: 'row',
59 + flexWrap: 'wrap',
60 + gap: 10,
61 + paddingHorizontal: 16,
62 + paddingVertical: 8,
63 + },
64 + chip: {
65 + paddingHorizontal: 16,
66 + paddingVertical: 12,
67 + borderRadius: 999,
68 + backgroundColor: '#fff',
69 + borderWidth: 1.5,
70 + borderColor: 'transparent',
71 + },
72 + chipActive: {
73 + backgroundColor: Colors.light.tint + '1A', // ~10% opacity tint background
74 + borderColor: Colors.light.tint,
75 + },
76 + chipInner: {
77 + flexDirection: 'row',
78 + alignItems: 'center',
79 + gap: 8,
80 + },
81 + iconWrapper: {
82 + width: 20,
83 + height: 20,
84 + alignItems: 'center',
85 + justifyContent: 'center',
86 + },
87 + label: {
88 + fontSize: 14,
89 + fontWeight: '500',
90 + color: '#3C3C43',
91 + letterSpacing: -0.1,
92 + },
93 + labelActive: {
94 + fontWeight: '600',
95 + },
96 + });