@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,484 @@
1
+ import React from 'react';
2
+ import { observer } from 'mobx-react-lite';
3
+ import { ChevronUp, ChevronDown, MoreVertical } from 'lucide-react';
4
+ import { cn } from '../../../../lib/utils';
5
+ import { FileBrowserItem } from '../../../types/FileBrowserTypes';
6
+ import { ListViewUIModel, ColumnDefinition } from '../../../models/ui/ListViewUIModel';
7
+ import { ResponsiveLayoutManagerModel } from '../../../models/ResponsiveLayoutManagerModel';
8
+ import FileIcon from '../../shared/FileIcon';
9
+
10
+ export interface ListViewProps {
11
+ items: FileBrowserItem[];
12
+ listModel: ListViewUIModel;
13
+ responsiveManager?: ResponsiveLayoutManagerModel;
14
+ onItemClick?: (item: FileBrowserItem) => void;
15
+ onItemDoubleClick?: (item: FileBrowserItem) => void;
16
+ onItemActivate?: (item: FileBrowserItem) => void;
17
+ onSelectionChange?: (selectedItems: FileBrowserItem[]) => void;
18
+ selectedItemIds?: Set<string>;
19
+ focusedItemId?: string;
20
+ className?: string;
21
+ getItemIcon?: (item: FileBrowserItem) => any;
22
+ }
23
+
24
+ const ListViewHeader: React.FC<{
25
+ columns: ColumnDefinition[];
26
+ listModel: ListViewUIModel;
27
+ className?: string;
28
+ }> = observer(({ columns, listModel, className }) => {
29
+ const handleColumnClick = (column: ColumnDefinition) => {
30
+ if (column.sortable) {
31
+ listModel.toggleSort(column.field);
32
+ }
33
+ };
34
+
35
+ const handleColumnResize = (columnId: string, newWidth: number) => {
36
+ listModel.resizeColumn(columnId, newWidth);
37
+ };
38
+
39
+ return (
40
+ <div
41
+ className={cn(
42
+ 'flex border-b bg-muted/30 sticky top-0 z-10',
43
+ listModel.showHeader ? 'block' : 'hidden',
44
+ className
45
+ )}
46
+ role="row"
47
+ >
48
+ {columns.map((column) => {
49
+ const isSorted = listModel.currentSort?.field === column.field;
50
+ const sortDirection = isSorted ? listModel.currentSort?.direction : undefined;
51
+
52
+ return (
53
+ <div
54
+ key={column.id}
55
+ className={cn(
56
+ 'flex items-center gap-2 px-3 py-2 text-sm font-medium border-r border-border last:border-r-0',
57
+ 'select-none transition-colors',
58
+ column.sortable && 'hover:bg-muted/50 cursor-pointer',
59
+ column.align === 'center' && 'justify-center',
60
+ column.align === 'right' && 'justify-end'
61
+ )}
62
+ style={{
63
+ width: column.width,
64
+ minWidth: column.minWidth,
65
+ maxWidth: column.maxWidth,
66
+ }}
67
+ onClick={() => handleColumnClick(column)}
68
+ role="columnheader"
69
+ aria-sort={
70
+ isSorted
71
+ ? sortDirection === 'asc' ? 'ascending' : 'descending'
72
+ : undefined
73
+ }
74
+ >
75
+ <span className="truncate">{column.label}</span>
76
+
77
+ {column.sortable && (
78
+ <div className="flex flex-col">
79
+ {isSorted && sortDirection === 'asc' && (
80
+ <ChevronUp className="w-3 h-3" />
81
+ )}
82
+ {isSorted && sortDirection === 'desc' && (
83
+ <ChevronDown className="w-3 h-3" />
84
+ )}
85
+ </div>
86
+ )}
87
+ </div>
88
+ );
89
+ })}
90
+ </div>
91
+ );
92
+ });
93
+
94
+ // Mobile-optimized row component
95
+ const MobileListViewRow: React.FC<{
96
+ item: FileBrowserItem;
97
+ isSelected: boolean;
98
+ isFocused: boolean;
99
+ onItemClick?: (item: FileBrowserItem) => void;
100
+ onItemDoubleClick?: (item: FileBrowserItem) => void;
101
+ getItemIcon?: (item: FileBrowserItem) => any;
102
+ index: number;
103
+ }> = observer(({
104
+ item,
105
+ isSelected,
106
+ isFocused,
107
+ onItemClick,
108
+ onItemDoubleClick,
109
+ getItemIcon,
110
+ index,
111
+ }) => {
112
+ const handleClick = () => {
113
+ onItemClick?.(item);
114
+ };
115
+
116
+ const handleDoubleClick = () => {
117
+ onItemDoubleClick?.(item);
118
+ };
119
+
120
+ const formatSize = (size: number): string => {
121
+ if (size === 0) return '0 B';
122
+ const units = ['B', 'KB', 'MB', 'GB'];
123
+ let fileSize = size;
124
+ let unitIndex = 0;
125
+ while (fileSize >= 1024 && unitIndex < units.length - 1) {
126
+ fileSize /= 1024;
127
+ unitIndex++;
128
+ }
129
+ return `${fileSize.toFixed(1)} ${units[unitIndex]}`;
130
+ };
131
+
132
+ const formatDate = (date: Date): string => {
133
+ return date.toLocaleDateString(undefined, {
134
+ month: 'short',
135
+ day: 'numeric',
136
+ year: date.getFullYear() !== new Date().getFullYear() ? 'numeric' : undefined
137
+ });
138
+ };
139
+
140
+ return (
141
+ <div
142
+ className={cn(
143
+ // Touch-friendly height (minimum 44px)
144
+ 'flex items-center gap-3 p-4 border-b border-border',
145
+ 'hover:bg-muted/30 active:bg-muted/50 transition-colors cursor-pointer',
146
+ 'focus:outline-none focus:ring-2 focus:ring-primary focus:ring-inset',
147
+ 'min-h-[44px]', // Ensure touch-friendly target size
148
+ isSelected && 'bg-primary/10 text-primary',
149
+ isFocused && 'ring-2 ring-primary ring-inset'
150
+ )}
151
+ onClick={handleClick}
152
+ onDoubleClick={handleDoubleClick}
153
+ tabIndex={0}
154
+ role="row"
155
+ aria-selected={isSelected}
156
+ >
157
+ {/* Icon */}
158
+ <div className="flex-shrink-0">
159
+ <FileIcon
160
+ item={item}
161
+ getItemIcon={getItemIcon}
162
+ size="md"
163
+ />
164
+ </div>
165
+
166
+ {/* Main content */}
167
+ <div className="flex-1 min-w-0">
168
+ <div className="flex items-center justify-between">
169
+ <div className="min-w-0 flex-1">
170
+ <p className="text-sm font-medium truncate" title={item.name}>
171
+ {item.name}
172
+ </p>
173
+ <div className="flex items-center gap-2 text-xs text-muted-foreground mt-1">
174
+ <span>{item.type === 'directory' ? 'Folder' : 'File'}</span>
175
+ {item.size !== undefined && item.type !== 'directory' && (
176
+ <>
177
+ <span>•</span>
178
+ <span>{formatSize(item.size)}</span>
179
+ </>
180
+ )}
181
+ {item.lastModified && (
182
+ <>
183
+ <span>•</span>
184
+ <span>{formatDate(item.lastModified)}</span>
185
+ </>
186
+ )}
187
+ </div>
188
+ </div>
189
+
190
+ {/* Action button */}
191
+ <button
192
+ className={cn(
193
+ 'flex-shrink-0 w-8 h-8 flex items-center justify-center',
194
+ 'rounded-md hover:bg-muted/50 transition-colors',
195
+ 'focus:outline-none focus:ring-2 focus:ring-primary'
196
+ )}
197
+ onClick={(e) => {
198
+ e.stopPropagation();
199
+ // Handle context menu or actions
200
+ }}
201
+ aria-label="More actions"
202
+ >
203
+ <MoreVertical className="w-4 h-4" />
204
+ </button>
205
+ </div>
206
+ </div>
207
+ </div>
208
+ );
209
+ });
210
+
211
+ const ListViewRow: React.FC<{
212
+ item: FileBrowserItem;
213
+ columns: ColumnDefinition[];
214
+ isSelected: boolean;
215
+ isFocused: boolean;
216
+ onItemClick?: (item: FileBrowserItem) => void;
217
+ onItemDoubleClick?: (item: FileBrowserItem) => void;
218
+ getItemIcon?: (item: FileBrowserItem) => any;
219
+ striped?: boolean;
220
+ index: number;
221
+ }> = observer(({
222
+ item,
223
+ columns,
224
+ isSelected,
225
+ isFocused,
226
+ onItemClick,
227
+ onItemDoubleClick,
228
+ getItemIcon,
229
+ striped = false,
230
+ index,
231
+ }) => {
232
+ const handleClick = () => {
233
+ onItemClick?.(item);
234
+ };
235
+
236
+ const handleDoubleClick = () => {
237
+ onItemDoubleClick?.(item);
238
+ };
239
+
240
+ const handleKeyDown = (e: React.KeyboardEvent) => {
241
+ switch (e.key) {
242
+ case 'Enter':
243
+ case ' ':
244
+ e.preventDefault();
245
+ handleClick();
246
+ break;
247
+ }
248
+ };
249
+
250
+ const getCellValue = (column: ColumnDefinition): string => {
251
+ const value = (item as any)[column.field];
252
+
253
+ if (column.format && value !== undefined && value !== null) {
254
+ return column.format(value);
255
+ }
256
+
257
+ // Default formatting for common fields
258
+ switch (column.field) {
259
+ case 'size':
260
+ if (typeof value === 'number') {
261
+ if (value === 0) return '0 B';
262
+ const units = ['B', 'KB', 'MB', 'GB'];
263
+ let size = value;
264
+ let unitIndex = 0;
265
+ while (size >= 1024 && unitIndex < units.length - 1) {
266
+ size /= 1024;
267
+ unitIndex++;
268
+ }
269
+ return `${size.toFixed(1)} ${units[unitIndex]}`;
270
+ }
271
+ return '-';
272
+ case 'lastModified':
273
+ if (value instanceof Date) {
274
+ return value.toLocaleDateString();
275
+ }
276
+ return '-';
277
+ case 'type':
278
+ return item.type === 'directory' ? 'Folder' : 'File';
279
+ default:
280
+ return value?.toString() || '-';
281
+ }
282
+ };
283
+
284
+ return (
285
+ <div
286
+ className={cn(
287
+ 'flex border-b border-border hover:bg-muted/30 transition-colors cursor-pointer',
288
+ 'focus:outline-none focus:ring-2 focus:ring-primary focus:ring-inset',
289
+ isSelected && 'bg-primary/10 text-primary',
290
+ isFocused && 'ring-2 ring-primary ring-inset',
291
+ striped && index % 2 === 1 && 'bg-muted/20'
292
+ )}
293
+ onClick={handleClick}
294
+ onDoubleClick={handleDoubleClick}
295
+ onKeyDown={handleKeyDown}
296
+ tabIndex={0}
297
+ role="row"
298
+ aria-selected={isSelected}
299
+ >
300
+ {columns.map((column, columnIndex) => (
301
+ <div
302
+ key={column.id}
303
+ className={cn(
304
+ 'flex items-center gap-2 px-3 py-2 text-sm border-r border-border last:border-r-0',
305
+ 'truncate',
306
+ column.align === 'center' && 'justify-center',
307
+ column.align === 'right' && 'justify-end'
308
+ )}
309
+ style={{
310
+ width: column.width,
311
+ minWidth: column.minWidth,
312
+ maxWidth: column.maxWidth,
313
+ }}
314
+ role="cell"
315
+ >
316
+ {/* Show icon only in first column (usually name) */}
317
+ {columnIndex === 0 && (
318
+ <FileIcon
319
+ item={item}
320
+ getItemIcon={getItemIcon}
321
+ size="sm"
322
+ />
323
+ )}
324
+
325
+ <span className="truncate" title={getCellValue(column)}>
326
+ {getCellValue(column)}
327
+ </span>
328
+ </div>
329
+ ))}
330
+ </div>
331
+ );
332
+ });
333
+
334
+ const ListView: React.FC<ListViewProps> = observer(({
335
+ items,
336
+ listModel,
337
+ responsiveManager,
338
+ onItemClick,
339
+ onItemDoubleClick,
340
+ onItemActivate,
341
+ onSelectionChange,
342
+ selectedItemIds = new Set<string>(),
343
+ focusedItemId,
344
+ className,
345
+ getItemIcon,
346
+ }) => {
347
+ const visibleColumns = listModel.visibleColumns;
348
+ const isMobile = responsiveManager?.isMobile ?? false;
349
+
350
+ // On mobile, use simplified columns or mobile layout
351
+ const effectiveColumns = isMobile
352
+ ? visibleColumns.filter(col => ['name'].includes(col.field)) // Only show name on mobile
353
+ : visibleColumns;
354
+
355
+ const handleItemClick = (item: FileBrowserItem) => {
356
+ onItemClick?.(item);
357
+ };
358
+
359
+ const handleItemDoubleClick = (item: FileBrowserItem) => {
360
+ onItemDoubleClick?.(item);
361
+ onItemActivate?.(item);
362
+ };
363
+
364
+ const sortedItems = React.useMemo(() => {
365
+ if (!listModel.currentSort) {
366
+ return items;
367
+ }
368
+
369
+ const { field, direction } = listModel.currentSort;
370
+
371
+ return [...items].sort((a, b) => {
372
+ const aValue = (a as any)[field];
373
+ const bValue = (b as any)[field];
374
+
375
+ // Handle null/undefined values
376
+ if (aValue == null && bValue == null) return 0;
377
+ if (aValue == null) return direction === 'asc' ? -1 : 1;
378
+ if (bValue == null) return direction === 'asc' ? 1 : -1;
379
+
380
+ // Type-specific sorting
381
+ if (typeof aValue === 'string' && typeof bValue === 'string') {
382
+ const result = aValue.localeCompare(bValue);
383
+ return direction === 'asc' ? result : -result;
384
+ }
385
+
386
+ if (typeof aValue === 'number' && typeof bValue === 'number') {
387
+ const result = aValue - bValue;
388
+ return direction === 'asc' ? result : -result;
389
+ }
390
+
391
+ if (aValue instanceof Date && bValue instanceof Date) {
392
+ const result = aValue.getTime() - bValue.getTime();
393
+ return direction === 'asc' ? result : -result;
394
+ }
395
+
396
+ // Fallback to string comparison
397
+ const result = String(aValue).localeCompare(String(bValue));
398
+ return direction === 'asc' ? result : -result;
399
+ });
400
+ }, [items, listModel.currentSort]);
401
+
402
+ if (items.length === 0) {
403
+ return (
404
+ <div className={cn('p-4 text-center text-muted-foreground', className)}>
405
+ <p className="text-sm">No files to display</p>
406
+ </div>
407
+ );
408
+ }
409
+
410
+ if (!isMobile && effectiveColumns.length === 0) {
411
+ return (
412
+ <div className={cn('p-4 text-center text-muted-foreground', className)}>
413
+ <p className="text-sm">No columns configured</p>
414
+ <button
415
+ onClick={() => listModel.resetToDefaults()}
416
+ className="mt-2 px-3 py-1 bg-primary text-primary-foreground rounded-md text-sm"
417
+ >
418
+ Reset to Defaults
419
+ </button>
420
+ </div>
421
+ );
422
+ }
423
+
424
+ // Mobile layout
425
+ if (isMobile) {
426
+ return (
427
+ <div
428
+ className={cn('list-view h-full overflow-hidden flex flex-col', className)}
429
+ role="list"
430
+ aria-label="File list view (mobile)"
431
+ >
432
+ <div className="flex-1 overflow-auto">
433
+ {sortedItems.map((item, index) => (
434
+ <MobileListViewRow
435
+ key={item.id}
436
+ item={item}
437
+ isSelected={selectedItemIds.has(item.id)}
438
+ isFocused={focusedItemId === item.id}
439
+ onItemClick={handleItemClick}
440
+ onItemDoubleClick={handleItemDoubleClick}
441
+ getItemIcon={getItemIcon}
442
+ index={index}
443
+ />
444
+ ))}
445
+ </div>
446
+ </div>
447
+ );
448
+ }
449
+
450
+ // Desktop layout
451
+ return (
452
+ <div
453
+ className={cn('list-view h-full overflow-hidden flex flex-col', className)}
454
+ role="grid"
455
+ aria-label="File list view"
456
+ >
457
+ <ListViewHeader
458
+ columns={effectiveColumns}
459
+ listModel={listModel}
460
+ />
461
+
462
+ <div className="flex-1 overflow-auto">
463
+ {sortedItems.map((item, index) => (
464
+ <ListViewRow
465
+ key={item.id}
466
+ item={item}
467
+ columns={effectiveColumns}
468
+ isSelected={selectedItemIds.has(item.id)}
469
+ isFocused={focusedItemId === item.id}
470
+ onItemClick={handleItemClick}
471
+ onItemDoubleClick={handleItemDoubleClick}
472
+ getItemIcon={getItemIcon}
473
+ striped={listModel.striped}
474
+ index={index}
475
+ />
476
+ ))}
477
+ </div>
478
+ </div>
479
+ );
480
+ });
481
+
482
+ ListView.displayName = 'ListView';
483
+
484
+ export default ListView;