@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,163 @@
|
|
|
1
|
+
import React, { useState, useEffect } from 'react';
|
|
2
|
+
import { observer } from 'mobx-react-lite';
|
|
3
|
+
import { ImageIcon, ZoomIn, ZoomOut, Maximize } from 'lucide-react';
|
|
4
|
+
import { useScaler } from '@panscale/react';
|
|
5
|
+
import { cn } from '../../lib/utils';
|
|
6
|
+
import { ViewerProps } from '../registry/ViewerRegistry';
|
|
7
|
+
import { ImageViewerModel } from './ImageViewerModel';
|
|
8
|
+
|
|
9
|
+
const MIME_MAP: Record<string, string> = {
|
|
10
|
+
'png': 'image/png', 'jpg': 'image/jpeg', 'jpeg': 'image/jpeg',
|
|
11
|
+
'gif': 'image/gif', 'webp': 'image/webp', 'svg': 'image/svg+xml',
|
|
12
|
+
'bmp': 'image/bmp', 'ico': 'image/x-icon', 'avif': 'image/avif',
|
|
13
|
+
'tiff': 'image/tiff', 'tif': 'image/tiff',
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
// Convert ArrayBuffer to base64 data URL — no lifecycle management needed (unlike blob URLs)
|
|
17
|
+
function arrayBufferToDataUrl(buffer: ArrayBuffer, mime: string): string {
|
|
18
|
+
const bytes = new Uint8Array(buffer);
|
|
19
|
+
let binary = '';
|
|
20
|
+
for (let i = 0; i < bytes.byteLength; i++) {
|
|
21
|
+
binary += String.fromCharCode(bytes[i]!);
|
|
22
|
+
}
|
|
23
|
+
return `data:${mime};base64,${btoa(binary)}`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function getImageUrl(content: string | ArrayBuffer, name: string, mimeType?: string): string | null {
|
|
27
|
+
if (content instanceof ArrayBuffer) {
|
|
28
|
+
const ext = name.split('.').pop()?.toLowerCase() || 'png';
|
|
29
|
+
const mime = MIME_MAP[ext] || mimeType || 'image/png';
|
|
30
|
+
return arrayBufferToDataUrl(content, mime);
|
|
31
|
+
}
|
|
32
|
+
if (typeof content === 'string') {
|
|
33
|
+
if (content.startsWith('data:') || content.startsWith('http')) {
|
|
34
|
+
return content;
|
|
35
|
+
}
|
|
36
|
+
if (name.endsWith('.svg')) {
|
|
37
|
+
return `data:image/svg+xml,${encodeURIComponent(content)}`;
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
const ext = name.split('.').pop()?.toLowerCase() || 'png';
|
|
41
|
+
const mime = MIME_MAP[ext] || mimeType || 'image/png';
|
|
42
|
+
return `data:${mime};base64,${btoa(content)}`;
|
|
43
|
+
} catch {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export const ImageViewer: React.FC<ViewerProps> = observer(({
|
|
51
|
+
file,
|
|
52
|
+
onClose,
|
|
53
|
+
onToolbarExtras,
|
|
54
|
+
className,
|
|
55
|
+
}) => {
|
|
56
|
+
const imageUrl = file.content ? getImageUrl(file.content, file.name, file.mimeType) : null;
|
|
57
|
+
const [model] = useState(() => new ImageViewerModel());
|
|
58
|
+
const scaler = useScaler({ zooming: true, bouncing: true });
|
|
59
|
+
|
|
60
|
+
// Wire scaler into model and handle file changes
|
|
61
|
+
model.setScaler(scaler);
|
|
62
|
+
model.onFileChange(file.path);
|
|
63
|
+
|
|
64
|
+
const zoomPercent = model.zoomPercent;
|
|
65
|
+
|
|
66
|
+
// Register zoom controls in parent toolbar
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
if (!onToolbarExtras || !imageUrl || model.imageError) {
|
|
69
|
+
onToolbarExtras?.(null);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
onToolbarExtras(
|
|
73
|
+
<div className="flex items-center gap-0.5">
|
|
74
|
+
<button
|
|
75
|
+
onClick={() => model.zoomOut()}
|
|
76
|
+
className="p-1 rounded hover:bg-muted text-muted-foreground hover:text-foreground transition-colors"
|
|
77
|
+
title="Zoom out"
|
|
78
|
+
>
|
|
79
|
+
<ZoomOut className="w-3.5 h-3.5" />
|
|
80
|
+
</button>
|
|
81
|
+
<span className="text-muted-foreground text-xs min-w-[2.5rem] text-center tabular-nums">
|
|
82
|
+
{zoomPercent}%
|
|
83
|
+
</span>
|
|
84
|
+
<button
|
|
85
|
+
onClick={() => model.zoomIn()}
|
|
86
|
+
className="p-1 rounded hover:bg-muted text-muted-foreground hover:text-foreground transition-colors"
|
|
87
|
+
title="Zoom in"
|
|
88
|
+
>
|
|
89
|
+
<ZoomIn className="w-3.5 h-3.5" />
|
|
90
|
+
</button>
|
|
91
|
+
<button
|
|
92
|
+
onClick={() => model.resetZoom()}
|
|
93
|
+
className="p-1 rounded hover:bg-muted text-muted-foreground hover:text-foreground transition-colors"
|
|
94
|
+
title="Fit to screen"
|
|
95
|
+
>
|
|
96
|
+
<Maximize className="w-3.5 h-3.5" />
|
|
97
|
+
</button>
|
|
98
|
+
</div>
|
|
99
|
+
);
|
|
100
|
+
}, [onToolbarExtras, imageUrl, model.imageError, zoomPercent, model]);
|
|
101
|
+
|
|
102
|
+
return (
|
|
103
|
+
<div className={cn("h-full flex flex-col", className)}>
|
|
104
|
+
{/* Pan/zoom image area — full height, no separate toolbar */}
|
|
105
|
+
<div
|
|
106
|
+
className="flex-1 min-h-0 relative"
|
|
107
|
+
style={{ backgroundColor: '#111' }}
|
|
108
|
+
>
|
|
109
|
+
{model.imageError || !imageUrl ? (
|
|
110
|
+
<div className="flex flex-col items-center justify-center h-full gap-3 text-white/60">
|
|
111
|
+
<ImageIcon className="w-16 h-16" />
|
|
112
|
+
<p className="text-sm">{model.imageError ? 'Failed to load image' : 'Preview not available'}</p>
|
|
113
|
+
<p className="text-xs text-white/40">{file.name}</p>
|
|
114
|
+
</div>
|
|
115
|
+
) : (
|
|
116
|
+
<>
|
|
117
|
+
{!model.isLoaded && (
|
|
118
|
+
<div className="absolute inset-0 flex flex-col items-center justify-center gap-2 text-white/60 z-10">
|
|
119
|
+
<div className="animate-spin w-6 h-6 border-2 border-white/40 border-t-white rounded-full" />
|
|
120
|
+
<p className="text-sm">Loading image...</p>
|
|
121
|
+
</div>
|
|
122
|
+
)}
|
|
123
|
+
{/* Panscale scaler container */}
|
|
124
|
+
<div
|
|
125
|
+
ref={scaler.ref}
|
|
126
|
+
style={{
|
|
127
|
+
width: '100%',
|
|
128
|
+
height: '100%',
|
|
129
|
+
overflow: 'hidden',
|
|
130
|
+
position: 'relative',
|
|
131
|
+
touchAction: 'none',
|
|
132
|
+
}}
|
|
133
|
+
>
|
|
134
|
+
<div
|
|
135
|
+
style={{
|
|
136
|
+
transform: `translate(${scaler.values.translateX}px, ${scaler.values.translateY}px) scale(${scaler.values.zoom})`,
|
|
137
|
+
transformOrigin: '0 0',
|
|
138
|
+
willChange: 'transform',
|
|
139
|
+
}}
|
|
140
|
+
>
|
|
141
|
+
<img
|
|
142
|
+
src={imageUrl}
|
|
143
|
+
alt={file.name}
|
|
144
|
+
className="select-none"
|
|
145
|
+
style={{
|
|
146
|
+
display: 'block',
|
|
147
|
+
maxWidth: 'none',
|
|
148
|
+
opacity: model.isLoaded ? 1 : 0,
|
|
149
|
+
}}
|
|
150
|
+
onLoad={(e) => model.handleImageLoad(e.currentTarget)}
|
|
151
|
+
onError={() => model.handleImageError()}
|
|
152
|
+
draggable={false}
|
|
153
|
+
/>
|
|
154
|
+
</div>
|
|
155
|
+
</div>
|
|
156
|
+
</>
|
|
157
|
+
)}
|
|
158
|
+
</div>
|
|
159
|
+
</div>
|
|
160
|
+
);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
ImageViewer.displayName = 'ImageViewer';
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { makeAutoObservable } from 'mobx';
|
|
2
|
+
|
|
3
|
+
type Scaler = {
|
|
4
|
+
ref: React.RefObject<HTMLDivElement | null>;
|
|
5
|
+
values: { zoom: number; translateX: number; translateY: number };
|
|
6
|
+
setZoomBounds: (min: number, max: number) => void;
|
|
7
|
+
setContentSize: (width: number, height: number) => void;
|
|
8
|
+
fitToContent: (animate?: boolean) => void;
|
|
9
|
+
zoomBy: (factor: number, animate?: boolean, originX?: number, originY?: number) => void;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export class ImageViewerModel {
|
|
13
|
+
imageError = false;
|
|
14
|
+
isLoaded = false;
|
|
15
|
+
currentFilePath = '';
|
|
16
|
+
private scaler: Scaler | null = null;
|
|
17
|
+
|
|
18
|
+
constructor() {
|
|
19
|
+
makeAutoObservable(this, { scaler: false });
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
setScaler(scaler: Scaler): void {
|
|
23
|
+
this.scaler = scaler;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
get zoomPercent(): number {
|
|
27
|
+
return Math.round((this.scaler?.values.zoom ?? 1) * 100);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Reset state when navigating to a different file */
|
|
31
|
+
onFileChange(filePath: string): void {
|
|
32
|
+
if (this.currentFilePath !== filePath) {
|
|
33
|
+
this.currentFilePath = filePath;
|
|
34
|
+
this.isLoaded = false;
|
|
35
|
+
this.imageError = false;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
handleImageLoad(img: HTMLImageElement): void {
|
|
40
|
+
if (!this.scaler) return;
|
|
41
|
+
this.scaler.setZoomBounds(0.01, 100);
|
|
42
|
+
requestAnimationFrame(() => {
|
|
43
|
+
this.scaler!.setContentSize(img.naturalWidth, img.naturalHeight);
|
|
44
|
+
this.scaler!.fitToContent(false);
|
|
45
|
+
this.isLoaded = true;
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
handleImageError(): void {
|
|
50
|
+
this.imageError = true;
|
|
51
|
+
this.isLoaded = true;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
zoomIn(): void {
|
|
55
|
+
if (!this.scaler) return;
|
|
56
|
+
const el = this.scaler.ref.current;
|
|
57
|
+
if (el) {
|
|
58
|
+
const r = el.getBoundingClientRect();
|
|
59
|
+
this.scaler.zoomBy(1.3, true, r.width / 2, r.height / 2);
|
|
60
|
+
} else {
|
|
61
|
+
this.scaler.zoomBy(1.3, true);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
zoomOut(): void {
|
|
66
|
+
if (!this.scaler) return;
|
|
67
|
+
const el = this.scaler.ref.current;
|
|
68
|
+
if (el) {
|
|
69
|
+
const r = el.getBoundingClientRect();
|
|
70
|
+
this.scaler.zoomBy(0.77, true, r.width / 2, r.height / 2);
|
|
71
|
+
} else {
|
|
72
|
+
this.scaler.zoomBy(0.77, true);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
resetZoom(): void {
|
|
77
|
+
this.scaler?.fitToContent(true);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import React, { useState, useEffect } from 'react';
|
|
2
|
+
import { observer } from 'mobx-react-lite';
|
|
3
|
+
import { FileText } from 'lucide-react';
|
|
4
|
+
import { cn } from '../../lib/utils';
|
|
5
|
+
import { ViewerProps } from '../registry/ViewerRegistry';
|
|
6
|
+
|
|
7
|
+
export const TextViewer: React.FC<ViewerProps> = observer(({
|
|
8
|
+
file,
|
|
9
|
+
onClose,
|
|
10
|
+
className,
|
|
11
|
+
}) => {
|
|
12
|
+
const [content, setContent] = useState<string>('');
|
|
13
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
14
|
+
const [error, setError] = useState<string | null>(null);
|
|
15
|
+
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
const loadContent = async () => {
|
|
18
|
+
try {
|
|
19
|
+
setIsLoading(true);
|
|
20
|
+
setError(null);
|
|
21
|
+
|
|
22
|
+
if (file.content) {
|
|
23
|
+
if (typeof file.content === 'string') {
|
|
24
|
+
setContent(file.content);
|
|
25
|
+
} else {
|
|
26
|
+
const decoder = new TextDecoder('utf-8');
|
|
27
|
+
setContent(decoder.decode(file.content));
|
|
28
|
+
}
|
|
29
|
+
} else {
|
|
30
|
+
setContent(`Sample content for ${file.name}\n\nPath: ${file.path}`);
|
|
31
|
+
}
|
|
32
|
+
} catch (err) {
|
|
33
|
+
setError(`Failed to load file: ${err instanceof Error ? err.message : 'Unknown error'}`);
|
|
34
|
+
} finally {
|
|
35
|
+
setIsLoading(false);
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
loadContent();
|
|
40
|
+
}, [file]);
|
|
41
|
+
|
|
42
|
+
if (isLoading) {
|
|
43
|
+
return (
|
|
44
|
+
<div className={cn("h-full flex flex-col", className)}>
|
|
45
|
+
<div className="flex items-center gap-2 px-4 py-3 border-b bg-muted/30 flex-shrink-0">
|
|
46
|
+
<FileText className="w-4 h-4 text-muted-foreground" />
|
|
47
|
+
<span className="text-sm font-medium">Loading {file.name}</span>
|
|
48
|
+
</div>
|
|
49
|
+
<div className="flex-1 flex items-center justify-center">
|
|
50
|
+
<span className="text-sm text-muted-foreground">Loading file content...</span>
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (error) {
|
|
57
|
+
return (
|
|
58
|
+
<div className={cn("h-full flex flex-col", className)}>
|
|
59
|
+
<div className="flex items-center gap-2 px-4 py-3 border-b bg-muted/30 flex-shrink-0">
|
|
60
|
+
<FileText className="w-4 h-4 text-destructive" />
|
|
61
|
+
<span className="text-sm font-medium text-destructive">Error loading {file.name}</span>
|
|
62
|
+
</div>
|
|
63
|
+
<div className="flex-1 flex items-center justify-center">
|
|
64
|
+
<div className="text-center">
|
|
65
|
+
<p className="text-sm font-medium text-destructive">Failed to load file</p>
|
|
66
|
+
<p className="text-xs text-muted-foreground mt-1">{error}</p>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<div className={cn("h-full flex flex-col", className)}>
|
|
75
|
+
<div className="flex items-center justify-between px-4 py-3 border-b bg-muted/30 flex-shrink-0">
|
|
76
|
+
<div className="flex items-center gap-2">
|
|
77
|
+
<FileText className="w-4 h-4 text-muted-foreground" />
|
|
78
|
+
<span className="text-sm font-medium">{file.name}</span>
|
|
79
|
+
{file.size != null && (
|
|
80
|
+
<span className="text-xs text-muted-foreground">
|
|
81
|
+
({(file.size / 1024).toFixed(1)} KB)
|
|
82
|
+
</span>
|
|
83
|
+
)}
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
<div className="flex-1 min-h-0 overflow-auto p-4">
|
|
87
|
+
<pre className="font-mono text-sm leading-relaxed whitespace-pre-wrap break-words m-0 p-4 bg-muted/50 rounded-md border">
|
|
88
|
+
{content}
|
|
89
|
+
</pre>
|
|
90
|
+
</div>
|
|
91
|
+
</div>
|
|
92
|
+
);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
TextViewer.displayName = 'TextViewer';
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import React, { useCallback } from 'react';
|
|
2
|
+
import { FileQuestion, Download } from 'lucide-react';
|
|
3
|
+
import { Button } from '@anymux/ui/components/button';
|
|
4
|
+
import { cn } from '../../lib/utils';
|
|
5
|
+
import type { ViewerProps } from '../registry/ViewerRegistry';
|
|
6
|
+
|
|
7
|
+
export const UnsupportedFileViewer: React.FC<ViewerProps> = ({
|
|
8
|
+
file,
|
|
9
|
+
onClose,
|
|
10
|
+
className,
|
|
11
|
+
}) => {
|
|
12
|
+
const extension = file.name.split('.').pop()?.toLowerCase() || '';
|
|
13
|
+
|
|
14
|
+
const handleDownload = useCallback(() => {
|
|
15
|
+
if (!file.content) return;
|
|
16
|
+
|
|
17
|
+
const blob = file.content instanceof ArrayBuffer
|
|
18
|
+
? new Blob([file.content])
|
|
19
|
+
: new Blob([file.content], { type: 'text/plain' });
|
|
20
|
+
|
|
21
|
+
const url = URL.createObjectURL(blob);
|
|
22
|
+
const a = document.createElement('a');
|
|
23
|
+
a.href = url;
|
|
24
|
+
a.download = file.name;
|
|
25
|
+
a.click();
|
|
26
|
+
URL.revokeObjectURL(url);
|
|
27
|
+
}, [file.content, file.name]);
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<div className={cn("h-full flex items-center justify-center bg-muted/10", className)}>
|
|
31
|
+
<div className="flex flex-col items-center gap-4 text-center px-6">
|
|
32
|
+
<FileQuestion className="w-16 h-16 text-muted-foreground/60" />
|
|
33
|
+
<div>
|
|
34
|
+
<p className="text-sm font-medium">{file.name}</p>
|
|
35
|
+
<p className="text-sm text-muted-foreground mt-1">
|
|
36
|
+
Preview not available{extension ? ` for .${extension} files` : ''}
|
|
37
|
+
</p>
|
|
38
|
+
</div>
|
|
39
|
+
<div className="flex items-center gap-2">
|
|
40
|
+
{file.content && (
|
|
41
|
+
<Button variant="outline" size="sm" onClick={handleDownload}>
|
|
42
|
+
<Download className="w-4 h-4 mr-1.5" />
|
|
43
|
+
Download
|
|
44
|
+
</Button>
|
|
45
|
+
)}
|
|
46
|
+
{onClose && (
|
|
47
|
+
<Button variant="ghost" size="sm" onClick={onClose}>
|
|
48
|
+
Close
|
|
49
|
+
</Button>
|
|
50
|
+
)}
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
);
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
UnsupportedFileViewer.displayName = 'UnsupportedFileViewer';
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { globalViewerRegistry } from '../registry/ViewerRegistry';
|
|
2
|
+
import { TextViewer } from './TextViewer';
|
|
3
|
+
import { ImageViewer } from './ImageViewer';
|
|
4
|
+
|
|
5
|
+
// Export viewers
|
|
6
|
+
export { TextViewer } from './TextViewer';
|
|
7
|
+
export { ImageViewer } from './ImageViewer';
|
|
8
|
+
export { UnsupportedFileViewer } from './UnsupportedFileViewer';
|
|
9
|
+
export { globalViewerRegistry, type ViewerConfig, type ViewerProps } from '../registry/ViewerRegistry';
|
|
10
|
+
|
|
11
|
+
// Also re-export new plugin types
|
|
12
|
+
export type { FileViewerPlugin, FileMatchContext, ResolvedViewer, ToolbarAction, KeyboardShortcut } from '../registry/types';
|
|
13
|
+
|
|
14
|
+
// Register default viewers for common file types using the new plugin API
|
|
15
|
+
export function registerDefaultViewers() {
|
|
16
|
+
// Text files viewer (built-in, priority 10, default fallback)
|
|
17
|
+
globalViewerRegistry.registerPlugin({
|
|
18
|
+
id: 'text-viewer',
|
|
19
|
+
name: 'Text Viewer',
|
|
20
|
+
description: 'View text files, code, and configuration files',
|
|
21
|
+
extensions: [
|
|
22
|
+
'txt', 'md', 'mdx', 'rst', 'rtf',
|
|
23
|
+
'js', 'jsx', 'ts', 'tsx', 'json', 'jsonc',
|
|
24
|
+
'html', 'htm', 'css', 'scss', 'sass', 'less',
|
|
25
|
+
'xml', 'svg', 'yaml', 'yml', 'toml',
|
|
26
|
+
'ini', 'conf', 'config', 'env',
|
|
27
|
+
'py', 'java', 'cpp', 'c', 'cs', 'php', 'rb', 'go', 'rs',
|
|
28
|
+
'swift', 'kt', 'scala', 'sh', 'bash', 'zsh', 'fish',
|
|
29
|
+
'log', 'gitignore', 'dockerignore', 'editorconfig',
|
|
30
|
+
],
|
|
31
|
+
capabilities: {
|
|
32
|
+
canEdit: false,
|
|
33
|
+
canPreview: true,
|
|
34
|
+
canFullscreen: true,
|
|
35
|
+
},
|
|
36
|
+
component: TextViewer,
|
|
37
|
+
priority: 10,
|
|
38
|
+
isDefault: true,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// Image files viewer (built-in, priority 20)
|
|
42
|
+
globalViewerRegistry.registerPlugin({
|
|
43
|
+
id: 'image-viewer',
|
|
44
|
+
name: 'Image Viewer',
|
|
45
|
+
description: 'View image files including photos and graphics',
|
|
46
|
+
extensions: [
|
|
47
|
+
'png', 'jpg', 'jpeg', 'gif', 'svg', 'webp',
|
|
48
|
+
'bmp', 'ico', 'tiff', 'tif', 'avif',
|
|
49
|
+
],
|
|
50
|
+
capabilities: {
|
|
51
|
+
canEdit: false,
|
|
52
|
+
canPreview: true,
|
|
53
|
+
canFullscreen: true,
|
|
54
|
+
},
|
|
55
|
+
component: ImageViewer,
|
|
56
|
+
priority: 20,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Auto-register default viewers when module is imported
|
|
61
|
+
registerDefaultViewers();
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import React, { useState, useMemo } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
GitBranch as GitBranchIcon,
|
|
4
|
+
Search,
|
|
5
|
+
Shield,
|
|
6
|
+
Check,
|
|
7
|
+
} from 'lucide-react';
|
|
8
|
+
import type { GitBranch } from '@anymux/file-system';
|
|
9
|
+
|
|
10
|
+
// ---- Types ----
|
|
11
|
+
|
|
12
|
+
export interface BranchListProps {
|
|
13
|
+
branches: GitBranch[];
|
|
14
|
+
currentBranch?: string;
|
|
15
|
+
onSelectBranch?: (branch: GitBranch) => void;
|
|
16
|
+
className?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// ---- BranchList Component ----
|
|
20
|
+
|
|
21
|
+
export const BranchList: React.FC<BranchListProps> = ({
|
|
22
|
+
branches,
|
|
23
|
+
currentBranch,
|
|
24
|
+
onSelectBranch,
|
|
25
|
+
className,
|
|
26
|
+
}) => {
|
|
27
|
+
const [search, setSearch] = useState('');
|
|
28
|
+
|
|
29
|
+
const filtered = useMemo(() => {
|
|
30
|
+
if (!search) return branches;
|
|
31
|
+
const lower = search.toLowerCase();
|
|
32
|
+
return branches.filter((b) => b.name.toLowerCase().includes(lower));
|
|
33
|
+
}, [branches, search]);
|
|
34
|
+
|
|
35
|
+
// Sort: default branch first, then current, then alphabetical
|
|
36
|
+
const sorted = useMemo(() => {
|
|
37
|
+
return [...filtered].sort((a, b) => {
|
|
38
|
+
if (a.isDefault && !b.isDefault) return -1;
|
|
39
|
+
if (!a.isDefault && b.isDefault) return 1;
|
|
40
|
+
if (a.name === currentBranch && b.name !== currentBranch) return -1;
|
|
41
|
+
if (a.name !== currentBranch && b.name === currentBranch) return 1;
|
|
42
|
+
return a.name.localeCompare(b.name);
|
|
43
|
+
});
|
|
44
|
+
}, [filtered, currentBranch]);
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<div className={`flex flex-col h-full ${className ?? ''}`}>
|
|
48
|
+
{/* Search input */}
|
|
49
|
+
<div className="px-2 py-2 border-b border-gray-200 dark:border-gray-700 flex-shrink-0">
|
|
50
|
+
<div className="relative">
|
|
51
|
+
<Search className="absolute left-2 top-1/2 -translate-y-1/2 h-3 w-3 text-gray-400" />
|
|
52
|
+
<input
|
|
53
|
+
type="text"
|
|
54
|
+
placeholder="Filter branches..."
|
|
55
|
+
value={search}
|
|
56
|
+
onChange={(e) => setSearch(e.target.value)}
|
|
57
|
+
className="w-full pl-7 pr-2 py-1.5 text-xs rounded-md border border-gray-200 dark:border-gray-600 bg-gray-50 dark:bg-gray-900 focus:outline-none focus:ring-1 focus:ring-blue-500 placeholder-gray-400"
|
|
58
|
+
/>
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
|
|
62
|
+
{/* Branch list */}
|
|
63
|
+
<div className="flex-1 overflow-auto">
|
|
64
|
+
{sorted.length === 0 ? (
|
|
65
|
+
<div className="px-3 py-6 text-center text-xs text-gray-400">
|
|
66
|
+
{search ? 'No matching branches' : 'No branches found'}
|
|
67
|
+
</div>
|
|
68
|
+
) : (
|
|
69
|
+
sorted.map((branch) => {
|
|
70
|
+
const isCurrent = branch.name === currentBranch;
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<button
|
|
74
|
+
key={branch.name}
|
|
75
|
+
onClick={() => onSelectBranch?.(branch)}
|
|
76
|
+
className={`flex items-center gap-2 w-full px-3 py-2 text-left transition-colors border-b border-gray-50 dark:border-gray-800/50 ${
|
|
77
|
+
isCurrent
|
|
78
|
+
? 'bg-blue-50 dark:bg-blue-900/20'
|
|
79
|
+
: 'hover:bg-gray-50 dark:hover:bg-gray-800/50'
|
|
80
|
+
}`}
|
|
81
|
+
>
|
|
82
|
+
{/* Current indicator */}
|
|
83
|
+
<span className="w-4 flex-shrink-0 flex items-center justify-center">
|
|
84
|
+
{isCurrent ? (
|
|
85
|
+
<Check className="h-3.5 w-3.5 text-blue-500" />
|
|
86
|
+
) : (
|
|
87
|
+
<GitBranchIcon className="h-3.5 w-3.5 text-gray-400" />
|
|
88
|
+
)}
|
|
89
|
+
</span>
|
|
90
|
+
|
|
91
|
+
{/* Branch name */}
|
|
92
|
+
<span
|
|
93
|
+
className={`text-xs truncate flex-1 ${
|
|
94
|
+
isCurrent
|
|
95
|
+
? 'font-semibold text-blue-700 dark:text-blue-300'
|
|
96
|
+
: 'text-gray-700 dark:text-gray-300'
|
|
97
|
+
}`}
|
|
98
|
+
>
|
|
99
|
+
{branch.name}
|
|
100
|
+
</span>
|
|
101
|
+
|
|
102
|
+
{/* Badges */}
|
|
103
|
+
<div className="flex items-center gap-1 flex-shrink-0">
|
|
104
|
+
{branch.isDefault && (
|
|
105
|
+
<span className="px-1.5 py-0.5 text-[9px] font-medium bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400 rounded">
|
|
106
|
+
default
|
|
107
|
+
</span>
|
|
108
|
+
)}
|
|
109
|
+
{branch.isProtected && (
|
|
110
|
+
<span className="flex items-center gap-0.5 px-1.5 py-0.5 text-[9px] font-medium bg-orange-100 text-orange-700 dark:bg-orange-900/30 dark:text-orange-400 rounded">
|
|
111
|
+
<Shield className="h-2.5 w-2.5" />
|
|
112
|
+
protected
|
|
113
|
+
</span>
|
|
114
|
+
)}
|
|
115
|
+
</div>
|
|
116
|
+
|
|
117
|
+
{/* Short SHA */}
|
|
118
|
+
<code className="text-[10px] font-mono text-gray-400 dark:text-gray-500 flex-shrink-0">
|
|
119
|
+
{branch.sha.slice(0, 7)}
|
|
120
|
+
</code>
|
|
121
|
+
</button>
|
|
122
|
+
);
|
|
123
|
+
})
|
|
124
|
+
)}
|
|
125
|
+
</div>
|
|
126
|
+
</div>
|
|
127
|
+
);
|
|
128
|
+
};
|