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,228 @@
|
|
|
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 { IAlphaProps } from '../../core/types';
|
|
10
|
+
import { ColorService } from '../../core/services';
|
|
11
|
+
import { clamp } from '../../core/utils';
|
|
12
|
+
import { Thumb } from './Thumb';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Alpha (transparency) slider bar component - Zero Dependencies Version
|
|
16
|
+
*
|
|
17
|
+
* Uses pure React Native Views for checkerboard and gradient.
|
|
18
|
+
* No react-native-svg required.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```tsx
|
|
22
|
+
* <Alpha
|
|
23
|
+
* color={color}
|
|
24
|
+
* onChange={handleChange}
|
|
25
|
+
* barHeight={10}
|
|
26
|
+
* thumbSize={24}
|
|
27
|
+
* />
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
export const Alpha = memo(
|
|
31
|
+
({
|
|
32
|
+
color,
|
|
33
|
+
onChange,
|
|
34
|
+
onChangeComplete,
|
|
35
|
+
barHeight,
|
|
36
|
+
thumbSize,
|
|
37
|
+
disabled = false,
|
|
38
|
+
}: IAlphaProps) => {
|
|
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 alpha value (a=1 should be at right)
|
|
44
|
+
const thumbPosition = useMemo(() => {
|
|
45
|
+
return color.hsv.a * width;
|
|
46
|
+
}, [color.hsv.a, 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 alpha
|
|
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 alpha = x / width;
|
|
68
|
+
|
|
69
|
+
const nextColor = ColorService.convert('hsv', {
|
|
70
|
+
...color.hsv,
|
|
71
|
+
a: clamp(alpha, 0, 1),
|
|
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 alpha gradient using colored Views with varying opacity
|
|
110
|
+
const alphaGradientSegments = useMemo(() => {
|
|
111
|
+
const segments: JSX.Element[] = [];
|
|
112
|
+
const steps = Math.max(1, Math.round(width));
|
|
113
|
+
const { r, g, b } = color.rgb;
|
|
114
|
+
|
|
115
|
+
for (let i = 0; i < steps; i++) {
|
|
116
|
+
const alpha = i / (steps - 1 || 1);
|
|
117
|
+
|
|
118
|
+
segments.push(
|
|
119
|
+
<View
|
|
120
|
+
key={i}
|
|
121
|
+
style={[
|
|
122
|
+
styles.gradientSegment,
|
|
123
|
+
{
|
|
124
|
+
flex: 1,
|
|
125
|
+
backgroundColor: `rgba(${r}, ${g}, ${b}, ${alpha})`,
|
|
126
|
+
},
|
|
127
|
+
]}
|
|
128
|
+
/>
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return segments;
|
|
133
|
+
}, [width, color.rgb.r, color.rgb.g, color.rgb.b]);
|
|
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
|
+
{/* Bar with checkerboard and gradient */}
|
|
149
|
+
<View style={[styles.barContainer, { height: barHeight, top: thumbSize / 2 }]}>
|
|
150
|
+
{/* Checkerboard background */}
|
|
151
|
+
<View style={[styles.checkerboard, { borderRadius: barHeight / 2 }]}>
|
|
152
|
+
{Array.from({ length: 2 }).map((_, rowIndex) => {
|
|
153
|
+
const checkerSize = barHeight / 2;
|
|
154
|
+
const cellCount = Math.ceil(width / checkerSize) + 1;
|
|
155
|
+
return (
|
|
156
|
+
<View key={rowIndex} style={[styles.checkerRow, { height: checkerSize }]}>
|
|
157
|
+
{Array.from({ length: cellCount }).map((_, i) => (
|
|
158
|
+
<View
|
|
159
|
+
key={i}
|
|
160
|
+
style={{
|
|
161
|
+
width: checkerSize,
|
|
162
|
+
height: checkerSize,
|
|
163
|
+
backgroundColor: (i + rowIndex) % 2 === 0 ? '#FFFFFF' : '#CCCCCC',
|
|
164
|
+
}}
|
|
165
|
+
/>
|
|
166
|
+
))}
|
|
167
|
+
</View>
|
|
168
|
+
);
|
|
169
|
+
})}
|
|
170
|
+
</View>
|
|
171
|
+
|
|
172
|
+
{/* Color gradient overlay using Views */}
|
|
173
|
+
<View style={[styles.gradientContainer, { borderRadius: barHeight / 2 }]}>
|
|
174
|
+
{alphaGradientSegments}
|
|
175
|
+
</View>
|
|
176
|
+
</View>
|
|
177
|
+
|
|
178
|
+
{/* Thumb indicator */}
|
|
179
|
+
<View
|
|
180
|
+
style={[
|
|
181
|
+
styles.thumbContainer,
|
|
182
|
+
{
|
|
183
|
+
left: thumbPosition - thumbSize / 2,
|
|
184
|
+
top: barHeight / 2,
|
|
185
|
+
},
|
|
186
|
+
]}
|
|
187
|
+
pointerEvents="none"
|
|
188
|
+
>
|
|
189
|
+
<Thumb size={thumbSize} color={`rgba(${color.rgb.r}, ${color.rgb.g}, ${color.rgb.b}, ${color.hsv.a})`} />
|
|
190
|
+
</View>
|
|
191
|
+
</View>
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
const styles = StyleSheet.create({
|
|
197
|
+
container: {
|
|
198
|
+
position: 'relative',
|
|
199
|
+
width: '100%',
|
|
200
|
+
},
|
|
201
|
+
barContainer: {
|
|
202
|
+
position: 'absolute',
|
|
203
|
+
left: 0,
|
|
204
|
+
right: 0,
|
|
205
|
+
overflow: 'hidden',
|
|
206
|
+
},
|
|
207
|
+
checkerboard: {
|
|
208
|
+
...StyleSheet.absoluteFillObject,
|
|
209
|
+
overflow: 'hidden',
|
|
210
|
+
},
|
|
211
|
+
checkerRow: {
|
|
212
|
+
flexDirection: 'row',
|
|
213
|
+
},
|
|
214
|
+
gradientContainer: {
|
|
215
|
+
...StyleSheet.absoluteFillObject,
|
|
216
|
+
flexDirection: 'row',
|
|
217
|
+
overflow: 'hidden',
|
|
218
|
+
},
|
|
219
|
+
gradientSegment: {
|
|
220
|
+
height: '100%',
|
|
221
|
+
},
|
|
222
|
+
thumbContainer: {
|
|
223
|
+
position: 'absolute',
|
|
224
|
+
zIndex: 10,
|
|
225
|
+
},
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
Alpha.displayName = 'Alpha';
|
|
@@ -0,0 +1,209 @@
|
|
|
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 (Lite version - Zero Dependencies)
|
|
13
|
+
*
|
|
14
|
+
* This version uses pure React Native Views instead of react-native-svg.
|
|
15
|
+
* No additional native dependencies required.
|
|
16
|
+
*
|
|
17
|
+
* Features:
|
|
18
|
+
* - Circular color wheel or rectangle for saturation/value selection
|
|
19
|
+
* - Horizontal hue bar
|
|
20
|
+
* - Horizontal alpha bar with checkerboard background
|
|
21
|
+
* - HEX and RGB input fields
|
|
22
|
+
* - Circle thumbs with white 2px border showing selected color
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```tsx
|
|
26
|
+
* import { ColorPicker, useColor } from 'react-native-color-palette/lite';
|
|
27
|
+
*
|
|
28
|
+
* function MyComponent() {
|
|
29
|
+
* const [color, setColor] = useColor('#FF0000');
|
|
30
|
+
*
|
|
31
|
+
* return (
|
|
32
|
+
* <ColorPicker
|
|
33
|
+
* color={color}
|
|
34
|
+
* onChange={setColor}
|
|
35
|
+
* variant="rectangle"
|
|
36
|
+
* onChangeComplete={(c) => console.log('Final color:', c.hex)}
|
|
37
|
+
* />
|
|
38
|
+
* );
|
|
39
|
+
* }
|
|
40
|
+
* ```
|
|
41
|
+
*/
|
|
42
|
+
export const ColorPicker = memo(
|
|
43
|
+
({
|
|
44
|
+
color,
|
|
45
|
+
onChange,
|
|
46
|
+
onChangeComplete,
|
|
47
|
+
variant = 'rectangle',
|
|
48
|
+
width = 250,
|
|
49
|
+
barHeight = 10,
|
|
50
|
+
thumbSize = 24,
|
|
51
|
+
hideHue = false,
|
|
52
|
+
hideAlpha = false,
|
|
53
|
+
hidePreview = false,
|
|
54
|
+
hideInput = false,
|
|
55
|
+
disabled = false,
|
|
56
|
+
}: IColorPickerProps) => {
|
|
57
|
+
// Rectangle height is 60% of width for better proportions
|
|
58
|
+
const rectangleHeight = width * 0.6;
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<View style={[styles.container, disabled && styles.disabled]}>
|
|
62
|
+
{/* Color picker (circle or rectangle) */}
|
|
63
|
+
<View style={styles.pickerContainer}>
|
|
64
|
+
{variant === 'circle' ? (
|
|
65
|
+
<Saturation
|
|
66
|
+
color={color}
|
|
67
|
+
onChange={onChange}
|
|
68
|
+
onChangeComplete={onChangeComplete}
|
|
69
|
+
size={width}
|
|
70
|
+
thumbSize={thumbSize}
|
|
71
|
+
disabled={disabled}
|
|
72
|
+
/>
|
|
73
|
+
) : (
|
|
74
|
+
<RectangleSaturation
|
|
75
|
+
color={color}
|
|
76
|
+
onChange={onChange}
|
|
77
|
+
onChangeComplete={onChangeComplete}
|
|
78
|
+
width={width}
|
|
79
|
+
height={rectangleHeight}
|
|
80
|
+
thumbSize={thumbSize}
|
|
81
|
+
disabled={disabled}
|
|
82
|
+
/>
|
|
83
|
+
)}
|
|
84
|
+
</View>
|
|
85
|
+
|
|
86
|
+
{/* Color preview */}
|
|
87
|
+
{!hidePreview && (
|
|
88
|
+
<View style={styles.previewContainer}>
|
|
89
|
+
<View
|
|
90
|
+
style={[
|
|
91
|
+
styles.preview,
|
|
92
|
+
{
|
|
93
|
+
backgroundColor: color.hex,
|
|
94
|
+
},
|
|
95
|
+
]}
|
|
96
|
+
/>
|
|
97
|
+
<View
|
|
98
|
+
style={[
|
|
99
|
+
styles.previewAlpha,
|
|
100
|
+
{
|
|
101
|
+
backgroundColor: `rgba(${color.rgb.r}, ${color.rgb.g}, ${color.rgb.b}, ${color.rgb.a})`,
|
|
102
|
+
},
|
|
103
|
+
]}
|
|
104
|
+
/>
|
|
105
|
+
</View>
|
|
106
|
+
)}
|
|
107
|
+
|
|
108
|
+
{/* Sliders section */}
|
|
109
|
+
<View style={styles.slidersContainer}>
|
|
110
|
+
{/* Hue slider - always shown for rectangle variant */}
|
|
111
|
+
{(!hideHue || variant === 'rectangle') && (
|
|
112
|
+
<View style={styles.sliderRow}>
|
|
113
|
+
<Hue
|
|
114
|
+
color={color}
|
|
115
|
+
onChange={onChange}
|
|
116
|
+
onChangeComplete={onChangeComplete}
|
|
117
|
+
barHeight={barHeight}
|
|
118
|
+
thumbSize={thumbSize}
|
|
119
|
+
disabled={disabled}
|
|
120
|
+
/>
|
|
121
|
+
</View>
|
|
122
|
+
)}
|
|
123
|
+
|
|
124
|
+
{/* Value/Brightness slider - only for circle variant */}
|
|
125
|
+
{variant === 'circle' && (
|
|
126
|
+
<View style={styles.sliderRow}>
|
|
127
|
+
<Value
|
|
128
|
+
color={color}
|
|
129
|
+
onChange={onChange}
|
|
130
|
+
onChangeComplete={onChangeComplete}
|
|
131
|
+
barHeight={barHeight}
|
|
132
|
+
thumbSize={thumbSize}
|
|
133
|
+
disabled={disabled}
|
|
134
|
+
/>
|
|
135
|
+
</View>
|
|
136
|
+
)}
|
|
137
|
+
|
|
138
|
+
{/* Alpha slider */}
|
|
139
|
+
{!hideAlpha && (
|
|
140
|
+
<View style={styles.sliderRow}>
|
|
141
|
+
<Alpha
|
|
142
|
+
color={color}
|
|
143
|
+
onChange={onChange}
|
|
144
|
+
onChangeComplete={onChangeComplete}
|
|
145
|
+
barHeight={barHeight}
|
|
146
|
+
thumbSize={thumbSize}
|
|
147
|
+
disabled={disabled}
|
|
148
|
+
/>
|
|
149
|
+
</View>
|
|
150
|
+
)}
|
|
151
|
+
</View>
|
|
152
|
+
|
|
153
|
+
{/* Input fields */}
|
|
154
|
+
{!hideInput && (
|
|
155
|
+
<View style={styles.fieldsContainer}>
|
|
156
|
+
<Fields
|
|
157
|
+
color={color}
|
|
158
|
+
onChange={onChange}
|
|
159
|
+
onChangeComplete={onChangeComplete}
|
|
160
|
+
disabled={disabled}
|
|
161
|
+
/>
|
|
162
|
+
</View>
|
|
163
|
+
)}
|
|
164
|
+
</View>
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
const styles = StyleSheet.create({
|
|
170
|
+
container: {
|
|
171
|
+
alignItems: 'center',
|
|
172
|
+
padding: 16,
|
|
173
|
+
},
|
|
174
|
+
disabled: {
|
|
175
|
+
opacity: 0.6,
|
|
176
|
+
},
|
|
177
|
+
pickerContainer: {
|
|
178
|
+
marginBottom: 20,
|
|
179
|
+
},
|
|
180
|
+
previewContainer: {
|
|
181
|
+
flexDirection: 'row',
|
|
182
|
+
marginBottom: 20,
|
|
183
|
+
borderRadius: 8,
|
|
184
|
+
overflow: 'hidden',
|
|
185
|
+
borderWidth: 1,
|
|
186
|
+
borderColor: '#E0E0E0',
|
|
187
|
+
},
|
|
188
|
+
preview: {
|
|
189
|
+
width: 40,
|
|
190
|
+
height: 40,
|
|
191
|
+
},
|
|
192
|
+
previewAlpha: {
|
|
193
|
+
width: 40,
|
|
194
|
+
height: 40,
|
|
195
|
+
},
|
|
196
|
+
slidersContainer: {
|
|
197
|
+
width: '100%',
|
|
198
|
+
gap: 16,
|
|
199
|
+
marginBottom: 20,
|
|
200
|
+
},
|
|
201
|
+
sliderRow: {
|
|
202
|
+
width: '100%',
|
|
203
|
+
},
|
|
204
|
+
fieldsContainer: {
|
|
205
|
+
width: '100%',
|
|
206
|
+
},
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
ColorPicker.displayName = 'ColorPicker';
|
|
@@ -0,0 +1,103 @@
|
|
|
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
|
+
export const HexField = memo(
|
|
15
|
+
({
|
|
16
|
+
color,
|
|
17
|
+
onChange,
|
|
18
|
+
onChangeComplete,
|
|
19
|
+
disabled = false,
|
|
20
|
+
}: IFieldsProps) => {
|
|
21
|
+
const [inputValue, setInputValue] = useState(color.hex);
|
|
22
|
+
const [isFocused, setIsFocused] = useState(false);
|
|
23
|
+
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
if (!isFocused) {
|
|
26
|
+
setInputValue(color.hex);
|
|
27
|
+
}
|
|
28
|
+
}, [color.hex, isFocused]);
|
|
29
|
+
|
|
30
|
+
const handleChangeText = useCallback((text: string) => {
|
|
31
|
+
let value = text.toUpperCase();
|
|
32
|
+
if (!value.startsWith('#')) {
|
|
33
|
+
value = '#' + value;
|
|
34
|
+
}
|
|
35
|
+
value = value.slice(0, 9);
|
|
36
|
+
setInputValue(value);
|
|
37
|
+
}, []);
|
|
38
|
+
|
|
39
|
+
const handleBlur = useCallback(() => {
|
|
40
|
+
setIsFocused(false);
|
|
41
|
+
const hexRegex = /^#([A-Fa-f0-9]{3}|[A-Fa-f0-9]{4}|[A-Fa-f0-9]{6}|[A-Fa-f0-9]{8})$/;
|
|
42
|
+
|
|
43
|
+
if (hexRegex.test(inputValue)) {
|
|
44
|
+
const nextColor = ColorService.convert('hex', inputValue);
|
|
45
|
+
onChange(nextColor);
|
|
46
|
+
onChangeComplete?.(nextColor);
|
|
47
|
+
} else {
|
|
48
|
+
setInputValue(color.hex);
|
|
49
|
+
}
|
|
50
|
+
}, [inputValue, color.hex, onChange, onChangeComplete]);
|
|
51
|
+
|
|
52
|
+
const handleFocus = useCallback(() => {
|
|
53
|
+
setIsFocused(true);
|
|
54
|
+
}, []);
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<View style={styles.container}>
|
|
58
|
+
<Text style={styles.label}>HEX</Text>
|
|
59
|
+
<TextInput
|
|
60
|
+
style={[styles.input, disabled && styles.inputDisabled]}
|
|
61
|
+
value={inputValue}
|
|
62
|
+
onChangeText={handleChangeText}
|
|
63
|
+
onBlur={handleBlur}
|
|
64
|
+
onFocus={handleFocus}
|
|
65
|
+
editable={!disabled}
|
|
66
|
+
autoCapitalize="characters"
|
|
67
|
+
autoCorrect={false}
|
|
68
|
+
maxLength={9}
|
|
69
|
+
placeholder="#000000"
|
|
70
|
+
placeholderTextColor="#999999"
|
|
71
|
+
numberOfLines={1}
|
|
72
|
+
/>
|
|
73
|
+
</View>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
const styles = StyleSheet.create({
|
|
79
|
+
container: { flex: 1 },
|
|
80
|
+
label: {
|
|
81
|
+
fontSize: 10,
|
|
82
|
+
fontWeight: '600',
|
|
83
|
+
color: '#666666',
|
|
84
|
+
marginBottom: 4,
|
|
85
|
+
textAlign: 'center',
|
|
86
|
+
},
|
|
87
|
+
input: {
|
|
88
|
+
height: 36,
|
|
89
|
+
borderWidth: 1,
|
|
90
|
+
borderColor: '#E0E0E0',
|
|
91
|
+
borderRadius: 6,
|
|
92
|
+
fontSize: 14,
|
|
93
|
+
color: '#333333',
|
|
94
|
+
backgroundColor: '#FFFFFF',
|
|
95
|
+
textAlign: 'center',
|
|
96
|
+
},
|
|
97
|
+
inputDisabled: {
|
|
98
|
+
backgroundColor: '#F5F5F5',
|
|
99
|
+
color: '#999999',
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
HexField.displayName = 'HexField';
|
|
@@ -0,0 +1,138 @@
|
|
|
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
|
+
import { clamp, safeParseInt } from '../../../core/utils';
|
|
6
|
+
|
|
7
|
+
interface RgbInputProps {
|
|
8
|
+
label: string;
|
|
9
|
+
value: number;
|
|
10
|
+
onChange: (value: number) => void;
|
|
11
|
+
onComplete: () => void;
|
|
12
|
+
disabled?: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const RgbInput = memo(
|
|
16
|
+
({ label, value, onChange, onComplete, disabled }: RgbInputProps) => {
|
|
17
|
+
const [inputValue, setInputValue] = useState(String(value));
|
|
18
|
+
const [isFocused, setIsFocused] = useState(false);
|
|
19
|
+
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
if (!isFocused) {
|
|
22
|
+
setInputValue(String(value));
|
|
23
|
+
}
|
|
24
|
+
}, [value, isFocused]);
|
|
25
|
+
|
|
26
|
+
const handleChangeText = useCallback((text: string) => {
|
|
27
|
+
const numericValue = text.replace(/[^0-9]/g, '');
|
|
28
|
+
setInputValue(numericValue);
|
|
29
|
+
}, []);
|
|
30
|
+
|
|
31
|
+
const handleBlur = useCallback(() => {
|
|
32
|
+
setIsFocused(false);
|
|
33
|
+
const numValue = clamp(safeParseInt(inputValue), 0, 255);
|
|
34
|
+
setInputValue(String(numValue));
|
|
35
|
+
onChange(numValue);
|
|
36
|
+
onComplete();
|
|
37
|
+
}, [inputValue, onChange, onComplete]);
|
|
38
|
+
|
|
39
|
+
const handleFocus = useCallback(() => {
|
|
40
|
+
setIsFocused(true);
|
|
41
|
+
}, []);
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<View style={styles.inputContainer}>
|
|
45
|
+
<Text style={styles.label}>{label}</Text>
|
|
46
|
+
<TextInput
|
|
47
|
+
style={[styles.input, disabled && styles.inputDisabled]}
|
|
48
|
+
value={inputValue}
|
|
49
|
+
onChangeText={handleChangeText}
|
|
50
|
+
onBlur={handleBlur}
|
|
51
|
+
onFocus={handleFocus}
|
|
52
|
+
editable={!disabled}
|
|
53
|
+
keyboardType="number-pad"
|
|
54
|
+
maxLength={3}
|
|
55
|
+
placeholder="0"
|
|
56
|
+
placeholderTextColor="#999999"
|
|
57
|
+
/>
|
|
58
|
+
</View>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* RGB color input fields
|
|
65
|
+
*/
|
|
66
|
+
export const RgbFields = memo(
|
|
67
|
+
({
|
|
68
|
+
color,
|
|
69
|
+
onChange,
|
|
70
|
+
onChangeComplete,
|
|
71
|
+
disabled = false,
|
|
72
|
+
}: IFieldsProps) => {
|
|
73
|
+
const handleRChange = useCallback(
|
|
74
|
+
(r: number) => {
|
|
75
|
+
const nextColor = ColorService.convert('rgb', { ...color.rgb, r });
|
|
76
|
+
onChange(nextColor);
|
|
77
|
+
},
|
|
78
|
+
[color.rgb, onChange]
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
const handleGChange = useCallback(
|
|
82
|
+
(g: number) => {
|
|
83
|
+
const nextColor = ColorService.convert('rgb', { ...color.rgb, g });
|
|
84
|
+
onChange(nextColor);
|
|
85
|
+
},
|
|
86
|
+
[color.rgb, onChange]
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
const handleBChange = useCallback(
|
|
90
|
+
(b: number) => {
|
|
91
|
+
const nextColor = ColorService.convert('rgb', { ...color.rgb, b });
|
|
92
|
+
onChange(nextColor);
|
|
93
|
+
},
|
|
94
|
+
[color.rgb, onChange]
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
const handleComplete = useCallback(() => {
|
|
98
|
+
onChangeComplete?.(color);
|
|
99
|
+
}, [color, onChangeComplete]);
|
|
100
|
+
|
|
101
|
+
return (
|
|
102
|
+
<View style={styles.container}>
|
|
103
|
+
<RgbInput label="R" value={color.rgb.r} onChange={handleRChange} onComplete={handleComplete} disabled={disabled} />
|
|
104
|
+
<RgbInput label="G" value={color.rgb.g} onChange={handleGChange} onComplete={handleComplete} disabled={disabled} />
|
|
105
|
+
<RgbInput label="B" value={color.rgb.b} onChange={handleBChange} onComplete={handleComplete} disabled={disabled} />
|
|
106
|
+
</View>
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
const styles = StyleSheet.create({
|
|
112
|
+
container: { flexDirection: 'row', gap: 8 },
|
|
113
|
+
inputContainer: { flex: 1 },
|
|
114
|
+
label: {
|
|
115
|
+
fontSize: 10,
|
|
116
|
+
fontWeight: '600',
|
|
117
|
+
color: '#666666',
|
|
118
|
+
marginBottom: 4,
|
|
119
|
+
textAlign: 'center',
|
|
120
|
+
},
|
|
121
|
+
input: {
|
|
122
|
+
height: 36,
|
|
123
|
+
borderWidth: 1,
|
|
124
|
+
borderColor: '#E0E0E0',
|
|
125
|
+
borderRadius: 6,
|
|
126
|
+
paddingHorizontal: 8,
|
|
127
|
+
fontSize: 14,
|
|
128
|
+
color: '#333333',
|
|
129
|
+
backgroundColor: '#FFFFFF',
|
|
130
|
+
textAlign: 'center',
|
|
131
|
+
},
|
|
132
|
+
inputDisabled: {
|
|
133
|
+
backgroundColor: '#F5F5F5',
|
|
134
|
+
color: '#999999',
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
RgbFields.displayName = 'RgbFields';
|