@anymux/ui-kit 0.1.0
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/dist/ExplorerLayout-CSIJd7N4.js +105 -0
- package/dist/ExplorerLayout-CSIJd7N4.js.map +1 -0
- package/dist/FileBrowserContext-B6jixa2j.js +11 -0
- package/dist/FileBrowserContext-B6jixa2j.js.map +1 -0
- package/dist/calendar-DSlrbHoj.js +761 -0
- package/dist/calendar-DSlrbHoj.js.map +1 -0
- package/dist/calendar.d.ts +3 -0
- package/dist/calendar.js +3 -0
- package/dist/contacts-DQXTZzHc.js +539 -0
- package/dist/contacts-DQXTZzHc.js.map +1 -0
- package/dist/contacts.d.ts +3 -0
- package/dist/contacts.js +3 -0
- package/dist/file-browser-m5atC3kF.js +6755 -0
- package/dist/file-browser-m5atC3kF.js.map +1 -0
- package/dist/file-browser.d.ts +11 -0
- package/dist/file-browser.js +9 -0
- package/dist/git-B55e6LL-.js +561 -0
- package/dist/git-B55e6LL-.js.map +1 -0
- package/dist/git.d.ts +2 -0
- package/dist/git.js +3 -0
- package/dist/iconMap-V4B8P-Uh.js +206 -0
- package/dist/iconMap-V4B8P-Uh.js.map +1 -0
- package/dist/icons-CIsIOZXR.js +0 -0
- package/dist/icons.d.ts +2 -0
- package/dist/icons.js +4 -0
- package/dist/index-BNmNIWBL.d.ts +71 -0
- package/dist/index-BNmNIWBL.d.ts.map +1 -0
- package/dist/index-Bryv_GCG.d.ts +1481 -0
- package/dist/index-Bryv_GCG.d.ts.map +1 -0
- package/dist/index-CuQIjSXs.d.ts +134 -0
- package/dist/index-CuQIjSXs.d.ts.map +1 -0
- package/dist/index-DSu19mq0.d.ts +153 -0
- package/dist/index-DSu19mq0.d.ts.map +1 -0
- package/dist/index-DmsyeHFr.d.ts +149 -0
- package/dist/index-DmsyeHFr.d.ts.map +1 -0
- package/dist/index-DxnJ8FYM.d.ts +17 -0
- package/dist/index-DxnJ8FYM.d.ts.map +1 -0
- package/dist/index-DzfY1Tok.d.ts +32 -0
- package/dist/index-DzfY1Tok.d.ts.map +1 -0
- package/dist/index-Ml_SgiKa.d.ts +1847 -0
- package/dist/index-Ml_SgiKa.d.ts.map +1 -0
- package/dist/index-kHr9udZD.d.ts +1025 -0
- package/dist/index-kHr9udZD.d.ts.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +15 -0
- package/dist/layout-Ca_4r8ka.js +89 -0
- package/dist/layout-Ca_4r8ka.js.map +1 -0
- package/dist/layout.d.ts +2 -0
- package/dist/layout.js +5 -0
- package/dist/list-CxfT6hix.js +6831 -0
- package/dist/list-CxfT6hix.js.map +1 -0
- package/dist/list.d.ts +2 -0
- package/dist/list.js +5 -0
- package/dist/media-DZ292aKK.js +557 -0
- package/dist/media-DZ292aKK.js.map +1 -0
- package/dist/media.d.ts +3 -0
- package/dist/media.js +3 -0
- package/dist/tree-Dd9Z0Aso.js +3351 -0
- package/dist/tree-Dd9Z0Aso.js.map +1 -0
- package/dist/tree.d.ts +2 -0
- package/dist/tree.js +6 -0
- package/dist/types-common-CB3kRek8.d.ts +26 -0
- package/dist/types-common-CB3kRek8.d.ts.map +1 -0
- package/dist/utils-B4fdKKsy.js +3 -0
- package/package.json +109 -0
- package/src/calendar/AgendaView.tsx +37 -0
- package/src/calendar/CalendarBrowser.tsx +90 -0
- package/src/calendar/CalendarModel.ts +142 -0
- package/src/calendar/CalendarSidebar.tsx +81 -0
- package/src/calendar/DayView.tsx +76 -0
- package/src/calendar/EventCard.tsx +51 -0
- package/src/calendar/MockCalendarProvider.ts +98 -0
- package/src/calendar/MonthView.tsx +77 -0
- package/src/calendar/WeekView.tsx +129 -0
- package/src/calendar/index.ts +18 -0
- package/src/calendar/types.ts +25 -0
- package/src/contacts/ContactAvatar.tsx +35 -0
- package/src/contacts/ContactBrowser.tsx +56 -0
- package/src/contacts/ContactCard.tsx +37 -0
- package/src/contacts/ContactDetail.tsx +63 -0
- package/src/contacts/ContactGroupSidebar.tsx +40 -0
- package/src/contacts/ContactList.tsx +32 -0
- package/src/contacts/ContactListModel.ts +120 -0
- package/src/contacts/MockContactProvider.ts +77 -0
- package/src/contacts/index.ts +17 -0
- package/src/contacts/types.ts +26 -0
- package/src/demos/CalendarBrowserDemo.tsx +15 -0
- package/src/demos/ContactBrowserDemo.tsx +15 -0
- package/src/demos/MediaBrowserDemo.tsx +15 -0
- package/src/file-browser/adapters/DocumentViewerAdapter.ts +371 -0
- package/src/file-browser/adapters/FileSystemBridge.ts +168 -0
- package/src/file-browser/adapters/GitBrowserAdapter.ts +546 -0
- package/src/file-browser/adapters/README.md +504 -0
- package/src/file-browser/adapters/index.ts +27 -0
- package/src/file-browser/adapters/types.ts +70 -0
- package/src/file-browser/architecture.md +645 -0
- package/src/file-browser/components/CreateItemDialog.tsx +71 -0
- package/src/file-browser/components/DeleteConfirmDialog.tsx +58 -0
- package/src/file-browser/components/FileBrowser.tsx +473 -0
- package/src/file-browser/components/FileBrowserContent.tsx +209 -0
- package/src/file-browser/components/FileBrowserHeader.tsx +151 -0
- package/src/file-browser/components/FileBrowserToolbar.tsx +145 -0
- package/src/file-browser/components/LeftPanel/LeftPanel.tsx +103 -0
- package/src/file-browser/components/LeftPanel/LeftPanelTabs.tsx +70 -0
- package/src/file-browser/components/LeftPanel/TreeNavigationView.tsx +256 -0
- package/src/file-browser/components/PreviewPane.tsx +146 -0
- package/src/file-browser/components/RightPanel/FilePreview.tsx +219 -0
- package/src/file-browser/components/RightPanel/RightPanel.tsx +186 -0
- package/src/file-browser/components/RightPanel/RightPanelToolbar.tsx +113 -0
- package/src/file-browser/components/UploadProgress.tsx +123 -0
- package/src/file-browser/components/ViewerHost.tsx +208 -0
- package/src/file-browser/components/mobile/MobileNavigation.tsx +227 -0
- package/src/file-browser/components/navigation/NavigationButtons.tsx +171 -0
- package/src/file-browser/components/shared/ErrorBoundary.tsx +116 -0
- package/src/file-browser/components/shared/FileBrowserItem.tsx +195 -0
- package/src/file-browser/components/shared/FileIcon.tsx +169 -0
- package/src/file-browser/components/toolbar/ViewModeToggle.tsx +200 -0
- package/src/file-browser/components/views/ListView/ListView.tsx +484 -0
- package/src/file-browser/components/views/ThumbnailView/ThumbnailView.tsx +323 -0
- package/src/file-browser/components/views/TreeView/TreeNode.tsx +186 -0
- package/src/file-browser/components/views/TreeView/TreeNodeList.tsx +191 -0
- package/src/file-browser/components/views/TreeView/TreeView.tsx +200 -0
- package/src/file-browser/components/views/TreemapView/TreemapView.tsx +339 -0
- package/src/file-browser/context/FileBrowserContext.tsx +13 -0
- package/src/file-browser/examples/BasicUsage.tsx +20 -0
- package/src/file-browser/index.ts +98 -0
- package/src/file-browser/models/FileBrowserModel.ts +623 -0
- package/src/file-browser/models/LeftPanelManagerModel.ts +105 -0
- package/src/file-browser/models/NavigationManagerModel.ts +312 -0
- package/src/file-browser/models/ResponsiveLayoutManagerModel.ts +437 -0
- package/src/file-browser/models/RightPanelManagerModel.ts +190 -0
- package/src/file-browser/models/SelectionManagerModel.ts +252 -0
- package/src/file-browser/models/ToolbarManagerModel.ts +144 -0
- package/src/file-browser/models/UploadModel.ts +147 -0
- package/src/file-browser/models/ViewModeManagerModel.ts +185 -0
- package/src/file-browser/models/ViewerHostModel.ts +44 -0
- package/src/file-browser/models/ui/ListViewUIModel.ts +265 -0
- package/src/file-browser/models/ui/PreviewUIModel.ts +297 -0
- package/src/file-browser/models/ui/ThumbnailViewUIModel.ts +254 -0
- package/src/file-browser/models/ui/TreeViewUIModel.ts +128 -0
- package/src/file-browser/models/ui/TreemapViewUIModel.ts +350 -0
- package/src/file-browser/providers/FileSystemListProvider.ts +552 -0
- package/src/file-browser/providers/FileSystemProvider.ts +401 -0
- package/src/file-browser/providers/FileSystemTreeProvider.ts +231 -0
- package/src/file-browser/providers/GitProvider.ts +337 -0
- package/src/file-browser/providers/GitRepositoryProvider.ts +376 -0
- package/src/file-browser/providers/IFileBrowserProvider.ts +56 -0
- package/src/file-browser/providers/MemoryProvider.ts +303 -0
- package/src/file-browser/providers/index.ts +4 -0
- package/src/file-browser/registry/ViewerRegistry.ts +551 -0
- package/src/file-browser/registry/types.ts +144 -0
- package/src/file-browser/scripts/performanceBenchmark.ts +553 -0
- package/src/file-browser/services/ThumbnailCacheService.ts +128 -0
- package/src/file-browser/tasks.md +537 -0
- package/src/file-browser/types/FileBrowserTypes.ts +126 -0
- package/src/file-browser/types/ProviderTypes.ts +155 -0
- package/src/file-browser/types/UITypes.ts +235 -0
- package/src/file-browser/types/ViewModeTypes.ts +150 -0
- package/src/file-browser/utils/gestures.ts +327 -0
- package/src/file-browser/utils/performance.ts +563 -0
- package/src/file-browser/viewers/ImageViewer.tsx +163 -0
- package/src/file-browser/viewers/ImageViewerModel.ts +79 -0
- package/src/file-browser/viewers/TextViewer.tsx +95 -0
- package/src/file-browser/viewers/UnsupportedFileViewer.tsx +57 -0
- package/src/file-browser/viewers/index.ts +61 -0
- package/src/git/BranchList.tsx +128 -0
- package/src/git/CommitGraph.tsx +239 -0
- package/src/git/CommitList.tsx +258 -0
- package/src/git/DiffViewer.tsx +219 -0
- package/src/git/index.ts +4 -0
- package/src/icons/iconMap.ts +146 -0
- package/src/icons/index.ts +9 -0
- package/src/index.ts +13 -0
- package/src/layout/README.md +307 -0
- package/src/layout/components/ExplorerLayout/ExplorerLayout.tsx +178 -0
- package/src/layout/examples/SimpleExample.tsx +60 -0
- package/src/layout/index.ts +6 -0
- package/src/lib/utils.ts +1 -0
- package/src/list/README.md +303 -0
- package/src/list/architecture.md +807 -0
- package/src/list/components/CalculatedGridView.tsx +252 -0
- package/src/list/components/DragPreview.tsx +102 -0
- package/src/list/components/ListContextMenu.tsx +274 -0
- package/src/list/components/ListItem.tsx +761 -0
- package/src/list/components/ListItems.tsx +919 -0
- package/src/list/components/MasonryView.tsx +241 -0
- package/src/list/components/SearchFilter.tsx +44 -0
- package/src/list/components/TreemapView.tsx +709 -0
- package/src/list/components/ViewSizeControls.tsx +205 -0
- package/src/list/components/ViewTypeSelector.tsx +312 -0
- package/src/list/components/VirtualizedDetailsView.tsx +231 -0
- package/src/list/components/VirtualizedGrid.tsx +164 -0
- package/src/list/components/VirtualizedList.tsx +154 -0
- package/src/list/components/VirtualizedMasonryView.tsx +344 -0
- package/src/list/components/shared/EmptyState.tsx +103 -0
- package/src/list/components/shared/ErrorBoundary.tsx +123 -0
- package/src/list/components/shared/ErrorDisplay.tsx +100 -0
- package/src/list/components/shared/ListLoader.tsx +146 -0
- package/src/list/components/shared/LoadingIndicator.tsx +80 -0
- package/src/list/index.ts +92 -0
- package/src/list/models/ListItemsModel.ts +1301 -0
- package/src/list/models/TreemapModel.ts +204 -0
- package/src/list/providers/ListItemsProvider.ts +313 -0
- package/src/list/providers/TestListProvider.ts +604 -0
- package/src/list/tasks.md +937 -0
- package/src/list/types/ListTypes.ts +178 -0
- package/src/list/utils/BenchmarkLogger.ts +243 -0
- package/src/list/utils/DragDropManager.ts +320 -0
- package/src/list/utils/GridLayoutCalculator.ts +290 -0
- package/src/list/utils/ListAccessibility.ts +367 -0
- package/src/list/utils/ListKeyboard.ts +414 -0
- package/src/list/utils/MasonryLayoutCalculator.ts +302 -0
- package/src/list/utils/MasonryLayoutEngine.ts +401 -0
- package/src/list/utils/__tests__/MasonryLayoutEngine.test.ts +157 -0
- package/src/list/utils/__tests__/VirtualizedMasonryView.test.tsx +251 -0
- package/src/media/AlbumSidebar.tsx +48 -0
- package/src/media/MediaBrowser.tsx +92 -0
- package/src/media/MediaBrowserModel.ts +138 -0
- package/src/media/MediaGrid.tsx +50 -0
- package/src/media/MediaList.tsx +49 -0
- package/src/media/MediaPreview.tsx +63 -0
- package/src/media/MediaTimeline.tsx +38 -0
- package/src/media/MockMediaProvider.ts +70 -0
- package/src/media/index.ts +18 -0
- package/src/media/types.ts +21 -0
- package/src/styles/variables.css +60 -0
- package/src/tree/DEVELOPMENT_SUMMARY.md +170 -0
- package/src/tree/__tests__/TreeModel.test.ts +16 -0
- package/src/tree/architecture.md +530 -0
- package/src/tree/components/Tree.tsx +283 -0
- package/src/tree/components/TreeCheckbox.tsx +147 -0
- package/src/tree/components/TreeContextMenu.tsx +139 -0
- package/src/tree/components/TreeNodeList.tsx +329 -0
- package/src/tree/components/TreeTable.tsx +382 -0
- package/src/tree/index.ts +58 -0
- package/src/tree/models/TreeModel.ts +839 -0
- package/src/tree/providers/SimpleTreeProvider.ts +463 -0
- package/src/tree/providers/TestTreeProvider.ts +946 -0
- package/src/tree/providers/TreeProvider.ts +308 -0
- package/src/tree/tasks.md +2046 -0
- package/src/tree/types/TreeTypes.ts +279 -0
- package/src/tree/utils/SelectionTheme.ts +150 -0
- package/src/tree/utils/logger.ts +203 -0
- package/src/types-common.ts +21 -0
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import React, { useEffect, useRef, useState, useCallback } from 'react';
|
|
2
|
+
import { observer } from 'mobx-react-lite';
|
|
3
|
+
import { ListItemsModel } from '../models/ListItemsModel';
|
|
4
|
+
import { ListItem } from './ListItem';
|
|
5
|
+
import type { GridItemLayout } from '../utils/GridLayoutCalculator';
|
|
6
|
+
import type { ListItemData } from '../types/ListTypes';
|
|
7
|
+
|
|
8
|
+
// AICODE-NOTE: Grid view component using precise calculator-based positioning
|
|
9
|
+
export interface CalculatedGridViewProps {
|
|
10
|
+
model: ListItemsModel;
|
|
11
|
+
className?: string;
|
|
12
|
+
height?: number;
|
|
13
|
+
width?: number;
|
|
14
|
+
enableVirtualization?: boolean;
|
|
15
|
+
onItemClick?: (item: ListItemData, event: React.MouseEvent) => void;
|
|
16
|
+
onItemDoubleClick?: (item: ListItemData, event: React.MouseEvent) => void;
|
|
17
|
+
onItemContextMenu?: (item: ListItemData, event: React.MouseEvent) => void;
|
|
18
|
+
onItemDragStart?: (item: ListItemData, event: React.DragEvent) => void;
|
|
19
|
+
onItemDragOver?: (item: ListItemData, event: React.DragEvent) => void;
|
|
20
|
+
onItemDragLeave?: (item: ListItemData, event: React.DragEvent) => void;
|
|
21
|
+
onItemDrop?: (item: ListItemData, event: React.DragEvent) => void;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// AICODE-NOTE: Extract grid item into separate observer component for MobX reactivity
|
|
25
|
+
interface GridItemWrapperProps {
|
|
26
|
+
item: ListItemData;
|
|
27
|
+
layout: GridItemLayout;
|
|
28
|
+
index: number;
|
|
29
|
+
model: ListItemsModel;
|
|
30
|
+
onItemClick?: (item: ListItemData, event: React.MouseEvent) => void;
|
|
31
|
+
onItemDoubleClick?: (item: ListItemData, event: React.MouseEvent) => void;
|
|
32
|
+
onItemContextMenu?: (item: ListItemData, event: React.MouseEvent) => void;
|
|
33
|
+
onItemDragStart?: (item: ListItemData, event: React.DragEvent) => void;
|
|
34
|
+
onItemDragOver?: (item: ListItemData, event: React.DragEvent) => void;
|
|
35
|
+
onItemDragLeave?: (item: ListItemData, event: React.DragEvent) => void;
|
|
36
|
+
onItemDrop?: (item: ListItemData, event: React.DragEvent) => void;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const GridItemWrapper = observer<GridItemWrapperProps>(({
|
|
40
|
+
item,
|
|
41
|
+
layout,
|
|
42
|
+
index,
|
|
43
|
+
model,
|
|
44
|
+
onItemClick,
|
|
45
|
+
onItemDoubleClick,
|
|
46
|
+
onItemContextMenu,
|
|
47
|
+
onItemDragStart,
|
|
48
|
+
onItemDragOver,
|
|
49
|
+
onItemDragLeave,
|
|
50
|
+
onItemDrop
|
|
51
|
+
}) => (
|
|
52
|
+
<div
|
|
53
|
+
data-item-id={item.id}
|
|
54
|
+
style={{
|
|
55
|
+
position: 'absolute',
|
|
56
|
+
left: layout.x,
|
|
57
|
+
top: layout.y,
|
|
58
|
+
width: layout.width,
|
|
59
|
+
height: layout.height
|
|
60
|
+
}}
|
|
61
|
+
>
|
|
62
|
+
<ListItem
|
|
63
|
+
item={item}
|
|
64
|
+
index={index}
|
|
65
|
+
totalItems={model.totalItemCount}
|
|
66
|
+
viewType={model.currentViewType}
|
|
67
|
+
provider={model.provider}
|
|
68
|
+
model={model}
|
|
69
|
+
itemWidth={layout.width}
|
|
70
|
+
itemHeight={layout.height}
|
|
71
|
+
thumbnailSize={layout.thumbnailSize}
|
|
72
|
+
isSelected={model.isItemSelected(item.id)}
|
|
73
|
+
isFocused={model.focusedItem === item.id}
|
|
74
|
+
isDraggedOver={model.dragOverItem === item.id}
|
|
75
|
+
dragOverPosition={model.dragOverItem === item.id ? model.dragOverPosition : null}
|
|
76
|
+
canDrag={model.canDragItem(item)}
|
|
77
|
+
onClick={onItemClick}
|
|
78
|
+
onDoubleClick={onItemDoubleClick}
|
|
79
|
+
onContextMenu={onItemContextMenu}
|
|
80
|
+
onDragStart={onItemDragStart}
|
|
81
|
+
onDragOver={onItemDragOver}
|
|
82
|
+
onDragLeave={onItemDragLeave}
|
|
83
|
+
onDrop={onItemDrop}
|
|
84
|
+
/>
|
|
85
|
+
</div>
|
|
86
|
+
));
|
|
87
|
+
|
|
88
|
+
GridItemWrapper.displayName = 'CalculatedGridItemWrapper';
|
|
89
|
+
|
|
90
|
+
const CalculatedGridViewComponent: React.FC<CalculatedGridViewProps> = ({
|
|
91
|
+
model,
|
|
92
|
+
className = '',
|
|
93
|
+
height,
|
|
94
|
+
width,
|
|
95
|
+
enableVirtualization = true,
|
|
96
|
+
onItemClick,
|
|
97
|
+
onItemDoubleClick,
|
|
98
|
+
onItemContextMenu,
|
|
99
|
+
onItemDragStart,
|
|
100
|
+
onItemDragOver,
|
|
101
|
+
onItemDragLeave,
|
|
102
|
+
onItemDrop
|
|
103
|
+
}) => {
|
|
104
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
105
|
+
const [scrollTop, setScrollTop] = useState(0);
|
|
106
|
+
// AICODE-NOTE: Initialize with provided dimensions so first render doesn't flash at 0x0
|
|
107
|
+
const [containerSize, setContainerSize] = useState({ width: width || 0, height: height || 0 });
|
|
108
|
+
const [hasMeasured, setHasMeasured] = useState(!!(width && height));
|
|
109
|
+
|
|
110
|
+
// AICODE-NOTE: Update container size when dimensions change
|
|
111
|
+
useEffect(() => {
|
|
112
|
+
const updateSize = () => {
|
|
113
|
+
if (containerRef.current) {
|
|
114
|
+
const rect = containerRef.current.getBoundingClientRect();
|
|
115
|
+
const newSize = {
|
|
116
|
+
width: width || rect.width,
|
|
117
|
+
height: height || rect.height
|
|
118
|
+
};
|
|
119
|
+
setContainerSize(newSize);
|
|
120
|
+
setHasMeasured(true);
|
|
121
|
+
model.updateContainerSize(newSize.width, newSize.height);
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
updateSize();
|
|
126
|
+
|
|
127
|
+
// Set up resize observer
|
|
128
|
+
const resizeObserver = new ResizeObserver(updateSize);
|
|
129
|
+
if (containerRef.current) {
|
|
130
|
+
resizeObserver.observe(containerRef.current);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return () => {
|
|
134
|
+
resizeObserver.disconnect();
|
|
135
|
+
};
|
|
136
|
+
}, [width, height, model]);
|
|
137
|
+
|
|
138
|
+
// AICODE-NOTE: Handle scroll for virtualization
|
|
139
|
+
const handleScroll = useCallback(() => {
|
|
140
|
+
if (containerRef.current) {
|
|
141
|
+
setScrollTop(containerRef.current.scrollTop);
|
|
142
|
+
}
|
|
143
|
+
}, []);
|
|
144
|
+
|
|
145
|
+
useEffect(() => {
|
|
146
|
+
const container = containerRef.current;
|
|
147
|
+
if (!container || !enableVirtualization) return;
|
|
148
|
+
|
|
149
|
+
let ticking = false;
|
|
150
|
+
const throttledScroll = () => {
|
|
151
|
+
if (!ticking) {
|
|
152
|
+
requestAnimationFrame(() => {
|
|
153
|
+
handleScroll();
|
|
154
|
+
ticking = false;
|
|
155
|
+
});
|
|
156
|
+
ticking = true;
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
container.addEventListener('scroll', throttledScroll);
|
|
161
|
+
return () => {
|
|
162
|
+
container.removeEventListener('scroll', throttledScroll);
|
|
163
|
+
};
|
|
164
|
+
}, [handleScroll, enableVirtualization]);
|
|
165
|
+
|
|
166
|
+
// Scroll to item when model requests it
|
|
167
|
+
useEffect(() => {
|
|
168
|
+
const targetId = model.scrollToItemId;
|
|
169
|
+
if (!targetId || !containerRef.current) return;
|
|
170
|
+
const el = containerRef.current.querySelector(`[data-item-id="${CSS.escape(targetId)}"]`);
|
|
171
|
+
if (el) {
|
|
172
|
+
el.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
|
|
173
|
+
}
|
|
174
|
+
model.clearScrollToItem();
|
|
175
|
+
}, [model.scrollToItemId, model]);
|
|
176
|
+
|
|
177
|
+
// AICODE-NOTE: Don't render items until container has been measured to prevent 0→final size flash
|
|
178
|
+
const gridLayout = model.getGridLayout();
|
|
179
|
+
if (!gridLayout || !hasMeasured || containerSize.width === 0) {
|
|
180
|
+
return (
|
|
181
|
+
<div
|
|
182
|
+
ref={containerRef}
|
|
183
|
+
className={`relative overflow-auto ${className}`}
|
|
184
|
+
style={{ height, width, overflowX: 'hidden' }}
|
|
185
|
+
/>
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const { layout: itemLayouts, totalHeight } = gridLayout;
|
|
190
|
+
|
|
191
|
+
// AICODE-NOTE: Determine which items to render
|
|
192
|
+
let visibleItems: Array<{ item: ListItemData; layout: GridItemLayout; index: number }> = [];
|
|
193
|
+
|
|
194
|
+
if (enableVirtualization && containerSize.height > 0) {
|
|
195
|
+
// Use virtualization - only render visible items
|
|
196
|
+
const visibleLayouts = model.getVisibleGridItems(scrollTop, containerSize.height);
|
|
197
|
+
visibleItems = visibleLayouts.map((layout, layoutIndex) => {
|
|
198
|
+
// Find the actual item index from the layout position
|
|
199
|
+
const itemIndex = itemLayouts.findIndex(l => l.x === layout.x && l.y === layout.y);
|
|
200
|
+
const item = model.items[itemIndex];
|
|
201
|
+
return item ? { item, layout, index: itemIndex } : null;
|
|
202
|
+
}).filter(Boolean) as Array<{ item: ListItemData; layout: GridItemLayout; index: number }>;
|
|
203
|
+
} else {
|
|
204
|
+
// Render all items
|
|
205
|
+
visibleItems = model.items.map((item, index) => {
|
|
206
|
+
const layout = itemLayouts[index];
|
|
207
|
+
return layout ? { item, layout, index } : null;
|
|
208
|
+
}).filter(Boolean) as Array<{ item: ListItemData; layout: GridItemLayout; index: number }>;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// console.log('🎨 [CALCULATED GRID] Rendering', visibleItems.length, 'items out of', model.items.length, 'total');
|
|
212
|
+
// console.log('📐 [GRID LAYOUT] Container:', containerSize, 'Total height:', totalHeight);
|
|
213
|
+
|
|
214
|
+
return (
|
|
215
|
+
<div
|
|
216
|
+
ref={containerRef}
|
|
217
|
+
className={`relative overflow-auto ${className}`}
|
|
218
|
+
style={{ height, width, overflowX: 'hidden' }}
|
|
219
|
+
>
|
|
220
|
+
{/* AICODE-NOTE: Container with calculated total height and no horizontal overflow */}
|
|
221
|
+
<div
|
|
222
|
+
style={{
|
|
223
|
+
position: 'relative',
|
|
224
|
+
height: totalHeight,
|
|
225
|
+
width: '100%',
|
|
226
|
+
maxWidth: containerSize.width || '100%',
|
|
227
|
+
overflow: 'hidden'
|
|
228
|
+
}}
|
|
229
|
+
>
|
|
230
|
+
{visibleItems.map(({ item, layout, index }) => (
|
|
231
|
+
<GridItemWrapper
|
|
232
|
+
key={item.id}
|
|
233
|
+
item={item}
|
|
234
|
+
layout={layout}
|
|
235
|
+
index={index}
|
|
236
|
+
model={model}
|
|
237
|
+
onItemClick={onItemClick}
|
|
238
|
+
onItemDoubleClick={onItemDoubleClick}
|
|
239
|
+
onItemContextMenu={onItemContextMenu}
|
|
240
|
+
onItemDragStart={onItemDragStart}
|
|
241
|
+
onItemDragOver={onItemDragOver}
|
|
242
|
+
onItemDragLeave={onItemDragLeave}
|
|
243
|
+
onItemDrop={onItemDrop}
|
|
244
|
+
/>
|
|
245
|
+
))}
|
|
246
|
+
</div>
|
|
247
|
+
</div>
|
|
248
|
+
);
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
// AICODE-NOTE: Export memoized component for performance
|
|
252
|
+
export const CalculatedGridView = observer(CalculatedGridViewComponent);
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { observer } from 'mobx-react-lite';
|
|
3
|
+
import { ListItemData } from '../types/ListTypes';
|
|
4
|
+
import { FileText, Image, Video, Music, Folder, Archive, File } from 'lucide-react';
|
|
5
|
+
|
|
6
|
+
// AICODE-NOTE: Enhanced drag preview component for better visual feedback during drag operations
|
|
7
|
+
|
|
8
|
+
export interface DragPreviewProps {
|
|
9
|
+
items: ListItemData[];
|
|
10
|
+
className?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const getItemIcon = (item: ListItemData) => {
|
|
14
|
+
const type = item.type?.toLowerCase() || '';
|
|
15
|
+
|
|
16
|
+
if (type.includes('folder') || type.includes('directory')) return Folder;
|
|
17
|
+
if (type.includes('image') || type.includes('photo')) return Image;
|
|
18
|
+
if (type.includes('video') || type.includes('movie')) return Video;
|
|
19
|
+
if (type.includes('audio') || type.includes('music')) return Music;
|
|
20
|
+
if (type.includes('text') || type.includes('document')) return FileText;
|
|
21
|
+
if (type.includes('archive') || type.includes('zip')) return Archive;
|
|
22
|
+
|
|
23
|
+
return File;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export const DragPreview = observer<DragPreviewProps>(({ items, className = '' }) => {
|
|
27
|
+
if (items.length === 0) return null;
|
|
28
|
+
|
|
29
|
+
const firstItem = items[0]!; // Safe because we checked length above
|
|
30
|
+
const Icon = getItemIcon(firstItem);
|
|
31
|
+
const hasMultiple = items.length > 1;
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<div className={`
|
|
35
|
+
bg-background border border-border rounded-lg shadow-lg p-3 min-w-48 max-w-64
|
|
36
|
+
${className}
|
|
37
|
+
`}>
|
|
38
|
+
{/* Single item preview */}
|
|
39
|
+
{!hasMultiple && (
|
|
40
|
+
<div className="flex items-center gap-3">
|
|
41
|
+
<div className="flex-shrink-0">
|
|
42
|
+
<Icon className="w-5 h-5 text-muted-foreground" />
|
|
43
|
+
</div>
|
|
44
|
+
<div className="flex-1 min-w-0">
|
|
45
|
+
<div className="text-sm font-medium truncate">
|
|
46
|
+
{firstItem.name}
|
|
47
|
+
</div>
|
|
48
|
+
{firstItem.type && (
|
|
49
|
+
<div className="text-xs text-muted-foreground truncate">
|
|
50
|
+
{firstItem.type}
|
|
51
|
+
</div>
|
|
52
|
+
)}
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
)}
|
|
56
|
+
|
|
57
|
+
{/* Multiple items preview */}
|
|
58
|
+
{hasMultiple && (
|
|
59
|
+
<div className="space-y-2">
|
|
60
|
+
<div className="flex items-center gap-3">
|
|
61
|
+
<div className="flex-shrink-0 relative">
|
|
62
|
+
<Icon className="w-5 h-5 text-muted-foreground" />
|
|
63
|
+
<div className="absolute -top-1 -right-1 bg-primary text-primary-foreground text-xs rounded-full w-4 h-4 flex items-center justify-center font-medium">
|
|
64
|
+
{items.length}
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
<div className="flex-1 min-w-0">
|
|
68
|
+
<div className="text-sm font-medium truncate">
|
|
69
|
+
{firstItem.name}
|
|
70
|
+
</div>
|
|
71
|
+
<div className="text-xs text-muted-foreground">
|
|
72
|
+
+{items.length - 1} more items
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
|
|
77
|
+
{/* Show up to 3 additional items */}
|
|
78
|
+
{items.slice(1, 4).map((item, index) => {
|
|
79
|
+
const ItemIcon = getItemIcon(item);
|
|
80
|
+
return (
|
|
81
|
+
<div key={item.id} className="flex items-center gap-3 pl-2 opacity-75">
|
|
82
|
+
<ItemIcon className="w-4 h-4 text-muted-foreground flex-shrink-0" />
|
|
83
|
+
<div className="text-xs text-muted-foreground truncate">
|
|
84
|
+
{item.name}
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
);
|
|
88
|
+
})}
|
|
89
|
+
|
|
90
|
+
{/* Show count if more than 4 items */}
|
|
91
|
+
{items.length > 4 && (
|
|
92
|
+
<div className="text-xs text-muted-foreground pl-7">
|
|
93
|
+
...and {items.length - 4} more
|
|
94
|
+
</div>
|
|
95
|
+
)}
|
|
96
|
+
</div>
|
|
97
|
+
)}
|
|
98
|
+
</div>
|
|
99
|
+
);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
DragPreview.displayName = 'DragPreview';
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import React, { useRef, useEffect, useState, useCallback } from 'react';
|
|
2
|
+
import { observer } from 'mobx-react-lite';
|
|
3
|
+
import { ListItemData, ListContextMenuItem } from '../types/ListTypes';
|
|
4
|
+
import { ListItemsProvider } from '../providers/ListItemsProvider';
|
|
5
|
+
import { contextMenuIconMap } from '../../icons/iconMap';
|
|
6
|
+
|
|
7
|
+
// AICODE-NOTE: Context menu component for list items — positioned near cursor,
|
|
8
|
+
// closes on click-outside, Escape, or scroll. Styled to match @anymux/ui context-menu.
|
|
9
|
+
|
|
10
|
+
export interface ListContextMenuProps {
|
|
11
|
+
isOpen: boolean;
|
|
12
|
+
position: { x: number; y: number };
|
|
13
|
+
items: ListItemData[];
|
|
14
|
+
provider: ListItemsProvider;
|
|
15
|
+
onClose: () => void;
|
|
16
|
+
/** Extra menu items to prepend before provider items */
|
|
17
|
+
extraMenuItems?: ListContextMenuItem[];
|
|
18
|
+
/** Handler for extra menu item actions (called instead of provider.onContextMenuAction) */
|
|
19
|
+
onExtraMenuAction?: (actionId: string, items: ListItemData[]) => void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// AICODE-NOTE: Individual context menu item with icon, label, shortcut, destructive variant
|
|
23
|
+
interface ContextMenuItemRowProps {
|
|
24
|
+
menuItem: ListContextMenuItem;
|
|
25
|
+
isFocused: boolean;
|
|
26
|
+
onMenuItemClick: (menuItem: ListContextMenuItem) => void;
|
|
27
|
+
onMouseEnter: () => void;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const ContextMenuItemRow: React.FC<ContextMenuItemRowProps> = ({
|
|
31
|
+
menuItem,
|
|
32
|
+
isFocused,
|
|
33
|
+
onMenuItemClick,
|
|
34
|
+
onMouseEnter,
|
|
35
|
+
}) => {
|
|
36
|
+
if (menuItem.type === 'separator') {
|
|
37
|
+
return <div className="bg-border -mx-1 my-1 h-px" />;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const isDestructive = menuItem.destructive;
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<button
|
|
44
|
+
role="menuitem"
|
|
45
|
+
onClick={() => !menuItem.disabled && onMenuItemClick(menuItem)}
|
|
46
|
+
onMouseEnter={onMouseEnter}
|
|
47
|
+
disabled={menuItem.disabled}
|
|
48
|
+
className={[
|
|
49
|
+
'w-full text-left px-2 py-1.5 text-sm outline-hidden select-none',
|
|
50
|
+
'flex items-center gap-2 rounded-sm cursor-default',
|
|
51
|
+
menuItem.disabled
|
|
52
|
+
? 'pointer-events-none opacity-50'
|
|
53
|
+
: '',
|
|
54
|
+
!menuItem.disabled && !isDestructive
|
|
55
|
+
? 'text-foreground'
|
|
56
|
+
: '',
|
|
57
|
+
!menuItem.disabled && isDestructive
|
|
58
|
+
? 'text-destructive'
|
|
59
|
+
: '',
|
|
60
|
+
isFocused && !isDestructive
|
|
61
|
+
? 'bg-accent text-accent-foreground'
|
|
62
|
+
: '',
|
|
63
|
+
isFocused && isDestructive
|
|
64
|
+
? 'bg-destructive/10 text-destructive'
|
|
65
|
+
: '',
|
|
66
|
+
].filter(Boolean).join(' ')}
|
|
67
|
+
title={menuItem.tooltip}
|
|
68
|
+
>
|
|
69
|
+
{/* AICODE-NOTE: Menu item icon */}
|
|
70
|
+
{menuItem.icon && (
|
|
71
|
+
<span className={[
|
|
72
|
+
'w-4 h-4 flex-shrink-0 flex items-center justify-center',
|
|
73
|
+
!isDestructive ? '[&_svg]:text-muted-foreground' : '',
|
|
74
|
+
].filter(Boolean).join(' ')}>
|
|
75
|
+
{typeof menuItem.icon === 'string' ? (
|
|
76
|
+
(() => {
|
|
77
|
+
const Icon = contextMenuIconMap[menuItem.icon as string];
|
|
78
|
+
return Icon ? <Icon className="size-4" /> : null;
|
|
79
|
+
})()
|
|
80
|
+
) : (
|
|
81
|
+
React.createElement(menuItem.icon, { className: 'size-4' })
|
|
82
|
+
)}
|
|
83
|
+
</span>
|
|
84
|
+
)}
|
|
85
|
+
|
|
86
|
+
{/* AICODE-NOTE: Menu item label */}
|
|
87
|
+
<span className="flex-1 truncate">{menuItem.label}</span>
|
|
88
|
+
|
|
89
|
+
{/* AICODE-NOTE: Keyboard shortcut hint */}
|
|
90
|
+
{menuItem.shortcut && (
|
|
91
|
+
<span className="text-xs text-muted-foreground ml-auto tracking-widest">
|
|
92
|
+
{menuItem.shortcut}
|
|
93
|
+
</span>
|
|
94
|
+
)}
|
|
95
|
+
</button>
|
|
96
|
+
);
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
ContextMenuItemRow.displayName = 'ContextMenuItemRow';
|
|
100
|
+
|
|
101
|
+
export const ListContextMenu = observer<ListContextMenuProps>(({
|
|
102
|
+
isOpen,
|
|
103
|
+
position,
|
|
104
|
+
items,
|
|
105
|
+
provider,
|
|
106
|
+
onClose,
|
|
107
|
+
extraMenuItems,
|
|
108
|
+
onExtraMenuAction,
|
|
109
|
+
}) => {
|
|
110
|
+
const menuRef = useRef<HTMLDivElement>(null);
|
|
111
|
+
const [menuItems, setMenuItems] = useState<ListContextMenuItem[]>([]);
|
|
112
|
+
const [focusedIndex, setFocusedIndex] = useState(-1);
|
|
113
|
+
const [adjustedPosition, setAdjustedPosition] = useState(position);
|
|
114
|
+
|
|
115
|
+
// Get the actionable (non-separator) items for keyboard navigation
|
|
116
|
+
const actionableIndices = menuItems
|
|
117
|
+
.map((item, idx) => (item.type !== 'separator' ? idx : -1))
|
|
118
|
+
.filter(idx => idx !== -1);
|
|
119
|
+
|
|
120
|
+
// AICODE-NOTE: Get context menu items from provider when menu opens
|
|
121
|
+
useEffect(() => {
|
|
122
|
+
if (isOpen) {
|
|
123
|
+
let providerItems: ListContextMenuItem[] = [];
|
|
124
|
+
if (items.length === 0) {
|
|
125
|
+
providerItems = provider.getEmptySpaceContextMenu?.() || [];
|
|
126
|
+
} else if (items.length === 1) {
|
|
127
|
+
const firstItem = items[0];
|
|
128
|
+
if (firstItem) {
|
|
129
|
+
providerItems = provider.getItemContextMenu?.(firstItem) || [];
|
|
130
|
+
}
|
|
131
|
+
} else {
|
|
132
|
+
providerItems = provider.getMultiItemContextMenu?.(items) || [];
|
|
133
|
+
}
|
|
134
|
+
// Prepend extra menu items if provided
|
|
135
|
+
if (extraMenuItems && extraMenuItems.length > 0) {
|
|
136
|
+
setMenuItems([...extraMenuItems, ...providerItems]);
|
|
137
|
+
} else {
|
|
138
|
+
setMenuItems(providerItems);
|
|
139
|
+
}
|
|
140
|
+
setFocusedIndex(-1);
|
|
141
|
+
setAdjustedPosition(position);
|
|
142
|
+
}
|
|
143
|
+
}, [isOpen, items, provider, position, extraMenuItems]);
|
|
144
|
+
|
|
145
|
+
// AICODE-NOTE: Adjust position once the menu has rendered to keep within viewport
|
|
146
|
+
useEffect(() => {
|
|
147
|
+
if (!isOpen || !menuRef.current) return;
|
|
148
|
+
|
|
149
|
+
const menu = menuRef.current;
|
|
150
|
+
const rect = menu.getBoundingClientRect();
|
|
151
|
+
const vw = window.innerWidth;
|
|
152
|
+
const vh = window.innerHeight;
|
|
153
|
+
|
|
154
|
+
let left = position.x;
|
|
155
|
+
let top = position.y;
|
|
156
|
+
|
|
157
|
+
if (left + rect.width > vw - 8) {
|
|
158
|
+
left = Math.max(8, position.x - rect.width);
|
|
159
|
+
}
|
|
160
|
+
if (top + rect.height > vh - 8) {
|
|
161
|
+
top = Math.max(8, position.y - rect.height);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
left = Math.max(8, Math.min(left, vw - rect.width - 8));
|
|
165
|
+
top = Math.max(8, Math.min(top, vh - rect.height - 8));
|
|
166
|
+
|
|
167
|
+
setAdjustedPosition({ x: left, y: top });
|
|
168
|
+
}, [isOpen, menuItems, position]);
|
|
169
|
+
|
|
170
|
+
// AICODE-NOTE: Close on click-outside, Escape, or scroll
|
|
171
|
+
useEffect(() => {
|
|
172
|
+
if (!isOpen) return;
|
|
173
|
+
|
|
174
|
+
const handleClickOutside = (event: MouseEvent) => {
|
|
175
|
+
if (menuRef.current && !menuRef.current.contains(event.target as Node)) {
|
|
176
|
+
onClose();
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
const handleKeyDown = (event: KeyboardEvent) => {
|
|
181
|
+
switch (event.key) {
|
|
182
|
+
case 'Escape':
|
|
183
|
+
event.preventDefault();
|
|
184
|
+
onClose();
|
|
185
|
+
break;
|
|
186
|
+
case 'ArrowDown': {
|
|
187
|
+
event.preventDefault();
|
|
188
|
+
const currentActionIdx = actionableIndices.indexOf(focusedIndex);
|
|
189
|
+
const nextIdx = currentActionIdx < actionableIndices.length - 1
|
|
190
|
+
? actionableIndices[currentActionIdx + 1]
|
|
191
|
+
: actionableIndices[0];
|
|
192
|
+
if (nextIdx !== undefined) setFocusedIndex(nextIdx);
|
|
193
|
+
break;
|
|
194
|
+
}
|
|
195
|
+
case 'ArrowUp': {
|
|
196
|
+
event.preventDefault();
|
|
197
|
+
const currentActionIdx = actionableIndices.indexOf(focusedIndex);
|
|
198
|
+
const prevIdx = currentActionIdx > 0
|
|
199
|
+
? actionableIndices[currentActionIdx - 1]
|
|
200
|
+
: actionableIndices[actionableIndices.length - 1];
|
|
201
|
+
if (prevIdx !== undefined) setFocusedIndex(prevIdx);
|
|
202
|
+
break;
|
|
203
|
+
}
|
|
204
|
+
case 'Enter': {
|
|
205
|
+
event.preventDefault();
|
|
206
|
+
const focused = menuItems[focusedIndex];
|
|
207
|
+
if (focused && focused.type !== 'separator' && !focused.disabled) {
|
|
208
|
+
handleMenuItemClick(focused);
|
|
209
|
+
}
|
|
210
|
+
break;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
const handleScroll = () => onClose();
|
|
216
|
+
|
|
217
|
+
// Use capture phase for mousedown so we close before other handlers fire
|
|
218
|
+
document.addEventListener('mousedown', handleClickOutside, true);
|
|
219
|
+
document.addEventListener('keydown', handleKeyDown);
|
|
220
|
+
window.addEventListener('scroll', handleScroll, true);
|
|
221
|
+
|
|
222
|
+
return () => {
|
|
223
|
+
document.removeEventListener('mousedown', handleClickOutside, true);
|
|
224
|
+
document.removeEventListener('keydown', handleKeyDown);
|
|
225
|
+
window.removeEventListener('scroll', handleScroll, true);
|
|
226
|
+
};
|
|
227
|
+
}, [isOpen, onClose, focusedIndex, menuItems, actionableIndices]);
|
|
228
|
+
|
|
229
|
+
// AICODE-NOTE: Handle menu item click — dispatch to extra handler or provider and close
|
|
230
|
+
const handleMenuItemClick = useCallback((menuItem: ListContextMenuItem) => {
|
|
231
|
+
if (extraMenuItems?.some(m => m.type !== 'separator' && m.id === menuItem.id) && onExtraMenuAction) {
|
|
232
|
+
onExtraMenuAction(menuItem.id, items);
|
|
233
|
+
} else if (provider.onContextMenuAction) {
|
|
234
|
+
provider.onContextMenuAction(menuItem.id, items);
|
|
235
|
+
}
|
|
236
|
+
onClose();
|
|
237
|
+
}, [provider, items, onClose, extraMenuItems, onExtraMenuAction]);
|
|
238
|
+
|
|
239
|
+
if (!isOpen || menuItems.length === 0) {
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return (
|
|
244
|
+
<div
|
|
245
|
+
ref={menuRef}
|
|
246
|
+
role="menu"
|
|
247
|
+
style={{
|
|
248
|
+
position: 'fixed',
|
|
249
|
+
left: adjustedPosition.x,
|
|
250
|
+
top: adjustedPosition.y,
|
|
251
|
+
zIndex: 50,
|
|
252
|
+
}}
|
|
253
|
+
className={[
|
|
254
|
+
'bg-popover text-popover-foreground',
|
|
255
|
+
'rounded-md border p-1 shadow-md',
|
|
256
|
+
'min-w-[180px] max-w-[260px]',
|
|
257
|
+
'animate-in fade-in-0 zoom-in-95',
|
|
258
|
+
'origin-top-left',
|
|
259
|
+
].join(' ')}
|
|
260
|
+
>
|
|
261
|
+
{menuItems.map((menuItem, index) => (
|
|
262
|
+
<ContextMenuItemRow
|
|
263
|
+
key={menuItem.type === 'separator' ? `sep-${index}` : menuItem.id}
|
|
264
|
+
menuItem={menuItem}
|
|
265
|
+
isFocused={focusedIndex === index}
|
|
266
|
+
onMenuItemClick={handleMenuItemClick}
|
|
267
|
+
onMouseEnter={() => setFocusedIndex(index)}
|
|
268
|
+
/>
|
|
269
|
+
))}
|
|
270
|
+
</div>
|
|
271
|
+
);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
ListContextMenu.displayName = 'ListContextMenu';
|