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:
153
apps/web/src/components/ui/approval-modal.tsx
Normal file
153
apps/web/src/components/ui/approval-modal.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -34,3 +34,6 @@ export {
|
||||
GridLinesBg,
|
||||
type DotMatrixBgProps,
|
||||
} from './dot-matrix-bg'
|
||||
|
||||
// Approval Modal (Phase 17 UI/UX 修復)
|
||||
export { ApprovalModal } from './approval-modal'
|
||||
|
||||
Reference in New Issue
Block a user