@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,227 @@
|
|
|
1
|
+
import React, { useRef, useEffect } from 'react';
|
|
2
|
+
import { observer } from 'mobx-react-lite';
|
|
3
|
+
import { X, ChevronLeft, ChevronRight } from 'lucide-react';
|
|
4
|
+
import { ResponsiveLayoutManagerModel } from '../../models/ResponsiveLayoutManagerModel';
|
|
5
|
+
import { useGestures } from '../../utils/gestures';
|
|
6
|
+
|
|
7
|
+
// Utility for class name concatenation
|
|
8
|
+
const cn = (...classes: (string | undefined | boolean)[]) => {
|
|
9
|
+
return classes.filter(Boolean).join(' ');
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
interface MobileNavigationProps {
|
|
13
|
+
responsiveLayout: ResponsiveLayoutManagerModel;
|
|
14
|
+
children: React.ReactNode;
|
|
15
|
+
className?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const MobileNavigation = observer(({
|
|
19
|
+
responsiveLayout,
|
|
20
|
+
children,
|
|
21
|
+
className
|
|
22
|
+
}: MobileNavigationProps) => {
|
|
23
|
+
const overlayRef = useRef<HTMLDivElement>(null);
|
|
24
|
+
const panelRef = useRef<HTMLDivElement>(null);
|
|
25
|
+
|
|
26
|
+
// Setup gesture handling
|
|
27
|
+
useGestures(
|
|
28
|
+
overlayRef as React.RefObject<HTMLElement>,
|
|
29
|
+
{
|
|
30
|
+
onSwipeDown: () => {
|
|
31
|
+
if (responsiveLayout.isMobileMenuOpen) {
|
|
32
|
+
responsiveLayout.hideMobilePanel();
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
onSwipeLeft: () => {
|
|
36
|
+
if (responsiveLayout.activePanel === 'left') {
|
|
37
|
+
responsiveLayout.showMobilePanel('right');
|
|
38
|
+
} else {
|
|
39
|
+
responsiveLayout.hideMobilePanel();
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
onSwipeRight: () => {
|
|
43
|
+
if (responsiveLayout.activePanel === 'right') {
|
|
44
|
+
responsiveLayout.showMobilePanel('left');
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
// Focus management
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
if (responsiveLayout.isMobileMenuOpen && panelRef.current) {
|
|
53
|
+
// Focus first focusable element in panel
|
|
54
|
+
const focusableElements = panelRef.current.querySelectorAll(
|
|
55
|
+
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
|
|
56
|
+
);
|
|
57
|
+
const firstFocusable = focusableElements[0] as HTMLElement;
|
|
58
|
+
firstFocusable?.focus();
|
|
59
|
+
}
|
|
60
|
+
}, [responsiveLayout.isMobileMenuOpen, responsiveLayout.activePanel]);
|
|
61
|
+
|
|
62
|
+
// Handle escape key
|
|
63
|
+
useEffect(() => {
|
|
64
|
+
const handleEscape = (e: KeyboardEvent) => {
|
|
65
|
+
if (e.key === 'Escape' && responsiveLayout.isMobileMenuOpen) {
|
|
66
|
+
responsiveLayout.hideMobilePanel();
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
if (responsiveLayout.trapFocus) {
|
|
71
|
+
document.addEventListener('keydown', handleEscape);
|
|
72
|
+
return () => document.removeEventListener('keydown', handleEscape);
|
|
73
|
+
}
|
|
74
|
+
}, [responsiveLayout.trapFocus, responsiveLayout.isMobileMenuOpen]);
|
|
75
|
+
|
|
76
|
+
// Prevent body scroll when overlay is open
|
|
77
|
+
useEffect(() => {
|
|
78
|
+
if (responsiveLayout.isMobileMenuOpen) {
|
|
79
|
+
document.body.style.overflow = 'hidden';
|
|
80
|
+
} else {
|
|
81
|
+
document.body.style.overflow = '';
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return () => {
|
|
85
|
+
document.body.style.overflow = '';
|
|
86
|
+
};
|
|
87
|
+
}, [responsiveLayout.isMobileMenuOpen]);
|
|
88
|
+
|
|
89
|
+
if (!responsiveLayout.isMobile) {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return (
|
|
94
|
+
<>
|
|
95
|
+
{/* Mobile Navigation Button */}
|
|
96
|
+
<button
|
|
97
|
+
onClick={() => responsiveLayout.toggleMobileMenu()}
|
|
98
|
+
className={cn(
|
|
99
|
+
// Touch-friendly size (44px minimum)
|
|
100
|
+
'w-11 h-11 flex items-center justify-center',
|
|
101
|
+
'bg-background border border-border rounded-md',
|
|
102
|
+
'text-foreground hover:bg-accent hover:text-accent-foreground',
|
|
103
|
+
'transition-all duration-200',
|
|
104
|
+
'active:scale-95', // Touch feedback
|
|
105
|
+
responsiveLayout.isMobileMenuOpen && 'bg-accent text-accent-foreground'
|
|
106
|
+
)}
|
|
107
|
+
aria-label={responsiveLayout.isMobileMenuOpen ? 'Close navigation menu' : 'Open navigation menu'}
|
|
108
|
+
aria-expanded={responsiveLayout.isMobileMenuOpen}
|
|
109
|
+
>
|
|
110
|
+
{responsiveLayout.isMobileMenuOpen ? (
|
|
111
|
+
<X className="w-5 h-5" />
|
|
112
|
+
) : (
|
|
113
|
+
<div className="flex flex-col space-y-1">
|
|
114
|
+
<div className="w-4 h-0.5 bg-current" />
|
|
115
|
+
<div className="w-4 h-0.5 bg-current" />
|
|
116
|
+
<div className="w-4 h-0.5 bg-current" />
|
|
117
|
+
</div>
|
|
118
|
+
)}
|
|
119
|
+
</button>
|
|
120
|
+
|
|
121
|
+
{/* Mobile Overlay */}
|
|
122
|
+
{responsiveLayout.isMobileMenuOpen && (
|
|
123
|
+
<div
|
|
124
|
+
ref={overlayRef}
|
|
125
|
+
className={cn(
|
|
126
|
+
'fixed inset-0 z-50',
|
|
127
|
+
'bg-background/80 backdrop-blur-sm',
|
|
128
|
+
'transition-all duration-300',
|
|
129
|
+
responsiveLayout.isAnimating && 'transition-all duration-300'
|
|
130
|
+
)}
|
|
131
|
+
onClick={(e) => {
|
|
132
|
+
// Close on backdrop click
|
|
133
|
+
if (e.target === e.currentTarget) {
|
|
134
|
+
responsiveLayout.hideMobilePanel();
|
|
135
|
+
}
|
|
136
|
+
}}
|
|
137
|
+
>
|
|
138
|
+
{/* Panel Container */}
|
|
139
|
+
<div
|
|
140
|
+
ref={panelRef}
|
|
141
|
+
className={cn(
|
|
142
|
+
'absolute top-0 bottom-0 w-80 max-w-[85vw]',
|
|
143
|
+
'bg-background border-r border-border',
|
|
144
|
+
'shadow-xl',
|
|
145
|
+
'transition-transform duration-300 ease-out',
|
|
146
|
+
'overflow-y-auto',
|
|
147
|
+
// Panel positioning and animation
|
|
148
|
+
responsiveLayout.activePanel === 'left' && 'left-0 transform-gpu translate-x-0',
|
|
149
|
+
responsiveLayout.activePanel === 'right' && 'right-0 transform-gpu translate-x-0',
|
|
150
|
+
!responsiveLayout.activePanel && 'translate-x-full',
|
|
151
|
+
className
|
|
152
|
+
)}
|
|
153
|
+
role="dialog"
|
|
154
|
+
aria-modal="true"
|
|
155
|
+
aria-label={`${responsiveLayout.activePanel} navigation panel`}
|
|
156
|
+
>
|
|
157
|
+
{/* Panel Header */}
|
|
158
|
+
<div className="flex items-center justify-between p-4 border-b border-border">
|
|
159
|
+
<h2 className="text-lg font-semibold">
|
|
160
|
+
{responsiveLayout.activePanel === 'left' ? 'Navigation' : 'Content'}
|
|
161
|
+
</h2>
|
|
162
|
+
|
|
163
|
+
<div className="flex items-center space-x-2">
|
|
164
|
+
{/* Panel switcher */}
|
|
165
|
+
{responsiveLayout.activePanel === 'left' && (
|
|
166
|
+
<button
|
|
167
|
+
onClick={() => responsiveLayout.showMobilePanel('right')}
|
|
168
|
+
className={cn(
|
|
169
|
+
'w-9 h-9 flex items-center justify-center',
|
|
170
|
+
'bg-muted hover:bg-muted-foreground/10 rounded-md',
|
|
171
|
+
'transition-colors duration-200'
|
|
172
|
+
)}
|
|
173
|
+
aria-label="Switch to content panel"
|
|
174
|
+
>
|
|
175
|
+
<ChevronRight className="w-4 h-4" />
|
|
176
|
+
</button>
|
|
177
|
+
)}
|
|
178
|
+
|
|
179
|
+
{responsiveLayout.activePanel === 'right' && (
|
|
180
|
+
<button
|
|
181
|
+
onClick={() => responsiveLayout.showMobilePanel('left')}
|
|
182
|
+
className={cn(
|
|
183
|
+
'w-9 h-9 flex items-center justify-center',
|
|
184
|
+
'bg-muted hover:bg-muted-foreground/10 rounded-md',
|
|
185
|
+
'transition-colors duration-200'
|
|
186
|
+
)}
|
|
187
|
+
aria-label="Switch to navigation panel"
|
|
188
|
+
>
|
|
189
|
+
<ChevronLeft className="w-4 h-4" />
|
|
190
|
+
</button>
|
|
191
|
+
)}
|
|
192
|
+
|
|
193
|
+
{/* Close button */}
|
|
194
|
+
<button
|
|
195
|
+
onClick={() => responsiveLayout.hideMobilePanel()}
|
|
196
|
+
className={cn(
|
|
197
|
+
'w-9 h-9 flex items-center justify-center',
|
|
198
|
+
'bg-muted hover:bg-muted-foreground/10 rounded-md',
|
|
199
|
+
'transition-colors duration-200'
|
|
200
|
+
)}
|
|
201
|
+
aria-label="Close panel"
|
|
202
|
+
>
|
|
203
|
+
<X className="w-4 h-4" />
|
|
204
|
+
</button>
|
|
205
|
+
</div>
|
|
206
|
+
</div>
|
|
207
|
+
|
|
208
|
+
{/* Panel Content */}
|
|
209
|
+
<div className="flex-1">
|
|
210
|
+
{children}
|
|
211
|
+
</div>
|
|
212
|
+
|
|
213
|
+
{/* Swipe Indicator */}
|
|
214
|
+
<div className="p-4 border-t border-border">
|
|
215
|
+
<div className="flex items-center justify-center space-x-1 text-xs text-muted-foreground">
|
|
216
|
+
<div className="w-6 h-1 bg-muted rounded-full" />
|
|
217
|
+
<span>Swipe to navigate</span>
|
|
218
|
+
</div>
|
|
219
|
+
</div>
|
|
220
|
+
</div>
|
|
221
|
+
</div>
|
|
222
|
+
)}
|
|
223
|
+
</>
|
|
224
|
+
);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
MobileNavigation.displayName = 'MobileNavigation';
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { observer } from 'mobx-react-lite';
|
|
3
|
+
import { ArrowLeft, ArrowRight, ArrowUp } from 'lucide-react';
|
|
4
|
+
import { cn } from '../../../lib/utils';
|
|
5
|
+
|
|
6
|
+
export interface NavigationButtonsProps {
|
|
7
|
+
canGoBack?: boolean;
|
|
8
|
+
canGoForward?: boolean;
|
|
9
|
+
canGoUp?: boolean;
|
|
10
|
+
onGoBack?: () => void;
|
|
11
|
+
onGoForward?: () => void;
|
|
12
|
+
onGoUp?: () => void;
|
|
13
|
+
className?: string;
|
|
14
|
+
variant?: 'default' | 'compact';
|
|
15
|
+
showLabels?: boolean;
|
|
16
|
+
disabled?: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface NavigationButtonProps {
|
|
20
|
+
icon: React.ReactNode;
|
|
21
|
+
label: string;
|
|
22
|
+
onClick?: () => void;
|
|
23
|
+
disabled?: boolean;
|
|
24
|
+
shortcut?: string;
|
|
25
|
+
className?: string;
|
|
26
|
+
showLabel?: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const NavigationButton: React.FC<NavigationButtonProps> = ({
|
|
30
|
+
icon,
|
|
31
|
+
label,
|
|
32
|
+
onClick,
|
|
33
|
+
disabled = false,
|
|
34
|
+
shortcut,
|
|
35
|
+
className,
|
|
36
|
+
showLabel = false,
|
|
37
|
+
}) => {
|
|
38
|
+
const tooltipText = shortcut ? `${label} (${shortcut})` : label;
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<button
|
|
42
|
+
onClick={onClick}
|
|
43
|
+
disabled={disabled}
|
|
44
|
+
className={cn(
|
|
45
|
+
'flex items-center gap-2 px-3 py-2 rounded-md text-sm transition-colors',
|
|
46
|
+
'hover:bg-muted focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2',
|
|
47
|
+
'disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-transparent',
|
|
48
|
+
className
|
|
49
|
+
)}
|
|
50
|
+
title={tooltipText}
|
|
51
|
+
aria-label={tooltipText}
|
|
52
|
+
>
|
|
53
|
+
{icon}
|
|
54
|
+
{showLabel && <span className="hidden sm:inline">{label}</span>}
|
|
55
|
+
</button>
|
|
56
|
+
);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const NavigationButtons: React.FC<NavigationButtonsProps> = observer(({
|
|
60
|
+
canGoBack = false,
|
|
61
|
+
canGoForward = false,
|
|
62
|
+
canGoUp = false,
|
|
63
|
+
onGoBack,
|
|
64
|
+
onGoForward,
|
|
65
|
+
onGoUp,
|
|
66
|
+
className,
|
|
67
|
+
variant = 'default',
|
|
68
|
+
showLabels = false,
|
|
69
|
+
disabled = false,
|
|
70
|
+
}) => {
|
|
71
|
+
// Handle keyboard shortcuts
|
|
72
|
+
React.useEffect(() => {
|
|
73
|
+
const handleKeyDown = (e: KeyboardEvent) => {
|
|
74
|
+
// Only handle shortcuts when not typing in inputs
|
|
75
|
+
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Check for Alt key combinations
|
|
80
|
+
if (e.altKey) {
|
|
81
|
+
switch (e.key) {
|
|
82
|
+
case 'ArrowLeft':
|
|
83
|
+
e.preventDefault();
|
|
84
|
+
if (canGoBack && onGoBack) {
|
|
85
|
+
onGoBack();
|
|
86
|
+
}
|
|
87
|
+
break;
|
|
88
|
+
case 'ArrowRight':
|
|
89
|
+
e.preventDefault();
|
|
90
|
+
if (canGoForward && onGoForward) {
|
|
91
|
+
onGoForward();
|
|
92
|
+
}
|
|
93
|
+
break;
|
|
94
|
+
case 'ArrowUp':
|
|
95
|
+
e.preventDefault();
|
|
96
|
+
if (canGoUp && onGoUp) {
|
|
97
|
+
onGoUp();
|
|
98
|
+
}
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Browser-style shortcuts
|
|
104
|
+
if ((e.ctrlKey || e.metaKey) && e.key === '[') {
|
|
105
|
+
e.preventDefault();
|
|
106
|
+
if (canGoBack && onGoBack) {
|
|
107
|
+
onGoBack();
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if ((e.ctrlKey || e.metaKey) && e.key === ']') {
|
|
112
|
+
e.preventDefault();
|
|
113
|
+
if (canGoForward && onGoForward) {
|
|
114
|
+
onGoForward();
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
window.addEventListener('keydown', handleKeyDown);
|
|
120
|
+
return () => window.removeEventListener('keydown', handleKeyDown);
|
|
121
|
+
}, [canGoBack, canGoForward, canGoUp, onGoBack, onGoForward, onGoUp]);
|
|
122
|
+
|
|
123
|
+
const isCompact = variant === 'compact';
|
|
124
|
+
const buttonSize = isCompact ? 'w-8 h-8 p-1' : 'px-3 py-2';
|
|
125
|
+
|
|
126
|
+
return (
|
|
127
|
+
<div
|
|
128
|
+
className={cn(
|
|
129
|
+
'flex items-center',
|
|
130
|
+
isCompact ? 'gap-1' : 'gap-2',
|
|
131
|
+
className
|
|
132
|
+
)}
|
|
133
|
+
role="toolbar"
|
|
134
|
+
aria-label="Navigation buttons"
|
|
135
|
+
>
|
|
136
|
+
<NavigationButton
|
|
137
|
+
icon={<ArrowLeft className={isCompact ? 'w-4 h-4' : 'w-4 h-4'} />}
|
|
138
|
+
label="Back"
|
|
139
|
+
onClick={onGoBack}
|
|
140
|
+
disabled={disabled || !canGoBack}
|
|
141
|
+
shortcut="Alt+←"
|
|
142
|
+
className={buttonSize}
|
|
143
|
+
showLabel={showLabels && !isCompact}
|
|
144
|
+
/>
|
|
145
|
+
|
|
146
|
+
<NavigationButton
|
|
147
|
+
icon={<ArrowRight className={isCompact ? 'w-4 h-4' : 'w-4 h-4'} />}
|
|
148
|
+
label="Forward"
|
|
149
|
+
onClick={onGoForward}
|
|
150
|
+
disabled={disabled || !canGoForward}
|
|
151
|
+
shortcut="Alt+→"
|
|
152
|
+
className={buttonSize}
|
|
153
|
+
showLabel={showLabels && !isCompact}
|
|
154
|
+
/>
|
|
155
|
+
|
|
156
|
+
<NavigationButton
|
|
157
|
+
icon={<ArrowUp className={isCompact ? 'w-4 h-4' : 'w-4 h-4'} />}
|
|
158
|
+
label="Up"
|
|
159
|
+
onClick={onGoUp}
|
|
160
|
+
disabled={disabled || !canGoUp}
|
|
161
|
+
shortcut="Alt+↑"
|
|
162
|
+
className={buttonSize}
|
|
163
|
+
showLabel={showLabels && !isCompact}
|
|
164
|
+
/>
|
|
165
|
+
</div>
|
|
166
|
+
);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
NavigationButtons.displayName = 'NavigationButtons';
|
|
170
|
+
|
|
171
|
+
export default NavigationButtons;
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import React, { Component, ReactNode } from 'react';
|
|
2
|
+
import { AlertTriangle } from 'lucide-react';
|
|
3
|
+
import { cn } from '../../../lib/utils';
|
|
4
|
+
|
|
5
|
+
export interface ErrorBoundaryProps {
|
|
6
|
+
children: ReactNode;
|
|
7
|
+
fallback?: (error: Error, errorInfo: string, onReset: () => void) => ReactNode;
|
|
8
|
+
onError?: (error: Error, errorInfo: string) => void;
|
|
9
|
+
className?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface ErrorBoundaryState {
|
|
13
|
+
hasError: boolean;
|
|
14
|
+
error: Error | null;
|
|
15
|
+
errorInfo: string | null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
|
|
19
|
+
constructor(props: ErrorBoundaryProps) {
|
|
20
|
+
super(props);
|
|
21
|
+
this.state = {
|
|
22
|
+
hasError: false,
|
|
23
|
+
error: null,
|
|
24
|
+
errorInfo: null,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
static getDerivedStateFromError(error: Error): Partial<ErrorBoundaryState> {
|
|
29
|
+
return {
|
|
30
|
+
hasError: true,
|
|
31
|
+
error,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
|
|
36
|
+
const errorInfoString = errorInfo.componentStack || '';
|
|
37
|
+
|
|
38
|
+
this.setState({
|
|
39
|
+
errorInfo: errorInfoString,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// Call optional error reporting callback
|
|
43
|
+
this.props.onError?.(error, errorInfoString);
|
|
44
|
+
|
|
45
|
+
// Log error for debugging
|
|
46
|
+
console.error('FileBrowser Error Boundary caught an error:', error);
|
|
47
|
+
console.error('Error Info:', errorInfo);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
handleReset = () => {
|
|
51
|
+
this.setState({
|
|
52
|
+
hasError: false,
|
|
53
|
+
error: null,
|
|
54
|
+
errorInfo: null,
|
|
55
|
+
});
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
render() {
|
|
59
|
+
if (this.state.hasError) {
|
|
60
|
+
const { fallback, className } = this.props;
|
|
61
|
+
const { error, errorInfo } = this.state;
|
|
62
|
+
|
|
63
|
+
// Use custom fallback if provided
|
|
64
|
+
if (fallback && error) {
|
|
65
|
+
return fallback(error, errorInfo || '', this.handleReset);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Default error UI
|
|
69
|
+
return (
|
|
70
|
+
<div
|
|
71
|
+
className={cn(
|
|
72
|
+
'flex flex-col items-center justify-center p-8 text-center border border-destructive/20 rounded-lg bg-destructive/5',
|
|
73
|
+
className
|
|
74
|
+
)}
|
|
75
|
+
role="alert"
|
|
76
|
+
aria-live="assertive"
|
|
77
|
+
>
|
|
78
|
+
<AlertTriangle
|
|
79
|
+
className="w-12 h-12 text-destructive mb-4"
|
|
80
|
+
aria-hidden="true"
|
|
81
|
+
/>
|
|
82
|
+
|
|
83
|
+
<h2 className="text-lg font-semibold text-destructive mb-2">
|
|
84
|
+
Something went wrong
|
|
85
|
+
</h2>
|
|
86
|
+
|
|
87
|
+
<p className="text-sm text-muted-foreground mb-4 max-w-md">
|
|
88
|
+
{error?.message || 'An unexpected error occurred while rendering the file browser.'}
|
|
89
|
+
</p>
|
|
90
|
+
|
|
91
|
+
<div className="flex gap-2">
|
|
92
|
+
<button
|
|
93
|
+
onClick={this.handleReset}
|
|
94
|
+
className="px-4 py-2 bg-primary text-primary-foreground rounded-md hover:bg-primary/90 focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2 transition-colors"
|
|
95
|
+
>
|
|
96
|
+
Try Again
|
|
97
|
+
</button>
|
|
98
|
+
|
|
99
|
+
<button
|
|
100
|
+
onClick={() => window.location.reload()}
|
|
101
|
+
className="px-4 py-2 bg-secondary text-secondary-foreground rounded-md hover:bg-secondary/90 focus:outline-none focus:ring-2 focus:ring-secondary focus:ring-offset-2 transition-colors"
|
|
102
|
+
>
|
|
103
|
+
Reload Page
|
|
104
|
+
</button>
|
|
105
|
+
</div>
|
|
106
|
+
|
|
107
|
+
{/* Error details removed to prevent build issues */}
|
|
108
|
+
</div>
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return this.props.children;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export default ErrorBoundary;
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { observer } from 'mobx-react-lite';
|
|
3
|
+
import { cn } from '../../../lib/utils';
|
|
4
|
+
import { FileBrowserItem as FileBrowserItemType } from '../../types/FileBrowserTypes';
|
|
5
|
+
import FileIcon from './FileIcon';
|
|
6
|
+
|
|
7
|
+
export interface FileBrowserItemProps {
|
|
8
|
+
item: FileBrowserItemType;
|
|
9
|
+
isSelected?: boolean;
|
|
10
|
+
isHovered?: boolean;
|
|
11
|
+
showDetails?: boolean;
|
|
12
|
+
showIcon?: boolean;
|
|
13
|
+
showSize?: boolean;
|
|
14
|
+
showDate?: boolean;
|
|
15
|
+
iconSize?: 'sm' | 'md' | 'lg';
|
|
16
|
+
layout?: 'horizontal' | 'vertical';
|
|
17
|
+
onClick?: (item: FileBrowserItemType, event: React.MouseEvent) => void;
|
|
18
|
+
onDoubleClick?: (item: FileBrowserItemType, event: React.MouseEvent) => void;
|
|
19
|
+
onContextMenu?: (item: FileBrowserItemType, event: React.MouseEvent) => void;
|
|
20
|
+
onKeyDown?: (item: FileBrowserItemType, event: React.KeyboardEvent) => void;
|
|
21
|
+
className?: string;
|
|
22
|
+
style?: React.CSSProperties;
|
|
23
|
+
children?: React.ReactNode;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const iconSizeClasses = {
|
|
27
|
+
sm: 'w-4 h-4',
|
|
28
|
+
md: 'w-6 h-6',
|
|
29
|
+
lg: 'w-8 h-8',
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const FileBrowserItemComponent: React.FC<FileBrowserItemProps> = observer(({
|
|
33
|
+
item,
|
|
34
|
+
isSelected = false,
|
|
35
|
+
isHovered = false,
|
|
36
|
+
showDetails = false,
|
|
37
|
+
showIcon = true,
|
|
38
|
+
showSize = false,
|
|
39
|
+
showDate = false,
|
|
40
|
+
iconSize = 'md',
|
|
41
|
+
layout = 'horizontal',
|
|
42
|
+
onClick,
|
|
43
|
+
onDoubleClick,
|
|
44
|
+
onContextMenu,
|
|
45
|
+
onKeyDown,
|
|
46
|
+
className,
|
|
47
|
+
style,
|
|
48
|
+
children,
|
|
49
|
+
}) => {
|
|
50
|
+
const handleClick = (event: React.MouseEvent) => {
|
|
51
|
+
onClick?.(item, event);
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const handleDoubleClick = (event: React.MouseEvent) => {
|
|
55
|
+
onDoubleClick?.(item, event);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const handleContextMenu = (event: React.MouseEvent) => {
|
|
59
|
+
event.preventDefault();
|
|
60
|
+
onContextMenu?.(item, event);
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const handleKeyDown = (event: React.KeyboardEvent) => {
|
|
64
|
+
if (event.key === 'Enter' || event.key === ' ') {
|
|
65
|
+
event.preventDefault();
|
|
66
|
+
onClick?.(item, event as any);
|
|
67
|
+
}
|
|
68
|
+
onKeyDown?.(item, event);
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const formatFileSize = (bytes?: number): string => {
|
|
72
|
+
if (!bytes || bytes === 0) return '';
|
|
73
|
+
|
|
74
|
+
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
75
|
+
let size = bytes;
|
|
76
|
+
let unitIndex = 0;
|
|
77
|
+
|
|
78
|
+
while (size >= 1024 && unitIndex < units.length - 1) {
|
|
79
|
+
size /= 1024;
|
|
80
|
+
unitIndex++;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return `${size.toFixed(unitIndex === 0 ? 0 : 1)} ${units[unitIndex]}`;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const formatDate = (date?: Date | string): string => {
|
|
87
|
+
if (!date) return '';
|
|
88
|
+
|
|
89
|
+
const dateObj = typeof date === 'string' ? new Date(date) : date;
|
|
90
|
+
return dateObj.toLocaleDateString(undefined, {
|
|
91
|
+
year: 'numeric',
|
|
92
|
+
month: 'short',
|
|
93
|
+
day: 'numeric',
|
|
94
|
+
});
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const isVertical = layout === 'vertical';
|
|
98
|
+
|
|
99
|
+
return (
|
|
100
|
+
<div
|
|
101
|
+
className={cn(
|
|
102
|
+
'group relative flex items-center gap-2 p-2 rounded-md cursor-pointer transition-colors',
|
|
103
|
+
'hover:bg-muted/50 focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-1',
|
|
104
|
+
isSelected && 'bg-primary/10 border border-primary/20',
|
|
105
|
+
isHovered && !isSelected && 'bg-muted/30',
|
|
106
|
+
isVertical && 'flex-col text-center',
|
|
107
|
+
className
|
|
108
|
+
)}
|
|
109
|
+
style={style}
|
|
110
|
+
onClick={handleClick}
|
|
111
|
+
onDoubleClick={handleDoubleClick}
|
|
112
|
+
onContextMenu={handleContextMenu}
|
|
113
|
+
onKeyDown={handleKeyDown}
|
|
114
|
+
tabIndex={0}
|
|
115
|
+
role="button"
|
|
116
|
+
aria-label={`${item.type === 'directory' ? 'Folder' : 'File'}: ${item.name}`}
|
|
117
|
+
aria-selected={isSelected}
|
|
118
|
+
>
|
|
119
|
+
{/* Icon */}
|
|
120
|
+
{showIcon && (
|
|
121
|
+
<div className={cn('flex-shrink-0', isVertical && 'mb-1')}>
|
|
122
|
+
<FileIcon
|
|
123
|
+
item={item}
|
|
124
|
+
size={iconSize}
|
|
125
|
+
className={iconSizeClasses[iconSize]}
|
|
126
|
+
/>
|
|
127
|
+
</div>
|
|
128
|
+
)}
|
|
129
|
+
|
|
130
|
+
{/* Content */}
|
|
131
|
+
<div className={cn(
|
|
132
|
+
'flex-1 min-w-0',
|
|
133
|
+
isVertical ? 'text-center' : 'flex items-center justify-between'
|
|
134
|
+
)}>
|
|
135
|
+
{/* Name and details */}
|
|
136
|
+
<div className={cn('min-w-0', !isVertical && 'flex-1')}>
|
|
137
|
+
<div className={cn(
|
|
138
|
+
'font-medium text-sm truncate',
|
|
139
|
+
item.type === 'directory' && 'text-primary',
|
|
140
|
+
isVertical && 'text-center'
|
|
141
|
+
)} title={item.name}>
|
|
142
|
+
{item.name}
|
|
143
|
+
</div>
|
|
144
|
+
|
|
145
|
+
{showDetails && (
|
|
146
|
+
<div className={cn(
|
|
147
|
+
'text-xs text-muted-foreground mt-0.5',
|
|
148
|
+
isVertical && 'text-center'
|
|
149
|
+
)}>
|
|
150
|
+
{item.type === 'directory' ? 'Folder' : 'File'}
|
|
151
|
+
</div>
|
|
152
|
+
)}
|
|
153
|
+
</div>
|
|
154
|
+
|
|
155
|
+
{/* Size and date */}
|
|
156
|
+
{(showSize || showDate) && !isVertical && (
|
|
157
|
+
<div className="flex-shrink-0 text-xs text-muted-foreground ml-2">
|
|
158
|
+
<div className="flex flex-col items-end gap-0.5">
|
|
159
|
+
{showSize && item.size !== undefined && (
|
|
160
|
+
<span>{formatFileSize(item.size)}</span>
|
|
161
|
+
)}
|
|
162
|
+
{showDate && item.lastModified && (
|
|
163
|
+
<span>{formatDate(item.lastModified)}</span>
|
|
164
|
+
)}
|
|
165
|
+
</div>
|
|
166
|
+
</div>
|
|
167
|
+
)}
|
|
168
|
+
|
|
169
|
+
{/* Vertical layout size/date */}
|
|
170
|
+
{(showSize || showDate) && isVertical && (
|
|
171
|
+
<div className="text-xs text-muted-foreground mt-1 text-center">
|
|
172
|
+
{showSize && item.size !== undefined && (
|
|
173
|
+
<div>{formatFileSize(item.size)}</div>
|
|
174
|
+
)}
|
|
175
|
+
{showDate && item.lastModified && (
|
|
176
|
+
<div>{formatDate(item.lastModified)}</div>
|
|
177
|
+
)}
|
|
178
|
+
</div>
|
|
179
|
+
)}
|
|
180
|
+
</div>
|
|
181
|
+
|
|
182
|
+
{/* Custom children */}
|
|
183
|
+
{children}
|
|
184
|
+
|
|
185
|
+
{/* Selection indicator */}
|
|
186
|
+
{isSelected && (
|
|
187
|
+
<div className="absolute inset-0 border-2 border-primary rounded-md pointer-events-none" />
|
|
188
|
+
)}
|
|
189
|
+
</div>
|
|
190
|
+
);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
FileBrowserItemComponent.displayName = 'FileBrowserItem';
|
|
194
|
+
|
|
195
|
+
export default FileBrowserItemComponent;
|