@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.
- package/dist/ExplorerLayout-CSIJd7N4.js +105 -0
- package/dist/ExplorerLayout-CSIJd7N4.js.map +1 -0
- package/dist/FileBrowserContext-B6jixa2j.js +11 -0
- package/dist/FileBrowserContext-B6jixa2j.js.map +1 -0
- package/dist/calendar-DSlrbHoj.js +761 -0
- package/dist/calendar-DSlrbHoj.js.map +1 -0
- package/dist/calendar.d.ts +3 -0
- package/dist/calendar.js +3 -0
- package/dist/contacts-DQXTZzHc.js +539 -0
- package/dist/contacts-DQXTZzHc.js.map +1 -0
- package/dist/contacts.d.ts +3 -0
- package/dist/contacts.js +3 -0
- package/dist/file-browser-m5atC3kF.js +6755 -0
- package/dist/file-browser-m5atC3kF.js.map +1 -0
- package/dist/file-browser.d.ts +11 -0
- package/dist/file-browser.js +9 -0
- package/dist/git-B55e6LL-.js +561 -0
- package/dist/git-B55e6LL-.js.map +1 -0
- package/dist/git.d.ts +2 -0
- package/dist/git.js +3 -0
- package/dist/iconMap-V4B8P-Uh.js +206 -0
- package/dist/iconMap-V4B8P-Uh.js.map +1 -0
- package/dist/icons-CIsIOZXR.js +0 -0
- package/dist/icons.d.ts +2 -0
- package/dist/icons.js +4 -0
- package/dist/index-BNmNIWBL.d.ts +71 -0
- package/dist/index-BNmNIWBL.d.ts.map +1 -0
- package/dist/index-Bryv_GCG.d.ts +1481 -0
- package/dist/index-Bryv_GCG.d.ts.map +1 -0
- package/dist/index-CuQIjSXs.d.ts +134 -0
- package/dist/index-CuQIjSXs.d.ts.map +1 -0
- package/dist/index-DSu19mq0.d.ts +153 -0
- package/dist/index-DSu19mq0.d.ts.map +1 -0
- package/dist/index-DmsyeHFr.d.ts +149 -0
- package/dist/index-DmsyeHFr.d.ts.map +1 -0
- package/dist/index-DxnJ8FYM.d.ts +17 -0
- package/dist/index-DxnJ8FYM.d.ts.map +1 -0
- package/dist/index-DzfY1Tok.d.ts +32 -0
- package/dist/index-DzfY1Tok.d.ts.map +1 -0
- package/dist/index-Ml_SgiKa.d.ts +1847 -0
- package/dist/index-Ml_SgiKa.d.ts.map +1 -0
- package/dist/index-kHr9udZD.d.ts +1025 -0
- package/dist/index-kHr9udZD.d.ts.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +15 -0
- package/dist/layout-Ca_4r8ka.js +89 -0
- package/dist/layout-Ca_4r8ka.js.map +1 -0
- package/dist/layout.d.ts +2 -0
- package/dist/layout.js +5 -0
- package/dist/list-CxfT6hix.js +6831 -0
- package/dist/list-CxfT6hix.js.map +1 -0
- package/dist/list.d.ts +2 -0
- package/dist/list.js +5 -0
- package/dist/media-DZ292aKK.js +557 -0
- package/dist/media-DZ292aKK.js.map +1 -0
- package/dist/media.d.ts +3 -0
- package/dist/media.js +3 -0
- package/dist/tree-Dd9Z0Aso.js +3351 -0
- package/dist/tree-Dd9Z0Aso.js.map +1 -0
- package/dist/tree.d.ts +2 -0
- package/dist/tree.js +6 -0
- package/dist/types-common-CB3kRek8.d.ts +26 -0
- package/dist/types-common-CB3kRek8.d.ts.map +1 -0
- package/dist/utils-B4fdKKsy.js +3 -0
- package/package.json +109 -0
- package/src/calendar/AgendaView.tsx +37 -0
- package/src/calendar/CalendarBrowser.tsx +90 -0
- package/src/calendar/CalendarModel.ts +142 -0
- package/src/calendar/CalendarSidebar.tsx +81 -0
- package/src/calendar/DayView.tsx +76 -0
- package/src/calendar/EventCard.tsx +51 -0
- package/src/calendar/MockCalendarProvider.ts +98 -0
- package/src/calendar/MonthView.tsx +77 -0
- package/src/calendar/WeekView.tsx +129 -0
- package/src/calendar/index.ts +18 -0
- package/src/calendar/types.ts +25 -0
- package/src/contacts/ContactAvatar.tsx +35 -0
- package/src/contacts/ContactBrowser.tsx +56 -0
- package/src/contacts/ContactCard.tsx +37 -0
- package/src/contacts/ContactDetail.tsx +63 -0
- package/src/contacts/ContactGroupSidebar.tsx +40 -0
- package/src/contacts/ContactList.tsx +32 -0
- package/src/contacts/ContactListModel.ts +120 -0
- package/src/contacts/MockContactProvider.ts +77 -0
- package/src/contacts/index.ts +17 -0
- package/src/contacts/types.ts +26 -0
- package/src/demos/CalendarBrowserDemo.tsx +15 -0
- package/src/demos/ContactBrowserDemo.tsx +15 -0
- package/src/demos/MediaBrowserDemo.tsx +15 -0
- package/src/file-browser/adapters/DocumentViewerAdapter.ts +371 -0
- package/src/file-browser/adapters/FileSystemBridge.ts +168 -0
- package/src/file-browser/adapters/GitBrowserAdapter.ts +546 -0
- package/src/file-browser/adapters/README.md +504 -0
- package/src/file-browser/adapters/index.ts +27 -0
- package/src/file-browser/adapters/types.ts +70 -0
- package/src/file-browser/architecture.md +645 -0
- package/src/file-browser/components/CreateItemDialog.tsx +71 -0
- package/src/file-browser/components/DeleteConfirmDialog.tsx +58 -0
- package/src/file-browser/components/FileBrowser.tsx +473 -0
- package/src/file-browser/components/FileBrowserContent.tsx +209 -0
- package/src/file-browser/components/FileBrowserHeader.tsx +151 -0
- package/src/file-browser/components/FileBrowserToolbar.tsx +145 -0
- package/src/file-browser/components/LeftPanel/LeftPanel.tsx +103 -0
- package/src/file-browser/components/LeftPanel/LeftPanelTabs.tsx +70 -0
- package/src/file-browser/components/LeftPanel/TreeNavigationView.tsx +256 -0
- package/src/file-browser/components/PreviewPane.tsx +146 -0
- package/src/file-browser/components/RightPanel/FilePreview.tsx +219 -0
- package/src/file-browser/components/RightPanel/RightPanel.tsx +186 -0
- package/src/file-browser/components/RightPanel/RightPanelToolbar.tsx +113 -0
- package/src/file-browser/components/UploadProgress.tsx +123 -0
- package/src/file-browser/components/ViewerHost.tsx +208 -0
- package/src/file-browser/components/mobile/MobileNavigation.tsx +227 -0
- package/src/file-browser/components/navigation/NavigationButtons.tsx +171 -0
- package/src/file-browser/components/shared/ErrorBoundary.tsx +116 -0
- package/src/file-browser/components/shared/FileBrowserItem.tsx +195 -0
- package/src/file-browser/components/shared/FileIcon.tsx +169 -0
- package/src/file-browser/components/toolbar/ViewModeToggle.tsx +200 -0
- package/src/file-browser/components/views/ListView/ListView.tsx +484 -0
- package/src/file-browser/components/views/ThumbnailView/ThumbnailView.tsx +323 -0
- package/src/file-browser/components/views/TreeView/TreeNode.tsx +186 -0
- package/src/file-browser/components/views/TreeView/TreeNodeList.tsx +191 -0
- package/src/file-browser/components/views/TreeView/TreeView.tsx +200 -0
- package/src/file-browser/components/views/TreemapView/TreemapView.tsx +339 -0
- package/src/file-browser/context/FileBrowserContext.tsx +13 -0
- package/src/file-browser/examples/BasicUsage.tsx +20 -0
- package/src/file-browser/index.ts +98 -0
- package/src/file-browser/models/FileBrowserModel.ts +623 -0
- package/src/file-browser/models/LeftPanelManagerModel.ts +105 -0
- package/src/file-browser/models/NavigationManagerModel.ts +312 -0
- package/src/file-browser/models/ResponsiveLayoutManagerModel.ts +437 -0
- package/src/file-browser/models/RightPanelManagerModel.ts +190 -0
- package/src/file-browser/models/SelectionManagerModel.ts +252 -0
- package/src/file-browser/models/ToolbarManagerModel.ts +144 -0
- package/src/file-browser/models/UploadModel.ts +147 -0
- package/src/file-browser/models/ViewModeManagerModel.ts +185 -0
- package/src/file-browser/models/ViewerHostModel.ts +44 -0
- package/src/file-browser/models/ui/ListViewUIModel.ts +265 -0
- package/src/file-browser/models/ui/PreviewUIModel.ts +297 -0
- package/src/file-browser/models/ui/ThumbnailViewUIModel.ts +254 -0
- package/src/file-browser/models/ui/TreeViewUIModel.ts +128 -0
- package/src/file-browser/models/ui/TreemapViewUIModel.ts +350 -0
- package/src/file-browser/providers/FileSystemListProvider.ts +552 -0
- package/src/file-browser/providers/FileSystemProvider.ts +401 -0
- package/src/file-browser/providers/FileSystemTreeProvider.ts +231 -0
- package/src/file-browser/providers/GitProvider.ts +337 -0
- package/src/file-browser/providers/GitRepositoryProvider.ts +376 -0
- package/src/file-browser/providers/IFileBrowserProvider.ts +56 -0
- package/src/file-browser/providers/MemoryProvider.ts +303 -0
- package/src/file-browser/providers/index.ts +4 -0
- package/src/file-browser/registry/ViewerRegistry.ts +551 -0
- package/src/file-browser/registry/types.ts +144 -0
- package/src/file-browser/scripts/performanceBenchmark.ts +553 -0
- package/src/file-browser/services/ThumbnailCacheService.ts +128 -0
- package/src/file-browser/tasks.md +537 -0
- package/src/file-browser/types/FileBrowserTypes.ts +126 -0
- package/src/file-browser/types/ProviderTypes.ts +155 -0
- package/src/file-browser/types/UITypes.ts +235 -0
- package/src/file-browser/types/ViewModeTypes.ts +150 -0
- package/src/file-browser/utils/gestures.ts +327 -0
- package/src/file-browser/utils/performance.ts +563 -0
- package/src/file-browser/viewers/ImageViewer.tsx +163 -0
- package/src/file-browser/viewers/ImageViewerModel.ts +79 -0
- package/src/file-browser/viewers/TextViewer.tsx +95 -0
- package/src/file-browser/viewers/UnsupportedFileViewer.tsx +57 -0
- package/src/file-browser/viewers/index.ts +61 -0
- package/src/git/BranchList.tsx +128 -0
- package/src/git/CommitGraph.tsx +239 -0
- package/src/git/CommitList.tsx +258 -0
- package/src/git/DiffViewer.tsx +219 -0
- package/src/git/index.ts +4 -0
- package/src/icons/iconMap.ts +146 -0
- package/src/icons/index.ts +9 -0
- package/src/index.ts +13 -0
- package/src/layout/README.md +307 -0
- package/src/layout/components/ExplorerLayout/ExplorerLayout.tsx +178 -0
- package/src/layout/examples/SimpleExample.tsx +60 -0
- package/src/layout/index.ts +6 -0
- package/src/lib/utils.ts +1 -0
- package/src/list/README.md +303 -0
- package/src/list/architecture.md +807 -0
- package/src/list/components/CalculatedGridView.tsx +252 -0
- package/src/list/components/DragPreview.tsx +102 -0
- package/src/list/components/ListContextMenu.tsx +274 -0
- package/src/list/components/ListItem.tsx +761 -0
- package/src/list/components/ListItems.tsx +919 -0
- package/src/list/components/MasonryView.tsx +241 -0
- package/src/list/components/SearchFilter.tsx +44 -0
- package/src/list/components/TreemapView.tsx +709 -0
- package/src/list/components/ViewSizeControls.tsx +205 -0
- package/src/list/components/ViewTypeSelector.tsx +312 -0
- package/src/list/components/VirtualizedDetailsView.tsx +231 -0
- package/src/list/components/VirtualizedGrid.tsx +164 -0
- package/src/list/components/VirtualizedList.tsx +154 -0
- package/src/list/components/VirtualizedMasonryView.tsx +344 -0
- package/src/list/components/shared/EmptyState.tsx +103 -0
- package/src/list/components/shared/ErrorBoundary.tsx +123 -0
- package/src/list/components/shared/ErrorDisplay.tsx +100 -0
- package/src/list/components/shared/ListLoader.tsx +146 -0
- package/src/list/components/shared/LoadingIndicator.tsx +80 -0
- package/src/list/index.ts +92 -0
- package/src/list/models/ListItemsModel.ts +1301 -0
- package/src/list/models/TreemapModel.ts +204 -0
- package/src/list/providers/ListItemsProvider.ts +313 -0
- package/src/list/providers/TestListProvider.ts +604 -0
- package/src/list/tasks.md +937 -0
- package/src/list/types/ListTypes.ts +178 -0
- package/src/list/utils/BenchmarkLogger.ts +243 -0
- package/src/list/utils/DragDropManager.ts +320 -0
- package/src/list/utils/GridLayoutCalculator.ts +290 -0
- package/src/list/utils/ListAccessibility.ts +367 -0
- package/src/list/utils/ListKeyboard.ts +414 -0
- package/src/list/utils/MasonryLayoutCalculator.ts +302 -0
- package/src/list/utils/MasonryLayoutEngine.ts +401 -0
- package/src/list/utils/__tests__/MasonryLayoutEngine.test.ts +157 -0
- package/src/list/utils/__tests__/VirtualizedMasonryView.test.tsx +251 -0
- package/src/media/AlbumSidebar.tsx +48 -0
- package/src/media/MediaBrowser.tsx +92 -0
- package/src/media/MediaBrowserModel.ts +138 -0
- package/src/media/MediaGrid.tsx +50 -0
- package/src/media/MediaList.tsx +49 -0
- package/src/media/MediaPreview.tsx +63 -0
- package/src/media/MediaTimeline.tsx +38 -0
- package/src/media/MockMediaProvider.ts +70 -0
- package/src/media/index.ts +18 -0
- package/src/media/types.ts +21 -0
- package/src/styles/variables.css +60 -0
- package/src/tree/DEVELOPMENT_SUMMARY.md +170 -0
- package/src/tree/__tests__/TreeModel.test.ts +16 -0
- package/src/tree/architecture.md +530 -0
- package/src/tree/components/Tree.tsx +283 -0
- package/src/tree/components/TreeCheckbox.tsx +147 -0
- package/src/tree/components/TreeContextMenu.tsx +139 -0
- package/src/tree/components/TreeNodeList.tsx +329 -0
- package/src/tree/components/TreeTable.tsx +382 -0
- package/src/tree/index.ts +58 -0
- package/src/tree/models/TreeModel.ts +839 -0
- package/src/tree/providers/SimpleTreeProvider.ts +463 -0
- package/src/tree/providers/TestTreeProvider.ts +946 -0
- package/src/tree/providers/TreeProvider.ts +308 -0
- package/src/tree/tasks.md +2046 -0
- package/src/tree/types/TreeTypes.ts +279 -0
- package/src/tree/utils/SelectionTheme.ts +150 -0
- package/src/tree/utils/logger.ts +203 -0
- 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';
|