@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,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;
|