@dloizides/game-shell 0.1.0

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/dist/index.js ADDED
@@ -0,0 +1,583 @@
1
+ 'use strict';
2
+
3
+ // src/crash/crashKind.ts
4
+ var CRASH_KIND = {
5
+ Error: "error",
6
+ Rejection: "rejection"
7
+ };
8
+
9
+ // src/dom.ts
10
+ function el(doc, tag, className = "", text = "") {
11
+ const node = doc.createElement(tag);
12
+ if (className !== "") {
13
+ node.className = className;
14
+ }
15
+ if (text !== "") {
16
+ node.textContent = text;
17
+ }
18
+ return node;
19
+ }
20
+ function iconSpan(doc, svg2) {
21
+ const span = el(doc, "span", "gs-ico");
22
+ span.innerHTML = svg2;
23
+ return span;
24
+ }
25
+ function button(doc, spec) {
26
+ const btn = el(doc, "button", spec.className);
27
+ btn.type = "button";
28
+ if (spec.icon !== void 0) {
29
+ btn.append(iconSpan(doc, spec.icon));
30
+ }
31
+ if (spec.text !== void 0) {
32
+ btn.append(doc.createTextNode(spec.text));
33
+ }
34
+ if (spec.ariaLabel !== void 0) {
35
+ btn.setAttribute("aria-label", spec.ariaLabel);
36
+ }
37
+ btn.addEventListener("click", () => spec.onClick());
38
+ return btn;
39
+ }
40
+
41
+ // src/crash/crashOverlay.ts
42
+ var CRASH_TITLE_ID = "gs-crash-title";
43
+ var CRASH_BODY_ID = "gs-crash-body";
44
+ function renderCrashOverlay(p) {
45
+ const root = el(p.doc, "div", "gs gs-crash");
46
+ root.setAttribute("role", "alertdialog");
47
+ root.setAttribute("aria-modal", "true");
48
+ root.setAttribute("aria-labelledby", CRASH_TITLE_ID);
49
+ root.setAttribute("aria-describedby", CRASH_BODY_ID);
50
+ const panel = el(p.doc, "div", "gs-panel");
51
+ const title = el(p.doc, "h2", "gs-h", p.labels.crashTitle);
52
+ title.id = CRASH_TITLE_ID;
53
+ const body = el(p.doc, "p", "", p.labels.crashBody);
54
+ body.id = CRASH_BODY_ID;
55
+ const acts = el(p.doc, "div", "gs-acts");
56
+ const reload = button(p.doc, { className: "gs-btn gs-primary", text: p.labels.reload, onClick: p.onReload });
57
+ acts.append(reload, button(p.doc, { className: "gs-btn gs-ghost", text: p.labels.copyDetails, onClick: p.onCopy }));
58
+ panel.append(title, body, acts);
59
+ if (p.reference !== void 0 && p.reference !== "") {
60
+ panel.append(el(p.doc, "p", "gs-ref", `${p.labels.errorReported} ${p.reference}`));
61
+ }
62
+ root.append(panel);
63
+ return { root, reload };
64
+ }
65
+
66
+ // src/crash/crash.ts
67
+ function fromErrorEvent(e) {
68
+ const err = e.error;
69
+ const stack = err instanceof Error ? err.stack : void 0;
70
+ const location = e.filename !== "" ? `${e.filename}:${e.lineno}:${e.colno}` : void 0;
71
+ const message = e.message !== "" ? e.message : String(err);
72
+ return { kind: CRASH_KIND.Error, message, source: stack ?? location };
73
+ }
74
+ function fromRejection(e) {
75
+ const reason = e.reason;
76
+ if (reason instanceof Error) {
77
+ return { kind: CRASH_KIND.Rejection, message: reason.message, source: reason.stack };
78
+ }
79
+ return { kind: CRASH_KIND.Rejection, message: String(reason) };
80
+ }
81
+ function safeReport(opts, info) {
82
+ try {
83
+ return opts.report?.(info) ?? void 0;
84
+ } catch {
85
+ return void 0;
86
+ }
87
+ }
88
+ function copyDetails(win, info, appVersion) {
89
+ const text = [info.message, info.source, appVersion].filter((part) => part !== void 0 && part !== "").join("\n");
90
+ try {
91
+ win.navigator.clipboard.writeText(text).catch(() => void 0);
92
+ } catch {
93
+ }
94
+ }
95
+ function startCrash(opts) {
96
+ const win = opts.win ?? window;
97
+ const reload = opts.reload ?? (() => win.location.reload());
98
+ let overlay = null;
99
+ const handle = (info) => {
100
+ const reference = safeReport(opts, info);
101
+ if (overlay) {
102
+ return;
103
+ }
104
+ const view = renderCrashOverlay({
105
+ doc: win.document,
106
+ labels: opts.labels,
107
+ reference,
108
+ onReload: reload,
109
+ onCopy: () => copyDetails(win, info, opts.appVersion)
110
+ });
111
+ overlay = view.root;
112
+ win.document.body.append(overlay);
113
+ view.reload.focus();
114
+ };
115
+ const onError = (e) => {
116
+ if (e instanceof ErrorEvent) {
117
+ handle(fromErrorEvent(e));
118
+ }
119
+ };
120
+ const onRejection = (e) => handle(fromRejection(e));
121
+ win.addEventListener("error", onError);
122
+ win.addEventListener("unhandledrejection", onRejection);
123
+ return {
124
+ dispose: () => {
125
+ win.removeEventListener("error", onError);
126
+ win.removeEventListener("unhandledrejection", onRejection);
127
+ overlay?.remove();
128
+ overlay = null;
129
+ }
130
+ };
131
+ }
132
+
133
+ // src/fit/fit.ts
134
+ var GS_RESIZE_EVENT = "gs:resize";
135
+ var GS_VH_VAR = "--gs-vh";
136
+ function measure(win) {
137
+ const vv = win.visualViewport;
138
+ if (vv) {
139
+ return { width: vv.width, height: vv.height };
140
+ }
141
+ return { width: win.innerWidth, height: win.innerHeight };
142
+ }
143
+ function scheduler(win) {
144
+ if (typeof win.requestAnimationFrame === "function") {
145
+ return (cb) => {
146
+ win.requestAnimationFrame(() => cb());
147
+ };
148
+ }
149
+ return (cb) => {
150
+ win.setTimeout(cb, 0);
151
+ };
152
+ }
153
+ function startFit(win = window) {
154
+ const root = win.document.documentElement;
155
+ const schedule = scheduler(win);
156
+ let last = measure(win);
157
+ let pending = false;
158
+ let disposed = false;
159
+ root.style.setProperty(GS_VH_VAR, `${last.height}px`);
160
+ const apply = () => {
161
+ pending = false;
162
+ const next = measure(win);
163
+ const unchanged = next.width === last.width && next.height === last.height;
164
+ if (disposed || unchanged) {
165
+ return;
166
+ }
167
+ last = next;
168
+ root.style.setProperty(GS_VH_VAR, `${next.height}px`);
169
+ win.dispatchEvent(new CustomEvent(GS_RESIZE_EVENT, { detail: next }));
170
+ };
171
+ const onChange = () => {
172
+ if (pending) {
173
+ return;
174
+ }
175
+ pending = true;
176
+ schedule(apply);
177
+ };
178
+ const vv = win.visualViewport;
179
+ vv?.addEventListener("resize", onChange);
180
+ win.addEventListener("resize", onChange);
181
+ win.addEventListener("orientationchange", onChange);
182
+ return {
183
+ dispose: () => {
184
+ disposed = true;
185
+ vv?.removeEventListener("resize", onChange);
186
+ win.removeEventListener("resize", onChange);
187
+ win.removeEventListener("orientationchange", onChange);
188
+ }
189
+ };
190
+ }
191
+
192
+ // src/icons.ts
193
+ var svg = (paths) => `<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">${paths}</svg>`;
194
+ var ICONS = {
195
+ download: svg('<path d="M12 4v11M7 10l5 5 5-5M5 20h14"/>'),
196
+ add: svg('<path d="M12 5v14M5 12h14"/>'),
197
+ close: svg('<path d="M6 6l12 12M18 6L6 18"/>'),
198
+ share: svg('<path d="M12 15V4M8 8l4-4 4 4M6 12v8h12v-8"/>')
199
+ };
200
+
201
+ // src/install/banner.ts
202
+ var STEPS_ID = "gs-install-steps";
203
+ function renderSteps(p) {
204
+ const list = el(p.doc, "ol", "gs-steps");
205
+ list.id = STEPS_ID;
206
+ const share = el(p.doc, "li");
207
+ share.append(iconSpan(p.doc, ICONS.share), p.doc.createTextNode(p.labels.tapShare));
208
+ const add = el(p.doc, "li");
209
+ add.append(iconSpan(p.doc, ICONS.add), p.doc.createTextNode(p.labels.tapAdd));
210
+ list.append(share, add);
211
+ return list;
212
+ }
213
+ function closeButton(p) {
214
+ return button(p.doc, { className: "gs-x", ariaLabel: p.labels.notNow, icon: ICONS.close, onClick: p.onDismiss });
215
+ }
216
+ function renderChip(p) {
217
+ const seal = el(p.doc, "div", "gs-seal");
218
+ if (p.variant === "prompt" /* Prompt */) {
219
+ seal.append(button(p.doc, { className: "gs-chip", text: p.labels.install, icon: ICONS.download, onClick: p.onInstall }));
220
+ seal.append(closeButton(p));
221
+ return [seal];
222
+ }
223
+ const steps = renderSteps(p);
224
+ steps.hidden = true;
225
+ const chip = button(p.doc, {
226
+ className: "gs-chip",
227
+ text: p.labels.addToHome,
228
+ icon: ICONS.add,
229
+ onClick: () => {
230
+ steps.hidden = !steps.hidden;
231
+ chip.setAttribute("aria-expanded", String(!steps.hidden));
232
+ }
233
+ });
234
+ chip.setAttribute("aria-expanded", "false");
235
+ chip.setAttribute("aria-controls", STEPS_ID);
236
+ seal.append(chip, closeButton(p));
237
+ return [seal, steps];
238
+ }
239
+ function renderCard(p) {
240
+ const card = el(p.doc, "div", "gs-card");
241
+ const app = el(p.doc, "div", "gs-app");
242
+ app.append(el(p.doc, "i"));
243
+ const txt = el(p.doc, "div", "gs-txt");
244
+ txt.append(el(p.doc, "b", "", p.appName), el(p.doc, "span", "", p.labels.installBody));
245
+ const acts = el(p.doc, "div", "gs-acts");
246
+ if (p.variant === "prompt" /* Prompt */) {
247
+ acts.append(button(p.doc, { className: "gs-btn gs-primary", text: p.labels.install, icon: ICONS.download, onClick: p.onInstall }));
248
+ }
249
+ acts.append(button(p.doc, { className: "gs-btn gs-ghost", text: p.labels.notNow, onClick: p.onDismiss }));
250
+ card.append(closeButton(p), app, txt);
251
+ if (p.variant === "ios" /* Ios */) {
252
+ card.append(renderSteps(p));
253
+ }
254
+ card.append(acts);
255
+ return [card];
256
+ }
257
+ function renderBanner(p) {
258
+ const wrap = el(p.doc, "div", "gs gs-install");
259
+ wrap.dataset.layout = p.layout;
260
+ wrap.dataset.variant = p.variant;
261
+ wrap.setAttribute("role", "region");
262
+ wrap.setAttribute("aria-label", p.variant === "prompt" /* Prompt */ ? p.labels.install : p.labels.addToHome);
263
+ wrap.addEventListener("keydown", (e) => {
264
+ if (e.key === "Escape") {
265
+ p.onDismiss();
266
+ }
267
+ });
268
+ wrap.append(...p.layout === "card" /* Card */ ? renderCard(p) : renderChip(p));
269
+ return wrap;
270
+ }
271
+
272
+ // src/install/platform.ts
273
+ var DESKTOP_MIN_WIDTH = 1024;
274
+ var IOS_DEVICE_RE = /iPad|iPhone|iPod/;
275
+ var IOS_OTHER_BROWSER_RE = /CriOS|FxiOS|EdgiOS|OPiOS/;
276
+ function isStandalone(win) {
277
+ const media = typeof win.matchMedia === "function" && win.matchMedia("(display-mode: standalone)").matches;
278
+ return media || win.navigator.standalone === true;
279
+ }
280
+ function isIosSafari(win) {
281
+ const ua = win.navigator.userAgent;
282
+ const iPadAsMac = ua.includes("Macintosh") && win.navigator.maxTouchPoints > 1;
283
+ const iosDevice = IOS_DEVICE_RE.test(ua) || iPadAsMac;
284
+ return iosDevice && !IOS_OTHER_BROWSER_RE.test(ua);
285
+ }
286
+ function isDesktop(win) {
287
+ return win.innerWidth >= DESKTOP_MIN_WIDTH;
288
+ }
289
+
290
+ // src/install/snooze.ts
291
+ var DAY_MS = 864e5;
292
+ function snoozeKey(appName, override) {
293
+ return override ?? `gs-install-snooze:${appName}`;
294
+ }
295
+ function isSnoozed(win, key, days, now) {
296
+ try {
297
+ const raw = win.localStorage.getItem(key);
298
+ if (raw === null) {
299
+ return false;
300
+ }
301
+ const at = Number(raw);
302
+ return Number.isFinite(at) && now - at < days * DAY_MS;
303
+ } catch {
304
+ return false;
305
+ }
306
+ }
307
+ function writeSnooze(win, key, now) {
308
+ try {
309
+ win.localStorage.setItem(key, String(now));
310
+ } catch {
311
+ }
312
+ }
313
+
314
+ // src/install/install.ts
315
+ var DEFAULT_SNOOZE_DAYS = 14;
316
+ var ACCEPTED = "accepted";
317
+ function createController(opts, win, now) {
318
+ const key = snoozeKey(opts.appName, opts.storageKey);
319
+ const state = { deferred: null, variant: null, node: null };
320
+ const unmount = () => {
321
+ state.node?.remove();
322
+ state.node = null;
323
+ };
324
+ const dismiss = () => {
325
+ writeSnooze(win, key, now());
326
+ unmount();
327
+ };
328
+ const install = async () => {
329
+ const evt = state.deferred;
330
+ state.deferred = null;
331
+ if (!evt) {
332
+ return;
333
+ }
334
+ try {
335
+ await evt.prompt();
336
+ const choice = await evt.userChoice;
337
+ if (choice.outcome === ACCEPTED) {
338
+ unmount();
339
+ return;
340
+ }
341
+ dismiss();
342
+ } catch {
343
+ unmount();
344
+ }
345
+ };
346
+ const mount = () => {
347
+ if (state.variant === null) {
348
+ return;
349
+ }
350
+ unmount();
351
+ state.node = renderBanner({
352
+ doc: win.document,
353
+ layout: isDesktop(win) ? "card" /* Card */ : "chip" /* Chip */,
354
+ variant: state.variant,
355
+ appName: opts.appName,
356
+ labels: opts.labels,
357
+ onInstall: () => void install(),
358
+ onDismiss: dismiss
359
+ });
360
+ win.document.body.append(state.node);
361
+ };
362
+ return { state, mount, unmount };
363
+ }
364
+ function startInstall(opts) {
365
+ const win = opts.win ?? window;
366
+ const now = opts.now ?? (() => Date.now());
367
+ const key = snoozeKey(opts.appName, opts.storageKey);
368
+ if (isStandalone(win) || isSnoozed(win, key, opts.snoozeDays ?? DEFAULT_SNOOZE_DAYS, now())) {
369
+ return { dispose: () => void 0 };
370
+ }
371
+ const ctl = createController(opts, win, now);
372
+ const onPrompt = (e) => {
373
+ e.preventDefault();
374
+ ctl.state.deferred = e;
375
+ ctl.state.variant = "prompt" /* Prompt */;
376
+ ctl.mount();
377
+ };
378
+ const onInstalled = () => {
379
+ ctl.state.variant = null;
380
+ ctl.unmount();
381
+ };
382
+ const onResize = () => {
383
+ const node = ctl.state.node;
384
+ if (node && node.dataset.layout === "card" /* Card */ !== isDesktop(win)) {
385
+ ctl.mount();
386
+ }
387
+ };
388
+ win.addEventListener("beforeinstallprompt", onPrompt);
389
+ win.addEventListener("appinstalled", onInstalled);
390
+ win.addEventListener("resize", onResize);
391
+ if (isIosSafari(win)) {
392
+ ctl.state.variant = "ios" /* Ios */;
393
+ ctl.mount();
394
+ }
395
+ return {
396
+ dispose: () => {
397
+ win.removeEventListener("beforeinstallprompt", onPrompt);
398
+ win.removeEventListener("appinstalled", onInstalled);
399
+ win.removeEventListener("resize", onResize);
400
+ ctl.unmount();
401
+ }
402
+ };
403
+ }
404
+
405
+ // src/lifecycle/lifecycle.ts
406
+ var REDUCED_MOTION_QUERY = "(prefers-reduced-motion: reduce)";
407
+ var UNLOCK_EVENTS = ["pointerdown", "keydown"];
408
+ function startLifecycle(opts, doc = document) {
409
+ const onChange = () => {
410
+ if (doc.visibilityState === "hidden") {
411
+ opts.onHidden?.();
412
+ return;
413
+ }
414
+ opts.onVisible?.();
415
+ };
416
+ doc.addEventListener("visibilitychange", onChange);
417
+ return { dispose: () => doc.removeEventListener("visibilitychange", onChange) };
418
+ }
419
+ function unlockAudio(ctx, win = window) {
420
+ const detach = () => {
421
+ UNLOCK_EVENTS.forEach((type) => win.removeEventListener(type, onFirst, true));
422
+ };
423
+ function onFirst() {
424
+ detach();
425
+ try {
426
+ Promise.resolve(ctx.resume()).catch(() => void 0);
427
+ } catch {
428
+ }
429
+ }
430
+ UNLOCK_EVENTS.forEach((type) => win.addEventListener(type, onFirst, true));
431
+ return { dispose: detach };
432
+ }
433
+ function prefersReducedMotion(win = window) {
434
+ if (new URLSearchParams(win.location.search).get("reducedMotion") === "1") {
435
+ return true;
436
+ }
437
+ return typeof win.matchMedia === "function" && win.matchMedia(REDUCED_MOTION_QUERY).matches;
438
+ }
439
+
440
+ // src/styles.ts
441
+ var STYLE_ID = "gs-styles";
442
+ var THEME = `:where(:root){--gs-font-display:system-ui,'Segoe UI',sans-serif;--gs-font-text:system-ui,'Segoe UI',sans-serif;--gs-surface:rgba(24,26,29,.95);--gs-surface-solid:#181a1d;--gs-edge:#41464d;--gs-ink:#f3f3f1;--gs-ink-dim:#b9bdc2;--gs-accent:#f3f3f1;--gs-on-accent:#181a1d;--gs-scrim:rgba(0,0,0,.72);--gs-radius:12px;--gs-radius-pill:12px;--gs-focus:#9cc2ff;--gs-safe-top:env(safe-area-inset-top,0px);--gs-safe-right:env(safe-area-inset-right,0px);--gs-safe-bottom:env(safe-area-inset-bottom,0px);--gs-safe-left:env(safe-area-inset-left,0px)}`;
443
+ var ROOT = `.gs-root{width:100%;height:100vh;height:100dvh;height:var(--gs-vh,100dvh)}`;
444
+ var BASE = `.gs{font-family:var(--gs-font-text);color:var(--gs-ink);box-sizing:border-box}
445
+ .gs *{box-sizing:border-box}
446
+ .gs button{min-width:44px;min-height:44px;margin:0;border:0;background:none;color:inherit;font:inherit;cursor:pointer;-webkit-tap-highlight-color:transparent}
447
+ .gs button:focus-visible{outline:2px solid var(--gs-focus);outline-offset:2px}
448
+ .gs svg{width:20px;height:20px;flex:none}
449
+ .gs-ico{display:inline-flex}
450
+ .gs-btn{display:inline-flex;align-items:center;justify-content:center;gap:8px;padding:0 18px;border-radius:var(--gs-radius);font:700 16px/1 var(--gs-font-text);white-space:nowrap}
451
+ .gs-primary{background:var(--gs-accent);color:var(--gs-on-accent)}
452
+ .gs .gs-ghost{color:var(--gs-ink);border:1px solid var(--gs-edge)}
453
+ .gs-x{width:44px;height:44px;display:grid;place-items:center;color:var(--gs-ink-dim);flex:none}
454
+ .gs-txt{min-width:0}
455
+ .gs-txt b{display:block;font:400 18px/1.2 var(--gs-font-display);color:var(--gs-ink)}
456
+ .gs-txt span{display:block;font-size:14px;line-height:1.4;color:var(--gs-ink-dim);margin-top:3px}`;
457
+ var INSTALL = `.gs-install{position:fixed;z-index:2147483000;top:calc(12px + var(--gs-safe-top));left:calc(12px + var(--gs-safe-left))}
458
+ .gs-install[data-layout=card]{top:auto;left:auto;right:calc(24px + var(--gs-safe-right));bottom:calc(24px + var(--gs-safe-bottom))}
459
+ .gs-seal{display:flex;align-items:center;background:var(--gs-surface);border:1px solid var(--gs-edge);border-radius:var(--gs-radius-pill)}
460
+ .gs-chip{display:flex;align-items:center;gap:8px;padding:0 6px 0 14px;font:700 15px var(--gs-font-text);color:var(--gs-ink)}
461
+ .gs-chip svg{color:var(--gs-accent)}
462
+ .gs-seal .gs-x{border-left:1px solid var(--gs-edge)}
463
+ .gs-steps{margin:8px 0 0;width:262px;padding:10px 14px;list-style:none;background:var(--gs-surface);border:1px solid var(--gs-edge);border-radius:var(--gs-radius);display:grid;gap:2px;counter-reset:s}
464
+ .gs-steps[hidden]{display:none}
465
+ .gs-steps li{display:flex;align-items:center;gap:10px;min-height:36px;font-size:15px;color:var(--gs-ink);counter-increment:s}
466
+ .gs-steps li::before{content:counter(s);font:400 15px var(--gs-font-display);color:var(--gs-ink-dim);width:12px}
467
+ .gs-steps svg{width:17px;height:17px;color:var(--gs-accent)}
468
+ .gs-card{position:relative;width:344px;max-width:calc(100vw - 48px);padding:18px;border-radius:var(--gs-radius);background:var(--gs-surface);border:1px solid var(--gs-edge);display:grid;gap:12px}
469
+ .gs-card .gs-x{position:absolute;top:4px;right:4px}
470
+ .gs-card .gs-txt{padding-right:36px}
471
+ .gs-card .gs-acts{display:flex;gap:8px}
472
+ .gs-card .gs-steps{width:auto;margin:0;border:0;padding:0;background:none}
473
+ .gs-app{width:40px;height:40px;border-radius:var(--gs-radius);background:var(--gs-surface-solid);border:1px solid var(--gs-edge);display:grid;place-items:center}
474
+ .gs-app i{width:13px;height:13px;border-radius:50%;background:var(--gs-accent);box-shadow:0 0 10px var(--gs-accent)}`;
475
+ var CRASH = `.gs-crash{position:fixed;inset:0;z-index:2147483001;background:var(--gs-scrim);display:grid;place-items:center;padding:20px}
476
+ .gs-panel{max-width:340px;background:var(--gs-surface-solid);border:1px solid var(--gs-edge);border-radius:var(--gs-radius);padding:24px}
477
+ .gs-h{margin:0;font:400 28px/1.15 var(--gs-font-display);color:var(--gs-ink)}
478
+ .gs-panel p{font-size:16px;line-height:1.5;color:var(--gs-ink-dim);margin:10px 0 0}
479
+ .gs-panel .gs-acts{display:flex;flex-direction:column;gap:8px;margin-top:20px}
480
+ .gs-panel .gs-ref{font:13px ui-monospace,monospace}
481
+ @media (min-width:1024px){.gs-panel{max-width:460px;padding:32px}.gs-panel .gs-acts{flex-direction:row}}`;
482
+ var GAME_SHELL_CSS = [THEME, ROOT, BASE, INSTALL, CRASH].join("\n");
483
+ function injectStyles(doc = document) {
484
+ if (doc.getElementById(STYLE_ID)) {
485
+ return;
486
+ }
487
+ const style = doc.createElement("style");
488
+ style.id = STYLE_ID;
489
+ style.textContent = GAME_SHELL_CSS;
490
+ doc.head.append(style);
491
+ }
492
+
493
+ // src/version/versionPoll.ts
494
+ function startVersionPoll(opts) {
495
+ const win = opts.win ?? window;
496
+ const fetchFn = opts.fetchFn ?? ((input, init) => win.fetch(input, init));
497
+ let baseline = null;
498
+ let fired = false;
499
+ let busy = false;
500
+ const check = async () => {
501
+ if (fired || busy) {
502
+ return;
503
+ }
504
+ busy = true;
505
+ try {
506
+ const res = await fetchFn(opts.url, { cache: "no-store" });
507
+ if (!res.ok) {
508
+ return;
509
+ }
510
+ const value = (await res.text()).trim();
511
+ if (baseline === null) {
512
+ baseline = value;
513
+ } else if (value !== baseline) {
514
+ fired = true;
515
+ opts.onNewVersion();
516
+ }
517
+ } catch {
518
+ } finally {
519
+ busy = false;
520
+ }
521
+ };
522
+ const onVisibility = () => {
523
+ if (win.document.visibilityState === "visible") {
524
+ void check();
525
+ }
526
+ };
527
+ void check();
528
+ const timer = win.setInterval(() => void check(), opts.intervalMs);
529
+ win.document.addEventListener("visibilitychange", onVisibility);
530
+ return {
531
+ dispose: () => {
532
+ win.clearInterval(timer);
533
+ win.document.removeEventListener("visibilitychange", onVisibility);
534
+ }
535
+ };
536
+ }
537
+
538
+ // src/initGameShell.ts
539
+ function initGameShell(opts) {
540
+ const doc = opts.root.ownerDocument;
541
+ injectStyles(doc);
542
+ opts.root.classList.add("gs-root");
543
+ const parts = [startFit()];
544
+ if (opts.install !== false) {
545
+ parts.push(startInstall({ appName: opts.appName, labels: opts.labels, ...opts.install }));
546
+ }
547
+ if (opts.crash !== false) {
548
+ parts.push(startCrash({ labels: opts.labels, report: opts.crash?.report, appVersion: opts.appVersion }));
549
+ }
550
+ if (opts.lifecycle) {
551
+ parts.push(startLifecycle(opts.lifecycle, doc));
552
+ }
553
+ if (opts.version) {
554
+ parts.push(startVersionPoll(opts.version));
555
+ }
556
+ return {
557
+ dispose: () => {
558
+ parts.forEach((part) => part.dispose());
559
+ opts.root.classList.remove("gs-root");
560
+ }
561
+ };
562
+ }
563
+
564
+ exports.CRASH_KIND = CRASH_KIND;
565
+ exports.DEFAULT_SNOOZE_DAYS = DEFAULT_SNOOZE_DAYS;
566
+ exports.DESKTOP_MIN_WIDTH = DESKTOP_MIN_WIDTH;
567
+ exports.GAME_SHELL_CSS = GAME_SHELL_CSS;
568
+ exports.GS_RESIZE_EVENT = GS_RESIZE_EVENT;
569
+ exports.GS_VH_VAR = GS_VH_VAR;
570
+ exports.initGameShell = initGameShell;
571
+ exports.injectStyles = injectStyles;
572
+ exports.isDesktop = isDesktop;
573
+ exports.isIosSafari = isIosSafari;
574
+ exports.isStandalone = isStandalone;
575
+ exports.prefersReducedMotion = prefersReducedMotion;
576
+ exports.startCrash = startCrash;
577
+ exports.startFit = startFit;
578
+ exports.startInstall = startInstall;
579
+ exports.startLifecycle = startLifecycle;
580
+ exports.startVersionPoll = startVersionPoll;
581
+ exports.unlockAudio = unlockAudio;
582
+ //# sourceMappingURL=index.js.map
583
+ //# sourceMappingURL=index.js.map