@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,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;