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.
- package/CHANGELOG.md +14 -0
- package/README.md +16 -12
- package/dist/index.d.ts +17 -7
- package/dist/index.js +1072 -225
- package/dist/index.js.map +1 -1
- package/package.json +31 -31
- package/src/commands/clone.mjs +54 -21
- package/src/commands/pull.mjs +57 -27
- package/src/commands/studio.mjs +7 -3
- package/src/commands/to-image.mjs +18 -5
- package/src/commands/to-pdf.mjs +27 -0
- package/src/core/animated.tsx +3 -3
- package/src/core/annotations/adapters/http.ts +8 -0
- package/src/core/annotations/annotation-overlay.tsx +77 -32
- package/src/core/annotations/annotation-panel.tsx +184 -51
- package/src/core/annotations/types.ts +5 -0
- package/src/core/annotations/use-annotations.ts +57 -7
- package/src/core/slide-deck.tsx +740 -116
- package/src/core/slide-renderer.tsx +3 -8
- package/src/core/transitions.ts +5 -3
- package/src/core/types.ts +2 -0
- package/src/core/use-slide-navigation.ts +93 -3
- package/src/editor/README.md +35 -0
- package/src/editor/geometry.ts +76 -0
- package/src/editor/mount.ts +24 -0
- package/src/editor/plugin.mjs +189 -0
- package/src/editor/source.mjs +363 -0
- package/src/editor/studio.css +1004 -0
- package/src/editor/studio.tsx +1948 -0
- package/src/index.mjs +6 -0
- package/src/utils/deck-config.mjs +30 -11
- package/src/utils/export.mjs +151 -52
- package/src/utils/studio-discovery.mjs +37 -0
- package/src/vite/config.mjs +23 -2
- package/src/vite/plugin.mjs +147 -16
- package/templates/default/AGENTS.md +3 -2
- package/templates/default/src/deck-config.ts +2 -2
package/src/core/slide-deck.tsx
CHANGED
|
@@ -1,14 +1,25 @@
|
|
|
1
1
|
import { LayoutGroup } from "framer-motion"
|
|
2
|
-
import {
|
|
3
|
-
|
|
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?: (
|
|
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
|
-
|
|
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({
|
|
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(
|
|
98
|
-
|
|
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
|
|
458
|
+
const availableWidth = window.innerWidth
|
|
151
459
|
const viewportHeight = window.innerHeight
|
|
152
|
-
const scaleX =
|
|
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
|
-
|
|
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
|
-
|
|
180
|
-
|
|
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
|
-
|
|
185
|
-
|
|
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
|
-
}, [
|
|
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 &&
|
|
234
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
281
|
-
isAnnotationMode
|
|
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
|
-
|
|
718
|
+
disabled={isEditorOpen}
|
|
719
|
+
aria-label="Comment"
|
|
720
|
+
aria-pressed={isAnnotationMode}
|
|
721
|
+
title="Comment (C)"
|
|
284
722
|
>
|
|
285
|
-
<MessageCircle className="h-
|
|
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
|
-
|
|
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
|
|
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
|
-
<
|
|
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
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
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
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
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
|
-
{
|
|
415
|
-
<
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
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=
|
|
1089
|
+
<div key={slideConfig.id ?? index} className="contents">
|
|
440
1090
|
{showSectionHeader && (
|
|
441
|
-
<h3 className="mt-4 mb-
|
|
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-
|
|
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
|
-
<
|
|
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
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
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>
|