refactor(ui): extract keyboard utilities and add padding constant

- Create shared `isSubmitEnter()` utility in `ui/src/lib/keyboard.ts`
  for IME-aware Enter key handling across all input components
- Extract magic number 48 to named constant `COLLAPSED_DEBUG_PANEL_CLEARANCE`
  with explanatory comment (40px panel header + 8px margin)
- Update 5 components to use the new utility:
  - AssistantChat.tsx
  - ExpandProjectChat.tsx
  - SpecCreationChat.tsx
  - FolderBrowser.tsx
  - TerminalTabs.tsx

This follows up on PR #121 which added IME composition checks. The
refactoring centralizes the logic for easier maintenance and documents
the padding value that prevents Kanban cards from being cut off when
the debug panel is collapsed.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Auto
2026-01-29 08:28:48 +02:00
parent 51d7d79695
commit a12e4aa3b8
7 changed files with 52 additions and 11 deletions

View File

@@ -36,6 +36,9 @@ import { Badge } from '@/components/ui/badge'
const STORAGE_KEY = 'autocoder-selected-project'
const VIEW_MODE_KEY = 'autocoder-view-mode'
// Bottom padding for main content when debug panel is collapsed (40px header + 8px margin)
const COLLAPSED_DEBUG_PANEL_CLEARANCE = 48
function App() {
// Initialize selected project from localStorage
const [selectedProject, setSelectedProject] = useState<string | null>(() => {
@@ -331,7 +334,7 @@ function App() {
{/* Main Content */}
<main
className="max-w-7xl mx-auto px-4 py-8"
style={{ paddingBottom: debugOpen ? debugPanelHeight + 32 : 48 }}
style={{ paddingBottom: debugOpen ? debugPanelHeight + 32 : COLLAPSED_DEBUG_PANEL_CLEARANCE }}
>
{!selectedProject ? (
<div className="text-center mt-12">

View File

@@ -12,6 +12,7 @@ import { useAssistantChat } from '../hooks/useAssistantChat'
import { ChatMessage as ChatMessageComponent } from './ChatMessage'
import { ConversationHistory } from './ConversationHistory'
import type { ChatMessage } from '../lib/types'
import { isSubmitEnter } from '../lib/keyboard'
import { Button } from '@/components/ui/button'
import { Textarea } from '@/components/ui/textarea'
@@ -134,8 +135,7 @@ export function AssistantChat({
}
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
// Skip if composing (e.g., Japanese IME input)
if (e.key === 'Enter' && !e.shiftKey && !e.nativeEvent.isComposing) {
if (isSubmitEnter(e)) {
e.preventDefault()
handleSend()
}

View File

@@ -11,6 +11,7 @@ import { useExpandChat } from '../hooks/useExpandChat'
import { ChatMessage } from './ChatMessage'
import { TypingIndicator } from './TypingIndicator'
import type { ImageAttachment } from '../lib/types'
import { isSubmitEnter } from '../lib/keyboard'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Card, CardContent } from '@/components/ui/card'
@@ -88,8 +89,7 @@ export function ExpandProjectChat({
}
const handleKeyDown = (e: React.KeyboardEvent) => {
// Skip if composing (e.g., Japanese IME input)
if (e.key === 'Enter' && !e.shiftKey && !e.nativeEvent.isComposing) {
if (isSubmitEnter(e)) {
e.preventDefault()
handleSendMessage()
}

View File

@@ -18,6 +18,7 @@ import {
ArrowLeft,
} from 'lucide-react'
import * as api from '../lib/api'
import { isSubmitEnter } from '../lib/keyboard'
import type { DirectoryEntry, DriveInfo } from '../lib/types'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
@@ -269,8 +270,7 @@ export function FolderBrowser({ onSelect, onCancel, initialPath }: FolderBrowser
className="flex-1"
autoFocus
onKeyDown={(e) => {
// Skip if composing (e.g., Japanese IME input)
if (e.key === 'Enter' && !e.nativeEvent.isComposing) handleCreateFolder()
if (isSubmitEnter(e, false)) handleCreateFolder()
if (e.key === 'Escape') {
setIsCreatingFolder(false)
setNewFolderName('')

View File

@@ -12,6 +12,7 @@ import { ChatMessage } from './ChatMessage'
import { QuestionOptions } from './QuestionOptions'
import { TypingIndicator } from './TypingIndicator'
import type { ImageAttachment } from '../lib/types'
import { isSubmitEnter } from '../lib/keyboard'
import { Button } from '@/components/ui/button'
import { Textarea } from '@/components/ui/textarea'
import { Card, CardContent } from '@/components/ui/card'
@@ -127,8 +128,7 @@ export function SpecCreationChat({
}
const handleKeyDown = (e: React.KeyboardEvent) => {
// Skip if composing (e.g., Japanese IME input)
if (e.key === 'Enter' && !e.shiftKey && !e.nativeEvent.isComposing) {
if (isSubmitEnter(e)) {
e.preventDefault()
handleSendMessage()
}

View File

@@ -8,6 +8,7 @@
import { useState, useRef, useEffect, useCallback } from 'react'
import { Plus, X } from 'lucide-react'
import type { TerminalInfo } from '@/lib/types'
import { isSubmitEnter } from '@/lib/keyboard'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
@@ -96,8 +97,7 @@ export function TerminalTabs({
// Handle key events during editing
const handleKeyDown = useCallback(
(e: React.KeyboardEvent) => {
// Skip if composing (e.g., Japanese IME input)
if (e.key === 'Enter' && !e.nativeEvent.isComposing) {
if (isSubmitEnter(e, false)) {
e.preventDefault()
submitEdit()
} else if (e.key === 'Escape') {

38
ui/src/lib/keyboard.ts Normal file
View File

@@ -0,0 +1,38 @@
/**
* Keyboard event utilities
*
* Helpers for handling keyboard events, particularly for IME-aware input handling.
*/
/**
* Check if an Enter keypress should trigger form submission.
*
* Returns false during IME composition (e.g., Japanese, Chinese, Korean input)
* to prevent accidental submission while selecting characters.
*
* @param e - The keyboard event from React
* @param allowShiftEnter - If true, Shift+Enter returns false (for multiline input)
* @returns true if Enter should submit, false if it should be ignored
*
* @example
* // In a chat input (Shift+Enter for newline)
* if (isSubmitEnter(e)) {
* e.preventDefault()
* handleSend()
* }
*
* @example
* // In a single-line input (Enter always submits)
* if (isSubmitEnter(e, false)) {
* handleSubmit()
* }
*/
export function isSubmitEnter(
e: React.KeyboardEvent,
allowShiftEnter: boolean = true
): boolean {
if (e.key !== 'Enter') return false
if (allowShiftEnter && e.shiftKey) return false
if (e.nativeEvent.isComposing) return false
return true
}