gdp-color-picker 1.0.0 → 1.1.1

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/src/lib/index.js CHANGED
@@ -1,85 +1,197 @@
1
+ import React, { useState, useRef, useEffect } from "react";
2
+ import ReactDOM from "react-dom";
3
+ import ColorBoard from "./ColorBoard";
4
+ import { HueSlider, AlphaSlider } from "./Sliders";
5
+ import ColorInput from "./ColorInput";
6
+ import {
7
+ hsvToRgb,
8
+ rgbToHex,
9
+ parseColor,
10
+ hexToRgb,
11
+ rgbToHsv,
12
+ } from "./utils/color";
13
+ import "./index.css";
1
14
 
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';
15
+ const DEFAULT_PRESETS = [
16
+ "#E0B5AE",
17
+ "#B2BBDE",
18
+ "#F2DDB3",
19
+ "#E0E5C5",
20
+ "#BBD0C3",
21
+ "#B5D1E3",
22
+ "#FFFFFF",
23
+ "#D25536",
24
+ "#EFA154",
25
+ "#F7DA7E",
26
+ "#5FA67F",
27
+ "#A2829D",
28
+ "#D390A2",
29
+ "#000000",
30
+ "#0099A4",
31
+ "#EABA00",
32
+ "#C9462C",
33
+ "#3171AC",
34
+ "#783780",
35
+ "#1E80EA",
36
+ "#2AAC3B",
37
+ "#B63455",
38
+ "#81532F",
39
+ "#AA9A5C",
40
+ "#828281",
41
+ "#055753",
42
+ "#8F262B",
43
+ "#F0D8D0",
44
+ ];
8
45
 
9
- const ColorPicker = ({ value, defaultValue, onChange }) => {
10
- // Initialize state
46
+ const ColorPicker = ({
47
+ value,
48
+ defaultValue,
49
+ onChange,
50
+ showLabel = true,
51
+ label = "Color",
52
+ showArrow = true,
53
+ showInput = true,
54
+ showColorBoard = true,
55
+ presets: customPresets,
56
+ }) => {
11
57
  const [isOpen, setIsOpen] = useState(false);
12
-
13
- // Internal state for color components
58
+ const [isExpanded, setIsExpanded] = useState(false);
59
+ const [recentColors, setRecentColors] = useState([]);
60
+ const presets = customPresets || DEFAULT_PRESETS;
61
+
14
62
  const [color, setColor] = useState(() => {
15
- const initColor = value || defaultValue || '#1677ff';
63
+ const initColor = value || defaultValue || "#1677ff";
16
64
  return parseColor(initColor);
17
65
  });
18
66
 
67
+ const [inputValue, setInputValue] = useState(() => {
68
+ const rgb = hsvToRgb(color.h, color.s, color.v);
69
+ return rgbToHex(rgb.r, rgb.g, rgb.b).toUpperCase();
70
+ });
71
+
19
72
  const containerRef = useRef(null);
73
+ const triggerRef = useRef(null);
74
+ const [panelStyle, setPanelStyle] = useState({});
20
75
 
21
- // Sync with prop value if controlled
22
76
  useEffect(() => {
23
77
  if (value !== undefined) {
24
- setColor(parseColor(value));
78
+ const newColor = parseColor(value);
79
+ setColor(newColor);
80
+ const rgb = hsvToRgb(newColor.h, newColor.s, newColor.v);
81
+ setInputValue(rgbToHex(rgb.r, rgb.g, rgb.b).toUpperCase());
25
82
  }
26
83
  }, [value]);
27
84
 
28
85
  const handleChange = (newColor) => {
29
86
  const nextColor = { ...color, ...newColor };
30
87
  setColor(nextColor);
31
-
88
+
89
+ const rgb = hsvToRgb(nextColor.h, nextColor.s, nextColor.v);
90
+ const hex = rgbToHex(rgb.r, rgb.g, rgb.b).toUpperCase();
91
+ setInputValue(hex);
92
+
32
93
  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
94
  onChange(hex, { ...rgb, a: nextColor.a });
39
95
  }
40
96
  };
41
97
 
42
- // Click outside to close
98
+ const handleInputChange = (e) => {
99
+ const val = e.target.value;
100
+ setInputValue(val);
101
+
102
+ if (/^#?([0-9A-F]{3}|[0-9A-F]{6})$/i.test(val)) {
103
+ const rgb = hexToRgb(val);
104
+ if (rgb) {
105
+ const hsv = rgbToHsv(rgb.r, rgb.g, rgb.b);
106
+ handleChange({ ...hsv, a: color.a });
107
+ }
108
+ }
109
+ };
110
+
111
+ useEffect(() => {
112
+ if (
113
+ !isOpen ||
114
+ !triggerRef.current ||
115
+ typeof window === "undefined" ||
116
+ typeof document === "undefined"
117
+ ) {
118
+ return;
119
+ }
120
+
121
+ const triggerRect = triggerRef.current.getBoundingClientRect();
122
+ const top = triggerRect.bottom + 4 + window.scrollY;
123
+ const left = triggerRect.left + window.scrollX;
124
+
125
+ setPanelStyle({
126
+ position: "absolute",
127
+ top,
128
+ left,
129
+ zIndex: 9999,
130
+ });
131
+ }, [isOpen]);
132
+
133
+ const handleEyeDropper = async () => {
134
+ if (!window.EyeDropper) {
135
+ alert("当前浏览器不支持屏幕取色功能");
136
+ return;
137
+ }
138
+
139
+ try {
140
+ const eyeDropper = new window.EyeDropper();
141
+ const result = await eyeDropper.open();
142
+ const { sRGBHex } = result;
143
+ const rgb = hexToRgb(sRGBHex);
144
+ if (rgb) {
145
+ const hsv = rgbToHsv(rgb.r, rgb.g, rgb.b);
146
+ handleChange({ ...hsv, a: 1 });
147
+ }
148
+ } catch (e) {
149
+ // 用户取消取色或发生错误
150
+ console.log("EyeDropper failed or canceled:", e);
151
+ }
152
+ };
153
+
43
154
  useEffect(() => {
44
155
  const handleClickOutside = (event) => {
45
- if (containerRef.current && !containerRef.current.contains(event.target)) {
156
+ if (
157
+ containerRef.current &&
158
+ !containerRef.current.contains(event.target)
159
+ ) {
46
160
  setIsOpen(false);
47
161
  }
48
162
  };
49
163
 
50
164
  if (isOpen) {
51
- document.addEventListener('mousedown', handleClickOutside);
165
+ document.addEventListener("mousedown", handleClickOutside);
166
+ } else {
167
+ const rgb = hsvToRgb(color.h, color.s, color.v);
168
+ const currentHex = rgbToHex(rgb.r, rgb.g, rgb.b).toUpperCase();
169
+ setRecentColors((prev) => {
170
+ if (prev.includes(currentHex)) return prev;
171
+ return [currentHex, ...prev].slice(0, 10);
172
+ });
52
173
  }
53
-
174
+
54
175
  return () => {
55
- document.removeEventListener('mousedown', handleClickOutside);
176
+ document.removeEventListener("mousedown", handleClickOutside);
56
177
  };
57
- }, [isOpen]);
178
+ }, [isOpen, color]);
58
179
 
59
180
  const rgb = hsvToRgb(color.h, color.s, color.v);
60
181
  const displayColor = `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${color.a})`;
182
+ const initialLimit = 14;
183
+ const visiblePresets = isExpanded ? presets : presets.slice(0, initialLimit);
184
+ const showExpand = presets.length > initialLimit;
61
185
 
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
186
+ const panel = isOpen ? (
187
+ <div className="color-picker-panel" style={panelStyle}>
188
+ {showColorBoard && (
189
+ <div
190
+ className="color-picker-board-wrapper"
191
+ style={{
192
+ marginTop: "10px",
193
+ paddingTop: "10px",
194
+ borderTop: "1px solid #eee",
83
195
  }}
84
196
  >
85
197
  <ColorBoard
@@ -88,10 +200,7 @@ const ColorPicker = ({ value, defaultValue, onChange }) => {
88
200
  value={color.v}
89
201
  onChange={(s, v) => handleChange({ s, v })}
90
202
  />
91
- <HueSlider
92
- hue={color.h}
93
- onChange={(h) => handleChange({ h })}
94
- />
203
+ <HueSlider hue={color.h} onChange={(h) => handleChange({ h })} />
95
204
  <AlphaSlider
96
205
  alpha={color.a}
97
206
  color={rgb}
@@ -106,6 +215,222 @@ const ColorPicker = ({ value, defaultValue, onChange }) => {
106
215
  />
107
216
  </div>
108
217
  )}
218
+ <div className="color-picker-presets">
219
+ <div className="palette-section-title" style={{ marginTop: 8 }}>
220
+ <span>Recommended</span>
221
+ <span
222
+ className="color-picker-icon-right"
223
+ onClick={handleEyeDropper}
224
+ title="屏幕取色"
225
+ >
226
+ <svg
227
+ width="16"
228
+ height="16"
229
+ viewBox="0 0 16 16"
230
+ xmlns="http://www.w3.org/2000/svg"
231
+ >
232
+ <defs>
233
+ <rect id="path-1" x="0" y="0" width="16" height="16" />
234
+ </defs>
235
+ <g stroke="none" strokeWidth="1" fill="none" fillRule="evenodd">
236
+ <g transform="translate(-920.000000, -594.000000)">
237
+ <g transform="translate(720.000000, 418.000000)">
238
+ <g transform="translate(200.000000, 176.000000)">
239
+ <mask id="mask-2" fill="white">
240
+ <use xlinkHref="#path-1" />
241
+ </mask>
242
+ <g />
243
+ <g
244
+ mask="url(#mask-2)"
245
+ fill="#000000"
246
+ fillOpacity="0.85"
247
+ fillRule="nonzero"
248
+ >
249
+ <g transform="translate(8.106532, 8.106532) rotate(-315.000000) translate(-8.106532, -8.106532) translate(-0.266667, -0.266667)">
250
+ <path d="M5.67238576,2.96585378 L6.99478644,4.28811977 L7.03978941,4.24867364 C7.66833128,3.72977631 8.60030981,3.76436946 9.18839345,4.3524531 L9.94264069,5.10670034 C10.1509203,5.31497996 10.1509203,5.65266795 9.94264069,5.86094757 L9.18778644,6.61411977 L13.7138769,11.1406782 C14.4428555,11.8696569 14.4428555,13.0515648 13.7138769,13.7805435 C12.9848982,14.5095222 11.8029902,14.5095222 11.0740115,13.7805435 L6.54778644,9.25411977 L5.7942809,10.0093074 C5.58600128,10.217587 5.24831329,10.217587 5.04003367,10.0093074 L4.28578644,9.25506012 C3.66094757,8.63022125 3.66094757,7.61715729 4.28578644,6.99231842 L4.35178644,6.92511977 L3.03252045,5.6057191 C2.30354177,4.87674042 2.30354177,3.69483246 3.03252045,2.96585378 C3.76149912,2.2368751 4.94340708,2.2368751 5.67238576,2.96585378 Z M8.43414622,7.36944204 L7.86778644,7.93411977 L7.30277537,8.50081289 L11.8282588,13.0262963 C12.1295204,13.3275579 12.6112769,13.3383172 12.925431,13.0585743 L12.9596296,13.0262963 C13.2720491,12.7138769 13.2720491,12.2073449 12.9596296,11.8949254 L8.43414622,7.36944204 Z M7.70907857,5.07958956 L7.67989899,5.10670034 L5.04003367,7.74656565 C4.83175405,7.95484528 4.83175405,8.29253326 5.04003367,8.50081289 L5.41715729,8.8779365 L8.81126984,5.48382395 L8.43414622,5.10670034 C8.23533385,4.90788797 7.91861014,4.89885105 7.70907857,5.07958956 Z M3.82096628,3.68782298 L3.78676768,3.72010101 C3.47434825,4.03252045 3.47434825,4.53905243 3.78676768,4.85147186 L5.10670034,6.17140452 L6.23807119,5.04003367 L4.91813853,3.72010101 C4.61687693,3.41883942 4.13512038,3.40808007 3.82096628,3.68782298 Z" transform="translate(8.373199, 8.373199) rotate(-315.000000) translate(-8.373199, -8.373199)" />
251
+ </g>
252
+ </g>
253
+ </g>
254
+ </g>
255
+ </g>
256
+ </g>
257
+ </svg>
258
+ </span>
259
+ </div>
260
+ <div className={`presets-grid modern`}>
261
+ {presets.map((preset, idx) => (
262
+ <div
263
+ key={idx}
264
+ className="preset-item preset-item-inner"
265
+ style={{ backgroundColor: preset }}
266
+ onClick={() => {
267
+ const rgbValue = hexToRgb(preset);
268
+ if (rgbValue) {
269
+ const hsv = rgbToHsv(rgbValue.r, rgbValue.g, rgbValue.b);
270
+ handleChange({ ...hsv, a: 1 });
271
+ }
272
+ }}
273
+ title={preset}
274
+ />
275
+ ))}
276
+ </div>
277
+ {/* {showExpand && (
278
+ <div
279
+ className="presets-collapse"
280
+ onClick={() => setIsExpanded(!isExpanded)}
281
+ >
282
+ {isExpanded ? "Less" : "More"}
283
+ <span className={`collapse-arrow ${isExpanded ? "expanded" : ""}`}>
284
+
285
+ </span>
286
+ </div>
287
+ )} */}
288
+
289
+ {recentColors.length > 0 && (
290
+ <>
291
+ <div className="palette-section-title">Recent</div>
292
+ <div className="presets-grid recent">
293
+ {recentColors.map((recentColor, idx) => (
294
+ <div
295
+ key={idx}
296
+ className="preset-item"
297
+ style={{ backgroundColor: recentColor }}
298
+ onClick={() => {
299
+ const rgbValue = hexToRgb(recentColor);
300
+ if (rgbValue) {
301
+ const hsv = rgbToHsv(
302
+ rgbValue.r,
303
+ rgbValue.g,
304
+ rgbValue.b
305
+ );
306
+ handleChange({ ...hsv, a: 1 });
307
+ }
308
+ }}
309
+ title={recentColor}
310
+ />
311
+ ))}
312
+ </div>
313
+ </>
314
+ )}
315
+ </div>
316
+ </div>
317
+ ) : null;
318
+
319
+ return (
320
+ <div className="color-picker-container" ref={containerRef}>
321
+ <div className="color-picker-main-row">
322
+ <div className="color-picker-tooltip-trigger">
323
+ {showLabel && <span className="color-picker-label">{label}</span>}
324
+
325
+ <div
326
+ className={`color-picker-trigger ${isOpen ? "is-open" : ""}`}
327
+ onClick={() => setIsOpen(!isOpen)}
328
+ ref={triggerRef}
329
+ >
330
+ <div
331
+ className="color-block"
332
+ style={{ backgroundColor: displayColor }}
333
+ />
334
+ {showArrow && (
335
+ <span className={`color-picker-arrow ${isOpen ? "open" : ""}`}>
336
+ <svg
337
+ width="16"
338
+ height="16"
339
+ viewBox="0 0 16 16"
340
+ xmlns="http://www.w3.org/2000/svg"
341
+ >
342
+ <g stroke="none" strokeWidth="1" fill="none" fillRule="evenodd">
343
+ <g
344
+ transform="translate(-1348.000000, -88.000000)"
345
+ fillRule="nonzero"
346
+ >
347
+ <g transform="translate(0.000000, 60.000000)">
348
+ <g transform="translate(584.000000, 20.000000)">
349
+ <g transform="translate(728.000000, 4.000000)">
350
+ <g transform="translate(44.000000, 12.000000) rotate(-360.000000) translate(-44.000000, -12.000000) translate(36.000000, 4.000000)">
351
+ <rect
352
+ fill="#000000"
353
+ opacity="0"
354
+ x="0"
355
+ y="0"
356
+ width="16"
357
+ height="16"
358
+ />
359
+ <path
360
+ d="M13.0750341,5.34788541 C12.9440655,5.21691678 12.7257845,5.21691678 12.5948158,5.34788541 L8.01091405,9.93178717 L3.40518417,5.34788541 C3.27421555,5.21691678 3.05593452,5.21691678 2.92496589,5.34788541 C2.79399727,5.47885403 2.79399727,5.69713506 2.92496589,5.82810369 L7.77080491,10.6521146 C7.90177353,10.7830832 8.12005456,10.7830832 8.25102319,10.6521146 L13.0750341,5.82810369 C13.2060027,5.69713506 13.2060027,5.47885403 13.0750341,5.34788541 Z"
361
+ stroke="#FFFFFF"
362
+ />
363
+ </g>
364
+ </g>
365
+ </g>
366
+ </g>
367
+ </g>
368
+ </g>
369
+ </svg>
370
+ </span>
371
+ )}
372
+ <span className="tooltip-text">Color Picker</span>
373
+ </div>
374
+ </div>
375
+
376
+ {showInput && (
377
+ <div className="color-picker-input-wrapper">
378
+ <input
379
+ type="text"
380
+ value={inputValue}
381
+ onChange={handleInputChange}
382
+ placeholder="Please input color"
383
+ />
384
+ {inputValue && (
385
+ <span
386
+ className="input-clear-icon"
387
+ onClick={() => setInputValue("")}
388
+ >
389
+ <img
390
+ src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAOCAYAAAAfSC3RAAAA8klEQVQ4T51SwVHDQAzcdQOETugAqISkg/Cw9CRf2Q+gAkwlJB1QimnAYnRz57m55AG5l2+9K632RFw44zg+ALiPX+5+FJFTS2MNmNme5AuATUOcARxE5L3gq9DMJpJPlxxU2CQiu7gnYe70mq19tgXcfcXc/VlV34pwJnkTBFXdDsOwBfCRO+1EZKoczSJyywjC3b8qO4mYxai+SyGQfKSZHXIg9XhJHEDTPXHC7vXCq63mVP8cjrv/qOrm7DkAxGyRan1qLM3/rwUoz7UuQCkdixCrFW9atwt7JPcl6TNhIUdgy7Lcxb3ruu++74/tKv4CNXed5xhXezYAAAAASUVORK5CYII="
391
+ alt=""
392
+ width="12"
393
+ height="12"
394
+ />
395
+ </span>
396
+ )}
397
+ </div>
398
+ )}
399
+ </div>
400
+
401
+ {typeof document !== "undefined" &&
402
+ ReactDOM.createPortal(panel, document.body)}
403
+
404
+ <div>
405
+ <div className={`presets-grid modern`}>
406
+ {visiblePresets.map((preset, idx) => (
407
+ <div
408
+ key={idx}
409
+ className="preset-item"
410
+ style={{ backgroundColor: preset }}
411
+ onClick={() => {
412
+ const rgb = hexToRgb(preset);
413
+ if (rgb) {
414
+ const hsv = rgbToHsv(rgb.r, rgb.g, rgb.b);
415
+ handleChange({ ...hsv, a: 1 });
416
+ }
417
+ }}
418
+ title={preset}
419
+ />
420
+ ))}
421
+ </div>
422
+ {showExpand && (
423
+ <div
424
+ className="presets-collapse"
425
+ onClick={() => setIsExpanded(!isExpanded)}
426
+ >
427
+ {isExpanded ? "Less" : "More"}
428
+ <span className={`collapse-arrow ${isExpanded ? "expanded" : ""}`}>
429
+
430
+ </span>
431
+ </div>
432
+ )}
433
+ </div>
109
434
  </div>
110
435
  );
111
436
  };