@anymux/ui-kit 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ExplorerLayout-CSIJd7N4.js +105 -0
- package/dist/ExplorerLayout-CSIJd7N4.js.map +1 -0
- package/dist/FileBrowserContext-B6jixa2j.js +11 -0
- package/dist/FileBrowserContext-B6jixa2j.js.map +1 -0
- package/dist/calendar-DSlrbHoj.js +761 -0
- package/dist/calendar-DSlrbHoj.js.map +1 -0
- package/dist/calendar.d.ts +3 -0
- package/dist/calendar.js +3 -0
- package/dist/contacts-DQXTZzHc.js +539 -0
- package/dist/contacts-DQXTZzHc.js.map +1 -0
- package/dist/contacts.d.ts +3 -0
- package/dist/contacts.js +3 -0
- package/dist/file-browser-m5atC3kF.js +6755 -0
- package/dist/file-browser-m5atC3kF.js.map +1 -0
- package/dist/file-browser.d.ts +11 -0
- package/dist/file-browser.js +9 -0
- package/dist/git-B55e6LL-.js +561 -0
- package/dist/git-B55e6LL-.js.map +1 -0
- package/dist/git.d.ts +2 -0
- package/dist/git.js +3 -0
- package/dist/iconMap-V4B8P-Uh.js +206 -0
- package/dist/iconMap-V4B8P-Uh.js.map +1 -0
- package/dist/icons-CIsIOZXR.js +0 -0
- package/dist/icons.d.ts +2 -0
- package/dist/icons.js +4 -0
- package/dist/index-BNmNIWBL.d.ts +71 -0
- package/dist/index-BNmNIWBL.d.ts.map +1 -0
- package/dist/index-Bryv_GCG.d.ts +1481 -0
- package/dist/index-Bryv_GCG.d.ts.map +1 -0
- package/dist/index-CuQIjSXs.d.ts +134 -0
- package/dist/index-CuQIjSXs.d.ts.map +1 -0
- package/dist/index-DSu19mq0.d.ts +153 -0
- package/dist/index-DSu19mq0.d.ts.map +1 -0
- package/dist/index-DmsyeHFr.d.ts +149 -0
- package/dist/index-DmsyeHFr.d.ts.map +1 -0
- package/dist/index-DxnJ8FYM.d.ts +17 -0
- package/dist/index-DxnJ8FYM.d.ts.map +1 -0
- package/dist/index-DzfY1Tok.d.ts +32 -0
- package/dist/index-DzfY1Tok.d.ts.map +1 -0
- package/dist/index-Ml_SgiKa.d.ts +1847 -0
- package/dist/index-Ml_SgiKa.d.ts.map +1 -0
- package/dist/index-kHr9udZD.d.ts +1025 -0
- package/dist/index-kHr9udZD.d.ts.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +15 -0
- package/dist/layout-Ca_4r8ka.js +89 -0
- package/dist/layout-Ca_4r8ka.js.map +1 -0
- package/dist/layout.d.ts +2 -0
- package/dist/layout.js +5 -0
- package/dist/list-CxfT6hix.js +6831 -0
- package/dist/list-CxfT6hix.js.map +1 -0
- package/dist/list.d.ts +2 -0
- package/dist/list.js +5 -0
- package/dist/media-DZ292aKK.js +557 -0
- package/dist/media-DZ292aKK.js.map +1 -0
- package/dist/media.d.ts +3 -0
- package/dist/media.js +3 -0
- package/dist/tree-Dd9Z0Aso.js +3351 -0
- package/dist/tree-Dd9Z0Aso.js.map +1 -0
- package/dist/tree.d.ts +2 -0
- package/dist/tree.js +6 -0
- package/dist/types-common-CB3kRek8.d.ts +26 -0
- package/dist/types-common-CB3kRek8.d.ts.map +1 -0
- package/dist/utils-B4fdKKsy.js +3 -0
- package/package.json +109 -0
- package/src/calendar/AgendaView.tsx +37 -0
- package/src/calendar/CalendarBrowser.tsx +90 -0
- package/src/calendar/CalendarModel.ts +142 -0
- package/src/calendar/CalendarSidebar.tsx +81 -0
- package/src/calendar/DayView.tsx +76 -0
- package/src/calendar/EventCard.tsx +51 -0
- package/src/calendar/MockCalendarProvider.ts +98 -0
- package/src/calendar/MonthView.tsx +77 -0
- package/src/calendar/WeekView.tsx +129 -0
- package/src/calendar/index.ts +18 -0
- package/src/calendar/types.ts +25 -0
- package/src/contacts/ContactAvatar.tsx +35 -0
- package/src/contacts/ContactBrowser.tsx +56 -0
- package/src/contacts/ContactCard.tsx +37 -0
- package/src/contacts/ContactDetail.tsx +63 -0
- package/src/contacts/ContactGroupSidebar.tsx +40 -0
- package/src/contacts/ContactList.tsx +32 -0
- package/src/contacts/ContactListModel.ts +120 -0
- package/src/contacts/MockContactProvider.ts +77 -0
- package/src/contacts/index.ts +17 -0
- package/src/contacts/types.ts +26 -0
- package/src/demos/CalendarBrowserDemo.tsx +15 -0
- package/src/demos/ContactBrowserDemo.tsx +15 -0
- package/src/demos/MediaBrowserDemo.tsx +15 -0
- package/src/file-browser/adapters/DocumentViewerAdapter.ts +371 -0
- package/src/file-browser/adapters/FileSystemBridge.ts +168 -0
- package/src/file-browser/adapters/GitBrowserAdapter.ts +546 -0
- package/src/file-browser/adapters/README.md +504 -0
- package/src/file-browser/adapters/index.ts +27 -0
- package/src/file-browser/adapters/types.ts +70 -0
- package/src/file-browser/architecture.md +645 -0
- package/src/file-browser/components/CreateItemDialog.tsx +71 -0
- package/src/file-browser/components/DeleteConfirmDialog.tsx +58 -0
- package/src/file-browser/components/FileBrowser.tsx +473 -0
- package/src/file-browser/components/FileBrowserContent.tsx +209 -0
- package/src/file-browser/components/FileBrowserHeader.tsx +151 -0
- package/src/file-browser/components/FileBrowserToolbar.tsx +145 -0
- package/src/file-browser/components/LeftPanel/LeftPanel.tsx +103 -0
- package/src/file-browser/components/LeftPanel/LeftPanelTabs.tsx +70 -0
- package/src/file-browser/components/LeftPanel/TreeNavigationView.tsx +256 -0
- package/src/file-browser/components/PreviewPane.tsx +146 -0
- package/src/file-browser/components/RightPanel/FilePreview.tsx +219 -0
- package/src/file-browser/components/RightPanel/RightPanel.tsx +186 -0
- package/src/file-browser/components/RightPanel/RightPanelToolbar.tsx +113 -0
- package/src/file-browser/components/UploadProgress.tsx +123 -0
- package/src/file-browser/components/ViewerHost.tsx +208 -0
- package/src/file-browser/components/mobile/MobileNavigation.tsx +227 -0
- package/src/file-browser/components/navigation/NavigationButtons.tsx +171 -0
- package/src/file-browser/components/shared/ErrorBoundary.tsx +116 -0
- package/src/file-browser/components/shared/FileBrowserItem.tsx +195 -0
- package/src/file-browser/components/shared/FileIcon.tsx +169 -0
- package/src/file-browser/components/toolbar/ViewModeToggle.tsx +200 -0
- package/src/file-browser/components/views/ListView/ListView.tsx +484 -0
- package/src/file-browser/components/views/ThumbnailView/ThumbnailView.tsx +323 -0
- package/src/file-browser/components/views/TreeView/TreeNode.tsx +186 -0
- package/src/file-browser/components/views/TreeView/TreeNodeList.tsx +191 -0
- package/src/file-browser/components/views/TreeView/TreeView.tsx +200 -0
- package/src/file-browser/components/views/TreemapView/TreemapView.tsx +339 -0
- package/src/file-browser/context/FileBrowserContext.tsx +13 -0
- package/src/file-browser/examples/BasicUsage.tsx +20 -0
- package/src/file-browser/index.ts +98 -0
- package/src/file-browser/models/FileBrowserModel.ts +623 -0
- package/src/file-browser/models/LeftPanelManagerModel.ts +105 -0
- package/src/file-browser/models/NavigationManagerModel.ts +312 -0
- package/src/file-browser/models/ResponsiveLayoutManagerModel.ts +437 -0
- package/src/file-browser/models/RightPanelManagerModel.ts +190 -0
- package/src/file-browser/models/SelectionManagerModel.ts +252 -0
- package/src/file-browser/models/ToolbarManagerModel.ts +144 -0
- package/src/file-browser/models/UploadModel.ts +147 -0
- package/src/file-browser/models/ViewModeManagerModel.ts +185 -0
- package/src/file-browser/models/ViewerHostModel.ts +44 -0
- package/src/file-browser/models/ui/ListViewUIModel.ts +265 -0
- package/src/file-browser/models/ui/PreviewUIModel.ts +297 -0
- package/src/file-browser/models/ui/ThumbnailViewUIModel.ts +254 -0
- package/src/file-browser/models/ui/TreeViewUIModel.ts +128 -0
- package/src/file-browser/models/ui/TreemapViewUIModel.ts +350 -0
- package/src/file-browser/providers/FileSystemListProvider.ts +552 -0
- package/src/file-browser/providers/FileSystemProvider.ts +401 -0
- package/src/file-browser/providers/FileSystemTreeProvider.ts +231 -0
- package/src/file-browser/providers/GitProvider.ts +337 -0
- package/src/file-browser/providers/GitRepositoryProvider.ts +376 -0
- package/src/file-browser/providers/IFileBrowserProvider.ts +56 -0
- package/src/file-browser/providers/MemoryProvider.ts +303 -0
- package/src/file-browser/providers/index.ts +4 -0
- package/src/file-browser/registry/ViewerRegistry.ts +551 -0
- package/src/file-browser/registry/types.ts +144 -0
- package/src/file-browser/scripts/performanceBenchmark.ts +553 -0
- package/src/file-browser/services/ThumbnailCacheService.ts +128 -0
- package/src/file-browser/tasks.md +537 -0
- package/src/file-browser/types/FileBrowserTypes.ts +126 -0
- package/src/file-browser/types/ProviderTypes.ts +155 -0
- package/src/file-browser/types/UITypes.ts +235 -0
- package/src/file-browser/types/ViewModeTypes.ts +150 -0
- package/src/file-browser/utils/gestures.ts +327 -0
- package/src/file-browser/utils/performance.ts +563 -0
- package/src/file-browser/viewers/ImageViewer.tsx +163 -0
- package/src/file-browser/viewers/ImageViewerModel.ts +79 -0
- package/src/file-browser/viewers/TextViewer.tsx +95 -0
- package/src/file-browser/viewers/UnsupportedFileViewer.tsx +57 -0
- package/src/file-browser/viewers/index.ts +61 -0
- package/src/git/BranchList.tsx +128 -0
- package/src/git/CommitGraph.tsx +239 -0
- package/src/git/CommitList.tsx +258 -0
- package/src/git/DiffViewer.tsx +219 -0
- package/src/git/index.ts +4 -0
- package/src/icons/iconMap.ts +146 -0
- package/src/icons/index.ts +9 -0
- package/src/index.ts +13 -0
- package/src/layout/README.md +307 -0
- package/src/layout/components/ExplorerLayout/ExplorerLayout.tsx +178 -0
- package/src/layout/examples/SimpleExample.tsx +60 -0
- package/src/layout/index.ts +6 -0
- package/src/lib/utils.ts +1 -0
- package/src/list/README.md +303 -0
- package/src/list/architecture.md +807 -0
- package/src/list/components/CalculatedGridView.tsx +252 -0
- package/src/list/components/DragPreview.tsx +102 -0
- package/src/list/components/ListContextMenu.tsx +274 -0
- package/src/list/components/ListItem.tsx +761 -0
- package/src/list/components/ListItems.tsx +919 -0
- package/src/list/components/MasonryView.tsx +241 -0
- package/src/list/components/SearchFilter.tsx +44 -0
- package/src/list/components/TreemapView.tsx +709 -0
- package/src/list/components/ViewSizeControls.tsx +205 -0
- package/src/list/components/ViewTypeSelector.tsx +312 -0
- package/src/list/components/VirtualizedDetailsView.tsx +231 -0
- package/src/list/components/VirtualizedGrid.tsx +164 -0
- package/src/list/components/VirtualizedList.tsx +154 -0
- package/src/list/components/VirtualizedMasonryView.tsx +344 -0
- package/src/list/components/shared/EmptyState.tsx +103 -0
- package/src/list/components/shared/ErrorBoundary.tsx +123 -0
- package/src/list/components/shared/ErrorDisplay.tsx +100 -0
- package/src/list/components/shared/ListLoader.tsx +146 -0
- package/src/list/components/shared/LoadingIndicator.tsx +80 -0
- package/src/list/index.ts +92 -0
- package/src/list/models/ListItemsModel.ts +1301 -0
- package/src/list/models/TreemapModel.ts +204 -0
- package/src/list/providers/ListItemsProvider.ts +313 -0
- package/src/list/providers/TestListProvider.ts +604 -0
- package/src/list/tasks.md +937 -0
- package/src/list/types/ListTypes.ts +178 -0
- package/src/list/utils/BenchmarkLogger.ts +243 -0
- package/src/list/utils/DragDropManager.ts +320 -0
- package/src/list/utils/GridLayoutCalculator.ts +290 -0
- package/src/list/utils/ListAccessibility.ts +367 -0
- package/src/list/utils/ListKeyboard.ts +414 -0
- package/src/list/utils/MasonryLayoutCalculator.ts +302 -0
- package/src/list/utils/MasonryLayoutEngine.ts +401 -0
- package/src/list/utils/__tests__/MasonryLayoutEngine.test.ts +157 -0
- package/src/list/utils/__tests__/VirtualizedMasonryView.test.tsx +251 -0
- package/src/media/AlbumSidebar.tsx +48 -0
- package/src/media/MediaBrowser.tsx +92 -0
- package/src/media/MediaBrowserModel.ts +138 -0
- package/src/media/MediaGrid.tsx +50 -0
- package/src/media/MediaList.tsx +49 -0
- package/src/media/MediaPreview.tsx +63 -0
- package/src/media/MediaTimeline.tsx +38 -0
- package/src/media/MockMediaProvider.ts +70 -0
- package/src/media/index.ts +18 -0
- package/src/media/types.ts +21 -0
- package/src/styles/variables.css +60 -0
- package/src/tree/DEVELOPMENT_SUMMARY.md +170 -0
- package/src/tree/__tests__/TreeModel.test.ts +16 -0
- package/src/tree/architecture.md +530 -0
- package/src/tree/components/Tree.tsx +283 -0
- package/src/tree/components/TreeCheckbox.tsx +147 -0
- package/src/tree/components/TreeContextMenu.tsx +139 -0
- package/src/tree/components/TreeNodeList.tsx +329 -0
- package/src/tree/components/TreeTable.tsx +382 -0
- package/src/tree/index.ts +58 -0
- package/src/tree/models/TreeModel.ts +839 -0
- package/src/tree/providers/SimpleTreeProvider.ts +463 -0
- package/src/tree/providers/TestTreeProvider.ts +946 -0
- package/src/tree/providers/TreeProvider.ts +308 -0
- package/src/tree/tasks.md +2046 -0
- package/src/tree/types/TreeTypes.ts +279 -0
- package/src/tree/utils/SelectionTheme.ts +150 -0
- package/src/tree/utils/logger.ts +203 -0
- package/src/types-common.ts +21 -0
|
@@ -0,0 +1,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;
|