@hkdigital/lib-core 0.5.10 → 0.5.12
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/README.md +148 -13
- package/dist/config/generators/imagetools.js +63 -0
- package/dist/config/imagetools.d.ts +4 -46
- package/dist/ui/components/game-box/GameBox.svelte +24 -70
- package/dist/ui/components/game-box/GameBox.svelte.d.ts +0 -5
- package/dist/ui/components/game-box/README.md +16 -0
- package/package.json +1 -1
- package/dist/ui/components/game-box/GameBox.svelte.with-scaling__ +0 -577
package/README.md
CHANGED
|
@@ -32,6 +32,9 @@ pnpm add @skeletonlabs/skeleton
|
|
|
32
32
|
# UI icons
|
|
33
33
|
pnpm add @steeze-ui/heroicons
|
|
34
34
|
|
|
35
|
+
# CSS processing (Tailwind and PostCSS)
|
|
36
|
+
pnpm add tailwindcss @tailwindcss/postcss postcss autoprefixer
|
|
37
|
+
|
|
35
38
|
# Logging
|
|
36
39
|
pnpm add pino pino-pretty
|
|
37
40
|
|
|
@@ -42,13 +45,13 @@ pnpm add jsonwebtoken
|
|
|
42
45
|
pnpm add @eslint/js eslint-plugin-import
|
|
43
46
|
|
|
44
47
|
# Image processing
|
|
45
|
-
pnpm add vite-imagetools
|
|
48
|
+
pnpm add -D vite-imagetools
|
|
46
49
|
```
|
|
47
50
|
|
|
48
51
|
**For other libraries**, install as dev dependencies and declare as peer dependencies:
|
|
49
52
|
```bash
|
|
50
53
|
# Install as dev dependencies and peer dependencies
|
|
51
|
-
pnpm add -D --save-peer @sveltejs/kit svelte svelte-preprocess runed valibot @skeletonlabs/skeleton @steeze-ui/heroicons pino pino-pretty jsonwebtoken @eslint/js eslint-plugin-import vite-imagetools
|
|
54
|
+
pnpm add -D --save-peer @sveltejs/kit svelte svelte-preprocess runed valibot @skeletonlabs/skeleton @steeze-ui/heroicons tailwindcss @tailwindcss/postcss postcss autoprefixer pino pino-pretty jsonwebtoken @eslint/js eslint-plugin-import vite-imagetools
|
|
52
55
|
```
|
|
53
56
|
|
|
54
57
|
### Design System & Configuration
|
|
@@ -135,7 +138,8 @@ This library includes a complete design system with Tailwind CSS integration. Ba
|
|
|
135
138
|
export default config;
|
|
136
139
|
```
|
|
137
140
|
|
|
138
|
-
5. **
|
|
141
|
+
5. **Image import type definitions** - Add imagetools type support to
|
|
142
|
+
`app.d.ts`:
|
|
139
143
|
```typescript
|
|
140
144
|
// src/app.d.ts
|
|
141
145
|
import '@hkdigital/lib-core/config/imagetools.d.ts';
|
|
@@ -179,6 +183,59 @@ This library includes a complete design system with Tailwind CSS integration. Ba
|
|
|
179
183
|
import heroResponsive from '$lib/assets/hero.jpg?preset=photo&responsive';
|
|
180
184
|
```
|
|
181
185
|
|
|
186
|
+
6. **(meta) folder setup** - Copy and configure PWA/favicon generation:
|
|
187
|
+
|
|
188
|
+
The library includes a complete `(meta)` folder with PWA configuration,
|
|
189
|
+
favicon generation, manifest.json, sitemap.xml, and robots.txt endpoints.
|
|
190
|
+
|
|
191
|
+
**Copy the folder to your project:**
|
|
192
|
+
```bash
|
|
193
|
+
cp -r node_modules/@hkdigital/lib-core/src/routes/\(meta\) src/routes/
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
**Customize for your app:**
|
|
197
|
+
- Replace `src/routes/(meta)/favicon.png` with your 512×512px image
|
|
198
|
+
- Edit `src/routes/(meta)/config.js`:
|
|
199
|
+
```javascript
|
|
200
|
+
export const name = 'Your App Name';
|
|
201
|
+
export const shortName = 'App';
|
|
202
|
+
export const description = 'Your app description';
|
|
203
|
+
export const backgroundAndThemeColor = '#082962';
|
|
204
|
+
|
|
205
|
+
// Add your site routes for sitemap
|
|
206
|
+
export const siteRoutes = ['/', '/about', '/contact'];
|
|
207
|
+
|
|
208
|
+
// Configure robots.txt (prevent test sites from being indexed)
|
|
209
|
+
export const robotsConfig = {
|
|
210
|
+
allowedHosts: ['mysite.com', 'www.mysite.com'],
|
|
211
|
+
disallowedPaths: ['/admin/*'],
|
|
212
|
+
includeSitemap: true
|
|
213
|
+
};
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
**Integrate into root layout:**
|
|
217
|
+
```svelte
|
|
218
|
+
<!-- src/routes/+layout.svelte -->
|
|
219
|
+
<script>
|
|
220
|
+
import { Favicons, PWA } from './(meta)/index.js';
|
|
221
|
+
</script>
|
|
222
|
+
|
|
223
|
+
<Favicons />
|
|
224
|
+
<PWA />
|
|
225
|
+
|
|
226
|
+
{@render children()}
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
**What you get:**
|
|
230
|
+
- Automatic favicon generation (16, 32, 192, 512px)
|
|
231
|
+
- Apple touch icons (120, 152, 167, 180px)
|
|
232
|
+
- Dynamic `/manifest.json` endpoint
|
|
233
|
+
- Dynamic `/sitemap.xml` endpoint
|
|
234
|
+
- Dynamic `/robots.txt` with host filtering
|
|
235
|
+
|
|
236
|
+
See [src/routes/(meta)/README.md](./src/routes/(meta)/README.md) for
|
|
237
|
+
complete documentation.
|
|
238
|
+
|
|
182
239
|
### Logging System
|
|
183
240
|
|
|
184
241
|
The library includes a comprehensive logging system that provides:
|
|
@@ -220,34 +277,112 @@ pnpm run upgrade:all # Update all packages
|
|
|
220
277
|
pnpm run publish:npm # Version bump and publish to npm
|
|
221
278
|
```
|
|
222
279
|
|
|
223
|
-
### Import
|
|
280
|
+
### Import Patterns and Export Structure
|
|
224
281
|
|
|
225
|
-
|
|
282
|
+
**Public exports use domain-specific files matching folder names:**
|
|
226
283
|
|
|
227
|
-
|
|
284
|
+
```js
|
|
285
|
+
// Standard pattern: folder → matching .js export file
|
|
286
|
+
import { httpGet, httpPost } from '@hkdigital/lib-core/network/http.js';
|
|
287
|
+
import { CacheManager } from '@hkdigital/lib-core/network/cache.js';
|
|
288
|
+
import { LCHAR } from '@hkdigital/lib-core/constants/regexp.js';
|
|
289
|
+
import { Button } from '@hkdigital/lib-core/ui/primitives.js';
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
**Pattern:**
|
|
293
|
+
- Folder `$lib/network/http/` → Export file `$lib/network/http.js`
|
|
294
|
+
- Folder `$lib/network/cache/` → Export file `$lib/network/cache.js`
|
|
295
|
+
- Folder `$lib/constants/regexp/` → Export file `$lib/constants/regexp.js`
|
|
296
|
+
|
|
297
|
+
**Rare exception - index.js for aggregated APIs:**
|
|
228
298
|
|
|
229
299
|
```js
|
|
230
|
-
//
|
|
231
|
-
import {
|
|
300
|
+
// Only used for large subsystems with public-facing aggregated APIs
|
|
301
|
+
import { designTokens } from '@hkdigital/lib-core/design/index.js';
|
|
232
302
|
```
|
|
233
303
|
|
|
304
|
+
**TypeDefs for JSDoc:**
|
|
305
|
+
|
|
234
306
|
```js
|
|
235
307
|
/**
|
|
236
|
-
* @param {import('@hkdigital/lib-core/network/typedef.js').
|
|
308
|
+
* @param {import('@hkdigital/lib-core/network/typedef.js').HttpGetOptions}
|
|
309
|
+
* options
|
|
237
310
|
*/
|
|
311
|
+
function fetchData(options) {
|
|
312
|
+
// ...
|
|
313
|
+
}
|
|
238
314
|
```
|
|
239
315
|
|
|
240
|
-
###
|
|
316
|
+
### CSS Architecture (app.css)
|
|
241
317
|
|
|
242
|
-
|
|
318
|
+
**CRITICAL**: Your `src/app.css` must include the complete design system
|
|
319
|
+
architecture. Incomplete imports will cause build failures.
|
|
243
320
|
|
|
244
|
-
|
|
321
|
+
**Required structure:**
|
|
245
322
|
|
|
246
323
|
```css
|
|
247
324
|
/* src/app.css */
|
|
248
|
-
|
|
325
|
+
|
|
326
|
+
/* 1. CSS Layers - Controls style precedence */
|
|
327
|
+
@layer theme, base, utilities, components;
|
|
328
|
+
|
|
329
|
+
/* 2. Tailwind CSS Core */
|
|
330
|
+
@import 'tailwindcss';
|
|
331
|
+
|
|
332
|
+
/* 3. HKdigital Design System Theme (ALL required for colors to work) */
|
|
333
|
+
@import '../node_modules/@hkdigital/lib-core/dist/design/themes/hkdev/theme.css' layer(theme);
|
|
334
|
+
@import '../node_modules/@hkdigital/lib-core/dist/design/themes/hkdev/globals.css';
|
|
335
|
+
@import '../node_modules/@hkdigital/lib-core/dist/design/themes/hkdev/components.css' layer(components);
|
|
336
|
+
@import '../node_modules/@hkdigital/lib-core/dist/design/themes/hkdev/responsive.css';
|
|
337
|
+
|
|
338
|
+
/* 4. Skeleton UI Framework */
|
|
339
|
+
@import '@skeletonlabs/skeleton' source('../node_modules/@skeletonlabs/skeleton-svelte/dist');
|
|
340
|
+
@import '@skeletonlabs/skeleton/optional/presets' source('../node_modules/@skeletonlabs/skeleton-svelte/dist');
|
|
341
|
+
|
|
342
|
+
/* 5. Tailwind Configuration Reference */
|
|
343
|
+
@config "../tailwind.config.js";
|
|
344
|
+
|
|
345
|
+
/* 6. Optional: Additional utilities */
|
|
346
|
+
/*@import '../node_modules/@hkdigital/lib-core/dist/css/utilities.css';*/
|
|
249
347
|
```
|
|
250
348
|
|
|
349
|
+
**Why all theme files are required:**
|
|
350
|
+
- Missing any theme file will cause "Cannot apply unknown utility class"
|
|
351
|
+
errors
|
|
352
|
+
- Classes like `bg-surface-100`, `text-primary-500` won't work without
|
|
353
|
+
complete theme
|
|
354
|
+
- All four theme files (theme.css, globals.css, components.css,
|
|
355
|
+
responsive.css) are needed
|
|
356
|
+
|
|
357
|
+
See [src/lib/design/README.md](./src/lib/design/README.md) for complete
|
|
358
|
+
CSS architecture documentation.
|
|
359
|
+
|
|
360
|
+
### Critical: data-theme Attribute
|
|
361
|
+
|
|
362
|
+
**IMPORTANT**: Add `data-theme="hkdev"` to your `<body>` element in
|
|
363
|
+
`src/app.html`. Without this, theme colors will not work correctly.
|
|
364
|
+
|
|
365
|
+
```html
|
|
366
|
+
<!-- src/app.html -->
|
|
367
|
+
<!doctype html>
|
|
368
|
+
<html lang="en">
|
|
369
|
+
<head>
|
|
370
|
+
<meta charset="utf-8" />
|
|
371
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
372
|
+
%sveltekit.head%
|
|
373
|
+
</head>
|
|
374
|
+
<body data-theme="hkdev" data-sveltekit-preload-data="hover">
|
|
375
|
+
<div>%sveltekit.body%</div>
|
|
376
|
+
</body>
|
|
377
|
+
</html>
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
**Why this is required:**
|
|
381
|
+
- Theme CSS custom properties are scoped to `[data-theme='hkdev']`
|
|
382
|
+
- Without this attribute, colors fall back to browser defaults
|
|
383
|
+
- **Symptom**: Colors like `bg-primary-500` show grey instead of magenta
|
|
384
|
+
- **Solution**: Add `data-theme="hkdev"` to `<body>` element
|
|
385
|
+
|
|
251
386
|
## Building the library
|
|
252
387
|
|
|
253
388
|
To build your library:
|
|
@@ -2,6 +2,25 @@ const DEFAULT_WIDTHS = [1920, 1536, 1024, 640];
|
|
|
2
2
|
|
|
3
3
|
const DEFAULT_THUMBNAIL_WIDTH = 150;
|
|
4
4
|
|
|
5
|
+
const FAVICON_SIZES = [
|
|
6
|
+
16, // classic browser tab icon
|
|
7
|
+
32, // high-resolution browser support
|
|
8
|
+
48, // Windows desktop shortcuts
|
|
9
|
+
120, // iPhone older retina
|
|
10
|
+
152, // iPad retina, iOS Safari bookmarks
|
|
11
|
+
167, // iPad Pro
|
|
12
|
+
180, // iPhone retina, iOS home screen
|
|
13
|
+
192, // Android home screen, Chrome PWA
|
|
14
|
+
512 // PWA application icon, Android splash
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
const APPLE_TOUCH_SIZES = [
|
|
18
|
+
120, // iPhone older retina
|
|
19
|
+
152, // iPad retina, iOS Safari bookmarks
|
|
20
|
+
167, // iPad Pro
|
|
21
|
+
180 // iPhone retina, iOS home screen
|
|
22
|
+
];
|
|
23
|
+
|
|
5
24
|
const DEFAULT_PRESETS = {
|
|
6
25
|
default: {
|
|
7
26
|
format: 'avif',
|
|
@@ -74,9 +93,15 @@ export function generateResponseConfigs(options) {
|
|
|
74
93
|
|
|
75
94
|
// @ts-ignore
|
|
76
95
|
const responsiveConfig = entries.find(([key]) => key === 'responsive');
|
|
96
|
+
// @ts-ignore
|
|
97
|
+
const faviconsConfig = entries.find(([key]) => key === 'favicons');
|
|
98
|
+
// @ts-ignore
|
|
99
|
+
const appleTouchIconsConfig = entries.find(([key]) => key === 'apple-touch-icons');
|
|
77
100
|
// console.log('responsiveConfig found:', !!responsiveConfig);
|
|
78
101
|
|
|
79
102
|
const widths = options?.widths ?? DEFAULT_WIDTHS;
|
|
103
|
+
const faviconSizes = options?.faviconSizes ?? FAVICON_SIZES;
|
|
104
|
+
const appleTouchSizes = options?.appleTouchSizes ?? APPLE_TOUCH_SIZES;
|
|
80
105
|
|
|
81
106
|
// Always include the main image(s) and a thumbnail version
|
|
82
107
|
const thumbnailConfig = {
|
|
@@ -84,6 +109,32 @@ export function generateResponseConfigs(options) {
|
|
|
84
109
|
w: String(options?.thumbnailWidth ?? DEFAULT_THUMBNAIL_WIDTH)
|
|
85
110
|
};
|
|
86
111
|
|
|
112
|
+
// Handle favicons directive - generate all favicon sizes as PNG
|
|
113
|
+
if (faviconsConfig) {
|
|
114
|
+
const faviconConfigs = faviconSizes.map((w) => {
|
|
115
|
+
return {
|
|
116
|
+
...configPairs,
|
|
117
|
+
w: String(w),
|
|
118
|
+
format: 'png'
|
|
119
|
+
};
|
|
120
|
+
});
|
|
121
|
+
// console.log('Returning favicon configs:', faviconConfigs);
|
|
122
|
+
return faviconConfigs;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Handle apple-touch-icons directive - generate Apple touch icon sizes as PNG
|
|
126
|
+
if (appleTouchIconsConfig) {
|
|
127
|
+
const appleTouchConfigs = appleTouchSizes.map((w) => {
|
|
128
|
+
return {
|
|
129
|
+
...configPairs,
|
|
130
|
+
w: String(w),
|
|
131
|
+
format: 'png'
|
|
132
|
+
};
|
|
133
|
+
});
|
|
134
|
+
// console.log('Returning apple-touch-icon configs:', appleTouchConfigs);
|
|
135
|
+
return appleTouchConfigs;
|
|
136
|
+
}
|
|
137
|
+
|
|
87
138
|
if (!responsiveConfig) {
|
|
88
139
|
// Directive 'responsive' was not set => return original + thumbnail
|
|
89
140
|
const originalConfig = configPairs; // No 'w' means original dimensions
|
|
@@ -129,6 +180,18 @@ export function generateDefaultDirectives(options) {
|
|
|
129
180
|
params.set('as', 'metadata');
|
|
130
181
|
}
|
|
131
182
|
|
|
183
|
+
// > Return metadata if directive 'favicons' is set
|
|
184
|
+
|
|
185
|
+
if (params.has('favicons')) {
|
|
186
|
+
params.set('as', 'metadata');
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// > Return metadata if directive 'apple-touch-icons' is set
|
|
190
|
+
|
|
191
|
+
if (params.has('apple-touch-icons')) {
|
|
192
|
+
params.set('as', 'metadata');
|
|
193
|
+
}
|
|
194
|
+
|
|
132
195
|
// > Process presets
|
|
133
196
|
|
|
134
197
|
if (presetName) {
|
|
@@ -73,56 +73,14 @@ declare module '*&preset=blur' {
|
|
|
73
73
|
|
|
74
74
|
/* For favicons */
|
|
75
75
|
|
|
76
|
-
//
|
|
77
|
-
declare module '*?
|
|
76
|
+
// Generate all favicon sizes (16, 32, 48, 120, 152, 167, 180, 192, 512)
|
|
77
|
+
declare module '*?favicons' {
|
|
78
78
|
const out: ImageSource;
|
|
79
79
|
export default out;
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
//
|
|
83
|
-
declare module '*?
|
|
84
|
-
const out: ImageSource;
|
|
85
|
-
export default out;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// Windows desktop shortcuts
|
|
89
|
-
declare module '*?w=48' {
|
|
90
|
-
const out: ImageSource;
|
|
91
|
-
export default out;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// iPhone older retina
|
|
95
|
-
declare module '*?w=120' {
|
|
96
|
-
const out: ImageSource;
|
|
97
|
-
export default out;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// iPad retina, iOS Safari bookmarks
|
|
101
|
-
declare module '*?w=152' {
|
|
102
|
-
const out: ImageSource;
|
|
103
|
-
export default out;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
// iPad Pro
|
|
107
|
-
declare module '*?w=167' {
|
|
108
|
-
const out: ImageSource;
|
|
109
|
-
export default out;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// iPhone retina, iOS home screen
|
|
113
|
-
declare module '*?w=180' {
|
|
114
|
-
const out: ImageSource;
|
|
115
|
-
export default out;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// Android home screen, Chrome PWA
|
|
119
|
-
declare module '*?w=192' {
|
|
120
|
-
const out: ImageSource;
|
|
121
|
-
export default out;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
// PWA application icon, Android splash
|
|
125
|
-
declare module '*?w=512' {
|
|
82
|
+
// Generate Apple touch icon sizes (120, 152, 167, 180)
|
|
83
|
+
declare module '*?apple-touch-icons' {
|
|
126
84
|
const out: ImageSource;
|
|
127
85
|
export default out;
|
|
128
86
|
}
|
|
@@ -21,8 +21,7 @@
|
|
|
21
21
|
* requestDevmode:function,
|
|
22
22
|
* requestFullscreen:function,
|
|
23
23
|
* gameWidth: number,
|
|
24
|
-
* gameHeight: number
|
|
25
|
-
* iosLandscapeHeightQuirk: boolean
|
|
24
|
+
* gameHeight: number
|
|
26
25
|
* }} SnippetParams
|
|
27
26
|
*/
|
|
28
27
|
|
|
@@ -134,34 +133,6 @@
|
|
|
134
133
|
let isIos = $derived(os === 'iOS');
|
|
135
134
|
let isAndroid = $derived(os === 'Android');
|
|
136
135
|
|
|
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
|
-
|
|
165
136
|
// Debounce window dimensions on iOS to skip intermediate resize events
|
|
166
137
|
let skipNextResize = false;
|
|
167
138
|
let resetTimer;
|
|
@@ -343,29 +314,23 @@
|
|
|
343
314
|
const html = document.documentElement;
|
|
344
315
|
html.classList.add(gameBoxNoScroll);
|
|
345
316
|
|
|
346
|
-
// Prevent page scroll while allowing child elements to scroll
|
|
347
|
-
const preventPageScroll = () => {
|
|
348
|
-
|
|
349
|
-
};
|
|
317
|
+
// // Prevent page scroll while allowing child elements to scroll
|
|
318
|
+
// const preventPageScroll = () => {
|
|
319
|
+
// window.scrollTo(0, 0);
|
|
320
|
+
// };
|
|
350
321
|
|
|
351
|
-
window.addEventListener('scroll', preventPageScroll, { passive: true });
|
|
322
|
+
// window.addEventListener('scroll', preventPageScroll, { passive: true });
|
|
352
323
|
|
|
324
|
+
// return () => {
|
|
325
|
+
// html.classList.remove(gameBoxNoScroll);
|
|
326
|
+
// window.removeEventListener('scroll', preventPageScroll);
|
|
327
|
+
// };
|
|
353
328
|
return () => {
|
|
354
329
|
html.classList.remove(gameBoxNoScroll);
|
|
355
|
-
window.removeEventListener('scroll', preventPageScroll);
|
|
356
|
-
};
|
|
357
|
-
});
|
|
358
|
-
|
|
359
|
-
// Toggle overflow content based on quirk detection
|
|
360
|
-
$effect(() => {
|
|
361
|
-
const html = document.documentElement;
|
|
362
|
-
if (iosLandscapeHeightQuirk) {
|
|
363
|
-
html.classList.add('ios-height-quirck');
|
|
364
|
-
} else {
|
|
365
|
-
html.classList.remove('ios-height-quirck');
|
|
366
330
|
}
|
|
367
331
|
});
|
|
368
332
|
|
|
333
|
+
|
|
369
334
|
function getOS() {
|
|
370
335
|
if (isAppleMobile) {
|
|
371
336
|
return 'iOS';
|
|
@@ -520,8 +485,7 @@
|
|
|
520
485
|
requestDevmode,
|
|
521
486
|
requestFullscreen,
|
|
522
487
|
gameWidth,
|
|
523
|
-
gameHeight
|
|
524
|
-
iosLandscapeHeightQuirk
|
|
488
|
+
gameHeight
|
|
525
489
|
})}
|
|
526
490
|
</ScaledContainer>
|
|
527
491
|
<!-- Portrait content -->
|
|
@@ -545,8 +509,7 @@
|
|
|
545
509
|
requestDevmode,
|
|
546
510
|
requestFullscreen,
|
|
547
511
|
gameWidth,
|
|
548
|
-
gameHeight
|
|
549
|
-
iosLandscapeHeightQuirk
|
|
512
|
+
gameHeight
|
|
550
513
|
})}
|
|
551
514
|
</ScaledContainer>
|
|
552
515
|
{:else if supportsFullscreen && !isDevMode}
|
|
@@ -570,8 +533,7 @@
|
|
|
570
533
|
requestDevmode,
|
|
571
534
|
requestFullscreen,
|
|
572
535
|
gameWidth,
|
|
573
|
-
gameHeight
|
|
574
|
-
iosLandscapeHeightQuirk
|
|
536
|
+
gameHeight
|
|
575
537
|
})}
|
|
576
538
|
</ScaledContainer>
|
|
577
539
|
{:else if isMobile && snippetInstallOnHomeScreen && !isDevMode}
|
|
@@ -595,8 +557,7 @@
|
|
|
595
557
|
requestDevmode,
|
|
596
558
|
requestFullscreen,
|
|
597
559
|
gameWidth,
|
|
598
|
-
gameHeight
|
|
599
|
-
iosLandscapeHeightQuirk
|
|
560
|
+
gameHeight
|
|
600
561
|
})}
|
|
601
562
|
</ScaledContainer>
|
|
602
563
|
{:else}
|
|
@@ -621,8 +582,7 @@
|
|
|
621
582
|
requestDevmode,
|
|
622
583
|
requestFullscreen,
|
|
623
584
|
gameWidth,
|
|
624
|
-
gameHeight
|
|
625
|
-
iosLandscapeHeightQuirk
|
|
585
|
+
gameHeight
|
|
626
586
|
})}
|
|
627
587
|
</ScaledContainer>
|
|
628
588
|
<!-- Portrait content -->
|
|
@@ -646,8 +606,7 @@
|
|
|
646
606
|
requestDevmode,
|
|
647
607
|
requestFullscreen,
|
|
648
608
|
gameWidth,
|
|
649
|
-
gameHeight
|
|
650
|
-
iosLandscapeHeightQuirk
|
|
609
|
+
gameHeight
|
|
651
610
|
})}
|
|
652
611
|
</ScaledContainer>
|
|
653
612
|
{/if}
|
|
@@ -674,8 +633,7 @@
|
|
|
674
633
|
requestDevmode,
|
|
675
634
|
requestFullscreen,
|
|
676
635
|
gameWidth,
|
|
677
|
-
gameHeight
|
|
678
|
-
iosLandscapeHeightQuirk
|
|
636
|
+
gameHeight
|
|
679
637
|
})}
|
|
680
638
|
</ScaledContainer>
|
|
681
639
|
<!-- Portrait content -->
|
|
@@ -699,8 +657,7 @@
|
|
|
699
657
|
requestDevmode,
|
|
700
658
|
requestFullscreen,
|
|
701
659
|
gameWidth,
|
|
702
|
-
gameHeight
|
|
703
|
-
iosLandscapeHeightQuirk
|
|
660
|
+
gameHeight
|
|
704
661
|
})}
|
|
705
662
|
</ScaledContainer>
|
|
706
663
|
{/if}
|
|
@@ -720,16 +677,13 @@
|
|
|
720
677
|
}
|
|
721
678
|
|
|
722
679
|
:global(html.game-box-no-scroll) {
|
|
723
|
-
|
|
680
|
+
/* Prevent all scrolling - clip is stricter than hidden */
|
|
681
|
+
overflow: clip;
|
|
724
682
|
scrollbar-width: none; /* Firefox */
|
|
725
683
|
-ms-overflow-style: none; /* IE and Edge */
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
content: '\A'; /* newline character */
|
|
730
|
-
white-space: pre; /* preserve newline */
|
|
731
|
-
display: block;
|
|
732
|
-
height: 20px; /* create overflow to trigger iOS resize events */
|
|
684
|
+
/* Prevent bounce/overscroll on iOS */
|
|
685
|
+
overscroll-behavior: none;
|
|
686
|
+
-webkit-overflow-scrolling: auto;
|
|
733
687
|
}
|
|
734
688
|
:global(html.game-box-no-scroll::-webkit-scrollbar) {
|
|
735
689
|
display: none;
|
|
@@ -100,7 +100,6 @@ declare const GameBox: import("svelte").Component<{
|
|
|
100
100
|
requestFullscreen: Function;
|
|
101
101
|
gameWidth: number;
|
|
102
102
|
gameHeight: number;
|
|
103
|
-
iosLandscapeHeightQuirk: boolean;
|
|
104
103
|
}]>;
|
|
105
104
|
snippetPortrait?: import("svelte").Snippet<[{
|
|
106
105
|
isLandscape: boolean;
|
|
@@ -115,7 +114,6 @@ declare const GameBox: import("svelte").Component<{
|
|
|
115
114
|
requestFullscreen: Function;
|
|
116
115
|
gameWidth: number;
|
|
117
116
|
gameHeight: number;
|
|
118
|
-
iosLandscapeHeightQuirk: boolean;
|
|
119
117
|
}]>;
|
|
120
118
|
snippetRequireFullscreen?: import("svelte").Snippet<[{
|
|
121
119
|
isLandscape: boolean;
|
|
@@ -130,7 +128,6 @@ declare const GameBox: import("svelte").Component<{
|
|
|
130
128
|
requestFullscreen: Function;
|
|
131
129
|
gameWidth: number;
|
|
132
130
|
gameHeight: number;
|
|
133
|
-
iosLandscapeHeightQuirk: boolean;
|
|
134
131
|
}]>;
|
|
135
132
|
snippetInstallOnHomeScreen?: import("svelte").Snippet<[{
|
|
136
133
|
isLandscape: boolean;
|
|
@@ -145,7 +142,6 @@ declare const GameBox: import("svelte").Component<{
|
|
|
145
142
|
requestFullscreen: Function;
|
|
146
143
|
gameWidth: number;
|
|
147
144
|
gameHeight: number;
|
|
148
|
-
iosLandscapeHeightQuirk: boolean;
|
|
149
145
|
}]>;
|
|
150
146
|
}, {}, "">;
|
|
151
147
|
type SnippetParams = {
|
|
@@ -161,5 +157,4 @@ type SnippetParams = {
|
|
|
161
157
|
requestFullscreen: Function;
|
|
162
158
|
gameWidth: number;
|
|
163
159
|
gameHeight: number;
|
|
164
|
-
iosLandscapeHeightQuirk: boolean;
|
|
165
160
|
};
|
|
@@ -248,6 +248,22 @@ The component includes special handling for mobile PWAs:
|
|
|
248
248
|
- **Screen Orientation**: Listens for orientation changes and updates layout
|
|
249
249
|
- **No Scroll**: Automatically prevents scrolling when active
|
|
250
250
|
|
|
251
|
+
### iOS Landscape Height Bug
|
|
252
|
+
|
|
253
|
+
For games and fullscreen applications, use `viewport-fit=cover` in your
|
|
254
|
+
viewport meta tag to prevent iOS landscape height bugs:
|
|
255
|
+
|
|
256
|
+
```html
|
|
257
|
+
<meta name="viewport"
|
|
258
|
+
content="initial-scale=1.0, maximum-scale=1.0, user-scalable=no,
|
|
259
|
+
width=device-width, height=device-height, viewport-fit=cover" />
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
This is handled automatically by the `PWA.svelte` component in the `(meta)`
|
|
263
|
+
folder when `disablePageZoom` is enabled. The `viewport-fit=cover` setting
|
|
264
|
+
ensures the viewport extends into safe areas on iOS devices, preventing a
|
|
265
|
+
common bug where landscape mode shows incorrect viewport heights.
|
|
266
|
+
|
|
251
267
|
## Development Mode
|
|
252
268
|
|
|
253
269
|
The component automatically enables development mode when:
|
package/package.json
CHANGED
|
@@ -1,577 +0,0 @@
|
|
|
1
|
-
<script>
|
|
2
|
-
import { onMount } from 'svelte';
|
|
3
|
-
|
|
4
|
-
import {
|
|
5
|
-
getGameWidthOnLandscape,
|
|
6
|
-
getGameWidthOnPortrait
|
|
7
|
-
} from './gamebox.util.js';
|
|
8
|
-
|
|
9
|
-
import { enableContainerScaling } from '$lib/design/index.js';
|
|
10
|
-
// import { enableContainerScaling } from '@hkdigital/lib-core/design/index.js';
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* @typedef {{
|
|
14
|
-
* isMobile:boolean,
|
|
15
|
-
* os:'Android'|'iOS',
|
|
16
|
-
* isFullscreen:boolean,
|
|
17
|
-
* isDevMode:boolean,
|
|
18
|
-
* requestDevmode:function,
|
|
19
|
-
* requestFullscreen:function,
|
|
20
|
-
* gameWidth: number,
|
|
21
|
-
* gameHeight: number
|
|
22
|
-
* }} SnippetParams
|
|
23
|
-
*/
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* @typedef {import('svelte').Snippet<[SnippetParams]>} GameBoxSnippet
|
|
27
|
-
*/
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* @type {{
|
|
31
|
-
* base?: string,
|
|
32
|
-
* bg?: string,
|
|
33
|
-
* classes?: string,
|
|
34
|
-
* style?: string,
|
|
35
|
-
* aspectOnLandscape?: number,
|
|
36
|
-
* aspectOnPortrait?: number,
|
|
37
|
-
* marginLeft?: number,
|
|
38
|
-
* marginRight?: number,
|
|
39
|
-
* marginTop?: number,
|
|
40
|
-
* marginBottom?: number,
|
|
41
|
-
* center?: boolean,
|
|
42
|
-
* enableScaling?: boolean,
|
|
43
|
-
* designLandscape?: {width: number, height: number},
|
|
44
|
-
* designPortrait?: {width: number, height: number},
|
|
45
|
-
* clamping?: {
|
|
46
|
-
* ui: {min: number, max: number},
|
|
47
|
-
* textBase: {min: number, max: number},
|
|
48
|
-
* textHeading: {min: number, max: number},
|
|
49
|
-
* textUi: {min: number, max: number}
|
|
50
|
-
* },
|
|
51
|
-
* snippetLandscape?:GameBoxSnippet,
|
|
52
|
-
* snippetPortrait?: GameBoxSnippet,
|
|
53
|
-
* snippetRequireFullscreen?: GameBoxSnippet,
|
|
54
|
-
* snippetInstallOnHomeScreen?: GameBoxSnippet,
|
|
55
|
-
* [attr: string]: any
|
|
56
|
-
* }}
|
|
57
|
-
*/
|
|
58
|
-
const {
|
|
59
|
-
// > Style
|
|
60
|
-
base = '',
|
|
61
|
-
bg = '',
|
|
62
|
-
classes = '',
|
|
63
|
-
style = '',
|
|
64
|
-
|
|
65
|
-
// > Functional properties
|
|
66
|
-
aspectOnLandscape,
|
|
67
|
-
aspectOnPortrait,
|
|
68
|
-
|
|
69
|
-
marginLeft = 0,
|
|
70
|
-
marginRight = 0,
|
|
71
|
-
|
|
72
|
-
marginTop = 0,
|
|
73
|
-
marginBottom = 0,
|
|
74
|
-
|
|
75
|
-
center,
|
|
76
|
-
|
|
77
|
-
// > Scaling options
|
|
78
|
-
enableScaling = false,
|
|
79
|
-
designLandscape = { width: 1920, height: 1080 },
|
|
80
|
-
designPortrait = { width: 1920, height: 1080 },
|
|
81
|
-
clamping = {
|
|
82
|
-
ui: { min: 0.3, max: 2 },
|
|
83
|
-
textBase: { min: 0.75, max: 1.5 },
|
|
84
|
-
textHeading: { min: 0.75, max: 2.25 },
|
|
85
|
-
textUi: { min: 0.5, max: 1.25 }
|
|
86
|
-
},
|
|
87
|
-
|
|
88
|
-
// > Snippets
|
|
89
|
-
snippetLandscape,
|
|
90
|
-
snippetPortrait,
|
|
91
|
-
snippetRequireFullscreen,
|
|
92
|
-
snippetInstallOnHomeScreen
|
|
93
|
-
} = $props();
|
|
94
|
-
|
|
95
|
-
// > Game dimensions and state
|
|
96
|
-
let windowWidth = $state();
|
|
97
|
-
let windowHeight = $state();
|
|
98
|
-
|
|
99
|
-
let gameWidth = $state();
|
|
100
|
-
let gameHeight = $state();
|
|
101
|
-
|
|
102
|
-
let iosWindowWidth = $state();
|
|
103
|
-
let iosWindowHeight = $state();
|
|
104
|
-
|
|
105
|
-
function getIsLandscape() {
|
|
106
|
-
if (isPwa && isAppleMobile) {
|
|
107
|
-
return iosWindowWidth > iosWindowHeight;
|
|
108
|
-
} else {
|
|
109
|
-
return windowWidth > windowHeight;
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
let isLandscape = $state();
|
|
114
|
-
|
|
115
|
-
// $derived.by(getIsLandscape);
|
|
116
|
-
|
|
117
|
-
$effect(() => {
|
|
118
|
-
isLandscape = getIsLandscape();
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
// Game container reference
|
|
122
|
-
let gameContainer = $state();
|
|
123
|
-
|
|
124
|
-
// Update game dimensions based on window size and orientation
|
|
125
|
-
$effect(() => {
|
|
126
|
-
const width = iosWindowWidth ?? windowWidth;
|
|
127
|
-
const height = iosWindowHeight ?? windowHeight;
|
|
128
|
-
|
|
129
|
-
if (!width || !height) return;
|
|
130
|
-
|
|
131
|
-
const availWidth = width - marginLeft - marginRight;
|
|
132
|
-
const availHeight = height - marginTop - marginBottom;
|
|
133
|
-
|
|
134
|
-
// console.debug('GameBox margins:', {
|
|
135
|
-
// marginLeft,
|
|
136
|
-
// marginRight,
|
|
137
|
-
// marginTop,
|
|
138
|
-
// marginBottom
|
|
139
|
-
// });
|
|
140
|
-
|
|
141
|
-
let gameAspect;
|
|
142
|
-
|
|
143
|
-
if (availWidth > availHeight) {
|
|
144
|
-
gameWidth = getGameWidthOnLandscape({
|
|
145
|
-
windowWidth: availWidth,
|
|
146
|
-
windowHeight: availHeight,
|
|
147
|
-
aspectOnLandscape
|
|
148
|
-
});
|
|
149
|
-
gameAspect = aspectOnLandscape;
|
|
150
|
-
} else {
|
|
151
|
-
gameWidth = getGameWidthOnPortrait({
|
|
152
|
-
windowWidth: availWidth,
|
|
153
|
-
windowHeight: availHeight,
|
|
154
|
-
aspectOnPortrait
|
|
155
|
-
});
|
|
156
|
-
gameAspect = aspectOnPortrait;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
if (gameAspect) {
|
|
160
|
-
gameHeight = gameWidth / gameAspect;
|
|
161
|
-
} else {
|
|
162
|
-
gameHeight = availHeight;
|
|
163
|
-
}
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
// Set up scaling if enabled, with orientation awareness
|
|
167
|
-
$effect(() => {
|
|
168
|
-
if (!enableScaling || !gameContainer || !gameWidth || !gameHeight) {
|
|
169
|
-
return () => {}; // No-op cleanup if scaling not enabled or required elements missing
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
// Select the appropriate design based on orientation
|
|
173
|
-
const activeDesign = isLandscape ? designLandscape : designPortrait;
|
|
174
|
-
|
|
175
|
-
// console.debug(
|
|
176
|
-
// `GameBox scaling [${isLandscape ? 'landscape' : 'portrait'}]:`,
|
|
177
|
-
// `game: ${gameWidth}x${gameHeight}`,
|
|
178
|
-
// `design: ${activeDesign.width}x${activeDesign.height}`
|
|
179
|
-
// );
|
|
180
|
-
|
|
181
|
-
// Apply scaling with the current design based on orientation
|
|
182
|
-
return enableContainerScaling({
|
|
183
|
-
container: gameContainer,
|
|
184
|
-
design: activeDesign,
|
|
185
|
-
clamping,
|
|
186
|
-
getDimensions: () => ({
|
|
187
|
-
width: gameWidth,
|
|
188
|
-
height: gameHeight
|
|
189
|
-
})
|
|
190
|
-
});
|
|
191
|
-
});
|
|
192
|
-
|
|
193
|
-
let show = $state(false);
|
|
194
|
-
|
|
195
|
-
const isAppleMobile = /iPhone|iPod/.test(navigator.userAgent);
|
|
196
|
-
|
|
197
|
-
let isPwa = $state(false);
|
|
198
|
-
|
|
199
|
-
let os = $state();
|
|
200
|
-
|
|
201
|
-
let isMobile = $state(false);
|
|
202
|
-
|
|
203
|
-
let isDevMode = $state(false);
|
|
204
|
-
|
|
205
|
-
// Check: always true for home app?
|
|
206
|
-
let isFullscreen = $state(false);
|
|
207
|
-
|
|
208
|
-
let supportsFullscreen = $state(false);
|
|
209
|
-
|
|
210
|
-
onMount(() => {
|
|
211
|
-
supportsFullscreen = document.fullscreenEnabled;
|
|
212
|
-
|
|
213
|
-
isMobile = getIsMobile();
|
|
214
|
-
|
|
215
|
-
os = getOS();
|
|
216
|
-
|
|
217
|
-
// Run before show
|
|
218
|
-
isFullscreen = !!document.fullscreenElement;
|
|
219
|
-
|
|
220
|
-
isPwa = window.matchMedia(
|
|
221
|
-
'(display-mode: fullscreen) or (display-mode: standalone)'
|
|
222
|
-
).matches;
|
|
223
|
-
|
|
224
|
-
isLandscape = getIsLandscape();
|
|
225
|
-
|
|
226
|
-
show = true;
|
|
227
|
-
|
|
228
|
-
function updateIosWidthHeight() {
|
|
229
|
-
// const isPwa = window.matchMedia(
|
|
230
|
-
// '(display-mode: fullscreen) or (display-mode: standalone)'
|
|
231
|
-
// ).matches;
|
|
232
|
-
|
|
233
|
-
if (isPwa && isAppleMobile) {
|
|
234
|
-
const angle = screen.orientation.angle;
|
|
235
|
-
|
|
236
|
-
if (angle === 90 || angle === 270) {
|
|
237
|
-
iosWindowWidth = screen.height;
|
|
238
|
-
iosWindowHeight = screen.width;
|
|
239
|
-
} else {
|
|
240
|
-
iosWindowWidth = screen.width;
|
|
241
|
-
iosWindowHeight = screen.height;
|
|
242
|
-
}
|
|
243
|
-
// console.debug( { iosWindowWidth, iosWindowHeight } );
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
updateIosWidthHeight();
|
|
248
|
-
|
|
249
|
-
function updateOrientation(event) {
|
|
250
|
-
// console.debug('updateOrientation');
|
|
251
|
-
const type = event.target.type;
|
|
252
|
-
const angle = event.target.angle;
|
|
253
|
-
|
|
254
|
-
// isPwa = window.matchMedia(
|
|
255
|
-
// '(display-mode: fullscreen) or (display-mode: standalone)'
|
|
256
|
-
// ).matches;
|
|
257
|
-
|
|
258
|
-
updateIosWidthHeight();
|
|
259
|
-
|
|
260
|
-
console.debug(
|
|
261
|
-
`ScreenOrientation change: ${type}, ${angle} degrees.`,
|
|
262
|
-
isPwa,
|
|
263
|
-
windowWidth,
|
|
264
|
-
windowHeight,
|
|
265
|
-
screen.width,
|
|
266
|
-
screen.height,
|
|
267
|
-
iosWindowWidth,
|
|
268
|
-
iosWindowHeight
|
|
269
|
-
);
|
|
270
|
-
|
|
271
|
-
// if( angle
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
$effect(() => {
|
|
275
|
-
screen.orientation.addEventListener('change', updateOrientation);
|
|
276
|
-
|
|
277
|
-
return () => {
|
|
278
|
-
screen.orientation.removeEventListener('change', updateOrientation);
|
|
279
|
-
};
|
|
280
|
-
});
|
|
281
|
-
|
|
282
|
-
//
|
|
283
|
-
});
|
|
284
|
-
|
|
285
|
-
onMount(() => {
|
|
286
|
-
const gameBoxNoScroll = 'game-box-no-scroll';
|
|
287
|
-
const html = document.documentElement;
|
|
288
|
-
html.classList.add(gameBoxNoScroll);
|
|
289
|
-
|
|
290
|
-
return () => {
|
|
291
|
-
html.classList.remove(gameBoxNoScroll);
|
|
292
|
-
};
|
|
293
|
-
});
|
|
294
|
-
|
|
295
|
-
function getOS() {
|
|
296
|
-
if (isAppleMobile) {
|
|
297
|
-
return 'iOS';
|
|
298
|
-
} else if (/Android/.test(navigator.userAgent)) {
|
|
299
|
-
return 'Android';
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
/**
|
|
304
|
-
* Returns true if a device is a mobile phone (or similar)
|
|
305
|
-
*/
|
|
306
|
-
function getIsMobile() {
|
|
307
|
-
// @ts-ignore
|
|
308
|
-
if (navigator?.userAgentData?.mobile !== undefined) {
|
|
309
|
-
// Supports for mobile flag
|
|
310
|
-
// @ts-ignore
|
|
311
|
-
return navigator.userAgentData.mobile;
|
|
312
|
-
} else if (isAppleMobile) {
|
|
313
|
-
return true;
|
|
314
|
-
} else if (/Android/.test(navigator.userAgent)) {
|
|
315
|
-
return true;
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
return false;
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
/**
|
|
322
|
-
* Returns true if the window is in full screen
|
|
323
|
-
* - Checks if CSS thinks we're in fullscreen mode
|
|
324
|
-
* - Checks if there is a fullscreen element (for safari)
|
|
325
|
-
*/
|
|
326
|
-
function getIsFullscreen() {
|
|
327
|
-
if (
|
|
328
|
-
window.matchMedia(
|
|
329
|
-
'(display-mode: fullscreen) or (display-mode: standalone)'
|
|
330
|
-
).matches
|
|
331
|
-
) {
|
|
332
|
-
return true;
|
|
333
|
-
} else if (document.fullscreenElement) {
|
|
334
|
-
// Safari
|
|
335
|
-
return true;
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
return false;
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
async function requestFullscreen() {
|
|
342
|
-
console.debug('Request full screen');
|
|
343
|
-
show = false;
|
|
344
|
-
|
|
345
|
-
await document.documentElement.requestFullscreen();
|
|
346
|
-
isFullscreen = true;
|
|
347
|
-
|
|
348
|
-
setTimeout(() => {
|
|
349
|
-
show = true;
|
|
350
|
-
}, 1000);
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
// async function exitFullscreen() {
|
|
354
|
-
// console.debug('Exit full screen');
|
|
355
|
-
// show = false;
|
|
356
|
-
|
|
357
|
-
// await document.exitFullscreen();
|
|
358
|
-
// isFullscreen = false;
|
|
359
|
-
|
|
360
|
-
// setTimeout( () => { show = true; }, 1000 );
|
|
361
|
-
// }
|
|
362
|
-
|
|
363
|
-
$effect(() => {
|
|
364
|
-
// Update isFullscreen if window width or height changes
|
|
365
|
-
|
|
366
|
-
windowWidth;
|
|
367
|
-
windowHeight;
|
|
368
|
-
|
|
369
|
-
isFullscreen = getIsFullscreen();
|
|
370
|
-
|
|
371
|
-
// if( !isFullscreen )
|
|
372
|
-
// {
|
|
373
|
-
// show = false;
|
|
374
|
-
// setTimeout( () => { show = true; }, 1000 );
|
|
375
|
-
// }
|
|
376
|
-
|
|
377
|
-
// console.debug('isFullscreen', isFullscreen);
|
|
378
|
-
});
|
|
379
|
-
|
|
380
|
-
isDevMode = false;
|
|
381
|
-
|
|
382
|
-
function requestDevmode() {
|
|
383
|
-
isDevMode = true;
|
|
384
|
-
console.debug(isDevMode);
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
$effect(() => {
|
|
388
|
-
if (location.hostname === 'localhost') {
|
|
389
|
-
isDevMode = true;
|
|
390
|
-
}
|
|
391
|
-
});
|
|
392
|
-
|
|
393
|
-
$effect(() => {
|
|
394
|
-
if (isFullscreen) {
|
|
395
|
-
const url = new URL(window.location.href);
|
|
396
|
-
url.searchParams.set('preset', 'cinema');
|
|
397
|
-
window.history.pushState({}, '', url);
|
|
398
|
-
}
|
|
399
|
-
});
|
|
400
|
-
</script>
|
|
401
|
-
|
|
402
|
-
<svelte:window bind:innerWidth={windowWidth} bind:innerHeight={windowHeight} />
|
|
403
|
-
|
|
404
|
-
{#if gameHeight}
|
|
405
|
-
<div class:center>
|
|
406
|
-
<div
|
|
407
|
-
data-component="game-box"
|
|
408
|
-
data-orientation={isLandscape ? 'landscape' : 'portrait'}
|
|
409
|
-
bind:this={gameContainer}
|
|
410
|
-
class="{base} {bg} {classes}"
|
|
411
|
-
class:isMobile
|
|
412
|
-
style:width="{gameWidth}px"
|
|
413
|
-
style:height="{gameHeight}px"
|
|
414
|
-
style:--game-width={gameWidth}
|
|
415
|
-
style:--game-height={gameHeight}
|
|
416
|
-
style:margin-left="{marginLeft}px"
|
|
417
|
-
style:margin-right="{marginRight}px"
|
|
418
|
-
style:margin-top="{marginTop}px"
|
|
419
|
-
style:margin-bottom="{marginBottom}px"
|
|
420
|
-
{style}
|
|
421
|
-
>
|
|
422
|
-
{#if show}
|
|
423
|
-
{#if isLandscape}
|
|
424
|
-
<!-- Landscape -->
|
|
425
|
-
{#if snippetRequireFullscreen}
|
|
426
|
-
<!-- Require fullscreen -->
|
|
427
|
-
{#if isFullscreen && !isDevMode}
|
|
428
|
-
{@render snippetLandscape({
|
|
429
|
-
isMobile,
|
|
430
|
-
os,
|
|
431
|
-
isFullscreen,
|
|
432
|
-
isDevMode,
|
|
433
|
-
requestDevmode,
|
|
434
|
-
requestFullscreen,
|
|
435
|
-
gameWidth,
|
|
436
|
-
gameHeight
|
|
437
|
-
})}
|
|
438
|
-
{:else if supportsFullscreen && !isDevMode}
|
|
439
|
-
<!-- Require fullscreen (on landscape) -->
|
|
440
|
-
{@render snippetRequireFullscreen({
|
|
441
|
-
isMobile,
|
|
442
|
-
os,
|
|
443
|
-
isFullscreen,
|
|
444
|
-
isDevMode,
|
|
445
|
-
requestDevmode,
|
|
446
|
-
requestFullscreen,
|
|
447
|
-
gameWidth,
|
|
448
|
-
gameHeight
|
|
449
|
-
})}
|
|
450
|
-
{:else if isMobile && snippetInstallOnHomeScreen && !isDevMode}
|
|
451
|
-
<!-- Require install on home screen on mobile -->
|
|
452
|
-
{@render snippetInstallOnHomeScreen({
|
|
453
|
-
isMobile,
|
|
454
|
-
os,
|
|
455
|
-
isFullscreen,
|
|
456
|
-
isDevMode,
|
|
457
|
-
requestDevmode,
|
|
458
|
-
requestFullscreen,
|
|
459
|
-
gameWidth,
|
|
460
|
-
gameHeight
|
|
461
|
-
})}
|
|
462
|
-
{:else}
|
|
463
|
-
{@render snippetLandscape({
|
|
464
|
-
isMobile,
|
|
465
|
-
os,
|
|
466
|
-
isFullscreen,
|
|
467
|
-
isDevMode,
|
|
468
|
-
requestDevmode,
|
|
469
|
-
requestFullscreen,
|
|
470
|
-
gameWidth,
|
|
471
|
-
gameHeight
|
|
472
|
-
})}
|
|
473
|
-
{/if}
|
|
474
|
-
{:else}
|
|
475
|
-
<!-- Do not require fullscreen -->
|
|
476
|
-
<!-- *we do not try install home app -->
|
|
477
|
-
{@render snippetLandscape({
|
|
478
|
-
isMobile,
|
|
479
|
-
os,
|
|
480
|
-
isFullscreen,
|
|
481
|
-
isDevMode,
|
|
482
|
-
requestDevmode,
|
|
483
|
-
requestFullscreen,
|
|
484
|
-
gameWidth,
|
|
485
|
-
gameHeight
|
|
486
|
-
})}
|
|
487
|
-
{/if}
|
|
488
|
-
{:else}
|
|
489
|
-
<!-- Portrait -->
|
|
490
|
-
{#if snippetRequireFullscreen}
|
|
491
|
-
<!-- Require fullscreen -->
|
|
492
|
-
{#if isFullscreen && !isDevMode}
|
|
493
|
-
{@render snippetPortrait({
|
|
494
|
-
isMobile,
|
|
495
|
-
os,
|
|
496
|
-
isFullscreen,
|
|
497
|
-
isDevMode,
|
|
498
|
-
requestDevmode,
|
|
499
|
-
requestFullscreen,
|
|
500
|
-
gameWidth,
|
|
501
|
-
gameHeight
|
|
502
|
-
})}
|
|
503
|
-
{:else if supportsFullscreen && !isDevMode}
|
|
504
|
-
<!-- Require fullscreen (on landscape) -->
|
|
505
|
-
{@render snippetRequireFullscreen({
|
|
506
|
-
isMobile,
|
|
507
|
-
os,
|
|
508
|
-
isFullscreen,
|
|
509
|
-
isDevMode,
|
|
510
|
-
requestDevmode,
|
|
511
|
-
requestFullscreen,
|
|
512
|
-
gameWidth,
|
|
513
|
-
gameHeight
|
|
514
|
-
})}
|
|
515
|
-
{:else if isMobile && snippetInstallOnHomeScreen && !isDevMode}
|
|
516
|
-
<!-- Require install on home screen on mobile -->
|
|
517
|
-
{@render snippetInstallOnHomeScreen({
|
|
518
|
-
isMobile,
|
|
519
|
-
os,
|
|
520
|
-
isFullscreen,
|
|
521
|
-
isDevMode,
|
|
522
|
-
requestDevmode,
|
|
523
|
-
requestFullscreen,
|
|
524
|
-
gameWidth,
|
|
525
|
-
gameHeight
|
|
526
|
-
})}
|
|
527
|
-
{:else}
|
|
528
|
-
{@render snippetPortrait({
|
|
529
|
-
isMobile,
|
|
530
|
-
os,
|
|
531
|
-
isFullscreen,
|
|
532
|
-
isDevMode,
|
|
533
|
-
requestDevmode,
|
|
534
|
-
requestFullscreen,
|
|
535
|
-
gameWidth,
|
|
536
|
-
gameHeight
|
|
537
|
-
})}
|
|
538
|
-
{/if}
|
|
539
|
-
{:else}
|
|
540
|
-
<!-- Do not require fullscreen -->
|
|
541
|
-
<!-- *we do not try install home app -->
|
|
542
|
-
{@render snippetPortrait({
|
|
543
|
-
isMobile,
|
|
544
|
-
os,
|
|
545
|
-
isFullscreen,
|
|
546
|
-
isDevMode,
|
|
547
|
-
requestDevmode,
|
|
548
|
-
requestFullscreen,
|
|
549
|
-
gameWidth,
|
|
550
|
-
gameHeight
|
|
551
|
-
})}
|
|
552
|
-
{/if}
|
|
553
|
-
{/if}
|
|
554
|
-
{/if}
|
|
555
|
-
</div>
|
|
556
|
-
</div>
|
|
557
|
-
{/if}
|
|
558
|
-
|
|
559
|
-
<style>
|
|
560
|
-
.center {
|
|
561
|
-
display: grid;
|
|
562
|
-
height: 100lvh;
|
|
563
|
-
display: grid;
|
|
564
|
-
justify-items: center;
|
|
565
|
-
align-items: center;
|
|
566
|
-
/* border: solid 1px red;*/
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
:global(html.game-box-no-scroll) {
|
|
570
|
-
overflow: clip;
|
|
571
|
-
scrollbar-width: none; /* Firefox */
|
|
572
|
-
-ms-overflow-style: none; /* IE and Edge */
|
|
573
|
-
}
|
|
574
|
-
:global(html.game-box-no-scroll::-webkit-scrollbar) {
|
|
575
|
-
display: none;
|
|
576
|
-
}
|
|
577
|
-
</style>
|