@anymux/ui-kit 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (244) hide show
  1. package/dist/ExplorerLayout-CSIJd7N4.js +105 -0
  2. package/dist/ExplorerLayout-CSIJd7N4.js.map +1 -0
  3. package/dist/FileBrowserContext-B6jixa2j.js +11 -0
  4. package/dist/FileBrowserContext-B6jixa2j.js.map +1 -0
  5. package/dist/calendar-DSlrbHoj.js +761 -0
  6. package/dist/calendar-DSlrbHoj.js.map +1 -0
  7. package/dist/calendar.d.ts +3 -0
  8. package/dist/calendar.js +3 -0
  9. package/dist/contacts-DQXTZzHc.js +539 -0
  10. package/dist/contacts-DQXTZzHc.js.map +1 -0
  11. package/dist/contacts.d.ts +3 -0
  12. package/dist/contacts.js +3 -0
  13. package/dist/file-browser-m5atC3kF.js +6755 -0
  14. package/dist/file-browser-m5atC3kF.js.map +1 -0
  15. package/dist/file-browser.d.ts +11 -0
  16. package/dist/file-browser.js +9 -0
  17. package/dist/git-B55e6LL-.js +561 -0
  18. package/dist/git-B55e6LL-.js.map +1 -0
  19. package/dist/git.d.ts +2 -0
  20. package/dist/git.js +3 -0
  21. package/dist/iconMap-V4B8P-Uh.js +206 -0
  22. package/dist/iconMap-V4B8P-Uh.js.map +1 -0
  23. package/dist/icons-CIsIOZXR.js +0 -0
  24. package/dist/icons.d.ts +2 -0
  25. package/dist/icons.js +4 -0
  26. package/dist/index-BNmNIWBL.d.ts +71 -0
  27. package/dist/index-BNmNIWBL.d.ts.map +1 -0
  28. package/dist/index-Bryv_GCG.d.ts +1481 -0
  29. package/dist/index-Bryv_GCG.d.ts.map +1 -0
  30. package/dist/index-CuQIjSXs.d.ts +134 -0
  31. package/dist/index-CuQIjSXs.d.ts.map +1 -0
  32. package/dist/index-DSu19mq0.d.ts +153 -0
  33. package/dist/index-DSu19mq0.d.ts.map +1 -0
  34. package/dist/index-DmsyeHFr.d.ts +149 -0
  35. package/dist/index-DmsyeHFr.d.ts.map +1 -0
  36. package/dist/index-DxnJ8FYM.d.ts +17 -0
  37. package/dist/index-DxnJ8FYM.d.ts.map +1 -0
  38. package/dist/index-DzfY1Tok.d.ts +32 -0
  39. package/dist/index-DzfY1Tok.d.ts.map +1 -0
  40. package/dist/index-Ml_SgiKa.d.ts +1847 -0
  41. package/dist/index-Ml_SgiKa.d.ts.map +1 -0
  42. package/dist/index-kHr9udZD.d.ts +1025 -0
  43. package/dist/index-kHr9udZD.d.ts.map +1 -0
  44. package/dist/index.d.ts +11 -0
  45. package/dist/index.js +15 -0
  46. package/dist/layout-Ca_4r8ka.js +89 -0
  47. package/dist/layout-Ca_4r8ka.js.map +1 -0
  48. package/dist/layout.d.ts +2 -0
  49. package/dist/layout.js +5 -0
  50. package/dist/list-CxfT6hix.js +6831 -0
  51. package/dist/list-CxfT6hix.js.map +1 -0
  52. package/dist/list.d.ts +2 -0
  53. package/dist/list.js +5 -0
  54. package/dist/media-DZ292aKK.js +557 -0
  55. package/dist/media-DZ292aKK.js.map +1 -0
  56. package/dist/media.d.ts +3 -0
  57. package/dist/media.js +3 -0
  58. package/dist/tree-Dd9Z0Aso.js +3351 -0
  59. package/dist/tree-Dd9Z0Aso.js.map +1 -0
  60. package/dist/tree.d.ts +2 -0
  61. package/dist/tree.js +6 -0
  62. package/dist/types-common-CB3kRek8.d.ts +26 -0
  63. package/dist/types-common-CB3kRek8.d.ts.map +1 -0
  64. package/dist/utils-B4fdKKsy.js +3 -0
  65. package/package.json +109 -0
  66. package/src/calendar/AgendaView.tsx +37 -0
  67. package/src/calendar/CalendarBrowser.tsx +90 -0
  68. package/src/calendar/CalendarModel.ts +142 -0
  69. package/src/calendar/CalendarSidebar.tsx +81 -0
  70. package/src/calendar/DayView.tsx +76 -0
  71. package/src/calendar/EventCard.tsx +51 -0
  72. package/src/calendar/MockCalendarProvider.ts +98 -0
  73. package/src/calendar/MonthView.tsx +77 -0
  74. package/src/calendar/WeekView.tsx +129 -0
  75. package/src/calendar/index.ts +18 -0
  76. package/src/calendar/types.ts +25 -0
  77. package/src/contacts/ContactAvatar.tsx +35 -0
  78. package/src/contacts/ContactBrowser.tsx +56 -0
  79. package/src/contacts/ContactCard.tsx +37 -0
  80. package/src/contacts/ContactDetail.tsx +63 -0
  81. package/src/contacts/ContactGroupSidebar.tsx +40 -0
  82. package/src/contacts/ContactList.tsx +32 -0
  83. package/src/contacts/ContactListModel.ts +120 -0
  84. package/src/contacts/MockContactProvider.ts +77 -0
  85. package/src/contacts/index.ts +17 -0
  86. package/src/contacts/types.ts +26 -0
  87. package/src/demos/CalendarBrowserDemo.tsx +15 -0
  88. package/src/demos/ContactBrowserDemo.tsx +15 -0
  89. package/src/demos/MediaBrowserDemo.tsx +15 -0
  90. package/src/file-browser/adapters/DocumentViewerAdapter.ts +371 -0
  91. package/src/file-browser/adapters/FileSystemBridge.ts +168 -0
  92. package/src/file-browser/adapters/GitBrowserAdapter.ts +546 -0
  93. package/src/file-browser/adapters/README.md +504 -0
  94. package/src/file-browser/adapters/index.ts +27 -0
  95. package/src/file-browser/adapters/types.ts +70 -0
  96. package/src/file-browser/architecture.md +645 -0
  97. package/src/file-browser/components/CreateItemDialog.tsx +71 -0
  98. package/src/file-browser/components/DeleteConfirmDialog.tsx +58 -0
  99. package/src/file-browser/components/FileBrowser.tsx +473 -0
  100. package/src/file-browser/components/FileBrowserContent.tsx +209 -0
  101. package/src/file-browser/components/FileBrowserHeader.tsx +151 -0
  102. package/src/file-browser/components/FileBrowserToolbar.tsx +145 -0
  103. package/src/file-browser/components/LeftPanel/LeftPanel.tsx +103 -0
  104. package/src/file-browser/components/LeftPanel/LeftPanelTabs.tsx +70 -0
  105. package/src/file-browser/components/LeftPanel/TreeNavigationView.tsx +256 -0
  106. package/src/file-browser/components/PreviewPane.tsx +146 -0
  107. package/src/file-browser/components/RightPanel/FilePreview.tsx +219 -0
  108. package/src/file-browser/components/RightPanel/RightPanel.tsx +186 -0
  109. package/src/file-browser/components/RightPanel/RightPanelToolbar.tsx +113 -0
  110. package/src/file-browser/components/UploadProgress.tsx +123 -0
  111. package/src/file-browser/components/ViewerHost.tsx +208 -0
  112. package/src/file-browser/components/mobile/MobileNavigation.tsx +227 -0
  113. package/src/file-browser/components/navigation/NavigationButtons.tsx +171 -0
  114. package/src/file-browser/components/shared/ErrorBoundary.tsx +116 -0
  115. package/src/file-browser/components/shared/FileBrowserItem.tsx +195 -0
  116. package/src/file-browser/components/shared/FileIcon.tsx +169 -0
  117. package/src/file-browser/components/toolbar/ViewModeToggle.tsx +200 -0
  118. package/src/file-browser/components/views/ListView/ListView.tsx +484 -0
  119. package/src/file-browser/components/views/ThumbnailView/ThumbnailView.tsx +323 -0
  120. package/src/file-browser/components/views/TreeView/TreeNode.tsx +186 -0
  121. package/src/file-browser/components/views/TreeView/TreeNodeList.tsx +191 -0
  122. package/src/file-browser/components/views/TreeView/TreeView.tsx +200 -0
  123. package/src/file-browser/components/views/TreemapView/TreemapView.tsx +339 -0
  124. package/src/file-browser/context/FileBrowserContext.tsx +13 -0
  125. package/src/file-browser/examples/BasicUsage.tsx +20 -0
  126. package/src/file-browser/index.ts +98 -0
  127. package/src/file-browser/models/FileBrowserModel.ts +623 -0
  128. package/src/file-browser/models/LeftPanelManagerModel.ts +105 -0
  129. package/src/file-browser/models/NavigationManagerModel.ts +312 -0
  130. package/src/file-browser/models/ResponsiveLayoutManagerModel.ts +437 -0
  131. package/src/file-browser/models/RightPanelManagerModel.ts +190 -0
  132. package/src/file-browser/models/SelectionManagerModel.ts +252 -0
  133. package/src/file-browser/models/ToolbarManagerModel.ts +144 -0
  134. package/src/file-browser/models/UploadModel.ts +147 -0
  135. package/src/file-browser/models/ViewModeManagerModel.ts +185 -0
  136. package/src/file-browser/models/ViewerHostModel.ts +44 -0
  137. package/src/file-browser/models/ui/ListViewUIModel.ts +265 -0
  138. package/src/file-browser/models/ui/PreviewUIModel.ts +297 -0
  139. package/src/file-browser/models/ui/ThumbnailViewUIModel.ts +254 -0
  140. package/src/file-browser/models/ui/TreeViewUIModel.ts +128 -0
  141. package/src/file-browser/models/ui/TreemapViewUIModel.ts +350 -0
  142. package/src/file-browser/providers/FileSystemListProvider.ts +552 -0
  143. package/src/file-browser/providers/FileSystemProvider.ts +401 -0
  144. package/src/file-browser/providers/FileSystemTreeProvider.ts +231 -0
  145. package/src/file-browser/providers/GitProvider.ts +337 -0
  146. package/src/file-browser/providers/GitRepositoryProvider.ts +376 -0
  147. package/src/file-browser/providers/IFileBrowserProvider.ts +56 -0
  148. package/src/file-browser/providers/MemoryProvider.ts +303 -0
  149. package/src/file-browser/providers/index.ts +4 -0
  150. package/src/file-browser/registry/ViewerRegistry.ts +551 -0
  151. package/src/file-browser/registry/types.ts +144 -0
  152. package/src/file-browser/scripts/performanceBenchmark.ts +553 -0
  153. package/src/file-browser/services/ThumbnailCacheService.ts +128 -0
  154. package/src/file-browser/tasks.md +537 -0
  155. package/src/file-browser/types/FileBrowserTypes.ts +126 -0
  156. package/src/file-browser/types/ProviderTypes.ts +155 -0
  157. package/src/file-browser/types/UITypes.ts +235 -0
  158. package/src/file-browser/types/ViewModeTypes.ts +150 -0
  159. package/src/file-browser/utils/gestures.ts +327 -0
  160. package/src/file-browser/utils/performance.ts +563 -0
  161. package/src/file-browser/viewers/ImageViewer.tsx +163 -0
  162. package/src/file-browser/viewers/ImageViewerModel.ts +79 -0
  163. package/src/file-browser/viewers/TextViewer.tsx +95 -0
  164. package/src/file-browser/viewers/UnsupportedFileViewer.tsx +57 -0
  165. package/src/file-browser/viewers/index.ts +61 -0
  166. package/src/git/BranchList.tsx +128 -0
  167. package/src/git/CommitGraph.tsx +239 -0
  168. package/src/git/CommitList.tsx +258 -0
  169. package/src/git/DiffViewer.tsx +219 -0
  170. package/src/git/index.ts +4 -0
  171. package/src/icons/iconMap.ts +146 -0
  172. package/src/icons/index.ts +9 -0
  173. package/src/index.ts +13 -0
  174. package/src/layout/README.md +307 -0
  175. package/src/layout/components/ExplorerLayout/ExplorerLayout.tsx +178 -0
  176. package/src/layout/examples/SimpleExample.tsx +60 -0
  177. package/src/layout/index.ts +6 -0
  178. package/src/lib/utils.ts +1 -0
  179. package/src/list/README.md +303 -0
  180. package/src/list/architecture.md +807 -0
  181. package/src/list/components/CalculatedGridView.tsx +252 -0
  182. package/src/list/components/DragPreview.tsx +102 -0
  183. package/src/list/components/ListContextMenu.tsx +274 -0
  184. package/src/list/components/ListItem.tsx +761 -0
  185. package/src/list/components/ListItems.tsx +919 -0
  186. package/src/list/components/MasonryView.tsx +241 -0
  187. package/src/list/components/SearchFilter.tsx +44 -0
  188. package/src/list/components/TreemapView.tsx +709 -0
  189. package/src/list/components/ViewSizeControls.tsx +205 -0
  190. package/src/list/components/ViewTypeSelector.tsx +312 -0
  191. package/src/list/components/VirtualizedDetailsView.tsx +231 -0
  192. package/src/list/components/VirtualizedGrid.tsx +164 -0
  193. package/src/list/components/VirtualizedList.tsx +154 -0
  194. package/src/list/components/VirtualizedMasonryView.tsx +344 -0
  195. package/src/list/components/shared/EmptyState.tsx +103 -0
  196. package/src/list/components/shared/ErrorBoundary.tsx +123 -0
  197. package/src/list/components/shared/ErrorDisplay.tsx +100 -0
  198. package/src/list/components/shared/ListLoader.tsx +146 -0
  199. package/src/list/components/shared/LoadingIndicator.tsx +80 -0
  200. package/src/list/index.ts +92 -0
  201. package/src/list/models/ListItemsModel.ts +1301 -0
  202. package/src/list/models/TreemapModel.ts +204 -0
  203. package/src/list/providers/ListItemsProvider.ts +313 -0
  204. package/src/list/providers/TestListProvider.ts +604 -0
  205. package/src/list/tasks.md +937 -0
  206. package/src/list/types/ListTypes.ts +178 -0
  207. package/src/list/utils/BenchmarkLogger.ts +243 -0
  208. package/src/list/utils/DragDropManager.ts +320 -0
  209. package/src/list/utils/GridLayoutCalculator.ts +290 -0
  210. package/src/list/utils/ListAccessibility.ts +367 -0
  211. package/src/list/utils/ListKeyboard.ts +414 -0
  212. package/src/list/utils/MasonryLayoutCalculator.ts +302 -0
  213. package/src/list/utils/MasonryLayoutEngine.ts +401 -0
  214. package/src/list/utils/__tests__/MasonryLayoutEngine.test.ts +157 -0
  215. package/src/list/utils/__tests__/VirtualizedMasonryView.test.tsx +251 -0
  216. package/src/media/AlbumSidebar.tsx +48 -0
  217. package/src/media/MediaBrowser.tsx +92 -0
  218. package/src/media/MediaBrowserModel.ts +138 -0
  219. package/src/media/MediaGrid.tsx +50 -0
  220. package/src/media/MediaList.tsx +49 -0
  221. package/src/media/MediaPreview.tsx +63 -0
  222. package/src/media/MediaTimeline.tsx +38 -0
  223. package/src/media/MockMediaProvider.ts +70 -0
  224. package/src/media/index.ts +18 -0
  225. package/src/media/types.ts +21 -0
  226. package/src/styles/variables.css +60 -0
  227. package/src/tree/DEVELOPMENT_SUMMARY.md +170 -0
  228. package/src/tree/__tests__/TreeModel.test.ts +16 -0
  229. package/src/tree/architecture.md +530 -0
  230. package/src/tree/components/Tree.tsx +283 -0
  231. package/src/tree/components/TreeCheckbox.tsx +147 -0
  232. package/src/tree/components/TreeContextMenu.tsx +139 -0
  233. package/src/tree/components/TreeNodeList.tsx +329 -0
  234. package/src/tree/components/TreeTable.tsx +382 -0
  235. package/src/tree/index.ts +58 -0
  236. package/src/tree/models/TreeModel.ts +839 -0
  237. package/src/tree/providers/SimpleTreeProvider.ts +463 -0
  238. package/src/tree/providers/TestTreeProvider.ts +946 -0
  239. package/src/tree/providers/TreeProvider.ts +308 -0
  240. package/src/tree/tasks.md +2046 -0
  241. package/src/tree/types/TreeTypes.ts +279 -0
  242. package/src/tree/utils/SelectionTheme.ts +150 -0
  243. package/src/tree/utils/logger.ts +203 -0
  244. package/src/types-common.ts +21 -0
@@ -0,0 +1,186 @@
1
+ import React from 'react';
2
+ import { observer } from 'mobx-react-lite';
3
+ import { RightPanelManagerModel } from '../../models/RightPanelManagerModel';
4
+ import { ResponsiveLayoutManagerModel } from '../../models/ResponsiveLayoutManagerModel';
5
+ import { RightPanelToolbar } from './RightPanelToolbar';
6
+ import { FilePreview } from './FilePreview';
7
+
8
+ // Import view components
9
+ import ListView from '../views/ListView/ListView';
10
+ import ThumbnailView from '../views/ThumbnailView/ThumbnailView';
11
+ import TreemapView from '../views/TreemapView/TreemapView';
12
+
13
+ interface RightPanelProps {
14
+ rightPanelManager: RightPanelManagerModel;
15
+ responsiveManager: ResponsiveLayoutManagerModel;
16
+ items?: any[];
17
+ onItemSelect?: (item: any) => void;
18
+ onItemActivate?: (item: any) => void;
19
+ onNavigate?: (path: string) => void;
20
+ className?: string;
21
+ }
22
+
23
+ export const RightPanel: React.FC<RightPanelProps> = observer(({
24
+ rightPanelManager,
25
+ responsiveManager,
26
+ items = [],
27
+ onItemSelect,
28
+ onItemActivate,
29
+ onNavigate,
30
+ className = ''
31
+ }) => {
32
+ const contentType = rightPanelManager.contentType;
33
+ const currentViewMode = rightPanelManager.currentViewMode;
34
+ const isVisible = responsiveManager.shouldShowRightPanel;
35
+
36
+ // Handle mobile overlay behavior
37
+ const shouldShowAsOverlay = responsiveManager.isMobile && responsiveManager.activePanel === 'right';
38
+
39
+ const handleOverlayClick = (event: React.MouseEvent) => {
40
+ // Close overlay when clicking outside on mobile
41
+ if (event.target === event.currentTarget && shouldShowAsOverlay) {
42
+ responsiveManager.hideMobilePanel();
43
+ }
44
+ };
45
+
46
+ const handleViewModeChange = (viewModeId: string) => {
47
+ rightPanelManager.setCurrentViewMode(viewModeId);
48
+ };
49
+
50
+ const handleItemSelectWithCoordination = (item: any) => {
51
+ // Use selection coordination through right panel manager
52
+ if (rightPanelManager.fileBrowserModel?.selectionManager) {
53
+ rightPanelManager.fileBrowserModel.selectionManager.selectFromRightPanel(item);
54
+ }
55
+
56
+ // Call original callback for backward compatibility
57
+ onItemSelect?.(item);
58
+ };
59
+
60
+ const handleItemActivateWithCoordination = (item: any) => {
61
+ // Handle double-click/activation - navigate to folders
62
+ if (item.type === 'directory' && rightPanelManager.fileBrowserModel?.navigationManager) {
63
+ rightPanelManager.fileBrowserModel.navigationManager.navigateToWithCoordination(item.path, 'right');
64
+ }
65
+
66
+ // Call original callback for backward compatibility
67
+ onItemActivate?.(item);
68
+ };
69
+
70
+ const renderContent = () => {
71
+ switch (contentType) {
72
+ case 'file':
73
+ return (
74
+ <FilePreview
75
+ previewUI={rightPanelManager.previewUI}
76
+ selectedItem={rightPanelManager.selectedItem}
77
+ className="right-panel-file-preview"
78
+ />
79
+ );
80
+
81
+ case 'folder':
82
+ return renderFolderContent();
83
+
84
+ case 'empty':
85
+ default:
86
+ return (
87
+ <div className="right-panel-empty-state">
88
+ <p>Select a file or folder to view its contents</p>
89
+ </div>
90
+ );
91
+ }
92
+ };
93
+
94
+ const renderFolderContent = () => {
95
+ const currentUIModel = rightPanelManager.currentUIModel;
96
+
97
+ switch (currentViewMode) {
98
+ case 'list':
99
+ return (
100
+ <ListView
101
+ items={items}
102
+ listModel={rightPanelManager.listViewUI}
103
+ responsiveManager={responsiveManager}
104
+ onItemClick={handleItemSelectWithCoordination}
105
+ onItemActivate={handleItemActivateWithCoordination}
106
+ className="right-panel-list-view"
107
+ />
108
+ );
109
+
110
+ case 'thumbnail':
111
+ return (
112
+ <ThumbnailView
113
+ items={items}
114
+ thumbnailModel={rightPanelManager.thumbnailViewUI}
115
+ responsiveManager={responsiveManager}
116
+ onItemClick={handleItemSelectWithCoordination}
117
+ onItemActivate={handleItemActivateWithCoordination}
118
+ className="right-panel-thumbnail-view"
119
+ />
120
+ );
121
+
122
+ case 'treemap':
123
+ return (
124
+ <TreemapView
125
+ items={items}
126
+ treemapModel={rightPanelManager.treemapViewUI}
127
+ onItemClick={handleItemSelectWithCoordination}
128
+ onItemActivate={handleItemActivateWithCoordination}
129
+ className="right-panel-treemap-view"
130
+ />
131
+ );
132
+
133
+ case 'detail':
134
+ // Use ListView with detail configuration
135
+ return (
136
+ <ListView
137
+ items={items}
138
+ listModel={rightPanelManager.listViewUI}
139
+ onItemClick={handleItemSelectWithCoordination}
140
+ onItemActivate={handleItemActivateWithCoordination}
141
+ className="right-panel-detail-view"
142
+ />
143
+ );
144
+
145
+ default:
146
+ return (
147
+ <div className="right-panel-unknown-view">
148
+ <p>Unknown view mode: {currentViewMode}</p>
149
+ </div>
150
+ );
151
+ }
152
+ };
153
+
154
+ if (!isVisible) {
155
+ return null;
156
+ }
157
+
158
+ return (
159
+ <div
160
+ className={`right-panel ${shouldShowAsOverlay ? 'right-panel-overlay' : 'right-panel-docked'} ${className}`}
161
+ onClick={handleOverlayClick}
162
+ aria-label="Content panel"
163
+ role="main"
164
+ >
165
+ <div
166
+ className="right-panel-content"
167
+ onClick={(e) => e.stopPropagation()} // Prevent overlay close when clicking content
168
+ >
169
+ {/* Toolbar with view mode toggles */}
170
+ <RightPanelToolbar
171
+ rightPanelManager={rightPanelManager}
172
+ responsiveManager={responsiveManager}
173
+ onViewModeChange={handleViewModeChange}
174
+ onNavigate={onNavigate}
175
+ />
176
+
177
+ {/* Content Area */}
178
+ <div className="right-panel-main-content">
179
+ {renderContent()}
180
+ </div>
181
+ </div>
182
+ </div>
183
+ );
184
+ });
185
+
186
+ RightPanel.displayName = 'RightPanel';
@@ -0,0 +1,113 @@
1
+ import React from 'react';
2
+ import { observer } from 'mobx-react-lite';
3
+ import { List, Grid3X3, LayoutDashboard, Info, X } from 'lucide-react';
4
+ import { RightPanelManagerModel } from '../../models/RightPanelManagerModel';
5
+ import { ResponsiveLayoutManagerModel } from '../../models/ResponsiveLayoutManagerModel';
6
+
7
+ interface RightPanelToolbarProps {
8
+ rightPanelManager: RightPanelManagerModel;
9
+ responsiveManager: ResponsiveLayoutManagerModel;
10
+ onViewModeChange: (viewModeId: string) => void;
11
+ onNavigate?: (path: string) => void;
12
+ className?: string;
13
+ }
14
+
15
+ const VIEW_MODE_ICONS = {
16
+ list: List,
17
+ thumbnail: Grid3X3,
18
+ treemap: LayoutDashboard,
19
+ detail: Info
20
+ };
21
+
22
+ export const RightPanelToolbar: React.FC<RightPanelToolbarProps> = observer(({
23
+ rightPanelManager,
24
+ responsiveManager,
25
+ onViewModeChange,
26
+ onNavigate,
27
+ className = ''
28
+ }) => {
29
+ const availableViewModes = rightPanelManager.availableViewModesList;
30
+ const currentViewMode = rightPanelManager.currentViewMode;
31
+ const contentType = rightPanelManager.contentType;
32
+ const hasViewModeOptions = rightPanelManager.hasViewModeOptions;
33
+
34
+ const handleViewModeClick = (viewModeId: string) => {
35
+ if (viewModeId !== currentViewMode) {
36
+ onViewModeChange(viewModeId);
37
+ }
38
+ };
39
+
40
+ const handleCloseClick = () => {
41
+ if (responsiveManager.isMobile) {
42
+ responsiveManager.hideMobilePanel();
43
+ }
44
+ };
45
+
46
+ const handleKeyDown = (event: React.KeyboardEvent, viewModeId: string) => {
47
+ if (event.key === 'Enter' || event.key === ' ') {
48
+ event.preventDefault();
49
+ handleViewModeClick(viewModeId);
50
+ }
51
+ };
52
+
53
+ return (
54
+ <div className={`right-panel-toolbar ${className}`}>
55
+ <div className="right-panel-toolbar-content">
56
+ {/* Content Type Indicator */}
57
+ <div className="right-panel-content-info">
58
+ <span className="right-panel-content-type">
59
+ {contentType === 'file' ? 'File Preview' :
60
+ contentType === 'folder' ? 'Folder Contents' :
61
+ 'No Selection'}
62
+ </span>
63
+ </div>
64
+
65
+ {/* View Mode Toggles - Only show for folder content */}
66
+ {hasViewModeOptions && (
67
+ <div
68
+ className="right-panel-view-modes"
69
+ role="toolbar"
70
+ aria-label="View modes"
71
+ >
72
+ {availableViewModes.map((viewMode) => {
73
+ const IconComponent = VIEW_MODE_ICONS[viewMode.id as keyof typeof VIEW_MODE_ICONS];
74
+ const isActive = currentViewMode === viewMode.id;
75
+
76
+ return (
77
+ <button
78
+ key={viewMode.id}
79
+ className={`right-panel-view-mode-button ${isActive ? 'active' : ''}`}
80
+ onClick={() => handleViewModeClick(viewMode.id)}
81
+ onKeyDown={(e) => handleKeyDown(e, viewMode.id)}
82
+ title={viewMode.name}
83
+ aria-label={`Switch to ${viewMode.name} view`}
84
+ aria-pressed={isActive}
85
+ disabled={!viewMode.applicableFor?.(contentType)}
86
+ >
87
+ {IconComponent && <IconComponent size={16} />}
88
+ <span className="right-panel-view-mode-label">
89
+ {viewMode.name}
90
+ </span>
91
+ </button>
92
+ );
93
+ })}
94
+ </div>
95
+ )}
96
+
97
+ {/* Mobile Close Button */}
98
+ {responsiveManager.isMobile && (
99
+ <button
100
+ className="right-panel-close-button"
101
+ onClick={handleCloseClick}
102
+ aria-label="Close panel"
103
+ title="Close panel"
104
+ >
105
+ <X size={20} />
106
+ </button>
107
+ )}
108
+ </div>
109
+ </div>
110
+ );
111
+ });
112
+
113
+ RightPanelToolbar.displayName = 'RightPanelToolbar';
@@ -0,0 +1,123 @@
1
+ import React from 'react';
2
+ import { observer } from 'mobx-react-lite';
3
+ import { X, CheckCircle2, AlertCircle, Upload, FileUp } from 'lucide-react';
4
+ import { cn } from '../../lib/utils';
5
+ import { Button } from '@anymux/ui/components/button';
6
+ import { Progress } from '@anymux/ui/components/progress';
7
+ import type { UploadModel } from '../models/UploadModel';
8
+
9
+ interface UploadProgressProps {
10
+ uploadModel: UploadModel;
11
+ className?: string;
12
+ }
13
+
14
+ /**
15
+ * Floating upload progress panel.
16
+ * Shows during active uploads and briefly after completion.
17
+ */
18
+ export const UploadProgress: React.FC<UploadProgressProps> = observer(({
19
+ uploadModel,
20
+ className,
21
+ }) => {
22
+ const { files, isUploading, overallProgress, completedCount, totalCount, showSummary, lastSummary } = uploadModel;
23
+
24
+ // Nothing to show
25
+ if (!isUploading && !showSummary) return null;
26
+
27
+ return (
28
+ <div
29
+ className={cn(
30
+ 'absolute bottom-3 right-3 z-40 w-72 rounded-lg border bg-background shadow-lg',
31
+ className
32
+ )}
33
+ >
34
+ {/* Header */}
35
+ <div className="flex items-center justify-between px-3 py-2 border-b bg-muted/30">
36
+ <div className="flex items-center gap-2 text-sm font-medium">
37
+ <FileUp className="w-4 h-4" />
38
+ {isUploading ? (
39
+ <span>Uploading {completedCount}/{totalCount}</span>
40
+ ) : (
41
+ <span>Upload Complete</span>
42
+ )}
43
+ </div>
44
+ <Button
45
+ variant="ghost"
46
+ size="icon"
47
+ className="h-5 w-5"
48
+ onClick={() => {
49
+ if (!isUploading) {
50
+ uploadModel.clear();
51
+ } else {
52
+ uploadModel.dismissSummary();
53
+ }
54
+ }}
55
+ title="Dismiss"
56
+ >
57
+ <X className="w-3 h-3" />
58
+ </Button>
59
+ </div>
60
+
61
+ {/* Overall progress bar */}
62
+ {isUploading && (
63
+ <div className="px-3 py-2">
64
+ <Progress value={overallProgress} className="h-1.5" />
65
+ <p className="text-xs text-muted-foreground mt-1">
66
+ {overallProgress}% complete
67
+ </p>
68
+ </div>
69
+ )}
70
+
71
+ {/* File list (max 4 visible, scrollable) */}
72
+ <div className="max-h-32 overflow-y-auto">
73
+ {files.map((entry, idx) => (
74
+ <div
75
+ key={`${entry.targetPath}-${idx}`}
76
+ className="flex items-center gap-2 px-3 py-1.5 text-xs border-t first:border-t-0"
77
+ >
78
+ {entry.status === 'done' && (
79
+ <CheckCircle2 className="w-3.5 h-3.5 text-green-500 flex-shrink-0" />
80
+ )}
81
+ {entry.status === 'error' && (
82
+ <AlertCircle className="w-3.5 h-3.5 text-destructive flex-shrink-0" />
83
+ )}
84
+ {entry.status === 'uploading' && (
85
+ <Upload className="w-3.5 h-3.5 text-primary animate-pulse flex-shrink-0" />
86
+ )}
87
+ {entry.status === 'pending' && (
88
+ <div className="w-3.5 h-3.5 rounded-full border border-muted-foreground/40 flex-shrink-0" />
89
+ )}
90
+ <span className="truncate flex-1" title={entry.file.name}>
91
+ {entry.file.name}
92
+ </span>
93
+ {entry.status === 'uploading' && (
94
+ <span className="text-muted-foreground flex-shrink-0">{entry.progress}%</span>
95
+ )}
96
+ {entry.status === 'error' && (
97
+ <span className="text-destructive flex-shrink-0 truncate max-w-[80px]" title={entry.error}>
98
+ {entry.error}
99
+ </span>
100
+ )}
101
+ </div>
102
+ ))}
103
+ </div>
104
+
105
+ {/* Summary footer */}
106
+ {showSummary && lastSummary && !isUploading && (
107
+ <div className="px-3 py-2 border-t bg-muted/20 text-xs text-muted-foreground">
108
+ {lastSummary.failed === 0 ? (
109
+ <span className="text-green-600 dark:text-green-400">
110
+ All {lastSummary.total} file{lastSummary.total !== 1 ? 's' : ''} uploaded successfully
111
+ </span>
112
+ ) : (
113
+ <span>
114
+ {lastSummary.succeeded} succeeded, {lastSummary.failed} failed
115
+ </span>
116
+ )}
117
+ </div>
118
+ )}
119
+ </div>
120
+ );
121
+ });
122
+
123
+ UploadProgress.displayName = 'UploadProgress';
@@ -0,0 +1,208 @@
1
+ import React, { Suspense, useState, useEffect, useCallback } from 'react';
2
+ import { observer } from 'mobx-react-lite';
3
+ import { Loader2 } from 'lucide-react';
4
+ import { cn } from '../../lib/utils';
5
+ import type { ViewerProps } from '../registry/ViewerRegistry';
6
+ import type { ResolvedViewer, KeyboardShortcut, ToolbarAction } from '../registry/types';
7
+ import { ViewerHostModel } from '../models/ViewerHostModel';
8
+
9
+ export interface ViewerHostProps {
10
+ /** The resolved viewer from the registry */
11
+ viewer: ResolvedViewer;
12
+ /** File metadata and content */
13
+ file: ViewerProps['file'];
14
+ /** 'full' for main viewer, 'preview' for inline pane */
15
+ mode: 'full' | 'preview';
16
+ /** Called when the viewer wants to close */
17
+ onClose?: () => void;
18
+ /** Called to save edits */
19
+ onSave?: (content: string | ArrayBuffer) => Promise<void>;
20
+ /** Whether the file is read-only */
21
+ readOnly?: boolean;
22
+ /** Optional URL for direct content access */
23
+ contentUrl?: string;
24
+ /** Optional lazy content fetcher */
25
+ fetchContent?: () => Promise<string | ArrayBuffer>;
26
+ /** Additional toolbar elements from the host (e.g., prev/next nav) */
27
+ hostToolbar?: React.ReactNode;
28
+ className?: string;
29
+ }
30
+
31
+ /**
32
+ * Loading fallback shown while a lazy-loaded viewer component is being fetched.
33
+ */
34
+ const ViewerLoadingFallback: React.FC<{ name: string }> = ({ name }) => (
35
+ <div className="flex flex-col items-center justify-center h-full gap-2 text-muted-foreground">
36
+ <Loader2 className="w-6 h-6 animate-spin" />
37
+ <p className="text-sm">Loading {name}...</p>
38
+ </div>
39
+ );
40
+
41
+ /**
42
+ * Render a single ToolbarAction as a button.
43
+ */
44
+ const ToolbarActionButton: React.FC<{ action: ToolbarAction }> = ({ action }) => {
45
+ const isEnabled = typeof action.enabled === 'function' ? action.enabled() : action.enabled !== false;
46
+ const Icon = action.icon;
47
+
48
+ return (
49
+ <button
50
+ onClick={action.onClick}
51
+ disabled={!isEnabled}
52
+ className={cn(
53
+ 'p-1 rounded hover:bg-muted text-muted-foreground hover:text-foreground transition-colors',
54
+ 'disabled:opacity-40 disabled:pointer-events-none'
55
+ )}
56
+ title={action.shortcutLabel ? `${action.label} (${action.shortcutLabel})` : action.label}
57
+ >
58
+ {Icon ? <Icon className="w-3.5 h-3.5" /> : <span className="text-xs px-1">{action.label}</span>}
59
+ </button>
60
+ );
61
+ };
62
+
63
+ /**
64
+ * ViewerHost: shared viewer surface component.
65
+ *
66
+ * Renders a resolved viewer plugin wrapped in Suspense, with:
67
+ * - Plugin's declarative toolbarActions (left + right positions)
68
+ * - Host toolbar slot (e.g., prev/next navigation)
69
+ * - Dynamic toolbar extras injected by the plugin via onToolbarExtras
70
+ * - Keyboard shortcut bindings from the plugin
71
+ * - Dirty state tracking via onDirtyChange
72
+ *
73
+ * Used by FileBrowser (full viewer mode), PreviewPane, and
74
+ * future ObjectStorageBrowser/GitBrowser integration.
75
+ */
76
+ export const ViewerHost: React.FC<ViewerHostProps> = observer(({
77
+ viewer,
78
+ file,
79
+ mode,
80
+ onClose,
81
+ onSave,
82
+ readOnly,
83
+ contentUrl,
84
+ fetchContent,
85
+ hostToolbar,
86
+ className,
87
+ }) => {
88
+ const [model] = useState(() => new ViewerHostModel());
89
+
90
+ // Reset model when viewer or file changes
91
+ useEffect(() => {
92
+ model.reset();
93
+ }, [viewer.plugin.id, file.path, model]);
94
+
95
+ // Bind keyboard shortcuts from the plugin
96
+ useEffect(() => {
97
+ const shortcuts = viewer.plugin.keyboardShortcuts;
98
+ if (!shortcuts || shortcuts.length === 0) return;
99
+
100
+ const handleKeyDown = (e: KeyboardEvent) => {
101
+ for (const shortcut of shortcuts) {
102
+ if (matchShortcut(e, shortcut.key)) {
103
+ e.preventDefault();
104
+ shortcut.handler();
105
+ return;
106
+ }
107
+ }
108
+ };
109
+
110
+ window.addEventListener('keydown', handleKeyDown);
111
+ return () => window.removeEventListener('keydown', handleKeyDown);
112
+ }, [viewer.plugin.keyboardShortcuts]);
113
+
114
+ // Callback for the plugin to report dirty state
115
+ const handleDirtyChange = useCallback((isDirty: boolean) => {
116
+ model.setDirty(isDirty);
117
+ }, [model]);
118
+
119
+ // Callback for the plugin to inject toolbar extras
120
+ const handleToolbarExtras = useCallback((extras: React.ReactNode) => {
121
+ model.setToolbarExtras(extras);
122
+ }, [model]);
123
+
124
+ const { Component, plugin } = viewer;
125
+
126
+ // Collect toolbar actions from the plugin
127
+ const leftActions = (plugin.toolbarActions ?? []).filter(a => a.position === 'left');
128
+ const rightActions = (plugin.toolbarActions ?? []).filter(a => a.position !== 'left');
129
+
130
+ const hasToolbar = mode === 'full' && (
131
+ leftActions.length > 0 ||
132
+ rightActions.length > 0 ||
133
+ hostToolbar ||
134
+ model.toolbarExtras
135
+ );
136
+
137
+ // Build viewer props, only setting optional fields when defined
138
+ // (exactOptionalPropertyTypes forbids assigning undefined to optional props)
139
+ const viewerProps: ViewerProps = {
140
+ file,
141
+ className: 'h-full',
142
+ onToolbarExtras: handleToolbarExtras,
143
+ onDirtyChange: handleDirtyChange,
144
+ mode,
145
+ };
146
+ if (onClose !== undefined) viewerProps.onClose = onClose;
147
+ if (onSave !== undefined) viewerProps.onSave = onSave;
148
+ if (readOnly !== undefined) viewerProps.readOnly = readOnly;
149
+ if (fetchContent !== undefined) viewerProps.fetchContent = fetchContent;
150
+ if (contentUrl !== undefined) viewerProps.contentUrl = contentUrl;
151
+
152
+ return (
153
+ <div className={cn('h-full flex flex-col', className)}>
154
+ {/* Toolbar (only in full mode, and only if there are items to show) */}
155
+ {hasToolbar && (
156
+ <div className="flex items-center justify-between px-2 py-1 border-b bg-muted/20 flex-shrink-0 min-h-[36px]">
157
+ {/* Left side: plugin left actions */}
158
+ <div className="flex items-center gap-0.5">
159
+ {leftActions.map(action => (
160
+ <ToolbarActionButton key={action.id} action={action} />
161
+ ))}
162
+ </div>
163
+
164
+ {/* Right side: plugin extras + plugin right actions + host toolbar */}
165
+ <div className="flex items-center gap-1">
166
+ {model.toolbarExtras}
167
+ {rightActions.map(action => (
168
+ <ToolbarActionButton key={action.id} action={action} />
169
+ ))}
170
+ {hostToolbar}
171
+ </div>
172
+ </div>
173
+ )}
174
+
175
+ {/* Viewer content wrapped in Suspense for lazy-loaded plugins */}
176
+ <div className="flex-1 min-h-0">
177
+ <Suspense fallback={<ViewerLoadingFallback name={plugin.name} />}>
178
+ <Component {...viewerProps} />
179
+ </Suspense>
180
+ </div>
181
+ </div>
182
+ );
183
+ });
184
+
185
+ ViewerHost.displayName = 'ViewerHost';
186
+
187
+ // ---- Keyboard shortcut matching ----
188
+
189
+ /**
190
+ * Match a keyboard event against a shortcut key string.
191
+ * Supports modifiers: ctrl, shift, alt, meta
192
+ * Examples: 'ctrl+s', 'escape', 'ctrl+shift+p', 'pagedown'
193
+ */
194
+ function matchShortcut(e: KeyboardEvent, shortcutKey: string): boolean {
195
+ const parts = shortcutKey.toLowerCase().split('+');
196
+ const key = parts[parts.length - 1]!;
197
+ const needCtrl = parts.includes('ctrl');
198
+ const needShift = parts.includes('shift');
199
+ const needAlt = parts.includes('alt');
200
+ const needMeta = parts.includes('meta');
201
+
202
+ if (e.ctrlKey !== needCtrl) return false;
203
+ if (e.shiftKey !== needShift) return false;
204
+ if (e.altKey !== needAlt) return false;
205
+ if (e.metaKey !== needMeta) return false;
206
+
207
+ return e.key.toLowerCase() === key;
208
+ }