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.
@@ -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
- if (!btnRef.current?.contains(e.target) && !menuRef.current?.contains(e.target))
20
- setOpen(false);
21
- }
22
- document.addEventListener('mousedown', onDown);
23
- return () => document.removeEventListener('mousedown', onDown);
24
- }, [open]);
25
-
26
- // Update position on scroll/resize while open
27
- useEffect(() => {
28
- if (!open) return;
29
- window.addEventListener('scroll', updateRect, true);
30
- window.addEventListener('resize', updateRect);
31
- return () => {
32
- window.removeEventListener('scroll', updateRect, true);
33
- window.removeEventListener('resize', updateRect);
34
- };
35
- }, [open, updateRect]);
36
-
37
- function toggle() {
38
- if (disabled) return;
39
- updateRect();
40
- setOpen(o => !o);
41
- }
42
-
43
- const selected = options.find(o => String(o.value) === String(value));
44
-
45
- const menu = open && rect && createPortal(
46
- <div
47
- ref={menuRef}
48
- style={{
49
- position: 'fixed',
50
- top: rect.bottom + 4,
51
- left: rect.left,
52
- width: rect.width,
53
- background: 'var(--surface)',
54
- border: '1px solid var(--border)',
55
- borderRadius: 'var(--rads)',
56
- boxShadow: 'var(--shadow-lg)',
57
- zIndex: 999999,
58
- animation: 'dropdownOpen .2s cubic-bezier(.16,1,.3,1)',
59
- }}>
60
- {options.map(o => {
61
- const active = String(o.value) === String(value);
62
- return (
63
- <div key={o.value}
64
- onMouseDown={e => e.preventDefault()}
65
- onClick={() => { onChange(o.value); setOpen(false); }}
66
- onMouseEnter={e => { if (!active) e.currentTarget.style.background = 'var(--hover-bg)'; }}
67
- onMouseLeave={e => { e.currentTarget.style.background = active ? 'var(--accent-bg)' : 'transparent'; }}
68
- style={{
69
- display: 'flex', alignItems: 'center', justifyContent: 'space-between',
70
- padding: '9px 12px', cursor: 'pointer', fontSize: '.82rem',
71
- color: active ? 'var(--accent)' : 'var(--primary-text)',
72
- background: active ? 'var(--accent-bg)' : 'transparent',
73
- fontWeight: active ? 600 : 400,
74
- }}>
75
- <span>{o.label}</span>
76
- {active && <Check size={12} />}
77
- </div>
78
- );
79
- })}
80
- </div>,
81
- document.body
82
- );
83
-
84
- return (
85
- <div ref={btnRef} style={{ position: 'relative' }}>
86
- <button
87
- type="button"
88
- disabled={disabled}
89
- onClick={toggle}
90
- style={{
91
- width: '100%', display: 'flex', alignItems: 'center', justifyContent: 'space-between',
92
- padding: '8px 11px', background: 'var(--input-bg)',
93
- border: `1px solid ${open ? 'var(--accent)' : 'var(--border)'}`,
94
- boxShadow: open ? '0 0 0 3px rgba(249,158,44,.12)' : 'var(--shadow)',
95
- borderRadius: 'var(--rads)', cursor: disabled ? 'not-allowed' : 'pointer',
96
- color: selected ? 'var(--primary-text)' : 'var(--muted)',
97
- fontSize: '.82rem', fontFamily: 'var(--sans)', transition: 'all .14s',
98
- opacity: disabled ? .5 : 1, gap: 8,
99
- }}>
100
- <span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
101
- {selected ? selected.label : placeholder}
102
- </span>
103
- <ChevronDown size={13} style={{ color: 'var(--muted)', flexShrink: 0, transform: open ? 'rotate(180deg)' : 'none', transition: 'transform .2s' }} />
104
- </button>
105
- {menu}
106
- </div>
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
+ }
@@ -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: 4,
57
+ display: 'inline-flex', gap: 3,
58
58
  background: 'var(--grey-1, #f5f5f8)',
59
- borderRadius: 'var(--rads, 8px)',
60
- padding: '4px',
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: 6,
72
- padding: padding || '8px 16px',
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 3px rgba(0,0,0,.08)' : 'none',
77
- fontSize: '.82rem', fontWeight: isActive ? 600 : 500,
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',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cynx-ui",
3
- "version": "1.2.14",
3
+ "version": "1.2.16",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./index.js"