@anymux/ui-kit 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (244) hide show
  1. package/dist/ExplorerLayout-CSIJd7N4.js +105 -0
  2. package/dist/ExplorerLayout-CSIJd7N4.js.map +1 -0
  3. package/dist/FileBrowserContext-B6jixa2j.js +11 -0
  4. package/dist/FileBrowserContext-B6jixa2j.js.map +1 -0
  5. package/dist/calendar-DSlrbHoj.js +761 -0
  6. package/dist/calendar-DSlrbHoj.js.map +1 -0
  7. package/dist/calendar.d.ts +3 -0
  8. package/dist/calendar.js +3 -0
  9. package/dist/contacts-DQXTZzHc.js +539 -0
  10. package/dist/contacts-DQXTZzHc.js.map +1 -0
  11. package/dist/contacts.d.ts +3 -0
  12. package/dist/contacts.js +3 -0
  13. package/dist/file-browser-m5atC3kF.js +6755 -0
  14. package/dist/file-browser-m5atC3kF.js.map +1 -0
  15. package/dist/file-browser.d.ts +11 -0
  16. package/dist/file-browser.js +9 -0
  17. package/dist/git-B55e6LL-.js +561 -0
  18. package/dist/git-B55e6LL-.js.map +1 -0
  19. package/dist/git.d.ts +2 -0
  20. package/dist/git.js +3 -0
  21. package/dist/iconMap-V4B8P-Uh.js +206 -0
  22. package/dist/iconMap-V4B8P-Uh.js.map +1 -0
  23. package/dist/icons-CIsIOZXR.js +0 -0
  24. package/dist/icons.d.ts +2 -0
  25. package/dist/icons.js +4 -0
  26. package/dist/index-BNmNIWBL.d.ts +71 -0
  27. package/dist/index-BNmNIWBL.d.ts.map +1 -0
  28. package/dist/index-Bryv_GCG.d.ts +1481 -0
  29. package/dist/index-Bryv_GCG.d.ts.map +1 -0
  30. package/dist/index-CuQIjSXs.d.ts +134 -0
  31. package/dist/index-CuQIjSXs.d.ts.map +1 -0
  32. package/dist/index-DSu19mq0.d.ts +153 -0
  33. package/dist/index-DSu19mq0.d.ts.map +1 -0
  34. package/dist/index-DmsyeHFr.d.ts +149 -0
  35. package/dist/index-DmsyeHFr.d.ts.map +1 -0
  36. package/dist/index-DxnJ8FYM.d.ts +17 -0
  37. package/dist/index-DxnJ8FYM.d.ts.map +1 -0
  38. package/dist/index-DzfY1Tok.d.ts +32 -0
  39. package/dist/index-DzfY1Tok.d.ts.map +1 -0
  40. package/dist/index-Ml_SgiKa.d.ts +1847 -0
  41. package/dist/index-Ml_SgiKa.d.ts.map +1 -0
  42. package/dist/index-kHr9udZD.d.ts +1025 -0
  43. package/dist/index-kHr9udZD.d.ts.map +1 -0
  44. package/dist/index.d.ts +11 -0
  45. package/dist/index.js +15 -0
  46. package/dist/layout-Ca_4r8ka.js +89 -0
  47. package/dist/layout-Ca_4r8ka.js.map +1 -0
  48. package/dist/layout.d.ts +2 -0
  49. package/dist/layout.js +5 -0
  50. package/dist/list-CxfT6hix.js +6831 -0
  51. package/dist/list-CxfT6hix.js.map +1 -0
  52. package/dist/list.d.ts +2 -0
  53. package/dist/list.js +5 -0
  54. package/dist/media-DZ292aKK.js +557 -0
  55. package/dist/media-DZ292aKK.js.map +1 -0
  56. package/dist/media.d.ts +3 -0
  57. package/dist/media.js +3 -0
  58. package/dist/tree-Dd9Z0Aso.js +3351 -0
  59. package/dist/tree-Dd9Z0Aso.js.map +1 -0
  60. package/dist/tree.d.ts +2 -0
  61. package/dist/tree.js +6 -0
  62. package/dist/types-common-CB3kRek8.d.ts +26 -0
  63. package/dist/types-common-CB3kRek8.d.ts.map +1 -0
  64. package/dist/utils-B4fdKKsy.js +3 -0
  65. package/package.json +109 -0
  66. package/src/calendar/AgendaView.tsx +37 -0
  67. package/src/calendar/CalendarBrowser.tsx +90 -0
  68. package/src/calendar/CalendarModel.ts +142 -0
  69. package/src/calendar/CalendarSidebar.tsx +81 -0
  70. package/src/calendar/DayView.tsx +76 -0
  71. package/src/calendar/EventCard.tsx +51 -0
  72. package/src/calendar/MockCalendarProvider.ts +98 -0
  73. package/src/calendar/MonthView.tsx +77 -0
  74. package/src/calendar/WeekView.tsx +129 -0
  75. package/src/calendar/index.ts +18 -0
  76. package/src/calendar/types.ts +25 -0
  77. package/src/contacts/ContactAvatar.tsx +35 -0
  78. package/src/contacts/ContactBrowser.tsx +56 -0
  79. package/src/contacts/ContactCard.tsx +37 -0
  80. package/src/contacts/ContactDetail.tsx +63 -0
  81. package/src/contacts/ContactGroupSidebar.tsx +40 -0
  82. package/src/contacts/ContactList.tsx +32 -0
  83. package/src/contacts/ContactListModel.ts +120 -0
  84. package/src/contacts/MockContactProvider.ts +77 -0
  85. package/src/contacts/index.ts +17 -0
  86. package/src/contacts/types.ts +26 -0
  87. package/src/demos/CalendarBrowserDemo.tsx +15 -0
  88. package/src/demos/ContactBrowserDemo.tsx +15 -0
  89. package/src/demos/MediaBrowserDemo.tsx +15 -0
  90. package/src/file-browser/adapters/DocumentViewerAdapter.ts +371 -0
  91. package/src/file-browser/adapters/FileSystemBridge.ts +168 -0
  92. package/src/file-browser/adapters/GitBrowserAdapter.ts +546 -0
  93. package/src/file-browser/adapters/README.md +504 -0
  94. package/src/file-browser/adapters/index.ts +27 -0
  95. package/src/file-browser/adapters/types.ts +70 -0
  96. package/src/file-browser/architecture.md +645 -0
  97. package/src/file-browser/components/CreateItemDialog.tsx +71 -0
  98. package/src/file-browser/components/DeleteConfirmDialog.tsx +58 -0
  99. package/src/file-browser/components/FileBrowser.tsx +473 -0
  100. package/src/file-browser/components/FileBrowserContent.tsx +209 -0
  101. package/src/file-browser/components/FileBrowserHeader.tsx +151 -0
  102. package/src/file-browser/components/FileBrowserToolbar.tsx +145 -0
  103. package/src/file-browser/components/LeftPanel/LeftPanel.tsx +103 -0
  104. package/src/file-browser/components/LeftPanel/LeftPanelTabs.tsx +70 -0
  105. package/src/file-browser/components/LeftPanel/TreeNavigationView.tsx +256 -0
  106. package/src/file-browser/components/PreviewPane.tsx +146 -0
  107. package/src/file-browser/components/RightPanel/FilePreview.tsx +219 -0
  108. package/src/file-browser/components/RightPanel/RightPanel.tsx +186 -0
  109. package/src/file-browser/components/RightPanel/RightPanelToolbar.tsx +113 -0
  110. package/src/file-browser/components/UploadProgress.tsx +123 -0
  111. package/src/file-browser/components/ViewerHost.tsx +208 -0
  112. package/src/file-browser/components/mobile/MobileNavigation.tsx +227 -0
  113. package/src/file-browser/components/navigation/NavigationButtons.tsx +171 -0
  114. package/src/file-browser/components/shared/ErrorBoundary.tsx +116 -0
  115. package/src/file-browser/components/shared/FileBrowserItem.tsx +195 -0
  116. package/src/file-browser/components/shared/FileIcon.tsx +169 -0
  117. package/src/file-browser/components/toolbar/ViewModeToggle.tsx +200 -0
  118. package/src/file-browser/components/views/ListView/ListView.tsx +484 -0
  119. package/src/file-browser/components/views/ThumbnailView/ThumbnailView.tsx +323 -0
  120. package/src/file-browser/components/views/TreeView/TreeNode.tsx +186 -0
  121. package/src/file-browser/components/views/TreeView/TreeNodeList.tsx +191 -0
  122. package/src/file-browser/components/views/TreeView/TreeView.tsx +200 -0
  123. package/src/file-browser/components/views/TreemapView/TreemapView.tsx +339 -0
  124. package/src/file-browser/context/FileBrowserContext.tsx +13 -0
  125. package/src/file-browser/examples/BasicUsage.tsx +20 -0
  126. package/src/file-browser/index.ts +98 -0
  127. package/src/file-browser/models/FileBrowserModel.ts +623 -0
  128. package/src/file-browser/models/LeftPanelManagerModel.ts +105 -0
  129. package/src/file-browser/models/NavigationManagerModel.ts +312 -0
  130. package/src/file-browser/models/ResponsiveLayoutManagerModel.ts +437 -0
  131. package/src/file-browser/models/RightPanelManagerModel.ts +190 -0
  132. package/src/file-browser/models/SelectionManagerModel.ts +252 -0
  133. package/src/file-browser/models/ToolbarManagerModel.ts +144 -0
  134. package/src/file-browser/models/UploadModel.ts +147 -0
  135. package/src/file-browser/models/ViewModeManagerModel.ts +185 -0
  136. package/src/file-browser/models/ViewerHostModel.ts +44 -0
  137. package/src/file-browser/models/ui/ListViewUIModel.ts +265 -0
  138. package/src/file-browser/models/ui/PreviewUIModel.ts +297 -0
  139. package/src/file-browser/models/ui/ThumbnailViewUIModel.ts +254 -0
  140. package/src/file-browser/models/ui/TreeViewUIModel.ts +128 -0
  141. package/src/file-browser/models/ui/TreemapViewUIModel.ts +350 -0
  142. package/src/file-browser/providers/FileSystemListProvider.ts +552 -0
  143. package/src/file-browser/providers/FileSystemProvider.ts +401 -0
  144. package/src/file-browser/providers/FileSystemTreeProvider.ts +231 -0
  145. package/src/file-browser/providers/GitProvider.ts +337 -0
  146. package/src/file-browser/providers/GitRepositoryProvider.ts +376 -0
  147. package/src/file-browser/providers/IFileBrowserProvider.ts +56 -0
  148. package/src/file-browser/providers/MemoryProvider.ts +303 -0
  149. package/src/file-browser/providers/index.ts +4 -0
  150. package/src/file-browser/registry/ViewerRegistry.ts +551 -0
  151. package/src/file-browser/registry/types.ts +144 -0
  152. package/src/file-browser/scripts/performanceBenchmark.ts +553 -0
  153. package/src/file-browser/services/ThumbnailCacheService.ts +128 -0
  154. package/src/file-browser/tasks.md +537 -0
  155. package/src/file-browser/types/FileBrowserTypes.ts +126 -0
  156. package/src/file-browser/types/ProviderTypes.ts +155 -0
  157. package/src/file-browser/types/UITypes.ts +235 -0
  158. package/src/file-browser/types/ViewModeTypes.ts +150 -0
  159. package/src/file-browser/utils/gestures.ts +327 -0
  160. package/src/file-browser/utils/performance.ts +563 -0
  161. package/src/file-browser/viewers/ImageViewer.tsx +163 -0
  162. package/src/file-browser/viewers/ImageViewerModel.ts +79 -0
  163. package/src/file-browser/viewers/TextViewer.tsx +95 -0
  164. package/src/file-browser/viewers/UnsupportedFileViewer.tsx +57 -0
  165. package/src/file-browser/viewers/index.ts +61 -0
  166. package/src/git/BranchList.tsx +128 -0
  167. package/src/git/CommitGraph.tsx +239 -0
  168. package/src/git/CommitList.tsx +258 -0
  169. package/src/git/DiffViewer.tsx +219 -0
  170. package/src/git/index.ts +4 -0
  171. package/src/icons/iconMap.ts +146 -0
  172. package/src/icons/index.ts +9 -0
  173. package/src/index.ts +13 -0
  174. package/src/layout/README.md +307 -0
  175. package/src/layout/components/ExplorerLayout/ExplorerLayout.tsx +178 -0
  176. package/src/layout/examples/SimpleExample.tsx +60 -0
  177. package/src/layout/index.ts +6 -0
  178. package/src/lib/utils.ts +1 -0
  179. package/src/list/README.md +303 -0
  180. package/src/list/architecture.md +807 -0
  181. package/src/list/components/CalculatedGridView.tsx +252 -0
  182. package/src/list/components/DragPreview.tsx +102 -0
  183. package/src/list/components/ListContextMenu.tsx +274 -0
  184. package/src/list/components/ListItem.tsx +761 -0
  185. package/src/list/components/ListItems.tsx +919 -0
  186. package/src/list/components/MasonryView.tsx +241 -0
  187. package/src/list/components/SearchFilter.tsx +44 -0
  188. package/src/list/components/TreemapView.tsx +709 -0
  189. package/src/list/components/ViewSizeControls.tsx +205 -0
  190. package/src/list/components/ViewTypeSelector.tsx +312 -0
  191. package/src/list/components/VirtualizedDetailsView.tsx +231 -0
  192. package/src/list/components/VirtualizedGrid.tsx +164 -0
  193. package/src/list/components/VirtualizedList.tsx +154 -0
  194. package/src/list/components/VirtualizedMasonryView.tsx +344 -0
  195. package/src/list/components/shared/EmptyState.tsx +103 -0
  196. package/src/list/components/shared/ErrorBoundary.tsx +123 -0
  197. package/src/list/components/shared/ErrorDisplay.tsx +100 -0
  198. package/src/list/components/shared/ListLoader.tsx +146 -0
  199. package/src/list/components/shared/LoadingIndicator.tsx +80 -0
  200. package/src/list/index.ts +92 -0
  201. package/src/list/models/ListItemsModel.ts +1301 -0
  202. package/src/list/models/TreemapModel.ts +204 -0
  203. package/src/list/providers/ListItemsProvider.ts +313 -0
  204. package/src/list/providers/TestListProvider.ts +604 -0
  205. package/src/list/tasks.md +937 -0
  206. package/src/list/types/ListTypes.ts +178 -0
  207. package/src/list/utils/BenchmarkLogger.ts +243 -0
  208. package/src/list/utils/DragDropManager.ts +320 -0
  209. package/src/list/utils/GridLayoutCalculator.ts +290 -0
  210. package/src/list/utils/ListAccessibility.ts +367 -0
  211. package/src/list/utils/ListKeyboard.ts +414 -0
  212. package/src/list/utils/MasonryLayoutCalculator.ts +302 -0
  213. package/src/list/utils/MasonryLayoutEngine.ts +401 -0
  214. package/src/list/utils/__tests__/MasonryLayoutEngine.test.ts +157 -0
  215. package/src/list/utils/__tests__/VirtualizedMasonryView.test.tsx +251 -0
  216. package/src/media/AlbumSidebar.tsx +48 -0
  217. package/src/media/MediaBrowser.tsx +92 -0
  218. package/src/media/MediaBrowserModel.ts +138 -0
  219. package/src/media/MediaGrid.tsx +50 -0
  220. package/src/media/MediaList.tsx +49 -0
  221. package/src/media/MediaPreview.tsx +63 -0
  222. package/src/media/MediaTimeline.tsx +38 -0
  223. package/src/media/MockMediaProvider.ts +70 -0
  224. package/src/media/index.ts +18 -0
  225. package/src/media/types.ts +21 -0
  226. package/src/styles/variables.css +60 -0
  227. package/src/tree/DEVELOPMENT_SUMMARY.md +170 -0
  228. package/src/tree/__tests__/TreeModel.test.ts +16 -0
  229. package/src/tree/architecture.md +530 -0
  230. package/src/tree/components/Tree.tsx +283 -0
  231. package/src/tree/components/TreeCheckbox.tsx +147 -0
  232. package/src/tree/components/TreeContextMenu.tsx +139 -0
  233. package/src/tree/components/TreeNodeList.tsx +329 -0
  234. package/src/tree/components/TreeTable.tsx +382 -0
  235. package/src/tree/index.ts +58 -0
  236. package/src/tree/models/TreeModel.ts +839 -0
  237. package/src/tree/providers/SimpleTreeProvider.ts +463 -0
  238. package/src/tree/providers/TestTreeProvider.ts +946 -0
  239. package/src/tree/providers/TreeProvider.ts +308 -0
  240. package/src/tree/tasks.md +2046 -0
  241. package/src/tree/types/TreeTypes.ts +279 -0
  242. package/src/tree/utils/SelectionTheme.ts +150 -0
  243. package/src/tree/utils/logger.ts +203 -0
  244. package/src/types-common.ts +21 -0
@@ -0,0 +1,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
+ };