openvibers/openvibe.live Commits

Dialogs when zoomed in: overlays scroll and keep a tall dialog's top on screen (safe center), the restream form's dialog scrolls inside and sits above the navbar (it was under it and cut off at both ends) (#283)

OP OpenVibers committed 24 days ago · 6758e3a9c6 · 4 files, +85 −1

Changes

ddiff --git a/public/css/broadcast.css b/public/css/broadcast.css
iindex 13f7167d..1e0c70e9 100644
---- a/public/css/broadcast.css
++++ b/public/css/broadcast.css
@@@ -1711,12 +1711,14 @@
.bc-ws-confirm-overlay {
position: fixed;
inset: 0;
-- z-index: 200;
++ /* Above the navbar (1000): at 200 the navbar sat on top of the restream form (owner, 2026-10-20). */
++ z-index: 2000;
background: rgba(0,0,0,0.5);
backdrop-filter: blur(4px);
display: flex;
align-items: center;
justify-content: center;
++ padding: 16px;
animation: bc-confirm-fade-in 0.15s ease;
}
@keyframes bc-confirm-fade-in {
ddiff --git a/public/css/features/broadcast.css b/public/css/features/broadcast.css
iindex c49b309d..62735c4a 100644
---- a/public/css/features/broadcast.css
++++ b/public/css/features/broadcast.css
@@@ -1885,6 +1885,11 @@
max-width: 400px;
width: 90%;
box-shadow: 0 16px 64px rgba(0,0,0,0.5);
++ /* Zoomed in, the restream form is taller than the window: it scrolls inside instead of running off both edges. */
++ max-height: calc(100vh - 32px);
++ max-height: calc(100dvh - 32px);
++ overflow-y: auto;
++ overscroll-behavior: contain;
}
.bc-ws-confirm-actions {
display: flex;
ddiff --git a/public/css/style.css b/public/css/style.css
iindex 65b547f3..424f0e15 100644
---- a/public/css/style.css
++++ b/public/css/style.css
@@@ -4629,6 +4629,17 @@ textarea.form-input { resize: vertical; min-height: 60px; }
align-items: center; justify-content: center;
}
.modal-overlay.show { display: flex; }
++/* A dialog taller than the window (zoomed in, a short window) must not be centred off both edges: these overlays
++ scroll, the dialog stays centred while it fits (safe center; browsers without it keep plain center), and scrolling
++ stops at the overlay instead of moving the page behind it (owner, 2026-10-20). `body :is()` outranks each
++ overlay's own later `align-items: center` (feature stylesheets load after this one). */
++body :is(.modal-overlay, .bc-ws-confirm-overlay, .bc-modal, .bc-perm-overlay, .bucks-log-overlay, .user-ai-modal-overlay,
++.paste-modal-overlay, .vc-modal-overlay, .chat-slur-nudge-modal, .cosmetics-modal-overlay, .link-trust-overlay,
++.link-preview-overlay, .cgw-pop-overlay, .image-censor-overlay) {
++ overflow-y: auto;
++ overscroll-behavior: contain;
++ align-items: safe center;
++}
.modal {
background: var(--bg-tertiary); border: 1px solid var(--border);
border-radius: 20px; padding: 24px;
ddiff --git a/test/modal-zoom.test.js b/test/modal-zoom.test.js
nnew file mode 100644
iindex 00000000..77fe31cf
---- /dev/null
++++ b/test/modal-zoom.test.js
@@@ -0,0 +1,66 @@
++/**
++ * Dialogs when the page is zoomed in (owner, 2026-10-20: "when zooming into the website the popovers like this are all
++ * messed up and hard to scroll through" — the restream destination form under the navbar, cut off at both ends).
++ *
++ * A full-screen overlay that centres its dialog with flexbox pushes a dialog taller than the window off both edges,
++ * where nothing can scroll to it. Every such overlay is in style.css's `body :is(…)` rule (it scrolls, centres only
++ * while the dialog fits, and keeps the scroll off the page behind), and sits above the navbar.
++ *
++ * node test/modal-zoom.test.js
++ */
++'use strict';
++const assert = require('assert');
++const fs = require('fs');
++const path = require('path');
++
++const root = path.join(__dirname, '..');
++const cssDir = path.join(root, 'public/css');
++const files = [];
++(function walk(dir) {
++ for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
++ const p = path.join(dir, e.name);
++ if (e.isDirectory()) walk(p);
++ else if (e.name.endsWith('.css') && !e.name.endsWith('.core.css')) files.push(p);
++ }
++})(cssDir);
++
++const style = fs.readFileSync(path.join(cssDir, 'style.css'), 'utf8');
++const ruleMatch = /body :is\(([^)]*)\) \{([^}]*)\}/.exec(style);
++assert.ok(ruleMatch, "style.css has the overlays' body :is(…) rule");
++const covered = new Set(ruleMatch[1].split(',').map((s) => s.trim()));
++assert.match(ruleMatch[2], /overflow-y: auto;/);
++assert.match(ruleMatch[2], /overscroll-behavior: contain;/);
++assert.match(ruleMatch[2], /align-items: safe center;/);
++
++// Every full-screen overlay that centres a dialog, in every stylesheet.
++const NAVBAR_Z = 1000;
++const missing = [];
++const below = [];
++for (const file of files) {
++ const css = fs.readFileSync(file, 'utf8').replace(/\/\*[\s\S]*?\*\//g, '');
++ for (const m of css.matchAll(/([^{}]+)\{([^{}]*)\}/g)) {
++ const body = m[2];
++ const fixed = /position:\s*fixed/.test(body) && (/inset:\s*0\b/.test(body) || (/top:\s*0\b/.test(body) && /bottom:\s*0\b/.test(body)));
++ if (!fixed || !/align-items:\s*center/.test(body) || /overflow(-y)?:\s*(auto|scroll)/.test(body)) continue;
++ for (const sel of m[1].split(',').map((s) => s.trim())) {
++ if (!/^\.[\w-]+$/.test(sel)) continue;
++ // Full-screen surfaces that are not dialogs: a call's own view and the stats page's chart viewer.
++ if (['.vc-call-overlay', '.sn-overlay'].includes(sel)) continue;
++ if (!covered.has(sel)) missing.push(`${path.relative(root, file)} ${sel}`);
++ const z = /z-index:\s*(\d+)/.exec(body);
++ if (z && Number(z[1]) < NAVBAR_Z) below.push(`${sel} (z-index ${z[1]})`);
++ }
++ }
++}
++assert.deepStrictEqual(missing, [], 'a centred full-screen overlay must scroll when its dialog is taller than the window');
++assert.deepStrictEqual(below, [], 'a dialog overlay sits above the navbar (z-index 1000)');
++
++// The restream form's dialog scrolls inside too, and its overlay is above the navbar.
++const features = fs.readFileSync(path.join(cssDir, 'features/broadcast.css'), 'utf8');
++const dialog = /\.bc-ws-confirm-dialog \{([^}]*)\}/.exec(features)[1];
++assert.match(dialog, /max-height: calc\(100dvh - 32px\);/);
++assert.match(dialog, /overflow-y: auto;/);
++const overlay = /\.bc-ws-confirm-overlay \{([^}]*)\}/.exec(fs.readFileSync(path.join(cssDir, 'broadcast.css'), 'utf8'))[1];
++assert.match(overlay, /z-index: 2000;/);
++
++console.log(`modal zoom: ${covered.size} dialog overlays scroll when zoomed in, above the navbar — all checks passed`);