@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,58 @@
1
+ import React from 'react';
2
+ import {
3
+ AlertDialog,
4
+ AlertDialogContent,
5
+ AlertDialogHeader,
6
+ AlertDialogTitle,
7
+ AlertDialogDescription,
8
+ AlertDialogFooter,
9
+ AlertDialogCancel,
10
+ } from '@anymux/ui/components/alert-dialog';
11
+ import { Button } from '@anymux/ui/components/button';
12
+
13
+ interface DeleteConfirmDialogProps {
14
+ targets: Array<{ path: string; isDirectory: boolean; name: string }>;
15
+ onConfirm: () => void;
16
+ onCancel: () => void;
17
+ }
18
+
19
+ export const DeleteConfirmDialog: React.FC<DeleteConfirmDialogProps> = ({
20
+ targets,
21
+ onConfirm,
22
+ onCancel,
23
+ }) => {
24
+ return (
25
+ <AlertDialog open onOpenChange={(open) => { if (!open) onCancel(); }}>
26
+ <AlertDialogContent>
27
+ <AlertDialogHeader>
28
+ <AlertDialogTitle>
29
+ Delete {targets.length === 1 ? `"${targets[0]?.name}"` : `${targets.length} items`}?
30
+ </AlertDialogTitle>
31
+ <AlertDialogDescription>
32
+ {targets.length === 1 ? (
33
+ <>This will permanently delete <strong>{targets[0]?.name}</strong>.</>
34
+ ) : (
35
+ <span>
36
+ This will permanently delete the following items:
37
+ <ul className="mt-2 list-disc list-inside text-sm">
38
+ {targets.map((t) => (
39
+ <li key={t.path}>{t.name}</li>
40
+ ))}
41
+ </ul>
42
+ </span>
43
+ )}
44
+ </AlertDialogDescription>
45
+ </AlertDialogHeader>
46
+ <AlertDialogFooter>
47
+ <AlertDialogCancel>Cancel</AlertDialogCancel>
48
+ <Button
49
+ variant="destructive"
50
+ onClick={onConfirm}
51
+ >
52
+ Delete
53
+ </Button>
54
+ </AlertDialogFooter>
55
+ </AlertDialogContent>
56
+ </AlertDialog>
57
+ );
58
+ };
@@ -0,0 +1,473 @@
1
+ import React, { useState, useEffect, useCallback, useRef } from 'react';
2
+ import { observer } from 'mobx-react-lite';
3
+ import { cn } from '../../lib/utils';
4
+ import { Badge } from '@anymux/ui/components/badge';
5
+ import { Button } from '@anymux/ui/components/button';
6
+ import { PathBreadcrumb } from '@anymux/ui/components/path-breadcrumb';
7
+ import { ArrowUp, RotateCw, FolderTree, Files, X, ChevronLeft, ChevronRight, Upload, Loader2, PanelRightOpen, PanelRightClose, CheckSquare } from 'lucide-react';
8
+ import { BrowserError } from '@anymux/ui/components/browser-error';
9
+ import { Tree } from '../../tree';
10
+ import { ListItems, ViewTypeSelector, SearchFilter } from '../../list';
11
+ import { ExplorerLayout } from '../../layout/components/ExplorerLayout/ExplorerLayout';
12
+ import { FileBrowserModel } from '../models/FileBrowserModel';
13
+ import type { IFileSystem } from '@anymux/file-system';
14
+ import { registerDefaultViewers } from '../viewers';
15
+ import { globalViewerRegistry } from '../registry/ViewerRegistry';
16
+ import { DeleteConfirmDialog } from './DeleteConfirmDialog';
17
+ import { CreateItemDialog } from './CreateItemDialog';
18
+ import { UploadProgress } from './UploadProgress';
19
+ import { PreviewPane } from './PreviewPane';
20
+ import { ViewerHost } from './ViewerHost';
21
+ import { FileBrowserContext } from '../context/FileBrowserContext';
22
+
23
+ export interface FileBrowserProps {
24
+ fileSystem: IFileSystem;
25
+ initialPath?: string;
26
+ className?: string;
27
+ showBreadcrumbs?: boolean;
28
+ showNavigation?: boolean;
29
+ onError?: (error: { message: string }) => void;
30
+ onPathChange?: (path: string) => void;
31
+ onNotify?: (type: 'success' | 'error' | 'warning', message: string) => void;
32
+ /** Structured action callback with undo support (preferred over onNotify for success actions) */
33
+ onAction?: (action: { type: 'create' | 'rename' | 'delete' | 'upload'; message: string; undo?: () => Promise<void> }) => void;
34
+ }
35
+
36
+ export const FileBrowser: React.FC<FileBrowserProps> = observer(({
37
+ fileSystem,
38
+ initialPath = '/',
39
+ className,
40
+ showBreadcrumbs = true,
41
+ showNavigation = true,
42
+ onError,
43
+ onPathChange,
44
+ onNotify,
45
+ onAction,
46
+ }) => {
47
+ const [model] = useState(() => new FileBrowserModel(fileSystem));
48
+
49
+ // Propagate errors to parent (e.g., auth errors for reconnect UI)
50
+ useEffect(() => {
51
+ if (model.error && onError) {
52
+ onError({ message: model.error });
53
+ }
54
+ }, [model.error, onError]);
55
+
56
+ // Wire callbacks into model (called directly from navigation/operation methods)
57
+ model.onPathChange = onPathChange;
58
+ model.onNotify = onNotify;
59
+ model.onAction = onAction;
60
+
61
+ // Native file drop support (drag files from OS into browser)
62
+ const [isDragOver, setIsDragOver] = useState(false);
63
+ const dragCounterRef = useRef(0);
64
+
65
+ const handleNativeDragOver = useCallback((e: React.DragEvent) => {
66
+ // Only handle native file drops, not internal DnD
67
+ if (e.dataTransfer.types.includes('Files')) {
68
+ e.preventDefault();
69
+ e.dataTransfer.dropEffect = 'copy';
70
+ }
71
+ }, []);
72
+
73
+ const handleNativeDragEnter = useCallback((e: React.DragEvent) => {
74
+ if (e.dataTransfer.types.includes('Files')) {
75
+ e.preventDefault();
76
+ dragCounterRef.current++;
77
+ setIsDragOver(true);
78
+ }
79
+ }, []);
80
+
81
+ const handleNativeDragLeave = useCallback((e: React.DragEvent) => {
82
+ dragCounterRef.current--;
83
+ if (dragCounterRef.current <= 0) {
84
+ dragCounterRef.current = 0;
85
+ setIsDragOver(false);
86
+ }
87
+ }, []);
88
+
89
+ const handleNativeDrop = useCallback(async (e: React.DragEvent) => {
90
+ e.preventDefault();
91
+ dragCounterRef.current = 0;
92
+ setIsDragOver(false);
93
+
94
+ const files = e.dataTransfer.files;
95
+ if (!files || files.length === 0) return;
96
+
97
+ // Delegate to the upload model for progress tracking
98
+ model.uploadFiles(files);
99
+ }, [model]);
100
+
101
+ const isInitialMount = useRef(true);
102
+ useEffect(() => {
103
+ // On initial mount, always load. On subsequent renders, only navigate if path changed.
104
+ if (!isInitialMount.current && initialPath === model.currentPath) return;
105
+ isInitialMount.current = false;
106
+ model.setInitialPath(initialPath);
107
+ }, [initialPath, model]);
108
+
109
+ useEffect(() => {
110
+ registerDefaultViewers();
111
+ }, []);
112
+
113
+ // Keyboard navigation for viewer (left/right arrows, Escape)
114
+ useEffect(() => {
115
+ if (!model.openFile) return;
116
+ const handleKeyDown = (e: KeyboardEvent) => {
117
+ if (e.key === 'Escape') { model.closeFileViewer(); return; }
118
+ if (e.key === 'ArrowRight') { model.navigateToNextFile(); return; }
119
+ if (e.key === 'ArrowLeft') { model.navigateToPrevFile(); return; }
120
+ };
121
+ window.addEventListener('keydown', handleKeyDown);
122
+ return () => window.removeEventListener('keydown', handleKeyDown);
123
+ }, [model, model.openFile]);
124
+
125
+ const handleBreadcrumbClick = (path: string) => {
126
+ model.navigateToPath(path);
127
+ };
128
+
129
+ // Show loading overlay while file is being read from remote
130
+ if (model.isLoadingFile && !model.openFile) {
131
+ return (
132
+ <div className={cn("h-full w-full flex items-center justify-center bg-background", className)}>
133
+ <div className="flex flex-col items-center gap-2 text-muted-foreground">
134
+ <Loader2 className="w-6 h-6 animate-spin" />
135
+ <p className="text-sm">Loading file...</p>
136
+ </div>
137
+ </div>
138
+ );
139
+ }
140
+
141
+ // If a file is open, show the inline ACDSee-style viewer via ViewerHost
142
+ if (model.openFile) {
143
+ const files = model.viewableFiles;
144
+ const idx = model.currentFileIndex;
145
+
146
+ // Resolve viewer from registry using the new plugin API
147
+ const fileCtx = globalViewerRegistry.buildFileMatchContext(
148
+ model.openFile.name,
149
+ {
150
+ path: model.openFile.path,
151
+ mimeType: model.openFile.mimeType,
152
+ size: model.openFile.size,
153
+ }
154
+ );
155
+ const resolvedViewer = globalViewerRegistry.resolveViewer(fileCtx);
156
+
157
+ // Fallback: use the legacy viewer from openFile state if no plugin match
158
+ const viewer = resolvedViewer ?? {
159
+ plugin: {
160
+ id: model.openFile.viewer.id,
161
+ name: model.openFile.viewer.name,
162
+ capabilities: { canPreview: true, canFullscreen: true },
163
+ component: model.openFile.viewer.component,
164
+ },
165
+ Component: model.openFile.viewer.component,
166
+ confidence: 0,
167
+ };
168
+
169
+ // Host-level toolbar: close button, file name, prev/next navigation
170
+ const hostToolbar = (
171
+ <>
172
+ {files.length > 1 && (
173
+ <>
174
+ <Button
175
+ variant="outline"
176
+ size="sm"
177
+ onClick={() => model.navigateToPrevFile()}
178
+ disabled={!model.canNavigatePrev}
179
+ title="Previous file (left arrow)"
180
+ >
181
+ <ChevronLeft className="w-4 h-4" />
182
+ </Button>
183
+ <span className="text-xs text-muted-foreground min-w-[4rem] text-center">
184
+ {idx + 1} / {files.length}
185
+ </span>
186
+ <Button
187
+ variant="outline"
188
+ size="sm"
189
+ onClick={() => model.navigateToNextFile()}
190
+ disabled={!model.canNavigateNext}
191
+ title="Next file (right arrow)"
192
+ >
193
+ <ChevronRight className="w-4 h-4" />
194
+ </Button>
195
+ </>
196
+ )}
197
+ </>
198
+ );
199
+
200
+ return (
201
+ <FileBrowserContext.Provider value={{
202
+ renameState: null,
203
+ onRenameCommit: () => {},
204
+ onRenameCancel: () => {},
205
+ }}>
206
+ <div className={cn("h-full w-full flex flex-col bg-background", className)}>
207
+ {/* Host-level toolbar with close/file info */}
208
+ <div className="flex items-center justify-between px-2 sm:px-3 py-1.5 sm:py-2 border-b bg-muted/30 flex-shrink-0">
209
+ <div className="flex items-center gap-1 sm:gap-2 min-w-0">
210
+ <Button variant="ghost" size="sm" onClick={() => model.closeFileViewer()} title="Back to files (Esc)" className="flex-shrink-0">
211
+ <X className="w-4 h-4 sm:mr-1" /> <span className="hidden sm:inline">Close</span>
212
+ </Button>
213
+ <span className="text-xs sm:text-sm font-medium truncate max-w-[120px] sm:max-w-[300px]" title={model.openFile.path}>
214
+ {model.openFile.name}
215
+ </span>
216
+ {model.openFile.size != null && (
217
+ <span className="text-xs text-muted-foreground hidden sm:inline">
218
+ ({model.openFile.size > 1024 * 1024
219
+ ? `${(model.openFile.size / (1024 * 1024)).toFixed(1)} MB`
220
+ : `${(model.openFile.size / 1024).toFixed(1)} KB`
221
+ })
222
+ </span>
223
+ )}
224
+ </div>
225
+ </div>
226
+
227
+ {/* ViewerHost renders the plugin component + its toolbar */}
228
+ <div className="flex-1 min-h-0">
229
+ <ViewerHost
230
+ viewer={viewer}
231
+ file={{
232
+ path: model.openFile.path,
233
+ name: model.openFile.name,
234
+ content: model.openFile.content,
235
+ size: model.openFile.size,
236
+ mimeType: model.openFile.mimeType,
237
+ }}
238
+ mode="full"
239
+ onClose={() => model.closeFileViewer()}
240
+ hostToolbar={hostToolbar}
241
+ className="h-full"
242
+ />
243
+ </div>
244
+ </div>
245
+ </FileBrowserContext.Provider>
246
+ );
247
+ }
248
+
249
+ // Header content
250
+ const headerContent = (showBreadcrumbs || showNavigation) ? (
251
+ <div className="flex items-center justify-between px-2 py-1.5 bg-muted/10">
252
+ <div className="flex items-center gap-2">
253
+ {showNavigation && (
254
+ <>
255
+ <Button
256
+ variant="outline"
257
+ size="sm"
258
+ onClick={() => model.navigateUp()}
259
+ disabled={!model.canNavigateUp}
260
+ title="Navigate up"
261
+ >
262
+ <ArrowUp className="w-4 h-4" />
263
+ </Button>
264
+ <Button
265
+ variant="outline"
266
+ size="sm"
267
+ onClick={() => model.refresh()}
268
+ disabled={model.isLoading}
269
+ title="Refresh"
270
+ >
271
+ <RotateCw className="w-4 h-4" />
272
+ </Button>
273
+ <Button
274
+ variant="outline"
275
+ size="sm"
276
+ onClick={() => model.triggerFileUpload()}
277
+ disabled={model.uploadModel.isUploading}
278
+ title="Upload files"
279
+ >
280
+ <Upload className="w-4 h-4" />
281
+ </Button>
282
+ {model.listModel.isLoading && (
283
+ <Loader2 className="w-4 h-4 animate-spin text-muted-foreground" />
284
+ )}
285
+ </>
286
+ )}
287
+
288
+ {showBreadcrumbs && (
289
+ <PathBreadcrumb
290
+ path={model.currentPath}
291
+ onNavigate={handleBreadcrumbClick}
292
+ showHome
293
+ editable
294
+ className="text-[13px]"
295
+ />
296
+ )}
297
+ </div>
298
+ </div>
299
+ ) : undefined;
300
+
301
+ // Sidebar content (Tree Navigation)
302
+ const sidebarContent = (
303
+ <Tree
304
+ provider={model.treeProvider}
305
+ model={model.treeModel}
306
+ className="h-full"
307
+ />
308
+ );
309
+
310
+ const sidebarToolbar = (
311
+ <h3 className="text-sm font-medium flex items-center gap-1.5"><FolderTree className="w-4 h-4 text-muted-foreground" />Folders</h3>
312
+ );
313
+
314
+ // Main content (List Items + optional Preview Pane)
315
+ const listContent = model.error ? (
316
+ <BrowserError
317
+ error={model.error}
318
+ context="File Browser"
319
+ onRetry={() => model.refresh()}
320
+ onGoBack={model.canNavigateUp ? () => model.navigateUp() : undefined}
321
+ />
322
+ ) : (
323
+ <ListItems
324
+ model={model.listModel}
325
+ className="h-full"
326
+ />
327
+ );
328
+
329
+ const previewWidth = model.previewEnabled && model.previewFile ? 360 : 0;
330
+
331
+ const mainContent = model.previewEnabled ? (
332
+ <div className="flex h-full w-full">
333
+ <div className="flex-1 min-w-0 h-full overflow-hidden">
334
+ {listContent}
335
+ </div>
336
+ <div
337
+ className={cn(
338
+ "flex-shrink-0 h-full overflow-hidden transition-[width] duration-200 ease-in-out",
339
+ previewWidth > 0 && "border-l"
340
+ )}
341
+ style={{ width: previewWidth }}
342
+ >
343
+ {previewWidth > 0 && (
344
+ <PreviewPane
345
+ previewFile={model.previewFile}
346
+ className="h-full"
347
+ />
348
+ )}
349
+ </div>
350
+ </div>
351
+ ) : listContent;
352
+
353
+ const mainToolbar = (
354
+ <div className="flex items-center justify-between">
355
+ <div className="flex items-center gap-2">
356
+ <h3 className="text-sm font-medium flex items-center gap-1.5"><Files className="w-4 h-4 text-muted-foreground" />Contents</h3>
357
+ {model.listModel.totalItemCount > 0 && (
358
+ <Badge variant="secondary" className="text-xs">
359
+ {model.listModel.hasSearchQuery
360
+ ? `${model.listModel.items.length} / ${model.listModel.totalItemCount}`
361
+ : model.listModel.totalItemCount} items
362
+ </Badge>
363
+ )}
364
+ <SearchFilter model={model.listModel} />
365
+ </div>
366
+ <div className="flex items-center gap-1">
367
+ <ViewTypeSelector
368
+ viewTypes={model.listProvider.supportedViewTypes}
369
+ currentViewType={model.listModel.currentViewType}
370
+ onViewTypeChange={(viewType) => model.listModel.setViewType(viewType)}
371
+ />
372
+ <Button
373
+ variant={model.listModel.showCheckboxes ? 'secondary' : 'ghost'}
374
+ size="icon"
375
+ className="h-7 w-7"
376
+ onClick={() => model.listModel.toggleCheckboxes()}
377
+ title={model.listModel.showCheckboxes ? 'Hide checkboxes' : 'Show checkboxes for multi-select'}
378
+ >
379
+ <CheckSquare className="h-4 w-4" />
380
+ </Button>
381
+ <Button
382
+ variant={model.previewEnabled ? 'secondary' : 'ghost'}
383
+ size="icon"
384
+ className="h-7 w-7"
385
+ onClick={() => model.togglePreview()}
386
+ title={model.previewEnabled ? 'Hide preview pane' : 'Show preview pane'}
387
+ >
388
+ {model.previewEnabled ? (
389
+ <PanelRightClose className="h-4 w-4" />
390
+ ) : (
391
+ <PanelRightOpen className="h-4 w-4" />
392
+ )}
393
+ </Button>
394
+ </div>
395
+ </div>
396
+ );
397
+
398
+ const contextValue = {
399
+ renameState: model.renameState ? { itemId: model.renameState.itemId, currentName: model.renameState.currentName, source: model.renameState.source } : null,
400
+ onRenameCommit: (newName: string) => model.confirmRename(newName),
401
+ onRenameCancel: () => model.cancelRename(),
402
+ };
403
+
404
+ return (
405
+ <FileBrowserContext.Provider value={contextValue}>
406
+ <div
407
+ className={cn("h-full w-full relative", className)}
408
+ onDragOver={handleNativeDragOver}
409
+ onDragEnter={handleNativeDragEnter}
410
+ onDragLeave={handleNativeDragLeave}
411
+ onDrop={handleNativeDrop}
412
+ >
413
+ {/* Native file drop overlay */}
414
+ {isDragOver && (
415
+ <div className="absolute inset-0 z-50 bg-blue-500/10 border-2 border-dashed border-blue-500 rounded-lg flex items-center justify-center pointer-events-none">
416
+ <div className="flex flex-col items-center gap-2 text-blue-600 dark:text-blue-400">
417
+ <Upload className="w-8 h-8" />
418
+ <p className="text-sm font-medium">Drop files here to upload</p>
419
+ <p className="text-xs text-muted-foreground">Files will be added to {model.currentPath}</p>
420
+ </div>
421
+ </div>
422
+ )}
423
+ <ExplorerLayout
424
+ sections={{
425
+ header: headerContent,
426
+ sidebar: {
427
+ content: sidebarContent,
428
+ toolbar: sidebarToolbar,
429
+ width: 260
430
+ },
431
+ main: {
432
+ content: mainContent,
433
+ toolbar: mainToolbar
434
+ }
435
+ }}
436
+ />
437
+
438
+ {/* Delete confirmation dialog */}
439
+ {model.deleteState && (
440
+ <DeleteConfirmDialog
441
+ targets={model.deleteState.targets}
442
+ onConfirm={() => model.confirmDelete()}
443
+ onCancel={() => model.cancelDelete()}
444
+ />
445
+ )}
446
+
447
+ {/* Create item dialog */}
448
+ {model.createState && (
449
+ <CreateItemDialog
450
+ type={model.createState.type}
451
+ onConfirm={(name) => model.confirmCreate(name)}
452
+ onCancel={() => model.cancelCreate()}
453
+ />
454
+ )}
455
+
456
+ {/* Upload progress panel */}
457
+ <UploadProgress uploadModel={model.uploadModel} />
458
+
459
+ {/* Loading overlay */}
460
+ {model.isLoading && !model.hasListItems && (
461
+ <div className="absolute inset-0 bg-background/80 flex items-center justify-center z-50">
462
+ <div className="text-center">
463
+ <div className="animate-spin w-6 h-6 border-2 border-primary border-t-transparent rounded-full mx-auto"></div>
464
+ <p className="text-sm text-muted-foreground mt-2">Loading...</p>
465
+ </div>
466
+ </div>
467
+ )}
468
+ </div>
469
+ </FileBrowserContext.Provider>
470
+ );
471
+ });
472
+
473
+ FileBrowser.displayName = 'FileBrowser';