@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,252 @@
1
+ import React, { useEffect, useRef, useState, useCallback } from 'react';
2
+ import { observer } from 'mobx-react-lite';
3
+ import { ListItemsModel } from '../models/ListItemsModel';
4
+ import { ListItem } from './ListItem';
5
+ import type { GridItemLayout } from '../utils/GridLayoutCalculator';
6
+ import type { ListItemData } from '../types/ListTypes';
7
+
8
+ // AICODE-NOTE: Grid view component using precise calculator-based positioning
9
+ export interface CalculatedGridViewProps {
10
+ model: ListItemsModel;
11
+ className?: string;
12
+ height?: number;
13
+ width?: number;
14
+ enableVirtualization?: boolean;
15
+ onItemClick?: (item: ListItemData, event: React.MouseEvent) => void;
16
+ onItemDoubleClick?: (item: ListItemData, event: React.MouseEvent) => void;
17
+ onItemContextMenu?: (item: ListItemData, event: React.MouseEvent) => void;
18
+ onItemDragStart?: (item: ListItemData, event: React.DragEvent) => void;
19
+ onItemDragOver?: (item: ListItemData, event: React.DragEvent) => void;
20
+ onItemDragLeave?: (item: ListItemData, event: React.DragEvent) => void;
21
+ onItemDrop?: (item: ListItemData, event: React.DragEvent) => void;
22
+ }
23
+
24
+ // AICODE-NOTE: Extract grid item into separate observer component for MobX reactivity
25
+ interface GridItemWrapperProps {
26
+ item: ListItemData;
27
+ layout: GridItemLayout;
28
+ index: number;
29
+ model: ListItemsModel;
30
+ onItemClick?: (item: ListItemData, event: React.MouseEvent) => void;
31
+ onItemDoubleClick?: (item: ListItemData, event: React.MouseEvent) => void;
32
+ onItemContextMenu?: (item: ListItemData, event: React.MouseEvent) => void;
33
+ onItemDragStart?: (item: ListItemData, event: React.DragEvent) => void;
34
+ onItemDragOver?: (item: ListItemData, event: React.DragEvent) => void;
35
+ onItemDragLeave?: (item: ListItemData, event: React.DragEvent) => void;
36
+ onItemDrop?: (item: ListItemData, event: React.DragEvent) => void;
37
+ }
38
+
39
+ const GridItemWrapper = observer<GridItemWrapperProps>(({
40
+ item,
41
+ layout,
42
+ index,
43
+ model,
44
+ onItemClick,
45
+ onItemDoubleClick,
46
+ onItemContextMenu,
47
+ onItemDragStart,
48
+ onItemDragOver,
49
+ onItemDragLeave,
50
+ onItemDrop
51
+ }) => (
52
+ <div
53
+ data-item-id={item.id}
54
+ style={{
55
+ position: 'absolute',
56
+ left: layout.x,
57
+ top: layout.y,
58
+ width: layout.width,
59
+ height: layout.height
60
+ }}
61
+ >
62
+ <ListItem
63
+ item={item}
64
+ index={index}
65
+ totalItems={model.totalItemCount}
66
+ viewType={model.currentViewType}
67
+ provider={model.provider}
68
+ model={model}
69
+ itemWidth={layout.width}
70
+ itemHeight={layout.height}
71
+ thumbnailSize={layout.thumbnailSize}
72
+ isSelected={model.isItemSelected(item.id)}
73
+ isFocused={model.focusedItem === item.id}
74
+ isDraggedOver={model.dragOverItem === item.id}
75
+ dragOverPosition={model.dragOverItem === item.id ? model.dragOverPosition : null}
76
+ canDrag={model.canDragItem(item)}
77
+ onClick={onItemClick}
78
+ onDoubleClick={onItemDoubleClick}
79
+ onContextMenu={onItemContextMenu}
80
+ onDragStart={onItemDragStart}
81
+ onDragOver={onItemDragOver}
82
+ onDragLeave={onItemDragLeave}
83
+ onDrop={onItemDrop}
84
+ />
85
+ </div>
86
+ ));
87
+
88
+ GridItemWrapper.displayName = 'CalculatedGridItemWrapper';
89
+
90
+ const CalculatedGridViewComponent: React.FC<CalculatedGridViewProps> = ({
91
+ model,
92
+ className = '',
93
+ height,
94
+ width,
95
+ enableVirtualization = true,
96
+ onItemClick,
97
+ onItemDoubleClick,
98
+ onItemContextMenu,
99
+ onItemDragStart,
100
+ onItemDragOver,
101
+ onItemDragLeave,
102
+ onItemDrop
103
+ }) => {
104
+ const containerRef = useRef<HTMLDivElement>(null);
105
+ const [scrollTop, setScrollTop] = useState(0);
106
+ // AICODE-NOTE: Initialize with provided dimensions so first render doesn't flash at 0x0
107
+ const [containerSize, setContainerSize] = useState({ width: width || 0, height: height || 0 });
108
+ const [hasMeasured, setHasMeasured] = useState(!!(width && height));
109
+
110
+ // AICODE-NOTE: Update container size when dimensions change
111
+ useEffect(() => {
112
+ const updateSize = () => {
113
+ if (containerRef.current) {
114
+ const rect = containerRef.current.getBoundingClientRect();
115
+ const newSize = {
116
+ width: width || rect.width,
117
+ height: height || rect.height
118
+ };
119
+ setContainerSize(newSize);
120
+ setHasMeasured(true);
121
+ model.updateContainerSize(newSize.width, newSize.height);
122
+ }
123
+ };
124
+
125
+ updateSize();
126
+
127
+ // Set up resize observer
128
+ const resizeObserver = new ResizeObserver(updateSize);
129
+ if (containerRef.current) {
130
+ resizeObserver.observe(containerRef.current);
131
+ }
132
+
133
+ return () => {
134
+ resizeObserver.disconnect();
135
+ };
136
+ }, [width, height, model]);
137
+
138
+ // AICODE-NOTE: Handle scroll for virtualization
139
+ const handleScroll = useCallback(() => {
140
+ if (containerRef.current) {
141
+ setScrollTop(containerRef.current.scrollTop);
142
+ }
143
+ }, []);
144
+
145
+ useEffect(() => {
146
+ const container = containerRef.current;
147
+ if (!container || !enableVirtualization) return;
148
+
149
+ let ticking = false;
150
+ const throttledScroll = () => {
151
+ if (!ticking) {
152
+ requestAnimationFrame(() => {
153
+ handleScroll();
154
+ ticking = false;
155
+ });
156
+ ticking = true;
157
+ }
158
+ };
159
+
160
+ container.addEventListener('scroll', throttledScroll);
161
+ return () => {
162
+ container.removeEventListener('scroll', throttledScroll);
163
+ };
164
+ }, [handleScroll, enableVirtualization]);
165
+
166
+ // Scroll to item when model requests it
167
+ useEffect(() => {
168
+ const targetId = model.scrollToItemId;
169
+ if (!targetId || !containerRef.current) return;
170
+ const el = containerRef.current.querySelector(`[data-item-id="${CSS.escape(targetId)}"]`);
171
+ if (el) {
172
+ el.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
173
+ }
174
+ model.clearScrollToItem();
175
+ }, [model.scrollToItemId, model]);
176
+
177
+ // AICODE-NOTE: Don't render items until container has been measured to prevent 0→final size flash
178
+ const gridLayout = model.getGridLayout();
179
+ if (!gridLayout || !hasMeasured || containerSize.width === 0) {
180
+ return (
181
+ <div
182
+ ref={containerRef}
183
+ className={`relative overflow-auto ${className}`}
184
+ style={{ height, width, overflowX: 'hidden' }}
185
+ />
186
+ );
187
+ }
188
+
189
+ const { layout: itemLayouts, totalHeight } = gridLayout;
190
+
191
+ // AICODE-NOTE: Determine which items to render
192
+ let visibleItems: Array<{ item: ListItemData; layout: GridItemLayout; index: number }> = [];
193
+
194
+ if (enableVirtualization && containerSize.height > 0) {
195
+ // Use virtualization - only render visible items
196
+ const visibleLayouts = model.getVisibleGridItems(scrollTop, containerSize.height);
197
+ visibleItems = visibleLayouts.map((layout, layoutIndex) => {
198
+ // Find the actual item index from the layout position
199
+ const itemIndex = itemLayouts.findIndex(l => l.x === layout.x && l.y === layout.y);
200
+ const item = model.items[itemIndex];
201
+ return item ? { item, layout, index: itemIndex } : null;
202
+ }).filter(Boolean) as Array<{ item: ListItemData; layout: GridItemLayout; index: number }>;
203
+ } else {
204
+ // Render all items
205
+ visibleItems = model.items.map((item, index) => {
206
+ const layout = itemLayouts[index];
207
+ return layout ? { item, layout, index } : null;
208
+ }).filter(Boolean) as Array<{ item: ListItemData; layout: GridItemLayout; index: number }>;
209
+ }
210
+
211
+ // console.log('🎨 [CALCULATED GRID] Rendering', visibleItems.length, 'items out of', model.items.length, 'total');
212
+ // console.log('📐 [GRID LAYOUT] Container:', containerSize, 'Total height:', totalHeight);
213
+
214
+ return (
215
+ <div
216
+ ref={containerRef}
217
+ className={`relative overflow-auto ${className}`}
218
+ style={{ height, width, overflowX: 'hidden' }}
219
+ >
220
+ {/* AICODE-NOTE: Container with calculated total height and no horizontal overflow */}
221
+ <div
222
+ style={{
223
+ position: 'relative',
224
+ height: totalHeight,
225
+ width: '100%',
226
+ maxWidth: containerSize.width || '100%',
227
+ overflow: 'hidden'
228
+ }}
229
+ >
230
+ {visibleItems.map(({ item, layout, index }) => (
231
+ <GridItemWrapper
232
+ key={item.id}
233
+ item={item}
234
+ layout={layout}
235
+ index={index}
236
+ model={model}
237
+ onItemClick={onItemClick}
238
+ onItemDoubleClick={onItemDoubleClick}
239
+ onItemContextMenu={onItemContextMenu}
240
+ onItemDragStart={onItemDragStart}
241
+ onItemDragOver={onItemDragOver}
242
+ onItemDragLeave={onItemDragLeave}
243
+ onItemDrop={onItemDrop}
244
+ />
245
+ ))}
246
+ </div>
247
+ </div>
248
+ );
249
+ };
250
+
251
+ // AICODE-NOTE: Export memoized component for performance
252
+ export const CalculatedGridView = observer(CalculatedGridViewComponent);
@@ -0,0 +1,102 @@
1
+ import React from 'react';
2
+ import { observer } from 'mobx-react-lite';
3
+ import { ListItemData } from '../types/ListTypes';
4
+ import { FileText, Image, Video, Music, Folder, Archive, File } from 'lucide-react';
5
+
6
+ // AICODE-NOTE: Enhanced drag preview component for better visual feedback during drag operations
7
+
8
+ export interface DragPreviewProps {
9
+ items: ListItemData[];
10
+ className?: string;
11
+ }
12
+
13
+ const getItemIcon = (item: ListItemData) => {
14
+ const type = item.type?.toLowerCase() || '';
15
+
16
+ if (type.includes('folder') || type.includes('directory')) return Folder;
17
+ if (type.includes('image') || type.includes('photo')) return Image;
18
+ if (type.includes('video') || type.includes('movie')) return Video;
19
+ if (type.includes('audio') || type.includes('music')) return Music;
20
+ if (type.includes('text') || type.includes('document')) return FileText;
21
+ if (type.includes('archive') || type.includes('zip')) return Archive;
22
+
23
+ return File;
24
+ };
25
+
26
+ export const DragPreview = observer<DragPreviewProps>(({ items, className = '' }) => {
27
+ if (items.length === 0) return null;
28
+
29
+ const firstItem = items[0]!; // Safe because we checked length above
30
+ const Icon = getItemIcon(firstItem);
31
+ const hasMultiple = items.length > 1;
32
+
33
+ return (
34
+ <div className={`
35
+ bg-background border border-border rounded-lg shadow-lg p-3 min-w-48 max-w-64
36
+ ${className}
37
+ `}>
38
+ {/* Single item preview */}
39
+ {!hasMultiple && (
40
+ <div className="flex items-center gap-3">
41
+ <div className="flex-shrink-0">
42
+ <Icon className="w-5 h-5 text-muted-foreground" />
43
+ </div>
44
+ <div className="flex-1 min-w-0">
45
+ <div className="text-sm font-medium truncate">
46
+ {firstItem.name}
47
+ </div>
48
+ {firstItem.type && (
49
+ <div className="text-xs text-muted-foreground truncate">
50
+ {firstItem.type}
51
+ </div>
52
+ )}
53
+ </div>
54
+ </div>
55
+ )}
56
+
57
+ {/* Multiple items preview */}
58
+ {hasMultiple && (
59
+ <div className="space-y-2">
60
+ <div className="flex items-center gap-3">
61
+ <div className="flex-shrink-0 relative">
62
+ <Icon className="w-5 h-5 text-muted-foreground" />
63
+ <div className="absolute -top-1 -right-1 bg-primary text-primary-foreground text-xs rounded-full w-4 h-4 flex items-center justify-center font-medium">
64
+ {items.length}
65
+ </div>
66
+ </div>
67
+ <div className="flex-1 min-w-0">
68
+ <div className="text-sm font-medium truncate">
69
+ {firstItem.name}
70
+ </div>
71
+ <div className="text-xs text-muted-foreground">
72
+ +{items.length - 1} more items
73
+ </div>
74
+ </div>
75
+ </div>
76
+
77
+ {/* Show up to 3 additional items */}
78
+ {items.slice(1, 4).map((item, index) => {
79
+ const ItemIcon = getItemIcon(item);
80
+ return (
81
+ <div key={item.id} className="flex items-center gap-3 pl-2 opacity-75">
82
+ <ItemIcon className="w-4 h-4 text-muted-foreground flex-shrink-0" />
83
+ <div className="text-xs text-muted-foreground truncate">
84
+ {item.name}
85
+ </div>
86
+ </div>
87
+ );
88
+ })}
89
+
90
+ {/* Show count if more than 4 items */}
91
+ {items.length > 4 && (
92
+ <div className="text-xs text-muted-foreground pl-7">
93
+ ...and {items.length - 4} more
94
+ </div>
95
+ )}
96
+ </div>
97
+ )}
98
+ </div>
99
+ );
100
+ });
101
+
102
+ DragPreview.displayName = 'DragPreview';
@@ -0,0 +1,274 @@
1
+ import React, { useRef, useEffect, useState, useCallback } from 'react';
2
+ import { observer } from 'mobx-react-lite';
3
+ import { ListItemData, ListContextMenuItem } from '../types/ListTypes';
4
+ import { ListItemsProvider } from '../providers/ListItemsProvider';
5
+ import { contextMenuIconMap } from '../../icons/iconMap';
6
+
7
+ // AICODE-NOTE: Context menu component for list items — positioned near cursor,
8
+ // closes on click-outside, Escape, or scroll. Styled to match @anymux/ui context-menu.
9
+
10
+ export interface ListContextMenuProps {
11
+ isOpen: boolean;
12
+ position: { x: number; y: number };
13
+ items: ListItemData[];
14
+ provider: ListItemsProvider;
15
+ onClose: () => void;
16
+ /** Extra menu items to prepend before provider items */
17
+ extraMenuItems?: ListContextMenuItem[];
18
+ /** Handler for extra menu item actions (called instead of provider.onContextMenuAction) */
19
+ onExtraMenuAction?: (actionId: string, items: ListItemData[]) => void;
20
+ }
21
+
22
+ // AICODE-NOTE: Individual context menu item with icon, label, shortcut, destructive variant
23
+ interface ContextMenuItemRowProps {
24
+ menuItem: ListContextMenuItem;
25
+ isFocused: boolean;
26
+ onMenuItemClick: (menuItem: ListContextMenuItem) => void;
27
+ onMouseEnter: () => void;
28
+ }
29
+
30
+ const ContextMenuItemRow: React.FC<ContextMenuItemRowProps> = ({
31
+ menuItem,
32
+ isFocused,
33
+ onMenuItemClick,
34
+ onMouseEnter,
35
+ }) => {
36
+ if (menuItem.type === 'separator') {
37
+ return <div className="bg-border -mx-1 my-1 h-px" />;
38
+ }
39
+
40
+ const isDestructive = menuItem.destructive;
41
+
42
+ return (
43
+ <button
44
+ role="menuitem"
45
+ onClick={() => !menuItem.disabled && onMenuItemClick(menuItem)}
46
+ onMouseEnter={onMouseEnter}
47
+ disabled={menuItem.disabled}
48
+ className={[
49
+ 'w-full text-left px-2 py-1.5 text-sm outline-hidden select-none',
50
+ 'flex items-center gap-2 rounded-sm cursor-default',
51
+ menuItem.disabled
52
+ ? 'pointer-events-none opacity-50'
53
+ : '',
54
+ !menuItem.disabled && !isDestructive
55
+ ? 'text-foreground'
56
+ : '',
57
+ !menuItem.disabled && isDestructive
58
+ ? 'text-destructive'
59
+ : '',
60
+ isFocused && !isDestructive
61
+ ? 'bg-accent text-accent-foreground'
62
+ : '',
63
+ isFocused && isDestructive
64
+ ? 'bg-destructive/10 text-destructive'
65
+ : '',
66
+ ].filter(Boolean).join(' ')}
67
+ title={menuItem.tooltip}
68
+ >
69
+ {/* AICODE-NOTE: Menu item icon */}
70
+ {menuItem.icon && (
71
+ <span className={[
72
+ 'w-4 h-4 flex-shrink-0 flex items-center justify-center',
73
+ !isDestructive ? '[&_svg]:text-muted-foreground' : '',
74
+ ].filter(Boolean).join(' ')}>
75
+ {typeof menuItem.icon === 'string' ? (
76
+ (() => {
77
+ const Icon = contextMenuIconMap[menuItem.icon as string];
78
+ return Icon ? <Icon className="size-4" /> : null;
79
+ })()
80
+ ) : (
81
+ React.createElement(menuItem.icon, { className: 'size-4' })
82
+ )}
83
+ </span>
84
+ )}
85
+
86
+ {/* AICODE-NOTE: Menu item label */}
87
+ <span className="flex-1 truncate">{menuItem.label}</span>
88
+
89
+ {/* AICODE-NOTE: Keyboard shortcut hint */}
90
+ {menuItem.shortcut && (
91
+ <span className="text-xs text-muted-foreground ml-auto tracking-widest">
92
+ {menuItem.shortcut}
93
+ </span>
94
+ )}
95
+ </button>
96
+ );
97
+ };
98
+
99
+ ContextMenuItemRow.displayName = 'ContextMenuItemRow';
100
+
101
+ export const ListContextMenu = observer<ListContextMenuProps>(({
102
+ isOpen,
103
+ position,
104
+ items,
105
+ provider,
106
+ onClose,
107
+ extraMenuItems,
108
+ onExtraMenuAction,
109
+ }) => {
110
+ const menuRef = useRef<HTMLDivElement>(null);
111
+ const [menuItems, setMenuItems] = useState<ListContextMenuItem[]>([]);
112
+ const [focusedIndex, setFocusedIndex] = useState(-1);
113
+ const [adjustedPosition, setAdjustedPosition] = useState(position);
114
+
115
+ // Get the actionable (non-separator) items for keyboard navigation
116
+ const actionableIndices = menuItems
117
+ .map((item, idx) => (item.type !== 'separator' ? idx : -1))
118
+ .filter(idx => idx !== -1);
119
+
120
+ // AICODE-NOTE: Get context menu items from provider when menu opens
121
+ useEffect(() => {
122
+ if (isOpen) {
123
+ let providerItems: ListContextMenuItem[] = [];
124
+ if (items.length === 0) {
125
+ providerItems = provider.getEmptySpaceContextMenu?.() || [];
126
+ } else if (items.length === 1) {
127
+ const firstItem = items[0];
128
+ if (firstItem) {
129
+ providerItems = provider.getItemContextMenu?.(firstItem) || [];
130
+ }
131
+ } else {
132
+ providerItems = provider.getMultiItemContextMenu?.(items) || [];
133
+ }
134
+ // Prepend extra menu items if provided
135
+ if (extraMenuItems && extraMenuItems.length > 0) {
136
+ setMenuItems([...extraMenuItems, ...providerItems]);
137
+ } else {
138
+ setMenuItems(providerItems);
139
+ }
140
+ setFocusedIndex(-1);
141
+ setAdjustedPosition(position);
142
+ }
143
+ }, [isOpen, items, provider, position, extraMenuItems]);
144
+
145
+ // AICODE-NOTE: Adjust position once the menu has rendered to keep within viewport
146
+ useEffect(() => {
147
+ if (!isOpen || !menuRef.current) return;
148
+
149
+ const menu = menuRef.current;
150
+ const rect = menu.getBoundingClientRect();
151
+ const vw = window.innerWidth;
152
+ const vh = window.innerHeight;
153
+
154
+ let left = position.x;
155
+ let top = position.y;
156
+
157
+ if (left + rect.width > vw - 8) {
158
+ left = Math.max(8, position.x - rect.width);
159
+ }
160
+ if (top + rect.height > vh - 8) {
161
+ top = Math.max(8, position.y - rect.height);
162
+ }
163
+
164
+ left = Math.max(8, Math.min(left, vw - rect.width - 8));
165
+ top = Math.max(8, Math.min(top, vh - rect.height - 8));
166
+
167
+ setAdjustedPosition({ x: left, y: top });
168
+ }, [isOpen, menuItems, position]);
169
+
170
+ // AICODE-NOTE: Close on click-outside, Escape, or scroll
171
+ useEffect(() => {
172
+ if (!isOpen) return;
173
+
174
+ const handleClickOutside = (event: MouseEvent) => {
175
+ if (menuRef.current && !menuRef.current.contains(event.target as Node)) {
176
+ onClose();
177
+ }
178
+ };
179
+
180
+ const handleKeyDown = (event: KeyboardEvent) => {
181
+ switch (event.key) {
182
+ case 'Escape':
183
+ event.preventDefault();
184
+ onClose();
185
+ break;
186
+ case 'ArrowDown': {
187
+ event.preventDefault();
188
+ const currentActionIdx = actionableIndices.indexOf(focusedIndex);
189
+ const nextIdx = currentActionIdx < actionableIndices.length - 1
190
+ ? actionableIndices[currentActionIdx + 1]
191
+ : actionableIndices[0];
192
+ if (nextIdx !== undefined) setFocusedIndex(nextIdx);
193
+ break;
194
+ }
195
+ case 'ArrowUp': {
196
+ event.preventDefault();
197
+ const currentActionIdx = actionableIndices.indexOf(focusedIndex);
198
+ const prevIdx = currentActionIdx > 0
199
+ ? actionableIndices[currentActionIdx - 1]
200
+ : actionableIndices[actionableIndices.length - 1];
201
+ if (prevIdx !== undefined) setFocusedIndex(prevIdx);
202
+ break;
203
+ }
204
+ case 'Enter': {
205
+ event.preventDefault();
206
+ const focused = menuItems[focusedIndex];
207
+ if (focused && focused.type !== 'separator' && !focused.disabled) {
208
+ handleMenuItemClick(focused);
209
+ }
210
+ break;
211
+ }
212
+ }
213
+ };
214
+
215
+ const handleScroll = () => onClose();
216
+
217
+ // Use capture phase for mousedown so we close before other handlers fire
218
+ document.addEventListener('mousedown', handleClickOutside, true);
219
+ document.addEventListener('keydown', handleKeyDown);
220
+ window.addEventListener('scroll', handleScroll, true);
221
+
222
+ return () => {
223
+ document.removeEventListener('mousedown', handleClickOutside, true);
224
+ document.removeEventListener('keydown', handleKeyDown);
225
+ window.removeEventListener('scroll', handleScroll, true);
226
+ };
227
+ }, [isOpen, onClose, focusedIndex, menuItems, actionableIndices]);
228
+
229
+ // AICODE-NOTE: Handle menu item click — dispatch to extra handler or provider and close
230
+ const handleMenuItemClick = useCallback((menuItem: ListContextMenuItem) => {
231
+ if (extraMenuItems?.some(m => m.type !== 'separator' && m.id === menuItem.id) && onExtraMenuAction) {
232
+ onExtraMenuAction(menuItem.id, items);
233
+ } else if (provider.onContextMenuAction) {
234
+ provider.onContextMenuAction(menuItem.id, items);
235
+ }
236
+ onClose();
237
+ }, [provider, items, onClose, extraMenuItems, onExtraMenuAction]);
238
+
239
+ if (!isOpen || menuItems.length === 0) {
240
+ return null;
241
+ }
242
+
243
+ return (
244
+ <div
245
+ ref={menuRef}
246
+ role="menu"
247
+ style={{
248
+ position: 'fixed',
249
+ left: adjustedPosition.x,
250
+ top: adjustedPosition.y,
251
+ zIndex: 50,
252
+ }}
253
+ className={[
254
+ 'bg-popover text-popover-foreground',
255
+ 'rounded-md border p-1 shadow-md',
256
+ 'min-w-[180px] max-w-[260px]',
257
+ 'animate-in fade-in-0 zoom-in-95',
258
+ 'origin-top-left',
259
+ ].join(' ')}
260
+ >
261
+ {menuItems.map((menuItem, index) => (
262
+ <ContextMenuItemRow
263
+ key={menuItem.type === 'separator' ? `sep-${index}` : menuItem.id}
264
+ menuItem={menuItem}
265
+ isFocused={focusedIndex === index}
266
+ onMenuItemClick={handleMenuItemClick}
267
+ onMouseEnter={() => setFocusedIndex(index)}
268
+ />
269
+ ))}
270
+ </div>
271
+ );
272
+ });
273
+
274
+ ListContextMenu.displayName = 'ListContextMenu';