fix: drastically simplify mobile keyboard handling

- Remove all complex viewport tracking (was causing scroll issues)
- Remove keyboard-visible class and CSS (not needed)
- Remove --keyboard-height CSS variable (not needed)
- Simple approach: on focusin, wait 400ms, scrollIntoView
- Keep scroll-margin on phone inputs as CSS fallback
- Much more reliable and no side effects

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
arkon
2026-01-31 08:06:38 +01:00
co-authored by Claude Opus 4.5
parent 61c7a57426
commit 24af779d54
4 changed files with 37 additions and 189 deletions
+1 -1
View File
@@ -16,7 +16,7 @@ When user says "COM":
1. Increment version in BOTH `package.json` AND `CLAUDE.md`
2. Run: `git add -A && git commit -m "chore: bump version to X.XXXX" && git push && npm run build && systemctl --user restart claudeman-web`
**Version**: 0.1453 (must match `package.json`)
**Version**: 0.1454 (must match `package.json`)
## Project Overview
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "claudeman",
"version": "0.1453",
"version": "0.1454",
"description": "The missing control plane for Claude Code - run 20 autonomous agents with real-time monitoring and session persistence",
"type": "module",
"main": "dist/index.js",
+31 -134
View File
@@ -183,169 +183,66 @@ function getEventCoords(e) {
// ============================================================================
/**
* KeyboardHandler - Detects virtual keyboard visibility and scrolls inputs into view.
* Uses visualViewport API (works on iOS Safari and Android Chrome).
* Sets CSS custom property --keyboard-height and toggles .keyboard-visible class.
* KeyboardHandler - Simple handler to scroll inputs into view when keyboard appears.
* Uses focusin event and scrollIntoView - keeps it simple and reliable.
*/
const KeyboardHandler = {
initialViewportHeight: null,
isKeyboardVisible: false,
keyboardHeight: 0,
resizeHandler: null,
/** Initialize keyboard detection */
/** Initialize keyboard handling */
init() {
// Only initialize on touch devices
if (!MobileDetection.isTouchDevice()) return;
const vv = window.visualViewport;
if (!vv) return;
// Simple focus handler - scroll input into view after keyboard appears
document.addEventListener('focusin', (e) => {
const target = e.target;
if (!this.isInputElement(target)) return;
// Store initial height after a brief delay to let viewport stabilize
setTimeout(() => {
this.initialViewportHeight = window.visualViewport?.height || window.innerHeight;
}, 500);
// Create bound handler for cleanup
this.resizeHandler = this.handleResize.bind(this);
// Listen for visualViewport resize (keyboard open/close)
vv.addEventListener('resize', this.resizeHandler);
// Update initial height on orientation change
window.addEventListener('orientationchange', () => {
// Wait for keyboard animation, then scroll input into view
setTimeout(() => {
if (window.visualViewport) {
this.initialViewportHeight = window.visualViewport.height;
}
}, 300);
this.scrollInputIntoView(target);
}, 400);
});
// Also update on regular resize (address bar hide/show)
window.addEventListener('resize', () => {
// Only update if keyboard is NOT visible (address bar change)
if (!this.isKeyboardVisible && window.visualViewport) {
this.initialViewportHeight = window.visualViewport.height;
}
});
// Focus event handling for scrolling inputs into view
document.addEventListener('focusin', this.handleFocusIn.bind(this));
// Focusout handler to help reset state when leaving all inputs
document.addEventListener('focusout', this.handleFocusOut.bind(this));
},
/** Handle visualViewport resize - detect keyboard */
handleResize() {
const vv = window.visualViewport;
if (!vv) return;
const heightDiff = this.initialViewportHeight - vv.height;
const threshold = 150; // Minimum height difference to consider keyboard open
if (heightDiff > threshold) {
this.updateKeyboardState(true, heightDiff);
} else {
this.updateKeyboardState(false, 0);
}
},
/** Update keyboard visibility state */
updateKeyboardState(visible, height) {
if (this.isKeyboardVisible === visible && this.keyboardHeight === height) return;
this.isKeyboardVisible = visible;
this.keyboardHeight = height;
// Update CSS custom property
document.documentElement.style.setProperty('--keyboard-height', `${height}px`);
// Toggle class for CSS targeting
document.body.classList.toggle('keyboard-visible', visible);
// Dispatch custom event for any listeners
window.dispatchEvent(new CustomEvent('keyboardchange', {
detail: { visible, height }
}));
},
/** Handle focus on input elements - scroll into view after keyboard appears */
handleFocusIn(e) {
const target = e.target;
if (!this.isInputElement(target)) return;
// Wait for keyboard to appear before scrolling
setTimeout(() => {
this.scrollInputIntoView(target);
}, 350);
},
/** Handle focus leaving input elements - help reset keyboard state */
handleFocusOut(e) {
if (!this.isInputElement(e.target)) return;
// Small delay to check if focus moved to another input
setTimeout(() => {
const activeEl = document.activeElement;
// If focus left all inputs, ensure keyboard state is reset
if (!this.isInputElement(activeEl)) {
// The visualViewport resize event should handle this,
// but we reset just in case on iOS where it can be flaky
if (this.isKeyboardVisible && window.visualViewport) {
const vv = window.visualViewport;
const heightDiff = this.initialViewportHeight - vv.height;
if (heightDiff < 100) {
this.updateKeyboardState(false, 0);
}
}
}
}, 300);
},
/** Check if element is an input that triggers keyboard */
isInputElement(el) {
if (!el) return false;
const tagName = el.tagName?.toLowerCase();
// Exclude type=range, type=checkbox, type=radio (don't trigger keyboard)
if (tagName === 'input') {
const type = el.type?.toLowerCase();
if (type === 'checkbox' || type === 'radio' || type === 'range' || type === 'file') {
return false;
}
}
return (
tagName === 'input' ||
tagName === 'textarea' ||
tagName === 'select' ||
el.isContentEditable
);
},
/** Scroll input into view above the keyboard */
scrollInputIntoView(input) {
const vv = window.visualViewport;
// Check if input is still focused (user might have tapped away)
if (document.activeElement !== input) return;
// Find scrollable container (modal-body or main viewport)
const modalBody = input.closest('.modal-body');
const scrollContainer = modalBody || document.scrollingElement || document.body;
// Find if we're in a modal
const modal = input.closest('.modal.active');
const modalBody = modal?.querySelector('.modal-body');
if (vv) {
const rect = input.getBoundingClientRect();
const visibleHeight = vv.height;
const inputBottom = rect.bottom;
const inputTop = rect.top;
if (modalBody) {
// For modals - scroll within the modal body
const inputRect = input.getBoundingClientRect();
const modalRect = modalBody.getBoundingClientRect();
// If input is in bottom 40% of visible area or below it, scroll up
if (inputBottom > visibleHeight * 0.6 || inputTop > visibleHeight * 0.5) {
// For modal body, scroll within the modal
if (modalBody) {
const inputOffsetTop = input.offsetTop;
const targetScroll = inputOffsetTop - (visibleHeight * 0.3);
modalBody.scrollTo({
top: Math.max(0, targetScroll),
behavior: 'smooth'
});
} else {
// For page-level scroll
input.scrollIntoView({ block: 'center', behavior: 'smooth' });
}
// If input is below middle of modal, scroll it up
if (inputRect.top > modalRect.top + modalRect.height * 0.4) {
const scrollAmount = inputRect.top - modalRect.top - 100;
modalBody.scrollBy({ top: scrollAmount, behavior: 'smooth' });
}
} else {
// Fallback for browsers without visualViewport
// For page-level - use scrollIntoView
input.scrollIntoView({ block: 'center', behavior: 'smooth' });
}
}
+4 -53
View File
@@ -7879,17 +7879,6 @@ kbd {
.project-insights-panel {
display: none;
}
/* Tablet Keyboard Input Visibility */
input, textarea, select, [contenteditable] {
scroll-margin-bottom: 200px;
scroll-margin-top: 80px;
}
.keyboard-visible .modal.active .modal-body {
max-height: calc(60vh - var(--keyboard-height) / 2);
overflow-y: auto;
}
}
/* ============================================================================
@@ -8406,51 +8395,13 @@ kbd {
padding-bottom: var(--safe-area-bottom);
}
/* -------------------------------------------------------------------------
Mobile Keyboard Input Visibility
------------------------------------------------------------------------- */
/* Scroll margin for inputs - ensures they scroll above keyboard */
input, textarea, select, [contenteditable] {
scroll-margin-bottom: 250px;
scroll-margin-top: 100px;
}
/* Modal adjustments when keyboard is visible */
.keyboard-visible .modal.active .modal-content {
/* Don't take full height - resize with keyboard */
height: auto;
max-height: calc(100vh - var(--keyboard-height));
}
.keyboard-visible .modal.active .modal-body {
/* Limit modal body height when keyboard up - make room for header/footer */
max-height: calc(50vh - var(--keyboard-height) / 3);
overflow-y: auto;
-webkit-overflow-scrolling: touch;
/* Scroll margin for inputs - helps with keyboard visibility */
input, textarea, [contenteditable] {
scroll-margin-bottom: 200px;
scroll-margin-top: 80px;
}
}
/* ============================================================================
Virtual Keyboard Handling
============================================================================ */
:root {
--keyboard-height: 0px;
}
.keyboard-visible .toolbar {
display: none;
}
.keyboard-visible .monitor-panel,
.keyboard-visible .subagents-panel {
display: none;
}
/* Ensure terminal fills space when keyboard is visible */
.keyboard-visible .main {
height: 100%;
}
/* ============================================================================
iOS Safari Specific Fixes