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,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!