@topconsultnpm/sdkui-react 6.19.0-dev2.26 → 6.19.0-dev2.28
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/lib/components/NewComponents/ContextMenu/TMContextMenu.d.ts +4 -0
- package/lib/components/NewComponents/ContextMenu/TMContextMenu.js +187 -0
- package/lib/components/NewComponents/ContextMenu/hooks.d.ts +11 -0
- package/lib/components/NewComponents/ContextMenu/hooks.js +48 -0
- package/lib/components/NewComponents/ContextMenu/index.d.ts +2 -0
- package/lib/components/NewComponents/ContextMenu/index.js +1 -0
- package/lib/components/NewComponents/ContextMenu/styles.d.ts +27 -0
- package/lib/components/NewComponents/ContextMenu/styles.js +308 -0
- package/lib/components/NewComponents/ContextMenu/types.d.ts +26 -0
- package/lib/components/NewComponents/ContextMenu/types.js +1 -0
- package/lib/components/NewComponents/FloatingMenuBar/TMFloatingMenuBar.d.ts +4 -0
- package/lib/components/NewComponents/FloatingMenuBar/TMFloatingMenuBar.js +370 -0
- package/lib/components/NewComponents/FloatingMenuBar/index.d.ts +2 -0
- package/lib/components/NewComponents/FloatingMenuBar/index.js +2 -0
- package/lib/components/NewComponents/FloatingMenuBar/styles.d.ts +38 -0
- package/lib/components/NewComponents/FloatingMenuBar/styles.js +267 -0
- package/lib/components/NewComponents/FloatingMenuBar/types.d.ts +30 -0
- package/lib/components/NewComponents/FloatingMenuBar/types.js +1 -0
- package/lib/components/NewComponents/Notification/Notification.d.ts +4 -0
- package/lib/components/NewComponents/Notification/Notification.js +60 -0
- package/lib/components/NewComponents/Notification/NotificationContainer.d.ts +8 -0
- package/lib/components/NewComponents/Notification/NotificationContainer.js +33 -0
- package/lib/components/NewComponents/Notification/index.d.ts +2 -0
- package/lib/components/NewComponents/Notification/index.js +2 -0
- package/lib/components/NewComponents/Notification/styles.d.ts +21 -0
- package/lib/components/NewComponents/Notification/styles.js +180 -0
- package/lib/components/NewComponents/Notification/types.d.ts +18 -0
- package/lib/components/NewComponents/Notification/types.js +1 -0
- package/lib/components/choosers/TMDcmtTypeChooser.js +2 -2
- package/lib/components/features/documents/TMDcmtForm.js +278 -17
- package/package.json +1 -1
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useState, useRef, useEffect } from 'react';
|
|
3
|
+
import * as S from './styles';
|
|
4
|
+
import { useIsMobile, useMenuPosition } from './hooks';
|
|
5
|
+
const TMContextMenu = ({ items, trigger = 'right', children }) => {
|
|
6
|
+
const [menuState, setMenuState] = useState({
|
|
7
|
+
visible: false,
|
|
8
|
+
position: { x: 0, y: 0 },
|
|
9
|
+
submenuStack: [items],
|
|
10
|
+
parentNames: [],
|
|
11
|
+
});
|
|
12
|
+
const [hoveredSubmenus, setHoveredSubmenus] = useState([]);
|
|
13
|
+
const isMobile = useIsMobile();
|
|
14
|
+
const menuRef = useRef(null);
|
|
15
|
+
const triggerRef = useRef(null);
|
|
16
|
+
const submenuTimeoutRef = useRef(null);
|
|
17
|
+
const { openLeft, openUp } = useMenuPosition(menuRef, menuState.position);
|
|
18
|
+
const handleClose = () => {
|
|
19
|
+
setMenuState(prev => ({
|
|
20
|
+
...prev,
|
|
21
|
+
visible: false,
|
|
22
|
+
submenuStack: [items],
|
|
23
|
+
parentNames: [],
|
|
24
|
+
}));
|
|
25
|
+
setHoveredSubmenus([]);
|
|
26
|
+
};
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
if (!menuState.visible)
|
|
29
|
+
return;
|
|
30
|
+
const handleClickOutside = (event) => {
|
|
31
|
+
const target = event.target;
|
|
32
|
+
// Check if click is inside main menu
|
|
33
|
+
if (menuRef.current?.contains(target)) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
// Check if click is inside any submenu
|
|
37
|
+
const submenus = document.querySelectorAll('[data-submenu="true"]');
|
|
38
|
+
for (const submenu of Array.from(submenus)) {
|
|
39
|
+
if (submenu.contains(target)) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
// Click is outside all menus, close them
|
|
44
|
+
handleClose();
|
|
45
|
+
};
|
|
46
|
+
document.addEventListener('mousedown', handleClickOutside);
|
|
47
|
+
document.addEventListener('touchstart', handleClickOutside);
|
|
48
|
+
return () => {
|
|
49
|
+
document.removeEventListener('mousedown', handleClickOutside);
|
|
50
|
+
document.removeEventListener('touchstart', handleClickOutside);
|
|
51
|
+
};
|
|
52
|
+
}, [menuState.visible]);
|
|
53
|
+
const handleContextMenu = (e) => {
|
|
54
|
+
if (trigger === 'right') {
|
|
55
|
+
e.preventDefault();
|
|
56
|
+
setMenuState({
|
|
57
|
+
visible: true,
|
|
58
|
+
position: { x: e.clientX, y: e.clientY },
|
|
59
|
+
submenuStack: [items],
|
|
60
|
+
parentNames: [],
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
const handleClick = (e) => {
|
|
65
|
+
if (trigger === 'left') {
|
|
66
|
+
e.preventDefault();
|
|
67
|
+
setMenuState({
|
|
68
|
+
visible: true,
|
|
69
|
+
position: { x: e.clientX, y: e.clientY },
|
|
70
|
+
submenuStack: [items],
|
|
71
|
+
parentNames: [],
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
const handleItemClick = (item) => {
|
|
76
|
+
if (item.disabled)
|
|
77
|
+
return;
|
|
78
|
+
// Always execute onClick if present
|
|
79
|
+
if (item.onClick) {
|
|
80
|
+
item.onClick();
|
|
81
|
+
}
|
|
82
|
+
if (item.submenu && item.submenu.length > 0) {
|
|
83
|
+
if (isMobile) {
|
|
84
|
+
// Mobile: Push submenu to stack
|
|
85
|
+
setMenuState(prev => ({
|
|
86
|
+
...prev,
|
|
87
|
+
submenuStack: [...prev.submenuStack, item.submenu],
|
|
88
|
+
parentNames: [...prev.parentNames, item.name],
|
|
89
|
+
}));
|
|
90
|
+
}
|
|
91
|
+
// Desktop: Submenus are handled by hover, don't close menu
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
// No submenu: close menu after executing action
|
|
95
|
+
handleClose();
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
const handleBack = () => {
|
|
99
|
+
setMenuState(prev => ({
|
|
100
|
+
...prev,
|
|
101
|
+
submenuStack: prev.submenuStack.slice(0, -1),
|
|
102
|
+
parentNames: prev.parentNames.slice(0, -1),
|
|
103
|
+
}));
|
|
104
|
+
};
|
|
105
|
+
const handleMouseEnter = (item, event, depth = 0) => {
|
|
106
|
+
if (isMobile || !item.submenu || item.submenu.length === 0)
|
|
107
|
+
return;
|
|
108
|
+
if (submenuTimeoutRef.current) {
|
|
109
|
+
clearTimeout(submenuTimeoutRef.current);
|
|
110
|
+
submenuTimeoutRef.current = null;
|
|
111
|
+
}
|
|
112
|
+
const rect = event.currentTarget.getBoundingClientRect();
|
|
113
|
+
// Calculate if submenu should open upward based on available space
|
|
114
|
+
// Estimate submenu height: ~35px per item (accounting for smaller padding) + container padding
|
|
115
|
+
const estimatedSubmenuHeight = (item.submenu.length * 35) + 8;
|
|
116
|
+
const spaceBelow = window.innerHeight - rect.top;
|
|
117
|
+
const spaceAbove = rect.bottom;
|
|
118
|
+
// Open upward only if there's not enough space below AND there's more space above
|
|
119
|
+
const shouldOpenUp = spaceBelow < estimatedSubmenuHeight && spaceAbove > spaceBelow;
|
|
120
|
+
// Remove all submenus at this depth and deeper
|
|
121
|
+
setHoveredSubmenus(prev => {
|
|
122
|
+
const filtered = prev.filter(sub => sub.depth < depth);
|
|
123
|
+
if (!item.submenu)
|
|
124
|
+
return filtered;
|
|
125
|
+
return [
|
|
126
|
+
...filtered,
|
|
127
|
+
{
|
|
128
|
+
items: item.submenu,
|
|
129
|
+
parentRect: rect,
|
|
130
|
+
depth: depth,
|
|
131
|
+
openUp: shouldOpenUp,
|
|
132
|
+
}
|
|
133
|
+
];
|
|
134
|
+
});
|
|
135
|
+
};
|
|
136
|
+
const handleMouseLeave = (depth = 0) => {
|
|
137
|
+
if (isMobile)
|
|
138
|
+
return;
|
|
139
|
+
if (submenuTimeoutRef.current) {
|
|
140
|
+
clearTimeout(submenuTimeoutRef.current);
|
|
141
|
+
}
|
|
142
|
+
const targetDepth = depth;
|
|
143
|
+
submenuTimeoutRef.current = setTimeout(() => {
|
|
144
|
+
setHoveredSubmenus(prev => prev.filter(sub => sub.depth < targetDepth));
|
|
145
|
+
}, 300);
|
|
146
|
+
};
|
|
147
|
+
const handleSubmenuMouseEnter = () => {
|
|
148
|
+
if (submenuTimeoutRef.current) {
|
|
149
|
+
clearTimeout(submenuTimeoutRef.current);
|
|
150
|
+
submenuTimeoutRef.current = null;
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
useEffect(() => {
|
|
154
|
+
return () => {
|
|
155
|
+
if (submenuTimeoutRef.current) {
|
|
156
|
+
clearTimeout(submenuTimeoutRef.current);
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
}, []);
|
|
160
|
+
const renderMenuItems = (menuItems, depth = 0) => {
|
|
161
|
+
return menuItems
|
|
162
|
+
.filter(item => item.visible !== false)
|
|
163
|
+
.map((item, idx) => {
|
|
164
|
+
const itemKey = `${item.name}-${idx}`.replaceAll(/\s+/g, '-');
|
|
165
|
+
const handleClick = (e) => {
|
|
166
|
+
if (item.disabled)
|
|
167
|
+
return;
|
|
168
|
+
e.stopPropagation();
|
|
169
|
+
handleItemClick(item);
|
|
170
|
+
};
|
|
171
|
+
const handleRightIconClick = (e) => {
|
|
172
|
+
e.stopPropagation();
|
|
173
|
+
// if (item.disabled) return;
|
|
174
|
+
item.onRightIconClick?.();
|
|
175
|
+
};
|
|
176
|
+
return (_jsxs(S.MenuItem, { "$disabled": item.disabled, "$hasSubmenu": !!item.submenu && item.submenu.length > 0, "$beginGroup": item.beginGroup, onMouseDown: handleClick, onMouseEnter: (e) => !isMobile && handleMouseEnter(item, e, depth + 1), onMouseLeave: () => !isMobile && handleMouseLeave(depth + 1), children: [_jsxs(S.MenuItemContent, { children: [item.icon && _jsx(S.IconWrapper, { children: item.icon }), _jsx(S.MenuItemName, { children: item.name })] }), item.rightIcon && item.onRightIconClick && (_jsx(S.RightIconButton, { onClick: handleRightIconClick, onMouseDown: (e) => e.stopPropagation(), "aria-label": `Action for ${item.name}`, children: item.rightIcon })), item.submenu && item.submenu.length > 0 && (_jsx(S.SubmenuIndicator, { "$isMobile": isMobile, children: isMobile ? '›' : '▸' }))] }, itemKey));
|
|
177
|
+
});
|
|
178
|
+
};
|
|
179
|
+
const currentMenu = menuState.submenuStack.at(-1) || items;
|
|
180
|
+
const currentParentName = menuState.parentNames.at(-1) || '';
|
|
181
|
+
return (_jsxs(_Fragment, { children: [_jsx("div", { ref: triggerRef, onContextMenu: handleContextMenu, onClick: handleClick, onKeyDown: (e) => {
|
|
182
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
183
|
+
handleClick(e);
|
|
184
|
+
}
|
|
185
|
+
}, role: "button", tabIndex: 0, style: { display: 'inline-block' }, children: children }), menuState.visible && (_jsxs(_Fragment, { children: [_jsx(S.Overlay, { onClick: handleClose }), _jsxs(S.MenuContainer, { ref: menuRef, "$x": menuState.position.x, "$y": menuState.position.y, "$openLeft": openLeft, "$openUp": openUp, children: [isMobile && menuState.parentNames.length > 0 && (_jsxs(S.MobileMenuHeader, { children: [_jsx(S.BackButton, { onClick: handleBack, "aria-label": "Go back", children: "\u2190" }), _jsx(S.HeaderTitle, { children: currentParentName })] })), renderMenuItems(currentMenu, 0)] }), !isMobile && hoveredSubmenus.map((submenu, idx) => (_jsx(S.Submenu, { "$parentRect": submenu.parentRect, "$openUp": submenu.openUp, "data-submenu": "true", onMouseEnter: handleSubmenuMouseEnter, onMouseLeave: () => handleMouseLeave(submenu.depth), children: renderMenuItems(submenu.items, submenu.depth) }, `submenu-${submenu.depth}-${idx}`)))] }))] }));
|
|
186
|
+
};
|
|
187
|
+
export default TMContextMenu;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export declare const useIsMobile: () => boolean;
|
|
2
|
+
export declare const useClickOutside: (callback: () => void) => import("react").RefObject<HTMLDivElement>;
|
|
3
|
+
interface Position {
|
|
4
|
+
x: number;
|
|
5
|
+
y: number;
|
|
6
|
+
}
|
|
7
|
+
export declare const useMenuPosition: (menuRef: React.RefObject<HTMLDivElement | null>, position: Position) => {
|
|
8
|
+
openLeft: boolean;
|
|
9
|
+
openUp: boolean;
|
|
10
|
+
};
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { useState, useEffect, useRef } from 'react';
|
|
2
|
+
export const useIsMobile = () => {
|
|
3
|
+
const [isMobile, setIsMobile] = useState(false);
|
|
4
|
+
useEffect(() => {
|
|
5
|
+
const checkMobile = () => {
|
|
6
|
+
const mobile = globalThis.innerWidth <= 768 || 'ontouchstart' in globalThis;
|
|
7
|
+
setIsMobile(mobile);
|
|
8
|
+
};
|
|
9
|
+
checkMobile();
|
|
10
|
+
window.addEventListener('resize', checkMobile);
|
|
11
|
+
return () => window.removeEventListener('resize', checkMobile);
|
|
12
|
+
}, []);
|
|
13
|
+
return isMobile;
|
|
14
|
+
};
|
|
15
|
+
export const useClickOutside = (callback) => {
|
|
16
|
+
const ref = useRef(null);
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
const handleClick = (event) => {
|
|
19
|
+
if (ref.current && !ref.current.contains(event.target)) {
|
|
20
|
+
callback();
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
document.addEventListener('mousedown', handleClick);
|
|
24
|
+
document.addEventListener('touchstart', handleClick);
|
|
25
|
+
return () => {
|
|
26
|
+
document.removeEventListener('mousedown', handleClick);
|
|
27
|
+
document.removeEventListener('touchstart', handleClick);
|
|
28
|
+
};
|
|
29
|
+
}, [callback]);
|
|
30
|
+
return ref;
|
|
31
|
+
};
|
|
32
|
+
export const useMenuPosition = (menuRef, position) => {
|
|
33
|
+
const [adjustedPosition, setAdjustedPosition] = useState({ openLeft: false, openUp: false });
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
if (!menuRef.current)
|
|
36
|
+
return;
|
|
37
|
+
const menuRect = menuRef.current.getBoundingClientRect();
|
|
38
|
+
const viewportWidth = window.innerWidth;
|
|
39
|
+
const viewportHeight = window.innerHeight;
|
|
40
|
+
const spaceRight = viewportWidth - position.x;
|
|
41
|
+
const spaceBottom = viewportHeight - position.y;
|
|
42
|
+
setAdjustedPosition({
|
|
43
|
+
openLeft: spaceRight < menuRect.width + 20,
|
|
44
|
+
openUp: spaceBottom < menuRect.height + 20,
|
|
45
|
+
});
|
|
46
|
+
}, [position, menuRef]);
|
|
47
|
+
return adjustedPosition;
|
|
48
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as ContextMenu } from './TMContextMenu';
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export declare const MenuContainer: import("styled-components/dist/types").IStyledComponentBase<"web", import("styled-components/dist/types").Substitute<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLDivElement>, HTMLDivElement>, {
|
|
2
|
+
$x: number;
|
|
3
|
+
$y: number;
|
|
4
|
+
$openLeft: boolean;
|
|
5
|
+
$openUp: boolean;
|
|
6
|
+
}>> & string;
|
|
7
|
+
export declare const MenuItem: import("styled-components/dist/types").IStyledComponentBase<"web", import("styled-components/dist/types").Substitute<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLDivElement>, HTMLDivElement>, {
|
|
8
|
+
$disabled?: boolean;
|
|
9
|
+
$hasSubmenu?: boolean;
|
|
10
|
+
$beginGroup?: boolean;
|
|
11
|
+
}>> & string;
|
|
12
|
+
export declare const MenuItemContent: import("styled-components/dist/types").IStyledComponentBase<"web", import("styled-components").FastOmit<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLDivElement>, HTMLDivElement>, never>> & string;
|
|
13
|
+
export declare const IconWrapper: import("styled-components/dist/types").IStyledComponentBase<"web", import("styled-components").FastOmit<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>, never>> & string;
|
|
14
|
+
export declare const MenuItemName: import("styled-components/dist/types").IStyledComponentBase<"web", import("styled-components").FastOmit<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>, never>> & string;
|
|
15
|
+
export declare const RightIconButton: import("styled-components/dist/types").IStyledComponentBase<"web", import("styled-components").FastOmit<import("react").DetailedHTMLProps<import("react").ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>, never>> & string;
|
|
16
|
+
export declare const SubmenuIndicator: import("styled-components/dist/types").IStyledComponentBase<"web", import("styled-components/dist/types").Substitute<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>, {
|
|
17
|
+
$isMobile?: boolean;
|
|
18
|
+
}>> & string;
|
|
19
|
+
export declare const Submenu: import("styled-components/dist/types").IStyledComponentBase<"web", import("styled-components/dist/types").Substitute<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLDivElement>, HTMLDivElement>, {
|
|
20
|
+
$parentRect: DOMRect;
|
|
21
|
+
$openUp?: boolean;
|
|
22
|
+
}>> & string;
|
|
23
|
+
export declare const MobileMenuHeader: import("styled-components/dist/types").IStyledComponentBase<"web", import("styled-components").FastOmit<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLDivElement>, HTMLDivElement>, never>> & string;
|
|
24
|
+
export declare const BackButton: import("styled-components/dist/types").IStyledComponentBase<"web", import("styled-components").FastOmit<import("react").DetailedHTMLProps<import("react").ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>, never>> & string;
|
|
25
|
+
export declare const HeaderTitle: import("styled-components/dist/types").IStyledComponentBase<"web", import("styled-components").FastOmit<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLHeadingElement>, HTMLHeadingElement>, never>> & string;
|
|
26
|
+
export declare const MenuDivider: import("styled-components/dist/types").IStyledComponentBase<"web", import("styled-components").FastOmit<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLDivElement>, HTMLDivElement>, never>> & string;
|
|
27
|
+
export declare const Overlay: import("styled-components/dist/types").IStyledComponentBase<"web", import("styled-components").FastOmit<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLDivElement>, HTMLDivElement>, never>> & string;
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
import styled, { keyframes } from 'styled-components';
|
|
2
|
+
const fadeIn = keyframes `
|
|
3
|
+
from {
|
|
4
|
+
opacity: 0;
|
|
5
|
+
transform: scale(0.95);
|
|
6
|
+
}
|
|
7
|
+
to {
|
|
8
|
+
opacity: 1;
|
|
9
|
+
transform: scale(1);
|
|
10
|
+
}
|
|
11
|
+
`;
|
|
12
|
+
const slideIn = keyframes `
|
|
13
|
+
from {
|
|
14
|
+
opacity: 0;
|
|
15
|
+
transform: translateX(-10px);
|
|
16
|
+
}
|
|
17
|
+
to {
|
|
18
|
+
opacity: 1;
|
|
19
|
+
transform: translateX(0);
|
|
20
|
+
}
|
|
21
|
+
`;
|
|
22
|
+
export const MenuContainer = styled.div `
|
|
23
|
+
position: fixed;
|
|
24
|
+
left: ${props => props.$openLeft ? 'auto' : `${props.$x}px`};
|
|
25
|
+
right: ${props => props.$openLeft ? `${window.innerWidth - props.$x}px` : 'auto'};
|
|
26
|
+
top: ${props => props.$openUp ? 'auto' : `${props.$y}px`};
|
|
27
|
+
bottom: ${props => props.$openUp ? `${window.innerHeight - props.$y}px` : 'auto'};
|
|
28
|
+
z-index: 10000;
|
|
29
|
+
background: #ffffff;
|
|
30
|
+
border-radius: 12px;
|
|
31
|
+
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12),
|
|
32
|
+
0 2px 8px rgba(0, 0, 0, 0.08);
|
|
33
|
+
min-width: max-content;
|
|
34
|
+
width: max-content;
|
|
35
|
+
padding: 4px 0;
|
|
36
|
+
animation: ${fadeIn} 0.15s ease-out;
|
|
37
|
+
backdrop-filter: blur(10px);
|
|
38
|
+
border: 1px solid rgba(0, 0, 0, 0.06);
|
|
39
|
+
|
|
40
|
+
[data-theme='dark'] & {
|
|
41
|
+
background: #2a2a2a;
|
|
42
|
+
border-color: rgba(255, 255, 255, 0.1);
|
|
43
|
+
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4),
|
|
44
|
+
0 2px 8px rgba(0, 0, 0, 0.3);
|
|
45
|
+
}
|
|
46
|
+
`;
|
|
47
|
+
export const MenuItem = styled.div `
|
|
48
|
+
display: flex;
|
|
49
|
+
align-items: center;
|
|
50
|
+
justify-content: space-between;
|
|
51
|
+
padding: 4px 12px;
|
|
52
|
+
cursor: ${props => props.$disabled ? 'not-allowed' : 'pointer'};
|
|
53
|
+
opacity: ${props => props.$disabled ? 0.4 : 1};
|
|
54
|
+
transition: all 0.15s ease;
|
|
55
|
+
position: relative;
|
|
56
|
+
user-select: none;
|
|
57
|
+
font-size: 13px;
|
|
58
|
+
color: ${props => props.$disabled ? '#999' : '#1a1a1a'};
|
|
59
|
+
font-weight: 500;
|
|
60
|
+
${props => props.$beginGroup && `
|
|
61
|
+
border-top: 1px solid rgba(0, 0, 0, 0.1);
|
|
62
|
+
margin-top: 4px;
|
|
63
|
+
padding-top: 8px;
|
|
64
|
+
`}
|
|
65
|
+
|
|
66
|
+
&:hover {
|
|
67
|
+
${props => !props.$disabled && `
|
|
68
|
+
background: linear-gradient(90deg, #f0f7ff 0%, #e6f2ff 100%);
|
|
69
|
+
color: #0066cc;
|
|
70
|
+
padding-left: 14px;
|
|
71
|
+
`}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
&:active {
|
|
75
|
+
${props => !props.$disabled && `
|
|
76
|
+
background: linear-gradient(90deg, #e6f2ff 0%, #d9ebff 100%);
|
|
77
|
+
transform: scale(0.98);
|
|
78
|
+
`}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
[data-theme='dark'] & {
|
|
82
|
+
color: ${props => props.$disabled ? '#666' : '#e0e0e0'};
|
|
83
|
+
${props => props.$beginGroup && `
|
|
84
|
+
border-top-color: rgba(255, 255, 255, 0.1);
|
|
85
|
+
`}
|
|
86
|
+
|
|
87
|
+
&:hover {
|
|
88
|
+
${props => !props.$disabled && `
|
|
89
|
+
background: linear-gradient(90deg, #1a3a52 0%, #1e4159 100%);
|
|
90
|
+
color: #4db8ff;
|
|
91
|
+
`}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
&:active {
|
|
95
|
+
${props => !props.$disabled && `
|
|
96
|
+
background: linear-gradient(90deg, #1e4159 0%, #234a66 100%);
|
|
97
|
+
`}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
@media (max-width: 768px) {
|
|
102
|
+
padding: 14px 16px;
|
|
103
|
+
font-size: 15px;
|
|
104
|
+
}
|
|
105
|
+
`;
|
|
106
|
+
export const MenuItemContent = styled.div `
|
|
107
|
+
display: flex;
|
|
108
|
+
align-items: center;
|
|
109
|
+
gap: 10px;
|
|
110
|
+
flex: 1;
|
|
111
|
+
`;
|
|
112
|
+
export const IconWrapper = styled.span `
|
|
113
|
+
display: flex;
|
|
114
|
+
align-items: center;
|
|
115
|
+
justify-content: center;
|
|
116
|
+
font-size: 14px;
|
|
117
|
+
width: 18px;
|
|
118
|
+
height: 18px;
|
|
119
|
+
`;
|
|
120
|
+
export const MenuItemName = styled.span `
|
|
121
|
+
flex: 1;
|
|
122
|
+
white-space: normal;
|
|
123
|
+
word-wrap: break-word;
|
|
124
|
+
overflow-wrap: break-word;
|
|
125
|
+
line-height: 1.4;
|
|
126
|
+
`;
|
|
127
|
+
export const RightIconButton = styled.button `
|
|
128
|
+
display: flex;
|
|
129
|
+
align-items: center;
|
|
130
|
+
justify-content: center;
|
|
131
|
+
background: transparent;
|
|
132
|
+
border: none;
|
|
133
|
+
cursor: pointer;
|
|
134
|
+
padding: 4px 8px;
|
|
135
|
+
margin-left: 8px;
|
|
136
|
+
border-radius: 6px;
|
|
137
|
+
color: inherit;
|
|
138
|
+
font-size: 14px;
|
|
139
|
+
opacity: 0.6;
|
|
140
|
+
transition: all 0.15s ease;
|
|
141
|
+
|
|
142
|
+
&:hover {
|
|
143
|
+
opacity: 1;
|
|
144
|
+
background: rgba(0, 0, 0, 0.05);
|
|
145
|
+
transform: scale(1.1);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
&:active {
|
|
149
|
+
transform: scale(0.95);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
[data-theme='dark'] & {
|
|
153
|
+
&:hover {
|
|
154
|
+
background: rgba(255, 255, 255, 0.1);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
`;
|
|
158
|
+
export const SubmenuIndicator = styled.span `
|
|
159
|
+
display: flex;
|
|
160
|
+
align-items: center;
|
|
161
|
+
font-size: 12px;
|
|
162
|
+
margin-left: 8px;
|
|
163
|
+
opacity: 0.6;
|
|
164
|
+
transition: transform 0.15s ease;
|
|
165
|
+
|
|
166
|
+
${MenuItem}:hover & {
|
|
167
|
+
${props => !props.$isMobile && `
|
|
168
|
+
transform: translateX(2px);
|
|
169
|
+
opacity: 1;
|
|
170
|
+
`}
|
|
171
|
+
}
|
|
172
|
+
`;
|
|
173
|
+
export const Submenu = styled.div `
|
|
174
|
+
position: fixed;
|
|
175
|
+
left: ${props => {
|
|
176
|
+
const spaceOnRight = globalThis.innerWidth - props.$parentRect.right;
|
|
177
|
+
return spaceOnRight > 240 ? `${props.$parentRect.right - 8}px` : 'auto';
|
|
178
|
+
}};
|
|
179
|
+
right: ${props => {
|
|
180
|
+
const spaceOnRight = globalThis.innerWidth - props.$parentRect.right;
|
|
181
|
+
return spaceOnRight > 240 ? 'auto' : `${globalThis.innerWidth - props.$parentRect.left + 8}px`;
|
|
182
|
+
}};
|
|
183
|
+
/* Vertical positioning: Each submenu independently decides direction based on its own space */
|
|
184
|
+
top: ${props => {
|
|
185
|
+
// If openUp is true, don't anchor to top
|
|
186
|
+
return props.$openUp ? 'auto' : `${props.$parentRect.top - 8}px`;
|
|
187
|
+
}};
|
|
188
|
+
bottom: ${props => {
|
|
189
|
+
// If openUp is true, anchor to bottom and grow upward
|
|
190
|
+
return props.$openUp ? `${globalThis.innerHeight - props.$parentRect.bottom - 8}px` : 'auto';
|
|
191
|
+
}};
|
|
192
|
+
z-index: 10001;
|
|
193
|
+
background: #ffffff;
|
|
194
|
+
border-radius: 12px;
|
|
195
|
+
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12),
|
|
196
|
+
0 2px 8px rgba(0, 0, 0, 0.08);
|
|
197
|
+
min-width: max-content;
|
|
198
|
+
width: max-content;
|
|
199
|
+
padding: 4px 0;
|
|
200
|
+
animation: ${slideIn} 0.15s ease-out;
|
|
201
|
+
backdrop-filter: blur(10px);
|
|
202
|
+
border: 1px solid rgba(0, 0, 0, 0.06);
|
|
203
|
+
|
|
204
|
+
/* Add invisible padding to bridge the gap - works for both sides */
|
|
205
|
+
&::before {
|
|
206
|
+
content: '';
|
|
207
|
+
position: absolute;
|
|
208
|
+
right: 100%;
|
|
209
|
+
top: 0;
|
|
210
|
+
bottom: 0;
|
|
211
|
+
width: 15px;
|
|
212
|
+
background: transparent;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/* Bridge on the right side for nested submenus */
|
|
216
|
+
&::after {
|
|
217
|
+
content: '';
|
|
218
|
+
position: absolute;
|
|
219
|
+
left: 100%;
|
|
220
|
+
top: 0;
|
|
221
|
+
bottom: 0;
|
|
222
|
+
width: 15px;
|
|
223
|
+
background: transparent;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
[data-theme='dark'] & {
|
|
227
|
+
background: #2a2a2a;
|
|
228
|
+
border-color: rgba(255, 255, 255, 0.1);
|
|
229
|
+
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4),
|
|
230
|
+
0 2px 8px rgba(0, 0, 0, 0.3);
|
|
231
|
+
}
|
|
232
|
+
`;
|
|
233
|
+
export const MobileMenuHeader = styled.div `
|
|
234
|
+
display: flex;
|
|
235
|
+
align-items: center;
|
|
236
|
+
padding: 12px 16px;
|
|
237
|
+
border-bottom: 1px solid rgba(0, 0, 0, 0.08);
|
|
238
|
+
margin-bottom: 8px;
|
|
239
|
+
gap: 12px;
|
|
240
|
+
background: linear-gradient(180deg, #f8f9fa 0%, #ffffff 100%);
|
|
241
|
+
border-radius: 12px 12px 0 0;
|
|
242
|
+
|
|
243
|
+
[data-theme='dark'] & {
|
|
244
|
+
background: linear-gradient(180deg, #1a1a1a 0%, #2a2a2a 100%);
|
|
245
|
+
border-bottom-color: rgba(255, 255, 255, 0.1);
|
|
246
|
+
}
|
|
247
|
+
`;
|
|
248
|
+
export const BackButton = styled.button `
|
|
249
|
+
display: flex;
|
|
250
|
+
align-items: center;
|
|
251
|
+
justify-content: center;
|
|
252
|
+
background: #0066cc;
|
|
253
|
+
color: white;
|
|
254
|
+
border: none;
|
|
255
|
+
border-radius: 8px;
|
|
256
|
+
width: 32px;
|
|
257
|
+
height: 32px;
|
|
258
|
+
cursor: pointer;
|
|
259
|
+
font-size: 18px;
|
|
260
|
+
transition: all 0.15s ease;
|
|
261
|
+
|
|
262
|
+
&:hover {
|
|
263
|
+
background: #0052a3;
|
|
264
|
+
transform: scale(1.05);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
&:active {
|
|
268
|
+
transform: scale(0.95);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
[data-theme='dark'] & {
|
|
272
|
+
background: #4db8ff;
|
|
273
|
+
color: #1a1a1a;
|
|
274
|
+
|
|
275
|
+
&:hover {
|
|
276
|
+
background: #66c2ff;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
`;
|
|
280
|
+
export const HeaderTitle = styled.h3 `
|
|
281
|
+
margin: 0;
|
|
282
|
+
font-size: 16px;
|
|
283
|
+
font-weight: 600;
|
|
284
|
+
color: #1a1a1a;
|
|
285
|
+
flex: 1;
|
|
286
|
+
|
|
287
|
+
[data-theme='dark'] & {
|
|
288
|
+
color: #e0e0e0;
|
|
289
|
+
}
|
|
290
|
+
`;
|
|
291
|
+
export const MenuDivider = styled.div `
|
|
292
|
+
height: 1px;
|
|
293
|
+
background: linear-gradient(90deg, transparent 0%, rgba(0, 0, 0, 0.1) 50%, transparent 100%);
|
|
294
|
+
margin: 8px 0;
|
|
295
|
+
|
|
296
|
+
[data-theme='dark'] & {
|
|
297
|
+
background: linear-gradient(90deg, transparent 0%, rgba(255, 255, 255, 0.1) 50%, transparent 100%);
|
|
298
|
+
}
|
|
299
|
+
`;
|
|
300
|
+
export const Overlay = styled.div `
|
|
301
|
+
position: fixed;
|
|
302
|
+
top: 0;
|
|
303
|
+
left: 0;
|
|
304
|
+
right: 0;
|
|
305
|
+
bottom: 0;
|
|
306
|
+
z-index: 9999;
|
|
307
|
+
background: transparent;
|
|
308
|
+
`;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export interface TMContextMenuItemProps {
|
|
2
|
+
name: string;
|
|
3
|
+
icon?: React.ReactNode;
|
|
4
|
+
disabled?: boolean;
|
|
5
|
+
onClick?: () => void;
|
|
6
|
+
submenu?: TMContextMenuItemProps[];
|
|
7
|
+
visible?: boolean;
|
|
8
|
+
rightIcon?: React.ReactNode;
|
|
9
|
+
onRightIconClick?: () => void;
|
|
10
|
+
beginGroup?: boolean;
|
|
11
|
+
}
|
|
12
|
+
export interface TMContextMenuProps {
|
|
13
|
+
items: TMContextMenuItemProps[];
|
|
14
|
+
trigger?: 'right' | 'left';
|
|
15
|
+
children?: React.ReactNode;
|
|
16
|
+
}
|
|
17
|
+
export interface Position {
|
|
18
|
+
x: number;
|
|
19
|
+
y: number;
|
|
20
|
+
}
|
|
21
|
+
export interface MenuState {
|
|
22
|
+
visible: boolean;
|
|
23
|
+
position: Position;
|
|
24
|
+
submenuStack: TMContextMenuItemProps[][];
|
|
25
|
+
parentNames: string[];
|
|
26
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|