@topconsultnpm/sdkui-react 6.20.0-dev1.7 → 6.20.0-dev1.71

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.
Files changed (116) hide show
  1. package/lib/components/NewComponents/ContextMenu/TMContextMenu.js +258 -17
  2. package/lib/components/NewComponents/ContextMenu/hooks.d.ts +5 -0
  3. package/lib/components/NewComponents/ContextMenu/hooks.js +38 -4
  4. package/lib/components/NewComponents/ContextMenu/index.d.ts +3 -0
  5. package/lib/components/NewComponents/ContextMenu/index.js +2 -0
  6. package/lib/components/NewComponents/ContextMenu/styles.d.ts +5 -1
  7. package/lib/components/NewComponents/ContextMenu/styles.js +59 -31
  8. package/lib/components/NewComponents/ContextMenu/types.d.ts +13 -0
  9. package/lib/components/NewComponents/ContextMenu/useLongPress.d.ts +21 -0
  10. package/lib/components/NewComponents/ContextMenu/useLongPress.js +112 -0
  11. package/lib/components/NewComponents/FloatingMenuBar/TMFloatingMenuBar.js +517 -100
  12. package/lib/components/NewComponents/FloatingMenuBar/styles.d.ts +19 -5
  13. package/lib/components/NewComponents/FloatingMenuBar/styles.js +206 -54
  14. package/lib/components/NewComponents/FloatingMenuBar/types.d.ts +1 -2
  15. package/lib/components/base/TMAccordionNew.js +35 -14
  16. package/lib/components/base/TMCustomButton.js +61 -17
  17. package/lib/components/base/TMDataGrid.d.ts +7 -4
  18. package/lib/components/base/TMDataGrid.js +142 -11
  19. package/lib/components/base/TMDropDownMenu.js +19 -18
  20. package/lib/components/base/TMPanel.js +1 -1
  21. package/lib/components/choosers/TMInvoiceRetrieveFormats.js +1 -1
  22. package/lib/components/choosers/TMMetadataChooser.js +8 -1
  23. package/lib/components/choosers/TMOrderRetrieveFormats.js +1 -1
  24. package/lib/components/choosers/TMUserChooser.d.ts +0 -5
  25. package/lib/components/choosers/TMUserChooser.js +25 -45
  26. package/lib/components/editors/TMMetadataValues.js +23 -5
  27. package/lib/components/editors/TMTextBox.js +6 -3
  28. package/lib/components/features/documents/TMDcmtForm.d.ts +13 -1
  29. package/lib/components/features/documents/TMDcmtForm.js +386 -194
  30. package/lib/components/features/documents/TMDcmtPreview.js +41 -105
  31. package/lib/components/features/documents/TMMasterDetailDcmts.js +37 -52
  32. package/lib/components/features/documents/TMRelationViewer.d.ts +1 -1
  33. package/lib/components/features/documents/TMRelationViewer.js +2 -2
  34. package/lib/components/features/search/TMDcmtCheckoutInfoForm.d.ts +8 -0
  35. package/lib/components/features/search/{TMSearchResultCheckoutInfoForm.js → TMDcmtCheckoutInfoForm.js} +5 -10
  36. package/lib/components/features/search/TMSavedQuerySelector.js +72 -67
  37. package/lib/components/features/search/TMSearch.js +41 -9
  38. package/lib/components/features/search/TMSearchQueryPanel.d.ts +1 -0
  39. package/lib/components/features/search/TMSearchQueryPanel.js +19 -18
  40. package/lib/components/features/search/TMSearchResult.js +118 -242
  41. package/lib/components/features/search/TMSearchResultsMenuItems.d.ts +3 -3
  42. package/lib/components/features/search/TMSearchResultsMenuItems.js +205 -169
  43. package/lib/components/features/search/TMSignSettingsForm.js +1 -1
  44. package/lib/components/features/search/TMSignatureInfoContent.d.ts +6 -0
  45. package/lib/components/features/search/TMSignatureInfoContent.js +140 -0
  46. package/lib/components/features/search/TMViewHistoryDcmt.js +1 -1
  47. package/lib/components/features/tasks/TMTaskForm.js +20 -1
  48. package/lib/components/features/tasks/TMTasksUtils.d.ts +2 -2
  49. package/lib/components/features/tasks/TMTasksUtils.js +62 -52
  50. package/lib/components/features/tasks/TMTasksView.js +6 -6
  51. package/lib/components/features/workflow/TMWorkflowPopup.d.ts +33 -2
  52. package/lib/components/features/workflow/TMWorkflowPopup.js +134 -24
  53. package/lib/components/features/workflow/diagram/DiagramItemComponent.d.ts +1 -0
  54. package/lib/components/features/workflow/diagram/DiagramItemComponent.js +2 -3
  55. package/lib/components/features/workflow/diagram/RecipientList.js +3 -2
  56. package/lib/components/features/workflow/diagram/WFDiagram.d.ts +2 -0
  57. package/lib/components/features/workflow/diagram/WFDiagram.js +21 -4
  58. package/lib/components/forms/Login/LoginValidatorService.d.ts +2 -0
  59. package/lib/components/forms/Login/LoginValidatorService.js +7 -2
  60. package/lib/components/forms/Login/TMLoginForm.js +34 -6
  61. package/lib/components/forms/TMChooserForm.js +1 -1
  62. package/lib/components/grids/TMBlogsPost.js +55 -30
  63. package/lib/components/grids/TMRecentsManager.js +20 -10
  64. package/lib/components/index.d.ts +4 -0
  65. package/lib/components/index.js +4 -0
  66. package/lib/components/settings/SettingsAppearance.js +92 -29
  67. package/lib/components/viewers/TMDataListItemViewer.d.ts +2 -1
  68. package/lib/components/viewers/TMDataListItemViewer.js +35 -71
  69. package/lib/components/viewers/TMDataUserIdItemViewer.d.ts +8 -0
  70. package/lib/components/viewers/TMDataUserIdItemViewer.js +39 -0
  71. package/lib/css/tm-sdkui.css +1 -1
  72. package/lib/helper/SDKUI_Globals.d.ts +19 -0
  73. package/lib/helper/SDKUI_Globals.js +11 -0
  74. package/lib/helper/SDKUI_Localizator.d.ts +15 -1
  75. package/lib/helper/SDKUI_Localizator.js +147 -1
  76. package/lib/helper/TMIcons.d.ts +2 -0
  77. package/lib/helper/TMIcons.js +6 -0
  78. package/lib/helper/TMPdfViewer.d.ts +8 -0
  79. package/lib/helper/TMPdfViewer.js +373 -0
  80. package/lib/helper/checkinCheckoutManager.d.ts +32 -2
  81. package/lib/helper/checkinCheckoutManager.js +115 -38
  82. package/lib/helper/devextremeCustomMessages.d.ts +30 -0
  83. package/lib/helper/devextremeCustomMessages.js +30 -0
  84. package/lib/helper/helpers.d.ts +2 -1
  85. package/lib/helper/helpers.js +14 -3
  86. package/lib/helper/index.d.ts +1 -0
  87. package/lib/helper/index.js +1 -0
  88. package/lib/helper/queryHelper.d.ts +1 -1
  89. package/lib/helper/queryHelper.js +33 -3
  90. package/lib/hooks/useCheckInOutOperations.d.ts +28 -0
  91. package/lib/hooks/useCheckInOutOperations.js +223 -0
  92. package/lib/hooks/useDataListItem.d.ts +12 -0
  93. package/lib/hooks/useDataListItem.js +131 -0
  94. package/lib/hooks/useDataUserIdItem.d.ts +10 -0
  95. package/lib/hooks/useDataUserIdItem.js +96 -0
  96. package/lib/hooks/useSettingsFeedback.d.ts +11 -0
  97. package/lib/hooks/useSettingsFeedback.js +38 -0
  98. package/lib/hooks/useWorkflowApprove.d.ts +4 -0
  99. package/lib/hooks/useWorkflowApprove.js +14 -1
  100. package/lib/index.d.ts +1 -0
  101. package/lib/index.js +1 -0
  102. package/lib/ts/types.d.ts +58 -1
  103. package/lib/utils/theme.d.ts +1 -1
  104. package/lib/utils/theme.js +1 -1
  105. package/package.json +5 -2
  106. package/lib/components/NewComponents/Notification/Notification.d.ts +0 -4
  107. package/lib/components/NewComponents/Notification/Notification.js +0 -60
  108. package/lib/components/NewComponents/Notification/NotificationContainer.d.ts +0 -8
  109. package/lib/components/NewComponents/Notification/NotificationContainer.js +0 -33
  110. package/lib/components/NewComponents/Notification/index.d.ts +0 -2
  111. package/lib/components/NewComponents/Notification/index.js +0 -2
  112. package/lib/components/NewComponents/Notification/styles.d.ts +0 -21
  113. package/lib/components/NewComponents/Notification/styles.js +0 -180
  114. package/lib/components/NewComponents/Notification/types.d.ts +0 -18
  115. package/lib/components/NewComponents/Notification/types.js +0 -1
  116. package/lib/components/features/search/TMSearchResultCheckoutInfoForm.d.ts +0 -8
@@ -1,8 +1,10 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { useState, useRef, useEffect } from 'react';
3
+ import { createPortal } from 'react-dom';
3
4
  import * as S from './styles';
4
- import { useIsMobile, useMenuPosition } from './hooks';
5
- const TMContextMenu = ({ items, trigger = 'right', children }) => {
5
+ import { useIsMobile, useMenuPosition, useIsIOS } from './hooks';
6
+ import { IconArrowLeft } from '../../../helper';
7
+ const TMContextMenu = ({ items, trigger = 'right', children, target, externalControl, keepOpenOnClick = false }) => {
6
8
  const [menuState, setMenuState] = useState({
7
9
  visible: false,
8
10
  position: { x: 0, y: 0 },
@@ -11,23 +13,208 @@ const TMContextMenu = ({ items, trigger = 'right', children }) => {
11
13
  });
12
14
  const [hoveredSubmenus, setHoveredSubmenus] = useState([]);
13
15
  const isMobile = useIsMobile();
16
+ const isIOS = useIsIOS();
14
17
  const menuRef = useRef(null);
15
18
  const triggerRef = useRef(null);
16
19
  const submenuTimeoutRef = useRef(null);
17
- const { openLeft, openUp } = useMenuPosition(menuRef, menuState.position);
20
+ const longPressTimeoutRef = useRef(null);
21
+ const touchStartPos = useRef(null);
22
+ const { openLeft, openUp, isCalculated } = useMenuPosition(menuRef, menuState.position);
18
23
  const handleClose = () => {
19
- setMenuState(prev => ({
20
- ...prev,
21
- visible: false,
22
- submenuStack: [items],
23
- parentNames: [],
24
- }));
24
+ if (externalControl) {
25
+ externalControl.onClose();
26
+ }
27
+ else {
28
+ setMenuState(prev => ({
29
+ ...prev,
30
+ visible: false,
31
+ submenuStack: [items],
32
+ parentNames: [],
33
+ }));
34
+ }
25
35
  setHoveredSubmenus([]);
26
36
  };
37
+ // Sync with external control when provided
38
+ useEffect(() => {
39
+ if (externalControl) {
40
+ setMenuState(prev => ({
41
+ ...prev,
42
+ visible: externalControl.visible,
43
+ position: externalControl.position,
44
+ submenuStack: [items],
45
+ parentNames: [],
46
+ }));
47
+ }
48
+ }, [externalControl, items]);
49
+ // iOS long-press support: attach touch listeners to target elements
50
+ // On long-press, dispatch synthetic contextmenu event to trigger existing handlers
51
+ useEffect(() => {
52
+ if (!target || !isIOS)
53
+ return;
54
+ const elements = document.querySelectorAll(target);
55
+ if (elements.length === 0)
56
+ return;
57
+ const touchStateMap = new WeakMap();
58
+ const handleTouchStart = (e) => {
59
+ const touchEvent = e;
60
+ const element = e.currentTarget;
61
+ const touch = touchEvent.touches[0];
62
+ // Prevent text selection during long press
63
+ e.preventDefault();
64
+ let state = touchStateMap.get(element);
65
+ if (!state) {
66
+ state = { timeout: null, startX: 0, startY: 0, longPressTriggered: false };
67
+ touchStateMap.set(element, state);
68
+ }
69
+ state.startX = touch.clientX;
70
+ state.startY = touch.clientY;
71
+ state.longPressTriggered = false;
72
+ if (state.timeout)
73
+ clearTimeout(state.timeout);
74
+ state.timeout = setTimeout(() => {
75
+ if (state)
76
+ state.longPressTriggered = true;
77
+ // Haptic feedback
78
+ if ('vibrate' in navigator)
79
+ navigator.vibrate(50);
80
+ const syntheticEvent = new MouseEvent('contextmenu', {
81
+ bubbles: true,
82
+ cancelable: true,
83
+ clientX: touch.clientX,
84
+ clientY: touch.clientY,
85
+ });
86
+ element.dispatchEvent(syntheticEvent);
87
+ if (state)
88
+ state.timeout = null;
89
+ }, 500);
90
+ };
91
+ const handleTouchMove = (e) => {
92
+ const touchEvent = e;
93
+ const element = e.currentTarget;
94
+ const state = touchStateMap.get(element);
95
+ if (!state?.timeout)
96
+ return;
97
+ const touch = touchEvent.touches[0];
98
+ const dx = Math.abs(touch.clientX - state.startX);
99
+ const dy = Math.abs(touch.clientY - state.startY);
100
+ if (dx > 10 || dy > 10) {
101
+ clearTimeout(state.timeout);
102
+ state.timeout = null;
103
+ }
104
+ };
105
+ const handleTouchEnd = (e) => {
106
+ const element = e.currentTarget;
107
+ const state = touchStateMap.get(element);
108
+ if (state?.timeout) {
109
+ clearTimeout(state.timeout);
110
+ state.timeout = null;
111
+ }
112
+ };
113
+ // Prevent click event after long-press was triggered
114
+ const handleClick = (e) => {
115
+ const element = e.currentTarget;
116
+ const state = touchStateMap.get(element);
117
+ if (state?.longPressTriggered) {
118
+ e.preventDefault();
119
+ e.stopPropagation();
120
+ e.stopImmediatePropagation();
121
+ state.longPressTriggered = false;
122
+ }
123
+ };
124
+ // Prevent default iOS context menu
125
+ const handleContextMenu = (e) => {
126
+ e.preventDefault();
127
+ e.stopPropagation();
128
+ return false;
129
+ };
130
+ // Attach listeners to all matching elements
131
+ elements.forEach(element => {
132
+ const el = element;
133
+ // Prevent iOS native callout and text selection
134
+ const style = el.style;
135
+ style.webkitTouchCallout = 'none';
136
+ style.webkitUserSelect = 'none';
137
+ style.userSelect = 'none';
138
+ el.addEventListener('touchstart', handleTouchStart, { passive: true });
139
+ el.addEventListener('touchmove', handleTouchMove, { passive: true });
140
+ el.addEventListener('touchend', handleTouchEnd);
141
+ el.addEventListener('touchcancel', handleTouchEnd);
142
+ el.addEventListener('contextmenu', handleContextMenu);
143
+ el.addEventListener('click', handleClick, { capture: true });
144
+ });
145
+ return () => {
146
+ elements.forEach(element => {
147
+ const el = element;
148
+ const style = el.style;
149
+ style.webkitTouchCallout = '';
150
+ style.webkitUserSelect = '';
151
+ style.userSelect = '';
152
+ el.removeEventListener('touchstart', handleTouchStart);
153
+ el.removeEventListener('touchmove', handleTouchMove);
154
+ el.removeEventListener('touchend', handleTouchEnd);
155
+ el.removeEventListener('touchcancel', handleTouchEnd);
156
+ el.removeEventListener('contextmenu', handleContextMenu);
157
+ el.removeEventListener('click', handleClick, { capture: true });
158
+ });
159
+ };
160
+ }, [target, isIOS]);
161
+ // Update items when they change while menu is visible (for keepOpenOnClick behavior)
162
+ useEffect(() => {
163
+ if (!keepOpenOnClick)
164
+ return;
165
+ if (!externalControl && menuState.visible) {
166
+ setMenuState(prev => ({
167
+ ...prev,
168
+ submenuStack: [items],
169
+ }));
170
+ // Update hoveredSubmenus with fresh items while keeping them open
171
+ setHoveredSubmenus(prev => {
172
+ if (prev.length === 0)
173
+ return prev;
174
+ // Rebuild hoveredSubmenus with updated items from the new items structure
175
+ return prev.map(submenu => {
176
+ // Find the matching submenu in the new items structure
177
+ const findSubmenuInItems = (searchItems) => {
178
+ for (const item of searchItems) {
179
+ if (item.submenu && item.submenu.length > 0) {
180
+ // Check if this submenu matches (compare first item name as identifier)
181
+ if (submenu.items.length > 0 && item.submenu.length > 0 &&
182
+ item.submenu[0].name === submenu.items[0].name) {
183
+ return item.submenu;
184
+ }
185
+ // Recursively search in nested submenus
186
+ const found = findSubmenuInItems(item.submenu);
187
+ if (found)
188
+ return found;
189
+ }
190
+ }
191
+ return null;
192
+ };
193
+ const updatedItems = findSubmenuInItems(items);
194
+ return {
195
+ ...submenu,
196
+ items: updatedItems || submenu.items, // Use updated items if found, otherwise keep old
197
+ };
198
+ });
199
+ });
200
+ }
201
+ }, [items, menuState.visible, externalControl, keepOpenOnClick]);
202
+ // Track when the menu was opened to prevent immediate close on iOS
203
+ const menuOpenedAtRef = useRef(0);
204
+ useEffect(() => {
205
+ if (menuState.visible) {
206
+ menuOpenedAtRef.current = Date.now();
207
+ }
208
+ }, [menuState.visible]);
27
209
  useEffect(() => {
28
210
  if (!menuState.visible)
29
211
  return;
30
212
  const handleClickOutside = (event) => {
213
+ // On iOS, prevent closing immediately after opening (within 300ms)
214
+ // This handles the case where touchend from long-press triggers touchstart listener
215
+ if (Date.now() - menuOpenedAtRef.current < 300) {
216
+ return;
217
+ }
31
218
  const target = event.target;
32
219
  // Check if click is inside main menu
33
220
  if (menuRef.current?.contains(target)) {
@@ -72,27 +259,73 @@ const TMContextMenu = ({ items, trigger = 'right', children }) => {
72
259
  });
73
260
  }
74
261
  };
262
+ // iOS-specific touch handlers for long press
263
+ const handleTouchStart = (e) => {
264
+ if (!isIOS || trigger !== 'right')
265
+ return;
266
+ const touch = e.touches[0];
267
+ touchStartPos.current = { x: touch.clientX, y: touch.clientY };
268
+ if (longPressTimeoutRef.current) {
269
+ clearTimeout(longPressTimeoutRef.current);
270
+ }
271
+ longPressTimeoutRef.current = setTimeout(() => {
272
+ if (touchStartPos.current) {
273
+ if ('vibrate' in navigator) {
274
+ navigator.vibrate(50);
275
+ }
276
+ setMenuState({
277
+ visible: true,
278
+ position: { x: touchStartPos.current.x, y: touchStartPos.current.y },
279
+ submenuStack: [items],
280
+ parentNames: [],
281
+ });
282
+ }
283
+ }, 500);
284
+ };
285
+ const handleTouchMove = (e) => {
286
+ if (!isIOS || trigger !== 'right' || !touchStartPos.current)
287
+ return;
288
+ const touch = e.touches[0];
289
+ const moveThreshold = 10; // pixels
290
+ // If finger moved too much, cancel long press
291
+ const deltaX = Math.abs(touch.clientX - touchStartPos.current.x);
292
+ const deltaY = Math.abs(touch.clientY - touchStartPos.current.y);
293
+ if (deltaX > moveThreshold || deltaY > moveThreshold) {
294
+ if (longPressTimeoutRef.current) {
295
+ clearTimeout(longPressTimeoutRef.current);
296
+ longPressTimeoutRef.current = null;
297
+ }
298
+ touchStartPos.current = null;
299
+ }
300
+ };
301
+ const handleTouchEnd = () => {
302
+ if (!isIOS || trigger !== 'right')
303
+ return;
304
+ if (longPressTimeoutRef.current) {
305
+ clearTimeout(longPressTimeoutRef.current);
306
+ longPressTimeoutRef.current = null;
307
+ }
308
+ touchStartPos.current = null;
309
+ };
75
310
  const handleItemClick = (item) => {
76
311
  if (item.disabled)
77
312
  return;
78
- // Always execute onClick if present
79
313
  if (item.onClick) {
80
314
  item.onClick();
81
315
  }
82
316
  if (item.submenu && item.submenu.length > 0) {
83
317
  if (isMobile) {
84
- // Mobile: Push submenu to stack
85
318
  setMenuState(prev => ({
86
319
  ...prev,
87
320
  submenuStack: [...prev.submenuStack, item.submenu],
88
321
  parentNames: [...prev.parentNames, item.name],
89
322
  }));
90
323
  }
91
- // Desktop: Submenus are handled by hover, don't close menu
92
324
  }
93
325
  else {
94
- // No submenu: close menu after executing action
95
- handleClose();
326
+ if (item.onClick || !keepOpenOnClick) {
327
+ handleClose();
328
+ }
96
329
  }
97
330
  };
98
331
  const handleBack = () => {
@@ -155,6 +388,9 @@ const TMContextMenu = ({ items, trigger = 'right', children }) => {
155
388
  if (submenuTimeoutRef.current) {
156
389
  clearTimeout(submenuTimeoutRef.current);
157
390
  }
391
+ if (longPressTimeoutRef.current) {
392
+ clearTimeout(longPressTimeoutRef.current);
393
+ }
158
394
  };
159
395
  }, []);
160
396
  const renderMenuItems = (menuItems, depth = 0) => {
@@ -172,16 +408,21 @@ const TMContextMenu = ({ items, trigger = 'right', children }) => {
172
408
  e.stopPropagation();
173
409
  // if (item.disabled) return;
174
410
  item.onRightIconClick?.();
411
+ handleClose();
175
412
  };
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));
413
+ 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), title: item.tooltip, 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
414
  });
178
415
  };
179
416
  const currentMenu = menuState.submenuStack.at(-1) || items;
180
417
  const currentParentName = menuState.parentNames.at(-1) || '';
181
- return (_jsxs(_Fragment, { children: [_jsx("div", { ref: triggerRef, onContextMenu: handleContextMenu, onClick: handleClick, onKeyDown: (e) => {
418
+ return (_jsxs(_Fragment, { children: [!externalControl && children && (_jsx("div", { ref: triggerRef, onContextMenu: handleContextMenu, onClick: handleClick, onTouchStart: handleTouchStart, onTouchMove: handleTouchMove, onTouchEnd: handleTouchEnd, onTouchCancel: handleTouchEnd, onKeyDown: (e) => {
182
419
  if (e.key === 'Enter' || e.key === ' ') {
183
420
  handleClick(e);
184
421
  }
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}`)))] }))] }));
422
+ }, role: "button", tabIndex: 0, style: {
423
+ display: 'inline-block',
424
+ WebkitTouchCallout: isIOS ? 'none' : undefined,
425
+ WebkitUserSelect: isIOS ? 'none' : undefined,
426
+ }, children: children })), menuState.visible && createPortal(_jsxs(_Fragment, { children: [_jsxs(S.MenuContainer, { ref: menuRef, "$x": menuState.position.x, "$y": menuState.position.y, "$openLeft": openLeft, "$openUp": openUp, "$isPositioned": isCalculated, "$externalControl": !!externalControl, children: [isMobile && menuState.parentNames.length > 0 && (_jsxs(S.MobileMenuHeader, { children: [_jsx(S.BackButton, { onClick: handleBack, "aria-label": "Go back", children: _jsx(IconArrowLeft, {}) }), _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}`)))] }), document.body)] }));
186
427
  };
187
428
  export default TMContextMenu;
@@ -1,3 +1,4 @@
1
+ export declare const useIsIOS: () => boolean;
1
2
  export declare const useIsMobile: () => boolean;
2
3
  export declare const useClickOutside: (callback: () => void) => import("react").RefObject<HTMLDivElement>;
3
4
  interface Position {
@@ -5,7 +6,11 @@ interface Position {
5
6
  y: number;
6
7
  }
7
8
  export declare const useMenuPosition: (menuRef: React.RefObject<HTMLDivElement | null>, position: Position) => {
9
+ isCalculated: boolean;
8
10
  openLeft: boolean;
9
11
  openUp: boolean;
10
12
  };
13
+ export declare const getContextMenuTarget: <T extends {
14
+ id: string;
15
+ }>(event: React.MouseEvent | undefined, focusedItem: T | undefined, selectedItem: T | undefined, dataSource: T[], idPrefix: string, isMobile: boolean) => T | undefined;
11
16
  export {};
@@ -1,4 +1,13 @@
1
- import { useState, useEffect, useRef } from 'react';
1
+ import { useState, useEffect, useLayoutEffect, useRef } from 'react';
2
+ export const useIsIOS = () => {
3
+ const [isIOS, setIsIOS] = useState(false);
4
+ useEffect(() => {
5
+ const iOS = /iPad|iPhone|iPod/.test(navigator.userAgent) ||
6
+ (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1);
7
+ setIsIOS(iOS);
8
+ }, []);
9
+ return isIOS;
10
+ };
2
11
  export const useIsMobile = () => {
3
12
  const [isMobile, setIsMobile] = useState(false);
4
13
  useEffect(() => {
@@ -31,9 +40,12 @@ export const useClickOutside = (callback) => {
31
40
  };
32
41
  export const useMenuPosition = (menuRef, position) => {
33
42
  const [adjustedPosition, setAdjustedPosition] = useState({ openLeft: false, openUp: false });
34
- useEffect(() => {
35
- if (!menuRef.current)
43
+ const [isCalculated, setIsCalculated] = useState(false);
44
+ useLayoutEffect(() => {
45
+ if (!menuRef.current) {
46
+ setIsCalculated(false);
36
47
  return;
48
+ }
37
49
  const menuRect = menuRef.current.getBoundingClientRect();
38
50
  const viewportWidth = window.innerWidth;
39
51
  const viewportHeight = window.innerHeight;
@@ -43,6 +55,28 @@ export const useMenuPosition = (menuRef, position) => {
43
55
  openLeft: spaceRight < menuRect.width + 20,
44
56
  openUp: spaceBottom < menuRect.height + 20,
45
57
  });
58
+ setIsCalculated(true);
46
59
  }, [position, menuRef]);
47
- return adjustedPosition;
60
+ return { ...adjustedPosition, isCalculated };
61
+ };
62
+ export const getContextMenuTarget = (event, focusedItem, selectedItem, dataSource, idPrefix, isMobile) => {
63
+ if (!event)
64
+ return undefined;
65
+ let targetItem = focusedItem ?? selectedItem;
66
+ if (!focusedItem && isMobile) {
67
+ // Find the actual item that was long-pressed by traversing up from the event target
68
+ let element = event.target;
69
+ while (element && element !== event.currentTarget) {
70
+ if (element.id && element.id.startsWith(`${idPrefix}-`)) {
71
+ const itemId = element.id.replace(`${idPrefix}-`, '');
72
+ const foundItem = dataSource.find(item => item.id === itemId);
73
+ if (foundItem) {
74
+ targetItem = foundItem;
75
+ break;
76
+ }
77
+ }
78
+ element = element.parentElement;
79
+ }
80
+ }
81
+ return targetItem;
48
82
  };
@@ -1,2 +1,5 @@
1
1
  export { default as ContextMenu } from './TMContextMenu';
2
2
  export type { TMContextMenuItemProps, TMContextMenuProps } from './types';
3
+ export { useLongPress, triggerContextMenuEvent } from './useLongPress';
4
+ export type { UseLongPressOptions } from './useLongPress';
5
+ export { useIsIOS } from './hooks';
@@ -1 +1,3 @@
1
1
  export { default as ContextMenu } from './TMContextMenu';
2
+ export { useLongPress, triggerContextMenuEvent } from './useLongPress';
3
+ export { useIsIOS } from './hooks';
@@ -3,6 +3,8 @@ export declare const MenuContainer: import("styled-components/dist/types").IStyl
3
3
  $y: number;
4
4
  $openLeft: boolean;
5
5
  $openUp: boolean;
6
+ $isPositioned: boolean;
7
+ $externalControl?: boolean;
6
8
  }>> & string;
7
9
  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
10
  $disabled?: boolean;
@@ -12,7 +14,9 @@ export declare const MenuItem: import("styled-components/dist/types").IStyledCom
12
14
  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
15
  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
16
  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;
17
+ export declare const RightIconButton: import("styled-components/dist/types").IStyledComponentBase<"web", import("styled-components").FastOmit<import("styled-components").FastOmit<import("styled-components/dist/types").Substitute<import("react").DetailedHTMLProps<import("react").ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>, Omit<import("react").DetailedHTMLProps<import("react").ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>, "ref"> & {
18
+ ref?: ((instance: HTMLButtonElement | null) => void | import("react").DO_NOT_USE_OR_YOU_WILL_BE_FIRED_CALLBACK_REF_RETURN_VALUES[keyof import("react").DO_NOT_USE_OR_YOU_WILL_BE_FIRED_CALLBACK_REF_RETURN_VALUES]) | import("react").RefObject<HTMLButtonElement> | null | undefined;
19
+ }>, never>, never>> & string;
16
20
  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
21
  $isMobile?: boolean;
18
22
  }>> & string;
@@ -25,7 +25,7 @@ export const MenuContainer = styled.div `
25
25
  right: ${props => props.$openLeft ? `${window.innerWidth - props.$x}px` : 'auto'};
26
26
  top: ${props => props.$openUp ? 'auto' : `${props.$y}px`};
27
27
  bottom: ${props => props.$openUp ? `${window.innerHeight - props.$y}px` : 'auto'};
28
- z-index: 10000;
28
+ z-index: 10100;
29
29
  background: #ffffff;
30
30
  border-radius: 12px;
31
31
  box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12),
@@ -36,6 +36,13 @@ export const MenuContainer = styled.div `
36
36
  animation: ${fadeIn} 0.15s ease-out;
37
37
  backdrop-filter: blur(10px);
38
38
  border: 1px solid rgba(0, 0, 0, 0.06);
39
+ opacity: ${props => props.$isPositioned ? 1 : 0};
40
+ transition: opacity 0.05s ease-in;
41
+
42
+ /* Reset color inheritance from parent with !important to override panel header styles */
43
+ & *:not(svg):not(.right-icon-btn):not(.right-icon-btn *) {
44
+ color: #1a1a1a !important;
45
+ }
39
46
 
40
47
  [data-theme='dark'] & {
41
48
  background: #2a2a2a;
@@ -43,14 +50,27 @@ export const MenuContainer = styled.div `
43
50
  box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4),
44
51
  0 2px 8px rgba(0, 0, 0, 0.3);
45
52
  }
53
+
54
+ [data-theme='dark'] & *:not(svg):not(.right-icon-btn):not(.right-icon-btn *) {
55
+ color: #e0e0e0 !important;
56
+ }
57
+
58
+ ${props => props.$externalControl && `
59
+ @media (max-width: 768px) {
60
+ left: 100px !important;
61
+ right: 100px !important;
62
+ max-width: calc(100vw - 200px);
63
+ width: auto;
64
+ min-width: auto;
65
+ }
66
+ `}
46
67
  `;
47
68
  export const MenuItem = styled.div `
48
69
  display: flex;
49
70
  align-items: center;
50
71
  justify-content: space-between;
51
72
  padding: 4px 12px;
52
- cursor: ${props => props.$disabled ? 'not-allowed' : 'pointer'};
53
- opacity: ${props => props.$disabled ? 0.4 : 1};
73
+ cursor: ${props => props.$disabled ? 'default' : 'pointer'};
54
74
  transition: all 0.15s ease;
55
75
  position: relative;
56
76
  user-select: none;
@@ -63,12 +83,26 @@ export const MenuItem = styled.div `
63
83
  padding-top: 8px;
64
84
  `}
65
85
 
86
+ /* Apply opacity only to direct children except right-icon-btn */
87
+ & > *:not(.right-icon-btn) {
88
+ opacity: ${props => props.$disabled ? 0.4 : 1};
89
+ }
90
+
91
+ /* Right icon button hidden by default, shown on hover */
92
+ & .right-icon-btn {
93
+ cursor: pointer !important;
94
+ }
95
+
66
96
  &:hover {
67
97
  ${props => !props.$disabled && `
68
98
  background: linear-gradient(90deg, #f0f7ff 0%, #e6f2ff 100%);
69
99
  color: #0066cc;
70
- padding-left: 14px;
71
100
  `}
101
+
102
+ /* Show right icon on hover */
103
+ & .right-icon-btn {
104
+ opacity: 1 !important;
105
+ }
72
106
  }
73
107
 
74
108
  &:active {
@@ -99,8 +133,8 @@ export const MenuItem = styled.div `
99
133
  }
100
134
 
101
135
  @media (max-width: 768px) {
102
- padding: 14px 16px;
103
- font-size: 15px;
136
+ padding: 4px 10px;
137
+ font-size: 12px;
104
138
  }
105
139
  `;
106
140
  export const MenuItemContent = styled.div `
@@ -124,23 +158,23 @@ export const MenuItemName = styled.span `
124
158
  overflow-wrap: break-word;
125
159
  line-height: 1.4;
126
160
  `;
127
- export const RightIconButton = styled.button `
161
+ export const RightIconButton = styled.button.attrs({
162
+ className: 'right-icon-btn'
163
+ }) `
128
164
  display: flex;
129
165
  align-items: center;
130
166
  justify-content: center;
131
167
  background: transparent;
132
168
  border: none;
133
- cursor: pointer;
169
+ cursor: pointer !important;
134
170
  padding: 4px 8px;
135
171
  margin-left: 8px;
136
172
  border-radius: 6px;
137
- color: inherit;
138
173
  font-size: 14px;
139
- opacity: 0.6;
140
- transition: all 0.15s ease;
174
+ opacity: 0 !important;
175
+ transition: opacity 0.15s ease, background 0.15s ease, transform 0.15s ease;
141
176
 
142
177
  &:hover {
143
- opacity: 1;
144
178
  background: rgba(0, 0, 0, 0.05);
145
179
  transform: scale(1.1);
146
180
  }
@@ -189,7 +223,7 @@ export const Submenu = styled.div `
189
223
  // If openUp is true, anchor to bottom and grow upward
190
224
  return props.$openUp ? `${globalThis.innerHeight - props.$parentRect.bottom - 8}px` : 'auto';
191
225
  }};
192
- z-index: 10001;
226
+ z-index: 10101;
193
227
  background: #ffffff;
194
228
  border-radius: 12px;
195
229
  box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12),
@@ -223,17 +257,26 @@ export const Submenu = styled.div `
223
257
  background: transparent;
224
258
  }
225
259
 
260
+ /* Reset color inheritance from parent with !important to override panel header styles */
261
+ & *:not(svg):not(.right-icon-btn):not(.right-icon-btn *) {
262
+ color: #1a1a1a !important;
263
+ }
264
+
226
265
  [data-theme='dark'] & {
227
266
  background: #2a2a2a;
228
267
  border-color: rgba(255, 255, 255, 0.1);
229
268
  box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4),
230
269
  0 2px 8px rgba(0, 0, 0, 0.3);
231
270
  }
271
+
272
+ [data-theme='dark'] & *:not(svg):not(.right-icon-btn):not(.right-icon-btn *) {
273
+ color: #e0e0e0 !important;
274
+ }
232
275
  `;
233
276
  export const MobileMenuHeader = styled.div `
234
277
  display: flex;
235
278
  align-items: center;
236
- padding: 12px 16px;
279
+ padding: 4px 8px;
237
280
  border-bottom: 1px solid rgba(0, 0, 0, 0.08);
238
281
  margin-bottom: 8px;
239
282
  gap: 12px;
@@ -249,33 +292,18 @@ export const BackButton = styled.button `
249
292
  display: flex;
250
293
  align-items: center;
251
294
  justify-content: center;
252
- background: #0066cc;
253
- color: white;
254
295
  border: none;
255
296
  border-radius: 8px;
256
297
  width: 32px;
257
298
  height: 32px;
258
299
  cursor: pointer;
259
- font-size: 18px;
260
300
  transition: all 0.15s ease;
261
-
262
- &:hover {
263
- background: #0052a3;
264
- transform: scale(1.05);
265
- }
301
+ font-size: 16px;
302
+ transform: translateY(-2px);
266
303
 
267
304
  &:active {
268
305
  transform: scale(0.95);
269
306
  }
270
-
271
- [data-theme='dark'] & {
272
- background: #4db8ff;
273
- color: #1a1a1a;
274
-
275
- &:hover {
276
- background: #66c2ff;
277
- }
278
- }
279
307
  `;
280
308
  export const HeaderTitle = styled.h3 `
281
309
  margin: 0;
@@ -1,4 +1,5 @@
1
1
  export interface TMContextMenuItemProps {
2
+ id?: string;
2
3
  name: string;
3
4
  icon?: React.ReactNode;
4
5
  disabled?: boolean;
@@ -8,11 +9,23 @@ export interface TMContextMenuItemProps {
8
9
  rightIcon?: React.ReactNode;
9
10
  onRightIconClick?: () => void;
10
11
  beginGroup?: boolean;
12
+ tooltip?: string;
13
+ operationType?: 'singleRow' | 'multiRow';
11
14
  }
12
15
  export interface TMContextMenuProps {
13
16
  items: TMContextMenuItemProps[];
14
17
  trigger?: 'right' | 'left';
15
18
  children?: React.ReactNode;
19
+ target?: string;
20
+ externalControl?: {
21
+ visible: boolean;
22
+ position: {
23
+ x: number;
24
+ y: number;
25
+ };
26
+ onClose: () => void;
27
+ };
28
+ keepOpenOnClick?: boolean;
16
29
  }
17
30
  export interface Position {
18
31
  x: number;