mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-02 05:29:42 +02:00
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:
@@ -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
@@ -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
@@ -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' });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user