diff --git a/src/main.js b/src/main.js index c957d1b..4ec2acd 100644 --- a/src/main.js +++ b/src/main.js @@ -50,9 +50,29 @@ const app = new Fractious( interactionManager, ); +// Let the render area extend under camera cutouts / system bars (controls stay +// inside the safe area via body padding). Only opt in when installed or in +// fullscreen: a static viewport-fit=cover breaks Chrome's in-tab install sheet. +function bindViewportFit() { + const meta = document.querySelector('meta[name="viewport"]'); + if (!meta) return; + const base = 'width=device-width, initial-scale=1'; + const edgeToEdge = window.matchMedia?.( + '(display-mode: standalone), (display-mode: fullscreen)', + ); + const update = () => { + const cover = Boolean(edgeToEdge?.matches || document.fullscreenElement); + meta.setAttribute('content', cover ? `${base}, viewport-fit=cover` : base); + }; + edgeToEdge?.addEventListener?.('change', update); + document.addEventListener('fullscreenchange', update); + update(); +} + // Allow test environments to import this without running it immediately if needed // eslint-disable-next-line no-undef if (typeof process === 'undefined' || process.env.NODE_ENV !== 'test') { + bindViewportFit(); app.init(); } diff --git a/src/main.test.js b/src/main.test.js index f66cff3..2e327f3 100644 --- a/src/main.test.js +++ b/src/main.test.js @@ -92,4 +92,27 @@ describe('main.js initialization', () => { expect(document.getElementById('c_re').value).toBe('-1.5'); expect(document.getElementById('c_im').value).toBe('0.0'); }); + + it.each([ + [false, 'width=device-width, initial-scale=1'], + [true, 'width=device-width, initial-scale=1, viewport-fit=cover'], + ])( + 'should set viewport-fit=cover only when installed (standalone=%s)', + async (matches, expected) => { + vi.stubGlobal('process', { env: { NODE_ENV: 'production' } }); + const meta = document.createElement('meta'); + meta.setAttribute('name', 'viewport'); + meta.setAttribute('content', 'width=device-width, initial-scale=1'); + document.head.appendChild(meta); + window.matchMedia = vi.fn(() => ({ + matches, + addEventListener: vi.fn(), + })); + + await import('./main.js'); + + expect(meta.getAttribute('content')).toBe(expected); + meta.remove(); + }, + ); });