@webqit/webflo 0.20.32 → 0.20.34

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.
package/package.json CHANGED
@@ -12,7 +12,7 @@
12
12
  "vanila-javascript"
13
13
  ],
14
14
  "homepage": "https://webqit.io/tooling/webflo",
15
- "version": "0.20.32",
15
+ "version": "0.20.34",
16
16
  "license": "MIT",
17
17
  "repository": {
18
18
  "type": "git",
@@ -35,6 +35,9 @@ export class WebfloClient extends AppRuntime {
35
35
  #background;
36
36
  get background() { return this.#background; }
37
37
 
38
+ #viewport;
39
+ get viewport() { return this.#viewport; }
40
+
38
41
  get isClientSide() { return true; }
39
42
 
40
43
  constructor(bootstrap, host) {
@@ -56,6 +59,68 @@ export class WebfloClient extends AppRuntime {
56
59
  phase: 0
57
60
  };
58
61
  this.#background = new StarPort({ handshake: 1, autoClose: false });
62
+
63
+ // ---------------------
64
+ // Dynamic viewport styling
65
+
66
+ const oskToken = 'interactive-widget=resizes-content';
67
+ const hasOsk = (content) => content?.includes(oskToken);
68
+ const removeOsk = (content) => {
69
+ if (content?.includes('interactive-widget')) {
70
+ return content
71
+ .split(',')
72
+ .filter((s) => !s.includes('interactive-widget'))
73
+ .map((s) => s.trim())
74
+ .join(', ');
75
+ }
76
+ return content;
77
+ };
78
+ const addOsk = (content) => {
79
+ if (content?.includes('interactive-widget')) {
80
+ return content
81
+ .split(',')
82
+ .map((s) => s.includes('interactive-widget') ? oskToken : s.trim())
83
+ .join(', ');
84
+ }
85
+ return content + ', ' + oskToken;
86
+ };
87
+
88
+ const viewportMeta = document.querySelector('meta[name="viewport"]');
89
+ const viewportMetaInitialContent = viewportMeta?.content;
90
+ const themeColorMeta = document.querySelector('meta[name="theme-color"]');
91
+ const renderViewportMetas = (entry) => {
92
+ viewportMeta?.setAttribute('content', entry.osk ? addOsk(viewportMetaInitialContent) : removeOsk(viewportMetaInitialContent));
93
+ themeColorMeta?.setAttribute('content', entry.themeColor);
94
+ };
95
+
96
+ const initial = {
97
+ themeColor: themeColorMeta?.content,
98
+ osk: hasOsk(viewportMetaInitialContent),
99
+ };
100
+ const viewportStack = [initial];
101
+
102
+ this.#viewport = {
103
+ push(entryId, { themeColor = viewportStack[0].themeColor, osk = viewportStack[0].osk }) {
104
+ if (typeof entryId !== 'string' || !entryId?.trim()) {
105
+ throw new Error('entryId cannot be ommited');
106
+ }
107
+ if (viewportStack.find((e) => e.entryId === entryId)) return;
108
+ viewportStack.unshift({ entryId, themeColor, osk });
109
+ renderViewportMetas(viewportStack[0]);
110
+ },
111
+ pop(entryId) {
112
+ if (typeof entryId !== 'string' || !entryId?.trim()) {
113
+ throw new Error('entryId cannot be ommited');
114
+ }
115
+ const index = viewportStack.findIndex((e) => e.entryId === entryId);
116
+ if (index === -1) return;
117
+ viewportStack.splice(index, 1);
118
+ renderViewportMetas(viewportStack[0]);
119
+ },
120
+ current() {
121
+ return viewportStack[0];
122
+ }
123
+ };
59
124
  }
60
125
 
61
126
  async initialize() {
@@ -600,6 +665,7 @@ export class WebfloClient extends AppRuntime {
600
665
  state: {},
601
666
  data: $response.body,
602
667
  env: 'client',
668
+ viewport: this.viewport,
603
669
  navigator: this.navigator,
604
670
  location: this.location,
605
671
  network: this.network, // request, redirect, error, status, remote
@@ -42,12 +42,12 @@ export class WebfloRootClientA extends WebfloClient {
42
42
  async initialize() {
43
43
  // INITIALIZATIONS
44
44
  const instanceController = await super.initialize();
45
-
45
+
46
46
  // Bind network status handlers
47
47
  const onlineHandler = () => Observer.set(this.network, 'status', window.navigator.onLine);
48
48
  window.addEventListener('online', onlineHandler, { signal: instanceController.signal });
49
49
  window.addEventListener('offline', onlineHandler, { signal: instanceController.signal });
50
-
50
+
51
51
  // Window opener pinging
52
52
  if (window.opener) {
53
53
  const beforeunloadHandler = () => window.opener.postMessage('close');
@@ -93,7 +93,7 @@ export class WebfloRootClientA extends WebfloClient {
93
93
  scopeObj.data = JSON.parse(this.host.querySelector(`script[rel="hydration"][type="application/json"]`)?.textContent?.trim() || 'null');
94
94
  } catch (e) { }
95
95
  scopeObj.response = new LiveResponse(scopeObj.data, { headers: { 'Content-Type': 'application/json' } });
96
-
96
+
97
97
  for (const name of ['X-Message-Port', 'X-Webflo-Dev-Mode']) {
98
98
  const metaElement = this.host.querySelector(`meta[name="${name}"]`);
99
99
  if (!metaElement) continue;
@@ -136,7 +136,7 @@ export class WebfloRootClientA extends WebfloClient {
136
136
  try { window.history.pushState({}, '', newHref); } catch (e) { }
137
137
  };
138
138
  const instanceController = super.controlClassic/*IMPORTANT*/(locationCallback);
139
-
139
+
140
140
  // ONPOPSTATE
141
141
  const popstateHandler = (e) => {
142
142
  if (this.isHashChange(location)) {
@@ -158,7 +158,7 @@ export class WebfloRootClientA extends WebfloClient {
158
158
  this.navigate(location.href, {}, detail);
159
159
  };
160
160
  window.addEventListener('popstate', popstateHandler, { signal: instanceController.signal });
161
-
161
+
162
162
  return instanceController;
163
163
  }
164
164
 
@@ -18,6 +18,8 @@ export class WebfloSubClient extends WebfloClient {
18
18
 
19
19
  get capabilities() { return this.#superRuntime.capabilities; }
20
20
 
21
+ get viewport() { return this.#superRuntime.viewport; }
22
+
21
23
  get withViewTransitions() { return this.host.hasAttribute('viewtransitions'); }
22
24
 
23
25
  constructor(superRuntime, host) {
@@ -1,6 +1,18 @@
1
1
  // ---------------- ToastElement
2
2
 
3
- export class ToastElement extends HTMLElement {
3
+ class ToastElement extends HTMLElement {
4
+
5
+ set type(value) {
6
+ if ([undefined, null].includes(value)) {
7
+ this.removeAttribute('type');
8
+ } else this.setAttribute('type', value);
9
+ }
10
+
11
+ get type() { return this.getAttribute('type'); }
12
+
13
+ get contentHTML() { return ''; }
14
+
15
+ get css() { return ''; }
4
16
 
5
17
  #childToast = null;
6
18
 
@@ -21,12 +33,6 @@ export class ToastElement extends HTMLElement {
21
33
  }
22
34
  }
23
35
 
24
- connectedCallback() {
25
- if (!this.popover) {
26
- this.popover = 'auto';
27
- }
28
- }
29
-
30
36
  render({ content, context }, childToast = null, recursion = 1) {
31
37
  if (context && recursion > 0) {
32
38
  const directChildToast = document.createElement(this.tagName);
@@ -54,18 +60,12 @@ export class ToastElement extends HTMLElement {
54
60
  this.innerHTML = content.message;
55
61
  }
56
62
 
57
- set type(value) {
58
- if ([undefined, null].includes(value)) {
59
- this.removeAttribute('type');
60
- } else this.setAttribute('type', value);
63
+ connectedCallback() {
64
+ if (!this.popover) {
65
+ this.popover = 'auto';
66
+ }
61
67
  }
62
68
 
63
- get type() { return this.getAttribute('type'); }
64
-
65
- get contentHTML() { return ''; }
66
-
67
- get css() { return ''; }
68
-
69
69
  constructor() {
70
70
  super();
71
71
  this.attachShadow({ mode: 'open' });
@@ -315,7 +315,7 @@ export class ToastElement extends HTMLElement {
315
315
 
316
316
  // ---------------- ModalElement
317
317
 
318
- export class ModalMinmaxEvent extends Event {
318
+ class ModalMinmaxEvent extends Event {
319
319
 
320
320
  #ratio;
321
321
  get ratio() { return this.#ratio; }
@@ -326,64 +326,110 @@ export class ModalMinmaxEvent extends Event {
326
326
  }
327
327
  }
328
328
 
329
- export class ModalElement extends HTMLElement {
329
+ class ModalElement extends HTMLElement {
330
330
 
331
- updateScrollViewDimensions() {
332
- const viewElement = this.shadowRoot.querySelector('.view');
333
- const headerElement = this.shadowRoot.querySelector('header');
334
- const headerBoxElement = this.shadowRoot.querySelector('.header-box');
335
- const footerElement = this.shadowRoot.querySelector('footer');
336
- requestAnimationFrame(() => {
337
- viewElement.style.setProperty('--header-box-height', headerBoxElement.offsetHeight + 'px');
338
- viewElement.style.setProperty('--header-max-height', headerElement.offsetHeight + 'px');
339
- viewElement.style.setProperty('--footer-max-height', footerElement.offsetHeight + 'px');
340
- if (this.classList.contains('_container')) return;
341
- viewElement.style.setProperty('--view-width', viewElement.clientWidth/* instead of offsetHeight; safari reasons */ + 'px');
342
- viewElement.style.setProperty('--view-height', viewElement.clientHeight/* instead of offsetHeight; safari reasons */ + 'px');
343
- });
344
- }
331
+ #onminmaxHandler = null;
345
332
 
346
- connectedCallback() {
347
- if (!this.popover) {
348
- this.popover = 'manual';
333
+ set onminmax(handler) {
334
+ if (this.#onminmaxHandler) {
335
+ this.removeEventListener('onminmax', this.#onminmaxHandler);
349
336
  }
350
- this.bindMinmaxWorker();
351
-
352
- if (this.hasAttribute('open')) {
353
- this.showPopover();
337
+ if (typeof handler === 'function') {
338
+ this.addEventListener('minmax', this.#onminmaxHandler);
339
+ } else if (handler !== null && handler !== undefined) {
340
+ throw new Error('onminmax must be null or a function');
354
341
  }
342
+ this.#onminmaxHandler = handler;
343
+ }
355
344
 
356
- if (this.matches(':popover-open')) {
357
- this.updateScrollViewDimensions();
358
- }
345
+ get onminmax() { return this.#onminmaxHandler; }
346
+
347
+ set type(value) {
348
+ if ([undefined, null].includes(value)) {
349
+ this.removeAttribute('type');
350
+ } else this.setAttribute('type', value);
359
351
  }
360
352
 
361
- disconnectedCallback() {
362
- this.#unbindMinmaxWorker?.();
363
- this.#unbindMinmaxWorker = null;
353
+ get type() { return this.getAttribute('type'); }
354
+
355
+ get headerBoxHTML() { return ''; }
356
+
357
+ get headerHTML() { return ''; }
358
+
359
+ get mainHTML() { return ''; }
360
+
361
+ get contentHTML() { return ''; }
362
+
363
+ get footerHTML() { return ''; }
364
+
365
+ get css() { return ''; }
366
+
367
+ #viewElement;
368
+ #sentinelElement;
369
+ #spacingElement;
370
+ #headerElement;
371
+ #headerBoxElement;
372
+ #footerElement;
373
+
374
+ updateScrollViewDimensions() {
375
+ requestAnimationFrame(() => {
376
+ let viewWidth, viewHeight;
377
+
378
+ const swipeDismiss = this.classList.contains('_swipe-dismiss');
379
+ const minmaxScroll = !!window.getComputedStyle(this).getPropertyValue('--modal-minmax-length');
380
+
381
+ if (swipeDismiss || minmaxScroll) {
382
+ requestAnimationFrame(() => {
383
+ let left = 0, top = 0;
384
+ if (this.matches('._left._horz, ._top:not(._horz)')) {
385
+ this.#viewElement.scrollTo({ top, left });
386
+ } else {
387
+ if (this.classList.contains('_horz')) {
388
+ viewWidth = this.#viewElement.clientWidth/* instead of offsetHeight; safari reasons */;
389
+ left = viewWidth - this.#spacingElement.clientWidth;
390
+ } else {
391
+ viewHeight = this.#viewElement.clientHeight/* instead of offsetHeight; safari reasons */;
392
+ top = viewHeight - this.#spacingElement.clientHeight;
393
+ }
394
+ if (this.#viewElement.scrollTop < top || this.#viewElement.scrollLeft < left) {
395
+ this.#viewElement.scrollTo({ top, left });
396
+ }
397
+ }
398
+ });
399
+ }
400
+
401
+ this.#viewElement.style.setProperty('--header-box-height', this.#headerBoxElement.offsetHeight + 'px');
402
+ this.#viewElement.style.setProperty('--header-max-height', this.#headerElement.offsetHeight + 'px');
403
+ this.#viewElement.style.setProperty('--footer-max-height', this.#footerElement.offsetHeight + 'px');
404
+
405
+ if (this.classList.contains('_container')) return;
406
+ if (viewWidth === undefined) viewWidth = this.#viewElement.clientWidth;
407
+ if (viewHeight === undefined) viewHeight = this.#viewElement.clientHeight;
408
+
409
+ this.#viewElement.style.setProperty('--view-width', viewWidth + 'px');
410
+ this.#viewElement.style.setProperty('--view-height', viewHeight + 'px');
411
+ });
364
412
  }
365
413
 
366
414
  #unbindMinmaxWorker = null;
367
415
 
368
416
  bindMinmaxWorker() {
369
417
  const swipeDismiss = this.classList.contains('_swipe-dismiss');
370
- const minmaxEvents = this.classList.contains('_minmax');
418
+ const minmaxEvents = this.classList.contains('_minmax-events');
371
419
 
372
420
  if (!swipeDismiss && !minmaxEvents) return;
373
421
 
374
- const viewElement = this.shadowRoot.querySelector('.view');
375
- const sentinelElement = this.shadowRoot.querySelector('.sentinel');
376
- const spacingElement = viewElement.querySelector('.spacing');
377
-
378
422
  const options = {
379
- root: viewElement,
423
+ root: this.#viewElement,
380
424
  threshold: [0, 1]
381
425
  };
382
426
 
383
427
  const observer = new IntersectionObserver((entries) => {
428
+ if (!this.#userScrolled) return;
429
+
384
430
  for (const entry of entries) {
385
431
  // Minmax events
386
- if (entry.target === spacingElement) {
432
+ if (entry.target === this.#spacingElement) {
387
433
  const event = new ModalMinmaxEvent(1 - entry.intersectionRatio);
388
434
  this.dispatchEvent(event);
389
435
 
@@ -394,65 +440,80 @@ export class ModalElement extends HTMLElement {
394
440
  }
395
441
 
396
442
  // For auto-closing
397
- if (entry.target === sentinelElement && entry.isIntersecting) {
443
+ if (entry.target === this.#sentinelElement
444
+ && entry.isIntersecting
445
+ && entry.intersectionRatio >= 0.8) {
398
446
  this.hidePopover();
399
- setTimeout(() => spacingElement.scrollIntoView(), 300);
400
447
  }
401
448
  }
402
449
  }, options);
403
450
 
404
- if (minmaxEvents) observer.observe(spacingElement);
405
- if (swipeDismiss) observer.observe(sentinelElement);
406
- this.#unbindMinmaxWorker = () => observer.disconnect();
407
- }
408
-
409
- #onminmaxHandler = null;
451
+ setTimeout(() => {
452
+ if (minmaxEvents) observer.observe(this.#spacingElement);
453
+ if (swipeDismiss) observer.observe(this.#sentinelElement);
454
+ }, 200);
410
455
 
411
- set onminmax(handler) {
412
- if (this.#onminmaxHandler) {
413
- this.removeEventListener('onminmax', this.#onminmaxHandler);
414
- }
415
- if (typeof handler === 'function') {
416
- this.addEventListener('minmax', this.#onminmaxHandler);
417
- } else if (handler !== null && handler !== undefined) {
418
- throw new Error('onminmax must be null or a function');
419
- }
420
- this.#onminmaxHandler = handler;
456
+ this.#unbindMinmaxWorker = () => observer.disconnect();
421
457
  }
422
458
 
423
- get onminmax() { return this.#onminmaxHandler; }
459
+ #userScrolled = false;
460
+ #unbindDimensionsWorker;
424
461
 
425
- set type(value) {
426
- if ([undefined, null].includes(value)) {
427
- this.removeAttribute('type');
428
- } else this.setAttribute('type', value);
429
- }
462
+ #bindDimensionsWorker() {
463
+ this.#userScrolled = false;
464
+ const handleUserScroll = () => this.#userScrolled = true;
465
+ this.#viewElement.addEventListener('scroll', handleUserScroll);
430
466
 
431
- get type() { return this.getAttribute('type'); }
432
467
 
433
- get headerBoxHTML() { return ''; }
468
+ this.updateScrollViewDimensions();
469
+ const handleResize = () => this.updateScrollViewDimensions();
470
+ window.addEventListener('resize', handleResize);
434
471
 
435
- get headerHTML() { return ''; }
472
+ this.#unbindDimensionsWorker?.();
473
+ this.#unbindDimensionsWorker = () => {
474
+ window.removeEventListener('resize', handleResize);
475
+ this.#viewElement.removeEventListener('scroll', handleUserScroll);
476
+ };
477
+ }
436
478
 
437
- get mainHTML() { return ''; }
479
+ connectedCallback() {
480
+ if (!this.popover) {
481
+ this.popover = 'manual';
482
+ }
483
+ if (this.hasAttribute('open')) {
484
+ this.showPopover();
485
+ }
486
+ }
438
487
 
439
- get contentHTML() { return ''; }
488
+ disconnectedCallback() {
489
+ this.#unbindDimensionsWorker?.();
490
+ this.#unbindDimensionsWorker = null;
491
+ this.#unbindMinmaxWorker?.();
492
+ this.#unbindMinmaxWorker = null;
493
+ }
440
494
 
441
- get footerHTML() { return ''; }
495
+ static get observedAttributes() {
496
+ return ['class'];
497
+ }
442
498
 
443
- get css() { return ''; }
499
+ attributeChangedCallback(name, old, _new) {
500
+ if (name === 'class') this.#bindDimensionsWorker();
501
+ }
444
502
 
445
503
  constructor() {
446
504
  super();
447
505
  this.attachShadow({ mode: 'open' });
448
506
 
449
507
  this.addEventListener('toggle', (e) => {
450
- if (e.newState !== 'open') return;
451
- this.updateScrollViewDimensions();
452
- });
453
-
454
- window.addEventListener('resize', () => {
455
- this.updateScrollViewDimensions();
508
+ if (e.newState === 'open') {
509
+ this.#bindDimensionsWorker();
510
+ this.bindMinmaxWorker();
511
+ } else if (e.newState === 'closed') {
512
+ this.#unbindDimensionsWorker?.();
513
+ this.#unbindDimensionsWorker = null;
514
+ this.#unbindMinmaxWorker?.();
515
+ this.#unbindMinmaxWorker = null;
516
+ }
456
517
  });
457
518
 
458
519
  this.shadowRoot.innerHTML = `
@@ -489,27 +550,20 @@ export class ModalElement extends HTMLElement {
489
550
  </button>
490
551
  </div>
491
552
 
492
- <div class="scrollport-anchor">
493
- <div class="scrollport">
494
- <div class="scrollbar-track">
495
- <div class="scrollbar-thumb"></div>
496
- </div>
553
+ </header>
554
+
555
+ <div class="scrollport-anchor">
556
+ <div class="scrollport">
557
+ <div class="scrollbar-track">
558
+ <div class="scrollbar-thumb"></div>
497
559
  </div>
498
560
  </div>
499
- </header>
561
+ </div>
500
562
 
501
563
  ${this.mainHTML || `<div class="main" part="main">${this.contentHTML || `<slot></slot>`
502
564
  }</div>`}
503
565
 
504
566
  <footer part="footer">
505
- <div class="scrollport-anchor">
506
- <div class="scrollport">
507
- <div class="scrollbar-track">
508
- <div class="scrollbar-thumb"></div>
509
- </div>
510
- </div>
511
- </div>
512
-
513
567
  <div class="footer-bar" part="footer-bar">
514
568
  <slot
515
569
  name="footer"
@@ -586,7 +640,6 @@ export class ModalElement extends HTMLElement {
586
640
 
587
641
  --expanse-length: var(--modal-expanse-length, 0px);
588
642
  --minmax-length: var(--modal-minmax-length, 0px);
589
- --swipe-dismiss-length: var(--modal-swipe-dismiss-length, 0px);
590
643
 
591
644
  --scrollbar-thumb-color: var(--modal-scrollbar-thumb-color, black);
592
645
  --scrollbar-thumb-width: var(--modal-scrollbar-thumb-width, 4px);
@@ -603,6 +656,14 @@ export class ModalElement extends HTMLElement {
603
656
  --entry-transform: translateY(var(--translation));
604
657
  --exit-transform: translateY(calc(var(--translation) * var(--exit-factor)));
605
658
  }
659
+
660
+ :host(._swipe-dismiss) .view {
661
+ --swipe-dismiss-length: var(--modal-swipe-dismiss-length, calc(var(--view-height) - var(--minmax-length)));
662
+ }
663
+
664
+ :host(._horz._swipe-dismiss) .view {
665
+ --swipe-dismiss-length: var(--modal-swipe-dismiss-length, calc(var(--view-width) - var(--minmax-length)));
666
+ }
606
667
 
607
668
  /* transform reversal */
608
669
 
@@ -648,6 +709,11 @@ export class ModalElement extends HTMLElement {
648
709
  --view-width: 100cqw;
649
710
  }
650
711
 
712
+ :host(._container._horz) .view {
713
+ --view-height: 100cqh;
714
+ --view-width: calc(100cqw - var(--expanse-length));
715
+ }
716
+
651
717
  /* transform reversal */
652
718
 
653
719
  :host(:is(._top:not(._horz), ._left._horz)) .view {
@@ -724,9 +790,8 @@ export class ModalElement extends HTMLElement {
724
790
 
725
791
  /* flex orientation */
726
792
 
727
- :host(:popover-open),
793
+ :host,
728
794
  .view {
729
- display: flex;
730
795
  flex-direction: column;
731
796
  align-items: stretch;
732
797
  }
@@ -748,7 +813,8 @@ export class ModalElement extends HTMLElement {
748
813
  /* spacing */
749
814
 
750
815
  :host>.spacing,
751
- .view>.spacing {
816
+ .view>.spacing,
817
+ .view>.sentinel {
752
818
  position: relative;
753
819
  display: block;
754
820
  flex-shrink: 0;
@@ -769,17 +835,15 @@ export class ModalElement extends HTMLElement {
769
835
  :host(:not(._horz)) .view>.spacing { height: var(--minmax-length); }
770
836
  :host(._horz) .view>.spacing { width: var(--minmax-length); }
771
837
 
772
- :host(:not(._top, ._horz)) .view>.spacing { margin-top: var(--swipe-dismiss-length); }
773
- :host(._top:not(._horz)) .view>.spacing { margin-bottom: var(--swipe-dismiss-length); }
774
-
775
- :host(._horz:not(._left)) .view>.spacing { margin-left: var(--swipe-dismiss-length); }
776
- :host(._horz._left) .view>.spacing { margin-right: var(--swipe-dismiss-length); }
838
+ :host(:not(._horz)) .view>.sentinel { height: var(--swipe-dismiss-length); }
839
+ :host(._horz) .view>.sentinel { width: var(--swipe-dismiss-length); }
777
840
 
778
841
  /* ----------- */
779
842
 
780
843
  .view {
781
844
  position: relative;
782
845
  flex-grow: 1;
846
+ display: flex;
783
847
 
784
848
  pointer-events: none;
785
849
 
@@ -833,16 +897,13 @@ export class ModalElement extends HTMLElement {
833
897
  position: relative;
834
898
  flex-grow: 1;
835
899
 
836
- min-height: 100%;
837
- min-width: 100%;
838
-
839
900
  pointer-events: auto;
840
901
 
841
902
  display: flex;
842
903
  flex-direction: column;
843
904
  }
844
905
 
845
- :host(._swipe-dismiss) .container {
906
+ :host(._swipe-dismiss-fadeout) .container {
846
907
  animation-timing-function: linear;
847
908
  animation-fill-mode: both;
848
909
  animation-name: appear;
@@ -850,7 +911,7 @@ export class ModalElement extends HTMLElement {
850
911
  animation-range: 0 var(--swipe-dismiss-length);
851
912
  }
852
913
 
853
- :host(._swipe-dismiss:is(._top:not(._horz), ._left._horz)) .container {
914
+ :host(._swipe-dismiss-fadeout:is(._top:not(._horz), ._left._horz)) .container {
854
915
  animation-name: disappear;
855
916
  animation-range: calc(100% - var(--swipe-dismiss-length)) 100%;
856
917
  }
@@ -860,7 +921,7 @@ export class ModalElement extends HTMLElement {
860
921
  header {
861
922
  position: sticky;
862
923
  top: calc(var(--header-box-height) * -1);
863
- z-index: 1;
924
+ z-index: 2;
864
925
 
865
926
  display: flex;
866
927
  flex-direction: column;
@@ -870,6 +931,8 @@ export class ModalElement extends HTMLElement {
870
931
 
871
932
  border-top-left-radius: var(--radius-top-left);
872
933
  border-top-right-radius: var(--radius-top-right);
934
+
935
+ order: 1;
873
936
  }
874
937
 
875
938
  :host(:not(._horz)) header {
@@ -962,6 +1025,8 @@ export class ModalElement extends HTMLElement {
962
1025
 
963
1026
  color: var(--footer-color-default);
964
1027
  background: var(--footer-background);
1028
+
1029
+ order: 5;
965
1030
  }
966
1031
 
967
1032
  :host([type="info"]) footer {
@@ -986,15 +1051,16 @@ export class ModalElement extends HTMLElement {
986
1051
 
987
1052
  /* ----------- */
988
1053
 
989
- :host(:popover-open) .view {
1054
+ .view {
990
1055
  scroll-snap-type: y mandatory;
991
1056
  }
992
1057
 
993
- :host(._horz:popover-open) .view {
1058
+ :host(._horz) .view {
994
1059
  scroll-snap-type: x mandatory;
995
1060
  }
996
1061
 
997
- .view>.spacing {
1062
+ .view>.spacing,
1063
+ .view>.sentinel {
998
1064
  scroll-snap-align: var(--scroll-snap-start);
999
1065
  }
1000
1066
 
@@ -1003,6 +1069,8 @@ export class ModalElement extends HTMLElement {
1003
1069
  scroll-margin-top: var(--header-min-height);
1004
1070
  scroll-margin-bottom: var(--footer-min-height);
1005
1071
  scroll-snap-align: var(--scroll-snap-start);
1072
+
1073
+ order: 3;
1006
1074
  }
1007
1075
 
1008
1076
  :host(:is(._top, ._left._horz)) .main {
@@ -1024,42 +1092,108 @@ export class ModalElement extends HTMLElement {
1024
1092
  /* ----------- */
1025
1093
 
1026
1094
  .scrollport-anchor {
1027
- position: relative;
1095
+ order: 2;
1096
+
1097
+ position: sticky;
1098
+ top: var(--header-min-height);
1099
+ bottom: var(--footer-min-height);
1100
+ left: 0;
1101
+ right: 0;
1102
+ display: flex;
1103
+ flex-direction: column;
1104
+
1028
1105
  height: 0;
1106
+ width: var(--view-width);
1029
1107
  }
1030
1108
 
1031
- :host(:not(._top:not(._horz))) footer .scrollport-anchor,
1032
- :host(._top:not(._horz)) header .scrollport-anchor {
1033
- display: none;
1109
+ :host(:is(._left._horz, ._top:not(._horz))) .scrollport-anchor {
1110
+ justify-content: end;
1111
+ order: 4;
1034
1112
  }
1035
1113
 
1036
1114
  .scrollport {
1037
- position: sticky;
1038
- top: var(--header-min-height);
1039
- left: 0;
1040
- right: 0;
1041
-
1042
- container-type: size;
1115
+ position: relative;
1116
+
1043
1117
  height: var(--view-inner-height);
1044
1118
  width: var(--view-width);
1119
+ flex-shrink: 0;
1045
1120
 
1046
1121
  pointer-events: none;
1047
1122
  }
1048
1123
 
1049
- footer .scrollport {
1124
+ :host(._top:not(._horz)) .scrollport {
1125
+ height: calc(var(--view-inner-height) - var(--header-box-height));
1126
+ }
1127
+
1128
+ /* -- scroll unfold -- */
1129
+
1130
+ :host(._scroll-unfold) .scrollport {
1131
+ display: flex;
1132
+ flex-direction: column;
1133
+ justify-content: space-between;
1134
+ align-items: stretch;
1135
+ }
1136
+
1137
+ :host(._scroll-unfold._horz) .scrollport {
1138
+ flex-direction: row;
1139
+ }
1140
+
1141
+ :host(._scroll-unfold) .scrollport::before,
1142
+ :host(._scroll-unfold) .scrollport::after {
1143
+ position: sticky;
1144
+ display: block;
1145
+ content: "";
1146
+ opacity: 0;
1147
+
1148
+ background: var(--background);
1149
+
1150
+ mask-repeat: no-repeat;
1151
+ mask-size: 100% 100%;
1152
+
1153
+ animation-timing-function: linear;
1154
+ animation-fill-mode: forwards;
1155
+ animation-name: appear;
1156
+ animation-timeline: --view-scroll;
1157
+
1158
+ animation-range: var(--scrollbar-progress-range);
1159
+ }
1160
+
1161
+ :host(._scroll-unfold:not(._horz)) .scrollport::before,
1162
+ :host(._scroll-unfold:not(._horz)) .scrollport::after {
1163
+ top: var(--header-min-height);
1164
+ height: 25%;
1165
+
1166
+ mask-image: linear-gradient(to bottom, black 0%, transparent 100%);
1167
+ -webkit-mask-image: linear-gradient(to bottom, black 0%, transparent 100%);
1168
+ }
1169
+
1170
+ :host(._scroll-unfold:not(._horz)) .scrollport::after {
1171
+ bottom: var(--footer-min-height);
1050
1172
  top: auto;
1051
- position: absolute;
1052
- bottom: 0;
1173
+ opacity: 1;
1174
+ animation-name: disappear;
1175
+ transform: scaleY(-1);
1053
1176
  }
1054
1177
 
1055
- :host(._scrollbars._top:not(._horz)) .scrollport {
1056
- height: calc(var(--view-inner-height) - var(--header-box-height));
1178
+ :host(._scroll-unfold._horz) .scrollport::before,
1179
+ :host(._scroll-unfold._horz) .scrollport::after {
1180
+ left: 0;
1181
+ width: 25%;
1182
+
1183
+ mask-image: linear-gradient(to right, black 0%, transparent 100%);
1184
+ -webkit-mask-image: linear-gradient(to right, black 0%, transparent 100%);
1057
1185
  }
1058
1186
 
1059
- :host(._scrollbars._left._horz) .scrollport {
1060
- width: calc(var(--view-width) - var(--minmax-length));
1187
+ :host(._scroll-unfold._horz) .scrollport::after {
1188
+ right: 0;
1189
+ left: auto;
1190
+ opacity: 1;
1191
+ animation-name: disappear;
1192
+ transform: scaleX(-1);
1061
1193
  }
1062
1194
 
1195
+ /* -- scrollbar -- */
1196
+
1063
1197
  :host(._scrollbars) .scrollbar-track {
1064
1198
  position: absolute;
1065
1199
  display: block;
@@ -1070,6 +1204,8 @@ export class ModalElement extends HTMLElement {
1070
1204
  right: 0;
1071
1205
  padding: 6px;
1072
1206
 
1207
+ container-type: size;
1208
+
1073
1209
  opacity: 0;
1074
1210
 
1075
1211
  animation: appear linear;
@@ -1104,6 +1240,7 @@ export class ModalElement extends HTMLElement {
1104
1240
  :host(._scrollbars._horz) .scrollbar-thumb {
1105
1241
  height: var(--scrollbar-thumb-width);
1106
1242
  width: var(--scrollbar-thumb-height);
1243
+
1107
1244
  --scrollbar-thumb-start: translateX(0);
1108
1245
  --scrollbar-thumb-length: translateX(calc(100cqw - 100%));
1109
1246
  }
@@ -1133,6 +1270,39 @@ export class ModalElement extends HTMLElement {
1133
1270
  backdrop-filter 0.2s;
1134
1271
  }
1135
1272
 
1273
+ :host(._swipe-dismiss._container) {
1274
+ timeline-scope: --view-scroll;
1275
+ }
1276
+
1277
+ :host(._swipe-dismiss._container)::backdrop {
1278
+ opacity: 0;
1279
+
1280
+ animation-timing-function: linear;
1281
+ animation-fill-mode: forwards;
1282
+ animation-name: appear;
1283
+ animation-timeline: --view-scroll;
1284
+
1285
+ animation-range: 0 calc(100cqh - var(--expanse-length) - var(--minmax-length));
1286
+ }
1287
+
1288
+ :host(._swipe-dismiss._container._horz)::backdrop {
1289
+ animation-range: 0 calc(100cqw - var(--expanse-length) - var(--minmax-length));
1290
+ }
1291
+
1292
+ :host(._swipe-dismiss._container._top:not(._horz))::backdrop,
1293
+ :host(._swipe-dismiss._container._left._horz)::backdrop {
1294
+ opacity: 1;
1295
+ animation-name: disappear;
1296
+ }
1297
+
1298
+ :host(._swipe-dismiss._container._top:not(._horz))::backdrop {
1299
+ animation-range: calc(100% - (100cqh - var(--expanse-length) - var(--minmax-length))) 100%;
1300
+ }
1301
+
1302
+ :host(._swipe-dismiss._container._left._horz)::backdrop {
1303
+ animation-range: calc(100% - (100cqw - var(--expanse-length) - var(--minmax-length))) 100%;
1304
+ }
1305
+
1136
1306
  :host(:popover-open)::backdrop {
1137
1307
  backdrop-filter: blur(3px);
1138
1308
  }
@@ -1185,7 +1355,7 @@ export class ModalElement extends HTMLElement {
1185
1355
 
1186
1356
  .main {
1187
1357
  color: var(--color-default);
1188
- background-color: var(--background);
1358
+ background: var(--background);
1189
1359
  }
1190
1360
 
1191
1361
  .view:not(:has(footer slot:is(.has-slotted, :not(:empty)))) .main {
@@ -1227,12 +1397,19 @@ export class ModalElement extends HTMLElement {
1227
1397
  ${this.css}
1228
1398
  </style>
1229
1399
  `;
1400
+
1401
+ this.#viewElement = this.shadowRoot.querySelector('.view');
1402
+ this.#sentinelElement = this.#viewElement.querySelector('.sentinel');
1403
+ this.#spacingElement = this.#viewElement.querySelector('.spacing');
1404
+ this.#headerElement = this.#viewElement.querySelector('header');
1405
+ this.#headerBoxElement = this.#viewElement.querySelector('.header-box');
1406
+ this.#footerElement = this.#viewElement.querySelector('footer');
1230
1407
  }
1231
1408
  }
1232
1409
 
1233
1410
  // ---------------- DialogElement
1234
1411
 
1235
- export class DialogResponseEvent extends Event {
1412
+ class DialogResponseEvent extends Event {
1236
1413
 
1237
1414
  #data;
1238
1415
  get data() { return this.#data; }
@@ -1243,7 +1420,7 @@ export class DialogResponseEvent extends Event {
1243
1420
  }
1244
1421
  }
1245
1422
 
1246
- export class DialogElement extends ModalElement {
1423
+ class DialogElement extends ModalElement {
1247
1424
 
1248
1425
  constructor() {
1249
1426
  super();
@@ -1336,6 +1513,7 @@ export class DialogElement extends ModalElement {
1336
1513
  }
1337
1514
 
1338
1515
  .main {
1516
+ flex-shrink: 0;
1339
1517
  display: flex;
1340
1518
  flex-direction: column;
1341
1519
  gap: 1rem;
@@ -1394,7 +1572,7 @@ export class DialogElement extends ModalElement {
1394
1572
 
1395
1573
  // ---------------- PromptElement
1396
1574
 
1397
- export class PromptElement extends DialogElement {
1575
+ class PromptElement extends DialogElement {
1398
1576
 
1399
1577
  static get observedAttributes() {
1400
1578
  return ['value', 'placeholder'].concat(super.observedAttributes || []);
@@ -1403,8 +1581,8 @@ export class PromptElement extends DialogElement {
1403
1581
  attributeChangedCallback(name, old, _new) {
1404
1582
  super.attributeChangedCallback?.(...arguments);
1405
1583
  const input = this.shadowRoot.querySelector('input');
1406
- if (name === 'value') { input.value = _new; }
1407
- if (name === 'placeholder') { input.placeholder = _new; }
1584
+ if (name === 'value') input.value = _new;
1585
+ if (name === 'placeholder') input.placeholder = _new;
1408
1586
  }
1409
1587
 
1410
1588
  set placeholder(value) {
@@ -1473,7 +1651,7 @@ export class PromptElement extends DialogElement {
1473
1651
 
1474
1652
  // ---------------- ConfirmElement
1475
1653
 
1476
- export class ConfirmElement extends DialogElement {
1654
+ class ConfirmElement extends DialogElement {
1477
1655
  get actionTexts() { return ['No', 'Yes']; }
1478
1656
 
1479
1657
  respondWith(response) { super.respondWith(!!response); }
@@ -1483,7 +1661,7 @@ export class ConfirmElement extends DialogElement {
1483
1661
 
1484
1662
  // ---------------- AlertElement
1485
1663
 
1486
- export class AlertElement extends DialogElement {
1664
+ class AlertElement extends DialogElement {
1487
1665
  get actionTexts() { return ['', 'Got it']; }
1488
1666
  }
1489
1667