gdp-color-picker 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/README.md +85 -0
- package/dist/index.css +2 -0
- package/dist/index.css.map +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/index.modern.mjs +2 -0
- package/dist/index.modern.mjs.map +1 -0
- package/dist/index.module.js +2 -0
- package/dist/index.module.js.map +1 -0
- package/dist/index.umd.js +2 -0
- package/dist/index.umd.js.map +1 -0
- package/package.json +52 -0
- package/src/lib/ColorBoard.js +70 -0
- package/src/lib/ColorInput.js +140 -0
- package/src/lib/Sliders.js +86 -0
- package/src/lib/index.css +198 -0
- package/src/lib/index.js +113 -0
- package/src/lib/utils/color.js +216 -0
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "gdp-color-picker",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"author": "Gap-L <13620238719@163.com>",
|
|
5
|
+
"private": false,
|
|
6
|
+
"description": "A custom color picker component compatible with React 16.8",
|
|
7
|
+
"source": "src/lib/index.js",
|
|
8
|
+
"main": "dist/index.js",
|
|
9
|
+
"module": "dist/index.module.js",
|
|
10
|
+
"unpkg": "dist/index.umd.js",
|
|
11
|
+
"files": [
|
|
12
|
+
"dist",
|
|
13
|
+
"src/lib",
|
|
14
|
+
"README.md"
|
|
15
|
+
],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"start": "react-scripts start",
|
|
18
|
+
"build:demo": "react-scripts build",
|
|
19
|
+
"build": "microbundle --jsx React.createElement --jsxFragment React.Fragment --globals react=React",
|
|
20
|
+
"prepare": "npm run build"
|
|
21
|
+
},
|
|
22
|
+
"peerDependencies": {
|
|
23
|
+
"react": ">=16.8.0",
|
|
24
|
+
"react-dom": ">=16.8.0"
|
|
25
|
+
},
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"prop-types": "^15.7.0"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"autoprefixer": "^10.4.23",
|
|
31
|
+
"microbundle": "^0.15.1",
|
|
32
|
+
"postcss": "^8.5.6",
|
|
33
|
+
"react": "^16.8.0",
|
|
34
|
+
"react-dom": "^16.8.0",
|
|
35
|
+
"react-scripts": "3.4.0"
|
|
36
|
+
},
|
|
37
|
+
"engines": {
|
|
38
|
+
"node": ">=10"
|
|
39
|
+
},
|
|
40
|
+
"browserslist": {
|
|
41
|
+
"production": [
|
|
42
|
+
">0.2%",
|
|
43
|
+
"not dead",
|
|
44
|
+
"not op_mini all"
|
|
45
|
+
],
|
|
46
|
+
"development": [
|
|
47
|
+
"last 1 chrome version",
|
|
48
|
+
"last 1 firefox version",
|
|
49
|
+
"last 1 safari version"
|
|
50
|
+
]
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
|
|
2
|
+
import React, { useRef, useEffect, useState, useCallback } from 'react';
|
|
3
|
+
import { clamp } from './utils/color';
|
|
4
|
+
|
|
5
|
+
const ColorBoard = ({ hue, saturation, value, onChange }) => {
|
|
6
|
+
const containerRef = useRef(null);
|
|
7
|
+
const [isDragging, setIsDragging] = useState(false);
|
|
8
|
+
|
|
9
|
+
const handleChange = useCallback((e) => {
|
|
10
|
+
if (!containerRef.current) return;
|
|
11
|
+
const { width, height, left, top } = containerRef.current.getBoundingClientRect();
|
|
12
|
+
const x = clamp(e.clientX - left, 0, width);
|
|
13
|
+
const y = clamp(e.clientY - top, 0, height);
|
|
14
|
+
|
|
15
|
+
const newSaturation = x / width;
|
|
16
|
+
const newValue = 1 - y / height;
|
|
17
|
+
|
|
18
|
+
onChange(newSaturation, newValue);
|
|
19
|
+
}, [onChange]);
|
|
20
|
+
|
|
21
|
+
const handleMouseDown = (e) => {
|
|
22
|
+
setIsDragging(true);
|
|
23
|
+
handleChange(e);
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const handleMouseUp = useCallback(() => {
|
|
27
|
+
setIsDragging(false);
|
|
28
|
+
}, []);
|
|
29
|
+
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
const handleMouseMove = (e) => {
|
|
32
|
+
if (isDragging) {
|
|
33
|
+
handleChange(e);
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
if (isDragging) {
|
|
38
|
+
window.addEventListener('mousemove', handleMouseMove);
|
|
39
|
+
window.addEventListener('mouseup', handleMouseUp);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return () => {
|
|
43
|
+
window.removeEventListener('mousemove', handleMouseMove);
|
|
44
|
+
window.removeEventListener('mouseup', handleMouseUp);
|
|
45
|
+
};
|
|
46
|
+
}, [isDragging, handleChange, handleMouseUp]);
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<div
|
|
50
|
+
ref={containerRef}
|
|
51
|
+
className="color-board"
|
|
52
|
+
style={{
|
|
53
|
+
backgroundColor: `hsl(${hue}, 100%, 50%)`,
|
|
54
|
+
}}
|
|
55
|
+
onMouseDown={handleMouseDown}
|
|
56
|
+
>
|
|
57
|
+
<div className="color-board-white" />
|
|
58
|
+
<div className="color-board-black" />
|
|
59
|
+
<div
|
|
60
|
+
className="color-board-handler"
|
|
61
|
+
style={{
|
|
62
|
+
left: `${saturation * 100}%`,
|
|
63
|
+
top: `${(1 - value) * 100}%`,
|
|
64
|
+
}}
|
|
65
|
+
/>
|
|
66
|
+
</div>
|
|
67
|
+
);
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
export default ColorBoard;
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import React, { useState, useEffect, useCallback, useRef } from 'react';
|
|
2
|
+
import { rgbToHex, hexToRgb, hsvToRgb, rgbToHsv, rgbToHsl, hslToRgb } from './utils/color';
|
|
3
|
+
|
|
4
|
+
const ColorInput = ({ hue, saturation, value, alpha, onChange }) => {
|
|
5
|
+
const [mode, setMode] = useState('HEX');
|
|
6
|
+
const [localValue, setLocalValue] = useState({});
|
|
7
|
+
const [showDropdown, setShowDropdown] = useState(false);
|
|
8
|
+
const dropdownRef = useRef(null);
|
|
9
|
+
|
|
10
|
+
// Close dropdown when clicking outside
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
const handleClickOutside = (event) => {
|
|
13
|
+
if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
|
|
14
|
+
setShowDropdown(false);
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
document.addEventListener('mousedown', handleClickOutside);
|
|
18
|
+
return () => {
|
|
19
|
+
document.removeEventListener('mousedown', handleClickOutside);
|
|
20
|
+
};
|
|
21
|
+
}, []);
|
|
22
|
+
|
|
23
|
+
const getRgb = useCallback(() => hsvToRgb(hue, saturation, value), [hue, saturation, value]);
|
|
24
|
+
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
const rgb = getRgb();
|
|
27
|
+
if (mode === 'HEX') {
|
|
28
|
+
setLocalValue({ hex: rgbToHex(rgb.r, rgb.g, rgb.b) });
|
|
29
|
+
} else if (mode === 'RGB') {
|
|
30
|
+
setLocalValue({ r: rgb.r, g: rgb.g, b: rgb.b });
|
|
31
|
+
} else if (mode === 'HSL') {
|
|
32
|
+
const hsl = rgbToHsl(rgb.r, rgb.g, rgb.b);
|
|
33
|
+
setLocalValue({ h: Math.round(hsl.h), s: Math.round(hsl.s * 100), l: Math.round(hsl.l * 100) });
|
|
34
|
+
}
|
|
35
|
+
}, [hue, saturation, value, mode, getRgb]);
|
|
36
|
+
|
|
37
|
+
const handleHexChange = (e) => {
|
|
38
|
+
const val = e.target.value;
|
|
39
|
+
setLocalValue({ ...localValue, hex: val });
|
|
40
|
+
if (/^#?([0-9A-F]{3}|[0-9A-F]{6})$/i.test(val)) {
|
|
41
|
+
const rgb = hexToRgb(val);
|
|
42
|
+
if (rgb) {
|
|
43
|
+
const hsv = rgbToHsv(rgb.r, rgb.g, rgb.b);
|
|
44
|
+
onChange({ ...hsv, a: alpha });
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const handleRgbChange = (key, val) => {
|
|
50
|
+
const newValue = { ...localValue, [key]: val };
|
|
51
|
+
setLocalValue(newValue);
|
|
52
|
+
const r = parseInt(newValue.r, 10);
|
|
53
|
+
const g = parseInt(newValue.g, 10);
|
|
54
|
+
const b = parseInt(newValue.b, 10);
|
|
55
|
+
if (!isNaN(r) && !isNaN(g) && !isNaN(b)) {
|
|
56
|
+
const hsv = rgbToHsv(Math.min(255, Math.max(0, r)), Math.min(255, Math.max(0, g)), Math.min(255, Math.max(0, b)));
|
|
57
|
+
onChange({ ...hsv, a: alpha });
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const handleHslChange = (key, val) => {
|
|
62
|
+
const newValue = { ...localValue, [key]: val };
|
|
63
|
+
setLocalValue(newValue);
|
|
64
|
+
const h = parseInt(newValue.h, 10);
|
|
65
|
+
const s = parseInt(newValue.s, 10);
|
|
66
|
+
const l = parseInt(newValue.l, 10);
|
|
67
|
+
if (!isNaN(h) && !isNaN(s) && !isNaN(l)) {
|
|
68
|
+
const rgb = hslToRgb(Math.min(360, Math.max(0, h)), Math.min(100, Math.max(0, s)) / 100, Math.min(100, Math.max(0, l)) / 100);
|
|
69
|
+
const hsv = rgbToHsv(rgb.r, rgb.g, rgb.b);
|
|
70
|
+
onChange({ ...hsv, a: alpha });
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const openEyeDropper = async () => {
|
|
75
|
+
if (!window.EyeDropper) return;
|
|
76
|
+
const eyeDropper = new window.EyeDropper();
|
|
77
|
+
try {
|
|
78
|
+
const result = await eyeDropper.open();
|
|
79
|
+
const rgb = hexToRgb(result.sRGBHex);
|
|
80
|
+
if (rgb) {
|
|
81
|
+
const hsv = rgbToHsv(rgb.r, rgb.g, rgb.b);
|
|
82
|
+
onChange({ ...hsv, a: alpha });
|
|
83
|
+
}
|
|
84
|
+
} catch (e) {
|
|
85
|
+
// User canceled
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
return (
|
|
90
|
+
<div className="color-input-container">
|
|
91
|
+
<div className="color-input-row">
|
|
92
|
+
<div className="mode-selector" ref={dropdownRef} onClick={() => setShowDropdown(!showDropdown)}>
|
|
93
|
+
<span>{mode}</span>
|
|
94
|
+
<span className="arrow" style={{ fontSize: '10px', marginLeft: '4px', transform: showDropdown ? 'rotate(180deg)' : 'rotate(0deg)', display: 'inline-block', transition: 'transform 0.2s' }}>▼</span>
|
|
95
|
+
{showDropdown && (
|
|
96
|
+
<div className="mode-dropdown">
|
|
97
|
+
<div className="mode-option" onClick={(e) => { e.stopPropagation(); setMode('HEX'); setShowDropdown(false); }}>HEX</div>
|
|
98
|
+
<div className="mode-option" onClick={(e) => { e.stopPropagation(); setMode('RGB'); setShowDropdown(false); }}>RGB</div>
|
|
99
|
+
<div className="mode-option" onClick={(e) => { e.stopPropagation(); setMode('HSL'); setShowDropdown(false); }}>HSL</div>
|
|
100
|
+
</div>
|
|
101
|
+
)}
|
|
102
|
+
</div>
|
|
103
|
+
|
|
104
|
+
{window.EyeDropper && (
|
|
105
|
+
<div className="eyedropper-icon" onClick={openEyeDropper} title="Pick Color">
|
|
106
|
+
<svg viewBox="0 0 24 24" width="16" height="16">
|
|
107
|
+
<path fill="currentColor" d="M17.5,1.5c-0.8,0-1.5,0.3-2.1,0.9l-8,8c-0.6,0.6-0.9,1.3-0.9,2.1v2.5l-4,4l1.5,1.5l4-4h2.5c0.8,0,1.5-0.3,2.1-0.9l8-8c0.6-0.6,0.9-1.3,0.9-2.1s-0.3-1.5-0.9-2.1S18.3,1.5,17.5,1.5z M17.5,10l-8-8l8,8z M6.5,12.5v2.5h-2.5L2,17l2,2l2-2v-2.5h2.5l8-8L10,5L6.5,8.5V12.5z"/>
|
|
108
|
+
<path d="M19.3 8.9L15.1 4.7 17.5 2.3c.4-.4 1-.4 1.4 0l2.7 2.7c.4.4.4 1 0 1.4L19.3 8.9zM13.7 6.1L4 15.8V20h4.2l9.7-9.7-4.2-4.2z" fill="currentColor"/>
|
|
109
|
+
</svg>
|
|
110
|
+
</div>
|
|
111
|
+
)}
|
|
112
|
+
</div>
|
|
113
|
+
|
|
114
|
+
<div className="color-input-fields">
|
|
115
|
+
{mode === 'HEX' && (
|
|
116
|
+
<input className="color-input" value={localValue.hex || ''} onChange={handleHexChange} />
|
|
117
|
+
)}
|
|
118
|
+
{mode === 'RGB' && (
|
|
119
|
+
<>
|
|
120
|
+
<input className="color-input" type="number" placeholder="R" value={localValue.r !== undefined ? localValue.r : ''} onChange={(e) => handleRgbChange('r', e.target.value)} />
|
|
121
|
+
<input className="color-input" type="number" placeholder="G" value={localValue.g !== undefined ? localValue.g : ''} onChange={(e) => handleRgbChange('g', e.target.value)} />
|
|
122
|
+
<input className="color-input" type="number" placeholder="B" value={localValue.b !== undefined ? localValue.b : ''} onChange={(e) => handleRgbChange('b', e.target.value)} />
|
|
123
|
+
</>
|
|
124
|
+
)}
|
|
125
|
+
{mode === 'HSL' && (
|
|
126
|
+
<>
|
|
127
|
+
<input className="color-input" type="number" placeholder="H" value={localValue.h !== undefined ? localValue.h : ''} onChange={(e) => handleHslChange('h', e.target.value)} />
|
|
128
|
+
<input className="color-input" type="number" placeholder="S" value={localValue.s !== undefined ? localValue.s : ''} onChange={(e) => handleHslChange('s', e.target.value)} />
|
|
129
|
+
<input className="color-input" type="number" placeholder="L" value={localValue.l !== undefined ? localValue.l : ''} onChange={(e) => handleHslChange('l', e.target.value)} />
|
|
130
|
+
</>
|
|
131
|
+
)}
|
|
132
|
+
<div className="color-input alpha-display">
|
|
133
|
+
{Math.round(alpha * 100)}%
|
|
134
|
+
</div>
|
|
135
|
+
</div>
|
|
136
|
+
</div>
|
|
137
|
+
);
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
export default ColorInput;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
|
|
2
|
+
import React, { useRef, useState, useEffect, useCallback } from 'react';
|
|
3
|
+
import { clamp } from './utils/color';
|
|
4
|
+
|
|
5
|
+
const Slider = ({ value, max, onChange, className, style, children }) => {
|
|
6
|
+
const containerRef = useRef(null);
|
|
7
|
+
const [isDragging, setIsDragging] = useState(false);
|
|
8
|
+
|
|
9
|
+
const handleChange = useCallback((e) => {
|
|
10
|
+
if (!containerRef.current) return;
|
|
11
|
+
const { width, left } = containerRef.current.getBoundingClientRect();
|
|
12
|
+
const x = clamp(e.clientX - left, 0, width);
|
|
13
|
+
const newValue = (x / width) * max;
|
|
14
|
+
onChange(newValue);
|
|
15
|
+
}, [max, onChange]);
|
|
16
|
+
|
|
17
|
+
const handleMouseDown = (e) => {
|
|
18
|
+
setIsDragging(true);
|
|
19
|
+
handleChange(e);
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const handleMouseUp = useCallback(() => {
|
|
23
|
+
setIsDragging(false);
|
|
24
|
+
}, []);
|
|
25
|
+
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
const handleMouseMove = (e) => {
|
|
28
|
+
if (isDragging) {
|
|
29
|
+
handleChange(e);
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
if (isDragging) {
|
|
34
|
+
window.addEventListener('mousemove', handleMouseMove);
|
|
35
|
+
window.addEventListener('mouseup', handleMouseUp);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return () => {
|
|
39
|
+
window.removeEventListener('mousemove', handleMouseMove);
|
|
40
|
+
window.removeEventListener('mouseup', handleMouseUp);
|
|
41
|
+
};
|
|
42
|
+
}, [isDragging, handleChange, handleMouseUp]);
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<div
|
|
46
|
+
ref={containerRef}
|
|
47
|
+
className={`color-slider ${className || ''}`}
|
|
48
|
+
style={style}
|
|
49
|
+
onMouseDown={handleMouseDown}
|
|
50
|
+
>
|
|
51
|
+
<div
|
|
52
|
+
className="color-slider-handler"
|
|
53
|
+
style={{
|
|
54
|
+
left: `${(value / max) * 100}%`,
|
|
55
|
+
}}
|
|
56
|
+
/>
|
|
57
|
+
{children}
|
|
58
|
+
</div>
|
|
59
|
+
);
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export const HueSlider = ({ hue, onChange }) => {
|
|
63
|
+
return (
|
|
64
|
+
<Slider
|
|
65
|
+
value={hue}
|
|
66
|
+
max={360}
|
|
67
|
+
onChange={onChange}
|
|
68
|
+
className="hue-slider"
|
|
69
|
+
/>
|
|
70
|
+
);
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export const AlphaSlider = ({ alpha, color, onChange }) => {
|
|
74
|
+
const { r, g, b } = color;
|
|
75
|
+
return (
|
|
76
|
+
<Slider
|
|
77
|
+
value={alpha}
|
|
78
|
+
max={1}
|
|
79
|
+
onChange={onChange}
|
|
80
|
+
className="alpha-slider-bg"
|
|
81
|
+
style={{
|
|
82
|
+
background: `linear-gradient(to right, rgba(${r},${g},${b},0) 0%, rgba(${r},${g},${b},1) 100%)`
|
|
83
|
+
}}
|
|
84
|
+
/>
|
|
85
|
+
);
|
|
86
|
+
};
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
|
|
2
|
+
.color-picker-panel {
|
|
3
|
+
width: 200px;
|
|
4
|
+
padding: 8px;
|
|
5
|
+
background: #fff;
|
|
6
|
+
border-radius: 4px;
|
|
7
|
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
|
8
|
+
user-select: none;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/* ColorBoard */
|
|
12
|
+
.color-board {
|
|
13
|
+
position: relative;
|
|
14
|
+
width: 100%;
|
|
15
|
+
height: 150px;
|
|
16
|
+
border-radius: 2px;
|
|
17
|
+
cursor: pointer;
|
|
18
|
+
overflow: hidden;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.color-board-white {
|
|
22
|
+
position: absolute;
|
|
23
|
+
top: 0;
|
|
24
|
+
left: 0;
|
|
25
|
+
right: 0;
|
|
26
|
+
bottom: 0;
|
|
27
|
+
background: linear-gradient(to right, #fff, rgba(255, 255, 255, 0));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.color-board-black {
|
|
31
|
+
position: absolute;
|
|
32
|
+
top: 0;
|
|
33
|
+
left: 0;
|
|
34
|
+
right: 0;
|
|
35
|
+
bottom: 0;
|
|
36
|
+
background: linear-gradient(to top, #000, rgba(0, 0, 0, 0));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.color-board-handler {
|
|
40
|
+
position: absolute;
|
|
41
|
+
width: 10px;
|
|
42
|
+
height: 10px;
|
|
43
|
+
border: 2px solid #fff;
|
|
44
|
+
border-radius: 50%;
|
|
45
|
+
transform: translate(-50%, -50%);
|
|
46
|
+
box-shadow: 0 0 2px rgba(0, 0, 0, 0.5);
|
|
47
|
+
pointer-events: none;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/* Sliders */
|
|
51
|
+
.color-slider {
|
|
52
|
+
position: relative;
|
|
53
|
+
width: 100%;
|
|
54
|
+
height: 10px;
|
|
55
|
+
margin-top: 8px;
|
|
56
|
+
border-radius: 5px;
|
|
57
|
+
cursor: pointer;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.color-slider-handler {
|
|
61
|
+
position: absolute;
|
|
62
|
+
top: 50%;
|
|
63
|
+
width: 12px;
|
|
64
|
+
height: 12px;
|
|
65
|
+
background: #fff;
|
|
66
|
+
border-radius: 50%;
|
|
67
|
+
transform: translate(-50%, -50%);
|
|
68
|
+
box-shadow: 0 0 2px rgba(0, 0, 0, 0.5);
|
|
69
|
+
pointer-events: none;
|
|
70
|
+
border: 1px solid rgba(0,0,0,0.1);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.hue-slider {
|
|
74
|
+
background: linear-gradient(to right, red 0%, #ff0 17%, lime 33%, cyan 50%, blue 66%, magenta 83%, red 100%);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.alpha-slider-bg {
|
|
78
|
+
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAGUlEQVQYV2M4gwH+YwCzekwMjDjZ1IxjJwApDkHN9Fi7xwAAAABJRU5ErkJggg==');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/* Inputs */
|
|
82
|
+
.color-input-container {
|
|
83
|
+
margin-top: 8px;
|
|
84
|
+
display: flex;
|
|
85
|
+
flex-direction: column;
|
|
86
|
+
gap: 8px;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.color-input-row {
|
|
90
|
+
display: flex;
|
|
91
|
+
justify-content: space-between;
|
|
92
|
+
align-items: center;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.mode-selector {
|
|
96
|
+
position: relative;
|
|
97
|
+
cursor: pointer;
|
|
98
|
+
font-size: 12px;
|
|
99
|
+
color: #333;
|
|
100
|
+
padding: 2px 4px;
|
|
101
|
+
border-radius: 2px;
|
|
102
|
+
user-select: none;
|
|
103
|
+
display: flex;
|
|
104
|
+
align-items: center;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
.mode-selector:hover {
|
|
108
|
+
background-color: #f5f5f5;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.mode-dropdown {
|
|
112
|
+
position: absolute;
|
|
113
|
+
top: 100%;
|
|
114
|
+
left: 0;
|
|
115
|
+
background: #fff;
|
|
116
|
+
border: 1px solid #d9d9d9;
|
|
117
|
+
border-radius: 2px;
|
|
118
|
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
|
119
|
+
z-index: 10;
|
|
120
|
+
min-width: 60px;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.mode-option {
|
|
124
|
+
padding: 4px 8px;
|
|
125
|
+
cursor: pointer;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.mode-option:hover {
|
|
129
|
+
background: #e6f7ff;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.eyedropper-icon {
|
|
133
|
+
cursor: pointer;
|
|
134
|
+
padding: 2px;
|
|
135
|
+
border-radius: 2px;
|
|
136
|
+
color: #666;
|
|
137
|
+
display: flex;
|
|
138
|
+
align-items: center;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
.eyedropper-icon:hover {
|
|
142
|
+
background-color: #f5f5f5;
|
|
143
|
+
color: #333;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
.color-input-fields {
|
|
147
|
+
display: flex;
|
|
148
|
+
gap: 4px;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.color-input {
|
|
152
|
+
width: 100%;
|
|
153
|
+
padding: 4px;
|
|
154
|
+
border: 1px solid #d9d9d9;
|
|
155
|
+
border-radius: 2px;
|
|
156
|
+
font-size: 12px;
|
|
157
|
+
box-sizing: border-box;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/* Chrome, Safari, Edge, Opera - hide spin buttons */
|
|
161
|
+
.color-input::-webkit-outer-spin-button,
|
|
162
|
+
.color-input::-webkit-inner-spin-button {
|
|
163
|
+
-webkit-appearance: none;
|
|
164
|
+
margin: 0;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/* Firefox */
|
|
168
|
+
.color-input[type=number] {
|
|
169
|
+
-moz-appearance: textfield;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
.alpha-display {
|
|
173
|
+
width: 40px;
|
|
174
|
+
text-align: center;
|
|
175
|
+
background-color: #f5f5f5;
|
|
176
|
+
color: #666;
|
|
177
|
+
border: 1px solid #d9d9d9;
|
|
178
|
+
display: flex;
|
|
179
|
+
align-items: center;
|
|
180
|
+
justify-content: center;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/* Trigger */
|
|
184
|
+
.color-picker-trigger {
|
|
185
|
+
display: inline-block;
|
|
186
|
+
padding: 4px;
|
|
187
|
+
border: 1px solid #d9d9d9;
|
|
188
|
+
border-radius: 4px;
|
|
189
|
+
cursor: pointer;
|
|
190
|
+
background: #fff;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
.color-block {
|
|
194
|
+
width: 20px;
|
|
195
|
+
height: 20px;
|
|
196
|
+
border-radius: 2px;
|
|
197
|
+
border: 1px solid rgba(0, 0, 0, 0.1);
|
|
198
|
+
}
|
package/src/lib/index.js
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
|
|
2
|
+
import React, { useState, useRef, useEffect } from 'react';
|
|
3
|
+
import ColorBoard from './ColorBoard';
|
|
4
|
+
import { HueSlider, AlphaSlider } from './Sliders';
|
|
5
|
+
import ColorInput from './ColorInput';
|
|
6
|
+
import { hsvToRgb, rgbToHex, parseColor } from './utils/color';
|
|
7
|
+
import './index.css';
|
|
8
|
+
|
|
9
|
+
const ColorPicker = ({ value, defaultValue, onChange }) => {
|
|
10
|
+
// Initialize state
|
|
11
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
12
|
+
|
|
13
|
+
// Internal state for color components
|
|
14
|
+
const [color, setColor] = useState(() => {
|
|
15
|
+
const initColor = value || defaultValue || '#1677ff';
|
|
16
|
+
return parseColor(initColor);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const containerRef = useRef(null);
|
|
20
|
+
|
|
21
|
+
// Sync with prop value if controlled
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
if (value !== undefined) {
|
|
24
|
+
setColor(parseColor(value));
|
|
25
|
+
}
|
|
26
|
+
}, [value]);
|
|
27
|
+
|
|
28
|
+
const handleChange = (newColor) => {
|
|
29
|
+
const nextColor = { ...color, ...newColor };
|
|
30
|
+
setColor(nextColor);
|
|
31
|
+
|
|
32
|
+
if (onChange) {
|
|
33
|
+
const rgb = hsvToRgb(nextColor.h, nextColor.s, nextColor.v);
|
|
34
|
+
const hex = rgbToHex(rgb.r, rgb.g, rgb.b);
|
|
35
|
+
// Return a rich object or just hex string?
|
|
36
|
+
// AntD returns an object, but simple string is often easier.
|
|
37
|
+
// Let's return the hex string and the object as second arg just in case.
|
|
38
|
+
onChange(hex, { ...rgb, a: nextColor.a });
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// Click outside to close
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
const handleClickOutside = (event) => {
|
|
45
|
+
if (containerRef.current && !containerRef.current.contains(event.target)) {
|
|
46
|
+
setIsOpen(false);
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
if (isOpen) {
|
|
51
|
+
document.addEventListener('mousedown', handleClickOutside);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return () => {
|
|
55
|
+
document.removeEventListener('mousedown', handleClickOutside);
|
|
56
|
+
};
|
|
57
|
+
}, [isOpen]);
|
|
58
|
+
|
|
59
|
+
const rgb = hsvToRgb(color.h, color.s, color.v);
|
|
60
|
+
const displayColor = `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${color.a})`;
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<div className="color-picker-container" ref={containerRef} style={{ position: 'relative', display: 'inline-block' }}>
|
|
64
|
+
<div
|
|
65
|
+
className="color-picker-trigger"
|
|
66
|
+
onClick={() => setIsOpen(!isOpen)}
|
|
67
|
+
>
|
|
68
|
+
<div
|
|
69
|
+
className="color-block"
|
|
70
|
+
style={{ backgroundColor: displayColor }}
|
|
71
|
+
/>
|
|
72
|
+
</div>
|
|
73
|
+
|
|
74
|
+
{isOpen && (
|
|
75
|
+
<div
|
|
76
|
+
className="color-picker-panel"
|
|
77
|
+
style={{
|
|
78
|
+
position: 'absolute',
|
|
79
|
+
top: '100%',
|
|
80
|
+
left: 0,
|
|
81
|
+
zIndex: 1000,
|
|
82
|
+
marginTop: 4
|
|
83
|
+
}}
|
|
84
|
+
>
|
|
85
|
+
<ColorBoard
|
|
86
|
+
hue={color.h}
|
|
87
|
+
saturation={color.s}
|
|
88
|
+
value={color.v}
|
|
89
|
+
onChange={(s, v) => handleChange({ s, v })}
|
|
90
|
+
/>
|
|
91
|
+
<HueSlider
|
|
92
|
+
hue={color.h}
|
|
93
|
+
onChange={(h) => handleChange({ h })}
|
|
94
|
+
/>
|
|
95
|
+
<AlphaSlider
|
|
96
|
+
alpha={color.a}
|
|
97
|
+
color={rgb}
|
|
98
|
+
onChange={(a) => handleChange({ a })}
|
|
99
|
+
/>
|
|
100
|
+
<ColorInput
|
|
101
|
+
hue={color.h}
|
|
102
|
+
saturation={color.s}
|
|
103
|
+
value={color.v}
|
|
104
|
+
alpha={color.a}
|
|
105
|
+
onChange={(newHsv) => handleChange(newHsv)}
|
|
106
|
+
/>
|
|
107
|
+
</div>
|
|
108
|
+
)}
|
|
109
|
+
</div>
|
|
110
|
+
);
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
export default ColorPicker;
|