mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-02 13:39:41 +02:00
feat: mobile keyboard input visibility - scroll inputs into view when keyboard opens
- Add KeyboardHandler module using visualViewport API for keyboard detection - Set --keyboard-height CSS variable and toggle .keyboard-visible class - Auto-scroll focused inputs into view after keyboard appears (350ms delay) - Update viewport meta with interactive-widget=resizes-content for Android - Add scroll-margin on inputs for tablet/phone breakpoints - Resize modal-content and modal-body when keyboard visible on mobile 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.1450 (must match `package.json`)
|
||||
**Version**: 0.1451 (must match `package.json`)
|
||||
|
||||
## Project Overview
|
||||
|
||||
@@ -24,7 +24,7 @@ Claudeman is a Claude Code session manager with web interface and autonomous Ral
|
||||
|
||||
**Tech Stack**: TypeScript (ES2022/NodeNext, strict mode), Node.js, Fastify, node-pty, xterm.js
|
||||
|
||||
**TypeScript Strictness** (see `tsconfig.json`): `noUnusedLocals`, `noUnusedParameters`, `noImplicitReturns`, `noImplicitOverride`, `noFallthroughCasesInSwitch`, `allowUnreachableCode: false`, `allowUnusedLabels: false`
|
||||
**TypeScript Strictness** (see `tsconfig.json`): `noUnusedLocals`, `noUnusedParameters`, `noImplicitReturns`, `noImplicitOverride`, `noFallthroughCasesInSwitch`, `allowUnreachableCode: false`, `allowUnusedLabels: false`. Note: `src/tui` is excluded from compilation (legacy/deprecated code path).
|
||||
|
||||
**Requirements**: Node.js 18+, Claude CLI, GNU Screen
|
||||
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "claudeman",
|
||||
"version": "0.1450",
|
||||
"version": "0.1451",
|
||||
"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",
|
||||
|
||||
@@ -133,8 +133,9 @@ const MAX_TRACKED_AGENTS = 500; // Maximum agents to track (LRU eviction when ex
|
||||
const INTERNAL_AGENT_PATTERNS = [
|
||||
/^\[?SUGGESTION MODE/i, // Claude Code's internal suggestion mode
|
||||
/^Suggest what user might/i, // Suggestion mode prompt variant
|
||||
/^aprompt$/i, // Internal prompt agent
|
||||
/aprompt/i, // Internal prompt agent (anywhere in string)
|
||||
/^a\s?prompt/i, // Variants of internal prompt agent
|
||||
/^prompt$/i, // Just "prompt"
|
||||
];
|
||||
|
||||
// Minimum description length - very short descriptions are likely internal or malformed
|
||||
|
||||
@@ -178,6 +178,156 @@ function getEventCoords(e) {
|
||||
return { clientX: e.clientX, clientY: e.clientY };
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Mobile Keyboard Handler
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
const KeyboardHandler = {
|
||||
initialViewportHeight: null,
|
||||
isKeyboardVisible: false,
|
||||
keyboardHeight: 0,
|
||||
resizeHandler: null,
|
||||
scrollHandler: null,
|
||||
|
||||
/** Initialize keyboard detection */
|
||||
init() {
|
||||
// Only initialize on touch devices
|
||||
if (!MobileDetection.isTouchDevice()) return;
|
||||
|
||||
const vv = window.visualViewport;
|
||||
if (!vv) return;
|
||||
|
||||
// Store initial height (update on orientation change)
|
||||
this.initialViewportHeight = vv.height;
|
||||
|
||||
// Create bound handlers for cleanup
|
||||
this.resizeHandler = this.handleResize.bind(this);
|
||||
this.scrollHandler = this.handleVisualViewportScroll.bind(this);
|
||||
|
||||
// Listen for visualViewport resize (keyboard open/close)
|
||||
vv.addEventListener('resize', this.resizeHandler);
|
||||
vv.addEventListener('scroll', this.scrollHandler);
|
||||
|
||||
// Update initial height on orientation change
|
||||
window.addEventListener('orientationchange', () => {
|
||||
setTimeout(() => {
|
||||
if (window.visualViewport) {
|
||||
this.initialViewportHeight = window.visualViewport.height;
|
||||
}
|
||||
}, 200);
|
||||
});
|
||||
|
||||
// Focus event handling for scrolling inputs into view
|
||||
document.addEventListener('focusin', this.handleFocusIn.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);
|
||||
}
|
||||
},
|
||||
|
||||
/** Handle visualViewport scroll - keep fixed elements positioned on iOS */
|
||||
handleVisualViewportScroll() {
|
||||
const vv = window.visualViewport;
|
||||
if (!vv || !this.isKeyboardVisible) return;
|
||||
|
||||
// Update CSS property for any elements that need to track viewport offset
|
||||
document.documentElement.style.setProperty('--viewport-offset-top', `${vv.offsetTop}px`);
|
||||
},
|
||||
|
||||
/** 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);
|
||||
},
|
||||
|
||||
/** Check if element is an input that triggers keyboard */
|
||||
isInputElement(el) {
|
||||
if (!el) return false;
|
||||
const tagName = el.tagName?.toLowerCase();
|
||||
return (
|
||||
tagName === 'input' ||
|
||||
tagName === 'textarea' ||
|
||||
tagName === 'select' ||
|
||||
el.isContentEditable
|
||||
);
|
||||
},
|
||||
|
||||
/** Scroll input into view above the keyboard */
|
||||
scrollInputIntoView(input) {
|
||||
const vv = window.visualViewport;
|
||||
|
||||
// Find scrollable container (modal-body or main viewport)
|
||||
const modalBody = input.closest('.modal-body');
|
||||
const scrollContainer = modalBody || document.scrollingElement || document.body;
|
||||
|
||||
if (vv) {
|
||||
const rect = input.getBoundingClientRect();
|
||||
const visibleHeight = vv.height;
|
||||
const inputBottom = rect.bottom;
|
||||
const inputTop = rect.top;
|
||||
|
||||
// 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' });
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Fallback for browsers without visualViewport
|
||||
input.scrollIntoView({ block: 'center', behavior: 'smooth' });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Accessibility: Focus Trap for Modals
|
||||
// ============================================================================
|
||||
@@ -884,6 +1034,8 @@ class ClaudemanApp {
|
||||
init() {
|
||||
// Initialize mobile detection first (adds device classes to body)
|
||||
MobileDetection.init();
|
||||
// Initialize keyboard handler for mobile input visibility
|
||||
KeyboardHandler.init();
|
||||
this.initTerminal();
|
||||
this.loadFontSize();
|
||||
this.applyHeaderVisibilitySettings();
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, interactive-widget=resizes-content">
|
||||
<meta name="description" content="Claude Code session manager with web interface">
|
||||
<meta name="theme-color" content="#0a0a0a">
|
||||
<title>Claudeman</title>
|
||||
|
||||
@@ -7879,6 +7879,17 @@ 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-body {
|
||||
max-height: calc(60vh - var(--keyboard-height) / 2);
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
|
||||
/* ============================================================================
|
||||
@@ -7957,6 +7968,7 @@ kbd {
|
||||
/* App container - prevent scrolling, strict layout */
|
||||
.app {
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Ultra-compact session tabs */
|
||||
@@ -8015,21 +8027,25 @@ kbd {
|
||||
height: 14px;
|
||||
}
|
||||
|
||||
/* Clean dark toolbar - simple black/gray with white text */
|
||||
/* Clean dark toolbar - overlaps terminal to hide Claude's status/input */
|
||||
.toolbar {
|
||||
position: absolute;
|
||||
bottom: calc(30px + var(--safe-area-bottom));
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 40px;
|
||||
min-height: 40px;
|
||||
max-height: 40px;
|
||||
padding: 0.3rem 0.4rem;
|
||||
padding-left: calc(0.4rem + var(--safe-area-left));
|
||||
padding-right: calc(0.4rem + var(--safe-area-right));
|
||||
padding-bottom: calc(0.3rem + var(--safe-area-bottom));
|
||||
flex-wrap: nowrap;
|
||||
gap: 0.3rem;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background: #111111;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||||
z-index: 50;
|
||||
}
|
||||
|
||||
/* Show case selector in center */
|
||||
@@ -8389,11 +8405,55 @@ kbd {
|
||||
padding-right: var(--safe-area-right);
|
||||
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-content {
|
||||
/* Don't take full height - resize with keyboard */
|
||||
height: auto;
|
||||
max-height: calc(100dvh - var(--keyboard-height) - var(--safe-area-top));
|
||||
/* Anchor to top */
|
||||
top: var(--safe-area-top);
|
||||
bottom: auto;
|
||||
}
|
||||
|
||||
.keyboard-visible .modal-body {
|
||||
/* Limit modal body height when keyboard up - make room for header/footer */
|
||||
max-height: calc(50dvh - var(--keyboard-height) / 3);
|
||||
overflow-y: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
/* Ensure modal footer stays visible above keyboard */
|
||||
.keyboard-visible .modal-footer,
|
||||
.keyboard-visible .wizard-footer,
|
||||
.keyboard-visible .form-actions {
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
/* Smooth transition for keyboard appearance */
|
||||
.modal-content {
|
||||
transition: max-height 0.2s ease-out, top 0.2s ease-out;
|
||||
}
|
||||
}
|
||||
|
||||
/* ============================================================================
|
||||
Virtual Keyboard Handling
|
||||
============================================================================ */
|
||||
:root {
|
||||
--keyboard-height: 0px;
|
||||
--viewport-offset-top: 0px;
|
||||
}
|
||||
|
||||
.keyboard-visible .toolbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user