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
@@ -0,0 +1,424 @@
1
+ // src/core/compose/features/gestures/pan.ts
2
+ /**
3
+ * @module core/compose/features/gestures
4
+ * @description Adds pan gesture recognition to components
5
+ */
6
+
7
+ import type { BaseComponent, ElementComponent } from "mtrl";
8
+ import { PanEvent, GestureHandler } from "../../../gestures";
9
+ import { hasLifecycle, hasEmit } from "mtrl";
10
+
11
+ /**
12
+ * Configuration for pan gesture feature
13
+ */
14
+ export interface PanGestureConfig {
15
+ /**
16
+ * Whether to prevent default behaviors on touch events
17
+ * @default true
18
+ */
19
+ preventDefault?: boolean;
20
+
21
+ /**
22
+ * Handler for pan start (first movement)
23
+ */
24
+ onPanStart?: GestureHandler;
25
+
26
+ /**
27
+ * Handler for pan move (continuous updates during pan)
28
+ */
29
+ onPan?: GestureHandler;
30
+
31
+ /**
32
+ * Handler for pan end (touch/mouse release)
33
+ */
34
+ onPanEnd?: GestureHandler;
35
+
36
+ /**
37
+ * Whether to enable pan recognition immediately
38
+ * @default true
39
+ */
40
+ enabled?: boolean;
41
+
42
+ [key: string]: any;
43
+ }
44
+
45
+ /**
46
+ * Extend the PanEvent interface to support our custom event types
47
+ */
48
+ interface ExtendedPanEvent extends Omit<PanEvent, "type"> {
49
+ type: "pan" | "panstart" | "panend";
50
+ }
51
+
52
+ /**
53
+ * Component with pan gesture recognition capabilities
54
+ */
55
+ export interface PanGestureComponent extends BaseComponent {
56
+ /**
57
+ * Add a handler for pan start
58
+ * @param handler - Event handler function
59
+ * @returns Component for chaining
60
+ */
61
+ onPanStart: (handler: (event: PanEvent) => void) => PanGestureComponent;
62
+
63
+ /**
64
+ * Add a handler for pan move (continuous updates)
65
+ * @param handler - Event handler function
66
+ * @returns Component for chaining
67
+ */
68
+ onPan: (handler: (event: PanEvent) => void) => PanGestureComponent;
69
+
70
+ /**
71
+ * Add a handler for pan end
72
+ * @param handler - Event handler function
73
+ * @returns Component for chaining
74
+ */
75
+ onPanEnd: (handler: (event: PanEvent) => void) => PanGestureComponent;
76
+
77
+ /**
78
+ * Remove a pan start handler
79
+ * @param handler - Event handler function
80
+ * @returns Component for chaining
81
+ */
82
+ offPanStart: (handler: (event: PanEvent) => void) => PanGestureComponent;
83
+
84
+ /**
85
+ * Remove a pan move handler
86
+ * @param handler - Event handler function
87
+ * @returns Component for chaining
88
+ */
89
+ offPan: (handler: (event: PanEvent) => void) => PanGestureComponent;
90
+
91
+ /**
92
+ * Remove a pan end handler
93
+ * @param handler - Event handler function
94
+ * @returns Component for chaining
95
+ */
96
+ offPanEnd: (handler: (event: PanEvent) => void) => PanGestureComponent;
97
+
98
+ /**
99
+ * Enable pan recognition
100
+ * @returns Component for chaining
101
+ */
102
+ enablePan: () => PanGestureComponent;
103
+
104
+ /**
105
+ * Disable pan recognition
106
+ * @returns Component for chaining
107
+ */
108
+ disablePan: () => PanGestureComponent;
109
+ }
110
+
111
+ /**
112
+ * Adds pan gesture recognition to a component.
113
+ * This is a lightweight alternative to the full gesture system,
114
+ * focused only on pan detection.
115
+ *
116
+ * @param config - Configuration object containing pan settings
117
+ * @returns Function that enhances a component with pan capabilities
118
+ *
119
+ * @example
120
+ * ```ts
121
+ * // Add pan gesture recognition to a component
122
+ * const component = pipe(
123
+ * createBase,
124
+ * withElement(...),
125
+ * withPanGesture({
126
+ * onPanStart: (e) => startDrag(e),
127
+ * onPan: (e) => updateDragPosition(e),
128
+ * onPanEnd: (e) => endDrag(e)
129
+ * })
130
+ * )(config);
131
+ * ```
132
+ */
133
+ export const withPanGesture =
134
+ (config: PanGestureConfig = {}) =>
135
+ <C extends ElementComponent>(component: C): C & PanGestureComponent => {
136
+ if (!component.element) {
137
+ console.warn("Cannot add pan gesture recognition: missing element");
138
+ return component as C & PanGestureComponent;
139
+ }
140
+
141
+ // Default configuration
142
+ const {
143
+ preventDefault = true,
144
+ onPanStart,
145
+ onPan,
146
+ onPanEnd,
147
+ enabled = true,
148
+ } = config;
149
+
150
+ // Event handlers storage by pan phase
151
+ const handlers = {
152
+ panstart: new Set<(event: PanEvent) => void>(),
153
+ pan: new Set<(event: PanEvent) => void>(),
154
+ panend: new Set<(event: PanEvent) => void>(),
155
+ };
156
+
157
+ // Add initial handlers if provided
158
+ if (onPanStart)
159
+ handlers.panstart.add(onPanStart as (event: PanEvent) => void);
160
+ if (onPan) handlers.pan.add(onPan as (event: PanEvent) => void);
161
+ if (onPanEnd) handlers.panend.add(onPanEnd as (event: PanEvent) => void);
162
+
163
+ // Gesture state for tracking
164
+ let startX = 0;
165
+ let startY = 0;
166
+ let lastX = 0;
167
+ let lastY = 0;
168
+ let currentX = 0;
169
+ let currentY = 0;
170
+ let active = false;
171
+ let isPanning = false;
172
+ let startTime = 0;
173
+ let isEnabled = enabled;
174
+
175
+ /**
176
+ * Create a pan event with the current state
177
+ */
178
+ const createPanEvent = (
179
+ e: MouseEvent | TouchEvent,
180
+ type: "panstart" | "pan" | "panend"
181
+ ): ExtendedPanEvent => {
182
+ const event: ExtendedPanEvent = {
183
+ type,
184
+ originalEvent: e,
185
+ target: e.target!,
186
+ startTime,
187
+ endTime: Date.now(),
188
+ duration: Date.now() - startTime,
189
+ defaultPrevented: false,
190
+ preventDefault: () => {
191
+ event.defaultPrevented = true;
192
+ if (e.cancelable) {
193
+ e.preventDefault();
194
+ }
195
+ },
196
+ stopPropagation: () => {
197
+ e.stopPropagation();
198
+ },
199
+ deltaX: currentX - startX,
200
+ deltaY: currentY - startY,
201
+ startX,
202
+ startY,
203
+ currentX,
204
+ currentY,
205
+ };
206
+ return event;
207
+ };
208
+
209
+ /**
210
+ * Dispatch a pan event to all registered handlers
211
+ */
212
+ const dispatchPan = (
213
+ e: MouseEvent | TouchEvent,
214
+ type: "panstart" | "pan" | "panend"
215
+ ): void => {
216
+ const extendedPanEvent = createPanEvent(e, type);
217
+
218
+ // Call each handler for this phase
219
+ handlers[type].forEach((handler) => {
220
+ try {
221
+ // Type assertion for the handler call - we're deliberately passing our extended event
222
+ handler(extendedPanEvent as unknown as PanEvent);
223
+ } catch (error) {
224
+ console.error(`Error in ${type} handler:`, error);
225
+ }
226
+ });
227
+
228
+ // Forward to component's event system if available
229
+ if (hasEmit(component)) {
230
+ component.emit(type, extendedPanEvent);
231
+ }
232
+
233
+ // Apply preventDefault if configured
234
+ if (preventDefault && !extendedPanEvent.defaultPrevented) {
235
+ extendedPanEvent.preventDefault();
236
+ }
237
+ };
238
+
239
+ /**
240
+ * Handle touch/mouse start
241
+ */
242
+ const handleStart = (e: MouseEvent | TouchEvent): void => {
243
+ if (!isEnabled) return;
244
+
245
+ const touch = "touches" in e ? e.touches[0] : e;
246
+
247
+ startX = lastX = currentX = touch.clientX;
248
+ startY = lastY = currentY = touch.clientY;
249
+ startTime = Date.now();
250
+ active = true;
251
+ isPanning = false;
252
+ };
253
+
254
+ /**
255
+ * Handle touch/mouse move
256
+ */
257
+ const handleMove = (e: MouseEvent | TouchEvent): void => {
258
+ if (!active || !isEnabled) return;
259
+
260
+ const touch = "touches" in e ? e.touches[0] : e;
261
+
262
+ // Update position
263
+ lastX = currentX;
264
+ lastY = currentY;
265
+ currentX = touch.clientX;
266
+ currentY = touch.clientY;
267
+
268
+ // Calculate movement delta
269
+ const moveDeltaX = currentX - lastX;
270
+ const moveDeltaY = currentY - lastY;
271
+ const moveDelta = Math.sqrt(
272
+ moveDeltaX * moveDeltaX + moveDeltaY * moveDeltaY
273
+ );
274
+
275
+ // Detect significant movement
276
+ if (moveDelta > 0) {
277
+ // If this is the first significant movement, trigger panstart
278
+ if (!isPanning) {
279
+ isPanning = true;
280
+ dispatchPan(e, "panstart");
281
+ }
282
+
283
+ // Then trigger the continuous pan event
284
+ dispatchPan(e, "pan");
285
+ }
286
+ };
287
+
288
+ /**
289
+ * Handle touch/mouse end
290
+ */
291
+ const handleEnd = (e: MouseEvent | TouchEvent): void => {
292
+ if (!active || !isEnabled) return;
293
+
294
+ // If we were panning, trigger the panend event
295
+ if (isPanning) {
296
+ dispatchPan(e, "panend");
297
+ }
298
+
299
+ active = false;
300
+ isPanning = false;
301
+ };
302
+
303
+ /**
304
+ * Handle touch/mouse cancel
305
+ */
306
+ const handleCancel = (e: MouseEvent | TouchEvent): void => {
307
+ if (!active || !isEnabled) return;
308
+
309
+ // If we were panning, trigger the panend event
310
+ if (isPanning) {
311
+ dispatchPan(e, "panend");
312
+ }
313
+
314
+ active = false;
315
+ isPanning = false;
316
+ };
317
+
318
+ // Event listeners dictionary
319
+ const eventListeners: Record<string, EventListener> = {
320
+ mousedown: handleStart as EventListener,
321
+ mousemove: handleMove as EventListener,
322
+ mouseup: handleEnd as EventListener,
323
+ mouseleave: handleCancel as EventListener,
324
+ touchstart: handleStart as EventListener,
325
+ touchmove: handleMove as EventListener,
326
+ touchend: handleEnd as EventListener,
327
+ touchcancel: handleCancel as EventListener,
328
+ };
329
+
330
+ /**
331
+ * Add event listeners to element
332
+ */
333
+ const setupEventListeners = (): void => {
334
+ Object.entries(eventListeners).forEach(([event, listener]) => {
335
+ component.element.addEventListener(event, listener, {
336
+ passive: !preventDefault,
337
+ });
338
+ });
339
+ };
340
+
341
+ /**
342
+ * Remove event listeners from element
343
+ */
344
+ const removeEventListeners = (): void => {
345
+ Object.entries(eventListeners).forEach(([event, listener]) => {
346
+ component.element.removeEventListener(event, listener);
347
+ });
348
+ };
349
+
350
+ // Setup listeners if initially enabled
351
+ if (isEnabled) {
352
+ setupEventListeners();
353
+ }
354
+
355
+ // Handle lifecycle integration
356
+ if (hasLifecycle(component)) {
357
+ const originalDestroy = component.lifecycle.destroy;
358
+
359
+ component.lifecycle.destroy = () => {
360
+ // Clean up event listeners
361
+ removeEventListeners();
362
+
363
+ // Clear handlers
364
+ Object.values(handlers).forEach((handlerSet) => handlerSet.clear());
365
+
366
+ // Call original destroy method
367
+ originalDestroy.call(component.lifecycle);
368
+ };
369
+ }
370
+
371
+ // Create enhanced component
372
+ return {
373
+ ...component,
374
+
375
+ // Add handler methods
376
+ onPanStart(handler: (event: PanEvent) => void) {
377
+ handlers.panstart.add(handler);
378
+ return this;
379
+ },
380
+
381
+ onPan(handler: (event: PanEvent) => void) {
382
+ handlers.pan.add(handler);
383
+ return this;
384
+ },
385
+
386
+ onPanEnd(handler: (event: PanEvent) => void) {
387
+ handlers.panend.add(handler);
388
+ return this;
389
+ },
390
+
391
+ // Remove handler methods
392
+ offPanStart(handler: (event: PanEvent) => void) {
393
+ handlers.panstart.delete(handler);
394
+ return this;
395
+ },
396
+
397
+ offPan(handler: (event: PanEvent) => void) {
398
+ handlers.pan.delete(handler);
399
+ return this;
400
+ },
401
+
402
+ offPanEnd(handler: (event: PanEvent) => void) {
403
+ handlers.panend.delete(handler);
404
+ return this;
405
+ },
406
+
407
+ // Enable/disable methods
408
+ enablePan() {
409
+ if (!isEnabled) {
410
+ isEnabled = true;
411
+ setupEventListeners();
412
+ }
413
+ return this;
414
+ },
415
+
416
+ disablePan() {
417
+ if (isEnabled) {
418
+ isEnabled = false;
419
+ removeEventListeners();
420
+ }
421
+ return this;
422
+ },
423
+ };
424
+ };