mirror of
https://github.com/SquidSec/SquidC5
synced 2026-08-09 12:22:26 +00:00
Merge pull request #78 from SquidSec/fix/inko-mobile-kb-jitter
fix(ops): INKO mobile keyboard scroll jitter
This commit is contained in:
+28
-7
@@ -1411,6 +1411,13 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function scrollAiChatLog() {
|
||||||
|
const parent = el("aiChatLog");
|
||||||
|
if (!parent) return;
|
||||||
|
// Instant only - smooth + keyboard resize causes mobile jitter
|
||||||
|
parent.scrollTop = parent.scrollHeight;
|
||||||
|
}
|
||||||
|
|
||||||
function showAiPending() {
|
function showAiPending() {
|
||||||
removeAiPending();
|
removeAiPending();
|
||||||
const parent = el("aiChatLog");
|
const parent = el("aiChatLog");
|
||||||
@@ -1421,7 +1428,7 @@
|
|||||||
div.innerHTML =
|
div.innerHTML =
|
||||||
'<div class="who">INKO</div><div class="ai-typing"><span class="ai-typing-dots" aria-hidden="true"><span></span><span></span><span></span></span><span>Thinking...</span></div>';
|
'<div class="who">INKO</div><div class="ai-typing"><span class="ai-typing-dots" aria-hidden="true"><span></span><span></span><span></span></span><span>Thinking...</span></div>';
|
||||||
parent.appendChild(div);
|
parent.appendChild(div);
|
||||||
parent.scrollTop = parent.scrollHeight;
|
scrollAiChatLog();
|
||||||
return div;
|
return div;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1484,19 +1491,26 @@
|
|||||||
div.appendChild(actions);
|
div.appendChild(actions);
|
||||||
}
|
}
|
||||||
parent.appendChild(div);
|
parent.appendChild(div);
|
||||||
parent.scrollTop = parent.scrollHeight;
|
if (!parent.dataset.skipScroll) scrollAiChatLog();
|
||||||
}
|
}
|
||||||
|
|
||||||
function rebuildAiChatDom() {
|
function rebuildAiChatDom() {
|
||||||
if (el("aiChatLog")) el("aiChatLog").innerHTML = "";
|
const log = el("aiChatLog");
|
||||||
|
if (log) log.innerHTML = "";
|
||||||
if (!aiChatHistory.length) {
|
if (!aiChatHistory.length) {
|
||||||
renderInkoStarters();
|
renderInkoStarters();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
aiChatHistory.forEach((m) => {
|
if (log) log.dataset.skipScroll = "1";
|
||||||
const who = m.role === "user" ? "user" : "bot";
|
try {
|
||||||
appendAiChat(who, m.content, m.tools || null);
|
aiChatHistory.forEach((m) => {
|
||||||
});
|
const who = m.role === "user" ? "user" : "bot";
|
||||||
|
appendAiChat(who, m.content, m.tools || null);
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
if (log) delete log.dataset.skipScroll;
|
||||||
|
}
|
||||||
|
scrollAiChatLog();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -1720,6 +1734,13 @@
|
|||||||
d.classList.toggle("hidden", !show);
|
d.classList.toggle("hidden", !show);
|
||||||
d.classList.toggle("open", show);
|
d.classList.toggle("open", show);
|
||||||
d.setAttribute("aria-hidden", show ? "false" : "true");
|
d.setAttribute("aria-hidden", show ? "false" : "true");
|
||||||
|
if (!show) {
|
||||||
|
// Drop visualViewport pin so next open starts full-screen again
|
||||||
|
d.classList.remove("kb-pinned");
|
||||||
|
d.style.top = "";
|
||||||
|
d.style.height = "";
|
||||||
|
d.style.bottom = "";
|
||||||
|
}
|
||||||
if (backdrop) {
|
if (backdrop) {
|
||||||
backdrop.hidden = !show;
|
backdrop.hidden = !show;
|
||||||
backdrop.classList.toggle("show", show);
|
backdrop.classList.toggle("show", show);
|
||||||
|
|||||||
+89
-15
@@ -585,15 +585,20 @@
|
|||||||
body.kb-open .ctx-body {
|
body.kb-open .ctx-body {
|
||||||
padding-bottom: calc(24px + var(--kb-inset, 0px));
|
padding-bottom: calc(24px + var(--kb-inset, 0px));
|
||||||
}
|
}
|
||||||
|
/* INKO drawer is resized to visualViewport when keyboard is open - do not
|
||||||
|
also pad the foot by kb-inset (that double-compensates and jitters the log). */
|
||||||
body.kb-open .ai-drawer-foot {
|
body.kb-open .ai-drawer-foot {
|
||||||
padding-bottom: calc(12px + var(--kb-inset, 0px));
|
padding-bottom: calc(8px + env(safe-area-inset-bottom, 0px));
|
||||||
}
|
}
|
||||||
body.kb-open .ai-drawer-body {
|
body.kb-open .ai-drawer.kb-pinned {
|
||||||
padding-bottom: 8px;
|
transition: none;
|
||||||
}
|
}
|
||||||
.ai-drawer-head .inko-titles { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
|
.ai-drawer-head .inko-titles { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
|
||||||
.ai-drawer-body {
|
.ai-drawer-body {
|
||||||
flex: 1; overflow: auto; padding: 10px; font-size: 0.82rem; min-height: 0;
|
flex: 1; overflow: auto; padding: 10px; font-size: 0.82rem; min-height: 0;
|
||||||
|
overflow-anchor: none;
|
||||||
|
overscroll-behavior: contain;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
}
|
}
|
||||||
.ai-msg { margin-bottom: 10px; padding: 8px 10px; border-radius: 8px; line-height: 1.45; }
|
.ai-msg { margin-bottom: 10px; padding: 8px 10px; border-radius: 8px; line-height: 1.45; }
|
||||||
.ai-msg.user { background: rgba(255,255,255,0.04); border: 1px solid var(--border); }
|
.ai-msg.user { background: rgba(255,255,255,0.04); border: 1px solid var(--border); }
|
||||||
@@ -1708,23 +1713,92 @@
|
|||||||
window.__SC5_buildPhoneShareUrl = buildPhoneShareUrl;
|
window.__SC5_buildPhoneShareUrl = buildPhoneShareUrl;
|
||||||
})();
|
})();
|
||||||
|
|
||||||
/* Mobile keyboard: keep focused inputs above keyboard */
|
/* Mobile keyboard: pin INKO drawer to visualViewport; avoid smooth-scroll jitter */
|
||||||
(function () {
|
(function () {
|
||||||
const vv = window.visualViewport;
|
const vv = window.visualViewport;
|
||||||
if (!vv) return;
|
if (!vv) return;
|
||||||
const apply = () => {
|
let raf = 0;
|
||||||
|
let lastKb = -1;
|
||||||
|
let lastTop = -1;
|
||||||
|
let lastH = -1;
|
||||||
|
|
||||||
|
function pinChatLog() {
|
||||||
|
const log = document.getElementById("aiChatLog");
|
||||||
|
if (!log) return;
|
||||||
|
// Instant snap only - never smooth (smooth + vv resize = jitter)
|
||||||
|
log.scrollTop = log.scrollHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearDrawerPin(drawer) {
|
||||||
|
if (!drawer) return;
|
||||||
|
drawer.classList.remove("kb-pinned");
|
||||||
|
drawer.style.top = "";
|
||||||
|
drawer.style.height = "";
|
||||||
|
drawer.style.bottom = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function apply(opts) {
|
||||||
const kb = Math.max(0, Math.round(window.innerHeight - vv.height - vv.offsetTop));
|
const kb = Math.max(0, Math.round(window.innerHeight - vv.height - vv.offsetTop));
|
||||||
document.documentElement.style.setProperty("--kb-inset", kb + "px");
|
const open = kb > 80;
|
||||||
document.body.classList.toggle("kb-open", kb > 60);
|
if (kb !== lastKb) {
|
||||||
const a = document.activeElement;
|
document.documentElement.style.setProperty("--kb-inset", kb + "px");
|
||||||
if (kb > 60 && a && (a.tagName === "TEXTAREA" || a.tagName === "INPUT" || a.tagName === "SELECT")) {
|
document.body.classList.toggle("kb-open", open);
|
||||||
try { a.scrollIntoView({ block: "center", behavior: "smooth" }); } catch (_) {}
|
lastKb = kb;
|
||||||
}
|
}
|
||||||
};
|
|
||||||
vv.addEventListener("resize", apply);
|
const drawer = document.getElementById("aiDrawer");
|
||||||
vv.addEventListener("scroll", apply);
|
const drawerOpen = drawer && drawer.classList.contains("open") && !drawer.classList.contains("hidden");
|
||||||
window.addEventListener("focusin", () => setTimeout(apply, 50));
|
if (drawerOpen && open) {
|
||||||
window.addEventListener("focusout", () => setTimeout(apply, 100));
|
const top = Math.round(vv.offsetTop);
|
||||||
|
const h = Math.max(120, Math.round(vv.height));
|
||||||
|
if (top !== lastTop || h !== lastH || !drawer.classList.contains("kb-pinned")) {
|
||||||
|
drawer.classList.add("kb-pinned");
|
||||||
|
drawer.style.top = top + "px";
|
||||||
|
drawer.style.height = h + "px";
|
||||||
|
drawer.style.bottom = "auto";
|
||||||
|
lastTop = top;
|
||||||
|
lastH = h;
|
||||||
|
}
|
||||||
|
} else if (drawer) {
|
||||||
|
clearDrawerPin(drawer);
|
||||||
|
lastTop = -1;
|
||||||
|
lastH = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opts && opts.pinChat && drawerOpen) pinChatLog();
|
||||||
|
}
|
||||||
|
|
||||||
|
function schedule(pinChat) {
|
||||||
|
if (raf) cancelAnimationFrame(raf);
|
||||||
|
raf = requestAnimationFrame(() => {
|
||||||
|
raf = 0;
|
||||||
|
apply({ pinChat: !!pinChat });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
vv.addEventListener("resize", () => schedule(false));
|
||||||
|
vv.addEventListener("scroll", () => schedule(false));
|
||||||
|
|
||||||
|
window.addEventListener("focusin", (e) => {
|
||||||
|
const a = e.target;
|
||||||
|
if (!a || !a.tagName) return;
|
||||||
|
const tag = a.tagName;
|
||||||
|
if (tag !== "TEXTAREA" && tag !== "INPUT" && tag !== "SELECT") return;
|
||||||
|
const inInko = !!(a.id === "aiPromptGlobal" || a.closest("#aiDrawer"));
|
||||||
|
// One settle after keyboard starts - no smooth scrollIntoView on the message log
|
||||||
|
setTimeout(() => {
|
||||||
|
apply({ pinChat: inInko });
|
||||||
|
try {
|
||||||
|
a.scrollIntoView({ block: "nearest", inline: "nearest", behavior: "instant" });
|
||||||
|
} catch (_) {
|
||||||
|
try { a.scrollIntoView(false); } catch (__) {}
|
||||||
|
}
|
||||||
|
}, 60);
|
||||||
|
});
|
||||||
|
|
||||||
|
window.addEventListener("focusout", () => {
|
||||||
|
setTimeout(() => apply({ pinChat: false }), 120);
|
||||||
|
});
|
||||||
})();
|
})();
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Reference in New Issue
Block a user