cynx-ui 1.2.14 → 1.2.16
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/components/Dropdown.jsx +116 -108
- package/components/TabBar.jsx +7 -7
- package/package.json +1 -1
package/components/Dropdown.jsx
CHANGED
|
@@ -1,108 +1,116 @@
|
|
|
1
|
-
import { useState, useEffect, useRef, useCallback } from 'react';
|
|
2
|
-
import { createPortal } from 'react-dom';
|
|
3
|
-
import { ChevronDown, Check } from 'lucide-react';
|
|
4
|
-
|
|
5
|
-
export default function Dropdown({ value, onChange, options, placeholder = 'Select...', disabled }) {
|
|
6
|
-
const [open, setOpen] = useState(false);
|
|
7
|
-
const [rect, setRect] = useState(null);
|
|
8
|
-
const btnRef = useRef();
|
|
9
|
-
const menuRef = useRef();
|
|
10
|
-
|
|
11
|
-
const updateRect = useCallback(() => {
|
|
12
|
-
if (btnRef.current) setRect(btnRef.current.getBoundingClientRect());
|
|
13
|
-
}, []);
|
|
14
|
-
|
|
15
|
-
// Close on outside click
|
|
16
|
-
useEffect(() => {
|
|
17
|
-
if (!open) return;
|
|
18
|
-
function onDown(e) {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
}}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
{
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
}
|
|
1
|
+
import { useState, useEffect, useRef, useCallback } from 'react';
|
|
2
|
+
import { createPortal } from 'react-dom';
|
|
3
|
+
import { ChevronDown, Check } from 'lucide-react';
|
|
4
|
+
|
|
5
|
+
export default function Dropdown({ value, onChange, options, placeholder = 'Select...', disabled }) {
|
|
6
|
+
const [open, setOpen] = useState(false);
|
|
7
|
+
const [rect, setRect] = useState(null);
|
|
8
|
+
const btnRef = useRef();
|
|
9
|
+
const menuRef = useRef();
|
|
10
|
+
|
|
11
|
+
const updateRect = useCallback(() => {
|
|
12
|
+
if (btnRef.current) setRect(btnRef.current.getBoundingClientRect());
|
|
13
|
+
}, []);
|
|
14
|
+
|
|
15
|
+
// Close on outside click
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
if (!open) return;
|
|
18
|
+
function onDown(e) {
|
|
19
|
+
const inBtn = btnRef.current?.contains(e.target);
|
|
20
|
+
const inMenu = menuRef.current?.contains(e.target);
|
|
21
|
+
if (!inBtn && !inMenu) setOpen(false);
|
|
22
|
+
}
|
|
23
|
+
// Use setTimeout to avoid closing on the same click that opened
|
|
24
|
+
const timer = setTimeout(() => {
|
|
25
|
+
document.addEventListener('mousedown', onDown, true);
|
|
26
|
+
}, 0);
|
|
27
|
+
return () => {
|
|
28
|
+
clearTimeout(timer);
|
|
29
|
+
document.removeEventListener('mousedown', onDown, true);
|
|
30
|
+
};
|
|
31
|
+
}, [open]);
|
|
32
|
+
|
|
33
|
+
// Update position on scroll/resize while open
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
if (!open) return;
|
|
36
|
+
window.addEventListener('scroll', updateRect, true);
|
|
37
|
+
window.addEventListener('resize', updateRect);
|
|
38
|
+
return () => {
|
|
39
|
+
window.removeEventListener('scroll', updateRect, true);
|
|
40
|
+
window.removeEventListener('resize', updateRect);
|
|
41
|
+
};
|
|
42
|
+
}, [open, updateRect]);
|
|
43
|
+
|
|
44
|
+
function toggle() {
|
|
45
|
+
if (disabled) return;
|
|
46
|
+
updateRect();
|
|
47
|
+
setOpen(o => !o);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const selected = options.find(o => String(o.value) === String(value));
|
|
51
|
+
|
|
52
|
+
const menu = open && rect && createPortal(
|
|
53
|
+
<div
|
|
54
|
+
ref={menuRef}
|
|
55
|
+
data-dropdown="true"
|
|
56
|
+
style={{
|
|
57
|
+
position: 'fixed',
|
|
58
|
+
top: rect.bottom + 4,
|
|
59
|
+
left: rect.left,
|
|
60
|
+
width: rect.width,
|
|
61
|
+
background: 'var(--surface)',
|
|
62
|
+
border: '1px solid var(--border)',
|
|
63
|
+
borderRadius: 'var(--rads)',
|
|
64
|
+
boxShadow: 'var(--shadow-lg)',
|
|
65
|
+
zIndex: 999999,
|
|
66
|
+
animation: 'dropdownOpen .2s cubic-bezier(.16,1,.3,1)',
|
|
67
|
+
}}>
|
|
68
|
+
{options.map(o => {
|
|
69
|
+
const active = String(o.value) === String(value);
|
|
70
|
+
return (
|
|
71
|
+
<div key={o.value}
|
|
72
|
+
onMouseDown={e => e.stopPropagation()}
|
|
73
|
+
onClick={() => { onChange(o.value); setOpen(false); }}
|
|
74
|
+
onMouseEnter={e => { if (!active) e.currentTarget.style.background = 'var(--hover-bg)'; }}
|
|
75
|
+
onMouseLeave={e => { e.currentTarget.style.background = active ? 'var(--accent-bg)' : 'transparent'; }}
|
|
76
|
+
style={{
|
|
77
|
+
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
|
|
78
|
+
padding: '9px 12px', cursor: 'pointer', fontSize: '.82rem',
|
|
79
|
+
color: active ? 'var(--accent)' : 'var(--primary-text)',
|
|
80
|
+
background: active ? 'var(--accent-bg)' : 'transparent',
|
|
81
|
+
fontWeight: active ? 600 : 400,
|
|
82
|
+
}}>
|
|
83
|
+
<span>{o.label}</span>
|
|
84
|
+
{active && <Check size={12} />}
|
|
85
|
+
</div>
|
|
86
|
+
);
|
|
87
|
+
})}
|
|
88
|
+
</div>,
|
|
89
|
+
document.body
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
return (
|
|
93
|
+
<div ref={btnRef} style={{ position: 'relative' }}>
|
|
94
|
+
<button
|
|
95
|
+
type="button"
|
|
96
|
+
disabled={disabled}
|
|
97
|
+
onClick={toggle}
|
|
98
|
+
style={{
|
|
99
|
+
width: '100%', display: 'flex', alignItems: 'center', justifyContent: 'space-between',
|
|
100
|
+
padding: '8px 11px', background: 'var(--input-bg)',
|
|
101
|
+
border: `1px solid ${open ? 'var(--accent)' : 'var(--border)'}`,
|
|
102
|
+
boxShadow: open ? '0 0 0 3px rgba(249,158,44,.12)' : 'var(--shadow)',
|
|
103
|
+
borderRadius: 'var(--rads)', cursor: disabled ? 'not-allowed' : 'pointer',
|
|
104
|
+
color: selected ? 'var(--primary-text)' : 'var(--muted)',
|
|
105
|
+
fontSize: '.82rem', fontFamily: 'var(--sans)', transition: 'all .14s',
|
|
106
|
+
opacity: disabled ? .5 : 1, gap: 8,
|
|
107
|
+
}}>
|
|
108
|
+
<span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
|
109
|
+
{selected ? selected.label : placeholder}
|
|
110
|
+
</span>
|
|
111
|
+
<ChevronDown size={13} style={{ color: 'var(--muted)', flexShrink: 0, transform: open ? 'rotate(180deg)' : 'none', transition: 'transform .2s' }} />
|
|
112
|
+
</button>
|
|
113
|
+
{menu}
|
|
114
|
+
</div>
|
|
115
|
+
);
|
|
116
|
+
}
|
package/components/TabBar.jsx
CHANGED
|
@@ -54,10 +54,10 @@ export default function TabBar({ tabs, active, onChange, capitalize = false, pad
|
|
|
54
54
|
if (variant === 'pill') {
|
|
55
55
|
return (
|
|
56
56
|
<div style={{
|
|
57
|
-
display: 'flex', gap:
|
|
57
|
+
display: 'inline-flex', gap: 3,
|
|
58
58
|
background: 'var(--grey-1, #f5f5f8)',
|
|
59
|
-
borderRadius: 'var(--
|
|
60
|
-
padding: '
|
|
59
|
+
borderRadius: 'var(--radxs, 6px)',
|
|
60
|
+
padding: '3px',
|
|
61
61
|
border: '1px solid var(--border-light, #ebebf0)',
|
|
62
62
|
}}>
|
|
63
63
|
{tabs.map(t => {
|
|
@@ -68,13 +68,13 @@ export default function TabBar({ tabs, active, onChange, capitalize = false, pad
|
|
|
68
68
|
data-tabid={t.id}
|
|
69
69
|
onClick={() => onChange(t.id)}
|
|
70
70
|
style={{
|
|
71
|
-
display: 'flex', alignItems: 'center', gap:
|
|
72
|
-
padding: padding || '
|
|
71
|
+
display: 'flex', alignItems: 'center', gap: 5,
|
|
72
|
+
padding: padding || '5px 12px',
|
|
73
73
|
border: 'none',
|
|
74
74
|
borderRadius: 'var(--radxs, 6px)',
|
|
75
75
|
background: isActive ? '#fff' : 'transparent',
|
|
76
|
-
boxShadow: isActive ? '0 1px
|
|
77
|
-
fontSize: '.
|
|
76
|
+
boxShadow: isActive ? '0 1px 2px rgba(0,0,0,.06)' : 'none',
|
|
77
|
+
fontSize: '.75rem', fontWeight: isActive ? 600 : 500,
|
|
78
78
|
color: isActive ? 'var(--primary-text, #0f111c)' : 'var(--muted, #9b9daa)',
|
|
79
79
|
cursor: 'pointer',
|
|
80
80
|
transition: 'all .18s ease',
|