@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,414 @@
1
+ import React from 'react';
2
+ import { ListItemsModel } from '../models/ListItemsModel';
3
+
4
+ // AICODE-NOTE: Keyboard navigation utility for list components
5
+
6
+ export interface KeyboardNavigationOptions {
7
+ enableArrowKeys?: boolean;
8
+ enableHomeEnd?: boolean;
9
+ enableSpaceEnter?: boolean;
10
+ enableSelectAll?: boolean;
11
+ enableEscape?: boolean;
12
+ enablePageUpDown?: boolean;
13
+ }
14
+
15
+ export class ListKeyboardHandler {
16
+ private model: ListItemsModel;
17
+ private options: Required<KeyboardNavigationOptions>;
18
+ private isAttached: boolean = false;
19
+
20
+ constructor(model: ListItemsModel, options: KeyboardNavigationOptions = {}) {
21
+ this.model = model;
22
+ this.options = {
23
+ enableArrowKeys: true,
24
+ enableHomeEnd: true,
25
+ enableSpaceEnter: true,
26
+ enableSelectAll: true,
27
+ enableEscape: true,
28
+ enablePageUpDown: true,
29
+ ...options
30
+ };
31
+ }
32
+
33
+ /**
34
+ * Attach keyboard event listeners
35
+ */
36
+ attach(element?: HTMLElement): void {
37
+ if (this.isAttached) return;
38
+
39
+ const target = element || document;
40
+ target.addEventListener('keydown', this.handleKeyDown as EventListener);
41
+ this.isAttached = true;
42
+ }
43
+
44
+ /**
45
+ * Detach keyboard event listeners
46
+ */
47
+ detach(element?: HTMLElement): void {
48
+ if (!this.isAttached) return;
49
+
50
+ const target = element || document;
51
+ target.removeEventListener('keydown', this.handleKeyDown as EventListener);
52
+ this.isAttached = false;
53
+ }
54
+
55
+ /**
56
+ * Main keyboard event handler
57
+ */
58
+ private handleKeyDown = (event: Event): void => {
59
+ const keyboardEvent = event as KeyboardEvent;
60
+
61
+ // Skip when an input/textarea is focused (e.g. inline rename)
62
+ const tag = (keyboardEvent.target as HTMLElement)?.tagName;
63
+ if (tag === 'INPUT' || tag === 'TEXTAREA') return;
64
+
65
+ // AICODE-NOTE: Only handle keyboard events when list has focus or items
66
+ if (this.model.items.length === 0) return;
67
+
68
+ const { key, ctrlKey, metaKey, shiftKey } = keyboardEvent;
69
+ const isModifierPressed = ctrlKey || metaKey;
70
+
71
+ // AICODE-NOTE: Handle different keyboard shortcuts
72
+ switch (key) {
73
+ case 'ArrowUp':
74
+ if (this.options.enableArrowKeys) {
75
+ keyboardEvent.preventDefault();
76
+ this.handleArrowUp(shiftKey);
77
+ }
78
+ break;
79
+
80
+ case 'ArrowDown':
81
+ if (this.options.enableArrowKeys) {
82
+ keyboardEvent.preventDefault();
83
+ this.handleArrowDown(shiftKey);
84
+ }
85
+ break;
86
+
87
+ case 'ArrowLeft':
88
+ if (this.options.enableArrowKeys && this.model.currentViewType.id === 'grid') {
89
+ keyboardEvent.preventDefault();
90
+ this.handleArrowLeft(shiftKey);
91
+ }
92
+ break;
93
+
94
+ case 'ArrowRight':
95
+ if (this.options.enableArrowKeys && this.model.currentViewType.id === 'grid') {
96
+ keyboardEvent.preventDefault();
97
+ this.handleArrowRight(shiftKey);
98
+ }
99
+ break;
100
+
101
+ case 'Home':
102
+ if (this.options.enableHomeEnd) {
103
+ keyboardEvent.preventDefault();
104
+ this.handleHome(shiftKey);
105
+ }
106
+ break;
107
+
108
+ case 'End':
109
+ if (this.options.enableHomeEnd) {
110
+ keyboardEvent.preventDefault();
111
+ this.handleEnd(shiftKey);
112
+ }
113
+ break;
114
+
115
+ case 'PageUp':
116
+ if (this.options.enablePageUpDown) {
117
+ keyboardEvent.preventDefault();
118
+ this.handlePageUp(shiftKey);
119
+ }
120
+ break;
121
+
122
+ case 'PageDown':
123
+ if (this.options.enablePageUpDown) {
124
+ keyboardEvent.preventDefault();
125
+ this.handlePageDown(shiftKey);
126
+ }
127
+ break;
128
+
129
+ case ' ': // Space
130
+ if (this.options.enableSpaceEnter) {
131
+ keyboardEvent.preventDefault();
132
+ this.handleSpace(shiftKey);
133
+ }
134
+ break;
135
+
136
+ case 'Enter':
137
+ if (this.options.enableSpaceEnter) {
138
+ keyboardEvent.preventDefault();
139
+ this.handleEnter();
140
+ }
141
+ break;
142
+
143
+ case 'a':
144
+ case 'A':
145
+ if (this.options.enableSelectAll && isModifierPressed) {
146
+ keyboardEvent.preventDefault();
147
+ this.handleSelectAll();
148
+ }
149
+ break;
150
+
151
+ case 'Escape':
152
+ if (this.options.enableEscape) {
153
+ keyboardEvent.preventDefault();
154
+ this.handleEscape();
155
+ }
156
+ break;
157
+ }
158
+ };
159
+
160
+ /**
161
+ * Handle arrow up navigation
162
+ */
163
+ private handleArrowUp(shiftKey: boolean): void {
164
+ if (this.model.currentViewType.id === 'grid') {
165
+ this.handleGridArrowUp(shiftKey);
166
+ } else {
167
+ this.handleListArrowUp(shiftKey);
168
+ }
169
+ }
170
+
171
+ /**
172
+ * Handle arrow down navigation
173
+ */
174
+ private handleArrowDown(shiftKey: boolean): void {
175
+ if (this.model.currentViewType.id === 'grid') {
176
+ this.handleGridArrowDown(shiftKey);
177
+ } else {
178
+ this.handleListArrowDown(shiftKey);
179
+ }
180
+ }
181
+
182
+ /**
183
+ * Handle arrow left navigation (grid view)
184
+ */
185
+ private handleArrowLeft(shiftKey: boolean): void {
186
+ const currentIndex = this.getCurrentFocusedIndex();
187
+ if (currentIndex > 0) {
188
+ this.navigateToIndex(currentIndex - 1, shiftKey);
189
+ }
190
+ }
191
+
192
+ /**
193
+ * Handle arrow right navigation (grid view)
194
+ */
195
+ private handleArrowRight(shiftKey: boolean): void {
196
+ const currentIndex = this.getCurrentFocusedIndex();
197
+ if (currentIndex < this.model.items.length - 1) {
198
+ this.navigateToIndex(currentIndex + 1, shiftKey);
199
+ }
200
+ }
201
+
202
+ /**
203
+ * Handle list view arrow up
204
+ */
205
+ private handleListArrowUp(shiftKey: boolean): void {
206
+ const currentIndex = this.getCurrentFocusedIndex();
207
+ if (currentIndex > 0) {
208
+ this.navigateToIndex(currentIndex - 1, shiftKey);
209
+ }
210
+ }
211
+
212
+ /**
213
+ * Handle list view arrow down
214
+ */
215
+ private handleListArrowDown(shiftKey: boolean): void {
216
+ const currentIndex = this.getCurrentFocusedIndex();
217
+ if (currentIndex < this.model.items.length - 1) {
218
+ this.navigateToIndex(currentIndex + 1, shiftKey);
219
+ }
220
+ }
221
+
222
+ /**
223
+ * Handle grid view arrow up (move up by grid columns)
224
+ */
225
+ private handleGridArrowUp(shiftKey: boolean): void {
226
+ const currentIndex = this.getCurrentFocusedIndex();
227
+ const columnsPerRow = this.getGridColumnsPerRow();
228
+ const newIndex = Math.max(0, currentIndex - columnsPerRow);
229
+ this.navigateToIndex(newIndex, shiftKey);
230
+ }
231
+
232
+ /**
233
+ * Handle grid view arrow down (move down by grid columns)
234
+ */
235
+ private handleGridArrowDown(shiftKey: boolean): void {
236
+ const currentIndex = this.getCurrentFocusedIndex();
237
+ const columnsPerRow = this.getGridColumnsPerRow();
238
+ const newIndex = Math.min(this.model.items.length - 1, currentIndex + columnsPerRow);
239
+ this.navigateToIndex(newIndex, shiftKey);
240
+ }
241
+
242
+ /**
243
+ * Handle Home key - go to first item
244
+ */
245
+ private handleHome(shiftKey: boolean): void {
246
+ this.navigateToIndex(0, shiftKey);
247
+ }
248
+
249
+ /**
250
+ * Handle End key - go to last item
251
+ */
252
+ private handleEnd(shiftKey: boolean): void {
253
+ this.navigateToIndex(this.model.items.length - 1, shiftKey);
254
+ }
255
+
256
+ /**
257
+ * Handle Page Up - move up by visible page size
258
+ */
259
+ private handlePageUp(shiftKey: boolean): void {
260
+ const currentIndex = this.getCurrentFocusedIndex();
261
+ const pageSize = this.getPageSize();
262
+ const newIndex = Math.max(0, currentIndex - pageSize);
263
+ this.navigateToIndex(newIndex, shiftKey);
264
+ }
265
+
266
+ /**
267
+ * Handle Page Down - move down by visible page size
268
+ */
269
+ private handlePageDown(shiftKey: boolean): void {
270
+ const currentIndex = this.getCurrentFocusedIndex();
271
+ const pageSize = this.getPageSize();
272
+ const newIndex = Math.min(this.model.items.length - 1, currentIndex + pageSize);
273
+ this.navigateToIndex(newIndex, shiftKey);
274
+ }
275
+
276
+ /**
277
+ * Handle Space key - toggle selection of focused item
278
+ */
279
+ private handleSpace(shiftKey: boolean): void {
280
+ const focusedItem = this.getFocusedItem();
281
+ if (focusedItem) {
282
+ this.model.selectItem(focusedItem, { ctrl: true, shift: shiftKey });
283
+ }
284
+ }
285
+
286
+ /**
287
+ * Handle Enter key - activate focused item
288
+ */
289
+ private handleEnter(): void {
290
+ const focusedItem = this.getFocusedItem();
291
+ if (focusedItem && this.model.provider.onItemDoubleClick) {
292
+ this.model.provider.onItemDoubleClick(focusedItem);
293
+ }
294
+ }
295
+
296
+ /**
297
+ * Handle Ctrl+A - select all items
298
+ */
299
+ private handleSelectAll(): void {
300
+ this.model.selectAll();
301
+ }
302
+
303
+ /**
304
+ * Handle Escape - clear selection first, then navigate up one directory
305
+ */
306
+ private handleEscape(): void {
307
+ if (this.model.hasSelection) {
308
+ this.model.clearSelection();
309
+ } else if (this.model.provider.onNavigateUp) {
310
+ this.model.provider.onNavigateUp();
311
+ }
312
+ }
313
+
314
+ /**
315
+ * Navigate to specific index with optional range selection
316
+ */
317
+ private navigateToIndex(index: number, extendSelection: boolean = false): void {
318
+ const item = this.model.items[index];
319
+ if (!item) return;
320
+
321
+ // Update focused item
322
+ this.model.setFocusedItem(item.id);
323
+
324
+ // Handle selection based on shift key
325
+ if (extendSelection && this.model.provider.isMultiSelectEnabled) {
326
+ // Shift+arrow: extend range selection from anchor
327
+ if (!this.model.selectionAnchor) {
328
+ // Set anchor if not already set
329
+ this.model.selectionAnchor = this.model.focusedItem || item.id;
330
+ }
331
+ this.model.selectRange(this.model.selectionAnchor, item.id);
332
+ } else {
333
+ // Plain arrow: single selection, reset anchor
334
+ this.model.selectItem(item);
335
+ }
336
+
337
+ // Ensure item is visible
338
+ this.model.ensureItemVisible(item.id);
339
+ }
340
+
341
+ /**
342
+ * Get current focused item index
343
+ */
344
+ private getCurrentFocusedIndex(): number {
345
+ if (!this.model.focusedItem) {
346
+ return 0;
347
+ }
348
+ return this.model.getItemIndex(this.model.focusedItem);
349
+ }
350
+
351
+ /**
352
+ * Get currently focused item
353
+ */
354
+ private getFocusedItem() {
355
+ if (!this.model.focusedItem) {
356
+ // AICODE-NOTE: If no item is focused, focus the first item
357
+ if (this.model.items.length > 0) {
358
+ const firstItem = this.model.items[0];
359
+ if (firstItem) {
360
+ this.model.setFocusedItem(firstItem.id);
361
+ return firstItem;
362
+ }
363
+ }
364
+ return null;
365
+ }
366
+ const item = this.model.getItem(this.model.focusedItem);
367
+ return item || null;
368
+ }
369
+
370
+ /**
371
+ * Get number of columns per row in grid view
372
+ */
373
+ private getGridColumnsPerRow(): number {
374
+ // AICODE-NOTE: Default to 4 columns, could be made configurable
375
+ return 4;
376
+ }
377
+
378
+ /**
379
+ * Get page size for page up/down navigation
380
+ */
381
+ private getPageSize(): number {
382
+ // AICODE-NOTE: Default to 10 items per page, could be calculated from viewport
383
+ return 10;
384
+ }
385
+ }
386
+
387
+ /**
388
+ * Hook for using keyboard navigation in React components
389
+ */
390
+ export const useListKeyboard = (
391
+ model: ListItemsModel,
392
+ options?: KeyboardNavigationOptions,
393
+ containerRef?: React.RefObject<HTMLElement | HTMLDivElement | null>
394
+ ) => {
395
+ const keyboardHandler = React.useMemo(
396
+ () => new ListKeyboardHandler(model, options),
397
+ [model, options]
398
+ );
399
+
400
+ React.useEffect(() => {
401
+ const element = containerRef?.current;
402
+ if (element) {
403
+ keyboardHandler.attach(element);
404
+ }
405
+
406
+ return () => {
407
+ if (element) {
408
+ keyboardHandler.detach(element);
409
+ }
410
+ };
411
+ }, [keyboardHandler, containerRef]);
412
+
413
+ return keyboardHandler;
414
+ };
@@ -0,0 +1,302 @@
1
+ import { makeAutoObservable } from "mobx";
2
+
3
+ // AICODE-NOTE: Masonry layout calculator for variable height items
4
+ export interface MasonryLayoutConfig {
5
+ containerWidth: number;
6
+ containerHeight: number;
7
+ columnCount: number | 'auto';
8
+ gap: number;
9
+ padding: number;
10
+ minColumnWidth: number;
11
+ maxColumnWidth: number;
12
+ }
13
+
14
+ export interface MasonryItemLayout {
15
+ x: number;
16
+ y: number;
17
+ width: number;
18
+ height: number;
19
+ column: number;
20
+ thumbnailWidth: number;
21
+ thumbnailHeight: number;
22
+ textHeight: number;
23
+ }
24
+
25
+ export interface MasonryLayoutResult {
26
+ columnCount: number;
27
+ columnWidth: number;
28
+ totalHeight: number;
29
+ items: MasonryItemLayout[];
30
+ columnHeights: number[];
31
+ }
32
+
33
+ // AICODE-NOTE: Item data interface for masonry calculations
34
+ export interface MasonryItemData {
35
+ id: string;
36
+ aspectRatio?: number;
37
+ customHeight?: number;
38
+ textLength?: number;
39
+ }
40
+
41
+ // AICODE-NOTE: MobX-enabled calculation class for masonry layouts with reactive updates
42
+ export class MasonryLayoutCalculator {
43
+ private config: MasonryLayoutConfig;
44
+
45
+ constructor(config: MasonryLayoutConfig) {
46
+ this.config = config;
47
+ makeAutoObservable(this, {});
48
+ }
49
+
50
+ // AICODE-NOTE: Update configuration and trigger reactive updates
51
+ updateConfig(updates: Partial<MasonryLayoutConfig>): void {
52
+ this.config = { ...this.config, ...updates };
53
+ }
54
+
55
+ // AICODE-NOTE: Computed getter for current configuration
56
+ get currentConfig(): MasonryLayoutConfig {
57
+ return this.config;
58
+ }
59
+
60
+ // AICODE-NOTE: Calculate optimal column count
61
+ calculateColumnCount(): number {
62
+ if (typeof this.config.columnCount === 'number') {
63
+ return Math.max(1, this.config.columnCount);
64
+ }
65
+
66
+ const { containerWidth, gap, padding, minColumnWidth, maxColumnWidth } = this.config;
67
+ const availableWidth = containerWidth - (padding * 2);
68
+
69
+ // Find optimal column count
70
+ let bestColumnCount = 1;
71
+
72
+ for (let columnCount = 1; columnCount <= 8; columnCount++) {
73
+ const totalGaps = (columnCount - 1) * gap;
74
+ const columnWidth = (availableWidth - totalGaps) / columnCount;
75
+
76
+ if (columnWidth >= minColumnWidth && columnWidth <= maxColumnWidth) {
77
+ bestColumnCount = columnCount;
78
+ } else if (columnWidth < minColumnWidth) {
79
+ break;
80
+ }
81
+ }
82
+
83
+ return bestColumnCount;
84
+ }
85
+
86
+ // AICODE-NOTE: Calculate column width
87
+ calculateColumnWidth(columnCount: number): number {
88
+ const { containerWidth, gap, padding } = this.config;
89
+ const availableWidth = containerWidth - (padding * 2);
90
+ const totalGaps = (columnCount - 1) * gap;
91
+ return Math.floor((availableWidth - totalGaps) / columnCount);
92
+ }
93
+
94
+ // AICODE-NOTE: Calculate item height based on content
95
+ calculateItemHeight(
96
+ item: MasonryItemData,
97
+ columnWidth: number,
98
+ baseHeight: number = 200
99
+ ): number {
100
+ // Use custom height if provided
101
+ if (item.customHeight) {
102
+ return item.customHeight;
103
+ }
104
+
105
+ // Calculate height based on aspect ratio
106
+ if (item.aspectRatio) {
107
+ const thumbnailHeight = columnWidth / item.aspectRatio;
108
+ const textHeight = this.calculateTextHeight(item.textLength || 0);
109
+ const padding = 16;
110
+ return Math.floor(thumbnailHeight + textHeight + padding);
111
+ }
112
+
113
+ // Default height with text consideration
114
+ const textHeight = this.calculateTextHeight(item.textLength || 0);
115
+ return baseHeight + textHeight;
116
+ }
117
+
118
+ // AICODE-NOTE: Calculate text height based on content length
119
+ private calculateTextHeight(textLength: number): number {
120
+ const baseTextHeight = 40;
121
+ const extraLines = Math.floor(textLength / 30); // Rough estimate
122
+ return baseTextHeight + (extraLines * 20);
123
+ }
124
+
125
+ // AICODE-NOTE: Calculate thumbnail dimensions within item
126
+ calculateThumbnailDimensions(
127
+ item: MasonryItemData,
128
+ columnWidth: number,
129
+ itemHeight: number
130
+ ): { width: number; height: number } {
131
+ const padding = 16;
132
+ const textHeight = this.calculateTextHeight(item.textLength || 0);
133
+ const availableWidth = columnWidth - padding;
134
+ const availableHeight = itemHeight - textHeight - padding;
135
+
136
+ if (item.aspectRatio) {
137
+ const thumbnailWidth = Math.min(availableWidth, availableHeight * item.aspectRatio);
138
+ const thumbnailHeight = thumbnailWidth / item.aspectRatio;
139
+ return {
140
+ width: Math.floor(thumbnailWidth),
141
+ height: Math.floor(thumbnailHeight)
142
+ };
143
+ }
144
+
145
+ // Square thumbnail by default
146
+ const size = Math.min(availableWidth, availableHeight);
147
+ return {
148
+ width: Math.floor(size),
149
+ height: Math.floor(size)
150
+ };
151
+ }
152
+
153
+ // AICODE-NOTE: Calculate layout for all items
154
+ calculateLayout(items: MasonryItemData[]): MasonryLayoutResult {
155
+ const columnCount = this.calculateColumnCount();
156
+ const columnWidth = this.calculateColumnWidth(columnCount);
157
+ const columnHeights = new Array(columnCount).fill(this.config.padding);
158
+ const layoutItems: MasonryItemLayout[] = [];
159
+
160
+ for (let i = 0; i < items.length; i++) {
161
+ const item = items[i];
162
+ if (!item) continue;
163
+
164
+ // Find shortest column
165
+ const shortestColumnIndex = columnHeights.indexOf(Math.min(...columnHeights));
166
+ const currentHeight = columnHeights[shortestColumnIndex];
167
+
168
+ // Calculate item dimensions
169
+ const itemHeight = this.calculateItemHeight(item, columnWidth);
170
+ const { width: thumbnailWidth, height: thumbnailHeight } =
171
+ this.calculateThumbnailDimensions(item, columnWidth, itemHeight);
172
+ const textHeight = this.calculateTextHeight(item.textLength || 0);
173
+
174
+ // Calculate position
175
+ const x = this.config.padding + (shortestColumnIndex * (columnWidth + this.config.gap));
176
+ const y = currentHeight;
177
+
178
+ layoutItems.push({
179
+ x,
180
+ y,
181
+ width: columnWidth,
182
+ height: itemHeight,
183
+ column: shortestColumnIndex,
184
+ thumbnailWidth,
185
+ thumbnailHeight,
186
+ textHeight
187
+ });
188
+
189
+ // Update column height
190
+ columnHeights[shortestColumnIndex] = currentHeight + itemHeight + this.config.gap;
191
+ }
192
+
193
+ const totalHeight = Math.max(...columnHeights) + this.config.padding;
194
+
195
+ return {
196
+ columnCount,
197
+ columnWidth,
198
+ totalHeight,
199
+ items: layoutItems,
200
+ columnHeights
201
+ };
202
+ }
203
+
204
+ // AICODE-NOTE: Calculate visible items for virtualization
205
+ calculateVisibleItems(
206
+ items: MasonryItemData[],
207
+ scrollTop: number,
208
+ viewportHeight: number,
209
+ overscan: number = 200
210
+ ): { startIndex: number; endIndex: number; visibleItems: MasonryItemLayout[] } {
211
+ const layout = this.calculateLayout(items);
212
+ const viewportTop = scrollTop - overscan;
213
+ const viewportBottom = scrollTop + viewportHeight + overscan;
214
+
215
+ const visibleIndices: number[] = [];
216
+ const visibleItems: MasonryItemLayout[] = [];
217
+
218
+ for (let i = 0; i < layout.items.length; i++) {
219
+ const item = layout.items[i];
220
+ if (item && item.y < viewportBottom && (item.y + item.height) > viewportTop) {
221
+ visibleIndices.push(i);
222
+ visibleItems.push(item);
223
+ }
224
+ }
225
+
226
+ const startIndex = visibleIndices.length > 0 ? Math.min(...visibleIndices) : 0;
227
+ const endIndex = visibleIndices.length > 0 ? Math.max(...visibleIndices) : 0;
228
+
229
+ return {
230
+ startIndex,
231
+ endIndex,
232
+ visibleItems
233
+ };
234
+ }
235
+
236
+ // AICODE-NOTE: Get item position by index
237
+ getItemPosition(itemIndex: number, items: MasonryItemData[]): MasonryItemLayout | null {
238
+ const layout = this.calculateLayout(items);
239
+ return layout.items[itemIndex] || null;
240
+ }
241
+
242
+ // AICODE-NOTE: Find item at position
243
+ getItemAtPosition(x: number, y: number, items: MasonryItemData[]): number | null {
244
+ const layout = this.calculateLayout(items);
245
+
246
+ for (let i = 0; i < layout.items.length; i++) {
247
+ const item = layout.items[i];
248
+ if (item &&
249
+ x >= item.x &&
250
+ x <= item.x + item.width &&
251
+ y >= item.y &&
252
+ y <= item.y + item.height
253
+ ) {
254
+ return i;
255
+ }
256
+ }
257
+
258
+ return null;
259
+ }
260
+ }
261
+
262
+ // AICODE-NOTE: Factory function for masonry calculator
263
+ export function createMasonryCalculator(
264
+ containerWidth: number,
265
+ containerHeight: number,
266
+ options: Partial<MasonryLayoutConfig> = {}
267
+ ): MasonryLayoutCalculator {
268
+ const defaultConfig: MasonryLayoutConfig = {
269
+ containerWidth,
270
+ containerHeight,
271
+ columnCount: 'auto',
272
+ gap: 4,
273
+ padding: 4,
274
+ minColumnWidth: 200,
275
+ maxColumnWidth: 400,
276
+ ...options
277
+ };
278
+
279
+ return new MasonryLayoutCalculator(defaultConfig);
280
+ }
281
+
282
+ // AICODE-NOTE: Preset configurations for masonry layouts
283
+ export const MASONRY_PRESETS = {
284
+ compact: {
285
+ gap: 4,
286
+ padding: 4,
287
+ minColumnWidth: 160,
288
+ maxColumnWidth: 250
289
+ },
290
+ comfortable: {
291
+ gap: 8,
292
+ padding: 8,
293
+ minColumnWidth: 200,
294
+ maxColumnWidth: 300
295
+ },
296
+ spacious: {
297
+ gap: 16,
298
+ padding: 16,
299
+ minColumnWidth: 250,
300
+ maxColumnWidth: 400
301
+ }
302
+ } as const;