@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,645 @@
|
|
|
1
|
+
# FileBrowser Architecture v2.0 - Integrated with TreeComponent and ListItemsComponent
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
The FileBrowser v2.0 is a unified, extensible dual-panel file browser component that leverages existing TreeComponent and ListItemsComponent architectures. It provides a modern file exploration experience with a dedicated tree navigation panel (folders only) and a synchronized content panel (files and folders) that maintains consistent selection state.
|
|
6
|
+
|
|
7
|
+
// AICODE-NOTE: Architecture updated to use existing TreeComponent and ListItemsComponent instead of custom implementations
|
|
8
|
+
// AICODE-NOTE: Tree shows directories only, list shows all items, both are synchronized via shared selection state
|
|
9
|
+
|
|
10
|
+
## Key Design Principles
|
|
11
|
+
|
|
12
|
+
- **Component Reuse**: Leverages existing TreeComponent (left panel) and ListItemsComponent (right panel)
|
|
13
|
+
- **File System Agnostic**: Works with any `@anymux/file-system` implementation
|
|
14
|
+
- **Synchronized State**: Tree selection drives list content, list navigation updates tree selection
|
|
15
|
+
- **Provider Bridge**: Custom providers bridge between `@anymux/file-system` and component providers
|
|
16
|
+
- **Directory-Only Tree**: Tree shows only directories for hierarchical navigation
|
|
17
|
+
- **Full Content List**: List shows all items (files and directories) in current selection
|
|
18
|
+
- **MobX Reactive State**: Following strict MobX model guidelines for state synchronization
|
|
19
|
+
|
|
20
|
+
## Architecture Overview
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
FileBrowser v2.0
|
|
24
|
+
├── FileBrowserModel (Orchestrator)
|
|
25
|
+
│ ├── FileSystemBridge (Adapter)
|
|
26
|
+
│ │ └── IFileSystem (@anymux/file-system)
|
|
27
|
+
│ ├── TreeProvider (Directory Tree)
|
|
28
|
+
│ │ └── TreeComponent (Left Panel)
|
|
29
|
+
│ ├── ListItemsProvider (Content List)
|
|
30
|
+
│ │ └── ListItemsComponent (Right Panel)
|
|
31
|
+
│ └── SelectionSynchronizer (State Coordinator)
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Core Components Integration
|
|
35
|
+
|
|
36
|
+
### 1. FileBrowserModel (Orchestrator)
|
|
37
|
+
**Location**: `/models/FileBrowserModel.ts`
|
|
38
|
+
|
|
39
|
+
**Purpose**: Main orchestrator that manages both TreeComponent and ListItemsComponent, handling state synchronization and file system interactions.
|
|
40
|
+
|
|
41
|
+
```typescript
|
|
42
|
+
class FileBrowserModel {
|
|
43
|
+
// File system integration
|
|
44
|
+
fileSystem: IFileSystem
|
|
45
|
+
fileSystemBridge: FileSystemBridge
|
|
46
|
+
|
|
47
|
+
// Component providers
|
|
48
|
+
treeProvider: FileSystemTreeProvider
|
|
49
|
+
listProvider: FileSystemListProvider
|
|
50
|
+
|
|
51
|
+
// Models
|
|
52
|
+
treeModel: TreeModel
|
|
53
|
+
listModel: ListItemsModel
|
|
54
|
+
|
|
55
|
+
// Synchronized state
|
|
56
|
+
currentPath: string = '/'
|
|
57
|
+
selectedPath: string | null = null
|
|
58
|
+
|
|
59
|
+
constructor(fileSystem: IFileSystem) {
|
|
60
|
+
makeAutoObservable(this, {})
|
|
61
|
+
|
|
62
|
+
this.fileSystem = fileSystem
|
|
63
|
+
this.fileSystemBridge = new FileSystemBridge(fileSystem)
|
|
64
|
+
|
|
65
|
+
// Create providers that bridge to file system
|
|
66
|
+
this.treeProvider = new FileSystemTreeProvider(this.fileSystemBridge, {
|
|
67
|
+
showFilesInTree: false, // AICODE-NOTE: Tree shows directories only
|
|
68
|
+
onSelectionChange: this.handleTreeSelection
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
this.listProvider = new FileSystemListProvider(this.fileSystemBridge, {
|
|
72
|
+
showAllItems: true, // AICODE-NOTE: List shows files and directories
|
|
73
|
+
onSelectionChange: this.handleListSelection,
|
|
74
|
+
onNavigation: this.handleListNavigation
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
// Create component models
|
|
78
|
+
this.treeModel = new TreeModel(this.treeProvider)
|
|
79
|
+
this.listModel = new ListItemsModel(this.listProvider)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Synchronization actions
|
|
83
|
+
handleTreeSelection = (path: string) => {
|
|
84
|
+
this.selectedPath = path
|
|
85
|
+
this.currentPath = path
|
|
86
|
+
// Load list content for selected directory
|
|
87
|
+
this.listModel.setPath(path)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
handleListNavigation = (path: string) => {
|
|
91
|
+
this.currentPath = path
|
|
92
|
+
this.selectedPath = path
|
|
93
|
+
// Update tree to highlight the directory
|
|
94
|
+
this.treeModel.selectPath(path)
|
|
95
|
+
// Expand tree to make path visible
|
|
96
|
+
this.treeModel.expandToPath(path)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
handleListSelection = (selectedItems: ListItemData[]) => {
|
|
100
|
+
// If single directory selected, could auto-navigate (optional)
|
|
101
|
+
if (selectedItems.length === 1 && selectedItems[0].type === 'directory') {
|
|
102
|
+
// Optional: auto-navigate to directory
|
|
103
|
+
// this.handleListNavigation(selectedItems[0].path)
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### 2. FileSystemBridge (Adapter)
|
|
110
|
+
**Location**: `/adapters/FileSystemBridge.ts`
|
|
111
|
+
|
|
112
|
+
**Purpose**: Adapter that converts `@anymux/file-system` operations to data structures expected by TreeComponent and ListItemsComponent.
|
|
113
|
+
|
|
114
|
+
```typescript
|
|
115
|
+
interface FileSystemItem {
|
|
116
|
+
id: string
|
|
117
|
+
path: string
|
|
118
|
+
name: string
|
|
119
|
+
type: 'file' | 'directory'
|
|
120
|
+
size?: number
|
|
121
|
+
modified?: Date
|
|
122
|
+
isDirectory: boolean
|
|
123
|
+
children?: FileSystemItem[]
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
class FileSystemBridge {
|
|
127
|
+
constructor(private fileSystem: IFileSystem) {
|
|
128
|
+
makeAutoObservable(this, {})
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Tree operations (directories only)
|
|
132
|
+
async loadDirectoryTree(path: string): Promise<FileSystemItem[]> {
|
|
133
|
+
const dirents = await this.fileSystem.readdir(path, { withFileTypes: true })
|
|
134
|
+
|
|
135
|
+
return dirents
|
|
136
|
+
.filter(dirent => dirent.isDirectory()) // AICODE-NOTE: Filter to directories only for tree
|
|
137
|
+
.map(dirent => ({
|
|
138
|
+
id: `${path}/${dirent.name}`.replace(/\/\//g, '/'),
|
|
139
|
+
path: `${path}/${dirent.name}`.replace(/\/\//g, '/'),
|
|
140
|
+
name: dirent.name,
|
|
141
|
+
type: 'directory' as const,
|
|
142
|
+
isDirectory: true
|
|
143
|
+
}))
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// List operations (all items)
|
|
147
|
+
async loadDirectoryContents(path: string): Promise<FileSystemItem[]> {
|
|
148
|
+
const dirents = await this.fileSystem.readdir(path, { withFileTypes: true })
|
|
149
|
+
|
|
150
|
+
const items = await Promise.all(
|
|
151
|
+
dirents.map(async (dirent) => {
|
|
152
|
+
const itemPath = `${path}/${dirent.name}`.replace(/\/\//g, '/')
|
|
153
|
+
let stats: Stats | null = null
|
|
154
|
+
|
|
155
|
+
try {
|
|
156
|
+
stats = await this.fileSystem.stat(itemPath)
|
|
157
|
+
} catch (error) {
|
|
158
|
+
// Handle permission errors gracefully
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
id: itemPath,
|
|
163
|
+
path: itemPath,
|
|
164
|
+
name: dirent.name,
|
|
165
|
+
type: dirent.isDirectory() ? 'directory' as const : 'file' as const,
|
|
166
|
+
size: stats?.size,
|
|
167
|
+
modified: stats?.mtime,
|
|
168
|
+
isDirectory: dirent.isDirectory()
|
|
169
|
+
}
|
|
170
|
+
})
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
// Sort: directories first, then files, both alphabetically
|
|
174
|
+
return items.sort((a, b) => {
|
|
175
|
+
if (a.type !== b.type) {
|
|
176
|
+
return a.type === 'directory' ? -1 : 1
|
|
177
|
+
}
|
|
178
|
+
return a.name.localeCompare(b.name)
|
|
179
|
+
})
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// File operations
|
|
183
|
+
async readFile(path: string): Promise<Buffer | string> {
|
|
184
|
+
return this.fileSystem.readFile(path)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async writeFile(path: string, content: Buffer | string): Promise<void> {
|
|
188
|
+
return this.fileSystem.writeFile(path, content)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async deleteItem(path: string, isDirectory: boolean): Promise<void> {
|
|
192
|
+
if (isDirectory) {
|
|
193
|
+
return this.fileSystem.rmdir(path, { recursive: true })
|
|
194
|
+
} else {
|
|
195
|
+
return this.fileSystem.unlink(path)
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
async createDirectory(path: string): Promise<void> {
|
|
200
|
+
return this.fileSystem.mkdir(path, { recursive: true })
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async renameItem(oldPath: string, newPath: string): Promise<void> {
|
|
204
|
+
return this.fileSystem.rename(oldPath, newPath)
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
### 3. FileSystemTreeProvider (Tree Integration)
|
|
210
|
+
**Location**: `/providers/FileSystemTreeProvider.ts`
|
|
211
|
+
|
|
212
|
+
**Purpose**: Bridges FileSystemBridge to TreeComponent provider interface, showing directories only.
|
|
213
|
+
|
|
214
|
+
```typescript
|
|
215
|
+
class FileSystemTreeProvider implements TreeProvider {
|
|
216
|
+
constructor(
|
|
217
|
+
private bridge: FileSystemBridge,
|
|
218
|
+
private options: {
|
|
219
|
+
showFilesInTree: boolean
|
|
220
|
+
onSelectionChange: (path: string) => void
|
|
221
|
+
}
|
|
222
|
+
) {
|
|
223
|
+
makeAutoObservable(this, {})
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// TreeProvider implementation
|
|
227
|
+
async loadNodes(options?: TreeLoadOptions): Promise<TreeLoadResult> {
|
|
228
|
+
const rootItems = await this.bridge.loadDirectoryTree(options?.path || '/')
|
|
229
|
+
|
|
230
|
+
const nodes: TreeNodeData[] = rootItems.map(item => ({
|
|
231
|
+
id: item.id,
|
|
232
|
+
text: item.name,
|
|
233
|
+
data: item,
|
|
234
|
+
hasChildren: true, // Assume directories have children
|
|
235
|
+
isExpanded: false,
|
|
236
|
+
icon: 'folder'
|
|
237
|
+
}))
|
|
238
|
+
|
|
239
|
+
return {
|
|
240
|
+
nodes,
|
|
241
|
+
totalCount: nodes.length
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
async loadChildren(nodeId: string): Promise<TreeNodeData[]> {
|
|
246
|
+
const items = await this.bridge.loadDirectoryTree(nodeId)
|
|
247
|
+
|
|
248
|
+
return items.map(item => ({
|
|
249
|
+
id: item.id,
|
|
250
|
+
text: item.name,
|
|
251
|
+
data: item,
|
|
252
|
+
hasChildren: true,
|
|
253
|
+
isExpanded: false,
|
|
254
|
+
icon: 'folder'
|
|
255
|
+
}))
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Selection handling
|
|
259
|
+
onSelectionChange = (selectedNodes: TreeNodeData[]) => {
|
|
260
|
+
if (selectedNodes.length > 0) {
|
|
261
|
+
const selectedNode = selectedNodes[0]
|
|
262
|
+
this.options.onSelectionChange(selectedNode.data.path)
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Context menu for directories
|
|
267
|
+
getNodeContextMenu(node: TreeNodeData): ContextMenuItem[] {
|
|
268
|
+
return [
|
|
269
|
+
{
|
|
270
|
+
id: 'open',
|
|
271
|
+
label: 'Open',
|
|
272
|
+
icon: 'folder-open',
|
|
273
|
+
action: () => this.options.onSelectionChange(node.data.path)
|
|
274
|
+
},
|
|
275
|
+
{ id: 'separator1', type: 'separator' },
|
|
276
|
+
{
|
|
277
|
+
id: 'new-folder',
|
|
278
|
+
label: 'New Folder',
|
|
279
|
+
icon: 'folder-plus',
|
|
280
|
+
action: () => this.createNewFolder(node.data.path)
|
|
281
|
+
},
|
|
282
|
+
{ id: 'separator2', type: 'separator' },
|
|
283
|
+
{
|
|
284
|
+
id: 'rename',
|
|
285
|
+
label: 'Rename',
|
|
286
|
+
icon: 'edit',
|
|
287
|
+
action: () => this.renameDirectory(node.data.path)
|
|
288
|
+
},
|
|
289
|
+
{
|
|
290
|
+
id: 'delete',
|
|
291
|
+
label: 'Delete',
|
|
292
|
+
icon: 'trash',
|
|
293
|
+
action: () => this.deleteDirectory(node.data.path)
|
|
294
|
+
}
|
|
295
|
+
]
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
private async createNewFolder(parentPath: string) {
|
|
299
|
+
// Implementation for creating new folder
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
private async renameDirectory(path: string) {
|
|
303
|
+
// Implementation for renaming directory
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
private async deleteDirectory(path: string) {
|
|
307
|
+
// Implementation for deleting directory
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
### 4. FileSystemListProvider (List Integration)
|
|
313
|
+
**Location**: `/providers/FileSystemListProvider.ts`
|
|
314
|
+
|
|
315
|
+
**Purpose**: Bridges FileSystemBridge to ListItemsComponent provider interface, showing all items with file previews and actions.
|
|
316
|
+
|
|
317
|
+
```typescript
|
|
318
|
+
class FileSystemListProvider implements ListItemsProvider {
|
|
319
|
+
currentPath: string = '/'
|
|
320
|
+
|
|
321
|
+
constructor(
|
|
322
|
+
private bridge: FileSystemBridge,
|
|
323
|
+
private options: {
|
|
324
|
+
showAllItems: boolean
|
|
325
|
+
onSelectionChange: (items: ListItemData[]) => void
|
|
326
|
+
onNavigation: (path: string) => void
|
|
327
|
+
}
|
|
328
|
+
) {
|
|
329
|
+
makeAutoObservable(this, {})
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
setPath(path: string) {
|
|
333
|
+
this.currentPath = path
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// ListItemsProvider implementation
|
|
337
|
+
async loadItems(options?: ListLoadOptions): Promise<ListLoadResult> {
|
|
338
|
+
const items = await this.bridge.loadDirectoryContents(this.currentPath)
|
|
339
|
+
|
|
340
|
+
const listItems: ListItemData[] = items.map(item => ({
|
|
341
|
+
id: item.id,
|
|
342
|
+
name: item.name,
|
|
343
|
+
type: item.type,
|
|
344
|
+
path: item.path,
|
|
345
|
+
size: item.size,
|
|
346
|
+
modified: item.modified,
|
|
347
|
+
isDirectory: item.isDirectory,
|
|
348
|
+
data: item,
|
|
349
|
+
// Thumbnails for images
|
|
350
|
+
thumbnailUrl: this.getThumbnailUrl(item),
|
|
351
|
+
// File type icons
|
|
352
|
+
icon: this.getFileIcon(item)
|
|
353
|
+
}))
|
|
354
|
+
|
|
355
|
+
return {
|
|
356
|
+
items: listItems,
|
|
357
|
+
totalCount: listItems.length
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// View type support
|
|
362
|
+
getSupportedViewTypes(): ListViewType[] {
|
|
363
|
+
return [
|
|
364
|
+
LIST_VIEW_TYPE,
|
|
365
|
+
GRID_VIEW_TYPE,
|
|
366
|
+
DETAILS_VIEW_TYPE,
|
|
367
|
+
MASONRY_HORIZONTAL_VIEW_TYPE
|
|
368
|
+
]
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// Double-click handling
|
|
372
|
+
onItemAction = async (item: ListItemData, action: string) => {
|
|
373
|
+
if (action === 'open' || action === 'double-click') {
|
|
374
|
+
if (item.isDirectory) {
|
|
375
|
+
// Navigate to directory
|
|
376
|
+
this.options.onNavigation(item.path)
|
|
377
|
+
} else {
|
|
378
|
+
// Open file (could trigger preview or download)
|
|
379
|
+
await this.openFile(item.path)
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Selection handling
|
|
385
|
+
onSelectionChange = (selectedItems: ListItemData[]) => {
|
|
386
|
+
this.options.onSelectionChange(selectedItems)
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// Context menu for files and directories
|
|
390
|
+
getItemContextMenu(item: ListItemData): ContextMenuItem[] {
|
|
391
|
+
const baseMenu = [
|
|
392
|
+
{
|
|
393
|
+
id: 'open',
|
|
394
|
+
label: item.isDirectory ? 'Open' : 'Open',
|
|
395
|
+
icon: item.isDirectory ? 'folder-open' : 'eye',
|
|
396
|
+
action: () => this.onItemAction(item, 'open')
|
|
397
|
+
}
|
|
398
|
+
]
|
|
399
|
+
|
|
400
|
+
if (!item.isDirectory) {
|
|
401
|
+
baseMenu.push(
|
|
402
|
+
{ id: 'separator1', type: 'separator' },
|
|
403
|
+
{
|
|
404
|
+
id: 'preview',
|
|
405
|
+
label: 'Preview',
|
|
406
|
+
icon: 'eye',
|
|
407
|
+
action: () => this.previewFile(item.path)
|
|
408
|
+
},
|
|
409
|
+
{
|
|
410
|
+
id: 'download',
|
|
411
|
+
label: 'Download',
|
|
412
|
+
icon: 'download',
|
|
413
|
+
action: () => this.downloadFile(item.path)
|
|
414
|
+
}
|
|
415
|
+
)
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
baseMenu.push(
|
|
419
|
+
{ id: 'separator2', type: 'separator' },
|
|
420
|
+
{
|
|
421
|
+
id: 'rename',
|
|
422
|
+
label: 'Rename',
|
|
423
|
+
icon: 'edit',
|
|
424
|
+
action: () => this.renameItem(item.path)
|
|
425
|
+
},
|
|
426
|
+
{
|
|
427
|
+
id: 'delete',
|
|
428
|
+
label: 'Delete',
|
|
429
|
+
icon: 'trash',
|
|
430
|
+
action: () => this.deleteItem(item.path, item.isDirectory)
|
|
431
|
+
}
|
|
432
|
+
)
|
|
433
|
+
|
|
434
|
+
return baseMenu
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
private getThumbnailUrl(item: FileSystemItem): string | undefined {
|
|
438
|
+
if (item.type === 'file' && this.isImageFile(item.name)) {
|
|
439
|
+
// Return thumbnail URL for image files
|
|
440
|
+
return `/api/thumbnail?path=${encodeURIComponent(item.path)}`
|
|
441
|
+
}
|
|
442
|
+
return undefined
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
private getFileIcon(item: FileSystemItem): string {
|
|
446
|
+
if (item.isDirectory) return 'folder'
|
|
447
|
+
|
|
448
|
+
const ext = item.name.split('.').pop()?.toLowerCase()
|
|
449
|
+
const iconMap: Record<string, string> = {
|
|
450
|
+
// Images
|
|
451
|
+
'jpg': '🖼️', 'jpeg': '🖼️', 'png': '🖼️', 'gif': '🖼️', 'svg': '🖼️',
|
|
452
|
+
// Documents
|
|
453
|
+
'txt': '📄', 'md': '📝', 'pdf': '📕', 'doc': '📘', 'docx': '📘',
|
|
454
|
+
// Code
|
|
455
|
+
'js': '📜', 'ts': '📜', 'tsx': '📜', 'jsx': '📜', 'json': '📋',
|
|
456
|
+
// Other
|
|
457
|
+
'zip': '📦', 'tar': '📦', 'gz': '📦'
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
return iconMap[ext || ''] || '📄'
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
private isImageFile(filename: string): boolean {
|
|
464
|
+
const ext = filename.split('.').pop()?.toLowerCase()
|
|
465
|
+
return ['jpg', 'jpeg', 'png', 'gif', 'svg', 'webp'].includes(ext || '')
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
private async openFile(path: string) {
|
|
469
|
+
// Implementation for opening files
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
private async previewFile(path: string) {
|
|
473
|
+
// Implementation for file preview
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
private async downloadFile(path: string) {
|
|
477
|
+
// Implementation for file download
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
private async renameItem(path: string) {
|
|
481
|
+
// Implementation for renaming items
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
private async deleteItem(path: string, isDirectory: boolean) {
|
|
485
|
+
await this.bridge.deleteItem(path, isDirectory)
|
|
486
|
+
// Refresh the list
|
|
487
|
+
// Could trigger parent to reload
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
## Component Layout
|
|
493
|
+
|
|
494
|
+
### FileBrowser Component Structure
|
|
495
|
+
```typescript
|
|
496
|
+
// Main FileBrowser component that combines tree and list
|
|
497
|
+
export const FileBrowser: React.FC<FileBrowserProps> = observer(({
|
|
498
|
+
fileSystem,
|
|
499
|
+
initialPath = '/',
|
|
500
|
+
className
|
|
501
|
+
}) => {
|
|
502
|
+
const [model] = useState(() => new FileBrowserModel(fileSystem))
|
|
503
|
+
|
|
504
|
+
useEffect(() => {
|
|
505
|
+
model.setInitialPath(initialPath)
|
|
506
|
+
}, [initialPath])
|
|
507
|
+
|
|
508
|
+
return (
|
|
509
|
+
<div className={cn("flex h-full w-full", className)}>
|
|
510
|
+
{/* Left Panel - Tree Navigation (Directories Only) */}
|
|
511
|
+
<div className="w-80 border-r bg-background">
|
|
512
|
+
<div className="p-2 border-b bg-muted/10">
|
|
513
|
+
<h3 className="text-sm font-medium">Folders</h3>
|
|
514
|
+
</div>
|
|
515
|
+
<Tree
|
|
516
|
+
provider={model.treeProvider}
|
|
517
|
+
className="h-full"
|
|
518
|
+
/>
|
|
519
|
+
</div>
|
|
520
|
+
|
|
521
|
+
{/* Right Panel - Content List (All Items) */}
|
|
522
|
+
<div className="flex-1 flex flex-col min-w-0">
|
|
523
|
+
<div className="p-2 border-b bg-muted/10 flex items-center justify-between">
|
|
524
|
+
<div className="flex items-center gap-2">
|
|
525
|
+
<h3 className="text-sm font-medium">Contents</h3>
|
|
526
|
+
<Badge variant="outline" className="text-xs">
|
|
527
|
+
{model.currentPath}
|
|
528
|
+
</Badge>
|
|
529
|
+
</div>
|
|
530
|
+
<ViewTypeSelector
|
|
531
|
+
viewTypes={model.listProvider.getSupportedViewTypes()}
|
|
532
|
+
currentViewType={model.listModel.currentViewType}
|
|
533
|
+
onViewTypeChange={(viewType) => model.listModel.setViewType(viewType)}
|
|
534
|
+
/>
|
|
535
|
+
</div>
|
|
536
|
+
<ListItems
|
|
537
|
+
provider={model.listProvider}
|
|
538
|
+
className="flex-1"
|
|
539
|
+
/>
|
|
540
|
+
</div>
|
|
541
|
+
</div>
|
|
542
|
+
)
|
|
543
|
+
})
|
|
544
|
+
```
|
|
545
|
+
|
|
546
|
+
## State Synchronization Flow
|
|
547
|
+
|
|
548
|
+
### Tree Selection → List Update
|
|
549
|
+
1. User clicks directory in tree
|
|
550
|
+
2. TreeProvider calls `onSelectionChange(path)`
|
|
551
|
+
3. FileBrowserModel.handleTreeSelection():
|
|
552
|
+
- Updates `currentPath` and `selectedPath`
|
|
553
|
+
- Calls `listModel.setPath(path)`
|
|
554
|
+
- Triggers list refresh for new directory
|
|
555
|
+
|
|
556
|
+
### List Navigation → Tree Update
|
|
557
|
+
1. User double-clicks directory in list
|
|
558
|
+
2. ListProvider calls `onNavigation(path)`
|
|
559
|
+
3. FileBrowserModel.handleListNavigation():
|
|
560
|
+
- Updates `currentPath` and `selectedPath`
|
|
561
|
+
- Calls `treeModel.selectPath(path)`
|
|
562
|
+
- Calls `treeModel.expandToPath(path)` to make selection visible
|
|
563
|
+
|
|
564
|
+
### File Operations Coordination
|
|
565
|
+
1. User performs file operation (delete, rename, etc.)
|
|
566
|
+
2. Provider executes operation via FileSystemBridge
|
|
567
|
+
3. Provider triggers refresh of affected components
|
|
568
|
+
4. Both tree and list update to reflect changes
|
|
569
|
+
|
|
570
|
+
## Integration with @anymux/file-system
|
|
571
|
+
|
|
572
|
+
The FileBrowser is designed to work with any implementation of `@anymux/file-system`:
|
|
573
|
+
|
|
574
|
+
```typescript
|
|
575
|
+
// Usage with different file systems
|
|
576
|
+
import { MemoryFileSystem } from '@anymux/file-system/mem-fs'
|
|
577
|
+
import { NodeFileSystem } from '@anymux/file-system/node-fs'
|
|
578
|
+
import { BrowserFileSystem } from '@anymux/file-system/browser-fs'
|
|
579
|
+
|
|
580
|
+
// Memory file system for demos
|
|
581
|
+
const memFs = new MemoryFileSystem()
|
|
582
|
+
const fileBrowser1 = <FileBrowser fileSystem={memFs} />
|
|
583
|
+
|
|
584
|
+
// Node.js file system for server
|
|
585
|
+
const nodeFs = new NodeFileSystem()
|
|
586
|
+
const fileBrowser2 = <FileBrowser fileSystem={nodeFs} />
|
|
587
|
+
|
|
588
|
+
// Browser file system for web
|
|
589
|
+
const browserFs = new BrowserFileSystem()
|
|
590
|
+
const fileBrowser3 = <FileBrowser fileSystem={browserFs} />
|
|
591
|
+
```
|
|
592
|
+
|
|
593
|
+
## File System Adapter Pattern
|
|
594
|
+
|
|
595
|
+
The FileSystemBridge acts as an adapter between the generic `@anymux/file-system` interface and the specific data structures needed by TreeComponent and ListItemsComponent. This allows:
|
|
596
|
+
|
|
597
|
+
1. **File System Independence**: Any compatible file system can be used
|
|
598
|
+
2. **Data Structure Mapping**: Converts Dirent/Stats to component-expected formats
|
|
599
|
+
3. **Error Handling**: Graceful handling of permission errors and missing files
|
|
600
|
+
4. **Performance Optimization**: Caching and batch operations where beneficial
|
|
601
|
+
5. **Type Safety**: Strong typing throughout the component chain
|
|
602
|
+
|
|
603
|
+
## Usage in apps/web/list-samples
|
|
604
|
+
|
|
605
|
+
The FileBrowser will be integrated into the list-samples page as a demonstration:
|
|
606
|
+
|
|
607
|
+
```typescript
|
|
608
|
+
// In apps/web/app/(fixed)/list-samples/page.tsx
|
|
609
|
+
import { FileBrowser } from '@anymux/fs-ui'
|
|
610
|
+
import { MemoryFileSystem } from '@anymux/file-system/mem-fs'
|
|
611
|
+
|
|
612
|
+
const ListSamplesPage = () => {
|
|
613
|
+
const [memFs] = useState(() => {
|
|
614
|
+
const fs = new MemoryFileSystem()
|
|
615
|
+
// Populate with sample data
|
|
616
|
+
return fs
|
|
617
|
+
})
|
|
618
|
+
|
|
619
|
+
return (
|
|
620
|
+
<Tabs defaultValue="list-only">
|
|
621
|
+
<TabsList>
|
|
622
|
+
<TabsTrigger value="list-only">List Component Only</TabsTrigger>
|
|
623
|
+
<TabsTrigger value="file-browser">File Browser (Tree + List)</TabsTrigger>
|
|
624
|
+
</TabsList>
|
|
625
|
+
|
|
626
|
+
<TabsContent value="list-only">
|
|
627
|
+
{/* Existing ListItems demo */}
|
|
628
|
+
</TabsContent>
|
|
629
|
+
|
|
630
|
+
<TabsContent value="file-browser">
|
|
631
|
+
<FileBrowser fileSystem={memFs} />
|
|
632
|
+
</TabsContent>
|
|
633
|
+
</Tabs>
|
|
634
|
+
)
|
|
635
|
+
}
|
|
636
|
+
```
|
|
637
|
+
|
|
638
|
+
This architecture provides:
|
|
639
|
+
- ✅ **Component Reuse**: Leverages existing TreeComponent and ListItemsComponent
|
|
640
|
+
- ✅ **File System Agnostic**: Works with any `@anymux/file-system` implementation
|
|
641
|
+
- ✅ **Synchronized Views**: Tree and list maintain consistent state
|
|
642
|
+
- ✅ **Directory-Only Tree**: Tree shows folders only for navigation
|
|
643
|
+
- ✅ **Full Content List**: List shows all items with rich interactions
|
|
644
|
+
- ✅ **MobX Reactive**: All state changes are reactive and efficient
|
|
645
|
+
- ✅ **Provider Pattern**: Clean separation of concerns with bridges/adapters
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import React, { useState, useRef, useEffect } 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
|
+
import { Input } from '@anymux/ui/components/input';
|
|
13
|
+
|
|
14
|
+
interface CreateItemDialogProps {
|
|
15
|
+
type: 'file' | 'folder';
|
|
16
|
+
onConfirm: (name: string) => void;
|
|
17
|
+
onCancel: () => void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const CreateItemDialog: React.FC<CreateItemDialogProps> = ({
|
|
21
|
+
type,
|
|
22
|
+
onConfirm,
|
|
23
|
+
onCancel,
|
|
24
|
+
}) => {
|
|
25
|
+
const [name, setName] = useState('');
|
|
26
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
27
|
+
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
requestAnimationFrame(() => inputRef.current?.focus());
|
|
30
|
+
}, []);
|
|
31
|
+
|
|
32
|
+
const handleSubmit = () => {
|
|
33
|
+
const trimmed = name.trim();
|
|
34
|
+
if (trimmed) {
|
|
35
|
+
onConfirm(trimmed);
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const handleKeyDown = (e: React.KeyboardEvent) => {
|
|
40
|
+
if (e.key === 'Enter') {
|
|
41
|
+
e.preventDefault();
|
|
42
|
+
handleSubmit();
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<AlertDialog open onOpenChange={(open) => { if (!open) onCancel(); }}>
|
|
48
|
+
<AlertDialogContent>
|
|
49
|
+
<AlertDialogHeader>
|
|
50
|
+
<AlertDialogTitle>New {type === 'folder' ? 'Folder' : 'File'}</AlertDialogTitle>
|
|
51
|
+
<AlertDialogDescription>
|
|
52
|
+
Enter a name for the new {type === 'folder' ? 'folder' : 'file'}.
|
|
53
|
+
</AlertDialogDescription>
|
|
54
|
+
</AlertDialogHeader>
|
|
55
|
+
<Input
|
|
56
|
+
ref={inputRef}
|
|
57
|
+
value={name}
|
|
58
|
+
onChange={(e) => setName(e.target.value)}
|
|
59
|
+
onKeyDown={handleKeyDown}
|
|
60
|
+
placeholder={type === 'folder' ? 'Folder name' : 'File name'}
|
|
61
|
+
/>
|
|
62
|
+
<AlertDialogFooter>
|
|
63
|
+
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
|
64
|
+
<Button onClick={handleSubmit} disabled={!name.trim()}>
|
|
65
|
+
Create
|
|
66
|
+
</Button>
|
|
67
|
+
</AlertDialogFooter>
|
|
68
|
+
</AlertDialogContent>
|
|
69
|
+
</AlertDialog>
|
|
70
|
+
);
|
|
71
|
+
};
|