mtrl-addons 0.1.2 → 0.2.1

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 (115) hide show
  1. package/build.js +139 -86
  2. package/package.json +13 -4
  3. package/scripts/debug/vlist-selection.ts +121 -0
  4. package/src/components/index.ts +5 -41
  5. package/src/components/{list → vlist}/config.ts +66 -95
  6. package/src/components/vlist/constants.ts +23 -0
  7. package/src/components/vlist/features/api.ts +322 -0
  8. package/src/components/vlist/features/index.ts +10 -0
  9. package/src/components/vlist/features/selection.ts +444 -0
  10. package/src/components/vlist/features/viewport.ts +65 -0
  11. package/src/components/vlist/index.ts +16 -0
  12. package/src/components/{list → vlist}/types.ts +104 -26
  13. package/src/components/vlist/vlist.ts +92 -0
  14. package/src/core/compose/features/gestures/index.ts +227 -0
  15. package/src/core/compose/features/gestures/longpress.ts +383 -0
  16. package/src/core/compose/features/gestures/pan.ts +424 -0
  17. package/src/core/compose/features/gestures/pinch.ts +475 -0
  18. package/src/core/compose/features/gestures/rotate.ts +485 -0
  19. package/src/core/compose/features/gestures/swipe.ts +492 -0
  20. package/src/core/compose/features/gestures/tap.ts +334 -0
  21. package/src/core/compose/features/index.ts +2 -38
  22. package/src/core/compose/index.ts +13 -29
  23. package/src/core/gestures/index.ts +31 -0
  24. package/src/core/gestures/longpress.ts +68 -0
  25. package/src/core/gestures/manager.ts +418 -0
  26. package/src/core/gestures/pan.ts +48 -0
  27. package/src/core/gestures/pinch.ts +58 -0
  28. package/src/core/gestures/rotate.ts +58 -0
  29. package/src/core/gestures/swipe.ts +66 -0
  30. package/src/core/gestures/tap.ts +45 -0
  31. package/src/core/gestures/types.ts +387 -0
  32. package/src/core/gestures/utils.ts +128 -0
  33. package/src/core/index.ts +27 -151
  34. package/src/core/layout/schema.ts +73 -35
  35. package/src/core/layout/types.ts +5 -2
  36. package/src/core/viewport/constants.ts +140 -0
  37. package/src/core/viewport/features/base.ts +73 -0
  38. package/src/core/viewport/features/collection.ts +882 -0
  39. package/src/core/viewport/features/events.ts +130 -0
  40. package/src/core/viewport/features/index.ts +20 -0
  41. package/src/core/{list-manager/features/viewport → viewport/features}/item-size.ts +27 -30
  42. package/src/core/{list-manager/features/viewport → viewport/features}/loading.ts +4 -4
  43. package/src/core/viewport/features/momentum.ts +260 -0
  44. package/src/core/viewport/features/placeholders.ts +335 -0
  45. package/src/core/viewport/features/rendering.ts +568 -0
  46. package/src/core/viewport/features/scrollbar.ts +434 -0
  47. package/src/core/viewport/features/scrolling.ts +618 -0
  48. package/src/core/viewport/features/utils.ts +88 -0
  49. package/src/core/viewport/features/virtual.ts +384 -0
  50. package/src/core/viewport/index.ts +31 -0
  51. package/src/core/viewport/types.ts +133 -0
  52. package/src/core/viewport/utils/speed-tracker.ts +79 -0
  53. package/src/core/viewport/viewport.ts +246 -0
  54. package/src/index.ts +0 -7
  55. package/src/styles/components/_vlist.scss +331 -0
  56. package/src/styles/index.scss +1 -1
  57. package/test/components/vlist-selection.test.ts +240 -0
  58. package/test/components/vlist.test.ts +63 -0
  59. package/test/core/collection/adapter.test.ts +161 -0
  60. package/bun.lock +0 -792
  61. package/src/components/list/api.ts +0 -314
  62. package/src/components/list/constants.ts +0 -56
  63. package/src/components/list/features/api.ts +0 -428
  64. package/src/components/list/features/index.ts +0 -31
  65. package/src/components/list/features/list-manager.ts +0 -502
  66. package/src/components/list/index.ts +0 -39
  67. package/src/components/list/list.ts +0 -234
  68. package/src/core/collection/base-collection.ts +0 -100
  69. package/src/core/collection/collection-composer.ts +0 -178
  70. package/src/core/collection/collection.ts +0 -745
  71. package/src/core/collection/constants.ts +0 -172
  72. package/src/core/collection/events.ts +0 -428
  73. package/src/core/collection/features/api/loading.ts +0 -279
  74. package/src/core/collection/features/operations/data-operations.ts +0 -147
  75. package/src/core/collection/index.ts +0 -104
  76. package/src/core/collection/state.ts +0 -497
  77. package/src/core/collection/types.ts +0 -404
  78. package/src/core/compose/features/collection.ts +0 -119
  79. package/src/core/compose/features/selection.ts +0 -213
  80. package/src/core/compose/features/styling.ts +0 -108
  81. package/src/core/list-manager/api.ts +0 -599
  82. package/src/core/list-manager/config.ts +0 -593
  83. package/src/core/list-manager/constants.ts +0 -268
  84. package/src/core/list-manager/features/api.ts +0 -58
  85. package/src/core/list-manager/features/collection/collection.ts +0 -705
  86. package/src/core/list-manager/features/collection/index.ts +0 -17
  87. package/src/core/list-manager/features/viewport/constants.ts +0 -42
  88. package/src/core/list-manager/features/viewport/index.ts +0 -16
  89. package/src/core/list-manager/features/viewport/placeholders.ts +0 -281
  90. package/src/core/list-manager/features/viewport/rendering.ts +0 -575
  91. package/src/core/list-manager/features/viewport/scrollbar.ts +0 -495
  92. package/src/core/list-manager/features/viewport/scrolling.ts +0 -795
  93. package/src/core/list-manager/features/viewport/template.ts +0 -220
  94. package/src/core/list-manager/features/viewport/viewport.ts +0 -654
  95. package/src/core/list-manager/features/viewport/virtual.ts +0 -309
  96. package/src/core/list-manager/index.ts +0 -279
  97. package/src/core/list-manager/list-manager.ts +0 -206
  98. package/src/core/list-manager/types.ts +0 -439
  99. package/src/core/list-manager/utils/calculations.ts +0 -290
  100. package/src/core/list-manager/utils/range-calculator.ts +0 -349
  101. package/src/core/list-manager/utils/speed-tracker.ts +0 -273
  102. package/src/styles/components/_list.scss +0 -244
  103. package/src/types/mtrl.d.ts +0 -6
  104. package/test/components/list.test.ts +0 -256
  105. package/test/core/collection/failed-ranges.test.ts +0 -270
  106. package/test/core/compose/features.test.ts +0 -183
  107. package/test/core/list-manager/features/collection.test.ts +0 -704
  108. package/test/core/list-manager/features/viewport.test.ts +0 -698
  109. package/test/core/list-manager/list-manager.test.ts +0 -593
  110. package/test/core/list-manager/utils/calculations.test.ts +0 -433
  111. package/test/core/list-manager/utils/range-calculator.test.ts +0 -569
  112. package/test/core/list-manager/utils/speed-tracker.test.ts +0 -530
  113. package/tsconfig.build.json +0 -23
  114. /package/src/components/{list → vlist}/features.ts +0 -0
  115. /package/src/core/{compose → viewport}/features/performance.ts +0 -0
@@ -0,0 +1,384 @@
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
+ }
17
+
18
+ /**
19
+ * Virtual scrolling feature for viewport
20
+ * Handles visible range calculations with compression for large datasets
21
+ */
22
+ export const withVirtual = (config: VirtualConfig = {}) => {
23
+ return <T extends ViewportContext & ViewportComponent>(component: T): T => {
24
+ const {
25
+ itemSize,
26
+ overscan = VIEWPORT_CONSTANTS.VIRTUAL_SCROLL.OVERSCAN_BUFFER,
27
+ orientation = "vertical",
28
+ autoDetectItemSize = itemSize === undefined
29
+ ? VIEWPORT_CONSTANTS.VIRTUAL_SCROLL.AUTO_DETECT_ITEM_SIZE
30
+ : false,
31
+ debug = false,
32
+ } = config;
33
+
34
+ // Use provided itemSize or default, but mark if we should auto-detect
35
+ const initialItemSize =
36
+ itemSize || VIEWPORT_CONSTANTS.VIRTUAL_SCROLL.DEFAULT_ITEM_SIZE;
37
+
38
+ const MAX_VIRTUAL_SIZE = VIEWPORT_CONSTANTS.VIRTUAL_SCROLL.MAX_VIRTUAL_SIZE;
39
+ let viewportState: any;
40
+ let hasCalculatedItemSize = false;
41
+
42
+ // Initialize using shared wrapper
43
+ wrapInitialize(component, () => {
44
+ viewportState = getViewportState(component);
45
+ if (!viewportState) return;
46
+
47
+ Object.assign(viewportState, {
48
+ itemSize: initialItemSize,
49
+ overscan,
50
+ containerSize:
51
+ viewportState.viewportElement?.[
52
+ orientation === "horizontal" ? "offsetWidth" : "offsetHeight"
53
+ ] || 600,
54
+ });
55
+
56
+ updateTotalVirtualSize(viewportState.totalItems);
57
+ updateVisibleRange(viewportState.scrollPosition || 0);
58
+
59
+ // Ensure container size is measured after DOM is ready
60
+ requestAnimationFrame(() => {
61
+ updateContainerSize();
62
+ });
63
+ });
64
+
65
+ // Helper functions
66
+ const getCompressionRatio = (): number => {
67
+ if (!viewportState?.virtualTotalSize) return 1;
68
+ const actualSize = viewportState.totalItems * viewportState.itemSize;
69
+ return actualSize <= MAX_VIRTUAL_SIZE ? 1 : MAX_VIRTUAL_SIZE / actualSize;
70
+ };
71
+
72
+ const log = (message: string, data?: any) => {
73
+ if (debug) console.log(`[Virtual] ${message}`, data);
74
+ };
75
+
76
+ // Calculate visible range
77
+ const calculateVisibleRange = (
78
+ scrollPosition: number
79
+ ): { start: number; end: number } => {
80
+ if (!viewportState) {
81
+ console.warn("[Virtual] No viewport state, returning empty range");
82
+ return { start: 0, end: 0 };
83
+ }
84
+
85
+ const { containerSize, totalItems } = viewportState;
86
+
87
+ // Early returns for invalid states
88
+ if (
89
+ !containerSize ||
90
+ containerSize <= 0 ||
91
+ !totalItems ||
92
+ totalItems <= 0
93
+ ) {
94
+ log(`Invalid state: container=${containerSize}, items=${totalItems}`);
95
+ return { start: 0, end: 0 };
96
+ }
97
+
98
+ const virtualSize =
99
+ viewportState.virtualTotalSize || totalItems * viewportState.itemSize;
100
+ const visibleCount = Math.ceil(containerSize / viewportState.itemSize);
101
+ const compressionRatio = getCompressionRatio();
102
+
103
+ let start: number, end: number;
104
+
105
+ if (compressionRatio < 1) {
106
+ // Compressed space calculation
107
+ const scrollRatio = scrollPosition / virtualSize;
108
+ const exactIndex = scrollRatio * totalItems;
109
+ start = Math.floor(exactIndex);
110
+ end = Math.ceil(exactIndex) + visibleCount;
111
+
112
+ // Near-bottom handling
113
+ const maxScroll = virtualSize - containerSize;
114
+ const distanceFromBottom = maxScroll - scrollPosition;
115
+
116
+ if (distanceFromBottom <= containerSize && distanceFromBottom >= -1) {
117
+ const itemsAtBottom = Math.floor(
118
+ containerSize / viewportState.itemSize
119
+ );
120
+ const firstVisibleAtBottom = Math.max(0, totalItems - itemsAtBottom);
121
+ const interpolation = Math.max(
122
+ 0,
123
+ Math.min(1, 1 - distanceFromBottom / containerSize)
124
+ );
125
+
126
+ start = Math.floor(
127
+ start + (firstVisibleAtBottom - start) * interpolation
128
+ );
129
+ end =
130
+ distanceFromBottom <= 1
131
+ ? totalItems - 1
132
+ : Math.min(totalItems - 1, start + visibleCount + overscan);
133
+
134
+ log("Near bottom calculation:", {
135
+ distanceFromBottom,
136
+ interpolation,
137
+ start,
138
+ end,
139
+ scrollPosition,
140
+ totalItems,
141
+ });
142
+ }
143
+
144
+ // Apply overscan
145
+ start = Math.max(0, start - overscan);
146
+ end = Math.min(totalItems - 1, end + overscan);
147
+ } else {
148
+ // Direct calculation
149
+ start = Math.max(
150
+ 0,
151
+ Math.floor(scrollPosition / viewportState.itemSize) - overscan
152
+ );
153
+ end = Math.min(totalItems - 1, start + visibleCount + overscan * 2);
154
+ }
155
+
156
+ // Validate output
157
+ if (isNaN(start) || isNaN(end)) {
158
+ console.error("[Virtual] NaN in range calculation:", {
159
+ scrollPosition,
160
+ containerSize,
161
+ totalItems,
162
+ itemSize: viewportState.itemSize,
163
+ compressionRatio,
164
+ });
165
+ return { start: 0, end: 0 };
166
+ }
167
+
168
+ // log(`Range: ${start}-${end} (scroll: ${scrollPosition})`);
169
+
170
+ // Strategic log for last range
171
+ // if (end >= totalItems - 10) {
172
+ // console.log(
173
+ // `[Virtual] Near end range: ${start}-${end}, totalItems=${totalItems}, lastItemPos=${
174
+ // end * viewportState.itemSize
175
+ // }px, virtualSize=${viewportState.virtualTotalSize}px`
176
+ // );
177
+ // }
178
+
179
+ return { start, end };
180
+ };
181
+
182
+ // Update functions
183
+ const updateVisibleRange = (scrollPosition: number) => {
184
+ if (!viewportState) return;
185
+ viewportState.visibleRange = calculateVisibleRange(scrollPosition);
186
+ component.emit?.("viewport:range-changed", {
187
+ range: viewportState.visibleRange,
188
+ scrollPosition,
189
+ });
190
+ };
191
+
192
+ // Update total virtual size
193
+ const updateTotalVirtualSize = (totalItems: number) => {
194
+ if (!viewportState) return;
195
+
196
+ const oldSize = viewportState.virtualTotalSize;
197
+ viewportState.totalItems = totalItems;
198
+ const actualSize = totalItems * viewportState.itemSize;
199
+
200
+ // Get padding from the items container if available
201
+ let totalPadding = 0;
202
+ if (viewportState.itemsContainer) {
203
+ const computedStyle = window.getComputedStyle(
204
+ viewportState.itemsContainer
205
+ );
206
+ const paddingTop = parseFloat(computedStyle.paddingTop) || 0;
207
+ const paddingBottom = parseFloat(computedStyle.paddingBottom) || 0;
208
+ totalPadding = paddingTop + paddingBottom;
209
+ }
210
+
211
+ // Include padding in the virtual size
212
+ viewportState.virtualTotalSize = Math.min(
213
+ actualSize + totalPadding,
214
+ MAX_VIRTUAL_SIZE
215
+ );
216
+
217
+ // Strategic log for debugging gap issue
218
+ // console.log(
219
+ // `[Virtual] Total size update: items=${totalItems}, itemSize=${viewportState.itemSize}px, padding=${totalPadding}px, virtualSize=${viewportState.virtualTotalSize}px (was ${oldSize}px)`
220
+ // );
221
+
222
+ component.emit?.("viewport:virtual-size-changed", {
223
+ totalVirtualSize: viewportState.virtualTotalSize,
224
+ totalItems: totalItems,
225
+ compressionRatio: getCompressionRatio(),
226
+ });
227
+ };
228
+
229
+ // Update container size
230
+ const updateContainerSize = () => {
231
+ if (!viewportState || !viewportState.viewportElement) return;
232
+
233
+ const size =
234
+ viewportState.viewportElement[
235
+ viewportState.orientation === "horizontal"
236
+ ? "offsetWidth"
237
+ : "offsetHeight"
238
+ ];
239
+
240
+ // Log the actual measurement
241
+ // console.log(
242
+ // `[Virtual] Container size measured: ${size}px from viewport element`
243
+ // );
244
+
245
+ if (size !== viewportState.containerSize) {
246
+ viewportState.containerSize = size;
247
+ updateVisibleRange(viewportState.scrollPosition || 0);
248
+ updateTotalVirtualSize(viewportState.totalItems); // Also update virtual size
249
+ component.emit?.("viewport:container-size-changed", {
250
+ containerSize: size,
251
+ });
252
+ }
253
+ };
254
+
255
+ // Override viewport methods
256
+ component.viewport.getVisibleRange = () =>
257
+ viewportState?.visibleRange ||
258
+ calculateVisibleRange(viewportState?.scrollPosition || 0);
259
+
260
+ // Event listeners
261
+ component.on?.("viewport:scroll", (data: any) =>
262
+ updateVisibleRange(data.position)
263
+ );
264
+
265
+ component.on?.("viewport:items-changed", (data: any) => {
266
+ if (data.totalItems !== undefined) {
267
+ updateTotalVirtualSize(data.totalItems);
268
+ updateVisibleRange(viewportState?.scrollPosition || 0);
269
+ }
270
+ });
271
+
272
+ // Listen for total items changes (important for cursor pagination)
273
+ component.on?.("viewport:total-items-changed", (data: any) => {
274
+ if (
275
+ data.total !== undefined &&
276
+ data.total !== viewportState?.totalItems
277
+ ) {
278
+ log(
279
+ `Total items changed from ${viewportState?.totalItems} to ${data.total}`
280
+ );
281
+ updateTotalVirtualSize(data.total);
282
+ updateVisibleRange(viewportState?.scrollPosition || 0);
283
+
284
+ // Trigger a render to update the view
285
+ component.viewport?.renderItems?.();
286
+ }
287
+ });
288
+
289
+ // Listen for container size changes to recalculate virtual size
290
+ component.on?.("viewport:container-size-changed", (data: any) => {
291
+ if (viewportState && data.containerSize !== viewportState.containerSize) {
292
+ viewportState.containerSize = data.containerSize;
293
+ // Recalculate virtual size with new container size
294
+ updateTotalVirtualSize(viewportState.totalItems);
295
+ updateVisibleRange(viewportState.scrollPosition || 0);
296
+ }
297
+ });
298
+
299
+ component.on?.("collection:range-loaded", (data: any) => {
300
+ if (
301
+ data.total !== undefined &&
302
+ data.total !== viewportState?.totalItems
303
+ ) {
304
+ updateTotalVirtualSize(data.total);
305
+ }
306
+ updateVisibleRange(viewportState?.scrollPosition || 0);
307
+ component.viewport?.renderItems?.();
308
+ });
309
+
310
+ // Listen for first items rendered to calculate item size (if auto-detection is enabled)
311
+ if (autoDetectItemSize) {
312
+ component.on?.("viewport:items-rendered", (data: any) => {
313
+ if (
314
+ !hasCalculatedItemSize &&
315
+ data.elements?.length > 0 &&
316
+ viewportState
317
+ ) {
318
+ // Calculate average item size from first rendered batch
319
+ const sizes: number[] = [];
320
+ const sizeProperty =
321
+ orientation === "horizontal" ? "offsetWidth" : "offsetHeight";
322
+
323
+ data.elements.forEach((element: HTMLElement) => {
324
+ const size = element[sizeProperty];
325
+ if (size > 0) {
326
+ sizes.push(size);
327
+ }
328
+ });
329
+
330
+ if (sizes.length > 0) {
331
+ const avgSize = Math.round(
332
+ sizes.reduce((sum, size) => sum + size, 0) / sizes.length
333
+ );
334
+
335
+ // console.log(
336
+ // `[Virtual] Auto-detected item size: ${avgSize}px (was ${viewportState.itemSize}px), based on ${sizes.length} items`
337
+ // );
338
+ viewportState.itemSize = avgSize;
339
+
340
+ // Recalculate everything with new size
341
+ updateTotalVirtualSize(viewportState.totalItems);
342
+ updateVisibleRange(viewportState.scrollPosition || 0);
343
+
344
+ // Re-render to adjust positions
345
+ component.viewport?.renderItems?.();
346
+
347
+ // Emit event for size change
348
+ component.emit?.("viewport:item-size-detected", {
349
+ previousSize: initialItemSize,
350
+ detectedSize: avgSize,
351
+ });
352
+
353
+ hasCalculatedItemSize = true;
354
+ }
355
+ }
356
+ });
357
+ }
358
+
359
+ // Expose virtual API
360
+ const compressionRatio = getCompressionRatio();
361
+ (component as any).virtual = {
362
+ calculateVisibleRange,
363
+ updateTotalVirtualSize,
364
+ getTotalVirtualSize: () => viewportState?.virtualTotalSize || 0,
365
+ getContainerSize: () => viewportState?.containerSize || 0,
366
+ updateContainerSize: (size: number) => {
367
+ if (viewportState) {
368
+ viewportState.containerSize = size;
369
+ updateVisibleRange(viewportState.scrollPosition || 0);
370
+ }
371
+ },
372
+ getItemSize: () => viewportState?.itemSize || initialItemSize,
373
+ calculateIndexFromPosition: (position: number) =>
374
+ Math.floor(
375
+ position /
376
+ ((viewportState?.itemSize || initialItemSize) * compressionRatio)
377
+ ),
378
+ calculatePositionForIndex: (index: number) =>
379
+ index * (viewportState?.itemSize || initialItemSize) * compressionRatio,
380
+ };
381
+
382
+ return component;
383
+ };
384
+ };
@@ -0,0 +1,31 @@
1
+ // src/core/viewport/index.ts
2
+
3
+ /**
4
+ * Viewport Module - High-Performance Virtual Scrolling
5
+ * A composable, feature-based virtual scrolling solution
6
+ */
7
+
8
+ // Main viewport creator
9
+ export * from "./viewport";
10
+
11
+ // Feature enhancers (for custom composition)
12
+ export { withBase } from "./features/base";
13
+ export { withVirtual } from "./features/virtual";
14
+ export { withScrolling } from "./features/scrolling";
15
+ export { withScrollbar } from "./features/scrollbar";
16
+ export { withRendering } from "./features/rendering";
17
+ export { withCollection } from "./features/collection";
18
+ export { withPlaceholders } from "./features/placeholders";
19
+ export { withEvents } from "./features/events";
20
+
21
+ // Types
22
+ export type {
23
+ ViewportConfig,
24
+ ViewportComponent,
25
+ ViewportContext,
26
+ ItemRange,
27
+ ViewportInfo,
28
+ } from "./types";
29
+
30
+ // Constants
31
+ export { VIEWPORT_CONSTANTS } from "./constants";
@@ -0,0 +1,133 @@
1
+ // src/core/viewport/types.ts
2
+
3
+ /**
4
+ * Core type definitions for Viewport
5
+ * Virtual scrolling and rendering types
6
+ */
7
+
8
+ import type { CollectionComponent } from "./features/collection";
9
+
10
+ /**
11
+ * Core Range and Item Types
12
+ */
13
+ export interface ItemRange {
14
+ start: number;
15
+ end: number;
16
+ }
17
+
18
+ export interface ViewportInfo {
19
+ containerSize: number;
20
+ totalVirtualSize: number;
21
+ visibleRange: ItemRange;
22
+ virtualScrollPosition: number;
23
+ }
24
+
25
+ /**
26
+ * Viewport context interface - provides context for viewport features
27
+ */
28
+ export interface ViewportContext {
29
+ element: HTMLElement;
30
+ items: any[];
31
+ totalItems: number;
32
+ template?: (item: any, index: number) => string | HTMLElement;
33
+ emit?: (event: string, data?: any) => void;
34
+ on?: (event: string, handler: Function) => () => void;
35
+ once?: (event: string, handler: Function) => () => void;
36
+ off?: (event: string, handler: Function) => void;
37
+ getClass?: (name: string) => string;
38
+ }
39
+
40
+ /**
41
+ * Viewport configuration - Feature-oriented structure
42
+ */
43
+ export interface ViewportConfig {
44
+ // Basic configuration
45
+ element?: HTMLElement;
46
+ className?: string;
47
+ debug?: boolean;
48
+
49
+ // Template for rendering items
50
+ template?: (
51
+ item: any,
52
+ index: number
53
+ ) => string | HTMLElement | any[] | Record<string, any>;
54
+
55
+ // Collection/data source configuration
56
+ collection?: {
57
+ adapter?: any; // Data adapter
58
+ transform?: (item: any) => any; // Transform items before rendering
59
+ };
60
+
61
+ // Pagination configuration
62
+ pagination?: {
63
+ strategy?: "page" | "offset" | "cursor";
64
+ limit?: number; // Items per page/range
65
+ };
66
+
67
+ // Virtual scrolling configuration
68
+ virtual?: {
69
+ itemSize?: number;
70
+ overscan?: number; // Extra items to render outside viewport
71
+ autoDetectItemSize?: boolean; // Auto-detect item size from first render
72
+ };
73
+
74
+ // Scrolling configuration
75
+ scrolling?: {
76
+ orientation?: "vertical" | "horizontal";
77
+ animation?: boolean; // Smooth scrolling
78
+ sensitivity?: number; // Scroll sensitivity
79
+ };
80
+
81
+ // Scrollbar configuration
82
+ scrollbar?: {
83
+ enabled?: boolean;
84
+ autoHide?: boolean;
85
+ };
86
+
87
+ // Performance configuration
88
+ performance?: {
89
+ maxConcurrentRequests?: number;
90
+ cancelLoadThreshold?: number; // Velocity threshold for cancelling loads
91
+ enableRequestQueue?: boolean;
92
+ recycleElements?: boolean;
93
+ bufferSize?: number;
94
+ renderDebounce?: number;
95
+ };
96
+
97
+ // Placeholder configuration
98
+ placeholders?: {
99
+ enabled?: boolean;
100
+ template?: (index: number) => string | HTMLElement;
101
+ maskCharacter?: string;
102
+ analyzeFirstLoad?: boolean;
103
+ };
104
+ }
105
+
106
+ /**
107
+ * Enhanced component after viewport is applied
108
+ */
109
+ export interface ViewportComponent extends ViewportContext {
110
+ viewport: {
111
+ // Core API
112
+ initialize(): void;
113
+ destroy(): void;
114
+ updateViewport(): void;
115
+
116
+ // Scrolling
117
+ scrollToIndex(index: number, alignment?: "start" | "center" | "end"): void;
118
+ scrollToPosition(position: number): void;
119
+ getScrollPosition(): number;
120
+ scrollBy(delta: number): void;
121
+ getVelocity(): number;
122
+
123
+ // Collection feature (optional)
124
+ collection?: CollectionComponent["collection"];
125
+
126
+ // Info
127
+ getVisibleRange(): ItemRange;
128
+ getViewportInfo(): ViewportInfo;
129
+
130
+ // Rendering
131
+ renderItems(): void;
132
+ };
133
+ }
@@ -0,0 +1,79 @@
1
+ // src/core/viewport/utils/speed-tracker.ts
2
+
3
+ /**
4
+ * Speed tracking utility for viewport
5
+ * Measures velocity and determines loading strategies
6
+ */
7
+
8
+ import { VIEWPORT_CONSTANTS } from "../constants";
9
+
10
+ export interface SpeedTracker {
11
+ velocity: number;
12
+ direction: "forward" | "backward";
13
+ isAccelerating: boolean;
14
+ lastMeasurement: number;
15
+ }
16
+
17
+ /**
18
+ * Create a new speed tracker instance
19
+ */
20
+ export function createSpeedTracker(): SpeedTracker {
21
+ return {
22
+ velocity: 0,
23
+ direction: "forward",
24
+ isAccelerating: false,
25
+ lastMeasurement: Date.now(),
26
+ };
27
+ }
28
+
29
+ /**
30
+ * Update speed tracker with new scroll position
31
+ */
32
+ export function updateSpeedTracker(
33
+ tracker: SpeedTracker,
34
+ newPosition: number,
35
+ previousPosition: number
36
+ ): SpeedTracker {
37
+ const now = Date.now();
38
+ const deltaTime = now - tracker.lastMeasurement;
39
+ const deltaPosition = newPosition - previousPosition;
40
+
41
+ // Calculate velocity (px/ms)
42
+ const currentVelocity =
43
+ deltaTime > 0 ? Math.abs(deltaPosition) / deltaTime : 0;
44
+
45
+ // Determine direction
46
+ const direction = deltaPosition >= 0 ? "forward" : "backward";
47
+
48
+ // Check if accelerating
49
+ const isAccelerating = currentVelocity > tracker.velocity;
50
+
51
+ // Apply smoothing for more stable velocity tracking
52
+ const smoothingFactor = 0.85;
53
+ const smoothedVelocity =
54
+ tracker.velocity * smoothingFactor +
55
+ currentVelocity * (1 - smoothingFactor);
56
+
57
+ return {
58
+ velocity: smoothedVelocity,
59
+ direction,
60
+ isAccelerating,
61
+ lastMeasurement: now,
62
+ };
63
+ }
64
+
65
+ /**
66
+ * Determine if scroll speed is fast (should defer loading)
67
+ */
68
+ export function isFastScrolling(tracker: SpeedTracker): boolean {
69
+ return tracker.velocity > VIEWPORT_CONSTANTS.LOADING.CANCEL_THRESHOLD;
70
+ }
71
+
72
+ /**
73
+ * Get loading strategy based on current speed
74
+ */
75
+ export function getLoadingStrategy(
76
+ tracker: SpeedTracker
77
+ ): "defer" | "immediate" {
78
+ return isFastScrolling(tracker) ? "defer" : "immediate";
79
+ }