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
@@ -5,19 +5,10 @@
5
5
  * Follows mtrl patterns for component configuration.
6
6
  */
7
7
 
8
- import {
9
- createComponentConfig,
10
- createElementConfig as coreCreateElementConfig,
11
- } from "mtrl/src/core/config/component";
12
- import { LIST_DEFAULTS, LIST_CLASSES } from "./constants";
8
+ import { createComponentConfig, createElementConfig } from "mtrl";
9
+ import { VLIST_CLASSES } from "./constants";
13
10
  import type { ListConfig, ListItem } from "./types";
14
11
  import type { ListComponent } from "./types";
15
- import { DATA_PAGINATION } from "../../core/collection/constants";
16
- import { VIRTUAL_SCROLLING } from "../../core/list-manager/constants";
17
- import {
18
- convertRenderItemToTemplate,
19
- getDefaultTemplate,
20
- } from "../../core/list-manager/features/viewport/template";
21
12
 
22
13
  /**
23
14
  * Default configuration for the mtrl-addons List component
@@ -42,15 +33,14 @@ export const defaultConfig: Partial<ListConfig> = {
42
33
  // Scroll settings
43
34
  scroll: {
44
35
  virtual: true,
45
- itemSize: "auto",
46
- estimatedItemSize: 50,
36
+ itemSize: 50,
47
37
  overscan: 5,
48
38
  animation: false,
49
39
  restorePosition: false,
50
40
  },
51
41
 
52
42
  // Component settings
53
- className: LIST_CLASSES.BASE,
43
+ className: VLIST_CLASSES.LIST,
54
44
  prefix: "mtrl",
55
45
  componentName: "list",
56
46
  ariaLabel: "List",
@@ -62,6 +52,59 @@ export const defaultConfig: Partial<ListConfig> = {
62
52
  * @param {ListConfig} config - User provided configuration
63
53
  * @returns {ListConfig} Complete configuration with defaults applied
64
54
  */
55
+ // Minimal converter to avoid hardcoding external helpers
56
+ function convertRenderItemToTemplate(render: any) {
57
+ if (typeof render === "function") return render;
58
+ if (render && typeof render === "object") {
59
+ const { headline, supporting, leading, trailing } = render as any;
60
+ return (item: any, index: number) => {
61
+ const container = document.createElement("div");
62
+ container.className = "mtrl-list-item";
63
+ if (leading) {
64
+ const el = document.createElement("div");
65
+ el.className = "mtrl-list-item__leading";
66
+ el.innerHTML =
67
+ typeof leading === "function"
68
+ ? leading(item, index)
69
+ : String(leading);
70
+ container.appendChild(el);
71
+ }
72
+ const content = document.createElement("div");
73
+ content.className = "mtrl-list-item__content";
74
+ if (headline) {
75
+ const h = document.createElement("div");
76
+ h.className = "mtrl-list-item__headline";
77
+ h.innerHTML =
78
+ typeof headline === "function"
79
+ ? headline(item, index)
80
+ : String(headline);
81
+ content.appendChild(h);
82
+ }
83
+ if (supporting) {
84
+ const s = document.createElement("div");
85
+ s.className = "mtrl-list-item__supporting";
86
+ s.innerHTML =
87
+ typeof supporting === "function"
88
+ ? supporting(item, index)
89
+ : String(supporting);
90
+ content.appendChild(s);
91
+ }
92
+ container.appendChild(content);
93
+ if (trailing) {
94
+ const el = document.createElement("div");
95
+ el.className = "mtrl-list-item__trailing";
96
+ el.innerHTML =
97
+ typeof trailing === "function"
98
+ ? trailing(item, index)
99
+ : String(trailing);
100
+ container.appendChild(el);
101
+ }
102
+ return container;
103
+ };
104
+ }
105
+ return (item: any) => String(item);
106
+ }
107
+
65
108
  export function createBaseConfig<T extends ListItem = ListItem>(
66
109
  config: ListConfig<T> = {}
67
110
  ): Required<ListConfig<T>> {
@@ -74,15 +117,14 @@ export function createBaseConfig<T extends ListItem = ListItem>(
74
117
  );
75
118
  }
76
119
 
77
- // Use mtrl core config system for proper merging and validation
78
- const mergedConfig = createComponentConfig(defaultConfig, config, "list");
120
+ const mergedConfig = createComponentConfig(
121
+ defaultConfig as any,
122
+ config as any,
123
+ "list"
124
+ ) as any;
79
125
 
80
126
  // Convert renderItem object to template function if provided
81
- if (
82
- (config as any).renderItem &&
83
- typeof (config as any).renderItem === "object" &&
84
- !mergedConfig.template
85
- ) {
127
+ if ((config as any).renderItem && !mergedConfig.template) {
86
128
  mergedConfig.template = convertRenderItemToTemplate(
87
129
  (config as any).renderItem
88
130
  );
@@ -114,11 +156,10 @@ export function getElementConfig<T extends ListItem = ListItem>(
114
156
  "aria-label": config.ariaLabel || "List",
115
157
  };
116
158
 
117
- // Create element config using mtrl core system
118
- return coreCreateElementConfig(config, {
159
+ return createElementConfig(config as any, {
119
160
  tag: "div",
120
161
  attributes,
121
- className: [LIST_CLASSES.BASE, LIST_CLASSES.ADDONS], // Clean: list list-addons
162
+ className: VLIST_CLASSES.LIST,
122
163
  });
123
164
  }
124
165
 
@@ -274,79 +315,9 @@ export const validateConfig = (config: ListConfig): void => {
274
315
  export const getCollectionConfig = (config: ListConfig) => ({
275
316
  adapter: config.adapter,
276
317
  items: config.items,
277
- pageSize: config.collection?.limit || DATA_PAGINATION.DEFAULT_PAGE_SIZE,
278
- cache: config.collection?.cache || {
318
+ cache: (config.collection as any)?.cache || {
279
319
  enabled: true,
280
320
  maxSize: 1000,
281
321
  ttl: 300000, // 5 minutes
282
322
  },
283
323
  });
284
-
285
- /**
286
- * Creates List Manager configuration from List config
287
- */
288
- export const getListManagerConfig = (config: ListConfig) => ({
289
- // Container
290
- container: config.container,
291
-
292
- // Collection configuration
293
- collection: getCollectionConfig(config),
294
-
295
- // Template configuration
296
- template: {
297
- template: config.template,
298
- },
299
-
300
- // Component prefix
301
- prefix: config.prefix || "mtrl",
302
-
303
- // Orientation configuration
304
- orientation: {
305
- orientation: config.orientation?.orientation || "vertical",
306
- autoDetect: config.orientation?.autoDetect || false,
307
- reverse: config.orientation?.reverse || false,
308
- crossAxisAlignment: config.orientation?.crossAxisAlignment || "stretch",
309
- },
310
-
311
- // Virtual scrolling
312
- virtual: {
313
- enabled: config.scroll?.virtual ?? true,
314
- itemSize: config.scroll?.itemSize ?? "auto",
315
- estimatedItemSize: config.scroll?.estimatedItemSize ?? 50,
316
- overscan: config.scroll?.overscan ?? VIRTUAL_SCROLLING.DEFAULT_OVERSCAN,
317
- ...config.listManager?.virtual,
318
- },
319
-
320
- // Element recycling
321
- recycling: {
322
- enabled: config.listManager?.recycling?.enabled ?? true,
323
- maxPoolSize: config.listManager?.recycling?.maxPoolSize ?? 100,
324
- minPoolSize: config.listManager?.recycling?.minPoolSize ?? 10,
325
- ...config.listManager?.recycling,
326
- },
327
-
328
- // Intersection observers
329
- intersection: {
330
- pagination: {
331
- enabled: true,
332
- rootMargin: "200px",
333
- threshold: 0.1,
334
- ...config.listManager?.intersection?.pagination,
335
- },
336
- loading: {
337
- enabled: true,
338
- ...config.listManager?.intersection?.loading,
339
- },
340
- },
341
-
342
- // Performance configuration
343
- performance: {
344
- frameScheduling: true,
345
- memoryCleanup: true,
346
- ...config.listManager?.performance,
347
- },
348
-
349
- // Debug and styling
350
- debug: config.debug || false,
351
- componentName: "list-manager",
352
- });
@@ -0,0 +1,23 @@
1
+ /**
2
+ * mtrl-addons List Component Constants
3
+ *
4
+ * Constants for the addons list component.
5
+ * Note: Base mtrl list functionality is handled by mtrl core.
6
+ * These constants are for addons-specific features only.
7
+ */
8
+
9
+ /**
10
+ * CSS class names for VList component
11
+ * Following BEM convention: component__element--modifier
12
+ * Note: mtrl prefix is added automatically by core DOM classes system
13
+ */
14
+ export const VLIST_CLASSES = {
15
+ /** List element */
16
+ LIST: "vlist",
17
+ /** List item */
18
+ ITEM: "viewport-item",
19
+ /** Selected list item */
20
+ SELECTED: "viewport-item--selected",
21
+ /** Empty state */
22
+ EMPTY: "vlist--empty",
23
+ } as const;
@@ -0,0 +1,322 @@
1
+ /**
2
+ * API feature for VList
3
+ * Provides a clean public API for the VList component
4
+ */
5
+
6
+ import type { VListConfig, VListItem } from "../types";
7
+
8
+ /**
9
+ * Adds public API methods to VList
10
+ */
11
+ export const withAPI = <T extends VListItem = VListItem>(
12
+ config: VListConfig<T>
13
+ ) => {
14
+ return (component: any) => {
15
+ // Initialize viewport on creation
16
+ setTimeout(() => {
17
+ if (component.viewport && component.viewport.initialize) {
18
+ component.viewport.initialize();
19
+ }
20
+ }, 0);
21
+
22
+ // Track loading state
23
+ let isLoading = false;
24
+ let selectedIds = new Set<string | number>();
25
+
26
+ // Listen for collection events
27
+ component.on?.("collection:range-loaded", () => {
28
+ isLoading = false;
29
+ });
30
+ component.on?.("collection:range-failed", () => {
31
+ isLoading = false;
32
+ });
33
+
34
+ return {
35
+ ...component,
36
+
37
+ // Data operations
38
+ setItems(items: T[]) {
39
+ if (component.viewport?.collection) {
40
+ component.viewport.collection.setItems(items);
41
+ } else {
42
+ component.items = items;
43
+ component.totalItems = items.length;
44
+ }
45
+ component.emit?.("items:set", { items, total: items.length });
46
+ },
47
+
48
+ getItems(): T[] {
49
+ // Collection is added directly to component, not to viewport.collection
50
+ if ((component as any).collection?.getItems) {
51
+ return (component as any).collection.getItems();
52
+ }
53
+ return component.items || [];
54
+ },
55
+
56
+ getVisibleItems(): T[] {
57
+ const range = component.viewport?.getVisibleRange() || {
58
+ start: 0,
59
+ end: 0,
60
+ };
61
+ const items = this.getItems();
62
+ return items.slice(range.start, range.end);
63
+ },
64
+
65
+ getItemCount(): number {
66
+ if (component.viewport?.collection) {
67
+ return component.viewport.collection.getTotalItems();
68
+ }
69
+ return component.totalItems || component.items?.length || 0;
70
+ },
71
+
72
+ // Loading operations
73
+ async loadRange(
74
+ page: number,
75
+ limit: number,
76
+ strategy: string = "page",
77
+ alignment?: string
78
+ ) {
79
+ isLoading = true;
80
+
81
+ if (component.viewport?.collection) {
82
+ const offset = strategy === "page" ? (page - 1) * limit : page;
83
+ try {
84
+ await component.viewport.collection.loadRange(offset, limit);
85
+ component.emit?.("load", { page, limit, strategy });
86
+
87
+ // Scroll to the loaded range if alignment is specified
88
+ if (alignment) {
89
+ const index = offset;
90
+ this.scrollToIndex(index, alignment);
91
+ }
92
+ } catch (error) {
93
+ component.emit?.("error", { error });
94
+ throw error;
95
+ } finally {
96
+ isLoading = false;
97
+ }
98
+ }
99
+ },
100
+
101
+ // Data loading
102
+ loadNext: async function () {
103
+ console.log(`[VList] loadNext()`);
104
+
105
+ if (component.viewport?.collection) {
106
+ const totalItems = component.viewport.collection.getTotalItems();
107
+ const loadedRanges = component.viewport.collection.getLoadedRanges();
108
+
109
+ // Find the next unloaded range
110
+ let nextOffset = 0;
111
+ for (const rangeId of loadedRanges) {
112
+ const rangeEnd = (rangeId + 1) * (config.pagination?.limit || 20);
113
+ if (rangeEnd > nextOffset) {
114
+ nextOffset = rangeEnd;
115
+ }
116
+ }
117
+
118
+ if (nextOffset < totalItems) {
119
+ await component.viewport.collection.loadRange(
120
+ nextOffset,
121
+ config.pagination?.limit || 20
122
+ );
123
+ }
124
+ }
125
+
126
+ return Promise.resolve();
127
+ },
128
+
129
+ isLoading(): boolean {
130
+ return isLoading;
131
+ },
132
+
133
+ hasNext(): boolean {
134
+ if (component.viewport?.collection) {
135
+ const total = component.viewport.collection.getTotalItems();
136
+ const items = component.viewport.collection.getItems();
137
+ return items.length < total;
138
+ }
139
+ return false;
140
+ },
141
+
142
+ // Note: Selection operations are handled by the withSelection feature
143
+ // These are kept for backward compatibility but delegate to selection feature if available
144
+ getSelectedIds(): (string | number)[] {
145
+ if (typeof component.getSelectedIndices === "function") {
146
+ // Use selection feature if available
147
+ const items = this.getItems();
148
+ const indices = component.getSelectedIndices();
149
+ return indices.map((idx: number) => (items[idx] as any)?.id || idx);
150
+ }
151
+ // Fallback to local tracking
152
+ return Array.from(selectedIds);
153
+ },
154
+
155
+ selectItem(id: string | number) {
156
+ if (typeof component.selectItems === "function") {
157
+ // Use selection feature if available
158
+ const items = this.getItems();
159
+ const index = items.findIndex((item: any) => item.id === id);
160
+ if (index >= 0) {
161
+ component.selectItems([index]);
162
+ }
163
+ } else {
164
+ // Fallback to local tracking
165
+ selectedIds.add(id);
166
+ component.emit?.("selection:change", {
167
+ selected: this.getSelectedIds(),
168
+ });
169
+ }
170
+ },
171
+
172
+ deselectItem(id: string | number) {
173
+ if (typeof component.deselectItems === "function") {
174
+ // Use selection feature if available
175
+ const items = this.getItems();
176
+ const index = items.findIndex((item: any) => item.id === id);
177
+ if (index >= 0) {
178
+ component.deselectItems([index]);
179
+ }
180
+ } else {
181
+ // Fallback to local tracking
182
+ selectedIds.delete(id);
183
+ component.emit?.("selection:change", {
184
+ selected: this.getSelectedIds(),
185
+ });
186
+ }
187
+ },
188
+
189
+ clearSelection() {
190
+ if (typeof component.clearSelection === "function") {
191
+ // Use selection feature if available
192
+ component.clearSelection();
193
+ } else {
194
+ // Fallback to local tracking
195
+ selectedIds.clear();
196
+ component.emit?.("selection:change", { selected: [] });
197
+ }
198
+ },
199
+
200
+ // Scrolling methods
201
+ scrollToIndex: (
202
+ index: number,
203
+ alignment: "start" | "center" | "end" = "start",
204
+ animate?: boolean
205
+ ) => {
206
+ if (component.viewport) {
207
+ component.viewport.scrollToIndex(index, alignment);
208
+ }
209
+ return Promise.resolve();
210
+ },
211
+
212
+ scrollToItem: async function (
213
+ itemId: string,
214
+ alignment: "start" | "center" | "end" = "start",
215
+ animate?: boolean
216
+ ) {
217
+ const items = this.getItems();
218
+ const index = items.findIndex(
219
+ (item: any) => String(item.id) === String(itemId)
220
+ );
221
+
222
+ if (index >= 0) {
223
+ return this.scrollToIndex(index, alignment, animate);
224
+ }
225
+
226
+ console.warn(`Item ${itemId} not found`);
227
+ return Promise.resolve();
228
+ },
229
+
230
+ scrollToTop: function () {
231
+ return this.scrollToIndex(0, "start");
232
+ },
233
+
234
+ scrollToBottom: function () {
235
+ const totalItems = this.getItemCount();
236
+ if (totalItems > 0) {
237
+ return this.scrollToIndex(totalItems - 1, "end");
238
+ }
239
+ return Promise.resolve();
240
+ },
241
+
242
+ scrollToPage: async function (
243
+ pageNum: number,
244
+ alignment: "start" | "center" | "end" = "start",
245
+ animate?: boolean
246
+ ) {
247
+ // console.log(`[VList] scrollToPage(${pageNum})`);
248
+
249
+ // Get limit from config (rangeSize) or default
250
+ const limit = config.pagination?.limit || 20;
251
+
252
+ // Check if we're in cursor mode
253
+ if (config.pagination?.strategy === "cursor") {
254
+ // In cursor mode, we need to handle sequential loading
255
+ const collection = (component.viewport as any)?.collection;
256
+ if (collection) {
257
+ const loadedRanges = collection.getLoadedRanges();
258
+ const highestLoadedPage = loadedRanges.size;
259
+
260
+ if (pageNum > highestLoadedPage + 1) {
261
+ console.warn(
262
+ `[VList] Cannot jump to page ${pageNum} in cursor mode. ` +
263
+ `Loading pages sequentially from ${
264
+ highestLoadedPage + 1
265
+ } to ${pageNum}`
266
+ );
267
+ }
268
+ }
269
+ }
270
+
271
+ // Use viewport's scrollToPage if available
272
+ if ((component.viewport as any)?.scrollToPage) {
273
+ (component.viewport as any).scrollToPage(pageNum, limit, alignment);
274
+ } else {
275
+ // Fallback to scrollToIndex
276
+ const targetIndex = (pageNum - 1) * limit;
277
+ await this.scrollToIndex(targetIndex, alignment);
278
+ }
279
+ },
280
+
281
+ getScrollPosition: () => {
282
+ return component.viewport?.getScrollPosition() || 0;
283
+ },
284
+
285
+ getCurrentCursor: () => {
286
+ const collection = (component.viewport as any)?.collection;
287
+ return collection?.getCurrentCursor?.() || null;
288
+ },
289
+
290
+ setScrollAnimation(enabled: boolean) {
291
+ // Store animation preference (would be used by viewport scrolling feature)
292
+ component.scrollAnimation = enabled;
293
+ },
294
+
295
+ // State
296
+ getState() {
297
+ return {
298
+ items: this.getItems(),
299
+ totalItems: component.viewport?.collection
300
+ ? component.viewport.collection.getTotalItems()
301
+ : component.totalItems || 0,
302
+ visibleRange: component.viewport?.getVisibleRange() || {
303
+ start: 0,
304
+ end: 0,
305
+ },
306
+ scrollPosition: component.viewport?.getScrollPosition() || 0,
307
+ selectedIds: this.getSelectedIds(),
308
+ isLoading: this.isLoading(),
309
+ };
310
+ },
311
+
312
+ // Lifecycle
313
+ destroy() {
314
+ if (component.viewport?.destroy) {
315
+ component.viewport.destroy();
316
+ }
317
+ selectedIds.clear();
318
+ component.emit?.("destroyed");
319
+ },
320
+ };
321
+ };
322
+ };
@@ -0,0 +1,10 @@
1
+ /**
2
+ * VList Component Features
3
+ *
4
+ * List-specific features and enhancements.
5
+ */
6
+
7
+ // VList-specific features
8
+ export { withViewport } from "./viewport";
9
+ export { withAPI } from "./api";
10
+ export { withSelection } from "./selection";