@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,169 @@
1
+ import React from 'react';
2
+ import {
3
+ File,
4
+ FileText,
5
+ FileImage,
6
+ FileVideo,
7
+ FileAudio,
8
+ FileCode,
9
+ Folder,
10
+ FolderOpen,
11
+ Archive,
12
+ FileSpreadsheet,
13
+ FileType,
14
+ Settings,
15
+ LucideIcon,
16
+ } from 'lucide-react';
17
+
18
+ // Map icon names to actual Lucide components
19
+ const lucideIconMap: Record<string, LucideIcon> = {
20
+ 'file': File,
21
+ 'file-text': FileText,
22
+ 'file-image': FileImage,
23
+ 'file-video': FileVideo,
24
+ 'file-audio': FileAudio,
25
+ 'file-code': FileCode,
26
+ 'folder': Folder,
27
+ 'folder-open': FolderOpen,
28
+ 'archive': Archive,
29
+ 'file-spreadsheet': FileSpreadsheet,
30
+ 'file-type': FileType,
31
+ 'settings': Settings,
32
+ };
33
+ import { cn } from '../../../lib/utils';
34
+ import { FileBrowserItem, IconDefinition } from '../../types/FileBrowserTypes';
35
+
36
+ export interface FileIconProps {
37
+ item: FileBrowserItem;
38
+ getItemIcon?: (item: FileBrowserItem) => IconDefinition;
39
+ size?: 'sm' | 'md' | 'lg' | 'xl';
40
+ className?: string;
41
+ isExpanded?: boolean; // For folders in tree view
42
+ }
43
+
44
+ const sizeClasses = {
45
+ sm: 'w-4 h-4',
46
+ md: 'w-5 h-5',
47
+ lg: 'w-6 h-6',
48
+ xl: 'w-8 h-8',
49
+ };
50
+
51
+ // Default icon mapping based on file types and extensions
52
+ const getDefaultIcon = (item: FileBrowserItem, isExpanded?: boolean): LucideIcon => {
53
+ if (item.type === 'directory') {
54
+ return isExpanded ? FolderOpen : Folder;
55
+ }
56
+
57
+ // Get file extension
58
+ const extension = item.name.split('.').pop()?.toLowerCase();
59
+
60
+ // Code files
61
+ const codeExtensions = ['js', 'jsx', 'ts', 'tsx', 'py', 'java', 'cpp', 'c', 'cs', 'php', 'rb', 'go', 'rs', 'swift', 'kt', 'scala', 'html', 'css', 'scss', 'sass', 'less', 'vue', 'svelte'];
62
+ if (extension && codeExtensions.includes(extension)) {
63
+ return FileCode;
64
+ }
65
+
66
+ // Image files
67
+ const imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'svg', 'webp', 'ico', 'tiff', 'tif'];
68
+ if (extension && imageExtensions.includes(extension)) {
69
+ return FileImage;
70
+ }
71
+
72
+ // Video files
73
+ const videoExtensions = ['mp4', 'avi', 'mov', 'wmv', 'flv', 'webm', 'mkv', '3gp', 'm4v'];
74
+ if (extension && videoExtensions.includes(extension)) {
75
+ return FileVideo;
76
+ }
77
+
78
+ // Audio files
79
+ const audioExtensions = ['mp3', 'wav', 'flac', 'aac', 'ogg', 'wma', 'm4a', 'opus'];
80
+ if (extension && audioExtensions.includes(extension)) {
81
+ return FileAudio;
82
+ }
83
+
84
+ // Archive files
85
+ const archiveExtensions = ['zip', 'rar', '7z', 'tar', 'gz', 'bz2', 'xz', 'tgz'];
86
+ if (extension && archiveExtensions.includes(extension)) {
87
+ return Archive;
88
+ }
89
+
90
+ // Spreadsheet files
91
+ const spreadsheetExtensions = ['xlsx', 'xls', 'csv', 'ods'];
92
+ if (extension && spreadsheetExtensions.includes(extension)) {
93
+ return FileSpreadsheet;
94
+ }
95
+
96
+ // Text files
97
+ const textExtensions = ['txt', 'md', 'mdx', 'rst', 'rtf', 'doc', 'docx', 'pdf'];
98
+ if (extension && textExtensions.includes(extension)) {
99
+ return FileText;
100
+ }
101
+
102
+ // Config files
103
+ const configExtensions = ['json', 'yaml', 'yml', 'toml', 'ini', 'conf', 'config', 'env'];
104
+ if (extension && configExtensions.includes(extension)) {
105
+ return Settings;
106
+ }
107
+
108
+ // Font files
109
+ const fontExtensions = ['ttf', 'otf', 'woff', 'woff2', 'eot'];
110
+ if (extension && fontExtensions.includes(extension)) {
111
+ return FileType;
112
+ }
113
+
114
+ // Default fallback
115
+ return File;
116
+ };
117
+
118
+ const FileIcon: React.FC<FileIconProps> = ({
119
+ item,
120
+ getItemIcon,
121
+ size = 'md',
122
+ className,
123
+ isExpanded = false,
124
+ }) => {
125
+ // Try to get icon from provider first
126
+ let iconDefinition: IconDefinition | undefined;
127
+
128
+ try {
129
+ iconDefinition = getItemIcon?.(item);
130
+ } catch (error) {
131
+ console.warn('Error getting icon from provider:', error);
132
+ }
133
+
134
+ // Determine icon to render
135
+ let IconComponent: LucideIcon;
136
+ let iconColor = '';
137
+
138
+ if (iconDefinition) {
139
+ // Use provider-defined icon
140
+ if (iconDefinition.type === 'lucide' && iconDefinition.name) {
141
+ // Map icon name to actual Lucide component
142
+ const MappedIcon = lucideIconMap[iconDefinition.name];
143
+ IconComponent = MappedIcon || getDefaultIcon(item, isExpanded);
144
+ } else if (iconDefinition.type === 'custom' && iconDefinition.component) {
145
+ // Custom React component (not implemented in this basic version)
146
+ IconComponent = getDefaultIcon(item, isExpanded);
147
+ } else {
148
+ IconComponent = getDefaultIcon(item, isExpanded);
149
+ }
150
+
151
+ iconColor = iconDefinition.color || '';
152
+ } else {
153
+ // Use default icon mapping
154
+ IconComponent = getDefaultIcon(item, isExpanded);
155
+ }
156
+
157
+ return (
158
+ <IconComponent
159
+ className={cn(
160
+ sizeClasses[size],
161
+ iconColor && `text-[${iconColor}]`,
162
+ className
163
+ )}
164
+ aria-hidden="true"
165
+ />
166
+ );
167
+ };
168
+
169
+ export default FileIcon;
@@ -0,0 +1,200 @@
1
+ import React from 'react';
2
+ import { observer } from 'mobx-react-lite';
3
+ import { List, Grid3X3, FileImage } from 'lucide-react';
4
+ import { cn } from '../../../lib/utils';
5
+ import type { ViewModeType } from '../../types/ViewModeTypes';
6
+
7
+ export interface ViewModeToggleProps {
8
+ currentMode: ViewModeType;
9
+ availableModes: ViewModeType[];
10
+ onModeChange?: (mode: ViewModeType) => void;
11
+ className?: string;
12
+ variant?: 'tabs' | 'buttons';
13
+ showLabels?: boolean;
14
+ disabled?: boolean;
15
+ }
16
+
17
+ interface ViewModeOption {
18
+ mode: ViewModeType;
19
+ icon: React.ReactNode;
20
+ label: string;
21
+ description: string;
22
+ }
23
+
24
+ const VIEW_MODE_OPTIONS: Partial<Record<ViewModeType, ViewModeOption>> = {
25
+ tree: {
26
+ mode: 'tree',
27
+ icon: <List className="w-4 h-4" />,
28
+ label: 'Tree',
29
+ description: 'Hierarchical tree view'
30
+ },
31
+ list: {
32
+ mode: 'list',
33
+ icon: <Grid3X3 className="w-4 h-4" />,
34
+ label: 'List',
35
+ description: 'Detailed list view'
36
+ },
37
+ thumbnail: {
38
+ mode: 'thumbnail',
39
+ icon: <FileImage className="w-4 h-4" />,
40
+ label: 'Thumbnails',
41
+ description: 'Thumbnail grid view'
42
+ }
43
+ };
44
+
45
+ const ViewModeToggle: React.FC<ViewModeToggleProps> = observer(({
46
+ currentMode,
47
+ availableModes,
48
+ onModeChange,
49
+ className,
50
+ variant = 'tabs',
51
+ showLabels = true,
52
+ disabled = false,
53
+ }) => {
54
+ const handleModeChange = (mode: ViewModeType) => {
55
+ if (!disabled && mode !== currentMode) {
56
+ onModeChange?.(mode);
57
+ }
58
+ };
59
+
60
+ const handleKeyDown = (e: React.KeyboardEvent, mode: ViewModeType) => {
61
+ if (e.key === 'Enter' || e.key === ' ') {
62
+ e.preventDefault();
63
+ handleModeChange(mode);
64
+ }
65
+
66
+ // Arrow key navigation
67
+ if (e.key === 'ArrowLeft' || e.key === 'ArrowRight') {
68
+ e.preventDefault();
69
+ const currentIndex = availableModes.indexOf(currentMode);
70
+ let nextIndex: number;
71
+
72
+ if (e.key === 'ArrowLeft') {
73
+ nextIndex = currentIndex > 0 ? currentIndex - 1 : availableModes.length - 1;
74
+ } else {
75
+ nextIndex = currentIndex < availableModes.length - 1 ? currentIndex + 1 : 0;
76
+ }
77
+
78
+ const nextMode = availableModes[nextIndex];
79
+ if (nextMode) {
80
+ handleModeChange(nextMode);
81
+ }
82
+ }
83
+ };
84
+
85
+ // Keyboard shortcuts
86
+ React.useEffect(() => {
87
+ const handleGlobalKeyDown = (e: KeyboardEvent) => {
88
+ // Only handle shortcuts when not typing in inputs
89
+ if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) {
90
+ return;
91
+ }
92
+
93
+ // Number keys 1-3 for view modes
94
+ if (e.key >= '1' && e.key <= '3') {
95
+ const index = parseInt(e.key) - 1;
96
+ const mode = availableModes[index];
97
+ if (mode && !disabled) {
98
+ e.preventDefault();
99
+ handleModeChange(mode);
100
+ }
101
+ }
102
+ };
103
+
104
+ window.addEventListener('keydown', handleGlobalKeyDown);
105
+ return () => window.removeEventListener('keydown', handleGlobalKeyDown);
106
+ }, [availableModes, disabled, currentMode, onModeChange]);
107
+
108
+ if (availableModes.length <= 1) {
109
+ return null;
110
+ }
111
+
112
+ const renderTabButton = (modeOption: ViewModeOption, index: number) => {
113
+ const isActive = modeOption.mode === currentMode;
114
+ const isDisabled = disabled;
115
+
116
+ return (
117
+ <button
118
+ key={modeOption.mode}
119
+ onClick={() => handleModeChange(modeOption.mode)}
120
+ onKeyDown={(e) => handleKeyDown(e, modeOption.mode)}
121
+ disabled={isDisabled}
122
+ className={cn(
123
+ 'flex items-center gap-2 px-3 py-2 text-sm font-medium transition-colors',
124
+ 'border-b-2 focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2',
125
+ 'disabled:opacity-50 disabled:cursor-not-allowed',
126
+ isActive
127
+ ? 'border-primary text-primary bg-primary/5'
128
+ : 'border-transparent text-muted-foreground hover:text-foreground hover:bg-muted',
129
+ !showLabels && 'px-2'
130
+ )}
131
+ role="tab"
132
+ aria-selected={isActive}
133
+ aria-controls={`view-${modeOption.mode}`}
134
+ title={`${modeOption.description} (${index + 1})`}
135
+ >
136
+ {modeOption.icon}
137
+ {showLabels && <span className="hidden sm:inline">{modeOption.label}</span>}
138
+ </button>
139
+ );
140
+ };
141
+
142
+ const renderToggleButton = (modeOption: ViewModeOption, index: number) => {
143
+ const isActive = modeOption.mode === currentMode;
144
+ const isDisabled = disabled;
145
+
146
+ return (
147
+ <button
148
+ key={modeOption.mode}
149
+ onClick={() => handleModeChange(modeOption.mode)}
150
+ onKeyDown={(e) => handleKeyDown(e, modeOption.mode)}
151
+ disabled={isDisabled}
152
+ className={cn(
153
+ 'flex items-center gap-2 px-3 py-2 text-sm font-medium transition-colors',
154
+ 'border border-input focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2',
155
+ 'disabled:opacity-50 disabled:cursor-not-allowed',
156
+ 'first:rounded-l-md last:rounded-r-md first:border-r-0 last:border-l-0',
157
+ isActive
158
+ ? 'bg-primary text-primary-foreground border-primary'
159
+ : 'bg-background hover:bg-muted',
160
+ !showLabels && 'px-2'
161
+ )}
162
+ aria-pressed={isActive}
163
+ title={`${modeOption.description} (${index + 1})`}
164
+ >
165
+ {modeOption.icon}
166
+ {showLabels && <span className="hidden sm:inline">{modeOption.label}</span>}
167
+ </button>
168
+ );
169
+ };
170
+
171
+ const availableOptions = availableModes
172
+ .map(mode => VIEW_MODE_OPTIONS[mode])
173
+ .filter((option): option is ViewModeOption => option !== undefined);
174
+
175
+ if (variant === 'tabs') {
176
+ return (
177
+ <div
178
+ className={cn('flex items-center', className)}
179
+ role="tablist"
180
+ aria-label="View mode selection"
181
+ >
182
+ {availableOptions.map(renderTabButton)}
183
+ </div>
184
+ );
185
+ }
186
+
187
+ return (
188
+ <div
189
+ className={cn('flex items-center', className)}
190
+ role="group"
191
+ aria-label="View mode selection"
192
+ >
193
+ {availableOptions.map(renderToggleButton)}
194
+ </div>
195
+ );
196
+ });
197
+
198
+ ViewModeToggle.displayName = 'ViewModeToggle';
199
+
200
+ export default ViewModeToggle;