@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,919 @@
|
|
|
1
|
+
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
|
2
|
+
import { observer } from 'mobx-react-lite';
|
|
3
|
+
import { ListItemsModel } from '../models/ListItemsModel';
|
|
4
|
+
import { ListItem } from './ListItem';
|
|
5
|
+
import { VirtualizedList } from './VirtualizedList';
|
|
6
|
+
import { VirtualizedGrid } from './VirtualizedGrid';
|
|
7
|
+
import { VirtualizedDetailsView } from './VirtualizedDetailsView';
|
|
8
|
+
import { ListContextMenu } from './ListContextMenu';
|
|
9
|
+
import { useListKeyboard } from '../utils/ListKeyboard';
|
|
10
|
+
import { MasonryView } from './MasonryView';
|
|
11
|
+
import { VirtualizedMasonryView } from './VirtualizedMasonryView';
|
|
12
|
+
import { ListLoader } from './shared/ListLoader';
|
|
13
|
+
import { LoadError } from './shared/ErrorDisplay';
|
|
14
|
+
import { NoItems } from './shared/EmptyState';
|
|
15
|
+
import { SearchX } from 'lucide-react';
|
|
16
|
+
import { getListAccessibilityProps, announceToScreenReader, createLiveRegionAnnouncement } from '../utils/ListAccessibility';
|
|
17
|
+
import { benchmark } from '../utils/BenchmarkLogger';
|
|
18
|
+
import { CalculatedGridView } from './CalculatedGridView';
|
|
19
|
+
import { TreemapView } from './TreemapView';
|
|
20
|
+
import type { TileInfo } from './TreemapView';
|
|
21
|
+
import { TreemapModel } from '../models/TreemapModel';
|
|
22
|
+
import { LIST_VIEW_TYPE } from '../providers/ListItemsProvider';
|
|
23
|
+
import type { ListItemData, ListContextMenuItem } from '../types/ListTypes';
|
|
24
|
+
import {
|
|
25
|
+
Pagination,
|
|
26
|
+
PaginationContent,
|
|
27
|
+
PaginationItem,
|
|
28
|
+
PaginationLink,
|
|
29
|
+
PaginationPrevious,
|
|
30
|
+
PaginationNext,
|
|
31
|
+
PaginationEllipsis,
|
|
32
|
+
} from '@anymux/ui/components/pagination';
|
|
33
|
+
|
|
34
|
+
const ListPagination = observer<{ model: ListItemsModel }>(({ model }) => {
|
|
35
|
+
if (model.totalPages <= 1) return null;
|
|
36
|
+
|
|
37
|
+
const pages: (number | 'ellipsis')[] = [];
|
|
38
|
+
const total = model.totalPages;
|
|
39
|
+
const current = model.currentPage;
|
|
40
|
+
|
|
41
|
+
pages.push(0);
|
|
42
|
+
if (current > 2) pages.push('ellipsis');
|
|
43
|
+
for (let i = Math.max(1, current - 1); i <= Math.min(total - 2, current + 1); i++) {
|
|
44
|
+
pages.push(i);
|
|
45
|
+
}
|
|
46
|
+
if (current < total - 3) pages.push('ellipsis');
|
|
47
|
+
if (total > 1) pages.push(total - 1);
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<Pagination className="py-2 border-t flex-shrink-0">
|
|
51
|
+
<PaginationContent>
|
|
52
|
+
<PaginationItem>
|
|
53
|
+
<PaginationPrevious
|
|
54
|
+
onClick={(e) => { e.preventDefault(); model.prevPage(); }}
|
|
55
|
+
className={model.hasPreviousPage ? 'cursor-pointer' : 'pointer-events-none opacity-50'}
|
|
56
|
+
/>
|
|
57
|
+
</PaginationItem>
|
|
58
|
+
{pages.map((page, i) =>
|
|
59
|
+
page === 'ellipsis' ? (
|
|
60
|
+
<PaginationItem key={`e${i}`}>
|
|
61
|
+
<PaginationEllipsis />
|
|
62
|
+
</PaginationItem>
|
|
63
|
+
) : (
|
|
64
|
+
<PaginationItem key={page}>
|
|
65
|
+
<PaginationLink
|
|
66
|
+
isActive={page === current}
|
|
67
|
+
onClick={(e) => { e.preventDefault(); model.goToPage(page); }}
|
|
68
|
+
className="cursor-pointer"
|
|
69
|
+
>
|
|
70
|
+
{page + 1}
|
|
71
|
+
</PaginationLink>
|
|
72
|
+
</PaginationItem>
|
|
73
|
+
)
|
|
74
|
+
)}
|
|
75
|
+
<PaginationItem>
|
|
76
|
+
<PaginationNext
|
|
77
|
+
onClick={(e) => { e.preventDefault(); model.nextPage(); }}
|
|
78
|
+
className={model.hasNextPage ? 'cursor-pointer' : 'pointer-events-none opacity-50'}
|
|
79
|
+
/>
|
|
80
|
+
</PaginationItem>
|
|
81
|
+
</PaginationContent>
|
|
82
|
+
</Pagination>
|
|
83
|
+
);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// AICODE-NOTE: Basic list container component that connects model with UI
|
|
87
|
+
|
|
88
|
+
export interface ListItemsProps {
|
|
89
|
+
model: ListItemsModel;
|
|
90
|
+
className?: string;
|
|
91
|
+
style?: React.CSSProperties;
|
|
92
|
+
height?: number;
|
|
93
|
+
width?: number;
|
|
94
|
+
enableVirtualization?: boolean;
|
|
95
|
+
enableKeyboardNavigation?: boolean;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export const ListItems = observer<ListItemsProps>(({
|
|
99
|
+
model,
|
|
100
|
+
className = '',
|
|
101
|
+
style,
|
|
102
|
+
height,
|
|
103
|
+
width,
|
|
104
|
+
enableVirtualization = true,
|
|
105
|
+
enableKeyboardNavigation = true
|
|
106
|
+
}) => {
|
|
107
|
+
// AICODE-NOTE: Benchmark render performance
|
|
108
|
+
benchmark.start('ListItems.render', {
|
|
109
|
+
viewType: model.currentViewType.id,
|
|
110
|
+
itemCount: model.items.length,
|
|
111
|
+
enableVirtualization,
|
|
112
|
+
hasSelection: model.hasSelection
|
|
113
|
+
});
|
|
114
|
+
// AICODE-NOTE: Container ref for keyboard navigation
|
|
115
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
116
|
+
|
|
117
|
+
// Auto-measure container size when width/height props are not provided
|
|
118
|
+
const [measuredSize, setMeasuredSize] = useState({ w: 0, h: 0 });
|
|
119
|
+
const resizeTimerRef = useRef<ReturnType<typeof setTimeout>>();
|
|
120
|
+
|
|
121
|
+
useEffect(() => {
|
|
122
|
+
if (width !== undefined && height !== undefined) return;
|
|
123
|
+
const el = containerRef.current;
|
|
124
|
+
if (!el) return;
|
|
125
|
+
let isFirst = true;
|
|
126
|
+
const ro = new ResizeObserver((entries) => {
|
|
127
|
+
const entry = entries[0];
|
|
128
|
+
if (!entry) return;
|
|
129
|
+
const newW = Math.round(entry.contentRect.width);
|
|
130
|
+
const newH = Math.round(entry.contentRect.height);
|
|
131
|
+
if (isFirst) {
|
|
132
|
+
// First measurement is immediate so layout renders correctly
|
|
133
|
+
isFirst = false;
|
|
134
|
+
setMeasuredSize({ w: newW, h: newH });
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
// Subsequent resize events are debounced
|
|
138
|
+
clearTimeout(resizeTimerRef.current);
|
|
139
|
+
resizeTimerRef.current = setTimeout(() => {
|
|
140
|
+
setMeasuredSize({ w: newW, h: newH });
|
|
141
|
+
}, 150);
|
|
142
|
+
});
|
|
143
|
+
ro.observe(el);
|
|
144
|
+
return () => { ro.disconnect(); clearTimeout(resizeTimerRef.current); };
|
|
145
|
+
}, [width, height]);
|
|
146
|
+
|
|
147
|
+
const effectiveWidth = width ?? measuredSize.w || 800;
|
|
148
|
+
const effectiveHeight = height ?? measuredSize.h || 600;
|
|
149
|
+
|
|
150
|
+
// When no explicit dimensions, use CSS sizing; otherwise inline style
|
|
151
|
+
const sizeStyle: React.CSSProperties | undefined =
|
|
152
|
+
height !== undefined || width !== undefined
|
|
153
|
+
? { height, width }
|
|
154
|
+
: undefined;
|
|
155
|
+
const sizeClass = height === undefined ? 'h-full' : '';
|
|
156
|
+
|
|
157
|
+
// AICODE-NOTE: Context menu state
|
|
158
|
+
const [contextMenu, setContextMenu] = useState<{
|
|
159
|
+
isOpen: boolean;
|
|
160
|
+
position: { x: number; y: number };
|
|
161
|
+
items: ListItemData[];
|
|
162
|
+
}>({
|
|
163
|
+
isOpen: false,
|
|
164
|
+
position: { x: 0, y: 0 },
|
|
165
|
+
items: []
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// AICODE-NOTE: Set up keyboard navigation
|
|
169
|
+
useListKeyboard(
|
|
170
|
+
model,
|
|
171
|
+
{
|
|
172
|
+
enableArrowKeys: true,
|
|
173
|
+
enableHomeEnd: true,
|
|
174
|
+
enableSpaceEnter: true,
|
|
175
|
+
enableSelectAll: true,
|
|
176
|
+
enableEscape: true,
|
|
177
|
+
enablePageUpDown: true
|
|
178
|
+
},
|
|
179
|
+
enableKeyboardNavigation ? containerRef : undefined
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
// Scroll to item for list/details views (grid/masonry handle it themselves)
|
|
183
|
+
useEffect(() => {
|
|
184
|
+
const targetId = model.scrollToItemId;
|
|
185
|
+
if (!targetId || !containerRef.current) return;
|
|
186
|
+
const viewType = model.currentViewType.id;
|
|
187
|
+
if (viewType === 'grid' || viewType.includes('masonry')) return; // handled by grid/masonry views
|
|
188
|
+
const index = model.items.findIndex(item => item.id === targetId);
|
|
189
|
+
if (index === -1) return;
|
|
190
|
+
const divideY = containerRef.current.querySelector('.divide-y');
|
|
191
|
+
const child = divideY?.children[index] as HTMLElement | undefined;
|
|
192
|
+
if (child) {
|
|
193
|
+
child.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
|
|
194
|
+
}
|
|
195
|
+
model.clearScrollToItem();
|
|
196
|
+
}, [model.scrollToItemId, model]);
|
|
197
|
+
|
|
198
|
+
// AICODE-NOTE: Load items when component mounts
|
|
199
|
+
useEffect(() => {
|
|
200
|
+
if (model.allItemsCount === 0 && !model.isLoading) {
|
|
201
|
+
model.loadItems();
|
|
202
|
+
}
|
|
203
|
+
}, [model]);
|
|
204
|
+
|
|
205
|
+
// AICODE-NOTE: Handle item clicks with proper modifier support
|
|
206
|
+
const handleItemClick = (item: any, event: React.MouseEvent) => {
|
|
207
|
+
const modifiers = {
|
|
208
|
+
ctrl: event.ctrlKey || event.metaKey,
|
|
209
|
+
shift: event.shiftKey
|
|
210
|
+
};
|
|
211
|
+
model.selectItem(item, modifiers);
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
// AICODE-NOTE: Handle item double clicks
|
|
215
|
+
const handleItemDoubleClick = (item: any, event: React.MouseEvent) => {
|
|
216
|
+
if (model.provider.onItemDoubleClick) {
|
|
217
|
+
model.provider.onItemDoubleClick(item);
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
// AICODE-NOTE: Handle context menu
|
|
222
|
+
const handleItemContextMenu = (item: any, event: React.MouseEvent) => {
|
|
223
|
+
event.preventDefault();
|
|
224
|
+
event.stopPropagation();
|
|
225
|
+
|
|
226
|
+
// AICODE-NOTE: If item is not selected, select it first
|
|
227
|
+
if (!model.isItemSelected(item.id)) {
|
|
228
|
+
model.selectItem(item);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// AICODE-NOTE: Get items for context menu (selected items or just this item)
|
|
232
|
+
const contextItems = model.hasSelection ? model.selectedItemsArray : [item];
|
|
233
|
+
|
|
234
|
+
setContextMenu({
|
|
235
|
+
isOpen: true,
|
|
236
|
+
position: { x: event.clientX, y: event.clientY },
|
|
237
|
+
items: contextItems
|
|
238
|
+
});
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
// AICODE-NOTE: Close context menu
|
|
242
|
+
const handleCloseContextMenu = () => {
|
|
243
|
+
setContextMenu(prev => ({ ...prev, isOpen: false }));
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
// AICODE-NOTE: Handle empty-space right-click for "New Folder" / "New File" actions
|
|
247
|
+
const handleContainerContextMenu = (event: React.MouseEvent) => {
|
|
248
|
+
// Only fire if the target is the container itself (not an item)
|
|
249
|
+
// Item context menus call stopPropagation, so this only fires for empty space
|
|
250
|
+
event.preventDefault();
|
|
251
|
+
const emptyMenuItems = model.provider.getEmptySpaceContextMenu?.();
|
|
252
|
+
if (emptyMenuItems && emptyMenuItems.length > 0) {
|
|
253
|
+
setContextMenu({
|
|
254
|
+
isOpen: true,
|
|
255
|
+
position: { x: event.clientX, y: event.clientY },
|
|
256
|
+
items: [] // empty items array signals empty-space menu
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
// AICODE-NOTE: Drag and drop handlers
|
|
262
|
+
const handleItemDragStart = (item: ListItemData, event: React.DragEvent) => {
|
|
263
|
+
// AICODE-NOTE: Determine items to drag before any selection changes
|
|
264
|
+
let itemsToDrag: ListItemData[];
|
|
265
|
+
|
|
266
|
+
if (model.isItemSelected(item.id) && model.hasSelection) {
|
|
267
|
+
// Item is already selected and we have a selection - drag all selected items
|
|
268
|
+
itemsToDrag = model.selectedItemsArray;
|
|
269
|
+
} else {
|
|
270
|
+
// Item is not selected or no selection - select it first, then drag just this item
|
|
271
|
+
model.selectItem(item);
|
|
272
|
+
itemsToDrag = [item];
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
model.startDrag(itemsToDrag, event.nativeEvent as DragEvent);
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
const handleItemDragOver = (item: ListItemData, event: React.DragEvent) => {
|
|
279
|
+
if (!model.isDragging) return;
|
|
280
|
+
|
|
281
|
+
// AICODE-NOTE: Calculate drop position based on mouse position
|
|
282
|
+
const rect = (event.currentTarget as HTMLElement).getBoundingClientRect();
|
|
283
|
+
const y = event.clientY - rect.top;
|
|
284
|
+
const height = rect.height;
|
|
285
|
+
|
|
286
|
+
let position: 'before' | 'after' | 'inside' = 'inside';
|
|
287
|
+
|
|
288
|
+
if (y < height * 0.25) {
|
|
289
|
+
position = 'before';
|
|
290
|
+
} else if (y > height * 0.75) {
|
|
291
|
+
position = 'after';
|
|
292
|
+
} else {
|
|
293
|
+
position = 'inside';
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// AICODE-NOTE: Check if drop is allowed
|
|
297
|
+
if (model.canDropItems(model.draggedItems, item, position)) {
|
|
298
|
+
model.setDragOver(item.id, position);
|
|
299
|
+
}
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
const handleItemDragLeave = (item: ListItemData, event: React.DragEvent) => {
|
|
303
|
+
// AICODE-NOTE: Only clear drag over if we're actually leaving the item
|
|
304
|
+
const rect = (event.currentTarget as HTMLElement).getBoundingClientRect();
|
|
305
|
+
const x = event.clientX;
|
|
306
|
+
const y = event.clientY;
|
|
307
|
+
|
|
308
|
+
if (x < rect.left || x > rect.right || y < rect.top || y > rect.bottom) {
|
|
309
|
+
model.setDragOver(null, null);
|
|
310
|
+
}
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
const handleItemDrop = (item: ListItemData, event: React.DragEvent) => {
|
|
314
|
+
if (!model.isDragging) return;
|
|
315
|
+
|
|
316
|
+
// AICODE-NOTE: Calculate drop position
|
|
317
|
+
const rect = (event.currentTarget as HTMLElement).getBoundingClientRect();
|
|
318
|
+
const y = event.clientY - rect.top;
|
|
319
|
+
const height = rect.height;
|
|
320
|
+
|
|
321
|
+
let position: 'before' | 'after' | 'inside' = 'inside';
|
|
322
|
+
|
|
323
|
+
if (y < height * 0.25) {
|
|
324
|
+
position = 'before';
|
|
325
|
+
} else if (y > height * 0.75) {
|
|
326
|
+
position = 'after';
|
|
327
|
+
} else {
|
|
328
|
+
position = 'inside';
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
model.handleDrop(item, position, event.nativeEvent as DragEvent);
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
// AICODE-NOTE: Handle drag end
|
|
335
|
+
const handleDragEnd = () => {
|
|
336
|
+
if (model.isDragging) {
|
|
337
|
+
model.endDrag(false);
|
|
338
|
+
}
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
// AICODE-NOTE: Get item width from provider if available
|
|
342
|
+
const getItemWidth = () => {
|
|
343
|
+
// AICODE-NOTE: Use model dimensions first (from size controls), then fallback to provider
|
|
344
|
+
if (model.currentViewType.id === 'grid' || model.currentViewType.id.includes('masonry')) {
|
|
345
|
+
return model.itemDimensions.width;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
if (model.provider.getItemWidth) {
|
|
349
|
+
return model.provider.getItemWidth(model.currentViewType);
|
|
350
|
+
}
|
|
351
|
+
return undefined;
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
// AICODE-NOTE: Get item height from model dimensions
|
|
355
|
+
const getItemHeight = () => {
|
|
356
|
+
if (model.currentViewType.id === 'grid' || model.currentViewType.id.includes('masonry')) {
|
|
357
|
+
return model.itemDimensions.height;
|
|
358
|
+
}
|
|
359
|
+
return undefined;
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
// AICODE-NOTE: Render loading state with skeleton
|
|
363
|
+
if (model.isLoading && model.allItemsCount === 0) {
|
|
364
|
+
benchmark.end('ListItems.render', { renderType: 'loading' });
|
|
365
|
+
return (
|
|
366
|
+
<div
|
|
367
|
+
ref={containerRef}
|
|
368
|
+
className={`${sizeClass} ${className}`}
|
|
369
|
+
style={sizeStyle}
|
|
370
|
+
tabIndex={0}
|
|
371
|
+
>
|
|
372
|
+
<ListLoader
|
|
373
|
+
viewType={model.currentViewType}
|
|
374
|
+
itemCount={12}
|
|
375
|
+
itemWidth={model.itemDimensions.width}
|
|
376
|
+
itemHeight={model.itemDimensions.height}
|
|
377
|
+
className="w-full h-full"
|
|
378
|
+
/>
|
|
379
|
+
</div>
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// AICODE-NOTE: Render error state
|
|
384
|
+
if (model.hasErrors && model.allItemsCount === 0) {
|
|
385
|
+
const errors = Array.from(model.errors.values());
|
|
386
|
+
const firstError = errors[0] || new Error('Unknown error occurred');
|
|
387
|
+
benchmark.end('ListItems.render', { renderType: 'error' });
|
|
388
|
+
return (
|
|
389
|
+
<div
|
|
390
|
+
ref={containerRef}
|
|
391
|
+
className={`flex items-center justify-center ${sizeClass} ${className}`}
|
|
392
|
+
style={sizeStyle}
|
|
393
|
+
tabIndex={0}
|
|
394
|
+
>
|
|
395
|
+
<LoadError
|
|
396
|
+
error={firstError}
|
|
397
|
+
onRetry={() => model.refresh()}
|
|
398
|
+
className="max-w-md"
|
|
399
|
+
/>
|
|
400
|
+
</div>
|
|
401
|
+
);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// AICODE-NOTE: Render empty state
|
|
405
|
+
if (!model.isLoading && model.items.length === 0) {
|
|
406
|
+
benchmark.end('ListItems.render', { renderType: 'empty' });
|
|
407
|
+
|
|
408
|
+
// Search returned no results but directory has items
|
|
409
|
+
if (model.hasSearchQuery) {
|
|
410
|
+
return (
|
|
411
|
+
<div
|
|
412
|
+
ref={containerRef}
|
|
413
|
+
className={`flex flex-col items-center justify-center gap-2 text-muted-foreground ${sizeClass} ${className}`}
|
|
414
|
+
style={sizeStyle}
|
|
415
|
+
tabIndex={0}
|
|
416
|
+
>
|
|
417
|
+
<SearchX className="w-10 h-10 opacity-40" />
|
|
418
|
+
<p className="text-sm">No files match “{model.searchQuery}”</p>
|
|
419
|
+
<button
|
|
420
|
+
onClick={() => model.clearSearch()}
|
|
421
|
+
className="text-xs text-primary hover:underline"
|
|
422
|
+
>
|
|
423
|
+
Clear filter
|
|
424
|
+
</button>
|
|
425
|
+
</div>
|
|
426
|
+
);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
return (
|
|
430
|
+
<>
|
|
431
|
+
<div
|
|
432
|
+
ref={containerRef}
|
|
433
|
+
className={`${sizeClass} ${className}`}
|
|
434
|
+
style={sizeStyle}
|
|
435
|
+
tabIndex={0}
|
|
436
|
+
onContextMenu={handleContainerContextMenu}
|
|
437
|
+
>
|
|
438
|
+
<NoItems
|
|
439
|
+
onRefresh={() => model.refresh()}
|
|
440
|
+
className="h-full"
|
|
441
|
+
/>
|
|
442
|
+
</div>
|
|
443
|
+
<ListContextMenu
|
|
444
|
+
isOpen={contextMenu.isOpen}
|
|
445
|
+
position={contextMenu.position}
|
|
446
|
+
items={contextMenu.items}
|
|
447
|
+
provider={model.provider}
|
|
448
|
+
onClose={handleCloseContextMenu}
|
|
449
|
+
/>
|
|
450
|
+
</>
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// AICODE-NOTE: Base container styles
|
|
455
|
+
const containerClasses = `
|
|
456
|
+
h-full w-full overflow-auto
|
|
457
|
+
${className}
|
|
458
|
+
`;
|
|
459
|
+
|
|
460
|
+
// AICODE-NOTE: Generate accessibility props
|
|
461
|
+
const accessibilityProps = getListAccessibilityProps({
|
|
462
|
+
viewType: model.currentViewType.id,
|
|
463
|
+
isMultiSelect: model.provider.isMultiSelectEnabled,
|
|
464
|
+
totalItems: model.totalItemCount,
|
|
465
|
+
selectedCount: model.selectedItemsArray.length,
|
|
466
|
+
label: `${model.currentViewType.name} view`
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
// AICODE-NOTE: Treemap layout for treemap view
|
|
470
|
+
if (model.currentViewType.id === 'treemap') {
|
|
471
|
+
benchmark.end('ListItems.render', { renderType: 'treemap' });
|
|
472
|
+
return (
|
|
473
|
+
<TreemapSection
|
|
474
|
+
model={model}
|
|
475
|
+
containerRef={containerRef}
|
|
476
|
+
sizeClass={sizeClass}
|
|
477
|
+
className={className}
|
|
478
|
+
sizeStyle={sizeStyle}
|
|
479
|
+
effectiveWidth={effectiveWidth}
|
|
480
|
+
effectiveHeight={effectiveHeight}
|
|
481
|
+
contextMenu={contextMenu}
|
|
482
|
+
setContextMenu={setContextMenu}
|
|
483
|
+
handleCloseContextMenu={handleCloseContextMenu}
|
|
484
|
+
handleContainerContextMenu={handleContainerContextMenu}
|
|
485
|
+
/>
|
|
486
|
+
);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// AICODE-NOTE: Grid layout for grid view
|
|
490
|
+
if (model.currentViewType.id === 'grid') {
|
|
491
|
+
benchmark.end('ListItems.render', { renderType: 'grid-calculated' });
|
|
492
|
+
return (
|
|
493
|
+
<>
|
|
494
|
+
<CalculatedGridView
|
|
495
|
+
model={model}
|
|
496
|
+
className={`${sizeClass} ${className}`}
|
|
497
|
+
height={effectiveHeight}
|
|
498
|
+
width={effectiveWidth}
|
|
499
|
+
enableVirtualization={enableVirtualization}
|
|
500
|
+
onItemClick={handleItemClick}
|
|
501
|
+
onItemDoubleClick={handleItemDoubleClick}
|
|
502
|
+
onItemContextMenu={handleItemContextMenu}
|
|
503
|
+
onItemDragStart={handleItemDragStart}
|
|
504
|
+
onItemDragOver={handleItemDragOver}
|
|
505
|
+
onItemDragLeave={handleItemDragLeave}
|
|
506
|
+
onItemDrop={handleItemDrop}
|
|
507
|
+
/>
|
|
508
|
+
<ListPagination model={model} />
|
|
509
|
+
|
|
510
|
+
{/* AICODE-NOTE: Context menu */}
|
|
511
|
+
<ListContextMenu
|
|
512
|
+
isOpen={contextMenu.isOpen}
|
|
513
|
+
position={contextMenu.position}
|
|
514
|
+
items={contextMenu.items}
|
|
515
|
+
provider={model.provider}
|
|
516
|
+
onClose={handleCloseContextMenu}
|
|
517
|
+
/>
|
|
518
|
+
</>
|
|
519
|
+
);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// AICODE-NOTE: Masonry layouts for masonry views
|
|
523
|
+
if (model.currentViewType.id === 'masonry-horizontal' || model.currentViewType.id === 'masonry-vertical') {
|
|
524
|
+
// AICODE-NOTE: Always use virtualized masonry when container dimensions are available
|
|
525
|
+
// Masonry layouts can have thousands of items, so virtualization is essential
|
|
526
|
+
if (effectiveHeight && effectiveWidth) {
|
|
527
|
+
benchmark.end('ListItems.render', { renderType: 'masonry-virtualized' });
|
|
528
|
+
return (
|
|
529
|
+
<>
|
|
530
|
+
<div ref={containerRef} tabIndex={0} className={`${sizeClass} ${className}`} style={sizeStyle}>
|
|
531
|
+
<VirtualizedMasonryView
|
|
532
|
+
model={model}
|
|
533
|
+
items={model.items}
|
|
534
|
+
containerWidth={effectiveWidth}
|
|
535
|
+
containerHeight={effectiveHeight}
|
|
536
|
+
isHorizontal={model.currentViewType.id === 'masonry-horizontal'}
|
|
537
|
+
overscanCount={10}
|
|
538
|
+
className="w-full h-full"
|
|
539
|
+
/>
|
|
540
|
+
</div>
|
|
541
|
+
<ListPagination model={model} />
|
|
542
|
+
|
|
543
|
+
{/* AICODE-NOTE: Context menu */}
|
|
544
|
+
<ListContextMenu
|
|
545
|
+
isOpen={contextMenu.isOpen}
|
|
546
|
+
position={contextMenu.position}
|
|
547
|
+
items={contextMenu.items}
|
|
548
|
+
provider={model.provider}
|
|
549
|
+
onClose={handleCloseContextMenu}
|
|
550
|
+
/>
|
|
551
|
+
</>
|
|
552
|
+
);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// AICODE-NOTE: Fallback to non-virtualized masonry (when container dimensions not yet available)
|
|
556
|
+
benchmark.end('ListItems.render', { renderType: 'masonry-standard' });
|
|
557
|
+
return (
|
|
558
|
+
<>
|
|
559
|
+
<div
|
|
560
|
+
ref={containerRef}
|
|
561
|
+
className={`overflow-y-auto overflow-x-hidden ${sizeClass} ${className}`}
|
|
562
|
+
style={sizeStyle}
|
|
563
|
+
tabIndex={0}
|
|
564
|
+
>
|
|
565
|
+
<MasonryView
|
|
566
|
+
model={model}
|
|
567
|
+
items={model.items}
|
|
568
|
+
containerWidth={effectiveWidth}
|
|
569
|
+
containerHeight={effectiveHeight}
|
|
570
|
+
isHorizontal={model.currentViewType.id === 'masonry-horizontal'}
|
|
571
|
+
/>
|
|
572
|
+
</div>
|
|
573
|
+
<ListPagination model={model} />
|
|
574
|
+
|
|
575
|
+
{/* AICODE-NOTE: Context menu */}
|
|
576
|
+
<ListContextMenu
|
|
577
|
+
isOpen={contextMenu.isOpen}
|
|
578
|
+
position={contextMenu.position}
|
|
579
|
+
items={contextMenu.items}
|
|
580
|
+
provider={model.provider}
|
|
581
|
+
onClose={handleCloseContextMenu}
|
|
582
|
+
/>
|
|
583
|
+
</>
|
|
584
|
+
);
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// AICODE-NOTE: Details view with headers
|
|
588
|
+
if (model.currentViewType.id === 'details') {
|
|
589
|
+
// AICODE-NOTE: Use virtualized details view if enabled and height provided
|
|
590
|
+
if (enableVirtualization && effectiveHeight && model.provider.isVirtualizationEnabled) {
|
|
591
|
+
benchmark.end('ListItems.render', { renderType: 'details-virtualized' });
|
|
592
|
+
return (
|
|
593
|
+
<>
|
|
594
|
+
<div ref={containerRef} tabIndex={0} className={`${sizeClass} ${className}`} style={sizeStyle}>
|
|
595
|
+
<VirtualizedDetailsView
|
|
596
|
+
model={model}
|
|
597
|
+
height={effectiveHeight}
|
|
598
|
+
width={effectiveWidth}
|
|
599
|
+
/>
|
|
600
|
+
</div>
|
|
601
|
+
<ListPagination model={model} />
|
|
602
|
+
</>
|
|
603
|
+
);
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
// AICODE-NOTE: Fallback to non-virtualized details view
|
|
607
|
+
benchmark.end('ListItems.render', { renderType: 'details-standard' });
|
|
608
|
+
return (
|
|
609
|
+
<>
|
|
610
|
+
<div
|
|
611
|
+
ref={containerRef}
|
|
612
|
+
className={`overflow-auto ${sizeClass} ${className}`}
|
|
613
|
+
style={sizeStyle}
|
|
614
|
+
role={accessibilityProps.role}
|
|
615
|
+
aria-label={accessibilityProps.ariaLabel}
|
|
616
|
+
aria-multiselectable={accessibilityProps.ariaMultiSelectable}
|
|
617
|
+
tabIndex={accessibilityProps.tabIndex}
|
|
618
|
+
onContextMenu={handleContainerContextMenu}
|
|
619
|
+
>
|
|
620
|
+
{/* AICODE-NOTE: Column headers — same CSS grid as ListItem details rows */}
|
|
621
|
+
<div
|
|
622
|
+
className="sticky top-0 grid items-center bg-background border-b px-4 py-2 text-sm font-medium text-muted-foreground"
|
|
623
|
+
style={{ gridTemplateColumns: model.detailsGridTemplateColumns, columnGap: 16 }}
|
|
624
|
+
>
|
|
625
|
+
{model.showCheckboxes && (
|
|
626
|
+
<div className="flex items-center justify-center">
|
|
627
|
+
<input
|
|
628
|
+
type="checkbox"
|
|
629
|
+
checked={model.items.length > 0 && model.selectedItems.size === model.items.length}
|
|
630
|
+
ref={(el) => {
|
|
631
|
+
if (el) el.indeterminate = model.selectedItems.size > 0 && model.selectedItems.size < model.items.length;
|
|
632
|
+
}}
|
|
633
|
+
onChange={() => {
|
|
634
|
+
if (model.selectedItems.size === model.items.length) {
|
|
635
|
+
model.clearSelection();
|
|
636
|
+
} else {
|
|
637
|
+
model.selectAll();
|
|
638
|
+
}
|
|
639
|
+
}}
|
|
640
|
+
className="h-3.5 w-3.5 rounded border-muted-foreground/50 accent-primary cursor-pointer"
|
|
641
|
+
aria-label="Select all"
|
|
642
|
+
/>
|
|
643
|
+
</div>
|
|
644
|
+
)}
|
|
645
|
+
<div />
|
|
646
|
+
<div>Name</div>
|
|
647
|
+
{!model.compactMode && model.columnVisibility.type && <div>Type</div>}
|
|
648
|
+
{!model.compactMode && model.columnVisibility.modified && <div>Modified</div>}
|
|
649
|
+
{!model.compactMode && model.columnVisibility.size && <div className="text-right">Size</div>}
|
|
650
|
+
</div>
|
|
651
|
+
|
|
652
|
+
{/* AICODE-NOTE: Items */}
|
|
653
|
+
<div className="divide-y">
|
|
654
|
+
{benchmark.time('details-items-render', () => {
|
|
655
|
+
return model.items.map((item, index) => (
|
|
656
|
+
<ListItem
|
|
657
|
+
key={item.id}
|
|
658
|
+
item={item}
|
|
659
|
+
index={index}
|
|
660
|
+
totalItems={model.totalItemCount}
|
|
661
|
+
viewType={model.currentViewType}
|
|
662
|
+
provider={model.provider}
|
|
663
|
+
model={model}
|
|
664
|
+
isSelected={model.isItemSelected(item.id)}
|
|
665
|
+
isFocused={model.focusedItem === item.id}
|
|
666
|
+
isDraggedOver={model.dragOverItem === item.id}
|
|
667
|
+
dragOverPosition={model.dragOverItem === item.id ? model.dragOverPosition : null}
|
|
668
|
+
canDrag={model.canDragItem(item)}
|
|
669
|
+
onClick={handleItemClick}
|
|
670
|
+
onDoubleClick={handleItemDoubleClick}
|
|
671
|
+
onContextMenu={handleItemContextMenu}
|
|
672
|
+
onDragStart={handleItemDragStart}
|
|
673
|
+
onDragOver={handleItemDragOver}
|
|
674
|
+
onDragLeave={handleItemDragLeave}
|
|
675
|
+
onDrop={handleItemDrop}
|
|
676
|
+
/>
|
|
677
|
+
));
|
|
678
|
+
})}
|
|
679
|
+
</div>
|
|
680
|
+
</div>
|
|
681
|
+
<ListPagination model={model} />
|
|
682
|
+
|
|
683
|
+
{/* AICODE-NOTE: Context menu */}
|
|
684
|
+
<ListContextMenu
|
|
685
|
+
isOpen={contextMenu.isOpen}
|
|
686
|
+
position={contextMenu.position}
|
|
687
|
+
items={contextMenu.items}
|
|
688
|
+
provider={model.provider}
|
|
689
|
+
onClose={handleCloseContextMenu}
|
|
690
|
+
/>
|
|
691
|
+
</>
|
|
692
|
+
);
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
// AICODE-NOTE: Default list view — use virtualized list if enabled and height provided
|
|
696
|
+
if (enableVirtualization && effectiveHeight && model.provider.isVirtualizationEnabled) {
|
|
697
|
+
benchmark.end('ListItems.render', { renderType: 'list-virtualized' });
|
|
698
|
+
return (
|
|
699
|
+
<>
|
|
700
|
+
<div ref={containerRef} tabIndex={0} className={`${sizeClass} ${className}`} style={sizeStyle}>
|
|
701
|
+
<VirtualizedList
|
|
702
|
+
model={model}
|
|
703
|
+
height={effectiveHeight}
|
|
704
|
+
width={effectiveWidth}
|
|
705
|
+
/>
|
|
706
|
+
</div>
|
|
707
|
+
<ListPagination model={model} />
|
|
708
|
+
</>
|
|
709
|
+
);
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
benchmark.end('ListItems.render', { renderType: 'list-standard' });
|
|
713
|
+
return (
|
|
714
|
+
<>
|
|
715
|
+
<div
|
|
716
|
+
ref={containerRef}
|
|
717
|
+
className={`overflow-auto ${sizeClass} ${className}`}
|
|
718
|
+
style={sizeStyle}
|
|
719
|
+
role={accessibilityProps.role}
|
|
720
|
+
aria-label={accessibilityProps.ariaLabel}
|
|
721
|
+
aria-multiselectable={accessibilityProps.ariaMultiSelectable}
|
|
722
|
+
tabIndex={accessibilityProps.tabIndex}
|
|
723
|
+
onContextMenu={handleContainerContextMenu}
|
|
724
|
+
>
|
|
725
|
+
<div className="divide-y">
|
|
726
|
+
{benchmark.time('list-items-render', () => {
|
|
727
|
+
return model.items.map((item, index) => (
|
|
728
|
+
<ListItem
|
|
729
|
+
key={item.id}
|
|
730
|
+
item={item}
|
|
731
|
+
index={index}
|
|
732
|
+
totalItems={model.totalItemCount}
|
|
733
|
+
viewType={model.currentViewType}
|
|
734
|
+
provider={model.provider}
|
|
735
|
+
model={model}
|
|
736
|
+
isSelected={model.isItemSelected(item.id)}
|
|
737
|
+
isFocused={model.focusedItem === item.id}
|
|
738
|
+
isDraggedOver={model.dragOverItem === item.id}
|
|
739
|
+
dragOverPosition={model.dragOverItem === item.id ? model.dragOverPosition : null}
|
|
740
|
+
canDrag={model.canDragItem(item)}
|
|
741
|
+
onClick={handleItemClick}
|
|
742
|
+
onDoubleClick={handleItemDoubleClick}
|
|
743
|
+
onContextMenu={handleItemContextMenu}
|
|
744
|
+
onDragStart={handleItemDragStart}
|
|
745
|
+
onDragOver={handleItemDragOver}
|
|
746
|
+
onDragLeave={handleItemDragLeave}
|
|
747
|
+
onDrop={handleItemDrop}
|
|
748
|
+
/>
|
|
749
|
+
));
|
|
750
|
+
})}
|
|
751
|
+
</div>
|
|
752
|
+
</div>
|
|
753
|
+
<ListPagination model={model} />
|
|
754
|
+
|
|
755
|
+
{/* AICODE-NOTE: Context menu */}
|
|
756
|
+
<ListContextMenu
|
|
757
|
+
isOpen={contextMenu.isOpen}
|
|
758
|
+
position={contextMenu.position}
|
|
759
|
+
items={contextMenu.items}
|
|
760
|
+
provider={model.provider}
|
|
761
|
+
onClose={handleCloseContextMenu}
|
|
762
|
+
/>
|
|
763
|
+
</>
|
|
764
|
+
);
|
|
765
|
+
});
|
|
766
|
+
|
|
767
|
+
ListItems.displayName = 'ListItems';
|
|
768
|
+
|
|
769
|
+
/** Treemap section — owns the TreemapModel and wires context menu */
|
|
770
|
+
const TreemapSection = observer<{
|
|
771
|
+
model: ListItemsModel;
|
|
772
|
+
containerRef: React.RefObject<HTMLDivElement | null>;
|
|
773
|
+
sizeClass: string;
|
|
774
|
+
className: string;
|
|
775
|
+
sizeStyle: React.CSSProperties | undefined;
|
|
776
|
+
effectiveWidth: number;
|
|
777
|
+
effectiveHeight: number;
|
|
778
|
+
contextMenu: { isOpen: boolean; position: { x: number; y: number }; items: ListItemData[] };
|
|
779
|
+
setContextMenu: React.Dispatch<React.SetStateAction<{ isOpen: boolean; position: { x: number; y: number }; items: ListItemData[] }>>;
|
|
780
|
+
handleCloseContextMenu: () => void;
|
|
781
|
+
handleContainerContextMenu: (event: React.MouseEvent) => void;
|
|
782
|
+
}>(({
|
|
783
|
+
model,
|
|
784
|
+
containerRef,
|
|
785
|
+
sizeClass,
|
|
786
|
+
className,
|
|
787
|
+
sizeStyle,
|
|
788
|
+
effectiveWidth,
|
|
789
|
+
effectiveHeight,
|
|
790
|
+
contextMenu,
|
|
791
|
+
setContextMenu,
|
|
792
|
+
handleCloseContextMenu,
|
|
793
|
+
handleContainerContextMenu,
|
|
794
|
+
}) => {
|
|
795
|
+
const treemapModelRef = useRef<TreemapModel | null>(null);
|
|
796
|
+
if (!treemapModelRef.current) {
|
|
797
|
+
treemapModelRef.current = new TreemapModel();
|
|
798
|
+
}
|
|
799
|
+
const treemapModel = treemapModelRef.current;
|
|
800
|
+
|
|
801
|
+
// Track the context-menu'd item for treemap-specific actions
|
|
802
|
+
const contextItemRef = useRef<ListItemData | null>(null);
|
|
803
|
+
|
|
804
|
+
const handleTileContextMenu = (info: TileInfo, event: React.MouseEvent) => {
|
|
805
|
+
// Build a fake ListItemData from TileInfo
|
|
806
|
+
const fakeItem: ListItemData = {
|
|
807
|
+
id: info.path,
|
|
808
|
+
name: info.name,
|
|
809
|
+
path: info.path,
|
|
810
|
+
type: info.isDirectory ? 'directory' : 'file',
|
|
811
|
+
isDirectory: info.isDirectory,
|
|
812
|
+
size: info.size,
|
|
813
|
+
};
|
|
814
|
+
|
|
815
|
+
// Select item in model
|
|
816
|
+
if (!model.isItemSelected(fakeItem.id)) {
|
|
817
|
+
model.selectItem(fakeItem);
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
contextItemRef.current = fakeItem;
|
|
821
|
+
|
|
822
|
+
const contextItems = model.hasSelection ? model.selectedItemsArray : [fakeItem];
|
|
823
|
+
setContextMenu({
|
|
824
|
+
isOpen: true,
|
|
825
|
+
position: { x: event.clientX, y: event.clientY },
|
|
826
|
+
items: contextItems,
|
|
827
|
+
});
|
|
828
|
+
};
|
|
829
|
+
|
|
830
|
+
const handleEmptyContextMenu = (event: React.MouseEvent) => {
|
|
831
|
+
contextItemRef.current = null;
|
|
832
|
+
handleContainerContextMenu(event);
|
|
833
|
+
};
|
|
834
|
+
|
|
835
|
+
// Build treemap-specific extra menu items
|
|
836
|
+
const extraMenuItems = useMemo((): ListContextMenuItem[] => {
|
|
837
|
+
const item = contextItemRef.current;
|
|
838
|
+
if (!item) return [];
|
|
839
|
+
|
|
840
|
+
const items: ListContextMenuItem[] = [];
|
|
841
|
+
|
|
842
|
+
if (item.isDirectory) {
|
|
843
|
+
items.push({
|
|
844
|
+
id: 'zoom-into',
|
|
845
|
+
label: 'Zoom Into',
|
|
846
|
+
icon: 'zoom-in',
|
|
847
|
+
type: 'item',
|
|
848
|
+
});
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
items.push({
|
|
852
|
+
id: 'show-in-list',
|
|
853
|
+
label: 'Show in File Browser',
|
|
854
|
+
icon: 'list',
|
|
855
|
+
type: 'item',
|
|
856
|
+
});
|
|
857
|
+
|
|
858
|
+
items.push({
|
|
859
|
+
id: 'sep-treemap',
|
|
860
|
+
label: '',
|
|
861
|
+
type: 'separator',
|
|
862
|
+
});
|
|
863
|
+
|
|
864
|
+
return items;
|
|
865
|
+
// Re-derive when context menu opens (items change)
|
|
866
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
867
|
+
}, [contextMenu.isOpen, contextMenu.items]);
|
|
868
|
+
|
|
869
|
+
const handleExtraMenuAction = (actionId: string, items: ListItemData[]) => {
|
|
870
|
+
const item = items[0];
|
|
871
|
+
if (!item) return;
|
|
872
|
+
|
|
873
|
+
if (actionId === 'zoom-into') {
|
|
874
|
+
treemapModel.zoomIn(item.path);
|
|
875
|
+
} else if (actionId === 'show-in-list') {
|
|
876
|
+
// Switch to list view
|
|
877
|
+
model.setViewType(LIST_VIEW_TYPE);
|
|
878
|
+
// Navigate to parent directory
|
|
879
|
+
if (model.provider.onItemDoubleClick) {
|
|
880
|
+
const parentPath = item.path.split('/').slice(0, -1).join('/');
|
|
881
|
+
const parentName = parentPath.split('/').pop() || '';
|
|
882
|
+
const parentItem: ListItemData = {
|
|
883
|
+
id: parentPath,
|
|
884
|
+
name: parentName,
|
|
885
|
+
path: parentPath,
|
|
886
|
+
type: 'directory',
|
|
887
|
+
isDirectory: true,
|
|
888
|
+
};
|
|
889
|
+
model.provider.onItemDoubleClick(parentItem);
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
};
|
|
893
|
+
|
|
894
|
+
return (
|
|
895
|
+
<>
|
|
896
|
+
<div ref={containerRef} tabIndex={0} className={`${sizeClass} ${className}`} style={sizeStyle}>
|
|
897
|
+
<TreemapView
|
|
898
|
+
model={model}
|
|
899
|
+
treemapModel={treemapModel}
|
|
900
|
+
width={effectiveWidth}
|
|
901
|
+
height={effectiveHeight}
|
|
902
|
+
onTileContextMenu={handleTileContextMenu}
|
|
903
|
+
onEmptyContextMenu={handleEmptyContextMenu}
|
|
904
|
+
/>
|
|
905
|
+
</div>
|
|
906
|
+
<ListContextMenu
|
|
907
|
+
isOpen={contextMenu.isOpen}
|
|
908
|
+
position={contextMenu.position}
|
|
909
|
+
items={contextMenu.items}
|
|
910
|
+
provider={model.provider}
|
|
911
|
+
onClose={handleCloseContextMenu}
|
|
912
|
+
extraMenuItems={extraMenuItems}
|
|
913
|
+
onExtraMenuAction={handleExtraMenuAction}
|
|
914
|
+
/>
|
|
915
|
+
</>
|
|
916
|
+
);
|
|
917
|
+
});
|
|
918
|
+
|
|
919
|
+
TreemapSection.displayName = 'TreemapSection';
|