/ components / CustomColorPicker.tsx
CustomColorPicker.tsx
1 import React, { useState, useEffect } from 'react'; 2 import { 3 View, 4 Text, 5 TouchableOpacity, 6 StyleSheet, 7 Modal, 8 FlatList, 9 Platform, 10 SafeAreaView, 11 } from 'react-native'; 12 import { Ionicons } from '@expo/vector-icons'; 13 14 // Expanded color palette to ensure even rows 15 const COLORS = [ 16 { 17 category: 'Greens', 18 items: [ 19 { color: '#2E8B57', name: 'Sea Green' }, 20 { color: '#3CB371', name: 'Medium Sea Green' }, 21 { color: '#00A36C', name: 'Jade' }, 22 { color: '#2AAA8A', name: 'Green Slate' }, 23 { color: '#4CBB17', name: 'Kelly Green' }, 24 { color: '#008000', name: 'Green' }, 25 { color: '#006400', name: 'Dark Green' }, 26 { color: '#32CD32', name: 'Lime Green' }, 27 ], 28 }, 29 { 30 category: 'Blues', 31 items: [ 32 { color: '#4682B4', name: 'Steel Blue' }, 33 { color: '#6495ED', name: 'Cornflower Blue' }, 34 { color: '#1E90FF', name: 'Dodger Blue' }, 35 { color: '#00BFFF', name: 'Deep Sky Blue' }, 36 { color: '#0047AB', name: 'Cobalt Blue' }, 37 { color: '#000080', name: 'Navy' }, 38 { color: '#0000CD', name: 'Medium Blue' }, 39 { color: '#5D8AA8', name: 'Air Force Blue' }, 40 ], 41 }, 42 { 43 category: 'Purples', 44 items: [ 45 { color: '#9370DB', name: 'Medium Purple' }, 46 { color: '#8A2BE2', name: 'Blue Violet' }, 47 { color: '#9932CC', name: 'Dark Orchid' }, 48 { color: '#BA55D3', name: 'Medium Orchid' }, 49 { color: '#6A0DAD', name: 'Purple' }, 50 { color: '#800080', name: 'Deep Purple' }, 51 { color: '#483D8B', name: 'Dark Slate Blue' }, 52 { color: '#DA70D6', name: 'Orchid' }, 53 ], 54 }, 55 { 56 category: 'Reds', 57 items: [ 58 { color: '#FF6B6B', name: 'Light Red' }, 59 { color: '#FF4500', name: 'Orange Red' }, 60 { color: '#FF1493', name: 'Deep Pink' }, 61 { color: '#C71585', name: 'Medium Violet Red' }, 62 { color: '#DC143C', name: 'Crimson' }, 63 { color: '#B22222', name: 'Firebrick' }, 64 { color: '#CD5C5C', name: 'Indian Red' }, 65 { color: '#8B0000', name: 'Dark Red' }, 66 ], 67 }, 68 { 69 category: 'Yellows & Oranges', 70 items: [ 71 { color: '#FFD700', name: 'Gold' }, 72 { color: '#FFA500', name: 'Orange' }, 73 { color: '#FF8C00', name: 'Dark Orange' }, 74 { color: '#FF7F50', name: 'Coral' }, 75 { color: '#F4C430', name: 'Saffron' }, 76 { color: '#DAA520', name: 'Goldenrod' }, 77 { color: '#FFFF00', name: 'Yellow' }, 78 { color: '#BDB76B', name: 'Dark Khaki' }, 79 ], 80 }, 81 ]; 82 83 interface CustomColorPickerProps { 84 visible: boolean; 85 onClose: () => void; 86 onColorSelected: (color: string) => void; 87 initialColor: string; 88 colors: any; 89 } 90 91 const CustomColorPicker: React.FC<CustomColorPickerProps> = ({ 92 visible, 93 onClose, 94 onColorSelected, 95 initialColor, 96 colors, 97 }) => { 98 const [selectedColor, setSelectedColor] = useState(initialColor); 99 100 // Fixed number of columns regardless of screen size 101 102 useEffect(() => { 103 if (visible) setSelectedColor(initialColor); 104 }, [visible, initialColor]); 105 106 const getContrastText = (color: string) => { 107 const hex = color.replace('#', ''); 108 const r = parseInt(hex.substr(0, 2), 16); 109 const g = parseInt(hex.substr(2, 2), 16); 110 const b = parseInt(hex.substr(4, 2), 16); 111 return (r * 299 + g * 587 + b * 114) / 1000 >= 128 ? '#000' : '#fff'; 112 }; 113 114 const selectedColorName = 115 COLORS.flatMap((cat) => cat.items).find( 116 (item) => item.color === selectedColor 117 )?.name || 'Custom'; 118 119 if (!visible) return null; 120 121 return ( 122 <Modal 123 visible={visible} 124 transparent={true} 125 animationType="fade" 126 onRequestClose={onClose} 127 statusBarTranslucent 128 > 129 <SafeAreaView style={styles.container}> 130 <View style={[styles.modal, { backgroundColor: colors.card }]}> 131 <View style={[styles.header, { borderBottomColor: colors.border }]}> 132 <Text style={[styles.title, { color: colors.text }]}> 133 Choose Color 134 </Text> 135 <TouchableOpacity 136 style={[styles.closeBtn, { backgroundColor: colors.background }]} 137 onPress={onClose} 138 > 139 <Ionicons name="close" size={20} color={colors.text} /> 140 </TouchableOpacity> 141 </View> 142 143 <FlatList 144 data={COLORS} 145 keyExtractor={(item) => item.category} 146 renderItem={({ item: category }) => ( 147 <View style={styles.categorySection}> 148 <Text style={[styles.categoryTitle, { color: colors.text }]}> 149 {category.category} 150 </Text> 151 <View style={styles.colorGrid}> 152 {category.items.map((colorItem) => ( 153 <TouchableOpacity 154 key={colorItem.color} 155 style={styles.colorItem} 156 onPress={() => setSelectedColor(colorItem.color)} 157 activeOpacity={0.7} 158 > 159 <View 160 style={[ 161 styles.colorSwatch, 162 { backgroundColor: colorItem.color }, 163 selectedColor === colorItem.color && [ 164 styles.selectedSwatch, 165 { borderColor: colors.primary }, 166 ], 167 ]} 168 > 169 {selectedColor === colorItem.color && ( 170 <Ionicons name="checkmark" size={22} color="#fff" /> 171 )} 172 </View> 173 <Text 174 style={[ 175 styles.colorName, 176 { color: colors.secondaryText }, 177 ]} 178 numberOfLines={1} 179 > 180 {colorItem.name} 181 </Text> 182 </TouchableOpacity> 183 ))} 184 </View> 185 </View> 186 )} 187 style={styles.list} 188 showsVerticalScrollIndicator={true} 189 /> 190 191 <View 192 style={[ 193 styles.preview, 194 { 195 borderTopColor: colors.border, 196 backgroundColor: colors.background + '40', 197 }, 198 ]} 199 > 200 <View 201 style={[ 202 styles.previewSwatch, 203 { 204 backgroundColor: selectedColor, 205 borderColor: colors.border, 206 }, 207 ]} 208 /> 209 <View style={styles.previewInfo}> 210 <Text style={[styles.previewName, { color: colors.text }]}> 211 {selectedColorName} 212 </Text> 213 <Text 214 style={[styles.previewHex, { color: colors.secondaryText }]} 215 > 216 {selectedColor.toUpperCase()} 217 </Text> 218 </View> 219 </View> 220 221 <View style={[styles.actions, { borderTopColor: colors.border }]}> 222 <TouchableOpacity 223 style={[ 224 styles.button, 225 styles.cancelButton, 226 { backgroundColor: colors.background }, 227 ]} 228 onPress={onClose} 229 > 230 <Text style={[styles.buttonText, { color: colors.text }]}> 231 Cancel 232 </Text> 233 </TouchableOpacity> 234 <TouchableOpacity 235 style={[styles.button, { backgroundColor: selectedColor }]} 236 onPress={() => { 237 onColorSelected(selectedColor); 238 onClose(); 239 }} 240 > 241 <Text 242 style={[ 243 styles.buttonText, 244 { color: getContrastText(selectedColor) }, 245 ]} 246 > 247 Apply 248 </Text> 249 </TouchableOpacity> 250 </View> 251 </View> 252 </SafeAreaView> 253 </Modal> 254 ); 255 }; 256 257 const styles = StyleSheet.create({ 258 container: { 259 flex: 1, 260 backgroundColor: 'rgba(0,0,0,0.5)', 261 justifyContent: 'center', 262 alignItems: 'center', 263 padding: 16, 264 }, 265 modal: { 266 width: '94%', 267 maxWidth: 500, 268 maxHeight: '80%', 269 borderRadius: 16, 270 overflow: 'hidden', 271 shadowColor: '#000', 272 shadowOffset: { width: 0, height: 4 }, 273 shadowOpacity: 0.2, 274 shadowRadius: 8, 275 elevation: 8, 276 }, 277 header: { 278 flexDirection: 'row', 279 justifyContent: 'space-between', 280 alignItems: 'center', 281 padding: 16, 282 borderBottomWidth: 1, 283 }, 284 title: { 285 fontSize: 18, 286 fontWeight: '600', 287 }, 288 closeBtn: { 289 width: 32, 290 height: 32, 291 borderRadius: 16, 292 alignItems: 'center', 293 justifyContent: 'center', 294 }, 295 list: { 296 flexGrow: 0, 297 maxHeight: 400, 298 }, 299 categorySection: { 300 marginBottom: 16, 301 paddingHorizontal: 16, 302 }, 303 categoryTitle: { 304 fontSize: 16, 305 fontWeight: '600', 306 marginVertical: 8, 307 }, 308 colorGrid: { 309 flexDirection: 'row', 310 flexWrap: 'wrap', 311 }, 312 colorItem: { 313 width: '25%', // Fixed width for 4 columns 314 alignItems: 'center', 315 marginBottom: 12, 316 paddingHorizontal: 4, 317 }, 318 colorSwatch: { 319 width: 60, 320 height: 60, 321 borderRadius: Platform.OS === 'ios' ? 12 : 30, 322 alignItems: 'center', 323 justifyContent: 'center', 324 borderWidth: 2, 325 borderColor: 'transparent', 326 marginBottom: 4, 327 }, 328 selectedSwatch: { 329 transform: [{ scale: 1.05 }], 330 }, 331 colorName: { 332 fontSize: 11, 333 textAlign: 'center', 334 width: '100%', 335 }, 336 preview: { 337 padding: 16, 338 flexDirection: 'row', 339 alignItems: 'center', 340 borderTopWidth: 1, 341 }, 342 previewSwatch: { 343 width: 48, 344 height: 48, 345 borderRadius: 24, 346 marginRight: 16, 347 borderWidth: 1.5, 348 }, 349 previewInfo: { 350 flex: 1, 351 }, 352 previewName: { 353 fontSize: 16, 354 fontWeight: '600', 355 }, 356 previewHex: { 357 fontSize: 14, 358 marginTop: 2, 359 }, 360 actions: { 361 flexDirection: 'row', 362 padding: 16, 363 borderTopWidth: 1, 364 }, 365 button: { 366 flex: 1, 367 height: 44, 368 borderRadius: 10, 369 alignItems: 'center', 370 justifyContent: 'center', 371 }, 372 cancelButton: { 373 marginRight: 8, 374 }, 375 buttonText: { 376 fontSize: 16, 377 fontWeight: '600', 378 }, 379 }); 380 381 export default CustomColorPicker;