45 個 component + 6 個 page 統一從舊 font-mono 遷移到 font-body (DM Mono),確保設計系統一致性。 font-body = DM Mono (等寬),視覺效果相同但走新設計 token。 保留: font-heading (Syne)、font-dot-matrix (VT323/DSEG7) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
158 lines
4.5 KiB
TypeScript
158 lines
4.5 KiB
TypeScript
'use client'
|
||
|
||
/**
|
||
* SlidePanel - 專業級側邊滑入面板
|
||
* ================================
|
||
* 業界標準 AIOps UX:
|
||
* - PagerDuty / ServiceNow 風格
|
||
* - 不遮蔽主內容,保持上下文
|
||
* - 固定底部操作按鈕
|
||
*
|
||
* Phase 19: 使用 Z_INDEX.SLIDE_PANEL (50)
|
||
* @see lib/constants/z-index.ts
|
||
*/
|
||
|
||
import { useEffect, useCallback } from 'react'
|
||
import { useTranslations } from 'next-intl'
|
||
import { cn } from '@/lib/utils'
|
||
import { X, ChevronLeft, ChevronRight } from 'lucide-react'
|
||
import { Z_INDEX } from '@/lib/constants/z-index'
|
||
|
||
interface SlidePanelProps {
|
||
open: boolean
|
||
onClose: () => void
|
||
children: React.ReactNode
|
||
title?: string
|
||
/** 上一個/下一個導航 */
|
||
onPrev?: () => void
|
||
onNext?: () => void
|
||
/** 當前位置 (1-based) */
|
||
current?: number
|
||
total?: number
|
||
/** 寬度 */
|
||
width?: 'sm' | 'md' | 'lg' | 'xl'
|
||
}
|
||
|
||
const widthMap = {
|
||
sm: 'w-80',
|
||
md: 'w-96',
|
||
lg: 'w-[480px]',
|
||
xl: 'w-[560px]',
|
||
}
|
||
|
||
export function SlidePanel({
|
||
open,
|
||
onClose,
|
||
children,
|
||
title,
|
||
onPrev,
|
||
onNext,
|
||
current,
|
||
total,
|
||
width = 'lg',
|
||
}: SlidePanelProps) {
|
||
const t = useTranslations('common')
|
||
// 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.addEventListener('keydown', handleKeyDown)
|
||
}
|
||
return () => {
|
||
document.removeEventListener('keydown', handleKeyDown)
|
||
}
|
||
}, [open, handleKeyDown])
|
||
|
||
return (
|
||
<>
|
||
{/* Backdrop - 半透明,點擊關閉 */}
|
||
<div
|
||
className={cn(
|
||
'fixed inset-0 bg-black/20 transition-opacity duration-300',
|
||
open ? 'opacity-100' : 'opacity-0 pointer-events-none'
|
||
)}
|
||
style={{ zIndex: Z_INDEX.SIDEBAR }}
|
||
onClick={onClose}
|
||
/>
|
||
|
||
{/* Panel */}
|
||
<div
|
||
className={cn(
|
||
'fixed top-0 right-0 h-full bg-white shadow-2xl',
|
||
'flex flex-col',
|
||
'transition-transform duration-300 ease-out',
|
||
widthMap[width],
|
||
open ? 'translate-x-0' : 'translate-x-full'
|
||
)}
|
||
style={{ zIndex: Z_INDEX.SLIDE_PANEL }}
|
||
>
|
||
{/* Header - 固定 */}
|
||
<div className="flex-shrink-0 flex items-center justify-between px-4 py-3 border-b border-nothing-gray-100 bg-nothing-gray-50">
|
||
<div className="flex items-center gap-3">
|
||
<button
|
||
onClick={onClose}
|
||
className="p-1.5 rounded-lg hover:bg-nothing-gray-200 transition-colors"
|
||
title={t('closeEsc')}
|
||
>
|
||
<X className="w-5 h-5 text-nothing-gray-500" />
|
||
</button>
|
||
{title && (
|
||
<h2 className="font-heading text-base font-bold text-nothing-gray-900">
|
||
{title}
|
||
</h2>
|
||
)}
|
||
</div>
|
||
|
||
{/* 導航控制 */}
|
||
{(onPrev || onNext) && current && total && (
|
||
<div className="flex items-center gap-2">
|
||
<span className="text-xs font-body text-nothing-gray-400">
|
||
{current} / {total}
|
||
</span>
|
||
<div className="flex items-center gap-1">
|
||
<button
|
||
onClick={onPrev}
|
||
disabled={current <= 1}
|
||
className={cn(
|
||
'p-1.5 rounded-lg transition-colors',
|
||
current <= 1
|
||
? 'text-nothing-gray-300 cursor-not-allowed'
|
||
: 'text-nothing-gray-500 hover:bg-nothing-gray-200'
|
||
)}
|
||
title={t('previous')}
|
||
>
|
||
<ChevronLeft className="w-4 h-4" />
|
||
</button>
|
||
<button
|
||
onClick={onNext}
|
||
disabled={current >= total}
|
||
className={cn(
|
||
'p-1.5 rounded-lg transition-colors',
|
||
current >= total
|
||
? 'text-nothing-gray-300 cursor-not-allowed'
|
||
: 'text-nothing-gray-500 hover:bg-nothing-gray-200'
|
||
)}
|
||
title={t('next')}
|
||
>
|
||
<ChevronRight className="w-4 h-4" />
|
||
</button>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* Content - 可滾動 */}
|
||
<div className="flex-1 overflow-y-auto">
|
||
{children}
|
||
</div>
|
||
</div>
|
||
</>
|
||
)
|
||
}
|