'use client' import * as React from 'react' import { ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight, Pencil, Plus, RefreshCw, Trash2 } from 'lucide-react' import { cn } from '../../lib/utils' import { SlideCanvas } from './SlideCanvas' import { SlideFilmstrip } from './SlideFilmstrip' import type { SlideData } from './types' export interface SlideDeckViewerProps { slides: SlideData[] /** Controlled active slide id. Omit for uncontrolled (defaults to first). */ activeId?: string onActiveChange?: (id: string) => void /** Regenerate the currently-active slide. */ onRegenerate?: (slide: SlideData) => void /** Delete the currently-active slide. Shows a delete control when provided. */ onDelete?: (slide: SlideData) => void /** Move the active slide one position left/right. Shows move controls when provided. */ onMove?: (slide: SlideData, direction: 'left' | 'right') => void /** Insert a blank slide after the active one. Shows an add control when provided. */ onInsert?: (afterSlide: SlideData) => void /** Edit the active slide's title/content (then it regenerates). Shows an edit control. */ onEdit?: (slide: SlideData, edits: { title?: string; content?: string }) => void /** Drag-to-reorder in the filmstrip: source + target 0-based indices. */ onReorder?: (fromIndex: number, toIndex: number) => void emptyState?: React.ReactNode /** Extra actions rendered in the stage toolbar (e.g. approve / edit). */ toolbarActions?: (slide: SlideData) => React.ReactNode renderStructured?: (slide: SlideData) => React.ReactNode className?: string } /** * Google-Slides-style deck viewer: a large active-slide stage with prev/next + * regenerate controls, a position indicator, and a thumbnail filmstrip. * Keyboard arrows navigate. App-agnostic — `present-web` supplies the slides. */ export function SlideDeckViewer({ slides, activeId, onActiveChange, onRegenerate, onDelete, onMove, onInsert, onEdit, onReorder, emptyState, toolbarActions, renderStructured, className, }: SlideDeckViewerProps) { const isControlled = activeId !== undefined const [internalId, setInternalId] = React.useState(slides[0]?.id) const currentId = isControlled ? activeId : internalId const index = Math.max(0, slides.findIndex((s) => s.id === currentId)) const active = slides[index] // Inline edit of the active slide's title/content. const [editing, setEditing] = React.useState(false) const [editTitle, setEditTitle] = React.useState('') const [editContent, setEditContent] = React.useState('') const openEditor = () => { setEditTitle(active?.title ?? '') setEditContent(active?.content ?? '') setEditing(true) } const saveEdit = () => { if (active) onEdit?.(active, { title: editTitle, content: editContent }) setEditing(false) } const goTo = React.useCallback( (i: number) => { const clamped = Math.min(slides.length - 1, Math.max(0, i)) const next = slides[clamped] if (!next) return if (!isControlled) setInternalId(next.id) onActiveChange?.(next.id) }, [slides, isControlled, onActiveChange] ) // Global ←/→ navigation: works without first focusing the stage, but stays // out of the way of text entry — when the user is typing in the chat composer // (or any input/textarea/contenteditable) or holding a modifier, the arrows // belong to the cursor, not the deck. React.useEffect(() => { function onKey(e: KeyboardEvent) { if (e.key !== 'ArrowRight' && e.key !== 'ArrowLeft') return if (e.defaultPrevented || e.metaKey || e.ctrlKey || e.altKey) return const t = e.target as HTMLElement | null const tag = t?.tagName if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT' || t?.isContentEditable) { return } e.preventDefault() goTo(e.key === 'ArrowRight' ? index + 1 : index - 1) } window.addEventListener('keydown', onKey) return () => window.removeEventListener('keydown', onKey) }, [goTo, index]) if (slides.length === 0) { return (
{emptyState ?? 'No slides yet.'}
) } return (
{/* Stage toolbar */}
{index + 1} / {slides.length}
{onMove && active && slides.length > 1 && (
)} {onInsert && active && ( )} {onEdit && active && ( )} {active && toolbarActions?.(active)} {onRegenerate && active && ( )} {onDelete && active && slides.length > 1 && ( )}
{/* Stage */}
{active && ( onRegenerate(active) : undefined} renderStructured={renderStructured} /> )} {editing && active && (

Edit slide {active.slideNumber}

setEditTitle(e.target.value)} aria-label="Slide title" className="mt-1 w-full rounded-md border border-input bg-background px-3 py-2 text-sm" />