@hkdigital/lib-core 0.5.7 → 0.5.8

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.
@@ -21,7 +21,8 @@
21
21
  * requestDevmode:function,
22
22
  * requestFullscreen:function,
23
23
  * gameWidth: number,
24
- * gameHeight: number
24
+ * gameHeight: number,
25
+ * iosLandscapeHeightQuirk: boolean
25
26
  * }} SnippetParams
26
27
  */
27
28
 
@@ -129,6 +130,38 @@
129
130
 
130
131
  const isAppleMobile = /iPhone|iPod/.test(navigator.userAgent);
131
132
 
133
+ let os = $state();
134
+ let isIos = $derived(os === 'iOS');
135
+ let isAndroid = $derived(os === 'Android');
136
+
137
+ /**
138
+ * Detect iOS landscape height quirk (status bar appears/disappears)
139
+ *
140
+ * Detection: in landscape, innerHeight is ~20px less than screen.width
141
+ *
142
+ * NOTE: This detection is reactive and will update when dimensions change.
143
+ * iOS PWA only fires viewport resize events when there's scrollable
144
+ * overflow content, which we add via CSS ::after when quirk is detected.
145
+ */
146
+ let iosLandscapeHeightQuirk = $derived.by(() => {
147
+ // Force reactivity by accessing these variables
148
+ const currentLandscape = isLandscape;
149
+ const currentIos = isIos;
150
+ const width = iosWindowWidth ?? windowWidth;
151
+ const height = iosWindowHeight ?? windowHeight;
152
+
153
+ if (!currentLandscape || !currentIos) return false;
154
+
155
+ if (!width || !height || !window.screen) return false;
156
+
157
+ // In landscape: window.innerHeight should equal screen.width
158
+ // If it's 20px less, the quirk is active
159
+ const screenWidth = window.screen.width;
160
+ const heightDifference = screenWidth - height;
161
+
162
+ return heightDifference >= 18 && heightDifference <= 22;
163
+ });
164
+
132
165
  // Debounce window dimensions on iOS to skip intermediate resize events
133
166
  let skipNextResize = false;
134
167
  let resetTimer;
@@ -170,24 +203,13 @@
170
203
  });
171
204
 
172
205
  $effect(() => {
173
- // console.debug('getIsLandscape effect running', {
174
- // isPwa,
175
- // isAppleMobile,
176
- // windowWidth,
177
- // windowHeight,
178
- // debouncedWindowWidth,
179
- // debouncedWindowHeight,
180
- // iosWindowWidth,
181
- // iosWindowHeight
182
- // });
206
+ // Use matchMedia for orientation detection (works on all iOS versions)
207
+ // This is more reliable than screen.orientation.angle
208
+ const isPortraitMedia =
209
+ typeof window !== 'undefined' &&
210
+ window.matchMedia('(orientation: portrait)').matches;
183
211
 
184
- if (isPwa && isAppleMobile) {
185
- // For iOS PWA, use iOS-specific dimensions
186
- isLandscape = iosWindowWidth > iosWindowHeight;
187
- } else {
188
- // For non-PWA, use debounced window dimensions
189
- isLandscape = debouncedWindowWidth > debouncedWindowHeight;
190
- }
212
+ isLandscape = !isPortraitMedia;
191
213
  });
192
214
 
193
215
  // $inspect('isLandscape', isLandscape);
@@ -250,11 +272,6 @@
250
272
 
251
273
  let isPwa = $state(false);
252
274
 
253
- let os = $state();
254
-
255
- let isIos = $derived(os === 'iOS');
256
- let isAndroid = $derived(os === 'Android');
257
-
258
275
  let isMobile = $state(false);
259
276
 
260
277
  let isDevMode = $state(false);
@@ -302,7 +319,23 @@
302
319
 
303
320
  updateIosWidthHeight();
304
321
 
322
+ // Listen for orientation changes using matchMedia (works on all iOS)
323
+ const portraitMediaQuery = window.matchMedia('(orientation: portrait)');
324
+ const handleOrientationChange = (e) => {
325
+ isLandscape = !e.matches;
326
+
327
+ // Update iOS dimensions if needed
328
+ if (isPwa && isAppleMobile) {
329
+ updateIosWidthHeight();
330
+ }
331
+ };
332
+ portraitMediaQuery.addEventListener('change', handleOrientationChange);
333
+
305
334
  show = true;
335
+
336
+ return () => {
337
+ portraitMediaQuery.removeEventListener('change', handleOrientationChange);
338
+ };
306
339
  });
307
340
 
308
341
  onMount(() => {
@@ -310,11 +343,29 @@
310
343
  const html = document.documentElement;
311
344
  html.classList.add(gameBoxNoScroll);
312
345
 
346
+ // Prevent page scroll while allowing child elements to scroll
347
+ const preventPageScroll = () => {
348
+ window.scrollTo(0, 0);
349
+ };
350
+
351
+ window.addEventListener('scroll', preventPageScroll, { passive: true });
352
+
313
353
  return () => {
314
354
  html.classList.remove(gameBoxNoScroll);
355
+ window.removeEventListener('scroll', preventPageScroll);
315
356
  };
316
357
  });
317
358
 
359
+ // Toggle overflow content based on quirk detection
360
+ $effect(() => {
361
+ const html = document.documentElement;
362
+ if (iosLandscapeHeightQuirk) {
363
+ html.classList.add('game-box-has-quirk');
364
+ } else {
365
+ html.classList.remove('game-box-has-quirk');
366
+ }
367
+ });
368
+
318
369
  function getOS() {
319
370
  if (isAppleMobile) {
320
371
  return 'iOS';
@@ -469,7 +520,8 @@
469
520
  requestDevmode,
470
521
  requestFullscreen,
471
522
  gameWidth,
472
- gameHeight
523
+ gameHeight,
524
+ iosLandscapeHeightQuirk
473
525
  })}
474
526
  </ScaledContainer>
475
527
  <!-- Portrait content -->
@@ -493,7 +545,8 @@
493
545
  requestDevmode,
494
546
  requestFullscreen,
495
547
  gameWidth,
496
- gameHeight
548
+ gameHeight,
549
+ iosLandscapeHeightQuirk
497
550
  })}
498
551
  </ScaledContainer>
499
552
  {:else if supportsFullscreen && !isDevMode}
@@ -517,7 +570,8 @@
517
570
  requestDevmode,
518
571
  requestFullscreen,
519
572
  gameWidth,
520
- gameHeight
573
+ gameHeight,
574
+ iosLandscapeHeightQuirk
521
575
  })}
522
576
  </ScaledContainer>
523
577
  {:else if isMobile && snippetInstallOnHomeScreen && !isDevMode}
@@ -541,7 +595,8 @@
541
595
  requestDevmode,
542
596
  requestFullscreen,
543
597
  gameWidth,
544
- gameHeight
598
+ gameHeight,
599
+ iosLandscapeHeightQuirk
545
600
  })}
546
601
  </ScaledContainer>
547
602
  {:else}
@@ -566,7 +621,8 @@
566
621
  requestDevmode,
567
622
  requestFullscreen,
568
623
  gameWidth,
569
- gameHeight
624
+ gameHeight,
625
+ iosLandscapeHeightQuirk
570
626
  })}
571
627
  </ScaledContainer>
572
628
  <!-- Portrait content -->
@@ -590,7 +646,8 @@
590
646
  requestDevmode,
591
647
  requestFullscreen,
592
648
  gameWidth,
593
- gameHeight
649
+ gameHeight,
650
+ iosLandscapeHeightQuirk
594
651
  })}
595
652
  </ScaledContainer>
596
653
  {/if}
@@ -609,13 +666,16 @@
609
666
  isLandscape,
610
667
  isPortrait: !isLandscape,
611
668
  isMobile,
669
+ isIos,
670
+ isAndroid,
612
671
  os,
613
672
  isFullscreen,
614
673
  isDevMode,
615
674
  requestDevmode,
616
675
  requestFullscreen,
617
676
  gameWidth,
618
- gameHeight
677
+ gameHeight,
678
+ iosLandscapeHeightQuirk
619
679
  })}
620
680
  </ScaledContainer>
621
681
  <!-- Portrait content -->
@@ -631,13 +691,16 @@
631
691
  isLandscape,
632
692
  isPortrait: !isLandscape,
633
693
  isMobile,
694
+ isIos,
695
+ isAndroid,
634
696
  os,
635
697
  isFullscreen,
636
698
  isDevMode,
637
699
  requestDevmode,
638
700
  requestFullscreen,
639
701
  gameWidth,
640
- gameHeight
702
+ gameHeight,
703
+ iosLandscapeHeightQuirk
641
704
  })}
642
705
  </ScaledContainer>
643
706
  {/if}
@@ -657,10 +720,17 @@
657
720
  }
658
721
 
659
722
  :global(html.game-box-no-scroll) {
660
- overflow: clip;
723
+ overflow: hidden; /* allow resize events, prevent via JavaScript */
661
724
  scrollbar-width: none; /* Firefox */
662
725
  -ms-overflow-style: none; /* IE and Edge */
663
726
  }
727
+ /* Only add overflow content when quirk is detected */
728
+ :global(html.game-box-no-scroll.game-box-has-quirk::after) {
729
+ content: '\A'; /* newline character */
730
+ white-space: pre; /* preserve newline */
731
+ display: block;
732
+ height: 20px; /* create overflow to trigger iOS resize events */
733
+ }
664
734
  :global(html.game-box-no-scroll::-webkit-scrollbar) {
665
735
  display: none;
666
736
  }
@@ -100,6 +100,7 @@ declare const GameBox: import("svelte").Component<{
100
100
  requestFullscreen: Function;
101
101
  gameWidth: number;
102
102
  gameHeight: number;
103
+ iosLandscapeHeightQuirk: boolean;
103
104
  }]>;
104
105
  snippetPortrait?: import("svelte").Snippet<[{
105
106
  isLandscape: boolean;
@@ -114,6 +115,7 @@ declare const GameBox: import("svelte").Component<{
114
115
  requestFullscreen: Function;
115
116
  gameWidth: number;
116
117
  gameHeight: number;
118
+ iosLandscapeHeightQuirk: boolean;
117
119
  }]>;
118
120
  snippetRequireFullscreen?: import("svelte").Snippet<[{
119
121
  isLandscape: boolean;
@@ -128,6 +130,7 @@ declare const GameBox: import("svelte").Component<{
128
130
  requestFullscreen: Function;
129
131
  gameWidth: number;
130
132
  gameHeight: number;
133
+ iosLandscapeHeightQuirk: boolean;
131
134
  }]>;
132
135
  snippetInstallOnHomeScreen?: import("svelte").Snippet<[{
133
136
  isLandscape: boolean;
@@ -142,6 +145,7 @@ declare const GameBox: import("svelte").Component<{
142
145
  requestFullscreen: Function;
143
146
  gameWidth: number;
144
147
  gameHeight: number;
148
+ iosLandscapeHeightQuirk: boolean;
145
149
  }]>;
146
150
  }, {}, "">;
147
151
  type SnippetParams = {
@@ -157,4 +161,5 @@ type SnippetParams = {
157
161
  requestFullscreen: Function;
158
162
  gameWidth: number;
159
163
  gameHeight: number;
164
+ iosLandscapeHeightQuirk: boolean;
160
165
  };
@@ -61,7 +61,7 @@
61
61
  return;
62
62
  }
63
63
 
64
- console.debug(`Enable scaling [${width},${height}]`);
64
+ // console.debug(`Enable scaling [${width},${height}]`);
65
65
 
66
66
  return enableContainerScaling({
67
67
  container,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hkdigital/lib-core",
3
- "version": "0.5.7",
3
+ "version": "0.5.8",
4
4
  "author": {
5
5
  "name": "HKdigital",
6
6
  "url": "https://hkdigital.nl"