mtrl-addons 0.1.2 → 0.2.2

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 (117) hide show
  1. package/AI.md +28 -230
  2. package/CLAUDE.md +882 -0
  3. package/build.js +253 -24
  4. package/package.json +14 -4
  5. package/scripts/debug/vlist-selection.ts +121 -0
  6. package/src/components/index.ts +5 -41
  7. package/src/components/{list → vlist}/config.ts +66 -95
  8. package/src/components/vlist/constants.ts +23 -0
  9. package/src/components/vlist/features/api.ts +626 -0
  10. package/src/components/vlist/features/index.ts +10 -0
  11. package/src/components/vlist/features/selection.ts +436 -0
  12. package/src/components/vlist/features/viewport.ts +59 -0
  13. package/src/components/vlist/index.ts +17 -0
  14. package/src/components/{list → vlist}/types.ts +242 -32
  15. package/src/components/vlist/vlist.ts +92 -0
  16. package/src/core/compose/features/gestures/index.ts +227 -0
  17. package/src/core/compose/features/gestures/longpress.ts +383 -0
  18. package/src/core/compose/features/gestures/pan.ts +424 -0
  19. package/src/core/compose/features/gestures/pinch.ts +475 -0
  20. package/src/core/compose/features/gestures/rotate.ts +485 -0
  21. package/src/core/compose/features/gestures/swipe.ts +492 -0
  22. package/src/core/compose/features/gestures/tap.ts +334 -0
  23. package/src/core/compose/features/index.ts +2 -38
  24. package/src/core/compose/index.ts +13 -29
  25. package/src/core/gestures/index.ts +31 -0
  26. package/src/core/gestures/longpress.ts +68 -0
  27. package/src/core/gestures/manager.ts +418 -0
  28. package/src/core/gestures/pan.ts +48 -0
  29. package/src/core/gestures/pinch.ts +58 -0
  30. package/src/core/gestures/rotate.ts +58 -0
  31. package/src/core/gestures/swipe.ts +66 -0
  32. package/src/core/gestures/tap.ts +45 -0
  33. package/src/core/gestures/types.ts +387 -0
  34. package/src/core/gestures/utils.ts +128 -0
  35. package/src/core/index.ts +27 -151
  36. package/src/core/layout/schema.ts +153 -72
  37. package/src/core/layout/types.ts +5 -2
  38. package/src/core/viewport/constants.ts +145 -0
  39. package/src/core/viewport/features/base.ts +73 -0
  40. package/src/core/viewport/features/collection.ts +1182 -0
  41. package/src/core/viewport/features/events.ts +130 -0
  42. package/src/core/viewport/features/index.ts +20 -0
  43. package/src/core/{list-manager/features/viewport → viewport/features}/item-size.ts +31 -34
  44. package/src/core/{list-manager/features/viewport → viewport/features}/loading.ts +4 -4
  45. package/src/core/viewport/features/momentum.ts +269 -0
  46. package/src/core/viewport/features/placeholders.ts +335 -0
  47. package/src/core/viewport/features/rendering.ts +962 -0
  48. package/src/core/viewport/features/scrollbar.ts +434 -0
  49. package/src/core/viewport/features/scrolling.ts +634 -0
  50. package/src/core/viewport/features/utils.ts +94 -0
  51. package/src/core/viewport/features/virtual.ts +525 -0
  52. package/src/core/viewport/index.ts +31 -0
  53. package/src/core/viewport/types.ts +133 -0
  54. package/src/core/viewport/utils/speed-tracker.ts +79 -0
  55. package/src/core/viewport/viewport.ts +265 -0
  56. package/src/index.ts +0 -7
  57. package/src/styles/components/_vlist.scss +352 -0
  58. package/src/styles/index.scss +1 -1
  59. package/test/components/vlist-selection.test.ts +240 -0
  60. package/test/components/vlist.test.ts +63 -0
  61. package/test/core/collection/adapter.test.ts +161 -0
  62. package/bun.lock +0 -792
  63. package/src/components/list/api.ts +0 -314
  64. package/src/components/list/constants.ts +0 -56
  65. package/src/components/list/features/api.ts +0 -428
  66. package/src/components/list/features/index.ts +0 -31
  67. package/src/components/list/features/list-manager.ts +0 -502
  68. package/src/components/list/index.ts +0 -39
  69. package/src/components/list/list.ts +0 -234
  70. package/src/core/collection/base-collection.ts +0 -100
  71. package/src/core/collection/collection-composer.ts +0 -178
  72. package/src/core/collection/collection.ts +0 -745
  73. package/src/core/collection/constants.ts +0 -172
  74. package/src/core/collection/events.ts +0 -428
  75. package/src/core/collection/features/api/loading.ts +0 -279
  76. package/src/core/collection/features/operations/data-operations.ts +0 -147
  77. package/src/core/collection/index.ts +0 -104
  78. package/src/core/collection/state.ts +0 -497
  79. package/src/core/collection/types.ts +0 -404
  80. package/src/core/compose/features/collection.ts +0 -119
  81. package/src/core/compose/features/selection.ts +0 -213
  82. package/src/core/compose/features/styling.ts +0 -108
  83. package/src/core/list-manager/api.ts +0 -599
  84. package/src/core/list-manager/config.ts +0 -593
  85. package/src/core/list-manager/constants.ts +0 -268
  86. package/src/core/list-manager/features/api.ts +0 -58
  87. package/src/core/list-manager/features/collection/collection.ts +0 -705
  88. package/src/core/list-manager/features/collection/index.ts +0 -17
  89. package/src/core/list-manager/features/viewport/constants.ts +0 -42
  90. package/src/core/list-manager/features/viewport/index.ts +0 -16
  91. package/src/core/list-manager/features/viewport/placeholders.ts +0 -281
  92. package/src/core/list-manager/features/viewport/rendering.ts +0 -575
  93. package/src/core/list-manager/features/viewport/scrollbar.ts +0 -495
  94. package/src/core/list-manager/features/viewport/scrolling.ts +0 -795
  95. package/src/core/list-manager/features/viewport/template.ts +0 -220
  96. package/src/core/list-manager/features/viewport/viewport.ts +0 -654
  97. package/src/core/list-manager/features/viewport/virtual.ts +0 -309
  98. package/src/core/list-manager/index.ts +0 -279
  99. package/src/core/list-manager/list-manager.ts +0 -206
  100. package/src/core/list-manager/types.ts +0 -439
  101. package/src/core/list-manager/utils/calculations.ts +0 -290
  102. package/src/core/list-manager/utils/range-calculator.ts +0 -349
  103. package/src/core/list-manager/utils/speed-tracker.ts +0 -273
  104. package/src/styles/components/_list.scss +0 -244
  105. package/src/types/mtrl.d.ts +0 -6
  106. package/test/components/list.test.ts +0 -256
  107. package/test/core/collection/failed-ranges.test.ts +0 -270
  108. package/test/core/compose/features.test.ts +0 -183
  109. package/test/core/list-manager/features/collection.test.ts +0 -704
  110. package/test/core/list-manager/features/viewport.test.ts +0 -698
  111. package/test/core/list-manager/list-manager.test.ts +0 -593
  112. package/test/core/list-manager/utils/calculations.test.ts +0 -433
  113. package/test/core/list-manager/utils/range-calculator.test.ts +0 -569
  114. package/test/core/list-manager/utils/speed-tracker.test.ts +0 -530
  115. package/tsconfig.build.json +0 -23
  116. /package/src/components/{list → vlist}/features.ts +0 -0
  117. /package/src/core/{compose → viewport}/features/performance.ts +0 -0
@@ -1,575 +0,0 @@
1
- /**
2
- * Rendering - Item rendering and positioning for viewport
3
- *
4
- * This viewport module handles all aspects of rendering items in the virtual viewport,
5
- * including DOM element creation, positioning, recycling, and updates.
6
- */
7
-
8
- import type { ListManagerComponent, ItemRange } from "../../types";
9
- import type { ItemSizeManager } from "./item-size";
10
- import type { VirtualManager } from "./virtual";
11
- import type { ScrollingManager } from "./scrolling";
12
- import { getDefaultTemplate } from "./template";
13
- import { VIEWPORT_CONSTANTS } from "./constants";
14
- import { addClass, removeClass, hasClass } from "mtrl";
15
-
16
- export interface RenderingConfig {
17
- orientation: "vertical" | "horizontal";
18
- overscan: number;
19
- loadDataForRange?: (
20
- range: { start: number; end: number },
21
- priority?: "high" | "normal" | "low"
22
- ) => void;
23
- measureItems?: boolean; // Add measureItems flag
24
- }
25
-
26
- export interface RenderingManager {
27
- renderItems(): void;
28
- updateItemPositions(): void;
29
- getRenderedElements(): Map<number, HTMLElement>;
30
- setItemsContainer(container: HTMLElement): void;
31
- clear(): void;
32
- }
33
-
34
- /**
35
- * Creates a rendering manager for virtual viewport item rendering
36
- */
37
- export const createRenderingManager = (
38
- component: ListManagerComponent,
39
- itemSizeManager: ItemSizeManager,
40
- virtualManager: VirtualManager,
41
- scrollingManager: ScrollingManager,
42
- config: RenderingConfig,
43
- getActualTotalItems: () => number
44
- ): RenderingManager => {
45
- const {
46
- orientation,
47
- overscan,
48
- loadDataForRange,
49
- measureItems = false,
50
- } = config;
51
-
52
- // Items container reference
53
- let itemsContainer: HTMLElement | null = null;
54
-
55
- // Rendered elements cache and virtual range tracking
56
- const renderedElements = new Map<number, HTMLElement>();
57
- let currentVisibleRange: ItemRange = { start: 0, end: 0 };
58
-
59
- /**
60
- * Set the items container element
61
- */
62
- const setItemsContainer = (container: HTMLElement): void => {
63
- itemsContainer = container;
64
- };
65
-
66
- /**
67
- * Clear all rendered elements
68
- */
69
- const clear = (): void => {
70
- renderedElements.clear();
71
- if (itemsContainer) {
72
- itemsContainer.innerHTML = "";
73
- }
74
- };
75
-
76
- /**
77
- * Render items in the viewport
78
- */
79
- const renderItems = (): void => {
80
- if (!itemsContainer) return;
81
-
82
- const newVisibleRange = virtualManager.calculateVisibleRange(
83
- scrollingManager.getScrollPosition()
84
- );
85
-
86
- // Validate range
87
- const actualTotalItems = getActualTotalItems();
88
- if (
89
- newVisibleRange.start < 0 ||
90
- newVisibleRange.start >= actualTotalItems ||
91
- newVisibleRange.end < newVisibleRange.start
92
- ) {
93
- return;
94
- }
95
-
96
- // Check if range changed
97
- const rangeChanged =
98
- newVisibleRange.start !== currentVisibleRange.start ||
99
- newVisibleRange.end !== currentVisibleRange.end;
100
-
101
- // Check if component has placeholders API
102
- const hasPlaceholders = !!(component as any).placeholders;
103
- const placeholdersAPI = hasPlaceholders
104
- ? (component as any).placeholders
105
- : null;
106
-
107
- // Check if we need to replace any placeholders with real data
108
- let needsPlaceholderReplacement = false;
109
-
110
- if (!rangeChanged && renderedElements.size > 0 && hasPlaceholders) {
111
- // Check if any rendered elements are placeholders but we now have real data
112
- for (let i = newVisibleRange.start; i <= newVisibleRange.end; i++) {
113
- const item = i < component.items.length ? component.items[i] : null;
114
- const element = renderedElements.get(i);
115
-
116
- if (item && element) {
117
- const isCurrentItemPlaceholder = placeholdersAPI.isPlaceholder(item);
118
- const elementIsPlaceholder = hasClass(
119
- element,
120
- VIEWPORT_CONSTANTS.PLACEHOLDER.CSS_CLASS
121
- );
122
-
123
- if (elementIsPlaceholder && !isCurrentItemPlaceholder) {
124
- needsPlaceholderReplacement = true;
125
- break;
126
- }
127
- }
128
- }
129
- }
130
-
131
- if (
132
- !rangeChanged &&
133
- renderedElements.size > 0 &&
134
- !needsPlaceholderReplacement
135
- ) {
136
- // Only skip rendering if we already have items rendered and no placeholders need replacement
137
- updateItemPositions();
138
- return;
139
- }
140
-
141
- // Check for missing data and show placeholders if needed
142
- if (hasPlaceholders && placeholdersAPI.isEnabled()) {
143
- const missingIndices: number[] = [];
144
-
145
- for (let i = newVisibleRange.start; i <= newVisibleRange.end; i++) {
146
- if (i >= actualTotalItems) break;
147
-
148
- const itemExists =
149
- i < component.items.length &&
150
- component.items[i] !== null &&
151
- component.items[i] !== undefined &&
152
- !placeholdersAPI.isPlaceholder(component.items[i]);
153
-
154
- if (!itemExists) {
155
- missingIndices.push(i);
156
- }
157
- }
158
-
159
- // Show placeholders for missing items
160
- if (missingIndices.length > 0) {
161
- const placeholderRange = {
162
- start: Math.min(...missingIndices),
163
- end: Math.max(...missingIndices),
164
- };
165
- placeholdersAPI.showPlaceholders(placeholderRange);
166
- }
167
- }
168
-
169
- // Proactively load data for visible range AND upcoming ranges
170
- // console.log(
171
- // `🔍 [VIEWPORT] Checking range ${newVisibleRange.start}-${newVisibleRange.end} for missing data`
172
- // );
173
-
174
- const collection = (component as any).collection;
175
- const hasCollection = !!collection;
176
- const hasLoadMissingRanges =
177
- hasCollection && typeof collection.loadMissingRanges === "function";
178
-
179
- // Calculate extended range for proactive loading
180
- const itemsPerViewport = Math.ceil(
181
- virtualManager.getState().containerSize /
182
- itemSizeManager.getEstimatedItemSize()
183
- );
184
-
185
- // DISABLED PREFETCH: Set to 0 to avoid loading wrong ranges
186
- // TODO: Fix the range calculation before re-enabling prefetch
187
- const prefetchBuffer = 0; // Was: Math.ceil(itemsPerViewport * 2);
188
- const extendedRange = {
189
- start: Math.max(0, newVisibleRange.start - prefetchBuffer),
190
- end: Math.min(actualTotalItems - 1, newVisibleRange.end + prefetchBuffer),
191
- };
192
-
193
- // Check for missing data in visible and extended ranges
194
- if (hasLoadMissingRanges) {
195
- // Count missing items in visible range
196
- let visibleMissingCount = 0;
197
- const visibleMissingIndices: number[] = [];
198
-
199
- for (let i = newVisibleRange.start; i <= newVisibleRange.end; i++) {
200
- // Skip if index is beyond actual total items
201
- if (i >= actualTotalItems) {
202
- break;
203
- }
204
-
205
- // Check if item is missing (null, undefined, or array doesn't extend to this index)
206
- const itemExists =
207
- i < component.items.length &&
208
- component.items[i] !== null &&
209
- component.items[i] !== undefined;
210
-
211
- // Also check if the item is a placeholder (which means real data is missing)
212
- const isItemPlaceholder =
213
- itemExists &&
214
- hasPlaceholders &&
215
- placeholdersAPI.isPlaceholder(component.items[i]);
216
-
217
- if (!itemExists || isItemPlaceholder) {
218
- visibleMissingCount++;
219
- visibleMissingIndices.push(i);
220
- }
221
- }
222
-
223
- // Count missing items in extended range (excluding visible range)
224
- let extendedMissingCount = 0;
225
- const extendedMissingIndices: number[] = [];
226
-
227
- for (let i = extendedRange.start; i <= extendedRange.end; i++) {
228
- // Skip visible range (already counted)
229
- if (i >= newVisibleRange.start && i <= newVisibleRange.end) {
230
- continue;
231
- }
232
-
233
- // Skip if index is beyond actual total items
234
- if (i >= actualTotalItems) {
235
- break;
236
- }
237
-
238
- // Check if item is missing
239
- const itemExists =
240
- i < component.items.length &&
241
- component.items[i] !== null &&
242
- component.items[i] !== undefined;
243
-
244
- // Also check if the item is a placeholder (which means real data is missing)
245
- const isItemPlaceholder =
246
- itemExists &&
247
- hasPlaceholders &&
248
- placeholdersAPI.isPlaceholder(component.items[i]);
249
-
250
- if (!itemExists || isItemPlaceholder) {
251
- extendedMissingCount++;
252
- extendedMissingIndices.push(i);
253
- }
254
- }
255
-
256
- // Load visible range with high priority
257
- if (visibleMissingCount > 0) {
258
- // Use loading manager for all loads to benefit from request queue
259
- if (loadDataForRange) {
260
- loadDataForRange(newVisibleRange, "high");
261
- } else {
262
- // Fallback to direct collection call only if no loading manager
263
- collection.loadMissingRanges(newVisibleRange).catch((error: any) => {
264
- console.error(
265
- "❌ [VIEWPORT] Failed to load visible ranges:",
266
- error
267
- );
268
- });
269
- }
270
- }
271
-
272
- // Load extended range with lower priority (proactive)
273
- // Skip if prefetch is disabled (prefetchBuffer === 0)
274
- if (extendedMissingCount > 0 && prefetchBuffer > 0) {
275
- // Use loading manager for proactive loads
276
- if (loadDataForRange) {
277
- loadDataForRange(extendedRange, "low");
278
- } else {
279
- // Fallback to direct collection call
280
- collection.loadMissingRanges(extendedRange).catch((error: any) => {
281
- console.error(
282
- "❌ [VIEWPORT] Failed to load extended range:",
283
- error
284
- );
285
- });
286
- }
287
- }
288
- }
289
-
290
- // Recycle out-of-range items
291
- const buffer = overscan; // Reduced from overscan * 2 for fewer DOM elements
292
- const recycleStart = newVisibleRange.start - buffer;
293
- const recycleEnd = newVisibleRange.end + buffer;
294
-
295
- for (const [index, element] of renderedElements) {
296
- if (index < recycleStart || index > recycleEnd) {
297
- // Remove from DOM
298
- element.remove();
299
- renderedElements.delete(index);
300
- }
301
- }
302
-
303
- // Render new items
304
- const newElements: { element: HTMLElement; index: number }[] = [];
305
-
306
- for (let i = newVisibleRange.start; i <= newVisibleRange.end; i++) {
307
- if (i >= component.items.length) {
308
- break;
309
- }
310
-
311
- const item = component.items[i];
312
-
313
- if (!item) {
314
- continue; // Skip empty slots
315
- }
316
-
317
- // Check if already rendered
318
- const existingElement = renderedElements.get(i);
319
- if (existingElement) {
320
- // Check if the rendered element is a placeholder but we now have real data
321
- const isCurrentItemPlaceholder =
322
- hasPlaceholders && placeholdersAPI.isPlaceholder(item);
323
- const elementIsPlaceholder = hasClass(
324
- existingElement,
325
- VIEWPORT_CONSTANTS.PLACEHOLDER.CSS_CLASS
326
- );
327
-
328
- if (elementIsPlaceholder && !isCurrentItemPlaceholder) {
329
- // We have real data now, remove the placeholder element and re-render
330
- existingElement.remove();
331
- renderedElements.delete(i);
332
- // Continue to render the real item below
333
- } else if (!elementIsPlaceholder && isCurrentItemPlaceholder) {
334
- // We have a real element but now need a placeholder (shouldn't happen often)
335
- existingElement.remove();
336
- renderedElements.delete(i);
337
- // Continue to render the placeholder below
338
- } else {
339
- continue;
340
- }
341
- }
342
-
343
- // Create element
344
- const element = renderItem(item, i);
345
- if (element) {
346
- itemsContainer.appendChild(element);
347
- renderedElements.set(i, element);
348
- newElements.push({ element, index: i });
349
- }
350
- }
351
-
352
- // Update visible range
353
- currentVisibleRange = newVisibleRange;
354
-
355
- // Emit event
356
- component.emit?.("range:rendered", {
357
- range: newVisibleRange,
358
- renderedCount: renderedElements.size,
359
- });
360
-
361
- // Batch measure new elements for performance
362
- if (newElements.length > 0) {
363
- // Use requestAnimationFrame to measure after browser layout
364
- requestAnimationFrame(() => {
365
- newElements.forEach(({ element, index }) => {
366
- if (measureItems) {
367
- itemSizeManager.measureItem(element, index, orientation);
368
- }
369
- });
370
-
371
- // Update positions after measuring
372
- updateItemPositions();
373
- });
374
- } else {
375
- // Update positions immediately if no new elements
376
- updateItemPositions();
377
- }
378
- };
379
-
380
- /**
381
- * Render a single item element
382
- */
383
- const renderItem = (item: any, index: number): HTMLElement | null => {
384
- let template = component.template;
385
-
386
- // Use default template if none provided
387
- if (!template) {
388
- template = getDefaultTemplate();
389
- }
390
-
391
- try {
392
- let element: HTMLElement;
393
-
394
- // Always create fresh element from template
395
- const result = template(item, index);
396
- if (typeof result === "string") {
397
- const wrapper = document.createElement("div");
398
- wrapper.innerHTML = result;
399
- element = wrapper.firstElementChild as HTMLElement;
400
- } else if (result instanceof HTMLElement) {
401
- element = result;
402
- } else {
403
- return null;
404
- }
405
-
406
- // Check if this is a placeholder and add the CSS class
407
- const hasPlaceholders = !!(component as any).placeholders;
408
- if (hasPlaceholders) {
409
- const placeholdersAPI = (component as any).placeholders;
410
- if (placeholdersAPI.isPlaceholder(item)) {
411
- addClass(element, VIEWPORT_CONSTANTS.PLACEHOLDER.CSS_CLASS);
412
- }
413
- }
414
-
415
- // Apply base styles for virtual positioning
416
- element.style.position = "absolute";
417
- element.style.left = "0";
418
- element.style.right = "0"; // For vertical, full width
419
- element.style.boxSizing = "border-box";
420
- element.style.willChange = "transform";
421
- element.style.visibility = "visible"; // Make visible immediately
422
-
423
- if (orientation === "horizontal") {
424
- element.style.top = "0";
425
- element.style.bottom = "0"; // Full height for horizontal
426
- element.style.width = `${itemSizeManager.getEstimatedItemSize()}px`; // Initial width
427
- } else {
428
- element.style.height = `${itemSizeManager.getEstimatedItemSize()}px`; // Initial height
429
- element.style.width = "100%";
430
- }
431
-
432
- return element;
433
- } catch (error) {
434
- console.error(`Error rendering item at index ${index}:`, error);
435
- return null;
436
- }
437
- };
438
-
439
- /**
440
- * Update positions of rendered items
441
- */
442
- const updateItemPositions = () => {
443
- if (!itemsContainer || renderedElements.size === 0) return;
444
-
445
- const isHorizontal = orientation === "horizontal";
446
- const axis = isHorizontal ? "X" : "Y";
447
-
448
- // Get the current scroll position and visible range
449
- const scrollPosition = scrollingManager.getScrollPosition();
450
- const visibleRange = virtualManager.calculateVisibleRange(scrollPosition);
451
-
452
- // Get basic measurements
453
- const totalItems = getActualTotalItems();
454
- const itemSize = itemSizeManager.getEstimatedItemSize();
455
- const virtualTotalSize = virtualManager.getTotalVirtualSize();
456
- const containerSize = virtualManager.getState().containerSize;
457
-
458
- // Get all indices in sorted order
459
- const sortedIndices = Array.from(renderedElements.keys()).sort(
460
- (a, b) => a - b
461
- );
462
-
463
- if (sortedIndices.length === 0) return;
464
-
465
- // Calculate positioning using unified index-based approach
466
- let currentPosition = 0;
467
- const firstRenderedIndex = sortedIndices[0];
468
- const lastRenderedIndex = sortedIndices[sortedIndices.length - 1];
469
-
470
- // Check if we're using compressed virtual space
471
- const actualTotalSize = totalItems * itemSize;
472
- const isCompressed = actualTotalSize > virtualTotalSize;
473
-
474
- if (isCompressed) {
475
- // When using compressed space, we need special handling near the bottom
476
- const maxScrollPosition = virtualTotalSize - containerSize;
477
- const distanceFromBottom = maxScrollPosition - scrollPosition;
478
- const nearBottomThreshold = containerSize; // Within one viewport height
479
-
480
- if (
481
- distanceFromBottom <= nearBottomThreshold &&
482
- distanceFromBottom >= -1
483
- ) {
484
- // Near or at the bottom - use interpolation for smooth transition
485
- const itemsThatFitCompletely = Math.floor(containerSize / itemSize);
486
- const firstVisibleAtBottom = Math.max(
487
- 0,
488
- totalItems - itemsThatFitCompletely
489
- );
490
-
491
- // Calculate normal scroll position
492
- const scrollRatio = scrollPosition / virtualTotalSize;
493
- const exactScrollIndex = scrollRatio * totalItems;
494
-
495
- // Interpolation factor: 0 when far from bottom, 1 when at bottom
496
- const interpolationFactor = Math.max(
497
- 0,
498
- Math.min(1, 1 - distanceFromBottom / nearBottomThreshold)
499
- );
500
-
501
- // For the first rendered item, interpolate between normal and bottom positions
502
- const bottomPosition =
503
- (firstRenderedIndex - firstVisibleAtBottom) * itemSize;
504
- const normalPosition =
505
- (firstRenderedIndex - exactScrollIndex) * itemSize;
506
-
507
- // Interpolate between the two positions
508
- currentPosition =
509
- normalPosition +
510
- (bottomPosition - normalPosition) * interpolationFactor;
511
- } else {
512
- // For normal scrolling in compressed space
513
- const scrollRatio = scrollPosition / virtualTotalSize;
514
- const exactScrollIndex = scrollRatio * totalItems;
515
-
516
- // Calculate offset from the exact scroll position
517
- const offset = firstRenderedIndex - exactScrollIndex;
518
- currentPosition = offset * itemSize;
519
- }
520
- } else {
521
- // When not compressed (actual size <= 10M), use direct positioning
522
- // This gives us natural 1:1 scrolling
523
- const firstItemPosition = firstRenderedIndex * itemSize;
524
- currentPosition = firstItemPosition - scrollPosition;
525
- }
526
-
527
- // Position each rendered item
528
- sortedIndices.forEach((index) => {
529
- const element = renderedElements.get(index);
530
- if (!element) return;
531
-
532
- // Get measured size for this item
533
- const size =
534
- itemSizeManager.getMeasuredSize(index) ||
535
- itemSizeManager.getEstimatedItemSize();
536
-
537
- // Position item relative to container
538
- element.style.position = "absolute";
539
- element.style.transform = `translate${axis}(${currentPosition}px)`;
540
- element.style.visibility = "visible";
541
-
542
- // Update dimensions
543
- if (isHorizontal) {
544
- element.style.width = `${size}px`;
545
- element.style.height = "100%";
546
- element.style.top = "0";
547
- } else {
548
- element.style.height = `${size}px`;
549
- element.style.width = "100%";
550
- element.style.left = "0";
551
- }
552
-
553
- // Move to next position
554
- currentPosition += size;
555
- });
556
-
557
- // Don't set container size to virtual size - keep it minimal
558
- // The scrollbar will handle the virtual size representation
559
- if (isHorizontal) {
560
- itemsContainer.style.width = "100%";
561
- itemsContainer.style.height = "100%";
562
- } else {
563
- itemsContainer.style.height = "100%";
564
- itemsContainer.style.width = "100%";
565
- }
566
- };
567
-
568
- return {
569
- renderItems,
570
- updateItemPositions,
571
- getRenderedElements: () => new Map(renderedElements),
572
- setItemsContainer,
573
- clear,
574
- };
575
- };