Spaces:
No application file
No application file
| 'use client'; | |
| import { useRef, useState } from 'react'; | |
| import { motion, AnimatePresence } from 'motion/react'; | |
| import { Eraser, History, Minimize2, PencilLine } from 'lucide-react'; | |
| import { WhiteboardCanvas } from './whiteboard-canvas'; | |
| import { WhiteboardHistory } from './whiteboard-history'; | |
| import { useStageStore } from '@/lib/store'; | |
| import { useCanvasStore } from '@/lib/store/canvas'; | |
| import { useWhiteboardHistoryStore } from '@/lib/store/whiteboard-history'; | |
| import { createStageAPI } from '@/lib/api/stage-api'; | |
| import { toast } from 'sonner'; | |
| import { useI18n } from '@/lib/hooks/use-i18n'; | |
| interface WhiteboardProps { | |
| readonly isOpen: boolean; | |
| readonly onClose: () => void; | |
| } | |
| /** | |
| * Whiteboard component | |
| */ | |
| export function Whiteboard({ isOpen, onClose }: WhiteboardProps) { | |
| const { t } = useI18n(); | |
| const stage = useStageStore.use.stage(); | |
| const isClearing = useCanvasStore.use.whiteboardClearing(); | |
| const clearingRef = useRef(false); | |
| const [historyOpen, setHistoryOpen] = useState(false); | |
| const snapshotCount = useWhiteboardHistoryStore((s) => s.snapshots.length); | |
| // Get element count for indicator | |
| const whiteboard = stage?.whiteboard?.[0]; | |
| const elementCount = whiteboard?.elements?.length || 0; | |
| const stageAPI = createStageAPI(useStageStore); | |
| const handleClear = async () => { | |
| if (!whiteboard || elementCount === 0 || clearingRef.current) return; | |
| clearingRef.current = true; | |
| // Save snapshot before clearing | |
| if (whiteboard.elements && whiteboard.elements.length > 0) { | |
| useWhiteboardHistoryStore | |
| .getState() | |
| .pushSnapshot(whiteboard.elements, t('whiteboard.beforeClear')); | |
| } | |
| // Trigger cascade exit animation | |
| useCanvasStore.getState().setWhiteboardClearing(true); | |
| // Wait for cascade: base 380ms + 55ms per element, capped at 1400ms | |
| const animMs = Math.min(380 + elementCount * 55, 1400); | |
| await new Promise((resolve) => setTimeout(resolve, animMs)); | |
| // Actually remove elements | |
| const result = stageAPI.whiteboard.delete(whiteboard.id); | |
| useCanvasStore.getState().setWhiteboardClearing(false); | |
| clearingRef.current = false; | |
| if (result.success) { | |
| toast.success(t('whiteboard.clearSuccess')); | |
| } else { | |
| toast.error(t('whiteboard.clearError') + result.error); | |
| } | |
| }; | |
| return ( | |
| <> | |
| {/* Main Whiteboard Overlay */} | |
| <AnimatePresence> | |
| {isOpen && ( | |
| <motion.div | |
| initial={{ opacity: 0, scale: 0.92, y: 30 }} | |
| animate={{ | |
| opacity: 1, | |
| scale: 1, | |
| y: 0, | |
| transition: { | |
| type: 'spring', | |
| stiffness: 120, | |
| damping: 18, | |
| mass: 1.2, | |
| }, | |
| }} | |
| exit={{ | |
| opacity: 0, | |
| scale: 0.95, | |
| y: 16, | |
| transition: { duration: 0.5, ease: [0.4, 0, 0.2, 1] }, | |
| }} | |
| className="absolute inset-4 pointer-events-auto bg-white/95 dark:bg-gray-800/95 backdrop-blur-2xl rounded-3xl shadow-[0_32px_80px_-20px_rgba(0,0,0,0.25)] border-2 border-purple-200/60 dark:border-purple-700/60 flex flex-col overflow-hidden z-[120] ring-4 ring-purple-100/40 dark:ring-purple-800/40" | |
| > | |
| {/* Header */} | |
| <div className="h-14 px-6 border-b border-gray-100 dark:border-gray-700 flex items-center justify-between shrink-0 bg-white/50 dark:bg-gray-800/50"> | |
| <div className="flex items-center gap-3"> | |
| <div className="w-8 h-8 rounded-xl bg-purple-100 dark:bg-purple-900/30 flex items-center justify-center text-purple-600 dark:text-purple-400"> | |
| <PencilLine className="w-4 h-4" /> | |
| </div> | |
| <span className="font-bold text-gray-800 dark:text-gray-200 tracking-tight"> | |
| {t('whiteboard.title')} | |
| </span> | |
| </div> | |
| <div className="flex items-center gap-2"> | |
| <motion.button | |
| type="button" | |
| onClick={handleClear} | |
| disabled={isClearing || elementCount === 0} | |
| whileTap={{ scale: 0.9 }} | |
| className="p-2 text-gray-400 dark:text-gray-500 hover:text-red-500 dark:hover:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/20 rounded-lg transition-colors disabled:opacity-40 disabled:pointer-events-none" | |
| title={t('whiteboard.clear')} | |
| > | |
| <motion.div | |
| animate={isClearing ? { rotate: [0, -15, 15, -10, 10, 0] } : { rotate: 0 }} | |
| transition={ | |
| isClearing ? { duration: 0.5, ease: 'easeInOut' } : { duration: 0.2 } | |
| } | |
| > | |
| <Eraser className="w-4 h-4" /> | |
| </motion.div> | |
| </motion.button> | |
| <motion.button | |
| type="button" | |
| onClick={() => setHistoryOpen(!historyOpen)} | |
| whileTap={{ scale: 0.9 }} | |
| className="relative p-2 text-gray-400 dark:text-gray-500 hover:text-purple-500 dark:hover:text-purple-400 hover:bg-purple-50 dark:hover:bg-purple-900/20 rounded-lg transition-colors" | |
| title={t('whiteboard.history')} | |
| > | |
| <History className="w-4 h-4" /> | |
| {snapshotCount > 0 && ( | |
| <span className="absolute -top-0.5 -right-0.5 min-w-[16px] h-4 px-1 rounded-full bg-purple-500 text-white text-[10px] font-bold flex items-center justify-center"> | |
| {snapshotCount} | |
| </span> | |
| )} | |
| </motion.button> | |
| <div className="w-px h-4 bg-gray-200 dark:bg-gray-700 mx-1" /> | |
| <button | |
| type="button" | |
| onClick={onClose} | |
| className="p-2 text-gray-400 dark:text-gray-500 hover:text-gray-800 dark:hover:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors" | |
| title={t('whiteboard.minimize')} | |
| > | |
| <Minimize2 className="w-5 h-5" /> | |
| </button> | |
| </div> | |
| </div> | |
| {/* Whiteboard Content Area */} | |
| <div className="flex-1 relative bg-[radial-gradient(#e5e7eb_1px,transparent_1px)] dark:bg-[radial-gradient(#374151_1px,transparent_1px)] [background-size:24px_24px] overflow-hidden"> | |
| <WhiteboardCanvas /> | |
| {/* History panel */} | |
| <WhiteboardHistory isOpen={historyOpen} onClose={() => setHistoryOpen(false)} /> | |
| {/* Test panel */} | |
| {/* <WhiteboardTestPanel /> */} | |
| </div> | |
| </motion.div> | |
| )} | |
| </AnimatePresence> | |
| </> | |
| ); | |
| } | |