promptslide 0.3.16 → 0.3.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,14 +1,25 @@
1
1
  import { LayoutGroup } from "framer-motion"
2
- import { ChevronLeft, ChevronRight, Download, Grid3X3, List, Maximize, MessageCircle, Monitor } from "lucide-react"
3
- import { useCallback, useEffect, useMemo, useRef, useState } from "react"
2
+ import {
3
+ ChevronLeft,
4
+ ChevronRight,
5
+ Download,
6
+ Grid3X3,
7
+ List,
8
+ MessageCircle,
9
+ Monitor,
10
+ PanelLeftClose,
11
+ PanelLeftOpen,
12
+ Play
13
+ } from "lucide-react"
14
+ import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react"
4
15
 
16
+ import type { Annotation, AnnotationTarget } from "./annotations"
5
17
  import type { SlideTransitionType } from "./transitions"
6
18
  import type { SlideConfig } from "./types"
7
19
 
8
20
  import { SLIDE_DIMENSIONS } from "./animation-config"
9
21
  import { AnimationProvider } from "./animation-context"
10
22
  import { AnnotationOverlay, AnnotationPanel, useAnnotations } from "./annotations"
11
- import type { Annotation, AnnotationTarget } from "./annotations"
12
23
  import { SlideErrorBoundary } from "./slide-error-boundary"
13
24
  import { SlideRenderer } from "./slide-renderer"
14
25
  import { useSlideNavigation } from "./use-slide-navigation"
@@ -19,6 +30,92 @@ import { cn } from "./utils"
19
30
  // =============================================================================
20
31
 
21
32
  type ViewMode = "slide" | "list" | "grid"
33
+ const NARROW_VIEWPORT_WIDTH = 768
34
+ const DEFAULT_THUMBNAIL_WIDTH = 240
35
+ const MIN_THUMBNAIL_WIDTH = 176
36
+ const MAX_THUMBNAIL_WIDTH = 480
37
+ const THUMBNAIL_WIDTH_STORAGE_KEY = "promptslide:thumbnail-width"
38
+
39
+ function GridThumbnail({
40
+ slide,
41
+ index,
42
+ total
43
+ }: {
44
+ slide: SlideConfig
45
+ index: number
46
+ total: number
47
+ }) {
48
+ const viewportRef = useRef<HTMLDivElement>(null)
49
+ const canvasRef = useRef<HTMLDivElement>(null)
50
+ const SlideComponent = slide.component
51
+
52
+ useLayoutEffect(() => {
53
+ const viewport = viewportRef.current
54
+ const canvas = canvasRef.current
55
+ if (!viewport || !canvas) return
56
+ const resize = () => {
57
+ canvas.style.transform = `scale(${viewport.clientWidth / SLIDE_DIMENSIONS.width})`
58
+ }
59
+ resize()
60
+ const observer = new ResizeObserver(resize)
61
+ observer.observe(viewport)
62
+ return () => observer.disconnect()
63
+ }, [])
64
+
65
+ return (
66
+ <div ref={viewportRef} className="absolute inset-0 overflow-hidden">
67
+ <div
68
+ ref={canvasRef}
69
+ className="absolute top-0 left-0 origin-top-left"
70
+ style={{ width: SLIDE_DIMENSIONS.width, height: SLIDE_DIMENSIONS.height }}
71
+ >
72
+ <AnimationProvider currentStep={slide.steps} totalSteps={slide.steps} showAllAnimations>
73
+ <SlideErrorBoundary slideIndex={index} slideTitle={slide.title}>
74
+ <SlideComponent slideNumber={index + 1} totalSlides={total} />
75
+ </SlideErrorBoundary>
76
+ </AnimationProvider>
77
+ </div>
78
+ </div>
79
+ )
80
+ }
81
+
82
+ function ListSlide({ slide, index, total }: { slide: SlideConfig; index: number; total: number }) {
83
+ const viewportRef = useRef<HTMLDivElement>(null)
84
+ const canvasRef = useRef<HTMLDivElement>(null)
85
+ const SlideComponent = slide.component
86
+
87
+ useLayoutEffect(() => {
88
+ const viewport = viewportRef.current
89
+ const canvas = canvasRef.current
90
+ if (!viewport || !canvas) return
91
+ const resize = () => {
92
+ canvas.style.transform = `scale(${viewport.clientWidth / SLIDE_DIMENSIONS.width})`
93
+ }
94
+ resize()
95
+ const observer = new ResizeObserver(resize)
96
+ observer.observe(viewport)
97
+ return () => observer.disconnect()
98
+ }, [])
99
+
100
+ return (
101
+ <div
102
+ ref={viewportRef}
103
+ className="relative aspect-video w-full overflow-hidden rounded-xl border border-neutral-800 bg-black shadow-sm print:m-0 print:h-[1080px] print:w-[1920px] print:break-after-page print:rounded-none print:border-0 print:shadow-none"
104
+ >
105
+ <div
106
+ ref={canvasRef}
107
+ className="list-slide-canvas absolute top-0 left-0 origin-top-left"
108
+ style={{ width: SLIDE_DIMENSIONS.width, height: SLIDE_DIMENSIONS.height }}
109
+ >
110
+ <AnimationProvider currentStep={slide.steps} totalSteps={slide.steps} showAllAnimations>
111
+ <SlideErrorBoundary slideIndex={index} slideTitle={slide.title}>
112
+ <SlideComponent slideNumber={index + 1} totalSlides={total} />
113
+ </SlideErrorBoundary>
114
+ </AnimationProvider>
115
+ </div>
116
+ </div>
117
+ )
118
+ }
22
119
 
23
120
  interface SlideDeckProps {
24
121
  slides: SlideConfig[]
@@ -27,9 +124,16 @@ interface SlideDeckProps {
27
124
  /** Annotation data to display. When provided (even empty array), annotation UI is enabled. When undefined, annotation UI is hidden. */
28
125
  annotations?: Annotation[]
29
126
  /** Called when the user creates an annotation */
30
- onAnnotationAdd?: (slideIndex: number, slideTitle: string, target: AnnotationTarget, body: string) => void
127
+ onAnnotationAdd?: (
128
+ slideIndex: number,
129
+ slideTitle: string,
130
+ target: AnnotationTarget,
131
+ body: string,
132
+ slideId?: string
133
+ ) => void
31
134
  /** Called when the user deletes an annotation */
32
135
  onAnnotationDelete?: (id: string) => void
136
+ onAnnotationUpdate?: (id: string, patch: Partial<Pick<Annotation, "body" | "status">>) => void
33
137
  }
34
138
 
35
139
  // =============================================================================
@@ -43,7 +147,20 @@ function SlideExportView({ slides, slideIndex }: { slides: SlideConfig[]; slideI
43
147
  const SlideComponent = slideConfig.component
44
148
 
45
149
  useEffect(() => {
46
- setReady(true)
150
+ let cancelled = false
151
+ const markReady = async () => {
152
+ await document.fonts.ready
153
+ await Promise.all(Array.from(document.images, image => image.decode().catch(() => {})))
154
+ requestAnimationFrame(() =>
155
+ requestAnimationFrame(() => {
156
+ if (!cancelled) setReady(true)
157
+ })
158
+ )
159
+ }
160
+ void markReady()
161
+ return () => {
162
+ cancelled = true
163
+ }
47
164
  }, [])
48
165
 
49
166
  return (
@@ -74,7 +191,15 @@ function SlideExportView({ slides, slideIndex }: { slides: SlideConfig[]; slideI
74
191
  // COMPONENT
75
192
  // =============================================================================
76
193
 
77
- export function SlideDeck({ slides, transition, directionalTransition, annotations, onAnnotationAdd, onAnnotationDelete }: SlideDeckProps) {
194
+ export function SlideDeck({
195
+ slides,
196
+ transition,
197
+ directionalTransition,
198
+ annotations,
199
+ onAnnotationAdd,
200
+ onAnnotationDelete,
201
+ onAnnotationUpdate
202
+ }: SlideDeckProps) {
78
203
  // Check for export mode via URL params
79
204
  const [exportParams] = useState(() => {
80
205
  if (typeof window === "undefined") return null
@@ -88,26 +213,203 @@ export function SlideDeck({ slides, transition, directionalTransition, annotatio
88
213
  }
89
214
 
90
215
  // Use internal useAnnotations as fallback when no external annotations prop is provided
91
- const internal = useAnnotations()
216
+ const internal = useAnnotations(slides)
92
217
  const isExternallyManaged = annotations !== undefined
93
218
  const effectiveAnnotations = isExternallyManaged ? annotations : internal.annotations
94
219
  const effectiveAdd = isExternallyManaged ? onAnnotationAdd : internal.addAnnotation
95
220
  const effectiveDelete = isExternallyManaged ? onAnnotationDelete : internal.deleteAnnotation
221
+ const effectiveUpdate = isExternallyManaged ? onAnnotationUpdate : internal.updateAnnotation
96
222
 
97
- const openCount = useMemo(() => effectiveAnnotations.filter(a => a.status === "open").length, [effectiveAnnotations])
98
- const getSlideAnnotations = useCallback(
99
- (slideIndex: number) => effectiveAnnotations.filter(a => a.slideIndex === slideIndex),
223
+ const openCount = useMemo(
224
+ () => effectiveAnnotations.filter(a => a.status === "open").length,
100
225
  [effectiveAnnotations]
101
226
  )
227
+ const getSlideAnnotations = useCallback(
228
+ (slideIndex: number) =>
229
+ effectiveAnnotations.filter(a =>
230
+ a.slideId ? a.slideId === slides[slideIndex]?.id : a.slideIndex === slideIndex
231
+ ),
232
+ [effectiveAnnotations, slides]
233
+ )
234
+ const unlinkedAnnotations = useMemo(
235
+ () =>
236
+ effectiveAnnotations.filter(a => a.slideId && !slides.some(slide => slide.id === a.slideId)),
237
+ [effectiveAnnotations, slides]
238
+ )
102
239
 
103
240
  const [viewMode, setViewMode] = useState<ViewMode>("slide")
241
+ const [isEditorOpen, setIsEditorOpen] = useState(false)
242
+ const [editorRevision, setEditorRevision] = useState(0)
243
+ useEffect(() => {
244
+ const onEditorState = (event: Event) => {
245
+ const open = Boolean((event as CustomEvent).detail?.open)
246
+ setIsEditorOpen(open)
247
+ if (open) {
248
+ setViewMode("slide")
249
+ setIsAnnotationMode(false)
250
+ setShowAnnotationPanel(false)
251
+ }
252
+ }
253
+ const refreshEditor = () => setEditorRevision(value => value + 1)
254
+ window.addEventListener("promptslide:editor-state", onEditorState)
255
+ window.addEventListener("promptslide:editor-refresh", refreshEditor)
256
+ return () => {
257
+ window.removeEventListener("promptslide:editor-state", onEditorState)
258
+ window.removeEventListener("promptslide:editor-refresh", refreshEditor)
259
+ }
260
+ }, [])
104
261
  const [isPresentationMode, setIsPresentationMode] = useState(false)
262
+ const [viewportWidth, setViewportWidth] = useState(() =>
263
+ typeof window === "undefined" ? 1280 : window.innerWidth
264
+ )
265
+ const [showThumbnailRail, setShowThumbnailRail] = useState(() =>
266
+ typeof window === "undefined" ? true : window.innerWidth >= NARROW_VIEWPORT_WIDTH
267
+ )
268
+ const [thumbnailWidth, setThumbnailWidth] = useState(() => {
269
+ if (typeof window === "undefined") return DEFAULT_THUMBNAIL_WIDTH
270
+ const savedWidth = Number(window.localStorage.getItem(THUMBNAIL_WIDTH_STORAGE_KEY))
271
+ return Number.isFinite(savedWidth) && savedWidth >= MIN_THUMBNAIL_WIDTH
272
+ ? Math.min(savedWidth, MAX_THUMBNAIL_WIDTH)
273
+ : DEFAULT_THUMBNAIL_WIDTH
274
+ })
105
275
  const [isAnnotationMode, setIsAnnotationMode] = useState(false)
106
276
  const [showAnnotationPanel, setShowAnnotationPanel] = useState(false)
107
277
  const [selectedAnnotationId, setSelectedAnnotationId] = useState<string | null>(null)
278
+ const [hoveredAnnotationId, setHoveredAnnotationId] = useState<string | null>(null)
279
+ const [isResizingThumbnails, setIsResizingThumbnails] = useState(false)
280
+ const [isDragCollapsed, setIsDragCollapsed] = useState(false)
281
+ const [isAnimatingDragBoundary, setIsAnimatingDragBoundary] = useState(false)
108
282
  const [scale, setScale] = useState(1)
109
283
  const containerRef = useRef<HTMLDivElement>(null)
110
284
  const slideContainerRef = useRef<HTMLDivElement>(null)
285
+ const previewViewportRef = useRef<HTMLDivElement>(null)
286
+ const activeThumbnailRef = useRef<HTMLButtonElement>(null)
287
+ const preferredRailOpenRef = useRef(true)
288
+ const wasNarrowViewportRef = useRef(viewportWidth < NARROW_VIEWPORT_WIDTH)
289
+ const resizeStartRef = useRef<{
290
+ pointerId: number
291
+ pointerX: number
292
+ width: number
293
+ } | null>(null)
294
+ const previousUserSelectRef = useRef("")
295
+ const previousCursorRef = useRef("")
296
+ const dragCollapsedRef = useRef(false)
297
+ const dragAnimationTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
298
+ const isNarrowViewport = viewportWidth < NARROW_VIEWPORT_WIDTH
299
+ const maxThumbnailWidth = isNarrowViewport
300
+ ? Math.max(120, Math.floor(viewportWidth * 0.8))
301
+ : Math.min(MAX_THUMBNAIL_WIDTH, viewportWidth - 360)
302
+ const minThumbnailWidth = Math.min(MIN_THUMBNAIL_WIDTH, maxThumbnailWidth)
303
+ const visibleThumbnailWidth = Math.min(thumbnailWidth, maxThumbnailWidth)
304
+ const isThumbnailRailVisible = showThumbnailRail && !isDragCollapsed
305
+
306
+ const setThumbnailRailOpen = useCallback((open: boolean) => {
307
+ preferredRailOpenRef.current = open
308
+ setShowThumbnailRail(open)
309
+ }, [])
310
+
311
+ useEffect(() => {
312
+ const handleResize = () => setViewportWidth(window.innerWidth)
313
+ window.addEventListener("resize", handleResize)
314
+ return () => window.removeEventListener("resize", handleResize)
315
+ }, [])
316
+
317
+ useEffect(() => {
318
+ if (isNarrowViewport !== wasNarrowViewportRef.current) {
319
+ setShowThumbnailRail(isNarrowViewport ? false : preferredRailOpenRef.current)
320
+ if (isNarrowViewport) {
321
+ setIsAnnotationMode(false)
322
+ setShowAnnotationPanel(false)
323
+ setSelectedAnnotationId(null)
324
+ }
325
+ wasNarrowViewportRef.current = isNarrowViewport
326
+ }
327
+ }, [isNarrowViewport])
328
+
329
+ useEffect(() => {
330
+ window.localStorage.setItem(THUMBNAIL_WIDTH_STORAGE_KEY, String(thumbnailWidth))
331
+ }, [thumbnailWidth])
332
+
333
+ useEffect(
334
+ () => () => {
335
+ if (resizeStartRef.current) {
336
+ document.body.style.userSelect = previousUserSelectRef.current
337
+ document.body.style.cursor = previousCursorRef.current
338
+ }
339
+ if (dragAnimationTimerRef.current) clearTimeout(dragAnimationTimerRef.current)
340
+ },
341
+ []
342
+ )
343
+
344
+ useEffect(() => {
345
+ if (
346
+ (!showThumbnailRail || isPresentationMode || viewMode !== "slide") &&
347
+ resizeStartRef.current
348
+ ) {
349
+ resizeStartRef.current = null
350
+ dragCollapsedRef.current = false
351
+ setIsResizingThumbnails(false)
352
+ setIsDragCollapsed(false)
353
+ setIsAnimatingDragBoundary(false)
354
+ if (dragAnimationTimerRef.current) clearTimeout(dragAnimationTimerRef.current)
355
+ document.body.style.userSelect = previousUserSelectRef.current
356
+ document.body.style.cursor = previousCursorRef.current
357
+ }
358
+ }, [showThumbnailRail, isPresentationMode, viewMode])
359
+
360
+ useEffect(() => {
361
+ if (!isResizingThumbnails) return
362
+
363
+ const getDraggedWidth = (clientX: number) => {
364
+ const start = resizeStartRef.current
365
+ return start ? start.width + clientX - start.pointerX : null
366
+ }
367
+ const handlePointerMove = (event: PointerEvent) => {
368
+ if (event.pointerId !== resizeStartRef.current?.pointerId) return
369
+ const nextWidth = getDraggedWidth(event.clientX)
370
+ if (nextWidth === null) return
371
+ const collapsed = nextWidth < minThumbnailWidth
372
+ if (collapsed !== dragCollapsedRef.current) {
373
+ dragCollapsedRef.current = collapsed
374
+ setIsDragCollapsed(collapsed)
375
+ setIsAnimatingDragBoundary(true)
376
+ if (dragAnimationTimerRef.current) clearTimeout(dragAnimationTimerRef.current)
377
+ dragAnimationTimerRef.current = setTimeout(() => {
378
+ setIsAnimatingDragBoundary(false)
379
+ dragAnimationTimerRef.current = null
380
+ }, 200)
381
+ }
382
+ if (!collapsed) setThumbnailWidth(Math.min(maxThumbnailWidth, nextWidth))
383
+ }
384
+ const stopResize = (event?: PointerEvent) => {
385
+ if (event && event.pointerId !== resizeStartRef.current?.pointerId) return
386
+ const nextWidth = event ? getDraggedWidth(event.clientX) : null
387
+ const collapsed =
388
+ event?.type === "pointerup" && nextWidth !== null && nextWidth < minThumbnailWidth
389
+ resizeStartRef.current = null
390
+ dragCollapsedRef.current = false
391
+ document.body.style.userSelect = previousUserSelectRef.current
392
+ document.body.style.cursor = previousCursorRef.current
393
+ setIsResizingThumbnails(false)
394
+ setIsDragCollapsed(false)
395
+ if (collapsed) setThumbnailRailOpen(false)
396
+ else if (nextWidth !== null) {
397
+ setThumbnailWidth(Math.max(minThumbnailWidth, Math.min(maxThumbnailWidth, nextWidth)))
398
+ }
399
+ }
400
+ const handleBlur = () => stopResize()
401
+
402
+ window.addEventListener("pointermove", handlePointerMove)
403
+ window.addEventListener("pointerup", stopResize)
404
+ window.addEventListener("pointercancel", stopResize)
405
+ window.addEventListener("blur", handleBlur)
406
+ return () => {
407
+ window.removeEventListener("pointermove", handlePointerMove)
408
+ window.removeEventListener("pointerup", stopResize)
409
+ window.removeEventListener("pointercancel", stopResize)
410
+ window.removeEventListener("blur", handleBlur)
411
+ }
412
+ }, [isResizingThumbnails, minThumbnailWidth, maxThumbnailWidth, setThumbnailRailOpen])
111
413
 
112
414
  const {
113
415
  currentSlide,
@@ -140,6 +442,12 @@ export function SlideDeck({ slides, transition, directionalTransition, annotatio
140
442
  return () => document.removeEventListener("fullscreenchange", handleFullscreenChange)
141
443
  }, [])
142
444
 
445
+ useEffect(() => {
446
+ if (viewMode === "slide" && !isPresentationMode && showThumbnailRail) {
447
+ activeThumbnailRef.current?.scrollIntoView({ block: "nearest" })
448
+ }
449
+ }, [currentSlide, isPresentationMode, showThumbnailRail, viewMode])
450
+
143
451
  // Calculate scale factor for presentation mode
144
452
  useEffect(() => {
145
453
  const calculateScale = () => {
@@ -147,9 +455,9 @@ export function SlideDeck({ slides, transition, directionalTransition, annotatio
147
455
  setScale(1)
148
456
  return
149
457
  }
150
- const viewportWidth = window.innerWidth
458
+ const availableWidth = window.innerWidth
151
459
  const viewportHeight = window.innerHeight
152
- const scaleX = viewportWidth / SLIDE_DIMENSIONS.width
460
+ const scaleX = availableWidth / SLIDE_DIMENSIONS.width
153
461
  const scaleY = viewportHeight / SLIDE_DIMENSIONS.height
154
462
  setScale(Math.min(scaleX, scaleY))
155
463
  }
@@ -159,7 +467,50 @@ export function SlideDeck({ slides, transition, directionalTransition, annotatio
159
467
  return () => window.removeEventListener("resize", calculateScale)
160
468
  }, [isPresentationMode])
161
469
 
162
- const handleExportPdf = () => {
470
+ // Keep slide layout at its design size; only scale the rendered canvas.
471
+ useLayoutEffect(() => {
472
+ const viewport = previewViewportRef.current
473
+ const canvas = slideContainerRef.current
474
+ if (!viewport || !canvas) return
475
+ const resize = (width: number, height: number) => {
476
+ const fitScale = Math.min(width / SLIDE_DIMENSIONS.width, height / SLIDE_DIMENSIONS.height)
477
+ canvas.style.transform = `scale(${fitScale})`
478
+ }
479
+ resize(viewport.clientWidth, viewport.clientHeight)
480
+ const observer = new ResizeObserver(([entry]) => {
481
+ if (!entry) return
482
+ resize(entry.contentRect.width, entry.contentRect.height)
483
+ })
484
+ observer.observe(viewport)
485
+ return () => observer.disconnect()
486
+ }, [viewMode, isPresentationMode])
487
+
488
+ const toggleCommentMode = useCallback(() => {
489
+ const next = !isAnnotationMode
490
+ setIsAnnotationMode(next)
491
+ setShowAnnotationPanel(next)
492
+ if (!next) {
493
+ setSelectedAnnotationId(null)
494
+ setHoveredAnnotationId(null)
495
+ }
496
+ }, [isAnnotationMode])
497
+
498
+ const handleExportPdf = useCallback(async () => {
499
+ try {
500
+ const response = await fetch("/__promptslide_pdf")
501
+ if (response.ok && response.headers.get("content-type")?.includes("application/pdf")) {
502
+ const blob = await response.blob()
503
+ const url = URL.createObjectURL(blob)
504
+ const link = document.createElement("a")
505
+ link.href = url
506
+ link.download = "slides.pdf"
507
+ link.click()
508
+ setTimeout(() => URL.revokeObjectURL(url), 1000)
509
+ return
510
+ }
511
+ } catch {
512
+ /* Static builds use the browser print fallback. */
513
+ }
163
514
  const previousMode = viewMode
164
515
  setViewMode("list")
165
516
 
@@ -171,22 +522,60 @@ export function SlideDeck({ slides, transition, directionalTransition, annotatio
171
522
  window.addEventListener("afterprint", handleAfterPrint)
172
523
  window.print()
173
524
  }, 100)
174
- }
525
+ }, [viewMode])
175
526
 
176
527
  // Keyboard navigation
177
528
  useEffect(() => {
178
529
  const handleKeyDown = (e: KeyboardEvent) => {
179
- if (e.key === "f" || e.key === "F") {
180
- togglePresentationMode()
530
+ const target = e.target as HTMLElement | null
531
+ if (target?.closest("input, textarea, [contenteditable='true']")) return
532
+ if (e.altKey || e.ctrlKey || e.metaKey) return
533
+ const key = e.key.toLowerCase()
534
+ if (e.repeat && "fvglcdt".includes(key)) return
535
+ if (isEditorOpen && ["f", "v", "g", "l", "c", "d"].includes(key)) return
536
+
537
+ if (key === "f") {
538
+ e.preventDefault()
539
+ void togglePresentationMode()
540
+ return
541
+ }
542
+
543
+ if (key === "v" && !isPresentationMode) {
544
+ e.preventDefault()
545
+ setViewMode("slide")
181
546
  return
182
547
  }
183
548
 
184
- // G for grid view toggle
185
- if (e.key === "g" || e.key === "G") {
549
+ if (key === "g") {
550
+ e.preventDefault()
186
551
  setViewMode(prev => (prev === "grid" ? "slide" : "grid"))
187
552
  return
188
553
  }
189
554
 
555
+ if (key === "l") {
556
+ e.preventDefault()
557
+ setViewMode(prev => (prev === "list" ? "slide" : "list"))
558
+ return
559
+ }
560
+
561
+ if (key === "c" && !isPresentationMode) {
562
+ e.preventDefault()
563
+ toggleCommentMode()
564
+ return
565
+ }
566
+
567
+ if (key === "d" && !isPresentationMode) {
568
+ e.preventDefault()
569
+ void handleExportPdf()
570
+ return
571
+ }
572
+
573
+ if (viewMode === "slide" && !isPresentationMode && key === "t") {
574
+ e.preventDefault()
575
+ setThumbnailRailOpen(!showThumbnailRail)
576
+ return
577
+ }
578
+
190
579
  if (viewMode !== "slide" || (isAnnotationMode && !isPresentationMode)) return
191
580
 
192
581
  if (e.key === "ArrowRight" || e.key === " ") {
@@ -200,12 +589,51 @@ export function SlideDeck({ slides, transition, directionalTransition, annotatio
200
589
 
201
590
  window.addEventListener("keydown", handleKeyDown)
202
591
  return () => window.removeEventListener("keydown", handleKeyDown)
203
- }, [advance, goBack, viewMode, togglePresentationMode, isAnnotationMode, isPresentationMode])
592
+ }, [
593
+ advance,
594
+ goBack,
595
+ viewMode,
596
+ togglePresentationMode,
597
+ isAnnotationMode,
598
+ isEditorOpen,
599
+ isPresentationMode,
600
+ setThumbnailRailOpen,
601
+ showThumbnailRail,
602
+ toggleCommentMode,
603
+ handleExportPdf
604
+ ])
605
+
606
+ const editorPanelWidth = viewportWidth <= 850 ? 280 : 320
204
607
 
205
608
  return (
206
609
  <div className="min-h-screen w-full bg-neutral-950 text-foreground">
207
610
  <style>{`
611
+ .slide-thumbnail-scroll-area {
612
+ scrollbar-width: thin;
613
+ scrollbar-color: #404040 transparent;
614
+ }
615
+ .slide-thumbnail-scroll-area::-webkit-scrollbar {
616
+ width: 10px;
617
+ }
618
+ .slide-thumbnail-scroll-area::-webkit-scrollbar-track {
619
+ background: transparent;
620
+ }
621
+ .slide-thumbnail-scroll-area::-webkit-scrollbar-thumb {
622
+ background-color: #404040;
623
+ border: 3px solid transparent;
624
+ border-radius: 9999px;
625
+ background-clip: padding-box;
626
+ }
627
+ .slide-thumbnail-scroll-area::-webkit-scrollbar-thumb:hover {
628
+ background-color: #525252;
629
+ }
630
+ .slide-thumbnail-scroll-area::-webkit-scrollbar-corner {
631
+ background: transparent;
632
+ }
208
633
  @media print {
634
+ .list-slide-canvas {
635
+ transform: scale(1.5) !important;
636
+ }
209
637
  @page {
210
638
  size: 1920px 1080px;
211
639
  margin: 0;
@@ -228,10 +656,19 @@ export function SlideDeck({ slides, transition, directionalTransition, annotatio
228
656
 
229
657
  {/* Toolbar */}
230
658
  <div
659
+ data-ps-viewer-toolbar
660
+ style={
661
+ isEditorOpen
662
+ ? { right: editorPanelWidth + 16, maxWidth: `calc(100vw - ${editorPanelWidth + 32}px)` }
663
+ : undefined
664
+ }
231
665
  className={cn(
232
- "fixed top-4 z-50 flex gap-1 rounded-lg border border-neutral-800 bg-neutral-950/90 p-1 backdrop-blur-sm transition-[right] print:hidden",
233
- isPresentationMode && "hidden",
234
- isAnnotationMode && showAnnotationPanel ? "right-[19.5rem]" : "right-4"
666
+ "fixed top-4 z-50 flex gap-1 rounded-lg border border-neutral-800 bg-neutral-950/90 p-1 backdrop-blur-sm transition-[right] duration-200 ease-out print:hidden",
667
+ (isPresentationMode || (isNarrowViewport && isAnnotationMode && showAnnotationPanel)) &&
668
+ "hidden",
669
+ isEditorOpen && "flex-wrap",
670
+ isEditorOpen && viewportWidth <= 850 && "[&_button:disabled]:hidden [&>div]:hidden",
671
+ !isEditorOpen && (isAnnotationMode && showAnnotationPanel ? "right-[21rem]" : "right-4")
235
672
  )}
236
673
  >
237
674
  <button
@@ -240,7 +677,8 @@ export function SlideDeck({ slides, transition, directionalTransition, annotatio
240
677
  "rounded-md p-2 text-neutral-400 transition-colors hover:bg-neutral-800 hover:text-white",
241
678
  viewMode === "slide" && "bg-neutral-800 text-white"
242
679
  )}
243
- title="Presentation View"
680
+ disabled={isEditorOpen}
681
+ title="Presentation View (V)"
244
682
  >
245
683
  <Monitor className="h-4 w-4" />
246
684
  </button>
@@ -250,7 +688,8 @@ export function SlideDeck({ slides, transition, directionalTransition, annotatio
250
688
  "rounded-md p-2 text-neutral-400 transition-colors hover:bg-neutral-800 hover:text-white",
251
689
  viewMode === "list" && "bg-neutral-800 text-white"
252
690
  )}
253
- title="List View"
691
+ disabled={isEditorOpen}
692
+ title="List View (L)"
254
693
  >
255
694
  <List className="h-4 w-4" />
256
695
  </button>
@@ -260,7 +699,8 @@ export function SlideDeck({ slides, transition, directionalTransition, annotatio
260
699
  "rounded-md p-2 text-neutral-400 transition-colors hover:bg-neutral-800 hover:text-white",
261
700
  viewMode === "grid" && "bg-neutral-800 text-white"
262
701
  )}
263
- title="Grid View"
702
+ disabled={isEditorOpen}
703
+ title="Grid View (G)"
264
704
  >
265
705
  <Grid3X3 className="h-4 w-4" />
266
706
  </button>
@@ -268,21 +708,20 @@ export function SlideDeck({ slides, transition, directionalTransition, annotatio
268
708
  <div className="mx-1 w-px bg-neutral-800" />
269
709
 
270
710
  <button
271
- onClick={() => {
272
- setIsAnnotationMode(prev => {
273
- const next = !prev
274
- setShowAnnotationPanel(next)
275
- if (!next) setSelectedAnnotationId(null)
276
- return next
277
- })
278
- }}
711
+ onClick={toggleCommentMode}
279
712
  className={cn(
280
- "relative rounded-md p-2 text-neutral-400 transition-colors hover:bg-neutral-800 hover:text-white",
281
- isAnnotationMode && "bg-[#FF6B35] text-white hover:bg-[#FF7A4A]"
713
+ "relative inline-flex h-8 items-center justify-center gap-1.5 rounded-md font-[family-name:system-ui] text-[13px] leading-none font-medium transition-colors",
714
+ isAnnotationMode
715
+ ? "border border-[#FF6B35]/50 bg-[#FF6B35]/15 p-[7px] text-[#FF6B35] hover:border-[#FF6B35] hover:bg-[#FF6B35]/25"
716
+ : "px-2.5 text-neutral-400 hover:bg-neutral-800 hover:text-white"
282
717
  )}
283
- title="Annotate slides"
718
+ disabled={isEditorOpen}
719
+ aria-label="Comment"
720
+ aria-pressed={isAnnotationMode}
721
+ title="Comment (C)"
284
722
  >
285
- <MessageCircle className="h-4 w-4" />
723
+ <MessageCircle className="h-[15px] w-[15px] shrink-0" />
724
+ {!isAnnotationMode && <span>Comment</span>}
286
725
  {openCount > 0 && (
287
726
  <span className="absolute -top-1 -right-1 flex h-4 w-4 items-center justify-center rounded-full bg-[#FF6B35] text-[10px] font-bold text-white">
288
727
  {openCount}
@@ -290,38 +729,184 @@ export function SlideDeck({ slides, transition, directionalTransition, annotatio
290
729
  )}
291
730
  </button>
292
731
 
732
+ <span data-ps-editor-toolbar className="contents" />
733
+
293
734
  <div className="mx-1 w-px bg-neutral-800" />
294
735
 
295
736
  <button
296
737
  onClick={handleExportPdf}
297
738
  className="rounded-md p-2 text-neutral-400 transition-colors hover:bg-neutral-800 hover:text-white"
298
- title="Download PDF"
739
+ disabled={isEditorOpen}
740
+ title="Download PDF (D)"
299
741
  >
300
742
  <Download className="h-4 w-4" />
301
743
  </button>
302
744
  <button
303
745
  onClick={togglePresentationMode}
304
- className="rounded-md p-2 text-neutral-400 transition-colors hover:bg-neutral-800 hover:text-white"
746
+ className="inline-flex h-8 items-center justify-center gap-1.5 rounded-md border border-[#FF6B35]/50 bg-[#FF6B35]/15 px-3 text-sm font-semibold text-[#FF6B35] transition-colors hover:border-[#FF6B35] hover:bg-[#FF6B35]/25"
747
+ disabled={isEditorOpen}
305
748
  title="Present (F)"
306
749
  >
307
- <Maximize className="h-4 w-4" />
750
+ <Play className="h-3.5 w-3.5 fill-current" />
751
+ <span>Present</span>
308
752
  </button>
309
753
  </div>
310
754
 
311
755
  {/* Slide View */}
312
756
  {viewMode === "slide" && (
313
757
  <div
314
- className={cn(
315
- "flex h-screen w-full print:hidden",
316
- isPresentationMode ? "bg-black" : ""
317
- )}
758
+ className={cn("flex h-screen w-full print:hidden", isPresentationMode ? "bg-black" : "")}
318
759
  >
760
+ {!isPresentationMode && (
761
+ <>
762
+ <div
763
+ className={cn(
764
+ "relative z-10 h-screen shrink-0",
765
+ isNarrowViewport && "fixed inset-y-0 left-0",
766
+ isResizingThumbnails && !isAnimatingDragBoundary
767
+ ? "transition-none"
768
+ : "transition-[width] duration-200 ease-out motion-reduce:transition-none"
769
+ )}
770
+ style={{ width: isThumbnailRailVisible ? visibleThumbnailWidth : 0 }}
771
+ >
772
+ <div className="h-full w-full overflow-hidden">
773
+ <aside
774
+ id="presentation-thumbnails"
775
+ aria-label="Slide thumbnails"
776
+ aria-hidden={!isThumbnailRailVisible}
777
+ inert={!isThumbnailRailVisible}
778
+ className={cn(
779
+ "flex h-screen shrink-0 flex-col border-r border-neutral-800 bg-neutral-950 text-white transition-opacity motion-reduce:transition-none",
780
+ isThumbnailRailVisible
781
+ ? "opacity-100 delay-100 duration-100"
782
+ : "opacity-0 duration-75"
783
+ )}
784
+ style={{ width: visibleThumbnailWidth }}
785
+ >
786
+ <div className="flex items-center justify-between border-b border-neutral-800 px-3 py-2">
787
+ <span className="text-sm font-semibold">Slides ({slides.length})</span>
788
+ <button
789
+ type="button"
790
+ onClick={() => setThumbnailRailOpen(false)}
791
+ className="rounded p-2 text-neutral-400 hover:bg-neutral-800 hover:text-white"
792
+ aria-label="Hide slide thumbnails"
793
+ title="Hide thumbnails (T)"
794
+ >
795
+ <PanelLeftClose className="h-4 w-4" />
796
+ </button>
797
+ </div>
798
+ <div className="slide-thumbnail-scroll-area min-h-0 flex-1 space-y-3 overflow-y-auto p-3">
799
+ {slides.map((slideConfig, index) => (
800
+ <button
801
+ key={slideConfig.id ?? index}
802
+ ref={index === currentSlide ? activeThumbnailRef : undefined}
803
+ type="button"
804
+ onClick={() => goToSlide(index)}
805
+ aria-label={`Go to slide ${index + 1}${slideConfig.title ? `: ${slideConfig.title}` : ""}`}
806
+ aria-current={index === currentSlide ? "page" : undefined}
807
+ className={cn(
808
+ "block w-full rounded-lg border p-1.5 text-left transition-colors focus:outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#FF6B35]",
809
+ index === currentSlide
810
+ ? "border-[#FF6B35] bg-[#FF6B35]/15 ring-1 ring-[#FF6B35]"
811
+ : "border-transparent hover:border-neutral-600 hover:bg-neutral-800"
812
+ )}
813
+ >
814
+ <div className="relative aspect-video w-full overflow-hidden rounded bg-black">
815
+ <GridThumbnail
816
+ slide={slideConfig}
817
+ index={index}
818
+ total={slides.length}
819
+ />
820
+ </div>
821
+ <div className="mt-1.5 flex items-baseline gap-2 px-0.5 text-xs">
822
+ {slideConfig.title ? (
823
+ <>
824
+ <span className="font-mono text-neutral-400">{index + 1}</span>
825
+ <span className="truncate">{slideConfig.title}</span>
826
+ </>
827
+ ) : (
828
+ <span>Slide {index + 1}</span>
829
+ )}
830
+ </div>
831
+ </button>
832
+ ))}
833
+ </div>
834
+ </aside>
835
+ </div>
836
+ {isThumbnailRailVisible && (
837
+ <div
838
+ role="separator"
839
+ aria-label="Resize slide thumbnails"
840
+ aria-orientation="vertical"
841
+ aria-valuemin={minThumbnailWidth}
842
+ aria-valuemax={maxThumbnailWidth}
843
+ aria-valuenow={visibleThumbnailWidth}
844
+ tabIndex={0}
845
+ className={cn(
846
+ "absolute inset-y-0 -right-1 w-2 cursor-col-resize touch-none focus-visible:outline-none after:pointer-events-none after:absolute after:inset-y-0 after:right-1 after:w-0.5 after:bg-transparent after:content-[''] after:transition-colors hover:after:bg-[#FF6B35]/50 focus-visible:after:bg-[#FF6B35]/50",
847
+ isResizingThumbnails && "after:bg-[#FF6B35]/60"
848
+ )}
849
+ onPointerDown={event => {
850
+ if (event.button !== 0) return
851
+ event.preventDefault()
852
+ resizeStartRef.current = {
853
+ pointerId: event.pointerId,
854
+ pointerX: event.clientX,
855
+ width: visibleThumbnailWidth
856
+ }
857
+ dragCollapsedRef.current = false
858
+ setIsAnimatingDragBoundary(false)
859
+ if (dragAnimationTimerRef.current) clearTimeout(dragAnimationTimerRef.current)
860
+ setIsResizingThumbnails(true)
861
+ previousUserSelectRef.current = document.body.style.userSelect
862
+ previousCursorRef.current = document.body.style.cursor
863
+ document.body.style.userSelect = "none"
864
+ document.body.style.cursor = "col-resize"
865
+ }}
866
+ onKeyDown={event => {
867
+ if (event.key === "ArrowLeft" || event.key === "ArrowRight") {
868
+ event.preventDefault()
869
+ event.stopPropagation()
870
+ const amount = event.key === "ArrowRight" ? 16 : -16
871
+ setThumbnailWidth(
872
+ Math.max(
873
+ minThumbnailWidth,
874
+ Math.min(maxThumbnailWidth, visibleThumbnailWidth + amount)
875
+ )
876
+ )
877
+ }
878
+ }}
879
+ />
880
+ )}
881
+ </div>
882
+ <div
883
+ aria-hidden={isThumbnailRailVisible || isResizingThumbnails}
884
+ inert={isThumbnailRailVisible || isResizingThumbnails}
885
+ className={cn(
886
+ "fixed top-4 left-4 z-50 rounded-lg border border-neutral-800 bg-neutral-950/90 backdrop-blur-sm transition-opacity duration-150 motion-reduce:transition-none",
887
+ isThumbnailRailVisible || isResizingThumbnails
888
+ ? "pointer-events-none opacity-0"
889
+ : "opacity-100 delay-200"
890
+ )}
891
+ >
892
+ <button
893
+ type="button"
894
+ onClick={() => setThumbnailRailOpen(true)}
895
+ className="rounded-md p-2 text-neutral-400 transition-colors hover:bg-neutral-800 hover:text-white"
896
+ aria-label="Show slide thumbnails"
897
+ title="Show thumbnails (T)"
898
+ >
899
+ <PanelLeftOpen className="h-4 w-4" />
900
+ </button>
901
+ </div>
902
+ </>
903
+ )}
319
904
  <div
320
905
  ref={containerRef}
321
906
  role="presentation"
322
907
  tabIndex={isPresentationMode ? 0 : undefined}
323
908
  className={cn(
324
- "flex flex-1 flex-col items-center justify-center overflow-hidden",
909
+ "flex min-w-0 flex-1 flex-col items-center justify-center overflow-hidden",
325
910
  isPresentationMode ? "bg-black p-0" : "p-4 md:p-8"
326
911
  )}
327
912
  onClick={isPresentationMode ? advance : undefined}
@@ -357,31 +942,52 @@ export function SlideDeck({ slides, transition, directionalTransition, annotatio
357
942
  />
358
943
  </div>
359
944
  ) : (
360
- <div ref={slideContainerRef} className="relative aspect-video w-full max-w-7xl overflow-hidden rounded-xl border border-neutral-800 bg-black shadow-2xl">
361
- <SlideRenderer
362
- slides={slides}
363
- currentSlide={currentSlide}
364
- animationStep={animationStep}
365
- totalSteps={totalSteps}
366
- direction={direction}
367
- showAllAnimations={showAllAnimations}
368
- transition={transition}
369
- directionalTransition={directionalTransition}
370
- onTransitionComplete={onTransitionComplete}
371
- />
372
- {isAnnotationMode && (
373
- <AnnotationOverlay
945
+ <div
946
+ ref={previewViewportRef}
947
+ className="relative aspect-video w-full max-w-7xl overflow-hidden rounded-xl border border-neutral-800 bg-black shadow-2xl"
948
+ >
949
+ <div
950
+ ref={slideContainerRef}
951
+ data-ps-editor-canvas
952
+ tabIndex={-1}
953
+ data-slide-index={currentSlide}
954
+ className="absolute top-0 left-0 overflow-hidden outline-none"
955
+ style={{
956
+ width: SLIDE_DIMENSIONS.width,
957
+ height: SLIDE_DIMENSIONS.height,
958
+ transformOrigin: "top left"
959
+ }}
960
+ >
961
+ <SlideRenderer
962
+ key={editorRevision}
374
963
  slides={slides}
375
964
  currentSlide={currentSlide}
376
- slideContainerRef={slideContainerRef}
377
- selectedId={selectedAnnotationId}
378
- onSelectId={setSelectedAnnotationId}
379
- onShowPanel={() => setShowAnnotationPanel(true)}
380
- slideAnnotations={getSlideAnnotations(currentSlide)}
381
- addAnnotation={effectiveAdd ?? (() => {})}
382
-
965
+ animationStep={animationStep}
966
+ totalSteps={totalSteps}
967
+ direction={direction}
968
+ showAllAnimations={showAllAnimations}
969
+ transition={transition}
970
+ directionalTransition={directionalTransition}
971
+ onTransitionComplete={onTransitionComplete}
383
972
  />
384
- )}
973
+ {isAnnotationMode && (
974
+ <AnnotationOverlay
975
+ slides={slides}
976
+ currentSlide={currentSlide}
977
+ slideContainerRef={slideContainerRef}
978
+ selectedId={selectedAnnotationId}
979
+ hoveredId={hoveredAnnotationId}
980
+ onSelectId={setSelectedAnnotationId}
981
+ onShowPanel={() => setShowAnnotationPanel(true)}
982
+ slideAnnotations={getSlideAnnotations(currentSlide)}
983
+ addAnnotation={effectiveAdd ?? (() => {})}
984
+ />
985
+ )}
986
+ </div>
987
+ <div
988
+ data-ps-editor-overlay
989
+ className="pointer-events-none absolute inset-0 z-20"
990
+ />
385
991
  </div>
386
992
  )}
387
993
  </LayoutGroup>
@@ -392,6 +998,7 @@ export function SlideDeck({ slides, transition, directionalTransition, annotatio
392
998
  <button
393
999
  onClick={goBack}
394
1000
  className="rounded-full border border-neutral-800 bg-black/50 p-2 text-neutral-400 backdrop-blur-sm transition-colors hover:bg-neutral-900 hover:text-white"
1001
+ title="Previous step or slide (←)"
395
1002
  >
396
1003
  <ChevronLeft className="h-5 w-5" />
397
1004
  </button>
@@ -403,6 +1010,7 @@ export function SlideDeck({ slides, transition, directionalTransition, annotatio
403
1010
  <button
404
1011
  onClick={advance}
405
1012
  className="rounded-full border border-neutral-800 bg-black/50 p-2 text-neutral-400 backdrop-blur-sm transition-colors hover:bg-neutral-900 hover:text-white"
1013
+ title="Next step or slide (→ / Space)"
406
1014
  >
407
1015
  <ChevronRight className="h-5 w-5" />
408
1016
  </button>
@@ -410,18 +1018,61 @@ export function SlideDeck({ slides, transition, directionalTransition, annotatio
410
1018
  )}
411
1019
  </div>
412
1020
 
1021
+ <div
1022
+ data-ps-editor-panel
1023
+ data-open={isEditorOpen}
1024
+ aria-hidden={!isEditorOpen}
1025
+ inert={!isEditorOpen}
1026
+ className="h-full shrink-0 overflow-hidden transition-[width] duration-200 ease-out motion-reduce:transition-none"
1027
+ style={{ width: isEditorOpen ? editorPanelWidth : 0 }}
1028
+ />
1029
+
413
1030
  {/* Annotation Panel — beside the slide */}
414
- {isAnnotationMode && showAnnotationPanel && !isPresentationMode && (
415
- <AnnotationPanel
416
- annotations={getSlideAnnotations(currentSlide)}
417
- selectedId={selectedAnnotationId}
418
- onSelect={setSelectedAnnotationId}
419
- onDelete={effectiveDelete ?? (() => {})}
420
- onClose={() => {
421
- setShowAnnotationPanel(false)
422
- setSelectedAnnotationId(null)
1031
+ {!isPresentationMode && (
1032
+ <div
1033
+ aria-hidden={!isAnnotationMode || !showAnnotationPanel}
1034
+ inert={!isAnnotationMode || !showAnnotationPanel}
1035
+ className={cn(
1036
+ "h-full shrink-0 overflow-hidden transition-[width,transform] duration-200 ease-out motion-reduce:transition-none",
1037
+ isNarrowViewport && "fixed inset-y-0 right-0 z-50 shadow-xl",
1038
+ isNarrowViewport &&
1039
+ (!isAnnotationMode || !showAnnotationPanel) &&
1040
+ "translate-x-full"
1041
+ )}
1042
+ style={{
1043
+ width:
1044
+ isAnnotationMode && showAnnotationPanel
1045
+ ? isNarrowViewport
1046
+ ? "min(20rem, 90vw)"
1047
+ : "20rem"
1048
+ : 0
423
1049
  }}
424
- />
1050
+ >
1051
+ <div
1052
+ className={cn(
1053
+ "h-full transition-opacity motion-reduce:transition-none",
1054
+ isAnnotationMode && showAnnotationPanel
1055
+ ? "opacity-100 delay-100 duration-100"
1056
+ : "opacity-0 duration-75"
1057
+ )}
1058
+ >
1059
+ <AnnotationPanel
1060
+ annotations={getSlideAnnotations(currentSlide)}
1061
+ unlinkedAnnotations={unlinkedAnnotations}
1062
+ selectedId={selectedAnnotationId}
1063
+ onSelect={setSelectedAnnotationId}
1064
+ onHover={setHoveredAnnotationId}
1065
+ onDelete={effectiveDelete ?? (() => {})}
1066
+ onUpdate={effectiveUpdate ?? (() => {})}
1067
+ onClose={() => {
1068
+ setIsAnnotationMode(false)
1069
+ setShowAnnotationPanel(false)
1070
+ setSelectedAnnotationId(null)
1071
+ setHoveredAnnotationId(null)
1072
+ }}
1073
+ />
1074
+ </div>
1075
+ </div>
425
1076
  )}
426
1077
  </div>
427
1078
  )}
@@ -431,14 +1082,13 @@ export function SlideDeck({ slides, transition, directionalTransition, annotatio
431
1082
  <div className="mx-auto max-w-7xl p-8 pt-16 print:hidden">
432
1083
  <div className="grid grid-cols-2 gap-4 md:grid-cols-3 lg:grid-cols-4">
433
1084
  {slides.map((slideConfig, index) => {
434
- const SlideComponent = slideConfig.component
435
1085
  const prevSection = index > 0 ? slides[index - 1]?.section : undefined
436
1086
  const showSectionHeader = slideConfig.section && slideConfig.section !== prevSection
437
1087
 
438
1088
  return (
439
- <div key={index} className={showSectionHeader ? "col-span-full" : undefined}>
1089
+ <div key={slideConfig.id ?? index} className="contents">
440
1090
  {showSectionHeader && (
441
- <h3 className="mt-4 mb-3 text-xs font-bold tracking-[0.2em] text-neutral-500 uppercase first:mt-0">
1091
+ <h3 className="col-span-full mt-4 mb-0 text-xs font-bold tracking-[0.2em] text-neutral-500 uppercase first:mt-0">
442
1092
  {slideConfig.section}
443
1093
  </h3>
444
1094
  )}
@@ -447,22 +1097,9 @@ export function SlideDeck({ slides, transition, directionalTransition, annotatio
447
1097
  goToSlide(index)
448
1098
  setViewMode("slide")
449
1099
  }}
450
- className="group relative aspect-video w-full overflow-hidden rounded-lg border border-neutral-800 bg-black shadow-sm transition-all hover:border-primary hover:shadow-lg hover:shadow-primary/10"
1100
+ className="group relative aspect-video w-full overflow-hidden rounded-lg border border-neutral-800 bg-black shadow-sm transition-all hover:border-[#FF6B35] hover:shadow-lg hover:shadow-[#FF6B35]/10"
451
1101
  >
452
- <div
453
- className="h-full w-full origin-top-left scale-[0.25]"
454
- style={{ width: "400%", height: "400%" }}
455
- >
456
- <AnimationProvider
457
- currentStep={slideConfig.steps}
458
- totalSteps={slideConfig.steps}
459
- showAllAnimations={true}
460
- >
461
- <SlideErrorBoundary slideIndex={index} slideTitle={slideConfig.title}>
462
- <SlideComponent slideNumber={index + 1} totalSlides={slides.length} />
463
- </SlideErrorBoundary>
464
- </AnimationProvider>
465
- </div>
1102
+ <GridThumbnail slide={slideConfig} index={index} total={slides.length} />
466
1103
  <div className="absolute inset-0 bg-black/0 transition-colors group-hover:bg-black/20" />
467
1104
  <div className="absolute bottom-2 left-2 rounded bg-black/70 px-2 py-1 text-xs font-medium text-white">
468
1105
  {slideConfig.title ? `${index + 1}. ${slideConfig.title}` : index + 1}
@@ -484,27 +1121,14 @@ export function SlideDeck({ slides, transition, directionalTransition, annotatio
484
1121
  )}
485
1122
  >
486
1123
  <div className="grid grid-cols-1 gap-8 print:block">
487
- {slides.map((slideConfig, index) => {
488
- const SlideComponent = slideConfig.component
489
- return (
490
- <div
491
- key={index}
492
- className="aspect-video w-full overflow-hidden rounded-xl border border-neutral-800 bg-black shadow-sm print:relative print:m-0 print:h-[1080px] print:w-[1920px] print:break-after-page print:overflow-hidden print:rounded-none print:border-0 print:shadow-none"
493
- >
494
- <div className="h-full w-full print:h-[720px] print:w-[1280px] print:origin-top-left print:scale-[1.5]">
495
- <AnimationProvider
496
- currentStep={slideConfig.steps}
497
- totalSteps={slideConfig.steps}
498
- showAllAnimations={true}
499
- >
500
- <SlideErrorBoundary slideIndex={index} slideTitle={slideConfig.title}>
501
- <SlideComponent slideNumber={index + 1} totalSlides={slides.length} />
502
- </SlideErrorBoundary>
503
- </AnimationProvider>
504
- </div>
505
- </div>
506
- )
507
- })}
1124
+ {slides.map((slideConfig, index) => (
1125
+ <ListSlide
1126
+ key={slideConfig.id ?? index}
1127
+ slide={slideConfig}
1128
+ index={index}
1129
+ total={slides.length}
1130
+ />
1131
+ ))}
508
1132
  </div>
509
1133
  </div>
510
1134
  </div>