remote trackpad and keyboard server
git clone https://git.lucas.co/cce-remote.git
feat: float the typing preview just above the software keyboard
The preview lived at the top of the page, but while typing the eyes are on
the keyboard — and iOS lays the keyboard over the page with no CSS-visible
layout change, so nothing in normal flow can sit "just above" it.
visualViewport is the honest signal: its height shrinks by the keyboard, and
offsetTop compensates for iOS sliding a standalone page. While the hidden
textarea has focus and the viewport is short, the preview switches to a fixed
float placed so its bottom edge rides the keyboard top; it returns to flow on
blur or when the API is missing.
Repositioning happens on viewport resize/scroll AND on every text change,
because wrapping changes the preview's own height. The floating state is
pointer-events:none — it hovers over the pad, and a display-only strip must
never eat trackpad gestures.
Co-Authored-By: Claude Fable 5 <[email protected]>
index.html | 24 ++++++++++++++++++++++++
1 file changed, 24 insertions(+)
diff --git a/index.html b/index.html
index 3aa1c8f..b7c1165 100644
--- a/index.html
+++ b/index.html
@@ -62,6 +62,12 @@
background: #191b23; border: 1px solid #2e3140; border-radius: 8px;
font-size: 15px; color: #cfe8e8; white-space: pre-wrap; word-break: break-all; }
#preview.show { display: block; }
+ /* While the software keyboard is up, the preview floats with its bottom
+ just above the keyboard (placed by JS from visualViewport) — eyes are on
+ the keyboard, not the top of the screen. Display-only: never intercept
+ pad gestures underneath it. */
+ #preview.float { position: fixed; left: 8px; right: 8px; margin: 0; z-index: 6;
+ pointer-events: none; background: rgba(25,27,35,0.94); }
#preview.show::after { content: "▍"; color: #7dffff; animation: blink 1s steps(1) infinite; }
@keyframes blink { 50% { opacity: 0; } }
#status { position: fixed; top: 4px; right: 10px; font-size: 10px; color: #4a4d5c; }
@@ -485,9 +491,27 @@ function armKb() { kb.value = SENTINEL; kb.setSelectionRange(SENTINEL.length, SE
// (The target app shows the real text; this is feedback for eyes-on-phone.)
const preview = document.getElementById("preview");
let typed = "";
+// The iOS keyboard overlays the page without any CSS-visible layout change;
+// visualViewport is the only honest signal (its height shrinks, and offsetTop
+// compensates for iOS sliding a standalone page). Reposition on viewport
+// changes AND on every text change, since wrapping alters the preview height.
+const vv = window.visualViewport;
+function placePreview() {
+ const kbOpen = vv && document.activeElement === kb
+ && vv.height < window.innerHeight * 0.85;
+ preview.classList.toggle("float", !!kbOpen);
+ preview.style.top = kbOpen
+ ? Math.max(0, vv.offsetTop + vv.height - preview.offsetHeight - 8) + "px"
+ : "";
+}
+if (vv) {
+ vv.addEventListener("resize", placePreview);
+ vv.addEventListener("scroll", placePreview);
+}
function updPreview() {
preview.textContent = typed;
preview.classList.toggle("show", document.activeElement === kb);
+ placePreview();
}
kb.addEventListener("focus", () => { armKb(); updPreview(); });
kb.addEventListener("blur", () => { typed = ""; updPreview(); });