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,289 @@
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 type { ISaturationProps } from '../../core/types';
10
+ import { ColorService } from '../../core/services';
11
+ import { clamp } from '../../core/utils';
12
+ import { Thumb } from './Thumb';
13
+
14
+ /**
15
+ * Circular Color Wheel - Zero Dependencies Version
16
+ *
17
+ * Uses pure React Native Views to create the color wheel.
18
+ * No react-native-svg required.
19
+ *
20
+ * @example
21
+ * ```tsx
22
+ * <Saturation
23
+ * color={color}
24
+ * onChange={handleChange}
25
+ * size={250}
26
+ * thumbSize={24}
27
+ * />
28
+ * ```
29
+ */
30
+ export const Saturation = memo(
31
+ ({
32
+ color,
33
+ onChange,
34
+ onChangeComplete,
35
+ size,
36
+ thumbSize,
37
+ disabled = false,
38
+ }: ISaturationProps) => {
39
+ const containerRef = useRef<View>(null);
40
+ const measurementCache = useRef<{ x: number; y: number } | null>(null);
41
+ const [isLayoutReady, setIsLayoutReady] = useState(false);
42
+
43
+ const radius = size / 2;
44
+ const effectiveRadius = radius - thumbSize / 2;
45
+
46
+ // Calculate thumb position from HSV values
47
+ const thumbPosition = useMemo(() => {
48
+ const angleRad = ((color.hsv.h - 90) * Math.PI) / 180;
49
+ const distance = (color.hsv.s / 100) * effectiveRadius;
50
+
51
+ const x = radius + Math.cos(angleRad) * distance - thumbSize / 2;
52
+ const y = radius + Math.sin(angleRad) * distance - thumbSize / 2;
53
+
54
+ return { x, y };
55
+ }, [color.hsv.h, color.hsv.s, radius, effectiveRadius, thumbSize]);
56
+
57
+ // Update measurement cache
58
+ const updateMeasurement = useCallback(() => {
59
+ return new Promise<{ x: number; y: number }>((resolve) => {
60
+ if (measurementCache.current) {
61
+ resolve(measurementCache.current);
62
+ return;
63
+ }
64
+ containerRef.current?.measureInWindow((x, y) => {
65
+ measurementCache.current = { x, y };
66
+ resolve({ x, y });
67
+ });
68
+ });
69
+ }, []);
70
+
71
+ // Convert touch coordinates to HSV
72
+ const updateColorFromPosition = useCallback(
73
+ async (pageX: number, pageY: number, isFinal: boolean) => {
74
+ const measurement = await updateMeasurement();
75
+
76
+ const x = pageX - measurement.x - radius;
77
+ const y = pageY - measurement.y - radius;
78
+
79
+ let angleDeg = (Math.atan2(y, x) * 180) / Math.PI + 90;
80
+ if (angleDeg < 0) angleDeg += 360;
81
+
82
+ const distance = Math.min(Math.sqrt(x * x + y * y), effectiveRadius);
83
+ const saturation = (distance / effectiveRadius) * 100;
84
+
85
+ const nextColor = ColorService.convert('hsv', {
86
+ h: clamp(angleDeg, 0, 360),
87
+ s: clamp(saturation, 0, 100),
88
+ v: color.hsv.v,
89
+ a: color.hsv.a,
90
+ });
91
+
92
+ onChange(nextColor);
93
+
94
+ if (isFinal) {
95
+ onChangeComplete?.(nextColor);
96
+ }
97
+ },
98
+ [color.hsv.v, color.hsv.a, radius, effectiveRadius, onChange, onChangeComplete, updateMeasurement]
99
+ );
100
+
101
+ // Handle layout
102
+ const handleLayout = useCallback((event: LayoutChangeEvent) => {
103
+ measurementCache.current = null;
104
+ setIsLayoutReady(true);
105
+ }, []);
106
+
107
+ // PanResponder
108
+ const panResponder = useMemo(
109
+ () =>
110
+ PanResponder.create({
111
+ onStartShouldSetPanResponder: () => !disabled,
112
+ onMoveShouldSetPanResponder: () => !disabled,
113
+ onPanResponderGrant: (evt: GestureResponderEvent) => {
114
+ measurementCache.current = null;
115
+ updateColorFromPosition(evt.nativeEvent.pageX, evt.nativeEvent.pageY, false);
116
+ },
117
+ onPanResponderMove: (evt: GestureResponderEvent) => {
118
+ updateColorFromPosition(evt.nativeEvent.pageX, evt.nativeEvent.pageY, false);
119
+ },
120
+ onPanResponderRelease: (evt: GestureResponderEvent) => {
121
+ updateColorFromPosition(evt.nativeEvent.pageX, evt.nativeEvent.pageY, true);
122
+ },
123
+ }),
124
+ [disabled, updateColorFromPosition]
125
+ );
126
+
127
+ // Generate hue wheel segments (360 Views for smooth gradient)
128
+ const colorWheelSegments = useMemo(() => {
129
+ const segments: JSX.Element[] = [];
130
+ const segmentCount = 360;
131
+ const angleStep = 360 / segmentCount;
132
+
133
+ for (let i = 0; i < segmentCount; i++) {
134
+ const hue = i * angleStep;
135
+ const rotation = hue - 90;
136
+
137
+ segments.push(
138
+ <View
139
+ key={i}
140
+ style={[
141
+ styles.segment,
142
+ {
143
+ width: size,
144
+ height: size,
145
+ transform: [{ rotate: `${rotation}deg` }],
146
+ },
147
+ ]}
148
+ >
149
+ <View
150
+ style={[
151
+ styles.segmentInner,
152
+ {
153
+ width: radius + 2,
154
+ height: radius * Math.tan((angleStep * Math.PI) / 360) * 2 + 4,
155
+ backgroundColor: `hsl(${hue}, 100%, 50%)`,
156
+ },
157
+ ]}
158
+ />
159
+ </View>
160
+ );
161
+ }
162
+
163
+ return segments;
164
+ }, [size, radius]);
165
+
166
+ // Saturation overlay - small constant opacity per circle, accumulates linearly
167
+ const saturationOverlay = useMemo(() => {
168
+ const circles: JSX.Element[] = [];
169
+ const steps = 50;
170
+ const opacityPerCircle = 2.5 / steps;
171
+
172
+ for (let i = 1; i <= steps; i++) {
173
+ const scale = 1 - i / steps;
174
+
175
+ if (scale > 0.01) {
176
+ circles.push(
177
+ <View
178
+ key={i}
179
+ style={[
180
+ styles.saturationCircle,
181
+ {
182
+ width: size * scale,
183
+ height: size * scale,
184
+ borderRadius: (size * scale) / 2,
185
+ backgroundColor: `rgba(255, 255, 255, ${opacityPerCircle})`,
186
+ },
187
+ ]}
188
+ />
189
+ );
190
+ }
191
+ }
192
+
193
+ return circles;
194
+ }, [size]);
195
+
196
+ return (
197
+ <View
198
+ ref={containerRef}
199
+ onLayout={handleLayout}
200
+ style={[
201
+ styles.container,
202
+ {
203
+ width: size,
204
+ height: size,
205
+ borderRadius: radius,
206
+ opacity: disabled ? 0.5 : 1,
207
+ },
208
+ ]}
209
+ {...panResponder.panHandlers}
210
+ >
211
+ {/* Color wheel segments */}
212
+ <View style={[styles.wheelContainer, { borderRadius: radius }]}>
213
+ {colorWheelSegments}
214
+ </View>
215
+
216
+ {/* Saturation overlay (white gradient to center) */}
217
+ <View style={[styles.saturationContainer, { borderRadius: radius }]}>
218
+ {saturationOverlay}
219
+ </View>
220
+
221
+ {/* Brightness overlay */}
222
+ <View
223
+ style={[
224
+ styles.brightnessOverlay,
225
+ {
226
+ borderRadius: radius,
227
+ backgroundColor: `rgba(0, 0, 0, ${1 - color.hsv.v / 100})`,
228
+ },
229
+ ]}
230
+ />
231
+
232
+ {/* Thumb */}
233
+ <View
234
+ style={[
235
+ styles.thumbContainer,
236
+ {
237
+ left: thumbPosition.x,
238
+ top: thumbPosition.y,
239
+ },
240
+ ]}
241
+ pointerEvents="none"
242
+ >
243
+ <Thumb size={thumbSize} />
244
+ </View>
245
+ </View>
246
+ );
247
+ }
248
+ );
249
+
250
+ const styles = StyleSheet.create({
251
+ container: {
252
+ position: 'relative',
253
+ overflow: 'hidden',
254
+ backgroundColor: '#808080',
255
+ },
256
+ wheelContainer: {
257
+ ...StyleSheet.absoluteFillObject,
258
+ overflow: 'hidden',
259
+ },
260
+ segment: {
261
+ position: 'absolute',
262
+ top: 0,
263
+ left: 0,
264
+ justifyContent: 'center',
265
+ alignItems: 'flex-end',
266
+ },
267
+ segmentInner: {
268
+ position: 'absolute',
269
+ right: 0,
270
+ },
271
+ saturationContainer: {
272
+ ...StyleSheet.absoluteFillObject,
273
+ justifyContent: 'center',
274
+ alignItems: 'center',
275
+ overflow: 'hidden',
276
+ },
277
+ saturationCircle: {
278
+ position: 'absolute',
279
+ },
280
+ brightnessOverlay: {
281
+ ...StyleSheet.absoluteFillObject,
282
+ },
283
+ thumbContainer: {
284
+ position: 'absolute',
285
+ zIndex: 10,
286
+ },
287
+ });
288
+
289
+ 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,201 @@
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 type { IValueProps } from '../../core/types';
10
+ import { ColorService } from '../../core/services';
11
+ import { clamp } from '../../core/utils';
12
+ import { Thumb } from './Thumb';
13
+
14
+ /**
15
+ * Value/Brightness slider bar component - Zero Dependencies Version
16
+ *
17
+ * Uses pure React Native Views to create the brightness gradient.
18
+ * No react-native-svg required.
19
+ *
20
+ * @example
21
+ * ```tsx
22
+ * <Value
23
+ * color={color}
24
+ * onChange={handleChange}
25
+ * barHeight={10}
26
+ * thumbSize={24}
27
+ * />
28
+ * ```
29
+ */
30
+ export const Value = memo(
31
+ ({
32
+ color,
33
+ onChange,
34
+ onChangeComplete,
35
+ barHeight,
36
+ thumbSize,
37
+ disabled = false,
38
+ }: IValueProps) => {
39
+ const containerRef = useRef<View>(null);
40
+ const [width, setWidth] = useState(1);
41
+ const measurementCache = useRef<number | null>(null);
42
+
43
+ // Calculate thumb position from value (v=100 should be at right)
44
+ const thumbPosition = useMemo(() => {
45
+ return (color.hsv.v / 100) * width;
46
+ }, [color.hsv.v, width]);
47
+
48
+ // Update measurement cache
49
+ const updateMeasurement = useCallback(() => {
50
+ return new Promise<number>((resolve) => {
51
+ if (measurementCache.current !== null) {
52
+ resolve(measurementCache.current);
53
+ return;
54
+ }
55
+ containerRef.current?.measureInWindow((x) => {
56
+ measurementCache.current = x;
57
+ resolve(x);
58
+ });
59
+ });
60
+ }, []);
61
+
62
+ // Convert touch position to value
63
+ const updateColorFromPosition = useCallback(
64
+ async (pageX: number, isFinal: boolean) => {
65
+ const wx = await updateMeasurement();
66
+ const x = clamp(pageX - wx, 0, width);
67
+ const value = (x / width) * 100;
68
+
69
+ const nextColor = ColorService.convert('hsv', {
70
+ ...color.hsv,
71
+ v: clamp(value, 0, 100),
72
+ });
73
+
74
+ onChange(nextColor);
75
+
76
+ if (isFinal) {
77
+ onChangeComplete?.(nextColor);
78
+ }
79
+ },
80
+ [color.hsv, width, onChange, onChangeComplete, updateMeasurement]
81
+ );
82
+
83
+ // PanResponder for touch handling
84
+ const panResponder = useMemo(
85
+ () =>
86
+ PanResponder.create({
87
+ onStartShouldSetPanResponder: () => !disabled,
88
+ onMoveShouldSetPanResponder: () => !disabled,
89
+ onPanResponderGrant: (evt: GestureResponderEvent) => {
90
+ measurementCache.current = null;
91
+ updateColorFromPosition(evt.nativeEvent.pageX, false);
92
+ },
93
+ onPanResponderMove: (evt: GestureResponderEvent) => {
94
+ updateColorFromPosition(evt.nativeEvent.pageX, false);
95
+ },
96
+ onPanResponderRelease: (evt: GestureResponderEvent) => {
97
+ updateColorFromPosition(evt.nativeEvent.pageX, true);
98
+ },
99
+ }),
100
+ [disabled, updateColorFromPosition]
101
+ );
102
+
103
+ // Handle layout to get width and clear cache
104
+ const handleLayout = useCallback((event: LayoutChangeEvent) => {
105
+ setWidth(event.nativeEvent.layout.width);
106
+ measurementCache.current = null;
107
+ }, []);
108
+
109
+ // Generate brightness gradient using Views (black to full color)
110
+ const valueSegments = useMemo(() => {
111
+ const segments: JSX.Element[] = [];
112
+ const steps = Math.max(1, Math.round(width));
113
+ const { h, s } = color.hsv;
114
+
115
+ for (let i = 0; i < steps; i++) {
116
+ const value = (i / steps) * 100;
117
+ // HSV to HSL conversion for display
118
+ const l = (value / 200) * (200 - s);
119
+
120
+ segments.push(
121
+ <View
122
+ key={i}
123
+ style={[
124
+ styles.segment,
125
+ {
126
+ flex: 1,
127
+ backgroundColor: `hsl(${h}, ${s}%, ${l}%)`,
128
+ },
129
+ ]}
130
+ />
131
+ );
132
+ }
133
+
134
+ return segments;
135
+ }, [width, color.hsv.h, color.hsv.s]);
136
+
137
+ // Calculate thumb color
138
+ const thumbColor = useMemo(() => {
139
+ const { h, s, v } = color.hsv;
140
+ const l = (v / 200) * (200 - s);
141
+ return `hsl(${h}, ${s}%, ${l}%)`;
142
+ }, [color.hsv.h, color.hsv.s, color.hsv.v]);
143
+
144
+ return (
145
+ <View
146
+ ref={containerRef}
147
+ style={[
148
+ styles.container,
149
+ {
150
+ height: barHeight + thumbSize,
151
+ opacity: disabled ? 0.5 : 1,
152
+ },
153
+ ]}
154
+ onLayout={handleLayout}
155
+ {...panResponder.panHandlers}
156
+ >
157
+ {/* Gradient bar using colored Views */}
158
+ <View style={[styles.barContainer, { height: barHeight, top: thumbSize / 2, borderRadius: barHeight / 2 }]}>
159
+ {valueSegments}
160
+ </View>
161
+
162
+ {/* Thumb indicator */}
163
+ <View
164
+ style={[
165
+ styles.thumbContainer,
166
+ {
167
+ left: thumbPosition - thumbSize / 2,
168
+ top: barHeight / 2,
169
+ },
170
+ ]}
171
+ pointerEvents="none"
172
+ >
173
+ <Thumb size={thumbSize} color={thumbColor} />
174
+ </View>
175
+ </View>
176
+ );
177
+ }
178
+ );
179
+
180
+ const styles = StyleSheet.create({
181
+ container: {
182
+ position: 'relative',
183
+ width: '100%',
184
+ },
185
+ barContainer: {
186
+ position: 'absolute',
187
+ left: 0,
188
+ right: 0,
189
+ flexDirection: 'row',
190
+ overflow: 'hidden',
191
+ },
192
+ segment: {
193
+ height: '100%',
194
+ },
195
+ thumbContainer: {
196
+ position: 'absolute',
197
+ zIndex: 10,
198
+ },
199
+ });
200
+
201
+ 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,75 @@
1
+ /**
2
+ * React Native Color Palette - Lite Version (Zero Dependencies)
3
+ *
4
+ * This version uses pure React Native Views instead of react-native-svg.
5
+ * No additional native dependencies required!
6
+ *
7
+ * Benefits:
8
+ * - No native linking required
9
+ * - Smaller bundle size
10
+ * - Works with Expo out of the box
11
+ * - No pod install needed
12
+ *
13
+ * @example
14
+ * ```tsx
15
+ * import { ColorPicker, useColor } from 'react-native-color-palette/lite';
16
+ *
17
+ * function MyComponent() {
18
+ * const [color, setColor] = useColor('#FF0000');
19
+ *
20
+ * return (
21
+ * <ColorPicker
22
+ * color={color}
23
+ * onChange={setColor}
24
+ * onChangeComplete={(color) => console.log('Selected:', color.hex)}
25
+ * width={250}
26
+ * barHeight={10}
27
+ * thumbSize={24}
28
+ * hideAlpha={false}
29
+ * hideInput={false}
30
+ * />
31
+ * );
32
+ * }
33
+ * ```
34
+ */
35
+
36
+ // Main component
37
+ export { ColorPicker } from './components';
38
+
39
+ // Individual components for custom layouts
40
+ export {
41
+ Saturation,
42
+ RectangleSaturation,
43
+ Hue,
44
+ Alpha,
45
+ Value,
46
+ Thumb,
47
+ Fields,
48
+ HexField,
49
+ RgbFields,
50
+ } from './components';
51
+
52
+ // Re-export core utilities
53
+ export {
54
+ ColorService,
55
+ useColor,
56
+ useColorWithCallback,
57
+ useComponentLayout,
58
+ } from '../core';
59
+
60
+ // Re-export types
61
+ export type {
62
+ IColor,
63
+ IRGB,
64
+ IHSV,
65
+ ColorModel,
66
+ IColorPickerProps,
67
+ ISaturationProps,
68
+ IRectangleSaturationProps,
69
+ IHueProps,
70
+ IAlphaProps,
71
+ IValueProps,
72
+ IThumbProps,
73
+ IFieldsProps,
74
+ ILayout,
75
+ } from '../core';