2 Commits
Author SHA1 Message Date
christianandClaude Opus 5.5 3891a027f3 Release 1.4.41
Browser tab: no more blue outline flashing around the search box while typing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 09:31:15 -04:00
christianandClaude Opus 5.5 e1f541d366 Browser tab: no focus highlight on the page
Every key on the app's keyboard takes focus from the page for a moment and
gives it back, so the site's focus ring (a 2px blue outline on the search
box, and a brighter border) flashed with each letter. Outline rings are
turned off with the page's clean-up CSS, and the guest preload strips
outline, border and box-shadow from the site's own :focus / :focus-within /
:focus-visible rules, keeping anything else those rules do.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 09:28:59 -04:00
4 changed files with 77 additions and 7 deletions
+4 -2
View File
@@ -1,6 +1,6 @@
# Browser tab # Browser tab
One web page, full screen in the tab: no address bar, bookmarks or scrollbars — just the page, in the One web page, full screen in the tab: no address bar, bookmarks, scrollbars or focus highlights — just the page, in the
same area the Graphics tab uses. It loads when the app starts and keeps running while other tabs are same area the Graphics tab uses. It loads when the app starts and keeps running while other tabs are
showing, so it is ready the moment it is opened. Built for It Is Written showing, so it is ready the moment it is opened. Built for It Is Written
(`https://it-is-written-waters-church.vercel.app/`), but any web page works. (`https://it-is-written-waters-church.vercel.app/`), but any web page works.
@@ -65,7 +65,9 @@ the search and could put different results on the program feed.
- **`browser-guest-preload.cjs`** runs inside the page (invisible to the site) and reports what the app - **`browser-guest-preload.cjs`** runs inside the page (invisible to the site) and reports what the app
cannot see from outside: a tapped text field, a tapped reference (found with the app's own cannot see from outside: a tapped text field, a tapped reference (found with the app's own
`scripture-scan.js` — the same rules ProPresenter Sync uses), scroll, activity, five fingers, and — only `scripture-scan.js` — the same rules ProPresenter Sync uses), scroll, activity, five fingers, and — only
while a follower is linked — a script-free copy of the page's DOM. while a follower is linked — a script-free copy of the page's DOM. It also takes the look (outline,
border, glow) out of the site's focus styles: every key on the app's keyboard takes focus from the page
for a moment, so a focus highlight would flash with each letter typed.
- **`browser-tab.js`** owns the tab: the pages, the keyboard (via `createPrayerKeyboard` with a "sink"), - **`browser-tab.js`** owns the tab: the pages, the keyboard (via `createPrayerKeyboard` with a "sink"),
scripture taps, scrolling for the phone / OSC / admin, and the follower's copy. On the follower it scripture taps, scrolling for the phone / OSC / admin, and the follower's copy. On the follower it
shows that copy in sandboxed frames (no scripts); in a plain browser (admin offstage copy, `npm start`) shows that copy in sandboxed frames (no scripts); in a plain browser (admin offstage copy, `npm start`)
+65
View File
@@ -167,6 +167,71 @@ document.addEventListener('focusout', (e) => {
}, 0); }, 0);
}, true); }, true);
// ------------------------------------------------------------------ no focus highlight
/*
* Nothing on the page shows that it has focus. The app's keyboard is outside the page, so every key press
* takes focus from the page for a moment and gives it straight back: a site's focus style would flash
* with each letter — and on stage it is clutter anyway. Outline rings (the browser's own and any site's)
* go with one rule in browser-tab.js's CLEAN_CSS; this takes the rest — a border that brightens, a glow —
* out of the site's own :focus / :focus-within / :focus-visible rules, and leaves everything else those
* rules do: a menu that opens on focus still opens. Sites add rules as they go (CSS-in-JS, late
* stylesheets), so this runs again just before anything can take focus. Another origin's stylesheets
* cannot be read and keep theirs; the outline rule still covers their rings.
*/
const FOCUS_LOOK = /^(?:outline|box-shadow|border)(?:-|$)/;
const seenRules = new WeakSet();
// Split at commas that are not inside brackets or quotes: "a:hover, b:is(c, d)" -> 2; "x:1; y:f(a;b)" -> 2.
function splitTop(text, sep) {
const out = [];
let depth = 0, quote = '', start = 0;
for (let i = 0; i < text.length; i++) {
const c = text[i];
if (quote) { if (c === '\\') i++; else if (c === quote) quote = ''; continue; }
if (c === '"' || c === "'") quote = c;
else if (c === '(' || c === '[') depth++;
else if (c === ')' || c === ']') depth--;
else if (c === sep && depth === 0) { out.push(text.slice(start, i).trim()); start = i + 1; }
}
out.push(text.slice(start).trim());
return out.filter(Boolean);
}
// Styled BECAUSE it has focus. ":not(:focus)" styles a field at rest, so it does not count.
function focusDriven(sel) {
return /:focus(?:-within|-visible)?(?![\w-])/.test(sel.replace(/:not\((?:[^()]|\([^()]*\))*\)/g, ''));
}
function isLook(decl) { return FOCUS_LOOK.test(decl.slice(0, decl.indexOf(':')).trim().toLowerCase()); }
function stripFocusLook(list, owner) {
for (let i = 0; i < list.length; i++) {
const r = list[i];
if (r.cssRules && r.cssRules.length) stripFocusLook(r.cssRules, r); // @media, @supports, @layer, nesting
if (!r.selectorText || !r.style || seenRules.has(r)) continue;
seenRules.add(r);
const sels = splitTop(r.selectorText, ',');
const focus = sels.filter(focusDriven);
if (!focus.length) continue;
// Declarations as written (cssText), so a shorthand built on var() survives intact.
const decls = splitTop(r.style.cssText, ';');
const keep = decls.filter((d) => !isLook(d));
if (keep.length === decls.length) continue;
if (focus.length === sels.length) { r.style.cssText = keep.join('; '); continue; }
// Shared with selectors that are not about focus ("a:hover, a:focus"): those keep the whole rule, and
// the focus ones move to a copy without the look.
r.selectorText = sels.filter((s) => !focusDriven(s)).join(', ');
if (!keep.length) continue;
try { owner.insertRule(focus.join(', ') + ' { ' + keep.join('; ') + ' }', i + 1); i++; seenRules.add(list[i]); } catch (_) {}
}
}
function hideFocusLook() {
const sheets = [...document.styleSheets, ...(document.adoptedStyleSheets || [])];
for (const sh of sheets) { try { stripFocusLook(sh.cssRules, sh); } catch (_) {} } // another origin's: unreadable
}
window.addEventListener('DOMContentLoaded', hideFocusLook);
window.addEventListener('load', hideFocusLook);
window.addEventListener('pointerdown', hideFocusLook, true);
document.addEventListener('focusin', hideFocusLook, true);
// ------------------------------------------------------------------ activity + five fingers // ------------------------------------------------------------------ activity + five fingers
let lastActivity = 0; let lastActivity = 0;
function activity() { function activity() {
+7 -4
View File
@@ -41,15 +41,18 @@
var RETRY_MS = 10000; var RETRY_MS = 10000;
/* /*
* The page itself is the only thing on screen: no scrollbars, no blue tap flash, no text selection from * The page itself is the only thing on screen: no scrollbars, no blue tap flash, no focus ring, no text
* a resting finger, and no rubber-band pull that would drag the whole page (or, sideways, swipe back a * selection from a resting finger, and no rubber-band pull that would drag the whole page (or, sideways,
* page). Inputs keep their selection so typing still behaves. Applied to the live page with insertCSS * swipe back a page). Inputs keep their selection so typing still behaves. Applied to the live page with
* and to the booth's copy as a <style>, so both screens look the same. * insertCSS and to the booth's copy as a <style>, so both screens look the same. (The ring matters
* twice: every key on the app's keyboard takes focus from the page for a moment, so a ring would flash
* with each letter. The site's other focus styles are taken out by browser-guest-preload.cjs.)
*/ */
var CLEAN_CSS = var CLEAN_CSS =
'html,body{scrollbar-width:none!important;overscroll-behavior:none!important;}' + 'html,body{scrollbar-width:none!important;overscroll-behavior:none!important;}' +
'::-webkit-scrollbar{display:none!important;width:0!important;height:0!important;}' + '::-webkit-scrollbar{display:none!important;width:0!important;height:0!important;}' +
'*{-webkit-tap-highlight-color:transparent!important;}' + '*{-webkit-tap-highlight-color:transparent!important;}' +
':focus,:focus-visible{outline:none!important;}' +
'body *{-webkit-user-select:none!important;user-select:none!important;-webkit-touch-callout:none!important;}' + 'body *{-webkit-user-select:none!important;user-select:none!important;-webkit-touch-callout:none!important;}' +
'input,textarea,[contenteditable],[contenteditable] *{-webkit-user-select:text!important;user-select:text!important;}'; 'input,textarea,[contenteditable],[contenteditable] *{-webkit-user-select:text!important;user-select:text!important;}';
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "bible-app", "name": "bible-app",
"version": "1.4.40", "version": "1.4.41",
"description": "Minimalist Bible App for Portrait-Oriented Touchscreen TV", "description": "Minimalist Bible App for Portrait-Oriented Touchscreen TV",
"type": "module", "type": "module",
"main": "main.cjs", "main": "main.cjs",