4 Commits
Author SHA1 Message Date
christianandClaude Opus 5.5 0f9c468b1e Release 1.4.40
Browser tab icon no longer looks selected when you're on another tab.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 21:27:24 -04:00
christianandClaude Opus 5.5 ede79f5c57 Browser tab icon dims like the others when the tab isn't selected
The globe was missing from the rule that dims tab icons at rest, so it
always showed at full brightness and looked like the active tab.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 21:25:35 -04:00
christianandClaude Opus 5.5 8ddbee199b Release 1.4.39
Browser tab: the keyboard opens right under the search box, and Enter
searches and puts it away.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 21:20:34 -04:00
christianandClaude Opus 5.5 9d63e00632 Browser tab keyboard: open under the field being typed into; Enter hides it
The stage TV is portrait, and the keyboard opened at the bottom of the screen — a
reach to the floor to type into a search box at the top.

- browser-guest-preload.cjs reports the tapped field's position (and which field it
  is) with the tap, and again when focus moves to another field.
- browser-tab.js opens the keyboard directly under that field, centred on it and
  kept on screen; if there is no room under it, just above it. Focus moving to a
  different field (Tab, the next memorize blank) moves the keyboard with it.
  Re-tapping the same field leaves a dragged keyboard where it was.
- The keyboard's Enter submits to the page and then hides the keyboard, so search
  results are not covered. Tapping a field brings it back.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 21:17:16 -04:00
5 changed files with 62 additions and 15 deletions
+1 -1
View File
@@ -10,7 +10,7 @@ showing, so it is ready the moment it is opened. Built for It Is Written
| Do this | What happens | | Do this | What happens |
|---|---| |---|---|
| Drag a finger | The page scrolls. | | Drag a finger | The page scrolls. |
| Tap a text box on the page | The app's own keyboard (the Prayer tab's) comes up and types into it. Enter submits; the keyboard puts itself away when the page moves on (e.g. after a search). × closes it; tap the box again to bring it back. | | Tap a text box on the page | The app's own keyboard (the Prayer tab's) comes up right under the box (above it if the box is near the bottom of the screen — the stage TV is portrait) and types into it. Enter submits and puts the keyboard away; it also goes away when the page moves on. × closes it; tap the box again to bring it back. |
| Tap a scripture reference (e.g. "Isaiah 41:10") | It opens in a new Bible tab, at that verse, in the translation printed beside it (NIV, ESV…) when that translation is switched on. | | Tap a scripture reference (e.g. "Isaiah 41:10") | It opens in a new Bible tab, at that verse, in the translation printed beside it (NIV, ESV…) when that translation is switched on. |
| Tap anything else | Works on the page as normal. | | Tap anything else | Works on the page as normal. |
| Leave the tab and come back | The page is exactly as it was: same search, same place, same typing. | | Leave the tab and come back | The page is exactly as it was: same search, same place, same typing. |
+18 -2
View File
@@ -127,6 +127,19 @@ function restoreOsKeyboard(el) {
if (v === null) el.removeAttribute('inputmode'); else el.setAttribute('inputmode', v); if (v === null) el.removeAttribute('inputmode'); else el.setAttribute('inputmode', v);
} }
/*
* Where the field is, so the app can open its keyboard right under it: the stage TV is portrait, and a
* keyboard parked at the bottom of a 75" screen is a reach to the floor. `field` tells one field from
* another, so focus moving to a different one (Tab, the next memorize blank) can move the keyboard too.
*/
let fieldSeq = 0;
const fieldIds = new WeakMap();
function fieldInfo(el) {
if (!fieldIds.has(el)) fieldIds.set(el, ++fieldSeq);
const r = el.getBoundingClientRect();
return { field: fieldIds.get(el), rect: { x: Math.round(r.left), y: Math.round(r.top), w: Math.round(r.width), h: Math.round(r.height) } };
}
/* /*
* Only a TAP raises the keyboard. A page that focuses its own search box as it loads must not throw a * Only a TAP raises the keyboard. A page that focuses its own search box as it loads must not throw a
* keyboard over the stage while nobody is touching it; focus moving between fields (Tab, the next * keyboard over the stage while nobody is touching it; focus moving between fields (Tab, the next
@@ -137,10 +150,13 @@ window.addEventListener('pointerdown', (e) => {
const el = editableFrom(e.target); const el = editableFrom(e.target);
if (!el) return; if (!el) return;
quietOsKeyboard(el); quietOsKeyboard(el);
send('bibleapp:editable', { on: true, tap: true }); send('bibleapp:editable', Object.assign({ on: true, tap: true }, fieldInfo(el)));
}, true); }, true);
document.addEventListener('focusin', (e) => { document.addEventListener('focusin', (e) => {
send('bibleapp:editable', { on: isEditable(e.target), tap: false }); const el = e.target;
if (!isEditable(el)) { send('bibleapp:editable', { on: false, tap: false }); return; }
// Measured a frame later: focusing can scroll the field into view first.
requestAnimationFrame(() => send('bibleapp:editable', Object.assign({ on: true, tap: false }, fieldInfo(el))));
}, true); }, true);
document.addEventListener('focusout', (e) => { document.addEventListener('focusout', (e) => {
restoreOsKeyboard(e.target); restoreOsKeyboard(e.target);
+39 -10
View File
@@ -360,7 +360,9 @@
// Only the page on screen may raise or drop the keyboard; a background page focusing its own // Only the page on screen may raise or drop the keyboard; a background page focusing its own
// search box as it loads must not. // search box as it loads must not.
if (!d || !isShown(v)) return; if (!d || !isShown(v)) return;
if (d.on && d.tap) showKeyboard(); if (d.on && d.tap) showKeyboard(v, d);
// Focus moved on to a DIFFERENT field while typing (Tab, the next memorize blank): follow it.
else if (d.on && kb && kb.el.style.display === 'block' && d.field && d.field !== kb.field) placeKeyboard(v, d);
else if (!d.on) hideKeyboard(); else if (!d.on) hideKeyboard();
return; return;
case 'bibleapp:activity': case 'bibleapp:activity':
@@ -600,6 +602,9 @@
if (name === 'Enter') v.el.sendInputEvent({ type: 'char', keyCode: '\r' }); if (name === 'Enter') v.el.sendInputEvent({ type: 'char', keyCode: '\r' });
v.el.sendInputEvent({ type: 'keyUp', keyCode: code }); v.el.sendInputEvent({ type: 'keyUp', keyCode: code });
} catch (_) {} } catch (_) {}
// Enter is "done": the search runs and the keyboard gets out of the way of the results. A tap on a
// field brings it back.
if (name === 'Enter') hideKeyboard();
}, },
focus: function () { focus: function () {
var v = activeView(); var v = activeView();
@@ -616,20 +621,44 @@
el.style.display = 'none'; el.style.display = 'none';
document.body.appendChild(el); document.body.appendChild(el);
a.createPrayerKeyboard(el, null, 'browser', sink); a.createPrayerKeyboard(el, null, 'browser', sink);
kb = { el: el, placed: false }; kb = { el: el, field: null };
return kb; return kb;
} }
function showKeyboard() { /*
* The keyboard opens directly under the field being typed into. The stage TV is portrait: anything at
* the bottom of the screen is a reach to the floor, and the page's search box is near the top. If the
* field is too low for the keyboard to fit under it, it goes just above instead. Re-tapping the same
* field while the keyboard is up leaves it wherever it was dragged.
*/
function placeKeyboard(v, d) {
var k = kb;
if (!k || !d || !d.rect || !v || !v.el) return;
var W = v.el.getBoundingClientRect(), z = v.zoom || 1;
var fieldTop = W.top + d.rect.y * z;
var fieldBottom = W.top + (d.rect.y + d.rect.h) * z;
var fieldMid = W.left + (d.rect.x + d.rect.w / 2) * z;
var w = k.el.offsetWidth || 760, h = k.el.offsetHeight || 340, gap = 14;
var y = fieldBottom + gap;
if (y + h > window.innerHeight - 8) {
var above = fieldTop - gap - h;
y = above >= 0 ? above : Math.max(0, window.innerHeight - h - 8);
}
var x = Math.max(0, Math.min(window.innerWidth - w, Math.round(fieldMid - w / 2)));
k.el.style.left = x + 'px';
k.el.style.top = Math.round(y) + 'px';
k.field = d.field || null;
}
function showKeyboard(v, d) {
if (ROLE !== 'stage') return; if (ROLE !== 'stage') return;
var k = keyboard(); var k = keyboard();
if (!k) return; if (!k) return;
k.el.style.display = 'block'; var wasHidden = k.el.style.display !== 'block';
// First time: bottom centre, clear of the page's search box at the top. After that, wherever it was dragged. k.el.style.display = 'block'; // shown before measuring: a hidden element has no size
if (!k.placed) { if (d && d.rect && (wasHidden || d.field !== k.field)) placeKeyboard(v, d);
k.placed = true; else if (wasHidden && !k.field) {
var w = k.el.offsetWidth || 760, hgt = k.el.offsetHeight || 340; // No position from the page (should not happen): centre it rather than leave it anywhere odd.
k.el.style.left = Math.max(0, Math.round((window.innerWidth - w) / 2)) + 'px'; k.el.style.left = Math.max(0, Math.round((window.innerWidth - (k.el.offsetWidth || 760)) / 2)) + 'px';
k.el.style.top = Math.max(0, Math.round(window.innerHeight - hgt - Math.max(24, window.innerHeight * 0.04))) + 'px'; k.el.style.top = Math.max(0, Math.round((window.innerHeight - (k.el.offsetHeight || 340)) / 3)) + 'px';
} }
} }
function hideKeyboard() { if (kb) kb.el.style.display = 'none'; } function hideKeyboard() { if (kb) kb.el.style.display = 'none'; }
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "bible-app", "name": "bible-app",
"version": "1.4.38", "version": "1.4.40",
"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",
+3 -1
View File
@@ -1547,7 +1547,7 @@ body.drawing * {
} }
/* Removed explicit close button; using hold-to-close gesture on tab */ /* Removed explicit close button; using hold-to-close gesture on tab */
.bible-icon, .media-icon, .whiteboard-icon, .media-tab-icon, .graphics-tab-icon, .maps-tab-icon, .prayer-tab-icon, .alpha-icon, .notes-icon, .ask-tab-icon { .bible-icon, .media-icon, .whiteboard-icon, .media-tab-icon, .graphics-tab-icon, .maps-tab-icon, .browser-tab-icon, .prayer-tab-icon, .alpha-icon, .notes-icon, .ask-tab-icon {
opacity: 0.7; opacity: 0.7;
transition: opacity 0.2s; transition: opacity 0.2s;
} }
@@ -1564,6 +1564,8 @@ body.drawing * {
.tab.active .graphics-tab-icon, .tab.active .graphics-tab-icon,
.tab:hover .maps-tab-icon, .tab:hover .maps-tab-icon,
.tab.active .maps-tab-icon, .tab.active .maps-tab-icon,
.tab:hover .browser-tab-icon,
.tab.active .browser-tab-icon,
.tab:hover .prayer-tab-icon, .tab:hover .prayer-tab-icon,
.tab.active .prayer-tab-icon, .tab.active .prayer-tab-icon,
.tab:hover .alpha-icon, .tab:hover .alpha-icon,