mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-09 16:59:43 +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`
|
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`
|
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
|
## Project Overview
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "claudeman",
|
"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",
|
"description": "The missing control plane for Claude Code - run 20 autonomous agents with real-time monitoring and session persistence",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
|
|||||||
+31
-134
@@ -183,169 +183,66 @@ function getEventCoords(e) {
|
|||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* KeyboardHandler - Detects virtual keyboard visibility and scrolls inputs into view.
|
* KeyboardHandler - Simple handler to scroll inputs into view when keyboard appears.
|
||||||
* Uses visualViewport API (works on iOS Safari and Android Chrome).
|
* Uses focusin event and scrollIntoView - keeps it simple and reliable.
|
||||||
* Sets CSS custom property --keyboard-height and toggles .keyboard-visible class.
|
|
||||||
*/
|
*/
|
||||||
const KeyboardHandler = {
|
const KeyboardHandler = {
|
||||||
initialViewportHeight: null,
|
/** Initialize keyboard handling */
|
||||||
isKeyboardVisible: false,
|
|
||||||
keyboardHeight: 0,
|
|
||||||
resizeHandler: null,
|
|
||||||
|
|
||||||
/** Initialize keyboard detection */
|
|
||||||
init() {
|
init() {
|
||||||
// Only initialize on touch devices
|
// Only initialize on touch devices
|
||||||
if (!MobileDetection.isTouchDevice()) return;
|
if (!MobileDetection.isTouchDevice()) return;
|
||||||
|
|
||||||
const vv = window.visualViewport;
|
// Simple focus handler - scroll input into view after keyboard appears
|
||||||
if (!vv) return;
|
document.addEventListener('focusin', (e) => {
|
||||||
|
const target = e.target;
|
||||||
|
if (!this.isInputElement(target)) return;
|
||||||
|
|
||||||
// Store initial height after a brief delay to let viewport stabilize
|
// Wait for keyboard animation, then scroll input into view
|
||||||
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', () => {
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (window.visualViewport) {
|
this.scrollInputIntoView(target);
|
||||||
this.initialViewportHeight = window.visualViewport.height;
|
}, 400);
|
||||||
}
|
|
||||||
}, 300);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// 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 */
|
/** Check if element is an input that triggers keyboard */
|
||||||
isInputElement(el) {
|
isInputElement(el) {
|
||||||
if (!el) return false;
|
if (!el) return false;
|
||||||
const tagName = el.tagName?.toLowerCase();
|
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 (
|
return (
|
||||||
tagName === 'input' ||
|
tagName === 'input' ||
|
||||||
tagName === 'textarea' ||
|
tagName === 'textarea' ||
|
||||||
tagName === 'select' ||
|
|
||||||
el.isContentEditable
|
el.isContentEditable
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
/** Scroll input into view above the keyboard */
|
/** Scroll input into view above the keyboard */
|
||||||
scrollInputIntoView(input) {
|
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)
|
// Find if we're in a modal
|
||||||
const modalBody = input.closest('.modal-body');
|
const modal = input.closest('.modal.active');
|
||||||
const scrollContainer = modalBody || document.scrollingElement || document.body;
|
const modalBody = modal?.querySelector('.modal-body');
|
||||||
|
|
||||||
if (vv) {
|
if (modalBody) {
|
||||||
const rect = input.getBoundingClientRect();
|
// For modals - scroll within the modal body
|
||||||
const visibleHeight = vv.height;
|
const inputRect = input.getBoundingClientRect();
|
||||||
const inputBottom = rect.bottom;
|
const modalRect = modalBody.getBoundingClientRect();
|
||||||
const inputTop = rect.top;
|
|
||||||
|
|
||||||
// If input is in bottom 40% of visible area or below it, scroll up
|
// If input is below middle of modal, scroll it up
|
||||||
if (inputBottom > visibleHeight * 0.6 || inputTop > visibleHeight * 0.5) {
|
if (inputRect.top > modalRect.top + modalRect.height * 0.4) {
|
||||||
// For modal body, scroll within the modal
|
const scrollAmount = inputRect.top - modalRect.top - 100;
|
||||||
if (modalBody) {
|
modalBody.scrollBy({ top: scrollAmount, behavior: 'smooth' });
|
||||||
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' });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Fallback for browsers without visualViewport
|
// For page-level - use scrollIntoView
|
||||||
input.scrollIntoView({ block: 'center', behavior: 'smooth' });
|
input.scrollIntoView({ block: 'center', behavior: 'smooth' });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7879,17 +7879,6 @@ kbd {
|
|||||||
.project-insights-panel {
|
.project-insights-panel {
|
||||||
display: none;
|
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);
|
padding-bottom: var(--safe-area-bottom);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* -------------------------------------------------------------------------
|
/* Scroll margin for inputs - helps with keyboard visibility */
|
||||||
Mobile Keyboard Input Visibility
|
input, textarea, [contenteditable] {
|
||||||
------------------------------------------------------------------------- */
|
scroll-margin-bottom: 200px;
|
||||||
|
scroll-margin-top: 80px;
|
||||||
/* 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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ============================================================================
|
|
||||||
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
|
iOS Safari Specific Fixes
|
||||||
|
|||||||
Reference in New Issue
Block a user