@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,552 @@
1
+ import { makeAutoObservable, observable, runInAction } from 'mobx';
2
+ import type {
3
+ ListItemsProvider,
4
+ ListItemsProviderListener
5
+ } from '../../list/providers/ListItemsProvider';
6
+ import type {
7
+ ListItemData,
8
+ ListLoadOptions,
9
+ ListLoadResult,
10
+ ListViewType,
11
+ ListContextMenuItem,
12
+ ListSelectionInfo,
13
+ VirtualizationConfig,
14
+ ListDragDropInfo
15
+ } from '../../list/types/ListTypes';
16
+ import { LIST_VIEW_TYPE, GRID_VIEW_TYPE, DETAILS_VIEW_TYPE } from '../../list';
17
+ import { MASONRY_HORIZONTAL_VIEW_TYPE, MASONRY_VERTICAL_VIEW_TYPE, TREEMAP_VIEW_TYPE } from '../../list/providers/ListItemsProvider';
18
+ import { FileSystemBridge, type FileSystemItem } from '../adapters/FileSystemBridge';
19
+ import { getIconForFile } from '../../icons/iconMap';
20
+ import { ThumbnailCacheService } from '../services/ThumbnailCacheService';
21
+
22
+ interface FileSystemListProviderOptions {
23
+ showAllItems: boolean;
24
+ /** Service identifier for IndexedDB thumbnail cache key scoping */
25
+ serviceId?: string;
26
+ onSelectionChange: (items: ListItemData[]) => void;
27
+ onNavigation: (path: string) => void;
28
+ onNavigateUp?: () => void;
29
+ onOpenFile?: (path: string, name: string, size?: number) => void;
30
+ onPreviewFile?: (path: string, name: string, size?: number) => void;
31
+ onRefresh?: () => Promise<void>;
32
+ onRenameRequest?: (itemId: string, currentName: string, path: string, source: 'list' | 'tree') => void;
33
+ onDeleteRequest?: (targets: Array<{ path: string; isDirectory: boolean; name: string }>) => void;
34
+ onNewItemRequest?: (parentPath: string, type: 'file' | 'folder') => void;
35
+ }
36
+
37
+ export class FileSystemListProvider implements ListItemsProvider {
38
+ readonly id = 'filesystem-list';
39
+ readonly name = 'File System List';
40
+ readonly supportedViewTypes: ListViewType[] = [
41
+ LIST_VIEW_TYPE,
42
+ GRID_VIEW_TYPE,
43
+ DETAILS_VIEW_TYPE,
44
+ MASONRY_HORIZONTAL_VIEW_TYPE,
45
+ MASONRY_VERTICAL_VIEW_TYPE,
46
+ TREEMAP_VIEW_TYPE
47
+ ];
48
+ readonly isMultiSelectEnabled = true;
49
+ readonly isVirtualizationEnabled = false;
50
+ readonly isDragDropEnabled = true;
51
+
52
+ currentPath: string = '/';
53
+ private listeners: ListItemsProviderListener[] = [];
54
+
55
+ // Thumbnail blob URL cache: path → blob URL
56
+ thumbnailCache = observable.map<string, string>();
57
+ // Aspect ratio cache: path → width/height ratio
58
+ aspectRatioCache = observable.map<string, number>();
59
+ // Track in-flight loads to avoid duplicate requests
60
+ private thumbnailLoading = new Set<string>();
61
+
62
+ constructor(
63
+ private bridge: FileSystemBridge,
64
+ private options: FileSystemListProviderOptions
65
+ ) {
66
+ makeAutoObservable(this, {
67
+ bridge: false,
68
+ options: false,
69
+ listeners: false,
70
+ thumbnailLoading: false,
71
+ });
72
+ // Evict expired IndexedDB thumbnails on startup (non-blocking)
73
+ ThumbnailCacheService.evictOld();
74
+ }
75
+
76
+ /**
77
+ * Get a cached thumbnail blob URL, or trigger async load if not cached.
78
+ * Returns undefined while loading (MobX will re-render when cache updates).
79
+ */
80
+ getThumbnailBlobUrl(path: string): string | undefined {
81
+ const cached = this.thumbnailCache.get(path);
82
+ if (cached) return cached;
83
+
84
+ // Start async load if not already loading
85
+ if (!this.thumbnailLoading.has(path)) {
86
+ this.thumbnailLoading.add(path);
87
+ this.loadThumbnail(path);
88
+ }
89
+ return undefined;
90
+ }
91
+
92
+ private async loadThumbnail(path: string): Promise<void> {
93
+ const serviceId = this.options.serviceId ?? 'default';
94
+ try {
95
+ // Try IndexedDB cache first
96
+ const cached = await ThumbnailCacheService.get(serviceId, path);
97
+ if (cached) {
98
+ runInAction(() => {
99
+ this.thumbnailCache.set(path, cached.blobUrl);
100
+ this.aspectRatioCache.set(path, cached.aspectRatio);
101
+ });
102
+ return;
103
+ }
104
+
105
+ // Fall back to network load
106
+ const content = await this.bridge.readFile(path);
107
+ const ext = path.split('.').pop()?.toLowerCase() || '';
108
+ const mimeMap: Record<string, string> = {
109
+ jpg: 'image/jpeg', jpeg: 'image/jpeg', png: 'image/png',
110
+ gif: 'image/gif', svg: 'image/svg+xml', webp: 'image/webp'
111
+ };
112
+ const mime = mimeMap[ext] || 'image/png';
113
+ const rawData = typeof content === 'string' ? new TextEncoder().encode(content) : content;
114
+ const blob = new Blob([rawData], { type: mime });
115
+ const url = URL.createObjectURL(blob);
116
+ runInAction(() => {
117
+ this.thumbnailCache.set(path, url);
118
+ });
119
+ // Measure actual image dimensions for masonry aspect ratios
120
+ const img = new Image();
121
+ img.src = url;
122
+ img.onload = () => {
123
+ if (img.naturalWidth > 0 && img.naturalHeight > 0) {
124
+ const aspectRatio = img.naturalWidth / img.naturalHeight;
125
+ runInAction(() => {
126
+ this.aspectRatioCache.set(path, aspectRatio);
127
+ });
128
+ // Persist to IndexedDB for future page loads
129
+ const buffer = rawData instanceof ArrayBuffer ? rawData : (rawData as Uint8Array).buffer;
130
+ ThumbnailCacheService.put(serviceId, path, buffer, mime, aspectRatio);
131
+ }
132
+ };
133
+ } catch (error) {
134
+ console.warn('Failed to load thumbnail:', path, error);
135
+ } finally {
136
+ this.thumbnailLoading.delete(path);
137
+ }
138
+ }
139
+
140
+ /**
141
+ * Get cached aspect ratio for an image path.
142
+ * Returns undefined if not yet measured.
143
+ */
144
+ getAspectRatio(path: string): number | undefined {
145
+ return this.aspectRatioCache.get(path);
146
+ }
147
+
148
+ /**
149
+ * Revoke all cached blob URLs (call on path change / unmount)
150
+ */
151
+ clearThumbnailCache(): void {
152
+ for (const url of this.thumbnailCache.values()) {
153
+ URL.revokeObjectURL(url);
154
+ }
155
+ this.thumbnailCache.clear();
156
+ this.aspectRatioCache.clear();
157
+ this.thumbnailLoading.clear();
158
+ }
159
+
160
+ setPath(path: string) {
161
+ this.currentPath = path;
162
+ this.clearThumbnailCache();
163
+ }
164
+
165
+ async loadItems(options?: ListLoadOptions): Promise<ListLoadResult> {
166
+ const items = await this.bridge.loadDirectoryContents(this.currentPath);
167
+
168
+ const allItems: ListItemData[] = items.map(item => ({
169
+ id: item.id,
170
+ name: item.name,
171
+ type: item.type,
172
+ path: item.path,
173
+ size: item.size,
174
+ modified: item.modified,
175
+ isDirectory: item.isDirectory,
176
+ metadata: item,
177
+ thumbnailUrl: this.getThumbnailUrl(item),
178
+ icon: this.getFileIconString(item)
179
+ }));
180
+
181
+ const totalCount = allItems.length;
182
+ const offset = options?.offset ?? 0;
183
+ const limit = options?.limit ?? totalCount;
184
+ const paginatedItems = allItems.slice(offset, offset + limit);
185
+
186
+ return {
187
+ items: paginatedItems,
188
+ totalCount,
189
+ hasMore: offset + limit < totalCount
190
+ };
191
+ }
192
+
193
+ async getItemCount(): Promise<number> {
194
+ const result = await this.loadItems();
195
+ return result.totalCount || 0;
196
+ }
197
+
198
+ async loadItemRange(start: number, end: number): Promise<ListLoadResult> {
199
+ const result = await this.loadItems();
200
+ const rangeItems = result.items.slice(start, end);
201
+
202
+ return {
203
+ items: rangeItems,
204
+ totalCount: result.totalCount
205
+ };
206
+ }
207
+
208
+ async refresh(): Promise<ListLoadResult> {
209
+ const result = await this.loadItems();
210
+ this.listeners.forEach(listener => {
211
+ listener.onItemsCountChanged(result.totalCount || 0);
212
+ });
213
+ return result;
214
+ }
215
+
216
+ getItemHeight(viewType: ListViewType): number {
217
+ switch (viewType.id) {
218
+ case 'list': return 32;
219
+ case 'grid': return 200;
220
+ case 'details': return 32;
221
+ case 'masonry-horizontal': return 160;
222
+ default: return 40;
223
+ }
224
+ }
225
+
226
+ getItemWidth(viewType: ListViewType): number {
227
+ switch (viewType.id) {
228
+ case 'grid': return 240;
229
+ case 'masonry-horizontal': return 200;
230
+ default: return 0;
231
+ }
232
+ }
233
+
234
+ getVirtualizationConfig(): VirtualizationConfig {
235
+ return {
236
+ itemHeight: 40,
237
+ overscan: 5,
238
+ threshold: 50
239
+ };
240
+ }
241
+
242
+ onSelectionChange = (selectionInfo: ListSelectionInfo) => {
243
+ this.options.onSelectionChange(selectionInfo.selectedItems);
244
+
245
+ // Trigger preview on single file selection
246
+ if (
247
+ selectionInfo.selectedItems.length === 1 &&
248
+ selectionInfo.selectionType === 'single' &&
249
+ this.options.onPreviewFile
250
+ ) {
251
+ const item = selectionInfo.selectedItems[0]!;
252
+ if (!item.isDirectory) {
253
+ const fsItem = item.metadata as FileSystemItem;
254
+ this.options.onPreviewFile(item.path, fsItem.name, fsItem.size);
255
+ }
256
+ }
257
+ };
258
+
259
+ onViewTypeChange?(viewType: ListViewType): void {}
260
+
261
+ async loadTreemapData(onProgress?: (progress: import('../../list/providers/ListItemsProvider').TreemapScanProgress) => void) {
262
+ const items = await this.bridge.loadDirectoryRecursive(this.currentPath, 3, onProgress);
263
+ const convert = (fsItems: FileSystemItem[]): import('../../list/providers/ListItemsProvider').TreemapNodeData[] =>
264
+ fsItems.map(item => ({
265
+ name: item.name,
266
+ path: item.path,
267
+ size: item.size,
268
+ isDirectory: item.isDirectory,
269
+ children: item.children ? convert(item.children) : undefined,
270
+ }));
271
+ return convert(items);
272
+ }
273
+
274
+ onItemDoubleClick = (item: ListItemData) => {
275
+ if (item.isDirectory) {
276
+ this.options.onNavigation(item.path);
277
+ } else {
278
+ this.openFileWithViewer(item);
279
+ }
280
+ };
281
+
282
+ addListener(listener: ListItemsProviderListener): void {
283
+ this.listeners.push(listener);
284
+ }
285
+
286
+ removeListener(listener: ListItemsProviderListener): void {
287
+ const index = this.listeners.indexOf(listener);
288
+ if (index >= 0) {
289
+ this.listeners.splice(index, 1);
290
+ }
291
+ }
292
+
293
+ getItemIcon(item: ListItemData): string {
294
+ const fsItem = item.metadata as FileSystemItem;
295
+ return this.getFileIconString(fsItem);
296
+ }
297
+
298
+ getItemContextMenu(item: ListItemData): ListContextMenuItem[] {
299
+ const baseMenu: ListContextMenuItem[] = [
300
+ {
301
+ id: 'open',
302
+ label: item.isDirectory ? 'Open Folder' : 'Open',
303
+ icon: item.isDirectory ? 'folder-open' : 'eye',
304
+ type: 'item'
305
+ }
306
+ ];
307
+
308
+ if (!item.isDirectory) {
309
+ baseMenu.push(
310
+ { id: 'separator1', type: 'separator', label: '' },
311
+ { id: 'download', label: 'Download', icon: 'download', type: 'item' }
312
+ );
313
+ }
314
+
315
+ if (item.isDirectory) {
316
+ baseMenu.push(
317
+ { id: 'separator-new', type: 'separator', label: '' },
318
+ { id: 'new-folder', label: 'New Folder', icon: 'folder-plus', type: 'item' },
319
+ { id: 'new-file', label: 'New File', icon: 'file-plus', type: 'item' }
320
+ );
321
+ }
322
+
323
+ baseMenu.push(
324
+ { id: 'separator2', type: 'separator', label: '' },
325
+ { id: 'copy-path', label: 'Copy Path', icon: 'copy', type: 'item' },
326
+ { id: 'rename', label: 'Rename', icon: 'edit', type: 'item', shortcut: 'F2' },
327
+ { id: 'separator3', type: 'separator', label: '' },
328
+ { id: 'delete', label: 'Delete', icon: 'trash', type: 'item', destructive: true, shortcut: 'Del' }
329
+ );
330
+
331
+ return baseMenu;
332
+ }
333
+
334
+ getEmptySpaceContextMenu(): ListContextMenuItem[] {
335
+ return [
336
+ { id: 'new-folder', label: 'New Folder', icon: 'folder-plus', type: 'item' },
337
+ { id: 'new-file', label: 'New File', icon: 'file-plus', type: 'item' }
338
+ ];
339
+ }
340
+
341
+ getMultiItemContextMenu(items: ListItemData[]): ListContextMenuItem[] {
342
+ const hasFiles = items.some(i => !i.isDirectory);
343
+ const menu: ListContextMenuItem[] = [];
344
+
345
+ if (hasFiles) {
346
+ menu.push(
347
+ { id: 'download-multiple', label: `Download ${items.filter(i => !i.isDirectory).length} Files`, icon: 'download', type: 'item' }
348
+ );
349
+ }
350
+
351
+ menu.push(
352
+ { id: 'copy-path', label: 'Copy Paths', icon: 'copy', type: 'item' },
353
+ { id: 'separator-del', type: 'separator', label: '' },
354
+ { id: 'delete-multiple', label: `Delete ${items.length} Items`, icon: 'trash', type: 'item', destructive: true, shortcut: 'Del' }
355
+ );
356
+
357
+ return menu;
358
+ }
359
+
360
+ onContextMenuAction = (menuItemId: string, items: ListItemData[]) => {
361
+ if (items.length === 0) {
362
+ switch (menuItemId) {
363
+ case 'new-folder':
364
+ this.requestNewItem(this.currentPath, 'folder');
365
+ break;
366
+ case 'new-file':
367
+ this.requestNewItem(this.currentPath, 'file');
368
+ break;
369
+ }
370
+ return;
371
+ }
372
+
373
+ const item = items[0];
374
+ if (!item) return;
375
+
376
+ switch (menuItemId) {
377
+ case 'open':
378
+ this.onItemDoubleClick(item);
379
+ break;
380
+ case 'download':
381
+ this.downloadFile(item.path);
382
+ break;
383
+ case 'download-multiple':
384
+ items.filter(i => !i.isDirectory).forEach(i => this.downloadFile(i.path));
385
+ break;
386
+ case 'copy-path':
387
+ this.copyPaths(items);
388
+ break;
389
+ case 'rename':
390
+ this.requestRename(item);
391
+ break;
392
+ case 'delete':
393
+ this.requestDelete([item]);
394
+ break;
395
+ case 'delete-multiple':
396
+ this.requestDelete(items);
397
+ break;
398
+ case 'new-folder':
399
+ this.requestNewItem(item.isDirectory ? item.path : this.currentPath, 'folder');
400
+ break;
401
+ case 'new-file':
402
+ this.requestNewItem(item.isDirectory ? item.path : this.currentPath, 'file');
403
+ break;
404
+ }
405
+ }
406
+
407
+ canDragItem?(item: ListItemData): boolean {
408
+ return true;
409
+ }
410
+
411
+ canDropItems?(draggedItems: ListItemData[], targetItem: ListItemData | null, position: 'before' | 'after' | 'inside'): boolean {
412
+ // Can only drop into directories
413
+ if (!targetItem?.isDirectory) return false;
414
+ if (position !== 'inside') return false;
415
+ // Cannot drop a folder into itself or its own children
416
+ for (const item of draggedItems) {
417
+ if (targetItem.path === item.path) return false;
418
+ if (targetItem.path.startsWith(item.path + '/')) return false;
419
+ }
420
+ return true;
421
+ }
422
+
423
+ onDragStart?(draggedItems: ListItemData[], event: DragEvent): void {}
424
+
425
+ onDrop?(dragDropInfo: ListDragDropInfo): void {
426
+ const { draggedItems, targetItem } = dragDropInfo;
427
+ if (!targetItem?.isDirectory) return;
428
+
429
+ // Move each dragged item into the target directory
430
+ const moves = draggedItems.map(async (item) => {
431
+ const name = item.path.split('/').pop() || '';
432
+ const newPath = `${targetItem.path}/${name}`.replace(/\/\//g, '/');
433
+ try {
434
+ await this.bridge.renameItem(item.path, newPath);
435
+ } catch (e) {
436
+ console.error(`Failed to move ${item.path} → ${newPath}:`, e);
437
+ }
438
+ });
439
+
440
+ Promise.all(moves).then(() => this.options.onRefresh?.());
441
+ }
442
+
443
+ onDragEnd?(draggedItems: ListItemData[], success: boolean): void {}
444
+
445
+ resolveThumbnailUrl(item: ListItemData): string | undefined {
446
+ if (item.type === 'file' && this.isImageFile(item.name)) {
447
+ return this.getThumbnailBlobUrl(item.path);
448
+ }
449
+ return undefined;
450
+ }
451
+
452
+ private getThumbnailUrl(item: FileSystemItem): string | undefined {
453
+ // Return path as marker that this item has a thumbnail.
454
+ // Actual blob URL resolved via resolveThumbnailUrl/thumbnailCache.
455
+ if (item.type === 'file' && this.isImageFile(item.name)) {
456
+ return item.path;
457
+ }
458
+ return undefined;
459
+ }
460
+
461
+ private getFileIconString(item: FileSystemItem): string {
462
+ return getIconForFile(item.name, item.isDirectory);
463
+ }
464
+
465
+ private isImageFile(filename: string): boolean {
466
+ const ext = filename.split('.').pop()?.toLowerCase();
467
+ return ['jpg', 'jpeg', 'png', 'gif', 'svg', 'webp'].includes(ext || '');
468
+ }
469
+
470
+ private openFileWithViewer(item: ListItemData): void {
471
+ const fsItem = item.metadata as FileSystemItem;
472
+ if (this.options.onOpenFile) {
473
+ this.options.onOpenFile(item.path, fsItem.name, fsItem.size);
474
+ }
475
+ }
476
+
477
+ private copyPaths(items: ListItemData[]) {
478
+ const paths = items.map(i => i.path).join('\n');
479
+ navigator.clipboard.writeText(paths).catch((err) => {
480
+ console.error('Failed to copy paths:', err);
481
+ });
482
+ }
483
+
484
+ private async downloadFile(path: string) {
485
+ try {
486
+ const content = await this.bridge.readFile(path);
487
+ const filename = path.split('/').pop() || 'download';
488
+ const blob = new Blob(
489
+ [typeof content === 'string' ? content : content],
490
+ { type: 'application/octet-stream' }
491
+ );
492
+ const url = URL.createObjectURL(blob);
493
+ const a = document.createElement('a');
494
+ a.href = url;
495
+ a.download = filename;
496
+ document.body.appendChild(a);
497
+ a.click();
498
+ document.body.removeChild(a);
499
+ URL.revokeObjectURL(url);
500
+ } catch (error) {
501
+ console.error('Download failed:', error);
502
+ }
503
+ }
504
+
505
+ private requestRename(item: ListItemData) {
506
+ if (this.options.onRenameRequest) {
507
+ const currentName = item.path.split('/').pop() || '';
508
+ this.options.onRenameRequest(item.id, currentName, item.path, 'list');
509
+ }
510
+ }
511
+
512
+ private requestDelete(items: ListItemData[]) {
513
+ if (this.options.onDeleteRequest) {
514
+ const targets = items.map(item => ({
515
+ path: item.path,
516
+ isDirectory: Boolean(item.isDirectory),
517
+ name: item.path.split('/').pop() || ''
518
+ }));
519
+ this.options.onDeleteRequest(targets);
520
+ }
521
+ }
522
+
523
+ private requestNewItem(parentPath: string, type: 'file' | 'folder') {
524
+ if (this.options.onNewItemRequest) {
525
+ this.options.onNewItemRequest(parentPath, type);
526
+ }
527
+ }
528
+
529
+ async executeRename(path: string, newName: string): Promise<void> {
530
+ const parentPath = path.split('/').slice(0, -1).join('/') || '/';
531
+ const newPath = `${parentPath}/${newName}`.replace(/\/\//g, '/');
532
+ await this.bridge.renameItem(path, newPath);
533
+ await this.options.onRefresh?.();
534
+ }
535
+
536
+ async executeDelete(targets: Array<{ path: string; isDirectory: boolean }>): Promise<void> {
537
+ await Promise.all(targets.map(t =>
538
+ this.bridge.deleteItem(t.path, t.isDirectory)
539
+ ));
540
+ await this.options.onRefresh?.();
541
+ }
542
+
543
+ async executeCreate(parentPath: string, name: string, type: 'file' | 'folder'): Promise<void> {
544
+ const newPath = `${parentPath}/${name}`.replace(/\/\//g, '/');
545
+ if (type === 'folder') {
546
+ await this.bridge.createDirectory(newPath);
547
+ } else {
548
+ await this.bridge.writeFile(newPath, '');
549
+ }
550
+ await this.options.onRefresh?.();
551
+ }
552
+ }