cynx-lib 1.3.91 → 1.3.92
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 +1 -1
- package/ui/components/ActionsMenu.jsx +40 -34
package/package.json
CHANGED
|
@@ -1,44 +1,47 @@
|
|
|
1
1
|
import { useState, useRef, useEffect, useCallback, useLayoutEffect } from 'react';
|
|
2
2
|
import { createPortal } from 'react-dom';
|
|
3
3
|
|
|
4
|
-
function ActionSubmenu({ items,
|
|
4
|
+
function ActionSubmenu({ items, menuRect, itemRect, closeAll, onMouseEnter, onMouseLeave }) {
|
|
5
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
6
|
|
|
12
7
|
useLayoutEffect(() => {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
const subHeight = subRef.current.offsetHeight || 150;
|
|
8
|
+
const sub = subRef.current;
|
|
9
|
+
if (!sub || !menuRect || !itemRect) return;
|
|
16
10
|
|
|
17
|
-
const
|
|
11
|
+
const subWidth = sub.offsetWidth || 190;
|
|
12
|
+
const subHeight = sub.offsetHeight || 150;
|
|
13
|
+
|
|
14
|
+
// Determine horizontal docking relative to PARENT MENU container
|
|
15
|
+
const spaceRight = window.innerWidth - menuRect.right;
|
|
18
16
|
let left;
|
|
19
|
-
let
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
17
|
+
let originX = 'left';
|
|
18
|
+
|
|
19
|
+
if (spaceRight >= subWidth + 8) {
|
|
20
|
+
// Dock cleanly to the right side of parent menu
|
|
21
|
+
left = menuRect.right + 4;
|
|
22
|
+
originX = 'left';
|
|
23
23
|
} else {
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
// Dock cleanly to the left side of parent menu (never overlaps!)
|
|
25
|
+
left = menuRect.left - subWidth - 4;
|
|
26
|
+
originX = 'right';
|
|
26
27
|
}
|
|
28
|
+
|
|
29
|
+
// Keep within viewport bounds
|
|
27
30
|
left = Math.max(8, Math.min(left, window.innerWidth - subWidth - 8));
|
|
28
31
|
|
|
29
|
-
|
|
30
|
-
let
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
32
|
+
// Determine vertical docking relative to the active item
|
|
33
|
+
let top = itemRect.top - 5;
|
|
34
|
+
let originY = 'top';
|
|
35
|
+
|
|
36
|
+
if (top + subHeight > window.innerHeight - 8) {
|
|
37
|
+
top = Math.max(8, window.innerHeight - subHeight - 8);
|
|
38
|
+
originY = 'bottom';
|
|
34
39
|
}
|
|
35
40
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
});
|
|
41
|
-
}, [parentRect]);
|
|
41
|
+
sub.style.top = top + 'px';
|
|
42
|
+
sub.style.left = left + 'px';
|
|
43
|
+
sub.style.transformOrigin = `${originY} ${originX}`;
|
|
44
|
+
}, [menuRect, itemRect]);
|
|
42
45
|
|
|
43
46
|
return createPortal(
|
|
44
47
|
<div
|
|
@@ -51,9 +54,8 @@ function ActionSubmenu({ items, parentRect, closeAll, onMouseEnter, onMouseLeave
|
|
|
51
54
|
zIndex: 100000,
|
|
52
55
|
minWidth: 190,
|
|
53
56
|
padding: 5,
|
|
54
|
-
top:
|
|
55
|
-
left:
|
|
56
|
-
transformOrigin: pos.transformOrigin,
|
|
57
|
+
top: itemRect ? (itemRect.top - 5) + 'px' : '0px',
|
|
58
|
+
left: menuRect ? (menuRect.left - 200) + 'px' : '0px',
|
|
57
59
|
background: 'var(--surface,#fff)',
|
|
58
60
|
border: '1.5px solid var(--border-light,#e2e8f0)',
|
|
59
61
|
borderRadius: 'var(--rads,8px)',
|
|
@@ -72,6 +74,7 @@ function ActionSubmenu({ items, parentRect, closeAll, onMouseEnter, onMouseLeave
|
|
|
72
74
|
display: 'flex', alignItems: 'center', gap: 8,
|
|
73
75
|
padding: '6px 10px', borderRadius: 'var(--rads,8px)',
|
|
74
76
|
fontSize: 12, cursor: 'pointer', transition: 'all .14s',
|
|
77
|
+
whiteSpace: 'nowrap',
|
|
75
78
|
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
79
|
fontWeight: 500,
|
|
77
80
|
}}
|
|
@@ -99,7 +102,7 @@ function ActionSubmenu({ items, parentRect, closeAll, onMouseEnter, onMouseLeave
|
|
|
99
102
|
export function ActionsMenu({ items, align = 'right' }) {
|
|
100
103
|
const [open, setOpen] = useState(false);
|
|
101
104
|
const [closing, setClosing] = useState(false);
|
|
102
|
-
const [activeSub, setActiveSub] = useState(null); // { index, items,
|
|
105
|
+
const [activeSub, setActiveSub] = useState(null); // { index, items, itemRect, menuRect }
|
|
103
106
|
const wrapRef = useRef(null);
|
|
104
107
|
const menuRef = useRef(null);
|
|
105
108
|
const btnRef = useRef(null);
|
|
@@ -114,8 +117,9 @@ export function ActionsMenu({ items, align = 'right' }) {
|
|
|
114
117
|
|
|
115
118
|
const openSub = (index, subItems, e) => {
|
|
116
119
|
clearTimeout(subTimerRef.current);
|
|
117
|
-
const
|
|
118
|
-
|
|
120
|
+
const itemRect = e.currentTarget.getBoundingClientRect();
|
|
121
|
+
const menuRect = menuRef.current ? menuRef.current.getBoundingClientRect() : itemRect;
|
|
122
|
+
setActiveSub({ index, items: subItems, itemRect, menuRect });
|
|
119
123
|
};
|
|
120
124
|
|
|
121
125
|
const scheduleCloseSub = () => {
|
|
@@ -280,6 +284,7 @@ export function ActionsMenu({ items, align = 'right' }) {
|
|
|
280
284
|
display: 'flex', alignItems: 'center', gap: 8,
|
|
281
285
|
padding: '6px 10px', borderRadius: 'var(--rads,8px)',
|
|
282
286
|
fontSize: 12, cursor: 'pointer', transition: 'all .14s',
|
|
287
|
+
whiteSpace: 'nowrap',
|
|
283
288
|
background: isSubActive ? 'var(--grey-1, #f4f4f5)' : 'transparent',
|
|
284
289
|
color: (item.variant === 'danger' || item.danger)
|
|
285
290
|
? 'var(--danger, #ef4444)'
|
|
@@ -325,7 +330,8 @@ export function ActionsMenu({ items, align = 'right' }) {
|
|
|
325
330
|
{open && activeSub && (
|
|
326
331
|
<ActionSubmenu
|
|
327
332
|
items={activeSub.items}
|
|
328
|
-
|
|
333
|
+
menuRect={activeSub.menuRect}
|
|
334
|
+
itemRect={activeSub.itemRect}
|
|
329
335
|
closeAll={close}
|
|
330
336
|
onMouseEnter={cancelCloseSub}
|
|
331
337
|
onMouseLeave={scheduleCloseSub}
|