@webqit/webflo 0.20.32 → 0.20.33

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.33",
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,108 @@ 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
+ if (this.classList.contains('_horz')) {
386
+ viewWidth = this.#viewElement.clientWidth/* instead of offsetHeight; safari reasons */;
387
+ left = viewWidth - this.#spacingElement.clientWidth;
388
+ } else {
389
+ viewHeight = this.#viewElement.clientHeight/* instead of offsetHeight; safari reasons */;
390
+ top = viewHeight - this.#spacingElement.clientHeight;
391
+ }
392
+ }
393
+ if (this.#viewElement.scrollTop < top || this.#viewElement.scrollLeft < left) {
394
+ this.#viewElement.scrollTo({ top, left });
395
+ }
396
+ });
397
+ }
398
+
399
+ this.#viewElement.style.setProperty('--header-box-height', this.#headerBoxElement.offsetHeight + 'px');
400
+ this.#viewElement.style.setProperty('--header-max-height', this.#headerElement.offsetHeight + 'px');
401
+ this.#viewElement.style.setProperty('--footer-max-height', this.#footerElement.offsetHeight + 'px');
402
+
403
+ if (this.classList.contains('_container')) return;
404
+ if (viewWidth === undefined) viewWidth = this.#viewElement.clientWidth;
405
+ if (viewHeight === undefined) viewHeight = this.#viewElement.clientHeight;
406
+
407
+ this.#viewElement.style.setProperty('--view-width', viewWidth + 'px');
408
+ this.#viewElement.style.setProperty('--view-height', viewHeight + 'px');
409
+ });
364
410
  }
365
411
 
366
412
  #unbindMinmaxWorker = null;
367
413
 
368
414
  bindMinmaxWorker() {
369
415
  const swipeDismiss = this.classList.contains('_swipe-dismiss');
370
- const minmaxEvents = this.classList.contains('_minmax');
416
+ const minmaxEvents = this.classList.contains('_minmax-events');
371
417
 
372
418
  if (!swipeDismiss && !minmaxEvents) return;
373
419
 
374
- const viewElement = this.shadowRoot.querySelector('.view');
375
- const sentinelElement = this.shadowRoot.querySelector('.sentinel');
376
- const spacingElement = viewElement.querySelector('.spacing');
377
-
378
420
  const options = {
379
- root: viewElement,
421
+ root: this.#viewElement,
380
422
  threshold: [0, 1]
381
423
  };
382
424
 
383
425
  const observer = new IntersectionObserver((entries) => {
426
+ if (!this.#userScrolled) return;
427
+
384
428
  for (const entry of entries) {
385
429
  // Minmax events
386
- if (entry.target === spacingElement) {
430
+ if (entry.target === this.#spacingElement) {
387
431
  const event = new ModalMinmaxEvent(1 - entry.intersectionRatio);
388
432
  this.dispatchEvent(event);
389
433
 
@@ -394,65 +438,80 @@ export class ModalElement extends HTMLElement {
394
438
  }
395
439
 
396
440
  // For auto-closing
397
- if (entry.target === sentinelElement && entry.isIntersecting) {
441
+ if (entry.target === this.#sentinelElement
442
+ && entry.isIntersecting
443
+ && entry.intersectionRatio >= 0.8) {
398
444
  this.hidePopover();
399
- setTimeout(() => spacingElement.scrollIntoView(), 300);
400
445
  }
401
446
  }
402
447
  }, options);
403
448
 
404
- if (minmaxEvents) observer.observe(spacingElement);
405
- if (swipeDismiss) observer.observe(sentinelElement);
406
- this.#unbindMinmaxWorker = () => observer.disconnect();
407
- }
408
-
409
- #onminmaxHandler = null;
449
+ setTimeout(() => {
450
+ if (minmaxEvents) observer.observe(this.#spacingElement);
451
+ if (swipeDismiss) observer.observe(this.#sentinelElement);
452
+ }, 200);
410
453
 
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;
454
+ this.#unbindMinmaxWorker = () => observer.disconnect();
421
455
  }
422
456
 
423
- get onminmax() { return this.#onminmaxHandler; }
457
+ #userScrolled = false;
458
+ #unbindDimensionsWorker;
424
459
 
425
- set type(value) {
426
- if ([undefined, null].includes(value)) {
427
- this.removeAttribute('type');
428
- } else this.setAttribute('type', value);
429
- }
460
+ #bindDimensionsWorker() {
461
+ this.#userScrolled = false;
462
+ const handleUserScroll = () => this.#userScrolled = true;
463
+ this.#viewElement.addEventListener('scroll', handleUserScroll);
430
464
 
431
- get type() { return this.getAttribute('type'); }
432
465
 
433
- get headerBoxHTML() { return ''; }
466
+ this.updateScrollViewDimensions();
467
+ const handleResize = () => this.updateScrollViewDimensions();
468
+ window.addEventListener('resize', handleResize);
434
469
 
435
- get headerHTML() { return ''; }
470
+ this.#unbindDimensionsWorker?.();
471
+ this.#unbindDimensionsWorker = () => {
472
+ window.removeEventListener('resize', handleResize);
473
+ this.#viewElement.removeEventListener('scroll', handleUserScroll);
474
+ };
475
+ }
436
476
 
437
- get mainHTML() { return ''; }
477
+ connectedCallback() {
478
+ if (!this.popover) {
479
+ this.popover = 'manual';
480
+ }
481
+ if (this.hasAttribute('open')) {
482
+ this.showPopover();
483
+ }
484
+ }
438
485
 
439
- get contentHTML() { return ''; }
486
+ disconnectedCallback() {
487
+ this.#unbindDimensionsWorker?.();
488
+ this.#unbindDimensionsWorker = null;
489
+ this.#unbindMinmaxWorker?.();
490
+ this.#unbindMinmaxWorker = null;
491
+ }
440
492
 
441
- get footerHTML() { return ''; }
493
+ static get observedAttributes() {
494
+ return ['class'];
495
+ }
442
496
 
443
- get css() { return ''; }
497
+ attributeChangedCallback(name, old, _new) {
498
+ if (name === 'class') this.#bindDimensionsWorker();
499
+ }
444
500
 
445
501
  constructor() {
446
502
  super();
447
503
  this.attachShadow({ mode: 'open' });
448
504
 
449
505
  this.addEventListener('toggle', (e) => {
450
- if (e.newState !== 'open') return;
451
- this.updateScrollViewDimensions();
452
- });
453
-
454
- window.addEventListener('resize', () => {
455
- this.updateScrollViewDimensions();
506
+ if (e.newState === 'open') {
507
+ this.#bindDimensionsWorker();
508
+ this.bindMinmaxWorker();
509
+ } else if (e.newState === 'closed') {
510
+ this.#unbindDimensionsWorker?.();
511
+ this.#unbindDimensionsWorker = null;
512
+ this.#unbindMinmaxWorker?.();
513
+ this.#unbindMinmaxWorker = null;
514
+ }
456
515
  });
457
516
 
458
517
  this.shadowRoot.innerHTML = `
@@ -489,27 +548,20 @@ export class ModalElement extends HTMLElement {
489
548
  </button>
490
549
  </div>
491
550
 
492
- <div class="scrollport-anchor">
493
- <div class="scrollport">
494
- <div class="scrollbar-track">
495
- <div class="scrollbar-thumb"></div>
496
- </div>
551
+ </header>
552
+
553
+ <div class="scrollport-anchor">
554
+ <div class="scrollport">
555
+ <div class="scrollbar-track">
556
+ <div class="scrollbar-thumb"></div>
497
557
  </div>
498
558
  </div>
499
- </header>
559
+ </div>
500
560
 
501
561
  ${this.mainHTML || `<div class="main" part="main">${this.contentHTML || `<slot></slot>`
502
562
  }</div>`}
503
563
 
504
564
  <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
565
  <div class="footer-bar" part="footer-bar">
514
566
  <slot
515
567
  name="footer"
@@ -586,7 +638,6 @@ export class ModalElement extends HTMLElement {
586
638
 
587
639
  --expanse-length: var(--modal-expanse-length, 0px);
588
640
  --minmax-length: var(--modal-minmax-length, 0px);
589
- --swipe-dismiss-length: var(--modal-swipe-dismiss-length, 0px);
590
641
 
591
642
  --scrollbar-thumb-color: var(--modal-scrollbar-thumb-color, black);
592
643
  --scrollbar-thumb-width: var(--modal-scrollbar-thumb-width, 4px);
@@ -603,6 +654,14 @@ export class ModalElement extends HTMLElement {
603
654
  --entry-transform: translateY(var(--translation));
604
655
  --exit-transform: translateY(calc(var(--translation) * var(--exit-factor)));
605
656
  }
657
+
658
+ :host(._swipe-dismiss) .view {
659
+ --swipe-dismiss-length: var(--modal-swipe-dismiss-length, calc(var(--view-height) - var(--minmax-length)));
660
+ }
661
+
662
+ :host(._horz._swipe-dismiss) .view {
663
+ --swipe-dismiss-length: var(--modal-swipe-dismiss-length, calc(var(--view-width) - var(--minmax-length)));
664
+ }
606
665
 
607
666
  /* transform reversal */
608
667
 
@@ -648,6 +707,11 @@ export class ModalElement extends HTMLElement {
648
707
  --view-width: 100cqw;
649
708
  }
650
709
 
710
+ :host(._container._horz) .view {
711
+ --view-height: 100cqh;
712
+ --view-width: calc(100cqw - var(--expanse-length));
713
+ }
714
+
651
715
  /* transform reversal */
652
716
 
653
717
  :host(:is(._top:not(._horz), ._left._horz)) .view {
@@ -724,9 +788,8 @@ export class ModalElement extends HTMLElement {
724
788
 
725
789
  /* flex orientation */
726
790
 
727
- :host(:popover-open),
791
+ :host,
728
792
  .view {
729
- display: flex;
730
793
  flex-direction: column;
731
794
  align-items: stretch;
732
795
  }
@@ -748,7 +811,8 @@ export class ModalElement extends HTMLElement {
748
811
  /* spacing */
749
812
 
750
813
  :host>.spacing,
751
- .view>.spacing {
814
+ .view>.spacing,
815
+ .view>.sentinel {
752
816
  position: relative;
753
817
  display: block;
754
818
  flex-shrink: 0;
@@ -769,17 +833,15 @@ export class ModalElement extends HTMLElement {
769
833
  :host(:not(._horz)) .view>.spacing { height: var(--minmax-length); }
770
834
  :host(._horz) .view>.spacing { width: var(--minmax-length); }
771
835
 
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); }
836
+ :host(:not(._horz)) .view>.sentinel { height: var(--swipe-dismiss-length); }
837
+ :host(._horz) .view>.sentinel { width: var(--swipe-dismiss-length); }
777
838
 
778
839
  /* ----------- */
779
840
 
780
841
  .view {
781
842
  position: relative;
782
843
  flex-grow: 1;
844
+ display: flex;
783
845
 
784
846
  pointer-events: none;
785
847
 
@@ -833,16 +895,13 @@ export class ModalElement extends HTMLElement {
833
895
  position: relative;
834
896
  flex-grow: 1;
835
897
 
836
- min-height: 100%;
837
- min-width: 100%;
838
-
839
898
  pointer-events: auto;
840
899
 
841
900
  display: flex;
842
901
  flex-direction: column;
843
902
  }
844
903
 
845
- :host(._swipe-dismiss) .container {
904
+ :host(._swipe-dismiss-fadeout) .container {
846
905
  animation-timing-function: linear;
847
906
  animation-fill-mode: both;
848
907
  animation-name: appear;
@@ -850,7 +909,7 @@ export class ModalElement extends HTMLElement {
850
909
  animation-range: 0 var(--swipe-dismiss-length);
851
910
  }
852
911
 
853
- :host(._swipe-dismiss:is(._top:not(._horz), ._left._horz)) .container {
912
+ :host(._swipe-dismiss-fadeout:is(._top:not(._horz), ._left._horz)) .container {
854
913
  animation-name: disappear;
855
914
  animation-range: calc(100% - var(--swipe-dismiss-length)) 100%;
856
915
  }
@@ -860,7 +919,7 @@ export class ModalElement extends HTMLElement {
860
919
  header {
861
920
  position: sticky;
862
921
  top: calc(var(--header-box-height) * -1);
863
- z-index: 1;
922
+ z-index: 2;
864
923
 
865
924
  display: flex;
866
925
  flex-direction: column;
@@ -870,6 +929,8 @@ export class ModalElement extends HTMLElement {
870
929
 
871
930
  border-top-left-radius: var(--radius-top-left);
872
931
  border-top-right-radius: var(--radius-top-right);
932
+
933
+ order: 1;
873
934
  }
874
935
 
875
936
  :host(:not(._horz)) header {
@@ -962,6 +1023,8 @@ export class ModalElement extends HTMLElement {
962
1023
 
963
1024
  color: var(--footer-color-default);
964
1025
  background: var(--footer-background);
1026
+
1027
+ order: 5;
965
1028
  }
966
1029
 
967
1030
  :host([type="info"]) footer {
@@ -986,15 +1049,16 @@ export class ModalElement extends HTMLElement {
986
1049
 
987
1050
  /* ----------- */
988
1051
 
989
- :host(:popover-open) .view {
1052
+ .view {
990
1053
  scroll-snap-type: y mandatory;
991
1054
  }
992
1055
 
993
- :host(._horz:popover-open) .view {
1056
+ :host(._horz) .view {
994
1057
  scroll-snap-type: x mandatory;
995
1058
  }
996
1059
 
997
- .view>.spacing {
1060
+ .view>.spacing,
1061
+ .view>.sentinel {
998
1062
  scroll-snap-align: var(--scroll-snap-start);
999
1063
  }
1000
1064
 
@@ -1003,6 +1067,8 @@ export class ModalElement extends HTMLElement {
1003
1067
  scroll-margin-top: var(--header-min-height);
1004
1068
  scroll-margin-bottom: var(--footer-min-height);
1005
1069
  scroll-snap-align: var(--scroll-snap-start);
1070
+
1071
+ order: 3;
1006
1072
  }
1007
1073
 
1008
1074
  :host(:is(._top, ._left._horz)) .main {
@@ -1024,42 +1090,108 @@ export class ModalElement extends HTMLElement {
1024
1090
  /* ----------- */
1025
1091
 
1026
1092
  .scrollport-anchor {
1027
- position: relative;
1093
+ order: 2;
1094
+
1095
+ position: sticky;
1096
+ top: var(--header-min-height);
1097
+ bottom: var(--footer-min-height);
1098
+ left: 0;
1099
+ right: 0;
1100
+ display: flex;
1101
+ flex-direction: column;
1102
+
1028
1103
  height: 0;
1104
+ width: var(--view-width);
1029
1105
  }
1030
1106
 
1031
- :host(:not(._top:not(._horz))) footer .scrollport-anchor,
1032
- :host(._top:not(._horz)) header .scrollport-anchor {
1033
- display: none;
1107
+ :host(:is(._left._horz, ._top:not(._horz))) .scrollport-anchor {
1108
+ justify-content: end;
1109
+ order: 4;
1034
1110
  }
1035
1111
 
1036
1112
  .scrollport {
1037
- position: sticky;
1038
- top: var(--header-min-height);
1039
- left: 0;
1040
- right: 0;
1041
-
1042
- container-type: size;
1113
+ position: relative;
1114
+
1043
1115
  height: var(--view-inner-height);
1044
1116
  width: var(--view-width);
1117
+ flex-shrink: 0;
1045
1118
 
1046
1119
  pointer-events: none;
1047
1120
  }
1048
1121
 
1049
- footer .scrollport {
1122
+ :host(._top:not(._horz)) .scrollport {
1123
+ height: calc(var(--view-inner-height) - var(--header-box-height));
1124
+ }
1125
+
1126
+ /* -- scroll unfold -- */
1127
+
1128
+ :host(._scroll-unfold) .scrollport {
1129
+ display: flex;
1130
+ flex-direction: column;
1131
+ justify-content: space-between;
1132
+ align-items: stretch;
1133
+ }
1134
+
1135
+ :host(._scroll-unfold._horz) .scrollport {
1136
+ flex-direction: row;
1137
+ }
1138
+
1139
+ :host(._scroll-unfold) .scrollport::before,
1140
+ :host(._scroll-unfold) .scrollport::after {
1141
+ position: sticky;
1142
+ display: block;
1143
+ content: "";
1144
+ opacity: 0;
1145
+
1146
+ background: var(--background);
1147
+
1148
+ mask-repeat: no-repeat;
1149
+ mask-size: 100% 100%;
1150
+
1151
+ animation-timing-function: linear;
1152
+ animation-fill-mode: forwards;
1153
+ animation-name: appear;
1154
+ animation-timeline: --view-scroll;
1155
+
1156
+ animation-range: var(--scrollbar-progress-range);
1157
+ }
1158
+
1159
+ :host(._scroll-unfold:not(._horz)) .scrollport::before,
1160
+ :host(._scroll-unfold:not(._horz)) .scrollport::after {
1161
+ top: var(--header-min-height);
1162
+ height: 25%;
1163
+
1164
+ mask-image: linear-gradient(to bottom, black 0%, transparent 100%);
1165
+ -webkit-mask-image: linear-gradient(to bottom, black 0%, transparent 100%);
1166
+ }
1167
+
1168
+ :host(._scroll-unfold:not(._horz)) .scrollport::after {
1169
+ bottom: var(--footer-min-height);
1050
1170
  top: auto;
1051
- position: absolute;
1052
- bottom: 0;
1171
+ opacity: 1;
1172
+ animation-name: disappear;
1173
+ transform: scaleY(-1);
1053
1174
  }
1054
1175
 
1055
- :host(._scrollbars._top:not(._horz)) .scrollport {
1056
- height: calc(var(--view-inner-height) - var(--header-box-height));
1176
+ :host(._scroll-unfold._horz) .scrollport::before,
1177
+ :host(._scroll-unfold._horz) .scrollport::after {
1178
+ left: 0;
1179
+ width: 25%;
1180
+
1181
+ mask-image: linear-gradient(to right, black 0%, transparent 100%);
1182
+ -webkit-mask-image: linear-gradient(to right, black 0%, transparent 100%);
1057
1183
  }
1058
1184
 
1059
- :host(._scrollbars._left._horz) .scrollport {
1060
- width: calc(var(--view-width) - var(--minmax-length));
1185
+ :host(._scroll-unfold._horz) .scrollport::after {
1186
+ right: 0;
1187
+ left: auto;
1188
+ opacity: 1;
1189
+ animation-name: disappear;
1190
+ transform: scaleX(-1);
1061
1191
  }
1062
1192
 
1193
+ /* -- scrollbar -- */
1194
+
1063
1195
  :host(._scrollbars) .scrollbar-track {
1064
1196
  position: absolute;
1065
1197
  display: block;
@@ -1070,6 +1202,8 @@ export class ModalElement extends HTMLElement {
1070
1202
  right: 0;
1071
1203
  padding: 6px;
1072
1204
 
1205
+ container-type: size;
1206
+
1073
1207
  opacity: 0;
1074
1208
 
1075
1209
  animation: appear linear;
@@ -1104,6 +1238,7 @@ export class ModalElement extends HTMLElement {
1104
1238
  :host(._scrollbars._horz) .scrollbar-thumb {
1105
1239
  height: var(--scrollbar-thumb-width);
1106
1240
  width: var(--scrollbar-thumb-height);
1241
+
1107
1242
  --scrollbar-thumb-start: translateX(0);
1108
1243
  --scrollbar-thumb-length: translateX(calc(100cqw - 100%));
1109
1244
  }
@@ -1185,7 +1320,7 @@ export class ModalElement extends HTMLElement {
1185
1320
 
1186
1321
  .main {
1187
1322
  color: var(--color-default);
1188
- background-color: var(--background);
1323
+ background: var(--background);
1189
1324
  }
1190
1325
 
1191
1326
  .view:not(:has(footer slot:is(.has-slotted, :not(:empty)))) .main {
@@ -1227,12 +1362,19 @@ export class ModalElement extends HTMLElement {
1227
1362
  ${this.css}
1228
1363
  </style>
1229
1364
  `;
1365
+
1366
+ this.#viewElement = this.shadowRoot.querySelector('.view');
1367
+ this.#sentinelElement = this.#viewElement.querySelector('.sentinel');
1368
+ this.#spacingElement = this.#viewElement.querySelector('.spacing');
1369
+ this.#headerElement = this.#viewElement.querySelector('header');
1370
+ this.#headerBoxElement = this.#viewElement.querySelector('.header-box');
1371
+ this.#footerElement = this.#viewElement.querySelector('footer');
1230
1372
  }
1231
1373
  }
1232
1374
 
1233
1375
  // ---------------- DialogElement
1234
1376
 
1235
- export class DialogResponseEvent extends Event {
1377
+ class DialogResponseEvent extends Event {
1236
1378
 
1237
1379
  #data;
1238
1380
  get data() { return this.#data; }
@@ -1243,7 +1385,7 @@ export class DialogResponseEvent extends Event {
1243
1385
  }
1244
1386
  }
1245
1387
 
1246
- export class DialogElement extends ModalElement {
1388
+ class DialogElement extends ModalElement {
1247
1389
 
1248
1390
  constructor() {
1249
1391
  super();
@@ -1336,6 +1478,7 @@ export class DialogElement extends ModalElement {
1336
1478
  }
1337
1479
 
1338
1480
  .main {
1481
+ flex-shrink: 0;
1339
1482
  display: flex;
1340
1483
  flex-direction: column;
1341
1484
  gap: 1rem;
@@ -1394,7 +1537,7 @@ export class DialogElement extends ModalElement {
1394
1537
 
1395
1538
  // ---------------- PromptElement
1396
1539
 
1397
- export class PromptElement extends DialogElement {
1540
+ class PromptElement extends DialogElement {
1398
1541
 
1399
1542
  static get observedAttributes() {
1400
1543
  return ['value', 'placeholder'].concat(super.observedAttributes || []);
@@ -1403,8 +1546,8 @@ export class PromptElement extends DialogElement {
1403
1546
  attributeChangedCallback(name, old, _new) {
1404
1547
  super.attributeChangedCallback?.(...arguments);
1405
1548
  const input = this.shadowRoot.querySelector('input');
1406
- if (name === 'value') { input.value = _new; }
1407
- if (name === 'placeholder') { input.placeholder = _new; }
1549
+ if (name === 'value') input.value = _new;
1550
+ if (name === 'placeholder') input.placeholder = _new;
1408
1551
  }
1409
1552
 
1410
1553
  set placeholder(value) {
@@ -1473,7 +1616,7 @@ export class PromptElement extends DialogElement {
1473
1616
 
1474
1617
  // ---------------- ConfirmElement
1475
1618
 
1476
- export class ConfirmElement extends DialogElement {
1619
+ class ConfirmElement extends DialogElement {
1477
1620
  get actionTexts() { return ['No', 'Yes']; }
1478
1621
 
1479
1622
  respondWith(response) { super.respondWith(!!response); }
@@ -1483,7 +1626,7 @@ export class ConfirmElement extends DialogElement {
1483
1626
 
1484
1627
  // ---------------- AlertElement
1485
1628
 
1486
- export class AlertElement extends DialogElement {
1629
+ class AlertElement extends DialogElement {
1487
1630
  get actionTexts() { return ['', 'Got it']; }
1488
1631
  }
1489
1632