@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,323 @@
|
|
|
1
|
+
import React, { useRef, useEffect, useState } from 'react';
|
|
2
|
+
import { observer } from 'mobx-react-lite';
|
|
3
|
+
import { cn } from '../../../../lib/utils';
|
|
4
|
+
import { FileBrowserItem } from '../../../types/FileBrowserTypes';
|
|
5
|
+
import { ThumbnailViewUIModel } from '../../../models/ui/ThumbnailViewUIModel';
|
|
6
|
+
import { ResponsiveLayoutManagerModel } from '../../../models/ResponsiveLayoutManagerModel';
|
|
7
|
+
import FileBrowserItemComponent from '../../shared/FileBrowserItem';
|
|
8
|
+
import { LoadingSpinner } from '@anymux/ui/components/loading-spinner';
|
|
9
|
+
import { EmptyState } from '@anymux/ui/components/empty-state';
|
|
10
|
+
|
|
11
|
+
export interface ThumbnailViewProps {
|
|
12
|
+
items: FileBrowserItem[];
|
|
13
|
+
thumbnailModel: ThumbnailViewUIModel;
|
|
14
|
+
responsiveManager?: ResponsiveLayoutManagerModel;
|
|
15
|
+
onItemClick?: (item: FileBrowserItem) => void;
|
|
16
|
+
onItemDoubleClick?: (item: FileBrowserItem) => void;
|
|
17
|
+
onItemActivate?: (item: FileBrowserItem) => void;
|
|
18
|
+
onSelectionChange?: (selectedItems: FileBrowserItem[]) => void;
|
|
19
|
+
selectedItemIds?: Set<string>;
|
|
20
|
+
focusedItemId?: string;
|
|
21
|
+
className?: string;
|
|
22
|
+
getThumbnail?: (item: FileBrowserItem) => Promise<string | null>;
|
|
23
|
+
virtualization?: boolean;
|
|
24
|
+
maxHeight?: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const ThumbnailView: React.FC<ThumbnailViewProps> = observer(({
|
|
28
|
+
items,
|
|
29
|
+
thumbnailModel,
|
|
30
|
+
responsiveManager,
|
|
31
|
+
onItemClick,
|
|
32
|
+
onItemDoubleClick,
|
|
33
|
+
onItemActivate,
|
|
34
|
+
onSelectionChange,
|
|
35
|
+
selectedItemIds = new Set<string>(),
|
|
36
|
+
focusedItemId,
|
|
37
|
+
className,
|
|
38
|
+
getThumbnail,
|
|
39
|
+
virtualization = false,
|
|
40
|
+
maxHeight,
|
|
41
|
+
}) => {
|
|
42
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
43
|
+
const [containerWidth, setContainerWidth] = useState(0);
|
|
44
|
+
const [thumbnailUrls, setThumbnailUrls] = useState<Map<string, string>>(new Map());
|
|
45
|
+
|
|
46
|
+
const isMobile = responsiveManager?.isMobile ?? false;
|
|
47
|
+
const isTablet = responsiveManager?.isTablet ?? false;
|
|
48
|
+
|
|
49
|
+
// Calculate responsive grid layout
|
|
50
|
+
const gridLayout = React.useMemo(() => {
|
|
51
|
+
if (containerWidth === 0) return { columns: 1, itemsPerRow: 1 };
|
|
52
|
+
|
|
53
|
+
// Mobile-specific layout adjustments
|
|
54
|
+
if (isMobile) {
|
|
55
|
+
// Force 2-3 columns on mobile for better touch targets
|
|
56
|
+
const mobileColumns = Math.min(3, Math.max(2, Math.floor(containerWidth / 120)));
|
|
57
|
+
return { columns: mobileColumns, itemsPerRow: mobileColumns };
|
|
58
|
+
} else if (isTablet) {
|
|
59
|
+
// Tablet gets 3-4 columns
|
|
60
|
+
const tabletColumns = Math.min(4, Math.max(3, Math.floor(containerWidth / 150)));
|
|
61
|
+
return { columns: tabletColumns, itemsPerRow: tabletColumns };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Desktop uses model calculation
|
|
65
|
+
return thumbnailModel.calculateGridLayout(containerWidth);
|
|
66
|
+
}, [containerWidth, thumbnailModel.itemWidth, isMobile, isTablet]);
|
|
67
|
+
|
|
68
|
+
// Mobile-optimized thumbnail size
|
|
69
|
+
const effectiveThumbnailSize = React.useMemo(() => {
|
|
70
|
+
if (isMobile) {
|
|
71
|
+
// Larger thumbnails on mobile for better touch targets
|
|
72
|
+
return Math.max(80, Math.min(120, (containerWidth - (gridLayout.columns + 1) * 8) / gridLayout.columns));
|
|
73
|
+
} else if (isTablet) {
|
|
74
|
+
return Math.max(100, Math.min(140, (containerWidth - (gridLayout.columns + 1) * 12) / gridLayout.columns));
|
|
75
|
+
}
|
|
76
|
+
return thumbnailModel.thumbnailSize;
|
|
77
|
+
}, [isMobile, isTablet, containerWidth, gridLayout.columns, thumbnailModel.thumbnailSize]);
|
|
78
|
+
|
|
79
|
+
// Handle container resize
|
|
80
|
+
useEffect(() => {
|
|
81
|
+
const updateContainerWidth = () => {
|
|
82
|
+
if (containerRef.current) {
|
|
83
|
+
setContainerWidth(containerRef.current.clientWidth);
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
updateContainerWidth();
|
|
88
|
+
|
|
89
|
+
const resizeObserver = new ResizeObserver(updateContainerWidth);
|
|
90
|
+
if (containerRef.current) {
|
|
91
|
+
resizeObserver.observe(containerRef.current);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return () => {
|
|
95
|
+
resizeObserver.disconnect();
|
|
96
|
+
};
|
|
97
|
+
}, []);
|
|
98
|
+
|
|
99
|
+
// Load thumbnails for visible items
|
|
100
|
+
useEffect(() => {
|
|
101
|
+
if (!getThumbnail || !thumbnailModel.loadThumbnails) return;
|
|
102
|
+
|
|
103
|
+
const loadThumbnailsForItems = async () => {
|
|
104
|
+
const imageItems = items.filter(item =>
|
|
105
|
+
item.type === 'file' &&
|
|
106
|
+
isImageFile(item.name) &&
|
|
107
|
+
!thumbnailUrls.has(item.id) &&
|
|
108
|
+
!thumbnailModel.isThumbnailLoading(item.id) &&
|
|
109
|
+
!thumbnailModel.isThumbnailFailed(item.id)
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
for (const item of imageItems) {
|
|
113
|
+
try {
|
|
114
|
+
thumbnailModel.setThumbnailLoading(item.id, true);
|
|
115
|
+
const thumbnailUrl = await getThumbnail(item);
|
|
116
|
+
|
|
117
|
+
if (thumbnailUrl) {
|
|
118
|
+
setThumbnailUrls(prev => new Map(prev).set(item.id, thumbnailUrl));
|
|
119
|
+
thumbnailModel.setThumbnailSuccess(item.id);
|
|
120
|
+
} else {
|
|
121
|
+
thumbnailModel.setThumbnailFailed(item.id);
|
|
122
|
+
}
|
|
123
|
+
} catch (error) {
|
|
124
|
+
thumbnailModel.logger?.info(`Failed to load thumbnail for ${item.name}: ${error}`);
|
|
125
|
+
thumbnailModel.setThumbnailFailed(item.id);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
loadThumbnailsForItems();
|
|
131
|
+
}, [items, getThumbnail, thumbnailModel.loadThumbnails]);
|
|
132
|
+
|
|
133
|
+
const isImageFile = (filename: string): boolean => {
|
|
134
|
+
const imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp', '.svg'];
|
|
135
|
+
const ext = filename.toLowerCase().substring(filename.lastIndexOf('.'));
|
|
136
|
+
return imageExtensions.includes(ext);
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
const handleItemClick = (item: FileBrowserItem, event: React.MouseEvent) => {
|
|
140
|
+
onItemClick?.(item);
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const handleItemDoubleClick = (item: FileBrowserItem, event: React.MouseEvent) => {
|
|
144
|
+
onItemDoubleClick?.(item);
|
|
145
|
+
onItemActivate?.(item);
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
const renderThumbnailItem = (item: FileBrowserItem, index: number) => {
|
|
149
|
+
const isSelected = selectedItemIds.has(item.id);
|
|
150
|
+
const isFocused = focusedItemId === item.id;
|
|
151
|
+
const thumbnailUrl = thumbnailUrls.get(item.id);
|
|
152
|
+
const isLoadingThumbnail = thumbnailModel.isThumbnailLoading(item.id);
|
|
153
|
+
const thumbnailFailed = thumbnailModel.isThumbnailFailed(item.id);
|
|
154
|
+
|
|
155
|
+
return (
|
|
156
|
+
<div
|
|
157
|
+
key={item.id}
|
|
158
|
+
className={cn(
|
|
159
|
+
'relative group',
|
|
160
|
+
// Mobile-friendly touch interactions
|
|
161
|
+
isMobile ? 'active:scale-95 transition-transform' : 'transition-transform hover:scale-105',
|
|
162
|
+
isSelected && 'ring-2 ring-primary ring-offset-2',
|
|
163
|
+
// Touch-friendly minimum size
|
|
164
|
+
isMobile && 'min-w-[80px] min-h-[80px]'
|
|
165
|
+
)}
|
|
166
|
+
style={{
|
|
167
|
+
width: effectiveThumbnailSize,
|
|
168
|
+
height: effectiveThumbnailSize + (thumbnailModel.showFilenames ? 40 : 0),
|
|
169
|
+
}}
|
|
170
|
+
>
|
|
171
|
+
{/* Thumbnail container */}
|
|
172
|
+
<div
|
|
173
|
+
className={cn(
|
|
174
|
+
'relative overflow-hidden rounded-lg border border-border bg-muted/30',
|
|
175
|
+
'flex items-center justify-center',
|
|
176
|
+
isSelected && 'border-primary'
|
|
177
|
+
)}
|
|
178
|
+
style={{
|
|
179
|
+
width: effectiveThumbnailSize,
|
|
180
|
+
height: effectiveThumbnailSize / (thumbnailModel.aspectRatio || 1),
|
|
181
|
+
}}
|
|
182
|
+
>
|
|
183
|
+
{/* Thumbnail image */}
|
|
184
|
+
{thumbnailUrl && !thumbnailFailed && (
|
|
185
|
+
<img
|
|
186
|
+
src={thumbnailUrl}
|
|
187
|
+
alt={item.name}
|
|
188
|
+
className="w-full h-full object-cover"
|
|
189
|
+
loading="lazy"
|
|
190
|
+
onError={() => thumbnailModel.setThumbnailFailed(item.id)}
|
|
191
|
+
/>
|
|
192
|
+
)}
|
|
193
|
+
|
|
194
|
+
{/* Loading state */}
|
|
195
|
+
{isLoadingThumbnail && (
|
|
196
|
+
<div className="absolute inset-0 flex items-center justify-center bg-muted/50">
|
|
197
|
+
<LoadingSpinner size="sm" />
|
|
198
|
+
</div>
|
|
199
|
+
)}
|
|
200
|
+
|
|
201
|
+
{/* File icon fallback */}
|
|
202
|
+
{!thumbnailUrl && !isLoadingThumbnail && (
|
|
203
|
+
<FileBrowserItemComponent
|
|
204
|
+
item={item}
|
|
205
|
+
showIcon={true}
|
|
206
|
+
showDetails={false}
|
|
207
|
+
iconSize="lg"
|
|
208
|
+
layout="vertical"
|
|
209
|
+
className="border-0 bg-transparent hover:bg-transparent p-0"
|
|
210
|
+
onClick={handleItemClick}
|
|
211
|
+
onDoubleClick={handleItemDoubleClick}
|
|
212
|
+
/>
|
|
213
|
+
)}
|
|
214
|
+
|
|
215
|
+
{/* Selection overlay */}
|
|
216
|
+
{isSelected && (
|
|
217
|
+
<div className="absolute inset-0 bg-primary/20 border-2 border-primary rounded-lg" />
|
|
218
|
+
)}
|
|
219
|
+
|
|
220
|
+
{/* Focus indicator */}
|
|
221
|
+
{isFocused && (
|
|
222
|
+
<div className="absolute inset-0 ring-2 ring-offset-2 ring-primary rounded-lg" />
|
|
223
|
+
)}
|
|
224
|
+
</div>
|
|
225
|
+
|
|
226
|
+
{/* File name */}
|
|
227
|
+
{thumbnailModel.showFilenames && (
|
|
228
|
+
<div className="mt-2 text-center">
|
|
229
|
+
<div className="text-xs font-medium text-foreground truncate px-1">
|
|
230
|
+
{item.name}
|
|
231
|
+
</div>
|
|
232
|
+
</div>
|
|
233
|
+
)}
|
|
234
|
+
|
|
235
|
+
{/* File details */}
|
|
236
|
+
{(thumbnailModel.showFileSize || thumbnailModel.showDate) && (
|
|
237
|
+
<div className="mt-1 text-center">
|
|
238
|
+
{thumbnailModel.showFileSize && item.size && (
|
|
239
|
+
<div className="text-xs text-muted-foreground">
|
|
240
|
+
{formatFileSize(item.size)}
|
|
241
|
+
</div>
|
|
242
|
+
)}
|
|
243
|
+
{thumbnailModel.showDate && item.lastModified && (
|
|
244
|
+
<div className="text-xs text-muted-foreground">
|
|
245
|
+
{formatDate(item.lastModified)}
|
|
246
|
+
</div>
|
|
247
|
+
)}
|
|
248
|
+
</div>
|
|
249
|
+
)}
|
|
250
|
+
|
|
251
|
+
{/* Click handler overlay */}
|
|
252
|
+
<div
|
|
253
|
+
className="absolute inset-0 cursor-pointer"
|
|
254
|
+
onClick={(e) => handleItemClick(item, e)}
|
|
255
|
+
onDoubleClick={(e) => handleItemDoubleClick(item, e)}
|
|
256
|
+
tabIndex={0}
|
|
257
|
+
role="button"
|
|
258
|
+
aria-label={`${item.type === 'directory' ? 'Folder' : 'File'}: ${item.name}`}
|
|
259
|
+
aria-selected={isSelected}
|
|
260
|
+
/>
|
|
261
|
+
</div>
|
|
262
|
+
);
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
const formatFileSize = (bytes: number): string => {
|
|
266
|
+
const units = ['B', 'KB', 'MB', 'GB'];
|
|
267
|
+
let size = bytes;
|
|
268
|
+
let unitIndex = 0;
|
|
269
|
+
|
|
270
|
+
while (size >= 1024 && unitIndex < units.length - 1) {
|
|
271
|
+
size /= 1024;
|
|
272
|
+
unitIndex++;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return `${size.toFixed(unitIndex === 0 ? 0 : 1)} ${units[unitIndex]}`;
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
const formatDate = (date: Date): string => {
|
|
279
|
+
return date.toLocaleDateString(undefined, {
|
|
280
|
+
month: 'short',
|
|
281
|
+
day: 'numeric',
|
|
282
|
+
});
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
if (!items || items.length === 0) {
|
|
286
|
+
return (
|
|
287
|
+
<div className={cn('h-full', className)}>
|
|
288
|
+
<EmptyState
|
|
289
|
+
preset="empty-folder"
|
|
290
|
+
className="h-full"
|
|
291
|
+
/>
|
|
292
|
+
</div>
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Mobile-optimized grid spacing
|
|
297
|
+
const effectiveGridSpacing = isMobile ? 8 : (isTablet ? 12 : thumbnailModel.gridSpacing);
|
|
298
|
+
|
|
299
|
+
const gridStyle: React.CSSProperties = {
|
|
300
|
+
display: 'grid',
|
|
301
|
+
gridTemplateColumns: `repeat(${gridLayout.columns}, 1fr)`,
|
|
302
|
+
gap: effectiveGridSpacing,
|
|
303
|
+
padding: effectiveGridSpacing,
|
|
304
|
+
...(maxHeight && { maxHeight, overflowY: 'auto' }),
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
return (
|
|
308
|
+
<div
|
|
309
|
+
ref={containerRef}
|
|
310
|
+
className={cn('thumbnail-view h-full', className)}
|
|
311
|
+
role="grid"
|
|
312
|
+
aria-label="Thumbnail view"
|
|
313
|
+
>
|
|
314
|
+
<div style={gridStyle}>
|
|
315
|
+
{items.map((item, index) => renderThumbnailItem(item, index))}
|
|
316
|
+
</div>
|
|
317
|
+
</div>
|
|
318
|
+
);
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
ThumbnailView.displayName = 'ThumbnailView';
|
|
322
|
+
|
|
323
|
+
export default ThumbnailView;
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { observer } from 'mobx-react-lite';
|
|
3
|
+
import { ChevronRight, ChevronDown } from 'lucide-react';
|
|
4
|
+
import { cn } from '../../../../lib/utils';
|
|
5
|
+
import { FileBrowserItem } from '../../../types/FileBrowserTypes';
|
|
6
|
+
import { TreeViewUIModel } from '../../../models/ui/TreeViewUIModel';
|
|
7
|
+
import FileIcon from '../../shared/FileIcon';
|
|
8
|
+
|
|
9
|
+
export interface TreeNodeProps {
|
|
10
|
+
item: FileBrowserItem;
|
|
11
|
+
treeModel: TreeViewUIModel;
|
|
12
|
+
level: number;
|
|
13
|
+
onItemClick?: (item: FileBrowserItem) => void;
|
|
14
|
+
onItemDoubleClick?: (item: FileBrowserItem) => void;
|
|
15
|
+
isSelected?: boolean;
|
|
16
|
+
isFocused?: boolean;
|
|
17
|
+
getItemIcon?: (item: FileBrowserItem) => any;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const TreeNode: React.FC<TreeNodeProps> = observer(({
|
|
21
|
+
item,
|
|
22
|
+
treeModel,
|
|
23
|
+
level,
|
|
24
|
+
onItemClick,
|
|
25
|
+
onItemDoubleClick,
|
|
26
|
+
isSelected = false,
|
|
27
|
+
isFocused = false,
|
|
28
|
+
getItemIcon,
|
|
29
|
+
}) => {
|
|
30
|
+
const isDirectory = item.type === 'directory';
|
|
31
|
+
const hasChildren = isDirectory && treeModel.hasChildren(item);
|
|
32
|
+
const isExpanded = isDirectory && treeModel.isFolderExpanded(item.path);
|
|
33
|
+
const indentSize = treeModel.indentSize;
|
|
34
|
+
|
|
35
|
+
const handleExpanderClick = (e: React.MouseEvent) => {
|
|
36
|
+
e.stopPropagation();
|
|
37
|
+
if (hasChildren) {
|
|
38
|
+
treeModel.toggleFolder(item.path);
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const handleItemClick = (e: React.MouseEvent) => {
|
|
43
|
+
e.preventDefault();
|
|
44
|
+
|
|
45
|
+
// Handle expand on single click if enabled
|
|
46
|
+
if (isDirectory && treeModel.expandOnSingleClick) {
|
|
47
|
+
treeModel.toggleFolder(item.path);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
onItemClick?.(item);
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const handleItemDoubleClick = (e: React.MouseEvent) => {
|
|
54
|
+
e.preventDefault();
|
|
55
|
+
|
|
56
|
+
// Handle collapse on second click if enabled
|
|
57
|
+
if (isDirectory && treeModel.collapseOnSecondClick && isExpanded) {
|
|
58
|
+
treeModel.collapseFolder(item.path);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
onItemDoubleClick?.(item);
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const handleKeyDown = (e: React.KeyboardEvent) => {
|
|
65
|
+
switch (e.key) {
|
|
66
|
+
case 'Enter':
|
|
67
|
+
case ' ':
|
|
68
|
+
e.preventDefault();
|
|
69
|
+
handleItemClick(e as any);
|
|
70
|
+
break;
|
|
71
|
+
case 'ArrowRight':
|
|
72
|
+
if (isDirectory && !isExpanded && hasChildren) {
|
|
73
|
+
e.preventDefault();
|
|
74
|
+
treeModel.expandFolder(item.path);
|
|
75
|
+
}
|
|
76
|
+
break;
|
|
77
|
+
case 'ArrowLeft':
|
|
78
|
+
if (isDirectory && isExpanded) {
|
|
79
|
+
e.preventDefault();
|
|
80
|
+
treeModel.collapseFolder(item.path);
|
|
81
|
+
}
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
<div
|
|
88
|
+
className={cn(
|
|
89
|
+
'flex items-center group relative select-none',
|
|
90
|
+
'hover:bg-muted/50 transition-colors cursor-pointer',
|
|
91
|
+
isSelected && 'bg-primary/10 text-primary',
|
|
92
|
+
isFocused && 'ring-2 ring-primary ring-inset'
|
|
93
|
+
)}
|
|
94
|
+
style={{ paddingLeft: `${level * indentSize}px` }}
|
|
95
|
+
onClick={handleItemClick}
|
|
96
|
+
onDoubleClick={handleItemDoubleClick}
|
|
97
|
+
onKeyDown={handleKeyDown}
|
|
98
|
+
tabIndex={0}
|
|
99
|
+
role="treeitem"
|
|
100
|
+
aria-expanded={isDirectory ? isExpanded : undefined}
|
|
101
|
+
aria-level={level + 1}
|
|
102
|
+
aria-selected={isSelected}
|
|
103
|
+
aria-label={`${item.type === 'directory' ? 'Folder' : 'File'}: ${item.name}`}
|
|
104
|
+
>
|
|
105
|
+
{/* Tree connectors */}
|
|
106
|
+
{treeModel.showConnectors && level > 0 && (
|
|
107
|
+
<div className="absolute left-0 top-0 h-full">
|
|
108
|
+
{/* Vertical line for parent levels */}
|
|
109
|
+
<div
|
|
110
|
+
className="absolute bg-border"
|
|
111
|
+
style={{
|
|
112
|
+
left: `${(level - 1) * indentSize + indentSize / 2 - 0.5}px`,
|
|
113
|
+
top: 0,
|
|
114
|
+
width: '1px',
|
|
115
|
+
height: '100%',
|
|
116
|
+
}}
|
|
117
|
+
/>
|
|
118
|
+
{/* Horizontal line to item */}
|
|
119
|
+
<div
|
|
120
|
+
className="absolute bg-border"
|
|
121
|
+
style={{
|
|
122
|
+
left: `${(level - 1) * indentSize + indentSize / 2}px`,
|
|
123
|
+
top: '50%',
|
|
124
|
+
width: `${indentSize / 2}px`,
|
|
125
|
+
height: '1px',
|
|
126
|
+
}}
|
|
127
|
+
/>
|
|
128
|
+
</div>
|
|
129
|
+
)}
|
|
130
|
+
|
|
131
|
+
{/* Expander icon */}
|
|
132
|
+
<div className="flex items-center justify-center w-5 h-5 mr-1">
|
|
133
|
+
{hasChildren && (
|
|
134
|
+
<button
|
|
135
|
+
onClick={handleExpanderClick}
|
|
136
|
+
className={cn(
|
|
137
|
+
'flex items-center justify-center w-4 h-4 rounded-sm',
|
|
138
|
+
'hover:bg-muted transition-colors',
|
|
139
|
+
'focus:outline-none focus:ring-1 focus:ring-primary'
|
|
140
|
+
)}
|
|
141
|
+
tabIndex={-1}
|
|
142
|
+
aria-label={isExpanded ? 'Collapse folder' : 'Expand folder'}
|
|
143
|
+
>
|
|
144
|
+
{isExpanded ? (
|
|
145
|
+
<ChevronDown className="w-3 h-3" />
|
|
146
|
+
) : (
|
|
147
|
+
<ChevronRight className="w-3 h-3" />
|
|
148
|
+
)}
|
|
149
|
+
</button>
|
|
150
|
+
)}
|
|
151
|
+
</div>
|
|
152
|
+
|
|
153
|
+
{/* File/folder icon */}
|
|
154
|
+
<div className="flex items-center justify-center w-5 h-5 mr-2">
|
|
155
|
+
<FileIcon
|
|
156
|
+
item={item}
|
|
157
|
+
getItemIcon={getItemIcon}
|
|
158
|
+
size="sm"
|
|
159
|
+
isExpanded={isExpanded}
|
|
160
|
+
/>
|
|
161
|
+
</div>
|
|
162
|
+
|
|
163
|
+
{/* Item name */}
|
|
164
|
+
<span
|
|
165
|
+
className={cn(
|
|
166
|
+
'flex-1 text-sm truncate',
|
|
167
|
+
item.isLoading && 'opacity-50'
|
|
168
|
+
)}
|
|
169
|
+
title={item.name}
|
|
170
|
+
>
|
|
171
|
+
{item.name}
|
|
172
|
+
</span>
|
|
173
|
+
|
|
174
|
+
{/* Loading indicator */}
|
|
175
|
+
{item.isLoading && (
|
|
176
|
+
<div className="flex items-center justify-center w-4 h-4 ml-2">
|
|
177
|
+
<div className="w-3 h-3 border border-primary border-t-transparent rounded-full animate-spin" />
|
|
178
|
+
</div>
|
|
179
|
+
)}
|
|
180
|
+
</div>
|
|
181
|
+
);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
TreeNode.displayName = 'TreeNode';
|
|
185
|
+
|
|
186
|
+
export default TreeNode;
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import React, { useCallback, useMemo } from 'react';
|
|
2
|
+
import { observer } from 'mobx-react-lite';
|
|
3
|
+
import { cn } from '../../../../lib/utils';
|
|
4
|
+
import { FileBrowserItem } from '../../../types/FileBrowserTypes';
|
|
5
|
+
import { TreeViewUIModel } from '../../../models/ui/TreeViewUIModel';
|
|
6
|
+
import TreeNode from './TreeNode';
|
|
7
|
+
|
|
8
|
+
export interface TreeNodeListProps {
|
|
9
|
+
items: FileBrowserItem[];
|
|
10
|
+
treeModel: TreeViewUIModel;
|
|
11
|
+
onItemClick?: (item: FileBrowserItem) => void;
|
|
12
|
+
onItemDoubleClick?: (item: FileBrowserItem) => void;
|
|
13
|
+
onSelectionChange?: (selectedItems: FileBrowserItem[]) => void;
|
|
14
|
+
selectedItemIds?: Set<string>;
|
|
15
|
+
focusedItemId?: string;
|
|
16
|
+
className?: string;
|
|
17
|
+
getItemIcon?: (item: FileBrowserItem) => any;
|
|
18
|
+
virtualization?: boolean;
|
|
19
|
+
maxHeight?: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface TreeNodeFlat {
|
|
23
|
+
item: FileBrowserItem;
|
|
24
|
+
level: number;
|
|
25
|
+
key: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const TreeNodeList: React.FC<TreeNodeListProps> = observer(({
|
|
29
|
+
items,
|
|
30
|
+
treeModel,
|
|
31
|
+
onItemClick,
|
|
32
|
+
onItemDoubleClick,
|
|
33
|
+
onSelectionChange,
|
|
34
|
+
selectedItemIds = new Set(),
|
|
35
|
+
focusedItemId,
|
|
36
|
+
className,
|
|
37
|
+
getItemIcon,
|
|
38
|
+
virtualization = false,
|
|
39
|
+
maxHeight = 400,
|
|
40
|
+
}) => {
|
|
41
|
+
// Flatten tree structure for rendering
|
|
42
|
+
const flattenedNodes = useMemo(() => {
|
|
43
|
+
const result: TreeNodeFlat[] = [];
|
|
44
|
+
|
|
45
|
+
const processItems = (items: FileBrowserItem[], level = 0) => {
|
|
46
|
+
const sortedItems = treeModel.sortTreeNodes(items);
|
|
47
|
+
|
|
48
|
+
for (const item of sortedItems) {
|
|
49
|
+
result.push({
|
|
50
|
+
item,
|
|
51
|
+
level,
|
|
52
|
+
key: `${item.path}-${level}`,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// Add children if folder is expanded
|
|
56
|
+
if (item.type === 'directory' &&
|
|
57
|
+
treeModel.isFolderExpanded(item.path) &&
|
|
58
|
+
item.children &&
|
|
59
|
+
item.children.length > 0) {
|
|
60
|
+
processItems(item.children, level + 1);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
processItems(items);
|
|
66
|
+
return result;
|
|
67
|
+
}, [items, treeModel, treeModel.expandedFoldersArray]);
|
|
68
|
+
|
|
69
|
+
const handleItemClick = useCallback((item: FileBrowserItem) => {
|
|
70
|
+
onItemClick?.(item);
|
|
71
|
+
}, [onItemClick]);
|
|
72
|
+
|
|
73
|
+
const handleItemDoubleClick = useCallback((item: FileBrowserItem) => {
|
|
74
|
+
onItemDoubleClick?.(item);
|
|
75
|
+
}, [onItemDoubleClick]);
|
|
76
|
+
|
|
77
|
+
const handleKeyDown = useCallback((e: React.KeyboardEvent, currentIndex: number) => {
|
|
78
|
+
const nodeCount = flattenedNodes.length;
|
|
79
|
+
|
|
80
|
+
switch (e.key) {
|
|
81
|
+
case 'ArrowDown':
|
|
82
|
+
e.preventDefault();
|
|
83
|
+
if (currentIndex < nodeCount - 1) {
|
|
84
|
+
const nextNode = flattenedNodes[currentIndex + 1];
|
|
85
|
+
if (nextNode) {
|
|
86
|
+
handleItemClick(nextNode.item);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
break;
|
|
90
|
+
|
|
91
|
+
case 'ArrowUp':
|
|
92
|
+
e.preventDefault();
|
|
93
|
+
if (currentIndex > 0) {
|
|
94
|
+
const prevNode = flattenedNodes[currentIndex - 1];
|
|
95
|
+
if (prevNode) {
|
|
96
|
+
handleItemClick(prevNode.item);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
break;
|
|
100
|
+
|
|
101
|
+
case 'Home':
|
|
102
|
+
e.preventDefault();
|
|
103
|
+
if (nodeCount > 0) {
|
|
104
|
+
const firstNode = flattenedNodes[0];
|
|
105
|
+
if (firstNode) {
|
|
106
|
+
handleItemClick(firstNode.item);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
break;
|
|
110
|
+
|
|
111
|
+
case 'End':
|
|
112
|
+
e.preventDefault();
|
|
113
|
+
if (nodeCount > 0) {
|
|
114
|
+
const lastNode = flattenedNodes[nodeCount - 1];
|
|
115
|
+
if (lastNode) {
|
|
116
|
+
handleItemClick(lastNode.item);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
break;
|
|
120
|
+
}
|
|
121
|
+
}, [flattenedNodes, handleItemClick]);
|
|
122
|
+
|
|
123
|
+
// Regular rendering (non-virtualized)
|
|
124
|
+
const renderNodes = () => {
|
|
125
|
+
return flattenedNodes.map((node, index) => (
|
|
126
|
+
<TreeNode
|
|
127
|
+
key={node.key}
|
|
128
|
+
item={node.item}
|
|
129
|
+
treeModel={treeModel}
|
|
130
|
+
level={node.level}
|
|
131
|
+
onItemClick={handleItemClick}
|
|
132
|
+
onItemDoubleClick={handleItemDoubleClick}
|
|
133
|
+
isSelected={selectedItemIds.has(node.item.id)}
|
|
134
|
+
isFocused={focusedItemId === node.item.id}
|
|
135
|
+
getItemIcon={getItemIcon}
|
|
136
|
+
/>
|
|
137
|
+
));
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
// Virtualized rendering for large lists
|
|
141
|
+
const renderVirtualizedNodes = () => {
|
|
142
|
+
// Basic virtualization implementation
|
|
143
|
+
// For production, consider using react-window or react-virtualized
|
|
144
|
+
const itemHeight = 24; // Approximate height per tree node
|
|
145
|
+
const containerHeight = Math.min(maxHeight, flattenedNodes.length * itemHeight);
|
|
146
|
+
const visibleCount = Math.ceil(containerHeight / itemHeight);
|
|
147
|
+
|
|
148
|
+
// For now, render all items - proper virtualization would require scroll handling
|
|
149
|
+
return (
|
|
150
|
+
<div
|
|
151
|
+
className="overflow-auto"
|
|
152
|
+
style={{ maxHeight: containerHeight }}
|
|
153
|
+
role="tree"
|
|
154
|
+
aria-label="File tree"
|
|
155
|
+
>
|
|
156
|
+
{renderNodes()}
|
|
157
|
+
</div>
|
|
158
|
+
);
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
if (flattenedNodes.length === 0) {
|
|
162
|
+
return (
|
|
163
|
+
<div className={cn('p-4 text-center text-muted-foreground', className)}>
|
|
164
|
+
<p className="text-sm">No items to display</p>
|
|
165
|
+
</div>
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return (
|
|
170
|
+
<div
|
|
171
|
+
className={cn('focus-within:outline-none', className)}
|
|
172
|
+
role="tree"
|
|
173
|
+
aria-label="File tree"
|
|
174
|
+
onKeyDown={(e) => {
|
|
175
|
+
// Find currently focused item index
|
|
176
|
+
const focusedIndex = flattenedNodes.findIndex(node =>
|
|
177
|
+
node.item.id === focusedItemId
|
|
178
|
+
);
|
|
179
|
+
if (focusedIndex >= 0) {
|
|
180
|
+
handleKeyDown(e, focusedIndex);
|
|
181
|
+
}
|
|
182
|
+
}}
|
|
183
|
+
>
|
|
184
|
+
{virtualization ? renderVirtualizedNodes() : renderNodes()}
|
|
185
|
+
</div>
|
|
186
|
+
);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
TreeNodeList.displayName = 'TreeNodeList';
|
|
190
|
+
|
|
191
|
+
export default TreeNodeList;
|