mtrl-addons 0.1.1 → 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 -108
  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 -14
  114. /package/src/components/{list → vlist}/features.ts +0 -0
  115. /package/src/core/{compose → viewport}/features/performance.ts +0 -0
@@ -1,279 +0,0 @@
1
- /**
2
- * API Loading Plugin
3
- *
4
- * Handles data loading from adapters (loadPage, loadMore, refresh)
5
- */
6
-
7
- import type {
8
- CollectionItem,
9
- CollectionAdapter,
10
- BaseCollection,
11
- AdapterResponse,
12
- } from "../../types";
13
- import { CollectionDataEvents } from "../../types";
14
- import { DATA_LOGGING, COLLECTION_DEFAULTS } from "../../constants";
15
- import { createEventPayload, CollectionEvents } from "../../events";
16
-
17
- export interface LoadingConfig {
18
- pageSize?: number;
19
- sequential?: boolean; // Whether to load pages sequentially or allow random access
20
- }
21
-
22
- export interface LoadingFeatures<T extends CollectionItem> {
23
- loadPage(page: number): Promise<AdapterResponse<T>>;
24
- loadMore(): Promise<AdapterResponse<T>>;
25
- refresh(): Promise<AdapterResponse<T>>;
26
- getCurrentPage(): number;
27
- hasNext(): boolean;
28
- }
29
-
30
- /**
31
- * API Loading Plugin (Pure Data Feature)
32
- */
33
- export const withLoading =
34
- <T extends CollectionItem = CollectionItem>(config: LoadingConfig = {}) =>
35
- (
36
- baseCollection: BaseCollection<T>
37
- ): BaseCollection<T> & LoadingFeatures<T> => {
38
- // Get adapter from config
39
- const adapter = baseCollection._config.adapter;
40
- if (!adapter) {
41
- console.warn(
42
- `${DATA_LOGGING.PREFIX} No adapter configured for loading plugin`
43
- );
44
- }
45
-
46
- // Plugin state
47
- let currentPage = COLLECTION_DEFAULTS.CURRENT_PAGE;
48
- let pageSize =
49
- config.pageSize ||
50
- baseCollection._config.pageSize ||
51
- COLLECTION_DEFAULTS.PAGE_SIZE;
52
- let hasMore = true;
53
- let isLoadingMore = false;
54
- let totalItemsExpected = 0;
55
-
56
- /**
57
- * Apply data transformations if configured
58
- */
59
- const applyDataTransformations = (items: any[]): T[] => {
60
- let processedItems = [...items];
61
-
62
- // Apply normalization if configured
63
- if (baseCollection._config.normalize) {
64
- processedItems = baseCollection._config.normalize(processedItems);
65
- }
66
-
67
- // Apply transformation if configured
68
- if (baseCollection._config.transform) {
69
- processedItems = processedItems.map(baseCollection._config.transform);
70
- }
71
-
72
- // Apply validation if configured
73
- if (baseCollection._config.validate) {
74
- processedItems = processedItems.filter(baseCollection._config.validate);
75
- }
76
-
77
- return processedItems;
78
- };
79
-
80
- /**
81
- * Determine if there's more data to load
82
- */
83
- const determineHasMore = (
84
- response: AdapterResponse<T>,
85
- itemsLength: number
86
- ): boolean => {
87
- const apiHasNext = response.meta?.hasNext;
88
- const gotFullPage = itemsLength === pageSize;
89
- const apiTotal = response.meta?.total;
90
-
91
- console.log(
92
- `${DATA_LOGGING.PREFIX} LOADING-PLUGIN determineHasMore debug:`,
93
- {
94
- apiTotal,
95
- apiHasNext,
96
- gotFullPage,
97
- itemsLength,
98
- pageSize,
99
- totalItemsExpected,
100
- currentItemsLength: baseCollection.getState().items.length,
101
- }
102
- );
103
-
104
- if (apiTotal !== undefined) {
105
- const result = totalItemsExpected < apiTotal;
106
- console.log(
107
- `${DATA_LOGGING.PREFIX} LOADING-PLUGIN Using apiTotal logic: ${totalItemsExpected} < ${apiTotal} = ${result}`
108
- );
109
- return result;
110
- } else if (apiHasNext !== undefined) {
111
- console.log(
112
- `${DATA_LOGGING.PREFIX} LOADING-PLUGIN Using apiHasNext logic: ${apiHasNext}`
113
- );
114
- return apiHasNext;
115
- } else {
116
- console.log(
117
- `${DATA_LOGGING.PREFIX} LOADING-PLUGIN Using gotFullPage logic: ${gotFullPage}`
118
- );
119
- return gotFullPage;
120
- }
121
- };
122
-
123
- /**
124
- * Load a specific page of data
125
- */
126
- const loadPage = async (page: number): Promise<AdapterResponse<T>> => {
127
- if (!adapter) {
128
- throw new Error("No adapter configured");
129
- }
130
-
131
- // Calculate what data range this page represents
132
- const startIndex = (page - 1) * pageSize;
133
- const endIndex = startIndex + pageSize - 1;
134
-
135
- const currentState = baseCollection.getState();
136
- const currentItems = currentState.items;
137
-
138
- // Check if we already have this data
139
- const hasData = currentItems.length > endIndex;
140
-
141
- if (hasData) {
142
- console.log(
143
- `${DATA_LOGGING.PREFIX} Data already loaded for page ${page} (items ${startIndex}-${endIndex})`
144
- );
145
-
146
- // Return the subset we already have
147
- const pageItems = currentItems.slice(startIndex, startIndex + pageSize);
148
-
149
- currentPage = page;
150
-
151
- // Emit items loaded event
152
- baseCollection.emit(CollectionDataEvents.ITEMS_LOADED, {
153
- items: pageItems,
154
- meta: { page, total: totalItemsExpected },
155
- });
156
-
157
- return {
158
- items: pageItems,
159
- meta: {
160
- total: totalItemsExpected,
161
- page: page,
162
- hasNext: hasMore,
163
- hasPrev: page > 1,
164
- },
165
- };
166
- }
167
-
168
- console.log(`${DATA_LOGGING.PREFIX} Loading page ${page} from adapter`);
169
-
170
- try {
171
- baseCollection.emit(
172
- CollectionDataEvents.LOADING_START,
173
- createEventPayload.loadingStart(`Loading page ${page}`)
174
- );
175
-
176
- baseCollection.setState({ loading: true });
177
-
178
- const response = await adapter.read({
179
- page,
180
- pageSize,
181
- });
182
-
183
- if (response.error) {
184
- throw new Error(response.error.message);
185
- }
186
-
187
- const processedItems = applyDataTransformations(response.items);
188
-
189
- // Add to existing items (append if sequential)
190
- let updatedItems: T[];
191
- if (startIndex === currentItems.length) {
192
- // Sequential loading - just append
193
- updatedItems = [...currentItems, ...processedItems];
194
- } else {
195
- // Non-sequential - for now just append (could be more sophisticated)
196
- updatedItems = [...currentItems, ...processedItems];
197
- }
198
-
199
- totalItemsExpected = response.meta?.total || updatedItems.length;
200
-
201
- baseCollection.setState({
202
- items: updatedItems,
203
- totalCount: totalItemsExpected,
204
- loading: false,
205
- error: null,
206
- });
207
-
208
- currentPage = page;
209
- hasMore = determineHasMore(response, processedItems.length);
210
-
211
- baseCollection.emit(
212
- CollectionDataEvents.ITEMS_LOADED,
213
- createEventPayload.itemsLoaded(processedItems, response.meta)
214
- );
215
-
216
- return response;
217
- } catch (error) {
218
- const errorObj = error as Error;
219
- baseCollection.setState({
220
- loading: false,
221
- error: errorObj,
222
- });
223
-
224
- baseCollection.emit(
225
- CollectionDataEvents.ERROR_OCCURRED,
226
- createEventPayload.errorOccurred(errorObj, { page })
227
- );
228
-
229
- throw error;
230
- }
231
- };
232
-
233
- /**
234
- * Load more items (next page)
235
- */
236
- const loadMore = async (): Promise<AdapterResponse<T>> => {
237
- if (!adapter || isLoadingMore) {
238
- throw new Error("No adapter configured or already loading");
239
- }
240
-
241
- isLoadingMore = true;
242
- const nextPage = currentPage + 1;
243
-
244
- try {
245
- const response = await loadPage(nextPage);
246
- return response;
247
- } finally {
248
- isLoadingMore = false;
249
- }
250
- };
251
-
252
- /**
253
- * Refresh all data (start from beginning)
254
- */
255
- const refresh = async (): Promise<AdapterResponse<T>> => {
256
- // Reset state
257
- currentPage = 0;
258
- hasMore = true;
259
- totalItemsExpected = 0;
260
-
261
- baseCollection.setState({
262
- items: [],
263
- filteredItems: [],
264
- totalCount: 0,
265
- });
266
-
267
- return loadMore();
268
- };
269
-
270
- console.log(`${DATA_LOGGING.PREFIX} Loading plugin installed`);
271
-
272
- return Object.assign(baseCollection, {
273
- loadPage,
274
- loadMore,
275
- refresh,
276
- getCurrentPage: () => currentPage,
277
- hasNext: () => hasMore,
278
- });
279
- };
@@ -1,147 +0,0 @@
1
- /**
2
- * Data Operations Plugin
3
- *
4
- * Handles CRUD operations on collection items (add, update, remove, clear)
5
- */
6
-
7
- import type { CollectionItem, BaseCollection } from "../../types";
8
- import { CollectionDataEvents } from "../../types";
9
- import { DATA_LOGGING } from "../../constants";
10
-
11
- export interface DataOperationsFeatures<T extends CollectionItem> {
12
- addItems(items: T[]): Promise<T[]>;
13
- updateItems(items: Partial<T>[]): Promise<T[]>;
14
- removeItems(ids: string[]): Promise<void>;
15
- clearItems(): Promise<void>;
16
- }
17
-
18
- /**
19
- * Data Operations Plugin
20
- */
21
- export const withDataOperations =
22
- <T extends CollectionItem = CollectionItem>() =>
23
- (
24
- baseCollection: BaseCollection<T>
25
- ): BaseCollection<T> & DataOperationsFeatures<T> => {
26
- /**
27
- * Apply data transformations if configured
28
- */
29
- const applyDataTransformations = (items: any[]): T[] => {
30
- let processedItems = [...items];
31
-
32
- // Apply normalization if configured
33
- if (baseCollection._config.normalize) {
34
- processedItems = baseCollection._config.normalize(processedItems);
35
- }
36
-
37
- // Apply transformation if configured
38
- if (baseCollection._config.transform) {
39
- processedItems = processedItems.map(baseCollection._config.transform);
40
- }
41
-
42
- // Apply validation if configured
43
- if (baseCollection._config.validate) {
44
- processedItems = processedItems.filter(baseCollection._config.validate);
45
- }
46
-
47
- return processedItems;
48
- };
49
-
50
- /**
51
- * Add items to the collection
52
- */
53
- const addItems = async (items: T[]): Promise<T[]> => {
54
- const processedItems = applyDataTransformations(items);
55
- const currentState = baseCollection.getState();
56
-
57
- // Add to existing items
58
- const newItems = [...currentState.items, ...processedItems];
59
-
60
- baseCollection.setState({
61
- items: newItems,
62
- totalCount: newItems.length,
63
- });
64
-
65
- // Emit data event
66
- baseCollection.emit(CollectionDataEvents.ITEMS_ADDED, {
67
- items: processedItems,
68
- indices: [currentState.items.length],
69
- });
70
-
71
- return processedItems;
72
- };
73
-
74
- /**
75
- * Update existing items in the collection
76
- */
77
- const updateItems = async (items: Partial<T>[]): Promise<T[]> => {
78
- const currentState = baseCollection.getState();
79
- const updatedItems: T[] = [];
80
-
81
- // Update existing items
82
- const newItems = currentState.items.map((item: T) => {
83
- const update = items.find((u) => u.id === item.id);
84
- if (update) {
85
- const updatedItem = { ...item, ...update };
86
- updatedItems.push(updatedItem);
87
- return updatedItem;
88
- }
89
- return item;
90
- });
91
-
92
- baseCollection.setState({ items: newItems });
93
-
94
- // Emit data event
95
- baseCollection.emit(CollectionDataEvents.ITEMS_UPDATED, {
96
- items: updatedItems,
97
- indices: [],
98
- });
99
-
100
- return updatedItems;
101
- };
102
-
103
- /**
104
- * Remove items from the collection
105
- */
106
- const removeItems = async (ids: string[]): Promise<void> => {
107
- const currentState = baseCollection.getState();
108
-
109
- // Remove items by ID
110
- const newItems = currentState.items.filter(
111
- (item: T) => !ids.includes(item.id)
112
- );
113
-
114
- baseCollection.setState({
115
- items: newItems,
116
- totalCount: newItems.length,
117
- });
118
-
119
- // Emit data event
120
- baseCollection.emit(CollectionDataEvents.ITEMS_REMOVED, {
121
- ids,
122
- });
123
- };
124
-
125
- /**
126
- * Clear all items from the collection
127
- */
128
- const clearItems = async (): Promise<void> => {
129
- baseCollection.setState({
130
- items: [],
131
- filteredItems: [],
132
- totalCount: 0,
133
- });
134
-
135
- // Emit data event
136
- baseCollection.emit(CollectionDataEvents.ITEMS_CLEARED, {});
137
- };
138
-
139
- console.log(`${DATA_LOGGING.PREFIX} Data Operations plugin installed`);
140
-
141
- return Object.assign(baseCollection, {
142
- addItems,
143
- updateItems,
144
- removeItems,
145
- clearItems,
146
- });
147
- };
@@ -1,104 +0,0 @@
1
- /**
2
- * Collection System - Composition-Based Architecture
3
- *
4
- * Following the blueprint with functional composition and plugin system
5
- */
6
-
7
- import type { CollectionItem } from "./types";
8
-
9
- // New composition-based architecture exports
10
- export { createCollection, createDataCollection } from "./collection-composer";
11
- export { createBaseCollection } from "./base-collection";
12
-
13
- // Plugin exports
14
- export { withLoading } from "./features/api/loading";
15
- export { withDataOperations } from "./features/operations/data-operations";
16
-
17
- // Core types
18
- export type {
19
- Collection,
20
- BaseCollection,
21
- CollectionConfig,
22
- CollectionItem,
23
- CollectionAdapter,
24
- AdapterParams,
25
- AdapterResponse,
26
- } from "./types";
27
-
28
- // Events and state
29
- export { CollectionDataEvents } from "./types";
30
- export { CollectionEvents, createEventPayload } from "./events";
31
- export { createCollectionState } from "./state";
32
-
33
- // Constants
34
- export * from "./constants";
35
-
36
- /**
37
- * Quick start utility for creating a simple REST adapter
38
- */
39
- export function createRestAdapter<T extends CollectionItem>(
40
- baseUrl: string,
41
- options: {
42
- headers?: Record<string, string>;
43
- timeout?: number;
44
- transform?: (response: any) => { items: T[]; meta?: any };
45
- } = {}
46
- ) {
47
- return {
48
- async read(params: any = {}) {
49
- const url = new URL(baseUrl);
50
-
51
- // Add query parameters
52
- if (params.page) url.searchParams.set("page", params.page.toString());
53
- if (params.pageSize)
54
- url.searchParams.set("limit", params.pageSize.toString());
55
- if (params.search) url.searchParams.set("q", params.search);
56
- if (params.cursor) url.searchParams.set("cursor", params.cursor);
57
-
58
- try {
59
- const response = await fetch(url.toString(), {
60
- method: "GET",
61
- headers: {
62
- "Content-Type": "application/json",
63
- ...options.headers,
64
- },
65
- signal: options.timeout
66
- ? AbortSignal.timeout(options.timeout)
67
- : undefined,
68
- });
69
-
70
- if (!response.ok) {
71
- throw new Error(`HTTP ${response.status}: ${response.statusText}`);
72
- }
73
-
74
- const data = await response.json();
75
-
76
- // Apply custom transform if provided
77
- if (options.transform) {
78
- return options.transform(data);
79
- }
80
-
81
- // Default transform
82
- return {
83
- items: data.items || data.data || [data].flat(),
84
- meta: {
85
- total: data.total,
86
- page: data.page,
87
- hasNext: data.hasNext || data.has_next,
88
- hasPrev: data.hasPrev || data.has_prev,
89
- },
90
- };
91
- } catch (error) {
92
- return {
93
- items: [],
94
- error: {
95
- message: error instanceof Error ? error.message : "Unknown error",
96
- code: "FETCH_ERROR",
97
- },
98
- };
99
- }
100
- },
101
- };
102
- }
103
-
104
- // Collection architecture is now complete!