react-native-color-picker-palette 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +340 -0
  3. package/package.json +70 -0
  4. package/src/core/hooks/index.ts +2 -0
  5. package/src/core/hooks/useColor.ts +93 -0
  6. package/src/core/hooks/useComponentLayout.ts +38 -0
  7. package/src/core/index.ts +19 -0
  8. package/src/core/services/ColorService.ts +338 -0
  9. package/src/core/services/index.ts +1 -0
  10. package/src/core/types/index.ts +211 -0
  11. package/src/core/utils/clamp.ts +16 -0
  12. package/src/core/utils/format.ts +59 -0
  13. package/src/core/utils/index.ts +8 -0
  14. package/src/full/components/Alpha.tsx +221 -0
  15. package/src/full/components/ColorPicker.tsx +206 -0
  16. package/src/full/components/Fields/HexField.tsx +125 -0
  17. package/src/full/components/Fields/RgbFields.tsx +192 -0
  18. package/src/full/components/Fields/index.tsx +70 -0
  19. package/src/full/components/Hue.tsx +188 -0
  20. package/src/full/components/RectangleSaturation.tsx +203 -0
  21. package/src/full/components/Saturation.tsx +258 -0
  22. package/src/full/components/Thumb.tsx +47 -0
  23. package/src/full/components/Value.tsx +192 -0
  24. package/src/full/components/index.ts +8 -0
  25. package/src/full/index.ts +69 -0
  26. package/src/index.ts +19 -0
  27. package/src/lite/components/Alpha.tsx +228 -0
  28. package/src/lite/components/ColorPicker.tsx +209 -0
  29. package/src/lite/components/Fields/HexField.tsx +103 -0
  30. package/src/lite/components/Fields/RgbFields.tsx +138 -0
  31. package/src/lite/components/Fields/index.tsx +53 -0
  32. package/src/lite/components/Hue.tsx +192 -0
  33. package/src/lite/components/RectangleSaturation.tsx +238 -0
  34. package/src/lite/components/Saturation.tsx +289 -0
  35. package/src/lite/components/Thumb.tsx +47 -0
  36. package/src/lite/components/Value.tsx +201 -0
  37. package/src/lite/components/index.ts +8 -0
  38. package/src/lite/index.ts +75 -0
@@ -0,0 +1,221 @@
1
+ import React, { memo, useCallback, useMemo, useRef, useState } from 'react';
2
+ import {
3
+ View,
4
+ StyleSheet,
5
+ PanResponder,
6
+ GestureResponderEvent,
7
+ LayoutChangeEvent,
8
+ } from 'react-native';
9
+ import Svg, { Defs, LinearGradient, Stop, Rect } from 'react-native-svg';
10
+ import type { IAlphaProps } from '../../core/types';
11
+ import { ColorService } from '../../core/services';
12
+ import { clamp } from '../../core/utils';
13
+ import { Thumb } from './Thumb';
14
+
15
+ /**
16
+ * Alpha (transparency) slider bar component
17
+ *
18
+ * Features:
19
+ * - Uses react-native-svg for gradient
20
+ * - Caches measurements for better performance
21
+ * - Static checkerboard pattern (not generated on each render)
22
+ * - Horizontal bar with gradient from transparent to solid
23
+ *
24
+ * @example
25
+ * ```tsx
26
+ * <Alpha
27
+ * color={color}
28
+ * onChange={handleChange}
29
+ * barHeight={10}
30
+ * thumbSize={24}
31
+ * />
32
+ * ```
33
+ */
34
+ export const Alpha = memo(
35
+ ({
36
+ color,
37
+ onChange,
38
+ onChangeComplete,
39
+ barHeight,
40
+ thumbSize,
41
+ disabled = false,
42
+ }: IAlphaProps) => {
43
+ const containerRef = useRef<View>(null);
44
+ const [width, setWidth] = useState(1);
45
+ const measurementCache = useRef<number | null>(null);
46
+
47
+ // Calculate thumb position from alpha value (a=1 should be at right)
48
+ const thumbPosition = useMemo(() => {
49
+ return color.hsv.a * width;
50
+ }, [color.hsv.a, width]);
51
+
52
+ // Update measurement cache
53
+ const updateMeasurement = useCallback(() => {
54
+ return new Promise<number>((resolve) => {
55
+ if (measurementCache.current !== null) {
56
+ resolve(measurementCache.current);
57
+ return;
58
+ }
59
+ containerRef.current?.measureInWindow((x) => {
60
+ measurementCache.current = x;
61
+ resolve(x);
62
+ });
63
+ });
64
+ }, []);
65
+
66
+ // Convert touch position to alpha
67
+ const updateColorFromPosition = useCallback(
68
+ async (pageX: number, isFinal: boolean) => {
69
+ const wx = await updateMeasurement();
70
+ const x = clamp(pageX - wx, 0, width);
71
+ const alpha = x / width;
72
+
73
+ const nextColor = ColorService.convert('hsv', {
74
+ ...color.hsv,
75
+ a: clamp(alpha, 0, 1),
76
+ });
77
+
78
+ onChange(nextColor);
79
+
80
+ if (isFinal) {
81
+ onChangeComplete?.(nextColor);
82
+ }
83
+ },
84
+ [color.hsv, width, onChange, onChangeComplete, updateMeasurement]
85
+ );
86
+
87
+ // PanResponder for touch handling
88
+ const panResponder = useMemo(
89
+ () =>
90
+ PanResponder.create({
91
+ onStartShouldSetPanResponder: () => !disabled,
92
+ onMoveShouldSetPanResponder: () => !disabled,
93
+ onPanResponderGrant: (evt: GestureResponderEvent) => {
94
+ measurementCache.current = null;
95
+ updateColorFromPosition(evt.nativeEvent.pageX, false);
96
+ },
97
+ onPanResponderMove: (evt: GestureResponderEvent) => {
98
+ updateColorFromPosition(evt.nativeEvent.pageX, false);
99
+ },
100
+ onPanResponderRelease: (evt: GestureResponderEvent) => {
101
+ updateColorFromPosition(evt.nativeEvent.pageX, true);
102
+ },
103
+ }),
104
+ [disabled, updateColorFromPosition]
105
+ );
106
+
107
+ // Handle layout to get width and clear cache
108
+ const handleLayout = useCallback((event: LayoutChangeEvent) => {
109
+ setWidth(event.nativeEvent.layout.width);
110
+ measurementCache.current = null;
111
+ }, []);
112
+
113
+ // Solid color for gradient end
114
+ const solidColor = `rgb(${color.rgb.r}, ${color.rgb.g}, ${color.rgb.b})`;
115
+
116
+ return (
117
+ <View
118
+ ref={containerRef}
119
+ style={[
120
+ styles.container,
121
+ {
122
+ height: barHeight + thumbSize,
123
+ opacity: disabled ? 0.5 : 1,
124
+ },
125
+ ]}
126
+ onLayout={handleLayout}
127
+ {...panResponder.panHandlers}
128
+ >
129
+ {/* Bar with checkerboard and gradient */}
130
+ <View style={[styles.barContainer, { height: barHeight, top: thumbSize / 2 }]}>
131
+ {/* Checkerboard background - dynamic size based on barHeight */}
132
+ <View style={[styles.checkerboard, { borderRadius: barHeight / 2 }]}>
133
+ {Array.from({ length: 2 }).map((_, rowIndex) => {
134
+ const checkerSize = barHeight / 2;
135
+ const cellCount = Math.ceil(width / checkerSize) + 1;
136
+ return (
137
+ <View key={rowIndex} style={[styles.checkerRow, { height: checkerSize }]}>
138
+ {Array.from({ length: cellCount }).map((_, i) => (
139
+ <View
140
+ key={i}
141
+ style={{
142
+ width: checkerSize,
143
+ height: checkerSize,
144
+ backgroundColor: (i + rowIndex) % 2 === 0 ? '#FFFFFF' : '#CCCCCC',
145
+ }}
146
+ />
147
+ ))}
148
+ </View>
149
+ );
150
+ })}
151
+ </View>
152
+
153
+ {/* Color gradient overlay using SVG */}
154
+ <View style={[styles.gradient, { borderRadius: barHeight / 2 }]}>
155
+ <Svg width="100%" height={barHeight}>
156
+ <Defs>
157
+ <LinearGradient id="alphaGradient" x1="0%" y1="0%" x2="100%" y2="0%">
158
+ <Stop offset="0%" stopColor={solidColor} stopOpacity="0" />
159
+ <Stop offset="100%" stopColor={solidColor} stopOpacity="1" />
160
+ </LinearGradient>
161
+ </Defs>
162
+ <Rect
163
+ x="0"
164
+ y="0"
165
+ width="100%"
166
+ height={barHeight}
167
+ rx={barHeight / 2}
168
+ ry={barHeight / 2}
169
+ fill="url(#alphaGradient)"
170
+ />
171
+ </Svg>
172
+ </View>
173
+ </View>
174
+
175
+ {/* Thumb indicator - centered vertically with bar */}
176
+ <View
177
+ style={[
178
+ styles.thumbContainer,
179
+ {
180
+ left: thumbPosition - thumbSize / 2,
181
+ top: barHeight / 2,
182
+ },
183
+ ]}
184
+ pointerEvents="none"
185
+ >
186
+ <Thumb size={thumbSize} color={`rgba(${color.rgb.r}, ${color.rgb.g}, ${color.rgb.b}, ${color.hsv.a})`} />
187
+ </View>
188
+ </View>
189
+ );
190
+ }
191
+ );
192
+
193
+ const styles = StyleSheet.create({
194
+ container: {
195
+ position: 'relative',
196
+ width: '100%',
197
+ },
198
+ barContainer: {
199
+ position: 'absolute',
200
+ left: 0,
201
+ right: 0,
202
+ overflow: 'hidden',
203
+ },
204
+ checkerboard: {
205
+ ...StyleSheet.absoluteFillObject,
206
+ overflow: 'hidden',
207
+ },
208
+ checkerRow: {
209
+ flexDirection: 'row',
210
+ },
211
+ gradient: {
212
+ ...StyleSheet.absoluteFillObject,
213
+ overflow: 'hidden',
214
+ },
215
+ thumbContainer: {
216
+ position: 'absolute',
217
+ zIndex: 10,
218
+ },
219
+ });
220
+
221
+ Alpha.displayName = 'Alpha';
@@ -0,0 +1,206 @@
1
+ import React, { memo } from 'react';
2
+ import { View, StyleSheet } from 'react-native';
3
+ import type { IColorPickerProps } from '../../core/types';
4
+ import { Saturation } from './Saturation';
5
+ import { RectangleSaturation } from './RectangleSaturation';
6
+ import { Hue } from './Hue';
7
+ import { Alpha } from './Alpha';
8
+ import { Value } from './Value';
9
+ import { Fields } from './Fields';
10
+
11
+ /**
12
+ * Main ColorPicker component (Full version with react-native-svg)
13
+ *
14
+ * Features:
15
+ * - Circular color wheel or rectangle for saturation/value selection
16
+ * - Horizontal hue bar
17
+ * - Horizontal alpha bar with checkerboard background
18
+ * - HEX and RGB input fields
19
+ * - Circle thumbs with white 2px border showing selected color
20
+ *
21
+ * @example
22
+ * ```tsx
23
+ * import { ColorPicker, useColor } from 'react-native-color-palette';
24
+ *
25
+ * function MyComponent() {
26
+ * const [color, setColor] = useColor('#FF0000');
27
+ *
28
+ * return (
29
+ * <ColorPicker
30
+ * color={color}
31
+ * onChange={setColor}
32
+ * variant="rectangle"
33
+ * onChangeComplete={(c) => console.log('Final color:', c.hex)}
34
+ * />
35
+ * );
36
+ * }
37
+ * ```
38
+ */
39
+ export const ColorPicker = memo(
40
+ ({
41
+ color,
42
+ onChange,
43
+ onChangeComplete,
44
+ variant = 'rectangle',
45
+ width = 250,
46
+ barHeight = 10,
47
+ thumbSize = 24,
48
+ hideHue = false,
49
+ hideAlpha = false,
50
+ hidePreview = false,
51
+ hideInput = false,
52
+ disabled = false,
53
+ }: IColorPickerProps) => {
54
+ // Rectangle height is 60% of width for better proportions
55
+ const rectangleHeight = width * 0.6;
56
+
57
+ return (
58
+ <View style={[styles.container, disabled && styles.disabled]}>
59
+ {/* Color picker (circle or rectangle) */}
60
+ <View style={styles.pickerContainer}>
61
+ {variant === 'circle' ? (
62
+ <Saturation
63
+ color={color}
64
+ onChange={onChange}
65
+ onChangeComplete={onChangeComplete}
66
+ size={width}
67
+ thumbSize={thumbSize}
68
+ disabled={disabled}
69
+ />
70
+ ) : (
71
+ <RectangleSaturation
72
+ color={color}
73
+ onChange={onChange}
74
+ onChangeComplete={onChangeComplete}
75
+ width={width}
76
+ height={rectangleHeight}
77
+ thumbSize={thumbSize}
78
+ disabled={disabled}
79
+ />
80
+ )}
81
+ </View>
82
+
83
+ {/* Color preview */}
84
+ {!hidePreview && (
85
+ <View style={styles.previewContainer}>
86
+ <View
87
+ style={[
88
+ styles.preview,
89
+ {
90
+ backgroundColor: color.hex,
91
+ },
92
+ ]}
93
+ />
94
+ <View
95
+ style={[
96
+ styles.previewAlpha,
97
+ {
98
+ backgroundColor: `rgba(${color.rgb.r}, ${color.rgb.g}, ${color.rgb.b}, ${color.rgb.a})`,
99
+ },
100
+ ]}
101
+ />
102
+ </View>
103
+ )}
104
+
105
+ {/* Sliders section */}
106
+ <View style={styles.slidersContainer}>
107
+ {/* Hue slider - always shown for rectangle variant */}
108
+ {(!hideHue || variant === 'rectangle') && (
109
+ <View style={styles.sliderRow}>
110
+ <Hue
111
+ color={color}
112
+ onChange={onChange}
113
+ onChangeComplete={onChangeComplete}
114
+ barHeight={barHeight}
115
+ thumbSize={thumbSize}
116
+ disabled={disabled}
117
+ />
118
+ </View>
119
+ )}
120
+
121
+ {/* Value/Brightness slider - only for circle variant */}
122
+ {variant === 'circle' && (
123
+ <View style={styles.sliderRow}>
124
+ <Value
125
+ color={color}
126
+ onChange={onChange}
127
+ onChangeComplete={onChangeComplete}
128
+ barHeight={barHeight}
129
+ thumbSize={thumbSize}
130
+ disabled={disabled}
131
+ />
132
+ </View>
133
+ )}
134
+
135
+ {/* Alpha slider */}
136
+ {!hideAlpha && (
137
+ <View style={styles.sliderRow}>
138
+ <Alpha
139
+ color={color}
140
+ onChange={onChange}
141
+ onChangeComplete={onChangeComplete}
142
+ barHeight={barHeight}
143
+ thumbSize={thumbSize}
144
+ disabled={disabled}
145
+ />
146
+ </View>
147
+ )}
148
+ </View>
149
+
150
+ {/* Input fields */}
151
+ {!hideInput && (
152
+ <View style={styles.fieldsContainer}>
153
+ <Fields
154
+ color={color}
155
+ onChange={onChange}
156
+ onChangeComplete={onChangeComplete}
157
+ disabled={disabled}
158
+ />
159
+ </View>
160
+ )}
161
+ </View>
162
+ );
163
+ }
164
+ );
165
+
166
+ const styles = StyleSheet.create({
167
+ container: {
168
+ alignItems: 'center',
169
+ padding: 16,
170
+ },
171
+ disabled: {
172
+ opacity: 0.6,
173
+ },
174
+ pickerContainer: {
175
+ marginBottom: 20,
176
+ },
177
+ previewContainer: {
178
+ flexDirection: 'row',
179
+ marginBottom: 20,
180
+ borderRadius: 8,
181
+ overflow: 'hidden',
182
+ borderWidth: 1,
183
+ borderColor: '#E0E0E0',
184
+ },
185
+ preview: {
186
+ width: 40,
187
+ height: 40,
188
+ },
189
+ previewAlpha: {
190
+ width: 40,
191
+ height: 40,
192
+ },
193
+ slidersContainer: {
194
+ width: '100%',
195
+ gap: 16,
196
+ marginBottom: 20,
197
+ },
198
+ sliderRow: {
199
+ width: '100%',
200
+ },
201
+ fieldsContainer: {
202
+ width: '100%',
203
+ },
204
+ });
205
+
206
+ ColorPicker.displayName = 'ColorPicker';
@@ -0,0 +1,125 @@
1
+ import React, { memo, useState, useCallback, useEffect } from 'react';
2
+ import { View, TextInput, Text, StyleSheet } from 'react-native';
3
+ import type { IFieldsProps } from '../../../core/types';
4
+ import { ColorService } from '../../../core/services';
5
+
6
+ /**
7
+ * Hex color input field
8
+ *
9
+ * Features:
10
+ * - Shows and edits hex value (e.g., #FF5500)
11
+ * - Validates input on blur
12
+ * - Auto-capitalizes and adds # prefix
13
+ *
14
+ * @example
15
+ * ```tsx
16
+ * <HexField
17
+ * color={color}
18
+ * onChange={handleChange}
19
+ * onChangeComplete={handleChangeComplete}
20
+ * />
21
+ * ```
22
+ */
23
+ export const HexField = memo(
24
+ ({
25
+ color,
26
+ onChange,
27
+ onChangeComplete,
28
+ disabled = false,
29
+ }: IFieldsProps) => {
30
+ const [inputValue, setInputValue] = useState(color.hex);
31
+ const [isFocused, setIsFocused] = useState(false);
32
+
33
+ // Update input when color changes externally
34
+ useEffect(() => {
35
+ if (!isFocused) {
36
+ setInputValue(color.hex);
37
+ }
38
+ }, [color.hex, isFocused]);
39
+
40
+ // Handle text change
41
+ const handleChangeText = useCallback((text: string) => {
42
+ // Add # if not present
43
+ let value = text.toUpperCase();
44
+ if (!value.startsWith('#')) {
45
+ value = '#' + value;
46
+ }
47
+ // Limit to valid hex length
48
+ value = value.slice(0, 9); // #RRGGBBAA max
49
+ setInputValue(value);
50
+ }, []);
51
+
52
+ // Validate and apply color on blur
53
+ const handleBlur = useCallback(() => {
54
+ setIsFocused(false);
55
+
56
+ // Validate hex format
57
+ const hexRegex = /^#([A-Fa-f0-9]{3}|[A-Fa-f0-9]{4}|[A-Fa-f0-9]{6}|[A-Fa-f0-9]{8})$/;
58
+
59
+ if (hexRegex.test(inputValue)) {
60
+ const nextColor = ColorService.convert('hex', inputValue);
61
+ onChange(nextColor);
62
+ onChangeComplete?.(nextColor);
63
+ } else {
64
+ // Reset to current color if invalid
65
+ setInputValue(color.hex);
66
+ }
67
+ }, [inputValue, color.hex, onChange, onChangeComplete]);
68
+
69
+ const handleFocus = useCallback(() => {
70
+ setIsFocused(true);
71
+ }, []);
72
+
73
+ return (
74
+ <View style={styles.container}>
75
+ <Text style={styles.label}>HEX</Text>
76
+ <TextInput
77
+ style={[
78
+ styles.input,
79
+ disabled && styles.inputDisabled,
80
+ ]}
81
+ value={inputValue}
82
+ onChangeText={handleChangeText}
83
+ onBlur={handleBlur}
84
+ onFocus={handleFocus}
85
+ editable={!disabled}
86
+ autoCapitalize="characters"
87
+ autoCorrect={false}
88
+ maxLength={9}
89
+ placeholder="#000000"
90
+ placeholderTextColor="#999999"
91
+ numberOfLines={1}
92
+ />
93
+ </View>
94
+ );
95
+ }
96
+ );
97
+
98
+ const styles = StyleSheet.create({
99
+ container: {
100
+ flex: 1,
101
+ },
102
+ label: {
103
+ fontSize: 10,
104
+ fontWeight: '600',
105
+ color: '#666666',
106
+ marginBottom: 4,
107
+ textAlign: 'center',
108
+ },
109
+ input: {
110
+ height: 36,
111
+ borderWidth: 1,
112
+ borderColor: '#E0E0E0',
113
+ borderRadius: 6,
114
+ fontSize: 14,
115
+ color: '#333333',
116
+ backgroundColor: '#FFFFFF',
117
+ textAlign: 'center',
118
+ },
119
+ inputDisabled: {
120
+ backgroundColor: '#F5F5F5',
121
+ color: '#999999',
122
+ },
123
+ });
124
+
125
+ HexField.displayName = 'HexField';