uibee 2.7.18 → 2.8.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/dist/src/components/inputs/input.js +24 -5
- package/dist/src/components/inputs/shared/colorPickerPopup.d.ts +7 -0
- package/dist/src/components/inputs/shared/colorPickerPopup.js +185 -0
- package/dist/src/components/inputs/shared/index.d.ts +1 -0
- package/dist/src/components/inputs/shared/index.js +1 -0
- package/dist/src/globals.css +148 -0
- package/package.json +1 -1
- package/src/components/inputs/input.tsx +37 -6
- package/src/components/inputs/shared/colorPickerPopup.tsx +240 -0
- package/src/components/inputs/shared/index.ts +1 -0
|
@@ -3,6 +3,7 @@ import { useRef, useState } from 'react';
|
|
|
3
3
|
import { Calendar, Clock } from 'lucide-react';
|
|
4
4
|
import { FieldWrapper } from './shared';
|
|
5
5
|
import DateTimePickerPopup from './shared/dateTimePickerPopup';
|
|
6
|
+
import ColorPickerPopup from './shared/colorPickerPopup';
|
|
6
7
|
import useClickOutside from '../../hooks/useClickOutside';
|
|
7
8
|
export default function Input(props) {
|
|
8
9
|
const { name, label, error, className, icon, info, ...inputProps } = props;
|
|
@@ -11,8 +12,10 @@ export default function Input(props) {
|
|
|
11
12
|
const [isOpen, setIsOpen] = useState(false);
|
|
12
13
|
const containerRef = useClickOutside(() => setIsOpen(false));
|
|
13
14
|
const isDateType = ['date', 'datetime-local', 'time'].includes(type);
|
|
15
|
+
const isColorType = type === 'color';
|
|
16
|
+
const isClickableType = isDateType || isColorType;
|
|
14
17
|
function handleIconClick() {
|
|
15
|
-
if (
|
|
18
|
+
if (isClickableType && !inputProps.disabled) {
|
|
16
19
|
setIsOpen(!isOpen);
|
|
17
20
|
}
|
|
18
21
|
else if (localRef.current && !inputProps.disabled) {
|
|
@@ -45,6 +48,19 @@ export default function Input(props) {
|
|
|
45
48
|
};
|
|
46
49
|
onChange(event);
|
|
47
50
|
}
|
|
51
|
+
function handleColorChange(color) {
|
|
52
|
+
const onChange = inputProps.onChange;
|
|
53
|
+
if (!onChange)
|
|
54
|
+
return;
|
|
55
|
+
const event = {
|
|
56
|
+
target: {
|
|
57
|
+
name,
|
|
58
|
+
value: color,
|
|
59
|
+
type,
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
onChange(event);
|
|
63
|
+
}
|
|
48
64
|
let displayIcon = icon;
|
|
49
65
|
if (!displayIcon && isDateType) {
|
|
50
66
|
if (type === 'time') {
|
|
@@ -54,6 +70,9 @@ export default function Input(props) {
|
|
|
54
70
|
displayIcon = _jsx(Calendar, { className: 'w-4 h-4' });
|
|
55
71
|
}
|
|
56
72
|
}
|
|
73
|
+
else if (!displayIcon && isColorType) {
|
|
74
|
+
displayIcon = (_jsx("div", { className: 'w-4 h-4 rounded border border-login-200', style: { backgroundColor: value || '#000000' } }));
|
|
75
|
+
}
|
|
57
76
|
function getDateValue() {
|
|
58
77
|
if (!value)
|
|
59
78
|
return null;
|
|
@@ -66,8 +85,8 @@ export default function Input(props) {
|
|
|
66
85
|
}
|
|
67
86
|
return (_jsx(FieldWrapper, { label: label, name: name, required: inputProps.required, info: info, error: error, className: className, children: _jsxs("div", { className: 'relative flex items-center', ref: containerRef, children: [displayIcon && (_jsx("div", { className: `
|
|
68
87
|
absolute left-3 text-login-200
|
|
69
|
-
${
|
|
70
|
-
`, onClick: handleIconClick, children: displayIcon })), _jsx("input", { ...inputProps, ref: localRef, id: name, name: name, type:
|
|
88
|
+
${isClickableType && !inputProps.disabled ? 'cursor-pointer hover:text-login-text' : 'pointer-events-none'}
|
|
89
|
+
`, onClick: handleIconClick, children: displayIcon })), _jsx("input", { ...inputProps, ref: localRef, id: name, name: name, type: isClickableType ? 'text' : type, value: value, readOnly: isClickableType, onClick: () => isClickableType && !inputProps.disabled && setIsOpen(true), title: label, "aria-invalid": !!error, "aria-describedby": error ? `${name}-error` : undefined, className: `
|
|
71
90
|
w-full rounded-md bg-login-500/50 border border-login-500
|
|
72
91
|
text-login-text placeholder-login-200
|
|
73
92
|
focus:outline-none focus:border-login focus:ring-1 focus:ring-login
|
|
@@ -76,6 +95,6 @@ export default function Input(props) {
|
|
|
76
95
|
transition-all duration-200
|
|
77
96
|
input-reset
|
|
78
97
|
${error ? 'border-red-500 focus:border-red-500 focus:ring-red-500' : ''}
|
|
79
|
-
${
|
|
80
|
-
` }), isOpen && isDateType && !inputProps.disabled && (_jsx(DateTimePickerPopup, { value: getDateValue(), onChange: handleDateChange, type: type, onClose: () => setIsOpen(false) }))] }) }));
|
|
98
|
+
${isClickableType && !inputProps.disabled ? 'cursor-pointer' : ''}
|
|
99
|
+
` }), isOpen && isDateType && !inputProps.disabled && (_jsx(DateTimePickerPopup, { value: getDateValue(), onChange: handleDateChange, type: type, onClose: () => setIsOpen(false) })), isOpen && isColorType && !inputProps.disabled && (_jsx(ColorPickerPopup, { value: value || '', onChange: handleColorChange, onClose: () => setIsOpen(false) }))] }) }));
|
|
81
100
|
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState, useEffect, useRef } from 'react';
|
|
3
|
+
function hexToHsv(hex) {
|
|
4
|
+
hex = hex.replace('#', '');
|
|
5
|
+
let r = 0, g = 0, b = 0;
|
|
6
|
+
if (hex.length === 3) {
|
|
7
|
+
r = parseInt(hex[0] + hex[0], 16);
|
|
8
|
+
g = parseInt(hex[1] + hex[1], 16);
|
|
9
|
+
b = parseInt(hex[2] + hex[2], 16);
|
|
10
|
+
}
|
|
11
|
+
else if (hex.length === 6) {
|
|
12
|
+
r = parseInt(hex.substring(0, 2), 16);
|
|
13
|
+
g = parseInt(hex.substring(2, 4), 16);
|
|
14
|
+
b = parseInt(hex.substring(4, 6), 16);
|
|
15
|
+
}
|
|
16
|
+
r /= 255;
|
|
17
|
+
g /= 255;
|
|
18
|
+
b /= 255;
|
|
19
|
+
const max = Math.max(r, g, b);
|
|
20
|
+
const min = Math.min(r, g, b);
|
|
21
|
+
const d = max - min;
|
|
22
|
+
let h = 0;
|
|
23
|
+
const s = max === 0 ? 0 : d / max;
|
|
24
|
+
const v = max;
|
|
25
|
+
if (max !== min) {
|
|
26
|
+
switch (max) {
|
|
27
|
+
case r:
|
|
28
|
+
h = (g - b) / d + (g < b ? 6 : 0);
|
|
29
|
+
break;
|
|
30
|
+
case g:
|
|
31
|
+
h = (b - r) / d + 2;
|
|
32
|
+
break;
|
|
33
|
+
case b:
|
|
34
|
+
h = (r - g) / d + 4;
|
|
35
|
+
break;
|
|
36
|
+
}
|
|
37
|
+
h /= 6;
|
|
38
|
+
}
|
|
39
|
+
return { h: h * 360, s: s * 100, v: v * 100 };
|
|
40
|
+
}
|
|
41
|
+
function hsvToRgb(h, s, v) {
|
|
42
|
+
let r = 0, g = 0, b = 0;
|
|
43
|
+
const i = Math.floor(h * 6);
|
|
44
|
+
const f = h * 6 - i;
|
|
45
|
+
const p = v * (1 - s);
|
|
46
|
+
const q = v * (1 - f * s);
|
|
47
|
+
const t = v * (1 - (1 - f) * s);
|
|
48
|
+
switch (i % 6) {
|
|
49
|
+
case 0:
|
|
50
|
+
r = v;
|
|
51
|
+
g = t;
|
|
52
|
+
b = p;
|
|
53
|
+
break;
|
|
54
|
+
case 1:
|
|
55
|
+
r = q;
|
|
56
|
+
g = v;
|
|
57
|
+
b = p;
|
|
58
|
+
break;
|
|
59
|
+
case 2:
|
|
60
|
+
r = p;
|
|
61
|
+
g = v;
|
|
62
|
+
b = t;
|
|
63
|
+
break;
|
|
64
|
+
case 3:
|
|
65
|
+
r = p;
|
|
66
|
+
g = q;
|
|
67
|
+
b = v;
|
|
68
|
+
break;
|
|
69
|
+
case 4:
|
|
70
|
+
r = t;
|
|
71
|
+
g = p;
|
|
72
|
+
b = v;
|
|
73
|
+
break;
|
|
74
|
+
case 5:
|
|
75
|
+
r = v;
|
|
76
|
+
g = p;
|
|
77
|
+
b = q;
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
return { r: Math.round(r * 255), g: Math.round(g * 255), b: Math.round(b * 255) };
|
|
81
|
+
}
|
|
82
|
+
function hsvToHex(h, s, v) {
|
|
83
|
+
const { r, g, b } = hsvToRgb(h / 360, s / 100, v / 100);
|
|
84
|
+
function toHex(x) {
|
|
85
|
+
const hex = x.toString(16);
|
|
86
|
+
return hex.length === 1 ? '0' + hex : hex;
|
|
87
|
+
}
|
|
88
|
+
return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
|
|
89
|
+
}
|
|
90
|
+
const PRESET_COLORS = [
|
|
91
|
+
'#f87171', '#fd8738', '#fbbf24', '#facc15', '#a3e635', '#4ade80', '#34d399', '#2dd4bf',
|
|
92
|
+
'#38bdf8', '#60a5fa', '#818cf8', '#a78bfa', '#c084fc', '#e879f9', '#f472b6', '#fb7185'
|
|
93
|
+
];
|
|
94
|
+
function SaturationPicker({ hsv, onChange }) {
|
|
95
|
+
const containerRef = useRef(null);
|
|
96
|
+
function handleMove(e) {
|
|
97
|
+
if (!containerRef.current)
|
|
98
|
+
return;
|
|
99
|
+
const { left, top, width, height } = containerRef.current.getBoundingClientRect();
|
|
100
|
+
const x = Math.min(Math.max((e.clientX - left) / width, 0), 1);
|
|
101
|
+
const y = Math.min(Math.max((e.clientY - top) / height, 0), 1);
|
|
102
|
+
onChange(x * 100, (1 - y) * 100);
|
|
103
|
+
}
|
|
104
|
+
function handleMouseDown(e) {
|
|
105
|
+
handleMove(e);
|
|
106
|
+
function moveHandler(e) { handleMove(e); }
|
|
107
|
+
function upHandler() {
|
|
108
|
+
window.removeEventListener('mousemove', moveHandler);
|
|
109
|
+
window.removeEventListener('mouseup', upHandler);
|
|
110
|
+
}
|
|
111
|
+
window.addEventListener('mousemove', moveHandler);
|
|
112
|
+
window.addEventListener('mouseup', upHandler);
|
|
113
|
+
}
|
|
114
|
+
const bgColor = hsvToHex(hsv.h, 100, 100);
|
|
115
|
+
return (_jsxs("div", { ref: containerRef, className: 'w-full h-32 relative rounded-md overflow-hidden cursor-crosshair mb-3 select-none', style: { backgroundColor: bgColor }, onMouseDown: handleMouseDown, children: [_jsx("div", { className: 'absolute inset-0 bg-linear-to-r from-white to-transparent' }), _jsx("div", { className: 'absolute inset-0 bg-linear-to-t from-black to-transparent' }), _jsx("div", { className: `
|
|
116
|
+
absolute w-3 h-3 border-2 border-white rounded-full
|
|
117
|
+
shadow-md -translate-x-1/2 -translate-y-1/2 pointer-events-none
|
|
118
|
+
`, style: { left: `${hsv.s}%`, top: `${100 - hsv.v}%` } })] }));
|
|
119
|
+
}
|
|
120
|
+
function HuePicker({ hue, onChange }) {
|
|
121
|
+
const containerRef = useRef(null);
|
|
122
|
+
function handleMove(e) {
|
|
123
|
+
if (!containerRef.current)
|
|
124
|
+
return;
|
|
125
|
+
const { left, width } = containerRef.current.getBoundingClientRect();
|
|
126
|
+
const x = Math.min(Math.max((e.clientX - left) / width, 0), 1);
|
|
127
|
+
onChange(x * 360);
|
|
128
|
+
}
|
|
129
|
+
function handleMouseDown(e) {
|
|
130
|
+
handleMove(e);
|
|
131
|
+
function moveHandler(e) { handleMove(e); }
|
|
132
|
+
function upHandler() {
|
|
133
|
+
window.removeEventListener('mousemove', moveHandler);
|
|
134
|
+
window.removeEventListener('mouseup', upHandler);
|
|
135
|
+
}
|
|
136
|
+
window.addEventListener('mousemove', moveHandler);
|
|
137
|
+
window.addEventListener('mouseup', upHandler);
|
|
138
|
+
}
|
|
139
|
+
return (_jsx("div", { ref: containerRef, className: 'w-full h-3 relative rounded-full cursor-pointer mb-4 select-none', style: { background: 'linear-gradient(to right, #f00 0%, #ff0 17%, #0f0 33%, #0ff 50%, #00f 67%, #f0f 83%, #f00 100%)' }, onMouseDown: handleMouseDown, children: _jsx("div", { className: `
|
|
140
|
+
absolute w-4 h-4 bg-white border border-gray-200
|
|
141
|
+
rounded-full shadow-sm -translate-x-1/2 -translate-y-1/2
|
|
142
|
+
top-1/2 pointer-events-none
|
|
143
|
+
`, style: { left: `${(hue / 360) * 100}%` } }) }));
|
|
144
|
+
}
|
|
145
|
+
export default function ColorPickerPopup({ value, onChange, onClose }) {
|
|
146
|
+
const [hsv, setHsv] = useState(() => hexToHsv(value || '#000000'));
|
|
147
|
+
const [hexInput, setHexInput] = useState(value || '#000000');
|
|
148
|
+
useEffect(() => {
|
|
149
|
+
if (value && value !== hexInput) {
|
|
150
|
+
setHsv(hexToHsv(value));
|
|
151
|
+
setHexInput(value);
|
|
152
|
+
}
|
|
153
|
+
}, [value]);
|
|
154
|
+
function handleColorChange(newHsv) {
|
|
155
|
+
setHsv(newHsv);
|
|
156
|
+
const hex = hsvToHex(newHsv.h, newHsv.s, newHsv.v);
|
|
157
|
+
setHexInput(hex);
|
|
158
|
+
onChange(hex);
|
|
159
|
+
}
|
|
160
|
+
function handleSaturationChange(s, v) { handleColorChange({ ...hsv, s, v }); }
|
|
161
|
+
function handleHueChange(h) { handleColorChange({ ...hsv, h }); }
|
|
162
|
+
function manualHexChange(e) {
|
|
163
|
+
const val = e.target.value;
|
|
164
|
+
setHexInput(val);
|
|
165
|
+
if (/^#[0-9A-F]{6}$/i.test(val)) {
|
|
166
|
+
const newHsv = hexToHsv(val);
|
|
167
|
+
setHsv(newHsv);
|
|
168
|
+
onChange(val);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return (_jsxs("div", { className: 'absolute top-full left-0 mt-1 z-50 bg-login-600 border border-login-500 rounded-md shadow-lg p-3 w-64 select-none', children: [_jsx(SaturationPicker, { hsv: hsv, onChange: handleSaturationChange }), _jsx(HuePicker, { hue: hsv.h, onChange: handleHueChange }), _jsxs("div", { className: 'flex items-center gap-2 mb-3', children: [_jsx("div", { className: 'text-xs text-login-200 font-mono', children: "HEX" }), _jsx("input", { type: 'text', value: hexInput, onChange: manualHexChange, className: `
|
|
172
|
+
flex-1 min-w-0 bg-login-500 border border-login-500 rounded
|
|
173
|
+
px-2 py-1 text-sm text-login-text focus:outline-none
|
|
174
|
+
focus:border-login focus:ring-1 focus:ring-login
|
|
175
|
+
`, spellCheck: false }), _jsx("div", { className: 'w-8 h-8 rounded border border-login-500 shrink-0', style: { backgroundColor: hexInput } })] }), _jsx("div", { className: 'grid grid-cols-8 gap-1.5 pt-3 border-t border-login-500', children: PRESET_COLORS.map(color => (_jsx("button", { type: 'button', className: `
|
|
176
|
+
w-6 h-6 rounded-sm cursor-pointer hover:scale-110
|
|
177
|
+
hover:zIndex-10 transition-transform ring-1 ring-inset ring-black/10
|
|
178
|
+
`, style: { backgroundColor: color }, onClick: () => {
|
|
179
|
+
const newHsv = hexToHsv(color);
|
|
180
|
+
setHsv(newHsv);
|
|
181
|
+
setHexInput(color);
|
|
182
|
+
onChange(color);
|
|
183
|
+
onClose();
|
|
184
|
+
}, title: color }, color))) })] }));
|
|
185
|
+
}
|
|
@@ -3,3 +3,4 @@ export { default as SelectionWrapper } from './selectionWrapper';
|
|
|
3
3
|
export { default as InputLabel } from './inputLabel';
|
|
4
4
|
export { default as InputInfo } from './inputInfo';
|
|
5
5
|
export { default as InputError } from './inputError';
|
|
6
|
+
export { default as ColorPickerPopup } from './colorPickerPopup';
|
|
@@ -3,3 +3,4 @@ export { default as SelectionWrapper } from './selectionWrapper';
|
|
|
3
3
|
export { default as InputLabel } from './inputLabel';
|
|
4
4
|
export { default as InputInfo } from './inputInfo';
|
|
5
5
|
export { default as InputError } from './inputError';
|
|
6
|
+
export { default as ColorPickerPopup } from './colorPickerPopup';
|
package/dist/src/globals.css
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
--color-yellow-500: oklch(79.5% 0.184 86.047);
|
|
14
14
|
--color-green-500: oklch(72.3% 0.219 149.579);
|
|
15
15
|
--color-blue-500: oklch(62.3% 0.214 259.815);
|
|
16
|
+
--color-gray-200: oklch(92.8% 0.006 264.531);
|
|
16
17
|
--color-gray-300: oklch(87.2% 0.01 258.338);
|
|
17
18
|
--color-gray-400: oklch(70.7% 0.022 261.325);
|
|
18
19
|
--color-black: #000;
|
|
@@ -43,6 +44,7 @@
|
|
|
43
44
|
--font-weight-extrabold: 800;
|
|
44
45
|
--tracking-tight: -0.025em;
|
|
45
46
|
--radius-xs: 0.125rem;
|
|
47
|
+
--radius-sm: 0.25rem;
|
|
46
48
|
--radius-md: 0.375rem;
|
|
47
49
|
--radius-lg: 0.5rem;
|
|
48
50
|
--radius-xl: 0.75rem;
|
|
@@ -291,6 +293,9 @@
|
|
|
291
293
|
.relative {
|
|
292
294
|
position: relative;
|
|
293
295
|
}
|
|
296
|
+
.inset-0 {
|
|
297
|
+
inset: calc(var(--spacing) * 0);
|
|
298
|
+
}
|
|
294
299
|
.inset-y-0 {
|
|
295
300
|
inset-block: calc(var(--spacing) * 0);
|
|
296
301
|
}
|
|
@@ -1005,6 +1010,12 @@
|
|
|
1005
1010
|
.mb-2 {
|
|
1006
1011
|
margin-bottom: calc(var(--spacing) * 2);
|
|
1007
1012
|
}
|
|
1013
|
+
.mb-3 {
|
|
1014
|
+
margin-bottom: calc(var(--spacing) * 3);
|
|
1015
|
+
}
|
|
1016
|
+
.mb-4 {
|
|
1017
|
+
margin-bottom: calc(var(--spacing) * 4);
|
|
1018
|
+
}
|
|
1008
1019
|
.-ml-4 {
|
|
1009
1020
|
margin-left: calc(var(--spacing) * -4);
|
|
1010
1021
|
}
|
|
@@ -1084,6 +1095,9 @@
|
|
|
1084
1095
|
.h-16 {
|
|
1085
1096
|
height: calc(var(--spacing) * 16);
|
|
1086
1097
|
}
|
|
1098
|
+
.h-32 {
|
|
1099
|
+
height: calc(var(--spacing) * 32);
|
|
1100
|
+
}
|
|
1087
1101
|
.h-fit {
|
|
1088
1102
|
height: fit-content;
|
|
1089
1103
|
}
|
|
@@ -1141,6 +1155,9 @@
|
|
|
1141
1155
|
.w-16 {
|
|
1142
1156
|
width: calc(var(--spacing) * 16);
|
|
1143
1157
|
}
|
|
1158
|
+
.w-64 {
|
|
1159
|
+
width: calc(var(--spacing) * 64);
|
|
1160
|
+
}
|
|
1144
1161
|
.w-\[4\.3rem\] {
|
|
1145
1162
|
width: 4.3rem;
|
|
1146
1163
|
}
|
|
@@ -1251,6 +1268,9 @@
|
|
|
1251
1268
|
.animate-jump {
|
|
1252
1269
|
animation: jump 0.4s 1;
|
|
1253
1270
|
}
|
|
1271
|
+
.cursor-crosshair {
|
|
1272
|
+
cursor: crosshair;
|
|
1273
|
+
}
|
|
1254
1274
|
.cursor-not-allowed {
|
|
1255
1275
|
cursor: not-allowed;
|
|
1256
1276
|
}
|
|
@@ -1269,6 +1289,9 @@
|
|
|
1269
1289
|
.grid-cols-7 {
|
|
1270
1290
|
grid-template-columns: repeat(7, minmax(0, 1fr));
|
|
1271
1291
|
}
|
|
1292
|
+
.grid-cols-8 {
|
|
1293
|
+
grid-template-columns: repeat(8, minmax(0, 1fr));
|
|
1294
|
+
}
|
|
1272
1295
|
.flex-col {
|
|
1273
1296
|
flex-direction: column;
|
|
1274
1297
|
}
|
|
@@ -1302,6 +1325,9 @@
|
|
|
1302
1325
|
.gap-1 {
|
|
1303
1326
|
gap: calc(var(--spacing) * 1);
|
|
1304
1327
|
}
|
|
1328
|
+
.gap-1\.5 {
|
|
1329
|
+
gap: calc(var(--spacing) * 1.5);
|
|
1330
|
+
}
|
|
1305
1331
|
.gap-2 {
|
|
1306
1332
|
gap: calc(var(--spacing) * 2);
|
|
1307
1333
|
}
|
|
@@ -1350,6 +1376,9 @@
|
|
|
1350
1376
|
.rounded-md {
|
|
1351
1377
|
border-radius: var(--radius-md);
|
|
1352
1378
|
}
|
|
1379
|
+
.rounded-sm {
|
|
1380
|
+
border-radius: var(--radius-sm);
|
|
1381
|
+
}
|
|
1353
1382
|
.rounded-xl {
|
|
1354
1383
|
border-radius: var(--radius-xl);
|
|
1355
1384
|
}
|
|
@@ -1376,6 +1405,12 @@
|
|
|
1376
1405
|
--tw-border-style: none;
|
|
1377
1406
|
border-style: none;
|
|
1378
1407
|
}
|
|
1408
|
+
.border-gray-200 {
|
|
1409
|
+
border-color: var(--color-gray-200);
|
|
1410
|
+
}
|
|
1411
|
+
.border-login-200 {
|
|
1412
|
+
border-color: var(--color-login-200);
|
|
1413
|
+
}
|
|
1379
1414
|
.border-login-400 {
|
|
1380
1415
|
border-color: var(--color-login-400);
|
|
1381
1416
|
}
|
|
@@ -1385,6 +1420,9 @@
|
|
|
1385
1420
|
.border-red-500 {
|
|
1386
1421
|
border-color: var(--color-red-500);
|
|
1387
1422
|
}
|
|
1423
|
+
.border-white {
|
|
1424
|
+
border-color: var(--color-white);
|
|
1425
|
+
}
|
|
1388
1426
|
.bg-\[\#181818f0\] {
|
|
1389
1427
|
background-color: #181818f0;
|
|
1390
1428
|
}
|
|
@@ -1451,9 +1489,35 @@
|
|
|
1451
1489
|
.bg-white {
|
|
1452
1490
|
background-color: var(--color-white);
|
|
1453
1491
|
}
|
|
1492
|
+
.bg-linear-to-r {
|
|
1493
|
+
--tw-gradient-position: to right;
|
|
1494
|
+
@supports (background-image: linear-gradient(in lab, red, red)) {
|
|
1495
|
+
--tw-gradient-position: to right in oklab;
|
|
1496
|
+
}
|
|
1497
|
+
background-image: linear-gradient(var(--tw-gradient-stops));
|
|
1498
|
+
}
|
|
1499
|
+
.bg-linear-to-t {
|
|
1500
|
+
--tw-gradient-position: to top;
|
|
1501
|
+
@supports (background-image: linear-gradient(in lab, red, red)) {
|
|
1502
|
+
--tw-gradient-position: to top in oklab;
|
|
1503
|
+
}
|
|
1504
|
+
background-image: linear-gradient(var(--tw-gradient-stops));
|
|
1505
|
+
}
|
|
1454
1506
|
.bg-none {
|
|
1455
1507
|
background-image: none;
|
|
1456
1508
|
}
|
|
1509
|
+
.from-black {
|
|
1510
|
+
--tw-gradient-from: var(--color-black);
|
|
1511
|
+
--tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));
|
|
1512
|
+
}
|
|
1513
|
+
.from-white {
|
|
1514
|
+
--tw-gradient-from: var(--color-white);
|
|
1515
|
+
--tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));
|
|
1516
|
+
}
|
|
1517
|
+
.to-transparent {
|
|
1518
|
+
--tw-gradient-to: transparent;
|
|
1519
|
+
--tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));
|
|
1520
|
+
}
|
|
1457
1521
|
.fill-\[var\(--foreground\)\] {
|
|
1458
1522
|
fill: var(--foreground);
|
|
1459
1523
|
}
|
|
@@ -1529,6 +1593,9 @@
|
|
|
1529
1593
|
.pt-2 {
|
|
1530
1594
|
padding-top: calc(var(--spacing) * 2);
|
|
1531
1595
|
}
|
|
1596
|
+
.pt-3 {
|
|
1597
|
+
padding-top: calc(var(--spacing) * 3);
|
|
1598
|
+
}
|
|
1532
1599
|
.pr-3 {
|
|
1533
1600
|
padding-right: calc(var(--spacing) * 3);
|
|
1534
1601
|
}
|
|
@@ -1562,6 +1629,9 @@
|
|
|
1562
1629
|
.text-right {
|
|
1563
1630
|
text-align: right;
|
|
1564
1631
|
}
|
|
1632
|
+
.font-mono {
|
|
1633
|
+
font-family: var(--font-mono);
|
|
1634
|
+
}
|
|
1565
1635
|
.text-2xl {
|
|
1566
1636
|
font-size: var(--text-2xl);
|
|
1567
1637
|
line-height: var(--tw-leading, var(--text-2xl--line-height));
|
|
@@ -1699,10 +1769,24 @@
|
|
|
1699
1769
|
--tw-shadow: 0 10px 15px -3px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 4px 6px -4px var(--tw-shadow-color, rgb(0 0 0 / 0.1));
|
|
1700
1770
|
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
|
|
1701
1771
|
}
|
|
1772
|
+
.shadow-md {
|
|
1773
|
+
--tw-shadow: 0 4px 6px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 2px 4px -2px var(--tw-shadow-color, rgb(0 0 0 / 0.1));
|
|
1774
|
+
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
|
|
1775
|
+
}
|
|
1776
|
+
.shadow-sm {
|
|
1777
|
+
--tw-shadow: 0 1px 3px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 1px 2px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1));
|
|
1778
|
+
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
|
|
1779
|
+
}
|
|
1702
1780
|
.ring-1 {
|
|
1703
1781
|
--tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor);
|
|
1704
1782
|
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
|
|
1705
1783
|
}
|
|
1784
|
+
.ring-black\/10 {
|
|
1785
|
+
--tw-ring-color: color-mix(in srgb, #000 10%, transparent);
|
|
1786
|
+
@supports (color: color-mix(in lab, red, red)) {
|
|
1787
|
+
--tw-ring-color: color-mix(in oklab, var(--color-black) 10%, transparent);
|
|
1788
|
+
}
|
|
1789
|
+
}
|
|
1706
1790
|
.ring-red-500 {
|
|
1707
1791
|
--tw-ring-color: var(--color-red-500);
|
|
1708
1792
|
}
|
|
@@ -1814,6 +1898,9 @@
|
|
|
1814
1898
|
-webkit-user-select: none;
|
|
1815
1899
|
user-select: none;
|
|
1816
1900
|
}
|
|
1901
|
+
.ring-inset {
|
|
1902
|
+
--tw-ring-inset: inset;
|
|
1903
|
+
}
|
|
1817
1904
|
.group-\[\.dropdown\]\:h-auto {
|
|
1818
1905
|
&:is(:where(.group):is(.dropdown) *) {
|
|
1819
1906
|
height: auto;
|
|
@@ -1998,6 +2085,16 @@
|
|
|
1998
2085
|
--tw-ring-color: var(--color-red-500);
|
|
1999
2086
|
}
|
|
2000
2087
|
}
|
|
2088
|
+
.hover\:scale-110 {
|
|
2089
|
+
&:hover {
|
|
2090
|
+
@media (hover: hover) {
|
|
2091
|
+
--tw-scale-x: 110%;
|
|
2092
|
+
--tw-scale-y: 110%;
|
|
2093
|
+
--tw-scale-z: 110%;
|
|
2094
|
+
scale: var(--tw-scale-x) var(--tw-scale-y);
|
|
2095
|
+
}
|
|
2096
|
+
}
|
|
2097
|
+
}
|
|
2001
2098
|
.hover\:bg-gray-400\/10 {
|
|
2002
2099
|
&:hover {
|
|
2003
2100
|
@media (hover: hover) {
|
|
@@ -2405,6 +2502,48 @@ input::-ms-clear {
|
|
|
2405
2502
|
inherits: false;
|
|
2406
2503
|
initial-value: solid;
|
|
2407
2504
|
}
|
|
2505
|
+
@property --tw-gradient-position {
|
|
2506
|
+
syntax: "*";
|
|
2507
|
+
inherits: false;
|
|
2508
|
+
}
|
|
2509
|
+
@property --tw-gradient-from {
|
|
2510
|
+
syntax: "<color>";
|
|
2511
|
+
inherits: false;
|
|
2512
|
+
initial-value: #0000;
|
|
2513
|
+
}
|
|
2514
|
+
@property --tw-gradient-via {
|
|
2515
|
+
syntax: "<color>";
|
|
2516
|
+
inherits: false;
|
|
2517
|
+
initial-value: #0000;
|
|
2518
|
+
}
|
|
2519
|
+
@property --tw-gradient-to {
|
|
2520
|
+
syntax: "<color>";
|
|
2521
|
+
inherits: false;
|
|
2522
|
+
initial-value: #0000;
|
|
2523
|
+
}
|
|
2524
|
+
@property --tw-gradient-stops {
|
|
2525
|
+
syntax: "*";
|
|
2526
|
+
inherits: false;
|
|
2527
|
+
}
|
|
2528
|
+
@property --tw-gradient-via-stops {
|
|
2529
|
+
syntax: "*";
|
|
2530
|
+
inherits: false;
|
|
2531
|
+
}
|
|
2532
|
+
@property --tw-gradient-from-position {
|
|
2533
|
+
syntax: "<length-percentage>";
|
|
2534
|
+
inherits: false;
|
|
2535
|
+
initial-value: 0%;
|
|
2536
|
+
}
|
|
2537
|
+
@property --tw-gradient-via-position {
|
|
2538
|
+
syntax: "<length-percentage>";
|
|
2539
|
+
inherits: false;
|
|
2540
|
+
initial-value: 50%;
|
|
2541
|
+
}
|
|
2542
|
+
@property --tw-gradient-to-position {
|
|
2543
|
+
syntax: "<length-percentage>";
|
|
2544
|
+
inherits: false;
|
|
2545
|
+
initial-value: 100%;
|
|
2546
|
+
}
|
|
2408
2547
|
@property --tw-leading {
|
|
2409
2548
|
syntax: "*";
|
|
2410
2549
|
inherits: false;
|
|
@@ -2552,6 +2691,15 @@ input::-ms-clear {
|
|
|
2552
2691
|
--tw-skew-y: initial;
|
|
2553
2692
|
--tw-space-x-reverse: 0;
|
|
2554
2693
|
--tw-border-style: solid;
|
|
2694
|
+
--tw-gradient-position: initial;
|
|
2695
|
+
--tw-gradient-from: #0000;
|
|
2696
|
+
--tw-gradient-via: #0000;
|
|
2697
|
+
--tw-gradient-to: #0000;
|
|
2698
|
+
--tw-gradient-stops: initial;
|
|
2699
|
+
--tw-gradient-via-stops: initial;
|
|
2700
|
+
--tw-gradient-from-position: 0%;
|
|
2701
|
+
--tw-gradient-via-position: 50%;
|
|
2702
|
+
--tw-gradient-to-position: 100%;
|
|
2555
2703
|
--tw-leading: initial;
|
|
2556
2704
|
--tw-font-weight: initial;
|
|
2557
2705
|
--tw-tracking: initial;
|
package/package.json
CHANGED
|
@@ -2,6 +2,7 @@ import { type ChangeEvent, type JSX, useRef, useState } from 'react'
|
|
|
2
2
|
import { Calendar, Clock } from 'lucide-react'
|
|
3
3
|
import { FieldWrapper } from './shared'
|
|
4
4
|
import DateTimePickerPopup from './shared/dateTimePickerPopup'
|
|
5
|
+
import ColorPickerPopup from './shared/colorPickerPopup'
|
|
5
6
|
import useClickOutside from '../../hooks/useClickOutside'
|
|
6
7
|
|
|
7
8
|
export type InputProps = Omit<React.ComponentProps<'input'>, 'name'> & {
|
|
@@ -22,9 +23,11 @@ export default function Input(props: InputProps) {
|
|
|
22
23
|
const containerRef = useClickOutside<HTMLDivElement>(() => setIsOpen(false))
|
|
23
24
|
|
|
24
25
|
const isDateType = ['date', 'datetime-local', 'time'].includes(type as string)
|
|
26
|
+
const isColorType = type === 'color'
|
|
27
|
+
const isClickableType = isDateType || isColorType
|
|
25
28
|
|
|
26
29
|
function handleIconClick() {
|
|
27
|
-
if (
|
|
30
|
+
if (isClickableType && !inputProps.disabled) {
|
|
28
31
|
setIsOpen(!isOpen)
|
|
29
32
|
} else if (localRef.current && !inputProps.disabled) {
|
|
30
33
|
localRef.current.focus()
|
|
@@ -57,6 +60,20 @@ export default function Input(props: InputProps) {
|
|
|
57
60
|
onChange(event)
|
|
58
61
|
}
|
|
59
62
|
|
|
63
|
+
function handleColorChange(color: string) {
|
|
64
|
+
const onChange = inputProps.onChange
|
|
65
|
+
if (!onChange) return
|
|
66
|
+
|
|
67
|
+
const event = {
|
|
68
|
+
target: {
|
|
69
|
+
name,
|
|
70
|
+
value: color,
|
|
71
|
+
type,
|
|
72
|
+
},
|
|
73
|
+
} as unknown as ChangeEvent<HTMLInputElement>
|
|
74
|
+
onChange(event)
|
|
75
|
+
}
|
|
76
|
+
|
|
60
77
|
let displayIcon = icon
|
|
61
78
|
if (!displayIcon && isDateType) {
|
|
62
79
|
if (type === 'time') {
|
|
@@ -64,6 +81,13 @@ export default function Input(props: InputProps) {
|
|
|
64
81
|
} else {
|
|
65
82
|
displayIcon = <Calendar className='w-4 h-4' />
|
|
66
83
|
}
|
|
84
|
+
} else if (!displayIcon && isColorType) {
|
|
85
|
+
displayIcon = (
|
|
86
|
+
<div
|
|
87
|
+
className='w-4 h-4 rounded border border-login-200'
|
|
88
|
+
style={{ backgroundColor: value as string || '#000000' }}
|
|
89
|
+
/>
|
|
90
|
+
)
|
|
67
91
|
}
|
|
68
92
|
|
|
69
93
|
function getDateValue() {
|
|
@@ -90,7 +114,7 @@ export default function Input(props: InputProps) {
|
|
|
90
114
|
<div
|
|
91
115
|
className={`
|
|
92
116
|
absolute left-3 text-login-200
|
|
93
|
-
${
|
|
117
|
+
${isClickableType && !inputProps.disabled ? 'cursor-pointer hover:text-login-text' : 'pointer-events-none'}
|
|
94
118
|
`}
|
|
95
119
|
onClick={handleIconClick}
|
|
96
120
|
>
|
|
@@ -102,10 +126,10 @@ export default function Input(props: InputProps) {
|
|
|
102
126
|
ref={localRef}
|
|
103
127
|
id={name}
|
|
104
128
|
name={name}
|
|
105
|
-
type={
|
|
129
|
+
type={isClickableType ? 'text' : type}
|
|
106
130
|
value={value}
|
|
107
|
-
readOnly={
|
|
108
|
-
onClick={() =>
|
|
131
|
+
readOnly={isClickableType}
|
|
132
|
+
onClick={() => isClickableType && !inputProps.disabled && setIsOpen(true)}
|
|
109
133
|
title={label}
|
|
110
134
|
aria-invalid={!!error}
|
|
111
135
|
aria-describedby={error ? `${name}-error` : undefined}
|
|
@@ -118,7 +142,7 @@ export default function Input(props: InputProps) {
|
|
|
118
142
|
transition-all duration-200
|
|
119
143
|
input-reset
|
|
120
144
|
${error ? 'border-red-500 focus:border-red-500 focus:ring-red-500' : ''}
|
|
121
|
-
${
|
|
145
|
+
${isClickableType && !inputProps.disabled ? 'cursor-pointer' : ''}
|
|
122
146
|
`}
|
|
123
147
|
/>
|
|
124
148
|
{isOpen && isDateType && !inputProps.disabled && (
|
|
@@ -129,6 +153,13 @@ export default function Input(props: InputProps) {
|
|
|
129
153
|
onClose={() => setIsOpen(false)}
|
|
130
154
|
/>
|
|
131
155
|
)}
|
|
156
|
+
{isOpen && isColorType && !inputProps.disabled && (
|
|
157
|
+
<ColorPickerPopup
|
|
158
|
+
value={value as string || ''}
|
|
159
|
+
onChange={handleColorChange}
|
|
160
|
+
onClose={() => setIsOpen(false)}
|
|
161
|
+
/>
|
|
162
|
+
)}
|
|
132
163
|
</div>
|
|
133
164
|
</FieldWrapper>
|
|
134
165
|
)
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import { type JSX, useState, useEffect, useRef } from 'react'
|
|
2
|
+
|
|
3
|
+
export type ColorPickerPopupProps = {
|
|
4
|
+
value: string
|
|
5
|
+
onChange: (color: string) => void
|
|
6
|
+
onClose: () => void
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function hexToHsv(hex: string): { h: number; s: number; v: number } {
|
|
10
|
+
hex = hex.replace('#', '')
|
|
11
|
+
let r = 0, g = 0, b = 0
|
|
12
|
+
if (hex.length === 3) {
|
|
13
|
+
r = parseInt(hex[0] + hex[0], 16)
|
|
14
|
+
g = parseInt(hex[1] + hex[1], 16)
|
|
15
|
+
b = parseInt(hex[2] + hex[2], 16)
|
|
16
|
+
} else if (hex.length === 6) {
|
|
17
|
+
r = parseInt(hex.substring(0, 2), 16)
|
|
18
|
+
g = parseInt(hex.substring(2, 4), 16)
|
|
19
|
+
b = parseInt(hex.substring(4, 6), 16)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
r /= 255
|
|
23
|
+
g /= 255
|
|
24
|
+
b /= 255
|
|
25
|
+
|
|
26
|
+
const max = Math.max(r, g, b)
|
|
27
|
+
const min = Math.min(r, g, b)
|
|
28
|
+
const d = max - min
|
|
29
|
+
let h = 0
|
|
30
|
+
const s = max === 0 ? 0 : d / max
|
|
31
|
+
const v = max
|
|
32
|
+
|
|
33
|
+
if (max !== min) {
|
|
34
|
+
switch (max) {
|
|
35
|
+
case r: h = (g - b) / d + (g < b ? 6 : 0); break
|
|
36
|
+
case g: h = (b - r) / d + 2; break
|
|
37
|
+
case b: h = (r - g) / d + 4; break
|
|
38
|
+
}
|
|
39
|
+
h /= 6
|
|
40
|
+
}
|
|
41
|
+
return { h: h * 360, s: s * 100, v: v * 100 }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function hsvToRgb(h: number, s: number, v: number): { r: number; g: number; b: number } {
|
|
45
|
+
let r = 0, g = 0, b = 0
|
|
46
|
+
const i = Math.floor(h * 6)
|
|
47
|
+
const f = h * 6 - i
|
|
48
|
+
const p = v * (1 - s)
|
|
49
|
+
const q = v * (1 - f * s)
|
|
50
|
+
const t = v * (1 - (1 - f) * s)
|
|
51
|
+
|
|
52
|
+
switch (i % 6) {
|
|
53
|
+
case 0: r = v; g = t; b = p; break
|
|
54
|
+
case 1: r = q; g = v; b = p; break
|
|
55
|
+
case 2: r = p; g = v; b = t; break
|
|
56
|
+
case 3: r = p; g = q; b = v; break
|
|
57
|
+
case 4: r = t; g = p; b = v; break
|
|
58
|
+
case 5: r = v; g = p; b = q; break
|
|
59
|
+
}
|
|
60
|
+
return { r: Math.round(r * 255), g: Math.round(g * 255), b: Math.round(b * 255) }
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function hsvToHex(h: number, s: number, v: number): string {
|
|
64
|
+
const { r, g, b } = hsvToRgb(h / 360, s / 100, v / 100)
|
|
65
|
+
function toHex(x: number) {
|
|
66
|
+
const hex = x.toString(16)
|
|
67
|
+
return hex.length === 1 ? '0' + hex : hex
|
|
68
|
+
}
|
|
69
|
+
return `#${toHex(r)}${toHex(g)}${toHex(b)}`
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const PRESET_COLORS: string[] = [
|
|
73
|
+
'#f87171', '#fd8738', '#fbbf24', '#facc15', '#a3e635', '#4ade80', '#34d399', '#2dd4bf',
|
|
74
|
+
'#38bdf8', '#60a5fa', '#818cf8', '#a78bfa', '#c084fc', '#e879f9', '#f472b6', '#fb7185'
|
|
75
|
+
]
|
|
76
|
+
|
|
77
|
+
function SaturationPicker({ hsv, onChange }: { hsv: { h: number, s: number, v: number }, onChange: (s: number, v: number) => void }) {
|
|
78
|
+
const containerRef = useRef<HTMLDivElement>(null)
|
|
79
|
+
|
|
80
|
+
function handleMove(e: MouseEvent | React.MouseEvent) {
|
|
81
|
+
if (!containerRef.current) return
|
|
82
|
+
const { left, top, width, height } = containerRef.current.getBoundingClientRect()
|
|
83
|
+
const x = Math.min(Math.max((e.clientX - left) / width, 0), 1)
|
|
84
|
+
const y = Math.min(Math.max((e.clientY - top) / height, 0), 1)
|
|
85
|
+
|
|
86
|
+
onChange(x * 100, (1 - y) * 100)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function handleMouseDown(e: React.MouseEvent) {
|
|
90
|
+
handleMove(e)
|
|
91
|
+
function moveHandler(e: MouseEvent) { handleMove(e) }
|
|
92
|
+
function upHandler() {
|
|
93
|
+
window.removeEventListener('mousemove', moveHandler)
|
|
94
|
+
window.removeEventListener('mouseup', upHandler)
|
|
95
|
+
}
|
|
96
|
+
window.addEventListener('mousemove', moveHandler)
|
|
97
|
+
window.addEventListener('mouseup', upHandler)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const bgColor = hsvToHex(hsv.h, 100, 100)
|
|
101
|
+
|
|
102
|
+
return (
|
|
103
|
+
<div
|
|
104
|
+
ref={containerRef}
|
|
105
|
+
className='w-full h-32 relative rounded-md overflow-hidden cursor-crosshair mb-3 select-none'
|
|
106
|
+
style={{ backgroundColor: bgColor }}
|
|
107
|
+
onMouseDown={handleMouseDown}
|
|
108
|
+
>
|
|
109
|
+
<div className='absolute inset-0 bg-linear-to-r from-white to-transparent' />
|
|
110
|
+
<div className='absolute inset-0 bg-linear-to-t from-black to-transparent' />
|
|
111
|
+
<div
|
|
112
|
+
className={`
|
|
113
|
+
absolute w-3 h-3 border-2 border-white rounded-full
|
|
114
|
+
shadow-md -translate-x-1/2 -translate-y-1/2 pointer-events-none
|
|
115
|
+
`}
|
|
116
|
+
style={{ left: `${hsv.s}%`, top: `${100 - hsv.v}%` }}
|
|
117
|
+
/>
|
|
118
|
+
</div>
|
|
119
|
+
)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function HuePicker({ hue, onChange }: { hue: number, onChange: (h: number) => void }) {
|
|
123
|
+
const containerRef = useRef<HTMLDivElement>(null)
|
|
124
|
+
|
|
125
|
+
function handleMove(e: MouseEvent | React.MouseEvent) {
|
|
126
|
+
if (!containerRef.current) return
|
|
127
|
+
const { left, width } = containerRef.current.getBoundingClientRect()
|
|
128
|
+
const x = Math.min(Math.max((e.clientX - left) / width, 0), 1)
|
|
129
|
+
onChange(x * 360)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function handleMouseDown(e: React.MouseEvent) {
|
|
133
|
+
handleMove(e)
|
|
134
|
+
function moveHandler(e: MouseEvent) { handleMove(e) }
|
|
135
|
+
function upHandler() {
|
|
136
|
+
window.removeEventListener('mousemove', moveHandler)
|
|
137
|
+
window.removeEventListener('mouseup', upHandler)
|
|
138
|
+
}
|
|
139
|
+
window.addEventListener('mousemove', moveHandler)
|
|
140
|
+
window.addEventListener('mouseup', upHandler)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return (
|
|
144
|
+
<div
|
|
145
|
+
ref={containerRef}
|
|
146
|
+
className='w-full h-3 relative rounded-full cursor-pointer mb-4 select-none'
|
|
147
|
+
style={{ background: 'linear-gradient(to right, #f00 0%, #ff0 17%, #0f0 33%, #0ff 50%, #00f 67%, #f0f 83%, #f00 100%)' }}
|
|
148
|
+
onMouseDown={handleMouseDown}
|
|
149
|
+
>
|
|
150
|
+
<div
|
|
151
|
+
className={`
|
|
152
|
+
absolute w-4 h-4 bg-white border border-gray-200
|
|
153
|
+
rounded-full shadow-sm -translate-x-1/2 -translate-y-1/2
|
|
154
|
+
top-1/2 pointer-events-none
|
|
155
|
+
`}
|
|
156
|
+
style={{ left: `${(hue / 360) * 100}%` }}
|
|
157
|
+
/>
|
|
158
|
+
</div>
|
|
159
|
+
)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export default function ColorPickerPopup({ value, onChange, onClose }: ColorPickerPopupProps): JSX.Element {
|
|
163
|
+
const [hsv, setHsv] = useState(() => hexToHsv(value || '#000000'))
|
|
164
|
+
const [hexInput, setHexInput] = useState(value || '#000000')
|
|
165
|
+
|
|
166
|
+
useEffect(() => {
|
|
167
|
+
if (value && value !== hexInput) {
|
|
168
|
+
setHsv(hexToHsv(value))
|
|
169
|
+
setHexInput(value)
|
|
170
|
+
}
|
|
171
|
+
}, [value])
|
|
172
|
+
|
|
173
|
+
function handleColorChange(newHsv: { h: number, s: number, v: number }) {
|
|
174
|
+
setHsv(newHsv)
|
|
175
|
+
const hex = hsvToHex(newHsv.h, newHsv.s, newHsv.v)
|
|
176
|
+
setHexInput(hex)
|
|
177
|
+
onChange(hex)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function handleSaturationChange(s: number, v: number) { handleColorChange({ ...hsv, s, v }) }
|
|
181
|
+
function handleHueChange(h: number) { handleColorChange({ ...hsv, h }) }
|
|
182
|
+
|
|
183
|
+
function manualHexChange(e: React.ChangeEvent<HTMLInputElement>) {
|
|
184
|
+
const val = e.target.value
|
|
185
|
+
setHexInput(val)
|
|
186
|
+
if (/^#[0-9A-F]{6}$/i.test(val)) {
|
|
187
|
+
const newHsv = hexToHsv(val)
|
|
188
|
+
setHsv(newHsv)
|
|
189
|
+
onChange(val)
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return (
|
|
194
|
+
<div className='absolute top-full left-0 mt-1 z-50 bg-login-600 border border-login-500 rounded-md shadow-lg p-3 w-64 select-none'>
|
|
195
|
+
<SaturationPicker hsv={hsv} onChange={handleSaturationChange} />
|
|
196
|
+
<HuePicker hue={hsv.h} onChange={handleHueChange} />
|
|
197
|
+
|
|
198
|
+
<div className='flex items-center gap-2 mb-3'>
|
|
199
|
+
<div className='text-xs text-login-200 font-mono'>HEX</div>
|
|
200
|
+
<input
|
|
201
|
+
type='text'
|
|
202
|
+
value={hexInput}
|
|
203
|
+
onChange={manualHexChange}
|
|
204
|
+
className={`
|
|
205
|
+
flex-1 min-w-0 bg-login-500 border border-login-500 rounded
|
|
206
|
+
px-2 py-1 text-sm text-login-text focus:outline-none
|
|
207
|
+
focus:border-login focus:ring-1 focus:ring-login
|
|
208
|
+
`}
|
|
209
|
+
spellCheck={false}
|
|
210
|
+
/>
|
|
211
|
+
<div
|
|
212
|
+
className='w-8 h-8 rounded border border-login-500 shrink-0'
|
|
213
|
+
style={{ backgroundColor: hexInput }}
|
|
214
|
+
/>
|
|
215
|
+
</div>
|
|
216
|
+
|
|
217
|
+
<div className='grid grid-cols-8 gap-1.5 pt-3 border-t border-login-500'>
|
|
218
|
+
{PRESET_COLORS.map(color => (
|
|
219
|
+
<button
|
|
220
|
+
key={color}
|
|
221
|
+
type='button'
|
|
222
|
+
className={`
|
|
223
|
+
w-6 h-6 rounded-sm cursor-pointer hover:scale-110
|
|
224
|
+
hover:zIndex-10 transition-transform ring-1 ring-inset ring-black/10
|
|
225
|
+
`}
|
|
226
|
+
style={{ backgroundColor: color }}
|
|
227
|
+
onClick={() => {
|
|
228
|
+
const newHsv = hexToHsv(color)
|
|
229
|
+
setHsv(newHsv)
|
|
230
|
+
setHexInput(color)
|
|
231
|
+
onChange(color)
|
|
232
|
+
onClose()
|
|
233
|
+
}}
|
|
234
|
+
title={color}
|
|
235
|
+
/>
|
|
236
|
+
))}
|
|
237
|
+
</div>
|
|
238
|
+
</div>
|
|
239
|
+
)
|
|
240
|
+
}
|
|
@@ -3,3 +3,4 @@ export { default as SelectionWrapper } from './selectionWrapper'
|
|
|
3
3
|
export { default as InputLabel } from './inputLabel'
|
|
4
4
|
export { default as InputInfo } from './inputInfo'
|
|
5
5
|
export { default as InputError } from './inputError'
|
|
6
|
+
export { default as ColorPickerPopup } from './colorPickerPopup'
|