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