@moni-labs/moni-ui 0.4.13 → 0.4.16

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 (104) hide show
  1. package/custom-elements.json +101 -81
  2. package/dist/browser/moni-ui.iife.min.js +1538 -1384
  3. package/dist/browser/moni-ui.iife.min.js.br +0 -0
  4. package/dist/browser/moni-ui.iife.min.js.gz +0 -0
  5. package/dist/browser/moni-ui.iife.min.js.map +1 -1
  6. package/dist/browser/moni-ui.min.js +2751 -2501
  7. package/dist/browser/moni-ui.min.js.br +0 -0
  8. package/dist/browser/moni-ui.min.js.gz +0 -0
  9. package/dist/browser/moni-ui.min.js.map +1 -1
  10. package/dist/cdn/chunks/{class-map-DsL1Jr2T.js → class-map-QyT9W7rZ.js} +1 -1
  11. package/dist/cdn/chunks/{decorate-DxJYS4gw.js → decorate-B7wZvwrn.js} +629 -543
  12. package/dist/cdn/chunks/if-defined-5hr_ccFw.js +1 -0
  13. package/dist/cdn/chunks/{live-D52JqeKf.js → live-Hm1Di_ND.js} +1 -1
  14. package/dist/cdn/components/moni-app-bar.js +1 -1
  15. package/dist/cdn/components/moni-badge.js +1 -1
  16. package/dist/cdn/components/moni-bottom-sheet.js +2 -1
  17. package/dist/cdn/components/moni-button-group.js +1 -1
  18. package/dist/cdn/components/moni-button-segment.js +1 -1
  19. package/dist/cdn/components/moni-button.js +1 -1
  20. package/dist/cdn/components/moni-card.js +1 -1
  21. package/dist/cdn/components/moni-carousel.js +1 -1
  22. package/dist/cdn/components/moni-checkbox.js +1 -1
  23. package/dist/cdn/components/moni-chip.js +1 -1
  24. package/dist/cdn/components/moni-color-field.js +1 -1
  25. package/dist/cdn/components/moni-context-menu.js +1 -1
  26. package/dist/cdn/components/moni-dialog.js +1 -1
  27. package/dist/cdn/components/moni-divider.js +1 -1
  28. package/dist/cdn/components/moni-expansion.js +1 -1
  29. package/dist/cdn/components/moni-fab-menu.js +1 -1
  30. package/dist/cdn/components/moni-fab.js +1 -1
  31. package/dist/cdn/components/moni-file-field.js +1 -1
  32. package/dist/cdn/components/moni-icon.js +1 -1
  33. package/dist/cdn/components/moni-list-item.js +1 -1
  34. package/dist/cdn/components/moni-list.js +1 -1
  35. package/dist/cdn/components/moni-loading-indicator.js +1 -1
  36. package/dist/cdn/components/moni-menu-item.js +1 -1
  37. package/dist/cdn/components/moni-menu.js +1 -1
  38. package/dist/cdn/components/moni-morph-modal.js +3 -2
  39. package/dist/cdn/components/moni-nav-item.js +1 -1
  40. package/dist/cdn/components/moni-nav.js +1 -1
  41. package/dist/cdn/components/moni-progress.js +1 -1
  42. package/dist/cdn/components/moni-radio.js +1 -1
  43. package/dist/cdn/components/moni-ripple.js +1 -1
  44. package/dist/cdn/components/moni-segmented-button.js +1 -1
  45. package/dist/cdn/components/moni-select-option.js +1 -1
  46. package/dist/cdn/components/moni-select.js +512 -473
  47. package/dist/cdn/components/moni-shape.js +1 -1
  48. package/dist/cdn/components/moni-side-sheet.js +1 -1
  49. package/dist/cdn/components/moni-slider.js +1 -1
  50. package/dist/cdn/components/moni-snackbar.js +1 -1
  51. package/dist/cdn/components/moni-split-button.js +1 -1
  52. package/dist/cdn/components/moni-step.js +1 -1
  53. package/dist/cdn/components/moni-stepper.js +1 -1
  54. package/dist/cdn/components/moni-switch.js +1 -1
  55. package/dist/cdn/components/moni-tab.js +1 -1
  56. package/dist/cdn/components/moni-tabs.js +1 -1
  57. package/dist/cdn/components/moni-text-field.js +122 -95
  58. package/dist/cdn/components/moni-textarea.js +52 -52
  59. package/dist/cdn/components/moni-time-picker.js +1 -1
  60. package/dist/cdn/components/moni-toolbar.js +1 -1
  61. package/dist/cdn/components/moni-tooltip.js +1 -1
  62. package/dist/cdn/components/moni-typography.js +1 -1
  63. package/dist/cdn/importmap.json +98 -98
  64. package/dist/components/_base/field-styles.d.ts.map +1 -1
  65. package/dist/components/_base/field-styles.js +662 -576
  66. package/dist/components/moni-bottom-sheet.d.ts +19 -5
  67. package/dist/components/moni-bottom-sheet.d.ts.map +1 -1
  68. package/dist/components/moni-bottom-sheet.js +111 -30
  69. package/dist/components/moni-morph-modal.d.ts +10 -0
  70. package/dist/components/moni-morph-modal.d.ts.map +1 -1
  71. package/dist/components/moni-morph-modal.js +323 -217
  72. package/dist/components/moni-select.d.ts +16 -16
  73. package/dist/components/moni-select.d.ts.map +1 -1
  74. package/dist/components/moni-select.js +797 -727
  75. package/dist/components/moni-text-field.d.ts +15 -15
  76. package/dist/components/moni-text-field.d.ts.map +1 -1
  77. package/dist/components/moni-text-field.js +224 -180
  78. package/dist/components/moni-textarea.d.ts +13 -13
  79. package/dist/components/moni-textarea.d.ts.map +1 -1
  80. package/dist/components/moni-textarea.js +127 -122
  81. package/docs/api.json +109 -89
  82. package/docs/assets/moni-color-field/shape--square.png +0 -0
  83. package/docs/assets/moni-file-field/shape--square.png +0 -0
  84. package/docs/assets/moni-morph-modal/autoHeight--false.png +0 -0
  85. package/docs/assets/moni-morph-modal/autoHeight--true.png +0 -0
  86. package/docs/components/bottom-sheet.md +2 -1
  87. package/docs/components/morph-modal.md +16 -7
  88. package/docs/components/select.md +74 -74
  89. package/docs/components/text-field.md +75 -66
  90. package/docs/components/textarea.md +51 -51
  91. package/docs/llms.txt +3 -3
  92. package/package.json +1 -1
  93. package/src/components/_base/field-styles.ts +723 -637
  94. package/src/components/moni-bottom-sheet.test.ts +331 -0
  95. package/src/components/moni-bottom-sheet.ts +117 -29
  96. package/src/components/moni-morph-modal.test.ts +490 -413
  97. package/src/components/moni-morph-modal.ts +2274 -2162
  98. package/src/components/moni-select.test.ts +489 -466
  99. package/src/components/moni-select.ts +1963 -1815
  100. package/src/components/moni-text-field.test.ts +366 -342
  101. package/src/components/moni-text-field.ts +717 -645
  102. package/src/components/moni-textarea.test.ts +201 -197
  103. package/src/components/moni-textarea.ts +476 -460
  104. package/dist/cdn/chunks/if-defined-DdzvcoI7.js +0 -1
@@ -432,4 +432,335 @@ describe('moni-bottom-sheet', () => {
432
432
  expect(closeSpy).not.toHaveBeenCalled();
433
433
  expect(el.open).toBe(true);
434
434
  });
435
+
436
+ it('detecta arrastre fuera de la pantalla y marca como cerrado', async () => {
437
+ el.handle = true;
438
+ el.open = true;
439
+ await el.updateComplete;
440
+
441
+ const dialog = el.shadowRoot?.querySelector('dialog') as HTMLDialogElement;
442
+ const handle = el.shadowRoot?.querySelector('.handle') as HTMLElement;
443
+
444
+ handle.setPointerCapture = vi.fn();
445
+ handle.releasePointerCapture = vi.fn();
446
+
447
+ vi.spyOn(dialog, 'getBoundingClientRect').mockReturnValue({
448
+ height: 300,
449
+ width: 375,
450
+ x: 0,
451
+ y: 300,
452
+ top: 300,
453
+ bottom: 600,
454
+ left: 0,
455
+ right: 375,
456
+ toJSON: () => {}
457
+ });
458
+
459
+ const closeSpy = vi.fn();
460
+ el.addEventListener('close', closeSpy);
461
+
462
+ handle.dispatchEvent(
463
+ new PointerEvent('pointerdown', {
464
+ bubbles: true,
465
+ clientX: 100,
466
+ clientY: 400,
467
+ pointerId: 1
468
+ })
469
+ );
470
+
471
+ // Arrastrar hacia el borde inferior/fuera de pantalla
472
+ dialog.dispatchEvent(
473
+ new PointerEvent('pointermove', {
474
+ bubbles: true,
475
+ clientX: 100,
476
+ clientY: window.innerHeight || 800,
477
+ pointerId: 1
478
+ })
479
+ );
480
+
481
+ // Soltar fuera de la pantalla
482
+ handle.dispatchEvent(
483
+ new PointerEvent('pointerup', {
484
+ bubbles: true,
485
+ clientX: 100,
486
+ clientY: (window.innerHeight || 800) + 50,
487
+ pointerId: 1
488
+ })
489
+ );
490
+
491
+ dialog.dispatchEvent(new Event('transitionend'));
492
+ await el.updateComplete;
493
+
494
+ expect(closeSpy).toHaveBeenCalled();
495
+ expect(el.open).toBe(false);
496
+ });
497
+
498
+ it('cierra y marca como open=false cuando ocurre pointercancel tras arrastrar hacia abajo', async () => {
499
+ el.handle = true;
500
+ el.open = true;
501
+ await el.updateComplete;
502
+
503
+ const dialog = el.shadowRoot?.querySelector('dialog') as HTMLDialogElement;
504
+ const handle = el.shadowRoot?.querySelector('.handle') as HTMLElement;
505
+
506
+ handle.setPointerCapture = vi.fn();
507
+ handle.releasePointerCapture = vi.fn();
508
+
509
+ vi.spyOn(dialog, 'getBoundingClientRect').mockReturnValue({
510
+ height: 300,
511
+ width: 375,
512
+ x: 0,
513
+ y: 300,
514
+ top: 300,
515
+ bottom: 600,
516
+ left: 0,
517
+ right: 375,
518
+ toJSON: () => {}
519
+ });
520
+
521
+ const closeSpy = vi.fn();
522
+ el.addEventListener('close', closeSpy);
523
+
524
+ handle.dispatchEvent(
525
+ new PointerEvent('pointerdown', {
526
+ bubbles: true,
527
+ clientX: 100,
528
+ clientY: 400,
529
+ pointerId: 1
530
+ })
531
+ );
532
+
533
+ // Arrastre hacia abajo significativo
534
+ dialog.dispatchEvent(
535
+ new PointerEvent('pointermove', {
536
+ bubbles: true,
537
+ clientX: 100,
538
+ clientY: 550, // 150px
539
+ pointerId: 1
540
+ })
541
+ );
542
+
543
+ // El navegador cancela el puntero (por ejemplo, salida de viewport en móvil/pantalla táctil)
544
+ dialog.dispatchEvent(
545
+ new PointerEvent('pointercancel', {
546
+ bubbles: true,
547
+ clientX: 0,
548
+ clientY: 0,
549
+ pointerId: 1
550
+ })
551
+ );
552
+
553
+ dialog.dispatchEvent(new Event('transitionend'));
554
+ await el.updateComplete;
555
+
556
+ expect(closeSpy).toHaveBeenCalled();
557
+ expect(el.open).toBe(false);
558
+ });
559
+
560
+ it('cierra y marca como open=false cuando ocurre lostpointercapture tras arrastrar hacia abajo', async () => {
561
+ el.handle = true;
562
+ el.open = true;
563
+ await el.updateComplete;
564
+
565
+ const dialog = el.shadowRoot?.querySelector('dialog') as HTMLDialogElement;
566
+ const handle = el.shadowRoot?.querySelector('.handle') as HTMLElement;
567
+
568
+ handle.setPointerCapture = vi.fn();
569
+ handle.releasePointerCapture = vi.fn();
570
+
571
+ vi.spyOn(dialog, 'getBoundingClientRect').mockReturnValue({
572
+ height: 300,
573
+ width: 375,
574
+ x: 0,
575
+ y: 300,
576
+ top: 300,
577
+ bottom: 600,
578
+ left: 0,
579
+ right: 375,
580
+ toJSON: () => {}
581
+ });
582
+
583
+ const closeSpy = vi.fn();
584
+ el.addEventListener('close', closeSpy);
585
+
586
+ handle.dispatchEvent(
587
+ new PointerEvent('pointerdown', {
588
+ bubbles: true,
589
+ clientX: 100,
590
+ clientY: 400,
591
+ pointerId: 1
592
+ })
593
+ );
594
+
595
+ dialog.dispatchEvent(
596
+ new PointerEvent('pointermove', {
597
+ bubbles: true,
598
+ clientX: 100,
599
+ clientY: 520, // 120px
600
+ pointerId: 1
601
+ })
602
+ );
603
+
604
+ dialog.dispatchEvent(
605
+ new PointerEvent('lostpointercapture', {
606
+ bubbles: true,
607
+ clientX: 0,
608
+ clientY: 0,
609
+ pointerId: 1
610
+ })
611
+ );
612
+
613
+ dialog.dispatchEvent(new Event('transitionend'));
614
+ await el.updateComplete;
615
+
616
+ expect(closeSpy).toHaveBeenCalled();
617
+ expect(el.open).toBe(false);
618
+ });
619
+
620
+ it('cierra y marca como open=false cuando se arrastra un bottom sheet expandido hacia abajo', async () => {
621
+ el.handle = true;
622
+ el.open = true;
623
+ await el.updateComplete;
624
+
625
+ const dialog = el.shadowRoot?.querySelector('dialog') as HTMLDialogElement;
626
+ const handle = el.shadowRoot?.querySelector('.handle') as HTMLElement;
627
+ dialog.classList.add('expanded');
628
+
629
+ handle.setPointerCapture = vi.fn();
630
+ handle.releasePointerCapture = vi.fn();
631
+
632
+ vi.spyOn(dialog, 'getBoundingClientRect').mockReturnValue({
633
+ height: 600,
634
+ width: 375,
635
+ x: 0,
636
+ y: 100,
637
+ top: 100,
638
+ bottom: 700,
639
+ left: 0,
640
+ right: 375,
641
+ toJSON: () => {}
642
+ });
643
+
644
+ const closeSpy = vi.fn();
645
+ el.addEventListener('close', closeSpy);
646
+
647
+ handle.dispatchEvent(
648
+ new PointerEvent('pointerdown', {
649
+ bubbles: true,
650
+ clientX: 100,
651
+ clientY: 100,
652
+ pointerId: 1
653
+ })
654
+ );
655
+
656
+ dialog.dispatchEvent(
657
+ new PointerEvent('pointermove', {
658
+ bubbles: true,
659
+ clientX: 100,
660
+ clientY: 550, // Delta de 450px hacia abajo (arrastrado fuera / muy abajo)
661
+ pointerId: 1
662
+ })
663
+ );
664
+
665
+ handle.dispatchEvent(
666
+ new PointerEvent('pointerup', {
667
+ bubbles: true,
668
+ clientX: 100,
669
+ clientY: 550,
670
+ pointerId: 1
671
+ })
672
+ );
673
+
674
+ dialog.dispatchEvent(new Event('transitionend'));
675
+ await el.updateComplete;
676
+
677
+ expect(closeSpy).toHaveBeenCalled();
678
+ expect(el.open).toBe(false);
679
+ });
680
+
681
+ it('no cierra el bottom sheet si el usuario arrastra hacia abajo pero lo sube de nuevo antes de soltar', async () => {
682
+ el.handle = true;
683
+ el.open = true;
684
+ await el.updateComplete;
685
+
686
+ const dialog = el.shadowRoot?.querySelector('dialog') as HTMLDialogElement;
687
+ const handle = el.shadowRoot?.querySelector('.handle') as HTMLElement;
688
+
689
+ handle.setPointerCapture = vi.fn();
690
+ handle.releasePointerCapture = vi.fn();
691
+
692
+ vi.spyOn(dialog, 'getBoundingClientRect').mockReturnValue({
693
+ height: 300,
694
+ width: 375,
695
+ x: 0,
696
+ y: 300,
697
+ top: 300,
698
+ bottom: 600,
699
+ left: 0,
700
+ right: 375,
701
+ toJSON: () => {}
702
+ });
703
+
704
+ const closeSpy = vi.fn();
705
+ el.addEventListener('close', closeSpy);
706
+
707
+ // Inicia arrastre en Y = 400
708
+ handle.dispatchEvent(
709
+ new PointerEvent('pointerdown', {
710
+ bubbles: true,
711
+ clientX: 100,
712
+ clientY: 400,
713
+ pointerId: 1
714
+ })
715
+ );
716
+
717
+ // Arrastra hacia abajo (Y = 600, delta = 200px)
718
+ dialog.dispatchEvent(
719
+ new PointerEvent('pointermove', {
720
+ bubbles: true,
721
+ clientX: 100,
722
+ clientY: 600,
723
+ pointerId: 1
724
+ })
725
+ );
726
+
727
+ // Cambia de opinión y lo sube de nuevo cerca del origen (Y = 410, delta = 10px)
728
+ dialog.dispatchEvent(
729
+ new PointerEvent('pointermove', {
730
+ bubbles: true,
731
+ clientX: 100,
732
+ clientY: 410,
733
+ pointerId: 1
734
+ })
735
+ );
736
+
737
+ // Suelta en Y = 410
738
+ handle.dispatchEvent(
739
+ new PointerEvent('pointerup', {
740
+ bubbles: true,
741
+ clientX: 100,
742
+ clientY: 410,
743
+ pointerId: 1
744
+ })
745
+ );
746
+
747
+ await el.updateComplete;
748
+
749
+ expect(closeSpy).not.toHaveBeenCalled();
750
+ expect(el.open).toBe(true);
751
+ });
752
+
753
+ it('permite abrir y cerrar mediante los métodos show() y close()', async () => {
754
+ expect(el.open).toBe(false);
755
+ el.show();
756
+ await el.updateComplete;
757
+ expect(el.open).toBe(true);
758
+
759
+ const dialog = el.shadowRoot?.querySelector('dialog') as HTMLDialogElement;
760
+ const closePromise = el.close();
761
+ dialog.dispatchEvent(new Event('transitionend'));
762
+ await closePromise;
763
+ await el.updateComplete;
764
+ expect(el.open).toBe(false);
765
+ });
435
766
  });
@@ -164,11 +164,34 @@ export class MoniBottomSheet extends MoniElement {
164
164
  @query('dialog') private _dialog!: HTMLDialogElement;
165
165
  private _isDragging = false;
166
166
  private _startY = 0;
167
- private _currentTranslationY = 0;
167
+ private _lastDeltaY = 0;
168
168
  private _sheetHeight = 0;
169
169
  private _defaultHeight = 0;
170
170
  private _justDragged = false;
171
171
  private _skipRequestClose = false;
172
+ private _isClosing = false;
173
+
174
+ /**
175
+ * Abre el bottom sheet.
176
+ */
177
+ show() {
178
+ this._isClosing = false;
179
+ this._isDragging = false;
180
+ this._lastDeltaY = 0;
181
+ if (this._dialog) {
182
+ this._dialog.classList.remove('dragging', 'expanded');
183
+ this._dialog.style.transform = '';
184
+ this._dialog.style.height = '';
185
+ }
186
+ this.open = true;
187
+ }
188
+
189
+ /**
190
+ * Cierra el bottom sheet ejecutando la animación fluida hacia abajo.
191
+ */
192
+ close() {
193
+ return this._animateAndClose();
194
+ }
172
195
 
173
196
  override willUpdate(changed: Map<string, unknown>) {
174
197
  if (changed.has('open') && this.open !== changed.get('open')) {
@@ -188,12 +211,18 @@ export class MoniBottomSheet extends MoniElement {
188
211
  }
189
212
 
190
213
  private async _animateAndClose() {
214
+ if (this._isClosing) return false;
215
+ if (!this.open) return false;
216
+ this._isClosing = true;
217
+
191
218
  this._skipRequestClose = true;
192
219
  const allowed = emitMoniEvent(this, 'moni-request-close', { cancelable: true });
193
220
  this._skipRequestClose = false;
194
- if (!allowed) return false;
221
+ if (!allowed) {
222
+ this._isClosing = false;
223
+ return false;
224
+ }
195
225
 
196
- this.dispatchEvent(new Event('close', { bubbles: true, composed: true }));
197
226
  emitMoniEvent(this, 'moni-close');
198
227
 
199
228
  this._dialog.classList.remove('expanded');
@@ -209,10 +238,18 @@ export class MoniBottomSheet extends MoniElement {
209
238
  setTimeout(onEnd, 300);
210
239
  });
211
240
 
212
- this._dialog.style.transform = '';
241
+ this._isClosing = false;
242
+ this._isDragging = false;
243
+ this._lastDeltaY = 0;
244
+ if (this._dialog) {
245
+ this._dialog.classList.remove('dragging', 'expanded');
246
+ this._dialog.style.transform = '';
247
+ this._dialog.style.height = '';
248
+ }
213
249
  this._skipRequestClose = true;
214
250
  this.open = false;
215
251
  this._skipRequestClose = false;
252
+ this.dispatchEvent(new Event('close', { bubbles: true, composed: true }));
216
253
  emitMoniEvent(this, 'moni-closed');
217
254
  return true;
218
255
  }
@@ -250,16 +287,22 @@ export class MoniBottomSheet extends MoniElement {
250
287
  this._isDragging = true;
251
288
  this._startY = e.clientY; // Almacenamos la coordenada inicial Y para calcular el delta luego
252
289
  this._sheetHeight = this._dialog.getBoundingClientRect().height; // Capturamos la altura actual
253
- this._currentTranslationY = 0;
290
+ this._lastDeltaY = 0;
254
291
 
255
292
  const isExpanded = this._dialog.classList.contains('expanded');
256
293
  // Si no estaba expandido previamente, guardamos su altura por defecto para usarla como punto de retorno.
257
294
  if (!isExpanded) {
258
295
  this._defaultHeight = this._sheetHeight;
296
+ } else if (!this._defaultHeight) {
297
+ this._defaultHeight = Math.min(this._sheetHeight * 0.6, 384);
259
298
  }
260
299
 
261
300
  // setPointerCapture enruta todos los eventos subsecuentes a este target
262
- target.setPointerCapture(e.pointerId);
301
+ try {
302
+ target.setPointerCapture(e.pointerId);
303
+ } catch {
304
+ // Ignorar si el navegador ya maneja el puntero de forma nativa
305
+ }
263
306
  this._dialog.classList.add('dragging'); // Desactiva las transiciones CSS para un seguimiento suave 1:1
264
307
  }
265
308
 
@@ -273,6 +316,7 @@ export class MoniBottomSheet extends MoniElement {
273
316
 
274
317
  // Diferencia en píxeles desde donde se inició el click
275
318
  const deltaY = e.clientY - this._startY;
319
+ this._lastDeltaY = deltaY;
276
320
  const isExpanded = this._dialog.classList.contains('expanded');
277
321
  const maxHeight = this._getMaxHeightPx();
278
322
 
@@ -291,7 +335,6 @@ export class MoniBottomSheet extends MoniElement {
291
335
  } else {
292
336
  if (deltaY > 0) {
293
337
  // Arrastrando hacia abajo desde su estado inicial: lo empujamos visualmente hacia abajo (translate)
294
- this._currentTranslationY = deltaY;
295
338
  this._dialog.style.transform = `translateY(${deltaY}px)`;
296
339
  this._dialog.style.height = '';
297
340
  } else {
@@ -306,17 +349,26 @@ export class MoniBottomSheet extends MoniElement {
306
349
  /**
307
350
  * Finaliza la interacción de arrastre, evaluando si el bottom-sheet debe
308
351
  * expandirse, regresar a su estado original o cerrarse completamente según la
309
- * inercia o la distancia arrastrada.
352
+ * inercia, la distancia arrastrada o si se soltó fuera/hacia la parte inferior de la pantalla.
310
353
  */
311
354
  private _onPointerUp(e: PointerEvent) {
312
355
  if (!this._isDragging) return;
313
356
 
314
357
  const target = e.target as HTMLElement;
315
- target.releasePointerCapture(e.pointerId); // Liberamos el control del puntero
358
+ try {
359
+ if (target && target.hasPointerCapture?.(e.pointerId)) {
360
+ target.releasePointerCapture(e.pointerId);
361
+ }
362
+ } catch {
363
+ // Ignorar si la captura ya fue liberada o perdida por el navegador
364
+ }
316
365
  this._isDragging = false;
317
366
  this._dialog.classList.remove('dragging'); // Reactivamos las transiciones CSS
318
367
 
319
- const deltaY = e.clientY - this._startY;
368
+ // Evaluamos la posición exacta al momento de soltar
369
+ const hasValidY = typeof e.clientY === 'number' && e.clientY > 0;
370
+ const deltaY = hasValidY ? (e.clientY - this._startY) : this._lastDeltaY;
371
+
320
372
  // Si se movió más de 5px, bloqueamos clics accidentales para evitar disparar eventos subyacentes
321
373
  if (Math.abs(deltaY) > 5) {
322
374
  this._justDragged = true;
@@ -324,14 +376,24 @@ export class MoniBottomSheet extends MoniElement {
324
376
  this._justDragged = false;
325
377
  }, 50);
326
378
  }
379
+
327
380
  const isExpanded = this._dialog.classList.contains('expanded');
328
381
  const maxHeight = this._getMaxHeightPx();
382
+ const defaultHeight = this._defaultHeight || (this._sheetHeight * 0.6) || 300;
383
+ // Solo se considera off-screen si al SOLTAR el puntero se encuentra en o más allá del límite inferior
384
+ const isOffScreenBottom = hasValidY && typeof window !== 'undefined' && e.clientY >= (window.innerHeight - 30);
329
385
 
330
386
  if (isExpanded) {
331
387
  // Evaluamos la altura final tras soltar el ratón/dedo
332
388
  const finalHeight = this._sheetHeight - deltaY;
333
- if (finalHeight < this._defaultHeight - 80) {
334
- // Si se arrastró significativamente por debajo de la altura por defecto, lo cerramos
389
+ const shouldClose =
390
+ isOffScreenBottom ||
391
+ deltaY > this._sheetHeight * 0.4 ||
392
+ finalHeight < (defaultHeight - 80) ||
393
+ finalHeight < 100;
394
+
395
+ if (shouldClose) {
396
+ // Si se soltó significativamente hacia abajo o fuera de pantalla, lo cerramos
335
397
  this._animateAndClose().then((closed) => {
336
398
  if (!closed) {
337
399
  // Si se canceló el evento, regresamos a su lugar
@@ -339,18 +401,18 @@ export class MoniBottomSheet extends MoniElement {
339
401
  this._dialog.style.height = '';
340
402
  }
341
403
  });
342
- } else if (finalHeight < (maxHeight + this._defaultHeight) / 2) {
404
+ } else if (finalHeight < (maxHeight + defaultHeight) / 2) {
343
405
  // Si se soltó a medio camino, lo colapsamos a su altura normal
344
406
  this._dialog.classList.remove('expanded');
345
407
  this._dialog.style.transform = '';
346
408
  this._dialog.style.height = '';
347
409
  } else {
348
- // Si no se arrastró lo suficiente hacia abajo, retorna a estado expandido (snap back)
410
+ // Si se subió de nuevo hacia arriba o no se arrastró suficiente, retorna a estado expandido (snap back)
349
411
  this._dialog.style.transform = '';
350
412
  this._dialog.style.height = '';
351
413
  }
352
414
  } else {
353
- if (deltaY < 0) {
415
+ if (deltaY < 0 && !isOffScreenBottom) {
354
416
  // Evaluamos el arrastre hacia arriba desde el estado colapsado
355
417
  const finalHeight = this._sheetHeight - deltaY;
356
418
  if (finalHeight > this._sheetHeight + 80) {
@@ -360,35 +422,47 @@ export class MoniBottomSheet extends MoniElement {
360
422
  this._dialog.style.transform = '';
361
423
  this._dialog.style.height = '';
362
424
  } else {
363
- // Evaluamos el arrastre hacia abajo para cerrar el componente
364
- if (this._currentTranslationY > 80) {
425
+ // Evaluamos si al soltar superó el umbral de descarte o se soltó fuera de la pantalla
426
+ const threshold = Math.min(80, this._sheetHeight * 0.25);
427
+ const shouldClose =
428
+ isOffScreenBottom ||
429
+ deltaY > threshold;
430
+
431
+ if (shouldClose) {
365
432
  this._animateAndClose().then((closed) => {
366
433
  if (!closed) {
367
434
  this._dialog.style.transform = '';
368
435
  }
369
436
  });
370
437
  } else {
371
- // Regresa a su estado colapsado (snap back al inicio)
438
+ // Si el usuario bajó pero subió de nuevo antes de soltar (o no superó el umbral), regresa a abierto
372
439
  this._dialog.style.transform = '';
373
440
  }
374
441
  }
375
442
  }
443
+
444
+ this._lastDeltaY = 0;
376
445
  }
377
446
 
378
447
  /**
379
- * Aborta limpiamente la interacción de arrastre (Pointer Cancel).
380
- * Se dispara cuando el SO interrumpe el touch (ej: entrada de una llamada)
381
- * o si el navegador toma el control para un scroll nativo imprevisto.
448
+ * Maneja la cancelación del puntero o interrupción del SO.
449
+ * Si el usuario ya lo había arrastrado hacia abajo o hacia fuera de la pantalla,
450
+ * evalúa el cierre en lugar de simplemente cancelar y dejar la hoja colgada.
382
451
  */
383
452
  private _onPointerCancel(e: PointerEvent) {
384
- if (!this._isDragging) return;
453
+ if (this._isDragging) {
454
+ this._onPointerUp(e);
455
+ }
456
+ }
385
457
 
386
- const target = e.target as HTMLElement;
387
- target.releasePointerCapture(e.pointerId);
388
- this._isDragging = false;
389
- this._dialog.classList.remove('dragging');
390
- this._dialog.style.transform = '';
391
- this._dialog.style.height = '';
458
+ /**
459
+ * Se ejecuta cuando se pierde la captura de puntero (por ejemplo al arrastrar fuera
460
+ * del viewport del navegador o perder el foco de la ventana).
461
+ */
462
+ private _onLostPointerCapture(e: PointerEvent) {
463
+ if (this._isDragging) {
464
+ this._onPointerUp(e);
465
+ }
392
466
  }
393
467
 
394
468
  /**
@@ -427,6 +501,14 @@ export class MoniBottomSheet extends MoniElement {
427
501
  }
428
502
  if (changed.has('open')) {
429
503
  if (this.open) {
504
+ this._isClosing = false;
505
+ this._isDragging = false;
506
+ this._lastDeltaY = 0;
507
+ if (this._dialog) {
508
+ this._dialog.classList.remove('dragging', 'expanded');
509
+ this._dialog.style.transform = '';
510
+ this._dialog.style.height = '';
511
+ }
430
512
  if (changed.get('open') !== undefined) {
431
513
  emitMoniEvent(this, 'moni-open');
432
514
  }
@@ -440,8 +522,13 @@ export class MoniBottomSheet extends MoniElement {
440
522
  setTimeout(() => emitMoniEvent(this, 'moni-opened'), 300);
441
523
  }
442
524
  } else {
525
+ this._isClosing = false;
526
+ this._isDragging = false;
527
+ this._lastDeltaY = 0;
443
528
  if (this._dialog) {
444
- this._dialog.classList.remove('expanded');
529
+ this._dialog.classList.remove('expanded', 'dragging');
530
+ this._dialog.style.transform = '';
531
+ this._dialog.style.height = '';
445
532
  }
446
533
  if (this.positioning === 'body' && this._originalParent && this.parentNode === document.body) {
447
534
  this._originalParent.insertBefore(this, this._originalSibling);
@@ -628,6 +715,7 @@ export class MoniBottomSheet extends MoniElement {
628
715
  @pointermove=${this._onPointerMove}
629
716
  @pointerup=${this._onPointerUp}
630
717
  @pointercancel=${this._onPointerCancel}
718
+ @lostpointercapture=${this._onLostPointerCapture}
631
719
  >
632
720
  ${this.handle ? html`<div class="handle" aria-hidden="true"></div>` : nothing}
633
721
  <header part="header">