@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,401 @@
|
|
|
1
|
+
// AICODE-NOTE: DOM-free masonry layout engine based on Masonry.js algorithm
|
|
2
|
+
// Supports both horizontal and vertical masonry with aspect ratio preservation
|
|
3
|
+
|
|
4
|
+
export interface MasonryItem {
|
|
5
|
+
id: string;
|
|
6
|
+
width: number;
|
|
7
|
+
height: number;
|
|
8
|
+
aspectRatio?: number; // width/height ratio
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface MasonryItemPosition {
|
|
12
|
+
id: string;
|
|
13
|
+
x: number;
|
|
14
|
+
y: number;
|
|
15
|
+
width: number;
|
|
16
|
+
height: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface MasonryLayoutResult {
|
|
20
|
+
items: MasonryItemPosition[];
|
|
21
|
+
totalHeight: number;
|
|
22
|
+
totalWidth: number;
|
|
23
|
+
columns: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface MasonryConfig {
|
|
27
|
+
containerWidth: number;
|
|
28
|
+
columnWidth?: number; // If not provided, calculated from first item or container
|
|
29
|
+
gutter: number;
|
|
30
|
+
horizontalOrder: boolean; // true for horizontal masonry, false for vertical
|
|
31
|
+
fitWidth?: boolean; // Fit container to used columns
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export class MasonryLayoutEngine {
|
|
35
|
+
private config: MasonryConfig;
|
|
36
|
+
private cache = new Map<string, MasonryLayoutResult>();
|
|
37
|
+
private cacheKey = '';
|
|
38
|
+
|
|
39
|
+
// Layout state
|
|
40
|
+
private cols: number = 0;
|
|
41
|
+
private colYs: number[] = [];
|
|
42
|
+
private maxY: number = 0;
|
|
43
|
+
private horizontalColIndex: number = 0;
|
|
44
|
+
private calculatedColumnWidth: number = 0;
|
|
45
|
+
|
|
46
|
+
// Horizontal masonry state
|
|
47
|
+
private rows: Array<{ y: number; height: number; items: Array<{ x: number; width: number }> }> = [];
|
|
48
|
+
|
|
49
|
+
// Public getter for calculated column width
|
|
50
|
+
get columnWidth(): number {
|
|
51
|
+
return this.calculatedColumnWidth;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
constructor(config: MasonryConfig) {
|
|
55
|
+
this.config = { ...config };
|
|
56
|
+
this.resetLayout();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
updateConfig(newConfig: Partial<MasonryConfig>): void {
|
|
60
|
+
this.config = { ...this.config, ...newConfig };
|
|
61
|
+
this.cache.clear(); // Clear cache when config changes
|
|
62
|
+
this.resetLayout();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
calculateLayout(items: MasonryItem[]): MasonryLayoutResult {
|
|
66
|
+
// Early return for empty items
|
|
67
|
+
if (items.length === 0) {
|
|
68
|
+
return {
|
|
69
|
+
items: [],
|
|
70
|
+
totalHeight: 0,
|
|
71
|
+
totalWidth: this.config.containerWidth,
|
|
72
|
+
columns: 0
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Generate cache key (optimized for large datasets)
|
|
77
|
+
const configKey = `${this.config.containerWidth}:${this.config.columnWidth}:${this.config.gutter}:${this.config.horizontalOrder}`;
|
|
78
|
+
const itemsHash = this.hashItems(items);
|
|
79
|
+
const cacheKey = `${configKey}:${itemsHash}`;
|
|
80
|
+
|
|
81
|
+
// Return cached result if available
|
|
82
|
+
if (this.cache.has(cacheKey)) {
|
|
83
|
+
return this.cache.get(cacheKey)!;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Calculate fresh layout
|
|
87
|
+
this.resetLayout();
|
|
88
|
+
this.measureColumns(items);
|
|
89
|
+
|
|
90
|
+
const positions: MasonryItemPosition[] = [];
|
|
91
|
+
|
|
92
|
+
for (const item of items) {
|
|
93
|
+
const position = this.getItemLayoutPosition(item);
|
|
94
|
+
positions.push({
|
|
95
|
+
id: item.id,
|
|
96
|
+
x: position.x,
|
|
97
|
+
y: position.y,
|
|
98
|
+
width: item.width,
|
|
99
|
+
height: item.height
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const result: MasonryLayoutResult = {
|
|
104
|
+
items: positions,
|
|
105
|
+
totalHeight: this.getContainerSize().height,
|
|
106
|
+
totalWidth: this.getContainerSize().width,
|
|
107
|
+
columns: this.cols
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
// Cache the result
|
|
111
|
+
this.cache.set(cacheKey, result);
|
|
112
|
+
|
|
113
|
+
// Limit cache size to prevent memory leaks
|
|
114
|
+
if (this.cache.size > 50) { // Reduced cache size for better memory usage
|
|
115
|
+
const firstKey = this.cache.keys().next().value;
|
|
116
|
+
if (firstKey) {
|
|
117
|
+
this.cache.delete(firstKey);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return result;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// AICODE-NOTE: Optimized hash function for large item arrays
|
|
125
|
+
private hashItems(items: MasonryItem[]): string {
|
|
126
|
+
if (items.length <= 100) {
|
|
127
|
+
// For small arrays, use detailed hash
|
|
128
|
+
return items.map(item => `${item.id}:${item.width}:${item.height}`).join('|');
|
|
129
|
+
} else {
|
|
130
|
+
// For large arrays, use summary hash to avoid performance issues
|
|
131
|
+
const summary = {
|
|
132
|
+
count: items.length,
|
|
133
|
+
firstId: items[0]?.id,
|
|
134
|
+
lastId: items[items.length - 1]?.id,
|
|
135
|
+
avgWidth: items.reduce((sum, item) => sum + item.width, 0) / items.length,
|
|
136
|
+
avgHeight: items.reduce((sum, item) => sum + item.height, 0) / items.length
|
|
137
|
+
};
|
|
138
|
+
return JSON.stringify(summary);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
private resetLayout(): void {
|
|
143
|
+
this.colYs = [];
|
|
144
|
+
this.maxY = 0;
|
|
145
|
+
this.horizontalColIndex = 0;
|
|
146
|
+
this.rows = [];
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
private measureColumns(items: MasonryItem[]): void {
|
|
150
|
+
// Calculate column width if not provided
|
|
151
|
+
if (!this.config.columnWidth) {
|
|
152
|
+
const firstItem = items[0];
|
|
153
|
+
this.calculatedColumnWidth = firstItem?.width || this.config.containerWidth;
|
|
154
|
+
} else {
|
|
155
|
+
this.calculatedColumnWidth = this.config.columnWidth;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const columnWidth = this.calculatedColumnWidth + this.config.gutter;
|
|
159
|
+
const containerWidth = this.config.containerWidth;
|
|
160
|
+
let cols = (containerWidth + this.config.gutter) / columnWidth;
|
|
161
|
+
|
|
162
|
+
// Fix rounding errors, typically with gutters
|
|
163
|
+
const excess = columnWidth - (containerWidth % columnWidth);
|
|
164
|
+
const mathMethod = excess && excess < 1 ? 'round' : 'floor';
|
|
165
|
+
cols = Math[mathMethod](cols);
|
|
166
|
+
this.cols = Math.max(cols, 1);
|
|
167
|
+
|
|
168
|
+
// Initialize column Y positions with initial gutter offset
|
|
169
|
+
this.colYs = [];
|
|
170
|
+
for (let i = 0; i < this.cols; i++) {
|
|
171
|
+
this.colYs.push(0);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
private getItemLayoutPosition(item: MasonryItem): { x: number; y: number } {
|
|
176
|
+
if (this.config.horizontalOrder) {
|
|
177
|
+
return this.getHorizontalLayoutPosition(item);
|
|
178
|
+
} else {
|
|
179
|
+
return this.getVerticalLayoutPosition(item);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
private getVerticalLayoutPosition(item: MasonryItem): { x: number; y: number } {
|
|
184
|
+
// Calculate how many columns this item spans
|
|
185
|
+
const remainder = item.width % this.calculatedColumnWidth;
|
|
186
|
+
const mathMethod = remainder && remainder < 1 ? 'round' : 'ceil';
|
|
187
|
+
let colSpan = Math[mathMethod](item.width / this.calculatedColumnWidth);
|
|
188
|
+
colSpan = Math.min(colSpan, this.cols);
|
|
189
|
+
|
|
190
|
+
const colPosition = this.getTopColPosition(colSpan);
|
|
191
|
+
|
|
192
|
+
// Calculate position: gutter on left edge, then (columnWidth + gutter) per column
|
|
193
|
+
const position = {
|
|
194
|
+
x: this.config.gutter + colPosition.col * (this.calculatedColumnWidth + this.config.gutter),
|
|
195
|
+
y: colPosition.y + this.config.gutter
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
// Update column heights with gutter spacing
|
|
199
|
+
const setHeight = colPosition.y + item.height + this.config.gutter;
|
|
200
|
+
const setMax = colSpan + colPosition.col;
|
|
201
|
+
for (let i = colPosition.col; i < setMax; i++) {
|
|
202
|
+
this.colYs[i] = setHeight;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
this.maxY = Math.max(this.maxY, setHeight);
|
|
206
|
+
|
|
207
|
+
return position;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
private getHorizontalLayoutPosition(item: MasonryItem): { x: number; y: number } {
|
|
211
|
+
const itemWidth = item.width;
|
|
212
|
+
const itemHeight = item.height;
|
|
213
|
+
const gutter = this.config.gutter;
|
|
214
|
+
const containerWidth = this.config.containerWidth;
|
|
215
|
+
|
|
216
|
+
// Find the row with the shortest current width or create new row
|
|
217
|
+
let targetRowIndex = -1;
|
|
218
|
+
let targetX = gutter; // Start with left margin
|
|
219
|
+
|
|
220
|
+
// Check existing rows to see if item fits
|
|
221
|
+
for (let i = 0; i < this.rows.length; i++) {
|
|
222
|
+
const row = this.rows[i];
|
|
223
|
+
if (!row) continue;
|
|
224
|
+
|
|
225
|
+
const currentRowWidth = row.items.reduce((sum, item) => sum + item.width + gutter, gutter);
|
|
226
|
+
|
|
227
|
+
// Check if item fits in this row
|
|
228
|
+
if (currentRowWidth + itemWidth + gutter <= containerWidth) {
|
|
229
|
+
targetRowIndex = i;
|
|
230
|
+
targetX = currentRowWidth;
|
|
231
|
+
break;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// If no existing row can fit the item, create a new row
|
|
236
|
+
if (targetRowIndex === -1) {
|
|
237
|
+
const lastRow = this.rows[this.rows.length - 1];
|
|
238
|
+
const newRowY = this.rows.length === 0 ? gutter :
|
|
239
|
+
(lastRow ? lastRow.y + lastRow.height + gutter : gutter);
|
|
240
|
+
|
|
241
|
+
this.rows.push({
|
|
242
|
+
y: newRowY,
|
|
243
|
+
height: itemHeight,
|
|
244
|
+
items: []
|
|
245
|
+
});
|
|
246
|
+
targetRowIndex = this.rows.length - 1;
|
|
247
|
+
targetX = gutter;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const targetRow = this.rows[targetRowIndex];
|
|
251
|
+
if (!targetRow) {
|
|
252
|
+
// Fallback: create a new row if somehow targetRow is undefined
|
|
253
|
+
this.rows.push({
|
|
254
|
+
y: gutter,
|
|
255
|
+
height: itemHeight,
|
|
256
|
+
items: []
|
|
257
|
+
});
|
|
258
|
+
targetRowIndex = this.rows.length - 1;
|
|
259
|
+
targetX = gutter;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const finalTargetRow = this.rows[targetRowIndex];
|
|
263
|
+
if (finalTargetRow) {
|
|
264
|
+
// Add item to the row
|
|
265
|
+
finalTargetRow.items.push({
|
|
266
|
+
x: targetX,
|
|
267
|
+
width: itemWidth
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
// Update row height to accommodate the tallest item
|
|
271
|
+
finalTargetRow.height = Math.max(finalTargetRow.height, itemHeight);
|
|
272
|
+
|
|
273
|
+
// Calculate total height
|
|
274
|
+
this.maxY = Math.max(this.maxY, finalTargetRow.y + finalTargetRow.height);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const finalRow = this.rows[targetRowIndex];
|
|
278
|
+
return {
|
|
279
|
+
x: targetX,
|
|
280
|
+
y: finalRow ? finalRow.y : gutter
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
private getTopColPosition(colSpan: number): { col: number; y: number } {
|
|
285
|
+
const colGroup = this.getTopColGroup(colSpan);
|
|
286
|
+
const minimumY = Math.min(...colGroup);
|
|
287
|
+
const colIndex = colGroup.indexOf(minimumY);
|
|
288
|
+
|
|
289
|
+
return {
|
|
290
|
+
col: colIndex >= 0 ? colIndex : 0,
|
|
291
|
+
y: minimumY
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
private getTopColGroup(colSpan: number): number[] {
|
|
296
|
+
if (colSpan < 2) {
|
|
297
|
+
return this.colYs;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const colGroup: number[] = [];
|
|
301
|
+
const groupCount = this.cols + 1 - colSpan;
|
|
302
|
+
|
|
303
|
+
for (let i = 0; i < groupCount; i++) {
|
|
304
|
+
colGroup[i] = this.getColGroupY(i, colSpan);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return colGroup;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
private getColGroupY(col: number, colSpan: number): number {
|
|
311
|
+
if (colSpan < 2) {
|
|
312
|
+
return this.colYs[col] ?? 0;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const groupColYs = this.colYs.slice(col, col + colSpan);
|
|
316
|
+
return groupColYs.length > 0 ? Math.max(...groupColYs) : 0;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
private getHorizontalColPosition(colSpan: number, item: MasonryItem): { col: number; y: number } {
|
|
320
|
+
let col = this.horizontalColIndex % this.cols;
|
|
321
|
+
const isOver = colSpan > 1 && col + colSpan > this.cols;
|
|
322
|
+
|
|
323
|
+
// Shift to next row if item can't fit on current row
|
|
324
|
+
col = isOver ? 0 : col;
|
|
325
|
+
|
|
326
|
+
// Don't let zero-size items take up space
|
|
327
|
+
const hasSize = item.width && item.height;
|
|
328
|
+
this.horizontalColIndex = hasSize ? col + colSpan : this.horizontalColIndex;
|
|
329
|
+
|
|
330
|
+
return {
|
|
331
|
+
col: col,
|
|
332
|
+
y: this.getColGroupY(col, colSpan)
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
private getContainerSize(): { height: number; width: number } {
|
|
337
|
+
// For vertical masonry, maxY comes from colYs (column heights).
|
|
338
|
+
// For horizontal masonry, maxY is accumulated in getHorizontalLayoutPosition
|
|
339
|
+
// from this.rows — do NOT overwrite it from colYs (which are never updated).
|
|
340
|
+
if (!this.config.horizontalOrder) {
|
|
341
|
+
this.maxY = this.colYs.length > 0 ? Math.max(...this.colYs) : 0;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const size = {
|
|
345
|
+
height: this.maxY + this.config.gutter, // Add final bottom gutter
|
|
346
|
+
width: this.config.containerWidth
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
if (this.config.fitWidth) {
|
|
350
|
+
size.width = this.getContainerFitWidth();
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
return size;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
private getContainerFitWidth(): number {
|
|
357
|
+
let unusedCols = 0;
|
|
358
|
+
let i = this.cols;
|
|
359
|
+
|
|
360
|
+
while (--i) {
|
|
361
|
+
if (this.colYs[i] !== 0) {
|
|
362
|
+
break;
|
|
363
|
+
}
|
|
364
|
+
unusedCols++;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
return (this.cols - unusedCols) * this.calculatedColumnWidth - this.config.gutter;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// Utility method to clear cache manually
|
|
371
|
+
clearCache(): void {
|
|
372
|
+
this.cache.clear();
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Get cache statistics for debugging
|
|
376
|
+
getCacheStats(): { size: number; keys: string[] } {
|
|
377
|
+
return {
|
|
378
|
+
size: this.cache.size,
|
|
379
|
+
keys: Array.from(this.cache.keys())
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Factory functions for common configurations
|
|
385
|
+
export function createVerticalMasonry(containerWidth: number, columnWidth?: number, gutter: number = 4): MasonryLayoutEngine {
|
|
386
|
+
return new MasonryLayoutEngine({
|
|
387
|
+
containerWidth,
|
|
388
|
+
columnWidth,
|
|
389
|
+
gutter,
|
|
390
|
+
horizontalOrder: false
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
export function createHorizontalMasonry(containerWidth: number, columnWidth?: number, gutter: number = 4): MasonryLayoutEngine {
|
|
395
|
+
return new MasonryLayoutEngine({
|
|
396
|
+
containerWidth,
|
|
397
|
+
columnWidth,
|
|
398
|
+
gutter,
|
|
399
|
+
horizontalOrder: true
|
|
400
|
+
});
|
|
401
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { MasonryLayoutEngine, createVerticalMasonry, createHorizontalMasonry, MasonryItem } from '../MasonryLayoutEngine';
|
|
2
|
+
|
|
3
|
+
describe('MasonryLayoutEngine', () => {
|
|
4
|
+
const containerWidth = 800;
|
|
5
|
+
const columnWidth = 200;
|
|
6
|
+
const gutter = 16;
|
|
7
|
+
|
|
8
|
+
const createTestItems = (): MasonryItem[] => [
|
|
9
|
+
{ id: '1', width: 200, height: 300, aspectRatio: 2/3 }, // Portrait
|
|
10
|
+
{ id: '2', width: 200, height: 150, aspectRatio: 4/3 }, // Landscape
|
|
11
|
+
{ id: '3', width: 200, height: 200, aspectRatio: 1 }, // Square
|
|
12
|
+
{ id: '4', width: 200, height: 400, aspectRatio: 1/2 }, // Tall portrait
|
|
13
|
+
{ id: '5', width: 200, height: 100, aspectRatio: 2 }, // Wide landscape
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
describe('Vertical Masonry', () => {
|
|
17
|
+
it('should create vertical masonry layout', () => {
|
|
18
|
+
const engine = createVerticalMasonry(containerWidth, columnWidth, gutter);
|
|
19
|
+
expect(engine).toBeInstanceOf(MasonryLayoutEngine);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('should calculate layout for vertical masonry', () => {
|
|
23
|
+
const engine = createVerticalMasonry(containerWidth, columnWidth, gutter);
|
|
24
|
+
const items = createTestItems();
|
|
25
|
+
|
|
26
|
+
const result = engine.calculateLayout(items);
|
|
27
|
+
|
|
28
|
+
expect(result.items).toHaveLength(items.length);
|
|
29
|
+
expect(result.totalHeight).toBeGreaterThan(0);
|
|
30
|
+
expect(result.totalWidth).toBe(containerWidth);
|
|
31
|
+
expect(result.columns).toBeGreaterThan(0);
|
|
32
|
+
|
|
33
|
+
// Check that all items have valid positions
|
|
34
|
+
result.items.forEach(item => {
|
|
35
|
+
expect(item.x).toBeGreaterThanOrEqual(0);
|
|
36
|
+
expect(item.y).toBeGreaterThanOrEqual(0);
|
|
37
|
+
expect(item.width).toBeGreaterThan(0);
|
|
38
|
+
expect(item.height).toBeGreaterThan(0);
|
|
39
|
+
expect(item.id).toBeTruthy();
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('should pack items efficiently in vertical masonry', () => {
|
|
44
|
+
const engine = createVerticalMasonry(containerWidth, columnWidth, gutter);
|
|
45
|
+
const items = createTestItems();
|
|
46
|
+
|
|
47
|
+
const result = engine.calculateLayout(items);
|
|
48
|
+
|
|
49
|
+
// Items should be distributed across columns
|
|
50
|
+
const xPositions = [...new Set(result.items.map(item => item.x))];
|
|
51
|
+
expect(xPositions.length).toBeGreaterThan(1); // Multiple columns used
|
|
52
|
+
|
|
53
|
+
// Items should start from gutter offset in at least one column
|
|
54
|
+
const minY = Math.min(...result.items.map(item => item.y));
|
|
55
|
+
expect(minY).toBe(gutter);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe('Horizontal Masonry', () => {
|
|
60
|
+
it('should create horizontal masonry layout', () => {
|
|
61
|
+
const engine = createHorizontalMasonry(containerWidth, columnWidth, gutter);
|
|
62
|
+
expect(engine).toBeInstanceOf(MasonryLayoutEngine);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('should calculate layout for horizontal masonry', () => {
|
|
66
|
+
const engine = createHorizontalMasonry(containerWidth, columnWidth, gutter);
|
|
67
|
+
const items = createTestItems();
|
|
68
|
+
|
|
69
|
+
const result = engine.calculateLayout(items);
|
|
70
|
+
|
|
71
|
+
expect(result.items).toHaveLength(items.length);
|
|
72
|
+
expect(result.totalHeight).toBeGreaterThan(0);
|
|
73
|
+
expect(result.totalWidth).toBe(containerWidth);
|
|
74
|
+
expect(result.columns).toBeGreaterThan(0);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('should arrange items differently than vertical masonry', () => {
|
|
78
|
+
const verticalEngine = createVerticalMasonry(containerWidth, columnWidth, gutter);
|
|
79
|
+
const horizontalEngine = createHorizontalMasonry(containerWidth, columnWidth, gutter);
|
|
80
|
+
const items = createTestItems();
|
|
81
|
+
|
|
82
|
+
const verticalResult = verticalEngine.calculateLayout(items);
|
|
83
|
+
const horizontalResult = horizontalEngine.calculateLayout(items);
|
|
84
|
+
|
|
85
|
+
// Results should be different (different positioning algorithm)
|
|
86
|
+
const verticalPositions = verticalResult.items.map(item => `${item.x},${item.y}`).join('|');
|
|
87
|
+
const horizontalPositions = horizontalResult.items.map(item => `${item.x},${item.y}`).join('|');
|
|
88
|
+
|
|
89
|
+
expect(verticalPositions).not.toBe(horizontalPositions);
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
describe('Caching', () => {
|
|
94
|
+
it('should cache layout results', () => {
|
|
95
|
+
const engine = createVerticalMasonry(containerWidth, columnWidth, gutter);
|
|
96
|
+
const items = createTestItems();
|
|
97
|
+
|
|
98
|
+
// First calculation
|
|
99
|
+
const result1 = engine.calculateLayout(items);
|
|
100
|
+
const stats1 = engine.getCacheStats();
|
|
101
|
+
|
|
102
|
+
// Second calculation with same items
|
|
103
|
+
const result2 = engine.calculateLayout(items);
|
|
104
|
+
const stats2 = engine.getCacheStats();
|
|
105
|
+
|
|
106
|
+
// Results should be identical (cached)
|
|
107
|
+
expect(result1).toBe(result2);
|
|
108
|
+
expect(stats1.size).toBe(1);
|
|
109
|
+
expect(stats2.size).toBe(1);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('should clear cache when config changes', () => {
|
|
113
|
+
const engine = createVerticalMasonry(containerWidth, columnWidth, gutter);
|
|
114
|
+
const items = createTestItems();
|
|
115
|
+
|
|
116
|
+
// First calculation
|
|
117
|
+
engine.calculateLayout(items);
|
|
118
|
+
expect(engine.getCacheStats().size).toBe(1);
|
|
119
|
+
|
|
120
|
+
// Update config
|
|
121
|
+
engine.updateConfig({ containerWidth: containerWidth + 100 });
|
|
122
|
+
expect(engine.getCacheStats().size).toBe(0);
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
describe('Edge Cases', () => {
|
|
127
|
+
it('should handle empty items array', () => {
|
|
128
|
+
const engine = createVerticalMasonry(containerWidth, columnWidth, gutter);
|
|
129
|
+
const result = engine.calculateLayout([]);
|
|
130
|
+
|
|
131
|
+
expect(result.items).toHaveLength(0);
|
|
132
|
+
expect(result.totalHeight).toBe(0);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('should handle single item', () => {
|
|
136
|
+
const engine = createVerticalMasonry(containerWidth, columnWidth, gutter);
|
|
137
|
+
const items = [{ id: '1', width: 200, height: 300, aspectRatio: 2/3 }];
|
|
138
|
+
|
|
139
|
+
const result = engine.calculateLayout(items);
|
|
140
|
+
|
|
141
|
+
expect(result.items).toHaveLength(1);
|
|
142
|
+
expect(result.items[0]?.x).toBe(gutter); // First item has gutter offset
|
|
143
|
+
expect(result.items[0]?.y).toBe(gutter);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('should handle very wide items', () => {
|
|
147
|
+
const engine = createVerticalMasonry(containerWidth, columnWidth, gutter);
|
|
148
|
+
const items = [{ id: '1', width: containerWidth, height: 200, aspectRatio: 4 }];
|
|
149
|
+
|
|
150
|
+
const result = engine.calculateLayout(items);
|
|
151
|
+
|
|
152
|
+
expect(result.items).toHaveLength(1);
|
|
153
|
+
expect(result.items[0]?.x).toBe(gutter); // First item has gutter offset
|
|
154
|
+
expect(result.items[0]?.y).toBe(gutter);
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
});
|