"use client"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { parseDeck, type DeckSlide } from "@/lib/deck"; import { useT } from "@/lib/i18n"; type Props = { html: string; /** When `true`, the deck viewer is the visible tab — it can grab keyboard events. */ active: boolean; /** Bubble out the latest main-slide iframe so screenshot/export can target it. */ onMainIframe?: (el: HTMLIFrameElement | null) => void; /** Bubble out the parsed slides so the export menu can iterate them. */ onSlides?: (slides: DeckSlide[]) => void; }; export function DeckViewer({ html, active, onMainIframe, onSlides }: Props) { const t = useT(); const parsed = useMemo(() => parseDeck(html), [html]); const slides = parsed.slides; const [index, setIndex] = useState(0); const [showNotes, setShowNotes] = useState(false); const wrapRef = useRef(null); const mainIframeRef = useRef(null); const [isFullscreen, setIsFullscreen] = useState(false); // Keep index in range when the deck shrinks (e.g. live re-render). useEffect(() => { if (index >= slides.length) setIndex(Math.max(0, slides.length - 1)); }, [slides.length, index]); // Bubble the parsed slides up so the export menu can use them. useEffect(() => { onSlides?.(slides); }, [slides, onSlides]); // Bubble main iframe up so screenshot can re-use the existing helpers. useEffect(() => { onMainIframe?.(mainIframeRef.current); }); const next = useCallback(() => setIndex((i) => Math.min(slides.length - 1, i + 1)), [slides.length]); const prev = useCallback(() => setIndex((i) => Math.max(0, i - 1)), []); // Keyboard nav (only when this tab is the active one — otherwise the // editor textarea would steal arrow keys, but we only listen for ←/→ on the // wrapper and on document while in fullscreen). useEffect(() => { if (!active) return; const onKey = (e: KeyboardEvent) => { if (e.target instanceof HTMLElement) { const tag = e.target.tagName; if (tag === "INPUT" || tag === "TEXTAREA" || e.target.isContentEditable) return; } if (e.key === "ArrowRight" || e.key === "PageDown" || e.key === " ") { e.preventDefault(); next(); } else if (e.key === "ArrowLeft" || e.key === "PageUp") { e.preventDefault(); prev(); } else if (e.key === "Home") { e.preventDefault(); setIndex(0); } else if (e.key === "End") { e.preventDefault(); setIndex(slides.length - 1); } else if (e.key === "f" || e.key === "F") { e.preventDefault(); enterFullscreen(); } else if (e.key === "n" || e.key === "N") { e.preventDefault(); setShowNotes((v) => !v); } }; window.addEventListener("keydown", onKey); return () => window.removeEventListener("keydown", onKey); }, [active, next, prev, slides.length]); // Track browser-level fullscreen state. useEffect(() => { const onFs = () => setIsFullscreen(document.fullscreenElement === wrapRef.current); document.addEventListener("fullscreenchange", onFs); return () => document.removeEventListener("fullscreenchange", onFs); }, []); const enterFullscreen = useCallback(() => { const el = wrapRef.current; if (!el) return; if (document.fullscreenElement === el) { document.exitFullscreen?.(); } else { el.requestFullscreen?.().catch(() => {}); } }, []); if (slides.length === 0) { return (
{t("deck.empty")}
); } const current = slides[index]; return (
{/* main slide canvas */}