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,221 @@
|
|
|
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 { IAlphaProps } from '../../core/types';
|
|
11
|
+
import { ColorService } from '../../core/services';
|
|
12
|
+
import { clamp } from '../../core/utils';
|
|
13
|
+
import { Thumb } from './Thumb';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Alpha (transparency) slider bar component
|
|
17
|
+
*
|
|
18
|
+
* Features:
|
|
19
|
+
* - Uses react-native-svg for gradient
|
|
20
|
+
* - Caches measurements for better performance
|
|
21
|
+
* - Static checkerboard pattern (not generated on each render)
|
|
22
|
+
* - Horizontal bar with gradient from transparent to solid
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```tsx
|
|
26
|
+
* <Alpha
|
|
27
|
+
* color={color}
|
|
28
|
+
* onChange={handleChange}
|
|
29
|
+
* barHeight={10}
|
|
30
|
+
* thumbSize={24}
|
|
31
|
+
* />
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
export const Alpha = memo(
|
|
35
|
+
({
|
|
36
|
+
color,
|
|
37
|
+
onChange,
|
|
38
|
+
onChangeComplete,
|
|
39
|
+
barHeight,
|
|
40
|
+
thumbSize,
|
|
41
|
+
disabled = false,
|
|
42
|
+
}: IAlphaProps) => {
|
|
43
|
+
const containerRef = useRef<View>(null);
|
|
44
|
+
const [width, setWidth] = useState(1);
|
|
45
|
+
const measurementCache = useRef<number | null>(null);
|
|
46
|
+
|
|
47
|
+
// Calculate thumb position from alpha value (a=1 should be at right)
|
|
48
|
+
const thumbPosition = useMemo(() => {
|
|
49
|
+
return color.hsv.a * width;
|
|
50
|
+
}, [color.hsv.a, width]);
|
|
51
|
+
|
|
52
|
+
// Update measurement cache
|
|
53
|
+
const updateMeasurement = useCallback(() => {
|
|
54
|
+
return new Promise<number>((resolve) => {
|
|
55
|
+
if (measurementCache.current !== null) {
|
|
56
|
+
resolve(measurementCache.current);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
containerRef.current?.measureInWindow((x) => {
|
|
60
|
+
measurementCache.current = x;
|
|
61
|
+
resolve(x);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
}, []);
|
|
65
|
+
|
|
66
|
+
// Convert touch position to alpha
|
|
67
|
+
const updateColorFromPosition = useCallback(
|
|
68
|
+
async (pageX: number, isFinal: boolean) => {
|
|
69
|
+
const wx = await updateMeasurement();
|
|
70
|
+
const x = clamp(pageX - wx, 0, width);
|
|
71
|
+
const alpha = x / width;
|
|
72
|
+
|
|
73
|
+
const nextColor = ColorService.convert('hsv', {
|
|
74
|
+
...color.hsv,
|
|
75
|
+
a: clamp(alpha, 0, 1),
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
onChange(nextColor);
|
|
79
|
+
|
|
80
|
+
if (isFinal) {
|
|
81
|
+
onChangeComplete?.(nextColor);
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
[color.hsv, width, onChange, onChangeComplete, updateMeasurement]
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
// PanResponder for touch handling
|
|
88
|
+
const panResponder = useMemo(
|
|
89
|
+
() =>
|
|
90
|
+
PanResponder.create({
|
|
91
|
+
onStartShouldSetPanResponder: () => !disabled,
|
|
92
|
+
onMoveShouldSetPanResponder: () => !disabled,
|
|
93
|
+
onPanResponderGrant: (evt: GestureResponderEvent) => {
|
|
94
|
+
measurementCache.current = null;
|
|
95
|
+
updateColorFromPosition(evt.nativeEvent.pageX, false);
|
|
96
|
+
},
|
|
97
|
+
onPanResponderMove: (evt: GestureResponderEvent) => {
|
|
98
|
+
updateColorFromPosition(evt.nativeEvent.pageX, false);
|
|
99
|
+
},
|
|
100
|
+
onPanResponderRelease: (evt: GestureResponderEvent) => {
|
|
101
|
+
updateColorFromPosition(evt.nativeEvent.pageX, true);
|
|
102
|
+
},
|
|
103
|
+
}),
|
|
104
|
+
[disabled, updateColorFromPosition]
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
// Handle layout to get width and clear cache
|
|
108
|
+
const handleLayout = useCallback((event: LayoutChangeEvent) => {
|
|
109
|
+
setWidth(event.nativeEvent.layout.width);
|
|
110
|
+
measurementCache.current = null;
|
|
111
|
+
}, []);
|
|
112
|
+
|
|
113
|
+
// Solid color for gradient end
|
|
114
|
+
const solidColor = `rgb(${color.rgb.r}, ${color.rgb.g}, ${color.rgb.b})`;
|
|
115
|
+
|
|
116
|
+
return (
|
|
117
|
+
<View
|
|
118
|
+
ref={containerRef}
|
|
119
|
+
style={[
|
|
120
|
+
styles.container,
|
|
121
|
+
{
|
|
122
|
+
height: barHeight + thumbSize,
|
|
123
|
+
opacity: disabled ? 0.5 : 1,
|
|
124
|
+
},
|
|
125
|
+
]}
|
|
126
|
+
onLayout={handleLayout}
|
|
127
|
+
{...panResponder.panHandlers}
|
|
128
|
+
>
|
|
129
|
+
{/* Bar with checkerboard and gradient */}
|
|
130
|
+
<View style={[styles.barContainer, { height: barHeight, top: thumbSize / 2 }]}>
|
|
131
|
+
{/* Checkerboard background - dynamic size based on barHeight */}
|
|
132
|
+
<View style={[styles.checkerboard, { borderRadius: barHeight / 2 }]}>
|
|
133
|
+
{Array.from({ length: 2 }).map((_, rowIndex) => {
|
|
134
|
+
const checkerSize = barHeight / 2;
|
|
135
|
+
const cellCount = Math.ceil(width / checkerSize) + 1;
|
|
136
|
+
return (
|
|
137
|
+
<View key={rowIndex} style={[styles.checkerRow, { height: checkerSize }]}>
|
|
138
|
+
{Array.from({ length: cellCount }).map((_, i) => (
|
|
139
|
+
<View
|
|
140
|
+
key={i}
|
|
141
|
+
style={{
|
|
142
|
+
width: checkerSize,
|
|
143
|
+
height: checkerSize,
|
|
144
|
+
backgroundColor: (i + rowIndex) % 2 === 0 ? '#FFFFFF' : '#CCCCCC',
|
|
145
|
+
}}
|
|
146
|
+
/>
|
|
147
|
+
))}
|
|
148
|
+
</View>
|
|
149
|
+
);
|
|
150
|
+
})}
|
|
151
|
+
</View>
|
|
152
|
+
|
|
153
|
+
{/* Color gradient overlay using SVG */}
|
|
154
|
+
<View style={[styles.gradient, { borderRadius: barHeight / 2 }]}>
|
|
155
|
+
<Svg width="100%" height={barHeight}>
|
|
156
|
+
<Defs>
|
|
157
|
+
<LinearGradient id="alphaGradient" x1="0%" y1="0%" x2="100%" y2="0%">
|
|
158
|
+
<Stop offset="0%" stopColor={solidColor} stopOpacity="0" />
|
|
159
|
+
<Stop offset="100%" stopColor={solidColor} stopOpacity="1" />
|
|
160
|
+
</LinearGradient>
|
|
161
|
+
</Defs>
|
|
162
|
+
<Rect
|
|
163
|
+
x="0"
|
|
164
|
+
y="0"
|
|
165
|
+
width="100%"
|
|
166
|
+
height={barHeight}
|
|
167
|
+
rx={barHeight / 2}
|
|
168
|
+
ry={barHeight / 2}
|
|
169
|
+
fill="url(#alphaGradient)"
|
|
170
|
+
/>
|
|
171
|
+
</Svg>
|
|
172
|
+
</View>
|
|
173
|
+
</View>
|
|
174
|
+
|
|
175
|
+
{/* Thumb indicator - centered vertically with bar */}
|
|
176
|
+
<View
|
|
177
|
+
style={[
|
|
178
|
+
styles.thumbContainer,
|
|
179
|
+
{
|
|
180
|
+
left: thumbPosition - thumbSize / 2,
|
|
181
|
+
top: barHeight / 2,
|
|
182
|
+
},
|
|
183
|
+
]}
|
|
184
|
+
pointerEvents="none"
|
|
185
|
+
>
|
|
186
|
+
<Thumb size={thumbSize} color={`rgba(${color.rgb.r}, ${color.rgb.g}, ${color.rgb.b}, ${color.hsv.a})`} />
|
|
187
|
+
</View>
|
|
188
|
+
</View>
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
const styles = StyleSheet.create({
|
|
194
|
+
container: {
|
|
195
|
+
position: 'relative',
|
|
196
|
+
width: '100%',
|
|
197
|
+
},
|
|
198
|
+
barContainer: {
|
|
199
|
+
position: 'absolute',
|
|
200
|
+
left: 0,
|
|
201
|
+
right: 0,
|
|
202
|
+
overflow: 'hidden',
|
|
203
|
+
},
|
|
204
|
+
checkerboard: {
|
|
205
|
+
...StyleSheet.absoluteFillObject,
|
|
206
|
+
overflow: 'hidden',
|
|
207
|
+
},
|
|
208
|
+
checkerRow: {
|
|
209
|
+
flexDirection: 'row',
|
|
210
|
+
},
|
|
211
|
+
gradient: {
|
|
212
|
+
...StyleSheet.absoluteFillObject,
|
|
213
|
+
overflow: 'hidden',
|
|
214
|
+
},
|
|
215
|
+
thumbContainer: {
|
|
216
|
+
position: 'absolute',
|
|
217
|
+
zIndex: 10,
|
|
218
|
+
},
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
Alpha.displayName = 'Alpha';
|
|
@@ -0,0 +1,206 @@
|
|
|
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 (Full version with react-native-svg)
|
|
13
|
+
*
|
|
14
|
+
* Features:
|
|
15
|
+
* - Circular color wheel or rectangle for saturation/value selection
|
|
16
|
+
* - Horizontal hue bar
|
|
17
|
+
* - Horizontal alpha bar with checkerboard background
|
|
18
|
+
* - HEX and RGB input fields
|
|
19
|
+
* - Circle thumbs with white 2px border showing selected color
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```tsx
|
|
23
|
+
* import { ColorPicker, useColor } from 'react-native-color-palette';
|
|
24
|
+
*
|
|
25
|
+
* function MyComponent() {
|
|
26
|
+
* const [color, setColor] = useColor('#FF0000');
|
|
27
|
+
*
|
|
28
|
+
* return (
|
|
29
|
+
* <ColorPicker
|
|
30
|
+
* color={color}
|
|
31
|
+
* onChange={setColor}
|
|
32
|
+
* variant="rectangle"
|
|
33
|
+
* onChangeComplete={(c) => console.log('Final color:', c.hex)}
|
|
34
|
+
* />
|
|
35
|
+
* );
|
|
36
|
+
* }
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
export const ColorPicker = memo(
|
|
40
|
+
({
|
|
41
|
+
color,
|
|
42
|
+
onChange,
|
|
43
|
+
onChangeComplete,
|
|
44
|
+
variant = 'rectangle',
|
|
45
|
+
width = 250,
|
|
46
|
+
barHeight = 10,
|
|
47
|
+
thumbSize = 24,
|
|
48
|
+
hideHue = false,
|
|
49
|
+
hideAlpha = false,
|
|
50
|
+
hidePreview = false,
|
|
51
|
+
hideInput = false,
|
|
52
|
+
disabled = false,
|
|
53
|
+
}: IColorPickerProps) => {
|
|
54
|
+
// Rectangle height is 60% of width for better proportions
|
|
55
|
+
const rectangleHeight = width * 0.6;
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<View style={[styles.container, disabled && styles.disabled]}>
|
|
59
|
+
{/* Color picker (circle or rectangle) */}
|
|
60
|
+
<View style={styles.pickerContainer}>
|
|
61
|
+
{variant === 'circle' ? (
|
|
62
|
+
<Saturation
|
|
63
|
+
color={color}
|
|
64
|
+
onChange={onChange}
|
|
65
|
+
onChangeComplete={onChangeComplete}
|
|
66
|
+
size={width}
|
|
67
|
+
thumbSize={thumbSize}
|
|
68
|
+
disabled={disabled}
|
|
69
|
+
/>
|
|
70
|
+
) : (
|
|
71
|
+
<RectangleSaturation
|
|
72
|
+
color={color}
|
|
73
|
+
onChange={onChange}
|
|
74
|
+
onChangeComplete={onChangeComplete}
|
|
75
|
+
width={width}
|
|
76
|
+
height={rectangleHeight}
|
|
77
|
+
thumbSize={thumbSize}
|
|
78
|
+
disabled={disabled}
|
|
79
|
+
/>
|
|
80
|
+
)}
|
|
81
|
+
</View>
|
|
82
|
+
|
|
83
|
+
{/* Color preview */}
|
|
84
|
+
{!hidePreview && (
|
|
85
|
+
<View style={styles.previewContainer}>
|
|
86
|
+
<View
|
|
87
|
+
style={[
|
|
88
|
+
styles.preview,
|
|
89
|
+
{
|
|
90
|
+
backgroundColor: color.hex,
|
|
91
|
+
},
|
|
92
|
+
]}
|
|
93
|
+
/>
|
|
94
|
+
<View
|
|
95
|
+
style={[
|
|
96
|
+
styles.previewAlpha,
|
|
97
|
+
{
|
|
98
|
+
backgroundColor: `rgba(${color.rgb.r}, ${color.rgb.g}, ${color.rgb.b}, ${color.rgb.a})`,
|
|
99
|
+
},
|
|
100
|
+
]}
|
|
101
|
+
/>
|
|
102
|
+
</View>
|
|
103
|
+
)}
|
|
104
|
+
|
|
105
|
+
{/* Sliders section */}
|
|
106
|
+
<View style={styles.slidersContainer}>
|
|
107
|
+
{/* Hue slider - always shown for rectangle variant */}
|
|
108
|
+
{(!hideHue || variant === 'rectangle') && (
|
|
109
|
+
<View style={styles.sliderRow}>
|
|
110
|
+
<Hue
|
|
111
|
+
color={color}
|
|
112
|
+
onChange={onChange}
|
|
113
|
+
onChangeComplete={onChangeComplete}
|
|
114
|
+
barHeight={barHeight}
|
|
115
|
+
thumbSize={thumbSize}
|
|
116
|
+
disabled={disabled}
|
|
117
|
+
/>
|
|
118
|
+
</View>
|
|
119
|
+
)}
|
|
120
|
+
|
|
121
|
+
{/* Value/Brightness slider - only for circle variant */}
|
|
122
|
+
{variant === 'circle' && (
|
|
123
|
+
<View style={styles.sliderRow}>
|
|
124
|
+
<Value
|
|
125
|
+
color={color}
|
|
126
|
+
onChange={onChange}
|
|
127
|
+
onChangeComplete={onChangeComplete}
|
|
128
|
+
barHeight={barHeight}
|
|
129
|
+
thumbSize={thumbSize}
|
|
130
|
+
disabled={disabled}
|
|
131
|
+
/>
|
|
132
|
+
</View>
|
|
133
|
+
)}
|
|
134
|
+
|
|
135
|
+
{/* Alpha slider */}
|
|
136
|
+
{!hideAlpha && (
|
|
137
|
+
<View style={styles.sliderRow}>
|
|
138
|
+
<Alpha
|
|
139
|
+
color={color}
|
|
140
|
+
onChange={onChange}
|
|
141
|
+
onChangeComplete={onChangeComplete}
|
|
142
|
+
barHeight={barHeight}
|
|
143
|
+
thumbSize={thumbSize}
|
|
144
|
+
disabled={disabled}
|
|
145
|
+
/>
|
|
146
|
+
</View>
|
|
147
|
+
)}
|
|
148
|
+
</View>
|
|
149
|
+
|
|
150
|
+
{/* Input fields */}
|
|
151
|
+
{!hideInput && (
|
|
152
|
+
<View style={styles.fieldsContainer}>
|
|
153
|
+
<Fields
|
|
154
|
+
color={color}
|
|
155
|
+
onChange={onChange}
|
|
156
|
+
onChangeComplete={onChangeComplete}
|
|
157
|
+
disabled={disabled}
|
|
158
|
+
/>
|
|
159
|
+
</View>
|
|
160
|
+
)}
|
|
161
|
+
</View>
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
const styles = StyleSheet.create({
|
|
167
|
+
container: {
|
|
168
|
+
alignItems: 'center',
|
|
169
|
+
padding: 16,
|
|
170
|
+
},
|
|
171
|
+
disabled: {
|
|
172
|
+
opacity: 0.6,
|
|
173
|
+
},
|
|
174
|
+
pickerContainer: {
|
|
175
|
+
marginBottom: 20,
|
|
176
|
+
},
|
|
177
|
+
previewContainer: {
|
|
178
|
+
flexDirection: 'row',
|
|
179
|
+
marginBottom: 20,
|
|
180
|
+
borderRadius: 8,
|
|
181
|
+
overflow: 'hidden',
|
|
182
|
+
borderWidth: 1,
|
|
183
|
+
borderColor: '#E0E0E0',
|
|
184
|
+
},
|
|
185
|
+
preview: {
|
|
186
|
+
width: 40,
|
|
187
|
+
height: 40,
|
|
188
|
+
},
|
|
189
|
+
previewAlpha: {
|
|
190
|
+
width: 40,
|
|
191
|
+
height: 40,
|
|
192
|
+
},
|
|
193
|
+
slidersContainer: {
|
|
194
|
+
width: '100%',
|
|
195
|
+
gap: 16,
|
|
196
|
+
marginBottom: 20,
|
|
197
|
+
},
|
|
198
|
+
sliderRow: {
|
|
199
|
+
width: '100%',
|
|
200
|
+
},
|
|
201
|
+
fieldsContainer: {
|
|
202
|
+
width: '100%',
|
|
203
|
+
},
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
ColorPicker.displayName = 'ColorPicker';
|
|
@@ -0,0 +1,125 @@
|
|
|
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
|
+
* @example
|
|
15
|
+
* ```tsx
|
|
16
|
+
* <HexField
|
|
17
|
+
* color={color}
|
|
18
|
+
* onChange={handleChange}
|
|
19
|
+
* onChangeComplete={handleChangeComplete}
|
|
20
|
+
* />
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
export const HexField = memo(
|
|
24
|
+
({
|
|
25
|
+
color,
|
|
26
|
+
onChange,
|
|
27
|
+
onChangeComplete,
|
|
28
|
+
disabled = false,
|
|
29
|
+
}: IFieldsProps) => {
|
|
30
|
+
const [inputValue, setInputValue] = useState(color.hex);
|
|
31
|
+
const [isFocused, setIsFocused] = useState(false);
|
|
32
|
+
|
|
33
|
+
// Update input when color changes externally
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
if (!isFocused) {
|
|
36
|
+
setInputValue(color.hex);
|
|
37
|
+
}
|
|
38
|
+
}, [color.hex, isFocused]);
|
|
39
|
+
|
|
40
|
+
// Handle text change
|
|
41
|
+
const handleChangeText = useCallback((text: string) => {
|
|
42
|
+
// Add # if not present
|
|
43
|
+
let value = text.toUpperCase();
|
|
44
|
+
if (!value.startsWith('#')) {
|
|
45
|
+
value = '#' + value;
|
|
46
|
+
}
|
|
47
|
+
// Limit to valid hex length
|
|
48
|
+
value = value.slice(0, 9); // #RRGGBBAA max
|
|
49
|
+
setInputValue(value);
|
|
50
|
+
}, []);
|
|
51
|
+
|
|
52
|
+
// Validate and apply color on blur
|
|
53
|
+
const handleBlur = useCallback(() => {
|
|
54
|
+
setIsFocused(false);
|
|
55
|
+
|
|
56
|
+
// Validate hex format
|
|
57
|
+
const hexRegex = /^#([A-Fa-f0-9]{3}|[A-Fa-f0-9]{4}|[A-Fa-f0-9]{6}|[A-Fa-f0-9]{8})$/;
|
|
58
|
+
|
|
59
|
+
if (hexRegex.test(inputValue)) {
|
|
60
|
+
const nextColor = ColorService.convert('hex', inputValue);
|
|
61
|
+
onChange(nextColor);
|
|
62
|
+
onChangeComplete?.(nextColor);
|
|
63
|
+
} else {
|
|
64
|
+
// Reset to current color if invalid
|
|
65
|
+
setInputValue(color.hex);
|
|
66
|
+
}
|
|
67
|
+
}, [inputValue, color.hex, onChange, onChangeComplete]);
|
|
68
|
+
|
|
69
|
+
const handleFocus = useCallback(() => {
|
|
70
|
+
setIsFocused(true);
|
|
71
|
+
}, []);
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<View style={styles.container}>
|
|
75
|
+
<Text style={styles.label}>HEX</Text>
|
|
76
|
+
<TextInput
|
|
77
|
+
style={[
|
|
78
|
+
styles.input,
|
|
79
|
+
disabled && styles.inputDisabled,
|
|
80
|
+
]}
|
|
81
|
+
value={inputValue}
|
|
82
|
+
onChangeText={handleChangeText}
|
|
83
|
+
onBlur={handleBlur}
|
|
84
|
+
onFocus={handleFocus}
|
|
85
|
+
editable={!disabled}
|
|
86
|
+
autoCapitalize="characters"
|
|
87
|
+
autoCorrect={false}
|
|
88
|
+
maxLength={9}
|
|
89
|
+
placeholder="#000000"
|
|
90
|
+
placeholderTextColor="#999999"
|
|
91
|
+
numberOfLines={1}
|
|
92
|
+
/>
|
|
93
|
+
</View>
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
const styles = StyleSheet.create({
|
|
99
|
+
container: {
|
|
100
|
+
flex: 1,
|
|
101
|
+
},
|
|
102
|
+
label: {
|
|
103
|
+
fontSize: 10,
|
|
104
|
+
fontWeight: '600',
|
|
105
|
+
color: '#666666',
|
|
106
|
+
marginBottom: 4,
|
|
107
|
+
textAlign: 'center',
|
|
108
|
+
},
|
|
109
|
+
input: {
|
|
110
|
+
height: 36,
|
|
111
|
+
borderWidth: 1,
|
|
112
|
+
borderColor: '#E0E0E0',
|
|
113
|
+
borderRadius: 6,
|
|
114
|
+
fontSize: 14,
|
|
115
|
+
color: '#333333',
|
|
116
|
+
backgroundColor: '#FFFFFF',
|
|
117
|
+
textAlign: 'center',
|
|
118
|
+
},
|
|
119
|
+
inputDisabled: {
|
|
120
|
+
backgroundColor: '#F5F5F5',
|
|
121
|
+
color: '#999999',
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
HexField.displayName = 'HexField';
|