@anymux/ui-kit 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (244) hide show
  1. package/dist/ExplorerLayout-CSIJd7N4.js +105 -0
  2. package/dist/ExplorerLayout-CSIJd7N4.js.map +1 -0
  3. package/dist/FileBrowserContext-B6jixa2j.js +11 -0
  4. package/dist/FileBrowserContext-B6jixa2j.js.map +1 -0
  5. package/dist/calendar-DSlrbHoj.js +761 -0
  6. package/dist/calendar-DSlrbHoj.js.map +1 -0
  7. package/dist/calendar.d.ts +3 -0
  8. package/dist/calendar.js +3 -0
  9. package/dist/contacts-DQXTZzHc.js +539 -0
  10. package/dist/contacts-DQXTZzHc.js.map +1 -0
  11. package/dist/contacts.d.ts +3 -0
  12. package/dist/contacts.js +3 -0
  13. package/dist/file-browser-m5atC3kF.js +6755 -0
  14. package/dist/file-browser-m5atC3kF.js.map +1 -0
  15. package/dist/file-browser.d.ts +11 -0
  16. package/dist/file-browser.js +9 -0
  17. package/dist/git-B55e6LL-.js +561 -0
  18. package/dist/git-B55e6LL-.js.map +1 -0
  19. package/dist/git.d.ts +2 -0
  20. package/dist/git.js +3 -0
  21. package/dist/iconMap-V4B8P-Uh.js +206 -0
  22. package/dist/iconMap-V4B8P-Uh.js.map +1 -0
  23. package/dist/icons-CIsIOZXR.js +0 -0
  24. package/dist/icons.d.ts +2 -0
  25. package/dist/icons.js +4 -0
  26. package/dist/index-BNmNIWBL.d.ts +71 -0
  27. package/dist/index-BNmNIWBL.d.ts.map +1 -0
  28. package/dist/index-Bryv_GCG.d.ts +1481 -0
  29. package/dist/index-Bryv_GCG.d.ts.map +1 -0
  30. package/dist/index-CuQIjSXs.d.ts +134 -0
  31. package/dist/index-CuQIjSXs.d.ts.map +1 -0
  32. package/dist/index-DSu19mq0.d.ts +153 -0
  33. package/dist/index-DSu19mq0.d.ts.map +1 -0
  34. package/dist/index-DmsyeHFr.d.ts +149 -0
  35. package/dist/index-DmsyeHFr.d.ts.map +1 -0
  36. package/dist/index-DxnJ8FYM.d.ts +17 -0
  37. package/dist/index-DxnJ8FYM.d.ts.map +1 -0
  38. package/dist/index-DzfY1Tok.d.ts +32 -0
  39. package/dist/index-DzfY1Tok.d.ts.map +1 -0
  40. package/dist/index-Ml_SgiKa.d.ts +1847 -0
  41. package/dist/index-Ml_SgiKa.d.ts.map +1 -0
  42. package/dist/index-kHr9udZD.d.ts +1025 -0
  43. package/dist/index-kHr9udZD.d.ts.map +1 -0
  44. package/dist/index.d.ts +11 -0
  45. package/dist/index.js +15 -0
  46. package/dist/layout-Ca_4r8ka.js +89 -0
  47. package/dist/layout-Ca_4r8ka.js.map +1 -0
  48. package/dist/layout.d.ts +2 -0
  49. package/dist/layout.js +5 -0
  50. package/dist/list-CxfT6hix.js +6831 -0
  51. package/dist/list-CxfT6hix.js.map +1 -0
  52. package/dist/list.d.ts +2 -0
  53. package/dist/list.js +5 -0
  54. package/dist/media-DZ292aKK.js +557 -0
  55. package/dist/media-DZ292aKK.js.map +1 -0
  56. package/dist/media.d.ts +3 -0
  57. package/dist/media.js +3 -0
  58. package/dist/tree-Dd9Z0Aso.js +3351 -0
  59. package/dist/tree-Dd9Z0Aso.js.map +1 -0
  60. package/dist/tree.d.ts +2 -0
  61. package/dist/tree.js +6 -0
  62. package/dist/types-common-CB3kRek8.d.ts +26 -0
  63. package/dist/types-common-CB3kRek8.d.ts.map +1 -0
  64. package/dist/utils-B4fdKKsy.js +3 -0
  65. package/package.json +109 -0
  66. package/src/calendar/AgendaView.tsx +37 -0
  67. package/src/calendar/CalendarBrowser.tsx +90 -0
  68. package/src/calendar/CalendarModel.ts +142 -0
  69. package/src/calendar/CalendarSidebar.tsx +81 -0
  70. package/src/calendar/DayView.tsx +76 -0
  71. package/src/calendar/EventCard.tsx +51 -0
  72. package/src/calendar/MockCalendarProvider.ts +98 -0
  73. package/src/calendar/MonthView.tsx +77 -0
  74. package/src/calendar/WeekView.tsx +129 -0
  75. package/src/calendar/index.ts +18 -0
  76. package/src/calendar/types.ts +25 -0
  77. package/src/contacts/ContactAvatar.tsx +35 -0
  78. package/src/contacts/ContactBrowser.tsx +56 -0
  79. package/src/contacts/ContactCard.tsx +37 -0
  80. package/src/contacts/ContactDetail.tsx +63 -0
  81. package/src/contacts/ContactGroupSidebar.tsx +40 -0
  82. package/src/contacts/ContactList.tsx +32 -0
  83. package/src/contacts/ContactListModel.ts +120 -0
  84. package/src/contacts/MockContactProvider.ts +77 -0
  85. package/src/contacts/index.ts +17 -0
  86. package/src/contacts/types.ts +26 -0
  87. package/src/demos/CalendarBrowserDemo.tsx +15 -0
  88. package/src/demos/ContactBrowserDemo.tsx +15 -0
  89. package/src/demos/MediaBrowserDemo.tsx +15 -0
  90. package/src/file-browser/adapters/DocumentViewerAdapter.ts +371 -0
  91. package/src/file-browser/adapters/FileSystemBridge.ts +168 -0
  92. package/src/file-browser/adapters/GitBrowserAdapter.ts +546 -0
  93. package/src/file-browser/adapters/README.md +504 -0
  94. package/src/file-browser/adapters/index.ts +27 -0
  95. package/src/file-browser/adapters/types.ts +70 -0
  96. package/src/file-browser/architecture.md +645 -0
  97. package/src/file-browser/components/CreateItemDialog.tsx +71 -0
  98. package/src/file-browser/components/DeleteConfirmDialog.tsx +58 -0
  99. package/src/file-browser/components/FileBrowser.tsx +473 -0
  100. package/src/file-browser/components/FileBrowserContent.tsx +209 -0
  101. package/src/file-browser/components/FileBrowserHeader.tsx +151 -0
  102. package/src/file-browser/components/FileBrowserToolbar.tsx +145 -0
  103. package/src/file-browser/components/LeftPanel/LeftPanel.tsx +103 -0
  104. package/src/file-browser/components/LeftPanel/LeftPanelTabs.tsx +70 -0
  105. package/src/file-browser/components/LeftPanel/TreeNavigationView.tsx +256 -0
  106. package/src/file-browser/components/PreviewPane.tsx +146 -0
  107. package/src/file-browser/components/RightPanel/FilePreview.tsx +219 -0
  108. package/src/file-browser/components/RightPanel/RightPanel.tsx +186 -0
  109. package/src/file-browser/components/RightPanel/RightPanelToolbar.tsx +113 -0
  110. package/src/file-browser/components/UploadProgress.tsx +123 -0
  111. package/src/file-browser/components/ViewerHost.tsx +208 -0
  112. package/src/file-browser/components/mobile/MobileNavigation.tsx +227 -0
  113. package/src/file-browser/components/navigation/NavigationButtons.tsx +171 -0
  114. package/src/file-browser/components/shared/ErrorBoundary.tsx +116 -0
  115. package/src/file-browser/components/shared/FileBrowserItem.tsx +195 -0
  116. package/src/file-browser/components/shared/FileIcon.tsx +169 -0
  117. package/src/file-browser/components/toolbar/ViewModeToggle.tsx +200 -0
  118. package/src/file-browser/components/views/ListView/ListView.tsx +484 -0
  119. package/src/file-browser/components/views/ThumbnailView/ThumbnailView.tsx +323 -0
  120. package/src/file-browser/components/views/TreeView/TreeNode.tsx +186 -0
  121. package/src/file-browser/components/views/TreeView/TreeNodeList.tsx +191 -0
  122. package/src/file-browser/components/views/TreeView/TreeView.tsx +200 -0
  123. package/src/file-browser/components/views/TreemapView/TreemapView.tsx +339 -0
  124. package/src/file-browser/context/FileBrowserContext.tsx +13 -0
  125. package/src/file-browser/examples/BasicUsage.tsx +20 -0
  126. package/src/file-browser/index.ts +98 -0
  127. package/src/file-browser/models/FileBrowserModel.ts +623 -0
  128. package/src/file-browser/models/LeftPanelManagerModel.ts +105 -0
  129. package/src/file-browser/models/NavigationManagerModel.ts +312 -0
  130. package/src/file-browser/models/ResponsiveLayoutManagerModel.ts +437 -0
  131. package/src/file-browser/models/RightPanelManagerModel.ts +190 -0
  132. package/src/file-browser/models/SelectionManagerModel.ts +252 -0
  133. package/src/file-browser/models/ToolbarManagerModel.ts +144 -0
  134. package/src/file-browser/models/UploadModel.ts +147 -0
  135. package/src/file-browser/models/ViewModeManagerModel.ts +185 -0
  136. package/src/file-browser/models/ViewerHostModel.ts +44 -0
  137. package/src/file-browser/models/ui/ListViewUIModel.ts +265 -0
  138. package/src/file-browser/models/ui/PreviewUIModel.ts +297 -0
  139. package/src/file-browser/models/ui/ThumbnailViewUIModel.ts +254 -0
  140. package/src/file-browser/models/ui/TreeViewUIModel.ts +128 -0
  141. package/src/file-browser/models/ui/TreemapViewUIModel.ts +350 -0
  142. package/src/file-browser/providers/FileSystemListProvider.ts +552 -0
  143. package/src/file-browser/providers/FileSystemProvider.ts +401 -0
  144. package/src/file-browser/providers/FileSystemTreeProvider.ts +231 -0
  145. package/src/file-browser/providers/GitProvider.ts +337 -0
  146. package/src/file-browser/providers/GitRepositoryProvider.ts +376 -0
  147. package/src/file-browser/providers/IFileBrowserProvider.ts +56 -0
  148. package/src/file-browser/providers/MemoryProvider.ts +303 -0
  149. package/src/file-browser/providers/index.ts +4 -0
  150. package/src/file-browser/registry/ViewerRegistry.ts +551 -0
  151. package/src/file-browser/registry/types.ts +144 -0
  152. package/src/file-browser/scripts/performanceBenchmark.ts +553 -0
  153. package/src/file-browser/services/ThumbnailCacheService.ts +128 -0
  154. package/src/file-browser/tasks.md +537 -0
  155. package/src/file-browser/types/FileBrowserTypes.ts +126 -0
  156. package/src/file-browser/types/ProviderTypes.ts +155 -0
  157. package/src/file-browser/types/UITypes.ts +235 -0
  158. package/src/file-browser/types/ViewModeTypes.ts +150 -0
  159. package/src/file-browser/utils/gestures.ts +327 -0
  160. package/src/file-browser/utils/performance.ts +563 -0
  161. package/src/file-browser/viewers/ImageViewer.tsx +163 -0
  162. package/src/file-browser/viewers/ImageViewerModel.ts +79 -0
  163. package/src/file-browser/viewers/TextViewer.tsx +95 -0
  164. package/src/file-browser/viewers/UnsupportedFileViewer.tsx +57 -0
  165. package/src/file-browser/viewers/index.ts +61 -0
  166. package/src/git/BranchList.tsx +128 -0
  167. package/src/git/CommitGraph.tsx +239 -0
  168. package/src/git/CommitList.tsx +258 -0
  169. package/src/git/DiffViewer.tsx +219 -0
  170. package/src/git/index.ts +4 -0
  171. package/src/icons/iconMap.ts +146 -0
  172. package/src/icons/index.ts +9 -0
  173. package/src/index.ts +13 -0
  174. package/src/layout/README.md +307 -0
  175. package/src/layout/components/ExplorerLayout/ExplorerLayout.tsx +178 -0
  176. package/src/layout/examples/SimpleExample.tsx +60 -0
  177. package/src/layout/index.ts +6 -0
  178. package/src/lib/utils.ts +1 -0
  179. package/src/list/README.md +303 -0
  180. package/src/list/architecture.md +807 -0
  181. package/src/list/components/CalculatedGridView.tsx +252 -0
  182. package/src/list/components/DragPreview.tsx +102 -0
  183. package/src/list/components/ListContextMenu.tsx +274 -0
  184. package/src/list/components/ListItem.tsx +761 -0
  185. package/src/list/components/ListItems.tsx +919 -0
  186. package/src/list/components/MasonryView.tsx +241 -0
  187. package/src/list/components/SearchFilter.tsx +44 -0
  188. package/src/list/components/TreemapView.tsx +709 -0
  189. package/src/list/components/ViewSizeControls.tsx +205 -0
  190. package/src/list/components/ViewTypeSelector.tsx +312 -0
  191. package/src/list/components/VirtualizedDetailsView.tsx +231 -0
  192. package/src/list/components/VirtualizedGrid.tsx +164 -0
  193. package/src/list/components/VirtualizedList.tsx +154 -0
  194. package/src/list/components/VirtualizedMasonryView.tsx +344 -0
  195. package/src/list/components/shared/EmptyState.tsx +103 -0
  196. package/src/list/components/shared/ErrorBoundary.tsx +123 -0
  197. package/src/list/components/shared/ErrorDisplay.tsx +100 -0
  198. package/src/list/components/shared/ListLoader.tsx +146 -0
  199. package/src/list/components/shared/LoadingIndicator.tsx +80 -0
  200. package/src/list/index.ts +92 -0
  201. package/src/list/models/ListItemsModel.ts +1301 -0
  202. package/src/list/models/TreemapModel.ts +204 -0
  203. package/src/list/providers/ListItemsProvider.ts +313 -0
  204. package/src/list/providers/TestListProvider.ts +604 -0
  205. package/src/list/tasks.md +937 -0
  206. package/src/list/types/ListTypes.ts +178 -0
  207. package/src/list/utils/BenchmarkLogger.ts +243 -0
  208. package/src/list/utils/DragDropManager.ts +320 -0
  209. package/src/list/utils/GridLayoutCalculator.ts +290 -0
  210. package/src/list/utils/ListAccessibility.ts +367 -0
  211. package/src/list/utils/ListKeyboard.ts +414 -0
  212. package/src/list/utils/MasonryLayoutCalculator.ts +302 -0
  213. package/src/list/utils/MasonryLayoutEngine.ts +401 -0
  214. package/src/list/utils/__tests__/MasonryLayoutEngine.test.ts +157 -0
  215. package/src/list/utils/__tests__/VirtualizedMasonryView.test.tsx +251 -0
  216. package/src/media/AlbumSidebar.tsx +48 -0
  217. package/src/media/MediaBrowser.tsx +92 -0
  218. package/src/media/MediaBrowserModel.ts +138 -0
  219. package/src/media/MediaGrid.tsx +50 -0
  220. package/src/media/MediaList.tsx +49 -0
  221. package/src/media/MediaPreview.tsx +63 -0
  222. package/src/media/MediaTimeline.tsx +38 -0
  223. package/src/media/MockMediaProvider.ts +70 -0
  224. package/src/media/index.ts +18 -0
  225. package/src/media/types.ts +21 -0
  226. package/src/styles/variables.css +60 -0
  227. package/src/tree/DEVELOPMENT_SUMMARY.md +170 -0
  228. package/src/tree/__tests__/TreeModel.test.ts +16 -0
  229. package/src/tree/architecture.md +530 -0
  230. package/src/tree/components/Tree.tsx +283 -0
  231. package/src/tree/components/TreeCheckbox.tsx +147 -0
  232. package/src/tree/components/TreeContextMenu.tsx +139 -0
  233. package/src/tree/components/TreeNodeList.tsx +329 -0
  234. package/src/tree/components/TreeTable.tsx +382 -0
  235. package/src/tree/index.ts +58 -0
  236. package/src/tree/models/TreeModel.ts +839 -0
  237. package/src/tree/providers/SimpleTreeProvider.ts +463 -0
  238. package/src/tree/providers/TestTreeProvider.ts +946 -0
  239. package/src/tree/providers/TreeProvider.ts +308 -0
  240. package/src/tree/tasks.md +2046 -0
  241. package/src/tree/types/TreeTypes.ts +279 -0
  242. package/src/tree/utils/SelectionTheme.ts +150 -0
  243. package/src/tree/utils/logger.ts +203 -0
  244. package/src/types-common.ts +21 -0
@@ -0,0 +1,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
+ });