@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,551 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type {
|
|
3
|
+
FileViewerPlugin,
|
|
4
|
+
FileMatchContext,
|
|
5
|
+
ResolvedViewer,
|
|
6
|
+
} from './types';
|
|
7
|
+
|
|
8
|
+
// Re-export types from types.ts for convenience
|
|
9
|
+
export type { FileViewerPlugin, FileMatchContext, ResolvedViewer, ToolbarAction, KeyboardShortcut } from './types';
|
|
10
|
+
|
|
11
|
+
// ---- Legacy interfaces (kept for backward compatibility) ----
|
|
12
|
+
|
|
13
|
+
export interface ViewerConfig {
|
|
14
|
+
id: string;
|
|
15
|
+
name: string;
|
|
16
|
+
description?: string;
|
|
17
|
+
extensions: string[];
|
|
18
|
+
component: React.ComponentType<ViewerProps>;
|
|
19
|
+
priority?: number;
|
|
20
|
+
canEdit?: boolean;
|
|
21
|
+
isDefault?: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface ViewerProps {
|
|
25
|
+
file: {
|
|
26
|
+
path: string;
|
|
27
|
+
name: string;
|
|
28
|
+
content?: string | ArrayBuffer;
|
|
29
|
+
size?: number;
|
|
30
|
+
mimeType?: string;
|
|
31
|
+
};
|
|
32
|
+
onClose?: () => void;
|
|
33
|
+
onSave?: (content: string | ArrayBuffer) => Promise<void>;
|
|
34
|
+
className?: string;
|
|
35
|
+
readOnly?: boolean;
|
|
36
|
+
/** Callback for viewer to register toolbar controls rendered in parent toolbar */
|
|
37
|
+
onToolbarExtras?: (extras: React.ReactNode) => void;
|
|
38
|
+
|
|
39
|
+
// ---- NEW: Edit lifecycle ----
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Called by the plugin when the content becomes dirty (modified).
|
|
43
|
+
* The host uses this to show unsaved-changes indicators and
|
|
44
|
+
* to prompt before closing.
|
|
45
|
+
*/
|
|
46
|
+
onDirtyChange?: (isDirty: boolean) => void;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Whether to render in compact/preview mode (inside the preview pane)
|
|
50
|
+
* vs full mode (inside the main viewer area).
|
|
51
|
+
* Plugins can adjust their layout based on this.
|
|
52
|
+
*/
|
|
53
|
+
mode?: 'preview' | 'full';
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Content fetcher for streaming/on-demand loading.
|
|
57
|
+
* Plugins that handle large files (PDF, video) can use this
|
|
58
|
+
* instead of receiving the full content upfront.
|
|
59
|
+
*/
|
|
60
|
+
fetchContent?: () => Promise<string | ArrayBuffer>;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* URL for the file if it can be accessed directly (e.g., S3 presigned URL).
|
|
64
|
+
* Useful for media players that need a URL, not a buffer.
|
|
65
|
+
*/
|
|
66
|
+
contentUrl?: string;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ---- Match type bonus constants ----
|
|
70
|
+
|
|
71
|
+
const MATCH_BONUS_EXTENSION = 80;
|
|
72
|
+
const MATCH_BONUS_MIME = 70;
|
|
73
|
+
const MATCH_BONUS_PATTERN = 60;
|
|
74
|
+
|
|
75
|
+
// ---- Adapter: wrap legacy ViewerConfig into FileViewerPlugin ----
|
|
76
|
+
|
|
77
|
+
export function viewerConfigToPlugin(config: ViewerConfig): FileViewerPlugin {
|
|
78
|
+
const plugin: FileViewerPlugin = {
|
|
79
|
+
id: config.id,
|
|
80
|
+
name: config.name,
|
|
81
|
+
extensions: config.extensions.map(ext => ext.toLowerCase().replace(/^\./, '')),
|
|
82
|
+
capabilities: {
|
|
83
|
+
canPreview: true,
|
|
84
|
+
canFullscreen: true,
|
|
85
|
+
},
|
|
86
|
+
priority: config.priority ?? 0,
|
|
87
|
+
component: config.component,
|
|
88
|
+
};
|
|
89
|
+
if (config.description !== undefined) plugin.description = config.description;
|
|
90
|
+
if (config.canEdit !== undefined) plugin.capabilities.canEdit = config.canEdit;
|
|
91
|
+
if (config.isDefault !== undefined) plugin.isDefault = config.isDefault;
|
|
92
|
+
return plugin;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ---- Adapter: convert FileViewerPlugin back to ViewerConfig (for backward-compat getters) ----
|
|
96
|
+
|
|
97
|
+
function pluginToViewerConfig(plugin: FileViewerPlugin): ViewerConfig {
|
|
98
|
+
const config: ViewerConfig = {
|
|
99
|
+
id: plugin.id,
|
|
100
|
+
name: plugin.name,
|
|
101
|
+
extensions: plugin.extensions ?? [],
|
|
102
|
+
component: resolveComponent(plugin),
|
|
103
|
+
priority: plugin.priority ?? 0,
|
|
104
|
+
};
|
|
105
|
+
if (plugin.description !== undefined) config.description = plugin.description;
|
|
106
|
+
if (plugin.capabilities.canEdit !== undefined) config.canEdit = plugin.capabilities.canEdit;
|
|
107
|
+
if (plugin.isDefault !== undefined) config.isDefault = plugin.isDefault;
|
|
108
|
+
return config;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ---- Lazy component cache ----
|
|
112
|
+
|
|
113
|
+
const lazyCache = new Map<string, React.LazyExoticComponent<React.ComponentType<ViewerProps>>>();
|
|
114
|
+
|
|
115
|
+
function resolveComponent(plugin: FileViewerPlugin): React.ComponentType<ViewerProps> {
|
|
116
|
+
if (plugin.component) {
|
|
117
|
+
return plugin.component;
|
|
118
|
+
}
|
|
119
|
+
if (plugin.load) {
|
|
120
|
+
if (!lazyCache.has(plugin.id)) {
|
|
121
|
+
lazyCache.set(plugin.id, React.lazy(plugin.load));
|
|
122
|
+
}
|
|
123
|
+
return lazyCache.get(plugin.id)!;
|
|
124
|
+
}
|
|
125
|
+
throw new Error(`Plugin "${plugin.id}" has no component or loader`);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ---- Simple glob matching for filePatterns ----
|
|
129
|
+
|
|
130
|
+
function matchesGlob(filename: string, pattern: string): boolean {
|
|
131
|
+
// Convert simple glob pattern to regex
|
|
132
|
+
// Supports: * (any chars), ? (single char), and literal strings
|
|
133
|
+
const escaped = pattern
|
|
134
|
+
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
|
135
|
+
.replace(/\*/g, '.*')
|
|
136
|
+
.replace(/\?/g, '.');
|
|
137
|
+
const regex = new RegExp(`^${escaped}$`, 'i');
|
|
138
|
+
return regex.test(filename);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// ---- ViewerRegistry class ----
|
|
142
|
+
|
|
143
|
+
export class ViewerRegistry {
|
|
144
|
+
// ---- Legacy storage (kept for backward compat) ----
|
|
145
|
+
private viewers = new Map<string, ViewerConfig>();
|
|
146
|
+
private extensionMap = new Map<string, ViewerConfig[]>();
|
|
147
|
+
|
|
148
|
+
// ---- New plugin storage ----
|
|
149
|
+
private plugins = new Map<string, FileViewerPlugin>();
|
|
150
|
+
private extensionIndex = new Map<string, FileViewerPlugin[]>();
|
|
151
|
+
private mimeTypeIndex = new Map<string, FileViewerPlugin[]>();
|
|
152
|
+
private patternPlugins: FileViewerPlugin[] = [];
|
|
153
|
+
|
|
154
|
+
// ==============================
|
|
155
|
+
// NEW: Plugin API
|
|
156
|
+
// ==============================
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Register a plugin. Replaces any existing plugin with the same ID.
|
|
160
|
+
*/
|
|
161
|
+
registerPlugin(plugin: FileViewerPlugin): void {
|
|
162
|
+
if (!plugin.component && !plugin.load) {
|
|
163
|
+
throw new Error(`Plugin "${plugin.id}" must provide either component or load()`);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Remove existing plugin with same ID (if any)
|
|
167
|
+
this.unregisterPlugin(plugin.id);
|
|
168
|
+
|
|
169
|
+
this.plugins.set(plugin.id, plugin);
|
|
170
|
+
this.rebuildIndexes();
|
|
171
|
+
|
|
172
|
+
// Also register in legacy storage for backward compat
|
|
173
|
+
this.syncPluginToLegacy(plugin);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Unregister a plugin by ID.
|
|
178
|
+
*/
|
|
179
|
+
unregisterPlugin(pluginId: string): void {
|
|
180
|
+
if (!this.plugins.has(pluginId)) return;
|
|
181
|
+
|
|
182
|
+
this.plugins.delete(pluginId);
|
|
183
|
+
this.rebuildIndexes();
|
|
184
|
+
|
|
185
|
+
// Also remove from legacy maps
|
|
186
|
+
this.removeLegacy(pluginId);
|
|
187
|
+
|
|
188
|
+
// Clean lazy cache
|
|
189
|
+
lazyCache.delete(pluginId);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Find the best viewer for a given file.
|
|
194
|
+
*
|
|
195
|
+
* Matching order with scoring:
|
|
196
|
+
* 1. Extension match (bonus: 80)
|
|
197
|
+
* 2. MIME type match (bonus: 70)
|
|
198
|
+
* 3. Filename pattern match (bonus: 60)
|
|
199
|
+
* 4. canHandle() callback (bonus: canHandle return value 0-100)
|
|
200
|
+
* 5. Default plugin (isDefault: true, bonus: 0)
|
|
201
|
+
*
|
|
202
|
+
* Final score = matchTypeBonus + plugin.priority + canHandle score (if applicable)
|
|
203
|
+
*/
|
|
204
|
+
resolveViewer(
|
|
205
|
+
file: FileMatchContext,
|
|
206
|
+
options?: { preferEditable?: boolean }
|
|
207
|
+
): ResolvedViewer | null {
|
|
208
|
+
const candidates = this.findCandidates(file);
|
|
209
|
+
|
|
210
|
+
if (options?.preferEditable) {
|
|
211
|
+
const editable = candidates.filter(c => c.plugin.capabilities.canEdit);
|
|
212
|
+
if (editable.length > 0) return editable[0]!;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return candidates[0] ?? null;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Find ALL viewers that can handle a file, sorted by score (descending).
|
|
220
|
+
* Used for "Open with..." menu.
|
|
221
|
+
*/
|
|
222
|
+
resolveAllViewers(file: FileMatchContext): ResolvedViewer[] {
|
|
223
|
+
return this.findCandidates(file);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Get a plugin by ID.
|
|
228
|
+
*/
|
|
229
|
+
getPluginById(pluginId: string): FileViewerPlugin | null {
|
|
230
|
+
return this.plugins.get(pluginId) ?? null;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Get all registered plugins.
|
|
235
|
+
*/
|
|
236
|
+
getAllPlugins(): FileViewerPlugin[] {
|
|
237
|
+
return Array.from(this.plugins.values()).sort((a, b) => a.name.localeCompare(b.name));
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Build a FileMatchContext from a filename (convenience helper).
|
|
242
|
+
*/
|
|
243
|
+
buildFileMatchContext(
|
|
244
|
+
filename: string,
|
|
245
|
+
opts?: { path?: string; mimeType?: string; size?: number }
|
|
246
|
+
): FileMatchContext {
|
|
247
|
+
const ctx: FileMatchContext = {
|
|
248
|
+
name: filename,
|
|
249
|
+
path: opts?.path ?? filename,
|
|
250
|
+
extension: this.extractExtension(filename),
|
|
251
|
+
};
|
|
252
|
+
if (opts?.mimeType !== undefined) ctx.mimeType = opts.mimeType;
|
|
253
|
+
if (opts?.size !== undefined) ctx.size = opts.size;
|
|
254
|
+
return ctx;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// ==============================
|
|
258
|
+
// LEGACY API (backward compat)
|
|
259
|
+
// ==============================
|
|
260
|
+
|
|
261
|
+
registerViewer(config: ViewerConfig): void {
|
|
262
|
+
// Register via plugin API, which also syncs to legacy
|
|
263
|
+
this.registerPlugin(viewerConfigToPlugin(config));
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
unregisterViewer(viewerId: string): void {
|
|
267
|
+
this.unregisterPlugin(viewerId);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
getViewer(filename: string, preferEditable = false): ViewerConfig | null {
|
|
271
|
+
const extension = this.extractExtension(filename);
|
|
272
|
+
const viewers = this.extensionMap.get(extension) || [];
|
|
273
|
+
|
|
274
|
+
if (viewers.length === 0) {
|
|
275
|
+
return this.getDefaultViewer();
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
let candidates = viewers;
|
|
279
|
+
if (preferEditable) {
|
|
280
|
+
const editableViewers = viewers.filter(v => v.canEdit);
|
|
281
|
+
if (editableViewers.length > 0) {
|
|
282
|
+
candidates = editableViewers;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return candidates[0] || null;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
getViewers(filename: string): ViewerConfig[] {
|
|
290
|
+
const extension = this.extractExtension(filename);
|
|
291
|
+
return [...(this.extensionMap.get(extension) || [])];
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
canHandle(filename: string): boolean {
|
|
295
|
+
const extension = this.extractExtension(filename);
|
|
296
|
+
if (this.extensionMap.has(extension)) return true;
|
|
297
|
+
|
|
298
|
+
// Also check plugin pattern/canHandle matching
|
|
299
|
+
const ctx = this.buildFileMatchContext(filename);
|
|
300
|
+
for (const plugin of this.patternPlugins) {
|
|
301
|
+
if (plugin.filePatterns) {
|
|
302
|
+
for (const pattern of plugin.filePatterns) {
|
|
303
|
+
if (matchesGlob(filename, pattern)) return true;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
if (plugin.canHandle) {
|
|
307
|
+
if (plugin.canHandle(ctx) > 0) return true;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return false;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
getAllViewers(): ViewerConfig[] {
|
|
315
|
+
return Array.from(this.viewers.values()).sort((a, b) => a.name.localeCompare(b.name));
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
getViewerById(id: string): ViewerConfig | null {
|
|
319
|
+
return this.viewers.get(id) || null;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
getFileCategory(filename: string): string {
|
|
323
|
+
const extension = this.extractExtension(filename);
|
|
324
|
+
|
|
325
|
+
const imageExts = ['jpg', 'jpeg', 'png', 'gif', 'svg', 'webp', 'bmp', 'ico'];
|
|
326
|
+
const videoExts = ['mp4', 'avi', 'mov', 'wmv', 'flv', 'webm', 'mkv'];
|
|
327
|
+
const audioExts = ['mp3', 'wav', 'flac', 'aac', 'ogg', 'wma', 'm4a'];
|
|
328
|
+
const codeExts = ['js', 'jsx', 'ts', 'tsx', 'py', 'java', 'cpp', 'c', 'cs', 'php', 'rb', 'go', 'rs', 'swift', 'kt', 'html', 'css', 'scss'];
|
|
329
|
+
const textExts = ['txt', 'md', 'mdx', 'rst', 'rtf'];
|
|
330
|
+
const documentExts = ['pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx'];
|
|
331
|
+
const archiveExts = ['zip', 'rar', '7z', 'tar', 'gz', 'bz2'];
|
|
332
|
+
|
|
333
|
+
if (imageExts.includes(extension)) return 'image';
|
|
334
|
+
if (videoExts.includes(extension)) return 'video';
|
|
335
|
+
if (audioExts.includes(extension)) return 'audio';
|
|
336
|
+
if (codeExts.includes(extension)) return 'code';
|
|
337
|
+
if (textExts.includes(extension)) return 'text';
|
|
338
|
+
if (documentExts.includes(extension)) return 'document';
|
|
339
|
+
if (archiveExts.includes(extension)) return 'archive';
|
|
340
|
+
|
|
341
|
+
return 'file';
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// ==============================
|
|
345
|
+
// INTERNAL
|
|
346
|
+
// ==============================
|
|
347
|
+
|
|
348
|
+
private extractExtension(filename: string): string {
|
|
349
|
+
const lastDot = filename.lastIndexOf('.');
|
|
350
|
+
if (lastDot === -1 || lastDot === filename.length - 1) {
|
|
351
|
+
return '';
|
|
352
|
+
}
|
|
353
|
+
return filename.slice(lastDot + 1).toLowerCase();
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
private getDefaultViewer(): ViewerConfig | null {
|
|
357
|
+
for (const viewer of this.viewers.values()) {
|
|
358
|
+
if (viewer.isDefault) {
|
|
359
|
+
return viewer;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
return null;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Find all candidate viewers for a file, scored and sorted descending.
|
|
367
|
+
*/
|
|
368
|
+
private findCandidates(file: FileMatchContext): ResolvedViewer[] {
|
|
369
|
+
const scored: Array<{ plugin: FileViewerPlugin; score: number }> = [];
|
|
370
|
+
|
|
371
|
+
// 1. Extension match
|
|
372
|
+
if (file.extension) {
|
|
373
|
+
const byExt = this.extensionIndex.get(file.extension) || [];
|
|
374
|
+
for (const plugin of byExt) {
|
|
375
|
+
let score = MATCH_BONUS_EXTENSION + (plugin.priority ?? 0);
|
|
376
|
+
if (plugin.canHandle) {
|
|
377
|
+
const canHandleScore = plugin.canHandle(file);
|
|
378
|
+
if (canHandleScore <= 0) continue; // canHandle rejected
|
|
379
|
+
score += canHandleScore;
|
|
380
|
+
}
|
|
381
|
+
scored.push({ plugin, score });
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// 2. MIME type match
|
|
386
|
+
if (file.mimeType) {
|
|
387
|
+
const byMime = this.mimeTypeIndex.get(file.mimeType) || [];
|
|
388
|
+
for (const plugin of byMime) {
|
|
389
|
+
// Skip if already scored by extension
|
|
390
|
+
if (scored.some(s => s.plugin.id === plugin.id)) continue;
|
|
391
|
+
|
|
392
|
+
let score = MATCH_BONUS_MIME + (plugin.priority ?? 0);
|
|
393
|
+
if (plugin.canHandle) {
|
|
394
|
+
const canHandleScore = plugin.canHandle(file);
|
|
395
|
+
if (canHandleScore <= 0) continue;
|
|
396
|
+
score += canHandleScore;
|
|
397
|
+
}
|
|
398
|
+
scored.push({ plugin, score });
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// 3. Pattern match + canHandle-only plugins
|
|
403
|
+
for (const plugin of this.patternPlugins) {
|
|
404
|
+
// Skip if already scored
|
|
405
|
+
if (scored.some(s => s.plugin.id === plugin.id)) continue;
|
|
406
|
+
|
|
407
|
+
let matched = false;
|
|
408
|
+
let matchBonus = 0;
|
|
409
|
+
|
|
410
|
+
// Check filePatterns
|
|
411
|
+
if (plugin.filePatterns) {
|
|
412
|
+
for (const pattern of plugin.filePatterns) {
|
|
413
|
+
if (matchesGlob(file.name, pattern)) {
|
|
414
|
+
matched = true;
|
|
415
|
+
matchBonus = MATCH_BONUS_PATTERN;
|
|
416
|
+
break;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// Check canHandle (for plugins with only canHandle, or in addition to pattern match)
|
|
422
|
+
if (plugin.canHandle) {
|
|
423
|
+
const canHandleScore = plugin.canHandle(file);
|
|
424
|
+
if (canHandleScore > 0) {
|
|
425
|
+
if (!matched) {
|
|
426
|
+
// canHandle-only match: score IS the canHandle return value
|
|
427
|
+
matched = true;
|
|
428
|
+
matchBonus = canHandleScore;
|
|
429
|
+
} else {
|
|
430
|
+
// Pattern + canHandle: add canHandle score
|
|
431
|
+
matchBonus += canHandleScore;
|
|
432
|
+
}
|
|
433
|
+
} else if (!matched) {
|
|
434
|
+
continue;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
if (matched) {
|
|
439
|
+
scored.push({ plugin, score: matchBonus + (plugin.priority ?? 0) });
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// 4. If nothing matched, try default plugin
|
|
444
|
+
if (scored.length === 0) {
|
|
445
|
+
for (const plugin of this.plugins.values()) {
|
|
446
|
+
if (plugin.isDefault) {
|
|
447
|
+
scored.push({ plugin, score: 0 });
|
|
448
|
+
break;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// Sort by score descending, break ties by priority descending
|
|
454
|
+
scored.sort((a, b) => {
|
|
455
|
+
if (b.score !== a.score) return b.score - a.score;
|
|
456
|
+
return (b.plugin.priority ?? 0) - (a.plugin.priority ?? 0);
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
// Resolve to ResolvedViewer
|
|
460
|
+
return scored.map(({ plugin, score }) => ({
|
|
461
|
+
plugin,
|
|
462
|
+
Component: resolveComponent(plugin),
|
|
463
|
+
confidence: score,
|
|
464
|
+
}));
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* Rebuild plugin indexes from the plugin map.
|
|
469
|
+
*/
|
|
470
|
+
private rebuildIndexes(): void {
|
|
471
|
+
this.extensionIndex.clear();
|
|
472
|
+
this.mimeTypeIndex.clear();
|
|
473
|
+
this.patternPlugins = [];
|
|
474
|
+
|
|
475
|
+
for (const plugin of this.plugins.values()) {
|
|
476
|
+
// Index by extension
|
|
477
|
+
if (plugin.extensions) {
|
|
478
|
+
for (const ext of plugin.extensions) {
|
|
479
|
+
const normalized = ext.toLowerCase().replace(/^\./, '');
|
|
480
|
+
const list = this.extensionIndex.get(normalized) || [];
|
|
481
|
+
list.push(plugin);
|
|
482
|
+
this.extensionIndex.set(normalized, list);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// Index by MIME type
|
|
487
|
+
if (plugin.mimeTypes) {
|
|
488
|
+
for (const mime of plugin.mimeTypes) {
|
|
489
|
+
const list = this.mimeTypeIndex.get(mime) || [];
|
|
490
|
+
list.push(plugin);
|
|
491
|
+
this.mimeTypeIndex.set(mime, list);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// Track plugins with patterns or canHandle
|
|
496
|
+
if (plugin.filePatterns || plugin.canHandle) {
|
|
497
|
+
this.patternPlugins.push(plugin);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* Sync a plugin into the legacy viewers/extensionMap storage.
|
|
504
|
+
*/
|
|
505
|
+
private syncPluginToLegacy(plugin: FileViewerPlugin): void {
|
|
506
|
+
const config = pluginToViewerConfig(plugin);
|
|
507
|
+
this.viewers.set(config.id, config);
|
|
508
|
+
|
|
509
|
+
if (config.extensions) {
|
|
510
|
+
config.extensions.forEach(ext => {
|
|
511
|
+
const normalizedExt = ext.toLowerCase().replace(/^\./, '');
|
|
512
|
+
const existingViewers = this.extensionMap.get(normalizedExt) || [];
|
|
513
|
+
|
|
514
|
+
const insertIndex = existingViewers.findIndex(
|
|
515
|
+
v => (v.priority || 0) < (config.priority || 0)
|
|
516
|
+
);
|
|
517
|
+
if (insertIndex === -1) {
|
|
518
|
+
existingViewers.push(config);
|
|
519
|
+
} else {
|
|
520
|
+
existingViewers.splice(insertIndex, 0, config);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
this.extensionMap.set(normalizedExt, existingViewers);
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/**
|
|
529
|
+
* Remove a plugin from legacy storage.
|
|
530
|
+
*/
|
|
531
|
+
private removeLegacy(pluginId: string): void {
|
|
532
|
+
const config = this.viewers.get(pluginId);
|
|
533
|
+
if (!config) return;
|
|
534
|
+
|
|
535
|
+
this.viewers.delete(pluginId);
|
|
536
|
+
|
|
537
|
+
config.extensions.forEach(ext => {
|
|
538
|
+
const normalizedExt = ext.toLowerCase().replace(/^\./, '');
|
|
539
|
+
const viewers = this.extensionMap.get(normalizedExt) || [];
|
|
540
|
+
const filtered = viewers.filter(v => v.id !== pluginId);
|
|
541
|
+
|
|
542
|
+
if (filtered.length === 0) {
|
|
543
|
+
this.extensionMap.delete(normalizedExt);
|
|
544
|
+
} else {
|
|
545
|
+
this.extensionMap.set(normalizedExt, filtered);
|
|
546
|
+
}
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
export const globalViewerRegistry = new ViewerRegistry();
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { ViewerProps } from './ViewerRegistry';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Plugin interface for file viewers.
|
|
6
|
+
* Extends the simple ViewerConfig with richer matching, lazy loading, and toolbar support.
|
|
7
|
+
*/
|
|
8
|
+
export interface FileViewerPlugin {
|
|
9
|
+
/** Unique plugin ID, e.g., 'pdf-viewer', 'monaco-editor' */
|
|
10
|
+
id: string;
|
|
11
|
+
|
|
12
|
+
/** Human-readable name shown in "Open with..." menu */
|
|
13
|
+
name: string;
|
|
14
|
+
|
|
15
|
+
/** Optional description for UI tooltips */
|
|
16
|
+
description?: string;
|
|
17
|
+
|
|
18
|
+
/** Plugin capabilities */
|
|
19
|
+
capabilities: {
|
|
20
|
+
/** Can this plugin edit files (enables save flow)? */
|
|
21
|
+
canEdit?: boolean;
|
|
22
|
+
/** Can this plugin render in the inline preview pane (smaller viewport)? */
|
|
23
|
+
canPreview?: boolean;
|
|
24
|
+
/** Can this plugin render full-screen (the main viewer mode)? */
|
|
25
|
+
canFullscreen?: boolean;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
// ---- MATCHING ----
|
|
29
|
+
|
|
30
|
+
/** File extensions this plugin handles (without dots), e.g., ['pdf', 'epub'] */
|
|
31
|
+
extensions?: string[];
|
|
32
|
+
|
|
33
|
+
/** MIME types this plugin handles, e.g., ['application/pdf'] */
|
|
34
|
+
mimeTypes?: string[];
|
|
35
|
+
|
|
36
|
+
/** Glob patterns for filenames, e.g., ['Dockerfile', 'Makefile', '.env*'] */
|
|
37
|
+
filePatterns?: string[];
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Advanced match function for content-sniffing or complex logic.
|
|
41
|
+
* Called only if extension/MIME/pattern matching passes or if no
|
|
42
|
+
* static matchers are defined.
|
|
43
|
+
* Return a confidence score 0-100 (0 = cannot handle, 100 = perfect match).
|
|
44
|
+
*/
|
|
45
|
+
canHandle?: (file: FileMatchContext) => number;
|
|
46
|
+
|
|
47
|
+
// ---- PRIORITY ----
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Priority (higher = preferred). Default: 0.
|
|
51
|
+
* Built-in viewers use 10-20. Third-party plugins should use 50+
|
|
52
|
+
* to override built-in viewers, or negative to serve as fallbacks.
|
|
53
|
+
*/
|
|
54
|
+
priority?: number;
|
|
55
|
+
|
|
56
|
+
// ---- COMPONENT LOADING ----
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* The viewer React component. Can be a concrete component or a lazy loader.
|
|
60
|
+
* When undefined, `load()` must be provided.
|
|
61
|
+
*/
|
|
62
|
+
component?: React.ComponentType<ViewerProps>;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Dynamic loader that returns the viewer component.
|
|
66
|
+
* Used for code-splitting. The registry wraps this in React.lazy().
|
|
67
|
+
*/
|
|
68
|
+
load?: () => Promise<{ default: React.ComponentType<ViewerProps> }>;
|
|
69
|
+
|
|
70
|
+
// ---- DEFAULT ----
|
|
71
|
+
|
|
72
|
+
/** If true, this plugin is used as the fallback for unrecognized file types */
|
|
73
|
+
isDefault?: boolean;
|
|
74
|
+
|
|
75
|
+
// ---- TOOLBAR EXTENSION ----
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Declarative toolbar actions rendered by the ViewerHost.
|
|
79
|
+
* For dynamic toolbar items, plugins should use the `onToolbarExtras`
|
|
80
|
+
* callback on ViewerProps (already supported).
|
|
81
|
+
*/
|
|
82
|
+
toolbarActions?: ToolbarAction[];
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Keyboard shortcuts the plugin handles.
|
|
86
|
+
* The ViewerHost binds these when the plugin is active.
|
|
87
|
+
*/
|
|
88
|
+
keyboardShortcuts?: KeyboardShortcut[];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Context passed to canHandle() and resolveViewer() for file matching */
|
|
92
|
+
export interface FileMatchContext {
|
|
93
|
+
/** Filename including extension */
|
|
94
|
+
name: string;
|
|
95
|
+
/** Full path */
|
|
96
|
+
path: string;
|
|
97
|
+
/** File extension without dot, lowercase */
|
|
98
|
+
extension: string;
|
|
99
|
+
/** MIME type if known */
|
|
100
|
+
mimeType?: string;
|
|
101
|
+
/** File size in bytes */
|
|
102
|
+
size?: number;
|
|
103
|
+
/**
|
|
104
|
+
* First bytes of content (for magic-number sniffing).
|
|
105
|
+
* Only available if the registry is configured to pre-read headers.
|
|
106
|
+
* Typically 512 bytes.
|
|
107
|
+
*/
|
|
108
|
+
headerBytes?: ArrayBuffer;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Declarative toolbar action for ViewerHost */
|
|
112
|
+
export interface ToolbarAction {
|
|
113
|
+
id: string;
|
|
114
|
+
label: string;
|
|
115
|
+
icon?: React.ComponentType<{ className?: string }>;
|
|
116
|
+
/** Called when the action is triggered */
|
|
117
|
+
onClick: () => void;
|
|
118
|
+
/** Whether the action is currently enabled */
|
|
119
|
+
enabled?: boolean | (() => boolean);
|
|
120
|
+
/** Keyboard shortcut label for tooltip, e.g., "Ctrl+S" */
|
|
121
|
+
shortcutLabel?: string;
|
|
122
|
+
/** Position: 'left' | 'right'. Default: 'right' */
|
|
123
|
+
position?: 'left' | 'right';
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Keyboard shortcut binding for ViewerHost */
|
|
127
|
+
export interface KeyboardShortcut {
|
|
128
|
+
/** Key combination, e.g., 'ctrl+s', 'escape', 'ctrl+shift+p' */
|
|
129
|
+
key: string;
|
|
130
|
+
/** Handler */
|
|
131
|
+
handler: () => void;
|
|
132
|
+
/** Only active when the viewer has focus? Default: true */
|
|
133
|
+
requireFocus?: boolean;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Result of resolving a viewer for a file */
|
|
137
|
+
export interface ResolvedViewer {
|
|
138
|
+
/** The matched plugin */
|
|
139
|
+
plugin: FileViewerPlugin;
|
|
140
|
+
/** The React component to render (resolved from component or load via React.lazy) */
|
|
141
|
+
Component: React.ComponentType<ViewerProps>;
|
|
142
|
+
/** Match confidence score. Extension match = 80, MIME = 70, pattern = 60, canHandle = dynamic */
|
|
143
|
+
confidence: number;
|
|
144
|
+
}
|