@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.
Files changed (244) hide show
  1. package/dist/ExplorerLayout-CSIJd7N4.js +105 -0
  2. package/dist/ExplorerLayout-CSIJd7N4.js.map +1 -0
  3. package/dist/FileBrowserContext-B6jixa2j.js +11 -0
  4. package/dist/FileBrowserContext-B6jixa2j.js.map +1 -0
  5. package/dist/calendar-DSlrbHoj.js +761 -0
  6. package/dist/calendar-DSlrbHoj.js.map +1 -0
  7. package/dist/calendar.d.ts +3 -0
  8. package/dist/calendar.js +3 -0
  9. package/dist/contacts-DQXTZzHc.js +539 -0
  10. package/dist/contacts-DQXTZzHc.js.map +1 -0
  11. package/dist/contacts.d.ts +3 -0
  12. package/dist/contacts.js +3 -0
  13. package/dist/file-browser-m5atC3kF.js +6755 -0
  14. package/dist/file-browser-m5atC3kF.js.map +1 -0
  15. package/dist/file-browser.d.ts +11 -0
  16. package/dist/file-browser.js +9 -0
  17. package/dist/git-B55e6LL-.js +561 -0
  18. package/dist/git-B55e6LL-.js.map +1 -0
  19. package/dist/git.d.ts +2 -0
  20. package/dist/git.js +3 -0
  21. package/dist/iconMap-V4B8P-Uh.js +206 -0
  22. package/dist/iconMap-V4B8P-Uh.js.map +1 -0
  23. package/dist/icons-CIsIOZXR.js +0 -0
  24. package/dist/icons.d.ts +2 -0
  25. package/dist/icons.js +4 -0
  26. package/dist/index-BNmNIWBL.d.ts +71 -0
  27. package/dist/index-BNmNIWBL.d.ts.map +1 -0
  28. package/dist/index-Bryv_GCG.d.ts +1481 -0
  29. package/dist/index-Bryv_GCG.d.ts.map +1 -0
  30. package/dist/index-CuQIjSXs.d.ts +134 -0
  31. package/dist/index-CuQIjSXs.d.ts.map +1 -0
  32. package/dist/index-DSu19mq0.d.ts +153 -0
  33. package/dist/index-DSu19mq0.d.ts.map +1 -0
  34. package/dist/index-DmsyeHFr.d.ts +149 -0
  35. package/dist/index-DmsyeHFr.d.ts.map +1 -0
  36. package/dist/index-DxnJ8FYM.d.ts +17 -0
  37. package/dist/index-DxnJ8FYM.d.ts.map +1 -0
  38. package/dist/index-DzfY1Tok.d.ts +32 -0
  39. package/dist/index-DzfY1Tok.d.ts.map +1 -0
  40. package/dist/index-Ml_SgiKa.d.ts +1847 -0
  41. package/dist/index-Ml_SgiKa.d.ts.map +1 -0
  42. package/dist/index-kHr9udZD.d.ts +1025 -0
  43. package/dist/index-kHr9udZD.d.ts.map +1 -0
  44. package/dist/index.d.ts +11 -0
  45. package/dist/index.js +15 -0
  46. package/dist/layout-Ca_4r8ka.js +89 -0
  47. package/dist/layout-Ca_4r8ka.js.map +1 -0
  48. package/dist/layout.d.ts +2 -0
  49. package/dist/layout.js +5 -0
  50. package/dist/list-CxfT6hix.js +6831 -0
  51. package/dist/list-CxfT6hix.js.map +1 -0
  52. package/dist/list.d.ts +2 -0
  53. package/dist/list.js +5 -0
  54. package/dist/media-DZ292aKK.js +557 -0
  55. package/dist/media-DZ292aKK.js.map +1 -0
  56. package/dist/media.d.ts +3 -0
  57. package/dist/media.js +3 -0
  58. package/dist/tree-Dd9Z0Aso.js +3351 -0
  59. package/dist/tree-Dd9Z0Aso.js.map +1 -0
  60. package/dist/tree.d.ts +2 -0
  61. package/dist/tree.js +6 -0
  62. package/dist/types-common-CB3kRek8.d.ts +26 -0
  63. package/dist/types-common-CB3kRek8.d.ts.map +1 -0
  64. package/dist/utils-B4fdKKsy.js +3 -0
  65. package/package.json +109 -0
  66. package/src/calendar/AgendaView.tsx +37 -0
  67. package/src/calendar/CalendarBrowser.tsx +90 -0
  68. package/src/calendar/CalendarModel.ts +142 -0
  69. package/src/calendar/CalendarSidebar.tsx +81 -0
  70. package/src/calendar/DayView.tsx +76 -0
  71. package/src/calendar/EventCard.tsx +51 -0
  72. package/src/calendar/MockCalendarProvider.ts +98 -0
  73. package/src/calendar/MonthView.tsx +77 -0
  74. package/src/calendar/WeekView.tsx +129 -0
  75. package/src/calendar/index.ts +18 -0
  76. package/src/calendar/types.ts +25 -0
  77. package/src/contacts/ContactAvatar.tsx +35 -0
  78. package/src/contacts/ContactBrowser.tsx +56 -0
  79. package/src/contacts/ContactCard.tsx +37 -0
  80. package/src/contacts/ContactDetail.tsx +63 -0
  81. package/src/contacts/ContactGroupSidebar.tsx +40 -0
  82. package/src/contacts/ContactList.tsx +32 -0
  83. package/src/contacts/ContactListModel.ts +120 -0
  84. package/src/contacts/MockContactProvider.ts +77 -0
  85. package/src/contacts/index.ts +17 -0
  86. package/src/contacts/types.ts +26 -0
  87. package/src/demos/CalendarBrowserDemo.tsx +15 -0
  88. package/src/demos/ContactBrowserDemo.tsx +15 -0
  89. package/src/demos/MediaBrowserDemo.tsx +15 -0
  90. package/src/file-browser/adapters/DocumentViewerAdapter.ts +371 -0
  91. package/src/file-browser/adapters/FileSystemBridge.ts +168 -0
  92. package/src/file-browser/adapters/GitBrowserAdapter.ts +546 -0
  93. package/src/file-browser/adapters/README.md +504 -0
  94. package/src/file-browser/adapters/index.ts +27 -0
  95. package/src/file-browser/adapters/types.ts +70 -0
  96. package/src/file-browser/architecture.md +645 -0
  97. package/src/file-browser/components/CreateItemDialog.tsx +71 -0
  98. package/src/file-browser/components/DeleteConfirmDialog.tsx +58 -0
  99. package/src/file-browser/components/FileBrowser.tsx +473 -0
  100. package/src/file-browser/components/FileBrowserContent.tsx +209 -0
  101. package/src/file-browser/components/FileBrowserHeader.tsx +151 -0
  102. package/src/file-browser/components/FileBrowserToolbar.tsx +145 -0
  103. package/src/file-browser/components/LeftPanel/LeftPanel.tsx +103 -0
  104. package/src/file-browser/components/LeftPanel/LeftPanelTabs.tsx +70 -0
  105. package/src/file-browser/components/LeftPanel/TreeNavigationView.tsx +256 -0
  106. package/src/file-browser/components/PreviewPane.tsx +146 -0
  107. package/src/file-browser/components/RightPanel/FilePreview.tsx +219 -0
  108. package/src/file-browser/components/RightPanel/RightPanel.tsx +186 -0
  109. package/src/file-browser/components/RightPanel/RightPanelToolbar.tsx +113 -0
  110. package/src/file-browser/components/UploadProgress.tsx +123 -0
  111. package/src/file-browser/components/ViewerHost.tsx +208 -0
  112. package/src/file-browser/components/mobile/MobileNavigation.tsx +227 -0
  113. package/src/file-browser/components/navigation/NavigationButtons.tsx +171 -0
  114. package/src/file-browser/components/shared/ErrorBoundary.tsx +116 -0
  115. package/src/file-browser/components/shared/FileBrowserItem.tsx +195 -0
  116. package/src/file-browser/components/shared/FileIcon.tsx +169 -0
  117. package/src/file-browser/components/toolbar/ViewModeToggle.tsx +200 -0
  118. package/src/file-browser/components/views/ListView/ListView.tsx +484 -0
  119. package/src/file-browser/components/views/ThumbnailView/ThumbnailView.tsx +323 -0
  120. package/src/file-browser/components/views/TreeView/TreeNode.tsx +186 -0
  121. package/src/file-browser/components/views/TreeView/TreeNodeList.tsx +191 -0
  122. package/src/file-browser/components/views/TreeView/TreeView.tsx +200 -0
  123. package/src/file-browser/components/views/TreemapView/TreemapView.tsx +339 -0
  124. package/src/file-browser/context/FileBrowserContext.tsx +13 -0
  125. package/src/file-browser/examples/BasicUsage.tsx +20 -0
  126. package/src/file-browser/index.ts +98 -0
  127. package/src/file-browser/models/FileBrowserModel.ts +623 -0
  128. package/src/file-browser/models/LeftPanelManagerModel.ts +105 -0
  129. package/src/file-browser/models/NavigationManagerModel.ts +312 -0
  130. package/src/file-browser/models/ResponsiveLayoutManagerModel.ts +437 -0
  131. package/src/file-browser/models/RightPanelManagerModel.ts +190 -0
  132. package/src/file-browser/models/SelectionManagerModel.ts +252 -0
  133. package/src/file-browser/models/ToolbarManagerModel.ts +144 -0
  134. package/src/file-browser/models/UploadModel.ts +147 -0
  135. package/src/file-browser/models/ViewModeManagerModel.ts +185 -0
  136. package/src/file-browser/models/ViewerHostModel.ts +44 -0
  137. package/src/file-browser/models/ui/ListViewUIModel.ts +265 -0
  138. package/src/file-browser/models/ui/PreviewUIModel.ts +297 -0
  139. package/src/file-browser/models/ui/ThumbnailViewUIModel.ts +254 -0
  140. package/src/file-browser/models/ui/TreeViewUIModel.ts +128 -0
  141. package/src/file-browser/models/ui/TreemapViewUIModel.ts +350 -0
  142. package/src/file-browser/providers/FileSystemListProvider.ts +552 -0
  143. package/src/file-browser/providers/FileSystemProvider.ts +401 -0
  144. package/src/file-browser/providers/FileSystemTreeProvider.ts +231 -0
  145. package/src/file-browser/providers/GitProvider.ts +337 -0
  146. package/src/file-browser/providers/GitRepositoryProvider.ts +376 -0
  147. package/src/file-browser/providers/IFileBrowserProvider.ts +56 -0
  148. package/src/file-browser/providers/MemoryProvider.ts +303 -0
  149. package/src/file-browser/providers/index.ts +4 -0
  150. package/src/file-browser/registry/ViewerRegistry.ts +551 -0
  151. package/src/file-browser/registry/types.ts +144 -0
  152. package/src/file-browser/scripts/performanceBenchmark.ts +553 -0
  153. package/src/file-browser/services/ThumbnailCacheService.ts +128 -0
  154. package/src/file-browser/tasks.md +537 -0
  155. package/src/file-browser/types/FileBrowserTypes.ts +126 -0
  156. package/src/file-browser/types/ProviderTypes.ts +155 -0
  157. package/src/file-browser/types/UITypes.ts +235 -0
  158. package/src/file-browser/types/ViewModeTypes.ts +150 -0
  159. package/src/file-browser/utils/gestures.ts +327 -0
  160. package/src/file-browser/utils/performance.ts +563 -0
  161. package/src/file-browser/viewers/ImageViewer.tsx +163 -0
  162. package/src/file-browser/viewers/ImageViewerModel.ts +79 -0
  163. package/src/file-browser/viewers/TextViewer.tsx +95 -0
  164. package/src/file-browser/viewers/UnsupportedFileViewer.tsx +57 -0
  165. package/src/file-browser/viewers/index.ts +61 -0
  166. package/src/git/BranchList.tsx +128 -0
  167. package/src/git/CommitGraph.tsx +239 -0
  168. package/src/git/CommitList.tsx +258 -0
  169. package/src/git/DiffViewer.tsx +219 -0
  170. package/src/git/index.ts +4 -0
  171. package/src/icons/iconMap.ts +146 -0
  172. package/src/icons/index.ts +9 -0
  173. package/src/index.ts +13 -0
  174. package/src/layout/README.md +307 -0
  175. package/src/layout/components/ExplorerLayout/ExplorerLayout.tsx +178 -0
  176. package/src/layout/examples/SimpleExample.tsx +60 -0
  177. package/src/layout/index.ts +6 -0
  178. package/src/lib/utils.ts +1 -0
  179. package/src/list/README.md +303 -0
  180. package/src/list/architecture.md +807 -0
  181. package/src/list/components/CalculatedGridView.tsx +252 -0
  182. package/src/list/components/DragPreview.tsx +102 -0
  183. package/src/list/components/ListContextMenu.tsx +274 -0
  184. package/src/list/components/ListItem.tsx +761 -0
  185. package/src/list/components/ListItems.tsx +919 -0
  186. package/src/list/components/MasonryView.tsx +241 -0
  187. package/src/list/components/SearchFilter.tsx +44 -0
  188. package/src/list/components/TreemapView.tsx +709 -0
  189. package/src/list/components/ViewSizeControls.tsx +205 -0
  190. package/src/list/components/ViewTypeSelector.tsx +312 -0
  191. package/src/list/components/VirtualizedDetailsView.tsx +231 -0
  192. package/src/list/components/VirtualizedGrid.tsx +164 -0
  193. package/src/list/components/VirtualizedList.tsx +154 -0
  194. package/src/list/components/VirtualizedMasonryView.tsx +344 -0
  195. package/src/list/components/shared/EmptyState.tsx +103 -0
  196. package/src/list/components/shared/ErrorBoundary.tsx +123 -0
  197. package/src/list/components/shared/ErrorDisplay.tsx +100 -0
  198. package/src/list/components/shared/ListLoader.tsx +146 -0
  199. package/src/list/components/shared/LoadingIndicator.tsx +80 -0
  200. package/src/list/index.ts +92 -0
  201. package/src/list/models/ListItemsModel.ts +1301 -0
  202. package/src/list/models/TreemapModel.ts +204 -0
  203. package/src/list/providers/ListItemsProvider.ts +313 -0
  204. package/src/list/providers/TestListProvider.ts +604 -0
  205. package/src/list/tasks.md +937 -0
  206. package/src/list/types/ListTypes.ts +178 -0
  207. package/src/list/utils/BenchmarkLogger.ts +243 -0
  208. package/src/list/utils/DragDropManager.ts +320 -0
  209. package/src/list/utils/GridLayoutCalculator.ts +290 -0
  210. package/src/list/utils/ListAccessibility.ts +367 -0
  211. package/src/list/utils/ListKeyboard.ts +414 -0
  212. package/src/list/utils/MasonryLayoutCalculator.ts +302 -0
  213. package/src/list/utils/MasonryLayoutEngine.ts +401 -0
  214. package/src/list/utils/__tests__/MasonryLayoutEngine.test.ts +157 -0
  215. package/src/list/utils/__tests__/VirtualizedMasonryView.test.tsx +251 -0
  216. package/src/media/AlbumSidebar.tsx +48 -0
  217. package/src/media/MediaBrowser.tsx +92 -0
  218. package/src/media/MediaBrowserModel.ts +138 -0
  219. package/src/media/MediaGrid.tsx +50 -0
  220. package/src/media/MediaList.tsx +49 -0
  221. package/src/media/MediaPreview.tsx +63 -0
  222. package/src/media/MediaTimeline.tsx +38 -0
  223. package/src/media/MockMediaProvider.ts +70 -0
  224. package/src/media/index.ts +18 -0
  225. package/src/media/types.ts +21 -0
  226. package/src/styles/variables.css +60 -0
  227. package/src/tree/DEVELOPMENT_SUMMARY.md +170 -0
  228. package/src/tree/__tests__/TreeModel.test.ts +16 -0
  229. package/src/tree/architecture.md +530 -0
  230. package/src/tree/components/Tree.tsx +283 -0
  231. package/src/tree/components/TreeCheckbox.tsx +147 -0
  232. package/src/tree/components/TreeContextMenu.tsx +139 -0
  233. package/src/tree/components/TreeNodeList.tsx +329 -0
  234. package/src/tree/components/TreeTable.tsx +382 -0
  235. package/src/tree/index.ts +58 -0
  236. package/src/tree/models/TreeModel.ts +839 -0
  237. package/src/tree/providers/SimpleTreeProvider.ts +463 -0
  238. package/src/tree/providers/TestTreeProvider.ts +946 -0
  239. package/src/tree/providers/TreeProvider.ts +308 -0
  240. package/src/tree/tasks.md +2046 -0
  241. package/src/tree/types/TreeTypes.ts +279 -0
  242. package/src/tree/utils/SelectionTheme.ts +150 -0
  243. package/src/tree/utils/logger.ts +203 -0
  244. 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 &ldquo;{model.searchQuery}&rdquo;</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';