@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,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
|
+
}
|