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,258 @@
1
+ import React, { memo, useCallback, useMemo, useRef } from 'react';
2
+ import {
3
+ View,
4
+ StyleSheet,
5
+ PanResponder,
6
+ GestureResponderEvent,
7
+ } from 'react-native';
8
+ import Svg, { Defs, RadialGradient, Stop, Circle, Path, G } from 'react-native-svg';
9
+ import type { ISaturationProps } from '../../core/types';
10
+ import { ColorService } from '../../core/services';
11
+ import { clamp } from '../../core/utils';
12
+ import { Thumb } from './Thumb';
13
+
14
+ // Number of segments for perfect color wheel (360 = 1° per segment)
15
+ const SEGMENT_COUNT = 360;
16
+
17
+ /**
18
+ * Circular Color Wheel picker component
19
+ *
20
+ * Features:
21
+ * - Uses 360 SVG segments (perfect 1° precision)
22
+ * - Caches measurements for better performance
23
+ * - Angle around circle = Hue (0-360°)
24
+ * - Distance from center = Saturation (0-100%)
25
+ *
26
+ * @example
27
+ * ```tsx
28
+ * <Saturation
29
+ * color={color}
30
+ * onChange={handleChange}
31
+ * size={250}
32
+ * thumbSize={24}
33
+ * />
34
+ * ```
35
+ */
36
+ export const Saturation = memo(
37
+ ({
38
+ color,
39
+ onChange,
40
+ onChangeComplete,
41
+ size,
42
+ thumbSize,
43
+ disabled = false,
44
+ }: ISaturationProps) => {
45
+ const containerRef = useRef<View>(null);
46
+ const measurementCache = useRef<{ x: number; y: number } | null>(null);
47
+
48
+ const radius = size / 2;
49
+ const effectiveRadius = radius - thumbSize / 2;
50
+
51
+ // Calculate thumb position from HSV values
52
+ const thumbPosition = useMemo(() => {
53
+ const angleRad = ((color.hsv.h - 90) * Math.PI) / 180;
54
+ const distance = (color.hsv.s / 100) * effectiveRadius;
55
+
56
+ const x = radius + Math.cos(angleRad) * distance - thumbSize / 2;
57
+ const y = radius + Math.sin(angleRad) * distance - thumbSize / 2;
58
+
59
+ return { x, y };
60
+ }, [color.hsv.h, color.hsv.s, radius, effectiveRadius, thumbSize]);
61
+
62
+ // Update measurement cache
63
+ const updateMeasurement = useCallback(() => {
64
+ return new Promise<{ x: number; y: number }>((resolve) => {
65
+ if (measurementCache.current) {
66
+ resolve(measurementCache.current);
67
+ return;
68
+ }
69
+ containerRef.current?.measureInWindow((x, y) => {
70
+ measurementCache.current = { x, y };
71
+ resolve({ x, y });
72
+ });
73
+ });
74
+ }, []);
75
+
76
+ // Convert touch coordinates to HSV
77
+ const updateColorFromPosition = useCallback(
78
+ async (pageX: number, pageY: number, isFinal: boolean) => {
79
+ const measurement = await updateMeasurement();
80
+
81
+ const x = pageX - measurement.x - radius;
82
+ const y = pageY - measurement.y - radius;
83
+
84
+ // Calculate angle (hue): 0° at top, clockwise
85
+ let angleDeg = (Math.atan2(y, x) * 180) / Math.PI + 90;
86
+ if (angleDeg < 0) angleDeg += 360;
87
+
88
+ // Calculate distance (saturation)
89
+ const distance = Math.min(Math.sqrt(x * x + y * y), effectiveRadius);
90
+ const saturation = (distance / effectiveRadius) * 100;
91
+
92
+ const nextColor = ColorService.convert('hsv', {
93
+ h: clamp(angleDeg, 0, 360),
94
+ s: clamp(saturation, 0, 100),
95
+ v: color.hsv.v,
96
+ a: color.hsv.a,
97
+ });
98
+
99
+ onChange(nextColor);
100
+
101
+ if (isFinal) {
102
+ onChangeComplete?.(nextColor);
103
+ }
104
+ },
105
+ [color.hsv.v, color.hsv.a, radius, effectiveRadius, onChange, onChangeComplete, updateMeasurement]
106
+ );
107
+
108
+ // Clear cache on layout change
109
+ const handleLayout = useCallback(() => {
110
+ measurementCache.current = null;
111
+ }, []);
112
+
113
+ // PanResponder for touch handling
114
+ const panResponder = useMemo(
115
+ () =>
116
+ PanResponder.create({
117
+ onStartShouldSetPanResponder: () => !disabled,
118
+ onMoveShouldSetPanResponder: () => !disabled,
119
+ onPanResponderGrant: (evt: GestureResponderEvent) => {
120
+ measurementCache.current = null;
121
+ updateColorFromPosition(
122
+ evt.nativeEvent.pageX,
123
+ evt.nativeEvent.pageY,
124
+ false
125
+ );
126
+ },
127
+ onPanResponderMove: (evt: GestureResponderEvent) => {
128
+ updateColorFromPosition(
129
+ evt.nativeEvent.pageX,
130
+ evt.nativeEvent.pageY,
131
+ false
132
+ );
133
+ },
134
+ onPanResponderRelease: (evt: GestureResponderEvent) => {
135
+ updateColorFromPosition(
136
+ evt.nativeEvent.pageX,
137
+ evt.nativeEvent.pageY,
138
+ true
139
+ );
140
+ },
141
+ }),
142
+ [disabled, updateColorFromPosition]
143
+ );
144
+
145
+ // Generate color wheel paths (360 segments - 1° per segment)
146
+ const colorWheelPaths = useMemo(() => {
147
+ const paths: { d: string; color: string }[] = [];
148
+ const angleStep = 360 / SEGMENT_COUNT;
149
+
150
+ for (let i = 0; i < SEGMENT_COUNT; i++) {
151
+ const startAngle = (i * angleStep - 90) * (Math.PI / 180);
152
+ const endAngle = ((i + 1) * angleStep - 90) * (Math.PI / 180);
153
+
154
+ const x1 = radius + Math.cos(startAngle) * radius;
155
+ const y1 = radius + Math.sin(startAngle) * radius;
156
+ const x2 = radius + Math.cos(endAngle) * radius;
157
+ const y2 = radius + Math.sin(endAngle) * radius;
158
+
159
+ // Use middle hue for the segment
160
+ const hue = (i + 0.5) * angleStep;
161
+
162
+ paths.push({
163
+ d: `M ${radius} ${radius} L ${x1} ${y1} A ${radius} ${radius} 0 0 1 ${x2} ${y2} Z`,
164
+ color: `hsl(${hue}, 100%, 50%)`,
165
+ });
166
+ }
167
+
168
+ return paths;
169
+ }, [radius]);
170
+
171
+ return (
172
+ <View
173
+ ref={containerRef}
174
+ onLayout={handleLayout}
175
+ style={[
176
+ styles.container,
177
+ {
178
+ width: size,
179
+ height: size,
180
+ opacity: disabled ? 0.5 : 1,
181
+ },
182
+ ]}
183
+ {...panResponder.panHandlers}
184
+ >
185
+ <Svg width={size} height={size} viewBox={`0 0 ${size} ${size}`}>
186
+ <Defs>
187
+ <RadialGradient
188
+ id="saturationGradient"
189
+ cx="50%"
190
+ cy="50%"
191
+ rx="50%"
192
+ ry="50%"
193
+ >
194
+ <Stop offset="0%" stopColor="#FFFFFF" stopOpacity="1" />
195
+ <Stop offset="100%" stopColor="#FFFFFF" stopOpacity="0" />
196
+ </RadialGradient>
197
+ </Defs>
198
+
199
+ {/* Color wheel segments */}
200
+ <G>
201
+ {colorWheelPaths.map((segment, index) => (
202
+ <Path
203
+ key={index}
204
+ d={segment.d}
205
+ fill={segment.color}
206
+ stroke={segment.color}
207
+ strokeWidth={1}
208
+ />
209
+ ))}
210
+ </G>
211
+
212
+ {/* Saturation overlay (white center fading out) */}
213
+ <Circle
214
+ cx={radius}
215
+ cy={radius}
216
+ r={radius}
217
+ fill="url(#saturationGradient)"
218
+ />
219
+
220
+ {/* Brightness overlay */}
221
+ <Circle
222
+ cx={radius}
223
+ cy={radius}
224
+ r={radius}
225
+ fill="#000000"
226
+ fillOpacity={1 - color.hsv.v / 100}
227
+ />
228
+ </Svg>
229
+
230
+ {/* Thumb indicator with selected color */}
231
+ <View
232
+ style={[
233
+ styles.thumbContainer,
234
+ {
235
+ left: thumbPosition.x,
236
+ top: thumbPosition.y,
237
+ },
238
+ ]}
239
+ pointerEvents="none"
240
+ >
241
+ <Thumb size={thumbSize} />
242
+ </View>
243
+ </View>
244
+ );
245
+ }
246
+ );
247
+
248
+ const styles = StyleSheet.create({
249
+ container: {
250
+ position: 'relative',
251
+ },
252
+ thumbContainer: {
253
+ position: 'absolute',
254
+ zIndex: 10,
255
+ },
256
+ });
257
+
258
+ Saturation.displayName = 'Saturation';
@@ -0,0 +1,47 @@
1
+ import React, { memo } from 'react';
2
+ import { View, StyleSheet, ViewStyle } from 'react-native';
3
+
4
+ interface ThumbProps {
5
+ size: number;
6
+ color?: string;
7
+ style?: ViewStyle;
8
+ }
9
+
10
+ /**
11
+ * Thumb component - Circle indicator for color pickers
12
+ *
13
+ * Features:
14
+ * - Shows the selected color inside (if provided)
15
+ * - White 2px border for visibility on any background
16
+ * - Shadow for depth
17
+ *
18
+ * @example
19
+ * ```tsx
20
+ * <Thumb size={24} color="#FF0000" />
21
+ * ```
22
+ */
23
+ export const Thumb = memo(({ size, color, style }: ThumbProps) => {
24
+ const thumbStyle: ViewStyle = {
25
+ width: size,
26
+ height: size,
27
+ borderRadius: size / 2,
28
+ backgroundColor: color || 'transparent',
29
+ };
30
+
31
+ return <View style={[styles.thumb, thumbStyle, style]} />;
32
+ });
33
+
34
+ const styles = StyleSheet.create({
35
+ thumb: {
36
+ borderWidth: 2,
37
+ borderColor: '#FFFFFF',
38
+ // Shadow for visibility on any background
39
+ shadowColor: '#000000',
40
+ shadowOffset: { width: 0, height: 1 },
41
+ shadowOpacity: 0.4,
42
+ shadowRadius: 2,
43
+ elevation: 4,
44
+ },
45
+ });
46
+
47
+ Thumb.displayName = 'Thumb';
@@ -0,0 +1,192 @@
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 { IValueProps } from '../../core/types';
11
+ import { ColorService } from '../../core/services';
12
+ import { clamp } from '../../core/utils';
13
+ import { Thumb } from './Thumb';
14
+
15
+ /**
16
+ * Value/Brightness slider bar component
17
+ *
18
+ * Features:
19
+ * - Uses react-native-svg for smooth gradient
20
+ * - Horizontal bar from black to full brightness
21
+ * - Used with circle variant for brightness control
22
+ *
23
+ * @example
24
+ * ```tsx
25
+ * <Value
26
+ * color={color}
27
+ * onChange={handleChange}
28
+ * barHeight={10}
29
+ * thumbSize={24}
30
+ * />
31
+ * ```
32
+ */
33
+ export const Value = memo(
34
+ ({
35
+ color,
36
+ onChange,
37
+ onChangeComplete,
38
+ barHeight,
39
+ thumbSize,
40
+ disabled = false,
41
+ }: IValueProps) => {
42
+ const containerRef = useRef<View>(null);
43
+ const [width, setWidth] = useState(1);
44
+ const measurementCache = useRef<number | null>(null);
45
+
46
+ // Calculate thumb position from value (v=100 should be at right)
47
+ const thumbPosition = useMemo(() => {
48
+ return (color.hsv.v / 100) * width;
49
+ }, [color.hsv.v, width]);
50
+
51
+ // Update measurement cache
52
+ const updateMeasurement = useCallback(() => {
53
+ return new Promise<number>((resolve) => {
54
+ if (measurementCache.current !== null) {
55
+ resolve(measurementCache.current);
56
+ return;
57
+ }
58
+ containerRef.current?.measureInWindow((x) => {
59
+ measurementCache.current = x;
60
+ resolve(x);
61
+ });
62
+ });
63
+ }, []);
64
+
65
+ // Convert touch position to value
66
+ const updateColorFromPosition = useCallback(
67
+ async (pageX: number, isFinal: boolean) => {
68
+ const wx = await updateMeasurement();
69
+ const x = clamp(pageX - wx, 0, width);
70
+ const value = (x / width) * 100;
71
+
72
+ const nextColor = ColorService.convert('hsv', {
73
+ ...color.hsv,
74
+ v: clamp(value, 0, 100),
75
+ });
76
+
77
+ onChange(nextColor);
78
+
79
+ if (isFinal) {
80
+ onChangeComplete?.(nextColor);
81
+ }
82
+ },
83
+ [color.hsv, width, onChange, onChangeComplete, updateMeasurement]
84
+ );
85
+
86
+ // PanResponder for touch handling
87
+ const panResponder = useMemo(
88
+ () =>
89
+ PanResponder.create({
90
+ onStartShouldSetPanResponder: () => !disabled,
91
+ onMoveShouldSetPanResponder: () => !disabled,
92
+ onPanResponderGrant: (evt: GestureResponderEvent) => {
93
+ measurementCache.current = null;
94
+ updateColorFromPosition(evt.nativeEvent.pageX, false);
95
+ },
96
+ onPanResponderMove: (evt: GestureResponderEvent) => {
97
+ updateColorFromPosition(evt.nativeEvent.pageX, false);
98
+ },
99
+ onPanResponderRelease: (evt: GestureResponderEvent) => {
100
+ updateColorFromPosition(evt.nativeEvent.pageX, true);
101
+ },
102
+ }),
103
+ [disabled, updateColorFromPosition]
104
+ );
105
+
106
+ // Handle layout to get width and clear cache
107
+ const handleLayout = useCallback((event: LayoutChangeEvent) => {
108
+ setWidth(event.nativeEvent.layout.width);
109
+ measurementCache.current = null;
110
+ }, []);
111
+
112
+ // Full brightness color for gradient end
113
+ const { h, s } = color.hsv;
114
+ const fullBrightnessColor = `hsl(${h}, ${s}%, ${50 - s / 4}%)`;
115
+
116
+ // Calculate thumb color
117
+ const thumbColor = useMemo(() => {
118
+ const { h, s, v } = color.hsv;
119
+ const l = (v / 200) * (200 - s);
120
+ return `hsl(${h}, ${s}%, ${l}%)`;
121
+ }, [color.hsv.h, color.hsv.s, color.hsv.v]);
122
+
123
+ return (
124
+ <View
125
+ ref={containerRef}
126
+ style={[
127
+ styles.container,
128
+ {
129
+ height: barHeight + thumbSize,
130
+ opacity: disabled ? 0.5 : 1,
131
+ },
132
+ ]}
133
+ onLayout={handleLayout}
134
+ {...panResponder.panHandlers}
135
+ >
136
+ {/* Gradient bar using SVG */}
137
+ <View style={[styles.barContainer, { height: barHeight, top: thumbSize / 2 }]}>
138
+ <Svg width="100%" height={barHeight}>
139
+ <Defs>
140
+ <LinearGradient id="valueGradient" x1="0%" y1="0%" x2="100%" y2="0%">
141
+ <Stop offset="0%" stopColor="#000000" />
142
+ <Stop offset="100%" stopColor={fullBrightnessColor} />
143
+ </LinearGradient>
144
+ </Defs>
145
+ <Rect
146
+ x="0"
147
+ y="0"
148
+ width="100%"
149
+ height={barHeight}
150
+ rx={barHeight / 2}
151
+ ry={barHeight / 2}
152
+ fill="url(#valueGradient)"
153
+ />
154
+ </Svg>
155
+ </View>
156
+
157
+ {/* Thumb indicator - centered vertically with bar */}
158
+ <View
159
+ style={[
160
+ styles.thumbContainer,
161
+ {
162
+ left: thumbPosition - thumbSize / 2,
163
+ top: barHeight / 2,
164
+ },
165
+ ]}
166
+ pointerEvents="none"
167
+ >
168
+ <Thumb size={thumbSize} color={thumbColor} />
169
+ </View>
170
+ </View>
171
+ );
172
+ }
173
+ );
174
+
175
+ const styles = StyleSheet.create({
176
+ container: {
177
+ position: 'relative',
178
+ width: '100%',
179
+ },
180
+ barContainer: {
181
+ position: 'absolute',
182
+ left: 0,
183
+ right: 0,
184
+ overflow: 'hidden',
185
+ },
186
+ thumbContainer: {
187
+ position: 'absolute',
188
+ zIndex: 10,
189
+ },
190
+ });
191
+
192
+ Value.displayName = 'Value';
@@ -0,0 +1,8 @@
1
+ export { ColorPicker } from './ColorPicker';
2
+ export { Saturation } from './Saturation';
3
+ export { RectangleSaturation } from './RectangleSaturation';
4
+ export { Hue } from './Hue';
5
+ export { Alpha } from './Alpha';
6
+ export { Value } from './Value';
7
+ export { Thumb } from './Thumb';
8
+ export { Fields, HexField, RgbFields } from './Fields';
@@ -0,0 +1,69 @@
1
+ /**
2
+ * React Native Color Palette - Full Version
3
+ *
4
+ * This version uses react-native-svg for smooth gradients and
5
+ * precise color rendering.
6
+ *
7
+ * @example
8
+ * ```tsx
9
+ * import { ColorPicker, useColor } from 'react-native-color-palette';
10
+ *
11
+ * function MyComponent() {
12
+ * const [color, setColor] = useColor('#FF0000');
13
+ *
14
+ * return (
15
+ * <ColorPicker
16
+ * color={color}
17
+ * onChange={setColor}
18
+ * onChangeComplete={(color) => console.log('Selected:', color.hex)}
19
+ * width={250}
20
+ * barHeight={10}
21
+ * thumbSize={24}
22
+ * hideAlpha={false}
23
+ * hideInput={false}
24
+ * />
25
+ * );
26
+ * }
27
+ * ```
28
+ */
29
+
30
+ // Main component
31
+ export { ColorPicker } from './components';
32
+
33
+ // Individual components for custom layouts
34
+ export {
35
+ Saturation,
36
+ RectangleSaturation,
37
+ Hue,
38
+ Alpha,
39
+ Value,
40
+ Thumb,
41
+ Fields,
42
+ HexField,
43
+ RgbFields,
44
+ } from './components';
45
+
46
+ // Re-export core utilities
47
+ export {
48
+ ColorService,
49
+ useColor,
50
+ useColorWithCallback,
51
+ useComponentLayout,
52
+ } from '../core';
53
+
54
+ // Re-export types
55
+ export type {
56
+ IColor,
57
+ IRGB,
58
+ IHSV,
59
+ ColorModel,
60
+ IColorPickerProps,
61
+ ISaturationProps,
62
+ IRectangleSaturationProps,
63
+ IHueProps,
64
+ IAlphaProps,
65
+ IValueProps,
66
+ IThumbProps,
67
+ IFieldsProps,
68
+ ILayout,
69
+ } from '../core';
package/src/index.ts ADDED
@@ -0,0 +1,19 @@
1
+ /**
2
+ * React Native Color Palette
3
+ *
4
+ * A versatile color picker for React Native with two flavors:
5
+ * - Full version (default): Uses react-native-svg for smooth gradients
6
+ * - Lite version: Zero external dependencies, uses pure React Native Views
7
+ *
8
+ * @example
9
+ * ```tsx
10
+ * // Full version (requires react-native-svg)
11
+ * import { ColorPicker, useColor } from 'react-native-color-palette';
12
+ *
13
+ * // Lite version (no external dependencies)
14
+ * import { ColorPicker, useColor } from 'react-native-color-palette/lite';
15
+ * ```
16
+ */
17
+
18
+ // Export everything from the full version as the default
19
+ export * from './full';