cynx-ui 1.3.11 → 1.3.13
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.
|
@@ -56,8 +56,10 @@ export function DatePicker({ value, onChange, label, placeholder = 'Select date'
|
|
|
56
56
|
const current = toDate(value) || new Date();
|
|
57
57
|
const next = new Date(current.getFullYear(), current.getMonth(), current.getDate() + delta);
|
|
58
58
|
|
|
59
|
-
|
|
60
|
-
|
|
59
|
+
const minD = toDate(minDate);
|
|
60
|
+
const maxD = toDate(maxDate);
|
|
61
|
+
if (minD && next < new Date(new Date(minD).setHours(0, 0, 0, 0))) return;
|
|
62
|
+
if (maxD && next > new Date(new Date(maxD).setHours(23, 59, 59, 999))) return;
|
|
61
63
|
|
|
62
64
|
if (typeof value === 'string') {
|
|
63
65
|
const y = next.getFullYear();
|
|
@@ -218,8 +220,10 @@ export function DatePicker({ value, onChange, label, placeholder = 'Select date'
|
|
|
218
220
|
const badge = badges[key];
|
|
219
221
|
const sel = isSameDay(date, value);
|
|
220
222
|
const today = isToday(y, m, c.day);
|
|
221
|
-
const
|
|
222
|
-
const
|
|
223
|
+
const minD = toDate(minDate);
|
|
224
|
+
const maxD = toDate(maxDate);
|
|
225
|
+
const disMin = minD && date < new Date(new Date(minD).setHours(0,0,0,0));
|
|
226
|
+
const disMax = maxD && date > new Date(new Date(maxD).setHours(23,59,59,999));
|
|
223
227
|
const dis = disMin || disMax;
|
|
224
228
|
return (
|
|
225
229
|
<div key={i} onClick={() => !dis && pick(date)} style={{
|
package/components/Dropdown.jsx
CHANGED
|
@@ -49,15 +49,23 @@ export default function Dropdown({ value, onChange, options, placeholder = 'Sele
|
|
|
49
49
|
|
|
50
50
|
const selected = options.find(o => String(o.value) === String(value));
|
|
51
51
|
|
|
52
|
+
const spaceBelow = rect ? window.innerHeight - rect.bottom : 200;
|
|
53
|
+
const spaceAbove = rect ? rect.top : 200;
|
|
54
|
+
const estimatedH = Math.min(options.length * 36 + 10, 220);
|
|
55
|
+
const openUp = spaceBelow < estimatedH + 10 && spaceAbove > spaceBelow;
|
|
56
|
+
const calculatedTop = rect ? (openUp ? Math.max(8, rect.top - estimatedH - 4) : rect.bottom + 4) : 0;
|
|
57
|
+
|
|
52
58
|
const menu = open && rect && createPortal(
|
|
53
59
|
<div
|
|
54
60
|
ref={menuRef}
|
|
55
61
|
data-dropdown="true"
|
|
56
62
|
style={{
|
|
57
63
|
position: 'fixed',
|
|
58
|
-
top:
|
|
64
|
+
top: calculatedTop,
|
|
59
65
|
left: rect.left,
|
|
60
66
|
width: rect.width,
|
|
67
|
+
maxHeight: 220,
|
|
68
|
+
overflowY: 'auto',
|
|
61
69
|
background: 'var(--surface)',
|
|
62
70
|
border: '1px solid var(--border)',
|
|
63
71
|
borderRadius: 'var(--rads)',
|
|
@@ -70,12 +70,18 @@ export default function MultiSelect({ value = [], onChange, options = [], placeh
|
|
|
70
70
|
if (open) setQuery('');
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
const spaceBelow = rect ? window.innerHeight - rect.bottom : 200;
|
|
74
|
+
const spaceAbove = rect ? rect.top : 200;
|
|
75
|
+
const estimatedH = 220;
|
|
76
|
+
const openUp = spaceBelow < estimatedH + 10 && spaceAbove > spaceBelow;
|
|
77
|
+
const top = rect ? (openUp ? Math.max(8, rect.top - estimatedH - 4) : rect.bottom + 4) : 0;
|
|
78
|
+
|
|
73
79
|
const menu = open && rect && createPortal(
|
|
74
80
|
<div
|
|
75
81
|
ref={menuRef}
|
|
76
82
|
style={{
|
|
77
83
|
position: 'fixed',
|
|
78
|
-
top
|
|
84
|
+
top,
|
|
79
85
|
left: rect.left,
|
|
80
86
|
width: rect.width,
|
|
81
87
|
zIndex: 999999,
|
|
@@ -48,9 +48,14 @@ export function SearchableDropdown({ value, onChange, options = [], placeholder
|
|
|
48
48
|
const btn = btnRef.current;
|
|
49
49
|
if (!btn) return;
|
|
50
50
|
const rect = btn.getBoundingClientRect();
|
|
51
|
+
const spaceBelow = window.innerHeight - rect.bottom;
|
|
52
|
+
const spaceAbove = rect.top;
|
|
53
|
+
const estimatedH = 220;
|
|
54
|
+
const openUp = spaceBelow < estimatedH + 10 && spaceAbove > spaceBelow;
|
|
55
|
+
const top = openUp ? Math.max(8, rect.top - estimatedH - 4) : rect.bottom + 4;
|
|
51
56
|
setMenuStyle({
|
|
52
57
|
position: 'fixed',
|
|
53
|
-
top
|
|
58
|
+
top,
|
|
54
59
|
left: rect.left,
|
|
55
60
|
width: rect.width,
|
|
56
61
|
zIndex: 999999,
|
package/components/Select.jsx
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
import { useState, useRef, useEffect, useMemo } from 'react';
|
|
1
|
+
import { useState, useRef, useEffect, useLayoutEffect, useMemo, useCallback } from 'react';
|
|
2
2
|
import { createPortal } from 'react-dom';
|
|
3
3
|
import { ChevronDown, Check, X } from 'lucide-react';
|
|
4
4
|
|
|
5
|
-
const KEYFRAMES =
|
|
5
|
+
const KEYFRAMES = `
|
|
6
|
+
@keyframes selFadeInDown{from{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:translateY(0)}}
|
|
7
|
+
@keyframes selFadeInUp{from{opacity:0;transform:translateY(4px)}to{opacity:1;transform:translateY(0)}}
|
|
8
|
+
`;
|
|
6
9
|
let _id = 'cynx-select-styles';
|
|
7
10
|
|
|
8
11
|
function ensureStyles() {
|
|
@@ -30,35 +33,73 @@ export default function Select({
|
|
|
30
33
|
}) {
|
|
31
34
|
const [open, setOpen] = useState(false);
|
|
32
35
|
const [query, setQuery] = useState('');
|
|
33
|
-
const [pos, setPos] = useState({ top: 0, left: 0, width: 0 });
|
|
36
|
+
const [pos, setPos] = useState({ top: 0, left: 0, width: 0, openUp: false, maxHeight: 200 });
|
|
34
37
|
const wrapRef = useRef(null);
|
|
35
38
|
const dropRef = useRef(null);
|
|
36
39
|
const inputRef = useRef(null);
|
|
37
40
|
|
|
38
41
|
ensureStyles();
|
|
39
42
|
|
|
43
|
+
const calcPosition = useCallback(() => {
|
|
44
|
+
if (!wrapRef.current) return;
|
|
45
|
+
const r = wrapRef.current.getBoundingClientRect();
|
|
46
|
+
const dropEl = dropRef.current;
|
|
47
|
+
const dropHeight = dropEl ? dropEl.offsetHeight : 220;
|
|
48
|
+
const spaceBelow = window.innerHeight - r.bottom;
|
|
49
|
+
const spaceAbove = r.top;
|
|
50
|
+
|
|
51
|
+
// If not enough space below (< dropHeight + 10) and more space above, open up
|
|
52
|
+
const openUp = spaceBelow < Math.min(dropHeight, 220) + 12 && spaceAbove > spaceBelow;
|
|
53
|
+
|
|
54
|
+
// Dynamic max-height for inner list to prevent overflow
|
|
55
|
+
const availableSpace = openUp ? spaceAbove - 16 : spaceBelow - 16;
|
|
56
|
+
const calculatedMaxHeight = Math.max(90, Math.min(220, availableSpace - (searchable ? 46 : 14)));
|
|
57
|
+
|
|
58
|
+
let top;
|
|
59
|
+
if (openUp) {
|
|
60
|
+
top = Math.max(8, r.top - (dropEl ? dropEl.offsetHeight : dropHeight) - 4);
|
|
61
|
+
} else {
|
|
62
|
+
top = Math.min(window.innerHeight - 40, r.bottom + 4);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
let left = r.left;
|
|
66
|
+
const width = r.width;
|
|
67
|
+
if (left + width > window.innerWidth - 8) {
|
|
68
|
+
left = Math.max(8, window.innerWidth - width - 8);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
setPos({
|
|
72
|
+
top,
|
|
73
|
+
left,
|
|
74
|
+
width,
|
|
75
|
+
openUp,
|
|
76
|
+
maxHeight: calculatedMaxHeight,
|
|
77
|
+
});
|
|
78
|
+
}, [searchable]);
|
|
79
|
+
|
|
40
80
|
useEffect(() => {
|
|
41
81
|
if (!open) return;
|
|
42
|
-
function updatePos() {
|
|
43
|
-
if (wrapRef.current) {
|
|
44
|
-
const r = wrapRef.current.getBoundingClientRect();
|
|
45
|
-
setPos({ top: r.bottom + 4, left: r.left, width: r.width });
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
82
|
function close(e) {
|
|
49
83
|
const inTrigger = wrapRef.current && wrapRef.current.contains(e.target);
|
|
50
84
|
const inDrop = dropRef.current && dropRef.current.contains(e.target);
|
|
51
85
|
if (!inTrigger && !inDrop) setOpen(false);
|
|
52
86
|
}
|
|
53
87
|
document.addEventListener('mousedown', close, true);
|
|
54
|
-
window.addEventListener('scroll',
|
|
55
|
-
window.addEventListener('resize',
|
|
88
|
+
window.addEventListener('scroll', calcPosition, true);
|
|
89
|
+
window.addEventListener('resize', calcPosition);
|
|
56
90
|
return () => {
|
|
57
91
|
document.removeEventListener('mousedown', close, true);
|
|
58
|
-
window.removeEventListener('scroll',
|
|
59
|
-
window.removeEventListener('resize',
|
|
92
|
+
window.removeEventListener('scroll', calcPosition, true);
|
|
93
|
+
window.removeEventListener('resize', calcPosition);
|
|
60
94
|
};
|
|
61
|
-
}, [open]);
|
|
95
|
+
}, [open, calcPosition]);
|
|
96
|
+
|
|
97
|
+
useLayoutEffect(() => {
|
|
98
|
+
if (open) {
|
|
99
|
+
calcPosition();
|
|
100
|
+
requestAnimationFrame(calcPosition);
|
|
101
|
+
}
|
|
102
|
+
}, [open, calcPosition]);
|
|
62
103
|
|
|
63
104
|
useEffect(() => {
|
|
64
105
|
if (open && searchable && inputRef.current) inputRef.current.focus();
|
|
@@ -85,8 +126,7 @@ export default function Select({
|
|
|
85
126
|
function handleOpen() {
|
|
86
127
|
if (disabled) return;
|
|
87
128
|
if (!open && wrapRef.current) {
|
|
88
|
-
|
|
89
|
-
setPos({ top: r.bottom + 4, left: r.left, width: r.width });
|
|
129
|
+
calcPosition();
|
|
90
130
|
}
|
|
91
131
|
setOpen(o => !o);
|
|
92
132
|
}
|
|
@@ -254,7 +294,7 @@ export default function Select({
|
|
|
254
294
|
borderRadius: radiusVal,
|
|
255
295
|
boxShadow: 'var(--shadow,0 1px 3px rgba(20,22,41,.1))',
|
|
256
296
|
padding: 3,
|
|
257
|
-
animation: '
|
|
297
|
+
animation: `${pos.openUp ? 'selFadeInUp' : 'selFadeInDown'} .15s ease`,
|
|
258
298
|
overflow: 'hidden',
|
|
259
299
|
}}
|
|
260
300
|
>
|
|
@@ -285,7 +325,7 @@ export default function Select({
|
|
|
285
325
|
/>
|
|
286
326
|
</div>
|
|
287
327
|
)}
|
|
288
|
-
<div style={{ maxHeight: 200, overflowY: 'auto' }}>
|
|
328
|
+
<div style={{ maxHeight: pos.maxHeight || 200, overflowY: 'auto' }}>
|
|
289
329
|
{filtered.length === 0 ? (
|
|
290
330
|
<div style={{ padding: '10px 8px', textAlign: 'center', color: 'var(--t3,var(--grey-3))', fontSize: 11 }}>
|
|
291
331
|
Ничего не найдено
|