|
|
|
|
| import { useMemo } from 'react'; |
| import { useSceneSelector } from '@/lib/contexts/scene-context'; |
| import { useCanvasStore } from '@/lib/store/canvas'; |
| import type { SlideContent } from '@/lib/types/stage'; |
| import type { PPTElement } from '@/lib/types/slides'; |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| export function HighlightOverlay() { |
| const highlightedElementIds = useCanvasStore.use.highlightedElementIds(); |
| const highlightOptions = useCanvasStore.use.highlightOptions(); |
|
|
| |
| const elements = useSceneSelector<SlideContent, PPTElement[]>( |
| (content) => content.canvas.elements, |
| ); |
|
|
| |
| const highlightedElements = useMemo(() => { |
| if (!highlightedElementIds.length) return []; |
| return elements.filter((el) => highlightedElementIds.includes(el.id) && el.type !== 'line'); |
| }, [elements, highlightedElementIds]); |
|
|
| |
| if (!highlightedElements.length || !highlightOptions) { |
| return null; |
| } |
|
|
| const { color = '#ff6b6b', opacity = 0.3, borderWidth = 3, animated = true } = highlightOptions; |
|
|
| return ( |
| <> |
| {highlightedElements.map((element) => { |
| // Type guard: line elements are already filtered out above |
| // Use 'in' operator for runtime checks to satisfy TypeScript |
| const height = 'height' in element ? element.height : 0; |
| const rotate = 'rotate' in element ? element.rotate : 0; |
| return ( |
| <div |
| key={element.id} |
| className="highlight-overlay absolute pointer-events-none" |
| style={{ |
| left: `${element.left}px`, |
| top: `${element.top}px`, |
| width: `${element.width}px`, |
| height: `${height}px`, |
| transform: `rotate(${rotate || 0}deg)`, |
| transformOrigin: 'center', |
| zIndex: 999, |
| transition: 'all 0.3s ease-in-out', |
| }} |
| > |
| {/* Highlight border */} |
| <div |
| className={`absolute inset-0 rounded ${animated ? 'animate-pulse' : ''}`} |
| style={{ |
| border: `${borderWidth}px solid ${color}`, |
| boxShadow: ` |
| 0 0 ${borderWidth * 3}px ${color}, |
| inset 0 0 ${borderWidth * 2}px rgba(255,255,255,${opacity * 0.5}) |
| `, |
| backgroundColor: `${color}${Math.round(opacity * 255) |
| .toString(16) |
| .padStart(2, '0')}`, |
| }} |
| /> |
| |
| {/* Glow effect */} |
| {animated && ( |
| <div |
| className="absolute inset-0 rounded animate-ping" |
| style={{ |
| border: `${borderWidth}px solid ${color}`, |
| opacity: 0.5, |
| animationDuration: '2s', |
| }} |
| /> |
| )} |
| </div> |
| ); |
| })} |
| |
| {/* CSS animation (breathing light effect) */} |
| <style>{` |
| @keyframes breathe { |
| 0%, |
| 100% { |
| opacity: 0.6; |
| transform: scale(1); |
| } |
| 50% { |
| opacity: 1; |
| transform: scale(1.02); |
| } |
| } |
| |
| .highlight-overlay.animate-pulse { |
| animation: breathe 2s ease-in-out infinite; |
| } |
| `}</style> |
| </> |
| ); |
| } |
|
|