Merge pull request #474 from presenton/fix/present-mode
refactor: Revamp Present mode
This commit is contained in:
commit
9fc867e23b
4 changed files with 281 additions and 201 deletions
|
|
@ -6,40 +6,54 @@ import { V1ContentRender } from '../../(presentation-generator)/components/V1Con
|
|||
const BASE_WIDTH = 1280;
|
||||
const BASE_HEIGHT = 720;
|
||||
|
||||
const SlideScale = ({ slide, theme }: { slide: any, theme?: any }) => {
|
||||
const SlideScale = ({
|
||||
slide,
|
||||
theme,
|
||||
isEditMode = true,
|
||||
/** Fill viewport; scale may exceed 1 so slides appear larger in present mode */
|
||||
presentMode = false,
|
||||
}: { slide: any; theme?: any; isEditMode?: boolean; presentMode?: boolean }) => {
|
||||
|
||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||
const [containerWidth, setContainerWidth] = useState<number>(0);
|
||||
const [box, setBox] = useState({ w: 0, h: 0 });
|
||||
|
||||
const scale = useMemo(() => {
|
||||
// Slight padding to avoid overflow due to borders/scrollbars
|
||||
const safeWidth = Math.max(0, containerWidth + 20);
|
||||
if (presentMode) {
|
||||
const { w, h } = box;
|
||||
if (w < 1 || h < 1) return 1;
|
||||
const sx = (w / BASE_WIDTH) * 0.995;
|
||||
const sy = (h / BASE_HEIGHT) * 0.995;
|
||||
return Math.min(sx, sy);
|
||||
}
|
||||
const safeWidth = Math.max(0, box.w + 20);
|
||||
if (!safeWidth) return 1;
|
||||
return Math.min((safeWidth / BASE_WIDTH) * 0.98, 1);
|
||||
}, [containerWidth]);
|
||||
}, [presentMode, box.w, box.h]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!containerRef.current) return;
|
||||
|
||||
const el = containerRef.current;
|
||||
const ro = new ResizeObserver(() => {
|
||||
// Use clientWidth so we match the actual available column width
|
||||
setContainerWidth(el.clientWidth);
|
||||
setBox({ w: el.clientWidth, h: el.clientHeight });
|
||||
});
|
||||
|
||||
ro.observe(el);
|
||||
// Initial measure
|
||||
setContainerWidth(el.clientWidth);
|
||||
setBox({ w: el.clientWidth, h: el.clientHeight });
|
||||
|
||||
return () => ro.disconnect();
|
||||
}, []);
|
||||
return (<div
|
||||
ref={containerRef}
|
||||
className="relative w-full shadow-md"
|
||||
className={`relative w-full ${presentMode ? "flex h-full min-h-0 items-center justify-center shadow-none" : "shadow-md"}`}
|
||||
>
|
||||
<div
|
||||
className="relative mx-auto max-w-[1280px] "
|
||||
style={{ height: `${BASE_HEIGHT * scale}px`, overflow: "hidden" }}
|
||||
className={presentMode ? "relative mx-auto shrink-0" : "relative mx-auto max-w-[1280px]"}
|
||||
style={{
|
||||
width: presentMode ? `${BASE_WIDTH * scale}px` : undefined,
|
||||
height: `${BASE_HEIGHT * scale}px`,
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="absolute top-0 left-0"
|
||||
|
|
@ -67,7 +81,7 @@ const SlideScale = ({ slide, theme }: { slide: any, theme?: any }) => {
|
|||
aria-hidden="true"
|
||||
|
||||
/> */}
|
||||
<V1ContentRender slide={slide} isEditMode={true} theme={theme} />
|
||||
<V1ContentRender slide={slide} isEditMode={isEditMode} theme={theme} />
|
||||
</div>
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
"use client";
|
||||
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
||||
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import {
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
|
|
@ -8,297 +9,360 @@ import {
|
|||
Maximize2,
|
||||
StickyNote,
|
||||
EyeOff,
|
||||
Keyboard,
|
||||
} from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Slide } from "../../types/slide";
|
||||
import { V1ContentRender } from "../../components/V1ContentRender";
|
||||
|
||||
|
||||
import SlideScale from "../../components/PresentationRender";
|
||||
import type { Theme } from "../../services/api/types";
|
||||
|
||||
interface PresentationModeProps {
|
||||
slides: Slide[];
|
||||
currentSlide: number;
|
||||
|
||||
theme?: Theme | null;
|
||||
isFullscreen: boolean;
|
||||
onFullscreenToggle: () => void;
|
||||
onExit: () => void;
|
||||
onSlideChange: (slideNumber: number) => void;
|
||||
}
|
||||
|
||||
const PresentationMode: React.FC<PresentationModeProps> = ({
|
||||
const CHROME_HIDE_MS = 800;
|
||||
|
||||
const PresentationMode: React.FC<PresentationModeProps> = ({
|
||||
slides,
|
||||
currentSlide,
|
||||
|
||||
theme,
|
||||
isFullscreen,
|
||||
onFullscreenToggle,
|
||||
onExit,
|
||||
onSlideChange,
|
||||
|
||||
|
||||
}) => {
|
||||
if (slides === undefined || slides === null || slides.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const rootRef = useRef<HTMLDivElement>(null);
|
||||
const hideChromeTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const [showSpeakerNotes, setShowSpeakerNotes] = useState(true);
|
||||
const [chromeVisible, setChromeVisible] = useState(true);
|
||||
|
||||
const currentSpeakerNote = useMemo(
|
||||
() => slides[currentSlide]?.speaker_note?.trim() || "",
|
||||
[slides, currentSlide]
|
||||
);
|
||||
|
||||
const activeSlide = slides[currentSlide];
|
||||
|
||||
const recomputeScale = useCallback(() => {
|
||||
if (typeof window === "undefined") return;
|
||||
const padding = isFullscreen ? 0 : 64; // match p-8 when not fullscreen
|
||||
const fullscreenMargin = isFullscreen ? 16 : 0; // small safety margin to prevent clipping
|
||||
const availableWidth = Math.max(window.innerWidth - padding - fullscreenMargin, 0);
|
||||
const availableHeight = Math.max(window.innerHeight - padding - fullscreenMargin, 0);
|
||||
const baseW = 1280;
|
||||
const baseH = 720;
|
||||
const s = Math.min(availableWidth / baseW, availableHeight / baseH);
|
||||
|
||||
const bumpChromeVisibility = useCallback(() => {
|
||||
setChromeVisible(true);
|
||||
if (hideChromeTimerRef.current) clearTimeout(hideChromeTimerRef.current);
|
||||
hideChromeTimerRef.current = setTimeout(() => {
|
||||
if (isFullscreen) setChromeVisible(false);
|
||||
}, CHROME_HIDE_MS);
|
||||
}, [isFullscreen]);
|
||||
|
||||
useEffect(() => {
|
||||
recomputeScale();
|
||||
window.addEventListener("resize", recomputeScale);
|
||||
return () => window.removeEventListener("resize", recomputeScale);
|
||||
}, [recomputeScale]);
|
||||
rootRef.current?.focus({ preventScroll: true });
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isFullscreen) {
|
||||
setChromeVisible(true);
|
||||
if (hideChromeTimerRef.current) {
|
||||
clearTimeout(hideChromeTimerRef.current);
|
||||
hideChromeTimerRef.current = null;
|
||||
}
|
||||
return;
|
||||
}
|
||||
bumpChromeVisibility();
|
||||
return () => {
|
||||
if (hideChromeTimerRef.current) clearTimeout(hideChromeTimerRef.current);
|
||||
};
|
||||
}, [isFullscreen, bumpChromeVisibility]);
|
||||
|
||||
// Modify the handleKeyPress to prevent default behavior
|
||||
const handleKeyPress = useCallback(
|
||||
const handlePointerActivity = useCallback(() => {
|
||||
bumpChromeVisibility();
|
||||
}, [bumpChromeVisibility]);
|
||||
|
||||
const goNext = useCallback(() => {
|
||||
if (currentSlide < slides.length - 1) onSlideChange(currentSlide + 1);
|
||||
}, [currentSlide, slides.length, onSlideChange]);
|
||||
|
||||
const goPrev = useCallback(() => {
|
||||
if (currentSlide > 0) onSlideChange(currentSlide - 1);
|
||||
}, [currentSlide, onSlideChange]);
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
(event: KeyboardEvent) => {
|
||||
event.preventDefault(); // Prevent default scroll behavior
|
||||
const navKeys = [
|
||||
"ArrowRight",
|
||||
"ArrowLeft",
|
||||
"ArrowUp",
|
||||
"ArrowDown",
|
||||
" ",
|
||||
"Home",
|
||||
"End",
|
||||
"PageDown",
|
||||
"PageUp",
|
||||
];
|
||||
if (navKeys.includes(event.key)) {
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
if (event.repeat) {
|
||||
if (event.key === " " || event.key === "ArrowRight" || event.key === "ArrowLeft") {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
switch (event.key) {
|
||||
case "ArrowRight":
|
||||
case "ArrowDown":
|
||||
case " ": // Space key
|
||||
if (currentSlide < slides.length - 1) {
|
||||
onSlideChange(currentSlide + 1);
|
||||
}
|
||||
case " ":
|
||||
case "PageDown":
|
||||
goNext();
|
||||
break;
|
||||
case "ArrowLeft":
|
||||
case "ArrowUp":
|
||||
if (currentSlide > 0) {
|
||||
onSlideChange(currentSlide - 1);
|
||||
case "PageUp":
|
||||
goPrev();
|
||||
break;
|
||||
case "Home":
|
||||
if (currentSlide !== 0) onSlideChange(0);
|
||||
break;
|
||||
case "End":
|
||||
if (slides.length > 0 && currentSlide !== slides.length - 1) {
|
||||
onSlideChange(slides.length - 1);
|
||||
}
|
||||
break;
|
||||
case "Escape":
|
||||
// If fullscreen is active, only exit fullscreen on first ESC. Second ESC exits present mode.
|
||||
if (document.fullscreenElement) {
|
||||
try { document.exitFullscreen(); } catch (_) { }
|
||||
try {
|
||||
document.exitFullscreen();
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
return;
|
||||
}
|
||||
onExit();
|
||||
break;
|
||||
case "f":
|
||||
case "F":
|
||||
onFullscreenToggle();
|
||||
if (!event.ctrlKey && !event.metaKey && !event.altKey) {
|
||||
onFullscreenToggle();
|
||||
}
|
||||
break;
|
||||
case "n":
|
||||
case "N":
|
||||
setShowSpeakerNotes((prev) => !prev);
|
||||
if (!event.ctrlKey && !event.metaKey && !event.altKey) {
|
||||
setShowSpeakerNotes((prev) => !prev);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
},
|
||||
[currentSlide, slides.length, onSlideChange, onExit, onFullscreenToggle, isFullscreen]
|
||||
[currentSlide, slides.length, onSlideChange, onExit, onFullscreenToggle, goNext, goPrev]
|
||||
);
|
||||
|
||||
// Add both keydown and keyup listeners
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
// Prevent default behavior for arrow keys and space
|
||||
if (
|
||||
["ArrowRight", "ArrowLeft", "ArrowUp", "ArrowDown", " "].includes(e.key)
|
||||
) {
|
||||
e.preventDefault();
|
||||
}
|
||||
handleKeyPress(e);
|
||||
};
|
||||
|
||||
window.addEventListener("keydown", handleKeyDown);
|
||||
return () => {
|
||||
window.removeEventListener("keydown", handleKeyDown);
|
||||
};
|
||||
}, [handleKeyPress]);
|
||||
|
||||
// Add click handlers for the slide area
|
||||
const handleSlideClick = (e: React.MouseEvent) => {
|
||||
// Don't trigger navigation if clicking on controls
|
||||
if ((e.target as HTMLElement).closest(".presentation-controls")) {
|
||||
return;
|
||||
}
|
||||
return () => window.removeEventListener("keydown", handleKeyDown);
|
||||
}, [handleKeyDown]);
|
||||
|
||||
const handleSlideAreaClick = (e: React.MouseEvent) => {
|
||||
if ((e.target as HTMLElement).closest(".presentation-controls")) return;
|
||||
const clickX = e.clientX;
|
||||
const windowWidth = window.innerWidth;
|
||||
|
||||
if (clickX < windowWidth / 3) {
|
||||
if (currentSlide > 0) {
|
||||
onSlideChange(currentSlide - 1);
|
||||
}
|
||||
} else if (clickX > (windowWidth * 2) / 3) {
|
||||
if (currentSlide < slides.length - 1) {
|
||||
onSlideChange(currentSlide + 1);
|
||||
}
|
||||
}
|
||||
const w = window.innerWidth;
|
||||
if (clickX < w / 5) goPrev();
|
||||
else if (clickX > (w * 4) / 5) goNext();
|
||||
};
|
||||
|
||||
// Handle Escape key separately
|
||||
useEffect(() => {
|
||||
const handleEscKey = (e: KeyboardEvent) => {
|
||||
if (e.key === "Escape" && isFullscreen) {
|
||||
onFullscreenToggle(); // Just toggle fullscreen, don't exit presentation
|
||||
}
|
||||
};
|
||||
const progress = slides.length > 0 ? ((currentSlide + 1) / slides.length) * 100 : 0;
|
||||
|
||||
document.addEventListener("keydown", handleEscKey);
|
||||
return () => document.removeEventListener("keydown", handleEscKey);
|
||||
}, [isFullscreen, onFullscreenToggle]);
|
||||
if (slides === undefined || slides === null || slides.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="fixed inset-0 flex flex-col"
|
||||
style={{ backgroundColor: "var(--page-background-color,#c8c7c9)" }}
|
||||
ref={rootRef}
|
||||
role="application"
|
||||
aria-label="Presentation"
|
||||
className="fixed inset-0 z-[100] flex flex-col outline-none select-none"
|
||||
style={{ backgroundColor: "var(--page-background-color, #c8c7c9)" }}
|
||||
tabIndex={0}
|
||||
onClick={handleSlideClick}
|
||||
onMouseMove={handlePointerActivity}
|
||||
onClick={handleSlideAreaClick}
|
||||
>
|
||||
{/* Controls - Only show when not in fullscreen */}
|
||||
{!isFullscreen && (
|
||||
<>
|
||||
<div className="presentation-controls absolute top-4 right-4 flex items-center gap-2 z-50">
|
||||
<Button
|
||||
variant="ghost"
|
||||
style={{ color: "var(--text-body-color,#000000)" }}
|
||||
size="icon"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onFullscreenToggle();
|
||||
}}
|
||||
className="text-white hover:bg-white/20"
|
||||
>
|
||||
{isFullscreen ? (
|
||||
<Minimize2 className="h-5 w-5" />
|
||||
) : (
|
||||
<Maximize2 className="h-5 w-5" />
|
||||
)}
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
style={{ color: "var(--text-body-color,#000000)" }}
|
||||
size="icon"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onExit();
|
||||
}}
|
||||
className="text-white hover:bg-white/20"
|
||||
>
|
||||
<X className="h-5 w-5" />
|
||||
</Button>
|
||||
</div>
|
||||
<span className="sr-only">
|
||||
Slide {currentSlide + 1} of {slides.length}
|
||||
</span>
|
||||
|
||||
<div className="presentation-controls absolute bottom-4 left-1/2 -translate-x-1/2 flex items-center gap-4 z-50">
|
||||
<Button
|
||||
variant="ghost"
|
||||
style={{ color: "var(--text-body-color,#000000)" }}
|
||||
size="icon"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onSlideChange(currentSlide - 1);
|
||||
}}
|
||||
disabled={currentSlide === 0}
|
||||
className="text-white hover:bg-white/20"
|
||||
>
|
||||
<ChevronLeft className="h-5 w-5" style={{ color: "var(--text-body-color,#000000)" }} />
|
||||
</Button>
|
||||
<span className="text-white"
|
||||
style={{ color: "var(--text-body-color,#000000)" }}
|
||||
>
|
||||
{currentSlide + 1} / {slides.length}
|
||||
</span>
|
||||
<Button
|
||||
variant="ghost"
|
||||
style={{ color: "var(--text-body-color,#000000)" }}
|
||||
size="icon"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onSlideChange(currentSlide + 1);
|
||||
}}
|
||||
disabled={currentSlide === slides.length - 1}
|
||||
className="text-white hover:bg-white/20"
|
||||
>
|
||||
<ChevronRight className="h-5 w-5" style={{ color: "var(--text-body-color,#000000)" }} />
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Centered 16:9 stage for consistent alignment in normal + fullscreen modes */}
|
||||
<div className={`flex-1 min-h-0 flex items-center justify-center ${isFullscreen ? "px-6 py-8 md:px-10 md:py-12" : "p-8"}`}>
|
||||
<div
|
||||
className="relative rounded-sm font-inter"
|
||||
style={{
|
||||
aspectRatio: "16 / 9",
|
||||
width: isFullscreen
|
||||
? "min(90vw, calc(88vh * 16 / 9))"
|
||||
: "min(calc(100vw - 4rem), calc((100vh - 4rem) * 16 / 9))",
|
||||
maxHeight: isFullscreen ? "88vh" : "calc(100vh - 4rem)",
|
||||
}}
|
||||
>
|
||||
{slides.length > 0 && slides.map((slide, index) => (
|
||||
<div
|
||||
key={slide.id}
|
||||
className={index === currentSlide ? "h-full w-full" : "hidden h-full w-full"}
|
||||
>
|
||||
<V1ContentRender slide={slide} isEditMode={true} />
|
||||
</div>
|
||||
))}
|
||||
{/* Top bar — fullscreen: auto-hide */}
|
||||
<div
|
||||
className={`presentation-controls absolute left-0 right-0 top-0 z-50 flex justify-end gap-2 px-3 py-3 transition-opacity duration-300 md:px-4 ${isFullscreen && !chromeVisible ? "pointer-events-none opacity-0" : "opacity-100"
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-1 rounded-full bg-white/95 px-1 py-1 backdrop-blur-sm">
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
title="Fullscreen (F)"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onFullscreenToggle();
|
||||
}}
|
||||
className="h-9 w-9 text-gray-800 hover:bg-gray-100"
|
||||
>
|
||||
{isFullscreen ? <Minimize2 className="h-5 w-5" /> : <Maximize2 className="h-5 w-5" />}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
title="Exit presentation (Esc)"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onExit();
|
||||
}}
|
||||
className="h-9 w-9 text-gray-800 hover:bg-gray-100"
|
||||
>
|
||||
<X className="h-5 w-5" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{currentSpeakerNote && (
|
||||
<div className="presentation-controls absolute bottom-4 right-4 z-50">
|
||||
{/* Slide stage — large viewport; SlideScale uses width+height so slides scale up */}
|
||||
<div
|
||||
className={`flex min-h-0 flex-1 items-stretch justify-stretch ${isFullscreen ? "px-2 pb-9 pt-12 sm:px-3" : "px-3 pb-24 pt-14 sm:px-4 md:pb-28 md:pt-16"
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className={`min-h-0 w-full flex-1 overflow-hidden rounded-sm `}
|
||||
>
|
||||
{activeSlide ? (
|
||||
<SlideScale
|
||||
key={activeSlide.id ?? `slide-${currentSlide}`}
|
||||
slide={activeSlide}
|
||||
theme={theme ?? undefined}
|
||||
isEditMode={false}
|
||||
presentMode
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Progress */}
|
||||
<div
|
||||
className={`absolute bottom-0 left-0 right-0 z-40 h-1 bg-gray-200 ${isFullscreen && !chromeVisible ? "opacity-70" : "opacity-100"
|
||||
}`}
|
||||
aria-hidden
|
||||
>
|
||||
<div
|
||||
className="h-full bg-[#5141e5] transition-[width] duration-300 ease-out"
|
||||
style={{ width: `${progress}%` }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Bottom controls */}
|
||||
<div
|
||||
className={`presentation-controls absolute bottom-4 left-1/2 z-50 flex -translate-x-1/2 items-center gap-3 rounded-full border border-gray-200/90 bg-white/95 px-2 py-2 shadow-md backdrop-blur-sm transition-all duration-300 md:gap-4 md:px-3 ${isFullscreen && !chromeVisible
|
||||
? "pointer-events-none translate-y-4 opacity-0"
|
||||
: "translate-y-0 opacity-100"
|
||||
}`}
|
||||
>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
title="Previous slide"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
goPrev();
|
||||
}}
|
||||
disabled={currentSlide === 0}
|
||||
className="h-10 w-10 text-gray-800 hover:bg-gray-100 disabled:opacity-35"
|
||||
>
|
||||
<ChevronLeft className="h-6 w-6" />
|
||||
</Button>
|
||||
<div
|
||||
className="min-w-22 text-center text-sm font-medium tabular-nums text-gray-800"
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
aria-atomic="true"
|
||||
>
|
||||
{currentSlide + 1} / {slides.length}
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
title="Next slide"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
goNext();
|
||||
}}
|
||||
disabled={currentSlide === slides.length - 1}
|
||||
className="h-10 w-10 text-gray-800 hover:bg-gray-100 disabled:opacity-35"
|
||||
>
|
||||
<ChevronRight className="h-6 w-6" />
|
||||
</Button>
|
||||
<div className="mx-1 hidden h-6 w-px bg-gray-200 sm:block" />
|
||||
<div
|
||||
className="hidden max-w-[200px] items-center gap-1.5 text-[11px] leading-tight text-gray-500 sm:flex"
|
||||
title="Keyboard shortcuts"
|
||||
>
|
||||
<Keyboard className="h-3.5 w-3.5 shrink-0" />
|
||||
<span>
|
||||
← → space · Home/End · F fullscreen · N notes · Esc exit
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{currentSpeakerNote ? (
|
||||
<div
|
||||
className={`presentation-controls absolute bottom-16 right-3 z-50 max-w-[min(380px,46vw)] md:bottom-20 md:right-6 ${isFullscreen && !chromeVisible ? "opacity-90" : ""
|
||||
}`}
|
||||
>
|
||||
{showSpeakerNotes ? (
|
||||
<div className="w-[360px] max-w-[50vw] rounded-xl border border-black/10 bg-white/95 shadow-xl backdrop-blur-sm">
|
||||
<div className="flex items-center justify-between border-b border-black/10 px-3 py-2">
|
||||
<div className="rounded-xl border border-gray-200/90 bg-white/95 shadow-lg backdrop-blur-sm">
|
||||
<div className="flex items-center justify-between border-b border-gray-100 px-3 py-2">
|
||||
<div className="flex items-center gap-2 text-sm font-medium text-gray-800">
|
||||
<StickyNote className="h-4 w-4" />
|
||||
<StickyNote className="h-4 w-4 text-amber-600" />
|
||||
Speaker notes
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setShowSpeakerNotes(false);
|
||||
}}
|
||||
className="h-8 px-2 text-gray-600 hover:bg-black/5 hover:text-gray-800"
|
||||
className="h-8 px-2 text-gray-600 hover:bg-gray-100 hover:text-gray-900"
|
||||
>
|
||||
<EyeOff className="mr-1 h-4 w-4" />
|
||||
Hide
|
||||
</Button>
|
||||
</div>
|
||||
<div className="max-h-[28vh] overflow-auto whitespace-pre-wrap px-3 py-2 text-sm text-gray-700">
|
||||
<div className="max-h-[min(28vh,220px)] overflow-auto whitespace-pre-wrap px-3 py-2.5 text-sm leading-relaxed text-gray-700">
|
||||
{currentSpeakerNote}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setShowSpeakerNotes(true);
|
||||
}}
|
||||
className="h-9 rounded-full border border-black/10 bg-white/95 px-3 text-gray-800 shadow-md hover:bg-white"
|
||||
className="h-9 rounded-full border border-gray-200 bg-white/95 px-3 text-gray-800 shadow-md backdrop-blur-sm hover:bg-gray-50"
|
||||
>
|
||||
<StickyNote className="mr-2 h-4 w-4" />
|
||||
<StickyNote className="mr-2 h-4 w-4 text-amber-600" />
|
||||
Show notes
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -61,6 +61,7 @@ const PresentationPage: React.FC<PresentationPageProps> = ({
|
|||
const {
|
||||
isPresentMode,
|
||||
stream,
|
||||
currentSlide: presentSlideFromUrl,
|
||||
handleSlideClick,
|
||||
toggleFullscreen,
|
||||
handlePresentExit,
|
||||
|
|
@ -93,7 +94,8 @@ const PresentationPage: React.FC<PresentationPageProps> = ({
|
|||
return (
|
||||
<PresentationMode
|
||||
slides={presentationData?.slides!}
|
||||
currentSlide={selectedSlide}
|
||||
currentSlide={presentSlideFromUrl}
|
||||
theme={presentationData?.theme ?? undefined}
|
||||
isFullscreen={isFullscreen}
|
||||
onFullscreenToggle={toggleFullscreen}
|
||||
onExit={handlePresentExit}
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
Loading…
Add table
Reference in a new issue