@hkdigital/lib-core 0.5.6 → 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,29 +203,18 @@
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
- $inspect('isLandscape', isLandscape);
194
- $inspect('windowWidth/Height', windowWidth, windowHeight);
195
- $inspect('iosWindowWidth/Height', iosWindowWidth, iosWindowHeight);
215
+ // $inspect('isLandscape', isLandscape);
216
+ // $inspect('windowWidth/Height', windowWidth, windowHeight);
217
+ // $inspect('iosWindowWidth/Height', iosWindowWidth, iosWindowHeight);
196
218
 
197
219
  // Update game dimensions based on window size and orientation
198
220
  $effect(() => {
@@ -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);
@@ -277,13 +294,13 @@
277
294
  iosWindowWidth = window.innerWidth;
278
295
  iosWindowHeight = window.innerHeight;
279
296
  }
280
- console.debug('updateIosWidthHeight', {
281
- angle,
282
- 'window.innerWidth': window.innerWidth,
283
- 'window.innerHeight': window.innerHeight,
284
- iosWindowWidth,
285
- iosWindowHeight
286
- });
297
+ // console.debug('updateIosWidthHeight', {
298
+ // angle,
299
+ // 'window.innerWidth': window.innerWidth,
300
+ // 'window.innerHeight': window.innerHeight,
301
+ // iosWindowWidth,
302
+ // iosWindowHeight
303
+ // });
287
304
  }
288
305
  }
289
306
 
@@ -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';
@@ -427,7 +478,10 @@
427
478
  <!-- margin: /* top | right | bottom | left */ -->
428
479
 
429
480
  {#if gameHeight}
430
- <div class:center>
481
+ <div
482
+ class:center
483
+ style:height={center ? `${iosWindowHeight ?? windowHeight}px` : undefined}
484
+ >
431
485
  <div
432
486
  data-component="game-box"
433
487
  data-orientation={isLandscape ? 'landscape' : 'portrait'}
@@ -466,7 +520,8 @@
466
520
  requestDevmode,
467
521
  requestFullscreen,
468
522
  gameWidth,
469
- gameHeight
523
+ gameHeight,
524
+ iosLandscapeHeightQuirk
470
525
  })}
471
526
  </ScaledContainer>
472
527
  <!-- Portrait content -->
@@ -490,7 +545,8 @@
490
545
  requestDevmode,
491
546
  requestFullscreen,
492
547
  gameWidth,
493
- gameHeight
548
+ gameHeight,
549
+ iosLandscapeHeightQuirk
494
550
  })}
495
551
  </ScaledContainer>
496
552
  {:else if supportsFullscreen && !isDevMode}
@@ -514,7 +570,8 @@
514
570
  requestDevmode,
515
571
  requestFullscreen,
516
572
  gameWidth,
517
- gameHeight
573
+ gameHeight,
574
+ iosLandscapeHeightQuirk
518
575
  })}
519
576
  </ScaledContainer>
520
577
  {:else if isMobile && snippetInstallOnHomeScreen && !isDevMode}
@@ -538,7 +595,8 @@
538
595
  requestDevmode,
539
596
  requestFullscreen,
540
597
  gameWidth,
541
- gameHeight
598
+ gameHeight,
599
+ iosLandscapeHeightQuirk
542
600
  })}
543
601
  </ScaledContainer>
544
602
  {:else}
@@ -563,7 +621,8 @@
563
621
  requestDevmode,
564
622
  requestFullscreen,
565
623
  gameWidth,
566
- gameHeight
624
+ gameHeight,
625
+ iosLandscapeHeightQuirk
567
626
  })}
568
627
  </ScaledContainer>
569
628
  <!-- Portrait content -->
@@ -587,7 +646,8 @@
587
646
  requestDevmode,
588
647
  requestFullscreen,
589
648
  gameWidth,
590
- gameHeight
649
+ gameHeight,
650
+ iosLandscapeHeightQuirk
591
651
  })}
592
652
  </ScaledContainer>
593
653
  {/if}
@@ -606,13 +666,16 @@
606
666
  isLandscape,
607
667
  isPortrait: !isLandscape,
608
668
  isMobile,
669
+ isIos,
670
+ isAndroid,
609
671
  os,
610
672
  isFullscreen,
611
673
  isDevMode,
612
674
  requestDevmode,
613
675
  requestFullscreen,
614
676
  gameWidth,
615
- gameHeight
677
+ gameHeight,
678
+ iosLandscapeHeightQuirk
616
679
  })}
617
680
  </ScaledContainer>
618
681
  <!-- Portrait content -->
@@ -628,13 +691,16 @@
628
691
  isLandscape,
629
692
  isPortrait: !isLandscape,
630
693
  isMobile,
694
+ isIos,
695
+ isAndroid,
631
696
  os,
632
697
  isFullscreen,
633
698
  isDevMode,
634
699
  requestDevmode,
635
700
  requestFullscreen,
636
701
  gameWidth,
637
- gameHeight
702
+ gameHeight,
703
+ iosLandscapeHeightQuirk
638
704
  })}
639
705
  </ScaledContainer>
640
706
  {/if}
@@ -654,10 +720,17 @@
654
720
  }
655
721
 
656
722
  :global(html.game-box-no-scroll) {
657
- overflow: clip;
723
+ overflow: hidden; /* allow resize events, prevent via JavaScript */
658
724
  scrollbar-width: none; /* Firefox */
659
725
  -ms-overflow-style: none; /* IE and Edge */
660
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
+ }
661
734
  :global(html.game-box-no-scroll::-webkit-scrollbar) {
662
735
  display: none;
663
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.6",
3
+ "version": "0.5.8",
4
4
  "author": {
5
5
  "name": "HKdigital",
6
6
  "url": "https://hkdigital.nl"