'use client' /** * AWOOOI Command Palette * ========================== * ⌘K 全局指令面板 — 高斯模糊背景 + 快速導航 + 動作 * * 快捷鍵: ⌘K (Mac) / Ctrl+K (其他) * Z-Index: DIALOG (70) * * @author Claude Code (首席架構師) * @version 1.0.0 * @date 2026-04-09 (台北時間) */ import React, { useEffect, useRef, useState, useCallback } from 'react' import { useTranslations } from 'next-intl' import { useRouter, usePathname } from 'next/navigation' import { useLocale } from 'next-intl' import { Search, Terminal, Home, Activity, Wrench, Shield, BookOpen, Settings, Zap, GitBranch } from 'lucide-react' import { useTerminalStore } from '@/stores/terminal.store' import { Z_INDEX } from '@/lib/constants/z-index' interface PaletteItem { id: string label: string group: string icon: React.ReactNode action: () => void keywords?: string[] } export function CommandPalette() { const t = useTranslations('commandPalette') const router = useRouter() const locale = useLocale() const pathname = usePathname() const { openTerminal } = useTerminalStore() const [open, setOpen] = useState(false) const [query, setQuery] = useState('') const [activeIdx, setActiveIdx] = useState(0) const inputRef = useRef(null) const listRef = useRef(null) const nav = (path: string) => { router.push(`/${locale}${path}`) setOpen(false) } const items: PaletteItem[] = [ // 快速動作 { id: 'terminal', label: t('actionOpenTerminal'), group: t('groupActions'), icon: , action: () => { openTerminal(); setOpen(false) }, keywords: ['terminal', '終端', 'omni', 'cmd', '指令'], }, // 導航 { id: 'home', label: t('actionGoHome'), group: t('groupNav'), icon: , action: () => nav(''), keywords: ['home', '首頁', '指令中心', 'dashboard'], }, { id: 'approvals', label: t('actionGoApprovals'), group: t('groupNav'), icon: , action: () => nav('/authorizations'), keywords: ['授權', 'approve', '批准', 'authorization'], }, { id: 'observability', label: t('actionGoObservability'), group: t('groupNav'), icon: , action: () => nav('/observability'), keywords: ['observability', '可觀測性', 'monitor', '監控'], }, { id: 'automation', label: t('actionGoAutomation'), group: t('groupNav'), icon: , action: () => nav('/automation'), keywords: ['automation', '自動化', 'auto'], }, { id: 'operations', label: t('actionGoOperations'), group: t('groupNav'), icon: , action: () => nav('/operations'), keywords: ['operations', '營運', 'ops'], }, { id: 'security', label: t('actionGoSecurity'), group: t('groupNav'), icon: , action: () => nav('/security-compliance'), keywords: ['security', '安全', 'compliance', '合規'], }, { id: 'knowledge', label: t('actionGoKnowledge'), group: t('groupNav'), icon: , action: () => nav('/knowledge'), keywords: ['knowledge', '知識', '殿堂', 'kb'], }, { id: 'settings', label: t('actionGoSettings'), group: t('groupNav'), icon: , action: () => nav('/settings'), keywords: ['settings', '設定', 'config'], }, { id: 'terminal-page', label: t('actionGoTerminal'), group: t('groupNav'), icon: , action: () => nav('/terminal'), keywords: ['terminal', '終端', 'shell'], }, ] const filtered = query.trim() === '' ? items : items.filter(item => { const q = query.toLowerCase() return item.label.toLowerCase().includes(q) || (item.keywords ?? []).some(k => k.toLowerCase().includes(q)) }) // 開啟時重置 useEffect(() => { if (open) { setQuery('') setActiveIdx(0) setTimeout(() => inputRef.current?.focus(), 50) } }, [open]) // 更新 activeIdx 時捲動 useEffect(() => { const el = listRef.current?.children[activeIdx] as HTMLElement | undefined el?.scrollIntoView({ block: 'nearest' }) }, [activeIdx]) // ⌘K / Ctrl+K 開啟 useEffect(() => { const handler = (e: KeyboardEvent) => { if ((e.metaKey || e.ctrlKey) && e.key === 'k') { e.preventDefault() setOpen(prev => !prev) } } window.addEventListener('keydown', handler) return () => window.removeEventListener('keydown', handler) }, []) // 鍵盤導航 const handleKeyDown = useCallback((e: React.KeyboardEvent) => { if (e.key === 'ArrowDown') { e.preventDefault() setActiveIdx(i => Math.min(i + 1, filtered.length - 1)) } else if (e.key === 'ArrowUp') { e.preventDefault() setActiveIdx(i => Math.max(i - 1, 0)) } else if (e.key === 'Enter') { e.preventDefault() filtered[activeIdx]?.action() } else if (e.key === 'Escape') { setOpen(false) } }, [filtered, activeIdx]) // query 變更時重置 activeIdx useEffect(() => setActiveIdx(0), [query]) if (!open) return null // 按 group 分組 const groups: Record = {} filtered.forEach(item => { if (!groups[item.group]) groups[item.group] = [] groups[item.group].push(item) }) // 攤平為帶 group header 的列表(用於 activeIdx 計算) let flatIdx = 0 return (
setOpen(false)} > {/* 高斯模糊背景 */}
{/* 面板 */}
e.stopPropagation()} > {/* 搜尋列 */}
setQuery(e.target.value)} onKeyDown={handleKeyDown} placeholder={t('placeholder')} style={{ flex: 1, border: 'none', outline: 'none', fontSize: 14, background: 'transparent', color: '#141413' }} /> ESC
{/* 結果列表 */}
{filtered.length === 0 ? (
{t('noResults')}
) : ( Object.entries(groups).map(([group, groupItems]) => (
{group}
{groupItems.map(item => { const idx = flatIdx++ const isActive = idx === activeIdx return (
setActiveIdx(idx)} style={{ display: 'flex', alignItems: 'center', gap: 10, padding: '8px 16px', cursor: 'pointer', background: isActive ? 'rgba(74,144,217,0.08)' : 'transparent', transition: 'background 0.1s', }} > {item.icon} {item.label} {isActive && ( )}
) })}
)) )}
{/* 底部提示 */}
{t('hint')}
) }