cynx-lib 1.3.89 → 1.3.91

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cynx-lib",
3
- "version": "1.3.89",
3
+ "version": "1.3.91",
4
4
  "type": "module",
5
5
  "main": "./index.cjs",
6
6
  "module": "./index.js",
@@ -1,158 +1,351 @@
1
- import { useState, useRef, useEffect, useCallback, useLayoutEffect } from 'react';
2
- import { createPortal } from 'react-dom';
3
-
4
- export function ActionsMenu({ items, align = 'right' }) {
5
- const [open, setOpen] = useState(false);
6
- const [closing, setClosing] = useState(false);
7
- const wrapRef = useRef(null);
8
- const menuRef = useRef(null);
9
- const btnRef = useRef(null);
10
-
11
- const close = useCallback(() => {
12
- setClosing(true);
13
- setTimeout(() => { setClosing(false); setOpen(false); }, 140);
14
- }, []);
15
-
16
- const positionMenu = useCallback(() => {
17
- const btn = btnRef.current;
18
- const menu = menuRef.current;
19
- if (!btn || !menu) return;
20
- const br = btn.getBoundingClientRect();
21
- const mw = menu.offsetWidth || 180;
22
- const mh = menu.offsetHeight || 120;
23
- const spaceBelow = window.innerHeight - br.bottom;
24
- const spaceAbove = br.top;
25
- const openUp = spaceBelow < mh + 12 && spaceAbove > spaceBelow;
26
-
27
- let top, left;
28
- if (openUp) {
29
- top = br.top - mh - 6;
30
- } else {
31
- top = br.bottom + 6;
32
- }
33
- if (align === 'left') {
34
- left = br.left;
35
- } else {
36
- left = br.right - mw;
37
- }
38
- left = Math.max(8, Math.min(left, window.innerWidth - mw - 8));
39
-
40
- menu.style.top = top + 'px';
41
- menu.style.left = left + 'px';
42
- menu.classList.toggle('open-up', openUp);
43
- }, [align]);
44
-
45
- useEffect(() => {
46
- if (!open) return;
47
- function onDocClick(e) {
48
- if (
49
- wrapRef.current && !wrapRef.current.contains(e.target) &&
50
- menuRef.current && !menuRef.current.contains(e.target)
51
- ) {
52
- close();
53
- }
54
- }
55
- function onScroll() { positionMenu(); }
56
- document.addEventListener('mousedown', onDocClick);
57
- document.addEventListener('scroll', onScroll, true);
58
- window.addEventListener('resize', onScroll);
59
- return () => {
60
- document.removeEventListener('mousedown', onDocClick);
61
- document.removeEventListener('scroll', onScroll, true);
62
- window.removeEventListener('resize', onScroll);
63
- };
64
- }, [open, close, positionMenu]);
65
-
66
- useLayoutEffect(() => {
67
- if (open) {
68
- positionMenu();
69
- }
70
- }, [open, positionMenu]);
71
-
72
- function toggle() {
73
- if (open) close();
74
- else { setOpen(true); setClosing(false); }
75
- }
76
-
77
- const isOpen = open && !closing;
78
-
79
- return (
80
- <div ref={wrapRef} style={{ position: 'relative', display: 'inline-block' }}>
81
- <button
82
- ref={btnRef}
83
- type="button"
84
- onClick={toggle}
85
- style={{
86
- width: 32, height: 32, borderRadius: 'var(--rads,8px)',
87
- border: isOpen ? '1px solid var(--accent,#f99e2c)' : '1px solid var(--border-light,#d7d8e0)',
88
- background: 'var(--surface,#fff)',
89
- cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center',
90
- color: isOpen ? 'var(--accent, #f99e2c)' : 'var(--muted,#797b8d)', transition: 'all .14s',
91
- boxShadow: isOpen ? 'var(--pd, rgba(249,158,44,.12))' : 'var(--shadow, 0 1px 3px rgba(0,0,0,.08), 0 1px 2px rgba(0,0,0,.06))',
92
- }}
93
- >
94
- <svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
95
- <circle cx="5" cy="12" r="1.5" /><circle cx="12" cy="12" r="1.5" /><circle cx="19" cy="12" r="1.5" />
96
- </svg>
97
- </button>
98
-
99
- {open && typeof document !== 'undefined' && createPortal(
100
- <div
101
- ref={menuRef}
102
- style={{
103
- position: 'fixed', zIndex: 99999, minWidth: 180, padding: 5,
104
- background: 'var(--surface,#fff)', border: '1.5px solid var(--border-light,#e2e8f0)',
105
- borderRadius: 'var(--rads,8px)',
106
- boxShadow: 'var(--shadow-lg, 0 10px 15px -3px rgba(0,0,0,.1), 0 4px 6px -4px rgba(0,0,0,.06))',
107
- animation: closing
108
- ? 'actionsClose .14s ease forwards'
109
- : 'actionsDrop .14s cubic-bezier(.16,1,.3,1)',
110
- }}
111
- >
112
- {items.map((item, i) => (
113
- item.dividerBefore ? (
114
- <div key={i} style={{ height: 1, background: 'var(--border-light,#e2e8f0)', margin: '4px 0' }} />
115
- ) : (
116
- <div
117
- key={i}
118
- onClick={() => { close(); item.onClick?.(); }}
119
- style={{
120
- display: 'flex', alignItems: 'center', gap: 8,
121
- padding: '6px 10px', borderRadius: 'var(--rads,8px)',
122
- fontSize: 12, cursor: 'pointer', transition: 'all .14s',
123
- color: (item.variant === 'danger' || item.danger) ? 'var(--danger, #ef4444)' : item.variant === 'success' ? 'var(--success, #16a34a)' : item.variant === 'warning' ? 'var(--warning, #f59e0b)' : 'var(--primary-text, #18181b)',
124
- fontWeight: 500,
125
- }}
126
- onMouseEnter={e => {
127
- e.currentTarget.style.background = (item.variant === 'danger' || item.danger) ? 'var(--danger-bg, rgba(239,68,68,.10))' : item.variant === 'success' ? 'var(--success-bg, rgba(22,163,74,.10))' : item.variant === 'warning' ? 'var(--warning-bg, rgba(245,158,11,.12))' : 'var(--grey-1, #f4f4f5)';
128
- }}
129
- onMouseLeave={e => { e.currentTarget.style.background = 'transparent'; }}
130
- >
131
- {item.icon && (
132
- <span style={{
133
- width: 24, height: 24, borderRadius: 5,
134
- display: 'flex', alignItems: 'center', justifyContent: 'center',
135
- flexShrink: 0, color: 'inherit', fontSize: 12,
136
- }}>{item.icon}</span>
137
- )}
138
- <span>{item.label}</span>
139
- </div>
140
- )
141
- ))}
142
- </div>,
143
- document.body
144
- )}
145
-
146
- <style>{`
147
- @keyframes actionsDrop {
148
- from { opacity: 0; transform: translateY(-6px) scaleY(.96); }
149
- to { opacity: 1; transform: translateY(0) scaleY(1); }
150
- }
151
- @keyframes actionsClose {
152
- from { opacity: 1; transform: translateY(0) scaleY(1); }
153
- to { opacity: 0; transform: translateY(-4px) scaleY(.96); }
154
- }
155
- `}</style>
156
- </div>
157
- );
158
- }
1
+ import { useState, useRef, useEffect, useCallback, useLayoutEffect } from 'react';
2
+ import { createPortal } from 'react-dom';
3
+
4
+ function ActionSubmenu({ items, parentRect, closeAll, onMouseEnter, onMouseLeave }) {
5
+ const subRef = useRef(null);
6
+ const [pos, setPos] = useState({
7
+ top: parentRect ? parentRect.top - 5 : 0,
8
+ left: parentRect ? parentRect.right + 4 : 0,
9
+ transformOrigin: 'top left',
10
+ });
11
+
12
+ useLayoutEffect(() => {
13
+ if (!subRef.current || !parentRect) return;
14
+ const subWidth = subRef.current.offsetWidth || 190;
15
+ const subHeight = subRef.current.offsetHeight || 150;
16
+
17
+ const spaceRight = window.innerWidth - parentRect.right;
18
+ let left;
19
+ let transformOriginX = 'left';
20
+ if (spaceRight >= subWidth + 12) {
21
+ left = parentRect.right + 4;
22
+ transformOriginX = 'left';
23
+ } else {
24
+ left = parentRect.left - subWidth - 4;
25
+ transformOriginX = 'right';
26
+ }
27
+ left = Math.max(8, Math.min(left, window.innerWidth - subWidth - 8));
28
+
29
+ let top = parentRect.top - 5;
30
+ let transformOriginY = 'top';
31
+ if (top + subHeight > window.innerHeight - 10) {
32
+ top = Math.max(10, window.innerHeight - subHeight - 10);
33
+ transformOriginY = 'bottom';
34
+ }
35
+
36
+ setPos({
37
+ top,
38
+ left,
39
+ transformOrigin: `${transformOriginY} ${transformOriginX}`,
40
+ });
41
+ }, [parentRect]);
42
+
43
+ return createPortal(
44
+ <div
45
+ ref={subRef}
46
+ data-cynx-submenu="true"
47
+ onMouseEnter={onMouseEnter}
48
+ onMouseLeave={onMouseLeave}
49
+ style={{
50
+ position: 'fixed',
51
+ zIndex: 100000,
52
+ minWidth: 190,
53
+ padding: 5,
54
+ top: pos.top + 'px',
55
+ left: pos.left + 'px',
56
+ transformOrigin: pos.transformOrigin,
57
+ background: 'var(--surface,#fff)',
58
+ border: '1.5px solid var(--border-light,#e2e8f0)',
59
+ borderRadius: 'var(--rads,8px)',
60
+ boxShadow: 'var(--shadow-lg, 0 10px 15px -3px rgba(0,0,0,.1), 0 4px 6px -4px rgba(0,0,0,.06))',
61
+ animation: 'actionsDrop .12s cubic-bezier(.16,1,.3,1)',
62
+ }}
63
+ >
64
+ {items.map((subItem, j) => (
65
+ subItem.dividerBefore ? (
66
+ <div key={j} style={{ height: 1, background: 'var(--border-light,#e2e8f0)', margin: '4px 0' }} />
67
+ ) : (
68
+ <div
69
+ key={j}
70
+ onClick={() => { closeAll(); subItem.onClick?.(); }}
71
+ style={{
72
+ display: 'flex', alignItems: 'center', gap: 8,
73
+ padding: '6px 10px', borderRadius: 'var(--rads,8px)',
74
+ fontSize: 12, cursor: 'pointer', transition: 'all .14s',
75
+ color: (subItem.variant === 'danger' || subItem.danger) ? 'var(--danger, #ef4444)' : subItem.variant === 'success' ? 'var(--success, #16a34a)' : subItem.variant === 'warning' ? 'var(--warning, #f59e0b)' : 'var(--primary-text, #18181b)',
76
+ fontWeight: 500,
77
+ }}
78
+ onMouseEnter={e => {
79
+ e.currentTarget.style.background = (subItem.variant === 'danger' || subItem.danger) ? 'var(--danger-bg, rgba(239,68,68,.10))' : subItem.variant === 'success' ? 'var(--success-bg, rgba(22,163,74,.10))' : subItem.variant === 'warning' ? 'var(--warning-bg, rgba(245,158,11,.12))' : 'var(--grey-1, #f4f4f5)';
80
+ }}
81
+ onMouseLeave={e => { e.currentTarget.style.background = 'transparent'; }}
82
+ >
83
+ {subItem.icon && (
84
+ <span style={{
85
+ width: 24, height: 24, borderRadius: 5,
86
+ display: 'flex', alignItems: 'center', justifyContent: 'center',
87
+ flexShrink: 0, color: 'inherit', fontSize: 12,
88
+ }}>{subItem.icon}</span>
89
+ )}
90
+ <span>{subItem.label}</span>
91
+ </div>
92
+ )
93
+ ))}
94
+ </div>,
95
+ document.body
96
+ );
97
+ }
98
+
99
+ export function ActionsMenu({ items, align = 'right' }) {
100
+ const [open, setOpen] = useState(false);
101
+ const [closing, setClosing] = useState(false);
102
+ const [activeSub, setActiveSub] = useState(null); // { index, items, rect }
103
+ const wrapRef = useRef(null);
104
+ const menuRef = useRef(null);
105
+ const btnRef = useRef(null);
106
+ const subTimerRef = useRef(null);
107
+
108
+ const close = useCallback(() => {
109
+ setClosing(true);
110
+ setActiveSub(null);
111
+ clearTimeout(subTimerRef.current);
112
+ setTimeout(() => { setClosing(false); setOpen(false); }, 140);
113
+ }, []);
114
+
115
+ const openSub = (index, subItems, e) => {
116
+ clearTimeout(subTimerRef.current);
117
+ const rect = e.currentTarget.getBoundingClientRect();
118
+ setActiveSub({ index, items: subItems, rect });
119
+ };
120
+
121
+ const scheduleCloseSub = () => {
122
+ clearTimeout(subTimerRef.current);
123
+ subTimerRef.current = setTimeout(() => {
124
+ setActiveSub(null);
125
+ }, 150);
126
+ };
127
+
128
+ const cancelCloseSub = () => {
129
+ clearTimeout(subTimerRef.current);
130
+ };
131
+
132
+ const positionMenu = useCallback(() => {
133
+ const btn = btnRef.current;
134
+ const menu = menuRef.current;
135
+ if (!btn || !menu) return;
136
+ const br = btn.getBoundingClientRect();
137
+ const mw = menu.offsetWidth || 180;
138
+ const mh = menu.offsetHeight || 120;
139
+ const spaceBelow = window.innerHeight - br.bottom;
140
+ const spaceAbove = br.top;
141
+ const openUp = spaceBelow < mh + 12 && spaceAbove > spaceBelow;
142
+
143
+ let top, left;
144
+ if (openUp) {
145
+ top = br.top - mh - 6;
146
+ } else {
147
+ top = br.bottom + 6;
148
+ }
149
+ if (align === 'left') {
150
+ left = br.left;
151
+ } else {
152
+ left = br.right - mw;
153
+ }
154
+ left = Math.max(8, Math.min(left, window.innerWidth - mw - 8));
155
+
156
+ menu.style.top = top + 'px';
157
+ menu.style.left = left + 'px';
158
+ menu.style.transformOrigin = openUp
159
+ ? (align === 'left' ? 'bottom left' : 'bottom right')
160
+ : (align === 'left' ? 'top left' : 'top right');
161
+ menu.classList.toggle('open-up', openUp);
162
+ if (!closing) {
163
+ menu.style.animationName = openUp ? 'actionsDropUp' : 'actionsDrop';
164
+ }
165
+ }, [align, closing]);
166
+
167
+ useEffect(() => {
168
+ if (!open) return;
169
+ function onDocClick(e) {
170
+ if (
171
+ wrapRef.current && !wrapRef.current.contains(e.target) &&
172
+ menuRef.current && !menuRef.current.contains(e.target) &&
173
+ !e.target.closest?.('[data-cynx-submenu]')
174
+ ) {
175
+ close();
176
+ }
177
+ }
178
+ function onScroll() { positionMenu(); }
179
+ document.addEventListener('mousedown', onDocClick);
180
+ document.addEventListener('scroll', onScroll, true);
181
+ window.addEventListener('resize', onScroll);
182
+ return () => {
183
+ document.removeEventListener('mousedown', onDocClick);
184
+ document.removeEventListener('scroll', onScroll, true);
185
+ window.removeEventListener('resize', onScroll);
186
+ };
187
+ }, [open, close, positionMenu]);
188
+
189
+ useLayoutEffect(() => {
190
+ if (open) {
191
+ positionMenu();
192
+ }
193
+ }, [open, positionMenu]);
194
+
195
+ function toggle() {
196
+ if (open) close();
197
+ else { setOpen(true); setClosing(false); setActiveSub(null); }
198
+ }
199
+
200
+ const isOpen = open && !closing;
201
+
202
+ return (
203
+ <div ref={wrapRef} style={{ position: 'relative', display: 'inline-block' }}>
204
+ <button
205
+ ref={btnRef}
206
+ type="button"
207
+ onClick={toggle}
208
+ style={{
209
+ width: 32, height: 32, borderRadius: 'var(--rads,8px)',
210
+ border: isOpen ? '1px solid var(--accent,#f99e2c)' : '1px solid var(--border-light,#d7d8e0)',
211
+ background: 'var(--surface,#fff)',
212
+ cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center',
213
+ color: isOpen ? 'var(--accent, #f99e2c)' : 'var(--muted,#797b8d)', transition: 'all .14s',
214
+ boxShadow: isOpen ? 'var(--pd, rgba(249,158,44,.12))' : 'var(--shadow, 0 1px 3px rgba(0,0,0,.08), 0 1px 2px rgba(0,0,0,.06))',
215
+ }}
216
+ >
217
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
218
+ <circle cx="5" cy="12" r="1.5" /><circle cx="12" cy="12" r="1.5" /><circle cx="19" cy="12" r="1.5" />
219
+ </svg>
220
+ </button>
221
+
222
+ {open && typeof document !== 'undefined' && createPortal(
223
+ <div
224
+ ref={menuRef}
225
+ style={{
226
+ position: 'fixed', zIndex: 99999, minWidth: 180, padding: 5,
227
+ background: 'var(--surface,#fff)', border: '1.5px solid var(--border-light,#e2e8f0)',
228
+ borderRadius: 'var(--rads,8px)',
229
+ boxShadow: 'var(--shadow-lg, 0 10px 15px -3px rgba(0,0,0,.1), 0 4px 6px -4px rgba(0,0,0,.06))',
230
+ transformOrigin: align === 'left' ? 'top left' : 'top right',
231
+ animation: closing
232
+ ? 'actionsClose .14s ease forwards'
233
+ : 'actionsDrop .14s cubic-bezier(.16,1,.3,1)',
234
+ }}
235
+ >
236
+ {items.map((item, i) => {
237
+ if (item.dividerBefore) {
238
+ return <div key={i} style={{ height: 1, background: 'var(--border-light,#e2e8f0)', margin: '4px 0' }} />;
239
+ }
240
+
241
+ const hasSub = Array.isArray(item.items) && item.items.length > 0;
242
+ const isSubActive = activeSub && activeSub.index === i;
243
+
244
+ return (
245
+ <div
246
+ key={i}
247
+ onClick={(e) => {
248
+ if (hasSub) {
249
+ e.stopPropagation();
250
+ openSub(i, item.items, e);
251
+ } else {
252
+ close();
253
+ item.onClick?.();
254
+ }
255
+ }}
256
+ onMouseEnter={(e) => {
257
+ if (hasSub) {
258
+ openSub(i, item.items, e);
259
+ } else {
260
+ clearTimeout(subTimerRef.current);
261
+ setActiveSub(null);
262
+ }
263
+ e.currentTarget.style.background = (item.variant === 'danger' || item.danger)
264
+ ? 'var(--danger-bg, rgba(239,68,68,.10))'
265
+ : item.variant === 'success'
266
+ ? 'var(--success-bg, rgba(22,163,74,.10))'
267
+ : item.variant === 'warning'
268
+ ? 'var(--warning-bg, rgba(245,158,11,.12))'
269
+ : 'var(--grey-1, #f4f4f5)';
270
+ }}
271
+ onMouseLeave={(e) => {
272
+ if (hasSub) {
273
+ scheduleCloseSub();
274
+ }
275
+ if (!isSubActive) {
276
+ e.currentTarget.style.background = 'transparent';
277
+ }
278
+ }}
279
+ style={{
280
+ display: 'flex', alignItems: 'center', gap: 8,
281
+ padding: '6px 10px', borderRadius: 'var(--rads,8px)',
282
+ fontSize: 12, cursor: 'pointer', transition: 'all .14s',
283
+ background: isSubActive ? 'var(--grey-1, #f4f4f5)' : 'transparent',
284
+ color: (item.variant === 'danger' || item.danger)
285
+ ? 'var(--danger, #ef4444)'
286
+ : item.variant === 'success'
287
+ ? 'var(--success, #16a34a)'
288
+ : item.variant === 'warning'
289
+ ? 'var(--warning, #f59e0b)'
290
+ : 'var(--primary-text, #18181b)',
291
+ fontWeight: 500,
292
+ }}
293
+ >
294
+ {item.icon && (
295
+ <span style={{
296
+ width: 24, height: 24, borderRadius: 5,
297
+ display: 'flex', alignItems: 'center', justifyContent: 'center',
298
+ flexShrink: 0, color: 'inherit', fontSize: 12,
299
+ }}>{item.icon}</span>
300
+ )}
301
+ <span>{item.label}</span>
302
+
303
+ {hasSub && (
304
+ <svg
305
+ width="12"
306
+ height="12"
307
+ viewBox="0 0 24 24"
308
+ fill="none"
309
+ stroke="currentColor"
310
+ strokeWidth="2.5"
311
+ strokeLinecap="round"
312
+ strokeLinejoin="round"
313
+ style={{ marginLeft: 'auto', opacity: 0.55 }}
314
+ >
315
+ <polyline points="9 18 15 12 9 6" />
316
+ </svg>
317
+ )}
318
+ </div>
319
+ );
320
+ })}
321
+ </div>,
322
+ document.body
323
+ )}
324
+
325
+ {open && activeSub && (
326
+ <ActionSubmenu
327
+ items={activeSub.items}
328
+ parentRect={activeSub.rect}
329
+ closeAll={close}
330
+ onMouseEnter={cancelCloseSub}
331
+ onMouseLeave={scheduleCloseSub}
332
+ />
333
+ )}
334
+
335
+ <style>{`
336
+ @keyframes actionsDrop {
337
+ from { opacity: 0; transform: translateY(-6px); }
338
+ to { opacity: 1; transform: translateY(0); }
339
+ }
340
+ @keyframes actionsDropUp {
341
+ from { opacity: 0; transform: translateY(6px); }
342
+ to { opacity: 1; transform: translateY(0); }
343
+ }
344
+ @keyframes actionsClose {
345
+ from { opacity: 1; transform: translateY(0); }
346
+ to { opacity: 0; transform: translateY(-4px); }
347
+ }
348
+ `}</style>
349
+ </div>
350
+ );
351
+ }