@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,320 @@
1
+ import { ListItemData } from '../types/ListTypes';
2
+ import { benchmark } from './BenchmarkLogger';
3
+
4
+ // AICODE-NOTE: Enhanced drag and drop manager for better visual feedback and performance
5
+
6
+ export interface DragDropState {
7
+ isDragging: boolean;
8
+ draggedItems: ListItemData[];
9
+ dragOverItem: string | null;
10
+ dragOverPosition: 'before' | 'after' | 'inside' | null;
11
+ dragStartPosition: { x: number; y: number } | null;
12
+ dragCurrentPosition: { x: number; y: number } | null;
13
+ }
14
+
15
+ export interface DragDropCallbacks {
16
+ onDragStart?: (items: ListItemData[], event: DragEvent) => void;
17
+ onDragEnd?: (items: ListItemData[], success: boolean) => void;
18
+ onDrop?: (draggedItems: ListItemData[], targetItem: ListItemData | null, position: 'before' | 'after' | 'inside', event: DragEvent) => boolean;
19
+ canDragItem?: (item: ListItemData) => boolean;
20
+ canDropItems?: (draggedItems: ListItemData[], targetItem: ListItemData | null, position: 'before' | 'after' | 'inside') => boolean;
21
+ }
22
+
23
+ export class DragDropManager {
24
+ private state: DragDropState = {
25
+ isDragging: false,
26
+ draggedItems: [],
27
+ dragOverItem: null,
28
+ dragOverPosition: null,
29
+ dragStartPosition: null,
30
+ dragCurrentPosition: null
31
+ };
32
+
33
+ private callbacks: DragDropCallbacks = {};
34
+ private dragPreviewElement: HTMLElement | null = null;
35
+ private dragThreshold = 5; // pixels to move before starting drag
36
+
37
+ constructor(callbacks: DragDropCallbacks = {}) {
38
+ this.callbacks = callbacks;
39
+ }
40
+
41
+ /**
42
+ * Update callbacks
43
+ */
44
+ updateCallbacks(callbacks: DragDropCallbacks): void {
45
+ this.callbacks = { ...this.callbacks, ...callbacks };
46
+ }
47
+
48
+ /**
49
+ * Get current drag drop state
50
+ */
51
+ getState(): DragDropState {
52
+ return { ...this.state };
53
+ }
54
+
55
+ /**
56
+ * Check if an item can be dragged
57
+ */
58
+ canDragItem(item: ListItemData): boolean {
59
+ return this.callbacks.canDragItem?.(item) ?? true;
60
+ }
61
+
62
+ /**
63
+ * Check if items can be dropped on target
64
+ */
65
+ canDropItems(draggedItems: ListItemData[], targetItem: ListItemData | null, position: 'before' | 'after' | 'inside'): boolean {
66
+ return this.callbacks.canDropItems?.(draggedItems, targetItem, position) ?? true;
67
+ }
68
+
69
+ /**
70
+ * Start drag operation
71
+ */
72
+ startDrag(items: ListItemData[], event: DragEvent): void {
73
+ benchmark.start('drag-operation', { itemCount: items.length });
74
+
75
+ // Check if any items can be dragged
76
+ const draggableItems = items.filter(item => this.canDragItem(item));
77
+ if (draggableItems.length === 0) {
78
+ benchmark.end('drag-operation', { result: 'no-draggable-items' });
79
+ return;
80
+ }
81
+
82
+ this.state = {
83
+ ...this.state,
84
+ isDragging: true,
85
+ draggedItems: draggableItems,
86
+ dragStartPosition: { x: event.clientX, y: event.clientY },
87
+ dragCurrentPosition: { x: event.clientX, y: event.clientY }
88
+ };
89
+
90
+ // Set drag data
91
+ const itemIds = draggableItems.map(item => item.id);
92
+ event.dataTransfer?.setData('application/json', JSON.stringify({
93
+ type: 'list-items',
94
+ itemIds,
95
+ itemCount: draggableItems.length
96
+ }));
97
+
98
+ // Set drag effect
99
+ if (event.dataTransfer) {
100
+ event.dataTransfer.effectAllowed = 'move';
101
+ event.dataTransfer.dropEffect = 'move';
102
+ }
103
+
104
+ // Create custom drag preview
105
+ this.createDragPreview(draggableItems, event);
106
+
107
+ // Notify callback
108
+ this.callbacks.onDragStart?.(draggableItems, event);
109
+
110
+ console.log('🚀 [DRAG] Started dragging', draggableItems.length, 'items');
111
+ }
112
+
113
+ /**
114
+ * Update drag over state
115
+ */
116
+ updateDragOver(itemId: string | null, position: 'before' | 'after' | 'inside' | null, event?: DragEvent): void {
117
+ if (!this.state.isDragging) return;
118
+
119
+ // Update current position if event provided
120
+ if (event) {
121
+ this.state.dragCurrentPosition = { x: event.clientX, y: event.clientY };
122
+ }
123
+
124
+ // Only update if changed
125
+ if (this.state.dragOverItem !== itemId || this.state.dragOverPosition !== position) {
126
+ this.state.dragOverItem = itemId;
127
+ this.state.dragOverPosition = position;
128
+
129
+ console.log('📍 [DRAG] Drag over:', itemId, position);
130
+ }
131
+ }
132
+
133
+ /**
134
+ * Handle drop operation
135
+ */
136
+ handleDrop(targetItem: ListItemData | null, position: 'before' | 'after' | 'inside', event: DragEvent): boolean {
137
+ if (!this.state.isDragging || this.state.draggedItems.length === 0) {
138
+ return false;
139
+ }
140
+
141
+ benchmark.start('drop-operation');
142
+
143
+ // Check if drop is allowed
144
+ if (!this.canDropItems(this.state.draggedItems, targetItem, position)) {
145
+ console.log('❌ [DROP] Drop not allowed');
146
+ this.endDrag(false);
147
+ benchmark.end('drop-operation', { result: 'not-allowed' });
148
+ return false;
149
+ }
150
+
151
+ // Notify callback
152
+ const success = this.callbacks.onDrop?.(this.state.draggedItems, targetItem, position, event) ?? true;
153
+
154
+ console.log(success ? '✅ [DROP] Drop successful' : '❌ [DROP] Drop failed');
155
+
156
+ this.endDrag(success);
157
+ benchmark.end('drop-operation', { result: success ? 'success' : 'failed' });
158
+
159
+ return success;
160
+ }
161
+
162
+ /**
163
+ * End drag operation
164
+ */
165
+ endDrag(success: boolean): void {
166
+ if (!this.state.isDragging) return;
167
+
168
+ const draggedItems = [...this.state.draggedItems];
169
+
170
+ // Clean up drag preview
171
+ this.removeDragPreview();
172
+
173
+ // Reset state
174
+ this.state = {
175
+ isDragging: false,
176
+ draggedItems: [],
177
+ dragOverItem: null,
178
+ dragOverPosition: null,
179
+ dragStartPosition: null,
180
+ dragCurrentPosition: null
181
+ };
182
+
183
+ // Notify callback
184
+ this.callbacks.onDragEnd?.(draggedItems, success);
185
+
186
+ benchmark.end('drag-operation', {
187
+ result: success ? 'success' : 'cancelled',
188
+ itemCount: draggedItems.length
189
+ });
190
+
191
+ console.log('🏁 [DRAG] Ended drag operation:', success ? 'success' : 'cancelled');
192
+ }
193
+
194
+ /**
195
+ * Calculate drop position based on mouse position within element
196
+ */
197
+ calculateDropPosition(event: DragEvent, element: HTMLElement): 'before' | 'after' | 'inside' {
198
+ const rect = element.getBoundingClientRect();
199
+ const y = event.clientY - rect.top;
200
+ const height = rect.height;
201
+
202
+ // Divide element into three zones
203
+ if (y < height * 0.25) {
204
+ return 'before';
205
+ } else if (y > height * 0.75) {
206
+ return 'after';
207
+ } else {
208
+ return 'inside';
209
+ }
210
+ }
211
+
212
+ /**
213
+ * Create custom drag preview element
214
+ */
215
+ private createDragPreview(items: ListItemData[], event: DragEvent): void {
216
+ // Remove existing preview
217
+ this.removeDragPreview();
218
+
219
+ // Create preview element
220
+ const preview = document.createElement('div');
221
+ preview.className = 'drag-preview';
222
+ preview.style.cssText = `
223
+ position: fixed;
224
+ top: -1000px;
225
+ left: -1000px;
226
+ z-index: 9999;
227
+ pointer-events: none;
228
+ background: white;
229
+ border: 1px solid #ccc;
230
+ border-radius: 8px;
231
+ padding: 8px 12px;
232
+ box-shadow: 0 4px 12px rgba(0,0,0,0.15);
233
+ font-size: 14px;
234
+ max-width: 200px;
235
+ `;
236
+
237
+ // Set preview content
238
+ if (items.length === 1) {
239
+ preview.textContent = items[0]?.name || 'Item';
240
+ } else {
241
+ preview.innerHTML = `
242
+ <div style="font-weight: 500;">${items[0]?.name || 'Item'}</div>
243
+ <div style="font-size: 12px; color: #666; margin-top: 2px;">+${items.length - 1} more items</div>
244
+ `;
245
+ }
246
+
247
+ document.body.appendChild(preview);
248
+ this.dragPreviewElement = preview;
249
+
250
+ // Set as drag image
251
+ if (event.dataTransfer) {
252
+ event.dataTransfer.setDragImage(preview, 10, 10);
253
+ }
254
+
255
+ // Clean up after a short delay
256
+ setTimeout(() => {
257
+ if (preview.parentNode) {
258
+ preview.parentNode.removeChild(preview);
259
+ }
260
+ }, 100);
261
+ }
262
+
263
+ /**
264
+ * Remove drag preview element
265
+ */
266
+ private removeDragPreview(): void {
267
+ if (this.dragPreviewElement && this.dragPreviewElement.parentNode) {
268
+ this.dragPreviewElement.parentNode.removeChild(this.dragPreviewElement);
269
+ this.dragPreviewElement = null;
270
+ }
271
+ }
272
+
273
+ /**
274
+ * Get visual feedback classes for drag state
275
+ */
276
+ getDragClasses(itemId: string): string {
277
+ const classes: string[] = [];
278
+
279
+ if (this.state.isDragging) {
280
+ // Item being dragged
281
+ if (this.state.draggedItems.some(item => item.id === itemId)) {
282
+ classes.push('dragging', 'opacity-50');
283
+ }
284
+
285
+ // Item being dragged over
286
+ if (this.state.dragOverItem === itemId) {
287
+ classes.push('drag-over');
288
+
289
+ switch (this.state.dragOverPosition) {
290
+ case 'before':
291
+ classes.push('drag-over-before');
292
+ break;
293
+ case 'after':
294
+ classes.push('drag-over-after');
295
+ break;
296
+ case 'inside':
297
+ classes.push('drag-over-inside');
298
+ break;
299
+ }
300
+ }
301
+ }
302
+
303
+ return classes.join(' ');
304
+ }
305
+
306
+ /**
307
+ * Cleanup resources
308
+ */
309
+ destroy(): void {
310
+ this.removeDragPreview();
311
+ this.state = {
312
+ isDragging: false,
313
+ draggedItems: [],
314
+ dragOverItem: null,
315
+ dragOverPosition: null,
316
+ dragStartPosition: null,
317
+ dragCurrentPosition: null
318
+ };
319
+ }
320
+ }
@@ -0,0 +1,290 @@
1
+ import { makeAutoObservable } from "mobx";
2
+
3
+ // AICODE-NOTE: Grid layout calculator for precise positioning and sizing
4
+ export interface GridLayoutConfig {
5
+ containerWidth: number;
6
+ containerHeight: number;
7
+ itemsPerRow: number | 'auto';
8
+ gap: number;
9
+ padding: number;
10
+ minItemWidth: number;
11
+ maxItemWidth: number;
12
+ aspectRatio: number; // width/height ratio for items
13
+ }
14
+
15
+ export interface GridItemLayout {
16
+ x: number;
17
+ y: number;
18
+ width: number;
19
+ height: number;
20
+ thumbnailSize: number;
21
+ textHeight: number;
22
+ }
23
+
24
+ export interface GridLayoutResult {
25
+ itemsPerRow: number;
26
+ itemWidth: number;
27
+ itemHeight: number;
28
+ totalRows: number;
29
+ totalHeight: number;
30
+ items: GridItemLayout[];
31
+ }
32
+
33
+ // AICODE-NOTE: MobX-enabled calculation class for grid layouts with reactive updates
34
+ export class GridLayoutCalculator {
35
+ private config: GridLayoutConfig;
36
+
37
+ constructor(config: GridLayoutConfig) {
38
+ this.config = config;
39
+ makeAutoObservable(this, {});
40
+ }
41
+
42
+ // AICODE-NOTE: Update configuration and trigger reactive updates
43
+ updateConfig(updates: Partial<GridLayoutConfig>): void {
44
+ this.config = { ...this.config, ...updates };
45
+ }
46
+
47
+ // AICODE-NOTE: Computed getter for current configuration
48
+ get currentConfig(): GridLayoutConfig {
49
+ return this.config;
50
+ }
51
+
52
+ // AICODE-NOTE: Calculate optimal items per row based on container width
53
+ calculateItemsPerRow(): number {
54
+ if (typeof this.config.itemsPerRow === 'number') {
55
+ return Math.max(1, this.config.itemsPerRow);
56
+ }
57
+
58
+ const { containerWidth, gap, padding, minItemWidth, maxItemWidth } = this.config;
59
+ const availableWidth = containerWidth - (padding * 2);
60
+
61
+ // console.log('🔢 [ITEMS PER ROW] Calculating:', {
62
+ // containerWidth,
63
+ // availableWidth,
64
+ // minItemWidth,
65
+ // maxItemWidth,
66
+ // gap,
67
+ // padding
68
+ // });
69
+
70
+ // If container is too small, return 1
71
+ if (availableWidth < minItemWidth) {
72
+ // console.log('🔢 [ITEMS PER ROW] Container too small, returning 1');
73
+ return 1;
74
+ }
75
+
76
+ // Find the maximum number of items that can fit while respecting minItemWidth
77
+ let bestItemsPerRow = 1;
78
+
79
+ for (let itemsPerRow = 1; itemsPerRow <= 12; itemsPerRow++) {
80
+ const totalGaps = (itemsPerRow - 1) * gap;
81
+ const itemWidth = (availableWidth - totalGaps) / itemsPerRow;
82
+
83
+ // console.log(`🔢 [ITEMS PER ROW] Testing ${itemsPerRow} items: itemWidth=${itemWidth.toFixed(1)}, fits=${itemWidth >= minItemWidth}, withinMax=${itemWidth <= maxItemWidth}`);
84
+
85
+ if (itemWidth >= minItemWidth) {
86
+ // Always use this if it fits the minimum width requirement
87
+ bestItemsPerRow = itemsPerRow;
88
+ // console.log(`🔢 [ITEMS PER ROW] Fits minimum: ${itemsPerRow} items (width: ${itemWidth.toFixed(1)})`);
89
+ } else {
90
+ // console.log(`🔢 [ITEMS PER ROW] Too narrow, breaking at ${itemsPerRow} items`);
91
+ break; // Too many items per row
92
+ }
93
+ }
94
+
95
+ // console.log(`🔢 [ITEMS PER ROW] Final result: ${bestItemsPerRow} items per row (container: ${containerWidth}px, min: ${minItemWidth}px)`);
96
+ return bestItemsPerRow;
97
+ }
98
+
99
+ // AICODE-NOTE: Calculate item dimensions based on items per row
100
+ calculateItemDimensions(itemsPerRow: number): { width: number; height: number } {
101
+ const { containerWidth, gap, padding, aspectRatio } = this.config;
102
+ const availableWidth = containerWidth - (padding * 2);
103
+ const totalGaps = (itemsPerRow - 1) * gap;
104
+ const itemWidth = (availableWidth - totalGaps) / itemsPerRow;
105
+ const itemHeight = itemWidth / aspectRatio;
106
+
107
+ // AICODE-NOTE: Ensure calculated width doesn't cause overflow
108
+ const maxAllowedWidth = (containerWidth - (padding * 2) - ((itemsPerRow - 1) * gap)) / itemsPerRow;
109
+ const safeItemWidth = Math.min(itemWidth, maxAllowedWidth);
110
+ const safeItemHeight = safeItemWidth / aspectRatio;
111
+
112
+
113
+
114
+ return {
115
+ width: Math.floor(safeItemWidth),
116
+ height: Math.floor(safeItemHeight)
117
+ };
118
+ }
119
+
120
+ // AICODE-NOTE: Calculate thumbnail size within item with proper spacing
121
+ calculateThumbnailSize(itemWidth: number, itemHeight: number): number {
122
+ const textHeight = 40; // Reserve space for text and metadata
123
+ const itemPadding = 8; // Internal padding around thumbnail
124
+ const availableWidth = itemWidth - itemPadding;
125
+ const availableHeight = itemHeight - textHeight - itemPadding;
126
+
127
+ // Use the smaller dimension to ensure thumbnail fits properly
128
+ const maxThumbnailSize = Math.min(availableWidth, availableHeight);
129
+
130
+ // Ensure minimum thumbnail size for usability
131
+ return Math.max(32, Math.floor(maxThumbnailSize));
132
+ }
133
+
134
+ // AICODE-NOTE: Calculate layout for all items with precise thumbnail sizing
135
+ calculateLayout(totalItems: number): GridLayoutResult {
136
+ const itemsPerRow = this.calculateItemsPerRow();
137
+ const { width: itemWidth, height: itemHeight } = this.calculateItemDimensions(itemsPerRow);
138
+ const thumbnailSize = this.calculateThumbnailSize(itemWidth, itemHeight);
139
+ const textHeight = 60;
140
+
141
+ // AICODE-NOTE: Validate that layout doesn't exceed container width
142
+ const totalRowWidth = (itemsPerRow * itemWidth) + ((itemsPerRow - 1) * this.config.gap) + (this.config.padding * 2);
143
+
144
+ // console.log('🧮 [GRID CALCULATOR] Layout debug:', {
145
+ // totalItems,
146
+ // itemsPerRow,
147
+ // itemWidth,
148
+ // itemHeight,
149
+ // thumbnailSize,
150
+ // totalRowWidth,
151
+ // containerWidth: this.config.containerWidth,
152
+ // exceeds: totalRowWidth > this.config.containerWidth,
153
+ // config: this.config
154
+ // });
155
+
156
+ const totalRows = Math.ceil(totalItems / itemsPerRow);
157
+ const totalHeight = (totalRows * itemHeight) + ((totalRows - 1) * this.config.gap) + (this.config.padding * 2);
158
+
159
+ const items: GridItemLayout[] = [];
160
+
161
+ for (let i = 0; i < totalItems; i++) {
162
+ const row = Math.floor(i / itemsPerRow);
163
+ const col = i % itemsPerRow;
164
+
165
+ const x = this.config.padding + (col * (itemWidth + this.config.gap));
166
+ const y = this.config.padding + (row * (itemHeight + this.config.gap));
167
+
168
+ // AICODE-NOTE: Ensure item doesn't exceed container bounds
169
+ const clampedX = Math.min(x, this.config.containerWidth - itemWidth - this.config.padding);
170
+
171
+ items.push({
172
+ x: clampedX,
173
+ y,
174
+ width: itemWidth,
175
+ height: itemHeight,
176
+ thumbnailSize,
177
+ textHeight
178
+ });
179
+ }
180
+
181
+ return {
182
+ itemsPerRow,
183
+ itemWidth,
184
+ itemHeight,
185
+ totalRows,
186
+ totalHeight,
187
+ items
188
+ };
189
+ }
190
+
191
+ // AICODE-NOTE: Calculate visible items for virtualization
192
+ calculateVisibleItems(
193
+ totalItems: number,
194
+ scrollTop: number,
195
+ viewportHeight: number,
196
+ overscan: number = 2
197
+ ): { startIndex: number; endIndex: number; visibleItems: GridItemLayout[] } {
198
+ const layout = this.calculateLayout(totalItems);
199
+ const rowHeight = layout.itemHeight + this.config.gap;
200
+
201
+ // Calculate visible row range
202
+ const startRow = Math.max(0, Math.floor(scrollTop / rowHeight) - overscan);
203
+ const endRow = Math.min(
204
+ layout.totalRows - 1,
205
+ Math.ceil((scrollTop + viewportHeight) / rowHeight) + overscan
206
+ );
207
+
208
+ const startIndex = startRow * layout.itemsPerRow;
209
+ const endIndex = Math.min(totalItems - 1, (endRow + 1) * layout.itemsPerRow - 1);
210
+
211
+ const visibleItems = layout.items.slice(startIndex, endIndex + 1);
212
+
213
+ return {
214
+ startIndex,
215
+ endIndex,
216
+ visibleItems
217
+ };
218
+ }
219
+
220
+ // AICODE-NOTE: Get item position by index
221
+ getItemPosition(itemIndex: number, totalItems: number): GridItemLayout | null {
222
+ const layout = this.calculateLayout(totalItems);
223
+ return layout.items[itemIndex] || null;
224
+ }
225
+
226
+ // AICODE-NOTE: Find item at position (for hit testing)
227
+ getItemAtPosition(x: number, y: number, totalItems: number): number | null {
228
+ const layout = this.calculateLayout(totalItems);
229
+
230
+ for (let i = 0; i < layout.items.length; i++) {
231
+ const item = layout.items[i];
232
+ if (item &&
233
+ x >= item.x &&
234
+ x <= item.x + item.width &&
235
+ y >= item.y &&
236
+ y <= item.y + item.height
237
+ ) {
238
+ return i;
239
+ }
240
+ }
241
+
242
+ return null;
243
+ }
244
+ }
245
+
246
+ // AICODE-NOTE: Factory function for common grid configurations
247
+ export function createGridCalculator(
248
+ containerWidth: number,
249
+ containerHeight: number,
250
+ options: Partial<GridLayoutConfig> = {}
251
+ ): GridLayoutCalculator {
252
+ const defaultConfig: GridLayoutConfig = {
253
+ containerWidth,
254
+ containerHeight,
255
+ itemsPerRow: 'auto',
256
+ gap: 12,
257
+ padding: 16,
258
+ minItemWidth: 160,
259
+ maxItemWidth: 400,
260
+ aspectRatio: 0.9, // Slightly portrait for better thumbnails
261
+ ...options
262
+ };
263
+
264
+ return new GridLayoutCalculator(defaultConfig);
265
+ }
266
+
267
+ // AICODE-NOTE: Preset configurations for different use cases
268
+ export const GRID_PRESETS = {
269
+ compact: {
270
+ gap: 8,
271
+ padding: 12,
272
+ minItemWidth: 120,
273
+ maxItemWidth: 200,
274
+ aspectRatio: 0.85
275
+ },
276
+ comfortable: {
277
+ gap: 16,
278
+ padding: 20,
279
+ minItemWidth: 200,
280
+ maxItemWidth: 300,
281
+ aspectRatio: 0.9
282
+ },
283
+ spacious: {
284
+ gap: 24,
285
+ padding: 32,
286
+ minItemWidth: 280,
287
+ maxItemWidth: 400,
288
+ aspectRatio: 0.95
289
+ }
290
+ } as const;