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,475 @@
1
+ // src/core/compose/features/gestures/pinch.ts
2
+ /**
3
+ * @module core/compose/features/gestures
4
+ * @description Adds pinch gesture recognition to components
5
+ */
6
+
7
+ import type { BaseComponent, ElementComponent } from "mtrl";
8
+ import { PinchEvent, GestureHandler } from "../../../gestures";
9
+ import { getDistance } from "../../../gestures/utils";
10
+ import { hasLifecycle, hasEmit } from "mtrl";
11
+
12
+ /**
13
+ * Extended PinchEvent to support our custom event types
14
+ */
15
+ interface ExtendedPinchEvent extends Omit<PinchEvent, "type"> {
16
+ type: "pinch" | "pinchstart" | "pinchend";
17
+ }
18
+
19
+ /**
20
+ * Configuration for pinch gesture feature
21
+ */
22
+ export interface PinchGestureConfig {
23
+ /**
24
+ * Whether to prevent default behaviors on touch events
25
+ * @default true
26
+ */
27
+ preventDefault?: boolean;
28
+
29
+ /**
30
+ * Handler for pinch gesture
31
+ */
32
+ onPinch?: GestureHandler;
33
+
34
+ /**
35
+ * Handler for pinch start
36
+ */
37
+ onPinchStart?: GestureHandler;
38
+
39
+ /**
40
+ * Handler for pinch end
41
+ */
42
+ onPinchEnd?: GestureHandler;
43
+
44
+ /**
45
+ * Whether to enable pinch recognition immediately
46
+ * @default true
47
+ */
48
+ enabled?: boolean;
49
+
50
+ [key: string]: any;
51
+ }
52
+
53
+ /**
54
+ * Component with pinch gesture recognition capabilities
55
+ */
56
+ export interface PinchGestureComponent extends BaseComponent {
57
+ /**
58
+ * Add a pinch event handler
59
+ * @param handler - Event handler function
60
+ * @returns Component for chaining
61
+ */
62
+ onPinch: (handler: (event: PinchEvent) => void) => PinchGestureComponent;
63
+
64
+ /**
65
+ * Add a pinch start event handler
66
+ * @param handler - Event handler function
67
+ * @returns Component for chaining
68
+ */
69
+ onPinchStart: (handler: (event: PinchEvent) => void) => PinchGestureComponent;
70
+
71
+ /**
72
+ * Add a pinch end event handler
73
+ * @param handler - Event handler function
74
+ * @returns Component for chaining
75
+ */
76
+ onPinchEnd: (handler: (event: PinchEvent) => void) => PinchGestureComponent;
77
+
78
+ /**
79
+ * Remove a pinch event handler
80
+ * @param handler - Event handler function
81
+ * @returns Component for chaining
82
+ */
83
+ offPinch: (handler: (event: PinchEvent) => void) => PinchGestureComponent;
84
+
85
+ /**
86
+ * Remove a pinch start event handler
87
+ * @param handler - Event handler function
88
+ * @returns Component for chaining
89
+ */
90
+ offPinchStart: (
91
+ handler: (event: PinchEvent) => void
92
+ ) => PinchGestureComponent;
93
+
94
+ /**
95
+ * Remove a pinch end event handler
96
+ * @param handler - Event handler function
97
+ * @returns Component for chaining
98
+ */
99
+ offPinchEnd: (handler: (event: PinchEvent) => void) => PinchGestureComponent;
100
+
101
+ /**
102
+ * Enable pinch recognition
103
+ * @returns Component for chaining
104
+ */
105
+ enablePinch: () => PinchGestureComponent;
106
+
107
+ /**
108
+ * Disable pinch recognition
109
+ * @returns Component for chaining
110
+ */
111
+ disablePinch: () => PinchGestureComponent;
112
+
113
+ /**
114
+ * Check if pinch gestures are supported on the current device
115
+ * @returns Whether pinch gestures are supported
116
+ */
117
+ isPinchSupported: () => boolean;
118
+ }
119
+
120
+ /**
121
+ * Adds pinch gesture recognition to a component.
122
+ * This is a lightweight alternative to the full gesture system,
123
+ * focused only on pinch detection.
124
+ *
125
+ * @param config - Configuration object containing pinch settings
126
+ * @returns Function that enhances a component with pinch capabilities
127
+ *
128
+ * @example
129
+ * ```ts
130
+ * // Add pinch gesture recognition to a component
131
+ * const component = pipe(
132
+ * createBase,
133
+ * withElement(...),
134
+ * withPinchGesture({
135
+ * onPinch: (e) => updateZoom(e.scale)
136
+ * })
137
+ * )(config);
138
+ * ```
139
+ */
140
+ export const withPinchGesture =
141
+ (config: PinchGestureConfig = {}) =>
142
+ <C extends ElementComponent>(component: C): C & PinchGestureComponent => {
143
+ if (!component.element) {
144
+ console.warn("Cannot add pinch gesture recognition: missing element");
145
+ return component as C & PinchGestureComponent;
146
+ }
147
+
148
+ // Default configuration
149
+ const {
150
+ preventDefault = true,
151
+ onPinch,
152
+ onPinchStart,
153
+ onPinchEnd,
154
+ enabled = true,
155
+ } = config;
156
+
157
+ // Event handlers storage
158
+ const handlers = {
159
+ pinch: new Set<(event: PinchEvent) => void>(),
160
+ pinchstart: new Set<(event: PinchEvent) => void>(),
161
+ pinchend: new Set<(event: PinchEvent) => void>(),
162
+ };
163
+
164
+ // Add initial handlers if provided
165
+ if (onPinch) handlers.pinch.add(onPinch as (event: PinchEvent) => void);
166
+ if (onPinchStart)
167
+ handlers.pinchstart.add(onPinchStart as (event: PinchEvent) => void);
168
+ if (onPinchEnd)
169
+ handlers.pinchend.add(onPinchEnd as (event: PinchEvent) => void);
170
+
171
+ // Check if device supports touch (required for pinch)
172
+ const isTouchSupported =
173
+ "ontouchstart" in window || navigator.maxTouchPoints > 0;
174
+
175
+ // Gesture state for tracking
176
+ let startDistance = 0;
177
+ let lastScale = 1;
178
+ let isPinching = false;
179
+ let startTime = 0;
180
+ let isEnabled = enabled;
181
+
182
+ /**
183
+ * Create a pinch event
184
+ */
185
+ const createPinchEvent = (
186
+ e: TouchEvent,
187
+ type: "pinch" | "pinchstart" | "pinchend",
188
+ scale: number,
189
+ centerX: number,
190
+ centerY: number
191
+ ): ExtendedPinchEvent => {
192
+ const event: ExtendedPinchEvent = {
193
+ type,
194
+ originalEvent: e,
195
+ target: e.target!,
196
+ startTime,
197
+ endTime: Date.now(),
198
+ duration: Date.now() - startTime,
199
+ defaultPrevented: false,
200
+ preventDefault: () => {
201
+ event.defaultPrevented = true;
202
+ if (e.cancelable) {
203
+ e.preventDefault();
204
+ }
205
+ },
206
+ stopPropagation: () => {
207
+ e.stopPropagation();
208
+ },
209
+ scale,
210
+ centerX,
211
+ centerY,
212
+ };
213
+ return event;
214
+ };
215
+
216
+ /**
217
+ * Dispatch a pinch event to registered handlers
218
+ */
219
+ const dispatchPinchEvent = (
220
+ e: TouchEvent,
221
+ type: "pinch" | "pinchstart" | "pinchend",
222
+ scale: number,
223
+ centerX: number,
224
+ centerY: number
225
+ ): void => {
226
+ const extendedPinchEvent = createPinchEvent(
227
+ e,
228
+ type,
229
+ scale,
230
+ centerX,
231
+ centerY
232
+ );
233
+
234
+ // Call each handler for this type
235
+ handlers[type].forEach((handler) => {
236
+ try {
237
+ // Type assertion for the handler call
238
+ handler(extendedPinchEvent as unknown as PinchEvent);
239
+ } catch (error) {
240
+ console.error(`Error in ${type} handler:`, error);
241
+ }
242
+ });
243
+
244
+ // Forward to component's event system if available
245
+ if (hasEmit(component)) {
246
+ component.emit(type, extendedPinchEvent);
247
+ }
248
+
249
+ // Apply preventDefault if configured
250
+ if (preventDefault && !extendedPinchEvent.defaultPrevented) {
251
+ extendedPinchEvent.preventDefault();
252
+ }
253
+ };
254
+
255
+ /**
256
+ * Handle touch start
257
+ */
258
+ const handleTouchStart = (e: TouchEvent): void => {
259
+ if (!isEnabled || e.touches.length !== 2) return;
260
+
261
+ // Calculate initial distance between touch points
262
+ const touch1 = e.touches[0];
263
+ const touch2 = e.touches[1];
264
+
265
+ startDistance = getDistance(
266
+ touch1.clientX,
267
+ touch1.clientY,
268
+ touch2.clientX,
269
+ touch2.clientY
270
+ );
271
+
272
+ startTime = Date.now();
273
+ lastScale = 1;
274
+ isPinching = false;
275
+ };
276
+
277
+ /**
278
+ * Handle touch move
279
+ */
280
+ const handleTouchMove = (e: TouchEvent): void => {
281
+ if (!isEnabled || e.touches.length !== 2 || startDistance === 0) return;
282
+
283
+ const touch1 = e.touches[0];
284
+ const touch2 = e.touches[1];
285
+
286
+ // Calculate current distance and scale
287
+ const currentDistance = getDistance(
288
+ touch1.clientX,
289
+ touch1.clientY,
290
+ touch2.clientX,
291
+ touch2.clientY
292
+ );
293
+
294
+ const scale = currentDistance / startDistance;
295
+ const centerX = (touch1.clientX + touch2.clientX) / 2;
296
+ const centerY = (touch1.clientY + touch2.clientY) / 2;
297
+
298
+ // Check if scale change is significant enough
299
+ const scaleDiff = Math.abs(scale - lastScale);
300
+ const SCALE_THRESHOLD = 0.01;
301
+
302
+ if (scaleDiff > SCALE_THRESHOLD) {
303
+ // Start pinch if this is the first significant scale change
304
+ if (!isPinching) {
305
+ isPinching = true;
306
+ dispatchPinchEvent(e, "pinchstart", scale, centerX, centerY);
307
+ }
308
+
309
+ // Dispatch continuous pinch event
310
+ dispatchPinchEvent(e, "pinch", scale, centerX, centerY);
311
+ lastScale = scale;
312
+ }
313
+ };
314
+
315
+ /**
316
+ * Handle touch end
317
+ */
318
+ const handleTouchEnd = (e: TouchEvent): void => {
319
+ if (!isEnabled || !isPinching) return;
320
+
321
+ // Calculate final center point if we still have one finger on screen
322
+ let centerX = 0;
323
+ let centerY = 0;
324
+
325
+ if (e.touches.length === 1) {
326
+ centerX = e.touches[0].clientX;
327
+ centerY = e.touches[0].clientY;
328
+ } else if (e.changedTouches.length > 0) {
329
+ centerX = e.changedTouches[0].clientX;
330
+ centerY = e.changedTouches[0].clientY;
331
+ }
332
+
333
+ // Dispatch pinch end event
334
+ dispatchPinchEvent(e, "pinchend", lastScale, centerX, centerY);
335
+
336
+ // Reset state
337
+ startDistance = 0;
338
+ isPinching = false;
339
+ };
340
+
341
+ /**
342
+ * Handle touch cancel
343
+ */
344
+ const handleTouchCancel = (e: TouchEvent): void => {
345
+ if (!isEnabled || !isPinching) return;
346
+
347
+ // Calculate final center point
348
+ let centerX = 0;
349
+ let centerY = 0;
350
+
351
+ if (e.touches.length > 0) {
352
+ centerX = e.touches[0].clientX;
353
+ centerY = e.touches[0].clientY;
354
+ }
355
+
356
+ // Dispatch pinch end event
357
+ dispatchPinchEvent(e, "pinchend", lastScale, centerX, centerY);
358
+
359
+ // Reset state
360
+ startDistance = 0;
361
+ isPinching = false;
362
+ };
363
+
364
+ // Event listeners dictionary
365
+ const eventListeners: Record<string, EventListener> = {
366
+ touchstart: handleTouchStart as EventListener,
367
+ touchmove: handleTouchMove as EventListener,
368
+ touchend: handleTouchEnd as EventListener,
369
+ touchcancel: handleTouchCancel as EventListener,
370
+ };
371
+
372
+ /**
373
+ * Add event listeners to element
374
+ */
375
+ const setupEventListeners = (): void => {
376
+ if (!isTouchSupported) return;
377
+
378
+ Object.entries(eventListeners).forEach(([event, listener]) => {
379
+ component.element.addEventListener(event, listener, {
380
+ passive: !preventDefault,
381
+ });
382
+ });
383
+ };
384
+
385
+ /**
386
+ * Remove event listeners from element
387
+ */
388
+ const removeEventListeners = (): void => {
389
+ if (!isTouchSupported) return;
390
+
391
+ Object.entries(eventListeners).forEach(([event, listener]) => {
392
+ component.element.removeEventListener(event, listener);
393
+ });
394
+ };
395
+
396
+ // Setup listeners if initially enabled
397
+ if (isEnabled && isTouchSupported) {
398
+ setupEventListeners();
399
+ }
400
+
401
+ // Handle lifecycle integration
402
+ if (hasLifecycle(component)) {
403
+ const originalDestroy = component.lifecycle.destroy;
404
+
405
+ component.lifecycle.destroy = () => {
406
+ // Clean up event listeners
407
+ removeEventListeners();
408
+
409
+ // Clear handlers
410
+ Object.values(handlers).forEach((handlerSet) => handlerSet.clear());
411
+
412
+ // Call original destroy method
413
+ originalDestroy.call(component.lifecycle);
414
+ };
415
+ }
416
+
417
+ // Create enhanced component
418
+ return {
419
+ ...component,
420
+
421
+ // Add handler methods
422
+ onPinch(handler: (event: PinchEvent) => void) {
423
+ handlers.pinch.add(handler);
424
+ return this;
425
+ },
426
+
427
+ onPinchStart(handler: (event: PinchEvent) => void) {
428
+ handlers.pinchstart.add(handler);
429
+ return this;
430
+ },
431
+
432
+ onPinchEnd(handler: (event: PinchEvent) => void) {
433
+ handlers.pinchend.add(handler);
434
+ return this;
435
+ },
436
+
437
+ // Remove handler methods
438
+ offPinch(handler: (event: PinchEvent) => void) {
439
+ handlers.pinch.delete(handler);
440
+ return this;
441
+ },
442
+
443
+ offPinchStart(handler: (event: PinchEvent) => void) {
444
+ handlers.pinchstart.delete(handler);
445
+ return this;
446
+ },
447
+
448
+ offPinchEnd(handler: (event: PinchEvent) => void) {
449
+ handlers.pinchend.delete(handler);
450
+ return this;
451
+ },
452
+
453
+ // Enable/disable methods
454
+ enablePinch() {
455
+ if (!isEnabled) {
456
+ isEnabled = true;
457
+ setupEventListeners();
458
+ }
459
+ return this;
460
+ },
461
+
462
+ disablePinch() {
463
+ if (isEnabled) {
464
+ isEnabled = false;
465
+ removeEventListeners();
466
+ }
467
+ return this;
468
+ },
469
+
470
+ // Support check method
471
+ isPinchSupported() {
472
+ return isTouchSupported;
473
+ },
474
+ };
475
+ };