fix(web): Phase 17 審核卡片 UI/UX 修復 - SlidePanel → ApprovalModal

問題:
- SlidePanel 側邊滑入與右側 OpenClaw 固定面板衝突
- 480px 寬度不足,內容被擠壓截斷
- 標題「審核詳情」只顯示「情」

解決方案:
- 新建 ApprovalModal 全屏 Modal 組件
- 居中對話框 (max-width: 672px)
- 深色遮罩 (60%) 強制用戶聚焦
- 符合 Nothing.tech「一次專注一件事」設計哲學

設計參考: GitHub PR Review, AWS IAM Console

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
OG T
2026-03-26 13:26:05 +08:00
parent a470a514e6
commit 540602fd63
3 changed files with 161 additions and 6 deletions

View File

@@ -0,0 +1,153 @@
'use client'
/**
* ApprovalModal - 全屏審核 Modal
* ===============================
* Phase 17: UI/UX 修復 - 取代 SlidePanel
*
* 設計決策:
* - 審核是關鍵決策,需要用戶全神貫注
* - 全屏遮罩強制用戶注意,防止誤操作
* - 居中對話框提供足夠空間顯示所有資訊
* - 符合 Nothing.tech「一次專注一件事」設計哲學
*
* 參考: GitHub PR Review, AWS IAM Console
*/
import { useEffect, useCallback } from 'react'
import { cn } from '@/lib/utils'
import { X, ChevronLeft, ChevronRight, Shield } from 'lucide-react'
interface ApprovalModalProps {
open: boolean
onClose: () => void
children: React.ReactNode
title?: string
/** 上一個/下一個導航 */
onPrev?: () => void
onNext?: () => void
/** 當前位置 (1-based) */
current?: number
total?: number
}
export function ApprovalModal({
open,
onClose,
children,
title = '審核詳情',
onPrev,
onNext,
current,
total,
}: ApprovalModalProps) {
// ESC 關閉 + 方向鍵導航
const handleKeyDown = useCallback((e: KeyboardEvent) => {
if (e.key === 'Escape') onClose()
if (e.key === 'ArrowLeft' && onPrev) onPrev()
if (e.key === 'ArrowRight' && onNext) onNext()
}, [onClose, onPrev, onNext])
// 鎖定背景滾動
useEffect(() => {
if (open) {
document.body.style.overflow = 'hidden'
document.addEventListener('keydown', handleKeyDown)
}
return () => {
document.body.style.overflow = ''
document.removeEventListener('keydown', handleKeyDown)
}
}, [open, handleKeyDown])
if (!open) return null
return (
<div className="fixed inset-0 z-50 flex items-center justify-center">
{/* Backdrop - 深色遮罩,強調聚焦 */}
<div
className="absolute inset-0 bg-black/60 backdrop-blur-sm animate-in fade-in duration-200"
onClick={onClose}
/>
{/* Modal Container */}
<div
className={cn(
'relative z-10 w-full max-w-2xl max-h-[90vh]',
'bg-white rounded-2xl shadow-2xl',
'flex flex-col',
'animate-in zoom-in-95 fade-in duration-200',
'mx-4' // 手機邊距
)}
>
{/* Header */}
<div className="flex-shrink-0 flex items-center justify-between px-6 py-4 border-b border-nothing-gray-100">
<div className="flex items-center gap-3">
<div className="p-2 bg-claw-blue/10 rounded-xl">
<Shield className="w-5 h-5 text-claw-blue" />
</div>
<div>
<h2 className="font-heading text-lg font-bold text-nothing-gray-900">
{title}
</h2>
<p className="text-xs text-nothing-gray-500 font-mono">
Multi-Sig Authorization Required
</p>
</div>
</div>
<div className="flex items-center gap-3">
{/* 導航控制 */}
{(onPrev || onNext) && current && total && total > 1 && (
<div className="flex items-center gap-2 px-3 py-1.5 bg-nothing-gray-50 rounded-lg">
<button
onClick={onPrev}
disabled={current <= 1}
className={cn(
'p-1 rounded transition-colors',
current <= 1
? 'text-nothing-gray-300 cursor-not-allowed'
: 'text-nothing-gray-600 hover:bg-nothing-gray-200'
)}
title="上一個 (←)"
>
<ChevronLeft className="w-4 h-4" />
</button>
<span className="text-sm font-mono text-nothing-gray-600 tabular-nums min-w-[3rem] text-center">
{current} / {total}
</span>
<button
onClick={onNext}
disabled={current >= total}
className={cn(
'p-1 rounded transition-colors',
current >= total
? 'text-nothing-gray-300 cursor-not-allowed'
: 'text-nothing-gray-600 hover:bg-nothing-gray-200'
)}
title="下一個 (→)"
>
<ChevronRight className="w-4 h-4" />
</button>
</div>
)}
{/* 關閉按鈕 */}
<button
onClick={onClose}
className="p-2 rounded-xl hover:bg-nothing-gray-100 transition-colors"
title="關閉 (ESC)"
>
<X className="w-5 h-5 text-nothing-gray-500" />
</button>
</div>
</div>
{/* Content - 可滾動 */}
<div className="flex-1 overflow-y-auto">
{children}
</div>
</div>
</div>
)
}

View File

@@ -34,3 +34,6 @@ export {
GridLinesBg,
type DotMatrixBgProps,
} from './dot-matrix-bg'
// Approval Modal (Phase 17 UI/UX 修復)
export { ApprovalModal } from './approval-modal'