@webqit/webflo 0.20.35 → 0.20.37

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.35",
15
+ "version": "0.20.37",
16
16
  "license": "MIT",
17
17
  "repository": {
18
18
  "type": "git",
@@ -0,0 +1,173 @@
1
+ class DeviceViewport {
2
+
3
+ #stack = [];
4
+ #ownedElements = new Set(); // Stores elements created by this class
5
+ #elements = {}; // Map of active elements/selectors
6
+ #timer = null; // For commit batching
7
+
8
+ #specials = {
9
+ themeColor: { name: 'theme-color', type: 'meta' },
10
+ appleStatusBarStyle: { name: 'apple-mobile-web-app-status-bar-style', type: 'meta' },
11
+ colorScheme: { name: 'color-scheme', type: 'meta' },
12
+ manifest: { name: 'manifest', type: 'link' }
13
+ };
14
+
15
+ constructor() {
16
+ const initialState = { _isInitial: true };
17
+
18
+ // 1. Ingest Viewport
19
+ const vMeta = document.querySelector('meta[name="viewport"]');
20
+ if (vMeta) {
21
+ this.#elements.viewport = vMeta;
22
+ Object.assign(initialState, this.#parseViewport(vMeta.content));
23
+ }
24
+
25
+ // 2. Ingest Title & Specials
26
+ initialState.title = document.title;
27
+ Object.entries(this.#specials).forEach(([jsKey, config]) => {
28
+ const el = this.#findDom(config);
29
+ if (el) {
30
+ this.#elements[jsKey] = el;
31
+ initialState[jsKey] = config.type === 'link' ? el.getAttribute('href') : el.getAttribute('content');
32
+ }
33
+ });
34
+
35
+ this.#stack.push(initialState);
36
+ }
37
+
38
+ #findDom({ name, type }) {
39
+ return type === 'link'
40
+ ? document.querySelector(`link[rel="${name}"]`)
41
+ : document.querySelector(`meta[name="${name}"]`);
42
+ }
43
+
44
+ #getOrCreate(jsKey, media = null) {
45
+ const cacheKey = media ? `${jsKey}-${media}` : jsKey;
46
+ if (this.#elements[cacheKey]) return this.#elements[cacheKey];
47
+
48
+ const config = this.#specials[jsKey] || { name: 'viewport', type: 'meta' };
49
+ const el = document.createElement(config.type);
50
+
51
+ if (config.type === 'link') el.rel = config.name;
52
+ else el.name = config.name;
53
+ if (media) el.setAttribute('media', media);
54
+
55
+ document.head.appendChild(el);
56
+ this.#ownedElements.add(el);
57
+ this.#elements[cacheKey] = el;
58
+ return el;
59
+ }
60
+
61
+ #scheduleRender() {
62
+ if (this.#timer) return;
63
+ this.#timer = requestAnimationFrame(() => {
64
+ this.#render();
65
+ this.#timer = null;
66
+ });
67
+ }
68
+
69
+ #render() {
70
+ const state = this.peek();
71
+ const viewportDirectives = [];
72
+ const activeKeys = new Set(Object.keys(state).filter(k => !k.startsWith('_')));
73
+
74
+ // 1. Handle Title
75
+ if ('title' in state) {
76
+ document.title = state.title || '';
77
+ activeKeys.delete('title');
78
+ }
79
+
80
+ // 2. Handle Specials (with Media Query Support)
81
+ Object.keys(this.#specials).forEach(jsKey => {
82
+ const val = state[jsKey]; // Can be string or { default, dark, light, "(media)": color }
83
+ activeKeys.delete(jsKey);
84
+
85
+ const seen = new Set();
86
+
87
+ if (val && typeof val === 'object' && jsKey === 'themeColor') {
88
+ // Media Query Logic
89
+ Object.entries(val).forEach(([query, color]) => {
90
+ const mediaStr = query === 'dark' ? '(prefers-color-scheme: dark)' :
91
+ query === 'light' ? '(prefers-color-scheme: light)' :
92
+ query === 'default' ? '' : query;
93
+ seen.add(mediaStr);
94
+ this.#setAttr(jsKey, color, mediaStr);
95
+ });
96
+ } else {
97
+ seen.add('');
98
+ this.#setAttr(jsKey, val);
99
+ }
100
+
101
+ // cleanup
102
+ Object.keys(this.#elements)
103
+ .filter(k => k === jsKey || k.startsWith(`${jsKey}-`))
104
+ .forEach(k => {
105
+ const keyId = k === jsKey ? '' : k.slice(jsKey.length + 1);
106
+ if (!seen.has(keyId)) {
107
+ this.#setAttr(jsKey, null, keyId);
108
+ }
109
+ });
110
+ });
111
+
112
+ // 3. Handle Viewport
113
+ activeKeys.forEach(key => {
114
+ const val = state[key];
115
+ if (val === null) return;
116
+ const kebab = key.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
117
+ viewportDirectives.push(val === true ? kebab : `${kebab}=${val}`);
118
+ });
119
+
120
+ const vContent = viewportDirectives.join(', ');
121
+ const vEl = this.#elements.viewport || (vContent ? this.#getOrCreate('viewport') : null);
122
+ if (vEl) {
123
+ vEl.setAttribute('content', vContent);
124
+ if (!vContent && this.#ownedElements.has(vEl)) {
125
+ vEl.remove();
126
+ delete this.#elements.viewport;
127
+ }
128
+ }
129
+ }
130
+
131
+ #setAttr(jsKey, val, media = null) {
132
+ const cacheKey = media ? `${jsKey}-${media}` : jsKey;
133
+ const config = this.#specials[jsKey];
134
+
135
+ if (val !== undefined && val !== null) {
136
+ const el = this.#getOrCreate(jsKey, media);
137
+ el.setAttribute(config.type === 'link' ? 'href' : 'content', val);
138
+ } else {
139
+ const el = this.#elements[cacheKey];
140
+ if (el) {
141
+ if (this.#ownedElements.has(el)) {
142
+ el.remove();
143
+ delete this.#elements[cacheKey];
144
+ } else {
145
+ el.setAttribute(config.type === 'link' ? 'href' : 'content', '');
146
+ }
147
+ }
148
+ }
149
+ }
150
+
151
+ #parseViewport = (c) => Object.fromEntries(c.split(',').filter(Boolean).map(s => {
152
+ const [k, v] = s.split('=').map(p => p.trim());
153
+ return [k.replace(/-([a-z])/g, g => g.toUpperCase()), v || true];
154
+ }));
155
+
156
+ push(id, config) {
157
+ if (!id) throw new Error("push() requires a unique ID");
158
+ if (this.#stack.some(e => e.id === id)) return;
159
+ this.#stack.push({ ...this.peek(), ...config, id, _isInitial: false });
160
+ this.#scheduleRender();
161
+ }
162
+
163
+ pop(id) {
164
+ if (!id) throw new Error("pop() requires a target ID");
165
+ const idx = this.#stack.findIndex(e => e.id === id);
166
+ if (idx > 0) { // Never pop the initial state at index 0
167
+ this.#stack.splice(idx, 1);
168
+ this.#scheduleRender();
169
+ }
170
+ }
171
+
172
+ peek() { return this.#stack[this.#stack.length - 1]; }
173
+ }
@@ -35,9 +35,6 @@ 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
-
41
38
  get isClientSide() { return true; }
42
39
 
43
40
  constructor(bootstrap, host) {
@@ -59,68 +56,6 @@ export class WebfloClient extends AppRuntime {
59
56
  phase: 0
60
57
  };
61
58
  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
- };
124
59
  }
125
60
 
126
61
  async initialize() {
@@ -3,6 +3,7 @@ import { LiveResponse } from '@webqit/fetch-plus';
3
3
  import { HttpEvent111 } from '../webflo-routing/HttpEvent111.js';
4
4
  import { ClientSideWorkport } from './ClientSideWorkport.js';
5
5
  import { DeviceCapabilities } from './DeviceCapabilities.js';
6
+ import { DeviceViewport } from './DeviceViewport.js';
6
7
  import { WebfloClient } from './WebfloClient.js';
7
8
  import { WebfloHMR } from './webflo-devmode.js';
8
9
 
@@ -12,6 +13,8 @@ export class WebfloRootClientA extends WebfloClient {
12
13
 
13
14
  static get DeviceCapabilities() { return DeviceCapabilities; }
14
15
 
16
+ static get DeviceViewport() { return DeviceViewport; }
17
+
15
18
  static create(bootstrap, host) {
16
19
  return new this(bootstrap, host);
17
20
  }
@@ -19,12 +22,15 @@ export class WebfloRootClientA extends WebfloClient {
19
22
  #network;
20
23
  get network() { return this.#network; }
21
24
 
22
- #workport;
23
- get workport() { return this.#workport; }
25
+ #viewport;
26
+ get viewport() { return this.#viewport; }
24
27
 
25
28
  #capabilities;
26
29
  get capabilities() { return this.#capabilities; }
27
30
 
31
+ #workport;
32
+ get workport() { return this.#workport; }
33
+
28
34
  #hmr;
29
35
 
30
36
  get withViewTransitions() {
@@ -67,10 +73,14 @@ export class WebfloRootClientA extends WebfloClient {
67
73
 
68
74
  async setupCapabilities() {
69
75
  const instanceController = await super.setupCapabilities();
70
- const cleanups = [];
71
76
 
72
- // Service Worker && Capabilities
77
+ const cleanups = [];
73
78
  instanceController.signal.addEventListener('abort', () => cleanups.forEach((c) => c()), { once: true });
79
+
80
+ // DeviceViewport, DeviceCapabilities, & Service Worker
81
+
82
+ this.#viewport = new this.constructor.DeviceViewport();
83
+
74
84
  this.#capabilities = await this.constructor.DeviceCapabilities.initialize(this, this.config.CLIENT.capabilities);
75
85
  cleanups.push(() => this.#capabilities.close());
76
86
 
@@ -272,30 +272,31 @@ export class ToastElement extends BaseElement {
272
272
 
273
273
  /* ----------- */
274
274
 
275
- :host(:not([popover="manual"]):popover-open)::backdrop {
275
+ :host(:not([popover="manual"], ._manual-dismiss):popover-open)::backdrop {
276
276
  animation: flash 0.3s ease-in;
277
277
  animation-iteration-count: 3;
278
278
  }
279
279
 
280
- :host([popover="manual"])::backdrop {
280
+ :host(:is([popover="manual"], ._manual-dismiss))::backdrop {
281
281
  /* Transition */
282
282
  transition:
283
283
  display 0.2s allow-discrete,
284
284
  overlay 0.2s allow-discrete,
285
- backdrop-filter 0.2s;
285
+ backdrop-filter 0.2s,
286
+ background 0.2s;
286
287
  }
287
288
 
288
- :host([popover="manual"]:popover-open)::backdrop {
289
+ :host(:is([popover="manual"], ._manual-dismiss):popover-open)::backdrop {
289
290
  backdrop-filter: blur(3px);
290
291
  }
291
292
 
292
293
  @starting-style {
293
- :host([popover="manual"]:popover-open)::backdrop {
294
+ :host(:is([popover="manual"], ._manual-dismiss):popover-open)::backdrop {
294
295
  backdrop-filter: none;
295
296
  }
296
297
  }
297
298
 
298
- :host([popover="manual"]:popover-open)::before {
299
+ :host(:is([popover="manual"], ._manual-dismiss):popover-open)::before {
299
300
  position: fixed;
300
301
  inset: 0;
301
302
  display: block;
@@ -345,7 +346,7 @@ export class ToastElement extends BaseElement {
345
346
  transform: translateX(0.1rem);
346
347
  }
347
348
 
348
- :host(:not([popover="manual"])) .close-button {
349
+ :host(:not([popover="manual"], ._manual-dismiss)) .close-button {
349
350
  display: none;
350
351
  }
351
352
 
@@ -579,6 +580,11 @@ export class ModalElement extends BaseElement {
579
580
  if (e.newState === 'open') {
580
581
  this.#bindDimensionsWorker();
581
582
  this.bindMinmaxWorker();
583
+
584
+ if (!this.delegatesFocus
585
+ && !this.querySelector('[autofocus]')) {
586
+ this.shadowRoot.querySelector('[autofocus]')?.focus();
587
+ }
582
588
  } else if (e.newState === 'closed') {
583
589
  this.#unbindDimensionsWorker?.();
584
590
  this.#unbindDimensionsWorker = null;
@@ -1364,7 +1370,8 @@ export class ModalElement extends BaseElement {
1364
1370
  transition:
1365
1371
  display 0.2s allow-discrete,
1366
1372
  overlay 0.2s allow-discrete,
1367
- backdrop-filter 0.2s;
1373
+ backdrop-filter 0.2s,
1374
+ background 0.2s;
1368
1375
  }
1369
1376
 
1370
1377
  :host(._swipe-dismiss._container) {
@@ -1404,7 +1411,7 @@ export class ModalElement extends BaseElement {
1404
1411
  backdrop-filter: blur(3px);
1405
1412
  }
1406
1413
 
1407
- :host(:not([popover="manual"]):popover-open)::backdrop {
1414
+ :host(:not([popover="manual"], ._manual-dismiss):popover-open)::backdrop {
1408
1415
  backdrop-filter: blur(0px);
1409
1416
  }
1410
1417
 
@@ -1472,12 +1479,8 @@ export class ModalElement extends BaseElement {
1472
1479
  border: none;
1473
1480
  background: none;
1474
1481
  }
1475
-
1476
- :host(:not([popover="manual"])) {
1477
- pointer-events: none;
1478
- }
1479
1482
 
1480
- :host(:not([popover="manual"])) .close-button {
1483
+ :host(:not([popover="manual"], ._manual-dismiss)) .close-button {
1481
1484
  display: none;
1482
1485
  }
1483
1486