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.
- package/LICENSE +21 -0
- package/README.md +340 -0
- package/package.json +70 -0
- package/src/core/hooks/index.ts +2 -0
- package/src/core/hooks/useColor.ts +93 -0
- package/src/core/hooks/useComponentLayout.ts +38 -0
- package/src/core/index.ts +19 -0
- package/src/core/services/ColorService.ts +338 -0
- package/src/core/services/index.ts +1 -0
- package/src/core/types/index.ts +211 -0
- package/src/core/utils/clamp.ts +16 -0
- package/src/core/utils/format.ts +59 -0
- package/src/core/utils/index.ts +8 -0
- package/src/full/components/Alpha.tsx +221 -0
- package/src/full/components/ColorPicker.tsx +206 -0
- package/src/full/components/Fields/HexField.tsx +125 -0
- package/src/full/components/Fields/RgbFields.tsx +192 -0
- package/src/full/components/Fields/index.tsx +70 -0
- package/src/full/components/Hue.tsx +188 -0
- package/src/full/components/RectangleSaturation.tsx +203 -0
- package/src/full/components/Saturation.tsx +258 -0
- package/src/full/components/Thumb.tsx +47 -0
- package/src/full/components/Value.tsx +192 -0
- package/src/full/components/index.ts +8 -0
- package/src/full/index.ts +69 -0
- package/src/index.ts +19 -0
- package/src/lite/components/Alpha.tsx +228 -0
- package/src/lite/components/ColorPicker.tsx +209 -0
- package/src/lite/components/Fields/HexField.tsx +103 -0
- package/src/lite/components/Fields/RgbFields.tsx +138 -0
- package/src/lite/components/Fields/index.tsx +53 -0
- package/src/lite/components/Hue.tsx +192 -0
- package/src/lite/components/RectangleSaturation.tsx +238 -0
- package/src/lite/components/Saturation.tsx +289 -0
- package/src/lite/components/Thumb.tsx +47 -0
- package/src/lite/components/Value.tsx +201 -0
- package/src/lite/components/index.ts +8 -0
- 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';
|