@anymux/ui-kit 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (244) hide show
  1. package/dist/ExplorerLayout-CSIJd7N4.js +105 -0
  2. package/dist/ExplorerLayout-CSIJd7N4.js.map +1 -0
  3. package/dist/FileBrowserContext-B6jixa2j.js +11 -0
  4. package/dist/FileBrowserContext-B6jixa2j.js.map +1 -0
  5. package/dist/calendar-DSlrbHoj.js +761 -0
  6. package/dist/calendar-DSlrbHoj.js.map +1 -0
  7. package/dist/calendar.d.ts +3 -0
  8. package/dist/calendar.js +3 -0
  9. package/dist/contacts-DQXTZzHc.js +539 -0
  10. package/dist/contacts-DQXTZzHc.js.map +1 -0
  11. package/dist/contacts.d.ts +3 -0
  12. package/dist/contacts.js +3 -0
  13. package/dist/file-browser-m5atC3kF.js +6755 -0
  14. package/dist/file-browser-m5atC3kF.js.map +1 -0
  15. package/dist/file-browser.d.ts +11 -0
  16. package/dist/file-browser.js +9 -0
  17. package/dist/git-B55e6LL-.js +561 -0
  18. package/dist/git-B55e6LL-.js.map +1 -0
  19. package/dist/git.d.ts +2 -0
  20. package/dist/git.js +3 -0
  21. package/dist/iconMap-V4B8P-Uh.js +206 -0
  22. package/dist/iconMap-V4B8P-Uh.js.map +1 -0
  23. package/dist/icons-CIsIOZXR.js +0 -0
  24. package/dist/icons.d.ts +2 -0
  25. package/dist/icons.js +4 -0
  26. package/dist/index-BNmNIWBL.d.ts +71 -0
  27. package/dist/index-BNmNIWBL.d.ts.map +1 -0
  28. package/dist/index-Bryv_GCG.d.ts +1481 -0
  29. package/dist/index-Bryv_GCG.d.ts.map +1 -0
  30. package/dist/index-CuQIjSXs.d.ts +134 -0
  31. package/dist/index-CuQIjSXs.d.ts.map +1 -0
  32. package/dist/index-DSu19mq0.d.ts +153 -0
  33. package/dist/index-DSu19mq0.d.ts.map +1 -0
  34. package/dist/index-DmsyeHFr.d.ts +149 -0
  35. package/dist/index-DmsyeHFr.d.ts.map +1 -0
  36. package/dist/index-DxnJ8FYM.d.ts +17 -0
  37. package/dist/index-DxnJ8FYM.d.ts.map +1 -0
  38. package/dist/index-DzfY1Tok.d.ts +32 -0
  39. package/dist/index-DzfY1Tok.d.ts.map +1 -0
  40. package/dist/index-Ml_SgiKa.d.ts +1847 -0
  41. package/dist/index-Ml_SgiKa.d.ts.map +1 -0
  42. package/dist/index-kHr9udZD.d.ts +1025 -0
  43. package/dist/index-kHr9udZD.d.ts.map +1 -0
  44. package/dist/index.d.ts +11 -0
  45. package/dist/index.js +15 -0
  46. package/dist/layout-Ca_4r8ka.js +89 -0
  47. package/dist/layout-Ca_4r8ka.js.map +1 -0
  48. package/dist/layout.d.ts +2 -0
  49. package/dist/layout.js +5 -0
  50. package/dist/list-CxfT6hix.js +6831 -0
  51. package/dist/list-CxfT6hix.js.map +1 -0
  52. package/dist/list.d.ts +2 -0
  53. package/dist/list.js +5 -0
  54. package/dist/media-DZ292aKK.js +557 -0
  55. package/dist/media-DZ292aKK.js.map +1 -0
  56. package/dist/media.d.ts +3 -0
  57. package/dist/media.js +3 -0
  58. package/dist/tree-Dd9Z0Aso.js +3351 -0
  59. package/dist/tree-Dd9Z0Aso.js.map +1 -0
  60. package/dist/tree.d.ts +2 -0
  61. package/dist/tree.js +6 -0
  62. package/dist/types-common-CB3kRek8.d.ts +26 -0
  63. package/dist/types-common-CB3kRek8.d.ts.map +1 -0
  64. package/dist/utils-B4fdKKsy.js +3 -0
  65. package/package.json +109 -0
  66. package/src/calendar/AgendaView.tsx +37 -0
  67. package/src/calendar/CalendarBrowser.tsx +90 -0
  68. package/src/calendar/CalendarModel.ts +142 -0
  69. package/src/calendar/CalendarSidebar.tsx +81 -0
  70. package/src/calendar/DayView.tsx +76 -0
  71. package/src/calendar/EventCard.tsx +51 -0
  72. package/src/calendar/MockCalendarProvider.ts +98 -0
  73. package/src/calendar/MonthView.tsx +77 -0
  74. package/src/calendar/WeekView.tsx +129 -0
  75. package/src/calendar/index.ts +18 -0
  76. package/src/calendar/types.ts +25 -0
  77. package/src/contacts/ContactAvatar.tsx +35 -0
  78. package/src/contacts/ContactBrowser.tsx +56 -0
  79. package/src/contacts/ContactCard.tsx +37 -0
  80. package/src/contacts/ContactDetail.tsx +63 -0
  81. package/src/contacts/ContactGroupSidebar.tsx +40 -0
  82. package/src/contacts/ContactList.tsx +32 -0
  83. package/src/contacts/ContactListModel.ts +120 -0
  84. package/src/contacts/MockContactProvider.ts +77 -0
  85. package/src/contacts/index.ts +17 -0
  86. package/src/contacts/types.ts +26 -0
  87. package/src/demos/CalendarBrowserDemo.tsx +15 -0
  88. package/src/demos/ContactBrowserDemo.tsx +15 -0
  89. package/src/demos/MediaBrowserDemo.tsx +15 -0
  90. package/src/file-browser/adapters/DocumentViewerAdapter.ts +371 -0
  91. package/src/file-browser/adapters/FileSystemBridge.ts +168 -0
  92. package/src/file-browser/adapters/GitBrowserAdapter.ts +546 -0
  93. package/src/file-browser/adapters/README.md +504 -0
  94. package/src/file-browser/adapters/index.ts +27 -0
  95. package/src/file-browser/adapters/types.ts +70 -0
  96. package/src/file-browser/architecture.md +645 -0
  97. package/src/file-browser/components/CreateItemDialog.tsx +71 -0
  98. package/src/file-browser/components/DeleteConfirmDialog.tsx +58 -0
  99. package/src/file-browser/components/FileBrowser.tsx +473 -0
  100. package/src/file-browser/components/FileBrowserContent.tsx +209 -0
  101. package/src/file-browser/components/FileBrowserHeader.tsx +151 -0
  102. package/src/file-browser/components/FileBrowserToolbar.tsx +145 -0
  103. package/src/file-browser/components/LeftPanel/LeftPanel.tsx +103 -0
  104. package/src/file-browser/components/LeftPanel/LeftPanelTabs.tsx +70 -0
  105. package/src/file-browser/components/LeftPanel/TreeNavigationView.tsx +256 -0
  106. package/src/file-browser/components/PreviewPane.tsx +146 -0
  107. package/src/file-browser/components/RightPanel/FilePreview.tsx +219 -0
  108. package/src/file-browser/components/RightPanel/RightPanel.tsx +186 -0
  109. package/src/file-browser/components/RightPanel/RightPanelToolbar.tsx +113 -0
  110. package/src/file-browser/components/UploadProgress.tsx +123 -0
  111. package/src/file-browser/components/ViewerHost.tsx +208 -0
  112. package/src/file-browser/components/mobile/MobileNavigation.tsx +227 -0
  113. package/src/file-browser/components/navigation/NavigationButtons.tsx +171 -0
  114. package/src/file-browser/components/shared/ErrorBoundary.tsx +116 -0
  115. package/src/file-browser/components/shared/FileBrowserItem.tsx +195 -0
  116. package/src/file-browser/components/shared/FileIcon.tsx +169 -0
  117. package/src/file-browser/components/toolbar/ViewModeToggle.tsx +200 -0
  118. package/src/file-browser/components/views/ListView/ListView.tsx +484 -0
  119. package/src/file-browser/components/views/ThumbnailView/ThumbnailView.tsx +323 -0
  120. package/src/file-browser/components/views/TreeView/TreeNode.tsx +186 -0
  121. package/src/file-browser/components/views/TreeView/TreeNodeList.tsx +191 -0
  122. package/src/file-browser/components/views/TreeView/TreeView.tsx +200 -0
  123. package/src/file-browser/components/views/TreemapView/TreemapView.tsx +339 -0
  124. package/src/file-browser/context/FileBrowserContext.tsx +13 -0
  125. package/src/file-browser/examples/BasicUsage.tsx +20 -0
  126. package/src/file-browser/index.ts +98 -0
  127. package/src/file-browser/models/FileBrowserModel.ts +623 -0
  128. package/src/file-browser/models/LeftPanelManagerModel.ts +105 -0
  129. package/src/file-browser/models/NavigationManagerModel.ts +312 -0
  130. package/src/file-browser/models/ResponsiveLayoutManagerModel.ts +437 -0
  131. package/src/file-browser/models/RightPanelManagerModel.ts +190 -0
  132. package/src/file-browser/models/SelectionManagerModel.ts +252 -0
  133. package/src/file-browser/models/ToolbarManagerModel.ts +144 -0
  134. package/src/file-browser/models/UploadModel.ts +147 -0
  135. package/src/file-browser/models/ViewModeManagerModel.ts +185 -0
  136. package/src/file-browser/models/ViewerHostModel.ts +44 -0
  137. package/src/file-browser/models/ui/ListViewUIModel.ts +265 -0
  138. package/src/file-browser/models/ui/PreviewUIModel.ts +297 -0
  139. package/src/file-browser/models/ui/ThumbnailViewUIModel.ts +254 -0
  140. package/src/file-browser/models/ui/TreeViewUIModel.ts +128 -0
  141. package/src/file-browser/models/ui/TreemapViewUIModel.ts +350 -0
  142. package/src/file-browser/providers/FileSystemListProvider.ts +552 -0
  143. package/src/file-browser/providers/FileSystemProvider.ts +401 -0
  144. package/src/file-browser/providers/FileSystemTreeProvider.ts +231 -0
  145. package/src/file-browser/providers/GitProvider.ts +337 -0
  146. package/src/file-browser/providers/GitRepositoryProvider.ts +376 -0
  147. package/src/file-browser/providers/IFileBrowserProvider.ts +56 -0
  148. package/src/file-browser/providers/MemoryProvider.ts +303 -0
  149. package/src/file-browser/providers/index.ts +4 -0
  150. package/src/file-browser/registry/ViewerRegistry.ts +551 -0
  151. package/src/file-browser/registry/types.ts +144 -0
  152. package/src/file-browser/scripts/performanceBenchmark.ts +553 -0
  153. package/src/file-browser/services/ThumbnailCacheService.ts +128 -0
  154. package/src/file-browser/tasks.md +537 -0
  155. package/src/file-browser/types/FileBrowserTypes.ts +126 -0
  156. package/src/file-browser/types/ProviderTypes.ts +155 -0
  157. package/src/file-browser/types/UITypes.ts +235 -0
  158. package/src/file-browser/types/ViewModeTypes.ts +150 -0
  159. package/src/file-browser/utils/gestures.ts +327 -0
  160. package/src/file-browser/utils/performance.ts +563 -0
  161. package/src/file-browser/viewers/ImageViewer.tsx +163 -0
  162. package/src/file-browser/viewers/ImageViewerModel.ts +79 -0
  163. package/src/file-browser/viewers/TextViewer.tsx +95 -0
  164. package/src/file-browser/viewers/UnsupportedFileViewer.tsx +57 -0
  165. package/src/file-browser/viewers/index.ts +61 -0
  166. package/src/git/BranchList.tsx +128 -0
  167. package/src/git/CommitGraph.tsx +239 -0
  168. package/src/git/CommitList.tsx +258 -0
  169. package/src/git/DiffViewer.tsx +219 -0
  170. package/src/git/index.ts +4 -0
  171. package/src/icons/iconMap.ts +146 -0
  172. package/src/icons/index.ts +9 -0
  173. package/src/index.ts +13 -0
  174. package/src/layout/README.md +307 -0
  175. package/src/layout/components/ExplorerLayout/ExplorerLayout.tsx +178 -0
  176. package/src/layout/examples/SimpleExample.tsx +60 -0
  177. package/src/layout/index.ts +6 -0
  178. package/src/lib/utils.ts +1 -0
  179. package/src/list/README.md +303 -0
  180. package/src/list/architecture.md +807 -0
  181. package/src/list/components/CalculatedGridView.tsx +252 -0
  182. package/src/list/components/DragPreview.tsx +102 -0
  183. package/src/list/components/ListContextMenu.tsx +274 -0
  184. package/src/list/components/ListItem.tsx +761 -0
  185. package/src/list/components/ListItems.tsx +919 -0
  186. package/src/list/components/MasonryView.tsx +241 -0
  187. package/src/list/components/SearchFilter.tsx +44 -0
  188. package/src/list/components/TreemapView.tsx +709 -0
  189. package/src/list/components/ViewSizeControls.tsx +205 -0
  190. package/src/list/components/ViewTypeSelector.tsx +312 -0
  191. package/src/list/components/VirtualizedDetailsView.tsx +231 -0
  192. package/src/list/components/VirtualizedGrid.tsx +164 -0
  193. package/src/list/components/VirtualizedList.tsx +154 -0
  194. package/src/list/components/VirtualizedMasonryView.tsx +344 -0
  195. package/src/list/components/shared/EmptyState.tsx +103 -0
  196. package/src/list/components/shared/ErrorBoundary.tsx +123 -0
  197. package/src/list/components/shared/ErrorDisplay.tsx +100 -0
  198. package/src/list/components/shared/ListLoader.tsx +146 -0
  199. package/src/list/components/shared/LoadingIndicator.tsx +80 -0
  200. package/src/list/index.ts +92 -0
  201. package/src/list/models/ListItemsModel.ts +1301 -0
  202. package/src/list/models/TreemapModel.ts +204 -0
  203. package/src/list/providers/ListItemsProvider.ts +313 -0
  204. package/src/list/providers/TestListProvider.ts +604 -0
  205. package/src/list/tasks.md +937 -0
  206. package/src/list/types/ListTypes.ts +178 -0
  207. package/src/list/utils/BenchmarkLogger.ts +243 -0
  208. package/src/list/utils/DragDropManager.ts +320 -0
  209. package/src/list/utils/GridLayoutCalculator.ts +290 -0
  210. package/src/list/utils/ListAccessibility.ts +367 -0
  211. package/src/list/utils/ListKeyboard.ts +414 -0
  212. package/src/list/utils/MasonryLayoutCalculator.ts +302 -0
  213. package/src/list/utils/MasonryLayoutEngine.ts +401 -0
  214. package/src/list/utils/__tests__/MasonryLayoutEngine.test.ts +157 -0
  215. package/src/list/utils/__tests__/VirtualizedMasonryView.test.tsx +251 -0
  216. package/src/media/AlbumSidebar.tsx +48 -0
  217. package/src/media/MediaBrowser.tsx +92 -0
  218. package/src/media/MediaBrowserModel.ts +138 -0
  219. package/src/media/MediaGrid.tsx +50 -0
  220. package/src/media/MediaList.tsx +49 -0
  221. package/src/media/MediaPreview.tsx +63 -0
  222. package/src/media/MediaTimeline.tsx +38 -0
  223. package/src/media/MockMediaProvider.ts +70 -0
  224. package/src/media/index.ts +18 -0
  225. package/src/media/types.ts +21 -0
  226. package/src/styles/variables.css +60 -0
  227. package/src/tree/DEVELOPMENT_SUMMARY.md +170 -0
  228. package/src/tree/__tests__/TreeModel.test.ts +16 -0
  229. package/src/tree/architecture.md +530 -0
  230. package/src/tree/components/Tree.tsx +283 -0
  231. package/src/tree/components/TreeCheckbox.tsx +147 -0
  232. package/src/tree/components/TreeContextMenu.tsx +139 -0
  233. package/src/tree/components/TreeNodeList.tsx +329 -0
  234. package/src/tree/components/TreeTable.tsx +382 -0
  235. package/src/tree/index.ts +58 -0
  236. package/src/tree/models/TreeModel.ts +839 -0
  237. package/src/tree/providers/SimpleTreeProvider.ts +463 -0
  238. package/src/tree/providers/TestTreeProvider.ts +946 -0
  239. package/src/tree/providers/TreeProvider.ts +308 -0
  240. package/src/tree/tasks.md +2046 -0
  241. package/src/tree/types/TreeTypes.ts +279 -0
  242. package/src/tree/utils/SelectionTheme.ts +150 -0
  243. package/src/tree/utils/logger.ts +203 -0
  244. package/src/types-common.ts +21 -0
@@ -0,0 +1,252 @@
1
+ import { makeAutoObservable, observable, reaction } from 'mobx';
2
+ import { FileBrowserItem } from '../types/FileBrowserTypes';
3
+
4
+ interface SelectionOwner {
5
+ logger?: { info: (message: string) => void } | null;
6
+ leftPanel?: any; // LeftPanelManagerModel
7
+ rightPanel?: any; // RightPanelManagerModel
8
+ navigationManager?: any; // NavigationManagerModel
9
+ }
10
+
11
+ export class SelectionManagerModel {
12
+ // Observable state
13
+ selectedItems = observable.map<string, boolean>();
14
+ lastSelectedIndex = -1;
15
+
16
+ // Current selected item for coordination
17
+ focusedItem: FileBrowserItem | null = null;
18
+
19
+ // Coordination state
20
+ isUpdatingFromPanels = false; // Prevent infinite loops
21
+
22
+ private owner: SelectionOwner;
23
+ private disposers: (() => void)[] = [];
24
+
25
+ constructor(owner: SelectionOwner) {
26
+ makeAutoObservable(this);
27
+
28
+ this.owner = owner;
29
+
30
+ // Set up coordination reactions
31
+ this.setupCoordinationReactions();
32
+ }
33
+
34
+ // Computed values
35
+ get selectedItemsArray() {
36
+ return Array.from(this.selectedItems.keys()).filter(id =>
37
+ this.selectedItems.get(id) === true
38
+ );
39
+ }
40
+
41
+ get hasSelection() {
42
+ return this.selectedItemsArray.length > 0;
43
+ }
44
+
45
+ get selectionCount() {
46
+ return this.selectedItemsArray.length;
47
+ }
48
+
49
+ get isMultipleSelection() {
50
+ return this.selectionCount > 1;
51
+ }
52
+
53
+ isSelected = (itemId: string): boolean => {
54
+ return this.selectedItems.get(itemId) === true;
55
+ };
56
+
57
+ // Actions
58
+ selectItem = (itemId: string, multiSelect = false) => {
59
+ this.owner.logger?.info(`selectItem: ${itemId}, multiSelect: ${multiSelect}`);
60
+
61
+ if (!multiSelect) {
62
+ this.clearSelection();
63
+ }
64
+
65
+ this.selectedItems.set(itemId, true);
66
+ };
67
+
68
+ deselectItem = (itemId: string) => {
69
+ this.owner.logger?.info(`deselectItem: ${itemId}`);
70
+ this.selectedItems.set(itemId, false);
71
+ };
72
+
73
+ toggleSelection = (itemId: string, multiSelect = false) => {
74
+ const isCurrentlySelected = this.isSelected(itemId);
75
+
76
+ if (isCurrentlySelected) {
77
+ this.deselectItem(itemId);
78
+ } else {
79
+ this.selectItem(itemId, multiSelect);
80
+ }
81
+ };
82
+
83
+ selectRange = (startItemId: string, endItemId: string, allItems: FileBrowserItem[]) => {
84
+ this.owner.logger?.info(`selectRange: ${startItemId} to ${endItemId}`);
85
+
86
+ const startIndex = allItems.findIndex(item => item.id === startItemId);
87
+ const endIndex = allItems.findIndex(item => item.id === endItemId);
88
+
89
+ if (startIndex === -1 || endIndex === -1) return;
90
+
91
+ const minIndex = Math.min(startIndex, endIndex);
92
+ const maxIndex = Math.max(startIndex, endIndex);
93
+
94
+ // Clear current selection
95
+ this.clearSelection();
96
+
97
+ // Select range
98
+ for (let i = minIndex; i <= maxIndex; i++) {
99
+ const item = allItems[i];
100
+ if (item) {
101
+ this.selectedItems.set(item.id, true);
102
+ }
103
+ }
104
+ };
105
+
106
+ selectAll = (items: FileBrowserItem[]) => {
107
+ this.owner.logger?.info(`selectAll: ${items.length} items`);
108
+
109
+ items.forEach(item => {
110
+ this.selectedItems.set(item.id, true);
111
+ });
112
+ };
113
+
114
+ clearSelection = () => {
115
+ this.owner.logger?.info('clearSelection');
116
+ this.selectedItems.clear();
117
+ this.lastSelectedIndex = -1;
118
+ };
119
+
120
+ invertSelection = (allItems: FileBrowserItem[]) => {
121
+ this.owner.logger?.info(`invertSelection: ${allItems.length} total items`);
122
+
123
+ allItems.forEach(item => {
124
+ const isSelected = this.isSelected(item.id);
125
+ this.selectedItems.set(item.id, !isSelected);
126
+ });
127
+ };
128
+
129
+ getSelectedItems = (allItems: FileBrowserItem[]): FileBrowserItem[] => {
130
+ return allItems.filter(item => this.isSelected(item.id));
131
+ };
132
+
133
+ // Coordination methods
134
+ setFocusedItem = (item: FileBrowserItem | null) => {
135
+ this.focusedItem = item;
136
+
137
+ if (item && !this.isUpdatingFromPanels) {
138
+ this.coordinateSelection(item);
139
+ }
140
+
141
+ this.owner.logger?.info(`setFocusedItem: ${item?.name || 'none'}`);
142
+ };
143
+
144
+ private coordinateSelection = (item: FileBrowserItem) => {
145
+ if (this.isUpdatingFromPanels) return;
146
+
147
+ this.isUpdatingFromPanels = true;
148
+
149
+ try {
150
+ // Update right panel based on selection
151
+ if (this.owner.rightPanel) {
152
+ this.owner.rightPanel.setSelectedItem(item);
153
+ }
154
+
155
+ // Update navigation if item is a directory
156
+ if (item.type === 'directory' && this.owner.navigationManager) {
157
+ this.owner.navigationManager.setCurrentPath(item.path);
158
+ }
159
+
160
+ this.owner.logger?.info(`Coordinated selection: ${item.name} (${item.type})`);
161
+
162
+ } finally {
163
+ this.isUpdatingFromPanels = false;
164
+ }
165
+ };
166
+
167
+ private setupCoordinationReactions = () => {
168
+ // React to selection changes for coordination
169
+ const selectionReaction = reaction(
170
+ () => ({
171
+ selectedIds: this.selectedItemsArray,
172
+ focusedItem: this.focusedItem
173
+ }),
174
+ ({ selectedIds, focusedItem }) => {
175
+ if (this.isUpdatingFromPanels) return;
176
+
177
+ this.owner.logger?.info(`Selection reaction: ${selectedIds.length} selected, focused: ${focusedItem?.name || 'none'}`);
178
+
179
+ // Update panel states based on selection
180
+ if (focusedItem) {
181
+ this.coordinateSelection(focusedItem);
182
+ }
183
+ },
184
+ { name: 'SelectionCoordination' }
185
+ );
186
+
187
+ this.disposers.push(selectionReaction);
188
+ };
189
+
190
+ // Panel-specific selection methods
191
+ selectFromTreePanel = (item: FileBrowserItem) => {
192
+ this.isUpdatingFromPanels = true;
193
+
194
+ try {
195
+ this.selectItem(item.id);
196
+ this.setFocusedItem(item);
197
+
198
+ // Update right panel
199
+ if (this.owner.rightPanel) {
200
+ this.owner.rightPanel.setSelectedItem(item);
201
+ }
202
+
203
+ this.owner.logger?.info(`Tree panel selection: ${item.name}`);
204
+
205
+ } finally {
206
+ this.isUpdatingFromPanels = false;
207
+ }
208
+ };
209
+
210
+ selectFromRightPanel = (item: FileBrowserItem) => {
211
+ this.isUpdatingFromPanels = true;
212
+
213
+ try {
214
+ this.selectItem(item.id);
215
+ this.setFocusedItem(item);
216
+
217
+ // TODO: Update tree panel expansion/selection when TreeNavigationView supports it
218
+ this.owner.logger?.info(`Right panel selection: ${item.name}`);
219
+
220
+ } finally {
221
+ this.isUpdatingFromPanels = false;
222
+ }
223
+ };
224
+
225
+ // Navigation-triggered selection
226
+ selectFromNavigation = (path: string, allItems: FileBrowserItem[]) => {
227
+ const item = allItems.find(item => item.path === path);
228
+
229
+ if (item) {
230
+ this.isUpdatingFromPanels = true;
231
+
232
+ try {
233
+ this.selectItem(item.id);
234
+ this.setFocusedItem(item);
235
+
236
+ this.owner.logger?.info(`Navigation selection: ${item.name}`);
237
+
238
+ } finally {
239
+ this.isUpdatingFromPanels = false;
240
+ }
241
+ }
242
+ };
243
+
244
+ // Cleanup
245
+ dispose = () => {
246
+ this.disposers.forEach(disposer => disposer());
247
+ this.disposers = [];
248
+ this.clearSelection();
249
+ this.focusedItem = null;
250
+ this.owner.logger?.info('SelectionManagerModel disposed');
251
+ };
252
+ }
@@ -0,0 +1,144 @@
1
+ import { makeAutoObservable, observable } from 'mobx';
2
+ import { ToolbarAction, ContextAction, ActionContext } from '../types/ProviderTypes';
3
+ import { FileBrowserItem } from '../types/FileBrowserTypes';
4
+
5
+ interface ToolbarOwner {
6
+ logger?: { info: (message: string) => void } | null;
7
+ }
8
+
9
+ export class ToolbarManagerModel {
10
+ // Observable state
11
+ toolbarActions = observable.map<string, ToolbarAction>();
12
+ contextActions = observable.map<string, ContextAction>();
13
+
14
+ private owner: ToolbarOwner;
15
+
16
+ constructor(owner: ToolbarOwner) {
17
+ makeAutoObservable(this);
18
+
19
+ this.owner = owner;
20
+ }
21
+
22
+ // Computed values
23
+ get globalActions() {
24
+ return Array.from(this.toolbarActions.values()).filter(action =>
25
+ action.category === 'global'
26
+ );
27
+ }
28
+
29
+ get selectionActions() {
30
+ return Array.from(this.toolbarActions.values()).filter(action =>
31
+ action.category === 'selection'
32
+ );
33
+ }
34
+
35
+ get allToolbarActions() {
36
+ return Array.from(this.toolbarActions.values());
37
+ }
38
+
39
+ get allContextActions() {
40
+ return Array.from(this.contextActions.values());
41
+ }
42
+
43
+ // Actions
44
+ addAction = (action: ToolbarAction) => {
45
+ this.owner.logger?.info(`addAction: ${action.id} (${action.category || 'default'})`);
46
+ this.toolbarActions.set(action.id, action);
47
+ };
48
+
49
+ removeAction = (actionId: string) => {
50
+ this.owner.logger?.info(`removeAction: ${actionId}`);
51
+ this.toolbarActions.delete(actionId);
52
+ };
53
+
54
+ updateAction = (actionId: string, updates: Partial<ToolbarAction>) => {
55
+ const existing = this.toolbarActions.get(actionId);
56
+ if (existing) {
57
+ this.owner.logger?.info(`updateAction: ${actionId}`);
58
+ this.toolbarActions.set(actionId, { ...existing, ...updates });
59
+ }
60
+ };
61
+
62
+ addContextAction = (action: ContextAction) => {
63
+ this.owner.logger?.info(`addContextAction: ${action.id}`);
64
+ this.contextActions.set(action.id, action);
65
+ };
66
+
67
+ removeContextAction = (actionId: string) => {
68
+ this.owner.logger?.info(`removeContextAction: ${actionId}`);
69
+ this.contextActions.delete(actionId);
70
+ };
71
+
72
+ // Context-sensitive filtering
73
+ getAvailableActions = (context: ActionContext): ToolbarAction[] => {
74
+ return this.allToolbarActions.filter(action => {
75
+ // Check if action is visible
76
+ if (action.isVisible && !action.isVisible(context)) {
77
+ return false;
78
+ }
79
+
80
+ // Check if action is enabled
81
+ if (action.isEnabled && !action.isEnabled(context)) {
82
+ return false;
83
+ }
84
+
85
+ return true;
86
+ });
87
+ };
88
+
89
+ getContextActionsForItem = (context: ActionContext): ContextAction[] => {
90
+ return this.allContextActions.filter(action => {
91
+ // Check if action is visible
92
+ if (action.isVisible && !action.isVisible(context)) {
93
+ return false;
94
+ }
95
+
96
+ // Check if action is enabled
97
+ if (action.isEnabled && !action.isEnabled(context)) {
98
+ return false;
99
+ }
100
+
101
+ return true;
102
+ });
103
+ };
104
+
105
+ // Action execution helpers
106
+ canExecuteAction = (actionId: string, context: ActionContext): boolean => {
107
+ const action = this.toolbarActions.get(actionId);
108
+ if (!action) return false;
109
+
110
+ const availableActions = this.getAvailableActions(context);
111
+ return availableActions.some(a => a.id === actionId);
112
+ };
113
+
114
+ getActionById = (actionId: string): ToolbarAction | undefined => {
115
+ return this.toolbarActions.get(actionId);
116
+ };
117
+
118
+ getContextActionById = (actionId: string): ContextAction | undefined => {
119
+ return this.contextActions.get(actionId);
120
+ };
121
+
122
+ // Bulk operations
123
+ setActions = (actions: ToolbarAction[]) => {
124
+ this.owner.logger?.info(`setActions: ${actions.length} actions`);
125
+ this.toolbarActions.clear();
126
+ actions.forEach(action => {
127
+ this.toolbarActions.set(action.id, action);
128
+ });
129
+ };
130
+
131
+ setContextActions = (actions: ContextAction[]) => {
132
+ this.owner.logger?.info(`setContextActions: ${actions.length} actions`);
133
+ this.contextActions.clear();
134
+ actions.forEach(action => {
135
+ this.contextActions.set(action.id, action);
136
+ });
137
+ };
138
+
139
+ clearAllActions = () => {
140
+ this.owner.logger?.info('clearAllActions');
141
+ this.toolbarActions.clear();
142
+ this.contextActions.clear();
143
+ };
144
+ }
@@ -0,0 +1,147 @@
1
+ import { makeAutoObservable, flow, observable } from 'mobx';
2
+ import type { FileSystemBridge } from '../adapters/FileSystemBridge';
3
+
4
+ export interface UploadFileEntry {
5
+ /** Original File object from the browser */
6
+ file: File;
7
+ /** Target path on the file system */
8
+ targetPath: string;
9
+ /** Upload progress 0-100 */
10
+ progress: number;
11
+ /** Status of this individual file */
12
+ status: 'pending' | 'uploading' | 'done' | 'error';
13
+ /** Error message if status is 'error' */
14
+ error?: string;
15
+ }
16
+
17
+ export class UploadModel {
18
+ private bridge: FileSystemBridge;
19
+
20
+ /** Files currently being uploaded or queued */
21
+ files: UploadFileEntry[] = [];
22
+
23
+ /** Overall upload state */
24
+ isUploading = false;
25
+
26
+ /** Number of completed uploads in the current batch */
27
+ completedCount = 0;
28
+
29
+ /** Whether the upload summary toast is visible */
30
+ showSummary = false;
31
+
32
+ /** Last batch summary */
33
+ lastSummary: { total: number; succeeded: number; failed: number } | null = null;
34
+
35
+ private notify: (type: 'success' | 'error' | 'warning', message: string) => void;
36
+
37
+ constructor(bridge: FileSystemBridge, notify?: (type: 'success' | 'error' | 'warning', message: string) => void) {
38
+ this.bridge = bridge;
39
+ this.notify = notify ?? (() => {});
40
+ makeAutoObservable(this, {
41
+ bridge: false,
42
+ notify: false,
43
+ files: observable.shallow,
44
+ });
45
+ }
46
+
47
+ /** Total number of files in the current batch */
48
+ get totalCount(): number {
49
+ return this.files.length;
50
+ }
51
+
52
+ /** Overall progress across all files (0-100) */
53
+ get overallProgress(): number {
54
+ if (this.files.length === 0) return 0;
55
+ const total = this.files.reduce((sum, f) => sum + f.progress, 0);
56
+ return Math.round(total / this.files.length);
57
+ }
58
+
59
+ /** Number of files that failed */
60
+ get failedCount(): number {
61
+ return this.files.filter(f => f.status === 'error').length;
62
+ }
63
+
64
+ /** Whether there are any active uploads */
65
+ get hasActiveUploads(): boolean {
66
+ return this.isUploading;
67
+ }
68
+
69
+ /**
70
+ * Upload multiple files to a target directory.
71
+ * Uses MobX flow for proper async tracking.
72
+ */
73
+ uploadFiles = flow(function* (
74
+ this: UploadModel,
75
+ fileList: FileList | File[],
76
+ targetDirectory: string
77
+ ) {
78
+ const files = Array.from(fileList);
79
+ if (files.length === 0) return;
80
+
81
+ // Build the upload queue
82
+ this.files = files.map(file => ({
83
+ file,
84
+ targetPath: `${targetDirectory}/${file.name}`.replace(/\/\//g, '/'),
85
+ progress: 0,
86
+ status: 'pending' as const,
87
+ }));
88
+ this.completedCount = 0;
89
+ this.isUploading = true;
90
+ this.showSummary = false;
91
+ this.lastSummary = null;
92
+
93
+ let succeeded = 0;
94
+ let failed = 0;
95
+
96
+ for (let i = 0; i < this.files.length; i++) {
97
+ const entry = this.files[i]!;
98
+ entry.status = 'uploading';
99
+ entry.progress = 0;
100
+
101
+ try {
102
+ // Read file content as ArrayBuffer
103
+ const buffer: ArrayBuffer = yield entry.file.arrayBuffer();
104
+ entry.progress = 50; // Mark 50% after reading into memory
105
+
106
+ // Write to the file system
107
+ yield this.bridge.writeFile(entry.targetPath, Buffer.from(buffer));
108
+
109
+ entry.progress = 100;
110
+ entry.status = 'done';
111
+ succeeded++;
112
+ } catch (err) {
113
+ entry.status = 'error';
114
+ entry.error = err instanceof Error ? err.message : 'Upload failed';
115
+ entry.progress = 0;
116
+ failed++;
117
+ }
118
+
119
+ this.completedCount = i + 1;
120
+ }
121
+
122
+ this.isUploading = false;
123
+ this.lastSummary = { total: files.length, succeeded, failed };
124
+ this.showSummary = true;
125
+
126
+ if (failed === 0) {
127
+ this.notify('success', `Uploaded ${succeeded} file${succeeded > 1 ? 's' : ''}`);
128
+ } else if (succeeded > 0) {
129
+ this.notify('warning', `Uploaded ${succeeded}/${files.length} files (${failed} failed)`);
130
+ } else {
131
+ this.notify('error', `Upload failed: ${failed} file${failed > 1 ? 's' : ''}`);
132
+ }
133
+ });
134
+
135
+ /** Clear the file list and summary */
136
+ clear(): void {
137
+ this.files = [];
138
+ this.completedCount = 0;
139
+ this.showSummary = false;
140
+ this.lastSummary = null;
141
+ }
142
+
143
+ /** Dismiss the summary toast */
144
+ dismissSummary(): void {
145
+ this.showSummary = false;
146
+ }
147
+ }