@addev-be/ui 3.3.0 → 3.3.3
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/assets/icons/arrow-down-1-9.svg +1 -1
- package/assets/icons/arrow-down-a-z.svg +1 -1
- package/assets/icons/arrow-up-z-a.svg +1 -1
- package/assets/icons/check.svg +1 -1
- package/assets/icons/circle-check.svg +1 -1
- package/assets/icons/down.svg +1 -1
- package/assets/icons/filter-full.svg +1 -1
- package/assets/icons/filter.svg +1 -1
- package/assets/icons/hashtag.svg +1 -1
- package/assets/icons/image-slash.svg +1 -1
- package/assets/icons/left.svg +1 -1
- package/assets/icons/magnifier.svg +1 -1
- package/assets/icons/phone.svg +1 -1
- package/assets/icons/right.svg +1 -1
- package/assets/icons/sort-calendar-ascending.svg +5 -5
- package/assets/icons/spinner-third.svg +1 -1
- package/assets/icons/table-columns.svg +1 -1
- package/assets/icons/table-footer-slash.svg +4 -4
- package/assets/icons/table-footer.svg +3 -3
- package/assets/icons/table.svg +1 -1
- package/assets/icons/up.svg +1 -1
- package/assets/icons/user-tie.svg +1 -1
- package/assets/icons/x-bar.svg +3 -3
- package/dist/components/grid/DataGrid/helpers/columns.js +22 -2
- package/dist/components/grid/DataGrid/types.d.ts +2 -7
- package/dist/components/grid/SqlRequestDataGrid/helpers/columns.js +13 -1
- package/dist/helpers/styled/typography.d.ts +5 -5
- package/dist/helpers/styled/typography.js +5 -5
- package/eslint.config.js +3 -3
- package/package.json +2 -2
- package/src/components/auth/LoginForm.tsx +92 -92
- package/src/components/auth/LoginPage.tsx +32 -32
- package/src/components/auth/PasswordRecoveryForm.tsx +53 -53
- package/src/components/auth/PasswordResetForm.tsx +114 -114
- package/src/components/auth/index.ts +4 -4
- package/src/components/auth/styles.ts +14 -14
- package/src/components/common/Accordion/Accordion.tsx +140 -140
- package/src/components/common/Accordion/index.ts +2 -2
- package/src/components/common/Avatar/index.tsx +54 -54
- package/src/components/common/Avatar/styles.ts +61 -61
- package/src/components/common/Card/index.tsx +17 -17
- package/src/components/common/Card/styles.ts +38 -38
- package/src/components/common/ContextMenu/index.tsx +79 -79
- package/src/components/common/ContextMenu/styles.ts +119 -119
- package/src/components/common/Ellipsis.tsx +33 -33
- package/src/components/common/Label.tsx +93 -93
- package/src/components/common/Message/index.tsx +57 -57
- package/src/components/common/Message/styles.ts +44 -44
- package/src/components/common/PropertiesList/PropertyInput.tsx +173 -173
- package/src/components/common/PropertiesList/index.tsx +48 -48
- package/src/components/common/PropertiesList/types.ts +60 -60
- package/src/components/common/TabsView/TabsList.tsx +49 -49
- package/src/components/common/TabsView/TabsView.tsx +42 -42
- package/src/components/common/TabsView/index.ts +3 -3
- package/src/components/common/TabsView/styles.ts +83 -83
- package/src/components/common/TabsView/types.ts +12 -12
- package/src/components/common/TreeView/TreeContext.tsx +379 -379
- package/src/components/common/TreeView/TreeView.tsx +281 -281
- package/src/components/common/TreeView/index.ts +3 -3
- package/src/components/common/index.ts +10 -10
- package/src/components/forms/AutoTextArea.tsx +48 -48
- package/src/components/forms/BillitIdentifier/index.tsx +78 -78
- package/src/components/forms/BillitIdentifier/styles.tsx +43 -43
- package/src/components/forms/Button.tsx +130 -130
- package/src/components/forms/Form/Checkbox.tsx +12 -12
- package/src/components/forms/Form/CustomSelect.tsx +86 -86
- package/src/components/forms/Form/FormGroup.tsx +33 -33
- package/src/components/forms/Form/Input.tsx +16 -16
- package/src/components/forms/Form/Row.tsx +28 -28
- package/src/components/forms/Form/Select.tsx +99 -99
- package/src/components/forms/Form/TextArea.tsx +17 -17
- package/src/components/forms/Form/index.tsx +48 -48
- package/src/components/forms/Form/styles.ts +148 -148
- package/src/components/forms/IconButton.tsx +61 -61
- package/src/components/forms/IndeterminateCheckbox.tsx +46 -46
- package/src/components/forms/NumberInput.tsx +53 -53
- package/src/components/forms/Select.tsx +34 -34
- package/src/components/forms/VerticalLabel.tsx +20 -20
- package/src/components/forms/index.ts +11 -11
- package/src/components/forms/styles.ts +42 -42
- package/src/components/grid/DataGrid/DataGridCell.tsx +82 -82
- package/src/components/grid/DataGrid/DataGridColumnsModal/helpers.ts +9 -9
- package/src/components/grid/DataGrid/DataGridColumnsModal/hooks.tsx +59 -59
- package/src/components/grid/DataGrid/DataGridColumnsModal/index.tsx +182 -182
- package/src/components/grid/DataGrid/DataGridColumnsModal/styles.ts +104 -104
- package/src/components/grid/DataGrid/DataGridEditableCell/CheckboxEditableCell.tsx +37 -37
- package/src/components/grid/DataGrid/DataGridEditableCell/DateEditableCell.tsx +38 -38
- package/src/components/grid/DataGrid/DataGridEditableCell/NumberEditableCell.tsx +71 -71
- package/src/components/grid/DataGrid/DataGridEditableCell/TextEditableCell.tsx +37 -37
- package/src/components/grid/DataGrid/DataGridEditableCell/index.tsx +112 -112
- package/src/components/grid/DataGrid/DataGridEditableCell/styles.ts +35 -35
- package/src/components/grid/DataGrid/DataGridEditableCell/types.ts +18 -18
- package/src/components/grid/DataGrid/DataGridFilterMenu/FilterValuesScroller.tsx +129 -129
- package/src/components/grid/DataGrid/DataGridFilterMenu/helpers.ts +23 -23
- package/src/components/grid/DataGrid/DataGridFilterMenu/hooks.tsx +81 -81
- package/src/components/grid/DataGrid/DataGridFilterMenu/index.tsx +370 -370
- package/src/components/grid/DataGrid/DataGridFilterMenu/styles.ts +97 -97
- package/src/components/grid/DataGrid/DataGridFooter.tsx +48 -48
- package/src/components/grid/DataGrid/DataGridHeader.tsx +74 -74
- package/src/components/grid/DataGrid/DataGridHeaderCell.tsx +112 -112
- package/src/components/grid/DataGrid/DataGridRowTemplate.tsx +90 -90
- package/src/components/grid/DataGrid/DataGridToolbar.tsx +134 -134
- package/src/components/grid/DataGrid/FilterModalContent/index.tsx +137 -137
- package/src/components/grid/DataGrid/FilterModalContent/styles.ts +22 -22
- package/src/components/grid/DataGrid/constants.ts +6 -6
- package/src/components/grid/DataGrid/helpers/columns.tsx +458 -452
- package/src/components/grid/DataGrid/helpers/filters.ts +287 -287
- package/src/components/grid/DataGrid/helpers/index.ts +2 -2
- package/src/components/grid/DataGrid/hooks/index.ts +29 -29
- package/src/components/grid/DataGrid/hooks/useDataGrid.tsx +383 -383
- package/src/components/grid/DataGrid/hooks/useDataGridChangedRows.ts +97 -97
- package/src/components/grid/DataGrid/hooks/useDataGridCopy.ts +174 -174
- package/src/components/grid/DataGrid/hooks/useDataGridSettings.ts +48 -48
- package/src/components/grid/DataGrid/hooks/useRefreshModal.tsx +48 -48
- package/src/components/grid/DataGrid/index.tsx +111 -111
- package/src/components/grid/DataGrid/styles.ts +446 -446
- package/src/components/grid/DataGrid/types.ts +375 -380
- package/src/components/grid/SqlRequestDataGrid/helpers/columns.tsx +528 -526
- package/src/components/grid/SqlRequestDataGrid/helpers/index.ts +2 -2
- package/src/components/grid/SqlRequestDataGrid/helpers/rows.ts +24 -24
- package/src/components/grid/SqlRequestDataGrid/helpers/sqlRequests.ts +17 -17
- package/src/components/grid/SqlRequestDataGrid/index.tsx +417 -417
- package/src/components/grid/SqlRequestDataGrid/styles.ts +15 -15
- package/src/components/grid/SqlRequestDataGrid/types.ts +74 -74
- package/src/components/grid/SqlRequestForeignList/index.tsx +254 -254
- package/src/components/grid/SqlRequestForeignList/styles.ts +43 -43
- package/src/components/grid/SqlRequestForeignList/types.ts +32 -32
- package/src/components/grid/SqlRequestGrid/filters/FiltersSidebar.tsx +108 -108
- package/src/components/grid/SqlRequestGrid/filters/styles.ts +88 -88
- package/src/components/grid/SqlRequestGrid/helpers/index.ts +1 -1
- package/src/components/grid/SqlRequestGrid/helpers/sqlRequests.ts +16 -16
- package/src/components/grid/SqlRequestGrid/index.tsx +304 -304
- package/src/components/grid/SqlRequestGrid/styles.ts +20 -20
- package/src/components/grid/SqlRequestGrid/types.ts +73 -73
- package/src/components/grid/VirtualScroller/hooks.ts +71 -71
- package/src/components/grid/VirtualScroller/index.tsx +89 -89
- package/src/components/grid/VirtualScroller/styles.ts +57 -57
- package/src/components/grid/VirtualScroller/types.ts +10 -10
- package/src/components/grid/index.ts +11 -11
- package/src/components/layout/Columns.ts +28 -28
- package/src/components/layout/ContextMenu/ContextMenu.tsx +471 -471
- package/src/components/layout/ContextMenu/index.ts +23 -23
- package/src/components/layout/Dropdown/index.tsx +113 -113
- package/src/components/layout/Dropdown/styles.ts +53 -53
- package/src/components/layout/Flexbox.ts +21 -21
- package/src/components/layout/Grid/index.tsx +8 -8
- package/src/components/layout/Grid/styles.ts +34 -34
- package/src/components/layout/LeftMenu/LeftMenu.tsx +89 -89
- package/src/components/layout/LeftMenu/index.ts +2 -2
- package/src/components/layout/Loading/index.tsx +29 -29
- package/src/components/layout/Loading/styles.ts +29 -29
- package/src/components/layout/Masonry/index.tsx +29 -29
- package/src/components/layout/Masonry/styles.ts +20 -20
- package/src/components/layout/Modal/index.tsx +51 -51
- package/src/components/layout/Modal/styles.ts +125 -125
- package/src/components/layout/PanelContainer/PanelContainer.tsx +198 -198
- package/src/components/layout/PanelContainer/index.ts +2 -2
- package/src/components/layout/index.ts +10 -10
- package/src/components/search/HighlightedText.tsx +41 -41
- package/src/components/search/QuickSearchBar.tsx +125 -125
- package/src/components/search/QuickSearchResults.tsx +90 -90
- package/src/components/search/index.ts +5 -5
- package/src/components/search/styles.ts +96 -96
- package/src/components/search/types.ts +29 -29
- package/src/config/index.ts +10 -10
- package/src/helpers/cn.ts +6 -6
- package/src/helpers/components.ts +9 -9
- package/src/helpers/dates.ts +17 -17
- package/src/helpers/getScrollbarSize.ts +14 -14
- package/src/helpers/index.ts +8 -8
- package/src/helpers/numbers.ts +63 -63
- package/src/helpers/responsive.ts +83 -83
- package/src/helpers/styled/size.ts +24 -24
- package/src/helpers/styled/space.ts +111 -111
- package/src/helpers/styled/typography.ts +25 -25
- package/src/helpers/text.ts +13 -13
- package/src/helpers/types.ts +9 -9
- package/src/hooks/index.ts +9 -9
- package/src/hooks/providers.ts +14 -14
- package/src/hooks/useContainerMediaQuery.ts +7 -7
- package/src/hooks/useElementSize.ts +24 -24
- package/src/hooks/useMediaQuery.ts +9 -9
- package/src/hooks/useMediaQueryForWidth.ts +35 -35
- package/src/hooks/useMutableState.test.ts +410 -410
- package/src/hooks/useMutableState.ts +39 -39
- package/src/hooks/useShowArchived.ts +28 -28
- package/src/hooks/useWindowSize.ts +20 -20
- package/src/icons/index.tsx +138 -138
- package/src/providers/AuthenticationProvider/helpers.ts +3 -3
- package/src/providers/AuthenticationProvider/index.tsx +303 -303
- package/src/providers/LoadingProvider/index.tsx +47 -47
- package/src/providers/PortalsProvider/index.tsx +54 -54
- package/src/providers/PortalsProvider/styles.ts +31 -31
- package/src/providers/SettingsProvider/index.tsx +70 -70
- package/src/providers/ToastProvider/index.tsx +93 -93
- package/src/providers/TrackingProvider/hooks.ts +14 -14
- package/src/providers/TrackingProvider/index.tsx +71 -71
- package/src/providers/UiProviders/index.tsx +74 -74
- package/src/providers/UiProviders/styles.ts +10 -10
- package/src/providers/index.ts +6 -6
- package/src/services/HttpService.ts +92 -92
- package/src/services/WebSocketService.ts +155 -155
- package/src/services/advancedRequests.ts +102 -102
- package/src/services/base.ts +23 -23
- package/src/services/globalSearch.ts +32 -32
- package/src/services/hooks.ts +92 -92
- package/src/services/index.ts +20 -20
- package/src/services/requests/auth.ts +44 -44
- package/src/services/requests/generic.ts +76 -76
- package/src/services/requests/printing.ts +12 -12
- package/src/services/requests/tracking.ts +12 -12
- package/src/services/requests/userProfiles.ts +35 -35
- package/src/services/requests/users.ts +28 -28
- package/src/services/smartQueries.ts +122 -122
- package/src/services/sqlRequests.ts +119 -119
- package/src/services/types/auth.ts +98 -98
- package/src/services/types/base.ts +10 -10
- package/src/services/types/generic.ts +102 -102
- package/src/services/types/printing.ts +10 -10
- package/src/services/types/tracking.ts +29 -29
- package/src/services/types/userProfiles.ts +79 -79
- package/src/services/types/users.ts +74 -74
- package/src/services/updateSqlRequests.ts +32 -32
- package/src/styles/animations.scss +30 -30
- package/src/styles/index.scss +42 -42
- package/src/theme/ThemeProvider.ts +73 -73
- package/src/theme/defaultTheme.ts +471 -471
- package/src/theme/helpers.ts +89 -89
- package/src/theme/index.ts +4 -4
- package/src/theme/types.ts +139 -139
- package/src/types/index.ts +8 -8
- package/src/typings.d.ts +2 -2
- package/tsconfig.json +41 -41
- package/tsconfig.tsbuildinfo +1 -0
|
@@ -1,471 +1,471 @@
|
|
|
1
|
-
import React, {
|
|
2
|
-
HTMLAttributes,
|
|
3
|
-
createContext,
|
|
4
|
-
useContext,
|
|
5
|
-
useEffect,
|
|
6
|
-
useRef,
|
|
7
|
-
useState,
|
|
8
|
-
} from 'react';
|
|
9
|
-
|
|
10
|
-
import { cn } from '@helpers/cn';
|
|
11
|
-
|
|
12
|
-
interface GlobalContextMenuContextValue {
|
|
13
|
-
currentMenuId: string | null;
|
|
14
|
-
setCurrentMenuId: (id: string | null) => void;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
const GlobalContextMenuContext = createContext<
|
|
18
|
-
GlobalContextMenuContextValue | undefined
|
|
19
|
-
>(undefined);
|
|
20
|
-
|
|
21
|
-
export const GlobalContextMenuProvider: React.FC<{
|
|
22
|
-
children: React.ReactNode;
|
|
23
|
-
}> = ({ children }) => {
|
|
24
|
-
const [currentMenuId, setCurrentMenuId] = useState<string | null>(null);
|
|
25
|
-
|
|
26
|
-
useEffect(() => {
|
|
27
|
-
const handleClick = () => setCurrentMenuId(null);
|
|
28
|
-
document.addEventListener('click', handleClick);
|
|
29
|
-
return () => document.removeEventListener('click', handleClick);
|
|
30
|
-
}, []);
|
|
31
|
-
|
|
32
|
-
return (
|
|
33
|
-
<GlobalContextMenuContext.Provider
|
|
34
|
-
value={{ currentMenuId, setCurrentMenuId }}
|
|
35
|
-
>
|
|
36
|
-
{children}
|
|
37
|
-
</GlobalContextMenuContext.Provider>
|
|
38
|
-
);
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
const useGlobalContextMenu = () => {
|
|
42
|
-
const context = useContext(GlobalContextMenuContext);
|
|
43
|
-
if (!context) {
|
|
44
|
-
throw new Error(
|
|
45
|
-
'ContextMenu must be used within GlobalContextMenuProvider'
|
|
46
|
-
);
|
|
47
|
-
}
|
|
48
|
-
return context;
|
|
49
|
-
};
|
|
50
|
-
|
|
51
|
-
interface ContextMenuContextValue {
|
|
52
|
-
menuId: string;
|
|
53
|
-
isOpen: boolean;
|
|
54
|
-
position: { x: number; y: number };
|
|
55
|
-
openMenu: (x: number, y: number) => void;
|
|
56
|
-
closeMenu: () => void;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const ContextMenuContext = createContext<ContextMenuContextValue | undefined>(
|
|
60
|
-
undefined
|
|
61
|
-
);
|
|
62
|
-
|
|
63
|
-
const useContextMenu = () => {
|
|
64
|
-
const context = useContext(ContextMenuContext);
|
|
65
|
-
if (!context) {
|
|
66
|
-
throw new Error('ContextMenu components must be used within a ContextMenu');
|
|
67
|
-
}
|
|
68
|
-
return context;
|
|
69
|
-
};
|
|
70
|
-
|
|
71
|
-
let menuIdCounter = 0;
|
|
72
|
-
|
|
73
|
-
export interface ContextMenuProps {
|
|
74
|
-
children: React.ReactNode;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
export const ContextMenu: React.FC<ContextMenuProps> = ({ children }) => {
|
|
78
|
-
const { currentMenuId, setCurrentMenuId } = useGlobalContextMenu();
|
|
79
|
-
const [menuId] = useState(() => `context-menu-${menuIdCounter++}`);
|
|
80
|
-
const [position, setPosition] = useState({ x: 0, y: 0 });
|
|
81
|
-
|
|
82
|
-
const isOpen = currentMenuId === menuId;
|
|
83
|
-
|
|
84
|
-
const openMenu = (x: number, y: number) => {
|
|
85
|
-
setPosition({ x, y });
|
|
86
|
-
setCurrentMenuId(menuId);
|
|
87
|
-
};
|
|
88
|
-
|
|
89
|
-
const closeMenu = () => {
|
|
90
|
-
if (currentMenuId === menuId) {
|
|
91
|
-
setCurrentMenuId(null);
|
|
92
|
-
}
|
|
93
|
-
};
|
|
94
|
-
|
|
95
|
-
return (
|
|
96
|
-
<ContextMenuContext.Provider
|
|
97
|
-
value={{ menuId, isOpen, position, openMenu, closeMenu }}
|
|
98
|
-
>
|
|
99
|
-
{children}
|
|
100
|
-
</ContextMenuContext.Provider>
|
|
101
|
-
);
|
|
102
|
-
};
|
|
103
|
-
|
|
104
|
-
export interface ContextMenuTriggerProps extends HTMLAttributes<HTMLDivElement> {
|
|
105
|
-
children: React.ReactNode;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
export const ContextMenuTrigger = React.forwardRef<
|
|
109
|
-
HTMLDivElement,
|
|
110
|
-
ContextMenuTriggerProps
|
|
111
|
-
>(({ children, className, ...props }, ref) => {
|
|
112
|
-
const { openMenu } = useContextMenu();
|
|
113
|
-
|
|
114
|
-
const handleContextMenu = (e: React.MouseEvent) => {
|
|
115
|
-
e.preventDefault();
|
|
116
|
-
openMenu(e.clientX, e.clientY);
|
|
117
|
-
};
|
|
118
|
-
|
|
119
|
-
return (
|
|
120
|
-
<div
|
|
121
|
-
ref={ref}
|
|
122
|
-
onContextMenu={handleContextMenu}
|
|
123
|
-
className={cn('cursor-pointer', className)}
|
|
124
|
-
{...props}
|
|
125
|
-
>
|
|
126
|
-
{children}
|
|
127
|
-
</div>
|
|
128
|
-
);
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
ContextMenuTrigger.displayName = 'ContextMenuTrigger';
|
|
132
|
-
|
|
133
|
-
export interface ContextMenuContentProps extends HTMLAttributes<HTMLDivElement> {
|
|
134
|
-
children: React.ReactNode;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
export const ContextMenuContent = React.forwardRef<
|
|
138
|
-
HTMLDivElement,
|
|
139
|
-
ContextMenuContentProps
|
|
140
|
-
>(({ children, className, ...props }, ref) => {
|
|
141
|
-
const { isOpen, position } = useContextMenu();
|
|
142
|
-
const contentRef = useRef<HTMLDivElement>(null);
|
|
143
|
-
|
|
144
|
-
useEffect(() => {
|
|
145
|
-
if (isOpen && contentRef.current) {
|
|
146
|
-
const rect = contentRef.current.getBoundingClientRect();
|
|
147
|
-
const viewportWidth = window.innerWidth;
|
|
148
|
-
const viewportHeight = window.innerHeight;
|
|
149
|
-
|
|
150
|
-
let adjustedX = position.x;
|
|
151
|
-
let adjustedY = position.y;
|
|
152
|
-
|
|
153
|
-
if (position.x + rect.width > viewportWidth) {
|
|
154
|
-
adjustedX = viewportWidth - rect.width - 10;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
if (position.y + rect.height > viewportHeight) {
|
|
158
|
-
adjustedY = viewportHeight - rect.height - 10;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
contentRef.current.style.left = `${adjustedX}px`;
|
|
162
|
-
contentRef.current.style.top = `${adjustedY}px`;
|
|
163
|
-
}
|
|
164
|
-
}, [isOpen, position]);
|
|
165
|
-
|
|
166
|
-
if (!isOpen) return null;
|
|
167
|
-
|
|
168
|
-
return (
|
|
169
|
-
<div
|
|
170
|
-
ref={(node) => {
|
|
171
|
-
if (typeof ref === 'function') ref(node);
|
|
172
|
-
else if (ref) ref.current = node;
|
|
173
|
-
contentRef.current = node;
|
|
174
|
-
}}
|
|
175
|
-
className={cn(
|
|
176
|
-
'fixed z-50 min-w-[12rem] bg-popover text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95',
|
|
177
|
-
'rounded-theme-md p-theme-xs',
|
|
178
|
-
className
|
|
179
|
-
)}
|
|
180
|
-
onClick={(e) => e.stopPropagation()}
|
|
181
|
-
{...props}
|
|
182
|
-
>
|
|
183
|
-
{children}
|
|
184
|
-
</div>
|
|
185
|
-
);
|
|
186
|
-
});
|
|
187
|
-
|
|
188
|
-
ContextMenuContent.displayName = 'ContextMenuContent';
|
|
189
|
-
|
|
190
|
-
export interface ContextMenuItemProps extends HTMLAttributes<HTMLDivElement> {
|
|
191
|
-
children: React.ReactNode;
|
|
192
|
-
disabled?: boolean;
|
|
193
|
-
onSelect?: () => void;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
export const ContextMenuItem = React.forwardRef<
|
|
197
|
-
HTMLDivElement,
|
|
198
|
-
ContextMenuItemProps
|
|
199
|
-
>(({ children, className, disabled, onSelect, ...props }, ref) => {
|
|
200
|
-
const { closeMenu } = useContextMenu();
|
|
201
|
-
|
|
202
|
-
const handleClick = () => {
|
|
203
|
-
if (!disabled && onSelect) {
|
|
204
|
-
onSelect();
|
|
205
|
-
closeMenu();
|
|
206
|
-
}
|
|
207
|
-
};
|
|
208
|
-
|
|
209
|
-
return (
|
|
210
|
-
<div
|
|
211
|
-
ref={ref}
|
|
212
|
-
className={cn(
|
|
213
|
-
'relative flex cursor-pointer select-none items-center text-sm outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground',
|
|
214
|
-
'px-theme-sm py-theme-xs rounded-theme-sm',
|
|
215
|
-
disabled && 'pointer-events-none opacity-50',
|
|
216
|
-
className
|
|
217
|
-
)}
|
|
218
|
-
onClick={handleClick}
|
|
219
|
-
{...props}
|
|
220
|
-
>
|
|
221
|
-
{children}
|
|
222
|
-
</div>
|
|
223
|
-
);
|
|
224
|
-
});
|
|
225
|
-
|
|
226
|
-
ContextMenuItem.displayName = 'ContextMenuItem';
|
|
227
|
-
|
|
228
|
-
export interface ContextMenuSeparatorProps extends HTMLAttributes<HTMLDivElement> {}
|
|
229
|
-
|
|
230
|
-
export const ContextMenuSeparator = React.forwardRef<
|
|
231
|
-
HTMLDivElement,
|
|
232
|
-
ContextMenuSeparatorProps
|
|
233
|
-
>(({ className, ...props }, ref) => {
|
|
234
|
-
return (
|
|
235
|
-
<div
|
|
236
|
-
ref={ref}
|
|
237
|
-
className={cn('bg-border h-px my-theme-xs', className)}
|
|
238
|
-
{...props}
|
|
239
|
-
/>
|
|
240
|
-
);
|
|
241
|
-
});
|
|
242
|
-
|
|
243
|
-
ContextMenuSeparator.displayName = 'ContextMenuSeparator';
|
|
244
|
-
|
|
245
|
-
export interface ContextMenuLabelProps extends HTMLAttributes<HTMLDivElement> {
|
|
246
|
-
children: React.ReactNode;
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
export const ContextMenuLabel = React.forwardRef<
|
|
250
|
-
HTMLDivElement,
|
|
251
|
-
ContextMenuLabelProps
|
|
252
|
-
>(({ children, className, ...props }, ref) => {
|
|
253
|
-
return (
|
|
254
|
-
<div
|
|
255
|
-
ref={ref}
|
|
256
|
-
className={cn(
|
|
257
|
-
'font-semibold text-foreground px-theme-sm py-theme-xs text-theme-sm',
|
|
258
|
-
className
|
|
259
|
-
)}
|
|
260
|
-
{...props}
|
|
261
|
-
>
|
|
262
|
-
{children}
|
|
263
|
-
</div>
|
|
264
|
-
);
|
|
265
|
-
});
|
|
266
|
-
|
|
267
|
-
ContextMenuLabel.displayName = 'ContextMenuLabel';
|
|
268
|
-
|
|
269
|
-
interface SubMenuContextValue {
|
|
270
|
-
isOpen: boolean;
|
|
271
|
-
openSubMenu: () => void;
|
|
272
|
-
closeSubMenu: () => void;
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
const SubMenuContext = createContext<SubMenuContextValue | undefined>(
|
|
276
|
-
undefined
|
|
277
|
-
);
|
|
278
|
-
|
|
279
|
-
const useSubMenu = () => {
|
|
280
|
-
const context = useContext(SubMenuContext);
|
|
281
|
-
if (!context) {
|
|
282
|
-
throw new Error('SubMenu components must be used within a ContextMenuSub');
|
|
283
|
-
}
|
|
284
|
-
return context;
|
|
285
|
-
};
|
|
286
|
-
|
|
287
|
-
export interface ContextMenuSubProps {
|
|
288
|
-
children: React.ReactNode;
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
export const ContextMenuSub: React.FC<ContextMenuSubProps> = ({ children }) => {
|
|
292
|
-
const [isOpen, setIsOpen] = useState(false);
|
|
293
|
-
const closeTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
294
|
-
|
|
295
|
-
const openSubMenu = () => {
|
|
296
|
-
if (closeTimerRef.current) {
|
|
297
|
-
clearTimeout(closeTimerRef.current);
|
|
298
|
-
closeTimerRef.current = null;
|
|
299
|
-
}
|
|
300
|
-
setIsOpen(true);
|
|
301
|
-
};
|
|
302
|
-
|
|
303
|
-
const closeSubMenu = () => {
|
|
304
|
-
closeTimerRef.current = setTimeout(() => {
|
|
305
|
-
setIsOpen(false);
|
|
306
|
-
closeTimerRef.current = null;
|
|
307
|
-
}, 150);
|
|
308
|
-
};
|
|
309
|
-
|
|
310
|
-
useEffect(() => {
|
|
311
|
-
return () => {
|
|
312
|
-
if (closeTimerRef.current) clearTimeout(closeTimerRef.current);
|
|
313
|
-
};
|
|
314
|
-
}, []);
|
|
315
|
-
|
|
316
|
-
return (
|
|
317
|
-
<SubMenuContext.Provider value={{ isOpen, openSubMenu, closeSubMenu }}>
|
|
318
|
-
<div className="relative w-full">{children}</div>
|
|
319
|
-
</SubMenuContext.Provider>
|
|
320
|
-
);
|
|
321
|
-
};
|
|
322
|
-
|
|
323
|
-
export interface ContextMenuSubTriggerProps extends HTMLAttributes<HTMLDivElement> {
|
|
324
|
-
children: React.ReactNode;
|
|
325
|
-
disabled?: boolean;
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
export const ContextMenuSubTrigger = React.forwardRef<
|
|
329
|
-
HTMLDivElement,
|
|
330
|
-
ContextMenuSubTriggerProps
|
|
331
|
-
>(({ children, className, disabled, ...props }, ref) => {
|
|
332
|
-
const { openSubMenu } = useSubMenu();
|
|
333
|
-
|
|
334
|
-
return (
|
|
335
|
-
<div
|
|
336
|
-
ref={ref}
|
|
337
|
-
className={cn(
|
|
338
|
-
'flex cursor-pointer select-none items-center justify-between text-sm outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground',
|
|
339
|
-
'px-theme-sm py-theme-xs rounded-theme-sm',
|
|
340
|
-
disabled && 'pointer-events-none opacity-50',
|
|
341
|
-
className
|
|
342
|
-
)}
|
|
343
|
-
onMouseEnter={openSubMenu}
|
|
344
|
-
onClick={(e) => e.stopPropagation()}
|
|
345
|
-
{...props}
|
|
346
|
-
>
|
|
347
|
-
{children}
|
|
348
|
-
<svg
|
|
349
|
-
width="15"
|
|
350
|
-
height="15"
|
|
351
|
-
viewBox="0 0 15 15"
|
|
352
|
-
fill="none"
|
|
353
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
354
|
-
className="ml-auto"
|
|
355
|
-
>
|
|
356
|
-
<path
|
|
357
|
-
d="M6.1584 3.13508C6.35985 2.94621 6.67627 2.95642 6.86514 3.15788L10.6151 7.15788C10.7954 7.3502 10.7954 7.64949 10.6151 7.84182L6.86514 11.8418C6.67627 12.0433 6.35985 12.0535 6.1584 11.8646C5.95694 11.6757 5.94673 11.3593 6.1356 11.1579L9.565 7.49985L6.1356 3.84182C5.94673 3.64036 5.95694 3.32394 6.1584 3.13508Z"
|
|
358
|
-
fill="currentColor"
|
|
359
|
-
fillRule="evenodd"
|
|
360
|
-
clipRule="evenodd"
|
|
361
|
-
/>
|
|
362
|
-
</svg>
|
|
363
|
-
</div>
|
|
364
|
-
);
|
|
365
|
-
});
|
|
366
|
-
|
|
367
|
-
ContextMenuSubTrigger.displayName = 'ContextMenuSubTrigger';
|
|
368
|
-
|
|
369
|
-
export interface ContextMenuSubContentProps extends HTMLAttributes<HTMLDivElement> {
|
|
370
|
-
children: React.ReactNode;
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
export const ContextMenuSubContent = React.forwardRef<
|
|
374
|
-
HTMLDivElement,
|
|
375
|
-
ContextMenuSubContentProps
|
|
376
|
-
>(({ children, className, ...props }, ref) => {
|
|
377
|
-
const { isOpen, openSubMenu, closeSubMenu } = useSubMenu();
|
|
378
|
-
const contentRef = useRef<HTMLDivElement>(null);
|
|
379
|
-
const [position, setPosition] = useState<{
|
|
380
|
-
left?: string;
|
|
381
|
-
right?: string;
|
|
382
|
-
top?: string;
|
|
383
|
-
}>({});
|
|
384
|
-
|
|
385
|
-
useEffect(() => {
|
|
386
|
-
const handleMouseLeave = (e: MouseEvent) => {
|
|
387
|
-
if (
|
|
388
|
-
contentRef.current &&
|
|
389
|
-
!contentRef.current.contains(e.relatedTarget as Node)
|
|
390
|
-
) {
|
|
391
|
-
const triggerParent = contentRef.current.parentElement;
|
|
392
|
-
if (triggerParent && !triggerParent.contains(e.relatedTarget as Node)) {
|
|
393
|
-
closeSubMenu();
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
};
|
|
397
|
-
|
|
398
|
-
const element = contentRef.current?.parentElement;
|
|
399
|
-
if (element) {
|
|
400
|
-
element.addEventListener('mouseleave', handleMouseLeave);
|
|
401
|
-
return () => element.removeEventListener('mouseleave', handleMouseLeave);
|
|
402
|
-
}
|
|
403
|
-
}, [closeSubMenu]);
|
|
404
|
-
|
|
405
|
-
useEffect(() => {
|
|
406
|
-
if (isOpen && contentRef.current) {
|
|
407
|
-
const submenuRect = contentRef.current.getBoundingClientRect();
|
|
408
|
-
const parentElement = contentRef.current.parentElement;
|
|
409
|
-
|
|
410
|
-
if (parentElement) {
|
|
411
|
-
const parentRect = parentElement.getBoundingClientRect();
|
|
412
|
-
const viewportWidth = window.innerWidth;
|
|
413
|
-
const viewportHeight = window.innerHeight;
|
|
414
|
-
|
|
415
|
-
const spaceOnRight = viewportWidth - parentRect.right;
|
|
416
|
-
const spaceOnLeft = parentRect.left;
|
|
417
|
-
|
|
418
|
-
const newPosition: { left?: string; right?: string; top?: string } = {};
|
|
419
|
-
|
|
420
|
-
if (spaceOnRight >= submenuRect.width + 8) {
|
|
421
|
-
newPosition.left = '100%';
|
|
422
|
-
} else if (spaceOnLeft >= submenuRect.width + 8) {
|
|
423
|
-
newPosition.right = '100%';
|
|
424
|
-
} else {
|
|
425
|
-
newPosition.left = '100%';
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
if (parentRect.top + submenuRect.height > viewportHeight) {
|
|
429
|
-
const adjustedTop = Math.max(
|
|
430
|
-
0,
|
|
431
|
-
viewportHeight - submenuRect.height - 10
|
|
432
|
-
);
|
|
433
|
-
newPosition.top = `${adjustedTop - parentRect.top}px`;
|
|
434
|
-
} else {
|
|
435
|
-
newPosition.top = '0';
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
setPosition(newPosition);
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
|
-
}, [isOpen]);
|
|
442
|
-
|
|
443
|
-
if (!isOpen) return null;
|
|
444
|
-
|
|
445
|
-
return (
|
|
446
|
-
<div
|
|
447
|
-
ref={(node) => {
|
|
448
|
-
if (typeof ref === 'function') ref(node);
|
|
449
|
-
else if (ref) ref.current = node;
|
|
450
|
-
contentRef.current = node;
|
|
451
|
-
}}
|
|
452
|
-
className={cn(
|
|
453
|
-
'absolute min-w-[12rem] overflow-hidden bg-popover text-popover-foreground shadow-lg border border-border animate-in fade-in-0 zoom-in-95 z-50',
|
|
454
|
-
'rounded-theme-md p-theme-xs',
|
|
455
|
-
className
|
|
456
|
-
)}
|
|
457
|
-
style={{
|
|
458
|
-
left: position.left,
|
|
459
|
-
right: position.right,
|
|
460
|
-
top: position.top || '0',
|
|
461
|
-
}}
|
|
462
|
-
onMouseEnter={openSubMenu}
|
|
463
|
-
onClick={(e) => e.stopPropagation()}
|
|
464
|
-
{...props}
|
|
465
|
-
>
|
|
466
|
-
{children}
|
|
467
|
-
</div>
|
|
468
|
-
);
|
|
469
|
-
});
|
|
470
|
-
|
|
471
|
-
ContextMenuSubContent.displayName = 'ContextMenuSubContent';
|
|
1
|
+
import React, {
|
|
2
|
+
HTMLAttributes,
|
|
3
|
+
createContext,
|
|
4
|
+
useContext,
|
|
5
|
+
useEffect,
|
|
6
|
+
useRef,
|
|
7
|
+
useState,
|
|
8
|
+
} from 'react';
|
|
9
|
+
|
|
10
|
+
import { cn } from '@helpers/cn';
|
|
11
|
+
|
|
12
|
+
interface GlobalContextMenuContextValue {
|
|
13
|
+
currentMenuId: string | null;
|
|
14
|
+
setCurrentMenuId: (id: string | null) => void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const GlobalContextMenuContext = createContext<
|
|
18
|
+
GlobalContextMenuContextValue | undefined
|
|
19
|
+
>(undefined);
|
|
20
|
+
|
|
21
|
+
export const GlobalContextMenuProvider: React.FC<{
|
|
22
|
+
children: React.ReactNode;
|
|
23
|
+
}> = ({ children }) => {
|
|
24
|
+
const [currentMenuId, setCurrentMenuId] = useState<string | null>(null);
|
|
25
|
+
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
const handleClick = () => setCurrentMenuId(null);
|
|
28
|
+
document.addEventListener('click', handleClick);
|
|
29
|
+
return () => document.removeEventListener('click', handleClick);
|
|
30
|
+
}, []);
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<GlobalContextMenuContext.Provider
|
|
34
|
+
value={{ currentMenuId, setCurrentMenuId }}
|
|
35
|
+
>
|
|
36
|
+
{children}
|
|
37
|
+
</GlobalContextMenuContext.Provider>
|
|
38
|
+
);
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const useGlobalContextMenu = () => {
|
|
42
|
+
const context = useContext(GlobalContextMenuContext);
|
|
43
|
+
if (!context) {
|
|
44
|
+
throw new Error(
|
|
45
|
+
'ContextMenu must be used within GlobalContextMenuProvider'
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
return context;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
interface ContextMenuContextValue {
|
|
52
|
+
menuId: string;
|
|
53
|
+
isOpen: boolean;
|
|
54
|
+
position: { x: number; y: number };
|
|
55
|
+
openMenu: (x: number, y: number) => void;
|
|
56
|
+
closeMenu: () => void;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const ContextMenuContext = createContext<ContextMenuContextValue | undefined>(
|
|
60
|
+
undefined
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
const useContextMenu = () => {
|
|
64
|
+
const context = useContext(ContextMenuContext);
|
|
65
|
+
if (!context) {
|
|
66
|
+
throw new Error('ContextMenu components must be used within a ContextMenu');
|
|
67
|
+
}
|
|
68
|
+
return context;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
let menuIdCounter = 0;
|
|
72
|
+
|
|
73
|
+
export interface ContextMenuProps {
|
|
74
|
+
children: React.ReactNode;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export const ContextMenu: React.FC<ContextMenuProps> = ({ children }) => {
|
|
78
|
+
const { currentMenuId, setCurrentMenuId } = useGlobalContextMenu();
|
|
79
|
+
const [menuId] = useState(() => `context-menu-${menuIdCounter++}`);
|
|
80
|
+
const [position, setPosition] = useState({ x: 0, y: 0 });
|
|
81
|
+
|
|
82
|
+
const isOpen = currentMenuId === menuId;
|
|
83
|
+
|
|
84
|
+
const openMenu = (x: number, y: number) => {
|
|
85
|
+
setPosition({ x, y });
|
|
86
|
+
setCurrentMenuId(menuId);
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const closeMenu = () => {
|
|
90
|
+
if (currentMenuId === menuId) {
|
|
91
|
+
setCurrentMenuId(null);
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
return (
|
|
96
|
+
<ContextMenuContext.Provider
|
|
97
|
+
value={{ menuId, isOpen, position, openMenu, closeMenu }}
|
|
98
|
+
>
|
|
99
|
+
{children}
|
|
100
|
+
</ContextMenuContext.Provider>
|
|
101
|
+
);
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
export interface ContextMenuTriggerProps extends HTMLAttributes<HTMLDivElement> {
|
|
105
|
+
children: React.ReactNode;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export const ContextMenuTrigger = React.forwardRef<
|
|
109
|
+
HTMLDivElement,
|
|
110
|
+
ContextMenuTriggerProps
|
|
111
|
+
>(({ children, className, ...props }, ref) => {
|
|
112
|
+
const { openMenu } = useContextMenu();
|
|
113
|
+
|
|
114
|
+
const handleContextMenu = (e: React.MouseEvent) => {
|
|
115
|
+
e.preventDefault();
|
|
116
|
+
openMenu(e.clientX, e.clientY);
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
return (
|
|
120
|
+
<div
|
|
121
|
+
ref={ref}
|
|
122
|
+
onContextMenu={handleContextMenu}
|
|
123
|
+
className={cn('cursor-pointer', className)}
|
|
124
|
+
{...props}
|
|
125
|
+
>
|
|
126
|
+
{children}
|
|
127
|
+
</div>
|
|
128
|
+
);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
ContextMenuTrigger.displayName = 'ContextMenuTrigger';
|
|
132
|
+
|
|
133
|
+
export interface ContextMenuContentProps extends HTMLAttributes<HTMLDivElement> {
|
|
134
|
+
children: React.ReactNode;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export const ContextMenuContent = React.forwardRef<
|
|
138
|
+
HTMLDivElement,
|
|
139
|
+
ContextMenuContentProps
|
|
140
|
+
>(({ children, className, ...props }, ref) => {
|
|
141
|
+
const { isOpen, position } = useContextMenu();
|
|
142
|
+
const contentRef = useRef<HTMLDivElement>(null);
|
|
143
|
+
|
|
144
|
+
useEffect(() => {
|
|
145
|
+
if (isOpen && contentRef.current) {
|
|
146
|
+
const rect = contentRef.current.getBoundingClientRect();
|
|
147
|
+
const viewportWidth = window.innerWidth;
|
|
148
|
+
const viewportHeight = window.innerHeight;
|
|
149
|
+
|
|
150
|
+
let adjustedX = position.x;
|
|
151
|
+
let adjustedY = position.y;
|
|
152
|
+
|
|
153
|
+
if (position.x + rect.width > viewportWidth) {
|
|
154
|
+
adjustedX = viewportWidth - rect.width - 10;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (position.y + rect.height > viewportHeight) {
|
|
158
|
+
adjustedY = viewportHeight - rect.height - 10;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
contentRef.current.style.left = `${adjustedX}px`;
|
|
162
|
+
contentRef.current.style.top = `${adjustedY}px`;
|
|
163
|
+
}
|
|
164
|
+
}, [isOpen, position]);
|
|
165
|
+
|
|
166
|
+
if (!isOpen) return null;
|
|
167
|
+
|
|
168
|
+
return (
|
|
169
|
+
<div
|
|
170
|
+
ref={(node) => {
|
|
171
|
+
if (typeof ref === 'function') ref(node);
|
|
172
|
+
else if (ref) ref.current = node;
|
|
173
|
+
contentRef.current = node;
|
|
174
|
+
}}
|
|
175
|
+
className={cn(
|
|
176
|
+
'fixed z-50 min-w-[12rem] bg-popover text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95',
|
|
177
|
+
'rounded-theme-md p-theme-xs',
|
|
178
|
+
className
|
|
179
|
+
)}
|
|
180
|
+
onClick={(e) => e.stopPropagation()}
|
|
181
|
+
{...props}
|
|
182
|
+
>
|
|
183
|
+
{children}
|
|
184
|
+
</div>
|
|
185
|
+
);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
ContextMenuContent.displayName = 'ContextMenuContent';
|
|
189
|
+
|
|
190
|
+
export interface ContextMenuItemProps extends HTMLAttributes<HTMLDivElement> {
|
|
191
|
+
children: React.ReactNode;
|
|
192
|
+
disabled?: boolean;
|
|
193
|
+
onSelect?: () => void;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export const ContextMenuItem = React.forwardRef<
|
|
197
|
+
HTMLDivElement,
|
|
198
|
+
ContextMenuItemProps
|
|
199
|
+
>(({ children, className, disabled, onSelect, ...props }, ref) => {
|
|
200
|
+
const { closeMenu } = useContextMenu();
|
|
201
|
+
|
|
202
|
+
const handleClick = () => {
|
|
203
|
+
if (!disabled && onSelect) {
|
|
204
|
+
onSelect();
|
|
205
|
+
closeMenu();
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
return (
|
|
210
|
+
<div
|
|
211
|
+
ref={ref}
|
|
212
|
+
className={cn(
|
|
213
|
+
'relative flex cursor-pointer select-none items-center text-sm outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground',
|
|
214
|
+
'px-theme-sm py-theme-xs rounded-theme-sm',
|
|
215
|
+
disabled && 'pointer-events-none opacity-50',
|
|
216
|
+
className
|
|
217
|
+
)}
|
|
218
|
+
onClick={handleClick}
|
|
219
|
+
{...props}
|
|
220
|
+
>
|
|
221
|
+
{children}
|
|
222
|
+
</div>
|
|
223
|
+
);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
ContextMenuItem.displayName = 'ContextMenuItem';
|
|
227
|
+
|
|
228
|
+
export interface ContextMenuSeparatorProps extends HTMLAttributes<HTMLDivElement> {}
|
|
229
|
+
|
|
230
|
+
export const ContextMenuSeparator = React.forwardRef<
|
|
231
|
+
HTMLDivElement,
|
|
232
|
+
ContextMenuSeparatorProps
|
|
233
|
+
>(({ className, ...props }, ref) => {
|
|
234
|
+
return (
|
|
235
|
+
<div
|
|
236
|
+
ref={ref}
|
|
237
|
+
className={cn('bg-border h-px my-theme-xs', className)}
|
|
238
|
+
{...props}
|
|
239
|
+
/>
|
|
240
|
+
);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
ContextMenuSeparator.displayName = 'ContextMenuSeparator';
|
|
244
|
+
|
|
245
|
+
export interface ContextMenuLabelProps extends HTMLAttributes<HTMLDivElement> {
|
|
246
|
+
children: React.ReactNode;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export const ContextMenuLabel = React.forwardRef<
|
|
250
|
+
HTMLDivElement,
|
|
251
|
+
ContextMenuLabelProps
|
|
252
|
+
>(({ children, className, ...props }, ref) => {
|
|
253
|
+
return (
|
|
254
|
+
<div
|
|
255
|
+
ref={ref}
|
|
256
|
+
className={cn(
|
|
257
|
+
'font-semibold text-foreground px-theme-sm py-theme-xs text-theme-sm',
|
|
258
|
+
className
|
|
259
|
+
)}
|
|
260
|
+
{...props}
|
|
261
|
+
>
|
|
262
|
+
{children}
|
|
263
|
+
</div>
|
|
264
|
+
);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
ContextMenuLabel.displayName = 'ContextMenuLabel';
|
|
268
|
+
|
|
269
|
+
interface SubMenuContextValue {
|
|
270
|
+
isOpen: boolean;
|
|
271
|
+
openSubMenu: () => void;
|
|
272
|
+
closeSubMenu: () => void;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const SubMenuContext = createContext<SubMenuContextValue | undefined>(
|
|
276
|
+
undefined
|
|
277
|
+
);
|
|
278
|
+
|
|
279
|
+
const useSubMenu = () => {
|
|
280
|
+
const context = useContext(SubMenuContext);
|
|
281
|
+
if (!context) {
|
|
282
|
+
throw new Error('SubMenu components must be used within a ContextMenuSub');
|
|
283
|
+
}
|
|
284
|
+
return context;
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
export interface ContextMenuSubProps {
|
|
288
|
+
children: React.ReactNode;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
export const ContextMenuSub: React.FC<ContextMenuSubProps> = ({ children }) => {
|
|
292
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
293
|
+
const closeTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
294
|
+
|
|
295
|
+
const openSubMenu = () => {
|
|
296
|
+
if (closeTimerRef.current) {
|
|
297
|
+
clearTimeout(closeTimerRef.current);
|
|
298
|
+
closeTimerRef.current = null;
|
|
299
|
+
}
|
|
300
|
+
setIsOpen(true);
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
const closeSubMenu = () => {
|
|
304
|
+
closeTimerRef.current = setTimeout(() => {
|
|
305
|
+
setIsOpen(false);
|
|
306
|
+
closeTimerRef.current = null;
|
|
307
|
+
}, 150);
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
useEffect(() => {
|
|
311
|
+
return () => {
|
|
312
|
+
if (closeTimerRef.current) clearTimeout(closeTimerRef.current);
|
|
313
|
+
};
|
|
314
|
+
}, []);
|
|
315
|
+
|
|
316
|
+
return (
|
|
317
|
+
<SubMenuContext.Provider value={{ isOpen, openSubMenu, closeSubMenu }}>
|
|
318
|
+
<div className="relative w-full">{children}</div>
|
|
319
|
+
</SubMenuContext.Provider>
|
|
320
|
+
);
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
export interface ContextMenuSubTriggerProps extends HTMLAttributes<HTMLDivElement> {
|
|
324
|
+
children: React.ReactNode;
|
|
325
|
+
disabled?: boolean;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
export const ContextMenuSubTrigger = React.forwardRef<
|
|
329
|
+
HTMLDivElement,
|
|
330
|
+
ContextMenuSubTriggerProps
|
|
331
|
+
>(({ children, className, disabled, ...props }, ref) => {
|
|
332
|
+
const { openSubMenu } = useSubMenu();
|
|
333
|
+
|
|
334
|
+
return (
|
|
335
|
+
<div
|
|
336
|
+
ref={ref}
|
|
337
|
+
className={cn(
|
|
338
|
+
'flex cursor-pointer select-none items-center justify-between text-sm outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground',
|
|
339
|
+
'px-theme-sm py-theme-xs rounded-theme-sm',
|
|
340
|
+
disabled && 'pointer-events-none opacity-50',
|
|
341
|
+
className
|
|
342
|
+
)}
|
|
343
|
+
onMouseEnter={openSubMenu}
|
|
344
|
+
onClick={(e) => e.stopPropagation()}
|
|
345
|
+
{...props}
|
|
346
|
+
>
|
|
347
|
+
{children}
|
|
348
|
+
<svg
|
|
349
|
+
width="15"
|
|
350
|
+
height="15"
|
|
351
|
+
viewBox="0 0 15 15"
|
|
352
|
+
fill="none"
|
|
353
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
354
|
+
className="ml-auto"
|
|
355
|
+
>
|
|
356
|
+
<path
|
|
357
|
+
d="M6.1584 3.13508C6.35985 2.94621 6.67627 2.95642 6.86514 3.15788L10.6151 7.15788C10.7954 7.3502 10.7954 7.64949 10.6151 7.84182L6.86514 11.8418C6.67627 12.0433 6.35985 12.0535 6.1584 11.8646C5.95694 11.6757 5.94673 11.3593 6.1356 11.1579L9.565 7.49985L6.1356 3.84182C5.94673 3.64036 5.95694 3.32394 6.1584 3.13508Z"
|
|
358
|
+
fill="currentColor"
|
|
359
|
+
fillRule="evenodd"
|
|
360
|
+
clipRule="evenodd"
|
|
361
|
+
/>
|
|
362
|
+
</svg>
|
|
363
|
+
</div>
|
|
364
|
+
);
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
ContextMenuSubTrigger.displayName = 'ContextMenuSubTrigger';
|
|
368
|
+
|
|
369
|
+
export interface ContextMenuSubContentProps extends HTMLAttributes<HTMLDivElement> {
|
|
370
|
+
children: React.ReactNode;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
export const ContextMenuSubContent = React.forwardRef<
|
|
374
|
+
HTMLDivElement,
|
|
375
|
+
ContextMenuSubContentProps
|
|
376
|
+
>(({ children, className, ...props }, ref) => {
|
|
377
|
+
const { isOpen, openSubMenu, closeSubMenu } = useSubMenu();
|
|
378
|
+
const contentRef = useRef<HTMLDivElement>(null);
|
|
379
|
+
const [position, setPosition] = useState<{
|
|
380
|
+
left?: string;
|
|
381
|
+
right?: string;
|
|
382
|
+
top?: string;
|
|
383
|
+
}>({});
|
|
384
|
+
|
|
385
|
+
useEffect(() => {
|
|
386
|
+
const handleMouseLeave = (e: MouseEvent) => {
|
|
387
|
+
if (
|
|
388
|
+
contentRef.current &&
|
|
389
|
+
!contentRef.current.contains(e.relatedTarget as Node)
|
|
390
|
+
) {
|
|
391
|
+
const triggerParent = contentRef.current.parentElement;
|
|
392
|
+
if (triggerParent && !triggerParent.contains(e.relatedTarget as Node)) {
|
|
393
|
+
closeSubMenu();
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
};
|
|
397
|
+
|
|
398
|
+
const element = contentRef.current?.parentElement;
|
|
399
|
+
if (element) {
|
|
400
|
+
element.addEventListener('mouseleave', handleMouseLeave);
|
|
401
|
+
return () => element.removeEventListener('mouseleave', handleMouseLeave);
|
|
402
|
+
}
|
|
403
|
+
}, [closeSubMenu]);
|
|
404
|
+
|
|
405
|
+
useEffect(() => {
|
|
406
|
+
if (isOpen && contentRef.current) {
|
|
407
|
+
const submenuRect = contentRef.current.getBoundingClientRect();
|
|
408
|
+
const parentElement = contentRef.current.parentElement;
|
|
409
|
+
|
|
410
|
+
if (parentElement) {
|
|
411
|
+
const parentRect = parentElement.getBoundingClientRect();
|
|
412
|
+
const viewportWidth = window.innerWidth;
|
|
413
|
+
const viewportHeight = window.innerHeight;
|
|
414
|
+
|
|
415
|
+
const spaceOnRight = viewportWidth - parentRect.right;
|
|
416
|
+
const spaceOnLeft = parentRect.left;
|
|
417
|
+
|
|
418
|
+
const newPosition: { left?: string; right?: string; top?: string } = {};
|
|
419
|
+
|
|
420
|
+
if (spaceOnRight >= submenuRect.width + 8) {
|
|
421
|
+
newPosition.left = '100%';
|
|
422
|
+
} else if (spaceOnLeft >= submenuRect.width + 8) {
|
|
423
|
+
newPosition.right = '100%';
|
|
424
|
+
} else {
|
|
425
|
+
newPosition.left = '100%';
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
if (parentRect.top + submenuRect.height > viewportHeight) {
|
|
429
|
+
const adjustedTop = Math.max(
|
|
430
|
+
0,
|
|
431
|
+
viewportHeight - submenuRect.height - 10
|
|
432
|
+
);
|
|
433
|
+
newPosition.top = `${adjustedTop - parentRect.top}px`;
|
|
434
|
+
} else {
|
|
435
|
+
newPosition.top = '0';
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
setPosition(newPosition);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
}, [isOpen]);
|
|
442
|
+
|
|
443
|
+
if (!isOpen) return null;
|
|
444
|
+
|
|
445
|
+
return (
|
|
446
|
+
<div
|
|
447
|
+
ref={(node) => {
|
|
448
|
+
if (typeof ref === 'function') ref(node);
|
|
449
|
+
else if (ref) ref.current = node;
|
|
450
|
+
contentRef.current = node;
|
|
451
|
+
}}
|
|
452
|
+
className={cn(
|
|
453
|
+
'absolute min-w-[12rem] overflow-hidden bg-popover text-popover-foreground shadow-lg border border-border animate-in fade-in-0 zoom-in-95 z-50',
|
|
454
|
+
'rounded-theme-md p-theme-xs',
|
|
455
|
+
className
|
|
456
|
+
)}
|
|
457
|
+
style={{
|
|
458
|
+
left: position.left,
|
|
459
|
+
right: position.right,
|
|
460
|
+
top: position.top || '0',
|
|
461
|
+
}}
|
|
462
|
+
onMouseEnter={openSubMenu}
|
|
463
|
+
onClick={(e) => e.stopPropagation()}
|
|
464
|
+
{...props}
|
|
465
|
+
>
|
|
466
|
+
{children}
|
|
467
|
+
</div>
|
|
468
|
+
);
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
ContextMenuSubContent.displayName = 'ContextMenuSubContent';
|