mtrl-addons 0.2.2 → 0.2.3

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 (128) hide show
  1. package/{src/components/index.ts → dist/components/index.d.ts} +0 -2
  2. package/dist/components/vlist/config.d.ts +86 -0
  3. package/{src/components/vlist/constants.ts → dist/components/vlist/constants.d.ts} +10 -11
  4. package/dist/components/vlist/features/api.d.ts +7 -0
  5. package/{src/components/vlist/features/index.ts → dist/components/vlist/features/index.d.ts} +0 -2
  6. package/dist/components/vlist/features/selection.d.ts +6 -0
  7. package/dist/components/vlist/features/viewport.d.ts +9 -0
  8. package/dist/components/vlist/features.d.ts +31 -0
  9. package/{src/components/vlist/index.ts → dist/components/vlist/index.d.ts} +1 -10
  10. package/dist/components/vlist/types.d.ts +596 -0
  11. package/dist/components/vlist/vlist.d.ts +29 -0
  12. package/dist/core/compose/features/gestures/index.d.ts +86 -0
  13. package/dist/core/compose/features/gestures/longpress.d.ts +85 -0
  14. package/dist/core/compose/features/gestures/pan.d.ts +108 -0
  15. package/dist/core/compose/features/gestures/pinch.d.ts +111 -0
  16. package/dist/core/compose/features/gestures/rotate.d.ts +111 -0
  17. package/dist/core/compose/features/gestures/swipe.d.ts +149 -0
  18. package/dist/core/compose/features/gestures/tap.d.ts +79 -0
  19. package/{src/core/compose/features/index.ts → dist/core/compose/features/index.d.ts} +1 -2
  20. package/{src/core/compose/index.ts → dist/core/compose/index.d.ts} +2 -11
  21. package/{src/core/gestures/index.ts → dist/core/gestures/index.d.ts} +1 -20
  22. package/dist/core/gestures/longpress.d.ts +23 -0
  23. package/dist/core/gestures/manager.d.ts +14 -0
  24. package/dist/core/gestures/pan.d.ts +12 -0
  25. package/dist/core/gestures/pinch.d.ts +14 -0
  26. package/dist/core/gestures/rotate.d.ts +14 -0
  27. package/dist/core/gestures/swipe.d.ts +20 -0
  28. package/dist/core/gestures/tap.d.ts +12 -0
  29. package/dist/core/gestures/types.d.ts +320 -0
  30. package/dist/core/gestures/utils.d.ts +57 -0
  31. package/dist/core/index.d.ts +13 -0
  32. package/dist/core/layout/config.d.ts +33 -0
  33. package/dist/core/layout/index.d.ts +51 -0
  34. package/dist/core/layout/jsx.d.ts +65 -0
  35. package/dist/core/layout/schema.d.ts +112 -0
  36. package/dist/core/layout/types.d.ts +69 -0
  37. package/dist/core/viewport/constants.d.ts +105 -0
  38. package/dist/core/viewport/features/base.d.ts +14 -0
  39. package/dist/core/viewport/features/collection.d.ts +41 -0
  40. package/dist/core/viewport/features/events.d.ts +13 -0
  41. package/{src/core/viewport/features/index.ts → dist/core/viewport/features/index.d.ts} +0 -7
  42. package/dist/core/viewport/features/item-size.d.ts +30 -0
  43. package/dist/core/viewport/features/loading.d.ts +34 -0
  44. package/dist/core/viewport/features/momentum.d.ts +17 -0
  45. package/dist/core/viewport/features/performance.d.ts +53 -0
  46. package/dist/core/viewport/features/placeholders.d.ts +38 -0
  47. package/dist/core/viewport/features/rendering.d.ts +16 -0
  48. package/dist/core/viewport/features/scrollbar.d.ts +26 -0
  49. package/dist/core/viewport/features/scrolling.d.ts +16 -0
  50. package/dist/core/viewport/features/utils.d.ts +43 -0
  51. package/dist/core/viewport/features/virtual.d.ts +18 -0
  52. package/{src/core/viewport/index.ts → dist/core/viewport/index.d.ts} +1 -17
  53. package/dist/core/viewport/types.d.ts +96 -0
  54. package/dist/core/viewport/utils/speed-tracker.d.ts +22 -0
  55. package/dist/core/viewport/viewport.d.ts +11 -0
  56. package/{src/index.ts → dist/index.d.ts} +0 -4
  57. package/dist/index.js +5143 -0
  58. package/dist/index.mjs +5111 -0
  59. package/dist/styles.css +254 -0
  60. package/dist/styles.css.map +1 -0
  61. package/package.json +5 -1
  62. package/.cursorrules +0 -117
  63. package/AI.md +0 -39
  64. package/CLAUDE.md +0 -882
  65. package/build.js +0 -377
  66. package/scripts/analyze-orphaned-functions.ts +0 -387
  67. package/scripts/debug/vlist-selection.ts +0 -121
  68. package/src/components/vlist/config.ts +0 -323
  69. package/src/components/vlist/features/api.ts +0 -626
  70. package/src/components/vlist/features/selection.ts +0 -436
  71. package/src/components/vlist/features/viewport.ts +0 -59
  72. package/src/components/vlist/features.ts +0 -112
  73. package/src/components/vlist/types.ts +0 -723
  74. package/src/components/vlist/vlist.ts +0 -92
  75. package/src/core/compose/features/gestures/index.ts +0 -227
  76. package/src/core/compose/features/gestures/longpress.ts +0 -383
  77. package/src/core/compose/features/gestures/pan.ts +0 -424
  78. package/src/core/compose/features/gestures/pinch.ts +0 -475
  79. package/src/core/compose/features/gestures/rotate.ts +0 -485
  80. package/src/core/compose/features/gestures/swipe.ts +0 -492
  81. package/src/core/compose/features/gestures/tap.ts +0 -334
  82. package/src/core/gestures/longpress.ts +0 -68
  83. package/src/core/gestures/manager.ts +0 -418
  84. package/src/core/gestures/pan.ts +0 -48
  85. package/src/core/gestures/pinch.ts +0 -58
  86. package/src/core/gestures/rotate.ts +0 -58
  87. package/src/core/gestures/swipe.ts +0 -66
  88. package/src/core/gestures/tap.ts +0 -45
  89. package/src/core/gestures/types.ts +0 -387
  90. package/src/core/gestures/utils.ts +0 -128
  91. package/src/core/index.ts +0 -43
  92. package/src/core/layout/config.ts +0 -102
  93. package/src/core/layout/index.ts +0 -168
  94. package/src/core/layout/jsx.ts +0 -174
  95. package/src/core/layout/schema.ts +0 -1044
  96. package/src/core/layout/types.ts +0 -95
  97. package/src/core/viewport/constants.ts +0 -145
  98. package/src/core/viewport/features/base.ts +0 -73
  99. package/src/core/viewport/features/collection.ts +0 -1182
  100. package/src/core/viewport/features/events.ts +0 -130
  101. package/src/core/viewport/features/item-size.ts +0 -271
  102. package/src/core/viewport/features/loading.ts +0 -263
  103. package/src/core/viewport/features/momentum.ts +0 -269
  104. package/src/core/viewport/features/performance.ts +0 -161
  105. package/src/core/viewport/features/placeholders.ts +0 -335
  106. package/src/core/viewport/features/rendering.ts +0 -962
  107. package/src/core/viewport/features/scrollbar.ts +0 -434
  108. package/src/core/viewport/features/scrolling.ts +0 -634
  109. package/src/core/viewport/features/utils.ts +0 -94
  110. package/src/core/viewport/features/virtual.ts +0 -525
  111. package/src/core/viewport/types.ts +0 -133
  112. package/src/core/viewport/utils/speed-tracker.ts +0 -79
  113. package/src/core/viewport/viewport.ts +0 -265
  114. package/test/benchmarks/layout/advanced.test.ts +0 -656
  115. package/test/benchmarks/layout/comparison.test.ts +0 -519
  116. package/test/benchmarks/layout/performance-comparison.test.ts +0 -274
  117. package/test/benchmarks/layout/real-components.test.ts +0 -733
  118. package/test/benchmarks/layout/simple.test.ts +0 -321
  119. package/test/benchmarks/layout/stress.test.ts +0 -990
  120. package/test/collection/basic.test.ts +0 -304
  121. package/test/components/vlist-selection.test.ts +0 -240
  122. package/test/components/vlist.test.ts +0 -63
  123. package/test/core/collection/adapter.test.ts +0 -161
  124. package/test/core/collection/collection.test.ts +0 -394
  125. package/test/core/layout/layout.test.ts +0 -201
  126. package/test/utils/dom-helpers.ts +0 -275
  127. package/test/utils/performance-helpers.ts +0 -392
  128. package/tsconfig.json +0 -20
@@ -1,94 +0,0 @@
1
- // src/core/viewport/features/utils.ts
2
-
3
- /**
4
- * Shared utilities for viewport features
5
- * Eliminates code duplication across features
6
- */
7
-
8
- import type { ViewportContext, ViewportComponent } from "../types";
9
-
10
- /**
11
- * Wraps viewport initialization with feature-specific logic
12
- * Eliminates the repeated initialization hook pattern
13
- */
14
- export function wrapInitialize<T extends ViewportContext & ViewportComponent>(
15
- component: T,
16
- featureInit: () => void,
17
- ): void {
18
- const originalInitialize = component.viewport.initialize;
19
- component.viewport.initialize = () => {
20
- // Check if already initialized (returns false if already done)
21
- const result = originalInitialize();
22
- // Only run feature init if base init actually ran (didn't return false)
23
- if (result !== false) {
24
- featureInit();
25
- }
26
- // Propagate the result through the chain so outer wrappers know to skip
27
- return result;
28
- };
29
- }
30
-
31
- /**
32
- * Wraps component destroy with cleanup logic
33
- * Eliminates the repeated destroy hook pattern
34
- */
35
- export function wrapDestroy<T extends Record<string, any>>(
36
- component: T,
37
- cleanup: () => void,
38
- ): void {
39
- if ("destroy" in component && typeof component.destroy === "function") {
40
- const originalDestroy = component.destroy;
41
- (component as any).destroy = () => {
42
- cleanup();
43
- originalDestroy?.();
44
- };
45
- }
46
- }
47
-
48
- /**
49
- * Gets viewport state with proper typing
50
- * Eliminates repeated (component.viewport as any).state
51
- */
52
- export function getViewportState(component: ViewportComponent): any {
53
- return (component.viewport as any).state;
54
- }
55
-
56
- /**
57
- * Checks if an item is a placeholder
58
- * Eliminates duplicated placeholder detection logic
59
- */
60
- export function isPlaceholder(item: any): boolean {
61
- return (
62
- item &&
63
- typeof item === "object" &&
64
- (item._placeholder === true || item["_placeholder"] === true) // Support both access patterns
65
- );
66
- }
67
-
68
- /**
69
- * Creates a range key for deduplication
70
- * Used by multiple features for tracking ranges
71
- */
72
- export function getRangeKey(range: { start: number; end: number }): string {
73
- return `${range.start}-${range.end}`;
74
- }
75
-
76
- /**
77
- * Clamps a value between min and max
78
- * Used by multiple features for boundary checking
79
- */
80
- export function clamp(value: number, min: number, max: number): number {
81
- return Math.max(min, Math.min(max, value));
82
- }
83
-
84
- /**
85
- * Stores a function reference on the component for later access
86
- * Eliminates pattern like (component as any)._featureFunction = fn
87
- */
88
- export function storeFeatureFunction<T extends Record<string, any>>(
89
- component: T,
90
- name: string,
91
- fn: Function,
92
- ): void {
93
- (component as any)[name] = fn;
94
- }
@@ -1,525 +0,0 @@
1
- /**
2
- * Virtual Feature - Core virtual scrolling calculations
3
- * Handles visible range calculation and total virtual size management
4
- */
5
-
6
- import type { ViewportContext, ViewportComponent } from "../types";
7
- import { VIEWPORT_CONSTANTS } from "../constants";
8
- import { wrapInitialize, getViewportState } from "./utils";
9
-
10
- export interface VirtualConfig {
11
- itemSize?: number;
12
- overscan?: number;
13
- orientation?: "vertical" | "horizontal";
14
- autoDetectItemSize?: boolean;
15
- debug?: boolean;
16
- initialScrollIndex?: number;
17
- }
18
-
19
- /**
20
- * Virtual scrolling feature for viewport
21
- * Handles visible range calculations with compression for large datasets
22
- */
23
- export const withVirtual = (config: VirtualConfig = {}) => {
24
- return <T extends ViewportContext & ViewportComponent>(component: T): T => {
25
- const {
26
- itemSize,
27
- overscan = VIEWPORT_CONSTANTS.VIRTUAL_SCROLL.OVERSCAN_BUFFER,
28
- orientation = "vertical",
29
- autoDetectItemSize = itemSize === undefined
30
- ? VIEWPORT_CONSTANTS.VIRTUAL_SCROLL.AUTO_DETECT_ITEM_SIZE
31
- : false,
32
- debug = false,
33
- initialScrollIndex = 0,
34
- } = config;
35
-
36
- // Use provided itemSize or default, but mark if we should auto-detect
37
- const initialItemSize =
38
- itemSize || VIEWPORT_CONSTANTS.VIRTUAL_SCROLL.DEFAULT_ITEM_SIZE;
39
-
40
- const MAX_VIRTUAL_SIZE = VIEWPORT_CONSTANTS.VIRTUAL_SCROLL.MAX_VIRTUAL_SIZE;
41
- let viewportState: any;
42
- let hasCalculatedItemSize = false;
43
- let hasRecalculatedScrollForCompression = false; // Track if we've recalculated scroll position for compression
44
-
45
- // Initialize using shared wrapper
46
- wrapInitialize(component, () => {
47
- viewportState = getViewportState(component);
48
- if (!viewportState) return;
49
-
50
- Object.assign(viewportState, {
51
- itemSize: initialItemSize,
52
- overscan,
53
- containerSize:
54
- viewportState.viewportElement?.[
55
- orientation === "horizontal" ? "offsetWidth" : "offsetHeight"
56
- ] || 600,
57
- });
58
-
59
- updateTotalVirtualSize(viewportState.totalItems);
60
-
61
- // If we have an initial scroll index, calculate initial scroll position
62
- // Note: We store the initialScrollIndex to use directly in range calculations
63
- // because scroll position may be compressed for large lists
64
- let initialScrollPosition = viewportState.scrollPosition || 0;
65
-
66
- if (initialScrollIndex > 0) {
67
- // Store the target index for use in range calculations
68
- (viewportState as any).targetScrollIndex = initialScrollIndex;
69
-
70
- // Calculate scroll position (may be compressed for large lists)
71
- initialScrollPosition =
72
- initialScrollIndex * (viewportState.itemSize || initialItemSize);
73
- viewportState.scrollPosition = initialScrollPosition;
74
-
75
- // Notify scrolling feature to sync its local scroll position
76
- // Use setTimeout to ensure scrolling feature has initialized
77
- setTimeout(() => {
78
- component.emit?.("viewport:scroll-position-sync", {
79
- position: initialScrollPosition,
80
- source: "initial-scroll-index",
81
- });
82
- }, 0);
83
- }
84
-
85
- updateVisibleRange(initialScrollPosition);
86
-
87
- // Ensure container size is measured after DOM is ready
88
- requestAnimationFrame(() => {
89
- updateContainerSize();
90
- });
91
- });
92
-
93
- // Helper functions
94
- const getCompressionRatio = (): number => {
95
- if (!viewportState?.virtualTotalSize) return 1;
96
- const actualSize = viewportState.totalItems * viewportState.itemSize;
97
- return actualSize <= MAX_VIRTUAL_SIZE ? 1 : MAX_VIRTUAL_SIZE / actualSize;
98
- };
99
-
100
- const log = (message: string, data?: any) => {
101
- if (debug) console.log(`[Virtual] ${message}`, data);
102
- };
103
-
104
- // Calculate visible range
105
- const calculateVisibleRange = (
106
- scrollPosition: number,
107
- ): { start: number; end: number } => {
108
- if (!viewportState) {
109
- console.warn("[Virtual] No viewport state, returning empty range");
110
- return { start: 0, end: 0 };
111
- }
112
-
113
- const { containerSize, totalItems } = viewportState;
114
-
115
- // Early returns for invalid states
116
- if (
117
- !containerSize ||
118
- containerSize <= 0 ||
119
- !totalItems ||
120
- totalItems <= 0
121
- ) {
122
- // If we have an initialScrollIndex, use it to calculate the range
123
- // even when totalItems is 0 (not yet loaded from API)
124
- if (initialScrollIndex > 0) {
125
- const visibleCount = Math.ceil(
126
- (containerSize || 600) /
127
- (viewportState.itemSize || initialItemSize),
128
- );
129
- const start = Math.max(0, initialScrollIndex - overscan);
130
- const end = initialScrollIndex + visibleCount + overscan;
131
- return { start, end };
132
- }
133
- log(`Invalid state: container=${containerSize}, items=${totalItems}`);
134
- return { start: 0, end: 0 };
135
- }
136
-
137
- const virtualSize =
138
- viewportState.virtualTotalSize || totalItems * viewportState.itemSize;
139
- const visibleCount = Math.ceil(containerSize / viewportState.itemSize);
140
- const compressionRatio = getCompressionRatio();
141
-
142
- let start: number, end: number;
143
-
144
- // Check if we have a target scroll index (for initialScrollIndex with compression)
145
- const targetScrollIndex = (viewportState as any).targetScrollIndex;
146
-
147
- if (compressionRatio < 1) {
148
- // Compressed space calculation
149
- // If we have a targetScrollIndex, use it directly instead of calculating from scroll position
150
- // This ensures we show the correct items even when virtual space is compressed
151
- if (targetScrollIndex !== undefined && targetScrollIndex > 0) {
152
- start = Math.max(0, targetScrollIndex - overscan);
153
- end = Math.min(
154
- totalItems - 1,
155
- targetScrollIndex + visibleCount + overscan,
156
- );
157
- // Clear targetScrollIndex after first use so normal scrolling works
158
- delete (viewportState as any).targetScrollIndex;
159
- } else {
160
- const scrollRatio = scrollPosition / virtualSize;
161
- const exactIndex = scrollRatio * totalItems;
162
- start = Math.floor(exactIndex);
163
- end = Math.ceil(exactIndex) + visibleCount;
164
- }
165
-
166
- // Near-bottom handling
167
- const maxScroll = virtualSize - containerSize;
168
- const distanceFromBottom = maxScroll - scrollPosition;
169
-
170
- if (distanceFromBottom <= containerSize && distanceFromBottom >= -1) {
171
- const itemsAtBottom = Math.floor(
172
- containerSize / viewportState.itemSize,
173
- );
174
- const firstVisibleAtBottom = Math.max(0, totalItems - itemsAtBottom);
175
- const interpolation = Math.max(
176
- 0,
177
- Math.min(1, 1 - distanceFromBottom / containerSize),
178
- );
179
-
180
- start = Math.floor(
181
- start + (firstVisibleAtBottom - start) * interpolation,
182
- );
183
- end =
184
- distanceFromBottom <= 1
185
- ? totalItems - 1
186
- : Math.min(totalItems - 1, start + visibleCount + overscan);
187
-
188
- log("Near bottom calculation:", {
189
- distanceFromBottom,
190
- interpolation,
191
- start,
192
- end,
193
- scrollPosition,
194
- totalItems,
195
- });
196
- }
197
-
198
- // Apply overscan
199
- start = Math.max(0, start - overscan);
200
- end = Math.min(totalItems - 1, end + overscan);
201
- } else {
202
- // Direct calculation
203
- start = Math.max(
204
- 0,
205
- Math.floor(scrollPosition / viewportState.itemSize) - overscan,
206
- );
207
- end = Math.min(totalItems - 1, start + visibleCount + overscan * 2);
208
- }
209
-
210
- // Validate output
211
- if (isNaN(start) || isNaN(end)) {
212
- console.error("[Virtual] NaN in range calculation:", {
213
- scrollPosition,
214
- containerSize,
215
- totalItems,
216
- itemSize: viewportState.itemSize,
217
- compressionRatio,
218
- });
219
- return { start: 0, end: 0 };
220
- }
221
-
222
- // log(`Range: ${start}-${end} (scroll: ${scrollPosition})`);
223
-
224
- // Strategic log for last range
225
- // if (end >= totalItems - 10) {
226
- // console.log(
227
- // `[Virtual] Near end range: ${start}-${end}, totalItems=${totalItems}, lastItemPos=${
228
- // end * viewportState.itemSize
229
- // }px, virtualSize=${viewportState.virtualTotalSize}px`
230
- // );
231
- // }
232
-
233
- return { start, end };
234
- };
235
-
236
- // Calculate actual visible range (without overscan buffer)
237
- const calculateActualVisibleRange = (scrollPosition: number) => {
238
- if (!viewportState) return { start: 0, end: 0 };
239
-
240
- const { containerSize, totalItems } = viewportState;
241
- if (!containerSize || !totalItems) return { start: 0, end: 0 };
242
-
243
- const itemSize = viewportState.itemSize;
244
- const visibleCount = Math.ceil(containerSize / itemSize);
245
- const compressionRatio = viewportState.virtualTotalSize
246
- ? (totalItems * itemSize) / viewportState.virtualTotalSize
247
- : 1;
248
-
249
- let start: number, end: number;
250
-
251
- if (compressionRatio < 1) {
252
- // Compressed space - calculate based on scroll ratio
253
- const virtualSize =
254
- viewportState.virtualTotalSize || totalItems * itemSize;
255
- const scrollRatio = scrollPosition / virtualSize;
256
- start = Math.floor(scrollRatio * totalItems);
257
- end = Math.min(totalItems - 1, start + visibleCount - 1);
258
- } else {
259
- // Direct calculation
260
- start = Math.floor(scrollPosition / itemSize);
261
- end = Math.min(totalItems - 1, start + visibleCount - 1);
262
- }
263
-
264
- // Ensure valid range
265
- start = Math.max(0, start);
266
- end = Math.max(start, end);
267
-
268
- return { start, end };
269
- };
270
-
271
- // Update functions
272
- const updateVisibleRange = (scrollPosition: number) => {
273
- if (!viewportState) return;
274
- viewportState.visibleRange = calculateVisibleRange(scrollPosition);
275
-
276
- // DEBUG: Log who calls updateVisibleRange with 0 when it should be non-zero
277
-
278
- // Calculate actual visible range (without overscan) for UI display
279
- const actualVisibleRange = calculateActualVisibleRange(scrollPosition);
280
-
281
- component.emit?.("viewport:range-changed", {
282
- range: viewportState.visibleRange,
283
- visibleRange: actualVisibleRange,
284
- scrollPosition,
285
- });
286
- };
287
-
288
- // Update total virtual size
289
- const updateTotalVirtualSize = (totalItems: number) => {
290
- if (!viewportState) return;
291
-
292
- const oldSize = viewportState.virtualTotalSize;
293
- viewportState.totalItems = totalItems;
294
- const actualSize = totalItems * viewportState.itemSize;
295
-
296
- // Get padding from the items container if available
297
- let totalPadding = 0;
298
- if (viewportState.itemsContainer) {
299
- const computedStyle = window.getComputedStyle(
300
- viewportState.itemsContainer,
301
- );
302
- const paddingTop = parseFloat(computedStyle.paddingTop) || 0;
303
- const paddingBottom = parseFloat(computedStyle.paddingBottom) || 0;
304
- totalPadding = paddingTop + paddingBottom;
305
- }
306
-
307
- // Include padding in the virtual size
308
- viewportState.virtualTotalSize = Math.min(
309
- actualSize + totalPadding,
310
- MAX_VIRTUAL_SIZE,
311
- );
312
-
313
- // Strategic log for debugging gap issue
314
- // console.log(
315
- // `[Virtual] Total size update: items=${totalItems}, itemSize=${viewportState.itemSize}px, padding=${totalPadding}px, virtualSize=${viewportState.virtualTotalSize}px (was ${oldSize}px)`
316
- // );
317
-
318
- component.emit?.("viewport:virtual-size-changed", {
319
- totalVirtualSize: viewportState.virtualTotalSize,
320
- totalItems: totalItems,
321
- compressionRatio: getCompressionRatio(),
322
- });
323
- };
324
-
325
- // Update container size
326
- const updateContainerSize = () => {
327
- if (!viewportState || !viewportState.viewportElement) return;
328
-
329
- const size =
330
- viewportState.viewportElement[
331
- viewportState.orientation === "horizontal"
332
- ? "offsetWidth"
333
- : "offsetHeight"
334
- ];
335
-
336
- // Log the actual measurement
337
- // console.log(
338
- // `[Virtual] Container size measured: ${size}px from viewport element`
339
- // );
340
-
341
- if (size !== viewportState.containerSize) {
342
- viewportState.containerSize = size;
343
- updateVisibleRange(viewportState.scrollPosition || 0);
344
- updateTotalVirtualSize(viewportState.totalItems); // Also update virtual size
345
- component.emit?.("viewport:container-size-changed", {
346
- containerSize: size,
347
- });
348
- }
349
- };
350
-
351
- // Override viewport methods
352
- component.viewport.getVisibleRange = () =>
353
- viewportState?.visibleRange ||
354
- calculateVisibleRange(viewportState?.scrollPosition || 0);
355
-
356
- // Event listeners
357
- component.on?.("viewport:scroll", (data: any) =>
358
- updateVisibleRange(data.position),
359
- );
360
-
361
- component.on?.("viewport:items-changed", (data: any) => {
362
- if (data.totalItems !== undefined) {
363
- updateTotalVirtualSize(data.totalItems);
364
- updateVisibleRange(viewportState?.scrollPosition || 0);
365
- }
366
- });
367
-
368
- // Listen for total items changes (important for cursor pagination)
369
- // Note: setTotalItems() updates viewportState.totalItems BEFORE emitting this event,
370
- // so we can't rely on viewportState.totalItems to detect the first total.
371
- // Instead we use the hasRecalculatedScrollForCompression flag.
372
- component.on?.("viewport:total-items-changed", (data: any) => {
373
- if (data.total === undefined) return;
374
-
375
- // FIX: When we first receive totalItems with initialScrollIndex and compression,
376
- // recalculate scroll position to account for compression.
377
- // Without this, initialScrollIndex calculates position as index * itemSize,
378
- // but rendering uses compressed space, causing items to appear off-screen.
379
- //
380
- // We check this BEFORE updateTotalVirtualSize because we need to recalculate
381
- // scroll position based on the new total, and the flag ensures we only do this once.
382
- if (
383
- initialScrollIndex > 0 &&
384
- !hasRecalculatedScrollForCompression &&
385
- data.total > 0
386
- ) {
387
- const actualTotalSize = data.total * viewportState.itemSize;
388
- const isCompressed = actualTotalSize > MAX_VIRTUAL_SIZE;
389
-
390
- if (isCompressed) {
391
- // Recalculate scroll position using compression-aware formula
392
- // Same formula used in scrollToIndex
393
- const ratio = initialScrollIndex / data.total;
394
- const compressedPosition = ratio * MAX_VIRTUAL_SIZE;
395
-
396
- viewportState.scrollPosition = compressedPosition;
397
-
398
- // Notify scrolling feature to sync its local scroll position
399
- component.emit?.("viewport:scroll-position-sync", {
400
- position: compressedPosition,
401
- source: "compression-recalculation",
402
- });
403
-
404
- // Clear targetScrollIndex since we've now correctly calculated the scroll position
405
- // The normal compressed scrolling formula will now work correctly
406
- delete (viewportState as any).targetScrollIndex;
407
- }
408
-
409
- // Mark as done even if not compressed - we only want to do this check once
410
- hasRecalculatedScrollForCompression = true;
411
- }
412
-
413
- updateTotalVirtualSize(data.total);
414
- updateVisibleRange(viewportState?.scrollPosition || 0);
415
-
416
- // Trigger a render to update the view
417
- component.viewport?.renderItems?.();
418
- });
419
-
420
- // Listen for container size changes to recalculate virtual size
421
- component.on?.("viewport:container-size-changed", (data: any) => {
422
- if (viewportState && data.containerSize !== viewportState.containerSize) {
423
- viewportState.containerSize = data.containerSize;
424
- // Recalculate virtual size with new container size
425
- updateTotalVirtualSize(viewportState.totalItems);
426
- updateVisibleRange(viewportState.scrollPosition || 0);
427
- }
428
- });
429
-
430
- component.on?.("collection:range-loaded", (data: any) => {
431
- if (
432
- data.total !== undefined &&
433
- data.total !== viewportState?.totalItems
434
- ) {
435
- updateTotalVirtualSize(data.total);
436
- }
437
- updateVisibleRange(viewportState?.scrollPosition || 0);
438
- component.viewport?.renderItems?.();
439
- });
440
-
441
- // Listen for first items rendered to calculate item size (if auto-detection is enabled)
442
- if (autoDetectItemSize) {
443
- component.on?.("viewport:items-rendered", (data: any) => {
444
- if (
445
- !hasCalculatedItemSize &&
446
- data.elements?.length > 0 &&
447
- viewportState
448
- ) {
449
- // Calculate average item size from first rendered batch
450
- const sizes: number[] = [];
451
- const sizeProperty =
452
- orientation === "horizontal" ? "offsetWidth" : "offsetHeight";
453
-
454
- data.elements.forEach((element: HTMLElement) => {
455
- const size = element[sizeProperty];
456
- if (size > 0) {
457
- sizes.push(size);
458
- }
459
- });
460
-
461
- if (sizes.length > 0) {
462
- const avgSize = Math.round(
463
- sizes.reduce((sum, size) => sum + size, 0) / sizes.length,
464
- );
465
- const previousItemSize = viewportState.itemSize;
466
- viewportState.itemSize = avgSize;
467
-
468
- // If we have an initialScrollIndex, recalculate scroll position
469
- // based on the newly detected item size
470
- if (initialScrollIndex > 0 && avgSize !== previousItemSize) {
471
- const newScrollPosition = initialScrollIndex * avgSize;
472
- viewportState.scrollPosition = newScrollPosition;
473
-
474
- // Notify scrolling feature to sync its local scroll position
475
- component.emit?.("viewport:scroll-position-sync", {
476
- position: newScrollPosition,
477
- source: "item-size-detected",
478
- });
479
- }
480
-
481
- // Recalculate everything with new size
482
- updateTotalVirtualSize(viewportState.totalItems);
483
- updateVisibleRange(viewportState.scrollPosition || 0);
484
-
485
- // Re-render to adjust positions
486
- component.viewport?.renderItems?.();
487
-
488
- // Emit event for size change
489
- component.emit?.("viewport:item-size-detected", {
490
- previousSize: initialItemSize,
491
- detectedSize: avgSize,
492
- });
493
-
494
- hasCalculatedItemSize = true;
495
- }
496
- }
497
- });
498
- }
499
-
500
- // Expose virtual API
501
- const compressionRatio = getCompressionRatio();
502
- (component as any).virtual = {
503
- calculateVisibleRange,
504
- updateTotalVirtualSize,
505
- getTotalVirtualSize: () => viewportState?.virtualTotalSize || 0,
506
- getContainerSize: () => viewportState?.containerSize || 0,
507
- updateContainerSize: (size: number) => {
508
- if (viewportState) {
509
- viewportState.containerSize = size;
510
- updateVisibleRange(viewportState.scrollPosition || 0);
511
- }
512
- },
513
- getItemSize: () => viewportState?.itemSize || initialItemSize,
514
- calculateIndexFromPosition: (position: number) =>
515
- Math.floor(
516
- position /
517
- ((viewportState?.itemSize || initialItemSize) * compressionRatio),
518
- ),
519
- calculatePositionForIndex: (index: number) =>
520
- index * (viewportState?.itemSize || initialItemSize) * compressionRatio,
521
- };
522
-
523
- return component;
524
- };
525
- };