diff --git a/src/App.svelte b/src/App.svelte index eef5bc1..b12d3da 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -28,8 +28,8 @@ const layout = $derived(width >= 1100 ? 'wide' : width >= 760 ? 'medium' : 'narrow'); // Arrangement (Appearance setting): side is the default, classic puts the channel list right - // of the chat, stacked above it. Phones always use drawers. - const arrangement = $derived(layout === 'narrow' ? 'side' : store.settings.arrangement ?? 'side'); + // of the chat, stacked above it. Narrow windows use drawers, except stacked, which works at any width. + const arrangement = $derived(store.settings.arrangement === 'stacked' ? 'stacked' : layout === 'narrow' ? 'side' : store.settings.arrangement ?? 'side'); // The member panel sits inline only in the side arrangement on wide windows, otherwise it overlays const panelInline = $derived(layout === 'wide' && arrangement === 'side'); let treeHeight = $state(Math.max(120, Number((() => { try { return localStorage.getItem('mumh5.treeHeight'); } catch { return null; } })()) || 280)); @@ -76,7 +76,8 @@ onsettings={() => (ui.settingsOpen = true)} /> {/snippet} {#snippet chat()} - (navOpen = true)} onpanel={togglePanel} /> + (navOpen = true)} onpanel={togglePanel} /> {/snippet}
diff --git a/src/ui/AppearanceSettings.svelte b/src/ui/AppearanceSettings.svelte index 0f823fb..5bcd4af 100644 --- a/src/ui/AppearanceSettings.svelte +++ b/src/ui/AppearanceSettings.svelte @@ -63,7 +63,7 @@ {/each}
-

Narrow windows always use the phone layout.

+

Narrow windows use the phone layout, except Stacked, which stays as it is.

diff --git a/test/e2e/share.e2e.ts b/test/e2e/share.e2e.ts index 78774f0..74179a2 100644 --- a/test/e2e/share.e2e.ts +++ b/test/e2e/share.e2e.ts @@ -83,6 +83,32 @@ try { await ownView.waitFor({ state: 'detached' }); console.log('ok: sharing started from the dialog, own tile shows the stream'); + // Stacked layout: only the picture, as wide as the chat, and the layout stays stacked on narrow windows + const setLayout = async (name: string) => { + await alice.page.getByTitle('Settings').click(); + await alice.page.getByRole('tab', { name: 'Appearance' }).click(); + await alice.page.getByRole('radio', { name }).click(); + await alice.page.getByRole('button', { name: 'Done', exact: true }).click(); + }; + await setLayout('Stacked'); + await alice.page.locator('.app.stacked .stage.solo').waitFor(); + assert.equal(await alice.page.locator('.stage .tiles').count(), 0, 'no participant tiles in the stacked layout'); + const fits = () => alice.page.evaluate(() => { + const v = document.querySelector('.stage.solo video') as HTMLVideoElement, c = document.querySelector('.chat')!; + return v.videoWidth > 0 && Math.abs(v.getBoundingClientRect().width - c.getBoundingClientRect().width) < 2; + }); + await alice.page.waitForFunction(() => (document.querySelector('.stage.solo video') as HTMLVideoElement | null)?.videoWidth! > 0); + assert.ok(await fits(), 'picture as wide as the chat'); + await alice.page.setViewportSize({ width: 600, height: 800 }); + await alice.page.locator('.app.stacked .stage.solo video').waitFor(); + assert.ok(await fits(), 'still as wide as the chat on a narrow window'); + if (process.env.SHOTS_DIR) await alice.page.screenshot({ path: `${process.env.SHOTS_DIR}/stacked.png` }); + await alice.page.getByText('0 watching').waitFor(); + await alice.page.setViewportSize({ width: 1280, height: 800 }); + await setLayout('Modern'); + await alice.page.locator('.stage .tiles').waitFor(); + console.log('ok: stacked layout shows only the picture and stays stacked when narrow'); + // Bob connects afterwards and still learns about the stream await join(bob.page, bobName); // Next to the name in the channel list, and on the tile above the chat