fix(web): OmniTerminal Escape 關閉 + 響應式底部抽屜

Phase 19.R - 修復 UX 問題:
- 新增 Escape 鍵關閉 Terminal (之前僅有 CMD+J)
- Mobile: 全螢幕改為 70vh 底部抽屜
- 新增半透明 backdrop,點擊可關閉
- 響應式: Mobile/Tablet/Desktop 三級適配

修復問題: Terminal 開啟後無法關閉

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
OG T
2026-03-29 01:47:05 +08:00
parent d6dc80bcbc
commit 8fa99209c3

View File

@@ -22,6 +22,7 @@
*/
import React, { useEffect, useRef, useState } from 'react'
import { useTranslations } from 'next-intl'
import {
useTerminalStore,
useIsConnected,
@@ -34,6 +35,7 @@ import { Z_INDEX } from '@/lib/constants/z-index'
import { SHORTCUTS, matchShortcut } from '@/lib/constants/shortcuts'
export const OmniTerminal = () => {
const t = useTranslations('omniTerminal')
// Use optimized selectors for better re-render performance
const isOpen = useIsTerminalOpen()
const messages = useTerminalMessages()
@@ -44,16 +46,24 @@ export const OmniTerminal = () => {
const inputRef = useRef<HTMLInputElement>(null)
const scrollRef = useRef<HTMLDivElement>(null)
// CMD+J 快捷鍵 (Phase 19 - 避免 CMD+K 瀏覽器衝突)
// 快捷鍵處理 (Phase 19 - 完整鍵盤支援)
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
// CMD+J: Toggle Terminal
// CMD+J: Toggle Terminal (全局)
if (matchShortcut(e, SHORTCUTS.TOGGLE_TERMINAL)) {
e.preventDefault()
toggleTerminal()
return
}
// '/': Quick open (非輸入框時)
// Escape: 關閉 Terminal (當 Terminal 開啟時)
if (e.key === 'Escape' && isOpen) {
e.preventDefault()
toggleTerminal()
return
}
// '/': Quick open (非輸入框時Terminal 關閉時)
if (e.key === '/' && !isOpen && document.activeElement?.tagName !== 'INPUT') {
e.preventDefault()
openTerminal()
@@ -162,26 +172,30 @@ export const OmniTerminal = () => {
}
return (
<div
className="fixed inset-0 sm:inset-auto sm:bottom-0 sm:left-0 sm:w-full flex justify-center sm:pb-4 sm:px-4 pointer-events-none"
style={{ zIndex: Z_INDEX.OMNI_TERMINAL }}
>
{/* Responsive container:
- Mobile: Full screen
- Tablet: 90% width
- Desktop: max-w-4xl centered */}
<>
{/* Backdrop overlay for mobile - 點擊可關閉 */}
<div
className="w-full h-full sm:h-auto sm:w-[90%] lg:w-full lg:max-w-4xl bg-white/90 sm:bg-white/70 backdrop-blur-[20px] sm:border border-gray-200 sm:rounded-lg shadow-xl overflow-hidden pointer-events-auto flex flex-col transition-all"
style={{ maxHeight: '100vh', minHeight: '100vh' }}
role="dialog"
aria-label="Omni-Terminal"
className="fixed inset-0 bg-black/20 backdrop-blur-sm sm:hidden"
style={{ zIndex: Z_INDEX.OMNI_TERMINAL - 1 }}
onClick={toggleTerminal}
aria-hidden="true"
/>
<div
className="fixed inset-x-0 bottom-0 flex justify-center px-2 pb-2 sm:px-4 sm:pb-4"
style={{ zIndex: Z_INDEX.OMNI_TERMINAL }}
>
{/* Responsive height on tablet/desktop */}
<style jsx>{`
@media (min-width: 640px) {
div { max-height: 60vh !important; min-height: 30vh !important; }
}
`}</style>
{/* Responsive container:
- Mobile: 底部彈出 (不再全螢幕!)
- Tablet: 90% width
- Desktop: max-w-4xl centered */}
<div
className="w-full sm:w-[90%] lg:max-w-4xl bg-white/95 backdrop-blur-[20px] border border-gray-200 rounded-xl shadow-2xl overflow-hidden flex flex-col transition-all"
style={{ maxHeight: '70vh', minHeight: '200px' }}
role="dialog"
aria-label="Omni-Terminal"
aria-modal="true"
>
{/* Terminal Header */}
<div className="flex justify-between items-center px-3 sm:px-4 py-2 border-b border-gray-200 bg-white/40">
@@ -233,15 +247,16 @@ export const OmniTerminal = () => {
type="text"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
placeholder="輸入指令..."
placeholder={t('inputPlaceholder')}
className="flex-1 bg-transparent border-none outline-none font-['VT323'] text-xl sm:text-2xl text-nothing-black placeholder-gray-400"
autoComplete="off"
spellCheck="false"
aria-label="Terminal input"
/>
</form>
</div>
</div>
</div>
</div>
</>
)
}