Fix iOS keyboard dismissal issue (#484)

Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
Helmut Januschka 2025-07-31 05:41:09 +02:00 committed by GitHub
parent e9a1ce0555
commit 32935878d8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 332 additions and 84 deletions

View file

@ -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"])'

View file

@ -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()

View file

@ -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>
`;
}
}

View file

@ -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();
}
});
}
}

View file

@ -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(): {

View file

@ -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);

View file

@ -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"

View file

@ -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

View file

@ -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,