@webqit/webflo 0.20.31 → 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.31",
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 beaderBoxElement = this.shadowRoot.querySelector('.header-box');
334
- const beaderBarElement = this.shadowRoot.querySelector('.header-bar');
335
- const footerBarElement = this.shadowRoot.querySelector('.footer-bar');
336
- requestAnimationFrame(() => {
337
- viewElement.style.setProperty('--header-box-height', beaderBoxElement.offsetHeight + 'px');
338
- viewElement.style.setProperty('--header-bar-height', beaderBarElement.offsetHeight + 'px');
339
- viewElement.style.setProperty('--footer-bar-height', footerBarElement.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 = `
@@ -480,7 +539,7 @@ export class ModalElement extends HTMLElement {
480
539
  <div class="_content" style="flex-grow: 1">
481
540
  <slot
482
541
  name="header"
483
- onslotchange="this.closest('.view').style.setProperty('--header-bar-height', this.closest('.header-bar').offsetHeight + 'px');"
542
+ onslotchange="this.closest('.view').style.setProperty('--header-max-height', this.closest('header').offsetHeight + 'px');"
484
543
  >${this.headerHTML}</slot>
485
544
  </div>
486
545
  </div>
@@ -489,31 +548,24 @@ 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"
516
- onslotchange="this.classList.toggle('has-slotted', !!this.assignedElements().length); this.closest('.view').style.setProperty('--footer-bar-height', this.closest('.footer-bar').offsetHeight + 'px');"
568
+ onslotchange="this.classList.toggle('has-slotted', !!this.assignedElements().length); this.closest('.view').style.setProperty('--footer-max-height', this.closest('footer').offsetHeight + 'px');"
517
569
  >${this.footerHTML}</slot>
518
570
  </div>
519
571
  </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
 
@@ -620,11 +679,14 @@ export class ModalElement extends HTMLElement {
620
679
  /* -------- internal, dynamic props (view) -------- */
621
680
 
622
681
  .view {
682
+ --header-max-height: 1.6rem;
623
683
  --header-box-height: 0px;
624
- --header-bar-height: 1.6rem;
625
- --footer-bar-height: 0px;
684
+ --footer-max-height: 0px;
626
685
 
627
- --view-inner-height: calc(var(--view-height) - var(--header-bar-height) - var(--footer-bar-height));
686
+ --header-min-height: calc(var(--header-max-height) - var(--header-box-height));
687
+ --footer-min-height: var(--footer-max-height);
688
+
689
+ --view-inner-height: calc(var(--view-height) - var(--header-min-height) - var(--footer-min-height));
628
690
  --total-minmax-length: calc(var(--minmax-length) + var(--swipe-dismiss-length));
629
691
 
630
692
  --y-scroll-effect-exclude: var(--total-minmax-length);
@@ -645,6 +707,11 @@ export class ModalElement extends HTMLElement {
645
707
  --view-width: 100cqw;
646
708
  }
647
709
 
710
+ :host(._container._horz) .view {
711
+ --view-height: 100cqh;
712
+ --view-width: calc(100cqw - var(--expanse-length));
713
+ }
714
+
648
715
  /* transform reversal */
649
716
 
650
717
  :host(:is(._top:not(._horz), ._left._horz)) .view {
@@ -721,9 +788,8 @@ export class ModalElement extends HTMLElement {
721
788
 
722
789
  /* flex orientation */
723
790
 
724
- :host(:popover-open),
791
+ :host,
725
792
  .view {
726
- display: flex;
727
793
  flex-direction: column;
728
794
  align-items: stretch;
729
795
  }
@@ -745,7 +811,8 @@ export class ModalElement extends HTMLElement {
745
811
  /* spacing */
746
812
 
747
813
  :host>.spacing,
748
- .view>.spacing {
814
+ .view>.spacing,
815
+ .view>.sentinel {
749
816
  position: relative;
750
817
  display: block;
751
818
  flex-shrink: 0;
@@ -766,17 +833,15 @@ export class ModalElement extends HTMLElement {
766
833
  :host(:not(._horz)) .view>.spacing { height: var(--minmax-length); }
767
834
  :host(._horz) .view>.spacing { width: var(--minmax-length); }
768
835
 
769
- :host(:not(._top, ._horz)) .view>.spacing { margin-top: var(--swipe-dismiss-length); }
770
- :host(._top:not(._horz)) .view>.spacing { margin-bottom: var(--swipe-dismiss-length); }
771
-
772
- :host(._horz:not(._left)) .view>.spacing { margin-left: var(--swipe-dismiss-length); }
773
- :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); }
774
838
 
775
839
  /* ----------- */
776
840
 
777
841
  .view {
778
842
  position: relative;
779
843
  flex-grow: 1;
844
+ display: flex;
780
845
 
781
846
  pointer-events: none;
782
847
 
@@ -830,16 +895,13 @@ export class ModalElement extends HTMLElement {
830
895
  position: relative;
831
896
  flex-grow: 1;
832
897
 
833
- min-height: 100%;
834
- min-width: 100%;
835
-
836
898
  pointer-events: auto;
837
899
 
838
900
  display: flex;
839
901
  flex-direction: column;
840
902
  }
841
903
 
842
- :host(._swipe-dismiss) .container {
904
+ :host(._swipe-dismiss-fadeout) .container {
843
905
  animation-timing-function: linear;
844
906
  animation-fill-mode: both;
845
907
  animation-name: appear;
@@ -847,7 +909,7 @@ export class ModalElement extends HTMLElement {
847
909
  animation-range: 0 var(--swipe-dismiss-length);
848
910
  }
849
911
 
850
- :host(._swipe-dismiss:is(._top:not(._horz), ._left._horz)) .container {
912
+ :host(._swipe-dismiss-fadeout:is(._top:not(._horz), ._left._horz)) .container {
851
913
  animation-name: disappear;
852
914
  animation-range: calc(100% - var(--swipe-dismiss-length)) 100%;
853
915
  }
@@ -857,7 +919,7 @@ export class ModalElement extends HTMLElement {
857
919
  header {
858
920
  position: sticky;
859
921
  top: calc(var(--header-box-height) * -1);
860
- z-index: 1;
922
+ z-index: 2;
861
923
 
862
924
  display: flex;
863
925
  flex-direction: column;
@@ -867,6 +929,8 @@ export class ModalElement extends HTMLElement {
867
929
 
868
930
  border-top-left-radius: var(--radius-top-left);
869
931
  border-top-right-radius: var(--radius-top-right);
932
+
933
+ order: 1;
870
934
  }
871
935
 
872
936
  :host(:not(._horz)) header {
@@ -959,6 +1023,8 @@ export class ModalElement extends HTMLElement {
959
1023
 
960
1024
  color: var(--footer-color-default);
961
1025
  background: var(--footer-background);
1026
+
1027
+ order: 5;
962
1028
  }
963
1029
 
964
1030
  :host([type="info"]) footer {
@@ -983,23 +1049,26 @@ export class ModalElement extends HTMLElement {
983
1049
 
984
1050
  /* ----------- */
985
1051
 
986
- :host(:popover-open) .view {
1052
+ .view {
987
1053
  scroll-snap-type: y mandatory;
988
1054
  }
989
1055
 
990
- :host(._horz:popover-open) .view {
1056
+ :host(._horz) .view {
991
1057
  scroll-snap-type: x mandatory;
992
1058
  }
993
1059
 
994
- .view>.spacing {
1060
+ .view>.spacing,
1061
+ .view>.sentinel {
995
1062
  scroll-snap-align: var(--scroll-snap-start);
996
1063
  }
997
1064
 
998
1065
  .main {
999
1066
  flex-grow: 1;
1000
- scroll-margin-top: var(--header-bar-height);
1001
- scroll-margin-bottom: var(--footer-bar-height);
1067
+ scroll-margin-top: var(--header-min-height);
1068
+ scroll-margin-bottom: var(--footer-min-height);
1002
1069
  scroll-snap-align: var(--scroll-snap-start);
1070
+
1071
+ order: 3;
1003
1072
  }
1004
1073
 
1005
1074
  :host(:is(._top, ._left._horz)) .main {
@@ -1021,42 +1090,108 @@ export class ModalElement extends HTMLElement {
1021
1090
  /* ----------- */
1022
1091
 
1023
1092
  .scrollport-anchor {
1024
- 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
+
1025
1103
  height: 0;
1104
+ width: var(--view-width);
1026
1105
  }
1027
1106
 
1028
- :host(:not(._top:not(._horz))) footer .scrollport-anchor,
1029
- :host(._top:not(._horz)) header .scrollport-anchor {
1030
- display: none;
1107
+ :host(:is(._left._horz, ._top:not(._horz))) .scrollport-anchor {
1108
+ justify-content: end;
1109
+ order: 4;
1031
1110
  }
1032
1111
 
1033
1112
  .scrollport {
1034
- position: sticky;
1035
- top: var(--header-bar-height);
1036
- left: 0;
1037
- right: 0;
1038
-
1039
- container-type: size;
1113
+ position: relative;
1114
+
1040
1115
  height: var(--view-inner-height);
1041
1116
  width: var(--view-width);
1117
+ flex-shrink: 0;
1042
1118
 
1043
1119
  pointer-events: none;
1044
1120
  }
1045
1121
 
1046
- 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);
1047
1170
  top: auto;
1048
- position: absolute;
1049
- bottom: 0;
1171
+ opacity: 1;
1172
+ animation-name: disappear;
1173
+ transform: scaleY(-1);
1050
1174
  }
1051
1175
 
1052
- :host(._scrollbars._top:not(._horz)) .scrollport {
1053
- 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%);
1054
1183
  }
1055
1184
 
1056
- :host(._scrollbars._left._horz) .scrollport {
1057
- 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);
1058
1191
  }
1059
1192
 
1193
+ /* -- scrollbar -- */
1194
+
1060
1195
  :host(._scrollbars) .scrollbar-track {
1061
1196
  position: absolute;
1062
1197
  display: block;
@@ -1067,6 +1202,8 @@ export class ModalElement extends HTMLElement {
1067
1202
  right: 0;
1068
1203
  padding: 6px;
1069
1204
 
1205
+ container-type: size;
1206
+
1070
1207
  opacity: 0;
1071
1208
 
1072
1209
  animation: appear linear;
@@ -1101,6 +1238,7 @@ export class ModalElement extends HTMLElement {
1101
1238
  :host(._scrollbars._horz) .scrollbar-thumb {
1102
1239
  height: var(--scrollbar-thumb-width);
1103
1240
  width: var(--scrollbar-thumb-height);
1241
+
1104
1242
  --scrollbar-thumb-start: translateX(0);
1105
1243
  --scrollbar-thumb-length: translateX(calc(100cqw - 100%));
1106
1244
  }
@@ -1182,7 +1320,7 @@ export class ModalElement extends HTMLElement {
1182
1320
 
1183
1321
  .main {
1184
1322
  color: var(--color-default);
1185
- background-color: var(--background);
1323
+ background: var(--background);
1186
1324
  }
1187
1325
 
1188
1326
  .view:not(:has(footer slot:is(.has-slotted, :not(:empty)))) .main {
@@ -1224,12 +1362,19 @@ export class ModalElement extends HTMLElement {
1224
1362
  ${this.css}
1225
1363
  </style>
1226
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');
1227
1372
  }
1228
1373
  }
1229
1374
 
1230
1375
  // ---------------- DialogElement
1231
1376
 
1232
- export class DialogResponseEvent extends Event {
1377
+ class DialogResponseEvent extends Event {
1233
1378
 
1234
1379
  #data;
1235
1380
  get data() { return this.#data; }
@@ -1240,7 +1385,7 @@ export class DialogResponseEvent extends Event {
1240
1385
  }
1241
1386
  }
1242
1387
 
1243
- export class DialogElement extends ModalElement {
1388
+ class DialogElement extends ModalElement {
1244
1389
 
1245
1390
  constructor() {
1246
1391
  super();
@@ -1333,6 +1478,7 @@ export class DialogElement extends ModalElement {
1333
1478
  }
1334
1479
 
1335
1480
  .main {
1481
+ flex-shrink: 0;
1336
1482
  display: flex;
1337
1483
  flex-direction: column;
1338
1484
  gap: 1rem;
@@ -1391,7 +1537,7 @@ export class DialogElement extends ModalElement {
1391
1537
 
1392
1538
  // ---------------- PromptElement
1393
1539
 
1394
- export class PromptElement extends DialogElement {
1540
+ class PromptElement extends DialogElement {
1395
1541
 
1396
1542
  static get observedAttributes() {
1397
1543
  return ['value', 'placeholder'].concat(super.observedAttributes || []);
@@ -1400,8 +1546,8 @@ export class PromptElement extends DialogElement {
1400
1546
  attributeChangedCallback(name, old, _new) {
1401
1547
  super.attributeChangedCallback?.(...arguments);
1402
1548
  const input = this.shadowRoot.querySelector('input');
1403
- if (name === 'value') { input.value = _new; }
1404
- if (name === 'placeholder') { input.placeholder = _new; }
1549
+ if (name === 'value') input.value = _new;
1550
+ if (name === 'placeholder') input.placeholder = _new;
1405
1551
  }
1406
1552
 
1407
1553
  set placeholder(value) {
@@ -1470,7 +1616,7 @@ export class PromptElement extends DialogElement {
1470
1616
 
1471
1617
  // ---------------- ConfirmElement
1472
1618
 
1473
- export class ConfirmElement extends DialogElement {
1619
+ class ConfirmElement extends DialogElement {
1474
1620
  get actionTexts() { return ['No', 'Yes']; }
1475
1621
 
1476
1622
  respondWith(response) { super.respondWith(!!response); }
@@ -1480,7 +1626,7 @@ export class ConfirmElement extends DialogElement {
1480
1626
 
1481
1627
  // ---------------- AlertElement
1482
1628
 
1483
- export class AlertElement extends DialogElement {
1629
+ class AlertElement extends DialogElement {
1484
1630
  get actionTexts() { return ['', 'Got it']; }
1485
1631
  }
1486
1632