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,53 @@
1
+ import React, { memo } from 'react';
2
+ import { View, StyleSheet } from 'react-native';
3
+ import type { IFieldsProps } from '../../../core/types';
4
+ import { HexField } from './HexField';
5
+ import { RgbFields } from './RgbFields';
6
+
7
+ /**
8
+ * Color input fields container
9
+ */
10
+ export const Fields = memo(
11
+ ({
12
+ color,
13
+ onChange,
14
+ onChangeComplete,
15
+ disabled = false,
16
+ }: IFieldsProps) => {
17
+ return (
18
+ <View style={styles.container}>
19
+ <View style={styles.hexContainer}>
20
+ <HexField
21
+ color={color}
22
+ onChange={onChange}
23
+ onChangeComplete={onChangeComplete}
24
+ disabled={disabled}
25
+ />
26
+ </View>
27
+ <View style={styles.rgbContainer}>
28
+ <RgbFields
29
+ color={color}
30
+ onChange={onChange}
31
+ onChangeComplete={onChangeComplete}
32
+ disabled={disabled}
33
+ />
34
+ </View>
35
+ </View>
36
+ );
37
+ }
38
+ );
39
+
40
+ const styles = StyleSheet.create({
41
+ container: {
42
+ flexDirection: 'row',
43
+ gap: 12,
44
+ alignItems: 'flex-end',
45
+ },
46
+ hexContainer: { flex: 1 },
47
+ rgbContainer: { flex: 2 },
48
+ });
49
+
50
+ Fields.displayName = 'Fields';
51
+
52
+ export { HexField } from './HexField';
53
+ export { RgbFields } from './RgbFields';
@@ -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 type { IHueProps } from '../../core/types';
10
+ import { ColorService } from '../../core/services';
11
+ import { clamp } from '../../core/utils';
12
+ import { Thumb } from './Thumb';
13
+
14
+ /**
15
+ * Hue slider bar component - Zero Dependencies Version
16
+ *
17
+ * Uses pure React Native Views to create the rainbow gradient.
18
+ * No react-native-svg required.
19
+ *
20
+ * @example
21
+ * ```tsx
22
+ * <Hue
23
+ * color={color}
24
+ * onChange={handleChange}
25
+ * barHeight={10}
26
+ * thumbSize={24}
27
+ * />
28
+ * ```
29
+ */
30
+ export const Hue = memo(
31
+ ({
32
+ color,
33
+ onChange,
34
+ onChangeComplete,
35
+ barHeight,
36
+ thumbSize,
37
+ disabled = false,
38
+ }: IHueProps) => {
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 hue value
44
+ const thumbPosition = useMemo(() => {
45
+ return (color.hsv.h / 360) * width;
46
+ }, [color.hsv.h, 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 hue
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 hue = (x / width) * 360;
68
+
69
+ const nextColor = ColorService.convert('hsv', {
70
+ ...color.hsv,
71
+ h: clamp(hue, 0, 360),
72
+ s: 100,
73
+ });
74
+
75
+ onChange(nextColor);
76
+
77
+ if (isFinal) {
78
+ onChangeComplete?.(nextColor);
79
+ }
80
+ },
81
+ [color.hsv, width, onChange, onChangeComplete, updateMeasurement]
82
+ );
83
+
84
+ // PanResponder for touch handling
85
+ const panResponder = useMemo(
86
+ () =>
87
+ PanResponder.create({
88
+ onStartShouldSetPanResponder: () => !disabled,
89
+ onMoveShouldSetPanResponder: () => !disabled,
90
+ onPanResponderGrant: (evt: GestureResponderEvent) => {
91
+ measurementCache.current = null;
92
+ updateColorFromPosition(evt.nativeEvent.pageX, false);
93
+ },
94
+ onPanResponderMove: (evt: GestureResponderEvent) => {
95
+ updateColorFromPosition(evt.nativeEvent.pageX, false);
96
+ },
97
+ onPanResponderRelease: (evt: GestureResponderEvent) => {
98
+ updateColorFromPosition(evt.nativeEvent.pageX, true);
99
+ },
100
+ }),
101
+ [disabled, updateColorFromPosition]
102
+ );
103
+
104
+ // Handle layout to get width and clear cache
105
+ const handleLayout = useCallback((event: LayoutChangeEvent) => {
106
+ setWidth(event.nativeEvent.layout.width);
107
+ measurementCache.current = null;
108
+ }, []);
109
+
110
+ // Generate rainbow gradient using colored Views (1 pixel per segment)
111
+ const hueSegments = useMemo(() => {
112
+ const segments: JSX.Element[] = [];
113
+ const steps = Math.max(1, Math.round(width));
114
+
115
+ for (let i = 0; i < steps; i++) {
116
+ const hue = (i / steps) * 360;
117
+
118
+ segments.push(
119
+ <View
120
+ key={i}
121
+ style={[
122
+ styles.segment,
123
+ {
124
+ flex: 1,
125
+ backgroundColor: `hsl(${hue}, 100%, 50%)`,
126
+ },
127
+ ]}
128
+ />
129
+ );
130
+ }
131
+
132
+ return segments;
133
+ }, [width]);
134
+
135
+ return (
136
+ <View
137
+ ref={containerRef}
138
+ style={[
139
+ styles.container,
140
+ {
141
+ height: barHeight + thumbSize,
142
+ opacity: disabled ? 0.5 : 1,
143
+ },
144
+ ]}
145
+ onLayout={handleLayout}
146
+ {...panResponder.panHandlers}
147
+ >
148
+ {/* Gradient bar using colored Views */}
149
+ <View style={[styles.barContainer, { height: barHeight, top: thumbSize / 2, borderRadius: barHeight / 2 }]}>
150
+ {hueSegments}
151
+ </View>
152
+
153
+ {/* Thumb indicator - centered vertically with bar */}
154
+ <View
155
+ style={[
156
+ styles.thumbContainer,
157
+ {
158
+ left: thumbPosition - thumbSize / 2,
159
+ top: barHeight / 2,
160
+ },
161
+ ]}
162
+ pointerEvents="none"
163
+ >
164
+ <Thumb size={thumbSize} color={`hsl(${color.hsv.h}, 100%, 50%)`} />
165
+ </View>
166
+ </View>
167
+ );
168
+ }
169
+ );
170
+
171
+ const styles = StyleSheet.create({
172
+ container: {
173
+ position: 'relative',
174
+ width: '100%',
175
+ },
176
+ barContainer: {
177
+ position: 'absolute',
178
+ left: 0,
179
+ right: 0,
180
+ flexDirection: 'row',
181
+ overflow: 'hidden',
182
+ },
183
+ segment: {
184
+ height: '100%',
185
+ },
186
+ thumbContainer: {
187
+ position: 'absolute',
188
+ zIndex: 10,
189
+ },
190
+ });
191
+
192
+ Hue.displayName = 'Hue';
@@ -0,0 +1,238 @@
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 { IRectangleSaturationProps } from '../../core/types';
10
+ import { ColorService } from '../../core/services';
11
+ import { clamp } from '../../core/utils';
12
+ import { Thumb } from './Thumb';
13
+
14
+ /**
15
+ * Rectangle Saturation/Value Picker - Zero Dependencies Version
16
+ *
17
+ * Uses pure React Native Views for gradients.
18
+ * No react-native-svg required.
19
+ *
20
+ * Layout:
21
+ * - Top-right: Current hue at full saturation (100% S, 100% V)
22
+ * - Top-left: White (0% S, 100% V)
23
+ * - Bottom-left: Black (0% S, 0% V)
24
+ * - Bottom-right: Black (100% S, 0% V)
25
+ *
26
+ * X-axis: Saturation (0% to 100%)
27
+ * Y-axis: Value/Brightness (100% to 0%)
28
+ *
29
+ * @example
30
+ * ```tsx
31
+ * <RectangleSaturation
32
+ * color={color}
33
+ * onChange={handleChange}
34
+ * width={250}
35
+ * height={150}
36
+ * thumbSize={24}
37
+ * />
38
+ * ```
39
+ */
40
+ export const RectangleSaturation = memo(
41
+ ({
42
+ color,
43
+ onChange,
44
+ onChangeComplete,
45
+ width,
46
+ height,
47
+ thumbSize,
48
+ disabled = false,
49
+ }: IRectangleSaturationProps) => {
50
+ // Key: steps = dimension (1 pixel per segment) avoids all sub-pixel gaps
51
+ const horizontalSteps = width;
52
+ const verticalSteps = height;
53
+ const containerRef = useRef<View>(null);
54
+ const measurementCache = useRef<{ x: number; y: number } | null>(null);
55
+ const [isLayoutReady, setIsLayoutReady] = useState(false);
56
+
57
+ // Calculate thumb position from HSV values
58
+ const thumbPosition = useMemo(() => {
59
+ const x = (color.hsv.s / 100) * width - thumbSize / 2;
60
+ const y = ((100 - color.hsv.v) / 100) * height - thumbSize / 2;
61
+ return { x: clamp(x, -thumbSize / 2, width - thumbSize / 2), y: clamp(y, -thumbSize / 2, height - thumbSize / 2) };
62
+ }, [color.hsv.s, color.hsv.v, width, height, thumbSize]);
63
+
64
+ // Update measurement cache
65
+ const updateMeasurement = useCallback(() => {
66
+ return new Promise<{ x: number; y: number }>((resolve) => {
67
+ if (measurementCache.current) {
68
+ resolve(measurementCache.current);
69
+ return;
70
+ }
71
+ containerRef.current?.measureInWindow((x, y) => {
72
+ measurementCache.current = { x, y };
73
+ resolve({ x, y });
74
+ });
75
+ });
76
+ }, []);
77
+
78
+ // Convert touch coordinates to HSV
79
+ const updateColorFromPosition = useCallback(
80
+ async (pageX: number, pageY: number, isFinal: boolean) => {
81
+ const measurement = await updateMeasurement();
82
+
83
+ const x = clamp(pageX - measurement.x, 0, width);
84
+ const y = clamp(pageY - measurement.y, 0, height);
85
+
86
+ const saturation = (x / width) * 100;
87
+ const value = 100 - (y / height) * 100;
88
+
89
+ const nextColor = ColorService.convert('hsv', {
90
+ h: color.hsv.h,
91
+ s: clamp(saturation, 0, 100),
92
+ v: clamp(value, 0, 100),
93
+ a: color.hsv.a,
94
+ });
95
+
96
+ onChange(nextColor);
97
+
98
+ if (isFinal) {
99
+ onChangeComplete?.(nextColor);
100
+ }
101
+ },
102
+ [color.hsv.h, color.hsv.a, width, height, onChange, onChangeComplete, updateMeasurement]
103
+ );
104
+
105
+ // Handle layout
106
+ const handleLayout = useCallback((event: LayoutChangeEvent) => {
107
+ measurementCache.current = null;
108
+ setIsLayoutReady(true);
109
+ }, []);
110
+
111
+ // PanResponder
112
+ const panResponder = useMemo(
113
+ () =>
114
+ PanResponder.create({
115
+ onStartShouldSetPanResponder: () => !disabled,
116
+ onMoveShouldSetPanResponder: () => !disabled,
117
+ onPanResponderGrant: (evt: GestureResponderEvent) => {
118
+ measurementCache.current = null;
119
+ updateColorFromPosition(evt.nativeEvent.pageX, evt.nativeEvent.pageY, false);
120
+ },
121
+ onPanResponderMove: (evt: GestureResponderEvent) => {
122
+ updateColorFromPosition(evt.nativeEvent.pageX, evt.nativeEvent.pageY, false);
123
+ },
124
+ onPanResponderRelease: (evt: GestureResponderEvent) => {
125
+ updateColorFromPosition(evt.nativeEvent.pageX, evt.nativeEvent.pageY, true);
126
+ },
127
+ }),
128
+ [disabled, updateColorFromPosition]
129
+ );
130
+
131
+ // Generate white gradient overlay (left to right: white to transparent)
132
+ const whiteGradient = useMemo(() => {
133
+ const segments: JSX.Element[] = [];
134
+ for (let i = 0; i < horizontalSteps; i++) {
135
+ const opacity = 1 - i / horizontalSteps;
136
+ segments.push(
137
+ <View
138
+ key={`white-${i}`}
139
+ style={{
140
+ flex: 1,
141
+ backgroundColor: `rgba(255, 255, 255, ${opacity})`,
142
+ }}
143
+ />
144
+ );
145
+ }
146
+ return segments;
147
+ }, [horizontalSteps]);
148
+
149
+ // Generate black gradient overlay (top to bottom: transparent to black)
150
+ const blackGradient = useMemo(() => {
151
+ const segments: JSX.Element[] = [];
152
+ for (let i = 0; i < verticalSteps; i++) {
153
+ const opacity = i / verticalSteps;
154
+ segments.push(
155
+ <View
156
+ key={`black-${i}`}
157
+ style={{
158
+ flex: 1,
159
+ backgroundColor: `rgba(0, 0, 0, ${opacity})`,
160
+ }}
161
+ />
162
+ );
163
+ }
164
+ return segments;
165
+ }, [verticalSteps]);
166
+
167
+ // Current hue color at full saturation
168
+ const hueColor = `hsl(${color.hsv.h}, 100%, 50%)`;
169
+
170
+ return (
171
+ <View
172
+ ref={containerRef}
173
+ onLayout={handleLayout}
174
+ style={[
175
+ styles.container,
176
+ {
177
+ width,
178
+ height,
179
+ opacity: disabled ? 0.5 : 1,
180
+ },
181
+ ]}
182
+ {...panResponder.panHandlers}
183
+ >
184
+ {/* Base hue color */}
185
+ <View style={[styles.hueLayer, { backgroundColor: hueColor }]} />
186
+
187
+ {/* White gradient (horizontal: left white to right transparent) */}
188
+ <View style={styles.whiteGradientContainer}>
189
+ {whiteGradient}
190
+ </View>
191
+
192
+ {/* Black gradient (vertical: top transparent to bottom black) */}
193
+ <View style={styles.blackGradientContainer}>
194
+ {blackGradient}
195
+ </View>
196
+
197
+ {/* Thumb */}
198
+ <View
199
+ style={[
200
+ styles.thumbContainer,
201
+ {
202
+ left: thumbPosition.x,
203
+ top: thumbPosition.y,
204
+ },
205
+ ]}
206
+ pointerEvents="none"
207
+ >
208
+ <Thumb size={thumbSize} color={color.hex} />
209
+ </View>
210
+ </View>
211
+ );
212
+ }
213
+ );
214
+
215
+ const styles = StyleSheet.create({
216
+ container: {
217
+ position: 'relative',
218
+ borderRadius: 8,
219
+ overflow: 'hidden',
220
+ },
221
+ hueLayer: {
222
+ ...StyleSheet.absoluteFillObject,
223
+ },
224
+ whiteGradientContainer: {
225
+ ...StyleSheet.absoluteFillObject,
226
+ flexDirection: 'row',
227
+ },
228
+ blackGradientContainer: {
229
+ ...StyleSheet.absoluteFillObject,
230
+ flexDirection: 'column',
231
+ },
232
+ thumbContainer: {
233
+ position: 'absolute',
234
+ zIndex: 10,
235
+ },
236
+ });
237
+
238
+ RectangleSaturation.displayName = 'RectangleSaturation';