cynx-ui 1.2.16 → 1.2.18

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,149 +1,149 @@
1
- import { useState, useRef, useEffect, useCallback } from 'react';
2
-
3
- export function ActionsMenu({ items, align = 'right' }) {
4
- const [open, setOpen] = useState(false);
5
- const [closing, setClosing] = useState(false);
6
- const wrapRef = useRef(null);
7
- const menuRef = useRef(null);
8
- const btnRef = useRef(null);
9
-
10
- const close = useCallback(() => {
11
- setClosing(true);
12
- setTimeout(() => { setClosing(false); setOpen(false); }, 140);
13
- }, []);
14
-
15
- useEffect(() => {
16
- if (!open) return;
17
- function onDocClick(e) {
18
- if (wrapRef.current && !wrapRef.current.contains(e.target)) close();
19
- }
20
- function onScroll() { if (menuRef.current) positionMenu(); }
21
- document.addEventListener('mousedown', onDocClick);
22
- document.addEventListener('scroll', onScroll, true);
23
- window.addEventListener('resize', onScroll);
24
- return () => {
25
- document.removeEventListener('mousedown', onDocClick);
26
- document.removeEventListener('scroll', onScroll, true);
27
- window.removeEventListener('resize', onScroll);
28
- };
29
- }, [open, close]);
30
-
31
- function positionMenu() {
32
- const btn = btnRef.current;
33
- const menu = menuRef.current;
34
- if (!btn || !menu) return;
35
- const br = btn.getBoundingClientRect();
36
- const mw = menu.offsetWidth || 180;
37
- const mh = menu.offsetHeight || 120;
38
- const spaceBelow = window.innerHeight - br.bottom;
39
- const spaceAbove = br.top;
40
- const openUp = spaceBelow < mh + 12 && spaceAbove > spaceBelow;
41
-
42
- let top, left;
43
- if (openUp) {
44
- top = br.top + window.scrollY - mh - 6;
45
- } else {
46
- top = br.bottom + window.scrollY + 6;
47
- }
48
- if (align === 'left') {
49
- left = br.left + window.scrollX;
50
- } else {
51
- left = br.right + window.scrollX - mw;
52
- }
53
- left = Math.max(8, Math.min(left, window.innerWidth - mw - 8));
54
-
55
- menu.style.top = top + 'px';
56
- menu.style.left = left + 'px';
57
- menu.classList.toggle('open-up', openUp);
58
- }
59
-
60
- useEffect(() => {
61
- if (open) requestAnimationFrame(positionMenu);
62
- }, [open]);
63
-
64
- function toggle() {
65
- if (open) close();
66
- else { setOpen(true); setClosing(false); }
67
- }
68
-
69
- const isOpen = open && !closing;
70
-
71
- return (
72
- <div ref={wrapRef} style={{ position: 'relative', display: 'inline-block' }}>
73
- <button
74
- ref={btnRef}
75
- type="button"
76
- onClick={toggle}
77
- style={{
78
- width: 30, height: 30, borderRadius: 'var(--rads,8px)',
79
- border: isOpen ? '1px solid var(--accent,#f99e2c)' : '1px solid var(--border-light,#d7d8e0)',
80
- background: '#fff',
81
- cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center',
82
- color: isOpen ? 'var(--accent)' : 'var(--muted,#797b8d)', transition: 'all .14s',
83
- boxShadow: isOpen ? '0 0 0 3px rgba(249,158,44,.12)' : '0 1px 3px rgba(0,0,0,.08), 0 1px 2px rgba(0,0,0,.06)',
84
- }}
85
- >
86
- <svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
87
- <circle cx="5" cy="12" r="1.5" /><circle cx="12" cy="12" r="1.5" /><circle cx="19" cy="12" r="1.5" />
88
- </svg>
89
- </button>
90
-
91
- {open && (
92
- <div
93
- ref={menuRef}
94
- style={{
95
- position: 'fixed', zIndex: 8100, minWidth: 180, padding: 5,
96
- background: '#fff', border: '1.5px solid var(--border-light,#e2e8f0)',
97
- borderRadius: 'var(--rads,8px)',
98
- boxShadow: '0 10px 15px -3px rgba(0,0,0,.1), 0 4px 6px -4px rgba(0,0,0,.06)',
99
- animation: closing
100
- ? 'actionsClose .14s ease forwards'
101
- : 'actionsDrop .14s cubic-bezier(.16,1,.3,1)',
102
- }}
103
- >
104
- {items.map((item, i) => (
105
- item.dividerBefore ? (
106
- <div key={i} style={{ height: 1, background: 'var(--border-light,#e2e8f0)', margin: '4px 0' }} />
107
- ) : (
108
- <div
109
- key={i}
110
- onClick={() => { close(); item.onClick?.(); }}
111
- style={{
112
- display: 'flex', alignItems: 'center', gap: 8,
113
- padding: '6px 10px', borderRadius: 'var(--rads,8px)',
114
- fontSize: 12, cursor: 'pointer', transition: 'all .14s',
115
- color: item.variant === 'danger' ? 'var(--danger)' : item.variant === 'success' ? 'var(--success)' : item.variant === 'warning' ? 'var(--accent)' : 'var(--text,#1a1a2e)',
116
- fontWeight: 500,
117
- }}
118
- onMouseEnter={e => {
119
- e.currentTarget.style.background = item.variant === 'danger' ? 'rgba(246,71,71,.08)' : item.variant === 'warning' ? 'rgba(249,158,44,.08)' : 'var(--grey-1,#f5f5f8)';
120
- }}
121
- onMouseLeave={e => { e.currentTarget.style.background = 'transparent'; }}
122
- >
123
- {item.icon && (
124
- <span style={{
125
- width: 24, height: 24, borderRadius: 5,
126
- display: 'flex', alignItems: 'center', justifyContent: 'center',
127
- flexShrink: 0, color: 'inherit', fontSize: 12,
128
- }}>{item.icon}</span>
129
- )}
130
- <span>{item.label}</span>
131
- </div>
132
- )
133
- ))}
134
- </div>
135
- )}
136
-
137
- <style>{`
138
- @keyframes actionsDrop {
139
- from { opacity: 0; transform: translateY(-6px) scaleY(.96); }
140
- to { opacity: 1; transform: translateY(0) scaleY(1); }
141
- }
142
- @keyframes actionsClose {
143
- from { opacity: 1; transform: translateY(0) scaleY(1); }
144
- to { opacity: 0; transform: translateY(-4px) scaleY(.96); }
145
- }
146
- `}</style>
147
- </div>
148
- );
149
- }
1
+ import { useState, useRef, useEffect, useCallback } from 'react';
2
+
3
+ export function ActionsMenu({ items, align = 'right' }) {
4
+ const [open, setOpen] = useState(false);
5
+ const [closing, setClosing] = useState(false);
6
+ const wrapRef = useRef(null);
7
+ const menuRef = useRef(null);
8
+ const btnRef = useRef(null);
9
+
10
+ const close = useCallback(() => {
11
+ setClosing(true);
12
+ setTimeout(() => { setClosing(false); setOpen(false); }, 140);
13
+ }, []);
14
+
15
+ useEffect(() => {
16
+ if (!open) return;
17
+ function onDocClick(e) {
18
+ if (wrapRef.current && !wrapRef.current.contains(e.target)) close();
19
+ }
20
+ function onScroll() { if (menuRef.current) positionMenu(); }
21
+ document.addEventListener('mousedown', onDocClick);
22
+ document.addEventListener('scroll', onScroll, true);
23
+ window.addEventListener('resize', onScroll);
24
+ return () => {
25
+ document.removeEventListener('mousedown', onDocClick);
26
+ document.removeEventListener('scroll', onScroll, true);
27
+ window.removeEventListener('resize', onScroll);
28
+ };
29
+ }, [open, close]);
30
+
31
+ function positionMenu() {
32
+ const btn = btnRef.current;
33
+ const menu = menuRef.current;
34
+ if (!btn || !menu) return;
35
+ const br = btn.getBoundingClientRect();
36
+ const mw = menu.offsetWidth || 180;
37
+ const mh = menu.offsetHeight || 120;
38
+ const spaceBelow = window.innerHeight - br.bottom;
39
+ const spaceAbove = br.top;
40
+ const openUp = spaceBelow < mh + 12 && spaceAbove > spaceBelow;
41
+
42
+ let top, left;
43
+ if (openUp) {
44
+ top = br.top + window.scrollY - mh - 6;
45
+ } else {
46
+ top = br.bottom + window.scrollY + 6;
47
+ }
48
+ if (align === 'left') {
49
+ left = br.left + window.scrollX;
50
+ } else {
51
+ left = br.right + window.scrollX - mw;
52
+ }
53
+ left = Math.max(8, Math.min(left, window.innerWidth - mw - 8));
54
+
55
+ menu.style.top = top + 'px';
56
+ menu.style.left = left + 'px';
57
+ menu.classList.toggle('open-up', openUp);
58
+ }
59
+
60
+ useEffect(() => {
61
+ if (open) requestAnimationFrame(positionMenu);
62
+ }, [open]);
63
+
64
+ function toggle() {
65
+ if (open) close();
66
+ else { setOpen(true); setClosing(false); }
67
+ }
68
+
69
+ const isOpen = open && !closing;
70
+
71
+ return (
72
+ <div ref={wrapRef} style={{ position: 'relative', display: 'inline-block' }}>
73
+ <button
74
+ ref={btnRef}
75
+ type="button"
76
+ onClick={toggle}
77
+ style={{
78
+ width: 30, height: 30, borderRadius: 'var(--rads,8px)',
79
+ border: isOpen ? '1px solid var(--accent,#f99e2c)' : '1px solid var(--border-light,#d7d8e0)',
80
+ background: '#fff',
81
+ cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center',
82
+ color: isOpen ? 'var(--accent)' : 'var(--muted,#797b8d)', transition: 'all .14s',
83
+ boxShadow: isOpen ? '0 0 0 3px rgba(249,158,44,.12)' : '0 1px 3px rgba(0,0,0,.08), 0 1px 2px rgba(0,0,0,.06)',
84
+ }}
85
+ >
86
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
87
+ <circle cx="5" cy="12" r="1.5" /><circle cx="12" cy="12" r="1.5" /><circle cx="19" cy="12" r="1.5" />
88
+ </svg>
89
+ </button>
90
+
91
+ {open && (
92
+ <div
93
+ ref={menuRef}
94
+ style={{
95
+ position: 'fixed', zIndex: 8100, minWidth: 180, padding: 5,
96
+ background: '#fff', border: '1.5px solid var(--border-light,#e2e8f0)',
97
+ borderRadius: 'var(--rads,8px)',
98
+ boxShadow: '0 10px 15px -3px rgba(0,0,0,.1), 0 4px 6px -4px rgba(0,0,0,.06)',
99
+ animation: closing
100
+ ? 'actionsClose .14s ease forwards'
101
+ : 'actionsDrop .14s cubic-bezier(.16,1,.3,1)',
102
+ }}
103
+ >
104
+ {items.map((item, i) => (
105
+ item.dividerBefore ? (
106
+ <div key={i} style={{ height: 1, background: 'var(--border-light,#e2e8f0)', margin: '4px 0' }} />
107
+ ) : (
108
+ <div
109
+ key={i}
110
+ onClick={() => { close(); item.onClick?.(); }}
111
+ style={{
112
+ display: 'flex', alignItems: 'center', gap: 8,
113
+ padding: '6px 10px', borderRadius: 'var(--rads,8px)',
114
+ fontSize: 12, cursor: 'pointer', transition: 'all .14s',
115
+ color: item.variant === 'danger' ? 'var(--danger)' : item.variant === 'success' ? 'var(--success)' : item.variant === 'warning' ? 'var(--accent)' : 'var(--text,#1a1a2e)',
116
+ fontWeight: 500,
117
+ }}
118
+ onMouseEnter={e => {
119
+ e.currentTarget.style.background = item.variant === 'danger' ? 'rgba(246,71,71,.08)' : item.variant === 'warning' ? 'rgba(249,158,44,.08)' : 'var(--grey-1,#f5f5f8)';
120
+ }}
121
+ onMouseLeave={e => { e.currentTarget.style.background = 'transparent'; }}
122
+ >
123
+ {item.icon && (
124
+ <span style={{
125
+ width: 24, height: 24, borderRadius: 5,
126
+ display: 'flex', alignItems: 'center', justifyContent: 'center',
127
+ flexShrink: 0, color: 'inherit', fontSize: 12,
128
+ }}>{item.icon}</span>
129
+ )}
130
+ <span>{item.label}</span>
131
+ </div>
132
+ )
133
+ ))}
134
+ </div>
135
+ )}
136
+
137
+ <style>{`
138
+ @keyframes actionsDrop {
139
+ from { opacity: 0; transform: translateY(-6px) scaleY(.96); }
140
+ to { opacity: 1; transform: translateY(0) scaleY(1); }
141
+ }
142
+ @keyframes actionsClose {
143
+ from { opacity: 1; transform: translateY(0) scaleY(1); }
144
+ to { opacity: 0; transform: translateY(-4px) scaleY(.96); }
145
+ }
146
+ `}</style>
147
+ </div>
148
+ );
149
+ }