Blur the page whenever the window isn't in front

Losing focus -- another tab, another window, another app -- now puts the
whole page behind a blur, so an app switcher's snapshot or a glance over
the shoulder gives nothing away. It comes back on focus, on the tab
becoming visible again, and on the first touch or key, which lands on the
veil rather than on a page nobody could see.

Painted as a backdrop filter on a fixed overlay above everything the app
can show: a filter on the page itself would make every fixed element
position against the wrong box.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MPZFnNdHbPGDTqQUNiE4ZN
This commit is contained in:
Simon
2026-09-22 08:30:20 +00:00
parent 741786c7cf
commit 1b38f9d9e7
5 changed files with 239 additions and 0 deletions

View File

@@ -3014,3 +3014,34 @@ body.theme-light .video-card img:not(.is-loaded) {
pointer-events: none;
}
/* Privacy veil -- the page goes behind a blur whenever this window isn't the
one in front. Above everything else the app can put on screen, and painted
as a backdrop filter rather than a filter on the page itself, which would
make every fixed element inside it position against the wrong box. */
.privacy-veil {
position: fixed;
inset: 0;
z-index: 9000;
opacity: 0;
pointer-events: none;
background: rgba(10, 8, 6, 0.38);
-webkit-backdrop-filter: blur(26px) saturate(0.55);
backdrop-filter: blur(26px) saturate(0.55);
transition: opacity 200ms ease;
}
.privacy-veil.is-on {
opacity: 1;
/* Going behind the blur is immediate -- a fade would be a window during
which the page is still readable. Coming back can take its time. */
transition: none;
/* So the tap that brings the window back lands here and lifts the veil,
rather than pressing something on a page nobody could see. */
pointer-events: auto;
}
/* Nothing to blur through without backdrop-filter, so the veil covers instead. */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
.privacy-veil { background: var(--bg-primary); }
}

View File

@@ -226,6 +226,8 @@
</div>
</div>
<div id="privacy-veil" class="privacy-veil" aria-hidden="true"></div>
<script src="static/js/state.js"></script>
<script src="static/js/marquee.js"></script>
<script src="static/js/storage.js"></script>
@@ -240,6 +242,7 @@
<script src="static/js/ui.js"></script>
<script src="static/js/version.js"></script>
<script src="static/js/enhance.js"></script>
<script src="static/js/privacy.js"></script>
<script src="static/js/main.js"></script>
</body>
</html>

View File

@@ -3,6 +3,10 @@ window.App = window.App || {};
(function() {
// App bootstrap: initialize storage, render UI, and load the first page.
async function initApp() {
// Armed before anything is on screen: there is nothing to show behind
// a blur yet, but the window can lose focus during startup too.
App.privacy.start();
await App.storage.ensureDefaults();
App.ui.applyTheme();
App.ui.applyPreferredQuality();

60
frontend/js/privacy.js Normal file
View File

@@ -0,0 +1,60 @@
window.App = window.App || {};
App.privacy = App.privacy || {};
(function() {
// The moment this window stops being the one in front -- another tab,
// another window, another app -- the page goes behind a blur. What the site
// is showing is nobody else's business: not a glance over the shoulder, not
// the app switcher's snapshot, not a screen share of the tab next door.
const ON = 'is-on';
let veil = null;
let veiled = false;
const element = function() {
if (!veil || !veil.isConnected) veil = document.getElementById('privacy-veil');
return veil;
};
const setVeiled = function(next) {
const el = element();
if (!el || next === veiled) return;
veiled = next;
el.classList.toggle(ON, next);
};
const cover = function() { setVeiled(true); };
const uncover = function() { setVeiled(false); };
function start() {
if (!element()) return;
// Leaving. Any one of these can be the last event before the window is
// out of sight, so each covers on its own, and covers at once: a blur
// that lands after the app switcher took its picture is no blur at all.
window.addEventListener('blur', cover);
window.addEventListener('pagehide', cover);
document.addEventListener('visibilitychange', function() {
if (document.visibilityState === 'hidden') cover();
else uncover();
});
// Coming back.
window.addEventListener('focus', uncover);
window.addEventListener('pageshow', uncover);
// A backstop, for whatever the browser did or didn't report: someone
// using the page is someone looking at it. The first touch lands on the
// veil and lifts it instead of reaching the thing underneath, which is
// the right way round for a tap that was meant for a page you could see.
['pointerdown', 'touchstart', 'keydown', 'wheel'].forEach(function(name) {
window.addEventListener(name, uncover, { capture: true, passive: true });
});
}
App.privacy = {
start: start,
cover: cover,
uncover: uncover,
isVeiled: function() { return veiled; }
};
})();