@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,623 @@
1
+ import { makeAutoObservable, runInAction } from 'mobx';
2
+ import { TreeModel } from '../../tree';
3
+ import { ListItemsModel } from '../../list';
4
+ import type { IFileSystem } from '@anymux/file-system';
5
+ import { FileSystemBridge } from '../adapters/FileSystemBridge';
6
+ import { FileSystemTreeProvider } from '../providers/FileSystemTreeProvider';
7
+ import { FileSystemListProvider } from '../providers/FileSystemListProvider';
8
+ import type { ListItemData } from '../../list/types/ListTypes';
9
+ import type { TreeNodeData } from '../../tree/types/TreeTypes';
10
+ import { globalViewerRegistry, type ViewerConfig, UnsupportedFileViewer } from '../viewers';
11
+ import { UploadModel } from './UploadModel';
12
+
13
+ export interface OpenFileState {
14
+ path: string;
15
+ name: string;
16
+ content?: string | ArrayBuffer;
17
+ size?: number;
18
+ mimeType?: string;
19
+ viewer: ViewerConfig;
20
+ }
21
+
22
+ export interface PreviewFileState {
23
+ path: string;
24
+ name: string;
25
+ content?: string | ArrayBuffer;
26
+ size?: number;
27
+ mimeType?: string;
28
+ viewer: ViewerConfig;
29
+ isLoading: boolean;
30
+ }
31
+
32
+ export class FileBrowserModel {
33
+ // File system integration
34
+ fileSystem: IFileSystem;
35
+ fileSystemBridge: FileSystemBridge;
36
+
37
+ // Component providers
38
+ treeProvider: FileSystemTreeProvider;
39
+ listProvider: FileSystemListProvider;
40
+
41
+ // Component models
42
+ treeModel: TreeModel;
43
+ listModel: ListItemsModel;
44
+
45
+ // Upload model
46
+ uploadModel: UploadModel;
47
+
48
+ // Synchronized state
49
+ currentPath: string = '/';
50
+ selectedPath: string | null = null;
51
+
52
+ // UI state
53
+ isLoading: boolean = false;
54
+ error: string | null = null;
55
+
56
+ // Callback for path changes (URL sync)
57
+ onPathChange?: (path: string) => void;
58
+
59
+ // Notification callbacks (wired by consuming code to toast/notifications)
60
+ onNotify?: (type: 'success' | 'error' | 'warning', message: string) => void;
61
+ /** Structured action callback with undo support */
62
+ onAction?: (action: { type: 'create' | 'rename' | 'delete' | 'upload'; message: string; undo?: () => Promise<void> }) => void;
63
+
64
+ // Viewer state
65
+ openFile: OpenFileState | null = null;
66
+ isLoadingFile: boolean = false;
67
+
68
+ // Preview pane state
69
+ previewEnabled: boolean = false;
70
+ previewFile: PreviewFileState | null = null;
71
+
72
+ // Dialog state for rename/delete/create operations
73
+ renameState: { itemId: string; currentName: string; path: string; source: 'list' | 'tree' } | null = null;
74
+ deleteState: { targets: Array<{ path: string; isDirectory: boolean; name: string }> } | null = null;
75
+ createState: { parentPath: string; type: 'file' | 'folder' } | null = null;
76
+
77
+ constructor(fileSystem: IFileSystem) {
78
+ makeAutoObservable(this, {
79
+ fileSystem: false,
80
+ fileSystemBridge: false,
81
+ onPathChange: false,
82
+ onNotify: false,
83
+ onAction: false,
84
+ });
85
+
86
+ this.fileSystem = fileSystem;
87
+ this.fileSystemBridge = new FileSystemBridge(fileSystem);
88
+
89
+ this.treeProvider = new FileSystemTreeProvider(this.fileSystemBridge, {
90
+ showFilesInTree: false,
91
+ onSelectionChange: this.handleTreeSelection,
92
+ onRefresh: () => this.refresh(),
93
+ onRenameRequest: (itemId, currentName, path, source) => this.requestRename(itemId, currentName, path, source),
94
+ onDeleteRequest: (targets) => this.requestDelete(targets),
95
+ onNewItemRequest: (parentPath, type) => this.requestCreate(parentPath, type)
96
+ });
97
+
98
+ this.listProvider = new FileSystemListProvider(this.fileSystemBridge, {
99
+ showAllItems: true,
100
+ onSelectionChange: this.handleListSelection,
101
+ onNavigation: this.handleListNavigation,
102
+ onNavigateUp: () => this.navigateUp(),
103
+ onOpenFile: (path, name, size) => this.openFileViewer(path, name, size),
104
+ onPreviewFile: (path, name, size) => this.loadPreviewFile(path, name, size),
105
+ onRefresh: () => this.refresh(),
106
+ onRenameRequest: (itemId, currentName, path, source) => this.requestRename(itemId, currentName, path, source),
107
+ onDeleteRequest: (targets) => this.requestDelete(targets),
108
+ onNewItemRequest: (parentPath, type) => this.requestCreate(parentPath, type)
109
+ });
110
+
111
+ this.treeModel = new TreeModel(this.treeProvider);
112
+ this.listModel = new ListItemsModel(this.listProvider);
113
+ this.uploadModel = new UploadModel(this.fileSystemBridge, (type, msg) => this.notify(type, msg));
114
+ }
115
+
116
+ private notify(type: 'success' | 'error' | 'warning', message: string): void {
117
+ this.onNotify?.(type, message);
118
+ }
119
+
120
+ async setInitialPath(path: string) {
121
+ this.currentPath = path;
122
+ this.selectedPath = path;
123
+
124
+ this.listProvider.setPath(path);
125
+
126
+ await Promise.all([
127
+ this.treeModel.loadNodes(),
128
+ this.listModel.loadItems()
129
+ ]);
130
+
131
+ this.selectPathInTree(path);
132
+ }
133
+
134
+ handleTreeSelection = async (path: string) => {
135
+ this.selectedPath = path;
136
+ this.currentPath = path;
137
+ this.previewFile = null;
138
+ this.error = null;
139
+ this.openFile = null;
140
+ this.onPathChange?.(path);
141
+
142
+ this.listProvider.setPath(path);
143
+ this.listModel.clearItems();
144
+ this.listModel.clearSearch();
145
+ this.listModel.totalItemCount = 0;
146
+ await this.listModel.loadItems();
147
+ };
148
+
149
+ handleListNavigation = async (path: string) => {
150
+ this.currentPath = path;
151
+ this.selectedPath = path;
152
+ this.previewFile = null;
153
+ this.error = null;
154
+ this.openFile = null;
155
+ this.onPathChange?.(path);
156
+
157
+ this.selectPathInTree(path);
158
+
159
+ this.listProvider.setPath(path);
160
+ this.listModel.clearItems();
161
+ this.listModel.clearSearch();
162
+ this.listModel.totalItemCount = 0;
163
+ await this.listModel.loadItems();
164
+ };
165
+
166
+ handleListSelection = (selectedItems: ListItemData[]) => {
167
+ // Optional: auto-navigate on single directory selection
168
+ };
169
+
170
+ private selectPathInTree(path: string) {
171
+ const node = this.findNodeByPath(path);
172
+ if (node) {
173
+ this.treeModel.selectNode(node);
174
+ }
175
+ }
176
+
177
+ private findNodeByPath(path: string): TreeNodeData | null {
178
+ const searchNodes = (nodes: TreeNodeData[]): TreeNodeData | null => {
179
+ for (const node of nodes) {
180
+ if (node.path === path) {
181
+ return node;
182
+ }
183
+ if (node.children) {
184
+ const found = searchNodes(node.children);
185
+ if (found) return found;
186
+ }
187
+ }
188
+ return null;
189
+ };
190
+
191
+ return searchNodes(this.treeModel.nodes);
192
+ }
193
+
194
+ async navigateUp() {
195
+ const parentPath = this.currentPath.split('/').slice(0, -1).join('/') || '/';
196
+ await this.handleListNavigation(parentPath);
197
+ }
198
+
199
+ async navigateToPath(path: string) {
200
+ await this.handleListNavigation(path);
201
+ }
202
+
203
+ async refresh() {
204
+ runInAction(() => {
205
+ this.isLoading = true;
206
+ this.error = null;
207
+ this.treeModel.errors.clear();
208
+ });
209
+
210
+ try {
211
+ await Promise.all([
212
+ this.treeModel.loadNodes(),
213
+ this.listModel.refresh()
214
+ ]);
215
+ } catch (error) {
216
+ runInAction(() => {
217
+ this.error = error instanceof Error ? error.message : 'Failed to refresh';
218
+ });
219
+ } finally {
220
+ runInAction(() => {
221
+ this.isLoading = false;
222
+ });
223
+ }
224
+ }
225
+
226
+ async navigateToBreadcrumb(path: string) {
227
+ await this.handleListNavigation(path);
228
+ }
229
+
230
+ get canNavigateUp(): boolean {
231
+ return this.currentPath !== '/';
232
+ }
233
+
234
+ get pathSegments(): Array<{ name: string; path: string }> {
235
+ if (this.currentPath === '/') {
236
+ return [{ name: 'Root', path: '/' }];
237
+ }
238
+
239
+ const segments = this.currentPath.split('/').filter(Boolean);
240
+ return [
241
+ { name: 'Root', path: '/' },
242
+ ...segments.map((segment, index) => ({
243
+ name: segment,
244
+ path: '/' + segments.slice(0, index + 1).join('/')
245
+ }))
246
+ ];
247
+ }
248
+
249
+ get isTreeLoading(): boolean {
250
+ return this.treeModel.isLoading;
251
+ }
252
+
253
+ get isListLoading(): boolean {
254
+ return this.listModel.isLoading;
255
+ }
256
+
257
+ get hasTreeNodes(): boolean {
258
+ return this.treeModel.hasNodes;
259
+ }
260
+
261
+ get hasListItems(): boolean {
262
+ return this.listModel.items.length > 0;
263
+ }
264
+
265
+ get hasError(): boolean {
266
+ return this.error !== null;
267
+ }
268
+
269
+ get hasItems(): boolean {
270
+ return this.hasListItems;
271
+ }
272
+
273
+ get itemsArray(): ListItemData[] {
274
+ return this.listModel.items;
275
+ }
276
+
277
+ async refreshItems(): Promise<void> {
278
+ await this.refresh();
279
+ }
280
+
281
+ clearError(): void {
282
+ this.error = null;
283
+ }
284
+
285
+ async setCurrentPath(path: string): Promise<void> {
286
+ await this.navigateToPath(path);
287
+ }
288
+
289
+ // Image extensions that should be read as binary
290
+ private static readonly BINARY_EXTENSIONS = new Set([
291
+ 'png', 'jpg', 'jpeg', 'gif', 'webp', 'bmp', 'ico', 'tiff', 'tif', 'avif',
292
+ 'pdf', 'zip', 'tar', 'gz',
293
+ ]);
294
+
295
+ private static readonly MIME_TYPES: Record<string, string> = {
296
+ 'txt': 'text/plain', 'md': 'text/markdown', 'json': 'application/json',
297
+ 'js': 'text/javascript', 'ts': 'text/typescript', 'css': 'text/css',
298
+ 'html': 'text/html', 'png': 'image/png', 'jpg': 'image/jpeg',
299
+ 'jpeg': 'image/jpeg', 'gif': 'image/gif', 'svg': 'image/svg+xml',
300
+ 'webp': 'image/webp', 'bmp': 'image/bmp', 'ico': 'image/x-icon',
301
+ 'avif': 'image/avif', 'tiff': 'image/tiff', 'tif': 'image/tiff',
302
+ 'pdf': 'application/pdf',
303
+ };
304
+
305
+ async openFileViewer(path: string, name: string, size?: number): Promise<void> {
306
+ const extension = name.split('.').pop()?.toLowerCase() || '';
307
+ const isSupported = globalViewerRegistry.canHandle(name);
308
+ const viewer: ViewerConfig = isSupported
309
+ ? globalViewerRegistry.getViewer(name)!
310
+ : { id: 'unsupported', name: 'Unsupported', extensions: [], component: UnsupportedFileViewer };
311
+
312
+ const isBinary = !isSupported || FileBrowserModel.BINARY_EXTENSIONS.has(extension);
313
+
314
+ // Show loading state immediately so UI stays responsive
315
+ runInAction(() => {
316
+ this.isLoadingFile = true;
317
+ });
318
+
319
+ let content: string | ArrayBuffer | undefined;
320
+ try {
321
+ const raw = await this.fileSystemBridge.readFile(path);
322
+ if (typeof raw === 'string') {
323
+ if (isBinary) {
324
+ const bytes = new Uint8Array(raw.length);
325
+ for (let i = 0; i < raw.length; i++) {
326
+ bytes[i] = raw.charCodeAt(i) & 0xff;
327
+ }
328
+ content = bytes.buffer as ArrayBuffer;
329
+ } else {
330
+ content = raw;
331
+ }
332
+ } else if (raw instanceof ArrayBuffer) {
333
+ if (isBinary) {
334
+ content = raw;
335
+ } else {
336
+ content = new TextDecoder('utf-8').decode(raw);
337
+ }
338
+ } else if (ArrayBuffer.isView(raw)) {
339
+ if (isBinary) {
340
+ const copy = new Uint8Array(raw.byteLength);
341
+ copy.set(new Uint8Array(raw.buffer, raw.byteOffset, raw.byteLength));
342
+ content = copy.buffer as ArrayBuffer;
343
+ } else {
344
+ content = new TextDecoder('utf-8').decode(raw);
345
+ }
346
+ }
347
+ } catch (err) {
348
+ console.warn(`[FileBrowser] Failed to read file ${path}:`, err instanceof Error ? err.message : err);
349
+ }
350
+
351
+ runInAction(() => {
352
+ this.isLoadingFile = false;
353
+ this.openFile = {
354
+ path,
355
+ name,
356
+ content,
357
+ size,
358
+ mimeType: FileBrowserModel.MIME_TYPES[extension] || 'application/octet-stream',
359
+ viewer,
360
+ };
361
+ });
362
+ }
363
+
364
+ get viewableFiles(): Array<{ path: string; name: string; size?: number }> {
365
+ return this.listModel.items
366
+ .filter(item => !item.isDirectory && globalViewerRegistry.canHandle(item.name))
367
+ .map(item => ({ path: item.path, name: item.name, size: item.size }));
368
+ }
369
+
370
+ get currentFileIndex(): number {
371
+ if (!this.openFile) return -1;
372
+ return this.viewableFiles.findIndex(f => f.path === this.openFile!.path);
373
+ }
374
+
375
+ get canNavigateNext(): boolean {
376
+ return this.currentFileIndex >= 0 && this.currentFileIndex < this.viewableFiles.length - 1;
377
+ }
378
+
379
+ get canNavigatePrev(): boolean {
380
+ return this.currentFileIndex > 0;
381
+ }
382
+
383
+ async navigateToNextFile(): Promise<void> {
384
+ const files = this.viewableFiles;
385
+ const idx = this.currentFileIndex;
386
+ if (idx >= 0 && idx < files.length - 1) {
387
+ const next = files[idx + 1]!;
388
+ await this.openFileViewer(next.path, next.name, next.size);
389
+ }
390
+ }
391
+
392
+ async navigateToPrevFile(): Promise<void> {
393
+ const files = this.viewableFiles;
394
+ const idx = this.currentFileIndex;
395
+ if (idx > 0) {
396
+ const prev = files[idx - 1]!;
397
+ await this.openFileViewer(prev.path, prev.name, prev.size);
398
+ }
399
+ }
400
+
401
+ closeFileViewer(): void {
402
+ this.openFile = null;
403
+ }
404
+
405
+ // Preview pane
406
+ togglePreview(): void {
407
+ this.previewEnabled = !this.previewEnabled;
408
+ if (!this.previewEnabled) {
409
+ this.previewFile = null;
410
+ }
411
+ }
412
+
413
+ async loadPreviewFile(path: string, name: string, size?: number): Promise<void> {
414
+ if (!this.previewEnabled) return;
415
+
416
+ const extension = name.split('.').pop()?.toLowerCase() || '';
417
+ const isSupported = globalViewerRegistry.canHandle(name);
418
+ const viewer: ViewerConfig = isSupported
419
+ ? globalViewerRegistry.getViewer(name)!
420
+ : { id: 'unsupported', name: 'Unsupported', extensions: [], component: UnsupportedFileViewer };
421
+
422
+ runInAction(() => {
423
+ this.previewFile = {
424
+ path, name, size,
425
+ mimeType: FileBrowserModel.MIME_TYPES[extension] || 'application/octet-stream',
426
+ viewer, isLoading: true,
427
+ };
428
+ });
429
+
430
+ const isBinary = !isSupported || FileBrowserModel.BINARY_EXTENSIONS.has(extension);
431
+ let content: string | ArrayBuffer | undefined;
432
+ try {
433
+ const raw = await this.fileSystemBridge.readFile(path);
434
+ if (typeof raw === 'string') {
435
+ if (isBinary) {
436
+ const bytes = new Uint8Array(raw.length);
437
+ for (let i = 0; i < raw.length; i++) bytes[i] = raw.charCodeAt(i) & 0xff;
438
+ content = bytes.buffer as ArrayBuffer;
439
+ } else {
440
+ content = raw;
441
+ }
442
+ } else if (raw instanceof ArrayBuffer) {
443
+ content = isBinary ? raw : new TextDecoder('utf-8').decode(raw);
444
+ } else if (ArrayBuffer.isView(raw)) {
445
+ if (isBinary) {
446
+ const copy = new Uint8Array(raw.byteLength);
447
+ copy.set(new Uint8Array(raw.buffer, raw.byteOffset, raw.byteLength));
448
+ content = copy.buffer as ArrayBuffer;
449
+ } else {
450
+ content = new TextDecoder('utf-8').decode(raw);
451
+ }
452
+ }
453
+ } catch (err) {
454
+ console.warn(`[FileBrowser] Failed to read preview for ${path}:`, err instanceof Error ? err.message : err);
455
+ }
456
+
457
+ runInAction(() => {
458
+ if (this.previewFile?.path === path) {
459
+ this.previewFile = {
460
+ path, name, content, size,
461
+ mimeType: FileBrowserModel.MIME_TYPES[extension] || 'application/octet-stream',
462
+ viewer, isLoading: false,
463
+ };
464
+ }
465
+ });
466
+ }
467
+
468
+ clearPreview(): void {
469
+ this.previewFile = null;
470
+ }
471
+
472
+ // Upload files
473
+ async uploadFiles(files: FileList | File[]): Promise<void> {
474
+ await this.uploadModel.uploadFiles(files, this.currentPath);
475
+ await this.refresh();
476
+ }
477
+
478
+ triggerFileUpload(): void {
479
+ const input = document.createElement('input');
480
+ input.type = 'file';
481
+ input.multiple = true;
482
+ input.onchange = () => {
483
+ if (input.files && input.files.length > 0) {
484
+ this.uploadFiles(input.files);
485
+ }
486
+ };
487
+ input.click();
488
+ }
489
+
490
+ // Rename dialog
491
+ requestRename(itemId: string, currentName: string, path: string, source: 'list' | 'tree' = 'list'): void {
492
+ this.renameState = { itemId, currentName, path, source };
493
+ }
494
+
495
+ async confirmRename(newName: string): Promise<void> {
496
+ if (!this.renameState) return;
497
+ const { path } = this.renameState;
498
+ runInAction(() => { this.renameState = null; });
499
+
500
+ for (const [key] of this.treeModel.expandedNodes) {
501
+ if (key === path || key.startsWith(path + '/')) {
502
+ this.treeModel.expandedNodes.delete(key);
503
+ }
504
+ }
505
+
506
+ const parentPath = path.split('/').slice(0, -1).join('/') || '/';
507
+ const newPath = `${parentPath}/${newName}`.replace(/\/\//g, '/');
508
+
509
+ if (this.currentPath === path) {
510
+ this.currentPath = newPath;
511
+ this.selectedPath = newPath;
512
+ this.listProvider.setPath(newPath);
513
+ } else if (this.currentPath.startsWith(path + '/')) {
514
+ const newCurrentPath = this.currentPath.replace(path, newPath);
515
+ this.currentPath = newCurrentPath;
516
+ this.selectedPath = newCurrentPath;
517
+ this.listProvider.setPath(newCurrentPath);
518
+ }
519
+
520
+ try {
521
+ const oldName = path.split('/').pop() || '';
522
+ await this.listProvider.executeRename(path, newName);
523
+ const message = `Renamed to "${newName}"`;
524
+ if (this.onAction) {
525
+ this.onAction({
526
+ type: 'rename',
527
+ message,
528
+ undo: async () => {
529
+ await this.listProvider.executeRename(newPath, oldName);
530
+ this.refresh();
531
+ },
532
+ });
533
+ } else {
534
+ this.notify('success', message);
535
+ }
536
+ } catch (err) {
537
+ this.notify('error', `Rename failed: ${err instanceof Error ? err.message : String(err)}`);
538
+ }
539
+ }
540
+
541
+ cancelRename(): void {
542
+ this.renameState = null;
543
+ }
544
+
545
+ // Delete dialog
546
+ requestDelete(targets: Array<{ path: string; isDirectory: boolean; name: string }>): void {
547
+ this.deleteState = { targets };
548
+ }
549
+
550
+ async confirmDelete(): Promise<void> {
551
+ if (!this.deleteState) return;
552
+ const { targets } = this.deleteState;
553
+ runInAction(() => { this.deleteState = null; });
554
+
555
+ for (const target of targets) {
556
+ if (target.isDirectory) {
557
+ if (this.currentPath === target.path || this.currentPath.startsWith(target.path + '/')) {
558
+ const parentPath = target.path.split('/').slice(0, -1).join('/') || '/';
559
+ this.currentPath = parentPath;
560
+ this.selectedPath = parentPath;
561
+ this.listProvider.setPath(parentPath);
562
+ break;
563
+ }
564
+ for (const [key] of this.treeModel.expandedNodes) {
565
+ if (key === target.path || key.startsWith(target.path + '/')) {
566
+ this.treeModel.expandedNodes.delete(key);
567
+ }
568
+ }
569
+ }
570
+ }
571
+
572
+ try {
573
+ await this.listProvider.executeDelete(targets);
574
+ const count = targets.length;
575
+ const message = count === 1 ? `Deleted "${targets[0]!.name}"` : `Deleted ${count} items`;
576
+ if (this.onAction) {
577
+ this.onAction({ type: 'delete', message });
578
+ } else {
579
+ this.notify('success', message);
580
+ }
581
+ } catch (err) {
582
+ this.notify('error', `Delete failed: ${err instanceof Error ? err.message : String(err)}`);
583
+ }
584
+ }
585
+
586
+ cancelDelete(): void {
587
+ this.deleteState = null;
588
+ }
589
+
590
+ // Create dialog
591
+ requestCreate(parentPath: string, type: 'file' | 'folder'): void {
592
+ this.createState = { parentPath, type };
593
+ }
594
+
595
+ async confirmCreate(name: string): Promise<void> {
596
+ if (!this.createState) return;
597
+ const { parentPath, type } = this.createState;
598
+ runInAction(() => { this.createState = null; });
599
+ try {
600
+ await this.listProvider.executeCreate(parentPath, name, type);
601
+ const message = `Created ${type} "${name}"`;
602
+ if (this.onAction) {
603
+ const itemPath = `${parentPath}/${name}`.replace(/\/\//g, '/');
604
+ this.onAction({
605
+ type: 'create',
606
+ message,
607
+ undo: async () => {
608
+ await this.listProvider.executeDelete([{ path: itemPath, isDirectory: type === 'folder', name }]);
609
+ this.refresh();
610
+ },
611
+ });
612
+ } else {
613
+ this.notify('success', message);
614
+ }
615
+ } catch (err) {
616
+ this.notify('error', `Create ${type} failed: ${err instanceof Error ? err.message : String(err)}`);
617
+ }
618
+ }
619
+
620
+ cancelCreate(): void {
621
+ this.createState = null;
622
+ }
623
+ }