mirror of
https://github.com/samsonjs/vibetunnel.git
synced 2026-03-25 09:25:50 +00:00
Fix iOS keyboard dismissal issue (#484)
Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
parent
e9a1ce0555
commit
32935878d8
9 changed files with 332 additions and 84 deletions
|
|
@ -48,8 +48,8 @@ export class ModalWrapper extends LitElement {
|
|||
}
|
||||
}
|
||||
|
||||
// Focus management
|
||||
if (changedProperties.has('visible') && this.visible) {
|
||||
// Focus management - but not if we have a special attribute to prevent it
|
||||
if (changedProperties.has('visible') && this.visible && !this.hasAttribute('no-autofocus')) {
|
||||
requestAnimationFrame(() => {
|
||||
const focusable = this.querySelector(
|
||||
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
|
||||
|
|
|
|||
|
|
@ -119,6 +119,9 @@ export class SessionView extends LitElement {
|
|||
setShowQuickKeys: (value: boolean) => this.directKeyboardManager.setShowQuickKeys(value),
|
||||
ensureHiddenInputVisible: () => this.directKeyboardManager.ensureHiddenInputVisible(),
|
||||
cleanup: () => this.directKeyboardManager.cleanup(),
|
||||
getKeyboardMode: () => this.directKeyboardManager.getKeyboardMode(),
|
||||
isRecentlyEnteredKeyboardMode: () =>
|
||||
this.directKeyboardManager.isRecentlyEnteredKeyboardMode(),
|
||||
}),
|
||||
setShowQuickKeys: (value: boolean) => {
|
||||
this.uiStateManager.setShowQuickKeys(value);
|
||||
|
|
@ -778,9 +781,13 @@ export class SessionView extends LitElement {
|
|||
this.uiStateManager.clearCtrlSequence();
|
||||
this.uiStateManager.setShowCtrlAlpha(false);
|
||||
|
||||
// Refocus the hidden input
|
||||
// Refocus the hidden input and restart focus retention
|
||||
if (this.directKeyboardManager.shouldRefocusHiddenInput()) {
|
||||
this.directKeyboardManager.refocusHiddenInput();
|
||||
// Use a small delay to ensure the modal is fully closed first
|
||||
setTimeout(() => {
|
||||
this.directKeyboardManager.refocusHiddenInput();
|
||||
this.directKeyboardManager.startFocusRetentionPublic();
|
||||
}, 50);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -792,9 +799,13 @@ export class SessionView extends LitElement {
|
|||
this.uiStateManager.setShowCtrlAlpha(false);
|
||||
this.uiStateManager.clearCtrlSequence();
|
||||
|
||||
// Refocus the hidden input
|
||||
// Refocus the hidden input and restart focus retention
|
||||
if (this.directKeyboardManager.shouldRefocusHiddenInput()) {
|
||||
this.directKeyboardManager.refocusHiddenInput();
|
||||
// Use a small delay to ensure the modal is fully closed first
|
||||
setTimeout(() => {
|
||||
this.directKeyboardManager.refocusHiddenInput();
|
||||
this.directKeyboardManager.startFocusRetentionPublic();
|
||||
}, 50);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -889,6 +900,12 @@ export class SessionView extends LitElement {
|
|||
if ('scrollToBottom' in terminal) {
|
||||
terminal.scrollToBottom();
|
||||
}
|
||||
|
||||
// Also ensure the terminal content is scrolled within its container
|
||||
const terminalArea = this.querySelector('.terminal-area');
|
||||
if (terminalArea) {
|
||||
terminalArea.scrollTop = terminalArea.scrollHeight;
|
||||
}
|
||||
}, 50);
|
||||
}
|
||||
}
|
||||
|
|
@ -1019,10 +1036,23 @@ export class SessionView extends LitElement {
|
|||
contain: layout style paint; /* Isolate terminal updates */
|
||||
}
|
||||
|
||||
/* Add padding to terminal when quick keys are visible */
|
||||
/* Make terminal content 50px larger to prevent clipping */
|
||||
.terminal-area vibe-terminal,
|
||||
.terminal-area vibe-terminal-binary {
|
||||
height: calc(100% + 50px) !important;
|
||||
margin-bottom: -50px !important;
|
||||
}
|
||||
|
||||
/* Transform terminal up when quick keys are visible */
|
||||
.terminal-area[data-quickkeys-visible="true"] {
|
||||
transform: translateY(-110px);
|
||||
transition: transform 0.2s ease-out;
|
||||
}
|
||||
|
||||
/* Add padding to terminal content when keyboard is visible */
|
||||
.terminal-area[data-quickkeys-visible="true"] vibe-terminal,
|
||||
.terminal-area[data-quickkeys-visible="true"] vibe-terminal-binary {
|
||||
padding-bottom: 120px !important;
|
||||
padding-bottom: 70px !important;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
|
|
@ -1123,7 +1153,7 @@ export class SessionView extends LitElement {
|
|||
// Add safe area padding for landscape mode on mobile to handle notch
|
||||
uiState.isMobile && uiState.isLandscape ? 'safe-area-left safe-area-right' : ''
|
||||
}"
|
||||
data-quickkeys-visible="${uiState.showQuickKeys && uiState.keyboardHeight > 0}"
|
||||
data-quickkeys-visible="${uiState.showQuickKeys}"
|
||||
>
|
||||
${
|
||||
this.loadingAnimationManager.isLoading()
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@
|
|||
*/
|
||||
import { html, LitElement } from 'lit';
|
||||
import { customElement, property } from 'lit/decorators.js';
|
||||
import '../modal-wrapper.js';
|
||||
|
||||
@customElement('ctrl-alpha-overlay')
|
||||
export class CtrlAlphaOverlay extends LitElement {
|
||||
|
|
@ -28,24 +27,26 @@ export class CtrlAlphaOverlay extends LitElement {
|
|||
}
|
||||
|
||||
render() {
|
||||
console.log('[CtrlAlphaOverlay] render called, visible:', this.visible);
|
||||
if (!this.visible) return null;
|
||||
|
||||
// Render directly without modal-wrapper to debug the issue
|
||||
return html`
|
||||
<modal-wrapper
|
||||
.visible=${this.visible}
|
||||
modalClass="" /* Use modal-wrapper's default z-index */
|
||||
contentClass="fixed inset-0 flex flex-col" /* Use modal-wrapper's default z-index */
|
||||
ariaLabel="Ctrl key sequence builder"
|
||||
@close=${() => this.onCancel?.()}
|
||||
.closeOnBackdrop=${true}
|
||||
.closeOnEscape=${false}
|
||||
<!-- Direct backdrop -->
|
||||
<div
|
||||
class="fixed inset-0 bg-bg/80 flex items-center justify-center p-4"
|
||||
style="z-index: 1000; backdrop-filter: blur(4px); -webkit-backdrop-filter: blur(4px);"
|
||||
@click=${(e: Event) => {
|
||||
if (e.target === e.currentTarget) {
|
||||
this.onCancel?.();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<!-- Spacer to push content up above keyboard -->
|
||||
<div class="flex-1"></div>
|
||||
|
||||
<!-- Modal content -->
|
||||
<div
|
||||
class="font-mono text-sm mx-4 max-w-sm w-full self-center bg-bg border border-primary rounded-lg p-2.5"
|
||||
style="margin-bottom: ${this.keyboardHeight > 0 ? `${this.keyboardHeight}px` : 'env(keyboard-inset-height, 0px)'};"
|
||||
class="bg-surface border-2 border-primary rounded-lg p-4 shadow-xl relative"
|
||||
style="z-index: 1001; background-color: rgb(var(--color-bg-secondary)); max-height: 80vh; overflow-y: auto; max-width: 24rem; width: 100%;"
|
||||
@click=${(e: Event) => e.stopPropagation()}
|
||||
>
|
||||
<div class="text-primary text-center mb-2 font-bold">Ctrl + Key</div>
|
||||
|
||||
|
|
@ -142,7 +143,7 @@ export class CtrlAlphaOverlay extends LitElement {
|
|||
}
|
||||
</div>
|
||||
</div>
|
||||
</modal-wrapper>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@
|
|||
import { Z_INDEX } from '../../utils/constants.js';
|
||||
import { createLogger } from '../../utils/logger.js';
|
||||
import type { InputManager } from './input-manager.js';
|
||||
import { ManagerEventEmitter } from './interfaces.js';
|
||||
|
||||
const logger = createLogger('direct-keyboard-manager');
|
||||
|
||||
|
|
@ -47,7 +48,7 @@ export interface DirectKeyboardCallbacks {
|
|||
clearCtrlSequence(): void;
|
||||
}
|
||||
|
||||
export class DirectKeyboardManager {
|
||||
export class DirectKeyboardManager extends ManagerEventEmitter {
|
||||
private hiddenInput: HTMLInputElement | null = null;
|
||||
private focusRetentionInterval: number | null = null;
|
||||
private inputManager: InputManager | null = null;
|
||||
|
|
@ -67,16 +68,17 @@ export class DirectKeyboardManager {
|
|||
private instanceId: string;
|
||||
// biome-ignore lint/correctness/noUnusedPrivateClassMembers: Used for focus state management
|
||||
private hiddenInputFocused = false;
|
||||
// biome-ignore lint/correctness/noUnusedPrivateClassMembers: Used for keyboard mode timing
|
||||
private keyboardModeTimestamp = 0;
|
||||
// biome-ignore lint/correctness/noUnusedPrivateClassMembers: Used for IME composition
|
||||
private compositionBuffer = '';
|
||||
|
||||
constructor(instanceId: string) {
|
||||
super();
|
||||
this.instanceId = instanceId;
|
||||
|
||||
// Add global paste listener for environments where Clipboard API doesn't work
|
||||
this.setupGlobalPasteListener();
|
||||
this.ensureHiddenInputVisible();
|
||||
}
|
||||
|
||||
setInputManager(inputManager: InputManager): void {
|
||||
|
|
@ -119,10 +121,10 @@ export class DirectKeyboardManager {
|
|||
|
||||
focusHiddenInput(): void {
|
||||
logger.log('Entering keyboard mode');
|
||||
|
||||
// Enter keyboard mode
|
||||
this.keyboardMode = true;
|
||||
this.keyboardModeTimestamp = Date.now();
|
||||
this.updateHiddenInputPosition();
|
||||
|
||||
// Add capture phase click handler to prevent any clicks from stealing focus
|
||||
if (!this.captureClickHandler) {
|
||||
|
|
@ -183,6 +185,11 @@ export class DirectKeyboardManager {
|
|||
ensureHiddenInputVisible(): void {
|
||||
if (!this.hiddenInput) {
|
||||
this.createHiddenInput();
|
||||
} else {
|
||||
// Make sure it's in the DOM
|
||||
if (!this.hiddenInput.parentNode) {
|
||||
document.body.appendChild(this.hiddenInput);
|
||||
}
|
||||
}
|
||||
|
||||
// Show quick keys immediately when entering keyboard mode
|
||||
|
|
@ -224,6 +231,8 @@ export class DirectKeyboardManager {
|
|||
this.hiddenInput = document.createElement('input');
|
||||
this.hiddenInput.type = 'text';
|
||||
this.hiddenInput.style.position = 'absolute';
|
||||
|
||||
// Hidden input that receives keyboard focus
|
||||
this.hiddenInput.style.opacity = '0.01'; // iOS needs non-zero opacity
|
||||
this.hiddenInput.style.fontSize = '16px'; // Prevent zoom on iOS
|
||||
this.hiddenInput.style.border = 'none';
|
||||
|
|
@ -233,6 +242,7 @@ export class DirectKeyboardManager {
|
|||
this.hiddenInput.style.caretColor = 'transparent';
|
||||
this.hiddenInput.style.cursor = 'default';
|
||||
this.hiddenInput.style.pointerEvents = 'none'; // Start with pointer events disabled
|
||||
this.hiddenInput.placeholder = '';
|
||||
this.hiddenInput.style.webkitUserSelect = 'text'; // iOS specific
|
||||
this.hiddenInput.autocapitalize = 'none'; // More explicit than 'off'
|
||||
this.hiddenInput.autocomplete = 'off';
|
||||
|
|
@ -391,8 +401,9 @@ export class DirectKeyboardManager {
|
|||
if (this.keyboardMode) {
|
||||
logger.log('In keyboard mode - maintaining focus');
|
||||
|
||||
// Immediately try to refocus
|
||||
// Add a small delay to allow Done button to exit keyboard mode first
|
||||
setTimeout(() => {
|
||||
// Re-check keyboard mode after delay - Done button might have exited it
|
||||
if (
|
||||
this.keyboardMode &&
|
||||
this.hiddenInput &&
|
||||
|
|
@ -401,7 +412,7 @@ export class DirectKeyboardManager {
|
|||
logger.log('Refocusing hidden input to maintain keyboard');
|
||||
this.hiddenInput.focus();
|
||||
}
|
||||
}, 0);
|
||||
}, 50); // 50ms delay to allow Done button processing
|
||||
|
||||
// Don't exit keyboard mode or hide quick keys
|
||||
return;
|
||||
|
|
@ -437,11 +448,8 @@ export class DirectKeyboardManager {
|
|||
}
|
||||
});
|
||||
|
||||
// Add to the terminal container
|
||||
const terminalContainer = this.sessionViewElement?.querySelector('#terminal-container');
|
||||
if (terminalContainer) {
|
||||
terminalContainer.appendChild(this.hiddenInput);
|
||||
}
|
||||
// Add to the body for debugging (so it's always visible)
|
||||
document.body.appendChild(this.hiddenInput);
|
||||
}
|
||||
|
||||
handleQuickKeyPress = async (
|
||||
|
|
@ -458,6 +466,7 @@ export class DirectKeyboardManager {
|
|||
if (isSpecial && key === 'Done') {
|
||||
// Dismiss the keyboard
|
||||
logger.log('Done button pressed - dismissing keyboard');
|
||||
// Set a flag to prevent refocus attempts
|
||||
this.dismissKeyboard();
|
||||
return;
|
||||
} else if (isModifier && key === 'Control') {
|
||||
|
|
@ -466,22 +475,17 @@ export class DirectKeyboardManager {
|
|||
return;
|
||||
} else if (key === 'CtrlFull') {
|
||||
// Toggle the full Ctrl+Alpha overlay
|
||||
console.log('[DirectKeyboardManager] CtrlFull pressed, toggling Ctrl+Alpha overlay');
|
||||
if (this.callbacks) {
|
||||
this.callbacks.toggleCtrlAlpha();
|
||||
}
|
||||
|
||||
const showCtrlAlpha = this.callbacks?.getShowCtrlAlpha() ?? false;
|
||||
console.log('[DirectKeyboardManager] showCtrlAlpha after toggle:', showCtrlAlpha);
|
||||
if (showCtrlAlpha) {
|
||||
// Stop focus retention when showing Ctrl overlay
|
||||
if (this.focusRetentionInterval) {
|
||||
clearInterval(this.focusRetentionInterval);
|
||||
this.focusRetentionInterval = null;
|
||||
}
|
||||
|
||||
// Blur the hidden input to prevent it from capturing input
|
||||
if (this.hiddenInput) {
|
||||
this.hiddenInput.blur();
|
||||
}
|
||||
// Keep focus retention running - we want the keyboard to stay visible
|
||||
// The Ctrl+Alpha overlay should show above the keyboard
|
||||
// Don't stop focus retention or blur the input
|
||||
} else {
|
||||
// Clear the Ctrl sequence when closing
|
||||
if (this.callbacks) {
|
||||
|
|
@ -581,6 +585,10 @@ export class DirectKeyboardManager {
|
|||
} else if (key === 'Delete') {
|
||||
// Send delete key
|
||||
this.inputManager.sendInput('delete');
|
||||
} else if (key === 'Done') {
|
||||
// Safety check - Done should have been handled earlier
|
||||
this.dismissKeyboard();
|
||||
return;
|
||||
} else if (key.startsWith('F')) {
|
||||
// Handle function keys F1-F12
|
||||
const fNum = Number.parseInt(key.substring(1));
|
||||
|
|
@ -613,7 +621,13 @@ export class DirectKeyboardManager {
|
|||
}
|
||||
|
||||
// Send the key to terminal
|
||||
this.inputManager.sendInput(keyToSend.toLowerCase());
|
||||
// For single character keys, send as text
|
||||
if (keyToSend.length === 1) {
|
||||
this.inputManager.sendInputText(keyToSend);
|
||||
} else {
|
||||
// For special keys, send as input command
|
||||
this.inputManager.sendInput(keyToSend.toLowerCase());
|
||||
}
|
||||
}
|
||||
|
||||
// Always keep focus on hidden input after any key press (except Done)
|
||||
|
|
@ -689,14 +703,15 @@ export class DirectKeyboardManager {
|
|||
if (!this.hiddenInput) return;
|
||||
|
||||
if (this.keyboardMode) {
|
||||
// In keyboard mode: cover the terminal to receive input
|
||||
this.hiddenInput.style.position = 'absolute';
|
||||
this.hiddenInput.style.top = '0';
|
||||
this.hiddenInput.style.left = '0';
|
||||
this.hiddenInput.style.width = '100%';
|
||||
// In keyboard mode: position at bottom center but invisible
|
||||
this.hiddenInput.style.position = 'fixed';
|
||||
this.hiddenInput.style.bottom = '50px'; // Above quick keys
|
||||
this.hiddenInput.style.left = '50%';
|
||||
this.hiddenInput.style.transform = 'translateX(-50%)';
|
||||
this.hiddenInput.style.width = '1px';
|
||||
this.hiddenInput.style.height = '1px';
|
||||
this.hiddenInput.style.zIndex = String(Z_INDEX.TERMINAL_OVERLAY);
|
||||
this.hiddenInput.style.pointerEvents = 'none';
|
||||
this.hiddenInput.style.zIndex = String(Z_INDEX.TERMINAL_OVERLAY + 100);
|
||||
this.hiddenInput.style.pointerEvents = 'auto'; // Allow focus
|
||||
} else {
|
||||
// In scroll mode: position off-screen
|
||||
this.hiddenInput.style.position = 'fixed';
|
||||
|
|
@ -897,4 +912,61 @@ export class DirectKeyboardManager {
|
|||
this.hiddenInput = null;
|
||||
}
|
||||
}
|
||||
|
||||
getKeyboardMode(): boolean {
|
||||
return this.keyboardMode;
|
||||
}
|
||||
|
||||
isRecentlyEnteredKeyboardMode(): boolean {
|
||||
// Check if we entered keyboard mode within the last 2 seconds
|
||||
// This helps prevent iOS keyboard animation from being interrupted
|
||||
if (!this.keyboardMode) return false;
|
||||
|
||||
const timeSinceEntry = Date.now() - this.keyboardModeTimestamp;
|
||||
return timeSinceEntry < 2000; // 2 seconds
|
||||
}
|
||||
|
||||
showVisibleInputForKeyboard(): void {
|
||||
// Prevent multiple inputs
|
||||
if (document.getElementById('vibe-visible-keyboard-input')) return;
|
||||
|
||||
const input = document.createElement('input');
|
||||
input.type = 'text';
|
||||
input.id = 'vibe-visible-keyboard-input';
|
||||
input.placeholder = 'Type here...';
|
||||
input.style.position = 'fixed';
|
||||
input.style.bottom = '80px'; // Just above your "Show Keyboard" button
|
||||
input.style.left = '50%';
|
||||
input.style.transform = 'translateX(-50%)';
|
||||
input.style.zIndex = '9999';
|
||||
input.style.fontSize = '18px';
|
||||
input.style.padding = '0.5em';
|
||||
input.style.background = '#fff';
|
||||
input.style.color = '#000';
|
||||
input.style.border = '1px solid #ccc';
|
||||
input.style.borderRadius = '6px';
|
||||
|
||||
document.body.appendChild(input);
|
||||
|
||||
// Add a slight delay before focusing
|
||||
setTimeout(() => {
|
||||
input.focus();
|
||||
console.log('Input focused:', document.activeElement === input);
|
||||
}, 50);
|
||||
|
||||
// On blur or enter, remove input and send text
|
||||
const cleanup = () => {
|
||||
if (input.value && this.inputManager) {
|
||||
this.inputManager.sendInputText(input.value);
|
||||
}
|
||||
input.remove();
|
||||
};
|
||||
|
||||
input.addEventListener('blur', cleanup);
|
||||
input.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
cleanup();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -68,6 +68,8 @@ export interface ManagerAccessCallbacks {
|
|||
setShowQuickKeys?(value: boolean): void;
|
||||
ensureHiddenInputVisible(): void;
|
||||
cleanup(): void;
|
||||
getKeyboardMode(): boolean;
|
||||
isRecentlyEnteredKeyboardMode(): boolean;
|
||||
};
|
||||
getInputManager(): { isKeyboardShortcut(e: KeyboardEvent): boolean } | null;
|
||||
getTerminalLifecycleManager(): {
|
||||
|
|
|
|||
|
|
@ -413,6 +413,18 @@ export class LifecycleEventManager extends ManagerEventEmitter {
|
|||
directKeyboardManager &&
|
||||
directKeyboardManager.getShowQuickKeys()
|
||||
) {
|
||||
// Check if we recently entered keyboard mode (within last 2 seconds)
|
||||
// This prevents iOS keyboard animation from being interrupted
|
||||
const isRecentlyEntered =
|
||||
directKeyboardManager.isRecentlyEnteredKeyboardMode?.() ?? false;
|
||||
|
||||
if (isRecentlyEntered) {
|
||||
logger.log(
|
||||
'Ignoring keyboard dismissal - recently entered keyboard mode, likely iOS animation'
|
||||
);
|
||||
return; // Don't hide quick keys during iOS keyboard animation
|
||||
}
|
||||
|
||||
// Force hide quick keys when keyboard dismisses
|
||||
this.callbacks.setShowQuickKeys(false);
|
||||
|
||||
|
|
|
|||
|
|
@ -107,15 +107,30 @@ export class OverlaysContainer extends LitElement {
|
|||
></mobile-input-overlay>
|
||||
|
||||
<!-- Ctrl+Alpha Overlay -->
|
||||
<ctrl-alpha-overlay
|
||||
.visible=${this.uiState.isMobile && this.uiState.showCtrlAlpha}
|
||||
.ctrlSequence=${this.uiState.ctrlSequence}
|
||||
.keyboardHeight=${this.uiState.keyboardHeight}
|
||||
.onCtrlKey=${this.callbacks.onCtrlKey}
|
||||
.onSendSequence=${this.callbacks.onSendCtrlSequence}
|
||||
.onClearSequence=${this.callbacks.onClearCtrlSequence}
|
||||
.onCancel=${this.callbacks.onCtrlAlphaCancel}
|
||||
></ctrl-alpha-overlay>
|
||||
${(() => {
|
||||
const visible = this.uiState.isMobile && this.uiState.showCtrlAlpha;
|
||||
console.log(
|
||||
'[OverlaysContainer] Ctrl+Alpha visible:',
|
||||
visible,
|
||||
'isMobile:',
|
||||
this.uiState.isMobile,
|
||||
'showCtrlAlpha:',
|
||||
this.uiState.showCtrlAlpha,
|
||||
'z-index should be above',
|
||||
Z_INDEX.TERMINAL_QUICK_KEYS
|
||||
);
|
||||
return html`
|
||||
<ctrl-alpha-overlay
|
||||
.visible=${visible}
|
||||
.ctrlSequence=${this.uiState.ctrlSequence}
|
||||
.keyboardHeight=${this.uiState.keyboardHeight}
|
||||
.onCtrlKey=${this.callbacks.onCtrlKey}
|
||||
.onSendSequence=${this.callbacks.onSendCtrlSequence}
|
||||
.onClearSequence=${this.callbacks.onClearCtrlSequence}
|
||||
.onCancel=${this.callbacks.onCtrlAlphaCancel}
|
||||
></ctrl-alpha-overlay>
|
||||
`;
|
||||
})()}
|
||||
|
||||
<!-- Floating Keyboard Button (for direct keyboard mode on mobile) -->
|
||||
${
|
||||
|
|
@ -126,10 +141,6 @@ export class OverlaysContainer extends LitElement {
|
|||
@pointerdown=${(e: PointerEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}}
|
||||
@click=${(e: MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this.callbacks?.onKeyboardButtonClick();
|
||||
}}
|
||||
title="Show keyboard"
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { html, LitElement, type PropertyValues } from 'lit';
|
||||
import { customElement, property, state } from 'lit/decorators.js';
|
||||
import { Z_INDEX } from '../utils/constants.js';
|
||||
|
||||
// Terminal-specific quick keys for mobile use
|
||||
const TERMINAL_QUICK_KEYS = [
|
||||
|
|
@ -27,7 +28,6 @@ const TERMINAL_QUICK_KEYS = [
|
|||
{ key: '/', label: '/', row: 2 },
|
||||
{ key: '\\', label: '\\', row: 2 },
|
||||
{ key: '-', label: '-', row: 2 },
|
||||
{ key: 'Done', label: 'Done', special: true, row: 2 },
|
||||
// Third row - additional special characters
|
||||
{ key: 'Option', label: '⌥', modifier: true, row: 3 },
|
||||
{ key: 'Command', label: '⌘', modifier: true, row: 3 },
|
||||
|
|
@ -63,6 +63,9 @@ const FUNCTION_KEYS = Array.from({ length: 12 }, (_, i) => ({
|
|||
func: true,
|
||||
}));
|
||||
|
||||
// Done button - always visible
|
||||
const DONE_BUTTON = { key: 'Done', label: 'Done', special: true };
|
||||
|
||||
@customElement('terminal-quick-keys')
|
||||
export class TerminalQuickKeys extends LitElement {
|
||||
createRenderRoot() {
|
||||
|
|
@ -110,8 +113,8 @@ export class TerminalQuickKeys extends LitElement {
|
|||
}
|
||||
|
||||
private getButtonSizeClass(_label: string): string {
|
||||
// Use flexible sizing without constraining width
|
||||
return this.isLandscape ? 'px-1 py-1' : 'px-1.5 py-1.5';
|
||||
// Use minimal padding to fit more buttons
|
||||
return this.isLandscape ? 'px-0.5 py-1' : 'px-1 py-1.5';
|
||||
}
|
||||
|
||||
private getButtonFontClass(label: string): string {
|
||||
|
|
@ -201,6 +204,7 @@ export class TerminalQuickKeys extends LitElement {
|
|||
this.requestUpdate();
|
||||
}
|
||||
|
||||
// Always pass the key press to the handler - let it decide what to do with special keys
|
||||
if (this.onKeyPress) {
|
||||
this.onKeyPress(key, isModifier, isSpecial, isToggle);
|
||||
}
|
||||
|
|
@ -272,21 +276,35 @@ export class TerminalQuickKeys extends LitElement {
|
|||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 999999;
|
||||
background-color: rgb(var(--color-bg-secondary));
|
||||
width: 100%;
|
||||
/* Properly handle safe areas */
|
||||
padding-left: env(safe-area-inset-left);
|
||||
padding-right: env(safe-area-inset-right);
|
||||
z-index: ${Z_INDEX.TERMINAL_QUICK_KEYS};
|
||||
background-color: rgb(var(--color-bg-secondary) / 0.98);
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
width: 100vw;
|
||||
max-width: 100vw;
|
||||
/* No safe areas needed when above keyboard */
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* The actual bar with buttons */
|
||||
.quick-keys-bar {
|
||||
background: rgb(var(--color-bg-secondary));
|
||||
border-top: 1px solid rgb(var(--color-border-base));
|
||||
background: transparent;
|
||||
border-top: 1px solid rgb(var(--color-border-base) / 0.5);
|
||||
padding: 0.25rem 0;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Button rows - ensure full width */
|
||||
.quick-keys-bar > div {
|
||||
width: 100%;
|
||||
padding-left: 0.125rem;
|
||||
padding-right: 0.125rem;
|
||||
}
|
||||
|
||||
/* Quick key buttons */
|
||||
|
|
@ -295,6 +313,8 @@ export class TerminalQuickKeys extends LitElement {
|
|||
-webkit-tap-highlight-color: transparent;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
flex: 1 1 0;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* Modifier key styling */
|
||||
|
|
@ -366,6 +386,25 @@ export class TerminalQuickKeys extends LitElement {
|
|||
-webkit-tap-highlight-color: transparent;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
flex: 1 1 0;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* Scrollable row styling */
|
||||
.scrollable-row {
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
/* Hide scrollbar but keep functionality */
|
||||
.scrollable-row::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.scrollable-row {
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
/* Toggle button styling */
|
||||
|
|
@ -394,6 +433,8 @@ export class TerminalQuickKeys extends LitElement {
|
|||
-webkit-tap-highlight-color: transparent;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
flex: 1 1 0;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
|
@ -412,7 +453,7 @@ export class TerminalQuickKeys extends LitElement {
|
|||
>
|
||||
<div class="quick-keys-bar">
|
||||
<!-- Row 1 -->
|
||||
<div class="flex gap-0.5 mb-0.5 overflow-x-auto scrollbar-hide px-0.5">
|
||||
<div class="flex gap-0.5 mb-0.5">
|
||||
${TERMINAL_QUICK_KEYS.filter((k) => k.row === 1).map(
|
||||
({ key, label, modifier, arrow, toggle }) => html`
|
||||
<button
|
||||
|
|
@ -459,12 +500,12 @@ export class TerminalQuickKeys extends LitElement {
|
|||
)}
|
||||
</div>
|
||||
|
||||
<!-- Row 2 or Function Keys or Ctrl Shortcuts -->
|
||||
<!-- Row 2 or Function Keys or Ctrl Shortcuts (with Done button always visible) -->
|
||||
${
|
||||
this.showCtrlKeys
|
||||
? html`
|
||||
<!-- Ctrl shortcuts row -->
|
||||
<div class="flex gap-0.5 mb-0.5 overflow-x-auto scrollbar-hide px-0.5">
|
||||
<!-- Ctrl shortcuts row with Done button -->
|
||||
<div class="flex gap-0.5 mb-0.5">
|
||||
${CTRL_SHORTCUTS.map(
|
||||
({ key, label, combo, special }) => html`
|
||||
<button
|
||||
|
|
@ -494,12 +535,38 @@ export class TerminalQuickKeys extends LitElement {
|
|||
</button>
|
||||
`
|
||||
)}
|
||||
<!-- Done button -->
|
||||
<button
|
||||
type="button"
|
||||
tabindex="-1"
|
||||
class="quick-key-btn ${this.getButtonFontClass(DONE_BUTTON.label)} min-w-0 ${this.getButtonSizeClass(DONE_BUTTON.label)} bg-bg-tertiary text-primary font-mono rounded border border-border hover:bg-surface hover:border-primary transition-all whitespace-nowrap special-key"
|
||||
@mousedown=${(e: Event) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}}
|
||||
@touchstart=${(e: Event) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}}
|
||||
@touchend=${(e: Event) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this.handleKeyPress(DONE_BUTTON.key, false, DONE_BUTTON.special, false, e);
|
||||
}}
|
||||
@click=${(e: MouseEvent) => {
|
||||
if (e.detail !== 0) {
|
||||
this.handleKeyPress(DONE_BUTTON.key, false, DONE_BUTTON.special, false, e);
|
||||
}
|
||||
}}
|
||||
>
|
||||
${DONE_BUTTON.label}
|
||||
</button>
|
||||
</div>
|
||||
`
|
||||
: this.showFunctionKeys
|
||||
? html`
|
||||
<!-- Function keys row -->
|
||||
<div class="flex gap-0.5 mb-0.5 overflow-x-auto scrollbar-hide px-0.5">
|
||||
<!-- Function keys row with Done button -->
|
||||
<div class="flex gap-0.5 mb-0.5">
|
||||
${FUNCTION_KEYS.map(
|
||||
({ key, label }) => html`
|
||||
<button
|
||||
|
|
@ -529,11 +596,37 @@ export class TerminalQuickKeys extends LitElement {
|
|||
</button>
|
||||
`
|
||||
)}
|
||||
<!-- Done button -->
|
||||
<button
|
||||
type="button"
|
||||
tabindex="-1"
|
||||
class="quick-key-btn ${this.getButtonFontClass(DONE_BUTTON.label)} min-w-0 ${this.getButtonSizeClass(DONE_BUTTON.label)} bg-bg-tertiary text-primary font-mono rounded border border-border hover:bg-surface hover:border-primary transition-all whitespace-nowrap special-key"
|
||||
@mousedown=${(e: Event) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}}
|
||||
@touchstart=${(e: Event) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}}
|
||||
@touchend=${(e: Event) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this.handleKeyPress(DONE_BUTTON.key, false, DONE_BUTTON.special, false, e);
|
||||
}}
|
||||
@click=${(e: MouseEvent) => {
|
||||
if (e.detail !== 0) {
|
||||
this.handleKeyPress(DONE_BUTTON.key, false, DONE_BUTTON.special, false, e);
|
||||
}
|
||||
}}
|
||||
>
|
||||
${DONE_BUTTON.label}
|
||||
</button>
|
||||
</div>
|
||||
`
|
||||
: html`
|
||||
<!-- Regular row 2 -->
|
||||
<div class="flex gap-0.5 mb-0.5 overflow-x-auto scrollbar-hide px-0.5">
|
||||
<div class="flex gap-0.5 mb-0.5 ">
|
||||
${TERMINAL_QUICK_KEYS.filter((k) => k.row === 2).map(
|
||||
({ key, label, modifier, combo, special, toggle }) => html`
|
||||
<button
|
||||
|
|
@ -567,12 +660,38 @@ export class TerminalQuickKeys extends LitElement {
|
|||
</button>
|
||||
`
|
||||
)}
|
||||
<!-- Done button (in regular row 2) -->
|
||||
<button
|
||||
type="button"
|
||||
tabindex="-1"
|
||||
class="quick-key-btn ${this.getButtonFontClass(DONE_BUTTON.label)} min-w-0 ${this.getButtonSizeClass(DONE_BUTTON.label)} bg-bg-tertiary text-primary font-mono rounded border border-border hover:bg-surface hover:border-primary transition-all whitespace-nowrap special-key"
|
||||
@mousedown=${(e: Event) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}}
|
||||
@touchstart=${(e: Event) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}}
|
||||
@touchend=${(e: Event) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this.handleKeyPress(DONE_BUTTON.key, false, DONE_BUTTON.special, false, e);
|
||||
}}
|
||||
@click=${(e: MouseEvent) => {
|
||||
if (e.detail !== 0) {
|
||||
this.handleKeyPress(DONE_BUTTON.key, false, DONE_BUTTON.special, false, e);
|
||||
}
|
||||
}}
|
||||
>
|
||||
${DONE_BUTTON.label}
|
||||
</button>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
|
||||
<!-- Row 3 - Additional special characters (always visible) -->
|
||||
<div class="flex gap-0.5 overflow-x-auto scrollbar-hide px-0.5">
|
||||
<div class="flex gap-0.5 ">
|
||||
${TERMINAL_QUICK_KEYS.filter((k) => k.row === 3).map(
|
||||
({ key, label, modifier, combo, special }) => html`
|
||||
<button
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ export const Z_INDEX = {
|
|||
SIDEBAR_MOBILE: 30,
|
||||
MOBILE_INPUT_OVERLAY: 40,
|
||||
CTRL_ALPHA_OVERLAY: 45,
|
||||
TERMINAL_QUICK_KEYS: 48,
|
||||
|
||||
// Dropdowns and popovers (50-99)
|
||||
WIDTH_SELECTOR_DROPDOWN: 60,
|
||||
|
|
|
|||
Loading…
Reference in a new issue