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.
@@ -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 (isDateType && !inputProps.disabled) {
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
- ${isDateType && !inputProps.disabled ? 'cursor-pointer hover:text-login-text' : 'pointer-events-none'}
70
- `, onClick: handleIconClick, children: displayIcon })), _jsx("input", { ...inputProps, ref: localRef, id: name, name: name, type: isDateType ? 'text' : type, value: value, readOnly: isDateType, onClick: () => isDateType && !inputProps.disabled && setIsOpen(true), title: label, "aria-invalid": !!error, "aria-describedby": error ? `${name}-error` : undefined, className: `
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
- ${isDateType && !inputProps.disabled ? 'cursor-pointer' : ''}
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,7 @@
1
+ import { type JSX } from 'react';
2
+ export type ColorPickerPopupProps = {
3
+ value: string;
4
+ onChange: (color: string) => void;
5
+ onClose: () => void;
6
+ };
7
+ export default function ColorPickerPopup({ value, onChange, onClose }: ColorPickerPopupProps): JSX.Element;
@@ -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';
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "uibee",
3
- "version": "2.7.18",
3
+ "version": "2.8.1",
4
4
  "description": "Shared components, functions and hooks for reuse across Login projects",
5
5
  "homepage": "https://github.com/Login-Linjeforening-for-IT/uibee#readme",
6
6
  "bugs": {
@@ -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 (isDateType && !inputProps.disabled) {
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
- ${isDateType && !inputProps.disabled ? 'cursor-pointer hover:text-login-text' : 'pointer-events-none'}
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={isDateType ? 'text' : type}
129
+ type={isClickableType ? 'text' : type}
106
130
  value={value}
107
- readOnly={isDateType}
108
- onClick={() => isDateType && !inputProps.disabled && setIsOpen(true)}
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
- ${isDateType && !inputProps.disabled ? 'cursor-pointer' : ''}
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'