Files
awoooi/apps/web/src/components/ui/slide-panel.tsx
OG T 4d46e6b9a7
Some checks failed
E2E Health Check / e2e-health (push) Successful in 17s
CD Pipeline / build-and-deploy (push) Has been cancelled
style(web): 全站 font-mono → font-body (DM Mono 設計系統套用)
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>
2026-04-02 09:37:03 +08:00

158 lines
4.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'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>
</>
)
}